fix(server): persist chat when a document task scope cannot be resolved
A 409 used to return before any message was written, so a refresh showed an empty conversation. Record the user turn and a failed assistant reply before rejecting the run. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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