@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,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;
|
|
@@ -62,13 +67,41 @@ interface ScheduleJobRow {
|
|
|
62
67
|
description?: string;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
interface InboxSelection {
|
|
71
|
+
where(query: Record<string, unknown>): InboxSelection;
|
|
72
|
+
/** 可选:DB 侧排序下推(wrapModel 链具备该能力,轻量 fake 可缺失)。 */
|
|
73
|
+
orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
|
|
74
|
+
/** 可选:DB 侧 limit 下推。 */
|
|
75
|
+
limit?(count: number): InboxSelection;
|
|
76
|
+
then<TResult1 = Record<string, unknown>[], TResult2 = never>(
|
|
77
|
+
onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
78
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
79
|
+
): Promise<TResult1 | TResult2>;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
interface InboxModel {
|
|
66
|
-
select():
|
|
67
|
-
|
|
83
|
+
select(): InboxSelection;
|
|
84
|
+
update?(patch: Record<string, unknown>): {
|
|
85
|
+
where(query: Record<string, unknown>): Promise<unknown> | unknown;
|
|
68
86
|
};
|
|
69
87
|
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
interface EndpointManagementPort {
|
|
90
|
+
listFriends?(): Promise<readonly { user_id: number | string; nickname: string; remark: string }[]>;
|
|
91
|
+
listGroups?(): Promise<readonly { group_id: number | string; name: string }[]>;
|
|
92
|
+
listChannels?(): Promise<readonly {
|
|
93
|
+
id: string;
|
|
94
|
+
name?: string;
|
|
95
|
+
parent?: { type: string; id: string; name?: string };
|
|
96
|
+
}[]>;
|
|
97
|
+
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
98
|
+
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
99
|
+
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
100
|
+
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
101
|
+
muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
|
|
102
|
+
setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
|
|
103
|
+
deleteFriend?(userId: string): Promise<void>;
|
|
104
|
+
}
|
|
72
105
|
|
|
73
106
|
export async function dispatchExtendedConsoleRpc(
|
|
74
107
|
type: string,
|
|
@@ -76,9 +109,11 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
76
109
|
ctx: ConsoleRpcExtendedCtx,
|
|
77
110
|
): Promise<ExtendedRpcResult | undefined> {
|
|
78
111
|
const d = data ?? {};
|
|
112
|
+
type = normalizeConsoleRpcType(type);
|
|
79
113
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
114
|
+
if (!ctx.fullScope) {
|
|
115
|
+
const denied = assertDemoConsoleRpcAllowed(type);
|
|
116
|
+
if (denied) return { error: denied };
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
switch (type) {
|
|
@@ -87,59 +122,61 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
87
122
|
return listSchedule(ctx);
|
|
88
123
|
|
|
89
124
|
case 'cron:add':
|
|
125
|
+
return addCron(d, ctx);
|
|
90
126
|
case 'cron:remove':
|
|
91
127
|
case 'cron:pause':
|
|
92
128
|
case 'cron:resume':
|
|
93
|
-
return
|
|
129
|
+
return mutateCron(type, d, ctx);
|
|
94
130
|
|
|
95
|
-
case '
|
|
131
|
+
case 'request.list':
|
|
96
132
|
return listPendingRequests(d, ctx);
|
|
97
|
-
case '
|
|
133
|
+
case 'inbox.requests':
|
|
98
134
|
return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
|
|
99
|
-
case '
|
|
135
|
+
case 'inbox.notices':
|
|
100
136
|
return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
|
|
101
|
-
case '
|
|
137
|
+
case 'inbox.messages':
|
|
102
138
|
return listInboxMessages(d, ctx);
|
|
103
139
|
|
|
104
|
-
case '
|
|
105
|
-
case '
|
|
140
|
+
case 'request.approve':
|
|
141
|
+
case 'request.reject':
|
|
106
142
|
return actOnRequest(type, d, ctx);
|
|
107
143
|
|
|
108
|
-
case '
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
case 'request.consumed':
|
|
145
|
+
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
146
|
+
case 'notice.consumed':
|
|
147
|
+
return markInboxConsumed(d, ctx, TABLE_NOTICE);
|
|
111
148
|
|
|
112
|
-
case 'endpoint
|
|
149
|
+
case 'endpoint.friends':
|
|
113
150
|
return listFriends(d, ctx);
|
|
114
|
-
case 'endpoint
|
|
151
|
+
case 'endpoint.groups':
|
|
115
152
|
return listGroups(d, ctx);
|
|
116
|
-
case 'endpoint
|
|
153
|
+
case 'endpoint.channels':
|
|
117
154
|
return listChannels(d, ctx);
|
|
118
|
-
case 'endpoint
|
|
155
|
+
case 'endpoint.group_members':
|
|
119
156
|
return listGroupMembers(d, ctx);
|
|
120
157
|
|
|
121
|
-
case 'endpoint
|
|
158
|
+
case 'endpoint.group_kick':
|
|
122
159
|
return groupWriteOp(d, ctx, {
|
|
123
|
-
|
|
160
|
+
method: 'kickGroupMember',
|
|
124
161
|
buildArgs: (gid, uid) => [gid, uid],
|
|
125
162
|
requireUser: true,
|
|
126
163
|
unsupported: '踢出群成员',
|
|
127
164
|
});
|
|
128
|
-
case 'endpoint
|
|
165
|
+
case 'endpoint.group_mute':
|
|
129
166
|
return groupWriteOp(d, ctx, {
|
|
130
|
-
|
|
167
|
+
method: 'muteGroupMember',
|
|
131
168
|
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
132
169
|
requireUser: true,
|
|
133
170
|
unsupported: '禁言群成员',
|
|
134
171
|
});
|
|
135
|
-
case 'endpoint
|
|
172
|
+
case 'endpoint.group_admin':
|
|
136
173
|
return groupWriteOp(d, ctx, {
|
|
137
|
-
|
|
174
|
+
method: 'setGroupAdmin',
|
|
138
175
|
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
139
176
|
requireUser: true,
|
|
140
177
|
unsupported: '设置群管理员',
|
|
141
178
|
});
|
|
142
|
-
case 'endpoint
|
|
179
|
+
case 'endpoint.delete_friend':
|
|
143
180
|
return deleteFriend(d, ctx);
|
|
144
181
|
|
|
145
182
|
default:
|
|
@@ -149,7 +186,7 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
149
186
|
|
|
150
187
|
// ---------------------------------------------------------------- cron
|
|
151
188
|
|
|
152
|
-
function listSchedule(ctx:
|
|
189
|
+
function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
|
|
153
190
|
const host = ctx.scheduleHost as { list?: () => unknown } | undefined;
|
|
154
191
|
if (!host || typeof host.list !== 'function') {
|
|
155
192
|
return { error: '调度服务未配置(scheduleHost 未挂载)' };
|
|
@@ -165,9 +202,94 @@ function listSchedule(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
|
|
|
165
202
|
id: job.id,
|
|
166
203
|
cron: job.cron,
|
|
167
204
|
...(typeof job.description === 'string' ? { description: job.description } : {}),
|
|
205
|
+
// console cron 页渲染字段(由 schedule host 提供时透传)
|
|
206
|
+
expression: (job as { expression?: string }).expression ?? job.cron,
|
|
207
|
+
running: (job as { running?: boolean }).running ?? true,
|
|
208
|
+
...((job as { plugin?: string }).plugin ? { plugin: (job as { plugin?: string }).plugin } : {}),
|
|
209
|
+
...((job as { nextExecution?: number }).nextExecution
|
|
210
|
+
? { nextExecution: (job as { nextExecution?: number }).nextExecution } : {}),
|
|
168
211
|
}));
|
|
169
|
-
|
|
170
|
-
|
|
212
|
+
return listPersistentJobs(ctx).then((persistent) => ({ data: { memory, persistent } }));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { error: errorMessage(error) };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ConsoleScheduleListCtx = ConsoleRpcExtendedCtx;
|
|
219
|
+
|
|
220
|
+
/** 持久化任务(Agent Host ScheduleJobEngine)→ 前端 PersistentCron 形状。 */
|
|
221
|
+
async function listPersistentJobs(ctx: ConsoleRpcExtendedCtx): Promise<Record<string, unknown>[]> {
|
|
222
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
223
|
+
if (!engine) return [];
|
|
224
|
+
try {
|
|
225
|
+
const jobs = await engine.listJobs();
|
|
226
|
+
return jobs.map((job) => ({
|
|
227
|
+
id: job.id,
|
|
228
|
+
label: job.label ?? job.id,
|
|
229
|
+
cronExpression: (job.schedule as { cron?: string } | undefined)?.cron ?? '',
|
|
230
|
+
prompt: (job.action as { prompt?: string } | undefined)?.prompt ?? '',
|
|
231
|
+
enabled: job.enabled !== false,
|
|
232
|
+
source: job.source ?? 'manual',
|
|
233
|
+
createdAt: (job as { createdAt?: number }).createdAt,
|
|
234
|
+
nextExecution: (job.state as { nextRunAtMs?: number } | undefined)?.nextRunAtMs,
|
|
235
|
+
...((job.notify as { target?: unknown } | undefined)?.target
|
|
236
|
+
? { context: { target: (job.notify as { target?: unknown }).target } } : {}),
|
|
237
|
+
lastExecutedAt: (job.state as { lastExecutedAt?: number } | undefined)?.lastExecutedAt,
|
|
238
|
+
lastStatus: (job.state as { lastStatus?: string } | undefined)?.lastStatus,
|
|
239
|
+
}));
|
|
240
|
+
} catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function addCron(
|
|
246
|
+
d: Record<string, unknown>,
|
|
247
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
248
|
+
): Promise<ExtendedRpcResult> {
|
|
249
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
250
|
+
if (!engine) return { error: CRON_NOT_WIRED };
|
|
251
|
+
const cronExpression = String(d.cronExpression ?? '').trim();
|
|
252
|
+
const prompt = String(d.prompt ?? '').trim();
|
|
253
|
+
if (!cronExpression) return { error: 'cronExpression is required' };
|
|
254
|
+
if (!prompt) return { error: 'prompt is required' };
|
|
255
|
+
const label = typeof d.label === 'string' && d.label.trim() ? d.label.trim() : undefined;
|
|
256
|
+
const context = (d.context ?? {}) as Record<string, unknown>;
|
|
257
|
+
const target = context.target ?? context.channel;
|
|
258
|
+
const job = {
|
|
259
|
+
id: `console-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
260
|
+
...(label ? { label } : {}),
|
|
261
|
+
enabled: true,
|
|
262
|
+
schedule: { kind: 'solar', cron: cronExpression },
|
|
263
|
+
action: { kind: 'agent', prompt },
|
|
264
|
+
notify: target
|
|
265
|
+
? { channel: 'im', target }
|
|
266
|
+
: { channel: 'silent' },
|
|
267
|
+
source: 'manual',
|
|
268
|
+
};
|
|
269
|
+
try {
|
|
270
|
+
const created = await engine.addJob(job);
|
|
271
|
+
return { data: { job: created, success: true } };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { error: errorMessage(error) };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function mutateCron(
|
|
278
|
+
type: 'cron:remove' | 'cron:pause' | 'cron:resume',
|
|
279
|
+
d: Record<string, unknown>,
|
|
280
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
281
|
+
): Promise<ExtendedRpcResult> {
|
|
282
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
283
|
+
if (!engine) return { error: CRON_NOT_WIRED };
|
|
284
|
+
const id = String(d.id ?? '').trim();
|
|
285
|
+
if (!id) return { error: 'id is required' };
|
|
286
|
+
try {
|
|
287
|
+
const ok = type === 'cron:remove'
|
|
288
|
+
? await engine.removeJob(id)
|
|
289
|
+
: type === 'cron:pause'
|
|
290
|
+
? await engine.pauseJob(id)
|
|
291
|
+
: await engine.resumeJob(id);
|
|
292
|
+
return ok ? { data: { success: true } } : { error: `任务不存在: ${id}` };
|
|
171
293
|
} catch (error) {
|
|
172
294
|
return { error: errorMessage(error) };
|
|
173
295
|
}
|
|
@@ -185,11 +307,24 @@ async function readInboxRows(
|
|
|
185
307
|
ctx: ConsoleRpcExtendedCtx,
|
|
186
308
|
table: string,
|
|
187
309
|
where: Record<string, unknown>,
|
|
310
|
+
options?: {
|
|
311
|
+
/** DB 侧排序下推(模型不支持时由调用方内存排序兜底)。 */
|
|
312
|
+
orderBy?: { field: string; direction?: 'ASC' | 'DESC' };
|
|
313
|
+
/** DB 侧 limit 下推(模型不支持时由调用方内存分页兜底)。 */
|
|
314
|
+
limit?: number;
|
|
315
|
+
},
|
|
188
316
|
): Promise<{ rows: Record<string, unknown>[]; enabled: boolean }> {
|
|
189
317
|
const model = getInboxModel(ctx, table);
|
|
190
318
|
if (!model) return { rows: [], enabled: false };
|
|
191
319
|
try {
|
|
192
|
-
|
|
320
|
+
let selection: InboxSelection = model.select().where(where);
|
|
321
|
+
if (options?.orderBy && typeof selection.orderBy === 'function') {
|
|
322
|
+
selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
|
|
323
|
+
}
|
|
324
|
+
if (options?.limit != null && typeof selection.limit === 'function') {
|
|
325
|
+
selection = selection.limit(options.limit);
|
|
326
|
+
}
|
|
327
|
+
const rows = await selection;
|
|
193
328
|
return { rows: Array.isArray(rows) ? rows : [], enabled: true };
|
|
194
329
|
} catch {
|
|
195
330
|
// 表未创建 / 方言未启动等场景降级为空,不向上抛。
|
|
@@ -212,6 +347,10 @@ async function listInbox(
|
|
|
212
347
|
const { rows, enabled } = await readInboxRows(ctx, table, {
|
|
213
348
|
adapter,
|
|
214
349
|
endpoint_id: endpointId,
|
|
350
|
+
}, {
|
|
351
|
+
// sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
|
|
352
|
+
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
353
|
+
limit: offset + limit,
|
|
215
354
|
});
|
|
216
355
|
const sorted = rows
|
|
217
356
|
.slice()
|
|
@@ -221,7 +360,7 @@ async function listInbox(
|
|
|
221
360
|
return { data: { [key]: sorted, inboxEnabled: enabled } };
|
|
222
361
|
}
|
|
223
362
|
|
|
224
|
-
/**
|
|
363
|
+
/** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
|
|
225
364
|
async function listPendingRequests(
|
|
226
365
|
d: Record<string, unknown>,
|
|
227
366
|
ctx: ConsoleRpcExtendedCtx,
|
|
@@ -232,6 +371,8 @@ async function listPendingRequests(
|
|
|
232
371
|
const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
|
|
233
372
|
adapter,
|
|
234
373
|
endpoint_id: endpointId,
|
|
374
|
+
}, {
|
|
375
|
+
orderBy: { field: 'created_at', direction: 'ASC' },
|
|
235
376
|
});
|
|
236
377
|
const requests = rows
|
|
237
378
|
.filter((row) => Number(row.resolved ?? 0) === 0)
|
|
@@ -266,7 +407,10 @@ async function listInboxMessages(
|
|
|
266
407
|
where.channel_parent_type = parent.type;
|
|
267
408
|
where.channel_parent_id = parent.id;
|
|
268
409
|
}
|
|
269
|
-
const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where
|
|
410
|
+
const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where, {
|
|
411
|
+
// before_ts/before_id 过滤保留在内存(涉及双字段比较),排序下推 DB 侧
|
|
412
|
+
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
413
|
+
});
|
|
270
414
|
const messages = rows
|
|
271
415
|
.filter((row) => (beforeTs == null || Number(row.created_at ?? 0) < beforeTs)
|
|
272
416
|
&& (beforeId == null || Number(row.id ?? 0) < beforeId))
|
|
@@ -287,62 +431,113 @@ async function listInboxMessages(
|
|
|
287
431
|
}
|
|
288
432
|
|
|
289
433
|
function mapRequestRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
434
|
+
const actorId = row.actor_id;
|
|
435
|
+
const actorName = row.actor_name ?? undefined;
|
|
436
|
+
const sceneId = row.scene_id;
|
|
437
|
+
const sceneType = row.scene_type ?? undefined;
|
|
290
438
|
return {
|
|
291
439
|
id: row.id,
|
|
292
440
|
platform_request_id: row.platform_request_id,
|
|
441
|
+
// console endpoint-detail 期望的 camelCase/扁平别名(SSE 推送路径同款形状)
|
|
442
|
+
platformRequestId: row.platform_request_id,
|
|
293
443
|
type: row.type,
|
|
294
|
-
scene_type:
|
|
295
|
-
scene_id:
|
|
444
|
+
scene_type: sceneType,
|
|
445
|
+
scene_id: sceneId,
|
|
446
|
+
channel_id: sceneId,
|
|
447
|
+
channel_type: sceneType,
|
|
448
|
+
channel: { id: sceneId, type: sceneType },
|
|
296
449
|
sub_type: row.sub_type ?? undefined,
|
|
297
|
-
actor: { id:
|
|
450
|
+
actor: { id: actorId, name: actorName },
|
|
451
|
+
sender: { id: actorId, name: actorName },
|
|
452
|
+
sender_id: actorId,
|
|
453
|
+
sender_name: actorName,
|
|
298
454
|
comment: row.comment ?? undefined,
|
|
299
455
|
created_at: row.created_at,
|
|
456
|
+
timestamp: row.created_at,
|
|
300
457
|
resolved: row.resolved,
|
|
301
458
|
resolved_at: row.resolved_at ?? undefined,
|
|
302
459
|
};
|
|
303
460
|
}
|
|
304
461
|
|
|
305
462
|
function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
463
|
+
const actorId = row.actor_id ?? undefined;
|
|
464
|
+
const actorName = row.actor_name ?? undefined;
|
|
465
|
+
const sceneId = row.scene_id;
|
|
466
|
+
const sceneType = row.scene_type ?? undefined;
|
|
306
467
|
return {
|
|
307
468
|
id: row.id,
|
|
308
469
|
platform_notice_id: row.platform_notice_id,
|
|
309
470
|
type: row.type,
|
|
310
|
-
scene_type:
|
|
311
|
-
scene_id:
|
|
471
|
+
scene_type: sceneType,
|
|
472
|
+
scene_id: sceneId,
|
|
473
|
+
channel_id: sceneId,
|
|
474
|
+
channel_type: sceneType,
|
|
312
475
|
sub_type: row.sub_type ?? undefined,
|
|
313
|
-
actor_id:
|
|
314
|
-
actor_name:
|
|
476
|
+
actor_id: actorId,
|
|
477
|
+
actor_name: actorName,
|
|
478
|
+
// console endpoint-detail 期望的操作人字段别名
|
|
479
|
+
operator_id: actorId,
|
|
480
|
+
operator_name: actorName,
|
|
315
481
|
target_id: row.target_id ?? undefined,
|
|
316
482
|
target_name: row.target_name ?? undefined,
|
|
317
483
|
payload: row.payload,
|
|
318
484
|
created_at: row.created_at,
|
|
485
|
+
timestamp: row.created_at,
|
|
319
486
|
};
|
|
320
487
|
}
|
|
321
488
|
|
|
489
|
+
// ---------------------------------------------------------------- consumed 标记
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* request.consumed / notice.consumed —— 按行 id 置 consumed=1。
|
|
493
|
+
* 表未注册(或 model 无 update)时保持"未接线"报错。
|
|
494
|
+
*/
|
|
495
|
+
async function markInboxConsumed(
|
|
496
|
+
d: Record<string, unknown>,
|
|
497
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
498
|
+
table: string,
|
|
499
|
+
): Promise<ExtendedRpcResult> {
|
|
500
|
+
const ids = numArrayField(d, '$row_ids', 'row_ids', 'rowIds', '$ids', 'ids');
|
|
501
|
+
// console UI 单条已读发 data:{id},归一为数组
|
|
502
|
+
if (ids.length === 0) {
|
|
503
|
+
const single = Number(d.id ?? d.$id);
|
|
504
|
+
if (Number.isFinite(single) && single > 0) ids.push(single);
|
|
505
|
+
}
|
|
506
|
+
if (ids.length === 0) return { error: '$row_ids required' };
|
|
507
|
+
const model = getInboxModel(ctx, table);
|
|
508
|
+
if (!model || typeof model.update !== 'function') return { error: CONSUMED_NOT_WIRED };
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
try {
|
|
511
|
+
for (const id of ids) {
|
|
512
|
+
await model.update({ consumed: 1, consumed_at: now }).where({ id });
|
|
513
|
+
}
|
|
514
|
+
return { data: { success: true, updated: ids.length } };
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return { error: errorMessage(error) };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
322
520
|
// ---------------------------------------------------------------- 请求审批
|
|
323
521
|
|
|
324
522
|
async function actOnRequest(
|
|
325
|
-
type: '
|
|
523
|
+
type: 'request.approve' | 'request.reject',
|
|
326
524
|
d: Record<string, unknown>,
|
|
327
525
|
ctx: ConsoleRpcExtendedCtx,
|
|
328
526
|
): Promise<ExtendedRpcResult> {
|
|
329
527
|
const adapter = strField(d, '$adapter', 'adapter');
|
|
330
528
|
const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
|
|
331
|
-
const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id');
|
|
529
|
+
const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
|
|
332
530
|
if (!adapter || !endpointId || !requestId) {
|
|
333
531
|
return { error: '$adapter, $endpoint, $id required' };
|
|
334
532
|
}
|
|
335
533
|
const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
|
|
336
534
|
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);
|
|
535
|
+
const management = resolved.management;
|
|
536
|
+
const approve = type === 'request.approve';
|
|
537
|
+
const method = approve ? management.approveRequest : management.rejectRequest;
|
|
343
538
|
if (!method) {
|
|
344
539
|
return {
|
|
345
|
-
error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${
|
|
540
|
+
error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
|
|
346
541
|
'且新 Runtime 尚未挂载 pending request 注册表',
|
|
347
542
|
};
|
|
348
543
|
}
|
|
@@ -350,7 +545,7 @@ async function actOnRequest(
|
|
|
350
545
|
const extra = approve
|
|
351
546
|
? strField(d, '$remark', 'remark')
|
|
352
547
|
: strField(d, '$reason', 'reason');
|
|
353
|
-
await method.call(
|
|
548
|
+
await method.call(management, requestId, extra || undefined);
|
|
354
549
|
return { data: { success: true } };
|
|
355
550
|
} catch (error) {
|
|
356
551
|
return { error: errorMessage(error) };
|
|
@@ -365,25 +560,10 @@ async function listFriends(
|
|
|
365
560
|
): Promise<ExtendedRpcResult> {
|
|
366
561
|
const resolved = requireEndpoint(d, ctx);
|
|
367
562
|
if ('error' in resolved) return resolved;
|
|
368
|
-
const {
|
|
563
|
+
const { management, adapter } = resolved;
|
|
369
564
|
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
|
-
}));
|
|
565
|
+
if (management.listFriends) {
|
|
566
|
+
const friends = [...await management.listFriends()];
|
|
387
567
|
return { data: { friends, count: friends.length } };
|
|
388
568
|
}
|
|
389
569
|
return { error: `当前适配器(${adapter})不支持好友列表` };
|
|
@@ -398,23 +578,10 @@ async function listGroups(
|
|
|
398
578
|
): Promise<ExtendedRpcResult> {
|
|
399
579
|
const resolved = requireEndpoint(d, ctx);
|
|
400
580
|
if ('error' in resolved) return resolved;
|
|
401
|
-
const {
|
|
581
|
+
const { management, adapter } = resolved;
|
|
402
582
|
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
|
-
}));
|
|
583
|
+
if (management.listGroups) {
|
|
584
|
+
const groups = [...await management.listGroups()];
|
|
418
585
|
return { data: { groups, count: groups.length } };
|
|
419
586
|
}
|
|
420
587
|
return { error: `当前适配器(${adapter})不支持群列表` };
|
|
@@ -429,43 +596,10 @@ async function listChannels(
|
|
|
429
596
|
): Promise<ExtendedRpcResult> {
|
|
430
597
|
const resolved = requireEndpoint(d, ctx);
|
|
431
598
|
if ('error' in resolved) return resolved;
|
|
432
|
-
const {
|
|
599
|
+
const { management, adapter } = resolved;
|
|
433
600
|
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
|
-
}
|
|
601
|
+
if (management.listChannels) {
|
|
602
|
+
const channels = [...await management.listChannels()];
|
|
469
603
|
return { data: { channels, count: channels.length } };
|
|
470
604
|
}
|
|
471
605
|
return { error: `当前适配器(${adapter})不支持频道列表` };
|
|
@@ -486,22 +620,14 @@ async function listGroupMembers(
|
|
|
486
620
|
}
|
|
487
621
|
const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
|
|
488
622
|
if ('error' in resolved) return resolved;
|
|
489
|
-
const {
|
|
623
|
+
const { management } = resolved;
|
|
490
624
|
try {
|
|
491
|
-
const method =
|
|
625
|
+
const method = management.listGroupMembers;
|
|
492
626
|
if (!method) {
|
|
493
627
|
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
494
628
|
}
|
|
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 } };
|
|
629
|
+
const members = [...await method(groupId)];
|
|
630
|
+
return { data: { members, count: members.length } };
|
|
505
631
|
} catch (error) {
|
|
506
632
|
return { error: errorMessage(error) };
|
|
507
633
|
}
|
|
@@ -510,7 +636,7 @@ async function listGroupMembers(
|
|
|
510
636
|
// ---------------------------------------------------------------- 群管/好友写操作
|
|
511
637
|
|
|
512
638
|
interface GroupWriteSpec {
|
|
513
|
-
|
|
639
|
+
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
514
640
|
buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
|
|
515
641
|
requireUser: boolean;
|
|
516
642
|
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
@@ -534,8 +660,8 @@ async function groupWriteOp(
|
|
|
534
660
|
}
|
|
535
661
|
const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
|
|
536
662
|
if ('error' in resolved) return resolved;
|
|
537
|
-
const {
|
|
538
|
-
const method =
|
|
663
|
+
const { management } = resolved;
|
|
664
|
+
const method = management[spec.method];
|
|
539
665
|
if (!method) {
|
|
540
666
|
return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
|
|
541
667
|
}
|
|
@@ -548,7 +674,7 @@ async function groupWriteOp(
|
|
|
548
674
|
: undefined,
|
|
549
675
|
enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
|
|
550
676
|
});
|
|
551
|
-
await method.call(
|
|
677
|
+
await (method as (...args: unknown[]) => Promise<void>).call(management, ...args);
|
|
552
678
|
return { data: { success: true } };
|
|
553
679
|
} catch (error) {
|
|
554
680
|
return { error: errorMessage(error) };
|
|
@@ -567,14 +693,13 @@ async function deleteFriend(
|
|
|
567
693
|
}
|
|
568
694
|
const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
|
|
569
695
|
if ('error' in resolved) return resolved;
|
|
570
|
-
const {
|
|
571
|
-
const method =
|
|
696
|
+
const { management } = resolved;
|
|
697
|
+
const method = management.deleteFriend;
|
|
572
698
|
if (!method) {
|
|
573
699
|
return { error: '当前适配器暂不支持删除好友' };
|
|
574
700
|
}
|
|
575
701
|
try {
|
|
576
|
-
|
|
577
|
-
await method.call(endpoint, Number.isFinite(numeric) && userId.trim() !== '' ? numeric : userId);
|
|
702
|
+
await method(userId);
|
|
578
703
|
return { data: { success: true } };
|
|
579
704
|
} catch (error) {
|
|
580
705
|
return { error: errorMessage(error) };
|
|
@@ -609,24 +734,34 @@ function optionalNum(d: Record<string, unknown>, ...keys: string[]): number | un
|
|
|
609
734
|
return undefined;
|
|
610
735
|
}
|
|
611
736
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
737
|
+
/** 数字数组字段($row_ids 等):取首个数组值,收敛为有限数字。 */
|
|
738
|
+
function numArrayField(d: Record<string, unknown>, ...keys: string[]): number[] {
|
|
739
|
+
for (const key of keys) {
|
|
740
|
+
const value = d[key];
|
|
741
|
+
if (!Array.isArray(value)) continue;
|
|
742
|
+
return value
|
|
743
|
+
.map((item) => Number(item))
|
|
744
|
+
.filter((item) => Number.isFinite(item));
|
|
619
745
|
}
|
|
620
|
-
return
|
|
746
|
+
return [];
|
|
621
747
|
}
|
|
622
748
|
|
|
623
|
-
type ResolveOk = {
|
|
749
|
+
type ResolveOk = {
|
|
750
|
+
management: EndpointManagementPort;
|
|
751
|
+
adapter: string;
|
|
752
|
+
endpointId: string;
|
|
753
|
+
};
|
|
624
754
|
|
|
625
755
|
function resolveLiveEndpoint(
|
|
626
756
|
ctx: ConsoleRpcExtendedCtx,
|
|
627
757
|
adapter: string,
|
|
628
758
|
endpointId: string,
|
|
629
759
|
): ResolveOk | { error: string } {
|
|
760
|
+
if (ctx.resolveEndpointManagement) {
|
|
761
|
+
const management = ctx.resolveEndpointManagement(adapter, endpointId);
|
|
762
|
+
if (!management) return { error: 'endpoint not found' };
|
|
763
|
+
return { management, adapter, endpointId };
|
|
764
|
+
}
|
|
630
765
|
if (!ctx.resolveEndpoint) {
|
|
631
766
|
return { error: 'Endpoint registry is not configured' };
|
|
632
767
|
}
|
|
@@ -634,7 +769,14 @@ function resolveLiveEndpoint(
|
|
|
634
769
|
if (!endpoint || typeof endpoint !== 'object') {
|
|
635
770
|
return { error: 'endpoint not found' };
|
|
636
771
|
}
|
|
637
|
-
|
|
772
|
+
const management = (endpoint as { management?: unknown }).management;
|
|
773
|
+
return {
|
|
774
|
+
management: management && typeof management === 'object'
|
|
775
|
+
? management as EndpointManagementPort
|
|
776
|
+
: {},
|
|
777
|
+
adapter,
|
|
778
|
+
endpointId,
|
|
779
|
+
};
|
|
638
780
|
}
|
|
639
781
|
|
|
640
782
|
function requireEndpoint(
|
|
@@ -647,18 +789,6 @@ function requireEndpoint(
|
|
|
647
789
|
return resolveLiveEndpoint(ctx, adapter, endpointId);
|
|
648
790
|
}
|
|
649
791
|
|
|
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
792
|
function normalizeParent(raw: unknown): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
|
|
663
793
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
664
794
|
const parent = raw as { type?: unknown; id?: unknown; name?: unknown };
|