@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.
- package/README.md +3 -2
- package/lib/console-events.d.ts +13 -0
- package/lib/console-events.js +33 -0
- package/lib/console-rest-pages.js +11 -4
- package/lib/console-rpc-extended.d.ts +44 -4
- package/lib/console-rpc-extended.js +247 -167
- package/lib/console-rpc.d.ts +9 -9
- package/lib/console-rpc.js +38 -61
- package/lib/http-host.js +9 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/json-body.js +10 -2
- package/lib/project-files.js +10 -0
- package/package.json +3 -2
- package/src/console-events.ts +47 -0
- package/src/console-rest-pages.ts +13 -4
- package/src/console-rpc-extended.ts +313 -183
- package/src/console-rpc.ts +52 -75
- package/src/http-host.ts +9 -0
- package/src/index.ts +10 -0
- package/src/json-body.ts +8 -2
- package/src/project-files.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) };
|
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
226
|
-
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:
|
|
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:
|
|
241
|
-
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:
|
|
244
|
-
actor_name:
|
|
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
|
|
263
|
-
const approve = type === '
|
|
264
|
-
const
|
|
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 不支持 ${
|
|
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(
|
|
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 {
|
|
429
|
+
const { management, adapter } = resolved;
|
|
291
430
|
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
|
-
}));
|
|
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 {
|
|
445
|
+
const { management, adapter } = resolved;
|
|
322
446
|
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
|
-
}));
|
|
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 {
|
|
461
|
+
const { management, adapter } = resolved;
|
|
351
462
|
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
|
-
}
|
|
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 {
|
|
483
|
+
const { management } = resolved;
|
|
403
484
|
try {
|
|
404
|
-
const method =
|
|
485
|
+
const method = management.listGroupMembers;
|
|
405
486
|
if (!method) {
|
|
406
487
|
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
407
488
|
}
|
|
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 } };
|
|
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 {
|
|
438
|
-
const method =
|
|
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(
|
|
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 {
|
|
469
|
-
const method =
|
|
541
|
+
const { management } = resolved;
|
|
542
|
+
const method = management.deleteFriend;
|
|
470
543
|
if (!method) {
|
|
471
544
|
return { error: '当前适配器暂不支持删除好友' };
|
|
472
545
|
}
|
|
473
546
|
try {
|
|
474
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
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;
|