@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.
@@ -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(() => render({ requester, name, props: body.props ?? {}, signal: controller.signal }));
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 output = await raceWithAbort(renderPromise, controller.signal);
571
- assertPreviewOutput(output, options.componentPreviewMaxBytes ?? 256 * 1024);
572
- writeJson(response, 200, { success: true, data: { requester, name, output } });
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`];
@@ -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';
@@ -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
- 'mcp',
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.4",
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.7",
22
- "@zhin.js/host-http-contract": "1.1.0",
23
- "@zhin.js/logger": "1.1.1",
24
- "@zhin.js/schedule": "1.1.2",
25
- "@zhin.js/plugin-runtime": "1.1.12"
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": "^20.19.0 || >=22.12.0"
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
- render({ requester, name, props: body.props ?? {}, signal: controller.signal }));
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 output = await raceWithAbort(
766
+ const preview = await raceWithAbort(
758
767
  renderPromise,
759
768
  controller.signal,
760
769
  );
761
- assertPreviewOutput(output, options.componentPreviewMaxBytes ?? 256 * 1024);
762
- writeJson(response, 200, { success: true, data: { requester, name, output } });
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',
@@ -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';
@@ -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
- 'mcp',
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',