feat: 外置插件一等公民——entry.renderer 宿主内 React 组件(.pi/extensions 一等插件源 + 开发版门控)

This commit is contained in:
Rick Ye
2026-09-11 00:46:52 +08:00
parent 52f93ddc84
commit 8b94d42930
11 changed files with 291 additions and 53 deletions
+55 -7
View File
@@ -28,12 +28,14 @@
"icon": "<svg…/>", // 侧栏/面板图标(svg 字符串;宿主清除事件属性与 script)
"order": 10, // 侧栏排序(只在插件间;核心入口恒在前)
"panel": true, // 在右侧 SidePanel 出现一个 tab
"entry": { "page": "index.html", "main": "main.mjs" }, // 内置插件省略 entry
"entry": { "page": "index.html", "main": "main.mjs", "renderer": "renderer.mjs" }, // 内置插件省略 entry
"replacesNative": false,
"updateURL": "https://…/index.json"
}
```
外置插件源(`EXTERNAL_PLUGINS_ENABLED` 开发版开/用户版关,`ASO_EXTERNAL_PLUGINS=1` 豁免):`userData/plugins/<id>/` > 项目 `.pi/extensions/<id>/` > 全局 `~/.pi/agent/extensions/<id>/`。渲染层模块文件**不要命名为 index.ts/index.js**——那是 pi 原生扩展的自动发现名,会被 pi CLI 双重加载。
## 主进程插件契约(`@aso/plugin-sdk` 主入口)
```ts
@@ -75,13 +77,56 @@ useHarnessTheme('hotwords') // { dark, accent }(深浅色/acce
- 安全:宿主校验 `event.source === iframe.contentWindow` **且** `event.origin === 'plugin://<id>'`;sandbox 无 `allow-top-navigation`;iframe 页**无** window.api 直达
- 主题连续性:插件页 `<link rel="stylesheet" href="peer://assets/tokens.css">`(宿主 beautifului.css 的 token 表,深浅色由 `.dark` 类 + 下发的 accent 生效);建议插件自带 Tailwind 按同 token 编译
## 渲染层模块契约(`@aso/plugin-sdk/renderer`)
外置插件以 React 组件形态进入宿主界面(与内置同槽位),不再是 iframe 物理隔离:
```jsonc
// manifest.json
{ "id": "demo-ui", "panel": true, "entry": { "renderer": "renderer.mjs" } }
```
```js
// renderer.mjs —— 宿主渲染层动态 import(plugin://<id>/renderer.mjs) 后调用默认导出
export default function mount(ctx) {
ctx.registerView(MyViewComponent) // 整页视图(侧栏入口 → 主内容区)
ctx.registerPanel(MyPanelComponent) // 面板 tab 内容(需 panel: true)
}
```
- **单实例约束**:组件必须用 `ctx.react`(宿主 React,即 `window.__ASO_PLUGIN_HOST__.react`)创建;插件自带 React 副本会导致 hooks 全部失效
- JSX 工程构建配方(esbuild,react 别名到宿主实例的 shim):
```sh
esbuild renderer.tsx --bundle --format=esm --outfile=renderer.mjs \
--jsx=automatic \
--alias:react=./shims/react.js --alias:react/jsx-runtime=./shims/jsx-runtime.js
```
```js
// shims/react.js —— 构建期别名目标
const R = window.__ASO_PLUGIN_HOST__.react
export default R
export const { useState, useEffect, useRef, useMemo, useCallback, createContext,
useContext, Fragment, memo, forwardRef, createElement } = R
```
```js
// shims/jsx-runtime.js
const J = window.__ASO_PLUGIN_HOST__.jsxRuntime
export const { jsx, jsxs, Fragment } = J
```
- 装载时机与热重载:插件列表变化(扫描/启停/`plugins:reload`)即按 `?t=` cache-bust 重装;被禁用/移除的插件注册随之清除
- 门控:仅开发版装载(`import.meta.env.DEV` + 主进程 `EXTERNAL_PLUGINS_ENABLED`);错误隔离到单插件(装载失败仅 console.error,渲染崩溃由 PluginErrorBoundary 兜底)
## UI 槽位总览(对位 pi 的 ctx.ui)
| 槽位 | 机制 | 状态 |
|---|---|---|
| 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ |
| 插件整页 Main | 内置组件级 / 外置 iframe;聊天列 display:none 保活 | ✅ |
| 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件或 iframe) | ✅ |
| 插件整页 Main | 内置组件级 / 外置 renderer 组件 / 外置 iframe;聊天列 display:none 保活 | ✅ |
| 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件 / 外置 renderer 组件 / iframe) | ✅ |
| 状态胶囊(B2) | `setStatusItem` 数据驱动,TitleBar 渲染,两种视图均可见 | ✅ |
| 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ |
| 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 |
@@ -99,12 +144,15 @@ useHarnessTheme('hotwords') // { dark, accent }(深浅色/acce
## 安全清单
- `plugin://` 特权协议:路径解析后必须落在该插件目录内(防穿越);`peer://` 仅暴露白名单资产(tokens.css)
- CSP:宿主页只加 `frame-src plugin:`(iframe 方案不动 script-src)
- 外置 main.mjs 全权运行于主进程(与 pi 扩展同信任级)——**签名校验前,仅自行放置的插件目录可装载**(M5 后强制)
- CSP:宿主页 `frame-src plugin:`(iframe)+ `script-src plugin:`(renderer 模块动态 import);.mjs/.js 强制 JS MIME
- 外置装载为 **build 级开关**(`EXTERNAL_PLUGINS_ENABLED`:开发版开/用户版关,`ASO_EXTERNAL_PLUGINS=1` 豁免);外置 main.mjs 全权运行于主进程(与 pi 扩展同信任级)——**签名校验前,仅自行放置的插件目录可装载**(M5 后强制)
- 外置 renderer 模块以渲染层特权运行(无 iframe 沙箱):同样受上述 build 开关门控;组件崩溃不越出 ErrorBoundary
## pi 原生目录(M7,未实现)
## pi 原生目录(M7)
事实:pi resource-loader **默认自动加载** `~/.pi/agent/extensions/*.ts` 与 `<cwd>/.pi/extensions/*.ts`(`noExtensions` 可关)——即本应用当前已在运行用户为 pi CLI 安装的扩展(逻辑部分;其 TUI UI 调用按 pi 非 TUI 模式语义降级)。M7 计划:设置开关(默认关 → `noExtensions`)+ 管理页可见/单禁 + 参与统一重载。
事实:pi resource-loader **默认自动加载** `~/.pi/agent/extensions/*.ts` 与 `<cwd>/.pi/extensions/* /index.ts`(`noExtensions` 可关)——即本应用当前已在运行用户为 pi CLI 安装的扩展(逻辑部分;其 TUI UI 调用按 pi 非 TUI 模式语义降级)。
已落地:harness 把同一批目录当作**一等外置插件源**——目录内含 harness `manifest.json` 的子目录获得完整插件待遇(main.mjs 工厂 / entry.renderer React 组件 / entry.page iframe / 面板 tab / 管理页开关)。pi 只自动发现 index.ts/index.js,harness 约定入口为 main.mjs/renderer.mjs,因此**不会被 pi 双重加载**;纯 pi 扩展(index.ts)仍由 pi 原生装载、不参与 harness UI。项目级目录随工作区切换同步(`setProjectExtensionsDir`)。未做:项目目录的 project-trust 门控(与 pi 侧对齐)、设置页按源展示。
## 开发工作流(外置插件,M6 模板落地后)