@zhin.js/host-http 1.0.4 → 1.0.6
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-rpc.d.ts +15 -7
- package/lib/console-rpc.js +19 -5
- package/lib/json-body.d.ts +2 -12
- package/lib/json-body.js +2 -45
- package/package.json +4 -3
- package/src/console-rpc.ts +41 -9
- package/src/json-body.ts +2 -47
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;
|
|
@@ -83,14 +97,8 @@ export type RuntimeDatabaseInfo = {
|
|
|
83
97
|
export type RuntimeEndpointSendInput = {
|
|
84
98
|
readonly adapter: string;
|
|
85
99
|
readonly endpointId: string;
|
|
86
|
-
readonly
|
|
87
|
-
readonly channelType: 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)
|
|
@@ -557,8 +574,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
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
|
-
// targets the bare channel id (parity with the legacy RPC handler).
|
|
577
|
+
// channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
|
|
562
578
|
if (!adapter || !endpointId || !channelId || content === undefined) {
|
|
563
579
|
emit({
|
|
564
580
|
requestId,
|
|
@@ -573,10 +589,8 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
573
589
|
const result = await ctx.sendEndpointMessage({
|
|
574
590
|
adapter,
|
|
575
591
|
endpointId,
|
|
576
|
-
channelId,
|
|
577
|
-
channelType,
|
|
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/json-body.d.ts
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
readonly statusCode: number;
|
|
4
|
-
constructor(message: string, statusCode?: number);
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Read and parse a JSON request body with a size limit (default 1 MiB).
|
|
8
|
-
* Empty bodies become `undefined`.
|
|
9
|
-
*/
|
|
10
|
-
export declare function readJsonBody<T = unknown>(request: IncomingMessage, options?: {
|
|
11
|
-
readonly limit?: number;
|
|
12
|
-
}): Promise<T | undefined>;
|
|
1
|
+
/** @deprecated Import from @zhin.js/host-http-contract in protocol hosts. */
|
|
2
|
+
export { HttpBodyError, readJsonBody } from '@zhin.js/host-http-contract';
|
package/lib/json-body.js
CHANGED
|
@@ -1,45 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
constructor(message, statusCode = 400) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.statusCode = statusCode;
|
|
6
|
-
this.name = 'HttpBodyError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Read and parse a JSON request body with a size limit (default 1 MiB).
|
|
11
|
-
* Empty bodies become `undefined`.
|
|
12
|
-
*/
|
|
13
|
-
export async function readJsonBody(request, options = {}) {
|
|
14
|
-
const limit = options.limit ?? 1_048_576;
|
|
15
|
-
const chunks = [];
|
|
16
|
-
let size = 0;
|
|
17
|
-
let exceeded = false;
|
|
18
|
-
for await (const chunk of request) {
|
|
19
|
-
// 超限后继续 drain 到 end:提前 break 会 destroy 底层 socket,
|
|
20
|
-
// 客户端将永远收不到后面的 413 响应。
|
|
21
|
-
if (exceeded)
|
|
22
|
-
continue;
|
|
23
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
-
size += buffer.length;
|
|
25
|
-
if (size > limit) {
|
|
26
|
-
exceeded = true;
|
|
27
|
-
chunks.length = 0;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
chunks.push(buffer);
|
|
31
|
-
}
|
|
32
|
-
if (exceeded)
|
|
33
|
-
throw new HttpBodyError(`Request body exceeds ${limit} bytes`, 413);
|
|
34
|
-
if (chunks.length === 0)
|
|
35
|
-
return undefined;
|
|
36
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
37
|
-
if (!raw)
|
|
38
|
-
return undefined;
|
|
39
|
-
try {
|
|
40
|
-
return JSON.parse(raw);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
throw new HttpBodyError('Invalid JSON body', 400);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/** @deprecated Import from @zhin.js/host-http-contract in protocol hosts. */
|
|
2
|
+
export { HttpBodyError, readJsonBody } from '@zhin.js/host-http-contract';
|
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.6",
|
|
4
4
|
"description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"ws": "^8.21.1",
|
|
21
21
|
"@zhin.js/console-protocol": "1.1.0",
|
|
22
|
-
"@zhin.js/
|
|
23
|
-
"@zhin.js/logger": "1.0.75"
|
|
22
|
+
"@zhin.js/host-http-contract": "1.0.0",
|
|
23
|
+
"@zhin.js/logger": "1.0.75",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.3"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/node": "^26.1.2",
|
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;
|
|
@@ -86,10 +124,8 @@ export type RuntimeDatabaseInfo = {
|
|
|
86
124
|
export type RuntimeEndpointSendInput = {
|
|
87
125
|
readonly adapter: string;
|
|
88
126
|
readonly endpointId: string;
|
|
89
|
-
readonly
|
|
90
|
-
readonly channelType: 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(
|
|
@@ -625,8 +661,7 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
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
|
-
// targets the bare channel id (parity with the legacy RPC handler).
|
|
664
|
+
// channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
|
|
630
665
|
if (!adapter || !endpointId || !channelId || content === undefined) {
|
|
631
666
|
emit({
|
|
632
667
|
requestId,
|
|
@@ -641,11 +676,8 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
641
676
|
const result = await ctx.sendEndpointMessage({
|
|
642
677
|
adapter,
|
|
643
678
|
endpointId,
|
|
644
|
-
channelId,
|
|
645
|
-
channelType,
|
|
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/json-body.ts
CHANGED
|
@@ -1,47 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export class HttpBodyError extends Error {
|
|
4
|
-
constructor(
|
|
5
|
-
message: string,
|
|
6
|
-
readonly statusCode: number = 400,
|
|
7
|
-
) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = 'HttpBodyError';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Read and parse a JSON request body with a size limit (default 1 MiB).
|
|
15
|
-
* Empty bodies become `undefined`.
|
|
16
|
-
*/
|
|
17
|
-
export async function readJsonBody<T = unknown>(
|
|
18
|
-
request: IncomingMessage,
|
|
19
|
-
options: { readonly limit?: number } = {},
|
|
20
|
-
): Promise<T | undefined> {
|
|
21
|
-
const limit = options.limit ?? 1_048_576;
|
|
22
|
-
const chunks: Buffer[] = [];
|
|
23
|
-
let size = 0;
|
|
24
|
-
let exceeded = false;
|
|
25
|
-
for await (const chunk of request) {
|
|
26
|
-
// 超限后继续 drain 到 end:提前 break 会 destroy 底层 socket,
|
|
27
|
-
// 客户端将永远收不到后面的 413 响应。
|
|
28
|
-
if (exceeded) continue;
|
|
29
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
30
|
-
size += buffer.length;
|
|
31
|
-
if (size > limit) {
|
|
32
|
-
exceeded = true;
|
|
33
|
-
chunks.length = 0;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
chunks.push(buffer);
|
|
37
|
-
}
|
|
38
|
-
if (exceeded) throw new HttpBodyError(`Request body exceeds ${limit} bytes`, 413);
|
|
39
|
-
if (chunks.length === 0) return undefined;
|
|
40
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
41
|
-
if (!raw) return undefined;
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(raw) as T;
|
|
44
|
-
} catch {
|
|
45
|
-
throw new HttpBodyError('Invalid JSON body', 400);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
/** @deprecated Import from @zhin.js/host-http-contract in protocol hosts. */
|
|
2
|
+
export { HttpBodyError, readJsonBody } from '@zhin.js/host-http-contract';
|