@ai_mind/ade-plugin-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1240 -17
- package/index.d.ts +5 -1
- package/package.json +1 -1
- package/types/ade-types.d.ts +1 -176
- package/types/api.d.ts +1 -42
- package/types/plugins.d.ts +31 -0
package/README.md
CHANGED
|
@@ -1,19 +1,116 @@
|
|
|
1
|
-
# ADE 插件 SDK
|
|
1
|
+
# ADE 插件 SDK 与接入指南
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@ai_mind/ade-plugin-sdk` 是 ADE 的公开 npm SDK。它提供 TypeScript 类型、插件 helper 和 `ade-plugin-build` 构建器。开发插件不需要获取 ADE 源码。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
插件可以增加自己的功能区域、工具按钮、设置组件和聊天 adapter。插件还可以调用 ADE 的文件、项目、worktree 和对话 API。需要 Node.js 时,在插件的 main 入口实现功能。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
本 README 同时是 ADE 内置的插件开发指南。在 ADE 中打开「设置 → 插件 → 开发指南」,即可离线阅读同一份内容。SDK 是开发依赖;用户安装到 ADE 的是你构建的插件包。
|
|
8
|
+
|
|
9
|
+
## 1. 阅读路线与开发条件
|
|
10
|
+
|
|
11
|
+
- 首次接入:阅读第 2 节,然后完成第 3 节。
|
|
12
|
+
- 需要 Node.js:完成第 5 节的文件统计示例。
|
|
13
|
+
- 需要扩展界面:阅读第 6 节。
|
|
14
|
+
- 需要插件间协作:阅读第 8 节。
|
|
15
|
+
- 需要接入 agent CLI 或模型 SDK:阅读第 10 节。
|
|
16
|
+
- 准备交付:完成第 12 节与第 14 节。
|
|
17
|
+
- 不使用 TypeScript:使用第 13 节的 JavaScript 示例。
|
|
18
|
+
|
|
19
|
+
| 开发条件 | 要求 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| ADE | 0.46.0 或更新的 0.x 版本;使用 SDK 0.2 的新增接口时需要此版本 |
|
|
22
|
+
| Node.js | 18 或更新版本;用于 npm 安装和构建 |
|
|
23
|
+
| 插件 API | `apiVersion: 1` |
|
|
24
|
+
| SDK | 安装 `@ai_mind/ade-plugin-sdk@0` |
|
|
25
|
+
| React | ADE 提供 React 19 与 ReactDOM;插件共享宿主实例 |
|
|
26
|
+
| 操作系统 | main 使用的命令、路径和原生依赖必须支持用户的平台 |
|
|
27
|
+
|
|
28
|
+
### 1.1 版本规则
|
|
29
|
+
|
|
30
|
+
SDK 与 ADE 的 **major 必须相同**。minor 和 patch 分别发布。
|
|
31
|
+
|
|
32
|
+
| 版本字段 | 含义 | 示例 |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| SDK npm 版本 | 开发工具和类型的版本 | SDK `0.x` 对应 ADE `0.x` |
|
|
35
|
+
| `plugin.json.adeMajor` | 插件面向的 ADE major | `0` |
|
|
36
|
+
| `plugin.json.apiVersion` | 插件宿主 API 协议版本 | `1` |
|
|
37
|
+
| `plugin.json.version` | 你自己的插件版本 | `1.0.0` |
|
|
38
|
+
|
|
39
|
+
SDK major 相同不代表所有 ADE minor 都提供相同能力。基础区域和 slot 从 ADE 0.45.0 起提供。SDK 0.2 增加的文件菜单、工作区操作和提示接口要求 ADE 0.46.0 或更新版本。使用以后增加的 API 时,先确认用户的 ADE 提供该 API。
|
|
40
|
+
|
|
41
|
+
builder 根据已安装 SDK 的 major 写入 `adeMajor`。如果 manifest 已声明不同 major,构建失败。手写 JavaScript 插件也应声明 `adeMajor`。
|
|
42
|
+
|
|
43
|
+
ADE 在执行入口前检查 `adeMajor` 和 `apiVersion`。不匹配时,插件列表显示错误。旧插件没有 `adeMajor` 时,ADE 保留按 `apiVersion` 检查的兼容行为。
|
|
44
|
+
|
|
45
|
+
## 2. 插件机制与能力边界
|
|
46
|
+
|
|
47
|
+
### 2.1 插件如何运行
|
|
48
|
+
|
|
49
|
+
一个插件是包含 `plugin.json` 的目录。manifest 指定运行文件、配置表单和可选的静态聊天 adapter。
|
|
50
|
+
|
|
51
|
+
安装时,ADE 将文件复制到用户插件目录。启用时,宿主校验 manifest,加载入口并调用 `activate(ctx)`。入口通过上下文 `ctx` 注册能力。停用时,宿主撤销注册,并执行清理函数。
|
|
52
|
+
|
|
53
|
+
| 入口 | 运行位置 | 主要职责 |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `main` | Electron 主进程 | Node.js、文件处理、进程、网络、聊天 adapter、主进程命令与服务 |
|
|
56
|
+
| `renderer` | ADE 的 React 树 | 功能区域、slot、交互、配置 UI、调用 preload API |
|
|
57
|
+
|
|
58
|
+
插件可以只声明一个入口。需要 Node.js 和 UI 时,声明两个入口。
|
|
59
|
+
|
|
60
|
+
main 和 renderer 是两个独立实例。它们不共享变量、服务注册表或事件总线。renderer 通过命令或 preload API 调用 main。跨进程参数和结果必须能被 Electron IPC 序列化。
|
|
61
|
+
|
|
62
|
+
插件按**可信本地代码**运行。main 保留完整 Node.js 能力。renderer 与 ADE 共享页面和 React。宿主没有为插件提供隔离沙箱。
|
|
63
|
+
|
|
64
|
+
### 2.2 可以实现哪些功能
|
|
65
|
+
|
|
66
|
+
| 能力 | 接入点 | 示例 |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| 独立功能区域 | renderer `registerView` | 看板、搜索、任务列表、数据浏览 |
|
|
69
|
+
| 状态栏组件 | renderer `registerSlot` | 文件状态、连接状态、统计结果 |
|
|
70
|
+
| 文件工具按钮 | `editor.toolbar` slot | 统计文件、复制路径、运行检查 |
|
|
71
|
+
| 插件设置组件 | `settings` slot | 自定义配置表单、连接测试 |
|
|
72
|
+
| 自定义扩展位置 | SDK `Slot` 与 `registerSlot` | 允许其他插件扩展你的工具栏 |
|
|
73
|
+
| 文件树菜单 | renderer `registerFileMenu` | 为目录和文件增加操作 |
|
|
74
|
+
| 编辑器与文件树交互 | renderer `ctx.workspace` | 打开文件、刷新目录、检查未保存状态 |
|
|
75
|
+
| 操作提示 | renderer `ctx.ui.showToast` | 显示结果或带链接的提示 |
|
|
76
|
+
| 文件、项目与 worktree 操作 | renderer `ctx.api`;main 内置服务或 Node.js | 读取文件、创建文件、查询 worktree |
|
|
77
|
+
| 对话与 agent 接入 | main `registerAdapter`;renderer `ctx.api.chat` | agent CLI、模型 SDK、会话工具 |
|
|
78
|
+
| 配置与持久数据 | `getConfig/setConfig`;main `dataPath` | 用户选项、插件缓存 |
|
|
79
|
+
| 插件间调用 | 命令、服务、事件 | 提供可复用功能、通知同进程插件 |
|
|
80
|
+
| 插件样式与资源 | manifest `styles`;renderer `addStyle` | 区域样式、图标、字体 |
|
|
81
|
+
|
|
82
|
+
### 2.3 当前边界
|
|
83
|
+
|
|
84
|
+
- 功能区域是 ADE 布局中的 React 区域。注册区域不会创建 Git worktree。
|
|
85
|
+
- 插件区域排在核心区域之后。当前不能用 `registerView` 替换核心编辑器或聊天区域。
|
|
86
|
+
- UI 上下文只提供项目、worktree 和当前文件的路径。它不提供编辑器实例、未保存正文或选区。
|
|
87
|
+
- 读取文件得到磁盘内容。统计未保存草稿需要以后提供的编辑器扩展 API。
|
|
88
|
+
- API 添加项目或创建 worktree,不会自动切换界面的当前选择。
|
|
89
|
+
- 插件没有自动更新、自动编译或开发目录热加载。更新代码后,需要构建并重新安装。
|
|
90
|
+
- 注册聊天 adapter 不会自动创建 daemon/relay provider。
|
|
91
|
+
- ADE 采用可撤销注册和 React 扩展点。DeepSeek Harness 插件需要按本指南移植。
|
|
92
|
+
|
|
93
|
+
## 3. 快速入门:创建一个 React 功能区域
|
|
94
|
+
|
|
95
|
+
**目标:**安装后出现「我的插件」区域。点击按钮时,计数从 0 变为 1。
|
|
96
|
+
|
|
97
|
+
### 步骤 1:创建开发目录
|
|
98
|
+
|
|
99
|
+
在终端执行:
|
|
8
100
|
|
|
9
101
|
```sh
|
|
102
|
+
mkdir my-plugin
|
|
103
|
+
cd my-plugin
|
|
10
104
|
npm init -y
|
|
11
|
-
npm install --save-dev @ai_mind/ade-plugin-sdk@0 @types/react @types/react-dom
|
|
105
|
+
npm install --save-dev @ai_mind/ade-plugin-sdk@0 @types/react@19 @types/react-dom@19
|
|
106
|
+
mkdir src
|
|
12
107
|
```
|
|
13
108
|
|
|
14
|
-
|
|
109
|
+
后续命令均在这个目录执行。
|
|
110
|
+
|
|
111
|
+
### 步骤 2:保存 manifest
|
|
15
112
|
|
|
16
|
-
|
|
113
|
+
保存以下内容为 `plugin.json`:
|
|
17
114
|
|
|
18
115
|
```json
|
|
19
116
|
{
|
|
@@ -27,7 +124,9 @@ npm install --save-dev @ai_mind/ade-plugin-sdk@0 @types/react @types/react-dom
|
|
|
27
124
|
}
|
|
28
125
|
```
|
|
29
126
|
|
|
30
|
-
|
|
127
|
+
### 步骤 3:保存 renderer 源码
|
|
128
|
+
|
|
129
|
+
保存以下内容为 `src/renderer.tsx`:
|
|
31
130
|
|
|
32
131
|
```tsx
|
|
33
132
|
import { useState } from 'react'
|
|
@@ -40,29 +139,1153 @@ export default definePlugin({
|
|
|
40
139
|
const [count, setCount] = useState(0)
|
|
41
140
|
return <button onClick={() => setCount(count + 1)}>点击计数:{count}</button>
|
|
42
141
|
}
|
|
43
|
-
ctx.registerView({
|
|
142
|
+
ctx.registerView({
|
|
143
|
+
id: 'counter', title: '我的插件', component: Counter,
|
|
144
|
+
defaultVisible: true, defaultSize: 22, minSize: 10
|
|
145
|
+
})
|
|
44
146
|
}
|
|
45
147
|
})
|
|
46
148
|
```
|
|
47
149
|
|
|
48
|
-
|
|
150
|
+
`definePlugin` 保留传入的插件对象。它不会启动宿主或调用 `activate`。ADE 负责调用入口。
|
|
151
|
+
|
|
152
|
+
### 步骤 4:构建插件
|
|
153
|
+
|
|
154
|
+
执行:
|
|
49
155
|
|
|
50
156
|
```sh
|
|
51
157
|
npx ade-plugin-build .
|
|
52
158
|
```
|
|
53
159
|
|
|
54
|
-
|
|
160
|
+
确认目录中出现 `lib/renderer.js`。ADE 加载这个文件,不加载 `src/renderer.tsx`。
|
|
161
|
+
|
|
162
|
+
### 步骤 5:安装并验证
|
|
163
|
+
|
|
164
|
+
1. 打开 ADE 的「设置 → 插件」。
|
|
165
|
+
2. 选择「本地目录」。
|
|
166
|
+
3. 选择 `my-plugin` 目录。
|
|
167
|
+
4. 启用插件。
|
|
168
|
+
5. 在「我的插件」区域点击按钮。
|
|
169
|
+
|
|
170
|
+
预期结果:按钮显示「点击计数:1」。区域可以拖拽调整大小,也可以在「视图」菜单中显示或隐藏。
|
|
171
|
+
|
|
172
|
+
### 步骤 6:更新代码
|
|
173
|
+
|
|
174
|
+
1. 修改 `src/renderer.tsx`。
|
|
175
|
+
2. 再次执行 `npx ade-plugin-build .`。
|
|
176
|
+
3. 在 ADE 重新安装同一个目录。
|
|
177
|
+
|
|
178
|
+
相同插件 ID 会更新已安装副本,并保留配置。仅停用再启用,会继续加载旧副本。
|
|
179
|
+
|
|
180
|
+
## 4. 目录与 manifest
|
|
181
|
+
|
|
182
|
+
### 4.1 推荐目录
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
my-plugin/
|
|
186
|
+
├── plugin.json
|
|
187
|
+
├── package.json
|
|
188
|
+
├── package-lock.json
|
|
189
|
+
├── src/
|
|
190
|
+
│ ├── renderer.tsx
|
|
191
|
+
│ ├── main.ts # 需要 Node.js 时添加
|
|
192
|
+
│ └── style.css # 按需添加
|
|
193
|
+
└── lib/
|
|
194
|
+
├── renderer.js # 构建产物
|
|
195
|
+
├── main.cjs # main 构建产物
|
|
196
|
+
└── assets/ # 构建生成的资源
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
源码用于开发。交付包必须包含 manifest 指向的运行文件、样式和资源。
|
|
200
|
+
|
|
201
|
+
### 4.2 manifest 字段
|
|
202
|
+
|
|
203
|
+
| 字段 | 类型 | 用途 |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `id` | string | 稳定技术标识。新插件显式声明小写 ID,例如 `file-statistics` |
|
|
206
|
+
| `name` | string | 插件管理页的显示名 |
|
|
207
|
+
| `version` | string | 插件自身版本 |
|
|
208
|
+
| `description` | string,可选 | 功能说明 |
|
|
209
|
+
| `apiVersion` | number | 新插件声明 `1` |
|
|
210
|
+
| `adeMajor` | number | 目标 ADE major;当前声明 `0` |
|
|
211
|
+
| `defaultEnabled` | boolean,可选 | 默认启用状态;用户可以修改 |
|
|
212
|
+
| `main` | string,可选 | 主进程 JavaScript 入口 |
|
|
213
|
+
| `renderer` | string,可选 | 渲染进程 JavaScript 入口 |
|
|
214
|
+
| `rendererFormat` | `register` 或 `module` | builder 产物用 `register`;浏览器 ESM 用 `module` |
|
|
215
|
+
| `styles` | string[],可选 | 随插件加载和移除的 CSS 文件 |
|
|
216
|
+
| `settings` | object,可选 | 自动生成的配置表单,见第 7 节 |
|
|
217
|
+
| `adapters` | array,可选 | 静态终端 adapter,见第 10 节 |
|
|
218
|
+
|
|
219
|
+
入口和样式路径相对于插件根目录。路径必须留在插件目录内。不要使用 `../` 指向插件外的运行文件。
|
|
220
|
+
|
|
221
|
+
升级时保持 `id` 不变。配置、命令命名空间和数据目录使用这个 ID。npm 的包名可以带组织前缀;插件 ID 使用自己的小写技术标识。
|
|
222
|
+
|
|
223
|
+
## 5. 完整接入:main 命令、React UI 与配置
|
|
224
|
+
|
|
225
|
+
**目标:**增加「文件统计」区域和文件工具按钮。按钮通过 main 读取已保存文件。配置控制字符统计是否包含空白。
|
|
226
|
+
|
|
227
|
+
这个示例独立于第 3 节。创建新的 `file-statistics` 目录。
|
|
228
|
+
|
|
229
|
+
### 步骤 1:安装开发依赖
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
mkdir file-statistics
|
|
233
|
+
cd file-statistics
|
|
234
|
+
npm init -y
|
|
235
|
+
npm install --save-dev @ai_mind/ade-plugin-sdk@0 @types/react@19 @types/react-dom@19 @types/node@24
|
|
236
|
+
mkdir src
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 步骤 2:保存 plugin.json
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"id": "file-statistics",
|
|
244
|
+
"name": "文件统计",
|
|
245
|
+
"version": "1.0.0",
|
|
246
|
+
"apiVersion": 1,
|
|
247
|
+
"adeMajor": 0,
|
|
248
|
+
"main": "lib/main.cjs",
|
|
249
|
+
"renderer": "lib/renderer.js",
|
|
250
|
+
"rendererFormat": "register",
|
|
251
|
+
"settings": {
|
|
252
|
+
"caption": {
|
|
253
|
+
"type": "string", "label": "状态栏文字", "default": "文件统计"
|
|
254
|
+
},
|
|
255
|
+
"includeWhitespace": {
|
|
256
|
+
"type": "boolean", "label": "统计空白字符", "default": true
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 步骤 3:保存 src/main.ts
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { readFile } from 'node:fs/promises'
|
|
266
|
+
import { definePlugin } from '@ai_mind/ade-plugin-sdk'
|
|
267
|
+
import type { MainPluginContext } from '@ai_mind/ade-plugin-sdk'
|
|
268
|
+
|
|
269
|
+
export default definePlugin({
|
|
270
|
+
activate(ctx: MainPluginContext) {
|
|
271
|
+
ctx.registerCommand('stats', async (_args, context) => {
|
|
272
|
+
if (!context.filePath) throw new Error('请先打开一个文本文件')
|
|
273
|
+
let text: string
|
|
274
|
+
try {
|
|
275
|
+
text = await readFile(context.filePath, 'utf8')
|
|
276
|
+
} catch {
|
|
277
|
+
throw new Error('无法读取当前文件,请确认文件存在且可读取')
|
|
278
|
+
}
|
|
279
|
+
const includeWhitespace = ctx.getConfig().includeWhitespace !== false
|
|
280
|
+
const counted = includeWhitespace ? text : text.replace(/\s/g, '')
|
|
281
|
+
return {
|
|
282
|
+
filePath: context.filePath,
|
|
283
|
+
lines: text.length === 0 ? 0 : text.split(/\r?\n/).length,
|
|
284
|
+
characters: [...counted].length,
|
|
285
|
+
includeWhitespace
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`stats` 是插件内部的命令 ID。handler 的第二个参数是调用时的 UI 上下文。没有活动文件时,命令返回中文错误。
|
|
293
|
+
|
|
294
|
+
这个示例按 Unicode 码点统计字符。行数按换行分段计算;末尾换行会增加一个空行。它读取 UTF-8 文本,不用于图片或其他二进制文件。
|
|
295
|
+
|
|
296
|
+
### 步骤 4:保存 src/renderer.tsx
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
import { useEffect, useRef, useState } from 'react'
|
|
300
|
+
import { definePlugin } from '@ai_mind/ade-plugin-sdk'
|
|
301
|
+
import type { PluginComponentProps, RendererPluginContext } from '@ai_mind/ade-plugin-sdk'
|
|
302
|
+
|
|
303
|
+
interface Statistics {
|
|
304
|
+
filePath: string
|
|
305
|
+
lines: number
|
|
306
|
+
characters: number
|
|
307
|
+
includeWhitespace: boolean
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default definePlugin({
|
|
311
|
+
activate(ctx: RendererPluginContext) {
|
|
312
|
+
function ReadButton({ context }: PluginComponentProps) {
|
|
313
|
+
const [result, setResult] = useState<Statistics | null>(null)
|
|
314
|
+
const [error, setError] = useState('')
|
|
315
|
+
const [busy, setBusy] = useState(false)
|
|
316
|
+
const mounted = useRef(false)
|
|
317
|
+
const config = ctx.useConfig()
|
|
318
|
+
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
mounted.current = true
|
|
321
|
+
return () => { mounted.current = false }
|
|
322
|
+
}, [])
|
|
323
|
+
|
|
324
|
+
async function calculate() {
|
|
325
|
+
setBusy(true)
|
|
326
|
+
setError('')
|
|
327
|
+
try {
|
|
328
|
+
const next = await ctx.executeCommand('stats') as Statistics
|
|
329
|
+
if (mounted.current && ctx.getContext().filePath === next.filePath) {
|
|
330
|
+
setResult(next)
|
|
331
|
+
}
|
|
332
|
+
} catch (cause) {
|
|
333
|
+
if (mounted.current) {
|
|
334
|
+
setError(cause instanceof Error ? cause.message : '统计失败,请重试')
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
if (mounted.current) setBusy(false)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const current = result?.filePath === context.filePath &&
|
|
342
|
+
result.includeWhitespace === (config.includeWhitespace !== false) ? result : null
|
|
343
|
+
|
|
344
|
+
return <span>
|
|
345
|
+
<button type="button" onClick={calculate} disabled={!context.filePath || busy}>
|
|
346
|
+
{busy ? '正在统计…' : '统计已保存文件'}
|
|
347
|
+
</button>
|
|
348
|
+
{current && <output> {current.lines} 行 · {current.characters} 字符</output>}
|
|
349
|
+
{error && <span role="alert"> {error}</span>}
|
|
350
|
+
</span>
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function Panel(props: PluginComponentProps) {
|
|
354
|
+
return <section style={{ padding: 16, fontFamily: 'var(--ade-font-sans)' }}>
|
|
355
|
+
<h2>文件统计</h2>
|
|
356
|
+
<p>{props.context.filePath ?? '尚未打开文件'}</p>
|
|
357
|
+
<ReadButton {...props} />
|
|
358
|
+
</section>
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
ctx.registerView({ id: 'panel', title: '文件统计', component: Panel })
|
|
362
|
+
ctx.registerSlot('statusBar.right', {
|
|
363
|
+
id: 'status', component: ({ config }) => <span>{String(config.caption)}</span>
|
|
364
|
+
})
|
|
365
|
+
ctx.registerSlot('editor.toolbar', {
|
|
366
|
+
id: 'calculate', component: ReadButton,
|
|
367
|
+
when: context => Boolean(context.filePath)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
React UI 负责显示结果。main 负责文件读取。组件卸载后,异步结果不会更新组件状态。切换文件或修改统计配置后,旧结果不再显示。
|
|
374
|
+
|
|
375
|
+
### 步骤 5:构建两个入口
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
npx ade-plugin-build . --main-entry src/main.ts
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
确认 `lib/main.cjs` 和 `lib/renderer.js` 都存在。然后按第 3 节的安装步骤安装 `file-statistics`。
|
|
382
|
+
|
|
383
|
+
### 步骤 6:验证调用链
|
|
384
|
+
|
|
385
|
+
1. 在 ADE 打开一个已保存的 UTF-8 文本文件。
|
|
386
|
+
2. 点击「文件统计」区域内的「统计已保存文件」。
|
|
387
|
+
3. 确认区域显示行数和字符数。
|
|
388
|
+
4. 在插件设置中关闭「统计空白字符」。
|
|
389
|
+
5. 保存配置。
|
|
390
|
+
6. 再次点击统计按钮。
|
|
391
|
+
|
|
392
|
+
预期结果:字符数不再包含空格和换行。修改「状态栏文字」并保存后,状态栏立即显示新文字。
|
|
393
|
+
|
|
394
|
+
文件工具栏也有同名按钮。点击工具栏按钮时,结果显示在按钮旁。区域和工具栏各自保存自己的统计结果。
|
|
395
|
+
|
|
396
|
+
### 5.1 命令的路由规则
|
|
397
|
+
|
|
398
|
+
| 调用 | 执行位置 |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| renderer `ctx.executeCommand('stats', args)` | 先查本插件 renderer 命令;没有时查本插件 main 命令 |
|
|
401
|
+
| renderer `ctx.executeCommand('plugin:file-statistics:stats', args)` | 指定插件的命令,按同样规则查找 |
|
|
402
|
+
| renderer `ctx.api.plugins.executeCommand(pluginId, command, args, context)` | 直接调用指定插件的 main 命令 |
|
|
403
|
+
|
|
404
|
+
renderer 和 main 都可以 `registerCommand`。main 上下文没有 `executeCommand`。不要在 main 中复制 renderer 调用代码。
|
|
405
|
+
|
|
406
|
+
跨进程传普通数据,例如字符串、数字、数组和对象。不要传 React 元素、函数、DOM 节点或 Node.js 实例。
|
|
407
|
+
|
|
408
|
+
## 6. UI 区域、slot 与上下文
|
|
409
|
+
|
|
410
|
+
### 6.1 注册功能区域
|
|
411
|
+
|
|
412
|
+
`ctx.registerView(view)` 返回清理函数。宿主自动管理这个注册。
|
|
413
|
+
|
|
414
|
+
| view 字段 | 含义 |
|
|
415
|
+
| --- | --- |
|
|
416
|
+
| `id` | 插件内部唯一 ID |
|
|
417
|
+
| `title` | 中文区域标题 |
|
|
418
|
+
| `component` | React 组件,接收 `PluginComponentProps` |
|
|
419
|
+
| `defaultVisible` | 初始可见状态;用户之后可以修改 |
|
|
420
|
+
| `defaultSize` | 初始区域大小,按布局百分比设置 |
|
|
421
|
+
| `minSize` / `maxSize` | 区域大小限制 |
|
|
422
|
+
| `order` | 插件区域排序,数值越小越靠前 |
|
|
423
|
+
|
|
424
|
+
宿主为区域 ID 加上插件命名空间。不同插件可以使用相同局部 ID。布局由 ADE 保存。
|
|
425
|
+
|
|
426
|
+
view 没有 `when` 字段。需要空状态时,在组件中检查上下文。用户通过「视图」菜单控制区域可见性。
|
|
427
|
+
|
|
428
|
+
### 6.2 注册内置 slot
|
|
429
|
+
|
|
430
|
+
`ctx.registerSlot(name, registration)` 在指定位置追加组件。
|
|
431
|
+
|
|
432
|
+
| 内置 slot 名称 | 位置 |
|
|
433
|
+
| --- | --- |
|
|
434
|
+
| `statusBar.left` | 状态栏左侧 |
|
|
435
|
+
| `statusBar.right` | 状态栏右侧 |
|
|
436
|
+
| `editor.toolbar` | 当前文件的右上角工具栏 |
|
|
437
|
+
| `settings` | 当前插件的独立设置区域 |
|
|
438
|
+
|
|
439
|
+
registration 包含 `id`、`component`、可选的 `order` 和 `when(context)`。`when` 返回 false 时,组件不显示。同一 slot 按 `order` 升序排列。顺序相同时,按带命名空间的 ID 排列。
|
|
440
|
+
|
|
441
|
+
`settings` slot 只显示所选插件自己的组件。它不会把所有插件的设置混在一个页面。
|
|
442
|
+
|
|
443
|
+
### 6.3 创建自己的 slot
|
|
444
|
+
|
|
445
|
+
在拥有该位置的 renderer 组件中使用 SDK 的 `Slot`:
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
import { Slot } from '@ai_mind/ade-plugin-sdk'
|
|
449
|
+
|
|
450
|
+
export function Toolbar() {
|
|
451
|
+
return <div><Slot name="file-statistics.toolbar" /></div>
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
把 `Toolbar` 放入自己的已注册组件。其他插件可以在其 `activate(ctx)` 中追加组件:
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
ctx.registerSlot('file-statistics.toolbar', {
|
|
459
|
+
id: 'extra-button', component: () => <button type="button">扩展按钮</button>
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
后一段是 renderer `activate` 内的代码片段。`Slot` 必须由宿主渲染。它不是独立应用容器。
|
|
464
|
+
|
|
465
|
+
`Slot` 接收 `name` 和可选的 `pluginId` 过滤条件。它不转发自定义 props。贡献组件仍接收自己的 `{ context, config, api }`。共享额外数据时,使用同进程服务或事件。
|
|
466
|
+
|
|
467
|
+
### 6.4 读取上下文
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
interface PluginCommandContext {
|
|
471
|
+
projectPath: string | null
|
|
472
|
+
worktreePath: string | null
|
|
473
|
+
filePath: string | null
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| 读取方式 | 使用位置 | 行为 |
|
|
478
|
+
| --- | --- | --- |
|
|
479
|
+
| 组件 `props.context` | 已注册组件 | 宿主更新项目、worktree 和文件上下文 |
|
|
480
|
+
| `ctx.useContext()` | React 组件内部 | hook;变化时重新渲染 |
|
|
481
|
+
| `ctx.getContext()` | 点击回调、异步处理 | 获取调用时的最新上下文 |
|
|
482
|
+
| main 命令第二参数 | main handler | 获取命令发起时的上下文快照 |
|
|
483
|
+
|
|
484
|
+
三个路径都可能是 `null`。在操作前检查需要的路径。hook 只在 React 组件或自定义 hook 中调用。
|
|
485
|
+
|
|
486
|
+
组件的 `api` 与 renderer 的 `ctx.api` 都是 ADE preload API。renderer 使用该 API 访问本地能力。不要在 renderer 导入 `node:fs` 或使用 `require` 加载 Node.js 模块。
|
|
487
|
+
|
|
488
|
+
### 6.5 样式
|
|
489
|
+
|
|
490
|
+
在 `src/renderer.tsx` 导入 `./style.css`。builder 生成 CSS,并追加到 manifest 的 `styles`。
|
|
491
|
+
|
|
492
|
+
```css
|
|
493
|
+
.file-statistics-panel {
|
|
494
|
+
padding: 16px;
|
|
495
|
+
color: var(--ade-text);
|
|
496
|
+
font-family: var(--ade-font-sans);
|
|
497
|
+
}
|
|
498
|
+
.file-statistics-code {
|
|
499
|
+
font-family: var(--ade-font-mono);
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
类名使用插件前缀。宿主主题变量提供颜色和字体。避免全局 `button`、`body` 或 `*` 选择器影响其他区域。
|
|
504
|
+
|
|
505
|
+
renderer 也可以 `ctx.addStyle(cssText)`。宿主在停用时移除样式。不要直接修改其他功能的 DOM。
|
|
506
|
+
|
|
507
|
+
### 6.6 文件树菜单
|
|
508
|
+
|
|
509
|
+
SDK 0.2 可以在 renderer `activate` 中注册文件菜单:
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
ctx.registerFileMenu({
|
|
513
|
+
id: 'open-local', label: '在 ADE 中打开', order: 10,
|
|
514
|
+
when: context => context.node.type === 'file',
|
|
515
|
+
onSelect: async context => {
|
|
516
|
+
await ctx.workspace.openFile(context.node.path)
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
菜单上下文包含 `projectPath`、`worktreePath`、`filePath`,以及 `node`、`isRoot` 和 `selection`。节点提供 `name`、`path`、`type`。`selection` 是打开菜单时的选中项。
|
|
522
|
+
|
|
523
|
+
`when` 控制是否显示。`enabled` 控制是否可用。宿主先关闭菜单,再执行 `onSelect`。执行前再次检查插件状态和条件。停用插件后,菜单注册自动撤销。
|
|
524
|
+
|
|
525
|
+
### 6.7 工作区操作与提示
|
|
526
|
+
|
|
527
|
+
这些接口在 renderer 中使用,不属于 preload IPC:
|
|
528
|
+
|
|
529
|
+
| 方法 | 结果与边界 |
|
|
530
|
+
| --- | --- |
|
|
531
|
+
| `await ctx.workspace.openFile(path)` | 在 ADE 打开真实存在的文件,返回 boolean。目录和缺失文件不会进入标签页;等待期间切换 worktree 或停用插件时,不打开旧结果 |
|
|
532
|
+
| `await ctx.workspace.refreshTree(directory?)` | 默认刷新当前 worktree。指定目录必须在当前 worktree 内;保留已展开缓存;忽略切换后的旧结果 |
|
|
533
|
+
| `ctx.workspace.isFileDirty(path)` | 返回文件当前是否有未保存修改。操作开始前读取;此方法不是 React hook |
|
|
534
|
+
| `ctx.ui.showToast(message, url?)` | 显示中文提示;可以附结果链接 |
|
|
535
|
+
|
|
536
|
+
这些接口不暴露编辑器草稿或选区。上传磁盘内容时,先检查 dirty;为 true 时提示用户保存。等待用户选择上传目标后,在真正上传前再次检查。
|
|
537
|
+
|
|
538
|
+
## 7. 配置与持久数据
|
|
539
|
+
|
|
540
|
+
### 7.1 声明配置表单
|
|
541
|
+
|
|
542
|
+
在 manifest 的 `settings` 中声明字段。每个字段使用一个稳定 key。
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"caption": {
|
|
547
|
+
"type": "string", "label": "显示文字", "default": "文件统计"
|
|
548
|
+
},
|
|
549
|
+
"limit": {
|
|
550
|
+
"type": "number", "label": "结果数量", "default": 20, "min": 1, "max": 100
|
|
551
|
+
},
|
|
552
|
+
"enabled": {
|
|
553
|
+
"type": "boolean", "label": "显示结果", "default": true
|
|
554
|
+
},
|
|
555
|
+
"mode": {
|
|
556
|
+
"type": "select", "label": "显示模式", "default": "brief",
|
|
557
|
+
"options": [
|
|
558
|
+
{ "value": "brief", "label": "简洁" },
|
|
559
|
+
{ "value": "full", "label": "完整" }
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
这段是 `settings` 字段的值,不是完整 manifest。字段还可以声明 `description`。string 字段支持 `multiline` 和 `secret`。
|
|
566
|
+
|
|
567
|
+
`secret: true` 只遮挡表单输入。配置存储没有因此加密。需要安全凭据存储时,在 main 中集成适当的凭据管理方案。
|
|
568
|
+
|
|
569
|
+
### 7.2 读取和修改配置
|
|
570
|
+
|
|
571
|
+
| 方法 | 使用位置 | 行为 |
|
|
572
|
+
| --- | --- | --- |
|
|
573
|
+
| `ctx.getConfig()` | main 或 renderer 回调 | 返回当前配置 |
|
|
574
|
+
| `ctx.useConfig()` | renderer React 组件 | 返回配置,并在变化时重新渲染 |
|
|
575
|
+
| `await ctx.setConfig(patch)` | main 或 renderer | 校验、浅合并并持久化,返回完整配置 |
|
|
576
|
+
| `ctx.onConfigChanged(listener)` | main 或 renderer | 订阅本插件配置;返回清理函数 |
|
|
577
|
+
| 组件 `props.config` | 已注册组件 | 宿主提供本插件的配置 |
|
|
578
|
+
|
|
579
|
+
main 和 renderer 读取同一份插件配置。表单保存和 `setConfig` 都会通知两侧。不同插件按插件 ID 分开保存配置。
|
|
580
|
+
|
|
581
|
+
下面的代码放在需要修改配置的异步回调中:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
await ctx.setConfig({ caption: '新的状态文字' })
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
只传要修改的字段。未修改字段保留原值。自动表单之外的交互,可以放在 `settings` slot 中。
|
|
588
|
+
|
|
589
|
+
### 7.3 保存插件数据
|
|
590
|
+
|
|
591
|
+
main 的 `ctx.pluginPath` 是安装副本目录。`ctx.dataPath` 是插件专属数据目录。宿主在激活前创建数据目录。
|
|
592
|
+
|
|
593
|
+
缓存、索引和用户数据写入 `dataPath`。不要写入 `pluginPath`;重新安装会替换插件文件。
|
|
594
|
+
|
|
595
|
+
下面是完整的 main CJS 入口示例。将 manifest 的 `main` 指向 `main.cjs`:
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
const fs = require('node:fs/promises')
|
|
599
|
+
const path = require('node:path')
|
|
600
|
+
|
|
601
|
+
exports.activate = function (ctx) {
|
|
602
|
+
const target = path.join(ctx.dataPath, 'note.txt')
|
|
603
|
+
ctx.registerCommand('saveNote', async (args) => {
|
|
604
|
+
if (typeof args !== 'string') throw new Error('笔记内容必须是文本')
|
|
605
|
+
try {
|
|
606
|
+
await fs.writeFile(target, args, 'utf8')
|
|
607
|
+
} catch {
|
|
608
|
+
throw new Error('保存插件笔记失败,请检查数据目录是否可写')
|
|
609
|
+
}
|
|
610
|
+
return { saved: true }
|
|
611
|
+
})
|
|
612
|
+
ctx.registerCommand('readNote', async () => {
|
|
613
|
+
try {
|
|
614
|
+
return await fs.readFile(target, 'utf8')
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (error.code === 'ENOENT') return ''
|
|
617
|
+
throw new Error('读取插件笔记失败')
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
renderer 使用 `ctx.executeCommand('saveNote', text)` 和 `ctx.executeCommand('readNote')`。ADE 卸载插件时删除安装副本,保留插件配置和数据目录。重新安装同 ID 插件可以继续使用它们。不要把插件目录当作数据备份。
|
|
624
|
+
|
|
625
|
+
## 8. 服务、事件与跨进程调用
|
|
626
|
+
|
|
627
|
+
### 8.1 同进程服务
|
|
628
|
+
|
|
629
|
+
服务适合暴露可复用操作。名称使用插件前缀,例如 `file-statistics.reader`。
|
|
630
|
+
|
|
631
|
+
main `provideService` 接收一个对象。renderer `provideService` 可以接收任意同进程值。两侧的 `getService` 只查询所在进程。
|
|
632
|
+
|
|
633
|
+
main 中已有以下内置服务:
|
|
634
|
+
|
|
635
|
+
| 名称 | 常用方法 | 返回形式 |
|
|
636
|
+
| --- | --- | --- |
|
|
637
|
+
| `files` | `readFile(path)`、`readDirTree(dir)`、`statFile(path)`、`writeFile(path, text)` | 这些方法同步返回 |
|
|
638
|
+
| `projects` | `listProjects()`、`addProject(path)`、`removeProject(path)` | 同步返回 |
|
|
639
|
+
| `worktrees` | `listWorktrees(project)`、`createWorktree(project, name)`、`removeWorktree(project, path)` | Promise |
|
|
640
|
+
| `settings` | `getSettings()`、`getSettingsFile()`、`updateSettings(patch)`、`onSettingsChanged(listener)`、`getLegacySettingsFile()` | 设置对象、完整设置文件、原始迁移备份或清理函数 |
|
|
641
|
+
| `app` | `getVersion()`、`getPath('userData' 或 'temp')`、`openExternal(url)` | 版本、应用目录字符串或 Promise |
|
|
642
|
+
| `chat` | `listAdapters()`、`listSessions()`、`listHistory()`、`sendMessage(id, prompt)`、`stop(id)` | 列表同步返回;发送和停止返回 Promise |
|
|
643
|
+
|
|
644
|
+
main `getService` 返回 `unknown`。先声明所需接口,再检查服务是否存在。下面的片段放在 main `activate` 内:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const files = ctx.getService('files') as {
|
|
648
|
+
readFile(path: string): string
|
|
649
|
+
} | undefined
|
|
650
|
+
if (!files) throw new Error('当前 ADE 未提供文件服务')
|
|
651
|
+
ctx.registerCommand('readSaved', (_args, context) => {
|
|
652
|
+
if (!context.filePath) throw new Error('请先打开文件')
|
|
653
|
+
return files.readFile(context.filePath)
|
|
654
|
+
})
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
main 内置服务与 renderer preload API 的方法名不同。例如 main 用 `files.readFile(path)`,renderer 用 `await ctx.api.files.read(path)`。
|
|
658
|
+
|
|
659
|
+
`settings.getLegacySettingsFile()` 返回规范化前保存的原始设置副本。核心移出的字段可供插件以后安装时迁移。该备份只用于一次性迁移,不作为新的插件配置存储。插件应校验数据,并用自己的迁移标记避免覆盖用户后续配置。
|
|
660
|
+
|
|
661
|
+
`settings.getSettingsFile()` 同步返回当前 `AdeSettingsFile`,包含 `settings` 和 `state`。插件迁移旧区域时,可读取 `state.pluginAreasVisible`,先检查用户是否已设置对应区域,再决定是否写入迁移值。
|
|
662
|
+
|
|
663
|
+
main `files` 服务没有 renderer 的 `watch`、`trash`、`reveal` 或剪贴板方法。main 需要其他本地能力时,使用 Node.js。
|
|
664
|
+
|
|
665
|
+
### 8.2 renderer 调用 main 服务
|
|
666
|
+
|
|
667
|
+
以下是完整 main CJS 入口 `main.cjs`:
|
|
668
|
+
|
|
669
|
+
```js
|
|
670
|
+
const fs = require('node:fs/promises')
|
|
671
|
+
|
|
672
|
+
exports.activate = function (ctx) {
|
|
673
|
+
ctx.provideService('file-statistics.reader', {
|
|
674
|
+
async read(filePath) {
|
|
675
|
+
if (typeof filePath !== 'string') throw new Error('文件路径必须是文本')
|
|
676
|
+
try {
|
|
677
|
+
return await fs.readFile(filePath, 'utf8')
|
|
678
|
+
} catch {
|
|
679
|
+
throw new Error('读取文件失败,请确认文件存在且可读取')
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
将下面的片段放在 renderer 的异步回调中。该插件 manifest 的 `id` 为 `file-statistics`:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
const context = ctx.getContext()
|
|
690
|
+
if (!context.filePath) throw new Error('请先打开文件')
|
|
691
|
+
const text = await ctx.api.plugins.invokeService(
|
|
692
|
+
ctx.pluginId, 'file-statistics.reader', 'read', [context.filePath]
|
|
693
|
+
)
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
`invokeService` 的参数依次为调用方插件 ID、主进程服务名、方法名、参数数组。它返回 Promise。目标服务可以来自其他主进程插件,也可以是内置服务。
|
|
697
|
+
|
|
698
|
+
可调用方法必须是服务对象自身的函数属性。类原型方法不能直接通过这个桥调用。使用对象方法,或将实例方法显式绑定为对象属性。
|
|
699
|
+
|
|
700
|
+
命令适合有明确操作名的请求。服务适合提供一组相关操作。两种方式都遵守 IPC 序列化要求。
|
|
701
|
+
|
|
702
|
+
### 8.3 事件
|
|
703
|
+
|
|
704
|
+
| 方法 | 总线 |
|
|
705
|
+
| --- | --- |
|
|
706
|
+
| renderer `ctx.on(name, listener)` / `ctx.emit(name, payload)` | 渲染进程 |
|
|
707
|
+
| main `ctx.events.on(name, listener)` / `ctx.events.emit(name, payload)` | 主进程 |
|
|
708
|
+
|
|
709
|
+
同名事件不会自动跨进程传播。跨进程使用命令或 preload API。
|
|
710
|
+
|
|
711
|
+
renderer 可以订阅内置 `context:changed`。下面的代码放在 renderer `activate` 中:
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
ctx.on('context:changed', () => {
|
|
715
|
+
const context = ctx.getContext()
|
|
716
|
+
console.info('活动文件:', context.filePath)
|
|
717
|
+
})
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
自定义事件名称加插件前缀。事件用于即时通知,不保存历史,也不代替配置或持久数据。不要依赖某个插件一定先于另一个插件激活。服务可能暂时不存在;必要时提示用户启用依赖插件。
|
|
721
|
+
|
|
722
|
+
## 9. 生命周期与清理
|
|
723
|
+
|
|
724
|
+
### 9.1 入口形式
|
|
725
|
+
|
|
726
|
+
新插件使用命名导出的 `activate(ctx)`,或默认插件对象的 `activate(ctx)`。`activate` 可以返回 Promise,也可以返回一个清理函数。入口可以另外导出 `deactivate()`。
|
|
727
|
+
|
|
728
|
+
轻量 CJS 使用 `exports.activate = ...`。旧 `register(host)` 仍受支持。不要把 CJS 入口写成 `module.exports = function (...) { ... }`;该形式没有宿主识别的入口方法。
|
|
729
|
+
|
|
730
|
+
启用后,宿主为插件创建上下文。停用、卸载、重新加载或激活失败时,宿主撤销注册。重新启用会创建新实例。
|
|
731
|
+
|
|
732
|
+
### 9.2 哪些资源自动清理
|
|
733
|
+
|
|
734
|
+
区域、slot、命令、服务、事件监听、配置监听和宿主添加的样式由宿主管理。它们通常返回 disposer,便于提前撤销。不要再把这些 disposer 重复放入 `subscriptions`。
|
|
735
|
+
|
|
736
|
+
main `registerAdapter` 返回 `void`。adapter 归属当前插件。停用插件会撤销 adapter,并停止相关任务;只读历史保留。
|
|
737
|
+
|
|
738
|
+
插件自己创建的定时器、进程、文件监听和连接需要自行清理。以下片段放在 main 或 renderer 的 `activate` 中:
|
|
739
|
+
|
|
740
|
+
```ts
|
|
741
|
+
const timer = setInterval(() => {
|
|
742
|
+
// 更新插件自己的状态。
|
|
743
|
+
}, 1000)
|
|
744
|
+
ctx.effect(() => clearInterval(timer))
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
`ctx.effect` 接收**清理函数**。它不会执行 setup,也不是 React 的 `useEffect`。也可以把清理函数放入 `ctx.subscriptions`,二选一即可。
|
|
748
|
+
|
|
749
|
+
组件资源使用 React cleanup。以下是完整 renderer 组件,可作为已注册区域的组件:
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
import { useEffect, useState } from 'react'
|
|
753
|
+
import type { PluginComponentProps } from '@ai_mind/ade-plugin-sdk'
|
|
754
|
+
|
|
755
|
+
export function FileRevision({ context, api }: PluginComponentProps) {
|
|
756
|
+
const [revision, setRevision] = useState(0)
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (!context.filePath) return
|
|
759
|
+
return api.files.watch(context.filePath, () => setRevision(value => value + 1))
|
|
760
|
+
}, [api, context.filePath])
|
|
761
|
+
return <span>文件变化次数:{revision}</span>
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
组件卸载或路径变化时,文件订阅解除。网络请求可以用 `AbortController`,并在 cleanup 中调用 `abort()`。
|
|
766
|
+
|
|
767
|
+
### 9.3 超时与旧上下文
|
|
768
|
+
|
|
769
|
+
main 的入口加载和激活分别有默认 10 秒等待期限。renderer 激活有等待期限。异步期限不能中断同步死循环。
|
|
770
|
+
|
|
771
|
+
停用时,main 先关闭上下文并撤销注册,再调用 `deactivate`,最后逆序执行清理函数。main 清理共用 10 秒总期限。超时后宿主仍启动剩余清理项,并继续退出。
|
|
772
|
+
|
|
773
|
+
不要在清理函数中重新注册资源。已停用实例的异步任务应停止访问旧上下文。给进程和请求设置自己的取消机制。
|
|
774
|
+
|
|
775
|
+
重启用会重新加载入口,但不保证所有 ESM 依赖都冷加载。更新开发代码时,使用「构建 → 重新安装」流程。
|
|
776
|
+
|
|
777
|
+
## 10. 接入聊天 adapter
|
|
778
|
+
|
|
779
|
+
聊天 adapter 在 main 中运行。它负责创建会话、接收输入、输出数据和释放资源。SDK 导出 `ChatAdapter`、`SessionContext`、`ChatSession` 和结构化对话类型。
|
|
780
|
+
|
|
781
|
+
### 10.1 静态终端 adapter
|
|
782
|
+
|
|
783
|
+
如果接入一个已安装的交互式 CLI,可以在 manifest 增加以下 `adapters` 字段:
|
|
784
|
+
|
|
785
|
+
```json
|
|
786
|
+
{
|
|
787
|
+
"adapters": [
|
|
788
|
+
{
|
|
789
|
+
"id": "my-plugin.agent",
|
|
790
|
+
"label": "我的 Agent",
|
|
791
|
+
"description": "运行本机已安装的 agent CLI",
|
|
792
|
+
"kind": "terminal",
|
|
793
|
+
"command": "my-agent-cli"
|
|
794
|
+
}
|
|
795
|
+
]
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
这段只展示需要合并的 manifest 字段。将 `my-agent-cli` 替换为真实可执行文件名或路径。安装 CLI 后,再启用插件并创建会话。
|
|
800
|
+
|
|
801
|
+
`command` 是可执行程序,不是完整 shell 命令。这里没有 `argv` 字段。需要自定义参数、环境或协议时,使用动态 adapter。
|
|
802
|
+
|
|
803
|
+
静态 manifest 只实现 terminal adapter。静态 `kind: "sdk"` 不会建立 SDK 会话。
|
|
804
|
+
|
|
805
|
+
### 10.2 动态 adapter 的最小完整入口
|
|
806
|
+
|
|
807
|
+
下面是 main CJS 入口 `main.cjs`。将 manifest 的 `main` 指向它。这个示例用于验证注册和终端输入输出;它不调用模型。
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
exports.activate = function (host) {
|
|
811
|
+
host.registerAdapter({
|
|
812
|
+
id: `${host.pluginId}.echo`,
|
|
813
|
+
label: '回声测试',
|
|
814
|
+
kind: 'terminal',
|
|
815
|
+
createSession(session) {
|
|
816
|
+
let disposed = false
|
|
817
|
+
session.onData('回声会话已启动\r\n')
|
|
818
|
+
return {
|
|
819
|
+
id: session.id,
|
|
820
|
+
write(data) {
|
|
821
|
+
if (!disposed) session.onData(data)
|
|
822
|
+
},
|
|
823
|
+
resize(_cols, _rows) {},
|
|
824
|
+
dispose() {
|
|
825
|
+
if (disposed) return
|
|
826
|
+
disposed = true
|
|
827
|
+
session.onExit(0)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
1. 安装并启用插件。
|
|
836
|
+
2. 在新建对话时选择「回声测试」。
|
|
837
|
+
3. 在终端视图输入文本。
|
|
838
|
+
4. 停止会话。
|
|
839
|
+
|
|
840
|
+
预期结果:输入出现在终端中,停止后会话退出。实际 CLI adapter 应在 `write` 转发输入,在 `resize` 调整终端,在 `dispose` 结束进程。
|
|
841
|
+
|
|
842
|
+
adapter ID 在 ADE 中全局唯一。建议使用 `<plugin-id>.<adapter-name>`。`shell`、`agent:*`、`daemon:*` 和重复 ID 被宿主保留或拒绝。
|
|
843
|
+
|
|
844
|
+
### 10.3 会话接口
|
|
845
|
+
|
|
846
|
+
| 接口成员 | 职责 |
|
|
847
|
+
| --- | --- |
|
|
848
|
+
| `SessionContext.id` | ADE 会话 ID |
|
|
849
|
+
| `cwd` / `worktree` | 会话工作目录与所属 worktree |
|
|
850
|
+
| `projectPath` | 可选项目路径 |
|
|
851
|
+
| `sessionId` / `resume` | provider 会话标识与续接状态 |
|
|
852
|
+
| `config` | 用户选择的会话参数 |
|
|
853
|
+
| `onData(text)` | 输出终端数据 |
|
|
854
|
+
| `onError(error)` | 报告运行错误;消息使用中文 |
|
|
855
|
+
| `onExit(code)` | 报告退出;退出时完成资源释放 |
|
|
856
|
+
| `ChatSession.write(data)` | 接收终端输入 |
|
|
857
|
+
| `ChatSession.resize(cols, rows)` | 接收终端尺寸变化 |
|
|
858
|
+
| `ChatSession.dispose()` | 停止会话并释放资源;可以异步 |
|
|
859
|
+
| `ChatSession.closed` | 可选 Promise;底层会话真正退出后返回退出码 |
|
|
860
|
+
|
|
861
|
+
### 10.4 模型 SDK 的兼容气泡接口
|
|
862
|
+
|
|
863
|
+
动态 `kind: "sdk"` 使用 SDK 会话界面。用户输入经 `ChatSession.write` 传入。通过 `onData` 输出换行分隔的 JSON 消息,每行格式为 `{ id?, role?, content }`。
|
|
864
|
+
|
|
865
|
+
`content` 必须是字符串。`role` 使用 `assistant`、`user` 或 `system`。每条完整 JSON 后追加 `\n`。ADE 将每条消息作为一个已完成气泡记录;这不是 token 增量事件协议。
|
|
866
|
+
|
|
867
|
+
以下是完整的 main CJS 测试入口。它验证 SDK 气泡通道,不调用模型:
|
|
868
|
+
|
|
869
|
+
```js
|
|
870
|
+
exports.activate = function (host) {
|
|
871
|
+
host.registerAdapter({
|
|
872
|
+
id: `${host.pluginId}.sdk-echo`,
|
|
873
|
+
label: 'SDK 气泡测试',
|
|
874
|
+
kind: 'sdk',
|
|
875
|
+
createSession(session) {
|
|
876
|
+
let disposed = false
|
|
877
|
+
return {
|
|
878
|
+
id: session.id,
|
|
879
|
+
write(data) {
|
|
880
|
+
if (disposed) return
|
|
881
|
+
const content = `收到:${data.trimEnd()}`
|
|
882
|
+
session.onData(JSON.stringify({ role: 'assistant', content }) + '\n')
|
|
883
|
+
},
|
|
884
|
+
resize(_cols, _rows) {},
|
|
885
|
+
dispose() {
|
|
886
|
+
if (disposed) return
|
|
887
|
+
disposed = true
|
|
888
|
+
session.onExit(0)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
}
|
|
894
|
+
```
|
|
55
895
|
|
|
56
|
-
|
|
896
|
+
安装并启用后,新建「SDK 气泡测试」会话。发送「你好」。预期结果:显示用户消息和「收到:你好」回复。实际模型接入时,在 `write` 中调用 provider,再按这个协议输出完整回复。释放或取消请求的逻辑由 adapter 实现。
|
|
57
897
|
|
|
58
|
-
|
|
898
|
+
这个路径不需要 `chat.startTurn`。当前 SDK 界面使用 `write` 输入;不要将 `kind: "sdk"` 与 `chat.startTurn` 组合成原生结构化聊天路径。
|
|
59
899
|
|
|
60
|
-
|
|
900
|
+
### 10.5 原生结构化聊天
|
|
61
901
|
|
|
62
|
-
|
|
902
|
+
需要流式事件、工具、用量、审批和聊天/终端切换时,使用 `kind: "terminal"` 并实现 `chat.startTurn(ctx, options)`。每轮由 ADE 调用一次。通过 `StructuredSessionContext.onEvent` 发送 `StructuredEvent`。轮次结束时调用 `onExit`。仍需提供 `createSession` 和返回的会话接口。
|
|
903
|
+
|
|
904
|
+
| 可选扩展 | 用途 |
|
|
905
|
+
| --- | --- |
|
|
906
|
+
| `chat.preassignedSessionId` / `prepareSession` | 分配 provider 会话标识 |
|
|
907
|
+
| `chat.readHistory` / `discoverSessions` | 读取和发现 provider 原生历史 |
|
|
908
|
+
| `chat.steer` | 对正在执行的轮次追加指令 |
|
|
909
|
+
| `chat.respond` | 回复审批或用户输入请求 |
|
|
910
|
+
| `chat.detach` / `disposeSession` | 释放 provider 连接或会话资源 |
|
|
911
|
+
| `chat.attachmentKinds` | 声明支持的附件类型 |
|
|
912
|
+
| `loadOptions` / `listModelsArgs` / `parseModels` | 提供模型和选项 |
|
|
913
|
+
| `excludedOptions` | 隐藏被协议自身管理的 CLI 参数 |
|
|
914
|
+
|
|
915
|
+
不同 provider 的续接、取消、审批和附件协议不同。按 SDK 的 `ChatTurnOptions`、`StructuredEvent` 和 `ChatApprovalResponse` 类型实现这些操作。不要把 provider 原始输出直接当成 ADE 结构化事件。
|
|
916
|
+
|
|
917
|
+
## 11. SDK 构建器与运行依赖
|
|
918
|
+
|
|
919
|
+
### 11.1 构建命令
|
|
920
|
+
|
|
921
|
+
| 命令 | 行为 |
|
|
922
|
+
| --- | --- |
|
|
923
|
+
| `npx ade-plugin-build .` | 构建默认 `src/renderer.tsx` 到 manifest 的 `renderer` |
|
|
924
|
+
| `npx ade-plugin-build . --entry src/panel.tsx` | 改用指定 renderer 源码 |
|
|
925
|
+
| `npx ade-plugin-build . --main-entry src/main.ts` | 同时构建 main 到 manifest 的 `main` |
|
|
926
|
+
|
|
927
|
+
builder 转译和打包源码。它不执行 TypeScript 类型检查。需要类型检查时,在开发目录安装 TypeScript:
|
|
63
928
|
|
|
64
929
|
```sh
|
|
65
|
-
npm install --save-dev
|
|
930
|
+
npm install --save-dev typescript
|
|
66
931
|
```
|
|
67
932
|
|
|
68
|
-
|
|
933
|
+
保存 `tsconfig.json`:
|
|
934
|
+
|
|
935
|
+
```json
|
|
936
|
+
{
|
|
937
|
+
"compilerOptions": {
|
|
938
|
+
"target": "ES2022",
|
|
939
|
+
"module": "ESNext",
|
|
940
|
+
"moduleResolution": "bundler",
|
|
941
|
+
"jsx": "react-jsx",
|
|
942
|
+
"strict": true,
|
|
943
|
+
"noEmit": true,
|
|
944
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
945
|
+
},
|
|
946
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
执行 `npx tsc --noEmit`。通过后再构建和分发。main 使用 Node.js 类型时,安装 `@types/node@24`。
|
|
951
|
+
|
|
952
|
+
当前 builder 要求有效 renderer 入口和 `rendererFormat: "register"`。`--main-entry` 是附加构建参数,不是 main-only 模式。main-only 插件可以直接交付第 7、8 或 10 节的 CJS;TypeScript main-only 需要自行配置构建。
|
|
953
|
+
|
|
954
|
+
### 11.2 React 与 renderer 格式
|
|
955
|
+
|
|
956
|
+
`register` 格式是 SDK builder 的产物。ADE 用 script 注册模块工厂。构建保留宿主的 React、ReactDOM 和 SDK 模块,不把第二份 React 打入插件。
|
|
957
|
+
|
|
958
|
+
其他浏览器依赖会被打入 bundle。不要调用 `createRoot` 接管 ADE 容器。使用宿主注册的组件和 slot。
|
|
959
|
+
|
|
960
|
+
`module` 格式用于预构建浏览器 ESM。它可以直接导出 `activate`。浏览器不能直接解析 npm 裸模块名。使用 `ctx.React`,或预先打包其他依赖。第 13 节提供完整示例。
|
|
961
|
+
|
|
962
|
+
宿主保留 `@ade/plugin-sdk` 和 `@ai_mind/plugin-sdk` 两个旧模块别名。新项目使用正式名称 `@ai_mind/ade-plugin-sdk`。
|
|
963
|
+
|
|
964
|
+
### 11.3 main 依赖与资源
|
|
965
|
+
|
|
966
|
+
main 构建生成 CJS,目标为 Node.js 24。Node.js 内置模块保留,`electron` 保留为外部模块。一般 JavaScript 依赖会被打包。
|
|
967
|
+
|
|
968
|
+
动态加载、原生模块和额外数据文件可能需要单独交付。原生模块必须匹配 ADE 使用的 Electron ABI、平台和 CPU 架构。用户安装时不会运行原生编译脚本。
|
|
969
|
+
|
|
970
|
+
CSS 导入会生成样式文件。支持的图片和字体导入写入产物的 `assets` 目录。生成的资源使用 `ade-plugin://` 地址。分发时保留这些资源;不要只复制入口 JS。
|
|
971
|
+
|
|
972
|
+
### 11.4 使用导出的 SDK 包
|
|
973
|
+
|
|
974
|
+
在 ADE 开发指南顶部点击「导出 SDK」。将包保存到本地。然后在插件开发目录执行:
|
|
975
|
+
|
|
976
|
+
```sh
|
|
977
|
+
npm install --save-dev '/绝对路径/导出的-sdk.tgz' @types/react@19 @types/react-dom@19
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
替换为真实文件路径。导出的 `.tgz` 包含 SDK 本身。首次安装构建器依赖和 React 类型时,仍需要网络或已有 npm 缓存。
|
|
981
|
+
|
|
982
|
+
## 12. 分发、安装与升级
|
|
983
|
+
|
|
984
|
+
### 12.1 四种安装来源
|
|
985
|
+
|
|
986
|
+
| 来源 | 用户操作 | 作者交付内容 |
|
|
987
|
+
| --- | --- | --- |
|
|
988
|
+
| 本地目录 | 选择包含 `plugin.json` 的目录 | 已构建的插件目录 |
|
|
989
|
+
| ZIP | 选择插件 ZIP 文件 | manifest、JavaScript、CSS、资源和运行依赖 |
|
|
990
|
+
| npm | 输入包名,可指定版本 | 带运行产物的 npm 包 |
|
|
991
|
+
| Git | 输入插件 Git 仓库来源 | 仓库中已提交的运行产物 |
|
|
992
|
+
|
|
993
|
+
npm/Git 安装生产依赖时跳过 scripts 和 devDependencies。ADE 不执行 `prepare`、`postinstall` 或 TypeScript 构建。目录/ZIP 安装不下载依赖。
|
|
994
|
+
|
|
995
|
+
安装 npm 来源需要本机有 npm。Git 来源需要 Git;有生产依赖时也需要 npm。网络来源需要能够访问对应仓库和 registry。
|
|
996
|
+
|
|
997
|
+
main 使用未打包依赖时,npm/Git 包需要在 `dependencies` 中声明它们。目录/ZIP 包需要附带可运行的依赖,或预先打包。renderer 的浏览器依赖应在发布前打入 bundle。
|
|
998
|
+
|
|
999
|
+
### 12.2 打包 ZIP
|
|
1000
|
+
|
|
1001
|
+
1. 构建插件。
|
|
1002
|
+
2. 检查 manifest 中的每个入口和样式文件。
|
|
1003
|
+
3. 将 `plugin.json`、`lib` 和其他运行文件放在交付目录。
|
|
1004
|
+
4. 将交付目录压缩为 ZIP。
|
|
1005
|
+
5. 在一个新用户配置中安装该 ZIP。
|
|
1006
|
+
|
|
1007
|
+
`plugin.json` 放在 ZIP 根目录,或唯一顶级插件文件夹中。宿主不会继续搜索更深目录。ZIP 不接受符号链接,最多包含 20,000 个条目,解压内容最多 512 MiB。不要把 SDK 的 `.tgz` 当作你的插件 ZIP。
|
|
1008
|
+
|
|
1009
|
+
### 12.3 发布自己的 npm 插件包
|
|
1010
|
+
|
|
1011
|
+
SDK npm 包与插件 npm 包是两个包。为插件选择自己的包名。以下是文件统计插件的 `package.json` 示例:
|
|
1012
|
+
|
|
1013
|
+
```json
|
|
1014
|
+
{
|
|
1015
|
+
"name": "ade-file-statistics-example",
|
|
1016
|
+
"version": "1.0.0",
|
|
1017
|
+
"description": "ADE 文件统计插件",
|
|
1018
|
+
"files": ["plugin.json", "lib", "README.md"],
|
|
1019
|
+
"scripts": {
|
|
1020
|
+
"build": "ade-plugin-build . --main-entry src/main.ts"
|
|
1021
|
+
},
|
|
1022
|
+
"devDependencies": {
|
|
1023
|
+
"@ai_mind/ade-plugin-sdk": "^0.1.1",
|
|
1024
|
+
"@types/node": "^24.0.0",
|
|
1025
|
+
"@types/react": "^19.0.0",
|
|
1026
|
+
"@types/react-dom": "^19.0.0"
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
包名只是示例。发布时使用你有权限的 npm 用户或组织名。将所有额外运行资源加入 `files`。
|
|
1032
|
+
|
|
1033
|
+
在已完成 npm 登录的开发机执行:
|
|
1034
|
+
|
|
1035
|
+
```sh
|
|
1036
|
+
npm run build
|
|
1037
|
+
npm pack --dry-run
|
|
1038
|
+
npm pack
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
先检查打包清单。确认入口和资源都在包中。验证生成的包后,再执行:
|
|
1042
|
+
|
|
1043
|
+
```sh
|
|
1044
|
+
npm publish --access public
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
用户在 ADE 的 npm 安装输入框中填写你的插件包名。例如 `ade-file-statistics-example@1.0.0`。用户不需要安装 SDK。
|
|
1048
|
+
|
|
1049
|
+
### 12.4 Git 交付与更新
|
|
1050
|
+
|
|
1051
|
+
Git 仓库必须包含 `plugin.json` 和已构建产物。使用 HTTPS 或 SSH 仓库地址。ADE 克隆默认分支;当前没有单独选择分支或 commit 的安装参数。不要只提交 `src`,然后依赖安装时构建。
|
|
1052
|
+
|
|
1053
|
+
修改代码后,更新插件自身 `version` 并重新构建。用相同 `id` 安装新目录、ZIP、npm 包或 Git 来源。ADE 替换副本并保留插件配置。
|
|
1054
|
+
|
|
1055
|
+
插件没有自动更新流程。作者应在插件 README 写清安装来源、最低 ADE 版本、外部 CLI 要求和升级方式。
|
|
1056
|
+
|
|
1057
|
+
### 12.5 停用与卸载
|
|
1058
|
+
|
|
1059
|
+
停用会撤销注册并释放资源,安装副本和配置保留。再次启用会创建新实例。
|
|
1060
|
+
|
|
1061
|
+
卸载会删除用户插件的安装副本,保留配置和 `dataPath`。内置插件可以停用,不能由用户插件覆盖或卸载。
|
|
1062
|
+
|
|
1063
|
+
## 13. 无需 SDK 的快速入门
|
|
1064
|
+
|
|
1065
|
+
这个例子不需要下载依赖、安装 SDK 或编译。它增加一个区域、一个状态栏组件和一个文件工具按钮。
|
|
1066
|
+
|
|
1067
|
+
### 步骤 1:创建目录和 manifest
|
|
1068
|
+
|
|
1069
|
+
创建 `my-first-plugin` 目录。保存 `plugin.json`:
|
|
1070
|
+
|
|
1071
|
+
```json
|
|
1072
|
+
{
|
|
1073
|
+
"id": "my-first-plugin",
|
|
1074
|
+
"name": "我的第一个插件",
|
|
1075
|
+
"version": "1.0.0",
|
|
1076
|
+
"apiVersion": 1,
|
|
1077
|
+
"adeMajor": 0,
|
|
1078
|
+
"renderer": "renderer.js",
|
|
1079
|
+
"rendererFormat": "module",
|
|
1080
|
+
"settings": {
|
|
1081
|
+
"caption": { "type": "string", "label": "显示文字", "default": "你好,ADE" }
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### 步骤 2:保存 renderer.js
|
|
1087
|
+
|
|
1088
|
+
```js
|
|
1089
|
+
export function activate(ctx) {
|
|
1090
|
+
const React = ctx.React
|
|
1091
|
+
|
|
1092
|
+
function Greeting({ context, config }) {
|
|
1093
|
+
const [count, setCount] = React.useState(0)
|
|
1094
|
+
return React.createElement('section', {
|
|
1095
|
+
style: { padding: 16, fontFamily: 'var(--ade-font-sans)' }
|
|
1096
|
+
},
|
|
1097
|
+
React.createElement('h2', null, config.caption),
|
|
1098
|
+
React.createElement('p', null, context.worktreePath || '尚未选择 worktree'),
|
|
1099
|
+
React.createElement('button', {
|
|
1100
|
+
type: 'button', onClick: () => setCount(count + 1)
|
|
1101
|
+
}, `点击计数:${count}`)
|
|
1102
|
+
)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function Status({ config }) {
|
|
1106
|
+
return React.createElement('span', null, config.caption)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function CopyFile({ context, api }) {
|
|
1110
|
+
const [label, setLabel] = React.useState('复制文件路径')
|
|
1111
|
+
async function copy() {
|
|
1112
|
+
try {
|
|
1113
|
+
await api.app.copyText(context.filePath)
|
|
1114
|
+
setLabel('已复制')
|
|
1115
|
+
} catch {
|
|
1116
|
+
setLabel('复制失败,请重试')
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return React.createElement('button', {
|
|
1120
|
+
type: 'button', onClick: copy, disabled: !context.filePath
|
|
1121
|
+
}, label)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
ctx.registerView({
|
|
1125
|
+
id: 'greeting', title: '我的插件', component: Greeting,
|
|
1126
|
+
defaultSize: 22, minSize: 10
|
|
1127
|
+
})
|
|
1128
|
+
ctx.registerSlot('statusBar.right', { id: 'status', component: Status })
|
|
1129
|
+
ctx.registerSlot('editor.toolbar', {
|
|
1130
|
+
id: 'copy-file', component: CopyFile,
|
|
1131
|
+
when: context => Boolean(context.filePath)
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### 步骤 3:安装并验证
|
|
1137
|
+
|
|
1138
|
+
1. 在 ADE 的插件设置中选择「本地目录」。
|
|
1139
|
+
2. 安装 `my-first-plugin`。
|
|
1140
|
+
3. 启用插件。
|
|
1141
|
+
4. 点击区域中的计数按钮。
|
|
1142
|
+
5. 打开一个文件。
|
|
1143
|
+
6. 点击文件工具栏的「复制文件路径」。
|
|
1144
|
+
7. 修改插件设置中的「显示文字」并保存。
|
|
1145
|
+
|
|
1146
|
+
预期结果:计数增加,文件路径写入剪贴板,区域和状态栏显示新的配置文字。
|
|
1147
|
+
|
|
1148
|
+
## 14. 排错与交付验证
|
|
1149
|
+
|
|
1150
|
+
### 14.1 常见问题
|
|
1151
|
+
|
|
1152
|
+
| 现象 | 检查与处理 |
|
|
1153
|
+
| --- | --- |
|
|
1154
|
+
| 插件列表显示入口不存在 | 确认已构建;确认交付包包含 manifest 指向的 JS |
|
|
1155
|
+
| API 或 ADE 版本不匹配 | 检查 `apiVersion: 1`、`adeMajor` 与最低 ADE 版本 |
|
|
1156
|
+
| 修改代码后没有变化 | 重新构建,再重新安装开发目录;停用再启用不会更新副本 |
|
|
1157
|
+
| 区域没有出现 | 检查插件已启用;检查「视图」菜单中的区域开关 |
|
|
1158
|
+
| slot 没有出现 | 检查名称、`when` 条件和当前位置是否渲染;settings 只显示所选插件 |
|
|
1159
|
+
| hook 错误或 Invalid hook call | 使用 SDK builder,共享宿主 React;移除自行打包的 React |
|
|
1160
|
+
| renderer 无法导入 Node.js | 把本地操作移到 main,通过命令调用 |
|
|
1161
|
+
| module 无法解析 npm 包名 | 使用 `ctx.React`,或改为 builder 的 `register` 产物 |
|
|
1162
|
+
| 命令找不到 | 检查注册入口、插件启用状态、局部 ID 和跨插件命名空间 |
|
|
1163
|
+
| main 服务在 renderer 中取不到 | 两侧注册表独立;使用 `api.plugins.invokeService` |
|
|
1164
|
+
| 服务方法不可调用 | 方法必须是对象自身的函数属性;不要直接暴露类原型 |
|
|
1165
|
+
| 事件没有传到另一进程 | 事件总线不跨进程;使用命令或 preload API |
|
|
1166
|
+
| 配置更新后结果仍旧 | 用 props 或 `useConfig` 读取;避免把初始配置永久缓存 |
|
|
1167
|
+
| 字符统计与编辑器内容不同 | 示例读取磁盘;先保存文件,再统计 |
|
|
1168
|
+
| 安装后依赖找不到 | 检查已打包依赖或运行 dependencies;安装不会执行构建脚本 |
|
|
1169
|
+
| 原生模块加载失败 | 检查 Electron ABI、平台和架构;预先构建并交付 |
|
|
1170
|
+
| 停用后仍有任务输出 | 清理进程、定时器、监听和连接;取消未完成请求 |
|
|
1171
|
+
| SDK 气泡没有回复 | 检查 `kind: "sdk"`、`write` 和 `onData` 的换行 JSON 消息;不要混用原生 chat 路径 |
|
|
1172
|
+
| terminal adapter 没有聊天视图 | 实现 `chat.startTurn` 和 `onEvent` 结构化事件 |
|
|
1173
|
+
|
|
1174
|
+
先查看插件管理中的状态和错误。main 用 `ctx.log('info' | 'warn' | 'error', message)` 记录插件日志。renderer 可以通过 `ctx.api.logs.list(filter)` 查询 ADE 日志。
|
|
1175
|
+
|
|
1176
|
+
出现用户可见错误时,使用中文说明失败原因和下一步。不要把完整凭据写入日志。
|
|
1177
|
+
|
|
1178
|
+
### 14.2 插件作者的交付检查
|
|
1179
|
+
|
|
1180
|
+
按顺序执行以下检查:
|
|
1181
|
+
|
|
1182
|
+
1. 在没有项目、worktree 和文件时启用插件。
|
|
1183
|
+
2. 选择项目,再打开和切换文件。
|
|
1184
|
+
3. 执行插件的主要操作。
|
|
1185
|
+
4. 修改配置并保存。
|
|
1186
|
+
5. 重启 ADE,检查配置恢复。
|
|
1187
|
+
6. 隐藏并重新显示插件区域。
|
|
1188
|
+
7. 连续停用、启用插件三次。
|
|
1189
|
+
8. 在异步任务执行期间停用插件。
|
|
1190
|
+
9. 重新安装同 ID 的新版本。
|
|
1191
|
+
10. 在独立用户配置中安装最终分发包。
|
|
1192
|
+
|
|
1193
|
+
检查结果:无白屏,无重复组件,无残留任务。空上下文有清楚的提示。失败消息为中文。配置和升级行为与插件说明一致。
|
|
1194
|
+
|
|
1195
|
+
如果插件读写文件、启动进程或调用远程服务,还要验证权限失败、目标不存在、网络失败和取消操作。实际 CLI 或模型 adapter 还要验证续接、停止和历史恢复。
|
|
1196
|
+
|
|
1197
|
+
## 15. API 速查
|
|
1198
|
+
|
|
1199
|
+
以下内容用于定位 API。完整参数和返回类型随 SDK 发布。编辑器可以直接补全,不需要 ADE 源码。
|
|
1200
|
+
|
|
1201
|
+
### 15.1 renderer 上下文
|
|
1202
|
+
|
|
1203
|
+
| 成员 | 用途 |
|
|
1204
|
+
| --- | --- |
|
|
1205
|
+
| `pluginId` | 当前插件 ID |
|
|
1206
|
+
| `React` / `ReactDOM` | 宿主模块实例 |
|
|
1207
|
+
| `api` | ADE preload API,见下表 |
|
|
1208
|
+
| `getContext` / `useContext` | 读取 UI 路径上下文 |
|
|
1209
|
+
| `getConfig` / `useConfig` / `setConfig` / `onConfigChanged` | 读取、修改和订阅配置 |
|
|
1210
|
+
| `registerView` / `registerSlot` | 注册区域与扩展组件 |
|
|
1211
|
+
| `registerFileMenu` | 注册文件树菜单,自动随插件清理 |
|
|
1212
|
+
| `workspace` | 打开文件、刷新文件树、读取 dirty 状态 |
|
|
1213
|
+
| `ui.showToast` | 操作提示与结果链接 |
|
|
1214
|
+
| `registerCommand` / `executeCommand` | 注册与调用命令 |
|
|
1215
|
+
| `provideService` / `getService` | 同进程服务 |
|
|
1216
|
+
| `on` / `emit` | 同进程事件 |
|
|
1217
|
+
| `addStyle` | 添加自动清理的 CSS |
|
|
1218
|
+
| `effect` / `subscriptions` | 登记自己的资源清理 |
|
|
1219
|
+
|
|
1220
|
+
### 15.2 main 上下文
|
|
1221
|
+
|
|
1222
|
+
| 成员 | 用途 |
|
|
1223
|
+
| --- | --- |
|
|
1224
|
+
| `pluginId` / `pluginPath` / `dataPath` | 插件标识、安装目录、数据目录 |
|
|
1225
|
+
| `registerAdapter` | 注册聊天 adapter |
|
|
1226
|
+
| `registerCommand` | 注册 main 命令 |
|
|
1227
|
+
| `provideService` / `getService` | 主进程服务 |
|
|
1228
|
+
| `events.on` / `events.emit` | 主进程事件 |
|
|
1229
|
+
| `getConfig` / `setConfig` / `onConfigChanged` | 读取、修改和订阅配置 |
|
|
1230
|
+
| `log` | 记录插件日志 |
|
|
1231
|
+
| `effect` / `subscriptions` | 登记资源清理 |
|
|
1232
|
+
|
|
1233
|
+
main 没有 renderer 的 `api`、hook、区域或 slot 方法。
|
|
1234
|
+
|
|
1235
|
+
### 15.3 ADE preload API
|
|
1236
|
+
|
|
1237
|
+
绝大多数操作返回 Promise。订阅方法返回解除订阅函数。调用前查看 SDK 中的精确签名。
|
|
1238
|
+
|
|
1239
|
+
| 命名空间 | 主要能力与方法 |
|
|
1240
|
+
| --- | --- |
|
|
1241
|
+
| `api.projects` | `list/add/remove/touch`;目录和 Markdown 文件选择 |
|
|
1242
|
+
| `api.worktrees` | `list/create/remove` |
|
|
1243
|
+
| `api.files` | `tree/read/readBinary/write/stat/isMarkdown`;创建、重命名、复制、回收站;文件监听、系统剪贴板、系统打开与定位 |
|
|
1244
|
+
| `api.chat` | adapter 查询;新建、输入、调整尺寸、发送消息、切换视图、停止、取消;历史、选项、模型、附件、审批;事件订阅 |
|
|
1245
|
+
| `api.app` | 版本、复制文本、关闭窗口、下载页、检查与安装 ADE 更新、更新状态 |
|
|
1246
|
+
| `api.settings` | 读取与修改 ADE 设置、agent 列表、设置变化订阅 |
|
|
1247
|
+
| `api.plugins` | 插件列表、配置、安装、启停、卸载与变更订阅;SDK 导出、main 命令和服务调用 |
|
|
1248
|
+
| `api.daemon` | daemon 信息与 relay 配置 |
|
|
1249
|
+
| `api.aone` | Aone 看板与工作项操作;需要相应账号配置 |
|
|
1250
|
+
| `api.logs` | 日志查询、scope 列表、清空、目录和摘要 |
|
|
1251
|
+
|
|
1252
|
+
调用写操作会产生实际效果。例如 `files.write` 修改磁盘,`chat.sendMessage` 发送消息。插件应在自己的交互中清楚表达操作目标。
|
|
1253
|
+
|
|
1254
|
+
ADE 0.46 起,钉钉业务由独立插件提供,SDK 不再声明 `api.dingtalk`。业务插件通过自己的 main 服务与 React 区域接入;其他插件经 `api.plugins.invokeService` 调用其公开服务。
|
|
1255
|
+
|
|
1256
|
+
下面的 renderer 回调片段读取当前已保存文件:
|
|
1257
|
+
|
|
1258
|
+
```ts
|
|
1259
|
+
const context = ctx.getContext()
|
|
1260
|
+
if (!context.filePath) throw new Error('请先打开文件')
|
|
1261
|
+
const savedText = await ctx.api.files.read(context.filePath)
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### 15.4 类型文件
|
|
1265
|
+
|
|
1266
|
+
安装后,这些文件位于 `node_modules/@ai_mind/ade-plugin-sdk/`:
|
|
1267
|
+
|
|
1268
|
+
| 文件 | 内容 |
|
|
1269
|
+
| --- | --- |
|
|
1270
|
+
| `index.d.ts` | renderer 上下文、组件 props、helper 与所有公共类型导出 |
|
|
1271
|
+
| `types/plugins.d.ts` | manifest、配置、区域、上下文与 disposer |
|
|
1272
|
+
| `types/api.d.ts` | 完整 ADE preload API |
|
|
1273
|
+
| `types/adapter.d.ts` | main 上下文、adapter、会话与轮次 |
|
|
1274
|
+
| `types/ade-types.d.ts` | 项目、worktree、文件、聊天消息和结构化事件等共享类型 |
|
|
1275
|
+
|
|
1276
|
+
从 SDK 根模块导入类型即可:
|
|
1277
|
+
|
|
1278
|
+
```ts
|
|
1279
|
+
import type {
|
|
1280
|
+
AdeApi, MainPluginContext, RendererPluginContext,
|
|
1281
|
+
PluginComponentProps, ChatAdapter, StructuredEvent
|
|
1282
|
+
} from '@ai_mind/ade-plugin-sdk'
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
## 16. 本指南的写作规则
|
|
1286
|
+
|
|
1287
|
+
本指南参考 ASD-STE100 Issue 9 的技术写作原则:固定术语、短句、主动表达、一个句子一个操作,并在步骤前说明条件。中文内容采用这些可迁移的原则。代码、API 名称和领域术语保留原文。[标准说明](https://www.asd-ste100.org/about_STE.html)、[程序写作规则](https://www.asd-ste100.org/assets/files/ASD-STE100_ISSUE9.pdf)。
|
|
1288
|
+
|
|
1289
|
+
“80%”是本指南的写作目标。ASD-STE100 是英语标准,英语受控词表和单词数不能直接用于中文。因此,本指南不声称经过正式的 STE 符合率认证。
|
|
1290
|
+
|
|
1291
|
+
后续维护按同一规则:先说明目标与前提,再给出操作,最后写明可观察的结果。完整示例标出文件路径。局部代码明确标出放置位置。能力说明同时写出运行位置和限制。
|