# oh-my-openagent 插件在 OpenCode Desktop 中无法加载?一文解决

9 阅读5分钟

适用版本: 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.jsv22.22.0
npm10.9.4
Bun1.3.12
OpenCode CLI1.14.39
OpenCode Desktop1.14.39 (Electron)
oh-my-openagent3.17.15

关键差异

运行时插件运行环境Bun 全局对象
OpenCode CLIBun 原生✅ 可用
OpenCode DesktopNode.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

根因

环节行为
配置 @latestDesktop 启动时自动从 npm 拉取最新版本到 @latest 缓存目录
自动创建缓存~.cache\opencode\packages\oh-my-openagent@latest 被创建
覆盖 Patch新创建的缓存没有我们的 polyfill
加载路径Desktop 从 @latest 加载,而非 @3.17.15

解决

  1. opencode.json 中的 @latest 改为 @3.17.15
  2. 将 patch 也应用到 @latest 缓存目录(应急)

经验教训

  • 永远不要用 @latest —— 自动更新会覆盖手动 patch
  • 固定版本号 —— @3.17.15@latest 更可控
  • 检查缓存目录 —— Desktop 可能自动创建多个版本目录

相关资源


常见问题

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