@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
package/lib/console-events.d.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import type { ServerResponse } from 'node:http';
|
|
2
|
+
import type { ConsoleEventEnvelope, ConsoleEventHistoryPage, ConsoleEventHistoryQuery, ConsoleEventPayloadMap, KnownConsoleEventType } from '@zhin.js/console-protocol';
|
|
2
3
|
/**
|
|
3
4
|
* Console 实时事件枢纽(Plugin Runtime Host 的 `/api/events` SSE 事件源)。
|
|
4
5
|
* 单进程内 fan-out:`publish` 向所有已订阅的 SSE response 写帧;
|
|
5
6
|
* write 失败或连接断开自动摘除订阅。
|
|
6
7
|
*/
|
|
8
|
+
export interface ConsoleEventSubscriptionOptions {
|
|
9
|
+
readonly runtimeId?: string;
|
|
10
|
+
readonly after?: number;
|
|
11
|
+
}
|
|
7
12
|
export interface ConsoleEventHub {
|
|
8
|
-
publish(type:
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
publish<Type extends string>(type: Type, data: Type extends KnownConsoleEventType ? ConsoleEventPayloadMap[Type] : unknown): ConsoleEventEnvelope<Type, Type extends KnownConsoleEventType ? ConsoleEventPayloadMap[Type] : unknown>;
|
|
14
|
+
history(query?: ConsoleEventHistoryQuery): ConsoleEventHistoryPage;
|
|
15
|
+
/** Atomically replay the resumable suffix, then join live fan-out. */
|
|
16
|
+
subscribe(response: ServerResponse, options?: ConsoleEventSubscriptionOptions): () => void;
|
|
17
|
+
readonly runtimeId: string;
|
|
11
18
|
readonly subscriberCount: number;
|
|
12
19
|
}
|
|
13
|
-
export
|
|
20
|
+
export interface ConsoleEventHubOptions {
|
|
21
|
+
readonly runtimeId?: string;
|
|
22
|
+
readonly historyLimit?: number;
|
|
23
|
+
readonly historyByteLimit?: number;
|
|
24
|
+
readonly historyPageByteLimit?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function createConsoleEventHub(options?: ConsoleEventHubOptions): ConsoleEventHub;
|
|
27
|
+
export declare function serializeConsoleEvent(event: ConsoleEventEnvelope): string;
|
package/lib/console-events.js
CHANGED
|
@@ -1,33 +1,145 @@
|
|
|
1
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const DEFAULT_HISTORY_LIMIT = 256;
|
|
3
|
+
const DEFAULT_PAGE_LIMIT = 200;
|
|
4
|
+
const MAX_PAGE_LIMIT = 500;
|
|
5
|
+
const MAX_EVENT_BYTES = 64 * 1024;
|
|
6
|
+
const DEFAULT_HISTORY_BYTE_LIMIT = 8 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_HISTORY_PAGE_BYTE_LIMIT = 1024 * 1024;
|
|
8
|
+
export function createConsoleEventHub(options = {}) {
|
|
2
9
|
let nextId = 0;
|
|
10
|
+
const runtimeId = options.runtimeId ?? randomUUID();
|
|
11
|
+
const historyLimit = positiveInteger(options.historyLimit, DEFAULT_HISTORY_LIMIT);
|
|
12
|
+
const historyByteLimit = positiveInteger(options.historyByteLimit, DEFAULT_HISTORY_BYTE_LIMIT);
|
|
13
|
+
const historyPageByteLimit = positiveInteger(options.historyPageByteLimit, DEFAULT_HISTORY_PAGE_BYTE_LIMIT);
|
|
14
|
+
const journal = [];
|
|
15
|
+
let journalBytes = 0;
|
|
3
16
|
const subscribers = new Set();
|
|
4
17
|
const remove = (response) => {
|
|
5
18
|
subscribers.delete(response);
|
|
6
19
|
};
|
|
20
|
+
const write = (response, entry) => {
|
|
21
|
+
let failed = false;
|
|
22
|
+
const accepted = response.write(entry.frame, (error) => {
|
|
23
|
+
if (error) {
|
|
24
|
+
failed = true;
|
|
25
|
+
remove(response);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (failed)
|
|
29
|
+
throw new Error('Console event subscriber rejected replay');
|
|
30
|
+
if (!accepted) {
|
|
31
|
+
remove(response);
|
|
32
|
+
response.destroy(new Error('Console event subscriber backpressure limit reached'));
|
|
33
|
+
throw new Error('Console event subscriber backpressure limit reached');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
7
36
|
return {
|
|
37
|
+
runtimeId,
|
|
8
38
|
get subscriberCount() {
|
|
9
39
|
return subscribers.size;
|
|
10
40
|
},
|
|
11
41
|
publish(type, data) {
|
|
12
|
-
|
|
13
|
-
const
|
|
42
|
+
const snapshot = jsonSnapshot(data);
|
|
43
|
+
const event = Object.freeze({
|
|
44
|
+
runtimeId,
|
|
45
|
+
eventId: ++nextId,
|
|
46
|
+
type,
|
|
47
|
+
data: snapshot,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
const frame = serializeConsoleEvent(event);
|
|
51
|
+
const entry = Object.freeze({ event, frame, bytes: Buffer.byteLength(frame, 'utf8') });
|
|
52
|
+
journal.push(entry);
|
|
53
|
+
journalBytes += entry.bytes;
|
|
54
|
+
while (journal.length > historyLimit || journalBytes > historyByteLimit) {
|
|
55
|
+
journalBytes -= journal.shift()?.bytes ?? 0;
|
|
56
|
+
}
|
|
14
57
|
for (const response of [...subscribers]) {
|
|
15
58
|
try {
|
|
16
|
-
|
|
17
|
-
if (error)
|
|
18
|
-
remove(response);
|
|
19
|
-
});
|
|
59
|
+
write(response, entry);
|
|
20
60
|
}
|
|
21
61
|
catch {
|
|
22
62
|
remove(response);
|
|
23
63
|
}
|
|
24
64
|
}
|
|
65
|
+
return event;
|
|
25
66
|
},
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
history(query = {}) {
|
|
68
|
+
const requestedAfter = nonNegativeInteger(query.after, 0);
|
|
69
|
+
const sameRuntime = !query.runtimeId || query.runtimeId === runtimeId;
|
|
70
|
+
const cursorAhead = requestedAfter > nextId;
|
|
71
|
+
const after = sameRuntime && !cursorAhead ? requestedAfter : 0;
|
|
72
|
+
const limit = Math.min(MAX_PAGE_LIMIT, positiveInteger(query.limit, DEFAULT_PAGE_LIMIT));
|
|
73
|
+
const oldestAvailableEventId = journal[0]?.event.eventId ?? null;
|
|
74
|
+
const latestEventId = nextId;
|
|
75
|
+
const gap = !sameRuntime || cursorAhead
|
|
76
|
+
|| (oldestAvailableEventId !== null
|
|
77
|
+
? after < oldestAvailableEventId - 1
|
|
78
|
+
: after < latestEventId);
|
|
79
|
+
const available = journal.filter((entry) => entry.event.eventId > after);
|
|
80
|
+
const selected = [];
|
|
81
|
+
let selectedBytes = 0;
|
|
82
|
+
for (const entry of available) {
|
|
83
|
+
if (selected.length >= limit)
|
|
84
|
+
break;
|
|
85
|
+
if (selected.length > 0 && selectedBytes + entry.bytes > historyPageByteLimit)
|
|
86
|
+
break;
|
|
87
|
+
selected.push(entry.event);
|
|
88
|
+
selectedBytes += entry.bytes;
|
|
89
|
+
}
|
|
90
|
+
const items = Object.freeze(selected);
|
|
91
|
+
const nextAfter = items.at(-1)?.eventId ?? after;
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
runtimeId,
|
|
94
|
+
items,
|
|
95
|
+
oldestAvailableEventId,
|
|
96
|
+
latestEventId,
|
|
97
|
+
nextAfter,
|
|
98
|
+
hasMore: available.length > items.length,
|
|
99
|
+
gap,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
subscribe(response, options = {}) {
|
|
103
|
+
// Replay and subscription are synchronous relative to publish(), closing
|
|
104
|
+
// the race between HTTP recovery and the live stream hand-off.
|
|
105
|
+
const requestedAfter = nonNegativeInteger(options.after, 0);
|
|
106
|
+
const after = options.runtimeId && options.runtimeId !== runtimeId
|
|
107
|
+
|| requestedAfter > nextId
|
|
108
|
+
? 0
|
|
109
|
+
: requestedAfter;
|
|
110
|
+
try {
|
|
111
|
+
for (const entry of journal) {
|
|
112
|
+
if (entry.event.eventId > after)
|
|
113
|
+
write(response, entry);
|
|
114
|
+
}
|
|
115
|
+
subscribers.add(response);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
remove(response);
|
|
119
|
+
}
|
|
28
120
|
response.once('close', () => remove(response));
|
|
29
121
|
response.once('error', () => remove(response));
|
|
30
122
|
return () => remove(response);
|
|
31
123
|
},
|
|
32
124
|
};
|
|
33
125
|
}
|
|
126
|
+
export function serializeConsoleEvent(event) {
|
|
127
|
+
return `id: ${event.eventId}\nevent: ${event.type}\nruntime: ${event.runtimeId}\ntimestamp: ${event.timestamp}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
|
128
|
+
}
|
|
129
|
+
function positiveInteger(value, fallback) {
|
|
130
|
+
const parsed = Number(value);
|
|
131
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
132
|
+
}
|
|
133
|
+
function nonNegativeInteger(value, fallback) {
|
|
134
|
+
const parsed = Number(value);
|
|
135
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
136
|
+
}
|
|
137
|
+
function jsonSnapshot(value) {
|
|
138
|
+
const encoded = JSON.stringify(value ?? null);
|
|
139
|
+
if (encoded === undefined)
|
|
140
|
+
throw new TypeError('Console event data must be JSON serializable');
|
|
141
|
+
if (Buffer.byteLength(encoded, 'utf8') > MAX_EVENT_BYTES) {
|
|
142
|
+
throw new RangeError(`Console event data exceeds ${MAX_EVENT_BYTES} bytes`);
|
|
143
|
+
}
|
|
144
|
+
return JSON.parse(encoded);
|
|
145
|
+
}
|
|
@@ -30,21 +30,25 @@ export interface ConsoleRestCtx {
|
|
|
30
30
|
/** 内省数据门面 — 由 basic/cli 用 agent 包 collectIntrospection* 装配。 */
|
|
31
31
|
export interface ConsoleAgentIntrospection {
|
|
32
32
|
commands?(): readonly unknown[];
|
|
33
|
+
middlewares?(): readonly unknown[];
|
|
34
|
+
components?(): readonly unknown[];
|
|
33
35
|
bindings?(): readonly unknown[];
|
|
34
36
|
tools?(): readonly unknown[];
|
|
35
37
|
mcp?(): {
|
|
36
38
|
rows: readonly unknown[];
|
|
37
39
|
note?: string;
|
|
38
40
|
} | readonly unknown[];
|
|
41
|
+
renderComponent?(input: Readonly<{
|
|
42
|
+
requester: string;
|
|
43
|
+
name: string;
|
|
44
|
+
props: unknown;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
}>): Promise<unknown>;
|
|
39
47
|
}
|
|
40
48
|
/** Session tree 门面 — 形状对齐 agent 包 SessionTreeRuntimeHandle。 */
|
|
41
49
|
export interface ConsoleAgentSessionTree {
|
|
42
50
|
resolveActiveSessionId(sessionKey: string): Promise<string | null>;
|
|
43
|
-
|
|
44
|
-
getBySessionId(sessionId: string): Promise<{
|
|
45
|
-
active_leaf_message_id?: number | null;
|
|
46
|
-
} | null | undefined>;
|
|
47
|
-
};
|
|
51
|
+
getActiveLeafMessageId(sessionId: string): Promise<number | null>;
|
|
48
52
|
listBranchPoints(sessionId: string): Promise<readonly unknown[]>;
|
|
49
53
|
switchActiveLeaf(sessionId: string, messageId: number): Promise<boolean>;
|
|
50
54
|
jumpToBranchIndex(sessionId: string, index: number): Promise<{
|
|
@@ -66,6 +70,12 @@ export interface ConsoleRestPagesOptions {
|
|
|
66
70
|
readonly pluginRegistryUrl?: string;
|
|
67
71
|
/** 默认 `https://registry.npmmirror.com`(legacy 同源)。 */
|
|
68
72
|
readonly npmRegistryUrl?: string;
|
|
73
|
+
/** Component preview deadline; default 5 seconds. */
|
|
74
|
+
readonly componentPreviewTimeoutMs?: number;
|
|
75
|
+
/** Maximum serialized Component preview; default 256 KiB. */
|
|
76
|
+
readonly componentPreviewMaxBytes?: number;
|
|
77
|
+
/** Maximum in-flight Component previews per Console route set; default 2. */
|
|
78
|
+
readonly componentPreviewMaxConcurrent?: number;
|
|
69
79
|
}
|
|
70
80
|
/**
|
|
71
81
|
* 注册 Console 页面 REST 路由。返回注销函数(注销全部已注册路由)。
|
|
@@ -3,6 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { HttpBodyError, readJsonBody } from './json-body.js';
|
|
4
4
|
const INTROSPECTION_PAGE_SIZES = {
|
|
5
5
|
commands: 25,
|
|
6
|
+
middlewares: 30,
|
|
7
|
+
components: 30,
|
|
6
8
|
tools: 15,
|
|
7
9
|
endpoints: 30,
|
|
8
10
|
bindings: 30,
|
|
@@ -26,7 +28,7 @@ export function registerConsoleRestPages(host, ctx, options = {}) {
|
|
|
26
28
|
};
|
|
27
29
|
registerLogsRoutes(route, base, ctx);
|
|
28
30
|
registerMarketplaceRoutes(route, base, ctx, fetchFn, pluginRegistryUrl, npmRegistryUrl);
|
|
29
|
-
registerIntrospectionRoutes(route, base, ctx);
|
|
31
|
+
registerIntrospectionRoutes(route, base, ctx, options);
|
|
30
32
|
registerAgentSessionRoutes(route, base, ctx);
|
|
31
33
|
return () => {
|
|
32
34
|
for (const dispose of disposers.splice(0))
|
|
@@ -378,17 +380,37 @@ async function listInstalledPluginPackages(projectRoot) {
|
|
|
378
380
|
// ---------------------------------------------------------------------------
|
|
379
381
|
// introspection(legacy introspection-rest-api.ts;分页形状对齐 IntrospectionJsonResponse)
|
|
380
382
|
// ---------------------------------------------------------------------------
|
|
381
|
-
function registerIntrospectionRoutes(route, base, ctx) {
|
|
383
|
+
function registerIntrospectionRoutes(route, base, ctx, options) {
|
|
382
384
|
const collectors = [
|
|
383
385
|
{
|
|
384
386
|
kind: 'commands',
|
|
385
|
-
collect: () => ({ rows:
|
|
387
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.commands?.() ?? []) }),
|
|
386
388
|
fields: [
|
|
387
389
|
(c) => stringProp(c, 'pattern'),
|
|
388
390
|
(c) => stringProp(c, 'desc'),
|
|
389
391
|
(c) => stringProp(c, 'plugin'),
|
|
390
392
|
],
|
|
391
393
|
},
|
|
394
|
+
{
|
|
395
|
+
kind: 'middlewares',
|
|
396
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.middlewares?.() ?? []) }),
|
|
397
|
+
fields: [
|
|
398
|
+
(m) => stringProp(m, 'name'),
|
|
399
|
+
(m) => stringProp(m, 'owner'),
|
|
400
|
+
(m) => stringProp(m, 'phase'),
|
|
401
|
+
(m) => stringProp(m, 'target'),
|
|
402
|
+
(m) => stringProp(m, 'source'),
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
kind: 'components',
|
|
407
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.components?.() ?? []) }),
|
|
408
|
+
fields: [
|
|
409
|
+
(c) => stringProp(c, 'name'),
|
|
410
|
+
(c) => stringProp(c, 'owner'),
|
|
411
|
+
(c) => stringProp(c, 'source'),
|
|
412
|
+
],
|
|
413
|
+
},
|
|
392
414
|
{
|
|
393
415
|
kind: 'endpoints',
|
|
394
416
|
collect: () => ({
|
|
@@ -403,7 +425,7 @@ function registerIntrospectionRoutes(route, base, ctx) {
|
|
|
403
425
|
},
|
|
404
426
|
{
|
|
405
427
|
kind: 'bindings',
|
|
406
|
-
collect: () => ({ rows:
|
|
428
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.bindings?.() ?? []) }),
|
|
407
429
|
fields: [
|
|
408
430
|
(a) => stringProp(a, 'name'),
|
|
409
431
|
(a) => stringProp(a, 'provider'),
|
|
@@ -412,7 +434,7 @@ function registerIntrospectionRoutes(route, base, ctx) {
|
|
|
412
434
|
},
|
|
413
435
|
{
|
|
414
436
|
kind: 'tools',
|
|
415
|
-
collect: () => ({ rows:
|
|
437
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.tools?.() ?? []) }),
|
|
416
438
|
fields: [
|
|
417
439
|
(t) => stringProp(t, 'name'),
|
|
418
440
|
(t) => stringProp(t, 'source'),
|
|
@@ -422,7 +444,7 @@ function registerIntrospectionRoutes(route, base, ctx) {
|
|
|
422
444
|
{
|
|
423
445
|
kind: 'mcp',
|
|
424
446
|
collect: () => {
|
|
425
|
-
const result =
|
|
447
|
+
const result = withAgentIntrospection(ctx, (value) => value?.mcp?.());
|
|
426
448
|
if (result == null)
|
|
427
449
|
return { rows: [] };
|
|
428
450
|
// readonly 数组不能用 Array.isArray 收窄,按对象形状判断
|
|
@@ -444,7 +466,7 @@ function registerIntrospectionRoutes(route, base, ctx) {
|
|
|
444
466
|
const slice = paginateItems(filtered, parsePositiveInt(query.get('page'), 1), parsePositiveInt(query.get('pageSize'), INTROSPECTION_PAGE_SIZES[kind]));
|
|
445
467
|
const missing = kind === 'endpoints'
|
|
446
468
|
? !ctx.getEndpoints
|
|
447
|
-
: !
|
|
469
|
+
: !hasAgentIntrospection(ctx);
|
|
448
470
|
const degradedNote = note ?? (missing
|
|
449
471
|
? kind === 'endpoints'
|
|
450
472
|
? 'Endpoints 数据源未接线(ctx.getEndpoints 缺失)'
|
|
@@ -464,18 +486,142 @@ function registerIntrospectionRoutes(route, base, ctx) {
|
|
|
464
486
|
}
|
|
465
487
|
}, { summary: `Introspection: ${kind}`, tags: ['console', 'introspection'] });
|
|
466
488
|
}
|
|
489
|
+
let activeComponentPreviews = 0;
|
|
490
|
+
route('POST', `${base}/introspection/components/render`, async (request, response, _url, authScope) => {
|
|
491
|
+
if (!requireWriteScope(response, ctx, authScope))
|
|
492
|
+
return;
|
|
493
|
+
const body = (await readJsonBody(request)) ?? {};
|
|
494
|
+
const requester = typeof body.requester === 'string' ? body.requester.trim() : '';
|
|
495
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
496
|
+
if (!requester || !name || requester.length > 256 || name.length > 160) {
|
|
497
|
+
writeJson(response, 400, { success: false, error: '请提供有效的 requester 与 component name' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const maxConcurrent = clampPositiveInt(options.componentPreviewMaxConcurrent, 2, 8);
|
|
501
|
+
if (activeComponentPreviews >= maxConcurrent) {
|
|
502
|
+
writeJson(response, 503, { success: false, error: 'Component preview 正忙,请稍后重试' });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const lease = ctx.acquireAgentRuntime?.();
|
|
506
|
+
const render = lease?.value.introspection?.renderComponent;
|
|
507
|
+
if (!lease || !render) {
|
|
508
|
+
lease?.release();
|
|
509
|
+
writeJson(response, 503, { success: false, error: 'Component Runtime 未就绪' });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const controller = new AbortController();
|
|
513
|
+
const timeoutMs = clampPositiveInt(options.componentPreviewTimeoutMs, 5_000, 30_000);
|
|
514
|
+
const timeout = setTimeout(() => controller.abort(new ComponentPreviewTimeoutError(`Component preview 超过 ${timeoutMs}ms`)), timeoutMs);
|
|
515
|
+
activeComponentPreviews += 1;
|
|
516
|
+
let released = false;
|
|
517
|
+
const release = () => {
|
|
518
|
+
if (released)
|
|
519
|
+
return;
|
|
520
|
+
released = true;
|
|
521
|
+
activeComponentPreviews -= 1;
|
|
522
|
+
lease.release();
|
|
523
|
+
};
|
|
524
|
+
const renderPromise = Promise.resolve().then(() => render({ requester, name, props: body.props ?? {}, signal: controller.signal }));
|
|
525
|
+
// A timed-out implementation may ignore AbortSignal. Keep its generation
|
|
526
|
+
// lease until the real operation settles; the bounded concurrency gate
|
|
527
|
+
// prevents non-cooperative previews from pinning unbounded generations.
|
|
528
|
+
void renderPromise.then(release, release);
|
|
529
|
+
try {
|
|
530
|
+
const output = await raceWithAbort(renderPromise, controller.signal);
|
|
531
|
+
assertPreviewOutput(output, options.componentPreviewMaxBytes ?? 256 * 1024);
|
|
532
|
+
writeJson(response, 200, { success: true, data: { requester, name, output } });
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
const status = error instanceof ComponentPreviewTimeoutError
|
|
536
|
+
? 408
|
|
537
|
+
: error instanceof ComponentPreviewLimitError ? 413 : 422;
|
|
538
|
+
writeJson(response, status, {
|
|
539
|
+
success: false,
|
|
540
|
+
error: error instanceof Error ? error.message : String(error),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
clearTimeout(timeout);
|
|
545
|
+
if (!controller.signal.aborted)
|
|
546
|
+
controller.abort();
|
|
547
|
+
}
|
|
548
|
+
}, { summary: 'Render a Component preview', tags: ['console', 'introspection', 'component'] });
|
|
549
|
+
}
|
|
550
|
+
class ComponentPreviewTimeoutError extends Error {
|
|
551
|
+
name = 'ComponentPreviewTimeoutError';
|
|
467
552
|
}
|
|
468
|
-
|
|
553
|
+
class ComponentPreviewLimitError extends Error {
|
|
554
|
+
name = 'ComponentPreviewLimitError';
|
|
555
|
+
}
|
|
556
|
+
function clampPositiveInt(value, fallback, ceiling) {
|
|
557
|
+
if (!Number.isFinite(value) || !value || value < 1)
|
|
558
|
+
return fallback;
|
|
559
|
+
return Math.min(Math.floor(value), ceiling);
|
|
560
|
+
}
|
|
561
|
+
async function raceWithAbort(promise, signal) {
|
|
562
|
+
if (signal.aborted)
|
|
563
|
+
throw abortReason(signal);
|
|
564
|
+
return await new Promise((resolve, reject) => {
|
|
565
|
+
const onAbort = () => reject(abortReason(signal));
|
|
566
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
567
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function abortReason(signal) {
|
|
571
|
+
return signal.reason instanceof Error
|
|
572
|
+
? signal.reason
|
|
573
|
+
: new DOMException('Component preview aborted', 'AbortError');
|
|
574
|
+
}
|
|
575
|
+
function assertPreviewOutput(output, maxBytes) {
|
|
576
|
+
const byteLimit = clampPositiveInt(maxBytes, 256 * 1024, 1024 * 1024);
|
|
577
|
+
const seen = new WeakSet();
|
|
578
|
+
let nodes = 0;
|
|
579
|
+
const visit = (value, depth) => {
|
|
580
|
+
nodes += 1;
|
|
581
|
+
if (nodes > 10_000)
|
|
582
|
+
throw new ComponentPreviewLimitError('Component preview 节点数超过 10000');
|
|
583
|
+
if (depth > 24)
|
|
584
|
+
throw new ComponentPreviewLimitError('Component preview 嵌套深度超过 24');
|
|
585
|
+
if (!value || typeof value !== 'object')
|
|
586
|
+
return;
|
|
587
|
+
if (seen.has(value))
|
|
588
|
+
throw new ComponentPreviewLimitError('Component preview 不能包含循环引用');
|
|
589
|
+
seen.add(value);
|
|
590
|
+
if (Array.isArray(value)) {
|
|
591
|
+
for (const item of value)
|
|
592
|
+
visit(item, depth + 1);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
for (const item of Object.values(value))
|
|
596
|
+
visit(item, depth + 1);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
visit(output, 0);
|
|
600
|
+
let serialized;
|
|
601
|
+
try {
|
|
602
|
+
serialized = JSON.stringify(output);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
throw new ComponentPreviewLimitError(`Component preview 无法序列化: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
|
+
}
|
|
607
|
+
if (Buffer.byteLength(serialized ?? '') > byteLimit) {
|
|
608
|
+
throw new ComponentPreviewLimitError(`Component preview 超过 ${byteLimit} bytes`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function withAgentIntrospection(ctx, operation) {
|
|
469
612
|
const lease = ctx.acquireAgentRuntime?.();
|
|
470
613
|
if (!lease)
|
|
471
|
-
return undefined;
|
|
614
|
+
return operation(undefined);
|
|
472
615
|
try {
|
|
473
|
-
return lease.value.introspection;
|
|
616
|
+
return operation(lease.value.introspection);
|
|
474
617
|
}
|
|
475
618
|
finally {
|
|
476
619
|
lease.release();
|
|
477
620
|
}
|
|
478
621
|
}
|
|
622
|
+
function hasAgentIntrospection(ctx) {
|
|
623
|
+
return withAgentIntrospection(ctx, (value) => value !== undefined);
|
|
624
|
+
}
|
|
479
625
|
// ---------------------------------------------------------------------------
|
|
480
626
|
// agent sessions(legacy agent-sessions-rest-api.ts;ADR 0010 D3)
|
|
481
627
|
// ---------------------------------------------------------------------------
|
|
@@ -518,14 +664,14 @@ function registerAgentSessionRoutes(route, base, ctx) {
|
|
|
518
664
|
});
|
|
519
665
|
return;
|
|
520
666
|
}
|
|
521
|
-
const
|
|
667
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
522
668
|
const points = await runtime.listBranchPoints(sessionId);
|
|
523
669
|
writeJson(response, 200, {
|
|
524
670
|
success: true,
|
|
525
671
|
data: {
|
|
526
672
|
sessionKey: parsed.sessionKey,
|
|
527
673
|
sessionId,
|
|
528
|
-
activeLeafMessageId
|
|
674
|
+
activeLeafMessageId,
|
|
529
675
|
points,
|
|
530
676
|
},
|
|
531
677
|
});
|
|
@@ -594,27 +740,27 @@ function registerAgentSessionRoutes(route, base, ctx) {
|
|
|
594
740
|
}
|
|
595
741
|
if (branchIndex !== undefined) {
|
|
596
742
|
const result = await runtime.jumpToBranchIndex(sessionId, branchIndex);
|
|
597
|
-
const
|
|
743
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
598
744
|
writeJson(response, result.ok ? 200 : 400, {
|
|
599
745
|
success: result.ok,
|
|
600
746
|
message: result.message,
|
|
601
747
|
data: {
|
|
602
748
|
sessionKey: parsed.sessionKey,
|
|
603
749
|
sessionId,
|
|
604
|
-
activeLeafMessageId
|
|
750
|
+
activeLeafMessageId,
|
|
605
751
|
},
|
|
606
752
|
});
|
|
607
753
|
return;
|
|
608
754
|
}
|
|
609
755
|
const ok = await runtime.switchActiveLeaf(sessionId, messageId);
|
|
610
|
-
const
|
|
756
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
611
757
|
writeJson(response, ok ? 200 : 400, {
|
|
612
758
|
success: ok,
|
|
613
759
|
message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
|
|
614
760
|
data: {
|
|
615
761
|
sessionKey: parsed.sessionKey,
|
|
616
762
|
sessionId,
|
|
617
|
-
activeLeafMessageId
|
|
763
|
+
activeLeafMessageId,
|
|
618
764
|
},
|
|
619
765
|
});
|
|
620
766
|
}
|
|
@@ -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 内
|
|
@@ -28,8 +30,85 @@ export interface ConsoleRpcExtendedCtx {
|
|
|
28
30
|
get(name: string): unknown;
|
|
29
31
|
};
|
|
30
32
|
};
|
|
31
|
-
/**
|
|
33
|
+
/** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
|
|
32
34
|
resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
|
|
35
|
+
/** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
|
|
36
|
+
loginAssist?: {
|
|
37
|
+
listPending(): readonly {
|
|
38
|
+
id: string;
|
|
39
|
+
adapter: string;
|
|
40
|
+
endpointKey: string;
|
|
41
|
+
type: string;
|
|
42
|
+
payload: Record<string, unknown>;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
expiresAt?: number;
|
|
45
|
+
}[];
|
|
46
|
+
submit(id: string, value: string | Record<string, unknown>): boolean;
|
|
47
|
+
cancel(id: string, reason?: string): boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
50
|
+
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
51
|
+
/** Authenticated HTTP principal; never accepted from the RPC body. */
|
|
52
|
+
authenticatedPrincipal?: Readonly<{
|
|
53
|
+
principalId: string;
|
|
54
|
+
}>;
|
|
55
|
+
workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
|
|
56
|
+
}
|
|
57
|
+
export interface ConsoleWorkroomProfileControlPort {
|
|
58
|
+
publishPack(command: Readonly<{
|
|
59
|
+
operationId: string;
|
|
60
|
+
pack: unknown;
|
|
61
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
62
|
+
principalId: string;
|
|
63
|
+
}>): Promise<unknown>;
|
|
64
|
+
publishProfile(command: Readonly<{
|
|
65
|
+
operationId: string;
|
|
66
|
+
projectId: string;
|
|
67
|
+
expectedRegistryRevision: number;
|
|
68
|
+
overlay: unknown;
|
|
69
|
+
activate: boolean;
|
|
70
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
71
|
+
principalId: string;
|
|
72
|
+
}>): Promise<unknown>;
|
|
73
|
+
publishRollback(command: Readonly<{
|
|
74
|
+
operationId: string;
|
|
75
|
+
projectId: string;
|
|
76
|
+
expectedRegistryRevision: number;
|
|
77
|
+
restoredFromRevisionId: string;
|
|
78
|
+
overlay: unknown;
|
|
79
|
+
activate: boolean;
|
|
80
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
81
|
+
principalId: string;
|
|
82
|
+
}>): Promise<unknown>;
|
|
83
|
+
publishPlanningPolicy(command: Readonly<{
|
|
84
|
+
operationId: string;
|
|
85
|
+
projectId: string;
|
|
86
|
+
profileRevisionId: string;
|
|
87
|
+
revision: number;
|
|
88
|
+
expectedPreviousDigest?: string;
|
|
89
|
+
policy: unknown;
|
|
90
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
91
|
+
principalId: string;
|
|
92
|
+
}>): Promise<unknown>;
|
|
93
|
+
}
|
|
94
|
+
export interface ConsoleWorkroomKnowledgeControlPort {
|
|
95
|
+
read(projectId: string): Promise<unknown>;
|
|
96
|
+
publish(command: Readonly<{
|
|
97
|
+
operationId: string;
|
|
98
|
+
projectId: string;
|
|
99
|
+
expectedRevision: number;
|
|
100
|
+
entries: readonly unknown[];
|
|
101
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
102
|
+
principalId: string;
|
|
103
|
+
}>): Promise<unknown>;
|
|
104
|
+
rollback(command: Readonly<{
|
|
105
|
+
operationId: string;
|
|
106
|
+
projectId: string;
|
|
107
|
+
expectedRevision: number;
|
|
108
|
+
restoreRevision: number;
|
|
109
|
+
}>, authenticatedPrincipal: Readonly<{
|
|
110
|
+
principalId: string;
|
|
111
|
+
}>): Promise<unknown>;
|
|
33
112
|
}
|
|
34
113
|
export interface ConsoleScheduleEngine {
|
|
35
114
|
addJob(job: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -63,6 +142,17 @@ interface EndpointManagementPort {
|
|
|
63
142
|
};
|
|
64
143
|
}[]>;
|
|
65
144
|
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
145
|
+
listRequests?(): Promise<readonly {
|
|
146
|
+
platform_request_id: string;
|
|
147
|
+
type: string;
|
|
148
|
+
scene_type?: string | null;
|
|
149
|
+
scene_id: string;
|
|
150
|
+
sub_type?: string | null;
|
|
151
|
+
actor_id: string;
|
|
152
|
+
actor_name?: string | null;
|
|
153
|
+
comment?: string | null;
|
|
154
|
+
created_at: number;
|
|
155
|
+
}[]>;
|
|
66
156
|
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
67
157
|
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
68
158
|
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|