项目背景 链接到标题
用 Electron 封装 PI Coding Agent SDK,做一个轻量桌面 AI 聊天客户端。目标是:
- 提供 GUI 界面替代终端交互
- 跨平台(macOS + Windows)
- 集成 PI SDK 的 tool calling、流式回复能力
架构 链接到标题
Renderer (index.html)
↕ contextBridge (preload.js)
Main Process (main.js)
↕ @earendil-works/pi-coding-agent
AgentSession (Tool calling, LLM 通信)
三进程架构:
- Renderer:纯前端 HTML/CSS/JS,显示聊天界面
- Preload:contextBridge 暴露
window.pi.*API - Main:Electron 主进程,管理窗口、PI SDK、IPC 通道
IPC 通道设计:
| Channel | 方向 | 说明 |
|---|---|---|
config:get |
Renderer → Main | 读取配置 |
config:save |
Renderer → Main | 保存配置 |
session:create |
Renderer → Main | 创建 AgentSession |
session:send |
Renderer → Main | 发送 prompt |
session:abort |
Renderer → Main | 中止回复 |
pi:event |
Main → Renderer | 推送流式事件 |
PI SDK 集成 链接到标题
模型解析 链接到标题
createAgentSession 需要完整的模型对象(含 baseUrl、api 等字段),不能只传 { provider, model }:
const modelRegistry = ModelRegistry.inMemory(authStorage)
const model = modelRegistry.find(config.provider, config.model)
if (!model) throw new Error(`模型 ${config.model} 未找到`)
const result = await createAgentSession({
model,
tools: ['read', 'bash', 'edit', 'write', 'grep', 'find'],
resourceLoader: new DefaultResourceLoader({
cwd: expandHome(config.cwd),
agentDir: getAgentDir(),
}),
sessionManager: SessionManager.inMemory(),
authStorage,
})
Auth 注入 链接到标题
process.env.DEEPSEEK_API_KEY = config.apiKey
const authStorage = new AuthStorage(new InMemoryAuthStorageBackend())
authStorage.setRuntimeApiKey(config.provider, config.apiKey)
事件订阅 链接到标题
piSession.subscribe((event) => sendToRenderer('pi:event', event))
踩坑记录 链接到标题
1. 模型字段名错误 链接到标题
误传 { provider, model } 而非 { provider, id },导致 model.baseUrl 为 undefined,SDK 内部 baseUrl.includes() 崩溃。修复:用 ModelRegistry.find() 解析完整模型对象。
2. SDK 错误处理 bug 链接到标题
error-body.js 中 error.message.includes(body) 未做空值保护,原始 API 错误被掩盖,UI 显示空气泡。临时修补:加 error.message && 判断。
3. 消息气泡渲染 链接到标题
SDK 在一个 turn 内发送两次 message_start/end(assistant)——第一次含 thinking + toolCall,第二次含最终回答。方案:不在 message_end 关闭气泡,改在 agent_end 关闭。工具调用和结果合并到同一气泡,可折叠。
4. Portable 单文件 ICU 错误 链接到标题
Windows portable 单文件 exe 报 Invalid file descriptor to ICU data received。修复:在 electron-builder.yml 中配置 asarUnpack 将 ICU 数据文件解压到 asar 外部:
asarUnpack:
- "**/icudtl*"
- "**/*.dat"
UI 设计 链接到标题
- 初次配置页 vs 模态对话框重新配置
- 消息气泡:用户蓝色、助手白色
- 工具调用块:可折叠,显示工具名 + 参数预览 + 耗时 + 结果
- Markdown 渲染:本地引入 marked.js
- 智能滚动:用户到底部时跟随,向上翻看时不强制滚动
跨平台构建 链接到标题
macOS 链接到标题
npm run build:mac
# 输出 release/ElectronPi-1.0.0.dmg
无开发者签名时配置 identity: null,首次打开需右键 → 打开,或 xattr -cr /Applications/ElectronPi.app。
Windows 链接到标题
# electron-builder.yml
win:
target:
- portable
asarUnpack:
- "**/icudtl*"
- "**/*.dat"
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npx electron-builder --win
Portable 单文件 .exe,无需安装。无签名时 SmartScreen 拦截,点"更多信息 → 仍要运行"。
总结 链接到标题
- 技术栈:Electron 43 + PI SDK 0.80 + marked.js
- 核心代码不到 500 行
- 适合作为 PI SDK 集成的参考实现