这几天高强度开发了一个叫 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)

  1. 自定义协议 memos-asset://:把图片 URL 改写,在主进程用 protocol.handle 代理并注入 token。失败——registerSchemesAsPrivileged 必须在 app.ready 之前调用,且 standard: true 标志缺失导致 Chromium 不识别。
  2. IPC 代理:渲染进程通过 IPC 让主进程下载图片转 base64,再 DOM 替换 img src。失败——时序问题。
  3. 预处理替换为 base64:DOMPurify 之前把 URL 替换为 data URI。失败。
  4. 本地下载缓存 + 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/朝夕见闻志⚡️ 的更多信息

订阅后即可通过电子邮件收到最新文章。