From ac4ea030a8391c48be3fc519761272127cf115f4 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sun, 4 Oct 2026 02:36:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=20F2b=20/?= =?UTF-8?q?=20B3=EF=BC=9A=E5=8F=91=E5=B8=83=E5=89=8D=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E9=87=8C=20SQL=20=E6=A3=80=E6=9F=A5=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=B8=A6=E4=B8=8A=E5=92=8C=E8=AF=81=E6=8D=AE=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E4=B8=AA=E7=BA=A7=E5=88=AB=E6=A0=87=E8=AF=86?= =?UTF-8?q?=EF=BC=88=E9=94=99=E8=AF=AF=20/=20=E6=8F=90=E9=86=92=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=AF=B4=E6=B8=85=E4=B8=A4=E4=B8=AA=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=BA=A7=E5=88=AB=E6=8B=A6=E4=B8=8D=E6=8B=A6=EF=BC=9A?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=BA=A7=E5=9C=A8=E5=B7=B2=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E5=8F=AA=E6=8F=90=E9=86=92=E3=80=81=E4=B8=8D?= =?UTF-8?q?=E6=8B=A6=EF=BC=8C=E5=8F=97=E7=AE=A1=E7=BA=A7=E5=88=AB=E4=BC=9A?= =?UTF-8?q?=E6=8B=A6=E4=B8=8B=EF=BC=9B=E7=BB=93=E8=AE=BA=E9=82=A3=E5=8F=A5?= =?UTF-8?q?=E8=AF=9D=E9=87=8C=E7=82=B9=E5=87=BA=E5=85=B6=E4=B8=AD=E5=87=A0?= =?UTF-8?q?=E5=A4=84=E6=98=AF=20SQL=20=E6=A3=80=E6=9F=A5=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=BA=A7=E9=97=AE=E9=A2=98=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=92=8C=E3=80=8C=E6=B2=A1=E6=8C=87=E5=AE=9A=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E3=80=8D=E8=BF=99=E7=B1=BB=E6=8F=90=E7=A4=BA=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=A0=B7=EF=BC=9B=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC=E7=9A=84?= =?UTF-8?q?=E4=BC=AA=E9=80=A0=E9=97=A8=E7=A6=81=E8=A1=A5=E4=B8=8A=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=8F=A3=E5=BE=84=E5=8D=A1=E7=9A=84=E5=9B=BE=EF=BC=9B?= =?UTF-8?q?=E8=A1=A5=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC=E7=9A=84=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/canvas/PublishDialog.tsx | 27 ++++++++++++++++++++++++--- frontend/src/lib/terms.ts | 5 +++++ scripts/check-publish.mjs | 25 ++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/frontend/src/canvas/PublishDialog.tsx b/frontend/src/canvas/PublishDialog.tsx index a83efba5..087c6fd0 100644 --- a/frontend/src/canvas/PublishDialog.tsx +++ b/frontend/src/canvas/PublishDialog.tsx @@ -12,8 +12,9 @@ import { import { useCatalog } from '../store/catalog' import { Modal, Spinner, toast, useRadioGroup } from '../components/ui' import { humanizeError } from '../lib/errors' -import { PUBLISH_FIX_TEXT as T, WORKFLOW_STATUS_HINT, WORKFLOW_STATUS_LABEL } from '../lib/terms' +import { CATALOG_UI_TEXT as UT, PUBLISH_FIX_TEXT as T, WORKFLOW_STATUS_HINT, WORKFLOW_STATUS_LABEL } from '../lib/terms' import { isSqlCheckCode, sqlRuleLabel } from '../lib/sqlcheck' +import { SqlLevelBadge } from '../run/SqlChecks' import { setLocalActor, useLocalActor } from '../lib/actor' import { autoFixIds, choiceReady, contentSig, fixFieldLabel, fixFor, fixValueLines, fixValueText, isMissingEndpoint, mergeIssues, @@ -443,6 +444,15 @@ export function usePreflight(workflowId: string | undefined, level: PublishLevel } } +/** + * 发布前检查里一条 SQL 检查问题原本的级别。门禁只收错误和提醒两级(提示的依据只是推断,不进门禁),问题的 level 换成了 + * 「拦不拦」:受管档错误级拦(error),其余都是 warning——原来是哪一级看不出来了。错误级只有两条规则 + * (服务端 sqlcheck._ERROR_CODES:一对多关联后重复计算、存量跨期加总),按规则编号认回来 + */ +const SQL_ERROR_CODES = new Set(['fanout_sum', 'stock_summed']) +const publishSqlLevel = (code: unknown): 'error' | 'warning' | null => + (isSqlCheckCode(code) ? (SQL_ERROR_CODES.has(code) ? 'error' : 'warning') : null) + function fixLabelOf(data: PublishCheck | undefined, id: string): string { return data?.fixes.find((f) => f.id === id)?.label ?? '' } @@ -467,6 +477,8 @@ export function Preflight({ pf, gate, onLocate, stale }: { const rows = useMemo(() => (issues ? mergeIssues(issues) : null), [issues]) const errors = (rows ?? []).filter((r) => r.issue.level === 'error').length const warns = (rows ?? []).length - errors + // 已发布档只提醒、不拦的 SQL 检查错误级问题:结论那句话里点出来,别和「没指定模型」这类提示混成一个数 + const sqlErrors = (rows ?? []).filter((r) => r.issue.level !== 'error' && publishSqlLevel(r.issue.code) === 'error').length const canFix = !lock && check.status !== 'unsupported' const autoIds = issues ? autoFixIds(issues, fixes) : [] const labelOf = (id?: string | null) => (id ? nodeOf(id)?.label || id : '') @@ -507,6 +519,7 @@ export function Preflight({ pf, gate, onLocate, stale }: { {gate ? T.gateBlocked(errors) : errors ? T.blocked(errors) : T.passed} {warns > 0 && {errors || gate ? ',' : ';'}{T.warnings(warns)}} + {sqlErrors > 0 && {UT.publishSqlErrors(sqlErrors)}} )} @@ -551,8 +564,9 @@ function PreflightRow({ issue, others, fix, where, pf, onLocate }: { }) { const err = issue.level === 'error' const Icon = err ? XCircle : AlertTriangle + const sqlLevel = publishSqlLevel(issue.code) return ( -
  • +
  • diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 831dd0b8..4694850e 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2533,6 +2533,11 @@ export const CATALOG_UI_TEXT = { patchNote: '说明', // ---- 合并查询的输入:连线时还没选工具、后来才选上的查询节点,一次带出来 mergeAddConnected: (n: number) => `带出连着的 ${formatNumber(n)} 个查询节点`, + // ---- 发布前检查里的 SQL 检查问题:级别标识和证据面板一致,并说清两个发布级别下拦不拦 + publishSqlErrorReminded: 'SQL 检查发现的错误级问题:已发布级别只提醒、不拦发布;受管级别会拦下', + publishSqlErrorBlocked: 'SQL 检查发现的错误级问题:受管级别会拦下发布,请先修改 SQL', + publishSqlWarning: 'SQL 检查的提醒:两个发布级别都只提醒、不拦发布', + publishSqlErrors: (n: number) => `,其中 ${formatNumber(n)} 处是 SQL 检查发现的错误级问题(受管级别会拦下)`, } // =========================================================================== diff --git a/scripts/check-publish.mjs b/scripts/check-publish.mjs index 748b6540..8e094a50 100644 --- a/scripts/check-publish.mjs +++ b/scripts/check-publish.mjs @@ -209,7 +209,7 @@ function lint(graph, level, { followsNever = false } = {}) { issues.push({ level: 'error', node_id: null, code: 'governed.no_contract', fix: id, field: null, message: '受管级别要求至少一个「成果」节点声明出具契约,否则无法判定出具档位' }) fixes.push({ id, code: 'governed.no_contract', node_id: outputs[0].id, kind: 'choice', multiple: true, - label: `为${who(outputs[0])}生成出具契约:核对${report ? who(report) : '「报告撰写」'}的文档,指标来自${who(cards[0])},再选择「必需指标」`, + label: `为${who(outputs[0])}生成出具契约:核对${report ? who(report) : '「报告撰写」'}的文档,指标来自${cards.length ? who(cards[0]) : '「口径卡」'},再选择「必需指标」`, options: cards.flatMap(metricOptions) }) } for (const n of graph.nodes) { @@ -834,6 +834,29 @@ await section('SQL 检查(数据目录阶段 4B):发布前检查写中文 const text = await row.innerText() check('发布弹窗:SQL 检查的问题写节点名和中文规则名,不露规则编号', text.includes('「订单汇总」') && text.includes('一对多关联后重复计算:') && text.includes('会重复计算') && !/fanout_sum|args\.sql/.test(text), text) + // 错误级的 SQL 检查在已发布档只提醒(设计如此),但不能和「没指定模型」这类提示一个样:级别标识和证据面板同一个 + // (SqlLevelBadge:图标 + 「错误」、失败色),并说清已发布级别只提醒、受管级别会拦 + const badge = row.locator('[data-sql-level]') + const badgeLook = await badge.evaluate((el) => { + const probe = document.createElement('span') + probe.style.color = 'var(--st-failed)' + document.body.append(probe) + const failed = getComputedStyle(probe).color + probe.remove() + return { level: el.getAttribute('data-sql-level'), text: el.innerText.trim(), color: getComputedStyle(el).color, failed, icon: !!el.querySelector('svg') } + }).catch(() => null) + check('……带证据面板同一个级别标识:「错误」、失败色、带图标', badgeLook?.level === 'error' && badgeLook.text === '错误' + && badgeLook.color === badgeLook.failed && badgeLook.icon, JSON.stringify(badgeLook)) + check('……写明这是 SQL 检查发现的错误级问题:已发布级别只提醒、不拦,受管级别会拦下', + (await row.locator('[data-preflight-sql-gate="reminded"]').innerText().catch(() => '')) === 'SQL 检查发现的错误级问题:已发布级别只提醒、不拦发布;受管级别会拦下') + check('……结论那句话里点出来,不和别的提示混成一个数', (await dialog(page).locator('[data-preflight-sql-errors="1"]').innerText().catch(() => '')) + .includes('其中 1 处是 SQL 检查发现的错误级问题')) + await dialog(page).locator('[role="radio"]:has-text("受管")').click() + await waitFor(page, async () => (await row.locator('[data-preflight-sql-gate="blocked"]').count()) === 1) + check('换成受管:同一条写「受管级别会拦下发布」,行首是错误', (await row.locator('[data-preflight-sql-gate]').innerText()).includes('受管级别会拦下发布') + && await row.locator('svg[aria-label="错误"]').count() === 1 && await dialog(page).locator('[data-preflight-sql-errors]').count() === 0) + await dialog(page).locator('[role="radio"]:has-text("已发布")').click() + await waitFor(page, async () => (await row.locator('[data-preflight-sql-gate="reminded"]').count()) === 1) await shoot(page, 'preflight-sqlcheck', dialog(page)) await row.locator('button').first().click() const where = () => page.evaluate(() => {