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 <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-15 13:00:33 +00:00
co-authored by Cursor
parent 92a9c6783d
commit 3f42149457
68 changed files with 458 additions and 315 deletions
+4 -3
View File
@@ -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` 探针) 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;编制任务链仍开放) 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 | event-e2-hook-guard | fix/event-e2-hook-guard | - | events | done |
| Cursor | doccontent-canonical-boundary | fix/doccontent-canonical-boundary | - | docgov | done | | Cursor | doccontent-canonical-boundary | fix/doccontent-canonical-boundary | - | docgov | done |
| Cursor | intent-bizdev-boundary | fix/intent-bizdev-boundary | - | intent | 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/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 > 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
@@ -78,7 +79,7 @@
| holder | phase | since | scope | queue | | 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` **phase**: `idle` | `merging` | `smoke` | `e2e`
@@ -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
@@ -73,13 +73,18 @@ imports:
- bizdev - bizdev
- config - config
- coordinator - coordinator
- ctxmgr
- docgov - docgov
- docrevision
- events - events
- freedoc
- intent - intent
- kb
- notification - notification
- observability - observability
- projectinstance - projectinstance
- projectworkflow - projectworkflow
- storage
- taskrun - taskrun
- workflowdef - workflowdef
- workspace - workspace
@@ -169,11 +174,13 @@ imports:
- events - events
- freedoc - freedoc
- intent - intent
- kb
- notification - notification
- observability - observability
- projectinstance - projectinstance
- projectworkflow - projectworkflow
- replyexport - replyexport
- storage
- squad - squad
- stream - stream
- stream/transport - stream/transport
+24 -24
View File
@@ -330,7 +330,7 @@ func main() {
func handleStageRun( func handleStageRun(
ctx context.Context, ctx context.Context,
svc bizdev.BizdevService, svc *bizdev.Service,
stageName string, stageName string,
project, customer, device, use, market, backend string, project, customer, device, use, market, backend string,
autoRepair bool, autoRepair bool,
@@ -455,7 +455,7 @@ func printAdvisorBrief(brief *bizdev.AdvisorBriefing) {
fmt.Println("==================================================================") 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) state, err := svc.InspectWorkspace(ctx, workDir)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "查看工作区失败: %v\n", err) fmt.Fprintf(os.Stderr, "查看工作区失败: %v\n", err)
@@ -586,7 +586,7 @@ func createCLIStreamListener(verbose bool) stream.Listener {
func handleChatMode( func handleChatMode(
ctx context.Context, ctx context.Context,
svc bizdev.BizdevService, svc *bizdev.Service,
chatQuery string, chatQuery string,
workDir string, workDir string,
project string, project string,
@@ -664,7 +664,7 @@ func handleChatMode(
rec := intent.NewRecognizer() rec := intent.NewRecognizer()
engine := coordinator.NewCoordinatorEngine(stageflow.New(svc)) engine := coordinator.NewCoordinatorEngine(stageflow.New(svc))
session := chat.NewSession("cli_chat_sess", userCtx, rec, engine) session := chat.NewSession("cli_chat_sess", userCtx, rec, engine)
session.CtxMgr = svc.ContextManager() session.CtxMgr = bizdev.Snapshot(svc).Ctx
// 单次提问模式 (支持脚本与自动化测试) // 单次提问模式 (支持脚本与自动化测试)
if chatQuery != "" { if chatQuery != "" {
@@ -805,8 +805,8 @@ func handleChatMode(
// 标杆案例独立检索 CLI 处理函数 // 标杆案例独立检索 CLI 处理函数
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func handleRecommendCases(ctx context.Context, svc bizdev.BizdevService, query string) { func handleRecommendCases(ctx context.Context, svc *bizdev.Service, query string) {
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
if ctxMgr == nil { if ctxMgr == nil {
fmt.Fprintf(os.Stderr, "错误: 上下文管理器未初始化\n") fmt.Fprintf(os.Stderr, "错误: 上下文管理器未初始化\n")
os.Exit(1) os.Exit(1)
@@ -851,8 +851,8 @@ func handleRecommendCases(ctx context.Context, svc bizdev.BizdevService, query s
// 受控文档治理 CLI 处理函数集 // 受控文档治理 CLI 处理函数集
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string) { func handleDocStatus(ctx context.Context, svc *bizdev.Service, docID string) {
doc, err := svc.DocGov().GetDocument(ctx, docID) doc, err := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "查询受控文档失败 [%s]: %v\n", docID, err) fmt.Fprintf(os.Stderr, "查询受控文档失败 [%s]: %v\n", docID, err)
os.Exit(1) 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 { if len(comments) > 0 {
fmt.Printf("\n批注与修改意见 (%d 条):\n", len(comments)) fmt.Printf("\n批注与修改意见 (%d 条):\n", len(comments))
for idx, c := range 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 { if len(audits) > 0 {
fmt.Printf("\n全生命周期审计轨迹 (%d 条):\n", len(audits)) fmt.Printf("\n全生命周期审计轨迹 (%d 条):\n", len(audits))
for idx, a := range audits { for idx, a := range audits {
@@ -899,9 +899,9 @@ func handleDocStatus(ctx context.Context, svc bizdev.BizdevService, docID string
fmt.Println("==================================================================") 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) 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 { if err != nil {
fmt.Fprintf(os.Stderr, "认领/查看文档失败: %v\n", err) fmt.Fprintf(os.Stderr, "认领/查看文档失败: %v\n", err)
os.Exit(1) 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:理由 // 格式: proposal_id:agree/reject:理由
parts := strings.SplitN(param, ":", 3) parts := strings.SplitN(param, ":", 3)
if len(parts) < 2 { 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" 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 { if err != nil {
fmt.Fprintf(os.Stderr, "拍板决策变更提案失败: %v\n", err) fmt.Fprintf(os.Stderr, "拍板决策变更提案失败: %v\n", err)
os.Exit(1) 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) { func handleDocSubmitReview(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().SubmitForReview(ctx, docID, userID, docgov.RoleAuthor, userName) doc, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, userID, docgov.RoleAuthor, userName)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "提交审核失败: %v\n", err) fmt.Fprintf(os.Stderr, "提交审核失败: %v\n", err)
os.Exit(1) 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) 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) { func handleDocReviewApprove(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().ReviewApprove(ctx, docID, userID, docgov.RoleReviewer, userName, "审核无误,同意提交批准") doc, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, userID, docgov.RoleReviewer, userName, "审核无误,同意提交批准")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "审核通过失败: %v\n", err) fmt.Fprintf(os.Stderr, "审核通过失败: %v\n", err)
os.Exit(1) 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) 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) parts := strings.SplitN(param, ":", 2)
if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" { if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n") 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]) docID := strings.TrimSpace(parts[0])
reason := strings.TrimSpace(parts[1]) 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 { if err != nil {
fmt.Fprintf(os.Stderr, "审核驳回失败: %v\n", err) fmt.Fprintf(os.Stderr, "审核驳回失败: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -982,8 +982,8 @@ func handleDocReviewReject(ctx context.Context, svc bizdev.BizdevService, param
userName, doc.DocID, doc.CurrentState, reason) userName, doc.DocID, doc.CurrentState, reason)
} }
func handleDocApproverApprove(ctx context.Context, svc bizdev.BizdevService, docID string, userID int64, userRoleStr string, userName string) { func handleDocApproverApprove(ctx context.Context, svc *bizdev.Service, docID string, userID int64, userRoleStr string, userName string) {
doc, err := svc.DocGov().ApproverApprove(ctx, docID, userID, docgov.RoleApprover, userName, "评估完备,批准受控发布") doc, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, userID, docgov.RoleApprover, userName, "评估完备,批准受控发布")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "批准失败: %v\n", err) fmt.Fprintf(os.Stderr, "批准失败: %v\n", err)
os.Exit(1) 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) 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) parts := strings.SplitN(param, ":", 2)
if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" { if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
fmt.Fprintf(os.Stderr, "错误: 驳回时必须提供驳回原因!格式: doc_id:驳回原因\n") 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]) docID := strings.TrimSpace(parts[0])
reason := strings.TrimSpace(parts[1]) 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 { if err != nil {
fmt.Fprintf(os.Stderr, "批准驳回失败: %v\n", err) fmt.Fprintf(os.Stderr, "批准驳回失败: %v\n", err)
os.Exit(1) os.Exit(1)
+1 -1
View File
@@ -27,7 +27,7 @@ func main() {
fmt.Fprintf(os.Stderr, "NewService: %v\n", err) fmt.Fprintf(os.Stderr, "NewService: %v\n", err)
os.Exit(1) os.Exit(1)
} }
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
if ctxMgr == nil { if ctxMgr == nil {
fmt.Fprintf(os.Stderr, "ContextManager 未初始化\n") fmt.Fprintf(os.Stderr, "ContextManager 未初始化\n")
os.Exit(1) os.Exit(1)
+37 -15
View File
@@ -19,16 +19,23 @@ import (
"chat-agent-system/internal/bizdev" "chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config" "chat-agent-system/internal/config"
"chat-agent-system/internal/coordinator" "chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov" "chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/events" "chat-agent-system/internal/events"
"chat-agent-system/internal/freedoc"
"chat-agent-system/internal/intent" "chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/notification" "chat-agent-system/internal/notification"
"chat-agent-system/internal/observability" "chat-agent-system/internal/observability"
"chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/taskrun" "chat-agent-system/internal/taskrun"
"chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workflowdef"
"chat-agent-system/internal/workspace" "chat-agent-system/internal/workspace"
"github.com/jackc/pgx/v5/pgxpool"
) )
const defaultReminderPollInterval = 15 * time.Minute const defaultReminderPollInterval = 15 * time.Minute
@@ -56,6 +63,13 @@ type Runtime struct {
ChatLog chatflow.ChatLog ChatLog chatflow.ChatLog
Assembler chatflow.ContextAssembler Assembler chatflow.ContextAssembler
Docflow *docflow.Commands 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 mu sync.Mutex
running bool running bool
@@ -89,35 +103,36 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
rec := intent.NewRecognizer() rec := intent.NewRecognizer()
dialogueBackend := agent.NewBackend(cfg.AgentBackend, cfg) dialogueBackend := agent.NewBackend(cfg.AgentBackend, cfg)
coordEngine := coordinator.NewCoordinatorEngineWithDialogue(nil, dialogueBackend, cfg.DataDir) 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) stores, storeErr := auth.NewStores(context.Background(), pool, cfg)
if storeErr != nil { if storeErr != nil {
return nil, fmt.Errorf("auth store: %w", storeErr) return nil, fmt.Errorf("auth store: %w", storeErr)
} }
authSvc := auth.NewService(stores.Auth, stores.Sessions, cfg) authSvc := auth.NewService(stores.Auth, stores.Sessions, cfg)
pwSvc := svc.ProjectWorkflow() pwSvc := parts.PW
if pwSvc == nil { if pwSvc == nil {
pwSvc = projectworkflow.NewService(projectworkflow.NewStore(nil), nil) pwSvc = projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
} }
authzSvc := authz.NewService(pool) authzSvc := authz.NewService(pool)
wfDefSvc := workflowdef.NewService(pool, svc.CAS()) wfDefSvc := workflowdef.NewService(pool, parts.CAS)
instStore := projectinstance.NewStore(pool) instStore := projectinstance.NewStore(pool)
var instSvc *projectinstance.Service var instSvc *projectinstance.Service
if wfDefSvc.Available() { if wfDefSvc.Available() {
docChecker := projectinstance.NewDocGovChecker(svc.DocGov()) docChecker := projectinstance.NewDocGovChecker(parts.Gov)
instSvc = projectinstance.NewService(instStore, wfDefSvc.Store(), pwSvc.Store(), docChecker) instSvc = projectinstance.NewService(instStore, wfDefSvc.Store(), pwSvc.Store(), docChecker)
if pool != nil { if pool != nil {
instSvc.SetActiveMemberLookup(projectinstance.NewPGMemberLookup(pool)) instSvc.SetActiveMemberLookup(projectinstance.NewPGMemberLookup(pool))
} }
} }
taskMgr := taskrun.NewManager(pwSvc, svc.ContextManager()) taskMgr := taskrun.NewManager(pwSvc, parts.Ctx)
if pool != nil { if pool != nil {
taskMgr.SetStore(taskrun.NewPGStore(pool)) taskMgr.SetStore(taskrun.NewPGStore(pool))
} }
wsCap := workspace.NewCapability(cfg.DataDir, workspace.NewLeaseTable()) wsCap := workspace.NewCapability(cfg.DataDir, workspace.NewLeaseTable())
cmds := &taskrun.Commands{ cmds := &taskrun.Commands{
Ctx: svc.ContextManager(), Ctx: parts.Ctx,
PW: pwSvc, PW: pwSvc,
Env: cfg.Env, Env: cfg.Env,
StorageIdentity: "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) stages := stageflow.New(svc)
coordEngine.SetStageTaskRunner(stages) coordEngine.SetStageTaskRunner(stages)
coordEngine.SetContextServices(svc.ContextManager(), svc.KB()) coordEngine.SetContextServices(parts.Ctx, parts.KB)
if instSvc != nil && instSvc.Available() { if instSvc != nil && instSvc.Available() {
coordEngine.SetInstanceService(instSvc) coordEngine.SetInstanceService(instSvc)
} }
@@ -152,8 +167,15 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
Commands: cmds, Commands: cmds,
Rec: rec, Rec: rec,
Stageflow: stages, 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 { rt.DocNotify.SkipWrite = func(ctx context.Context) bool {
return rt.WriterLive(ctx, events.ConsumerNotify) return rt.WriterLive(ctx, events.ConsumerNotify)
} }
@@ -162,7 +184,7 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
envName = cfg.Env envName = cfg.Env
} }
rt.Docflow = &docflow.Commands{ rt.Docflow = &docflow.Commands{
Gov: svc.DocGov(), Rev: svc.DocRevision(), Auth: docflow.DutyAuthorizer(), Gov: parts.Gov, Rev: parts.Rev, Auth: docflow.DutyAuthorizer(),
Environment: envName, Environment: envName,
} }
if pool != nil { if pool != nil {
@@ -179,8 +201,8 @@ func (r *Runtime) bindDocumentNotify() {
if r == nil || r.notifyBound { if r == nil || r.notifyBound {
return return
} }
if r.Biz != nil && r.Biz.DocRevision() != nil { if r.Biz != nil && r.Rev != nil {
if docnotify.Bind(r.Biz.DocRevision(), r.DocNotify) { if docnotify.Bind(r.Rev, r.DocNotify) {
r.subscribers++ r.subscribers++
} }
} }
@@ -188,25 +210,25 @@ func (r *Runtime) bindDocumentNotify() {
} }
func (r *Runtime) bindWorkflowApprovalHook() { 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 r.hookBound = true
return return
} }
r.Biz.DocGov().AddOnApprovedHook(r.onWorkflowDocumentApproved) r.Gov.AddOnApprovedHook(r.onWorkflowDocumentApproved)
r.hookBound = true r.hookBound = true
r.subscribers++ r.subscribers++
} }
// WriterLive reports whether the durable consumer already owns the visible write. // WriterLive reports whether the durable consumer already owns the visible write.
func (r *Runtime) WriterLive(ctx context.Context, consumer string) bool { func (r *Runtime) WriterLive(ctx context.Context, consumer string) bool {
if r == nil || r.Biz == nil { if r == nil {
return false return false
} }
env := "" env := ""
if r.Cfg != nil { if r.Cfg != nil {
env = r.Cfg.Env 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) { func (r *Runtime) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) {
-1
View File
@@ -32,7 +32,6 @@ func TestUseCaseTypeIdentity(t *testing.T) {
if TaskStatusFailed != "failed" || TaskStatusPendingAuthorConfirmation != "pending_author_confirmation" { if TaskStatusFailed != "failed" || TaskStatusPendingAuthorConfirmation != "pending_author_confirmation" {
t.Fatalf("status tokens drifted") t.Fatalf("status tokens drifted")
} }
}
biz := &stubBiz{} biz := &stubBiz{}
httpUC := New(biz) httpUC := New(biz)
cliUC := New(biz) cliUC := New(biz)
+4 -4
View File
@@ -14,7 +14,7 @@ import (
func activityTitles(t *testing.T, svc *Service, projectID int64) []string { func activityTitles(t *testing.T, svc *Service, projectID int64) []string {
t.Helper() t.Helper()
pCtx, err := svc.ContextManager().GetProjectContext(context.Background(), projectID) pCtx, err := svc.ctxMgr.GetProjectContext(context.Background(), projectID)
if err != nil { if err != nil {
t.Fatalf("GetProjectContext: %v", err) 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) { func TestExecuteTask_MaterializeFailure_DoesNotRecordCompletedActivity(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background() ctx := context.Background()
svc.DocRevision().AgentCommit = false svc.docRev.AgentCommit = false
projectID := int64(5601) projectID := int64(5601)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{ res, err := svc.ExecuteTask(ctx, TaskRunRequest{
@@ -60,10 +60,10 @@ func TestExecuteTask_PendingConfirmation_RecordsAwaitingNotCompleted(t *testing.
t.Fatal(err) t.Fatal(err)
} }
base := "# base\n\n法规引用 GB9706\n" 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) 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) t.Fatal(err)
} }
+3 -3
View File
@@ -40,10 +40,10 @@ func TestService_BlendedContextInjection(t *testing.T) {
FocusAreas: []string{"cost_budget", "timeline"}, FocusAreas: []string{"cost_budget", "timeline"},
OutputFormatPref: "ppt_bullets", OutputFormatPref: "ppt_bullets",
} }
_ = svc.ContextManager().UpdateUserPreference(ctx, pref) _ = svc.ctxMgr.UpdateUserPreference(ctx, pref)
// 2. 记录先验里程碑决策 // 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 后端进行快速逻辑验证) // 3. 执行任务 (使用 stub 后端进行快速逻辑验证)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{ res, err := svc.ExecuteTask(ctx, TaskRunRequest{
@@ -89,7 +89,7 @@ func TestService_BlendedContextInjection(t *testing.T) {
} }
// 6. 验证任务完成后自动记录了里程碑活动 // 6. 验证任务完成后自动记录了里程碑活动
pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) pCtx, err := svc.ctxMgr.GetProjectContext(ctx, projectID)
if err != nil { if err != nil {
t.Fatalf("get project context failed: %v", err) t.Fatalf("get project context failed: %v", err)
} }
+9 -9
View File
@@ -47,7 +47,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) {
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte(histOutput), 0644) _ = 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超高清荧光内窥镜系统立项与可行性报告", ctx, docID, 99, "feasibility_reg", "4K超高清荧光内窥镜系统立项与可行性报告",
"V0.5_feasibility", workDir, "V0.5_feasibility", workDir,
101, "张工", 201, "李审核", 301, "王总监", 101, "张工", 201, "李审核", 301, "王总监",
@@ -57,12 +57,12 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) {
} }
// 模拟流转至 approved 终态 // 模拟流转至 approved 终态
docRecord, _ := svc.DocGov().GetDocument(ctx, docID) docRecord, _ := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID)
docRecord.IsTemplateOnly = false docRecord.IsTemplateOnly = false
docRecord.WorkDir = workDir docRecord.WorkDir = workDir
_, _ = svc.DocGov().SubmitForReview(ctx, docID, 101, docgov.RoleAuthor, "张工") _, _ = bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, 101, docgov.RoleAuthor, "张工")
_, _ = svc.DocGov().ReviewApprove(ctx, docID, 201, docgov.RoleReviewer, "李审核", "无误") _, _ = bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, 201, docgov.RoleReviewer, "李审核", "无误")
docApproved, err := svc.DocGov().ApproverApprove(ctx, docID, 301, docgov.RoleApprover, "王总监", "批准生效") docApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, 301, docgov.RoleApprover, "王总监", "批准生效")
if err != nil { if err != nil {
t.Fatalf("ApproverApprove failed: %v", err) 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 { if err != nil {
t.Fatalf("IndexApprovedDocument manual failed: %v", err) t.Fatalf("IndexApprovedDocument manual failed: %v", err)
} }
@@ -81,7 +81,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) {
// 校验自动切片与向量化是否触发入库 // 校验自动切片与向量化是否触发入库
time.Sleep(50 * time.Millisecond) 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超高清荧光内窥镜系统立项与可行性报告", Query: "4K超高清荧光内窥镜系统立项与可行性报告",
Limit: 3, Limit: 3,
MinScore: 0.10, MinScore: 0.10,
@@ -101,7 +101,7 @@ func TestBizDev_CaseRecommendation_FullLink_E2E(t *testing.T) {
rec := intent.NewRecognizer() rec := intent.NewRecognizer()
engine := coordinator.NewCoordinatorEngine(svc) engine := coordinator.NewCoordinatorEngine(svc)
engine.SetContextServices(svc.ContextManager(), svc.KB()) engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB)
userCtx := &coordinator.UserProjectContext{ userCtx := &coordinator.UserProjectContext{
SnapshotID: "ctx_new_consult", 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 := chat.NewSession("sess_consult_e2e", userCtx, rec, engine)
session.CtxMgr = svc.ContextManager() session.CtxMgr = bizdev.Snapshot(svc).Ctx
chatQuery := "我们之前做过类似的硬管内窥镜摄像系统吗?型检和报价大概怎么定的?" chatQuery := "我们之前做过类似的硬管内窥镜摄像系统吗?型检和报价大概怎么定的?"
resp, err := session.SendMessage(ctx, chatQuery) resp, err := session.SendMessage(ctx, chatQuery)
+7 -7
View File
@@ -54,7 +54,7 @@ func TestResolveAndValidateDocID_ExistingDocMismatchRejected(t *testing.T) {
ctx := context.Background() ctx := context.Background()
docID := "doc_proj56_research" docID := "doc_proj56_research"
_, err := svc.DocGov().CreateOrGetDocument( _, err := svc.docGov.CreateOrGetDocument(
ctx, docID, 56, "research", "调研报告", "V0.5", "", ctx, docID, 56, "research", "调研报告", "V0.5", "",
1, "张工", 2, "李主管", 3, "王总监", 1, "张工", 2, "李主管", 3, "王总监",
) )
@@ -87,7 +87,7 @@ func TestResolveAndValidateDocID_ClientDocIDCannotOverrideTaskKey(t *testing.T)
ctx := context.Background() ctx := context.Background()
docID := "doc_proj6109_intake" docID := "doc_proj6109_intake"
_, err := svc.DocGov().CreateOrGetDocument( _, err := svc.docGov.CreateOrGetDocument(
ctx, docID, 6109, "intake", "用户初始需求收集表", "V0.1", "", ctx, docID, 6109, "intake", "用户初始需求收集表", "V0.1", "",
1, "张工", 2, "李主管", 3, "王总监", 1, "张工", 2, "李主管", 3, "王总监",
) )
@@ -122,7 +122,7 @@ func TestResolveAndValidateDocID_ExistingDocAccepted(t *testing.T) {
ctx := context.Background() ctx := context.Background()
docID := "doc_proj56_research" docID := "doc_proj56_research"
_, err := svc.DocGov().CreateOrGetDocument( _, err := svc.docGov.CreateOrGetDocument(
ctx, docID, 56, "research", "调研报告", "V0.5", "", ctx, docID, 56, "research", "调研报告", "V0.5", "",
1, "张工", 2, "李主管", 3, "王总监", 1, "张工", 2, "李主管", 3, "王总监",
) )
@@ -154,7 +154,7 @@ func TestCreateOrGetDocument_ConcurrentSameDocID(t *testing.T) {
} }
ch := make(chan outcome, 2) ch := make(chan outcome, 2)
create := func() { create := func() {
doc, err := svc.DocGov().CreateOrGetDocument( doc, err := svc.docGov.CreateOrGetDocument(
ctx, docID, 77, "research", "调研报告", "V0.5", "", ctx, docID, 77, "research", "调研报告", "V0.5", "",
1, "张工", 2, "李主管", 3, "王总监", 1, "张工", 2, "李主管", 3, "王总监",
) )
@@ -177,7 +177,7 @@ func TestCreateOrGetDocument_ConcurrentSameDocID(t *testing.T) {
if first.id != docID || second.id != docID { if first.id != docID || second.id != docID {
t.Fatalf("doc ids mismatch: first=%q second=%q want %q", first.id, 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 { if err != nil || doc == nil {
t.Fatalf("expected single persisted doc %s, got err=%v doc=%v", docID, err, doc) 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 { if res.DocRecord != nil {
t.Fatalf("empty docID must not create a document record, got %#v", res.DocRecord) 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) 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) { func TestExecuteTask_MaterializeFailureSurfacesInTaskRunResult(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background() 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{ res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "research", TaskKey: "research",
+11 -11
View File
@@ -87,7 +87,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
// 阶段 3:编制负责人查看已生成的正式交付物 // 阶段 3:编制负责人查看已生成的正式交付物
// 预期:触发认领机制,author_claimed 变为 true! // 预期:触发认领机制,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 { if err != nil {
t.Fatalf("RecordDocumentViewed failed: %v", err) t.Fatalf("RecordDocumentViewed failed: %v", err)
} }
@@ -122,7 +122,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
// 预期:提案变为 approved_by_author,并放行修改 // 预期:提案变为 approved_by_author,并放行修改
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
propID := res3.PendingProposal.ProposalID 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 { if err != nil {
t.Fatalf("DecideModificationProposal failed: %v", err) t.Fatalf("DecideModificationProposal failed: %v", err)
} }
@@ -141,7 +141,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
// 阶段 6:送审、审核批注、审核驳回(必填原因)退回编制草稿、重新送审通过 // 阶段 6:送审、审核批注、审核驳回(必填原因)退回编制草稿、重新送审通过
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// 6.1 编制人送审 // 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 { if err != nil {
t.Fatalf("SubmitForReview failed: %v", err) t.Fatalf("SubmitForReview failed: %v", err)
} }
@@ -150,25 +150,25 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
} }
// 6.2 审核人在锁定状态下严禁直接修改正文 // 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) { if !errors.Is(err, docgov.ErrDocumentLocked) {
t.Fatalf("reviewer modifying locked document should return ErrDocumentLocked, got: %v", err) t.Fatalf("reviewer modifying locked document should return ErrDocumentLocked, got: %v", err)
} }
// 6.3 审核人添加修改建议 // 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 { if err != nil {
t.Fatalf("AddComment failed: %v", err) t.Fatalf("AddComment failed: %v", err)
} }
// 6.4 审核人驳回 (验证原因必填) // 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 { if err != docgov.ErrRejectReasonRequired {
t.Fatalf("ReviewReject without reason should fail with 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 { if err != nil {
t.Fatalf("ReviewReject with reason failed: %v", err) t.Fatalf("ReviewReject with reason failed: %v", err)
} }
@@ -177,8 +177,8 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
} }
// 6.5 编制人修订后重新送审 // 6.5 编制人修订后重新送审
_, _ = svc.DocGov().RecordDocumentEdited(ctx, docID, authorID, docgov.RoleAuthor, authorName) _, _ = bizdev.Snapshot(svc).Gov.RecordDocumentEdited(ctx, docID, authorID, docgov.RoleAuthor, authorName)
docResubmitted, err := svc.DocGov().SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) docResubmitted, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName)
if err != nil { if err != nil {
t.Fatalf("resubmit review failed: %v", err) t.Fatalf("resubmit review failed: %v", err)
} }
@@ -187,7 +187,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
} }
// 6.6 审核人审核通过 -> 进入 approving // 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 { if err != nil {
t.Fatalf("ReviewApprove failed: %v", err) t.Fatalf("ReviewApprove failed: %v", err)
} }
@@ -198,7 +198,7 @@ func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// 阶段 7:批准人最终批准 -> approved 终态归档锁定 // 阶段 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 { if err != nil {
t.Fatalf("ApproverApprove failed: %v", err) t.Fatalf("ApproverApprove failed: %v", err)
} }
+14 -14
View File
@@ -52,22 +52,22 @@ func executeStageDrainingPending(
t.Fatalf("pending stage missing DocRecord: %+v", res) t.Fatalf("pending stage missing DocRecord: %+v", res)
} }
docID := pending.DocRecord.DocID docID := pending.DocRecord.DocID
st, err := svc.DocRevision().GetRevisionState(ctx, docID) st, err := bizdev.Snapshot(svc).Rev.GetRevisionState(ctx, docID)
if err != nil { if err != nil {
t.Fatalf("GetRevisionState(%s): %v", docID, err) 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, Decision: "commit", Confirmed: true, ActorUserID: authorID,
ExpectedInputSHA: st.HeadSHA256, ExpectedInputSHA: st.HeadSHA256,
}) })
if err != nil { if err != nil {
t.Fatalf("CommitAgentVersion(%s,#%d): %v", docID, pending.PendingAgentCandidateID, err) 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) t.Fatalf("ProjectOutputMarkdown(%s): %v", docID, err)
} }
if st2 != nil && st2.HeadSHA256 != "" { 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) 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) resV01.StageTitle, resV01.AdvisorBrief.MaturityHeadline, resV01.AdvisorBrief.MaturityScore)
// 验证项目事实黑板已沉淀 V0.1 提取事实 // 验证项目事实黑板已沉淀 V0.1 提取事实
pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) pCtx, err := bizdev.Snapshot(svc).Ctx.GetProjectContext(ctx, projectID)
if err != nil || pCtx == nil { if err != nil || pCtx == nil {
t.Fatalf("get project context failed: %v", err) t.Fatalf("get project context failed: %v", err)
} }
@@ -196,7 +196,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
// 验证 feasibility_reg 受控文档已建立 // 验证 feasibility_reg 受控文档已建立
docID := "doc_proj1001_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 { if err != nil || docRec == nil {
t.Fatalf("get docgov record failed: %v", err) 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(">>> 阶段 3: 编制负责人查阅正文触发认领 (Author Claims Document) <<<")
t.Log("================================================================================") 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 { if err != nil {
t.Fatalf("RecordDocumentViewed failed: %v", err) 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(">>> 阶段 5: 编制负责人拍板同意变更提案 (Author Approves Proposal) <<<")
t.Log("================================================================================") t.Log("================================================================================")
proposalDecided, err := svc.DocGov().DecideModificationProposal( proposalDecided, err := bizdev.Snapshot(svc).Gov.DecideModificationProposal(
ctx, ctx,
propID, propID,
authorID, authorID,
@@ -283,7 +283,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
} }
for _, art := range resV05Updated.SubArtifacts { for _, art := range resV05Updated.SubArtifacts {
if art.TaskKey == "feasibility_reg" { if art.TaskKey == "feasibility_reg" {
_ = svc.DocGov().SetDocumentWorkDir(ctx, docID, art.WorkDir) _ = bizdev.Snapshot(svc).Gov.SetDocumentWorkDir(ctx, docID, art.WorkDir)
break break
} }
} }
@@ -293,7 +293,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
t.Log("================================================================================") t.Log("================================================================================")
// 1. 编制人送审 // 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 { if err != nil {
t.Fatalf("SubmitForReview failed: %v", err) 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) t.Logf("✓ 编制人提交送审: doc_state=%s, is_locked=%v", docReviewing.CurrentState, docReviewing.IsLocked)
// 2. 审核人添加批注意见 // 2. 审核人添加批注意见
comm, err := svc.DocGov().AddComment( comm, err := bizdev.Snapshot(svc).Gov.AddComment(
ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer,
"## 关键性能与型检预估", docgov.CommentSuggestion, "## 关键性能与型检预估", docgov.CommentSuggestion,
"针对荧光双波长光学滤光衰减特性,建议在送检前增加YY 0068.1硬管内窥镜机械强度摸底", "针对荧光双波长光学滤光衰减特性,建议在送检前增加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) t.Logf("✓ 审核人添加章节批注: comment_id=%d, type=%s, section=%s", comm.ID, comm.CommentType, comm.TargetSection)
// 3. 审核人审核通过 // 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 { if err != nil {
t.Fatalf("ReviewApprove failed: %v", err) 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) t.Logf("✓ 审核人审核通过: doc_state=%s, is_locked=%v", docApproving.CurrentState, docApproving.IsLocked)
// 4. 批准人终态批准归档 (自动触发 onApprovedHook 切片向量化沉淀为标杆案例) // 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 { if err != nil {
t.Fatalf("ApproverApprove failed: %v", err) 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(">>> 阶段 10: 验证受控批准交付物沉淀为标杆案例,并在后续跨项目咨询中自动召回 <<<")
t.Log("================================================================================") t.Log("================================================================================")
recCases, err := svc.ContextManager().SearchSimilarCases(ctx, bizdev.CaseSearchOptions{ recCases, err := bizdev.Snapshot(svc).Ctx.SearchSimilarCases(ctx, bizdev.CaseSearchOptions{
Query: "4K超高清荧光硬管内窥镜摄像系统", Query: "4K超高清荧光硬管内窥镜摄像系统",
Limit: 3, Limit: 3,
MinScore: 0.05, MinScore: 0.05,
+1 -1
View File
@@ -41,7 +41,7 @@ func TestResolveMaterials_RejectEmptyBinary(t *testing.T) {
func TestResolveMaterials_ProjectAssetPool(t *testing.T) { func TestResolveMaterials_ProjectAssetPool(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir()} cfg := &config.Config{DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg) 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") ref, err := cas.StoreFile(context.Background(), 42, 1, "pool.txt", "material", []byte("pool asset body"), "text/plain")
if err != nil { if err != nil {
t.Fatalf("StoreFile failed: %v", err) t.Fatalf("StoreFile failed: %v", err)
+35
View File
@@ -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
}
+7 -7
View File
@@ -55,18 +55,18 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te
t.Fatal(err) t.Fatal(err)
} }
base := "# 正式受控 head\n\n本正文为账本事实源,质量失败后不得被未过门禁稿覆盖。\n" 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
stBefore, err := svc.DocRevision().GetRevisionState(ctx, docID) stBefore, err := svc.docRev.GetRevisionState(ctx, docID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
versBefore, err := svc.DocRevision().ListVersions(ctx, docID) versBefore, err := svc.docRev.ListVersions(ctx, docID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -122,7 +122,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te
t.Fatal("failure summary body must remain visible") 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -132,7 +132,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te
if afterDoc.ContentSHA256 != beforeSHA { if afterDoc.ContentSHA256 != beforeSHA {
t.Fatalf("formal content hash mutated: before=%q after=%q", beforeSHA, afterDoc.ContentSHA256) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -143,7 +143,7 @@ func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *te
if stAfter.PendingCandidateID != nil { if stAfter.PendingCandidateID != nil {
t.Fatalf("quality_failed must not create pending candidate: %v", *stAfter.PendingCandidateID) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+11
View File
@@ -46,6 +46,7 @@ type Service struct {
schemaVersion int64 schemaVersion int64
stageGate StageAccessGate stageGate StageAccessGate
materializer AgentOutputMaterializer materializer AgentOutputMaterializer
parts Parts
// stageTaskHook 仅测试注入:覆盖 ExecuteStage 子任务执行,验证链中止边界。 // stageTaskHook 仅测试注入:覆盖 ExecuteStage 子任务执行,验证链中止边界。
stageTaskHook func(context.Context, TaskRunRequest) (*TaskRunResult, error) stageTaskHook func(context.Context, TaskRunRequest) (*TaskRunResult, error)
// createDocumentHook 仅测试注入:覆盖账本创建失败路径。 // createDocumentHook 仅测试注入:覆盖账本创建失败路径。
@@ -151,6 +152,16 @@ func NewService(cfg *config.Config) (*Service, error) {
pool: pool, pool: pool,
schemaVersion: schemaVersion, 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) { docGov.SetOnApprovedHook(func(ctx context.Context, doc *docgov.DocumentRecord) {
+3 -3
View File
@@ -25,10 +25,10 @@ func TestExecuteStage_PendingAgentCandidate_StopsChainWithoutAbsorbingFacts(t *t
} }
base := "# research base\n\n法规引用 GB9706\n敏感验收标准 A\n" 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) 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) 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. // 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 { if err != nil {
t.Fatalf("GetProjectContext: %v", err) t.Fatalf("GetProjectContext: %v", err)
} }
+1 -1
View File
@@ -147,7 +147,7 @@ func TestExecuteStage_QualityFailed_StopsChainWithoutAbsorbingFactsOrPriorDoc(t
t.Fatal("quality stop must not write completed stage workspace.json") 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 { if err != nil {
t.Fatalf("GetProjectContext: %v", err) t.Fatalf("GetProjectContext: %v", err)
} }
+5 -5
View File
@@ -12,17 +12,17 @@ func TestExecuteTask_TestManagerAfterApprovedUpstream(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background() ctx := context.Background()
srDocID := "doc_proj1001_system_reqs" srDocID := "doc_proj1001_system_reqs"
_, err := svc.DocGov().CreateOrGetDocument( _, err := svc.docGov.CreateOrGetDocument(
ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "", ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "",
2, "张工", 3, "李主管", 4, "王总监", 2, "张工", 3, "李主管", 4, "王总监",
) )
if err != nil { if err != nil {
t.Fatalf("CreateOrGetDocument: %v", err) t.Fatalf("CreateOrGetDocument: %v", err)
} }
_ = svc.DocGov().MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) _ = svc.docGov.MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false)
_, _ = svc.DocGov().SubmitForReview(ctx, srDocID, 2, "author", "张工") _, _ = svc.docGov.SubmitForReview(ctx, srDocID, 2, "author", "张工")
_, _ = svc.DocGov().ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") _, _ = svc.docGov.ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意")
_, err = svc.DocGov().ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") _, err = svc.docGov.ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准")
if err != nil { if err != nil {
t.Fatalf("ApproverApprove: %v", err) t.Fatalf("ApproverApprove: %v", err)
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import (
func TestExecuteAction_DocGovReviewApprove(t *testing.T) { func TestExecuteAction_DocGovReviewApprove(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir()} cfg := &config.Config{DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
docGov := svc.DocGov() docGov := bizdev.Snapshot(svc).Gov
ctx := context.Background() ctx := context.Background()
docID := "doc_proj1_research" docID := "doc_proj1_research"
+1 -1
View File
@@ -101,7 +101,7 @@ func TestLongTaskGovernance_QualityE2E(t *testing.T) {
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
dialogueBackend := agent.NewBackend("pi-agent", cfg) dialogueBackend := agent.NewBackend("pi-agent", cfg)
engine := coordinator.NewCoordinatorEngineWithDialogue(svc, dialogueBackend, cfg.DataDir) 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() rec := intent.NewRecognizer()
reportRoot := qualityReportDir(t) reportRoot := qualityReportDir(t)
ctx := context.Background() ctx := context.Background()
+3 -3
View File
@@ -270,7 +270,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) {
## 关键性能与型检预估 ## 关键性能与型检预估
重点满足 GB 9706.1-2020 及 YY 0644 专用安全规范,型检周期约 5 个月。` 重点满足 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 { if err != nil {
t.Fatalf("IndexApprovedDocument failed: %v", err) t.Fatalf("IndexApprovedDocument failed: %v", err)
} }
@@ -278,7 +278,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) {
// 2. 发起新会话咨询 // 2. 发起新会话咨询
rec := intent.NewRecognizer() rec := intent.NewRecognizer()
engine := coordinator.NewCoordinatorEngine(svc) engine := coordinator.NewCoordinatorEngine(svc)
engine.SetContextServices(svc.ContextManager(), svc.KB()) engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB)
userCtx := &coordinator.UserProjectContext{ userCtx := &coordinator.UserProjectContext{
Project: coordinator.ProjectInfo{ Project: coordinator.ProjectInfo{
Name: "超声软组织刀研发咨询", Name: "超声软组织刀研发咨询",
@@ -294,7 +294,7 @@ func TestChatSession_AdvisorCaseRecommendation(t *testing.T) {
} }
session := chat.NewSession("sess_case_recommend", userCtx, rec, engine) session := chat.NewSession("sess_case_recommend", userCtx, rec, engine)
session.CtxMgr = svc.ContextManager() session.CtxMgr = bizdev.Snapshot(svc).Ctx
resp, err := session.SendMessage(ctx, "我们之前做过类似的超声手术设备吗?注册和型检要注意什么?") resp, err := session.SendMessage(ctx, "我们之前做过类似的超声手术设备吗?注册和型检要注意什么?")
if err != nil { if err != nil {
+1 -1
View File
@@ -38,7 +38,7 @@ func TestChatSession_AssistantMetadataTurnIntentAndReplySource(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir()} cfg := &config.Config{DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
mgr := svc.ContextManager() mgr := bizdev.Snapshot(svc).Ctx
sess := chat.NewSession("sess_test_turn_intent", userCtx, rec, engine) sess := chat.NewSession("sess_test_turn_intent", userCtx, rec, engine)
sess.CtxMgr = mgr sess.CtxMgr = mgr
+2 -2
View File
@@ -66,7 +66,7 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
return return
} }
cas := s.svc.CAS() cas := s.cas
if cas == nil { if cas == nil {
http.Error(w, "storage unavailable", http.StatusServiceUnavailable) http.Error(w, "storage unavailable", http.StatusServiceUnavailable)
return return
@@ -96,7 +96,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
if _, ok := s.requireProjectAccess(w, r, projectID); !ok { if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
return return
} }
cas := s.svc.CAS() cas := s.cas
if cas == nil { if cas == nil {
writeJSONResponse(w, http.StatusOK, []projectAssetResponse{}) writeJSONResponse(w, http.StatusOK, []projectAssetResponse{})
return return
+2 -2
View File
@@ -76,11 +76,11 @@ func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request
// assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。 // assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。
func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool { 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return false return false
} }
ownerID, err := s.svc.ContextManager().GetSessionOwnerID(r.Context(), sessionID) ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID)
if err != nil || ownerID <= 0 { if err != nil || ownerID <= 0 {
writeJSONResponse(w, http.StatusNotFound, map[string]string{ writeJSONResponse(w, http.StatusNotFound, map[string]string{
"error_code": "SESSION_NOT_FOUND", "error_code": "SESSION_NOT_FOUND",
+2 -2
View File
@@ -24,7 +24,7 @@ func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth
if user != nil { if user != nil {
userID = user.ID userID = user.ID
} }
if projectID <= 0 || userID <= 0 || s.svc == nil || s.svc.DocGov() == nil { if projectID <= 0 || userID <= 0 || s.gov == nil {
return 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 { if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, projectID, userID, s.isSystemAdminUser(ctx, user)); err != nil {
return nil, err return nil, err
} }
docs, err := s.svc.DocGov().ListWorkflowDocumentsByProject(ctx, projectID) docs, err := s.gov.ListWorkflowDocumentsByProject(ctx, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+4 -4
View File
@@ -33,13 +33,13 @@ func TestServer_ChatHistoryAndCaseRecommendationAPIs(t *testing.T) {
sessID := "sess_api_history_001" sessID := "sess_api_history_001"
userID := adminID 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) t.Fatalf("ensure session: %v", err)
} }
// 1. 模拟写入对话消息 // 1. 模拟写入对话消息
_ = svc.ContextManager().RecordUserMessage(ctx, sessID, userID, "user", "我们准备立项一款便携无线掌上超声,预算大约150万", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil) _ = bizdev.Snapshot(svc).Ctx.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, 0, "assistant", "已为您建立超声设备立项基准,预估研发周期6个月", "ADVANCE_STAGE", "EXPECTATION_HIT", nil)
// 2. 测试 GET /api/v1/chat/sessions // 2. 测试 GET /api/v1/chat/sessions
reqSessions := httptest.NewRequest("GET", "/api/v1/chat/sessions?user_id="+strconv.FormatInt(userID, 10), nil) 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 与无线电合规要求。` 符合 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 { if err != nil {
t.Fatalf("IndexApprovedDocument failed: %v", err) t.Fatalf("IndexApprovedDocument failed: %v", err)
} }
+6 -6
View File
@@ -33,8 +33,8 @@ func (s *Server) authorizeSessionRead(
return nil, false return nil, false
} }
sessionOwnerID := user.ID sessionOwnerID := user.ID
if s.svc != nil && s.svc.ContextManager() != nil { if s.ctxMgr != nil {
if ownerID, err := s.svc.ContextManager().GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 { if ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 {
sessionOwnerID = ownerID sessionOwnerID = ownerID
} }
} }
@@ -43,8 +43,8 @@ func (s *Server) authorizeSessionRead(
SessionOwnerID: sessionOwnerID, SessionOwnerID: sessionOwnerID,
ViewerUserID: user.ID, ViewerUserID: user.ID,
} }
if s.svc != nil && s.svc.ContextManager() != nil { if s.ctxMgr != nil {
if projectID, err := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); err == nil { if projectID, err := s.ctxMgr.GetSessionProjectID(r.Context(), sessionID); err == nil {
authCtx.ProjectID = projectID 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) { 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 return nil, nil
} }
msgs, err := s.svc.ContextManager().ListSessionMessagesForAuth(ctx, authCtx.SessionID) msgs, err := s.ctxMgr.ListSessionMessagesForAuth(ctx, authCtx.SessionID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workflowdef"
@@ -24,7 +25,7 @@ func TestChatHistoryRefresh_PG_PreActivationAndTaskScope(t *testing.T) {
ctx := context.Background() ctx := context.Background()
handler, srv := setupPGGovernanceServer(t, pool) handler, srv := setupPGGovernanceServer(t, pool)
ctxMgr := srv.BizdevService().ContextManager() ctxMgr := bizdev.Snapshot(srv.BizdevService()).Ctx
if ctxMgr == nil { if ctxMgr == nil {
t.Fatal("context manager required") t.Fatal("context manager required")
} }
+2 -2
View File
@@ -12,7 +12,7 @@ type recategorizeReq struct {
} }
func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "context manager unavailable", http.StatusServiceUnavailable)
return return
} }
@@ -22,7 +22,7 @@ func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request)
_ = json.NewDecoder(r.Body).Decode(&req) _ = 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 { if err != nil {
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"success": false, "success": false,
@@ -25,7 +25,7 @@ func TestServer_ChatRecategorizeAPI(t *testing.T) {
sessID := "sess_recategorize_api_001" sessID := "sess_recategorize_api_001"
ctx := t.Context() ctx := t.Context()
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
// 注入种子消息 // 注入种子消息
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "请问二类注册审批周期?", "QUERY_REGULATIONS", "", nil) _, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "请问二类注册审批周期?", "QUERY_REGULATIONS", "", nil)
+2 -2
View File
@@ -57,7 +57,7 @@ func TestServer_ChatRestoreAfterRestart(t *testing.T) {
t.Logf("✓ Turn 1 完成: %s", reply1) t.Logf("✓ Turn 1 完成: %s", reply1)
// 验证消息已被持久化至 ContextManager // 验证消息已被持久化至 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 { if err != nil || len(msgs) != 2 {
t.Fatalf("expected 2 messages in ContextManager, got %d (err: %v)", len(msgs), err) 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) t.Logf("✓ Turn 2 完成: %s", reply2)
// 验证完整历史记录包含全部 4 条消息 (Turn 1 user+asst, Turn 2 user+asst) // 验证完整历史记录包含全部 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 { if err != nil {
t.Fatalf("failed to query chat history: %v", err) t.Fatalf("failed to query chat history: %v", err)
} }
@@ -29,7 +29,7 @@ func TestServer_ChatSearchAndDigestAPIs(t *testing.T) {
// 注入种子消息 // 注入种子消息
ctx := t.Context() ctx := t.Context()
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "我们启动超声探头研发预算,计划投入150万元", "TRIGGER_TASK", "", map[string]any{ _, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "我们启动超声探头研发预算,计划投入150万元", "TRIGGER_TASK", "", map[string]any{
"day_key": dayKey, "day_key": dayKey,
}) })
+1 -1
View File
@@ -17,7 +17,7 @@ func (s *Server) handleGetChatTurn(w http.ResponseWriter, r *http.Request) {
return return
} }
ctxMgr := s.svc.ContextManager() ctxMgr := s.ctxMgr
if ctxMgr == nil { if ctxMgr == nil {
http.Error(w, "context manager unavailable", http.StatusServiceUnavailable) http.Error(w, "context manager unavailable", http.StatusServiceUnavailable)
return return
@@ -30,7 +30,7 @@ func TestServer_ChatUnreadAndExportAPIs(t *testing.T) {
dayKey2 := "2026-09-09" dayKey2 := "2026-09-09"
ctx := t.Context() ctx := t.Context()
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
token := loginTestAdmin(t, handler, cfg) token := loginTestAdmin(t, handler, cfg)
if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "未读导出测试"); err != nil { if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "未读导出测试"); err != nil {
t.Fatalf("EnsureChatSession failed: %v", err) t.Fatalf("EnsureChatSession failed: %v", err)
+2 -2
View File
@@ -162,7 +162,7 @@ func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request
// handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用) // handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用)
// POST /api/v1/chat/messages/import // POST /api/v1/chat/messages/import
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -207,7 +207,7 @@ func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request
item.Metadata["created_at"] = item.CreatedAt item.Metadata["created_at"] = item.CreatedAt
} }
_, err := s.svc.ContextManager().RecordUserMessageWithID( _, err := s.ctxMgr.RecordUserMessageWithID(
r.Context(), r.Context(),
item.SessionID, item.SessionID,
item.UserID, item.UserID,
+5 -5
View File
@@ -29,13 +29,13 @@ func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccou
if strings.TrimSpace(docID) == "" { if strings.TrimSpace(docID) == "" {
return nil, docrevision.ErrInvalidDocID return nil, docrevision.ErrInvalidDocID
} }
if s.svc != nil && s.svc.DocGov() != nil { if s.gov != nil {
if doc, err := s.svc.DocGov().GetDocument(ctx, docID); err == nil && doc != nil { if doc, err := s.gov.GetDocument(ctx, docID); err == nil && doc != nil {
ref := &docRevisionRef{ ref := &docRevisionRef{
Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir, Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir,
LifecycleState: string(doc.CurrentState), AuthorID: doc.AuthorID, 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 ref.Markdown = content.Markdown
} else if doc.WorkDir != "" { } else if doc.WorkDir != "" {
md, _ := readOutputMarkdown(s.dataRoot(), 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 return ref, nil
} }
} }
if s.svc != nil && s.svc.FreeformDoc() != nil && user != nil { if s.freeDoc != nil && user != nil {
if detail, err := s.svc.FreeformDoc().GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil { if detail, err := s.freeDoc.GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil {
return &docRevisionRef{ return &docRevisionRef{
Kind: "freeform", ProjectID: int64(detail.Document.ProjectID), Kind: "freeform", ProjectID: int64(detail.Document.ProjectID),
LifecycleState: "drafting", Markdown: detail.Content, Title: detail.Document.Title, LifecycleState: "drafting", Markdown: detail.Content, Title: detail.Document.Title,
+18 -18
View File
@@ -76,10 +76,10 @@ func TestDocRevision_ControlledDocumentRoutes(t *testing.T) {
srv := newTestServer(t) srv := newTestServer(t)
ctx := context.Background() ctx := context.Background()
docID := "doc_proj1_intake" 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) 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) t.Fatal(err)
} }
token := loginTestAdmin(t, srv) token := loginTestAdmin(t, srv)
@@ -102,14 +102,14 @@ func TestDocRevision_PendingCandidateExposedAndDecisionRequiresAuthor(t *testing
srv := newTestServer(t) srv := newTestServer(t)
ctx := context.Background() ctx := context.Background()
docID := "doc_proj1_pending" 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) 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 { if err != nil {
t.Fatal(err) 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, DocID: docID, RunID: "high-1", InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, 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). // Non-author cannot decide (author is 1=admin in this fixture — use author 99 doc).
docID2 := "doc_proj1_pending_na" 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) 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 { if err != nil {
t.Fatal(err) 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, DocID: docID2, RunID: "high-2", InputSnapshotID: st2.HeadSnapshotID,
OutputMarkdown: "# x\n\n", ActorKind: docrevision.ActorAgentSquad, OutputMarkdown: "# x\n\n", ActorKind: docrevision.ActorAgentSquad,
}) })
@@ -191,15 +191,15 @@ func TestDocRevision_CommitWithMissingProposalReturnsGovernanceError(t *testing.
srv := newTestServer(t) srv := newTestServer(t)
ctx := context.Background() ctx := context.Background()
docID := "doc_proj1_gov_prop" 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ghostProp := "prop_does_not_exist" 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, DocID: docID, RunID: "gov-miss-1", ProposalID: ghostProp,
InputSnapshotID: st.HeadSnapshotID, InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, 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) 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 { if err != nil || gotCand.Status != docrevision.CandidateCommitted {
t.Fatalf("ledger must stay committed after governance failure: %+v err=%v", gotCand, err) 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) srv := newTestServer(t)
ctx := context.Background() ctx := context.Background()
docID := "doc_proj1_gov_prop_ok" 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) t.Fatal(err)
} }
prop, err := srv.svc.DocGov().ProposeModification(ctx, docID, "agent", "改", "影响", "风险") prop, err := srv.gov.ProposeModification(ctx, docID, "agent", "改", "影响", "风险")
if err != nil { if err != nil {
t.Fatal(err) 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) 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 { if err != nil {
t.Fatal(err) 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, DocID: docID, RunID: "gov-ok-1", ProposalID: prop.ProposalID,
InputSnapshotID: st.HeadSnapshotID, InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad, 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()) t.Fatalf("%s decision want 200 got %d %s", label, w.Code, w.Body.String())
} }
if i == 0 { 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 { if err != nil || got.Status != docrevision.CandidateCommitted {
t.Fatalf("after first: %+v err=%v", got, err) t.Fatalf("after first: %+v err=%v", got, err)
} }
@@ -22,7 +22,7 @@ func TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite(t *testing.T) {
strangerID int64 = 99 strangerID int64 = 99
docID = "doc_proj_duty_write" docID = "doc_proj_duty_write"
) )
if _, err := srv.svc.DocGov().CreateOrGetDocument( if _, err := srv.gov.CreateOrGetDocument(
ctx, docID, 6145, "intake", "摸底", "V0.1", "", ctx, docID, 6145, "intake", "摸底", "V0.1", "",
authorID, "编制人A", reviewerID, "审核人B", 0, "", authorID, "编制人A", reviewerID, "审核人B", 0, "",
); err != nil { ); err != nil {
+11 -11
View File
@@ -40,7 +40,7 @@ func (s *Server) docRev() *docrevision.Service {
if s == nil || s.svc == nil { if s == nil || s.svc == nil {
return nil return nil
} }
return s.svc.DocRevision() return s.rev
} }
func (s *Server) handleDocRevisionState(w http.ResponseWriter, r *http.Request) { 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). // 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 // 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. // (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 kind == "controlled" && s.gov != nil {
if err := s.svc.DocGov().AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil { if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil {
writeDocGovError(w, err) writeDocGovError(w, err)
return return
} }
@@ -221,7 +221,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
holderID = "human_anon" holderID = "human_anon"
} }
defer func() { 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 // 3) Project fact-source AFTER ledger commit. Controlled docs use SaveDocumentContent so
// author_last_edited / audit trail stay consistent; failure → DOCUMENT_PROJECTION_FAILED // author_last_edited / audit trail stay consistent; failure → DOCUMENT_PROJECTION_FAILED
// (ledger remains authoritative; same Idempotency-Key retry re-projects). // (ledger remains authoritative; same Idempotency-Key retry re-projects).
if kind == "controlled" && s.svc != nil && s.svc.DocGov() != nil { if kind == "controlled" && s.gov != nil {
if _, err := s.svc.DocGov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ if _, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
DocID: docID, DocID: docID,
ActorID: user.ID, ActorID: user.ID,
ActorRole: docgov.RoleAuthor, ActorRole: docgov.RoleAuthor,
@@ -300,9 +300,9 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
return return
} }
} }
if kind == "freeform" && s.svc != nil && s.svc.FreeformDoc() != nil { if kind == "freeform" && s.freeDoc != nil {
md := body.Markdown 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, ContentMarkdown: &md,
ChangeSummary: "revision ledger human save", ChangeSummary: "revision ledger human save",
}); err != nil { }); err != nil {
@@ -390,8 +390,8 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
}) })
return return
} }
if s.svc != nil && s.svc.DocGov() != nil && st != nil { if s.gov != nil && st != nil {
if err := s.svc.DocGov().MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil { if err := s.gov.MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil {
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"code": "DOCUMENT_PROJECTION_FAILED", "code": "DOCUMENT_PROJECTION_FAILED",
"error": err.Error(), "error": err.Error(),
@@ -401,7 +401,7 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
return return
} }
if body.Decision == "commit" && cand != nil && strings.TrimSpace(cand.ProposalID) != "" { 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{ writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"code": "DOCUMENT_GOVERNANCE_FAILED", "code": "DOCUMENT_GOVERNANCE_FAILED",
"error": err.Error(), "error": err.Error(),
@@ -29,10 +29,10 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger(
workDir := filepath.Join(blocker, "ws-proj") workDir := filepath.Join(blocker, "ws-proj")
docID := "doc_proj1_patch_order" 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -58,7 +58,7 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger(
if payload["code"] != "DOCUMENT_PROJECTION_FAILED" { if payload["code"] != "DOCUMENT_PROJECTION_FAILED" {
t.Fatalf("code want DOCUMENT_PROJECTION_FAILED got %#v", payload["code"]) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -76,10 +76,10 @@ func TestDocRevision_PatchOrder_LeaseDeniedDoesNotAdvanceLedger(t *testing.T) {
_ = os.MkdirAll(workDir, 0o755) _ = os.MkdirAll(workDir, 0o755)
// Author is user 99; login admin may bypass route author gate but AssertHumanCanEdit (RoleAuthor) still fails. // 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -96,14 +96,14 @@ func TestDocRevision_PatchOrder_LeaseDeniedDoesNotAdvanceLedger(t *testing.T) {
if w.Code == http.StatusOK { if w.Code == http.StatusOK {
t.Fatalf("non-author must not save successfully: %s", w.Body.String()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if after.HeadRevisionNo != before { if after.HeadRevisionNo != before {
t.Fatalf("ledger must not advance when AssertHumanCanEdit fails: before=%d after=%d", before, after.HeadRevisionNo) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -119,10 +119,10 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T)
_ = os.MkdirAll(workDir, 0o755) _ = os.MkdirAll(workDir, 0o755)
docID := "doc_proj1_patch_audit" 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -139,7 +139,7 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Fatalf("want 200 got %d %s", w.Code, w.Body.String()) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -162,10 +162,10 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) {
_ = os.MkdirAll(workDir, 0o755) _ = os.MkdirAll(workDir, 0o755)
docID := "doc_proj1_patch_conflict_lease" 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -199,14 +199,14 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) {
func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID string) { func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID string) {
t.Helper() t.Helper()
ctx := context.Background() ctx := context.Background()
doc, err := srv.svc.DocGov().GetDocument(ctx, docID) doc, err := srv.gov.GetDocument(ctx, docID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if doc.HasActiveLease(time.Now()) { if doc.HasActiveLease(time.Now()) {
t.Fatalf("human lease must be released after PATCH, holder=%s/%s", doc.LeaseHolderType, doc.LeaseHolderID) 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, DocID: docID,
HolderType: docgov.LeaseHolderSquad, HolderType: docgov.LeaseHolderSquad,
HolderID: "run_after_human_save", HolderID: "run_after_human_save",
@@ -218,7 +218,7 @@ func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID st
if leased.LeaseHolderType != docgov.LeaseHolderSquad { if leased.LeaseHolderType != docgov.LeaseHolderSquad {
t.Fatalf("want squad holder, got %s", leased.LeaseHolderType) 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) t.Fatal(err)
} }
} }
@@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/db" "chat-agent-system/internal/db"
"chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workflowdef"
@@ -96,7 +97,7 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) {
_ = json.Unmarshal(recActivate.Body.Bytes(), &activated) _ = json.Unmarshal(recActivate.Body.Bytes(), &activated)
docID := fmt.Sprintf("doc_proj%d_test_manager", projectID) 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", "", _, err = docEngine.CreateOrGetDocument(ctx, docID, projectID, "test_manager", "系统验证方案及计划", "system_verification_planning", "",
creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver") creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver")
if err != nil { if err != nil {
+16 -16
View File
@@ -36,7 +36,7 @@ func extractDocIDFromPath(r *http.Request, actionSuffix string) string {
// POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private) // POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private)
func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -82,7 +82,7 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request)
return 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 err != nil {
if errors.Is(err, freedoc.ErrInvalidParam) { if errors.Is(err, freedoc.ErrInvalidParam) {
writeJSONResponse(w, http.StatusBadRequest, map[string]any{ 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 // POST /api/v1/freeform-documents/from-message
func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -124,7 +124,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht
return 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 err != nil {
if errors.Is(err, freedoc.ErrMessageNotExportable) { if errors.Is(err, freedoc.ErrMessageNotExportable) {
writeJSONResponse(w, http.StatusForbidden, map[string]any{ 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 // GET /api/v1/freeform-documents
func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -186,7 +186,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request)
projectID, _ = strconv.Atoi(pStr) 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 { if err != nil {
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{ writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
"error_code": "LIST_FAILED", "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} // GET /api/v1/freeform-documents/{doc_id}
func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -224,7 +224,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
return return
} }
detail, err := s.svc.FreeformDoc().GetDocument(r.Context(), userID, docID) detail, err := s.freeDoc.GetDocument(r.Context(), userID, docID)
if err != nil { if err != nil {
if errors.Is(err, freedoc.ErrNotFound) { if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{ 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} // PUT /api/v1/freeform-documents/{doc_id}
func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return 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 err != nil {
if errors.Is(err, freedoc.ErrNotFound) { if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{ 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 // POST /api/v1/freeform-documents/{doc_id}/share
func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -349,7 +349,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request)
return return
} }
doc, err := s.svc.FreeformDoc().ShareDocument(r.Context(), userID, docID) doc, err := s.freeDoc.ShareDocument(r.Context(), userID, docID)
if err != nil { if err != nil {
if errors.Is(err, freedoc.ErrNotFound) { if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) 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 // POST /api/v1/freeform-documents/{doc_id}/unshare
func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -390,7 +390,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request
return return
} }
doc, err := s.svc.FreeformDoc().UnshareDocument(r.Context(), userID, docID) doc, err := s.freeDoc.UnshareDocument(r.Context(), userID, docID)
if err != nil { if err != nil {
if errors.Is(err, freedoc.ErrNotFound) { if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) 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} // DELETE /api/v1/freeform-documents/{doc_id}
func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -431,7 +431,7 @@ func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request)
return return
} }
err := s.svc.FreeformDoc().DeleteDocument(r.Context(), userID, docID) err := s.freeDoc.DeleteDocument(r.Context(), userID, docID)
if err != nil { if err != nil {
if errors.Is(err, freedoc.ErrNotFound) { if errors.Is(err, freedoc.ErrNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"}) writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
+1 -1
View File
@@ -25,7 +25,7 @@ func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler,
handler := srv.Routes() handler := srv.Routes()
ctx := context.Background() 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) validMsgID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-test", 1, "assistant", "# 临床安全评估\n\n评估合格。", "consult", "main", nil)
require.NoError(t, err) require.NoError(t, err)
+40 -40
View File
@@ -102,7 +102,7 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
} }
} }
schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory) 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" status := "ok"
code := http.StatusOK code := http.StatusOK
@@ -415,12 +415,12 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) {
} }
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return 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 { if err != nil {
http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -450,7 +450,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque
return return
} }
if s.svc == nil || s.svc.ContextManager() == nil { if s.ctxMgr == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -463,7 +463,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque
CustomInstructions: req.CustomInstructions, 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) http.Error(w, "save user preference failed: "+err.Error(), http.StatusInternalServerError)
return return
} }
@@ -490,12 +490,12 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request)
return return
} }
if s.svc == nil || s.svc.ContextManager() == nil { if s.ctxMgr == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
pCtx, err := s.svc.ContextManager().GetProjectContext(r.Context(), projectID) pCtx, err := s.ctxMgr.GetProjectContext(r.Context(), projectID)
if err != nil { if err != nil {
http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -516,12 +516,12 @@ func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
doc, err := s.svc.DocGov().GetDocument(r.Context(), docID) doc, err := s.gov.GetDocument(r.Context(), docID)
if err != nil { if err != nil {
http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound) http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound)
return return
@@ -555,12 +555,12 @@ func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) {
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return 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 { if err != nil {
http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -589,7 +589,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -597,7 +597,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
return 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 { if err != nil {
http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest) http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest)
return return
@@ -630,7 +630,7 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -643,13 +643,13 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
var err error var err error
if action == "approve" { 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" { } else if action == "reject" {
reason := req.Reason reason := req.Reason
if reason == "" { if reason == "" {
reason = req.Comment 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 { } else {
http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest) http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest)
return return
@@ -676,7 +676,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -746,12 +746,12 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return 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 { if err != nil {
http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest) http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest)
return return
@@ -768,12 +768,12 @@ func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
comments, err := s.svc.DocGov().ListComments(r.Context(), docID) comments, err := s.gov.ListComments(r.Context(), docID)
if err != nil { if err != nil {
http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -801,7 +801,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -811,7 +811,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
cType = docgov.CommentGeneral cType = docgov.CommentGeneral
} }
comment, err := s.svc.DocGov().AddComment( comment, err := s.gov.AddComment(
r.Context(), req.DocID, req.UserID, req.UserName, r.Context(), req.DocID, req.UserID, req.UserName,
docgov.UserRole(req.UserRole), req.TargetSection, cType, req.Content, docgov.UserRole(req.UserRole), req.TargetSection, cType, req.Content,
) )
@@ -831,12 +831,12 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
logs, err := s.svc.DocGov().ListAuditLogs(r.Context(), docID) logs, err := s.gov.ListAuditLogs(r.Context(), docID)
if err != nil { if err != nil {
http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -851,7 +851,7 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques
// handleListChatSessions 分页查询用户/项目关联的历史会话列表 // handleListChatSessions 分页查询用户/项目关联的历史会话列表
func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -872,7 +872,7 @@ func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request)
offset = 0 offset = 0
} }
sessions, err := s.svc.ContextManager().ListChatSessions(r.Context(), ctxmgr.SessionFilter{ sessions, err := s.ctxMgr.ListChatSessions(r.Context(), ctxmgr.SessionFilter{
UserID: userID, UserID: userID,
ProjectID: projectID, ProjectID: projectID,
Limit: limit, Limit: limit,
@@ -895,7 +895,7 @@ type upsertChatSessionRequest struct {
// handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。 // handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。
func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -910,7 +910,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
return 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 { if err != nil {
http.Error(w, "upsert chat session failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "upsert chat session failed: "+err.Error(), http.StatusInternalServerError)
return return
@@ -921,7 +921,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
// handleGetChatMessages 分页获取指定会话的历史多轮消息记录 // handleGetChatMessages 分页获取指定会话的历史多轮消息记录
func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -972,7 +972,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
if s.svc == nil || s.svc.ContextManager() == nil { if s.ctxMgr == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -998,7 +998,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
minScore = 0.50 minScore = 0.50
} }
cases, err := s.svc.ContextManager().SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{ cases, err := s.ctxMgr.SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{
Query: query, Query: query,
TaskKey: taskKey, TaskKey: taskKey,
ExcludeDoc: excludeDoc, 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) http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
role := docgov.RoleAuthor 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, DocID: req.DocID,
ActorID: user.ID, ActorID: user.ID,
ActorRole: role, 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) http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -1088,7 +1088,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
workDir = "" workDir = ""
} }
ttl := time.Duration(req.TTLSeconds) * time.Second 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, DocID: req.DocID,
HolderType: docgov.LeaseHolderType(req.HolderType), HolderType: docgov.LeaseHolderType(req.HolderType),
HolderID: req.HolderID, 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) http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
ttl := time.Duration(req.TTLSeconds) * time.Second 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 { if err != nil {
writeDocGovError(w, err) writeDocGovError(w, err)
return 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) http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
return return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return 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 { if err != nil {
writeDocGovError(w, err) writeDocGovError(w, err)
return return
+2 -2
View File
@@ -29,7 +29,7 @@ func TestDocEditLease_HTTPConcurrencyGates(t *testing.T) {
docID := fmt.Sprintf("doc_lease_http_concurrency_%d_a", time.Now().UnixNano()) docID := fmt.Sprintf("doc_lease_http_concurrency_%d_a", time.Now().UnixNano())
ctx := t.Context() 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, "批准") 1, "作者", 2, "审核", 3, "批准")
if err != nil { if err != nil {
t.Fatalf("create doc: %v", err) t.Fatalf("create doc: %v", err)
@@ -113,7 +113,7 @@ func TestDocEditLease_HTTPConcurrencyGates(t *testing.T) {
// 6. 不同文档可并行持有租约 // 6. 不同文档可并行持有租约
doc2 := fmt.Sprintf("doc_lease_http_concurrency_%d_b", time.Now().UnixNano()) 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, "批准") 1, "作者", 2, "审核", 3, "批准")
acqA, _ := json.Marshal(map[string]any{ acqA, _ := json.Marshal(map[string]any{
"doc_id": docID, "holder_type": "squad", "holder_id": "run_a", "ttl_seconds": 60, "doc_id": docID, "holder_type": "squad", "holder_id": "run_a", "ttl_seconds": 60,
@@ -108,7 +108,7 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http
configRevision = activated.Instance.ConfigRevision configRevision = activated.Instance.ConfigRevision
docID := fmt.Sprintf("doc_p5_fault_%d", projectID) 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.CreateOrGetDocument(ctx, docID, projectID, "intake", "Intake", "V0.1", "", creatorID, "A", reviewerID, "R", approverID, "P")
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_p5_fault", false) _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_p5_fault", false)
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
@@ -85,7 +85,7 @@ func TestPhase5_RollingRestartReplay_PG(t *testing.T) {
// 模拟三次进程重启:新 Manager 仅挂载 Store // 模拟三次进程重启:新 Manager 仅挂载 Store
for i := 0; i < 3; i++ { 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) restarted.SetStore(store)
rec, err := restarted.GetRun(runID) rec, err := restarted.GetRun(runID)
if err != nil { if err != nil {
+6 -6
View File
@@ -19,7 +19,7 @@ func (s *Server) docflowQuery() *docflow.Query {
return nil return nil
} }
return &docflow.Query{Docs: &docquery.Service{ return &docflow.Query{Docs: &docquery.Service{
Gov: s.svc.DocGov(), Gov: s.gov,
Rev: s.docRev(), Rev: s.docRev(),
RepoRoot: s.repoRoot(), RepoRoot: s.repoRoot(),
Authorize: func(ctx context.Context, p docquery.Principal, rec *docgov.DocumentRecord) error { 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 return
} }
if s.svc == nil || s.svc.DocGov() == nil { if s.gov == nil {
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
return return
} }
docs, err := s.svc.DocGov().ListWorkflowDocumentsByProject(r.Context(), projectID) docs, err := s.gov.ListWorkflowDocumentsByProject(r.Context(), projectID)
if err != nil { if err != nil {
http.Error(w, "list project documents failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "list project documents failed: "+err.Error(), http.StatusInternalServerError)
return 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) { 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 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}) view, err := svc.Get(ctx, docquery.Principal{UserID: 1}, docquery.DocRef{DocID: docID})
if err != nil && !errors.Is(err, docquery.ErrLedgerMissing) { if err != nil && !errors.Is(err, docquery.ErrLedgerMissing) {
return nil, err return nil, err
@@ -145,7 +145,7 @@ func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string
if view != nil && view.Markdown != "" { if view != nil && view.Markdown != "" {
return view.ToDocumentContent(), nil 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 // serveLedgerHeadOrConflict keeps the historical helper name. Projection is a
+5 -5
View File
@@ -62,17 +62,17 @@ func TestHandleRunProjectWorkflow_DecodesBodyWithoutContentLength(t *testing.T)
ctx := t.Context() ctx := t.Context()
srDocID := "doc_proj1001_system_reqs" srDocID := "doc_proj1001_system_reqs"
_, err := svc.DocGov().CreateOrGetDocument( _, err := bizdev.Snapshot(svc).Gov.CreateOrGetDocument(
ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "", ctx, srDocID, 1001, "system_reqs", "系统需求规格书", "V0.5", "",
2, "张工", 3, "李主管", 4, "王总监", 2, "张工", 3, "李主管", 4, "王总监",
) )
if err != nil { if err != nil {
t.Fatalf("CreateOrGetDocument: %v", err) t.Fatalf("CreateOrGetDocument: %v", err)
} }
_ = svc.DocGov().MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false) _ = bizdev.Snapshot(svc).Gov.MarkDocumentContentUpdated(ctx, srDocID, "hash_v1", false)
_, _ = svc.DocGov().SubmitForReview(ctx, srDocID, 2, "author", "张工") _, _ = bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, srDocID, 2, "author", "张工")
_, _ = svc.DocGov().ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意") _, _ = bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, srDocID, 3, "reviewer", "李主管", "同意")
_, err = svc.DocGov().ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准") _, err = bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, srDocID, 4, "approver", "王总监", "批准")
if err != nil { if err != nil {
t.Fatalf("ApproverApprove: %v", err) t.Fatalf("ApproverApprove: %v", err)
} }
+1 -1
View File
@@ -33,7 +33,7 @@ func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error {
if s == nil || s.svc == nil || projectID <= 0 { if s == nil || s.svc == nil || projectID <= 0 {
return nil return nil
} }
pool := s.svc.Pool() pool := s.pool
if pool == nil { if pool == nil {
return nil return nil
} }
+5 -5
View File
@@ -203,16 +203,16 @@ func TestProjectWorkflow_TestManagerAndLineageStale(t *testing.T) {
// 3. 在受控治理中创建并批准系统需求规格书 // 3. 在受控治理中创建并批准系统需求规格书
srDocID := fmt.Sprintf("doc_proj%d_system_reqs", projectID) 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", "系统需求规格书", nil, srDocID, projectID, "system_reqs", "系统需求规格书",
"V0.5_feasibility", "", "V0.5_feasibility", "",
2, "张工", 3, "李主管", 4, "王总监", 2, "张工", 3, "李主管", 4, "王总监",
) )
require.NoError(t, err) require.NoError(t, err)
_ = srv.BizdevService().DocGov().MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false) _ = bizdev.Snapshot(srv.BizdevService()).Gov.MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false)
_, _ = srv.BizdevService().DocGov().SubmitForReview(nil, srDocID, 2, "author", "张工") _, _ = bizdev.Snapshot(srv.BizdevService()).Gov.SubmitForReview(nil, srDocID, 2, "author", "张工")
_, _ = srv.BizdevService().DocGov().ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意") _, _ = bizdev.Snapshot(srv.BizdevService()).Gov.ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意")
approvedDoc, err := srv.BizdevService().DocGov().ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效") approvedDoc, err := bizdev.Snapshot(srv.BizdevService()).Gov.ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "approved", string(approvedDoc.CurrentState)) assert.Equal(t, "approved", string(approvedDoc.CurrentState))
+4 -4
View File
@@ -17,7 +17,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques
return return
} }
if s.svc == nil || s.svc.ContextManager() == nil { if s.ctxMgr == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -31,7 +31,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques
return return
} }
msg, err := s.svc.ContextManager().GetMessageByID(r.Context(), msgID) msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID)
if err != nil || msg == nil { if err != nil || msg == nil {
writeJSONResponse(w, http.StatusNotFound, map[string]string{ writeJSONResponse(w, http.StatusNotFound, map[string]string{
"error_code": "MESSAGE_NOT_FOUND", "error_code": "MESSAGE_NOT_FOUND",
@@ -84,7 +84,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) {
return return
} }
if s.svc == nil || s.svc.ContextManager() == nil { if s.ctxMgr == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return return
} }
@@ -98,7 +98,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) {
return return
} }
msg, err := s.svc.ContextManager().GetMessageByID(r.Context(), msgID) msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID)
if err != nil || msg == nil { if err != nil || msg == nil {
writeJSONResponse(w, http.StatusNotFound, map[string]string{ writeJSONResponse(w, http.StatusNotFound, map[string]string{
"error_code": "MESSAGE_NOT_FOUND", "error_code": "MESSAGE_NOT_FOUND",
+1 -1
View File
@@ -24,7 +24,7 @@ func TestServer_ReplyExportAPI(t *testing.T) {
handler := srv.Routes() handler := srv.Routes()
ctx := t.Context() ctx := t.Context()
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
sessID := "sess_export_api_test" sessID := "sess_export_api_test"
token := loginTestAdmin(t, handler, cfg) token := loginTestAdmin(t, handler, cfg)
if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "导出测试会话"); err != nil { if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "导出测试会话"); err != nil {
+26 -5
View File
@@ -20,13 +20,20 @@ import (
"chat-agent-system/internal/config" "chat-agent-system/internal/config"
"chat-agent-system/internal/coordinator" "chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr" "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/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/notification" "chat-agent-system/internal/notification"
"chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/taskrun" "chat-agent-system/internal/taskrun"
"chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workflowdef"
"chat-agent-system/internal/workspace" "chat-agent-system/internal/workspace"
"github.com/jackc/pgx/v5/pgxpool"
) )
// Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务 // Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务
@@ -52,6 +59,13 @@ type Server struct {
chatLog chatflow.ChatLog chatLog chatflow.ChatLog
assembler chatflow.ContextAssembler assembler chatflow.ContextAssembler
stageflow *stageflow.UseCase 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 httpServer *http.Server
} }
@@ -79,8 +93,15 @@ func NewFromRuntime(rt *app.Runtime) *Server {
wsCap: rt.WSCap, wsCap: rt.WSCap,
cmds: rt.Commands, cmds: rt.Commands,
stageflow: rt.Stageflow, 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) s.assembler = chatflow.NewAssembler(coordinator.NewContextAssembler(), s.chatLog)
rt.ChatLog = s.chatLog rt.ChatLog = s.chatLog
rt.Assembler = s.assembler rt.Assembler = s.assembler
@@ -412,8 +433,8 @@ func (s *Server) getOrCreateSession(
// 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3) // 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3)
var restored *ctxmgr.RestoredSessionContext var restored *ctxmgr.RestoredSessionContext
if s.svc != nil && s.svc.ContextManager() != nil { if s.ctxMgr != nil {
if r, err := s.svc.ContextManager().RestoreSessionContext(context.Background(), sessionID); err == nil && r != nil && len(r.Messages) > 0 { if r, err := s.ctxMgr.RestoreSessionContext(context.Background(), sessionID); err == nil && r != nil && len(r.Messages) > 0 {
restored = r restored = r
} }
} }
@@ -480,9 +501,9 @@ func (s *Server) getOrCreateSession(
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine) sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
sess.ChatLog = s.chatLog sess.ChatLog = s.chatLog
if s.svc != nil { if s.svc != nil {
sess.CtxMgr = s.svc.ContextManager() sess.CtxMgr = s.ctxMgr
sess.Actions = &chat.ActionServices{ sess.Actions = &chat.ActionServices{
DocGov: s.svc.DocGov(), DocGov: s.gov,
Bizdev: s.svc, Bizdev: s.svc,
TaskRuns: s.taskRuns, TaskRuns: s.taskRuns,
} }
+2 -2
View File
@@ -93,14 +93,14 @@ func TestServer_ChatDocumentSummaryReadsExistingDocumentWithoutWriteAction(t *te
if err := os.WriteFile(filepath.Join(workDir, "output.md"), markdown, 0o644); err != nil { if err := os.WriteFile(filepath.Join(workDir, "output.md"), markdown, 0o644); err != nil {
t.Fatalf("write output.md: %v", err) 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, ctx, "doc_proj143_intake", 143, "intake", "用户初始需求收集表", "V0.1_triage", workDir,
1, "编制人", 1, "审核人", 1, "批准人", 1, "编制人", 1, "审核人", 1, "批准人",
); err != nil { ); err != nil {
t.Fatalf("create document: %v", err) t.Fatalf("create document: %v", err)
} }
hash := fmt.Sprintf("%x", sha256.Sum256(markdown)) 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) t.Fatalf("mark content: %v", err)
} }
+2 -2
View File
@@ -68,8 +68,8 @@ func (s *Server) enforceDocGovWorkflowAction(w http.ResponseWriter, ctx context.
if docID == "" { if docID == "" {
return true return true
} }
if s.svc != nil && s.svc.DocGov() != nil { if s.gov != nil {
doc, err := s.svc.DocGov().GetDocument(ctx, docID) doc, err := s.gov.GetDocument(ctx, docID)
if err == nil && doc != nil && doc.ProjectID > 0 && doc.TaskKey != "" { if err == nil && doc != nil && doc.ProjectID > 0 && doc.TaskKey != "" {
return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action) return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action)
} }
+2 -2
View File
@@ -31,7 +31,7 @@ func (s *Server) finalizeTaskRun(ctx context.Context, record *taskrun.RunRecord,
if s != nil { if s != nil {
cmds.PW = s.pwSvc cmds.PW = s.pwSvc
if s.svc != nil { if s.svc != nil {
cmds.Ctx = s.svc.ContextManager() cmds.Ctx = s.ctxMgr
} }
} }
return cmds.Finalize(ctx, record, result) return cmds.Finalize(ctx, record, result)
@@ -50,7 +50,7 @@ func (s *Server) taskCommands() *taskrun.Commands {
cmds.StorageIdentity = "env:" + s.cfg.Env cmds.StorageIdentity = "env:" + s.cfg.Env
} }
if s != nil && s.svc != nil { if s != nil && s.svc != nil {
cmds.Ctx = s.svc.ContextManager() cmds.Ctx = s.ctxMgr
} }
return cmds return cmds
} }
+5 -5
View File
@@ -28,7 +28,7 @@ func TestFinalizeTaskRun_ReplacesPlaceholderContent(t *testing.T) {
workDir := t.TempDir() workDir := t.TempDir()
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# 最终交付\n\n正文内容。"), 0o644) _ = 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{ asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-finalize", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{
"task_status": "running", "task_status": "running",
}) })
@@ -106,7 +106,7 @@ func TestFinalizeTaskRun_PendingAuthorConfirmation_NotCompleted(t *testing.T) {
workDir := t.TempDir() workDir := t.TempDir()
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# base head\n"), 0o644) _ = 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{ asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), "sess-pending", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{
"task_status": "running", "task_status": "running",
}) })
@@ -180,7 +180,7 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"} cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil) pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
workDir := t.TempDir() workDir := t.TempDir()
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# async finalize\n"), 0o644) _ = 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"} cfg := &config.Config{Env: "test", DataDir: t.TempDir(), AgentBackend: "stub"}
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil) pwSvc := projectworkflow.NewService(projectworkflow.NewStore(nil), nil)
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
workDir := t.TempDir() workDir := t.TempDir()
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# head\n"), 0o644) _ = 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 { if err := srv.wsCap.WriteArtifact(ctx, ref, workspace.DeliverableKey, body); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ctxMgr := svc.ContextManager() ctxMgr := bizdev.Snapshot(svc).Ctx
asstID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-hash", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil) asstID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-hash", 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -17,19 +17,19 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
srv := newTestServer(t) srv := newTestServer(t)
ctx := t.Context() ctx := t.Context()
docID := "doc_proj143_intake" docID := "doc_proj143_intake"
if _, err := srv.svc.DocGov().CreateOrGetDocument( if _, err := srv.gov.CreateOrGetDocument(
ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "", ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "",
1, "编制人", 1, "审核人", 1, "批准人", 1, "编制人", 1, "审核人", 1, "批准人",
); err != nil { ); err != nil {
t.Fatalf("create document: %v", err) 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, DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "editor-9", TTL: time.Minute,
}); err != nil { }); err != nil {
t.Fatalf("acquire fixture lease: %v", err) t.Fatalf("acquire fixture lease: %v", err)
} }
defer func() { 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{ requestBody, _ := json.Marshal(createTaskRunRequest{
@@ -53,7 +53,7 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
if _, exists := payload["run_id"]; exists { if _, exists := payload["run_id"]; exists {
t.Fatalf("busy preflight must not return run_id: %+v", payload) 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 { if err != nil {
t.Fatalf("get busy history: %v", err) 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" { 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) 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 { if err != nil {
t.Fatalf("chat history: %v", err) 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) srv.instSvc = projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil)
docID := "doc_proj9002_intake" docID := "doc_proj9002_intake"
if _, err := srv.svc.DocGov().CreateOrGetDocument( if _, err := srv.gov.CreateOrGetDocument(
t.Context(), docID, 9002, "intake", "立项文档", "V0.1_triage", "", t.Context(), docID, 9002, "intake", "立项文档", "V0.1_triage", "",
1, "编制人", 2, "审核人", 3, "批准人", 1, "编制人", 2, "审核人", 3, "批准人",
); err != nil { ); err != nil {
+4 -4
View File
@@ -104,7 +104,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
} }
} }
ctxMgr := s.svc.ContextManager() ctxMgr := s.ctxMgr
if ctxMgr != nil { if ctxMgr != nil {
_, _ = ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.ProjectName) _, _ = 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 return
} }
ctxMgr := s.svc.ContextManager() ctxMgr := s.ctxMgr
if ctxMgr != nil && req.UserMessage != "" { if ctxMgr != nil && req.UserMessage != "" {
userMeta := map[string]any{ userMeta := map[string]any{
"project_id": req.ProjectID, "project_id": req.ProjectID,
@@ -338,7 +338,7 @@ func (s *Server) recordBusyPreflightMessages(
// preflightTaskDocumentBusy 在写入聊天占位与 task_runs 之前拦截已知租约占用。 // preflightTaskDocumentBusy 在写入聊天占位与 task_runs 之前拦截已知租约占用。
// 真正的租约获取仍由 docgov 原子执行;本检查用于避免用户重复点击产生必然失败的 run。 // 真正的租约获取仍由 docgov 原子执行;本检查用于避免用户重复点击产生必然失败的 run。
func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRunRequest) (*docgov.DocumentBusyError, error) { 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 return nil, nil
} }
taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, "") taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, "")
@@ -349,7 +349,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
if docID == "" { if docID == "" {
return nil, nil return nil, nil
} }
doc, err := s.svc.DocGov().GetDocument(ctx, docID) doc, err := s.gov.GetDocument(ctx, docID)
if errors.Is(err, docgov.ErrDocumentNotFound) { if errors.Is(err, docgov.ErrDocumentNotFound) {
return nil, nil return nil, nil
} }
@@ -43,7 +43,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
) )
docID := "doc_proj143_intake" docID := "doc_proj143_intake"
if _, err := srv.svc.DocGov().CreateOrGetDocument( if _, err := srv.gov.CreateOrGetDocument(
ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "", ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "",
userID, "编制人", userID, "审核人", userID, "批准人", userID, "编制人", userID, "审核人", userID, "批准人",
); err != nil { ); err != nil {
@@ -71,7 +71,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
t.Fatal("missing run_id") 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 { if err != nil {
t.Fatalf("history1: %v", err) t.Fatalf("history1: %v", err)
} }
@@ -80,13 +80,13 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
} }
// 活跃小队租约:模拟长任务仍占文档(旧路径会 409) // 活跃小队租约:模拟长任务仍占文档(旧路径会 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, DocID: docID, HolderType: docgov.LeaseHolderSquad, HolderID: first.RunID, TTL: time.Minute,
}); err != nil { }); err != nil {
t.Fatalf("acquire squad lease: %v", err) t.Fatalf("acquire squad lease: %v", err)
} }
t.Cleanup(func() { 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{ 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) 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 { if err != nil {
t.Fatalf("history2: %v", err) t.Fatalf("history2: %v", err)
} }
@@ -88,7 +88,7 @@ func TestTaskRun_PersistenceAndReplayAfterRestart_PG(t *testing.T) {
} }
// 模拟进程重启:新 Manager 仅挂载 Store // 模拟进程重启:新 Manager 仅挂载 Store
restarted := taskrun.NewManager(srv.ProjectWorkflowService(), srv.BizdevService().ContextManager()) restarted := taskrun.NewManager(srv.ProjectWorkflowService(), bizdev.Snapshot(srv.BizdevService()).Ctx)
restarted.SetStore(store) restarted.SetStore(store)
reloaded, err := restarted.GetRun(runID) reloaded, err := restarted.GetRun(runID)
+2 -2
View File
@@ -9,10 +9,10 @@ import (
) )
func (s *Server) registerWorkflowApprovalHook() { 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 return
} }
s.svc.DocGov().AddOnApprovedHook(s.onWorkflowDocumentApproved) s.gov.AddOnApprovedHook(s.onWorkflowDocumentApproved)
} }
func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) { func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) {
@@ -84,7 +84,7 @@ func setupPGGovernanceServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *s
t.Helper() t.Helper()
cfg := configFromTestDatabaseURL(t) cfg := configFromTestDatabaseURL(t)
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
if svc.Pool() == nil { if bizdev.Snapshot(svc).Pool == nil {
t.Skip("bizdev service not connected to postgres") t.Skip("bizdev service not connected to postgres")
} }
srv := server.NewServer(cfg, svc) srv := server.NewServer(cfg, svc)
@@ -194,7 +194,7 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
// 4. 文档批准链(经 bizdev DocGov 引擎,触发 onWorkflowDocumentApproved hook) // 4. 文档批准链(经 bizdev DocGov 引擎,触发 onWorkflowDocumentApproved hook)
docID := fmt.Sprintf("doc_pg_p5_intake_%d", projectID) 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") _, err = docEngine.CreateOrGetDocument(ctx, docID, projectID, "intake", "Intake Doc", "V0.1", "", creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver")
if err != nil { if err != nil {
t.Fatalf("create doc: %v", err) t.Fatalf("create doc: %v", err)
@@ -45,7 +45,7 @@ func setupPGPiAgentServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *serv
} }
mergePiAgentE2EKbConfig(cfg) mergePiAgentE2EKbConfig(cfg)
svc := bizdev.MustNewService(t, cfg) svc := bizdev.MustNewService(t, cfg)
if svc.Pool() == nil { if bizdev.Snapshot(svc).Pool == nil {
t.Skip("bizdev service not connected to postgres") t.Skip("bizdev service not connected to postgres")
} }
srv := server.NewServer(cfg, svc) 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)) 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.MarkDocumentContentUpdated(ctx, docID, "hash_pi_pg", false)
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok") _, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
+1 -1
View File
@@ -44,7 +44,7 @@ func main() {
srv := server.NewServer(cfg, svc) srv := server.NewServer(cfg, svc)
ctx := context.Background() ctx := context.Background()
pwSvc := svc.ProjectWorkflow() pwSvc := bizdev.Snapshot(svc).PW
if pwSvc == nil { if pwSvc == nil {
log.Fatal("project workflow service unavailable") log.Fatal("project workflow service unavailable")
} }