Ver Fonte

fix(research67): 修正阶段进度与论文重编译状态

Research67 进度展示此前按工具调用文本做宽松匹配,并在源文件更新后继续保留旧的编译成功状态,导致阶段进度可能提前完成或无法反映需要重新编译。

- 解析 WriteFile 的结构化 file_path,并基于阶段目录和相对路径精确归类阶段产物。

- 仅对真实 WriteFile 产物推进里程碑,避免 ReadFile 内容、临时文件和正文中的路径误触发进度。

- 在论文源文件成功更新后使编译与报告里程碑失效,显示等待重新编译,并保留失败现场直到显式重试。

- 让 BuildResearchPaper 的各语言编译状态按实际尝试重置和推进,保持 Guard 与阶段回执一致。

- 扩充远端事件序列测试,覆盖上游读取、临时写入、失败构建、源文件修复和重新构建流程;OpenCode 包内相关测试 3 项已通过。
Jinchen Zhang há 1 semana atrás
pai
commit
fcb6e0ccf6

+ 71 - 30
.opencode/tool/agentpaas_run.ts

@@ -445,6 +445,14 @@ function getPositiveInteger(value: unknown): number {
   return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 1
 }
 
+function getWriteFilePath(content: string): string {
+  try {
+    const filePath = getString(asRecord(JSON.parse(content))?.file_path)
+    if (filePath) return filePath
+  } catch {}
+  return content.match(/["']file_path["']\s*:\s*(["'])(.*?)\1/)?.[2] ?? ""
+}
+
 function withStage(title: string, metadata: Record<string, unknown>): string {
   const stageName = getString(metadata.stage_name)
   return stageName ? `${title}(${stageName})` : title
@@ -498,10 +506,6 @@ function createResearch67Progress() {
   const startMilestone = (milestone: ProgressMilestone) => {
     if (!current) return
     if (milestone.status === "completed") return
-    for (const item of current.milestones) {
-      if (item === milestone) break
-      if (item.status !== "failed") item.status = "completed"
-    }
     milestone.status = "running"
     if (milestone.detail === "尚未开始") milestone.detail = undefined
     if (active !== milestone) activeSince = Date.now()
@@ -520,45 +524,69 @@ function createResearch67Progress() {
     pendingArtifacts.clear()
   }
 
-  const artifact = (content: string) => {
+  const artifact = (filePath: string) => {
     if (!current) return undefined
-    const value = content.replaceAll("\\", "/").toLowerCase()
+    const value = filePath.replaceAll("\\", "/").toLowerCase()
+    const stage = value
+      .split("/")
+      .findLastIndex((part) => part === current?.id || new RegExp(`^${current?.id}_a\\d+$`).test(part))
+    if (stage < 0) return undefined
+    const relative = value
+      .split("/")
+      .slice(stage + 1)
+      .join("/")
     const match = (milestoneID: string, key: string) => {
       const milestone = current?.milestones.find((item) => item.id === milestoneID)
       return milestone ? { milestone, key } : undefined
     }
-    if (value.includes("/work_plan.md") || value.includes('"work_plan.md"')) {
+    if (relative === "work_plan.md") {
       return match(current.id === "paper-writing" ? "outline" : "plan", "work_plan")
     }
     if (current.id === "idea-analysis") {
-      if (value.includes("search_queries.json")) return match("directions", "search_queries")
-      if (value.includes("similar_papers.json")) return match("directions", "similar_papers")
-      if (value.includes("report.json")) return match("report", "report_json")
-      if (value.includes("report.md")) return match("report", "report_md")
+      if (relative === "artifacts/search_queries.json") return match("directions", "search_queries")
+      if (relative === "artifacts/similar_papers.json") return match("directions", "similar_papers")
+      if (relative === "report.json") return match("report", "report_json")
+      if (relative === "report.md") return match("report", "report_md")
     }
     if (current.id === "literature-review") {
-      if (value.includes("papers_selected.json")) return match("select", "papers_selected")
-      if (value.includes("report.json")) return match("report", "report_json")
-      if (value.includes("report.md")) return match("report", "report_md")
+      if (relative === "artifacts/papers_selected.json") return match("select", "papers_selected")
+      if (relative === "report.json") return match("report", "report_json")
+      if (relative === "report.md") return match("report", "report_md")
     }
     if (current.id !== "paper-writing") return undefined
-    if (value.includes("outline.json")) return match("outline", "outline")
-    const section = value.match(
-      /artifacts\/sections\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex/,
+    if (relative === "artifacts/outline.json") return match("outline", "outline")
+    const section = relative.match(
+      /^artifacts\/sections\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex$/,
     )
     if (section) return match("english", `section:${section[1]}`)
-    if (value.includes("paper.tex")) return match("english", "paper_tex")
-    if (value.includes("references.bib")) return match("english", "references")
-    const sectionZh = value.match(
-      /artifacts\/sections_zh\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex/,
+    if (relative === "artifacts/paper.tex") return match("english", "paper_tex")
+    if (relative === "artifacts/references.bib") return match("english", "references")
+    const sectionZh = relative.match(
+      /^artifacts\/sections_zh\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex$/,
     )
     if (sectionZh) return match("chinese", `section:${sectionZh[1]}`)
-    if (value.includes("paper_zh.tex")) return match("chinese", "paper_zh_tex")
-    if (value.includes("report.json")) return match("report", "report_json")
-    if (value.includes("report.md")) return match("report", "report_md")
+    if (relative === "artifacts/paper_zh.tex") return match("chinese", "paper_zh_tex")
+    if (relative === "report.json") return match("report", "report_json")
+    if (relative === "report.md") return match("report", "report_md")
     return undefined
   }
 
+  const invalidatePaperBuild = (items: Array<{ milestone: ProgressMilestone; key: string }>) => {
+    if (current?.id !== "paper-writing") return
+    if (!items.some((item) => ["english", "chinese"].includes(item.milestone.id))) return
+    const compile = current.milestones.find((item) => item.id === "compile")!
+    const report = current.milestones.find((item) => item.id === "report")!
+    if (compile.status === "pending" && !compile.done?.size && report.status === "pending") return
+    compile.status = "pending"
+    compile.done?.clear()
+    report.status = "pending"
+    report.done?.clear()
+    if (active === compile || active === report) active = undefined
+    activeSince = Date.now()
+    timerRunning = true
+    note = "论文源文件已更新,等待重新编译"
+  }
+
   const completeArtifact = (item: { milestone: ProgressMilestone; key: string }) => {
     item.milestone.done?.add(item.key)
     if (!item.milestone.total || item.milestone.done?.size !== item.milestone.total) return
@@ -643,14 +671,17 @@ function createResearch67Progress() {
       }
       if (current?.id === "paper-writing" && lower === "compilelatex") {
         const milestone = current.milestones.find((item) => item.id === "compile")!
+        const key = content.toLowerCase().includes("paper_zh.tex") ? "paper_zh_pdf" : "paper_pdf"
+        milestone.done?.delete(key)
+        if (milestone.status === "completed") milestone.status = "pending"
         startMilestone(milestone)
-        enqueueArtifacts(lower, [
-          { milestone, key: content.toLowerCase().includes("paper_zh.tex") ? "paper_zh_pdf" : "paper_pdf" },
-        ])
+        enqueueArtifacts(lower, [{ milestone, key }])
         return
       }
       if (current?.id === "paper-writing" && lower === "buildresearchpaper") {
         const milestone = current.milestones.find((item) => item.id === "compile")!
+        milestone.status = "pending"
+        milestone.done?.clear()
         startMilestone(milestone)
         enqueueArtifacts(lower, [
           { milestone, key: "paper_pdf" },
@@ -658,8 +689,15 @@ function createResearch67Progress() {
         ])
         return
       }
-      const item = artifact(content)
-      if (!item) return
+      if (lower !== "writefile") return
+      const item = artifact(getWriteFilePath(content))
+      if (!item) {
+        enqueueArtifacts(lower, [])
+        return
+      }
+      if (current?.id === "literature-review" && item.milestone.id === "select" && searchCalls) {
+        current.milestones.find((milestone) => milestone.id === "search")!.status = "completed"
+      }
       startMilestone(item.milestone)
       enqueueArtifacts(lower, [item])
     },
@@ -676,7 +714,10 @@ function createResearch67Progress() {
       }
       const items = dequeueArtifacts(lower)
       if (!items) return
-      if (!failed) items.forEach(completeArtifact)
+      if (!failed) {
+        items.forEach(completeArtifact)
+        invalidatePaperBuild(items)
+      }
       if (failed) {
         for (const item of items) item.milestone.status = "failed"
         note = "当前操作失败,等待重试"

+ 111 - 10
packages/opencode/test/tool/agentpaas-run.test.ts

@@ -63,6 +63,11 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
         status: "running",
       },
     ],
+    [
+      "tool_call",
+      { tool: "ReadFile", step: "lit-upstream", content: "{'file_path':'/run/idea-analysis/report.json'}" },
+    ],
+    ["tool_result", { tool: "ReadFile", step: "lit-upstream", content: "idea analysis report" }],
     [
       "confirm_required",
       {
@@ -109,6 +114,24 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
         status: "running",
       },
     ],
+    [
+      "tool_call",
+      {
+        tool: "ReadFile",
+        step: "paper-upstream",
+        content: '{"file_path":"/run/literature-review/report.json"}',
+      },
+    ],
+    ["tool_result", { tool: "ReadFile", step: "paper-upstream", content: "literature review report" }],
+    [
+      "tool_call",
+      {
+        tool: "WriteFile",
+        step: "paper-scratch",
+        content: '{"file_path":"/run/paper-writing/scratch.md","content":"later write /run/paper-writing/report.json"}',
+      },
+    ],
+    ["tool_result", { tool: "WriteFile", step: "paper-scratch", content: "[OK] Created scratch.md" }],
     ["tool_call", { tool: "WriteFile", step: 4, content: '{"file_path":"/run/paper-writing/artifacts/outline.json"}' }],
     ["tool_result", { tool: "WriteFile", step: 4, content: "[OK] Created outline.json" }],
     [
@@ -125,11 +148,30 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
           '{"phase_dir":"/run/paper-writing","papers_selected_path":"/run/literature-review/artifacts/papers_selected.json"}',
       },
     ],
-    ["tool_result", { tool: "BuildResearchPaper", step: 6, content: '{"status":"completed","passed":true}' }],
-    ["tool_call", { tool: "WriteFile", step: 7, content: '{"file_path":"/run/paper-writing/report.json"}' }],
-    ["tool_call", { tool: "WriteFile", step: 7, content: '{"file_path":"/run/paper-writing/report.md"}' }],
-    ["tool_result", { tool: "WriteFile", step: 7, content: "[OK] Created report.json" }],
-    ["tool_result", { tool: "WriteFile", step: 7, content: "[OK] Created report.md" }],
+    ["tool_result", { tool: "BuildResearchPaper", step: 6, content: '{"status":"failed","passed":false}' }],
+    [
+      "tool_call",
+      {
+        tool: "WriteFile",
+        step: "paper-repair",
+        content: '{"file_path":"/run/paper-writing/artifacts/sections/abstract.tex"}',
+      },
+    ],
+    ["tool_result", { tool: "WriteFile", step: "paper-repair", content: "[OK] Updated abstract.tex" }],
+    [
+      "tool_call",
+      {
+        tool: "BuildResearchPaper",
+        step: 7,
+        content:
+          '{"phase_dir":"/run/paper-writing","papers_selected_path":"/run/literature-review/artifacts/papers_selected.json"}',
+      },
+    ],
+    ["tool_result", { tool: "BuildResearchPaper", step: 7, content: '{"status":"completed","passed":true}' }],
+    ["tool_call", { tool: "WriteFile", step: 8, content: '{"file_path":"/run/paper-writing/report.json"}' }],
+    ["tool_call", { tool: "WriteFile", step: 8, content: '{"file_path":"/run/paper-writing/report.md"}' }],
+    ["tool_result", { tool: "WriteFile", step: 8, content: "[OK] Created report.json" }],
+    ["tool_result", { tool: "WriteFile", step: 8, content: "[OK] Created report.md" }],
     [
       "stage_passed",
       {
@@ -230,6 +272,16 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
         ]),
       }),
     )
+    const litUpstream = updates.find(
+      (update) => update.metadata?.latest_event === "tool_result" && update.metadata.step === "lit-upstream",
+    )?.metadata?.research67_progress
+    expect(litUpstream).toMatchObject({
+      title: "Research67 · lit-searcher · 文献检索 · 制定检索计划",
+      rows: expect.arrayContaining([
+        expect.objectContaining({ id: "literature-review:plan", status: "running" }),
+        expect.objectContaining({ id: "literature-review:report", status: "pending" }),
+      ]),
+    })
     expect(snapshots).toContainEqual(
       expect.objectContaining({
         footer: "正在进行 Guard / 阶段产物验收",
@@ -237,6 +289,55 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
         rows: expect.arrayContaining([expect.objectContaining({ id: "idea-analysis:report", status: "completed" })]),
       }),
     )
+    for (const step of ["paper-upstream", "paper-scratch"]) {
+      expect(
+        updates.find((update) => update.metadata?.latest_event === "tool_result" && update.metadata.step === step)
+          ?.metadata?.research67_progress,
+      ).toMatchObject({
+        title: "Research67 · paper-writer · 论文撰写 · 写作计划与大纲",
+        rows: expect.arrayContaining([
+          expect.objectContaining({ id: "paper-writing:outline", status: "running" }),
+          expect.objectContaining({ id: "paper-writing:compile", status: "pending" }),
+          expect.objectContaining({ id: "paper-writing:report", status: "pending" }),
+        ]),
+      })
+    }
+    expect(
+      updates.find(
+        (update) =>
+          update.metadata?.latest_event === "tool_result" &&
+          update.metadata.latest_tool === "BuildResearchPaper" &&
+          update.metadata.step === 6,
+      )?.metadata?.research67_progress,
+    ).toMatchObject({
+      rows: expect.arrayContaining([
+        expect.objectContaining({ id: "paper-writing:compile", status: "failed" }),
+        expect.objectContaining({ id: "paper-writing:report", status: "pending" }),
+      ]),
+      footer: "当前操作失败,等待重试",
+    })
+    expect(
+      updates.find(
+        (update) => update.metadata?.latest_event === "tool_result" && update.metadata.step === "paper-repair",
+      )?.metadata?.research67_progress,
+    ).toMatchObject({
+      rows: expect.arrayContaining([
+        expect.objectContaining({ id: "paper-writing:compile", status: "pending" }),
+        expect.objectContaining({ id: "paper-writing:report", status: "pending" }),
+      ]),
+      footer: "论文源文件已更新,等待重新编译",
+    })
+    expect(
+      updates.find(
+        (update) =>
+          update.metadata?.latest_event === "tool_call" &&
+          update.metadata.latest_tool === "BuildResearchPaper" &&
+          update.metadata.step === 7,
+      )?.metadata?.research67_progress,
+    ).toMatchObject({
+      title: "Research67 · paper-writer · 论文撰写 · 编译双语 PDF",
+      rows: expect.arrayContaining([expect.objectContaining({ id: "paper-writing:compile", status: "running" })]),
+    })
     expect(snapshots).toContainEqual(
       expect.objectContaining({
         rows: expect.arrayContaining([
@@ -451,11 +552,11 @@ test("preserves completed artifacts and forbids automatic reruns after failure",
       updates.find((update) => update.metadata?.latest_event === "tool_result" && update.metadata.step === "repair")
         ?.metadata?.research67_progress,
     ).toMatchObject({
-      title: "Research67 · paper-writer · 论文撰写 · 编译双语 PDF",
-      footer: "当前操作失败,等待重试",
+      title: "Research67 · paper-writer · 论文撰写 · 英文稿与参考文献",
+      footer: "论文源文件已更新,等待重新编译",
       rows: expect.arrayContaining([
-        expect.objectContaining({ id: "paper-writing:english", status: "completed" }),
-        expect.objectContaining({ id: "paper-writing:compile", status: "failed" }),
+        expect.objectContaining({ id: "paper-writing:english", status: "running" }),
+        expect.objectContaining({ id: "paper-writing:compile", status: "pending" }),
       ]),
     })
     const attemptStarts = updates.filter((update) => update.metadata?.latest_event === "stage_started")
@@ -485,7 +586,7 @@ test("preserves completed artifacts and forbids automatic reruns after failure",
       workspace_path: workspacePath,
       research67_progress: {
         rows: expect.arrayContaining([
-          expect.objectContaining({ id: "paper-writing:outline", status: "completed" }),
+          expect.objectContaining({ id: "paper-writing:outline", status: "failed" }),
           expect.objectContaining({ id: "paper-writing:compile", status: "failed" }),
           expect.objectContaining({ id: "paper-writing:report", status: "pending" }),
         ]),