合入集成分支(阶段 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:
ThearchyHelios
2026-10-03 12:35:47 +08:00
51 changed files with 4803 additions and 147 deletions
+45 -24
View File
@@ -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>
)
}
+4 -3
View File
@@ -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>
))}
+21 -1
View File
@@ -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>
+74
View File
@@ -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>
)
}
+6 -3
View File
@@ -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('、')
}
/** 一项的值写成一行字(过程里的明细、卡片里的改前改后都用它)。没有值写「未填写」 */
+84 -9
View File
@@ -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':