From ee923ef6f82ee27d49d4029b07454b4d0f9faaf7 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 18:35:22 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=AE=E6=95=B0=E6=8D=AE=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=BA=BF=E7=9A=84=E5=88=86=E6=9E=90=E6=AD=A5=E9=AA=A4=E4=B8=8E?= =?UTF-8?q?=E5=87=BA=E5=85=B7=E6=A8=AA=E5=B9=85=E7=9A=84=E4=B8=89=E9=A1=B9?= =?UTF-8?q?=EF=BC=88=E5=88=86=E6=9E=90=E8=83=BD=E5=8A=9B=E4=B8=80=E8=BD=AE?= =?UTF-8?q?=EF=BC=89=E3=80=82=E6=97=B6=E9=97=B4=E7=BA=BF=EF=BC=88run/decod?= =?UTF-8?q?e=EF=BC=89=EF=BC=9A=E6=96=B0=E6=AD=A5=E9=AA=A4=E7=A7=8D?= =?UTF-8?q?=E7=B1=BB=20analysis=E2=80=94=E2=80=94Agent=20=E7=9A=84?= =?UTF-8?q?=E4=BA=94=E4=B8=AA=E5=88=86=E6=9E=90=E5=B7=A5=E5=85=B7=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E8=B0=83=E7=94=A8=E4=B8=80=E8=A1=8C=EF=BC=8C=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E5=86=99=E7=AE=97=E5=AD=90=E7=9A=84=E5=8F=AB=E6=B3=95?= =?UTF-8?q?=EF=BC=88=E5=A5=91=E7=BA=A6=E7=9A=84=E5=8F=AB=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=99=20analysis=5Fdecompose=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E5=81=9A=E6=88=90=E5=90=8E=E7=85=A7=20tool.end=20=E7=9A=84=20a?= =?UTF-8?q?nalysis=20=E6=8E=A5=E4=B8=8A=E6=9C=AC=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E7=9A=84=E7=BC=96=E5=8F=B7=EF=BC=88A1=EF=BC=8C=E5=92=8C?= =?UTF-8?q?=E4=BA=A4=E5=9B=9E=E6=A8=A1=E5=9E=8B=E7=9A=84=E3=80=8C=E3=80=90?= =?UTF-8?q?=E5=88=86=E6=9E=90=20A1=E3=80=91=E3=80=8D=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=EF=BC=89=E3=80=81=E5=89=AF=E6=A0=87=E9=A2=98=E6=98=AF?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=B8=B2=E6=9F=93=E7=9A=84=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E8=AF=AD=EF=BC=8C=E7=BB=93=E8=AE=BA=E5=B7=A5=E4=BB=B6?= =?UTF-8?q?=E5=8F=AF=E4=B8=8B=E9=92=BB=EF=BC=8C=E5=8F=82=E6=95=B0=EF=BC=88?= =?UTF-8?q?=E6=B2=A1=E5=A1=AB=E7=9A=84=E4=B8=8D=E5=88=97=EF=BC=89=E8=BF=9B?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E5=8C=BA=E3=80=81Agent=20=E5=86=99=E7=9A=84?= =?UTF-8?q?=E5=81=87=E8=AE=BE=E5=8F=A6=E5=88=97=EF=BC=9B=E7=AE=97=E4=B8=8D?= =?UTF-8?q?=E4=BA=86=E7=9A=84=E6=98=AF=E4=B8=80=E6=9D=A1=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=EF=BC=88=E6=8F=90=E9=86=92=E8=89=B2=EF=BC=8C=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E8=AF=AD=E5=B0=B1=E6=98=AF=E3=80=8C=E2=80=A6=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E5=81=9A=E6=88=90=EF=BC=9A=E5=8E=9F=E5=9B=A0=E3=80=8D=EF=BC=8C?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E6=AD=A5=E8=AF=B4=E7=85=A7=E5=AE=9E=E5=86=99?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E3=80=81=E4=B8=8D=E8=83=BD=E6=8D=AE=E6=AD=A4?= =?UTF-8?q?=E4=B8=8B=E5=88=A4=E6=96=AD=EF=BC=89=EF=BC=8C=E7=B4=A7=E8=B7=9F?= =?UTF-8?q?=E7=9D=80=E7=9A=84=20analysis=5Frefused=20=E6=8F=90=E9=86=92?= =?UTF-8?q?=E6=8A=98=E8=BF=9B=E9=82=A3=E4=B8=80=E8=A1=8C=EF=BC=9B=E5=86=99?= =?UTF-8?q?=E9=94=99=E7=9A=84=E8=B0=83=E7=94=A8=EF=BC=88tool.error=20?= =?UTF-8?q?=E5=B8=A6=20call=5Ferror=EF=BC=89=E5=89=AF=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E5=86=99=E6=B2=A1=E6=9C=89=E8=AE=B0=E6=88=90=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=E3=80=81=E4=B8=8B=E4=B8=80=E6=AD=A5=E5=86=99=20Agent=20?= =?UTF-8?q?=E4=BC=9A=E7=85=A7=E7=9D=80=E6=94=B9=E3=80=81=E5=8E=9F=E8=AF=9D?= =?UTF-8?q?=E5=9C=A8=E5=B1=95=E5=BC=80=E5=8C=BA=EF=BC=9B=E8=80=81=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E6=B2=A1=E6=9C=89=20analysis=20=E9=82=A3?= =?UTF-8?q?=E4=B8=80=E6=AE=B5=E6=97=B6=E5=8F=AA=E7=85=A7=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=86=99=E5=8F=AB=E6=B3=95=EF=BC=8C=E4=B8=8D=E4=BB=8E=20previe?= =?UTF-8?q?w=EF=BC=88=E7=BB=93=E8=AE=BA=20JSON=20=E7=9A=84=E5=89=8D?= =?UTF-8?q?=E4=B8=80=E6=88=AA=EF=BC=89=E9=87=8C=E6=8B=BC=E3=80=82=E5=88=86?= =?UTF-8?q?=E6=9E=90=E5=8D=A1=E6=AF=8F=E9=A1=B9=E4=B8=80=E8=A1=8C=EF=BC=88?= =?UTF-8?q?analysis.end=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=90=BD=E6=88=90?= =?UTF-8?q?=E3=80=8C=E6=9C=AA=E8=AF=86=E5=88=AB=E7=9A=84=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E3=80=8D=EF=BC=89=EF=BC=9A=E5=8F=AB=E6=B3=95=E5=92=8C=E8=BF=99?= =?UTF-8?q?=E4=B8=80=E9=A1=B9=E7=9A=84=E6=A0=87=E8=AF=86=E3=80=81=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E8=AF=AD=E3=80=81=E7=BB=93=E8=AE=BA=E5=B7=A5=E4=BB=B6?= =?UTF-8?q?=E3=80=82=E6=8A=A5=E5=91=8A=E7=9A=84=E5=88=86=E6=9E=90=E8=AF=B4?= =?UTF-8?q?=E6=B3=95=E9=87=8D=E5=86=99=E5=90=8E=E4=BB=8D=E6=9C=89=EF=BC=88?= =?UTF-8?q?report=5Fanalysis=5Fclaims=EF=BC=89=E3=80=81=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E5=8D=A1=E7=9A=84=E5=8F=AF=E6=AF=94=E6=80=A7=E6=B2=A1=E5=81=9A?= =?UTF-8?q?=E6=88=90=EF=BC=88metric=5Fcomparability=5Fskipped=EF=BC=89?= =?UTF-8?q?=E8=AF=B4=E6=88=90=E4=BA=BA=E8=AF=9D=E3=80=82=E5=9B=9E=E7=AD=94?= =?UTF-8?q?=E6=B5=81=EF=BC=9A=E5=88=86=E6=9E=90=E7=9A=84=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E3=80=81=E5=B1=95=E5=BC=80=E7=9C=8B=E5=88=A4=E5=AE=9A=E8=AF=AD?= =?UTF-8?q?=E5=92=8C=E5=81=87=E8=AE=BE=EF=BC=8C=E9=95=BF=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=8A=98=E8=B5=B7=E6=9D=A5=E6=97=B6=E5=88=86=E6=9E=90=E9=82=A3?= =?UTF-8?q?=E5=87=A0=E8=A1=8C=E7=85=A7=E6=A0=B7=E9=9C=B2=E5=87=BA=E6=9D=A5?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E7=AE=97=E5=87=BA=E4=BA=86=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E8=BF=90=E8=A1=8C=E4=B8=AD=E7=9A=84=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=AD=97=E5=86=99=E3=80=8C=E6=AD=A3=E5=9C=A8=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=88=86=E6=9E=90=EF=BC=9A=E5=8F=98=E5=8C=96=E6=8B=86?= =?UTF-8?q?=E8=A7=A3=EF=BC=88=E5=8F=AF=E5=8A=A0=EF=BC=89=E2=80=A6=E3=80=8D?= =?UTF-8?q?=E3=80=82=E5=87=BA=E5=85=B7=E6=A8=AA=E5=B9=85=EF=BC=88IssuanceB?= =?UTF-8?q?anner=EF=BC=89=E7=85=A7=E5=87=BA=E5=85=B7=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E5=88=97=E4=B8=89=E9=A1=B9=EF=BC=9A=E5=88=86=E6=9E=90=E8=AF=B4?= =?UTF-8?q?=E6=B3=95=E6=A0=B8=E5=AF=B9=EF=BC=88strict=20=E8=AE=B0=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3=E3=80=81check=20=E5=8F=AA=E6=A0=87=E6=B3=A8=EF=BC=8C?= =?UTF-8?q?=E5=87=A0=E5=A4=84=E3=80=81=E5=8E=9F=E8=AF=9D=E3=80=81=E7=A7=8D?= =?UTF-8?q?=E7=B1=BB=E7=9A=84=E5=8F=AB=E6=B3=95=E5=90=8C=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=EF=BC=89=E3=80=81=E6=8C=87=E6=A0=87=E7=9A=84=E4=B8=A4=E6=9C=9F?= =?UTF-8?q?=E5=8F=AF=E6=AF=94=E6=80=A7=EF=BC=88=E5=88=A4=E5=AE=9A=E8=AF=AD?= =?UTF-8?q?=E5=92=8C=E5=8E=9F=E5=9B=A0=EF=BC=89=E3=80=81=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=9A=84=E6=8C=87=E6=A0=87=E6=B2=A1=E8=BF=BD?= =?UTF-8?q?=E5=88=B0=E5=8E=9F=E5=A7=8B=E6=95=B0=E6=8D=AE=EF=BC=88=E8=BF=BD?= =?UTF-8?q?=E5=88=B0=E5=93=AA=E4=B8=BA=E6=AD=A2=E3=80=81=E8=AE=B0=E6=B2=A1?= =?UTF-8?q?=E8=AE=B0=E7=BC=BA=E5=8F=A3=EF=BC=89=E3=80=82=E5=A4=B9=E5=85=B7?= =?UTF-8?q?=EF=BC=9Afixtures.json=20=E5=8A=A0=E4=B8=A4=E6=AC=A1=E7=9C=9F?= =?UTF-8?q?=E8=B7=91=E7=9A=84=E8=BF=90=E8=A1=8C=EF=BC=88=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=BA=93=E9=87=8C=E7=9A=84=E8=99=9A=E6=9E=84?= =?UTF-8?q?=E9=9B=B6=E5=94=AE=E5=BA=93=20shop=E3=80=81=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=8D=A2=E6=88=90=E8=84=9A=E6=9C=AC=EF=BC=9AAgent=20=E6=9F=A5?= =?UTF-8?q?=E5=BA=93=E3=80=81=E5=86=99=E9=94=99=E4=B8=80=E6=AC=A1=E6=8B=86?= =?UTF-8?q?=E8=A7=A3=E3=80=81=E6=8B=86=E8=A7=A3=E3=80=81=E6=B3=A2=E5=8A=A8?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E7=AE=97=E4=B8=8D=E4=BA=86=E3=80=81=E5=8F=AF?= =?UTF-8?q?=E6=AF=94=E6=80=A7=E6=A3=80=E6=9F=A5=EF=BC=8C=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=203=20=E5=A4=84=E8=AF=B4=E6=B3=95=E6=A0=87=E6=B3=A8=EF=BC=9B?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=8D=A1=E4=B8=A4=E9=A1=B9=EF=BC=89=EF=BC=8C?= =?UTF-8?q?synthetic=20=E5=8A=A0=E5=8D=8F=E4=BD=9C=E6=88=90=E5=91=98?= =?UTF-8?q?=E8=B0=83=E5=88=86=E6=9E=90=E5=B7=A5=E5=85=B7=EF=BC=8Cdev/fixtu?= =?UTF-8?q?res/analysis.tsx=20=E4=B8=89=E4=B8=AA=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E3=80=82check-decode=E3=80=81check-stream=20?= =?UTF-8?q?=E5=90=84=E5=8A=A0=E5=88=86=E6=9E=90=E7=9A=84=E5=87=A0=E8=8A=82?= =?UTF-8?q?=EF=BC=88check-stream=20=E5=8A=A0=20STREAM=5FONLY=20=E5=8F=AA?= =?UTF-8?q?=E8=B7=91=E5=87=A0=E6=AE=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dev/fixtures/analysis.tsx | 91 ++ frontend/src/run/AssistantStream.tsx | 110 +- frontend/src/run/__tests__/fixtures.json | 1738 ++++++++++++++++++++++ frontend/src/run/__tests__/synthetic.ts | 41 + frontend/src/run/decode.ts | 144 +- frontend/src/store/chat.ts | 4 + scripts/check-decode.mjs | 62 + scripts/check-stream.mjs | 81 + 8 files changed, 2265 insertions(+), 6 deletions(-) create mode 100644 frontend/src/dev/fixtures/analysis.tsx diff --git a/frontend/src/dev/fixtures/analysis.tsx b/frontend/src/dev/fixtures/analysis.tsx new file mode 100644 index 00000000..38c7bb9e --- /dev/null +++ b/frontend/src/dev/fixtures/analysis.tsx @@ -0,0 +1,91 @@ +import type { JSX } from 'react' +import fixtures from '../../run/__tests__/fixtures.json' +import { AssistantStream, type StreamTurn } from '../../run/AssistantStream' +import { decodeRun } from '../../run/decode' +import { analysisTeamRun } from '../../run/__tests__/synthetic' +import type { RunEvent } from '../../types' + +/** + * 分析能力一轮在问数据时间线、出具横幅上的样子,scripts/check-stream.mjs 按它检查: + * + * - ?syn=analysis:两轮真跑出来的事件(fixtures.json 的 analysis_agent、analysis_card,后端测试库里的虚构零售库 shop、模型换成 + * 脚本录下的)。第一轮 Agent 查两次库、写错一次变化拆解(调用有误)、做变化拆解、波动基线(算不了)、可比性检查,报告有 3 处 + * 分析说法标注(check,不影响档位);第二轮分析卡两项(波动基线算不了、按地区拆解) + * - ?syn=analysis-issued:出具横幅里分析能力一轮的三项都在——分析说法 strict 记了缺口、指标两期有条件可比和不可比、报告引用的 + * 指标没追到原始数据(受管级别记了缺口) + * - ?syn=analysis-team:协作成员调分析工具(编号是成员自己的) + * + * 地区、门店、指标、数字都是虚构的;说法照服务端的写法 + */ + +type Fixture = Record +const F = fixtures as unknown as Fixture + +const outputOf = (events: RunEvent[]) => (events.find((e) => e.type === 'run.finished')?.data as { output?: Record })?.output + +function recordedTurns(): StreamTurn[] { + const agent = F.analysis_agent + const card = F.analysis_card + return [ + { id: 'analysis-agent', question: '本周销售额为什么下降', phase: 'done', status: '已完成', + steps: decodeRun(agent, { status: 'succeeded' }), output: outputOf(agent), runId: 'syn-analysis-agent', runClass: 'exploratory' }, + { id: 'analysis-card', question: '按模板出本周的异动分析', phase: 'done', status: '已完成', + steps: decodeRun(card, { status: 'succeeded' }), output: outputOf(card), runId: 'syn-analysis-card', runClass: 'exploratory' }, + ] +} + +/** 三项都在的出具声明(照 engine/nodes/io.py 的形状;strict 的那几处原话照服务端 analysis_claims 的写法) */ +export const ANALYSIS_ISSUANCE = { + mode: 'citations', tier: 'degraded', matched_numbers: 3, metrics_checked: 1, missing_required: [], missing_expected: [], + unmatched_numbers: [], calibers: [], + gaps: [ + '报告「写周报」有 1 处分析说法未通过核对(1 处与所挂结论的判定对不上):判断要挂对应的分析结论并照判定写,原因要有知识库原话或运行输入作依据', + '指标「环比」两期不可比:两边除了日期,筛选的还不一样', + '指标「客单价」的输入没追到原始数据:vars.aov(停在「沙箱估算」)', + ], + analysis_claims: { + mode: 'strict', count: 1, counts: { analysis_claim_mismatch: 1 }, counted: true, + violations: [{ code: 'analysis_claim_mismatch', text: '主要来自', claim: 'source', op: 'decompose', analyses: ['A1'], unit: 'u3', + segment: 's12', span: [80, 84], message: '「主要来自华南」与所挂的分析结论 A1 对不上:判定的主要来源是华东,不是华南', + context: '其中下降主要来自华南。' }], + }, + comparability: [ + { metric: 'wow', name: '环比', verdict: 'incomparable', phrase: '两期不可比:两边除了日期,筛选的还不一样', reasons: ['两边除了日期,筛选的还不一样'] }, + { metric: 'mom', name: '月环比', verdict: 'conditional', phrase: '两期有条件可比:两段的周末天数不同:4 天对 6 天', + reasons: ['两段的周末天数不同:4 天对 6 天'] }, + ], + untraced: { metrics: [{ metric: 'aov', name: '客单价', stops: [{ path: 'vars.aov', node_id: 'estimate' }] }], counted: true }, +} + +function issuedTurn(): StreamTurn { + const at = Date.now() / 1000 - 30 + const output = { 周报: '本周销售额下降,主要来自华东。', _issuance: ANALYSIS_ISSUANCE } + const events: RunEvent[] = [ + { seq: 1, type: 'run.started', node_id: null, ts: at, data: { nodes: 1 } }, + { seq: 2, type: 'node.started', node_id: 'done', ts: at + 1, data: { node_type: 'output', label: '成果' } }, + { seq: 3, type: 'issuance', node_id: 'done', ts: at + 1.2, data: { tier: 'degraded', mode: 'citations', + gaps: ANALYSIS_ISSUANCE.gaps, analysis_claims: { mode: 'strict', count: 1, counted: true } } }, + { seq: 4, type: 'node.finished', node_id: 'done', ts: at + 1.3, data: { duration_ms: 300 } }, + { seq: 5, type: 'run.finished', node_id: null, ts: at + 1.4, data: { output, duration_ms: 1400 } }, + ] + return { id: 'analysis-issued', question: '本周销售周报', phase: 'done', status: '已完成', + steps: decodeRun(events, { status: 'succeeded' }), output, runId: 'syn-analysis-issued', runClass: 'formal' } +} + +function teamTurn(): StreamTurn { + const ev = analysisTeamRun() + return { id: 'analysis-team', question: '销售额为什么下降', phase: 'done', status: '已完成', + steps: decodeRun(ev, { status: 'succeeded' }), runId: 'syn-analysis-team' } +} + +export const cases: Record JSX.Element> = { + analysis: () => ( +
+ ), + 'analysis-issued': () => ( +
+ ), + 'analysis-team': () => ( +
+ ), +} diff --git a/frontend/src/run/AssistantStream.tsx b/frontend/src/run/AssistantStream.tsx index 79899ade..51d675d6 100644 --- a/frontend/src/run/AssistantStream.tsx +++ b/frontend/src/run/AssistantStream.tsx @@ -4,7 +4,7 @@ import { } from 'react' import { Link } from 'react-router-dom' import { - AlertCircle, AlertTriangle, ArrowDown, Brain, ChevronRight, CircleCheck, CircleDot, CornerDownRight, + AlertCircle, AlertTriangle, ArrowDown, Brain, ChartColumnStacked, ChevronRight, CircleCheck, CircleDot, CornerDownRight, Database, Download, ExternalLink, FileCode, FileDown, GitBranch, Hand, Info, ListChecks, Play, Settings2, Scale, ShieldCheck, Sparkles, Table2, Terminal, UserX, Users, Wrench, XCircle, } from 'lucide-react' @@ -20,6 +20,7 @@ import { nodeTypeLabel, } from '../lib/terms' import { SqlCheckList } from './SqlChecks' +import { ANALYSIS_TEXT } from '../lib/termsAnalysis' import { ISSUANCE_HINT, childrenByExec, compactSteps, parseQueryResult, progressOf, splitIssuanceGaps, spread, teamVerdictOf, type Exec, type FixKind, type ResultTable as Table, type Step, type StepKind, type TeamMemberEx, @@ -28,7 +29,9 @@ import { import { CODE_COLUMN, CopyChip, LEADING_ZERO, Markdown, type MarkSpec } from './Markdown' import { ClaimTally, EvidenceField, type EvidenceDocHandle } from './EvidenceDoc' import { EVIDENCE_STATE, claimProblems, docTally, evidenceFields, issuanceMarks, type EvidenceTally } from '../lib/evidence' -import type { EvidenceDocData, IssuanceSqlCheck } from '../types' +import type { + EvidenceDocData, IssuanceAnalysisClaims, IssuanceComparability, IssuanceSqlCheck, IssuanceUntraced, +} from '../types' import { useRunClock } from './useRunClock' import { CatalogPatchCard } from './CatalogPatchCard' import type { CatalogPatch } from './catalogPatch' @@ -131,7 +134,7 @@ export interface StreamTurn { } const ICONS: Record = { - node: CircleDot, think: Brain, llm: Sparkles, query: Database, schema: Table2, + node: CircleDot, think: Brain, llm: Sparkles, query: Database, schema: Table2, analysis: ChartColumnStacked, tool: Wrench, code: Terminal, branch: GitBranch, human: Hand, issuance: FileCode, note: CircleDot, error: XCircle, lifecycle: CircleCheck, } @@ -1011,6 +1014,11 @@ const REVEAL_TRIES = 4 const notable = (s: Step): boolean => s.status === 'failed' || s.status === 'running' || s.status === 'waiting' || s.status === 'rejected' || s.level === 'error' || s.level === 'warn' +/** + * 折起来时也留着的:上面那些,加上分析的那几行——判定语就是这一步查出来的结论(变化来自哪里、算不算异常),收进「前面的 N 条」 + * 就看不到 Agent 是怎么下的判断。它们不算出了问题(分轮的警示标记只看 notable) + */ +const kept = (s: Step): boolean => notable(s) || s.kind === 'analysis' function StepList({ steps, depth, turnMs, fold = false }: { steps: Step[]; depth: number; turnMs?: number @@ -1025,7 +1033,7 @@ function StepList({ steps, depth, turnMs, fold = false }: { // 问题的一律留着。反过来只画前 20 条的话,长运行跑到后面,正在跑的那一步反而 // 被收进「展开其余」里看不见 const keep = fold && rows.length > 6 ? 3 : rows.length > LIST_CAP ? LIST_HEAD : 0 - const shown = !all && keep ? rows.filter((s, i) => notable(s) || i >= rows.length - keep) : rows + const shown = !all && keep ? rows.filter((s, i) => kept(s) || i >= rows.length - keep) : rows const hidden = rows.length - shown.length // 画布上点名要看的节点正好收在前面那一截里:先展开,上面的定位下一帧就找得到它 useLayoutEffect(() => { @@ -1086,7 +1094,8 @@ function StepLine({ step, depth, turnMs }: { step: Step; depth: number; turnMs?: const [open, setOpen] = useState(false) const Icon = ICONS[step.kind] ?? CircleDot const result = step.result - const expandable = !!(step.detail || result || step.raw || step.artifact || step.checks?.length) + const expandable = !!(step.detail || result || step.raw || step.artifact || step.checks?.length + || step.analysis?.phrase || step.analysis?.hypothesis) const table = result ? parseQueryResult(result) : null const status = step.status ?? 'done' // 失败的节点:收着的时候就说为什么(和轮次顶上的报错同一份 lib/explain),展开再看原因、 @@ -1136,6 +1145,7 @@ function StepLine({ step, depth, turnMs }: { step: Step; depth: number; turnMs?: data-node-id={step.nodeId} data-step-status={failed ? 'failed' : status} data-step-code={step.code} + data-step-kind={step.kind} aria-busy={running || undefined} >