@zhin.js/host-http 1.0.6 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,11 @@ Console can register path-scoped WebSocket / HTTP routes without importing Koa o
7
7
  Root installs the host via `installResources` (see `basic/cli/src/plugin-runtime/http-host-installer.ts`
8
8
  and `console-host-installer.ts`).
9
9
 
10
+ The composition root owns the process listener through `ProcessHttpHost`. Generation scopes receive
11
+ only the `HttpHost` routing port: they may register HTTP/WS routes but cannot listen or close the
12
+ shared server. Registrations are bound to generation admission, so candidate routes stay invisible
13
+ until snapshot commit and retired routes stop matching immediately while their leases drain.
14
+
10
15
  ## Capabilities (this slice)
11
16
 
12
17
  - Path-scoped WebSocket upgrades (`ws(path)`)
@@ -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) {
@@ -11,8 +11,8 @@
11
11
  * - inbox:`ctx.databaseHost.models`(unified_inbox_message/request/notice 三张表,
12
12
  * 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
13
13
  * 按行 id update consumed=1,表/model 缺失时报"未接线")。
14
- * - 社交/群管:`ctx.resolveEndpoint` 拿 live endpoint 实例,只消费其
15
- * `management` 语义端口;平台 SDK 兼容与字段归一化由 Adapter 负责。
14
+ * - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
15
+ * 消费 `management` 语义端口;平台 SDK 字段归一化由 Adapter 负责。
16
16
  */
17
17
  export interface ConsoleRpcExtendedCtx {
18
18
  /** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
@@ -20,10 +20,8 @@ export interface ConsoleRpcExtendedCtx {
20
20
  projectRoot: string;
21
21
  /** Plugin Runtime ScheduleHost(basic/cli schedule-host-installer 提供)。 */
22
22
  scheduleHost?: unknown;
23
- /** 经 ImRuntime / AdapterEndpointIndex 解析管理语义端口。 */
24
- resolveEndpointManagement?: (adapter: string, endpointId: string) => EndpointManagementPort | null | undefined;
25
- /** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
26
- resolveEndpoint?: (adapter: string, endpointId: string) => unknown;
23
+ /** 在固定 generation lease 内执行一次 Endpoint 管理操作。 */
24
+ withEndpointManagement?: <T>(adapter: string, endpointKey: string, run: (management: EndpointManagementPort) => T | Promise<T>) => Promise<T | null>;
27
25
  /** Plugin Runtime DatabaseHost 的 models 视图。 */
28
26
  databaseHost?: {
29
27
  models: {
@@ -11,8 +11,8 @@
11
11
  * - inbox:`ctx.databaseHost.models`(unified_inbox_message/request/notice 三张表,
12
12
  * 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
13
13
  * 按行 id update consumed=1,表/model 缺失时报"未接线")。
14
- * - 社交/群管:`ctx.resolveEndpoint` 拿 live endpoint 实例,只消费其
15
- * `management` 语义端口;平台 SDK 兼容与字段归一化由 Adapter 负责。
14
+ * - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
15
+ * 消费 `management` 语义端口;平台 SDK 字段归一化由 Adapter 负责。
16
16
  */
17
17
  /** 新 Runtime 统一收件箱表名(SSOT:@zhin.js/plugin-runtime inbox.js)。 */
18
18
  import { assertDemoConsoleRpcAllowed, normalizeConsoleRpcType, } from '@zhin.js/console-protocol';
@@ -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,126 +393,96 @@ 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);
402
- if ('error' in resolved)
403
- return resolved;
404
- const management = resolved.management;
405
- const approve = type === 'request.approve';
406
- const method = approve ? management.approveRequest : management.rejectRequest;
407
- if (!method) {
408
- return {
409
- error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
410
- '且新 Runtime 尚未挂载 pending request 注册表',
411
- };
412
- }
413
- try {
401
+ return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
402
+ const approve = type === 'request.approve';
403
+ const method = approve ? management.approveRequest : management.rejectRequest;
404
+ if (!method) {
405
+ return {
406
+ error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
407
+ '且新 Runtime 尚未挂载 pending request 注册表',
408
+ };
409
+ }
414
410
  const extra = approve
415
411
  ? strField(d, '$remark', 'remark')
416
412
  : strField(d, '$reason', 'reason');
417
413
  await method.call(management, requestId, extra || undefined);
418
414
  return { data: { success: true } };
419
- }
420
- catch (error) {
421
- return { error: errorMessage(error) };
422
- }
415
+ });
423
416
  }
424
417
  // ---------------------------------------------------------------- 社交读取
425
418
  async function listFriends(d, ctx) {
426
- const resolved = requireEndpoint(d, ctx);
427
- if ('error' in resolved)
428
- return resolved;
429
- const { management, adapter } = resolved;
430
- try {
419
+ const endpoint = requireEndpointAddress(d);
420
+ if ('error' in endpoint)
421
+ return endpoint;
422
+ return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
431
423
  if (management.listFriends) {
432
424
  const friends = [...await management.listFriends()];
433
425
  return { data: { friends, count: friends.length } };
434
426
  }
435
- return { error: `当前适配器(${adapter})不支持好友列表` };
436
- }
437
- catch (error) {
438
- return { error: errorMessage(error) };
439
- }
427
+ return { error: `当前适配器(${endpoint.adapter})不支持好友列表` };
428
+ });
440
429
  }
441
430
  async function listGroups(d, ctx) {
442
- const resolved = requireEndpoint(d, ctx);
443
- if ('error' in resolved)
444
- return resolved;
445
- const { management, adapter } = resolved;
446
- try {
431
+ const endpoint = requireEndpointAddress(d);
432
+ if ('error' in endpoint)
433
+ return endpoint;
434
+ return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
447
435
  if (management.listGroups) {
448
436
  const groups = [...await management.listGroups()];
449
437
  return { data: { groups, count: groups.length } };
450
438
  }
451
- return { error: `当前适配器(${adapter})不支持群列表` };
452
- }
453
- catch (error) {
454
- return { error: errorMessage(error) };
455
- }
439
+ return { error: `当前适配器(${endpoint.adapter})不支持群列表` };
440
+ });
456
441
  }
457
442
  async function listChannels(d, ctx) {
458
- const resolved = requireEndpoint(d, ctx);
459
- if ('error' in resolved)
460
- return resolved;
461
- const { management, adapter } = resolved;
462
- try {
443
+ const endpoint = requireEndpointAddress(d);
444
+ if ('error' in endpoint)
445
+ return endpoint;
446
+ return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
463
447
  if (management.listChannels) {
464
448
  const channels = [...await management.listChannels()];
465
449
  return { data: { channels, count: channels.length } };
466
450
  }
467
- return { error: `当前适配器(${adapter})不支持频道列表` };
468
- }
469
- catch (error) {
470
- return { error: errorMessage(error) };
471
- }
451
+ return { error: `当前适配器(${endpoint.adapter})不支持频道列表` };
452
+ });
472
453
  }
473
454
  async function listGroupMembers(d, ctx) {
474
455
  const adapter = strField(d, '$adapter', 'adapter');
475
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
456
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
476
457
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
477
- if (!adapter || !endpointId || !groupId) {
458
+ if (!adapter || !endpointKey || !groupId) {
478
459
  return { error: '$adapter, $endpoint, $group_id required' };
479
460
  }
480
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
481
- if ('error' in resolved)
482
- return resolved;
483
- const { management } = resolved;
484
- try {
461
+ return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
485
462
  const method = management.listGroupMembers;
486
463
  if (!method) {
487
464
  return { error: `当前适配器(${adapter})不支持群成员列表` };
488
465
  }
489
466
  const members = [...await method(groupId)];
490
467
  return { data: { members, count: members.length } };
491
- }
492
- catch (error) {
493
- return { error: errorMessage(error) };
494
- }
468
+ });
495
469
  }
496
470
  async function groupWriteOp(d, ctx, spec) {
497
471
  const adapter = strField(d, '$adapter', 'adapter');
498
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
472
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
499
473
  const groupId = strField(d, '$group_id', 'groupId', 'group_id');
500
474
  const userId = strField(d, '$user_id', 'userId', 'user_id');
501
- if (!adapter || !endpointId || !groupId) {
475
+ if (!adapter || !endpointKey || !groupId) {
502
476
  return { error: '$adapter, $endpoint, $group_id required' };
503
477
  }
504
478
  if (spec.requireUser && !userId) {
505
479
  return { error: '$user_id required' };
506
480
  }
507
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
508
- if ('error' in resolved)
509
- return resolved;
510
- const { management } = resolved;
511
- const method = management[spec.method];
512
- if (!method) {
513
- return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
514
- }
515
- try {
481
+ return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
482
+ const method = management[spec.method];
483
+ if (!method) {
484
+ return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
485
+ }
516
486
  const durationRaw = d.$duration ?? d.duration;
517
487
  const enableRaw = d.$enable ?? d.enable;
518
488
  const args = spec.buildArgs(groupId, userId, {
@@ -523,33 +493,23 @@ async function groupWriteOp(d, ctx, spec) {
523
493
  });
524
494
  await method.call(management, ...args);
525
495
  return { data: { success: true } };
526
- }
527
- catch (error) {
528
- return { error: errorMessage(error) };
529
- }
496
+ });
530
497
  }
531
498
  async function deleteFriend(d, ctx) {
532
499
  const adapter = strField(d, '$adapter', 'adapter');
533
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
500
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
534
501
  const userId = strField(d, '$user_id', 'userId', 'user_id');
535
- if (!adapter || !endpointId || !userId) {
502
+ if (!adapter || !endpointKey || !userId) {
536
503
  return { error: '$adapter, $endpoint, $user_id required' };
537
504
  }
538
- const resolved = resolveLiveEndpoint(ctx, adapter, endpointId);
539
- if ('error' in resolved)
540
- return resolved;
541
- const { management } = resolved;
542
- const method = management.deleteFriend;
543
- if (!method) {
544
- return { error: '当前适配器暂不支持删除好友' };
545
- }
546
- try {
505
+ return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
506
+ const method = management.deleteFriend;
507
+ if (!method) {
508
+ return { error: '当前适配器暂不支持删除好友' };
509
+ }
547
510
  await method(userId);
548
511
  return { data: { success: true } };
549
- }
550
- catch (error) {
551
- return { error: errorMessage(error) };
552
- }
512
+ });
553
513
  }
554
514
  // ---------------------------------------------------------------- helpers
555
515
  function strField(d, ...keys) {
@@ -592,35 +552,24 @@ function numArrayField(d, ...keys) {
592
552
  }
593
553
  return [];
594
554
  }
595
- function resolveLiveEndpoint(ctx, adapter, endpointId) {
596
- if (ctx.resolveEndpointManagement) {
597
- const management = ctx.resolveEndpointManagement(adapter, endpointId);
598
- if (!management)
599
- return { error: 'endpoint not found' };
600
- return { management, adapter, endpointId };
601
- }
602
- if (!ctx.resolveEndpoint) {
555
+ async function withLiveEndpoint(ctx, adapter, endpointKey, run) {
556
+ if (!ctx.withEndpointManagement) {
603
557
  return { error: 'Endpoint registry is not configured' };
604
558
  }
605
- const endpoint = ctx.resolveEndpoint(adapter, endpointId);
606
- if (!endpoint || typeof endpoint !== 'object') {
607
- return { error: 'endpoint not found' };
559
+ try {
560
+ return await ctx.withEndpointManagement(adapter, endpointKey, run)
561
+ ?? { error: 'endpoint not found' };
562
+ }
563
+ catch (error) {
564
+ return { error: errorMessage(error) };
608
565
  }
609
- const management = endpoint.management;
610
- return {
611
- management: management && typeof management === 'object'
612
- ? management
613
- : {},
614
- adapter,
615
- endpointId,
616
- };
617
566
  }
618
- function requireEndpoint(d, ctx) {
567
+ function requireEndpointAddress(d) {
619
568
  const adapter = strField(d, '$adapter', 'adapter');
620
- const endpointId = strField(d, '$endpoint', 'endpointId', 'endpoint');
621
- if (!adapter || !endpointId)
569
+ const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
570
+ if (!adapter || !endpointKey)
622
571
  return { error: '$adapter and $endpoint required' };
623
- return resolveLiveEndpoint(ctx, adapter, endpointId);
572
+ return { adapter, endpointKey };
624
573
  }
625
574
  function normalizeParent(raw) {
626
575
  if (!raw || typeof raw !== 'object')
@@ -68,7 +68,7 @@ export type RuntimeConsoleRpcContext = {
68
68
  getAllSchemas?(): Promise<Record<string, unknown>>;
69
69
  /** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
70
70
  listEndpoints?(): Promise<readonly RuntimeEndpointSummary[]>;
71
- getEndpoint?(adapter: string, endpointId: string): Promise<RuntimeEndpointSummary | null>;
71
+ getEndpoint?(adapter: string, endpointKey: string): Promise<RuntimeEndpointSummary | null>;
72
72
  sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{
73
73
  messageId: string;
74
74
  }>;
@@ -96,7 +96,7 @@ export type RuntimeDatabaseInfo = {
96
96
  };
97
97
  export type RuntimeEndpointSendInput = {
98
98
  readonly adapter: string;
99
- readonly endpointId: string;
99
+ readonly endpointKey: string;
100
100
  readonly conversation: ConversationAddress;
101
101
  readonly content: unknown;
102
102
  };