chattercatcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +157 -0
- package/LICENSE +21 -0
- package/README.md +331 -0
- package/assets/chattercatcher-hero.svg +58 -0
- package/assets/chattercatcher-logo.png +0 -0
- package/assets/readme/preview-answer.svg +12 -0
- package/assets/readme/preview-chat.svg +13 -0
- package/assets/readme/preview-rag.svg +18 -0
- package/assets/readme/preview-web.svg +20 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3752 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +808 -0
- package/dist/index.js +3353 -0
- package/dist/index.js.map +1 -0
- package/docs/DEVELOPMENT_PLAN.md +163 -0
- package/docs/PRD.md +235 -0
- package/docs/TECHNICAL_ARCHITECTURE.md +402 -0
- package/package.json +67 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# 技术架构
|
|
2
|
+
|
|
3
|
+
## 总体架构
|
|
4
|
+
|
|
5
|
+
```mermaid
|
|
6
|
+
flowchart TD
|
|
7
|
+
A["飞书/Lark 群"] --> B["本地 Gateway"]
|
|
8
|
+
B --> C["消息接收器"]
|
|
9
|
+
C --> D["SQLite 原始存储"]
|
|
10
|
+
C --> E["媒体下载器"]
|
|
11
|
+
E --> F["本地文件库"]
|
|
12
|
+
D --> G["解析与切块"]
|
|
13
|
+
F --> G
|
|
14
|
+
G --> H["Embedding Worker"]
|
|
15
|
+
H --> I["向量库"]
|
|
16
|
+
G --> J["SQLite FTS 索引"]
|
|
17
|
+
G --> K["事实抽取器"]
|
|
18
|
+
K --> L["事实与版本"]
|
|
19
|
+
|
|
20
|
+
A --> M["@机器人提问"]
|
|
21
|
+
M --> N["查询路由"]
|
|
22
|
+
N --> O["混合检索器"]
|
|
23
|
+
O --> P["冲突处理器"]
|
|
24
|
+
P --> Q["LLM 答案生成器"]
|
|
25
|
+
Q --> A
|
|
26
|
+
|
|
27
|
+
R["CLI"] --> S["配置与运行时"]
|
|
28
|
+
T["本地 Web UI"] --> S
|
|
29
|
+
T --> D
|
|
30
|
+
T --> I
|
|
31
|
+
T --> L
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 运行时
|
|
35
|
+
|
|
36
|
+
- Node.js 20+。
|
|
37
|
+
- TypeScript。
|
|
38
|
+
- npm 全局包。
|
|
39
|
+
- 本地优先运行。
|
|
40
|
+
|
|
41
|
+
## 推荐技术栈
|
|
42
|
+
|
|
43
|
+
### CLI
|
|
44
|
+
|
|
45
|
+
- `commander`:命令结构。
|
|
46
|
+
- `@inquirer/prompts`:交互式 setup/settings。
|
|
47
|
+
- `pino`:日志。
|
|
48
|
+
|
|
49
|
+
### Gateway 和 API
|
|
50
|
+
|
|
51
|
+
- `fastify`:本地 HTTP API 和 Web UI 后端。
|
|
52
|
+
- `@larksuiteoapi/node-sdk`:飞书/Lark API 和长连接能力。
|
|
53
|
+
|
|
54
|
+
### Web UI
|
|
55
|
+
|
|
56
|
+
- React。
|
|
57
|
+
- Vite。
|
|
58
|
+
- TanStack Query。
|
|
59
|
+
- TanStack Router。
|
|
60
|
+
- Tailwind CSS。
|
|
61
|
+
- Radix UI 或 shadcn/ui。
|
|
62
|
+
|
|
63
|
+
### 存储
|
|
64
|
+
|
|
65
|
+
- SQLite:元数据、原始消息、任务、配置、事实。
|
|
66
|
+
- Drizzle ORM:schema 和 migration。
|
|
67
|
+
- SQLite FTS5:关键词检索。
|
|
68
|
+
- LanceDB 或其他本地嵌入式向量库:embedding 检索。
|
|
69
|
+
|
|
70
|
+
SQLite 不能作为完整 RAG 的唯一后端。它只负责结构化元数据和关键词召回;语义召回必须走向量库 adapter。MVP 默认优先本地向量库,首选 LanceDB,保留 Qdrant/Chroma adapter 扩展空间。
|
|
71
|
+
|
|
72
|
+
### LLM 和 Embedding
|
|
73
|
+
|
|
74
|
+
- OpenAI-compatible chat completions API。
|
|
75
|
+
- OpenAI-compatible embeddings API。
|
|
76
|
+
- 可配置 base URL、API key、模型名和向量维度。
|
|
77
|
+
- `doctor` 必须验证 provider 兼容性。
|
|
78
|
+
|
|
79
|
+
### 解析器
|
|
80
|
+
|
|
81
|
+
- PDF:`pdf-parse` 或 `unpdf`。
|
|
82
|
+
- DOCX:`mammoth`。
|
|
83
|
+
- XLSX:`xlsx`。
|
|
84
|
+
- PPTX:先解压 XML 提取,之后再接专用 parser。
|
|
85
|
+
- HTML/链接:`cheerio` 加 readability。
|
|
86
|
+
- OCR:可配置路径,使用 Tesseract.js 或基于视觉模型的 OCR。
|
|
87
|
+
- 音频:先支持可配置的 OpenAI-compatible transcription,之后支持本地 Whisper。
|
|
88
|
+
|
|
89
|
+
## 本地数据布局
|
|
90
|
+
|
|
91
|
+
默认:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
~/.chattercatcher/
|
|
95
|
+
config.json
|
|
96
|
+
secrets.json
|
|
97
|
+
data/
|
|
98
|
+
chattercatcher.db
|
|
99
|
+
exports/
|
|
100
|
+
files/
|
|
101
|
+
thumbnails/
|
|
102
|
+
transcripts/
|
|
103
|
+
vector/
|
|
104
|
+
logs/
|
|
105
|
+
cache/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`config.json` 存普通配置。
|
|
109
|
+
|
|
110
|
+
`secrets.json` 存敏感值:
|
|
111
|
+
|
|
112
|
+
- 飞书 App Secret。
|
|
113
|
+
- LLM API Key。
|
|
114
|
+
- embedding API Key,若和 LLM 分开。
|
|
115
|
+
|
|
116
|
+
## 核心数据模型
|
|
117
|
+
|
|
118
|
+
### chats
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
id
|
|
122
|
+
platform
|
|
123
|
+
platform_chat_id
|
|
124
|
+
name
|
|
125
|
+
created_at
|
|
126
|
+
updated_at
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### messages
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
id
|
|
133
|
+
platform
|
|
134
|
+
platform_message_id
|
|
135
|
+
chat_id
|
|
136
|
+
sender_id
|
|
137
|
+
sender_name
|
|
138
|
+
message_type
|
|
139
|
+
text
|
|
140
|
+
raw_payload_json
|
|
141
|
+
sent_at
|
|
142
|
+
received_at
|
|
143
|
+
created_at
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### files
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
id
|
|
150
|
+
message_id
|
|
151
|
+
platform_file_key
|
|
152
|
+
file_name
|
|
153
|
+
mime_type
|
|
154
|
+
local_path
|
|
155
|
+
sha256
|
|
156
|
+
size_bytes
|
|
157
|
+
parse_status
|
|
158
|
+
created_at
|
|
159
|
+
updated_at
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### chunks
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
id
|
|
166
|
+
source_type
|
|
167
|
+
source_id
|
|
168
|
+
chunk_index
|
|
169
|
+
text
|
|
170
|
+
metadata_json
|
|
171
|
+
created_at
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### embeddings
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
id
|
|
178
|
+
chunk_id
|
|
179
|
+
provider
|
|
180
|
+
model
|
|
181
|
+
dimension
|
|
182
|
+
vector_ref
|
|
183
|
+
created_at
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### facts
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
id
|
|
190
|
+
subject
|
|
191
|
+
predicate
|
|
192
|
+
value
|
|
193
|
+
confidence
|
|
194
|
+
status
|
|
195
|
+
source_chunk_id
|
|
196
|
+
supersedes_fact_id
|
|
197
|
+
valid_from
|
|
198
|
+
created_at
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### qa_logs
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
id
|
|
205
|
+
chat_id
|
|
206
|
+
question_message_id
|
|
207
|
+
question
|
|
208
|
+
answer
|
|
209
|
+
citations_json
|
|
210
|
+
retrieval_debug_json
|
|
211
|
+
created_at
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### jobs
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
id
|
|
218
|
+
type
|
|
219
|
+
status
|
|
220
|
+
payload_json
|
|
221
|
+
attempts
|
|
222
|
+
last_error
|
|
223
|
+
created_at
|
|
224
|
+
updated_at
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## RAG 设计
|
|
228
|
+
|
|
229
|
+
RAG 是强制架构路径,不能被 prompt 堆叠替代。
|
|
230
|
+
|
|
231
|
+
检索流程:
|
|
232
|
+
|
|
233
|
+
1. 问题改写和意图识别。
|
|
234
|
+
2. 对 chunk 做向量检索,默认接本地向量库 adapter。
|
|
235
|
+
3. 通过 SQLite FTS5 做关键词检索。
|
|
236
|
+
4. 按群、时间、发送人、来源类型做元数据过滤。
|
|
237
|
+
5. 引入时间权重和来源权重重排。
|
|
238
|
+
6. 进入事实级冲突处理。
|
|
239
|
+
7. LLM 只基于最终证据块生成答案和引用。
|
|
240
|
+
|
|
241
|
+
答案生成器只能接收压缩后的证据块,不能接收无限制原始聊天历史。
|
|
242
|
+
|
|
243
|
+
## 冲突处理
|
|
244
|
+
|
|
245
|
+
冲突处理不能简单“最新消息赢”。
|
|
246
|
+
|
|
247
|
+
必须检查:
|
|
248
|
+
|
|
249
|
+
- 主体相同或高度相似。
|
|
250
|
+
- 谓词相同。
|
|
251
|
+
- 新来源时间更晚。
|
|
252
|
+
- 存在更新语义,例如:
|
|
253
|
+
- 改到。
|
|
254
|
+
- 更新为。
|
|
255
|
+
- 最终定。
|
|
256
|
+
- 以这个为准。
|
|
257
|
+
- 不是之前那个。
|
|
258
|
+
- 有足够置信度认为消息陈述的是事实,而不是建议。
|
|
259
|
+
|
|
260
|
+
状态:
|
|
261
|
+
|
|
262
|
+
- `active`:当前最可信事实。
|
|
263
|
+
- `superseded`:被较新确认事实取代的旧事实。
|
|
264
|
+
- `ambiguous`:相关证据,但不覆盖 active 事实。
|
|
265
|
+
|
|
266
|
+
## 飞书/Lark Gateway
|
|
267
|
+
|
|
268
|
+
MVP 采用本地 Gateway 模式:
|
|
269
|
+
|
|
270
|
+
- 用户创建飞书/Lark 自建应用。
|
|
271
|
+
- 用户启用机器人能力。
|
|
272
|
+
- 用户配置 App ID 和 App Secret。
|
|
273
|
+
- 用户启用长连接事件订阅。
|
|
274
|
+
- ChatterCatcher 从本机打开长连接。
|
|
275
|
+
- 传入消息事件被归一化为内部 message。
|
|
276
|
+
- 回复通过飞书/Lark Bot API 发送。
|
|
277
|
+
|
|
278
|
+
实现上使用 `@larksuiteoapi/node-sdk` 的 `WSClient` 和 `EventDispatcher`:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
WSClient 长连接 -> EventDispatcher im.message.receive_v1 -> GatewayIngestor -> SQLite/LanceDB RAG
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Gateway 层只负责接收和归一化事件,不直接参与 RAG 答案生成,避免平台细节污染知识库和检索层。
|
|
285
|
+
|
|
286
|
+
当消息命中 `@ChatterCatcher` 时,Gateway 会在入库后触发问答流程,但检索时必须排除本次提问消息,避免把问题本身当作证据。回复链路为:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
@消息 -> 入库 -> 排除当前消息 -> 混合检索 -> askWithRag -> 飞书 message.create 回复群聊
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
必需事件:
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
im.message.receive_v1
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
必需行为:
|
|
299
|
+
|
|
300
|
+
- 群聊默认需要 `@机器人` 才回答。
|
|
301
|
+
- 除非配置禁用,否则所有消息仍会被捕获。
|
|
302
|
+
- 私聊可以后续支持。
|
|
303
|
+
|
|
304
|
+
## 配置
|
|
305
|
+
|
|
306
|
+
配置必须能从这些入口编辑:
|
|
307
|
+
|
|
308
|
+
- `chattercatcher setup`
|
|
309
|
+
- `chattercatcher settings`
|
|
310
|
+
- 本地 Web UI
|
|
311
|
+
|
|
312
|
+
关键字段:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"feishu": {
|
|
317
|
+
"domain": "feishu",
|
|
318
|
+
"appId": "",
|
|
319
|
+
"groupPolicy": "open",
|
|
320
|
+
"requireMention": true
|
|
321
|
+
},
|
|
322
|
+
"llm": {
|
|
323
|
+
"baseUrl": "",
|
|
324
|
+
"model": ""
|
|
325
|
+
},
|
|
326
|
+
"embedding": {
|
|
327
|
+
"baseUrl": "",
|
|
328
|
+
"model": "",
|
|
329
|
+
"dimension": null
|
|
330
|
+
},
|
|
331
|
+
"storage": {
|
|
332
|
+
"dataDir": "~/.chattercatcher/data"
|
|
333
|
+
},
|
|
334
|
+
"web": {
|
|
335
|
+
"host": "127.0.0.1",
|
|
336
|
+
"port": 3878
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
secrets 不得存入该文件。
|
|
342
|
+
|
|
343
|
+
## 运维命令
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
chattercatcher setup
|
|
347
|
+
chattercatcher settings
|
|
348
|
+
chattercatcher doctor
|
|
349
|
+
chattercatcher gateway start
|
|
350
|
+
chattercatcher gateway status
|
|
351
|
+
chattercatcher gateway stop
|
|
352
|
+
chattercatcher gateway restart
|
|
353
|
+
chattercatcher logs --follow
|
|
354
|
+
chattercatcher logs --lines 200 --file gateway.log
|
|
355
|
+
chattercatcher index rebuild
|
|
356
|
+
chattercatcher process messages
|
|
357
|
+
chattercatcher export --out ./backup.json
|
|
358
|
+
chattercatcher restore ./backup.json --replace
|
|
359
|
+
chattercatcher data delete message <messageId> --yes
|
|
360
|
+
chattercatcher data delete file <messageId> --yes
|
|
361
|
+
chattercatcher data delete chat <chatId> --yes
|
|
362
|
+
chattercatcher web start
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`gateway start` 以前台进程运行,并在 `~/.chattercatcher/gateway.pid` 写入运行记录。`gateway stop` 读取该 PID 文件发送停止信号;如果 PID 已过期,会清理陈旧记录。后台服务安装仍属于 M3 的 service 能力。
|
|
366
|
+
|
|
367
|
+
`restore` 默认合并导入导出文件中的 chats、messages、message_chunks 和 file_jobs,并重建 SQLite FTS。只有显式传入 `--replace` 时才会先清空当前本地知识库;恢复后如果使用 LanceDB 语义检索,需要运行 `index rebuild` 重建向量。
|
|
368
|
+
|
|
369
|
+
`data delete` 删除 SQLite 知识库记录、关联 chunks、SQLite FTS 条目和文件解析任务。删除文件知识源时,只会清理位于 `storage.dataDir` 内的本地保存文件,不会删除外部源文件。删除后如果使用 LanceDB 语义检索,需要运行 `index rebuild` 清理向量侧残留。
|
|
370
|
+
|
|
371
|
+
`process messages` 立即运行消息索引处理。SQLite FTS 在消息入库时已经即时更新;该命令主要用于立刻把消息 chunks 写入 LanceDB 向量索引,等价于手动触发原本可由定时任务承担的处理动作。Web UI 首页的“立即处理”按钮调用同一条 API。
|
|
372
|
+
|
|
373
|
+
## 测试策略
|
|
374
|
+
|
|
375
|
+
分层测试:
|
|
376
|
+
|
|
377
|
+
- 纯逻辑单测:
|
|
378
|
+
- 配置校验。
|
|
379
|
+
- 消息归一化。
|
|
380
|
+
- 切块。
|
|
381
|
+
- 冲突处理。
|
|
382
|
+
- 引用格式。
|
|
383
|
+
- 集成测试:
|
|
384
|
+
- SQLite migration。
|
|
385
|
+
- 向量库 adapter。
|
|
386
|
+
- 索引任务。
|
|
387
|
+
- mock 飞书事件。
|
|
388
|
+
- mock OpenAI-compatible provider。
|
|
389
|
+
- UI 测试:
|
|
390
|
+
- settings 页面。
|
|
391
|
+
- history 页面。
|
|
392
|
+
- 文件库。
|
|
393
|
+
- 问答日志。
|
|
394
|
+
|
|
395
|
+
手工 smoke test:
|
|
396
|
+
|
|
397
|
+
- 全新 setup。
|
|
398
|
+
- Gateway start。
|
|
399
|
+
- 消息捕获。
|
|
400
|
+
- `@机器人` 触发回答。
|
|
401
|
+
- 重建索引。
|
|
402
|
+
- Web UI 检查。
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chattercatcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "本地优先的飞书/Lark 家庭群知识库机器人",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"homepage": "https://github.com/FlashingChen2024/chattercatcher#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/FlashingChen2024/chattercatcher.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/FlashingChen2024/chattercatcher/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"chattercatcher": "dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"assets",
|
|
21
|
+
"dist",
|
|
22
|
+
"docs",
|
|
23
|
+
"README.md",
|
|
24
|
+
"AGENTS.md"
|
|
25
|
+
],
|
|
26
|
+
"directories": {
|
|
27
|
+
"doc": "docs"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsx src/cli.ts",
|
|
32
|
+
"lint": "tsc --noEmit",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "vitest run"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"feishu",
|
|
38
|
+
"lark",
|
|
39
|
+
"rag",
|
|
40
|
+
"local-first",
|
|
41
|
+
"knowledge-base"
|
|
42
|
+
],
|
|
43
|
+
"author": "FlashingChen2024",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@inquirer/prompts": "^8.4.2",
|
|
47
|
+
"@lancedb/lancedb": "^0.27.2",
|
|
48
|
+
"@larksuiteoapi/node-sdk": "^1.62.0",
|
|
49
|
+
"better-sqlite3": "^12.9.0",
|
|
50
|
+
"commander": "^14.0.3",
|
|
51
|
+
"fastify": "^5.8.5",
|
|
52
|
+
"mammoth": "^1.12.0",
|
|
53
|
+
"pdf-parse": "^2.4.5",
|
|
54
|
+
"pino": "^10.3.1",
|
|
55
|
+
"zod": "^4.3.6"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
59
|
+
"@types/node": "^25.6.0",
|
|
60
|
+
"@types/yazl": "^3.3.1",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"tsx": "^4.21.0",
|
|
63
|
+
"typescript": "^6.0.3",
|
|
64
|
+
"vitest": "^4.1.5",
|
|
65
|
+
"yazl": "^3.3.1"
|
|
66
|
+
}
|
|
67
|
+
}
|