feat: iPet → iAs 完整迁移,Rust 版微信 AI 助手

Phase 1  CLI 框架 + 配置系统
  - clap 子命令: login / listen / send / whoami / usage
  - config.json + env var 替换
  - tracing 日志系统
  - state 持久化(auth/runtime 文件存 + PostgreSQL)

Phase 2  微信通道
  - wechat::client — 完整 iLink Bot HTTP API 实现
  - 扫码登录(终端二维码 + 轮询状态)
  - 长轮询 getupdates / 消息收发 / 监听注册

Phase 3  AI 对话(纯文本 + function calling)
  - LlmProvider trait: DeepSeek + LM Studio 实现
  - SSE 流式解析(text / reasoning / tool_calls delta / usage)
  - Conversation: 消息历史 + chat / chat_with_tools 工具循环

Phase 4  PostgreSQL 集成
  - app_state(认证 KV 存储)
  - chat_records(消息收发记录)
  - llm_usage(Token 用量统计缓存命中率)
  - user_memories(长期记忆持久化)
  - pending_approvals(审批确认码)
  - scheduled_tasks(定时任务表)

Phase 5  一切皆 Skill(工具系统)
  - SkillRegistry: 系统 + 用户 skills 双目录合并
  - SKILL.md 解析器 + 子进程执行器(stdin JSON → stdout)
  - 9 个系统 Skills: datetime / weather / search / email /
    shell / schedule / memos / read_memories / read_summaries
  - ApprovalManager: High 风险技能 → 确认码审批(透明模式)
  - High 风险技能:确认码审批(透明模式)

Phase 6  定时任务调度器

上下文管理
  - ChatSession: checkpoint + token budget (28K) + summaries
  - Token 估算器(中英文自适应)
  - 12h 空闲 → trigger_idle_summary(不入会话)
  - Budget 溢出 → trigger_overflow_summary(入会话 + drain 旧消息)
  - Summarizer: LLM 生成自然语言摘要(fallback 简单截断)
  - 长期记忆 / 摘要 通过 read_memories / read_summaries 工具按需读取

工具调用日志 + Token 统计
  - INFO: 工具名 + 参数 + 结果摘要
  - DEBUG: 子进程 exit/stdout/stderr
  - ias usage --since --until --model 查看用量和缓存命中率
This commit is contained in:
2026-06-01 17:21:43 +08:00
parent 3a2d2769b5
commit b9de3665d9
53 changed files with 9874 additions and 2 deletions
+154
View File
@@ -0,0 +1,154 @@
use rand::Rng;
use sha2::{Digest, Sha256};
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, oneshot};
/// 审批决策
#[derive(Debug, Clone, PartialEq)]
pub enum ApprovalDecision {
Approved,
Rejected,
Expired,
}
/// 内存中待审批的条目
struct PendingEntry {
code_hash: String,
skill_name: String,
attempts_left: i32,
/// 通知等待方
sender: oneshot::Sender<ApprovalDecision>,
}
/// 审批管理器
pub struct ApprovalManager {
pool: Option<Arc<PgPool>>,
/// user_id → 该用户的待审批队列
pending: Arc<Mutex<HashMap<String, Vec<PendingEntry>>>>,
}
impl ApprovalManager {
pub fn new(pool: Option<Arc<PgPool>>) -> Self {
Self {
pool,
pending: Arc::new(Mutex::new(HashMap::new())),
}
}
/// 创建审批请求
/// 返回 (确认码明文, 接收器) — 调用方 await 接收器等待用户确认
pub async fn create(
&self,
user_id: &str,
skill_name: &str,
) -> Result<(String, oneshot::Receiver<ApprovalDecision>), String> {
let code: u32 = rand::thread_rng().gen_range(100000..999999);
let code_str = code.to_string();
let code_hash = format!("{:x}", Sha256::digest(code_str.as_bytes()));
let (tx, rx) = oneshot::channel();
// 入库
if let Some(ref pool) = self.pool {
let id = uuid::Uuid::new_v4();
let expires_at = chrono::Utc::now() + chrono::Duration::minutes(5);
let params = serde_json::json!({"name": skill_name});
let _ = sqlx::query(
r#"
INSERT INTO pending_approvals (id, expires_at, user_id, skill_name, params, code_hash, status)
VALUES ($1, $2, $3, $4, $5, $6, 'pending')
"#,
)
.bind(id)
.bind(expires_at)
.bind(user_id)
.bind(skill_name)
.bind(&params)
.bind(&code_hash)
.execute(pool.as_ref())
.await;
}
// 注册到内存
let mut map = self.pending.lock().await;
map.entry(user_id.to_string())
.or_default()
.push(PendingEntry {
code_hash,
skill_name: skill_name.to_string(),
attempts_left: 3,
sender: tx,
});
Ok((code_str, rx))
}
/// 处理用户回复(由消息循环调用)
/// 匹配成功时返回 Some(技能名),匹配失败返回 None
pub async fn handle_reply(&self, user_id: &str, reply: &str) -> Option<String> {
let mut map = self.pending.lock().await;
let entries = map.get_mut(user_id)?;
if entries.is_empty() {
return None;
}
let reply_hash = format!("{:x}", Sha256::digest(reply.as_bytes()));
let reply_trimmed = reply.trim();
// 检查取消
if reply_trimmed == "0" || reply_trimmed == "取消" {
let entry = entries.remove(0);
let name = entry.skill_name.clone();
let _ = entry.sender.send(ApprovalDecision::Rejected);
return Some(name);
}
// 检查确认码
let pos = entries.iter().position(|e| e.code_hash == reply_hash);
if let Some(idx) = pos {
let entry = entries.remove(idx);
let name = entry.skill_name.clone();
let _ = entry.sender.send(ApprovalDecision::Approved);
return Some(name);
}
// 确认码错误
if let Some(entry) = entries.first_mut() {
entry.attempts_left -= 1;
if entry.attempts_left <= 0 {
let entry = entries.remove(0);
let name = entry.skill_name.clone();
let _ = entry.sender.send(ApprovalDecision::Rejected);
return Some(name);
}
}
None
}
/// 清理过期(由定时任务调用)
pub async fn clean_expired(&self) {
let mut map = self.pending.lock().await;
for entries in map.values_mut() {
entries.retain(|e| !e.sender.is_closed());
}
map.retain(|_, v| !v.is_empty());
if let Some(ref pool) = self.pool {
let _ = sqlx::query(
"UPDATE pending_approvals SET status = 'expired' WHERE status = 'pending' AND expires_at < NOW()",
)
.execute(pool.as_ref())
.await;
}
}
/// 获取某个用户的待审批条数
pub async fn pending_count(&self, user_id: &str) -> usize {
let map = self.pending.lock().await;
map.get(user_id).map(|v| v.len()).unwrap_or(0)
}
}