中文 | English
OpenCodex 支持把插件目录放进内置或外部插件根目录后自动发现、加载并在设置页生成插件开关。当前插件系统用于承载 OpenCodex 自有增强能力,例如移动端软键盘优化和移动端侧栏优化。
当前插件以可信同页脚本方式运行,没有沙箱隔离。插件可以访问
window、document和页面运行时对象,所以只应放入你信任的插件文件。
内置插件放在:
web-shell/plugins/<plugin-name>/index.js
gateway 会在请求 /opencodex-plugin-loader.js 时扫描插件根目录,并加载符合安全目录名规则的 <plugin-name>/index.js。刷新页面即可重新扫描插件目录。
有效目录名示例:
my-plugin
mobile-helper.v1
插件文件会通过以下 URL 加载:
/opencodex-plugins/builtin/<plugin-name>/index.js
通过环境变量 OPENCODEX_PLUGIN_DIRS 可以追加外部插件根目录。外部根目录和内置目录使用相同结构:
/path/to/plugins/
my-plugin/
index.js
i18.zh.json
i18.en.json
多个外部根目录可以用系统路径分隔符传入,也可以使用 JSON 数组:
OPENCODEX_PLUGIN_DIRS="/path/to/plugins:/another/plugins"
OPENCODEX_PLUGIN_DIRS='["/path/to/plugins", "/another/plugins"]'外部插件 URL 会带独立 source 段,例如:
/opencodex-plugins/external-1/<plugin-name>/index.js
插件系统分为注册和激活两个阶段:
- 登录页会加载插件系统和插件 loader,所以插件 JS 会在认证前完成
registerPlugin()注册,设置页也能显示插件开关。 - 认证通过并加载 Codex renderer 后,
codex-bridge-polyfill.js会调用activate("renderer", capabilities),插件的activate(context)才会执行,插件功能才真正生效。
(function () {
const pluginSystem = window.OpenCodexPluginSystem || window.__OpenCodexPluginSystem;
if (!pluginSystem || typeof pluginSystem.registerPlugin !== "function") return;
pluginSystem.registerPlugin({
id: "example.hello",
name: "Hello plugin",
labelKey: "plugin.exampleHello.label",
label: "示例插件",
descKey: "plugin.exampleHello.desc",
desc: "这段描述会显示在插件标题下面;为空时不显示。",
defaultEnabled: true,
order: 100,
activate(context) {
if (context.scope !== "renderer") return null;
if (!context.plugin.isEnabled()) return null;
console.log("[example.hello] activated");
return () => {
console.log("[example.hello] disposed");
};
},
});
})();| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 插件唯一 ID。建议使用反向域名或命名空间,例如 opencodex.mobile-keyboard-optimization。 |
name |
否 | 插件内部名称。 |
label |
否 | 设置页插件开关标题。未提供时回退到 name 或 id。 |
labelKey |
否 | 设置页插件开关标题的 i18 key,优先从插件自己的 i18 文件解析。 |
desc |
否 | 设置页标题下方描述。为空时不显示描述。 |
descKey |
否 | 设置页插件描述的 i18 key,优先从插件自己的 i18 文件解析。 |
defaultEnabled |
否 | 插件总开关默认值。未声明时默认 true。 |
enableStorageKey |
否 | 插件总开关在本地设置里的字段名。未声明时使用 plugin.<id>.enabled。 |
builtin |
否 | 是否内置插件。当前主要用于元信息标记。 |
order |
否 | 设置页排序值,数字越小越靠前。 |
settings |
否 | 插件自定义设置声明。当前设置页只自动渲染插件总开关,该字段暂作扩展预留。 |
activate(context) |
否 | 插件激活函数。启用状态下进入对应 scope 时调用。可返回 dispose 函数。 |
插件文案不要写进宿主 locale。插件目录内可放置语言文件:
i18.zh.json
i18.en.json
示例:
{
"plugin.exampleHello.label": "示例插件",
"plugin.exampleHello.desc": "这段描述会显示在插件标题下面。"
}宿主会先读取中文默认文件,再按当前语言叠加对应语言文件。插件 JS 里继续使用 labelKey / descKey,并保留 label / desc 作为缺失 i18 文案时的兜底。
插件开关保存在浏览器 localStorage:
opencodex_web_settings_v1
如果插件没有配置 enableStorageKey,默认字段名是:
plugin.<plugin-id>.enabled
示例:
{
"plugin.example.hello.enabled": true
}如果需要兼容旧设置名,可以显式指定:
pluginSystem.registerPlugin({
id: "example.legacy",
label: "兼容旧设置",
enableStorageKey: "legacyPluginEnabled",
});activate(context) 当前可以使用以下能力:
| 能力 | 说明 |
|---|---|
context.scope |
当前激活范围。目前 renderer 使用 "renderer"。 |
context.capabilities |
宿主传入的原始能力对象。 |
context.events.on(name, handler) |
监听插件事件,返回取消监听函数。 |
context.events.emit(name, payload) |
发出插件事件。 |
context.plugin.id |
当前插件 ID。 |
context.plugin.isEnabled() |
读取当前插件总开关状态。 |
context.preferences.get(id) |
读取设置值。 |
context.preferences.set(id, value) |
写入设置值。 |
context.preferences.isEnabled(id) |
按 !== false 判断设置是否启用。 |
context.preferences.load() |
读取完整设置对象。 |
context.preferences.save(next) |
保存完整设置对象。 |
context.preferences.defaults() |
获取当前默认设置对象。 |
context.settings.list(options) |
读取插件设置声明。 |
context.settings.register(setting) |
动态注册插件设置声明。当前 UI 暂未自动渲染这些子设置。 |
context.platform.isMobile() |
判断当前环境是否更像移动端输入设备。 |
context.capabilities.tokenUsage |
按 threadId + turnId 读取归一化后的回复 token 用量。 |
tokenUsage 是 bridge 挂载的专用能力,具体逻辑位于 codex-token-usage-capability.js,用于插件按需读取 AI 回复的 token 消耗。它不会向插件暴露原始 gateway、app-host 消息或 session 正文。只有插件调用 acquireConsumer 后,bridge 才会监听/解析相关数据;getForTurn 在运行期缓存未命中时才会读取对应回复的 session token 记录。运行期缓存会按全局和会话维度裁剪,不会无限增长。
const release = context.capabilities.tokenUsage.acquireConsumer("example.token-usage");
const disposeUpdate = context.capabilities.tokenUsage.onUpdate((usage) => {
console.log(usage.threadId, usage.turnId, usage.inputTokens, usage.outputTokens, usage.cacheHitRate);
});
context.capabilities.tokenUsage
.getForTurn({ threadId, turnId })
.then((usage) => {
// usage 可能为 null,表示当前没有可安全关联到该回复的数据。
});返回值字段:
| 字段 | 说明 |
|---|---|
threadId |
会话 ID。 |
turnId |
回复对应的 turn ID。 |
inputTokens |
输入 token 数;未知时为 null。 |
outputTokens |
输出 token 数;未知时为 null。 |
cachedInputTokens |
命中缓存的输入 token 数;未知时为 null。 |
cacheHitRate |
cachedInputTokens / inputTokens;无法计算时为 null。 |
updatedAt |
bridge 归一化该记录的时间戳。 |
source |
数据来源标记,例如 app-host、gateway 或 session-api。 |
| 事件 | 来源 | 说明 |
|---|---|---|
plugin:enabled-changed |
插件系统 | 插件总开关变化时触发。 |
preference:changed |
插件系统 | 设置值变化时触发。 |
ipc:invoke |
bridge polyfill | renderer 调用 gateway IPC 前触发。 |
view:message |
bridge polyfill | renderer 处理 view message 时触发。 |
示例:
activate(context) {
const dispose = context.events.on("plugin:enabled-changed", (payload) => {
if (payload.id === context.plugin.id) {
console.log("enabled:", payload.enabled);
}
});
return dispose;
}- 插件注册后,如果当前 scope 已经激活并且插件开关为启用,插件会立即执行
activate(context)。 - 插件关闭时,插件系统会调用
activate()返回的 dispose 函数。 - 插件再次打开时,插件系统会重新调用
activate(context)。 - 同一个 scope 只会激活一次,避免重复安装监听器。
- 插件是普通
<script>,没有沙箱和权限隔离。 - 插件可以直接访问 DOM,但这也意味着插件需要自己承担兼容性和安全风险。
- 设置页目前只自动渲染每个插件的总开关,插件子设置声明还没有完整 UI。
- 插件描述暂不接入主项目 i18n,建议插件自己提供最终展示文案。