新增: 飞书文件消息入站与补充注入链完善

- 文件消息入站:20MB 上限落盘引用,模型经 read_file 按需读盘;下载带 3 次退避+120s 超时防线,复用媒体域 hash 去重/OSS 双写
- 群聊门控对齐 u-claw 模式:前移至类型分流前,@自己/@所有人响应,未艾特静默旁观(原门控后置致未支持类型误回提示卡)
- 修复补充注入丢失:pending_injects 未消化计数+收敛出口续轮+桌面注入 IPC+前端本地气泡
- df-ai-core 测试构造器补 source 字段(溯源字段遗漏,全量门禁暴露)
This commit is contained in:
lxy
2026-10-15 14:27:22 +08:00
parent 7548896f6d
commit 795935bf6a
10 changed files with 395 additions and 79 deletions
+31 -3
View File
@@ -4,7 +4,7 @@
//!
//! L2 发送韧性:三级降级
//! L0 normal:后端 idle → 直接发送
//! L1 queued: 后端 busy → 入队等待(≤30s 正常排队)
//! L1 queued: 后端 busy → 纯文本先试补充注入(并入当前轮,u-claw 语义);失败/带载荷 → 入队等待(≤30s 正常排队)
//! L2 force: 排队>30s → 弹 confirm → 用户确认后调 ai_chat_force_send 复位残留并强制发出
//!
//! 耦合:
@@ -356,7 +356,7 @@ export function drainQueue(convId?: string | null) {
* 发送消息 — 三级降级(L0 直接发 / L1 入队 / L2 强制):
*
* L0 normal: 后端 idle → 直接 doSend()
* L1 queued: 后端 busy → 入队等 AiCompleted 续发
* L1 queued: 后端 busy → 纯文本先试 ai_inject_runtime(补充并入当前轮);失败回退入队等 AiCompleted 续发
* L2 force: 排队>30s → 返回特殊标记,由调用方(handleSend)弹 confirm;
* 用户确认后重新进 sendMessage(forceMode=true)走 forceSend IPC
*
@@ -387,9 +387,37 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
backendGenerating = false
}
// ── L1:后端 busy → 入队(消息已在本地显示,待后端空闲后自动发送)──
// ── L1:后端 busy → 纯文本先试补充注入,失败回退入队(消息已在本地显示,待后端空闲后自动发送)──
// 仅用 backendGenerating,不用 state.streaming。streaming 是渲染态,不是后端忙的代理。
if (backendGenerating) {
// 补充注入旁路(u-claw 语义,与飞书渠道 feishu_channel 同源同一后端实现):纯文本补充
// 经 ai_inject_runtime 直写运行中会话,loop 下一轮自然消化——补充并进当前回答,不开新轮不入队。
// 注入通道只收文本:带图(parts 含 image)/带技能/带 mention 增强的消息注入会丢载荷
// (同飞书「图被丢、引导词空跑」判定),照旧入队走完整发送路径。
// Ok(false)=会话不存在、Err=非活跃/Stopping 均为常态路径:静默回退下方入队,
// 行为与接入注入前逐字一致。
const pureText = !skill && !(spans && spans.length > 0) && !(parts && parts.some(p => p.type === 'image'))
if (pureText && targetConvId) {
try {
const injected = await invoke<boolean>('ai_inject_runtime', { conversationId: targetConvId, message: text.trim() })
if (injected) {
// 本地气泡即时可见(后端已落库同内容,重进会话从 DB 加载不重复):
// 当前视图才 push,后台会话续发不污染当前视图(与下方 L0 既有口径一致)
if (targetConvId === state.activeConversationId) {
state.messages.push({
id: `user-${nextMsgId()}` as MessageId,
role: 'user',
content: text.trim(),
timestamp: Date.now(),
})
}
void emit('ai-toast', { msg: t('ai.injectMerged'), type: 'success' })
return
}
} catch {
// 非活跃(生成刚收尾的竞态窗口)是常态:静默回退入队,不弹错不打扰
}
}
if (state.queue.length >= QUEUE_LIMIT) {
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
}
+2
View File
@@ -52,6 +52,8 @@ export default {
// UX-P2-A2: toast when queued messages get dropped by error/regenerate/edit (was silent)
queueCleared: 'Cleared {n} queued message(s)',
queueWillContinue: 'Stopped. {n} queued message(s) will still send automatically.',
// Success toast when a runtime supplement is merged into the in-flight answer (u-claw semantics: no new round)
injectMerged: 'Merged into the current answer; takes effect next round.',
approvalTimeout: '⚰ Approval timed out, automatically rejected: {toolName}',
// conversation load/switch failure feedback (network jitter/IPC down → empty list/blank, was silent)
loadConvFail: 'Failed to load conversations. Please check your network or backend connection.',
+2
View File
@@ -52,6 +52,8 @@ export default {
// UX-P2-A2:出错/重生成/编辑清排队消息的 toast 提示(原静默丢弃无任何反馈)
queueCleared: '已清 {n} 条排队消息',
queueWillContinue: '已停止,{n} 条排队消息仍将自动发送',
// 运行中补充注入成功(u-claw 语义:纯文本补充并进当前回答,不开新轮)的 toast 提示
injectMerged: '已并入当前回答,下一轮生效',
approvalTimeout: '⚰ 审批超时,已自动拒绝:{toolName}',
// 对话加载/切换失败反馈(网络抖动/IPC 断开 → 列表空/切换空白,原静默无反馈)
loadConvFail: '对话列表加载失败,请检查网络或后端连接',