@zhin.js/host-http 1.0.1 → 1.0.3

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,90 +9,82 @@
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
- /** 新 Runtime 统一收件箱表名(与 packages/im/zhin/src/setup/register-inbox.ts 一致)。 */
17
+ /** 新 Runtime 统一收件箱表名(SSOT:@zhin.js/plugin-runtime inbox.js)。 */
18
+ import { assertDemoConsoleRpcAllowed, normalizeConsoleRpcType, } from '@zhin.js/console-protocol';
17
19
  const TABLE_MESSAGE = 'unified_inbox_message';
18
20
  const TABLE_REQUEST = 'unified_inbox_request';
19
21
  const TABLE_NOTICE = 'unified_inbox_notice';
20
- /** demo scope 只读放行清单之外的写操作 type。 */
21
- const WRITE_TYPES = new Set([
22
- 'cron:add',
23
- 'cron:remove',
24
- 'cron:pause',
25
- 'cron:resume',
26
- 'endpoint:requestApprove',
27
- 'endpoint:requestReject',
28
- 'endpoint:requestConsumed',
29
- 'endpoint:noticeConsumed',
30
- 'endpoint:groupKick',
31
- 'endpoint:groupMute',
32
- 'endpoint:groupAdmin',
33
- 'endpoint:deleteFriend',
34
- ]);
35
22
  const CRON_NOT_WIRED = '持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
36
23
  '不支持 add/remove/pause/resume(需要 @zhin.js/agent 持久化调度引擎)';
37
- const CONSUMED_NOT_WIRED = '收件箱已读标记未接线:新 Runtime 尚未提供 request/notice 的 consumed 写路径';
24
+ const CONSUMED_NOT_WIRED = '收件箱已读标记未接线:unified_inbox_request/notice 表未注册或 DatabaseHost 未启动';
38
25
  export async function dispatchExtendedConsoleRpc(type, data, ctx) {
39
26
  const d = data ?? {};
40
- if (WRITE_TYPES.has(type) && !ctx.fullScope) {
41
- return { error: `Demo scope: RPC "${type}" is forbidden` };
27
+ type = normalizeConsoleRpcType(type);
28
+ if (!ctx.fullScope) {
29
+ const denied = assertDemoConsoleRpcAllowed(type);
30
+ if (denied)
31
+ return { error: denied };
42
32
  }
43
33
  switch (type) {
44
34
  case 'schedule:list':
45
35
  case 'cron:list':
46
36
  return listSchedule(ctx);
47
37
  case 'cron:add':
38
+ return addCron(d, ctx);
48
39
  case 'cron:remove':
49
40
  case 'cron:pause':
50
41
  case 'cron:resume':
51
- return { error: CRON_NOT_WIRED };
52
- case 'endpoint:requests':
42
+ return mutateCron(type, d, ctx);
43
+ case 'request.list':
53
44
  return listPendingRequests(d, ctx);
54
- case 'endpoint:inboxRequests':
45
+ case 'inbox.requests':
55
46
  return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
56
- case 'endpoint:inboxNotices':
47
+ case 'inbox.notices':
57
48
  return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
58
- case 'endpoint:inboxMessages':
49
+ case 'inbox.messages':
59
50
  return listInboxMessages(d, ctx);
60
- case 'endpoint:requestApprove':
61
- case 'endpoint:requestReject':
51
+ case 'request.approve':
52
+ case 'request.reject':
62
53
  return actOnRequest(type, d, ctx);
63
- case 'endpoint:requestConsumed':
64
- case 'endpoint:noticeConsumed':
65
- return { error: CONSUMED_NOT_WIRED };
66
- case 'endpoint:friends':
54
+ case 'request.consumed':
55
+ return markInboxConsumed(d, ctx, TABLE_REQUEST);
56
+ case 'notice.consumed':
57
+ return markInboxConsumed(d, ctx, TABLE_NOTICE);
58
+ case 'endpoint.friends':
67
59
  return listFriends(d, ctx);
68
- case 'endpoint:groups':
60
+ case 'endpoint.groups':
69
61
  return listGroups(d, ctx);
70
- case 'endpoint:channels':
62
+ case 'endpoint.channels':
71
63
  return listChannels(d, ctx);
72
- case 'endpoint:groupMembers':
64
+ case 'endpoint.group_members':
73
65
  return listGroupMembers(d, ctx);
74
- case 'endpoint:groupKick':
66
+ case 'endpoint.group_kick':
75
67
  return groupWriteOp(d, ctx, {
76
- methods: ['removeMember', 'kickMember', 'setGroupKick'],
68
+ method: 'kickGroupMember',
77
69
  buildArgs: (gid, uid) => [gid, uid],
78
70
  requireUser: true,
79
71
  unsupported: '踢出群成员',
80
72
  });
81
- case 'endpoint:groupMute':
73
+ case 'endpoint.group_mute':
82
74
  return groupWriteOp(d, ctx, {
83
- methods: ['muteMember', 'setGroupBan', 'banMember'],
75
+ method: 'muteGroupMember',
84
76
  buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
85
77
  requireUser: true,
86
78
  unsupported: '禁言群成员',
87
79
  });
88
- case 'endpoint:groupAdmin':
80
+ case 'endpoint.group_admin':
89
81
  return groupWriteOp(d, ctx, {
90
- methods: ['setModerator', 'setGroupAdmin', 'setAdmin'],
82
+ method: 'setGroupAdmin',
91
83
  buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
92
84
  requireUser: true,
93
85
  unsupported: '设置群管理员',
94
86
  });
95
- case 'endpoint:deleteFriend':
87
+ case 'endpoint.delete_friend':
96
88
  return deleteFriend(d, ctx);
97
89
  default:
98
90
  return undefined;
@@ -114,9 +106,91 @@ function listSchedule(ctx) {
114
106
  id: job.id,
115
107
  cron: job.cron,
116
108
  ...(typeof job.description === 'string' ? { description: job.description } : {}),
109
+ // console cron 页渲染字段(由 schedule host 提供时透传)
110
+ expression: job.expression ?? job.cron,
111
+ running: job.running ?? true,
112
+ ...(job.plugin ? { plugin: job.plugin } : {}),
113
+ ...(job.nextExecution
114
+ ? { nextExecution: job.nextExecution } : {}),
117
115
  }));
118
- // persistent 引擎未接入新 Runtime,返回空数组保持 legacy 形状。
119
- return { data: { memory, persistent: [] } };
116
+ return listPersistentJobs(ctx).then((persistent) => ({ data: { memory, persistent } }));
117
+ }
118
+ catch (error) {
119
+ return { error: errorMessage(error) };
120
+ }
121
+ }
122
+ /** 持久化任务(Agent Host ScheduleJobEngine)→ 前端 PersistentCron 形状。 */
123
+ async function listPersistentJobs(ctx) {
124
+ const engine = ctx.resolveScheduleEngine?.();
125
+ if (!engine)
126
+ return [];
127
+ try {
128
+ const jobs = await engine.listJobs();
129
+ return jobs.map((job) => ({
130
+ id: job.id,
131
+ label: job.label ?? job.id,
132
+ cronExpression: job.schedule?.cron ?? '',
133
+ prompt: job.action?.prompt ?? '',
134
+ enabled: job.enabled !== false,
135
+ source: job.source ?? 'manual',
136
+ createdAt: job.createdAt,
137
+ nextExecution: job.state?.nextRunAtMs,
138
+ ...(job.notify?.target
139
+ ? { context: { target: job.notify.target } } : {}),
140
+ lastExecutedAt: job.state?.lastExecutedAt,
141
+ lastStatus: job.state?.lastStatus,
142
+ }));
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }
148
+ async function addCron(d, ctx) {
149
+ const engine = ctx.resolveScheduleEngine?.();
150
+ if (!engine)
151
+ return { error: CRON_NOT_WIRED };
152
+ const cronExpression = String(d.cronExpression ?? '').trim();
153
+ const prompt = String(d.prompt ?? '').trim();
154
+ if (!cronExpression)
155
+ return { error: 'cronExpression is required' };
156
+ if (!prompt)
157
+ return { error: 'prompt is required' };
158
+ const label = typeof d.label === 'string' && d.label.trim() ? d.label.trim() : undefined;
159
+ const context = (d.context ?? {});
160
+ const target = context.target ?? context.channel;
161
+ const job = {
162
+ id: `console-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
163
+ ...(label ? { label } : {}),
164
+ enabled: true,
165
+ schedule: { kind: 'solar', cron: cronExpression },
166
+ action: { kind: 'agent', prompt },
167
+ notify: target
168
+ ? { channel: 'im', target }
169
+ : { channel: 'silent' },
170
+ source: 'manual',
171
+ };
172
+ try {
173
+ const created = await engine.addJob(job);
174
+ return { data: { job: created, success: true } };
175
+ }
176
+ catch (error) {
177
+ return { error: errorMessage(error) };
178
+ }
179
+ }
180
+ async function mutateCron(type, d, ctx) {
181
+ const engine = ctx.resolveScheduleEngine?.();
182
+ if (!engine)
183
+ return { error: CRON_NOT_WIRED };
184
+ const id = String(d.id ?? '').trim();
185
+ if (!id)
186
+ return { error: 'id is required' };
187
+ try {
188
+ const ok = type === 'cron:remove'
189
+ ? await engine.removeJob(id)
190
+ : type === 'cron:pause'
191
+ ? await engine.pauseJob(id)
192
+ : await engine.resumeJob(id);
193
+ return ok ? { data: { success: true } } : { error: `任务不存在: ${id}` };
120
194
  }
121
195
  catch (error) {
122
196
  return { error: errorMessage(error) };
@@ -129,12 +203,19 @@ function getInboxModel(ctx, table) {
129
203
  return undefined;
130
204
  return model;
131
205
  }
132
- async function readInboxRows(ctx, table, where) {
206
+ async function readInboxRows(ctx, table, where, options) {
133
207
  const model = getInboxModel(ctx, table);
134
208
  if (!model)
135
209
  return { rows: [], enabled: false };
136
210
  try {
137
- const rows = await model.select().where(where);
211
+ let selection = model.select().where(where);
212
+ if (options?.orderBy && typeof selection.orderBy === 'function') {
213
+ selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
214
+ }
215
+ if (options?.limit != null && typeof selection.limit === 'function') {
216
+ selection = selection.limit(options.limit);
217
+ }
218
+ const rows = await selection;
138
219
  return { rows: Array.isArray(rows) ? rows : [], enabled: true };
139
220
  }
140
221
  catch {
@@ -152,6 +233,10 @@ async function listInbox(d, ctx, table, key, mapRow) {
152
233
  const { rows, enabled } = await readInboxRows(ctx, table, {
153
234
  adapter,
154
235
  endpoint_id: endpointId,
236
+ }, {
237
+ // sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
238
+ orderBy: { field: 'created_at', direction: 'DESC' },
239
+ limit: offset + limit,
155
240
  });
156
241
  const sorted = rows
157
242
  .slice()
@@ -160,7 +245,7 @@ async function listInbox(d, ctx, table, key, mapRow) {
160
245
  .map(mapRow);
161
246
  return { data: { [key]: sorted, inboxEnabled: enabled } };
162
247
  }
163
- /** endpoint:requests —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
248
+ /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
164
249
  async function listPendingRequests(d, ctx) {
165
250
  const adapter = strField(d, '$adapter', 'adapter');
166
251
  const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
@@ -169,6 +254,8 @@ async function listPendingRequests(d, ctx) {
169
254
  const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
170
255
  adapter,
171
256
  endpoint_id: endpointId,
257
+ }, {
258
+ orderBy: { field: 'created_at', direction: 'ASC' },
172
259
  });
173
260
  const requests = rows
174
261
  .filter((row) => Number(row.resolved ?? 0) === 0)
@@ -198,7 +285,10 @@ async function listInboxMessages(d, ctx) {
198
285
  where.channel_parent_type = parent.type;
199
286
  where.channel_parent_id = parent.id;
200
287
  }
201
- const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where);
288
+ const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where, {
289
+ // before_ts/before_id 过滤保留在内存(涉及双字段比较),排序下推 DB 侧
290
+ orderBy: { field: 'created_at', direction: 'DESC' },
291
+ });
202
292
  const messages = rows
203
293
  .filter((row) => (beforeTs == null || Number(row.created_at ?? 0) < beforeTs)
204
294
  && (beforeId == null || Number(row.id ?? 0) < beforeId))
@@ -218,56 +308,105 @@ async function listInboxMessages(d, ctx) {
218
308
  return { data: { messages, inboxEnabled: enabled } };
219
309
  }
220
310
  function mapRequestRow(row) {
311
+ const actorId = row.actor_id;
312
+ const actorName = row.actor_name ?? undefined;
313
+ const sceneId = row.scene_id;
314
+ const sceneType = row.scene_type ?? undefined;
221
315
  return {
222
316
  id: row.id,
223
317
  platform_request_id: row.platform_request_id,
318
+ // console endpoint-detail 期望的 camelCase/扁平别名(SSE 推送路径同款形状)
319
+ platformRequestId: row.platform_request_id,
224
320
  type: row.type,
225
- scene_type: row.scene_type ?? undefined,
226
- scene_id: row.scene_id,
321
+ scene_type: sceneType,
322
+ scene_id: sceneId,
323
+ channel_id: sceneId,
324
+ channel_type: sceneType,
325
+ channel: { id: sceneId, type: sceneType },
227
326
  sub_type: row.sub_type ?? undefined,
228
- actor: { id: row.actor_id, name: row.actor_name ?? undefined },
327
+ actor: { id: actorId, name: actorName },
328
+ sender: { id: actorId, name: actorName },
329
+ sender_id: actorId,
330
+ sender_name: actorName,
229
331
  comment: row.comment ?? undefined,
230
332
  created_at: row.created_at,
333
+ timestamp: row.created_at,
231
334
  resolved: row.resolved,
232
335
  resolved_at: row.resolved_at ?? undefined,
233
336
  };
234
337
  }
235
338
  function mapNoticeRow(row) {
339
+ const actorId = row.actor_id ?? undefined;
340
+ const actorName = row.actor_name ?? undefined;
341
+ const sceneId = row.scene_id;
342
+ const sceneType = row.scene_type ?? undefined;
236
343
  return {
237
344
  id: row.id,
238
345
  platform_notice_id: row.platform_notice_id,
239
346
  type: row.type,
240
- scene_type: row.scene_type ?? undefined,
241
- scene_id: row.scene_id,
347
+ scene_type: sceneType,
348
+ scene_id: sceneId,
349
+ channel_id: sceneId,
350
+ channel_type: sceneType,
242
351
  sub_type: row.sub_type ?? undefined,
243
- actor_id: row.actor_id ?? undefined,
244
- actor_name: row.actor_name ?? undefined,
352
+ actor_id: actorId,
353
+ actor_name: actorName,
354
+ // console endpoint-detail 期望的操作人字段别名
355
+ operator_id: actorId,
356
+ operator_name: actorName,
245
357
  target_id: row.target_id ?? undefined,
246
358
  target_name: row.target_name ?? undefined,
247
359
  payload: row.payload,
248
360
  created_at: row.created_at,
361
+ timestamp: row.created_at,
249
362
  };
250
363
  }
364
+ // ---------------------------------------------------------------- consumed 标记
365
+ /**
366
+ * request.consumed / notice.consumed —— 按行 id 置 consumed=1。
367
+ * 表未注册(或 model 无 update)时保持"未接线"报错。
368
+ */
369
+ async function markInboxConsumed(d, ctx, table) {
370
+ const ids = numArrayField(d, '$row_ids', 'row_ids', 'rowIds', '$ids', 'ids');
371
+ // console UI 单条已读发 data:{id},归一为数组
372
+ if (ids.length === 0) {
373
+ const single = Number(d.id ?? d.$id);
374
+ if (Number.isFinite(single) && single > 0)
375
+ ids.push(single);
376
+ }
377
+ if (ids.length === 0)
378
+ return { error: '$row_ids required' };
379
+ const model = getInboxModel(ctx, table);
380
+ if (!model || typeof model.update !== 'function')
381
+ return { error: CONSUMED_NOT_WIRED };
382
+ const now = Date.now();
383
+ try {
384
+ for (const id of ids) {
385
+ await model.update({ consumed: 1, consumed_at: now }).where({ id });
386
+ }
387
+ return { data: { success: true, updated: ids.length } };
388
+ }
389
+ catch (error) {
390
+ return { error: errorMessage(error) };
391
+ }
392
+ }
251
393
  // ---------------------------------------------------------------- 请求审批
252
394
  async function actOnRequest(type, d, ctx) {
253
395
  const adapter = strField(d, '$adapter', 'adapter');
254
396
  const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
255
- const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id');
397
+ const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
256
398
  if (!adapter || !endpointId || !requestId) {
257
399
  return { error: '$adapter, $endpoint, $id required' };
258
400
  }
259
401
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
260
402
  if ('error' in resolved)
261
403
  return resolved;
262
- const endpoint = resolved.endpoint;
263
- const approve = type === 'endpoint:requestApprove';
264
- const methodNames = approve
265
- ? ['approveRequest', 'approve_request', '$approveRequest']
266
- : ['rejectRequest', 'reject_request', '$rejectRequest'];
267
- const method = pickMethod(endpoint, methodNames);
404
+ const management = resolved.management;
405
+ const approve = type === 'request.approve';
406
+ const method = approve ? management.approveRequest : management.rejectRequest;
268
407
  if (!method) {
269
408
  return {
270
- error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${methodNames[0]},` +
409
+ error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
271
410
  '且新 Runtime 尚未挂载 pending request 注册表',
272
411
  };
273
412
  }
@@ -275,7 +414,7 @@ async function actOnRequest(type, d, ctx) {
275
414
  const extra = approve
276
415
  ? strField(d, '$remark', 'remark')
277
416
  : strField(d, '$reason', 'reason');
278
- await method.call(endpoint, requestId, extra || undefined);
417
+ await method.call(management, requestId, extra || undefined);
279
418
  return { data: { success: true } };
280
419
  }
281
420
  catch (error) {
@@ -287,25 +426,10 @@ async function listFriends(d, ctx) {
287
426
  const resolved = requireEndpoint(d, ctx);
288
427
  if ('error' in resolved)
289
428
  return resolved;
290
- const { endpoint, adapter } = resolved;
429
+ const { management, adapter } = resolved;
291
430
  try {
292
- const map = (endpoint.friends ?? endpoint.fl);
293
- if (map && typeof map.values === 'function') {
294
- const friends = Array.from(map.values()).map((f) => ({
295
- user_id: Number(f.user_id),
296
- nickname: String(f.nickname ?? ''),
297
- remark: String(f.remark ?? ''),
298
- }));
299
- return { data: { friends, count: friends.length } };
300
- }
301
- const getFriendList = pickMethod(endpoint, ['getFriendList', 'getFriends', 'listFriends']);
302
- if (getFriendList) {
303
- const raw = await getFriendList.call(endpoint);
304
- const friends = unwrapList(raw).map((row) => ({
305
- user_id: Number(row.user_id ?? row.userId ?? row.id ?? 0),
306
- nickname: String(row.nickname ?? row.name ?? row.user_id ?? ''),
307
- remark: String(row.remark ?? ''),
308
- }));
431
+ if (management.listFriends) {
432
+ const friends = [...await management.listFriends()];
309
433
  return { data: { friends, count: friends.length } };
310
434
  }
311
435
  return { error: `当前适配器(${adapter})不支持好友列表` };
@@ -318,23 +442,10 @@ async function listGroups(d, ctx) {
318
442
  const resolved = requireEndpoint(d, ctx);
319
443
  if ('error' in resolved)
320
444
  return resolved;
321
- const { endpoint, adapter } = resolved;
445
+ const { management, adapter } = resolved;
322
446
  try {
323
- const map = (endpoint.groups ?? endpoint.gl);
324
- if (map && typeof map.values === 'function') {
325
- const groups = Array.from(map.values()).map((g) => ({
326
- group_id: Number(g.group_id),
327
- name: String(g.group_name ?? g.name ?? g.group_id ?? ''),
328
- }));
329
- return { data: { groups, count: groups.length } };
330
- }
331
- const getGroupList = pickMethod(endpoint, ['getGroupList', 'getGroups', 'listGroups']);
332
- if (getGroupList) {
333
- const raw = await getGroupList.call(endpoint);
334
- const groups = unwrapList(raw).map((row) => ({
335
- group_id: Number(row.group_id ?? row.groupId ?? row.id ?? 0),
336
- name: String(row.name ?? row.group_name ?? row.group_id ?? ''),
337
- }));
447
+ if (management.listGroups) {
448
+ const groups = [...await management.listGroups()];
338
449
  return { data: { groups, count: groups.length } };
339
450
  }
340
451
  return { error: `当前适配器(${adapter})不支持群列表` };
@@ -347,40 +458,10 @@ async function listChannels(d, ctx) {
347
458
  const resolved = requireEndpoint(d, ctx);
348
459
  if ('error' in resolved)
349
460
  return resolved;
350
- const { endpoint, adapter } = resolved;
461
+ const { management, adapter } = resolved;
351
462
  try {
352
- const channels = [];
353
- const getGuildChannelList = pickMethod(endpoint, ['getGuildChannelList']);
354
- if (getGuildChannelList) {
355
- const list = await getGuildChannelList.call(endpoint);
356
- for (const item of Array.isArray(list) ? list : []) {
357
- const parent = normalizeParent(item.parent);
358
- channels.push({
359
- id: String(item.id ?? ''),
360
- ...(item.name != null ? { name: String(item.name) } : {}),
361
- ...(parent ? { parent } : {}),
362
- });
363
- }
364
- return { data: { channels, count: channels.length } };
365
- }
366
- const getGuilds = pickMethod(endpoint, ['getGuilds', 'listGuilds']);
367
- const getChannels = pickMethod(endpoint, ['getChannels', 'listChannels']);
368
- if (getGuilds && getChannels) {
369
- const guilds = unwrapList(await getGuilds.call(endpoint));
370
- for (const g of guilds) {
371
- const gid = String(g.id ?? g.guild_id ?? '');
372
- if (!gid)
373
- continue;
374
- const guildName = String(g.name ?? g.guild_name ?? gid);
375
- const chs = unwrapList(await getChannels.call(endpoint, gid));
376
- for (const c of chs) {
377
- channels.push({
378
- id: String(c.id ?? c.channel_id ?? ''),
379
- name: String(c.name ?? c.channel_name ?? c.id ?? ''),
380
- parent: { type: 'guild', id: gid, name: guildName },
381
- });
382
- }
383
- }
463
+ if (management.listChannels) {
464
+ const channels = [...await management.listChannels()];
384
465
  return { data: { channels, count: channels.length } };
385
466
  }
386
467
  return { error: `当前适配器(${adapter})不支持频道列表` };
@@ -399,22 +480,14 @@ async function listGroupMembers(d, ctx) {
399
480
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
400
481
  if ('error' in resolved)
401
482
  return resolved;
402
- const { endpoint } = resolved;
483
+ const { management } = resolved;
403
484
  try {
404
- const method = pickMethod(endpoint, ['getGroupMemberList', 'listMembers', 'getMemberList']);
485
+ const method = management.listGroupMembers;
405
486
  if (!method) {
406
487
  return { error: `当前适配器(${adapter})不支持群成员列表` };
407
488
  }
408
- const raw = await method.call(endpoint, groupId);
409
- if (Array.isArray(raw)) {
410
- return { data: { members: raw, count: raw.length } };
411
- }
412
- // Map(icqq 风格 gml)→ values 数组
413
- if (raw && typeof raw.values === 'function') {
414
- const members = Array.from(raw.values());
415
- return { data: { members, count: members.length } };
416
- }
417
- return { data: raw ?? { members: [], count: 0 } };
489
+ const members = [...await method(groupId)];
490
+ return { data: { members, count: members.length } };
418
491
  }
419
492
  catch (error) {
420
493
  return { error: errorMessage(error) };
@@ -434,8 +507,8 @@ async function groupWriteOp(d, ctx, spec) {
434
507
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
435
508
  if ('error' in resolved)
436
509
  return resolved;
437
- const { endpoint } = resolved;
438
- const method = pickMethod(endpoint, spec.methods);
510
+ const { management } = resolved;
511
+ const method = management[spec.method];
439
512
  if (!method) {
440
513
  return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
441
514
  }
@@ -448,7 +521,7 @@ async function groupWriteOp(d, ctx, spec) {
448
521
  : undefined,
449
522
  enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
450
523
  });
451
- await method.call(endpoint, ...args);
524
+ await method.call(management, ...args);
452
525
  return { data: { success: true } };
453
526
  }
454
527
  catch (error) {
@@ -465,14 +538,13 @@ async function deleteFriend(d, ctx) {
465
538
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
466
539
  if ('error' in resolved)
467
540
  return resolved;
468
- const { endpoint } = resolved;
469
- const method = pickMethod(endpoint, ['deleteFriend', 'delete_friend']);
541
+ const { management } = resolved;
542
+ const method = management.deleteFriend;
470
543
  if (!method) {
471
544
  return { error: '当前适配器暂不支持删除好友' };
472
545
  }
473
546
  try {
474
- const numeric = Number(userId);
475
- await method.call(endpoint, Number.isFinite(numeric) && userId.trim() !== '' ? numeric : userId);
547
+ await method(userId);
476
548
  return { data: { success: true } };
477
549
  }
478
550
  catch (error) {
@@ -508,15 +580,25 @@ function optionalNum(d, ...keys) {
508
580
  }
509
581
  return undefined;
510
582
  }
511
- function pickMethod(endpoint, names) {
512
- for (const name of names) {
513
- const fn = endpoint[name];
514
- if (typeof fn === 'function')
515
- return fn;
583
+ /** 数字数组字段($row_ids 等):取首个数组值,收敛为有限数字。 */
584
+ function numArrayField(d, ...keys) {
585
+ for (const key of keys) {
586
+ const value = d[key];
587
+ if (!Array.isArray(value))
588
+ continue;
589
+ return value
590
+ .map((item) => Number(item))
591
+ .filter((item) => Number.isFinite(item));
516
592
  }
517
- return undefined;
593
+ return [];
518
594
  }
519
595
  function resolveLiveEndpoint(ctx, adapter, endpointId) {
596
+ if (ctx.resolveEndpointManagement) {
597
+ const management = ctx.resolveEndpointManagement(adapter, endpointId);
598
+ if (!management)
599
+ return { error: 'endpoint not found' };
600
+ return { management, adapter, endpointId };
601
+ }
520
602
  if (!ctx.resolveEndpoint) {
521
603
  return { error: 'Endpoint registry is not configured' };
522
604
  }
@@ -524,7 +606,14 @@ function resolveLiveEndpoint(ctx, adapter, endpointId) {
524
606
  if (!endpoint || typeof endpoint !== 'object') {
525
607
  return { error: 'endpoint not found' };
526
608
  }
527
- return { endpoint: endpoint, adapter, endpointId };
609
+ const management = endpoint.management;
610
+ return {
611
+ management: management && typeof management === 'object'
612
+ ? management
613
+ : {},
614
+ adapter,
615
+ endpointId,
616
+ };
528
617
  }
529
618
  function requireEndpoint(d, ctx) {
530
619
  const adapter = strField(d, '$adapter', 'adapter');
@@ -533,15 +622,6 @@ function requireEndpoint(d, ctx) {
533
622
  return { error: '$adapter and $endpoint required' };
534
623
  return resolveLiveEndpoint(ctx, adapter, endpointId);
535
624
  }
536
- /** 兼容数组 / `{ data: [...] }` / `{ list: [...] }` 的列表返回。 */
537
- function unwrapList(raw) {
538
- const arr = Array.isArray(raw)
539
- ? raw
540
- : raw && typeof raw === 'object'
541
- ? (raw.data ?? raw.list)
542
- : undefined;
543
- return (Array.isArray(arr) ? arr : []).filter((item) => !!item && typeof item === 'object');
544
- }
545
625
  function normalizeParent(raw) {
546
626
  if (!raw || typeof raw !== 'object')
547
627
  return undefined;
@@ -1,5 +1,6 @@
1
1
  import type { AuthScope } from './token-registry.js';
2
2
  import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
3
+ import { type ConsoleEndpointSummary } from '@zhin.js/console-protocol';
3
4
  import { type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
4
5
  export type RuntimeConsoleRpcMessage = {
5
6
  readonly type?: unknown;
@@ -63,18 +64,17 @@ export type RuntimeConsoleRpcContext = {
63
64
  database?: DatabaseHostConsole;
64
65
  /** Full-scope: request process restart (CLI daemon watches exit code). */
65
66
  requestRestart?(): Promise<void> | void;
67
+ /**
68
+ * Optional console event sink (`/api/events` SSE fan-out). Called after
69
+ * successful mutations: `config:updated` / `system:restarting`.
70
+ */
71
+ publishEvent?(type: string, data: unknown): void;
66
72
  /** Extended RPC surface(cron/schedule、endpoint 社交/inbox),fullScope 由 authScope 推导。 */
67
73
  extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
68
74
  };
69
- export type RuntimeEndpointPhase = 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
70
- export type RuntimeEndpointSummary = {
71
- readonly name: string;
72
- readonly adapter: string;
73
- readonly connected: boolean;
74
- readonly status: 'online' | 'offline';
75
- readonly phase?: RuntimeEndpointPhase;
76
- readonly pendingLogin?: boolean;
77
- };
75
+ export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
76
+ /** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
77
+ export type RuntimeEndpointSummary = ConsoleEndpointSummary;
78
78
  export type RuntimeDatabaseInfo = {
79
79
  readonly dialect: string | null;
80
80
  readonly connected: boolean;