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
+92
View File
@@ -0,0 +1,92 @@
use sqlx::PgPool;
use std::sync::Arc;
use tokio::sync::Mutex;
/// 长期记忆管理器(内存 + PostgreSQL 双写)
pub struct MemoryStore {
pool: Option<Arc<PgPool>>,
cache: Arc<Mutex<Vec<String>>>,
}
impl MemoryStore {
pub fn new(pool: Option<Arc<PgPool>>) -> Self {
Self {
pool,
cache: Arc::new(Mutex::new(Vec::new())),
}
}
/// 初始化:从数据库加载记忆到缓存
pub async fn load(&self) {
let pool = match self.pool.as_ref() {
Some(p) => p.clone(),
None => return,
};
if let Ok(rows) = sqlx::query_as::<_, (String,)>(
"SELECT content FROM user_memories WHERE user_id = 'default' ORDER BY id",
)
.fetch_all(pool.as_ref())
.await
{
let mut cache = self.cache.lock().await;
*cache = rows.into_iter().map(|(c,)| c).collect();
}
}
/// 读取所有记忆
pub async fn read(&self) -> String {
let mems = self.cache.lock().await;
if mems.is_empty() {
"暂无长期记忆".to_string()
} else {
mems.iter()
.enumerate()
.map(|(i, m)| format!("{}. {}", i + 1, m))
.collect::<Vec<_>>()
.join("\n")
}
}
/// 写入一条记忆
pub async fn write(&self, content: &str) -> String {
// 写入缓存
self.cache.lock().await.push(content.to_string());
// 写入数据库
if let Some(ref pool) = self.pool {
let _ = sqlx::query(
"INSERT INTO user_memories (user_id, content) VALUES ('default', $1)",
)
.bind(content)
.execute(pool.as_ref())
.await;
}
"已添加长期记忆".to_string()
}
}
/// read_summaries 工具:读取历史摘要
pub async fn read_summaries(
session: &Arc<Mutex<super::types::ChatSession>>,
) -> String {
let s = session.lock().await;
if s.summaries.is_empty() {
"暂无历史摘要".to_string()
} else {
s.summaries
.iter()
.map(|sum| {
let reason = match sum.reason {
super::types::SummaryReason::Overflow => "上下文压缩",
super::types::SummaryReason::Timeout => "空闲超时",
};
format!(
"[{}] {} (原因: {})",
sum.created_at.format("%Y-%m-%d %H:%M"),
sum.text,
reason,
)
})
.collect::<Vec<_>>()
.join("\n---\n")
}
}