docs: 渐进式插件教程(L0→L5)+ DESIGN.md 视觉规范 + plugin-template 脚手架
This commit is contained in:
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
# 插件系统(harness 插件契约)
|
# 插件系统(harness 插件契约)
|
||||||
|
|
||||||
|
> **教程入口**:[docs/plugins/README.md](./plugins/README.md) —— L0 第一个插件 → L1 工具命令 → L2 设置 → L3 UI 槽位 → L4 React 组件 → L5 自诊断;视觉规范 [DESIGN.md](./plugins/DESIGN.md);脚手架 [packages/plugin-template](../packages/plugin-template/)。本文件是契约全集(参考手册)。
|
||||||
|
>
|
||||||
> 状态:v0.8 进行中(M0–M3 已落地;M4 更新管道 / M6 模板与外置示例 / M7 pi 原生目录管控见 roadmap)
|
> 状态:v0.8 进行中(M0–M3 已落地;M4 更新管道 / M6 模板与外置示例 / M7 pi 原生目录管控见 roadmap)
|
||||||
> 契约版本:`HARNESS_CONTRACT_VERSION = 0.1.0`(`src/main/plugins/plugin-host.ts`;manifest.minContract 与之比对,不满足拒载)
|
> 契约版本:`HARNESS_CONTRACT_VERSION = 0.2.0`(`src/main/plugins/plugin-host.ts`;manifest.minContract 与之比对,不满足拒载)
|
||||||
|
|
||||||
## 两层能力模型
|
## 两层能力模型
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# L0 · 第一个插件
|
||||||
|
|
||||||
|
> 目标:10 分钟内拥有一个被宿主装载、可在日志里看到自己痕迹的最小插件。不需要构建工具。
|
||||||
|
|
||||||
|
## 1. 建目录
|
||||||
|
|
||||||
|
放在**全局**插件目录(所有工作区可见;项目级放 `<工作区>/.pi/extensions/`,见 README):
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.pi/agent/extensions/hello/
|
||||||
|
manifest.json
|
||||||
|
main.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
> Windows 下即 `C:\Users\<你>\.pi\agent\extensions\hello\`。
|
||||||
|
|
||||||
|
## 2. manifest.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "hello",
|
||||||
|
"name": "Hello",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"minContract": "0.1.0",
|
||||||
|
"description": "我的第一个插件"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段规则:`id` 必须匹配 `^[a-z][a-z0-9-]*$`;`version` 是 semver;`minContract` 是要求的宿主契约版本(当前 `0.2.0`,填 `0.1.0` 即可向后兼容)。全字段见 [../plugins.md](../plugins.md#manifestjson)。
|
||||||
|
|
||||||
|
## 3. main.mjs
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 默认导出一个工厂:宿主激活插件时调用,返回的函数在禁用/重载时被调用
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
api.log.info('hello activated', { version: api.manifest.version })
|
||||||
|
|
||||||
|
// 演示:往 Agent 可见的目录里注册一个极简工具
|
||||||
|
api.pi.registerTool({
|
||||||
|
name: 'hello_now',
|
||||||
|
description: '返回当前时间的问候语',
|
||||||
|
parameters: {},
|
||||||
|
async execute() {
|
||||||
|
api.log.debug('hello_now called')
|
||||||
|
return { output: `Hello! 现在是 ${new Date().toISOString()}` }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api.log.info('hello disposed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `api` 的完整形状(pi / ui / storage / log / manifest)见 [../plugins.md](../plugins.md#主进程插件契约aso-plugin-sdk-主入口)。
|
||||||
|
|
||||||
|
## 4. 验证
|
||||||
|
|
||||||
|
1. 重启应用(`pnpm dev`;扫描发生在启动与插件重载时)
|
||||||
|
2. 打开 设置 → Agent 能力 → 插件 → **全局** 作用域,应看到「Hello v0.1.0」,开关为「已启用」
|
||||||
|
3. 打开日志文件 `userData/logs/main-YYYY-MM-DD.log`,应能搜到:
|
||||||
|
|
||||||
|
```
|
||||||
|
<时间> INFO [plugin-host] external main loaded pluginId=hello version=0.1.0
|
||||||
|
<时间> INFO [plugin-host] plugin activated pluginId=hello source=external
|
||||||
|
<时间> INFO [plugin] hello activated pluginId=hello version=0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 改代码后如何生效
|
||||||
|
|
||||||
|
- 改 `main.mjs`:设置 → 插件页任意开关触发重载,或重启应用(宿主以 cache-bust 重新 import)
|
||||||
|
- 改 `manifest.json`:同上;`id` 变了等于新插件
|
||||||
|
|
||||||
|
## 常见装载失败
|
||||||
|
|
||||||
|
| 现象(日志里搜) | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| `external main load failed` | main.mjs 语法错误 / import 了不存在的模块 |
|
||||||
|
| `external main invalid export` | 忘了 `export default`,或默认导出不是函数 |
|
||||||
|
| `需要契约版本 >=` | manifest.minContract 高于当前宿主契约(0.2.0) |
|
||||||
|
| 插件页全局作用域为空 | 目录放错(注意是 `extensions` 复数);或用户版构建(装载关闭) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**下一篇**:[L1 工具与命令](./02-tools-and-commands.md)——把真正的能力交给 Agent。
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# L1 · 工具与命令
|
||||||
|
|
||||||
|
> 前置:[L0](./01-hello-plugin.md)。`api.pi` 就是 pi 的原生 `ExtensionAPI`——pi CLI 扩展能做的这里都能做,语义一致。
|
||||||
|
|
||||||
|
## 1. 注册一个带参数的工具
|
||||||
|
|
||||||
|
工具是 Agent 的「手」:注册后自动进入系统提示词与工具列表,受审批拦截管理。
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
api.pi.registerTool({
|
||||||
|
name: 'dice_roll',
|
||||||
|
description: '掷一个 N 面骰子,返回点数',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sides: { type: 'number', description: '面数,默认 6' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async execute(_id, params) {
|
||||||
|
const sides = Number(params.sides) || 6
|
||||||
|
const value = 1 + Math.floor(Math.random() * sides)
|
||||||
|
api.log.info('dice rolled', { sides, value })
|
||||||
|
return { output: String(value), metadata: { sides, value } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- `description` 会注入系统提示词,写清楚「什么时候该用这个工具」,Agent 的调用准确率取决于它
|
||||||
|
- `execute` 返回 `{ output: string }`——output 是喂回模型的内容,保持精炼
|
||||||
|
- 工具名全局唯一,用 `<插件名>_<动作>` 前缀避免撞名
|
||||||
|
|
||||||
|
## 2. 注册斜杠命令
|
||||||
|
|
||||||
|
命令是用户的「入口」:聊天框输入 `/` 出现,回车触发。
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
api.pi.registerCommand({
|
||||||
|
name: 'dice',
|
||||||
|
description: '掷骰子',
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const sides = Number(args) || 6
|
||||||
|
ctx.notify(`🎲 ${1 + Math.floor(Math.random() * sides)} / ${sides}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 订阅事件
|
||||||
|
|
||||||
|
在 Agent 生命周期钩子上挂逻辑(发消息前注入、工具调用前后处理等):
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
const off = api.pi.on('before_agent_start', (event) => {
|
||||||
|
api.log.debug('agent starting', { prompt: event.prompt?.slice(0, 50) })
|
||||||
|
})
|
||||||
|
// 返回的取消函数放进 dispose,宿主在禁用/重载时调用
|
||||||
|
return () => off()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常用事件:`before_agent_start` / `agent_start` / `tool_call` / `tool_result` / `before_provider_request` / `agent_end`。全集见 pi 的 `ExtensionAPI` 类型(`@earendil-works/pi-coding-agent`)。
|
||||||
|
|
||||||
|
## 4. 与插件私有存储配合
|
||||||
|
|
||||||
|
需要跨回合记住状态时用 `api.storage`(按插件隔离,落盘 `aso-plugin-storage.json`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
let count = api.storage.get('count') ?? 0
|
||||||
|
api.storage.set('count', ++count)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
|
||||||
|
1. 重载插件(开关切换或重启)
|
||||||
|
2. 日志确认 `plugin activated`
|
||||||
|
3. 聊天框输入 `/dice` 回车 → 收到点数;对 Agent 说「掷个 20 面骰」→ 观察它调用 `dice_roll`
|
||||||
|
4. 日志中应出现 `dice rolled sides=20 value=…`
|
||||||
|
|
||||||
|
## 排障
|
||||||
|
|
||||||
|
- 工具没出现在 Agent 里:日志搜 `plugin activated` 确认激活成功;`registerTool` 抛错会记为 `plugin activation failed`
|
||||||
|
- 命令没出现在 `/` 面板:命令名冲突(重名被去重),换名再试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一篇** [L0](./01-hello-plugin.md) · **下一篇**:[L2 设置表单](./03-settings.md)——让用户能配置你的插件。
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# L2 · 设置表单
|
||||||
|
|
||||||
|
> 前置:[L0](./01-hello-plugin.md)。声明式 schema:插件声明「要什么配置」,宿主负责渲染、持久化、实时回推。不用写任何 UI 代码。
|
||||||
|
|
||||||
|
## 1. 声明设置
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
api.ui.registerSettings({
|
||||||
|
items: [
|
||||||
|
{ type: 'string', key: 'apiKey', label: 'API Token', password: true, placeholder: 'sk-...' },
|
||||||
|
{ type: 'boolean', key: 'verbose', label: '详细日志', default: false },
|
||||||
|
{
|
||||||
|
type: 'select', key: 'endpoint', label: '接入点', default: 'prod',
|
||||||
|
options: [
|
||||||
|
{ value: 'prod', label: '生产' },
|
||||||
|
{ value: 'staging', label: '预发' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ type: 'button', key: 'test', label: '测试连接', variant: 'primary' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
保存后(开关切换或重启):
|
||||||
|
- 设置 → 插件 → **全局/项目** 作用域里,该插件行内出现 ⚙ 按钮(打开配置页)和「一级入口」开关
|
||||||
|
- **items 为空或不声明 → 两个控件都不出现**(这正是"留空则无一级入口按钮"的规则)
|
||||||
|
|
||||||
|
## 2. 一级入口
|
||||||
|
|
||||||
|
行内「一级入口」开关打开后,设置弹窗左侧导航「Agent 能力」组末尾出现以插件名命名的入口,点击进入该插件的独立配置页。用户随时可以关掉。
|
||||||
|
|
||||||
|
## 3. 读写值
|
||||||
|
|
||||||
|
```js
|
||||||
|
const values = api.ui.getSettingValues() // { apiKey: '...', verbose: false, endpoint: 'prod' }
|
||||||
|
// schema 里的 default 已合并——没被用户改过的键直接拿默认值
|
||||||
|
|
||||||
|
api.ui.onSettingsChange((next) => { // 用户在设置页改值后实时推送
|
||||||
|
values = next
|
||||||
|
api.log.info('settings changed', { endpoint: next.endpoint })
|
||||||
|
})
|
||||||
|
|
||||||
|
api.ui.onSettingsAction((key) => { // button 条目点击(key = 按钮项 key)
|
||||||
|
if (key === 'test') {
|
||||||
|
const ok = values.apiKey ? '✅' : '❌ 缺少 API Token'
|
||||||
|
api.ui.setStatusItem('test', { text: `连接测试 ${ok}`, tone: 'accent' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
值持久化在 `userData/aso-plugin-values.json`(按插件隔离),重装插件后仍在。
|
||||||
|
|
||||||
|
## 4. 13 种标准控件
|
||||||
|
|
||||||
|
| type | 说明 | 关键字段 |
|
||||||
|
|---|---|---|
|
||||||
|
| `string` | 单行输入 | `password` 掩码、`placeholder` |
|
||||||
|
| `text` | 多行文本 | `rows` |
|
||||||
|
| `number` | 数字 | `min` / `max` / `step` |
|
||||||
|
| `boolean` | 开关 | — |
|
||||||
|
| `select` | 下拉单选 | `options[{value,label}]` |
|
||||||
|
| `multiSelect` | 多选(chip 组) | `options` |
|
||||||
|
| `slider` | 滑条 | `min` / `max` 必填、`step` |
|
||||||
|
| `color` | 颜色(#rrggbb) | — |
|
||||||
|
| `path` | 文件/目录选择 | `mode: 'file' \| 'directory'` |
|
||||||
|
| `image` | 图片选择 + 缩略图 | — |
|
||||||
|
| `button` | 动作按钮 | `variant: 'primary' \| 'ghost'` |
|
||||||
|
| `info` | 静态说明 | — |
|
||||||
|
| `divider` | 分割线/小节标题 | `label?` |
|
||||||
|
|
||||||
|
全部条目支持 `label` / `description` / `default`(button/info/divider 除外)。
|
||||||
|
|
||||||
|
## 5. 设计约束(为什么是数据不是代码)
|
||||||
|
|
||||||
|
- schema 是**纯数据**:不允许函数/React 元素——宿主跨进程渲染并持久化快照
|
||||||
|
- 需要动态选项(比如扫描出来的设备列表)?在激活时计算好再 `registerSettings`,每次激活都会重新声明
|
||||||
|
- 需要复杂交互 UI?那不是设置的职责——走 [L4 React 组件](./05-renderer-components.md)
|
||||||
|
|
||||||
|
## 6. 验证
|
||||||
|
|
||||||
|
1. 重载插件 → 插件页该行出现 ⚙ 与「一级入口」
|
||||||
|
2. 打开配置页改一个值 → 日志出现 `settings changed`(`onSettingsChange` 生效)
|
||||||
|
3. 点按钮 → 状态胶囊出现(动作回调生效)
|
||||||
|
4. `userData/aso-plugin-values.json` 里能看到写入的键值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一篇** [L1](./02-tools-and-commands.md) · **下一篇**:[L3 UI 槽位](./04-ui-slots.md)——状态胶囊、面板、视图与 iframe 页。
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# L3 · UI 槽位:胶囊、工具栏、视图、面板、iframe 页
|
||||||
|
|
||||||
|
> 前置:[L0](./01-hello-plugin.md)。宿主开放 5 个数据驱动 UI 槽位 + 1 个 iframe 页形态。**数据驱动**意味着插件只提交数据,渲染与样式由宿主统一负责——这是 UI 一致性的第一道保障(自定义视觉见 [DESIGN.md](./DESIGN.md) 与 [L4](./05-renderer-components.md))。
|
||||||
|
|
||||||
|
## 1. 状态胶囊(TitleBar 顶部)
|
||||||
|
|
||||||
|
轻量状态提示,点击可深链跳转:
|
||||||
|
|
||||||
|
```js
|
||||||
|
api.ui.setStatusItem('conn', {
|
||||||
|
text: '已连接 · 3 任务',
|
||||||
|
tone: 'accent', // default | accent | warn | danger
|
||||||
|
openView: { view: 'my-plugin' } // 点击跳到插件整页视图(可省略)
|
||||||
|
})
|
||||||
|
api.ui.removeStatusItem('conn')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 工具栏按钮(输入区工具行)
|
||||||
|
|
||||||
|
```js
|
||||||
|
api.ui.registerToolbarButton({
|
||||||
|
id: 'open',
|
||||||
|
title: '打开我的面板',
|
||||||
|
icon: '<svg .../>', // 16-20px 线条风,currentColor
|
||||||
|
openView: { view: 'my-plugin' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 整页视图 + 侧栏入口
|
||||||
|
|
||||||
|
manifest 声明 `icon` / `order` 后,插件自动出现在侧栏(有 UI 能力时);任何槽位用 `openView` 深链:
|
||||||
|
|
||||||
|
```js
|
||||||
|
api.ui.openView('my-plugin', { section: 'detail' }) // params 透传给插件 UI
|
||||||
|
api.ui.openView('chat') // 回到聊天
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 面板 tab(右侧 SidePanel)
|
||||||
|
|
||||||
|
manifest 加 `"panel": true`,插件在右侧面板获得一个 tab。
|
||||||
|
|
||||||
|
## 5. iframe 插件页(entry.page)
|
||||||
|
|
||||||
|
需要**完全自定义的 Web UI** 且不想构建 React 时用。物理隔离(sandbox iframe),经桥通信:
|
||||||
|
|
||||||
|
```
|
||||||
|
<插件目录>/index.html + 页面 JS
|
||||||
|
manifest: "entry": { "page": "index.html" }
|
||||||
|
```
|
||||||
|
|
||||||
|
页面侧用 SDK 桥(`@aso/plugin-sdk/client`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import { getHarnessClient, useHarnessTheme, useDeepLinkParams } from './sdk-client.mjs'
|
||||||
|
const harness = getHarnessClient('my-plugin')
|
||||||
|
harness.openView('chat') // 深链
|
||||||
|
harness.setStatus('state', { text: '就绪' })
|
||||||
|
harness.invoke('getState') // 白名单:getState / send / getSettings
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**样式统一(强制)**:插件页 `<head>` 里必须引宿主 token 表,并用自己的 Tailwind 按同样 token 名编译——
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="peer://assets/tokens.css" />
|
||||||
|
```
|
||||||
|
|
||||||
|
主题(深浅色/accent)由宿主推送(`useHarnessTheme`),禁止自行探测。详细规则见 [DESIGN.md](./DESIGN.md#iframe-插件页)。
|
||||||
|
|
||||||
|
## 6. 安全边界速记
|
||||||
|
|
||||||
|
| 形态 | 代码位置 | 能力 |
|
||||||
|
|---|---|---|
|
||||||
|
| main.mjs | 主进程 | 全权(与 pi 扩展同信任级) |
|
||||||
|
| renderer.mjs(L4) | 渲染层 | 宿主 React + window.api 受控面 |
|
||||||
|
| iframe 页 | 沙箱 iframe | 仅白名单桥(getState/send/getSettings/setStatus/openView) |
|
||||||
|
|
||||||
|
## 7. 验证
|
||||||
|
|
||||||
|
1. 状态胶囊:`setStatusItem` 后 TitleBar 立即出现,`removeStatusItem` 消失
|
||||||
|
2. 面板 tab:`panel: true` + 任一 UI 形态 → 右侧面板出现 tab
|
||||||
|
3. iframe 页:改页面文件 → 插件页开关触发重载 → iframe 换 src 整页刷新
|
||||||
|
4. 全部 UI 状态在日志可查(`plugin activated` 之后你在 main.mjs 里的 `api.log` 输出)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一篇** [L2](./03-settings.md) · **下一篇**:[L4 React 组件](./05-renderer-components.md)——宿主内真 React 组件。
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# L4 · 宿主内 React 组件(entry.renderer)
|
||||||
|
|
||||||
|
> 前置:[L3](./04-ui-slots.md)。iframe 页是隔离的「贴进去的网页」;`entry.renderer` 则是**真正的宿主内 React 组件**——与内置插件同槽位、同观感、同主题。这是外置插件的最高 UI 形态。
|
||||||
|
|
||||||
|
## 1. 声明
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "my-plugin", "panel": true, "entry": { "renderer": "renderer.mjs" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
> 文件名**不要叫 index.ts/index.js**——那是 pi 原生扩展的自动发现名,会被 pi CLI 双重加载。
|
||||||
|
|
||||||
|
## 2. renderer.mjs 契约
|
||||||
|
|
||||||
|
宿主动态 import 后调用默认导出 `mount(ctx)`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function mount(ctx) {
|
||||||
|
// ctx.react 是宿主的 React 单实例——必须用它创建组件(自带 React 副本会让 hooks 全部失效)
|
||||||
|
const R = ctx.react
|
||||||
|
|
||||||
|
function Panel() {
|
||||||
|
const [n, setN] = R.useState(0)
|
||||||
|
return R.createElement('div', { className: 'p-3 space-y-2' },
|
||||||
|
R.createElement('div', { className: 'text-[12.5px] text-ink' }, `点击 ${n} 次`),
|
||||||
|
R.createElement('button', {
|
||||||
|
className: 'rounded-control border border-line-strong px-2.5 py-1 text-[11.5px] text-ink hover:bg-hover',
|
||||||
|
onClick: () => setN(n + 1)
|
||||||
|
}, '+1')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.registerPanel(Panel) // 右侧面板 tab 内容(需 manifest.panel: true)
|
||||||
|
ctx.registerView(Panel) // 整页视图(侧栏入口 → 主内容区);两个都注册也可以
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. JSX 工程构建配方(推荐)
|
||||||
|
|
||||||
|
手写 `createElement` 只适合玩具;真实插件用 JSX + esbuild,构建时把 `react` 别名到**宿主全局实例**:
|
||||||
|
|
||||||
|
```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
|
||||||
|
export const { jsx, jsxs, Fragment } = window.__ASO_PLUGIN_HOST__.jsxRuntime
|
||||||
|
```
|
||||||
|
|
||||||
|
`packages/plugin-template/` 已内置这套配置,复制即用。
|
||||||
|
|
||||||
|
## 4. 硬性约束
|
||||||
|
|
||||||
|
| 约束 | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| React 必须来自 `ctx.react` / 宿主全局 | 双 React 实例 → hooks 全部报错 |
|
||||||
|
| 模块**自包含**(单文件 bundle) | 宿主以 Blob URL 装载,相对导入没有基路径 |
|
||||||
|
| 不 import 裸包名(react 等) | 同上;一切依赖打进 bundle(react 走别名除外) |
|
||||||
|
| 单文件 ≤ 8MB | 超限拒绝装载 |
|
||||||
|
| 仅开发版装载 | 用户版第三方装载关闭(`ASO_EXTERNAL_PLUGINS=1` 豁免) |
|
||||||
|
|
||||||
|
## 5. 样式
|
||||||
|
|
||||||
|
组件直接跑在宿主树里——**宿主的 Tailwind 类与 token 全部可用**(`text-ink`、`border-line`、`rounded-card`、`bg-accent` …)。必须遵守 [DESIGN.md](./DESIGN.md) 的类名词汇与布局模式,禁止硬编码色值/阴影。
|
||||||
|
|
||||||
|
## 6. 生命周期
|
||||||
|
|
||||||
|
- 装载:插件列表变化(启停/重载/启动)即重装;`mount` 同步执行注册
|
||||||
|
- 卸载:插件禁用/移除 → 注册的组件自动从宿主卸载
|
||||||
|
- 崩溃隔离:渲染错误被 `PluginErrorBoundary` 捕获,不影响宿主;装载失败仅 console.error + 日志
|
||||||
|
|
||||||
|
## 7. 验证
|
||||||
|
|
||||||
|
1. 重载插件 → 插件页该行出现 ⚙(若已声明设置)/侧栏入口/面板 tab
|
||||||
|
2. 控制台无 `[plugins] … renderer 装载失败`
|
||||||
|
3. 点击组件内按钮,state 正常更新(hooks 生效 = 单实例约束满足)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一篇** [L3](./04-ui-slots.md) · **下一篇**:[L5 自诊断](./06-self-diagnosis.md)——让 Agent 能发现并修复你的插件问题。
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# L5 · 自诊断:日志规范与 Agent 自迭代闭环
|
||||||
|
|
||||||
|
> 本项目的愿景是「Agent 写插件、Agent 排障、Agent 迭代」。达成的前提是**一切可观测**:宿主日志、插件日志、渲染层错误都落盘且结构化。这一篇讲日志从哪来、到哪去、Agent 怎么用它排障。
|
||||||
|
|
||||||
|
## 1. 日志在哪
|
||||||
|
|
||||||
|
```
|
||||||
|
userData/logs/main-YYYY-MM-DD.log ← 唯一日志文件,按天滚动,保留 14 天
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:`C:\Users\<你>\AppData\Roaming\@aso\desktop\logs\`
|
||||||
|
(Agent 侧:pi 的 read/grep 工具可直接读这个路径——排障第一步永远是读它。)
|
||||||
|
|
||||||
|
级别:dev 默认 `debug`,打包版默认 `info`;环境变量 `ASO_LOG_LEVEL=debug|info|warn|error` 覆盖。
|
||||||
|
|
||||||
|
## 2. 行格式
|
||||||
|
|
||||||
|
```
|
||||||
|
<ISO时间> <LEVEL> [<scope>] <message> k=v k=v
|
||||||
|
2026-09-10T10:21:33.120Z INFO [plugin-host] plugin activated pluginId=demo-ui source=external
|
||||||
|
2026-09-10T10:21:33.451Z INFO [plugin] settings changed pluginId=demo-ui endpoint=staging
|
||||||
|
2026-09-10T10:21:40.002Z ERROR [ipc] ipc failed channel=plugins:setEnabled ms=1203 err=Error: runtime rebuild failed
|
||||||
|
```
|
||||||
|
|
||||||
|
scope 一览:`main` / `plugin-host` / `plugin`(所有插件日志,带 `pluginId` 字段)/ `agent-service` / `browser` / `ipc` / `renderer` / `logger`。
|
||||||
|
|
||||||
|
## 3. 插件怎么写日志:`api.log.*`
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function definePlugin(api) {
|
||||||
|
const log = api.log // 自动带 pluginId 字段,进同一文件
|
||||||
|
|
||||||
|
api.log.info('activation payload', { tools: 3, mode: 'standard' })
|
||||||
|
try {
|
||||||
|
risky()
|
||||||
|
} catch (err) {
|
||||||
|
api.log.error('risky step failed', { step: 'connect', err }) // err 会被序列化成 stack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**埋点规范**(Agent 排障依赖这些惯例):
|
||||||
|
- 激活时:`info` 一条「我装了什么」(能力清单/配置摘要)
|
||||||
|
- 关键决策与外部调用:`info` / `debug` + 结果字段
|
||||||
|
- 一切 catch:`error` + `err` 字段——**禁止吞错不记**
|
||||||
|
- message 用英文短句、kv 用结构化字段(便于 grep `pluginId=xxx`)
|
||||||
|
|
||||||
|
## 4. 宿主替你记录的(不用自己写)
|
||||||
|
|
||||||
|
| 事件 | scope | 级别 |
|
||||||
|
|---|---|---|
|
||||||
|
| 插件扫描/装载/激活/禁用/重载全链路 | plugin-host | info |
|
||||||
|
| main.mjs 加载失败 / 激活失败 | plugin-host | error |
|
||||||
|
| 设置值变更 / 一级入口切换 / 按钮动作 | plugin-host | info/debug |
|
||||||
|
| 全部 73 个 IPC 通道调用(成功 debug 含耗时,失败 error) | ipc | debug/error |
|
||||||
|
| Agent 回合开始/结束/结算、运行时重建 | agent-service | info/error |
|
||||||
|
| 渲染层 console(含插件 renderer 模块与 iframe 页)、白屏、崩溃 | renderer | debug/error |
|
||||||
|
| 进程级 uncaughtException / unhandledRejection | logger | error |
|
||||||
|
|
||||||
|
## 5. Agent 排障闭环(自迭代的核心流程)
|
||||||
|
|
||||||
|
```
|
||||||
|
发现问题(用户反馈 / 日志 error)
|
||||||
|
↓ 读 userData/logs/main-<今天>.log
|
||||||
|
↓ grep pluginId=<自己的id> 与 ERROR
|
||||||
|
↓ 对照本文第 4 节判断:装载失败?激活抛错?IPC 超时?渲染崩溃?
|
||||||
|
↓ 改插件代码(L0-L4 的对应章节)
|
||||||
|
↓ 触发重载(设置页开关 / 重启)→ 再读日志验证新一次 plugin activated
|
||||||
|
↓ 循环直到日志干净
|
||||||
|
```
|
||||||
|
|
||||||
|
写插件时请遵守:**每个可能失败的操作都有日志、每条 error 都能定位到步骤**。这是 Agent 能替你维护插件的最低要求。
|
||||||
|
|
||||||
|
## 6. 快速自查清单
|
||||||
|
|
||||||
|
- [ ] 装载了吗:`external main loaded pluginId=<id>`
|
||||||
|
- [ ] 激活了吗:`plugin activated pluginId=<id>`
|
||||||
|
- [ ] 有报错吗:`ERROR` + `pluginId=<id>`
|
||||||
|
- [ ] IPC 通吗:`ipc failed channel=…` 是宿主问题还是插件问题
|
||||||
|
- [ ] 渲染崩了吗:`renderer console` 中 `[window.onerror]` / `[unhandledrejection]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一篇** [L4](./05-renderer-components.md) · 回到 [索引](./README.md) · 契约全集 [../plugins.md](../plugins.md) · 视觉规范 [DESIGN.md](./DESIGN.md)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# DESIGN.md · 插件视觉一致性规范
|
||||||
|
|
||||||
|
> 读者是**写 UI 的 Agent/开发者**。目标:任何来源的插件,界面观感与宿主完全一致。
|
||||||
|
> 规则分两类:**强制**(可机器校验/一眼可辨)与**默认**(无充分理由不得偏离)。
|
||||||
|
> 契约问题看 [../plugins.md](../plugins.md);本文件只管"长什么样"。
|
||||||
|
|
||||||
|
## 0. 两条铁律
|
||||||
|
|
||||||
|
1. **色值只允许来自 token**。任何 `#hex`/`rgb()` 硬编码(规范中列出的除外)都算违规;语义永远用 token 名表达(`text-ink-3` 而不是 `text-gray-400`)。
|
||||||
|
2. **不发明新组件形态**。先查本文 §4 标准模式;确需新形态时,用 §1 的 token 组合拼装,不引入新色/新圆角/新阴影。
|
||||||
|
|
||||||
|
## 1. Token 表(单一事实源:`peer://assets/tokens.css` = 宿主 beautifului.css)
|
||||||
|
|
||||||
|
### 表面(背景)
|
||||||
|
|
||||||
|
| token | 意图 |
|
||||||
|
|---|---|
|
||||||
|
| `--page` | 页面最底层背景 |
|
||||||
|
| `--surface` | 卡片/面板表面 |
|
||||||
|
| `--inset` | 内嵌区域(输入框底、预览底、代码块底) |
|
||||||
|
| `--hover` / `--hover-2` | 悬停态一级/二级 |
|
||||||
|
|
||||||
|
### 文字
|
||||||
|
|
||||||
|
| token | 意图 |
|
||||||
|
|---|---|
|
||||||
|
| `--ink` | 主文字(标题、关键值) |
|
||||||
|
| `--ink-2` | 次级文字(正文说明) |
|
||||||
|
| `--ink-3` | 弱化文字(占位、提示、元信息、路径) |
|
||||||
|
|
||||||
|
### 边线
|
||||||
|
|
||||||
|
| token | 意图 |
|
||||||
|
|---|---|
|
||||||
|
| `--line` | 常规边框(卡片、列表分隔) |
|
||||||
|
| `--line-strong` | 强边框(输入框、可点击描边按钮) |
|
||||||
|
| `--line-soft` | 极弱分隔 |
|
||||||
|
|
||||||
|
### 强调与语义
|
||||||
|
|
||||||
|
| token | 意图 |
|
||||||
|
|---|---|
|
||||||
|
| `--accent` / `--accent-ink` / `--accent-tint` | 品牌强调(主按钮、选中、链接) |
|
||||||
|
| `--green` / `--green-tint` | 成功/正常 |
|
||||||
|
| `--orange` / `--orange-tint` | 警示 |
|
||||||
|
| `--red` / `--red-tint` | 错误/危险 |
|
||||||
|
|
||||||
|
### 阴影(全部是描边式,禁止投影类阴影)
|
||||||
|
|
||||||
|
`--shadow-hairline`(发丝线)· `--shadow-btn`(按钮)· `--shadow-card`(卡片)· `--shadow-raised`(浮起)· `--shadow-overlay`(弹层)· `--shadow-inset-field`(内嵌输入)
|
||||||
|
|
||||||
|
### 动效
|
||||||
|
|
||||||
|
`--duration-fast/base/slow`(统一 400ms)+ `--ease-standard`。悬停/切换用 `transition-colors` 即可。
|
||||||
|
|
||||||
|
## 2. 类名词汇(宿主 Tailwind 映射,renderer 组件直接用)
|
||||||
|
|
||||||
|
背景 `bg-page / bg-surface / bg-inset / bg-hover / bg-accent / bg-accent/10` ·
|
||||||
|
文字 `text-ink / text-ink-2 / text-ink-3 / text-accent / text-red` ·
|
||||||
|
边框 `border-line / border-line-strong / border-dashed border-line` ·
|
||||||
|
圆角 `rounded-card`(卡片/输入块)`rounded-control`(按钮/输入框/徽标)。
|
||||||
|
|
||||||
|
## 3. 字号与间距(默认)
|
||||||
|
|
||||||
|
- 字号阶:`10px`(徽标)/ `11px`(辅助按钮)/ `11.5px`(次要按钮、说明)/ `12px`(正文说明) / `12.5px`(行标题/正文)/ `16px`(页面标题)。**不使用其他字号**;行高 `leading-4/5`。
|
||||||
|
- 间距基数为 4px:卡片内边距 `px-3 py-2.5`,区块间 `space-y-1.5`,小节间 `pt-3`。
|
||||||
|
- 图标 12–16px,线条风格,`currentColor`。
|
||||||
|
|
||||||
|
## 4. 标准模式(复制即用)
|
||||||
|
|
||||||
|
### 设置行(label 左 / 控件右)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex items-center justify-between gap-3 rounded-card border border-line px-3 py-2.5">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-[12.5px] text-ink">标题</div>
|
||||||
|
<div class="mt-0.5 text-[11px] leading-4 text-ink-3">说明文字</div>
|
||||||
|
</div>
|
||||||
|
<!-- 控件 -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 按钮
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- primary -->
|
||||||
|
<button class="rounded-control bg-accent px-3 py-1.5 text-[11.5px] font-medium text-white hover:opacity-90">主操作</button>
|
||||||
|
<!-- ghost -->
|
||||||
|
<button class="rounded-control border border-line-strong px-3 py-1.5 text-[11.5px] text-ink hover:bg-hover">次要操作</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 徽标
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">全局 v0.1.0</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 空态
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
|
||||||
|
<div class="text-[13px] font-medium text-ink-2">暂无数据</div>
|
||||||
|
<div class="text-[11.5px] text-ink-3">怎么产生的提示</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开关(switch)
|
||||||
|
|
||||||
|
宿主规格:`h-5 w-9`(行内)/ `h-6 w-10`(表单级),滑块 `size-4/5 rounded-full bg-white shadow`,开 `bg-accent`、关 `bg-line-strong`。
|
||||||
|
|
||||||
|
## 5. iframe 插件页附加规则
|
||||||
|
|
||||||
|
- `<head>` 必须引入 `<link rel="stylesheet" href="peer://assets/tokens.css" />`,并用同名 token 编译自己的 Tailwind(token 名 → 变量:`page/ink/ink-2/ink-3/line/line-strong/accent…`)
|
||||||
|
- 主题跟随宿主推送(`useHarnessTheme`),**禁止** `prefers-color-scheme` 自测
|
||||||
|
- 与宿主的视觉差异应趋近于零:同 token + 同类名词汇 + 同字号阶
|
||||||
|
|
||||||
|
## 6. renderer 组件附加规则
|
||||||
|
|
||||||
|
- 类名直接用宿主词汇(§2),无需引样式
|
||||||
|
- React 必须来自 `ctx.react`(见 [L4](./05-renderer-components.md)),否则 hooks 失效
|
||||||
|
- 禁止操作宿主 DOM(document 全局查询/改样式),一切交互限制在自己的组件子树
|
||||||
|
|
||||||
|
## 7. 自检清单(提发布前)
|
||||||
|
|
||||||
|
- [ ] 无硬编码色值/阴影/圆角(铁律 1)
|
||||||
|
- [ ] 字号都在 §3 的阶上
|
||||||
|
- [ ] 深浅色都检查过(切主题看一眼;token 正确时自动成立)
|
||||||
|
- [ ] 复用了 §4 模式而非新造形态
|
||||||
|
- [ ] iframe 页引了 tokens.css 且无 `prefers-color-scheme`
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 插件开发教程(渐进式)
|
||||||
|
|
||||||
|
> 面向「写插件的人(含 Agent)」。从零到高级共 6 篇,每篇 10-30 分钟,逐篇递进、可独立验证。
|
||||||
|
> 契约细节(类型/字段全集)见 [../plugins.md](../plugins.md);UI 规范见 [DESIGN.md](./DESIGN.md)。
|
||||||
|
|
||||||
|
## 学习路径
|
||||||
|
|
||||||
|
| 篇 | 内容 | 产出 |
|
||||||
|
|---|---|---|
|
||||||
|
| [L0 第一个插件](./01-hello-plugin.md) | manifest + main.mjs,装载/启停/日志 | 侧栏可见、日志可查的最小插件 |
|
||||||
|
| [L1 工具与命令](./02-tools-and-commands.md) | 给 Agent 注册工具、斜杠命令、事件 | Agent 能力扩展 |
|
||||||
|
| [L2 设置表单](./03-settings.md) | 声明式 schema、13 种标准控件、一级入口 | 用户可配置的插件 |
|
||||||
|
| [L3 UI 槽位](./04-ui-slots.md) | 状态胶囊/工具栏按钮/视图/面板 tab/iframe 页 | 界面存在感 |
|
||||||
|
| [L4 React 组件](./05-renderer-components.md) | entry.renderer:宿主内 React 组件 + JSX 构建配方 | 一等公民级 UI |
|
||||||
|
| [L5 自诊断](./06-self-diagnosis.md) | 日志规范、排障闭环、Agent 自迭代约定 | 可被 Agent 维护的插件 |
|
||||||
|
|
||||||
|
## 30 秒总览
|
||||||
|
|
||||||
|
```
|
||||||
|
<插件目录>/ 三个可选位置(按优先级):
|
||||||
|
manifest.json 1. userData/plugins/<id>/ (应用托管)
|
||||||
|
main.mjs (可选) 2. <工作区>/.pi/extensions/<id>/ (项目级)
|
||||||
|
renderer.mjs (可选) 3. ~/.pi/agent/extensions/<id>/ (全局)
|
||||||
|
index.html (可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **main.mjs**:Agent 能力(工具/命令/事件)+ 声明设置表单 + 内联 UI 数据
|
||||||
|
- **renderer.mjs**:宿主内 React 组件(需构建,见 L4)
|
||||||
|
- **index.html**:iframe 插件页(物理隔离,桥接见 L3)
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- 第三方插件装载默认**仅开发版开启**(打包版关闭,`ASO_EXTERNAL_PLUGINS=1` 可豁免)
|
||||||
|
- 日志文件:`userData/logs/main-YYYY-MM-DD.log`(排查问题的第一入口,见 L5)
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
| v0.4.0 | 工具与审批 | 工具调用卡片(参数/结果折叠展示)、tool_call 拦截 → Approval Card、defineTool 业务工具骨架(本地可跑的演示工具) |
|
| v0.4.0 | 工具与审批 | 工具调用卡片(参数/结果折叠展示)、tool_call 拦截 → Approval Card、defineTool 业务工具骨架(本地可跑的演示工具) |
|
||||||
| v0.5.0 | 打磨 | 深浅主题、GSAP 动效细节、快捷键、错误态/空态、队列可视化、自动滚动策略 |
|
| v0.5.0 | 打磨 | 深浅主题、GSAP 动效细节、快捷键、错误态/空态、队列可视化、自动滚动策略 |
|
||||||
| v0.8.0 | 动态插件系统(M0–M3) | 两层能力模型(内置组件级/外置 iframe)、pi 原生契约聚合 extension、@aso/plugin-sdk、plugin://peer:// 协议、视图切换+聊天保活+徽标、侧栏/面板/胶囊/深链槽位、浏览器面板插件化、插件重载 |
|
| v0.8.0 | 动态插件系统(M0–M3) | 两层能力模型(内置组件级/外置 iframe)、pi 原生契约聚合 extension、@aso/plugin-sdk、plugin://peer:// 协议、视图切换+聊天保活+徽标、侧栏/面板/胶囊/深链槽位、浏览器面板插件化、插件重载 |
|
||||||
|
| v0.9.0 | 插件一等公民 + 可观测 | entry.renderer 宿主内 React 组件(Blob 装载+单实例 React)、.pi/extensions 一等插件源(build 开关门控)、声明式设置体系(13 控件+一级导航入口)、统一日志系统(滚动文件/api.log/IPC 追踪/渲染层错误归档)、渐进式教程 docs/plugins/ + DESIGN.md + plugin-template |
|
||||||
| 未来 | | 插件管理与 zip 导入(M4)、ed25519 签名在线更新(M5)、外置插件模板与开发工作流(M6)、pi 原生扩展目录管控(M7)、B3/B4/B5 槽位、electron-builder 打包与自动更新、i18n(中/英)、sqlite 持久化与迁移、本地备份恢复、热点词/生成/模板业务插件、手动模式编排 UI、RPC 子进程隔离模式 |
|
| 未来 | | 插件管理与 zip 导入(M4)、ed25519 签名在线更新(M5)、外置插件模板与开发工作流(M6)、pi 原生扩展目录管控(M7)、B3/B4/B5 槽位、electron-builder 打包与自动更新、i18n(中/英)、sqlite 持久化与迁移、本地备份恢复、热点词/生成/模板业务插件、手动模式编排 UI、RPC 子进程隔离模式 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# plugin-template · 插件脚手架
|
||||||
|
|
||||||
|
复制本目录到插件位置(改目录名 = 改前先改 manifest.id),改掉 `id`/`name`/`description` 即完成最小定制。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
```
|
||||||
|
manifest.json 清单(id/name/version/minContract 必改)
|
||||||
|
main.mjs 主进程侧:Agent 能力 + 设置声明 + 日志埋点(范例齐备)
|
||||||
|
src/renderer.tsx 宿主内 React 组件源码(JSX)
|
||||||
|
shims/ 构建期把 react 别名到宿主单例(勿改)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建渲染层组件
|
||||||
|
|
||||||
|
```sh
|
||||||
|
esbuild src/renderer.tsx --bundle --format=esm --outfile=renderer.mjs \
|
||||||
|
--jsx=automatic \
|
||||||
|
--alias:react=./shims/react.js --alias:react/jsx-runtime=./shims/jsx-runtime.js
|
||||||
|
```
|
||||||
|
|
||||||
|
(esbuild 可用 `pnpm dlx esbuild@<版本>` 或任意 ≥0.17 版本;`--alias` 需 ≥0.17。)
|
||||||
|
不需要 UI 时:删除 `entry.renderer` 与 `src/`、`shims/`,只保留 main.mjs。
|
||||||
|
|
||||||
|
## 放置位置(三选一,按优先级)
|
||||||
|
|
||||||
|
1. `userData/plugins/<id>/`(应用托管目录)
|
||||||
|
2. `<工作区>/.pi/extensions/<id>/`(项目级)
|
||||||
|
3. `~/.pi/agent/extensions/<id>/`(全局)
|
||||||
|
|
||||||
|
第三方装载仅开发版开启(`ASO_EXTERNAL_PLUGINS=1` 豁免)。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. 重启或开关重载 → 设置 → 插件 → 对应作用域出现插件
|
||||||
|
2. 日志 `userData/logs/main-*.log` 有 `plugin activated pluginId=my-plugin`
|
||||||
|
3. 对 Agent 说"ping 一下我的插件" → 触发 `my_plugin_ping`
|
||||||
|
4. 面板/侧栏出现 React 组件,点击 +1 正常(hooks 生效)
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 教程索引:`docs/plugins/README.md`(L0→L5)
|
||||||
|
- 视觉规范:`docs/plugins/DESIGN.md`(提发布前过一遍自检清单)
|
||||||
|
- 契约全集:`docs/plugins.md`
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// 主进程侧:Agent 能力(pi 工具/命令/事件)+ 设置声明 + 内联 UI。
|
||||||
|
// 日志规范见 docs/plugins/06-self-diagnosis.md:激活时 info 一条能力清单,一切 catch 记 error。
|
||||||
|
import { definePlugin } from '@aso/plugin-sdk'
|
||||||
|
|
||||||
|
export default definePlugin((api) => {
|
||||||
|
const log = api.log
|
||||||
|
log.info('activated', { manifest: api.manifest.version })
|
||||||
|
|
||||||
|
// ---- 设置(用户可配置;值为宿主持久化) ----
|
||||||
|
api.ui.registerSettings({
|
||||||
|
items: [
|
||||||
|
{ type: 'string', key: 'greeting', label: '问候语', default: 'Hello', placeholder: 'Hello' },
|
||||||
|
{ type: 'boolean', key: 'verbose', label: '详细日志', default: false }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
let values = api.ui.getSettingValues()
|
||||||
|
api.ui.onSettingsChange((next) => {
|
||||||
|
values = next
|
||||||
|
log.info('settings changed', { greeting: next.greeting })
|
||||||
|
})
|
||||||
|
api.ui.onSettingsAction((key) => {
|
||||||
|
log.info('settings action', { key })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- Agent 工具示例 ----
|
||||||
|
api.pi.registerTool({
|
||||||
|
name: 'my_plugin_ping',
|
||||||
|
description: '返回问候语(示例工具:把这段换成真正的能力)',
|
||||||
|
parameters: {},
|
||||||
|
async execute() {
|
||||||
|
log.debug('ping called', { verbose: values.verbose })
|
||||||
|
return { output: `${values.greeting ?? 'Hello'} from my-plugin` }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- 面板/视图组件在 renderer.mjs(JSX 构建见 README);无 UI 需求可整段去掉 ----
|
||||||
|
log.info('ready', { hasRenderer: true })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
log.info('disposed')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "my-plugin",
|
||||||
|
"name": "我的插件",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"minContract": "0.2.0",
|
||||||
|
"description": "TODO:一句话描述插件做什么",
|
||||||
|
"order": 50,
|
||||||
|
"panel": true,
|
||||||
|
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/></svg>",
|
||||||
|
"entry": {
|
||||||
|
"main": "main.mjs",
|
||||||
|
"renderer": "renderer.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// 构建期别名目标:react/jsx-runtime → 宿主全局单例(esbuild --jsx=automatic)
|
||||||
|
export const { jsx, jsxs, Fragment } = window.__ASO_PLUGIN_HOST__.jsxRuntime
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
// 构建期别名目标:react → 宿主全局单例(双 React 实例会导致 hooks 失效)
|
||||||
|
const R = window.__ASO_PLUGIN_HOST__.react
|
||||||
|
export default R
|
||||||
|
export const {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
Fragment,
|
||||||
|
memo,
|
||||||
|
forwardRef,
|
||||||
|
createElement
|
||||||
|
} = R
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 渲染层组件源码(JSX)。构建:
|
||||||
|
* esbuild src/renderer.tsx --bundle --format=esm --outfile=renderer.mjs \
|
||||||
|
* --jsx=automatic --alias:react=./shims/react.js --alias:react/jsx-runtime=./shims/jsx-runtime.js
|
||||||
|
* 产物 renderer.mjs 是自包含单文件,放回插件目录即被宿主装载。
|
||||||
|
* 样式类名只用 DESIGN.md 里的宿主词汇。
|
||||||
|
*/
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function mount(ctx) {
|
||||||
|
function Panel() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col gap-2 p-3">
|
||||||
|
<div className="text-[12.5px] text-ink">我的插件面板</div>
|
||||||
|
<div className="text-[12px] text-ink-3">计数:{count}</div>
|
||||||
|
<button
|
||||||
|
className="self-start rounded-control border border-line-strong px-2.5 py-1 text-[11.5px] text-ink hover:bg-hover"
|
||||||
|
onClick={() => setCount((c) => c + 1)}
|
||||||
|
>
|
||||||
|
+1
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.registerPanel(Panel)
|
||||||
|
ctx.registerView(Panel)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user