@zhin.js/host-http 1.1.4 → 1.1.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-plugin-rpc.js +37 -4
- package/lib/console-rest-pages.d.ts +13 -0
- package/lib/console-rest-pages.js +12 -4
- package/lib/console-rpc-extended/inbox-rpc.js +42 -0
- package/lib/console-rpc.d.ts +58 -3
- package/lib/index.d.ts +1 -1
- package/lib/project-files.js +14 -1
- package/package.json +7 -8
- package/src/console-plugin-rpc.ts +29 -1
- package/src/console-rest-pages.ts +14 -5
- package/src/console-rpc-extended/inbox-rpc.ts +34 -0
- package/src/console-rpc.ts +58 -3
- package/src/index.ts +3 -0
- package/src/project-files.ts +14 -1
|
@@ -30,6 +30,39 @@ export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
case PLUGIN_RPC.INSPECT:
|
|
34
|
+
case PLUGIN_RPC.APPLY:
|
|
35
|
+
case PLUGIN_RPC.PLAN_VERIFICATION:
|
|
36
|
+
case PLUGIN_RPC.VERIFY: {
|
|
37
|
+
const name = stringField(message, 'instanceKey') || stringField(message, 'packageName');
|
|
38
|
+
if (!name)
|
|
39
|
+
return { requestId, error: 'instanceKey or packageName is required' };
|
|
40
|
+
const port = ctx.pluginManagement;
|
|
41
|
+
if (!port)
|
|
42
|
+
return { requestId, error: 'Plugin management is not configured' };
|
|
43
|
+
if (type === PLUGIN_RPC.APPLY || type === PLUGIN_RPC.PLAN_VERIFICATION || type === PLUGIN_RPC.VERIFY) {
|
|
44
|
+
if (ctx.authScope !== 'full')
|
|
45
|
+
return { requestId, error: 'Full scope required' };
|
|
46
|
+
}
|
|
47
|
+
if (type === PLUGIN_RPC.INSPECT && port.inspect)
|
|
48
|
+
return { requestId, data: await port.inspect(name) };
|
|
49
|
+
if (type === PLUGIN_RPC.APPLY && port.apply)
|
|
50
|
+
return { requestId, data: await port.apply(name, optionalString(message.expectedProjectRevision)) };
|
|
51
|
+
if (type === PLUGIN_RPC.PLAN_VERIFICATION && port.verificationPlan)
|
|
52
|
+
return { requestId, data: await port.verificationPlan(name) };
|
|
53
|
+
if (type === PLUGIN_RPC.VERIFY && port.verify) {
|
|
54
|
+
const checkId = stringField(message, 'checkId');
|
|
55
|
+
if (!checkId)
|
|
56
|
+
return { requestId, error: 'checkId is required' };
|
|
57
|
+
const expectedGeneration = message.expectedGeneration;
|
|
58
|
+
const projectRevision = stringField(message, 'expectedProjectRevision');
|
|
59
|
+
const checkRevision = stringField(message, 'expectedCheckRevision');
|
|
60
|
+
if (!(expectedGeneration === null || typeof expectedGeneration === 'number' && Number.isSafeInteger(expectedGeneration) && expectedGeneration >= 0) || !projectRevision || !checkRevision)
|
|
61
|
+
return { requestId, error: 'expectedGeneration, expectedProjectRevision and expectedCheckRevision are required' };
|
|
62
|
+
return { requestId, data: await port.verify(name, checkId, expectedGeneration, projectRevision, checkRevision) };
|
|
63
|
+
}
|
|
64
|
+
return { requestId, error: 'Plugin workflow operation is not configured' };
|
|
65
|
+
}
|
|
33
66
|
case PLUGIN_RPC.PLAN_INSTALL: {
|
|
34
67
|
const packageName = stringField(message, 'packageName');
|
|
35
68
|
if (!packageName)
|
|
@@ -45,7 +78,7 @@ export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
|
45
78
|
if (!ctx.pluginManagement?.install) {
|
|
46
79
|
return { requestId, error: 'Plugin installation is not configured' };
|
|
47
80
|
}
|
|
48
|
-
const result = await ctx.pluginManagement.install(packageName, optionalString(message.expectedRevision));
|
|
81
|
+
const result = await ctx.pluginManagement.install(packageName, optionalString(message.expectedRevision), optionalString(message.expectedProjectRevision));
|
|
49
82
|
ctx.publishEvent?.('plugin:installed', { packageName, restartRequired: result.restartRequired });
|
|
50
83
|
return { requestId, data: { success: true, ...result } };
|
|
51
84
|
}
|
|
@@ -66,7 +99,7 @@ export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
|
66
99
|
if (!ctx.pluginManagement?.uninstall) {
|
|
67
100
|
return { requestId, error: 'Plugin uninstall is not configured' };
|
|
68
101
|
}
|
|
69
|
-
const result = await ctx.pluginManagement.uninstall(packageName, optionalString(message.expectedRevision));
|
|
102
|
+
const result = await ctx.pluginManagement.uninstall(packageName, optionalString(message.expectedRevision), optionalString(message.expectedProjectRevision));
|
|
70
103
|
ctx.publishEvent?.('plugin:uninstalled', { packageName, restartRequired: result.restartRequired });
|
|
71
104
|
return { requestId, data: { success: true, ...result } };
|
|
72
105
|
}
|
|
@@ -89,7 +122,7 @@ export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
|
89
122
|
}
|
|
90
123
|
if (!ctx.pluginManagement?.update)
|
|
91
124
|
return { requestId, error: 'Plugin update is not configured' };
|
|
92
|
-
const result = await ctx.pluginManagement.update(packageName, targetVersion, optionalString(message.expectedRevision));
|
|
125
|
+
const result = await ctx.pluginManagement.update(packageName, targetVersion, optionalString(message.expectedRevision), optionalString(message.expectedProjectRevision));
|
|
93
126
|
ctx.publishEvent?.('plugin:updated', {
|
|
94
127
|
packageName,
|
|
95
128
|
version: result.installedVersion,
|
|
@@ -118,7 +151,7 @@ export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
|
118
151
|
}
|
|
119
152
|
}
|
|
120
153
|
catch (error) {
|
|
121
|
-
return { requestId, error: error instanceof Error ? error.message : String(error) };
|
|
154
|
+
return { requestId, error: error instanceof Error ? error.message : String(error), ...(error instanceof Error && 'completedStages' in error ? { data: { completedStages: error.completedStages, ...('state' in error ? { state: error.state } : {}) } } : {}) };
|
|
122
155
|
}
|
|
123
156
|
}
|
|
124
157
|
function stringField(message, key) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HttpHost } from './http-host.js';
|
|
2
|
+
import type { ConsoleMessageEventData } from '@zhin.js/console-protocol';
|
|
2
3
|
/** 与 basic/cli 接线方对齐的上下文约定。 */
|
|
3
4
|
export interface ConsoleRestCtx {
|
|
4
5
|
/** false = demo 部署:只读 GET 放行,写操作一律 403。 */
|
|
@@ -47,6 +48,18 @@ export interface ConsoleAgentIntrospection {
|
|
|
47
48
|
props: unknown;
|
|
48
49
|
signal: AbortSignal;
|
|
49
50
|
}>): Promise<unknown>;
|
|
51
|
+
previewComponentDelivery?(input: Readonly<{
|
|
52
|
+
requester: string;
|
|
53
|
+
name: string;
|
|
54
|
+
props: unknown;
|
|
55
|
+
target: unknown;
|
|
56
|
+
signal: AbortSignal;
|
|
57
|
+
}>): Promise<Readonly<{
|
|
58
|
+
output: unknown;
|
|
59
|
+
rendering: NonNullable<ConsoleMessageEventData['rendering']>;
|
|
60
|
+
generation: number;
|
|
61
|
+
note: string;
|
|
62
|
+
}>>;
|
|
50
63
|
}
|
|
51
64
|
/** Session tree 门面 — 形状对齐 agent 包 SessionTreeRuntimeHandle。 */
|
|
52
65
|
export interface ConsoleAgentSessionTree {
|
|
@@ -544,6 +544,7 @@ function registerIntrospectionRoutes(route, base, ctx, options) {
|
|
|
544
544
|
}
|
|
545
545
|
const lease = ctx.acquireAgentRuntime?.();
|
|
546
546
|
const render = lease?.value.introspection?.renderComponent;
|
|
547
|
+
const previewDelivery = lease?.value.introspection?.previewComponentDelivery;
|
|
547
548
|
if (!lease || !render) {
|
|
548
549
|
lease?.release();
|
|
549
550
|
writeJson(response, 503, { success: false, error: 'Component Runtime 未就绪' });
|
|
@@ -561,15 +562,22 @@ function registerIntrospectionRoutes(route, base, ctx, options) {
|
|
|
561
562
|
activeComponentPreviews -= 1;
|
|
562
563
|
lease.release();
|
|
563
564
|
};
|
|
564
|
-
const renderPromise = Promise.resolve().then(() =>
|
|
565
|
+
const renderPromise = Promise.resolve().then(() => {
|
|
566
|
+
const input = { requester, name, props: body.props ?? {}, signal: controller.signal };
|
|
567
|
+
if (body.target === undefined)
|
|
568
|
+
return render(input).then(output => ({ output }));
|
|
569
|
+
if (!previewDelivery)
|
|
570
|
+
throw new Error('Target-aware Component preview 未就绪');
|
|
571
|
+
return previewDelivery({ ...input, target: body.target });
|
|
572
|
+
});
|
|
565
573
|
// A timed-out implementation may ignore AbortSignal. Keep its generation
|
|
566
574
|
// lease until the real operation settles; the bounded concurrency gate
|
|
567
575
|
// prevents non-cooperative previews from pinning unbounded generations.
|
|
568
576
|
void renderPromise.then(release, release);
|
|
569
577
|
try {
|
|
570
|
-
const
|
|
571
|
-
assertPreviewOutput(
|
|
572
|
-
writeJson(response, 200, { success: true, data: { requester, name,
|
|
578
|
+
const preview = await raceWithAbort(renderPromise, controller.signal);
|
|
579
|
+
assertPreviewOutput(preview, options.componentPreviewMaxBytes ?? 256 * 1024);
|
|
580
|
+
writeJson(response, 200, { success: true, data: { requester, name, ...preview } });
|
|
573
581
|
}
|
|
574
582
|
catch (error) {
|
|
575
583
|
const status = error instanceof ComponentPreviewTimeoutError
|
|
@@ -222,6 +222,7 @@ export function mapNoticeRow(row) {
|
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
function mapMessageRow(row) {
|
|
225
|
+
const observation = readMessageObservation(row.raw);
|
|
225
226
|
return {
|
|
226
227
|
id: row.id,
|
|
227
228
|
platformMessageId: row.platform_message_id,
|
|
@@ -230,8 +231,49 @@ function mapMessageRow(row) {
|
|
|
230
231
|
raw: row.raw,
|
|
231
232
|
timestamp: row.created_at,
|
|
232
233
|
channel: channelFromStoredRow(row),
|
|
234
|
+
...observation,
|
|
233
235
|
};
|
|
234
236
|
}
|
|
237
|
+
function isStoredConversation(value) {
|
|
238
|
+
if (!value || typeof value !== 'object')
|
|
239
|
+
return false;
|
|
240
|
+
const conversation = value;
|
|
241
|
+
const endpoint = conversation.endpoint;
|
|
242
|
+
if (!endpoint || typeof endpoint !== 'object'
|
|
243
|
+
|| typeof endpoint.id !== 'string' || typeof endpoint.adapter !== 'string'
|
|
244
|
+
|| typeof conversation.kind !== 'string' || typeof conversation.id !== 'string')
|
|
245
|
+
return false;
|
|
246
|
+
if (conversation.threadId !== undefined && typeof conversation.threadId !== 'string')
|
|
247
|
+
return false;
|
|
248
|
+
if (conversation.parent !== undefined) {
|
|
249
|
+
const parent = conversation.parent;
|
|
250
|
+
if (!parent || typeof parent !== 'object' || typeof parent.kind !== 'string' || typeof parent.id !== 'string')
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
/** Older rows have no observation; do not invent their direction or diagnostics. */
|
|
256
|
+
function readMessageObservation(raw) {
|
|
257
|
+
if (typeof raw !== 'string')
|
|
258
|
+
return {};
|
|
259
|
+
try {
|
|
260
|
+
const value = JSON.parse(raw);
|
|
261
|
+
if (!value || typeof value !== 'object')
|
|
262
|
+
return {};
|
|
263
|
+
const entry = value;
|
|
264
|
+
if (entry.$console !== 'zhin.message-observation/1'
|
|
265
|
+
|| (entry.direction !== 'inbound' && entry.direction !== 'outbound'))
|
|
266
|
+
return {};
|
|
267
|
+
return { direction: entry.direction,
|
|
268
|
+
...(isStoredConversation(entry.conversation) ? { conversation: entry.conversation } : {}),
|
|
269
|
+
...(entry.direction === 'outbound' && entry.rendering && typeof entry.rendering === 'object'
|
|
270
|
+
? { rendering: entry.rendering } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
235
277
|
function actorFromStoredRow(row, field) {
|
|
236
278
|
const id = String(row[`${field}_id`] ?? '');
|
|
237
279
|
const name = row[`${field}_name`];
|
package/lib/console-rpc.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type RuntimeConsoleRpcMessage = {
|
|
|
23
23
|
};
|
|
24
24
|
export type RuntimeConsoleRpcReply = Record<string, unknown>;
|
|
25
25
|
export interface PluginInstallPlan {
|
|
26
|
+
readonly projectRevision?: string;
|
|
27
|
+
readonly configurationRevision?: string | null;
|
|
26
28
|
readonly packageName: string;
|
|
27
29
|
readonly instanceKey: string;
|
|
28
30
|
readonly alreadyDeclared: boolean;
|
|
@@ -35,6 +37,8 @@ export interface PluginInstallPlan {
|
|
|
35
37
|
readonly warnings: readonly string[];
|
|
36
38
|
}
|
|
37
39
|
export interface PluginUninstallPlan {
|
|
40
|
+
readonly projectRevision?: string;
|
|
41
|
+
readonly configurationRevision?: string | null;
|
|
38
42
|
readonly packageName: string;
|
|
39
43
|
readonly instanceKey: string;
|
|
40
44
|
readonly installed: boolean;
|
|
@@ -43,6 +47,8 @@ export interface PluginUninstallPlan {
|
|
|
43
47
|
readonly restartRequired: boolean;
|
|
44
48
|
}
|
|
45
49
|
export interface PluginUpdatePlan {
|
|
50
|
+
readonly projectRevision?: string;
|
|
51
|
+
readonly configurationRevision?: string | null;
|
|
46
52
|
readonly packageName: string;
|
|
47
53
|
readonly instanceKey: string;
|
|
48
54
|
readonly currentVersion: string | null;
|
|
@@ -52,25 +58,74 @@ export interface PluginUpdatePlan {
|
|
|
52
58
|
readonly alreadyCurrent: boolean;
|
|
53
59
|
readonly restartRequired: boolean;
|
|
54
60
|
}
|
|
61
|
+
export interface PluginManagementState {
|
|
62
|
+
readonly packageName: string;
|
|
63
|
+
readonly instanceKey: string;
|
|
64
|
+
readonly installed: boolean;
|
|
65
|
+
readonly plugin: boolean;
|
|
66
|
+
readonly declared: boolean;
|
|
67
|
+
readonly version: string | null;
|
|
68
|
+
readonly revision: string | null;
|
|
69
|
+
readonly projectRevision: string;
|
|
70
|
+
readonly status: 'not-installed' | 'installed' | 'declared' | 'missing-config' | 'pending-restart' | 'enabled' | 'degraded';
|
|
71
|
+
readonly dependencies: 'unchecked' | 'passed' | 'failed';
|
|
72
|
+
readonly validation: PluginConfigValidation | null;
|
|
73
|
+
readonly generation: number | null;
|
|
74
|
+
readonly published: boolean;
|
|
75
|
+
readonly readiness: 'unknown' | 'ready' | 'pending' | 'degraded';
|
|
76
|
+
readonly restartRequired: boolean;
|
|
77
|
+
readonly disabled: boolean;
|
|
78
|
+
readonly issues: readonly string[];
|
|
79
|
+
/** Publication is never proof of a successful business action. */
|
|
80
|
+
readonly verification: 'not-configured' | 'available';
|
|
81
|
+
}
|
|
82
|
+
export interface PluginVerificationPlan {
|
|
83
|
+
readonly packageName: string;
|
|
84
|
+
readonly instanceKey: string;
|
|
85
|
+
readonly generation: number | null;
|
|
86
|
+
readonly projectRevision: string;
|
|
87
|
+
readonly checkRevision: string;
|
|
88
|
+
readonly checks: readonly import('@zhin.js/plugin-runtime').PluginVerificationCheck[];
|
|
89
|
+
}
|
|
90
|
+
export interface PluginVerificationResult {
|
|
91
|
+
readonly packageName: string;
|
|
92
|
+
readonly instanceKey: string;
|
|
93
|
+
readonly checkId: string;
|
|
94
|
+
readonly generation: number | null;
|
|
95
|
+
readonly current: boolean;
|
|
96
|
+
readonly status: 'passed' | 'failed' | 'not-configured';
|
|
97
|
+
readonly scope: 'local' | 'platform' | null;
|
|
98
|
+
readonly summary: string;
|
|
99
|
+
readonly evidence: readonly string[];
|
|
100
|
+
}
|
|
55
101
|
export interface PluginManagementPort {
|
|
102
|
+
inspect?(packageName: string): Promise<PluginManagementState>;
|
|
103
|
+
apply?(packageName: string, expectedProjectRevision?: string): Promise<{
|
|
104
|
+
readonly requested: boolean;
|
|
105
|
+
readonly state: PluginManagementState;
|
|
106
|
+
}>;
|
|
107
|
+
verificationPlan?(packageName: string): Promise<PluginVerificationPlan>;
|
|
108
|
+
verify?(packageName: string, checkId: string, expectedGeneration: number | null, expectedProjectRevision: string, expectedCheckRevision: string): Promise<PluginVerificationResult>;
|
|
56
109
|
planInstall(packageName: string): Promise<PluginInstallPlan>;
|
|
57
|
-
install?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
110
|
+
install?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
58
111
|
readonly plan: PluginInstallPlan;
|
|
59
112
|
readonly restartRequired: boolean;
|
|
60
113
|
}>>;
|
|
61
114
|
planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
|
|
62
|
-
uninstall?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
115
|
+
uninstall?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
63
116
|
readonly plan: PluginUninstallPlan;
|
|
64
117
|
readonly restartRequired: boolean;
|
|
65
118
|
}>>;
|
|
66
119
|
planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
|
|
67
|
-
update?(packageName: string, targetVersion: string, expectedRevision?: string): Promise<Readonly<{
|
|
120
|
+
update?(packageName: string, targetVersion: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
68
121
|
readonly plan: PluginUpdatePlan;
|
|
69
122
|
readonly installedVersion: string;
|
|
70
123
|
readonly restartRequired: boolean;
|
|
71
124
|
}>>;
|
|
72
125
|
}
|
|
73
126
|
export interface PluginConfigValidation {
|
|
127
|
+
/** False means only environment-reference checks ran; Runtime must validate TS-only declarations. */
|
|
128
|
+
readonly schemaAvailable?: boolean;
|
|
74
129
|
readonly valid: boolean;
|
|
75
130
|
readonly errors: readonly {
|
|
76
131
|
readonly path: string;
|
package/lib/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { timingSafeEqualString } from './timing-safe-equal.js';
|
|
|
5
5
|
export { createConsoleEventHub, consoleEventHubToken, type ConsoleEventHub, } from './console-events.js';
|
|
6
6
|
export { buildOpenApiDocument, patternToOpenApiPath, routeRequiresBearerAuth, type BuildOpenApiOptions, type ListedRoute, type OpenApiParameter, type RouteMeta, } from './openapi.js';
|
|
7
7
|
export { HttpBodyError, readJsonBody, } from './json-body.js';
|
|
8
|
-
export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, type RuntimeConsolePage, type RuntimeConsoleRpcContext, type RuntimeConsoleRpcMessage, type RuntimeConsoleRpcReply, type RuntimeEndpointSendInput, type PluginInstallPlan, type PluginUninstallPlan, type PluginUpdatePlan, type PluginManagementPort, type PluginConfigValidation, } from './console-rpc.js';
|
|
8
|
+
export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, type RuntimeConsolePage, type RuntimeConsoleRpcContext, type RuntimeConsoleRpcMessage, type RuntimeConsoleRpcReply, type RuntimeEndpointSendInput, type PluginInstallPlan, type PluginUninstallPlan, type PluginUpdatePlan, type PluginManagementPort, type PluginManagementState, type PluginVerificationResult, type PluginVerificationPlan, type PluginConfigValidation, } from './console-rpc.js';
|
|
9
9
|
export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
10
10
|
export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, type FileTreeNode, } from './project-files.js';
|
|
11
11
|
export { registerConsoleRestPages, type ConsoleAgentIntrospection, type ConsoleAgentRuntime, type ConsoleAgentSessionTree, type ConsoleRestCtx, type ConsoleRestPagesOptions, } from './console-rest-pages.js';
|
package/lib/project-files.js
CHANGED
|
@@ -10,14 +10,27 @@ export const FILE_MANAGER_ALLOWED = Object.freeze([
|
|
|
10
10
|
'adapters',
|
|
11
11
|
'components',
|
|
12
12
|
'middlewares',
|
|
13
|
+
'handlers',
|
|
14
|
+
'schedules',
|
|
15
|
+
'hooks',
|
|
16
|
+
'prompt-sections',
|
|
13
17
|
'tools',
|
|
14
|
-
'
|
|
18
|
+
'mcps',
|
|
15
19
|
'pages',
|
|
16
20
|
'skills',
|
|
17
21
|
'agents',
|
|
18
22
|
'package.json',
|
|
23
|
+
'plugin.ts',
|
|
24
|
+
'plugin.tsx',
|
|
25
|
+
'plugin.js',
|
|
26
|
+
'plugin.mjs',
|
|
27
|
+
'plugin.cjs',
|
|
28
|
+
'schema.json',
|
|
29
|
+
'AGENTS.md',
|
|
19
30
|
'tsconfig.json',
|
|
20
31
|
'zhin.config.yml',
|
|
32
|
+
'zhin.config.yaml',
|
|
33
|
+
'zhin.config.json',
|
|
21
34
|
'config.yml',
|
|
22
35
|
'config.yaml',
|
|
23
36
|
'config.json',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/host-http",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
|
-
"development": "./src/index.ts",
|
|
12
11
|
"import": "./lib/index.js"
|
|
13
12
|
}
|
|
14
13
|
},
|
|
@@ -18,11 +17,11 @@
|
|
|
18
17
|
],
|
|
19
18
|
"dependencies": {
|
|
20
19
|
"ws": "^8.21.1",
|
|
21
|
-
"@zhin.js/console-protocol": "1.1.
|
|
22
|
-
"@zhin.js/host-http-contract": "1.1.
|
|
23
|
-
"@zhin.js/logger": "1.1.
|
|
24
|
-
"@zhin.js/schedule": "1.1.
|
|
25
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
20
|
+
"@zhin.js/console-protocol": "1.1.9",
|
|
21
|
+
"@zhin.js/host-http-contract": "1.1.1",
|
|
22
|
+
"@zhin.js/logger": "1.1.2",
|
|
23
|
+
"@zhin.js/schedule": "1.1.3",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.14"
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
27
|
"@types/node": "^26.1.2",
|
|
@@ -30,7 +29,7 @@
|
|
|
30
29
|
"typescript": "^6.0.3"
|
|
31
30
|
},
|
|
32
31
|
"engines": {
|
|
33
|
-
"node": "
|
|
32
|
+
"node": ">=22.19.0"
|
|
34
33
|
},
|
|
35
34
|
"repository": {
|
|
36
35
|
"type": "git",
|
|
@@ -40,6 +40,31 @@ export async function dispatchPluginConsoleRpc(
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
case PLUGIN_RPC.INSPECT:
|
|
44
|
+
case PLUGIN_RPC.APPLY:
|
|
45
|
+
case PLUGIN_RPC.PLAN_VERIFICATION:
|
|
46
|
+
case PLUGIN_RPC.VERIFY: {
|
|
47
|
+
const name = stringField(message, 'instanceKey') || stringField(message, 'packageName');
|
|
48
|
+
if (!name) return { requestId, error: 'instanceKey or packageName is required' };
|
|
49
|
+
const port = ctx.pluginManagement;
|
|
50
|
+
if (!port) return { requestId, error: 'Plugin management is not configured' };
|
|
51
|
+
if (type === PLUGIN_RPC.APPLY || type === PLUGIN_RPC.PLAN_VERIFICATION || type === PLUGIN_RPC.VERIFY) {
|
|
52
|
+
if (ctx.authScope !== 'full') return { requestId, error: 'Full scope required' };
|
|
53
|
+
}
|
|
54
|
+
if (type === PLUGIN_RPC.INSPECT && port.inspect) return { requestId, data: await port.inspect(name) };
|
|
55
|
+
if (type === PLUGIN_RPC.APPLY && port.apply) return { requestId, data: await port.apply(name, optionalString(message.expectedProjectRevision)) };
|
|
56
|
+
if (type === PLUGIN_RPC.PLAN_VERIFICATION && port.verificationPlan) return { requestId, data: await port.verificationPlan(name) };
|
|
57
|
+
if (type === PLUGIN_RPC.VERIFY && port.verify) {
|
|
58
|
+
const checkId = stringField(message, 'checkId');
|
|
59
|
+
if (!checkId) return { requestId, error: 'checkId is required' };
|
|
60
|
+
const expectedGeneration = message.expectedGeneration;
|
|
61
|
+
const projectRevision = stringField(message, 'expectedProjectRevision');
|
|
62
|
+
const checkRevision = stringField(message, 'expectedCheckRevision');
|
|
63
|
+
if (!(expectedGeneration === null || typeof expectedGeneration === 'number' && Number.isSafeInteger(expectedGeneration) && expectedGeneration >= 0) || !projectRevision || !checkRevision) return { requestId, error: 'expectedGeneration, expectedProjectRevision and expectedCheckRevision are required' };
|
|
64
|
+
return { requestId, data: await port.verify(name, checkId, expectedGeneration, projectRevision, checkRevision) };
|
|
65
|
+
}
|
|
66
|
+
return { requestId, error: 'Plugin workflow operation is not configured' };
|
|
67
|
+
}
|
|
43
68
|
case PLUGIN_RPC.PLAN_INSTALL: {
|
|
44
69
|
const packageName = stringField(message, 'packageName');
|
|
45
70
|
if (!packageName) return { requestId, error: 'packageName is required' };
|
|
@@ -55,6 +80,7 @@ export async function dispatchPluginConsoleRpc(
|
|
|
55
80
|
const result = await ctx.pluginManagement.install(
|
|
56
81
|
packageName,
|
|
57
82
|
optionalString(message.expectedRevision),
|
|
83
|
+
optionalString(message.expectedProjectRevision),
|
|
58
84
|
);
|
|
59
85
|
ctx.publishEvent?.('plugin:installed', { packageName, restartRequired: result.restartRequired });
|
|
60
86
|
return { requestId, data: { success: true, ...result } };
|
|
@@ -78,6 +104,7 @@ export async function dispatchPluginConsoleRpc(
|
|
|
78
104
|
const result = await ctx.pluginManagement.uninstall(
|
|
79
105
|
packageName,
|
|
80
106
|
optionalString(message.expectedRevision),
|
|
107
|
+
optionalString(message.expectedProjectRevision),
|
|
81
108
|
);
|
|
82
109
|
ctx.publishEvent?.('plugin:uninstalled', { packageName, restartRequired: result.restartRequired });
|
|
83
110
|
return { requestId, data: { success: true, ...result } };
|
|
@@ -104,6 +131,7 @@ export async function dispatchPluginConsoleRpc(
|
|
|
104
131
|
packageName,
|
|
105
132
|
targetVersion,
|
|
106
133
|
optionalString(message.expectedRevision),
|
|
134
|
+
optionalString(message.expectedProjectRevision),
|
|
107
135
|
);
|
|
108
136
|
ctx.publishEvent?.('plugin:updated', {
|
|
109
137
|
packageName,
|
|
@@ -128,7 +156,7 @@ export async function dispatchPluginConsoleRpc(
|
|
|
128
156
|
return undefined;
|
|
129
157
|
}
|
|
130
158
|
} catch (error) {
|
|
131
|
-
return { requestId, error: error instanceof Error ? error.message : String(error) };
|
|
159
|
+
return { requestId, error: error instanceof Error ? error.message : String(error), ...(error instanceof Error && 'completedStages' in error ? { data: { completedStages: error.completedStages, ...('state' in error ? { state: error.state } : {}) } } : {}) };
|
|
132
160
|
}
|
|
133
161
|
}
|
|
134
162
|
|
|
@@ -13,6 +13,7 @@ import type { HttpHost } from './http-host.js';
|
|
|
13
13
|
import { HttpBodyError, readJsonBody } from './json-body.js';
|
|
14
14
|
import { MarketplaceRegistry } from './marketplace-registry.js';
|
|
15
15
|
import type { AuthScope } from './token-registry.js';
|
|
16
|
+
import type { ConsoleMessageEventData } from '@zhin.js/console-protocol';
|
|
16
17
|
|
|
17
18
|
/** 与 basic/cli 接线方对齐的上下文约定。 */
|
|
18
19
|
export interface ConsoleRestCtx {
|
|
@@ -60,6 +61,9 @@ export interface ConsoleAgentIntrospection {
|
|
|
60
61
|
props: unknown;
|
|
61
62
|
signal: AbortSignal;
|
|
62
63
|
}>): Promise<unknown>;
|
|
64
|
+
previewComponentDelivery?(input: Readonly<{
|
|
65
|
+
requester: string; name: string; props: unknown; target: unknown; signal: AbortSignal;
|
|
66
|
+
}>): Promise<Readonly<{ output: unknown; rendering: NonNullable<ConsoleMessageEventData['rendering']>; generation: number; note: string }>>;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** Session tree 门面 — 形状对齐 agent 包 SessionTreeRuntimeHandle。 */
|
|
@@ -729,6 +733,7 @@ function registerIntrospectionRoutes(
|
|
|
729
733
|
}
|
|
730
734
|
const lease = ctx.acquireAgentRuntime?.();
|
|
731
735
|
const render = lease?.value.introspection?.renderComponent;
|
|
736
|
+
const previewDelivery = lease?.value.introspection?.previewComponentDelivery;
|
|
732
737
|
if (!lease || !render) {
|
|
733
738
|
lease?.release();
|
|
734
739
|
writeJson(response, 503, { success: false, error: 'Component Runtime 未就绪' });
|
|
@@ -747,19 +752,23 @@ function registerIntrospectionRoutes(
|
|
|
747
752
|
activeComponentPreviews -= 1;
|
|
748
753
|
lease.release();
|
|
749
754
|
};
|
|
750
|
-
const renderPromise = Promise.resolve().then(() =>
|
|
751
|
-
|
|
755
|
+
const renderPromise = Promise.resolve().then(() => {
|
|
756
|
+
const input = { requester, name, props: body.props ?? {}, signal: controller.signal };
|
|
757
|
+
if (body.target === undefined) return render(input).then(output => ({ output }));
|
|
758
|
+
if (!previewDelivery) throw new Error('Target-aware Component preview 未就绪');
|
|
759
|
+
return previewDelivery({ ...input, target: body.target });
|
|
760
|
+
});
|
|
752
761
|
// A timed-out implementation may ignore AbortSignal. Keep its generation
|
|
753
762
|
// lease until the real operation settles; the bounded concurrency gate
|
|
754
763
|
// prevents non-cooperative previews from pinning unbounded generations.
|
|
755
764
|
void renderPromise.then(release, release);
|
|
756
765
|
try {
|
|
757
|
-
const
|
|
766
|
+
const preview = await raceWithAbort(
|
|
758
767
|
renderPromise,
|
|
759
768
|
controller.signal,
|
|
760
769
|
);
|
|
761
|
-
assertPreviewOutput(
|
|
762
|
-
writeJson(response, 200, { success: true, data: { requester, name,
|
|
770
|
+
assertPreviewOutput(preview, options.componentPreviewMaxBytes ?? 256 * 1024);
|
|
771
|
+
writeJson(response, 200, { success: true, data: { requester, name, ...preview } });
|
|
763
772
|
} catch (error) {
|
|
764
773
|
const status = error instanceof ComponentPreviewTimeoutError
|
|
765
774
|
? 408
|
|
@@ -259,6 +259,7 @@ export function mapNoticeRow(row: Record<string, unknown>): Record<string, unkno
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
function mapMessageRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
262
|
+
const observation = readMessageObservation(row.raw);
|
|
262
263
|
return {
|
|
263
264
|
id: row.id,
|
|
264
265
|
platformMessageId: row.platform_message_id,
|
|
@@ -267,9 +268,42 @@ function mapMessageRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
|
267
268
|
raw: row.raw,
|
|
268
269
|
timestamp: row.created_at,
|
|
269
270
|
channel: channelFromStoredRow(row),
|
|
271
|
+
...observation,
|
|
270
272
|
};
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
function isStoredConversation(value: unknown): boolean {
|
|
276
|
+
if (!value || typeof value !== 'object') return false;
|
|
277
|
+
const conversation = value as Record<string, unknown>;
|
|
278
|
+
const endpoint = conversation.endpoint as Record<string, unknown> | null | undefined;
|
|
279
|
+
if (!endpoint || typeof endpoint !== 'object'
|
|
280
|
+
|| typeof endpoint.id !== 'string' || typeof endpoint.adapter !== 'string'
|
|
281
|
+
|| typeof conversation.kind !== 'string' || typeof conversation.id !== 'string') return false;
|
|
282
|
+
if (conversation.threadId !== undefined && typeof conversation.threadId !== 'string') return false;
|
|
283
|
+
if (conversation.parent !== undefined) {
|
|
284
|
+
const parent = conversation.parent as Record<string, unknown> | null;
|
|
285
|
+
if (!parent || typeof parent !== 'object' || typeof parent.kind !== 'string' || typeof parent.id !== 'string') return false;
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Older rows have no observation; do not invent their direction or diagnostics. */
|
|
291
|
+
function readMessageObservation(raw: unknown): Record<string, unknown> {
|
|
292
|
+
if (typeof raw !== 'string') return {};
|
|
293
|
+
try {
|
|
294
|
+
const value: unknown = JSON.parse(raw);
|
|
295
|
+
if (!value || typeof value !== 'object') return {};
|
|
296
|
+
const entry = value as Record<string, unknown>;
|
|
297
|
+
if (entry.$console !== 'zhin.message-observation/1'
|
|
298
|
+
|| (entry.direction !== 'inbound' && entry.direction !== 'outbound')) return {};
|
|
299
|
+
return { direction: entry.direction,
|
|
300
|
+
...(isStoredConversation(entry.conversation) ? { conversation: entry.conversation } : {}),
|
|
301
|
+
...(entry.direction === 'outbound' && entry.rendering && typeof entry.rendering === 'object'
|
|
302
|
+
? { rendering: entry.rendering } : {}),
|
|
303
|
+
};
|
|
304
|
+
} catch { return {}; }
|
|
305
|
+
}
|
|
306
|
+
|
|
273
307
|
function actorFromStoredRow(
|
|
274
308
|
row: Record<string, unknown>,
|
|
275
309
|
field: 'actor' | 'sender',
|
package/src/console-rpc.ts
CHANGED
|
@@ -54,6 +54,8 @@ export type RuntimeConsoleRpcMessage = {
|
|
|
54
54
|
export type RuntimeConsoleRpcReply = Record<string, unknown>;
|
|
55
55
|
|
|
56
56
|
export interface PluginInstallPlan {
|
|
57
|
+
readonly projectRevision?: string;
|
|
58
|
+
readonly configurationRevision?: string | null;
|
|
57
59
|
readonly packageName: string;
|
|
58
60
|
readonly instanceKey: string;
|
|
59
61
|
readonly alreadyDeclared: boolean;
|
|
@@ -67,6 +69,8 @@ export interface PluginInstallPlan {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export interface PluginUninstallPlan {
|
|
72
|
+
readonly projectRevision?: string;
|
|
73
|
+
readonly configurationRevision?: string | null;
|
|
70
74
|
readonly packageName: string;
|
|
71
75
|
readonly instanceKey: string;
|
|
72
76
|
readonly installed: boolean;
|
|
@@ -76,6 +80,8 @@ export interface PluginUninstallPlan {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
export interface PluginUpdatePlan {
|
|
83
|
+
readonly projectRevision?: string;
|
|
84
|
+
readonly configurationRevision?: string | null;
|
|
79
85
|
readonly packageName: string;
|
|
80
86
|
readonly instanceKey: string;
|
|
81
87
|
readonly currentVersion: string | null;
|
|
@@ -86,19 +92,66 @@ export interface PluginUpdatePlan {
|
|
|
86
92
|
readonly restartRequired: boolean;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
export interface PluginManagementState {
|
|
96
|
+
readonly packageName: string;
|
|
97
|
+
readonly instanceKey: string;
|
|
98
|
+
readonly installed: boolean;
|
|
99
|
+
readonly plugin: boolean;
|
|
100
|
+
readonly declared: boolean;
|
|
101
|
+
readonly version: string | null;
|
|
102
|
+
readonly revision: string | null;
|
|
103
|
+
readonly projectRevision: string;
|
|
104
|
+
readonly status: 'not-installed' | 'installed' | 'declared' | 'missing-config' | 'pending-restart' | 'enabled' | 'degraded';
|
|
105
|
+
readonly dependencies: 'unchecked' | 'passed' | 'failed';
|
|
106
|
+
readonly validation: PluginConfigValidation | null;
|
|
107
|
+
readonly generation: number | null;
|
|
108
|
+
readonly published: boolean;
|
|
109
|
+
readonly readiness: 'unknown' | 'ready' | 'pending' | 'degraded';
|
|
110
|
+
readonly restartRequired: boolean;
|
|
111
|
+
readonly disabled: boolean;
|
|
112
|
+
readonly issues: readonly string[];
|
|
113
|
+
/** Publication is never proof of a successful business action. */
|
|
114
|
+
readonly verification: 'not-configured' | 'available';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface PluginVerificationPlan {
|
|
118
|
+
readonly packageName: string;
|
|
119
|
+
readonly instanceKey: string;
|
|
120
|
+
readonly generation: number | null;
|
|
121
|
+
readonly projectRevision: string;
|
|
122
|
+
readonly checkRevision: string;
|
|
123
|
+
readonly checks: readonly import('@zhin.js/plugin-runtime').PluginVerificationCheck[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PluginVerificationResult {
|
|
127
|
+
readonly packageName: string;
|
|
128
|
+
readonly instanceKey: string;
|
|
129
|
+
readonly checkId: string;
|
|
130
|
+
readonly generation: number | null;
|
|
131
|
+
readonly current: boolean;
|
|
132
|
+
readonly status: 'passed' | 'failed' | 'not-configured';
|
|
133
|
+
readonly scope: 'local' | 'platform' | null;
|
|
134
|
+
readonly summary: string;
|
|
135
|
+
readonly evidence: readonly string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
export interface PluginManagementPort {
|
|
139
|
+
inspect?(packageName: string): Promise<PluginManagementState>;
|
|
140
|
+
apply?(packageName: string, expectedProjectRevision?: string): Promise<{ readonly requested: boolean; readonly state: PluginManagementState }>;
|
|
141
|
+
verificationPlan?(packageName: string): Promise<PluginVerificationPlan>;
|
|
142
|
+
verify?(packageName: string, checkId: string, expectedGeneration: number | null, expectedProjectRevision: string, expectedCheckRevision: string): Promise<PluginVerificationResult>;
|
|
90
143
|
planInstall(packageName: string): Promise<PluginInstallPlan>;
|
|
91
|
-
install?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
144
|
+
install?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
92
145
|
readonly plan: PluginInstallPlan;
|
|
93
146
|
readonly restartRequired: boolean;
|
|
94
147
|
}>>;
|
|
95
148
|
planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
|
|
96
|
-
uninstall?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
149
|
+
uninstall?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
97
150
|
readonly plan: PluginUninstallPlan;
|
|
98
151
|
readonly restartRequired: boolean;
|
|
99
152
|
}>>;
|
|
100
153
|
planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
|
|
101
|
-
update?(packageName: string, targetVersion: string, expectedRevision?: string): Promise<Readonly<{
|
|
154
|
+
update?(packageName: string, targetVersion: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
102
155
|
readonly plan: PluginUpdatePlan;
|
|
103
156
|
readonly installedVersion: string;
|
|
104
157
|
readonly restartRequired: boolean;
|
|
@@ -106,6 +159,8 @@ export interface PluginManagementPort {
|
|
|
106
159
|
}
|
|
107
160
|
|
|
108
161
|
export interface PluginConfigValidation {
|
|
162
|
+
/** False means only environment-reference checks ran; Runtime must validate TS-only declarations. */
|
|
163
|
+
readonly schemaAvailable?: boolean;
|
|
109
164
|
readonly valid: boolean;
|
|
110
165
|
readonly errors: readonly { readonly path: string; readonly message: string }[];
|
|
111
166
|
readonly missingEnv: readonly string[];
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,9 @@ export {
|
|
|
59
59
|
type PluginUninstallPlan,
|
|
60
60
|
type PluginUpdatePlan,
|
|
61
61
|
type PluginManagementPort,
|
|
62
|
+
type PluginManagementState,
|
|
63
|
+
type PluginVerificationResult,
|
|
64
|
+
type PluginVerificationPlan,
|
|
62
65
|
type PluginConfigValidation,
|
|
63
66
|
} from './console-rpc.js';
|
|
64
67
|
export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
package/src/project-files.ts
CHANGED
|
@@ -11,14 +11,27 @@ export const FILE_MANAGER_ALLOWED = Object.freeze([
|
|
|
11
11
|
'adapters',
|
|
12
12
|
'components',
|
|
13
13
|
'middlewares',
|
|
14
|
+
'handlers',
|
|
15
|
+
'schedules',
|
|
16
|
+
'hooks',
|
|
17
|
+
'prompt-sections',
|
|
14
18
|
'tools',
|
|
15
|
-
'
|
|
19
|
+
'mcps',
|
|
16
20
|
'pages',
|
|
17
21
|
'skills',
|
|
18
22
|
'agents',
|
|
19
23
|
'package.json',
|
|
24
|
+
'plugin.ts',
|
|
25
|
+
'plugin.tsx',
|
|
26
|
+
'plugin.js',
|
|
27
|
+
'plugin.mjs',
|
|
28
|
+
'plugin.cjs',
|
|
29
|
+
'schema.json',
|
|
30
|
+
'AGENTS.md',
|
|
20
31
|
'tsconfig.json',
|
|
21
32
|
'zhin.config.yml',
|
|
33
|
+
'zhin.config.yaml',
|
|
34
|
+
'zhin.config.json',
|
|
22
35
|
'config.yml',
|
|
23
36
|
'config.yaml',
|
|
24
37
|
'config.json',
|