@zhin.js/host-http 1.0.0 → 1.0.2

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.
@@ -9,12 +9,18 @@
9
9
  * - cron:`ctx.scheduleHost`(Plugin Runtime ScheduleHost,仅 register/list,
10
10
  * 无持久化引擎,故 cron:add/remove/pause/resume 报"未接线")。
11
11
  * - inbox:`ctx.databaseHost.models`(unified_inbox_message/request/notice 三张表,
12
- * 表不存在或查询失败 → 空数组 + inboxEnabled:false)。
13
- * - 社交/群管:`ctx.resolveEndpoint` 拿 live endpoint 实例,
14
- * 实例上探测到对应方法才调用,否则报"该平台不支持"。
12
+ * 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
13
+ * 按行 id update consumed=1,表/model 缺失时报"未接线")。
14
+ * - 社交/群管:`ctx.resolveEndpoint` 拿 live endpoint 实例,只消费其
15
+ * `management` 语义端口;平台 SDK 兼容与字段归一化由 Adapter 负责。
15
16
  */
16
17
 
17
- /** 新 Runtime 统一收件箱表名(与 packages/im/zhin/src/setup/register-inbox.ts 一致)。 */
18
+ /** 新 Runtime 统一收件箱表名(SSOT:@zhin.js/plugin-runtime inbox.js)。 */
19
+ import {
20
+ assertDemoConsoleRpcAllowed,
21
+ normalizeConsoleRpcType,
22
+ } from '@zhin.js/console-protocol';
23
+
18
24
  const TABLE_MESSAGE = 'unified_inbox_message';
19
25
  const TABLE_REQUEST = 'unified_inbox_request';
20
26
  const TABLE_NOTICE = 'unified_inbox_notice';
@@ -25,36 +31,35 @@ export interface ConsoleRpcExtendedCtx {
25
31
  projectRoot: string;
26
32
  /** Plugin Runtime ScheduleHost(basic/cli schedule-host-installer 提供)。 */
27
33
  scheduleHost?: unknown;
28
- /** 经 ImRuntime / AdapterEndpointIndex 解析 live endpoint 实例。 */
34
+ /** 经 ImRuntime / AdapterEndpointIndex 解析管理语义端口。 */
35
+ resolveEndpointManagement?: (
36
+ adapter: string,
37
+ endpointId: string,
38
+ ) => EndpointManagementPort | null | undefined;
39
+ /** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
29
40
  resolveEndpoint?: (adapter: string, endpointId: string) => unknown;
30
41
  /** Plugin Runtime DatabaseHost 的 models 视图。 */
31
42
  databaseHost?: { models: { get(name: string): unknown } };
43
+ /** Agent Host 持久化调度引擎(@zhin.js/agent getAssistantRuntime().engine),未 init 时返回 null。 */
44
+ resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
32
45
  }
33
46
 
34
- export type ExtendedRpcResult = { data: unknown } | { error: string };
47
+ export interface ConsoleScheduleEngine {
48
+ addJob(job: Record<string, unknown>): Promise<Record<string, unknown>>;
49
+ removeJob(id: string): Promise<boolean>;
50
+ pauseJob(id: string): Promise<boolean>;
51
+ resumeJob(id: string): Promise<boolean>;
52
+ listJobs(): Promise<Record<string, unknown>[]>;
53
+ }
35
54
 
36
- /** demo scope 只读放行清单之外的写操作 type。 */
37
- const WRITE_TYPES = new Set([
38
- 'cron:add',
39
- 'cron:remove',
40
- 'cron:pause',
41
- 'cron:resume',
42
- 'endpoint:requestApprove',
43
- 'endpoint:requestReject',
44
- 'endpoint:requestConsumed',
45
- 'endpoint:noticeConsumed',
46
- 'endpoint:groupKick',
47
- 'endpoint:groupMute',
48
- 'endpoint:groupAdmin',
49
- 'endpoint:deleteFriend',
50
- ]);
55
+ export type ExtendedRpcResult = { data: unknown } | { error: string };
51
56
 
52
57
  const CRON_NOT_WIRED =
53
58
  '持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
54
59
  '不支持 add/remove/pause/resume(需要 @zhin.js/agent 持久化调度引擎)';
55
60
 
56
61
  const CONSUMED_NOT_WIRED =
57
- '收件箱已读标记未接线:新 Runtime 尚未提供 request/notice 的 consumed 写路径';
62
+ '收件箱已读标记未接线:unified_inbox_request/notice 表未注册或 DatabaseHost 未启动';
58
63
 
59
64
  interface ScheduleJobRow {
60
65
  id: string;
@@ -66,9 +71,27 @@ interface InboxModel {
66
71
  select(): {
67
72
  where(query: Record<string, unknown>): Promise<Record<string, unknown>[]> | Record<string, unknown>[];
68
73
  };
74
+ update?(patch: Record<string, unknown>): {
75
+ where(query: Record<string, unknown>): Promise<unknown> | unknown;
76
+ };
69
77
  }
70
78
 
71
- type EndpointAny = Record<string, unknown>;
79
+ interface EndpointManagementPort {
80
+ listFriends?(): Promise<readonly { user_id: number; nickname: string; remark: string }[]>;
81
+ listGroups?(): Promise<readonly { group_id: number; name: string }[]>;
82
+ listChannels?(): Promise<readonly {
83
+ id: string;
84
+ name?: string;
85
+ parent?: { type: string; id: string; name?: string };
86
+ }[]>;
87
+ listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
88
+ approveRequest?(requestId: string, remark?: string): Promise<void>;
89
+ rejectRequest?(requestId: string, reason?: string): Promise<void>;
90
+ kickGroupMember?(groupId: string, userId: string): Promise<void>;
91
+ muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
92
+ setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
93
+ deleteFriend?(userId: string): Promise<void>;
94
+ }
72
95
 
73
96
  export async function dispatchExtendedConsoleRpc(
74
97
  type: string,
@@ -76,9 +99,11 @@ export async function dispatchExtendedConsoleRpc(
76
99
  ctx: ConsoleRpcExtendedCtx,
77
100
  ): Promise<ExtendedRpcResult | undefined> {
78
101
  const d = data ?? {};
102
+ type = normalizeConsoleRpcType(type);
79
103
 
80
- if (WRITE_TYPES.has(type) && !ctx.fullScope) {
81
- return { error: `Demo scope: RPC "${type}" is forbidden` };
104
+ if (!ctx.fullScope) {
105
+ const denied = assertDemoConsoleRpcAllowed(type);
106
+ if (denied) return { error: denied };
82
107
  }
83
108
 
84
109
  switch (type) {
@@ -87,59 +112,61 @@ export async function dispatchExtendedConsoleRpc(
87
112
  return listSchedule(ctx);
88
113
 
89
114
  case 'cron:add':
115
+ return addCron(d, ctx);
90
116
  case 'cron:remove':
91
117
  case 'cron:pause':
92
118
  case 'cron:resume':
93
- return { error: CRON_NOT_WIRED };
119
+ return mutateCron(type, d, ctx);
94
120
 
95
- case 'endpoint:requests':
121
+ case 'request.list':
96
122
  return listPendingRequests(d, ctx);
97
- case 'endpoint:inboxRequests':
123
+ case 'inbox.requests':
98
124
  return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
99
- case 'endpoint:inboxNotices':
125
+ case 'inbox.notices':
100
126
  return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
101
- case 'endpoint:inboxMessages':
127
+ case 'inbox.messages':
102
128
  return listInboxMessages(d, ctx);
103
129
 
104
- case 'endpoint:requestApprove':
105
- case 'endpoint:requestReject':
130
+ case 'request.approve':
131
+ case 'request.reject':
106
132
  return actOnRequest(type, d, ctx);
107
133
 
108
- case 'endpoint:requestConsumed':
109
- case 'endpoint:noticeConsumed':
110
- return { error: CONSUMED_NOT_WIRED };
134
+ case 'request.consumed':
135
+ return markInboxConsumed(d, ctx, TABLE_REQUEST);
136
+ case 'notice.consumed':
137
+ return markInboxConsumed(d, ctx, TABLE_NOTICE);
111
138
 
112
- case 'endpoint:friends':
139
+ case 'endpoint.friends':
113
140
  return listFriends(d, ctx);
114
- case 'endpoint:groups':
141
+ case 'endpoint.groups':
115
142
  return listGroups(d, ctx);
116
- case 'endpoint:channels':
143
+ case 'endpoint.channels':
117
144
  return listChannels(d, ctx);
118
- case 'endpoint:groupMembers':
145
+ case 'endpoint.group_members':
119
146
  return listGroupMembers(d, ctx);
120
147
 
121
- case 'endpoint:groupKick':
148
+ case 'endpoint.group_kick':
122
149
  return groupWriteOp(d, ctx, {
123
- methods: ['removeMember', 'kickMember', 'setGroupKick'],
150
+ method: 'kickGroupMember',
124
151
  buildArgs: (gid, uid) => [gid, uid],
125
152
  requireUser: true,
126
153
  unsupported: '踢出群成员',
127
154
  });
128
- case 'endpoint:groupMute':
155
+ case 'endpoint.group_mute':
129
156
  return groupWriteOp(d, ctx, {
130
- methods: ['muteMember', 'setGroupBan', 'banMember'],
157
+ method: 'muteGroupMember',
131
158
  buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
132
159
  requireUser: true,
133
160
  unsupported: '禁言群成员',
134
161
  });
135
- case 'endpoint:groupAdmin':
162
+ case 'endpoint.group_admin':
136
163
  return groupWriteOp(d, ctx, {
137
- methods: ['setModerator', 'setGroupAdmin', 'setAdmin'],
164
+ method: 'setGroupAdmin',
138
165
  buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
139
166
  requireUser: true,
140
167
  unsupported: '设置群管理员',
141
168
  });
142
- case 'endpoint:deleteFriend':
169
+ case 'endpoint.delete_friend':
143
170
  return deleteFriend(d, ctx);
144
171
 
145
172
  default:
@@ -149,7 +176,7 @@ export async function dispatchExtendedConsoleRpc(
149
176
 
150
177
  // ---------------------------------------------------------------- cron
151
178
 
152
- function listSchedule(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
179
+ function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
153
180
  const host = ctx.scheduleHost as { list?: () => unknown } | undefined;
154
181
  if (!host || typeof host.list !== 'function') {
155
182
  return { error: '调度服务未配置(scheduleHost 未挂载)' };
@@ -165,9 +192,94 @@ function listSchedule(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
165
192
  id: job.id,
166
193
  cron: job.cron,
167
194
  ...(typeof job.description === 'string' ? { description: job.description } : {}),
195
+ // console cron 页渲染字段(由 schedule host 提供时透传)
196
+ expression: (job as { expression?: string }).expression ?? job.cron,
197
+ running: (job as { running?: boolean }).running ?? true,
198
+ ...((job as { plugin?: string }).plugin ? { plugin: (job as { plugin?: string }).plugin } : {}),
199
+ ...((job as { nextExecution?: number }).nextExecution
200
+ ? { nextExecution: (job as { nextExecution?: number }).nextExecution } : {}),
168
201
  }));
169
- // persistent 引擎未接入新 Runtime,返回空数组保持 legacy 形状。
170
- return { data: { memory, persistent: [] } };
202
+ return listPersistentJobs(ctx).then((persistent) => ({ data: { memory, persistent } }));
203
+ } catch (error) {
204
+ return { error: errorMessage(error) };
205
+ }
206
+ }
207
+
208
+ type ConsoleScheduleListCtx = ConsoleRpcExtendedCtx;
209
+
210
+ /** 持久化任务(Agent Host ScheduleJobEngine)→ 前端 PersistentCron 形状。 */
211
+ async function listPersistentJobs(ctx: ConsoleRpcExtendedCtx): Promise<Record<string, unknown>[]> {
212
+ const engine = ctx.resolveScheduleEngine?.();
213
+ if (!engine) return [];
214
+ try {
215
+ const jobs = await engine.listJobs();
216
+ return jobs.map((job) => ({
217
+ id: job.id,
218
+ label: job.label ?? job.id,
219
+ cronExpression: (job.schedule as { cron?: string } | undefined)?.cron ?? '',
220
+ prompt: (job.action as { prompt?: string } | undefined)?.prompt ?? '',
221
+ enabled: job.enabled !== false,
222
+ source: job.source ?? 'manual',
223
+ createdAt: (job as { createdAt?: number }).createdAt,
224
+ nextExecution: (job.state as { nextRunAtMs?: number } | undefined)?.nextRunAtMs,
225
+ ...((job.notify as { target?: unknown } | undefined)?.target
226
+ ? { context: { target: (job.notify as { target?: unknown }).target } } : {}),
227
+ lastExecutedAt: (job.state as { lastExecutedAt?: number } | undefined)?.lastExecutedAt,
228
+ lastStatus: (job.state as { lastStatus?: string } | undefined)?.lastStatus,
229
+ }));
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+
235
+ async function addCron(
236
+ d: Record<string, unknown>,
237
+ ctx: ConsoleRpcExtendedCtx,
238
+ ): Promise<ExtendedRpcResult> {
239
+ const engine = ctx.resolveScheduleEngine?.();
240
+ if (!engine) return { error: CRON_NOT_WIRED };
241
+ const cronExpression = String(d.cronExpression ?? '').trim();
242
+ const prompt = String(d.prompt ?? '').trim();
243
+ if (!cronExpression) return { error: 'cronExpression is required' };
244
+ if (!prompt) return { error: 'prompt is required' };
245
+ const label = typeof d.label === 'string' && d.label.trim() ? d.label.trim() : undefined;
246
+ const context = (d.context ?? {}) as Record<string, unknown>;
247
+ const target = context.target ?? context.channel;
248
+ const job = {
249
+ id: `console-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
250
+ ...(label ? { label } : {}),
251
+ enabled: true,
252
+ schedule: { kind: 'solar', cron: cronExpression },
253
+ action: { kind: 'agent', prompt },
254
+ notify: target
255
+ ? { channel: 'im', target }
256
+ : { channel: 'silent' },
257
+ source: 'manual',
258
+ };
259
+ try {
260
+ const created = await engine.addJob(job);
261
+ return { data: { job: created, success: true } };
262
+ } catch (error) {
263
+ return { error: errorMessage(error) };
264
+ }
265
+ }
266
+
267
+ async function mutateCron(
268
+ type: 'cron:remove' | 'cron:pause' | 'cron:resume',
269
+ d: Record<string, unknown>,
270
+ ctx: ConsoleRpcExtendedCtx,
271
+ ): Promise<ExtendedRpcResult> {
272
+ const engine = ctx.resolveScheduleEngine?.();
273
+ if (!engine) return { error: CRON_NOT_WIRED };
274
+ const id = String(d.id ?? '').trim();
275
+ if (!id) return { error: 'id is required' };
276
+ try {
277
+ const ok = type === 'cron:remove'
278
+ ? await engine.removeJob(id)
279
+ : type === 'cron:pause'
280
+ ? await engine.pauseJob(id)
281
+ : await engine.resumeJob(id);
282
+ return ok ? { data: { success: true } } : { error: `任务不存在: ${id}` };
171
283
  } catch (error) {
172
284
  return { error: errorMessage(error) };
173
285
  }
@@ -221,7 +333,7 @@ async function listInbox(
221
333
  return { data: { [key]: sorted, inboxEnabled: enabled } };
222
334
  }
223
335
 
224
- /** endpoint:requests —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
336
+ /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
225
337
  async function listPendingRequests(
226
338
  d: Record<string, unknown>,
227
339
  ctx: ConsoleRpcExtendedCtx,
@@ -287,62 +399,113 @@ async function listInboxMessages(
287
399
  }
288
400
 
289
401
  function mapRequestRow(row: Record<string, unknown>): Record<string, unknown> {
402
+ const actorId = row.actor_id;
403
+ const actorName = row.actor_name ?? undefined;
404
+ const sceneId = row.scene_id;
405
+ const sceneType = row.scene_type ?? undefined;
290
406
  return {
291
407
  id: row.id,
292
408
  platform_request_id: row.platform_request_id,
409
+ // console endpoint-detail 期望的 camelCase/扁平别名(SSE 推送路径同款形状)
410
+ platformRequestId: row.platform_request_id,
293
411
  type: row.type,
294
- scene_type: row.scene_type ?? undefined,
295
- scene_id: row.scene_id,
412
+ scene_type: sceneType,
413
+ scene_id: sceneId,
414
+ channel_id: sceneId,
415
+ channel_type: sceneType,
416
+ channel: { id: sceneId, type: sceneType },
296
417
  sub_type: row.sub_type ?? undefined,
297
- actor: { id: row.actor_id, name: row.actor_name ?? undefined },
418
+ actor: { id: actorId, name: actorName },
419
+ sender: { id: actorId, name: actorName },
420
+ sender_id: actorId,
421
+ sender_name: actorName,
298
422
  comment: row.comment ?? undefined,
299
423
  created_at: row.created_at,
424
+ timestamp: row.created_at,
300
425
  resolved: row.resolved,
301
426
  resolved_at: row.resolved_at ?? undefined,
302
427
  };
303
428
  }
304
429
 
305
430
  function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
431
+ const actorId = row.actor_id ?? undefined;
432
+ const actorName = row.actor_name ?? undefined;
433
+ const sceneId = row.scene_id;
434
+ const sceneType = row.scene_type ?? undefined;
306
435
  return {
307
436
  id: row.id,
308
437
  platform_notice_id: row.platform_notice_id,
309
438
  type: row.type,
310
- scene_type: row.scene_type ?? undefined,
311
- scene_id: row.scene_id,
439
+ scene_type: sceneType,
440
+ scene_id: sceneId,
441
+ channel_id: sceneId,
442
+ channel_type: sceneType,
312
443
  sub_type: row.sub_type ?? undefined,
313
- actor_id: row.actor_id ?? undefined,
314
- actor_name: row.actor_name ?? undefined,
444
+ actor_id: actorId,
445
+ actor_name: actorName,
446
+ // console endpoint-detail 期望的操作人字段别名
447
+ operator_id: actorId,
448
+ operator_name: actorName,
315
449
  target_id: row.target_id ?? undefined,
316
450
  target_name: row.target_name ?? undefined,
317
451
  payload: row.payload,
318
452
  created_at: row.created_at,
453
+ timestamp: row.created_at,
319
454
  };
320
455
  }
321
456
 
457
+ // ---------------------------------------------------------------- consumed 标记
458
+
459
+ /**
460
+ * request.consumed / notice.consumed —— 按行 id 置 consumed=1。
461
+ * 表未注册(或 model 无 update)时保持"未接线"报错。
462
+ */
463
+ async function markInboxConsumed(
464
+ d: Record<string, unknown>,
465
+ ctx: ConsoleRpcExtendedCtx,
466
+ table: string,
467
+ ): Promise<ExtendedRpcResult> {
468
+ const ids = numArrayField(d, '$row_ids', 'row_ids', 'rowIds', '$ids', 'ids');
469
+ // console UI 单条已读发 data:{id},归一为数组
470
+ if (ids.length === 0) {
471
+ const single = Number(d.id ?? d.$id);
472
+ if (Number.isFinite(single) && single > 0) ids.push(single);
473
+ }
474
+ if (ids.length === 0) return { error: '$row_ids required' };
475
+ const model = getInboxModel(ctx, table);
476
+ if (!model || typeof model.update !== 'function') return { error: CONSUMED_NOT_WIRED };
477
+ const now = Date.now();
478
+ try {
479
+ for (const id of ids) {
480
+ await model.update({ consumed: 1, consumed_at: now }).where({ id });
481
+ }
482
+ return { data: { success: true, updated: ids.length } };
483
+ } catch (error) {
484
+ return { error: errorMessage(error) };
485
+ }
486
+ }
487
+
322
488
  // ---------------------------------------------------------------- 请求审批
323
489
 
324
490
  async function actOnRequest(
325
- type: 'endpoint:requestApprove' | 'endpoint:requestReject',
491
+ type: 'request.approve' | 'request.reject',
326
492
  d: Record<string, unknown>,
327
493
  ctx: ConsoleRpcExtendedCtx,
328
494
  ): Promise<ExtendedRpcResult> {
329
495
  const adapter = strField(d, '$adapter', 'adapter');
330
496
  const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
331
- const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id');
497
+ const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
332
498
  if (!adapter || !endpointId || !requestId) {
333
499
  return { error: '$adapter, $endpoint, $id required' };
334
500
  }
335
501
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
336
502
  if ('error' in resolved) return resolved;
337
- const endpoint = resolved.endpoint;
338
- const approve = type === 'endpoint:requestApprove';
339
- const methodNames = approve
340
- ? ['approveRequest', 'approve_request', '$approveRequest']
341
- : ['rejectRequest', 'reject_request', '$rejectRequest'];
342
- const method = pickMethod(endpoint, methodNames);
503
+ const management = resolved.management;
504
+ const approve = type === 'request.approve';
505
+ const method = approve ? management.approveRequest : management.rejectRequest;
343
506
  if (!method) {
344
507
  return {
345
- error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${methodNames[0]},` +
508
+ error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
346
509
  '且新 Runtime 尚未挂载 pending request 注册表',
347
510
  };
348
511
  }
@@ -350,7 +513,7 @@ async function actOnRequest(
350
513
  const extra = approve
351
514
  ? strField(d, '$remark', 'remark')
352
515
  : strField(d, '$reason', 'reason');
353
- await method.call(endpoint, requestId, extra || undefined);
516
+ await method.call(management, requestId, extra || undefined);
354
517
  return { data: { success: true } };
355
518
  } catch (error) {
356
519
  return { error: errorMessage(error) };
@@ -365,25 +528,10 @@ async function listFriends(
365
528
  ): Promise<ExtendedRpcResult> {
366
529
  const resolved = requireEndpoint(d, ctx);
367
530
  if ('error' in resolved) return resolved;
368
- const { endpoint, adapter } = resolved;
531
+ const { management, adapter } = resolved;
369
532
  try {
370
- const map = (endpoint.friends ?? endpoint.fl) as Map<unknown, Record<string, unknown>> | undefined;
371
- if (map && typeof map.values === 'function') {
372
- const friends = Array.from(map.values()).map((f) => ({
373
- user_id: Number(f.user_id),
374
- nickname: String(f.nickname ?? ''),
375
- remark: String(f.remark ?? ''),
376
- }));
377
- return { data: { friends, count: friends.length } };
378
- }
379
- const getFriendList = pickMethod(endpoint, ['getFriendList', 'getFriends', 'listFriends']);
380
- if (getFriendList) {
381
- const raw = await getFriendList.call(endpoint);
382
- const friends = unwrapList(raw).map((row) => ({
383
- user_id: Number(row.user_id ?? row.userId ?? row.id ?? 0),
384
- nickname: String(row.nickname ?? row.name ?? row.user_id ?? ''),
385
- remark: String(row.remark ?? ''),
386
- }));
533
+ if (management.listFriends) {
534
+ const friends = [...await management.listFriends()];
387
535
  return { data: { friends, count: friends.length } };
388
536
  }
389
537
  return { error: `当前适配器(${adapter})不支持好友列表` };
@@ -398,23 +546,10 @@ async function listGroups(
398
546
  ): Promise<ExtendedRpcResult> {
399
547
  const resolved = requireEndpoint(d, ctx);
400
548
  if ('error' in resolved) return resolved;
401
- const { endpoint, adapter } = resolved;
549
+ const { management, adapter } = resolved;
402
550
  try {
403
- const map = (endpoint.groups ?? endpoint.gl) as Map<unknown, Record<string, unknown>> | undefined;
404
- if (map && typeof map.values === 'function') {
405
- const groups = Array.from(map.values()).map((g) => ({
406
- group_id: Number(g.group_id),
407
- name: String(g.group_name ?? g.name ?? g.group_id ?? ''),
408
- }));
409
- return { data: { groups, count: groups.length } };
410
- }
411
- const getGroupList = pickMethod(endpoint, ['getGroupList', 'getGroups', 'listGroups']);
412
- if (getGroupList) {
413
- const raw = await getGroupList.call(endpoint);
414
- const groups = unwrapList(raw).map((row) => ({
415
- group_id: Number(row.group_id ?? row.groupId ?? row.id ?? 0),
416
- name: String(row.name ?? row.group_name ?? row.group_id ?? ''),
417
- }));
551
+ if (management.listGroups) {
552
+ const groups = [...await management.listGroups()];
418
553
  return { data: { groups, count: groups.length } };
419
554
  }
420
555
  return { error: `当前适配器(${adapter})不支持群列表` };
@@ -429,43 +564,10 @@ async function listChannels(
429
564
  ): Promise<ExtendedRpcResult> {
430
565
  const resolved = requireEndpoint(d, ctx);
431
566
  if ('error' in resolved) return resolved;
432
- const { endpoint, adapter } = resolved;
567
+ const { management, adapter } = resolved;
433
568
  try {
434
- const channels: Array<{
435
- id: string;
436
- name?: string;
437
- parent?: { type: string; id: string; name?: string };
438
- }> = [];
439
- const getGuildChannelList = pickMethod(endpoint, ['getGuildChannelList']);
440
- if (getGuildChannelList) {
441
- const list = await getGuildChannelList.call(endpoint) as Array<Record<string, unknown>>;
442
- for (const item of Array.isArray(list) ? list : []) {
443
- const parent = normalizeParent(item.parent);
444
- channels.push({
445
- id: String(item.id ?? ''),
446
- ...(item.name != null ? { name: String(item.name) } : {}),
447
- ...(parent ? { parent } : {}),
448
- });
449
- }
450
- return { data: { channels, count: channels.length } };
451
- }
452
- const getGuilds = pickMethod(endpoint, ['getGuilds', 'listGuilds']);
453
- const getChannels = pickMethod(endpoint, ['getChannels', 'listChannels']);
454
- if (getGuilds && getChannels) {
455
- const guilds = unwrapList(await getGuilds.call(endpoint));
456
- for (const g of guilds) {
457
- const gid = String(g.id ?? g.guild_id ?? '');
458
- if (!gid) continue;
459
- const guildName = String(g.name ?? g.guild_name ?? gid);
460
- const chs = unwrapList(await getChannels.call(endpoint, gid));
461
- for (const c of chs) {
462
- channels.push({
463
- id: String(c.id ?? c.channel_id ?? ''),
464
- name: String(c.name ?? c.channel_name ?? c.id ?? ''),
465
- parent: { type: 'guild', id: gid, name: guildName },
466
- });
467
- }
468
- }
569
+ if (management.listChannels) {
570
+ const channels = [...await management.listChannels()];
469
571
  return { data: { channels, count: channels.length } };
470
572
  }
471
573
  return { error: `当前适配器(${adapter})不支持频道列表` };
@@ -486,22 +588,14 @@ async function listGroupMembers(
486
588
  }
487
589
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
488
590
  if ('error' in resolved) return resolved;
489
- const { endpoint } = resolved;
591
+ const { management } = resolved;
490
592
  try {
491
- const method = pickMethod(endpoint, ['getGroupMemberList', 'listMembers', 'getMemberList']);
593
+ const method = management.listGroupMembers;
492
594
  if (!method) {
493
595
  return { error: `当前适配器(${adapter})不支持群成员列表` };
494
596
  }
495
- const raw = await method.call(endpoint, groupId);
496
- if (Array.isArray(raw)) {
497
- return { data: { members: raw, count: raw.length } };
498
- }
499
- // Map(icqq 风格 gml)→ values 数组
500
- if (raw && typeof (raw as Map<unknown, unknown>).values === 'function') {
501
- const members = Array.from((raw as Map<unknown, unknown>).values());
502
- return { data: { members, count: members.length } };
503
- }
504
- return { data: raw ?? { members: [], count: 0 } };
597
+ const members = [...await method(groupId)];
598
+ return { data: { members, count: members.length } };
505
599
  } catch (error) {
506
600
  return { error: errorMessage(error) };
507
601
  }
@@ -510,7 +604,7 @@ async function listGroupMembers(
510
604
  // ---------------------------------------------------------------- 群管/好友写操作
511
605
 
512
606
  interface GroupWriteSpec {
513
- methods: string[];
607
+ method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
514
608
  buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
515
609
  requireUser: boolean;
516
610
  /** 中文操作名,用于"该平台不支持 xxx"。 */
@@ -534,8 +628,8 @@ async function groupWriteOp(
534
628
  }
535
629
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
536
630
  if ('error' in resolved) return resolved;
537
- const { endpoint } = resolved;
538
- const method = pickMethod(endpoint, spec.methods);
631
+ const { management } = resolved;
632
+ const method = management[spec.method];
539
633
  if (!method) {
540
634
  return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
541
635
  }
@@ -548,7 +642,7 @@ async function groupWriteOp(
548
642
  : undefined,
549
643
  enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
550
644
  });
551
- await method.call(endpoint, ...args);
645
+ await (method as (...args: unknown[]) => Promise<void>).call(management, ...args);
552
646
  return { data: { success: true } };
553
647
  } catch (error) {
554
648
  return { error: errorMessage(error) };
@@ -567,14 +661,13 @@ async function deleteFriend(
567
661
  }
568
662
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
569
663
  if ('error' in resolved) return resolved;
570
- const { endpoint } = resolved;
571
- const method = pickMethod(endpoint, ['deleteFriend', 'delete_friend']);
664
+ const { management } = resolved;
665
+ const method = management.deleteFriend;
572
666
  if (!method) {
573
667
  return { error: '当前适配器暂不支持删除好友' };
574
668
  }
575
669
  try {
576
- const numeric = Number(userId);
577
- await method.call(endpoint, Number.isFinite(numeric) && userId.trim() !== '' ? numeric : userId);
670
+ await method(userId);
578
671
  return { data: { success: true } };
579
672
  } catch (error) {
580
673
  return { error: errorMessage(error) };
@@ -609,24 +702,34 @@ function optionalNum(d: Record<string, unknown>, ...keys: string[]): number | un
609
702
  return undefined;
610
703
  }
611
704
 
612
- function pickMethod(
613
- endpoint: EndpointAny,
614
- names: readonly string[],
615
- ): ((...args: unknown[]) => unknown) | undefined {
616
- for (const name of names) {
617
- const fn = endpoint[name];
618
- if (typeof fn === 'function') return fn as (...args: unknown[]) => unknown;
705
+ /** 数字数组字段($row_ids 等):取首个数组值,收敛为有限数字。 */
706
+ function numArrayField(d: Record<string, unknown>, ...keys: string[]): number[] {
707
+ for (const key of keys) {
708
+ const value = d[key];
709
+ if (!Array.isArray(value)) continue;
710
+ return value
711
+ .map((item) => Number(item))
712
+ .filter((item) => Number.isFinite(item));
619
713
  }
620
- return undefined;
714
+ return [];
621
715
  }
622
716
 
623
- type ResolveOk = { endpoint: EndpointAny; adapter: string; endpointId: string };
717
+ type ResolveOk = {
718
+ management: EndpointManagementPort;
719
+ adapter: string;
720
+ endpointId: string;
721
+ };
624
722
 
625
723
  function resolveLiveEndpoint(
626
724
  ctx: ConsoleRpcExtendedCtx,
627
725
  adapter: string,
628
726
  endpointId: string,
629
727
  ): ResolveOk | { error: string } {
728
+ if (ctx.resolveEndpointManagement) {
729
+ const management = ctx.resolveEndpointManagement(adapter, endpointId);
730
+ if (!management) return { error: 'endpoint not found' };
731
+ return { management, adapter, endpointId };
732
+ }
630
733
  if (!ctx.resolveEndpoint) {
631
734
  return { error: 'Endpoint registry is not configured' };
632
735
  }
@@ -634,7 +737,14 @@ function resolveLiveEndpoint(
634
737
  if (!endpoint || typeof endpoint !== 'object') {
635
738
  return { error: 'endpoint not found' };
636
739
  }
637
- return { endpoint: endpoint as EndpointAny, adapter, endpointId };
740
+ const management = (endpoint as { management?: unknown }).management;
741
+ return {
742
+ management: management && typeof management === 'object'
743
+ ? management as EndpointManagementPort
744
+ : {},
745
+ adapter,
746
+ endpointId,
747
+ };
638
748
  }
639
749
 
640
750
  function requireEndpoint(
@@ -647,18 +757,6 @@ function requireEndpoint(
647
757
  return resolveLiveEndpoint(ctx, adapter, endpointId);
648
758
  }
649
759
 
650
- /** 兼容数组 / `{ data: [...] }` / `{ list: [...] }` 的列表返回。 */
651
- function unwrapList(raw: unknown): Record<string, unknown>[] {
652
- const arr = Array.isArray(raw)
653
- ? raw
654
- : raw && typeof raw === 'object'
655
- ? ((raw as Record<string, unknown>).data ?? (raw as Record<string, unknown>).list)
656
- : undefined;
657
- return (Array.isArray(arr) ? arr : []).filter(
658
- (item): item is Record<string, unknown> => !!item && typeof item === 'object',
659
- );
660
- }
661
-
662
760
  function normalizeParent(raw: unknown): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
663
761
  if (!raw || typeof raw !== 'object') return undefined;
664
762
  const parent = raw as { type?: unknown; id?: unknown; name?: unknown };