@zhin.js/host-http 1.0.17 → 1.1.1

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 +6 -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 +5 -5
  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,104 @@
1
+ import { errorMessage, optionalRpcText, optionalRpcTextArray, requiredRpcBoolean, requiredRpcInteger, requiredRpcText, requiredRpcValue, } from './rpc-values.js';
2
+ export async function mutateWorkroomProfile(type, data, ctx) {
3
+ const control = ctx.workroomProfileControl;
4
+ if (!control)
5
+ return { error: 'Workroom Profile authority control is unavailable' };
6
+ for (const forbidden of [
7
+ 'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
8
+ ]) {
9
+ if (forbidden in data)
10
+ return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
11
+ }
12
+ if (type === 'workroom.profile.status') {
13
+ try {
14
+ return { data: await control.getPlanningStatus(requiredRpcText(data.projectId, 'projectId'), ctx.authenticatedPrincipal) };
15
+ }
16
+ catch (error) {
17
+ return { error: errorMessage(error) };
18
+ }
19
+ }
20
+ const principal = ctx.authenticatedPrincipal;
21
+ if (!principal)
22
+ return { error: 'Workroom Profile control requires an authenticated principal' };
23
+ try {
24
+ const operationId = requiredRpcText(data.operationId, 'operationId');
25
+ if (type === 'workroom.profile.bootstrap') {
26
+ const projectId = requiredRpcText(data.projectId, 'projectId');
27
+ const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
28
+ const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
29
+ return { data: await control.bootstrapPlanning(Object.freeze({
30
+ operationId,
31
+ projectId,
32
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
33
+ ...(includeTools ? { includeTools } : {}),
34
+ ...(includeSkills ? { includeSkills } : {}),
35
+ }), principal) };
36
+ }
37
+ if (type === 'workroom.profile.pack.publish') {
38
+ return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
39
+ }
40
+ const projectId = requiredRpcText(data.projectId, 'projectId');
41
+ if (type === 'workroom.profile.policy.publish') {
42
+ const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
43
+ return { data: await control.publishPlanningPolicy(Object.freeze({
44
+ operationId,
45
+ projectId,
46
+ profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
47
+ revision: requiredRpcInteger(data.revision, 'revision'),
48
+ ...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
49
+ policy: requiredRpcValue(data.policy, 'policy'),
50
+ }), principal) };
51
+ }
52
+ const common = {
53
+ operationId,
54
+ projectId,
55
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
56
+ overlay: requiredRpcValue(data.overlay, 'overlay'),
57
+ activate: requiredRpcBoolean(data.activate, 'activate'),
58
+ };
59
+ return type === 'workroom.profile.rollback'
60
+ ? { data: await control.publishRollback(Object.freeze({
61
+ ...common,
62
+ restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
63
+ }), principal) }
64
+ : { data: await control.publishProfile(Object.freeze(common), principal) };
65
+ }
66
+ catch (error) {
67
+ return { error: errorMessage(error) };
68
+ }
69
+ }
70
+ export async function mutateWorkroomKnowledge(type, data, ctx) {
71
+ const control = ctx.workroomKnowledgeControl;
72
+ if (!control)
73
+ return { error: 'Workroom Knowledge control is unavailable' };
74
+ for (const forbidden of [
75
+ 'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
76
+ 'governance', 'approval', 'authority', 'body', 'content',
77
+ ]) {
78
+ if (forbidden in data)
79
+ return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
80
+ }
81
+ try {
82
+ const projectId = requiredRpcText(data.projectId, 'projectId');
83
+ if (type === 'workroom.knowledge.get')
84
+ return { data: await control.read(projectId) };
85
+ const principal = ctx.authenticatedPrincipal;
86
+ if (!principal)
87
+ return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
88
+ const common = {
89
+ operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
90
+ expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
91
+ };
92
+ if (type === 'workroom.knowledge.rollback') {
93
+ return { data: await control.rollback(Object.freeze({
94
+ ...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
95
+ }), principal) };
96
+ }
97
+ if (!Array.isArray(data.entries))
98
+ throw new Error('entries is invalid');
99
+ return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
100
+ }
101
+ catch (error) {
102
+ return { error: errorMessage(error) };
103
+ }
104
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AuthScope } from './token-registry.js';
2
2
  import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
3
- import { type ConsoleEndpointSummary } from '@zhin.js/console-protocol';
4
- import { type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
3
+ import { type ConsoleConfigSource, type ConsoleEndpointSummary } from '@zhin.js/console-protocol';
4
+ import { type ConsoleRpcExtendedCtx } from './console-rpc-extended/index.js';
5
5
  /**
6
6
  * Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
7
7
  * `Omit<ConversationRef,'endpoint'>`;host/http 架构分层不允许依赖
@@ -22,6 +22,62 @@ export type RuntimeConsoleRpcMessage = {
22
22
  readonly [key: string]: unknown;
23
23
  };
24
24
  export type RuntimeConsoleRpcReply = Record<string, unknown>;
25
+ export interface PluginInstallPlan {
26
+ readonly packageName: string;
27
+ readonly instanceKey: string;
28
+ readonly alreadyDeclared: boolean;
29
+ readonly alreadyInstalled: boolean;
30
+ readonly restartRequired: boolean;
31
+ readonly changes: Readonly<{
32
+ readonly packageManifest: 'unchanged' | 'add-plugin';
33
+ readonly config: 'unchanged' | 'create-entry' | 'schema-required';
34
+ }>;
35
+ readonly warnings: readonly string[];
36
+ }
37
+ export interface PluginUninstallPlan {
38
+ readonly packageName: string;
39
+ readonly instanceKey: string;
40
+ readonly installed: boolean;
41
+ readonly declared: boolean;
42
+ readonly hasConfig: boolean;
43
+ readonly restartRequired: boolean;
44
+ }
45
+ export interface PluginUpdatePlan {
46
+ readonly packageName: string;
47
+ readonly instanceKey: string;
48
+ readonly currentVersion: string | null;
49
+ readonly targetVersion: string;
50
+ readonly installed: boolean;
51
+ readonly declared: boolean;
52
+ readonly alreadyCurrent: boolean;
53
+ readonly restartRequired: boolean;
54
+ }
55
+ export interface PluginManagementPort {
56
+ planInstall(packageName: string): Promise<PluginInstallPlan>;
57
+ install?(packageName: string, expectedRevision?: string): Promise<Readonly<{
58
+ readonly plan: PluginInstallPlan;
59
+ readonly restartRequired: boolean;
60
+ }>>;
61
+ planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
62
+ uninstall?(packageName: string, expectedRevision?: string): Promise<Readonly<{
63
+ readonly plan: PluginUninstallPlan;
64
+ readonly restartRequired: boolean;
65
+ }>>;
66
+ planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
67
+ update?(packageName: string, targetVersion: string, expectedRevision?: string): Promise<Readonly<{
68
+ readonly plan: PluginUpdatePlan;
69
+ readonly installedVersion: string;
70
+ readonly restartRequired: boolean;
71
+ }>>;
72
+ }
73
+ export interface PluginConfigValidation {
74
+ readonly valid: boolean;
75
+ readonly errors: readonly {
76
+ readonly path: string;
77
+ readonly message: string;
78
+ }[];
79
+ readonly missingEnv: readonly string[];
80
+ }
25
81
  export type RuntimeConsolePage = {
26
82
  readonly id: string;
27
83
  readonly localName: string;
@@ -35,17 +91,16 @@ export type RuntimeConsoleRpcContext = {
35
91
  readonly authScope: AuthScope;
36
92
  listPages(): Promise<readonly RuntimeConsolePage[]>;
37
93
  /** Optional project config accessors for read-only config RPCs. */
38
- readConfigYaml?(): Promise<string>;
39
- listPluginKeys?(): Promise<readonly string[]>;
94
+ readConfigSource?(): Promise<ConsoleConfigSource>;
40
95
  readConfigDocument?(): Promise<Record<string, unknown>>;
41
- /**
42
- * Full-scope write: replace project config file contents (YAML or JSON text).
43
- * Runtime Host does not hot-reload the process — callers should restart.
44
- */
45
- writeConfigYaml?(yaml: string): Promise<void>;
96
+ /** Full-scope, revision-checked replacement of the active Root config source. */
97
+ replaceConfigSource?(source: string, expectedRevision: string): Promise<{
98
+ readonly revision: string;
99
+ readonly restartRequired: boolean;
100
+ }>;
46
101
  /**
47
102
  * Full-scope write: set `document[pluginName] = data` and persist.
48
- * Returns whether a process restart is required (always true without ConfigFeature).
103
+ * Returns whether a process restart is required.
49
104
  */
50
105
  setConfigKey?(pluginName: string, data: unknown): Promise<{
51
106
  restartRequired: boolean;
@@ -54,6 +109,10 @@ export type RuntimeConsoleRpcContext = {
54
109
  setPluginEnabled?(instanceKey: string, enabled: boolean): Promise<Readonly<{
55
110
  disabled: readonly string[];
56
111
  }>>;
112
+ /** Read-only install planning shared by Console Web and CLI surfaces. */
113
+ pluginManagement?: PluginManagementPort;
114
+ validatePluginConfig?(pluginName: string, data: unknown): Promise<PluginConfigValidation>;
115
+ diagnosePlugin?(pluginName: string): Promise<unknown>;
57
116
  /** Atomic, revision-checked runtime Workroom Catalog read/write. */
58
117
  readWorkroomCatalog?(): Promise<Readonly<{
59
118
  agents: Readonly<Record<string, unknown>>;
@@ -83,8 +142,8 @@ export type RuntimeConsoleRpcContext = {
83
142
  getSchema?(pluginName?: string): Promise<unknown>;
84
143
  getAllSchemas?(): Promise<Record<string, unknown>>;
85
144
  /** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
86
- listEndpoints?(): Promise<readonly RuntimeEndpointSummary[]>;
87
- getEndpoint?(adapter: string, endpointKey: string): Promise<RuntimeEndpointSummary | null>;
145
+ listEndpoints?(): Promise<readonly ConsoleEndpointSummary[]>;
146
+ getEndpoint?(adapter: string, endpointKey: string): Promise<ConsoleEndpointSummary | null>;
88
147
  sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{
89
148
  messageId: string;
90
149
  }>;
@@ -102,9 +161,6 @@ export type RuntimeConsoleRpcContext = {
102
161
  /** Extended RPC surface(cron/schedule、endpoint 社交/inbox),fullScope 由 authScope 推导。 */
103
162
  extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
104
163
  };
105
- export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
106
- /** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
107
- export type RuntimeEndpointSummary = ConsoleEndpointSummary;
108
164
  export type RuntimeDatabaseInfo = {
109
165
  readonly dialect: string | null;
110
166
  readonly connected: boolean;
@@ -1,11 +1,9 @@
1
- import { assertDemoConsoleRpcAllowed, endpointSendResult, normalizeConsoleRpcMessage, } from '@zhin.js/console-protocol';
2
- import { dispatchExtendedConsoleRpc } from './console-rpc-extended.js';
3
- /** `$channel_type`/`$channel_id`/`$parent` wire 字段 → 会话地址(RPC 边界组帧)。 */
1
+ import { CONFIG_RPC, ENDPOINT_RPC, assertDemoConsoleRpcAllowed, } from '@zhin.js/console-protocol';
2
+ import { dispatchExtendedConsoleRpc } from './console-rpc-extended/index.js';
3
+ import { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
4
+ /** Canonical Console wire fields → runtime conversation address. */
4
5
  function wireConversation(channelType, channelId, parent) {
5
- // console 旧 wire 允许 channel_id 自带场景前缀(`group:123`),解析结果优先
6
- const prefixed = /^(private|group|channel|direct|c2c|temp):(.+)$/iu.exec(channelId);
7
- const rawKind = channelType || prefixed?.[1] || 'private';
8
- const kind = rawKind === 'group' || rawKind === 'channel' ? rawKind : 'private';
6
+ const kind = channelType === 'group' || channelType === 'channel' ? channelType : 'private';
9
7
  const wire = parent && typeof parent === 'object'
10
8
  ? parent
11
9
  : undefined;
@@ -13,7 +11,7 @@ function wireConversation(channelType, channelId, parent) {
13
11
  const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
14
12
  return {
15
13
  kind,
16
- id: prefixed ? prefixed[2] : channelId,
14
+ id: channelId,
17
15
  ...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
18
16
  };
19
17
  }
@@ -28,7 +26,6 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
28
26
  const emit = (payload) => {
29
27
  payloads.push(payload);
30
28
  };
31
- message = normalizeConsoleRpcMessage(message);
32
29
  const type = String(message.type ?? '');
33
30
  const requestId = message.requestId;
34
31
  if (ctx.authScope === 'demo') {
@@ -48,6 +45,11 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
48
45
  return payloads;
49
46
  }
50
47
  }
48
+ const pluginReply = await dispatchPluginConsoleRpc(type, message, ctx);
49
+ if (pluginReply !== undefined) {
50
+ emit(pluginReply);
51
+ return payloads;
52
+ }
51
53
  switch (type) {
52
54
  case 'ping':
53
55
  emit({ type: 'pong', requestId });
@@ -73,11 +75,12 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
73
75
  emit({ requestId, data: pages });
74
76
  return payloads;
75
77
  }
76
- case 'config:get-yaml': {
78
+ case CONFIG_RPC.GET_SOURCE: {
77
79
  try {
78
- const yaml = ctx.readConfigYaml ? await ctx.readConfigYaml() : '';
79
- const pluginKeys = ctx.listPluginKeys ? await ctx.listPluginKeys() : [];
80
- emit({ requestId, data: { yaml, pluginKeys } });
80
+ const config = ctx.readConfigSource
81
+ ? await ctx.readConfigSource()
82
+ : { source: '', format: 'yaml', revision: '', configKeys: [] };
83
+ emit({ requestId, data: config });
81
84
  }
82
85
  catch (error) {
83
86
  emit({
@@ -87,7 +90,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
87
90
  }
88
91
  return payloads;
89
92
  }
90
- case 'config:get-all': {
93
+ case CONFIG_RPC.GET_ALL: {
91
94
  try {
92
95
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
93
96
  emit({ requestId, data: document });
@@ -100,11 +103,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
100
103
  }
101
104
  return payloads;
102
105
  }
103
- case 'config:get': {
106
+ case CONFIG_RPC.GET: {
104
107
  try {
105
- // 兼容 console UI 的 data:{plugin} / data:{key} 形状
106
- const data = (message.data ?? {});
107
- const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
108
+ const key = String(message.pluginName ?? '');
108
109
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
109
110
  emit({ requestId, data: key ? document[key] : document });
110
111
  }
@@ -151,22 +152,33 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
151
152
  }
152
153
  return payloads;
153
154
  }
154
- case 'config:save-yaml': {
155
+ case CONFIG_RPC.REPLACE_SOURCE: {
155
156
  try {
156
- const yaml = message.yaml;
157
- if (typeof yaml !== 'string') {
158
- emit({ requestId, error: 'yaml field is required' });
157
+ const source = message.source;
158
+ const expectedRevision = message.expectedRevision;
159
+ if (typeof source !== 'string') {
160
+ emit({ requestId, error: 'source field is required' });
161
+ return payloads;
162
+ }
163
+ if (typeof expectedRevision !== 'string' || !/^[a-f0-9]{64}$/u.test(expectedRevision)) {
164
+ emit({ requestId, error: 'expectedRevision is required' });
159
165
  return payloads;
160
166
  }
161
- if (!ctx.writeConfigYaml) {
167
+ if (!ctx.replaceConfigSource) {
162
168
  emit({ requestId, error: 'Config write is not configured' });
163
169
  return payloads;
164
170
  }
165
- await ctx.writeConfigYaml(yaml);
171
+ const result = await ctx.replaceConfigSource(source, expectedRevision);
166
172
  ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
167
173
  emit({
168
174
  requestId,
169
- data: { success: true, message: '配置已保存,需重启生效' },
175
+ data: {
176
+ success: true,
177
+ ...result,
178
+ message: result.restartRequired
179
+ ? '配置已保存,需重启进程才能生效'
180
+ : '配置已保存',
181
+ },
170
182
  });
171
183
  }
172
184
  catch (error) {
@@ -177,7 +189,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
177
189
  }
178
190
  return payloads;
179
191
  }
180
- case 'config:set': {
192
+ case CONFIG_RPC.SET: {
181
193
  try {
182
194
  const pluginName = message.pluginName;
183
195
  if (typeof pluginName !== 'string' || !pluginName) {
@@ -188,6 +200,17 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
188
200
  emit({ requestId, error: 'Config write is not configured' });
189
201
  return payloads;
190
202
  }
203
+ if (ctx.validatePluginConfig) {
204
+ const validation = await ctx.validatePluginConfig(pluginName, message.data);
205
+ if (!validation.valid) {
206
+ emit({
207
+ requestId,
208
+ error: '配置校验失败',
209
+ data: validation,
210
+ });
211
+ return payloads;
212
+ }
213
+ }
191
214
  const result = await ctx.setConfigKey(pluginName, message.data);
192
215
  ctx.publishEvent?.('config:updated', {
193
216
  pluginName,
@@ -340,10 +363,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
340
363
  }
341
364
  case 'schema:get': {
342
365
  try {
343
- // 兼容 console UI 的 data:{plugin} / data:{pluginName} 形状(SDK 发顶层 pluginName)
344
- const data = (message.data ?? {});
345
- const candidate = message.pluginName ?? data.pluginName ?? data.plugin;
346
- const pluginName = typeof candidate === 'string' ? candidate : undefined;
366
+ const pluginName = typeof message.pluginName === 'string'
367
+ ? message.pluginName
368
+ : undefined;
347
369
  const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
348
370
  emit({ requestId, data: schema });
349
371
  }
@@ -575,11 +597,11 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
575
597
  }
576
598
  case 'endpoint.info': {
577
599
  try {
578
- const data = message;
579
- const adapter = String(data.$adapter ?? '');
580
- const endpointKey = String(data.$endpoint ?? '');
600
+ const data = recordField(message.data) ?? message;
601
+ const adapter = String(data.adapter ?? '');
602
+ const endpointKey = String(data.endpointKey ?? '');
581
603
  if (!adapter || !endpointKey) {
582
- emit({ requestId, error: '$adapter and $endpoint required' });
604
+ emit({ requestId, error: 'adapter and endpointKey are required' });
583
605
  return payloads;
584
606
  }
585
607
  if (!ctx.getEndpoint) {
@@ -601,19 +623,68 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
601
623
  }
602
624
  return payloads;
603
625
  }
626
+ case ENDPOINT_RPC.TEST: {
627
+ const data = recordField(message.data) ?? message;
628
+ const adapter = stringField(data, 'adapter');
629
+ const endpointKey = stringField(data, 'endpointKey');
630
+ if (!adapter || !endpointKey) {
631
+ emit({ requestId, error: 'adapter and endpointKey are required' });
632
+ return payloads;
633
+ }
634
+ if (!ctx.getEndpoint) {
635
+ emit({ requestId, error: 'Endpoint registry is not configured' });
636
+ return payloads;
637
+ }
638
+ const startedAt = Date.now();
639
+ try {
640
+ const endpoint = await ctx.getEndpoint(adapter, endpointKey);
641
+ if (!endpoint) {
642
+ emit({ requestId, data: {
643
+ reachable: false,
644
+ connected: false,
645
+ phase: 'unconfigured',
646
+ latencyMs: Date.now() - startedAt,
647
+ message: 'Endpoint 未注册到当前 generation',
648
+ } });
649
+ return payloads;
650
+ }
651
+ emit({ requestId, data: {
652
+ reachable: endpoint.connected && endpoint.status === 'online',
653
+ connected: endpoint.connected,
654
+ status: endpoint.status,
655
+ phase: endpoint.phase ?? (endpoint.connected ? 'online' : 'pending'),
656
+ pendingLogin: endpoint.pendingLogin ?? false,
657
+ latencyMs: Date.now() - startedAt,
658
+ message: endpoint.connected
659
+ ? 'Endpoint 已连接'
660
+ : endpoint.pendingLogin
661
+ ? 'Endpoint 等待登录操作'
662
+ : 'Endpoint 当前离线',
663
+ } });
664
+ }
665
+ catch (error) {
666
+ emit({ requestId, data: {
667
+ reachable: false,
668
+ connected: false,
669
+ phase: 'failed',
670
+ latencyMs: Date.now() - startedAt,
671
+ message: error instanceof Error ? error.message : String(error),
672
+ } });
673
+ }
674
+ return payloads;
675
+ }
604
676
  case 'endpoint.send_message': {
605
677
  try {
606
- const data = message;
607
- const adapter = String(data.$adapter ?? '');
608
- const endpointKey = String(data.$endpoint ?? '');
609
- const channelId = String(data.$channel_id ?? '');
610
- const channelType = String(data.$channel_type ?? '');
611
- const content = data.$content;
612
- // channelType 可省:wireConversation 归一为 private(与旧 RPC 行为对齐)。
613
- if (!adapter || !endpointKey || !channelId || content === undefined) {
678
+ const data = recordField(message.data) ?? message;
679
+ const adapter = String(data.adapter ?? '');
680
+ const endpointKey = String(data.endpointKey ?? '');
681
+ const channelId = String(data.channelId ?? data.id ?? '');
682
+ const channelType = String(data.channelType ?? data.type ?? '');
683
+ const content = data.content;
684
+ if (!adapter || !endpointKey || !channelId || !channelType || content === undefined) {
614
685
  emit({
615
686
  requestId,
616
- error: '$adapter, $endpoint, $channel_id, $content required',
687
+ error: 'adapter, endpointKey, channelId, channelType, and content are required',
617
688
  });
618
689
  return payloads;
619
690
  }
@@ -624,11 +695,10 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
624
695
  const result = await ctx.sendEndpointMessage({
625
696
  adapter,
626
697
  endpointKey,
627
- conversation: wireConversation(channelType, channelId, data.$parent),
698
+ conversation: wireConversation(channelType, channelId, data.parent),
628
699
  content,
629
700
  });
630
- // Legacy contract: { message_id }. Keep messageId for new callers.
631
- emit({ requestId, data: endpointSendResult(result.messageId) });
701
+ emit({ requestId, data: { messageId: result.messageId } });
632
702
  }
633
703
  catch (error) {
634
704
  emit({
@@ -661,42 +731,6 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
661
731
  }
662
732
  return payloads;
663
733
  }
664
- case 'plugin:set-enabled': {
665
- try {
666
- const instanceKey = typeof message.instanceKey === 'string' ? message.instanceKey : '';
667
- const enabled = message.enabled;
668
- if (!instanceKey || typeof enabled !== 'boolean') {
669
- emit({ requestId, error: 'instanceKey and boolean enabled are required' });
670
- return payloads;
671
- }
672
- if (!ctx.setPluginEnabled) {
673
- emit({ requestId, error: 'Plugin lifecycle management is not configured' });
674
- return payloads;
675
- }
676
- const lifecycle = await ctx.setPluginEnabled(instanceKey, enabled);
677
- ctx.publishEvent?.('plugin:lifecycle-updated', { instanceKey, enabled });
678
- emit({
679
- requestId,
680
- data: {
681
- success: true,
682
- instanceKey,
683
- enabled,
684
- disabled: lifecycle.disabled,
685
- restartRequired: true,
686
- message: `${instanceKey} 已${enabled ? '启用' : '停用'},Host 正在重启`,
687
- },
688
- });
689
- if (ctx.requestRestart) {
690
- setTimeout(() => {
691
- void Promise.resolve(ctx.requestRestart?.()).catch(() => undefined);
692
- }, 500);
693
- }
694
- }
695
- catch (error) {
696
- emit({ requestId, error: error instanceof Error ? error.message : String(error) });
697
- }
698
- return payloads;
699
- }
700
734
  default:
701
735
  if (type.startsWith('db:')) {
702
736
  emit({
package/lib/index.d.ts CHANGED
@@ -5,7 +5,8 @@ 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 RuntimeEndpointSummary, } 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 PluginConfigValidation, } from './console-rpc.js';
9
+ export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
9
10
  export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, type FileTreeNode, } from './project-files.js';
10
11
  export { registerConsoleRestPages, type ConsoleAgentIntrospection, type ConsoleAgentRuntime, type ConsoleAgentSessionTree, type ConsoleRestCtx, type ConsoleRestPagesOptions, } from './console-rest-pages.js';
11
- export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ConsoleWorkroomProfileControlPort, type ConsoleWorkroomKnowledgeControlPort, type ExtendedRpcResult, } from './console-rpc-extended.js';
12
+ export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ConsoleWorkroomProfileControlPort, type ConsoleWorkroomKnowledgeControlPort, type ExtendedRpcResult, } from './console-rpc-extended/index.js';
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ export { createConsoleEventHub, consoleEventHubToken, } from './console-events.j
6
6
  export { buildOpenApiDocument, patternToOpenApiPath, routeRequiresBearerAuth, } from './openapi.js';
7
7
  export { HttpBodyError, readJsonBody, } from './json-body.js';
8
8
  export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, } from './console-rpc.js';
9
+ export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
9
10
  export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, } from './project-files.js';
10
11
  export { registerConsoleRestPages, } from './console-rest-pages.js';
11
- export { dispatchExtendedConsoleRpc, } from './console-rpc-extended.js';
12
+ export { dispatchExtendedConsoleRpc, } from './console-rpc-extended/index.js';
@@ -0,0 +1,12 @@
1
+ export interface MarketplaceRegistryOptions {
2
+ readonly fetchFn: typeof fetch;
3
+ readonly url: string;
4
+ readonly ttlMs: number;
5
+ readonly now?: () => number;
6
+ }
7
+ /** Host-owned plugin registry reader with TTL caching and request coalescing. */
8
+ export declare class MarketplaceRegistry {
9
+ #private;
10
+ constructor(options: MarketplaceRegistryOptions);
11
+ list(): Promise<readonly unknown[]>;
12
+ }
@@ -0,0 +1,45 @@
1
+ /** Host-owned plugin registry reader with TTL caching and request coalescing. */
2
+ export class MarketplaceRegistry {
3
+ #fetch;
4
+ #url;
5
+ #ttlMs;
6
+ #now;
7
+ #snapshot;
8
+ #inflight;
9
+ constructor(options) {
10
+ this.#fetch = options.fetchFn;
11
+ this.#url = options.url;
12
+ this.#ttlMs = options.ttlMs;
13
+ this.#now = options.now ?? Date.now;
14
+ }
15
+ list() {
16
+ const snapshot = this.#snapshot;
17
+ if (snapshot && this.#now() < snapshot.expiresAt) {
18
+ return Promise.resolve(snapshot.plugins);
19
+ }
20
+ if (this.#inflight)
21
+ return this.#inflight;
22
+ const request = this.#load();
23
+ this.#inflight = request;
24
+ const clearInflight = () => {
25
+ if (this.#inflight === request)
26
+ this.#inflight = undefined;
27
+ };
28
+ void request.then(clearInflight, clearInflight);
29
+ return request;
30
+ }
31
+ async #load() {
32
+ const response = await this.#fetch(this.#url, {
33
+ signal: AbortSignal.timeout(30_000),
34
+ });
35
+ if (!response.ok)
36
+ throw new Error(`plugins.json fetch failed: ${response.status}`);
37
+ const json = (await response.json());
38
+ const plugins = Object.freeze([...(json.plugins ?? [])]);
39
+ this.#snapshot = Object.freeze({
40
+ plugins,
41
+ expiresAt: this.#now() + this.#ttlMs,
42
+ });
43
+ return plugins;
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.0.17",
3
+ "version": "1.1.1",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,10 +18,10 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "ws": "^8.21.1",
21
- "@zhin.js/console-protocol": "1.1.4",
22
- "@zhin.js/host-http-contract": "1.0.1",
23
- "@zhin.js/logger": "1.0.77",
24
- "@zhin.js/plugin-runtime": "1.1.8"
21
+ "@zhin.js/logger": "1.1.1",
22
+ "@zhin.js/host-http-contract": "1.1.0",
23
+ "@zhin.js/console-protocol": "1.1.6",
24
+ "@zhin.js/plugin-runtime": "1.1.10"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",