起因比较直接:opencode 桌面版默认界面是纯色,用久了想换个能放背景图的皮肤。网上能用的成品不多,能改 app.asar 的又有更新失效和变砖的风险,于是决定自己用 CDP 从外部注入 CSS 做一层皮肤,不碰安装文件。

整体架构

Electron 应用开启远程调试端口后,渲染进程会暴露一个 Chrome DevTools Protocol 端点。思路是:

OpenCode.exe --remote-debugging-port=9336
        │
        ▼ CDP /json/list 发现渲染页面
inject.mjs ──WebSocket──▶ Runtime.evaluate
        │
        ▼ 注入一段 <style>
html 背景层(cover/fixed) + 面板半透明 + backdrop-filter 磨砂

好处:不改二进制、还原即删样式、opencode 更新不会丢补丁。

第一步:拿到渲染进程

调试端口打开后,先通过 HTTP 端点拿到 target 列表,再建立 WebSocket 通道:

const targets = await fetch(`http://127.0.0.1:${PORT}/json/list`).then(r => r.json());
const page = targets.find(t => t.type === 'page' && t.url.startsWith('oc://renderer'));

const ws = new WebSocket(page.webSocketDebuggerUrl);
ws.onopen = () => ws.send(JSON.stringify({
  id: 1,
  method: 'Runtime.evaluate',
  params: { expression: CSS_EXPR }
}));

oc://renderer 是 opencode 渲染进程的专用协议,必须精确匹配,否则会连到别的页面。

第二步:注入样式

背景图直接内联成 base64,铺在 html 上;需要读文字的面板单独加深:

html {
  background: url("data:image/jpeg;base64,...") center / cover fixed;
}

/* 输入框、消息卡片:半透明 + 磨砂,背景仍清晰可见 */
.group\/prompt-input,
.message-content {
  background: rgba(15, 17, 22, 0.35);
  backdrop-filter: blur(6px);
}

background-attachment: fixed 让图随视口固定,滚动时只有内容动,观感更稳。

关键难点:注入时机

这套方案最折腾的不是 CSS,而是注入时机。最初版本在 opencode 一启动就注入,经常卡死——症状是背景图正常,界面全空。原因是 React 还没挂载完,样式注入干扰了渲染进程的初始化。

靠固定延迟不可靠,机器快慢每次都不一样。后来改成"就绪握手":轮询采样,确认 UI 真正渲染完再动手。

function isReady(doc) {
  return doc.readyState === 'complete'        // 页面加载完成
      && doc.querySelector('#root')?.children.length > 0   // React 挂载
      && doc.querySelector('textarea, [contenteditable]')  // 输入框就位
      && doc.querySelectorAll('button').length > 0         // 组件树完整
      && doc.body.innerText.length > 100;                  // 有实际内容
}

// 连续三次采样都通过才注入,避免瞬时误判
let stable = 0;
while (stable < 3) {
  await sleep(1500);
  stable = isReady(document) ? stable + 1 : 0;
}

五个条件缺一不可:输入框和按钮都出现,说明整个组件树完成了首轮渲染;正文文本量排除空白页误判。

一个放弃的方案:Shadow DOM

工具调用里的 diff 区背景是 Shadow DOM 内部渲染的,普通 CSS 穿透不进去。最初用 MutationObserver 强制改行内样式,但观察器一旦同步遍历整棵 DOM 就拖垮主线程。最后权衡下来放弃 JS 强改,回归纯 CSS——diff 区保持原样,图个稳定。要能接受这一点,这套方案才成立。

使用方式

  • 桌面快捷方式 → launcher.vbs(隐藏窗口)→ 带调试端口启动 → inject.mjs 自动注入
  • 换背景图:替换 background.jpg,重跑一次 node inject.mjs 9336(运行中注入是安全的)
  • 还原默认:删掉快捷方式,直接用 exe 启动即可

小结

说到底就是"从外面给界面贴了一层皮":不改二进制、可随时还原、更新不失效。核心不是那几行 CSS,而是"等 UI 完全就绪再注入"这个时序问题——如果哪天官方把背景图功能合并进去,就不用自己操心这些了。