From 546d0e93260724491e63c82246a89426d66cd89c Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sun, 4 Oct 2026 11:28:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B2=A1=E6=9C=89=E6=95=B0=E6=8D=AE=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E7=9A=84=E5=A4=A7=E5=BA=93=EF=BC=9A=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E3=80=8C=E5=8F=82=E8=80=83=E4=BA=86=20N=20=E5=BC=A0=E8=A1=A8?= =?UTF-8?q?=E3=80=8D=E9=82=A3=E4=B8=80=E8=A1=8C=E6=8F=90=E7=A4=BA=E5=85=88?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E7=9B=AE=E5=BD=95=E3=80=82=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E9=87=8C=E7=9A=84=E5=A4=A7=E5=BA=93=E4=B8=80?= =?UTF-8?q?=E5=BC=A0=E7=9B=AE=E5=BD=95=E9=83=BD=E6=B2=A1=E6=9C=89=EF=BC=8C?= =?UTF-8?q?=E6=8C=91=E8=A1=A8=E5=92=8C=20SQL=20=E6=A3=80=E6=9F=A5=E9=83=BD?= =?UTF-8?q?=E7=94=A8=E4=B8=8D=E4=B8=8A=E7=9B=AE=E5=BD=95=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B9=9F=E4=B8=8D=E7=9F=A5=E9=81=93=E8=AF=A5=E5=85=88?= =?UTF-8?q?=E5=8E=BB=E8=B5=B7=E8=8D=89=E3=80=82=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=20context=20=E6=93=8D=E4=BD=9C=E7=9A=84=E6=AF=8F=E4=B8=AA?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=BA=90=E5=A4=9A=E5=B8=A6=20source=5Fid=20?= =?UTF-8?q?=E5=92=8C=20catalog=5Ftables=EF=BC=88=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E9=A9=B3=E5=9B=9E=E9=A1=B9=E5=90=8E=E8=BF=98=E6=9C=89=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E7=9A=84=E8=A1=A8=E6=95=B0=EF=BC=89=EF=BC=9B=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20copilotContext=20=E5=9C=A8=E8=A6=81=E6=8C=91?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E5=A4=A7=E5=BA=93=20catalog=5Ftables=20?= =?UTF-8?q?=E4=B8=BA=200=20=E6=97=B6=E7=BB=99=E5=87=BA=E4=B8=8B=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E3=80=8C=E8=BF=99=E4=B8=AA=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E8=BF=98=E6=B2=A1=E6=9C=89=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E8=B5=B7=E8=8D=89=E5=90=8E=E6=8C=91=E8=A1=A8=E5=92=8C?= =?UTF-8?q?=20SQL=20=E6=A3=80=E6=9F=A5=E4=BC=9A=E6=9B=B4=E5=87=86=E3=80=8D?= =?UTF-8?q?=EF=BC=8C=E5=85=A5=E5=8F=A3=E7=9B=B4=E8=BE=BE=20/data/catalog/:?= =?UTF-8?q?sourceId=EF=BC=88=E5=87=A0=E4=B8=AA=E5=BA=93=E6=97=B6=E7=82=B9?= =?UTF-8?q?=E5=90=8D=EF=BC=8C=E8=80=81=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=B8=8D?= =?UTF-8?q?=E7=BB=99=E7=9B=AE=E5=BD=95=E8=A1=A8=E6=95=B0=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=89=E3=80=82check-decode=E3=80=81check-?= =?UTF-8?q?stream=E3=80=81check-chat=20=E8=A1=A5=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/copilot_context.py | 19 ++++++++---- backend/tests/test_copilot_table_pick.py | 37 ++++++++++++++++++++---- frontend/src/lib/terms.ts | 3 ++ frontend/src/run/AssistantStream.tsx | 12 ++++++-- frontend/src/run/__tests__/synthetic.ts | 10 +++++-- frontend/src/run/decode.ts | 32 ++++++++++++++++++-- scripts/check-chat.mjs | 11 ++++++- scripts/check-decode.mjs | 22 ++++++++++++++ scripts/check-stream.mjs | 12 ++++++++ 9 files changed, 138 insertions(+), 20 deletions(-) diff --git a/backend/app/api/copilot_context.py b/backend/app/api/copilot_context.py index 5b065047..3e963b5d 100644 --- a/backend/app/api/copilot_context.py +++ b/backend/app/api/copilot_context.py @@ -214,6 +214,11 @@ class SourcePlan: """给模型看的目录:去掉驳回项。""" return {t: catalog.visible_notes(n) for t, n in self.raw_notes.items()} + @property + def catalog_tables(self) -> int: + """有数据目录的表数:去掉驳回项之后还有内容的。整份被驳回的不算——挑表、SQL 检查都用不上它。""" + return sum(1 for n in self.raw_notes.values() if catalog.visible_notes(n)) + def _modes(sources: list[Any]) -> list[Literal["all", "pick"]]: """每个源给详细摘要还是先挑表。 @@ -565,16 +570,20 @@ class DatasourceContext: def op(self) -> dict[str, Any] | None: """告诉前端这一轮参考了哪些表:{"op": "context", "sources": [...], "elapsed_ms"?}。没有可查的表时为 None。 - 每个源一项:source 源名,tables 给了全字段的表(全名),selected_by,total 这个源一共几张表(表结构缓存里 - 记的真实总数);探查截断了的再带 explored(探查到几张),界面写「共 N 张,只探查了 200 张」;挑表失败的 - 带上 reason。只列了名字的表不进 tables:界面上说的「参考了 N 张表」是模型看得到字段的那些。 + 每个源一项:source 源名,source_id 源的 id(界面据此给去数据目录页的入口),tables 给了全字段的表(全名), + selected_by,total 这个源一共几张表(表结构缓存里记的真实总数),catalog_tables 有数据目录的表数(没有目录的 + 大库,界面提示先去起草目录:挑表和 SQL 检查都靠它);探查截断了的再带 explored(探查到几张),界面写「共 N 张, + 只探查了 200 张」;挑表失败的带上 reason。只列了名字的表不进 tables:界面上说的「参考了 N 张表」是模型看得到 + 字段的那些。 """ rows = [] for s in self.sources: if not s.explored: continue - row: dict[str, Any] = {"source": s.source.name, "tables": [s.qualified(t) for t in s.tables], - "selected_by": s.selected_by, "total": s.total} + row: dict[str, Any] = {"source": s.source.name, "source_id": getattr(s.source, "id", None), + "tables": [s.qualified(t) for t in s.tables], + "selected_by": s.selected_by, "total": s.total, + "catalog_tables": s.plan.catalog_tables} if s.plan.unexplored: row["explored"] = s.explored if s.selected_by == "fallback" and self.reason: diff --git a/backend/tests/test_copilot_table_pick.py b/backend/tests/test_copilot_table_pick.py index 7e249404..53361e70 100644 --- a/backend/tests/test_copilot_table_pick.py +++ b/backend/tests/test_copilot_table_pick.py @@ -322,8 +322,9 @@ async def test_small_source_keeps_detail_and_carries_the_catalog(small_source): assert f"中文名:订单{INFERRED_MARK}" in text and "单位:元" in text assert f"orders.customer_id = customers.id(多对一){INFERRED_MARK}" in text op = ctx.op() - assert op["sources"] == [{"source": small_source.name, "tables": ["customers", "orders"], "selected_by": "all", - "total": 2}] + assert op["sources"] == [{"source": small_source.name, "source_id": small_source.id, + "tables": ["customers", "orders"], "selected_by": "all", "total": 2, + "catalog_tables": 1}] # ---------------------------------------------------------------- 回退:绝不阻断生成 @@ -346,8 +347,9 @@ async def test_pick_failures_fall_back_to_names_only(client, monkeypatch, scenic "instruction": NEED, "intent": "answer", "datasource_ids": [scenic_source.id]}) op = _context_op(events) - assert op["sources"] == [{"source": scenic_source.name, "tables": [], "selected_by": "fallback", - "total": scenic.TABLE_COUNT + len(scenic.VIEWS), "reason": op["sources"][0]["reason"]}] + assert op["sources"] == [{"source": scenic_source.name, "source_id": scenic_source.id, "tables": [], + "selected_by": "fallback", "total": scenic.TABLE_COUNT + len(scenic.VIEWS), + "catalog_tables": 0, "reason": op["sources"][0]["reason"]}] assert op["sources"][0]["reason"] system = model.systems[0] # 和以前一样只列表名、提醒先查字段 @@ -371,8 +373,9 @@ async def test_stream_reports_the_context_and_prompt_carries_selected_tables(cli "instruction": NEED, "intent": "answer", "datasource_ids": [scenic_source.id]}) op = _context_op(events) - assert op["sources"] == [{"source": scenic_source.name, "tables": ["visits", "channels", "channel_visits"], - "selected_by": "model", "total": scenic.TABLE_COUNT + len(scenic.VIEWS)}] + assert op["sources"] == [{"source": scenic_source.name, "source_id": scenic_source.id, + "tables": ["visits", "channels", "channel_visits"], "selected_by": "model", + "total": scenic.TABLE_COUNT + len(scenic.VIEWS), "catalog_tables": 0}] assert isinstance(op["elapsed_ms"], int) and op["elapsed_ms"] >= 0 # context 在模型首帧之后、第一条图操作之前 kinds = [e["op"] for e in events] @@ -423,6 +426,28 @@ async def test_self_check_repair_reuses_the_same_context(client, monkeypatch, sc assert len(picker.calls) == 1 +async def test_context_op_says_how_much_of_each_source_has_a_catalog(scenic_source): + """context 操作里每个源带上有数据目录的表数(catalog_tables)和源的 id:没有目录的大库,界面提示先去起草目录。 + + 真实运行里数据源一张目录都没有,挑表和 SQL 检查都用不上目录,用户也不知道该先去起草。只算有可见项的表: + 整份被驳回的不算有目录。""" + from app.api import copilot_context + + async def op() -> dict: + plan = await copilot_context.plan_context([scenic_source]) + return (await plan.resolve(Picker(PICKED), need=NEED)).op()["sources"][0] + + first = await op() + assert first["catalog_tables"] == 0 and first["source_id"] == scenic_source.id + async with SessionLocal() as session: + await catalog.write_entry(session, scenic_source.id, "visits", { + "label": make_item("入园记录", "llm")}, if_version=0, actor="王敏") + rejected = make_item("门店", "llm") + rejected["status"] = "rejected" + await catalog.write_entry(session, scenic_source.id, "stores", {"label": rejected}, if_version=0, actor="王敏") + assert (await op())["catalog_tables"] == 1 + + async def test_small_sources_do_not_call_the_picker(client, monkeypatch, small_source): picker = Picker(error=AssertionError("小库不该挑表")) _use_picker(monkeypatch, picker) diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 37388a16..6b017e2f 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2657,4 +2657,7 @@ export const COPILOT_CONTEXT_TEXT = { withFields: (n: number) => `参考了 ${formatNumber(n)} 张表的字段`, namesOnly: (n: number) => `另有 ${formatNumber(n)} 张表只提供了表名`, notPicked: '未能按需求挑选', + /** 没有数据目录的大库:只有一个数据源时说「这个数据源」,几个时点名 */ + noCatalog: (who: string | null) => `${who ?? '这个数据源'}还没有数据目录,起草后挑表和 SQL 检查会更准`, + toCatalog: '去数据目录起草', } diff --git a/frontend/src/run/AssistantStream.tsx b/frontend/src/run/AssistantStream.tsx index 9ecf00fb..ecdc7eb0 100644 --- a/frontend/src/run/AssistantStream.tsx +++ b/frontend/src/run/AssistantStream.tsx @@ -15,7 +15,9 @@ import { humanizeError } from '../lib/errors' import { explainRunError } from '../lib/explain' import { formatClock, formatDuration, formatNumber, NONE, shortId } from '../lib/format' import { statusLabel } from '../lib/status' -import { EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, evidenceTally, issuanceLabel, nodeTypeLabel } from '../lib/terms' +import { + COPILOT_CONTEXT_TEXT, EVIDENCE_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, evidenceTally, issuanceLabel, nodeTypeLabel, +} from '../lib/terms' import { SqlCheckList } from './SqlChecks' import { ISSUANCE_HINT, childrenByExec, compactSteps, parseQueryResult, progressOf, spread, teamVerdictOf, @@ -901,6 +903,12 @@ function FixAction({ fix, nodeId, label, to, first, field }: { if (fix === 'settings') { return 去模型接入 } + if (fix === 'catalog' && to) { + // 助手参考的大库还没有数据目录:直达那个数据源的目录页去起草 + return ( + {COPILOT_CONTEXT_TEXT.toCatalog} + ) + } if (fix === 'tools') { // 数据源工具(db_query__ 这类)不在工具库里接入,在数据页 const data = !!to?.startsWith('/data') @@ -1170,7 +1178,7 @@ function StepLine({ step, depth, turnMs }: { step: Step; depth: number; turnMs?: {/* 要改的是另一个节点(指标的问题出在来源查询的 SQL 上):入口落到那个节点的那一栏 */} + label={step.fixNode ? step.fixLabel : undefined} to={step.fixTo} /> )} diff --git a/frontend/src/run/__tests__/synthetic.ts b/frontend/src/run/__tests__/synthetic.ts index 3177ed1f..46fea4ed 100644 --- a/frontend/src/run/__tests__/synthetic.ts +++ b/frontend/src/run/__tests__/synthetic.ts @@ -503,8 +503,10 @@ export const COPILOT_CONTEXT = [ { op: 'model', model: 'claude-sonnet-4' }, { op: 'heartbeat', phase: 'planning', elapsed_ms: 3000 }, { op: 'context', elapsed_ms: 3420, sources: [ - { source: 'scenic', tables: ['visits', 'channels', 'channel_visits'], selected_by: 'model', total: 51 }, - { source: 'shop', tables: ['orders', 'customers'], selected_by: 'all', total: 2 }, + { source: 'scenic', source_id: 'src-scenic', tables: ['visits', 'channels', 'channel_visits'], selected_by: 'model', total: 51, + catalog_tables: 12 }, + // 小库全部表都带字段:没有数据目录也不提示 + { source: 'shop', source_id: 'src-shop', tables: ['orders', 'customers'], selected_by: 'all', total: 2, catalog_tables: 0 }, ] }, { op: 'heartbeat', phase: 'planning', elapsed_ms: 6000 }, { op: 'thinking', delta: '入园记录不带渠道,要经 channel_visits 连到渠道表,再按渠道汇总上月的入园人次。' }, @@ -539,7 +541,9 @@ export const COPILOT_CONTEXT_FALLBACK = [ { op: 'model', model: 'claude-sonnet-4' }, { op: 'heartbeat', phase: 'planning', elapsed_ms: 18000 }, { op: 'context', elapsed_ms: 20004, sources: [ - { source: 'scenic', tables: [], selected_by: 'fallback', total: 51, reason: '挑选数据表超过 20 秒未完成' }, + // 大库一张目录都没有:过程里提示先去起草目录 + { source: 'scenic', source_id: 'src-scenic', tables: [], selected_by: 'fallback', total: 51, catalog_tables: 0, + reason: '挑选数据表超过 20 秒未完成' }, ] }, { op: 'plan', summary: '先查表结构,再取数' }, { op: 'add_node', node: { id: 's', type: 'tool', label: '查看表结构' } }, diff --git a/frontend/src/run/decode.ts b/frontend/src/run/decode.ts index cd668956..e352daf3 100644 --- a/frontend/src/run/decode.ts +++ b/frontend/src/run/decode.ts @@ -48,7 +48,8 @@ export const teamVerdictOf = (team: TeamRun | undefined | null): TeamVerdict | u (team as TeamRunEx | undefined | null)?.verdict /** 下一步该去哪儿改。界面据此给一个直达的入口,而不只是一句话 */ -export type FixKind = 'canvas' | 'settings' | 'tools' +/** catalog:去数据目录页(fixTo 指到哪个数据源),助手参考的大库还没有数据目录时给 */ +export type FixKind = 'canvas' | 'settings' | 'tools' | 'catalog' /** * 事件 → 人能看懂的步骤。 @@ -147,6 +148,8 @@ export interface Step { next?: string /** 下一步该去的地方 */ fix?: FixKind + /** fix 要去的页面地址(catalog:/data/catalog/:sourceId) */ + fixTo?: string /** 工具的时限(秒,tool.start.timeout_s)。进行中的行超过它就要说破 */ limitS?: number /** 出具那一行的档位(formal / degraded / withheld) */ @@ -2649,6 +2652,10 @@ export interface CopilotContextSource { explored?: number /** 没能挑出来的原因 */ reason?: string + /** 数据源 id:去数据目录页的入口用(老服务端不给) */ + sourceId?: string + /** 有数据目录的表数(老服务端不给:不知道就不提示) */ + catalogTables?: number } /** 挑表失败时的说法:模型这一轮只看到表名,字段要它自己去查 */ @@ -2668,10 +2675,24 @@ function contextSources(op: CopilotOp): CopilotContextSource[] { ...(total != null ? { total } : {}), ...(total != null && explored != null && explored < total ? { explored } : {}), ...(typeof x.reason === 'string' && x.reason ? { reason: x.reason } : {}), + ...(typeof x.source_id === 'string' && x.source_id ? { sourceId: x.source_id } : {}), + ...(num(x.catalog_tables) != null ? { catalogTables: num(x.catalog_tables) } : {}), } }) } +/** + * 没有数据目录的大库(要按需求挑表的那种):挑表只能照表名猜,SQL 检查也没有目录可对照。真实运行里数据源一张目录 + * 都没有,用户也不知道该先去起草。只认服务端明说了 0 张的;小库全部表都带字段,不提示 + */ +function noCatalog(groups: CopilotContextSource[]): { next: string; fixTo?: string } | null { + const bare = groups.filter((g) => g.catalogTables === 0 && g.selectedBy !== 'all') + if (!bare.length) return null + const who = groups.length === 1 ? null : `「${bare.map((g) => g.source).join('」「')}」` + const to = bare.find((g) => g.sourceId)?.sourceId + return { next: COPILOT_CONTEXT_TEXT.noCatalog(who), ...(to ? { fixTo: `/data/catalog/${encodeURIComponent(to)}` } : {}) } +} + /** * 一个库在展开区里的那一行:怎么来的 + 表名。探查结构截断了的库写明「共 205 张,只探查了 200 张」: * 模型只看得到探查到的那些,用户说的表不在里面时,先得知道是没探查到,不是库里没有 @@ -2702,7 +2723,8 @@ function contextLine(g: CopilotContextSource): string { * 先得知道模型当时看到的是哪几张。标题只说几张(「参考了 8 张表」),展开按数据源分组列表名;挑表失败时 * 标题直说只给了表名,原因放在展开区。认不出的形状返回 null:不出这一行,也不报错。 */ -export function copilotContext(op: CopilotOp): { title: string; sub?: string; detail: string; ms?: number } | null { +export function copilotContext(op: CopilotOp): + { title: string; sub?: string; detail: string; ms?: number; next?: string; fixTo?: string } | null { // 展开区里按需求挑过表的库排在前面:请求里点名的那个库往往就是它,排在一串小库后面得在内层滚动框里翻才看得到 const order: Record = { model: 0, all: 1, fallback: 2 } const groups = contextSources(op).map((g, i) => ({ g, i })) @@ -2721,11 +2743,13 @@ export function copilotContext(op: CopilotOp): { title: string; sub?: string; de const reasons = [...new Set(failed.map((g) => g.reason).filter(Boolean))] // 挑过表才有 elapsed_ms:挑表是生成之前多出来的一次模型调用,没挑成(比如超时)也要让人看到等了多久 const ms = num(op.elapsed_ms) + const hint = noCatalog(groups) return { title: count ? (namesOnly ? COPILOT_CONTEXT_TEXT.withFields(count) : `参考了 ${count} 张表`) : CONTEXT_FALLBACK_TEXT, ...(sub ? { sub } : {}), detail: [...groups.map(contextLine), ...reasons.map((r) => `原因:${r}`)].join('\n'), ...(ms != null && ms >= 10 ? { ms } : {}), + ...(hint ?? {}), } } @@ -2848,7 +2872,9 @@ export function decodeCopilot(ops: CopilotOp[], opts?: { add({ id: `cx-${i}`, seq: i, kind: 'schema', status: 'done', code: 'copilot_context', title: ctx.title, detail: ctx.detail, ...(ctx.sub ? { sub: ctx.sub } : {}), - ...(ctx.ms != null ? { ms: ctx.ms, meta: formatDuration(ctx.ms) } : {}) }) + ...(ctx.ms != null ? { ms: ctx.ms, meta: formatDuration(ctx.ms) } : {}), + // 没有数据目录的大库:下一步去起草目录,入口直达那个数据源的目录页 + ...(ctx.next ? { next: ctx.next, ...(ctx.fixTo ? { fix: 'catalog' as const, fixTo: ctx.fixTo } : {}) } : {}) }) break } case 'catalog_patch': { diff --git a/scripts/check-chat.mjs b/scripts/check-chat.mjs index 69b7bd90..d1f65737 100644 --- a/scripts/check-chat.mjs +++ b/scripts/check-chat.mjs @@ -487,8 +487,10 @@ async function fakeApi(route) { const ops = [ { op: 'heartbeat', phase: 'planning', elapsed_ms: 800 }, // 服务端按需求挑了表(copilot_context):过程里多一行「参考了 N 张表」 + // 这个库还没有数据目录(catalog_tables: 0):那一行下面提示先去起草目录 { op: 'context', elapsed_ms: 1200, sources: [ - { source: 'shop', tables: ['orders', 'order_items'], selected_by: 'model', total: 48 }] }, + { source: 'shop', source_id: 'src-shop', tables: ['orders', 'order_items'], selected_by: 'model', total: 48, + catalog_tables: 0 }] }, { op: 'plan', summary: '查一下再回答' }, ...GRAPH.nodes.map((n) => ({ op: 'add_node', node: n })), { op: 'done', explanation: '一张三步的图' }, @@ -1069,6 +1071,13 @@ for (const theme of THEMES) { await ctxRow.locator('button').first().click() check('再点开看到按数据源分组的表名', await ctxRow.getByText('「shop」按需求从 48 张表中挑出 2 张:orders、order_items') .waitFor({ timeout: 3000 }).then(() => true, () => false)) + // 真实运行里数据源一张目录都没有,挑表和 SQL 检查都用不上目录:这一行下面提示起草,并给去目录页的入口 + const noCatalog = ctxRow.locator('[data-step-next]') + check('没有数据目录的大库:提示先起草目录', await noCatalog.count() === 1 + && (await noCatalog.innerText()).includes('这个数据源还没有数据目录,起草后挑表和 SQL 检查会更准'), + await noCatalog.count() ? await noCatalog.innerText() : '没有这一句') + check('……入口直达这个数据源的目录页', await ctxRow.locator('a[data-fix="catalog"]').getAttribute('href').catch(() => null) + === '/data/catalog/src-shop') check('没有运行时报错', errors.length === 0, errors[0] ?? '') await ctx.close() }) diff --git a/scripts/check-decode.mjs b/scripts/check-decode.mjs index c3ef2f87..b021e395 100644 --- a/scripts/check-decode.mjs +++ b/scripts/check-decode.mjs @@ -682,6 +682,28 @@ await section('Copilot:这一轮参考了哪些表(context 操作)', async check('挑表失败:原因和库的规模放在展开区', !!fb?.detail?.includes('原因:挑选数据表超过 20 秒未完成') && fb.detail.includes('「scenic」共 51 张表'), fb?.detail) check('挑表失败也说等了多久', fb?.ms === 20004, String(fb?.ms)) + // 真实运行里数据源一张目录都没有,挑表、SQL 检查都用不上目录,用户也不知道该先去起草 + check('没有数据目录的大库:下一步提示起草目录,入口直达那个源的目录页', + fb?.next === '这个数据源还没有数据目录,起草后挑表和 SQL 检查会更准' && fb?.fix === 'catalog' + && fb?.fixTo === '/data/catalog/src-scenic', JSON.stringify({ next: fb?.next, fix: fb?.fix, fixTo: fb?.fixTo })) + check('有目录的大库、没有目录的小库:不提示', !row?.next && !row?.fix && !row?.fixTo, JSON.stringify({ next: row?.next })) + const bare = mod.copilotContext({ op: 'context', sources: [ + { source: 'a', source_id: 'id-a', tables: ['x'], selected_by: 'model', total: 60, catalog_tables: 0 }, + { source: 'b', source_id: 'id-b', tables: ['y'], selected_by: 'all', total: 3, catalog_tables: 0 }, + { source: 'c', source_id: 'id c', tables: [], selected_by: 'fallback', total: 90, catalog_tables: 0, reason: 'r' }] }) + check('几个数据源:点名没有目录的大库,入口去第一个', bare?.next === '「a」「c」还没有数据目录,起草后挑表和 SQL 检查会更准' + && bare?.fixTo === '/data/catalog/id-a', JSON.stringify({ next: bare?.next, fixTo: bare?.fixTo })) + const partly = mod.copilotContext({ op: 'context', sources: [ + { source: 'a', source_id: 'id-a', tables: ['x'], selected_by: 'model', total: 60, catalog_tables: 3 }] }) + check('起草过一部分目录:不再提示', !partly?.next, partly?.next) + const old = mod.copilotContext({ op: 'context', sources: [{ source: 'a', tables: ['x'], selected_by: 'model', total: 60 }] }) + check('老服务端不给目录表数:不猜,不提示', !old?.next && !old?.fixTo, old?.next) + const noId = mod.copilotContext({ op: 'context', sources: [ + { source: 'a', tables: [], selected_by: 'fallback', total: 60, catalog_tables: 0 }] }) + check('没给数据源 id:照样提示,只是不给入口', !!noId?.next && !noId?.fixTo, JSON.stringify(noId)) + const step = mod.decodeCopilot([{ op: 'context', sources: [ + { source: 'a', tables: [], selected_by: 'fallback', total: 60, catalog_tables: 0 }] }]).find((s) => s.code === 'copilot_context') + check('没有入口时不带 fix:不画一个点了没处去的按钮', !!step?.next && !step?.fix, JSON.stringify(step)) const seeded = mod.copilotContext({ op: 'context', sources: [ { source: 'scenic', tables: ['stores'], selected_by: 'fallback', total: 51, reason: '模型未挑出任何表' }] }) diff --git a/scripts/check-stream.mjs b/scripts/check-stream.mjs index f4660f41..db54b27e 100644 --- a/scripts/check-stream.mjs +++ b/scripts/check-stream.mjs @@ -774,12 +774,24 @@ await section('参考了哪些表:画布右栏和问数据页(context 操作 && expanded.includes('channel_visits') && expanded.includes('「shop」全部 2 张表'), expanded.slice(0, 160)) const overflow = await c.page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth) check('展开后不撑破 380px 窄栏', overflow <= 0, `${overflow}px`) + check('有数据目录的大库:不提示起草目录', await row.locator('[data-step-next]').count() === 0 + && await row.locator('[data-fix="catalog"]').count() === 0) check('没有运行时报错', c.errors.length === 0, c.errors.join(' | ')) await c.page.close() const f = await open('syn=copilot-fallback&dense=1', { w: 380, h: 800, name: 'copilot-fallback' }) const fb = f.page.locator('[data-step-code="copilot_context"]') check('挑表失败:写明只给了表名', await fb.count() === 1 && (await fb.innerText()).includes('未能按需求挑选,已提供全部表名')) + // 真实运行里数据源一张目录都没有:这一行下面常显下一步,入口去那个源的目录页 + const hint = fb.locator('[data-step-next]') + check('没有数据目录的大库:常显「还没有数据目录」,不收进展开区', await hint.count() === 1 + && (await hint.innerText()).includes('这个数据源还没有数据目录,起草后挑表和 SQL 检查会更准'), + await hint.count() ? await hint.innerText() : '没有这一句') + const go = fb.locator('a[data-fix="catalog"]') + check('……附去数据目录页的入口', await go.count() === 1 && await go.getAttribute('href') === '/data/catalog/src-scenic' + && (await go.innerText()).includes('去数据目录起草'), await go.count() ? await go.getAttribute('href') : '没有入口') + const narrow = await f.page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth) + check('……不撑破 380px 窄栏', narrow <= 0, `${narrow}px`) await fb.locator('button').first().click() await f.page.waitForTimeout(150) check('挑表失败:展开看到原因', (await fb.innerText()).includes('原因:挑选数据表超过 20 秒未完成'))