@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.
- package/lib/console-events.d.ts +13 -0
- package/lib/console-events.js +33 -0
- package/lib/console-rpc-extended.d.ts +44 -4
- package/lib/console-rpc-extended.js +228 -164
- package/lib/console-rpc.d.ts +9 -9
- package/lib/console-rpc.js +38 -61
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/package.json +3 -2
- package/src/console-events.ts +47 -0
- package/src/console-rpc-extended.ts +277 -179
- package/src/console-rpc.ts +52 -75
- package/src/index.ts +10 -0
|
@@ -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
|
-
*
|
|
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
|
|
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 = '
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
52
|
-
case '
|
|
42
|
+
return mutateCron(type, d, ctx);
|
|
43
|
+
case 'request.list':
|
|
53
44
|
return listPendingRequests(d, ctx);
|
|
54
|
-
case '
|
|
45
|
+
case 'inbox.requests':
|
|
55
46
|
return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
|
|
56
|
-
case '
|
|
47
|
+
case 'inbox.notices':
|
|
57
48
|
return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
|
|
58
|
-
case '
|
|
49
|
+
case 'inbox.messages':
|
|
59
50
|
return listInboxMessages(d, ctx);
|
|
60
|
-
case '
|
|
61
|
-
case '
|
|
51
|
+
case 'request.approve':
|
|
52
|
+
case 'request.reject':
|
|
62
53
|
return actOnRequest(type, d, ctx);
|
|
63
|
-
case '
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
60
|
+
case 'endpoint.groups':
|
|
69
61
|
return listGroups(d, ctx);
|
|
70
|
-
case 'endpoint
|
|
62
|
+
case 'endpoint.channels':
|
|
71
63
|
return listChannels(d, ctx);
|
|
72
|
-
case 'endpoint
|
|
64
|
+
case 'endpoint.group_members':
|
|
73
65
|
return listGroupMembers(d, ctx);
|
|
74
|
-
case 'endpoint
|
|
66
|
+
case 'endpoint.group_kick':
|
|
75
67
|
return groupWriteOp(d, ctx, {
|
|
76
|
-
|
|
68
|
+
method: 'kickGroupMember',
|
|
77
69
|
buildArgs: (gid, uid) => [gid, uid],
|
|
78
70
|
requireUser: true,
|
|
79
71
|
unsupported: '踢出群成员',
|
|
80
72
|
});
|
|
81
|
-
case 'endpoint
|
|
73
|
+
case 'endpoint.group_mute':
|
|
82
74
|
return groupWriteOp(d, ctx, {
|
|
83
|
-
|
|
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
|
|
80
|
+
case 'endpoint.group_admin':
|
|
89
81
|
return groupWriteOp(d, ctx, {
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
/**
|
|
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:
|
|
226
|
-
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:
|
|
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:
|
|
241
|
-
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:
|
|
244
|
-
actor_name:
|
|
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
|
|
263
|
-
const approve = type === '
|
|
264
|
-
const
|
|
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 不支持 ${
|
|
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(
|
|
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 {
|
|
413
|
+
const { management, adapter } = resolved;
|
|
291
414
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
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 {
|
|
429
|
+
const { management, adapter } = resolved;
|
|
322
430
|
try {
|
|
323
|
-
|
|
324
|
-
|
|
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 {
|
|
445
|
+
const { management, adapter } = resolved;
|
|
351
446
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
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 {
|
|
467
|
+
const { management } = resolved;
|
|
403
468
|
try {
|
|
404
|
-
const method =
|
|
469
|
+
const method = management.listGroupMembers;
|
|
405
470
|
if (!method) {
|
|
406
471
|
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
407
472
|
}
|
|
408
|
-
const
|
|
409
|
-
|
|
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 {
|
|
438
|
-
const method =
|
|
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(
|
|
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 {
|
|
469
|
-
const method =
|
|
525
|
+
const { management } = resolved;
|
|
526
|
+
const method = management.deleteFriend;
|
|
470
527
|
if (!method) {
|
|
471
528
|
return { error: '当前适配器暂不支持删除好友' };
|
|
472
529
|
}
|
|
473
530
|
try {
|
|
474
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/lib/console-rpc.d.ts
CHANGED
|
@@ -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 = '
|
|
70
|
-
|
|
71
|
-
|
|
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;
|