From 7346fcae64baa7600a926fbbbe155056457aeb1c Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Mon, 7 Sep 2026 21:27:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20CLI=E6=94=AF=E6=8C=81=E6=88=90?= =?UTF-8?q?=E7=86=9F=E5=BA=A6=E9=98=B6=E6=AE=B5=E6=89=A7=E8=A1=8C=E4=B8=8E?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E5=8F=82=E8=B0=8B=E9=94=A6=E5=9B=8A=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- cmd/bizdev-cli/main.go | 134 ++++++++++++++++++++++++++++++++++-- cmd/bizdev-cli/main_test.go | 47 +++++++++++++ 2 files changed, 174 insertions(+), 7 deletions(-) diff --git a/cmd/bizdev-cli/main.go b/cmd/bizdev-cli/main.go index cab3826b..36de9a2d 100644 --- a/cmd/bizdev-cli/main.go +++ b/cmd/bizdev-cli/main.go @@ -14,6 +14,7 @@ import ( ) func main() { + stageFlag := flag.String("stage", "", "执行成熟度阶段 (可选: V0.1_triage, V0.5_feasibility, V1.0_quoting)") taskKey := flag.String("task", "intake", "任务标识 (intake, research, cdo_quote 等)") backend := flag.String("backend", "", "智能体后端 (留空则读取 .env 配置,可选: stub, pi-agent, cursor-cli)") project := flag.String("project", "医疗器械研发演示项目", "项目名称") @@ -22,10 +23,12 @@ func main() { use := flag.String("use", "临床诊疗", "预期用途") market := flag.String("market", "国内", "目标市场") materials := flag.String("materials", "", "输入补充材料内容") + materialsDir := flag.String("materials-dir", "", "输入材料文件夹路径 (批量载入目录下所有文件)") envPath := flag.String("env", ".env", "环境配置文件路径") inspectDir := flag.String("inspect", "", "审查并查看指定工作区状态目录") autoRepair := flag.Bool("auto-repair", true, "启用质量门禁自动闭环修正") listTasks := flag.Bool("list-tasks", false, "列出所有受支持的商务对接任务清单") + listStages := flag.Bool("list-stages", false, "列出所有受支持的商务成熟度阶段清单") addStep := flag.String("add-step", "", "动态向工作区追加任务步骤 (格式: role:title,配合 -inspect)") flag.Parse() @@ -37,6 +40,17 @@ func main() { svc := bizdev.NewService(cfg) + // 0. 列出支持的阶段 + if *listStages { + fmt.Println("=== 商务对接成熟度螺旋收敛阶段清单 ===") + for _, st := range svc.ListStages() { + fmt.Printf("- %-18s: %s\n", st.Stage, st.Title) + fmt.Printf(" 说明: %s\n", st.Description) + fmt.Printf(" 包含任务: %v\n", st.Tasks) + } + return + } + // 1. 列出支持的任务 if *listTasks { fmt.Println("=== 商务对接小队支持的独立任务清单 ===") @@ -55,7 +69,7 @@ func main() { return } - // 3. 执行常规任务 + // 3. 执行阶段或常规任务 activeBackend := cfg.AgentBackend if *backend != "" { activeBackend = *backend @@ -63,13 +77,40 @@ func main() { } var materialsList []bizdev.MaterialInput - if *materials != "" { - materialsList = []bizdev.MaterialInput{ - { - Filename: "user_input.md", - Content: []byte(*materials), - }, + if *materialsDir != "" { + entries, err := os.ReadDir(*materialsDir) + if err != nil { + fmt.Fprintf(os.Stderr, "读取材料目录失败 %s: %v\n", *materialsDir, err) + os.Exit(1) } + for _, entry := range entries { + if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { + continue + } + fPath := filepath.Join(*materialsDir, entry.Name()) + content, err := os.ReadFile(fPath) + if err != nil { + fmt.Fprintf(os.Stderr, "读取材料文件失败 %s: %v\n", fPath, err) + continue + } + materialsList = append(materialsList, bizdev.MaterialInput{ + Filename: entry.Name(), + Content: content, + }) + fmt.Printf("已载入输入材料: %s (%d 字节)\n", entry.Name(), len(content)) + } + } + if *materials != "" { + materialsList = append(materialsList, bizdev.MaterialInput{ + Filename: "user_input.md", + Content: []byte(*materials), + }) + } + + // 如果指定了 -stage,执行阶段编排 + if *stageFlag != "" { + handleStageRun(context.Background(), svc, *stageFlag, *project, *customer, *device, *use, *market, activeBackend, *autoRepair, materialsList) + return } res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{ @@ -129,6 +170,85 @@ func main() { fmt.Printf(" - %s (%d bytes) -> %s\n", a.Filename, len(a.Data), p) } } + + // 打印 B 轨业务参谋锦囊 + if res.AdvisorBrief != nil { + printAdvisorBrief(res.AdvisorBrief) + } +} + +func handleStageRun( + ctx context.Context, + svc bizdev.BizdevService, + stageName string, + project, customer, device, use, market, backend string, + autoRepair bool, + materials []bizdev.MaterialInput, +) { + fmt.Printf("\n>>> 启动商务对接成熟度阶段: [%s] <<<\n", stageName) + stageRes, err := svc.ExecuteStage(ctx, bizdev.StageRunRequest{ + Stage: bizdev.MaturityStage(stageName), + ProjectName: project, + CustomerName: customer, + DeviceType: device, + IntendedUse: use, + TargetMarket: market, + Backend: backend, + AutoRepair: autoRepair, + Materials: materials, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "阶段执行失败: %v\n", err) + os.Exit(1) + } + + fmt.Printf("\n=== 阶段执行成功: %s ===\n", stageRes.StageTitle) + fmt.Printf("总耗时: %v\n", stageRes.Duration) + fmt.Printf("阶段工作区根目录: %s\n", stageRes.WorkDir) + fmt.Printf("包含交付任务数: %d\n", len(stageRes.SubArtifacts)) + for idx, sub := range stageRes.SubArtifacts { + fmt.Printf(" [%d] %-18s: %s -> %s\n", idx+1, sub.TaskKey, sub.ArtifactTitle, filepath.Join(sub.WorkDir, "output.md")) + } + + // 优先呈现 B 轨:贴身业务参谋锦囊 + if stageRes.AdvisorBrief != nil { + printAdvisorBrief(stageRes.AdvisorBrief) + } +} + +func printAdvisorBrief(brief *bizdev.AdvisorBriefing) { + fmt.Println("\n==================================================================") + fmt.Println(" 💡【B轨:贴身业务参谋决策锦囊】 ") + fmt.Println("==================================================================") + fmt.Printf("【成熟度诊断】 等级: %s | 完备度评分: %d / 100\n", brief.MaturityLevel, brief.MaturityScore) + fmt.Printf("【进展研判】 %s\n", brief.MaturityHeadline) + + if len(brief.BaselineAssumptions) > 0 { + fmt.Println("\n【基准预设假设(替代填空,为您先行兜底)】:") + for _, a := range brief.BaselineAssumptions { + fmt.Printf(" • %s\n", a) + } + } + + if len(brief.PreemptiveRisks) > 0 { + fmt.Println("\n【预见性风险雷达(提前预警后续法规/商务卡点)】:") + for _, r := range brief.PreemptiveRisks { + fmt.Printf(" ⚠️ %s\n", r) + } + } + + if len(brief.NextBestActions) > 0 { + fmt.Println("\n【建议下一步行动(提供选项供您掌舵决策)】:") + for idx, act := range brief.NextBestActions { + fmt.Printf(" [%d] %s\n", idx+1, act) + } + } + + if brief.CommunicationKit != "" { + fmt.Println("\n【对外赋能:建议沟通话术(可直接复制使用)】:") + fmt.Println(brief.CommunicationKit) + } + fmt.Println("==================================================================") } func handleInspect(ctx context.Context, svc bizdev.BizdevService, workDir string, addStepFlag string, backend string) { diff --git a/cmd/bizdev-cli/main_test.go b/cmd/bizdev-cli/main_test.go index 0b056943..65e3f2ce 100644 --- a/cmd/bizdev-cli/main_test.go +++ b/cmd/bizdev-cli/main_test.go @@ -68,6 +68,9 @@ func TestBizdevCLIExecuteAndInspect(t *testing.T) { if !strings.Contains(outStr, "质量门禁评分") { t.Fatalf("cli output missing quality gate score: %s", outStr) } + if !strings.Contains(outStr, "【B轨:贴身业务参谋决策锦囊】") { + t.Fatalf("cli output missing B-Track advisor brief: %s", outStr) + } // 从输出提取工作区路径 var workDir string @@ -102,3 +105,47 @@ func TestBizdevCLIExecuteAndInspect(t *testing.T) { t.Fatalf("expected dynamic step success: %s", inspectStr) } } + +func TestBizdevCLIStageExecution(t *testing.T) { + tmpDir := t.TempDir() + envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envPath := filepath.Join(tmpDir, ".env") + _ = os.WriteFile(envPath, []byte(envContent), 0o644) + + // 1. 验证 -list-stages 输出 + listCmd := exec.Command("go", "run", "./cmd/bizdev-cli", "-list-stages", "-env", envPath) + listCmd.Dir = moduleRoot(t) + listOut, err := listCmd.CombinedOutput() + if err != nil { + t.Fatalf("cli -list-stages failed: %v, out: %s", err, string(listOut)) + } + if !strings.Contains(string(listOut), "V0.1_triage") || !strings.Contains(string(listOut), "V1.0_quoting") { + t.Fatalf("missing stages in list: %s", string(listOut)) + } + + // 2. 运行 Stage 1 (V0.1_triage) + stageCmd := exec.Command("go", "run", "./cmd/bizdev-cli", + "-env", envPath, + "-stage", "V0.1_triage", + "-backend", "stub", + "-project", "腹腔镜手术机器人系统", + "-customer", "水木医用机器人中心", + "-device", "三类手术机器人", + ) + stageCmd.Dir = moduleRoot(t) + stageOut, err := stageCmd.CombinedOutput() + if err != nil { + t.Fatalf("cli -stage run failed: %v, out: %s", err, string(stageOut)) + } + + outStr := string(stageOut) + if !strings.Contains(outStr, "阶段执行成功: V0.1 闪电摸底与盲区探测") { + t.Errorf("missing stage success headline: %s", outStr) + } + if !strings.Contains(outStr, "【B轨:贴身业务参谋决策锦囊】") { + t.Errorf("missing advisor brief section: %s", outStr) + } + if !strings.Contains(outStr, "水木医用机器人中心") { + t.Errorf("missing customer name in communication kit: %s", outStr) + } +}