@zhin.js/host-http 1.0.17 → 1.1.1
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 +6 -0
- package/lib/console-plugin-rpc.d.ts +3 -0
- package/lib/console-plugin-rpc.js +130 -0
- package/lib/console-rest-pages.d.ts +1 -1
- package/lib/console-rest-pages.js +11 -20
- package/lib/{console-rpc-extended.d.ts → console-rpc-extended/contracts.d.ts} +22 -21
- package/lib/console-rpc-extended/contracts.js +4 -0
- package/lib/console-rpc-extended/dispatcher.d.ts +2 -0
- package/lib/console-rpc-extended/dispatcher.js +92 -0
- package/lib/console-rpc-extended/endpoint-rpc.d.ts +24 -0
- package/lib/console-rpc-extended/endpoint-rpc.js +137 -0
- package/lib/console-rpc-extended/inbox-rpc.d.ts +23 -0
- package/lib/console-rpc-extended/inbox-rpc.js +292 -0
- package/lib/console-rpc-extended/index.d.ts +2 -0
- package/lib/console-rpc-extended/index.js +1 -0
- package/lib/console-rpc-extended/login-rpc.d.ts +4 -0
- package/lib/console-rpc-extended/login-rpc.js +46 -0
- package/lib/console-rpc-extended/rpc-values.d.ts +16 -0
- package/lib/console-rpc-extended/rpc-values.js +87 -0
- package/lib/console-rpc-extended/schedule-rpc.d.ts +6 -0
- package/lib/console-rpc-extended/schedule-rpc.js +109 -0
- package/lib/console-rpc-extended/workroom-rpc.d.ts +3 -0
- package/lib/console-rpc-extended/workroom-rpc.js +104 -0
- package/lib/console-rpc.d.ts +71 -15
- package/lib/console-rpc.js +116 -82
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/marketplace-registry.d.ts +12 -0
- package/lib/marketplace-registry.js +45 -0
- package/package.json +5 -5
- package/src/console-plugin-rpc.ts +142 -0
- package/src/console-rest-pages.ts +14 -26
- package/src/console-rpc-extended/contracts.ts +155 -0
- package/src/console-rpc-extended/dispatcher.ts +114 -0
- package/src/console-rpc-extended/endpoint-rpc.ts +174 -0
- package/src/console-rpc-extended/inbox-rpc.ts +342 -0
- package/src/console-rpc-extended/index.ts +8 -0
- package/src/console-rpc-extended/login-rpc.ts +50 -0
- package/src/console-rpc-extended/rpc-values.ts +104 -0
- package/src/console-rpc-extended/schedule-rpc.ts +117 -0
- package/src/console-rpc-extended/workroom-rpc.ts +116 -0
- package/src/console-rpc.ts +189 -97
- package/src/index.ts +7 -2
- package/src/marketplace-registry.ts +58 -0
- package/lib/console-rpc-extended.js +0 -916
- package/src/console-rpc-extended.ts +0 -1192
|
@@ -1,1192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extended Console RPC: cron/schedule + endpoint social/inbox surface.
|
|
3
|
-
*
|
|
4
|
-
* 挂在 `dispatchRuntimeConsoleRpc`(console-rpc.ts)之后的第二级 dispatch:
|
|
5
|
-
* 返回 `undefined` 表示不识别该 type,由调用方继续走默认分支;
|
|
6
|
-
* 识别后返回 `{ data }` 或 `{ error }`(requestId 由调用方补齐)。
|
|
7
|
-
*
|
|
8
|
-
* 数据源约定(只读真实数据源,不做内存假数据):
|
|
9
|
-
* - cron:`ctx.scheduleHost`(Plugin Runtime ScheduleHost,仅 register/list,
|
|
10
|
-
* 无持久化引擎,故 cron:add/remove/pause/resume 报"未接线")。
|
|
11
|
-
* - request.list:优先 `management.listRequests()`(live endpoint);否则回退
|
|
12
|
-
* unified_inbox_request。审批始终走 approveRequest/rejectRequest。
|
|
13
|
-
* - inbox(历史):`ctx.databaseHost.models`(unified_inbox_message/request/notice,
|
|
14
|
-
* 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
|
|
15
|
-
* 按行 id update consumed=1,表/model 缺失时报"未接线")。
|
|
16
|
-
* - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
|
|
17
|
-
* 消费 `management` 语义端口;平台 SDK 字段归一化由 Adapter 负责。
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** 新 Runtime 统一收件箱表名(SSOT:@zhin.js/plugin-runtime inbox.js)。 */
|
|
21
|
-
import {
|
|
22
|
-
assertDemoConsoleRpcAllowed,
|
|
23
|
-
normalizeConsoleRpcType,
|
|
24
|
-
} from '@zhin.js/console-protocol';
|
|
25
|
-
|
|
26
|
-
const TABLE_MESSAGE = 'unified_inbox_message';
|
|
27
|
-
const TABLE_REQUEST = 'unified_inbox_request';
|
|
28
|
-
const TABLE_NOTICE = 'unified_inbox_notice';
|
|
29
|
-
const MAX_INBOX_OFFSET = 10_000;
|
|
30
|
-
|
|
31
|
-
export interface ConsoleRpcExtendedCtx {
|
|
32
|
-
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
33
|
-
fullScope: boolean;
|
|
34
|
-
projectRoot: string;
|
|
35
|
-
/** Plugin Runtime ScheduleHost(basic/cli schedule-host-installer 提供)。 */
|
|
36
|
-
scheduleHost?: unknown;
|
|
37
|
-
/** 在固定 generation lease 内执行一次 Endpoint 管理操作。 */
|
|
38
|
-
withEndpointManagement?: <T>(
|
|
39
|
-
adapter: string,
|
|
40
|
-
endpointKey: string,
|
|
41
|
-
run: (management: EndpointManagementPort) => T | Promise<T>,
|
|
42
|
-
) => Promise<T | null>;
|
|
43
|
-
/** Plugin Runtime DatabaseHost 的 models 视图。 */
|
|
44
|
-
databaseHost?: { models: { get(name: string): unknown } };
|
|
45
|
-
/** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
|
|
46
|
-
resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
|
|
47
|
-
/** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
|
|
48
|
-
loginAssist?: {
|
|
49
|
-
listPending(): readonly {
|
|
50
|
-
id: string;
|
|
51
|
-
adapter: string;
|
|
52
|
-
endpointKey: string;
|
|
53
|
-
type: string;
|
|
54
|
-
payload: Record<string, unknown>;
|
|
55
|
-
createdAt: number;
|
|
56
|
-
expiresAt?: number;
|
|
57
|
-
}[];
|
|
58
|
-
submit(id: string, value: string | Record<string, unknown>): boolean;
|
|
59
|
-
cancel(id: string, reason?: string): boolean;
|
|
60
|
-
};
|
|
61
|
-
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
62
|
-
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
63
|
-
/** Authenticated HTTP principal; never accepted from the RPC body. */
|
|
64
|
-
authenticatedPrincipal?: Readonly<{ principalId: string }>;
|
|
65
|
-
workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ConsoleWorkroomProfileControlPort {
|
|
69
|
-
getPlanningStatus(projectId: string,
|
|
70
|
-
authenticatedPrincipal?: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
71
|
-
bootstrapPlanning(command: Readonly<{
|
|
72
|
-
operationId: string;
|
|
73
|
-
projectId: string;
|
|
74
|
-
expectedRegistryRevision: number;
|
|
75
|
-
includeTools?: readonly string[];
|
|
76
|
-
includeSkills?: readonly string[];
|
|
77
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
78
|
-
publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
|
|
79
|
-
authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
80
|
-
publishProfile(command: Readonly<{
|
|
81
|
-
operationId: string;
|
|
82
|
-
projectId: string;
|
|
83
|
-
expectedRegistryRevision: number;
|
|
84
|
-
overlay: unknown;
|
|
85
|
-
activate: boolean;
|
|
86
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
87
|
-
publishRollback(command: Readonly<{
|
|
88
|
-
operationId: string;
|
|
89
|
-
projectId: string;
|
|
90
|
-
expectedRegistryRevision: number;
|
|
91
|
-
restoredFromRevisionId: string;
|
|
92
|
-
overlay: unknown;
|
|
93
|
-
activate: boolean;
|
|
94
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
95
|
-
publishPlanningPolicy(command: Readonly<{
|
|
96
|
-
operationId: string;
|
|
97
|
-
projectId: string;
|
|
98
|
-
profileRevisionId: string;
|
|
99
|
-
revision: number;
|
|
100
|
-
expectedPreviousDigest?: string;
|
|
101
|
-
policy: unknown;
|
|
102
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface ConsoleWorkroomKnowledgeControlPort {
|
|
106
|
-
read(projectId: string): Promise<unknown>;
|
|
107
|
-
publish(command: Readonly<{
|
|
108
|
-
operationId: string; projectId: string; expectedRevision: number; entries: readonly unknown[];
|
|
109
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
110
|
-
rollback(command: Readonly<{
|
|
111
|
-
operationId: string; projectId: string; expectedRevision: number; restoreRevision: number;
|
|
112
|
-
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface ConsoleScheduleEngine {
|
|
116
|
-
addJob(job: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
117
|
-
removeJob(id: string): Promise<boolean>;
|
|
118
|
-
pauseJob(id: string): Promise<boolean>;
|
|
119
|
-
resumeJob(id: string): Promise<boolean>;
|
|
120
|
-
listJobs(): Promise<Record<string, unknown>[]>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export type ExtendedRpcResult = { data: unknown } | { error: string };
|
|
124
|
-
|
|
125
|
-
const CRON_NOT_WIRED =
|
|
126
|
-
'持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
|
|
127
|
-
'不支持 add/remove/pause/resume(需要 @zhin.js/agent 持久化调度引擎)';
|
|
128
|
-
|
|
129
|
-
const CONSUMED_NOT_WIRED =
|
|
130
|
-
'收件箱已读标记未接线:unified_inbox_request/notice 表未注册或 DatabaseHost 未启动';
|
|
131
|
-
|
|
132
|
-
interface ScheduleJobRow {
|
|
133
|
-
id: string;
|
|
134
|
-
cron: string;
|
|
135
|
-
description?: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface InboxSelection {
|
|
139
|
-
where(query: Record<string, unknown>): InboxSelection;
|
|
140
|
-
/** 可选:DB 侧排序下推(wrapModel 链具备该能力,轻量 fake 可缺失)。 */
|
|
141
|
-
orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
|
|
142
|
-
/** 可选:DB 侧 limit 下推。 */
|
|
143
|
-
limit?(count: number): InboxSelection;
|
|
144
|
-
/** 可选:DB 侧 offset 下推。 */
|
|
145
|
-
offset?(count: number): InboxSelection;
|
|
146
|
-
then<TResult1 = Record<string, unknown>[], TResult2 = never>(
|
|
147
|
-
onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
148
|
-
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
149
|
-
): Promise<TResult1 | TResult2>;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
interface InboxModel {
|
|
153
|
-
select(): InboxSelection;
|
|
154
|
-
update?(patch: Record<string, unknown>): {
|
|
155
|
-
where(query: Record<string, unknown>): Promise<unknown> | unknown;
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface EndpointManagementPort {
|
|
160
|
-
listFriends?(): Promise<readonly { user_id: number | string; nickname: string; remark: string }[]>;
|
|
161
|
-
listGroups?(): Promise<readonly { group_id: number | string; name: string }[]>;
|
|
162
|
-
listChannels?(): Promise<readonly {
|
|
163
|
-
id: string;
|
|
164
|
-
name?: string;
|
|
165
|
-
parent?: { type: string; id: string; name?: string };
|
|
166
|
-
}[]>;
|
|
167
|
-
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
168
|
-
listRequests?(): Promise<readonly {
|
|
169
|
-
platform_request_id: string;
|
|
170
|
-
type: string;
|
|
171
|
-
scene_type?: string | null;
|
|
172
|
-
scene_id: string;
|
|
173
|
-
sub_type?: string | null;
|
|
174
|
-
actor_id: string;
|
|
175
|
-
actor_name?: string | null;
|
|
176
|
-
comment?: string | null;
|
|
177
|
-
created_at: number;
|
|
178
|
-
}[]>;
|
|
179
|
-
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
180
|
-
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
181
|
-
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
182
|
-
muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
|
|
183
|
-
setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
|
|
184
|
-
deleteFriend?(userId: string): Promise<void>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function dispatchExtendedConsoleRpc(
|
|
188
|
-
type: string,
|
|
189
|
-
data: Record<string, unknown> | undefined,
|
|
190
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
191
|
-
): Promise<ExtendedRpcResult | undefined> {
|
|
192
|
-
const d = data ?? {};
|
|
193
|
-
type = normalizeConsoleRpcType(type);
|
|
194
|
-
|
|
195
|
-
if (!ctx.fullScope) {
|
|
196
|
-
const denied = assertDemoConsoleRpcAllowed(type);
|
|
197
|
-
if (denied) return { error: denied };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
switch (type) {
|
|
201
|
-
case 'schedule:list':
|
|
202
|
-
case 'cron:list':
|
|
203
|
-
return listSchedule(ctx);
|
|
204
|
-
|
|
205
|
-
case 'cron:add':
|
|
206
|
-
return addCron(d, ctx);
|
|
207
|
-
case 'cron:remove':
|
|
208
|
-
case 'cron:pause':
|
|
209
|
-
case 'cron:resume':
|
|
210
|
-
return mutateCron(type, d, ctx);
|
|
211
|
-
|
|
212
|
-
case 'request.list':
|
|
213
|
-
return listPendingRequests(d, ctx);
|
|
214
|
-
case 'inbox.requests':
|
|
215
|
-
return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
|
|
216
|
-
case 'inbox.notices':
|
|
217
|
-
return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
|
|
218
|
-
case 'inbox.messages':
|
|
219
|
-
return listInboxMessages(d, ctx);
|
|
220
|
-
case 'inbox.recent':
|
|
221
|
-
return listRecentInboxMessages(d, ctx);
|
|
222
|
-
|
|
223
|
-
case 'request.approve':
|
|
224
|
-
case 'request.reject':
|
|
225
|
-
return actOnRequest(type, d, ctx);
|
|
226
|
-
|
|
227
|
-
case 'login.list':
|
|
228
|
-
return listLoginPending(ctx);
|
|
229
|
-
case 'login.submit':
|
|
230
|
-
return submitLogin(d, ctx);
|
|
231
|
-
case 'login.cancel':
|
|
232
|
-
return cancelLogin(d, ctx);
|
|
233
|
-
|
|
234
|
-
case 'request.consumed':
|
|
235
|
-
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
236
|
-
case 'notice.consumed':
|
|
237
|
-
return markInboxConsumed(d, ctx, TABLE_NOTICE);
|
|
238
|
-
|
|
239
|
-
case 'endpoint.friends':
|
|
240
|
-
return listFriends(d, ctx);
|
|
241
|
-
case 'endpoint.groups':
|
|
242
|
-
return listGroups(d, ctx);
|
|
243
|
-
case 'endpoint.channels':
|
|
244
|
-
return listChannels(d, ctx);
|
|
245
|
-
case 'endpoint.group_members':
|
|
246
|
-
return listGroupMembers(d, ctx);
|
|
247
|
-
|
|
248
|
-
case 'endpoint.group_kick':
|
|
249
|
-
return groupWriteOp(d, ctx, {
|
|
250
|
-
method: 'kickGroupMember',
|
|
251
|
-
buildArgs: (gid, uid) => [gid, uid],
|
|
252
|
-
requireUser: true,
|
|
253
|
-
unsupported: '踢出群成员',
|
|
254
|
-
});
|
|
255
|
-
case 'endpoint.group_mute':
|
|
256
|
-
return groupWriteOp(d, ctx, {
|
|
257
|
-
method: 'muteGroupMember',
|
|
258
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
259
|
-
requireUser: true,
|
|
260
|
-
unsupported: '禁言群成员',
|
|
261
|
-
});
|
|
262
|
-
case 'endpoint.group_admin':
|
|
263
|
-
return groupWriteOp(d, ctx, {
|
|
264
|
-
method: 'setGroupAdmin',
|
|
265
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
266
|
-
requireUser: true,
|
|
267
|
-
unsupported: '设置群管理员',
|
|
268
|
-
});
|
|
269
|
-
case 'endpoint.delete_friend':
|
|
270
|
-
return deleteFriend(d, ctx);
|
|
271
|
-
|
|
272
|
-
case 'workroom.profile.status':
|
|
273
|
-
case 'workroom.profile.bootstrap':
|
|
274
|
-
case 'workroom.profile.pack.publish':
|
|
275
|
-
case 'workroom.profile.publish':
|
|
276
|
-
case 'workroom.profile.rollback':
|
|
277
|
-
case 'workroom.profile.policy.publish':
|
|
278
|
-
return mutateWorkroomProfile(type, d, ctx);
|
|
279
|
-
|
|
280
|
-
case 'workroom.knowledge.get':
|
|
281
|
-
case 'workroom.knowledge.publish':
|
|
282
|
-
case 'workroom.knowledge.rollback':
|
|
283
|
-
return mutateWorkroomKnowledge(type, d, ctx);
|
|
284
|
-
|
|
285
|
-
default:
|
|
286
|
-
return undefined;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function mutateWorkroomProfile(
|
|
291
|
-
type: 'workroom.profile.status' | 'workroom.profile.bootstrap'
|
|
292
|
-
| 'workroom.profile.pack.publish' | 'workroom.profile.publish'
|
|
293
|
-
| 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
|
|
294
|
-
data: Record<string, unknown>,
|
|
295
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
296
|
-
): Promise<ExtendedRpcResult> {
|
|
297
|
-
const control = ctx.workroomProfileControl;
|
|
298
|
-
if (!control) return { error: 'Workroom Profile authority control is unavailable' };
|
|
299
|
-
for (const forbidden of [
|
|
300
|
-
'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
|
|
301
|
-
]) {
|
|
302
|
-
if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
|
|
303
|
-
}
|
|
304
|
-
if (type === 'workroom.profile.status') {
|
|
305
|
-
try {
|
|
306
|
-
return { data: await control.getPlanningStatus(
|
|
307
|
-
requiredRpcText(data.projectId, 'projectId'),
|
|
308
|
-
ctx.authenticatedPrincipal,
|
|
309
|
-
) };
|
|
310
|
-
} catch (error) {
|
|
311
|
-
return { error: errorMessage(error) };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const principal = ctx.authenticatedPrincipal;
|
|
315
|
-
if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
|
|
316
|
-
try {
|
|
317
|
-
const operationId = requiredRpcText(data.operationId, 'operationId');
|
|
318
|
-
if (type === 'workroom.profile.bootstrap') {
|
|
319
|
-
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
320
|
-
const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
|
|
321
|
-
const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
|
|
322
|
-
return { data: await control.bootstrapPlanning(Object.freeze({
|
|
323
|
-
operationId,
|
|
324
|
-
projectId,
|
|
325
|
-
expectedRegistryRevision: requiredRpcInteger(
|
|
326
|
-
data.expectedRegistryRevision,
|
|
327
|
-
'expectedRegistryRevision',
|
|
328
|
-
-1,
|
|
329
|
-
),
|
|
330
|
-
...(includeTools ? { includeTools } : {}),
|
|
331
|
-
...(includeSkills ? { includeSkills } : {}),
|
|
332
|
-
}), principal) };
|
|
333
|
-
}
|
|
334
|
-
if (type === 'workroom.profile.pack.publish') {
|
|
335
|
-
return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
|
|
336
|
-
}
|
|
337
|
-
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
338
|
-
if (type === 'workroom.profile.policy.publish') {
|
|
339
|
-
const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
|
|
340
|
-
return { data: await control.publishPlanningPolicy(Object.freeze({
|
|
341
|
-
operationId,
|
|
342
|
-
projectId,
|
|
343
|
-
profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
|
|
344
|
-
revision: requiredRpcInteger(data.revision, 'revision'),
|
|
345
|
-
...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
|
|
346
|
-
policy: requiredRpcValue(data.policy, 'policy'),
|
|
347
|
-
}), principal) };
|
|
348
|
-
}
|
|
349
|
-
const common = {
|
|
350
|
-
operationId,
|
|
351
|
-
projectId,
|
|
352
|
-
expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
|
|
353
|
-
overlay: requiredRpcValue(data.overlay, 'overlay'),
|
|
354
|
-
activate: requiredRpcBoolean(data.activate, 'activate'),
|
|
355
|
-
};
|
|
356
|
-
return type === 'workroom.profile.rollback'
|
|
357
|
-
? { data: await control.publishRollback(Object.freeze({
|
|
358
|
-
...common,
|
|
359
|
-
restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
|
|
360
|
-
}), principal) }
|
|
361
|
-
: { data: await control.publishProfile(Object.freeze(common), principal) };
|
|
362
|
-
} catch (error) {
|
|
363
|
-
return { error: errorMessage(error) };
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function mutateWorkroomKnowledge(
|
|
368
|
-
type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
|
|
369
|
-
data: Record<string, unknown>,
|
|
370
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
371
|
-
): Promise<ExtendedRpcResult> {
|
|
372
|
-
const control = ctx.workroomKnowledgeControl;
|
|
373
|
-
if (!control) return { error: 'Workroom Knowledge control is unavailable' };
|
|
374
|
-
for (const forbidden of [
|
|
375
|
-
'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
|
|
376
|
-
'governance', 'approval', 'authority', 'body', 'content',
|
|
377
|
-
]) {
|
|
378
|
-
if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
|
|
379
|
-
}
|
|
380
|
-
try {
|
|
381
|
-
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
382
|
-
if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
|
|
383
|
-
const principal = ctx.authenticatedPrincipal;
|
|
384
|
-
if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
|
|
385
|
-
const common = {
|
|
386
|
-
operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
|
|
387
|
-
expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
|
|
388
|
-
};
|
|
389
|
-
if (type === 'workroom.knowledge.rollback') {
|
|
390
|
-
return { data: await control.rollback(Object.freeze({
|
|
391
|
-
...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
|
|
392
|
-
}), principal) };
|
|
393
|
-
}
|
|
394
|
-
if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
|
|
395
|
-
return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
|
|
396
|
-
} catch (error) {
|
|
397
|
-
return { error: errorMessage(error) };
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function requiredRpcValue(value: unknown, name: string): unknown {
|
|
402
|
-
if (value === undefined || value === null) throw new Error(`${name} is required`);
|
|
403
|
-
return structuredClone(value);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function requiredRpcText(value: unknown, name: string): string {
|
|
407
|
-
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
408
|
-
throw new Error(`${name} is invalid`);
|
|
409
|
-
}
|
|
410
|
-
return value;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function optionalRpcText(value: unknown, name: string): string | undefined {
|
|
414
|
-
return value === undefined ? undefined : requiredRpcText(value, name);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function optionalRpcTextArray(value: unknown, name: string): readonly string[] | undefined {
|
|
418
|
-
if (value === undefined) return undefined;
|
|
419
|
-
if (!Array.isArray(value)) throw new Error(`${name} is invalid`);
|
|
420
|
-
const result = value.map((item, index) => requiredRpcText(item, `${name}.${index}`));
|
|
421
|
-
if (new Set(result).size !== result.length) throw new Error(`${name} contains duplicates`);
|
|
422
|
-
return Object.freeze(result);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function requiredRpcInteger(value: unknown, name: string, minimum = 1): number {
|
|
426
|
-
if (!Number.isSafeInteger(value) || Number(value) < minimum) throw new Error(`${name} is invalid`);
|
|
427
|
-
return Number(value);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function requiredRpcBoolean(value: unknown, name: string): boolean {
|
|
431
|
-
if (typeof value !== 'boolean') throw new Error(`${name} is invalid`);
|
|
432
|
-
return value;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// ---------------------------------------------------------------- cron
|
|
436
|
-
|
|
437
|
-
function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
|
|
438
|
-
const host = ctx.scheduleHost as { list?: () => unknown } | undefined;
|
|
439
|
-
if (!host || typeof host.list !== 'function') {
|
|
440
|
-
return { error: '调度服务未配置(scheduleHost 未挂载)' };
|
|
441
|
-
}
|
|
442
|
-
try {
|
|
443
|
-
const raw = host.list();
|
|
444
|
-
const memory: ScheduleJobRow[] = (Array.isArray(raw) ? raw : [])
|
|
445
|
-
.filter((job): job is ScheduleJobRow =>
|
|
446
|
-
!!job && typeof job === 'object'
|
|
447
|
-
&& typeof (job as ScheduleJobRow).id === 'string'
|
|
448
|
-
&& typeof (job as ScheduleJobRow).cron === 'string')
|
|
449
|
-
.map((job) => ({
|
|
450
|
-
id: job.id,
|
|
451
|
-
cron: job.cron,
|
|
452
|
-
...(typeof job.description === 'string' ? { description: job.description } : {}),
|
|
453
|
-
// console cron 页渲染字段(由 schedule host 提供时透传)
|
|
454
|
-
expression: (job as { expression?: string }).expression ?? job.cron,
|
|
455
|
-
running: (job as { running?: boolean }).running ?? true,
|
|
456
|
-
...((job as { plugin?: string }).plugin ? { plugin: (job as { plugin?: string }).plugin } : {}),
|
|
457
|
-
...((job as { nextExecution?: number }).nextExecution
|
|
458
|
-
? { nextExecution: (job as { nextExecution?: number }).nextExecution } : {}),
|
|
459
|
-
}));
|
|
460
|
-
return listPersistentJobs(ctx).then((persistent) => ({ data: { memory, persistent } }));
|
|
461
|
-
} catch (error) {
|
|
462
|
-
return { error: errorMessage(error) };
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
type ConsoleScheduleListCtx = ConsoleRpcExtendedCtx;
|
|
467
|
-
|
|
468
|
-
/** 持久化任务(Agent Host ScheduleJobEngine)→ 前端 PersistentCron 形状。 */
|
|
469
|
-
async function listPersistentJobs(ctx: ConsoleRpcExtendedCtx): Promise<Record<string, unknown>[]> {
|
|
470
|
-
const engine = ctx.resolveScheduleEngine?.();
|
|
471
|
-
if (!engine) return [];
|
|
472
|
-
try {
|
|
473
|
-
const jobs = await engine.listJobs();
|
|
474
|
-
return jobs.map((job) => ({
|
|
475
|
-
id: job.id,
|
|
476
|
-
label: job.label ?? job.id,
|
|
477
|
-
cronExpression: (job.schedule as { cron?: string } | undefined)?.cron ?? '',
|
|
478
|
-
prompt: (job.action as { prompt?: string } | undefined)?.prompt ?? '',
|
|
479
|
-
enabled: job.enabled !== false,
|
|
480
|
-
source: job.source ?? 'manual',
|
|
481
|
-
createdAt: (job as { createdAt?: number }).createdAt,
|
|
482
|
-
nextExecution: (job.state as { nextRunAtMs?: number } | undefined)?.nextRunAtMs,
|
|
483
|
-
...((job.notify as { target?: unknown } | undefined)?.target
|
|
484
|
-
? { context: { target: (job.notify as { target?: unknown }).target } } : {}),
|
|
485
|
-
lastExecutedAt: (job.state as { lastExecutedAt?: number } | undefined)?.lastExecutedAt,
|
|
486
|
-
lastStatus: (job.state as { lastStatus?: string } | undefined)?.lastStatus,
|
|
487
|
-
}));
|
|
488
|
-
} catch {
|
|
489
|
-
return [];
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function addCron(
|
|
494
|
-
d: Record<string, unknown>,
|
|
495
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
496
|
-
): Promise<ExtendedRpcResult> {
|
|
497
|
-
const engine = ctx.resolveScheduleEngine?.();
|
|
498
|
-
if (!engine) return { error: CRON_NOT_WIRED };
|
|
499
|
-
const cronExpression = String(d.cronExpression ?? '').trim();
|
|
500
|
-
const prompt = String(d.prompt ?? '').trim();
|
|
501
|
-
if (!cronExpression) return { error: 'cronExpression is required' };
|
|
502
|
-
if (!prompt) return { error: 'prompt is required' };
|
|
503
|
-
const label = typeof d.label === 'string' && d.label.trim() ? d.label.trim() : undefined;
|
|
504
|
-
const context = (d.context ?? {}) as Record<string, unknown>;
|
|
505
|
-
const target = context.target ?? context.channel;
|
|
506
|
-
const job = {
|
|
507
|
-
id: `console-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
508
|
-
...(label ? { label } : {}),
|
|
509
|
-
enabled: true,
|
|
510
|
-
schedule: { kind: 'solar', cron: cronExpression },
|
|
511
|
-
action: { kind: 'agent', prompt },
|
|
512
|
-
notify: target
|
|
513
|
-
? { channel: 'im', target }
|
|
514
|
-
: { channel: 'silent' },
|
|
515
|
-
source: 'manual',
|
|
516
|
-
};
|
|
517
|
-
try {
|
|
518
|
-
const created = await engine.addJob(job);
|
|
519
|
-
return { data: { job: created, success: true } };
|
|
520
|
-
} catch (error) {
|
|
521
|
-
return { error: errorMessage(error) };
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async function mutateCron(
|
|
526
|
-
type: 'cron:remove' | 'cron:pause' | 'cron:resume',
|
|
527
|
-
d: Record<string, unknown>,
|
|
528
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
529
|
-
): Promise<ExtendedRpcResult> {
|
|
530
|
-
const engine = ctx.resolveScheduleEngine?.();
|
|
531
|
-
if (!engine) return { error: CRON_NOT_WIRED };
|
|
532
|
-
const id = String(d.id ?? '').trim();
|
|
533
|
-
if (!id) return { error: 'id is required' };
|
|
534
|
-
try {
|
|
535
|
-
const ok = type === 'cron:remove'
|
|
536
|
-
? await engine.removeJob(id)
|
|
537
|
-
: type === 'cron:pause'
|
|
538
|
-
? await engine.pauseJob(id)
|
|
539
|
-
: await engine.resumeJob(id);
|
|
540
|
-
return ok ? { data: { success: true } } : { error: `任务不存在: ${id}` };
|
|
541
|
-
} catch (error) {
|
|
542
|
-
return { error: errorMessage(error) };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ---------------------------------------------------------------- inbox
|
|
547
|
-
|
|
548
|
-
function getInboxModel(ctx: ConsoleRpcExtendedCtx, table: string): InboxModel | undefined {
|
|
549
|
-
const model = ctx.databaseHost?.models?.get(table) as InboxModel | undefined;
|
|
550
|
-
if (!model || typeof model.select !== 'function') return undefined;
|
|
551
|
-
return model;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async function readInboxRows(
|
|
555
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
556
|
-
table: string,
|
|
557
|
-
where: Record<string, unknown>,
|
|
558
|
-
options?: {
|
|
559
|
-
/** DB 侧排序下推(模型不支持时由调用方内存排序兜底)。 */
|
|
560
|
-
orderBy?: { field: string; direction?: 'ASC' | 'DESC' };
|
|
561
|
-
/** DB 侧 limit 下推(模型不支持时由调用方内存分页兜底)。 */
|
|
562
|
-
limit?: number;
|
|
563
|
-
/** DB 侧 offset 下推(模型不支持时由调用方内存分页兜底)。 */
|
|
564
|
-
offset?: number;
|
|
565
|
-
},
|
|
566
|
-
): Promise<{ rows: Record<string, unknown>[]; enabled: boolean; offsetApplied: boolean }> {
|
|
567
|
-
const model = getInboxModel(ctx, table);
|
|
568
|
-
if (!model) return { rows: [], enabled: false, offsetApplied: false };
|
|
569
|
-
try {
|
|
570
|
-
let selection: InboxSelection = model.select().where(where);
|
|
571
|
-
let orderApplied = options?.orderBy == null;
|
|
572
|
-
if (options?.orderBy && typeof selection.orderBy === 'function') {
|
|
573
|
-
selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
|
|
574
|
-
orderApplied = true;
|
|
575
|
-
}
|
|
576
|
-
const offset = options?.offset ?? 0;
|
|
577
|
-
const offsetSelection = orderApplied ? selection.offset : undefined;
|
|
578
|
-
const canApplyOffset = typeof offsetSelection === 'function';
|
|
579
|
-
const offsetApplied = offset === 0 || canApplyOffset;
|
|
580
|
-
if (offset > 0 && canApplyOffset) {
|
|
581
|
-
selection = offsetSelection.call(selection, offset);
|
|
582
|
-
}
|
|
583
|
-
if (options?.limit != null && orderApplied && typeof selection.limit === 'function') {
|
|
584
|
-
selection = selection.limit(options.limit + (offsetApplied ? 0 : offset));
|
|
585
|
-
}
|
|
586
|
-
const rows = await selection;
|
|
587
|
-
return { rows: Array.isArray(rows) ? rows : [], enabled: true, offsetApplied };
|
|
588
|
-
} catch {
|
|
589
|
-
// 表未创建 / 方言未启动等场景降级为空,不向上抛。
|
|
590
|
-
return { rows: [], enabled: false, offsetApplied: false };
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function listInbox(
|
|
595
|
-
d: Record<string, unknown>,
|
|
596
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
597
|
-
table: string,
|
|
598
|
-
key: 'requests' | 'notices',
|
|
599
|
-
mapRow: (row: Record<string, unknown>) => Record<string, unknown>,
|
|
600
|
-
): Promise<ExtendedRpcResult> {
|
|
601
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
602
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
603
|
-
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
604
|
-
const limit = boundedIntegerField(d, 30, 1, 100, '$limit', 'limit');
|
|
605
|
-
const offset = boundedIntegerField(
|
|
606
|
-
d, 0, 0, MAX_INBOX_OFFSET, '$offset', 'offset',
|
|
607
|
-
);
|
|
608
|
-
const unreadOnly = table === TABLE_NOTICE
|
|
609
|
-
&& boolField(d, false, '$unread_only', 'unreadOnly', 'unread_only');
|
|
610
|
-
const { rows, enabled, offsetApplied } = await readInboxRows(ctx, table, {
|
|
611
|
-
adapter,
|
|
612
|
-
endpoint_id: endpointKey,
|
|
613
|
-
...(unreadOnly ? { consumed: 0 } : {}),
|
|
614
|
-
}, {
|
|
615
|
-
// sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
|
|
616
|
-
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
617
|
-
limit,
|
|
618
|
-
offset,
|
|
619
|
-
});
|
|
620
|
-
const sorted = rows
|
|
621
|
-
.slice()
|
|
622
|
-
.sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
|
|
623
|
-
.slice(offsetApplied ? 0 : offset, (offsetApplied ? 0 : offset) + limit)
|
|
624
|
-
.map(mapRow);
|
|
625
|
-
return { data: { [key]: sorted, inboxEnabled: enabled } };
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
|
|
629
|
-
async function listPendingRequests(
|
|
630
|
-
d: Record<string, unknown>,
|
|
631
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
632
|
-
): Promise<ExtendedRpcResult> {
|
|
633
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
634
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
635
|
-
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
636
|
-
|
|
637
|
-
if (ctx.withEndpointManagement) {
|
|
638
|
-
try {
|
|
639
|
-
const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
|
|
640
|
-
if (!management.listRequests) return undefined;
|
|
641
|
-
const pending = await management.listRequests();
|
|
642
|
-
const requests = pending.map((row) => mapRequestRow({
|
|
643
|
-
platform_request_id: row.platform_request_id,
|
|
644
|
-
type: row.type,
|
|
645
|
-
scene_type: row.scene_type ?? null,
|
|
646
|
-
scene_id: row.scene_id,
|
|
647
|
-
sub_type: row.sub_type ?? null,
|
|
648
|
-
actor_id: row.actor_id,
|
|
649
|
-
actor_name: row.actor_name ?? null,
|
|
650
|
-
comment: row.comment ?? null,
|
|
651
|
-
created_at: row.created_at,
|
|
652
|
-
resolved: 0,
|
|
653
|
-
}));
|
|
654
|
-
return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
|
|
655
|
-
});
|
|
656
|
-
if (live) return live;
|
|
657
|
-
} catch (error) {
|
|
658
|
-
return { error: errorMessage(error) };
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
|
|
663
|
-
adapter,
|
|
664
|
-
endpoint_id: endpointKey,
|
|
665
|
-
}, {
|
|
666
|
-
orderBy: { field: 'created_at', direction: 'ASC' },
|
|
667
|
-
});
|
|
668
|
-
const requests = rows
|
|
669
|
-
.filter((row) => Number(row.resolved ?? 0) === 0)
|
|
670
|
-
.sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
|
|
671
|
-
.map(mapRequestRow);
|
|
672
|
-
return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function listInboxMessages(
|
|
676
|
-
d: Record<string, unknown>,
|
|
677
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
678
|
-
): Promise<ExtendedRpcResult> {
|
|
679
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
680
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
681
|
-
const channelId = strField(d, '$channel_id', 'channelId', 'channel_id');
|
|
682
|
-
const channelType = strField(d, '$channel_type', 'channelType', 'channel_type');
|
|
683
|
-
if (!adapter || !endpointKey || !channelId || !channelType) {
|
|
684
|
-
return { error: '$adapter, $endpoint, $channel_id, $channel_type required' };
|
|
685
|
-
}
|
|
686
|
-
const limit = boundedIntegerField(d, 50, 1, 100, '$limit', 'limit');
|
|
687
|
-
const beforeTs = optionalNum(d, '$before_ts', 'beforeTs', 'before_ts');
|
|
688
|
-
const beforeId = optionalNum(d, '$before_id', 'beforeId', 'before_id');
|
|
689
|
-
const parent = normalizeParent(d.$parent ?? d.parent);
|
|
690
|
-
|
|
691
|
-
const where: Record<string, unknown> = {
|
|
692
|
-
adapter,
|
|
693
|
-
endpoint_id: endpointKey,
|
|
694
|
-
channel_id: channelId,
|
|
695
|
-
channel_type: channelType,
|
|
696
|
-
};
|
|
697
|
-
if (parent) {
|
|
698
|
-
where.channel_parent_type = parent.type;
|
|
699
|
-
where.channel_parent_id = parent.id;
|
|
700
|
-
}
|
|
701
|
-
const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where, {
|
|
702
|
-
// before_ts/before_id 过滤保留在内存(涉及双字段比较),排序下推 DB 侧
|
|
703
|
-
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
704
|
-
});
|
|
705
|
-
const messages = rows
|
|
706
|
-
.filter((row) => (beforeTs == null || Number(row.created_at ?? 0) < beforeTs)
|
|
707
|
-
&& (beforeId == null || Number(row.id ?? 0) < beforeId))
|
|
708
|
-
.sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
|
|
709
|
-
.slice(0, limit)
|
|
710
|
-
.map((row) => ({
|
|
711
|
-
id: row.id,
|
|
712
|
-
platform_message_id: row.platform_message_id,
|
|
713
|
-
sender_id: row.sender_id,
|
|
714
|
-
sender_name: row.sender_name,
|
|
715
|
-
content: row.content,
|
|
716
|
-
raw: row.raw,
|
|
717
|
-
created_at: row.created_at,
|
|
718
|
-
channel: channelFromStoredRow(row),
|
|
719
|
-
parent: parentFromStoredRow(row),
|
|
720
|
-
}));
|
|
721
|
-
return { data: { messages, inboxEnabled: enabled } };
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Cross-endpoint recent inbox used by paired Mobile Console clients.
|
|
726
|
-
* Unlike endpoint-detail `inbox.messages`, every row carries its complete
|
|
727
|
-
* routable address so selecting a conversation can safely call
|
|
728
|
-
* `endpoint.send_message` through the Device Protocol write allowlist.
|
|
729
|
-
*/
|
|
730
|
-
async function listRecentInboxMessages(
|
|
731
|
-
d: Record<string, unknown>,
|
|
732
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
733
|
-
): Promise<ExtendedRpcResult> {
|
|
734
|
-
const limit = boundedIntegerField(d, 50, 1, 100, '$limit', 'limit');
|
|
735
|
-
const [messagePage, requestPage] = await Promise.all([
|
|
736
|
-
readInboxRows(ctx, TABLE_MESSAGE, {}, {
|
|
737
|
-
orderBy: {field: 'created_at', direction: 'DESC'}, limit,
|
|
738
|
-
}),
|
|
739
|
-
readInboxRows(ctx, TABLE_REQUEST, {resolved: 0}, {
|
|
740
|
-
orderBy: {field: 'created_at', direction: 'DESC'}, limit,
|
|
741
|
-
}),
|
|
742
|
-
]);
|
|
743
|
-
const messages = messagePage.rows
|
|
744
|
-
.slice()
|
|
745
|
-
.sort((left, right) => Number(right.created_at ?? 0) - Number(left.created_at ?? 0))
|
|
746
|
-
.slice(0, limit)
|
|
747
|
-
.map(row => ({
|
|
748
|
-
id: row.id,
|
|
749
|
-
adapter: row.adapter,
|
|
750
|
-
endpoint_id: row.endpoint_id,
|
|
751
|
-
platform_message_id: row.platform_message_id,
|
|
752
|
-
sender_id: row.sender_id,
|
|
753
|
-
sender_name: row.sender_name,
|
|
754
|
-
content: row.content,
|
|
755
|
-
raw: row.raw,
|
|
756
|
-
created_at: row.created_at,
|
|
757
|
-
channel: channelFromStoredRow(row),
|
|
758
|
-
parent: parentFromStoredRow(row),
|
|
759
|
-
}));
|
|
760
|
-
const requests = requestPage.rows
|
|
761
|
-
.filter(row => Number(row.resolved ?? 0) === 0)
|
|
762
|
-
.slice()
|
|
763
|
-
.sort((left, right) => Number(right.created_at ?? 0) - Number(left.created_at ?? 0))
|
|
764
|
-
.slice(0, limit)
|
|
765
|
-
.map(row => ({...mapRequestRow(row), adapter: row.adapter, endpoint_id: row.endpoint_id}));
|
|
766
|
-
return {data: {messages, requests, inboxEnabled: messagePage.enabled || requestPage.enabled}};
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function mapRequestRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
770
|
-
const actorId = row.actor_id;
|
|
771
|
-
const actorName = row.actor_name ?? undefined;
|
|
772
|
-
const sceneId = row.scene_id;
|
|
773
|
-
const sceneType = row.scene_type ?? undefined;
|
|
774
|
-
return {
|
|
775
|
-
id: row.id,
|
|
776
|
-
platform_request_id: row.platform_request_id,
|
|
777
|
-
// console endpoint-detail 期望的 camelCase/扁平别名(SSE 推送路径同款形状)
|
|
778
|
-
platformRequestId: row.platform_request_id,
|
|
779
|
-
type: row.type,
|
|
780
|
-
scene_type: sceneType,
|
|
781
|
-
scene_id: sceneId,
|
|
782
|
-
channel_id: sceneId,
|
|
783
|
-
channel_type: sceneType,
|
|
784
|
-
channel: { id: sceneId, type: sceneType },
|
|
785
|
-
sub_type: row.sub_type ?? undefined,
|
|
786
|
-
actor: { id: actorId, name: actorName },
|
|
787
|
-
sender: { id: actorId, name: actorName },
|
|
788
|
-
sender_id: actorId,
|
|
789
|
-
sender_name: actorName,
|
|
790
|
-
comment: row.comment ?? undefined,
|
|
791
|
-
created_at: row.created_at,
|
|
792
|
-
timestamp: row.created_at,
|
|
793
|
-
resolved: row.resolved,
|
|
794
|
-
resolved_at: row.resolved_at ?? undefined,
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
799
|
-
const actorId = row.actor_id ?? undefined;
|
|
800
|
-
const actorName = row.actor_name ?? undefined;
|
|
801
|
-
const sceneId = String(row.scene_id ?? '');
|
|
802
|
-
const sceneType = row.scene_type == null ? undefined : String(row.scene_type);
|
|
803
|
-
return {
|
|
804
|
-
id: row.id,
|
|
805
|
-
platform_notice_id: row.platform_notice_id,
|
|
806
|
-
type: row.type,
|
|
807
|
-
noticeType: row.type,
|
|
808
|
-
scene_type: sceneType,
|
|
809
|
-
scene_id: sceneId,
|
|
810
|
-
channel_id: sceneId,
|
|
811
|
-
channel_type: sceneType,
|
|
812
|
-
channel: { id: sceneId, type: sceneType ?? '' },
|
|
813
|
-
sub_type: row.sub_type ?? undefined,
|
|
814
|
-
actor_id: actorId,
|
|
815
|
-
actor_name: actorName,
|
|
816
|
-
// console endpoint-detail 期望的操作人字段别名
|
|
817
|
-
operator_id: actorId,
|
|
818
|
-
operator_name: actorName,
|
|
819
|
-
target_id: row.target_id ?? undefined,
|
|
820
|
-
target_name: row.target_name ?? undefined,
|
|
821
|
-
payload: row.payload,
|
|
822
|
-
created_at: row.created_at,
|
|
823
|
-
timestamp: row.created_at,
|
|
824
|
-
consumed: row.consumed,
|
|
825
|
-
consumed_at: row.consumed_at ?? undefined,
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// ---------------------------------------------------------------- consumed 标记
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* request.consumed / notice.consumed —— 按行 id 置 consumed=1。
|
|
833
|
-
* 表未注册(或 model 无 update)时保持"未接线"报错。
|
|
834
|
-
*/
|
|
835
|
-
async function markInboxConsumed(
|
|
836
|
-
d: Record<string, unknown>,
|
|
837
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
838
|
-
table: string,
|
|
839
|
-
): Promise<ExtendedRpcResult> {
|
|
840
|
-
const ids = numArrayField(d, '$row_ids', 'row_ids', 'rowIds', '$ids', 'ids');
|
|
841
|
-
// console UI 单条已读发 data:{id},归一为数组
|
|
842
|
-
if (ids.length === 0) {
|
|
843
|
-
const single = Number(d.id ?? d.$id);
|
|
844
|
-
if (Number.isFinite(single) && single > 0) ids.push(single);
|
|
845
|
-
}
|
|
846
|
-
if (ids.length === 0) return { error: '$row_ids required' };
|
|
847
|
-
const model = getInboxModel(ctx, table);
|
|
848
|
-
if (!model || typeof model.update !== 'function') return { error: CONSUMED_NOT_WIRED };
|
|
849
|
-
const now = Date.now();
|
|
850
|
-
try {
|
|
851
|
-
for (const id of ids) {
|
|
852
|
-
await model.update({ consumed: 1, consumed_at: now }).where({ id });
|
|
853
|
-
}
|
|
854
|
-
return { data: { success: true, updated: ids.length } };
|
|
855
|
-
} catch (error) {
|
|
856
|
-
return { error: errorMessage(error) };
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// ---------------------------------------------------------------- 请求审批
|
|
861
|
-
|
|
862
|
-
async function actOnRequest(
|
|
863
|
-
type: 'request.approve' | 'request.reject',
|
|
864
|
-
d: Record<string, unknown>,
|
|
865
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
866
|
-
): Promise<ExtendedRpcResult> {
|
|
867
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
868
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
869
|
-
const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
|
|
870
|
-
if (!adapter || !endpointKey || !requestId) {
|
|
871
|
-
return { error: '$adapter, $endpoint, $id required' };
|
|
872
|
-
}
|
|
873
|
-
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
874
|
-
const approve = type === 'request.approve';
|
|
875
|
-
const method = approve ? management.approveRequest : management.rejectRequest;
|
|
876
|
-
if (!method) {
|
|
877
|
-
return {
|
|
878
|
-
error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
|
|
879
|
-
'且新 Runtime 尚未挂载 pending request 注册表',
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const extra = approve
|
|
883
|
-
? strField(d, '$remark', 'remark')
|
|
884
|
-
: strField(d, '$reason', 'reason');
|
|
885
|
-
await method.call(management, requestId, extra || undefined);
|
|
886
|
-
const model = getInboxModel(ctx, TABLE_REQUEST);
|
|
887
|
-
if (model && typeof model.update === 'function') {
|
|
888
|
-
await model.update({resolved: 1, resolved_at: Date.now()}).where({
|
|
889
|
-
adapter, endpoint_id: endpointKey, platform_request_id: requestId,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
return { data: { success: true } };
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
|
|
897
|
-
const assist = ctx.loginAssist;
|
|
898
|
-
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
899
|
-
const tasks = assist.listPending().map((task) => ({
|
|
900
|
-
id: task.id,
|
|
901
|
-
adapter: task.adapter,
|
|
902
|
-
endpointKey: task.endpointKey,
|
|
903
|
-
type: task.type,
|
|
904
|
-
payload: task.payload,
|
|
905
|
-
createdAt: task.createdAt,
|
|
906
|
-
expiresAt: task.expiresAt,
|
|
907
|
-
}));
|
|
908
|
-
return { data: { tasks, count: tasks.length } };
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function submitLogin(
|
|
912
|
-
d: Record<string, unknown>,
|
|
913
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
914
|
-
): ExtendedRpcResult {
|
|
915
|
-
const assist = ctx.loginAssist;
|
|
916
|
-
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
917
|
-
const id = strField(d, '$id', 'id', 'taskId');
|
|
918
|
-
if (!id) return { error: '$id required' };
|
|
919
|
-
const raw = d.$value ?? d.value ?? d.ticket ?? d.code ?? '';
|
|
920
|
-
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
921
|
-
? raw as string | Record<string, unknown>
|
|
922
|
-
: String(raw ?? '');
|
|
923
|
-
const ok = assist.submit(id, value);
|
|
924
|
-
if (!ok) return { error: `login task not found: ${id}` };
|
|
925
|
-
return { data: { success: true } };
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function cancelLogin(
|
|
929
|
-
d: Record<string, unknown>,
|
|
930
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
931
|
-
): ExtendedRpcResult {
|
|
932
|
-
const assist = ctx.loginAssist;
|
|
933
|
-
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
934
|
-
const id = strField(d, '$id', 'id', 'taskId');
|
|
935
|
-
if (!id) return { error: '$id required' };
|
|
936
|
-
const reason = strField(d, '$reason', 'reason') || 'cancelled';
|
|
937
|
-
const ok = assist.cancel(id, reason);
|
|
938
|
-
if (!ok) return { error: `login task not found: ${id}` };
|
|
939
|
-
return { data: { success: true } };
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// ---------------------------------------------------------------- 社交读取
|
|
943
|
-
|
|
944
|
-
async function listFriends(
|
|
945
|
-
d: Record<string, unknown>,
|
|
946
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
947
|
-
): Promise<ExtendedRpcResult> {
|
|
948
|
-
const endpoint = requireEndpointAddress(d);
|
|
949
|
-
if ('error' in endpoint) return endpoint;
|
|
950
|
-
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
951
|
-
if (management.listFriends) {
|
|
952
|
-
const friends = [...await management.listFriends()];
|
|
953
|
-
return { data: { friends, count: friends.length } };
|
|
954
|
-
}
|
|
955
|
-
return { error: `当前适配器(${endpoint.adapter})不支持好友列表` };
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
async function listGroups(
|
|
960
|
-
d: Record<string, unknown>,
|
|
961
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
962
|
-
): Promise<ExtendedRpcResult> {
|
|
963
|
-
const endpoint = requireEndpointAddress(d);
|
|
964
|
-
if ('error' in endpoint) return endpoint;
|
|
965
|
-
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
966
|
-
if (management.listGroups) {
|
|
967
|
-
const groups = [...await management.listGroups()];
|
|
968
|
-
return { data: { groups, count: groups.length } };
|
|
969
|
-
}
|
|
970
|
-
return { error: `当前适配器(${endpoint.adapter})不支持群列表` };
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
async function listChannels(
|
|
975
|
-
d: Record<string, unknown>,
|
|
976
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
977
|
-
): Promise<ExtendedRpcResult> {
|
|
978
|
-
const endpoint = requireEndpointAddress(d);
|
|
979
|
-
if ('error' in endpoint) return endpoint;
|
|
980
|
-
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
981
|
-
if (management.listChannels) {
|
|
982
|
-
const channels = [...await management.listChannels()];
|
|
983
|
-
return { data: { channels, count: channels.length } };
|
|
984
|
-
}
|
|
985
|
-
return { error: `当前适配器(${endpoint.adapter})不支持频道列表` };
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
async function listGroupMembers(
|
|
990
|
-
d: Record<string, unknown>,
|
|
991
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
992
|
-
): Promise<ExtendedRpcResult> {
|
|
993
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
994
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
995
|
-
const groupId = strField(d, '$group_id', 'groupId', 'group_id');
|
|
996
|
-
if (!adapter || !endpointKey || !groupId) {
|
|
997
|
-
return { error: '$adapter, $endpoint, $group_id required' };
|
|
998
|
-
}
|
|
999
|
-
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
1000
|
-
const method = management.listGroupMembers;
|
|
1001
|
-
if (!method) {
|
|
1002
|
-
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
1003
|
-
}
|
|
1004
|
-
const members = [...await method(groupId)];
|
|
1005
|
-
return { data: { members, count: members.length } };
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// ---------------------------------------------------------------- 群管/好友写操作
|
|
1010
|
-
|
|
1011
|
-
interface GroupWriteSpec {
|
|
1012
|
-
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
1013
|
-
buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
|
|
1014
|
-
requireUser: boolean;
|
|
1015
|
-
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
1016
|
-
unsupported: string;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
async function groupWriteOp(
|
|
1020
|
-
d: Record<string, unknown>,
|
|
1021
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
1022
|
-
spec: GroupWriteSpec,
|
|
1023
|
-
): Promise<ExtendedRpcResult> {
|
|
1024
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
1025
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
1026
|
-
const groupId = strField(d, '$group_id', 'groupId', 'group_id');
|
|
1027
|
-
const userId = strField(d, '$user_id', 'userId', 'user_id');
|
|
1028
|
-
if (!adapter || !endpointKey || !groupId) {
|
|
1029
|
-
return { error: '$adapter, $endpoint, $group_id required' };
|
|
1030
|
-
}
|
|
1031
|
-
if (spec.requireUser && !userId) {
|
|
1032
|
-
return { error: '$user_id required' };
|
|
1033
|
-
}
|
|
1034
|
-
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
1035
|
-
const method = management[spec.method];
|
|
1036
|
-
if (!method) {
|
|
1037
|
-
return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
|
|
1038
|
-
}
|
|
1039
|
-
const durationRaw = d.$duration ?? d.duration;
|
|
1040
|
-
const enableRaw = d.$enable ?? d.enable;
|
|
1041
|
-
const args = spec.buildArgs(groupId, userId, {
|
|
1042
|
-
duration: typeof durationRaw === 'number' && Number.isFinite(durationRaw)
|
|
1043
|
-
? durationRaw
|
|
1044
|
-
: undefined,
|
|
1045
|
-
enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
|
|
1046
|
-
});
|
|
1047
|
-
await (method as (...args: unknown[]) => Promise<void>).call(management, ...args);
|
|
1048
|
-
return { data: { success: true } };
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
async function deleteFriend(
|
|
1053
|
-
d: Record<string, unknown>,
|
|
1054
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
1055
|
-
): Promise<ExtendedRpcResult> {
|
|
1056
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
1057
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
1058
|
-
const userId = strField(d, '$user_id', 'userId', 'user_id');
|
|
1059
|
-
if (!adapter || !endpointKey || !userId) {
|
|
1060
|
-
return { error: '$adapter, $endpoint, $user_id required' };
|
|
1061
|
-
}
|
|
1062
|
-
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
1063
|
-
const method = management.deleteFriend;
|
|
1064
|
-
if (!method) {
|
|
1065
|
-
return { error: '当前适配器暂不支持删除好友' };
|
|
1066
|
-
}
|
|
1067
|
-
await method(userId);
|
|
1068
|
-
return { data: { success: true } };
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// ---------------------------------------------------------------- helpers
|
|
1073
|
-
|
|
1074
|
-
function strField(d: Record<string, unknown>, ...keys: string[]): string {
|
|
1075
|
-
for (const key of keys) {
|
|
1076
|
-
const value = d[key];
|
|
1077
|
-
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
1078
|
-
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
1079
|
-
}
|
|
1080
|
-
return '';
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function numField(d: Record<string, unknown>, fallback: number, ...keys: string[]): number {
|
|
1084
|
-
for (const key of keys) {
|
|
1085
|
-
const parsed = Number(d[key]);
|
|
1086
|
-
if (d[key] != null && Number.isFinite(parsed)) return parsed;
|
|
1087
|
-
}
|
|
1088
|
-
return fallback;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function boundedIntegerField(
|
|
1092
|
-
d: Record<string, unknown>,
|
|
1093
|
-
fallback: number,
|
|
1094
|
-
minimum: number,
|
|
1095
|
-
maximum: number,
|
|
1096
|
-
...keys: string[]
|
|
1097
|
-
): number {
|
|
1098
|
-
const value = numField(d, fallback, ...keys);
|
|
1099
|
-
if (!Number.isFinite(value)) return fallback;
|
|
1100
|
-
return Math.min(maximum, Math.max(minimum, Math.trunc(value)));
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function boolField(d: Record<string, unknown>, fallback: boolean, ...keys: string[]): boolean {
|
|
1104
|
-
for (const key of keys) {
|
|
1105
|
-
const value = d[key];
|
|
1106
|
-
if (typeof value === 'boolean') return value;
|
|
1107
|
-
}
|
|
1108
|
-
return fallback;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function optionalNum(d: Record<string, unknown>, ...keys: string[]): number | undefined {
|
|
1112
|
-
for (const key of keys) {
|
|
1113
|
-
if (d[key] == null) continue;
|
|
1114
|
-
const parsed = Number(d[key]);
|
|
1115
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
1116
|
-
}
|
|
1117
|
-
return undefined;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
/** 数字数组字段($row_ids 等):取首个数组值,收敛为有限数字。 */
|
|
1121
|
-
function numArrayField(d: Record<string, unknown>, ...keys: string[]): number[] {
|
|
1122
|
-
for (const key of keys) {
|
|
1123
|
-
const value = d[key];
|
|
1124
|
-
if (!Array.isArray(value)) continue;
|
|
1125
|
-
return value
|
|
1126
|
-
.map((item) => Number(item))
|
|
1127
|
-
.filter((item) => Number.isFinite(item));
|
|
1128
|
-
}
|
|
1129
|
-
return [];
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
async function withLiveEndpoint(
|
|
1133
|
-
ctx: ConsoleRpcExtendedCtx,
|
|
1134
|
-
adapter: string,
|
|
1135
|
-
endpointKey: string,
|
|
1136
|
-
run: (management: EndpointManagementPort) => ExtendedRpcResult | Promise<ExtendedRpcResult>,
|
|
1137
|
-
): Promise<ExtendedRpcResult> {
|
|
1138
|
-
if (!ctx.withEndpointManagement) {
|
|
1139
|
-
return { error: 'Endpoint registry is not configured' };
|
|
1140
|
-
}
|
|
1141
|
-
try {
|
|
1142
|
-
return await ctx.withEndpointManagement(adapter, endpointKey, run)
|
|
1143
|
-
?? { error: 'endpoint not found' };
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
return { error: errorMessage(error) };
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function requireEndpointAddress(
|
|
1150
|
-
d: Record<string, unknown>,
|
|
1151
|
-
): { adapter: string; endpointKey: string } | { error: string } {
|
|
1152
|
-
const adapter = strField(d, '$adapter', 'adapter');
|
|
1153
|
-
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
1154
|
-
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
1155
|
-
return { adapter, endpointKey };
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function normalizeParent(raw: unknown): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
|
|
1159
|
-
if (!raw || typeof raw !== 'object') return undefined;
|
|
1160
|
-
const parent = raw as { type?: unknown; id?: unknown; name?: unknown };
|
|
1161
|
-
const id = typeof parent.id === 'string' && parent.id.trim() ? parent.id.trim() : undefined;
|
|
1162
|
-
if (!id) return undefined;
|
|
1163
|
-
let type: 'group' | 'guild' | undefined;
|
|
1164
|
-
if (parent.type === 'group' || parent.type === 'guild') type = parent.type;
|
|
1165
|
-
else if (parent.type === 'channel') type = 'guild';
|
|
1166
|
-
if (!type) return undefined;
|
|
1167
|
-
const name = typeof parent.name === 'string' && parent.name.trim() ? parent.name.trim() : undefined;
|
|
1168
|
-
return name ? { type, id, name } : { type, id };
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function channelFromStoredRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
1172
|
-
const parent = parentFromStoredRow(row);
|
|
1173
|
-
return {
|
|
1174
|
-
id: String(row.channel_id ?? ''),
|
|
1175
|
-
type: String(row.channel_type ?? ''),
|
|
1176
|
-
...(row.channel_name != null ? { name: String(row.channel_name) } : {}),
|
|
1177
|
-
...(parent ? { parent } : {}),
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function parentFromStoredRow(
|
|
1182
|
-
row: Record<string, unknown>,
|
|
1183
|
-
): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
|
|
1184
|
-
return normalizeParent({
|
|
1185
|
-
type: row.channel_parent_type,
|
|
1186
|
-
id: row.channel_parent_id,
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function errorMessage(error: unknown): string {
|
|
1191
|
-
return error instanceof Error ? error.message : String(error);
|
|
1192
|
-
}
|