@zhin.js/adapter 1.1.5 → 1.1.8
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 +7 -5
- package/lib/adapter-index.d.ts +9 -9
- package/lib/adapter-index.js +124 -256
- package/lib/definition.d.ts +3 -3
- package/lib/endpoint-commands.d.ts +17 -12
- package/lib/endpoint-commands.js +88 -38
- package/lib/endpoint-control.d.ts +8 -12
- package/lib/endpoint-control.js +3 -48
- package/lib/endpoint-lifecycle.js +1 -1
- package/lib/provider.js +2 -14
- package/package.json +6 -6
- package/src/adapter-index.ts +137 -282
- package/src/definition.ts +3 -3
- package/src/endpoint-commands.ts +116 -45
- package/src/endpoint-control.ts +9 -88
- package/src/endpoint-lifecycle.ts +1 -1
- package/src/provider.ts +3 -16
|
@@ -11,13 +11,18 @@ export type EndpointCommandReply = (text: string) => Promise<unknown>;
|
|
|
11
11
|
* 从命令 input(Runtime Message)提取 $reply;非消息来源(如 Host API 调用)降级为 no-op。
|
|
12
12
|
*/
|
|
13
13
|
export declare function extractEndpointCommandReply(input: unknown): EndpointCommandReply;
|
|
14
|
+
/**
|
|
15
|
+
* bindFlow 后续状态推送:优先走 OutboundHost(不受 inbound Message reply scope 限制)。
|
|
16
|
+
* 扫码绑定等长流程会在命令结果已送达、`$reply` 已冻结后继续 notify,必须用 durable 出站。
|
|
17
|
+
*/
|
|
18
|
+
export declare function createDurableEndpointCommandReply(input: unknown, use: EndpointCommandUse): EndpointCommandReply;
|
|
14
19
|
export interface EndpointRunningInfo {
|
|
15
|
-
readonly
|
|
20
|
+
readonly id: string;
|
|
16
21
|
/** 连接模式(ws / wss / polling / socket-mode …),仅用于 list 展示。 */
|
|
17
22
|
readonly mode?: string;
|
|
18
23
|
}
|
|
19
24
|
export interface EndpointRuntimeState {
|
|
20
|
-
/** 当前 generation 已成功创建的 endpoint(
|
|
25
|
+
/** 当前 generation 已成功创建的 endpoint(id → 描述) */
|
|
21
26
|
readonly endpoints: Map<string, EndpointRunningInfo>;
|
|
22
27
|
}
|
|
23
28
|
export declare function createEndpointRuntimeState(): EndpointRuntimeState;
|
|
@@ -26,21 +31,21 @@ export declare function defineEndpointRuntimeStateToken(adapterKey: string): Tok
|
|
|
26
31
|
/** 项目根:ZHIN_PROJECT_ROOT 优先,缺省 process.cwd()(替代 legacy runtimeCwd) */
|
|
27
32
|
export declare function resolveProjectRoot(): string;
|
|
28
33
|
/** 派生 endpoint 凭据的 env 键:`${ADAPTER}_${NAME}_${FIELD}`(如 `TELEGRAM_MY_BOT_TOKEN`) */
|
|
29
|
-
export declare function buildEndpointEnvKey(adapterKey: string,
|
|
34
|
+
export declare function buildEndpointEnvKey(adapterKey: string, endpointId: string, fieldKey: string): string;
|
|
30
35
|
/** 写入或更新 `.env` 中的键值,并同步到当前进程 `process.env` */
|
|
31
36
|
export declare function persistEndpointEnvValues(values: Readonly<Record<string, string>>, projectRoot?: string): void;
|
|
32
37
|
export interface ConfiguredEndpointEntry {
|
|
33
|
-
|
|
38
|
+
id: string;
|
|
34
39
|
[key: string]: unknown;
|
|
35
40
|
}
|
|
36
41
|
/** 定位项目配置文件:ZHIN_CONFIG 指定优先,否则发现 zhin.config.yml/.yaml,都没有则默认新建 zhin.config.yml */
|
|
37
42
|
export declare function findEndpointConfigFile(adapterKey: string, projectRoot?: string): string;
|
|
38
43
|
/** 读取 plugins.<adapterKey>.endpoints(plain JS);plugins/<adapterKey> 缺失或形态不符时返回 [] */
|
|
39
44
|
export declare function listConfiguredEndpoints(adapterKey: string, projectRoot?: string): ConfiguredEndpointEntry[];
|
|
40
|
-
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;
|
|
45
|
+
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;id 已存在时报错 */
|
|
41
46
|
export declare function addEndpointToConfig(adapterKey: string, entry: ConfiguredEndpointEntry, projectRoot?: string): string;
|
|
42
|
-
/** 按
|
|
43
|
-
export declare function removeEndpointFromConfig(adapterKey: string,
|
|
47
|
+
/** 按 id 移除 plugins.<adapterKey>.endpoints 项;不存在返回 false */
|
|
48
|
+
export declare function removeEndpointFromConfig(adapterKey: string, id: string, projectRoot?: string): {
|
|
44
49
|
removed: boolean;
|
|
45
50
|
filePath: string;
|
|
46
51
|
};
|
|
@@ -58,9 +63,9 @@ export interface EndpointFieldSpec {
|
|
|
58
63
|
export type EndpointCommandUse = <T>(token: Token<T>) => T;
|
|
59
64
|
/** bindFlow 钩子上下文:接管 add 命令的自定义绑定流程(如 QQ 扫码)。 */
|
|
60
65
|
export interface EndpointBindFlowContext {
|
|
61
|
-
/** 命令参数
|
|
62
|
-
readonly
|
|
63
|
-
/** 向当前会话推送后续状态(二维码刷新 / 成功 /
|
|
66
|
+
/** 命令参数 id(未指定时为 undefined,流程可自行决定终名) */
|
|
67
|
+
readonly id?: string;
|
|
68
|
+
/** 向当前会话推送后续状态(二维码刷新 / 成功 / 失败;走 durable OutboundHost,可在命令 reply scope 结束后调用) */
|
|
64
69
|
readonly reply: EndpointCommandReply;
|
|
65
70
|
readonly config: unknown;
|
|
66
71
|
readonly input: unknown;
|
|
@@ -126,8 +131,8 @@ export declare function formatEndpointList(spec: Pick<EndpointCommandsSpec, 'ada
|
|
|
126
131
|
readonly footer?: string;
|
|
127
132
|
}): string;
|
|
128
133
|
/** add(kv 模式)的完整业务逻辑:解析 kv → 凭据写 .env → 追加 yaml;返回回复文本。 */
|
|
129
|
-
export declare function addEndpointFromKeyValues(spec: EndpointCommandsSpec,
|
|
134
|
+
export declare function addEndpointFromKeyValues(spec: EndpointCommandsSpec, id: string, args: readonly string[], projectRoot?: string): string;
|
|
130
135
|
/** remove 的完整业务逻辑:从 yaml 移除;返回回复文本。 */
|
|
131
|
-
export declare function
|
|
136
|
+
export declare function removeEndpointById(spec: Pick<EndpointCommandsSpec, 'adapterKey'>, id: string, projectRoot?: string): string;
|
|
132
137
|
/** 生成 `<adapter> endpoint` 的 list / add / remove 三个命令定义(见文件头接入步骤)。 */
|
|
133
138
|
export declare function createEndpointCommands<TCommand>(spec: EndpointCommandsSpec, defineCommand: (definition: EndpointCommandDefinition) => TCommand): EndpointCommands<TCommand>;
|
package/lib/endpoint-commands.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 接入步骤(以 telegram 为例):
|
|
17
17
|
* 1. plugin.ts setup 里 `context.resources.provide(telegramRuntimeStateToken, createEndpointRuntimeState())`,
|
|
18
18
|
* token 由 `defineEndpointRuntimeStateToken('telegram')` 创建。
|
|
19
|
-
* 2. adapters/telegram.ts create() 里 `context.use(token).endpoints.set(config.
|
|
19
|
+
* 2. adapters/telegram.ts create() 里 `context.use(token).endpoints.set(config.id, { id, mode })`。
|
|
20
20
|
* 3. src 下 `export const telegramEndpointCommands = createEndpointCommands({ adapterKey: 'telegram', ... }, defineCommand)`
|
|
21
21
|
* (defineCommand 由调用方从 @zhin.js/command 传入——provider 包之间禁止互相 import,
|
|
22
22
|
* 见 scripts/check-architecture-layers.mjs,故 defineCommand 走依赖注入)。
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import fs from 'node:fs';
|
|
30
30
|
import path from 'node:path';
|
|
31
|
-
import { createToken } from '@zhin.js/plugin-runtime';
|
|
31
|
+
import { createToken, outboundHostToken, } from '@zhin.js/plugin-runtime';
|
|
32
32
|
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
// 权限:master 判定
|
|
@@ -55,8 +55,9 @@ export function isEndpointOperator(config, input) {
|
|
|
55
55
|
}
|
|
56
56
|
if (masters.size === 0)
|
|
57
57
|
return true;
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
const senderRaw = input?.sender;
|
|
59
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? senderRaw.id.trim() : '';
|
|
60
|
+
return !!senderId && masters.has(senderId);
|
|
60
61
|
}
|
|
61
62
|
/** add/remove 的拒绝文案(list 只读,不校验)。 */
|
|
62
63
|
export function endpointCommandForbidden(adapterDisplayName) {
|
|
@@ -72,6 +73,56 @@ export function extractEndpointCommandReply(input) {
|
|
|
72
73
|
}
|
|
73
74
|
return async () => undefined;
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* bindFlow 后续状态推送:优先走 OutboundHost(不受 inbound Message reply scope 限制)。
|
|
78
|
+
* 扫码绑定等长流程会在命令结果已送达、`$reply` 已冻结后继续 notify,必须用 durable 出站。
|
|
79
|
+
*/
|
|
80
|
+
export function createDurableEndpointCommandReply(input, use) {
|
|
81
|
+
const scoped = extractEndpointCommandReply(input);
|
|
82
|
+
const target = readOutboundSendTarget(input);
|
|
83
|
+
if (!target)
|
|
84
|
+
return scoped;
|
|
85
|
+
return async (text) => {
|
|
86
|
+
let outbound;
|
|
87
|
+
try {
|
|
88
|
+
outbound = use(outboundHostToken);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
outbound = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (outbound) {
|
|
94
|
+
await outbound.send({ ...target, content: text });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await scoped(text);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function readOutboundSendTarget(input) {
|
|
101
|
+
const message = input;
|
|
102
|
+
const conversation = message?.conversation;
|
|
103
|
+
const endpointKeyRaw = conversation?.endpoint?.id;
|
|
104
|
+
const adapterRaw = conversation?.endpoint?.adapter;
|
|
105
|
+
const kind = conversation?.kind;
|
|
106
|
+
const id = conversation?.id;
|
|
107
|
+
if (endpointKeyRaw == null
|
|
108
|
+
|| adapterRaw == null
|
|
109
|
+
|| (kind !== 'private' && kind !== 'group' && kind !== 'channel')
|
|
110
|
+
|| typeof id !== 'string'
|
|
111
|
+
|| !id) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const live = String(message?.metadata?.endpoint ?? message?.metadata?.endpointKey ?? '').trim();
|
|
115
|
+
return {
|
|
116
|
+
adapter: String(adapterRaw),
|
|
117
|
+
endpointKey: live || String(endpointKeyRaw),
|
|
118
|
+
conversation: {
|
|
119
|
+
kind,
|
|
120
|
+
id,
|
|
121
|
+
parent: conversation.parent,
|
|
122
|
+
threadId: typeof conversation.threadId === 'string' ? conversation.threadId : undefined,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
75
126
|
export function createEndpointRuntimeState() {
|
|
76
127
|
return { endpoints: new Map() };
|
|
77
128
|
}
|
|
@@ -94,8 +145,8 @@ function envSlug(text) {
|
|
|
94
145
|
.toUpperCase();
|
|
95
146
|
}
|
|
96
147
|
/** 派生 endpoint 凭据的 env 键:`${ADAPTER}_${NAME}_${FIELD}`(如 `TELEGRAM_MY_BOT_TOKEN`) */
|
|
97
|
-
export function buildEndpointEnvKey(adapterKey,
|
|
98
|
-
return `${envSlug(adapterKey)}_${envSlug(
|
|
148
|
+
export function buildEndpointEnvKey(adapterKey, endpointId, fieldKey) {
|
|
149
|
+
return `${envSlug(adapterKey)}_${envSlug(endpointId)}_${envSlug(fieldKey)}`;
|
|
99
150
|
}
|
|
100
151
|
function escapeRegExp(text) {
|
|
101
152
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -162,13 +213,13 @@ export function listConfiguredEndpoints(adapterKey, projectRoot) {
|
|
|
162
213
|
const endpoints = plugins[adapterKey]?.endpoints;
|
|
163
214
|
if (!Array.isArray(endpoints))
|
|
164
215
|
return [];
|
|
165
|
-
return endpoints.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.
|
|
216
|
+
return endpoints.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.id === 'string');
|
|
166
217
|
}
|
|
167
|
-
function
|
|
218
|
+
function entryId(item) {
|
|
168
219
|
if (!isMap(item))
|
|
169
220
|
return undefined;
|
|
170
|
-
const
|
|
171
|
-
return typeof
|
|
221
|
+
const id = item.get('id');
|
|
222
|
+
return typeof id === 'string' && id ? id : undefined;
|
|
172
223
|
}
|
|
173
224
|
/**
|
|
174
225
|
* 确保 plugins.<adapterKey>.endpoints 存在并返回其 YAMLSeq(节点级操作,保留既有条目与注释)。
|
|
@@ -202,22 +253,22 @@ function ensureEndpointsSeq(doc, adapterKey) {
|
|
|
202
253
|
}
|
|
203
254
|
return doc.getIn(['plugins', adapterKey, 'endpoints']);
|
|
204
255
|
}
|
|
205
|
-
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;
|
|
256
|
+
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;id 已存在时报错 */
|
|
206
257
|
export function addEndpointToConfig(adapterKey, entry, projectRoot) {
|
|
207
258
|
const document = readConfigDocument(adapterKey, projectRoot);
|
|
208
259
|
const seq = ensureEndpointsSeq(document.doc, adapterKey);
|
|
209
|
-
if (seq.items.some((item) =>
|
|
210
|
-
throw new Error(`配置中已存在 ${adapterKey} endpoint「${entry.
|
|
260
|
+
if (seq.items.some((item) => entryId(item) === entry.id)) {
|
|
261
|
+
throw new Error(`配置中已存在 ${adapterKey} endpoint「${entry.id}」,可先 ${adapterKey}.endpoint remove ${entry.id} 再重新添加`);
|
|
211
262
|
}
|
|
212
263
|
seq.items.push(document.doc.createNode(entry));
|
|
213
264
|
writeConfigDocument(document);
|
|
214
265
|
return document.filePath;
|
|
215
266
|
}
|
|
216
|
-
/** 按
|
|
217
|
-
export function removeEndpointFromConfig(adapterKey,
|
|
267
|
+
/** 按 id 移除 plugins.<adapterKey>.endpoints 项;不存在返回 false */
|
|
268
|
+
export function removeEndpointFromConfig(adapterKey, id, projectRoot) {
|
|
218
269
|
const document = readConfigDocument(adapterKey, projectRoot);
|
|
219
270
|
const seq = ensureEndpointsSeq(document.doc, adapterKey);
|
|
220
|
-
const next = seq.items.filter((item) =>
|
|
271
|
+
const next = seq.items.filter((item) => entryId(item) !== id);
|
|
221
272
|
if (next.length === seq.items.length) {
|
|
222
273
|
return { removed: false, filePath: document.filePath };
|
|
223
274
|
}
|
|
@@ -225,9 +276,9 @@ export function removeEndpointFromConfig(adapterKey, name, projectRoot) {
|
|
|
225
276
|
writeConfigDocument(document);
|
|
226
277
|
return { removed: true, filePath: document.filePath };
|
|
227
278
|
}
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
return typeof
|
|
279
|
+
function endpointIdParam(params) {
|
|
280
|
+
const id = params.id;
|
|
281
|
+
return typeof id === 'string' && id.trim() ? id.trim() : undefined;
|
|
231
282
|
}
|
|
232
283
|
/** list 文案:运行中 + 配置中两段,footer 可选。 */
|
|
233
284
|
export function formatEndpointList(spec, source) {
|
|
@@ -239,7 +290,7 @@ export function formatEndpointList(spec, source) {
|
|
|
239
290
|
}
|
|
240
291
|
else {
|
|
241
292
|
for (const endpoint of running) {
|
|
242
|
-
lines.push(endpoint.mode ? ` - ${endpoint.
|
|
293
|
+
lines.push(endpoint.mode ? ` - ${endpoint.id}(${endpoint.mode})` : ` - ${endpoint.id}`);
|
|
243
294
|
}
|
|
244
295
|
}
|
|
245
296
|
lines.push(`【配置中的 ${spec.adapterDisplayName} endpoints】(zhin.config.yml → plugins.${spec.adapterKey}.endpoints)`);
|
|
@@ -249,7 +300,7 @@ export function formatEndpointList(spec, source) {
|
|
|
249
300
|
else {
|
|
250
301
|
for (const entry of source.configured) {
|
|
251
302
|
const detail = spec.describeEntry?.(entry);
|
|
252
|
-
lines.push(detail ? ` - ${entry.
|
|
303
|
+
lines.push(detail ? ` - ${entry.id}(${detail})` : ` - ${entry.id}`);
|
|
253
304
|
}
|
|
254
305
|
}
|
|
255
306
|
if (source.footer)
|
|
@@ -268,10 +319,10 @@ function addUsage(spec) {
|
|
|
268
319
|
].filter(Boolean).join(',');
|
|
269
320
|
return marks ? `${field.key}(${marks})` : field.key;
|
|
270
321
|
}).join('、')}`;
|
|
271
|
-
return `用法:${spec.adapterKey}.endpoint add <
|
|
322
|
+
return `用法:${spec.adapterKey}.endpoint add <id> <key=value...>${fieldText}`;
|
|
272
323
|
}
|
|
273
324
|
/** add(kv 模式)的完整业务逻辑:解析 kv → 凭据写 .env → 追加 yaml;返回回复文本。 */
|
|
274
|
-
export function addEndpointFromKeyValues(spec,
|
|
325
|
+
export function addEndpointFromKeyValues(spec, id, args, projectRoot) {
|
|
275
326
|
const fields = spec.fields ?? [];
|
|
276
327
|
const known = new Map(fields.map((field) => [field.key, field]));
|
|
277
328
|
const values = new Map();
|
|
@@ -293,14 +344,14 @@ export function addEndpointFromKeyValues(spec, name, args, projectRoot) {
|
|
|
293
344
|
if (missing.length > 0) {
|
|
294
345
|
return `缺少必填字段:${missing.map((field) => field.key).join('、')}。${addUsage(spec)}`;
|
|
295
346
|
}
|
|
296
|
-
const entry = {
|
|
347
|
+
const entry = { id };
|
|
297
348
|
const envValues = {};
|
|
298
349
|
for (const field of fields) {
|
|
299
350
|
const value = values.get(field.key);
|
|
300
351
|
if (value === undefined)
|
|
301
352
|
continue;
|
|
302
353
|
if (field.env) {
|
|
303
|
-
const envKey = buildEndpointEnvKey(spec.adapterKey,
|
|
354
|
+
const envKey = buildEndpointEnvKey(spec.adapterKey, id, field.key);
|
|
304
355
|
envValues[envKey] = value;
|
|
305
356
|
entry[field.key] = `\${${envKey}}`;
|
|
306
357
|
}
|
|
@@ -309,11 +360,10 @@ export function addEndpointFromKeyValues(spec, name, args, projectRoot) {
|
|
|
309
360
|
}
|
|
310
361
|
}
|
|
311
362
|
try {
|
|
312
|
-
// 先写配置(重名等校验失败时不留孤儿 .env 键),再落 .env 凭据
|
|
313
363
|
const filePath = addEndpointToConfig(spec.adapterKey, entry, projectRoot);
|
|
314
364
|
if (Object.keys(envValues).length > 0)
|
|
315
365
|
persistEndpointEnvValues(envValues, projectRoot);
|
|
316
|
-
return (`✅ endpoint「${
|
|
366
|
+
return (`✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.${spec.adapterKey}.endpoints` +
|
|
317
367
|
`${Object.keys(envValues).length > 0 ? '(凭据已写入 .env)' : ''}。\n` +
|
|
318
368
|
'⚠️ 需重启 zhin 后新 endpoint 才会生效。');
|
|
319
369
|
}
|
|
@@ -322,10 +372,10 @@ export function addEndpointFromKeyValues(spec, name, args, projectRoot) {
|
|
|
322
372
|
}
|
|
323
373
|
}
|
|
324
374
|
/** remove 的完整业务逻辑:从 yaml 移除;返回回复文本。 */
|
|
325
|
-
export function
|
|
326
|
-
const trimmed =
|
|
375
|
+
export function removeEndpointById(spec, id, projectRoot) {
|
|
376
|
+
const trimmed = id.trim();
|
|
327
377
|
if (!trimmed)
|
|
328
|
-
return `用法:${spec.adapterKey}.endpoint remove <
|
|
378
|
+
return `用法:${spec.adapterKey}.endpoint remove <id>`;
|
|
329
379
|
try {
|
|
330
380
|
const { removed, filePath } = removeEndpointFromConfig(spec.adapterKey, trimmed, projectRoot);
|
|
331
381
|
if (!removed) {
|
|
@@ -355,32 +405,32 @@ export function createEndpointCommands(spec, defineCommand) {
|
|
|
355
405
|
add: defineCommand({
|
|
356
406
|
description: spec.addDescription
|
|
357
407
|
?? `手动添加 ${spec.adapterDisplayName} endpoint(凭据写入 .env 并追加到 zhin.config.yml,重启生效)`,
|
|
358
|
-
params: {
|
|
408
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
359
409
|
execute({ config, input, params, args, use }) {
|
|
360
410
|
if (!isEndpointOperator(config, input))
|
|
361
411
|
return forbidden;
|
|
362
|
-
const
|
|
412
|
+
const id = endpointIdParam(params);
|
|
363
413
|
if (spec.bindFlow) {
|
|
364
414
|
return spec.bindFlow({
|
|
365
|
-
|
|
366
|
-
reply:
|
|
415
|
+
id,
|
|
416
|
+
reply: createDurableEndpointCommandReply(input, use),
|
|
367
417
|
config,
|
|
368
418
|
input,
|
|
369
419
|
use,
|
|
370
420
|
});
|
|
371
421
|
}
|
|
372
|
-
if (!
|
|
422
|
+
if (!id)
|
|
373
423
|
return addUsage(spec);
|
|
374
|
-
return addEndpointFromKeyValues(spec,
|
|
424
|
+
return addEndpointFromKeyValues(spec, id, args);
|
|
375
425
|
},
|
|
376
426
|
}),
|
|
377
427
|
remove: defineCommand({
|
|
378
428
|
description: `从 zhin.config.yml 的 plugins.${spec.adapterKey}.endpoints 移除指定 endpoint(重启生效)`,
|
|
379
|
-
params: {
|
|
429
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
380
430
|
execute({ config, input, params }) {
|
|
381
431
|
if (!isEndpointOperator(config, input))
|
|
382
432
|
return forbidden;
|
|
383
|
-
return
|
|
433
|
+
return removeEndpointById(spec, String(params.id ?? ''));
|
|
384
434
|
},
|
|
385
435
|
}),
|
|
386
436
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ConversationRef, MessageRef } from '@zhin.js/im-contract';
|
|
2
2
|
/**
|
|
3
3
|
* Transport-neutral control plane for a live endpoint.
|
|
4
4
|
*
|
|
@@ -7,24 +7,20 @@ import { type ConversationTarget, type MessageTarget } from '@zhin.js/im-contrac
|
|
|
7
7
|
* to know a protocol's method names or identifier layout.
|
|
8
8
|
*/
|
|
9
9
|
export interface EndpointControl {
|
|
10
|
-
recall?(message:
|
|
11
|
-
edit?(message:
|
|
12
|
-
addReaction?(message:
|
|
10
|
+
recall?(message: MessageRef): Promise<void>;
|
|
11
|
+
edit?(message: MessageRef, content: unknown): Promise<string | null>;
|
|
12
|
+
addReaction?(message: MessageRef, emoji: string, hint?: {
|
|
13
13
|
readonly sceneType?: string;
|
|
14
14
|
readonly channelId?: string;
|
|
15
15
|
}): Promise<string | null>;
|
|
16
|
-
removeReaction?(message:
|
|
17
|
-
typing?(conversation:
|
|
16
|
+
removeReaction?(message: MessageRef, reactionId: string): Promise<void>;
|
|
17
|
+
typing?(conversation: ConversationRef, active?: boolean): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
export interface EndpointWithControl {
|
|
20
20
|
readonly control?: EndpointControl;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* Adapter only: it is a migration bridge for existing protocol endpoints, not
|
|
25
|
-
* an IM Core extension point. New adapters must expose `control` directly.
|
|
26
|
-
*/
|
|
27
|
-
export declare function resolveEndpointControl(endpoint: unknown): EndpointControl | undefined;
|
|
22
|
+
/** Reads the canonical control port without probing protocol-specific methods. */
|
|
23
|
+
export declare function endpointControlOf(endpoint: unknown): EndpointControl | undefined;
|
|
28
24
|
/** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
|
|
29
25
|
export declare function hasExplicitEndpointOperation(endpoint: unknown, operation: 'recall' | 'edit' | 'reaction' | 'typing'): boolean;
|
|
30
26
|
/** Rejects a declaration that cannot be fulfilled by the explicit control port. */
|
package/lib/endpoint-control.js
CHANGED
|
@@ -1,54 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Resolves the public control port. The legacy branch is deliberately kept in
|
|
4
|
-
* Adapter only: it is a migration bridge for existing protocol endpoints, not
|
|
5
|
-
* an IM Core extension point. New adapters must expose `control` directly.
|
|
6
|
-
*/
|
|
7
|
-
export function resolveEndpointControl(endpoint) {
|
|
1
|
+
/** Reads the canonical control port without probing protocol-specific methods. */
|
|
2
|
+
export function endpointControlOf(endpoint) {
|
|
8
3
|
if (!endpoint || typeof endpoint !== 'object')
|
|
9
4
|
return undefined;
|
|
10
5
|
const explicit = endpoint.control;
|
|
11
|
-
|
|
12
|
-
return explicit;
|
|
13
|
-
const legacy = endpoint;
|
|
14
|
-
const recall = legacy.recallMessage ?? legacy.$recallMessage;
|
|
15
|
-
const edit = legacy.editMessage ?? legacy.$editMessage;
|
|
16
|
-
const addReaction = legacy.addReaction ?? legacy.$addReaction;
|
|
17
|
-
const removeReaction = legacy.removeReaction ?? legacy.$removeReaction;
|
|
18
|
-
const typing = legacy.typing ?? legacy.$typing;
|
|
19
|
-
if (!recall && !edit && !addReaction && !removeReaction && !typing)
|
|
20
|
-
return undefined;
|
|
21
|
-
return Object.freeze({
|
|
22
|
-
...(recall
|
|
23
|
-
? { recall: (message) => recall.call(endpoint, legacyMessageId(message)) }
|
|
24
|
-
: {}),
|
|
25
|
-
...(edit
|
|
26
|
-
? {
|
|
27
|
-
edit: (message, content) => edit.call(endpoint, legacyMessageId(message), content),
|
|
28
|
-
}
|
|
29
|
-
: {}),
|
|
30
|
-
...(addReaction
|
|
31
|
-
? {
|
|
32
|
-
addReaction: (message, emoji, hint) => addReaction.call(endpoint, legacyMessageId(message), emoji, hint),
|
|
33
|
-
}
|
|
34
|
-
: {}),
|
|
35
|
-
...(removeReaction
|
|
36
|
-
? {
|
|
37
|
-
removeReaction: (message, reactionId) => removeReaction.call(endpoint, legacyMessageId(message), reactionId),
|
|
38
|
-
}
|
|
39
|
-
: {}),
|
|
40
|
-
...(typing
|
|
41
|
-
? {
|
|
42
|
-
typing: (conversation, active) => typing.call(endpoint, legacyConversationTarget(conversation), active),
|
|
43
|
-
}
|
|
44
|
-
: {}),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function legacyMessageId(message) {
|
|
48
|
-
return typeof message === 'string' ? message : formatLegacyMessageRef(message);
|
|
49
|
-
}
|
|
50
|
-
function legacyConversationTarget(conversation) {
|
|
51
|
-
return typeof conversation === 'string' ? conversation : formatLegacyConversationRef(conversation);
|
|
6
|
+
return explicit && typeof explicit === 'object' ? explicit : undefined;
|
|
52
7
|
}
|
|
53
8
|
/** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
|
|
54
9
|
export function hasExplicitEndpointOperation(endpoint, operation) {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* 迁移指引(以 napcat/milky/onebot WS endpoint 为例):
|
|
21
21
|
* 1. 删除 #started / #stopping / #reconnectTimer / #heartbeatTimer / opened 旗标,
|
|
22
|
-
* 构造器里 `this.#lifecycle = createEndpointLifecycle({ name: config.
|
|
22
|
+
* 构造器里 `this.#lifecycle = createEndpointLifecycle({ name: config.id, reconnect, heartbeat })`。
|
|
23
23
|
* 2. `start()` 改为:
|
|
24
24
|
* ```ts
|
|
25
25
|
* this.#unregisterAgent = registerXxxAgentEndpoint(name, this); // agent 注册仍在适配器侧
|
package/lib/provider.js
CHANGED
|
@@ -16,29 +16,17 @@ const adapterFeature = defineFeatureProvider({
|
|
|
16
16
|
},
|
|
17
17
|
runtime: {
|
|
18
18
|
async project(slots, context) {
|
|
19
|
-
const index = await AdapterIndex.create(slots, context.snapshot);
|
|
20
|
-
let previousIndex;
|
|
19
|
+
const index = await AdapterIndex.create(slots, context.snapshot, context.signal);
|
|
21
20
|
return {
|
|
22
21
|
value: index,
|
|
23
22
|
dispose: () => index.stop(),
|
|
24
23
|
handoff: {
|
|
25
|
-
|
|
26
|
-
previousIndex = previousAdapterIndex(previous);
|
|
27
|
-
return previousIndex?.close();
|
|
28
|
-
},
|
|
29
|
-
activateNext: () => index.start(),
|
|
24
|
+
activateNext: (signal) => index.activate(signal),
|
|
30
25
|
deactivateNext: () => index.stop(),
|
|
31
|
-
resumePrevious() {
|
|
32
|
-
previousIndex?.open();
|
|
33
|
-
},
|
|
34
|
-
openNext: () => index.open(),
|
|
35
26
|
},
|
|
36
27
|
};
|
|
37
28
|
},
|
|
38
29
|
},
|
|
39
30
|
});
|
|
40
|
-
function previousAdapterIndex(snapshot) {
|
|
41
|
-
return snapshot.projections.get(adapterFeatureId);
|
|
42
|
-
}
|
|
43
31
|
export { adapterFeature };
|
|
44
32
|
export default adapterFeature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Convention-based Adapter and Endpoint Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"yaml": "^2.9.0",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
22
|
-
"@zhin.js/im-contract": "1.0.
|
|
23
|
-
"@zhin.js/logger": "1.0.
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.9",
|
|
22
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
23
|
+
"@zhin.js/logger": "1.0.76",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.6"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"@zhin.js/command": "1.0.
|
|
29
|
+
"@zhin.js/command": "1.0.12"
|
|
30
30
|
},
|
|
31
31
|
"zhin": {
|
|
32
32
|
"protocol": 1,
|