@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.
@@ -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
- agentSessionStore: {
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: agentIntrospection(ctx)?.commands?.() ?? [] }),
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: agentIntrospection(ctx)?.bindings?.() ?? [] }),
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: agentIntrospection(ctx)?.tools?.() ?? [] }),
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 = agentIntrospection(ctx)?.mcp?.();
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
- : !agentIntrospection(ctx);
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
- function agentIntrospection(ctx) {
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 session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
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 session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
750
+ activeLeafMessageId,
605
751
  },
606
752
  });
607
753
  return;
608
754
  }
609
755
  const ok = await runtime.switchActiveLeaf(sessionId, messageId);
610
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
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: session?.active_leaf_message_id ?? null,
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
- * - 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 内
@@ -28,8 +30,85 @@ export interface ConsoleRpcExtendedCtx {
28
30
  get(name: string): unknown;
29
31
  };
30
32
  };
31
- /** Agent Host 持久化调度引擎(@zhin.js/agent getAssistantRuntime().engine),未 init 时返回 null。 */
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>;