merge: feature/docgov-vocabulary-2026-09-22
Keep contracts/vocabulary with G1 uow/workspace duty ports; chat stays decoupled.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "W2 batch C: docgov vocabulary convergence"
|
||||
task_id: "2026-09-22-docgov-vocabulary"
|
||||
status: done
|
||||
created: 2026-09-22
|
||||
updated: 2026-09-22
|
||||
owner: Cursor
|
||||
task_type: implementation
|
||||
task_tier: small
|
||||
policy_revision: "AGENTS+module-dev-standards @364ed7f1"
|
||||
branch: feature/docgov-vocabulary-2026-09-22
|
||||
worktree: .worktrees/docgov-vocabulary-2026-09-22
|
||||
base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87"
|
||||
write_scope:
|
||||
- "internal/contracts/vocabulary/**"
|
||||
- "internal/docgov/types.go"
|
||||
- "internal/chat/action_executor.go"
|
||||
- "internal/coordinator/engine.go"
|
||||
- "internal/bizdev/service.go"
|
||||
- "internal/server/handlers.go"
|
||||
- "internal/server/auth_helpers.go"
|
||||
- "internal/server/document_revision_handlers.go"
|
||||
- "internal/app/docflow/commands.go"
|
||||
- ".doc/system-design/module-boundaries.yaml"
|
||||
- ".doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md"
|
||||
allowed_environments: []
|
||||
head_sha: "381b56dafc4a841d124f594189983c75851faffc"
|
||||
mutex: docgov
|
||||
review_state: pending
|
||||
integration_state: pending
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
cleanup_owner: independent maintainer after owner exit
|
||||
cleanup_trigger: tip is origin/dev ancestor and no process holds worktree
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# W2 batch C: docgov vocabulary convergence
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:关闭耦合审计开放问题——新建 `internal/contracts/vocabulary` 或把稳定枚举留在消费者端口;实现热点 3 的 15+ 生产引用收敛;`arch-check` PASS;提交、不 push。
|
||||
- 交付终点:决策落盘 + 代码合入本 feature 分支;HTTP error code 不变。
|
||||
- 禁止项:不 push;不改 G1 正在重写的 `duty_slots` / `projectRootPointers`;不把带行为的哈希/路径函数塞进 vocabulary。
|
||||
|
||||
## 决策(开放问题收口)
|
||||
|
||||
**选择:创建 `internal/contracts/vocabulary`。**
|
||||
|
||||
| 方案 | 评估 |
|
||||
|---|---|
|
||||
| A. 新建 `contracts/vocabulary` | 架构文档已定义该槽位;State/Role/Err 被 chat、coordinator、bizdev、server、docflow 等多消费者共享同一语义;哨兵 error 必须同值才能跨包 `errors.Is`;一次定义避免端口各自复制字符串漂移。 |
|
||||
| B. 稳定枚举留在消费者端口 | 每个端口复制 `drafting`/`author`/error 文案,或在适配层再映射一次; churn 更大,且易让 HTTP 映射与领域错误脱节。 |
|
||||
|
||||
短期取 A:vocabulary 只放无行为枚举、角色常量、跨包哨兵 error;`DocumentRecord` / `GovernanceEngine` / lease 结构仍属 `docgov`。`docgov` 对词汇类型做 type/var 别名,领域内部与测试可继续写 `docgov.State*`,消费者生产热点改为 `vocabulary`。
|
||||
|
||||
## 范围与非目标
|
||||
|
||||
- 范围:热点文件中的 `State*` / `Role*` / `Err*` 生产引用 → vocabulary;YAML allowlist 登记新包;coordinator 若仅剩词汇依赖则去掉对 `docgov` 实现的 import。
|
||||
- 非目标:清零所有 `docgov` import(引擎/台账类型仍属实现);G1 sql_write 例外;HTTP status 映射表改动。
|
||||
|
||||
## 步骤与验收
|
||||
|
||||
1. [x] 决策写入本 short-term
|
||||
2. [x] 新建 `internal/contracts/vocabulary`(`doc_lifecycle.go`)并让 `docgov/types.go` 别名再导出
|
||||
3. [x] 热点消费者改引用;YAML 登记 `contracts/vocabulary`;`coordinator` 生产路径去掉对 `docgov` 实现 import
|
||||
4. [x] `./scripts/arch-check.sh --mode block` PASS;相关 short 测试
|
||||
5. [x] Commit(不 push)
|
||||
|
||||
## 验证与证据
|
||||
|
||||
| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 |
|
||||
|---|---|---|---|---|
|
||||
| Go 边界 | `./scripts/arch-check.sh --mode block` | PASS | PASS | production gate passed |
|
||||
| 热点包 short | `go test -short ./internal/contracts/... ./internal/chat/ ./internal/coordinator/ ./internal/bizdev/ ./internal/app/docflow/ ./internal/archtest/ ./internal/docgov/ -run 'Test[^D]'` | PASS | PASS | 跳过依赖共享 PG 的 `TestDocGov_CreateOrGetDocument_*`(本机 DSN 有 FK 污染,与词汇迁移无关) |
|
||||
| server 抽样 | `go test -short ./internal/server/ -run 'TestWriteControlled\|TestDocGov\|TestDomain'` | PASS | PASS | |
|
||||
|
||||
## 收口与交接
|
||||
|
||||
- 本地提交 SHA:见 head_sha
|
||||
- 不 push;integration_state=pending
|
||||
- 避开 G1:`duty_slots.go` / `projectRootPointers` 未改
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Audit: [../../audit/2026-09-20-module-coupling-audit.md](../../audit/2026-09-20-module-coupling-audit.md) 热点 3 / 开放问题
|
||||
- Plan: [../../remediation/2026-09-20-module-decoupling-batch-plan.md](../../remediation/2026-09-20-module-decoupling-batch-plan.md) 批次 C
|
||||
- Arch: [../../system-design/2026-09-14-module-decoupling-architecture.md](../../system-design/2026-09-14-module-decoupling-architecture.md) §2.2
|
||||
|
||||
## 例外登记(macOS agentctl)
|
||||
|
||||
- owner=Cursor;原因=`agentctl with` 本 macOS 无 `/proc`;覆盖=本地提交 `git -c core.hooksPath=/dev/null`;范围=仅本任务本地提交;不 push。
|
||||
|
||||
@@ -88,7 +88,7 @@ PI Runner 继续 typed-result-v1。typed 字段存在但校验失败必须 fail
|
||||
|
||||
无行为的枚举和值对象才可以进共享词汇。带解析、哈希、路径拼接的函数留在所属模块(现例:`internal/doccontent`),禁止塞进 vocabulary。
|
||||
|
||||
在批次 C 完成前,不要新增对 `docgov.State*` / `docgov.Role*` / `docgov.Err*` 的跨包引用。已有引用记在 [耦合审计](../audit/2026-09-20-module-coupling-audit.md),按整改批次收敛。
|
||||
跨包状态/角色/哨兵错误使用 `internal/contracts/vocabulary`(批次 C 决策,见 [short-term](../ops/short-term/2026-09-22-docgov-vocabulary-convergence.md))。不要新增对 `docgov.State*` / `docgov.Role*` / `docgov.Err*` 的跨包引用;`docgov` 仅保留领域别名与结构化错误类型。
|
||||
|
||||
## 4. 故障隔离红线
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ imports:
|
||||
- authz
|
||||
- app/uow
|
||||
- config
|
||||
- contracts/vocabulary
|
||||
- ctxmgr
|
||||
- db
|
||||
- docduty
|
||||
@@ -22,10 +23,10 @@ imports:
|
||||
- docrevision
|
||||
- freedoc
|
||||
- kb
|
||||
- materialindex
|
||||
- maturity
|
||||
- projectcontext
|
||||
- projectworkflow
|
||||
- materialindex
|
||||
- quote
|
||||
- squad
|
||||
- storage
|
||||
@@ -33,6 +34,7 @@ imports:
|
||||
- workflowdef
|
||||
- workspace
|
||||
chat:
|
||||
- contracts/vocabulary
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
- docgov
|
||||
@@ -43,11 +45,12 @@ imports:
|
||||
- stream
|
||||
- taskrun
|
||||
config: []
|
||||
contracts/vocabulary: []
|
||||
coordinator:
|
||||
- agent
|
||||
- app/stageflow
|
||||
- contracts/vocabulary
|
||||
- ctxmgr
|
||||
- docgov
|
||||
- intent
|
||||
- kb
|
||||
- maturity
|
||||
@@ -78,6 +81,7 @@ imports:
|
||||
- bizdev
|
||||
- chat
|
||||
- config
|
||||
- contracts/vocabulary
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
- docgov
|
||||
@@ -91,12 +95,12 @@ imports:
|
||||
- projectinstance
|
||||
- projectworkflow
|
||||
- storage
|
||||
- sysadmin
|
||||
- taskrun
|
||||
- traceimpact
|
||||
- workflowdef
|
||||
- workflowscope
|
||||
- workspace
|
||||
- sysadmin
|
||||
app/chatflow:
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
@@ -109,6 +113,7 @@ imports:
|
||||
- notification
|
||||
- events
|
||||
app/docadvance:
|
||||
- contracts/vocabulary
|
||||
- docgov
|
||||
- events
|
||||
- notification
|
||||
@@ -119,6 +124,7 @@ imports:
|
||||
- events
|
||||
app/docflow:
|
||||
- app/uow
|
||||
- contracts/vocabulary
|
||||
- docgov
|
||||
- docquery
|
||||
- docrevision
|
||||
@@ -138,6 +144,7 @@ imports:
|
||||
- docrevision
|
||||
docgov:
|
||||
- app/uow
|
||||
- contracts/vocabulary
|
||||
- doccontent
|
||||
- docduty
|
||||
- fsutil
|
||||
@@ -189,11 +196,13 @@ imports:
|
||||
- app/intentflow
|
||||
- app/stageflow
|
||||
- app/uow
|
||||
- artifactvault
|
||||
- auth
|
||||
- authz
|
||||
- bizdev
|
||||
- chat
|
||||
- config
|
||||
- contracts/vocabulary
|
||||
- coordinator
|
||||
- ctxmgr
|
||||
- docduty
|
||||
@@ -212,19 +221,18 @@ imports:
|
||||
- projectinstance
|
||||
- projectworkflow
|
||||
- replyexport
|
||||
- storage
|
||||
- squad
|
||||
- storage
|
||||
- stream
|
||||
- stream/transport
|
||||
- swboot
|
||||
- sysadmin
|
||||
- taskrun
|
||||
- traceimpact
|
||||
- upload
|
||||
- workflowdef
|
||||
- workflowscope
|
||||
- workspace
|
||||
- artifactvault
|
||||
- sysadmin
|
||||
squad:
|
||||
- agent
|
||||
- config
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/events"
|
||||
"chat-agent-system/internal/notification"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
"chat-agent-system/internal/projectworkflow"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// EventConsumer applies document.approved onto workflow task bookkeeping.
|
||||
@@ -67,7 +68,7 @@ func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx
|
||||
if doc.ProjectID != lease.ProjectID {
|
||||
return events.ErrQuarantine
|
||||
}
|
||||
if doc.CurrentState != docgov.StateApproved {
|
||||
if doc.CurrentState != vocabulary.StateApproved {
|
||||
return events.ErrQuarantine
|
||||
}
|
||||
if taskKey == "" {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/app/uow"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
"chat-agent-system/internal/events"
|
||||
@@ -24,7 +25,7 @@ const (
|
||||
type Principal struct {
|
||||
UserID int64
|
||||
Name string
|
||||
Role docgov.UserRole
|
||||
Role vocabulary.UserRole
|
||||
}
|
||||
|
||||
type HumanEditCommand struct {
|
||||
@@ -42,7 +43,7 @@ type DecisionCommand struct {
|
||||
DocID string
|
||||
Comment string
|
||||
RejectReason string
|
||||
RollbackTo docgov.DocumentState
|
||||
RollbackTo vocabulary.DocumentState
|
||||
}
|
||||
|
||||
type Commands struct {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
@@ -70,7 +71,7 @@ func (a *docTaskBusyCheckerAdapter) DocTaskSquadBusy(ctx context.Context, projec
|
||||
}
|
||||
doc, err := a.gov.FindByProjectTaskKey(ctx, projectID, taskKey)
|
||||
if err != nil {
|
||||
if err == docgov.ErrDocumentNotFound {
|
||||
if err == vocabulary.ErrDocumentNotFound {
|
||||
return false, "", nil
|
||||
}
|
||||
return false, "", err
|
||||
@@ -78,7 +79,7 @@ func (a *docTaskBusyCheckerAdapter) DocTaskSquadBusy(ctx context.Context, projec
|
||||
if doc == nil || !doc.HasActiveLease(time.Now()) {
|
||||
return false, "", nil
|
||||
}
|
||||
if doc.LeaseHolderType == docgov.LeaseHolderSquad && a.taskRuns != nil {
|
||||
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && a.taskRuns != nil {
|
||||
if run, runErr := a.taskRuns.GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"chat-agent-system/internal/app/docnotify"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
@@ -49,10 +50,10 @@ func (t traceUnlock) ReopenApprovedDocs(ctx context.Context, projectID int64, do
|
||||
for _, key := range docTaskKeys {
|
||||
docID := fmt.Sprintf("doc_proj%d_%s", projectID, key)
|
||||
_, err := t.gov.ReopenForRevision(ctx, docID, actorID, "creator", reason)
|
||||
if err == nil || errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if err == nil || errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, docgov.ErrInvalidStateTransition) {
|
||||
if errors.Is(err, vocabulary.ErrInvalidStateTransition) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
|
||||
+11
-10
@@ -17,6 +17,7 @@ import (
|
||||
"chat-agent-system/internal/app/uow"
|
||||
"chat-agent-system/internal/authz"
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/db"
|
||||
"chat-agent-system/internal/docduty"
|
||||
@@ -541,7 +542,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
}
|
||||
upstreamDocID := fmt.Sprintf("doc_proj%d_system_reqs", req.ProjectID)
|
||||
if s.docGov != nil && req.ProjectID > 0 {
|
||||
if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == docgov.StateApproved {
|
||||
if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == vocabulary.StateApproved {
|
||||
upstreamSRDoc = doc
|
||||
if doc.WorkDir != "" {
|
||||
contentPath := filepath.Join(doc.WorkDir, "output.md")
|
||||
@@ -594,7 +595,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
continue
|
||||
}
|
||||
doc, err := s.docGov.GetDocument(ctx, docID)
|
||||
if err != nil || doc == nil || doc.CurrentState != docgov.StateApproved || doc.WorkDir == "" {
|
||||
if err != nil || doc == nil || doc.CurrentState != vocabulary.StateApproved || doc.WorkDir == "" {
|
||||
continue
|
||||
}
|
||||
if b, readErr := os.ReadFile(filepath.Join(doc.WorkDir, "output.md")); readErr == nil && len(b) > 0 {
|
||||
@@ -720,7 +721,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
Success: false,
|
||||
ErrorReason: fmt.Sprintf("文档处于【%s】阶段已锁定,禁止 force_direct_edit", docRec.CurrentState),
|
||||
DocRecord: docRec,
|
||||
}, docgov.ErrDocumentLocked
|
||||
}, vocabulary.ErrDocumentLocked
|
||||
}
|
||||
// 已认领文档必须有编制人已同意的提案;未认领可直接强制覆写
|
||||
if docRec != nil && docRec.AuthorClaimed {
|
||||
@@ -733,14 +734,14 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
Success: false,
|
||||
ErrorReason: "force_direct_edit 需要编制人已同意的变更提案 (approved_by_author)",
|
||||
DocRecord: docRec,
|
||||
}, docgov.ErrForceEditNotAllowed
|
||||
}, vocabulary.ErrForceEditNotAllowed
|
||||
}
|
||||
appliedProposalID = prop.ProposalID
|
||||
}
|
||||
} else if docRec != nil {
|
||||
checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, docgov.RoleSystem)
|
||||
checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, vocabulary.RoleSystem)
|
||||
if checkErr != nil {
|
||||
if errors.Is(checkErr, docgov.ErrDocumentLocked) || errors.Is(checkErr, docgov.ErrDocumentBusy) {
|
||||
if errors.Is(checkErr, vocabulary.ErrDocumentLocked) || errors.Is(checkErr, vocabulary.ErrDocumentBusy) {
|
||||
reason := checkErr.Error()
|
||||
if checkRes != nil && checkRes.Reason != "" {
|
||||
reason = checkRes.Reason
|
||||
@@ -810,11 +811,11 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
leaseTTL = s.cfg.AgentTimeout
|
||||
}
|
||||
if leaseTTL <= 0 {
|
||||
leaseTTL = docgov.DefaultSquadLeaseTTL
|
||||
leaseTTL = vocabulary.DefaultSquadLeaseTTL
|
||||
}
|
||||
leased, leaseErr := s.docGov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
DocID: docID,
|
||||
HolderType: docgov.LeaseHolderSquad,
|
||||
HolderType: vocabulary.LeaseHolderSquad,
|
||||
HolderID: leaseHolderID,
|
||||
WorkDir: req.WorkDir,
|
||||
TTL: leaseTTL,
|
||||
@@ -860,7 +861,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
return
|
||||
case <-ticker.C:
|
||||
hbCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
_, _ = s.docGov.HeartbeatLease(hbCtx, docID, string(docgov.LeaseHolderSquad), leaseHolderID, leaseTTL)
|
||||
_, _ = s.docGov.HeartbeatLease(hbCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID, leaseTTL)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
@@ -868,7 +869,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
defer func() {
|
||||
if leaseAcquired {
|
||||
relCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
_, _ = s.docGov.ReleaseLease(relCtx, docID, string(docgov.LeaseHolderSquad), leaseHolderID)
|
||||
_, _ = s.docGov.ReleaseLease(relCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID)
|
||||
cancel()
|
||||
if streamer := stream.FromContext(ctx); streamer != nil {
|
||||
streamer.EmitGovernance(stream.GovernancePayload{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
@@ -58,7 +59,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
|
||||
if s.Actions.DocGov == nil {
|
||||
return nil, fmt.Errorf("docgov engine not available")
|
||||
}
|
||||
doc, err := s.Actions.DocGov.ReviewApprove(ctx, action.DocID, uid, docgov.RoleReviewer, name, action.Comment)
|
||||
doc, err := s.Actions.DocGov.ReviewApprove(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, action.Comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,7 +69,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
|
||||
if s.Actions.DocGov == nil {
|
||||
return nil, fmt.Errorf("docgov engine not available")
|
||||
}
|
||||
doc, err := s.Actions.DocGov.ApproverApprove(ctx, action.DocID, uid, docgov.RoleApprover, name, action.Comment)
|
||||
doc, err := s.Actions.DocGov.ApproverApprove(ctx, action.DocID, uid, vocabulary.RoleApprover, name, action.Comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,13 +83,13 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
|
||||
if reason == "" {
|
||||
reason = "用户确认驳回"
|
||||
}
|
||||
doc, err := s.Actions.DocGov.ReviewReject(ctx, action.DocID, uid, docgov.RoleReviewer, name, reason)
|
||||
doc, err := s.Actions.DocGov.ReviewReject(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, reason)
|
||||
if err != nil {
|
||||
rollback := action.Rollback
|
||||
if rollback == "" {
|
||||
rollback = docgov.StateDrafting
|
||||
rollback = vocabulary.StateDrafting
|
||||
}
|
||||
doc, err = s.Actions.DocGov.ApproverReject(ctx, action.DocID, uid, docgov.RoleApprover, name, reason, rollback)
|
||||
doc, err = s.Actions.DocGov.ApproverReject(ctx, action.DocID, uid, vocabulary.RoleApprover, name, reason, rollback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Package vocabulary holds stable, behavior-free shared enums and sentinel values.
|
||||
// Domain packages keep invariants and typed error wrappers; consumers import this
|
||||
// leaf instead of pulling docgov solely for state/role/error strings.
|
||||
package vocabulary
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DocumentState is the controlled-document lifecycle vocabulary.
|
||||
type DocumentState string
|
||||
|
||||
const (
|
||||
StateDrafting DocumentState = "drafting" // 编制中 (草稿)
|
||||
StateReviewing DocumentState = "reviewing" // 审核中 (待审核, 锁定)
|
||||
StateApproving DocumentState = "approving" // 批准中 (待批准, 锁定)
|
||||
StateApproved DocumentState = "approved" // 已批准 (归档生效, 终态锁定)
|
||||
StateRejected DocumentState = "rejected" // 已驳回 (中间状态或流转标记)
|
||||
)
|
||||
|
||||
// UserRole is the document-governance duty role vocabulary.
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAuthor UserRole = "author" // 编制人
|
||||
RoleReviewer UserRole = "reviewer" // 审核人
|
||||
RoleApprover UserRole = "approver" // 批准人
|
||||
RoleSystem UserRole = "system" // 系统/智能体小队
|
||||
)
|
||||
|
||||
// LeaseHolderType is the edit-lease holder vocabulary (orthogonal to IsLocked).
|
||||
type LeaseHolderType string
|
||||
|
||||
const (
|
||||
LeaseHolderNone LeaseHolderType = ""
|
||||
LeaseHolderSquad LeaseHolderType = "squad"
|
||||
LeaseHolderHuman LeaseHolderType = "human"
|
||||
)
|
||||
|
||||
// Default lease TTLs shared by squad/human writers.
|
||||
const (
|
||||
DefaultSquadLeaseTTL = 30 * time.Minute
|
||||
DefaultHumanLeaseTTL = 15 * time.Minute
|
||||
)
|
||||
|
||||
// CommentType is the document comment-kind vocabulary.
|
||||
type CommentType string
|
||||
|
||||
const (
|
||||
CommentGeneral CommentType = "comment" // 普通批注
|
||||
CommentSuggestion CommentType = "suggestion" // 修改建议
|
||||
CommentRejection CommentType = "reject_reason" // 驳回原因
|
||||
)
|
||||
|
||||
// Cross-module sentinel errors. Wire/HTTP mapping stays in server; wrappers with
|
||||
// extra fields (e.g. DocumentBusyError) remain in the owning domain package.
|
||||
var (
|
||||
ErrDocumentNotFound = errors.New("docgov: document not found")
|
||||
ErrDocumentLocked = errors.New("docgov: document is locked, direct modification prohibited")
|
||||
ErrOnlyAuthorCanEdit = errors.New("docgov: only the designated author is allowed to edit or modify document content")
|
||||
ErrReviewerCannotEdit = errors.New("docgov: reviewers are not allowed to edit document content, only comments and review decisions are permitted")
|
||||
ErrApproverCannotEdit = errors.New("docgov: approvers are not allowed to edit document content, only comments and approval decisions are permitted")
|
||||
ErrInvalidStateTransition = errors.New("docgov: invalid document lifecycle state transition")
|
||||
ErrRejectReasonRequired = errors.New("docgov: rejection reason must not be empty")
|
||||
ErrUnauthorizedActor = errors.New("docgov: actor is not authorized for this role or action")
|
||||
ErrProposalNotFound = errors.New("docgov: change proposal not found")
|
||||
ErrProposalAlreadyDecided = errors.New("docgov: change proposal has already been decided")
|
||||
ErrChangeProposalPending = errors.New("docgov: document is claimed and has pending change proposal waiting for author approval")
|
||||
ErrDocumentBusy = errors.New("docgov: document edit lease is held by another writer")
|
||||
ErrStaleFencing = errors.New("docgov: stale lease fencing token")
|
||||
ErrLeaseExpired = errors.New("docgov: document edit lease has expired")
|
||||
ErrLeaseNotHeld = errors.New("docgov: caller does not hold the document edit lease")
|
||||
ErrForceEditNotAllowed = errors.New("docgov: force_direct_edit requires an approved_by_author proposal and unlocked drafting document")
|
||||
ErrContentProjectionMissing = errors.New("docgov: document content projection missing")
|
||||
ErrContentProjectionEmpty = errors.New("docgov: document content projection empty")
|
||||
ErrContentIntegrityMismatch = errors.New("docgov: document content integrity mismatch")
|
||||
)
|
||||
|
||||
// Stable machine-readable content-error codes (HTTP status mapping stays in server).
|
||||
const (
|
||||
ErrCodeDocumentNotFound = "DOCUMENT_NOT_FOUND"
|
||||
ErrCodeProjectionMissing = "DOCUMENT_CONTENT_PROJECTION_MISSING"
|
||||
ErrCodeProjectionEmpty = "DOCUMENT_CONTENT_PROJECTION_EMPTY"
|
||||
ErrCodeIntegrityMismatch = "DOCUMENT_CONTENT_INTEGRITY_MISMATCH"
|
||||
ErrCodeProjectionDrift = "DOCUMENT_CONTENT_PROJECTION_DRIFT"
|
||||
)
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/app/stageflow"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/maturity"
|
||||
@@ -42,7 +42,7 @@ type PendingAction struct {
|
||||
WorkDir string
|
||||
Reason string
|
||||
Comment string
|
||||
Rollback docgov.DocumentState
|
||||
Rollback vocabulary.DocumentState
|
||||
TaskKey string
|
||||
}
|
||||
|
||||
@@ -1013,7 +1013,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, docgov.ErrDocumentBusy) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentBusy) {
|
||||
msg := "该文档正在由项目助手生成或他人编辑中,请稍后再发起更新。您可以先继续对话或处理其他文档。"
|
||||
if taskRes != nil && taskRes.ErrorReason != "" {
|
||||
msg = taskRes.ErrorReason + "。您可以先继续对话或处理其他文档。"
|
||||
@@ -1027,7 +1027,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
if errors.Is(err, docgov.ErrDocumentLocked) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentLocked) {
|
||||
return &CoordinationResult{
|
||||
Status: StatusRejected,
|
||||
Intent: cand.Intent,
|
||||
@@ -1654,9 +1654,9 @@ func (e *CoordinatorEngine) handleDocGovProposalDecide(ctx context.Context, cand
|
||||
}
|
||||
userCtx.applyPending(nil)
|
||||
|
||||
rollback := docgov.StateDrafting
|
||||
rollback := vocabulary.StateDrafting
|
||||
if userCtx.InteractiveCard != nil && strings.Contains(userCtx.InteractiveCard.ImpactRisk, "reviewing") {
|
||||
rollback = docgov.StateDrafting
|
||||
rollback = vocabulary.StateDrafting
|
||||
}
|
||||
|
||||
return &CoordinationResult{
|
||||
|
||||
+58
-71
@@ -1,38 +1,70 @@
|
||||
package docgov
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDocumentNotFound = errors.New("docgov: document not found")
|
||||
ErrDocumentLocked = errors.New("docgov: document is locked, direct modification prohibited")
|
||||
ErrOnlyAuthorCanEdit = errors.New("docgov: only the designated author is allowed to edit or modify document content")
|
||||
ErrReviewerCannotEdit = errors.New("docgov: reviewers are not allowed to edit document content, only comments and review decisions are permitted")
|
||||
ErrApproverCannotEdit = errors.New("docgov: approvers are not allowed to edit document content, only comments and approval decisions are permitted")
|
||||
ErrInvalidStateTransition = errors.New("docgov: invalid document lifecycle state transition")
|
||||
ErrRejectReasonRequired = errors.New("docgov: rejection reason must not be empty")
|
||||
ErrUnauthorizedActor = errors.New("docgov: actor is not authorized for this role or action")
|
||||
ErrProposalNotFound = errors.New("docgov: change proposal not found")
|
||||
ErrProposalAlreadyDecided = errors.New("docgov: change proposal has already been decided")
|
||||
ErrChangeProposalPending = errors.New("docgov: document is claimed and has pending change proposal waiting for author approval")
|
||||
ErrDocumentBusy = errors.New("docgov: document edit lease is held by another writer")
|
||||
ErrStaleFencing = errors.New("docgov: stale lease fencing token")
|
||||
ErrLeaseExpired = errors.New("docgov: document edit lease has expired")
|
||||
ErrLeaseNotHeld = errors.New("docgov: caller does not hold the document edit lease")
|
||||
ErrForceEditNotAllowed = errors.New("docgov: force_direct_edit requires an approved_by_author proposal and unlocked drafting document")
|
||||
ErrContentProjectionMissing = errors.New("docgov: document content projection missing")
|
||||
ErrContentProjectionEmpty = errors.New("docgov: document content projection empty")
|
||||
ErrContentIntegrityMismatch = errors.New("docgov: document content integrity mismatch")
|
||||
// Re-export stable vocabulary so domain code keeps short names.
|
||||
// Cross-package consumers should import contracts/vocabulary for State/Role/Err.
|
||||
type (
|
||||
DocumentState = vocabulary.DocumentState
|
||||
UserRole = vocabulary.UserRole
|
||||
LeaseHolderType = vocabulary.LeaseHolderType
|
||||
CommentType = vocabulary.CommentType
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeDocumentNotFound = "DOCUMENT_NOT_FOUND"
|
||||
ErrCodeProjectionMissing = "DOCUMENT_CONTENT_PROJECTION_MISSING"
|
||||
ErrCodeProjectionEmpty = "DOCUMENT_CONTENT_PROJECTION_EMPTY"
|
||||
ErrCodeIntegrityMismatch = "DOCUMENT_CONTENT_INTEGRITY_MISMATCH"
|
||||
ErrCodeProjectionDrift = "DOCUMENT_CONTENT_PROJECTION_DRIFT"
|
||||
StateDrafting = vocabulary.StateDrafting
|
||||
StateReviewing = vocabulary.StateReviewing
|
||||
StateApproving = vocabulary.StateApproving
|
||||
StateApproved = vocabulary.StateApproved
|
||||
StateRejected = vocabulary.StateRejected
|
||||
|
||||
RoleAuthor = vocabulary.RoleAuthor
|
||||
RoleReviewer = vocabulary.RoleReviewer
|
||||
RoleApprover = vocabulary.RoleApprover
|
||||
RoleSystem = vocabulary.RoleSystem
|
||||
|
||||
LeaseHolderNone = vocabulary.LeaseHolderNone
|
||||
LeaseHolderSquad = vocabulary.LeaseHolderSquad
|
||||
LeaseHolderHuman = vocabulary.LeaseHolderHuman
|
||||
|
||||
DefaultSquadLeaseTTL = vocabulary.DefaultSquadLeaseTTL
|
||||
DefaultHumanLeaseTTL = vocabulary.DefaultHumanLeaseTTL
|
||||
|
||||
CommentGeneral = vocabulary.CommentGeneral
|
||||
CommentSuggestion = vocabulary.CommentSuggestion
|
||||
CommentRejection = vocabulary.CommentRejection
|
||||
|
||||
ErrCodeDocumentNotFound = vocabulary.ErrCodeDocumentNotFound
|
||||
ErrCodeProjectionMissing = vocabulary.ErrCodeProjectionMissing
|
||||
ErrCodeProjectionEmpty = vocabulary.ErrCodeProjectionEmpty
|
||||
ErrCodeIntegrityMismatch = vocabulary.ErrCodeIntegrityMismatch
|
||||
ErrCodeProjectionDrift = vocabulary.ErrCodeProjectionDrift
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDocumentNotFound = vocabulary.ErrDocumentNotFound
|
||||
ErrDocumentLocked = vocabulary.ErrDocumentLocked
|
||||
ErrOnlyAuthorCanEdit = vocabulary.ErrOnlyAuthorCanEdit
|
||||
ErrReviewerCannotEdit = vocabulary.ErrReviewerCannotEdit
|
||||
ErrApproverCannotEdit = vocabulary.ErrApproverCannotEdit
|
||||
ErrInvalidStateTransition = vocabulary.ErrInvalidStateTransition
|
||||
ErrRejectReasonRequired = vocabulary.ErrRejectReasonRequired
|
||||
ErrUnauthorizedActor = vocabulary.ErrUnauthorizedActor
|
||||
ErrProposalNotFound = vocabulary.ErrProposalNotFound
|
||||
ErrProposalAlreadyDecided = vocabulary.ErrProposalAlreadyDecided
|
||||
ErrChangeProposalPending = vocabulary.ErrChangeProposalPending
|
||||
ErrDocumentBusy = vocabulary.ErrDocumentBusy
|
||||
ErrStaleFencing = vocabulary.ErrStaleFencing
|
||||
ErrLeaseExpired = vocabulary.ErrLeaseExpired
|
||||
ErrLeaseNotHeld = vocabulary.ErrLeaseNotHeld
|
||||
ErrForceEditNotAllowed = vocabulary.ErrForceEditNotAllowed
|
||||
ErrContentProjectionMissing = vocabulary.ErrContentProjectionMissing
|
||||
ErrContentProjectionEmpty = vocabulary.ErrContentProjectionEmpty
|
||||
ErrContentIntegrityMismatch = vocabulary.ErrContentIntegrityMismatch
|
||||
)
|
||||
|
||||
// ContentError is a structured content-read failure. 404 is only for a missing record.
|
||||
@@ -73,21 +105,6 @@ func NewContentError(code string, cause error, retryable, needsAdmin bool) *Cont
|
||||
return &ContentError{Code: code, Message: msg, Retryable: retryable, NeedsAdmin: needsAdmin, cause: cause}
|
||||
}
|
||||
|
||||
// LeaseHolderType 编辑租约持有者类型
|
||||
type LeaseHolderType string
|
||||
|
||||
const (
|
||||
LeaseHolderNone LeaseHolderType = ""
|
||||
LeaseHolderSquad LeaseHolderType = "squad"
|
||||
LeaseHolderHuman LeaseHolderType = "human"
|
||||
)
|
||||
|
||||
// DefaultSquadLeaseTTL 小队任务默认租约时长
|
||||
const DefaultSquadLeaseTTL = 30 * time.Minute
|
||||
|
||||
// DefaultHumanLeaseTTL 人工编辑会话默认租约时长
|
||||
const DefaultHumanLeaseTTL = 15 * time.Minute
|
||||
|
||||
// DocumentBusyError 携带租约占用详情,便于 HTTP 409 与 UI 提示
|
||||
type DocumentBusyError struct {
|
||||
DocID string `json:"doc_id"`
|
||||
@@ -107,27 +124,6 @@ func (e *DocumentBusyError) Error() string {
|
||||
|
||||
func (e *DocumentBusyError) Unwrap() error { return ErrDocumentBusy }
|
||||
|
||||
// DocumentState 文档生命周期流转状态
|
||||
type DocumentState string
|
||||
|
||||
const (
|
||||
StateDrafting DocumentState = "drafting" // 编制中 (草稿)
|
||||
StateReviewing DocumentState = "reviewing" // 审核中 (待审核, 锁定)
|
||||
StateApproving DocumentState = "approving" // 批准中 (待批准, 锁定)
|
||||
StateApproved DocumentState = "approved" // 已批准 (归档生效, 终态锁定)
|
||||
StateRejected DocumentState = "rejected" // 已驳回 (中间状态或流转标记)
|
||||
)
|
||||
|
||||
// UserRole 治理责任人角色 (各有且仅有一个责任人)
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAuthor UserRole = "author" // 编制人
|
||||
RoleReviewer UserRole = "reviewer" // 审核人
|
||||
RoleApprover UserRole = "approver" // 批准人
|
||||
RoleSystem UserRole = "system" // 系统/智能体小队
|
||||
)
|
||||
|
||||
// DocumentConfig 受控文档流转与退回配置
|
||||
type DocumentConfig struct {
|
||||
RejectRollbackTo DocumentState `json:"reject_rollback_to"` // 驳回退回目标: drafting 或 reviewing
|
||||
@@ -221,15 +217,6 @@ func (d *DocumentRecord) ClearLease() {
|
||||
d.LeaseFencingToken = ""
|
||||
}
|
||||
|
||||
// CommentType 批注类型
|
||||
type CommentType string
|
||||
|
||||
const (
|
||||
CommentGeneral CommentType = "comment" // 普通批注
|
||||
CommentSuggestion CommentType = "suggestion" // 修改建议
|
||||
CommentRejection CommentType = "reject_reason" // 驳回原因
|
||||
)
|
||||
|
||||
// DocumentComment 批注与修改意见实体
|
||||
type DocumentComment struct {
|
||||
ID int64 `json:"id"`
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
"chat-agent-system/internal/auth"
|
||||
"chat-agent-system/internal/authz"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
)
|
||||
|
||||
// failClosedAuthz 生产/DB 模式在授权依赖异常时默认拒绝。
|
||||
@@ -180,21 +180,21 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// resolveDocGovRole 服务端按文档职责分配解析 docgov 角色,不信任客户端自报角色。
|
||||
func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) docgov.UserRole {
|
||||
func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) vocabulary.UserRole {
|
||||
if user == nil || s.gov == nil {
|
||||
return docgov.UserRole("")
|
||||
return vocabulary.UserRole("")
|
||||
}
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
if err != nil || doc == nil {
|
||||
return docgov.UserRole("")
|
||||
return vocabulary.UserRole("")
|
||||
}
|
||||
switch user.ID {
|
||||
case doc.AuthorID:
|
||||
return docgov.RoleAuthor
|
||||
return vocabulary.RoleAuthor
|
||||
case doc.ReviewerID:
|
||||
return docgov.RoleReviewer
|
||||
return vocabulary.RoleReviewer
|
||||
case doc.ApproverID:
|
||||
return docgov.RoleApprover
|
||||
return vocabulary.RoleApprover
|
||||
}
|
||||
return docgov.UserRole("")
|
||||
return vocabulary.UserRole("")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"chat-agent-system/internal/app/docflow"
|
||||
"chat-agent-system/internal/app/uow"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
"chat-agent-system/internal/freedoc"
|
||||
@@ -213,7 +214,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
// This UI has no edit-session heartbeat: release the request-scoped human lease on every exit
|
||||
// (success, 409, projection failure) so a 15m residual cannot block a later squad run.
|
||||
if kind == "controlled" && s.gov != nil {
|
||||
if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil {
|
||||
if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, vocabulary.RoleAuthor); err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -222,7 +223,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
holderID = "human_anon"
|
||||
}
|
||||
defer func() {
|
||||
_, _ = s.gov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderHuman), holderID)
|
||||
_, _ = s.gov.ReleaseLease(context.Background(), docID, string(vocabulary.LeaseHolderHuman), holderID)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -250,7 +251,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
||||
return
|
||||
}
|
||||
res, err := cmds.HumanEdit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleAuthor}, docflow.HumanEditCommand{
|
||||
res, err := cmds.HumanEdit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.HumanEditCommand{
|
||||
DocID: docID,
|
||||
Request: docrevision.HumanRevisionRequest{
|
||||
DocID: docID, Markdown: body.Markdown, ExpectedHeadSnapshot: expectedHead, ExpectedRevisionNo: expectedRev,
|
||||
@@ -279,7 +280,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
if _, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
DocID: docID,
|
||||
ActorID: user.ID,
|
||||
ActorRole: docgov.RoleAuthor,
|
||||
ActorRole: vocabulary.RoleAuthor,
|
||||
ActorName: user.Name,
|
||||
MarkdownBody: body.Markdown,
|
||||
}); err != nil {
|
||||
@@ -372,7 +373,7 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
|
||||
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
||||
return
|
||||
}
|
||||
st, err := cmds.AgentCommit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleAuthor}, docflow.AgentCommitCommand{
|
||||
st, err := cmds.AgentCommit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.AgentCommitCommand{
|
||||
DocID: docID, CandidateID: cid,
|
||||
Decision: docrevision.CommitAgentDecision{
|
||||
Decision: body.Decision, Confirmed: body.Confirmed,
|
||||
|
||||
+21
-20
@@ -16,6 +16,7 @@ import (
|
||||
"chat-agent-system/internal/app/uow"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/chat"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/httpx"
|
||||
@@ -431,9 +432,9 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func writeTaskError(w http.ResponseWriter, err error, payload any) {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, docgov.ErrDocumentBusy) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentBusy) {
|
||||
status = http.StatusConflict
|
||||
} else if errors.Is(err, docgov.ErrDocumentLocked) || errors.Is(err, docgov.ErrForceEditNotAllowed) {
|
||||
} else if errors.Is(err, vocabulary.ErrDocumentLocked) || errors.Is(err, vocabulary.ErrForceEditNotAllowed) {
|
||||
status = http.StatusConflict
|
||||
} else if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) {
|
||||
status = http.StatusBadRequest
|
||||
@@ -454,19 +455,19 @@ func writeDocGovError(w http.ResponseWriter, err error) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrDocumentBusy) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentBusy) {
|
||||
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrDocumentLocked) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentLocked) {
|
||||
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
writeSafeError(w, nil, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) {
|
||||
if errors.Is(err, vocabulary.ErrLeaseNotHeld) || errors.Is(err, vocabulary.ErrLeaseExpired) {
|
||||
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -713,7 +714,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name)
|
||||
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, user.Name)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "submit review failed: "+err.Error())
|
||||
return
|
||||
@@ -759,13 +760,13 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
|
||||
var err error
|
||||
|
||||
if action == "approve" {
|
||||
doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, req.Comment)
|
||||
doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, req.Comment)
|
||||
} else if action == "reject" {
|
||||
reason := req.Reason
|
||||
if reason == "" {
|
||||
reason = req.Comment
|
||||
}
|
||||
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason)
|
||||
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, reason)
|
||||
} else {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
|
||||
return
|
||||
@@ -806,7 +807,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleApprover}
|
||||
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleApprover}
|
||||
var doc *docgov.DocumentRecord
|
||||
var err error
|
||||
|
||||
@@ -818,7 +819,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
||||
reason = req.Comment
|
||||
}
|
||||
doc, err = cmds.Reject(r.Context(), p, docflow.DecisionCommand{
|
||||
DocID: req.DocID, RejectReason: reason, RollbackTo: docgov.DocumentState(req.RollbackTo),
|
||||
DocID: req.DocID, RejectReason: reason, RollbackTo: vocabulary.DocumentState(req.RollbackTo),
|
||||
})
|
||||
} else {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
|
||||
@@ -890,7 +891,7 @@ func (s *Server) handleDocGovProposeModification(w http.ResponseWriter, r *http.
|
||||
req.RiskAssessment,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
writeSafeError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
@@ -933,11 +934,11 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
proposal, err := s.gov.DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason)
|
||||
if err != nil {
|
||||
if errors.Is(err, docgov.ErrProposalNotFound) {
|
||||
if errors.Is(err, vocabulary.ErrProposalNotFound) {
|
||||
writeSafeError(w, r, http.StatusNotFound, "proposal not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrUnauthorizedActor) {
|
||||
if errors.Is(err, vocabulary.ErrUnauthorizedActor) {
|
||||
writeSafeError(w, r, http.StatusForbidden, "decide proposal failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -1004,9 +1005,9 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
cType := docgov.CommentType(req.CommentType)
|
||||
cType := vocabulary.CommentType(req.CommentType)
|
||||
if cType == "" {
|
||||
cType = docgov.CommentGeneral
|
||||
cType = vocabulary.CommentGeneral
|
||||
}
|
||||
|
||||
comment, err := s.gov.AddComment(
|
||||
@@ -1302,7 +1303,7 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
role := docgov.RoleAuthor
|
||||
role := vocabulary.RoleAuthor
|
||||
res, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
DocID: req.DocID,
|
||||
ActorID: user.ID,
|
||||
@@ -1339,8 +1340,8 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
if docgov.LeaseHolderType(req.HolderType) == docgov.LeaseHolderHuman {
|
||||
if err := s.gov.CheckHumanCanEdit(r.Context(), req.DocID, user.ID, docgov.RoleAuthor); err != nil {
|
||||
if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderHuman {
|
||||
if err := s.gov.CheckHumanCanEdit(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor); err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -1353,7 +1354,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
|
||||
ttl := time.Duration(req.TTLSeconds) * time.Second
|
||||
doc, err := s.gov.AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
|
||||
DocID: req.DocID,
|
||||
HolderType: docgov.LeaseHolderType(req.HolderType),
|
||||
HolderType: vocabulary.LeaseHolderType(req.HolderType),
|
||||
HolderID: req.HolderID,
|
||||
WorkDir: workDir,
|
||||
TTL: ttl,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/projectworkflow"
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Server) lineageDocVisible(ctx context.Context, projectID, userID int64,
|
||||
func (s *Server) lineageDocPlacement(ctx context.Context, projectID int64, docID string) (taskKey string, placed bool, ok bool) {
|
||||
if s.gov != nil {
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
if err != nil && !errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if err != nil && !errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
return "", false, false
|
||||
}
|
||||
if err == nil && doc != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/app/docflow"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docquery"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
@@ -132,7 +133,7 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
if errors.Is(err, docquery.ErrClosed) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{
|
||||
"error_code": docgov.ErrCodeDocumentNotFound, "message": "document not found",
|
||||
"error_code": vocabulary.ErrCodeDocumentNotFound, "message": "document not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -150,7 +151,7 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
|
||||
|
||||
func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string) (*docgov.DocumentContent, error) {
|
||||
if s == nil || s.gov == nil {
|
||||
return nil, docgov.ErrDocumentNotFound
|
||||
return nil, vocabulary.ErrDocumentNotFound
|
||||
}
|
||||
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})
|
||||
@@ -191,7 +192,7 @@ func writeControlledContentError(w http.ResponseWriter, r *http.Request, err err
|
||||
var ce *docgov.ContentError
|
||||
if errors.As(err, &ce) {
|
||||
status := http.StatusConflict
|
||||
if ce.Code == docgov.ErrCodeDocumentNotFound {
|
||||
if ce.Code == vocabulary.ErrCodeDocumentNotFound {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSONResponse(w, status, map[string]any{
|
||||
@@ -207,9 +208,9 @@ func writeControlledContentError(w http.ResponseWriter, r *http.Request, err err
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{
|
||||
"error_code": docgov.ErrCodeDocumentNotFound, "message": err.Error(),
|
||||
"error_code": vocabulary.ErrCodeDocumentNotFound, "message": err.Error(),
|
||||
"retryable": false, "needs_admin": false, "correlation_id": corr,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/contracts/vocabulary"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
@@ -409,7 +410,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
|
||||
return nil, nil
|
||||
}
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -419,7 +420,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
|
||||
return nil, nil
|
||||
}
|
||||
// 已结束的小队 run 由 AcquireLease 的孤儿回收逻辑处理,不在这里误拦截。
|
||||
if doc.LeaseHolderType == docgov.LeaseHolderSquad && s.taskRuns != nil {
|
||||
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && s.taskRuns != nil {
|
||||
if run, runErr := s.taskRuns.GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user