修复: 走查发现(空 id 跨轮全局唯一 + generate_image SSRF/OOM + 技能日志 + fallback 测试)
- 空 tool_call id fallback 改全局 AtomicU64 计数器(跨轮跨 assistant 唯一,治空气泡+ L1 误熔断+三元组错位,实证 af2fab4e);fallback 测试硬编码改 starts_with(解耦计数器) - generate_image provider POST 补 SSRF 防护(复用 http.rs validate_url/resolve_and_check_host, 治 base_url 用户可配打内网)+ b64_json 解码前估算长度防 OOM - SkillResolver 加 tracing 日志(定位技能发送后未注入:name 匹配/read 失败)
This commit is contained in:
@@ -125,22 +125,43 @@ impl ToolCall {
|
||||
/// 路由结果,id 空时所有结果落到同一 key(`audit/mod.rs:203` 的 `seen_ids` 去重把空 id
|
||||
/// 视为相同,只留首个 tool_call)→ AI 看到「所有调用同一结果」,工具全失败。
|
||||
///
|
||||
/// 兜底在**解析点**生成 fallback id:raw 非空用 raw,空用 `format!("{prefix}_{index}")`
|
||||
/// (index 取 tool_call 在数组中的位置,保证同 assistant 内多 tool_call id 唯一)。
|
||||
/// 兜底在**解析点**生成 fallback id:raw 非空用 raw,空用 `format!("{prefix}_{n}")`
|
||||
/// (n 取自下方 `FALLBACK_ID_COUNTER` **全局递增计数器**,跨轮跨 assistant 唯一)。
|
||||
/// 下游(工具执行 / tool 结果回填 tool_call_id)从解析后的 `ToolCall.id` 取,不重复生成,
|
||||
/// 确保 assistant tool_call.id 与 tool 结果 tool_call_id 匹配(防 sanitize 三元组断裂)。
|
||||
///
|
||||
/// 三处解析点共用本 helper(DRY):OpenAI 同步 `parse_tool_calls`(prefix=`gen_tool`)、
|
||||
/// OpenAI 流式 chunk(prefix=`gen_stream`)、Anthropic 同步 + 流式(prefix=`gen_anthropic` /
|
||||
/// `gen_anthropic_stream`)。正常 provider(OpenAI/Claude/GLM id 非空)原样透传零介入。
|
||||
pub fn tool_call_id_or_fallback(raw: &str, index: usize, prefix: &str) -> String {
|
||||
///
|
||||
/// # 为何用全局计数器而非单轮 index(实证 af2fab4e)
|
||||
///
|
||||
/// 旧实现 fallback 用 `format!("{prefix}_{index}")`,index 是**单轮** tool_call 数组
|
||||
/// 位置。跨轮(不同 assistant)index 都从 0 起 → `gen_stream_0` 跨轮重复。agentic 的
|
||||
/// `id_to_name`(`insert(id, name)`)后者覆盖前者 → run_command 的 exit=1 被误标
|
||||
/// grep::exit=1 → L1 误熔断 grep(冤枉)→ loop 停 → 最后 assistant 空 content tool_calls
|
||||
/// 没执行(空气泡)。更严重:id 重复 → tool 结果配错 tool_call(三元组配对错位)。
|
||||
///
|
||||
/// 全局 `AtomicU64`(SeqCst)跨轮跨 assistant 严格递增,fallback id 永不重复。`index`
|
||||
/// 参数保留仅为签名兼容(4 处调用点 parse_tool_calls / 流式 chunk / push / agentic 都传),
|
||||
/// fallback 内部不再使用 index。
|
||||
///
|
||||
/// 单测跨进程实例计数器从 0 起;并发场景下两线程拿到的 fallback id 也严格递增(SeqCst),
|
||||
/// 保证全局唯一。
|
||||
pub fn tool_call_id_or_fallback(raw: &str, _index: usize, prefix: &str) -> String {
|
||||
if !raw.is_empty() {
|
||||
raw.to_string()
|
||||
} else {
|
||||
format!("{prefix}_{index}")
|
||||
let n = FALLBACK_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
format!("{prefix}_{n}")
|
||||
}
|
||||
}
|
||||
|
||||
/// fallback id 全局计数器:跨轮跨 assistant 严格递增,保证空 id fallback 永不重复。
|
||||
///
|
||||
/// 见 `tool_call_id_or_fallback` 文档说明(实证 af2fab4e 跨轮重复根因)。
|
||||
static FALLBACK_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// LLM Provider trait
|
||||
#[async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
@@ -462,33 +483,51 @@ mod tests {
|
||||
/// CR-空 id:tool_call_id_or_fallback 共享 helper —— 空 raw → fallback,非空原样。
|
||||
#[test]
|
||||
fn tool_call_id_or_fallback_non_empty_passthrough() {
|
||||
// 非空 raw 原样透传(provider 真 id 如 call_xxx 保留),与 index/prefix 无关
|
||||
assert_eq!(tool_call_id_or_fallback("call_abc", 0, "gen_tool"), "call_abc");
|
||||
assert_eq!(tool_call_id_or_fallback("x", 5, "p"), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_id_or_fallback_empty_generates_with_index() {
|
||||
assert_eq!(tool_call_id_or_fallback("", 0, "gen_tool"), "gen_tool_0");
|
||||
assert_eq!(tool_call_id_or_fallback("", 1, "gen_tool"), "gen_tool_1");
|
||||
assert_eq!(tool_call_id_or_fallback("", 7, "gen_stream"), "gen_stream_7");
|
||||
fn tool_call_id_or_fallback_empty_starts_with_prefix() {
|
||||
// 空 raw → "{prefix}_{n}",n 取自全局计数器(跨进程实例从 0 起,单测不假设具体值)
|
||||
let a = tool_call_id_or_fallback("", 0, "gen_tool");
|
||||
assert!(a.starts_with("gen_tool_"), "空 fallback 应以 gen_tool_ 开头, got: {a}");
|
||||
let b = tool_call_id_or_fallback("", 7, "gen_stream");
|
||||
assert!(b.starts_with("gen_stream_"), "空 fallback 应以 gen_stream_ 开头, got: {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_id_or_fallback_empty_unique_per_index() {
|
||||
// 同 prefix 不同 index → 不同 fallback(保证同 assistant 多 tool_call id 唯一)
|
||||
let ids: Vec<String> = (0..3).map(|i| tool_call_id_or_fallback("", i, "gen_tool")).collect();
|
||||
let mut sorted = ids.clone();
|
||||
sorted.sort();
|
||||
sorted.dedup();
|
||||
assert_eq!(ids.len(), sorted.len(), "各 index fallback 应唯一: {:?}", ids);
|
||||
fn tool_call_id_or_fallback_empty_globally_unique() {
|
||||
// 跨轮跨 assistant 唯一:连续两次空 fallback id 必不同(全局计数器递增)。
|
||||
// 这是修复 af2fab4e 跨轮重复(旧单轮 index 跨轮都从 0 起 → 重复)的核心断言。
|
||||
let a = tool_call_id_or_fallback("", 0, "gen_tool");
|
||||
let b = tool_call_id_or_fallback("", 0, "gen_tool");
|
||||
assert_ne!(a, b, "两次空 fallback 应不同(全局计数器跨轮唯一): {a} vs {b}");
|
||||
// 即使同 index(模拟跨轮 index 都从 0 起),fallback 也必唯一
|
||||
let c = tool_call_id_or_fallback("", 0, "gen_tool");
|
||||
let mut set = std::collections::HashSet::new();
|
||||
assert!(set.insert(a), "fallback a 应唯一");
|
||||
assert!(set.insert(b), "fallback b 应唯一");
|
||||
assert!(set.insert(c), "fallback c 应唯一");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_id_or_fallback_index_unused() {
|
||||
// index 参数仅为签名兼容保留(4 处调用点都传),fallback 不再使用 index。
|
||||
// 同 prefix + 同 index 连续两次 → 不同 fallback(全局计数器递增,与 index 无关)。
|
||||
let a = tool_call_id_or_fallback("", 3, "gen_tool");
|
||||
let b = tool_call_id_or_fallback("", 3, "gen_tool");
|
||||
assert_ne!(a, b, "同 index 两次空 fallback 应不同: {a} vs {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_id_or_fallback_prefix_distinguishes_sources() {
|
||||
// 不同 prefix 区分来源(同步 gen_tool / 流式 gen_stream / anthropic gen_anthropic)
|
||||
assert_ne!(
|
||||
tool_call_id_or_fallback("", 0, "gen_tool"),
|
||||
tool_call_id_or_fallback("", 0, "gen_stream")
|
||||
);
|
||||
// 注意:两次空 fallback 因全局计数器递增 id 不同,故只比 prefix 前缀
|
||||
let a = tool_call_id_or_fallback("", 0, "gen_tool");
|
||||
let b = tool_call_id_or_fallback("", 0, "gen_stream");
|
||||
assert!(a.starts_with("gen_tool_"));
|
||||
assert!(b.starts_with("gen_stream_"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user