新增: 飞书文件消息入站与补充注入链完善
- 文件消息入站:20MB 上限落盘引用,模型经 read_file 按需读盘;下载带 3 次退避+120s 超时防线,复用媒体域 hash 去重/OSS 双写 - 群聊门控对齐 u-claw 模式:前移至类型分流前,@自己/@所有人响应,未艾特静默旁观(原门控后置致未支持类型误回提示卡) - 修复补充注入丢失:pending_injects 未消化计数+收敛出口续轮+桌面注入 IPC+前端本地气泡 - df-ai-core 测试构造器补 source 字段(溯源字段遗漏,全量门禁暴露)
This commit is contained in:
@@ -527,6 +527,31 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 热改 agent_max_iterations 不影响在跑 loop)+ 清零上 loop 残留的注入顺延余量。
|
||||
conv.loop_max_rounds = max_iterations;
|
||||
conv.round_budget_extra = 0;
|
||||
// 真目标源(2026-10-15 目标语义重构):pinned_goals 从「工具推理流水账」改为「用户任务原文」
|
||||
// ——流水账模式下用户真实意图从不进目标体系,压缩/重启后 AI 即失忆(2026-10-12/15
|
||||
// 两次丢目标事故同根)。首条任务型 user 消息写入首位;已有目标不覆盖(尊重
|
||||
// UPDATE_GOALS 显式修正);[系统恢复] 引导与超短碎片不入(递归套娃教训)。
|
||||
if conv.pinned_goals.is_empty() {
|
||||
let task_text = conv
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| {
|
||||
matches!(m.role, df_ai::provider::MessageRole::User)
|
||||
&& !m.content.starts_with("[系统恢复]")
|
||||
&& m.content.chars().count() >= 4
|
||||
})
|
||||
.map(|m| {
|
||||
let mut t: String = m.content.chars().take(GOAL_MAX_CHARS).collect();
|
||||
if m.content.chars().count() > GOAL_MAX_CHARS {
|
||||
t.push('…');
|
||||
}
|
||||
t
|
||||
});
|
||||
if let Some(t) = task_text {
|
||||
conv.pinned_goals.push(super::GoalEntry::new(t));
|
||||
tracing::info!(conv_id = %conv_id, "[ai] 会话目标取首条任务消息(目标语义重构)");
|
||||
}
|
||||
}
|
||||
(
|
||||
conv.stop_flag.clone(),
|
||||
conv.notify.clone(),
|
||||
@@ -2013,6 +2038,9 @@ async fn prepare_round_inputs(
|
||||
// 条目已消失则跳过,防 conv() 惰性创建孤儿状态
|
||||
if session.conv_read(conv_id).is_some() {
|
||||
session.conv(conv_id).messages.set_cache_friendly(cache_friendly);
|
||||
// 本轮请求已含截至此刻的全部消息,注入计数清零(=已消化);此后到收敛前
|
||||
// 再到的注入会计数,收敛出口凭 >0 续一轮消化(见收敛分支守卫)。
|
||||
session.conv(conv_id).pending_injects = 0;
|
||||
}
|
||||
let conv = match session.conv_read(conv_id) {
|
||||
Some(c) => c,
|
||||
@@ -2218,7 +2246,10 @@ async fn finalize_round_output(
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
update_pinned_goals(&mut session, conv_id, tool_calls_acc);
|
||||
// 工具推理流水账停写(2026-10-15 目标语义重构):「执行 xxx/查看 xxx」是手段记录
|
||||
// 不是用户目标,入 pinned_goals 挤占真目标且压缩/重启后即失忆;工具轨迹已由
|
||||
// 轨迹卡与审计表完整覆盖。目标来源收敛为:首条任务消息(入口写入)+UPDATE_GOALS
|
||||
// 显式修正(上方分支,LLM 显式目标管理继续有效)。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2345,19 +2376,49 @@ async fn finalize_round_output(
|
||||
// 正常收敛(loop 主体置 converged 后 break)
|
||||
// P2-1 主会话侧最小修复:收敛文本若被 max_tokens 腰斩,追加系统提示消息
|
||||
// (对齐 MidStream 保文的系统提示机制),用户/下轮请求能看见截断而非当完整回复。
|
||||
if round_truncated_by_max_tokens {
|
||||
// 注入未消化守卫(2026-10-14 实测):本轮组装后到达的补充指令,纯问答轮一轮收敛即
|
||||
// 无人消费(本轮上下文已在生成中定型)。计数>0 → 插提示续一轮,下轮
|
||||
// build_for_request 自然带上补充内容;计数已 take 清零,同批注入不重复触发。
|
||||
let pending_injects = {
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.per_conv.contains_key(conv_id) {
|
||||
std::mem::take(&mut session.conv(conv_id).pending_injects)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
if pending_injects > 0 {
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.per_conv.contains_key(conv_id) {
|
||||
let conv = session.conv(conv_id);
|
||||
let mut notice = ChatMessage::system(
|
||||
"⚠ 回复因 max_tokens 上限被截断,以上内容可能不完整;可要求模型继续输出。",
|
||||
);
|
||||
notice.model = Some(resolved_model.to_string());
|
||||
conv.messages.push(notice);
|
||||
let mut tip = ChatMessage::system(format!(
|
||||
"用户在回答生成期间补充了 {pending_injects} 条消息(见对话记录中的补充指令);\
|
||||
请结合补充内容继续或修正上面的回答,勿忽略。"
|
||||
));
|
||||
tip.model = Some(resolved_model.to_string());
|
||||
conv.messages.push(tip);
|
||||
}
|
||||
tracing::warn!(conv_id = %conv_id, "[ai] 收敛回复因 max_tokens 截断,已追加系统提示");
|
||||
tracing::info!(
|
||||
conv_id = %conv_id, pending = pending_injects,
|
||||
"[ai] 收敛时检出未消化注入,插提示续一轮消化"
|
||||
);
|
||||
// 不 return Converged:掉到函数尾部 RunTools(本轮无工具空转),
|
||||
// loop 顶部正常续轮,补充指令获得完整消费轮。
|
||||
} else {
|
||||
if round_truncated_by_max_tokens {
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.per_conv.contains_key(conv_id) {
|
||||
let conv = session.conv(conv_id);
|
||||
let mut notice = ChatMessage::system(
|
||||
"⚠ 回复因 max_tokens 上限被截断,以上内容可能不完整;可要求模型继续输出。",
|
||||
);
|
||||
notice.model = Some(resolved_model.to_string());
|
||||
conv.messages.push(notice);
|
||||
}
|
||||
tracing::warn!(conv_id = %conv_id, "[ai] 收敛回复因 max_tokens 截断,已追加系统提示");
|
||||
}
|
||||
return RoundEpilogue::Converged;
|
||||
}
|
||||
return RoundEpilogue::Converged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3602,6 +3663,7 @@ fn push_assistant_message(
|
||||
// ── update_pinned_goals: G1 目标钉扎提取+裁剪(扁平抽自原嵌套块,原 8 层 → 3 层) ──
|
||||
// 从工具调用推理新目标 → 去重 add → 标记旧 active 为 completed → 超上限时淘汰最早 completed。
|
||||
// 入参 session 需外层调用方持锁;纯内存 mutate,无 await。
|
||||
#[allow(dead_code)]
|
||||
fn update_pinned_goals(
|
||||
session: &mut AiSession,
|
||||
conv_id: &str,
|
||||
|
||||
@@ -1197,6 +1197,9 @@ pub(crate) async fn inject_runtime_message(
|
||||
if extended {
|
||||
conv.round_budget_extra += 1;
|
||||
}
|
||||
// 未消化计数(2026-10-14 实测):纯问答轮一轮收敛无下一轮,末轮扩轮机制(loop_max>0 才生效)
|
||||
// 兜不住;收敛出口凭此计数感知「组装后有注入到达」并续一轮消化。
|
||||
conv.pending_injects = conv.pending_injects.saturating_add(1);
|
||||
drop(session);
|
||||
// 落库(增量 save):与 loop 的 save 走同一 per-conv save 锁串行,幂等无交叉
|
||||
save_conversation(&state.ai_session, &state.db, conv_id, None, None, true).await;
|
||||
@@ -1204,6 +1207,20 @@ pub(crate) async fn inject_runtime_message(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 桌面端补充注入入口(桌面 AiChat 运行中发消息的 u-claw 语义:补充并进当前回答,不开新轮)。
|
||||
/// 把 inject_runtime_message 暴露为 Tauri command,与飞书渠道(feishu_channel.rs)同源同一实现,
|
||||
/// 锁纪律/末轮顺延语义收敛在 inject_runtime_message 单一真值,本 command 纯透传零附加逻辑。
|
||||
/// 返回契约:Ok(true)=已注入(前端不入队,提示已并入当前回答);Ok(false)=会话不存在
|
||||
/// (前端按普通新消息入队);Err=非活跃/Stopping(前端零破坏回退常规入队)。
|
||||
#[tauri::command]
|
||||
pub async fn ai_inject_runtime(
|
||||
state: State<'_, AppState>,
|
||||
conversation_id: String,
|
||||
message: String,
|
||||
) -> Result<bool, String> {
|
||||
inject_runtime_message(&state, &conversation_id, &message).await
|
||||
}
|
||||
|
||||
/// 批准/拒绝挂起的工具调用
|
||||
#[tauri::command]
|
||||
pub async fn ai_approve(
|
||||
|
||||
@@ -1065,6 +1065,10 @@ pub struct PerConvState {
|
||||
/// 原轮次终点时读后清零消费、续跑一轮,让补充指令获得带工具的完整消费轮。
|
||||
/// 同锁读写(session lock 串行);loop 入口清零防跨 loop 残留。
|
||||
pub round_budget_extra: usize,
|
||||
/// 未消化注入计数(2026-10-14 实测):inject_runtime_message 每次 +1;每轮 build_for_request
|
||||
/// 组装后清零(消息已进本轮请求=消化);收敛出口查 >0 则不收敛续一轮,治「纯问答轮
|
||||
/// 一轮收敛,生成期间到达的补充指令无人消费只能等下次请求」的丢失。内存态不落库。
|
||||
pub pending_injects: u32,
|
||||
/// 当前 agent 循环的语言设置(用于审批后恢复循环)
|
||||
pub agent_language: Option<String>,
|
||||
/// 用户指定模型 override(主对话专用,语义同 AiSession.model_override)
|
||||
@@ -1266,6 +1270,7 @@ impl PerConvState {
|
||||
iteration_used: 0,
|
||||
loop_max_rounds: 0,
|
||||
round_budget_extra: 0,
|
||||
pending_injects: 0,
|
||||
agent_language: None,
|
||||
model_override: None,
|
||||
session_trust: HashSet::new(),
|
||||
|
||||
Reference in New Issue
Block a user