修复: AI上下文邻接性收口 + 设置页i18n缺失键

- df-ai 三元组邻接性收口: sanitize/openai_compat 闭合判定由「id 全局存在」收紧为「紧后 tool 连续段覆盖」,夹层序列(后台压缩/用户插话楔入)不再直送 provider 致 400 insufficient tool messages
- 设置页补 settings.loading 缺失键(zh/en),修版本信息返回前渲染字面串
- 看板: 907 接手核查补注(实现口径=剥除非归位,有损)+ 遗留登记 909 + 908 销账
This commit is contained in:
lxy
2026-10-10 00:08:35 +08:00
parent f4ca01f2f2
commit c71d0c4657
5 changed files with 102 additions and 16 deletions
+26 -9
View File
@@ -372,18 +372,34 @@ fn extract_tool_call_ids(msg: &OpenAiMessage) -> Vec<String> {
fn sanitize_openai_triplets(messages: &mut Vec<OpenAiMessage>) {
use std::collections::HashSet;
// step 1:resolved_ids = 所有 tool 消息提供的 tool_call_id(有 result 响应的 id)。
let resolved_ids: HashSet<String> = messages
.iter()
.filter(|m| m.role == "tool")
.filter_map(|m| m.tool_call_id.clone())
.collect();
// step 1:相邻性判定的依据 = 每个 assistant 头「紧后 tool 连续段」覆盖的 id 集合。
// 根因(2026-10-09):原实现按「id 全局存在」判闭合(位置无关),而 OpenAI 协议要求
// tool 响应必须**紧随**其 assistant(tool_calls)。后台压缩摘要在 head 已 push、tool
// 结果未 push 的窗口内可楔入 system(context_lifecycle),被隔开的响应仍被判"已闭合"
// → 畸形序列放行 → 上游 400「insufficient tool messages」。故判定口径由「id 全局存在」
// 收紧为「紧后连续段覆盖」:未覆盖的 call 视同未闭合剥除,其游离响应随后由 step3 丢弃。
let mut adjacency: std::collections::HashMap<usize, HashSet<String>> =
std::collections::HashMap::new();
for i in 0..messages.len() {
if messages[i].role != "assistant" {
continue;
}
let mut seg: HashSet<String> = HashSet::new();
let mut j = i + 1;
while j < messages.len() && messages[j].role == "tool" {
if let Some(id) = messages[j].tool_call_id.as_deref() {
seg.insert(id.to_string());
}
j += 1;
}
adjacency.insert(i, seg);
}
let mut stripped_heads = 0u32;
let mut total_stripped = 0u32;
// step 2:assistant 剥未闭合 tool_call(无对应 tool result 响应)。
for m in messages.iter_mut() {
// step 2:assistant 剥未闭合 tool_call(其响应不在紧后连续段内)。
for (i, m) in messages.iter_mut().enumerate() {
if m.role != "assistant" {
continue;
}
@@ -393,12 +409,13 @@ fn sanitize_openai_triplets(messages: &mut Vec<OpenAiMessage>) {
if calls.is_empty() {
continue;
}
let seg = adjacency.get(&i);
let kept: Vec<serde_json::Value> = calls
.iter()
.filter(|tc| {
tc.get("id")
.and_then(|v| v.as_str())
.is_some_and(|id| resolved_ids.contains(id))
.is_some_and(|id| seg.is_some_and(|s| s.contains(id)))
})
.cloned()
.collect();