From 3f42149457a5fc442a859c96145dc680c76ffbbd Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 13:00:33 +0000 Subject: [PATCH 1/3] feat(bizdev): snapshot collaborators onto Runtime and Server ports Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 7 +- .../2026-09-15-bizdev-locator-removal.md | 45 +++++++++++ .doc/system-design/module-boundaries.yaml | 7 ++ cmd/bizdev-cli/main.go | 48 +++++------ cmd/recategorize/main.go | 2 +- internal/app/app.go | 52 ++++++++---- internal/app/stageflow/usecase_test.go | 1 - internal/bizdev/activity_outcome_test.go | 8 +- internal/bizdev/blended_context_test.go | 6 +- internal/bizdev/case_recommend_e2e_test.go | 18 ++--- internal/bizdev/doc_id_test.go | 14 ++-- internal/bizdev/docgov_bizdev_test.go | 22 ++--- internal/bizdev/full_link_e2e_test.go | 28 +++---- internal/bizdev/materials_test.go | 2 +- internal/bizdev/parts.go | 35 ++++++++ internal/bizdev/quality_projection_test.go | 14 ++-- internal/bizdev/service.go | 11 +++ internal/bizdev/stage_pending_stop_test.go | 6 +- internal/bizdev/stage_quality_stop_test.go | 2 +- internal/bizdev/test_manager_flow_test.go | 10 +-- internal/chat/action_executor_test.go | 2 +- internal/chat/longtask_quality_e2e_test.go | 2 +- internal/chat/session_test.go | 6 +- internal/chat/session_turn_intent_test.go | 2 +- internal/server/asset_handlers.go | 4 +- internal/server/auth_helpers.go | 4 +- internal/server/chat_document_reader.go | 4 +- internal/server/chat_history_api_test.go | 8 +- internal/server/chat_history_auth.go | 12 +-- .../server/chat_history_refresh_pg_test.go | 3 +- internal/server/chat_recategorize_api.go | 4 +- internal/server/chat_recategorize_api_test.go | 2 +- internal/server/chat_restore_test.go | 4 +- .../server/chat_search_and_digest_api_test.go | 2 +- internal/server/chat_turn_api.go | 2 +- .../server/chat_unread_and_export_api_test.go | 2 +- internal/server/chat_window_api.go | 4 +- internal/server/document_revision_access.go | 10 +-- .../server/document_revision_authz_test.go | 36 ++++----- .../document_revision_duty_write_test.go | 2 +- internal/server/document_revision_handlers.go | 22 ++--- .../document_revision_patch_order_test.go | 30 +++---- .../server/e2e_workflow_governance_test.go | 3 +- internal/server/freeform_doc_api.go | 32 ++++---- internal/server/freeform_doc_api_test.go | 2 +- internal/server/handlers.go | 80 +++++++++---------- internal/server/lease_concurrency_test.go | 4 +- .../server/phase5_fault_injection_pg_test.go | 2 +- .../server/phase5_finalize_fault_pg_test.go | 2 +- internal/server/project_doc_handlers.go | 12 +-- internal/server/project_handlers_test.go | 10 +-- internal/server/project_members_handlers.go | 2 +- internal/server/project_workflow_api_test.go | 10 +-- internal/server/reply_export_api.go | 8 +- internal/server/reply_export_api_test.go | 2 +- internal/server/server.go | 31 +++++-- internal/server/server_test.go | 4 +- internal/server/stage_access_helpers.go | 4 +- internal/server/task_finalize.go | 4 +- internal/server/task_finalize_test.go | 10 +-- .../server/task_run_busy_preflight_test.go | 12 +-- internal/server/task_run_handlers.go | 8 +- .../server/task_run_idempotent_sse_test.go | 10 +-- .../server/task_run_persistence_pg_test.go | 2 +- internal/server/workflow_approval_hook.go | 4 +- .../server/workflow_governance_pg_test.go | 4 +- .../workflow_governance_piagent_pg_test.go | 4 +- scripts/backfill_task_run_projection.go | 2 +- 68 files changed, 458 insertions(+), 315 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-15-bizdev-locator-removal.md create mode 100644 internal/bizdev/parts.go diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 8bb9875a..1c2dc0a8 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,7 +5,7 @@ ## 焦点 -1. **TRACK-S**: [D5 stageflow](./short-term/2026-09-15-stageflow-type-ownership.md)(D6/D7a 已合入。D4 getter 仍开放) +1. **TRACK-S**: [D4 locator 移除](./short-term/2026-09-15-bizdev-locator-removal.md)(D5/D6/D7a 已合入;本波删 8 个 getter) 2. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针) 3. **CHAT**: [对话历史刷新活验已补](../test-reports/2026-09-13-chat-history-refresh-test-report.md)(项目 6252 闲聊刷新同集 + guest 403;编制任务链仍开放) @@ -66,7 +66,8 @@ | Cursor | event-e2-hook-guard | fix/event-e2-hook-guard | - | events | done | | Cursor | doccontent-canonical-boundary | fix/doccontent-canonical-boundary | - | docgov | done | | Cursor | intent-bizdev-boundary | fix/intent-bizdev-boundary | - | intent | done | -| Cursor | stageflow-type-ownership | fix/stageflow-type-ownership | - | stageflow | in_progress | +| Cursor | stageflow-type-ownership | fix/stageflow-type-ownership | - | stageflow | done | +| Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | in_progress | > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 @@ -78,7 +79,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| Cursor | merging | 2026-09-15 | D5 stageflow types;mode=off | - | +| Cursor | merging | 2026-09-15 | D4 bizdev locator removal;mode=off | - | **phase**: `idle` | `merging` | `smoke` | `e2e` diff --git a/.doc/ops/short-term/2026-09-15-bizdev-locator-removal.md b/.doc/ops/short-term/2026-09-15-bizdev-locator-removal.md new file mode 100644 index 00000000..f832924e --- /dev/null +++ b/.doc/ops/short-term/2026-09-15-bizdev-locator-removal.md @@ -0,0 +1,45 @@ +--- +title: "D4 bizdev locator 最终移除" +status: in_progress +created: 2026-09-15 +updated: 2026-09-15 +owner: Cursor +branch: fix/bizdev-locator-removal +worktree: "-" +mutex: bizdev +--- + +# D4 bizdev locator 最终移除 + +## 目标 + +删除 bizdev 上 8 个 locator getter;组合根一次性 Snapshot 后由 Runtime/Server 持有端口。 + +## 范围 + +- `Pool` `DocGov` `DocRevision` `ContextManager` `CAS` `ProjectWorkflow` `FreeformDoc` `KB` +- Runtime / Server 字段 +- archtest 禁止把 getter 加回去 +- 调用方迁移 + +## 非目标 + +- 不删 SchemaVersion / DatabaseReady +- 不切事件 live +- 不实现 Track P + +## 步骤 + +1. [ ] Snapshot + Runtime 端口 +2. [ ] 迁移 server/cmd/tests +3. [ ] 同批删除 8 getter +4. [ ] 报告合入 + +## 验收标准 + +- [ ] 生产无 8 个 getter +- [ ] [S1 报告](../../test-reports/2026-09-15-bizdev-locator-removal-test-report.md) + +## 相关链接 + +- Phase 2 D4 diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index 60a78a54..1a25ace9 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -73,13 +73,18 @@ imports: - bizdev - config - coordinator + - ctxmgr - docgov + - docrevision - events + - freedoc - intent + - kb - notification - observability - projectinstance - projectworkflow + - storage - taskrun - workflowdef - workspace @@ -169,11 +174,13 @@ imports: - events - freedoc - intent + - kb - notification - observability - projectinstance - projectworkflow - replyexport + - storage - squad - stream - stream/transport diff --git a/cmd/bizdev-cli/main.go b/cmd/bizdev-cli/main.go index b37ba3ea..30e04ffe 100644 --- a/cmd/bizdev-cli/main.go +++ b/cmd/bizdev-cli/main.go @@ -330,7 +330,7 @@ func main() { func handleStageRun( ctx context.Context, - svc bizdev.BizdevService, + svc *bizdev.Service, stageName string, project, customer, device, use, market, backend string, autoRepair bool, @@ -455,7 +455,7 @@ func printAdvisorBrief(brief *bizdev.AdvisorBriefing) { fmt.Println("==================================================================") } -func handleInspect(ctx context.Context, svc bizdev.BizdevService, workDir string, addStepFlag string, backend string) { +func handleInspect(ctx context.Context, svc *bizdev.Service, workDir string, addStepFlag string, backend string) { state, err := svc.InspectWorkspace(ctx, workDir) if err != nil { fmt.Fprintf(os.Stderr, "查看工作区失败: %v\n", err) @@ -586,7 +586,7 @@ func createCLIStreamListener(verbose bool) stream.Listener { func handleChatMode( ctx context.Context, - svc bizdev.BizdevService, + svc *bizdev.Service, chatQuery string, workDir string, project string, @@ -664,7 +664,7 @@ func handleChatMode( rec := intent.NewRecognizer() engine := coordinator.NewCoordinatorEngine(stageflow.New(svc)) session := chat.NewSession("cli_chat_sess", userCtx, rec, engine) - session.CtxMgr = svc.ContextManager() + session.CtxMgr = bizdev.Snapshot(svc).Ctx // 单次提问模式 (支持脚本与自动化测试) if chatQuery != "" { @@ -805,8 +805,8 @@ func handleChatMode( // 标杆案例独立检索 CLI 处理函数 // ----------------------------------------------------------------------------- -func handleRecommendCases(ctx context.Context, svc bizdev.BizdevService, query string) { - ctxMgr := svc.ContextManager() +func handleRecommendCases(ctx context.Context, svc *bizdev.Service, query string) { + ctxMgr := bizdev.Snapshot(svc).Ctx if ctxMgr == nil { fmt.Fprintf(os.Stderr, "错误: 上下文管理器未初始化\n") os.Exit(1) @@ -851,8 +851,8 @@ func handleRecommendCases(ctx context.Context, svc bizdev.BizdevService, query s // 受控文档治理 CLI 处理函数集 // ----------------------------------------------------------------------------- -func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string) { - doc, err := svc.DocGov().GetDocument(ctx, docID) +func handleDocStatus(ctx context.Context, svc *bizdev.Service, docID string) { + doc, err := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID) if err != nil { fmt.Fprintf(os.Stderr, "查询受控文档失败 [%s]: %v\n", docID, err) os.Exit(1) @@ -879,7 +879,7 @@ func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string } // 打印批注 - comments, _ := svc.DocGov().ListComments(ctx, docID) + comments, _ := bizdev.Snapshot(svc).Gov.ListComments(ctx, docID) if len(comments) > 0 { fmt.Printf("\n批注与修改意见 (%d 条):\n", len(comments)) for idx, c := range comments { @@ -888,7 +888,7 @@ func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string } // 打印审计记录 - audits, _ := svc.DocGov().ListAuditLogs(ctx, docID) + audits, _ := bizdev.Snapshot(svc).Gov.ListAuditLogs(ctx, docID) if len(audits) > 0 { fmt.Printf("\n全生命周期审计轨迹 (%d 条):\n", len(audits)) for idx, a := range audits { @@ -899,9 +899,9 @@ func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string fmt.Println("==================================================================") } -func handleDocClaim(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) { +func handleDocClaim(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) { role := docgov.UserRole(userRoleStr) - doc, err := svc.DocGov().RecordDocumentViewed(ctx, docID, userID, role, userName) + doc, err := bizdev.Snapshot(svc).Gov.RecordDocumentViewed(ctx, docID, userID, role, userName) if err != nil { fmt.Fprintf(os.Stderr, "认领/查看文档失败: %v\n", err) os.Exit(1) @@ -917,7 +917,7 @@ func handleDocClaim(ctx context.Context, svc bizdev.BizdevService, docID string, } } -func handleDecideProposal(ctx context.Context, svc bizdev.BizdevService, param string, authorID int64) { +func handleDecideProposal(ctx context.Context, svc *bizdev.Service, param string, authorID int64) { // 格式: proposal_id:agree/reject:理由 parts := strings.SplitN(param, ":", 3) if len(parts) < 2 { @@ -932,7 +932,7 @@ func handleDecideProposal(ctx context.Context, svc bizdev.BizdevService, param s } approve := decision == "agree" || decision == "approve" || decision == "yes" || decision == "true" - prop, err := svc.DocGov().DecideModificationProposal(ctx, proposalID, authorID, approve, reason) + prop, err := bizdev.Snapshot(svc).Gov.DecideModificationProposal(ctx, proposalID, authorID, approve, reason) if err != nil { fmt.Fprintf(os.Stderr, "拍板决策变更提案失败: %v\n", err) os.Exit(1) @@ -946,8 +946,8 @@ func handleDecideProposal(ctx context.Context, svc bizdev.BizdevService, param s } } -func handleDocSubmitReview(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) { - doc, err := svc.DocGov().SubmitForReview(ctx, docID, userID, docgov.RoleAuthor, userName) +func handleDocSubmitReview(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) { + doc, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, userID, docgov.RoleAuthor, userName) if err != nil { fmt.Fprintf(os.Stderr, "提交审核失败: %v\n", err) os.Exit(1) @@ -955,8 +955,8 @@ func handleDocSubmitReview(ctx context.Context, svc bizdev.BizdevService, docID fmt.Printf("✓ 文档 [%s] 编制完成并成功送审!当前状态: %s,文档已进入受控锁定。\n", doc.DocID, doc.CurrentState) } -func handleDocReviewApprove(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) { - doc, err := svc.DocGov().ReviewApprove(ctx, docID, userID, docgov.RoleReviewer, userName, "审核无误,同意提交批准") +func handleDocReviewApprove(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) { + doc, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, userID, docgov.RoleReviewer, userName, "审核无误,同意提交批准") if err != nil { fmt.Fprintf(os.Stderr, "审核通过失败: %v\n", err) os.Exit(1) @@ -964,7 +964,7 @@ func handleDocReviewApprove(ctx context.Context, svc bizdev.BizdevService, docID fmt.Printf("✓ 审核人 %s 审核通过!文档 [%s] 流转至批准阶段: %s\n", userName, doc.DocID, doc.CurrentState) } -func handleDocReviewReject(ctx context.Context, svc bizdev.BizdevService, param string, userID int64, userRoleStr string, userName string) { +func handleDocReviewReject(ctx context.Context, svc *bizdev.Service, param string, userID int64, userRoleStr string, userName string) { parts := strings.SplitN(param, ":", 2) if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" { fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n") @@ -973,7 +973,7 @@ func handleDocReviewReject(ctx context.Context, svc bizdev.BizdevService, param docID := strings.TrimSpace(parts[0]) reason := strings.TrimSpace(parts[1]) - doc, err := svc.DocGov().ReviewReject(ctx, docID, userID, docgov.RoleReviewer, userName, reason) + doc, err := bizdev.Snapshot(svc).Gov.ReviewReject(ctx, docID, userID, docgov.RoleReviewer, userName, reason) if err != nil { fmt.Fprintf(os.Stderr, "审核驳回失败: %v\n", err) os.Exit(1) @@ -982,8 +982,8 @@ func handleDocReviewReject(ctx context.Context, svc bizdev.BizdevService, param userName, doc.DocID, doc.CurrentState, reason) } -func handleDocApproverApprove(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) { - doc, err := svc.DocGov().ApproverApprove(ctx, docID, userID, docgov.RoleApprover, userName, "评估完备,批准受控发布") +func handleDocApproverApprove(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) { + doc, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, userID, docgov.RoleApprover, userName, "评估完备,批准受控发布") if err != nil { fmt.Fprintf(os.Stderr, "批准失败: %v\n", err) os.Exit(1) @@ -991,7 +991,7 @@ func handleDocApproverApprove(ctx context.Context, svc bizdev.BizdevService, doc fmt.Printf("🎉 批准负责人 %s 最终批准文档 [%s]!当前状态: %s,受控终态归档锁定。\n", userName, doc.DocID, doc.CurrentState) } -func handleDocApproverReject(ctx context.Context, svc bizdev.BizdevService, param string, userID int64, userRoleStr string, userName string) { +func handleDocApproverReject(ctx context.Context, svc *bizdev.Service, param string, userID int64, userRoleStr string, userName string) { parts := strings.SplitN(param, ":", 2) if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" { fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n") @@ -1000,7 +1000,7 @@ func handleDocApproverReject(ctx context.Context, svc bizdev.BizdevService, para docID := strings.TrimSpace(parts[0]) reason := strings.TrimSpace(parts[1]) - doc, err := svc.DocGov().ApproverReject(ctx, docID, userID, docgov.RoleApprover, userName, reason, docgov.StateDrafting) + doc, err := bizdev.Snapshot(svc).Gov.ApproverReject(ctx, docID, userID, docgov.RoleApprover, userName, reason, docgov.StateDrafting) if err != nil { fmt.Fprintf(os.Stderr, "批准驳回失败: %v\n", err) os.Exit(1) diff --git a/cmd/recategorize/main.go b/cmd/recategorize/main.go index c4a6a657..690e2429 100644 --- a/cmd/recategorize/main.go +++ b/cmd/recategorize/main.go @@ -27,7 +27,7 @@ func main() { fmt.Fprintf(os.Stderr, "NewService: %v\n", err) os.Exit(1) } - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx if ctxMgr == nil { fmt.Fprintf(os.Stderr, "ContextManager 未初始化\n") os.Exit(1) diff --git a/internal/app/app.go b/internal/app/app.go index c8058b16..6ee92ef7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,16 +19,23 @@ import ( "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/coordinator" + "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" + "chat-agent-system/internal/docrevision" "chat-agent-system/internal/events" + "chat-agent-system/internal/freedoc" "chat-agent-system/internal/intent" + "chat-agent-system/internal/kb" "chat-agent-system/internal/notification" "chat-agent-system/internal/observability" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" + "chat-agent-system/internal/storage" "chat-agent-system/internal/taskrun" "chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workspace" + + "github.com/jackc/pgx/v5/pgxpool" ) const defaultReminderPollInterval = 15 * time.Minute @@ -56,6 +63,13 @@ type Runtime struct { ChatLog chatflow.ChatLog Assembler chatflow.ContextAssembler Docflow *docflow.Commands + Pool *pgxpool.Pool + Gov *docgov.GovernanceEngine + Rev *docrevision.Service + Ctx *ctxmgr.ContextManager + CAS *storage.CAS + FreeDoc *freedoc.Service + KB *kb.Client mu sync.Mutex running bool @@ -89,35 +103,36 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) { rec := intent.NewRecognizer() dialogueBackend := agent.NewBackend(cfg.AgentBackend, cfg) coordEngine := coordinator.NewCoordinatorEngineWithDialogue(nil, dialogueBackend, cfg.DataDir) - pool := svc.Pool() + parts := bizdev.Snapshot(svc) + pool := parts.Pool stores, storeErr := auth.NewStores(context.Background(), pool, cfg) if storeErr != nil { return nil, fmt.Errorf("auth store: %w", storeErr) } authSvc := auth.NewService(stores.Auth, stores.Sessions, cfg) - pwSvc := svc.ProjectWorkflow() + pwSvc := parts.PW if pwSvc == nil { pwSvc = projectworkflow.NewService(projectworkflow.NewStore(nil), nil) } authzSvc := authz.NewService(pool) - wfDefSvc := workflowdef.NewService(pool, svc.CAS()) + wfDefSvc := workflowdef.NewService(pool, parts.CAS) instStore := projectinstance.NewStore(pool) var instSvc *projectinstance.Service if wfDefSvc.Available() { - docChecker := projectinstance.NewDocGovChecker(svc.DocGov()) + docChecker := projectinstance.NewDocGovChecker(parts.Gov) instSvc = projectinstance.NewService(instStore, wfDefSvc.Store(), pwSvc.Store(), docChecker) if pool != nil { instSvc.SetActiveMemberLookup(projectinstance.NewPGMemberLookup(pool)) } } - taskMgr := taskrun.NewManager(pwSvc, svc.ContextManager()) + taskMgr := taskrun.NewManager(pwSvc, parts.Ctx) if pool != nil { taskMgr.SetStore(taskrun.NewPGStore(pool)) } wsCap := workspace.NewCapability(cfg.DataDir, workspace.NewLeaseTable()) cmds := &taskrun.Commands{ - Ctx: svc.ContextManager(), + Ctx: parts.Ctx, PW: pwSvc, Env: cfg.Env, StorageIdentity: "env:" + cfg.Env, @@ -130,7 +145,7 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) { stages := stageflow.New(svc) coordEngine.SetStageTaskRunner(stages) - coordEngine.SetContextServices(svc.ContextManager(), svc.KB()) + coordEngine.SetContextServices(parts.Ctx, parts.KB) if instSvc != nil && instSvc.Available() { coordEngine.SetInstanceService(instSvc) } @@ -152,8 +167,15 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) { Commands: cmds, Rec: rec, Stageflow: stages, + Pool: pool, + Gov: parts.Gov, + Rev: parts.Rev, + Ctx: parts.Ctx, + CAS: parts.CAS, + FreeDoc: parts.FreeDoc, + KB: parts.KB, } - rt.DocNotify = &docnotify.Subscriber{Notify: rt.Notify, Docs: svc.DocGov()} + rt.DocNotify = &docnotify.Subscriber{Notify: rt.Notify, Docs: parts.Gov} rt.DocNotify.SkipWrite = func(ctx context.Context) bool { return rt.WriterLive(ctx, events.ConsumerNotify) } @@ -162,7 +184,7 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) { envName = cfg.Env } rt.Docflow = &docflow.Commands{ - Gov: svc.DocGov(), Rev: svc.DocRevision(), Auth: docflow.DutyAuthorizer(), + Gov: parts.Gov, Rev: parts.Rev, Auth: docflow.DutyAuthorizer(), Environment: envName, } if pool != nil { @@ -179,8 +201,8 @@ func (r *Runtime) bindDocumentNotify() { if r == nil || r.notifyBound { return } - if r.Biz != nil && r.Biz.DocRevision() != nil { - if docnotify.Bind(r.Biz.DocRevision(), r.DocNotify) { + if r.Biz != nil && r.Rev != nil { + if docnotify.Bind(r.Rev, r.DocNotify) { r.subscribers++ } } @@ -188,25 +210,25 @@ func (r *Runtime) bindDocumentNotify() { } func (r *Runtime) bindWorkflowApprovalHook() { - if r == nil || r.hookBound || r.Biz == nil || r.Biz.DocGov() == nil || r.Inst == nil || !r.Inst.Available() { + if r == nil || r.hookBound || r.Gov == nil || r.Inst == nil || !r.Inst.Available() { r.hookBound = true return } - r.Biz.DocGov().AddOnApprovedHook(r.onWorkflowDocumentApproved) + r.Gov.AddOnApprovedHook(r.onWorkflowDocumentApproved) r.hookBound = true r.subscribers++ } // WriterLive reports whether the durable consumer already owns the visible write. func (r *Runtime) WriterLive(ctx context.Context, consumer string) bool { - if r == nil || r.Biz == nil { + if r == nil { return false } env := "" if r.Cfg != nil { env = r.Cfg.Env } - return events.WriterLive(ctx, r.Biz.Pool(), env, consumer) + return events.WriterLive(ctx, r.Pool, env, consumer) } func (r *Runtime) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) { diff --git a/internal/app/stageflow/usecase_test.go b/internal/app/stageflow/usecase_test.go index 28e4d20a..e5556a09 100644 --- a/internal/app/stageflow/usecase_test.go +++ b/internal/app/stageflow/usecase_test.go @@ -32,7 +32,6 @@ func TestUseCaseTypeIdentity(t *testing.T) { if TaskStatusFailed != "failed" || TaskStatusPendingAuthorConfirmation != "pending_author_confirmation" { t.Fatalf("status tokens drifted") } -} biz := &stubBiz{} httpUC := New(biz) cliUC := New(biz) diff --git a/internal/bizdev/activity_outcome_test.go b/internal/bizdev/activity_outcome_test.go index 1619f65a..7b06f561 100644 --- a/internal/bizdev/activity_outcome_test.go +++ b/internal/bizdev/activity_outcome_test.go @@ -14,7 +14,7 @@ import ( func activityTitles(t *testing.T, svc *Service, projectID int64) []string { t.Helper() - pCtx, err := svc.ContextManager().GetProjectContext(context.Background(), projectID) + pCtx, err := svc.ctxMgr.GetProjectContext(context.Background(), projectID) if err != nil { t.Fatalf("GetProjectContext: %v", err) } @@ -28,7 +28,7 @@ func activityTitles(t *testing.T, svc *Service, projectID int64) []string { func TestExecuteTask_MaterializeFailure_DoesNotRecordCompletedActivity(t *testing.T) { svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) ctx := context.Background() - svc.DocRevision().AgentCommit = false + svc.docRev.AgentCommit = false projectID := int64(5601) res, err := svc.ExecuteTask(ctx, TaskRunRequest{ @@ -60,10 +60,10 @@ func TestExecuteTask_PendingConfirmation_RecordsAwaitingNotCompleted(t *testing. t.Fatal(err) } base := "# base\n\n法规引用 GB9706\n" - if _, err := svc.DocRevision().OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "t"}, projectID, workDir); err != nil { + if _, err := svc.docRev.OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "t"}, projectID, workDir); err != nil { t.Fatal(err) } - if _, err := svc.DocGov().CreateOrGetDocument(ctx, docID, projectID, "research", "调研报告", "V0.5", workDir, 1, "a", 2, "r", 3, "p"); err != nil { + if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "research", "调研报告", "V0.5", workDir, 1, "a", 2, "r", 3, "p"); err != nil { t.Fatal(err) } diff --git a/internal/bizdev/blended_context_test.go b/internal/bizdev/blended_context_test.go index 33aa747b..9cf18a79 100644 --- a/internal/bizdev/blended_context_test.go +++ b/internal/bizdev/blended_context_test.go @@ -40,10 +40,10 @@ func TestService_BlendedContextInjection(t *testing.T) { FocusAreas: []string{"cost_budget", "timeline"}, OutputFormatPref: "ppt_bullets", } - _ = svc.ContextManager().UpdateUserPreference(ctx, pref) + _ = svc.ctxMgr.UpdateUserPreference(ctx, pref) // 2. 记录先验里程碑决策 - _, _ = svc.ContextManager().RecordActivity(ctx, projectID, userID, "项目总监", "decision", "首期控制在50万预算内并优先过型检", nil, 1.0, true) + _, _ = svc.ctxMgr.RecordActivity(ctx, projectID, userID, "项目总监", "decision", "首期控制在50万预算内并优先过型检", nil, 1.0, true) // 3. 执行任务 (使用 stub 后端进行快速逻辑验证) res, err := svc.ExecuteTask(ctx, TaskRunRequest{ @@ -89,7 +89,7 @@ func TestService_BlendedContextInjection(t *testing.T) { } // 6. 验证任务完成后自动记录了里程碑活动 - pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) + pCtx, err := svc.ctxMgr.GetProjectContext(ctx, projectID) if err != nil { t.Fatalf("get project context failed: %v", err) } diff --git a/internal/bizdev/case_recommend_e2e_test.go b/internal/bizdev/case_recommend_e2e_test.go index 07ec6ed7..cd2c3e9b 100644 --- a/internal/bizdev/case_recommend_e2e_test.go +++ b/internal/bizdev/case_recommend_e2e_test.go @@ -47,7 +47,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte(histOutput), 0644) - _, err := svc.DocGov().CreateOrGetDocument( + _, err := bizdev.Snapshot(svc).Gov.CreateOrGetDocument( ctx, docID, 99, "feasibility_reg", "4K超高清荧光内窥镜系统立项与可行性报告", "V0.5_feasibility", workDir, 101, "张工", 201, "李审核", 301, "王总监", @@ -57,12 +57,12 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { } // 模拟流转至 approved 终态 - docRecord, _ := svc.DocGov().GetDocument(ctx, docID) + docRecord, _ := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID) docRecord.IsTemplateOnly = false docRecord.WorkDir = workDir - _, _ = svc.DocGov().SubmitForReview(ctx, docID, 101, docgov.RoleAuthor, "张工") - _, _ = svc.DocGov().ReviewApprove(ctx, docID, 201, docgov.RoleReviewer, "李审核", "无误") - docApproved, err := svc.DocGov().ApproverApprove(ctx, docID, 301, docgov.RoleApprover, "王总监", "批准生效") + _, _ = bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, 101, docgov.RoleAuthor, "张工") + _, _ = bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, 201, docgov.RoleReviewer, "李审核", "无误") + docApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, 301, docgov.RoleApprover, "王总监", "批准生效") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } @@ -71,7 +71,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { } // 主动索引备用保底 - chunkIDs, err := svc.ContextManager().IndexApprovedDocument(ctx, docApproved, histOutput) + chunkIDs, err := bizdev.Snapshot(svc).Ctx.IndexApprovedDocument(ctx, docApproved, histOutput) if err != nil { t.Fatalf("IndexApprovedDocument manual failed: %v", err) } @@ -81,7 +81,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { // 校验自动切片与向量化是否触发入库 time.Sleep(50 * time.Millisecond) - cases, err := svc.ContextManager().SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ + cases, err := bizdev.Snapshot(svc).Ctx.SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ Query: "4K超高清荧光内窥镜系统立项与可行性报告", Limit: 3, MinScore: 0.10, @@ -101,7 +101,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { rec := intent.NewRecognizer() engine := coordinator.NewCoordinatorEngine(svc) - engine.SetContextServices(svc.ContextManager(), svc.KB()) + engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB) userCtx := &coordinator.UserProjectContext{ SnapshotID: "ctx_new_consult", @@ -127,7 +127,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) { } session := chat.NewSession("sess_consult_e2e", userCtx, rec, engine) - session.CtxMgr = svc.ContextManager() + session.CtxMgr = bizdev.Snapshot(svc).Ctx chatQuery := "我们之前做过类似的硬管内窥镜摄像系统吗?型检和报价大概怎么定的?" resp, err := session.SendMessage(ctx, chatQuery) diff --git a/internal/bizdev/doc_id_test.go b/internal/bizdev/doc_id_test.go index f6e53f2e..9dc2565c 100644 --- a/internal/bizdev/doc_id_test.go +++ b/internal/bizdev/doc_id_test.go @@ -54,7 +54,7 @@ func TestResolveAndValidateDocID_ExistingDocMismatchRejected(t *testing.T) { ctx := context.Background() docID := "doc_proj56_research" - _, err := svc.DocGov().CreateOrGetDocument( + _, err := svc.docGov.CreateOrGetDocument( ctx, docID, 56, "research", "调研报告", "V0.5", "", 1, "张工", 2, "李主管", 3, "王总监", ) @@ -87,7 +87,7 @@ func TestResolveAndValidateDocID_ClientDocIDCannotOverrideTaskKey(t *testing.T) ctx := context.Background() docID := "doc_proj6109_intake" - _, err := svc.DocGov().CreateOrGetDocument( + _, err := svc.docGov.CreateOrGetDocument( ctx, docID, 6109, "intake", "用户初始需求收集表", "V0.1", "", 1, "张工", 2, "李主管", 3, "王总监", ) @@ -122,7 +122,7 @@ func TestResolveAndValidateDocID_ExistingDocAccepted(t *testing.T) { ctx := context.Background() docID := "doc_proj56_research" - _, err := svc.DocGov().CreateOrGetDocument( + _, err := svc.docGov.CreateOrGetDocument( ctx, docID, 56, "research", "调研报告", "V0.5", "", 1, "张工", 2, "李主管", 3, "王总监", ) @@ -154,7 +154,7 @@ func TestCreateOrGetDocument_ConcurrentSameDocID(t *testing.T) { } ch := make(chan outcome, 2) create := func() { - doc, err := svc.DocGov().CreateOrGetDocument( + doc, err := svc.docGov.CreateOrGetDocument( ctx, docID, 77, "research", "调研报告", "V0.5", "", 1, "张工", 2, "李主管", 3, "王总监", ) @@ -177,7 +177,7 @@ func TestCreateOrGetDocument_ConcurrentSameDocID(t *testing.T) { if first.id != docID || second.id != docID { t.Fatalf("doc ids mismatch: first=%q second=%q want %q", first.id, second.id, docID) } - doc, err := svc.DocGov().GetDocument(ctx, docID) + doc, err := svc.docGov.GetDocument(ctx, docID) if err != nil || doc == nil { t.Fatalf("expected single persisted doc %s, got err=%v doc=%v", docID, err, doc) } @@ -262,7 +262,7 @@ func TestExecuteTask_EmptyDocIDSkipsDocRevision(t *testing.T) { if res.DocRecord != nil { t.Fatalf("empty docID must not create a document record, got %#v", res.DocRecord) } - if _, stErr := svc.DocRevision().GetRevisionState(ctx, ""); !strings.Contains(stErr.Error(), "invalid doc_id") { + if _, stErr := svc.docRev.GetRevisionState(ctx, ""); !strings.Contains(stErr.Error(), "invalid doc_id") { t.Fatalf("empty GetRevisionState should be invalid, got %v", stErr) } } @@ -270,7 +270,7 @@ func TestExecuteTask_EmptyDocIDSkipsDocRevision(t *testing.T) { func TestExecuteTask_MaterializeFailureSurfacesInTaskRunResult(t *testing.T) { svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) ctx := context.Background() - svc.DocRevision().AgentCommit = false // force MaterializeAgentOutput to fail after squad success + svc.docRev.AgentCommit = false // force MaterializeAgentOutput to fail after squad success res, err := svc.ExecuteTask(ctx, TaskRunRequest{ TaskKey: "research", diff --git a/internal/bizdev/docgov_bizdev_test.go b/internal/bizdev/docgov_bizdev_test.go index 803bea03..2fc318a8 100644 --- a/internal/bizdev/docgov_bizdev_test.go +++ b/internal/bizdev/docgov_bizdev_test.go @@ -87,7 +87,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { // 阶段 3:编制负责人查看已生成的正式交付物 // 预期:触发认领机制,author_claimed 变为 true! // ------------------------------------------------------------------------- - docAfterView, err := svc.DocGov().RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) + docAfterView, err := bizdev.Snapshot(svc).Gov.RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } @@ -122,7 +122,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { // 预期:提案变为 approved_by_author,并放行修改 // ------------------------------------------------------------------------- propID := res3.PendingProposal.ProposalID - _, err = svc.DocGov().DecideModificationProposal(ctx, propID, authorID, true, "经论证追加CE市场符合出海战略,同意更新注册可行性报告") + _, err = bizdev.Snapshot(svc).Gov.DecideModificationProposal(ctx, propID, authorID, true, "经论证追加CE市场符合出海战略,同意更新注册可行性报告") if err != nil { t.Fatalf("DecideModificationProposal failed: %v", err) } @@ -141,7 +141,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { // 阶段 6:送审、审核批注、审核驳回(必填原因)退回编制草稿、重新送审通过 // ------------------------------------------------------------------------- // 6.1 编制人送审 - docDraft, err := svc.DocGov().SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) + docDraft, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } @@ -150,25 +150,25 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { } // 6.2 审核人在锁定状态下严禁直接修改正文 - _, err = svc.DocGov().RecordDocumentEdited(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName) + _, err = bizdev.Snapshot(svc).Gov.RecordDocumentEdited(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName) if !errors.Is(err, docgov.ErrDocumentLocked) { t.Fatalf("reviewer modifying locked document should return ErrDocumentLocked, got: %v", err) } // 6.3 审核人添加修改建议 - _, err = svc.DocGov().AddComment(ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, "## 临床评价路径", docgov.CommentSuggestion, "请增加与同类同品种比对器械的产品注册证号") + _, err = bizdev.Snapshot(svc).Gov.AddComment(ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, "## 临床评价路径", docgov.CommentSuggestion, "请增加与同类同品种比对器械的产品注册证号") if err != nil { t.Fatalf("AddComment failed: %v", err) } // 6.4 审核人驳回 (验证原因必填) - _, err = svc.DocGov().ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "") + _, err = bizdev.Snapshot(svc).Gov.ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "") if err != docgov.ErrRejectReasonRequired { t.Fatalf("ReviewReject without reason should fail with ErrRejectReasonRequired") } // 审核人提供原因驳回 -> 退回编制草稿并解锁 - docRejected, err := svc.DocGov().ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "同品种比对缺少实质等同临床文献支持,请补充") + docRejected, err := bizdev.Snapshot(svc).Gov.ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "同品种比对缺少实质等同临床文献支持,请补充") if err != nil { t.Fatalf("ReviewReject with reason failed: %v", err) } @@ -177,8 +177,8 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { } // 6.5 编制人修订后重新送审 - _, _ = svc.DocGov().RecordDocumentEdited(ctx, docID, authorID, docgov.RoleAuthor, authorName) - docResubmitted, err := svc.DocGov().SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) + _, _ = bizdev.Snapshot(svc).Gov.RecordDocumentEdited(ctx, docID, authorID, docgov.RoleAuthor, authorName) + docResubmitted, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("resubmit review failed: %v", err) } @@ -187,7 +187,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { } // 6.6 审核人审核通过 -> 进入 approving - docReviewApproved, err := svc.DocGov().ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "临床比对资料已补充完备,同意转交批准") + docReviewApproved, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "临床比对资料已补充完备,同意转交批准") if err != nil { t.Fatalf("ReviewApprove failed: %v", err) } @@ -198,7 +198,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { // ------------------------------------------------------------------------- // 阶段 7:批准人最终批准 -> approved 终态归档锁定 // ------------------------------------------------------------------------- - docFinalApproved, err := svc.DocGov().ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "可行性评估与合规路径严谨可靠,准予受控生效") + docFinalApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "可行性评估与合规路径严谨可靠,准予受控生效") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } diff --git a/internal/bizdev/full_link_e2e_test.go b/internal/bizdev/full_link_e2e_test.go index 4e42d9f9..717452a8 100644 --- a/internal/bizdev/full_link_e2e_test.go +++ b/internal/bizdev/full_link_e2e_test.go @@ -52,22 +52,22 @@ func executeStageDrainingPending( t.Fatalf("pending stage missing DocRecord: %+v", res) } docID := pending.DocRecord.DocID - st, err := svc.DocRevision().GetRevisionState(ctx, docID) + st, err := bizdev.Snapshot(svc).Rev.GetRevisionState(ctx, docID) if err != nil { t.Fatalf("GetRevisionState(%s): %v", docID, err) } - st2, err := svc.DocRevision().CommitAgentVersion(ctx, docID, pending.PendingAgentCandidateID, docrevision.CommitAgentDecision{ + st2, err := bizdev.Snapshot(svc).Rev.CommitAgentVersion(ctx, docID, pending.PendingAgentCandidateID, docrevision.CommitAgentDecision{ Decision: "commit", Confirmed: true, ActorUserID: authorID, ExpectedInputSHA: st.HeadSHA256, }) if err != nil { t.Fatalf("CommitAgentVersion(%s,#%d): %v", docID, pending.PendingAgentCandidateID, err) } - if err := svc.DocRevision().ProjectOutputMarkdown(docID, dataRoot); err != nil { + if err := bizdev.Snapshot(svc).Rev.ProjectOutputMarkdown(docID, dataRoot); err != nil { t.Fatalf("ProjectOutputMarkdown(%s): %v", docID, err) } if st2 != nil && st2.HeadSHA256 != "" { - if err := svc.DocGov().MarkDocumentContentUpdated(ctx, docID, st2.HeadSHA256, false); err != nil { + if err := bizdev.Snapshot(svc).Gov.MarkDocumentContentUpdated(ctx, docID, st2.HeadSHA256, false); err != nil { t.Fatalf("MarkDocumentContentUpdated: %v", err) } } @@ -149,7 +149,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { resV01.StageTitle, resV01.AdvisorBrief.MaturityHeadline, resV01.AdvisorBrief.MaturityScore) // 验证项目事实黑板已沉淀 V0.1 提取事实 - pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) + pCtx, err := bizdev.Snapshot(svc).Ctx.GetProjectContext(ctx, projectID) if err != nil || pCtx == nil { t.Fatalf("get project context failed: %v", err) } @@ -196,7 +196,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { // 验证 feasibility_reg 受控文档已建立 docID := "doc_proj1001_feasibility_reg" - docRec, err := svc.DocGov().GetDocument(ctx, docID) + docRec, err := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID) if err != nil || docRec == nil { t.Fatalf("get docgov record failed: %v", err) } @@ -213,7 +213,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Log(">>> 阶段 3: 编制负责人查阅正文触发认领 (Author Claims Document) <<<") t.Log("================================================================================") - docClaimed, err := svc.DocGov().RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) + docClaimed, err := bizdev.Snapshot(svc).Gov.RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } @@ -254,7 +254,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Log(">>> 阶段 5: 编制负责人拍板同意变更提案 (Author Approves Proposal) <<<") t.Log("================================================================================") - proposalDecided, err := svc.DocGov().DecideModificationProposal( + proposalDecided, err := bizdev.Snapshot(svc).Gov.DecideModificationProposal( ctx, propID, authorID, @@ -283,7 +283,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { } for _, art := range resV05Updated.SubArtifacts { if art.TaskKey == "feasibility_reg" { - _ = svc.DocGov().SetDocumentWorkDir(ctx, docID, art.WorkDir) + _ = bizdev.Snapshot(svc).Gov.SetDocumentWorkDir(ctx, docID, art.WorkDir) break } } @@ -293,7 +293,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Log("================================================================================") // 1. 编制人送审 - docReviewing, err := svc.DocGov().SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) + docReviewing, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } @@ -303,7 +303,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Logf("✓ 编制人提交送审: doc_state=%s, is_locked=%v", docReviewing.CurrentState, docReviewing.IsLocked) // 2. 审核人添加批注意见 - comm, err := svc.DocGov().AddComment( + comm, err := bizdev.Snapshot(svc).Gov.AddComment( ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, "## 关键性能与型检预估", docgov.CommentSuggestion, "针对荧光双波长光学滤光衰减特性,建议在送检前增加YY 0068.1硬管内窥镜机械强度摸底", @@ -314,7 +314,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Logf("✓ 审核人添加章节批注: comment_id=%d, type=%s, section=%s", comm.ID, comm.CommentType, comm.TargetSection) // 3. 审核人审核通过 - docApproving, err := svc.DocGov().ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "经审阅合规策略严密,准予进入总监批准") + docApproving, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "经审阅合规策略严密,准予进入总监批准") if err != nil { t.Fatalf("ReviewApprove failed: %v", err) } @@ -324,7 +324,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Logf("✓ 审核人审核通过: doc_state=%s, is_locked=%v", docApproving.CurrentState, docApproving.IsLocked) // 4. 批准人终态批准归档 (自动触发 onApprovedHook 切片向量化沉淀为标杆案例) - docApproved, err := svc.DocGov().ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "符合转化中心立项及CDO报价基线标准,正式批准生效") + docApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "符合转化中心立项及CDO报价基线标准,正式批准生效") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } @@ -425,7 +425,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) { t.Log(">>> 阶段 10: 验证受控批准交付物沉淀为标杆案例,并在后续跨项目咨询中自动召回 <<<") t.Log("================================================================================") - recCases, err := svc.ContextManager().SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ + recCases, err := bizdev.Snapshot(svc).Ctx.SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ Query: "4K超高清荧光硬管内窥镜摄像系统", Limit: 3, MinScore: 0.05, diff --git a/internal/bizdev/materials_test.go b/internal/bizdev/materials_test.go index 06efbe2e..ae23be92 100644 --- a/internal/bizdev/materials_test.go +++ b/internal/bizdev/materials_test.go @@ -41,7 +41,7 @@ func TestResolveMaterials_RejectEmptyBinary(t *testing.T) { func TestResolveMaterials_ProjectAssetPool(t *testing.T) { cfg := &config.Config{DataDir: t.TempDir()} svc := bizdev.MustNewService(t, cfg) - cas := svc.CAS() + cas := bizdev.Snapshot(svc).CAS ref, err := cas.StoreFile(context.Background(), 42, 1, "pool.txt", "material", []byte("pool asset body"), "text/plain") if err != nil { t.Fatalf("StoreFile failed: %v", err) diff --git a/internal/bizdev/parts.go b/internal/bizdev/parts.go new file mode 100644 index 00000000..67a83e72 --- /dev/null +++ b/internal/bizdev/parts.go @@ -0,0 +1,35 @@ +package bizdev + +import ( + "chat-agent-system/internal/ctxmgr" + "chat-agent-system/internal/docgov" + "chat-agent-system/internal/docrevision" + "chat-agent-system/internal/freedoc" + "chat-agent-system/internal/kb" + "chat-agent-system/internal/projectworkflow" + "chat-agent-system/internal/storage" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// Parts is the construction-time collaborator snapshot for app/cmd wiring. +// Domain and HTTP handlers must not call this on every request; composition +// roots copy the fields onto Runtime once. +type Parts struct { + Pool *pgxpool.Pool + Gov *docgov.GovernanceEngine + Rev *docrevision.Service + Ctx *ctxmgr.ContextManager + CAS *storage.CAS + PW *projectworkflow.Service + FreeDoc *freedoc.Service + KB *kb.Client +} + +// Snapshot returns collaborators captured when the service was constructed. +func Snapshot(s *Service) Parts { + if s == nil { + return Parts{} + } + return s.parts +} diff --git a/internal/bizdev/quality_projection_test.go b/internal/bizdev/quality_projection_test.go index fada1da2..8495a95a 100644 --- a/internal/bizdev/quality_projection_test.go +++ b/internal/bizdev/quality_projection_test.go @@ -55,18 +55,18 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te t.Fatal(err) } base := "# 正式受控 head\n\n本正文为账本事实源,质量失败后不得被未过门禁稿覆盖。\n" - if _, err := svc.DocRevision().OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "baseline"}, projectID, officialWD); err != nil { + if _, err := svc.docRev.OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "baseline"}, projectID, officialWD); err != nil { t.Fatal(err) } - docRec, err := svc.DocGov().CreateOrGetDocument(ctx, docID, projectID, "intake", "用户初始需求收集表", "V0.5", officialWD, 1, "a", 2, "r", 3, "p") + docRec, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "intake", "用户初始需求收集表", "V0.5", officialWD, 1, "a", 2, "r", 3, "p") if err != nil { t.Fatal(err) } - stBefore, err := svc.DocRevision().GetRevisionState(ctx, docID) + stBefore, err := svc.docRev.GetRevisionState(ctx, docID) if err != nil { t.Fatal(err) } - versBefore, err := svc.DocRevision().ListVersions(ctx, docID) + versBefore, err := svc.docRev.ListVersions(ctx, docID) if err != nil { t.Fatal(err) } @@ -122,7 +122,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te t.Fatal("failure summary body must remain visible") } - afterDoc, err := svc.DocGov().GetDocument(ctx, docID) + afterDoc, err := svc.docGov.GetDocument(ctx, docID) if err != nil { t.Fatal(err) } @@ -132,7 +132,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te if afterDoc.ContentSHA256 != beforeSHA { t.Fatalf("formal content hash mutated: before=%q after=%q", beforeSHA, afterDoc.ContentSHA256) } - stAfter, err := svc.DocRevision().GetRevisionState(ctx, docID) + stAfter, err := svc.docRev.GetRevisionState(ctx, docID) if err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te if stAfter.PendingCandidateID != nil { t.Fatalf("quality_failed must not create pending candidate: %v", *stAfter.PendingCandidateID) } - versAfter, err := svc.DocRevision().ListVersions(ctx, docID) + versAfter, err := svc.docRev.ListVersions(ctx, docID) if err != nil { t.Fatal(err) } diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 8f98fb60..07757016 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -46,6 +46,7 @@ type Service struct { schemaVersion int64 stageGate StageAccessGate materializer AgentOutputMaterializer + parts Parts // stageTaskHook 仅测试注入:覆盖 ExecuteStage 子任务执行,验证链中止边界。 stageTaskHook func(context.Context, TaskRunRequest) (*TaskRunResult, error) // createDocumentHook 仅测试注入:覆盖账本创建失败路径。 @@ -151,6 +152,16 @@ func NewService(cfg *config.Config) (*Service, error) { pool: pool, schemaVersion: schemaVersion, } + svc.parts = Parts{ + Pool: pool, + Gov: docGov, + Rev: docRev, + Ctx: ctxMgr, + CAS: casStore, + PW: pwSvc, + FreeDoc: freeDoc, + KB: svc.kb, + } // 注册终审批准回调:自动切片向量化沉淀为往期标杆案例 docGov.SetOnApprovedHook(func(ctx context.Context, doc *docgov.DocumentRecord) { diff --git a/internal/bizdev/stage_pending_stop_test.go b/internal/bizdev/stage_pending_stop_test.go index 867736be..aded071c 100644 --- a/internal/bizdev/stage_pending_stop_test.go +++ b/internal/bizdev/stage_pending_stop_test.go @@ -25,10 +25,10 @@ func TestExecuteStage_PendingAgentCandidate_StopsChainWithoutAbsorbingFacts(t *t } base := "# research base\n\n法规引用 GB9706\n敏感验收标准 A\n" - if _, err := svc.DocRevision().OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "t"}, projectID, researchWork); err != nil { + if _, err := svc.docRev.OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "t"}, projectID, researchWork); err != nil { t.Fatal(err) } - if _, err := svc.DocGov().CreateOrGetDocument(ctx, docID, projectID, "research", "调研报告", "V0.5", researchWork, 1, "a", 2, "r", 3, "p"); err != nil { + if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "research", "调研报告", "V0.5", researchWork, 1, "a", 2, "r", 3, "p"); err != nil { t.Fatal(err) } @@ -88,7 +88,7 @@ func TestExecuteStage_PendingAgentCandidate_StopsChainWithoutAbsorbingFacts(t *t } // Blackboard / stage milestone must not treat this as a completed stage advance. - pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) + pCtx, err := svc.ctxMgr.GetProjectContext(ctx, projectID) if err != nil { t.Fatalf("GetProjectContext: %v", err) } diff --git a/internal/bizdev/stage_quality_stop_test.go b/internal/bizdev/stage_quality_stop_test.go index 8f20cd2e..2dfe59d8 100644 --- a/internal/bizdev/stage_quality_stop_test.go +++ b/internal/bizdev/stage_quality_stop_test.go @@ -147,7 +147,7 @@ func TestExecuteStage_QualityFailed_StopsChainWithoutAbsorbingFactsOrPriorDoc(t t.Fatal("quality stop must not write completed stage workspace.json") } - pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) + pCtx, err := svc.ctxMgr.GetProjectContext(ctx, projectID) if err != nil { t.Fatalf("GetProjectContext: %v", err) } diff --git a/internal/bizdev/test_manager_flow_test.go b/internal/bizdev/test_manager_flow_test.go index ff59e4eb..c53d27de 100644 --- a/internal/bizdev/test_manager_flow_test.go +++ b/internal/bizdev/test_manager_flow_test.go @@ -12,17 +12,17 @@ func TestExecuteTask_TestManagerAfterApprovedUpstream(t *testing.T) { svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) ctx := context.Background() srDocID := "doc_proj1001_system_reqs" - _, err := svc.DocGov().CreateOrGetDocument( + _, err := svc.docGov.CreateOrGetDocument( ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "", 2, "张工", 3, "李主管", 4, "王总监", ) if err != nil { t.Fatalf("CreateOrGetDocument: %v", err) } - _ = svc.DocGov().MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) - _, _ = svc.DocGov().SubmitForReview(ctx, srDocID, 2, "author", "张工") - _, _ = svc.DocGov().ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") - _, err = svc.DocGov().ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") + _ = svc.docGov.MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) + _, _ = svc.docGov.SubmitForReview(ctx, srDocID, 2, "author", "张工") + _, _ = svc.docGov.ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") + _, err = svc.docGov.ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") if err != nil { t.Fatalf("ApproverApprove: %v", err) } diff --git a/internal/chat/action_executor_test.go b/internal/chat/action_executor_test.go index 91ec8041..673c4b91 100644 --- a/internal/chat/action_executor_test.go +++ b/internal/chat/action_executor_test.go @@ -13,7 +13,7 @@ import ( func TestExecuteAction_DocGovReviewApprove(t *testing.T) { cfg := &config.Config{DataDir: t.TempDir()} svc := bizdev.MustNewService(t, cfg) - docGov := svc.DocGov() + docGov := bizdev.Snapshot(svc).Gov ctx := context.Background() docID := "doc_proj1_research" diff --git a/internal/chat/longtask_quality_e2e_test.go b/internal/chat/longtask_quality_e2e_test.go index c2c7db28..422a2221 100644 --- a/internal/chat/longtask_quality_e2e_test.go +++ b/internal/chat/longtask_quality_e2e_test.go @@ -101,7 +101,7 @@ func TestLongTaskGovernance_QualityE2E(t *testing.T) { svc := bizdev.MustNewService(t, cfg) dialogueBackend := agent.NewBackend("pi-agent", cfg) engine := coordinator.NewCoordinatorEngineWithDialogue(svc, dialogueBackend, cfg.DataDir) - engine.SetContextServices(svc.ContextManager(), svc.KB()) + engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB) rec := intent.NewRecognizer() reportRoot := qualityReportDir(t) ctx := context.Background() diff --git a/internal/chat/session_test.go b/internal/chat/session_test.go index a8f6bd3b..db008593 100644 --- a/internal/chat/session_test.go +++ b/internal/chat/session_test.go @@ -270,7 +270,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) { ## 关键性能与型检预估 重点满足 GB 9706.1-2020 及 YY 0644 专用安全规范,型检周期约 5 个月。` - _, err := svc.ContextManager().IndexApprovedDocument(ctx, doc, body) + _, err := bizdev.Snapshot(svc).Ctx.IndexApprovedDocument(ctx, doc, body) if err != nil { t.Fatalf("IndexApprovedDocument failed: %v", err) } @@ -278,7 +278,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) { // 2. 发起新会话咨询 rec := intent.NewRecognizer() engine := coordinator.NewCoordinatorEngine(svc) - engine.SetContextServices(svc.ContextManager(), svc.KB()) + engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "超声软组织刀研发咨询", @@ -294,7 +294,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) { } session := chat.NewSession("sess_case_recommend", userCtx, rec, engine) - session.CtxMgr = svc.ContextManager() + session.CtxMgr = bizdev.Snapshot(svc).Ctx resp, err := session.SendMessage(ctx, "我们之前做过类似的超声手术设备吗?注册和型检要注意什么?") if err != nil { diff --git a/internal/chat/session_turn_intent_test.go b/internal/chat/session_turn_intent_test.go index 88a6dc22..d9b8f7d8 100644 --- a/internal/chat/session_turn_intent_test.go +++ b/internal/chat/session_turn_intent_test.go @@ -38,7 +38,7 @@ func TestChatSession_AssistantMetadataTurnIntentAndReplySource(t *testing.T) { cfg := &config.Config{DataDir: t.TempDir()} svc := bizdev.MustNewService(t, cfg) - mgr := svc.ContextManager() + mgr := bizdev.Snapshot(svc).Ctx sess := chat.NewSession("sess_test_turn_intent", userCtx, rec, engine) sess.CtxMgr = mgr diff --git a/internal/server/asset_handlers.go b/internal/server/asset_handlers.go index 680745c2..29277c6c 100644 --- a/internal/server/asset_handlers.go +++ b/internal/server/asset_handlers.go @@ -66,7 +66,7 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request return } - cas := s.svc.CAS() + cas := s.cas if cas == nil { http.Error(w, "storage unavailable", http.StatusServiceUnavailable) return @@ -96,7 +96,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request) if _, ok := s.requireProjectAccess(w, r, projectID); !ok { return } - cas := s.svc.CAS() + cas := s.cas if cas == nil { writeJSONResponse(w, http.StatusOK, []projectAssetResponse{}) return diff --git a/internal/server/auth_helpers.go b/internal/server/auth_helpers.go index a95ba552..8ad1d9aa 100644 --- a/internal/server/auth_helpers.go +++ b/internal/server/auth_helpers.go @@ -76,11 +76,11 @@ func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request // assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。 func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return false } - ownerID, err := s.svc.ContextManager().GetSessionOwnerID(r.Context(), sessionID) + ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID) if err != nil || ownerID <= 0 { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "SESSION_NOT_FOUND", diff --git a/internal/server/chat_document_reader.go b/internal/server/chat_document_reader.go index 9c2d6324..36a7d959 100644 --- a/internal/server/chat_document_reader.go +++ b/internal/server/chat_document_reader.go @@ -24,7 +24,7 @@ func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth if user != nil { userID = user.ID } - if projectID <= 0 || userID <= 0 || s.svc == nil || s.svc.DocGov() == nil { + if projectID <= 0 || userID <= 0 || s.gov == nil { return } @@ -35,7 +35,7 @@ func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, projectID, userID, s.isSystemAdminUser(ctx, user)); err != nil { return nil, err } - docs, err := s.svc.DocGov().ListWorkflowDocumentsByProject(ctx, projectID) + docs, err := s.gov.ListWorkflowDocumentsByProject(ctx, projectID) if err != nil { return nil, err } diff --git a/internal/server/chat_history_api_test.go b/internal/server/chat_history_api_test.go index e7d5d6b4..1727c3ea 100644 --- a/internal/server/chat_history_api_test.go +++ b/internal/server/chat_history_api_test.go @@ -33,13 +33,13 @@ func TestServer_ChatHistoryAndCaseRecommendationAPIs(t *testing.T) { sessID := "sess_api_history_001" userID := adminID - if _, err := svc.ContextManager().EnsureChatSession(ctx, sessID, userID, 0, "历史会话"); err != nil { + if _, err := bizdev.Snapshot(svc).Ctx.EnsureChatSession(ctx, sessID, userID, 0, "历史会话"); err != nil { t.Fatalf("ensure session: %v", err) } // 1. 模拟写入对话消息 - _ = svc.ContextManager().RecordUserMessage(ctx, sessID, userID, "user", "我们准备立项一款便携无线掌上超声,预算大约150万", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil) - _ = svc.ContextManager().RecordUserMessage(ctx, sessID, 0, "assistant", "已为您建立超声设备立项基准,预估研发周期6个月", "ADVANCE_STAGE", "EXPECTATION_HIT", nil) + _ = bizdev.Snapshot(svc).Ctx.RecordUserMessage(ctx, sessID, userID, "user", "我们准备立项一款便携无线掌上超声,预算大约150万", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil) + _ = bizdev.Snapshot(svc).Ctx.RecordUserMessage(ctx, sessID, 0, "assistant", "已为您建立超声设备立项基准,预估研发周期6个月", "ADVANCE_STAGE", "EXPECTATION_HIT", nil) // 2. 测试 GET /api/v1/chat/sessions reqSessions := httptest.NewRequest("GET", "/api/v1/chat/sessions?user_id="+strconv.FormatInt(userID, 10), nil) @@ -123,7 +123,7 @@ func TestServer_ChatHistoryAndCaseRecommendationAPIs(t *testing.T) { ## 关键性能与型检预估 符合 GB 9706.1-2020 和 YY 0644 标准要求,探头无线射频满足 SRRC 与无线电合规要求。` - _, err := svc.ContextManager().IndexApprovedDocument(ctx, doc, docBody) + _, err := bizdev.Snapshot(svc).Ctx.IndexApprovedDocument(ctx, doc, docBody) if err != nil { t.Fatalf("IndexApprovedDocument failed: %v", err) } diff --git a/internal/server/chat_history_auth.go b/internal/server/chat_history_auth.go index afb5823f..af96edce 100644 --- a/internal/server/chat_history_auth.go +++ b/internal/server/chat_history_auth.go @@ -33,8 +33,8 @@ func (s *Server) authorizeSessionRead( return nil, false } sessionOwnerID := user.ID - if s.svc != nil && s.svc.ContextManager() != nil { - if ownerID, err := s.svc.ContextManager().GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 { + if s.ctxMgr != nil { + if ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 { sessionOwnerID = ownerID } } @@ -43,8 +43,8 @@ func (s *Server) authorizeSessionRead( SessionOwnerID: sessionOwnerID, ViewerUserID: user.ID, } - if s.svc != nil && s.svc.ContextManager() != nil { - if projectID, err := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); err == nil { + if s.ctxMgr != nil { + if projectID, err := s.ctxMgr.GetSessionProjectID(r.Context(), sessionID); err == nil { authCtx.ProjectID = projectID } } @@ -200,10 +200,10 @@ func aggregateCategoryStats(msgs []ctxmgr.ChatMessageRecord) []ctxmgr.ChatCatego } func (s *Server) listVisibleSessionMessages(ctx context.Context, authCtx *chatHistoryAuth) ([]ctxmgr.ChatMessageRecord, error) { - if s.svc == nil || s.svc.ContextManager() == nil || authCtx == nil { + if s.ctxMgr == nil || authCtx == nil { return nil, nil } - msgs, err := s.svc.ContextManager().ListSessionMessagesForAuth(ctx, authCtx.SessionID) + msgs, err := s.ctxMgr.ListSessionMessagesForAuth(ctx, authCtx.SessionID) if err != nil { return nil, err } diff --git a/internal/server/chat_history_refresh_pg_test.go b/internal/server/chat_history_refresh_pg_test.go index 6bbfa75f..9cc3ce25 100644 --- a/internal/server/chat_history_refresh_pg_test.go +++ b/internal/server/chat_history_refresh_pg_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "chat-agent-system/internal/bizdev" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/workflowdef" @@ -24,7 +25,7 @@ func TestChatHistoryRefresh_PG_PreActivationAndTaskScope(t *testing.T) { ctx := context.Background() handler, srv := setupPGGovernanceServer(t, pool) - ctxMgr := srv.BizdevService().ContextManager() + ctxMgr := bizdev.Snapshot(srv.BizdevService()).Ctx if ctxMgr == nil { t.Fatal("context manager required") } diff --git a/internal/server/chat_recategorize_api.go b/internal/server/chat_recategorize_api.go index ffb7f43b..4532f51f 100644 --- a/internal/server/chat_recategorize_api.go +++ b/internal/server/chat_recategorize_api.go @@ -12,7 +12,7 @@ type recategorizeReq struct { } func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager unavailable", http.StatusServiceUnavailable) return } @@ -22,7 +22,7 @@ func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request) _ = json.NewDecoder(r.Body).Decode(&req) } - res, err := s.svc.ContextManager().RecategorizeMessages(r.Context(), req.SessionID) + res, err := s.ctxMgr.RecategorizeMessages(r.Context(), req.SessionID) if err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "success": false, diff --git a/internal/server/chat_recategorize_api_test.go b/internal/server/chat_recategorize_api_test.go index 16917636..cdc8f74f 100644 --- a/internal/server/chat_recategorize_api_test.go +++ b/internal/server/chat_recategorize_api_test.go @@ -25,7 +25,7 @@ func TestServer_ChatRecategorizeAPI(t *testing.T) { sessID := "sess_recategorize_api_001" ctx := t.Context() - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx // 注入种子消息 _, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "请问二类注册审批周期?", "QUERY_REGULATIONS", "", nil) diff --git a/internal/server/chat_restore_test.go b/internal/server/chat_restore_test.go index e7a38cf5..86677993 100644 --- a/internal/server/chat_restore_test.go +++ b/internal/server/chat_restore_test.go @@ -57,7 +57,7 @@ func TestServer_ChatRestoreAfterRestart(t *testing.T) { t.Logf("✓ Turn 1 完成: %s", reply1) // 验证消息已被持久化至 ContextManager - msgs, err := svc.ContextManager().GetChatHistory(context.Background(), sessID, 10, 0) + msgs, err := bizdev.Snapshot(svc).Ctx.GetChatHistory(context.Background(), sessID, 10, 0) if err != nil || len(msgs) != 2 { t.Fatalf("expected 2 messages in ContextManager, got %d (err: %v)", len(msgs), err) } @@ -97,7 +97,7 @@ func TestServer_ChatRestoreAfterRestart(t *testing.T) { t.Logf("✓ Turn 2 完成: %s", reply2) // 验证完整历史记录包含全部 4 条消息 (Turn 1 user+asst, Turn 2 user+asst) - allMsgs, err := svc.ContextManager().GetChatHistory(context.Background(), sessID, 10, 0) + allMsgs, err := bizdev.Snapshot(svc).Ctx.GetChatHistory(context.Background(), sessID, 10, 0) if err != nil { t.Fatalf("failed to query chat history: %v", err) } diff --git a/internal/server/chat_search_and_digest_api_test.go b/internal/server/chat_search_and_digest_api_test.go index fe51f070..8f52b555 100644 --- a/internal/server/chat_search_and_digest_api_test.go +++ b/internal/server/chat_search_and_digest_api_test.go @@ -29,7 +29,7 @@ func TestServer_ChatSearchAndDigestAPIs(t *testing.T) { // 注入种子消息 ctx := t.Context() - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx _, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "我们启动超声探头研发预算,计划投入150万元", "TRIGGER_TASK", "", map[string]any{ "day_key": dayKey, }) diff --git a/internal/server/chat_turn_api.go b/internal/server/chat_turn_api.go index ac775705..e61d30f4 100644 --- a/internal/server/chat_turn_api.go +++ b/internal/server/chat_turn_api.go @@ -17,7 +17,7 @@ func (s *Server) handleGetChatTurn(w http.ResponseWriter, r *http.Request) { return } - ctxMgr := s.svc.ContextManager() + ctxMgr := s.ctxMgr if ctxMgr == nil { http.Error(w, "context manager unavailable", http.StatusServiceUnavailable) return diff --git a/internal/server/chat_unread_and_export_api_test.go b/internal/server/chat_unread_and_export_api_test.go index 9ee042a0..5fcd188a 100644 --- a/internal/server/chat_unread_and_export_api_test.go +++ b/internal/server/chat_unread_and_export_api_test.go @@ -30,7 +30,7 @@ func TestServer_ChatUnreadAndExportAPIs(t *testing.T) { dayKey2 := "2026-09-09" ctx := t.Context() - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx token := loginTestAdmin(t, handler, cfg) if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "未读导出测试"); err != nil { t.Fatalf("EnsureChatSession failed: %v", err) diff --git a/internal/server/chat_window_api.go b/internal/server/chat_window_api.go index fb3af997..6ebfdc77 100644 --- a/internal/server/chat_window_api.go +++ b/internal/server/chat_window_api.go @@ -162,7 +162,7 @@ func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request // handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用) // POST /api/v1/chat/messages/import func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -207,7 +207,7 @@ func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request item.Metadata["created_at"] = item.CreatedAt } - _, err := s.svc.ContextManager().RecordUserMessageWithID( + _, err := s.ctxMgr.RecordUserMessageWithID( r.Context(), item.SessionID, item.UserID, diff --git a/internal/server/document_revision_access.go b/internal/server/document_revision_access.go index 4a12e246..5f52241a 100644 --- a/internal/server/document_revision_access.go +++ b/internal/server/document_revision_access.go @@ -29,13 +29,13 @@ func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccou if strings.TrimSpace(docID) == "" { return nil, docrevision.ErrInvalidDocID } - if s.svc != nil && s.svc.DocGov() != nil { - if doc, err := s.svc.DocGov().GetDocument(ctx, docID); err == nil && doc != nil { + if s.gov != nil { + if doc, err := s.gov.GetDocument(ctx, docID); err == nil && doc != nil { ref := &docRevisionRef{ Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir, LifecycleState: string(doc.CurrentState), AuthorID: doc.AuthorID, } - if content, cErr := s.svc.DocGov().ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil { + if content, cErr := s.gov.ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil { ref.Markdown = content.Markdown } else if doc.WorkDir != "" { md, _ := readOutputMarkdown(s.dataRoot(), doc.WorkDir) @@ -44,8 +44,8 @@ func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccou return ref, nil } } - if s.svc != nil && s.svc.FreeformDoc() != nil && user != nil { - if detail, err := s.svc.FreeformDoc().GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil { + if s.freeDoc != nil && user != nil { + if detail, err := s.freeDoc.GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil { return &docRevisionRef{ Kind: "freeform", ProjectID: int64(detail.Document.ProjectID), LifecycleState: "drafting", Markdown: detail.Content, Title: detail.Document.Title, diff --git a/internal/server/document_revision_authz_test.go b/internal/server/document_revision_authz_test.go index 368a2a59..8e6dbe0c 100644 --- a/internal/server/document_revision_authz_test.go +++ b/internal/server/document_revision_authz_test.go @@ -76,10 +76,10 @@ func TestDocRevision_ControlledDocumentRoutes(t *testing.T) { srv := newTestServer(t) ctx := context.Background() docID := "doc_proj1_intake" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底表", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底表", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - if _, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# 摸底\n\n正文\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, ""); err != nil { + if _, err := srv.rev.OpenTemplateVersion(ctx, docID, "# 摸底\n\n正文\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, ""); err != nil { t.Fatal(err) } token := loginTestAdmin(t, srv) @@ -102,14 +102,14 @@ func TestDocRevision_PendingCandidateExposedAndDecisionRequiresAuthor(t *testing srv := newTestServer(t) ctx := context.Background() docID := "doc_proj1_pending" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") if err != nil { t.Fatal(err) } - cand, _, err := srv.svc.DocRevision().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ + cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ DocID: docID, RunID: "high-1", InputSnapshotID: st.HeadSnapshotID, OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, }) @@ -141,14 +141,14 @@ func TestDocRevision_PendingCandidateExposedAndDecisionRequiresAuthor(t *testing // Non-author cannot decide (author is 1=admin in this fixture — use author 99 doc). docID2 := "doc_proj1_pending_na" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID2, 1, "research", "研", "V0.5", "", 99, "other", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID2, 1, "research", "研", "V0.5", "", 99, "other", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st2, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID2, "# x\n\n法规\n", docrevision.ActorSnapshot{}, 1, "") + st2, err := srv.rev.OpenTemplateVersion(ctx, docID2, "# x\n\n法规\n", docrevision.ActorSnapshot{}, 1, "") if err != nil { t.Fatal(err) } - cand2, _, err := srv.svc.DocRevision().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ + cand2, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ DocID: docID2, RunID: "high-2", InputSnapshotID: st2.HeadSnapshotID, OutputMarkdown: "# x\n\n", ActorKind: docrevision.ActorAgentSquad, }) @@ -191,15 +191,15 @@ func TestDocRevision_CommitWithMissingProposalReturnsGovernanceError(t *testing. srv := newTestServer(t) ctx := context.Background() docID := "doc_proj1_gov_prop" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") if err != nil { t.Fatal(err) } ghostProp := "prop_does_not_exist" - cand, _, err := srv.svc.DocRevision().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ + cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ DocID: docID, RunID: "gov-miss-1", ProposalID: ghostProp, InputSnapshotID: st.HeadSnapshotID, OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, @@ -230,7 +230,7 @@ func TestDocRevision_CommitWithMissingProposalReturnsGovernanceError(t *testing. t.Fatalf("code=%v body=%#v", body["code"], body) } - gotCand, err := srv.svc.DocRevision().GetCandidate(ctx, docID, cand.ID) + gotCand, err := srv.rev.GetCandidate(ctx, docID, cand.ID) if err != nil || gotCand.Status != docrevision.CandidateCommitted { t.Fatalf("ledger must stay committed after governance failure: %+v err=%v", gotCand, err) } @@ -257,21 +257,21 @@ func TestDocRevision_DecisionIdempotentWithRealProposal(t *testing.T) { srv := newTestServer(t) ctx := context.Background() docID := "doc_proj1_gov_prop_ok" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - prop, err := srv.svc.DocGov().ProposeModification(ctx, docID, "agent", "改", "影响", "风险") + prop, err := srv.gov.ProposeModification(ctx, docID, "agent", "改", "影响", "风险") if err != nil { t.Fatal(err) } - if _, err := srv.svc.DocGov().DecideModificationProposal(ctx, prop.ProposalID, 1, true, "同意"); err != nil { + if _, err := srv.gov.DecideModificationProposal(ctx, prop.ProposalID, 1, true, "同意"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "") if err != nil { t.Fatal(err) } - cand, _, err := srv.svc.DocRevision().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ + cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{ DocID: docID, RunID: "gov-ok-1", ProposalID: prop.ProposalID, InputSnapshotID: st.HeadSnapshotID, OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, @@ -295,7 +295,7 @@ func TestDocRevision_DecisionIdempotentWithRealProposal(t *testing.T) { t.Fatalf("%s decision want 200 got %d %s", label, w.Code, w.Body.String()) } if i == 0 { - got, err := srv.svc.DocRevision().GetCandidate(ctx, docID, cand.ID) + got, err := srv.rev.GetCandidate(ctx, docID, cand.ID) if err != nil || got.Status != docrevision.CandidateCommitted { t.Fatalf("after first: %+v err=%v", got, err) } diff --git a/internal/server/document_revision_duty_write_test.go b/internal/server/document_revision_duty_write_test.go index c678ebc2..cad76068 100644 --- a/internal/server/document_revision_duty_write_test.go +++ b/internal/server/document_revision_duty_write_test.go @@ -22,7 +22,7 @@ func TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite(t *testing.T) { strangerID int64 = 99 docID = "doc_proj_duty_write" ) - if _, err := srv.svc.DocGov().CreateOrGetDocument( + if _, err := srv.gov.CreateOrGetDocument( ctx, docID, 6145, "intake", "摸底", "V0.1", "", authorID, "编制人A", reviewerID, "审核人B", 0, "", ); err != nil { diff --git a/internal/server/document_revision_handlers.go b/internal/server/document_revision_handlers.go index 5e034622..489989c6 100644 --- a/internal/server/document_revision_handlers.go +++ b/internal/server/document_revision_handlers.go @@ -40,7 +40,7 @@ func (s *Server) docRev() *docrevision.Service { if s == nil || s.svc == nil { return nil } - return s.svc.DocRevision() + return s.rev } func (s *Server) handleDocRevisionState(w http.ResponseWriter, r *http.Request) { @@ -211,8 +211,8 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re // 1) Controlled: author duty + lease WITHOUT writing body (fail-closed before ledger). // This UI has no edit-session heartbeat: release the request-scoped human lease on every exit // (success, 409, projection failure) so a 15m residual cannot block a later squad run. - if kind == "controlled" && s.svc != nil && s.svc.DocGov() != nil { - if err := s.svc.DocGov().AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil { + if kind == "controlled" && s.gov != nil { + if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil { writeDocGovError(w, err) return } @@ -221,7 +221,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re holderID = "human_anon" } defer func() { - _, _ = s.svc.DocGov().ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderHuman), holderID) + _, _ = s.gov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderHuman), holderID) }() } @@ -273,8 +273,8 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re // 3) Project fact-source AFTER ledger commit. Controlled docs use SaveDocumentContent so // author_last_edited / audit trail stay consistent; failure → DOCUMENT_PROJECTION_FAILED // (ledger remains authoritative; same Idempotency-Key retry re-projects). - if kind == "controlled" && s.svc != nil && s.svc.DocGov() != nil { - if _, err := s.svc.DocGov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ + if kind == "controlled" && s.gov != nil { + if _, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ DocID: docID, ActorID: user.ID, ActorRole: docgov.RoleAuthor, @@ -300,9 +300,9 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re return } } - if kind == "freeform" && s.svc != nil && s.svc.FreeformDoc() != nil { + if kind == "freeform" && s.freeDoc != nil { md := body.Markdown - if _, err := s.svc.FreeformDoc().UpdateDocument(r.Context(), int(user.ID), docID, freedoc.UpdateFreeformDocRequest{ + if _, err := s.freeDoc.UpdateDocument(r.Context(), int(user.ID), docID, freedoc.UpdateFreeformDocRequest{ ContentMarkdown: &md, ChangeSummary: "revision ledger human save", }); err != nil { @@ -390,8 +390,8 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht }) return } - if s.svc != nil && s.svc.DocGov() != nil && st != nil { - if err := s.svc.DocGov().MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil { + if s.gov != nil && st != nil { + if err := s.gov.MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "code": "DOCUMENT_PROJECTION_FAILED", "error": err.Error(), @@ -401,7 +401,7 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht return } if body.Decision == "commit" && cand != nil && strings.TrimSpace(cand.ProposalID) != "" { - if err := s.svc.DocGov().MarkProposalApplied(r.Context(), cand.ProposalID); err != nil { + if err := s.gov.MarkProposalApplied(r.Context(), cand.ProposalID); err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "code": "DOCUMENT_GOVERNANCE_FAILED", "error": err.Error(), diff --git a/internal/server/document_revision_patch_order_test.go b/internal/server/document_revision_patch_order_test.go index d06bc709..8875e86b 100644 --- a/internal/server/document_revision_patch_order_test.go +++ b/internal/server/document_revision_patch_order_test.go @@ -29,10 +29,10 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger( workDir := filepath.Join(blocker, "ws-proj") docID := "doc_proj1_patch_order" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) if err != nil { t.Fatal(err) } @@ -58,7 +58,7 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger( if payload["code"] != "DOCUMENT_PROJECTION_FAILED" { t.Fatalf("code want DOCUMENT_PROJECTION_FAILED got %#v", payload["code"]) } - after, err := srv.svc.DocRevision().GetRevisionState(ctx, docID) + after, err := srv.rev.GetRevisionState(ctx, docID) if err != nil { t.Fatal(err) } @@ -76,10 +76,10 @@ func TestDocRevision_PatchOrder_LeaseDeniedDoesNotAdvanceLedger(t *testing.T) { _ = os.MkdirAll(workDir, 0o755) // Author is user 99; login admin may bypass route author gate but AssertHumanCanEdit (RoleAuthor) still fails. - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 99, "other", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 99, "other", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# L\n\nx\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# L\n\nx\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) if err != nil { t.Fatal(err) } @@ -96,14 +96,14 @@ func TestDocRevision_PatchOrder_LeaseDeniedDoesNotAdvanceLedger(t *testing.T) { if w.Code == http.StatusOK { t.Fatalf("non-author must not save successfully: %s", w.Body.String()) } - after, err := srv.svc.DocRevision().GetRevisionState(ctx, docID) + after, err := srv.rev.GetRevisionState(ctx, docID) if err != nil { t.Fatal(err) } if after.HeadRevisionNo != before { t.Fatalf("ledger must not advance when AssertHumanCanEdit fails: before=%d after=%d", before, after.HeadRevisionNo) } - doc, err := srv.svc.DocGov().GetDocument(ctx, docID) + doc, err := srv.gov.GetDocument(ctx, docID) if err != nil { t.Fatal(err) } @@ -119,10 +119,10 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T) _ = os.MkdirAll(workDir, 0o755) docID := "doc_proj1_patch_audit" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) if err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T) if w.Code != http.StatusOK { t.Fatalf("want 200 got %d %s", w.Code, w.Body.String()) } - doc, err := srv.svc.DocGov().GetDocument(ctx, docID) + doc, err := srv.gov.GetDocument(ctx, docID) if err != nil { t.Fatal(err) } @@ -162,10 +162,10 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) { _ = os.MkdirAll(workDir, 0o755) docID := "doc_proj1_patch_conflict_lease" - if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil { t.Fatal(err) } - st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) + st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir) if err != nil { t.Fatal(err) } @@ -199,14 +199,14 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) { func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID string) { t.Helper() ctx := context.Background() - doc, err := srv.svc.DocGov().GetDocument(ctx, docID) + doc, err := srv.gov.GetDocument(ctx, docID) if err != nil { t.Fatal(err) } if doc.HasActiveLease(time.Now()) { t.Fatalf("human lease must be released after PATCH, holder=%s/%s", doc.LeaseHolderType, doc.LeaseHolderID) } - leased, err := srv.svc.DocGov().AcquireLease(ctx, docgov.LeaseAcquireRequest{ + leased, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{ DocID: docID, HolderType: docgov.LeaseHolderSquad, HolderID: "run_after_human_save", @@ -218,7 +218,7 @@ func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID st if leased.LeaseHolderType != docgov.LeaseHolderSquad { t.Fatalf("want squad holder, got %s", leased.LeaseHolderType) } - if _, err := srv.svc.DocGov().ReleaseLease(ctx, docID, string(docgov.LeaseHolderSquad), "run_after_human_save"); err != nil { + if _, err := srv.gov.ReleaseLease(ctx, docID, string(docgov.LeaseHolderSquad), "run_after_human_save"); err != nil { t.Fatal(err) } } diff --git a/internal/server/e2e_workflow_governance_test.go b/internal/server/e2e_workflow_governance_test.go index 5ec70d35..4c370807 100644 --- a/internal/server/e2e_workflow_governance_test.go +++ b/internal/server/e2e_workflow_governance_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "chat-agent-system/internal/bizdev" "chat-agent-system/internal/db" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/workflowdef" @@ -96,7 +97,7 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) { _ = json.Unmarshal(recActivate.Body.Bytes(), &activated) docID := fmt.Sprintf("doc_proj%d_test_manager", projectID) - docEngine := srv.BizdevService().DocGov() + docEngine := bizdev.Snapshot(srv.BizdevService()).Gov _, err = docEngine.CreateOrGetDocument(ctx, docID, projectID, "test_manager", "系统验证方案及计划", "system_verification_planning", "", creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver") if err != nil { diff --git a/internal/server/freeform_doc_api.go b/internal/server/freeform_doc_api.go index 9841ceda..56262d56 100644 --- a/internal/server/freeform_doc_api.go +++ b/internal/server/freeform_doc_api.go @@ -36,7 +36,7 @@ func extractDocIDFromPath(r *http.Request, actionSuffix string) string { // POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private) func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -82,7 +82,7 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) return } - detail, err := s.svc.FreeformDoc().CreateDocument(r.Context(), int(user.ID), req) + detail, err := s.freeDoc.CreateDocument(r.Context(), int(user.ID), req) if err != nil { if errors.Is(err, freedoc.ErrInvalidParam) { writeJSONResponse(w, http.StatusBadRequest, map[string]any{ @@ -106,7 +106,7 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) // POST /api/v1/freeform-documents/from-message func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -124,7 +124,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht return } - detail, isExisting, err := s.svc.FreeformDoc().CreateFromMessage(r.Context(), userID, req) + detail, isExisting, err := s.freeDoc.CreateFromMessage(r.Context(), userID, req) if err != nil { if errors.Is(err, freedoc.ErrMessageNotExportable) { writeJSONResponse(w, http.StatusForbidden, map[string]any{ @@ -168,7 +168,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht // GET /api/v1/freeform-documents func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -186,7 +186,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) projectID, _ = strconv.Atoi(pStr) } - list, err := s.svc.FreeformDoc().ListDocuments(r.Context(), userID, projectID, space) + list, err := s.freeDoc.ListDocuments(r.Context(), userID, projectID, space) if err != nil { writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ "error_code": "LIST_FAILED", @@ -206,7 +206,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) // GET /api/v1/freeform-documents/{doc_id} func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -224,7 +224,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) { return } - detail, err := s.svc.FreeformDoc().GetDocument(r.Context(), userID, docID) + detail, err := s.freeDoc.GetDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{ @@ -255,7 +255,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) { // PUT /api/v1/freeform-documents/{doc_id} func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -292,7 +292,7 @@ func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) } } - detail, err := s.svc.FreeformDoc().UpdateDocument(r.Context(), userID, docID, req) + detail, err := s.freeDoc.UpdateDocument(r.Context(), userID, docID, req) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{ @@ -331,7 +331,7 @@ func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) // POST /api/v1/freeform-documents/{doc_id}/share func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -349,7 +349,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) return } - doc, err := s.svc.FreeformDoc().ShareDocument(r.Context(), userID, docID) + doc, err := s.freeDoc.ShareDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) @@ -372,7 +372,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) // POST /api/v1/freeform-documents/{doc_id}/unshare func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -390,7 +390,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request return } - doc, err := s.svc.FreeformDoc().UnshareDocument(r.Context(), userID, docID) + doc, err := s.freeDoc.UnshareDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) @@ -413,7 +413,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request // DELETE /api/v1/freeform-documents/{doc_id} func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.FreeformDoc() == nil { + if s.freeDoc == nil { http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable) return } @@ -431,7 +431,7 @@ func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) return } - err := s.svc.FreeformDoc().DeleteDocument(r.Context(), userID, docID) + err := s.freeDoc.DeleteDocument(r.Context(), userID, docID) if err != nil { if errors.Is(err, freedoc.ErrNotFound) { writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) diff --git a/internal/server/freeform_doc_api_test.go b/internal/server/freeform_doc_api_test.go index c9174972..2b695949 100644 --- a/internal/server/freeform_doc_api_test.go +++ b/internal/server/freeform_doc_api_test.go @@ -25,7 +25,7 @@ func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler, handler := srv.Routes() ctx := context.Background() - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx validMsgID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-test", 1, "assistant", "# 临床安全评估\n\n评估合格。", "consult", "main", nil) require.NoError(t, err) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index ef183972..cc20ab89 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -102,7 +102,7 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) { } } schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory) - taskStoreReady := s.svc != nil && taskrun.TablesReady(context.Background(), s.svc.Pool()) + taskStoreReady := taskrun.TablesReady(context.Background(), s.pool) status := "ok" code := http.StatusOK @@ -415,12 +415,12 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) { } projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } - uCtx, err := s.svc.ContextManager().GetUserContext(r.Context(), projectID, userID, sessionID) + uCtx, err := s.ctxMgr.GetUserContext(r.Context(), projectID, userID, sessionID) if err != nil { http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError) return @@ -450,7 +450,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque return } - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -463,7 +463,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque CustomInstructions: req.CustomInstructions, } - if err := s.svc.ContextManager().UpdateUserPreference(r.Context(), pref); err != nil { + if err := s.ctxMgr.UpdateUserPreference(r.Context(), pref); err != nil { http.Error(w, "save user preference failed: "+err.Error(), http.StatusInternalServerError) return } @@ -490,12 +490,12 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request) return } - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } - pCtx, err := s.svc.ContextManager().GetProjectContext(r.Context(), projectID) + pCtx, err := s.ctxMgr.GetProjectContext(r.Context(), projectID) if err != nil { http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError) return @@ -516,12 +516,12 @@ func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - doc, err := s.svc.DocGov().GetDocument(r.Context(), docID) + doc, err := s.gov.GetDocument(r.Context(), docID) if err != nil { http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound) return @@ -555,12 +555,12 @@ func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) { return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - doc, err := s.svc.DocGov().RecordDocumentViewed(r.Context(), req.DocID, user.ID, docgov.UserRole(req.UserRole), user.Name) + doc, err := s.gov.RecordDocumentViewed(r.Context(), req.DocID, user.ID, docgov.UserRole(req.UserRole), user.Name) if err != nil { http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError) return @@ -589,7 +589,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } @@ -597,7 +597,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request return } - doc, err := s.svc.DocGov().SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name) + doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name) if err != nil { http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest) return @@ -630,7 +630,7 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } @@ -643,13 +643,13 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque var err error if action == "approve" { - doc, err = s.svc.DocGov().ReviewApprove(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, req.Comment) + doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, req.Comment) } else if action == "reject" { reason := req.Reason if reason == "" { reason = req.Comment } - doc, err = s.svc.DocGov().ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason) + doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason) } else { http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest) return @@ -676,7 +676,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } @@ -746,12 +746,12 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - proposal, err := s.svc.DocGov().DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason) + proposal, err := s.gov.DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason) if err != nil { http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest) return @@ -768,12 +768,12 @@ func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - comments, err := s.svc.DocGov().ListComments(r.Context(), docID) + comments, err := s.gov.ListComments(r.Context(), docID) if err != nil { http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError) return @@ -801,7 +801,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } @@ -811,7 +811,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) cType = docgov.CommentGeneral } - comment, err := s.svc.DocGov().AddComment( + comment, err := s.gov.AddComment( r.Context(), req.DocID, req.UserID, req.UserName, docgov.UserRole(req.UserRole), req.TargetSection, cType, req.Content, ) @@ -831,12 +831,12 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - logs, err := s.svc.DocGov().ListAuditLogs(r.Context(), docID) + logs, err := s.gov.ListAuditLogs(r.Context(), docID) if err != nil { http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError) return @@ -851,7 +851,7 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques // handleListChatSessions 分页查询用户/项目关联的历史会话列表 func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -872,7 +872,7 @@ func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) offset = 0 } - sessions, err := s.svc.ContextManager().ListChatSessions(r.Context(), ctxmgr.SessionFilter{ + sessions, err := s.ctxMgr.ListChatSessions(r.Context(), ctxmgr.SessionFilter{ UserID: userID, ProjectID: projectID, Limit: limit, @@ -895,7 +895,7 @@ type upsertChatSessionRequest struct { // handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。 func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -910,7 +910,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) return } - summary, err := s.svc.ContextManager().EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title) + summary, err := s.ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title) if err != nil { http.Error(w, "upsert chat session failed: "+err.Error(), http.StatusInternalServerError) return @@ -921,7 +921,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) // handleGetChatMessages 分页获取指定会话的历史多轮消息记录 func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) { - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -972,7 +972,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) { if !ok { return } - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -998,7 +998,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) { minScore = 0.50 } - cases, err := s.svc.ContextManager().SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{ + cases, err := s.ctxMgr.SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{ Query: query, TaskKey: taskKey, ExcludeDoc: excludeDoc, @@ -1043,12 +1043,12 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request) http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } role := docgov.RoleAuthor - res, err := s.svc.DocGov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ + res, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ DocID: req.DocID, ActorID: user.ID, ActorRole: role, @@ -1079,7 +1079,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } @@ -1088,7 +1088,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request workDir = "" } ttl := time.Duration(req.TTLSeconds) * time.Second - doc, err := s.svc.DocGov().AcquireLease(r.Context(), docgov.LeaseAcquireRequest{ + doc, err := s.gov.AcquireLease(r.Context(), docgov.LeaseAcquireRequest{ DocID: req.DocID, HolderType: docgov.LeaseHolderType(req.HolderType), HolderID: req.HolderID, @@ -1111,12 +1111,12 @@ func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Reque http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } ttl := time.Duration(req.TTLSeconds) * time.Second - doc, err := s.svc.DocGov().HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl) + doc, err := s.gov.HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl) if err != nil { writeDocGovError(w, err) return @@ -1133,11 +1133,11 @@ func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - doc, err := s.svc.DocGov().ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID) + doc, err := s.gov.ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID) if err != nil { writeDocGovError(w, err) return diff --git a/internal/server/lease_concurrency_test.go b/internal/server/lease_concurrency_test.go index 3f5d6da3..b566958e 100644 --- a/internal/server/lease_concurrency_test.go +++ b/internal/server/lease_concurrency_test.go @@ -29,7 +29,7 @@ func TestDocEditLease_HTTPConcurrencyGates(t *testing.T) { docID := fmt.Sprintf("doc_lease_http_concurrency_%d_a", time.Now().UnixNano()) ctx := t.Context() - _, err := svc.DocGov().CreateOrGetDocument(ctx, docID, 91001, "intake", "并发互斥测试文档", "V0.1", "", + _, err := bizdev.Snapshot(svc).Gov.CreateOrGetDocument(ctx, docID, 91001, "intake", "并发互斥测试文档", "V0.1", "", 1, "作者", 2, "审核", 3, "批准") if err != nil { t.Fatalf("create doc: %v", err) @@ -113,7 +113,7 @@ func TestDocEditLease_HTTPConcurrencyGates(t *testing.T) { // 6. 不同文档可并行持有租约 doc2 := fmt.Sprintf("doc_lease_http_concurrency_%d_b", time.Now().UnixNano()) - _, _ = svc.DocGov().CreateOrGetDocument(ctx, doc2, 91001, "research", "并行文档B", "V0.5", "", + _, _ = bizdev.Snapshot(svc).Gov.CreateOrGetDocument(ctx, doc2, 91001, "research", "并行文档B", "V0.5", "", 1, "作者", 2, "审核", 3, "批准") acqA, _ := json.Marshal(map[string]any{ "doc_id": docID, "holder_type": "squad", "holder_id": "run_a", "ttl_seconds": 60, diff --git a/internal/server/phase5_fault_injection_pg_test.go b/internal/server/phase5_fault_injection_pg_test.go index e3d38372..f9358e57 100644 --- a/internal/server/phase5_fault_injection_pg_test.go +++ b/internal/server/phase5_fault_injection_pg_test.go @@ -108,7 +108,7 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http configRevision = activated.Instance.ConfigRevision docID := fmt.Sprintf("doc_p5_fault_%d", projectID) - docEngine := srv.BizdevService().DocGov() + docEngine := bizdev.Snapshot(srv.BizdevService()).Gov _, _ = docEngine.CreateOrGetDocument(ctx, docID, projectID, "intake", "Intake", "V0.1", "", creatorID, "A", reviewerID, "R", approverID, "P") _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_p5_fault", false) _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") diff --git a/internal/server/phase5_finalize_fault_pg_test.go b/internal/server/phase5_finalize_fault_pg_test.go index ccc29eb4..e0d8bb8b 100644 --- a/internal/server/phase5_finalize_fault_pg_test.go +++ b/internal/server/phase5_finalize_fault_pg_test.go @@ -85,7 +85,7 @@ func TestPhase5_RollingRestartReplay_PG(t *testing.T) { // 模拟三次进程重启:新 Manager 仅挂载 Store for i := 0; i < 3; i++ { - restarted := taskrun.NewManager(srv.ProjectWorkflowService(), srv.BizdevService().ContextManager()) + restarted := taskrun.NewManager(srv.ProjectWorkflowService(), bizdev.Snapshot(srv.BizdevService()).Ctx) restarted.SetStore(store) rec, err := restarted.GetRun(runID) if err != nil { diff --git a/internal/server/project_doc_handlers.go b/internal/server/project_doc_handlers.go index aa23ab00..0f0f249a 100644 --- a/internal/server/project_doc_handlers.go +++ b/internal/server/project_doc_handlers.go @@ -19,7 +19,7 @@ func (s *Server) docflowQuery() *docflow.Query { return nil } return &docflow.Query{Docs: &docquery.Service{ - Gov: s.svc.DocGov(), + Gov: s.gov, Rev: s.docRev(), RepoRoot: s.repoRoot(), Authorize: func(ctx context.Context, p docquery.Principal, rec *docgov.DocumentRecord) error { @@ -78,12 +78,12 @@ func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *ht return } - if s.svc == nil || s.svc.DocGov() == nil { + if s.gov == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } - docs, err := s.svc.DocGov().ListWorkflowDocumentsByProject(r.Context(), projectID) + docs, err := s.gov.ListWorkflowDocumentsByProject(r.Context(), projectID) if err != nil { http.Error(w, "list project documents failed: "+err.Error(), http.StatusInternalServerError) return @@ -134,10 +134,10 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R } func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string) (*docgov.DocumentContent, error) { - if s == nil || s.svc == nil || s.svc.DocGov() == nil { + if s == nil || s.gov == nil { return nil, docgov.ErrDocumentNotFound } - svc := &docquery.Service{Gov: s.svc.DocGov(), Rev: s.docRev(), RepoRoot: s.repoRoot()} + svc := &docquery.Service{Gov: s.gov, Rev: s.docRev(), RepoRoot: s.repoRoot()} view, err := svc.Get(ctx, docquery.Principal{UserID: 1}, docquery.DocRef{DocID: docID}) if err != nil && !errors.Is(err, docquery.ErrLedgerMissing) { return nil, err @@ -145,7 +145,7 @@ func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string if view != nil && view.Markdown != "" { return view.ToDocumentContent(), nil } - return s.svc.DocGov().ReadDocumentContent(ctx, docID, s.repoRoot()) + return s.gov.ReadDocumentContent(ctx, docID, s.repoRoot()) } // serveLedgerHeadOrConflict keeps the historical helper name. Projection is a diff --git a/internal/server/project_handlers_test.go b/internal/server/project_handlers_test.go index 408c659c..e4f8e9c5 100644 --- a/internal/server/project_handlers_test.go +++ b/internal/server/project_handlers_test.go @@ -62,17 +62,17 @@ func TestHandleRunProjectWorkflow_DecodesBodyWithoutContentLength(t *testing.T) ctx := t.Context() srDocID := "doc_proj1001_system_reqs" - _, err := svc.DocGov().CreateOrGetDocument( + _, err := bizdev.Snapshot(svc).Gov.CreateOrGetDocument( ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "", 2, "张工", 3, "李主管", 4, "王总监", ) if err != nil { t.Fatalf("CreateOrGetDocument: %v", err) } - _ = svc.DocGov().MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) - _, _ = svc.DocGov().SubmitForReview(ctx, srDocID, 2, "author", "张工") - _, _ = svc.DocGov().ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") - _, err = svc.DocGov().ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") + _ = bizdev.Snapshot(svc).Gov.MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) + _, _ = bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, srDocID, 2, "author", "张工") + _, _ = bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") + _, err = bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") if err != nil { t.Fatalf("ApproverApprove: %v", err) } diff --git a/internal/server/project_members_handlers.go b/internal/server/project_members_handlers.go index d2f5e9ea..d3bd9ad9 100644 --- a/internal/server/project_members_handlers.go +++ b/internal/server/project_members_handlers.go @@ -33,7 +33,7 @@ func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error { if s == nil || s.svc == nil || projectID <= 0 { return nil } - pool := s.svc.Pool() + pool := s.pool if pool == nil { return nil } diff --git a/internal/server/project_workflow_api_test.go b/internal/server/project_workflow_api_test.go index f54f0d56..e48f4a2d 100644 --- a/internal/server/project_workflow_api_test.go +++ b/internal/server/project_workflow_api_test.go @@ -203,16 +203,16 @@ func TestProjectWorkflow_TestManagerAndLineageStale(t *testing.T) { // 3. 在受控治理中创建并批准系统需求规格书 srDocID := fmt.Sprintf("doc_proj%d_system_reqs", projectID) - _, err := srv.BizdevService().DocGov().CreateOrGetDocument( + _, err := bizdev.Snapshot(srv.BizdevService()).Gov.CreateOrGetDocument( nil, srDocID, projectID, "system_reqs", "系统需求规格书", "V0.5_feasibility", "", 2, "张工", 3, "李主管", 4, "王总监", ) require.NoError(t, err) - _ = srv.BizdevService().DocGov().MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false) - _, _ = srv.BizdevService().DocGov().SubmitForReview(nil, srDocID, 2, "author", "张工") - _, _ = srv.BizdevService().DocGov().ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意") - approvedDoc, err := srv.BizdevService().DocGov().ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效") + _ = bizdev.Snapshot(srv.BizdevService()).Gov.MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false) + _, _ = bizdev.Snapshot(srv.BizdevService()).Gov.SubmitForReview(nil, srDocID, 2, "author", "张工") + _, _ = bizdev.Snapshot(srv.BizdevService()).Gov.ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意") + approvedDoc, err := bizdev.Snapshot(srv.BizdevService()).Gov.ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效") require.NoError(t, err) assert.Equal(t, "approved", string(approvedDoc.CurrentState)) diff --git a/internal/server/reply_export_api.go b/internal/server/reply_export_api.go index 5165899c..7ec3dedf 100644 --- a/internal/server/reply_export_api.go +++ b/internal/server/reply_export_api.go @@ -17,7 +17,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques return } - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -31,7 +31,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques return } - msg, err := s.svc.ContextManager().GetMessageByID(r.Context(), msgID) + msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID) if err != nil || msg == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "MESSAGE_NOT_FOUND", @@ -84,7 +84,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) { return } - if s.svc == nil || s.svc.ContextManager() == nil { + if s.ctxMgr == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } @@ -98,7 +98,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) { return } - msg, err := s.svc.ContextManager().GetMessageByID(r.Context(), msgID) + msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID) if err != nil || msg == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "MESSAGE_NOT_FOUND", diff --git a/internal/server/reply_export_api_test.go b/internal/server/reply_export_api_test.go index 1bbdee42..b598598b 100644 --- a/internal/server/reply_export_api_test.go +++ b/internal/server/reply_export_api_test.go @@ -24,7 +24,7 @@ func TestServer_ReplyExportAPI(t *testing.T) { handler := srv.Routes() ctx := t.Context() - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx sessID := "sess_export_api_test" token := loginTestAdmin(t, handler, cfg) if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "导出测试会话"); err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index ca1ee34c..8c56f188 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,13 +20,20 @@ import ( "chat-agent-system/internal/config" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/ctxmgr" + "chat-agent-system/internal/docgov" + "chat-agent-system/internal/docrevision" + "chat-agent-system/internal/freedoc" "chat-agent-system/internal/intent" + "chat-agent-system/internal/kb" "chat-agent-system/internal/notification" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" + "chat-agent-system/internal/storage" "chat-agent-system/internal/taskrun" "chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workspace" + + "github.com/jackc/pgx/v5/pgxpool" ) // Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务 @@ -52,6 +59,13 @@ type Server struct { chatLog chatflow.ChatLog assembler chatflow.ContextAssembler stageflow *stageflow.UseCase + gov *docgov.GovernanceEngine + rev *docrevision.Service + ctxMgr *ctxmgr.ContextManager + cas *storage.CAS + pool *pgxpool.Pool + freeDoc *freedoc.Service + kb *kb.Client httpServer *http.Server } @@ -79,8 +93,15 @@ func NewFromRuntime(rt *app.Runtime) *Server { wsCap: rt.WSCap, cmds: rt.Commands, stageflow: rt.Stageflow, + gov: rt.Gov, + rev: rt.Rev, + ctxMgr: rt.Ctx, + cas: rt.CAS, + pool: rt.Pool, + freeDoc: rt.FreeDoc, + kb: rt.KB, } - s.chatLog = chatflow.NewAdapter(s.svc.ContextManager(), s.resolveStageAccess, s.failClosedAuthz()) + s.chatLog = chatflow.NewAdapter(s.ctxMgr, s.resolveStageAccess, s.failClosedAuthz()) s.assembler = chatflow.NewAssembler(coordinator.NewContextAssembler(), s.chatLog) rt.ChatLog = s.chatLog rt.Assembler = s.assembler @@ -412,8 +433,8 @@ func (s *Server) getOrCreateSession( // 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3) var restored *ctxmgr.RestoredSessionContext - if s.svc != nil && s.svc.ContextManager() != nil { - if r, err := s.svc.ContextManager().RestoreSessionContext(context.Background(), sessionID); err == nil && r != nil && len(r.Messages) > 0 { + if s.ctxMgr != nil { + if r, err := s.ctxMgr.RestoreSessionContext(context.Background(), sessionID); err == nil && r != nil && len(r.Messages) > 0 { restored = r } } @@ -480,9 +501,9 @@ func (s *Server) getOrCreateSession( sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine) sess.ChatLog = s.chatLog if s.svc != nil { - sess.CtxMgr = s.svc.ContextManager() + sess.CtxMgr = s.ctxMgr sess.Actions = &chat.ActionServices{ - DocGov: s.svc.DocGov(), + DocGov: s.gov, Bizdev: s.svc, TaskRuns: s.taskRuns, } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 748ea870..9beafd75 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -93,14 +93,14 @@ func TestServer_ChatDocumentSummaryReadsExistingDocumentWithoutWriteAction(t *te if err := os.WriteFile(filepath.Join(workDir, "output.md"), markdown, 0o644); err != nil { t.Fatalf("write output.md: %v", err) } - if _, err := srv.svc.DocGov().CreateOrGetDocument( + if _, err := srv.gov.CreateOrGetDocument( ctx, "doc_proj143_intake", 143, "intake", "用户初始需求收集表", "V0.1_triage", workDir, 1, "编制人", 1, "审核人", 1, "批准人", ); err != nil { t.Fatalf("create document: %v", err) } hash := fmt.Sprintf("%x", sha256.Sum256(markdown)) - if err := srv.svc.DocGov().MarkDocumentContentUpdated(ctx, "doc_proj143_intake", hash, false); err != nil { + if err := srv.gov.MarkDocumentContentUpdated(ctx, "doc_proj143_intake", hash, false); err != nil { t.Fatalf("mark content: %v", err) } diff --git a/internal/server/stage_access_helpers.go b/internal/server/stage_access_helpers.go index 949eda82..3909f4ae 100644 --- a/internal/server/stage_access_helpers.go +++ b/internal/server/stage_access_helpers.go @@ -68,8 +68,8 @@ func (s *Server) enforceDocGovWorkflowAction(w http.ResponseWriter, ctx context. if docID == "" { return true } - if s.svc != nil && s.svc.DocGov() != nil { - doc, err := s.svc.DocGov().GetDocument(ctx, docID) + if s.gov != nil { + doc, err := s.gov.GetDocument(ctx, docID) if err == nil && doc != nil && doc.ProjectID > 0 && doc.TaskKey != "" { return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action) } diff --git a/internal/server/task_finalize.go b/internal/server/task_finalize.go index 1a84ed13..bb1573bf 100644 --- a/internal/server/task_finalize.go +++ b/internal/server/task_finalize.go @@ -31,7 +31,7 @@ func (s *Server) finalizeTaskRun(ctx context.Context, record *taskrun.RunRecord, if s != nil { cmds.PW = s.pwSvc if s.svc != nil { - cmds.Ctx = s.svc.ContextManager() + cmds.Ctx = s.ctxMgr } } return cmds.Finalize(ctx, record, result) @@ -50,7 +50,7 @@ func (s *Server) taskCommands() *taskrun.Commands { cmds.StorageIdentity = "env:" + s.cfg.Env } if s != nil && s.svc != nil { - cmds.Ctx = s.svc.ContextManager() + cmds.Ctx = s.ctxMgr } return cmds } diff --git a/internal/server/task_finalize_test.go b/internal/server/task_finalize_test.go index 12d21518..75a8bb61 100644 --- a/internal/server/task_finalize_test.go +++ b/internal/server/task_finalize_test.go @@ -28,7 +28,7 @@ func TestFinalizeTaskRun_ReplacesPlaceholderContent(t *testing.T) { workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# 最终交付\n\n正文内容。"), 0o644) - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-finalize", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{ "task_status": "running", }) @@ -106,7 +106,7 @@ func TestFinalizeTaskRun_PendingAuthorConfirmation_NotCompleted(t *testing.T) { workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# base head\n"), 0o644) - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-pending", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{ "task_status": "running", }) @@ -180,7 +180,7 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) { cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"} svc := bizdev.MustNewService(t, cfg) pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil) - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# async finalize\n"), 0o644) @@ -237,7 +237,7 @@ func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) { cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"} svc := bizdev.MustNewService(t, cfg) pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil) - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# head\n"), 0o644) @@ -315,7 +315,7 @@ func TestFinalizeTaskRun_HashesViaWorkspaceCapability(t *testing.T) { if err := srv.wsCap.WriteArtifact(ctx, ref, workspace.DeliverableKey, body); err != nil { t.Fatal(err) } - ctxMgr := svc.ContextManager() + ctxMgr := bizdev.Snapshot(svc).Ctx asstID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-hash", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil) if err != nil { t.Fatal(err) diff --git a/internal/server/task_run_busy_preflight_test.go b/internal/server/task_run_busy_preflight_test.go index ee6113e4..523891f0 100644 --- a/internal/server/task_run_busy_preflight_test.go +++ b/internal/server/task_run_busy_preflight_test.go @@ -17,19 +17,19 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) { srv := newTestServer(t) ctx := t.Context() docID := "doc_proj143_intake" - if _, err := srv.svc.DocGov().CreateOrGetDocument( + if _, err := srv.gov.CreateOrGetDocument( ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "", 1, "编制人", 1, "审核人", 1, "批准人", ); err != nil { t.Fatalf("create document: %v", err) } - if _, err := srv.svc.DocGov().AcquireLease(ctx, docgov.LeaseAcquireRequest{ + if _, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{ DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "editor-9", TTL: time.Minute, }); err != nil { t.Fatalf("acquire fixture lease: %v", err) } defer func() { - _, _ = srv.svc.DocGov().ReleaseLease(t.Context(), docID, string(docgov.LeaseHolderHuman), "editor-9") + _, _ = srv.gov.ReleaseLease(t.Context(), docID, string(docgov.LeaseHolderHuman), "editor-9") }() requestBody, _ := json.Marshal(createTaskRunRequest{ @@ -53,7 +53,7 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) { if _, exists := payload["run_id"]; exists { t.Fatalf("busy preflight must not return run_id: %+v", payload) } - history, err := srv.svc.ContextManager().GetChatHistory(ctx, "fixture-busy-preflight", 20, 0) + history, err := srv.ctxMgr.GetChatHistory(ctx, "fixture-busy-preflight", 20, 0) if err != nil { t.Fatalf("get busy history: %v", err) } @@ -110,7 +110,7 @@ func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) { if rec, err := srv.taskRuns.GetRun(payload.RunID); err == nil && rec != nil && rec.Status == "running" { t.Fatalf("pre-activation run must not start a live worker: %+v", rec) } - history, err := srv.svc.ContextManager().GetChatHistory(t.Context(), "sess-p9001-u1", 20, 0) + history, err := srv.ctxMgr.GetChatHistory(t.Context(), "sess-p9001-u1", 20, 0) if err != nil { t.Fatalf("chat history: %v", err) } @@ -133,7 +133,7 @@ func TestDocGovActions_PreActivationRejected(t *testing.T) { srv.instSvc = projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil) docID := "doc_proj9002_intake" - if _, err := srv.svc.DocGov().CreateOrGetDocument( + if _, err := srv.gov.CreateOrGetDocument( t.Context(), docID, 9002, "intake", "立项文档", "V0.1_triage", "", 1, "编制人", 2, "审核人", 3, "批准人", ); err != nil { diff --git a/internal/server/task_run_handlers.go b/internal/server/task_run_handlers.go index 675361a2..83874b81 100644 --- a/internal/server/task_run_handlers.go +++ b/internal/server/task_run_handlers.go @@ -104,7 +104,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) { } } - ctxMgr := s.svc.ContextManager() + ctxMgr := s.ctxMgr if ctxMgr != nil { _, _ = ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.ProjectName) } @@ -248,7 +248,7 @@ func (s *Server) createBlockedGovernanceRun(w http.ResponseWriter, r *http.Reque }) return } - ctxMgr := s.svc.ContextManager() + ctxMgr := s.ctxMgr if ctxMgr != nil && req.UserMessage != "" { userMeta := map[string]any{ "project_id": req.ProjectID, @@ -338,7 +338,7 @@ func (s *Server) recordBusyPreflightMessages( // preflightTaskDocumentBusy 在写入聊天占位与 task_runs 之前拦截已知租约占用。 // 真正的租约获取仍由 docgov 原子执行;本检查用于避免用户重复点击产生必然失败的 run。 func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRunRequest) (*docgov.DocumentBusyError, error) { - if req.Stage != "" || req.ProjectID <= 0 || req.TaskKey == "" || s.svc == nil || s.svc.DocGov() == nil { + if req.Stage != "" || req.ProjectID <= 0 || req.TaskKey == "" || s.gov == nil { return nil, nil } taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, "") @@ -349,7 +349,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu if docID == "" { return nil, nil } - doc, err := s.svc.DocGov().GetDocument(ctx, docID) + doc, err := s.gov.GetDocument(ctx, docID) if errors.Is(err, docgov.ErrDocumentNotFound) { return nil, nil } diff --git a/internal/server/task_run_idempotent_sse_test.go b/internal/server/task_run_idempotent_sse_test.go index 838c0e28..b4d40448 100644 --- a/internal/server/task_run_idempotent_sse_test.go +++ b/internal/server/task_run_idempotent_sse_test.go @@ -43,7 +43,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test ) docID := "doc_proj143_intake" - if _, err := srv.svc.DocGov().CreateOrGetDocument( + if _, err := srv.gov.CreateOrGetDocument( ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "", userID, "编制人", userID, "审核人", userID, "批准人", ); err != nil { @@ -71,7 +71,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test t.Fatal("missing run_id") } - hist1, err := srv.svc.ContextManager().GetChatHistory(ctx, "sess-idem-busy", 50, 0) + hist1, err := srv.ctxMgr.GetChatHistory(ctx, "sess-idem-busy", 50, 0) if err != nil { t.Fatalf("history1: %v", err) } @@ -80,13 +80,13 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test } // 活跃小队租约:模拟长任务仍占文档(旧路径会 409) - if _, err := srv.svc.DocGov().AcquireLease(ctx, docgov.LeaseAcquireRequest{ + if _, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{ DocID: docID, HolderType: docgov.LeaseHolderSquad, HolderID: first.RunID, TTL: time.Minute, }); err != nil { t.Fatalf("acquire squad lease: %v", err) } t.Cleanup(func() { - _, _ = srv.svc.DocGov().ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderSquad), first.RunID) + _, _ = srv.gov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderSquad), first.RunID) }) body2, _ := json.Marshal(createTaskRunRequest{ @@ -108,7 +108,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test t.Fatalf("want same run %s, got %s", first.RunID, second.RunID) } - hist2, err := srv.svc.ContextManager().GetChatHistory(ctx, "sess-idem-busy", 50, 0) + hist2, err := srv.ctxMgr.GetChatHistory(ctx, "sess-idem-busy", 50, 0) if err != nil { t.Fatalf("history2: %v", err) } diff --git a/internal/server/task_run_persistence_pg_test.go b/internal/server/task_run_persistence_pg_test.go index 65d8e566..2f715c67 100644 --- a/internal/server/task_run_persistence_pg_test.go +++ b/internal/server/task_run_persistence_pg_test.go @@ -88,7 +88,7 @@ func TestTaskRun_PersistenceAndReplayAfterRestart_PG(t *testing.T) { } // 模拟进程重启:新 Manager 仅挂载 Store - restarted := taskrun.NewManager(srv.ProjectWorkflowService(), srv.BizdevService().ContextManager()) + restarted := taskrun.NewManager(srv.ProjectWorkflowService(), bizdev.Snapshot(srv.BizdevService()).Ctx) restarted.SetStore(store) reloaded, err := restarted.GetRun(runID) diff --git a/internal/server/workflow_approval_hook.go b/internal/server/workflow_approval_hook.go index ef5e5f6f..abd85414 100644 --- a/internal/server/workflow_approval_hook.go +++ b/internal/server/workflow_approval_hook.go @@ -9,10 +9,10 @@ import ( ) func (s *Server) registerWorkflowApprovalHook() { - if s == nil || s.svc == nil || s.svc.DocGov() == nil || s.instSvc == nil || !s.instSvc.Available() { + if s == nil || s.gov == nil || s.instSvc == nil || !s.instSvc.Available() { return } - s.svc.DocGov().AddOnApprovedHook(s.onWorkflowDocumentApproved) + s.gov.AddOnApprovedHook(s.onWorkflowDocumentApproved) } func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) { diff --git a/internal/server/workflow_governance_pg_test.go b/internal/server/workflow_governance_pg_test.go index bad064d4..138ab804 100644 --- a/internal/server/workflow_governance_pg_test.go +++ b/internal/server/workflow_governance_pg_test.go @@ -84,7 +84,7 @@ func setupPGGovernanceServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *s t.Helper() cfg := configFromTestDatabaseURL(t) svc := bizdev.MustNewService(t, cfg) - if svc.Pool() == nil { + if bizdev.Snapshot(svc).Pool == nil { t.Skip("bizdev service not connected to postgres") } srv := server.NewServer(cfg, svc) @@ -194,7 +194,7 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) { // 4. 文档批准链(经 bizdev DocGov 引擎,触发 onWorkflowDocumentApproved hook) docID := fmt.Sprintf("doc_pg_p5_intake_%d", projectID) - docEngine := srv.BizdevService().DocGov() + docEngine := bizdev.Snapshot(srv.BizdevService()).Gov _, err = docEngine.CreateOrGetDocument(ctx, docID, projectID, "intake", "Intake Doc", "V0.1", "", creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver") if err != nil { t.Fatalf("create doc: %v", err) diff --git a/internal/server/workflow_governance_piagent_pg_test.go b/internal/server/workflow_governance_piagent_pg_test.go index d60f4653..84cf15f4 100644 --- a/internal/server/workflow_governance_piagent_pg_test.go +++ b/internal/server/workflow_governance_piagent_pg_test.go @@ -45,7 +45,7 @@ func setupPGPiAgentServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *serv } mergePiAgentE2EKbConfig(cfg) svc := bizdev.MustNewService(t, cfg) - if svc.Pool() == nil { + if bizdev.Snapshot(svc).Pool == nil { t.Skip("bizdev service not connected to postgres") } srv := server.NewServer(cfg, svc) @@ -207,7 +207,7 @@ func runPiAgentGovernanceE2E(t *testing.T, spec piAgentGovernanceCase) { } t.Logf("✓ [3. pi-agent %s] doc=%s title=%s elapsed=%s", spec.TaskKey, docID, taskRes.ArtifactTitle, time.Since(taskStart).Round(time.Second)) - docEngine := srv.BizdevService().DocGov() + docEngine := bizdev.Snapshot(srv.BizdevService()).Gov _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_pi_pg", false) _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") _, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok") diff --git a/scripts/backfill_task_run_projection.go b/scripts/backfill_task_run_projection.go index 02f431d4..0590ff90 100644 --- a/scripts/backfill_task_run_projection.go +++ b/scripts/backfill_task_run_projection.go @@ -44,7 +44,7 @@ func main() { srv := server.NewServer(cfg, svc) ctx := context.Background() - pwSvc := svc.ProjectWorkflow() + pwSvc := bizdev.Snapshot(svc).PW if pwSvc == nil { log.Fatal("project workflow service unavailable") } From 2a75abb39e55ec188a39fe179a7ecb40c3884438 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 13:08:25 +0000 Subject: [PATCH 2/3] refactor(bizdev): delete eight locator getters Composition roots already hold Snapshot ports; the interface and archtest now forbid putting Pool/DocGov/DocRevision/ContextManager/CAS/ProjectWorkflow/FreeformDoc/KB back on Service. Co-authored-by: Cursor --- internal/archtest/arch_test.go | 58 +++++++++++++++++++ internal/bizdev/service.go | 35 ----------- internal/bizdev/types.go | 6 -- .../instance_advance_governance_test.go | 8 --- .../pending_confirmation_reply_test.go | 7 --- internal/server/task_finalize.go | 6 +- internal/server/task_finalize_test.go | 4 +- 7 files changed, 62 insertions(+), 62 deletions(-) diff --git a/internal/archtest/arch_test.go b/internal/archtest/arch_test.go index 8bce1e52..35b04e39 100644 --- a/internal/archtest/arch_test.go +++ b/internal/archtest/arch_test.go @@ -1,7 +1,12 @@ package archtest import ( + "go/ast" + "go/parser" + "go/token" "os" + "path/filepath" + "strings" "testing" "time" ) @@ -48,3 +53,56 @@ func TestArchitectureBoundaries(t *testing.T) { } } } + +var forbiddenBizdevLocators = []string{ + "Pool", "DocGov", "DocRevision", "ContextManager", "CAS", "ProjectWorkflow", "FreeformDoc", "KB", +} + +func TestBizdevNoLocatorGetters(t *testing.T) { + root, err := ModuleRoot() + if err != nil { + t.Fatal(err) + } + dir := filepath.Join(root, "internal", "bizdev") + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { + name := info.Name() + return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") + }, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + forbidden := make(map[string]struct{}, len(forbiddenBizdevLocators)) + for _, name := range forbiddenBizdevLocators { + forbidden[name] = struct{}{} + } + for _, pkg := range pkgs { + for path, file := range pkg.Files { + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Recv == nil || fn.Name == nil { + continue + } + if _, hit := forbidden[fn.Name.Name]; !hit { + continue + } + if !recvIsBizdevService(fn.Recv) { + continue + } + t.Errorf("locator getter %s must not exist on *bizdev.Service (%s)", fn.Name.Name, relFile(root, path)) + } + } + } +} + +func recvIsBizdevService(recv *ast.FieldList) bool { + if recv == nil || len(recv.List) == 0 { + return false + } + expr := recv.List[0].Type + if star, ok := expr.(*ast.StarExpr); ok { + expr = star.X + } + ident, ok := expr.(*ast.Ident) + return ok && ident.Name == "Service" +} diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 07757016..78869523 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -238,25 +238,6 @@ 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 -} - // AgentOutputMaterializer is the production materialize path. Runtime injects // *docflow.Commands so candidate-ready/commit envelopes share the unique UoW. type AgentOutputMaterializer interface { @@ -279,22 +260,6 @@ func (s *Service) materializeAgentOutput(ctx context.Context, req docrevision.Ag return s.docRev.MaterializeAgentOutput(ctx, req) } -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{ diff --git a/internal/bizdev/types.go b/internal/bizdev/types.go index c8cb2ca4..e8f65d14 100644 --- a/internal/bizdev/types.go +++ b/internal/bizdev/types.go @@ -6,10 +6,8 @@ import ( "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" - "chat-agent-system/internal/kb" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" - "chat-agent-system/internal/storage" ) type TaskMeta = squad.TaskMeta @@ -157,8 +155,4 @@ type BizdevService interface { ExecuteStage(ctx context.Context, req StageRunRequest) (*StageRunResult, error) InspectWorkspace(ctx context.Context, workDir string) (*squad.WorkspaceState, error) ResumeOrModifyWorkspace(ctx context.Context, req squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) - ContextManager() *ctxmgr.ContextManager - CAS() *storage.CAS - DocGov() *docgov.GovernanceEngine - KB() *kb.Client } diff --git a/internal/coordinator/instance_advance_governance_test.go b/internal/coordinator/instance_advance_governance_test.go index 89c58892..f451d557 100644 --- a/internal/coordinator/instance_advance_governance_test.go +++ b/internal/coordinator/instance_advance_governance_test.go @@ -5,14 +5,10 @@ import ( "testing" "chat-agent-system/internal/app/stageflow" - "chat-agent-system/internal/ctxmgr" - "chat-agent-system/internal/docgov" "chat-agent-system/internal/intent" - "chat-agent-system/internal/kb" "chat-agent-system/internal/maturity" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/squad" - "chat-agent-system/internal/storage" ) type countingBizdev struct { @@ -34,10 +30,6 @@ func (c *countingBizdev) InspectWorkspace(_ context.Context, _ string) (*squad.W func (c *countingBizdev) ResumeOrModifyWorkspace(_ context.Context, _ squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) { return nil, nil } -func (c *countingBizdev) ContextManager() *ctxmgr.ContextManager { return nil } -func (c *countingBizdev) CAS() *storage.CAS { return nil } -func (c *countingBizdev) DocGov() *docgov.GovernanceEngine { return nil } -func (c *countingBizdev) KB() *kb.Client { return nil } func TestDialogueAdvanceStage_LiveInstanceDoesNotUseBizdev(t *testing.T) { store := projectinstance.NewMemoryStore() diff --git a/internal/coordinator/pending_confirmation_reply_test.go b/internal/coordinator/pending_confirmation_reply_test.go index 628e5d62..86e004a3 100644 --- a/internal/coordinator/pending_confirmation_reply_test.go +++ b/internal/coordinator/pending_confirmation_reply_test.go @@ -7,13 +7,10 @@ import ( "time" "chat-agent-system/internal/app/stageflow" - "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" "chat-agent-system/internal/intent" - "chat-agent-system/internal/kb" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" - "chat-agent-system/internal/storage" ) type pendingReplyBizdev struct { @@ -34,10 +31,6 @@ func (p *pendingReplyBizdev) InspectWorkspace(_ context.Context, _ string) (*squ func (p *pendingReplyBizdev) ResumeOrModifyWorkspace(_ context.Context, _ squad.ModifyWorkflowRequest) (*squad.SquadRunResult, error) { return nil, nil } -func (p *pendingReplyBizdev) ContextManager() *ctxmgr.ContextManager { return nil } -func (p *pendingReplyBizdev) CAS() *storage.CAS { return nil } -func (p *pendingReplyBizdev) DocGov() *docgov.GovernanceEngine { return nil } -func (p *pendingReplyBizdev) KB() *kb.Client { return nil } func TestSyncTriggerTask_PendingConfirmation_NotSavedCompleted(t *testing.T) { pending := &stageflow.TaskRunResult{ diff --git a/internal/server/task_finalize.go b/internal/server/task_finalize.go index bb1573bf..ec346813 100644 --- a/internal/server/task_finalize.go +++ b/internal/server/task_finalize.go @@ -30,9 +30,7 @@ func (s *Server) finalizeTaskRun(ctx context.Context, record *taskrun.RunRecord, cmds := s.taskCommands() if s != nil { cmds.PW = s.pwSvc - if s.svc != nil { - cmds.Ctx = s.ctxMgr - } + cmds.Ctx = s.ctxMgr } return cmds.Finalize(ctx, record, result) } @@ -49,7 +47,7 @@ func (s *Server) taskCommands() *taskrun.Commands { cmds.Env = s.cfg.Env cmds.StorageIdentity = "env:" + s.cfg.Env } - if s != nil && s.svc != nil { + if s != nil { cmds.Ctx = s.ctxMgr } return cmds diff --git a/internal/server/task_finalize_test.go b/internal/server/task_finalize_test.go index 75a8bb61..ae9a07c9 100644 --- a/internal/server/task_finalize_test.go +++ b/internal/server/task_finalize_test.go @@ -185,7 +185,7 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) { workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# async finalize\n"), 0o644) - srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)} + srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)} srv.taskRuns.SetExecutor( func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) { return &bizdev.TaskRunResult{ @@ -242,7 +242,7 @@ func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) { workDir := t.TempDir() _ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# head\n"), 0o644) - srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)} + srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)} srv.taskRuns.SetExecutor( func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) { return &bizdev.TaskRunResult{ From 6a9e3fb51ada3dc09e46f3b72947fb474dbc2d0c Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 13:10:47 +0000 Subject: [PATCH 3/3] docs(test): record D4 bizdev locator removal S1 report, archive the short-term task, and mark the getter contract closed. Co-authored-by: Cursor --- .../ops}/2026-09-15-bizdev-locator-removal.md | 14 ++-- .doc/ops/CURRENT.md | 4 +- .doc/status/FEATURE-MATRIX.md | 2 +- ...9-15-bizdev-locator-removal-test-report.md | 68 +++++++++++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) rename .doc/{ops/short-term => archive/ops}/2026-09-15-bizdev-locator-removal.md (75%) create mode 100644 .doc/test-reports/2026-09-15-bizdev-locator-removal-test-report.md diff --git a/.doc/ops/short-term/2026-09-15-bizdev-locator-removal.md b/.doc/archive/ops/2026-09-15-bizdev-locator-removal.md similarity index 75% rename from .doc/ops/short-term/2026-09-15-bizdev-locator-removal.md rename to .doc/archive/ops/2026-09-15-bizdev-locator-removal.md index f832924e..582b9edf 100644 --- a/.doc/ops/short-term/2026-09-15-bizdev-locator-removal.md +++ b/.doc/archive/ops/2026-09-15-bizdev-locator-removal.md @@ -1,6 +1,6 @@ --- title: "D4 bizdev locator 最终移除" -status: in_progress +status: done created: 2026-09-15 updated: 2026-09-15 owner: Cursor @@ -30,15 +30,15 @@ mutex: bizdev ## 步骤 -1. [ ] Snapshot + Runtime 端口 -2. [ ] 迁移 server/cmd/tests -3. [ ] 同批删除 8 getter -4. [ ] 报告合入 +1. [x] Snapshot + Runtime 端口 +2. [x] 迁移 server/cmd/tests +3. [x] 同批删除 8 getter +4. [x] 报告合入 ## 验收标准 -- [ ] 生产无 8 个 getter -- [ ] [S1 报告](../../test-reports/2026-09-15-bizdev-locator-removal-test-report.md) +- [x] 生产无 8 个 getter +- [x] [S1 报告](../../test-reports/2026-09-15-bizdev-locator-removal-test-report.md) ## 相关链接 diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 1c2dc0a8..6a71dc06 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,7 +5,7 @@ ## 焦点 -1. **TRACK-S**: [D4 locator 移除](./short-term/2026-09-15-bizdev-locator-removal.md)(D5/D6/D7a 已合入;本波删 8 个 getter) +1. **TRACK-S**: [D4 locator 已删](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md);下一步 D7b chat 生产 import 2. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针) 3. **CHAT**: [对话历史刷新活验已补](../test-reports/2026-09-13-chat-history-refresh-test-report.md)(项目 6252 闲聊刷新同集 + guest 403;编制任务链仍开放) @@ -67,7 +67,7 @@ | Cursor | doccontent-canonical-boundary | fix/doccontent-canonical-boundary | - | docgov | done | | Cursor | intent-bizdev-boundary | fix/intent-bizdev-boundary | - | intent | done | | Cursor | stageflow-type-ownership | fix/stageflow-type-ownership | - | stageflow | done | -| Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | in_progress | +| Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | done | > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index c0ec8ac4..d583bfd3 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -27,7 +27,7 @@ | PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` | | 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` | | CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) | CI `TEST_DATABASE_URL` 隔离库仍缺;文档运行时真实 UI 已按 UI_PHASE 分文件且 post-restart 断言最终 A1 正文。`-race` 全包并行 TempDir flake 为基建残留 | `ci` | -| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md) | E0A–E2 隔离库已过;dispatcher 默认 off。D5/D6/D7a/D7b-chatflow 已合入。删除闭包 API 须等 live。D4 getter 原子删除与 Track P 实现、FAV 另列。HTTP/PI 已有独立 remediation(Phase 2 只立项) | `architecture-docs` | +| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md);[D4](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md) | E0A–E2 隔离库已过;dispatcher 默认 off。D4–D7a 与 D7b 首批已合入。删除闭包 API 须等 live。`internal/chat` 生产仍 import bizdev(D7b 剩余)。Track P 实现、FAV 另列。HTTP/PI 已有独立 remediation(Phase 2 只立项) | `architecture-docs` | --- diff --git a/.doc/test-reports/2026-09-15-bizdev-locator-removal-test-report.md b/.doc/test-reports/2026-09-15-bizdev-locator-removal-test-report.md new file mode 100644 index 00000000..9a8b634f --- /dev/null +++ b/.doc/test-reports/2026-09-15-bizdev-locator-removal-test-report.md @@ -0,0 +1,68 @@ +# D4 bizdev locator 移除测试报告 + +## 一、 测试背景 (Background) + +`bizdev.Service` 对外暴露 8 个 locator getter(`Pool` / `DocGov` / `DocRevision` / `ContextManager` / `CAS` / `ProjectWorkflow` / `FreeformDoc` / `KB`),HTTP handler 与 CLI 在每次请求上穿过编排根取协作对象。D4 要求组合根一次性 `Snapshot`,由 Runtime/Server 持有端口,并原子删除 getter。 + +## 二、 测试目标 (Test Objectives) + +- 生产代码不再调用上述 8 个 getter。 +- `*bizdev.Service` 上不再存在这 8 个方法;`BizdevService` 接口同步去掉 locator。 +- archtest 禁止把 getter 加回去。 +- `go test -short ./...` 与 `arch-check --mode block` 全绿。 +- 不切事件 live;不删 hook API。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|-----| +| OS | Linux 7.0.0-31-generic | +| Go | go1.27.1 linux/amd64 | +| 分支 | `fix/bizdev-locator-removal` | +| 层级 | 表征/短测 + arch-check | +| 项目 143 | 只读 | + +## 四、 预期结果 (Expected Results) + +| 用例 | 期望 | +|------|------| +| Snapshot + Assemble | Runtime 字段来自 `bizdev.Snapshot(svc)`,不再走 getter | +| Server 端口 | handlers 使用 `s.gov` / `s.ctxMgr` / `s.rev` 等 | +| 删除 getter | `func (s *Service) DocGov()` 等 8 个方法不存在 | +| archtest | `TestBizdevNoLocatorGetters` PASS | +| 短测 | `go test -short ./...` PASS | + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 getter 门禁 + +- **测试输入 (Input)**:`go test -count=1 ./internal/archtest -run TestBizdevNoLocatorGetters` +- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/archtest 0.120s`(含于 arch-check) +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:~0.12s + +### 5.2 arch-check + +- **测试输入 (Input)**:`./scripts/arch-check.sh --mode block` +- **实际输出 (Actual Output)**:`arch-check block: production gate passed` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:~0.79s + +### 5.3 短测全量 + +- **测试输入 (Input)**:`unset TEST_DATABASE_URL DATABASE_URL CONTEXT_DB_HOST CONTEXT_DSN ALLOW_IN_MEMORY; go vet ./... && go test -short -count=1 ./...` +- **实际输出 (Actual Output)**:`go vet` 无输出;各包 `ok`(server 9.926s,cli 2.007s,其余 <2s) +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:约 12.2s + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +| 命令 | 结果 | +|------|------| +| `go vet ./...` | PASS | +| `go test -short -count=1 ./...` | PASS | +| `./scripts/arch-check.sh --mode block` | PASS | + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +D4 **PASS**。8 个 locator getter 已删除;组合根持有端口。`internal/chat` 生产包仍 import bizdev 跑 `CreateAndStart`(D7b 剩余批次)。Track P 仍只立项。不切 shared consumer live。