Kimi 对话历史管理器 —— 将你在 Kimi (kimi.moonshot.cn) 上的所有对话归档到本地,使用 LLM 自动分类打标签,配合可视化图表,构建个人知识库。
| 层 |
技术 |
| 后端 |
Express (TypeScript), better-sqlite3 (SQLite), Playwright (浏览器自动化), OpenAI SDK |
| 前端 |
React 18 + Vite, Tailwind CSS, Recharts, React Router v6 |
| 包管理 |
npm workspaces (monorepo) |
kimi-bucket/
├── package.json # 根 workspaces 配置 (concurrently 并行启动)
├── .env # PORT 配置 (默认 3001)
├── .env.example
├── cookies.json # Kimi 登录 Cookie (由 Playwright 登录后保存)
│
├── server/ # 后端 (Express + SQLite)
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── index.ts # Express 入口, 路由挂载, 静态文件服务
│ ├── db/
│ │ └── index.ts # 数据库初始化 (建表) + 配置存取
│ ├── lib/
│ │ └── day-boundary.ts # 凌晨 4 点日界线工具
│ ├── scraper/
│ │ ├── auth.ts # Playwright 登录, Cookie 管理, 验证
│ │ └── kimi.ts # Kimi API v2 客户端 (ListChats, ListMessages)
│ └── routes/
│ ├── conversations.ts # 对话列表 (分页/搜索/标签日期筛选) + 详情 + 手动打标签
│ ├── tags.ts # 标签 CRUD + 导出/导入 (.txt)
│ ├── scraper.ts # 登录状态 / 登录 / 同步 (SSE 流)
│ ├── llm.ts # Token 估算 / AI 分类分析 (SSE 流)
│ ├── config.ts # 应用配置读写 (LLM, 预览长度, 截断行数, 日期策略)
│ ├── charts.ts # 图表数据 (日/周/月聚合)
│ └── migrate.ts # 历史报告导入 (output/*.md)
│
├── client/ # 前端 (React + Vite)
│ ├── package.json
│ ├── vite.config.ts # 开发代理 /api → localhost:3001, build → server/public/
│ ├── tailwind.config.js
│ └── src/
│ ├── App.tsx # 根组件: 侧边导航 + 路由 (/ /charts /settings)
│ ├── main.tsx # React 入口
│ ├── types.ts # 共享类型定义
│ ├── lib/
│ │ ├── api.ts # API 客户端 (fetch + EventSource)
│ │ └── utils.ts # 工具函数 (日期格式化, 颜色亮度, Kimi 链接, SSE 监听)
│ └── pages/
│ ├── Conversations/ # 对话库主页面 (列表/搜索/筛选/同步/分析)
│ ├── Charts/ # 图表页 (堆叠柱状图 + 饼图 + 明细表)
│ └── Settings/ # 设置页 (偏好设置/标签管理/Prompt/登录)
│
├── data/
│ └── kimi-bucket.db # SQLite 数据库 (gitignore)
└── output/ # 历史日报导入源 (gitignore)
-- 对话
conversations (
id TEXT PRIMARY KEY, -- 等同于 kimi_id
kimi_id TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
kimi_created_at TEXT NOT NULL,
kimi_updated_at TEXT NOT NULL,
messages TEXT NOT NULL DEFAULT '[]', -- JSON: [{role, content}]
preview TEXT, -- 首条用户消息截取
synced_at TEXT NOT NULL -- 本地同步时间
);
-- 标签
tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
color TEXT NOT NULL DEFAULT '#6366f1',
is_system INTEGER NOT NULL DEFAULT 0, -- 1=系统内置 (未分类/其他)
created_at TEXT
);
-- 对话-标签关联
conversation_tags (
conversation_id TEXT NOT NULL,
tag_id INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'manual', -- manual/llm/system
assigned_at TEXT,
PRIMARY KEY (conversation_id, tag_id)
);
-- LLM 分析记录
llm_analyses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
tags_json TEXT NOT NULL DEFAULT '[]', -- JSON: ["标签1", "标签2"]
summary TEXT, -- LLM 生成的一句话摘要
model TEXT NOT NULL,
prompt_used TEXT, -- 使用的完整 prompt
tokens_used INTEGER,
created_at TEXT
);
-- 历史日报
legacy_reports (report_date, content, imported_at);
-- 应用配置 (键值对)
app_config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
| 标签 |
颜色 |
说明 |
未分类 |
#94a3b8 (slate-400) |
新同步的对话自动打上,LLM 分析成功后自动移除 |
其他 |
#cbd5e1 (slate-300) |
LLM 认为不属于任何预定义标签时使用 |
用户点击「打开浏览器登录」
→ Playwright 启动 Chromium headful
→ 打开 kimi.com, 用户手动登录
→ 终端按 Enter 确认
→ 保存 Cookie 到 server/cookies.json (含 kimi-auth token)
→ 后续请求携带 Cookie + Authorization Bearer header
认证文件: server/src/scraper/auth.ts
loginInteractive() — 交互式登录
verifyCookies() — 通过实际 API 调用验证是否过期
getAuthToken() — 从 Cookie 中提取 kimi-auth token
用户选择同步模式并确认
→ POST /api/scraper/sync (SSE 流式响应)
→ Kimi API v2: ListChats (分页获取全部聊天列表)
→ 根据模式筛选:
- incremental: 只取 updateTime > 本地最新记录的
- range: 按用户指定日期区间
- recent: 取最近 N 条
→ 对每个对话: ListMessages 获取完整消息
→ 写入/更新 SQLite:
- 新对话: INSERT + 自动打「未分类」标签
- 已有对话: UPDATE (日期策略由配置决定)
→ SSE 推送实时进度到前端
同步模式实现: server/src/scraper/kimi.ts
- KIMI_API_V2 =
https://www.kimi.com/apiv2
- gRPC-web 端点:
kimi.chat.v1.ChatService/ListChats, kimi.gateway.chat.v1.ChatService/ListMessages
日期冲突策略 (sync_date_conflict 配置项):
kimi (默认): 使用 Kimi 返回的日期
local: 保留本地已有日期
newer: 取两者中较新的
用户选中对话 → 点击「分析选中」
→ GET /api/llm/estimate?ids=... 估算 token 消耗
→ 弹出确认框 (显示选中数量和预估 tokens)
→ 用户确认 → GET /api/llm/analyze?ids=... (SSE)
→ 对每条对话:
1. 构建 Prompt (注入 {tags} {title} {messages})
2. 调用 OpenAI-compatible API
3. 解析 JSON 响应 → 提取 tags 和 summary
4. 清理标签名 (去除 ":描述" 后缀, "()" 等)
5. 匹配数据库中的标签:
- 匹配成功 → 打上对应标签
- LLM 返回"未分类" → 保持未分类状态
- 匹配失败但有标签名 → 归入「其他」
6. 删除旧的 LLM 标签 + '未分类' 系统标签 (如果匹配成功)
7. 保存分析记录到 llm_analyses 表
模型管理: server/src/routes/llm.ts
extractJson() — 容错 JSON 解析 (支持 json 代码块, 裸 JSON)
normalizeTags() — 标准化标签数组
cleanName() — 去除 LLM 输出中的「标签:描述」模式和括号后缀
estimateTokens() — 中文字符按 1.5 char/token, 英文按 4 char/token 估算
server/src/lib/day-boundary.ts 和 client/src/lib/utils.ts:
- 凌晨 4 点之前更新的对话,其
naturalDay 算作前一天
- 例如:
2026-01-15 03:30 → naturalDay = 2026-01-14
- 前端筛选日期范围时使用 naturalDay 作为查询维度
GET /api/charts/data?period=daily|weekly|monthly&from=&to=
→ 查询所有对话 + 关联标签
→ 按日/周/月聚合: 每个时间桶 × 每个标签 = 计数
→ 返回 { data: [{ date, 标签A: N, 标签B: M, ... }], tagNames: [...] }
前端使用 Recharts 渲染:
- 堆叠柱状图 — 标签分布趋势 (点击柱子可跳转到对应日期+标签筛选)
- 饼图 — 总体分布 (按数量降序, 点击扇区跳转)
- 数据明细表
- 导出:
GET /api/tags/export → 下载 .txt 文件, 格式 标签名 或 标签名|描述
- 导入:
POST /api/tags/import → 解析每行, INSERT OR IGNORE 保存到数据库
所有配置存储在 app_config 表中,通过设置页面的「偏好设置」Tab 管理:
| Key |
默认值 |
说明 |
llm_base_url |
https://api.openai.com/v1 |
LLM API 地址 |
llm_api_key |
(空) |
API Key |
llm_model |
gpt-4o-mini |
模型名称 |
preview_length |
200 |
对话列表预览文本长度 (50-2000) |
message_max_lines |
15 |
展开消息时的截断行数 (5-100) |
sync_date_conflict |
kimi |
重复对话时的日期策略 (kimi/local/newer) |
classification_prompt |
(预设模板) |
LLM 分类 Prompt, 支持 {tags} {title} {messages} 变量 |
你是一个对话分类器。请根据以下预定义标签,为给定的对话分配一个或多个标签。
可用标签:
{tags}
请返回 JSON 格式:
{"tags": ["标签名1", "标签名2"], "summary": "一句话概括这个对话的内容"}
如果对话不属于任何标签,返回 {"tags": ["未分类"], "summary": "..."}
对话标题:{title}
对话内容(截取前2000字):
{messages}
cd kimi-bucket
npm install # 安装所有 workspace 依赖
cp .env.example .env
# 编辑 .env: PORT=3001
npm run dev
# server → localhost:3001 (tsx watch)
# client → localhost:5173 (Vite dev server, proxy /api → 3001)
npm run build
# client 构建到 server/public/
# server 编译到 server/dist/
npm start # → node server/dist/index.js
# 访问 http://localhost:3001
| 方法 |
路径 |
说明 |
| GET |
/api/conversations |
列表 (分页/搜索/标签/日期筛选) |
| GET |
/api/conversations/:id |
详情 (含完整消息) |
| POST |
/api/conversations/:id/tags |
手动设置标签 |
| 方法 |
路径 |
说明 |
| GET |
/api/tags |
所有标签 |
| POST |
/api/tags |
创建标签 |
| PATCH |
/api/tags/:id |
编辑标签 |
| DELETE |
/api/tags/:id |
删除标签 |
| GET |
/api/tags/export |
导出 (text/plain) |
| POST |
/api/tags/import |
导入 (text/plain) |
| 方法 |
路径 |
说明 |
| GET |
/api/scraper/status |
登录状态 + 最后同步时间 |
| POST |
/api/scraper/login |
打开浏览器登录 |
| GET |
/api/scraper/sync |
同步 (SSE), query: mode/dateFrom/dateTo/limit/ids |
| 方法 |
路径 |
说明 |
| GET |
/api/llm/estimate |
Token 估算, query: ids |
| GET |
/api/llm/analyze |
分类分析 (SSE), query: ids |
| 方法 |
路径 |
说明 |
| GET |
/api/charts/data |
图表数据, query: period/from/to |
| 方法 |
路径 |
说明 |
| GET |
/api/config |
获取所有配置 |
| PATCH |
/api/config |
更新配置 |
| GET |
/api/config/preview-update |
批量更新预览文本 |
- Kimi API v2 直调: 使用
kimi.chat.v1.ChatService/ListChats 和 kimi.gateway.chat.v1.ChatService/ListMessages gRPC-web 端点,替代旧的页面爬取方式,兼容 Kimi 2.x 所有版本
- 凌晨 4 点日界线: 适配使用习惯,凌晨三四点还在聊天的场景算作前一天
- 解耦 LLM 分析: 用户先同步对话,手动勾选需要分析的条目,确认 token 估算后再执行。而非自动全量分析
- 标签名清洗: LLM 可能返回
标签名:描述 格式,通过 cleanName() 截取冒号/括号前部分来精确匹配
- 「未分类」标签自动管理: 新同步自动添加 → LLM 分析成功自动移除,形成一个状态机
- 单文件 SQLite: 便于备份和迁移,无需额外数据库服务
express + cors — HTTP 服务
better-sqlite3 — SQLite 数据库
playwright — 浏览器自动化登录
openai — LLM API 调用
dayjs — 日期处理
dotenv — 环境变量
react + react-dom (v18)
react-router-dom (v6)
recharts — 图表
tailwindcss — 样式
dayjs — 日期格式化
vite — 构建工具