Files
Agent-System/internal/bizdev/service.go
T

887 lines
29 KiB
Go

package bizdev
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/freedoc"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/stream"
"github.com/jackc/pgx/v5/pgxpool"
)
type Service struct {
cfg *config.Config
kb *kb.Client
squad *squad.Service
ctxMgr *ctxmgr.ContextManager
cas *storage.CAS
docGov *docgov.GovernanceEngine
freeDoc *freedoc.Service
projectWorkflow *projectworkflow.Service
pool *pgxpool.Pool
}
func NewService(cfg *config.Config) *Service {
if cfg == nil {
cfg = &config.Config{
DataDir: "./data/dev",
}
}
var pool *pgxpool.Pool
if cfg.ContextDbHost != "" {
poolCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if p, err := pgxpool.New(poolCtx, cfg.ContextDSN()); err == nil {
if p.Ping(poolCtx) == nil {
pool = p
}
}
}
casStore, _ := storage.NewCAS(filepath.Join(cfg.DataDir, "storage"), pool, nil)
ctxMgr := ctxmgr.NewContextManager(pool, casStore, ctxmgr.DefaultTimeDecayConfig())
if cfg.EmbeddingEndpointURL != "" {
ctxMgr.SetEmbedder(kb.NewEmbedder(cfg.EmbeddingEndpointURL, cfg.EmbeddingAPIKey, cfg.EmbeddingModel))
}
docGov := docgov.NewGovernanceEngine(pool)
freeDoc := freedoc.NewService(pool, ctxMgr)
pwStore := projectworkflow.NewStore(pool)
pwSvc := projectworkflow.NewService(pwStore, projectworkflow.NewDefaultCatalog())
svc := &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(cfg),
ctxMgr: ctxMgr,
cas: casStore,
docGov: docGov,
freeDoc: freeDoc,
projectWorkflow: pwSvc,
pool: pool,
}
// 注册终审批准回调:自动切片向量化沉淀为往期标杆案例
docGov.SetOnApprovedHook(func(ctx context.Context, doc *docgov.DocumentRecord) {
if ctxMgr == nil || doc == nil {
return
}
var contentPath string
if doc.WorkDir != "" {
candidates := []string{
doc.WorkDir,
filepath.Join(doc.WorkDir, "output.md"),
filepath.Join(cfg.DataDir, doc.WorkDir, "output.md"),
filepath.Join(cfg.DataDir, "workspaces", doc.WorkDir, "output.md"),
}
for _, p := range candidates {
if b, err := os.ReadFile(p); err == nil && len(b) > 0 {
contentPath = p
break
}
}
}
if contentPath != "" {
if b, err := os.ReadFile(contentPath); err == nil && len(b) > 0 {
_, _ = ctxMgr.IndexApprovedDocument(ctx, doc, string(b))
}
} else {
// 若未在默认工作区直接发现 output.md,尝试深度探测当前数据目录下的工作区文件
pattern := filepath.Join(cfg.DataDir, "workspaces", "**", doc.TaskKey, "output.md")
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
pattern2 := filepath.Join(cfg.DataDir, "**", doc.TaskKey, "output.md")
matches, _ = filepath.Glob(pattern2)
}
if len(matches) > 0 {
if b, err := os.ReadFile(matches[len(matches)-1]); err == nil && len(b) > 0 {
_, _ = ctxMgr.IndexApprovedDocument(ctx, doc, string(b))
}
}
}
if pwSvc != nil && doc.TaskKey == "system_reqs" {
_, _ = pwSvc.CheckAndPropagateStale(ctx, doc.DocID, doc.ContentSHA256, fmt.Sprintf("上游系统需求规格书 V%d 已正式批准,已标记关联下游验证方案为待复核(stale)", doc.Version))
}
})
return svc
}
func (s *Service) ContextManager() *ctxmgr.ContextManager {
return s.ctxMgr
}
func (s *Service) CAS() *storage.CAS {
return s.cas
}
func (s *Service) DocGov() *docgov.GovernanceEngine {
return s.docGov
}
func (s *Service) ProjectWorkflow() *projectworkflow.Service {
return s.projectWorkflow
}
func (s *Service) Pool() *pgxpool.Pool {
return s.pool
}
func (s *Service) FreeformDoc() *freedoc.Service {
return s.freeDoc
}
func (s *Service) KB() *kb.Client {
return s.kb
}
var SupportedTasks = squad.SupportedTasks
var SupportedStages = []StageMeta{
{
Stage: StageV01_Triage,
Title: "V0.1 闪电摸底与盲区探测",
Description: "快速梳理客户诉求与约束,建立基准技术假设,生成首期澄清微信话术与初始需求表",
Tasks: []string{"intake"},
},
{
Stage: StageV05_Feasibility,
Title: "V0.5 方案成型与法规对齐",
Description: "法规与注册路径初筛、竞品调研、系统初步架构设计与关键选型建议",
Tasks: []string{"research", "feasibility_reg", "prelim_design"},
},
{
Stage: StageV10_Quoting,
Title: "V1.0 工程拆解与商业锁定",
Description: "全维度 WBS 工时拆解、Excel 定价核算、正式 CDO 商务报价单与谈判防御条款建议",
Tasks: []string{"cdo_quote"},
},
}
func (s *Service) ListTasks() []TaskMeta {
return s.squad.ListTasks()
}
func (s *Service) ListStages() []StageMeta {
return SupportedStages
}
func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error) {
// 转换 MaterialInput 到 squad.MaterialItem
var materials []squad.MaterialItem
for _, m := range req.Materials {
materials = append(materials, squad.MaterialItem{
Filename: m.Filename,
Content: m.Content,
})
}
// 针对 system_reqs(智能产品经理):若未显式提供 materials,自动抽取项目事实黑板/集体上下文为需求输入
if req.TaskKey == "system_reqs" && len(materials) == 0 && s.ctxMgr != nil && req.ProjectID > 0 {
coll, err := s.ctxMgr.GetProjectContext(ctx, req.ProjectID)
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 && (req.TaskKey == "research" || req.TaskKey == "feasibility_reg" || req.TaskKey == "cleanroom_spec" || req.TaskKey == "prelim_design" || req.TaskKey == "system_reqs" || req.TaskKey == "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 := req.DocID
if docID == "" {
docID = fmt.Sprintf("doc_proj%d_%s", req.ProjectID, req.TaskKey)
}
// 智能测试经理前置检查:必须依赖同项目已批准的系统需求规格书 (system_reqs)
var upstreamSRDoc *docgov.DocumentRecord
if req.TaskKey == "test_manager" {
upstreamDocID := fmt.Sprintf("doc_proj%d_system_reqs", req.ProjectID)
if s.docGov != nil && req.ProjectID > 0 {
doc, err := s.docGov.GetDocument(ctx, upstreamDocID)
if err == nil && doc != nil && doc.CurrentState == docgov.StateApproved {
upstreamSRDoc = doc
}
}
if upstreamSRDoc == nil {
return &TaskRunResult{
TaskKey: req.TaskKey,
ArtifactTitle: artifactTitle,
WorkDir: req.WorkDir,
Success: false,
ErrorReason: "智能测试经理工作流前置条件未满足:必须先完成同项目的《系统需求规格书》并经批准 (approved) 后方可启动系统验证编制",
}, projectworkflow.ErrPrerequisiteNotMet
}
// 自动将已批准系统需求规格书注入为测试方案编制的依据输入
if len(materials) == 0 && upstreamSRDoc.WorkDir != "" {
contentPath := filepath.Join(upstreamSRDoc.WorkDir, "output.md")
if b, err := os.ReadFile(contentPath); err == nil && len(b) > 0 {
materials = append(materials, squad.MaterialItem{
Filename: "upstream_system_requirements.md",
Content: b,
})
}
}
}
authorID := req.AuthorID
if authorID == 0 {
authorID = req.UserID
}
authorName := req.AuthorName
if authorName == "" {
authorName = "编制工程师"
}
reviewerID := req.ReviewerID
if reviewerID == 0 {
reviewerID = 2
}
reviewerName := req.ReviewerName
if reviewerName == "" {
reviewerName = "审核负责人"
}
approverID := req.ApproverID
if approverID == 0 {
approverID = 3
}
approverName := req.ApproverName
if approverName == "" {
approverName = "批准负责人"
}
var docRec *docgov.DocumentRecord
var leaseHolderID string
var leaseAcquired bool
var appliedProposalID string
if s.docGov != nil {
docRec, _ = s.docGov.CreateOrGetDocument(
ctx, docID, req.ProjectID, req.TaskKey, artifactTitle,
"V0.5_feasibility", req.WorkDir,
authorID, authorName, reviewerID, reviewerName, approverID, approverName,
)
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,
}, docgov.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,
}, docgov.ErrForceEditNotAllowed
}
appliedProposalID = prop.ProposalID
}
} else if docRec != nil {
checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, docgov.RoleSystem)
if checkErr != nil {
if errors.Is(checkErr, docgov.ErrDocumentLocked) || errors.Is(checkErr, docgov.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)
}
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 = fmt.Sprintf("run_%d", time.Now().UnixNano())
leaseTTL := req.Timeout
if leaseTTL <= 0 && s.cfg != nil {
leaseTTL = s.cfg.AgentTimeout
}
if leaseTTL <= 0 {
leaseTTL = docgov.DefaultSquadLeaseTTL
}
leased, leaseErr := s.docGov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
DocID: docID,
HolderType: docgov.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:
_, _ = s.docGov.HeartbeatLease(context.Background(), docID, string(docgov.LeaseHolderSquad), leaseHolderID, leaseTTL)
}
}
}()
defer func() {
if leaseAcquired {
_, _ = s.docGov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderSquad), leaseHolderID)
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)
}
if s.ctxMgr != nil {
blended, err := s.ctxMgr.BuildBlendedSquadContext(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")
}
}
}
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: req.AutoRepair,
KnowledgeHits: hits,
PriorDocs: req.PriorDocs,
UserContext: userCtxStr,
MemoryBankFiles: memBank,
}
res, err := s.squad.ExecuteTask(ctx, squadReq)
if err != nil {
if res != nil {
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
WorkDir: res.WorkDir,
BackendUsed: res.BackendUsed,
Duration: res.Duration,
Success: false,
ErrorReason: res.ErrorReason,
AdvisorBrief: res.AdvisorBrief,
}, err
}
return nil, err
}
if res.Success && s.ctxMgr != nil && req.ProjectID > 0 {
_, _ = s.ctxMgr.RecordActivity(ctx, req.ProjectID, req.UserID, "系统智能体小队", "operation",
fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle),
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir},
1.0, true)
}
if res.Success && s.docGov != nil {
_ = s.docGov.SetDocumentWorkDir(ctx, docID, res.WorkDir)
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
_ = s.docGov.MarkDocumentContentUpdated(ctx, docID, shaStr, false)
if appliedProposalID != "" {
_ = s.docGov.MarkProposalApplied(ctx, appliedProposalID)
}
if latest, err := s.docGov.GetDocument(ctx, docID); err == nil {
docRec = latest
}
}
if res.Success && req.TaskKey == "test_manager" && upstreamSRDoc != nil && s.projectWorkflow != nil && req.ProjectID > 0 {
_ = s.projectWorkflow.RecordLineage(ctx, &projectworkflow.DocumentLineage{
ProjectID: req.ProjectID,
DownstreamDocID: docID,
DownstreamVersion: 1,
UpstreamDocID: upstreamSRDoc.DocID,
UpstreamVersion: upstreamSRDoc.Version,
UpstreamSHA256: upstreamSRDoc.ContentSHA256,
IsStale: false,
})
}
if res.Success && req.TaskKey == "system_reqs" && s.projectWorkflow != nil && req.ProjectID > 0 {
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
_, _ = s.projectWorkflow.CheckAndPropagateStale(ctx, docID, shaStr, "上游系统需求规格书已更新,下游验证方案需复核")
}
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
MarkdownBody: res.MarkdownBody,
ExtractedData: res.ExtractedData,
Attachments: res.Attachments,
WorkDir: res.WorkDir,
BackendUsed: res.BackendUsed,
Duration: res.Duration,
Success: res.Success,
ErrorReason: res.ErrorReason,
QualityReport: res.QualityReport,
Manifest: res.Manifest,
AdvisorBrief: res.AdvisorBrief,
DocRecord: docRec,
}, nil
}
// ExecuteStage 执行指定成熟度阶段
func (s *Service) ExecuteStage(ctx context.Context, req StageRunRequest) (*StageRunResult, error) {
return s.executeStageInternal(ctx, req)
}
func (s *Service) executeStageInternal(ctx context.Context, req StageRunRequest) (*StageRunResult, error) {
start := time.Now()
// 1. 确定工作区目录基准
stageKey := string(req.Stage)
if stageKey == "" {
stageKey = string(StageV01_Triage)
req.Stage = StageV01_Triage
}
workDir := req.WorkDir
if workDir == "" {
workDir = filepath.Join(s.cfg.DataDir, "workspaces", "bizdev", fmt.Sprintf("stage_%s_%d", stageKey, time.Now().UnixNano()))
}
_ = os.MkdirAll(workDir, 0o755)
// 2. 匹配阶段元数据与任务序列
var targetMeta *StageMeta
for _, sm := range SupportedStages {
if sm.Stage == req.Stage {
targetMeta = &sm
break
}
}
if targetMeta == nil {
targetMeta = &SupportedStages[0]
}
streamer := stream.FromContext(ctx)
if streamer != nil {
streamer.EmitPhase("stage_started", fmt.Sprintf("开始推进商务成熟度阶段【%s】(包含 %d 项任务序列)...", req.Stage, len(targetMeta.Tasks)), "coordinator", 0, len(targetMeta.Tasks), 5)
}
// 3. 汇总当前阶段累积的事实与前序文档
currentFields := make(map[string]string)
// 若存在项目协同管理器,优先载入已沉淀的项目事实与黑板
if s.ctxMgr != nil && req.ProjectID > 0 {
if projCtx, err := s.ctxMgr.GetProjectContext(ctx, req.ProjectID); err == nil && projCtx != nil {
for k, v := range projCtx.BlackboardFacts {
currentFields[k] = v
}
}
}
if req.Fields != nil {
for k, v := range req.Fields {
currentFields[k] = v
}
}
if req.ProjectName != "" {
currentFields["project_name"] = req.ProjectName
}
if req.CustomerName != "" {
currentFields["customer_name"] = req.CustomerName
}
if req.DeviceType != "" {
currentFields["device_type"] = req.DeviceType
}
if req.IntendedUse != "" {
currentFields["intended_use"] = req.IntendedUse
}
if req.TargetMarket != "" {
currentFields["target_market"] = req.TargetMarket
}
accumulatedPriorDocs := append([]squad.PriorDocItem{}, req.PriorDocs...)
var executedArtifacts []*TaskRunResult
// 4. 按顺序执行当前阶段的子任务序列,并将每一轮产出作为后续任务的 PriorDoc 和事实补充
for _, tKey := range targetMeta.Tasks {
subWorkDir := filepath.Join(workDir, tKey)
tReq := TaskRunRequest{
TaskKey: tKey,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: currentFields,
Materials: req.Materials,
Backend: req.Backend,
Timeout: req.Timeout,
WorkDir: subWorkDir,
AutoRepair: req.AutoRepair,
KnowledgeHits: req.KnowledgeHits,
PriorDocs: accumulatedPriorDocs,
ProjectID: req.ProjectID,
UserID: req.UserID,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
ReviewerID: req.ReviewerID,
ReviewerName: req.ReviewerName,
ApproverID: req.ApproverID,
ApproverName: req.ApproverName,
ForceDirectEdit: req.ForceDirectEdit,
}
res, err := s.ExecuteTask(ctx, tReq)
if err != nil {
return &StageRunResult{
Stage: req.Stage,
StageTitle: targetMeta.Title,
Success: false,
WorkDir: workDir,
Duration: time.Since(start),
ErrorReason: fmt.Sprintf("task %s failed: %v", tKey, err),
SubArtifacts: executedArtifacts,
}, err
}
if res != nil && res.RequiresAuthorApproval {
return &StageRunResult{
Stage: req.Stage,
StageTitle: targetMeta.Title,
Success: false,
WorkDir: workDir,
Duration: time.Since(start),
ErrorReason: fmt.Sprintf("任务【%s】对应文档已被编制负责人认领,系统已拦截直接修改并生成变更提案,需编制人拍板授权", tKey),
SubArtifacts: append(executedArtifacts, res),
}, nil
}
executedArtifacts = append(executedArtifacts, res)
// 吸收回填事实
if res.ExtractedData != nil {
for k, v := range res.ExtractedData {
if v != "" {
currentFields[k] = v
}
}
}
// 追加前序文档供同阶段后续任务使用
accumulatedPriorDocs = append(accumulatedPriorDocs, squad.PriorDocItem{
DocKey: tKey,
Title: res.ArtifactTitle,
Filename: fmt.Sprintf("%s.md", tKey),
Content: res.MarkdownBody,
Stage: string(req.Stage),
})
}
// 5. 确定主交付物并组装阶段级参谋简报
var primaryArtifact *TaskRunResult
if len(executedArtifacts) > 0 {
primaryArtifact = executedArtifacts[len(executedArtifacts)-1]
}
stageBrief := primaryArtifact.AdvisorBrief
if stageBrief != nil {
// 覆盖校准为当前阶段等级
switch req.Stage {
case StageV01_Triage:
stageBrief.MaturityLevel = "V0.1"
case StageV05_Feasibility:
stageBrief.MaturityLevel = "V0.5"
case StageV10_Quoting:
stageBrief.MaturityLevel = "V1.0"
}
}
// 持久化阶段级元数据与事实黑板,便于 L2 上下文装载与对话系统读取
stageMetaRecord := squad.WorkspaceMetadata{
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
TaskKey: string(req.Stage),
Status: "completed",
CreatedAt: start,
}
if b, err := json.MarshalIndent(stageMetaRecord, "", " "); err == nil {
_ = os.WriteFile(filepath.Join(workDir, "workspace.json"), b, 0o644)
}
bbRecord := squad.BlackboardState{
Fields: currentFields,
}
if b, err := json.MarshalIndent(bbRecord, "", " "); err == nil {
_ = os.WriteFile(filepath.Join(workDir, "blackboard.json"), b, 0o644)
}
if stageBrief != nil {
if b, err := json.MarshalIndent(stageBrief, "", " "); err == nil {
_ = os.WriteFile(filepath.Join(workDir, "advisor_brief.json"), b, 0o644)
}
}
// 将本阶段累积沉淀的事实回填到 ContextManager,供下一阶段或后续对话读取
if s.ctxMgr != nil && req.ProjectID > 0 {
_ = s.ctxMgr.UpdateProjectBlackboard(ctx, req.ProjectID, currentFields)
_ = s.ctxMgr.UpdateProjectStage(ctx, req.ProjectID, string(req.Stage))
authorName := req.AuthorName
if authorName == "" {
authorName = "系统协调员"
}
_, _ = s.ctxMgr.RecordActivity(
ctx,
req.ProjectID,
req.UserID,
authorName,
"stage_advance",
fmt.Sprintf("阶段跃迁至【%s】", targetMeta.Title),
map[string]any{
"stage": string(req.Stage),
"duration_ms": time.Since(start).Milliseconds(),
"artifacts": len(executedArtifacts),
"device_type": req.DeviceType,
"target_market": req.TargetMarket,
},
1.0,
true, // 里程碑决策,豁免衰减
)
}
if streamer != nil {
streamer.EmitPhase("stage_completed", fmt.Sprintf("商务成熟度阶段【%s】全部任务协同推演完成!", req.Stage), "coordinator", len(targetMeta.Tasks), len(targetMeta.Tasks), 100)
if stageBrief != nil {
streamer.EmitSummary(targetMeta.Title+"完成", stageBrief.MaturityHeadline, stageBrief.MaturityScore, stageBrief.MaturityLevel)
streamer.EmitBriefing(stageBrief)
}
}
return &StageRunResult{
Stage: req.Stage,
StageTitle: targetMeta.Title,
PrimaryArtifact: primaryArtifact,
SubArtifacts: executedArtifacts,
AdvisorBrief: stageBrief,
Duration: time.Since(start),
Success: true,
WorkDir: workDir,
}, nil
}
// InspectWorkspace 查看指定工作区的当前执行状态、黑板数据与各专员产出
func (s *Service) InspectWorkspace(ctx context.Context, workDir string) (*squad.WorkspaceState, error) {
return s.squad.InspectWorkspace(ctx, workDir)
}
// ResumeOrModifyWorkspace 动态调整任务并继续执行
func (s *Service) ResumeOrModifyWorkspace(ctx context.Context, req squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) {
return s.squad.ResumeOrModifyWorkspace(ctx, req)
}