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 Handleragent / chat / conversation / model handler
License授权验证、设备指纹
StorechatStore 状态管理

沙箱 Fuzz 测试尤其重要——测试用例覆盖了绝对路径、../ 穿越、null byte 注入等攻击向量。

记忆蒸馏:/distill 命令

Agent 在长期对话中会积累大量记忆笔记(workspace/memory/ 下的日期笔记)。/distill 命令实现了一个 6 步蒸馏流程:

  1. 读取 workspace/MEMORY.md
  2. 列出所有日期笔记
  3. 分类每条笔记:promote(提升到 MEMORY.md)/ keep / remove
  4. 回写 MEMORY.md
  5. 删除过期笔记
  6. 流式输出蒸馏结果

这解决了 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 的核心设计哲学:

  1. Agent 即配置:Markdown 文件定义一切,代码是运行时
  2. 安全优先:沙箱、签名、白名单,违反即 P0
  3. 模块化:IPC handler 按 domain 拆分,system prompt 按 section 组装
  4. 双层持久化:SQLite 存结构化数据,JSONL 存对话流
  5. 内嵌运行时:Python 内嵌,用户零配置

这是一个仍在活跃开发的项目,还有很多待完善的地方(更多 Agent 类型、更好的 UI/UX、更多工具支持)。但架构基础已经打好,后续扩展是增量式的。