fix: token统计+聊天记录入库静默失败, 列名/类型/去重不匹配
根因:insert_llm_usage / insert_chat_record 因旧 iPet 遗留列 (kind/entry NOT NULL, context_token_present boolean→context_token text) 导致 INSERT 静默失败, 四个调用点 let _ = 吞掉错误。ias usage 显示 token 总量为 0。 修复: - llm_usage: 迁移 0010 ADD COLUMN IF NOT EXISTS + 条件 DROP NOT NULL, insert_llm_usage 写入 kind/entry 默认值 - chat_records: 迁移 0011 列名 context_token_present→context_token 类型转换+重命名, 迁移 0012 清理存量重复→partial UNIQUE index→恢复 ON CONFLICT - main.rs: 所有 let _ = insert_* 改为 if let Err(e) 日志输出, message_id 缺失时归为 '' 而非 '0'
This commit is contained in:
@@ -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;
|
||||||
|
$$;
|
||||||
@@ -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;
|
||||||
|
$$;
|
||||||
@@ -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');
|
||||||
+9
-6
@@ -66,11 +66,11 @@ pub async fn insert_chat_record(
|
|||||||
) -> Result<i64, String> {
|
) -> Result<i64, String> {
|
||||||
let ctx = context_token.unwrap_or("");
|
let ctx = context_token.unwrap_or("");
|
||||||
|
|
||||||
let row: (i64,) = sqlx::query_as(
|
let row: Option<(i64,)> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO chat_records (direction, user_id, account_id, text, source, context_token, message_id)
|
INSERT INTO chat_records (direction, user_id, account_id, text, source, context_token, message_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
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
|
RETURNING id
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -81,11 +81,11 @@ pub async fn insert_chat_record(
|
|||||||
.bind(source)
|
.bind(source)
|
||||||
.bind(ctx)
|
.bind(ctx)
|
||||||
.bind(message_id)
|
.bind(message_id)
|
||||||
.fetch_one(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("插入聊天记录失败: {}", e))?;
|
.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 用量 ───
|
||||||
|
|
||||||
/// 插入一条 LLM 用量记录
|
/// 插入一条 LLM 用量记录
|
||||||
|
/// kind/entry 为 iPet 遗留兼容字段,写入默认值 ("llm_call" / null)
|
||||||
pub async fn insert_llm_usage(
|
pub async fn insert_llm_usage(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@@ -128,8 +129,8 @@ pub async fn insert_llm_usage(
|
|||||||
let total = prompt_tokens + completion_tokens;
|
let total = prompt_tokens + completion_tokens;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO llm_usage (user_id, model, provider, prompt_tokens, completion_tokens, total_tokens, cache_hit_tokens, cache_miss_tokens)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
@@ -140,6 +141,8 @@ pub async fn insert_llm_usage(
|
|||||||
.bind(total as i32)
|
.bind(total as i32)
|
||||||
.bind(cache_hit_tokens as i32)
|
.bind(cache_hit_tokens as i32)
|
||||||
.bind(cache_miss_tokens as i32)
|
.bind(cache_miss_tokens as i32)
|
||||||
|
.bind("llm_call")
|
||||||
|
.bind(serde_json::Value::Null)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("插入 LLM 用量失败: {}", e))?;
|
.map_err(|e| format!("插入 LLM 用量失败: {}", e))?;
|
||||||
|
|||||||
+29
-12
@@ -162,7 +162,9 @@ async fn cmd_listen(
|
|||||||
account_id: fa.account_id,
|
account_id: fa.account_id,
|
||||||
base_url: fa.base_url,
|
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);
|
break 'load Some(a);
|
||||||
}
|
}
|
||||||
} else if let Some(a) = file_state.load_auth() {
|
} 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 from = msg.from_user_id.as_deref().unwrap_or("unknown");
|
||||||
let text = msg.text_content().unwrap_or("(非文本消息)");
|
let text = msg.text_content().unwrap_or("(非文本消息)");
|
||||||
let ctx_token = msg.context_token.as_deref();
|
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);
|
info!("收到消息 from={}: {}", from, text);
|
||||||
|
|
||||||
// === 入库:收到的消息 ===
|
// === 入库:收到的消息 ===
|
||||||
if let Some(db) = database {
|
if let Some(db) = database {
|
||||||
let _ = db::models::insert_chat_record(
|
if let Err(e) = db::models::insert_chat_record(
|
||||||
db.pool(),
|
db.pool(),
|
||||||
"inbound",
|
"inbound",
|
||||||
from,
|
from,
|
||||||
@@ -497,7 +499,10 @@ async fn listen_loop(
|
|||||||
ctx_token,
|
ctx_token,
|
||||||
&msg_id,
|
&msg_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("保存聊天记录失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Echo 模式
|
// Echo 模式
|
||||||
@@ -507,7 +512,7 @@ async fn listen_loop(
|
|||||||
info!("回显成功 msg_id={}", sent_id);
|
info!("回显成功 msg_id={}", sent_id);
|
||||||
// === 入库:发送的回显 ===
|
// === 入库:发送的回显 ===
|
||||||
if let Some(db) = database {
|
if let Some(db) = database {
|
||||||
let _ = db::models::insert_chat_record(
|
if let Err(e) = db::models::insert_chat_record(
|
||||||
db.pool(),
|
db.pool(),
|
||||||
"outbound",
|
"outbound",
|
||||||
from,
|
from,
|
||||||
@@ -517,7 +522,10 @@ async fn listen_loop(
|
|||||||
ctx_token,
|
ctx_token,
|
||||||
&sent_id,
|
&sent_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("保存聊天记录失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("回显失败: {}", e),
|
Err(e) => error!("回显失败: {}", e),
|
||||||
@@ -579,7 +587,7 @@ async fn listen_loop(
|
|||||||
Ok(sent_id) => {
|
Ok(sent_id) => {
|
||||||
info!("LLM 回复成功 msg_id={}", sent_id);
|
info!("LLM 回复成功 msg_id={}", sent_id);
|
||||||
if let Some(db) = database {
|
if let Some(db) = database {
|
||||||
let _ = db::models::insert_chat_record(
|
if let Err(e) = db::models::insert_chat_record(
|
||||||
db.pool(),
|
db.pool(),
|
||||||
"outbound",
|
"outbound",
|
||||||
&from_owned,
|
&from_owned,
|
||||||
@@ -589,7 +597,10 @@ async fn listen_loop(
|
|||||||
ctx_owned.as_deref(),
|
ctx_owned.as_deref(),
|
||||||
&sent_id,
|
&sent_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("保存聊天记录失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("发送 LLM 回复失败: {}", e),
|
Err(e) => error!("发送 LLM 回复失败: {}", e),
|
||||||
@@ -659,7 +670,7 @@ async fn store_usage(
|
|||||||
u: &Usage,
|
u: &Usage,
|
||||||
) {
|
) {
|
||||||
if let Some(db) = db {
|
if let Some(db) = db {
|
||||||
let _ = db::models::insert_llm_usage(
|
if let Err(e) = db::models::insert_llm_usage(
|
||||||
db.pool(),
|
db.pool(),
|
||||||
user_id,
|
user_id,
|
||||||
model,
|
model,
|
||||||
@@ -669,7 +680,10 @@ async fn store_usage(
|
|||||||
u.prompt_cache_hit_tokens,
|
u.prompt_cache_hit_tokens,
|
||||||
u.prompt_cache_miss_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 {
|
if let Some(db) = database {
|
||||||
let _ = db::models::insert_chat_record(
|
if let Err(e) = db::models::insert_chat_record(
|
||||||
db.pool(),
|
db.pool(),
|
||||||
"outbound",
|
"outbound",
|
||||||
to,
|
to,
|
||||||
@@ -807,7 +821,10 @@ async fn cmd_send(
|
|||||||
context_token,
|
context_token,
|
||||||
&msg_id,
|
&msg_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("保存聊天记录失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("发送失败: {}", e),
|
Err(e) => error!("发送失败: {}", e),
|
||||||
|
|||||||
Reference in New Issue
Block a user