修复: checkpoint 参数化 + 压缩 permit 死锁降级 + 密钥归一化 + SSE CRLF/多字节
This commit is contained in:
@@ -643,18 +643,20 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 去重复 keyring resolve 调用。逻辑等价于 secret::build_provider_for(resolve→ensure→build 三步),
|
||||
// 仅因 build_provider_for 隐藏 resolved key 无法复用而在此内联(未改 secret.rs 锁边界)。
|
||||
let resolved_key = super::secret::resolve_provider_secret(&primary_provider);
|
||||
let key_len = resolved_key.len();
|
||||
let provider: Box<dyn LlmProvider> = match super::secret::ensure_resolved_key(
|
||||
let normalized_key = match super::secret::ensure_resolved_key(
|
||||
&primary_provider.name, &resolved_key,
|
||||
) {
|
||||
Ok(()) => df_ai::build_provider(
|
||||
&primary_provider.provider_type,
|
||||
&primary_provider.base_url,
|
||||
&resolved_key,
|
||||
&primary_provider.default_model,
|
||||
),
|
||||
Ok(k) => k,
|
||||
Err(msg) => return Err(ProviderResolveError::EnsureKeyFailed(msg)),
|
||||
};
|
||||
// key_len 用归一化后长度(剥引号/trim 后),与实际发往 provider 的 key 一致
|
||||
let key_len = normalized_key.len();
|
||||
let provider: Box<dyn LlmProvider> = df_ai::build_provider(
|
||||
&primary_provider.provider_type,
|
||||
&primary_provider.base_url,
|
||||
&normalized_key,
|
||||
&primary_provider.default_model,
|
||||
);
|
||||
Ok::<_, ProviderResolveError>((primary_provider, candidates, provider, key_len))
|
||||
},
|
||||
).await;
|
||||
|
||||
@@ -98,7 +98,24 @@ pub(crate) async fn compress_via_llm(
|
||||
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)
|
||||
// F-09 B 批5: per_conv 改 HashMap<conv_id>,压缩在 loop 内针对本对话,用 conv_id 共享限流槽。
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv(conv_id).await;
|
||||
// compress-permit-try: per_conv 改非阻塞 try。loop 入口已持 1 个 per_conv permit,
|
||||
// set_per_conv(1) 后 per_conv Semaphore 仅 1 permit 被入口占满,此处阻塞 acquire 将永久等待
|
||||
// → is_compressing 永真 → 卡死。改 try_acquire_per_conv:拿不到(None)提前返 Err 降级
|
||||
// (maybe_auto_compress 走失败兜底分支:关键词摘要 + set_compressing(false),下轮再试),非崩溃。
|
||||
let _per_conv_permit = match llm_concurrency.try_acquire_per_conv(conv_id).await {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
active_count,
|
||||
"[ai] 压缩 per_conv permit 已耗尽(loop 入口占用),降级返回走兜底分支,下轮再试"
|
||||
);
|
||||
return Err(format!(
|
||||
"压缩 per_conv 并发限流槽已耗尽(active {} 条,loop 入口占用;降级走兜底,下轮再试)",
|
||||
active_count
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// F-15 卡死根治(2026-06-21 诊断):provider.complete 原无超时,LLM hang → agentic loop
|
||||
// (agentic/mod.rs:780)await 永不返 → run_agentic_loop 永挂 → generating 卡死
|
||||
|
||||
@@ -343,21 +343,17 @@ async fn save_conversation_inner(
|
||||
Err(e) => tracing::warn!("读取对话 {conv_id} 失败: {e}"),
|
||||
}
|
||||
|
||||
// T6: 自动 checkpoint(每 20 轮或总 token > 150k 时创建)
|
||||
// 自动 checkpoint(每 20 轮或总 token > 150k 时创建)
|
||||
{
|
||||
let total_tokens: i64 = persist_msgs.len() as i64;
|
||||
if total_tokens > 0 && total_tokens % 20 == 0 {
|
||||
let snapshot = serde_json::to_string(&persist_msgs).unwrap_or_default();
|
||||
if !snapshot.is_empty() && snapshot.len() < 1_000_000 {
|
||||
let ck_id = format!("ck_{}_{}", conv_id.replace('-', ""), total_tokens);
|
||||
let escaped_snapshot = snapshot.replace("'", "''");
|
||||
let ck_now = now_millis();
|
||||
let sql = format!("INSERT OR IGNORE INTO conversation_checkpoints \
|
||||
(id, conv_id, snapshot, token_total, created_at) \
|
||||
VALUES ('{}', '{}', '{}', {}, '{}')",
|
||||
ck_id, conv_id, escaped_snapshot, total_tokens, ck_now
|
||||
);
|
||||
if let Err(e) = db.conn().lock().await.execute_batch(&sql) {
|
||||
let ck_now = now_millis().to_string();
|
||||
// 参数化绑定(走 AiConversationRepo::insert_checkpoint),替代原 format! 拼 SQL,
|
||||
// 防 snapshot 含引号/特殊字符致注入或损坏。
|
||||
if let Err(e) = conv_repo.insert_checkpoint(&ck_id, conv_id, &snapshot, total_tokens, &ck_now).await {
|
||||
tracing::warn!("checkpoint 写入失败 {conv_id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn build_provider_for(
|
||||
record: &AiProviderRecord,
|
||||
) -> Result<Box<dyn df_ai::provider::LlmProvider>, String> {
|
||||
let api_key = df_storage::secret::resolve_provider_secret(record);
|
||||
df_storage::secret::ensure_resolved_key(&record.name, &api_key)?;
|
||||
let api_key = df_storage::secret::ensure_resolved_key(&record.name, &api_key)?;
|
||||
Ok(df_ai::build_provider(
|
||||
&record.provider_type,
|
||||
&record.base_url,
|
||||
|
||||
Reference in New Issue
Block a user