Files
Agent-System/internal/bizdev/service.go
T
WeiCHEN dca33c705d refactor(boundaries): split five oversized packages by use-case
Move bizdev/freedoc/intent/projectworkflow/squad giant files into
≤800-line use-case siblings without behavior change; short tests green.
2026-09-23 09:52:16 +08:00

305 lines
11 KiB
Go

package bizdev
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"time"
"chat-agent-system/internal/app/uow"
"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/materialindex"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/workflowdef"
"chat-agent-system/internal/workspace"
"github.com/jackc/pgx/v5/pgxpool"
)
type Service struct {
cfg *config.Config
kb *kb.Client
squad *squad.Service
ctxMgr *ctxmgr.ContextManager
cas *storage.CAS
materialIndex *materialindex.Pipeline
docGov *docgov.GovernanceEngine
docRev *docrevision.Service
freeDoc *freedoc.Service
projectWorkflow *projectworkflow.Service
pool *pgxpool.Pool
schemaVersion int64
stageGate StageAccessGate
materializer AgentOutputMaterializer
parts Parts
// stageTaskHook 仅测试注入:覆盖 ExecuteStage 子任务执行,验证链中止边界。
stageTaskHook func(context.Context, TaskRunRequest) (*TaskRunResult, error)
// createDocumentHook 仅测试注入:覆盖账本创建失败路径。
createDocumentHook func(ctx context.Context, docID string, projectID int64, taskKey, title, stage, workDir string, authorID int64, authorName string, reviewerID int64, reviewerName string, approverID int64, approverName string) (*docgov.DocumentRecord, error)
// completedMainHook 仅测试注入:覆盖「main 实例已 completed」查询。
completedMainHook func(ctx context.Context, projectID int64) (bool, error)
// instanceDuty 可选:活跃工作流实例对 taskKey 的治理职责(由 app 装配,避免导入 projectinstance)。
instanceDuty InstanceDutySource
}
// InstanceDutySource 返回项目当前活跃实例对 taskKey 的治理结论。
// governed=false 表示未受实例治理,调用方应回退项目默认职责逻辑。
type InstanceDutySource interface {
ResolveGovernedDuty(ctx context.Context, projectID int64, taskKey string) (author, reviewer, approver int64, dutyPolicy string, governed bool, err error)
}
// SetInstanceDutySource 注入实例职责解析源(组合根可选装配)。
func (s *Service) SetInstanceDutySource(src InstanceDutySource) {
if s == nil {
return
}
s.instanceDuty = src
}
// NewService 构造业务服务;生产/test 环境数据库不可用且未显式 ALLOW_IN_MEMORY 时返回 error。
func NewService(cfg *config.Config) (*Service, error) {
if cfg == nil {
return nil, errors.New("bizdev: config is required (data roots come from config, not package defaults)")
}
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.SetDutySlotWriter(docgov.DutySlotWriter{})
_ = authzStore.BackfillCreatorMembers(poolCtx)
if ver, verErr := db.CurrentVersion(poolCtx, pool); verErr == nil {
schemaVersion = ver
}
}
}
}
var casEmbedder storage.Embedder
if cfg.EmbeddingEndpointURL != "" {
casEmbedder = kb.NewEmbedder(cfg.EmbeddingEndpointURL, cfg.EmbeddingAPIKey, cfg.EmbeddingModel)
}
casStore, _ := storage.NewCAS(filepath.Join(cfg.DataDir, "storage"), pool, casEmbedder)
ctxMgr := ctxmgr.NewContextManager(pool, casStore, ctxmgr.DefaultTimeDecayConfig())
if cfg.EmbeddingEndpointURL != "" {
ctxMgr.SetEmbedder(kb.NewEmbedder(cfg.EmbeddingEndpointURL, cfg.EmbeddingAPIKey, cfg.EmbeddingModel))
}
matIndex := materialindex.NewPipeline(cfg, pool)
docGov := docgov.NewGovernanceEngine(pool)
docRev := docrevision.NewService(pool)
docRev.DataDir = cfg.DataDir
docRev.SetRootPointerProjector(docrevision.CompositeRootPointerProjector{
Controlled: func(ctx context.Context, scope uow.TxScope, root *docrevision.DocRootPointers) error {
return (docgov.Repository{}).ProjectHeadFull(ctx, scope, root.DocID,
root.LedgerVersionID, root.HeadSnapshotID, root.HeadRevisionNo, root.RevisionSchemaVer, root.ContentSHA256)
},
Freeform: func(ctx context.Context, scope uow.TxScope, root *docrevision.DocRootPointers) error {
return (freedoc.Repository{}).ProjectHead(ctx, scope, root.DocID,
root.LedgerVersionID, root.HeadSnapshotID, root.HeadRevisionNo, root.RevisionSchemaVer)
},
})
docRev.SetDeliverableStore(workspace.DirDeliverable{})
freeDoc := freedoc.NewService(pool, ctxMgr)
freeDoc.SetDocRevision(docRev)
pwStore := projectworkflow.NewStore(pool)
wfStore := workflowdef.NewStore(pool)
if pool != nil && workflowdef.TablesReady(context.Background(), pool) {
if seedErr := workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1); seedErr != nil {
slog.Warn("workflow seed bootstrap failed", "error", seedErr)
}
}
flatCat := workflowdef.NewCatalog(context.Background(), wfStore, newDefaultFlatCatalog())
pwSvc := projectworkflow.NewService(pwStore, adaptFlatCatalog(flatCat))
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,
materialIndex: matIndex,
docGov: docGov,
docRev: docRev,
freeDoc: freeDoc,
projectWorkflow: pwSvc,
pool: pool,
schemaVersion: schemaVersion,
}
svc.parts = Parts{
Pool: pool,
Gov: docGov,
Rev: docRev,
Ctx: ctxMgr,
CAS: casStore,
MaterialIndex: matIndex,
PW: pwSvc,
FreeDoc: freeDoc,
KB: svc.kb,
}
// 注册终审批准回调:自动切片向量化沉淀为往期标杆案例
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, workspace.DeliverableKey),
filepath.Join(cfg.DataDir, doc.WorkDir, workspace.DeliverableKey),
filepath.Join(cfg.DataDir, workspace.WorkspacesDirName, doc.WorkDir, workspace.DeliverableKey),
}
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 {
// 若未在默认工作区直接发现 deliverable,尝试深度探测当前数据目录下的工作区文件
pattern := filepath.Join(cfg.DataDir, workspace.WorkspacesDirName, "**", doc.TaskKey, workspace.DeliverableKey)
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
pattern2 := filepath.Join(cfg.DataDir, "**", doc.TaskKey, workspace.DeliverableKey)
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 && workflowdef.SameTaskKey(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
}
// AgentOutputMaterializer is the production materialize path. Runtime injects
// *docflow.Commands so candidate-ready/commit envelopes share the unique UoW.
type AgentOutputMaterializer interface {
MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error)
}
func (s *Service) SetAgentMaterializer(m AgentOutputMaterializer) {
if s != nil {
s.materializer = m
}
}
func (s *Service) ListTasks() []TaskMeta {
return s.squad.ListTasks()
}
// resolveDocStage 解析写入文档账本的 Stage 字段。
// 优先请求透传的冻结实例阶段键;否则按 main 三阶段任务映射;再回落 V0.5_feasibility。
func (s *Service) ListStages() []StageMeta {
return SupportedStages
}
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)
}