@zhin.js/host-http 1.0.1 → 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,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) };
@@ -160,7 +234,7 @@ async function listInbox(d, ctx, table, key, mapRow) {
160
234
  .map(mapRow);
161
235
  return { data: { [key]: sorted, inboxEnabled: enabled } };
162
236
  }
163
- /** endpoint:requests —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
237
+ /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
164
238
  async function listPendingRequests(d, ctx) {
165
239
  const adapter = strField(d, '$adapter', 'adapter');
166
240
  const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
@@ -218,56 +292,105 @@ async function listInboxMessages(d, ctx) {
218
292
  return { data: { messages, inboxEnabled: enabled } };
219
293
  }
220
294
  function mapRequestRow(row) {
295
+ const actorId = row.actor_id;
296
+ const actorName = row.actor_name ?? undefined;
297
+ const sceneId = row.scene_id;
298
+ const sceneType = row.scene_type ?? undefined;
221
299
  return {
222
300
  id: row.id,
223
301
  platform_request_id: row.platform_request_id,
302
+ // console endpoint-detail 期望的 camelCase/扁平别名(SSE 推送路径同款形状)
303
+ platformRequestId: row.platform_request_id,
224
304
  type: row.type,
225
- scene_type: row.scene_type ?? undefined,
226
- scene_id: row.scene_id,
305
+ scene_type: sceneType,
306
+ scene_id: sceneId,
307
+ channel_id: sceneId,
308
+ channel_type: sceneType,
309
+ channel: { id: sceneId, type: sceneType },
227
310
  sub_type: row.sub_type ?? undefined,
228
- actor: { id: row.actor_id, name: row.actor_name ?? undefined },
311
+ actor: { id: actorId, name: actorName },
312
+ sender: { id: actorId, name: actorName },
313
+ sender_id: actorId,
314
+ sender_name: actorName,
229
315
  comment: row.comment ?? undefined,
230
316
  created_at: row.created_at,
317
+ timestamp: row.created_at,
231
318
  resolved: row.resolved,
232
319
  resolved_at: row.resolved_at ?? undefined,
233
320
  };
234
321
  }
235
322
  function mapNoticeRow(row) {
323
+ const actorId = row.actor_id ?? undefined;
324
+ const actorName = row.actor_name ?? undefined;
325
+ const sceneId = row.scene_id;
326
+ const sceneType = row.scene_type ?? undefined;
236
327
  return {
237
328
  id: row.id,
238
329
  platform_notice_id: row.platform_notice_id,
239
330
  type: row.type,
240
- scene_type: row.scene_type ?? undefined,
241
- scene_id: row.scene_id,
331
+ scene_type: sceneType,
332
+ scene_id: sceneId,
333
+ channel_id: sceneId,
334
+ channel_type: sceneType,
242
335
  sub_type: row.sub_type ?? undefined,
243
- actor_id: row.actor_id ?? undefined,
244
- actor_name: row.actor_name ?? undefined,
336
+ actor_id: actorId,
337
+ actor_name: actorName,
338
+ // console endpoint-detail 期望的操作人字段别名
339
+ operator_id: actorId,
340
+ operator_name: actorName,
245
341
  target_id: row.target_id ?? undefined,
246
342
  target_name: row.target_name ?? undefined,
247
343
  payload: row.payload,
248
344
  created_at: row.created_at,
345
+ timestamp: row.created_at,
249
346
  };
250
347
  }
348
+ // ---------------------------------------------------------------- consumed 标记
349
+ /**
350
+ * request.consumed / notice.consumed —— 按行 id 置 consumed=1。
351
+ * 表未注册(或 model 无 update)时保持"未接线"报错。
352
+ */
353
+ async function markInboxConsumed(d, ctx, table) {
354
+ const ids = numArrayField(d, '$row_ids', 'row_ids', 'rowIds', '$ids', 'ids');
355
+ // console UI 单条已读发 data:{id},归一为数组
356
+ if (ids.length === 0) {
357
+ const single = Number(d.id ?? d.$id);
358
+ if (Number.isFinite(single) && single > 0)
359
+ ids.push(single);
360
+ }
361
+ if (ids.length === 0)
362
+ return { error: '$row_ids required' };
363
+ const model = getInboxModel(ctx, table);
364
+ if (!model || typeof model.update !== 'function')
365
+ return { error: CONSUMED_NOT_WIRED };
366
+ const now = Date.now();
367
+ try {
368
+ for (const id of ids) {
369
+ await model.update({ consumed: 1, consumed_at: now }).where({ id });
370
+ }
371
+ return { data: { success: true, updated: ids.length } };
372
+ }
373
+ catch (error) {
374
+ return { error: errorMessage(error) };
375
+ }
376
+ }
251
377
  // ---------------------------------------------------------------- 请求审批
252
378
  async function actOnRequest(type, d, ctx) {
253
379
  const adapter = strField(d, '$adapter', 'adapter');
254
380
  const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
255
- const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id');
381
+ const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
256
382
  if (!adapter || !endpointId || !requestId) {
257
383
  return { error: '$adapter, $endpoint, $id required' };
258
384
  }
259
385
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
260
386
  if ('error' in resolved)
261
387
  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);
388
+ const management = resolved.management;
389
+ const approve = type === 'request.approve';
390
+ const method = approve ? management.approveRequest : management.rejectRequest;
268
391
  if (!method) {
269
392
  return {
270
- error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${methodNames[0]},` +
393
+ error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
271
394
  '且新 Runtime 尚未挂载 pending request 注册表',
272
395
  };
273
396
  }
@@ -275,7 +398,7 @@ async function actOnRequest(type, d, ctx) {
275
398
  const extra = approve
276
399
  ? strField(d, '$remark', 'remark')
277
400
  : strField(d, '$reason', 'reason');
278
- await method.call(endpoint, requestId, extra || undefined);
401
+ await method.call(management, requestId, extra || undefined);
279
402
  return { data: { success: true } };
280
403
  }
281
404
  catch (error) {
@@ -287,25 +410,10 @@ async function listFriends(d, ctx) {
287
410
  const resolved = requireEndpoint(d, ctx);
288
411
  if ('error' in resolved)
289
412
  return resolved;
290
- const { endpoint, adapter } = resolved;
413
+ const { management, adapter } = resolved;
291
414
  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
- }));
415
+ if (management.listFriends) {
416
+ const friends = [...await management.listFriends()];
309
417
  return { data: { friends, count: friends.length } };
310
418
  }
311
419
  return { error: `当前适配器(${adapter})不支持好友列表` };
@@ -318,23 +426,10 @@ async function listGroups(d, ctx) {
318
426
  const resolved = requireEndpoint(d, ctx);
319
427
  if ('error' in resolved)
320
428
  return resolved;
321
- const { endpoint, adapter } = resolved;
429
+ const { management, adapter } = resolved;
322
430
  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
- }));
431
+ if (management.listGroups) {
432
+ const groups = [...await management.listGroups()];
338
433
  return { data: { groups, count: groups.length } };
339
434
  }
340
435
  return { error: `当前适配器(${adapter})不支持群列表` };
@@ -347,40 +442,10 @@ async function listChannels(d, ctx) {
347
442
  const resolved = requireEndpoint(d, ctx);
348
443
  if ('error' in resolved)
349
444
  return resolved;
350
- const { endpoint, adapter } = resolved;
445
+ const { management, adapter } = resolved;
351
446
  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
- }
447
+ if (management.listChannels) {
448
+ const channels = [...await management.listChannels()];
384
449
  return { data: { channels, count: channels.length } };
385
450
  }
386
451
  return { error: `当前适配器(${adapter})不支持频道列表` };
@@ -399,22 +464,14 @@ async function listGroupMembers(d, ctx) {
399
464
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
400
465
  if ('error' in resolved)
401
466
  return resolved;
402
- const { endpoint } = resolved;
467
+ const { management } = resolved;
403
468
  try {
404
- const method = pickMethod(endpoint, ['getGroupMemberList', 'listMembers', 'getMemberList']);
469
+ const method = management.listGroupMembers;
405
470
  if (!method) {
406
471
  return { error: `当前适配器(${adapter})不支持群成员列表` };
407
472
  }
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 } };
473
+ const members = [...await method(groupId)];
474
+ return { data: { members, count: members.length } };
418
475
  }
419
476
  catch (error) {
420
477
  return { error: errorMessage(error) };
@@ -434,8 +491,8 @@ async function groupWriteOp(d, ctx, spec) {
434
491
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
435
492
  if ('error' in resolved)
436
493
  return resolved;
437
- const { endpoint } = resolved;
438
- const method = pickMethod(endpoint, spec.methods);
494
+ const { management } = resolved;
495
+ const method = management[spec.method];
439
496
  if (!method) {
440
497
  return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
441
498
  }
@@ -448,7 +505,7 @@ async function groupWriteOp(d, ctx, spec) {
448
505
  : undefined,
449
506
  enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
450
507
  });
451
- await method.call(endpoint, ...args);
508
+ await method.call(management, ...args);
452
509
  return { data: { success: true } };
453
510
  }
454
511
  catch (error) {
@@ -465,14 +522,13 @@ async function deleteFriend(d, ctx) {
465
522
  const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
466
523
  if ('error' in resolved)
467
524
  return resolved;
468
- const { endpoint } = resolved;
469
- const method = pickMethod(endpoint, ['deleteFriend', 'delete_friend']);
525
+ const { management } = resolved;
526
+ const method = management.deleteFriend;
470
527
  if (!method) {
471
528
  return { error: '当前适配器暂不支持删除好友' };
472
529
  }
473
530
  try {
474
- const numeric = Number(userId);
475
- await method.call(endpoint, Number.isFinite(numeric) && userId.trim() !== '' ? numeric : userId);
531
+ await method(userId);
476
532
  return { data: { success: true } };
477
533
  }
478
534
  catch (error) {
@@ -508,15 +564,25 @@ function optionalNum(d, ...keys) {
508
564
  }
509
565
  return undefined;
510
566
  }
511
- function pickMethod(endpoint, names) {
512
- for (const name of names) {
513
- const fn = endpoint[name];
514
- if (typeof fn === 'function')
515
- return fn;
567
+ /** 数字数组字段($row_ids 等):取首个数组值,收敛为有限数字。 */
568
+ function numArrayField(d, ...keys) {
569
+ for (const key of keys) {
570
+ const value = d[key];
571
+ if (!Array.isArray(value))
572
+ continue;
573
+ return value
574
+ .map((item) => Number(item))
575
+ .filter((item) => Number.isFinite(item));
516
576
  }
517
- return undefined;
577
+ return [];
518
578
  }
519
579
  function resolveLiveEndpoint(ctx, adapter, endpointId) {
580
+ if (ctx.resolveEndpointManagement) {
581
+ const management = ctx.resolveEndpointManagement(adapter, endpointId);
582
+ if (!management)
583
+ return { error: 'endpoint not found' };
584
+ return { management, adapter, endpointId };
585
+ }
520
586
  if (!ctx.resolveEndpoint) {
521
587
  return { error: 'Endpoint registry is not configured' };
522
588
  }
@@ -524,7 +590,14 @@ function resolveLiveEndpoint(ctx, adapter, endpointId) {
524
590
  if (!endpoint || typeof endpoint !== 'object') {
525
591
  return { error: 'endpoint not found' };
526
592
  }
527
- return { endpoint: endpoint, adapter, endpointId };
593
+ const management = endpoint.management;
594
+ return {
595
+ management: management && typeof management === 'object'
596
+ ? management
597
+ : {},
598
+ adapter,
599
+ endpointId,
600
+ };
528
601
  }
529
602
  function requireEndpoint(d, ctx) {
530
603
  const adapter = strField(d, '$adapter', 'adapter');
@@ -533,15 +606,6 @@ function requireEndpoint(d, ctx) {
533
606
  return { error: '$adapter and $endpoint required' };
534
607
  return resolveLiveEndpoint(ctx, adapter, endpointId);
535
608
  }
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
609
  function normalizeParent(raw) {
546
610
  if (!raw || typeof raw !== 'object')
547
611
  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;