Files
Agent-System/cmd/bizdev-cli/main.go
T

1005 lines
39 KiB
Go

package main
import (
"bufio"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/chat"
"chat-agent-system/internal/config"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/stream"
)
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", "医疗器械研发演示项目", "项目名称")
customer := flag.String("customer", "北京水木东方", "客户名称")
device := flag.String("device", "医疗器械", "设备类型")
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)")
// 对话协同与意图识别相关命令行参数
chatMode := flag.Bool("chat", false, "启动与项目参谋的自然语言交互式对话 (REPL)")
chatQuery := flag.String("query", "", "直接在命令行传入单次提问/指令并输出参谋诊断与调度结果 (配合 -chat)")
workDirFlag := flag.String("work-dir", "", "关联现有工作区目录 (如 sandbox/intake_xxx)")
userName := flag.String("user", "张工", "用户姓名")
userRole := flag.String("role", "engineer", "用户角色")
userIDFlag := flag.Int64("user-id", 1, "操作用户ID (关联偏好记忆)")
projectIDFlag := flag.Int64("project-id", 1, "项目ID (关联协同上下文)")
streamMode := flag.Bool("stream", true, "启用实时流式阶段状态指示 (默认开启)")
verboseMode := flag.Bool("verbose", false, "展开打印底层专员详细推演草稿与思考链 (默认隐藏)")
// 标杆案例语义检索命令行参数
recommendCasesQuery := flag.String("recommend-cases", "", "根据自然语言输入检索历史受控文档中的标杆案例与报价经验")
// 受控文档编制、审核、批准治理与认领操作参数
docStatus := flag.String("doc-status", "", "查询指定受控文档的编制/审核/批准与认领治理状态 (传入 doc_id)")
docClaim := flag.String("doc-claim", "", "以当前用户角色查看并认领指定受控文档 (传入 doc_id)")
docSubmitReview := flag.String("doc-submit-review", "", "以编制人身份将文档送审 (传入 doc_id)")
docReviewApprove := flag.String("doc-review-approve", "", "以审核人身份通过文档审核 (传入 doc_id)")
docReviewReject := flag.String("doc-review-reject", "", "以审核人身份驳回文档 (格式: doc_id:驳回原因)")
docApproverApprove := flag.String("doc-approver-approve", "", "以批准人身份最终批准文档 (传入 doc_id)")
docApproverReject := flag.String("doc-approver-reject", "", "以批准人身份驳回文档 (格式: doc_id:驳回原因)")
decideProposal := flag.String("decide-proposal", "", "编制人拍板变更提案 (格式: proposal_id:agree/reject:理由)")
forceDirectEdit := flag.Bool("force-direct-edit", false, "在编制人已拍板授权变更后,强制执行正文覆写更新")
authorIDFlag := flag.Int64("author-id", 0, "文档编制人用户ID (默认等同于 -user-id)")
authorNameFlag := flag.String("author", "", "文档编制人姓名 (默认等同于 -user)")
reviewerIDFlag := flag.Int64("reviewer-id", 0, "文档审核人用户ID")
reviewerNameFlag := flag.String("reviewer", "", "文档审核人姓名")
approverIDFlag := flag.Int64("approver-id", 0, "文档批准人用户ID")
approverNameFlag := flag.String("approver", "", "文档批准人姓名")
flag.Parse()
cfg, err := config.Load(*envPath)
if err != nil {
fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err)
os.Exit(1)
}
if *backend != "" {
cfg.AgentBackend = *backend
}
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("=== 商务对接小队支持的独立任务清单 ===")
for _, t := range svc.ListTasks() {
fmt.Printf("- %-18s: %s (交付: %s)\n", t.Key, t.Title, t.ArtifactTitle)
if t.Description != "" {
fmt.Printf(" 说明: %s\n", t.Description)
}
}
return
}
// 2. 审查已有工作区
if *inspectDir != "" {
handleInspect(context.Background(), svc, *inspectDir, *addStep, *backend)
return
}
// 2.1 标杆案例语义推荐独立检索
if *recommendCasesQuery != "" {
handleRecommendCases(context.Background(), svc, *recommendCasesQuery)
return
}
// 3. 交互式对话参谋模式
if *chatMode || *chatQuery != "" {
handleChatMode(
context.Background(),
svc,
*chatQuery,
*workDirFlag,
*project,
*customer,
*device,
*use,
*market,
*userName,
*userRole,
*streamMode,
*verboseMode,
)
return
}
// 4. 执行阶段或常规任务
activeBackend := cfg.AgentBackend
if *backend != "" {
activeBackend = *backend
cfg.AgentBackend = *backend
}
var materialsList []bizdev.MaterialInput
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),
})
}
// 受控文档治理状态查询与操作处理
if *docStatus != "" {
handleDocStatus(context.Background(), svc, *docStatus)
return
}
if *docClaim != "" {
handleDocClaim(context.Background(), svc, *docClaim, *userIDFlag, *userRole, *userName)
return
}
if *decideProposal != "" {
handleDecideProposal(context.Background(), svc, *decideProposal, *userIDFlag)
return
}
if *docSubmitReview != "" {
handleDocSubmitReview(context.Background(), svc, *docSubmitReview, *userIDFlag, *userRole, *userName)
return
}
if *docReviewApprove != "" {
handleDocReviewApprove(context.Background(), svc, *docReviewApprove, *userIDFlag, *userRole, *userName)
return
}
if *docReviewReject != "" {
handleDocReviewReject(context.Background(), svc, *docReviewReject, *userIDFlag, *userRole, *userName)
return
}
if *docApproverApprove != "" {
handleDocApproverApprove(context.Background(), svc, *docApproverApprove, *userIDFlag, *userRole, *userName)
return
}
if *docApproverReject != "" {
handleDocApproverReject(context.Background(), svc, *docApproverReject, *userIDFlag, *userRole, *userName)
return
}
authorID := *authorIDFlag
if authorID <= 0 {
authorID = *userIDFlag
}
authorName := *authorNameFlag
if authorName == "" {
authorName = *userName
}
// 如果指定了 -stage,执行阶段编排
if *stageFlag != "" {
handleStageRun(
context.Background(),
svc,
*stageFlag,
*project, *customer, *device, *use, *market, activeBackend,
*autoRepair, materialsList,
*userIDFlag, *projectIDFlag,
authorID, authorName,
*reviewerIDFlag, *reviewerNameFlag,
*approverIDFlag, *approverNameFlag,
*forceDirectEdit,
*streamMode, *verboseMode,
)
return
}
res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
TaskKey: *taskKey,
ProjectName: *project,
CustomerName: *customer,
DeviceType: *device,
IntendedUse: *use,
TargetMarket: *market,
Materials: materialsList,
Backend: activeBackend,
AutoRepair: *autoRepair,
UserID: *userIDFlag,
ProjectID: *projectIDFlag,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: *reviewerIDFlag,
ReviewerName: *reviewerNameFlag,
ApproverID: *approverIDFlag,
ApproverName: *approverNameFlag,
ForceDirectEdit: *forceDirectEdit,
})
if err != nil {
fmt.Fprintf(os.Stderr, "任务执行失败: %v\n", err)
os.Exit(1)
}
if res.RequiresAuthorApproval && res.PendingProposal != nil {
fmt.Printf("\n================================================================================\n")
fmt.Printf("⚠️ 【文档治理拦截】交付物【%s】已被编制负责人认领!\n", res.ArtifactTitle)
fmt.Printf("系统已拦截智能体小队直接静默覆写,并生成变更提案等待编制人拍板确认:\n")
fmt.Printf(" • 提案编号: %s\n", res.PendingProposal.ProposalID)
fmt.Printf(" • 拟修改点: %s\n", res.PendingProposal.ProposedChanges)
fmt.Printf(" • 影响分析: %s\n", res.PendingProposal.ImpactAnalysis)
fmt.Printf(" • 风险预警: %s\n", res.PendingProposal.RiskAssessment)
fmt.Printf("\n提示:请编制负责人确认后使用如下命令拍板授权:\n")
fmt.Printf(" bizdev-cli -decide-proposal \"%s:agree:经评估同意修改\" -force-direct-edit\n", res.PendingProposal.ProposalID)
fmt.Printf("================================================================================\n")
return
}
if !res.Success {
fmt.Fprintf(os.Stderr, "警告: 后端执行未完全成功 [%s]: %s\n", res.TaskKey, res.ErrorReason)
}
fmt.Printf("=== 任务执行成功 [%s] ===\n", res.TaskKey)
fmt.Printf("耗时: %v\n", res.Duration)
fmt.Printf("工作区沙箱: %s\n", res.WorkDir)
fmt.Printf("交付文档产物: %s\n", filepath.Join(res.WorkDir, "output.md"))
if _, err := os.Stat(filepath.Join(res.WorkDir, "execution.log")); err == nil {
fmt.Printf("执行审计日志: %s\n", filepath.Join(res.WorkDir, "execution.log"))
}
fmt.Printf("使用的后端: %s\n", res.BackendUsed)
// 打印质量门禁与指纹报告
if res.QualityReport != nil {
fmt.Printf("质量门禁评分: %.1f / 100 (达标: %v)\n", res.QualityReport.Score, res.QualityReport.Passed)
if len(res.QualityReport.Violations) > 0 {
fmt.Printf("门禁检测项:\n")
for _, v := range res.QualityReport.Violations {
fmt.Printf(" - %s\n", v)
}
}
}
if res.Manifest != nil {
fmt.Printf("审计指纹清单: %s (追踪文件数: %d)\n", filepath.Join(res.WorkDir, "manifest.json"), len(res.Manifest.Files))
}
if len(res.ExtractedData) > 0 {
fmt.Printf("黑板抽取事实字段:\n")
for k, v := range res.ExtractedData {
fmt.Printf(" - %s: %s\n", k, v)
}
}
if len(res.Attachments) > 0 {
fmt.Printf("附加产物:\n")
for _, a := range res.Attachments {
p := filepath.Join(res.WorkDir, a.Filename)
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,
userID, projectID int64,
authorID int64, authorName string,
reviewerID int64, reviewerName string,
approverID int64, approverName string,
forceDirectEdit bool,
streamMode, verboseMode bool,
) {
fmt.Printf("\n>>> 启动商务对接成熟度阶段: [%s] <<<\n", stageName)
if streamMode {
listener := createCLIStreamListener(verboseMode)
ctx, _ = stream.WithListener(ctx, verboseMode, listener)
}
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,
UserID: userID,
ProjectID: projectID,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: reviewerID,
ReviewerName: reviewerName,
ApproverID: approverID,
ApproverName: approverName,
ForceDirectEdit: forceDirectEdit,
})
if err != nil {
fmt.Fprintf(os.Stderr, "阶段执行失败: %v\n", err)
os.Exit(1)
}
for _, sub := range stageRes.SubArtifacts {
if sub != nil && sub.RequiresAuthorApproval && sub.PendingProposal != nil {
fmt.Printf("\n================================================================================\n")
fmt.Printf("⚠️ 【文档治理拦截】阶段任务【%s】对应交付物【%s】已被编制负责人认领!\n", sub.TaskKey, sub.ArtifactTitle)
fmt.Printf("系统已拦截智能体小队直接静默覆写,并生成变更提案等待编制人拍板确认:\n")
fmt.Printf(" • 提案编号: %s\n", sub.PendingProposal.ProposalID)
fmt.Printf(" • 拟修改点: %s\n", sub.PendingProposal.ProposedChanges)
fmt.Printf(" • 影响分析: %s\n", sub.PendingProposal.ImpactAnalysis)
fmt.Printf(" • 风险预警: %s\n", sub.PendingProposal.RiskAssessment)
fmt.Printf("\n提示:请编制负责人确认后使用如下命令拍板授权:\n")
fmt.Printf(" bizdev-cli -decide-proposal \"%s:agree:经评估同意修改\" -stage %s -force-direct-edit\n", sub.PendingProposal.ProposalID, stageName)
fmt.Printf("================================================================================\n")
return
}
}
if !stageRes.Success {
fmt.Fprintf(os.Stderr, "\n阶段执行未完全成功: %s\n", stageRes.ErrorReason)
return
}
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 len(brief.RecommendedCases) > 0 {
fmt.Println("\n【往期标杆案例与历史报价经验自推荐(智能召回)】:")
for idx, c := range brief.RecommendedCases {
fmt.Printf(" ⭐ 标杆案例 [%d] 《%s》 (相似度: %.0f%%)\n", idx+1, c.ArtifactTitle, c.SimilarityScore*100)
fmt.Printf(" • 命中文档/章节: %s (%s)\n", c.DocID, c.SectionTitle)
if len(c.KeyRegulations) > 0 {
fmt.Printf(" • 参考合规依据: %s\n", strings.Join(c.KeyRegulations, ", "))
}
fmt.Printf(" • 核心结论摘要: %s\n", c.SummarySnippet)
}
}
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) {
state, err := svc.InspectWorkspace(ctx, workDir)
if err != nil {
fmt.Fprintf(os.Stderr, "查看工作区失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("=== 工作区状态审查 [%s] ===\n", workDir)
fmt.Printf("项目: %s | 客户: %s | 阶段: %s | 状态: %s\n",
state.Metadata.ProjectName, state.Metadata.CustomerName, state.Metadata.TaskKey, state.Metadata.Status)
fmt.Printf("\n小队角色编制 (%d 位):\n", len(state.Squad.Agents))
for _, a := range state.Squad.Agents {
fmt.Printf(" - %-12s: %s (模型: %s, 工具: %v)\n", a.Role, a.Name, a.Model, a.Tools)
}
fmt.Printf("\n工作流步骤清单 (%d 步):\n", len(state.Workflow.Steps))
for _, s := range state.Workflow.Steps {
fmt.Printf(" - [%s] %-10s (%-8s) : %s -> %s\n", s.Status, s.ID, s.Role, s.Title, s.SubWorkspace)
}
if len(state.Blackboard.Fields) > 0 {
fmt.Printf("\n黑板抽取字段:\n")
for k, v := range state.Blackboard.Fields {
fmt.Printf(" - %s: %s\n", k, v)
}
}
// 若用户指定了动态追加步骤
if addStepFlag != "" {
parts := strings.SplitN(addStepFlag, ":", 2)
if len(parts) == 2 {
role := strings.TrimSpace(parts[0])
title := strings.TrimSpace(parts[1])
fmt.Printf("\n动态追加步骤: [%s] %s\n", role, title)
modifyReq := squad.ModifyWorkflowRequest{
WorkDir: workDir,
Backend: backend,
AddSteps: []squad.StepSpec{
{
ID: fmt.Sprintf("step_dyn_%d", len(state.Workflow.Steps)+1),
Role: role,
Title: title,
SubWorkspace: fmt.Sprintf("tasks/dyn_%s", role),
Mode: "execute",
},
},
}
runRes, err := svc.ResumeOrModifyWorkspace(ctx, modifyReq)
if err != nil {
fmt.Fprintf(os.Stderr, "执行动态追加步骤失败: %v\n", err)
} else {
fmt.Printf("动态步骤执行成功! 耗时: %v\n", runRes.Duration)
}
}
}
}
func createCLIStreamListener(verbose bool) stream.Listener {
return func(ev stream.StreamEvent) {
switch ev.Type {
case stream.EventPhase:
if ev.Phase != nil {
icon := "⏳"
switch ev.Phase.PhaseID {
case "squad_assembly":
icon = "👥"
case "knowledge_retrieval", "knowledge_retrieved":
icon = "📚"
case "step_executing":
icon = "⚙️ "
case "step_completed":
icon = "✅"
case "quality_gate":
icon = "🛡️ "
case "advisor_briefing":
icon = "💡"
case "stage_transition", "stage_started":
icon = "🚀"
case "stage_completed":
icon = "🎉"
}
fmt.Printf("%s [%s] %s\n", icon, ev.Phase.PhaseID, ev.Phase.Message)
}
case stream.EventStatus:
if ev.Status != nil && ev.Status.Message != "" {
fmt.Printf("🎯 [意图状态] %s\n", ev.Status.Message)
}
case stream.EventSummary:
if ev.Summary != nil {
fmt.Println("\n------------------------------------------------------------------")
fmt.Println("📌【核心进展与诊断摘要】")
fmt.Printf("标 题: %s\n", ev.Summary.Title)
fmt.Printf("诊 断: %s\n", ev.Summary.Content)
if ev.Summary.MaturityScore > 0 {
fmt.Printf("成熟度: %s (%d / 100)\n", ev.Summary.MaturityLevel, ev.Summary.MaturityScore)
}
fmt.Println("------------------------------------------------------------------")
}
case stream.EventDeliverable:
if ev.Deliverable != nil {
fmt.Printf("📄【A轨合规文档生成】%s (评分: %.1f) -> %s\n", ev.Deliverable.Title, ev.Deliverable.QualityScore, ev.Deliverable.FilePath)
if !verbose {
fmt.Println("💡 [提示: 底层专员思考过程与推演草稿已在沙箱归档,可使用 --verbose 参数在控制台展开查看]")
}
}
case stream.EventDetail:
if verbose && ev.Detail != nil {
fmt.Printf("\n--- 🔍 [底层推演详情: %s - %s] ---\n%s\n------------------------------------------------------------------\n", ev.Detail.Role, ev.Detail.Title, ev.Detail.Content)
}
case stream.EventGovernance:
if ev.Governance != nil {
fmt.Println("\n==================================================================")
fmt.Printf("⚠️ 【文档治理拦截告警】%s\n", ev.Governance.NotificationTitle)
fmt.Printf("说 明: %s\n", ev.Governance.NotificationMsg)
if ev.Governance.ProposalID != "" {
fmt.Printf("提案编号: %s\n", ev.Governance.ProposalID)
fmt.Printf("拟修改点: %s\n", ev.Governance.ProposedChanges)
fmt.Printf("影响分析: %s\n", ev.Governance.ImpactSummary)
fmt.Printf("风险预警: %s\n", ev.Governance.RiskSummary)
}
fmt.Println("==================================================================")
}
case stream.EventError:
fmt.Printf("⚠️ [执行告警] %s\n", ev.Error)
}
}
}
func handleChatMode(
ctx context.Context,
svc bizdev.BizdevService,
chatQuery string,
workDir string,
project string,
customer string,
device string,
use string,
market string,
userName string,
userRole string,
streamMode bool,
verboseMode bool,
) {
projRole := "composer"
if userRole == "reviewer" || userRole == "lead_engineer" {
projRole = "reviewer"
} else if userRole == "approver" || userRole == "project_manager" {
projRole = "approver"
}
u := coordinator.UserInfo{
ID: "u_local",
Name: userName,
Role: userRole,
ProjectRole: projRole,
}
assembler := coordinator.NewContextAssembler()
var userCtx *coordinator.UserProjectContext
var err error
if workDir != "" {
userCtx, err = assembler.AssembleFromWorkspace(workDir, u)
if err != nil {
fmt.Fprintf(os.Stderr, "从工作区装载上下文失败 %s: %v,将初始化新上下文\n", workDir, err)
}
}
if userCtx == nil {
userCtx = &coordinator.UserProjectContext{
SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()),
AssembledAt: time.Now(),
User: u,
Project: coordinator.ProjectInfo{
Name: project,
CustomerName: customer,
DeviceType: device,
IntendedUse: use,
TargetMarket: market,
CurrentWorkflow: "intake",
CurrentStage: bizdev.StageV05_Feasibility,
},
BlackboardFacts: make(map[string]string),
AvailableActions: coordinator.ComputeAvailableActions(bizdev.StageV05_Feasibility, u.ProjectRole),
}
if u.Role == "reviewer" {
userCtx.InteractiveCard = &intent.InteractiveCardPayload{
CurrentStatus: "active",
TargetDocID: "doc-002",
TTLTurns: 3,
}
}
if market != "" {
userCtx.BlackboardFacts["target_market"] = market
}
if device != "" {
userCtx.BlackboardFacts["device_type"] = device
}
}
rec := intent.NewRecognizer()
engine := coordinator.NewCoordinatorEngine(svc)
session := chat.NewSession("cli_chat_sess", userCtx, rec, engine)
session.CtxMgr = svc.ContextManager()
// 单次提问模式 (支持脚本与自动化测试)
if chatQuery != "" {
var resp *chat.ChatResponse
if streamMode {
resp, err = session.SendMessageStream(ctx, chatQuery, createCLIStreamListener(verboseMode), verboseMode)
} else {
resp, err = session.SendMessage(ctx, chatQuery)
}
if err != nil {
fmt.Fprintf(os.Stderr, "对话处理失败: %v\n", err)
os.Exit(1)
}
top := resp.IntentResult.TopIntent
fmt.Printf("\n[意图识别: %s (置信度: %.0f%%) | 消解分支: %s] - %s\n", top.Intent, top.Confidence*100, resp.Branch, top.Reasoning)
fmt.Printf("\n%s\n", resp.ReplyText)
if resp.InteractiveCard != nil {
fmt.Println("\n------------------------------------------------------------------")
fmt.Printf("📋 [挂载对话交互卡片] %s (%s)\n", resp.InteractiveCard.Title, resp.InteractiveCard.CardType)
if resp.InteractiveCard.TargetDocTitle != "" {
fmt.Printf(" • 目标受控文档: %s (ID: %s)\n", resp.InteractiveCard.TargetDocTitle, resp.InteractiveCard.TargetDocID)
}
if resp.InteractiveCard.SectionTitle != "" {
fmt.Printf(" • 智能定位章节: %s (Block ID: %s)\n", resp.InteractiveCard.SectionTitle, resp.InteractiveCard.TargetSectionID)
}
if resp.InteractiveCard.ProposedContent != "" {
fmt.Printf(" • 拟注入审查批注: %s\n", resp.InteractiveCard.ProposedContent)
}
if resp.InteractiveCard.ImpactRisk != "" {
fmt.Printf(" • 法规与流转风险: %s\n", resp.InteractiveCard.ImpactRisk)
}
fmt.Printf(" • 卡片时效: 剩余 %d 轮 | 状态: %s\n", resp.InteractiveCard.TTLTurns, resp.InteractiveCard.CurrentStatus)
if len(resp.InteractiveCard.Actions) > 0 {
fmt.Println(" • 可选操作动作:")
for _, act := range resp.InteractiveCard.Actions {
defMark := ""
if act.IsAffirmativeDefault {
defMark = " (默认肯定/可回复确认)"
}
fmt.Printf(" [%s] %s%s\n", act.ActionID, act.Label, defMark)
}
}
fmt.Println("------------------------------------------------------------------")
}
if resp.AdvisorBrief != nil {
printAdvisorBrief(resp.AdvisorBrief)
}
return
}
// 交互式 REPL 模式
fmt.Println("==================================================================")
fmt.Println(" 🤖 医疗器械商务与研发智能参谋(L1/L2 协作对话终端) ")
fmt.Println("==================================================================")
fmt.Printf("项目名称: %s | 客户单位: %s\n", userCtx.Project.Name, userCtx.Project.CustomerName)
fmt.Printf("当前阶段: %s | 用户身份: %s (%s)\n", userCtx.Project.CurrentStage, userCtx.User.Name, userCtx.User.Role)
fmt.Printf("可用动作: %v\n", userCtx.AvailableActions)
fmt.Printf("流式反馈: %v | 详细推演模式: %v\n", streamMode, verboseMode)
fmt.Println("------------------------------------------------------------------")
fmt.Println("💡 参谋互动小贴士(三分支柔性消解):")
fmt.Println(" • 极速兑现(分支 1):当参谋给出下一步建议时,回复“好”、“行”或“1”直接推进")
fmt.Println(" • 复合补丁(分支 1):可以肯定并同时附带微调,如:“好,但目标市场改成欧洲CE”")
fmt.Println(" • 暂缓撤销(分支 1):如果不想推进,直接说:“先别推,等等”")
fmt.Println(" • 突发抢占(分支 2):随时发起新问题,如:“查一下探头功耗要怎么设计”")
fmt.Println(" • 模糊分忧(分支 3):拿不定主意时,可以说:“这个先放放,你觉得呢”")
fmt.Println(" • 输入 'exit' 或 'quit' 退出")
fmt.Println("==================================================================")
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("\n[您] > ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("已退出参谋对话。")
break
}
var resp *chat.ChatResponse
if streamMode {
resp, err = session.SendMessageStream(ctx, input, createCLIStreamListener(verboseMode), verboseMode)
} else {
resp, err = session.SendMessage(ctx, input)
}
if err != nil {
fmt.Fprintf(os.Stderr, "处理异常: %v\n", err)
continue
}
top := resp.IntentResult.TopIntent
fmt.Printf("\n[智能参谋] (消解分支: %s | 核心意图: %s | 置信度: %.0f%%)\n", resp.Branch, top.Intent, top.Confidence*100)
fmt.Println(resp.ReplyText)
if resp.InteractiveCard != nil {
fmt.Println("\n------------------------------------------------------------------")
fmt.Printf("📋 [挂载对话交互卡片] %s (%s)\n", resp.InteractiveCard.Title, resp.InteractiveCard.CardType)
if resp.InteractiveCard.TargetDocTitle != "" {
fmt.Printf(" • 目标受控文档: %s (ID: %s)\n", resp.InteractiveCard.TargetDocTitle, resp.InteractiveCard.TargetDocID)
}
if resp.InteractiveCard.SectionTitle != "" {
fmt.Printf(" • 智能定位章节: %s (Block ID: %s)\n", resp.InteractiveCard.SectionTitle, resp.InteractiveCard.TargetSectionID)
}
if resp.InteractiveCard.ProposedContent != "" {
fmt.Printf(" • 拟注入审查批注: %s\n", resp.InteractiveCard.ProposedContent)
}
if resp.InteractiveCard.ImpactRisk != "" {
fmt.Printf(" • 法规与流转风险: %s\n", resp.InteractiveCard.ImpactRisk)
}
fmt.Printf(" • 卡片时效: 剩余 %d 轮 | 状态: %s\n", resp.InteractiveCard.TTLTurns, resp.InteractiveCard.CurrentStatus)
if len(resp.InteractiveCard.Actions) > 0 {
fmt.Println(" • 可选操作动作:")
for _, act := range resp.InteractiveCard.Actions {
defMark := ""
if act.IsAffirmativeDefault {
defMark = " (默认肯定/可回复确认)"
}
fmt.Printf(" [%s] %s%s\n", act.ActionID, act.Label, defMark)
}
}
fmt.Println("------------------------------------------------------------------")
}
if resp.AdvisorBrief != nil {
printAdvisorBrief(resp.AdvisorBrief)
}
}
}
// -----------------------------------------------------------------------------
// 标杆案例独立检索 CLI 处理函数
// -----------------------------------------------------------------------------
func handleRecommendCases(ctx context.Context, svc bizdev.BizdevService, query string) {
ctxMgr := svc.ContextManager()
if ctxMgr == nil {
fmt.Fprintf(os.Stderr, "错误: 上下文管理器未初始化\n")
os.Exit(1)
}
cases, err := ctxMgr.SearchSimilarCases(ctx, bizdev.CaseSearchOptions{
Query: query,
Limit: 5,
MinScore: 0.30,
})
if err != nil {
fmt.Fprintf(os.Stderr, "检索标杆案例失败: %v\n", err)
os.Exit(1)
}
fmt.Println("==================================================================")
fmt.Printf(" 🔍 往期标杆案例与历史报价经验语义推荐 (检索词: %s) \n", query)
fmt.Println("==================================================================")
if len(cases) == 0 {
fmt.Println("未检索到相似度高于 30% 的往期受控交付物切片。")
fmt.Println("==================================================================")
return
}
for idx, c := range cases {
fmt.Printf("⭐ 标杆案例 [%d] 《%s》 (相似度: %.1f%%)\n", idx+1, c.ArtifactTitle, c.SimilarityScore*100)
fmt.Printf(" • 命中文档: %s (任务: %s)\n", c.DocID, c.TaskKey)
fmt.Printf(" • 章节定位: %s\n", c.SectionTitle)
if len(c.KeyRegulations) > 0 {
fmt.Printf(" • 涉及法规标准: %s\n", strings.Join(c.KeyRegulations, ", "))
}
fmt.Printf(" • 核心结论与提要: %s\n", c.SummarySnippet)
if c.ApprovedAt != "" {
fmt.Printf(" • 终审批准时间: %s\n", c.ApprovedAt)
}
fmt.Println("------------------------------------------------------------------")
}
fmt.Println("==================================================================")
}
// -----------------------------------------------------------------------------
// 受控文档治理 CLI 处理函数集
// -----------------------------------------------------------------------------
func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string) {
doc, err := svc.DocGov().GetDocument(ctx, docID)
if err != nil {
fmt.Fprintf(os.Stderr, "查询受控文档失败 [%s]: %v\n", docID, err)
os.Exit(1)
}
fmt.Println("==================================================================")
fmt.Printf(" 受控文档治理状态看板 [%s] \n", doc.DocID)
fmt.Println("==================================================================")
fmt.Printf("标题: %s (任务: %s | 阶段: %s | 版本: V%d)\n", doc.Title, doc.TaskKey, doc.Stage, doc.Version)
fmt.Printf("当前生命周期状态: %s (锁定状态: %v | 骨架模板: %v)\n", doc.CurrentState, doc.IsLocked, doc.IsTemplateOnly)
fmt.Println("------------------------------------------------------------------")
fmt.Println("法定责任人及认领状态(每角色各有且仅有一人):")
fmt.Printf(" • 编制负责人 (Author) : [ID: %d] %-10s | 认领: %v (最后查看: %v)\n",
doc.AuthorID, doc.AuthorName, doc.AuthorClaimed, formatTimePtr(doc.AuthorLastViewedAt))
fmt.Printf(" • 审核负责人 (Reviewer): [ID: %d] %-10s | 认领: %v (审核时间: %v)\n",
doc.ReviewerID, doc.ReviewerName, doc.ReviewerClaimed, formatTimePtr(doc.ReviewerClaimedAt))
fmt.Printf(" • 批准负责人 (Approver): [ID: %d] %-10s | 认领: %v (批准时间: %v)\n",
doc.ApproverID, doc.ApproverName, doc.ApproverClaimed, formatTimePtr(doc.ApprovedAt))
if doc.LastRejectedReason != "" {
fmt.Println("------------------------------------------------------------------")
fmt.Printf("最近驳回记录: 驳回人=%s(%s) | 原因=%s | 时间=%v\n",
doc.LastRejectedByName, doc.LastRejectedByRole, doc.LastRejectedReason, formatTimePtr(doc.LastRejectedAt))
}
// 打印批注
comments, _ := svc.DocGov().ListComments(ctx, docID)
if len(comments) > 0 {
fmt.Printf("\n批注与修改意见 (%d 条):\n", len(comments))
for idx, c := range comments {
fmt.Printf(" [%d] [%s] %s (%s): %s (%s)\n", idx+1, c.CommentType, c.UserName, c.UserRole, c.Content, c.TargetSection)
}
}
// 打印审计记录
audits, _ := svc.DocGov().ListAuditLogs(ctx, docID)
if len(audits) > 0 {
fmt.Printf("\n全生命周期审计轨迹 (%d 条):\n", len(audits))
for idx, a := range audits {
fmt.Printf(" [%d] %s | 动作: %-15s | 操作人: %s(%s) | 状态: %s -> %s\n",
idx+1, a.CreatedAt.Format("15:04:05"), a.Action, a.ActorName, a.ActorRole, a.FromState, a.ToState)
}
}
fmt.Println("==================================================================")
}
func handleDocClaim(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) {
role := docgov.UserRole(userRoleStr)
doc, err := svc.DocGov().RecordDocumentViewed(ctx, docID, userID, role, userName)
if err != nil {
fmt.Fprintf(os.Stderr, "认领/查看文档失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ 用户 %s (%s) 成功查阅受控文档 [%s]\n", userName, role, docID)
if role == docgov.RoleAuthor {
if doc.AuthorClaimed {
fmt.Println("✓ 该文档非模板正式正文已被编制负责人成功认领!后续阶段修改将生成变更提案待您确认。")
} else {
fmt.Println("ℹ️ 当前文档仍处于模板状态,查看不视为认领。")
}
}
}
func handleDecideProposal(ctx context.Context, svc bizdev.BizdevService, param string, authorID int64) {
// 格式: proposal_id:agree/reject:理由
parts := strings.SplitN(param, ":", 3)
if len(parts) < 2 {
fmt.Fprintf(os.Stderr, "参数格式错误,应为: proposal_id:agree/reject:理由\n")
os.Exit(1)
}
proposalID := strings.TrimSpace(parts[0])
decision := strings.ToLower(strings.TrimSpace(parts[1]))
reason := ""
if len(parts) == 3 {
reason = strings.TrimSpace(parts[2])
}
approve := decision == "agree" || decision == "approve" || decision == "yes" || decision == "true"
prop, err := svc.DocGov().DecideModificationProposal(ctx, proposalID, authorID, approve, reason)
if err != nil {
fmt.Fprintf(os.Stderr, "拍板决策变更提案失败: %v\n", err)
os.Exit(1)
}
if approve {
fmt.Printf("✓ 编制负责人已【同意授权】变更提案 [%s]!状态: %s\n", prop.ProposalID, prop.Status)
fmt.Println("提示:可使用 -force-direct-edit 参数让智能体小队执行正式正文修改。")
} else {
fmt.Printf("✓ 编制负责人已【拒绝】变更提案 [%s]!当前文档保持原版不作修改。\n", prop.ProposalID)
}
}
func handleDocSubmitReview(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().SubmitForReview(ctx, docID, userID, docgov.RoleAuthor, userName)
if err != nil {
fmt.Fprintf(os.Stderr, "提交审核失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ 文档 [%s] 编制完成并成功送审!当前状态: %s,文档已进入受控锁定。\n", doc.DocID, doc.CurrentState)
}
func handleDocReviewApprove(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().ReviewApprove(ctx, docID, userID, docgov.RoleReviewer, userName, "审核无误,同意提交批准")
if err != nil {
fmt.Fprintf(os.Stderr, "审核通过失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ 审核人 %s 审核通过!文档 [%s] 流转至批准阶段: %s\n", userName, doc.DocID, doc.CurrentState)
}
func handleDocReviewReject(ctx context.Context, svc bizdev.BizdevService, param string, userID int64, userRoleStr string, userName string) {
parts := strings.SplitN(param, ":", 2)
if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n")
os.Exit(1)
}
docID := strings.TrimSpace(parts[0])
reason := strings.TrimSpace(parts[1])
doc, err := svc.DocGov().ReviewReject(ctx, docID, userID, docgov.RoleReviewer, userName, reason)
if err != nil {
fmt.Fprintf(os.Stderr, "审核驳回失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ 审核人 %s 驳回文档 [%s]!退回编制阶段: %s,文档已解锁供修订。\n驳回原因: %s\n",
userName, doc.DocID, doc.CurrentState, reason)
}
func handleDocApproverApprove(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().ApproverApprove(ctx, docID, userID, docgov.RoleApprover, userName, "评估完备,批准受控发布")
if err != nil {
fmt.Fprintf(os.Stderr, "批准失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("🎉 批准负责人 %s 最终批准文档 [%s]!当前状态: %s,受控终态归档锁定。\n", userName, doc.DocID, doc.CurrentState)
}
func handleDocApproverReject(ctx context.Context, svc bizdev.BizdevService, param string, userID int64, userRoleStr string, userName string) {
parts := strings.SplitN(param, ":", 2)
if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n")
os.Exit(1)
}
docID := strings.TrimSpace(parts[0])
reason := strings.TrimSpace(parts[1])
doc, err := svc.DocGov().ApproverReject(ctx, docID, userID, docgov.RoleApprover, userName, reason, docgov.StateDrafting)
if err != nil {
fmt.Fprintf(os.Stderr, "批准驳回失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ 批准负责人 %s 驳回文档 [%s]!退回编制阶段: %s,文档已解锁供修订。\n驳回原因: %s\n",
userName, doc.DocID, doc.CurrentState, reason)
}
func formatTimePtr(t *time.Time) string {
if t == nil {
return "未发生"
}
return t.Format("2006-01-02 15:04:05")
}