@zhin.js/host-http 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/console-events.d.ts +18 -4
- package/lib/console-events.js +121 -9
- package/lib/console-rest-pages.d.ts +15 -5
- package/lib/console-rest-pages.js +162 -16
- package/lib/console-rpc-extended.d.ts +92 -2
- package/lib/console-rpc-extended.js +243 -16
- package/lib/console-rpc.d.ts +10 -0
- package/lib/console-rpc.js +48 -0
- package/lib/http-host.d.ts +2 -2
- package/lib/http-host.js +4 -3
- package/lib/index.d.ts +2 -2
- package/lib/token-registry.d.ts +7 -0
- package/lib/token-registry.js +29 -8
- package/package.json +3 -3
- package/src/console-events.ts +170 -12
- package/src/console-rest-pages.ts +181 -20
- package/src/console-rpc-extended.ts +332 -18
- package/src/console-rpc.ts +55 -0
- package/src/http-host.ts +7 -4
- package/src/index.ts +3 -0
- package/src/token-registry.ts +36 -8
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* 数据源约定(只读真实数据源,不做内存假数据):
|
|
9
9
|
* - cron:`ctx.scheduleHost`(Plugin Runtime ScheduleHost,仅 register/list,
|
|
10
10
|
* 无持久化引擎,故 cron:add/remove/pause/resume 报"未接线")。
|
|
11
|
-
* -
|
|
11
|
+
* - request.list:优先 `management.listRequests()`(live endpoint);否则回退
|
|
12
|
+
* unified_inbox_request。审批始终走 approveRequest/rejectRequest。
|
|
13
|
+
* - inbox(历史):`ctx.databaseHost.models`(unified_inbox_message/request/notice,
|
|
12
14
|
* 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
|
|
13
15
|
* 按行 id update consumed=1,表/model 缺失时报"未接线")。
|
|
14
16
|
* - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
const TABLE_MESSAGE = 'unified_inbox_message';
|
|
25
27
|
const TABLE_REQUEST = 'unified_inbox_request';
|
|
26
28
|
const TABLE_NOTICE = 'unified_inbox_notice';
|
|
29
|
+
const MAX_INBOX_OFFSET = 10_000;
|
|
27
30
|
|
|
28
31
|
export interface ConsoleRpcExtendedCtx {
|
|
29
32
|
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
@@ -39,8 +42,65 @@ export interface ConsoleRpcExtendedCtx {
|
|
|
39
42
|
) => Promise<T | null>;
|
|
40
43
|
/** Plugin Runtime DatabaseHost 的 models 视图。 */
|
|
41
44
|
databaseHost?: { models: { get(name: string): unknown } };
|
|
42
|
-
/**
|
|
45
|
+
/** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
|
|
43
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
|
+
publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
|
|
70
|
+
authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
71
|
+
publishProfile(command: Readonly<{
|
|
72
|
+
operationId: string;
|
|
73
|
+
projectId: string;
|
|
74
|
+
expectedRegistryRevision: number;
|
|
75
|
+
overlay: unknown;
|
|
76
|
+
activate: boolean;
|
|
77
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
78
|
+
publishRollback(command: Readonly<{
|
|
79
|
+
operationId: string;
|
|
80
|
+
projectId: string;
|
|
81
|
+
expectedRegistryRevision: number;
|
|
82
|
+
restoredFromRevisionId: string;
|
|
83
|
+
overlay: unknown;
|
|
84
|
+
activate: boolean;
|
|
85
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
86
|
+
publishPlanningPolicy(command: Readonly<{
|
|
87
|
+
operationId: string;
|
|
88
|
+
projectId: string;
|
|
89
|
+
profileRevisionId: string;
|
|
90
|
+
revision: number;
|
|
91
|
+
expectedPreviousDigest?: string;
|
|
92
|
+
policy: unknown;
|
|
93
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ConsoleWorkroomKnowledgeControlPort {
|
|
97
|
+
read(projectId: string): Promise<unknown>;
|
|
98
|
+
publish(command: Readonly<{
|
|
99
|
+
operationId: string; projectId: string; expectedRevision: number; entries: readonly unknown[];
|
|
100
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
101
|
+
rollback(command: Readonly<{
|
|
102
|
+
operationId: string; projectId: string; expectedRevision: number; restoreRevision: number;
|
|
103
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
44
104
|
}
|
|
45
105
|
|
|
46
106
|
export interface ConsoleScheduleEngine {
|
|
@@ -72,6 +132,8 @@ interface InboxSelection {
|
|
|
72
132
|
orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
|
|
73
133
|
/** 可选:DB 侧 limit 下推。 */
|
|
74
134
|
limit?(count: number): InboxSelection;
|
|
135
|
+
/** 可选:DB 侧 offset 下推。 */
|
|
136
|
+
offset?(count: number): InboxSelection;
|
|
75
137
|
then<TResult1 = Record<string, unknown>[], TResult2 = never>(
|
|
76
138
|
onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
77
139
|
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
@@ -94,6 +156,17 @@ interface EndpointManagementPort {
|
|
|
94
156
|
parent?: { type: string; id: string; name?: string };
|
|
95
157
|
}[]>;
|
|
96
158
|
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
159
|
+
listRequests?(): Promise<readonly {
|
|
160
|
+
platform_request_id: string;
|
|
161
|
+
type: string;
|
|
162
|
+
scene_type?: string | null;
|
|
163
|
+
scene_id: string;
|
|
164
|
+
sub_type?: string | null;
|
|
165
|
+
actor_id: string;
|
|
166
|
+
actor_name?: string | null;
|
|
167
|
+
comment?: string | null;
|
|
168
|
+
created_at: number;
|
|
169
|
+
}[]>;
|
|
97
170
|
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
98
171
|
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
99
172
|
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
@@ -140,6 +213,13 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
140
213
|
case 'request.reject':
|
|
141
214
|
return actOnRequest(type, d, ctx);
|
|
142
215
|
|
|
216
|
+
case 'login.list':
|
|
217
|
+
return listLoginPending(ctx);
|
|
218
|
+
case 'login.submit':
|
|
219
|
+
return submitLogin(d, ctx);
|
|
220
|
+
case 'login.cancel':
|
|
221
|
+
return cancelLogin(d, ctx);
|
|
222
|
+
|
|
143
223
|
case 'request.consumed':
|
|
144
224
|
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
145
225
|
case 'notice.consumed':
|
|
@@ -178,11 +258,132 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
178
258
|
case 'endpoint.delete_friend':
|
|
179
259
|
return deleteFriend(d, ctx);
|
|
180
260
|
|
|
261
|
+
case 'workroom.profile.pack.publish':
|
|
262
|
+
case 'workroom.profile.publish':
|
|
263
|
+
case 'workroom.profile.rollback':
|
|
264
|
+
case 'workroom.profile.policy.publish':
|
|
265
|
+
return mutateWorkroomProfile(type, d, ctx);
|
|
266
|
+
|
|
267
|
+
case 'workroom.knowledge.get':
|
|
268
|
+
case 'workroom.knowledge.publish':
|
|
269
|
+
case 'workroom.knowledge.rollback':
|
|
270
|
+
return mutateWorkroomKnowledge(type, d, ctx);
|
|
271
|
+
|
|
181
272
|
default:
|
|
182
273
|
return undefined;
|
|
183
274
|
}
|
|
184
275
|
}
|
|
185
276
|
|
|
277
|
+
async function mutateWorkroomProfile(
|
|
278
|
+
type: 'workroom.profile.pack.publish' | 'workroom.profile.publish'
|
|
279
|
+
| 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
|
|
280
|
+
data: Record<string, unknown>,
|
|
281
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
282
|
+
): Promise<ExtendedRpcResult> {
|
|
283
|
+
const control = ctx.workroomProfileControl;
|
|
284
|
+
if (!control) return { error: 'Workroom Profile authority control is unavailable' };
|
|
285
|
+
for (const forbidden of [
|
|
286
|
+
'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
|
|
287
|
+
]) {
|
|
288
|
+
if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
|
|
289
|
+
}
|
|
290
|
+
const principal = ctx.authenticatedPrincipal;
|
|
291
|
+
if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
|
|
292
|
+
try {
|
|
293
|
+
const operationId = requiredRpcText(data.operationId, 'operationId');
|
|
294
|
+
if (type === 'workroom.profile.pack.publish') {
|
|
295
|
+
return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
|
|
296
|
+
}
|
|
297
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
298
|
+
if (type === 'workroom.profile.policy.publish') {
|
|
299
|
+
const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
|
|
300
|
+
return { data: await control.publishPlanningPolicy(Object.freeze({
|
|
301
|
+
operationId,
|
|
302
|
+
projectId,
|
|
303
|
+
profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
|
|
304
|
+
revision: requiredRpcInteger(data.revision, 'revision'),
|
|
305
|
+
...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
|
|
306
|
+
policy: requiredRpcValue(data.policy, 'policy'),
|
|
307
|
+
}), principal) };
|
|
308
|
+
}
|
|
309
|
+
const common = {
|
|
310
|
+
operationId,
|
|
311
|
+
projectId,
|
|
312
|
+
expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
|
|
313
|
+
overlay: requiredRpcValue(data.overlay, 'overlay'),
|
|
314
|
+
activate: requiredRpcBoolean(data.activate, 'activate'),
|
|
315
|
+
};
|
|
316
|
+
return type === 'workroom.profile.rollback'
|
|
317
|
+
? { data: await control.publishRollback(Object.freeze({
|
|
318
|
+
...common,
|
|
319
|
+
restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
|
|
320
|
+
}), principal) }
|
|
321
|
+
: { data: await control.publishProfile(Object.freeze(common), principal) };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return { error: errorMessage(error) };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function mutateWorkroomKnowledge(
|
|
328
|
+
type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
|
|
329
|
+
data: Record<string, unknown>,
|
|
330
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
331
|
+
): Promise<ExtendedRpcResult> {
|
|
332
|
+
const control = ctx.workroomKnowledgeControl;
|
|
333
|
+
if (!control) return { error: 'Workroom Knowledge control is unavailable' };
|
|
334
|
+
for (const forbidden of [
|
|
335
|
+
'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
|
|
336
|
+
'governance', 'approval', 'authority', 'body', 'content',
|
|
337
|
+
]) {
|
|
338
|
+
if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
342
|
+
if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
|
|
343
|
+
const principal = ctx.authenticatedPrincipal;
|
|
344
|
+
if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
|
|
345
|
+
const common = {
|
|
346
|
+
operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
|
|
347
|
+
expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
|
|
348
|
+
};
|
|
349
|
+
if (type === 'workroom.knowledge.rollback') {
|
|
350
|
+
return { data: await control.rollback(Object.freeze({
|
|
351
|
+
...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
|
|
352
|
+
}), principal) };
|
|
353
|
+
}
|
|
354
|
+
if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
|
|
355
|
+
return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { error: errorMessage(error) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function requiredRpcValue(value: unknown, name: string): unknown {
|
|
362
|
+
if (value === undefined || value === null) throw new Error(`${name} is required`);
|
|
363
|
+
return structuredClone(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function requiredRpcText(value: unknown, name: string): string {
|
|
367
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
368
|
+
throw new Error(`${name} is invalid`);
|
|
369
|
+
}
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function optionalRpcText(value: unknown, name: string): string | undefined {
|
|
374
|
+
return value === undefined ? undefined : requiredRpcText(value, name);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function requiredRpcInteger(value: unknown, name: string, minimum = 1): number {
|
|
378
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) throw new Error(`${name} is invalid`);
|
|
379
|
+
return Number(value);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function requiredRpcBoolean(value: unknown, name: string): boolean {
|
|
383
|
+
if (typeof value !== 'boolean') throw new Error(`${name} is invalid`);
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
|
|
186
387
|
// ---------------------------------------------------------------- cron
|
|
187
388
|
|
|
188
389
|
function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
|
|
@@ -311,23 +512,34 @@ async function readInboxRows(
|
|
|
311
512
|
orderBy?: { field: string; direction?: 'ASC' | 'DESC' };
|
|
312
513
|
/** DB 侧 limit 下推(模型不支持时由调用方内存分页兜底)。 */
|
|
313
514
|
limit?: number;
|
|
515
|
+
/** DB 侧 offset 下推(模型不支持时由调用方内存分页兜底)。 */
|
|
516
|
+
offset?: number;
|
|
314
517
|
},
|
|
315
|
-
): Promise<{ rows: Record<string, unknown>[]; enabled: boolean }> {
|
|
518
|
+
): Promise<{ rows: Record<string, unknown>[]; enabled: boolean; offsetApplied: boolean }> {
|
|
316
519
|
const model = getInboxModel(ctx, table);
|
|
317
|
-
if (!model) return { rows: [], enabled: false };
|
|
520
|
+
if (!model) return { rows: [], enabled: false, offsetApplied: false };
|
|
318
521
|
try {
|
|
319
522
|
let selection: InboxSelection = model.select().where(where);
|
|
523
|
+
let orderApplied = options?.orderBy == null;
|
|
320
524
|
if (options?.orderBy && typeof selection.orderBy === 'function') {
|
|
321
525
|
selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
|
|
526
|
+
orderApplied = true;
|
|
322
527
|
}
|
|
323
|
-
|
|
324
|
-
|
|
528
|
+
const offset = options?.offset ?? 0;
|
|
529
|
+
const offsetSelection = orderApplied ? selection.offset : undefined;
|
|
530
|
+
const canApplyOffset = typeof offsetSelection === 'function';
|
|
531
|
+
const offsetApplied = offset === 0 || canApplyOffset;
|
|
532
|
+
if (offset > 0 && canApplyOffset) {
|
|
533
|
+
selection = offsetSelection.call(selection, offset);
|
|
534
|
+
}
|
|
535
|
+
if (options?.limit != null && orderApplied && typeof selection.limit === 'function') {
|
|
536
|
+
selection = selection.limit(options.limit + (offsetApplied ? 0 : offset));
|
|
325
537
|
}
|
|
326
538
|
const rows = await selection;
|
|
327
|
-
return { rows: Array.isArray(rows) ? rows : [], enabled: true };
|
|
539
|
+
return { rows: Array.isArray(rows) ? rows : [], enabled: true, offsetApplied };
|
|
328
540
|
} catch {
|
|
329
541
|
// 表未创建 / 方言未启动等场景降级为空,不向上抛。
|
|
330
|
-
return { rows: [], enabled: false };
|
|
542
|
+
return { rows: [], enabled: false, offsetApplied: false };
|
|
331
543
|
}
|
|
332
544
|
}
|
|
333
545
|
|
|
@@ -341,25 +553,31 @@ async function listInbox(
|
|
|
341
553
|
const adapter = strField(d, '$adapter', 'adapter');
|
|
342
554
|
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
343
555
|
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
344
|
-
const limit =
|
|
345
|
-
const offset =
|
|
346
|
-
|
|
556
|
+
const limit = boundedIntegerField(d, 30, 1, 100, '$limit', 'limit');
|
|
557
|
+
const offset = boundedIntegerField(
|
|
558
|
+
d, 0, 0, MAX_INBOX_OFFSET, '$offset', 'offset',
|
|
559
|
+
);
|
|
560
|
+
const unreadOnly = table === TABLE_NOTICE
|
|
561
|
+
&& boolField(d, false, '$unread_only', 'unreadOnly', 'unread_only');
|
|
562
|
+
const { rows, enabled, offsetApplied } = await readInboxRows(ctx, table, {
|
|
347
563
|
adapter,
|
|
348
564
|
endpoint_id: endpointKey,
|
|
565
|
+
...(unreadOnly ? { consumed: 0 } : {}),
|
|
349
566
|
}, {
|
|
350
567
|
// sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
|
|
351
568
|
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
352
|
-
limit
|
|
569
|
+
limit,
|
|
570
|
+
offset,
|
|
353
571
|
});
|
|
354
572
|
const sorted = rows
|
|
355
573
|
.slice()
|
|
356
574
|
.sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
|
|
357
|
-
.slice(offset, offset + limit)
|
|
575
|
+
.slice(offsetApplied ? 0 : offset, (offsetApplied ? 0 : offset) + limit)
|
|
358
576
|
.map(mapRow);
|
|
359
577
|
return { data: { [key]: sorted, inboxEnabled: enabled } };
|
|
360
578
|
}
|
|
361
579
|
|
|
362
|
-
/** request.list ——
|
|
580
|
+
/** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
|
|
363
581
|
async function listPendingRequests(
|
|
364
582
|
d: Record<string, unknown>,
|
|
365
583
|
ctx: ConsoleRpcExtendedCtx,
|
|
@@ -367,6 +585,32 @@ async function listPendingRequests(
|
|
|
367
585
|
const adapter = strField(d, '$adapter', 'adapter');
|
|
368
586
|
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
369
587
|
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
588
|
+
|
|
589
|
+
if (ctx.withEndpointManagement) {
|
|
590
|
+
try {
|
|
591
|
+
const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
|
|
592
|
+
if (!management.listRequests) return undefined;
|
|
593
|
+
const pending = await management.listRequests();
|
|
594
|
+
const requests = pending.map((row) => mapRequestRow({
|
|
595
|
+
platform_request_id: row.platform_request_id,
|
|
596
|
+
type: row.type,
|
|
597
|
+
scene_type: row.scene_type ?? null,
|
|
598
|
+
scene_id: row.scene_id,
|
|
599
|
+
sub_type: row.sub_type ?? null,
|
|
600
|
+
actor_id: row.actor_id,
|
|
601
|
+
actor_name: row.actor_name ?? null,
|
|
602
|
+
comment: row.comment ?? null,
|
|
603
|
+
created_at: row.created_at,
|
|
604
|
+
resolved: 0,
|
|
605
|
+
}));
|
|
606
|
+
return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
|
|
607
|
+
});
|
|
608
|
+
if (live) return live;
|
|
609
|
+
} catch (error) {
|
|
610
|
+
return { error: errorMessage(error) };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
370
614
|
const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
|
|
371
615
|
adapter,
|
|
372
616
|
endpoint_id: endpointKey,
|
|
@@ -377,7 +621,7 @@ async function listPendingRequests(
|
|
|
377
621
|
.filter((row) => Number(row.resolved ?? 0) === 0)
|
|
378
622
|
.sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
|
|
379
623
|
.map(mapRequestRow);
|
|
380
|
-
return { data: { requests, inboxEnabled: enabled } };
|
|
624
|
+
return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
|
|
381
625
|
}
|
|
382
626
|
|
|
383
627
|
async function listInboxMessages(
|
|
@@ -391,7 +635,7 @@ async function listInboxMessages(
|
|
|
391
635
|
if (!adapter || !endpointKey || !channelId || !channelType) {
|
|
392
636
|
return { error: '$adapter, $endpoint, $channel_id, $channel_type required' };
|
|
393
637
|
}
|
|
394
|
-
const limit =
|
|
638
|
+
const limit = boundedIntegerField(d, 50, 1, 100, '$limit', 'limit');
|
|
395
639
|
const beforeTs = optionalNum(d, '$before_ts', 'beforeTs', 'before_ts');
|
|
396
640
|
const beforeId = optionalNum(d, '$before_id', 'beforeId', 'before_id');
|
|
397
641
|
const parent = normalizeParent(d.$parent ?? d.parent);
|
|
@@ -461,16 +705,18 @@ function mapRequestRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
|
461
705
|
function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
462
706
|
const actorId = row.actor_id ?? undefined;
|
|
463
707
|
const actorName = row.actor_name ?? undefined;
|
|
464
|
-
const sceneId = row.scene_id;
|
|
465
|
-
const sceneType = row.scene_type
|
|
708
|
+
const sceneId = String(row.scene_id ?? '');
|
|
709
|
+
const sceneType = row.scene_type == null ? undefined : String(row.scene_type);
|
|
466
710
|
return {
|
|
467
711
|
id: row.id,
|
|
468
712
|
platform_notice_id: row.platform_notice_id,
|
|
469
713
|
type: row.type,
|
|
714
|
+
noticeType: row.type,
|
|
470
715
|
scene_type: sceneType,
|
|
471
716
|
scene_id: sceneId,
|
|
472
717
|
channel_id: sceneId,
|
|
473
718
|
channel_type: sceneType,
|
|
719
|
+
channel: { id: sceneId, type: sceneType ?? '' },
|
|
474
720
|
sub_type: row.sub_type ?? undefined,
|
|
475
721
|
actor_id: actorId,
|
|
476
722
|
actor_name: actorName,
|
|
@@ -482,6 +728,8 @@ function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
|
482
728
|
payload: row.payload,
|
|
483
729
|
created_at: row.created_at,
|
|
484
730
|
timestamp: row.created_at,
|
|
731
|
+
consumed: row.consumed,
|
|
732
|
+
consumed_at: row.consumed_at ?? undefined,
|
|
485
733
|
};
|
|
486
734
|
}
|
|
487
735
|
|
|
@@ -546,6 +794,52 @@ async function actOnRequest(
|
|
|
546
794
|
});
|
|
547
795
|
}
|
|
548
796
|
|
|
797
|
+
function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
|
|
798
|
+
const assist = ctx.loginAssist;
|
|
799
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
800
|
+
const tasks = assist.listPending().map((task) => ({
|
|
801
|
+
id: task.id,
|
|
802
|
+
adapter: task.adapter,
|
|
803
|
+
endpointKey: task.endpointKey,
|
|
804
|
+
type: task.type,
|
|
805
|
+
payload: task.payload,
|
|
806
|
+
createdAt: task.createdAt,
|
|
807
|
+
expiresAt: task.expiresAt,
|
|
808
|
+
}));
|
|
809
|
+
return { data: { tasks, count: tasks.length } };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function submitLogin(
|
|
813
|
+
d: Record<string, unknown>,
|
|
814
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
815
|
+
): ExtendedRpcResult {
|
|
816
|
+
const assist = ctx.loginAssist;
|
|
817
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
818
|
+
const id = strField(d, '$id', 'id', 'taskId');
|
|
819
|
+
if (!id) return { error: '$id required' };
|
|
820
|
+
const raw = d.$value ?? d.value ?? d.ticket ?? d.code ?? '';
|
|
821
|
+
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
822
|
+
? raw as string | Record<string, unknown>
|
|
823
|
+
: String(raw ?? '');
|
|
824
|
+
const ok = assist.submit(id, value);
|
|
825
|
+
if (!ok) return { error: `login task not found: ${id}` };
|
|
826
|
+
return { data: { success: true } };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function cancelLogin(
|
|
830
|
+
d: Record<string, unknown>,
|
|
831
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
832
|
+
): ExtendedRpcResult {
|
|
833
|
+
const assist = ctx.loginAssist;
|
|
834
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
835
|
+
const id = strField(d, '$id', 'id', 'taskId');
|
|
836
|
+
if (!id) return { error: '$id required' };
|
|
837
|
+
const reason = strField(d, '$reason', 'reason') || 'cancelled';
|
|
838
|
+
const ok = assist.cancel(id, reason);
|
|
839
|
+
if (!ok) return { error: `login task not found: ${id}` };
|
|
840
|
+
return { data: { success: true } };
|
|
841
|
+
}
|
|
842
|
+
|
|
549
843
|
// ---------------------------------------------------------------- 社交读取
|
|
550
844
|
|
|
551
845
|
async function listFriends(
|
|
@@ -695,6 +989,26 @@ function numField(d: Record<string, unknown>, fallback: number, ...keys: string[
|
|
|
695
989
|
return fallback;
|
|
696
990
|
}
|
|
697
991
|
|
|
992
|
+
function boundedIntegerField(
|
|
993
|
+
d: Record<string, unknown>,
|
|
994
|
+
fallback: number,
|
|
995
|
+
minimum: number,
|
|
996
|
+
maximum: number,
|
|
997
|
+
...keys: string[]
|
|
998
|
+
): number {
|
|
999
|
+
const value = numField(d, fallback, ...keys);
|
|
1000
|
+
if (!Number.isFinite(value)) return fallback;
|
|
1001
|
+
return Math.min(maximum, Math.max(minimum, Math.trunc(value)));
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function boolField(d: Record<string, unknown>, fallback: boolean, ...keys: string[]): boolean {
|
|
1005
|
+
for (const key of keys) {
|
|
1006
|
+
const value = d[key];
|
|
1007
|
+
if (typeof value === 'boolean') return value;
|
|
1008
|
+
}
|
|
1009
|
+
return fallback;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
698
1012
|
function optionalNum(d: Record<string, unknown>, ...keys: string[]): number | undefined {
|
|
699
1013
|
for (const key of keys) {
|
|
700
1014
|
if (d[key] == null) continue;
|
package/src/console-rpc.ts
CHANGED
|
@@ -81,6 +81,16 @@ export type RuntimeConsoleRpcContext = {
|
|
|
81
81
|
* Returns whether a process restart is required (always true without ConfigFeature).
|
|
82
82
|
*/
|
|
83
83
|
setConfigKey?(pluginName: string, data: unknown): Promise<{ restartRequired: boolean }>;
|
|
84
|
+
/** Atomic, revision-checked runtime Workroom Catalog read/write. */
|
|
85
|
+
readWorkroomCatalog?(): Promise<Readonly<{
|
|
86
|
+
agents: Readonly<Record<string, unknown>>;
|
|
87
|
+
workrooms: Readonly<Record<string, unknown>>;
|
|
88
|
+
revision: string;
|
|
89
|
+
}>>;
|
|
90
|
+
setWorkroomCatalog?(
|
|
91
|
+
workrooms: unknown,
|
|
92
|
+
expectedRevision: string,
|
|
93
|
+
): Promise<Readonly<{ revision: string; restartRequired: boolean }>>;
|
|
84
94
|
/** Project file manager (allowlisted paths under project root). */
|
|
85
95
|
listProjectFiles?(): Promise<readonly unknown[]>;
|
|
86
96
|
readProjectFile?(filePath: string): Promise<{ content: string; size: number }>;
|
|
@@ -240,6 +250,39 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
240
250
|
}
|
|
241
251
|
return payloads;
|
|
242
252
|
}
|
|
253
|
+
case 'workrooms:get': {
|
|
254
|
+
try {
|
|
255
|
+
if (!ctx.readWorkroomCatalog) {
|
|
256
|
+
emit({ requestId, error: 'Workroom Catalog read is not configured' });
|
|
257
|
+
return payloads;
|
|
258
|
+
}
|
|
259
|
+
emit({ requestId, data: await ctx.readWorkroomCatalog() });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
emit({ requestId, error: error instanceof Error ? error.message : String(error) });
|
|
262
|
+
}
|
|
263
|
+
return payloads;
|
|
264
|
+
}
|
|
265
|
+
case 'workrooms:set': {
|
|
266
|
+
try {
|
|
267
|
+
if (!ctx.setWorkroomCatalog) {
|
|
268
|
+
emit({ requestId, error: 'Workroom Catalog write is not configured' });
|
|
269
|
+
return payloads;
|
|
270
|
+
}
|
|
271
|
+
const expectedRevision = typeof message.expectedRevision === 'string'
|
|
272
|
+
? message.expectedRevision
|
|
273
|
+
: '';
|
|
274
|
+
if (!/^[a-f0-9]{64}$/u.test(expectedRevision)) {
|
|
275
|
+
emit({ requestId, error: 'expectedRevision is required' });
|
|
276
|
+
return payloads;
|
|
277
|
+
}
|
|
278
|
+
const result = await ctx.setWorkroomCatalog(message.data, expectedRevision);
|
|
279
|
+
ctx.publishEvent?.('workrooms:updated', { revision: result.revision });
|
|
280
|
+
emit({ requestId, data: { success: true, ...result } });
|
|
281
|
+
} catch (error) {
|
|
282
|
+
emit({ requestId, error: error instanceof Error ? error.message : String(error) });
|
|
283
|
+
}
|
|
284
|
+
return payloads;
|
|
285
|
+
}
|
|
243
286
|
case 'config:save-yaml': {
|
|
244
287
|
try {
|
|
245
288
|
const yaml = message.yaml;
|
|
@@ -758,6 +801,8 @@ export function isDemoHttpAllowed(method: string, pathname: string, apiBase: str
|
|
|
758
801
|
const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
|
|
759
802
|
const m = method.toUpperCase();
|
|
760
803
|
if (m === 'GET' && pathname === `${normalizedBase}/events`) return true;
|
|
804
|
+
if (m === 'GET' && pathname === `${normalizedBase}/events/history`) return true;
|
|
805
|
+
if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length))) return true;
|
|
761
806
|
if (m === 'POST' && pathname === `${normalizedBase}/console/request`) return true;
|
|
762
807
|
// Read-only dashboard / plugin inspection routes (legacy host-api parity).
|
|
763
808
|
if (m === 'GET' && pathname === `${normalizedBase}/system/status`) return true;
|
|
@@ -766,3 +811,13 @@ export function isDemoHttpAllowed(method: string, pathname: string, apiBase: str
|
|
|
766
811
|
|| pathname.startsWith(`${normalizedBase}/plugins/`))) return true;
|
|
767
812
|
return false;
|
|
768
813
|
}
|
|
814
|
+
|
|
815
|
+
const DEMO_INTROSPECTION_PATHS: ReadonlySet<string> = new Set([
|
|
816
|
+
'/introspection/commands',
|
|
817
|
+
'/introspection/middlewares',
|
|
818
|
+
'/introspection/components',
|
|
819
|
+
'/introspection/endpoints',
|
|
820
|
+
'/introspection/bindings',
|
|
821
|
+
'/introspection/tools',
|
|
822
|
+
'/introspection/mcp',
|
|
823
|
+
]);
|
package/src/http-host.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TokenRegistry,
|
|
18
18
|
extractBearerToken,
|
|
19
19
|
isDemoWebSocketPath,
|
|
20
|
+
type AuthenticatedTokenPrincipal,
|
|
20
21
|
type AuthScope,
|
|
21
22
|
type ScopedTokenConfig,
|
|
22
23
|
type TokenRegistryConfig,
|
|
@@ -73,6 +74,7 @@ export type HttpHandler = (
|
|
|
73
74
|
response: ServerResponse,
|
|
74
75
|
url: URL,
|
|
75
76
|
authScope: AuthScope,
|
|
77
|
+
authenticatedPrincipal?: AuthenticatedTokenPrincipal,
|
|
76
78
|
) => void | Promise<void>;
|
|
77
79
|
|
|
78
80
|
export interface HttpRouteRegistration {
|
|
@@ -323,7 +325,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
|
|
|
323
325
|
return;
|
|
324
326
|
}
|
|
325
327
|
if (!release) {
|
|
326
|
-
await route.handler(request, response, url, auth.scope);
|
|
328
|
+
await route.handler(request, response, url, auth.scope, auth.principal);
|
|
327
329
|
return;
|
|
328
330
|
}
|
|
329
331
|
let released = false;
|
|
@@ -336,7 +338,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
|
|
|
336
338
|
};
|
|
337
339
|
response.once('finish', releaseResponse);
|
|
338
340
|
response.once('close', releaseResponse);
|
|
339
|
-
await route.handler(request, response, url, auth.scope);
|
|
341
|
+
await route.handler(request, response, url, auth.scope, auth.principal);
|
|
340
342
|
if (response.writableFinished || response.destroyed) releaseResponse();
|
|
341
343
|
} catch (err) {
|
|
342
344
|
// 统一把请求体错误(400/413)映射回对应状态码,
|
|
@@ -363,7 +365,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
|
|
|
363
365
|
request: IncomingMessage,
|
|
364
366
|
url: URL,
|
|
365
367
|
pathname: string,
|
|
366
|
-
): { ok: true; scope: AuthScope } | { ok: false } {
|
|
368
|
+
): { ok: true; scope: AuthScope; principal?: AuthenticatedTokenPrincipal } | { ok: false } {
|
|
367
369
|
if (!requiresHttpAuth(pathname, apiBase, authExempt)) {
|
|
368
370
|
return { ok: true, scope: 'full' };
|
|
369
371
|
}
|
|
@@ -380,7 +382,8 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
|
|
|
380
382
|
const method = (request.method ?? 'GET').toUpperCase();
|
|
381
383
|
if (!isDemoHttpAllowed(method, pathname, apiBase)) return { ok: false };
|
|
382
384
|
}
|
|
383
|
-
|
|
385
|
+
const principal = tokenRegistry.resolvePrincipal(token);
|
|
386
|
+
return { ok: true, scope, ...(principal ? { principal } : {}) };
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
function listListedRoutes(): readonly ListedRoute[] {
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
TokenRegistry,
|
|
16
16
|
extractBearerToken,
|
|
17
17
|
isDemoWebSocketPath,
|
|
18
|
+
type AuthenticatedTokenPrincipal,
|
|
18
19
|
type AuthScope,
|
|
19
20
|
type ScopedTokenConfig,
|
|
20
21
|
type TokenRegistryConfig,
|
|
@@ -70,5 +71,7 @@ export {
|
|
|
70
71
|
dispatchExtendedConsoleRpc,
|
|
71
72
|
type ConsoleRpcExtendedCtx,
|
|
72
73
|
type ConsoleScheduleEngine,
|
|
74
|
+
type ConsoleWorkroomProfileControlPort,
|
|
75
|
+
type ConsoleWorkroomKnowledgeControlPort,
|
|
73
76
|
type ExtendedRpcResult,
|
|
74
77
|
} from './console-rpc-extended.js';
|