@zhin.js/host-http 1.0.10 → 1.0.11
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-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 +198 -3
- package/lib/console-rpc.d.ts +10 -0
- package/lib/console-rpc.js +35 -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-rest-pages.ts +181 -20
- package/src/console-rpc-extended.ts +274 -4
- package/src/console-rpc.ts +43 -0
- package/src/http-host.ts +7 -4
- package/src/index.ts +3 -0
- package/src/token-registry.ts +36 -8
|
@@ -47,19 +47,23 @@ export interface ConsoleRestCtx {
|
|
|
47
47
|
/** 内省数据门面 — 由 basic/cli 用 agent 包 collectIntrospection* 装配。 */
|
|
48
48
|
export interface ConsoleAgentIntrospection {
|
|
49
49
|
commands?(): readonly unknown[];
|
|
50
|
+
middlewares?(): readonly unknown[];
|
|
51
|
+
components?(): readonly unknown[];
|
|
50
52
|
bindings?(): readonly unknown[];
|
|
51
53
|
tools?(): readonly unknown[];
|
|
52
54
|
mcp?(): { rows: readonly unknown[]; note?: string } | readonly unknown[];
|
|
55
|
+
renderComponent?(input: Readonly<{
|
|
56
|
+
requester: string;
|
|
57
|
+
name: string;
|
|
58
|
+
props: unknown;
|
|
59
|
+
signal: AbortSignal;
|
|
60
|
+
}>): Promise<unknown>;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/** Session tree 门面 — 形状对齐 agent 包 SessionTreeRuntimeHandle。 */
|
|
56
64
|
export interface ConsoleAgentSessionTree {
|
|
57
65
|
resolveActiveSessionId(sessionKey: string): Promise<string | null>;
|
|
58
|
-
|
|
59
|
-
getBySessionId(sessionId: string): Promise<
|
|
60
|
-
{ active_leaf_message_id?: number | null } | null | undefined
|
|
61
|
-
>;
|
|
62
|
-
};
|
|
66
|
+
getActiveLeafMessageId(sessionId: string): Promise<number | null>;
|
|
63
67
|
listBranchPoints(sessionId: string): Promise<readonly unknown[]>;
|
|
64
68
|
switchActiveLeaf(sessionId: string, messageId: number): Promise<boolean>;
|
|
65
69
|
jumpToBranchIndex(
|
|
@@ -83,6 +87,12 @@ export interface ConsoleRestPagesOptions {
|
|
|
83
87
|
readonly pluginRegistryUrl?: string;
|
|
84
88
|
/** 默认 `https://registry.npmmirror.com`(legacy 同源)。 */
|
|
85
89
|
readonly npmRegistryUrl?: string;
|
|
90
|
+
/** Component preview deadline; default 5 seconds. */
|
|
91
|
+
readonly componentPreviewTimeoutMs?: number;
|
|
92
|
+
/** Maximum serialized Component preview; default 256 KiB. */
|
|
93
|
+
readonly componentPreviewMaxBytes?: number;
|
|
94
|
+
/** Maximum in-flight Component previews per Console route set; default 2. */
|
|
95
|
+
readonly componentPreviewMaxConcurrent?: number;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
type LogModelLike = {
|
|
@@ -109,6 +119,8 @@ type LogRow = {
|
|
|
109
119
|
|
|
110
120
|
const INTROSPECTION_PAGE_SIZES = {
|
|
111
121
|
commands: 25,
|
|
122
|
+
middlewares: 30,
|
|
123
|
+
components: 30,
|
|
112
124
|
tools: 15,
|
|
113
125
|
endpoints: 30,
|
|
114
126
|
bindings: 30,
|
|
@@ -143,7 +155,7 @@ export function registerConsoleRestPages(
|
|
|
143
155
|
|
|
144
156
|
registerLogsRoutes(route, base, ctx);
|
|
145
157
|
registerMarketplaceRoutes(route, base, ctx, fetchFn, pluginRegistryUrl, npmRegistryUrl);
|
|
146
|
-
registerIntrospectionRoutes(route, base, ctx);
|
|
158
|
+
registerIntrospectionRoutes(route, base, ctx, options);
|
|
147
159
|
registerAgentSessionRoutes(route, base, ctx);
|
|
148
160
|
|
|
149
161
|
return () => {
|
|
@@ -551,6 +563,7 @@ function registerIntrospectionRoutes(
|
|
|
551
563
|
route: HttpHost['route'],
|
|
552
564
|
base: string,
|
|
553
565
|
ctx: ConsoleRestCtx,
|
|
566
|
+
options: ConsoleRestPagesOptions,
|
|
554
567
|
): void {
|
|
555
568
|
const collectors: Array<{
|
|
556
569
|
kind: IntrospectionKind;
|
|
@@ -559,13 +572,33 @@ function registerIntrospectionRoutes(
|
|
|
559
572
|
}> = [
|
|
560
573
|
{
|
|
561
574
|
kind: 'commands',
|
|
562
|
-
collect: () => ({ rows:
|
|
575
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.commands?.() ?? []) }),
|
|
563
576
|
fields: [
|
|
564
577
|
(c) => stringProp(c, 'pattern'),
|
|
565
578
|
(c) => stringProp(c, 'desc'),
|
|
566
579
|
(c) => stringProp(c, 'plugin'),
|
|
567
580
|
],
|
|
568
581
|
},
|
|
582
|
+
{
|
|
583
|
+
kind: 'middlewares',
|
|
584
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.middlewares?.() ?? []) }),
|
|
585
|
+
fields: [
|
|
586
|
+
(m) => stringProp(m, 'name'),
|
|
587
|
+
(m) => stringProp(m, 'owner'),
|
|
588
|
+
(m) => stringProp(m, 'phase'),
|
|
589
|
+
(m) => stringProp(m, 'target'),
|
|
590
|
+
(m) => stringProp(m, 'source'),
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
kind: 'components',
|
|
595
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.components?.() ?? []) }),
|
|
596
|
+
fields: [
|
|
597
|
+
(c) => stringProp(c, 'name'),
|
|
598
|
+
(c) => stringProp(c, 'owner'),
|
|
599
|
+
(c) => stringProp(c, 'source'),
|
|
600
|
+
],
|
|
601
|
+
},
|
|
569
602
|
{
|
|
570
603
|
kind: 'endpoints',
|
|
571
604
|
collect: () => ({
|
|
@@ -580,7 +613,7 @@ function registerIntrospectionRoutes(
|
|
|
580
613
|
},
|
|
581
614
|
{
|
|
582
615
|
kind: 'bindings',
|
|
583
|
-
collect: () => ({ rows:
|
|
616
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.bindings?.() ?? []) }),
|
|
584
617
|
fields: [
|
|
585
618
|
(a) => stringProp(a, 'name'),
|
|
586
619
|
(a) => stringProp(a, 'provider'),
|
|
@@ -589,7 +622,7 @@ function registerIntrospectionRoutes(
|
|
|
589
622
|
},
|
|
590
623
|
{
|
|
591
624
|
kind: 'tools',
|
|
592
|
-
collect: () => ({ rows:
|
|
625
|
+
collect: () => ({ rows: withAgentIntrospection(ctx, (value) => value?.tools?.() ?? []) }),
|
|
593
626
|
fields: [
|
|
594
627
|
(t) => stringProp(t, 'name'),
|
|
595
628
|
(t) => stringProp(t, 'source'),
|
|
@@ -599,7 +632,7 @@ function registerIntrospectionRoutes(
|
|
|
599
632
|
{
|
|
600
633
|
kind: 'mcp',
|
|
601
634
|
collect: () => {
|
|
602
|
-
const result =
|
|
635
|
+
const result = withAgentIntrospection(ctx, (value) => value?.mcp?.());
|
|
603
636
|
if (result == null) return { rows: [] as readonly unknown[] };
|
|
604
637
|
// readonly 数组不能用 Array.isArray 收窄,按对象形状判断
|
|
605
638
|
if (typeof result === 'object' && 'rows' in result) {
|
|
@@ -625,7 +658,7 @@ function registerIntrospectionRoutes(
|
|
|
625
658
|
);
|
|
626
659
|
const missing = kind === 'endpoints'
|
|
627
660
|
? !ctx.getEndpoints
|
|
628
|
-
: !
|
|
661
|
+
: !hasAgentIntrospection(ctx);
|
|
629
662
|
const degradedNote = note ?? (missing
|
|
630
663
|
? kind === 'endpoints'
|
|
631
664
|
? 'Endpoints 数据源未接线(ctx.getEndpoints 缺失)'
|
|
@@ -644,18 +677,146 @@ function registerIntrospectionRoutes(
|
|
|
644
677
|
}
|
|
645
678
|
}, { summary: `Introspection: ${kind}`, tags: ['console', 'introspection'] });
|
|
646
679
|
}
|
|
680
|
+
|
|
681
|
+
let activeComponentPreviews = 0;
|
|
682
|
+
route('POST', `${base}/introspection/components/render`, async (request, response, _url, authScope) => {
|
|
683
|
+
if (!requireWriteScope(response, ctx, authScope)) return;
|
|
684
|
+
const body = (await readJsonBody<Record<string, unknown>>(request)) ?? {};
|
|
685
|
+
const requester = typeof body.requester === 'string' ? body.requester.trim() : '';
|
|
686
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
687
|
+
if (!requester || !name || requester.length > 256 || name.length > 160) {
|
|
688
|
+
writeJson(response, 400, { success: false, error: '请提供有效的 requester 与 component name' });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const maxConcurrent = clampPositiveInt(options.componentPreviewMaxConcurrent, 2, 8);
|
|
692
|
+
if (activeComponentPreviews >= maxConcurrent) {
|
|
693
|
+
writeJson(response, 503, { success: false, error: 'Component preview 正忙,请稍后重试' });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const lease = ctx.acquireAgentRuntime?.();
|
|
697
|
+
const render = lease?.value.introspection?.renderComponent;
|
|
698
|
+
if (!lease || !render) {
|
|
699
|
+
lease?.release();
|
|
700
|
+
writeJson(response, 503, { success: false, error: 'Component Runtime 未就绪' });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const controller = new AbortController();
|
|
704
|
+
const timeoutMs = clampPositiveInt(options.componentPreviewTimeoutMs, 5_000, 30_000);
|
|
705
|
+
const timeout = setTimeout(() => controller.abort(
|
|
706
|
+
new ComponentPreviewTimeoutError(`Component preview 超过 ${timeoutMs}ms`),
|
|
707
|
+
), timeoutMs);
|
|
708
|
+
activeComponentPreviews += 1;
|
|
709
|
+
let released = false;
|
|
710
|
+
const release = () => {
|
|
711
|
+
if (released) return;
|
|
712
|
+
released = true;
|
|
713
|
+
activeComponentPreviews -= 1;
|
|
714
|
+
lease.release();
|
|
715
|
+
};
|
|
716
|
+
const renderPromise = Promise.resolve().then(() =>
|
|
717
|
+
render({ requester, name, props: body.props ?? {}, signal: controller.signal }));
|
|
718
|
+
// A timed-out implementation may ignore AbortSignal. Keep its generation
|
|
719
|
+
// lease until the real operation settles; the bounded concurrency gate
|
|
720
|
+
// prevents non-cooperative previews from pinning unbounded generations.
|
|
721
|
+
void renderPromise.then(release, release);
|
|
722
|
+
try {
|
|
723
|
+
const output = await raceWithAbort(
|
|
724
|
+
renderPromise,
|
|
725
|
+
controller.signal,
|
|
726
|
+
);
|
|
727
|
+
assertPreviewOutput(output, options.componentPreviewMaxBytes ?? 256 * 1024);
|
|
728
|
+
writeJson(response, 200, { success: true, data: { requester, name, output } });
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const status = error instanceof ComponentPreviewTimeoutError
|
|
731
|
+
? 408
|
|
732
|
+
: error instanceof ComponentPreviewLimitError ? 413 : 422;
|
|
733
|
+
writeJson(response, status, {
|
|
734
|
+
success: false,
|
|
735
|
+
error: error instanceof Error ? error.message : String(error),
|
|
736
|
+
});
|
|
737
|
+
} finally {
|
|
738
|
+
clearTimeout(timeout);
|
|
739
|
+
if (!controller.signal.aborted) controller.abort();
|
|
740
|
+
}
|
|
741
|
+
}, { summary: 'Render a Component preview', tags: ['console', 'introspection', 'component'] });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
class ComponentPreviewTimeoutError extends Error {
|
|
745
|
+
override readonly name = 'ComponentPreviewTimeoutError';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
class ComponentPreviewLimitError extends Error {
|
|
749
|
+
override readonly name = 'ComponentPreviewLimitError';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function clampPositiveInt(value: number | undefined, fallback: number, ceiling: number): number {
|
|
753
|
+
if (!Number.isFinite(value) || !value || value < 1) return fallback;
|
|
754
|
+
return Math.min(Math.floor(value), ceiling);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
758
|
+
if (signal.aborted) throw abortReason(signal);
|
|
759
|
+
return await new Promise<T>((resolve, reject) => {
|
|
760
|
+
const onAbort = () => reject(abortReason(signal));
|
|
761
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
762
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function abortReason(signal: AbortSignal): Error {
|
|
767
|
+
return signal.reason instanceof Error
|
|
768
|
+
? signal.reason
|
|
769
|
+
: new DOMException('Component preview aborted', 'AbortError');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function assertPreviewOutput(output: unknown, maxBytes: number): void {
|
|
773
|
+
const byteLimit = clampPositiveInt(maxBytes, 256 * 1024, 1024 * 1024);
|
|
774
|
+
const seen = new WeakSet<object>();
|
|
775
|
+
let nodes = 0;
|
|
776
|
+
const visit = (value: unknown, depth: number): void => {
|
|
777
|
+
nodes += 1;
|
|
778
|
+
if (nodes > 10_000) throw new ComponentPreviewLimitError('Component preview 节点数超过 10000');
|
|
779
|
+
if (depth > 24) throw new ComponentPreviewLimitError('Component preview 嵌套深度超过 24');
|
|
780
|
+
if (!value || typeof value !== 'object') return;
|
|
781
|
+
if (seen.has(value)) throw new ComponentPreviewLimitError('Component preview 不能包含循环引用');
|
|
782
|
+
seen.add(value);
|
|
783
|
+
if (Array.isArray(value)) {
|
|
784
|
+
for (const item of value) visit(item, depth + 1);
|
|
785
|
+
} else {
|
|
786
|
+
for (const item of Object.values(value as Record<string, unknown>)) visit(item, depth + 1);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
visit(output, 0);
|
|
790
|
+
let serialized: string;
|
|
791
|
+
try {
|
|
792
|
+
serialized = JSON.stringify(output);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
throw new ComponentPreviewLimitError(
|
|
795
|
+
`Component preview 无法序列化: ${error instanceof Error ? error.message : String(error)}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (Buffer.byteLength(serialized ?? '') > byteLimit) {
|
|
799
|
+
throw new ComponentPreviewLimitError(`Component preview 超过 ${byteLimit} bytes`);
|
|
800
|
+
}
|
|
647
801
|
}
|
|
648
802
|
|
|
649
|
-
function
|
|
803
|
+
function withAgentIntrospection<T>(
|
|
804
|
+
ctx: ConsoleRestCtx,
|
|
805
|
+
operation: (value: ConsoleAgentIntrospection | undefined) => T,
|
|
806
|
+
): T {
|
|
650
807
|
const lease = ctx.acquireAgentRuntime?.();
|
|
651
|
-
if (!lease) return undefined;
|
|
808
|
+
if (!lease) return operation(undefined);
|
|
652
809
|
try {
|
|
653
|
-
return lease.value.introspection;
|
|
810
|
+
return operation(lease.value.introspection);
|
|
654
811
|
} finally {
|
|
655
812
|
lease.release();
|
|
656
813
|
}
|
|
657
814
|
}
|
|
658
815
|
|
|
816
|
+
function hasAgentIntrospection(ctx: ConsoleRestCtx): boolean {
|
|
817
|
+
return withAgentIntrospection(ctx, (value) => value !== undefined);
|
|
818
|
+
}
|
|
819
|
+
|
|
659
820
|
// ---------------------------------------------------------------------------
|
|
660
821
|
// agent sessions(legacy agent-sessions-rest-api.ts;ADR 0010 D3)
|
|
661
822
|
// ---------------------------------------------------------------------------
|
|
@@ -708,14 +869,14 @@ function registerAgentSessionRoutes(
|
|
|
708
869
|
return;
|
|
709
870
|
}
|
|
710
871
|
|
|
711
|
-
const
|
|
872
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
712
873
|
const points = await runtime.listBranchPoints(sessionId);
|
|
713
874
|
writeJson(response, 200, {
|
|
714
875
|
success: true,
|
|
715
876
|
data: {
|
|
716
877
|
sessionKey: parsed.sessionKey,
|
|
717
878
|
sessionId,
|
|
718
|
-
activeLeafMessageId
|
|
879
|
+
activeLeafMessageId,
|
|
719
880
|
points,
|
|
720
881
|
},
|
|
721
882
|
});
|
|
@@ -782,28 +943,28 @@ function registerAgentSessionRoutes(
|
|
|
782
943
|
|
|
783
944
|
if (branchIndex !== undefined) {
|
|
784
945
|
const result = await runtime.jumpToBranchIndex(sessionId, branchIndex);
|
|
785
|
-
const
|
|
946
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
786
947
|
writeJson(response, result.ok ? 200 : 400, {
|
|
787
948
|
success: result.ok,
|
|
788
949
|
message: result.message,
|
|
789
950
|
data: {
|
|
790
951
|
sessionKey: parsed.sessionKey,
|
|
791
952
|
sessionId,
|
|
792
|
-
activeLeafMessageId
|
|
953
|
+
activeLeafMessageId,
|
|
793
954
|
},
|
|
794
955
|
});
|
|
795
956
|
return;
|
|
796
957
|
}
|
|
797
958
|
|
|
798
959
|
const ok = await runtime.switchActiveLeaf(sessionId, messageId!);
|
|
799
|
-
const
|
|
960
|
+
const activeLeafMessageId = await runtime.getActiveLeafMessageId(sessionId);
|
|
800
961
|
writeJson(response, ok ? 200 : 400, {
|
|
801
962
|
success: ok,
|
|
802
963
|
message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
|
|
803
964
|
data: {
|
|
804
965
|
sessionKey: parsed.sessionKey,
|
|
805
966
|
sessionId,
|
|
806
|
-
activeLeafMessageId
|
|
967
|
+
activeLeafMessageId,
|
|
807
968
|
},
|
|
808
969
|
});
|
|
809
970
|
} finally {
|
|
@@ -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 内
|
|
@@ -39,8 +41,65 @@ export interface ConsoleRpcExtendedCtx {
|
|
|
39
41
|
) => Promise<T | null>;
|
|
40
42
|
/** Plugin Runtime DatabaseHost 的 models 视图。 */
|
|
41
43
|
databaseHost?: { models: { get(name: string): unknown } };
|
|
42
|
-
/**
|
|
44
|
+
/** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
|
|
43
45
|
resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
|
|
46
|
+
/** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
|
|
47
|
+
loginAssist?: {
|
|
48
|
+
listPending(): readonly {
|
|
49
|
+
id: string;
|
|
50
|
+
adapter: string;
|
|
51
|
+
endpointKey: string;
|
|
52
|
+
type: string;
|
|
53
|
+
payload: Record<string, unknown>;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
expiresAt?: number;
|
|
56
|
+
}[];
|
|
57
|
+
submit(id: string, value: string | Record<string, unknown>): boolean;
|
|
58
|
+
cancel(id: string, reason?: string): boolean;
|
|
59
|
+
};
|
|
60
|
+
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
61
|
+
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
62
|
+
/** Authenticated HTTP principal; never accepted from the RPC body. */
|
|
63
|
+
authenticatedPrincipal?: Readonly<{ principalId: string }>;
|
|
64
|
+
workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ConsoleWorkroomProfileControlPort {
|
|
68
|
+
publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
|
|
69
|
+
authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
70
|
+
publishProfile(command: Readonly<{
|
|
71
|
+
operationId: string;
|
|
72
|
+
projectId: string;
|
|
73
|
+
expectedRegistryRevision: number;
|
|
74
|
+
overlay: unknown;
|
|
75
|
+
activate: boolean;
|
|
76
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
77
|
+
publishRollback(command: Readonly<{
|
|
78
|
+
operationId: string;
|
|
79
|
+
projectId: string;
|
|
80
|
+
expectedRegistryRevision: number;
|
|
81
|
+
restoredFromRevisionId: string;
|
|
82
|
+
overlay: unknown;
|
|
83
|
+
activate: boolean;
|
|
84
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
85
|
+
publishPlanningPolicy(command: Readonly<{
|
|
86
|
+
operationId: string;
|
|
87
|
+
projectId: string;
|
|
88
|
+
profileRevisionId: string;
|
|
89
|
+
revision: number;
|
|
90
|
+
expectedPreviousDigest?: string;
|
|
91
|
+
policy: unknown;
|
|
92
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ConsoleWorkroomKnowledgeControlPort {
|
|
96
|
+
read(projectId: string): Promise<unknown>;
|
|
97
|
+
publish(command: Readonly<{
|
|
98
|
+
operationId: string; projectId: string; expectedRevision: number; entries: readonly unknown[];
|
|
99
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
100
|
+
rollback(command: Readonly<{
|
|
101
|
+
operationId: string; projectId: string; expectedRevision: number; restoreRevision: number;
|
|
102
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
export interface ConsoleScheduleEngine {
|
|
@@ -94,6 +153,17 @@ interface EndpointManagementPort {
|
|
|
94
153
|
parent?: { type: string; id: string; name?: string };
|
|
95
154
|
}[]>;
|
|
96
155
|
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
156
|
+
listRequests?(): Promise<readonly {
|
|
157
|
+
platform_request_id: string;
|
|
158
|
+
type: string;
|
|
159
|
+
scene_type?: string | null;
|
|
160
|
+
scene_id: string;
|
|
161
|
+
sub_type?: string | null;
|
|
162
|
+
actor_id: string;
|
|
163
|
+
actor_name?: string | null;
|
|
164
|
+
comment?: string | null;
|
|
165
|
+
created_at: number;
|
|
166
|
+
}[]>;
|
|
97
167
|
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
98
168
|
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
99
169
|
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
@@ -140,6 +210,13 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
140
210
|
case 'request.reject':
|
|
141
211
|
return actOnRequest(type, d, ctx);
|
|
142
212
|
|
|
213
|
+
case 'login.list':
|
|
214
|
+
return listLoginPending(ctx);
|
|
215
|
+
case 'login.submit':
|
|
216
|
+
return submitLogin(d, ctx);
|
|
217
|
+
case 'login.cancel':
|
|
218
|
+
return cancelLogin(d, ctx);
|
|
219
|
+
|
|
143
220
|
case 'request.consumed':
|
|
144
221
|
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
145
222
|
case 'notice.consumed':
|
|
@@ -178,11 +255,132 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
178
255
|
case 'endpoint.delete_friend':
|
|
179
256
|
return deleteFriend(d, ctx);
|
|
180
257
|
|
|
258
|
+
case 'workroom.profile.pack.publish':
|
|
259
|
+
case 'workroom.profile.publish':
|
|
260
|
+
case 'workroom.profile.rollback':
|
|
261
|
+
case 'workroom.profile.policy.publish':
|
|
262
|
+
return mutateWorkroomProfile(type, d, ctx);
|
|
263
|
+
|
|
264
|
+
case 'workroom.knowledge.get':
|
|
265
|
+
case 'workroom.knowledge.publish':
|
|
266
|
+
case 'workroom.knowledge.rollback':
|
|
267
|
+
return mutateWorkroomKnowledge(type, d, ctx);
|
|
268
|
+
|
|
181
269
|
default:
|
|
182
270
|
return undefined;
|
|
183
271
|
}
|
|
184
272
|
}
|
|
185
273
|
|
|
274
|
+
async function mutateWorkroomProfile(
|
|
275
|
+
type: 'workroom.profile.pack.publish' | 'workroom.profile.publish'
|
|
276
|
+
| 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
|
|
277
|
+
data: Record<string, unknown>,
|
|
278
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
279
|
+
): Promise<ExtendedRpcResult> {
|
|
280
|
+
const control = ctx.workroomProfileControl;
|
|
281
|
+
if (!control) return { error: 'Workroom Profile authority control is unavailable' };
|
|
282
|
+
for (const forbidden of [
|
|
283
|
+
'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
|
|
284
|
+
]) {
|
|
285
|
+
if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
|
|
286
|
+
}
|
|
287
|
+
const principal = ctx.authenticatedPrincipal;
|
|
288
|
+
if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
|
|
289
|
+
try {
|
|
290
|
+
const operationId = requiredRpcText(data.operationId, 'operationId');
|
|
291
|
+
if (type === 'workroom.profile.pack.publish') {
|
|
292
|
+
return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
|
|
293
|
+
}
|
|
294
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
295
|
+
if (type === 'workroom.profile.policy.publish') {
|
|
296
|
+
const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
|
|
297
|
+
return { data: await control.publishPlanningPolicy(Object.freeze({
|
|
298
|
+
operationId,
|
|
299
|
+
projectId,
|
|
300
|
+
profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
|
|
301
|
+
revision: requiredRpcInteger(data.revision, 'revision'),
|
|
302
|
+
...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
|
|
303
|
+
policy: requiredRpcValue(data.policy, 'policy'),
|
|
304
|
+
}), principal) };
|
|
305
|
+
}
|
|
306
|
+
const common = {
|
|
307
|
+
operationId,
|
|
308
|
+
projectId,
|
|
309
|
+
expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
|
|
310
|
+
overlay: requiredRpcValue(data.overlay, 'overlay'),
|
|
311
|
+
activate: requiredRpcBoolean(data.activate, 'activate'),
|
|
312
|
+
};
|
|
313
|
+
return type === 'workroom.profile.rollback'
|
|
314
|
+
? { data: await control.publishRollback(Object.freeze({
|
|
315
|
+
...common,
|
|
316
|
+
restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
|
|
317
|
+
}), principal) }
|
|
318
|
+
: { data: await control.publishProfile(Object.freeze(common), principal) };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return { error: errorMessage(error) };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function mutateWorkroomKnowledge(
|
|
325
|
+
type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
|
|
326
|
+
data: Record<string, unknown>,
|
|
327
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
328
|
+
): Promise<ExtendedRpcResult> {
|
|
329
|
+
const control = ctx.workroomKnowledgeControl;
|
|
330
|
+
if (!control) return { error: 'Workroom Knowledge control is unavailable' };
|
|
331
|
+
for (const forbidden of [
|
|
332
|
+
'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
|
|
333
|
+
'governance', 'approval', 'authority', 'body', 'content',
|
|
334
|
+
]) {
|
|
335
|
+
if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
339
|
+
if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
|
|
340
|
+
const principal = ctx.authenticatedPrincipal;
|
|
341
|
+
if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
|
|
342
|
+
const common = {
|
|
343
|
+
operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
|
|
344
|
+
expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
|
|
345
|
+
};
|
|
346
|
+
if (type === 'workroom.knowledge.rollback') {
|
|
347
|
+
return { data: await control.rollback(Object.freeze({
|
|
348
|
+
...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
|
|
349
|
+
}), principal) };
|
|
350
|
+
}
|
|
351
|
+
if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
|
|
352
|
+
return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return { error: errorMessage(error) };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function requiredRpcValue(value: unknown, name: string): unknown {
|
|
359
|
+
if (value === undefined || value === null) throw new Error(`${name} is required`);
|
|
360
|
+
return structuredClone(value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function requiredRpcText(value: unknown, name: string): string {
|
|
364
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
365
|
+
throw new Error(`${name} is invalid`);
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function optionalRpcText(value: unknown, name: string): string | undefined {
|
|
371
|
+
return value === undefined ? undefined : requiredRpcText(value, name);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function requiredRpcInteger(value: unknown, name: string, minimum = 1): number {
|
|
375
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) throw new Error(`${name} is invalid`);
|
|
376
|
+
return Number(value);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function requiredRpcBoolean(value: unknown, name: string): boolean {
|
|
380
|
+
if (typeof value !== 'boolean') throw new Error(`${name} is invalid`);
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
|
|
186
384
|
// ---------------------------------------------------------------- cron
|
|
187
385
|
|
|
188
386
|
function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
|
|
@@ -359,7 +557,7 @@ async function listInbox(
|
|
|
359
557
|
return { data: { [key]: sorted, inboxEnabled: enabled } };
|
|
360
558
|
}
|
|
361
559
|
|
|
362
|
-
/** request.list ——
|
|
560
|
+
/** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
|
|
363
561
|
async function listPendingRequests(
|
|
364
562
|
d: Record<string, unknown>,
|
|
365
563
|
ctx: ConsoleRpcExtendedCtx,
|
|
@@ -367,6 +565,32 @@ async function listPendingRequests(
|
|
|
367
565
|
const adapter = strField(d, '$adapter', 'adapter');
|
|
368
566
|
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
369
567
|
if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
|
|
568
|
+
|
|
569
|
+
if (ctx.withEndpointManagement) {
|
|
570
|
+
try {
|
|
571
|
+
const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
|
|
572
|
+
if (!management.listRequests) return undefined;
|
|
573
|
+
const pending = await management.listRequests();
|
|
574
|
+
const requests = pending.map((row) => mapRequestRow({
|
|
575
|
+
platform_request_id: row.platform_request_id,
|
|
576
|
+
type: row.type,
|
|
577
|
+
scene_type: row.scene_type ?? null,
|
|
578
|
+
scene_id: row.scene_id,
|
|
579
|
+
sub_type: row.sub_type ?? null,
|
|
580
|
+
actor_id: row.actor_id,
|
|
581
|
+
actor_name: row.actor_name ?? null,
|
|
582
|
+
comment: row.comment ?? null,
|
|
583
|
+
created_at: row.created_at,
|
|
584
|
+
resolved: 0,
|
|
585
|
+
}));
|
|
586
|
+
return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
|
|
587
|
+
});
|
|
588
|
+
if (live) return live;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
return { error: errorMessage(error) };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
370
594
|
const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
|
|
371
595
|
adapter,
|
|
372
596
|
endpoint_id: endpointKey,
|
|
@@ -377,7 +601,7 @@ async function listPendingRequests(
|
|
|
377
601
|
.filter((row) => Number(row.resolved ?? 0) === 0)
|
|
378
602
|
.sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
|
|
379
603
|
.map(mapRequestRow);
|
|
380
|
-
return { data: { requests, inboxEnabled: enabled } };
|
|
604
|
+
return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
|
|
381
605
|
}
|
|
382
606
|
|
|
383
607
|
async function listInboxMessages(
|
|
@@ -546,6 +770,52 @@ async function actOnRequest(
|
|
|
546
770
|
});
|
|
547
771
|
}
|
|
548
772
|
|
|
773
|
+
function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
|
|
774
|
+
const assist = ctx.loginAssist;
|
|
775
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
776
|
+
const tasks = assist.listPending().map((task) => ({
|
|
777
|
+
id: task.id,
|
|
778
|
+
adapter: task.adapter,
|
|
779
|
+
endpointKey: task.endpointKey,
|
|
780
|
+
type: task.type,
|
|
781
|
+
payload: task.payload,
|
|
782
|
+
createdAt: task.createdAt,
|
|
783
|
+
expiresAt: task.expiresAt,
|
|
784
|
+
}));
|
|
785
|
+
return { data: { tasks, count: tasks.length } };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function submitLogin(
|
|
789
|
+
d: Record<string, unknown>,
|
|
790
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
791
|
+
): ExtendedRpcResult {
|
|
792
|
+
const assist = ctx.loginAssist;
|
|
793
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
794
|
+
const id = strField(d, '$id', 'id', 'taskId');
|
|
795
|
+
if (!id) return { error: '$id required' };
|
|
796
|
+
const raw = d.$value ?? d.value ?? d.ticket ?? d.code ?? '';
|
|
797
|
+
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
798
|
+
? raw as string | Record<string, unknown>
|
|
799
|
+
: String(raw ?? '');
|
|
800
|
+
const ok = assist.submit(id, value);
|
|
801
|
+
if (!ok) return { error: `login task not found: ${id}` };
|
|
802
|
+
return { data: { success: true } };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function cancelLogin(
|
|
806
|
+
d: Record<string, unknown>,
|
|
807
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
808
|
+
): ExtendedRpcResult {
|
|
809
|
+
const assist = ctx.loginAssist;
|
|
810
|
+
if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
811
|
+
const id = strField(d, '$id', 'id', 'taskId');
|
|
812
|
+
if (!id) return { error: '$id required' };
|
|
813
|
+
const reason = strField(d, '$reason', 'reason') || 'cancelled';
|
|
814
|
+
const ok = assist.cancel(id, reason);
|
|
815
|
+
if (!ok) return { error: `login task not found: ${id}` };
|
|
816
|
+
return { data: { success: true } };
|
|
817
|
+
}
|
|
818
|
+
|
|
549
819
|
// ---------------------------------------------------------------- 社交读取
|
|
550
820
|
|
|
551
821
|
async function listFriends(
|