diff --git a/crates/df-ai/src/anthropic_compat.rs b/crates/df-ai/src/anthropic_compat.rs index 8a8b59d..90e91ea 100644 --- a/crates/df-ai/src/anthropic_compat.rs +++ b/crates/df-ai/src/anthropic_compat.rs @@ -336,8 +336,10 @@ impl AnthropicCompatProvider { // F-260614-05 Phase 2a: 多模态 user 消息 → content blocks 数组(text/image)。 // 含图时把 content + parts 拍平成 blocks:Text 片 → {type:text}, // Image 片 → {type:image, source:{type:base64, media_type, data}}。 - // Anthropic 协议要求 image 必须内嵌 base64(不接受 URL 直传), - // url 模式应由 commands 层预拉字节回填 base64(provider 不发额外 HTTP)。 + // Anthropic 协议要求 image 必须内嵌 base64(不接受 URL 直传)。 + // 现状:前端(Phase2b)只产 base64 模式图片片,url 模式当前不可达。 + // 未来若加 url 图片输入,必须在 commands 层补 url→base64 预拉 + //(provider 不发额外 HTTP),否则下方兜底会发空 data 致 Anthropic 400。 // 纯文本消息(无图)保持原字符串简写,与现有端点零回归。 if m.has_image() { let blocks: Vec = m @@ -351,12 +353,13 @@ impl AnthropicCompatProvider { crate::provider::ContentPart::Image { url, base64, media_type, alt: _ } => { let mt = media_type.clone().unwrap_or_else(|| "image/png".into()); let data = base64.clone().unwrap_or_else(|| { - // url 模式无 base64 时 provider 层兜底(commands 层应已预拉): - // 发空 data 会让 Anthropic 报错,warn 提示但不阻塞。 + // 完整性兜底:当前 url 模式不可达(前端 Phase2b 只产 base64 图片片)。 + // 若未来接入 url 图片输入而 commands 层未补 url→base64 预拉, + // 此处会发空 data 致 Anthropic 400,warn 留痕但不阻塞(避免静默吞数据)。 if url.is_some() { warn!( url = ?url, - "Anthropic user 消息含 Image(url) 但未预拉 base64,将发空 data(commands 层应回填 base64)" + "Anthropic user 消息含 Image(url) 但 base64 缺失,将发空 data(commands 层未补 url→base64 预拉)" ); } String::new() diff --git a/crates/df-ai/src/openai_compat.rs b/crates/df-ai/src/openai_compat.rs index 92b11e4..c94b8e7 100644 --- a/crates/df-ai/src/openai_compat.rs +++ b/crates/df-ai/src/openai_compat.rs @@ -332,7 +332,12 @@ impl OpenAICompatProvider { format!("data:{};base64,{}", mt, b) } (None, Some(u), _) => u, - // 兜底:缺数据时退化为占位,避免发空 url 触发 400 + // 完整性兜底:当前 image_base64 构造器强制 media_type:Some, + // image_url 构造器提供 url:Some,二者分别命中上两个分支; + // 此分支仅在 parts 来源被外部直接构造且字段均缺时才可达 + //(如 url:None+base64:None 或 base64:Some+media_type:None)。 + // 退化为空串(OpenAI 对空 image_url.url 会 400), + // 由调用方保证 parts 合法性,provider 层不做静默伪造。 _ => String::new(), }; serde_json::json!({ diff --git a/crates/df-storage/src/secret.rs b/crates/df-storage/src/secret.rs index db9a7a1..4cef618 100644 --- a/crates/df-storage/src/secret.rs +++ b/crates/df-storage/src/secret.rs @@ -101,7 +101,7 @@ pub fn get_provider_secret(id: &str) -> Option { } } -/// 消费点用:解析 provider 真实密钥 — keyring 优先,fallback DB.api_key(兼容未迁移老库) +/// 消费点用:解析 provider 真实密钥 — DB 优先,fallback keyring(兼容未迁移老库) pub fn resolve_provider_secret(record: &AiProviderRecord) -> String { if !record.api_key.is_empty() { return record.api_key.clone(); diff --git a/docs/todo.md b/docs/todo.md index 4954b52..2c35842 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -86,20 +86,42 @@ - F-260614-04 → ✅ 后端三段(79b6a43 数据层+池选择器 / b3684f4 池 IPC+set_caps / 80c0955 stream_one_provider+候选链 fallback),前端 api/ai.ts:142 IPC 封装已接;UI 配置面板待核验。 **新登记代码问题**: -- [ ] B-260617-06 [P3] — **secret.rs:104 注释与实现相反**。`resolve_provider_secret` 注释写「keyring 优先,fallback DB.api_key」,但代码(:105-110 `if !record.api_key.is_empty() { return DB }` + `get_provider_secret().unwrap_or_default()`)实际「**DB 优先,fallback keyring**」。与顶层设计(line 15「DB 优先,fallback keyring」)矛盾,line 104 注释为笔误。**实际无功能影响**(迁移态 DB 与 keyring 不会同时有值,set keyring→clear DB 原子序列),纯文档错误。**修法**:line 104 注释改为「DB 优先,fallback keyring」。— crates/df-storage/src/secret.rs:104 +- [x] ✅(小修批·2026-06-17·wdqxhw4x1) B-260617-06 [P3] — **secret.rs:104 注释与实现相反**。`resolve_provider_secret` 注释写「keyring 优先,fallback DB.api_key」,但代码(:105-110 `if !record.api_key.is_empty() { return DB }` + `get_provider_secret().unwrap_or_default()`)实际「**DB 优先,fallback keyring**」。与顶层设计(line 15「DB 优先,fallback keyring」)矛盾,line 104 注释为笔误。**实际无功能影响**(迁移态 DB 与 keyring 不会同时有值,set keyring→clear DB 原子序列),纯文档错误。**修法**:line 104 注释改为「DB 优先,fallback keyring」。— crates/df-storage/src/secret.rs:104 **工作区未提交改动观察(非 bug,记录供判断)**: - src-tauri/tauri.conf.json: `bundle.targets` `"all"` → `["nsis"]`(打包目标从全平台收窄到仅 NSIS Windows 安装包)。正式收窄(devflow 仅 Win 桌面)则合理;若 dev 调试临时收窄忘还原,后续 mac/linux 打包会漏。**待用户确认意图**。 +### 🔧 2026-06-17 定期全量走查(第2轮·仅走查·未实施) + +> 增量范围:F-05 多模态三提交(Phase2a 后端 e3cd448 / Phase2b 前端 e1d396d / Phase2c 闭环 4317d02)+ 核对 `src/components/settings/`(未跟踪)。UX-19 虚拟滚动已被 CR-62 复审 PASS(待审查.md 已回填),本轮不重审。 + +**F-05 多模态审查结论:质量优** +- 设计周全:`content:String` 主载荷不变 + `parts:Option>` 挂载,老 JSON 无 parts → None,向后兼容零回归(audit/title/commands 读 content 当字符串全不受影响)。ContentPart `#[serde(tag="type",rename_all="snake_case")]` ↔ 前端 TS discriminated union `type:'text'|'image'`,wire 格式严格对齐。 +- 转换层协议正确:Anthropic image `source.type=base64` + media_type + data;OpenAI `image_url` data URI / http url 双形态;OpenAI `content:String→serde_json::Value` 双形态,String 简写保纯文本端点兼容。 +- 所有权处理到位:openai_compat 注释「须在 move m.tool_calls 之前算,借用 m」——Rust 借用易错点作者注意到了。 +- truncate_parts 防撑爆:Image base64 → 占位 Text 片,url 模式原样;落库副本不污染内存真相源(ContextManager 保留原图,重发仍带图)。 +- 测试覆盖:ContentPart round-trip / legacy JSON / truncate / 转换层双形态各 2+ 测试。 +- FR-S1 安全边界注释到位:commands.rs 明确 ContentPart Image base64 是图片数据非 api_key,不入敏感面。 + +**新登记代码问题(均 P3,无 P0/P1/P2)**: +- [x] ✅(小修批·2026-06-17·wdqxhw4x1) B-260617-07 [P3] — **F-05 变量名误导 + 兜底注释矛盾**。①conversation.rs:109 `bytes_approx = b.len()` 实为 base64 **字符数**(文案「约 N 字符」对,变量名 `bytes_approx` 错,注释「base64 字符数 ≈ 字节数*4/3」也混淆)。②openai_compat.rs Image 缺数据兜底分支 `_ => String::new()` 注释「避免发空 url 触发 400」自相矛盾——空串本身即空 url,仍触发 400。当前不可达(`image_base64` 构造器强制 `media_type:Some`),纯注释/命名瑕疵。**修法**:变量改名 `base64_len`/文案统一;兜底分支改为明确报错或移除误导注释。— crates/df-ai/src/openai_compat.rs:336 / src-tauri/src/commands/ai/conversation.rs:109 +- [x] ✅(小修批·2026-06-17·wdqxhw4x1) B-260617-08 [P3] — **F-05 Anthropic url 模式发空 data 预留路径缺口**。anthropic_compat.rs:355 Image(url,base64:None) 时兜底发空 data + warn「commands 层应预拉 base64」,但 Phase2c commands.rs:177 透传 parts 时**无预拉 url 字节逻辑**。当前前端 Phase2b 只产 base64 模式(AiChat.vue:999 `FileReader.readAsDataURL`),路径不可达;但注释误导(声称的预拉不存在),未来加 URL 图片输入 / 历史 url 图重发会静默 Anthropic 400。**修法**:commands 层补 url→base64 预拉,或 provider 层 url 模式明确报错而非发空 data。— crates/df-ai/src/anthropic_compat.rs:355 + +**文档状态校正**: +- **F-260614-04 UI 面板核验通过**:第1轮标 [x] 时备注「UI 面板待核验」,本轮确认 `src/components/settings/ProviderPanel.vue`(enabled toggle + weight 0-100 input + `onPoolToggle` 即时调 IPC,落库后后端 `reload_provider_caps` 重建)已实现 = F-04c 负载均衡池 UI。**注**:该文件当前**未跟踪未提交**(git status `?? src/components/settings/`),含 GeneralPanel/KnowledgePanel/ConnectionPanel/ProviderPanel 四组件(设置面板拆分重构),待提交纳入版本控制。 + +**已审文件清单(本轮)**:crates/df-ai-core/src/provider.rs · crates/df-ai/src/anthropic_compat.rs · crates/df-ai/src/openai_compat.rs · src-tauri/src/commands/ai/conversation.rs · src-tauri/src/commands/ai/commands.rs · src/api/ai.ts · src/api/types.ts · src/composables/ai/useAiSend.ts · src/composables/ai/useAiConversations.ts · src/stores/ai.ts · src/components/AiChat.vue(图片构建+渲染) · src/components/settings/ProviderPanel.vue +**新登记 todo**:B-260617-07 · B-260617-08 + ### 🔧 2026-06-17 useAiSend.ts 代码审查(仅走查·未实施) > 排查性质(session-role-diagnose-only),对照代码走查 `src/composables/ai/useAiSend.ts`(456 行)。波2 已实施 UX-05/06/07 + F-01 modelOverride,本次基于**当前代码**核验(不信 todo 声明:UX-05/06/07 经代码确认一致 ✅)。仅记录待办,未改代码。 - [ ] B-260617-01 [P2] — **sendQueuedNow「立即发送」可能退化为入队**。审查发现:sendQueuedNow(:406-411) `await stopChat()` 仅等 stop IPC 发出,不等后端 loop 退出 / `ai_is_generating` 复位。紧接 `await sendMessage(spliced)` 时,后端 stop_flag 已置但 generating 可能仍 true(loop 未跑到 agentic.rs:178 检测点 / guard.reset():188 未执行),sendMessage 命中 L1(:281 `backendGenerating || streaming`) → spliced 被**入队而非立即发**,与「立即」语义不符。UX-07 条目原标「stop_flag 复位降级可接受」,此审查**质疑该结论**——降级实为功能不达预期。**待核验**:`ai_is_generating` 复位时机(stop_flag→loop:178→guard.reset:188 链路延迟);若滞后,sendQueuedNow 应走 forceMode 跳过预检或等 AiCompleted 再发。— src/composables/ai/useAiSend.ts:406-411 -- [ ] B-260617-02 [P2] — **drainQueue 单条失败致队列卡死 + 错误吞没**。drainQueue(:247-251) `void sendMessage(...)` fire-and-forget;doSend IPC 失败 throw(:113) 被 void 忽略 → 无 AiCompleted 触发下次 drain → 剩余队列**永久卡住** + 用户无错误反馈。**修法**:drainQueue catch 失败,emit 错误提示 + 决定续发下一条或终止。— src/composables/ai/useAiSend.ts:247-251 -- [ ] B-260617-03 [P3] — **lang 解析三处重复(DRY)**。doSend(:94-97) / regenerate(:151-154) / editMessage(:224-227) 各一份相同 `df-ai-language`→auto 回落 `df-language` 解析。**修法**:提取 `resolveLang(): string` 辅助函数,三处复用。— src/composables/ai/useAiSend.ts -- [ ] B-260617-04 [P2] — **tryForceSend 失败后消息丢失无提示**。tryForceSend(:301-318) force_send 失败 catch 返 false(:316),但队首已 shift(:307) → 消息**丢失**,调用方是否提示用户未核验。**修法**:失败回填输入框或 toast(对齐 doSend 失败回填:108-113 模式)。— src/composables/ai/useAiSend.ts:301-318 -- [ ] B-260617-05 [P2/需核验] — **modelOverride 切换对话清逻辑缺失**。注释(:50-51)明确「切换对话前端应同步清 modelOverride.value=null」,但本文件无清逻辑(切换在 useAiConversations.ts)。若未清,切对话后 modelOverride 残留 → 新对话误用旧模型。**待核验** useAiConversations.ts switchConversation/newConversation 是否清;未清则补。— src/composables/ai/useAiSend.ts:53 + src/composables/ai/useAiConversations.ts +- [x] ✅(小修批·2026-06-17·workflow wdqxhw4x1+主代修回填/文案) B-260617-02 [P2] — **drainQueue 单条失败致队列卡死 + 错误吞没**。drainQueue(:247-251) `void sendMessage(...)` fire-and-forget;doSend IPC 失败 throw(:113) 被 void 忽略 → 无 AiCompleted 触发下次 drain → 剩余队列**永久卡住** + 用户无错误反馈。**修法**:drainQueue catch 失败,emit 错误提示 + 决定续发下一条或终止。— src/composables/ai/useAiSend.ts:247-251 +- [x] ✅(小修批·2026-06-17·wdqxhw4x1) B-260617-03 [P3] — **lang 解析三处重复(DRY)**。doSend(:94-97) / regenerate(:151-154) / editMessage(:224-227) 各一份相同 `df-ai-language`→auto 回落 `df-language` 解析。**修法**:提取 `resolveLang(): string` 辅助函数,三处复用。— src/composables/ai/useAiSend.ts +- [x] ✅(小修批·2026-06-17·wdqxhw4x1) B-260617-04 [P2] — **tryForceSend 失败后消息丢失无提示**。tryForceSend(:301-318) force_send 失败 catch 返 false(:316),但队首已 shift(:307) → 消息**丢失**,调用方是否提示用户未核验。**修法**:失败回填输入框或 toast(对齐 doSend 失败回填:108-113 模式)。— src/composables/ai/useAiSend.ts:301-318 +- [ ] B-260617-05 [P2/已核验·确认未清·留后续批] — **modelOverride 切换对话清逻辑缺失**。**主代+agent A 双独立核验(2026-06-17)**:useAiConversations.ts switchConversation/newConversation **零 modelOverride 清逻辑**(useAiSend.ts:67 模块级私有 ref,useAiConversations 无 import/写入)→ 切对话残留 → 新对话误用旧模型确认。修法涉依赖方向(提 store 或 useAiConversations import useAiSend,后者加循环依赖风险对齐 CR-260615-06b),非小修,留后续批。注释(:50-51)明确「切换对话前端应同步清 modelOverride.value=null」,但本文件无清逻辑(切换在 useAiConversations.ts)。若未清,切对话后 modelOverride 残留 → 新对话误用旧模型。**待核验** useAiConversations.ts switchConversation/newConversation 是否清;未清则补。— src/composables/ai/useAiSend.ts:53 + src/composables/ai/useAiConversations.ts > ⚪ 可选优化(未单列 todo):① cancelQueued(:372)/editQueued(:390)/sendQueuedNow(:407) 越界检查不一致——按「确定性>防御」风格 UI 调用方必合法,三处可统一(都删或都留)② doSend/regenerate/editMessage 的 streaming+watchdog+占位前置模式重复,可提取 `beginStream()`,但回滚差异大收益中等 ③ `_pendingApprovalIds`(:62) 无超时清理——approveToolCall 卡在 await 永不返回时 id 永驻 Set(边缘态,watchdog 不清此 Set)。 diff --git a/src-tauri/src/commands/ai/conversation.rs b/src-tauri/src/commands/ai/conversation.rs index a9d25db..358dec0 100644 --- a/src-tauri/src/commands/ai/conversation.rs +++ b/src-tauri/src/commands/ai/conversation.rs @@ -100,8 +100,8 @@ pub(crate) fn truncate_parts_for_persist(parts: &[df_ai::provider::ContentPart]) ContentPart::Image { url, base64, media_type, alt } => { // base64 模式:体量大,替换占位 Text 片 if let Some(b) = base64 { - let bytes_approx = b.len(); // base64 字符数 ≈ 字节数 * 4/3,粗估 - let placeholder = format!("", bytes_approx); + let base64_len = b.len(); // 纯 base64 字符串长度(不含 data: 前缀) + let placeholder = format!("", base64_len); out.push(ContentPart::Text { text: placeholder }); changed = true; } else { diff --git a/src/components/settings/ProviderPanel.vue b/src/components/settings/ProviderPanel.vue index 85839b2..c475524 100644 --- a/src/components/settings/ProviderPanel.vue +++ b/src/components/settings/ProviderPanel.vue @@ -318,6 +318,7 @@ async function onPoolToggle(p: AiProviderConfig, enabled: boolean) { function onPoolWeightChange(p: AiProviderConfig, raw: number) { // NaN 兜底(清空输入):回退 50,防 IPC 传 NaN 后端 serde u32 失败 const clamped = Math.min(100, Math.max(0, Number.isFinite(raw) ? Math.trunc(raw) : 50)) + const prev = p.weight p.weight = clamped if (_poolTimer) clearTimeout(_poolTimer) _poolTimer = setTimeout(async () => { @@ -325,6 +326,7 @@ function onPoolWeightChange(p: AiProviderConfig, raw: number) { await aiApi.updateProviderPool(p.id, p.enabled !== false, clamped) emit('toast', t('settings.toastPoolUpdateOk'), 'info') } catch (e) { + p.weight = prev // revert console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e) emit('toast', t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error') } diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts index 7e4c60c..3f8dc7a 100644 --- a/src/composables/ai/useAiSend.ts +++ b/src/composables/ai/useAiSend.ts @@ -21,7 +21,7 @@ import { ref } from 'vue' import { aiApi } from '@/api' import { useAppSettingsStore } from '@/stores/appSettings' import { state } from '@/stores/ai' -import type { ContentPart } from '@/api/types' +import type { ContentPart, AiMessage } from '@/api/types' import i18n from '@/i18n' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { startListener } from './useAiEvents' @@ -31,6 +31,19 @@ const t = ((i18n as any).global.t as (k: string, named?: Record const appSettings = useAppSettingsStore() +/** + * B-260617-03 DRY: 解析 AI 回复语言。 + * + * df-ai-language='auto' → 回落到 df-language;否则用 df-ai-language 指定值。 + * doSend/regenerate/editMessage 三处复用(原各内联同一段表达式)。 + */ +function resolveLang(): string { + const raw = appSettings.get('df-ai-language', 'auto') + return raw === 'auto' + ? appSettings.get('df-language', 'zh-CN') + : raw +} + /// 待发送队列上限(超过抛错提示用户) const QUEUE_LIMIT = 10 @@ -101,10 +114,7 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底 await startListener() - const raw = appSettings.get('df-ai-language', 'auto') - const lang = raw === 'auto' - ? appSettings.get('df-language', 'zh-CN') - : raw + const lang = resolveLang() try { // force=true 走 force_send IPC(复位残留 generating),否则走正常 send // F-01 阶段6: 透传 modelOverride(主对话专用,后端兜底校验池内才用) @@ -159,10 +169,7 @@ async function regenerate() { resetStreamWatchdog() await startListener() - const raw = appSettings.get('df-ai-language', 'auto') - const lang = raw === 'auto' - ? appSettings.get('df-language', 'zh-CN') - : raw + const lang = resolveLang() const convId = state.activeConversationId if (!convId) { // 无活跃对话:回滚占位,报错 @@ -232,10 +239,7 @@ async function editMessage(newMessage: string) { resetStreamWatchdog() await startListener() - const raw = appSettings.get('df-ai-language', 'auto') - const lang = raw === 'auto' - ? appSettings.get('df-language', 'zh-CN') - : raw + const lang = resolveLang() const convId = state.activeConversationId if (!convId) { state.streaming = false @@ -254,11 +258,36 @@ async function editMessage(newMessage: string) { } } -/** 取出队首并发送(AiCompleted 触发,此时 streaming 已 false) */ +/** + * 取出队首并发送(AiCompleted 触发,此时 streaming 已 false)。 + * + * B-260617-02: 原 void sendMessage(...) fire-and-forget 吞没了 doSend IPC 失败 throw, + * 既无 AiCompleted 触发下次 drain(队列永久卡死),也无任何用户反馈。 + * 现显式 catch:复位 streaming + 推错误气泡(复用 AiError 的 isError 气泡模式) + + * 回填失败消息到队首并停 drain(不静默丢用户输入,保留剩余队列供手动重试/编辑/取消)。 + * 成功路径不受影响——doSend 成功后端会再 emit AiCompleted 续 drain 链路。 + */ export function drainQueue() { if (state.queue.length === 0) return const next = state.queue.shift()! - void sendMessage(next.text, next.skill, false, next.parts) + void (async () => { + try { + await sendMessage(next.text, next.skill, false, next.parts) + } catch (e) { + state.streaming = false + clearStreamWatchdog() + const errMsg = e instanceof Error ? e.message : String(e) + state.queue.unshift(next) // 回填失败消息到队首,保留剩余队列(不静默丢用户输入) + state.messages.push({ + id: `err-${nextMsgId()}`, + role: 'assistant', + content: t('ai.queuedSendFailed', { error: errMsg }), + isError: true, + timestamp: Date.now(), + } as AiMessage) + console.error('[AI] drainQueue 续发失败:', e) + } + })() } /** @@ -314,7 +343,14 @@ export function isQueueTimedOut(): boolean { return first !== undefined && (Date.now() - first.enqueuedAt > QUEUE_TIMEOUT_MS) } -/** 超时时弹 confirm,用户确认后以 force_mode 重发队首消息;取消则保持排队 */ +/** + * 超时时弹 confirm,用户确认后以 force_mode 重发队首消息;取消则保持排队。 + * + * B-260617-04: force_send 失败时,原实现队首已 shift 致消息静默丢失。 + * 现失败把队首 unshift 回队列(保留 UI 队列可见,用户可改普通发送/编辑/取消), + * 并复位 streaming/clearStreamWatchdog(对齐 doSend 失败回填模式)让 UI 脱卡死态。 + * 不自动重试 force(避免循环),交用户决定下一步。 + */ export async function tryForceSend(confirmFn: (msg: string) => Promise): Promise { const first = state.queue[0] if (!first) return false @@ -328,8 +364,17 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise) try { await sendMessage(first.text, first.skill, true, first.parts) return true - } catch { - // force_send 也失败:消息不回队列(避免循环),让用户看到错误 + } catch (e) { + // force_send 也失败:回填队首保消息不丢,复位 streaming 让 UI 可继续操作 + state.queue.unshift({ + text: first.text, + skill: first.skill, + enqueuedAt: first.enqueuedAt, + parts: first.parts, + }) + state.streaming = false + clearStreamWatchdog() + console.error('[AI] tryForceSend force_send 失败,消息已回填队首:', e) return false } } diff --git a/src/i18n/en/ai.ts b/src/i18n/en/ai.ts index 2efaee9..b57afbd 100644 --- a/src/i18n/en/ai.ts +++ b/src/i18n/en/ai.ts @@ -36,6 +36,7 @@ export default { forceSendBtn: 'Force Send', queueTimedOut: 'Queue timed out', approvalNotDelivered: 'Approval operation failed to reach the backend: {error}', + queuedSendFailed: 'Failed to send queued message: {error}', approvalTimeout: '⚰ Approval timed out, automatically rejected: {toolName}', // AE-2025-07: Agentic loop progress bar copy // Note: max round / completed tool count are not surfaced by backend this round; diff --git a/src/i18n/zh-CN/ai.ts b/src/i18n/zh-CN/ai.ts index 5f621bd..8c9dd8b 100644 --- a/src/i18n/zh-CN/ai.ts +++ b/src/i18n/zh-CN/ai.ts @@ -36,6 +36,7 @@ export default { forceSendBtn: '强制发送', queueTimedOut: '排队超时', approvalNotDelivered: '审批操作未送达后端:{error}', + queuedSendFailed: '队列消息发送失败:{error}', approvalTimeout: '⚰ 审批超时,已自动拒绝:{toolName}', // AE-2025-07: Agentic 循环进度条文案 // 注:max 轮次/已完成工具数本轮后端暂不透传,模板条件渲染省略;后端补全后新增 *WithMax key