适用版本: oh-my-openagent 3.17.x, OpenCode Desktop 1.14.x (Electron)
发布日期: 2026-05-07
问题状态: ✅ 已有解决方案
问题描述
安装了 oh-my-openagent 插件后,在 OpenCode CLI 中可以正常切换 Agents(Sisyphus、Hephaestus、Prometheus、Atlas 等),但在 OpenCode Desktop 中 Agents 列表为空,无法切换。
日志中出现以下错误:
ERROR service=plugin path=oh-my-openagent@3.17.15
error=Bun is not defined
failed to load plugin
或:
ERROR service=plugin path=oh-my-openagent@3.17.15
error=Only URLs with a scheme in: file, data, node, and electron are supported
by the default ESM loader. Received protocol 'bun:'
failed to load plugin
环境信息
以下为已验证的环境配置:
| 项目 | 值 |
|---|---|
| 操作系统 | Windows 11 (Build 26200) |
| Node.js | v22.22.0 |
| npm | 10.9.4 |
| Bun | 1.3.12 |
| OpenCode CLI | 1.14.39 |
| OpenCode Desktop | 1.14.39 (Electron) |
| oh-my-openagent | 3.17.15 |
关键差异:
| 运行时 | 插件运行环境 | Bun 全局对象 |
|---|---|---|
| OpenCode CLI | Bun 原生 | ✅ 可用 |
| OpenCode Desktop | Node.js (Electron in-process) | ❌ 不可用 |
根因分析
OpenCode Desktop 从 Tauri + Bun sidecar 架构迁移到了 Electron + Node.js in-process 架构。
这意味着:
- 以前:Desktop 启动一个 Bun 子进程来运行插件 →
Bun全局对象可用 - 现在:Desktop 在 Electron 主进程中直接用 Node.js 运行插件 →
Bun全局对象不存在
而 oh-my-openagent 插件使用 bun build --target bun 构建,产物中包含大量 Bun 专有 API 调用:
| API | 用途 | Node.js 等价物 |
|---|---|---|
Bun.file() | 读取文件 | fs.promises.readFile() |
Bun.write() | 写入文件 | fs.promises.writeFile() |
Bun.spawn() | 启动进程 | child_process.spawn() |
Bun.which() | 查找可执行文件 | which / where 命令 |
Bun.hash.xxHash32() | 哈希计算 | crypto.createHash() |
在 Node.js 环境中,这些调用会抛出 ReferenceError: Bun is not defined,导致整个插件加载失败。
解决方案
前提条件
- 已安装 Bun(用于从源码构建插件)
- 已克隆 oh-my-openagent 源码到本地
步骤 1:创建 Bun Polyfill 脚本
在插件源码目录创建 script/patch-bun-polyfill.ts:
#!/usr/bin/env bun
import { readFileSync, writeFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
const DIST_PATH = join(SCRIPT_DIR, "..", "dist", "index.js")
const POLYFILL_MARKER = "// __BUN_POLYFILL_INJECTED__"
const POLYFILL = `
${POLYFILL_MARKER}
if (typeof Bun === 'undefined') {
const fs = __require('fs');
const path = __require('path');
const { spawn, spawnSync, execSync } = __require('child_process');
const { createHash } = __require('crypto');
const { Readable } = __require('stream');
globalThis.Bun = {
file: (filePath) => ({
text: () => fs.promises.readFile(filePath, 'utf-8'),
arrayBuffer: () => fs.promises.readFile(filePath).then(b => b.buffer),
exists: () => fs.promises.access(filePath).then(() => true, () => false),
delete: () => fs.promises.unlink(filePath),
size: fs.statSync(filePath).size,
}),
write: (filePath, data) => fs.promises.writeFile(filePath, data),
spawn: (cmd, opts) => {
const args = Array.isArray(cmd) ? cmd : [cmd];
const proc = spawn(args[0], args.slice(1), {
...opts,
stdio: opts?.stdio || ['ignore', 'pipe', 'pipe']
});
return {
exited: new Promise((resolve, reject) => {
proc.on('close', (code) => resolve(code));
proc.on('error', reject);
}),
stdout: proc.stdout,
stderr: proc.stderr,
pid: proc.pid,
kill: (signal) => proc.kill(signal),
};
},
spawnSync: (cmd, opts) => {
const args = Array.isArray(cmd) ? cmd : [cmd];
const result = spawnSync(args[0], args.slice(1), {
...opts,
stdio: opts?.stdio || ['ignore', 'pipe', 'pipe']
});
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.status ?? 1,
pid: result.pid ?? -1,
};
},
which: (cmd) => {
try {
const result = process.platform === 'win32'
? execSync('where ' + cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
.trim().split('\n')[0]
: execSync('which ' + cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
.trim();
return result || null;
} catch { return null; }
},
hash: {
xxHash32: (data, seed) => {
const h = createHash('md5')
.update(typeof data === 'string' ? data : Buffer.from(data))
.digest();
return h.readUInt32LE(0) ^ (seed || 0);
},
},
serve: () => { throw new Error('Bun.serve not available in Node.js'); },
readableStreamToText: async (stream) => {
if (stream instanceof Readable) {
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
return Buffer.concat(chunks).toString('utf-8');
}
return new Response(stream).text();
},
sleep: (ms) => new Promise(r => setTimeout(r, ms)),
};
}
`
const original = readFileSync(DIST_PATH, "utf-8")
if (original.includes(POLYFILL_MARKER)) {
console.log("Bun polyfill already present, skipping.")
process.exit(0)
}
// 在 __require 定义之后插入 polyfill
const lines = original.split("\n")
const requireLineIndex = lines.findIndex(
l => l.includes('var __require = typeof import.meta.require')
)
if (requireLineIndex === -1) {
throw new Error("Could not find __require definition line")
}
lines.splice(requireLineIndex + 1, 0, POLYFILL)
const patched = lines.join("\n")
writeFileSync(DIST_PATH, patched, "utf-8")
console.log("Injected Bun polyfill for Node.js/Electron compatibility")
步骤 2:构建并注入 Polyfill
cd oh-my-openagent
# 安装依赖
bun install
# 构建插件(自动应用 node-require-shim)
bun run build
# 注入 Bun polyfill
bun run script/patch-bun-polyfill.ts
步骤 3:部署到 OpenCode
将构建产物复制到 OpenCode 的插件目录:
# Windows
copy dist\index.js "%USERPROFILE%.cache\opencode\packages\oh-my-openagent@3.17.15\node_modules\oh-my-openagent\dist\index.js"
copy dist\index.js "%USERPROFILE%.config\opencode\node_modules\oh-my-openagent\dist\index.js"
# macOS/Linux
cp dist/index.js ~/.cache/opencode/packages/oh-my-openagent@3.17.15/node_modules/oh-my-openagent/dist/index.js
cp dist/index.js ~/.config/opencode/node_modules/oh-my-openagent/dist/index.js
步骤 4:固定插件版本
重要:将 opencode.json 中的 @latest 改为固定版本,避免自动更新覆盖 patch:
{
"plugin": ["oh-my-openagent@3.17.15"]
}
步骤 5:重启验证
重启 OpenCode Desktop,验证:
- ✅ Agents 列表显示 Sisyphus、Hephaestus、Prometheus、Atlas
- ✅ Tab 键可以切换 Agents
- ✅
@mention可以调用 Oracle、Librarian、Explore 等
案例演示:@latest 导致 Patch 失效
问题现象
修复成功后,Desktop 重启突然又无法切换 Agents,但 CLI 仍然正常。
排查过程
检查缓存目录发现两个版本并存:
~.cache\opencode\packages\
├── oh-my-openagent@3.17.15\ ← 已 patch(有 polyfill)
└── oh-my-openagent@latest\ ← Desktop 自动创建(无 polyfill)
检查日志确认 Desktop 加载的是 @latest:
service=plugin path=oh-my-openagent@latest loading plugin
根因
| 环节 | 行为 |
|---|---|
配置 @latest | Desktop 启动时自动从 npm 拉取最新版本到 @latest 缓存目录 |
| 自动创建缓存 | ~.cache\opencode\packages\oh-my-openagent@latest 被创建 |
| 覆盖 Patch | 新创建的缓存没有我们的 polyfill |
| 加载路径 | Desktop 从 @latest 加载,而非 @3.17.15 |
解决
- 将
opencode.json中的@latest改为@3.17.15 - 将 patch 也应用到
@latest缓存目录(应急)
经验教训
- 永远不要用
@latest—— 自动更新会覆盖手动 patch - 固定版本号 ——
@3.17.15比@latest更可控 - 检查缓存目录 —— Desktop 可能自动创建多个版本目录
相关资源
- oh-my-openagent 仓库: github.com/code-yeongy…
- Issue #3797: v3.17.15 仍有
globalThis.Bun解构问题 - PR #3604:
bun:sqlite动态导入修复 - OpenCode Desktop 迁移文章: dev.to/brendonovic…
常见问题
Q: 这个 patch 会影响 CLI 吗?
A: 不会。polyfill 使用 typeof Bun === 'undefined' 守卫,在 Bun 环境中完全跳过。
Q: 插件更新后需要重新 patch 吗?
A: 是的。每次更新 oh-my-openagent 版本,都需要重新执行构建和部署步骤。
Q: 什么时候能有官方修复?
A: 关注 Issue #3797 和 PR #3798。等插件作者发布官方 Node 兼容构建后,就不需要手动 patch 了。
Q: macOS/Linux 也有这个问题吗?
A: 是的。只要使用 OpenCode Desktop (Electron),就会遇到同样的问题。解决方案相同。
最后更新: 2026-05-07