@ycodium-ai/plugin-api 0.1.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 +106 -0
- package/dist/index.d.ts +1870 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/ui.d.ts +225 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +112 -0
- package/dist/ui.js.map +1 -0
- package/package.json +36 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ycodium-ai/plugin-api` —— 第三方插件写代码时唯一要看的东西。
|
|
3
|
+
*
|
|
4
|
+
* 硬约束:这个包运行时零依赖,只有类型加一个 identity 函数。打包后的 server 是
|
|
5
|
+
* 单文件 bundle,插件目录里 `import "effect"` 解析不到,所以插件不 import 宿主
|
|
6
|
+
* 运行时,能力全部从 `ctx` 拿。
|
|
7
|
+
*
|
|
8
|
+
* 唯一可以在运行时 import 的是本包:宿主在 Ycodium home 下写了一份只含
|
|
9
|
+
* `definePlugin` 与 `PLUGIN_API_VERSION` 的垫片(`pluginHost/pluginApiShim.ts`),
|
|
10
|
+
* 磁盘插件与 selfdev 暂存插件都解析得到。其余部分仍是类型,编译期擦除,所以本包
|
|
11
|
+
* 进插件的 devDependencies 就够。
|
|
12
|
+
*
|
|
13
|
+
* 同理 ctx 上的 API 是普通 TS / Promise,不是 Effect —— 第三方不该被宿主的
|
|
14
|
+
* effect 版本绑住。
|
|
15
|
+
*
|
|
16
|
+
* @module @ycodium-ai/plugin-api
|
|
17
|
+
*/
|
|
18
|
+
/** 宿主认的 API 版本。对不上直接拒绝加载,没有兼容层。 */
|
|
19
|
+
export declare const PLUGIN_API_VERSION = 2;
|
|
20
|
+
export type PluginApiVersion = typeof PLUGIN_API_VERSION;
|
|
21
|
+
/**
|
|
22
|
+
* 注册类调用统一返回它;不调用也行,插件 Scope 关闭时宿主会替你撤销。
|
|
23
|
+
*
|
|
24
|
+
* 插件已经 dispose 之后再注册任何贡献(activate 之后异步注册的常见情形):宿主只记
|
|
25
|
+
* 一条 warning,返回一个 **no-op disposer**,那条贡献不会进已经死掉的记录,也不抛错。
|
|
26
|
+
*/
|
|
27
|
+
export type PluginDisposer = () => void;
|
|
28
|
+
export interface PluginLogFields {
|
|
29
|
+
readonly [key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
/** 日志自动带 pluginId,不进 i18n catalog。 */
|
|
32
|
+
export interface PluginLogger {
|
|
33
|
+
readonly info: (message: string, fields?: PluginLogFields) => void;
|
|
34
|
+
readonly warn: (message: string, fields?: PluginLogFields) => void;
|
|
35
|
+
readonly error: (message: string, fields?: PluginLogFields) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface PluginPaths {
|
|
38
|
+
/**
|
|
39
|
+
* 插件代码所在目录(只读地用,宿主不保证可写)。
|
|
40
|
+
*
|
|
41
|
+
* 自带翻译包的插件把 catalog 放在这个目录下的 `locales/<locale>.json`
|
|
42
|
+
* (`en.json`、`zh-CN.json`……),宿主加载时读进 `plugin:<pluginId>` 命名
|
|
43
|
+
* 空间,缺 locale 回落英文。宿主只加载,不翻译第三方插件的文案。
|
|
44
|
+
*/
|
|
45
|
+
readonly pluginDir: string;
|
|
46
|
+
/** `<ycodium home>/plugin-data/<pluginId>`,卸载插件时不动。 */
|
|
47
|
+
readonly dataDir: string;
|
|
48
|
+
/**
|
|
49
|
+
* `<dataDir>/cache`。语义是**可随时重建**:宿主(或用户)随时可以整个删掉,
|
|
50
|
+
* 插件必须能从零重新算出来。要保留的东西写 `dataDir`。
|
|
51
|
+
*
|
|
52
|
+
* 目录惰性创建 —— 读到这个属性时才建,没用过缓存的插件不留空目录。
|
|
53
|
+
*/
|
|
54
|
+
readonly cacheDir: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PluginCacheResult<T> {
|
|
57
|
+
readonly value: T;
|
|
58
|
+
readonly stale: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 落 `paths.cacheDir` 下的按 key JSON 缓存(口子 #11):插件不用自己拼文件名、
|
|
62
|
+
* 手搓 TTL 判断。文件名是 `sha256(key)` 的十六进制串。
|
|
63
|
+
*
|
|
64
|
+
* `getJson` 的过期语义:
|
|
65
|
+
* - 不传 `maxAgeMs`:文件存在且能解析就返回,`stale` 恒 `false`(不做新鲜度判断,
|
|
66
|
+
* 写的时候自己决定要不要覆盖)。
|
|
67
|
+
* - 传了 `maxAgeMs` 且未过期:返回,`stale` `false`。
|
|
68
|
+
* - 传了 `maxAgeMs` 且已过期:`allowStale` 为 `true` 时照样返回旧值,标
|
|
69
|
+
* `stale: true`;否则视同没有缓存,返回 `null`——这是"退回旧结果顶一阵"与
|
|
70
|
+
* "过期就必须重新请求"两种插件都要用到的口子,用一个参数分开。
|
|
71
|
+
* - 缺失 / JSON 解析失败 / 形状不对:返回 `null`;后两种情况宿主会顺手删掉这份
|
|
72
|
+
* 坏文件,不留着占位置。
|
|
73
|
+
*/
|
|
74
|
+
export interface PluginCache {
|
|
75
|
+
readonly getJson: <T = unknown>(key: string, maxAgeMs?: number, allowStale?: boolean) => Promise<PluginCacheResult<T> | null>;
|
|
76
|
+
readonly setJson: (key: string, value: unknown) => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 插件看到的编排事件。字段是宿主事件信封的子集 —— 插件不 import contracts,
|
|
80
|
+
* 所以这里是结构化描述,payload 保持 unknown,由插件自己按 `type` 收窄。
|
|
81
|
+
*/
|
|
82
|
+
export interface PluginEvent {
|
|
83
|
+
readonly type: string;
|
|
84
|
+
readonly payload: unknown;
|
|
85
|
+
readonly sequence: number;
|
|
86
|
+
readonly eventId: string;
|
|
87
|
+
readonly aggregateKind: string;
|
|
88
|
+
readonly aggregateId: string;
|
|
89
|
+
readonly occurredAt: string;
|
|
90
|
+
}
|
|
91
|
+
export type PluginEventHandler = (event: PluginEvent) => void | Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* 一条触发事件是谁报的。宿主从编排事件翻出来的是 `host`;插件经 `ctx.events.emit`
|
|
94
|
+
* 报的带那个插件的 id。
|
|
95
|
+
*/
|
|
96
|
+
export type PluginTriggerEventSource = {
|
|
97
|
+
readonly kind: "host";
|
|
98
|
+
} | {
|
|
99
|
+
readonly kind: "plugin";
|
|
100
|
+
readonly pluginId: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* 触发事件使用开放的 kind,领域标识放在 data 中;宿主补充事件来源。
|
|
104
|
+
*/
|
|
105
|
+
export interface PluginTriggerEvent {
|
|
106
|
+
readonly eventId: string;
|
|
107
|
+
readonly kind: string;
|
|
108
|
+
readonly occurredAt: string;
|
|
109
|
+
readonly projectId: string | null;
|
|
110
|
+
readonly threadId: string | null;
|
|
111
|
+
/** 事件相关的短文本(thread 标题、审批摘要、run 结局)。 */
|
|
112
|
+
readonly summary: string;
|
|
113
|
+
/** 谁报的。用户配的触发器与界面靠它分辨真事件与某个插件的说法。 */
|
|
114
|
+
readonly source: PluginTriggerEventSource;
|
|
115
|
+
/** kind 特有的结构化字段;宿主翻出来的事件这里是空对象。 */
|
|
116
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
117
|
+
}
|
|
118
|
+
export type PluginTriggerHandler = (event: PluginTriggerEvent) => void | Promise<void>;
|
|
119
|
+
/** `ctx.events.emit` 的那一半:宿主补 `eventId`、`source`,缺省补 `occurredAt`。 */
|
|
120
|
+
export interface PluginTriggerEventInput {
|
|
121
|
+
/** 重试同一触发时保持不变;宿主补插件命名空间。 */
|
|
122
|
+
readonly idempotencyKey?: string;
|
|
123
|
+
/** 事件相关的短文本。用户把它绑到团队时,这句就是那次 run 的 goal。 */
|
|
124
|
+
readonly summary: string;
|
|
125
|
+
readonly projectId?: string | null | undefined;
|
|
126
|
+
readonly threadId?: string | null | undefined;
|
|
127
|
+
/** ISO 时间;不给就是宿主收到这条的时刻。 */
|
|
128
|
+
readonly occurredAt?: string | undefined;
|
|
129
|
+
/** kind 特有的结构化字段。只放长期稳定的标识,订阅者靠它定位这条事件说的是谁。 */
|
|
130
|
+
readonly data?: Readonly<Record<string, unknown>> | undefined;
|
|
131
|
+
}
|
|
132
|
+
export interface PluginEvents {
|
|
133
|
+
/** 本插件作为目标的机器级触发绑定;读取实际设置,不读取插件局部配置。 */
|
|
134
|
+
readonly listBindings: () => Promise<Readonly<Record<string, PluginTriggerBinding>>>;
|
|
135
|
+
/**
|
|
136
|
+
* 订阅一类触发事件。串行、可 drain,按 eventId 去重。
|
|
137
|
+
*/
|
|
138
|
+
readonly on: (kind: string, handler: PluginTriggerHandler) => PluginDisposer;
|
|
139
|
+
/**
|
|
140
|
+
* 报一条触发事件,进的是宿主翻译出来的那些事件的同一条分发链:同一份去重、同一批
|
|
141
|
+
* 插件订阅者、同一套用户配的触发器。`kind` 是非空字符串,插件拥有其领域命名。
|
|
142
|
+
*
|
|
143
|
+
* **你报的事件带你的插件 id**(`event.source`),用户看得见是谁报的——所以这里没有
|
|
144
|
+
* 权限门:能报假事件的插件本来也能直接调命令。
|
|
145
|
+
*
|
|
146
|
+
* 你自己的 `on` 处理器收不到你自己报的事件,`emit` → `on` → `emit` 转不起来。
|
|
147
|
+
*/
|
|
148
|
+
readonly emit: (kind: string, event: PluginTriggerEventInput) => Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* 订阅原始编排事件类型。等价于 `ctx.reactor({ events: [type], handle })`;
|
|
151
|
+
* 形状随宿主演进,能用触发事件就别用它。
|
|
152
|
+
*/
|
|
153
|
+
readonly onRaw: (type: string, handler: PluginEventHandler) => PluginDisposer;
|
|
154
|
+
/**
|
|
155
|
+
* 把你自己的一条事实写进宿主同一条事件日志。
|
|
156
|
+
*
|
|
157
|
+
* 什么时候用它:你要记住的东西跨事件、跨重启(看板、名册、信箱)。把它攒在内存里
|
|
158
|
+
* 就是已知会丢的那条路;落成事实,再用 `ctx.derivedState` 折出来。
|
|
159
|
+
*
|
|
160
|
+
* 三条硬规矩:
|
|
161
|
+
* - `name` 会被宿主强制成 `<你的插件 id>/…`。不带这个前缀当场拒绝——两个插件的
|
|
162
|
+
* 事件名撞上,谁的折叠都会吃到对方的事实。
|
|
163
|
+
* - `data` 是你自己的形状,宿主原样存、原样发回,**不校验也不解释**。形状变了
|
|
164
|
+
* 要么自己兼容旧数据,要么把 `derivedState` 的 `version` 加一从头重折。
|
|
165
|
+
* - 事实挂在哪个聚合上由你指定(project / thread / plugin)。挂错了不会报错,但
|
|
166
|
+
* 按聚合查的人找不到它。
|
|
167
|
+
*
|
|
168
|
+
* 写进去的事实之后会以 `plugin.event-appended` 出现在 `onRaw` 与 `reactor` 里,
|
|
169
|
+
* 你自己的也一样——要区分是不是自己写的,看 `payload.pluginId`。
|
|
170
|
+
* 返回提交序号,可传给派生状态 `get({ throughSequence })` 读取这次写入。
|
|
171
|
+
*/
|
|
172
|
+
readonly append: (input: PluginEventAppendInput) => Promise<{
|
|
173
|
+
readonly sequence: number;
|
|
174
|
+
}>;
|
|
175
|
+
}
|
|
176
|
+
export interface PluginTriggerBinding {
|
|
177
|
+
readonly bindingId: string;
|
|
178
|
+
readonly enabled: boolean;
|
|
179
|
+
readonly projectId: string;
|
|
180
|
+
readonly source: {
|
|
181
|
+
readonly kind: "event";
|
|
182
|
+
readonly eventKind: string;
|
|
183
|
+
};
|
|
184
|
+
readonly target: {
|
|
185
|
+
readonly kind: "plugin";
|
|
186
|
+
readonly pluginId: string;
|
|
187
|
+
readonly command: string;
|
|
188
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** `ctx.events.append` 的入参。 */
|
|
192
|
+
export interface PluginEventAppendInput {
|
|
193
|
+
/** 同一操作重试时保持不变;宿主按安装条目、插件与数据目录隔离。省略则每次新建。 */
|
|
194
|
+
readonly commandId?: string;
|
|
195
|
+
/** 事实的名字。宿主会强制它以 `<你的插件 id>/` 开头。 */
|
|
196
|
+
readonly name: string;
|
|
197
|
+
/** 事实挂在哪个聚合上。 */
|
|
198
|
+
readonly aggregate: {
|
|
199
|
+
readonly kind: "project";
|
|
200
|
+
readonly id: string;
|
|
201
|
+
} | {
|
|
202
|
+
readonly kind: "thread";
|
|
203
|
+
readonly id: string;
|
|
204
|
+
} | {
|
|
205
|
+
readonly kind: "plugin";
|
|
206
|
+
readonly id: string;
|
|
207
|
+
};
|
|
208
|
+
/** 你自己的形状。必须是能 JSON 序列化的值。 */
|
|
209
|
+
readonly data: unknown;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 一块由事件折出来的状态,宿主替你记住、替你重建。
|
|
213
|
+
*
|
|
214
|
+
* 什么时候用它:你要记住的东西跨事件、跨重启(看板、名册、信箱)。**别把这种东西攒
|
|
215
|
+
* 在插件的模块变量或 Map 里** —— 插件停用、宿主重启、会话换进程,那份内存就没了,
|
|
216
|
+
* 而且没有任何东西会告诉你它没了。
|
|
217
|
+
*
|
|
218
|
+
* 只有一条硬规矩,但它排掉一整类缺陷:**`apply` 必须是纯函数**。同样的入参给同样的
|
|
219
|
+
* 结果,不读时钟、不读随机数、不发请求、不改传进来的 `state`(返回新值)。它会被跑
|
|
220
|
+
* 两次——事件来的时候一次,重启重建的时候一次——不纯就意味着两次结果不同,而你只会
|
|
221
|
+
* 在重启之后才发现。
|
|
222
|
+
*
|
|
223
|
+
* 状态必须能 JSON 序列化。宿主不校验它的形状:改了形状就把 `version` 加一,宿主会
|
|
224
|
+
* 丢掉旧的从头重折。不加,旧状态会带着旧规则一直用下去。
|
|
225
|
+
*/
|
|
226
|
+
export interface PluginDerivedState<S> {
|
|
227
|
+
/** 你自己的名字,插件内唯一。宿主会加上你的插件 id 做前缀,和别人撞不上。 */
|
|
228
|
+
readonly key: string;
|
|
229
|
+
/** 折法或状态形状改了就 +1。宿主据此丢掉旧的重折。 */
|
|
230
|
+
readonly version: number;
|
|
231
|
+
/** 只折这些事件类型;省略表示全收。过滤在宿主侧做,不进你的进程时间。 */
|
|
232
|
+
readonly events?: readonly string[] | undefined;
|
|
233
|
+
/** 空状态。重建从这里起折。 */
|
|
234
|
+
readonly init: () => S;
|
|
235
|
+
/** 纯折叠。不许就地改 `state`。 */
|
|
236
|
+
readonly apply: (state: S, event: PluginEvent) => S;
|
|
237
|
+
}
|
|
238
|
+
/** `ctx.derivedState` 给回来的把手。 */
|
|
239
|
+
export interface PluginDerivedStateHandle<S> {
|
|
240
|
+
/**
|
|
241
|
+
* 此刻的状态。在 `reactor` / `events.onRaw` 处理器里读到的,**已经含当前这条事件**
|
|
242
|
+
* ——宿主先推进状态再把事件发给你。
|
|
243
|
+
* 指定 throughSequence 会直接补读已提交日志,保证包含该序号;不等待 reactor 队列。
|
|
244
|
+
* 未提交的未来序号会拒绝,不会挂起等待。
|
|
245
|
+
*/
|
|
246
|
+
readonly get: (options?: {
|
|
247
|
+
readonly throughSequence: number;
|
|
248
|
+
}) => Promise<S>;
|
|
249
|
+
}
|
|
250
|
+
export interface PluginReactor {
|
|
251
|
+
/** 用于日志归因,插件内唯一即可。 */
|
|
252
|
+
readonly name: string;
|
|
253
|
+
/** 关心的事件类型;省略表示全收。过滤在宿主侧做,不进插件进程时间。 */
|
|
254
|
+
readonly events?: readonly string[] | undefined;
|
|
255
|
+
/** 串行执行;抛错只记 warning,不断流、不影响别的插件。 */
|
|
256
|
+
readonly handle: PluginEventHandler;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 宿主托管的重复任务(路线图 #37)。
|
|
260
|
+
*
|
|
261
|
+
* 只在**没有编排事件可驱动**时才用它 —— 要跟的东西是别的程序写的(终端会话导入
|
|
262
|
+
* 跟读别人写的记录就是这么一例)。有事件能驱动就用 `ctx.events.on`:定时器每一跳
|
|
263
|
+
* 都是白花的电。
|
|
264
|
+
*
|
|
265
|
+
* 别自己 `setInterval`:那种循环宿主看不见(`ycodium_inspect` 与插件卡上数不到),
|
|
266
|
+
* 停用插件时也没人替你关。
|
|
267
|
+
*/
|
|
268
|
+
export interface PluginInterval {
|
|
269
|
+
/** 用于日志归因与插件卡展示,插件内唯一;重名当场抛错,与命令同一条规矩。 */
|
|
270
|
+
readonly name: string;
|
|
271
|
+
/** 两次开跑之间的间隔。下限 `MIN_PLUGIN_INTERVAL_MS`,低于它当场抛错。 */
|
|
272
|
+
readonly everyMs: number;
|
|
273
|
+
/** 默认 false:第一跳在一个间隔之后。true 表示注册当场先跑一次。 */
|
|
274
|
+
readonly runImmediately?: boolean | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* 同一条循环不会重入:上一跳没跑完,下一跳等它。抛错只记一条 warning,循环照跑,
|
|
277
|
+
* 不影响别的插件,也不会把宿主带下水。
|
|
278
|
+
*/
|
|
279
|
+
readonly handle: () => void | Promise<void>;
|
|
280
|
+
}
|
|
281
|
+
/** `everyMs` 的下限。比这更密的循环没有正当用途,只会烧 CPU。 */
|
|
282
|
+
export declare const MIN_PLUGIN_INTERVAL_MS = 1000;
|
|
283
|
+
/**
|
|
284
|
+
* 位置参数。声明顺序即命令行顺序;`variadic` 只能是最后一个。
|
|
285
|
+
*/
|
|
286
|
+
export interface PluginCliArgument {
|
|
287
|
+
/** 也是 `input.arguments` 里的键。 */
|
|
288
|
+
readonly name: string;
|
|
289
|
+
readonly description?: string | undefined;
|
|
290
|
+
/** 默认 true。可选参数缺席时键不出现。 */
|
|
291
|
+
readonly required?: boolean | undefined;
|
|
292
|
+
/** 吃掉剩下的全部参数,值是 `string[]`。只能声明在最后一个。 */
|
|
293
|
+
readonly variadic?: boolean | undefined;
|
|
294
|
+
}
|
|
295
|
+
/** `--name` 选项。`kind` 决定 `input.options[name]` 的类型。 */
|
|
296
|
+
export interface PluginCliOption {
|
|
297
|
+
readonly name: string;
|
|
298
|
+
readonly kind?: "string" | "boolean" | undefined;
|
|
299
|
+
/** 单字母短名,`-n`。 */
|
|
300
|
+
readonly alias?: string | undefined;
|
|
301
|
+
readonly description?: string | undefined;
|
|
302
|
+
/** 缺省值。不给的话 string 选项缺席即 undefined,boolean 缺席即 false。 */
|
|
303
|
+
readonly defaultValue?: string | boolean | undefined;
|
|
304
|
+
}
|
|
305
|
+
export interface PluginCliInput {
|
|
306
|
+
/** 按声明的名字取;variadic 的那个是 `string[]`。 */
|
|
307
|
+
readonly arguments: Readonly<Record<string, string | readonly string[]>>;
|
|
308
|
+
readonly options: Readonly<Record<string, string | boolean | undefined>>;
|
|
309
|
+
}
|
|
310
|
+
/** 命令的输出通道。日志走 `ctx.log`,给人看的输出走这里。 */
|
|
311
|
+
export interface PluginCliIo {
|
|
312
|
+
/** 写一行到 stdout。 */
|
|
313
|
+
readonly out: (line: string) => void;
|
|
314
|
+
/** 写一行到 stderr。 */
|
|
315
|
+
readonly err: (line: string) => void;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 一条 `ycodium x <pluginId> <name>` 子命令(路线图 #6)。
|
|
319
|
+
*
|
|
320
|
+
* 跑它的是 **CLI 进程**,不是服务器进程:那里没有编排引擎也没有数据库,所以
|
|
321
|
+
* `ctx.commands.dispatch` / `ctx.decisions` / `ctx.projections` / `ctx.providers`
|
|
322
|
+
* 一律被拒(`reason: "host-unavailable"`)。`ctx.config` / `ctx.fs` / `ctx.http` /
|
|
323
|
+
* `ctx.process` / `ctx.cache` / `ctx.settings` 照常可用。
|
|
324
|
+
*
|
|
325
|
+
* 参数与选项是**声明**,由宿主翻成真正的命令行解析——插件不 import CLI 框架,
|
|
326
|
+
* `--help`、缺参报错、类型转换都由宿主给。
|
|
327
|
+
*
|
|
328
|
+
* 返回值即退出码:`undefined` / 0 表示成功,非 0 会让 `ycodium x` 以该码退出。
|
|
329
|
+
*/
|
|
330
|
+
export interface PluginCliCommand {
|
|
331
|
+
/** 插件内唯一;重名当场抛错。与 `ctx.command` 是两张名字表,可以同名。 */
|
|
332
|
+
readonly name: string;
|
|
333
|
+
readonly description?: string | undefined;
|
|
334
|
+
readonly arguments?: readonly PluginCliArgument[] | undefined;
|
|
335
|
+
readonly options?: readonly PluginCliOption[] | undefined;
|
|
336
|
+
readonly handle: (input: PluginCliInput, io: PluginCliIo) => void | number | Promise<void | number>;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 工具结果里的一张图。宿主把它翻成 MCP 的 `image` 内容块(协议上是 base64,编码
|
|
340
|
+
* 由宿主做,插件不要自己编)。
|
|
341
|
+
*
|
|
342
|
+
* `data` 只认原始字节,不认文件路径:读磁盘只有 `ctx.fs` 那一道 `fs.read` 前缀门,
|
|
343
|
+
* 工具结果这边不另开第二道。要回一张磁盘上的图就自己走
|
|
344
|
+
* `ctx.fs.createReadStream(path)` 把分片拼起来,权限与越界拒绝与别处完全一致。
|
|
345
|
+
*
|
|
346
|
+
* `mimeType` 只认 `image/png`、`image/jpeg`、`image/gif`、`image/webp` —— 与用户上传
|
|
347
|
+
* 附件共用同一张「provider 能内联的图片」表,大小写不敏感。
|
|
348
|
+
*/
|
|
349
|
+
export interface PluginToolImage {
|
|
350
|
+
readonly data: Uint8Array;
|
|
351
|
+
readonly mimeType: string;
|
|
352
|
+
}
|
|
353
|
+
export interface PluginToolResult {
|
|
354
|
+
readonly text: string;
|
|
355
|
+
/**
|
|
356
|
+
* 随文本一起回给模型的图片。上限:单张 3 MiB,一次结果最多 4 张。
|
|
357
|
+
*
|
|
358
|
+
* 超限的那张、mimeType 不认的那张、`data` 不是 `Uint8Array` 的那张都只是被丢掉,
|
|
359
|
+
* `text` 末尾附一行说明为什么,整次调用**不算失败**(与 `ctx.process.run` 超出
|
|
360
|
+
* `maxOutputBytes` 就截断同一条纪律:降级,不是错误)。
|
|
361
|
+
*
|
|
362
|
+
* provider 认不认图片各家不同,`text` 永远是那条保底的路——别把只有图片才说得清
|
|
363
|
+
* 的信息全押在图上。
|
|
364
|
+
*/
|
|
365
|
+
readonly images?: readonly PluginToolImage[] | undefined;
|
|
366
|
+
/** 置 true 表示这次调用失败;宿主原样转成 MCP 的 `isError`。 */
|
|
367
|
+
readonly isError?: boolean | undefined;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 工具被调用时宿主知道的上下文。字段只增不改;宿主解析不出来的一律给 null,插件
|
|
371
|
+
* 不该假设它们一定有值。
|
|
372
|
+
*/
|
|
373
|
+
export interface PluginToolInvocation {
|
|
374
|
+
readonly threadId: string | null;
|
|
375
|
+
readonly projectId: string | null;
|
|
376
|
+
/** 该 thread 所属 project 的工作区根目录。 */
|
|
377
|
+
readonly projectRoot: string | null;
|
|
378
|
+
}
|
|
379
|
+
export interface PluginTool {
|
|
380
|
+
readonly name: string;
|
|
381
|
+
readonly description: string;
|
|
382
|
+
/** JSON Schema(draft-07 子集),宿主原样交给 MCP 客户端。 */
|
|
383
|
+
readonly inputSchema: Record<string, unknown>;
|
|
384
|
+
readonly handle: (input: Record<string, unknown>, invocation: PluginToolInvocation) => Promise<PluginToolResult>;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 工具在 MCP 上以 `<pluginId>_<tool>` 出现,防撞名。插件卸载后工具立即从
|
|
388
|
+
* `tools/list` 消失,此后再调用只会拿到一条明确的错误。
|
|
389
|
+
*
|
|
390
|
+
* **线程级插件的工具只发给挂载它的那条线程的 MCP 会话。** 两道门:`tools/list` 时按
|
|
391
|
+
* 会话归属的 threadId 过滤(软边界),`tools/call` 时按 (threadId, name) 硬查表——
|
|
392
|
+
* 别的线程猜到名字硬调也只拿到一条干净的错误。所以两条线程可以各注册一个同名工具,
|
|
393
|
+
* 互不影响;线程工具撞上已装好的全局工具名时让位(同一会话的列表里不出现重名)。
|
|
394
|
+
*
|
|
395
|
+
* 工具表变化时宿主会发 `tools/list_changed`,但 **provider CLI 的工具清单通常在会话
|
|
396
|
+
* 启动时就冻结了**:新挂的工具往往要等这条线程下一次 provider 会话才真正可调。
|
|
397
|
+
* reactor 与触发器不吃这条,注册完立刻生效。
|
|
398
|
+
*/
|
|
399
|
+
export interface PluginToolkit {
|
|
400
|
+
readonly name: string;
|
|
401
|
+
readonly tools: readonly PluginTool[];
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* `password` 的值不落 patch 文件、也不下发给客户端:宿主把它存进机器上的密文库,
|
|
405
|
+
* patch 里只留一条引用,装配期解出来放进 `ctx.config`。所以卡片上永远不会回显
|
|
406
|
+
* 已存的密码,改密码只能整值替换,并且换值会让本插件重启一次。
|
|
407
|
+
*/
|
|
408
|
+
/**
|
|
409
|
+
* `proxy` 字段里「明确不加代理」那一档的值。空串是「跟机器默认走」,其余是某个代理的 id。
|
|
410
|
+
*
|
|
411
|
+
* 常量在这里,别在插件里各写一份 `"direct"`:三态语义是宿主定的,字面量散出去之后
|
|
412
|
+
* 改一个就得找齐所有副本。
|
|
413
|
+
*/
|
|
414
|
+
export declare const PLUGIN_PROXY_DIRECT = "direct";
|
|
415
|
+
export type PluginSettingsColumnKind = "text" | "password" | "number" | "boolean" | "select"
|
|
416
|
+
/**
|
|
417
|
+
* 代理选择器。**选项不用你填**:宿主会画出「跟机器默认」「不加代理」,以及这台机器
|
|
418
|
+
* 上配的每一个代理;值就是 `ctx.proxies.fetch` 认的那三态(空串/`"direct"`/某个 id)。
|
|
419
|
+
*
|
|
420
|
+
* 之所以不是一个自带选项的 `select`:宿主要**看得懂**这个值。删代理前设置里会问
|
|
421
|
+
* 「谁还在用」,而配置里的一串文字它认不出是代理引用——声明成这一种它就认得出,
|
|
422
|
+
* 你正用着的代理因此删不掉(路线图 #64)。
|
|
423
|
+
*/
|
|
424
|
+
| "proxy";
|
|
425
|
+
/** `table` 的值形状见 `PluginSettingsTableValue`;其余种类的值是标量。 */
|
|
426
|
+
export type PluginSettingsFieldKind = PluginSettingsColumnKind | "table";
|
|
427
|
+
export interface PluginSettingsFieldOption {
|
|
428
|
+
readonly value: string;
|
|
429
|
+
readonly label: string;
|
|
430
|
+
/**
|
|
431
|
+
* 选项图标。与页面 / 面板槽位同一套写法、同一份白名单:宿主的图标名,或指向自己
|
|
432
|
+
* `assets` 目录里一张 PNG 的相对路径。认不出就只画文字,不留空格子。
|
|
433
|
+
*/
|
|
434
|
+
readonly icon?: string | undefined;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 一个格子什么时候才出现:同一层里另一个格子的值落在 `equals` 里。
|
|
438
|
+
*
|
|
439
|
+
* 用来表达「先选类型、再填对应字段」。没有它,声明只能把所有变体的字段并排摊开——选了
|
|
440
|
+
* Telegram 的那一行,飞书与企微的凭据列空着摆在旁边。
|
|
441
|
+
*
|
|
442
|
+
* **只看同一层**:列看同一行的别的列,顶层字段看别的顶层字段。指向不存在的 key、或
|
|
443
|
+
* `equals` 是空数组,都表示恒不显示。
|
|
444
|
+
*
|
|
445
|
+
* **隐藏不等于清空**:藏起来的格子值仍然留着,改回去还能看见。所以插件读配置时该按自己
|
|
446
|
+
* 的语义判断,不能假设隐藏的格子是空的。
|
|
447
|
+
*/
|
|
448
|
+
export interface PluginSettingsVisibility {
|
|
449
|
+
readonly key: string;
|
|
450
|
+
readonly equals: readonly string[];
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 实例表的一列。列复用字段的全部声明与校验,只是种类里没有 `table`——表不能自嵌套。
|
|
454
|
+
* `password` 列走的是顶层 password 字段那一套:每行每列一条密文引用,明文不进
|
|
455
|
+
* patch 文件也不下发给客户端,装配期解出来放回同一格。
|
|
456
|
+
*/
|
|
457
|
+
export interface PluginSettingsColumn {
|
|
458
|
+
readonly key: string;
|
|
459
|
+
readonly kind: PluginSettingsColumnKind;
|
|
460
|
+
readonly label: string;
|
|
461
|
+
readonly description?: string | undefined;
|
|
462
|
+
readonly placeholder?: string | undefined;
|
|
463
|
+
readonly required?: boolean | undefined;
|
|
464
|
+
readonly options?: readonly PluginSettingsFieldOption[] | undefined;
|
|
465
|
+
/** 缺席=一直显示。见 {@link PluginSettingsVisibility}。 */
|
|
466
|
+
readonly showWhen?: PluginSettingsVisibility | undefined;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 行 id:字母开头,后接字母 / 数字 / `_` / `-`,最长 64。不许数字开头——那种键会被
|
|
470
|
+
* JS 重排到最前,界面上的行序就跟加行顺序对不上了。
|
|
471
|
+
*/
|
|
472
|
+
export type PluginSettingsRowId = string;
|
|
473
|
+
/** 表的一行:列 key → 值。 */
|
|
474
|
+
export type PluginSettingsTableRow = Readonly<Record<string, unknown>>;
|
|
475
|
+
/**
|
|
476
|
+
* `kind: "table"` 字段在 config 里的值。整表替换:保存时没出现的行就是被删掉的行,
|
|
477
|
+
* 它那几格密文跟着回收;行的顺序即 `Object.keys` 的顺序。
|
|
478
|
+
*/
|
|
479
|
+
export type PluginSettingsTableValue = Readonly<Record<PluginSettingsRowId, PluginSettingsTableRow>>;
|
|
480
|
+
/**
|
|
481
|
+
* `label` / `description` 由插件提供,宿主不翻译,UI 会标明这一点。
|
|
482
|
+
*
|
|
483
|
+
* `kind: "table"` 是一张实例表——一行一个实例,`columns` 声明每行有哪些子字段。
|
|
484
|
+
* 声明表时 `columns` 必须给;`rowIds` 说明行 id 谁生成:`client`(缺席即此)表示
|
|
485
|
+
* 人在界面上加行、客户端生成 id,`plugin` 表示行由插件预置进 config,界面只能改
|
|
486
|
+
* 值不能增删行。表字段自己的 `placeholder` / `options` 不用。
|
|
487
|
+
*/
|
|
488
|
+
export interface PluginSettingsField extends Omit<PluginSettingsColumn, "kind"> {
|
|
489
|
+
readonly kind: PluginSettingsFieldKind;
|
|
490
|
+
readonly columns?: readonly PluginSettingsColumn[] | undefined;
|
|
491
|
+
readonly rowIds?: "client" | "plugin" | undefined;
|
|
492
|
+
}
|
|
493
|
+
export interface PluginSettingsCard {
|
|
494
|
+
readonly title: string;
|
|
495
|
+
readonly description?: string | undefined;
|
|
496
|
+
readonly fields: readonly PluginSettingsField[];
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 命令被调用时宿主知道的上下文。RPC 是全局作用域(thread-scoped 命令刻意
|
|
500
|
+
* 推迟),所以这里只有调用方的粗粒度信息;字段只增不改。
|
|
501
|
+
*/
|
|
502
|
+
export interface PluginCommandInvocation {
|
|
503
|
+
/** 调用方凭据已通过的 scope("read" 或 "operate")。 */
|
|
504
|
+
readonly scope: "read" | "operate";
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* JSON Schema,draft-07 的一个**白名单子集**。宿主自带编译器(仓库没有 JSON Schema
|
|
508
|
+
* 运行时依赖),认不出来的关键字一律编译期报错——插件 activate 当场失败,不会静默
|
|
509
|
+
* 变成"写了等于没写"。
|
|
510
|
+
*
|
|
511
|
+
* 下面这份清单**以 `apps/server/src/pluginHost/jsonSchema.ts` 为准**(`SUPPORTED_KEYWORDS`
|
|
512
|
+
* 与 `ANNOTATION_KEYWORDS` 两张表),那边改了这里跟着改。
|
|
513
|
+
*
|
|
514
|
+
* 支持:`type`(object / string / number / integer / boolean / array / null,也可以
|
|
515
|
+
* 写成数组表示可空)、`properties`、`required`、`additionalProperties`(布尔或
|
|
516
|
+
* schema)、`enum`、`const`、`items`(单个 schema,不支持元组形式)、
|
|
517
|
+
* `minimum` / `maximum` / `exclusiveMinimum` / `exclusiveMaximum`、
|
|
518
|
+
* `minLength` / `maxLength`、`pattern`(`u` flag 的 RegExp)、`minItems` / `maxItems`、
|
|
519
|
+
* `oneOf`(至少两个分支,恰好一个匹配才算过)。
|
|
520
|
+
* 另外接受但不校验这几个注解:`description`、`title`、`default`、`examples`、
|
|
521
|
+
* `$schema`、`$comment`。
|
|
522
|
+
*
|
|
523
|
+
* 判别联合就写 `oneOf`:共有字段留在同层的 `properties` / `required` 上(同层关键字
|
|
524
|
+
* 与分支一起生效),分支里只写判别键的 `const` 与那一支自己的 `required`,这样
|
|
525
|
+
* "status 是 ok 却没带 items" 会被挡在出口。
|
|
526
|
+
*
|
|
527
|
+
* 不支持:`$ref`、`anyOf` / `allOf` / `not`、`if` / `then` / `else`、
|
|
528
|
+
* `patternProperties`、`dependencies`、`format`。一个 `oneOf` 足够表达判别联合,
|
|
529
|
+
* 其余组合关键字是复杂度不换收益。
|
|
530
|
+
*
|
|
531
|
+
* 顶层形状只有 `payloadSchema` 有额外限制(必须 `"type": "object"`);`resultSchema`
|
|
532
|
+
* 与 `itemSchema` 顶层随意。
|
|
533
|
+
*/
|
|
534
|
+
export type PluginJsonSchema = Record<string, unknown>;
|
|
535
|
+
/**
|
|
536
|
+
* 请求-响应命令。客户端经宿主的 `plugins.invoke` 单信封调用;payload 在
|
|
537
|
+
* 信封入口按 payloadSchema 校验,不合格的请求不会到达 handle。handle 抛错
|
|
538
|
+
* 会以 handler-failed 的错误形状过线,message 原样透出——别把秘密放进异常。
|
|
539
|
+
*
|
|
540
|
+
* 要让调用方能**分辨**是哪一类失败,就在抛出的错误上挂一个字符串 `code`:宿主把
|
|
541
|
+
* 它原样放进过线错误的 `pluginCode`。过线错误的 `code` 字段是宿主自己的信封分类
|
|
542
|
+
* (plugin-unavailable / invalid-payload / forbidden / handler-failed),插件写不
|
|
543
|
+
* 进去;调用方也不该去 parse 人读的 message。
|
|
544
|
+
*
|
|
545
|
+
* 要让调用方**照着做**(等到几点再试之类),再挂一个 `data`:宿主做一次 JSON 往返
|
|
546
|
+
* 后放进过线错误的 `pluginData`。只有 JSON 值能过去,序列化不了的整个丢掉;形状由
|
|
547
|
+
* 插件与它的调用方约定,宿主不解释。速率限制的写法是 `{ retryAt: <epoch ms> }`。
|
|
548
|
+
*/
|
|
549
|
+
/**
|
|
550
|
+
* 宿主 contracts schema 的名字(如 `"UsageSummary"`),用来替代手写 JSON Schema。
|
|
551
|
+
*
|
|
552
|
+
* **只有内置插件能用。** 第三方插件的运行环境里没有 `@ycodium/contracts`,声明了
|
|
553
|
+
* 也不可能与宿主对齐同一份形状,所以宿主在注册时直接拒绝——插件加载失败,失败信息
|
|
554
|
+
* 里写明「改用 payloadSchema / resultSchema 的 JSON Schema」。
|
|
555
|
+
*
|
|
556
|
+
* 与同一条命令上对应的 `*Schema` 字段**互斥**:两个都给是注册错误,一个都不给
|
|
557
|
+
* (payload 侧)也是注册错误。校验只判合法、不改值:contracts 里的
|
|
558
|
+
* `TrimmedNonEmptyString` 之类是 transformation,宿主丢弃 decode 的产物,命令的
|
|
559
|
+
* payload 与结果都按原值过线。
|
|
560
|
+
*/
|
|
561
|
+
export type PluginContractName = string;
|
|
562
|
+
export interface PluginCommand {
|
|
563
|
+
/**
|
|
564
|
+
* 插件内唯一;跨插件由 pluginId 隔开,不会撞名。
|
|
565
|
+
*
|
|
566
|
+
* 同一插件内重复注册同名命令**当场抛错**,不顶掉旧的:命令名是过线的寻址键,静默
|
|
567
|
+
* 替换会让已经在飞的调用换了含义。抛在 `ctx.command` / `ctx.stream` 调用处,即
|
|
568
|
+
* activate 失败、整个插件挂载失败。`ctx.command` 与 `ctx.stream` 共用同一张名字表。
|
|
569
|
+
*/
|
|
570
|
+
readonly name: string;
|
|
571
|
+
readonly description?: string | undefined;
|
|
572
|
+
/** 缺省 "read"。声明 "operate" 的命令要求调用方持有 operate 凭据。 */
|
|
573
|
+
readonly scope?: "read" | "operate" | undefined;
|
|
574
|
+
/**
|
|
575
|
+
* 顶层必须是 `"type": "object"`;handle 收到的就是它校验过的形状。与
|
|
576
|
+
* `payloadContract` 二选一,缺一不可。
|
|
577
|
+
*/
|
|
578
|
+
readonly payloadSchema?: PluginJsonSchema | undefined;
|
|
579
|
+
/** 仅内置插件:按名引用宿主注册表里的 contracts schema,替代 `payloadSchema`。 */
|
|
580
|
+
readonly payloadContract?: PluginContractName | undefined;
|
|
581
|
+
/**
|
|
582
|
+
* 可选。声明了,宿主就在**出口**按它校验 handle 的返回值:不合格的返回值不过线,
|
|
583
|
+
* 调用方收到 `invalid-result` 错误(detail 是 JSON Pointer 风格的第一条违例),
|
|
584
|
+
* 宿主同时打一条带 pluginId / command 的 warn。不声明就不校验,返回值原样过线。
|
|
585
|
+
*
|
|
586
|
+
* 与 `payloadSchema` 不同,**顶层不限于 `"type": "object"`**:结果可以是数组、
|
|
587
|
+
* 字符串、数字这类标量,用的是同一个编译器(同一份关键字白名单)。
|
|
588
|
+
*
|
|
589
|
+
* 声明它的价值在于形状漂移当场可见:命令结果的类型在插件与调用方两边各写一份,
|
|
590
|
+
* 只有出口校验能让"少了个字段"在运行期炸出来,而不是客户端拿到半个对象。
|
|
591
|
+
*/
|
|
592
|
+
readonly resultSchema?: PluginJsonSchema | undefined;
|
|
593
|
+
/** 仅内置插件:按名引用宿主注册表里的 contracts schema,替代 `resultSchema`。 */
|
|
594
|
+
readonly resultContract?: PluginContractName | undefined;
|
|
595
|
+
/** 返回值必须可 JSON 序列化;形状由插件与自己的 UI/调用方约定。 */
|
|
596
|
+
readonly handle: (payload: Record<string, unknown>, invocation: PluginCommandInvocation) => Promise<unknown> | unknown;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* `buffer.latest` 的上限。留得越多,慢订阅方压在宿主内存里的东西越多——这个数封的
|
|
600
|
+
* 正是那个。够用就行:宿主自己的高频流留 8 条。
|
|
601
|
+
*/
|
|
602
|
+
export declare const MAX_PLUGIN_STREAM_BUFFER = 1024;
|
|
603
|
+
/**
|
|
604
|
+
* 流式命令(`plugins.subscribe`)。emit 的每个值必须可 JSON 序列化;signal
|
|
605
|
+
* 中止表示订阅方挂断,handle 应尽快返回,之后的 emit 会被宿主静默丢弃。
|
|
606
|
+
*/
|
|
607
|
+
export interface PluginStreamCommand {
|
|
608
|
+
/** 与 `PluginCommand.name` 同一张表:重复即抛错,activate 失败。 */
|
|
609
|
+
readonly name: string;
|
|
610
|
+
readonly description?: string | undefined;
|
|
611
|
+
readonly scope?: "read" | "operate" | undefined;
|
|
612
|
+
/**
|
|
613
|
+
* 顶层必须是 `"type": "object"`;handle 收到的就是它校验过的形状。与
|
|
614
|
+
* `payloadContract` 二选一,缺一不可。
|
|
615
|
+
*/
|
|
616
|
+
readonly payloadSchema?: PluginJsonSchema | undefined;
|
|
617
|
+
/** 仅内置插件:按名引用宿主注册表里的 contracts schema,替代 `payloadSchema`。 */
|
|
618
|
+
readonly payloadContract?: PluginContractName | undefined;
|
|
619
|
+
/**
|
|
620
|
+
* 可选。声明了,宿主就在出口按它校验**每一个** emit 的值:出现不合格的值时这条流
|
|
621
|
+
* 立刻以 `invalid-result` 结束(此前已发出的值不撤回),并打一条带 pluginId /
|
|
622
|
+
* command 的 warn。不声明就不校验。
|
|
623
|
+
*
|
|
624
|
+
* 与 `resultSchema` 一样,**顶层不限于 `"type": "object"`** —— 只有 `payloadSchema`
|
|
625
|
+
* 那一处有这条限制。
|
|
626
|
+
*/
|
|
627
|
+
readonly itemSchema?: PluginJsonSchema | undefined;
|
|
628
|
+
/** 仅内置插件:按名引用宿主注册表里的 contracts schema,替代 `itemSchema`。 */
|
|
629
|
+
readonly itemContract?: PluginContractName | undefined;
|
|
630
|
+
/**
|
|
631
|
+
* 订阅方收得比你发得慢时,积压怎么办。
|
|
632
|
+
*
|
|
633
|
+
* **省略=全留**(今天的行为),适合「跑一次就完」的流——diff 分片、扫描进度:
|
|
634
|
+
* 丢中间一块等于结果是错的,宁可积压。这类流的总量由那一次操作封顶,涨不到天上去。
|
|
635
|
+
*
|
|
636
|
+
* **持续推状态的流必须写 `{ latest: N }`**:只留最近 N 条,慢订阅方丢掉的是**旧的**。
|
|
637
|
+
* 一秒一份的快照配「全留」,订阅方一慢内存就一直涨——宿主自己的高频流用的正是这个
|
|
638
|
+
* 策略(`PubSub.sliding(8)`)。N 的上限见 `MAX_PLUGIN_STREAM_BUFFER`。
|
|
639
|
+
*/
|
|
640
|
+
readonly buffer?: {
|
|
641
|
+
readonly latest: number;
|
|
642
|
+
} | undefined;
|
|
643
|
+
readonly handle: (payload: Record<string, unknown>, emit: (value: unknown) => void, signal: AbortSignal, invocation: PluginCommandInvocation) => Promise<void>;
|
|
644
|
+
}
|
|
645
|
+
export interface PluginSettings<Config> {
|
|
646
|
+
/**
|
|
647
|
+
* 读本插件在 home patch 层的那份 config;`password` 字段已解成明文,与 `ctx.config` 一致。
|
|
648
|
+
*
|
|
649
|
+
* home 层没写过这条 entry 时**回退到装配后的 `ctx.config`**,不是返回空对象——插件
|
|
650
|
+
* 两条读法看到的始终是它正在跑的那份配置。
|
|
651
|
+
*/
|
|
652
|
+
readonly read: () => Promise<Config>;
|
|
653
|
+
/**
|
|
654
|
+
* 整体替换本插件那条 config;写完宿主会重启这一个插件。
|
|
655
|
+
* 已经存进密文库的 `password` 字段不受影响:这里写什么都覆盖不掉它,改密码只
|
|
656
|
+
* 能走 settings card。
|
|
657
|
+
*/
|
|
658
|
+
readonly write: (config: Config) => Promise<void>;
|
|
659
|
+
}
|
|
660
|
+
export type PluginCommandReceipt = {
|
|
661
|
+
readonly status: "absent";
|
|
662
|
+
} | {
|
|
663
|
+
readonly status: "accepted";
|
|
664
|
+
readonly sequence: number;
|
|
665
|
+
} | {
|
|
666
|
+
readonly status: "rejected";
|
|
667
|
+
readonly error: string | null;
|
|
668
|
+
};
|
|
669
|
+
export interface PluginCommandDispatchResult {
|
|
670
|
+
/** accepted 只表示命令持久受理;provider 投递确认须查 turn intent。 */
|
|
671
|
+
readonly status: "accepted" | "noop";
|
|
672
|
+
readonly sequence: number;
|
|
673
|
+
readonly replayed: boolean;
|
|
674
|
+
}
|
|
675
|
+
export interface PluginNotifications {
|
|
676
|
+
/** 来源须有执行归属,只能通知同项目的来源会话或其祖先;正文走通信记录,唤醒走持久队列。 */
|
|
677
|
+
readonly send: (input: {
|
|
678
|
+
readonly threadId: string;
|
|
679
|
+
readonly sourceThreadId: string;
|
|
680
|
+
readonly summary: string;
|
|
681
|
+
/** 同一通知跨重启重试必须复用 generation;返回不表示 provider 已收到。 */
|
|
682
|
+
readonly generation?: string | undefined;
|
|
683
|
+
}) => Promise<PluginCommandDispatchResult & {
|
|
684
|
+
readonly recordId: string;
|
|
685
|
+
}>;
|
|
686
|
+
}
|
|
687
|
+
export interface PluginOperations {
|
|
688
|
+
/** 只约束本插件创建的会话及其委派后代;规则随插件停用撤销。 */
|
|
689
|
+
readonly guard: (guard: {
|
|
690
|
+
readonly operations: readonly "server.upgrade"[];
|
|
691
|
+
readonly check: (input: {
|
|
692
|
+
readonly operation: "server.upgrade";
|
|
693
|
+
readonly threadId: string;
|
|
694
|
+
}) => Promise<{
|
|
695
|
+
readonly allowed: true;
|
|
696
|
+
} | {
|
|
697
|
+
readonly allowed: false;
|
|
698
|
+
readonly detail: string;
|
|
699
|
+
}>;
|
|
700
|
+
}) => PluginDisposer;
|
|
701
|
+
}
|
|
702
|
+
export interface PluginCommands {
|
|
703
|
+
/**
|
|
704
|
+
* 为本插件创建的会话检查起轮准入,覆盖聊天、工具和后台入口。
|
|
705
|
+
* 检查期间只能读取状态,不能再派发命令;停用时自动撤销。
|
|
706
|
+
*/
|
|
707
|
+
readonly guard: (guard: {
|
|
708
|
+
readonly name: string;
|
|
709
|
+
readonly check: (input: {
|
|
710
|
+
readonly threadId: string;
|
|
711
|
+
readonly messageId: string;
|
|
712
|
+
readonly throughSequence: number;
|
|
713
|
+
}) => Promise<{
|
|
714
|
+
readonly allowed: true;
|
|
715
|
+
} | {
|
|
716
|
+
readonly allowed: false;
|
|
717
|
+
readonly detail: string;
|
|
718
|
+
}>;
|
|
719
|
+
}) => PluginDisposer;
|
|
720
|
+
/**
|
|
721
|
+
* 派发一条编排 command。形状不合法会 reject。
|
|
722
|
+
*
|
|
723
|
+
* **两道门**(路线图 #62):
|
|
724
|
+
*
|
|
725
|
+
* 1. **要声明。** manifest 的 `commands.dispatch` 权限逐条列出你要派的命令类型,
|
|
726
|
+
* 没声明或不在名单里当场 reject(reason `undeclared` / `not-declared`)。
|
|
727
|
+
* 能选的命令是宿主定的一张封闭名单——`project.delete`、`thread.delete`、
|
|
728
|
+
* `thread.message.assistant.complete` 这类不在里面,写进 manifest 解码就断。
|
|
729
|
+
* 2. **只碰自己开的会话。** 带 `threadId` 的那几条(`thread.turn.start` /
|
|
730
|
+
* `thread.turn.interrupt` / `thread.approval.respond` /
|
|
731
|
+
* `thread.external-messages.append`)只许落在你用 `thread.create` 或
|
|
732
|
+
* `thread.import` 开出来的会话上,别的一律 reject(reason `foreign-thread`)。
|
|
733
|
+
* 这条规矩不可声明——没有「任何会话」这个选项。**插件挂载所在的那条会话也不算
|
|
734
|
+
* 自己的**:那是人跟 agent 对话的地方,能在那儿替人按批准等于自己给自己开绿灯。
|
|
735
|
+
*
|
|
736
|
+
* `thread.create` 的执行归属由宿主持久保存,同一安装条目和数据范围在重新挂载后
|
|
737
|
+
* 仍可控制原会话。同项目的 continuation 沿来源链继承归属,显式新归属优先;普通
|
|
738
|
+
* 委派或分叉不继承。`thread.import` 的临时执行权限不跨挂载;已有历史跟读授权不会
|
|
739
|
+
* 因重启或重放导入回执而变成执行权限。停用后的旧实例不可继续派发命令。
|
|
740
|
+
*/
|
|
741
|
+
readonly dispatch: (command: unknown) => Promise<PluginCommandDispatchResult>;
|
|
742
|
+
/** 仅查询已声明的线程命令;创建/导入按安装身份映射 commandId,其余要求线程执行归属。 */
|
|
743
|
+
readonly getReceipt: (input: {
|
|
744
|
+
readonly commandId: string;
|
|
745
|
+
readonly commandType: string;
|
|
746
|
+
readonly threadId: string;
|
|
747
|
+
}) => Promise<PluginCommandReceipt>;
|
|
748
|
+
}
|
|
749
|
+
/** 卡上的一个按钮。`id` 是回答里回传的那个值,一张卡内唯一。 */
|
|
750
|
+
export interface PluginDecisionOption {
|
|
751
|
+
/** 小写字母开头,`[a-zA-Z0-9._-]`。 */
|
|
752
|
+
readonly id: string;
|
|
753
|
+
readonly label: string;
|
|
754
|
+
/** 只影响按钮的显示档位;`danger` 用于「这一下会拆掉东西」。 */
|
|
755
|
+
readonly tone?: "default" | "primary" | "danger";
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* 一张待决定的卡。内容封顶四样:一行标题、几行说明、一块可折叠的文本或代码、
|
|
759
|
+
* 若干按钮。塞不进任意组件——渲染只有一份,web / mobile 一致。
|
|
760
|
+
*/
|
|
761
|
+
export interface PluginDecisionRequest {
|
|
762
|
+
/** 卡挂在哪条会话上。决定卡只有会话级,没有「与任何会话无关」的问法。 */
|
|
763
|
+
readonly threadId: string;
|
|
764
|
+
readonly title: string;
|
|
765
|
+
/** 说明行;给字符串就是一行。最多 8 行,超出截断。 */
|
|
766
|
+
readonly body?: string | readonly string[];
|
|
767
|
+
/** 折叠体。`language` 给了就按代码渲染。 */
|
|
768
|
+
readonly detail?: {
|
|
769
|
+
readonly label?: string;
|
|
770
|
+
readonly text: string;
|
|
771
|
+
readonly language?: string;
|
|
772
|
+
};
|
|
773
|
+
/** 1 到 5 个按钮,缺一个卡就按不下去。 */
|
|
774
|
+
readonly options: readonly PluginDecisionOption[];
|
|
775
|
+
/**
|
|
776
|
+
* 幂等键。同一个值重复贴只出一张卡(宿主会在前面加上 `plugin:<你的 id>:`)。
|
|
777
|
+
* 省略就由宿主发一个。
|
|
778
|
+
*/
|
|
779
|
+
readonly requestId?: string;
|
|
780
|
+
}
|
|
781
|
+
export interface PluginDecisionAnswer {
|
|
782
|
+
readonly requestId: string;
|
|
783
|
+
readonly threadId: string;
|
|
784
|
+
/** 人按的那个按钮的 `id`。 */
|
|
785
|
+
readonly optionId: string;
|
|
786
|
+
readonly answeredAt: string;
|
|
787
|
+
}
|
|
788
|
+
export interface PluginDecisionHandle {
|
|
789
|
+
/** 宿主发的完整 id,`cancel` 与 `on` 里看到的就是它。 */
|
|
790
|
+
readonly requestId: string;
|
|
791
|
+
}
|
|
792
|
+
export type PluginDecisionHandler = (answer: PluginDecisionAnswer) => void | Promise<void>;
|
|
793
|
+
/**
|
|
794
|
+
* 问一个人。**卡上带你的插件 id,用户看得见是谁在问**——所以这里没有权限门。
|
|
795
|
+
*
|
|
796
|
+
* 两种等法:`ask` 贴完就走,答案经 `on` 回来;`askAndWait` 等到有人按,
|
|
797
|
+
* `timeoutMs` 到点自动把卡作废并 resolve 成 null。长活儿用前者:等待占着一条
|
|
798
|
+
* Promise,插件被停用时它会以 null 收场。
|
|
799
|
+
*/
|
|
800
|
+
export interface PluginDecisions {
|
|
801
|
+
/** 贴一张卡就走。返回宿主发的 requestId。 */
|
|
802
|
+
readonly ask: (request: PluginDecisionRequest) => Promise<PluginDecisionHandle>;
|
|
803
|
+
/**
|
|
804
|
+
* 贴一张卡并等回答。超时、被作废、插件停用都 resolve 成 null,不 reject——
|
|
805
|
+
* 「没人回答」是正常结局,不是错误。
|
|
806
|
+
*/
|
|
807
|
+
readonly askAndWait: (request: PluginDecisionRequest & {
|
|
808
|
+
readonly timeoutMs?: number;
|
|
809
|
+
}) => Promise<PluginDecisionAnswer | null>;
|
|
810
|
+
/** 收本插件贴的卡的回答。别的插件的卡收不到。 */
|
|
811
|
+
readonly on: (handler: PluginDecisionHandler) => PluginDisposer;
|
|
812
|
+
/** 收回一张还没人答的卡。已经答过的静默忽略。 */
|
|
813
|
+
readonly cancel: (requestId: string, summary?: string) => Promise<void>;
|
|
814
|
+
}
|
|
815
|
+
/** 这台 environment 的身份。用户看到的名字是「机器」。 */
|
|
816
|
+
export interface PluginEnvironment {
|
|
817
|
+
/** 服务器持久化的 environment id,不是主机名:两台机器可以同名。 */
|
|
818
|
+
readonly id: string;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* provider 实例当前报出来的一个模型。挑模型只要这三个字段,快照里其余的
|
|
822
|
+
* (能力位、上下文窗口、隐藏标记)是界面的事。
|
|
823
|
+
*/
|
|
824
|
+
export interface PluginProviderModel {
|
|
825
|
+
/** 发起 turn 时写进 modelSelection 的那个值。 */
|
|
826
|
+
readonly slug: string;
|
|
827
|
+
readonly name: string;
|
|
828
|
+
/** 该实例的默认模型。一个实例最多一个。 */
|
|
829
|
+
readonly isDefault: boolean;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* 一个 provider 实例的高层视图。宿主替插件解析各 driver 的 home 布局,
|
|
833
|
+
* 插件不抄这些细节。字段只增不改。
|
|
834
|
+
*/
|
|
835
|
+
export interface PluginProviderInstance {
|
|
836
|
+
readonly instanceId: string;
|
|
837
|
+
/** `claudeAgent`、`codex`、`cursor`…… 宿主的 driver 名。 */
|
|
838
|
+
readonly driver: string;
|
|
839
|
+
/**
|
|
840
|
+
* 这个实例是 provider 目录里的哪一行。一等 provider 等于 `driver`;ACP 厂商
|
|
841
|
+
* (`kimiCode`、`geminiCli`…)共用 `acp` driver,按 driver 分辨不出是哪家,
|
|
842
|
+
* 要认厂商就看这个。
|
|
843
|
+
*/
|
|
844
|
+
readonly providerId: string;
|
|
845
|
+
readonly displayName: string | null;
|
|
846
|
+
readonly enabled: boolean;
|
|
847
|
+
/** 只有解析得出 home 布局的 driver 有值(当前是 claude / codex),其余是 null。 */
|
|
848
|
+
readonly homePath: string | null;
|
|
849
|
+
/** provider CLI 自己的会话转录目录;解析不出来是 null。 */
|
|
850
|
+
readonly transcriptDir: string | null;
|
|
851
|
+
/**
|
|
852
|
+
* provider CLI 读自己配置文件的那个目录:claude 的 `settings.json`、codex 的
|
|
853
|
+
* `config.toml` 都在这里。解析不出来是 null。
|
|
854
|
+
*
|
|
855
|
+
* 判断「这个实例连的到底是不是官方服务」要读这些文件,路径只有宿主算得准
|
|
856
|
+
* (`CLAUDE_CONFIG_DIR` 的优先级、codex 的 auth overlay 布局)。读它本身仍然要
|
|
857
|
+
* `fs.read` 声明覆盖到这个目录。
|
|
858
|
+
*/
|
|
859
|
+
readonly configDirPath: string | null;
|
|
860
|
+
/**
|
|
861
|
+
* 这个实例 spawn 出来的 provider CLI 真正会看到的环境:宿主进程环境叠加实例自己
|
|
862
|
+
* 配的环境变量表。
|
|
863
|
+
*
|
|
864
|
+
* 给「按环境变量判断实例被改道了没有」这类判断用。它不是新的权限面 —— 插件与
|
|
865
|
+
* 服务器同进程,`node:process` 一样读得到 —— 这里给的是**叠加之后**的那份,
|
|
866
|
+
* 实例级覆盖只有宿主知道。
|
|
867
|
+
*/
|
|
868
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
869
|
+
/**
|
|
870
|
+
* 只有用户为这个实例填的那些变量,不含宿主进程环境。
|
|
871
|
+
*
|
|
872
|
+
* 判断「用户为这个 provider 配了密钥吗」要读这一份:合并那份里,一台机器上
|
|
873
|
+
* export 过一次就人人有份,会把每个实例都判成配了密钥。宿主自己决定认证方式
|
|
874
|
+
* 时看的也是这一份,两处必须一致。
|
|
875
|
+
*/
|
|
876
|
+
readonly instanceEnvironment: Readonly<Record<string, string>>;
|
|
877
|
+
/**
|
|
878
|
+
* 这个实例的 **driver 专属设置原文**,宿主一个字不改地交过来。
|
|
879
|
+
*
|
|
880
|
+
* **形状随 driver 变,恕不另行通知。** 上面那些字段是宿主挑出来、并且承诺稳定的;
|
|
881
|
+
* 这一份不是承诺,是逃生口——它存在的理由是**不必为每个新需求往上面那张表里再加一个
|
|
882
|
+
* 字段**。加一个字段就是对所有第三方插件的硬承诺,加进去就删不掉;这里给一袋值,
|
|
883
|
+
* 插件自己判断形状,宿主什么都不欠。
|
|
884
|
+
*
|
|
885
|
+
* 值是 `unknown` 而不是某个联合:codex 的 `homePath`、cursor 的 `apiEndpoint`、
|
|
886
|
+
* ollama 的 `endpoint`……各家自己定,宿主不认识它们。读之前必须自己收窄类型;
|
|
887
|
+
* 键不存在、类型不对、driver 换了写法,都要能不炸地退回默认行为。
|
|
888
|
+
*
|
|
889
|
+
* 实例根本没配 driver 设置时是空对象,不是 undefined —— 少一层判空。
|
|
890
|
+
*/
|
|
891
|
+
readonly driverConfig: Readonly<Record<string, unknown>>;
|
|
892
|
+
/**
|
|
893
|
+
* 实例配置里的启动参数原文;没有就是空串。
|
|
894
|
+
*
|
|
895
|
+
* 与 `apiEndpoint` 一样,是 Usage 搬迁时从 `driverConfig` 里挑出来单开的字段。留着是
|
|
896
|
+
* 因为拿掉会弄坏既有插件,不是因为它们比别的键更重要。**新需求走 `driverConfig`**,
|
|
897
|
+
* 别再往这张表上加 driver 专属字段。
|
|
898
|
+
*/
|
|
899
|
+
readonly launchArgs: string;
|
|
900
|
+
/** 实例配置里的自定义 API endpoint(cursor 的 `-e`);没配是 null。见 `launchArgs`。 */
|
|
901
|
+
readonly apiEndpoint: string | null;
|
|
902
|
+
/**
|
|
903
|
+
* 这个实例当前可选的模型,来自与客户端共用的 provider 模型目录,包含已恢复的缓存。
|
|
904
|
+
*
|
|
905
|
+
* **缺席不等于没有模型**:它表示这个实例在注册表里还没加载出来(driver 没注册、
|
|
906
|
+
* 配置解不动、快照还没建好)。要给一条新 thread 挑模型的插件必须把缺席当作
|
|
907
|
+
* 「这个实例现在起不了 turn」处理,而不是退到某个猜的 slug。
|
|
908
|
+
*/
|
|
909
|
+
readonly models?: readonly PluginProviderModel[] | undefined;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* provider 会话目录里的一条绑定:哪条 thread 挂在哪个 provider 实例上,以及
|
|
913
|
+
* 重开原生会话要用的游标。
|
|
914
|
+
*/
|
|
915
|
+
export interface PluginProviderSessionBinding {
|
|
916
|
+
readonly threadId: string;
|
|
917
|
+
/** 绑定写下的 driver kind(`claudeAgent`、`codex`……),不是实例 id。 */
|
|
918
|
+
readonly driver: string;
|
|
919
|
+
/** 绑定所属的 provider 实例;老库里的行可能没有,缺就是 null。 */
|
|
920
|
+
readonly instanceId: string | null;
|
|
921
|
+
/**
|
|
922
|
+
* 各 driver 自己的形状(claude 是 `{ resume }`,codex 是 `{ threadId }`),宿主
|
|
923
|
+
* 不替插件解析 —— 原生会话 id 藏在哪个字段是 driver 的事。没有游标是 null。
|
|
924
|
+
*/
|
|
925
|
+
readonly resumeCursor: unknown;
|
|
926
|
+
/** 绑定记下的会话运行状态;读不出来是 null。 */
|
|
927
|
+
readonly status: string | null;
|
|
928
|
+
/** 这条绑定最后一次被写的时间(ISO)。 */
|
|
929
|
+
readonly lastSeenAt: string;
|
|
930
|
+
}
|
|
931
|
+
export interface PluginProviderResumeCursorInput {
|
|
932
|
+
readonly instanceId: string;
|
|
933
|
+
readonly threadId: string;
|
|
934
|
+
/** 写进绑定的游标原文,形状由 driver 定。 */
|
|
935
|
+
readonly cursor: unknown;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* provider 会话绑定。`list` 只读;`setResumeCursor` 是插件面上**唯一的写口**,
|
|
939
|
+
* 边界刻意收得很窄:
|
|
940
|
+
*
|
|
941
|
+
* - 只能写 `resumeCursor` 一个字段。driver kind 由宿主按 `instanceId` 解析,
|
|
942
|
+
* 状态、runtimeMode、runtimePayload 都归编排,插件碰不到。
|
|
943
|
+
* - 目标 thread 必须存在;`instanceId` 必须是这台机器上配着的 provider 实例。
|
|
944
|
+
* - thread 已经绑在**另一个**实例上时拒绝 —— 把别人的会话改道过来是数据损坏,
|
|
945
|
+
* 不是配置。要换实例走编排命令,不走这里。
|
|
946
|
+
*
|
|
947
|
+
* 拒绝时宿主留一条带 pluginId 的 warning。
|
|
948
|
+
*/
|
|
949
|
+
export interface PluginProviderSessions {
|
|
950
|
+
readonly list: () => Promise<readonly PluginProviderSessionBinding[]>;
|
|
951
|
+
readonly setResumeCursor: (input: PluginProviderResumeCursorInput) => Promise<void>;
|
|
952
|
+
/**
|
|
953
|
+
* 停掉一条 thread 的 provider 会话进程。第二个写口,边界同样收得很窄:
|
|
954
|
+
*
|
|
955
|
+
* - 只停,不启、不重启、不发消息。重开是下一条用户消息的事,由编排决定。
|
|
956
|
+
* - thread 必须存在,且此刻确实挂着一个**活着**的会话(`running` / `starting`)。
|
|
957
|
+
* 空闲、已停、压根没建过会话的一律拒绝——"停一个没在跑的东西"是插件算错了状态,
|
|
958
|
+
* 静默成功会把那个错误一直藏着。
|
|
959
|
+
* - 会话正在跑一个 turn 时照停:这正是它存在的理由(原生记录被别处写了,常驻进程
|
|
960
|
+
* 拿的是过期的上下文)。
|
|
961
|
+
*
|
|
962
|
+
* 拿不到"这条 thread 是不是本插件建的"这个事实(`ThreadOrigin` 不区分导入),所以
|
|
963
|
+
* 归属判不了;能查证的三条上面写全了。拒绝时宿主留一条带 pluginId 的 warning。
|
|
964
|
+
*/
|
|
965
|
+
readonly stop: (threadId: string) => Promise<void>;
|
|
966
|
+
}
|
|
967
|
+
export interface PluginProviders {
|
|
968
|
+
readonly listInstances: () => Promise<readonly PluginProviderInstance[]>;
|
|
969
|
+
readonly getRouting: () => Promise<{
|
|
970
|
+
readonly allowlistOnly: boolean;
|
|
971
|
+
readonly roles: Readonly<Record<string, {
|
|
972
|
+
readonly instanceId: string;
|
|
973
|
+
readonly model: string;
|
|
974
|
+
}>>;
|
|
975
|
+
}>;
|
|
976
|
+
/** 按当前路由、可用实例和模型管理解析选择;读取失败或目标不可用时拒绝。 */
|
|
977
|
+
readonly resolveSelection: (input: {
|
|
978
|
+
readonly base: PluginModelSelection | null;
|
|
979
|
+
readonly instanceId?: string;
|
|
980
|
+
readonly model?: string;
|
|
981
|
+
readonly options?: PluginModelSelection["options"];
|
|
982
|
+
readonly routingKey?: string;
|
|
983
|
+
}) => Promise<{
|
|
984
|
+
readonly modelSelection: PluginModelSelection;
|
|
985
|
+
readonly executionSelection?: PluginExecutionSelection;
|
|
986
|
+
}>;
|
|
987
|
+
readonly sessions: PluginProviderSessions;
|
|
988
|
+
/**
|
|
989
|
+
* 按这个 provider 实例自己配置的代理出网的 fetch —— 不是 `ctx.http` 那份全局
|
|
990
|
+
* 默认。它连的就是这个实例的 provider 官方 API(额度探针、健康检查……),
|
|
991
|
+
* provider CLI 能连通而这类查询连不通是自相矛盾的。
|
|
992
|
+
*
|
|
993
|
+
* proxyId 在**调用时**解析(跟着当前设置走,实例是动态枚举的)。实例没配代理时
|
|
994
|
+
* 等同全局默认;实例指着一个已删代理时这个**悬空 proxyId 静默退化成直连**(与
|
|
995
|
+
* `ctx.http` 同一条规则),不报错。`instanceId` 不存在时 reject,不会悄悄退化成
|
|
996
|
+
* 全局默认 —— 那会把「这个实例其实没配」的配置错误伪装成正常工作。
|
|
997
|
+
*/
|
|
998
|
+
readonly fetchAs: (instanceId: string) => Promise<typeof globalThis.fetch>;
|
|
999
|
+
}
|
|
1000
|
+
export interface PluginWorkspace {
|
|
1001
|
+
/** 为本插件创建的独立工作树会话运行项目已配置的 setup 脚本,沿用宿主终端。 */
|
|
1002
|
+
readonly setupThread: (threadId: string) => Promise<{
|
|
1003
|
+
readonly status: "no-script";
|
|
1004
|
+
} | {
|
|
1005
|
+
readonly status: "started";
|
|
1006
|
+
readonly scriptId: string;
|
|
1007
|
+
readonly scriptName: string;
|
|
1008
|
+
readonly terminalId: string;
|
|
1009
|
+
readonly cwd: string;
|
|
1010
|
+
}>;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* 这台机器上配着的一个代理。**不含密码**:宿主自己往连接上加 `Proxy-Authorization`,
|
|
1014
|
+
* 插件不需要、也拿不到那份凭据。
|
|
1015
|
+
*/
|
|
1016
|
+
export interface PluginProxy {
|
|
1017
|
+
readonly id: string;
|
|
1018
|
+
readonly displayName?: string | undefined;
|
|
1019
|
+
readonly protocol: "http" | "https";
|
|
1020
|
+
readonly host: string;
|
|
1021
|
+
readonly port: number;
|
|
1022
|
+
/** 不经这个代理直连的地址,逗号分隔。本机地址是硬约束,不在这里出现。 */
|
|
1023
|
+
readonly bypass?: string | undefined;
|
|
1024
|
+
/** 这一个就是机器的全局默认。整张表都没有 true 时,默认即直连。 */
|
|
1025
|
+
readonly isDefault: boolean;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* 选哪个代理。三态,与设置里每条功能的代理选择完全同义:
|
|
1029
|
+
*
|
|
1030
|
+
* - **省略 / `undefined`** —— 跟这台机器的全局默认走。
|
|
1031
|
+
* - **`null`** —— 不加 Ycodium 配的代理。注意它只停 Ycodium 这一份:机器环境里的
|
|
1032
|
+
* `HTTP_PROXY` / `HTTPS_PROXY` 照旧生效,宿主不去剥它们。
|
|
1033
|
+
* - **代理 id** —— 用那一个。
|
|
1034
|
+
*
|
|
1035
|
+
* 指向一个不存在的 id(代理被删了、或者字符串压根不合法)**静默退化成直连**,与
|
|
1036
|
+
* `ctx.http`、`ctx.providers.fetchAs` 同一条规则:手改配置文件不该把长连接搞挂。
|
|
1037
|
+
* 要确认某个 id 现在还在,先 `list()`。
|
|
1038
|
+
*/
|
|
1039
|
+
export type PluginProxySelection = string | null;
|
|
1040
|
+
/**
|
|
1041
|
+
* 按代理出网。列出这台机器配了什么,然后挑一个出网 —— IM 桥那种「每个实例各配
|
|
1042
|
+
* 各的代理」的功能靠它,不必给每个实例塞一份自己的 HTTP 客户端。
|
|
1043
|
+
*/
|
|
1044
|
+
export interface PluginProxies {
|
|
1045
|
+
/** 这台机器配着的代理,按 id 升序。一个都没配就是空数组,不是错误。 */
|
|
1046
|
+
readonly list: () => Promise<readonly PluginProxy[]>;
|
|
1047
|
+
/**
|
|
1048
|
+
* 按三态选择出网的 fetch。选择在**调用时**解析(跟着当前设置走),拿到的 fetch
|
|
1049
|
+
* 之后一直走那个代理 —— 所以设置改了要重新调一次,别把返回值缓存一辈子。
|
|
1050
|
+
*
|
|
1051
|
+
* `ctx.http` 等价于建插件时调过一次 `fetch()`(省略参数那一档):一样的解析规则、
|
|
1052
|
+
* 一样的连接池,只是解析发生在宿主建能力面的时刻。想让改过的代理设置立刻生效而
|
|
1053
|
+
* 不重启,就用这条重新要一个。
|
|
1054
|
+
*/
|
|
1055
|
+
readonly fetch: (proxy?: PluginProxySelection | undefined) => Promise<typeof globalThis.fetch>;
|
|
1056
|
+
}
|
|
1057
|
+
/** 投影出来的一条线程活动。payload 保持 unknown,由插件按 kind 收窄。 */
|
|
1058
|
+
export interface PluginActivity {
|
|
1059
|
+
readonly activityId: string;
|
|
1060
|
+
readonly threadId: string;
|
|
1061
|
+
readonly turnId: string | null;
|
|
1062
|
+
readonly kind: string;
|
|
1063
|
+
readonly tone?: string | undefined;
|
|
1064
|
+
readonly summary: string;
|
|
1065
|
+
readonly payload: unknown;
|
|
1066
|
+
readonly createdAt: string;
|
|
1067
|
+
}
|
|
1068
|
+
export interface PluginActivityQuery {
|
|
1069
|
+
readonly kind: string;
|
|
1070
|
+
/**
|
|
1071
|
+
* 闭区间,ISO 时间戳。省略 = 不设下界 / 上界取「现在」。
|
|
1072
|
+
*
|
|
1073
|
+
* 解析不出来的字符串**当场 reject**,不静默退回默认值。
|
|
1074
|
+
*/
|
|
1075
|
+
readonly sinceIso?: string | undefined;
|
|
1076
|
+
readonly untilIso?: string | undefined;
|
|
1077
|
+
}
|
|
1078
|
+
/** 在某个 provider 实例上选定的一个模型。发起 turn 的命令载荷用的就是这个形状。 */
|
|
1079
|
+
export interface PluginModelSelection {
|
|
1080
|
+
readonly instanceId: string;
|
|
1081
|
+
readonly model: string;
|
|
1082
|
+
readonly options?: readonly {
|
|
1083
|
+
readonly id: string;
|
|
1084
|
+
readonly value: string | boolean;
|
|
1085
|
+
}[];
|
|
1086
|
+
}
|
|
1087
|
+
export interface PluginExecutionSelection {
|
|
1088
|
+
readonly environmentId: string;
|
|
1089
|
+
readonly runtimeId: string;
|
|
1090
|
+
readonly sourceId: string;
|
|
1091
|
+
readonly profileId: string;
|
|
1092
|
+
readonly endpointId?: string;
|
|
1093
|
+
readonly optionOverrides: readonly {
|
|
1094
|
+
readonly id: string;
|
|
1095
|
+
readonly value: string | boolean;
|
|
1096
|
+
}[];
|
|
1097
|
+
readonly expectedRevisions: {
|
|
1098
|
+
readonly runtime: number;
|
|
1099
|
+
readonly source: number;
|
|
1100
|
+
readonly profile: number;
|
|
1101
|
+
readonly endpoint?: number;
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* 一个 project 背后的 git 仓库身份,由宿主按工作区根解析。
|
|
1106
|
+
*
|
|
1107
|
+
* `canonicalKey` 是跨机器可比的仓库标识,形状固定为 `host/owner/repo`
|
|
1108
|
+
* (如 `github.com/acme/widgets`),拿它做去重与分组,别自己拼 remoteUrl。
|
|
1109
|
+
* `provider` 只有三个值:`github` / `gitlab` / `unknown`;托管方认不出来时是
|
|
1110
|
+
* `unknown`,不是缺省缺席——插件按自己配置的域名认领 unknown 的那部分。
|
|
1111
|
+
*/
|
|
1112
|
+
export interface PluginRepositoryIdentity {
|
|
1113
|
+
readonly provider: "github" | "gitlab" | "unknown";
|
|
1114
|
+
/** `host/owner/repo`,跨机器可比。 */
|
|
1115
|
+
readonly canonicalKey: string;
|
|
1116
|
+
/** 解析用的那个 git remote 名,通常是 `origin`。 */
|
|
1117
|
+
readonly remoteName: string;
|
|
1118
|
+
readonly remoteUrl: string;
|
|
1119
|
+
readonly displayName?: string;
|
|
1120
|
+
readonly owner?: string;
|
|
1121
|
+
readonly name?: string;
|
|
1122
|
+
/** 仓库根目录,与 project 的 workspaceRoot 可能不同(工作区在子目录里)。 */
|
|
1123
|
+
readonly rootPath?: string;
|
|
1124
|
+
}
|
|
1125
|
+
/** 一个 project 的只读视图。工作区根路径与默认模型,够开一条新 thread。 */
|
|
1126
|
+
export interface PluginProject {
|
|
1127
|
+
readonly projectId: string;
|
|
1128
|
+
readonly title: string;
|
|
1129
|
+
/** 这个 project 的工作区根目录(绝对路径)。 */
|
|
1130
|
+
readonly workspaceRoot: string;
|
|
1131
|
+
/** 项目默认模型选择;没设是 null。 */
|
|
1132
|
+
readonly defaultModelSelection: PluginModelSelection | null;
|
|
1133
|
+
/** 工作区不是 git 仓库、或者没有可用 remote 时是 null。 */
|
|
1134
|
+
readonly repositoryIdentity: PluginRepositoryIdentity | null;
|
|
1135
|
+
}
|
|
1136
|
+
/** thread 里的一条消息。附件、逐条模型选择这类界面用的字段不透出。 */
|
|
1137
|
+
/**
|
|
1138
|
+
* 一份要落成会话附件的字节。
|
|
1139
|
+
*
|
|
1140
|
+
* 大小上限沿用宿主对聊天附件的那一份(按 mime 分档,图片与文件不同),超了就 reject
|
|
1141
|
+
* —— 插件不该有一条比人从网页上传更宽的路。
|
|
1142
|
+
*/
|
|
1143
|
+
export interface PluginAttachmentInput {
|
|
1144
|
+
/** 附件挂在哪条会话下。落盘目录按它分,删会话时一起回收。 */
|
|
1145
|
+
readonly threadId: string;
|
|
1146
|
+
readonly name: string;
|
|
1147
|
+
readonly mimeType: string;
|
|
1148
|
+
readonly bytes: Uint8Array;
|
|
1149
|
+
}
|
|
1150
|
+
/** 落好盘的附件。原样塞进 `thread.turn.start` 的 `message.attachments` 即可。 */
|
|
1151
|
+
export interface PluginAttachment {
|
|
1152
|
+
/** mime 是 `image/*` 就是 `image`,其余都是 `file`。 */
|
|
1153
|
+
readonly type: "image" | "file";
|
|
1154
|
+
readonly id: string;
|
|
1155
|
+
readonly name: string;
|
|
1156
|
+
readonly mimeType: string;
|
|
1157
|
+
readonly sizeBytes: number;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* 把字节落成会话附件。插件面上唯一的字节写口。
|
|
1161
|
+
*
|
|
1162
|
+
* 为什么需要它:`ctx.commands.dispatch` 收的是**已经规范化**的命令,附件那一格只认
|
|
1163
|
+
* 落好盘的记录(`{id, name, mimeType, sizeBytes}`),不认 data URL —— 网页客户端那条
|
|
1164
|
+
* 路由宿主的规范化器代劳,插件得自己先落一次。
|
|
1165
|
+
*/
|
|
1166
|
+
export interface PluginAttachments {
|
|
1167
|
+
readonly store: (input: PluginAttachmentInput) => Promise<PluginAttachment>;
|
|
1168
|
+
/**
|
|
1169
|
+
* 按 id 读回字节。附件不在这台机器上就 reject,不回空字节 —— 「没有」与「是空的」
|
|
1170
|
+
* 是两回事。
|
|
1171
|
+
*
|
|
1172
|
+
* 不需要声明:这些 id 本来就写在 `getThread` / `getThreadMessage` 读得到的消息上,
|
|
1173
|
+
* 这条只是把「看得见记录」补成「读得到内容」。
|
|
1174
|
+
*/
|
|
1175
|
+
readonly read: (attachmentId: string) => Promise<Uint8Array>;
|
|
1176
|
+
}
|
|
1177
|
+
export interface PluginThreadMessage {
|
|
1178
|
+
readonly messageId: string;
|
|
1179
|
+
readonly role: string;
|
|
1180
|
+
readonly text: string;
|
|
1181
|
+
readonly turnId: string | null;
|
|
1182
|
+
readonly createdAt: string;
|
|
1183
|
+
readonly modelSelection?: PluginModelSelection | undefined;
|
|
1184
|
+
readonly source?: {
|
|
1185
|
+
readonly kind: "native-session";
|
|
1186
|
+
readonly provider: "claude" | "codex";
|
|
1187
|
+
readonly sessionId: string;
|
|
1188
|
+
readonly messageId: string;
|
|
1189
|
+
readonly turnId?: string | undefined;
|
|
1190
|
+
readonly order: number;
|
|
1191
|
+
} | undefined;
|
|
1192
|
+
/** 这条消息带的附件;要字节走 `ctx.attachments.read(id)`。 */
|
|
1193
|
+
readonly attachments: readonly PluginAttachment[];
|
|
1194
|
+
}
|
|
1195
|
+
/** thread 当前的 provider 会话状态。没建过会话是 null。 */
|
|
1196
|
+
export interface PluginThreadSession {
|
|
1197
|
+
/** `idle` / `starting` / `running` / `stopped`…… 宿主的会话状态名。 */
|
|
1198
|
+
readonly status: string;
|
|
1199
|
+
readonly providerInstanceId: string | null;
|
|
1200
|
+
/** 正在跑的 turn;空闲是 null。 */
|
|
1201
|
+
readonly activeTurnId: string | null;
|
|
1202
|
+
readonly lastError?: string | null | undefined;
|
|
1203
|
+
readonly updatedAt?: string | undefined;
|
|
1204
|
+
readonly endReason?: "server-restart" | undefined;
|
|
1205
|
+
}
|
|
1206
|
+
export interface PluginThreadOrigin {
|
|
1207
|
+
readonly parentThreadId: string;
|
|
1208
|
+
readonly delegationDepth: number;
|
|
1209
|
+
readonly requestedBy: "agent" | "continuation";
|
|
1210
|
+
readonly notificationPolicy?: "wake" | "activity-only" | undefined;
|
|
1211
|
+
}
|
|
1212
|
+
export interface PluginThreadForkPoint {
|
|
1213
|
+
readonly sourceThreadId: string;
|
|
1214
|
+
readonly sourceMessageId: string;
|
|
1215
|
+
}
|
|
1216
|
+
export interface PluginTurnIntent {
|
|
1217
|
+
readonly intentId: string;
|
|
1218
|
+
readonly threadId: string;
|
|
1219
|
+
readonly sequence: number;
|
|
1220
|
+
readonly commandId: string;
|
|
1221
|
+
readonly messageId: string;
|
|
1222
|
+
readonly revision: number;
|
|
1223
|
+
readonly turn: {
|
|
1224
|
+
readonly turnId: string | null;
|
|
1225
|
+
readonly userMessageId: string;
|
|
1226
|
+
readonly state: "pending" | "running" | "interrupted" | "completed" | "error";
|
|
1227
|
+
readonly requestedAt: string;
|
|
1228
|
+
readonly startedAt: string | null;
|
|
1229
|
+
readonly completedAt: string | null;
|
|
1230
|
+
readonly waitSpans: readonly {
|
|
1231
|
+
readonly kind: "approval" | "userInput";
|
|
1232
|
+
readonly requestId: string;
|
|
1233
|
+
readonly startedAt: string;
|
|
1234
|
+
readonly completedAt: string | null;
|
|
1235
|
+
}[];
|
|
1236
|
+
} | null;
|
|
1237
|
+
readonly status: "queued" | "preparing" | "dispatching" | "running" | "completed" | "interrupted" | "failed" | "reconciling" | "blocked" | "cancelled" | "abandoned";
|
|
1238
|
+
readonly deliveryCertainty: "not-dispatched" | "confirmed" | "unknown";
|
|
1239
|
+
readonly attempt: {
|
|
1240
|
+
readonly attemptId: string;
|
|
1241
|
+
readonly workerEpoch: number;
|
|
1242
|
+
readonly sourceBindingId: string | null;
|
|
1243
|
+
readonly targetBindingId: string | null;
|
|
1244
|
+
readonly stage: "preparing" | "dispatching" | "running" | "reconciling";
|
|
1245
|
+
readonly deliveryCertainty: "not-dispatched" | "confirmed" | "unknown";
|
|
1246
|
+
readonly nativeLocator: {
|
|
1247
|
+
readonly sessionId?: string | undefined;
|
|
1248
|
+
readonly turnId?: string | undefined;
|
|
1249
|
+
readonly generationId?: string | undefined;
|
|
1250
|
+
} | null;
|
|
1251
|
+
readonly createdAt: string;
|
|
1252
|
+
readonly updatedAt: string;
|
|
1253
|
+
} | null;
|
|
1254
|
+
readonly blockedReason: string | null;
|
|
1255
|
+
readonly replacementIntentId: string | null;
|
|
1256
|
+
readonly createdAt: string;
|
|
1257
|
+
readonly updatedAt: string;
|
|
1258
|
+
}
|
|
1259
|
+
/** 不加载消息历史;持久状态在同一数据库快照内读取,进程存活另取当前 provider 句柄。 */
|
|
1260
|
+
export interface PluginThreadExecution {
|
|
1261
|
+
readonly threadId: string;
|
|
1262
|
+
readonly projectId: string;
|
|
1263
|
+
readonly title: string;
|
|
1264
|
+
readonly modelSelection: PluginModelSelection;
|
|
1265
|
+
readonly executionSelection?: PluginExecutionSelection | undefined;
|
|
1266
|
+
readonly runtimeMode: "approval-required" | "auto-accept-edits" | "auto" | "full-access";
|
|
1267
|
+
readonly interactionMode: "default" | "plan";
|
|
1268
|
+
readonly branch: string | null;
|
|
1269
|
+
readonly worktreePath: string | null;
|
|
1270
|
+
readonly origin: PluginThreadOrigin | null;
|
|
1271
|
+
readonly forkedFrom: PluginThreadForkPoint | null;
|
|
1272
|
+
readonly archivedAt: string | null;
|
|
1273
|
+
readonly createdAt: string;
|
|
1274
|
+
readonly updatedAt: string;
|
|
1275
|
+
readonly session: (PluginThreadSession & {
|
|
1276
|
+
readonly lastError: string | null;
|
|
1277
|
+
readonly updatedAt: string;
|
|
1278
|
+
}) | null;
|
|
1279
|
+
readonly latestTurn: {
|
|
1280
|
+
readonly turnId: string;
|
|
1281
|
+
readonly state: "running" | "interrupted" | "completed" | "error";
|
|
1282
|
+
readonly requestedAt: string;
|
|
1283
|
+
readonly startedAt: string | null;
|
|
1284
|
+
readonly completedAt: string | null;
|
|
1285
|
+
readonly userMessageId?: string | undefined;
|
|
1286
|
+
readonly assistantMessageId: string | null;
|
|
1287
|
+
readonly waitSpans?: readonly {
|
|
1288
|
+
readonly kind: "approval" | "userInput";
|
|
1289
|
+
readonly requestId: string;
|
|
1290
|
+
readonly startedAt: string;
|
|
1291
|
+
readonly completedAt: string | null;
|
|
1292
|
+
}[] | undefined;
|
|
1293
|
+
} | null;
|
|
1294
|
+
readonly turnIntents: readonly PluginTurnIntent[];
|
|
1295
|
+
readonly queuePaused: boolean;
|
|
1296
|
+
readonly providerProcessLive: boolean;
|
|
1297
|
+
readonly hasPendingApprovals: boolean;
|
|
1298
|
+
readonly hasPendingUserInput: boolean;
|
|
1299
|
+
readonly pendingApprovalCount: number;
|
|
1300
|
+
readonly pendingUserInputCount: number;
|
|
1301
|
+
/** 持久 turn-start-requested 事实总数,含普通客户端入口;与导入用户消息数不同。 */
|
|
1302
|
+
readonly turnStartCount: number;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* 一条 thread 的只读视图:消息列表加会话状态。
|
|
1306
|
+
*
|
|
1307
|
+
* 默认不带活动、checkpoint、提案计划;原生历史对齐可显式请求有界活动窗口,
|
|
1308
|
+
* 跨会话按 kind 读活动继续走 `listActivitiesByKind`。
|
|
1309
|
+
*/
|
|
1310
|
+
export interface PluginThread {
|
|
1311
|
+
readonly threadId: string;
|
|
1312
|
+
readonly projectId: string;
|
|
1313
|
+
readonly title: string;
|
|
1314
|
+
readonly modelSelection: PluginModelSelection;
|
|
1315
|
+
readonly origin?: PluginThreadOrigin | null | undefined;
|
|
1316
|
+
readonly forkedFrom?: PluginThreadForkPoint | null | undefined;
|
|
1317
|
+
/** 归档过的 thread 有值;活着的是 null。 */
|
|
1318
|
+
readonly archivedAt: string | null;
|
|
1319
|
+
readonly messages: readonly PluginThreadMessage[];
|
|
1320
|
+
/** 仅显式请求时带回当前有界窗口的活动,供原生历史对齐;不含检查点或提案。 */
|
|
1321
|
+
readonly activities?: readonly PluginActivity[] | undefined;
|
|
1322
|
+
readonly session: PluginThreadSession | null;
|
|
1323
|
+
readonly updatedAt: string;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* 只读投影查询。写投影是编排的事,插件碰不到。
|
|
1327
|
+
*
|
|
1328
|
+
* `getProject` / `listProjects` / `getThread` 三个走投影读模型:这台机器没装配读
|
|
1329
|
+
* 模型时它们**全部 reject**(不是返回空),因为"查不到"与"这台机器答不了"是两回事。
|
|
1330
|
+
* `listActivitiesByKind` 走的是活动仓库,不吃这条。
|
|
1331
|
+
*/
|
|
1332
|
+
export interface PluginApprovalOption {
|
|
1333
|
+
/** `accept` / `acceptForSession` / `acceptAlways` / `decline` / `cancel`。 */
|
|
1334
|
+
readonly decision: string;
|
|
1335
|
+
readonly label: string;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* 一条还没人应答的审批请求。
|
|
1339
|
+
*
|
|
1340
|
+
* 应答走 `ctx.commands.dispatch({ type: "thread.approval.respond", threadId, requestId,
|
|
1341
|
+
* decision, ... })` —— 那条命令一直是可派发的,缺的一直是这份清单(触发事件
|
|
1342
|
+
* `thread.approval-pending` 只带 threadId 与一句摘要,没有 requestId,也就答不出来)。
|
|
1343
|
+
*
|
|
1344
|
+
* **只应答你自己开的会话。** 插件开一轮时本来就能选满权限让 agent 压根不问,所以对
|
|
1345
|
+
* 自己开的会话按批准键不多给什么;别人在网页上开的会话不是你该替他按的(见路线图 #62)。
|
|
1346
|
+
*/
|
|
1347
|
+
export interface PluginApproval {
|
|
1348
|
+
readonly requestId: string;
|
|
1349
|
+
/** 请求的类别,比如 `exec` / `edit`;由 provider 定,宿主不收窄。 */
|
|
1350
|
+
readonly requestKind: string;
|
|
1351
|
+
/** 要批的那件事,给人读的一句话。 */
|
|
1352
|
+
readonly detail: string | null;
|
|
1353
|
+
/** 发起的应用名,provider 报了才有。 */
|
|
1354
|
+
readonly appName: string | null;
|
|
1355
|
+
/** provider 给的选项;没给就自己按 accept / decline 两档处理。 */
|
|
1356
|
+
readonly options: readonly PluginApprovalOption[];
|
|
1357
|
+
readonly createdAt: string;
|
|
1358
|
+
}
|
|
1359
|
+
export interface PluginProjections {
|
|
1360
|
+
readonly listActivitiesByKind: (query: PluginActivityQuery) => Promise<readonly PluginActivity[]>;
|
|
1361
|
+
/** 没有这个 project(或已删)返回 null,不抛错。 */
|
|
1362
|
+
readonly getProject: (projectId: string) => Promise<PluginProject | null>;
|
|
1363
|
+
/**
|
|
1364
|
+
* 这台 environment 上所有未删除的 project。已按客户端树里的静态顺序返回。
|
|
1365
|
+
*
|
|
1366
|
+
* 每条都带 `repositoryIdentity`,所以按仓库分组不必再逐个 `getProject`。
|
|
1367
|
+
*/
|
|
1368
|
+
readonly listProjects: () => Promise<readonly PluginProject[]>;
|
|
1369
|
+
/** 没有这条 thread(或已删)返回 null,不抛错。已归档的照样返回。 */
|
|
1370
|
+
readonly getThread: (threadId: string, options?: {
|
|
1371
|
+
readonly includeActivities?: boolean | undefined;
|
|
1372
|
+
}) => Promise<PluginThread | null>;
|
|
1373
|
+
readonly getThreadExecution: (threadId: string) => Promise<PluginThreadExecution | null>;
|
|
1374
|
+
/** 持久起轮事实计数包含已删除会话,用于跨接班和清理的执行预算。 */
|
|
1375
|
+
readonly countThreadTurnStarts: (threadIds: readonly string[]) => Promise<number>;
|
|
1376
|
+
/**
|
|
1377
|
+
* 按 messageId 点读一条消息。没有这条消息返回 null,不抛错。
|
|
1378
|
+
*
|
|
1379
|
+
* 用它而不是 `getThread(...).messages.find(...)`:`getThread` 把整条会话的消息
|
|
1380
|
+
* 全解出来,一条长会话就是上百条、几十上百 KB。收到 `thread.message-sent` 想读
|
|
1381
|
+
* 正文(聊天桥、通知渠道都是这个形状)就走这条。
|
|
1382
|
+
*
|
|
1383
|
+
* **正文要从这里读,不要从事件 payload 里读。** 流式回复的最后一个事件
|
|
1384
|
+
* (`streaming: false`,也就是「这条说完了」那个)`text` 是**空的**,文本在它
|
|
1385
|
+
* 之前那些 `streaming: true` 的事件里。事件只告诉你「哪条消息定稿了」,正文以
|
|
1386
|
+
* 投影为准。
|
|
1387
|
+
*/
|
|
1388
|
+
readonly getThreadMessage: (messageId: string) => Promise<PluginThreadMessage | null>;
|
|
1389
|
+
/**
|
|
1390
|
+
* 这台机器此刻的电源状况。非桌面环境全是 `"unknown"`。
|
|
1391
|
+
*
|
|
1392
|
+
* 数据源是桌面客户端经一条私有管道报上来的,插件读不到那条管道——所以它是宿主
|
|
1393
|
+
* 代读的一份快照,不是订阅。要跟着它变就自己轮询,别指望有推送。
|
|
1394
|
+
*/
|
|
1395
|
+
readonly getPower: () => Promise<PluginPowerSnapshot>;
|
|
1396
|
+
/**
|
|
1397
|
+
* 宿主自己的逻辑 I/O 记账。记的是宿主写了多少,不是内核的磁盘计数。
|
|
1398
|
+
*
|
|
1399
|
+
* 只有宿主知道自己在哪些地方写盘,所以这份账没法由插件自己攒。
|
|
1400
|
+
*/
|
|
1401
|
+
readonly getAttribution: () => Promise<PluginAttributionSnapshot>;
|
|
1402
|
+
/**
|
|
1403
|
+
* 这条会话上还没人应答的审批请求,按发起时间升序。没有就是空数组。
|
|
1404
|
+
*
|
|
1405
|
+
* 收到 `thread.approval-pending` 触发事件后用它取回 requestId 与选项;那条事件本身
|
|
1406
|
+
* 只带 threadId 与摘要。同一条请求在被应答之前会一直出现在这里。
|
|
1407
|
+
*/
|
|
1408
|
+
readonly listPendingApprovals: (threadId: string) => Promise<readonly PluginApproval[]>;
|
|
1409
|
+
}
|
|
1410
|
+
export interface PluginFileStat {
|
|
1411
|
+
readonly type: "file" | "directory" | "other";
|
|
1412
|
+
readonly size: number;
|
|
1413
|
+
/** 取不到修改时间就是 null。 */
|
|
1414
|
+
readonly modifiedAtIso: string | null;
|
|
1415
|
+
/**
|
|
1416
|
+
* 文件系统身份,`device:inode`;取不到是空串。
|
|
1417
|
+
*
|
|
1418
|
+
* 用来分辨「两台服务器读的是同一个目录」和「两台机器的主机名与路径恰好一样」。
|
|
1419
|
+
* 路径本身不够:同一个 fleet 里每台机器都 resolve 得出同一个 `~/.claude`。
|
|
1420
|
+
*/
|
|
1421
|
+
readonly volumeId: string;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* 只读文件访问。需要 manifest 里的 `fs.read` 声明,且目标路径必须落在声明的
|
|
1425
|
+
* 路径前缀内;没声明或越界的调用一律 reject。
|
|
1426
|
+
*
|
|
1427
|
+
* **前缀怎么比**:声明的前缀与请求的路径都先展开 `~`(用户 home)再 `path.resolve`
|
|
1428
|
+
* 成绝对路径,然后按**路径分隔符边界**比对——声明 `/a` 不放行 `/ab`,`..` 也climb
|
|
1429
|
+
* 不出去(resolve 时就被吃掉)。**不解析 symlink**:指向前缀外的链接照样读得到。
|
|
1430
|
+
* 这道门兑现的是安装时给人看的那份声明,不是沙箱——插件与服务器同进程,自己
|
|
1431
|
+
* `import "node:fs"` 一样读得到。
|
|
1432
|
+
*
|
|
1433
|
+
* 声明里的 `paths` 项也认 `config:<key>` 间接引用,与 `process.spawn` 的程序白名单
|
|
1434
|
+
* 同一套机制:见 `PluginProgramSelector`。
|
|
1435
|
+
*
|
|
1436
|
+
* 这个能力面的形状不随权限变化:没声明 `fs.read` 的插件照样看得见 `ctx.fs`,
|
|
1437
|
+
* 只是每次调用都被拒。
|
|
1438
|
+
*/
|
|
1439
|
+
/** 一条目录项。`type` 与 {@link PluginFileStat} 同一套取值。 */
|
|
1440
|
+
export interface PluginDirectoryEntry {
|
|
1441
|
+
readonly name: string;
|
|
1442
|
+
readonly type: "file" | "directory" | "other";
|
|
1443
|
+
}
|
|
1444
|
+
export interface PluginFileSystem {
|
|
1445
|
+
readonly readFile: (path: string) => Promise<string>;
|
|
1446
|
+
/**
|
|
1447
|
+
* 目录项,不递归。
|
|
1448
|
+
*
|
|
1449
|
+
* **带类型**:不带的话,「这是文件还是目录」只能对每一项再 `stat` 一次——一个装着
|
|
1450
|
+
* 一千多份转录的目录就是一千多次多余的系统调用,而底层 `readdir` 本来一次就能给出
|
|
1451
|
+
* 来(路线图 #23)。
|
|
1452
|
+
*/
|
|
1453
|
+
readonly readDir: (path: string) => Promise<readonly PluginDirectoryEntry[]>;
|
|
1454
|
+
readonly stat: (path: string) => Promise<PluginFileStat>;
|
|
1455
|
+
/**
|
|
1456
|
+
* 流式读一个文件。整份读进内存放不下、或者只想一行行扫过去的时候用它。
|
|
1457
|
+
*
|
|
1458
|
+
* 路径检查发生在第一次 `next()`,所以被拒的调用是在 `for await` 里抛出来的,
|
|
1459
|
+
* 不是在拿到这个可迭代对象的时候。
|
|
1460
|
+
*/
|
|
1461
|
+
readonly createReadStream: (path: string) => AsyncIterable<Uint8Array>;
|
|
1462
|
+
/**
|
|
1463
|
+
* 按行流式读一个文件(UTF-8,`\r\n` 与 `\n` 都算换行,末行没有换行符也照样吐)。
|
|
1464
|
+
*
|
|
1465
|
+
* 存在的理由是性能:几百 MB 的日志/转录一行行扫,比 `readFile` 全量materialise
|
|
1466
|
+
* 便宜一个数量级。走的是与 `readFile` 同一道路径前缀门。
|
|
1467
|
+
*/
|
|
1468
|
+
readonly readLines: (path: string) => AsyncIterable<string>;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* 三态。取值与宿主那份逐字一致(`BackgroundBooleanState`)——**是字符串不是布尔**,
|
|
1472
|
+
* 因为「不知道」必须与「否」分得开:非桌面环境读不到电源,那时不该说「没在用电池」。
|
|
1473
|
+
*/
|
|
1474
|
+
export type PluginPowerState = "true" | "false" | "unknown";
|
|
1475
|
+
/**
|
|
1476
|
+
* 这台机器此刻的电源状况。
|
|
1477
|
+
*
|
|
1478
|
+
* 数据源只有一个——桌面客户端经一条私有管道报上来的,所以**非桌面环境全是
|
|
1479
|
+
* `"unknown"`**,不是「没在用电池」。想按它调节奏的话,`unknown` 要当成「不知道」
|
|
1480
|
+
* 处理,而不是当成插着电。
|
|
1481
|
+
*/
|
|
1482
|
+
export interface PluginPowerSnapshot {
|
|
1483
|
+
/** 谁报的。没有桌面客户端时是 `"unknown"`。 */
|
|
1484
|
+
readonly source: string;
|
|
1485
|
+
readonly idle: PluginPowerState;
|
|
1486
|
+
/** 空闲了多少秒;取不到是 null。 */
|
|
1487
|
+
readonly idleSeconds: number | null;
|
|
1488
|
+
readonly locked: PluginPowerState;
|
|
1489
|
+
readonly suspended: boolean;
|
|
1490
|
+
readonly onBattery: PluginPowerState;
|
|
1491
|
+
readonly lowPowerMode: PluginPowerState;
|
|
1492
|
+
/** `"nominal"` / `"fair"` / `"serious"` / `"critical"` / `"unknown"`。 */
|
|
1493
|
+
readonly thermalState: string;
|
|
1494
|
+
/** 桌面端断了之后这一份就过期了:值还在,但别再据它做决定。 */
|
|
1495
|
+
readonly stale: boolean;
|
|
1496
|
+
readonly updatedAtIso: string;
|
|
1497
|
+
}
|
|
1498
|
+
/** 一条逻辑 I/O 记账:某个部件做某件事,累计读写了多少。 */
|
|
1499
|
+
export interface PluginAttributionEntry {
|
|
1500
|
+
readonly component: string;
|
|
1501
|
+
readonly operation: string;
|
|
1502
|
+
readonly logicalReadBytes: number;
|
|
1503
|
+
readonly logicalWriteBytes: number;
|
|
1504
|
+
readonly count: number;
|
|
1505
|
+
readonly durationMs: number;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* 宿主自己的逻辑 I/O 记账快照。
|
|
1509
|
+
*
|
|
1510
|
+
* 记的是**宿主写了多少**(provider 事件日志、trace sink 这些),不是操作系统层面的
|
|
1511
|
+
* 磁盘计数——两者对不上是正常的,一个是应用视角一个是内核视角。
|
|
1512
|
+
*/
|
|
1513
|
+
export interface PluginAttributionSnapshot {
|
|
1514
|
+
readonly readAtIso: string;
|
|
1515
|
+
readonly entries: readonly PluginAttributionEntry[];
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* 一份 provider 凭据的原文。宿主不替插件解析语义 —— 各家格式自己认。
|
|
1519
|
+
*/
|
|
1520
|
+
export interface PluginProviderCredentials {
|
|
1521
|
+
readonly instanceId: string;
|
|
1522
|
+
readonly driver: string;
|
|
1523
|
+
/** 凭据来自磁盘文件还是 macOS 登录钥匙串。 */
|
|
1524
|
+
readonly source: "file" | "keychain";
|
|
1525
|
+
/** 文件路径,或 `keychain:<service>`。 */
|
|
1526
|
+
readonly location: string;
|
|
1527
|
+
/** 原文。 */
|
|
1528
|
+
readonly content: string;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* provider 凭据读取。需要 manifest 里的 `credentials.read` 声明。
|
|
1532
|
+
*
|
|
1533
|
+
* 只读:宿主绝不刷新 token,也绝不写回 —— provider CLI 自己拥有 auth 生命周期,
|
|
1534
|
+
* 插件从旁边读一眼不能把它的登录状态搅乱。
|
|
1535
|
+
*/
|
|
1536
|
+
export interface PluginCredentials {
|
|
1537
|
+
/**
|
|
1538
|
+
* 这个实例**所有能读到的**凭据候选,按宿主的查找顺序。读不到任何一份就是空数组,
|
|
1539
|
+
* 不抛错。
|
|
1540
|
+
*
|
|
1541
|
+
* **为什么给一串而不是一个**:宿主不认各家的凭据格式(`content` 是不透明原文),
|
|
1542
|
+
* 所以它判不出「这份还有效吗」——登录过期后常留下一个存在、非空、但没有 token 的
|
|
1543
|
+
* 空壳文件,宿主眼里它和有效的那份没区别。谁有效只有插件说得出来:它本来就要解析
|
|
1544
|
+
* 这份原文才能拿到 token。挨个试是廉价的——解不出 token 的那份当场短路,不会发出
|
|
1545
|
+
* 任何网络请求(路线图 #22)。
|
|
1546
|
+
*/
|
|
1547
|
+
readonly readProviderCredentials: (instanceId: string) => Promise<readonly PluginProviderCredentials[]>;
|
|
1548
|
+
}
|
|
1549
|
+
/** `kill` 认的信号。不开放任意字符串——能用到的就这几个。 */
|
|
1550
|
+
export type PluginProcessSignal = "SIGTERM" | "SIGKILL" | "SIGINT" | "SIGHUP";
|
|
1551
|
+
/**
|
|
1552
|
+
* 按键指程序:manifest 的白名单项写成 `{ key, program }`(或 `config:<key>` 注入一份
|
|
1553
|
+
* `Record<键, 程序>`)时,插件只报键,宿主查出程序。声明里没有这个键就被拒。
|
|
1554
|
+
*
|
|
1555
|
+
* 用它而不是自己从 config 里读路径:那样插件手上就多了一份可能与白名单漂开的副本。
|
|
1556
|
+
*/
|
|
1557
|
+
export interface PluginProgramRef {
|
|
1558
|
+
readonly key: string;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* 要跑的程序。字符串形态必须逐字在 manifest 的 `process.spawn` 白名单里:裸名(宿主
|
|
1562
|
+
* 在 PATH 上查)、`./` 开头的插件目录内相对路径、或声明里 `config:<key>` 注入的那个值。
|
|
1563
|
+
*
|
|
1564
|
+
* **三种字符串形态互不通用**,宿主按形态分别判:
|
|
1565
|
+
* - 裸名(不含 `/`、`\`,也不是绝对路径):白名单里必须有逐字相同的一条,然后在
|
|
1566
|
+
* PATH 上解析,且解析结果的 basename 还要与声明一致(挡住 PATH 上撞名解析到别的
|
|
1567
|
+
* 文件)。找不到就是 `not-on-path`。
|
|
1568
|
+
* - 路径形态(含分隔符或绝对路径):按 `pluginDir` resolve 后与白名单里同为路径形态
|
|
1569
|
+
* 的条目逐一比绝对路径。
|
|
1570
|
+
* - 两种形态之间不互通:声明 `gh` 不放行 `/tmp/gh`(否则谁都能拿一个同名文件冒名
|
|
1571
|
+
* 顶替),声明 `./bin/x` 也不放行裸名 `x`。
|
|
1572
|
+
*
|
|
1573
|
+
* ## `config:<key>` 间接引用(`fs.read` 的 `paths` 与 `process.spawn` 的 `programs` 共用)
|
|
1574
|
+
*
|
|
1575
|
+
* 声明期钉不住的值(宿主装配时才算得出的 trace 目录、Codex 的 `binaryPath`……)写成
|
|
1576
|
+
* `config:<key>`,指回本条 entry 的 `config[key]`。缺这个键、或值不是**非空字符串**,
|
|
1577
|
+
* 等同于这条声明不存在——不静默放行成 undefined。值有三种形状:
|
|
1578
|
+
* - `string`:展开成一条。
|
|
1579
|
+
* - `string[]`:展开成多条(空串与非字符串项丢掉)。像「每个 provider 实例各自的
|
|
1580
|
+
* 转录目录」这种条数随设置变的前缀,声明期连条数都钉不住。
|
|
1581
|
+
* - `Record<键, 程序>`:**只有 `process.spawn` 的裸白名单项认这一形状**,整条展开成
|
|
1582
|
+
* 多条带键项(等价于写了一串 `{ key, program }`),插件之后按 `{ key }` 报程序。
|
|
1583
|
+
* `fs.read` 的 `paths` 不认它。已经写成 `{ key, program }` 的白名单项,它的
|
|
1584
|
+
* `config:` 值只当 string / string[] 用——键已经由声明给了,值里再带一层键没意义。
|
|
1585
|
+
*/
|
|
1586
|
+
export type PluginProgramSelector = string | PluginProgramRef;
|
|
1587
|
+
export interface PluginProcessRunInput {
|
|
1588
|
+
readonly program: PluginProgramSelector;
|
|
1589
|
+
readonly args?: readonly string[] | undefined;
|
|
1590
|
+
/**
|
|
1591
|
+
* 只许 pluginDir / dataDir / cacheDir / 已声明的 `fs.read` 前缀之内;另外声明了
|
|
1592
|
+
* `process.workspace` 的插件还可以落在这台机器上任一项目的工作区目录或其子目录里
|
|
1593
|
+
* (`gh` 这类程序得站在仓库里才知道自己在哪)。缺省 dataDir。
|
|
1594
|
+
*
|
|
1595
|
+
* 比对与 `fs.read` 同一条纪律:展开 `~`、resolve、按分隔符边界切。每次 `run` /
|
|
1596
|
+
* `spawn` 前宿主都会先把 `dataDir` 建出来(缺席就 recursive 创建),所以缺省 cwd
|
|
1597
|
+
* 与显式传 dataDir 两条路都不会 ENOENT。
|
|
1598
|
+
*/
|
|
1599
|
+
readonly cwd?: string | undefined;
|
|
1600
|
+
/** 叠加在宿主环境上。`PATH`、`HOME`、`YCODIUM_*` 不许覆盖,写了整次调用被拒。 */
|
|
1601
|
+
readonly env?: Readonly<Record<string, string>> | undefined;
|
|
1602
|
+
readonly stdin?: string | undefined;
|
|
1603
|
+
/** 缺省 60000。到点先 SIGTERM 后 SIGKILL,结果带 `timedOut: true`。 */
|
|
1604
|
+
readonly timeoutMs?: number | undefined;
|
|
1605
|
+
/** 每条流的上限,缺省 1MB。超出即截断并置 `truncated`,不是错误。 */
|
|
1606
|
+
readonly maxOutputBytes?: number | undefined;
|
|
1607
|
+
}
|
|
1608
|
+
export interface PluginProcessRunResult {
|
|
1609
|
+
/** 被信号杀掉(含超时)没有退出码,是 null。 */
|
|
1610
|
+
readonly exitCode: number | null;
|
|
1611
|
+
readonly stdout: string;
|
|
1612
|
+
readonly stderr: string;
|
|
1613
|
+
/** 任一条流碰到 `maxOutputBytes` 就是 true。 */
|
|
1614
|
+
readonly truncated: boolean;
|
|
1615
|
+
readonly timedOut: boolean;
|
|
1616
|
+
readonly durationMs: number;
|
|
1617
|
+
}
|
|
1618
|
+
export interface PluginProcessSpawnInput {
|
|
1619
|
+
readonly program: PluginProgramSelector;
|
|
1620
|
+
readonly args?: readonly string[] | undefined;
|
|
1621
|
+
/** 与 `run` 同一条 cwd 规则,含 `process.workspace` 那一档。 */
|
|
1622
|
+
readonly cwd?: string | undefined;
|
|
1623
|
+
readonly env?: Readonly<Record<string, string>> | undefined;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* 一个还活着的子进程。
|
|
1627
|
+
*
|
|
1628
|
+
* **背压**:`stdout` / `stderr` 各自后面是一条有界队列(256 块)。队列满了宿主就
|
|
1629
|
+
* 停止从子进程的 pipe 读,子进程写日志的那次 write 会阻塞——**不丢块,也不无限
|
|
1630
|
+
* 缓冲**。所以拿了句柄就要把两条流都读掉;只读 stdout 不读 stderr 的插件迟早会把
|
|
1631
|
+
* 一个话痨子进程卡住。进程退出后队列里剩的块照样读得到,读完迭代自然结束。
|
|
1632
|
+
*/
|
|
1633
|
+
export interface PluginProcessHandle {
|
|
1634
|
+
readonly pid: number;
|
|
1635
|
+
readonly stdout: AsyncIterable<Uint8Array>;
|
|
1636
|
+
readonly stderr: AsyncIterable<Uint8Array>;
|
|
1637
|
+
/** 队列满时这个 promise 挂着,直到子进程读走为止。 */
|
|
1638
|
+
readonly writeStdin: (chunk: Uint8Array | string) => Promise<void>;
|
|
1639
|
+
/** 关掉子进程的 stdin。等 EOF 才动的程序(很多 CLI)不关就永远等下去。 */
|
|
1640
|
+
readonly closeStdin: () => Promise<void>;
|
|
1641
|
+
/** 缺省 SIGTERM;5 秒后仍活着宿主补 SIGKILL。 */
|
|
1642
|
+
readonly kill: (signal?: PluginProcessSignal) => Promise<void>;
|
|
1643
|
+
/** 正常退出给退出码;被信号杀掉是 null。 */
|
|
1644
|
+
readonly exited: Promise<number | null>;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* 被宿主拒掉的调用,抛出的 Error 上带的结构化原因。
|
|
1648
|
+
*
|
|
1649
|
+
* 约束:`ctx.process` / `ctx.fs` / `ctx.credentials` 被门拦下时抛的 Error 一定带
|
|
1650
|
+
* `capability`(如 `"process.spawn"`)与 `reason` 两个字段——分支照着它写,别去解析
|
|
1651
|
+
* message 文本,那行字是给人读的,会变。
|
|
1652
|
+
*
|
|
1653
|
+
* - `undeclared` —— manifest 里根本没有这条权限。
|
|
1654
|
+
* - `not-declared` —— 有 `process.spawn`,但这个程序不在白名单里。
|
|
1655
|
+
* - `unknown-key` —— 报的 `{ key }` 在白名单里没有对应项。
|
|
1656
|
+
* - `not-on-path` —— 白名单里的裸名在这台机器的 PATH 上找不到。
|
|
1657
|
+
* - `outside-prefixes` —— 路径不在声明的 `fs.read` 前缀之内。
|
|
1658
|
+
* - `cwd-outside-roots` —— cwd 不在插件自己的目录、声明的 `fs.read` 前缀、
|
|
1659
|
+
* (声明了 `process.workspace` 时)这台机器的项目目录之内。
|
|
1660
|
+
* - `projects-unavailable` —— 这台机器读不出项目清单,`process.workspace` 判不了。
|
|
1661
|
+
* - `protected-env` —— `env` 想覆盖 `PATH` / `HOME` / `YCODIUM_*`。
|
|
1662
|
+
* - `too-many-processes` —— 这个插件活着的子进程已到上限(默认 8,见 `PluginProcess`)。
|
|
1663
|
+
* - `foreign-thread` —— 命令指的会话不是这个插件开的(路线图 #62)。
|
|
1664
|
+
* - `not-found` —— `ctx.fs` 的目标路径不存在。与「路径在,但读不了」分得开:后者
|
|
1665
|
+
* (权限、坏设备……)没有结构化原因,`reason` 留空(路线图 #14)。
|
|
1666
|
+
* - `spawn-failed` —— 门都过了,但进程没起来(可执行文件不在、cwd 不存在之类)。
|
|
1667
|
+
* - `too-many-sockets` —— 这个插件活着的 WebSocket 已到上限(见 `PluginSocket`)。
|
|
1668
|
+
* - `socket-connect-failed` —— WebSocket 连不上(URL 不合法、握手被拒、开连接超时)。
|
|
1669
|
+
*/
|
|
1670
|
+
export type PluginCapabilityDeniedReason = "undeclared" | "not-declared" | "unknown-key" | "not-on-path" | "outside-prefixes" | "cwd-outside-roots" | "projects-unavailable"
|
|
1671
|
+
/** 这次装配里没有编排引擎——`ycodium x` 的 CLI 进程就是这样。 */
|
|
1672
|
+
| "host-unavailable"
|
|
1673
|
+
/** 插件实例已经停用;旧回调不能继续写入或控制会话。 */
|
|
1674
|
+
| "disposed" | "protected-env" | "foreign-thread" | "not-found" | "too-many-processes" | "spawn-failed" | "too-many-sockets" | "socket-connect-failed"
|
|
1675
|
+
/**
|
|
1676
|
+
* 事实折不动,所以不让它进日志。
|
|
1677
|
+
*
|
|
1678
|
+
* `ctx.events.append` 在写进日志之前会拿你注册的每一块 `derivedState` 试折一遍——
|
|
1679
|
+
* 用的是状态副本,真状态不动。`apply` 抛错、或者折出来的状态存不下去,这次 append
|
|
1680
|
+
* 就被拒绝。
|
|
1681
|
+
*
|
|
1682
|
+
* 进了日志的事实删不掉:之后每一次重启重建都会再踩一次同一个坑。所以宁可在门口拒。
|
|
1683
|
+
*/
|
|
1684
|
+
| "does-not-fold";
|
|
1685
|
+
/**
|
|
1686
|
+
* 子进程能力。需要 manifest 里的 `process.spawn` 声明,且 `program` 必须在声明的
|
|
1687
|
+
* 白名单内;没声明或不在白名单的调用一律 reject(ADR-0022)。
|
|
1688
|
+
*
|
|
1689
|
+
* **这是插件跑子进程的唯一合法通道。** 插件与服务器同进程,自己 `import
|
|
1690
|
+
* "node:child_process"` 当然也起得来——但那样起的进程不在声明里、不进审计日志、
|
|
1691
|
+
* 插件停用时没人杀,等于绕开安装时给人看的那份清单,视同违反安装承诺。
|
|
1692
|
+
*
|
|
1693
|
+
* 每个插件同时最多 8 个活着的子进程(`run` 在飞的也算,不止 `spawn`),超出当场被拒,
|
|
1694
|
+
* 不排队。这个 8 是宿主默认值,不是硬顶:manifest 的 `process.spawn` 权限可以带
|
|
1695
|
+
* `maxConcurrent`(1..16 的整数)把它提高,写多少就是多少——不排队、不自动重试,
|
|
1696
|
+
* 数字要按这个插件实际会同时跑几个长驻子进程来定,不是越大越好(路线图 #46)。
|
|
1697
|
+
* 省略字段=吃宿主默认值 8。改这个数字与改程序白名单一样,要重新过一次安装确认门。
|
|
1698
|
+
* 插件停用 / 卸载 / 所在线程删除时,它起的子进程一律先 SIGTERM、5 秒后 SIGKILL。
|
|
1699
|
+
*
|
|
1700
|
+
* manifest 里写**多条** `process.spawn` 声明是合法的:程序白名单按顺序合并成一张表,
|
|
1701
|
+
* 上限取**第一条带 `maxConcurrent` 的**那个值(同一个插件报两个不同上限没有意义)。
|
|
1702
|
+
*
|
|
1703
|
+
* 被拒的调用 reject 出来的 Error 带 `capability` 与 `reason`(见
|
|
1704
|
+
* `PluginCapabilityDeniedReason`);起不来的进程 reason 是 `spawn-failed`。
|
|
1705
|
+
*/
|
|
1706
|
+
export interface PluginProcess {
|
|
1707
|
+
/** 跑完收结果。长驻的东西用 `spawn`,别拿它等一个不退出的进程。 */
|
|
1708
|
+
readonly run: (input: PluginProcessRunInput) => Promise<PluginProcessRunResult>;
|
|
1709
|
+
readonly spawn: (input: PluginProcessSpawnInput) => Promise<PluginProcessHandle>;
|
|
1710
|
+
}
|
|
1711
|
+
export interface PluginSocketConnectInput {
|
|
1712
|
+
/** `ws://` 或 `wss://`。别的 scheme 当场 reject。 */
|
|
1713
|
+
readonly url: string;
|
|
1714
|
+
/** WebSocket 子协议(`Sec-WebSocket-Protocol`)。 */
|
|
1715
|
+
readonly protocols?: readonly string[] | undefined;
|
|
1716
|
+
/** 走哪个代理,三态见 `PluginProxySelection`。省略=机器的全局默认。 */
|
|
1717
|
+
readonly proxy?: PluginProxySelection | undefined;
|
|
1718
|
+
/** 等 `open` 的上限,缺省 10000。到点算连接失败,`connect` reject。 */
|
|
1719
|
+
readonly openTimeoutMs?: number | undefined;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* 连接怎么收场的。三种来源摊平成同一份记录,插件不必分辨是谁先动的手:
|
|
1723
|
+
*
|
|
1724
|
+
* - 对端或本端正常关闭 —— `code` 是 WebSocket 关闭码(本端不给码时是 1000),
|
|
1725
|
+
* `reason` 是对端给的那句话(没给就是 null),`error` 是 null。
|
|
1726
|
+
* - 连着的时候读写出错 —— `code` 为 null,`error` 是一句给人读的说明。
|
|
1727
|
+
* - 插件被停用 / 卸载 —— 宿主自己收的连接,`code` 为 null,`error` 说明是宿主关的。
|
|
1728
|
+
*/
|
|
1729
|
+
export interface PluginSocketClose {
|
|
1730
|
+
readonly code: number | null;
|
|
1731
|
+
readonly reason: string | null;
|
|
1732
|
+
readonly error: string | null;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* 一条还活着的出站 WebSocket。
|
|
1736
|
+
*
|
|
1737
|
+
* **背压**:`messages` 后面是一条有界队列(256 帧)。队列满了宿主就不再从连接上读,
|
|
1738
|
+
* TCP 层随之背压 —— 不丢帧,也不无限缓冲。所以拿了句柄就要把 `messages` 消费掉;
|
|
1739
|
+
* 只连不读的插件迟早把一个话痨端点堵死。连接断开后队列里剩的帧照样读得到,读完
|
|
1740
|
+
* 迭代自然结束(不是抛错 —— 断开的原因在 `closed` 里)。
|
|
1741
|
+
*/
|
|
1742
|
+
export interface PluginSocketHandle {
|
|
1743
|
+
/** 收到的帧:文本帧是 string,二进制帧是 Uint8Array,宿主不替你解码。 */
|
|
1744
|
+
readonly messages: AsyncIterable<string | Uint8Array>;
|
|
1745
|
+
/** 连接已经断了再发就 reject。 */
|
|
1746
|
+
readonly send: (data: string | Uint8Array) => Promise<void>;
|
|
1747
|
+
/** 主动关。缺省关闭码 1000(正常关闭)。重复关是空操作。 */
|
|
1748
|
+
readonly close: (code?: number, reason?: string) => Promise<void>;
|
|
1749
|
+
/** 断开原因。永远 resolve,不 reject —— 断开是结果,不是错误。 */
|
|
1750
|
+
readonly closed: Promise<PluginSocketClose>;
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* 出站 WebSocket。**不需要任何 manifest 声明**:可达范围与 `ctx.http` 完全相同
|
|
1754
|
+
* (同一台机器、同一套代理设置、同样的出网权限),而插件与服务器同进程,本就能
|
|
1755
|
+
* 自己开连接 —— 为同一件事新加一档声明只会让审批卡变长,不会让谁更安全。
|
|
1756
|
+
*
|
|
1757
|
+
* 用它而不是自己 `new WebSocket`:宿主这条路才认代理设置、才在插件停用时把连接
|
|
1758
|
+
* 收掉、才受同时连接数上限约束。自己开的连接在配了代理的机器上静默连不上,插件
|
|
1759
|
+
* 卸载后还挂着。
|
|
1760
|
+
*
|
|
1761
|
+
* 每个插件同时最多 16 条活着的连接,超出当场被拒(`reason: "too-many-sockets"`),
|
|
1762
|
+
* 不排队。这个数字不像 `ctx.process` 那样可以在 manifest 里抬高(这里根本没有声明
|
|
1763
|
+
* 可写),所以一次给够:够「每个配好的实例一条连接」那种插件用,也拦得住断线不收
|
|
1764
|
+
* 旧连接的泄漏。
|
|
1765
|
+
*
|
|
1766
|
+
* 插件停用 / 卸载 / 所在线程删除时,它开的连接一律被宿主关掉,`closed` 随之 resolve。
|
|
1767
|
+
*/
|
|
1768
|
+
export interface PluginSocket {
|
|
1769
|
+
readonly connect: (input: PluginSocketConnectInput) => Promise<PluginSocketHandle>;
|
|
1770
|
+
}
|
|
1771
|
+
export interface PluginContext<Config = Record<string, unknown>> {
|
|
1772
|
+
readonly id: string;
|
|
1773
|
+
/** 本插件在配置树里的条目 id,patch 的定位键。 */
|
|
1774
|
+
readonly entryId: string;
|
|
1775
|
+
/**
|
|
1776
|
+
* 本条目合成后的 config(四层 patch 栈的产物)。`password` 字段在这里是明文——
|
|
1777
|
+
* 宿主装配期从密文库解出来的,只活在这个进程里,别再往日志、命令返回值或
|
|
1778
|
+
* 自己写的文件里带。
|
|
1779
|
+
*/
|
|
1780
|
+
readonly config: Config;
|
|
1781
|
+
readonly paths: PluginPaths;
|
|
1782
|
+
readonly cache: PluginCache;
|
|
1783
|
+
readonly log: PluginLogger;
|
|
1784
|
+
readonly events: PluginEvents;
|
|
1785
|
+
readonly commands: PluginCommands;
|
|
1786
|
+
readonly operations: PluginOperations;
|
|
1787
|
+
/** 经宿主信封调用已启用插件的命令;目标缺席必须明确失败。 */
|
|
1788
|
+
readonly plugins: {
|
|
1789
|
+
readonly invoke: (input: {
|
|
1790
|
+
readonly pluginId: string;
|
|
1791
|
+
readonly command: string;
|
|
1792
|
+
readonly payload: unknown;
|
|
1793
|
+
}) => Promise<unknown>;
|
|
1794
|
+
};
|
|
1795
|
+
readonly notifications: PluginNotifications;
|
|
1796
|
+
readonly decisions: PluginDecisions;
|
|
1797
|
+
readonly settings: PluginSettings<Config>;
|
|
1798
|
+
/**
|
|
1799
|
+
* fetch 形状的 HTTP 客户端,按宿主的代理设置出网。插件不要自己用全局
|
|
1800
|
+
* `fetch` —— 那条路绕开代理,配了代理的机器上会静默失败。
|
|
1801
|
+
*
|
|
1802
|
+
* 走的是**建能力面时**读到的默认代理设置:等价于那一刻调过一次
|
|
1803
|
+
* `ctx.proxies.fetch()`,之后这个引用就钉在那个代理上了,**改代理设置要重启才
|
|
1804
|
+
* 生效**。要让改动立刻生效(或者本来就想走别的代理)就调 `ctx.proxies.fetch(...)`
|
|
1805
|
+
* 现要一个 —— 那一条每次调用时解析。两条路共用同一份连接池,同一个代理不会因为
|
|
1806
|
+
* 换个入口就多建一个 dispatcher。
|
|
1807
|
+
*
|
|
1808
|
+
* 设置读不出来就直连,不让插件出网整个失败。指向一个已删代理的**悬空 proxyId
|
|
1809
|
+
* 静默退化成直连**,与 `ctx.proxies` / `fetchAs` 同一条规则。
|
|
1810
|
+
*/
|
|
1811
|
+
readonly http: typeof globalThis.fetch;
|
|
1812
|
+
readonly environment: PluginEnvironment;
|
|
1813
|
+
readonly providers: PluginProviders;
|
|
1814
|
+
readonly workspace: PluginWorkspace;
|
|
1815
|
+
/** 列这台机器配了哪些代理,并按其中一个出网。不需要任何声明,见 `PluginProxies`。 */
|
|
1816
|
+
readonly proxies: PluginProxies;
|
|
1817
|
+
/** 出站 WebSocket。不需要任何声明(可达范围与 `ctx.http` 相同),见 `PluginSocket`。 */
|
|
1818
|
+
readonly socket: PluginSocket;
|
|
1819
|
+
readonly projections: PluginProjections;
|
|
1820
|
+
/** 把字节落成会话附件,见 `PluginAttachments`。不需要声明。 */
|
|
1821
|
+
readonly attachments: PluginAttachments;
|
|
1822
|
+
/** 需要 `fs.read` 声明;未声明时调用一律被拒。 */
|
|
1823
|
+
readonly fs: PluginFileSystem;
|
|
1824
|
+
/** 需要 `credentials.read` 声明;未声明时调用一律被拒。 */
|
|
1825
|
+
readonly credentials: PluginCredentials;
|
|
1826
|
+
/** 需要 `process.spawn` 声明;未声明、或程序不在白名单时调用一律被拒。项目目录里跑要再加 `process.workspace`。 */
|
|
1827
|
+
readonly process: PluginProcess;
|
|
1828
|
+
readonly toolkit: (toolkit: PluginToolkit) => PluginDisposer;
|
|
1829
|
+
readonly command: (command: PluginCommand) => PluginDisposer;
|
|
1830
|
+
readonly stream: (command: PluginStreamCommand) => PluginDisposer;
|
|
1831
|
+
readonly reactor: (reactor: PluginReactor) => PluginDisposer;
|
|
1832
|
+
/**
|
|
1833
|
+
* 注册一块托管的跨事件状态。宿主负责落盘、重启重建、补你没在线那段的课。
|
|
1834
|
+
*
|
|
1835
|
+
* 返回的把手只有 `get`:推进由宿主做,而且**在把事件发给任何处理器之前**做。
|
|
1836
|
+
* 你不需要(也不能)自己推进——两个推进点就是两条时间线。
|
|
1837
|
+
*/
|
|
1838
|
+
readonly derivedState: <S>(definition: PluginDerivedState<S>) => PluginDerivedStateHandle<S>;
|
|
1839
|
+
/**
|
|
1840
|
+
* 挂一条宿主托管的后台循环。插件卸载 / 停用即自动停,不用自己收尾。
|
|
1841
|
+
* 只在没有编排事件可驱动时才用,见 `PluginInterval`。
|
|
1842
|
+
*/
|
|
1843
|
+
readonly interval: (interval: PluginInterval) => PluginDisposer;
|
|
1844
|
+
/**
|
|
1845
|
+
* 贡献一条 `ycodium x <pluginId> <name>` 子命令。它跑在 CLI 进程里,
|
|
1846
|
+
* 那里没有编排引擎与数据库,见 `PluginCliCommand`。
|
|
1847
|
+
*/
|
|
1848
|
+
readonly cli: (command: PluginCliCommand) => PluginDisposer;
|
|
1849
|
+
/**
|
|
1850
|
+
* 声明一张设置卡。`password` 字段的值走密文库,见 `PluginSettingsFieldKind`;
|
|
1851
|
+
* 「先选类型、再填对应字段」用 `showWhen`(见 `PluginSettingsVisibility`),别把所有变体
|
|
1852
|
+
* 的字段并排摊开;`select` 的选项可以带 `icon`(手机端只画文字);
|
|
1853
|
+
* `table` 字段是一张实例表,一行一个实例,`password` 列每行各存一份密文。
|
|
1854
|
+
*/
|
|
1855
|
+
readonly settingsCard: (card: PluginSettingsCard) => PluginDisposer;
|
|
1856
|
+
/** 兜底 finalizer;与各注册的 disposer 一起逆序执行。 */
|
|
1857
|
+
readonly onDispose: (dispose: () => void | Promise<void>) => void;
|
|
1858
|
+
}
|
|
1859
|
+
export interface PluginDefinition<Config = Record<string, unknown>> {
|
|
1860
|
+
/** 必须与 patch 条目 id 的后缀一致,否则宿主拒绝加载。 */
|
|
1861
|
+
readonly id: string;
|
|
1862
|
+
readonly apiVersion: PluginApiVersion;
|
|
1863
|
+
/** 超过 5 秒未返回视为加载失败,插件被隔离掉。 */
|
|
1864
|
+
readonly activate: (ctx: PluginContext<Config>) => void | Promise<void>;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* 只做类型锚定:默认导出经过它,宿主校验的字段就都被 TS 盯住了。
|
|
1868
|
+
*/
|
|
1869
|
+
export declare const definePlugin: <Config = Record<string, unknown>>(definition: PluginDefinition<Config>) => PluginDefinition<Config>;
|
|
1870
|
+
//# sourceMappingURL=index.d.ts.map
|