修复: checkpoint 参数化 + 压缩 permit 死锁降级 + 密钥归一化 + SSE CRLF/多字节
This commit is contained in:
@@ -323,6 +323,40 @@ impl AiToolExecutionRepo {
|
||||
// AiConversationRepo 的整体更新已由 impl_repo! 宏统一生成的 update_full 提供。
|
||||
|
||||
impl AiConversationRepo {
|
||||
/// 写入对话版本化快照 checkpoint(INSERT OR IGNORE,同 id 已存在则跳过)。
|
||||
///
|
||||
/// 参数化绑定(替代原调用方的 format! 拼 SQL + execute_batch),防 snapshot 含引号/
|
||||
/// 特殊字符致注入或损坏。列对齐 conversation_checkpoints(id, conv_id, snapshot,
|
||||
/// token_total, created_at)。
|
||||
pub async fn insert_checkpoint(
|
||||
&self,
|
||||
id: &str,
|
||||
conv_id: &str,
|
||||
snapshot: &str,
|
||||
token_total: i64,
|
||||
created_at: &str,
|
||||
) -> Result<()> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_string();
|
||||
let conv_id = conv_id.to_string();
|
||||
let snapshot = snapshot.to_string();
|
||||
let created_at = created_at.to_string();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
guard
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO conversation_checkpoints \
|
||||
(id, conv_id, snapshot, token_total, created_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![id, conv_id, snapshot, token_total, created_at],
|
||||
)
|
||||
.map_err(storage_err)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清空对话消息内容(保留 conversation 记录本身,只清 messages JSON + 清零 token 计数)
|
||||
///
|
||||
/// "清空对话"语义:对话壳保留(侧栏仍可见,可继续在该对话内聊),仅清空历史消息。
|
||||
|
||||
@@ -200,17 +200,40 @@ pub async fn migrate_secrets_to_keyring(repo: &AiProviderRepo) -> anyhow::Result
|
||||
Ok(migrated)
|
||||
}
|
||||
|
||||
/// 校验已解析的密钥是否可用:空(含纯空白)→明确错误信息,非空→Ok。
|
||||
/// 校验已解析的密钥是否可用并归一化:空(含纯空白)→明确错误信息;非空→返回归一化后的 String。
|
||||
///
|
||||
/// 归一化 = trim → 剥首尾配对引号(`"`/`'`)→ 再 trim。覆盖用户粘贴脏 key 的常见场景:
|
||||
/// 复制带前后引号/换行/全角空格/尾部空白,校验能过但原样发 provider 致 401 误报"API Key 无效"。
|
||||
/// 调用方应用返回值(归一化后的 key)替代原 resolved 传给 provider。
|
||||
///
|
||||
/// 用于消费点(build_provider 前)早失败,避免空 key 发请求吃 401,错误伪装成"API Key 无效"。
|
||||
pub fn ensure_resolved_key(provider_name: &str, resolved: &str) -> Result<(), String> {
|
||||
pub fn ensure_resolved_key(provider_name: &str, resolved: &str) -> Result<String, String> {
|
||||
// 先 trim 判空(纯空白视为无密钥,防粘贴时只有空格)。
|
||||
if resolved.trim().is_empty() {
|
||||
Err(format!(
|
||||
return Err(format!(
|
||||
"未读取到「{}」的 API 密钥(系统钥匙串无记录或已损坏),请在设置中重新填写并保存",
|
||||
provider_name
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
));
|
||||
}
|
||||
// 归一化:trim → 剥首尾引号(成对,支持 ""xxx"" / ''xxx'' 多层)→ 再 trim。
|
||||
let mut cleaned = resolved.trim().to_string();
|
||||
while cleaned.len() >= 2 {
|
||||
let first = cleaned.chars().next().unwrap();
|
||||
let last = cleaned.chars().last().unwrap();
|
||||
if (first == '"' || first == '\'') && first == last {
|
||||
cleaned = cleaned[first.len_utf8()..cleaned.len() - last.len_utf8()].trim().to_string();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 剥引号后可能变空(如粘贴仅一对引号)→ 视为无密钥。
|
||||
if cleaned.is_empty() {
|
||||
return Err(format!(
|
||||
"未读取到「{}」的 API 密钥(系统钥匙串无记录或已损坏),请在设置中重新填写并保存",
|
||||
provider_name
|
||||
));
|
||||
}
|
||||
Ok(cleaned)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -230,7 +253,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ensure_resolved_key_accepts_nonempty() {
|
||||
assert!(ensure_resolved_key("GLM", "sk-abc").is_ok());
|
||||
assert_eq!(ensure_resolved_key("GLM", "sk-abc").unwrap(), "sk-abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -239,6 +262,28 @@ mod tests {
|
||||
assert!(err.contains("我的提供商"), "错误信息应含 provider 名便于定位");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_resolved_key_trims_surrounding_whitespace() {
|
||||
// 粘贴带前后空白/换行 → 归一化为干净 key
|
||||
assert_eq!(ensure_resolved_key("GLM", " sk-abc \n").unwrap(), "sk-abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_resolved_key_strips_surrounding_quotes() {
|
||||
// 粘贴带前后引号(双引号/单引号,多层嵌套)→ 剥引号
|
||||
assert_eq!(ensure_resolved_key("GLM", "\"sk-abc\"").unwrap(), "sk-abc");
|
||||
assert_eq!(ensure_resolved_key("GLM", "'sk-abc'").unwrap(), "sk-abc");
|
||||
assert_eq!(ensure_resolved_key("GLM", "\"\"sk-abc\"\"").unwrap(), "sk-abc");
|
||||
assert_eq!(ensure_resolved_key("GLM", " \"sk-abc\"\n").unwrap(), "sk-abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_resolved_key_rejects_only_quotes() {
|
||||
// 仅一对引号(剥后为空)→ 视为无密钥
|
||||
assert!(ensure_resolved_key("GLM", "\"\"").is_err());
|
||||
assert!(ensure_resolved_key("GLM", " '' ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_db_when_non_empty() {
|
||||
// DB api_key 非空 → 直接返回 DB 值,不触发 keyring(兼容未迁移老库)
|
||||
|
||||
Reference in New Issue
Block a user