mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 22:42:01 +00:00
合入集成分支(阶段 2A 数据剖析、4B 剖析与 SQL 检查界面及修正)到 4A:两边功能都保留
冲突的解法: - 后端 api/catalog.py:同时导入 catalog_impact 和 catalog_profile、SnapshotTampered;data/catalog.py:__all__ 取两边的并集重排。 - 2A 放宽了码值校验(含义可以为空,剖析写进来的是「含义待填写」):目录修改提案的码值补充改为含义为空的码不覆盖已有含义、 只在原来没有这个码时加进去,补了测试;卡片和过程里的码值把空含义写成「含义待填写」,和目录页同一个说法。 - 前端 types.ts、terms.ts:两边各在同一处追加了新段落,按顺序都留下(提案、影响面、目录版本在前,剖析、码值、SQL 检查在后), 没有重复的键;client.ts、decode.ts、CatalogPage.tsx、TableDetail.tsx 的导入取并集。 - check-catalog.mjs:影响面和剖析、保存数据源三条假路由都留下,各节和最后核对真库数量的逻辑不变。 - 4A 的测试数据源改用 tests/fixtures/sources.py 的 drop_source 删除,顺带作废连接池。
This commit is contained in:
@@ -14,13 +14,16 @@ import { splitShrunk } from '../canvas/copilotMerge'
|
||||
import { confirmDialog, IconButton, StatusBadge, toast } from '../components/ui'
|
||||
import { formatCost, formatLapse, formatOffset, formatSpan, formatTokens, shortId, NONE } from '../lib/format'
|
||||
import { statusLabel } from '../lib/status'
|
||||
import { runClassLabel } from '../lib/terms'
|
||||
import { runClassLabel, SQL_CHECK_TEXT } from '../lib/terms'
|
||||
import { sortSqlChecks } from '../lib/sqlcheck'
|
||||
import type { RunUsage, ToolChange } from '../types'
|
||||
import { AssistantStream, type StreamTurn } from './AssistantStream'
|
||||
import { Composer } from './Composer'
|
||||
import { SqlCheckBody } from './SqlChecks'
|
||||
import { EvidenceHostContext, type EvidenceHost } from './evidenceHost'
|
||||
import {
|
||||
COPILOT_PHASE_TEXT, copilotOutcome, decodeCopilot, decodeRun, exitLabels, issueLine, SELF_CHECK_ROUNDS, type CopilotIssue, type CopilotOutcome,
|
||||
COPILOT_PHASE_TEXT, copilotOutcome, decodeCopilot, decodeRun, exitLabels, issueLine, issueSqlCheck, SELF_CHECK_ROUNDS, type CopilotIssue,
|
||||
type CopilotOutcome,
|
||||
} from './decode'
|
||||
import { catalogPatchesOf } from './catalogPatch'
|
||||
import { ApprovalCard } from './RunPanel'
|
||||
@@ -289,11 +292,13 @@ function ChatView({ hasRun, onOpenRun }: { hasRun: boolean; onOpenRun: () => voi
|
||||
)
|
||||
}
|
||||
const issues = out.check?.status === 'failed' ? out.check.issues : []
|
||||
// 对照数据目录的 SQL 检查:不挡运行的提醒、提示(错误交回模型改过,没改好的在上面那组)
|
||||
const sqlNotes = sortSqlChecks(out.sqlChecks)
|
||||
const { unasked, asked } = toolLossOf(out)
|
||||
// 画布一处没变(unchanged)就没有可撤的:给了按钮,撤掉的是这一轮之前的那一步
|
||||
const undo = source.outcome === 'applied'
|
||||
|| (out.kind === 'built' && source.outcome !== 'reverted' && source.outcome !== 'unchanged')
|
||||
if (!issues.length && !out.skipped.length && !unasked.length && !asked.length) {
|
||||
if (!issues.length && !out.skipped.length && !unasked.length && !asked.length && !sqlNotes.length) {
|
||||
return undo ? (
|
||||
<button type="button" className="btn btn-xs btn-ghost" onClick={() => undoCopilotTurn(source.id)}>
|
||||
<Undo2 size={11} aria-hidden /> 撤销这次生成
|
||||
@@ -306,6 +311,10 @@ function ChatView({ hasRun, onOpenRun }: { hasRun: boolean; onOpenRun: () => voi
|
||||
<div className="w-full space-y-1.5">
|
||||
{/* 没修好的问题直接摊开,不折叠:这是此刻唯一要做的事 */}
|
||||
{issues.length > 0 && <IssueList issues={issues} onLocate={locateIssue} />}
|
||||
{sqlNotes.length > 0 && (
|
||||
<IssueList issues={sqlNotes} onLocate={locateIssue} title={SQL_CHECK_TEXT.title}
|
||||
plain={sqlNotes.every((x) => x.level === 'info')} />
|
||||
)}
|
||||
{(unasked.length > 0 || asked.length > 0) && <ToolLossList unasked={unasked} asked={asked} onLocate={locateTools} />}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{repairable && (
|
||||
@@ -380,28 +389,40 @@ function ChatView({ hasRun, onOpenRun }: { hasRun: boolean; onOpenRun: () => voi
|
||||
)
|
||||
}
|
||||
|
||||
/** 自查没修好的问题,逐条带「定位」。问题落在哪个节点上认得出来才给;写节点名,认不出才写 id */
|
||||
function IssueList({ issues, onLocate }: { issues: CopilotIssue[]; onLocate: (x: CopilotIssue) => void }) {
|
||||
/**
|
||||
* 自查没修好的问题,逐条带「定位」。问题落在哪个节点上认得出来才给;写节点名,认不出才写 id。
|
||||
* 对照数据目录的 SQL 检查另画级别、中文规则名、涉及的表和列、SQL 片段(和证据面板同一套),「定位」落到 SQL 那一项。
|
||||
* title 有值时是不挡运行的那一组(SQL 检查的提醒、提示):全是提示时不用提醒色
|
||||
*/
|
||||
function IssueList({ issues, onLocate, title, plain = false }: {
|
||||
issues: CopilotIssue[]; onLocate: (x: CopilotIssue) => void; title?: string; plain?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ul data-issue-list="" className="space-y-0.5 rounded border px-2 py-1.5 text-2xs leading-relaxed"
|
||||
style={{ borderColor: 'color-mix(in srgb, var(--st-waiting) 45%, var(--border))',
|
||||
background: 'var(--st-waiting-soft)' }}>
|
||||
{issues.map((x, i) => (
|
||||
<li key={i} className="flex items-start gap-1.5" data-issue-node={x.nodeId} data-issue-field={x.field}>
|
||||
<span className="min-w-0 flex-1 [overflow-wrap:anywhere]" title={issueLine(x)}>
|
||||
{x.nodeId && <span className={clsx('text-dim', !x.label && 'mono')}>「{x.label ?? x.nodeId}」</span>}{x.message}
|
||||
</span>
|
||||
{x.nodeId && (
|
||||
<button type="button"
|
||||
className="inline-flex shrink-0 items-center gap-0.5 rounded px-1 text-dim transition-colors hover:bg-hover hover:text-fg"
|
||||
title={x.field ? '打开该节点的设置,定位到出问题的配置项' : '在画布上定位这个节点'}
|
||||
onClick={() => onLocate(x)}>
|
||||
<Crosshair size={10} aria-hidden /> 定位
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div data-issue-list={title ? 'sql' : ''} className="rounded border px-2 py-1.5 text-2xs leading-relaxed"
|
||||
style={plain ? undefined : { borderColor: 'color-mix(in srgb, var(--st-waiting) 45%, var(--border))', background: 'var(--st-waiting-soft)' }}>
|
||||
{title && <div className="mb-1 font-medium text-fg">{title}</div>}
|
||||
<ul className="space-y-1">
|
||||
{issues.map((x, i) => {
|
||||
const sql = issueSqlCheck(x)
|
||||
return (
|
||||
<li key={i} className="flex items-start gap-1.5" data-issue-node={x.nodeId} data-issue-field={x.field} data-issue-code={x.code}>
|
||||
<div className="min-w-0 flex-1 [overflow-wrap:anywhere]" title={issueLine(x)}>
|
||||
{x.nodeId && <span className={clsx('text-dim', !x.label && 'mono')}>「{x.label ?? x.nodeId}」</span>}
|
||||
{sql ? <div className="mt-0.5"><SqlCheckBody check={sql} /></div> : x.message}
|
||||
</div>
|
||||
{x.nodeId && (
|
||||
<button type="button"
|
||||
className="inline-flex shrink-0 items-center gap-0.5 rounded px-1 text-dim transition-colors hover:bg-hover hover:text-fg"
|
||||
title={x.field ? '打开该节点的设置,定位到出问题的配置项' : '在画布上定位这个节点'}
|
||||
onClick={() => onLocate(x)}>
|
||||
<Crosshair size={10} aria-hidden /> 定位
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CatalogImpactList } from '../components/CatalogImpact'
|
||||
import { CATALOG_PATCH_TEXT as T, CATALOG_STATUS_LABEL } from '../lib/terms'
|
||||
import type { CatalogPatchChange } from '../types'
|
||||
import {
|
||||
catalogTableHref, patchSubmit, patchTarget, patchValueText, patchWhere, type CatalogPatch,
|
||||
catalogTableHref, codeMeaning, patchSubmit, patchTarget, patchValueText, patchWhere, type CatalogPatch,
|
||||
} from './catalogPatch'
|
||||
|
||||
// ===========================================================================
|
||||
@@ -211,8 +211,9 @@ function CodesAfter({ before, after }: { before: unknown; after: unknown }) {
|
||||
{entries.map(([k, v], i) => (
|
||||
<span key={k}>
|
||||
{i > 0 && '、'}
|
||||
<span className={clsx(prev[k] !== v && 'font-medium text-[var(--accent)]')} data-code-changed={prev[k] !== v ? k : undefined}>
|
||||
{k}={v}
|
||||
<span className={clsx(prev[k] !== v && 'font-medium text-[var(--accent)]', !String(v ?? '').trim() && 'text-faint')}
|
||||
data-code-changed={prev[k] !== v ? k : undefined}>
|
||||
{k}={codeMeaning(v)}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '../lib/evidence'
|
||||
import { humanizeError } from '../lib/errors'
|
||||
import { formatDateTime, formatNumber, NONE, shortId } from '../lib/format'
|
||||
import { EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, NODE_TYPE_LABEL, nodeTypeLabel } from '../lib/terms'
|
||||
import { EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, NODE_TYPE_LABEL, SQL_CHECK_TEXT, nodeTypeLabel } from '../lib/terms'
|
||||
import { useCatalog } from '../store/catalog'
|
||||
import { askKey, segmentKey, useEvidence, useExplore, useVerdicts } from '../store/evidence'
|
||||
import type {
|
||||
@@ -29,6 +29,7 @@ import { ArtifactViewer, ResultTable } from './AssistantStream'
|
||||
import { useEvidenceHost } from './evidenceHost'
|
||||
import { EvidenceProvenance } from './EvidenceProvenance'
|
||||
import { CopyChip } from './Markdown'
|
||||
import { SqlCheckList } from './SqlChecks'
|
||||
|
||||
/**
|
||||
* 证据面板:点开报告里的一个片段,看它从哪来。
|
||||
@@ -1109,6 +1110,9 @@ function MetricPart({ step, entry, inputs, runId, seal, pending, failed, resolve
|
||||
// 拿截断的查询结果整组算出来的:值是真的算出来的,但只算到了取回的那部分,出具已按缺口降档
|
||||
const incomplete = step?.incomplete === true || entry?.incomplete === true
|
||||
const incompleteReason = step?.incomplete_reason || entry?.incomplete_reason || EVIDENCE_TEXT.incompleteFallback
|
||||
// 所依据的查询对照数据目录查出了错误级的问题:值照算,结果不可靠,出具同样按缺口降档。问题和改法在查询步骤里
|
||||
const sqlFailed = step?.sql_check_failed === true || entry?.sql_check_failed === true
|
||||
const sqlReason = step?.sql_check_reason || entry?.sql_check_reason || EVIDENCE_TEXT.sqlCheckFallback
|
||||
|
||||
return (
|
||||
<Part title={EVIDENCE_TEXT.metric} data-ev-metric="">
|
||||
@@ -1121,6 +1125,12 @@ function MetricPart({ step, entry, inputs, runId, seal, pending, failed, resolve
|
||||
{EVIDENCE_TEXT.incomplete}
|
||||
</span>
|
||||
)}
|
||||
{sqlFailed && (
|
||||
<span className="chip" data-ev-sql-check-failed="" title={sqlReason}
|
||||
style={{ color: 'var(--st-waiting)', borderColor: 'var(--st-waiting)' }}>
|
||||
{EVIDENCE_TEXT.sqlCheckFailed}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(caliber || version || artifact || from) && (
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 text-2xs text-dim">
|
||||
@@ -1139,6 +1149,9 @@ function MetricPart({ step, entry, inputs, runId, seal, pending, failed, resolve
|
||||
{incomplete && (
|
||||
<p className="mt-1" style={{ color: 'var(--st-waiting)' }} data-ev-incomplete-reason="">{incompleteReason}</p>
|
||||
)}
|
||||
{sqlFailed && (
|
||||
<p className="mt-1" style={{ color: 'var(--st-waiting)' }} data-ev-sql-check-reason="">{sqlReason}</p>
|
||||
)}
|
||||
{unshowable && (
|
||||
<p className="mt-1" style={{ color: 'var(--st-waiting)' }} data-ev-unshowable="">
|
||||
{EVIDENCE_TEXT.unshowable(valueText(value))}
|
||||
@@ -1366,6 +1379,13 @@ function QueryPart({ id, step, seal, masked }: { id: string; step: EvidenceStep;
|
||||
{step.sql}
|
||||
</pre>
|
||||
)}
|
||||
{!!step.checks?.length && (
|
||||
// 这次查询对照数据目录查出的问题:说明、涉及的表和列、SQL 片段。没查出问题时接口不给这个键
|
||||
<div className="mb-1.5" data-ev-sql-checks="">
|
||||
<div className="mb-0.5 text-2xs text-faint">{SQL_CHECK_TEXT.title}</div>
|
||||
<SqlCheckList checks={step.checks} />
|
||||
</div>
|
||||
)}
|
||||
{!!step.merge?.warnings.length && (
|
||||
<div className="mb-1.5 text-2xs" data-ev-merge-warnings="">
|
||||
<div className="mb-0.5 text-faint">{MERGE_TEXT.executionWarnings}</div>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Info, OctagonX, TriangleAlert } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import type { SqlCheckItem, SqlCheckLevel } from '../types'
|
||||
import { SQL_CHECK_LEVEL_HINT, SQL_CHECK_LEVEL_LABEL, SQL_CHECK_RULE_HINT, SQL_CHECK_TEXT } from '../lib/terms'
|
||||
import { SQL_CHECK_TONE, isSqlCheckCode, sortSqlChecks, sqlCheckLevelOf, sqlCheckWhere, sqlRuleLabel } from '../lib/sqlcheck'
|
||||
|
||||
// ===========================================================================
|
||||
// SQL 检查的结果:级别标识(错误、提醒、提示,四通道:图标、文字、颜色、边框)、中文规则名、给人看的说明、涉及的表和列、
|
||||
// SQL 片段。证据面板的查询步骤和助手面板共用这一份。
|
||||
// ===========================================================================
|
||||
|
||||
const LEVEL_ICON: Record<SqlCheckLevel, LucideIcon> = { error: OctagonX, warning: TriangleAlert, info: Info }
|
||||
|
||||
/** 级别标识:图标 + 「错误 / 提醒 / 提示」,悬停写明依据的确证程度 */
|
||||
export function SqlLevelBadge({ level: raw, className }: { level: unknown; className?: string }) {
|
||||
const level = sqlCheckLevelOf(raw)
|
||||
const Icon = LEVEL_ICON[level]
|
||||
const tone = SQL_CHECK_TONE[level]
|
||||
return (
|
||||
<span className={clsx('inline-flex shrink-0 items-center gap-0.5 whitespace-nowrap rounded-full border px-1.5 text-2xs leading-[16px]', className)}
|
||||
style={{ color: tone, borderColor: `color-mix(in srgb, ${tone} 45%, var(--border))`, background: `color-mix(in srgb, ${tone} 8%, transparent)` }}
|
||||
title={SQL_CHECK_LEVEL_HINT[level]} data-sql-level={level}>
|
||||
<Icon size={10} aria-hidden />
|
||||
{SQL_CHECK_LEVEL_LABEL[level]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 一条检查的正文:规则名、说明、涉及的表和列、SQL 片段 */
|
||||
export function SqlCheckBody({ check: c, excerpt = true }: { check: SqlCheckItem; excerpt?: boolean }) {
|
||||
const where = sqlCheckWhere(c)
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5">
|
||||
<SqlLevelBadge level={c.level} />
|
||||
<span className="font-medium text-fg" title={isSqlCheckCode(c.code) ? SQL_CHECK_RULE_HINT[c.code] : undefined} data-sql-rule="">
|
||||
{sqlRuleLabel(c.code)}
|
||||
</span>
|
||||
{where && (
|
||||
<span className="text-faint" data-sql-where={where}>
|
||||
{SQL_CHECK_TEXT.where} <span className="mono text-dim [overflow-wrap:anywhere]">{where}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{c.message && <p className="mt-0.5 leading-relaxed text-dim [overflow-wrap:anywhere]" data-sql-message="">{c.message}</p>}
|
||||
{excerpt && c.sql_excerpt && (
|
||||
<pre className="mono mt-1 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-bg px-1.5 py-1 leading-relaxed text-dim [overflow-wrap:anywhere]"
|
||||
aria-label={SQL_CHECK_TEXT.excerpt} title={SQL_CHECK_TEXT.excerpt} data-sql-excerpt="">
|
||||
{c.sql_excerpt}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 检查结果的清单:错误在前。每条一个淡色框,框的颜色跟级别走 */
|
||||
export function SqlCheckList({ checks, className }: { checks: SqlCheckItem[]; className?: string }) {
|
||||
const list = sortSqlChecks(checks)
|
||||
return (
|
||||
<ul className={clsx('space-y-1 text-2xs', className)} aria-label={SQL_CHECK_TEXT.listLabel(list.length)} data-sql-checks={list.length}>
|
||||
{list.map((c, i) => {
|
||||
const tone = SQL_CHECK_TONE[sqlCheckLevelOf(c.level)]
|
||||
return (
|
||||
<li key={`${c.code}:${c.table}:${c.column ?? ''}:${i}`} className="rounded border px-2 py-1.5"
|
||||
style={{ borderColor: `color-mix(in srgb, ${tone} 30%, var(--border))`, background: `color-mix(in srgb, ${tone} 5%, transparent)` }}
|
||||
data-sql-check={c.code} data-level={sqlCheckLevelOf(c.level)}>
|
||||
<SqlCheckBody check={c} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CatalogBusinessDate, CatalogCardinality, CatalogMeasure, CatalogPatchChange, CatalogTableKind } from '../types'
|
||||
import {
|
||||
CATALOG_CARDINALITY_LABEL, CATALOG_COLUMN_FIELD_LABEL, CATALOG_KIND_LABEL, CATALOG_MEASURE_LABEL, CATALOG_PATCH_TEXT,
|
||||
CATALOG_TABLE_FIELD_LABEL, CATALOG_TEXT,
|
||||
CATALOG_TABLE_FIELD_LABEL, CATALOG_TEXT, CODES_TEXT,
|
||||
} from '../lib/terms'
|
||||
|
||||
// ===========================================================================
|
||||
@@ -108,10 +108,13 @@ export function relationText(v: unknown): string {
|
||||
return `${left} → ${right}${card ? `(${card})` : ''}`
|
||||
}
|
||||
|
||||
/** 码值写成一行:1=有效、9=作废 */
|
||||
/** 码值的含义:空串是数据剖析写进来的候选,含义还要人填,和目录页同一个说法 */
|
||||
export const codeMeaning = (x: unknown) => (String(x ?? '').trim() ? String(x) : CODES_TEXT.pending)
|
||||
|
||||
/** 码值写成一行:1=有效、9=作废、8=含义待填写 */
|
||||
export function codesText(v: unknown): string {
|
||||
if (!v || typeof v !== 'object') return String(v)
|
||||
return Object.entries(v as Record<string, string>).map(([k, x]) => `${k}=${x}`).join('、')
|
||||
return Object.entries(v as Record<string, string>).map(([k, x]) => `${k}=${codeMeaning(x)}`).join('、')
|
||||
}
|
||||
|
||||
/** 一项的值写成一行字(过程里的明细、卡片里的改前改后都用它)。没有值写「未填写」 */
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { CatalogDriftTable, RunEvent, TeamMember, TeamRound, TeamRun, ToolChange } from '../types'
|
||||
import type { CatalogDriftTable, RunEvent, SqlCheckItem, TeamMember, TeamRound, TeamRun, ToolChange } from '../types'
|
||||
import type { NodeState, RunPhase } from './trace'
|
||||
import { formatDuration, formatNumber } from '../lib/format'
|
||||
import {
|
||||
CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, JUDGE_TEXT, MERGE_TEXT, TYPE_LABEL, claimTally, evidenceTally, issuanceLabel, nodeTypeLabel,
|
||||
CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, JUDGE_TEXT, MERGE_TEXT, SQL_CHECK_TEXT, TYPE_LABEL, claimTally, evidenceTally,
|
||||
issuanceLabel, nodeTypeLabel,
|
||||
} from '../lib/terms'
|
||||
import { catalogPatchOf, patchValueText, patchWhere } from './catalogPatch'
|
||||
import { claimCountsOf, isJudged, statsTally } from '../lib/evidence'
|
||||
import { isSqlCheckCode, sortSqlChecks, sqlCheckWhere, sqlRuleLabel } from '../lib/sqlcheck'
|
||||
|
||||
// 泳道数据画布也要用(supervisor 节点要展开成协作矩阵),所以类型放在
|
||||
// types.ts 里;这里再导出一遍,老引用不用改
|
||||
@@ -778,6 +780,20 @@ function explainLog(code: string | undefined, message: string): Pick<Step, 'titl
|
||||
fix: 'canvas',
|
||||
}
|
||||
}
|
||||
case 'metric_sql_check': {
|
||||
// 「指标「客单价」所依据的查询未通过 SQL 检查(「订单」关联「订单明细」是一对多,对「订单」的「订单金额」求和会
|
||||
// 重复计算),结果不可靠」;几处时是「所依据的查询有 2 处未通过 SQL 检查(…等),结果不可靠」(metrics.py
|
||||
// _sql_check_reason)。括号里的问题本身可能带括号:取到「,结果不可靠」前面最后一个右括号
|
||||
const name = message.match(/^指标「(.+?)」/)?.[1]
|
||||
const n = message.match(/有\s*(\d+)\s*处未通过 SQL 检查/)?.[1]
|
||||
const why = message.match(/未通过 SQL 检查(([\s\S]+))[,,]\s*结果不可靠\s*$/)?.[1]
|
||||
return {
|
||||
title: name ? SQL_CHECK_TEXT.metricTitle(name, n ? Number(n) : 1) : SQL_CHECK_TEXT.metricTitleAnon,
|
||||
sub: why,
|
||||
next: SQL_CHECK_TEXT.metricNext,
|
||||
fix: 'canvas',
|
||||
}
|
||||
}
|
||||
case 'judge_limit': {
|
||||
// 「结论句裁判已到上限(这份报告的裁判金额上限 $0.05):3 句没判,记为未裁判;已判的保留」(judge.py run_request)
|
||||
const n = message.match(/[::]\s*(\d+)\s*句(?:没判|未裁判)/)?.[1]
|
||||
@@ -2452,8 +2468,14 @@ export interface CopilotIssue {
|
||||
label?: string
|
||||
/** 落在节点的哪一项配置上(condition、tools、agents[1].tools):「定位」据此聚焦到检查器里那个输入框 */
|
||||
field?: string
|
||||
/** 机读代号:datasource_out_of_scope、tools_dropped…… */
|
||||
/** 机读代号:datasource_out_of_scope、tools_dropped……;SQL 检查是规则编号(fanout_sum……) */
|
||||
code?: string
|
||||
/** error / warning / info。老会话、老后端不给 */
|
||||
level?: 'error' | 'warning' | 'info'
|
||||
/** SQL 检查的问题另带:涉及的表和列、SQL 片段 */
|
||||
table?: string
|
||||
column?: string
|
||||
sqlExcerpt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2468,11 +2490,16 @@ function parseIssue(v: unknown, labels?: (id: string) => string | undefined): Co
|
||||
}
|
||||
if (v && typeof v === 'object') {
|
||||
const o = v as Record<string, unknown>
|
||||
const str = (x: unknown) => (typeof x === 'string' && x ? x : undefined)
|
||||
return named({
|
||||
message: String(o.message ?? ''),
|
||||
...(o.node_id ? { nodeId: String(o.node_id) } : {}),
|
||||
...(typeof o.field === 'string' && o.field ? { field: o.field } : {}),
|
||||
...(typeof o.code === 'string' && o.code ? { code: o.code } : {}),
|
||||
...(str(o.field) ? { field: str(o.field) } : {}),
|
||||
...(str(o.code) ? { code: str(o.code) } : {}),
|
||||
...(o.level === 'error' || o.level === 'warning' || o.level === 'info' ? { level: o.level } : {}),
|
||||
...(str(o.table) ? { table: str(o.table) } : {}),
|
||||
...(str(o.column) ? { column: str(o.column) } : {}),
|
||||
...(str(o.sql_excerpt) ? { sqlExcerpt: str(o.sql_excerpt) } : {}),
|
||||
})
|
||||
}
|
||||
const text = String(v ?? '')
|
||||
@@ -2480,9 +2507,37 @@ function parseIssue(v: unknown, labels?: (id: string) => string | undefined): Co
|
||||
return named(m ? { nodeId: m[1], message: m[2] || text } : { message: text })
|
||||
}
|
||||
|
||||
/** 一条自查问题写成一行:带节点名(认得出时),认不出才退到 id */
|
||||
export const issueLine = (x: CopilotIssue): string =>
|
||||
x.nodeId ? `「${x.label ?? x.nodeId}」${x.message}` : x.message
|
||||
/**
|
||||
* 一条自查问题写成一行:带节点名(认得出时),认不出才退到 id。SQL 检查的问题前面写中文规则名、后面写涉及的表和列,
|
||||
* 不露规则编号
|
||||
*/
|
||||
export const issueLine = (x: CopilotIssue): string => {
|
||||
const where = isSqlCheckCode(x.code) ? sqlCheckWhere({ table: x.table ?? '', column: x.column }) : ''
|
||||
const body = isSqlCheckCode(x.code)
|
||||
? `${sqlRuleLabel(x.code)}:${x.message}${where ? `(${SQL_CHECK_TEXT.where} ${where})` : ''}`
|
||||
: x.message
|
||||
return x.nodeId ? `「${x.label ?? x.nodeId}」${body}` : body
|
||||
}
|
||||
|
||||
/** 一条自查问题按 SQL 检查读(界面上画级别、规则名、表和列、SQL 片段);不是 SQL 检查的返回 null */
|
||||
export function issueSqlCheck(x: CopilotIssue): SqlCheckItem | null {
|
||||
if (!isSqlCheckCode(x.code)) return null
|
||||
return {
|
||||
code: x.code, level: x.level ?? 'error', message: x.message, table: x.table ?? '',
|
||||
...(x.column ? { column: x.column } : {}), ...(x.sqlExcerpt ? { sql_excerpt: x.sqlExcerpt } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** final.issues 里对照数据目录的 SQL 检查结果(不挡运行的提醒、提示也在里面);自查没修好、已经列过的错误不再算 */
|
||||
function sqlIssuesOf(ops: CopilotOp[], labels: (id: string) => string | undefined): CopilotIssue[] {
|
||||
const final = [...ops].reverse().find((o) => o.op === 'final')
|
||||
const list: unknown[] = final && Array.isArray(final.issues) ? final.issues : []
|
||||
const failed = [...ops].reverse().find((o) => o.op === 'check' && o.status === 'failed')
|
||||
const listed = new Set((failed && Array.isArray(failed.issues) ? failed.issues : [])
|
||||
.map((x: unknown) => parseIssue(x)).map((x: CopilotIssue) => `${x.nodeId ?? ''}|${x.code ?? ''}|${x.message}`))
|
||||
return list.map((x) => parseIssue(x, labels))
|
||||
.filter((x) => isSqlCheckCode(x.code) && !listed.has(`${x.nodeId ?? ''}|${x.code ?? ''}|${x.message}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点 id → 名字。先认这一轮操作流里加过、改过的(问数据页没有画布,只有这一份),
|
||||
@@ -2817,6 +2872,23 @@ export function decodeCopilot(ops: CopilotOp[], opts?: {
|
||||
add({ id: `cm-${i}-${t}`, seq: i, kind: 'note', status: 'done', level: 'warn',
|
||||
title: clip(`已跳过一个步骤:节点类型「${t}」不存在`, 80) })
|
||||
}
|
||||
// 对照数据目录的 SQL 检查:错误交回模型改过(没改好的在自查那一行),不挡运行的提醒、提示照样要看得见
|
||||
const sql = sqlIssuesOf(ops.slice(0, i), labels)
|
||||
if (sql.length) {
|
||||
const n = { error: 0, warning: 0, info: 0 }
|
||||
for (const x of sql) n[x.level ?? 'error'] += 1
|
||||
const nodes = [...new Set(sql.map((x) => x.nodeId).filter(Boolean))]
|
||||
add({
|
||||
id: `cs-${i}`, seq: i, kind: 'note', status: 'done', code: 'sql_check',
|
||||
...(n.error || n.warning ? { level: 'warn' as const } : {}),
|
||||
title: SQL_CHECK_TEXT.assistantTitle(SQL_CHECK_TEXT.counts(n.error, n.warning, n.info)),
|
||||
detail: sortSqlChecks(sql).map(issueLine).join('\n'),
|
||||
// 问数据页没有画布:下一步说去画布里打开这个工作流
|
||||
next: n.error || n.warning ? (canvas ? SQL_CHECK_TEXT.assistantNext : SQL_CHECK_TEXT.assistantNextChat)
|
||||
: SQL_CHECK_TEXT.assistantInfoNext,
|
||||
...(nodes.length === 1 ? { nodeId: nodes[0], ...(canvas ? { fix: 'canvas' as const } : {}) } : {}),
|
||||
})
|
||||
}
|
||||
// 工具绑定变化单独成一行:改图回执以前只说「修改 1」,模型改提示词时漏写 tools、
|
||||
// 把查库的工具整个抹掉,要到运行结果不对才发现
|
||||
const changes = toolChangesOf(op)
|
||||
@@ -2913,6 +2985,8 @@ export interface CopilotOutcome {
|
||||
toolChanges: ToolChange[]
|
||||
/** 自查的「工具被去掉了」警告(tools_dropped):这一轮的要求里没提到去掉 */
|
||||
dropped: CopilotIssue[]
|
||||
/** 对照数据目录的 SQL 检查结果(final.issues),自查没修好、已经列在 check 里的不重复 */
|
||||
sqlChecks: CopilotIssue[]
|
||||
}
|
||||
|
||||
export function copilotOutcome(
|
||||
@@ -2920,7 +2994,7 @@ export function copilotOutcome(
|
||||
): CopilotOutcome {
|
||||
const res: CopilotOutcome = {
|
||||
kind: running ? 'running' : 'empty', skipped: [], added: 0, updated: 0, removed: 0,
|
||||
toolChanges: [], dropped: [],
|
||||
toolChanges: [], dropped: [], sqlChecks: [],
|
||||
}
|
||||
const labels = labelsOf(ops, labelOf)
|
||||
for (const op of ops) {
|
||||
@@ -2952,6 +3026,7 @@ export function copilotOutcome(
|
||||
res.toolChanges = toolChangesOf(op)
|
||||
// 同一批警告 final.issues 里也有一份:自查已经带回来了就不再算一遍
|
||||
if (!res.dropped.length) res.dropped = droppedOf(op)
|
||||
res.sqlChecks = sqlIssuesOf(ops, labels)
|
||||
if (!running) res.kind = 'built'
|
||||
break
|
||||
case 'error':
|
||||
|
||||
Reference in New Issue
Block a user