docs: 渐进式插件教程(L0→L5)+ DESIGN.md 视觉规范 + plugin-template 脚手架
This commit is contained in:
+3
-1
@@ -1,7 +1,9 @@
|
||||
# 插件系统(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)
|
||||
> 契约版本:`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.5.0 | 打磨 | 深浅主题、GSAP 动效细节、快捷键、错误态/空态、队列可视化、自动滚动策略 |
|
||||
| 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 子进程隔离模式 |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user