@zhin.js/host-http 1.0.5 → 1.0.8

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.
@@ -14,7 +14,10 @@ export interface ConsoleRestCtx {
14
14
  * 可选:返回新 Runtime 的 agent 门面(见 {@link ConsoleAgentRuntime})。
15
15
  * 未接线时 introspection 降级为空列表 + note,session tree 返回 503。
16
16
  */
17
- readonly getAgentRuntime?: () => unknown;
17
+ readonly acquireAgentRuntime?: () => {
18
+ readonly value: ConsoleAgentRuntime;
19
+ release(): void;
20
+ } | null;
18
21
  /** Database host(logs 页数据源:SystemLog 模型)。 */
19
22
  readonly databaseHost?: {
20
23
  readonly dialect: string;
@@ -49,7 +52,7 @@ export interface ConsoleAgentSessionTree {
49
52
  message: string;
50
53
  }>;
51
54
  }
52
- /** `ctx.getAgentRuntime()` 返回值的最小结构约定(全部可选,逐项降级)。 */
55
+ /** `ctx.acquireAgentRuntime()` 返回值的最小结构约定(全部可选,逐项降级)。 */
53
56
  export interface ConsoleAgentRuntime {
54
57
  readonly introspection?: ConsoleAgentIntrospection;
55
58
  readonly sessionTree?: ConsoleAgentSessionTree;
@@ -448,7 +448,7 @@ function registerIntrospectionRoutes(route, base, ctx) {
448
448
  const degradedNote = note ?? (missing
449
449
  ? kind === 'endpoints'
450
450
  ? 'Endpoints 数据源未接线(ctx.getEndpoints 缺失)'
451
- : 'Agent runtime 未装配(basic/cli 未接线 getAgentRuntime)'
451
+ : 'Agent runtime 未装配(basic/cli 未接线 acquireAgentRuntime)'
452
452
  : undefined);
453
453
  writeJson(response, 200, {
454
454
  success: true,
@@ -466,27 +466,34 @@ function registerIntrospectionRoutes(route, base, ctx) {
466
466
  }
467
467
  }
468
468
  function agentIntrospection(ctx) {
469
- const runtime = ctx.getAgentRuntime?.();
470
- return runtime && typeof runtime === 'object' ? runtime.introspection : undefined;
469
+ const lease = ctx.acquireAgentRuntime?.();
470
+ if (!lease)
471
+ return undefined;
472
+ try {
473
+ return lease.value.introspection;
474
+ }
475
+ finally {
476
+ lease.release();
477
+ }
471
478
  }
472
479
  // ---------------------------------------------------------------------------
473
480
  // agent sessions(legacy agent-sessions-rest-api.ts;ADR 0010 D3)
474
481
  // ---------------------------------------------------------------------------
475
482
  function registerAgentSessionRoutes(route, base, ctx) {
476
483
  const prefix = `${base}/agent/sessions/`;
477
- const getSessionTree = () => {
478
- const runtime = ctx.getAgentRuntime?.();
479
- const tree = runtime && typeof runtime === 'object' ? runtime.sessionTree : undefined;
480
- if (!tree)
484
+ const acquireSessionTree = () => {
485
+ const lease = ctx.acquireAgentRuntime?.();
486
+ const tree = lease?.value.sessionTree;
487
+ if (!lease || !tree || typeof tree.resolveActiveSessionId !== 'function') {
488
+ lease?.release();
481
489
  return null;
482
- if (typeof tree.resolveActiveSessionId !== 'function')
483
- return null;
484
- return tree;
490
+ }
491
+ return { tree, release: () => lease.release() };
485
492
  };
486
493
  const unavailable = (response) => {
487
494
  writeJson(response, 503, {
488
495
  success: false,
489
- error: 'Agent session tree runtime 未就绪(新 Runtime 需经 getAgentRuntime 装配 sessionTree)',
496
+ error: 'Agent session tree runtime 未就绪(新 Runtime 需经 acquireAgentRuntime 装配 sessionTree)',
490
497
  });
491
498
  };
492
499
  // HttpHost 无 `:param` 路由,用前缀路由匹配 `<sessionKey>/tree|leaf`。
@@ -496,30 +503,36 @@ function registerAgentSessionRoutes(route, base, ctx) {
496
503
  writeJson(response, 404, { success: false, error: 'Not found' });
497
504
  return;
498
505
  }
499
- const runtime = getSessionTree();
500
- if (!runtime) {
506
+ const lease = acquireSessionTree();
507
+ if (!lease) {
501
508
  unavailable(response);
502
509
  return;
503
510
  }
504
- const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
505
- if (!sessionId) {
506
- writeJson(response, 404, {
507
- success: false,
508
- error: `未找到活跃会话:${parsed.sessionKey}`,
511
+ const runtime = lease.tree;
512
+ try {
513
+ const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
514
+ if (!sessionId) {
515
+ writeJson(response, 404, {
516
+ success: false,
517
+ error: `未找到活跃会话:${parsed.sessionKey}`,
518
+ });
519
+ return;
520
+ }
521
+ const session = await runtime.agentSessionStore.getBySessionId(sessionId);
522
+ const points = await runtime.listBranchPoints(sessionId);
523
+ writeJson(response, 200, {
524
+ success: true,
525
+ data: {
526
+ sessionKey: parsed.sessionKey,
527
+ sessionId,
528
+ activeLeafMessageId: session?.active_leaf_message_id ?? null,
529
+ points,
530
+ },
509
531
  });
510
- return;
511
532
  }
512
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
513
- const points = await runtime.listBranchPoints(sessionId);
514
- writeJson(response, 200, {
515
- success: true,
516
- data: {
517
- sessionKey: parsed.sessionKey,
518
- sessionId,
519
- activeLeafMessageId: session?.active_leaf_message_id ?? null,
520
- points,
521
- },
522
- });
533
+ finally {
534
+ lease.release();
535
+ }
523
536
  }, { summary: 'Get agent session tree', tags: ['console', 'agent'] });
524
537
  route('POST', `${base}/agent/sessions/*`, async (request, response, url, authScope) => {
525
538
  const parsed = parseSessionAction(url.pathname, prefix, 'leaf');
@@ -529,19 +542,6 @@ function registerAgentSessionRoutes(route, base, ctx) {
529
542
  }
530
543
  if (!requireWriteScope(response, ctx, authScope))
531
544
  return;
532
- const runtime = getSessionTree();
533
- if (!runtime) {
534
- unavailable(response);
535
- return;
536
- }
537
- const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
538
- if (!sessionId) {
539
- writeJson(response, 404, {
540
- success: false,
541
- error: `未找到活跃会话:${parsed.sessionKey}`,
542
- });
543
- return;
544
- }
545
545
  let body;
546
546
  try {
547
547
  body = (await readJsonBody(request) ?? {});
@@ -556,48 +556,71 @@ function registerAgentSessionRoutes(route, base, ctx) {
556
556
  const messageIdRaw = body.messageId;
557
557
  const indexRaw = body.index;
558
558
  let messageId;
559
+ let branchIndex;
559
560
  if (messageIdRaw != null && messageIdRaw !== '') {
560
- const n = Number(messageIdRaw);
561
- if (!Number.isFinite(n) || n < 1) {
561
+ const value = Number(messageIdRaw);
562
+ if (!Number.isFinite(value) || value < 1) {
562
563
  writeJson(response, 400, { success: false, error: 'messageId 须为正整数' });
563
564
  return;
564
565
  }
565
- messageId = n;
566
+ messageId = value;
566
567
  }
567
568
  else if (indexRaw != null && indexRaw !== '') {
568
- const index = Number(indexRaw);
569
- if (!Number.isFinite(index) || index < 1) {
569
+ const value = Number(indexRaw);
570
+ if (!Number.isFinite(value) || value < 1) {
570
571
  writeJson(response, 400, { success: false, error: 'index 须为正整数' });
571
572
  return;
572
573
  }
573
- const result = await runtime.jumpToBranchIndex(sessionId, index);
574
+ branchIndex = value;
575
+ }
576
+ else {
577
+ writeJson(response, 400, { success: false, error: '需要 messageId 或 index 之一' });
578
+ return;
579
+ }
580
+ const lease = acquireSessionTree();
581
+ if (!lease) {
582
+ unavailable(response);
583
+ return;
584
+ }
585
+ const runtime = lease.tree;
586
+ try {
587
+ const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
588
+ if (!sessionId) {
589
+ writeJson(response, 404, {
590
+ success: false,
591
+ error: `未找到活跃会话:${parsed.sessionKey}`,
592
+ });
593
+ return;
594
+ }
595
+ if (branchIndex !== undefined) {
596
+ const result = await runtime.jumpToBranchIndex(sessionId, branchIndex);
597
+ const session = await runtime.agentSessionStore.getBySessionId(sessionId);
598
+ writeJson(response, result.ok ? 200 : 400, {
599
+ success: result.ok,
600
+ message: result.message,
601
+ data: {
602
+ sessionKey: parsed.sessionKey,
603
+ sessionId,
604
+ activeLeafMessageId: session?.active_leaf_message_id ?? null,
605
+ },
606
+ });
607
+ return;
608
+ }
609
+ const ok = await runtime.switchActiveLeaf(sessionId, messageId);
574
610
  const session = await runtime.agentSessionStore.getBySessionId(sessionId);
575
- writeJson(response, result.ok ? 200 : 400, {
576
- success: result.ok,
577
- message: result.message,
611
+ writeJson(response, ok ? 200 : 400, {
612
+ success: ok,
613
+ message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
578
614
  data: {
579
615
  sessionKey: parsed.sessionKey,
580
616
  sessionId,
581
617
  activeLeafMessageId: session?.active_leaf_message_id ?? null,
582
618
  },
583
619
  });
584
- return;
585
620
  }
586
- else {
587
- writeJson(response, 400, { success: false, error: '需要 messageId 或 index 之一' });
588
- return;
621
+ finally {
622
+ lease.release();
589
623
  }
590
- const ok = await runtime.switchActiveLeaf(sessionId, messageId);
591
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
592
- writeJson(response, ok ? 200 : 400, {
593
- success: ok,
594
- message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
595
- data: {
596
- sessionKey: parsed.sessionKey,
597
- sessionId,
598
- activeLeafMessageId: session?.active_leaf_message_id ?? null,
599
- },
600
- });
601
624
  }, { summary: 'Switch agent session active leaf', tags: ['console', 'agent'] });
602
625
  }
603
626
  function parseSessionAction(pathname, prefix, action) {
@@ -21,9 +21,9 @@ export interface ConsoleRpcExtendedCtx {
21
21
  /** Plugin Runtime ScheduleHost(basic/cli schedule-host-installer 提供)。 */
22
22
  scheduleHost?: unknown;
23
23
  /** 经 ImRuntime / AdapterEndpointIndex 解析管理语义端口。 */
24
- resolveEndpointManagement?: (adapter: string, endpointId: string) => EndpointManagementPort | null | undefined;
24
+ resolveEndpointManagement?: (adapter: string, endpointKey: string) => EndpointManagementPort | null | undefined;
25
25
  /** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
26
- resolveEndpoint?: (adapter: string, endpointId: string) => unknown;
26
+ resolveEndpoint?: (adapter: string, endpointKey: string) => unknown;
27
27
  /** Plugin Runtime DatabaseHost 的 models 视图。 */
28
28
  databaseHost?: {
29
29
  models: {
@@ -225,14 +225,14 @@ async function readInboxRows(ctx, table, where, options) {
225
225
  }
226
226
  async function listInbox(d, ctx, table, key, mapRow) {
227
227
  const adapter = strField(d, '$adapter', 'adapter');
228
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
229
- if (!adapter || !endpointId)
228
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
229
+ if (!adapter || !endpointKey)
230
230
  return { error: '$adapter and $endpoint required' };
231
231
  const limit = Math.min(numField(d, 30, '$limit', 'limit'), 100);
232
232
  const offset = Math.max(0, numField(d, 0, '$offset', 'offset'));
233
233
  const { rows, enabled } = await readInboxRows(ctx, table, {
234
234
  adapter,
235
- endpoint_id: endpointId,
235
+ endpoint_id: endpointKey,
236
236
  }, {
237
237
  // sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
238
238
  orderBy: { field: 'created_at', direction: 'DESC' },
@@ -248,12 +248,12 @@ async function listInbox(d, ctx, table, key, mapRow) {
248
248
  /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
249
249
  async function listPendingRequests(d, ctx) {
250
250
  const adapter = strField(d, '$adapter', 'adapter');
251
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
252
- if (!adapter || !endpointId)
251
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
252
+ if (!adapter || !endpointKey)
253
253
  return { error: '$adapter and $endpoint required' };
254
254
  const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
255
255
  adapter,
256
- endpoint_id: endpointId,
256
+ endpoint_id: endpointKey,
257
257
  }, {
258
258
  orderBy: { field: 'created_at', direction: 'ASC' },
259
259
  });
@@ -265,10 +265,10 @@ async function listPendingRequests(d, ctx) {
265
265
  }
266
266
  async function listInboxMessages(d, ctx) {
267
267
  const adapter = strField(d, '$adapter', 'adapter');
268
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
268
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
269
269
  const channelId = strField(d, '$channel_id', 'channelId', 'channel_id');
270
270
  const channelType = strField(d, '$channel_type', 'channelType', 'channel_type');
271
- if (!adapter || !endpointId || !channelId || !channelType) {
271
+ if (!adapter || !endpointKey || !channelId || !channelType) {
272
272
  return { error: '$adapter, $endpoint, $channel_id, $channel_type required' };
273
273
  }
274
274
  const limit = Math.min(numField(d, 50, '$limit', 'limit'), 100);
@@ -277,7 +277,7 @@ async function listInboxMessages(d, ctx) {
277
277
  const parent = normalizeParent(d.$parent ?? d.parent);
278
278
  const where = {
279
279
  adapter,
280
- endpoint_id: endpointId,
280
+ endpoint_id: endpointKey,
281
281
  channel_id: channelId,
282
282
  channel_type: channelType,
283
283
  };
@@ -393,12 +393,12 @@ async function markInboxConsumed(d, ctx, table) {
393
393
  // ---------------------------------------------------------------- 请求审批
394
394
  async function actOnRequest(type, d, ctx) {
395
395
  const adapter = strField(d, '$adapter', 'adapter');
396
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
396
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
397
397
  const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
398
- if (!adapter || !endpointId || !requestId) {
398
+ if (!adapter || !endpointKey || !requestId) {
399
399
  return { error: '$adapter, $endpoint, $id required' };
400
400
  }
401
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
401
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
402
402
  if ('error' in resolved)
403
403
  return resolved;
404
404
  const management = resolved.management;
@@ -472,12 +472,12 @@ async function listChannels(d, ctx) {
472
472
  }
473
473
  async function listGroupMembers(d, ctx) {
474
474
  const adapter = strField(d, '$adapter', 'adapter');
475
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
475
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
476
476
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
477
- if (!adapter || !endpointId || !groupId) {
477
+ if (!adapter || !endpointKey || !groupId) {
478
478
  return { error: '$adapter, $endpoint, $group_id required' };
479
479
  }
480
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
480
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
481
481
  if ('error' in resolved)
482
482
  return resolved;
483
483
  const { management } = resolved;
@@ -495,16 +495,16 @@ async function listGroupMembers(d, ctx) {
495
495
  }
496
496
  async function groupWriteOp(d, ctx, spec) {
497
497
  const adapter = strField(d, '$adapter', 'adapter');
498
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
498
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
499
499
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
500
500
  const userId = strField(d, '$user_id', 'userId', 'user_id');
501
- if (!adapter || !endpointId || !groupId) {
501
+ if (!adapter || !endpointKey || !groupId) {
502
502
  return { error: '$adapter, $endpoint, $group_id required' };
503
503
  }
504
504
  if (spec.requireUser && !userId) {
505
505
  return { error: '$user_id required' };
506
506
  }
507
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
507
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
508
508
  if ('error' in resolved)
509
509
  return resolved;
510
510
  const { management } = resolved;
@@ -530,12 +530,12 @@ async function groupWriteOp(d, ctx, spec) {
530
530
  }
531
531
  async function deleteFriend(d, ctx) {
532
532
  const adapter = strField(d, '$adapter', 'adapter');
533
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
533
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
534
534
  const userId = strField(d, '$user_id', 'userId', 'user_id');
535
- if (!adapter || !endpointId || !userId) {
535
+ if (!adapter || !endpointKey || !userId) {
536
536
  return { error: '$adapter, $endpoint, $user_id required' };
537
537
  }
538
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
538
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
539
539
  if ('error' in resolved)
540
540
  return resolved;
541
541
  const { management } = resolved;
@@ -592,17 +592,17 @@ function numArrayField(d, ...keys) {
592
592
  }
593
593
  return [];
594
594
  }
595
- function resolveLiveEndpoint(ctx, adapter, endpointId) {
595
+ function resolveLiveEndpoint(ctx, adapter, endpointKey) {
596
596
  if (ctx.resolveEndpointManagement) {
597
- const management = ctx.resolveEndpointManagement(adapter, endpointId);
597
+ const management = ctx.resolveEndpointManagement(adapter, endpointKey);
598
598
  if (!management)
599
599
  return { error: 'endpoint not found' };
600
- return { management, adapter, endpointId };
600
+ return { management, adapter, endpointKey };
601
601
  }
602
602
  if (!ctx.resolveEndpoint) {
603
603
  return { error: 'Endpoint registry is not configured' };
604
604
  }
605
- const endpoint = ctx.resolveEndpoint(adapter, endpointId);
605
+ const endpoint = ctx.resolveEndpoint(adapter, endpointKey);
606
606
  if (!endpoint || typeof endpoint !== 'object') {
607
607
  return { error: 'endpoint not found' };
608
608
  }
@@ -612,15 +612,15 @@ function resolveLiveEndpoint(ctx, adapter, endpointId) {
612
612
  ? management
613
613
  : {},
614
614
  adapter,
615
- endpointId,
615
+ endpointKey,
616
616
  };
617
617
  }
618
618
  function requireEndpoint(d, ctx) {
619
619
  const adapter = strField(d, '$adapter', 'adapter');
620
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
621
- if (!adapter || !endpointId)
620
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
621
+ if (!adapter || !endpointKey)
622
622
  return { error: '$adapter and $endpoint required' };
623
- return resolveLiveEndpoint(ctx, adapter, endpointId);
623
+ return resolveLiveEndpoint(ctx, adapter, endpointKey);
624
624
  }
625
625
  function normalizeParent(raw) {
626
626
  if (!raw || typeof raw !== 'object')
@@ -2,6 +2,20 @@ import type { AuthScope } from './token-registry.js';
2
2
  import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
3
3
  import { type ConsoleEndpointSummary } from '@zhin.js/console-protocol';
4
4
  import { type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
5
+ /**
6
+ * Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
7
+ * `Omit<ConversationRef,'endpoint'>`;host/http 架构分层不允许依赖
8
+ * `@zhin.js/im-contract`,故本地结构化声明,endpoint 由 ImRuntime 锚定)。
9
+ */
10
+ export interface ConversationAddress {
11
+ readonly kind: 'private' | 'group' | 'channel';
12
+ readonly id: string;
13
+ readonly parent?: Readonly<{
14
+ readonly kind: 'group' | 'channel';
15
+ readonly id: string;
16
+ }>;
17
+ readonly threadId?: string;
18
+ }
5
19
  export type RuntimeConsoleRpcMessage = {
6
20
  readonly type?: unknown;
7
21
  readonly requestId?: unknown;
@@ -54,7 +68,7 @@ export type RuntimeConsoleRpcContext = {
54
68
  getAllSchemas?(): Promise<Record<string, unknown>>;
55
69
  /** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
56
70
  listEndpoints?(): Promise<readonly RuntimeEndpointSummary[]>;
57
- getEndpoint?(adapter: string, endpointId: string): Promise<RuntimeEndpointSummary | null>;
71
+ getEndpoint?(adapter: string, endpointKey: string): Promise<RuntimeEndpointSummary | null>;
58
72
  sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{
59
73
  messageId: string;
60
74
  }>;
@@ -82,15 +96,9 @@ export type RuntimeDatabaseInfo = {
82
96
  };
83
97
  export type RuntimeEndpointSendInput = {
84
98
  readonly adapter: string;
85
- readonly endpointId: string;
86
- readonly channelId: string;
87
- readonly channelType: string;
99
+ readonly endpointKey: string;
100
+ readonly conversation: ConversationAddress;
88
101
  readonly content: unknown;
89
- readonly parent?: {
90
- type?: string;
91
- id?: string;
92
- name?: string;
93
- };
94
102
  };
95
103
  export declare function pickRpcReply(message: RuntimeConsoleRpcMessage, payloads: readonly RuntimeConsoleRpcReply[]): RuntimeConsoleRpcReply | null;
96
104
  export declare function dispatchRuntimeConsoleRpc(message: RuntimeConsoleRpcMessage, ctx: RuntimeConsoleRpcContext): Promise<RuntimeConsoleRpcReply[]>;
@@ -1,5 +1,22 @@
1
1
  import { assertDemoConsoleRpcAllowed, endpointSendResult, normalizeConsoleRpcMessage, } from '@zhin.js/console-protocol';
2
2
  import { dispatchExtendedConsoleRpc } from './console-rpc-extended.js';
3
+ /** `$channel_type`/`$channel_id`/`$parent` wire 字段 → 会话地址(RPC 边界组帧)。 */
4
+ function wireConversation(channelType, channelId, parent) {
5
+ // console 旧 wire 允许 channel_id 自带场景前缀(`group:123`),解析结果优先
6
+ const prefixed = /^(private|group|channel|direct|c2c|temp):(.+)$/iu.exec(channelId);
7
+ const rawKind = channelType || prefixed?.[1] || 'private';
8
+ const kind = rawKind === 'group' || rawKind === 'channel' ? rawKind : 'private';
9
+ const wire = parent && typeof parent === 'object'
10
+ ? parent
11
+ : undefined;
12
+ const parentKind = wire?.type === 'group' || wire?.type === 'channel' ? wire.type : undefined;
13
+ const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
14
+ return {
15
+ kind,
16
+ id: prefixed ? prefixed[2] : channelId,
17
+ ...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
18
+ };
19
+ }
3
20
  export function pickRpcReply(message, payloads) {
4
21
  const requestId = message.requestId;
5
22
  return ((requestId != null ? payloads.find((payload) => payload.requestId === requestId) : undefined)
@@ -525,8 +542,8 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
525
542
  try {
526
543
  const data = message;
527
544
  const adapter = String(data.$adapter ?? '');
528
- const endpointId = String(data.$endpoint ?? '');
529
- if (!adapter || !endpointId) {
545
+ const endpointKey = String(data.$endpoint ?? '');
546
+ if (!adapter || !endpointKey) {
530
547
  emit({ requestId, error: '$adapter and $endpoint required' });
531
548
  return payloads;
532
549
  }
@@ -534,7 +551,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
534
551
  emit({ requestId, error: 'Endpoint registry is not configured' });
535
552
  return payloads;
536
553
  }
537
- const endpoint = await ctx.getEndpoint(adapter, endpointId);
554
+ const endpoint = await ctx.getEndpoint(adapter, endpointKey);
538
555
  if (!endpoint) {
539
556
  emit({ requestId, error: 'endpoint not found' });
540
557
  return payloads;
@@ -553,13 +570,12 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
553
570
  try {
554
571
  const data = message;
555
572
  const adapter = String(data.$adapter ?? '');
556
- const endpointId = String(data.$endpoint ?? '');
573
+ const endpointKey = String(data.$endpoint ?? '');
557
574
  const channelId = String(data.$channel_id ?? '');
558
575
  const channelType = String(data.$channel_type ?? '');
559
576
  const content = data.$content;
560
- // channelType is optional: ImRuntime tolerates an empty value and
561
- // targets the bare channel id (parity with the legacy RPC handler).
562
- if (!adapter || !endpointId || !channelId || content === undefined) {
577
+ // channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
578
+ if (!adapter || !endpointKey || !channelId || content === undefined) {
563
579
  emit({
564
580
  requestId,
565
581
  error: '$adapter, $endpoint, $channel_id, $content required',
@@ -572,11 +588,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
572
588
  }
573
589
  const result = await ctx.sendEndpointMessage({
574
590
  adapter,
575
- endpointId,
576
- channelId,
577
- channelType,
591
+ endpointKey,
592
+ conversation: wireConversation(channelType, channelId, data.$parent),
578
593
  content,
579
- parent: data.$parent,
580
594
  });
581
595
  // Legacy contract: { message_id }. Keep messageId for new callers.
582
596
  emit({ requestId, data: endpointSendResult(result.messageId) });
package/lib/http-host.js CHANGED
@@ -6,7 +6,7 @@ import { TokenRegistry, extractBearerToken, isDemoWebSocketPath, } from './token
6
6
  import { buildOpenApiDocument, } from './openapi.js';
7
7
  import { isDemoHttpAllowed } from './console-rpc.js';
8
8
  import { HttpBodyError } from './json-body.js';
9
- const logger = getLogger('HttpHost');
9
+ const logger = getLogger('http');
10
10
  export const REMOTE_CONSOLE_ORIGIN = 'https://console.zhin.dev';
11
11
  const DEFAULT_CORS_ORIGINS = Object.freeze([REMOTE_CONSOLE_ORIGIN]);
12
12
  export const httpHostToken = createToken('zhin.host.http');
@@ -290,15 +290,13 @@ export function createHttpHost(options = {}) {
290
290
  const listenPort = typeof bound === 'object' && bound ? bound.port : port;
291
291
  const publicHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
292
292
  address = Object.freeze({ host: publicHost, port: listenPort });
293
- logger.info(formatCompact({
294
- http: `http://${publicHost}:${listenPort}`,
295
- token: tokenRegistry.hasAnyToken()
293
+ logger.info(`listening http://${publicHost}:${listenPort}`
294
+ + ` | routes: ${httpRoutes.length}`
295
+ + ` | token: ${tokenRegistry.hasAnyToken()
296
296
  ? `${tokenRegistry.primaryTokenPrefixForLog()}…`
297
- : '(none)',
298
- cors: corsOrigins.join(','),
299
- ws_routes: [...wsRoutes.keys()].join(',') || '(none)',
300
- http_routes: httpRoutes.length,
301
- }));
297
+ : '(none)'}`);
298
+ logger.debug(`listen detail | cors: ${corsOrigins.join(',')}`
299
+ + ` | ws: ${[...wsRoutes.keys()].join(',') || '(none)'}`);
302
300
  resolve(address);
303
301
  });
304
302
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,10 +18,10 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "ws": "^8.21.1",
21
- "@zhin.js/console-protocol": "1.1.0",
22
- "@zhin.js/host-http-contract": "1.0.0",
23
- "@zhin.js/logger": "1.0.75",
24
- "@zhin.js/plugin-runtime": "1.1.2"
21
+ "@zhin.js/console-protocol": "1.1.1",
22
+ "@zhin.js/host-http-contract": "1.0.1",
23
+ "@zhin.js/logger": "1.0.76",
24
+ "@zhin.js/plugin-runtime": "1.1.5"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",
@@ -32,7 +32,10 @@ export interface ConsoleRestCtx {
32
32
  * 可选:返回新 Runtime 的 agent 门面(见 {@link ConsoleAgentRuntime})。
33
33
  * 未接线时 introspection 降级为空列表 + note,session tree 返回 503。
34
34
  */
35
- readonly getAgentRuntime?: () => unknown;
35
+ readonly acquireAgentRuntime?: () => {
36
+ readonly value: ConsoleAgentRuntime;
37
+ release(): void;
38
+ } | null;
36
39
  /** Database host(logs 页数据源:SystemLog 模型)。 */
37
40
  readonly databaseHost?: {
38
41
  readonly dialect: string;
@@ -65,7 +68,7 @@ export interface ConsoleAgentSessionTree {
65
68
  ): Promise<{ ok: boolean; message: string }>;
66
69
  }
67
70
 
68
- /** `ctx.getAgentRuntime()` 返回值的最小结构约定(全部可选,逐项降级)。 */
71
+ /** `ctx.acquireAgentRuntime()` 返回值的最小结构约定(全部可选,逐项降级)。 */
69
72
  export interface ConsoleAgentRuntime {
70
73
  readonly introspection?: ConsoleAgentIntrospection;
71
74
  readonly sessionTree?: ConsoleAgentSessionTree;
@@ -626,7 +629,7 @@ function registerIntrospectionRoutes(
626
629
  const degradedNote = note ?? (missing
627
630
  ? kind === 'endpoints'
628
631
  ? 'Endpoints 数据源未接线(ctx.getEndpoints 缺失)'
629
- : 'Agent runtime 未装配(basic/cli 未接线 getAgentRuntime)'
632
+ : 'Agent runtime 未装配(basic/cli 未接线 acquireAgentRuntime)'
630
633
  : undefined);
631
634
  writeJson(response, 200, {
632
635
  success: true,
@@ -644,8 +647,13 @@ function registerIntrospectionRoutes(
644
647
  }
645
648
 
646
649
  function agentIntrospection(ctx: ConsoleRestCtx): ConsoleAgentIntrospection | undefined {
647
- const runtime = ctx.getAgentRuntime?.() as ConsoleAgentRuntime | null | undefined;
648
- return runtime && typeof runtime === 'object' ? runtime.introspection : undefined;
650
+ const lease = ctx.acquireAgentRuntime?.();
651
+ if (!lease) return undefined;
652
+ try {
653
+ return lease.value.introspection;
654
+ } finally {
655
+ lease.release();
656
+ }
649
657
  }
650
658
 
651
659
  // ---------------------------------------------------------------------------
@@ -658,17 +666,22 @@ function registerAgentSessionRoutes(
658
666
  ctx: ConsoleRestCtx,
659
667
  ): void {
660
668
  const prefix = `${base}/agent/sessions/`;
661
- const getSessionTree = (): ConsoleAgentSessionTree | null => {
662
- const runtime = ctx.getAgentRuntime?.() as ConsoleAgentRuntime | null | undefined;
663
- const tree = runtime && typeof runtime === 'object' ? runtime.sessionTree : undefined;
664
- if (!tree) return null;
665
- if (typeof tree.resolveActiveSessionId !== 'function') return null;
666
- return tree;
669
+ const acquireSessionTree = (): {
670
+ readonly tree: ConsoleAgentSessionTree;
671
+ release(): void;
672
+ } | null => {
673
+ const lease = ctx.acquireAgentRuntime?.();
674
+ const tree = lease?.value.sessionTree;
675
+ if (!lease || !tree || typeof tree.resolveActiveSessionId !== 'function') {
676
+ lease?.release();
677
+ return null;
678
+ }
679
+ return { tree, release: () => lease.release() };
667
680
  };
668
681
  const unavailable = (response: ServerResponse) => {
669
682
  writeJson(response, 503, {
670
683
  success: false,
671
- error: 'Agent session tree runtime 未就绪(新 Runtime 需经 getAgentRuntime 装配 sessionTree)',
684
+ error: 'Agent session tree runtime 未就绪(新 Runtime 需经 acquireAgentRuntime 装配 sessionTree)',
672
685
  });
673
686
  };
674
687
 
@@ -679,32 +692,36 @@ function registerAgentSessionRoutes(
679
692
  writeJson(response, 404, { success: false, error: 'Not found' });
680
693
  return;
681
694
  }
682
- const runtime = getSessionTree();
683
- if (!runtime) {
695
+ const lease = acquireSessionTree();
696
+ if (!lease) {
684
697
  unavailable(response);
685
698
  return;
686
699
  }
700
+ const runtime = lease.tree;
701
+ try {
702
+ const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
703
+ if (!sessionId) {
704
+ writeJson(response, 404, {
705
+ success: false,
706
+ error: `未找到活跃会话:${parsed.sessionKey}`,
707
+ });
708
+ return;
709
+ }
687
710
 
688
- const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
689
- if (!sessionId) {
690
- writeJson(response, 404, {
691
- success: false,
692
- error: `未找到活跃会话:${parsed.sessionKey}`,
711
+ const session = await runtime.agentSessionStore.getBySessionId(sessionId);
712
+ const points = await runtime.listBranchPoints(sessionId);
713
+ writeJson(response, 200, {
714
+ success: true,
715
+ data: {
716
+ sessionKey: parsed.sessionKey,
717
+ sessionId,
718
+ activeLeafMessageId: session?.active_leaf_message_id ?? null,
719
+ points,
720
+ },
693
721
  });
694
- return;
722
+ } finally {
723
+ lease.release();
695
724
  }
696
-
697
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
698
- const points = await runtime.listBranchPoints(sessionId);
699
- writeJson(response, 200, {
700
- success: true,
701
- data: {
702
- sessionKey: parsed.sessionKey,
703
- sessionId,
704
- activeLeafMessageId: session?.active_leaf_message_id ?? null,
705
- points,
706
- },
707
- });
708
725
  }, { summary: 'Get agent session tree', tags: ['console', 'agent'] });
709
726
 
710
727
  route('POST', `${base}/agent/sessions/*`, async (request, response, url, authScope) => {
@@ -714,21 +731,6 @@ function registerAgentSessionRoutes(
714
731
  return;
715
732
  }
716
733
  if (!requireWriteScope(response, ctx, authScope)) return;
717
- const runtime = getSessionTree();
718
- if (!runtime) {
719
- unavailable(response);
720
- return;
721
- }
722
-
723
- const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
724
- if (!sessionId) {
725
- writeJson(response, 404, {
726
- success: false,
727
- error: `未找到活跃会话:${parsed.sessionKey}`,
728
- });
729
- return;
730
- }
731
-
732
734
  let body: Record<string, unknown>;
733
735
  try {
734
736
  body = (await readJsonBody(request) ?? {}) as Record<string, unknown>;
@@ -741,49 +743,72 @@ function registerAgentSessionRoutes(
741
743
  }
742
744
  const messageIdRaw = body.messageId;
743
745
  const indexRaw = body.index;
744
-
745
746
  let messageId: number | undefined;
747
+ let branchIndex: number | undefined;
746
748
  if (messageIdRaw != null && messageIdRaw !== '') {
747
- const n = Number(messageIdRaw);
748
- if (!Number.isFinite(n) || n < 1) {
749
+ const value = Number(messageIdRaw);
750
+ if (!Number.isFinite(value) || value < 1) {
749
751
  writeJson(response, 400, { success: false, error: 'messageId 须为正整数' });
750
752
  return;
751
753
  }
752
- messageId = n;
754
+ messageId = value;
753
755
  } else if (indexRaw != null && indexRaw !== '') {
754
- const index = Number(indexRaw);
755
- if (!Number.isFinite(index) || index < 1) {
756
+ const value = Number(indexRaw);
757
+ if (!Number.isFinite(value) || value < 1) {
756
758
  writeJson(response, 400, { success: false, error: 'index 须为正整数' });
757
759
  return;
758
760
  }
759
- const result = await runtime.jumpToBranchIndex(sessionId, index);
761
+ branchIndex = value;
762
+ } else {
763
+ writeJson(response, 400, { success: false, error: '需要 messageId 或 index 之一' });
764
+ return;
765
+ }
766
+
767
+ const lease = acquireSessionTree();
768
+ if (!lease) {
769
+ unavailable(response);
770
+ return;
771
+ }
772
+ const runtime = lease.tree;
773
+ try {
774
+ const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
775
+ if (!sessionId) {
776
+ writeJson(response, 404, {
777
+ success: false,
778
+ error: `未找到活跃会话:${parsed.sessionKey}`,
779
+ });
780
+ return;
781
+ }
782
+
783
+ if (branchIndex !== undefined) {
784
+ const result = await runtime.jumpToBranchIndex(sessionId, branchIndex);
785
+ const session = await runtime.agentSessionStore.getBySessionId(sessionId);
786
+ writeJson(response, result.ok ? 200 : 400, {
787
+ success: result.ok,
788
+ message: result.message,
789
+ data: {
790
+ sessionKey: parsed.sessionKey,
791
+ sessionId,
792
+ activeLeafMessageId: session?.active_leaf_message_id ?? null,
793
+ },
794
+ });
795
+ return;
796
+ }
797
+
798
+ const ok = await runtime.switchActiveLeaf(sessionId, messageId!);
760
799
  const session = await runtime.agentSessionStore.getBySessionId(sessionId);
761
- writeJson(response, result.ok ? 200 : 400, {
762
- success: result.ok,
763
- message: result.message,
800
+ writeJson(response, ok ? 200 : 400, {
801
+ success: ok,
802
+ message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
764
803
  data: {
765
804
  sessionKey: parsed.sessionKey,
766
805
  sessionId,
767
806
  activeLeafMessageId: session?.active_leaf_message_id ?? null,
768
807
  },
769
808
  });
770
- return;
771
- } else {
772
- writeJson(response, 400, { success: false, error: '需要 messageId 或 index 之一' });
773
- return;
809
+ } finally {
810
+ lease.release();
774
811
  }
775
-
776
- const ok = await runtime.switchActiveLeaf(sessionId, messageId);
777
- const session = await runtime.agentSessionStore.getBySessionId(sessionId);
778
- writeJson(response, ok ? 200 : 400, {
779
- success: ok,
780
- message: ok ? `已切换 active leaf 至消息 #${messageId}` : '切换失败',
781
- data: {
782
- sessionKey: parsed.sessionKey,
783
- sessionId,
784
- activeLeafMessageId: session?.active_leaf_message_id ?? null,
785
- },
786
- });
787
812
  }, { summary: 'Switch agent session active leaf', tags: ['console', 'agent'] });
788
813
  }
789
814
 
@@ -34,10 +34,10 @@ export interface ConsoleRpcExtendedCtx {
34
34
  /** 经 ImRuntime / AdapterEndpointIndex 解析管理语义端口。 */
35
35
  resolveEndpointManagement?: (
36
36
  adapter: string,
37
- endpointId: string,
37
+ endpointKey: string,
38
38
  ) => EndpointManagementPort | null | undefined;
39
39
  /** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
40
- resolveEndpoint?: (adapter: string, endpointId: string) => unknown;
40
+ resolveEndpoint?: (adapter: string, endpointKey: string) => unknown;
41
41
  /** Plugin Runtime DatabaseHost 的 models 视图。 */
42
42
  databaseHost?: { models: { get(name: string): unknown } };
43
43
  /** Agent Host 持久化调度引擎(@zhin.js/agent getAssistantRuntime().engine),未 init 时返回 null。 */
@@ -340,13 +340,13 @@ async function listInbox(
340
340
  mapRow: (row: Record<string, unknown>) => Record<string, unknown>,
341
341
  ): Promise<ExtendedRpcResult> {
342
342
  const adapter = strField(d, '$adapter', 'adapter');
343
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
344
- if (!adapter || !endpointId) return { error: '$adapter and $endpoint required' };
343
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
344
+ if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
345
345
  const limit = Math.min(numField(d, 30, '$limit', 'limit'), 100);
346
346
  const offset = Math.max(0, numField(d, 0, '$offset', 'offset'));
347
347
  const { rows, enabled } = await readInboxRows(ctx, table, {
348
348
  adapter,
349
- endpoint_id: endpointId,
349
+ endpoint_id: endpointKey,
350
350
  }, {
351
351
  // sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
352
352
  orderBy: { field: 'created_at', direction: 'DESC' },
@@ -366,11 +366,11 @@ async function listPendingRequests(
366
366
  ctx: ConsoleRpcExtendedCtx,
367
367
  ): Promise<ExtendedRpcResult> {
368
368
  const adapter = strField(d, '$adapter', 'adapter');
369
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
370
- if (!adapter || !endpointId) return { error: '$adapter and $endpoint required' };
369
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
370
+ if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
371
371
  const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
372
372
  adapter,
373
- endpoint_id: endpointId,
373
+ endpoint_id: endpointKey,
374
374
  }, {
375
375
  orderBy: { field: 'created_at', direction: 'ASC' },
376
376
  });
@@ -386,10 +386,10 @@ async function listInboxMessages(
386
386
  ctx: ConsoleRpcExtendedCtx,
387
387
  ): Promise<ExtendedRpcResult> {
388
388
  const adapter = strField(d, '$adapter', 'adapter');
389
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
389
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
390
390
  const channelId = strField(d, '$channel_id', 'channelId', 'channel_id');
391
391
  const channelType = strField(d, '$channel_type', 'channelType', 'channel_type');
392
- if (!adapter || !endpointId || !channelId || !channelType) {
392
+ if (!adapter || !endpointKey || !channelId || !channelType) {
393
393
  return { error: '$adapter, $endpoint, $channel_id, $channel_type required' };
394
394
  }
395
395
  const limit = Math.min(numField(d, 50, '$limit', 'limit'), 100);
@@ -399,7 +399,7 @@ async function listInboxMessages(
399
399
 
400
400
  const where: Record<string, unknown> = {
401
401
  adapter,
402
- endpoint_id: endpointId,
402
+ endpoint_id: endpointKey,
403
403
  channel_id: channelId,
404
404
  channel_type: channelType,
405
405
  };
@@ -525,12 +525,12 @@ async function actOnRequest(
525
525
  ctx: ConsoleRpcExtendedCtx,
526
526
  ): Promise<ExtendedRpcResult> {
527
527
  const adapter = strField(d, '$adapter', 'adapter');
528
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
528
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
529
529
  const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
530
- if (!adapter || !endpointId || !requestId) {
530
+ if (!adapter || !endpointKey || !requestId) {
531
531
  return { error: '$adapter, $endpoint, $id required' };
532
532
  }
533
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
533
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
534
534
  if ('error' in resolved) return resolved;
535
535
  const management = resolved.management;
536
536
  const approve = type === 'request.approve';
@@ -613,12 +613,12 @@ async function listGroupMembers(
613
613
  ctx: ConsoleRpcExtendedCtx,
614
614
  ): Promise<ExtendedRpcResult> {
615
615
  const adapter = strField(d, '$adapter', 'adapter');
616
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
616
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
617
617
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
618
- if (!adapter || !endpointId || !groupId) {
618
+ if (!adapter || !endpointKey || !groupId) {
619
619
  return { error: '$adapter, $endpoint, $group_id required' };
620
620
  }
621
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
621
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
622
622
  if ('error' in resolved) return resolved;
623
623
  const { management } = resolved;
624
624
  try {
@@ -649,16 +649,16 @@ async function groupWriteOp(
649
649
  spec: GroupWriteSpec,
650
650
  ): Promise<ExtendedRpcResult> {
651
651
  const adapter = strField(d, '$adapter', 'adapter');
652
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
652
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
653
653
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
654
654
  const userId = strField(d, '$user_id', 'userId', 'user_id');
655
- if (!adapter || !endpointId || !groupId) {
655
+ if (!adapter || !endpointKey || !groupId) {
656
656
  return { error: '$adapter, $endpoint, $group_id required' };
657
657
  }
658
658
  if (spec.requireUser && !userId) {
659
659
  return { error: '$user_id required' };
660
660
  }
661
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
661
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
662
662
  if ('error' in resolved) return resolved;
663
663
  const { management } = resolved;
664
664
  const method = management[spec.method];
@@ -686,12 +686,12 @@ async function deleteFriend(
686
686
  ctx: ConsoleRpcExtendedCtx,
687
687
  ): Promise<ExtendedRpcResult> {
688
688
  const adapter = strField(d, '$adapter', 'adapter');
689
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
689
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
690
690
  const userId = strField(d, '$user_id', 'userId', 'user_id');
691
- if (!adapter || !endpointId || !userId) {
691
+ if (!adapter || !endpointKey || !userId) {
692
692
  return { error: '$adapter, $endpoint, $user_id required' };
693
693
  }
694
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
694
+ const resolved = resolveLiveEndpoint(ctx, adapter, endpointKey);
695
695
  if ('error' in resolved) return resolved;
696
696
  const { management } = resolved;
697
697
  const method = management.deleteFriend;
@@ -749,23 +749,23 @@ function numArrayField(d: Record<string, unknown>, ...keys: string[]): number[]
749
749
  type ResolveOk = {
750
750
  management: EndpointManagementPort;
751
751
  adapter: string;
752
- endpointId: string;
752
+ endpointKey: string;
753
753
  };
754
754
 
755
755
  function resolveLiveEndpoint(
756
756
  ctx: ConsoleRpcExtendedCtx,
757
757
  adapter: string,
758
- endpointId: string,
758
+ endpointKey: string,
759
759
  ): ResolveOk | { error: string } {
760
760
  if (ctx.resolveEndpointManagement) {
761
- const management = ctx.resolveEndpointManagement(adapter, endpointId);
761
+ const management = ctx.resolveEndpointManagement(adapter, endpointKey);
762
762
  if (!management) return { error: 'endpoint not found' };
763
- return { management, adapter, endpointId };
763
+ return { management, adapter, endpointKey };
764
764
  }
765
765
  if (!ctx.resolveEndpoint) {
766
766
  return { error: 'Endpoint registry is not configured' };
767
767
  }
768
- const endpoint = ctx.resolveEndpoint(adapter, endpointId);
768
+ const endpoint = ctx.resolveEndpoint(adapter, endpointKey);
769
769
  if (!endpoint || typeof endpoint !== 'object') {
770
770
  return { error: 'endpoint not found' };
771
771
  }
@@ -775,7 +775,7 @@ function resolveLiveEndpoint(
775
775
  ? management as EndpointManagementPort
776
776
  : {},
777
777
  adapter,
778
- endpointId,
778
+ endpointKey,
779
779
  };
780
780
  }
781
781
 
@@ -784,9 +784,9 @@ function requireEndpoint(
784
784
  ctx: ConsoleRpcExtendedCtx,
785
785
  ): ResolveOk | { error: string } {
786
786
  const adapter = strField(d, '$adapter', 'adapter');
787
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
788
- if (!adapter || !endpointId) return { error: '$adapter and $endpoint required' };
789
- return resolveLiveEndpoint(ctx, adapter, endpointId);
787
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
788
+ if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
789
+ return resolveLiveEndpoint(ctx, adapter, endpointKey);
790
790
  }
791
791
 
792
792
  function normalizeParent(raw: unknown): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
@@ -8,6 +8,44 @@ import {
8
8
  } from '@zhin.js/console-protocol';
9
9
  import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
10
10
 
11
+ /**
12
+ * Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
13
+ * `Omit<ConversationRef,'endpoint'>`;host/http 架构分层不允许依赖
14
+ * `@zhin.js/im-contract`,故本地结构化声明,endpoint 由 ImRuntime 锚定)。
15
+ */
16
+ export interface ConversationAddress {
17
+ readonly kind: 'private' | 'group' | 'channel';
18
+ readonly id: string;
19
+ readonly parent?: Readonly<{
20
+ readonly kind: 'group' | 'channel';
21
+ readonly id: string;
22
+ }>;
23
+ readonly threadId?: string;
24
+ }
25
+
26
+ /** `$channel_type`/`$channel_id`/`$parent` wire 字段 → 会话地址(RPC 边界组帧)。 */
27
+ function wireConversation(
28
+ channelType: string,
29
+ channelId: string,
30
+ parent: unknown,
31
+ ): ConversationAddress {
32
+ // console 旧 wire 允许 channel_id 自带场景前缀(`group:123`),解析结果优先
33
+ const prefixed = /^(private|group|channel|direct|c2c|temp):(.+)$/iu.exec(channelId);
34
+ const rawKind = channelType || prefixed?.[1] || 'private';
35
+ const kind: ConversationAddress['kind'] =
36
+ rawKind === 'group' || rawKind === 'channel' ? rawKind : 'private';
37
+ const wire = parent && typeof parent === 'object'
38
+ ? parent as { readonly type?: unknown; readonly id?: unknown }
39
+ : undefined;
40
+ const parentKind = wire?.type === 'group' || wire?.type === 'channel' ? wire.type : undefined;
41
+ const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
42
+ return {
43
+ kind,
44
+ id: prefixed ? prefixed[2]! : channelId,
45
+ ...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
46
+ };
47
+ }
48
+
11
49
  export type RuntimeConsoleRpcMessage = {
12
50
  readonly type?: unknown;
13
51
  readonly requestId?: unknown;
@@ -55,7 +93,7 @@ export type RuntimeConsoleRpcContext = {
55
93
  getAllSchemas?(): Promise<Record<string, unknown>>;
56
94
  /** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
57
95
  listEndpoints?(): Promise<readonly RuntimeEndpointSummary[]>;
58
- getEndpoint?(adapter: string, endpointId: string): Promise<RuntimeEndpointSummary | null>;
96
+ getEndpoint?(adapter: string, endpointKey: string): Promise<RuntimeEndpointSummary | null>;
59
97
  sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{ messageId: string }>;
60
98
  /** Optional Database host surface (CLI wires DatabaseHost from plugin-runtime). */
61
99
  dbInfo?(): Promise<RuntimeDatabaseInfo> | RuntimeDatabaseInfo;
@@ -85,11 +123,9 @@ export type RuntimeDatabaseInfo = {
85
123
 
86
124
  export type RuntimeEndpointSendInput = {
87
125
  readonly adapter: string;
88
- readonly endpointId: string;
89
- readonly channelId: string;
90
- readonly channelType: string;
126
+ readonly endpointKey: string;
127
+ readonly conversation: ConversationAddress;
91
128
  readonly content: unknown;
92
- readonly parent?: { type?: string; id?: string; name?: string };
93
129
  };
94
130
 
95
131
  export function pickRpcReply(
@@ -594,8 +630,8 @@ export async function dispatchRuntimeConsoleRpc(
594
630
  try {
595
631
  const data = message as Record<string, unknown>;
596
632
  const adapter = String(data.$adapter ?? '');
597
- const endpointId = String(data.$endpoint ?? '');
598
- if (!adapter || !endpointId) {
633
+ const endpointKey = String(data.$endpoint ?? '');
634
+ if (!adapter || !endpointKey) {
599
635
  emit({ requestId, error: '$adapter and $endpoint required' });
600
636
  return payloads;
601
637
  }
@@ -603,7 +639,7 @@ export async function dispatchRuntimeConsoleRpc(
603
639
  emit({ requestId, error: 'Endpoint registry is not configured' });
604
640
  return payloads;
605
641
  }
606
- const endpoint = await ctx.getEndpoint(adapter, endpointId);
642
+ const endpoint = await ctx.getEndpoint(adapter, endpointKey);
607
643
  if (!endpoint) {
608
644
  emit({ requestId, error: 'endpoint not found' });
609
645
  return payloads;
@@ -621,13 +657,12 @@ export async function dispatchRuntimeConsoleRpc(
621
657
  try {
622
658
  const data = message as Record<string, unknown>;
623
659
  const adapter = String(data.$adapter ?? '');
624
- const endpointId = String(data.$endpoint ?? '');
660
+ const endpointKey = String(data.$endpoint ?? '');
625
661
  const channelId = String(data.$channel_id ?? '');
626
662
  const channelType = String(data.$channel_type ?? '');
627
663
  const content = data.$content;
628
- // channelType is optional: ImRuntime tolerates an empty value and
629
- // targets the bare channel id (parity with the legacy RPC handler).
630
- if (!adapter || !endpointId || !channelId || content === undefined) {
664
+ // channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
665
+ if (!adapter || !endpointKey || !channelId || content === undefined) {
631
666
  emit({
632
667
  requestId,
633
668
  error: '$adapter, $endpoint, $channel_id, $content required',
@@ -640,12 +675,9 @@ export async function dispatchRuntimeConsoleRpc(
640
675
  }
641
676
  const result = await ctx.sendEndpointMessage({
642
677
  adapter,
643
- endpointId,
644
- channelId,
645
- channelType,
678
+ endpointKey,
679
+ conversation: wireConversation(channelType, channelId, data.$parent),
646
680
  content,
647
- parent: data.$parent as
648
- { type?: string; id?: string; name?: string } | undefined,
649
681
  });
650
682
  // Legacy contract: { message_id }. Keep messageId for new callers.
651
683
  emit({ requestId, data: endpointSendResult(result.messageId) });
package/src/http-host.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import { isDemoHttpAllowed } from './console-rpc.js';
25
25
  import { HttpBodyError } from './json-body.js';
26
26
 
27
- const logger = getLogger('HttpHost');
27
+ const logger = getLogger('http');
28
28
 
29
29
  export const REMOTE_CONSOLE_ORIGIN = 'https://console.zhin.dev';
30
30
  const DEFAULT_CORS_ORIGINS = Object.freeze([REMOTE_CONSOLE_ORIGIN]);
@@ -401,15 +401,17 @@ export function createHttpHost(options: HttpHostOptions = {}): HttpHost {
401
401
  const listenPort = typeof bound === 'object' && bound ? bound.port : port;
402
402
  const publicHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
403
403
  address = Object.freeze({ host: publicHost, port: listenPort });
404
- logger.info(formatCompact({
405
- http: `http://${publicHost}:${listenPort}`,
406
- token: tokenRegistry.hasAnyToken()
404
+ logger.info(
405
+ `listening http://${publicHost}:${listenPort}`
406
+ + ` | routes: ${httpRoutes.length}`
407
+ + ` | token: ${tokenRegistry.hasAnyToken()
407
408
  ? `${tokenRegistry.primaryTokenPrefixForLog()}…`
408
- : '(none)',
409
- cors: corsOrigins.join(','),
410
- ws_routes: [...wsRoutes.keys()].join(',') || '(none)',
411
- http_routes: httpRoutes.length,
412
- }));
409
+ : '(none)'}`,
410
+ );
411
+ logger.debug(
412
+ `listen detail | cors: ${corsOrigins.join(',')}`
413
+ + ` | ws: ${[...wsRoutes.keys()].join(',') || '(none)'}`,
414
+ );
413
415
  resolve(address);
414
416
  });
415
417
  });