Agent Dashboard 开发笔记:从零构建本地多 Agent 桌面应用
一个基于 Electron + React 的本地多 Agent 桌面应用的架构设计与踩坑记录。
项目是什么
Agent Dashboard 是一个本地运行的多 Agent 桌面客户端。它不是又一个 ChatGPT wrapper,而是一个 Agent 运行时——每个 Agent 有自己的人设(SOUL.md)、能力说明(AGENTS.md)、工具约定(TOOLS.md)和技能目录(skills/),通过 pi-agent-core 驱动 LLM,支持工具调用、子任务委派、会话持久化。
核心定位:让非技术用户也能通过配置文件定义自己的 Agent,开箱即用。
技术栈选型
| 层 | 选型 | 理由 |
|---|---|---|
| 桌面框架 | Electron 33 + Electron Forge | 跨平台(macOS/Windows),生态成熟 |
| 前端 | React 18 + Zustand + Tailwind CSS | 轻量状态管理,原子化 CSS |
| 构建 | Vite 6 | 快速 HMR,Electron Forge 原生支持 |
| 数据库 | better-sqlite3(WAL 模式) | 零配置、嵌入式、高性能 |
| 会话持久化 | JSONL 文件 | 流式追加,适合 LLM 对话场景 |
| LLM 驱动 | pi-agent-core / pi-ai | 自研 Agent 框架,支持多 Provider |
| 测试 | Vitest + happy-dom | 快速,原生 ESM 支持 |
架构设计
整体分层
┌─────────────────────────────────────────────────┐
│ Renderer (React) │
│ App.tsx → Sidebar + ChatArea + InputBar │
│ Stores: chatStore / conversationStore / ... │
└──────────────────────┬──────────────────────────┘
│ IPC (contextBridge)
┌──────────────────────┴──────────────────────────┐
│ Main Process (Node) │
│ index.ts → App Lifecycle │
│ ipc/*.handler.ts → 按 domain 拆分 │
│ agent-runner.ts → AgentHarness 生命周期 │
│ system-prompt.ts → Sectioned Prompt Pipeline │
│ tools/*.ts → Agent 工具(沙箱化) │
│ db/ → SQLite + Migration │
│ session-manager.ts → JSONL 持久化 │
└─────────────────────────────────────────────────┘
IPC Handler 按 Domain 拆分
主入口 index.ts 只有 110 行。所有 IPC handler 按业务域拆到独立文件:
src/main/ipc/
├── agent.handler.ts # Agent CRUD
├── chat.handler.ts # 聊天消息流
├── conversation.handler.ts # 会话管理
├── model.handler.ts # 模型 Provider 配置
├── settings.handler.ts # 应用设置
├── file.handler.ts # 文件操作
├── backup.handler.ts # 备份恢复
├── license.handler.ts # 授权验证
└── index.ts # 统一注册入口
每个 handler 导出 register(deps: IpcDependencies): void,由 ipc/index.ts 统一调用。新增 domain 只需新建文件 + 加一行注册。
教训:曾经所有 handler 写在 index.ts,膨胀到 597 行的”上帝模块”,维护噩梦。
System Prompt 的 Sectioned Array Pipeline
系统提示词不是一坨模板字符串,而是sectioned array pipeline:
interface PromptSection {
id: string;
render: (ctx: PromptContext) => string | null;
}
每个 section 是一个小函数,返回 markdown 片段或 null(跳过)。添加新 section 只需往数组追加一个元素,不用改 if/else 链。
好处:
- 新增 section 是一行代码
- 缺少 SOUL.md 时对应 section 返回
null,无需条件判断 - Orchestrator 和普通 Agent 共享同一套 base sections,行为一致
Agent 双源扫描
Agent 来自两个目录:
- 内置:项目
agents/目录(只读,打包后不可写) - 用户:
~/.agentdashboard/agents/(可读写)
同 ID 时用户版本覆盖内置版本。编辑内置 Agent 时自动 copy-on-write(先复制到用户目录再编辑)。
安全设计:工具沙箱
Agent 能执行 bash 命令、读写文件,安全是重中之重。
路径沙箱
所有文件系统操作都限制在 workspace 目录内:
// path-utils.ts
function isWithinDir(filePath: string, dir: string): boolean {
if (path.isAbsolute(filePath)) return false; // 绝对路径直接拒绝
const resolved = path.resolve(dir, filePath);
const relative = path.relative(dir, resolved);
return !relative.startsWith('..') && !path.isAbsolute(relative);
}
关键决策:绝对路径直接拒绝,不静默重写。曾经用 path.resolve 把绝对路径拼成相对路径,结果沙箱失效。
webContents.send 安全
Agent 运行是异步的,用户可能随时关闭窗口。window.webContents.send() 在窗口销毁后会抛 Object has been destroyed。
解决方案:safeSend() 包裹所有 send 调用:
function safeSend(window: BrowserWindow | null | undefined, channel: string, ...args: unknown[]): boolean {
if (!window || window.isDestroyed?.()) return false;
try {
window.webContents.send(channel, ...args);
return true;
} catch (err) {
console.warn(`safeSend dropped payload on '${channel}':`, err);
return false;
}
}
License 分级控制
不同工具按 tier 分级:
- free:bash、read_file、write_file、web_search、web_fetch、memory_search、read_document
- pro+:grok_search、browser_control、delegate(子任务委派)
License 使用 RSA 签名验证,设备指纹基于 hostname + username + CPU model + platform(排除了不可靠的 MAC 地址)。
内嵌 Python 运行时
Agent 需要执行 Python 脚本,但用户不应该被要求安装 Python。
方案:应用内嵌 Python 3.12,预装常用包(requests、beautifulsoup4、pandas 等)。
// python-env.ts
function findEmbeddedPython(): string | null {
// 1. 生产环境:process.resourcesPath/python/
// 2. 开发环境:scripts/python-cache/python-{platform}/
// 3. 未找到:回退系统 Python
}
用户可以通过 pip install --target ~/.agentdashboard/python-packages 安装额外包,PYTHONPATH 已自动配置。
跨平台:支持 macOS (arm64 + x64) 和 Windows (x64),打包时自动从缓存复制到应用内。
Native Module 的坑
better-sqlite3 是 C++ 编译的原生模块,必须匹配当前 Node ABI 版本。问题是开发和测试使用不同的 Node 运行时:
- Electron:使用 Electron 内置的 Node
- Vitest:使用系统 Node
两者的 NODE_MODULE_VERSION 不同,binary 不能共存。
错误做法:用 postinstall 自动跑 electron-rebuild——会把 binary 编译成 Electron 版本,导致测试跑不了。
正确做法:手动切换:
- 跑 Electron 前:
npm run rebuild - 跑测试前:
npx node-gyp rebuild --directory=node_modules/better-sqlite3 --build-from-source
打包时 Electron Forge 会自动用 Electron headers 重编译,产物放在 app.asar.unpacked/。
数据持久化:SQLite + JSONL 双层
SQLite(结构化数据)
-- 5 张表,干净的关系模型
agents -- Agent 配置
model_providers -- LLM Provider(OpenAI / Anthropic / Ollama / 自定义)
conversations -- 会话元数据
messages -- 消息记录
app_settings -- 键值对设置
Migration 用版本化机制,版本号存在 app_settings 表中(而非 SQLite 的 user_version pragma),方便 SQL join 查询。
JSONL(会话流)
LLM 对话天然适合 JSONL——每行一个 JSON 对象,流式追加,不需要事务。session-manager.ts 封装了 JsonlSessionRepo,通过 Node.js FileSystem adapter 对接。
测试策略
项目有 17 个测试文件,覆盖:
| 类型 | 测试内容 |
|---|---|
| 工具测试 | bash、read_file、write_file、web_search、grok_search |
| 沙箱测试 | 路径穿越、绝对路径、符号链接 |
| Agent 测试 | agent-runner、agent-discovery |
| 系统提示词 | Snapshot 测试(每次改动都能 diff) |
| 数据库 | 新建 DB / 升级旧 DB / 幂等性 |
| IPC Handler | agent / chat / conversation / model handler |
| License | 授权验证、设备指纹 |
| Store | chatStore 状态管理 |
沙箱 Fuzz 测试尤其重要——测试用例覆盖了绝对路径、../ 穿越、null byte 注入等攻击向量。
记忆蒸馏:/distill 命令
Agent 在长期对话中会积累大量记忆笔记(workspace/memory/ 下的日期笔记)。/distill 命令实现了一个 6 步蒸馏流程:
- 读取 workspace/MEMORY.md
- 列出所有日期笔记
- 分类每条笔记:promote(提升到 MEMORY.md)/ keep / remove
- 回写 MEMORY.md
- 删除过期笔记
- 流式输出蒸馏结果
这解决了 LLM 长期记忆的”遗忘”问题——重要信息被提升到持久记忆,临时信息被清理。
写给自己的教训
1. 安全不能事后补
路径沙箱、webContents.send 安全、zip-slip 防护——这些都是在代码审查中发现的 P0 问题。安全设计必须在第一行代码之前就定好。
2. 模块拆分要趁早
597 行的 index.ts 上帝模块是最大的技术债。IPC handler 按 domain 拆分后,每个文件 50-100 行,职责清晰。
3. Native Module 是 Electron 的坑
better-sqlite3 的 ABI 兼容问题花了大量时间。记住:开发和测试的 Node 运行时不同,binary 不能共存。
4. 测试是安全网
沙箱 Fuzz 测试发现了多个路径穿越漏洞。Snapshot 测试让 system prompt 的每次改动都可审计。没有测试的安全代码是不可信的。
5. 配置优于硬编码
Agent 的人设、能力、工具都通过 Markdown 文件配置,不需要改代码。这让非技术用户也能定义自己的 Agent。
总结
Agent Dashboard 的核心设计哲学:
- Agent 即配置:Markdown 文件定义一切,代码是运行时
- 安全优先:沙箱、签名、白名单,违反即 P0
- 模块化:IPC handler 按 domain 拆分,system prompt 按 section 组装
- 双层持久化:SQLite 存结构化数据,JSONL 存对话流
- 内嵌运行时:Python 内嵌,用户零配置
这是一个仍在活跃开发的项目,还有很多待完善的地方(更多 Agent 类型、更好的 UI/UX、更多工具支持)。但架构基础已经打好,后续扩展是增量式的。