package bizdev import ( "context" "errors" "fmt" "log" "os" "path/filepath" "strings" "time" "chat-agent-system/internal/conflictresolve" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/docduty" "chat-agent-system/internal/docgov" "chat-agent-system/internal/docrevision" "chat-agent-system/internal/intent" "chat-agent-system/internal/kb" "chat-agent-system/internal/notification" "chat-agent-system/internal/projectcontext" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/squad" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workspace" ) func (s *Service) materializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) { if s != nil && s.materializer != nil { return s.materializer.MaterializeAgentOutput(ctx, req) } if s == nil || s.docRev == nil { return nil, fmt.Errorf("bizdev: document revision unavailable") } return s.docRev.MaterializeAgentOutput(ctx, req) } var SupportedTasks = squad.SupportedTasks var SupportedStages = []StageMeta{ { Stage: StageV01_Triage, Title: "V0.1 闪电摸底与盲区探测", Description: "快速梳理客户诉求与约束,建立基准技术假设,生成首期澄清微信话术与初始需求表", Tasks: []string{"bd_intake"}, }, { Stage: StageV05_Feasibility, Title: "V0.5 方案成型与法规对齐", Description: "法规与注册路径初筛、竞品调研、系统初步架构设计与关键选型建议", Tasks: []string{"bd_research", "bd_feasibility_dev", "bd_feasibility_reg", "bd_user_requirements", "bd_prelim_design"}, }, { Stage: StageV10_Quoting, Title: "V1.0 工程拆解与商业锁定", Description: "全维度 WBS 工时拆解、Excel 定价核算、正式 CDO 商务报价单与谈判防御条款建议", Tasks: []string{"bd_cdo_quote"}, }, } func taskIs(key string, wants ...string) bool { for _, want := range wants { if workflowdef.SameTaskKey(key, want) { return true } } return false } func resolveDocStage(req TaskRunRequest) string { if sk := strings.TrimSpace(req.StageKey); sk != "" { return sk } switch workflowdef.ImplementationTaskKey(strings.TrimSpace(req.TaskKey)) { case "intake": return string(StageV01_Triage) case "research", "feasibility_dev", "feasibility_reg", "user_requirements", "prelim_design": return string(StageV05_Feasibility) case "cdo_quote": return string(StageV10_Quoting) default: return string(StageV05_Feasibility) } } const rdevPrerequisiteMessage = "请先完成商务对接流程(CDO 报价批准)后再启动研发设计控制流程" func (s *Service) ensureRDevMainCompleted(ctx context.Context, projectID int64) error { if projectID <= 0 { return fmt.Errorf("%w: %s", projectworkflow.ErrPrerequisiteNotMet, rdevPrerequisiteMessage) } ok, err := s.hasCompletedMainInstance(ctx, projectID) if err != nil { return err } if !ok { return fmt.Errorf("%w: %s", projectworkflow.ErrPrerequisiteNotMet, rdevPrerequisiteMessage) } return nil } func (s *Service) hasCompletedMainInstance(ctx context.Context, projectID int64) (bool, error) { if s.completedMainHook != nil { return s.completedMainHook(ctx, projectID) } if s.pool == nil { return false, nil } var ok bool err := s.pool.QueryRow(ctx, ` SELECT EXISTS ( SELECT 1 FROM project_workflow_instances WHERE project_id = $1 AND status = 'completed' AND config_snapshot->>'workflow_definition_key' = 'main' ) `, projectID).Scan(&ok) return ok, err } // ErrUnboundControlledProject 表示受控目录任务没有项目绑定,不能静默落到隔离工作区。 var ErrUnboundControlledProject = errors.New("会话未绑定项目,受控交付物无法入账") // controlledDeliverableRequiresProject 覆盖发布目录里的受控任务(旧键与新键)和研发设计控制 rd_*。 // 未知自由任务两边都原样返回,不在此列。 func controlledDeliverableRequiresProject(taskKey string) bool { key := strings.TrimSpace(taskKey) if key == "" { return false } if strings.HasPrefix(key, "rd_") { return true } return workflowdef.CanonicalTaskKey(key) != key || workflowdef.ImplementationTaskKey(key) != key } func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error) { if controlledDeliverableRequiresProject(req.TaskKey) && req.ProjectID <= 0 { return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: squad.TaskArtifactTitle(req.TaskKey), Success: false, ErrorReason: ErrUnboundControlledProject.Error(), }, ErrUnboundControlledProject } if err := s.ensureTaskStageAccess(ctx, req.ProjectID, req.UserID, req.TaskKey); err != nil { return &TaskRunResult{ TaskKey: req.TaskKey, Success: false, ErrorReason: err.Error(), }, err } if strings.HasPrefix(strings.TrimSpace(req.TaskKey), "rd_") { if err := s.ensureRDevMainCompleted(ctx, req.ProjectID); err != nil { return &TaskRunResult{ TaskKey: req.TaskKey, Success: false, ErrorReason: err.Error(), }, err } } materials, inputManifest, err := s.ResolveMaterials(ctx, req) if err != nil { return nil, err } if req.RunID != "" && inputManifest != nil { inputManifest["run_id"] = req.RunID } // 针对 system_reqs(智能产品经理):若未显式提供 materials,自动抽取项目事实黑板/集体上下文为需求输入 if workflowdef.SameTaskKey(req.TaskKey, "system_reqs") && len(materials) == 0 && s.ctxMgr != nil && req.ProjectID > 0 { coll, err := s.getProjectContextForUser(ctx, req.ProjectID, req.UserID) if err == nil && coll != nil { var b strings.Builder customer := coll.BlackboardFacts["customer_name"] if customer == "" { customer = req.CustomerName } device := coll.BlackboardFacts["device_type"] if device == "" { device = req.DeviceType } intendedUse := coll.BlackboardFacts["intended_use"] if intendedUse == "" { intendedUse = req.IntendedUse } targetMarket := coll.BlackboardFacts["target_market"] if targetMarket == "" { targetMarket = req.TargetMarket } b.WriteString(fmt.Sprintf("# 项目需求背景事实与上下文输入\n\n- 项目名称: %s\n- 客户名称: %s\n- 产品类型: %s\n- 预期用途: %s\n- 目标市场: %s\n\n", coll.ProjectName, customer, device, intendedUse, targetMarket)) if len(coll.BlackboardFacts) > 0 { b.WriteString("## 已提取项目事实与技术参数\n") for k, v := range coll.BlackboardFacts { b.WriteString(fmt.Sprintf("- %s: %s\n", k, v)) } b.WriteString("\n") } if len(coll.MilestoneDecisions) > 0 { b.WriteString("## 关键里程碑与团队决策\n") for _, d := range coll.MilestoneDecisions { b.WriteString(fmt.Sprintf("- [%s] %s: %s\n", d.UserName, d.ActivityType, d.Title)) } b.WriteString("\n") } materials = append(materials, squad.MaterialItem{ Filename: "project_requirements_context.md", Content: []byte(b.String()), }) } } // 自动补充知识库检索 (如针对 research、feasibility_reg、cleanroom_spec、prelim_design、system_reqs、test_manager 等阶段) hits := req.KnowledgeHits if len(hits) == 0 && taskIs(req.TaskKey, "research", "feasibility_reg", "cleanroom_spec", "prelim_design", "system_reqs", "test_manager") { device := req.DeviceType if device == "" && req.Fields != nil { device = req.Fields["device_type"] } market := req.TargetMarket if market == "" && req.Fields != nil { market = req.Fields["target_market"] } searchQuery := strings.TrimSpace(req.ProjectName + " " + device + " " + market) if searchQuery != "" { streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitPhase("knowledge_retrieval", fmt.Sprintf("项目助手正在检索权威法规知识库并筛选适用条款(关键词: %s)...", searchQuery), "regulatory", 0, 0, 10) } if kbHits, err := s.kb.Search(ctx, searchQuery, kb.SearchOptions{Limit: 5}); err == nil && len(kbHits) > 0 { if streamer != nil { streamer.EmitPhase("knowledge_retrieved", fmt.Sprintf("法规知识库初筛完成,精准命中 %d 条国家与行业标准条款", len(kbHits)), "regulatory", 0, 0, 15) var hitLines []string for _, h := range kbHits { hitLines = append(hitLines, fmt.Sprintf("- [%s] %s (%s)", h.DocCode, h.Title, h.CiteKey)) } streamer.EmitDetail("knowledge_hits", "knowledge", "regulatory", "命中法规标准清单", strings.Join(hitLines, "\n"), true) } for _, kh := range kbHits { hits = append(hits, squad.KnowledgeHit{ DocCode: kh.DocCode, Category: kh.Category, Title: kh.Title, HeadingPath: kh.HeadingPath, Content: kh.Content, CiteKey: kh.CiteKey, Source: kh.Source, }) } } } } // 文档治理与认领拦截前置判定 artifactTitle := squad.TaskArtifactTitle(req.TaskKey) docID, docErr := s.ResolveAndValidateDocID(ctx, req) if docErr != nil { return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: docErr.Error(), }, docErr } // 智能测试工程师:统一项目上下文快照;SRS 仅为可选高权威来源 var upstreamSRDoc *docgov.DocumentRecord if taskIs(req.TaskKey, "test_manager", "system_reqs") { priorDocs := make([]squad.PriorDocItem, 0) facts := map[string]string{} if req.Fields != nil { for k, v := range req.Fields { facts[k] = v } } if s.ctxMgr != nil && req.ProjectID > 0 { if coll, err := s.getProjectContextForUser(ctx, req.ProjectID, req.UserID); err == nil && coll != nil { for k, v := range coll.BlackboardFacts { facts[k] = v } } } upstreamDocID := fmt.Sprintf("doc_proj%d_system_reqs", req.ProjectID) if s.docGov != nil && req.ProjectID > 0 { if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == vocabulary.StateApproved { upstreamSRDoc = doc if doc.WorkDir != "" { contentPath := filepath.Join(doc.WorkDir, workspace.DeliverableKey) if b, readErr := os.ReadFile(contentPath); readErr == nil && len(b) > 0 { priorDocs = append(priorDocs, squad.PriorDocItem{ DocKey: "system_reqs", Title: "系统需求规格书", Content: string(b), Stage: "approved", }) } } } } dialogueInputs := s.collectSnapshotDialogueInputs(ctx, req.ProjectID, req.UserID) ctxSvc := projectcontext.NewService(s.pool) snap, _ := ctxSvc.BuildSnapshot(ctx, projectcontext.BuildInput{ ProjectID: req.ProjectID, TaskKey: req.TaskKey, UserID: req.UserID, ProjectName: req.ProjectName, Facts: facts, Materials: materials, PriorDocs: priorDocs, Knowledge: hits, Dialogue: dialogueInputs.Dialogue, Decisions: dialogueInputs.Decisions, }) if snap != nil { snap.ExcludedPrivateCount += dialogueInputs.ExcludedPrivate snap.ExcludedNoAccessCount += dialogueInputs.ExcludedNoAccess } if snap != nil { if extra := projectcontext.MaterialsForWorkspace(snap, 12); len(extra) > 0 { materials = append(materials, extra...) } } } // 研发设计控制:项目上下文快照 + 同项目已批准文档摘要注入 if strings.HasPrefix(strings.TrimSpace(req.TaskKey), "rd_") && req.ProjectID > 0 { priorDocs := make([]squad.PriorDocItem, 0) facts := map[string]string{} if req.Fields != nil { for k, v := range req.Fields { facts[k] = v } } if s.ctxMgr != nil { if coll, err := s.getProjectContextForUser(ctx, req.ProjectID, req.UserID); err == nil && coll != nil { for k, v := range coll.BlackboardFacts { facts[k] = v } } } // 优先注入已批准 SR(PM 或 rd_system_reqs) for _, key := range []string{"system_reqs", "pm_system_reqs", "rd_system_reqs"} { docID := fmt.Sprintf("doc_proj%d_%s", req.ProjectID, key) if s.docGov == nil { continue } doc, err := s.docGov.GetDocument(ctx, docID) if err != nil || doc == nil || doc.CurrentState != vocabulary.StateApproved || doc.WorkDir == "" { continue } if b, readErr := os.ReadFile(filepath.Join(doc.WorkDir, workspace.DeliverableKey)); readErr == nil && len(b) > 0 { priorDocs = append(priorDocs, squad.PriorDocItem{ DocKey: key, Title: doc.Title, Content: string(b), Stage: "approved", }) } } dialogueInputs := s.collectSnapshotDialogueInputs(ctx, req.ProjectID, req.UserID) ctxSvc := projectcontext.NewService(s.pool) snap, _ := ctxSvc.BuildSnapshot(ctx, projectcontext.BuildInput{ ProjectID: req.ProjectID, TaskKey: req.TaskKey, UserID: req.UserID, ProjectName: req.ProjectName, Facts: facts, Materials: materials, PriorDocs: priorDocs, Knowledge: hits, Dialogue: dialogueInputs.Dialogue, Decisions: dialogueInputs.Decisions, }) if snap != nil { snap.ExcludedPrivateCount += dialogueInputs.ExcludedPrivate snap.ExcludedNoAccessCount += dialogueInputs.ExcludedNoAccess if extra := projectcontext.MaterialsForWorkspace(snap, 12); len(extra) > 0 { materials = append(materials, extra...) } } } duties, dutyPolicy, governed := resolveTaskDocumentDuties(ctx, s.pool, req, s.instanceDuty) authorID, authorName := duties.Author.UserID, duties.Author.Name reviewerID, reviewerName := duties.Reviewer.UserID, duties.Reviewer.Name approverID, approverName := duties.Approver.UserID, duties.Approver.Name if authorName == "" { authorName = "编制工程师" } if reviewerName == "" && reviewerID > 0 { reviewerName = "审核负责人" } if approverName == "" && approverID > 0 { approverName = "批准负责人" } var docRec *docgov.DocumentRecord var leaseHolderID string var leaseAcquired bool var appliedProposalID string if s.docGov != nil && strings.TrimSpace(docID) != "" { docStage := resolveDocStage(req) var createErr error if s.createDocumentHook != nil { docRec, createErr = s.createDocumentHook( ctx, docID, req.ProjectID, req.TaskKey, artifactTitle, docStage, req.WorkDir, authorID, authorName, reviewerID, reviewerName, approverID, approverName, ) } else { docRec, createErr = s.docGov.CreateOrGetDocument( ctx, docID, req.ProjectID, req.TaskKey, artifactTitle, docStage, req.WorkDir, authorID, authorName, reviewerID, reviewerName, approverID, approverName, ) } if createErr != nil { return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "create document ledger failed: " + createErr.Error(), DocRecord: docRec, }, createErr } if governed && docRec != nil { if lockErr := s.docGov.LockDocumentDuty(ctx, docRec.DocID, dutyPolicy); lockErr != nil { log.Printf("[docgov] lock document duty %s failed: %v", docRec.DocID, lockErr) return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "lock document duty failed: " + lockErr.Error(), DocRecord: docRec, }, lockErr } docRec.DutyLocked = true docRec.DutyPolicy = dutyPolicy } if s.pool != nil && req.ProjectID > 0 { assigned, _, healErr := docgov.HealProjectDocuments(ctx, s.pool, req.ProjectID) if healErr != nil { log.Printf("[docduty] heal project %d after generate failed: %v", req.ProjectID, healErr) return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "document duty heal failed: " + healErr.Error(), DocRecord: docRec, }, healErr } // 受实例治理的文档不得用项目默认职责覆盖内存态返回值。 if !governed && docRec != nil { members, creator, loadErr := docduty.LoadProjectMembers(ctx, s.pool, req.ProjectID) if loadErr != nil { log.Printf("[docduty] load members project %d after generate failed: %v", req.ProjectID, loadErr) return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "document duty load failed: " + loadErr.Error(), DocRecord: docRec, }, loadErr } next, _ := docduty.HealBinding(bindingFromDoc(docRec), assigned, docduty.ActiveMemberIDs(members, creator)) writeBindingToDoc(docRec, next) } } if req.ForceDirectEdit { if docRec != nil && docRec.IsLocked { return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: fmt.Sprintf("文档处于【%s】阶段已锁定,禁止 force_direct_edit", docRec.CurrentState), DocRecord: docRec, }, vocabulary.ErrDocumentLocked } // 已认领文档必须有编制人已同意的提案;未认领可直接强制覆写 if docRec != nil && docRec.AuthorClaimed { prop, _ := s.docGov.FindApprovedProposal(ctx, docID) if prop == nil { return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "force_direct_edit 需要编制人已同意的变更提案 (approved_by_author)", DocRecord: docRec, }, vocabulary.ErrForceEditNotAllowed } appliedProposalID = prop.ProposalID } } else if docRec != nil { checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, vocabulary.RoleSystem) if checkErr != nil { if errors.Is(checkErr, vocabulary.ErrDocumentLocked) || errors.Is(checkErr, vocabulary.ErrDocumentBusy) { reason := checkErr.Error() if checkRes != nil && checkRes.Reason != "" { reason = checkRes.Reason } return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: reason, DocRecord: docRec, }, checkErr } } if checkRes != nil && checkRes.RequiresProposal { // 文档处于已认领状态,必须拦截直接修改,生成变更提案待编制人拍板 prop := checkRes.PendingProposal if prop == nil { proposedChanges := fmt.Sprintf("基于项目最新输入【%s - %s】优化交付物技术评估与法规条款", req.DeviceType, req.IntendedUse) impactAnalysis := "更新正文关键章节及提取字段,可能对前序设计基线产生联动影响" riskAssessment := "修改已认领的交付物结论,需由编制负责人核准技术风险与合规结论" prop, _ = s.docGov.ProposeModification(ctx, docID, "agent_squad", proposedChanges, impactAnalysis, riskAssessment) } if s.notify != nil && prop != nil && docRec.AuthorID > 0 { _ = s.notify.UpsertDocumentDigest(ctx, notification.UpsertDigestParams{ UserID: docRec.AuthorID, ProjectID: req.ProjectID, DigestKind: notification.DigestKindProposalRequired, DocID: docID, DocTitle: artifactTitle, Severity: notification.SeverityActionRequired, ActorID: req.UserID, ActorName: "agent_squad", Summary: prop.ProposedChanges, ProposalID: prop.ProposalID, }) } streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitGovernance(stream.GovernancePayload{ DocID: docID, Action: "proposal_required", CurrentState: string(docRec.CurrentState), IsLocked: docRec.IsLocked, AuthorClaimed: docRec.AuthorClaimed, ProposalID: prop.ProposalID, ProposedChanges: prop.ProposedChanges, ImpactSummary: prop.ImpactAnalysis, RiskSummary: prop.RiskAssessment, NotificationTitle: fmt.Sprintf("文档【%s】已被认领,变更已拦截", artifactTitle), NotificationMsg: "系统已拦截智能体小队直接静默覆写,并自动生成变更提案等待编制负责人拍板授权", }) streamer.EmitSummary( fmt.Sprintf("⚠️ 文档【%s】变更拦截", artifactTitle), fmt.Sprintf("该文档已由编制负责人认领。本次调整需拍板提案 [%s],确认后方可覆写更新。", prop.ProposalID), 80, "V0.5", ) } return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: "文档已被编制负责人认领,系统已拦截自动覆写并生成变更提案,请编制负责人拍板确认", DocRecord: docRec, PendingProposal: prop, RequiresAuthorApproval: true, }, nil } } // 通过权限检查后获取小队编辑租约(提案拦截路径不持锁) leaseHolderID = strings.TrimSpace(req.RunID) if leaseHolderID == "" { leaseHolderID = fmt.Sprintf("run_%d", time.Now().UnixNano()) } leaseTTL := req.Timeout if leaseTTL <= 0 && s.cfg != nil { leaseTTL = s.cfg.AgentTimeout } if leaseTTL <= 0 { leaseTTL = vocabulary.DefaultSquadLeaseTTL } leased, leaseErr := s.docGov.AcquireLease(ctx, docgov.LeaseAcquireRequest{ DocID: docID, HolderType: vocabulary.LeaseHolderSquad, HolderID: leaseHolderID, WorkDir: req.WorkDir, TTL: leaseTTL, }) if leaseErr != nil { reason := leaseErr.Error() if leased != nil { docRec = leased } return &TaskRunResult{ TaskKey: req.TaskKey, ArtifactTitle: artifactTitle, WorkDir: req.WorkDir, Success: false, ErrorReason: reason, DocRecord: docRec, }, leaseErr } docRec = leased leaseAcquired = true streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitGovernance(stream.GovernancePayload{ DocID: docID, Action: "lease_acquired", CurrentState: string(docRec.CurrentState), IsLocked: docRec.IsLocked, AuthorClaimed: docRec.AuthorClaimed, NotificationTitle: fmt.Sprintf("文档【%s】编辑租约已获取", artifactTitle), NotificationMsg: "智能体小队正在生成/更新该文档,期间禁止人工编辑与批注", }) } stopHB := make(chan struct{}) defer close(stopHB) go func() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-stopHB: return case <-ticker.C: hbCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) _, _ = s.docGov.HeartbeatLease(hbCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID, leaseTTL) cancel() } } }() defer func() { if leaseAcquired { relCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) _, _ = s.docGov.ReleaseLease(relCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID) cancel() if streamer := stream.FromContext(ctx); streamer != nil { streamer.EmitGovernance(stream.GovernancePayload{ DocID: docID, Action: "lease_released", NotificationTitle: fmt.Sprintf("文档【%s】编辑租约已释放", artifactTitle), NotificationMsg: "文档已可继续人工编辑或批注", }) } } }() } // 混合上下文装配 (Context Blender) userCtxStr := req.UserContext memBank := req.MemoryBankFiles if memBank == nil { memBank = make(map[string]string) } blackboardFacts := map[string]string{} if s.ctxMgr != nil { blended, err := s.buildBlendedSquadContextForUser(ctx, req.ProjectID, req.UserID, req.TaskKey, "squad") if err == nil && blended != nil { if userCtxStr == "" && blended.PersonalizedGuidance != "" { userCtxStr = blended.PersonalizedGuidance } if blended.PersonalizedGuidance != "" { memBank["user_preferences.md"] = blended.PersonalizedGuidance } if len(blended.KeyDecisions) > 0 { memBank["team_decisions.md"] = "# 项目团队关键决策与里程碑记录\n\n" + strings.Join(blended.KeyDecisions, "\n") } for k, v := range blended.BlackboardFacts { blackboardFacts[k] = v } } } dialoguePkg, conflictExps, dlgErr := s.buildDialoguePackageForRun(ctx, req, blackboardFacts) if dlgErr != nil { log.Printf("dialogue_ctx_package_failed project_id=%d err=%v", req.ProjectID, dlgErr) } priorDocs := req.PriorDocs if s.stageGate != nil && req.ProjectID > 0 && req.UserID > 0 { priorDocs = s.stageGate.FilterPriorDocs(ctx, req.ProjectID, req.UserID, priorDocs) } squadReq := squad.SquadRunRequest{ TaskKey: req.TaskKey, Title: req.Title, ProjectName: req.ProjectName, CustomerName: req.CustomerName, DeviceType: req.DeviceType, IntendedUse: req.IntendedUse, TargetMarket: req.TargetMarket, Fields: req.Fields, Materials: materials, Backend: req.Backend, Timeout: req.Timeout, WorkDir: req.WorkDir, CustomSteps: req.CustomSteps, AutoRepair: autoRepairForControlledSquad(req.AutoRepair, docID, docRec), KnowledgeHits: hits, PriorDocs: priorDocs, UserContext: userCtxStr, MemoryBankFiles: memBank, DialoguePackage: dialoguePkg, SessionID: req.SessionID, UserID: req.UserID, ProjectID: req.ProjectID, RunID: req.RunID, } res, err := s.squad.ExecuteTask(withSquadProgress(ctx), squadReq) if err != nil { if res != nil { out := &TaskRunResult{ TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration, Success: false, ErrorReason: res.ErrorReason, AdvisorBrief: res.AdvisorBrief, } appendContextConflictCards(out, req, conflictExps) return out, err } return nil, err } outRes, finErr := s.finalizeSquadDeliverable(ctx, req, res, docID, docRec, appliedProposalID, upstreamSRDoc) if outRes != nil { appendContextConflictCards(outRes, req, conflictExps) } return outRes, finErr } // appendContextConflictCards projects trigger_user HITL verdicts onto the run // result so the transport layer can surface confirmation cards to the user. // Verdicts exist regardless of task success; cards are capped by the engine's // MaxCardsPerTask and deduped per (run, fact key). func appendContextConflictCards(out *TaskRunResult, req TaskRunRequest, exps []*intent.PendingExpectation) { if out == nil || len(exps) == 0 { return } seen := map[string]struct{}{} for _, exp := range exps { if exp == nil || exp.TargetAction != "confirm_context_fact" { continue } factKey := exp.Payload["fact_key"] if _, dup := seen[factKey]; dup { continue } seen[factKey] = struct{}{} out.PendingContextConflicts = append(out.PendingContextConflicts, ContextConflictCard{ ExpectationID: exp.ExpectationID, FactKey: factKey, WinningValue: exp.Payload["winning_value"], Prompt: exp.Prompt, Snippet: exp.ContextSnippet, }) if len(out.PendingContextConflicts) >= conflictresolve.MaxCardsPerTask { break } } } // qualityBlocksControlledProjection 质量门禁未过时禁止任何正式受控投影。 // 独立于 Success,避免 Success 误为 true 时仍 Materialize。