AI Diary 重构计划
最后更新:2026-05-22 | 版本 0.1.1-dev
目录
1. 项目健康评估
概览
| 维度 | 状态 | 说明 |
|---|---|---|
| P0 阻塞级 Bug | ✅ 全部清除 | 输入框卡顿、429 误禁、重复归档、历史不可见 |
| P1 高优 Bug/优化 | ⚠️ 过半完成 | 会话恢复、Provider 重启用等 3 完成 + 4 部分 + 4 未/新增 |
| P2 体验改善 | ⚠️ 少量完成 | 侧边栏信息密度已优化;搜索、日志、连接复用待定 |
| P3 代码整洁 | ⚠️ 少数顺手修了 | dead code 等基本未动 |
| 测试覆盖 | ❌ 0% | 无任何测试 |
| 日志系统 | ❌ 无 | 异常被静默吞噬,排查困难 |
| 架构解耦 | ❌ 差 | 837 行 God Object,UI/业务/命令混在一起 |
功能缺失清单
以下核心功能尚未实现:
- 文本编辑:历史消息不可编辑、无
/edit命令 - 自动归档:日期间隔不会自动归档,换天手动 Ctrl+G
- 上下文注入:仅注入日期和标题列表,策略粗糙,AI 不知道如何有效利用
进度统计
P0 ■■■■■ 5/5 清除
P1 ■■■■□ 7/11(3 完成 + 4 部分 + 3 未开始 + 1 新增)
P2 ■□□□□ 1/8
P3 ■■□□□ 3/8
─────────────────
总计 15/32 完成或部分完成
2. 架构诊断
2.1 当前架构
app.py (837 行 God Object)
├── UI 渲染 ── Textual widgets, CSS 内联字符串 → 已抽至 diary.tcss
├── 命令解析 ── 字符串 split + 巨型 if/elif 链 (180+ 行)
├── 命令自动执行 ── 正则从 AI 回复中挖 /command (70 行)
├── 业务逻辑 ── 归档、Provider、目标、倒数日分散各处
├── 错误处理 ── 混在 UI 层,大量 except:pass
└── 状态管理 ── self 属性散落(_awaiting_provider, _archiving...)
2.2 核心矛盾
每增加一个 /命令,需要修改 3 个地方:
| 位置 | 改什么 |
|---|---|
_handle_command |
加 elif cmd == "/xxx" 分支 |
_handle_command 上半部分 |
改 /help 硬编码字符串列表 |
_auto_execute_commands |
同步一份解析逻辑 |
三处格式对不齐就出 Bug,维护成本随命令数量线性增长。
2.3 关键缺失流程
文本编辑
当前消息一旦发送不可更改。用户打错字、想删除某句话都无法操作。这是日记工具的基本能力——写作必然伴随修改。
CLI Creator 类比:相当于只有 create 没有 update/delete。
自动归档
用户昨天写的日记,今天打开 App 不会自动归档。需手动 Ctrl+G。隔三天打开,三天前的日记还挂着 active。
CLI Creator 类比:缺少 doctor 风格的启动检查——"startup 时扫描状态,把该收尾的收尾"。
上下文注入
当前 build_messages 把历史消息全量灌入(无截断风险),注入的上下文(目标、倒数日、摘要日期)只有标题列表,LLM 不知道如何有效利用这些信息来提问。
CLI Creator 类比:相当于 --help 只列出命令名,没有用法示例和最佳实践提示。
2.4 架构目标
┌─ TUI 层 ────────── 薄层,只有渲染和事件绑定
├─ 命令注册表 ────── 一处定义,自动生成 help + 校验 + 分发
└─ 操作层 ────────── 纯函数/类,独立可测,不依赖 Textual
├── SessionService ├── ArchiveService
├── GoalService ├── CountdownService
├── ProviderService ├── SearchService
├── DoctorService ├── EditService
├── MemoryService (Mem0 记忆层)
└── ContextService (整合记忆 + 目标的上下文引擎)
3. Bug 清单
3.1 已修复
| # | Bug | 文件 | 修复内容 |
|---|---|---|---|
| 1 | mkdir 缺 parents=True |
storage/txt.py:13 |
改为 mkdir(parents=True, exist_ok=True) |
| 2 | 归档总标记 summary_edited=True |
archive/manager.py:22 |
直接设 summary_edited=False |
| 4 | 429 永久禁用 Provider | error_handler.py + sqlite.py |
拆分 402=quota, 429=rate_limit |
| 5 | Ctrl+S 重复归档 | ui/app.py:854 |
_archiving 锁 |
| 6 | not_found 关键词过宽 |
error_handler.py:24 |
从列表中移除 |
| 7 | Provider 无法重新启用 | sqlite.py + app.py |
/provider enable <name> |
| 8 | 输入框提交后卡住 | ui/app.py:234 |
event.input.value = "" 移至 await 前 |
| 9 | 空会话堆积 | sqlite.py + app.py |
abandon_empty_sessions() |
| 10 | 幽灵测试数据 | DB 清理 | 删除残留会话 + txt |
3.2 待修复
| # | Bug | 严重度 | 文件 | 根因 |
|---|---|---|---|---|
| 3 | _with_retry else 分支不可达 |
低 | llm/pool.py:47 |
last_error 在所有出循环路径上已赋值 |
4. 优化清单
4.1 已完成
| # | 优化 | 类型 |
|---|---|---|
| 4 | 会话恢复 | 功能 |
| 7 | base_url 重复 strip |
整洁 |
| 9 | .gitignore |
工程 |
| 12 | /history 历史浏览 |
功能 |
| 14 | 侧边栏信息密度 | 功能 |
| 15 | 归档后翻阅 | 功能 |
4.2 部分完成
| # | 优化 | 已完成 | 缺失 |
|---|---|---|---|
| 11 | 首次使用体验 | 无 Provider 时显示引导 | 非向导式,仍填键值对 |
| 13 | AI 记忆 | 注入日记日期列表 + 目标 + 倒数日 | 不注入摘要内容,记忆不深 |
| 19 | 提示词设计 | System prompt 重写、Archive prompt 重构 | 上下文注入策略粗糙(详见 §5.4) |
| 20 | 核心原则 | 去拟人化、三层追问框架 | 工具调用用行内命令代替 API |
| 21 | 工具调用架构 | _auto_execute_commands 行内解析 |
无 tool calling API、无确认机制 |
4.3 未开始
| # | 优化 | 影响 | 依赖 |
|---|---|---|---|
| 1 | Token 截断 | 高 | 简单轮数截断即可 |
| 2 | DB 连接复用 | 低 | SQLite 单用户场景开销可忽略 |
| 3 | LlmProvider dead code |
低 | — |
| 5 | /goal add 支持类型 |
中 | 需改命令格式 |
| 6 | 流式重试 | 中 | _with_retry 改造 |
| 8 | 日志系统 | 高 | 需全项目引入 logging |
| 10 | 侧边栏全表查询 | 低 | — |
| 16 | 搜索过滤 | 低 | 单人本地日记写作,全文搜索非刚需 |
| 17 | 统计洞察 | 中 | 需数据聚合 |
| 18 | Provider 配置向导 | 中 | 需交互式输入流程 |
4.4 新增优化项
| # | 优化 | 影响 | 说明 |
|---|---|---|---|
| 22 | 文本编辑 | 高 | 消息历史可编辑、删除、重新生成 AI 回复 |
| 23 | 自动归档 | 高 | 启动时检测日期间隔,自动归档昨日/多日前活跃会话 |
| 24 | 上下文注入重构 | 高 | 从标题列表注入升级为结构化注入,给 LLM 明确的使用指引 |
| 25 | 流式对话接入 UI | 中 | LlmPool.stream_chat() 已实现,app.py 未使用,仍为"等待全文"体验 |
| 26 | 记忆系统 (Mem0) | 高 | 基于 Mem0 的持久记忆层,语义检索、跨会话回忆 |
5. 重构路线图
Phase 1: 止血 —— 架构解耦基础
目标:消灭 God Object 的前 50% 毒性
1.1 命令注册表
| 步骤 | 产出 | 收益 |
|---|---|---|
新建 src/commands/registry.py |
命令注册、解析、help 自动生成 | 消除 _handle_command 180 行 if/elif |
| 迁移现有命令 | 每个命令独立模块 | _auto_execute_commands 复用同一注册表 |
| 输入校验层 | 参数 schema、类型自动校验 | /goal add 不再需硬编码类型 |
目标结构:
src/commands/
├── registry.py # CommandRegistry: 注册、解析、help
├── base.py # Command 基类: name, help, schema, execute()
├── goals.py # GoalCommands
├── providers.py # ProviderCommands
├── countdowns.py # CountdownCommands
├── history.py # HistoryCommands
├── sessions.py # SessionCommands
├── memory.py # MemoryCommands (新增): /memory search
├── edit.py # EditCommands (新增): /edit delete, /edit retry
├── doctor.py # DoctorCommand (新增)
注意:Phase 2 的
/edit命令应基于此注册表实现,不继续扩展 app.py 的 if/elif 链。
1.2 操作层剥离
将 app.py 中散落的业务逻辑抽成 service:
src/services/
├── session.py # 从 app.py _init_session, _create_new_session 抽
├── archive.py # 从 app.py _do_archive, archive/manager.py 整合
├── provider.py # 从 app.py _parse_provider_input 抽
├── context.py # 新增: 上下文构建引擎 (从 prompt/engine.py 升级)
├── doctor.py # 新增: DB、config、provider 全量健康检查
└── __init__.py
1.3 Doctor 命令
/doctor
├── Database ✓ connected (diary.db, 10 sessions)
├── Config ✓ ~/.ai-diary/config.json
├── Diary Dir ✓ /Users/xxx/.ai-diary/diary/
├── Providers ✗ DeepSeek (429 rate limited)
│ ✓ OpenAI (active, quota: 850)
└── Prompts ✓ ~/.ai-diary/prompts/prompts.json
Phase 2: 回血 —— 核心体验补齐
2.1 文本编辑 (Opt 22)
提供类似聊天应用的消息编辑能力,使用 CLI Creator 的"发现→选中→操作"模式:
/edit 3 # 编辑第 3 条消息(弹出编辑区)
/edit 4 "修正后的内容" # 直接替换第 4 条消息
/edit delete 5 # 删除第 5 条消息及之后的所有回复
/edit retry 6 # 从第 6 条消息位置重新生成 AI 回复
设计要点:
- 消息索引从 1 开始(用户可见顺序)
- 删除消息后,之后的 AI/用户消息级联删除(因为上下文断裂)
/edit retry N保留前 N-1 条,从第 N 条位置重新调用 LLM- 编辑后自动更新会话展示,同时触发侧边栏刷新
CLI Creator 视角:编辑是一个"写操作",应该接受最小资源 ID(消息索引)和操作类型,返回结果给调用方展示。UI 只负责渲染编辑前后的状态。
2.2 自动归档 (Opt 23)
启动时智能检测,自动处理日期跨越:
_startup_check():
active = get_active_session()
if not active or not active.messages:
return # 空会话,不处理
session_date = parse_date(active.created_at)
today = date.today()
if session_date != today:
# 日期变了,自动归档旧会话
archive_session(active.id)
create_new_session()
show_notification(f"已自动归档 {session_date} 的日记")
设计要点:
- 仅在 App 启动时执行一次(
on_mount末尾) - 只归档有消息的活跃会话,空会话已在 Bug 9 中清理
- 归档后创建新会话,用户看到的是空白对话 + 归档通知
- 不自动归档当日会话(用户可能还在写)
- 多天未打开(例如 3 天):只归档最早的会话,其他活跃会话状态保持不变(用户可通过
/history手动查阅后决定)
CLI Creator 视角:相当于 doctor 的健康检查扩展——startup 时扫描状态,对不健康的条目执行自动修复并报告。
2.3 记忆系统 (Opt 26) ⬅️ 新增
当前 AI 的"记忆"仅靠上下文注入日期列表(prompt/engine.py:42 "仅知日期,不知内容"),LLM 不知道用户昨天具体写了什么,也无法跨会话回忆过往事件。
方案:基于 Mem0(56.4k stars, Apache 2.0)构建持久记忆层。
为什么选 Mem0
| 约束 | Mem0 | 其他 |
|---|---|---|
| Python 库模式(无 server) | ✅ | 仅 cognee 部分支持 |
| SQLite 存储 | ✅ mem0_history.db |
Letta/Zep 需 Postgres/Neo4j |
| OpenAI 兼容 API | ✅ | 均支持 |
| DeepSeek(中文 LLM) | ✅ | 需自行配置 |
| 本地无 Docker | ✅ | Letta/Zep 需要 |
唯一短板:默认英文 embedding 模型,需手动配置 BAAI/bge-large-zh-v1.5 来保证中文语义搜索质量。
配置
from mem0 import Memory
config = {
"vector_store": {
"provider": "chroma", # SQLite 后端,零额外服务
"config": {"path": "~/.ai-diary/chroma"}
},
"llm": {
"provider": "openai", # 复用现有 DeepSeek
},
"embedder": {
"provider": "huggingface",
"config": {"model": "BAAI/bge-large-zh-v1.5"}
},
"history_db_path": "~/.ai-diary/mem0_history.db"
}
新增依赖
mem0ai >= 0.1.0
chromadb >= 0.5.0
sentence-transformers >= 3.0.0
调用时机
每轮对话后:
user_input → LLM 回复 → memory.add(本轮关键事实)
每次归档时:
Ctrl+G → 生成摘要 → memory.add(摘要全文)
对话开始时:
build_context → memory.search(当前话题) → 注入 context buffer
用户主动查询:
/memory search "上次提到 Rust" → 语义检索过往记忆
架构影响
src/services/memory.py ← 新建,封装 Mem0
├── remember(content) → memory.add()
├── recall(query) → memory.search()
└── archive_session() → 归档时批量提取
src/services/context.py ← 新建,整合记忆 + 其他上下文
└── build_context() → recall() + goals + countdowns
src/prompt/engine.py ← 简化,只负责 prompt 模板
提取策略
Mem0 内置 LLM 驱动的 fact extraction,每轮对话自动提取结构化记忆(who/what/where/when)。对于日记场景,额外规则:
- 事实性内容:事件、决策、计划 → 提取为长期记忆
- 情绪表达:作为元数据附着,不单独存储
- 对话流水:不提取,已保存在 session messages 中
- 归档摘要:标记
importance=HIGH,搜索时优先匹配
2.4 上下文注入重构 (Opt 24)
当前注入的问题:
# 现状: 只给列表,不给用法
"用户当前目标:学Rust, 每天跑步"
"即将到来的事件:培训(0天)"
"用户最近写日记的日期:2026-05-21, 2026-05-20(仅知日期,不知内容)"
LLM 收到这些后不知道该怎么做——是直接问"你今天学 Rust 了吗?"还是自然提及?没有指引。
重构策略:分级注入 + 记忆融合 + 使用指引
上下文由 ContextService 统一构建,融合 Mem0 记忆库的语义检索结果:
┌─ System Prompt ────────────────────────────┐
│ 1. 当前日期时间(已做) │
│ 2. 上次对话断点摘要(来自 Mem0 记忆检索) │
│ 3. 对话节奏提示 │
├─ Context Buffer(注入到对话开头) ──────────┤
│ 1. 相关记忆(Mem0 语义检索,≤3 条) │
│ 2. 近期摘要(最近 3 篇,含实际内容) │
│ 3. 活跃目标 + 进度描述 │
│ 4. 即将到来的倒数日 │
│ 5. 使用策略指引(告诉 LLM 如何用) │
└─────────────────────────────────────────────┘
使用策略指引示例:
## 上下文使用策略
你有权访问以下用户信息。使用原则:
- 记忆库:历史相关事件,用于连接过往,自然提及作对话延续。
- 近期摘要:了解用户最近关注方向。
- 活跃目标:用户主动提及时可追问进展,不主动查岗。
- 倒数日:事件在 3 天内时可温和提醒,超出不提。
注入优先级:
| 优先级 | 内容 | 裁剪策略 | 来源 |
|---|---|---|---|
| 1 | System prompt | 不裁剪 | prompt/engine.py |
| 2 | 当前消息 | 不裁剪 | 用户输入 |
| 3 | 最近 20 轮对话 | 超出截断 | session.messages |
| 4 | 相关记忆(Mem0 语义检索) | 最多 3 条 | memory.recall() |
| 5 | 上次对话断点(3 轮摘要) | 超 3 轮截断 | memory.search() |
| 6 | 近期摘要(3 篇完整) | 超 3 篇截断 | storage |
| 7 | 活跃目标(最多 5 个) | 超 5 个截断 | goal_manager |
| 8 | 倒数日(最近 3 个) | 超 3 个截断 | countdown_manager |
裁剪优先从低优先级内容开始。
2.5 Token 截断 (Opt 1)
在上下文注入重构的基础上,从最早的消息开始裁剪:
# build_messages 截断策略
MAX_HISTORY_ROUNDS = 20 # 保留最近 20 轮
history_to_send = history[-MAX_HISTORY_ROUNDS:] if len(history) > MAX_HISTORY_ROUNDS else history
2.6 日志系统 (Opt 8)
import logging
logger = logging.getLogger("ai_diary")
# 关键路径加日志,不需要全量覆盖
except Exception as e:
logger.warning("Failed to fetch goals", exc_info=True)
在 LLM 调用、归档、Provider 切换等关键路径加上日志,输出到 ~/.ai-diary/diary.log。
2.7 搜索过滤 (Opt 16)
-- SQLite FTS5 全文索引
CREATE VIRTUAL TABLE messages_fts USING fts5(
session_id, role, content, created_date
);
命令:/search <关键词> [-t 标签] [--from 日期] [--to 日期]
2.8 /goal add 支持类型 (Opt 5)
/goal add yearly 学 Rust
/goal add monthly 每天跑步
/goal add habit 早起
/goal add okr "Q2 上线日记 App, 3个KR"
Phase 3: 造血 —— 命令注册表落地 + 工具调用 + 测试
3.1 命令注册表实现
将 Phase 1 的蓝图落地:
# registry.py
class CommandRegistry:
def register(self, command: Command):
"""注册一个命令,自动暴露给 /help 和 _auto_execute_commands"""
def parse(self, raw: str) -> ParsedCommand:
"""解析用户输入,返回命令名 + 参数"""
async def execute(self, parsed: ParsedCommand, ctx: CommandContext):
"""执行命令,ctx 包含 storage, llm_pool, session 等依赖"""
def help_text(self) -> str:
"""自动生成 /help 输出"""
def tool_schemas(self) -> list[dict]:
"""生成 OpenAI compatible tool definitions"""
执行时依赖注入:CommandContext 封装 storage, session_manager, llm_pool 等,通过注册表注入到每个命令的 execute() 方法。
3.2 工具调用架构 (Opt 21)
从行内命令解析升级为 OpenAI compatible tool calling:
# LLM 请求注入 tools 参数
tools = registry.tool_schemas() # 自动从注册表生成
# LLM 返回 tool_calls 自动分发
for tool_call in response.tool_calls:
result = await registry.execute_tool(tool_call)
保留确认机制:写入类操作在 UI 弹出 [确认执行 /goal add xxx? y/n]。
3.3 测试基础设施
tests/
├── test_storage.py # SQLite CRUD (fixture db)
├── test_commands.py # 命令注册表 (无 UI)
├── test_services.py # 服务层 (mock LLM)
├── test_pool.py # Provider failover (mock HTTP)
└── fixtures/
└── sample_sessions.json
目标:操作层和命令层覆盖率 > 70%,TUI 层不做自动化测试。
6. 架构目标
完成后的架构
┌──────────────────────────────────────────┐
│ TUI (app.py ~250 行) │
│ 只做: 渲染、事件绑定、UI 状态 │
│ CSS: diary.tcss (独立文件) │
├──────────────────────────────────────────┤
│ Command Registry (src/commands/) │
│ 一处定义 → 自动 help + 用户输入 + AI输出复用│
├──────────────────────────────────────────┤
│ Context Engine (src/services/context.py) │
│ 整合 Mem0 记忆 + 目标 + 倒数日,分级注入 │
├──────────────────────────────────────────┤
│ Memory Layer (src/services/memory.py) │
│ Mem0 封装: remember / recall / archive │
├──────────────────────────────────────────┤
│ Service Layer (src/services/) │
│ Session, Archive, Goal, Countdown, │
│ Provider, Search, Doctor, Edit │
│ 纯函数/类,无 UI 依赖,可独立测试 │
├──────────────────────────────────────────┤
│ Storage / LLM / Mem0 / Config │
└──────────────────────────────────────────┘
各层职责边界
| 层 | 拥有什么 | 不拥有什么 |
|---|---|---|
| TUI | Textual widget、事件路由、通知 | 业务逻辑、命令执行、数据库操作 |
| Command Registry | 命令定义、参数校验、帮助生成 | UI 渲染、LLM 调用 |
| Context Engine | 记忆检索调度、上下文拼接、注入优先级 | prompt 文本内容 |
| Memory Layer | Mem0 CRUD、语义搜索、记忆提取规则 | UI、对话管理 |
| Service | 纯逻辑、状态转换、数据验证 | UI、Textual 依赖 |
重构原则
- 始终向前兼容 — 每次 PR 后 App 仍可正常启动使用
- 先抽离再改造 — 先建 service 层包装现有逻辑,再优化内部实现
- TUI 不减功能 — 重构期间不丢任何现有功能
- 每个 Phase 提交一次 — 不搞大爆炸式合并
- CLI Creator 思维 — 每个操作都是可组合的独立单元,TUI 只是消费者
附录: 文件清理记录
| 旧文件 | 处理 | 原因 |
|---|---|---|
BUGS.md |
合并后删除 | 内容归入本文 §3 |
OPTIMIZATIONS.md |
合并后删除 | 内容归入本文 §4 |
PLAN.md |
合并后删除 | 内容归入本文 §5, §6 |