@zhin.js/adapter 1.0.1 → 1.1.1
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 +36 -0
- package/lib/adapter-index.d.ts +8 -1
- package/lib/adapter-index.js +103 -24
- package/lib/definition.d.ts +43 -0
- package/lib/definition.js +42 -0
- package/lib/endpoint-commands.d.ts +127 -0
- package/lib/endpoint-commands.js +385 -0
- package/lib/endpoint-lifecycle.d.ts +68 -0
- package/lib/endpoint-lifecycle.js +333 -0
- package/lib/endpoint-management.d.ts +53 -0
- package/lib/endpoint-management.js +30 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/provider.js +1 -0
- package/package.json +7 -5
- package/src/adapter-index.ts +124 -22
- package/src/definition.ts +107 -0
- package/src/endpoint-commands.ts +559 -0
- package/src/endpoint-lifecycle.ts +422 -0
- package/src/endpoint-management.ts +86 -0
- package/src/index.ts +5 -0
- package/src/provider.ts +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EndpointLifecycle — WS/SSE 长连接端点生命周期基座。
|
|
3
|
+
*
|
|
4
|
+
* 把 napcat / milky / onebot11·12 各自重复实现(且重复犯错)的状态机收敛为一处:
|
|
5
|
+
*
|
|
6
|
+
* 状态机:idle → connecting → open → reconnecting → open … → stopped / closed
|
|
7
|
+
*
|
|
8
|
+
* - start(connectFn):连接失败自动复位回 idle 且不武装重连(start 的 catch 语义);
|
|
9
|
+
* stop-during-connect 竞态时 start() 静默 settle(视为主动停止,不抛错)。
|
|
10
|
+
* - stop():主动断开,清全部定时器、调用已注册的强关函数、唤醒所有竞态等待,绝不重连。
|
|
11
|
+
* - handle.notifyClosed():对端断开(ws close / SSE 流结束)时由适配器调用;
|
|
12
|
+
* 仅在连接曾 open 时才按指数退避 + jitter 武装重连,初始连接失败不武装。
|
|
13
|
+
* - startHeartbeat(fn, interval):心跳 + 看门狗——连续 N 轮无回包(notifyHeartbeatAck
|
|
14
|
+
* 未复位计数)时主动调用 onForceClose 注册的强关函数,由底层 close 事件驱动重连。
|
|
15
|
+
* - 定时器集中管理:重连 timer 与心跳 timer 均在 close / stop / 看门狗触发时清理。
|
|
16
|
+
*
|
|
17
|
+
* 防叠套:重连循环单例(#reconnectRunning),且每次 connect 尝试递增 generation,
|
|
18
|
+
* 陈旧连接句柄的 notifyClosed / onForceClose 一律忽略。
|
|
19
|
+
*
|
|
20
|
+
* 迁移指引(以 napcat/milky/onebot WS endpoint 为例):
|
|
21
|
+
* 1. 删除 #started / #stopping / #reconnectTimer / #heartbeatTimer / opened 旗标,
|
|
22
|
+
* 构造器里 `this.#lifecycle = createEndpointLifecycle({ name: config.name, reconnect, heartbeat })`。
|
|
23
|
+
* 2. `start()` 改为:
|
|
24
|
+
* ```ts
|
|
25
|
+
* this.#unregisterAgent = registerXxxAgentEndpoint(name, this); // agent 注册仍在适配器侧
|
|
26
|
+
* try {
|
|
27
|
+
* await this.#lifecycle.start(async (handle) => {
|
|
28
|
+
* this.#handle = handle; // 供 ws 'close' 回调引用
|
|
29
|
+
* await new Promise<void>((resolve, reject) => {
|
|
30
|
+
* const ws = createWebSocket(...); this.#ws = ws;
|
|
31
|
+
* ws.on('open', () => { this.#lifecycle.startHeartbeat(() => beat(), interval); resolve(); });
|
|
32
|
+
* ws.on('close', (code, reason) => { handle.notifyClosed(...); rejectIfNotSettled(...); });
|
|
33
|
+
* ws.on('error', (err) => rejectIfNotSettled(err));
|
|
34
|
+
* });
|
|
35
|
+
* });
|
|
36
|
+
* } catch (err) {
|
|
37
|
+
* this.#unregisterAgent?.(); this.#unregisterAgent = undefined; // 反注册对称
|
|
38
|
+
* throw err;
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
* start 失败复位由基座保证;agent 注册/反注册是适配器专有依赖,刻意不收入基座。
|
|
42
|
+
* 3. `stop()` 改为:先 `await this.#lifecycle.stop()`(清定时器 + 强关 ws + 竞态 settle),
|
|
43
|
+
* 再做适配器专有清理(rejectAllPending、deduper.clear、agent 反注册)。
|
|
44
|
+
* 4. `handle.onForceClose(() => this.#ws?.close())` 在每次拿到新 socket 后注册,
|
|
45
|
+
* 供心跳看门狗主动断开;ws 'message'/'pong' 回调里调 `notifyHeartbeatAck()` 喂狗。
|
|
46
|
+
* 5. 退避参数由配置映射:reconnect_interval → initialIntervalMs,可按需覆盖
|
|
47
|
+
* multiplier / maxIntervalMs / jitterMs / maxAttempts;重连成功后退避自动复位。
|
|
48
|
+
*/
|
|
49
|
+
// 使用全局定时器(而非 node:timers 导入):vitest fake timers 只接管全局绑定,
|
|
50
|
+
// 这样单测可用 vi.useFakeTimers 驱动退避/心跳。
|
|
51
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
52
|
+
const logger = getLogger('adapter');
|
|
53
|
+
const DEFAULT_RECONNECT = {
|
|
54
|
+
initialIntervalMs: 5_000,
|
|
55
|
+
multiplier: 2,
|
|
56
|
+
maxIntervalMs: 60_000,
|
|
57
|
+
jitterMs: 250,
|
|
58
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
59
|
+
};
|
|
60
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
61
|
+
class EndpointLifecycleImpl {
|
|
62
|
+
#name;
|
|
63
|
+
#reconnect;
|
|
64
|
+
#heartbeat;
|
|
65
|
+
#random;
|
|
66
|
+
#state = 'idle';
|
|
67
|
+
#connect;
|
|
68
|
+
/** 每次 connect 尝试 +1,识别陈旧句柄。 */
|
|
69
|
+
#generation = 0;
|
|
70
|
+
/** 连续重连失败计数,open 成功后复位。 */
|
|
71
|
+
#attempt = 0;
|
|
72
|
+
/** 重连循环单例旗标(防叠套)。 */
|
|
73
|
+
#reconnectRunning = false;
|
|
74
|
+
#reconnectTimer;
|
|
75
|
+
#reconnectWake;
|
|
76
|
+
#heartbeatTimer;
|
|
77
|
+
#heartbeatMisses = 0;
|
|
78
|
+
#forceClose;
|
|
79
|
+
/** stop() 时唤醒的竞态等待(start / 重连中的 connect 尝试)。 */
|
|
80
|
+
#stopWaiters = [];
|
|
81
|
+
constructor(options) {
|
|
82
|
+
this.#name = options.name;
|
|
83
|
+
this.#reconnect = options.reconnect === false
|
|
84
|
+
? false
|
|
85
|
+
: { ...DEFAULT_RECONNECT, ...options.reconnect };
|
|
86
|
+
this.#heartbeat = {
|
|
87
|
+
intervalMs: options.heartbeat?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
88
|
+
watchdogMisses: options.heartbeat?.watchdogMisses ?? 0,
|
|
89
|
+
};
|
|
90
|
+
this.#random = options.random ?? Math.random;
|
|
91
|
+
}
|
|
92
|
+
get state() {
|
|
93
|
+
return this.#state;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 并发安全的状态读取:stop() 可能在任意 await 点并发改写 #state,
|
|
97
|
+
* 经方法调用读取可避免 TS 对字段/getter 的控制流窄化误判(TS2367)。
|
|
98
|
+
*/
|
|
99
|
+
#currentState() {
|
|
100
|
+
return this.#state;
|
|
101
|
+
}
|
|
102
|
+
get started() {
|
|
103
|
+
return this.#state === 'connecting' || this.#state === 'open' || this.#state === 'reconnecting';
|
|
104
|
+
}
|
|
105
|
+
async start(connect) {
|
|
106
|
+
if (this.started)
|
|
107
|
+
return;
|
|
108
|
+
this.#connect = connect;
|
|
109
|
+
this.#state = 'connecting';
|
|
110
|
+
this.#attempt = 0;
|
|
111
|
+
try {
|
|
112
|
+
await this.#runConnect(connect);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// 注意:stop() 可能在 await 期间并发改写 #state,必须经 getter 读取避免 TS 窄化误判
|
|
116
|
+
if (this.#currentState() === 'stopped')
|
|
117
|
+
return; // stop-during-connect 竞态:静默 settle
|
|
118
|
+
// start 失败复位:回 idle、不武装重连,允许调用方重试
|
|
119
|
+
this.#state = 'idle';
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
if (this.#currentState() === 'stopped')
|
|
123
|
+
return; // stop 竞态先于 open
|
|
124
|
+
this.#state = 'open';
|
|
125
|
+
}
|
|
126
|
+
async stop() {
|
|
127
|
+
const wasActive = this.#state !== 'stopped';
|
|
128
|
+
this.#state = 'stopped';
|
|
129
|
+
this.#attempt = 0;
|
|
130
|
+
this.stopHeartbeat();
|
|
131
|
+
if (this.#reconnectTimer) {
|
|
132
|
+
clearTimeout(this.#reconnectTimer);
|
|
133
|
+
this.#reconnectTimer = undefined;
|
|
134
|
+
}
|
|
135
|
+
this.#reconnectWake?.(false);
|
|
136
|
+
this.#reconnectWake = undefined;
|
|
137
|
+
for (const wake of this.#stopWaiters.splice(0))
|
|
138
|
+
wake();
|
|
139
|
+
const close = this.#forceClose;
|
|
140
|
+
this.#forceClose = undefined;
|
|
141
|
+
if (close) {
|
|
142
|
+
try {
|
|
143
|
+
close();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* ignore */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (wasActive) {
|
|
150
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#name }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
startHeartbeat(beat, intervalMs = this.#heartbeat.intervalMs) {
|
|
154
|
+
this.stopHeartbeat();
|
|
155
|
+
if (intervalMs <= 0)
|
|
156
|
+
return;
|
|
157
|
+
const watchdogMisses = this.#heartbeat.watchdogMisses;
|
|
158
|
+
this.#heartbeatMisses = 0;
|
|
159
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
160
|
+
if (watchdogMisses > 0) {
|
|
161
|
+
this.#heartbeatMisses += 1;
|
|
162
|
+
if (this.#heartbeatMisses > watchdogMisses) {
|
|
163
|
+
this.stopHeartbeat();
|
|
164
|
+
logger.warn(formatCompact({
|
|
165
|
+
op: 'heartbeat_watchdog',
|
|
166
|
+
endpoint: this.#name,
|
|
167
|
+
ok: false,
|
|
168
|
+
misses: this.#heartbeatMisses,
|
|
169
|
+
}));
|
|
170
|
+
const close = this.#forceClose;
|
|
171
|
+
if (close) {
|
|
172
|
+
try {
|
|
173
|
+
close();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* ignore */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
beat();
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
logger.warn(formatCompact({
|
|
187
|
+
op: 'heartbeat',
|
|
188
|
+
endpoint: this.#name,
|
|
189
|
+
ok: false,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
}, intervalMs);
|
|
194
|
+
}
|
|
195
|
+
stopHeartbeat() {
|
|
196
|
+
if (this.#heartbeatTimer) {
|
|
197
|
+
clearInterval(this.#heartbeatTimer);
|
|
198
|
+
this.#heartbeatTimer = undefined;
|
|
199
|
+
}
|
|
200
|
+
this.#heartbeatMisses = 0;
|
|
201
|
+
}
|
|
202
|
+
notifyHeartbeatAck() {
|
|
203
|
+
this.#heartbeatMisses = 0;
|
|
204
|
+
}
|
|
205
|
+
#createHandle(generation) {
|
|
206
|
+
return {
|
|
207
|
+
notifyClosed: (reason) => {
|
|
208
|
+
if (generation !== this.#generation)
|
|
209
|
+
return; // 陈旧连接的迟到事件
|
|
210
|
+
this.#forceClose = undefined;
|
|
211
|
+
this.stopHeartbeat(); // close 清心跳
|
|
212
|
+
// 仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
213
|
+
if (this.#state !== 'open')
|
|
214
|
+
return;
|
|
215
|
+
logger.warn(formatCompact({
|
|
216
|
+
op: 'disconnect',
|
|
217
|
+
endpoint: this.#name,
|
|
218
|
+
ok: false,
|
|
219
|
+
error: reason instanceof Error ? reason.message : reason != null ? String(reason) : 'closed',
|
|
220
|
+
}));
|
|
221
|
+
if (!this.#reconnect) {
|
|
222
|
+
this.#state = 'closed';
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.#state = 'reconnecting';
|
|
226
|
+
this.#scheduleReconnect();
|
|
227
|
+
},
|
|
228
|
+
onForceClose: (close) => {
|
|
229
|
+
if (generation === this.#generation)
|
|
230
|
+
this.#forceClose = close;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** 跑一次 connect 尝试;与 stop 信号竞态,stop 先到则静默返回。 */
|
|
235
|
+
async #runConnect(connect) {
|
|
236
|
+
const generation = ++this.#generation;
|
|
237
|
+
this.#forceClose = undefined;
|
|
238
|
+
const handle = this.#createHandle(generation);
|
|
239
|
+
// Promise.resolve().then 兜底同步抛错;额外 catch 防止 stop 竞态后迟到拒绝变 unhandled
|
|
240
|
+
const connecting = Promise.resolve().then(() => connect(handle));
|
|
241
|
+
connecting.catch(() => { });
|
|
242
|
+
let wake;
|
|
243
|
+
const stopped = new Promise((resolve) => {
|
|
244
|
+
wake = resolve;
|
|
245
|
+
});
|
|
246
|
+
this.#stopWaiters.push(wake);
|
|
247
|
+
try {
|
|
248
|
+
await Promise.race([connecting, stopped]);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
const index = this.#stopWaiters.indexOf(wake);
|
|
252
|
+
if (index >= 0)
|
|
253
|
+
this.#stopWaiters.splice(index, 1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** 武装重连循环(单例,防叠套)。 */
|
|
257
|
+
#scheduleReconnect() {
|
|
258
|
+
if (this.#reconnectRunning)
|
|
259
|
+
return;
|
|
260
|
+
this.#reconnectRunning = true;
|
|
261
|
+
void this.#reconnectLoop().finally(() => {
|
|
262
|
+
this.#reconnectRunning = false;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async #reconnectLoop() {
|
|
266
|
+
const config = this.#reconnect;
|
|
267
|
+
const connect = this.#connect;
|
|
268
|
+
if (!config || !connect)
|
|
269
|
+
return;
|
|
270
|
+
while (this.#state === 'reconnecting') {
|
|
271
|
+
if (this.#attempt >= config.maxAttempts) {
|
|
272
|
+
this.#state = 'closed';
|
|
273
|
+
logger.warn(formatCompact({
|
|
274
|
+
op: 'reconnect',
|
|
275
|
+
endpoint: this.#name,
|
|
276
|
+
ok: false,
|
|
277
|
+
error: `gave up after ${this.#attempt} attempts`,
|
|
278
|
+
}));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const base = Math.min(config.initialIntervalMs * config.multiplier ** this.#attempt, config.maxIntervalMs);
|
|
282
|
+
const delay = base + Math.floor(this.#random() * config.jitterMs);
|
|
283
|
+
// 首次断开 WARN,后续重试静默为 DEBUG,避免刷屏(对齐 icqq)
|
|
284
|
+
const log = this.#attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
|
|
285
|
+
log(formatCompact({
|
|
286
|
+
op: 'reconnect',
|
|
287
|
+
endpoint: this.#name,
|
|
288
|
+
delay_ms: delay,
|
|
289
|
+
attempt: this.#attempt + 1,
|
|
290
|
+
}));
|
|
291
|
+
const elapsed = await this.#sleep(delay);
|
|
292
|
+
if (!elapsed || this.#currentState() !== 'reconnecting')
|
|
293
|
+
return;
|
|
294
|
+
try {
|
|
295
|
+
await this.#runConnect(connect);
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
if (this.#currentState() === 'stopped')
|
|
299
|
+
return;
|
|
300
|
+
this.#attempt += 1;
|
|
301
|
+
logger.debug(formatCompact({
|
|
302
|
+
op: 'reconnect',
|
|
303
|
+
endpoint: this.#name,
|
|
304
|
+
ok: false,
|
|
305
|
+
attempt: this.#attempt,
|
|
306
|
+
error: err instanceof Error ? err.message : String(err),
|
|
307
|
+
}));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (this.#currentState() === 'stopped')
|
|
311
|
+
return;
|
|
312
|
+
this.#state = 'open';
|
|
313
|
+
this.#attempt = 0;
|
|
314
|
+
logger.info(formatCompact({ op: 'reconnect', endpoint: this.#name, ok: true }));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** 可中断 sleep:stop() 唤醒并返回 false。 */
|
|
319
|
+
#sleep(ms) {
|
|
320
|
+
return new Promise((resolve) => {
|
|
321
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
322
|
+
this.#reconnectTimer = undefined;
|
|
323
|
+
this.#reconnectWake = undefined;
|
|
324
|
+
resolve(true);
|
|
325
|
+
}, ms);
|
|
326
|
+
this.#reconnectWake = resolve;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** 创建端点生命周期基座实例(见文件头迁移指引)。 */
|
|
331
|
+
export function createEndpointLifecycle(options) {
|
|
332
|
+
return new EndpointLifecycleImpl(options);
|
|
333
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface EndpointFriend {
|
|
2
|
+
/** 数字平台(QQ 系)用 number;Slack/LINE/微信系用 string。 */
|
|
3
|
+
readonly user_id: number | string;
|
|
4
|
+
readonly nickname: string;
|
|
5
|
+
readonly remark: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EndpointGroup {
|
|
8
|
+
/** 数字平台(QQ 系)用 number;Slack/LINE/微信系用 string。 */
|
|
9
|
+
readonly group_id: number | string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface EndpointChannelParent {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface EndpointChannel {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
readonly parent?: EndpointChannelParent;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Optional, platform-neutral management surface exposed by an Endpoint.
|
|
24
|
+
*
|
|
25
|
+
* Platform adapters own SDK aliases, identifier coercion, and response
|
|
26
|
+
* normalization. Hosts consume this interface without inspecting adapter
|
|
27
|
+
* names or transport-specific fields.
|
|
28
|
+
*/
|
|
29
|
+
export interface EndpointManagement {
|
|
30
|
+
listFriends?(): Promise<readonly EndpointFriend[]>;
|
|
31
|
+
listGroups?(): Promise<readonly EndpointGroup[]>;
|
|
32
|
+
listChannels?(): Promise<readonly EndpointChannel[]>;
|
|
33
|
+
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
34
|
+
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
35
|
+
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
36
|
+
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
37
|
+
muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
|
|
38
|
+
setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
|
|
39
|
+
deleteFriend?(userId: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface EndpointWithManagement {
|
|
42
|
+
readonly management?: EndpointManagement;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Stable, transport-neutral capability ids exposed to Host/Console clients.
|
|
46
|
+
* Values intentionally mirror EndpointManagement method names so adapters only
|
|
47
|
+
* need to implement the semantic port; no second capability declaration exists.
|
|
48
|
+
*/
|
|
49
|
+
export declare const endpointManagementCapabilityIds: readonly ["listFriends", "listGroups", "listChannels", "listGroupMembers", "approveRequest", "rejectRequest", "kickGroupMember", "muteGroupMember", "setGroupAdmin", "deleteFriend"];
|
|
50
|
+
export type EndpointManagementCapability = (typeof endpointManagementCapabilityIds)[number];
|
|
51
|
+
export declare function resolveEndpointManagement(endpoint: unknown): EndpointManagement | undefined;
|
|
52
|
+
/** Derive advertised capabilities from the live semantic port implementation. */
|
|
53
|
+
export declare function listEndpointManagementCapabilities(endpoint: unknown): readonly EndpointManagementCapability[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, transport-neutral capability ids exposed to Host/Console clients.
|
|
3
|
+
* Values intentionally mirror EndpointManagement method names so adapters only
|
|
4
|
+
* need to implement the semantic port; no second capability declaration exists.
|
|
5
|
+
*/
|
|
6
|
+
export const endpointManagementCapabilityIds = [
|
|
7
|
+
'listFriends',
|
|
8
|
+
'listGroups',
|
|
9
|
+
'listChannels',
|
|
10
|
+
'listGroupMembers',
|
|
11
|
+
'approveRequest',
|
|
12
|
+
'rejectRequest',
|
|
13
|
+
'kickGroupMember',
|
|
14
|
+
'muteGroupMember',
|
|
15
|
+
'setGroupAdmin',
|
|
16
|
+
'deleteFriend',
|
|
17
|
+
];
|
|
18
|
+
export function resolveEndpointManagement(endpoint) {
|
|
19
|
+
if (!endpoint || typeof endpoint !== 'object')
|
|
20
|
+
return undefined;
|
|
21
|
+
const management = endpoint.management;
|
|
22
|
+
return management && typeof management === 'object' ? management : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Derive advertised capabilities from the live semantic port implementation. */
|
|
25
|
+
export function listEndpointManagementCapabilities(endpoint) {
|
|
26
|
+
const management = resolveEndpointManagement(endpoint);
|
|
27
|
+
if (!management)
|
|
28
|
+
return Object.freeze([]);
|
|
29
|
+
return Object.freeze(endpointManagementCapabilityIds.filter((capability) => typeof management[capability] === 'function'));
|
|
30
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/** @internal 适配器 projection(AdapterIndex),框架内部机制,不承诺不 break。 */
|
|
1
2
|
export * from './adapter-index.js';
|
|
2
3
|
export * from './credentials.js';
|
|
4
|
+
/** @public 用户侧创作面:`defineAdapter`(`adapters/` 约定目录默认导出,承诺 semver)。 */
|
|
3
5
|
export * from './definition.js';
|
|
6
|
+
export * from './endpoint-commands.js';
|
|
7
|
+
export * from './endpoint-lifecycle.js';
|
|
8
|
+
export * from './endpoint-management.js';
|
|
4
9
|
export * from './provider.js';
|
|
5
10
|
export { default } from './provider.js';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/** @internal 适配器 projection(AdapterIndex),框架内部机制,不承诺不 break。 */
|
|
1
2
|
export * from './adapter-index.js';
|
|
2
3
|
export * from './credentials.js';
|
|
4
|
+
/** @public 用户侧创作面:`defineAdapter`(`adapters/` 约定目录默认导出,承诺 semver)。 */
|
|
3
5
|
export * from './definition.js';
|
|
6
|
+
export * from './endpoint-commands.js';
|
|
7
|
+
export * from './endpoint-lifecycle.js';
|
|
8
|
+
export * from './endpoint-management.js';
|
|
4
9
|
export * from './provider.js';
|
|
5
10
|
export { default } from './provider.js';
|
package/lib/provider.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Convention-based Adapter and Endpoint Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"
|
|
21
|
-
"@zhin.js/
|
|
22
|
-
"@zhin.js/logger": "1.0.75"
|
|
20
|
+
"yaml": "^2.9.0",
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.3",
|
|
22
|
+
"@zhin.js/logger": "1.0.75",
|
|
23
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^26.1.0",
|
|
26
|
-
"typescript": "^6.0.3"
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"@zhin.js/command": "1.0.3"
|
|
27
29
|
},
|
|
28
30
|
"zhin": {
|
|
29
31
|
"protocol": 1,
|