diff --git a/.doc/audit/ISSUES.md b/.doc/audit/ISSUES.md index 53ded071..fd05c662 100644 --- a/.doc/audit/ISSUES.md +++ b/.doc/audit/ISSUES.md @@ -6,6 +6,11 @@ | issue_id | 模块 / 检索词 | 问题 | 严重度 | issue_state | owner | 证据日期 / 范围 | 唯一主记录 | 经验 | |---|---|---|---|---|---|---|---|---| +| ISSUE-20260928-task-write-verb | intent / hasTaskWriteVerb / 帮我定 / 制定 / AMBIGUOUS_FALLBACK | 「帮我定/制定」设计开发计划任务书未进入任务触发,对话只有掂量阶段模板 | medium | resolved | Cursor | 2026-09-28;test sess-p8-u33 msg 89/90;意图单测 | [主记录](../ops/short-term/2026-09-28-test-user-doc-issues.md) | [LESSON-20260928-task-write-verb-visible-refusal](../ops/lessons/2026-09-28-task-write-verb-visible-refusal.md) | +| ISSUE-20260928-workflow-scope-reply | coordinator / ClarificationQuestion / ErrPrerequisiteNotMet / rd_design_plan | 跨工作流澄清与研发前置未满足时,原因没有进入对话 | medium | resolved | Cursor | 2026-09-28;协调器单测。test 原例停在动词未命中,未走到本分支 | [主记录](../ops/short-term/2026-09-28-test-user-doc-issues.md) | [LESSON-20260928-task-write-verb-visible-refusal](../ops/lessons/2026-09-28-task-write-verb-visible-refusal.md) | +| ISSUE-20260928-quality-failed-success-copy | bizdev / quality_failed / 正文已保存至 / output.md | 质量未通过仍声称正文已保存,并写出工作区绝对路径 | medium | resolved | Cursor | 2026-09-28;test cdo_quote_1790560155851280170;同步回复单测 | [主记录](../ops/short-term/2026-09-28-test-user-doc-issues.md) | [LESSON-20260928-task-write-verb-visible-refusal](../ops/lessons/2026-09-28-task-write-verb-visible-refusal.md) | +| ISSUE-20260928-pi-runner-data-dir | agent / PI runner / DATA_DIR / MISSING_TARGET / 18866 | test 启动时根 .env 的 dev runner 地址盖过本环境 sidecar,工作区被重写到空目录 | high | resolved | Cursor | 2026-09-28;dev `4f581306`。当时 test 二进制仍是修复前制品 | [验收报告](../test-reports/2026-09-28-pi-runner-env-mismatch-test-report.md) | — | +| ISSUE-20260928-material-chunk-embed | knowledge / chunk index failed / 8192 / 34635 tokens | 材料分块批超过嵌入模型上下文,索引失败 | medium | open | — | 2026-09-28;test 日志 project 8,文件 W020251117353305579317.docx | [主记录](../ops/short-term/2026-09-28-test-user-doc-issues.md) | 本轮不修 | | ISSUE-20260928-attachment-only-turn | chat / chat/stream / message cannot be empty / HTTP_400 / attachments | 只上传文档、不输入文字时 turn 被空消息 400 拒绝,前端不读信封,用户看不到任何回复 | high | resolved | Cursor | 2026-09-28;dev :8080 重载后浏览器仅附件得到梳理审阅,无关文字先回答再追问;pending 分支 API 200 | [主记录](../ops/short-term/2026-09-28-attachment-only-turn.md) | 空消息门禁要放在附件判定之后;4xx 必须读信封。见[测试报告](../test-reports/2026-09-28-attachment-only-turn-test-report.md) | | ISSUE-20260928-task-run-replay-502 | web / useRecoveredTaskPhases / tasks/runs / ERR_HTTP2_PROTOCOL_ERROR / 502 | 历史失败气泡恢复订阅 task run 事件时出现 HTTP/2 协议错误和 502,与附件空消息 400 无因果关系 | medium | investigating | Cursor | 2026-09-28;用户控制台 `run_1790558010622744831`;本轮未改订阅逻辑 | [主记录](../ops/short-term/2026-09-28-attachment-only-turn.md#范围外) | 未修,保持观察 | | ISSUE-20260928-activation-preview-precheck | web / projectSettings / activation-preview / NO_GOVERNED_DOCUMENT / clientDutyIssues | 全部文档未纳入人工审批时前端仍 POST activation-preview,控制台 400 | medium | resolved | Cursor | 2026-09-28;dev 项目 6023 默认计划无 activation-preview 请求,兼任后 preview 200 | [主记录](../ops/short-term/2026-09-28-activation-preview-precheck.md#issue-20260928-activation-preview-precheck) | [LESSON-20260928-activation-preview-client-gate](../ops/lessons/2026-09-28-activation-preview-client-gate.md) | diff --git a/.doc/ops/lessons/2026-09-28-task-write-verb-visible-refusal.md b/.doc/ops/lessons/2026-09-28-task-write-verb-visible-refusal.md new file mode 100644 index 00000000..13ce0a33 --- /dev/null +++ b/.doc/ops/lessons/2026-09-28-task-write-verb-visible-refusal.md @@ -0,0 +1,20 @@ +--- +lesson_id: LESSON-20260928-task-write-verb-visible-refusal +lesson_state: candidate +owner: Cursor +reviewer: Cursor(自审,待含本提交的运行制品复验原句) +last_reviewed: 2026-09-28 +reviewed_revision: db859e185ebcd0e7eccd2241ca07f0e9f4fe4c6b +source_issues: [ISSUE-20260928-task-write-verb, ISSUE-20260928-workflow-scope-reply, ISSUE-20260928-quality-failed-success-copy] +keywords: [intent, hasTaskWriteVerb, 帮我定, ClarificationQuestion, ErrPrerequisiteNotMet, quality_failed] +--- + +# 任务写动词要覆盖「定」,拒绝原因要进对话 + +- 触发条件:用户说「帮我定/制定/拟定」某交付物,对话却进入「再掂量一下」;或任务被工作流范围、研发前置、质量门禁挡住,气泡仍像成功或只有协调失败。 +- 机制与来源:强写动词表原先只有生成/做/跑/出/编写/编制/起草。「定」单独出现会误伤「决定」,所以只认「制定」「拟定」以及「帮我/请/给我/帮忙」后面的「定/拟」。跨流澄清和前置失败必须成为用户可见回复,不能再被阶段模板或错误事件盖掉。质量未通过不要写宿主机路径。见 ISSUE-20260928-task-write-verb。 +- 开发检查:新增交付动词时补强动词或祈使短语,并加一条反例(决定、看看)。`RequiresClarifying` 的问题原文要到达会话。`ErrPrerequisiteNotMet` 的中文原因用拒绝态返回,错误值为空,避免会话层丢掉 `NaturalReply`。 +- 验证方式:`go test ./internal/intent/ ./internal/coordinator/` 中写动词、跨流澄清、前置可见、质量失败文案四类用例。原句在运行制品上的复验完成前,本条保持 candidate。 +- 适用边界:只覆盖中文任务触发与同步回复。材料嵌入分块超限、PI runner 数据目录错配是相邻但独立的问题。 +- 防护落实:`doc_workflow_scope_test.go` 与 `pending_confirmation_reply_test.go`。 +- 修订与失效:若产品明确「定」永不表示编写,应删祈使短语并退役本条。 diff --git a/.doc/ops/lessons/INDEX.md b/.doc/ops/lessons/INDEX.md index 7f4eb686..12baec2f 100644 --- a/.doc/ops/lessons/INDEX.md +++ b/.doc/ops/lessons/INDEX.md @@ -4,6 +4,7 @@ | lesson_id | 触发条件 / 关键词 | 注意事项 | lesson_state | 主记录 / 最近复核 | |---|---|---|---|---| +| LESSON-20260928-task-write-verb-visible-refusal | 帮我定、制定、ClarificationQuestion、前置条件、quality_failed | 写动词覆盖制定与帮我定;拒绝和澄清进对话;质量失败不写宿主机路径 | candidate | [写动词与可见拒绝](2026-09-28-task-write-verb-visible-refusal.md);2026-09-28 | | LESSON-20260928-activation-preview-client-gate | activation-preview、NO_GOVERNED_DOCUMENT、clientDutyIssues、400 | 服务端激活门禁要有同等前端预检;live_edit 不套激活门禁 | validated | [激活预检对齐](2026-09-28-activation-preview-client-gate.md);2026-09-28 | | LESSON-20260928-squad-commit-as-author | 小队落盘、uow permission denied、AutoCommitUserID、OnBehalfOf、非编制人 | 自动提交身份用文档编制人,触发者只写入 OnBehalfOf;不要放宽 ActionCommit | candidate | [编制人代理提交](2026-09-28-squad-commit-as-author.md);2026-09-28 | | LESSON-20260928-standard-role-reseed | 注册、role-guest、system_roles、is_system、23503、23505、INTERNAL | 硬编码标准角色须幂等补种并标 is_system;用户名冲突返回领域 400,不要包进 insert user | validated | [标准角色可自愈](2026-09-28-standard-role-reseed.md);2026-09-28 | diff --git a/.doc/ops/short-term/2026-09-28-test-user-doc-issues.md b/.doc/ops/short-term/2026-09-28-test-user-doc-issues.md new file mode 100644 index 00000000..66e1a9d6 --- /dev/null +++ b/.doc/ops/short-term/2026-09-28-test-user-doc-issues.md @@ -0,0 +1,54 @@ +--- +title: "修复第三方 test 用户文档三问题" +task_id: test-user-doc-issues-2026-09-28 +status: in_progress +created: 2026-09-28 +updated: 2026-09-28 +owner: Cursor +task_type: fix +policy_revision: "AGENTS.md + agent-worktree-workflow @ 2026-09-21" +branch: fix/test-user-doc-issues-2026-09-28 +worktree: .worktrees/test-user-doc-issues-2026-09-28 +base_dev_sha: "8f77053f5169502eaf3e4e4135a082e8914f3704" +write_scope: + - internal + - .doc +mutex: intent-write-verb +review_state: pending +integration_state: pending +deployment: pending +cleanup: pending +remote_sync: not_requested +--- + +# 修复第三方 test 用户文档三问题 + +## 用户目标与授权 + +用户提供《AI 测试问题.docx》(放在仓库 `.tmp/`)。授权:阅读图文、定位并修复第三方用户在 test 环境遇到的问题。 + +## 检索与重叠 + +- 检索词:`帮我定设计开发计划任务书`、`hasTaskWriteVerb`、`质量检查未通过`、`PI runner data_dir`。 +- P1(只上传附件被空消息拒绝)已由 ISSUE-20260928-attachment-only-turn 在 dev 修复。 +- catalog overlap(无 scope 绑定)hard/soft 为空。带 `--intent` 注册被 `same_scope` 拒绝,持有者 `agent-reply-llm-narrative-2026-09-28`(scopes `internal`+`.doc`)。本任务不改其文件(`dialogue_llm.go` / `intent_reply_llm.go` 等),只改意图动词与任务回复文案。 +- P3/P4 的 runner 数据目录错配由并行任务 `pi-runner-env-mismatch-2026-09-28`(`4f581306`)修复。开工时其 integrate 为 DIRTY_TREE;收口前已确认该提交是 local dev `193a752f` 的祖先。本任务不代合入、不改其文件。 + +## 事实 + +test 库 `agent_system_test`(127.0.0.1:5434)会话 `sess-p8-u33`(2026-09-28): + +- 「帮我定设计开发计划任务书」落到 `handleAmbiguousConsult` 模板「完全理解您想再掂量一下」,未触发 `rd_design_plan`。`hasTaskWriteVerb` 不含「制定 / 帮我定」。 +- 「帮我生成报价单」回复声称正文已保存到宿主机 `cdo_quote_*/output.md`。该工作区 `quality_report.passed=false`(缺报价范围/模型/说明/约束)。质量失败仍走成功文案。 +- `bd_intake` run `run_1790558895559080556` 为 `quality_failed`,writer 修复步 `MISSING_TARGET`。sidecar 把 test 工作区交给了数据目录不一致的 runner(见并行任务提交说明)。 + +## 修复 + +- 写动词增加「制定」「拟定」,以及祈使「帮我定 / 请定」;「决定」不计。 +- 质量未通过或未完成时,同步回复改用 `CompletionSummary`,不展示宿主机路径、不声称正文已保存。 +- `ErrPrerequisiteNotMet` 以中文前置条件回复结束回合,不再把哨兵错误当成协调失败丢掉回复。 +- 模糊咨询若已带澄清问题,直接透传,不再覆盖成阶段对照模板。 + +## 验证 + +单测与 test 进程核对见 [测试报告](../../test-reports/2026-09-28-test-user-doc-issues-test-report.md)。报价单未在本轮重跑。test 进程 pid 1450126 已指向 18866 与 `data/test`,二进制来自主目录 dev `193a752f`,不含本提交。 diff --git a/.doc/test-reports/2026-09-28-test-user-doc-issues-test-report.md b/.doc/test-reports/2026-09-28-test-user-doc-issues-test-report.md new file mode 100644 index 00000000..331cfa6c --- /dev/null +++ b/.doc/test-reports/2026-09-28-test-user-doc-issues-test-report.md @@ -0,0 +1,96 @@ +# 第三方 test 用户文档问题 测试报告 + +## 一、 测试背景 (Background) + +第三方用户在 test 环境整理了《AI 测试问题》。只上传附件被空消息拒绝已由 ISSUE-20260928-attachment-only-turn 修复。本轮处理另外两类: + +- 「帮我定设计开发计划任务书」没有进入任务,对话是掂量阶段模板。 +- 「帮我生成报价单」气泡写成正文已保存到宿主机路径,工作区实际是质量未通过的阻塞说明。根因是 PI runner 数据目录错配,已由 `4f581306` 合入 local dev。 + +主记录:[2026-09-28-test-user-doc-issues](../ops/short-term/2026-09-28-test-user-doc-issues.md)。 + +## 二、 测试目标 (Test Objectives) + +- 「帮我定设计开发计划任务书」在商务流激活时进入任务,并带出研发流澄清;「决定」不触发编写。 +- 研发前置未满足时,用户看到中文原因,而不是协调失败。 +- 质量未通过的同步回复使用质量摘要,不写宿主机路径,不声称正文已保存。 +- 核对 test 进程是否已经指向本环境 runner 与 `data/test`。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +- 源码:worktree `.worktrees/test-user-doc-issues-2026-09-28`,提交 `db859e185ebcd0e7eccd2241ca07f0e9f4fe4c6b`,基线 `8f77053f`。 +- 命令:`go test ./internal/intent/ ./internal/coordinator/ -count=1 -timeout 180s`,以及 `-run` 聚焦用例;`go vet` 同两包。 +- test 运行制品:`bin/agent-server-test` 修改时间 2026-09-28 04:04:03 +0000,主目录 `HEAD` `193a752f`(含 `4f581306`)。进程 pid `1450126`,`ENV=test`,`DATA_DIR=.../data/test`,`PI_AGENT_RUNNER_URL=http://127.0.0.1:18866`。18866 的 `data_dir` 为 `data/test/workspaces`。本提交的回复文案尚未打进该二进制。 +- 未用第三方账号再跑「生成报价单」。该操作会写入其项目;本会话没有登录凭证。runner 数据目录的产品链路证据见 [PI runner 环境错配报告](./2026-09-28-pi-runner-env-mismatch-test-report.md) 的 `bd_research` 实测。竞品视频未发现独立处理缺陷,未扩测。 + +## 四、 预期结果 (Expected Results) + +- 「帮我定设计开发计划任务书」+ 激活 `main`:`TRIGGER_TASK`,工作流范围不匹配,澄清含研发设计控制流程,`RequiresClarifying`。 +- 激活 `rdev` 时「帮我制定…」命中 `rd_design_plan` 且不拦截。 +- 「我决定先放一放,再看看进展」不是 `TRIGGER_TASK`。 +- 质量失败回复不含「正文已保存」和 `output.md` 绝对路径。 +- 前置未满足回复含「请先完成商务对接流程」,错误值为空。 +- 已完成回复仍含「正文已保存。」 +- test 进程 runner 为 18866,数据目录为 test。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 用例 1:帮我定 + 商务流 + +- **输入**:`TestWriteVerb_DingDesignPlanCrossFlow` +- **实际输出**:`intent=TRIGGER_TASK branch=NEW_INTENT_PREEMPT confidence=0.86`,`PASS` +- **判定**:PASS +- **耗时**:包内 <0.01s + +### 用例 2:制定 + 研发流 + +- **输入**:`TestWriteVerb_ZhidingDesignPlanOnRdev` +- **实际输出**:同上 `TRIGGER_TASK`,`PASS`(断言任务键 `rd_design_plan` 且无范围拦截) +- **判定**:PASS + +### 用例 3:决定不是编写 + +- **输入**:`TestWriteVerb_DecideIsNotWrite` +- **实际输出**:`QUERY_PROGRESS` / `CASUAL_CHAT`,无 `TRIGGER_TASK`,`PASS` +- **判定**:PASS + +### 用例 4:质量失败文案 + +- **输入**:`TestFormatTriggerTaskReply_QualityFailedOmitsHostPath` +- **实际输出**:`PASS`(回复走质量摘要,不含宿主机路径) +- **判定**:PASS + +### 用例 5:前置条件可见 + +- **输入**:`TestSyncTriggerTask_PrerequisiteIsUserVisible` +- **实际输出**:`PASS` +- **判定**:PASS + +### 用例 6:成功文案仍保留「正文已保存。」 + +- **输入**:`TestSyncTriggerTask_Committed_KeepsSavedWording` +- **实际输出**:`PASS` +- **判定**:PASS + +### 用例 7:test 进程 runner 指向 + +- **输入**:读 pid `1450126` 的 `ENV`、`DATA_DIR`、`PI_AGENT_RUNNER_URL`;`GET` 18866 与 8081 `/health`;在二进制中查找 `RUNNER_MISMATCH` 用户文案。 +- **实际输出**:`ENV=test`;`DATA_DIR=/home/weichen/projects/Agent-System/./data/test`;`PI_AGENT_RUNNER_URL=http://127.0.0.1:18866`;18866 `data_dir=/home/weichen/projects/Agent-System/data/test/workspaces`;health `schema_version=78` `agent_backend=pi-agent`;二进制含「模型执行环境与数据目录不一致」。 +- **判定**:PASS(runner 接线)。报价单全链路本轮未运行。 +- **耗时**:健康检查 <1s。 + +日志:`.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-focused.txt`、`go-test-packages.txt`、`go-vet.txt`。 + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +| 命令 | 结果 | +|---|---| +| `go test ./internal/intent/ ./internal/coordinator/ -count=1 -timeout 180s` | PASS(intent 0.078s,coordinator 0.015s) | +| 聚焦 `-run 'TestWriteVerb_\|TestFormatTriggerTaskReply_QualityFailed\|TestSyncTriggerTask_Prerequisite\|TestSyncTriggerTask_Committed_KeepsSavedWording'` | PASS | +| `go vet ./internal/intent/ ./internal/coordinator/` | 无输出,退出码 0 | + +未跑全量 `go test ./...`,也未跑 nightly 五项。本变更不改运行模板。材料嵌入超限(约 34635 tokens,模型上限 8192)只登记 ISSUE-20260928-material-chunk-embed,未测修复。 + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +意图动词、澄清透传、前置可见、质量失败文案由单测覆盖,达到本任务的代码验收。test 当前制品已指向本环境 runner,runner 错配修复在 dev `193a752f`。本提交的回复文案还没进 `bin/agent-server-test`。报价单没有在本轮用第三方项目重跑。嵌入分块超限仍开放。 diff --git a/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-focused.txt b/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-focused.txt new file mode 100644 index 00000000..01505695 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-focused.txt @@ -0,0 +1,23 @@ +=== RUN TestWriteVerb_DingDesignPlanCrossFlow +2026/09/28 04:05:41 INFO intent_recognition intent=TRIGGER_TASK branch=NEW_INTENT_PREEMPT confidence=0.86 +--- PASS: TestWriteVerb_DingDesignPlanCrossFlow (0.00s) +=== RUN TestWriteVerb_ZhidingDesignPlanOnRdev +2026/09/28 04:05:41 INFO intent_recognition intent=TRIGGER_TASK branch=NEW_INTENT_PREEMPT confidence=0.86 +--- PASS: TestWriteVerb_ZhidingDesignPlanOnRdev (0.00s) +=== RUN TestWriteVerb_DecideIsNotWrite +2026/09/28 04:05:41 INFO intent_recognition intent=QUERY_PROGRESS branch=NEW_INTENT_PREEMPT confidence=0.9 +2026/09/28 04:05:41 INFO intent_recognition intent=CASUAL_CHAT branch=AMBIGUOUS_FALLBACK confidence=0.2 +2026/09/28 04:05:41 INFO intent_recognition intent=QUERY_PROGRESS branch=NEW_INTENT_PREEMPT confidence=0.9 +--- PASS: TestWriteVerb_DecideIsNotWrite (0.00s) +PASS +ok chat-agent-system/internal/intent 0.008s +=== RUN TestSyncTriggerTask_Committed_KeepsSavedWording +2026/09/28 04:05:41 INFO coordinator_route intent=TRIGGER_TASK branch=NEW_INTENT_PREEMPT confidence=0 channel=SQUAD_FIXED reason=标准任务/阶段推进/工作流运行/长任务续跑指令,进入小队受控执行 +--- PASS: TestSyncTriggerTask_Committed_KeepsSavedWording (0.00s) +=== RUN TestFormatTriggerTaskReply_QualityFailedOmitsHostPath +--- PASS: TestFormatTriggerTaskReply_QualityFailedOmitsHostPath (0.00s) +=== RUN TestSyncTriggerTask_PrerequisiteIsUserVisible +2026/09/28 04:05:41 INFO coordinator_route intent=TRIGGER_TASK branch=NEW_INTENT_PREEMPT confidence=0 channel=SQUAD_FIXED reason=标准任务/阶段推进/工作流运行/长任务续跑指令,进入小队受控执行 +--- PASS: TestSyncTriggerTask_PrerequisiteIsUserVisible (0.00s) +PASS +ok chat-agent-system/internal/coordinator 0.007s diff --git a/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-packages.txt b/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-packages.txt new file mode 100644 index 00000000..b5eca2df --- /dev/null +++ b/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-test-packages.txt @@ -0,0 +1,2 @@ +ok chat-agent-system/internal/intent 0.078s +ok chat-agent-system/internal/coordinator 0.015s diff --git a/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-vet.txt b/.doc/test-reports/assets/2026-09-28-test-user-doc-issues/go-vet.txt new file mode 100644 index 00000000..e69de29b diff --git a/internal/coordinator/engine_consult.go b/internal/coordinator/engine_consult.go index 8e40ae37..8946bccb 100644 --- a/internal/coordinator/engine_consult.go +++ b/internal/coordinator/engine_consult.go @@ -11,6 +11,17 @@ import ( // handleAmbiguousConsult 处理用户犹豫或征询建议 (分支 3 模糊分忧) func (e *CoordinatorEngine) handleAmbiguousConsult(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) { + if cand != nil && strings.TrimSpace(cand.ClarificationQuestion) != "" { + return &CoordinationResult{ + Status: StatusClarificationRequired, + Intent: cand.Intent, + Branch: cand.Branch, + ClarificationPrompt: cand.ClarificationQuestion, + NaturalReply: cand.ClarificationQuestion, + UpdatedContext: userCtx, + }, nil + } + currentStage := userCtx.Project.CurrentStage projName := userCtx.Project.Name if projName == "" { diff --git a/internal/coordinator/engine_task.go b/internal/coordinator/engine_task.go index 41e9de0f..499e9469 100644 --- a/internal/coordinator/engine_task.go +++ b/internal/coordinator/engine_task.go @@ -10,6 +10,7 @@ import ( "chat-agent-system/internal/app/stageflow" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/intent" + "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowscope" "chat-agent-system/internal/workspace" @@ -124,6 +125,20 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent. UpdatedContext: userCtx, }, nil } + if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) { + msg := userFacingPrerequisite(err) + if taskRes != nil && strings.Contains(taskRes.ErrorReason, "请先完成商务对接流程") { + msg = taskRes.ErrorReason[strings.Index(taskRes.ErrorReason, "请先完成商务对接流程"):] + } + return &CoordinationResult{ + Status: StatusRejected, + Intent: cand.Intent, + Branch: cand.Branch, + RejectReason: err.Error(), + NaturalReply: msg, + UpdatedContext: userCtx, + }, nil + } return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, @@ -158,17 +173,30 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent. // formatTriggerTaskReply builds sync coordinator copy for a TaskRunResult. // Pending high-risk candidates must not claim "正文已保存 / 已完成". +func userFacingPrerequisite(err error) string { + const hint = "请先完成商务对接流程" + if err != nil && strings.Contains(err.Error(), hint) { + return err.Error()[strings.Index(err.Error(), hint):] + } + return "请先完成商务对接流程(CDO 报价批准)后再启动研发设计控制流程" +} + func formatTriggerTaskReply(taskRes *stageflow.TaskRunResult, outPath string) (reply string, taskStatus string) { + _ = outPath // 不把宿主机工作区路径写进用户可见回复 if taskRes == nil { return "任务执行失败。", stageflow.TaskStatusFailed } taskStatus = taskRes.TerminalStatus() - if taskRes.AwaitsAgentConfirmation() { + if taskRes.AwaitsAgentConfirmation() || taskStatus != stageflow.TaskStatusCompleted { return taskRes.CompletionSummary(), taskStatus } var sb strings.Builder - sb.WriteString(fmt.Sprintf("已为您自适应规划并完成生成【%s】!\n交付物正文已保存至:%s\n耗时: %v\n", - taskRes.ArtifactTitle, outPath, taskRes.Duration)) + sb.WriteString(fmt.Sprintf("已为您自适应规划并完成生成【%s】!\n", taskRes.ArtifactTitle)) + if taskRes.DocRecord != nil && taskRes.DocRecord.DocID != "" { + sb.WriteString(fmt.Sprintf("交付物已写入受控文档:%s\n", taskRes.DocRecord.DocID)) + } + sb.WriteString("正文已保存。\n") + sb.WriteString(fmt.Sprintf("耗时: %v\n", taskRes.Duration)) if taskRes.AdvisorBrief != nil && len(taskRes.AdvisorBrief.BaselineAssumptions) > 0 { sb.WriteString("\n📌 预设基准假设(已融入方案,您可随时修正):\n") for _, a := range taskRes.AdvisorBrief.BaselineAssumptions { diff --git a/internal/coordinator/pending_confirmation_reply_test.go b/internal/coordinator/pending_confirmation_reply_test.go index 86e004a3..beff43ec 100644 --- a/internal/coordinator/pending_confirmation_reply_test.go +++ b/internal/coordinator/pending_confirmation_reply_test.go @@ -2,6 +2,7 @@ package coordinator import ( "context" + "fmt" "strings" "testing" "time" @@ -10,6 +11,7 @@ import ( "chat-agent-system/internal/docgov" "chat-agent-system/internal/intent" "chat-agent-system/internal/maturity" + "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/squad" ) @@ -22,6 +24,26 @@ func (p *pendingReplyBizdev) ListStages() []stageflow.StageMeta { return nil } func (p *pendingReplyBizdev) ExecuteTask(_ context.Context, _ stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error) { return p.res, nil } + +type errReplyBizdev struct { + res *stageflow.TaskRunResult + err error +} + +func (p *errReplyBizdev) ListTasks() []stageflow.TaskMeta { return nil } +func (p *errReplyBizdev) ListStages() []stageflow.StageMeta { return nil } +func (p *errReplyBizdev) ExecuteTask(_ context.Context, _ stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error) { + return p.res, p.err +} +func (p *errReplyBizdev) ExecuteStage(_ context.Context, _ stageflow.StageRunRequest) (*stageflow.StageRunResult, error) { + return nil, nil +} +func (p *errReplyBizdev) InspectWorkspace(_ context.Context, _ string) (*squad.WorkspaceState, error) { + return nil, nil +} +func (p *errReplyBizdev) ResumeOrModifyWorkspace(_ context.Context, _ squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) { + return nil, nil +} func (p *pendingReplyBizdev) ExecuteStage(_ context.Context, _ stageflow.StageRunRequest) (*stageflow.StageRunResult, error) { return nil, nil } @@ -133,3 +155,61 @@ func TestFormatTriggerTaskReply_PendingUsesCompletionSummary(t *testing.T) { t.Fatalf("reply=%s", reply) } } + +func TestFormatTriggerTaskReply_QualityFailedOmitsHostPath(t *testing.T) { + failed := &stageflow.TaskRunResult{ + Success: false, + TaskKey: "cdo_quote", + ArtifactTitle: "CDO报价单(含定价核算 Excel)", + WorkDir: "/home/weichen/projects/Agent-System/data/test/workspaces/bizdev/cdo_quote_x", + ErrorReason: "质量门禁未通过(评分 40/100)", + QualityReport: &squad.QualityReport{ + Passed: false, + Score: 40, + Violations: []string{"缺少必填章节: 报价范围"}, + }, + } + reply, status := formatTriggerTaskReply(failed, failed.WorkDir+"/output.md") + if status != stageflow.TaskStatusQualityFailed { + t.Fatalf("status=%q", status) + } + if strings.Contains(reply, "/home/weichen") || strings.Contains(reply, "正文已保存") || strings.Contains(reply, "已完成生成") { + t.Fatalf("quality failure must not claim a saved host file: %s", reply) + } + if !strings.Contains(reply, "质量检查未通过") || !strings.Contains(reply, "报价范围") { + t.Fatalf("reply=%s", reply) + } +} + +func TestSyncTriggerTask_PrerequisiteIsUserVisible(t *testing.T) { + preErr := fmt.Errorf("%w: %s", projectworkflow.ErrPrerequisiteNotMet, "请先完成商务对接流程(CDO 报价批准)后再启动研发设计控制流程") + eng := NewCoordinatorEngine(&errReplyBizdev{ + res: &stageflow.TaskRunResult{Success: false, TaskKey: "rd_design_plan", ErrorReason: preErr.Error()}, + err: preErr, + }) + userCtx := &UserProjectContext{ + User: UserInfo{ID: "33", ProjectRole: "member"}, + Project: ProjectInfo{ID: "8", Name: "全自动冲击波治疗仪1", CurrentStage: maturity.StageV01_Triage, ActiveWorkflowKey: "rdev"}, + } + cand := &intent.CandidateIntent{ + Intent: intent.IntentTriggerTask, + FilledSlots: map[string]string{intent.SlotTargetTask: "rd_design_plan"}, + } + res, err := eng.ProcessIntent(context.Background(), &intent.IntentRecognitionResult{ + Branch: intent.BranchNewIntentPreempt, + TopIntent: cand, + RawInput: "帮我制定设计开发计划任务书", + }, userCtx) + if err != nil { + t.Fatalf("prerequisite must not fail the turn: %v", err) + } + if res.Status != StatusRejected { + t.Fatalf("status=%s", res.Status) + } + if !strings.Contains(res.NaturalReply, "请先完成商务对接流程") { + t.Fatalf("reply=%s", res.NaturalReply) + } + if strings.Contains(res.NaturalReply, "precondition failed") { + t.Fatalf("reply leaked sentinel: %s", res.NaturalReply) + } +} diff --git a/internal/intent/doc_workflow_scope_test.go b/internal/intent/doc_workflow_scope_test.go index 47b92bf3..ee87c96a 100644 --- a/internal/intent/doc_workflow_scope_test.go +++ b/internal/intent/doc_workflow_scope_test.go @@ -179,3 +179,53 @@ func TestWorkflowScope_SnapshotCarriesActiveWorkflowKey(t *testing.T) { t.Fatalf("ActiveWorkflowKey must be settable, got %q", snap.ActiveWorkflowKey) } } + +func TestWriteVerb_DingDesignPlanCrossFlow(t *testing.T) { + rec := intent.NewRecognizer() + ctx := &intent.ContextSnapshot{SnapshotID: "s_ding", ActiveWorkflowKey: "main"} + res, err := rec.Recognize(context.Background(), "帮我定设计开发计划任务书", ctx) + if err != nil { + t.Fatal(err) + } + if res.TopIntent.Intent != intent.IntentTriggerTask { + t.Fatalf("want TRIGGER_TASK, got %s reasoning=%s", res.TopIntent.Intent, res.TopIntent.Reasoning) + } + if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] != "true" { + t.Fatalf("main flow must block rd_design_plan, slots=%v", res.TopIntent.FilledSlots) + } + if !strings.Contains(res.TopIntent.ClarificationQuestion, "研发设计控制流程") { + t.Fatalf("guidance=%q", res.TopIntent.ClarificationQuestion) + } + if res.Branch != intent.BranchAmbiguousFallback && !res.RequiresClarifying { + t.Fatalf("cross-flow write should require clarifying, branch=%s", res.Branch) + } +} + +func TestWriteVerb_ZhidingDesignPlanOnRdev(t *testing.T) { + rec := intent.NewRecognizer() + ctx := &intent.ContextSnapshot{SnapshotID: "s_zhiding", ActiveWorkflowKey: "rdev"} + res, err := rec.Recognize(context.Background(), "帮我制定设计开发计划任务书", ctx) + if err != nil { + t.Fatal(err) + } + if res.TopIntent.Intent != intent.IntentTriggerTask { + t.Fatalf("want TRIGGER_TASK, got %s", res.TopIntent.Intent) + } + if res.TopIntent.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" { + t.Fatalf("owning flow must not block, slots=%v", res.TopIntent.FilledSlots) + } + if got := res.TopIntent.FilledSlots[intent.SlotTargetTask]; got != "rd_design_plan" { + t.Fatalf("task=%q", got) + } +} + +func TestWriteVerb_DecideIsNotWrite(t *testing.T) { + rec := intent.NewRecognizer() + res, err := rec.Recognize(context.Background(), "我决定先放一放,再看看进展", nil) + if err != nil { + t.Fatal(err) + } + if res.TopIntent.Intent == intent.IntentTriggerTask { + t.Fatalf("决定 must not trigger a deliverable, slots=%v", res.TopIntent.FilledSlots) + } +} diff --git a/internal/intent/recognizer_matchers_task.go b/internal/intent/recognizer_matchers_task.go index 04a97274..d9a4e9a8 100644 --- a/internal/intent/recognizer_matchers_task.go +++ b/internal/intent/recognizer_matchers_task.go @@ -164,11 +164,27 @@ func matchQueryProgress(input string) *CandidateIntent { } func hasTaskWriteVerb(input string) bool { - for _, verb := range []string{"生成", "做", "跑", "出", "编写", "编制", "起草"} { + for _, verb := range []string{"生成", "做", "跑", "出", "编写", "编制", "起草", "制定", "拟定"} { if strings.Contains(input, verb) { return true } } + // 「定」「拟」只在祈使前缀后算写动词。「帮我决定」里的「定」不算。 + return hasBareWriteImperative(input, "定") || hasBareWriteImperative(input, "拟") +} + +func hasBareWriteImperative(input, verb string) bool { + for _, marker := range []string{"帮我" + verb, "请" + verb, "给我" + verb, "帮忙" + verb} { + idx := strings.Index(input, marker) + if idx < 0 { + continue + } + rest := input[idx+len(marker):] + if verb == "定" && strings.HasPrefix(rest, "决") { + continue + } + return true + } return false }