@zhin.js/host-http 1.0.1 → 1.0.3
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 +3 -2
- package/lib/console-events.d.ts +13 -0
- package/lib/console-events.js +33 -0
- package/lib/console-rest-pages.js +11 -4
- package/lib/console-rpc-extended.d.ts +44 -4
- package/lib/console-rpc-extended.js +247 -167
- package/lib/console-rpc.d.ts +9 -9
- package/lib/console-rpc.js +38 -61
- package/lib/http-host.js +9 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/json-body.js +10 -2
- package/lib/project-files.js +10 -0
- package/package.json +3 -2
- package/src/console-events.ts +47 -0
- package/src/console-rest-pages.ts +13 -4
- package/src/console-rpc-extended.ts +313 -183
- package/src/console-rpc.ts +52 -75
- package/src/http-host.ts +9 -0
- package/src/index.ts +10 -0
- package/src/json-body.ts +8 -2
- package/src/project-files.ts +10 -0
package/src/console-rpc.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AuthScope } from './token-registry.js';
|
|
2
2
|
import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
|
|
3
|
+
import {
|
|
4
|
+
type ConsoleEndpointSummary,
|
|
5
|
+
assertDemoConsoleRpcAllowed,
|
|
6
|
+
endpointSendResult,
|
|
7
|
+
normalizeConsoleRpcMessage,
|
|
8
|
+
} from '@zhin.js/console-protocol';
|
|
3
9
|
import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
|
|
4
10
|
|
|
5
11
|
export type RuntimeConsoleRpcMessage = {
|
|
@@ -57,21 +63,19 @@ export type RuntimeConsoleRpcContext = {
|
|
|
57
63
|
database?: DatabaseHostConsole;
|
|
58
64
|
/** Full-scope: request process restart (CLI daemon watches exit code). */
|
|
59
65
|
requestRestart?(): Promise<void> | void;
|
|
66
|
+
/**
|
|
67
|
+
* Optional console event sink (`/api/events` SSE fan-out). Called after
|
|
68
|
+
* successful mutations: `config:updated` / `system:restarting`.
|
|
69
|
+
*/
|
|
70
|
+
publishEvent?(type: string, data: unknown): void;
|
|
60
71
|
/** Extended RPC surface(cron/schedule、endpoint 社交/inbox),fullScope 由 authScope 推导。 */
|
|
61
72
|
extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
|
|
62
73
|
};
|
|
63
74
|
|
|
64
|
-
export type RuntimeEndpointPhase =
|
|
65
|
-
'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
|
|
75
|
+
export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
readonly adapter: string;
|
|
70
|
-
readonly connected: boolean;
|
|
71
|
-
readonly status: 'online' | 'offline';
|
|
72
|
-
readonly phase?: RuntimeEndpointPhase;
|
|
73
|
-
readonly pendingLogin?: boolean;
|
|
74
|
-
};
|
|
77
|
+
/** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
|
|
78
|
+
export type RuntimeEndpointSummary = ConsoleEndpointSummary;
|
|
75
79
|
|
|
76
80
|
export type RuntimeDatabaseInfo = {
|
|
77
81
|
readonly dialect: string | null;
|
|
@@ -88,47 +92,6 @@ export type RuntimeEndpointSendInput = {
|
|
|
88
92
|
readonly parent?: { type?: string; id?: string; name?: string };
|
|
89
93
|
};
|
|
90
94
|
|
|
91
|
-
/**
|
|
92
|
-
* Frontend SDK (@zhin.js/client) calls colon-named aliases; normalize to the
|
|
93
|
-
* canonical dot names before scope checks and dispatch. Alias parameter shapes
|
|
94
|
-
* (`data.{adapter, endpointId, id, type, content, parent}`) are mapped onto the
|
|
95
|
-
* `$`-prefixed fields inside each endpoint case.
|
|
96
|
-
*/
|
|
97
|
-
const RPC_TYPE_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
98
|
-
'endpoint:list': 'endpoint.list',
|
|
99
|
-
'endpoint:info': 'endpoint.info',
|
|
100
|
-
'endpoint:sendMessage': 'endpoint.send_message',
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
/** ADR 0016 demo allowlist subset available on Plugin Runtime Host. */
|
|
104
|
-
const DEMO_RPC_ALLOWLIST = new Set([
|
|
105
|
-
'ping',
|
|
106
|
-
'entries:get',
|
|
107
|
-
'pages:list',
|
|
108
|
-
'config:get',
|
|
109
|
-
'config:get-all',
|
|
110
|
-
'config:get-yaml',
|
|
111
|
-
'schema:get',
|
|
112
|
-
'schema:get-all',
|
|
113
|
-
'endpoint.list',
|
|
114
|
-
'endpoint.info',
|
|
115
|
-
'endpoint.send_message',
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
const DEMO_RPC_WRITE_BLOCKLIST = new Set([
|
|
119
|
-
'config:set',
|
|
120
|
-
'config:save-yaml',
|
|
121
|
-
'files:save',
|
|
122
|
-
'env:save',
|
|
123
|
-
'db:insert',
|
|
124
|
-
'db:update',
|
|
125
|
-
'db:delete',
|
|
126
|
-
'db:drop-table',
|
|
127
|
-
'db:kv:set',
|
|
128
|
-
'db:kv:delete',
|
|
129
|
-
'system:restart',
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
95
|
export function pickRpcReply(
|
|
133
96
|
message: RuntimeConsoleRpcMessage,
|
|
134
97
|
payloads: readonly RuntimeConsoleRpcReply[],
|
|
@@ -149,14 +112,23 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
149
112
|
const emit = (payload: RuntimeConsoleRpcReply) => {
|
|
150
113
|
payloads.push(payload);
|
|
151
114
|
};
|
|
152
|
-
|
|
115
|
+
message = normalizeConsoleRpcMessage(message);
|
|
116
|
+
const type = String(message.type ?? '');
|
|
153
117
|
const requestId = message.requestId as number | string | undefined;
|
|
154
118
|
|
|
155
|
-
|
|
119
|
+
if (ctx.authScope === 'demo') {
|
|
120
|
+
const denied = assertDemoConsoleRpcAllowed(type);
|
|
121
|
+
if (denied) {
|
|
122
|
+
emit({ requestId, error: denied });
|
|
123
|
+
return payloads;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extended RPC surface (cron/schedule、endpoint 社交/inbox)
|
|
156
128
|
if (ctx.extended) {
|
|
157
129
|
const extended = await dispatchExtendedConsoleRpc(
|
|
158
130
|
type,
|
|
159
|
-
message
|
|
131
|
+
message as Record<string, unknown>,
|
|
160
132
|
{ ...ctx.extended, fullScope: ctx.authScope === 'full' },
|
|
161
133
|
);
|
|
162
134
|
if (extended !== undefined) {
|
|
@@ -167,11 +139,6 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
167
139
|
}
|
|
168
140
|
}
|
|
169
141
|
|
|
170
|
-
if (ctx.authScope === 'demo' && (DEMO_RPC_WRITE_BLOCKLIST.has(type) || !DEMO_RPC_ALLOWLIST.has(type))) {
|
|
171
|
-
emit({ requestId, error: `Demo scope: RPC "${type}" is forbidden` });
|
|
172
|
-
return payloads;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
142
|
switch (type) {
|
|
176
143
|
case 'ping':
|
|
177
144
|
emit({ type: 'pong', requestId });
|
|
@@ -224,7 +191,9 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
224
191
|
}
|
|
225
192
|
case 'config:get': {
|
|
226
193
|
try {
|
|
227
|
-
|
|
194
|
+
// 兼容 console UI 的 data:{plugin} / data:{key} 形状
|
|
195
|
+
const data = (message.data ?? {}) as Record<string, unknown>;
|
|
196
|
+
const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
|
|
228
197
|
const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
|
|
229
198
|
emit({ requestId, data: key ? document[key] : document });
|
|
230
199
|
} catch (error) {
|
|
@@ -247,6 +216,7 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
247
216
|
return payloads;
|
|
248
217
|
}
|
|
249
218
|
await ctx.writeConfigYaml(yaml);
|
|
219
|
+
ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
|
|
250
220
|
emit({
|
|
251
221
|
requestId,
|
|
252
222
|
data: { success: true, message: '配置已保存,需重启生效' },
|
|
@@ -271,6 +241,12 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
271
241
|
return payloads;
|
|
272
242
|
}
|
|
273
243
|
const result = await ctx.setConfigKey(pluginName, message.data);
|
|
244
|
+
ctx.publishEvent?.('config:updated', {
|
|
245
|
+
pluginName,
|
|
246
|
+
keys: message.data && typeof message.data === 'object' && !Array.isArray(message.data)
|
|
247
|
+
? Object.keys(message.data as Record<string, unknown>)
|
|
248
|
+
: [],
|
|
249
|
+
});
|
|
274
250
|
emit({
|
|
275
251
|
requestId,
|
|
276
252
|
data: {
|
|
@@ -409,7 +385,10 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
409
385
|
}
|
|
410
386
|
case 'schema:get': {
|
|
411
387
|
try {
|
|
412
|
-
|
|
388
|
+
// 兼容 console UI 的 data:{plugin} / data:{pluginName} 形状(SDK 发顶层 pluginName)
|
|
389
|
+
const data = (message.data ?? {}) as Record<string, unknown>;
|
|
390
|
+
const candidate = message.pluginName ?? data.pluginName ?? data.plugin;
|
|
391
|
+
const pluginName = typeof candidate === 'string' ? candidate : undefined;
|
|
413
392
|
const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
|
|
414
393
|
emit({ requestId, data: schema });
|
|
415
394
|
} catch (error) {
|
|
@@ -613,10 +592,9 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
613
592
|
}
|
|
614
593
|
case 'endpoint.info': {
|
|
615
594
|
try {
|
|
616
|
-
const data =
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
const endpointId = String(data.$endpoint ?? data.endpointId ?? '');
|
|
595
|
+
const data = message as Record<string, unknown>;
|
|
596
|
+
const adapter = String(data.$adapter ?? '');
|
|
597
|
+
const endpointId = String(data.$endpoint ?? '');
|
|
620
598
|
if (!adapter || !endpointId) {
|
|
621
599
|
emit({ requestId, error: '$adapter and $endpoint required' });
|
|
622
600
|
return payloads;
|
|
@@ -641,14 +619,12 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
641
619
|
}
|
|
642
620
|
case 'endpoint.send_message': {
|
|
643
621
|
try {
|
|
644
|
-
const data =
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
const
|
|
650
|
-
const channelType = String(data.$channel_type ?? data.type ?? '');
|
|
651
|
-
const content = data.$content !== undefined ? data.$content : data.content;
|
|
622
|
+
const data = message as Record<string, unknown>;
|
|
623
|
+
const adapter = String(data.$adapter ?? '');
|
|
624
|
+
const endpointId = String(data.$endpoint ?? '');
|
|
625
|
+
const channelId = String(data.$channel_id ?? '');
|
|
626
|
+
const channelType = String(data.$channel_type ?? '');
|
|
627
|
+
const content = data.$content;
|
|
652
628
|
// channelType is optional: ImRuntime tolerates an empty value and
|
|
653
629
|
// targets the bare channel id (parity with the legacy RPC handler).
|
|
654
630
|
if (!adapter || !endpointId || !channelId || content === undefined) {
|
|
@@ -668,11 +644,11 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
668
644
|
channelId,
|
|
669
645
|
channelType,
|
|
670
646
|
content,
|
|
671
|
-
parent:
|
|
647
|
+
parent: data.$parent as
|
|
672
648
|
{ type?: string; id?: string; name?: string } | undefined,
|
|
673
649
|
});
|
|
674
650
|
// Legacy contract: { message_id }. Keep messageId for new callers.
|
|
675
|
-
emit({ requestId, data:
|
|
651
|
+
emit({ requestId, data: endpointSendResult(result.messageId) });
|
|
676
652
|
} catch (error) {
|
|
677
653
|
emit({
|
|
678
654
|
requestId,
|
|
@@ -687,6 +663,7 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
687
663
|
emit({ requestId, error: 'Process restart is not configured' });
|
|
688
664
|
return payloads;
|
|
689
665
|
}
|
|
666
|
+
ctx.publishEvent?.('system:restarting', { timestamp: Date.now() });
|
|
690
667
|
emit({ requestId, data: { success: true, message: '正在重启...' } });
|
|
691
668
|
// Delay so the HTTP response can flush (parity with legacy host-api).
|
|
692
669
|
setTimeout(() => {
|
package/src/http-host.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type RouteMeta,
|
|
23
23
|
} from './openapi.js';
|
|
24
24
|
import { isDemoHttpAllowed } from './console-rpc.js';
|
|
25
|
+
import { HttpBodyError } from './json-body.js';
|
|
25
26
|
|
|
26
27
|
const logger = getLogger('HttpHost');
|
|
27
28
|
|
|
@@ -276,6 +277,14 @@ export function createHttpHost(options: HttpHostOptions = {}): HttpHost {
|
|
|
276
277
|
response.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
277
278
|
await route.handler(request, response, url, auth.scope);
|
|
278
279
|
} catch (err) {
|
|
280
|
+
// 统一把请求体错误(400/413)映射回对应状态码,
|
|
281
|
+
// 避免个别端点未捕获时退化成 500 空响应。
|
|
282
|
+
if (err instanceof HttpBodyError) {
|
|
283
|
+
if (!response.headersSent) {
|
|
284
|
+
writeJson(response, err.statusCode, { success: false, error: err.message });
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
279
288
|
logger.warn(formatCompact({
|
|
280
289
|
op: 'http_handler_failed',
|
|
281
290
|
path: pathname,
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,10 @@ export {
|
|
|
19
19
|
type TokenRegistryConfig,
|
|
20
20
|
} from './token-registry.js';
|
|
21
21
|
export { timingSafeEqualString } from './timing-safe-equal.js';
|
|
22
|
+
export {
|
|
23
|
+
createConsoleEventHub,
|
|
24
|
+
type ConsoleEventHub,
|
|
25
|
+
} from './console-events.js';
|
|
22
26
|
export {
|
|
23
27
|
buildOpenApiDocument,
|
|
24
28
|
patternToOpenApiPath,
|
|
@@ -61,3 +65,9 @@ export {
|
|
|
61
65
|
type ConsoleRestCtx,
|
|
62
66
|
type ConsoleRestPagesOptions,
|
|
63
67
|
} from './console-rest-pages.js';
|
|
68
|
+
export {
|
|
69
|
+
dispatchExtendedConsoleRpc,
|
|
70
|
+
type ConsoleRpcExtendedCtx,
|
|
71
|
+
type ConsoleScheduleEngine,
|
|
72
|
+
type ExtendedRpcResult,
|
|
73
|
+
} from './console-rpc-extended.js';
|
package/src/json-body.ts
CHANGED
|
@@ -21,15 +21,21 @@ export async function readJsonBody<T = unknown>(
|
|
|
21
21
|
const limit = options.limit ?? 1_048_576;
|
|
22
22
|
const chunks: Buffer[] = [];
|
|
23
23
|
let size = 0;
|
|
24
|
+
let exceeded = false;
|
|
24
25
|
for await (const chunk of request) {
|
|
26
|
+
// 超限后继续 drain 到 end:提前 break 会 destroy 底层 socket,
|
|
27
|
+
// 客户端将永远收不到后面的 413 响应。
|
|
28
|
+
if (exceeded) continue;
|
|
25
29
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
26
30
|
size += buffer.length;
|
|
27
31
|
if (size > limit) {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
exceeded = true;
|
|
33
|
+
chunks.length = 0;
|
|
34
|
+
continue;
|
|
30
35
|
}
|
|
31
36
|
chunks.push(buffer);
|
|
32
37
|
}
|
|
38
|
+
if (exceeded) throw new HttpBodyError(`Request body exceeds ${limit} bytes`, 413);
|
|
33
39
|
if (chunks.length === 0) return undefined;
|
|
34
40
|
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
35
41
|
if (!raw) return undefined;
|
package/src/project-files.ts
CHANGED
|
@@ -6,6 +6,16 @@ export const FILE_MANAGER_ALLOWED = Object.freeze([
|
|
|
6
6
|
'src',
|
|
7
7
|
'plugins',
|
|
8
8
|
'client',
|
|
9
|
+
// Plugin Runtime convention directories (file-based discovery)
|
|
10
|
+
'commands',
|
|
11
|
+
'adapters',
|
|
12
|
+
'components',
|
|
13
|
+
'middlewares',
|
|
14
|
+
'tools',
|
|
15
|
+
'mcp',
|
|
16
|
+
'pages',
|
|
17
|
+
'skills',
|
|
18
|
+
'agents',
|
|
9
19
|
'package.json',
|
|
10
20
|
'tsconfig.json',
|
|
11
21
|
'zhin.config.yml',
|