这几天高强度开发了一个叫 ACKS Studio 的 Electron 桌面 AI Agent 客户端,记录一下整个过程。
项目简介
ACKS Studio 是基于 Electron 32 + React 18 + Vite 5 构建的桌面应用,核心功能是管理和运行 Hermes AI Agent 任务。整体架构:
- 主进程:Electron main.js + SQLite(better-sqlite3)持久化
- 渲染进程:React SPA,通过 preload.js IPC 与主进程通信
- 后端:本地运行的 Hermes Agent 服务(SSE 流式输出)
功能开发时间线
v0.x 阶段:核心框架
最早的版本建立了整个应用骨架:Rail 导航栏、Tasks 面板、ChatPanel 实时 SSE 流、Kanban 看板、ArtifactsPanel。这阶段解决了几个关键技术问题:
- Hermes CORS 问题:用
webRequest.onBeforeSendHeaders去掉 Origin 头,onHeadersReceived注入 CORS 响应头 - SQLite 持久化:所有任务、消息、产物通过
dbSaveState/dbLoadAll双向同步 - 键盘快捷键:Cmd+N 新建任务,Cmd+1~8 导航,Cmd+[/] 切换任务
v0.5–v0.6:笔记本 + 知识库
这是一个大版本。参照 Memos 的设计哲学,把笔记功能重新设计为流式捕获模式——打开即写,#标签 自动提取,内容第一行自动成为标题。
同时加入了知识库功能:挂载本地 Markdown 文件夹(兼容 Obsidian),支持关键词检索和向量语义检索(bge-m3 + sqlite-vec)。
这阶段还完成了:
- 生图面板:接入 DALL-E 3,会话历史,Prompt 模板管理
- 卡片工坊:iframe 嵌入小红书卡片生成器,AI 文案助手一键导入
- 自动更新:electron-updater 集成
期间修复了一个隐蔽的 Bug——任务标签重启后丢失。根因是创建时存 { tag: "bug" } 但 DB schema 是 tags TEXT(JSON数组),导致读取时 task.tags 为 undefined,保存为 []。全链路统一为 tags: string[] 后解决。
v1.0.0:Memos 双向同步
实现了与自托管 Memos 实例的双向同步:
- PUSH:本地改动 → Memos API
- PULL:拉取 Memos 变更 → 本地 SQLite
- 增量同步:CEL filter
update_time >= timestamp(...)+ nextPageToken 分页 - Settings 页面配置 URL + Token,支持测试连接
最大的坑:Memos 图片显示(v1.0.1 到 v1.0.9,8小时)
这是这几天最痛苦的部分,值得单独记录。
问题描述
同步回来的 Memos 笔记中,图片全部显示为破裂图标。
根本原因
Memos 的私有图片 URL(如 https://memos.example.com/file/attachments/{uid}/{filename})需要 Authorization: Bearer token 才能访问。在 Memos 网页版里,浏览器有 session,所以能正常加载。但在 Electron 渲染进程里,<img src="https://..."> 是普通 HTTP 请求,不携带任何认证信息。
走过的弯路(v1.0.1 ~ v1.0.8)
- 自定义协议 memos-asset://:把图片 URL 改写,在主进程用
protocol.handle代理并注入 token。失败——registerSchemesAsPrivileged必须在app.ready之前调用,且standard: true标志缺失导致 Chromium 不识别。 - IPC 代理:渲染进程通过 IPC 让主进程下载图片转 base64,再 DOM 替换 img src。失败——时序问题。
- 预处理替换为 base64:DOMPurify 之前把 URL 替换为 data URI。失败。
- 本地下载缓存 + localfile:// 协议(v1.0.7/v1.0.8):Sync 时把图片下载到本地,内容存 localfile:// URL。思路对,但下载 URL 用了错误的路径
/o/r/(服务器返回 HTML),正确路径是/file/attachments/。修正后图片依然不显示——因为整个下载方向就是多余的。
最终解法(v1.0.9)
关键转折点是重新思考问题本质:我们需要的不是搬运图片,而是让 Electron 的 Chromium 发请求时带上 token。
Electron 提供了 session.defaultSession.webRequest.onBeforeSendHeaders API,可以在渲染进程发出任何 HTTP 请求前拦截并注入 header:
session.defaultSession.webRequest.onBeforeSendHeaders(
{ urls: ["http://*/*", "https://*/*"] },
(details, callback) => {
const headers = { ...details.requestHeaders }
if (memosInterceptConfig && new URL(details.url).host === memosInterceptConfig.host) {
headers["Authorization"] =
}
callback({ requestHeaders: headers })
}
)
就这几行。内容里保留原始 https:// URL,Electron 自动注入 Bearer token,图片直接加载——完全复现了 Memos 网页版的行为。同时删掉了所有下载逻辑,代码量大幅减少。
教训
复杂的方案(下载缓存、自定义协议、IPC 代理)都是在绕开问题,而不是解决问题。正确的思路是:想清楚问题的本质(认证),然后用平台提供的最简单的机制来解决它。在这个案例里,兜兜转转 8 小时、发布十几个版本,最终的修复只有不到 20 行代码。
顺带解决的问题
Memos 的 Nginx 没有配置 client_max_body_size,上传图片时触发 HTTP 413 错误。在 Nginx 配置里加一行 client_max_body_size 20m; 解决。
当前状态:v1.0.9
功能完整,图片正常显示,Memos 双向同步稳定运行。整个项目从零到现在历经数十个版本,核心功能已全部落地。
记录时间:2026年5月18日
了解 ~/jintaoblog/朝夕见闻志⚡️ 的更多信息
订阅后即可通过电子邮件收到最新文章。
