@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
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
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatLegacyConversationRef,
|
|
3
|
-
formatLegacyMessageRef,
|
|
4
|
-
type ConversationTarget,
|
|
5
|
-
type MessageTarget,
|
|
6
|
-
} from '@zhin.js/im-contract';
|
|
1
|
+
import type { ConversationRef, MessageRef } from '@zhin.js/im-contract';
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* Transport-neutral control plane for a live endpoint.
|
|
@@ -13,100 +8,26 @@ import {
|
|
|
13
8
|
* to know a protocol's method names or identifier layout.
|
|
14
9
|
*/
|
|
15
10
|
export interface EndpointControl {
|
|
16
|
-
recall?(message:
|
|
17
|
-
edit?(message:
|
|
11
|
+
recall?(message: MessageRef): Promise<void>;
|
|
12
|
+
edit?(message: MessageRef, content: unknown): Promise<string | null>;
|
|
18
13
|
addReaction?(
|
|
19
|
-
message:
|
|
14
|
+
message: MessageRef,
|
|
20
15
|
emoji: string,
|
|
21
16
|
hint?: { readonly sceneType?: string; readonly channelId?: string },
|
|
22
17
|
): Promise<string | null>;
|
|
23
|
-
removeReaction?(message:
|
|
24
|
-
typing?(conversation:
|
|
18
|
+
removeReaction?(message: MessageRef, reactionId: string): Promise<void>;
|
|
19
|
+
typing?(conversation: ConversationRef, active?: boolean): Promise<void>;
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
export interface EndpointWithControl {
|
|
28
23
|
readonly control?: EndpointControl;
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
/**
|
|
53
|
-
* Resolves the public control port. The legacy branch is deliberately kept in
|
|
54
|
-
* Adapter only: it is a migration bridge for existing protocol endpoints, not
|
|
55
|
-
* an IM Core extension point. New adapters must expose `control` directly.
|
|
56
|
-
*/
|
|
57
|
-
export function resolveEndpointControl(endpoint: unknown): EndpointControl | undefined {
|
|
26
|
+
/** Reads the canonical control port without probing protocol-specific methods. */
|
|
27
|
+
export function endpointControlOf(endpoint: unknown): EndpointControl | undefined {
|
|
58
28
|
if (!endpoint || typeof endpoint !== 'object') return undefined;
|
|
59
29
|
const explicit = (endpoint as EndpointWithControl).control;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const legacy = endpoint as LegacyEndpointControlSurface;
|
|
63
|
-
const recall = legacy.recallMessage ?? legacy.$recallMessage;
|
|
64
|
-
const edit = legacy.editMessage ?? legacy.$editMessage;
|
|
65
|
-
const addReaction = legacy.addReaction ?? legacy.$addReaction;
|
|
66
|
-
const removeReaction = legacy.removeReaction ?? legacy.$removeReaction;
|
|
67
|
-
const typing = legacy.typing ?? legacy.$typing;
|
|
68
|
-
if (!recall && !edit && !addReaction && !removeReaction && !typing) return undefined;
|
|
69
|
-
|
|
70
|
-
return Object.freeze({
|
|
71
|
-
...(recall
|
|
72
|
-
? { recall: (message: MessageTarget) => recall.call(endpoint, legacyMessageId(message)) }
|
|
73
|
-
: {}),
|
|
74
|
-
...(edit
|
|
75
|
-
? {
|
|
76
|
-
edit: (message: MessageTarget, content: unknown) =>
|
|
77
|
-
edit.call(endpoint, legacyMessageId(message), content),
|
|
78
|
-
}
|
|
79
|
-
: {}),
|
|
80
|
-
...(addReaction
|
|
81
|
-
? {
|
|
82
|
-
addReaction: (
|
|
83
|
-
message: MessageTarget,
|
|
84
|
-
emoji: string,
|
|
85
|
-
hint?: { readonly sceneType?: string; readonly channelId?: string },
|
|
86
|
-
) => addReaction.call(endpoint, legacyMessageId(message), emoji, hint),
|
|
87
|
-
}
|
|
88
|
-
: {}),
|
|
89
|
-
...(removeReaction
|
|
90
|
-
? {
|
|
91
|
-
removeReaction: (message: MessageTarget, reactionId: string) =>
|
|
92
|
-
removeReaction.call(endpoint, legacyMessageId(message), reactionId),
|
|
93
|
-
}
|
|
94
|
-
: {}),
|
|
95
|
-
...(typing
|
|
96
|
-
? {
|
|
97
|
-
typing: (conversation: ConversationTarget, active?: boolean) =>
|
|
98
|
-
typing.call(endpoint, legacyConversationTarget(conversation), active),
|
|
99
|
-
}
|
|
100
|
-
: {}),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function legacyMessageId(message: MessageTarget): string {
|
|
105
|
-
return typeof message === 'string' ? message : formatLegacyMessageRef(message);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function legacyConversationTarget(conversation: ConversationTarget): string {
|
|
109
|
-
return typeof conversation === 'string' ? conversation : formatLegacyConversationRef(conversation);
|
|
30
|
+
return explicit && typeof explicit === 'object' ? explicit : undefined;
|
|
110
31
|
}
|
|
111
32
|
|
|
112
33
|
/** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
|
|
@@ -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/src/provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { featureId
|
|
1
|
+
import { featureId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { defineFeatureProvider, typeScriptModules } from '@zhin.js/feature-kit';
|
|
3
3
|
import { AdapterIndex } from './adapter-index.js';
|
|
4
4
|
import { parseAdapterDefinition } from './definition.js';
|
|
@@ -18,31 +18,18 @@ const adapterFeature = defineFeatureProvider({
|
|
|
18
18
|
},
|
|
19
19
|
runtime: {
|
|
20
20
|
async project(slots, context) {
|
|
21
|
-
const index = await AdapterIndex.create(slots, context.snapshot);
|
|
22
|
-
let previousIndex: AdapterIndex | undefined;
|
|
21
|
+
const index = await AdapterIndex.create(slots, context.snapshot, context.signal);
|
|
23
22
|
return {
|
|
24
23
|
value: index,
|
|
25
24
|
dispose: () => index.stop(),
|
|
26
25
|
handoff: {
|
|
27
|
-
|
|
28
|
-
previousIndex = previousAdapterIndex(previous);
|
|
29
|
-
return previousIndex?.close();
|
|
30
|
-
},
|
|
31
|
-
activateNext: () => index.start(),
|
|
26
|
+
activateNext: (signal) => index.activate(signal),
|
|
32
27
|
deactivateNext: () => index.stop(),
|
|
33
|
-
resumePrevious() {
|
|
34
|
-
previousIndex?.open();
|
|
35
|
-
},
|
|
36
|
-
openNext: () => index.open(),
|
|
37
28
|
},
|
|
38
29
|
};
|
|
39
30
|
},
|
|
40
31
|
},
|
|
41
32
|
});
|
|
42
33
|
|
|
43
|
-
function previousAdapterIndex(snapshot: RuntimeSnapshot): AdapterIndex | undefined {
|
|
44
|
-
return snapshot.projections.get(adapterFeatureId) as AdapterIndex | undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
34
|
export { adapterFeature };
|
|
48
35
|
export default adapterFeature;
|