fix(intent): 让「帮我定任务书」进入任务并说清失败
写动词补上制定/拟定与祈使「帮我定」,避免落到阶段对照模板。质量未通过不再声称正文已保存到宿主机路径;研发前置未满足时把中文原因直接回复给用户。
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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 队列为 failed(DIRTY_TREE),本任务不代合入。
|
||||
|
||||
## 事实
|
||||
|
||||
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` 以中文前置条件回复结束回合,不再把哨兵错误当成协调失败丢掉回复。
|
||||
- 模糊咨询若已带澄清问题,直接透传,不再覆盖成阶段对照模板。
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user