@zhin.js/adapter 1.1.5 → 1.1.7
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/lib/adapter-index.d.ts +2 -2
- package/lib/adapter-index.js +33 -33
- package/lib/endpoint-commands.d.ts +17 -12
- package/lib/endpoint-commands.js +88 -38
- package/lib/endpoint-control.d.ts +6 -2
- package/lib/endpoint-control.js +5 -2
- package/lib/endpoint-lifecycle.js +1 -1
- package/package.json +6 -6
- package/src/adapter-index.ts +35 -35
- package/src/endpoint-commands.ts +116 -45
- package/src/endpoint-control.ts +8 -23
- package/src/endpoint-lifecycle.ts +1 -1
package/lib/adapter-index.d.ts
CHANGED
|
@@ -31,11 +31,11 @@ export declare class AdapterIndex {
|
|
|
31
31
|
* Resolve a Console `$adapter` + `$endpoint` pair to a capability id.
|
|
32
32
|
* Matches local name, capability id, or owner path segments.
|
|
33
33
|
*/
|
|
34
|
-
resolve(adapter: string,
|
|
34
|
+
resolve(adapter: string, endpointKey: string): CapabilityId | undefined;
|
|
35
35
|
/**
|
|
36
36
|
* Resolve a live EndpointInstance for Host-side side channels (reactions, etc.).
|
|
37
37
|
*/
|
|
38
|
-
instance(adapter: string,
|
|
38
|
+
instance(adapter: string, endpointKey: string): EndpointInstance | undefined;
|
|
39
39
|
owner(id: CapabilityId): PluginId;
|
|
40
40
|
/**
|
|
41
41
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
package/lib/adapter-index.js
CHANGED
|
@@ -28,13 +28,13 @@ export class AdapterIndex {
|
|
|
28
28
|
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
29
29
|
const endpoint = await createEndpointSoft(slot, snapshot, expansion);
|
|
30
30
|
if (endpoint.unconfigured)
|
|
31
|
-
unconfigured.push(expansion.
|
|
31
|
+
unconfigured.push(expansion.endpointId);
|
|
32
32
|
records.push({
|
|
33
33
|
id: expansion.id,
|
|
34
34
|
owner: slot.owner,
|
|
35
|
-
// 展开模式下 record name 即 endpoint
|
|
36
|
-
// 保证 Console 展示与 resolve/instance 按 entry
|
|
37
|
-
name: expansion.
|
|
35
|
+
// 展开模式下 record name 即 endpoint id(entry.id),
|
|
36
|
+
// 保证 Console 展示与 resolve/instance 按 entry id 命中唯一 record
|
|
37
|
+
name: expansion.endpointId,
|
|
38
38
|
source: slot.source,
|
|
39
39
|
capabilities: slot.definition.capabilities,
|
|
40
40
|
endpoint: endpoint.instance,
|
|
@@ -85,21 +85,21 @@ export class AdapterIndex {
|
|
|
85
85
|
* Resolve a Console `$adapter` + `$endpoint` pair to a capability id.
|
|
86
86
|
* Matches local name, capability id, or owner path segments.
|
|
87
87
|
*/
|
|
88
|
-
resolve(adapter,
|
|
89
|
-
const matches = this.#order.filter((record) => matchesEndpoint(record, adapter,
|
|
88
|
+
resolve(adapter, endpointKey) {
|
|
89
|
+
const matches = this.#order.filter((record) => matchesEndpoint(record, adapter, endpointKey));
|
|
90
90
|
if (matches.length === 1)
|
|
91
91
|
return matches[0]?.id;
|
|
92
92
|
if (matches.length === 0)
|
|
93
93
|
return undefined;
|
|
94
|
-
// Prefer exact localName ===
|
|
95
|
-
const exact = matches.find((record) => record.name ===
|
|
94
|
+
// Prefer exact localName === endpointKey when ambiguous.
|
|
95
|
+
const exact = matches.find((record) => record.name === endpointKey);
|
|
96
96
|
return exact?.id ?? matches[0]?.id;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* Resolve a live EndpointInstance for Host-side side channels (reactions, etc.).
|
|
100
100
|
*/
|
|
101
|
-
instance(adapter,
|
|
102
|
-
const id = this.resolve(adapter,
|
|
101
|
+
instance(adapter, endpointKey) {
|
|
102
|
+
const id = this.resolve(adapter, endpointKey);
|
|
103
103
|
if (!id)
|
|
104
104
|
return undefined;
|
|
105
105
|
return this.#records.get(id)?.endpoint;
|
|
@@ -266,7 +266,7 @@ export function isAdapterIndex(value) {
|
|
|
266
266
|
return !!value && typeof value === 'object'
|
|
267
267
|
&& value.$projection === 'zhin.adapter-index/1';
|
|
268
268
|
}
|
|
269
|
-
function matchesEndpoint(record, adapter,
|
|
269
|
+
function matchesEndpoint(record, adapter, endpointKey) {
|
|
270
270
|
// 消息上的 $adapter 是 CapabilityId 的 localName 段(多 endpoint 展开后形如
|
|
271
271
|
// `icqq~8596238`)。CapabilityId 段分隔符是 \0(owner\0feature\0localName),
|
|
272
272
|
// 不能用 `/` 去 endsWith,否则永远匹配不上(endpoint not found)。
|
|
@@ -281,10 +281,10 @@ function matchesEndpoint(record, adapter, endpointId) {
|
|
|
281
281
|
// activity-feedback resolve with that id; slot.localName alone is not enough
|
|
282
282
|
// when multiple plugin instances share localName "icqq".
|
|
283
283
|
const liveName = endpointLiveName(record.endpoint);
|
|
284
|
-
const endpointOk = record.name ===
|
|
285
|
-
|| record.id ===
|
|
286
|
-
|| record.id.endsWith(`/${
|
|
287
|
-
|| (liveName !== undefined && liveName ===
|
|
284
|
+
const endpointOk = record.name === endpointKey
|
|
285
|
+
|| record.id === endpointKey
|
|
286
|
+
|| record.id.endsWith(`/${endpointKey}`)
|
|
287
|
+
|| (liveName !== undefined && liveName === endpointKey);
|
|
288
288
|
return adapterOk && endpointOk;
|
|
289
289
|
}
|
|
290
290
|
function endpointLiveName(endpoint) {
|
|
@@ -316,7 +316,7 @@ function isUnconfiguredError(error) {
|
|
|
316
316
|
&& /requires|not configured|missing|未配置|缺少/i.test(error.message));
|
|
317
317
|
}
|
|
318
318
|
/**
|
|
319
|
-
* 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{
|
|
319
|
+
* 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{id, ...覆盖}]` 时
|
|
320
320
|
* 按数组一一创建 endpoint(基础配置为实例 config 去掉 `endpoints` 键,逐项合并),
|
|
321
321
|
* 否则按实例 config 创建单个 endpoint(历史行为)。
|
|
322
322
|
*/
|
|
@@ -325,53 +325,53 @@ function expandEndpointConfigs(slot, snapshot) {
|
|
|
325
325
|
const raw = config?.endpoints;
|
|
326
326
|
const entries = Array.isArray(raw)
|
|
327
327
|
? raw.filter((entry) => !!entry && typeof entry === 'object'
|
|
328
|
-
&& typeof entry.
|
|
329
|
-
&& entry.
|
|
328
|
+
&& typeof entry.id === 'string'
|
|
329
|
+
&& entry.id.length > 0)
|
|
330
330
|
: [];
|
|
331
331
|
if (entries.length === 0) {
|
|
332
332
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
333
333
|
logger.warn(formatCompact({
|
|
334
334
|
op: 'adapter_endpoints_entries_dropped',
|
|
335
335
|
id: slot.id,
|
|
336
|
-
reason: 'every endpoints entry is missing a non-empty string
|
|
336
|
+
reason: 'every endpoints entry is missing a non-empty string id',
|
|
337
337
|
}));
|
|
338
338
|
}
|
|
339
|
-
return Object.freeze([{ id: slot.id,
|
|
339
|
+
return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
|
|
340
340
|
}
|
|
341
341
|
// `~` 是 record id 的分隔符、\0 是 CapabilityId 的分隔符,混入会破坏解析
|
|
342
342
|
const valid = entries.filter((entry) => {
|
|
343
|
-
if (/[~\0]/u.test(entry.
|
|
343
|
+
if (/[~\0]/u.test(entry.id)) {
|
|
344
344
|
logger.warn(formatCompact({
|
|
345
|
-
op: '
|
|
345
|
+
op: 'adapter_endpoint_id_invalid',
|
|
346
346
|
id: slot.id,
|
|
347
|
-
|
|
347
|
+
endpointId: entry.id,
|
|
348
348
|
}));
|
|
349
349
|
return false;
|
|
350
350
|
}
|
|
351
351
|
return true;
|
|
352
352
|
});
|
|
353
|
-
//
|
|
353
|
+
// 重 id 会让 #records 覆盖与 #order/resolve 三者不一致;保留首个并告警
|
|
354
354
|
const seen = new Set();
|
|
355
355
|
const deduped = valid.filter((entry) => {
|
|
356
|
-
if (seen.has(entry.
|
|
356
|
+
if (seen.has(entry.id)) {
|
|
357
357
|
logger.warn(formatCompact({
|
|
358
|
-
op: '
|
|
358
|
+
op: 'adapter_endpoint_id_duplicate',
|
|
359
359
|
id: slot.id,
|
|
360
|
-
|
|
360
|
+
endpointId: entry.id,
|
|
361
361
|
}));
|
|
362
362
|
return false;
|
|
363
363
|
}
|
|
364
|
-
seen.add(entry.
|
|
364
|
+
seen.add(entry.id);
|
|
365
365
|
return true;
|
|
366
366
|
});
|
|
367
367
|
if (deduped.length === 0) {
|
|
368
|
-
return Object.freeze([{ id: slot.id,
|
|
368
|
+
return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
|
|
369
369
|
}
|
|
370
370
|
const { endpoints: _drop, ...base } = (config ?? {});
|
|
371
371
|
return Object.freeze(deduped.map((entry) => Object.freeze({
|
|
372
|
-
id: `${slot.id}~${entry.
|
|
373
|
-
|
|
374
|
-
config: Object.freeze({ ...base, ...entry,
|
|
372
|
+
id: `${slot.id}~${entry.id}`,
|
|
373
|
+
endpointId: entry.id,
|
|
374
|
+
config: Object.freeze({ ...base, ...entry, id: entry.id }),
|
|
375
375
|
})));
|
|
376
376
|
}
|
|
377
377
|
async function createEndpointSoft(slot, snapshot, expansion) {
|
|
@@ -394,7 +394,7 @@ async function createEndpointSoft(slot, snapshot, expansion) {
|
|
|
394
394
|
log(formatCompact({
|
|
395
395
|
op: 'adapter_create_soft_fail',
|
|
396
396
|
id: expansion?.id ?? slot.id,
|
|
397
|
-
name: expansion?.
|
|
397
|
+
name: expansion?.endpointId ?? slot.localName,
|
|
398
398
|
error: message,
|
|
399
399
|
}));
|
|
400
400
|
return {
|
|
@@ -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,5 @@
|
|
|
1
1
|
import { type ConversationTarget, type MessageTarget } from '@zhin.js/im-contract';
|
|
2
|
+
export type { LegacyEndpointControlSurface } from '@zhin.js/im-contract';
|
|
2
3
|
/**
|
|
3
4
|
* Transport-neutral control plane for a live endpoint.
|
|
4
5
|
*
|
|
@@ -21,8 +22,11 @@ export interface EndpointWithControl {
|
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
24
|
* Resolves the public control port. The legacy branch is deliberately kept in
|
|
24
|
-
* Adapter only: it is a migration bridge for existing protocol
|
|
25
|
-
*
|
|
25
|
+
* Adapter only: it is a migration bridge for existing classic protocol
|
|
26
|
+
* endpoints (`LegacyEndpointControlSurface` lives in `@zhin.js/im-contract`),
|
|
27
|
+
* not an IM Core extension point. New adapters must expose `control` directly.
|
|
28
|
+
* 下线条件:classic Plugin 轨下线后,legacy 分支与 LegacyEndpointControlSurface
|
|
29
|
+
* 一并删除。
|
|
26
30
|
*/
|
|
27
31
|
export declare function resolveEndpointControl(endpoint: unknown): EndpointControl | undefined;
|
|
28
32
|
/** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
|
package/lib/endpoint-control.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { formatLegacyConversationRef, formatLegacyMessageRef, } from '@zhin.js/im-contract';
|
|
2
2
|
/**
|
|
3
3
|
* Resolves the public control port. The legacy branch is deliberately kept in
|
|
4
|
-
* Adapter only: it is a migration bridge for existing protocol
|
|
5
|
-
*
|
|
4
|
+
* Adapter only: it is a migration bridge for existing classic protocol
|
|
5
|
+
* endpoints (`LegacyEndpointControlSurface` lives in `@zhin.js/im-contract`),
|
|
6
|
+
* not an IM Core extension point. New adapters must expose `control` directly.
|
|
7
|
+
* 下线条件:classic Plugin 轨下线后,legacy 分支与 LegacyEndpointControlSurface
|
|
8
|
+
* 一并删除。
|
|
6
9
|
*/
|
|
7
10
|
export function resolveEndpointControl(endpoint) {
|
|
8
11
|
if (!endpoint || typeof endpoint !== 'object')
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
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.8",
|
|
22
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
23
|
+
"@zhin.js/logger": "1.0.76",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
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.9"
|
|
30
30
|
},
|
|
31
31
|
"zhin": {
|
|
32
32
|
"protocol": 1,
|
package/src/adapter-index.ts
CHANGED
|
@@ -89,13 +89,13 @@ export class AdapterIndex {
|
|
|
89
89
|
for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
90
90
|
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
91
91
|
const endpoint = await createEndpointSoft(slot, snapshot, expansion);
|
|
92
|
-
if (endpoint.unconfigured) unconfigured.push(expansion.
|
|
92
|
+
if (endpoint.unconfigured) unconfigured.push(expansion.endpointId);
|
|
93
93
|
records.push({
|
|
94
94
|
id: expansion.id,
|
|
95
95
|
owner: slot.owner,
|
|
96
|
-
// 展开模式下 record name 即 endpoint
|
|
97
|
-
// 保证 Console 展示与 resolve/instance 按 entry
|
|
98
|
-
name: expansion.
|
|
96
|
+
// 展开模式下 record name 即 endpoint id(entry.id),
|
|
97
|
+
// 保证 Console 展示与 resolve/instance 按 entry id 命中唯一 record
|
|
98
|
+
name: expansion.endpointId,
|
|
99
99
|
source: slot.source,
|
|
100
100
|
capabilities: slot.definition.capabilities,
|
|
101
101
|
endpoint: endpoint.instance,
|
|
@@ -155,21 +155,21 @@ export class AdapterIndex {
|
|
|
155
155
|
* Resolve a Console `$adapter` + `$endpoint` pair to a capability id.
|
|
156
156
|
* Matches local name, capability id, or owner path segments.
|
|
157
157
|
*/
|
|
158
|
-
resolve(adapter: string,
|
|
158
|
+
resolve(adapter: string, endpointKey: string): CapabilityId | undefined {
|
|
159
159
|
const matches = this.#order.filter((record) =>
|
|
160
|
-
matchesEndpoint(record, adapter,
|
|
160
|
+
matchesEndpoint(record, adapter, endpointKey));
|
|
161
161
|
if (matches.length === 1) return matches[0]?.id;
|
|
162
162
|
if (matches.length === 0) return undefined;
|
|
163
|
-
// Prefer exact localName ===
|
|
164
|
-
const exact = matches.find((record) => record.name ===
|
|
163
|
+
// Prefer exact localName === endpointKey when ambiguous.
|
|
164
|
+
const exact = matches.find((record) => record.name === endpointKey);
|
|
165
165
|
return exact?.id ?? matches[0]?.id;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
169
|
* Resolve a live EndpointInstance for Host-side side channels (reactions, etc.).
|
|
170
170
|
*/
|
|
171
|
-
instance(adapter: string,
|
|
172
|
-
const id = this.resolve(adapter,
|
|
171
|
+
instance(adapter: string, endpointKey: string): EndpointInstance | undefined {
|
|
172
|
+
const id = this.resolve(adapter, endpointKey);
|
|
173
173
|
if (!id) return undefined;
|
|
174
174
|
return this.#records.get(id)?.endpoint;
|
|
175
175
|
}
|
|
@@ -341,7 +341,7 @@ export function isAdapterIndex(value: unknown): value is AdapterIndex {
|
|
|
341
341
|
function matchesEndpoint(
|
|
342
342
|
record: AdapterRecord,
|
|
343
343
|
adapter: string,
|
|
344
|
-
|
|
344
|
+
endpointKey: string,
|
|
345
345
|
): boolean {
|
|
346
346
|
// 消息上的 $adapter 是 CapabilityId 的 localName 段(多 endpoint 展开后形如
|
|
347
347
|
// `icqq~8596238`)。CapabilityId 段分隔符是 \0(owner\0feature\0localName),
|
|
@@ -357,10 +357,10 @@ function matchesEndpoint(
|
|
|
357
357
|
// activity-feedback resolve with that id; slot.localName alone is not enough
|
|
358
358
|
// when multiple plugin instances share localName "icqq".
|
|
359
359
|
const liveName = endpointLiveName(record.endpoint);
|
|
360
|
-
const endpointOk = record.name ===
|
|
361
|
-
|| record.id ===
|
|
362
|
-
|| record.id.endsWith(`/${
|
|
363
|
-
|| (liveName !== undefined && liveName ===
|
|
360
|
+
const endpointOk = record.name === endpointKey
|
|
361
|
+
|| record.id === endpointKey
|
|
362
|
+
|| record.id.endsWith(`/${endpointKey}`)
|
|
363
|
+
|| (liveName !== undefined && liveName === endpointKey);
|
|
364
364
|
return adapterOk && endpointOk;
|
|
365
365
|
}
|
|
366
366
|
|
|
@@ -397,12 +397,12 @@ function isUnconfiguredError(error: unknown): boolean {
|
|
|
397
397
|
/** 单个实例配置展开的 endpoint 描述(多账号适配器经 `endpoints` 数组声明)。 */
|
|
398
398
|
interface EndpointExpansion {
|
|
399
399
|
readonly id: CapabilityId;
|
|
400
|
-
readonly
|
|
400
|
+
readonly endpointId: string;
|
|
401
401
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
/**
|
|
405
|
-
* 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{
|
|
405
|
+
* 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{id, ...覆盖}]` 时
|
|
406
406
|
* 按数组一一创建 endpoint(基础配置为实例 config 去掉 `endpoints` 键,逐项合并),
|
|
407
407
|
* 否则按实例 config 创建单个 endpoint(历史行为)。
|
|
408
408
|
*/
|
|
@@ -415,55 +415,55 @@ function expandEndpointConfigs(
|
|
|
415
415
|
| undefined;
|
|
416
416
|
const raw = config?.endpoints;
|
|
417
417
|
const entries = Array.isArray(raw)
|
|
418
|
-
? raw.filter((entry): entry is Record<string, unknown> & {
|
|
418
|
+
? raw.filter((entry): entry is Record<string, unknown> & { id: string } =>
|
|
419
419
|
!!entry && typeof entry === 'object'
|
|
420
|
-
&& typeof (entry as {
|
|
421
|
-
&& (entry as {
|
|
420
|
+
&& typeof (entry as { id?: unknown }).id === 'string'
|
|
421
|
+
&& (entry as { id: string }).id.length > 0)
|
|
422
422
|
: [];
|
|
423
423
|
if (entries.length === 0) {
|
|
424
424
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
425
425
|
logger.warn(formatCompact({
|
|
426
426
|
op: 'adapter_endpoints_entries_dropped',
|
|
427
427
|
id: slot.id,
|
|
428
|
-
reason: 'every endpoints entry is missing a non-empty string
|
|
428
|
+
reason: 'every endpoints entry is missing a non-empty string id',
|
|
429
429
|
}));
|
|
430
430
|
}
|
|
431
|
-
return Object.freeze([{ id: slot.id,
|
|
431
|
+
return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
|
|
432
432
|
}
|
|
433
433
|
// `~` 是 record id 的分隔符、\0 是 CapabilityId 的分隔符,混入会破坏解析
|
|
434
434
|
const valid = entries.filter((entry) => {
|
|
435
|
-
if (/[~\0]/u.test(entry.
|
|
435
|
+
if (/[~\0]/u.test(entry.id)) {
|
|
436
436
|
logger.warn(formatCompact({
|
|
437
|
-
op: '
|
|
437
|
+
op: 'adapter_endpoint_id_invalid',
|
|
438
438
|
id: slot.id,
|
|
439
|
-
|
|
439
|
+
endpointId: entry.id,
|
|
440
440
|
}));
|
|
441
441
|
return false;
|
|
442
442
|
}
|
|
443
443
|
return true;
|
|
444
444
|
});
|
|
445
|
-
//
|
|
445
|
+
// 重 id 会让 #records 覆盖与 #order/resolve 三者不一致;保留首个并告警
|
|
446
446
|
const seen = new Set<string>();
|
|
447
447
|
const deduped = valid.filter((entry) => {
|
|
448
|
-
if (seen.has(entry.
|
|
448
|
+
if (seen.has(entry.id)) {
|
|
449
449
|
logger.warn(formatCompact({
|
|
450
|
-
op: '
|
|
450
|
+
op: 'adapter_endpoint_id_duplicate',
|
|
451
451
|
id: slot.id,
|
|
452
|
-
|
|
452
|
+
endpointId: entry.id,
|
|
453
453
|
}));
|
|
454
454
|
return false;
|
|
455
455
|
}
|
|
456
|
-
seen.add(entry.
|
|
456
|
+
seen.add(entry.id);
|
|
457
457
|
return true;
|
|
458
458
|
});
|
|
459
459
|
if (deduped.length === 0) {
|
|
460
|
-
return Object.freeze([{ id: slot.id,
|
|
460
|
+
return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
|
|
461
461
|
}
|
|
462
462
|
const { endpoints: _drop, ...base } = (config ?? {}) as Record<string, unknown>;
|
|
463
463
|
return Object.freeze(deduped.map((entry) => Object.freeze({
|
|
464
|
-
id: `${slot.id}~${entry.
|
|
465
|
-
|
|
466
|
-
config: Object.freeze({ ...base, ...entry,
|
|
464
|
+
id: `${slot.id}~${entry.id}` as CapabilityId,
|
|
465
|
+
endpointId: entry.id,
|
|
466
|
+
config: Object.freeze({ ...base, ...entry, id: entry.id }),
|
|
467
467
|
})));
|
|
468
468
|
}
|
|
469
469
|
|
|
@@ -492,7 +492,7 @@ async function createEndpointSoft(
|
|
|
492
492
|
log(formatCompact({
|
|
493
493
|
op: 'adapter_create_soft_fail',
|
|
494
494
|
id: expansion?.id ?? slot.id,
|
|
495
|
-
name: expansion?.
|
|
495
|
+
name: expansion?.endpointId ?? slot.localName,
|
|
496
496
|
error: message,
|
|
497
497
|
}));
|
|
498
498
|
return {
|
package/src/endpoint-commands.ts
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,13 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import fs from 'node:fs';
|
|
30
30
|
import path from 'node:path';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createToken,
|
|
33
|
+
outboundHostToken,
|
|
34
|
+
type OutboundHost,
|
|
35
|
+
type OutboundSendInput,
|
|
36
|
+
type Token,
|
|
37
|
+
} from '@zhin.js/plugin-runtime';
|
|
32
38
|
import { isMap, isSeq, parseDocument, type YAMLSeq } from 'yaml';
|
|
33
39
|
|
|
34
40
|
// ---------------------------------------------------------------------------
|
|
@@ -54,8 +60,9 @@ export function isEndpointOperator(config: unknown, input: unknown): boolean {
|
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
if (masters.size === 0) return true;
|
|
57
|
-
const
|
|
58
|
-
|
|
63
|
+
const senderRaw = (input as { sender?: { id?: string } | null } | null | undefined)?.sender;
|
|
64
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? senderRaw.id.trim() : '';
|
|
65
|
+
return !!senderId && masters.has(senderId);
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/** add/remove 的拒绝文案(list 只读,不校验)。 */
|
|
@@ -80,18 +87,83 @@ export function extractEndpointCommandReply(input: unknown): EndpointCommandRepl
|
|
|
80
87
|
return async () => undefined;
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* bindFlow 后续状态推送:优先走 OutboundHost(不受 inbound Message reply scope 限制)。
|
|
92
|
+
* 扫码绑定等长流程会在命令结果已送达、`$reply` 已冻结后继续 notify,必须用 durable 出站。
|
|
93
|
+
*/
|
|
94
|
+
export function createDurableEndpointCommandReply(
|
|
95
|
+
input: unknown,
|
|
96
|
+
use: EndpointCommandUse,
|
|
97
|
+
): EndpointCommandReply {
|
|
98
|
+
const scoped = extractEndpointCommandReply(input);
|
|
99
|
+
const target = readOutboundSendTarget(input);
|
|
100
|
+
if (!target) return scoped;
|
|
101
|
+
|
|
102
|
+
return async (text) => {
|
|
103
|
+
let outbound: OutboundHost | undefined;
|
|
104
|
+
try {
|
|
105
|
+
outbound = use(outboundHostToken);
|
|
106
|
+
} catch {
|
|
107
|
+
outbound = undefined;
|
|
108
|
+
}
|
|
109
|
+
if (outbound) {
|
|
110
|
+
await outbound.send({ ...target, content: text });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await scoped(text);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readOutboundSendTarget(input: unknown): Omit<OutboundSendInput, 'content'> | undefined {
|
|
118
|
+
const message = input as {
|
|
119
|
+
conversation?: {
|
|
120
|
+
endpoint?: { id?: unknown; adapter?: unknown };
|
|
121
|
+
kind?: unknown;
|
|
122
|
+
id?: unknown;
|
|
123
|
+
parent?: OutboundSendInput['conversation']['parent'];
|
|
124
|
+
threadId?: unknown;
|
|
125
|
+
};
|
|
126
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
127
|
+
} | null | undefined;
|
|
128
|
+
const conversation = message?.conversation;
|
|
129
|
+
const endpointKeyRaw = conversation?.endpoint?.id;
|
|
130
|
+
const adapterRaw = conversation?.endpoint?.adapter;
|
|
131
|
+
const kind = conversation?.kind;
|
|
132
|
+
const id = conversation?.id;
|
|
133
|
+
if (
|
|
134
|
+
endpointKeyRaw == null
|
|
135
|
+
|| adapterRaw == null
|
|
136
|
+
|| (kind !== 'private' && kind !== 'group' && kind !== 'channel')
|
|
137
|
+
|| typeof id !== 'string'
|
|
138
|
+
|| !id
|
|
139
|
+
) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const live = String(message?.metadata?.endpoint ?? message?.metadata?.endpointKey ?? '').trim();
|
|
143
|
+
return {
|
|
144
|
+
adapter: String(adapterRaw),
|
|
145
|
+
endpointKey: live || String(endpointKeyRaw),
|
|
146
|
+
conversation: {
|
|
147
|
+
kind,
|
|
148
|
+
id,
|
|
149
|
+
parent: conversation.parent,
|
|
150
|
+
threadId: typeof conversation.threadId === 'string' ? conversation.threadId : undefined,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
83
155
|
// ---------------------------------------------------------------------------
|
|
84
156
|
// 运行时状态:adapter create() 注册的 running endpoints
|
|
85
157
|
// ---------------------------------------------------------------------------
|
|
86
158
|
|
|
87
159
|
export interface EndpointRunningInfo {
|
|
88
|
-
readonly
|
|
160
|
+
readonly id: string;
|
|
89
161
|
/** 连接模式(ws / wss / polling / socket-mode …),仅用于 list 展示。 */
|
|
90
162
|
readonly mode?: string;
|
|
91
163
|
}
|
|
92
164
|
|
|
93
165
|
export interface EndpointRuntimeState {
|
|
94
|
-
/** 当前 generation 已成功创建的 endpoint(
|
|
166
|
+
/** 当前 generation 已成功创建的 endpoint(id → 描述) */
|
|
95
167
|
readonly endpoints: Map<string, EndpointRunningInfo>;
|
|
96
168
|
}
|
|
97
169
|
|
|
@@ -127,10 +199,10 @@ function envSlug(text: string): string {
|
|
|
127
199
|
/** 派生 endpoint 凭据的 env 键:`${ADAPTER}_${NAME}_${FIELD}`(如 `TELEGRAM_MY_BOT_TOKEN`) */
|
|
128
200
|
export function buildEndpointEnvKey(
|
|
129
201
|
adapterKey: string,
|
|
130
|
-
|
|
202
|
+
endpointId: string,
|
|
131
203
|
fieldKey: string,
|
|
132
204
|
): string {
|
|
133
|
-
return `${envSlug(adapterKey)}_${envSlug(
|
|
205
|
+
return `${envSlug(adapterKey)}_${envSlug(endpointId)}_${envSlug(fieldKey)}`;
|
|
134
206
|
}
|
|
135
207
|
|
|
136
208
|
function escapeRegExp(text: string): string {
|
|
@@ -170,7 +242,7 @@ export function persistEndpointEnvValues(
|
|
|
170
242
|
// ---------------------------------------------------------------------------
|
|
171
243
|
|
|
172
244
|
export interface ConfiguredEndpointEntry {
|
|
173
|
-
|
|
245
|
+
id: string;
|
|
174
246
|
[key: string]: unknown;
|
|
175
247
|
}
|
|
176
248
|
|
|
@@ -223,14 +295,14 @@ export function listConfiguredEndpoints(
|
|
|
223
295
|
if (!Array.isArray(endpoints)) return [];
|
|
224
296
|
return endpoints.filter(
|
|
225
297
|
(entry): entry is ConfiguredEndpointEntry =>
|
|
226
|
-
!!entry && typeof entry === 'object' && typeof (entry as {
|
|
298
|
+
!!entry && typeof entry === 'object' && typeof (entry as { id?: unknown }).id === 'string',
|
|
227
299
|
);
|
|
228
300
|
}
|
|
229
301
|
|
|
230
|
-
function
|
|
302
|
+
function entryId(item: unknown): string | undefined {
|
|
231
303
|
if (!isMap(item)) return undefined;
|
|
232
|
-
const
|
|
233
|
-
return typeof
|
|
304
|
+
const id = item.get('id');
|
|
305
|
+
return typeof id === 'string' && id ? id : undefined;
|
|
234
306
|
}
|
|
235
307
|
|
|
236
308
|
/**
|
|
@@ -269,7 +341,7 @@ function ensureEndpointsSeq(
|
|
|
269
341
|
return doc.getIn(['plugins', adapterKey, 'endpoints']) as YAMLSeq;
|
|
270
342
|
}
|
|
271
343
|
|
|
272
|
-
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;
|
|
344
|
+
/** 追加 endpoint 到 plugins.<adapterKey>.endpoints;id 已存在时报错 */
|
|
273
345
|
export function addEndpointToConfig(
|
|
274
346
|
adapterKey: string,
|
|
275
347
|
entry: ConfiguredEndpointEntry,
|
|
@@ -277,23 +349,23 @@ export function addEndpointToConfig(
|
|
|
277
349
|
): string {
|
|
278
350
|
const document = readConfigDocument(adapterKey, projectRoot);
|
|
279
351
|
const seq = ensureEndpointsSeq(document.doc, adapterKey);
|
|
280
|
-
if (seq.items.some((item) =>
|
|
281
|
-
throw new Error(`配置中已存在 ${adapterKey} endpoint「${entry.
|
|
352
|
+
if (seq.items.some((item) => entryId(item) === entry.id)) {
|
|
353
|
+
throw new Error(`配置中已存在 ${adapterKey} endpoint「${entry.id}」,可先 ${adapterKey}.endpoint remove ${entry.id} 再重新添加`);
|
|
282
354
|
}
|
|
283
355
|
seq.items.push(document.doc.createNode(entry));
|
|
284
356
|
writeConfigDocument(document);
|
|
285
357
|
return document.filePath;
|
|
286
358
|
}
|
|
287
359
|
|
|
288
|
-
/** 按
|
|
360
|
+
/** 按 id 移除 plugins.<adapterKey>.endpoints 项;不存在返回 false */
|
|
289
361
|
export function removeEndpointFromConfig(
|
|
290
362
|
adapterKey: string,
|
|
291
|
-
|
|
363
|
+
id: string,
|
|
292
364
|
projectRoot?: string,
|
|
293
365
|
): { removed: boolean; filePath: string } {
|
|
294
366
|
const document = readConfigDocument(adapterKey, projectRoot);
|
|
295
367
|
const seq = ensureEndpointsSeq(document.doc, adapterKey);
|
|
296
|
-
const next = seq.items.filter((item) =>
|
|
368
|
+
const next = seq.items.filter((item) => entryId(item) !== id);
|
|
297
369
|
if (next.length === seq.items.length) {
|
|
298
370
|
return { removed: false, filePath: document.filePath };
|
|
299
371
|
}
|
|
@@ -322,9 +394,9 @@ export type EndpointCommandUse = <T>(token: Token<T>) => T;
|
|
|
322
394
|
|
|
323
395
|
/** bindFlow 钩子上下文:接管 add 命令的自定义绑定流程(如 QQ 扫码)。 */
|
|
324
396
|
export interface EndpointBindFlowContext {
|
|
325
|
-
/** 命令参数
|
|
326
|
-
readonly
|
|
327
|
-
/** 向当前会话推送后续状态(二维码刷新 / 成功 /
|
|
397
|
+
/** 命令参数 id(未指定时为 undefined,流程可自行决定终名) */
|
|
398
|
+
readonly id?: string;
|
|
399
|
+
/** 向当前会话推送后续状态(二维码刷新 / 成功 / 失败;走 durable OutboundHost,可在命令 reply scope 结束后调用) */
|
|
328
400
|
readonly reply: EndpointCommandReply;
|
|
329
401
|
readonly config: unknown;
|
|
330
402
|
readonly input: unknown;
|
|
@@ -406,9 +478,9 @@ export interface EndpointCommands<TCommand = EndpointCommandDefinition> {
|
|
|
406
478
|
readonly remove: TCommand;
|
|
407
479
|
}
|
|
408
480
|
|
|
409
|
-
function
|
|
410
|
-
const
|
|
411
|
-
return typeof
|
|
481
|
+
function endpointIdParam(params: Readonly<Record<string, unknown>>): string | undefined {
|
|
482
|
+
const id = params.id;
|
|
483
|
+
return typeof id === 'string' && id.trim() ? id.trim() : undefined;
|
|
412
484
|
}
|
|
413
485
|
|
|
414
486
|
/** list 文案:运行中 + 配置中两段,footer 可选。 */
|
|
@@ -427,7 +499,7 @@ export function formatEndpointList(
|
|
|
427
499
|
lines.push(' (无)');
|
|
428
500
|
} else {
|
|
429
501
|
for (const endpoint of running) {
|
|
430
|
-
lines.push(endpoint.mode ? ` - ${endpoint.
|
|
502
|
+
lines.push(endpoint.mode ? ` - ${endpoint.id}(${endpoint.mode})` : ` - ${endpoint.id}`);
|
|
431
503
|
}
|
|
432
504
|
}
|
|
433
505
|
lines.push(`【配置中的 ${spec.adapterDisplayName} endpoints】(zhin.config.yml → plugins.${spec.adapterKey}.endpoints)`);
|
|
@@ -436,7 +508,7 @@ export function formatEndpointList(
|
|
|
436
508
|
} else {
|
|
437
509
|
for (const entry of source.configured) {
|
|
438
510
|
const detail = spec.describeEntry?.(entry);
|
|
439
|
-
lines.push(detail ? ` - ${entry.
|
|
511
|
+
lines.push(detail ? ` - ${entry.id}(${detail})` : ` - ${entry.id}`);
|
|
440
512
|
}
|
|
441
513
|
}
|
|
442
514
|
if (source.footer) lines.push(source.footer);
|
|
@@ -455,13 +527,13 @@ function addUsage(spec: EndpointCommandsSpec): string {
|
|
|
455
527
|
].filter(Boolean).join(',');
|
|
456
528
|
return marks ? `${field.key}(${marks})` : field.key;
|
|
457
529
|
}).join('、')}`;
|
|
458
|
-
return `用法:${spec.adapterKey}.endpoint add <
|
|
530
|
+
return `用法:${spec.adapterKey}.endpoint add <id> <key=value...>${fieldText}`;
|
|
459
531
|
}
|
|
460
532
|
|
|
461
533
|
/** add(kv 模式)的完整业务逻辑:解析 kv → 凭据写 .env → 追加 yaml;返回回复文本。 */
|
|
462
534
|
export function addEndpointFromKeyValues(
|
|
463
535
|
spec: EndpointCommandsSpec,
|
|
464
|
-
|
|
536
|
+
id: string,
|
|
465
537
|
args: readonly string[],
|
|
466
538
|
projectRoot?: string,
|
|
467
539
|
): string {
|
|
@@ -484,13 +556,13 @@ export function addEndpointFromKeyValues(
|
|
|
484
556
|
if (missing.length > 0) {
|
|
485
557
|
return `缺少必填字段:${missing.map((field) => field.key).join('、')}。${addUsage(spec)}`;
|
|
486
558
|
}
|
|
487
|
-
const entry: ConfiguredEndpointEntry = {
|
|
559
|
+
const entry: ConfiguredEndpointEntry = { id };
|
|
488
560
|
const envValues: Record<string, string> = {};
|
|
489
561
|
for (const field of fields) {
|
|
490
562
|
const value = values.get(field.key);
|
|
491
563
|
if (value === undefined) continue;
|
|
492
564
|
if (field.env) {
|
|
493
|
-
const envKey = buildEndpointEnvKey(spec.adapterKey,
|
|
565
|
+
const envKey = buildEndpointEnvKey(spec.adapterKey, id, field.key);
|
|
494
566
|
envValues[envKey] = value;
|
|
495
567
|
entry[field.key] = `\${${envKey}}`;
|
|
496
568
|
} else {
|
|
@@ -498,11 +570,10 @@ export function addEndpointFromKeyValues(
|
|
|
498
570
|
}
|
|
499
571
|
}
|
|
500
572
|
try {
|
|
501
|
-
// 先写配置(重名等校验失败时不留孤儿 .env 键),再落 .env 凭据
|
|
502
573
|
const filePath = addEndpointToConfig(spec.adapterKey, entry, projectRoot);
|
|
503
574
|
if (Object.keys(envValues).length > 0) persistEndpointEnvValues(envValues, projectRoot);
|
|
504
575
|
return (
|
|
505
|
-
`✅ endpoint「${
|
|
576
|
+
`✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.${spec.adapterKey}.endpoints` +
|
|
506
577
|
`${Object.keys(envValues).length > 0 ? '(凭据已写入 .env)' : ''}。\n` +
|
|
507
578
|
'⚠️ 需重启 zhin 后新 endpoint 才会生效。'
|
|
508
579
|
);
|
|
@@ -512,13 +583,13 @@ export function addEndpointFromKeyValues(
|
|
|
512
583
|
}
|
|
513
584
|
|
|
514
585
|
/** remove 的完整业务逻辑:从 yaml 移除;返回回复文本。 */
|
|
515
|
-
export function
|
|
586
|
+
export function removeEndpointById(
|
|
516
587
|
spec: Pick<EndpointCommandsSpec, 'adapterKey'>,
|
|
517
|
-
|
|
588
|
+
id: string,
|
|
518
589
|
projectRoot?: string,
|
|
519
590
|
): string {
|
|
520
|
-
const trimmed =
|
|
521
|
-
if (!trimmed) return `用法:${spec.adapterKey}.endpoint remove <
|
|
591
|
+
const trimmed = id.trim();
|
|
592
|
+
if (!trimmed) return `用法:${spec.adapterKey}.endpoint remove <id>`;
|
|
522
593
|
try {
|
|
523
594
|
const { removed, filePath } = removeEndpointFromConfig(spec.adapterKey, trimmed, projectRoot);
|
|
524
595
|
if (!removed) {
|
|
@@ -553,29 +624,29 @@ export function createEndpointCommands<TCommand>(
|
|
|
553
624
|
add: defineCommand({
|
|
554
625
|
description: spec.addDescription
|
|
555
626
|
?? `手动添加 ${spec.adapterDisplayName} endpoint(凭据写入 .env 并追加到 zhin.config.yml,重启生效)`,
|
|
556
|
-
params: {
|
|
627
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
557
628
|
execute({ config, input, params, args, use }) {
|
|
558
629
|
if (!isEndpointOperator(config, input)) return forbidden;
|
|
559
|
-
const
|
|
630
|
+
const id = endpointIdParam(params);
|
|
560
631
|
if (spec.bindFlow) {
|
|
561
632
|
return spec.bindFlow({
|
|
562
|
-
|
|
563
|
-
reply:
|
|
633
|
+
id,
|
|
634
|
+
reply: createDurableEndpointCommandReply(input, use),
|
|
564
635
|
config,
|
|
565
636
|
input,
|
|
566
637
|
use,
|
|
567
638
|
});
|
|
568
639
|
}
|
|
569
|
-
if (!
|
|
570
|
-
return addEndpointFromKeyValues(spec,
|
|
640
|
+
if (!id) return addUsage(spec);
|
|
641
|
+
return addEndpointFromKeyValues(spec, id, args);
|
|
571
642
|
},
|
|
572
643
|
}),
|
|
573
644
|
remove: defineCommand({
|
|
574
645
|
description: `从 zhin.config.yml 的 plugins.${spec.adapterKey}.endpoints 移除指定 endpoint(重启生效)`,
|
|
575
|
-
params: {
|
|
646
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
576
647
|
execute({ config, input, params }) {
|
|
577
648
|
if (!isEndpointOperator(config, input)) return forbidden;
|
|
578
|
-
return
|
|
649
|
+
return removeEndpointById(spec, String(params.id ?? ''));
|
|
579
650
|
},
|
|
580
651
|
}),
|
|
581
652
|
});
|
package/src/endpoint-control.ts
CHANGED
|
@@ -2,9 +2,12 @@ import {
|
|
|
2
2
|
formatLegacyConversationRef,
|
|
3
3
|
formatLegacyMessageRef,
|
|
4
4
|
type ConversationTarget,
|
|
5
|
+
type LegacyEndpointControlSurface,
|
|
5
6
|
type MessageTarget,
|
|
6
7
|
} from '@zhin.js/im-contract';
|
|
7
8
|
|
|
9
|
+
export type { LegacyEndpointControlSurface } from '@zhin.js/im-contract';
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Transport-neutral control plane for a live endpoint.
|
|
10
13
|
*
|
|
@@ -28,31 +31,13 @@ export interface EndpointWithControl {
|
|
|
28
31
|
readonly control?: EndpointControl;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
interface LegacyEndpointControlSurface {
|
|
32
|
-
recallMessage?(messageId: string): Promise<void>;
|
|
33
|
-
$recallMessage?(messageId: string): Promise<void>;
|
|
34
|
-
editMessage?(messageId: string, content: unknown): Promise<string | null>;
|
|
35
|
-
$editMessage?(messageId: string, content: unknown): Promise<string | null>;
|
|
36
|
-
addReaction?(
|
|
37
|
-
messageId: string,
|
|
38
|
-
emoji: string,
|
|
39
|
-
hint?: { readonly sceneType?: string; readonly channelId?: string },
|
|
40
|
-
): Promise<string | null>;
|
|
41
|
-
$addReaction?(
|
|
42
|
-
messageId: string,
|
|
43
|
-
emoji: string,
|
|
44
|
-
hint?: { readonly sceneType?: string; readonly channelId?: string },
|
|
45
|
-
): Promise<string | null>;
|
|
46
|
-
removeReaction?(messageId: string, reactionId: string): Promise<void>;
|
|
47
|
-
$removeReaction?(messageId: string, reactionId: string): Promise<void>;
|
|
48
|
-
typing?(target: string, active?: boolean): Promise<void>;
|
|
49
|
-
$typing?(target: string, active?: boolean): Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
34
|
/**
|
|
53
35
|
* Resolves the public control port. The legacy branch is deliberately kept in
|
|
54
|
-
* Adapter only: it is a migration bridge for existing protocol
|
|
55
|
-
*
|
|
36
|
+
* Adapter only: it is a migration bridge for existing classic protocol
|
|
37
|
+
* endpoints (`LegacyEndpointControlSurface` lives in `@zhin.js/im-contract`),
|
|
38
|
+
* not an IM Core extension point. New adapters must expose `control` directly.
|
|
39
|
+
* 下线条件:classic Plugin 轨下线后,legacy 分支与 LegacyEndpointControlSurface
|
|
40
|
+
* 一并删除。
|
|
56
41
|
*/
|
|
57
42
|
export function resolveEndpointControl(endpoint: unknown): EndpointControl | undefined {
|
|
58
43
|
if (!endpoint || typeof endpoint !== 'object') return undefined;
|
|
@@ -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 注册仍在适配器侧
|