diff --git a/migrations/20260601000010_fix_llm_usage_legacy_columns.sql b/migrations/20260601000010_fix_llm_usage_legacy_columns.sql new file mode 100644 index 0000000..961a84f --- /dev/null +++ b/migrations/20260601000010_fix_llm_usage_legacy_columns.sql @@ -0,0 +1,18 @@ +-- 修复 llm_usage 表兼容性:旧库有 iPet 遗留的 kind/entry 列 (NOT NULL),新库没有 +-- kind/entry 保留为兼容字段,新代码写入默认值 ("llm_call" / null) +-- 1) 添加列(如果不存在) +ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS kind TEXT; +ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS entry JSONB; +-- 2) 只在列存在且 NOT NULL 时解除约束 +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'llm_usage' AND column_name = 'kind' AND is_nullable = 'NO') THEN + ALTER TABLE llm_usage ALTER COLUMN kind DROP NOT NULL; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'llm_usage' AND column_name = 'entry' AND is_nullable = 'NO') THEN + ALTER TABLE llm_usage ALTER COLUMN entry DROP NOT NULL; + END IF; +END; +$$; diff --git a/migrations/20260601000011_fix_chat_records_context_token.sql b/migrations/20260601000011_fix_chat_records_context_token.sql new file mode 100644 index 0000000..d55af5b --- /dev/null +++ b/migrations/20260601000011_fix_chat_records_context_token.sql @@ -0,0 +1,26 @@ +-- 修复 chat_records 表中 iPet 遗留的 context_token_present (boolean) → context_token (text) +-- 迁移 0001 创建的是 context_token TEXT,但旧库中列名是 context_token_present BOOLEAN +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'chat_records' AND column_name = 'context_token_present') THEN + + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'chat_records' AND column_name = 'context_token') THEN + -- 两列都存在(混合/半迁移库):回填数据后删除旧列 + UPDATE chat_records + SET context_token = CASE WHEN context_token_present::boolean THEN '1' ELSE '' END + WHERE context_token = ''; + ALTER TABLE chat_records DROP COLUMN context_token_present; + ELSE + -- 只有旧列:类型转换 boolean → text (true → '1', false → ''),再重命名 + ALTER TABLE chat_records + ALTER COLUMN context_token_present TYPE TEXT + USING CASE WHEN context_token_present::boolean THEN '1' ELSE '' END; + ALTER TABLE chat_records RENAME COLUMN context_token_present TO context_token; + ALTER TABLE chat_records ALTER COLUMN context_token SET DEFAULT ''; + END IF; + + END IF; +END; +$$; diff --git a/migrations/20260601000012_fix_chat_dedup.sql b/migrations/20260601000012_fix_chat_dedup.sql new file mode 100644 index 0000000..0cee8d1 --- /dev/null +++ b/migrations/20260601000012_fix_chat_dedup.sql @@ -0,0 +1,12 @@ +-- 聊天记录去重:清理存量重复 → 建 partial UNIQUE 索引支持 ON CONFLICT +-- 1) 删除重复记录(保留每个 message_id 最早的那条) +DELETE FROM chat_records a +USING chat_records b +WHERE a.message_id = b.message_id + AND a.message_id NOT IN ('', '0') + AND a.id > b.id; + +-- 2) 替换旧普通索引为 UNIQUE partial index(排除空和占位值) +DROP INDEX IF EXISTS idx_chat_records_message_id; +CREATE UNIQUE INDEX idx_chat_records_message_id + ON chat_records (message_id) WHERE message_id NOT IN ('', '0'); diff --git a/src/db/models.rs b/src/db/models.rs index 94911d6..896aa26 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -66,11 +66,11 @@ pub async fn insert_chat_record( ) -> Result { let ctx = context_token.unwrap_or(""); - let row: (i64,) = sqlx::query_as( + let row: Option<(i64,)> = sqlx::query_as( r#" INSERT INTO chat_records (direction, user_id, account_id, text, source, context_token, message_id) VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (message_id) DO NOTHING + ON CONFLICT (message_id) WHERE message_id NOT IN ('', '0') DO NOTHING RETURNING id "#, ) @@ -81,11 +81,11 @@ pub async fn insert_chat_record( .bind(source) .bind(ctx) .bind(message_id) - .fetch_one(pool) + .fetch_optional(pool) .await .map_err(|e| format!("插入聊天记录失败: {}", e))?; - Ok(row.0) + Ok(row.map(|r| r.0).unwrap_or(0)) } /// 查询最近的聊天记录(用户维度) @@ -115,6 +115,7 @@ pub async fn list_recent_chat_records( // ─── LLM 用量 ─── /// 插入一条 LLM 用量记录 +/// kind/entry 为 iPet 遗留兼容字段,写入默认值 ("llm_call" / null) pub async fn insert_llm_usage( pool: &PgPool, user_id: &str, @@ -128,8 +129,8 @@ pub async fn insert_llm_usage( let total = prompt_tokens + completion_tokens; sqlx::query( r#" - INSERT INTO llm_usage (user_id, model, provider, prompt_tokens, completion_tokens, total_tokens, cache_hit_tokens, cache_miss_tokens) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + INSERT INTO llm_usage (user_id, model, provider, prompt_tokens, completion_tokens, total_tokens, cache_hit_tokens, cache_miss_tokens, kind, entry) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) "#, ) .bind(user_id) @@ -140,6 +141,8 @@ pub async fn insert_llm_usage( .bind(total as i32) .bind(cache_hit_tokens as i32) .bind(cache_miss_tokens as i32) + .bind("llm_call") + .bind(serde_json::Value::Null) .execute(pool) .await .map_err(|e| format!("插入 LLM 用量失败: {}", e))?; diff --git a/src/main.rs b/src/main.rs index 304ca5e..bb7fe16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -162,7 +162,9 @@ async fn cmd_listen( account_id: fa.account_id, base_url: fa.base_url, }; - let _ = db::models::save_auth(db.pool(), &a).await; + if let Err(e) = db::models::save_auth(db.pool(), &a).await { + error!("保存认证信息失败: {}", e); + } break 'load Some(a); } } else if let Some(a) = file_state.load_auth() { @@ -481,13 +483,13 @@ async fn listen_loop( let from = msg.from_user_id.as_deref().unwrap_or("unknown"); let text = msg.text_content().unwrap_or("(非文本消息)"); let ctx_token = msg.context_token.as_deref(); - let msg_id = msg.message_id.unwrap_or(0).to_string(); + let msg_id = msg.message_id.map(|id| id.to_string()).unwrap_or_default(); info!("收到消息 from={}: {}", from, text); // === 入库:收到的消息 === if let Some(db) = database { - let _ = db::models::insert_chat_record( + if let Err(e) = db::models::insert_chat_record( db.pool(), "inbound", from, @@ -497,7 +499,10 @@ async fn listen_loop( ctx_token, &msg_id, ) - .await; + .await + { + error!("保存聊天记录失败: {}", e); + } } // Echo 模式 @@ -507,7 +512,7 @@ async fn listen_loop( info!("回显成功 msg_id={}", sent_id); // === 入库:发送的回显 === if let Some(db) = database { - let _ = db::models::insert_chat_record( + if let Err(e) = db::models::insert_chat_record( db.pool(), "outbound", from, @@ -517,7 +522,10 @@ async fn listen_loop( ctx_token, &sent_id, ) - .await; + .await + { + error!("保存聊天记录失败: {}", e); + } } } Err(e) => error!("回显失败: {}", e), @@ -579,7 +587,7 @@ async fn listen_loop( Ok(sent_id) => { info!("LLM 回复成功 msg_id={}", sent_id); if let Some(db) = database { - let _ = db::models::insert_chat_record( + if let Err(e) = db::models::insert_chat_record( db.pool(), "outbound", &from_owned, @@ -589,7 +597,10 @@ async fn listen_loop( ctx_owned.as_deref(), &sent_id, ) - .await; + .await + { + error!("保存聊天记录失败: {}", e); + } } } Err(e) => error!("发送 LLM 回复失败: {}", e), @@ -659,7 +670,7 @@ async fn store_usage( u: &Usage, ) { if let Some(db) = db { - let _ = db::models::insert_llm_usage( + if let Err(e) = db::models::insert_llm_usage( db.pool(), user_id, model, @@ -669,7 +680,10 @@ async fn store_usage( u.prompt_cache_hit_tokens, u.prompt_cache_miss_tokens, ) - .await; + .await + { + tracing::error!("存储 LLM 用量失败: {}", e); + } } } @@ -797,7 +811,7 @@ async fn cmd_send( // 入库:发送的消息 if let Some(db) = database { - let _ = db::models::insert_chat_record( + if let Err(e) = db::models::insert_chat_record( db.pool(), "outbound", to, @@ -807,7 +821,10 @@ async fn cmd_send( context_token, &msg_id, ) - .await; + .await + { + error!("保存聊天记录失败: {}", e); + } } } Err(e) => error!("发送失败: {}", e),