1. 周日深夜的 47 个收藏夹,和那个找了 23 分钟的问题
InsightDeck 是一个跑在 Electron 桌面端的个人知识中枢,它要做的事情很具体:把你散落在飞书、Notion、微信收藏、Chrome 书签、本地 PDF 里的内容,统一抽取、向量化、本地索引,然后用一个 ⌘K 搜索框在 1 秒内跨源问答。适合谁?适合那些收藏夹已经堆到几十个文件夹、每次找东西都要翻历史记录、又不想把私人笔记全丢进云端的人。
我试过的起点很狼狈。周日 23:47,我想翻一篇上周看过的「Electron 内存泄漏排查」文章。微信收藏翻了 6 屏没看到,Chrome 书签栏被 47 个未分类文件夹埋了,飞书只有标题没正文,Notion 搜出来 9 条都不是。最后在浏览器历史记录里翻到它,已经过去 23 分钟。真正的痛点不是「找不到」,而是找的过程消耗掉了读那篇文章本来要解决的问题的时间。
这篇复盘按 5 天日记体走,重点交付四样可复制的东西:一份 AGENTS.md 骨架、MCP 配置片段、Electron 侧 settings.json 示例,以及 1 秒问答延迟的验证动作和收藏夹导入检查清单。编码入口我用的是华为云码道(CodeArts),配合 AGENTS.md 约定和 MCP 接入,把 47 个收藏夹炼成一个本地知识脑。
先说结论性的数字,方便你判断值不值得跟:5 个工作日、一个人、总 Token 消耗约 413K、最终产出 macOS/Windows 双端安装包 + 一个 MV3 浏览器扩展 + 一个 sqlite-vss 本地向量库。下面按天拆。
2. 前置准备:TaoToken 与码道的分工,别混着用
在动手前先把两个东西的定位理清楚,否则后面配置会乱。
华为云码道(CodeArts)在这套流程里扮演的是「项目经理 + 架构师 + 多个工程师」的复合角色,它读 AGENTS.md、跑子代理、并行多任务。而模型调用这一层,我用 TaoToken 来做统一的 API 入口,好处是模型对话、coding-plan、API Key 管理都在一个控制台里,切换模型不用改一堆环境变量。
你需要先拿到两样东西:
第一,TaoToken 的 API Key。进控制台创建,地址是 https://taotoken.net/api-keys ,创建后复制保存,后面 Electron 侧和 MCP 配置都要用。
第二,确认你要用的模型。如果你只是想让 InsightDeck 的问答链路跑通,用模型对话页先验证一下模型能不能正常返回,地址 https://taotoken.net/models 。如果你打算长期用码道做编码和 Agent 任务,建议直接上 Coding Plan,地址 https://taotoken.net/coding-plan ,按周期计费比按量更可控。
接入文档在这里,配置参数、base_url、鉴权方式都写得很清楚:https://taotoken.net/doc 。API 的基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接填进配置里。
注意:API Key 只放在主进程或环境变量里,绝对不要写进渲染进程代码,也不要提交到 git。这一点后面 AGENTS.md 里会作为硬规则写死。
环境上你需要:Node.js 20+、pnpm、一个能跑 Electron 31 的机器。华为云码道 IDE 装好,登录后新建工程 InsightDeck。
3. 可复制配置:AGENTS.md 骨架 + MCP 片段 + settings.json
这一节是全文最该抄的部分。三份配置我都给可直接用的版本。
3.1 AGENTS.md 骨架(9 节硬规则)
AGENTS.md 是码道的「员工手册」,进门先读再干活。写得越严,生成代码越准。我第一版只写了一句「使用 TypeScript」,结果到处是 any。下面是沉淀后的骨架:
# AGENTS.md — InsightDeck 项目规约 ## 1. 项目定位 Electron 31 桌面端知识中枢,本地优先,云端可选同步。 ## 2. Hard Rules(违反即拒绝合并) 1. 全栈 TypeScript 5.4+,禁止 any(unknown 除外) 2. 主进程/渲染进程通过 contextBridge + ipcRenderer.invoke 通信, 禁止 nodeIntegration: true 3. 耗时 > 100ms 的任务必须放进 worker_threads,禁止阻塞 UI 主线程 4. 向量库走 better-sqlite3 + sqlite-vss,禁止任何 ORM 5. 云端调用统一走 src/main/cloud/ 网关,禁止在渲染进程持有 AK/SK 6. 错误兜底:云端 Embedding 失败 → 自动降级本地 ONNX bge-small-zh-v1.5 7. 任何写入用户磁盘的代码必须先 app.getPath('userData') 8. 日志使用 electron-log,禁止 console.log;敏感词必须 *** 脱敏 ## 3. 子代理表 @architect 架构与 ADR / @main 主进程 / @renderer 渲染进程 @ext 浏览器扩展 / @cloud 云端网关 / @reviewer 只读评审 ## 4. 记忆策略 包管理器 pnpm;commit 格式 type(scope): subject; 口语映射:跑一下=pnpm dev,打个包=pnpm build && pnpm electron:make ## 5. 多任务并行守则 同一文件禁止并发修改;冲突时暂停后到任务,等待裁决。 ## 6. 安全合规 扩展仅请求 activeTab;preload 只暴露收紧后的方法。 ## 7. 包体积 浏览器扩展产物 < 200KB。 ## 8. 流式规范 禁止 setInterval 伪流式,必须用 async iterator / ReadableStream。 ## 9. 三方 OpenAPI 前置检查 调用任何 SaaS OpenAPI 前先拉最新 endpoint,检查 deprecation, 并在 docs/adr/ 留一条 ADR 记录 endpoint 与抓取时间。3.2 MCP 配置片段
MCP 的价值是让码道读到的是你账号下真实的资源,而不是通用模板。下面是我接入对象存储和 Embedding 网关的配置片段,放在码道的 MCP 配置里:
{ "mcpServers": { "insightdeck-vault": { "command": "npx", "args": ["-y", "@your-scope/obs-mcp"], "env": { "OBS_BUCKET": "insightdeck-vault", "OBS_REGION": "cn-north-4", "OBS_ENDPOINT": "https://obs.cn-north-4.myhuaweicloud.com", "OBS_KMS_KEY_ID": "alias/insightdeck" } }, "taotoken-gateway": { "command": "npx", "args": ["-y", "@your-scope/openai-mcp"], "env": { "OPENAI_BASE_URL": "https://taotoken.net/api", "OPENAI_API_KEY": "${TAOTOKEN_API_KEY}" } } } }配好之后,你对码道说「把本地 db 增量同步到 OBS」,它生成的代码里桶名、region、KMS Key 都是真实值,第一次就能跑。
3.3 Electron 侧 settings.json 示例
这是应用运行时的配置,放在app.getPath('userData')/settings.json,由 electron-store 管理:
{ "cloud": { "enabled": true, "provider": "taotoken", "baseUrl": "https://taotoken.net/api", "embeddingModel": "bge-large-zh", "timeoutMs": 8000, "fallbackToLocal": true }, "index": { "chunkSize": 512, "chunkOverlap": 64, "vectorDim": 768, "workerThreads": 2 }, "sync": { "mode": "hourly", "obsBucket": "insightdeck-vault", "obsRegion": "cn-north-4", "kmsKeyId": "alias/insightdeck" }, "ui": { "hotkey": "CommandOrControl+K", "showOfflineBadge": true } }注意timeoutMs: 8000和fallbackToLocal: true这两项,它们是 1 秒问答体验的保险丝——云端卡住时迅速切本地,UI 不会转圈到天荒地老。
4. 验证请求:从收藏到 1 秒问答的完整链路
配置写完,接下来验证链路能不能跑通。分三步。
4.1 先验证模型调用通不通
在终端里直接打一发,确认 API Key 和 base_url 没问题:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "回复 OK 两个字母"}] }'返回里有choices[0].message.content就说明通了。这一步别跳过,后面 Electron 里报错时你能快速判断是网络层还是应用层的问题。
4.2 验证向量库写入与检索
主进程里向量库用三张表分离,结构化元数据和向量解耦:
CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, source_id TEXT NOT NULL, title TEXT, url TEXT, created_at INTEGER NOT NULL, UNIQUE(source, source_id) ); CREATE TABLE IF NOT EXISTS chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, doc_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, ord INTEGER NOT NULL, content TEXT NOT NULL, token_cnt INTEGER NOT NULL ); CREATE VIRTUAL TABLE IF NOT EXISTS vss_chunks USING vss0(embedding(768));UNIQUE(source, source_id)保证幂等,重复导入同一个收藏不会爆库。写入用事务加预编译:
const tx = db.transaction((rows: ChunkInsert[]) => { for (const r of rows) { const info = insChunk.run(r.docId, r.ord, r.content, r.tokenCnt); insVss.run(info.lastInsertRowid, Buffer.from(r.embedding.buffer)); } }); tx(items);4.3 验证 1 秒问答延迟
这是最关键的验证动作。在渲染进程里对search:ask通道计时:
const t0 = performance.now(); let firstChunkAt = 0; for await (const chunk of askStream(q)) { if (!firstChunkAt) firstChunkAt = performance.now(); appendToAnswer(chunk); } const total = performance.now() - t0; console.log(`首字 ${(firstChunkAt - t0).toFixed(0)}ms / 完成 ${total.toFixed(0)}ms`);实测下来,本地向量库 1.2 万块、768 维的情况下,首字延迟稳定在 600–900ms,完整回答 1.5–2.5 秒。如果你首字超过 1.5 秒,八成是 Embedding 没走缓存或者 worker 线程数配少了。
4.4 收藏夹导入检查清单
导入 47 个收藏夹时,按这个清单逐项过:
| 检查项 | 通过标准 | 常见问题 |
|---|---|---|
| 来源去重 | 同一 URL 只入库一次 | source_id 没归一化 |
| 正文抽取 | 无导航/页脚残留 | 未剔除 nav/footer |
| 分块大小 | 512 token ± 10% | 中文按字符切导致超长 |
| 向量维度 | 768 与模型对齐 | 本地降级 384 维需 padding |
| 幂等写入 | 重复导入不增行 | 缺 UNIQUE 约束 |
| 引用可点 | 回答里链接能跳原文 | url 字段为空 |
5. 本篇常见错排查
下面这几个坑我都真实踩过,按报错现象对号入座。
报错一:Error: Cannot find module 'better-sqlite3'打包后启动失败。原生模块没 rebuild。在 electron-builder 配置里补两行:
buildDependenciesFromSource: true nodeGypRebuild: true报错二:渲染进程报require is not defined。你在渲染进程里直接用了 Node API。检查webPreferences是否contextIsolation: true且nodeIntegration: false,所有能力通过 preload 的 contextBridge 暴露。
报错三:问答一直转圈,最后超时。云端 Embedding 卡住且没降级。检查fallbackToLocal是否为 true,以及本地 ONNX 模型路径是否正确。降级逻辑要包在 try/catch 里:
try { const v = await callCloud(text); return { vector: v, source: 'cloud' }; } catch (err) { log.warn('云端失败,降级本地:', (err as Error).message); return { vector: await embedLocal(text), source: 'local' }; }报错四:多任务并行时两个子代理改了同一个文件。这是 AGENTS.md 第 5 节没写清楚。补上「同一文件禁止并发修改」,码道会在冲突时主动暂停后到任务并告警。
报错五:飞书/Notion 接口 404。三方 OpenAPI 端点迁移了。别硬猜,先拉最新文档确认 endpoint,再在 docs/adr/ 留一条记录,避免下次又踩。
报错六:扩展包体积超标。别引入 Readability 这类大库,用极简正文提取,剔除 nav/footer/script/style 后取article或main的 innerText 即可。
排障和接入相关的完整参数,建议对照接入文档逐项核:https://taotoken.net/doc 。API Key 管理在 https://taotoken.net/api-keys ,模型可用性在 https://taotoken.net/models 先验证再写进配置。
6. 把 47 个收藏夹变成 1 秒可问答的桌面知识脑
5 天下来,最直观的变化是:周日深夜那个找了 23 分钟的问题,现在 ⌘K 输入后 0.8 秒命中,引用清晰,点一下跳回原文。
这套打法的核心不是「AI 帮我写代码」,而是把工程纪律沉淀成 AGENTS.md,让码道按规则跑;把真实资源通过 MCP 接进来,让生成的代码第一次就能执行;把流程用 Skills 编排成蓝图,新增数据源时只写适配器。三条建议给想跟做的人:AGENTS.md 是灵魂不是文档,至少写硬规则、子代理表、记忆策略、多任务守则、安全合规五节;子代理按边界切而不是按语言切,Electron 的真实边界是进程边界加部署边界;多任务并行的关键不是开多少,而是契约多严,一份 IPC 契约表能让两个子代理生成的字段名一字不差。
如果你也想长期用这套流程做编码和 Agent 任务,Coding Plan 比按量更省心:https://taotoken.net/coding-plan 。想先验证模型效果,直接去模型对话页试:https://taotoken.net/models 。配置过程中卡在鉴权或参数上,接入文档和 API Key 页面基本能解决:https://taotoken.net/doc 、https://taotoken.net/api-keys 。