@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.
- package/lib/console-rest-pages.d.ts +5 -2
- package/lib/console-rest-pages.js +90 -67
- package/lib/console-rpc-extended.d.ts +2 -2
- package/lib/console-rpc-extended.js +29 -29
- package/lib/console-rpc.d.ts +17 -9
- package/lib/console-rpc.js +25 -11
- package/lib/http-host.js +7 -9
- package/package.json +5 -5
- package/src/console-rest-pages.ts +98 -73
- package/src/console-rpc-extended.ts +32 -32
- package/src/console-rpc.ts +49 -17
- package/src/http-host.ts +11 -9
|
@@ -14,7 +14,10 @@ export interface ConsoleRestCtx {
|
|
|
14
14
|
* 可选:返回新 Runtime 的 agent 门面(见 {@link ConsoleAgentRuntime})。
|
|
15
15
|
* 未接线时 introspection 降级为空列表 + note,session tree 返回 503。
|
|
16
16
|
*/
|
|
17
|
-
readonly
|
|
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.
|
|
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 未接线
|
|
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
|
|
470
|
-
|
|
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
|
|
478
|
-
const
|
|
479
|
-
const tree =
|
|
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
|
-
|
|
483
|
-
|
|
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 需经
|
|
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
|
|
500
|
-
if (!
|
|
506
|
+
const lease = acquireSessionTree();
|
|
507
|
+
if (!lease) {
|
|
501
508
|
unavailable(response);
|
|
502
509
|
return;
|
|
503
510
|
}
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
561
|
-
if (!Number.isFinite(
|
|
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 =
|
|
566
|
+
messageId = value;
|
|
566
567
|
}
|
|
567
568
|
else if (indexRaw != null && indexRaw !== '') {
|
|
568
|
-
const
|
|
569
|
-
if (!Number.isFinite(
|
|
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
|
-
|
|
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,
|
|
576
|
-
success:
|
|
577
|
-
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
|
-
|
|
587
|
-
|
|
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,
|
|
24
|
+
resolveEndpointManagement?: (adapter: string, endpointKey: string) => EndpointManagementPort | null | undefined;
|
|
25
25
|
/** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
|
|
26
|
-
resolveEndpoint?: (adapter: string,
|
|
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
|
|
229
|
-
if (!adapter || !
|
|
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:
|
|
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
|
|
252
|
-
if (!adapter || !
|
|
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:
|
|
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
|
|
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 || !
|
|
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:
|
|
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
|
|
396
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
397
397
|
const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
|
|
398
|
-
if (!adapter || !
|
|
398
|
+
if (!adapter || !endpointKey || !requestId) {
|
|
399
399
|
return { error: '$adapter, $endpoint, $id required' };
|
|
400
400
|
}
|
|
401
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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
|
|
475
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
476
476
|
const groupId = strField(d, '$group_id', 'groupId', 'group_id');
|
|
477
|
-
if (!adapter || !
|
|
477
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
478
478
|
return { error: '$adapter, $endpoint, $group_id required' };
|
|
479
479
|
}
|
|
480
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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
|
|
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 || !
|
|
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,
|
|
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
|
|
533
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
534
534
|
const userId = strField(d, '$user_id', 'userId', 'user_id');
|
|
535
|
-
if (!adapter || !
|
|
535
|
+
if (!adapter || !endpointKey || !userId) {
|
|
536
536
|
return { error: '$adapter, $endpoint, $user_id required' };
|
|
537
537
|
}
|
|
538
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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,
|
|
595
|
+
function resolveLiveEndpoint(ctx, adapter, endpointKey) {
|
|
596
596
|
if (ctx.resolveEndpointManagement) {
|
|
597
|
-
const management = ctx.resolveEndpointManagement(adapter,
|
|
597
|
+
const management = ctx.resolveEndpointManagement(adapter, endpointKey);
|
|
598
598
|
if (!management)
|
|
599
599
|
return { error: 'endpoint not found' };
|
|
600
|
-
return { management, adapter,
|
|
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,
|
|
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
|
-
|
|
615
|
+
endpointKey,
|
|
616
616
|
};
|
|
617
617
|
}
|
|
618
618
|
function requireEndpoint(d, ctx) {
|
|
619
619
|
const adapter = strField(d, '$adapter', 'adapter');
|
|
620
|
-
const
|
|
621
|
-
if (!adapter || !
|
|
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,
|
|
623
|
+
return resolveLiveEndpoint(ctx, adapter, endpointKey);
|
|
624
624
|
}
|
|
625
625
|
function normalizeParent(raw) {
|
|
626
626
|
if (!raw || typeof raw !== 'object')
|
package/lib/console-rpc.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
86
|
-
readonly
|
|
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[]>;
|
package/lib/console-rpc.js
CHANGED
|
@@ -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
|
|
529
|
-
if (!adapter || !
|
|
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,
|
|
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
|
|
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
|
|
561
|
-
|
|
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
|
-
|
|
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('
|
|
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(
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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.
|
|
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.
|
|
22
|
-
"@zhin.js/host-http-contract": "1.0.
|
|
23
|
-
"@zhin.js/logger": "1.0.
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
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
|
|
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.
|
|
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 未接线
|
|
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
|
|
648
|
-
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 需经
|
|
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
|
|
683
|
-
if (!
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
writeJson(response,
|
|
691
|
-
success:
|
|
692
|
-
|
|
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
|
-
|
|
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
|
|
748
|
-
if (!Number.isFinite(
|
|
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 =
|
|
754
|
+
messageId = value;
|
|
753
755
|
} else if (indexRaw != null && indexRaw !== '') {
|
|
754
|
-
const
|
|
755
|
-
if (!Number.isFinite(
|
|
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
|
-
|
|
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,
|
|
762
|
-
success:
|
|
763
|
-
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
|
-
|
|
771
|
-
|
|
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
|
-
|
|
37
|
+
endpointKey: string,
|
|
38
38
|
) => EndpointManagementPort | null | undefined;
|
|
39
39
|
/** @deprecated Use `resolveEndpointManagement`; accepted for one compatibility cycle. */
|
|
40
|
-
resolveEndpoint?: (adapter: string,
|
|
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
|
|
344
|
-
if (!adapter || !
|
|
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:
|
|
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
|
|
370
|
-
if (!adapter || !
|
|
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:
|
|
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
|
|
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 || !
|
|
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:
|
|
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
|
|
528
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
529
529
|
const requestId = strField(d, '$id', 'id', 'platformRequestId', 'platform_request_id', 'requestId');
|
|
530
|
-
if (!adapter || !
|
|
530
|
+
if (!adapter || !endpointKey || !requestId) {
|
|
531
531
|
return { error: '$adapter, $endpoint, $id required' };
|
|
532
532
|
}
|
|
533
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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
|
|
616
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
617
617
|
const groupId = strField(d, '$group_id', 'groupId', 'group_id');
|
|
618
|
-
if (!adapter || !
|
|
618
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
619
619
|
return { error: '$adapter, $endpoint, $group_id required' };
|
|
620
620
|
}
|
|
621
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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
|
|
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 || !
|
|
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,
|
|
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
|
|
689
|
+
const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
|
|
690
690
|
const userId = strField(d, '$user_id', 'userId', 'user_id');
|
|
691
|
-
if (!adapter || !
|
|
691
|
+
if (!adapter || !endpointKey || !userId) {
|
|
692
692
|
return { error: '$adapter, $endpoint, $user_id required' };
|
|
693
693
|
}
|
|
694
|
-
const resolved = resolveLiveEndpoint(ctx, adapter,
|
|
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
|
-
|
|
752
|
+
endpointKey: string;
|
|
753
753
|
};
|
|
754
754
|
|
|
755
755
|
function resolveLiveEndpoint(
|
|
756
756
|
ctx: ConsoleRpcExtendedCtx,
|
|
757
757
|
adapter: string,
|
|
758
|
-
|
|
758
|
+
endpointKey: string,
|
|
759
759
|
): ResolveOk | { error: string } {
|
|
760
760
|
if (ctx.resolveEndpointManagement) {
|
|
761
|
-
const management = ctx.resolveEndpointManagement(adapter,
|
|
761
|
+
const management = ctx.resolveEndpointManagement(adapter, endpointKey);
|
|
762
762
|
if (!management) return { error: 'endpoint not found' };
|
|
763
|
-
return { management, adapter,
|
|
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,
|
|
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
|
-
|
|
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
|
|
788
|
-
if (!adapter || !
|
|
789
|
-
return resolveLiveEndpoint(ctx, adapter,
|
|
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 {
|
package/src/console-rpc.ts
CHANGED
|
@@ -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,
|
|
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
|
|
89
|
-
readonly
|
|
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
|
|
598
|
-
if (!adapter || !
|
|
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,
|
|
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
|
|
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
|
|
629
|
-
|
|
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
|
-
|
|
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('
|
|
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(
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
logger.info(
|
|
405
|
+
`listening http://${publicHost}:${listenPort}`
|
|
406
|
+
+ ` | routes: ${httpRoutes.length}`
|
|
407
|
+
+ ` | token: ${tokenRegistry.hasAnyToken()
|
|
407
408
|
? `${tokenRegistry.primaryTokenPrefixForLog()}…`
|
|
408
|
-
: '(none)'
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
});
|