From 78800511e0d0e3fca249b7efd9f18b2b5c9f056e Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 18:17:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=8D=A1=E8=8A=82=E7=82=B9=EF=BC=88=E5=88=86=E6=9E=90=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E4=B8=80=E8=BD=AE=EF=BC=89=E3=80=82=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E5=BA=93=E3=80=8C=E6=8A=8A=E5=85=B3=E3=80=8D=E4=B8=80=E7=B1=BB?= =?UTF-8?q?=E5=8A=A0=E3=80=8C=E5=88=86=E6=9E=90=E5=8D=A1=E3=80=8D=EF=BC=88?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=89=B2=20--nt-analysis=EF=BC=8C=E4=BA=AE?= =?UTF-8?q?=E6=9A=97=E4=B8=A4=E5=A5=97=EF=BC=89=EF=BC=8C=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=91=98=E8=A6=81=E5=86=99=E5=81=9A=E5=93=AA=E5=87=A0=E7=A7=8D?= =?UTF-8?q?=E5=88=86=E6=9E=90=EF=BC=88=E3=80=8C=E6=B3=A2=E5=8A=A8=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF=E3=80=81=E5=8F=98=E5=8C=96=E6=8B=86=E8=A7=A3=EF=BC=88?= =?UTF-8?q?=E5=8F=AF=E5=8A=A0=EF=BC=89=E3=80=8D=EF=BC=8C=E5=8F=AB=E6=B3=95?= =?UTF-8?q?=E7=85=A7=E5=A5=91=E7=BA=A6=EF=BC=89=EF=BC=9B=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E5=88=86=E6=9E=90=E9=A1=B9=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=EF=BC=9A=E6=AF=8F=E9=A1=B9=E6=A0=87=E8=AF=86=E3=80=81?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=96=B9=E6=B3=95=EF=BC=88=E4=BA=94=E4=B8=AA?= =?UTF-8?q?=E7=AE=97=E5=AD=90=EF=BC=89=E3=80=81=E6=A8=A1=E5=BC=8F=EF=BC=88?= =?UTF-8?q?=E6=9C=89=E6=A8=A1=E5=BC=8F=E7=9A=84=E6=89=8D=E7=BB=99=EF=BC=89?= =?UTF-8?q?=E3=80=81=E8=BE=93=E5=85=A5=E5=8F=AA=E5=88=97=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E7=9A=84=E6=9F=A5=E8=AF=A2=E8=8A=82=E7=82=B9=EF=BC=88=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9F=A5=E8=AF=A2=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9A=84=E3=80=8C=E8=B0=83=E7=94=A8=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E3=80=8D=E8=8A=82=E7=82=B9=E3=80=81=E5=90=88=E5=B9=B6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=EF=BC=8C=E5=88=A0=E6=8E=89=E6=88=96=E6=8C=AA=E5=88=B0?= =?UTF-8?q?=E4=B8=8B=E6=B8=B8=E7=9A=84=E7=85=A7=E5=AE=9E=E5=88=97=E5=87=BA?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=8C=89=E6=96=B9=E6=B3=95=E6=91=86=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=A0=BC=EF=BC=88analysisItems.paramSpecs=EF=BC=9A?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=E5=88=97=E3=80=81=E6=9C=9F=E6=AC=A1=E3=80=81?= =?UTF-8?q?=E5=9F=BA=E6=9C=9F=E3=80=81=E6=9C=AC=E6=9C=9F=E3=80=81=E5=BA=A6?= =?UTF-8?q?=E9=87=8F=E5=88=97=E6=88=96=E5=88=86=E5=AD=90=E5=88=86=E6=AF=8D?= =?UTF-8?q?=E3=80=81=E4=B9=98=E7=A7=AF=E5=92=8C=E5=9B=A0=E5=AD=90=E3=80=81?= =?UTF-8?q?=E6=AF=94=E8=BE=83=E6=96=B9=E5=BC=8F=E3=80=81=E7=B2=92=E5=BA=A6?= =?UTF-8?q?=E3=80=81=E5=AF=B9=E7=85=A7=E7=BB=84=E3=80=81=E5=87=A0=E5=AF=B9?= =?UTF-8?q?=E2=80=A6=E2=80=A6=EF=BC=8C=E6=9C=9F=E6=AC=A1=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E5=86=99=20{{=20}}=EF=BC=89=EF=BC=8C=E5=8F=AF=E5=8A=A0?= =?UTF-8?q?=E3=80=81=E6=AF=94=E7=8E=87=E7=9A=84=E5=8F=98=E5=8C=96=E6=8B=86?= =?UTF-8?q?=E8=A7=A3=E5=8F=A6=E6=9C=89=E6=80=BB=E6=95=B0=E6=A0=B8=E5=AF=B9?= =?UTF-8?q?=E4=B8=A4=E6=A0=BC=E5=92=8C=E4=B8=8B=E9=92=BB=EF=BC=88=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E5=B1=82=E5=8F=AA=E5=88=97=E6=8E=92=E5=9C=A8=E5=89=8D?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E5=8F=98=E5=8C=96=E6=8B=86=E8=A7=A3=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=8F=98=E5=8C=96=E6=8B=86=E8=A7=A3=E3=80=81=E5=8F=AF?= =?UTF-8?q?=E6=AF=94=E6=80=A7=E6=A3=80=E6=9F=A5=E5=8F=AF=E4=BB=A5=E5=86=99?= =?UTF-8?q?=E6=88=90=E5=9F=BA=E6=9C=9F=E3=80=81=E6=9C=AC=E6=9C=9F=E4=B8=A4?= =?UTF-8?q?=E4=BB=BD=E6=9F=A5=E8=AF=A2=EF=BC=8C=E6=8D=A2=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E3=80=81=E6=A8=A1=E5=BC=8F=E6=97=B6=E5=8F=AA=E7=95=99=E6=96=B0?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=94=A8=E5=BE=97=E5=88=B0=E7=9A=84=E5=8F=82?= =?UTF-8?q?=E6=95=B0=EF=BC=9B=E5=9C=A8=E9=AA=8C=E8=AF=81=E4=BB=80=E4=B9=88?= =?UTF-8?q?=E4=B8=80=E5=8F=A5=E8=AF=9D=EF=BC=9B=E9=98=88=E5=80=BC=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E5=8F=AA=E5=88=97=E5=86=99=E4=BA=86=E7=9A=84=E5=87=A0?= =?UTF-8?q?=E9=A1=B9=E3=80=81=E4=BB=8E=E6=B2=A1=E5=86=99=E7=9A=84=E9=87=8C?= =?UTF-8?q?=E9=80=89=EF=BC=8C=E8=8C=83=E5=9B=B4=E5=BD=93=E5=9C=BA=E6=8C=89?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E8=AF=B4=EF=BC=88=E4=B8=A4=E4=B8=A4=E4=B9=8B?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E5=A4=A7=E5=B0=8F=E4=BA=A4=E7=BB=99=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=EF=BC=89=EF=BC=8C=E5=85=A8=E5=88=A0=E6=8E=89?= =?UTF-8?q?=E4=B8=8D=E7=95=99=E7=A9=BA=E5=AF=B9=E8=B1=A1=E3=80=82=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E6=8C=89=20items[i].<=E9=94=AE>=20=E6=8A=A5?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E8=90=BD=E5=9C=A8=E9=82=A3=E4=B8=80?= =?UTF-8?q?=E6=A0=BC=E4=B8=8B=E9=9D=A2=EF=BC=8C=E5=AE=9A=E4=BD=8D=E9=80=80?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E7=9A=84=E5=AD=90=E9=94=AE=EF=BC=88params.pa?= =?UTF-8?q?rent.key=EF=BC=89=E9=80=90=E6=AE=B5=E5=BE=80=E4=B8=8A=E9=80=80?= =?UTF-8?q?=EF=BC=9B=E6=9F=A5=E8=AF=A2=E8=8A=82=E7=82=B9=E8=BF=9E=E5=88=B0?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=8D=A1=E6=97=B6=E8=BF=98=E6=B2=A1=E9=80=89?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E7=9A=84=E7=AC=AC=E4=B8=80=E9=A1=B9=E9=80=89?= =?UTF-8?q?=E4=B8=8A=E5=AE=83=EF=BC=88=E5=90=8C=E5=90=88=E5=B9=B6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=EF=BC=8C=E5=92=8C=E8=BF=9E=E7=BA=BF=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E6=92=A4=E9=94=80=EF=BC=89=E3=80=82=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=8A=A0=E3=80=8C=E5=88=86=E6=9E=90=E8=AF=B4?= =?UTF-8?q?=E6=B3=95=E6=A0=B8=E5=AF=B9=E3=80=8D=EF=BC=88=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E9=87=8D=E5=86=99=E4=B8=80=E6=AC=A1=E5=90=8E=E6=A0=87=E5=87=BA?= =?UTF-8?q?=EF=BC=89=E3=80=81=E3=80=8C=E5=88=86=E6=9E=90=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E3=80=8D=EF=BC=88=E9=AB=98=E7=BA=A7=EF=BC=8C=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E6=9C=89=E5=88=86=E6=9E=90=E7=BB=93=E8=AE=BA=E6=97=B6=E9=99=84?= =?UTF-8?q?=E4=B8=8A=EF=BC=89=EF=BC=8C=E9=80=89=E9=A1=B9=E4=BB=A5=20engine?= =?UTF-8?q?/labels.py=20=E7=9A=84=E7=9F=AD=E8=AF=B4=E6=B3=95=E5=BC=80?= =?UTF-8?q?=E5=A4=B4=E3=80=82=E5=8F=AB=E6=B3=95=E3=80=81=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E5=8F=AA=E7=85=A7=E5=A5=91=E7=BA=A6=E4=B8=80=E4=BB=BD=EF=BC=88?= =?UTF-8?q?lib/termsAnalysis=EF=BC=9AOP=5FLABELS=20/=20MODE=5FLABELS=20/?= =?UTF-8?q?=20LABELS=20/=20RANGES=EF=BC=8Ccheck-studio=20=E8=AF=BB?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=BA=90=E7=A0=81=E9=80=90=E9=A1=B9=E6=AF=94?= =?UTF-8?q?=E5=AF=B9=EF=BC=89=E3=80=82check-studio=20=E5=8A=A0=E4=B8=A4?= =?UTF-8?q?=E8=8A=82=EF=BC=9A=E5=8F=AB=E6=B3=95=E4=B8=8E=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E3=80=81=E5=88=86=E6=9E=90=E5=8D=A1=E7=9A=84?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E7=BC=96=E8=BE=91=E4=B8=8E=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/canvas/Inspector.tsx | 380 ++++++++++++++++++++++-- frontend/src/canvas/InspectorSheet.tsx | 9 +- frontend/src/canvas/NodeCard.tsx | 4 + frontend/src/canvas/analysisItems.ts | 165 +++++++++++ frontend/src/canvas/nodeDefs.ts | 41 ++- frontend/src/index.css | 4 + frontend/src/lib/terms.ts | 2 + frontend/src/lib/termsAnalysis.ts | 391 +++++++++++++++++++++++++ frontend/src/store/studio.ts | 6 +- frontend/src/types.ts | 258 +++++++++++++++- scripts/check-studio.mjs | 226 ++++++++++++++ 11 files changed, 1454 insertions(+), 32 deletions(-) create mode 100644 frontend/src/canvas/analysisItems.ts create mode 100644 frontend/src/lib/termsAnalysis.ts diff --git a/frontend/src/canvas/Inspector.tsx b/frontend/src/canvas/Inspector.tsx index d0a099e8..e67938bc 100644 --- a/frontend/src/canvas/Inspector.tsx +++ b/frontend/src/canvas/Inspector.tsx @@ -15,7 +15,15 @@ import { formatShortcut } from '../lib/keys' import { INPUT_FIELD_TEXT, JUDGE_FIELD_LABEL, JUDGE_ON_UNSUPPORTED_HINT, JUDGE_ON_UNSUPPORTED_LABEL, JUDGE_SETTING_TEXT, MERGE_TEXT, CATALOG_UI_TEXT, NODE_TYPE_LABEL, PROVIDER_REF_TEXT, SUBGRAPH_UPGRADE_HELP, judgeByModelText, upgradeNewerText } from '../lib/terms' import { TemplateText } from './TemplateText' import { MERGE_ALIAS, isQueryNode, mergeAlias } from './mergeInputs' -import type { NodeType, Provider, ValidationIssue, WorkflowVersion } from '../types' +import { + ITEM_ID, TWO_QUERY_SKIP, allowsHeadline, allowsParent, allowsTwoQueries, itemsOf, nextItemId, paramSpecs, paramText, + paramValue, withMode, withOp, type ParamSpec, +} from './analysisItems' +import { + ANALYSIS_MODES, ANALYSIS_MODE_LABEL, ANALYSIS_OPS, ANALYSIS_OP_LABEL, ANALYSIS_SETTING_KEYS, ANALYSIS_SETTING_LABEL, + ANALYSIS_TEXT, isAnalysisOp, settingRangeText, settingValueProblem, type AnalysisSettingKey, +} from '../lib/termsAnalysis' +import type { AnalysisItem, NodeType, Provider, ValidationIssue, WorkflowVersion } from '../types' /** 检查器里一条落到字段上的问题 */ type FieldIssue = ValidationIssue & { at: FieldRef | null } @@ -151,6 +159,34 @@ function NodeRefsInput({ nodeId, type, value, onChange }: { ) } +/** 上游能当查询输入的节点(选了数据库查询工具的「调用工具」节点、合并查询):合并查询、分析卡共用 */ +function useUpstreamQueries(nodeId: string): { value: string; label: string; hint?: string }[] { + const nodes = useStudio((s) => s.nodes) + const edges = useStudio((s) => s.edges) + return useMemo(() => { + const parents = new Map() + for (const e of edges) parents.set(e.target, [...(parents.get(e.target) ?? []), e.source]) + const seen = new Set() + const stack = [...(parents.get(nodeId) ?? [])] + while (stack.length) { + const id = stack.pop()! + if (seen.has(id) || id === nodeId) continue + seen.add(id) + stack.push(...(parents.get(id) ?? [])) + } + return nodes + .filter((n) => seen.has(n.id) && isQueryNode(n)) + .map((n) => { + const label = String(n.data?.label || n.id) + const tool = String((n.data?.config as Record | undefined)?.tool ?? '') + return { + value: n.id, label: label === n.id ? label : `${label}(${n.id})`, + hint: n.data?.nodeType === 'merge' ? NODE_TYPE_LABEL.merge : tool, + } + }) + }, [nodes, edges, nodeId]) +} + interface MergeRow { alias: string; node: string } /** config.inputs({别名: 节点 id})→ 行。对象的键顺序就是行的顺序 */ @@ -178,28 +214,7 @@ function MergeInputList({ nodeId, value, issues, onChange }: { const nodes = useStudio((s) => s.nodes) const edges = useStudio((s) => s.edges) const uid = useId() - const options = useMemo(() => { - const parents = new Map() - for (const e of edges) parents.set(e.target, [...(parents.get(e.target) ?? []), e.source]) - const seen = new Set() - const stack = [...(parents.get(nodeId) ?? [])] - while (stack.length) { - const id = stack.pop()! - if (seen.has(id) || id === nodeId) continue - seen.add(id) - stack.push(...(parents.get(id) ?? [])) - } - return nodes - .filter((n) => seen.has(n.id) && isQueryNode(n)) - .map((n) => { - const label = String(n.data?.label || n.id) - const tool = String((n.data?.config as Record | undefined)?.tool ?? '') - return { - value: n.id, label: label === n.id ? label : `${label}(${n.id})`, - hint: n.data?.nodeType === 'merge' ? NODE_TYPE_LABEL.merge : tool, - } - }) - }, [nodes, edges, nodeId]) + const options = useUpstreamQueries(nodeId) const external = useMemo(() => mergeRowsOf(value), [value]) const [rows, setRows] = useState(external) @@ -295,6 +310,319 @@ function MergeInputList({ nodeId, value, issues, onChange }: { ) } +const AT = ANALYSIS_TEXT.node + +/** 选一个上游查询节点。已经选了、后来删掉或挪到下游的节点照样列出来并说明,好让人看见并改掉 */ +function QueryPick({ id, value, options, onChange }: { + id: string; value: string; options: { value: string; label: string }[]; onChange: (v: string) => void +}) { + const known = options.some((o) => o.value === value) + return ( + + ) +} + +/** + * 分析卡的分析项(engine/schema.analysis_issues 的配置):每一项一个标识、一种分析方法(有模式的再选模式)、要分析的上游 + * 查询节点(或基期、本期两份查询),再按方法摆参数格(analysisItems.paramSpecs);可加、比率的变化拆解另有总数核对、下钻。 + * 阈值不在这里写(统一写在阈值覆盖里)。服务端按 items[i].<键> 报的问题落在那一格下面,定位按 [data-item] [data-sub] 落进来 + */ +function AnalysisItemList({ nodeId, value, issues, onChange }: { + nodeId: string; value: any; issues: FieldIssue[]; onChange: (v: any) => void +}) { + const uid = useId() + const options = useUpstreamQueries(nodeId) + const items = itemsOf(value) + const commit = (next: AnalysisItem[]) => onChange(next) + const update = (i: number, next: AnalysisItem) => commit(items.map((it, j) => (j === i ? next : it))) + const setParam = (i: number, key: string, v: unknown) => { + const params = { ...(items[i].params ?? {}) } + if (v === undefined) delete params[key] + else params[key] = v + update(i, { ...items[i], params }) + } + const add = () => commit([...items, { id: nextItemId(items), params: {} }]) + return ( +
+ {items.map((item, i) => { + const own = issues.filter((x) => x.at?.index === i) + const at = (sub: string) => own.filter((x) => x.at?.sub === sub || x.at?.sub?.startsWith(`${sub}.`)) + const lines = (sub: string) => at(sub).map((x, k) => ) + const op = isAnalysisOp(item.op) ? item.op : null + const modes = op ? ANALYSIS_MODES[op] : undefined + const params = (item.params && typeof item.params === 'object' ? item.params : {}) as Record + const two = !!op && allowsTwoQueries(op) && ('base_query' in params || 'target_query' in params) + const id = String(item.id ?? '') + const dup = !!id && items.some((other, j) => j !== i && String(other.id ?? '') === id) + const local = !id || !ITEM_ID.test(id) ? AT.idInvalid : dup ? AT.idDuplicate(id) : '' + const tone = own.some((x) => x.level === 'error') || local ? 'error' : own.length ? 'warning' : undefined + const before = items.slice(0, i).filter((it) => it.op === 'decompose' && it.id) + const parent = params.parent && typeof params.parent === 'object' ? params.parent as { of?: string; key?: unknown } : null + const field = (spec: ParamSpec) => { + const fid = `${uid}-${i}-p-${spec.key}` + const label = AT.param[spec.key] ?? spec.key + const text = paramText(spec.kind, params[spec.key]) + const set = (t: string) => setParam(i, spec.key, paramValue(spec.kind, t)) + let control + if (spec.kind === 'basis' || spec.kind === 'grain') { + const opts = spec.kind === 'basis' ? AT.basis : AT.grain + control = ( + + ) + } else if (spec.kind === 'pairs') { + control =