diff --git a/crates/df-ai/src/anthropic_compat.rs b/crates/df-ai/src/anthropic_compat.rs index f70d686..a38ba4c 100644 --- a/crates/df-ai/src/anthropic_compat.rs +++ b/crates/df-ai/src/anthropic_compat.rs @@ -216,6 +216,12 @@ impl AnthropicCompatProvider { } Self::flush_tool_results(&mut messages, &mut pending_tool_results); + // B-260619-03: 合并相邻 user 块。Anthropic 协议要求 user/assistant 严格交替,连续 user + // 触发 GLM 1214。场景:drainQueue 续发(前一轮以 tool_result 结尾 + 新 user)→ flush 把 + // tool_result 转成 user 后紧跟 push 新 user → 连续两 user。合并成一条 user 含 + // [tool_result..., text] blocks(Anthropic 允许一条 user 多 blocks),打破恶性循环。 + Self::merge_consecutive_users(&mut messages); + let tools = req.tools.map(|defs| { defs.into_iter() .map(|d| AnthropicToolDef { @@ -256,6 +262,39 @@ impl AnthropicCompatProvider { messages.push(serde_json::json!({ "role": "user", "content": blocks })); } + /// B-260619-03: 合并相邻 user 消息为一条(content 拼成 blocks 数组)。 + /// Anthropic 协议要求 user/assistant 严格交替,连续 user 触发 1214。 + /// 触发场景:flush_tool_results 把 tool_result 转 user 后紧跟新 user(drainQueue 续发, + /// 前一轮以 tool_result 结尾)。合并成一条 user 含 [tool_result..., text] blocks,合法。 + /// content 形态:string(简写)或 array(blocks),统一规范化为 array 后拼接。 + fn merge_consecutive_users(messages: &mut Vec) { + let is_user = |m: &serde_json::Value| m.get("role").and_then(|r| r.as_str()) == Some("user"); + let mut i = 0; + while i + 1 < messages.len() { + if is_user(&messages[i]) && is_user(&messages[i + 1]) { + let next_content = messages[i + 1].get("content").cloned().unwrap_or(serde_json::Value::Null); + let cur = &mut messages[i]; + // 当前 user content 规范化为 array + let mut blocks: Vec = match cur.get("content").cloned() { + Some(serde_json::Value::String(s)) => vec![serde_json::json!({ "type": "text", "text": s })], + Some(serde_json::Value::Array(a)) => a, + _ => vec![], + }; + // 并入下一个 user content + match next_content { + serde_json::Value::String(s) => blocks.push(serde_json::json!({ "type": "text", "text": s })), + serde_json::Value::Array(a) => blocks.extend(a), + _ => {} + } + cur["content"] = serde_json::Value::Array(blocks); + messages.remove(i + 1); + // 不增 i:继续合并 i 与新 i+1(可能多个连续 user) + } else { + i += 1; + } + } + } + /// 生成 messages 诊断摘要(每条 role + content 形态 + tool 标记),不含敏感数据。 /// B-260618-27: 1214 类错误时随 bail 文案直达前端 raw,定位哪条/字段非法。 fn summarize_messages(messages: &[serde_json::Value]) -> String { diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index b506b49..afa6971 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -675,7 +675,10 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc) { } let repo = df_storage::crud::TaskRepo::new(&db); repo.update_field(id, field, value).await?; - Ok(serde_json::json!({ "id": id, "field": field, "updated": true })) + // UX-260619-04: 返回 title 供前端卡片友好展示(对齐 advance_task 返 TaskRecord, + // 防裸 UUID id 辨识度低)。update_field 返 bool 不含 record,单独 get_by_id 取 title。 + let title = repo.get_by_id(id).await?.map(|t| t.title).unwrap_or_default(); + Ok(serde_json::json!({ "id": id, "title": title, "field": field, "updated": true })) }) })}, ); diff --git a/src/composables/ai/useToolCard.ts b/src/composables/ai/useToolCard.ts index 3963fdd..f84fe2a 100644 --- a/src/composables/ai/useToolCard.ts +++ b/src/composables/ai/useToolCard.ts @@ -369,8 +369,9 @@ function formatAdvanceTask(r: ToolResult): string { /** update_task 共享格式化: updated!==false 显字段更新,否则通用 updated */ function formatUpdateTask(r: ToolResult): string { + // UX-260619-04: title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。 return r.updated !== false - ? t('aiTool.updatedTaskField', { id: r.id || '', field: r.field || '' }) + ? t('aiTool.updatedTaskField', { title: r.title || r.id || '', field: r.field || '' }) : t('aiTool.updated') } diff --git a/src/i18n/en/aiTool.ts b/src/i18n/en/aiTool.ts index bca488c..b76cb9e 100644 --- a/src/i18n/en/aiTool.ts +++ b/src/i18n/en/aiTool.ts @@ -63,7 +63,7 @@ export default { workflowTriggeredShort: 'Execution {exec}', // UX-260616-04: formatToolResult covers all tools with human-readable summaries (missing keys) - updatedTaskField: 'Task {id} updated field {field}', + updatedTaskField: 'Task「{title}」 updated field {field}', advancedTask: 'Task「{title}」 advanced to {status}', // UX-260618-09: advance_task return-round accumulation (shown only when >0); UX-260618-14: {title} = task name (TaskRecord), fallback id advancedTaskWithRounds: 'Task「{title}」 advanced to {status} (returned {n} times)', diff --git a/src/i18n/zh-CN/aiTool.ts b/src/i18n/zh-CN/aiTool.ts index 878320b..d136716 100644 --- a/src/i18n/zh-CN/aiTool.ts +++ b/src/i18n/zh-CN/aiTool.ts @@ -64,7 +64,7 @@ export default { workflowTriggeredShort: '执行实例 {exec}', // UX-260616-04:formatToolResult 覆盖全部工具产出人类可读摘要(补缺失 key) - updatedTaskField: '任务 {id} 已更新字段 {field}', + updatedTaskField: '任务「{title}」已更新字段 {field}', advancedTask: '任务「{title}」已推进至 {status}', // UX-260618-09:advance_task 退回累加轮数(>0 才显);UX-260618-14:{title} 显任务名(后端返 TaskRecord),回退 id advancedTaskWithRounds: '任务「{title}」已推进至 {status}(已退回 {n} 次)',