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