Files
Agent-System/internal/bizdev/service.go
T
weichenandCursor 2d86d330e5 fix(docgov): keep squad AutoRepair/Success semantics; force repair only on controlled docs
小队修复预算只认 AutoRepair,Success 只表示执行完成;bizdev 在解析出 docRec 后强制开启修复,投影与阶段停链仍以 QualityReport 为准。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 15:19:20 +00:00

1300 lines
45 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"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/freedoc"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/projectcontext"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/workflowdef"
"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
docRev *docrevision.Service
freeDoc *freedoc.Service
projectWorkflow *projectworkflow.Service
pool *pgxpool.Pool
schemaVersion int64
stageGate StageAccessGate
// stageTaskHook 仅测试注入:覆盖 ExecuteStage 子任务执行,验证链中止边界。
stageTaskHook func(context.Context, TaskRunRequest) (*TaskRunResult, error)
}
// NewService 构造业务服务;生产/test 环境数据库不可用且未显式 ALLOW_IN_MEMORY 时返回 error。
func NewService(cfg *config.Config) (*Service, error) {
if cfg == nil {
cfg = &config.Config{
DataDir: "./data/dev",
}
}
var pool *pgxpool.Pool
var schemaVersion int64
dbConfigured := cfg.ContextDbHost != ""
if dbConfigured {
poolCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p, err := pgxpool.New(poolCtx, cfg.ContextDSN())
if err != nil {
if !cfg.AllowInMemory {
return nil, fmt.Errorf("database connect failed: %w", err)
}
} else if pingErr := p.Ping(poolCtx); pingErr != nil {
p.Close()
if !cfg.AllowInMemory {
return nil, fmt.Errorf("database ping failed: %w", pingErr)
}
} else {
pool = p
migrationsDir := db.ResolveMigrationsDir()
if migErr := db.RunMigrations(poolCtx, pool, migrationsDir); migErr != nil {
pool.Close()
if !cfg.AllowInMemory {
return nil, fmt.Errorf("database migration failed: %w", migErr)
}
} else {
authzStore := authz.NewAuthorizer(pool)
_ = authzStore.BackfillCreatorMembers(poolCtx)
if ver, verErr := db.CurrentVersion(poolCtx, pool); verErr == nil {
schemaVersion = ver
}
}
}
}
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)
docRev := docrevision.NewService(pool)
docRev.DataDir = cfg.DataDir
freeDoc := freedoc.NewService(pool, ctxMgr)
freeDoc.SetDocRevision(docRev)
pwStore := projectworkflow.NewStore(pool)
fallbackCatalog := projectworkflow.NewDefaultCatalog()
wfStore := workflowdef.NewStore(pool)
if pool != nil && workflowdef.TablesReady(context.Background(), pool) {
_ = workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1)
}
catalog := workflowdef.NewCatalog(context.Background(), wfStore, fallbackCatalog)
pwSvc := projectworkflow.NewService(pwStore, catalog)
docRev.ImpactResolver = func(ctx context.Context, docID string) []docrevision.DocImpact {
if pwSvc == nil {
return nil
}
lineages, err := pwSvc.Store().GetLineagesForUpstream(ctx, docID)
if err != nil || len(lineages) == 0 {
return nil
}
down := make([]struct {
DocID string
Reason string
}, 0, len(lineages))
for _, ln := range lineages {
if ln == nil {
continue
}
reason := ln.StaleReason
if reason == "" {
reason = "上游文档内容变更,下游需复核"
}
down = append(down, struct {
DocID string
Reason string
}{DocID: ln.DownstreamDocID, Reason: reason})
}
return docrevision.ImpactFromLineage(down)
}
svc := &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(cfg),
ctxMgr: ctxMgr,
cas: casStore,
docGov: docGov,
docRev: docRev,
freeDoc: freeDoc,
projectWorkflow: pwSvc,
pool: pool,
schemaVersion: schemaVersion,
}
// 注册终审批准回调:自动切片向量化沉淀为往期标杆案例
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, nil
}
// SchemaVersion 返回启动时缓存的数据库 schema 版本(goose)。
func (s *Service) SchemaVersion() int64 {
if s == nil {
return 0
}
return s.schemaVersion
}
// LiveSchemaVersion 从数据库实时读取当前 schema 版本;失败时回退缓存值。
func (s *Service) LiveSchemaVersion(ctx context.Context) int64 {
if s == nil {
return 0
}
if s.pool == nil {
return s.schemaVersion
}
ver, err := db.CurrentVersion(ctx, s.pool)
if err != nil {
return s.schemaVersion
}
return ver
}
// DatabaseReady 数据库是否已连接且迁移完成。
func (s *Service) DatabaseReady() bool {
return s != nil && s.pool != nil
}
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) DocRevision() *docrevision.Service {
if s == nil {
return nil
}
return s.docRev
}
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_dev", "feasibility_reg", "user_requirements", "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) {
if err := s.ensureTaskStageAccess(ctx, req.ProjectID, req.UserID, req.TaskKey); 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 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 && (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, 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 req.TaskKey == "test_manager" || req.TaskKey == "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 == docgov.StateApproved {
upstreamSRDoc = doc
if doc.WorkDir != "" {
contentPath := filepath.Join(doc.WorkDir, "output.md")
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...)
}
}
}
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 && strings.TrimSpace(docID) != "" {
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 = 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 = 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.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")
}
}
}
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,
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 {
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
}
return s.finalizeSquadDeliverable(ctx, req, res, docID, docRec, appliedProposalID, upstreamSRDoc)
}
// qualityBlocksControlledProjection 质量门禁未过时禁止任何正式受控投影。
// 独立于 Success,避免 Success 误为 true 时仍 Materialize。
func qualityBlocksControlledProjection(report *squad.QualityReport) bool {
return report != nil && !report.Passed
}
// autoRepairForControlledSquad 已解析出正式受控文档后强制开启修复;非受控调用仍尊重请求值。
func autoRepairForControlledSquad(reqAutoRepair bool, docID string, docRec *docgov.DocumentRecord) bool {
if strings.TrimSpace(docID) != "" && docRec != nil {
return true
}
return reqAutoRepair
}
func qualityFailedErrorReason(report *squad.QualityReport) string {
if report == nil {
return "质量门禁未通过"
}
if len(report.Violations) > 0 {
return "质量门禁未通过:" + strings.Join(report.Violations, ";")
}
return fmt.Sprintf("质量门禁未通过(评分 %.0f/100)", report.Score)
}
// finalizeSquadDeliverable 处理小队返回后的受控文档投影边界。
// 质量失败必须在 SetDocumentWorkDir / Materialize / MarkDocumentContentUpdated / 完成活动之前短路。
func (s *Service) finalizeSquadDeliverable(
ctx context.Context,
req TaskRunRequest,
res *squad.SquadRunResult,
docID string,
docRec *docgov.DocumentRecord,
appliedProposalID string,
upstreamSRDoc *docgov.DocumentRecord,
) (*TaskRunResult, error) {
if res == nil {
return &TaskRunResult{TaskKey: req.TaskKey, Success: false, ErrorReason: "小队无返回结果"}, nil
}
// 质量门禁短路:保留隔离工作区与可见失败摘要,不改正式 doc work_dir/head/hash,不激活账本版本。
if qualityBlocksControlledProjection(res.QualityReport) {
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: false,
ErrorReason: qualityFailedErrorReason(res.QualityReport),
QualityReport: res.QualityReport,
Manifest: res.Manifest,
AdvisorBrief: res.AdvisorBrief,
DocRecord: docRec,
}, nil
}
// RecordActivity for "完成交付物" must NOT run here: Materialize may still fail or
// leave needs_confirmation. Activity is recorded only after a committed projection
// (or a pending-awaiting note that is explicitly not completion).
if res.Success && s.docGov != nil && strings.TrimSpace(docID) != "" && docRec != nil {
_ = s.docGov.SetDocumentWorkDir(ctx, docID, res.WorkDir)
dataRoot := "."
if s.cfg != nil && s.cfg.DataDir != "" {
dataRoot = s.cfg.DataDir
}
// DOCREV: materialize first; only committed heads may update DocGov hash / proposal applied.
// Squad may have already written candidate body to workDir/output.md — pending must restore ledger head.
if s.docRev != nil && docRec != nil && strings.TrimSpace(docRec.DocID) != "" {
actor := docrevision.ActorSnapshot{SquadName: "bizdev-squad", Model: res.BackendUsed}
mat, matErr := s.docRev.MaterializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{
DocID: docID, RunID: fmt.Sprintf("bizdev-%d-%s", time.Now().UnixNano(), req.TaskKey),
ProposalID: appliedProposalID, OutputMarkdown: res.MarkdownBody,
ProjectID: req.ProjectID, WorkDir: res.WorkDir,
ActorKind: docrevision.ActorAgentSquad, ActorSnapshot: actor,
AutoCommitUserID: req.UserID,
})
if matErr != nil {
reason := fmt.Sprintf("文档版本账本/变更报告落盘失败: %v", matErr)
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: false,
ErrorReason: reason,
QualityReport: res.QualityReport,
Manifest: res.Manifest,
AdvisorBrief: res.AdvisorBrief,
DocRecord: docRec,
}, matErr
}
if mat != nil && mat.Candidate != nil && mat.Candidate.Status == docrevision.CandidateNeedsConfirmation {
if err := s.docRev.ProjectOutputMarkdown(docID, dataRoot); err != nil {
reason := fmt.Sprintf("高风险待确认:恢复账本 head 到事实源失败: %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
headSHA := ""
if mat.State != nil {
headSHA = mat.State.HeadSHA256
}
if headSHA != "" {
if err := s.docGov.MarkDocumentContentUpdated(ctx, docID, headSHA, false); err != nil {
reason := fmt.Sprintf("高风险待确认:恢复 document_records 哈希失败: %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
}
if latest, err := s.docGov.GetDocument(ctx, docID); err == nil {
docRec = latest
}
risk := ""
if mat.Report != nil {
risk = mat.Report.RiskLevel
}
var pendCand, pendRep int64
if mat.Candidate != nil {
pendCand = mat.Candidate.ID
if mat.Candidate.ChangeReportID != nil {
pendRep = *mat.Candidate.ChangeReportID
}
}
note := fmt.Sprintf(
"高风险变更(%s)待编制人确认:候选 #%d。正式正文仍为当前账本 head,候选正文仅保留在不可变快照中;请打开「待确认报告」确认提交或拒绝。",
risk, pendCand,
)
s.recordSquadDeliveryActivity(ctx, req, res.ArtifactTitle,
fmt.Sprintf("交付物【%s】候选待编制人确认", res.ArtifactTitle),
map[string]any{
"task_key": res.TaskKey,
"work_dir": res.WorkDir,
"doc_id": docID,
"pending_agent_candidate_id": pendCand,
"pending_change_report_id": pendRep,
"pending_risk_level": risk,
"awaiting_author_confirmation": true,
}, true)
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: true,
QualityReport: res.QualityReport, Manifest: res.Manifest, AdvisorBrief: res.AdvisorBrief,
DocRecord: docRec,
// RequiresAuthorApproval signals "needs human decision"; distinguish from DocGov
// proposal intercept via PendingAgentCandidateID (no PendingProposal).
RequiresAuthorApproval: true,
PendingAgentCandidateID: pendCand, PendingChangeReportID: pendRep,
PendingRiskLevel: risk, PendingConfirmationNote: note,
}, nil
}
// Committed (auto-commit or ready path): project head then update DocGov.
if err := s.docRev.ProjectOutputMarkdown(docID, dataRoot); err != nil {
reason := fmt.Sprintf("账本已提交但工作区投影失败: %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
headSHA := ""
if mat != nil && mat.State != nil {
headSHA = mat.State.HeadSHA256
}
if headSHA == "" {
sha := sha256.Sum256([]byte(res.MarkdownBody))
headSHA = hex.EncodeToString(sha[:])
}
if err := s.docGov.MarkDocumentContentUpdated(ctx, docID, headSHA, false); err != nil {
reason := fmt.Sprintf("账本已提交但 DocGov 哈希投影失败: %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
if appliedProposalID != "" {
if err := s.docGov.MarkProposalApplied(ctx, appliedProposalID); err != nil {
reason := fmt.Sprintf("账本已提交但变更提案标记失败(可重试): %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
}
if latest, err := s.docGov.GetDocument(ctx, docID); err == nil {
docRec = latest
}
s.recordSquadDeliveryActivity(ctx, req, res.ArtifactTitle,
fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle),
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir, "doc_id": docID}, true)
} else {
// No docrevision: preserve prior best-effort hash update (CLI/legacy).
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
_ = s.docGov.MarkDocumentContentUpdated(ctx, docID, shaStr, false)
if appliedProposalID != "" {
if err := s.docGov.MarkProposalApplied(ctx, appliedProposalID); err != nil {
reason := fmt.Sprintf("正文已更新但变更提案标记失败(可重试): %v", err)
return &TaskRunResult{
TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, MarkdownBody: res.MarkdownBody,
WorkDir: res.WorkDir, BackendUsed: res.BackendUsed, Duration: res.Duration,
Success: false, ErrorReason: reason, DocRecord: docRec,
}, err
}
}
if latest, err := s.docGov.GetDocument(ctx, docID); err == nil {
docRec = latest
}
s.recordSquadDeliveryActivity(ctx, req, res.ArtifactTitle,
fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle),
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir, "doc_id": docID}, true)
}
} else if res.Success {
// Uncontrolled / no DocGov path: squad success is the completion boundary.
s.recordSquadDeliveryActivity(ctx, req, res.ArtifactTitle,
fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle),
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir}, true)
}
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,
})
}
// Stale propagation only after fact source committed (not while pending confirmation).
if res.Success && req.TaskKey == "system_reqs" && s.projectWorkflow != nil && req.ProjectID > 0 &&
(s.docRev == nil || (docRec != nil && docRec.ContentSHA256 != "")) {
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
if docRec != nil && docRec.ContentSHA256 != "" {
shaStr = docRec.ContentSHA256
}
_, _ = 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
}
// recordSquadDeliveryActivity writes project activity only for explicit outcomes.
// Callers must not invoke this for Materialize/projection failures.
func (s *Service) recordSquadDeliveryActivity(ctx context.Context, req TaskRunRequest, artifactTitle, title string, detail map[string]any, milestone bool) {
if s == nil || s.ctxMgr == nil || req.ProjectID <= 0 || strings.TrimSpace(title) == "" {
return
}
if detail == nil {
detail = map[string]any{}
}
if _, ok := detail["task_key"]; !ok && req.TaskKey != "" {
detail["task_key"] = req.TaskKey
}
if artifactTitle != "" {
detail["artifact_title"] = artifactTitle
}
_, _ = s.ctxMgr.RecordActivity(ctx, req.ProjectID, req.UserID, "系统智能体小队", "operation",
title, detail, 1.0, milestone)
}
// 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()
if err := s.ensureTaskStageAccess(ctx, req.ProjectID, req.UserID, string(req.Stage)); err != nil {
return &StageRunResult{
Stage: req.Stage,
Success: false,
ErrorReason: err.Error(),
}, err
}
// 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.getProjectContextForUser(ctx, req.ProjectID, req.UserID); 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...)
if s.stageGate != nil && req.ProjectID > 0 && req.UserID > 0 {
accumulatedPriorDocs = s.stageGate.FilterPriorDocs(ctx, req.ProjectID, req.UserID, accumulatedPriorDocs)
}
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,
SessionID: req.SessionID,
ProjectID: req.ProjectID,
UserID: req.UserID,
RunID: req.RunID,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
ReviewerID: req.ReviewerID,
ReviewerName: req.ReviewerName,
ApproverID: req.ApproverID,
ApproverName: req.ApproverName,
ForceDirectEdit: req.ForceDirectEdit,
}
res, err := s.executeStageTask(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 halt := classifyStageTaskHalt(res, tKey); halt != nil {
out := &StageRunResult{
Stage: req.Stage,
StageTitle: targetMeta.Title,
Success: false,
WorkDir: workDir,
Duration: time.Since(start),
ErrorReason: halt.reason,
PrimaryArtifact: res,
SubArtifacts: append(executedArtifacts, res),
}
if halt.pending {
out.PendingAgentCandidateID = res.PendingAgentCandidateID
out.PendingChangeReportID = res.PendingChangeReportID
out.PendingRiskLevel = res.PendingRiskLevel
}
return out, 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)
}