From 3ece89872bc5bf665ffa93b139c7f84bd8e9e3a2 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sun, 4 Oct 2026 01:24:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=20F3=20/?= =?UTF-8?q?=20B1=20=E7=95=8C=E9=9D=A2=EF=BC=9A=E8=BF=90=E8=A1=8C=E6=A8=AA?= =?UTF-8?q?=E5=B9=85=E3=80=81=E6=97=B6=E9=97=B4=E7=BA=BF=E5=92=8C=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8E=86=E5=8F=B2=E6=8A=8A=20Agent=20=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E6=9F=A5=E8=AF=A2=E7=9A=84=E8=A1=A8=E5=92=8C=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=BC=95=E7=94=A8=E5=88=86=E5=BC=80=E8=AF=B4=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=9B=B4=E6=8E=A5=E5=BC=95=E7=94=A8=E5=86=99=E3=80=8C?= =?UTF-8?q?=E6=9C=89=E5=8F=98=E5=8C=96=E3=80=8D=EF=BC=8C=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=B6=89=E5=8F=8A=E5=86=99=E3=80=8CAgent=20=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E7=9A=84=E8=A1=A8=E6=9C=89=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E3=80=8D=E5=B9=B6=E5=8F=A6=E8=B5=B7=E4=B8=80=E7=BB=84=EF=BC=9B?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2=E6=8C=89=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E8=AE=B0=E5=BD=95=E4=B8=A4=E7=BB=84=E5=88=97?= =?UTF-8?q?=E5=87=BA=EF=BC=8C=E5=8F=91=E5=B8=83=E4=B9=8B=E5=90=8E=E6=89=8D?= =?UTF-8?q?=E5=BB=BA=E7=9B=AE=E5=BD=95=E7=9A=84=E8=A1=A8=E4=B9=9F=E5=88=97?= =?UTF-8?q?=EF=BC=9B=E8=A1=A5=E5=8F=91=E5=B8=83=E8=AE=B0=E5=BD=95=E4=B8=A4?= =?UTF-8?q?=E7=A7=8D=E5=86=99=E6=B3=95=E7=9A=84=E5=8D=95=E6=B5=8B=E5=92=8C?= =?UTF-8?q?=E4=B8=89=E4=B8=AA=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC=E7=9A=84?= =?UTF-8?q?=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/test_catalog_impact.py | 15 +++++ frontend/src/canvas/VersionsSheet.tsx | 83 ++++++++++++++++++--------- frontend/src/lib/terms.ts | 18 +++++- frontend/src/pages/runs/Banners.tsx | 57 +++++++++++------- frontend/src/run/decode.ts | 36 +++++++++--- frontend/src/types.ts | 17 +++++- scripts/check-decode.mjs | 20 +++++++ scripts/check-runs.mjs | 19 ++++-- scripts/check-studio.mjs | 26 +++++++-- 9 files changed, 222 insertions(+), 69 deletions(-) diff --git a/backend/tests/test_catalog_impact.py b/backend/tests/test_catalog_impact.py index bf932094..128c1e20 100644 --- a/backend/tests/test_catalog_impact.py +++ b/backend/tests/test_catalog_impact.py @@ -205,3 +205,18 @@ def test_assistant_seeds_read_tool_nodes_the_same_way(): assert graph_tables(graph, [shop]) == {"shop": ["customers", "orders", "stores"]} graph["nodes"] = [n for n in graph["nodes"] if n["id"] != "q2"] assert graph_tables(graph, [shop]) == {"shop": ["customers", "orders"]} + + +def test_recorded_versions_reads_both_shapes(): + """发布记录两种写法:现在的 {direct, possible},和可能涉及上线之前只记直接引用的 {源名: {表名: 版本}}。 + 源名可以就叫 direct / possible,靠第二层是不是版本号分开。""" + from app.data.catalog_impact import recorded_versions + + now = {"direct": {"shop": {"orders": 2}}, "possible": {"shop": {"customers": 1}, "crm": {}}} + assert recorded_versions(now) == now + assert recorded_versions({"direct": {}, "possible": {}}) == {"direct": {}, "possible": {}} + assert recorded_versions({"shop": {"orders": 2}}) == {"direct": {"shop": {"orders": 2}}, "possible": {}} + # 旧写法里一个叫 direct 的源 + assert recorded_versions({"direct": {"orders": 2}}) == {"direct": {"direct": {"orders": 2}}, "possible": {}} + assert recorded_versions({}) == {"direct": {}, "possible": {}} + assert recorded_versions(None) is None and recorded_versions("x") is None and recorded_versions({"shop": 3}) is None diff --git a/frontend/src/canvas/VersionsSheet.tsx b/frontend/src/canvas/VersionsSheet.tsx index 5ea271f5..7b047a97 100644 --- a/frontend/src/canvas/VersionsSheet.tsx +++ b/frontend/src/canvas/VersionsSheet.tsx @@ -10,7 +10,7 @@ import { formatDateTime, formatTime } from '../lib/format' import { CATALOG_DRIFT_TEXT, WORKFLOW_STATUS_LABEL } from '../lib/terms' import { hintOf } from './shortcuts' import type { Edge } from '@xyflow/react' -import type { CatalogDriftTable, GraphSpec, Workflow, WorkflowVersion } from '../types' +import type { CatalogDriftTable, CatalogVersions as CatalogVersionsRecord, GraphSpec, Workflow, WorkflowVersion } from '../types' /** * 版本历史。盖在右栏上的一层,和属性面板同一种「临时造访」。 @@ -212,38 +212,65 @@ function VersionPreview({ version, target, diff, lock, onRestore }: { const DIFF_COLOR = { add: 'var(--ok)', del: 'var(--err)', mod: 'var(--warn)' } as const /** - * 发布时的目录版本:这一版 SQL 用到的表在发布时是数据目录的第几版,之后改过的标出来——下次从这一版发起正式 - * 运行时会提醒「数据目录有变化」,这里先让人看到是哪几张表 + * 发布时的目录版本:这一版用到的表在发布时是数据目录的第几版,之后改过的标出来——下次从这一版发起正式运行时会 + * 提醒「数据目录有变化」,这里先让人看到是哪几张表。 + * + * 两组分开列:SQL 里写着的表在前;Agent 可能查询的表(Agent 绑定了数据源的查询工具,发布时记下数据源中所有有 + * 目录的表)在小标题下面。可能涉及的数据源里发布之后新建了目录的表,记录里没有它,按变化里给的补进来(发布时 + * 「尚无目录」) */ -function CatalogVersions({ versions, changes }: { - versions: Record> - changes: CatalogDriftTable[] -}) { - const rows = Object.entries(versions).flatMap(([source, tables]) => - Object.entries(tables).map(([table, v]) => ({ source, table, v }))) - const changed = new Map(changes.map((c) => [`${c.source}/${c.table}`, c])) +function CatalogVersions({ versions, changes }: { versions: CatalogVersionsRecord; changes: CatalogDriftTable[] }) { + // 老后端给的变化没有 impact:按直接引用 + const impactOf = (c: CatalogDriftTable) => (c.impact === 'possible' ? 'possible' : 'direct') + const changed = new Map(changes.map((c) => [`${impactOf(c)}/${c.source}/${c.table}`, c])) + const rowsOf = (impact: CatalogDriftTable['impact']) => { + const recorded = Object.entries(versions[impact] ?? {}).flatMap(([source, tables]) => + Object.entries(tables).map(([table, v]) => ({ source, table, v: v as number | null }))) + const known = new Set(recorded.map((r) => `${r.source}/${r.table}`)) + const added = changes.filter((c) => impactOf(c) === impact && !known.has(`${c.source}/${c.table}`)) + .map((c) => ({ source: c.source, table: c.table, v: c.published })) + return [...recorded, ...added] + } + const direct = rowsOf('direct') + const possible = rowsOf('possible') + const bare = Object.entries(versions.possible ?? {}) + .filter(([source, tables]) => !Object.keys(tables).length && !possible.some((r) => r.source === source)) + .map(([source]) => source) + const list = (impact: CatalogDriftTable['impact'], rows: typeof direct) => ( + + ) + const empty = !direct.length && !possible.length && !bare.length return (
{CATALOG_DRIFT_TEXT.section}
- {!rows.length ?
{CATALOG_DRIFT_TEXT.empty}
: ( + {empty ?
{CATALOG_DRIFT_TEXT.empty}
: ( <> -
    - {rows.map(({ source, table, v }) => { - const c = changed.get(`${source}/${table}`) - return ( -
  • - {table} - {CATALOG_DRIFT_TEXT.version(v)} - {c && ( - - {CATALOG_DRIFT_TEXT.changedSince(CATALOG_DRIFT_TEXT.version(c.current))} - - )} -
  • - ) - })} -
+ {direct.length > 0 && list('direct', direct)} + {(possible.length > 0 || bare.length > 0) && ( +
+
{CATALOG_DRIFT_TEXT.possibleHead}
+ {possible.length > 0 && list('possible', possible)} + {bare.map((source) => ( +
{CATALOG_DRIFT_TEXT.possibleNone(source)}
+ ))} +
+ )}
{changes.length ? CATALOG_DRIFT_TEXT.changedCount(changes.length) : CATALOG_DRIFT_TEXT.unchanged} diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 3bc567b2..689a7c01 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2224,26 +2224,40 @@ export const CATALOG_IMPACT_TEXT = { } /** - * 发布时的目录版本:发布时记下这一版 SQL 用到的表的数据目录版本;从这一版发起正式运行时目录有变化,提醒而不拦。 + * 发布时的目录版本:发布时记下这一版用到的表的数据目录版本;从这一版发起正式运行时目录有变化,提醒而不拦。 * 版本号写「第 N 版」,还没有目录的表写「尚无目录」。 + * + * 和影响面(CATALOG_IMPACT_TEXT)同一个分法:SQL 里写着的表是直接引用,说「有变化」;Agent 绑定了数据源的查询工具、 + * SQL 运行时才写的,记的是这个数据源中所有有目录的表,说「Agent 可能查询的表有变化」——两组分开写,不混成一句。 */ export const CATALOG_DRIFT_TEXT = { /** 运行里的提醒:「自发布以来,数据目录中「入园记录」等 2 张表有变化」 */ title: (first: string, n: number) => (n > 1 ? `自发布以来,数据目录中「${first}」等 ${formatNumber(n)} 张表有变化` : `自发布以来,数据目录中「${first}」有变化`), + /** 只有 Agent 可能查询的表变了:「自发布以来,Agent 可能查询的表有变化:「入园记录」等 3 张」 */ + possibleTitle: (first: string, n: number) => (n > 1 + ? `自发布以来,Agent 可能查询的表有变化:「${first}」等 ${formatNumber(n)} 张` + : `自发布以来,Agent 可能查询的表有变化:「${first}」`), + /** 两组都有:直接引用的标题后面补一句 */ + alsoPossible: (n: number) => `;另有 ${formatNumber(n)} 张 Agent 可能查询的表有变化`, sub: '运行未被拦截。请核对这些表的目录修改是否影响本次结果', table: (label: string | null, table: string) => (label ? `${label}(${table})` : table), version: (v: number | null) => (v ? `第 ${formatNumber(v)} 版` : '尚无目录'), line: (source: string, table: string, from: string, to: string) => `「${source}」${table}:发布时${from},现为${to}`, open: '查看目录', + /** 可能涉及那一组的小标题和说明(运行横幅、时间线展开、版本历史共用) */ + possibleHead: 'Agent 可能查询的表', + possibleHint: 'Agent 绑定了这些数据源的查询工具,SQL 在运行时生成:发布时记下了数据源中所有有目录的表,不一定都会用到', // ---- 版本历史里的「发布时的目录版本」 section: '发布时的目录版本', - sectionHint: '这一版 SQL 用到的表在发布时的数据目录版本', + sectionHint: '这一版 SQL 用到的表,以及 Agent 可能查询的表,在发布时的数据目录版本', changedSince: (now: string) => `之后有变化,现为${now}`, changedCount: (n: number) => `${formatNumber(n)} 张表在发布之后有变化,下次正式运行时会提醒`, unchanged: '发布之后这些表的目录没有变化', empty: '这一版的 SQL 没有用到数据库表', + /** 可能涉及的数据源在发布时一张有目录的表都没有 */ + possibleNone: (source: string) => `「${source}」发布时还没有目录`, } // =========================================================================== diff --git a/frontend/src/pages/runs/Banners.tsx b/frontend/src/pages/runs/Banners.tsx index 28643dad..06d45f43 100644 --- a/frontend/src/pages/runs/Banners.tsx +++ b/frontend/src/pages/runs/Banners.tsx @@ -5,6 +5,7 @@ import { CopyButton, Spinner, StatusBadge } from '../../components/ui' import { formatDateTime, formatSpan, formatTime } from '../../lib/format' import type { Approval, CatalogDriftTable } from '../../types' import { CATALOG_DRIFT_TEXT, CATALOG_IMPACT_TEXT } from '../../lib/terms' +import { catalogDriftLine } from '../../run/decode' import type { RunErrorExplain } from '../../lib/explain' import { LONG_WAIT_MS, ageMs } from './model' import { copyText } from './parts' @@ -28,12 +29,14 @@ function Shell({ color, children, data }: { color: string; children: ReactNode; // ------------------------------------------------------------------------- /** - * 从发布版本发起的正式运行,开始时发现这一版 SQL 用到的表的数据目录在发布之后改过。运行照常(目录是说明, + * 从发布版本发起的正式运行,开始时发现这一版用到的表的数据目录在发布之后改过。运行照常(目录是说明, * 不是运行的输入),但看结果的人要知道:「在园人数」在发布之后被改成了存量,这一版的 SQL 可能还在跨天相加。 - * 每张表给到数据目录的入口;表多时只列前几张 + * SQL 里写着的表在前;Agent 可能查询的表(Agent 运行时自己写 SQL)另起一组、带小标题,和直接引用分开。 + * 每张表给到数据目录的入口;每组表多时只列前几张 */ -export function CatalogDriftBanner({ drift }: { drift: { title: string; tables: CatalogDriftTable[] } }) { - const shown = drift.tables.slice(0, DRIFT_SHOWN) +export function CatalogDriftBanner({ drift }: { + drift: { title: string; direct: CatalogDriftTable[]; possible: CatalogDriftTable[] } +}) { return (
@@ -41,29 +44,41 @@ export function CatalogDriftBanner({ drift }: { drift: { title: string; tables:

{drift.title}

{CATALOG_DRIFT_TEXT.sub}

-
    - {shown.map((t) => ( -
  • - - {CATALOG_DRIFT_TEXT.line(t.source, CATALOG_DRIFT_TEXT.table(t.label, t.table), - CATALOG_DRIFT_TEXT.version(t.published), CATALOG_DRIFT_TEXT.version(t.current))} - - {t.source_id && ( - - {CATALOG_DRIFT_TEXT.open} - - )} -
  • - ))} - {drift.tables.length > shown.length &&
  • {CATALOG_IMPACT_TEXT.more(drift.tables.length - shown.length)}
  • } -
+ {drift.direct.length > 0 && } + {drift.possible.length > 0 && ( +
+

{CATALOG_DRIFT_TEXT.possibleHead}

+

{CATALOG_DRIFT_TEXT.possibleHint}

+ +
+ )}
) } +function DriftTables({ tables }: { tables: CatalogDriftTable[] }) { + const shown = tables.slice(0, DRIFT_SHOWN) + return ( +
    + {shown.map((t) => ( +
  • + {catalogDriftLine(t)} + {t.source_id && ( + + {CATALOG_DRIFT_TEXT.open} + + )} +
  • + ))} + {tables.length > shown.length &&
  • {CATALOG_IMPACT_TEXT.more(tables.length - shown.length)}
  • } +
+ ) +} + const DRIFT_SHOWN = 6 // ------------------------------------------------------------------------- diff --git a/frontend/src/run/decode.ts b/frontend/src/run/decode.ts index bf3a959e..8bea3225 100644 --- a/frontend/src/run/decode.ts +++ b/frontend/src/run/decode.ts @@ -2556,25 +2556,45 @@ function labelsOf(ops: CopilotOp[], fallback?: (id: string) => string | undefine return (id) => map.get(id) ?? fallback?.(id) } +/** 一张表的那一行:「scenic」入园记录(visits):发布时第 3 版,现为第 5 版 */ +export function catalogDriftLine(t: CatalogDriftTable): string { + return CATALOG_DRIFT_TEXT.line(t.source, CATALOG_DRIFT_TEXT.table(t.label, t.table), + CATALOG_DRIFT_TEXT.version(t.published), CATALOG_DRIFT_TEXT.version(t.current)) +} + /** - * catalog.drift 事件 → 提醒的标题和逐表明细。标题按第一张表的中文名说(「入园记录」等 2 张表),明细每张表一行: - * 发布时第几版、现在第几版(0 写「尚无目录」)。认不出的形状返回 null + * catalog.drift 事件 → 提醒的标题和逐表明细。每张表带 impact:direct 是 SQL 里写着的表,possible 是 Agent 可能 + * 查询的表(Agent 绑定了数据源的查询工具,SQL 运行时才生成),两组分开说: + * - 标题按第一张直接引用的表说(「入园记录」等 2 张表有变化),还有可能涉及的补一句「另有 N 张 Agent 可能查询的表 + * 有变化」;只有可能涉及的,标题直说「Agent 可能查询的表有变化」 + * - 明细每张表一行:发布时第几版、现在第几版(0 写「尚无目录」);可能涉及的放在小标题下面 + * 老事件的表没有 impact,按直接引用。认不出的形状返回 null */ -export function catalogDrift(d: Record): { title: string; detail: string; tables: CatalogDriftTable[] } | null { +export function catalogDrift(d: Record): { + title: string; detail: string; tables: CatalogDriftTable[]; direct: CatalogDriftTable[]; possible: CatalogDriftTable[] +} | null { const tables: CatalogDriftTable[] = (Array.isArray(d?.tables) ? d.tables : []) .filter((t: any) => t && typeof t === 'object' && typeof t.table === 'string' && typeof t.source === 'string') .map((t: any) => ({ source: t.source, source_id: String(t.source_id ?? ''), table: t.table, label: typeof t.label === 'string' && t.label ? t.label : null, published: num(t.published) ?? null, current: num(t.current) ?? 0, + impact: t.impact === 'possible' ? 'possible' : 'direct', })) if (!tables.length) return null - const first = tables[0].label ?? tables[0].table + const direct = tables.filter((t) => t.impact === 'direct') + const possible = tables.filter((t) => t.impact === 'possible') + const name = (t: CatalogDriftTable) => t.label ?? t.table + const title = direct.length + ? CATALOG_DRIFT_TEXT.title(name(direct[0]), direct.length) + (possible.length ? CATALOG_DRIFT_TEXT.alsoPossible(possible.length) : '') + : CATALOG_DRIFT_TEXT.possibleTitle(name(possible[0]), possible.length) return { - title: CATALOG_DRIFT_TEXT.title(first, tables.length), - detail: tables.map((t) => CATALOG_DRIFT_TEXT.line(t.source, CATALOG_DRIFT_TEXT.table(t.label, t.table), - CATALOG_DRIFT_TEXT.version(t.published), CATALOG_DRIFT_TEXT.version(t.current))).join('\n'), - tables, + title, + detail: [ + ...direct.map(catalogDriftLine), + ...(possible.length ? [`${CATALOG_DRIFT_TEXT.possibleHead}:`, ...possible.map(catalogDriftLine)] : []), + ].join('\n'), + tables, direct, possible, } } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1798b3b3..6fb841b9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -56,12 +56,23 @@ export interface WorkflowVersion { input_fields?: { name: string; required?: boolean; [key: string]: any }[] /** 是不是当前的已发布版本 */ published?: boolean - /** 发布时这一版 SQL 用到的表的数据目录版本 {源名: {表名: 版本}},0 表示当时还没有目录;没记过为 null */ - catalog_versions?: Record> | null + /** 发布时记下的数据目录版本,没记过为 null */ + catalog_versions?: CatalogVersions | null /** 发布之后目录有变化的表 */ catalog_changes?: CatalogDriftTable[] } +/** + * 发布时记下的数据目录版本(服务端 catalog_impact.recorded_versions),两组都是 {源名: {表名: 版本}}: + * - direct:SQL 里写着的表,0 表示当时还没有目录 + * - possible:Agent 绑定了查询工具的数据源中所有有目录的表(SQL 运行时才生成);源下为空表示当时一张有目录的 + * 表都没有。只记了直接引用的老版本这一组为空 + */ +export interface CatalogVersions { + direct: Record> + possible: Record> +} + /** 发布之后数据目录有变化的一张表(服务端 catalog_impact.catalog_drift;运行里的 catalog.drift 事件同形) */ export interface CatalogDriftTable { source: string @@ -72,6 +83,8 @@ export interface CatalogDriftTable { /** 发布时的版本,0 表示当时还没有目录 */ published: number | null current: number + /** direct:SQL 里写着这张表;possible:Agent 可能查询的表。老事件没有这一项,按 direct */ + impact: 'direct' | 'possible' } /** diff --git a/scripts/check-decode.mjs b/scripts/check-decode.mjs index d0d05ee4..0bf2a0a8 100644 --- a/scripts/check-decode.mjs +++ b/scripts/check-decode.mjs @@ -737,6 +737,26 @@ await section('正式运行:发布之后数据目录有变化(catalog.drift && row.detail.includes('「scenic」parks:发布时尚无目录,现为第 1 版'), row?.detail) const one = mod.catalogDrift({ tables: [{ source: 's', table: 't', label: null, published: 1, current: 2 }] }) check('只有一张表:标题不写「等」', one?.title === '自发布以来,数据目录中「t」有变化', one?.title) + check('老事件的表没有 impact:按直接引用', one?.direct.length === 1 && one?.possible.length === 0 && one?.tables[0].impact === 'direct') + // Agent 可能查询的表(SQL 运行时才写):和直接引用分开说 + const mixed = mod.catalogDrift({ tables: [ + { source: 'scenic', source_id: 'src-scenic', table: 'visits', label: '入园记录', published: 3, current: 5, impact: 'direct' }, + { source: 'scenic', source_id: 'src-scenic', table: 'orders', label: '订单', published: 2, current: 4, impact: 'possible' }, + { source: 'scenic', source_id: 'src-scenic', table: 'gates', label: null, published: 0, current: 1, impact: 'possible' }, + ] }) + check('两组都有:标题说直接引用的,再补一句 Agent 可能查询的表有变化', + mixed?.title === '自发布以来,数据目录中「入园记录」有变化;另有 2 张 Agent 可能查询的表有变化', mixed?.title) + check('……展开:直接引用在前,可能涉及的在小标题下面', mixed?.detail === [ + '「scenic」入园记录(visits):发布时第 3 版,现为第 5 版', 'Agent 可能查询的表:', + '「scenic」订单(orders):发布时第 2 版,现为第 4 版', '「scenic」gates:发布时尚无目录,现为第 1 版'].join('\n'), mixed?.detail) + const agentOnly = mod.catalogDrift({ tables: [ + { source: 'scenic', source_id: 'src-scenic', table: 'orders', label: '订单', published: 2, current: 4, impact: 'possible' }, + ] }) + check('只有可能涉及的:标题直说「Agent 可能查询的表有变化」', agentOnly?.title === '自发布以来,Agent 可能查询的表有变化:「订单」', agentOnly?.title) + const run = mod.decodeRun([{ seq: 1, type: 'catalog.drift', node_id: null, ts: 1, data: { version: 2, count: 1, tables: [ + { source: 'scenic', source_id: 'src-scenic', table: 'orders', label: '订单', published: 2, current: 4, impact: 'possible' }] } }]) + .find((s) => s.code === 'catalog_drift') + check('……时间线里那一行同样是警告', run?.level === 'warn' && run?.title === agentOnly?.title, run?.title) check('形状不对不出行', mod.decodeRun([{ seq: 1, type: 'catalog.drift', node_id: null, ts: 1, data: { tables: 'x' } }]) .every((s) => s.code !== 'catalog_drift')) }) diff --git a/scripts/check-runs.mjs b/scripts/check-runs.mjs index c86ba48c..fc4e9ef4 100644 --- a/scripts/check-runs.mjs +++ b/scripts/check-runs.mjs @@ -706,9 +706,11 @@ await section('正式运行:发布之后数据目录有变化,提醒但不 ], edges: [{ source: 'a', target: 'q' }] } const events = [ { seq: 1, type: 'run.started', node_id: null, ts: t, data: { nodes: 2 } }, - { seq: 2, type: 'catalog.drift', node_id: null, ts: t + 0.01, data: { version: 3, count: 2, tables: [ - { source: 'scenic', source_id: 'src-scenic', table: 'visits', label: '入园记录', published: 3, current: 5 }, - { source: 'scenic', source_id: 'src-scenic', table: 'parks', label: null, published: 0, current: 1 }, + { seq: 2, type: 'catalog.drift', node_id: null, ts: t + 0.01, data: { version: 3, count: 3, tables: [ + { source: 'scenic', source_id: 'src-scenic', table: 'visits', label: '入园记录', published: 3, current: 5, impact: 'direct' }, + { source: 'scenic', source_id: 'src-scenic', table: 'parks', label: null, published: 0, current: 1, impact: 'direct' }, + // Agent 绑定了这个源的查询工具:SQL 运行时才写,记的是源里有目录的表 + { source: 'scenic', source_id: 'src-scenic', table: 'orders', label: '订单', published: 2, current: 4, impact: 'possible' }, ] } }, { seq: 3, type: 'node.started', node_id: 'a', ts: t + 0.1, data: {} }, { seq: 4, type: 'node.finished', node_id: 'a', ts: t + 0.2, data: { duration_ms: 100 } }, @@ -723,11 +725,20 @@ await section('正式运行:发布之后数据目录有变化,提醒但不 const banner = page.locator('[data-run-banner="catalog-drift"]') check('详情显示目录变化的横幅', await banner.waitFor({ timeout: 5000 }).then(() => true, () => false)) const text = await banner.innerText().catch(() => '') - check('横幅标题:「入园记录」等 2 张表有变化', text.includes('自发布以来,数据目录中「入园记录」等 2 张表有变化'), text.split('\n')[0]) + check('横幅标题:「入园记录」等 2 张表有变化,另有 1 张 Agent 可能查询的表有变化', + text.includes('自发布以来,数据目录中「入园记录」等 2 张表有变化;另有 1 张 Agent 可能查询的表有变化'), text.split('\n')[0]) check('写明运行未被拦截', text.includes('运行未被拦截')) check('逐表写发布时和现在的版本,没有目录写「尚无目录」', text.includes('「scenic」入园记录(visits):发布时第 3 版,现为第 5 版') && text.includes('「scenic」parks:发布时尚无目录,现为第 1 版'), text.replace(/\s+/g, ' ')) check('每张表给到数据目录的入口', (await banner.locator('[data-drift-open="visits"]').getAttribute('href')) === '/data/catalog/src-scenic/visits') + const possible = banner.locator('[data-drift-possible]') + const possibleText = await possible.innerText().catch(() => '') + check('Agent 可能查询的表另起一组:小标题、说明、逐表一行', await possible.count() === 1 + && possibleText.includes('Agent 可能查询的表') && possibleText.includes('SQL 在运行时生成') + && possibleText.includes('「scenic」订单(orders):发布时第 2 版,现为第 4 版'), possibleText.replace(/\s+/g, ' ')) + check('……直接引用的表不在这一组里', await possible.locator('[data-drift-table="visits"]').count() === 0 + && (await banner.locator('[data-drift-table="orders"]').getAttribute('data-drift-impact')) === 'possible') + check('……也给到数据目录的入口', (await banner.locator('[data-drift-open="orders"]').getAttribute('href')) === '/data/catalog/src-scenic/orders') check('运行照常完成:没有失败横幅', await page.locator('[data-run-banner=failed]').count() === 0) const row = page.locator('[data-step-code="catalog_drift"]') check('时间线里也有一行提醒', await row.count() === 1 && (await row.innerText()).includes('等 2 张表有变化')) diff --git a/scripts/check-studio.mjs b/scripts/check-studio.mjs index 02bd4b84..90cba8cc 100644 --- a/scripts/check-studio.mjs +++ b/scripts/check-studio.mjs @@ -263,11 +263,17 @@ async function open({ width = 1440, height = 900, path = '/studio/st-main', pref if (sub === 'versions' && !v) return json(route, VERSIONS) if (sub === 'versions' && v) { const n = Number(v) - // 已发布的 v2 记着发布时的目录版本:入园记录之后改过(第 3 版 → 第 5 版),景区表没变 + // 已发布的 v2 记着发布时的目录版本:入园记录之后改过(第 3 版 → 第 5 版),景区表没变; + // Agent 绑定了 shop 的查询工具,可能查询的表里订单改过、闸机是发布之后才建的目录,另一个源发布时还没有目录 return json(route, { ...VERSIONS.find((x) => x.version === n), workflow_id: id, graph: n === 1 ? V1 : GRAPH, graph_hash: 'x', input_fields: [{ name: 'goal' }], published: n === 2, - catalog_versions: n === 2 ? { shop: { parks: 1, visits: 3 } } : null, - catalog_changes: n === 2 ? [{ source: 'shop', source_id: 'src-shop', table: 'visits', label: '入园记录', published: 3, current: 5 }] : [] }) + catalog_versions: n === 2 + ? { direct: { shop: { parks: 1, visits: 3 } }, possible: { shop: { orders: 2, channels: 1 }, crm: {} } } : null, + catalog_changes: n === 2 ? [ + { source: 'shop', source_id: 'src-shop', table: 'visits', label: '入园记录', published: 3, current: 5, impact: 'direct' }, + { source: 'shop', source_id: 'src-shop', table: 'orders', label: '订单', published: 2, current: 4, impact: 'possible' }, + { source: 'shop', source_id: 'src-shop', table: 'gates', label: null, published: 0, current: 1, impact: 'possible' }, + ] : [] }) } if (method === 'GET' && !sub) return state.deleted.has(id) ? json(route, { detail: '工作流不存在,可能已被删除' }, 404) : json(route, FAKES[id]) if (method === 'DELETE' && !sub) { @@ -1011,8 +1017,20 @@ await section('版本历史:预览、恢复成一次可撤销的改动', async const catText = await cat.innerText().catch(() => '') check('已发布的版本写「发布时的目录版本」', catText.includes('发布时的目录版本') && catText.includes('parks') && catText.includes('第 1 版'), catText) check('之后改过的表标出来', (await page.locator('[data-version-catalog-table="visits"]').getAttribute('data-changed')) === 'true' - && catText.includes('之后有变化,现为第 5 版') && catText.includes('1 张表在发布之后有变化'), catText.replace(/\s+/g, ' ')) + && catText.includes('之后有变化,现为第 5 版') && catText.includes('3 张表在发布之后有变化'), catText.replace(/\s+/g, ' ')) check('没改过的表不标', (await page.locator('[data-version-catalog-table="parks"]').getAttribute('data-changed')) === null) + // Agent 可能查询的表:另起一组,和 SQL 里写着的表分开 + const possible = cat.locator('[data-version-catalog-possible]') + const possibleText = await possible.innerText().catch(() => '') + check('Agent 可能查询的表另起一组', await possible.count() === 1 && possibleText.includes('Agent 可能查询的表') + && await possible.locator('[data-version-catalog-table="visits"]').count() === 0, possibleText.replace(/\s+/g, ' ')) + check('……改过的标出来,没改过的不标', (await possible.locator('[data-version-catalog-table="orders"]').getAttribute('data-changed')) === 'true' + && (await possible.locator('[data-version-catalog-table="channels"]').getAttribute('data-changed')) === null + && possibleText.includes('之后有变化,现为第 4 版'), possibleText.replace(/\s+/g, ' ')) + check('……发布之后才建目录的表也列出来,发布时写「尚无目录」', + (await possible.locator('[data-version-catalog-table="gates"]').getAttribute('data-changed')) === 'true' + && (await possible.locator('[data-version-catalog-table="gates"]').innerText()).includes('尚无目录')) + check('……发布时一张有目录的表都没有的源写一句', possibleText.includes('「crm」发布时还没有目录'), possibleText.replace(/\s+/g, ' ')) await page.locator('ol[aria-label="版本"] > li').nth(2).locator('button').first().click() await page.waitForTimeout(300) check('没发布过的版本没有目录版本这一栏', await count(page, '[data-version-catalog]') === 0)