merge: fix/taskrun-scope-persist-2026-09-21
This commit is contained in:
+4
-2
@@ -5,7 +5,8 @@
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **任务错误透传已合入 dev**:`RunRecord` snake_case 与前端真实失败原因(见 [报告](../test-reports/2026-09-21-taskrun-error-transparency-test-report.md))。test/main 未晋升。
|
||||
1. **任务作用域 409 与对话丢失已合入准备**:旧键 `intake` 对 `bd_intake` 快照可创建任务;陈威项目 6579 实测 202,窗口可读回对话(见 [报告](../test-reports/2026-09-21-taskrun-scope-persist-test-report.md))。test/main 未晋升。
|
||||
2. **任务错误透传已合入 dev**:`RunRecord` snake_case 与前端真实失败原因(见 [报告](../test-reports/2026-09-21-taskrun-error-transparency-test-report.md))。test/main 未晋升。
|
||||
2. **阶段回溯与影响闭环已合入 dev**:T1–T3 与 B1–B3 在 `dev`。空追踪 JSON `null` 已修复,登录后显示「已索引 0 份文档 · 条目 0」(见 [报告](../test-reports/2026-09-21-trace-panel-null-crash-test-report.md))。test/main 未晋升。不改 `production_ready`,不签 D7 对话 ADR。
|
||||
3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读;SWBOOT-10 保持 fail-closed stub;D7 对话 ADR 仍待合规签字(追踪表留存类别已定为 `record`,不授权物理删除);**不改 `production_ready`**;模块解耦批次 A–G 与回溯实施切片未单独认领前保持 `planned`。
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|-------|------|--------|----------|-------|--------|
|
||||
| Cursor | 2026-09-21-taskrun-scope-persist | fix/taskrun-scope-persist-2026-09-21 | - | projectinstance+taskrun | done |
|
||||
| Cursor | 2026-09-21-taskrun-error-transparency | fix/taskrun-error-transparency-2026-09-21 | - | chat+web | done |
|
||||
| Cursor | 2026-09-21-trace-panel-null-crash | fix/trace-panel-null-crash-2026-09-21 | - | trace-index+web | done |
|
||||
| Cursor-Auto | 2026-09-20-stage-backtrack-design | feature/stage-backtrack-design | - | trace-impact-design | done |
|
||||
@@ -172,7 +174,7 @@
|
||||
|
||||
| holder | phase | since | scope | queue |
|
||||
|--------|-------|-------|-------|-------|
|
||||
| - | idle | 2026-09-21 | 任务错误透传已合入 dev 并 rebuild;`GET /api/v1/tasks/runs/{id}` 为 snake_case;`/health` schema_version=59 | - |
|
||||
| - | idle | 2026-09-21 | 任务作用域键归一化已在 dev 容器验收;`/health` schema_version=59;项目 6579 任务 `run_1789967333594919025` 为 running | - |
|
||||
|
||||
**phase**: `idle` | `merging` | `smoke` | `e2e`
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "任务作用域键归一化与失败对话落库"
|
||||
status: done
|
||||
created: 2026-09-21
|
||||
updated: 2026-09-21
|
||||
owner: Cursor
|
||||
branch: fix/taskrun-scope-persist-2026-09-21
|
||||
worktree: "-"
|
||||
mutex: projectinstance+taskrun
|
||||
---
|
||||
|
||||
# 任务作用域键归一化与失败对话落库
|
||||
|
||||
## 目标
|
||||
|
||||
新工作流实例使用 `bd_*` 任务键时,前端仍发送旧键(如 `intake`)不再返回 `TASK_SCOPE_UNRESOLVED`;若作用域仍无法解析,本轮用户消息与失败回复先落库。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ResolveMessageScopeForTask`、`TaskKeyToStage`、文档任务查找使用 `workflowdef.SameTaskKey`
|
||||
- `POST /api/v1/tasks/runs` 在 409 早退前写入 user + assistant 消息
|
||||
- 前端 `TASK_SCOPE_UNRESOLVED` 文案
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改快照数据、不改 schema
|
||||
- 不晋升 test/main
|
||||
- 不改 `production_ready`
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] 键别名归一化与回归测试
|
||||
2. [x] 409 路径落库与回归测试
|
||||
3. [x] dev rebuild 与陈威项目 6579 实测
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [x] live 实例快照为 `bd_intake`、请求 `intake` 时任务创建成功(202)
|
||||
- [x] 无法解析时 `chat_messages` 仍有用户句与失败回复
|
||||
- [x] 刷新后该会话不再为空
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Test report: `.doc/test-reports/2026-09-21-taskrun-scope-persist-test-report.md`
|
||||
|
||||
## 证据 / 备注
|
||||
|
||||
dev 库实例 366(项目 6579)快照任务键为 `bd_intake`。陈威 user_id=398,会话 `sess-p6579-u398` 修复前 0 条消息。
|
||||
@@ -0,0 +1,82 @@
|
||||
# 任务作用域 409 与对话丢失 测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
dev 上以用户「陈威」在项目 6579 发送「编写初始用户需求表」时,接口返回 `TASK_SCOPE_UNRESOLVED`(409)。刷新页面后该会话没有任何消息。
|
||||
|
||||
根因:2026-09-19 任务键命名空间化之后,实例 366 的快照任务键是 `bd_intake`,前端仍提交旧键 `intake`。作用域解析做精确匹配,访问门在找不到任务时直接放行,于是请求在创建任务之前以 409 结束,用户消息也还没写入。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
- 快照为 `bd_intake`、请求为 `intake` 时,`POST /api/v1/tasks/runs` 返回 202,而不是 `TASK_SCOPE_UNRESOLVED`。
|
||||
- 成功路径写入用户句与助手占位,刷新用的消息窗口能读回这两条。
|
||||
- 作用域仍无法解析时,409 之前先落库用户句和失败回复(单测覆盖;本次陈威场景已不再走该分支)。
|
||||
- 验收后端为真实 `pi-agent`(`MiniMax-M2.7-highspeed`),不用 stub 输出充当验收数据。
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
- OS:Linux。dev 编排 `agent-dev`,HTTP `:8080`,业务库 `:5433`。在 `fix/taskrun-scope-persist-2026-09-21` 上执行 `./scripts/env-service.sh dev rebuild`。
|
||||
- 后端:`go vet ./...`;`go test -short -count=1 -timeout 20m ./...`。
|
||||
- 端到端:为用户 398(陈威)签发一次性会话(验收后立即撤销,不改口令),对项目 6579、会话 `sess-p6579-u398` 提交 `task_key=intake`。
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
- `/health` 为 `env=dev`、`agent_backend=pi-agent`、`schema_version=59`。
|
||||
- 创建任务 HTTP 202,`status=running`,`workflow_stage_key=V0.1_triage`。
|
||||
- `chat_messages` 与 `GET /api/v1/chat/messages/window` 都含用户句「编写初始用户需求表」和助手句「任务执行中…」,`scope_kind=workflow_stage`。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 用例 1:dev 健康与真实 pi-agent
|
||||
|
||||
- **测试输入 (Input)**:`curl -sf http://127.0.0.1:8080/health`;`curl -sf http://127.0.0.1:18865/health`。
|
||||
- **实际输出 (Actual Output)**:
|
||||
|
||||
```json
|
||||
{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"kb_ready":true,"database_ready":true,"schema_version":59,"schema_ready":true,"task_store_ready":true}
|
||||
```
|
||||
|
||||
pi-agent:`ok=true`,`model_id=MiniMax-M2.7-highspeed`。
|
||||
|
||||
- **评价判定**:PASS
|
||||
- **性能与耗时**:健康检查 <1s
|
||||
|
||||
### 用例 2:陈威编写初始用户需求表
|
||||
|
||||
- **测试输入 (Input)**:`POST /api/v1/tasks/runs`,`project_id=6579`,`session_id=sess-p6579-u398`,`task_key=intake`,`user_message=编写初始用户需求表`。
|
||||
- **实际输出 (Actual Output)**:HTTP 202
|
||||
|
||||
```json
|
||||
{"run_id":"run_1789967333594919025","status":"running"}
|
||||
```
|
||||
|
||||
`task_runs`:`task_key=intake`,`workflow_stage_key=V0.1_triage`,`status=running`,无 error。
|
||||
|
||||
`chat_messages`:
|
||||
|
||||
```text
|
||||
4362 | user | TRIGGER_TASK | workflow_stage | V0.1_triage | 编写初始用户需求表
|
||||
4363 | assistant | TRIGGER_TASK | workflow_stage | V0.1_triage | 任务执行中…
|
||||
```
|
||||
|
||||
- **评价判定**:PASS。旧键已解析到 V0.1,任务已创建,对话已落库。
|
||||
- **性能与耗时**:接口约 0.3s 返回 202(生成仍在后台进行)。
|
||||
|
||||
### 用例 3:刷新窗口
|
||||
|
||||
- **测试输入 (Input)**:`GET /api/v1/chat/messages/window?session_id=sess-p6579-u398&days=7`(同一用户)。
|
||||
- **实际输出 (Actual Output)**:HTTP 200,1 个日期桶、2 条消息:用户「编写初始用户需求表」、助手「任务执行中…」,阶段键 `V0.1_triage`。
|
||||
- **评价判定**:PASS。刷新读取不再为空。
|
||||
- **性能与耗时**:约 0.3s
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
- `go vet ./...`:通过。
|
||||
- 定向:`go test -count=1 ./internal/projectinstance/ -run 'TestResolveMessageScopeForTask_LegacyKeyMatchesCanonicalSnapshot|TestResolveMessageScopeForTask_CanonicalAliasAmbiguousAcrossStages|TestTaskKeyToStage_LegacyAlias|TestResolveMessageScopeForTask_LiveStageScope'` 通过;`go test -count=1 ./internal/server/ -run 'TestCreateTaskRun_ScopeUnresolvedPersistsChat|TestCreateTaskRun_PreActivationAcceptedNot409|TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages'` 通过。
|
||||
- `go test -short -count=1 -timeout 20m ./...`:`internal/projectinstance`、`internal/server` 通过。另有 3 个既有库集成用例失败,与本次 diff 无关,且会写入当前 dev 库:`TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK`(项目外键)、`TestPG_StageAdvanceReminder24h` / `TestPG_EmitStageReadyDedupe` / `TestCAS_DatabaseIntegration`(项目名全局唯一冲突)。
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
达到合入 dev 的标准:陈威场景不再 409,刷新后对话还在。`run_1789967333594919025` 在取样时仍为 `running`,正文生成由真实 pi-agent 继续。test/main 不晋升,不改 `production_ready`。
|
||||
|
||||
后续:追踪面板改为对话输出在独立任务中做,不纳入本次缺陷修复。
|
||||
@@ -2,6 +2,8 @@ package projectinstance
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,7 +69,7 @@ func (s *Service) CheckDocTaskAccess(
|
||||
var stageKey string
|
||||
for _, st := range detail.Stages {
|
||||
for i := range st.DocTasks {
|
||||
if st.DocTasks[i].TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
|
||||
copyDoc := st.DocTasks[i]
|
||||
docInst = ©Doc
|
||||
stageKey = st.StageKey
|
||||
@@ -119,7 +121,7 @@ func (s *Service) CheckDocTaskAccess(
|
||||
func findDocTaskPlan(plan WorkflowPlan, taskKey string) *DocTaskPlan {
|
||||
for _, st := range plan.Stages {
|
||||
for i := range st.DocTasks {
|
||||
if st.DocTasks[i].TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
|
||||
return &st.DocTasks[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -144,7 +146,7 @@ func (s *Service) ResolveMessageScopeForTask(ctx context.Context, projectID int6
|
||||
var matches []MessageScope
|
||||
for _, st := range detail.Stages {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(dt.TaskKey, taskKey) {
|
||||
matches = append(matches, MessageScope{
|
||||
ScopeKind: ScopeWorkflowStage,
|
||||
WorkflowInstanceID: gov.ID,
|
||||
|
||||
@@ -122,6 +122,70 @@ func TestMessageVisible_LegacyUnscopedOwnerWhenRestricted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMessageScopeForTask_LegacyKeyMatchesCanonicalSnapshot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
_ = SeedProjectMeta(store, 42, LifecyclePlanning, "", 1, 0)
|
||||
svc := NewService(store, testResolver(), nil, nil)
|
||||
mustActivateTestPlan(t, svc, 42, 1)
|
||||
|
||||
store.mu.Lock()
|
||||
live := store.live[42]
|
||||
live.ConfigSnapshot.Stages[0].DocTasks[0].TaskKey = "bd_intake"
|
||||
for i := range store.stages[live.ID] {
|
||||
for j := range store.stages[live.ID][i].tasks {
|
||||
if store.stages[live.ID][i].tasks[j].taskKey == "intake" {
|
||||
store.stages[live.ID][i].tasks[j].taskKey = "bd_intake"
|
||||
}
|
||||
}
|
||||
}
|
||||
store.mu.Unlock()
|
||||
|
||||
scope, err := svc.ResolveMessageScopeForTask(ctx, 42, "intake")
|
||||
if err != nil {
|
||||
t.Fatalf("legacy intake must resolve against bd_intake snapshot: %v", err)
|
||||
}
|
||||
if scope.ScopeKind != ScopeWorkflowStage || scope.StageKey != "V0.1_triage" {
|
||||
t.Fatalf("want V0.1_triage workflow_stage, got %+v", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMessageScopeForTask_CanonicalAliasAmbiguousAcrossStages(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
_ = SeedProjectMeta(store, 43, LifecyclePlanning, "", 1, 0)
|
||||
svc := NewService(store, testResolver(), nil, nil)
|
||||
mustActivateTestPlan(t, svc, 43, 1)
|
||||
|
||||
store.mu.Lock()
|
||||
live := store.live[43]
|
||||
live.ConfigSnapshot.Stages = append(live.ConfigSnapshot.Stages, StagePlan{
|
||||
StageKey: "V0.5_feasibility", Enabled: true,
|
||||
DocTasks: []DocTaskPlan{{TaskKey: "bd_intake", Enabled: true}},
|
||||
})
|
||||
store.stages[live.ID] = append(store.stages[live.ID], memStageRecord{
|
||||
id: live.ID + 100, stageKey: "V0.5_feasibility", position: 1, enabled: true, status: StagePending,
|
||||
tasks: []memDocTaskRecord{{id: 900, taskKey: "bd_intake", enabled: true, status: "pending"}},
|
||||
})
|
||||
store.mu.Unlock()
|
||||
|
||||
_, err := svc.ResolveMessageScopeForTask(ctx, 43, "intake")
|
||||
if err == nil || !errors.Is(err, ErrTaskScopeNotResolved) {
|
||||
t.Fatalf("same task in two stages must stay unresolved, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskKeyToStage_LegacyAlias(t *testing.T) {
|
||||
plan := validPlan()
|
||||
plan.Stages[0].DocTasks[0].TaskKey = "bd_user_requirements"
|
||||
if got := TaskKeyToStage(plan, "user_requirements"); got != "V0.1_triage" {
|
||||
t.Fatalf("TaskKeyToStage=%q", got)
|
||||
}
|
||||
if got := TaskKeyToStage(plan, "intake"); got != "" {
|
||||
t.Fatalf("unrelated legacy key must miss, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeFromMetadata_RoundTrip(t *testing.T) {
|
||||
meta := map[string]any{
|
||||
"scope_kind": ScopeWorkflowStage,
|
||||
|
||||
@@ -152,7 +152,7 @@ func MessageStageKey(metadata map[string]any) string {
|
||||
func TaskKeyToStage(plan WorkflowPlan, taskKey string) string {
|
||||
for _, st := range plan.Stages {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(dt.TaskKey, taskKey) {
|
||||
return st.StageKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,51 @@ func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskRun_ScopeUnresolvedPersistsChat(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
token := loginTestAdmin(t, srv)
|
||||
srv.cfg.AllowInMemory = false
|
||||
srv.instSvc = nil
|
||||
|
||||
requestBody, _ := json.Marshal(createTaskRunRequest{
|
||||
TaskRunRequestDTO: TaskRunRequestDTO{TaskKey: "intake", ProjectID: 9102, ProjectName: "作用域失败"},
|
||||
SessionID: "sess-scope-unresolved",
|
||||
ClientTurnID: "turn-scope-1",
|
||||
UserMessage: "编写初始用户需求表",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/runs", bytes.NewReader(requestBody))
|
||||
withBearerAuth(req, token)
|
||||
w := httptest.NewRecorder()
|
||||
srv.Routes().ServeHTTP(w, req)
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(w.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if payload["error_code"] != "TASK_SCOPE_UNRESOLVED" {
|
||||
t.Fatalf("payload=%+v", payload)
|
||||
}
|
||||
history, err := srv.ctxMgr.GetChatHistory(t.Context(), "sess-scope-unresolved", 20, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("history: %v", err)
|
||||
}
|
||||
if len(history) != 2 || history[0].Role != "user" || history[1].Role != "assistant" {
|
||||
t.Fatalf("history=%+v", history)
|
||||
}
|
||||
if history[0].Content != "编写初始用户需求表" {
|
||||
t.Fatalf("user content=%q", history[0].Content)
|
||||
}
|
||||
errorInfo, ok := history[1].Metadata["error_info"].(map[string]any)
|
||||
if !ok || errorInfo["code"] != "TASK_SCOPE_UNRESOLVED" {
|
||||
t.Fatalf("error_info=%+v", history[1].Metadata["error_info"])
|
||||
}
|
||||
if history[1].Metadata["task_status"] != "failed" {
|
||||
t.Fatalf("assistant metadata=%+v", history[1].Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocGovActions_PreActivationRejected(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
store := projectinstance.NewMemoryStore()
|
||||
|
||||
@@ -120,6 +120,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
taskScope, scopeErr := s.resolveTaskMessageScope(r.Context(), req.ProjectID, req.TaskKey)
|
||||
if scopeErr != nil {
|
||||
s.recordUnresolvedScopeMessages(r.Context(), ctxMgr, req)
|
||||
writeJSONResponse(w, http.StatusConflict, map[string]string{
|
||||
"error_code": "TASK_SCOPE_UNRESOLVED",
|
||||
"message": "无法解析文档任务的工作流阶段作用域",
|
||||
@@ -347,6 +348,52 @@ func (s *Server) recordBusyPreflightMessages(
|
||||
_, _ = ctxMgr.RecordUserMessageWithID(ctx, req.SessionID, 0, "assistant", busy.Reason, "TRIGGER_TASK", "", asstMeta)
|
||||
}
|
||||
|
||||
const taskScopeUnresolvedMessage = "无法解析文档任务的工作流阶段作用域,当前请求未创建任务"
|
||||
|
||||
// recordUnresolvedScopeMessages 在 409 早退前落库本轮对话,避免刷新后用户消息与失败原因一起消失。
|
||||
func (s *Server) recordUnresolvedScopeMessages(ctx context.Context, ctxMgr *ctxmgr.ContextManager, req createTaskRunRequest) {
|
||||
if ctxMgr == nil || req.UserMessage == "" || req.SessionID == "" {
|
||||
return
|
||||
}
|
||||
userMeta := map[string]any{
|
||||
"project_id": req.ProjectID,
|
||||
"client_turn_id": req.ClientTurnID,
|
||||
"task_key": req.TaskKey,
|
||||
}
|
||||
_, _ = ctxMgr.RecordUserMessageWithID(ctx, req.SessionID, req.UserID, "user", req.UserMessage, "TRIGGER_TASK", "", userMeta)
|
||||
|
||||
taskRequest := map[string]any{
|
||||
"task_key": req.TaskKey,
|
||||
"project_name": req.ProjectName,
|
||||
"customer_name": req.CustomerName,
|
||||
"device_type": req.DeviceType,
|
||||
"intended_use": req.IntendedUse,
|
||||
"target_market": req.TargetMarket,
|
||||
"session_id": req.SessionID,
|
||||
"user_id": req.UserID,
|
||||
"project_id": req.ProjectID,
|
||||
"client_turn_id": req.ClientTurnID,
|
||||
"user_message": req.UserMessage,
|
||||
"doc_id": req.DocID,
|
||||
}
|
||||
errorInfo := map[string]any{
|
||||
"code": "TASK_SCOPE_UNRESOLVED",
|
||||
"class": "fatal",
|
||||
"user_message": taskScopeUnresolvedMessage,
|
||||
"actions": []string{"retry"},
|
||||
"session_id": req.SessionID,
|
||||
}
|
||||
asstMeta := map[string]any{
|
||||
"project_id": req.ProjectID,
|
||||
"client_turn_id": req.ClientTurnID,
|
||||
"task_key": req.TaskKey,
|
||||
"task_status": "failed",
|
||||
"task_request": taskRequest,
|
||||
"error_info": errorInfo,
|
||||
}
|
||||
_, _ = ctxMgr.RecordUserMessageWithID(ctx, req.SessionID, 0, "assistant", taskScopeUnresolvedMessage, "TRIGGER_TASK", "", asstMeta)
|
||||
}
|
||||
|
||||
// preflightTaskDocumentBusy 在写入聊天占位与 task_runs 之前拦截已知租约占用。
|
||||
// 真正的租约获取仍由 docgov 原子执行;本检查用于避免用户重复点击产生必然失败的 run。
|
||||
func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRunRequest) (*docgov.DocumentBusyError, error) {
|
||||
|
||||
@@ -56,9 +56,9 @@ test('unknown 4xx business code is not reported as a network error', () => {
|
||||
assert.equal(unknown.userMessage, '字段不合法');
|
||||
assert.equal(unknown.userMessage.includes('请检查网络'), false);
|
||||
|
||||
const bare = buildNetworkErrorInfo(400, { errorCode: 'TASK_SCOPE_UNRESOLVED' });
|
||||
assert.equal(bare.code, 'TASK_SCOPE_UNRESOLVED');
|
||||
assert.match(bare.userMessage, /请求失败/);
|
||||
assert.equal(bare.userMessage.includes('请检查网络'), false);
|
||||
const bare = buildNetworkErrorInfo(400, { errorCode: 'TASK_SCOPE_UNRESOLVED' });
|
||||
assert.equal(bare.code, 'TASK_SCOPE_UNRESOLVED');
|
||||
assert.match(bare.userMessage, /工作流阶段作用域/);
|
||||
assert.equal(bare.userMessage.includes('请检查网络'), false);
|
||||
});
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ const fatalDenialCopy: Record<string, string> = {
|
||||
WORKFLOW_INSTANCE_CLOSED: '工作流实例已关闭,无法执行该文档任务',
|
||||
DOC_DUTY_NOT_MANAGED: '该文档未配置职责,无法执行',
|
||||
SESSION_FORBIDDEN: '无权访问该会话',
|
||||
TASK_SCOPE_UNRESOLVED: '无法解析文档任务的工作流阶段作用域,当前请求未创建任务',
|
||||
};
|
||||
|
||||
export function buildNetworkErrorInfo(
|
||||
@@ -65,7 +66,7 @@ export function buildNetworkErrorInfo(
|
||||
};
|
||||
}
|
||||
if (fatalDenialCopy[code]) {
|
||||
const retryable = code === 'WORKFLOW_NOT_ACTIVATED' || code === 'DOC_DUTY_NOT_MANAGED';
|
||||
const retryable = code === 'WORKFLOW_NOT_ACTIVATED' || code === 'DOC_DUTY_NOT_MANAGED' || code === 'TASK_SCOPE_UNRESOLVED';
|
||||
return {
|
||||
userMessage: detail || fatalDenialCopy[code],
|
||||
code,
|
||||
|
||||
Reference in New Issue
Block a user