@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.
@@ -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
- agentSessionStore: {
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: agentIntrospection(ctx)?.commands?.() ?? [] }),
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: agentIntrospection(ctx)?.bindings?.() ?? [] }),
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: agentIntrospection(ctx)?.tools?.() ?? [] }),
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 = agentIntrospection(ctx)?.mcp?.();
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
- : !agentIntrospection(ctx);
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 agentIntrospection(ctx: ConsoleRestCtx): ConsoleAgentIntrospection | undefined {
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 session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
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 session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
953
+ activeLeafMessageId,
793
954
  },
794
955
  });
795
956
  return;
796
957
  }
797
958
 
798
959
  const ok = await runtime.switchActiveLeaf(sessionId, messageId!);
799
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
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
- * - inbox:`ctx.databaseHost.models`(unified_inbox_message/request/notice 三张表,
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
- /** Agent Host 持久化调度引擎(@zhin.js/agent getAssistantRuntime().engine),未 init 时返回 null。 */
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 —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
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(