feat: 为全部 39 个 Rust 源文件添加系统性中文注释

为整个项目添加了详尽的中文文档注释,覆盖所有模块:

入口与架构:
- main.rs — 架构图、数据流、4 个关键设计决策
- cli.rs — 所有 CLI 子命令中文化
- Cargo.toml — 每个依赖的作用说明
- channel/mod.rs — 渠道标识设计意图

核心守护进程与消息队列:
- daemon.rs — 三消费者架构图、审批流
- queue/* — 公平轮转算法、路由调度架构

LLM 层:
- types.rs — Role/Message/ToolCall 等每个字段含义
- provider.rs — Provider trait 设计 + SSE 解析
- conversation.rs — 工具循环流程图、摘要机制
- deepseek.rs — API 请求格式

上下文管理:
- context/types.rs — ChatSession 消息生命周期
- context/builder.rs — Token 预算管理策略
- context/tools.rs — MemoryStore 双写策略

工具系统:
- tools/mod.rs — 两层元工具架构图
- tools/approval.rs — 审批流程
- tools/definitions.rs — 声明式工具三要素
- tools/subprocess.rs — 执行流程 + 缓存策略

数据库与状态:
- db/* — 5 张表用途、回退策略
- state.rs — 文件存储回退方案
- scheduler.rs — SKIP LOCKED 防重复

已废弃模块:
- ipc.rs — 旧 UDS 协议
- worker.rs — 旧 vs 新架构对比
This commit is contained in:
2026-06-09 20:52:24 +08:00
parent 937556917c
commit 23c7fbac62
30 changed files with 799 additions and 149 deletions
+51 -40
View File
@@ -2,45 +2,56 @@
name = "iAs" name = "iAs"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "微信 AI 智能助手 — 通过 DeepSeek LLM 实现 AI 自动回复"
[dependencies] [dependencies]
# 异步运行时 # ── 异步运行时 ──
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] } # 异步运行时核心
# HTTP 客户端 # ── HTTP 客户端 ──
reqwest = { version = "0.12", features = ["json", "stream", "gzip"] } reqwest = { version = "0.12", features = ["json", "stream", "gzip"] } # HTTP 请求(微信 API + DeepSeek API + 工具调用)
# 序列化 # ── 序列化 ──
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } # 序列化/反序列化
serde_json = "1.0" serde_json = "1.0" # JSON 处理
# 环境变量 # ── 环境变量加载 ──
dotenvy = "0.15" dotenvy = "0.15" # .env 文件加载
# CLI 参数解析 # ── CLI 参数解析 ──
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] } # 命令行参数解析
# 终端输入 # ── 终端交互 ──
rustyline = "14.0" rustyline = "14.0" # REPL 行编辑
# UUID dialoguer = "0.12.0" # 终端交互对话框
uuid = { version = "1", features = ["v4", "serde"] } console = "0.16.3" # 终端控制
# 时间 # ── UUID ──
chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } # 唯一标识符
# 加密 # ── 时间处理 ──
base64 = "0.22" chrono = { version = "0.4", features = ["serde"] } # 日期时间
sha2 = "0.10" # ── 加密与签名 ──
hex = "0.4" base64 = "0.22" # Base64 编码
rand = "0.9" sha2 = "0.10" # SHA-256 哈希(审批码)
# 日志 hex = "0.4" # 十六进制
tracing = "0.1" rand = "0.9" # 随机数
tracing-subscriber = { version = "0.3", features = ["env-filter"] } ed25519-dalek = { version = "2", features = ["pem"] } # Ed25519 JWT 签名(和风天气 API
# 二维码终端显示 # ── 日志 ──
qrcode = "0.14" tracing = "0.1" # 结构化日志
image = "0.25" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # 日志输出
# 异步 trait tracing-appender = "0.2.5" # 文件日志(日滚)
async-trait = "0.1" # ── 二维码 ──
# 流式处理 qrcode = "0.14" # 二维码生成
futures-util = "0.3" image = "0.25" # 图片处理
sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono", "derive", "migrate"] } # ── 异步 trait ──
tracing-appender = "0.2.5" async-trait = "0.1" # 异步 trait 支持
dialoguer = "0.12.0" # ── 流式处理 ──
console = "0.16.3" futures-util = "0.3" # 流式处理工具
dirs = "6.0.0" # ── 数据库 ──
# Ed25519 JWT 签名(天气 API sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono", "derive", "migrate"] } # PostgreSQL
ed25519-dalek = { version = "2", features = ["pem"] } # ── 文件系统 ──
scraper = "0.27.0" dirs = "6.0.0" # 系统目录路径
# ── HTML 解析 ──
scraper = "0.27.0" # HTML 解析(fetch_page 工具)
[[bin]]
name = "ias"
path = "src/main.rs"
[[bin]]
name = "ias-auth"
path = "src/bin/ias-auth.rs"
+15 -7
View File
@@ -1,10 +1,18 @@
//! ias-auth — 独立 Token 生成器 /// ## ias-auth — 独立 Token 生成器(辅助二进制)
//! ///
//! 专供 JSON 工具定义通过 `${cmd:ias-auth gen-qweather-jwt}` 调用 /// 这是一个独立的二进制文件,不依赖主程序的任何模块
//! 编译快、无运行时依赖,独立于主程序 `ias`。 /// 专供 JSON 工具定义通过 `${cmd:ias-auth gen-qweather-jwt}` 调用,
//! /// 用于生成和风天气 API 的 Ed25519 JWT Token。
//! 用法: ///
//! ias-auth gen-qweather-jwt 生成和风天气 Ed25519 JWT Token /// ### 设计原因
/// 独立编译有两个好处:
/// 1. 编译速度快(没有主程序的庞大依赖链)
/// 2. 在 JSON 工具定义中通过 shell 命令调用的开销很小
///
/// ### 用法
/// ```bash
/// ias-auth gen-qweather-jwt
/// ```
use std::process::exit; use std::process::exit;
+34 -4
View File
@@ -1,11 +1,33 @@
//! 渠道 / 通道基础类型 //! ## 渠道/通道基础类型
//! //!
//! 定义消息的来源/目标渠道标识,以及从外部(微信等)收到的原始消息。 //! 定义消息的来源目标渠道标识,以及从外部轮询收到的原始消息。
//!
//! ### 设计意图
//!
//! `ChannelId` 是本项目中所有消息路由的核心标识符。
//! 它由 `platform`(平台名,如 "wechat")和 `user_id`(用户 ID)两部分组成,
//! 确保不同平台上的用户可以被唯一区分。
//!
//! 未来如果要接入 Telegram、Discord 等新平台,只需要新增 `ChannelId` 的构造方法,
//! 消息队列的路由逻辑无需改动。
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
/// 渠道标识 —— 区分消息来自哪个平台和哪个用户 /// ## 渠道标识 —— 区分消息来自哪个平台和哪个用户
///
/// 这是消息队列和 daemon 中所有消息路由的基础。
/// 每个 `PipelineMessage` 都携带一个 `ChannelId`,用来追踪消息的来龙去脉。
///
/// ### 字段
/// * `platform` — 平台名称,如 `"wechat"`、`"telegram"`、`"console"`
/// * `user_id` — 在该平台上的用户标识
///
/// ### 示例
/// ```
/// let ch = ChannelId::wechat("wxid_123");
/// assert_eq!(ch.to_string(), "wechat:wxid_123");
/// ```
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ChannelId { pub struct ChannelId {
/// 平台名称,如 "wechat"、"telegram"、"console" /// 平台名称,如 "wechat"、"telegram"、"console"
@@ -27,7 +49,15 @@ impl fmt::Display for ChannelId {
} }
} }
/// 从外部轮询收到的原始消息 /// ## 从外部轮询收到的原始消息
///
/// 在 daemon 的主循环中,从 WeChat API 获取到的消息会被转换为这个结构,
/// 然后封装成 `PipelineMessage` 投递到消息队列。
///
/// * `channel` — 来源渠道(平台 + 用户)
/// * `text` — 消息文本
/// * `message_id` — 消息 ID
/// * `context_token` — 微信的上下文令牌(用于多轮对话的上下文恢复)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct IncomingMessage { pub struct IncomingMessage {
+8 -6
View File
@@ -1,15 +1,17 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
/// iAs — 微信 AI 智能助手 /// ## iAs — 微信 AI 智能助手
/// ///
/// 支持扫码登录、长轮询收发消息、AI 自动回复(DeepSeek)、 /// 支持扫码登录、长轮询收发消息、AI 自动回复(DeepSeek)、
/// 内置工具(天气/搜索/备忘录/日期时间等)、Token 用量统计。 /// 内置工具(天气/搜索/备忘录/日期时间等)、Token 用量统计。
/// ///
/// 快速开始: /// ### 快速开始
/// ias login 扫码登录微信 /// ```bash
/// ias listen --llm 启动 AI 自动回复 /// ias login # 扫码登录微信
/// ias tool weather 北京 查询天气 /// ias listen --llm # 启动 AI 自动回复
/// ias tool search "最新新闻" 联网搜索 /// ias tool weather 北京 # 查询天气
/// ias tool search "最新新闻" # 联网搜索
/// ```
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(name = "ias", version, about, long_about = None)] #[command(name = "ias", version, about, long_about = None)]
pub struct Cli { pub struct Cli {
+42 -6
View File
@@ -4,13 +4,24 @@ use crate::llm::types::Message;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
/// 估算消息的 token 数 /// ## 估算消息的 token 数
///
/// 粗略估算一条消息的 token 消耗,用于 token budget 管理。
/// 公式:`文本 token + 8`(8 是消息格式的开销)。
///
/// 注意这只是估算,不是精确值,但在实践中足够做预算判断。
pub fn estimate_tokens(msg: &Message) -> u32 { pub fn estimate_tokens(msg: &Message) -> u32 {
let text = msg.content.as_str(); let text = msg.content.as_str();
estimate_text_tokens(text) + 8 estimate_text_tokens(text) + 8
} }
/// 估算纯文本的 token 数 /// ## 估算纯文本的 token 数
///
/// 中文和英文的 token 消耗不同:
/// * 中文字符 ≈ 1.5 tokens/字(CJK 字符更密集)
/// * 英文字符 ≈ 0.25 tokens/字符
///
/// 这是一个粗略估算,用于在调用 API 之前判断上下文是否超 budget。
pub fn estimate_text_tokens(text: &str) -> u32 { pub fn estimate_text_tokens(text: &str) -> u32 {
if text.is_empty() { if text.is_empty() {
return 0; return 0;
@@ -27,9 +38,17 @@ fn is_cjk(c: char) -> bool {
|| ('\u{f900}'..='\u{faff}').contains(&c) || ('\u{f900}'..='\u{faff}').contains(&c)
} }
/// 构建给 LLM 的消息数组(带 token budget 管理) /// ## 构建给 LLM 的消息数组(带 token budget 管理)
/// ///
/// 返回消息数组和是否需要摘要的信息 /// 这是 LLM 调用前的关键步骤 —— 从 `ChatSession` 中提取合适的消息,
/// 在 token 预算内尽可能保留更多上下文。
///
/// ### 构建策略
/// 1. **系统提示词** — 始终包含
/// 2. **溢出摘要** — 如果有最新的 overflow 摘要,加入(仍在 budget 内时)
/// 3. **近期消息** — 从 checkpoint 开始,从最新消息倒序添加,直到接近 budget
/// 4. **保护最近用户消息** — 确保最后一条用户消息始终在上下文中
/// (即使超过 budget,也在 +2000 范围内强制包含)
pub async fn build_context(session: &Arc<Mutex<ChatSession>>, system_prompt: &str) -> Vec<Message> { pub async fn build_context(session: &Arc<Mutex<ChatSession>>, system_prompt: &str) -> Vec<Message> {
let s = session.lock().await; let s = session.lock().await;
@@ -92,7 +111,18 @@ pub async fn build_context(session: &Arc<Mutex<ChatSession>>, system_prompt: &st
messages messages
} }
/// 触发溢出摘要压缩 checkpoint 到当前位置之间的新消息 /// ## 触发溢出摘要 —— 压缩 checkpoint 到当前位置之间的新消息
///
/// 当上下文 token 超 budget 时调用。把 checkpoint 之后的消息交给 LLM 生成摘要,
/// 然后推进 checkpoint,让旧消息可以被释放(或截断)。
///
/// ### 流程
/// 1. 获取 checkpoint 到当前末尾的消息(最多 40 条)
/// 2. 用 LLM summarizer 生成摘要(不可用时回退到简单截断)
/// 3. 将摘要存入 `summaries` 列表
/// 4. 持久化到数据库
/// 5. 推进 checkpoint 到当前末尾
/// 6. 如果 checkpoint 超过 200,截断最早的消息(保留最近 100 条用于调试)
pub async fn trigger_overflow_summary( pub async fn trigger_overflow_summary(
session: &Arc<Mutex<ChatSession>>, session: &Arc<Mutex<ChatSession>>,
summarizer: Option<&Summarizer>, summarizer: Option<&Summarizer>,
@@ -153,7 +183,13 @@ pub async fn trigger_overflow_summary(
true true
} }
/// 触发空闲摘要(不注入,保存到 summaries 供 LLM 工具查询 /// ## 触发空闲超时摘要(不注入上下文,只存档
///
/// 当距上条用户消息超过 12 小时时触发。
/// 与溢出摘要不同,空闲摘要**不会**作为 system prompt 注入到下一次对话中,
/// 而是只保存到数据库,供 `read_summaries` 工具查询。
///
/// 生成摘要后会清空所有消息,checkpoint 重置为 0。
pub async fn trigger_idle_summary( pub async fn trigger_idle_summary(
session: &Arc<Mutex<ChatSession>>, session: &Arc<Mutex<ChatSession>>,
summarizer: Option<&Summarizer>, summarizer: Option<&Summarizer>,
+18 -2
View File
@@ -3,7 +3,20 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
/// 长期记忆管理器(内存 + PostgreSQL 双写,按用户隔离 /// ## 长期记忆管理器(内存 + PostgreSQL 双写)
///
/// 为每个用户维护独立的长期记忆列表,支持读写操作。
///
/// ### 存储策略
/// * **内存缓存** — `HashMap<String, Vec<String>>`,按 user_id 隔离
/// * **数据库持久化** — 可选的 `PgPool`,写入时同时写到 PostgreSQL
///
/// ### 数据流
/// ```text
/// 启动时 load(user_id) → 从数据库加载到缓存
/// 对话中 read_for() → 从缓存读取,返回 "1. ...\n2. ..." 格式
/// 对话中 write_for() → 写入缓存 + 写入数据库
/// ```
pub struct MemoryStore { pub struct MemoryStore {
pool: Option<Arc<PgPool>>, pool: Option<Arc<PgPool>>,
cache: Arc<Mutex<HashMap<String, Vec<String>>>>, cache: Arc<Mutex<HashMap<String, Vec<String>>>>,
@@ -61,7 +74,10 @@ impl MemoryStore {
} }
} }
/// read_summaries 工具读取历史摘要(内存 + 数据库) /// ## `read_summaries` 工具 —— 读取历史摘要(内存 + 数据库)
///
/// 返回当前 session 中的摘要列表 + 数据库中存档的摘要。
/// 去重:数据库中的摘要如果与 session 中的摘要前 30 个字符相同,则跳过。
pub async fn read_summaries( pub async fn read_summaries(
session: &Arc<Mutex<super::types::ChatSession>>, session: &Arc<Mutex<super::types::ChatSession>>,
) -> String { ) -> String {
+28 -3
View File
@@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
/// 摘要原因 /// ## 摘要原因
///
/// 区分两种触发摘要的场景:
/// * `Overflow` — 上下文 token 总数超过预算(token budget),触发压缩
/// * `Timeout` — 距上条用户消息超过 12 小时空闲,触发会话总结
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SummaryReason { pub enum SummaryReason {
/// 上下文 token 超 budget 触发 /// 上下文 token 超 budget 触发
@@ -13,7 +17,12 @@ pub enum SummaryReason {
Timeout, Timeout,
} }
/// 一条摘要记录 /// ## 一条摘要记录
///
/// * `checkpoint` — 摘要对应的消息位置,此位置之前的消息已被压缩成摘要
/// * `text` — 摘要文本
/// * `reason` — 生成原因(Overflow / Timeout
/// * `created_at` — 生成时间
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SummaryEntry { pub struct SummaryEntry {
/// 摘要对应的消息位置(checkpoint) /// 摘要对应的消息位置(checkpoint)
@@ -26,7 +35,23 @@ pub struct SummaryEntry {
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
/// 聊天会话状态 /// ## 聊天会话状态(核心数据结构)
///
/// 这是整个项目中最重要的数据结构之一 —— 它维护了一次 LLM 对话的完整状态。
///
/// ### 关键设计
/// * **消息历史** (`messages`) — 从 checkpoint 之后的消息保持完整
/// * **摘要机制** (`summaries` + `checkpoint`) — checkpoint 之前的消息被压缩成摘要,
/// 用 token budget 确保上下文不会超出 LLM 的限制
/// * **Token 预算** (`token_budget`) — 默认 28000,留 4000 给回复
/// * **空闲超时** (`idle_timeout_secs`) — 默认 12 小时,超时后生成空闲摘要
/// * **会话 ID** (`session_id`) — 每次新对话分配一个 Uuid,用于追踪工具调用往返
///
/// ### 消息生命周期
/// 1. 用户消息到达 → `add_user()` → 存入 `messages`
/// 2. AI 回复 → `add_assistant()` → 存入 `messages`
/// 3. Token 超 budget → `trigger_overflow_summary()` → 压缩 checkpoint 前的消息
/// 4. 12h 空闲 → `trigger_idle_summary()` → 生成摘要,清空消息
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ChatSession { pub struct ChatSession {
pub user_id: String, pub user_id: String,
+53 -5
View File
@@ -1,7 +1,43 @@
//! Daemon — 常驻进程 //! ## Daemon — 常驻守护进程(核心循环)
//! //!
//! 持有 WeChat 长连接 + 数据库。使用消息队列架构将消息处理分解为 //! 这是整个项目的核心入口循环。它的职责:
//! LLM / Tool / Send 三个消费者(tokio task),通过 QueueRunner 路由。 //!
//! 1. **持有 WeChat 长连接** — 通过 `WeChatClient` 长轮询接收微信消息
//! 2. **持有数据库连接** — 可选 PostgreSQL,回退到文件存储
//! 3. **管理审批流程** — `ApprovalManager` 处理高风险工具的确认码审批
//! 4. **管理长期记忆** — `MemoryStore` 提供按用户隔离的记忆读写
//!
//! ### 消息处理架构(三消费者 + 公平队列)
//!
//! daemon 将消息处理拆分为 3 个独立的 tokio async task(消费者),
//! 通过 `MessageQueue` + `QueueRunner` 做公平轮转路由:
//!
//! ```text
//! receive_messages → enqueue PipelineMessage
//! │
//! ▼
//! ┌──────────────────┐
//! │ MessageQueue │ ← 公平轮转(A1, B1, A2, A3
//! └────────┬─────────┘
//! │ dequeue
//! ▼
//! ┌──────────────────┐
//! │ QueueRunner │ ← 按 MessageKind 路由
//! └──┬──────┬──────┬─┘
//! │ │ │
//! ▼ ▼ ▼
//! LLM Tool Send
//! Cons. Cons. Cons.
//! ```
//!
//! - **LLM Consumer** — 调用 DeepSeek API 做对话 + 工具循环
//! - **Tool Consumer** — 执行内置工具,高风险工具先走审批
//! - **Send Consumer** — 将 LLM 回复通过 WeChat API 发回给用户
//!
//! ### 与旧架构的关系
//!
//! 旧架构使用 `daemon + worker` 分离模式(UDS IPC),worker 已废弃。
//! 当前是统一的单进程 tokio task 架构。
use crate::channel::ChannelId; use crate::channel::ChannelId;
use crate::context::MemoryStore; use crate::context::MemoryStore;
@@ -19,17 +55,29 @@ use tokio::sync::Mutex;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use uuid::Uuid; use uuid::Uuid;
/// Daemon 上下文 —— 消费者共享的所有状态 /// ## Daemon 上下文 —— 消费者之间共享的全部状态
///
/// 这个结构体被所有三个消费者(LLM / Tool / Send)通过 `Arc` 共享访问。
/// 每个字段都只读(或内部有锁),所以不需要额外的 `Mutex` 包装。
struct DaemonCtx { struct DaemonCtx {
/// 数据库句柄(可选,没有时走文件存储)
db: Option<Arc<Database>>, db: Option<Arc<Database>>,
/// WeChat API 客户端(内部用 Arc<Mutex> 保护 token/updates_buf
client: WeChatClient, client: WeChatClient,
/// 本机器人的微信账号 ID
account_id: String, account_id: String,
/// 审批管理器 —— 处理高风险工具的用户确认码流程
approval: Arc<ApprovalManager>, approval: Arc<ApprovalManager>,
/// 长期记忆管理器 —— 按用户隔离的记忆读写
memory_store: Arc<MemoryStore>, memory_store: Arc<MemoryStore>,
/// 注册给 LLM 的工具列表(JSON 格式,符合 function calling 规范)
tools_list: Vec<serde_json::Value>, tools_list: Vec<serde_json::Value>,
/// 系统提示词,覆盖默认值
system_prompt: String, system_prompt: String,
/// 使用的模型名称(如 `deepseek-v4-flash`
model: String, model: String,
/// 待审批: user_id → (原始文本, context_token, 工具名, tool_args, 创建时间) /// 待审批的上下文:`user_id → (原始文本, context_token, 工具名, 工具参数, 创建时间)`
/// 用户在聊天中发送确认码时,从这里面查找匹配项
approval_ctx: Mutex<HashMap<String, (String, Option<String>, String, String, Instant)>>, approval_ctx: Mutex<HashMap<String, (String, Option<String>, String, String, Instant)>>,
} }
+16 -2
View File
@@ -1,10 +1,24 @@
//! ## 数据库管理
//!
//! ### 职责
//! 1. 从 `DATABASE_URL` 环境变量连接 PostgreSQL
//! 2. 自动运行 `./migrations/` 下的 SQL 迁移
//! 3. 提供连接池引用给各模块使用
//!
//! ### 回退策略
//! 数据库是可选的。如果数据库不可用,系统会优雅地回退到文件存储
//! `StateManager`),核心功能仍然可用。
pub mod models; pub mod models;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::time::Duration; use std::time::Duration;
/// 数据库管理器 /// ## 数据库管理器
///
/// 封装了 PostgreSQL 连接池的创建和迁移管理。
/// 通过 `Database::connect()` 创建,所有数据库操作都在 `models` 模块中。
pub struct Database { pub struct Database {
pool: PgPool, pool: PgPool,
} }
+12
View File
@@ -1,3 +1,15 @@
//! ## 数据库查询(models
//!
//! 所有数据库操作集中在此模块,包括:
//!
//! ### 表操作
//! * `app_state` — 认证状态的持久化(key-value 存储)
//! * `chat_records` — 聊天记录(inbound/outbound
//! * `llm_usage` — Token 用量记录和统计
//! * `session_summaries` — 会话摘要存档
//! * `user_memories` — 用户长期记忆
//! * `pending_approvals` — 待审批记录
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
+14 -5
View File
@@ -1,10 +1,19 @@
//! IPC 帧格式 — Unix Domain Socket 长度前缀帧 //! ## IPC 帧格式 — Unix Domain Socket 长度前缀帧(已废弃)
//! //!
//! 帧格式: [4 字节 u32 BE: payload_len][N 字节: JSON payload] //! 这是旧架构(daemon + worker 分离模式)的进程间通信协议。
//! 新架构已改为统一的单进程 tokio task,此模块保留仅为编译参考。
//! //!
//! 消息类型定义: //! ### 帧格式
//! Daemon → Worker: Task (携带用户消息、历史、记忆、摘要、环境变量) //! ```text
//! Worker → Daemon: Chunk / Reply / NeedApproval / DbWrite / UsageReport / Bye //! [4 字节 u32 大端: payload_len][N 字节: JSON payload]
//! ```
//!
//! ### 消息类型
//! * Daemon → Worker: `TaskFrame`(用户消息 + 上下文 + 环境变量)
//! * Worker → Daemon: `OutputFrame`Chunk / Reply / NeedApproval / DbWrite / UsageReport / Bye
//!
//! ### 最大帧大小
//! 10MB(防止恶意超大帧导致 OOM)
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
+78 -5
View File
@@ -8,18 +8,39 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
/// ## 工具执行器 —— LLM 调用的工具回调
///
/// 这是一个函数别名:`Fn(工具名, JSON参数) → Future<Result<String>>`。
///
/// 当 LLM 在对话中发起工具调用时,Conversation 会调用这个 executor 来执行工具。
///
/// ### 两种执行模式
/// 1. **同步模式** — executor 直接执行并返回结果字符串
/// 2. **异步模式** — executor 将工具调用入队到消息队列,返回 `ASYNC_MARKER + 工具名`
/// 表示"工具已异步入队,结果稍后通过 ToolResult 消息回喂"
pub type ToolExecutor = Arc< pub type ToolExecutor = Arc<
dyn Fn(&str, &str) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> dyn Fn(&str, &str) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>>
+ Send + Send
+ Sync, + Sync,
>; >;
/// 摘要生成器:接收要摘要的消息文本 → 返回 LLM 生成的摘要 /// ## 摘要生成器 —— 用于生成对话摘要的回调
///
/// Conversation 在两种情况下会触发摘要:
/// 1. **溢出摘要** — 消息 token 总数接近 budget 时,压缩早期消息
/// 2. **空闲超时摘要** — 距上条用户消息超过 12 小时时,总结本次会话
pub type Summarizer = Arc< pub type Summarizer = Arc<
dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> + Send + Sync, dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> + Send + Sync,
>; >;
/// `chat_with_tools` 的返回结果 /// ## `chat_with_tools` 的返回结果
///
/// * `reply` — LLM 最终的文本回复
/// * `used_tools` — 本轮对话中是否调用了工具
/// * `usage` — Token 用量统计(可选)
/// * `has_pending_async` — 是否有异步工具调用仍在等待结果
/// 如果为 true,调用方应该保留 Conversation 实例,等待 ToolResult 回来后再调用
/// `resume_tool_loop()` 恢复对话
#[derive(Debug)] #[derive(Debug)]
pub struct ChatResult { pub struct ChatResult {
pub reply: String, pub reply: String,
@@ -32,6 +53,27 @@ pub struct ChatResult {
/// 异步工具标记 —— 工具执行器返回此前缀表示工具已异步入队 /// 异步工具标记 —— 工具执行器返回此前缀表示工具已异步入队
pub const ASYNC_MARKER: &str = "__ASYNC_PENDING__"; pub const ASYNC_MARKER: &str = "__ASYNC_PENDING__";
/// ## Conversation —— LLM 对话管理器(核心类)
///
/// 这是整个项目中"与 LLM 对话"的核心抽象,负责:
///
/// ### 职责
/// 1. **管理对话状态** — 持有 `ChatSession`(消息历史、摘要、token 预算)
/// 2. **流式对话** — 调用 provider 发起流式请求,逐 chunk 消费
/// 3. **工具循环** — 自动处理 LLM 发起的工具调用,循环直到无工具或达到轮次上限
/// 4. **异步工具** — 支持工具通过消息队列异步执行,结果回来后恢复对话
/// 5. **上下文管理** — token 预算检查、溢出摘要、空闲超时摘要
///
/// ### 工具循环流程
/// ```text
/// chat_with_tools(user_msg)
/// → run_tool_loop()
/// → 1. build_context() 构建上下文(含摘要、system prompt
/// → 2. provider.chat_stream() 发起流式请求
/// → 3. 消费流,收集文本 + tool_calls
/// → 4. 有 tool_calls?→ 执行 → 回到步骤 1(最多 5 轮)
/// → 5. 无 tool_calls?→ 返回最终回复
/// ```
pub struct Conversation { pub struct Conversation {
config: ConversationConfig, config: ConversationConfig,
provider: BoxedProvider, provider: BoxedProvider,
@@ -42,6 +84,11 @@ pub struct Conversation {
} }
impl Conversation { impl Conversation {
/// ## 创建 Conversation
///
/// 1. 通过 `create_provider()` 工厂从配置创建 LLM 提供商
/// 2. 初始化空的 `ChatSession`(消息历史在收到第一条用户消息后按需加载)
/// 3. tool_executor 初始为 None,需要调用 `set_tool_executor` 设置
pub fn new(config: ConversationConfig) -> Result<Self, String> { pub fn new(config: ConversationConfig) -> Result<Self, String> {
let provider = create_provider(&config)?; let provider = create_provider(&config)?;
Ok(Self { Ok(Self {
@@ -77,7 +124,12 @@ impl Conversation {
self.create_summarizer() self.create_summarizer()
} }
/// 创建 LLM 摘要生成器(用当前 provider 做非流式调用) /// ## 创建 LLM 摘要生成器
///
/// 用当前 LLM provider 创建一个非流式调用的摘要器。
/// 把消息列表拼成 prompt 发给 LLM,返回生成的摘要文本。
///
/// 摘要 prompt 为:"你是一个对话摘要助手,用中文输出简洁摘要。"
pub fn create_summarizer(&self) -> Summarizer { pub fn create_summarizer(&self) -> Summarizer {
let provider = self.provider.clone(); let provider = self.provider.clone();
Arc::new(move |prompt: String| { Arc::new(move |prompt: String| {
@@ -110,7 +162,17 @@ impl Conversation {
}) })
} }
/// 单次对话(无工具) /// ## 单次对话(无工具调用
///
/// 简单的流式对话:发送用户消息 → 获取 AI 回复。
/// 不会触发工具循环,适用于不需要 function calling 的场景。
///
/// ### 流程
/// 1. 检查是否空闲超时(距上条消息 > 12h),如果是则触发摘要
/// 2. 将用户消息添加到 session
/// 3. 构建上下文(system prompt + 摘要 + 近期消息)
/// 4. 发起流式请求
/// 5. 返回 `ChatHandle`(可逐 chunk 消费)
pub async fn chat(&self, user_message: String) -> Result<ChatHandle, String> { pub async fn chat(&self, user_message: String) -> Result<ChatHandle, String> {
// 空闲超时检查 // 空闲超时检查
{ {
@@ -134,7 +196,18 @@ impl Conversation {
}) })
} }
/// 对话 + 工具循环(带上下文管理 /// ## 对话 + 工具循环(主入口
///
/// 这是最常用的方法:发送用户消息,AI 可以选择回复文本或调用工具。
/// 如果调用了工具,工具执行结果会被自动回喂给 AI,AI 再决定下一步操作。
///
/// ### 完整流程
/// 1. 空闲超时检查 → 触发摘要
/// 2. 添加用户消息到 session
/// 3. 进入 `run_tool_loop()`
/// - 构建上下文 → 调 LLM → 解析流
/// - 有 tool_calls?→ 执行工具 → 继续循环(最多 5 轮)
/// - 无 tool_calls?→ 返回最终回复
pub async fn chat_with_tools(&self, user_message: String) -> Result<ChatResult, String> { pub async fn chat_with_tools(&self, user_message: String) -> Result<ChatResult, String> {
// ── 空闲超时检查 ── // ── 空闲超时检查 ──
{ {
+21
View File
@@ -7,6 +7,27 @@ use reqwest::Client as HttpClient;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use tokio::sync::mpsc; use tokio::sync::mpsc;
/// ## DeepSeek LLM 提供商
///
/// 实现了 `LlmProvider` trait,通过 DeepSeek API 提供流式对话能力。
///
/// ### 配置
/// 从环境变量读取:
/// * `DEEPSEEK_API_KEY` — API 密钥(必需)
/// * `DEEPSEEK_BASE_URL` — API 地址(可选,默认 `https://api.deepseek.com/v1`
/// * `DEEPSEEK_MODEL` — 模型名(可选,默认 `deepseek-v4-flash`
///
/// ### 请求格式
/// 发送 POST 到 `{base_url}/chat/completions`,标准 OpenAI-compatible 格式:
/// ```json
/// {
/// "model": "deepseek-v4-flash",
/// "messages": [...],
/// "stream": true,
/// "tools": [...], // 可选,function calling
/// "thinking": {...} // 可选,DeepSeek 推理模式
/// }
/// ```
pub struct DeepSeekProvider { pub struct DeepSeekProvider {
http: HttpClient, http: HttpClient,
api_key: String, api_key: String,
+35 -4
View File
@@ -8,7 +8,16 @@ use tokio::sync::mpsc;
pub type StreamReceiver = mpsc::Receiver<StreamChunk>; pub type StreamReceiver = mpsc::Receiver<StreamChunk>;
pub type StreamSender = mpsc::Sender<StreamChunk>; pub type StreamSender = mpsc::Sender<StreamChunk>;
/// LLM 提供商抽象 /// ## LLM 提供商抽象trait
///
/// 这是 LLM 层的核心抽象接口,定义了"一个 LLM 提供商需要提供什么能力"。
/// 目前唯一的实现是 `DeepSeekProvider`,但通过这个 trait 可以轻松添加
/// 其他提供商(如 OpenAI、Claude 等)。
///
/// ### 关键设计
/// * 使用 `async_trait` 宏支持异步 trait 方法
/// * 返回 `StreamReceiver`mpsc::Receiver),调用方可以按 chunk 消费流式结果
/// * `Send + Sync` 保证可以在 tokio task 间安全共享
#[async_trait] #[async_trait]
pub trait LlmProvider: Send + Sync { pub trait LlmProvider: Send + Sync {
/// 提供商名称 /// 提供商名称
@@ -26,14 +35,23 @@ pub trait LlmProvider: Send + Sync {
pub type BoxedProvider = Arc<dyn LlmProvider>; pub type BoxedProvider = Arc<dyn LlmProvider>;
/// 从配置创建恰当的提供商 /// ## `create_provider` — 工厂函数:从配置创建恰当的 LLM 提供商
///
/// 目前只返回 DeepSeek,后续可以在这里根据配置 switch 到不同提供商。
pub fn create_provider(_config: &ConversationConfig) -> Result<BoxedProvider, String> { pub fn create_provider(_config: &ConversationConfig) -> Result<BoxedProvider, String> {
Ok(Arc::new(super::deepseek::DeepSeekProvider::new()?)) Ok(Arc::new(super::deepseek::DeepSeekProvider::new()?))
} }
// ─── 内部:SSE 解析工具 ─── // ─── 内部:SSE 解析工具 ───
/// 流式块解析结果 /// ## 流式块解析结果(内部枚举)
///
/// 用于从 SSE 的 JSON delta 中解析出不同种类的块。
/// * `Text` — 普通文本 delta
/// * `Reasoning` — 思考内容(DeepSeek reasoning_content
/// * `ToolCallDelta` — 工具调用的增量信息(可能需要跨多个 chunk 拼接)
/// * `FinishReason` — 结束原因(stop / tool_calls 等)
/// * `Usage` — 用量统计(通常在最后一个 chunk 中)
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enum ParsedChunk { pub(crate) enum ParsedChunk {
Text(String), Text(String),
@@ -48,7 +66,20 @@ pub(crate) enum ParsedChunk {
Usage(super::types::Usage), Usage(super::types::Usage),
} }
/// 从 JSON body 中解析 DeepSeek/OpenAI 流式 delta /// ## 从 JSON body 中解析 DeepSeek/OpenAI 流式 delta
///
/// ### 输入
/// 一行 SSE 数据,格式为 `data: {json}`。
///
/// ### 处理逻辑
/// 1. 跳过非 `data: ` 前缀的行和 `[DONE]` 行
/// 2. 解析 JSON 为 `ChunkResponse`(内嵌多个 `choices`
/// 3. 提取 usage(可能在任意一个 chunk 中)
/// 4. 对每个 choice,按优先级提取:
/// - tool_calls delta → `ToolCallDelta`
/// - reasoning_content → `Reasoning`
/// - content → `Text`
/// - finish_reason → `FinishReason`
pub(crate) fn parse_chat_chunk(line: &str) -> Option<ParsedChunk> { pub(crate) fn parse_chat_chunk(line: &str) -> Option<ParsedChunk> {
if !line.starts_with("data: ") { if !line.starts_with("data: ") {
return None; return None;
+48 -12
View File
@@ -1,7 +1,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// ─── 角色 ─── /// ## 对话角色(System / User / Assistant / Tool
///
/// 符合 OpenAI/DeepSeek 的 Chat Completion API 标准角色定义。
/// * `System` — 系统提示词,设定 AI 的行为和限制
/// * `User` — 用户消息
/// * `Assistant` — AI 回复(可以是纯文本或带 tool_calls
/// * `Tool` — 工具执行结果的回馈
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Role { pub enum Role {
@@ -11,8 +16,17 @@ pub enum Role {
Tool, Tool,
} }
// ─── 消息 ─── /// ## LLM 消息 —— 符合 OpenAI/DeepSeek Chat Completion API 格式
///
/// 这是整个 LLM 交互的核心类型。`Vec<Message>` 构成一次对话的全部上下文,
/// 被序列化为 JSON 发送给 DeepSeek API。
///
/// ### 字段
/// * `role` — 消息角色(system/user/assistant/tool
/// * `content` — 消息内容(文本)
/// * `tool_call_id` — 如果是 tool 角色的结果消息,关联到原始 tool_call 的 ID
/// * `name` — 工具名(tool 角色时使用)
/// * `tool_calls` — assistant 发起工具调用时携带的调用列表
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message { pub struct Message {
pub role: Role, pub role: Role,
@@ -48,8 +62,12 @@ impl Message {
} }
} }
// ─── 工具调用 ─── /// ## 工具调用 —— LLM 发起的一个工具调用请求
///
/// 当 LLM 认为需要调用工具时,会在回复中携带这个结构。
/// * `id` — 唯一标识这个调用(用于 tool result 回馈时匹配)
/// * `call_type` — 固定为 "function"
/// * `function` — 函数名 + JSON 格式的参数
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall { pub struct ToolCall {
pub id: String, pub id: String,
@@ -64,8 +82,13 @@ pub struct ToolFunctionCall {
pub arguments: String, // JSON string pub arguments: String, // JSON string
} }
// ─── 流式响应块 ─── /// ## 流式响应块 —— 从 DeepSeek API 的 SSE 流中解析出的每个 delta
///
/// ### 变体
/// * `Text` — 文本片段 delta(逐步拼接得到完整回复)
/// * `Reasoning` — 思考/推理内容(DeepSeek 的 thinking 机制)
/// * `Done` — 完成信号,携带完整文本 + 可选的工具调用 + 用量统计
/// * `Error` — 错误
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum StreamChunk { pub enum StreamChunk {
@@ -85,8 +108,14 @@ pub enum StreamChunk {
Error(String), Error(String),
} }
// ─── Token 用量 ─── /// ## Token 用量统计
///
/// DeepSeek API 在流式响应的最后一个 chunk 中会附带用量信息。
/// * `prompt_tokens` — 提示词消耗的 token 数
/// * `completion_tokens` — 生成文本消耗的 token 数
/// * `total_tokens` = prompt + completion
/// * `prompt_cache_hit_tokens` — 命中上下文的缓存 token 数(省钱)
/// * `prompt_cache_miss_tokens` — 未命中缓存的 token 数
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Usage { pub struct Usage {
pub prompt_tokens: u32, pub prompt_tokens: u32,
@@ -98,8 +127,15 @@ pub struct Usage {
pub prompt_cache_miss_tokens: u32, pub prompt_cache_miss_tokens: u32,
} }
// ─── 对话配置 ─── /// ## 对话配置 —— 创建 Conversation 时的参数
///
/// ### 关键字段
/// * `system_prompt` — 系统提示词,决定 AI 的行为方式
/// * `model` — 模型名称(如 `deepseek-v4-flash`
/// * `temperature` — 生成随机性(0.0-1.0,越高越有创意)
/// * `max_tokens` — 最大生成 token 数
/// * `thinking` — 是否启用 DeepSeek 的 thinking 模式
/// * `tools` — OpenAI 格式的工具定义列表(function calling 用)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ConversationConfig { pub struct ConversationConfig {
pub system_prompt: String, pub system_prompt: String,
+8
View File
@@ -3,6 +3,14 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
/// ## 日志初始化
///
/// ### 两种模式
/// * **终端模式** — 彩色输出到 stderr,适合开发调试
/// * **文件模式** (`with_file=true`) — 同时输出到终端和日滚文件
/// `~/.ias/logs/ias.log.YYYY-MM-DD`
///
/// 日志级别通过 `RUST_LOG` 环境变量控制(默认 `info`)。
pub fn init_logger(log_dir: Option<&str>, with_file: bool) { pub fn init_logger(log_dir: Option<&str>, with_file: bool) {
let env_filter = EnvFilter::try_from_default_env() let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info")); .unwrap_or_else(|_| EnvFilter::new("info"));
+31
View File
@@ -1,3 +1,34 @@
//! ## iAs —— 微信 AI 智能助手(入口)
//!
//! ### 系统架构概览
//!
//! ```text
//! CLI (main.rs → clap)
//! ├── ias login → 扫码登录,保存 token 到 DB 或文件
//! ├── ias listen --llm → Daemon 模式(主运行模式)
//! │ └── daemon.rs: 长轮询 → MessageQueue → 3 个 tokio 消费者
//! │ ├── LLM Consumer → Conversation.chat_with_tools() → DeepSeek API
//! │ ├── Tool Consumer → BuiltinRegistry.execute() / ApprovalManager
//! │ └── Send Consumer → WeChatClient.send_message()
//! ├── ias service → 等同 listen --llm 但启用文件日志
//! ├── ias daemon → 守护进程入口(等同 listen --llm
//! ├── ias tool <tool> → 直接调用内置工具(无需登录)
//! └── ias usage → Token 消耗统计查询
//! ```
//!
//! ### 数据流
//! ```text
//! 微信消息 → 存 chat_records → 加载历史 + 记忆 → 构建 LLM 上下文
//! → Conversation.chat_with_tools() → LLM 回复或调工具
//! → 高风险工具走审批 → 工具结果回喂 LLM → 最终回复发送
//! ```
//!
//! ### 关键设计决策
//! 1. **单进程架构** — 旧版 daemon+worker 分离模式已废弃,统一用 tokio task
//! 2. **公平队列** — MessageQueue 按用户轮转,防止一个用户刷屏饿死其他人
//! 3. **可选数据库** — PostgreSQL 不可用时回退到文件存储
//! 4. **两层元工具** — LLM 通过 query_capabilities / call_capability 间接调用工具
mod channel; mod channel;
mod cli; mod cli;
mod context; mod context;
+11 -7
View File
@@ -1,14 +1,18 @@
//! ## 多渠道消息队列 MessageQueue //! ## 多渠道公平轮转消息队列 (MessageQueue)
//! //!
//! # 设计目标 //! ### 设计目标
//! //!
//! 多用户场景下保证公平性 //! 多用户场景下保证**公平性**:如果一个用户连续发了很多消息,不会让其他用户饿死。
//! - A 发了 3 条消息、B 发了 1 条 → 处理顺序 A1, B1, A2, A3 //! 出队顺序是**轮转(round-robin**的:
//! - 不会因为 A 消息多而饿死 B //! - A 发了 3 条、B 发了 1 条 → 处理顺序 A1, B1, A2, A3
//! //!
//! 每个消息携带 user_id + correlation_id,保证回复能追溯到正确的用户。 //! ### 消息关联追踪
//! //!
//! # 架构位置 //! 每条消息有一个 `correlation_id`Uuid),用来追踪一次用户请求的完整生命周期:
//! 用户消息 → LLM 处理 → 工具调用 → 工具结果回喂 → 最终回复发送。
//! 这样即使多用户并发,回复也不会串。
//!
//! ### 架构位置
//! //!
//! ```text //! ```text
//! receive_messages → enqueue PipelineMessage //! receive_messages → enqueue PipelineMessage
+10 -3
View File
@@ -1,9 +1,16 @@
//! QueueRunner — 消息队列运行时 //! ## QueueRunner — 消息队列运行时(路由调度器)
//! //!
//! 从公平轮转 MessageQueue 中出队消息,按 MessageKind 路由到对应的 //! 从公平轮转 `MessageQueue` 中出队消息,然后`MessageKind` 路由到对应的
//! tokio mpsc 通道,由注册的消费者 task 处理。 //! tokio mpsc 通道,由注册的消费者 task 处理。
//! //!
//! # 架构 //! ### 角色定位
//!
//! QueueRunner 是整个消息管线的**调度核心**:
//! 1. 持有 `MessageQueue`(公平轮转)+ 3 个 mpsc 通道
//! 2. 在 `run()` 循环中不断出队 → 按类型路由 → 投递到正确的消费者
//! 3. 支持优雅关闭(AtomicBool 信号)
//!
//! ### 架构
//! //!
//! ```text //! ```text
//! ┌──────────────┐ //! ┌──────────────┐
+15 -1
View File
@@ -3,7 +3,21 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::interval; use tokio::time::interval;
/// 定时任务调度器 —— 轮询 PostgreSQL 中到期的 scheduled_tasks /// ## 定时任务调度器
///
/// 轮询 PostgreSQL 中到期的 `scheduled_tasks` 表,执行任务后将结果通知用户。
///
/// ### 调度流程
/// 1. 每 5 秒检查一次是否有到期任务(`next_run_at <= NOW()`
/// 2. 使用 `FOR UPDATE SKIP LOCKED` 防止多个实例重复执行
/// 3. 执行任务 shell 命令(120 秒超时)
/// 4. 更新 `next_run_at`(当前时间 + interval_seconds
/// 5. 通过回调函数通知用户结果
///
/// ### 安全保障
/// * `SKIP LOCKED` — 多实例部署时不会抢同一任务
/// * `kill_on_drop` — 子进程在超时时会被杀掉
/// * 120 秒超时 — 防止 shell 命令无限执行
pub struct Scheduler { pub struct Scheduler {
pool: Option<Arc<PgPool>>, pool: Option<Arc<PgPool>>,
} }
+21 -1
View File
@@ -1,3 +1,14 @@
//! ## 文件状态管理(PostgreSQL 不可用时的回退方案)
//!
//! ### 设计意图
//! 在数据库不可用时,使用本地 JSON 文件持久化认证和运行时状态。
//! 当数据库可用时,数据优先存数据库,并从文件迁移到数据库后清理文件。
//!
//! ### 存储位置
//! 默认 `~/.ias/data/weixin-ilink/`
//! * `auth.json` — 认证信息(token, account_id, base_url
//! * `runtime.json` — 运行时状态(get_updates_buf
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
@@ -15,7 +26,16 @@ pub struct RuntimeState {
pub get_updates_buf: String, pub get_updates_buf: String,
} }
/// 状态管理器 /// ## 状态管理器 —— 基于文件的状态持久化
///
/// 在数据库不可用时代替 DB 存储认证和运行时状态。
///
/// ### 文件格式
/// * `auth.json` — `AuthState { token, account_id, base_url }`
/// * `runtime.json` — `RuntimeState { get_updates_buf }`
///
/// ### 安全
/// Unix 系统上 auth.json 的权限被设置为 600(仅所有者可读写)
pub struct StateManager { pub struct StateManager {
state_dir: PathBuf, state_dir: PathBuf,
} }
+22 -2
View File
@@ -6,7 +6,11 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use tokio::sync::{Mutex, oneshot}; use tokio::sync::{Mutex, oneshot};
/// 审批决策 /// ## 审批决策
///
/// * `Approved` — 用户输入了正确的确认码
/// * `Rejected` — 用户拒绝了(输入 0 或"取消"
/// * `Expired` — 超时未确认(5 分钟有效)
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum ApprovalDecision { pub enum ApprovalDecision {
Approved, Approved,
@@ -24,7 +28,23 @@ struct PendingEntry {
sender: oneshot::Sender<ApprovalDecision>, sender: oneshot::Sender<ApprovalDecision>,
} }
/// 审批管理器 /// ## 审批管理器
///
/// 管理高风险工具的用户确认流程。
///
/// ### 流程
/// 1. `create(user_id, skill_name)` → 生成 6 位随机确认码,哈希后存储
/// 2. 向用户发送"⚠️ 操作确认\n\n技能:{name}\n确认码:{code}"
/// 3. 用户在微信聊天中输入确认码
/// 4. `handle_reply(user_id, reply)` → 验证确认码哈希
/// - 匹配 → Approved
/// - 不匹配 → 减少尝试次数(最多 3 次)
/// - 输入 0 或"取消" → Rejected
/// - 超时 → Expired(每 60 秒清理一次)
///
/// ### 存储
/// * 内存 — `HashMap<user_id, Vec<PendingEntry>>`(快速访问)
/// * 数据库 — `pending_approvals` 表(持久化审计)
pub struct ApprovalManager { pub struct ApprovalManager {
pool: Option<Arc<PgPool>>, pool: Option<Arc<PgPool>>,
/// user_id → 该用户的待审批队列 /// user_id → 该用户的待审批队列
+16 -1
View File
@@ -1,6 +1,21 @@
use crate::tools::types::{RiskLevel, SkillResult}; use crate::tools::types::{RiskLevel, SkillResult};
/// 内置工具注册表:名称 → 执行、spec 列表、风险判断 /// ## 内置工具注册表
///
/// 连接 LLM 的元工具调用和实际 Rust 实现的工具函数。
///
/// ### 职责
/// 1. `execute(name, args)` — 按名称路由到对应的 builtin 工具执行
/// 2. `specs()` — 返回所有内置工具的 SkillSpec(给元工具 `query_capabilities` 使用)
/// 3. `is_high_risk(name)` — 判断工具是否需要审批
///
/// ### 当前内置工具
/// * `get_current_datetime` — 获取当前北京时间
/// * `manage_memos` — 管理备忘录(CRUD
/// * `query_weather` — 查询天气(和风天气 API)
/// * `web_search` — 联网搜索(Tavily API
/// * `fetch_page` — 抓取网页内容
/// * `amap_*` — 高德地图系列(POI搜索、地理编码、路径规划等)
pub struct BuiltinRegistry; pub struct BuiltinRegistry;
impl BuiltinRegistry { impl BuiltinRegistry {
+34 -5
View File
@@ -1,16 +1,30 @@
//! JSON 工具定义 — 像 curl + jq 一样描述一个工具 //! ## JSON 工具定义格式
//! //!
//! 三要素:URL → 取节点 → 选字段 //! 本模块定义了一种"声明式工具"格式 —— 只需要写一个 JSON 文件就能注册一个新工具,
//! 无需改 Rust 代码、重新编译。
//!
//! ### 设计理念:像 curl + jq 一样描述工具
//! //!
//! ```json //! ```json
//! { //! {
//! "name": "stocks", //! "name": "stocks",
//! "description": "股票行情", //! "description": "股票行情查询",
//! "url": "https://api.example.com/stocks", //! "url": "https://api.example.com/stocks/{{code}}",
//! "method": "GET",
//! "headers": { "Authorization": "${env:API_KEY}" },
//! "extract": "data.items", //! "extract": "data.items",
//! "fields": ["name", "price"] //! "fields": ["name", "price"]
//! } //! }
//! ``` //! ```
//!
//! ### 三要素
//! 1. **URL** — API 端点模板(支持 `{{var}}` 和 `${param:name:type}` 传参)
//! 2. **取节点** — `extract` 字段指定 JSON 点路径(类似 jq)
//! 3. **选字段** — `fields` 字段只保留需要的字段
//!
//! ### 参数推导
//! 自动从 URL/Body/Headers 中推导参数定义(无需手动写 parameters 字段),
//! 也支持显式 `parameters` 声明以获得更精确的类型描述。
use std::collections::HashMap; use std::collections::HashMap;
@@ -60,7 +74,22 @@ impl<'de> serde::Deserialize<'de> for HttpMethod {
} }
} }
/// 一个 JSON 工具定义 /// ## JSON 工具定义ToolDef
///
/// 描述一个通过 HTTP API 调用的外部工具。
///
/// ### 字段说明
/// * `name` — 工具名称,用作 `call_capability` 的 name
/// * `description` — 工具描述(给 LLM 看的)
/// * `url` — URL 模板,支持 `{{var}}` 和 `${param:name:type}` 两种传参
/// * `method` — HTTP 方法(GET/POST/PUT/DELETE),默认 GET
/// * `headers` — HTTP 请求头,值可包含 `${env:KEY}`(环境变量)、
/// `${cmd:command}`(命令输出)、`${param:name:type}`(参数值)
/// * `body` — POST/PUT 的 JSON body 模板,同样支持变量替换
/// * `parameters` — 显式参数声明(可选,未声明时自动推导)
/// * `extract` — JSON 提取路径,如 "data.items"
/// * `fields` — 筛选要保留的字段
/// * `timeout` — 超时秒数,默认 30
#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)]
pub struct ToolDef { pub struct ToolDef {
/// 工具名称,用作 call_capability 的 name /// 工具名称,用作 call_capability 的 name
+36 -5
View File
@@ -1,14 +1,45 @@
//! ## 工具系统 —— 元工具 + 分离式内置工具
//!
//! 本模块是 iAs 的"工具系统",让 LLM 能够调用外部功能。
//!
//! ### 架构设计(两层元工具)
//!
//! LLM 不直接感知每个具体工具,而是通过两个**元工具**间接调用:
//!
//! 1. **`query_capabilities`** — 列出所有可用工具的名称、描述、参数格式
//! 2. **`call_capability`** — 按名称调用工具,参数通过 JSON 传递
//!
//! 这样做的优势:新增工具时只需要在服务端注册,不需要修改 LLM 的 tool definitions。
//!
//! ### 工具类型
//! * **内置工具** (builtins/) — Rust 代码实现的工具(天气、搜索、高德地图等)
//! * **JSON 工具** (definitions.rs) — 从 `~/.ias/tools/*.json` 加载,由 ToolDef 定义
//! * **上下文工具** — read_memories, write_memory, read_summaries(直接在 daemon 中处理)
//!
//! ### 工具执行流程
//! ```text
//! LLM 请求 call_capability("query_weather", {"location":"北京"})
//! → daemon 解析出 target_name="query_weather",提取参数
//! → 高风险?→ 走 ApprovalManager 审批流
//! → 低风险?→ BuiltinRegistry::execute() 或 SubprocessRunner::execute()
//! → 结果返回 LLM 继续对话
pub mod approval; pub mod approval;
pub mod builtin; pub mod builtin;
pub mod builtins; pub mod builtins;
pub mod definitions;
pub mod subprocess;
pub mod types; pub mod types;
/// 从 call_capability 的 `{name, prompt}` JSON 中提取目标工具的真实参数 /// ##`call_capability` 的 `{name, prompt}` JSON 中提取目标工具的真实参数
/// ///
/// 逻辑: /// `call_capability` 的参数格式为 `{name: "工具名", prompt: "参数JSON字符串"}`。
/// - 如果 prompt 是 JSON 字符串 → parse 后合并 /// 这个函数负责从这种格式中提取出工具真正的参数。
/// - 如果 prompt 是 JSON 对象 → 直接合并 ///
/// - 顶层字段(非 name/prompt)也合并进去 /// ### 提取逻辑
/// 1. 如果 `prompt` 是 JSON 字符串 → parse 后合并
/// 2. 如果 `prompt` 是 JSON 对象 → 直接合并
/// 3. 顶层字段(非 name/prompt)也合并进去(支持直接传参)
pub fn unpack_call_params(cp: &serde_json::Value) -> serde_json::Value { pub fn unpack_call_params(cp: &serde_json::Value) -> serde_json::Value {
let mut params = serde_json::Map::new(); let mut params = serde_json::Map::new();
+33 -10
View File
@@ -1,14 +1,30 @@
//! JSON 工具子进程执行器 //! ## JSON 工具子进程执行器
//! //!
//! 从 ~/.ias/tools/*.json 加载 ToolDef,运行时通过 HTTP 执行。 //! ### 设计理念:"放 JSON 即用"
//! 无需编译,放 JSON 即用。
//! //!
//! 执行流程: //! 从 `~/.ias/tools/*.json` 加载 `ToolDef`,运行时通过 HTTP 执行。
//! 模板替换 → HTTP 请求 → JSON 解析 //! 不需要修改 Rust 代码、不需要重新编译,放一个 JSON 文件就能注册新工具。
//! ├─ 失败 → 返回纯文本 //!
//! ├─ 提取 → json_select() //! ### 执行流程
//! ├─ 筛选 → filter_fields() //!
//! └─ 格式化输出 //! ```text
//! 1. 模板替换
//! {{var}} → args.varURL 编码)
//! ${param:name:type} → args.name
//! ${env:KEY} → 环境变量
//! ${cmd:command} → shell 命令输出(有缓存,TTL=300s
//!
//! 2. HTTP 请求(GET/POST/PUT/DELETE
//!
//! 3. 返回处理
//! ├─ 不是 JSON → 直接返回纯文本
//! ├─ 有 extract → json_select() 按点路径取子节点
//! ├─ 有 fields → filter_fields() 只保留指定字段
//! └─ 格式化输出 → 可读文本
//! ```
//!
//! ### 缓存
//! `${cmd:...}` 的结果会被缓存 300 秒,适合 JWT token 等有时效的值。
use crate::tools::definitions::{HttpMethod, ToolDef}; use crate::tools::definitions::{HttpMethod, ToolDef};
use crate::tools::types::{SkillResult, SkillSpec}; use crate::tools::types::{SkillResult, SkillSpec};
@@ -102,7 +118,14 @@ pub fn runner() -> &'static SubprocessRunner {
&RUNNER &RUNNER
} }
/// 子进程执行器 /// ## 子进程执行器 —— 管理所有从 JSON 文件加载的工具
///
/// ### 全局单例
/// 通过 `LazyLock` 在首次访问时初始化,从 `~/ias/tools/*.json` 加载全部工具。
///
/// ### 运行时热加载
/// `reload()` 方法可以重新扫描工具目录(目前未在运行时调用,
/// 但为后续热更新保留了接口)。
pub struct SubprocessRunner { pub struct SubprocessRunner {
tools: RwLock<HashMap<String, ToolDef>>, tools: RwLock<HashMap<String, ToolDef>>,
} }
+23 -4
View File
@@ -3,7 +3,10 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
// ─── 风险等级 ─── /// ## 风险等级 —— 工具的安全级别
///
/// * `Low` — 低风险,直接执行(如天气查询、日期时间)
/// * `High` — 高风险,需要用户输入确认码审批(如写备忘录、发消息)
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
@@ -14,7 +17,14 @@ pub enum RiskLevel {
impl RiskLevel {} impl RiskLevel {}
// ─── 工具元数据 ─── /// ## 工具元数据(SkillSpec)—— 工具的自描述信息
///
/// 用于给 LLM 展示"有什么工具可用、怎么用"。
/// * `name` — 工具名称
/// * `description` — 工具描述
/// * `risk_level` — 风险等级(Low/High
/// * `parameters` — JSON Schema 格式的参数定义
/// * `timeout_secs` — 执行超时时间
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct SkillSpec { pub struct SkillSpec {
@@ -36,7 +46,11 @@ fn default_timeout() -> u64 {
30 30
} }
// ─── 工具执行结果 ─── /// ## 工具执行结果
///
/// * `success` — 执行是否成功
/// * `output` — 可读的输出文本(将返回给 LLM)
/// * `data` — 结构化数据(可选),用于后续处理
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct SkillResult { pub struct SkillResult {
@@ -89,7 +103,12 @@ impl SkillResult {
} }
} }
// ─── 执行上下文 ─── /// ## 执行上下文 —— 工具执行时需要的环境信息
///
/// 当工具在 daemon 上下文中执行时,携带以下信息:
/// * `user_id` — 触发工具的用户
/// * `approval_manager` — 审批管理器(高风险工具需要)
/// * `send_wechat` — 微信发送回调(用于向用户发送审批提示)
pub type WechatSender = Arc< pub type WechatSender = Arc<
dyn Fn(&str, &str) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>> + Send + Sync, dyn Fn(&str, &str) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>> + Send + Sync,
+38 -3
View File
@@ -6,7 +6,24 @@ use std::time::Duration;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
/// 微信 iLink Bot API 客户端 /// ## 微信 iLink Bot API 客户端
///
/// ### 职责
/// 封装了与微信 iLink Bot API 的所有通信,包括:
/// * 扫码登录流程(获取二维码、轮询状态)
/// * 消息接收(长轮询 getUpdates
/// * 消息发送(sendMessage
/// * 生命周期管理(notifyStart/notifyStop
///
/// ### 线程安全
/// `#[derive(Clone)]` + 内部 `Arc<Mutex<>>` 保证所有方法可以在 tokio task 间共享。
///
/// ### API 端点
/// * 二维码: `{base_url}/ilink/bot/get_bot_qrcode`
/// * 登录状态: `{base_url}/ilink/bot/get_qrcode_status`
/// * 收消息: `{base_url}/ilink/bot/getupdates`
/// * 发消息: `{base_url}/ilink/bot/sendmessage`
/// * 注册监听: `{base_url}/ilink/bot/msg/notifystart`
#[derive(Clone)] #[derive(Clone)]
pub struct WeChatClient { pub struct WeChatClient {
http: HttpClient, http: HttpClient,
@@ -46,7 +63,10 @@ impl WeChatClient {
// ─── 公共方法 ─── // ─── 公共方法 ───
/// 获取登录二维码链接 /// ## 获取登录二维码链接
///
/// 调用 iLink API `/ilink/bot/get_bot_qrcode` 获取二维码。
/// 二维码内容是一个 URL,浏览器打开后显示二维码图片。
pub async fn get_qrcode(&self) -> Result<(String, String), String> { pub async fn get_qrcode(&self) -> Result<(String, String), String> {
let url = format!( let url = format!(
"{}/ilink/bot/get_bot_qrcode?bot_type={}", "{}/ilink/bot/get_bot_qrcode?bot_type={}",
@@ -68,7 +88,16 @@ impl WeChatClient {
Ok((resp.qrcode, resp.qrcode_img_content)) Ok((resp.qrcode, resp.qrcode_img_content))
} }
/// 轮询扫码状态 /// ## 轮询扫码状态(带 35 秒超时的长轮询)
///
/// 调用 iLink API 检查用户是否已扫码。
/// * `wait` — 继续等待
/// * `scaned` — 已扫码,等待确认
/// * `scaned_but_redirect` — IDC 重定向(切换到 redirect_host
/// * `confirmed` — 登录成功
/// * `expired` — 二维码过期
///
/// 网络错误不会导致退出,而是返回 `wait` 让调用者继续轮询。
pub async fn poll_qr_status( pub async fn poll_qr_status(
&self, &self,
qrcode: &str, qrcode: &str,
@@ -358,6 +387,12 @@ impl WeChatClient {
// ─── 内部方法 ─── // ─── 内部方法 ───
/// ## 公共请求头
///
/// 每个请求都携带以下头信息:
/// * `Content-Type: application/json`
/// * `iLink-App-ClientVersion` — 客户端版本号编码
/// * `X-WECHAT-UIN` — 随机 uint32 → base64 编码的会话标识
fn common_headers() -> reqwest::header::HeaderMap { fn common_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
headers.insert( headers.insert(
+18 -2
View File
@@ -116,7 +116,12 @@ pub struct RefMessage {
pub title: Option<String>, pub title: Option<String>,
} }
/// 消息条目(支持 text/image/voice/file/video /// ## 消息条目 —— 消息的具体内容
///
/// 支持 5 种消息类型:text(1), image(2), voice(3), file(4), video(5)。
/// 每种类型有对应的 `*_item` 字段,其他字段保持 None。
///
/// `text()` 便捷方法用于快速构造文本消息(发送消息时使用)。
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageItem { pub struct MessageItem {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")] #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
@@ -183,7 +188,18 @@ impl Default for MessageItem {
} }
} }
// ─── 微信消息 ─── /// ## 微信消息结构
///
/// 对应 iLink API 返回的单个消息。所有字段都是 Option 的,
/// 因为不同消息类型携带不同字段。
///
/// ### 关键字段
/// * `msg_type` — 消息类型:1=用户消息(TYPE_USER), 2=机器人消息(TYPE_BOT)
/// * `from_user_id` — 发送者微信 ID
/// * `to_user_id` — 接收者微信 ID
/// * `item_list` — 消息内容列表(支持 text/image/voice/file/video
/// * `context_token` — 微信上下文令牌(用于多轮对话的上下文恢复)
/// * `group_id` — 群聊 ID(群消息时非空)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeixinMessage { pub struct WeixinMessage {
+10 -4
View File
@@ -1,9 +1,15 @@
#![deprecated = "Worker 进程模式已废弃,请使用 daemon 模式(统一 tokio task 架构)"] //! ## Worker —— 无状态执行进程(已废弃)
//! Worker — 无状态执行进程(已废弃)
//! //!
//! ### 旧架构说明
//! 在旧架构中,每条消息会 spawn 一个独立的 Worker 进程,
//! Worker 通过 UDS 连接到 daemon,处理完一条消息后退出。
//!
//! 优势:Worker 代码热更新(编译后下一条消息自动用新版本)
//! 劣势:进程启动开销、IPC 序列化开销、状态管理复杂
//!
//! ### 新架构
//! 现在改为 daemon 内的 LLM/Tool/Send 三个消费者 tokio task。
//! 此模块保留供编译参考,不再被 daemon 使用。 //! 此模块保留供编译参考,不再被 daemon 使用。
//! 新架构使用 daemon 内的 LLM/Tool/Send 三个消费者 tokio task。
//! 1. 连接 daemon 的 Unix Domain Socket //! 1. 连接 daemon 的 Unix Domain Socket
//! 2. 读取 TaskFrame (用户消息 + 上下文 + 环境变量) //! 2. 读取 TaskFrame (用户消息 + 上下文 + 环境变量)
//! 3. 注入环境变量,构建 Conversation,执行 LLM 对话 + 工具调用 //! 3. 注入环境变量,构建 Conversation,执行 LLM 对话 + 工具调用