docs: 统一 README,删除冗余文档;refactor: 消除全部 15 个 warning

- 新建 README.md,整合项目架构、命令、配置、数据流
- 删除 6 个冗余/过时 md 文档
- save_summary_to_db 委托 db::models::save_summary,避免 SQL 重复
- #[allow(dead_code)] 标注 serde 反序列化字段和预留 API
- 移除未使用的 re-export (Summarizer, Message, Role, StreamChunk)
- 修复 README 中 manage_memos 风险等级和缺失的 AMAP_KEY 配置
This commit is contained in:
2026-06-03 17:14:01 +08:00
parent b2fe054eaf
commit 69ba8887ff
13 changed files with 267 additions and 1301 deletions
-193
View File
@@ -1,193 +0,0 @@
# iAs 上下文管理系统设计 v2
## 核心理念
1. **长期记忆不固定注入** — 通过工具让 LLM 按需读写
2. **摘要不入上下文** — 除非因上下文空间不足被迫压缩
3. **摘要总结点** — 记录压缩位置,之后的消息始终完整保留
4. **12 小时空闲触发** — 超过 12h 自动压缩,但不插入会话
5. **Token Budget 驱动** — 达到上限才压缩,保持消息完整度优先
---
## 状态模型
```
ChatSession
├── checkpoint: usize = 0 ← 摘要总结点(消息列表中的位置)
├── messages: Vec<Message> ← 全部消息(checkpoint 之前的是历史)
├── summaries: Vec<SummaryEntry> ← 所有摘要
│ ├── pos: usize ← 对应消息列表中的位置
│ ├── text: String ← 摘要内容
│ └── reason: "overflow"|"timeout" ← 生成原因
└── last_user_at: Option<DateTime> ← 上一条用户消息时间
```
### 消息列表视图(LLM 实际看到的内容)
```
TOOL: read_memories / write_memory / read_summaries
↑ LLM 按需调用,不自动注入
VIEW: [system_prompt]
[overflow_summary] ← 仅当因 context 满产生时注入
[messages from checkpoint..] ← 始终完整保留
```
---
## 核心类型
```rust
pub struct SummaryEntry {
pub checkpoint: usize, // 对应 messages 中的位置
pub text: String,
pub reason: SummaryReason,
}
pub enum SummaryReason {
Overflow, // 上下文满了自动压缩
Timeout, // 超过 12 小时空闲
}
pub struct ChatSession {
checkpoint: usize,
messages: Vec<Message>,
summaries: Vec<SummaryEntry>,
last_user_at: Option<DateTime<Utc>>,
token_budget: u32, // 默认 32000
}
```
---
## 核心流程
```
用户发消息 "合肥天气"
1. 距离检查
├── last_user_at 超过 12h
│ └── YES → summarize_all(reason=timeout)
│ ├── 生成摘要存入 summaries(不插入会话)
│ └── checkpoint = messages.len()
2. 构建 LLM 请求
├── system_prompt
├── 【如果有 overflow_summary 且 checkpoint > 0】
│ └── { role: "system", content: "历史摘要:..." }
└── messages[checkpoint..]
3. Token 估算
├── 在 budget 内 → 直接发送
└── 超出 budget → summarize_all(reason=overflow)
├── 生成摘要存入 summaries
├── checkpoint = messages.len()
├── 摘要作为 system message 注入本次请求
└── 重新构建请求(现在只有摘要 + 当前消息)
4. 发送给 LLM
├── LLM 回复(可能包含工具调用)
│ ├── read_memories → 返回长期记忆
│ ├── write_memory → 保存到长期记忆
│ └── read_summaries → 返回 summaries 列表
└── LLM 最终回复 → record_turn(user_msg, assistant_msg)
├── messages.push(user_msg, assistant_msg)
└── last_user_at = now
```
---
## 摘要触发条件对比
| 原因 | 触发条件 | 是否插入会话 | 用途 |
|------|---------|------------|------|
| **Overflow** | token 超 budget | ✅ 自动插入 | 保持上下文不超限 |
| **Timeout** | 距上条消息 >12h | ❌ 不插入(工具可读) | 跨天会话,LLM 自行决定是否查看 |
---
## 长期记忆工具(LLM 按需调用)
```json
{
"name": "read_memories",
"description": "读取用户的长期记忆(跨会话持久保存的个人信息)",
"parameters": { "type": "object", "properties": {} }
}
{
"name": "write_memory",
"description": "写入一条用户的长期记忆",
"parameters": {
"type": "object",
"properties": {
"content": { "type": "string", "description": "记忆内容" }
},
"required": ["content"]
}
}
{
"name": "read_summaries",
"description": "读取历史会话摘要(包括空闲超时产生的摘要)",
"parameters": {
"type": "object",
"properties": {}
}
}
```
这些工具注册为系统 skills(只对 `chat_with_tools` 启用,不暴露给用户)。
## 数据库存储
```sql
-- 长期记忆(跨会话持久)
CREATE TABLE user_memories (
user_id TEXT PRIMARY KEY,
content JSONB NOT NULL DEFAULT '[]',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
摘要和消息会话不持久化(会话消息保存到 `chat_records` 表,但摘要只存在于运行时的 `ChatSession`)。
## 与 iPet 的区别
| 特性 | iPet | iAs v2 |
|------|------|--------|
| 长期记忆 | 自动注入每个请求 | 通过工具按需读写 |
| 滚动摘要 | 自动注入每个请求 | 不注入,工具可读 |
| 会话摘要 | 自动注入 | 不注入,overflow 时例外 |
| 摘要触发 | 定期 + 按消息数 | Token budget + 12h 空闲 |
| 历史完整保留 | ❌ 始终压缩 | ✅ checkpoint 后完整保留 |
## Token 估算
```rust
pub fn estimate_tokens(text: &str) -> u32 {
let cjk = text.chars().filter(|c| c >= '\u{4e00}' && c <= '\u{9fff}').count() as u32;
let other = text.chars().count() as u32 - cjk;
cjk * 15 / 10 + other / 4 + 8 // +8 消息 overhead
}
```
## 实施步骤
| 步骤 | 内容 |
|------|------|
| 1 | ChatSession + SummaryEntry 核心类型 |
| 2 | build_context() — 构建 LLM 消息数组 |
| 3 | record_turn() — 记录对话轮次 |
| 4 | summarize_all() — 摘要生成 + checkpoint 更新 |
| 5 | token_estimate() — Token 估算 |
| 6 | 长期记忆工具(read_memories / write_memory|
| 7 | 摘要读取工具(read_summaries|
| 8 | 集成到 Conversation + main.rs |
+9 -16
View File
@@ -177,23 +177,16 @@ impl ChatSession {
}
}
/// 保存摘要到数据库
/// 保存摘要到数据库(委托给 db::models::save_summary
pub async fn save_summary_to_db(&self, text: &str, reason: &str, message_count: i32) {
if let Some(pool) = &self.db_pool {
let uid = if self.current_user_id.is_empty() {
if self.user_id.is_empty() { "default" } else { &self.user_id }
} else {
&self.current_user_id
};
let _ = sqlx::query(
"INSERT INTO session_summaries (user_id, summary_text, reason, message_count) VALUES ($1, $2, $3, $4)",
)
.bind(uid)
.bind(text)
.bind(reason)
.bind(message_count)
.execute(pool.as_ref())
.await;
let Some(pool) = &self.db_pool else { return };
let uid = if self.current_user_id.is_empty() {
if self.user_id.is_empty() { "default" } else { &self.user_id }
} else {
&self.current_user_id
};
if let Err(e) = crate::db::models::save_summary(pool, uid, text, reason, message_count).await {
tracing::warn!("保存摘要到数据库失败: {}", e);
}
}
}
+2 -2
View File
@@ -3,5 +3,5 @@ pub mod deepseek;
pub mod provider;
pub mod types;
pub use conversation::{Conversation, Summarizer, ToolExecutor, DEFAULT_SYSTEM_PROMPT};
pub use types::{ConversationConfig, Message, Role, StreamChunk, Usage};
pub use conversation::{Conversation, ToolExecutor, DEFAULT_SYSTEM_PROMPT};
pub use types::{ConversationConfig, Usage};
+2
View File
@@ -34,6 +34,7 @@ pub fn create_provider(_config: &ConversationConfig) -> Result<BoxedProvider, St
// ─── 内部:SSE 解析工具 ───
/// 流式块解析结果
#[allow(dead_code)]
pub(crate) enum ParsedChunk {
Text(String),
Reasoning(String),
@@ -65,6 +66,7 @@ pub(crate) fn parse_chat_chunk(line: &str) -> Option<ParsedChunk> {
#[serde(default)]
id: Option<String>,
#[serde(rename = "type", default)]
#[allow(dead_code)]
call_type: Option<String>,
#[serde(default)]
function: Option<ToolFunctionDelta>,
+2
View File
@@ -67,6 +67,7 @@ pub struct ToolFunctionCall {
// ─── 流式响应块 ───
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum StreamChunk {
/// 文本片段 delta
Text(String),
@@ -75,6 +76,7 @@ pub enum StreamChunk {
/// 完成(携带完整文本,以及可选的工具调用)
Done {
text: String,
#[allow(dead_code)]
reasoning: Option<String>,
tool_calls: Option<Vec<ToolCall>>,
usage: Option<Usage>,
-328
View File
@@ -1,328 +0,0 @@
# iAs 工具系统设计
## 核心理念
**一切皆 Skill。【todo:向内置工具转变】**
没有"内置工具"和"外部技能"的区别——所有工具都是 SKILL.md,区别仅在来源:
- **系统 skills** — `resources/skills/` 目录,随 iAs 分发,只读
- **用户 skills** — `skills/` 项目目录,用户自己添加
加载时自动合并,同名时用户 skills 覆盖系统 skills。
---
## 架构
```
┌──────────────────────────────────────────────────────────┐
│ LLM │
│ generate_reply() ←→ tool_choice → tool_execute(result) │
└─────────────────────┬────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ SkillRegistry │
│ │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ 系统 Skills │ │ 用户 Skills │ │
│ │ resources/skills/ │ │ skills/ │ │
│ │ │ │ │ │
│ │ • datetime │ │ • query_qweather/ │ │
│ │ • weather │ │ • search_tavily/ │ │
│ │ • search_web │ │ • start_pc/ │ │
│ │ • email │ │ • ... │ │
│ │ • shell_exec │ │ │ │
│ │ • schedule │ │ │ │
│ └────────┬──────────┘ └────────────┬──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Approval Gate │ │
│ │ risk == High → 确认码审批 / Low → 自动执行 │ │
│ │ 审批过程对 LLM 完全透明 │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 核心类型
```rust
/// 风险等级(仅两级)
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RiskLevel {
Low, // 只读/安全,自动执行
High, // 写操作/有风险,需微信确认码审批
}
/// 技能来源
#[derive(Debug, Clone, PartialEq)]
pub enum SkillSource {
System, // resources/skills/
User, // skills/
}
/// 技能元数据(从 SKILL.md 解析)
#[derive(Debug, Clone, Deserialize)]
pub struct SkillSpec {
pub name: String,
pub description: String,
pub risk_level: RiskLevel,
pub parameters: serde_json::Value, // JSON Schema
pub timeout_secs: u64,
}
/// 技能(加载到内存后)
pub struct Skill {
pub spec: SkillSpec,
pub source: SkillSource,
pub dir: PathBuf,
pub execute_command: String,
}
```
---
## SKILL.md 格式
每个技能是一个目录,包含 `SKILL.md` 文件:
```markdown
# Get Current Datetime
获取当前日期时间(北京时间 UTC+8)。
## Risk Level
Low
## Parameters
\`\`\`json
{
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式: full | date | time",
"enum": ["full", "date", "time"],
"default": "full"
}
}
}
\`\`\`
## Execute
\`\`\`bash
#!/usr/bin/env bash
scripts/datetime.sh
\`\`\`
```
---
## 内置 Skills 一览
随 iAs 分发的系统 skills
| Skill | 风险 | 说明 |
|-------|------|------|
| `get_current_datetime` | Low | 获取当前时间(UTC+8 |
| `query_weather` | Low | 和风天气查询 |
| `search_web` | Low | Tavily 网络搜索 |
| `email` | High | 收发邮件 |
| `execute_shell` | High | 执行 shell 命令 |
| `list_scheduled_tasks` | Low | 列出定时任务 |
| `manage_scheduled_task` | High | 增删改定时任务 |
---
## SkillRegistry
```rust
pub struct SkillRegistry {
skills: HashMap<String, Arc<Skill>>,
}
impl SkillRegistry {
pub async fn load(system_dir: &str, user_dir: &str) -> Result<Self, LoadError>;
pub fn list_specs(&self) -> Vec<&SkillSpec>;
pub fn list_specs_by_risk(&self, level: RiskLevel) -> Vec<&SkillSpec>;
pub fn get(&self, name: &str) -> Option<&Skill>;
/// 执行技能(含审批流程)
pub async fn execute(
&self,
name: &str,
params: serde_json::Value,
ctx: &ExecutionContext,
) -> Result<SkillResult, SkillError>;
}
pub struct ExecutionContext {
pub user_id: String,
pub db: Option<Arc<Database>>,
/// 发送微信消息的回调
pub send_wechat: Option<Box<dyn Fn(&str, &str) -> Result<(), String> + Send + Sync>>,
/// 等待用户回复的回调(返回用户消息文本)
pub wait_for_reply: Option<Box<dyn Fn(&str, Duration) -> Result<Option<String>, String> + Send + Sync>>,
}
```
---
## 审批流程(透明模式)
**核心原则:审批过程对 LLM 完全不可见。** LLM 只看到最终结果。
```
LLM 调用 High 风险技能
SkillRegistry::execute()
├── 查找 Skill
├── risk_level == High
└── YES ──────────────────────────────────────────┐
│ │
▼ │
┌──────────────────────────────────────────┐ │
│ 等待阶段(LLM 不感知) │ │
│ │ │
│ ① 生成 6 位确认码(如 482731) │ │
│ ② SHA-256 哈希存入 pending_approvals │ │
│ ③ 通过微信发送确认消息给用户: │ │
│ │ │
│ ┌─────────────────────────────┐ │ │
│ │ ⚠️ 操作确认 │ │ │
│ │ │ │ │
│ │ 技能:send_email │ │ │
│ │ 参数:收件人 xxx@xxx.com │ │ │
│ │ │ │ │
│ │ 确认码:482731 │ │ │
│ │ 回复确认码以继续 │ │ │
│ │ 回复 0 或「取消」以取消 │ │ │
│ │ (5分钟内有效,最多3次尝试) │ │ │
│ └─────────────────────────────┘ │ │
│ │ │
│ ④ 等待用户回复 │ │
│ │ │ │
│ ├── 回复正确确认码 ──┐ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ ⑤ 执行技能 │ │ │
│ │ │ ⑥ LLM 看到 │ │ │
│ │ │ 执行结果 │ │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ ├── 回复错误确认码 │ │
│ │ → "确认码错误,还剩 N 次" │ │
│ │ → 重试(最多 3 次) │ │
│ │ → 3 次都错 │ │
│ │ → LLM 看到: │ │
│ │ "用户拒绝了你的调用: │ │
│ │ send_email" │ │
│ │ │ │
│ ├── 回复 0 / 「取消」 │ │
│ │ → LLM 看到: │ │
│ │ "用户拒绝了你的调用: │ │
│ │ send_email" │ │
│ │ │ │
│ └── 5 分钟超时 │ │
│ → LLM 看到: │ │
│ "用户没有确认操作: │ │
│ send_email" │ │
└──────────────────────────────────────────┘
```
### LLM 视角(两种可能)
```
✅ 通过:
LLM: 调用 send_email({to: "xxx", subject: "..."})
系统: { success: true, message: "邮件已发送" }
❌ 拒绝:
LLM: 调用 send_email({to: "xxx", subject: "..."})
系统: { success: false, error: "用户拒绝了你的调用:send_email" }
```
LLM 不知道确认码的存在,不知道审批过程。它只知道自己调用了工具,然后得到了结果或拒绝。
---
## 数据库表
```sql
-- 待审批的工具调用
CREATE TABLE pending_approvals (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL, -- 5分钟后过期
user_id TEXT NOT NULL,
skill_name TEXT NOT NULL,
params JSONB NOT NULL,
code_hash TEXT NOT NULL, -- 确认码 SHA-256(不存明文)
attempts_left INTEGER NOT NULL DEFAULT 3, -- 剩余尝试次数
status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | rejected | expired
result JSONB,
consumed_at TIMESTAMPTZ
);
```
---
## Skills 目录结构
```
resources/skills/ # 系统内置 Skills(随 iAs 分发)
├── get_current_datetime/
│ ├── SKILL.md
│ └── scripts/datetime.sh
├── query_weather/
│ ├── SKILL.md
│ └── scripts/weather.sh
├── search_web/
│ ├── SKILL.md
│ └── scripts/search.sh
├── email/
│ ├── SKILL.md
│ └── scripts/email.sh
├── execute_shell/
│ ├── SKILL.md
│ └── scripts/shell.sh
├── list_scheduled_tasks/
│ ├── SKILL.md
│ └── scripts/list.sh
└── manage_scheduled_task/
├── SKILL.md
└── scripts/manage.sh
skills/ # 用户 Skills(用户自行添加)
├── query_qweather/
│ ├── SKILL.md
│ └── scripts/query.sh
├── search_tavily/
│ ├── SKILL.md
│ └── scripts/search.sh
└── start_pc/
├── SKILL.md
└── scripts/wake.sh
```
---
## 实施步骤
| 步骤 | 内容 | 预估 |
|------|------|------|
| Step 1 | SkillSpec/Skill/SkillRegistry 核心类型 + 注册表 | 半天 |
| Step 2 | SKILL.md 解析器 + 目录扫描(system + user 合并) | 半天 |
| Step 3 | 技能执行器:子进程管理 + 参数 JSON 传递 + 结果解析 | 半天 |
| Step 4 | 系统 Skills 脚本实现(7 个 bash 脚本) | 1 天 |
| Step 5 | 审批系统:pending_approvals 表 + 确认码生成/验证 + 微信透明通知 | 1 天 |
| Step 6 | LLM function calling 集成 + 审计日志 | 1 天 |
+2
View File
@@ -76,6 +76,7 @@ struct SearchRequest {
// ─── 响应 ───
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SearchResponse {
query: String,
@@ -102,6 +103,7 @@ struct SearchResponse {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SearchResult {
title: String,
url: String,
+2
View File
@@ -25,6 +25,7 @@ pub struct SkillSpec {
#[serde(default)]
pub parameters: serde_json::Value,
#[serde(default = "default_timeout")]
#[allow(dead_code)]
pub timeout_secs: u64,
}
@@ -70,6 +71,7 @@ impl SkillResult {
}
}
#[allow(dead_code)]
pub fn expired(skill_name: &str) -> Self {
Self {
success: false,
+8
View File
@@ -145,9 +145,13 @@ pub struct MessageItem {
impl MessageItem {
pub const TYPE_TEXT: i32 = 1;
#[allow(dead_code)]
pub const TYPE_IMAGE: i32 = 2;
#[allow(dead_code)]
pub const TYPE_VOICE: i32 = 3;
#[allow(dead_code)]
pub const TYPE_FILE: i32 = 4;
#[allow(dead_code)]
pub const TYPE_VIDEO: i32 = 5;
pub fn text(text: &str) -> Self {
@@ -235,6 +239,7 @@ impl Default for WeixinMessage {
}
impl WeixinMessage {
#[allow(dead_code)]
pub const TYPE_NONE: i32 = 0;
pub const TYPE_USER: i32 = 1;
pub const TYPE_BOT: i32 = 2;
@@ -296,6 +301,7 @@ pub struct GetUpdatesResp {
#[serde(default)]
pub get_updates_buf: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(dead_code)]
pub longpolling_timeout_ms: Option<i64>,
}
@@ -320,11 +326,13 @@ pub struct LoginResult {
pub token: String,
pub account_id: String,
pub base_url: String,
#[allow(dead_code)]
pub user_id: String,
}
/// 收到的消息事件
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct MessageEvent {
pub from_user_id: String,
pub text: String,