From 70b4a11297a1904441d378d0a336e144c8439de3 Mon Sep 17 00:00:00 2001 From: yibo Date: Sat, 22 Aug 2026 10:42:50 +0800 Subject: [PATCH] feat(graph-console): 加任务级产物汇总面板 + 顺手修正 34 号文档的错误前提 --- docs/README.md | 2 +- docs/todo/system-design/34-validator-scored-without-real-deliverable.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------- server/routes/graph-run.js | 5 +++++ server/services/graph-engine.js | 4 ++++ server/tests/unit/graph-run-route-io.test.js | 24 ++++++++++++++++++++++++ web/src/components/graph-console/GraphRunPane.vue | 6 ++++++ web/src/components/graph-console/TaskFilesPanel.vue | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/src/views/GraphConsole.vue | 17 +++++++++++++++++ 8 files changed, 367 insertions(+), 35 deletions(-) create mode 100644 web/src/components/graph-console/TaskFilesPanel.vue diff --git a/docs/README.md b/docs/README.md index 50f4da7..fb8846e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 | | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | -| [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录**。真实回归任务里 `org-coder-impl` 从未产出任何 HTML/CSS 文件,`org-coder-validator` 却给六个视觉/交互维度全打了 100 分——rubric 的 `evidence_hint` 明确要求打开真实页面检查,validator 显然没真查就评了分。只记现象与证据,根因(提示词缺前置检查?工具不可用?)留给下次 | +| [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录,原始前提已订正**。最初以为 `org-coder-impl` 从未产出任何 HTML/CSS(`org-coder-validator` 却给六维度全打 100 分),后来发现真实网站其实存在于 `graph_files` 索引不覆盖的「项目根」目录(`data/companies/{cid}/projects/{pid}/`)——这半个发现已经顺手修了(控制台「产物」面板现在会展示项目根)。订正后仍未解决的窄问题:validator 有没有真的照 `evidence_hint` 做浏览器级检查,还是即使产物存在也没真查 | | [33-needs-gate-role-filter-bug.md](./todo/system-design/33-needs-gate-role-filter-bug.md) | ✅ **已修复**。`executor.json`/`executor-fanout.json` 里 `produces:["log"]` 声明了一个 test/integration 类节点永远不会被索引到的 kind,导致 `org-coder-validator` 被 `missing_required_input` 反复误判 blocked;改成 `required:false`(对齐 `validator.json` 既有写法)。文档记录了一条**走过的弯路**(按 role 过滤 `inboundContracts` 会削弱 T-mt25ecbd 的产出闸门,已撤回),发现于 31 号文档的真实回归验证过程中,与兜底边问题无关 | ## todo/future/(暂缓,非当前重心) diff --git a/docs/todo/system-design/34-validator-scored-without-real-deliverable.md b/docs/todo/system-design/34-validator-scored-without-real-deliverable.md index 1ae0353..1ca3657 100644 --- a/docs/todo/system-design/34-validator-scored-without-real-deliverable.md +++ b/docs/todo/system-design/34-validator-scored-without-real-deliverable.md @@ -1,8 +1,10 @@ # 34-validator-scored-without-real-deliverable. coder-validator 在没有真实产物的情况下给了满分 > **日期**: 2026-08-22 -> **状态**: 📝 **调研记录,非设计方案**——只记录现象与证据,不现场深挖根因(用户明确选择 -> 先记文档,验收链路本身的调查留给下次) +> **状态**: 📝 **调研记录,非设计方案,且 §一/§二的核心前提已被 §零 推翻大半**—— +> 原始判断("index.html 从未被写出来")是错的,真实网站文件其实存在,只是不在 +> `graph_files` 索引里。修订后的问题更窄:validator 有没有**真的**照 `evidence_hint` +> 做浏览器级检查,还是即使产物存在也照样走了个形式。这一层仍未核实,留待下次。 > **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, > root run `run_0e9074815130`)——[31 号文档](./31-work-graph-fail-fallback-coverage.md) > 兜底边修复 + [33 号文档](./33-needs-gate-role-filter-bug.md) 契约修复之后,把这条任务 @@ -10,6 +12,36 @@ --- +## 零、2026-08-22 补充:原始判断错了一半——真实网站文件其实存在 + +写这份文档时只查了两处:`org-coder-impl`/`org-coder-test` 各自的节点私有目录 +(`runs/{rootRunId}/nodes/{nodeRunId}/`)和 `graph_files` 索引表,两处都没有 +HTML/CSS/JS,于是断言"首页从未被写出来"。 + +后来在**同一次会话里**给控制台加"任务产物汇总"面板(用户反馈的另一个需求)时, +读 `graph-engine.js` 的 W2 段落才发现:这个引擎给每个任务的代码分配了一个**跨节点 +共享的项目根**(`ensureProjectRoot`,复用 DevLoop 的 `path-resolver.allocateProjectRoot`, +落在 `data/companies/{cid}/projects/{pid}/`),这个目录**不受 `graph_files`/ +`indexNodeOutputs` 覆盖**——后者只扫每个节点自己的私有产出目录。真去看 +`data/companies/e2e-demo/projects/p-v82tpc/` 才发现: + +``` +index.html 2.2KB 真实的 apple.com 风格单页站点(hero + 3 个卖点区块 + 平滑滚动导航) +script.js 1.0KB +styles.css 3.3KB +tests/unit-tester.check.js +workgraph-plan-T-mt2r6fz0-125b.md +``` + +内容核对过:3 个卖点区块的标题与顺序(多智能体协作 → 全链路自动化 → Skill 竞技场) +与 `org-coder-impl` 自己 `_plan.md` 里写的验收对照表逐字对应,不是空壳/占位文件。 + +**这次查漏的教训**:`graph_files` 索引 + 节点私有目录,只是这个系统里**两个**产出 +落点中的一个;第三个(项目根)当时完全没查。下面 §一/§二 保留原文作历史记录, +但"index.html 从未被写出来"这句话是错的,请按本节订正读。 + +## 一、(原始记录,前提有误,见 §零) + ## 一、现象 任务最终状态:`completed`,`org-planner` / `org-coder` / `org-validator` 三个 org 节点 @@ -55,43 +87,46 @@ contract.json 跨节点契约快照 | `responsive_layout` | 「浏览器依次设置视口宽度 375/768/1440px 检查横向滚动条与重叠」 | | `resource_integrity` | 「打开浏览器开发者工具 Console + Network 面板刷新页面检查」 | -这五项全都以"打开浏览器 / 打开 `index.html`"为前提——而 `index.html` 从来没有被 -`org-coder-impl` 写出来过。`org-coder-validator` 不可能真的执行了这些检查步骤, -却仍然给出了 100/100。唯一有证据支撑的是第六维度 `pipeline_auto_recovery` -(权重 0.25,本次回归验证真正要看的那一维)——它的 `evidence_hint` 是"读 -`attempt-*.json` 搜索 `fail_reason=input_underspecified`",这条本身不依赖前端产物, -`org-coder` 的 `remediationNote` 里也确实写了"检索本 run 下 nodes/\*/attempt-\*.json -的 verdict/fail_reason 字段,未发现任何一条真实的判定"——这条像是真查过的。 +这五项全都以"打开浏览器 / 打开 `index.html`"为前提——**当时以为** `index.html` +从没被写出来过,所以断言 validator 不可能真的执行了这些检查步骤。**这个前提错了** +(见 §零):`index.html`/`styles.css`/`script.js` 其实都在 `projects/p-v82tpc/` +下真实存在,内容也对得上任务要求。 -**结论**:`org-coder-validator` 至少在 5/6 维度上没有依据 `evidence_hint` 去核实 -产物是否存在就给了分,是一次**没有真实检查支撑的评分**(rubber-stamped), -不是"检查过但判断错误"。 +**订正后的结论**:不能再说这是"没有产物却给了满分"——产物是有的。真正没验证过的 +是**更窄的一个问题**:`org-coder-validator` 是否真的照 `evidence_hint` 打开了这个 +页面、量了字号比例、切了三档视口、开了 Console/Network 面板检查,还是即使产物 +存在,它也只是读了读源码/自己的 `_plan.md` 就直接给了 100 分,没有真的做那些 +`evidence_hint` 点名要求的浏览器级验证动作。这个问题现在的证据**不足以下结论**—— +六个维度全 100 分本身既可能是"真查过、页面确实做得不错",也可能是"没查、乐观估计", +两种情况从现有的 `attempt-*.json`/`remediationNote` 文本里区分不出来。 -## 三、影响面(未核实,留给下次) +## 三、影响面(订正后,仍未核实,留给下次) - 这不是 31/33 号文档改动引入的新问题——两次修复只是让流程**不再卡死**, - 让这条原本会一直 blocked 的任务真的走到了 `completed`,才把这个一直存在的 - 验收缺口暴露出来。 -- 如果这是系统性行为(不是这一次运气差),意味着 `executor.json` 这类骨架的 - `validator` 节点在**缺少真实产物**时可能倾向于给出宽松评分,而不是判 fail - 或至少低分——这会让"任务显示已完成"和"任务真的达成了原始需求"之间出现 - 静默的落差,且这个落差**在界面上完全看不出来**(`org-coder` 的 - `remediationNote` 里其实提到了"artifacts/ 下产物缺失"这类线索,但那是给 - 下一轮 agent 看的内部备注,不是给人看的告警)。 + 让这条原本会一直 blocked 的任务真的走到了 `completed`,才有机会去看"最终产出 + 在哪、验收是怎么做的"这件事。 +- 真正独立于本文档、且已经确认修复的一个衍生问题:`graph_files`/`taskFiles` + 索引**不覆盖项目根**,这意味着控制台原本设想的"任务级产物汇总"面板如果只读 + `graph_files`,会永远看不到真实交付物(这正是 §零 发现问题的路径)。这一半 + 已经在同一次改动里修了:`graph-engine.readProjectRootRecord` 现在随 + `GET /api/graph-runs/:id` 一起吐出来,控制台的「产物」按钮会分别列"最终交付物 · + 项目根"和"节点产出索引"两节,见 `web/src/components/graph-console/TaskFilesPanel.vue`。 +- 仍然悬而未决的是 validator **有没有真的做过视觉/交互检查**这件事本身——这需要 + 证据链(比如强制要求 validator 在 `attempt-*.json` 里回填它量出的具体数字: + h1:tagline 字号比、三档视口下的滚动条状态等),而不是只回一个分数。 ## 四、留待下次调查的问题(不在本次范围内动手) - [ ] `coder-validator` 本体(`system/agents/unit-tester/agent.md` 或对应角色)的 - 提示词里,评分前有没有一步"先确认产物文件真的存在"的强制检查?如果没有, - 这可能是这次跑分虚高的直接原因。 -- [ ] `coder-validator` 是否有可用的浏览器/文件读取工具去真的执行 - `evidence_hint` 里描述的那些检查(截图、开发者工具)?如果工具不可用, - "满分"可能是它在无法验证时的默认基调,而不是主动撒谎。 -- [ ] 这是不是这次真实回归测试里 `org-coder`/`unit-tester` 两个新招聘本体 - (见 [33 号文档提交时的记录](./33-needs-gate-role-filter-bug.md))本身 - 还没配好角色提示词导致的一次性现象,还是所有 `executor.json` 骨架的 - validator 节点普遍如此? -- [ ] 是否需要给这类"需要打开真实产物检查"的维度加一道**引擎级的前置闸门** - (类似 T-mt25ecbd 的 produces 闸门):`evidence_hint` 里点名要读的文件 - 如果在产物索引里查不到实例,直接判该维度 0 分或整体 blocked,不依赖 - validator 自己主动去查。 + 提示词里,评分前有没有一步强制要求"回填 evidence_hint 点名要量的具体数字", + 而不是只回一个分数?没有这一步的话,"真查过"与"没查"从产出上完全无法区分。 +- [ ] `coder-validator` 是否有可用的浏览器工具(截图、DOM 检查)去真的执行 + `evidence_hint` 里描述的那些检查?如果连工具都没有,让它对着"评分要求打开浏览器" + 的 rubric 打分,本身就是设计上要求了它做不到的事。 +- [ ] 抽查其他任务(比如 `p-bkhylu`/`p-v1mpcl` 这两个已存在的项目根)的 validator + 评分记录,看这次的六个满分是个例还是常态。 +- [ ] 是否需要给"必须打开真实产物检查"的维度加一道**引擎级的前置闸门**:至少确保 + 被检查的文件(如 `index.html`)在 `evidence_hint` 点名的路径下真实存在 + (现在项目根已经可查,这道闸门补起来比 §零 之前容易),如果连文件都不存在 + 就直接拦下不许打分;文件存在之后 validator 有没有真的看,还是只能靠 §四第 + 一条的"强制回填具体数字"来间接约束。 diff --git a/server/routes/graph-run.js b/server/routes/graph-run.js index 305193e..93d148f 100644 --- a/server/routes/graph-run.js +++ b/server/routes/graph-run.js @@ -368,6 +368,11 @@ router.get('/:id', h((req, res) => { edges, companyRoot: companyRootDir(state.run.company_id), taskFiles: findFiles({ companyId: state.run.company_id, taskId: state.run.task_id }), + // 控制台「产物」面板要区分两类产出:graph_files 只索引各节点私有目录下的东西 + // (规划笔记、日志、契约快照……),真实交付物(代码)写在这个任务共享的项目根里 + // (见 graph-engine.js「W2 · 每个 root run 一个项目根」那段),两者不是同一份数据。 + // readProjectRootRecord 按 root_run_id 查,任一层 run 传进去结果相同(同一棵树共享一个项目根)。 + projectRoot: engine.readProjectRootRecord(state.run), loopRecords: listLoopRecords(state.run.id), childRuns: listChildRuns(state.run.id), budget, diff --git a/server/services/graph-engine.js b/server/services/graph-engine.js index bb47e06..8db913c 100644 --- a/server/services/graph-engine.js +++ b/server/services/graph-engine.js @@ -4363,6 +4363,10 @@ module.exports = { // ensureProjectRoot 的真实调用方是 advanceOne,diffRepoStatus 的是 applyBoundaryGuard; // 导出只是让单测能直接验这两条判据,不是新开一圈公开面。 ensureProjectRoot, + // readProjectRootRecord:控制台「产物」面板要知道这个任务的真实交付物写在哪—— + // graph_files 只索引每个节点自己私有目录下的产出,project-root.json 指向的才是 + // 跨节点共享的项目根(真实代码落盘的地方,见本文件 W2 那段大注释)。 + readProjectRootRecord, diffRepoStatus, // 真实调用方是 advanceOne(每次节点执行前后各一次)。导出 + 可注入的 after 快照, // 是为了让「越界 → success 降级成 fail + fail_reason=boundary_escape」这条分支 diff --git a/server/tests/unit/graph-run-route-io.test.js b/server/tests/unit/graph-run-route-io.test.js index e67b21d..ca16df0 100644 --- a/server/tests/unit/graph-run-route-io.test.js +++ b/server/tests/unit/graph-run-route-io.test.js @@ -144,4 +144,28 @@ describe('GET /api/graph-runs/:id — R-19 输入输出三字段', () => { const r = await fetch(`${base}/api/graph-runs/run_nope`); expect(r.status).toBe(404); }); + + // 34 号文档:graph_files 只索引节点私有目录,真实交付物写在这个任务共享的 + // 项目根(W2 ensureProjectRoot)——控制台「产物」面板要同时看到两处,缺一个字段 + // 就是同一路「实现在、零调用方」再来一次。 + it('projectRoot:没分配过就是 null(不假装有)', async () => { + const r = await fetch(`${base}/api/graph-runs/${runId}`); + const d = await r.json(); + expect(d.projectRoot).toBeNull(); + }); + + it('projectRoot:分配过之后原样透传(root_path/pid 与落库的记录一致)', async () => { + const engine = require('../../services/graph-engine'); + const run = wdb.getRun(runId); + const rec = engine.ensureProjectRoot({ run, companyId, nodeRunId: 'rn-fake' }); + expect(rec).toBeTruthy(); + + const r = await fetch(`${base}/api/graph-runs/${runId}`); + const d = await r.json(); + expect(d.projectRoot).toBeTruthy(); + expect(d.projectRoot.pid).toBe(rec.pid); + expect(d.projectRoot.root_path).toBe(rec.root_path); + // 幂等:再查一次不会重新分配一个不同的项目根 + expect(engine.ensureProjectRoot({ run, companyId, nodeRunId: 'rn-fake-2' }).root_path).toBe(rec.root_path); + }); }); diff --git a/web/src/components/graph-console/GraphRunPane.vue b/web/src/components/graph-console/GraphRunPane.vue index 97872b0..664e699 100644 --- a/web/src/components/graph-console/GraphRunPane.vue +++ b/web/src/components/graph-console/GraphRunPane.vue @@ -101,6 +101,10 @@ const budget = ref(null); // 「没有产物」和「后端没告诉我有没有产物」必须分得开。 const taskFiles = ref(null); const companyRoot = ref(null); +// 这个任务真实交付物的落点(W2「每个 root run 一个项目根」)——与 taskFiles 是两类数据, +// 不要合并:graph_files 只索引节点私有目录,projectRoot 才是代码真正写的地方。 +// 没有记录(节点还没跑到 ensureProjectRoot 那一步)时保持 null。 +const projectRoot = ref(null); const events = ref([]); // SSE 事件缓冲,供详情面板的「运行日志」tab 消费 const selectedDefId = ref(null); const loading = ref(false); @@ -208,6 +212,7 @@ async function load() { budget.value = j.budget || null; taskFiles.value = Array.isArray(j.taskFiles) ? j.taskFiles : null; companyRoot.value = j.companyRoot || null; + projectRoot.value = j.projectRoot || null; mode.value = j.run?.execution_mode || 'manual'; push(); } catch (e) { @@ -226,6 +231,7 @@ function push() { childRuns: childRuns.value, // R-19:右栏「输入输出」tab 的实例侧数据,与 nodes/edges 走同一条 load taskFiles: taskFiles.value, companyRoot: companyRoot.value, + projectRoot: projectRoot.value, }); } diff --git a/web/src/components/graph-console/TaskFilesPanel.vue b/web/src/components/graph-console/TaskFilesPanel.vue new file mode 100644 index 0000000..d157bd6 --- /dev/null +++ b/web/src/components/graph-console/TaskFilesPanel.vue @@ -0,0 +1,241 @@ + + + + + + diff --git a/web/src/views/GraphConsole.vue b/web/src/views/GraphConsole.vue index 414d19c..2a86a4b 100644 --- a/web/src/views/GraphConsole.vue +++ b/web/src/views/GraphConsole.vue @@ -24,6 +24,13 @@
无 root run 预算
+ @@ -173,6 +180,7 @@ import GraphRunPane from '@/components/graph-console/GraphRunPane.vue'; import NodeDetailPanel from '@/components/graph-console/NodeDetailPanel.vue'; import ReplanOrgGraphButton from '@/components/graph-console/ReplanOrgGraphButton.vue'; import GoalTaskPane from '@/components/graph-console/GoalTaskPane.vue'; +import TaskFilesPanel from '@/components/graph-console/TaskFilesPanel.vue'; import { findTask, pickDefaultSelection, runStateToTaskPatch, hasStartedRun, pendingRunHint, } from '@/components/graph-console/goal-task-model.js'; @@ -241,6 +249,15 @@ const hasOrgGraph = computed(() => defNodes.value.length > 0); const currentRunId = computed(() => (stack.value.length ? stack.value[stack.value.length - 1].runId : null)); const budget = computed(() => rootState.value.budget || null); const spent = computed(() => budget.value?.spent || {}); +/** + * 任务级产物汇总按钮要的两份数据(TaskFilesPanel)。taskFiles 是**任务级**检索 + * (`findFiles({companyId, taskId})`),不管从 root run 还是 mid run 拉都是同一份—— + * 优先用 root(选中 task 就有),它还没加载时(比如只下钻过中栏还没点过左栏)退到 mid, + * 两者都没有就是 null(整块降级,不假装是空数组)。 + */ +const taskFiles = computed(() => rootState.value.taskFiles ?? midState.value.taskFiles ?? null); +const taskFilesRoot = computed(() => rootState.value.companyRoot ?? midState.value.companyRoot ?? null); +const taskProjectRoot = computed(() => rootState.value.projectRoot ?? midState.value.projectRoot ?? null); const selectedTask = computed(() => (findTask(goals.value, selectedTaskId.value) || {}).task || null); -- libgit2 0.21.4