@zhin.js/host-http 1.0.13 → 1.0.15

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.
@@ -26,6 +26,8 @@ export interface ConsoleRestCtx {
26
26
  get(name: string): unknown;
27
27
  };
28
28
  };
29
+ /** Distinguishes a real IM conversation with no Agent turn from a typo key. */
30
+ readonly isKnownConversationSession?: (sessionKey: string) => boolean | undefined | Promise<boolean | undefined>;
29
31
  }
30
32
  /** 内省数据门面 — 由 basic/cli 用 agent 包 collectIntrospection* 装配。 */
31
33
  export interface ConsoleAgentIntrospection {
@@ -663,9 +663,33 @@ function registerAgentSessionRoutes(route, base, ctx) {
663
663
  try {
664
664
  const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
665
665
  if (!sessionId) {
666
- writeJson(response, 404, {
667
- success: false,
668
- error: `未找到活跃会话:${parsed.sessionKey}`,
666
+ if (!isRuntimeImSessionKey(parsed.sessionKey)) {
667
+ writeJson(response, 404, {
668
+ success: false,
669
+ error: `未找到会话:${parsed.sessionKey}`,
670
+ });
671
+ return;
672
+ }
673
+ const knownConversation = await ctx.isKnownConversationSession?.(parsed.sessionKey);
674
+ if (knownConversation === false) {
675
+ writeJson(response, 404, {
676
+ success: false,
677
+ error: `未找到会话:${parsed.sessionKey}`,
678
+ });
679
+ return;
680
+ }
681
+ // 渠道会话与 Agent 会话的生命周期不同:用户可以先在 Console
682
+ // 选中一个真实渠道会话,而该会话尚未触发过 Agent。树查询是只读操作,
683
+ // 因而将这种正常状态返回为可渲染空态,而不是误报资源不存在。
684
+ writeJson(response, 200, {
685
+ success: true,
686
+ data: {
687
+ state: 'not_started',
688
+ sessionKey: parsed.sessionKey,
689
+ sessionId: null,
690
+ activeLeafMessageId: null,
691
+ points: [],
692
+ },
669
693
  });
670
694
  return;
671
695
  }
@@ -674,6 +698,7 @@ function registerAgentSessionRoutes(route, base, ctx) {
674
698
  writeJson(response, 200, {
675
699
  success: true,
676
700
  data: {
701
+ state: 'active',
677
702
  sessionKey: parsed.sessionKey,
678
703
  sessionId,
679
704
  activeLeafMessageId,
@@ -774,6 +799,16 @@ function registerAgentSessionRoutes(route, base, ctx) {
774
799
  }
775
800
  }, { summary: 'Switch agent session active leaf', tags: ['console', 'agent'] });
776
801
  }
802
+ /** Runtime IM session keys are `<platform>:<endpoint>:<scope>:<sceneId>`. */
803
+ function isRuntimeImSessionKey(value) {
804
+ const first = value.indexOf(':');
805
+ const second = value.indexOf(':', first + 1);
806
+ const third = value.indexOf(':', second + 1);
807
+ return first > 0
808
+ && second > first + 1
809
+ && third > second + 1
810
+ && third < value.length - 1;
811
+ }
777
812
  function parseSessionAction(pathname, prefix, action) {
778
813
  if (!pathname.startsWith(prefix))
779
814
  return null;
@@ -55,6 +55,18 @@ export interface ConsoleRpcExtendedCtx {
55
55
  workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
56
56
  }
57
57
  export interface ConsoleWorkroomProfileControlPort {
58
+ getPlanningStatus(projectId: string, authenticatedPrincipal?: Readonly<{
59
+ principalId: string;
60
+ }>): Promise<unknown>;
61
+ bootstrapPlanning(command: Readonly<{
62
+ operationId: string;
63
+ projectId: string;
64
+ expectedRegistryRevision: number;
65
+ includeTools?: readonly string[];
66
+ includeSkills?: readonly string[];
67
+ }>, authenticatedPrincipal: Readonly<{
68
+ principalId: string;
69
+ }>): Promise<unknown>;
58
70
  publishPack(command: Readonly<{
59
71
  operationId: string;
60
72
  pack: unknown;
@@ -95,6 +95,8 @@ export async function dispatchExtendedConsoleRpc(type, data, ctx) {
95
95
  });
96
96
  case 'endpoint.delete_friend':
97
97
  return deleteFriend(d, ctx);
98
+ case 'workroom.profile.status':
99
+ case 'workroom.profile.bootstrap':
98
100
  case 'workroom.profile.pack.publish':
99
101
  case 'workroom.profile.publish':
100
102
  case 'workroom.profile.rollback':
@@ -118,11 +120,31 @@ async function mutateWorkroomProfile(type, data, ctx) {
118
120
  if (forbidden in data)
119
121
  return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
120
122
  }
123
+ if (type === 'workroom.profile.status') {
124
+ try {
125
+ return { data: await control.getPlanningStatus(requiredRpcText(data.projectId, 'projectId'), ctx.authenticatedPrincipal) };
126
+ }
127
+ catch (error) {
128
+ return { error: errorMessage(error) };
129
+ }
130
+ }
121
131
  const principal = ctx.authenticatedPrincipal;
122
132
  if (!principal)
123
133
  return { error: 'Workroom Profile control requires an authenticated principal' };
124
134
  try {
125
135
  const operationId = requiredRpcText(data.operationId, 'operationId');
136
+ if (type === 'workroom.profile.bootstrap') {
137
+ const projectId = requiredRpcText(data.projectId, 'projectId');
138
+ const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
139
+ const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
140
+ return { data: await control.bootstrapPlanning(Object.freeze({
141
+ operationId,
142
+ projectId,
143
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
144
+ ...(includeTools ? { includeTools } : {}),
145
+ ...(includeSkills ? { includeSkills } : {}),
146
+ }), principal) };
147
+ }
126
148
  if (type === 'workroom.profile.pack.publish') {
127
149
  return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
128
150
  }
@@ -205,6 +227,16 @@ function requiredRpcText(value, name) {
205
227
  function optionalRpcText(value, name) {
206
228
  return value === undefined ? undefined : requiredRpcText(value, name);
207
229
  }
230
+ function optionalRpcTextArray(value, name) {
231
+ if (value === undefined)
232
+ return undefined;
233
+ if (!Array.isArray(value))
234
+ throw new Error(`${name} is invalid`);
235
+ const result = value.map((item, index) => requiredRpcText(item, `${name}.${index}`));
236
+ if (new Set(result).size !== result.length)
237
+ throw new Error(`${name} contains duplicates`);
238
+ return Object.freeze(result);
239
+ }
208
240
  function requiredRpcInteger(value, name, minimum = 1) {
209
241
  if (!Number.isSafeInteger(value) || Number(value) < minimum)
210
242
  throw new Error(`${name} is invalid`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -42,6 +42,10 @@ export interface ConsoleRestCtx {
42
42
  readonly started: boolean;
43
43
  readonly models: { get(name: string): unknown };
44
44
  };
45
+ /** Distinguishes a real IM conversation with no Agent turn from a typo key. */
46
+ readonly isKnownConversationSession?: (
47
+ sessionKey: string,
48
+ ) => boolean | undefined | Promise<boolean | undefined>;
45
49
  }
46
50
 
47
51
  /** 内省数据门面 — 由 basic/cli 用 agent 包 collectIntrospection* 装配。 */
@@ -864,9 +868,33 @@ function registerAgentSessionRoutes(
864
868
  try {
865
869
  const sessionId = await runtime.resolveActiveSessionId(parsed.sessionKey);
866
870
  if (!sessionId) {
867
- writeJson(response, 404, {
868
- success: false,
869
- error: `未找到活跃会话:${parsed.sessionKey}`,
871
+ if (!isRuntimeImSessionKey(parsed.sessionKey)) {
872
+ writeJson(response, 404, {
873
+ success: false,
874
+ error: `未找到会话:${parsed.sessionKey}`,
875
+ });
876
+ return;
877
+ }
878
+ const knownConversation = await ctx.isKnownConversationSession?.(parsed.sessionKey);
879
+ if (knownConversation === false) {
880
+ writeJson(response, 404, {
881
+ success: false,
882
+ error: `未找到会话:${parsed.sessionKey}`,
883
+ });
884
+ return;
885
+ }
886
+ // 渠道会话与 Agent 会话的生命周期不同:用户可以先在 Console
887
+ // 选中一个真实渠道会话,而该会话尚未触发过 Agent。树查询是只读操作,
888
+ // 因而将这种正常状态返回为可渲染空态,而不是误报资源不存在。
889
+ writeJson(response, 200, {
890
+ success: true,
891
+ data: {
892
+ state: 'not_started',
893
+ sessionKey: parsed.sessionKey,
894
+ sessionId: null,
895
+ activeLeafMessageId: null,
896
+ points: [],
897
+ },
870
898
  });
871
899
  return;
872
900
  }
@@ -876,6 +904,7 @@ function registerAgentSessionRoutes(
876
904
  writeJson(response, 200, {
877
905
  success: true,
878
906
  data: {
907
+ state: 'active',
879
908
  sessionKey: parsed.sessionKey,
880
909
  sessionId,
881
910
  activeLeafMessageId,
@@ -975,6 +1004,17 @@ function registerAgentSessionRoutes(
975
1004
  }, { summary: 'Switch agent session active leaf', tags: ['console', 'agent'] });
976
1005
  }
977
1006
 
1007
+ /** Runtime IM session keys are `<platform>:<endpoint>:<scope>:<sceneId>`. */
1008
+ function isRuntimeImSessionKey(value: string): boolean {
1009
+ const first = value.indexOf(':');
1010
+ const second = value.indexOf(':', first + 1);
1011
+ const third = value.indexOf(':', second + 1);
1012
+ return first > 0
1013
+ && second > first + 1
1014
+ && third > second + 1
1015
+ && third < value.length - 1;
1016
+ }
1017
+
978
1018
  function parseSessionAction(
979
1019
  pathname: string,
980
1020
  prefix: string,
@@ -66,6 +66,15 @@ export interface ConsoleRpcExtendedCtx {
66
66
  }
67
67
 
68
68
  export interface ConsoleWorkroomProfileControlPort {
69
+ getPlanningStatus(projectId: string,
70
+ authenticatedPrincipal?: Readonly<{ principalId: string }>): Promise<unknown>;
71
+ bootstrapPlanning(command: Readonly<{
72
+ operationId: string;
73
+ projectId: string;
74
+ expectedRegistryRevision: number;
75
+ includeTools?: readonly string[];
76
+ includeSkills?: readonly string[];
77
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
69
78
  publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
70
79
  authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
71
80
  publishProfile(command: Readonly<{
@@ -258,6 +267,8 @@ export async function dispatchExtendedConsoleRpc(
258
267
  case 'endpoint.delete_friend':
259
268
  return deleteFriend(d, ctx);
260
269
 
270
+ case 'workroom.profile.status':
271
+ case 'workroom.profile.bootstrap':
261
272
  case 'workroom.profile.pack.publish':
262
273
  case 'workroom.profile.publish':
263
274
  case 'workroom.profile.rollback':
@@ -275,7 +286,8 @@ export async function dispatchExtendedConsoleRpc(
275
286
  }
276
287
 
277
288
  async function mutateWorkroomProfile(
278
- type: 'workroom.profile.pack.publish' | 'workroom.profile.publish'
289
+ type: 'workroom.profile.status' | 'workroom.profile.bootstrap'
290
+ | 'workroom.profile.pack.publish' | 'workroom.profile.publish'
279
291
  | 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
280
292
  data: Record<string, unknown>,
281
293
  ctx: ConsoleRpcExtendedCtx,
@@ -287,10 +299,36 @@ async function mutateWorkroomProfile(
287
299
  ]) {
288
300
  if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
289
301
  }
302
+ if (type === 'workroom.profile.status') {
303
+ try {
304
+ return { data: await control.getPlanningStatus(
305
+ requiredRpcText(data.projectId, 'projectId'),
306
+ ctx.authenticatedPrincipal,
307
+ ) };
308
+ } catch (error) {
309
+ return { error: errorMessage(error) };
310
+ }
311
+ }
290
312
  const principal = ctx.authenticatedPrincipal;
291
313
  if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
292
314
  try {
293
315
  const operationId = requiredRpcText(data.operationId, 'operationId');
316
+ if (type === 'workroom.profile.bootstrap') {
317
+ const projectId = requiredRpcText(data.projectId, 'projectId');
318
+ const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
319
+ const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
320
+ return { data: await control.bootstrapPlanning(Object.freeze({
321
+ operationId,
322
+ projectId,
323
+ expectedRegistryRevision: requiredRpcInteger(
324
+ data.expectedRegistryRevision,
325
+ 'expectedRegistryRevision',
326
+ -1,
327
+ ),
328
+ ...(includeTools ? { includeTools } : {}),
329
+ ...(includeSkills ? { includeSkills } : {}),
330
+ }), principal) };
331
+ }
294
332
  if (type === 'workroom.profile.pack.publish') {
295
333
  return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
296
334
  }
@@ -374,6 +412,14 @@ function optionalRpcText(value: unknown, name: string): string | undefined {
374
412
  return value === undefined ? undefined : requiredRpcText(value, name);
375
413
  }
376
414
 
415
+ function optionalRpcTextArray(value: unknown, name: string): readonly string[] | undefined {
416
+ if (value === undefined) return undefined;
417
+ if (!Array.isArray(value)) throw new Error(`${name} is invalid`);
418
+ const result = value.map((item, index) => requiredRpcText(item, `${name}.${index}`));
419
+ if (new Set(result).size !== result.length) throw new Error(`${name} contains duplicates`);
420
+ return Object.freeze(result);
421
+ }
422
+
377
423
  function requiredRpcInteger(value: unknown, name: string, minimum = 1): number {
378
424
  if (!Number.isSafeInteger(value) || Number(value) < minimum) throw new Error(`${name} is invalid`);
379
425
  return Number(value);