feat(server): 接线对话 workflow.run 并补 P0 验证记录

server 侧窄端口适配 projectworkflow,消除 chat→projectworkflow 边界违规;记录单测与 arch-check 证据,PI 全链路待补证。
This commit is contained in:
weichen
2026-09-25 14:37:11 +00:00
parent 9a540fa54a
commit 0ab4a8bd2d
5 changed files with 114 additions and 20 deletions
@@ -0,0 +1,51 @@
---
title: "对话办公 P0 验证记录"
created: 2026-09-25
updated: 2026-09-25
owner: Cursor
task_id: dialogue-office-p0-2026-09-25
status: done
policy_revision: cursor-governance-2026-09-24-r1
---
# 对话办公 P0 验证记录
配套:[整改计划](./2026-09-25-dialogue-office-expansion-plan.md) P0 · [研究](../audit/2026-09-25-dialogue-office-capability-research.md)
## 范围与授权
- 写路径:`internal/coordinator`、`internal/chat`、`internal/server`(Workflow 窄端口适配)
- 基线:建场 `7cfd6851`;本记录对应 feature tip 以提交收据为准
- **产品 PI 全链路**:本轮 **未运行**(`.doc/test-reports` 被并行任务占用;未获独立 PI 环境授权)。按验证契约「Go/接口」层以单测 + arch-check 验收;PI 对话写路径标为 **待补证**,不冒充 PASS。
## 验收对照(计划清单)
| 项 | 判定 | 证据 |
|----|------|------|
| 五处无「已成功」且状态未变 | PASS(单测) | `p0_fake_success_test.go`;假成功文案已从 handler 移除 |
| 失败路径明确 RejectReason | PASS | `WORKFLOW_SWITCH_UI_ONLY`、`SUBMIT_STATE_DENIED`、`DOC_RESOLVE_FAILED` |
| approve/reject/final 确认链不坏 | PASS | `go test ./internal/coordinator/` 全套;既有 DocGov approve/reject 用例 |
| `go test ./internal/coordinator/... ./internal/chat/...` | PASS | 见下方命令输出(跳过无关 flaky SemanticMemory) |
| 真实对话写路径 PI 报告 | 未运行 | 待获授权环境与 test-reports 写路径后补 |
## 实际命令与结果
```text
$ go test ./internal/coordinator/ ./internal/chat/ ./internal/server/ -count=1 -skip 'SemanticMemory|PiAgent|RealPI|E2E|PG_'
ok chat-agent-system/internal/coordinator
ok chat-agent-system/internal/chat
ok chat-agent-system/internal/server
$ ./scripts/arch-check.sh --mode block
arch-check block: production gate passed
```
## 接线说明
- 生产会话:`server.go` → `adaptChatWorkflow(s.pwSvc)` → `chat.WorkflowRunner`
- `chat` **不** import `projectworkflow`(边界门禁)
- `BindChatSession`(`internal/app`)当前无调用方;`u9u12-keys-rollout` 占用 app 时未改
## 结论
P0 代码与 Go/接口验收完成;**不合入 dev**(待用户集成指令)。产品 PI 对话写路径补证为收口后可选增强,不阻断本切片 code-complete。
+20 -11
View File
@@ -10,23 +10,33 @@ import (
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/taskrun"
)
// WorkflowRunner creates workflow run records (narrow port; composition injects projectworkflow.Service).
// WorkflowStartRequest is the narrow DTO for starting a workflow run from dialogue.
type WorkflowStartRequest struct {
RunID string
ProjectID int64
WorkflowKey string
StartedBy int64
Status string
}
// WorkflowStartResult is the narrow receipt after CreateWorkflowRun.
type WorkflowStartResult struct {
RunID string
}
// WorkflowRunner creates workflow run records (composition adapts projectworkflow.Service).
type WorkflowRunner interface {
CreateWorkflowRun(ctx context.Context, run *projectworkflow.WorkflowRun) (*projectworkflow.WorkflowRun, error)
CreateWorkflowRun(ctx context.Context, req WorkflowStartRequest) (*WorkflowStartResult, error)
}
// ActionServices 对话层执行 PendingAction 所需的后端服务。
// 具体 chatflow / intentflow / taskrun 实现由组合根经窄端口注入。
//
// 合入接线(被 server-image-field-closeout-r4 占用 app/server 时延后):
//
// sess.Actions.Workflow = rt.PW // 或 s.pwSvc
//
// 未接线时 workflow.run 会诚实失败(「workflow service not available」),不会假成功。
// 生产主路径(server 会话装配)已接线:Workflow = AdaptChatWorkflow(s.pwSvc)。
// BindChatSession(internal/app)若启用亦应适配 rt.PW。
type ActionServices struct {
DocGov *docgov.GovernanceEngine
TaskRuns TaskStarter
@@ -193,12 +203,11 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
key = "main"
}
runID := fmt.Sprintf("wf_run_%d", time.Now().UnixNano())
startedBy := uid
created, err := s.Actions.Workflow.CreateWorkflowRun(ctx, &projectworkflow.WorkflowRun{
created, err := s.Actions.Workflow.CreateWorkflowRun(ctx, WorkflowStartRequest{
RunID: runID,
ProjectID: s.ProjectID,
WorkflowKey: key,
StartedBy: &startedBy,
StartedBy: uid,
Status: "running",
})
if err != nil {
+6 -9
View File
@@ -8,20 +8,19 @@ import (
"chat-agent-system/internal/config"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectworkflow"
)
type stubWorkflowRunner struct {
last *projectworkflow.WorkflowRun
last WorkflowStartRequest
err error
}
func (s *stubWorkflowRunner) CreateWorkflowRun(_ context.Context, run *projectworkflow.WorkflowRun) (*projectworkflow.WorkflowRun, error) {
func (s *stubWorkflowRunner) CreateWorkflowRun(_ context.Context, req WorkflowStartRequest) (*WorkflowStartResult, error) {
if s.err != nil {
return nil, s.err
}
s.last = run
return run, nil
s.last = req
return &WorkflowStartResult{RunID: req.RunID}, nil
}
func TestExecuteAction_DocGovClaimAndSubmit(t *testing.T) {
@@ -80,16 +79,14 @@ func TestExecuteAction_WorkflowRun(t *testing.T) {
User: coordinator.UserInfo{ID: "7", Name: "运维"},
Project: coordinator.ProjectInfo{ActiveWorkflowKey: "main"},
}
res, err := sess.executeAction(ctxBackground(), &coordinator.PendingAction{Kind: "workflow.run", WorkflowKey: "main"}, userCtx)
res, err := sess.executeAction(context.Background(), &coordinator.PendingAction{Kind: "workflow.run", WorkflowKey: "main"}, userCtx)
if err != nil {
t.Fatalf("workflow.run: %v", err)
}
if stub.last == nil || stub.last.ProjectID != 42 || stub.last.WorkflowKey != "main" {
if stub.last.ProjectID != 42 || stub.last.WorkflowKey != "main" || stub.last.StartedBy != 7 {
t.Fatalf("unexpected run %+v", stub.last)
}
if res.RunID == "" {
t.Fatal("expected run id")
}
}
func ctxBackground() context.Context { return context.Background() }
+36
View File
@@ -0,0 +1,36 @@
package server
import (
"context"
"chat-agent-system/internal/chat"
"chat-agent-system/internal/projectworkflow"
)
// chatWorkflowAdapter adapts projectworkflow.Service to chat.WorkflowRunner
// without letting the chat package import projectworkflow (module boundary).
type chatWorkflowAdapter struct {
svc *projectworkflow.Service
}
func adaptChatWorkflow(svc *projectworkflow.Service) chat.WorkflowRunner {
if svc == nil {
return nil
}
return chatWorkflowAdapter{svc: svc}
}
func (a chatWorkflowAdapter) CreateWorkflowRun(ctx context.Context, req chat.WorkflowStartRequest) (*chat.WorkflowStartResult, error) {
startedBy := req.StartedBy
created, err := a.svc.CreateWorkflowRun(ctx, &projectworkflow.WorkflowRun{
RunID: req.RunID,
ProjectID: req.ProjectID,
WorkflowKey: req.WorkflowKey,
StartedBy: &startedBy,
Status: req.Status,
})
if err != nil {
return nil, err
}
return &chat.WorkflowStartResult{RunID: created.RunID}, nil
}
+1
View File
@@ -669,6 +669,7 @@ func (s *Server) getOrCreateSession(
DocGov: s.gov,
TaskRuns: app.AdaptChatTaskStarter(s.taskRuns),
IntentPlans: app.AdaptChatIntentPlans(s.intentPlans),
Workflow: adaptChatWorkflow(s.pwSvc),
Environment: env,
}
if s.vaultIngest != nil {