@zhin.js/host-http 1.1.0 → 1.1.2

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.
Files changed (46) hide show
  1. package/README.md +5 -0
  2. package/lib/console-plugin-rpc.d.ts +3 -0
  3. package/lib/console-plugin-rpc.js +130 -0
  4. package/lib/console-rest-pages.d.ts +1 -1
  5. package/lib/console-rest-pages.js +11 -20
  6. package/lib/{console-rpc-extended.d.ts → console-rpc-extended/contracts.d.ts} +22 -21
  7. package/lib/console-rpc-extended/contracts.js +4 -0
  8. package/lib/console-rpc-extended/dispatcher.d.ts +2 -0
  9. package/lib/console-rpc-extended/dispatcher.js +92 -0
  10. package/lib/console-rpc-extended/endpoint-rpc.d.ts +24 -0
  11. package/lib/console-rpc-extended/endpoint-rpc.js +137 -0
  12. package/lib/console-rpc-extended/inbox-rpc.d.ts +23 -0
  13. package/lib/console-rpc-extended/inbox-rpc.js +292 -0
  14. package/lib/console-rpc-extended/index.d.ts +2 -0
  15. package/lib/console-rpc-extended/index.js +1 -0
  16. package/lib/console-rpc-extended/login-rpc.d.ts +4 -0
  17. package/lib/console-rpc-extended/login-rpc.js +46 -0
  18. package/lib/console-rpc-extended/rpc-values.d.ts +16 -0
  19. package/lib/console-rpc-extended/rpc-values.js +87 -0
  20. package/lib/console-rpc-extended/schedule-rpc.d.ts +6 -0
  21. package/lib/console-rpc-extended/schedule-rpc.js +109 -0
  22. package/lib/console-rpc-extended/workroom-rpc.d.ts +3 -0
  23. package/lib/console-rpc-extended/workroom-rpc.js +104 -0
  24. package/lib/console-rpc.d.ts +71 -15
  25. package/lib/console-rpc.js +116 -82
  26. package/lib/index.d.ts +3 -2
  27. package/lib/index.js +2 -1
  28. package/lib/marketplace-registry.d.ts +12 -0
  29. package/lib/marketplace-registry.js +45 -0
  30. package/package.json +4 -4
  31. package/src/console-plugin-rpc.ts +142 -0
  32. package/src/console-rest-pages.ts +14 -26
  33. package/src/console-rpc-extended/contracts.ts +155 -0
  34. package/src/console-rpc-extended/dispatcher.ts +114 -0
  35. package/src/console-rpc-extended/endpoint-rpc.ts +174 -0
  36. package/src/console-rpc-extended/inbox-rpc.ts +342 -0
  37. package/src/console-rpc-extended/index.ts +8 -0
  38. package/src/console-rpc-extended/login-rpc.ts +50 -0
  39. package/src/console-rpc-extended/rpc-values.ts +104 -0
  40. package/src/console-rpc-extended/schedule-rpc.ts +117 -0
  41. package/src/console-rpc-extended/workroom-rpc.ts +116 -0
  42. package/src/console-rpc.ts +189 -97
  43. package/src/index.ts +7 -2
  44. package/src/marketplace-registry.ts +58 -0
  45. package/lib/console-rpc-extended.js +0 -916
  46. package/src/console-rpc-extended.ts +0 -1192
@@ -0,0 +1,116 @@
1
+ import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
2
+ import {
3
+ errorMessage, optionalRpcText, optionalRpcTextArray, requiredRpcBoolean, requiredRpcInteger,
4
+ requiredRpcText, requiredRpcValue,
5
+ } from './rpc-values.js';
6
+
7
+ export async function mutateWorkroomProfile(
8
+ type: 'workroom.profile.status' | 'workroom.profile.bootstrap'
9
+ | 'workroom.profile.pack.publish' | 'workroom.profile.publish'
10
+ | 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
11
+ data: Record<string, unknown>,
12
+ ctx: ConsoleRpcExtendedCtx,
13
+ ): Promise<ExtendedRpcResult> {
14
+ const control = ctx.workroomProfileControl;
15
+ if (!control) return { error: 'Workroom Profile authority control is unavailable' };
16
+ for (const forbidden of [
17
+ 'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
18
+ ]) {
19
+ if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
20
+ }
21
+ if (type === 'workroom.profile.status') {
22
+ try {
23
+ return { data: await control.getPlanningStatus(
24
+ requiredRpcText(data.projectId, 'projectId'),
25
+ ctx.authenticatedPrincipal,
26
+ ) };
27
+ } catch (error) {
28
+ return { error: errorMessage(error) };
29
+ }
30
+ }
31
+ const principal = ctx.authenticatedPrincipal;
32
+ if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
33
+ try {
34
+ const operationId = requiredRpcText(data.operationId, 'operationId');
35
+ if (type === 'workroom.profile.bootstrap') {
36
+ const projectId = requiredRpcText(data.projectId, 'projectId');
37
+ const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
38
+ const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
39
+ return { data: await control.bootstrapPlanning(Object.freeze({
40
+ operationId,
41
+ projectId,
42
+ expectedRegistryRevision: requiredRpcInteger(
43
+ data.expectedRegistryRevision,
44
+ 'expectedRegistryRevision',
45
+ -1,
46
+ ),
47
+ ...(includeTools ? { includeTools } : {}),
48
+ ...(includeSkills ? { includeSkills } : {}),
49
+ }), principal) };
50
+ }
51
+ if (type === 'workroom.profile.pack.publish') {
52
+ return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
53
+ }
54
+ const projectId = requiredRpcText(data.projectId, 'projectId');
55
+ if (type === 'workroom.profile.policy.publish') {
56
+ const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
57
+ return { data: await control.publishPlanningPolicy(Object.freeze({
58
+ operationId,
59
+ projectId,
60
+ profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
61
+ revision: requiredRpcInteger(data.revision, 'revision'),
62
+ ...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
63
+ policy: requiredRpcValue(data.policy, 'policy'),
64
+ }), principal) };
65
+ }
66
+ const common = {
67
+ operationId,
68
+ projectId,
69
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
70
+ overlay: requiredRpcValue(data.overlay, 'overlay'),
71
+ activate: requiredRpcBoolean(data.activate, 'activate'),
72
+ };
73
+ return type === 'workroom.profile.rollback'
74
+ ? { data: await control.publishRollback(Object.freeze({
75
+ ...common,
76
+ restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
77
+ }), principal) }
78
+ : { data: await control.publishProfile(Object.freeze(common), principal) };
79
+ } catch (error) {
80
+ return { error: errorMessage(error) };
81
+ }
82
+ }
83
+
84
+ export async function mutateWorkroomKnowledge(
85
+ type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
86
+ data: Record<string, unknown>,
87
+ ctx: ConsoleRpcExtendedCtx,
88
+ ): Promise<ExtendedRpcResult> {
89
+ const control = ctx.workroomKnowledgeControl;
90
+ if (!control) return { error: 'Workroom Knowledge control is unavailable' };
91
+ for (const forbidden of [
92
+ 'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
93
+ 'governance', 'approval', 'authority', 'body', 'content',
94
+ ]) {
95
+ if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
96
+ }
97
+ try {
98
+ const projectId = requiredRpcText(data.projectId, 'projectId');
99
+ if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
100
+ const principal = ctx.authenticatedPrincipal;
101
+ if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
102
+ const common = {
103
+ operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
104
+ expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
105
+ };
106
+ if (type === 'workroom.knowledge.rollback') {
107
+ return { data: await control.rollback(Object.freeze({
108
+ ...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
109
+ }), principal) };
110
+ }
111
+ if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
112
+ return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
113
+ } catch (error) {
114
+ return { error: errorMessage(error) };
115
+ }
116
+ }
@@ -1,12 +1,14 @@
1
1
  import type { AuthScope } from './token-registry.js';
2
2
  import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
3
3
  import {
4
+ CONFIG_RPC,
5
+ ENDPOINT_RPC,
6
+ type ConsoleConfigSource,
4
7
  type ConsoleEndpointSummary,
5
8
  assertDemoConsoleRpcAllowed,
6
- endpointSendResult,
7
- normalizeConsoleRpcMessage,
8
9
  } from '@zhin.js/console-protocol';
9
- import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
10
+ import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended/index.js';
11
+ import { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
10
12
 
11
13
  /**
12
14
  * Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
@@ -23,17 +25,14 @@ export interface ConversationAddress {
23
25
  readonly threadId?: string;
24
26
  }
25
27
 
26
- /** `$channel_type`/`$channel_id`/`$parent` wire 字段 → 会话地址(RPC 边界组帧)。 */
28
+ /** Canonical Console wire fields → runtime conversation address. */
27
29
  function wireConversation(
28
30
  channelType: string,
29
31
  channelId: string,
30
32
  parent: unknown,
31
33
  ): 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
34
  const kind: ConversationAddress['kind'] =
36
- rawKind === 'group' || rawKind === 'channel' ? rawKind : 'private';
35
+ channelType === 'group' || channelType === 'channel' ? channelType : 'private';
37
36
  const wire = parent && typeof parent === 'object'
38
37
  ? parent as { readonly type?: unknown; readonly id?: unknown }
39
38
  : undefined;
@@ -41,7 +40,7 @@ function wireConversation(
41
40
  const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
42
41
  return {
43
42
  kind,
44
- id: prefixed ? prefixed[2]! : channelId,
43
+ id: channelId,
45
44
  ...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
46
45
  };
47
46
  }
@@ -54,6 +53,64 @@ export type RuntimeConsoleRpcMessage = {
54
53
 
55
54
  export type RuntimeConsoleRpcReply = Record<string, unknown>;
56
55
 
56
+ export interface PluginInstallPlan {
57
+ readonly packageName: string;
58
+ readonly instanceKey: string;
59
+ readonly alreadyDeclared: boolean;
60
+ readonly alreadyInstalled: boolean;
61
+ readonly restartRequired: boolean;
62
+ readonly changes: Readonly<{
63
+ readonly packageManifest: 'unchanged' | 'add-plugin';
64
+ readonly config: 'unchanged' | 'create-entry' | 'schema-required';
65
+ }>;
66
+ readonly warnings: readonly string[];
67
+ }
68
+
69
+ export interface PluginUninstallPlan {
70
+ readonly packageName: string;
71
+ readonly instanceKey: string;
72
+ readonly installed: boolean;
73
+ readonly declared: boolean;
74
+ readonly hasConfig: boolean;
75
+ readonly restartRequired: boolean;
76
+ }
77
+
78
+ export interface PluginUpdatePlan {
79
+ readonly packageName: string;
80
+ readonly instanceKey: string;
81
+ readonly currentVersion: string | null;
82
+ readonly targetVersion: string;
83
+ readonly installed: boolean;
84
+ readonly declared: boolean;
85
+ readonly alreadyCurrent: boolean;
86
+ readonly restartRequired: boolean;
87
+ }
88
+
89
+ export interface PluginManagementPort {
90
+ planInstall(packageName: string): Promise<PluginInstallPlan>;
91
+ install?(packageName: string, expectedRevision?: string): Promise<Readonly<{
92
+ readonly plan: PluginInstallPlan;
93
+ readonly restartRequired: boolean;
94
+ }>>;
95
+ planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
96
+ uninstall?(packageName: string, expectedRevision?: string): Promise<Readonly<{
97
+ readonly plan: PluginUninstallPlan;
98
+ readonly restartRequired: boolean;
99
+ }>>;
100
+ planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
101
+ update?(packageName: string, targetVersion: string, expectedRevision?: string): Promise<Readonly<{
102
+ readonly plan: PluginUpdatePlan;
103
+ readonly installedVersion: string;
104
+ readonly restartRequired: boolean;
105
+ }>>;
106
+ }
107
+
108
+ export interface PluginConfigValidation {
109
+ readonly valid: boolean;
110
+ readonly errors: readonly { readonly path: string; readonly message: string }[];
111
+ readonly missingEnv: readonly string[];
112
+ }
113
+
57
114
  export type RuntimeConsolePage = {
58
115
  readonly id: string;
59
116
  readonly localName: string;
@@ -68,17 +125,16 @@ export type RuntimeConsoleRpcContext = {
68
125
  readonly authScope: AuthScope;
69
126
  listPages(): Promise<readonly RuntimeConsolePage[]>;
70
127
  /** Optional project config accessors for read-only config RPCs. */
71
- readConfigYaml?(): Promise<string>;
72
- listPluginKeys?(): Promise<readonly string[]>;
128
+ readConfigSource?(): Promise<ConsoleConfigSource>;
73
129
  readConfigDocument?(): Promise<Record<string, unknown>>;
74
- /**
75
- * Full-scope write: replace project config file contents (YAML or JSON text).
76
- * Runtime Host does not hot-reload the process — callers should restart.
77
- */
78
- writeConfigYaml?(yaml: string): Promise<void>;
130
+ /** Full-scope, revision-checked replacement of the active Root config source. */
131
+ replaceConfigSource?(
132
+ source: string,
133
+ expectedRevision: string,
134
+ ): Promise<{ readonly revision: string; readonly restartRequired: boolean }>;
79
135
  /**
80
136
  * Full-scope write: set `document[pluginName] = data` and persist.
81
- * Returns whether a process restart is required (always true without ConfigFeature).
137
+ * Returns whether a process restart is required.
82
138
  */
83
139
  setConfigKey?(pluginName: string, data: unknown): Promise<{ restartRequired: boolean }>;
84
140
  /** Persist a declared child Plugin lifecycle state. Takes effect after restart. */
@@ -86,6 +142,10 @@ export type RuntimeConsoleRpcContext = {
86
142
  instanceKey: string,
87
143
  enabled: boolean,
88
144
  ): Promise<Readonly<{ disabled: readonly string[] }>>;
145
+ /** Read-only install planning shared by Console Web and CLI surfaces. */
146
+ pluginManagement?: PluginManagementPort;
147
+ validatePluginConfig?(pluginName: string, data: unknown): Promise<PluginConfigValidation>;
148
+ diagnosePlugin?(pluginName: string): Promise<unknown>;
89
149
  /** Atomic, revision-checked runtime Workroom Catalog read/write. */
90
150
  readWorkroomCatalog?(): Promise<Readonly<{
91
151
  agents: Readonly<Record<string, unknown>>;
@@ -109,8 +169,8 @@ export type RuntimeConsoleRpcContext = {
109
169
  getSchema?(pluginName?: string): Promise<unknown>;
110
170
  getAllSchemas?(): Promise<Record<string, unknown>>;
111
171
  /** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
112
- listEndpoints?(): Promise<readonly RuntimeEndpointSummary[]>;
113
- getEndpoint?(adapter: string, endpointKey: string): Promise<RuntimeEndpointSummary | null>;
172
+ listEndpoints?(): Promise<readonly ConsoleEndpointSummary[]>;
173
+ getEndpoint?(adapter: string, endpointKey: string): Promise<ConsoleEndpointSummary | null>;
114
174
  sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{ messageId: string }>;
115
175
  /** Optional Database host surface (CLI wires DatabaseHost from plugin-runtime). */
116
176
  dbInfo?(): Promise<RuntimeDatabaseInfo> | RuntimeDatabaseInfo;
@@ -127,11 +187,6 @@ export type RuntimeConsoleRpcContext = {
127
187
  extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
128
188
  };
129
189
 
130
- export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
131
-
132
- /** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
133
- export type RuntimeEndpointSummary = ConsoleEndpointSummary;
134
-
135
190
  export type RuntimeDatabaseInfo = {
136
191
  readonly dialect: string | null;
137
192
  readonly connected: boolean;
@@ -165,7 +220,6 @@ export async function dispatchRuntimeConsoleRpc(
165
220
  const emit = (payload: RuntimeConsoleRpcReply) => {
166
221
  payloads.push(payload);
167
222
  };
168
- message = normalizeConsoleRpcMessage(message);
169
223
  const type = String(message.type ?? '');
170
224
  const requestId = message.requestId as number | string | undefined;
171
225
 
@@ -192,6 +246,12 @@ export async function dispatchRuntimeConsoleRpc(
192
246
  }
193
247
  }
194
248
 
249
+ const pluginReply = await dispatchPluginConsoleRpc(type, message, ctx);
250
+ if (pluginReply !== undefined) {
251
+ emit(pluginReply);
252
+ return payloads;
253
+ }
254
+
195
255
  switch (type) {
196
256
  case 'ping':
197
257
  emit({ type: 'pong', requestId });
@@ -217,11 +277,12 @@ export async function dispatchRuntimeConsoleRpc(
217
277
  emit({ requestId, data: pages });
218
278
  return payloads;
219
279
  }
220
- case 'config:get-yaml': {
280
+ case CONFIG_RPC.GET_SOURCE: {
221
281
  try {
222
- const yaml = ctx.readConfigYaml ? await ctx.readConfigYaml() : '';
223
- const pluginKeys = ctx.listPluginKeys ? await ctx.listPluginKeys() : [];
224
- emit({ requestId, data: { yaml, pluginKeys } });
282
+ const config: ConsoleConfigSource = ctx.readConfigSource
283
+ ? await ctx.readConfigSource()
284
+ : { source: '', format: 'yaml', revision: '', configKeys: [] };
285
+ emit({ requestId, data: config });
225
286
  } catch (error) {
226
287
  emit({
227
288
  requestId,
@@ -230,7 +291,7 @@ export async function dispatchRuntimeConsoleRpc(
230
291
  }
231
292
  return payloads;
232
293
  }
233
- case 'config:get-all': {
294
+ case CONFIG_RPC.GET_ALL: {
234
295
  try {
235
296
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
236
297
  emit({ requestId, data: document });
@@ -242,11 +303,9 @@ export async function dispatchRuntimeConsoleRpc(
242
303
  }
243
304
  return payloads;
244
305
  }
245
- case 'config:get': {
306
+ case CONFIG_RPC.GET: {
246
307
  try {
247
- // 兼容 console UI 的 data:{plugin} / data:{key} 形状
248
- const data = (message.data ?? {}) as Record<string, unknown>;
249
- const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
308
+ const key = String(message.pluginName ?? '');
250
309
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
251
310
  emit({ requestId, data: key ? document[key] : document });
252
311
  } catch (error) {
@@ -290,22 +349,33 @@ export async function dispatchRuntimeConsoleRpc(
290
349
  }
291
350
  return payloads;
292
351
  }
293
- case 'config:save-yaml': {
352
+ case CONFIG_RPC.REPLACE_SOURCE: {
294
353
  try {
295
- const yaml = message.yaml;
296
- if (typeof yaml !== 'string') {
297
- emit({ requestId, error: 'yaml field is required' });
354
+ const source = message.source;
355
+ const expectedRevision = message.expectedRevision;
356
+ if (typeof source !== 'string') {
357
+ emit({ requestId, error: 'source field is required' });
358
+ return payloads;
359
+ }
360
+ if (typeof expectedRevision !== 'string' || !/^[a-f0-9]{64}$/u.test(expectedRevision)) {
361
+ emit({ requestId, error: 'expectedRevision is required' });
298
362
  return payloads;
299
363
  }
300
- if (!ctx.writeConfigYaml) {
364
+ if (!ctx.replaceConfigSource) {
301
365
  emit({ requestId, error: 'Config write is not configured' });
302
366
  return payloads;
303
367
  }
304
- await ctx.writeConfigYaml(yaml);
368
+ const result = await ctx.replaceConfigSource(source, expectedRevision);
305
369
  ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
306
370
  emit({
307
371
  requestId,
308
- data: { success: true, message: '配置已保存,需重启生效' },
372
+ data: {
373
+ success: true,
374
+ ...result,
375
+ message: result.restartRequired
376
+ ? '配置已保存,需重启进程才能生效'
377
+ : '配置已保存',
378
+ },
309
379
  });
310
380
  } catch (error) {
311
381
  emit({
@@ -315,7 +385,7 @@ export async function dispatchRuntimeConsoleRpc(
315
385
  }
316
386
  return payloads;
317
387
  }
318
- case 'config:set': {
388
+ case CONFIG_RPC.SET: {
319
389
  try {
320
390
  const pluginName = message.pluginName;
321
391
  if (typeof pluginName !== 'string' || !pluginName) {
@@ -326,6 +396,17 @@ export async function dispatchRuntimeConsoleRpc(
326
396
  emit({ requestId, error: 'Config write is not configured' });
327
397
  return payloads;
328
398
  }
399
+ if (ctx.validatePluginConfig) {
400
+ const validation = await ctx.validatePluginConfig(pluginName, message.data);
401
+ if (!validation.valid) {
402
+ emit({
403
+ requestId,
404
+ error: '配置校验失败',
405
+ data: validation,
406
+ });
407
+ return payloads;
408
+ }
409
+ }
329
410
  const result = await ctx.setConfigKey(pluginName, message.data);
330
411
  ctx.publishEvent?.('config:updated', {
331
412
  pluginName,
@@ -471,10 +552,9 @@ export async function dispatchRuntimeConsoleRpc(
471
552
  }
472
553
  case 'schema:get': {
473
554
  try {
474
- // 兼容 console UI 的 data:{plugin} / data:{pluginName} 形状(SDK 发顶层 pluginName)
475
- const data = (message.data ?? {}) as Record<string, unknown>;
476
- const candidate = message.pluginName ?? data.pluginName ?? data.plugin;
477
- const pluginName = typeof candidate === 'string' ? candidate : undefined;
555
+ const pluginName = typeof message.pluginName === 'string'
556
+ ? message.pluginName
557
+ : undefined;
478
558
  const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
479
559
  emit({ requestId, data: schema });
480
560
  } catch (error) {
@@ -678,11 +758,11 @@ export async function dispatchRuntimeConsoleRpc(
678
758
  }
679
759
  case 'endpoint.info': {
680
760
  try {
681
- const data = message as Record<string, unknown>;
682
- const adapter = String(data.$adapter ?? '');
683
- const endpointKey = String(data.$endpoint ?? '');
761
+ const data = recordField(message.data) ?? message;
762
+ const adapter = String(data.adapter ?? '');
763
+ const endpointKey = String(data.endpointKey ?? '');
684
764
  if (!adapter || !endpointKey) {
685
- emit({ requestId, error: '$adapter and $endpoint required' });
765
+ emit({ requestId, error: 'adapter and endpointKey are required' });
686
766
  return payloads;
687
767
  }
688
768
  if (!ctx.getEndpoint) {
@@ -703,19 +783,67 @@ export async function dispatchRuntimeConsoleRpc(
703
783
  }
704
784
  return payloads;
705
785
  }
786
+ case ENDPOINT_RPC.TEST: {
787
+ const data = recordField(message.data) ?? message;
788
+ const adapter = stringField(data, 'adapter');
789
+ const endpointKey = stringField(data, 'endpointKey');
790
+ if (!adapter || !endpointKey) {
791
+ emit({ requestId, error: 'adapter and endpointKey are required' });
792
+ return payloads;
793
+ }
794
+ if (!ctx.getEndpoint) {
795
+ emit({ requestId, error: 'Endpoint registry is not configured' });
796
+ return payloads;
797
+ }
798
+ const startedAt = Date.now();
799
+ try {
800
+ const endpoint = await ctx.getEndpoint(adapter, endpointKey);
801
+ if (!endpoint) {
802
+ emit({ requestId, data: {
803
+ reachable: false,
804
+ connected: false,
805
+ phase: 'unconfigured',
806
+ latencyMs: Date.now() - startedAt,
807
+ message: 'Endpoint 未注册到当前 generation',
808
+ } });
809
+ return payloads;
810
+ }
811
+ emit({ requestId, data: {
812
+ reachable: endpoint.connected && endpoint.status === 'online',
813
+ connected: endpoint.connected,
814
+ status: endpoint.status,
815
+ phase: endpoint.phase ?? (endpoint.connected ? 'online' : 'pending'),
816
+ pendingLogin: endpoint.pendingLogin ?? false,
817
+ latencyMs: Date.now() - startedAt,
818
+ message: endpoint.connected
819
+ ? 'Endpoint 已连接'
820
+ : endpoint.pendingLogin
821
+ ? 'Endpoint 等待登录操作'
822
+ : 'Endpoint 当前离线',
823
+ } });
824
+ } catch (error) {
825
+ emit({ requestId, data: {
826
+ reachable: false,
827
+ connected: false,
828
+ phase: 'failed',
829
+ latencyMs: Date.now() - startedAt,
830
+ message: error instanceof Error ? error.message : String(error),
831
+ } });
832
+ }
833
+ return payloads;
834
+ }
706
835
  case 'endpoint.send_message': {
707
836
  try {
708
- const data = message as Record<string, unknown>;
709
- const adapter = String(data.$adapter ?? '');
710
- const endpointKey = String(data.$endpoint ?? '');
711
- const channelId = String(data.$channel_id ?? '');
712
- const channelType = String(data.$channel_type ?? '');
713
- const content = data.$content;
714
- // channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
715
- if (!adapter || !endpointKey || !channelId || content === undefined) {
837
+ const data = recordField(message.data) ?? message;
838
+ const adapter = String(data.adapter ?? '');
839
+ const endpointKey = String(data.endpointKey ?? '');
840
+ const channelId = String(data.channelId ?? data.id ?? '');
841
+ const channelType = String(data.channelType ?? data.type ?? '');
842
+ const content = data.content;
843
+ if (!adapter || !endpointKey || !channelId || !channelType || content === undefined) {
716
844
  emit({
717
845
  requestId,
718
- error: '$adapter, $endpoint, $channel_id, $content required',
846
+ error: 'adapter, endpointKey, channelId, channelType, and content are required',
719
847
  });
720
848
  return payloads;
721
849
  }
@@ -726,11 +854,10 @@ export async function dispatchRuntimeConsoleRpc(
726
854
  const result = await ctx.sendEndpointMessage({
727
855
  adapter,
728
856
  endpointKey,
729
- conversation: wireConversation(channelType, channelId, data.$parent),
857
+ conversation: wireConversation(channelType, channelId, data.parent),
730
858
  content,
731
859
  });
732
- // Legacy contract: { message_id }. Keep messageId for new callers.
733
- emit({ requestId, data: endpointSendResult(result.messageId) });
860
+ emit({ requestId, data: { messageId: result.messageId } });
734
861
  } catch (error) {
735
862
  emit({
736
863
  requestId,
@@ -761,41 +888,6 @@ export async function dispatchRuntimeConsoleRpc(
761
888
  }
762
889
  return payloads;
763
890
  }
764
- case 'plugin:set-enabled': {
765
- try {
766
- const instanceKey = typeof message.instanceKey === 'string' ? message.instanceKey : '';
767
- const enabled = message.enabled;
768
- if (!instanceKey || typeof enabled !== 'boolean') {
769
- emit({ requestId, error: 'instanceKey and boolean enabled are required' });
770
- return payloads;
771
- }
772
- if (!ctx.setPluginEnabled) {
773
- emit({ requestId, error: 'Plugin lifecycle management is not configured' });
774
- return payloads;
775
- }
776
- const lifecycle = await ctx.setPluginEnabled(instanceKey, enabled);
777
- ctx.publishEvent?.('plugin:lifecycle-updated', { instanceKey, enabled });
778
- emit({
779
- requestId,
780
- data: {
781
- success: true,
782
- instanceKey,
783
- enabled,
784
- disabled: lifecycle.disabled,
785
- restartRequired: true,
786
- message: `${instanceKey} 已${enabled ? '启用' : '停用'},Host 正在重启`,
787
- },
788
- });
789
- if (ctx.requestRestart) {
790
- setTimeout(() => {
791
- void Promise.resolve(ctx.requestRestart?.()).catch(() => undefined);
792
- }, 500);
793
- }
794
- } catch (error) {
795
- emit({ requestId, error: error instanceof Error ? error.message : String(error) });
796
- }
797
- return payloads;
798
- }
799
891
  default:
800
892
  if (type.startsWith('db:')) {
801
893
  emit({
package/src/index.ts CHANGED
@@ -55,8 +55,13 @@ export {
55
55
  type RuntimeConsoleRpcMessage,
56
56
  type RuntimeConsoleRpcReply,
57
57
  type RuntimeEndpointSendInput,
58
- type RuntimeEndpointSummary,
58
+ type PluginInstallPlan,
59
+ type PluginUninstallPlan,
60
+ type PluginUpdatePlan,
61
+ type PluginManagementPort,
62
+ type PluginConfigValidation,
59
63
  } from './console-rpc.js';
64
+ export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
60
65
  export {
61
66
  buildProjectFileTree,
62
67
  FILE_MANAGER_ALLOWED,
@@ -82,4 +87,4 @@ export {
82
87
  type ConsoleWorkroomProfileControlPort,
83
88
  type ConsoleWorkroomKnowledgeControlPort,
84
89
  type ExtendedRpcResult,
85
- } from './console-rpc-extended.js';
90
+ } from './console-rpc-extended/index.js';
@@ -0,0 +1,58 @@
1
+ export interface MarketplaceRegistryOptions {
2
+ readonly fetchFn: typeof fetch;
3
+ readonly url: string;
4
+ readonly ttlMs: number;
5
+ readonly now?: () => number;
6
+ }
7
+
8
+ interface MarketplaceSnapshot {
9
+ readonly plugins: readonly unknown[];
10
+ readonly expiresAt: number;
11
+ }
12
+
13
+ /** Host-owned plugin registry reader with TTL caching and request coalescing. */
14
+ export class MarketplaceRegistry {
15
+ readonly #fetch: typeof fetch;
16
+ readonly #url: string;
17
+ readonly #ttlMs: number;
18
+ readonly #now: () => number;
19
+ #snapshot?: MarketplaceSnapshot;
20
+ #inflight?: Promise<readonly unknown[]>;
21
+
22
+ constructor(options: MarketplaceRegistryOptions) {
23
+ this.#fetch = options.fetchFn;
24
+ this.#url = options.url;
25
+ this.#ttlMs = options.ttlMs;
26
+ this.#now = options.now ?? Date.now;
27
+ }
28
+
29
+ list(): Promise<readonly unknown[]> {
30
+ const snapshot = this.#snapshot;
31
+ if (snapshot && this.#now() < snapshot.expiresAt) {
32
+ return Promise.resolve(snapshot.plugins);
33
+ }
34
+ if (this.#inflight) return this.#inflight;
35
+
36
+ const request = this.#load();
37
+ this.#inflight = request;
38
+ const clearInflight = () => {
39
+ if (this.#inflight === request) this.#inflight = undefined;
40
+ };
41
+ void request.then(clearInflight, clearInflight);
42
+ return request;
43
+ }
44
+
45
+ async #load(): Promise<readonly unknown[]> {
46
+ const response = await this.#fetch(this.#url, {
47
+ signal: AbortSignal.timeout(30_000),
48
+ });
49
+ if (!response.ok) throw new Error(`plugins.json fetch failed: ${response.status}`);
50
+ const json = (await response.json()) as { plugins?: unknown[] };
51
+ const plugins = Object.freeze([...(json.plugins ?? [])]);
52
+ this.#snapshot = Object.freeze({
53
+ plugins,
54
+ expiresAt: this.#now() + this.#ttlMs,
55
+ });
56
+ return plugins;
57
+ }
58
+ }