Files
Agent-System/internal/bizdev/service_task_run.go
T
weichen 76ddf4dd35 feat(ctx): surface dialogue conflict confirmation cards to triggering user
Capture trigger_user HITL expectations from dialogue adjudication (previously
dropped at the bizdev call site), project them as chat confirmation cards via
taskrun OnTerminal (exactly-once), arm the session PendingExpectation, and
handle confirm_context_fact hits through the supplement-info blackboard path.
Decline (纠正) guides the user to state the correct fact.
2026-09-25 21:01:37 +00:00

743 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/intent"
"chat-agent-system/internal/notification"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/kb"
"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
}
func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error) {
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(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。