@zhin.js/client 1.1.5 → 1.1.7

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 (103) hide show
  1. package/README.md +25 -19
  2. package/client/app.test.ts +37 -0
  3. package/client/app.ts +130 -138
  4. package/client/bootstrap/loadConsoleEntries.test.ts +0 -15
  5. package/client/bootstrap/loadConsoleEntries.ts +1 -20
  6. package/client/console-client-context.ts +24 -0
  7. package/client/console-client.test.ts +46 -0
  8. package/client/console-client.ts +70 -0
  9. package/client/console-events.ts +4 -3
  10. package/client/console-utils/remoteApi.ts +0 -7
  11. package/client/index.ts +21 -13
  12. package/client/persistence/idb-store.test.ts +0 -18
  13. package/client/persistence/idb-store.ts +3 -6
  14. package/client/segments.test.ts +2 -0
  15. package/client/segments.ts +1 -1
  16. package/client/{websocket/manager.test.ts → transport/console-transport.test.ts} +79 -34
  17. package/client/{websocket/manager.ts → transport/console-transport.ts} +237 -178
  18. package/client/{websocket → transport}/hooks.ts +240 -23
  19. package/client/transport/index.ts +23 -0
  20. package/client/{websocket → transport}/types.ts +11 -17
  21. package/client/types.ts +0 -3
  22. package/client/workroom/registerWorkroomConsole.test.ts +0 -1
  23. package/dist/app.d.ts +17 -52
  24. package/dist/app.d.ts.map +1 -1
  25. package/dist/app.js +110 -95
  26. package/dist/app.js.map +1 -1
  27. package/dist/bootstrap/loadConsoleEntries.d.ts +0 -6
  28. package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -1
  29. package/dist/bootstrap/loadConsoleEntries.js +0 -9
  30. package/dist/bootstrap/loadConsoleEntries.js.map +1 -1
  31. package/dist/console-client-context.d.ts +9 -0
  32. package/dist/console-client-context.d.ts.map +1 -0
  33. package/dist/console-client-context.js +13 -0
  34. package/dist/console-client-context.js.map +1 -0
  35. package/dist/console-client.d.ts +25 -0
  36. package/dist/console-client.d.ts.map +1 -0
  37. package/dist/console-client.js +45 -0
  38. package/dist/console-client.js.map +1 -0
  39. package/dist/console-events.d.ts +1 -0
  40. package/dist/console-events.d.ts.map +1 -1
  41. package/dist/console-events.js +3 -3
  42. package/dist/console-events.js.map +1 -1
  43. package/dist/console-utils/remoteApi.d.ts +0 -1
  44. package/dist/console-utils/remoteApi.d.ts.map +1 -1
  45. package/dist/console-utils/remoteApi.js +0 -6
  46. package/dist/console-utils/remoteApi.js.map +1 -1
  47. package/dist/index.d.ts +7 -6
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +9 -8
  50. package/dist/index.js.map +1 -1
  51. package/dist/persistence/idb-store.d.ts.map +1 -1
  52. package/dist/persistence/idb-store.js +3 -5
  53. package/dist/persistence/idb-store.js.map +1 -1
  54. package/dist/segments.js +1 -1
  55. package/dist/segments.js.map +1 -1
  56. package/dist/transport/console-transport.d.ts +217 -0
  57. package/dist/transport/console-transport.d.ts.map +1 -0
  58. package/dist/{websocket/manager.js → transport/console-transport.js} +147 -165
  59. package/dist/transport/console-transport.js.map +1 -0
  60. package/dist/transport/hooks.d.ts +253 -0
  61. package/dist/transport/hooks.d.ts.map +1 -0
  62. package/dist/{websocket → transport}/hooks.js +234 -19
  63. package/dist/transport/hooks.js.map +1 -0
  64. package/dist/transport/index.d.ts +4 -0
  65. package/dist/transport/index.d.ts.map +1 -0
  66. package/dist/transport/index.js +4 -0
  67. package/dist/transport/index.js.map +1 -0
  68. package/dist/{websocket → transport}/types.d.ts +10 -13
  69. package/dist/transport/types.d.ts.map +1 -0
  70. package/dist/{websocket → transport}/types.js +4 -9
  71. package/dist/transport/types.js.map +1 -0
  72. package/dist/types.d.ts +0 -1
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +3 -3
  75. package/client/runtime/index.ts +0 -12
  76. package/client/websocket/index.ts +0 -11
  77. package/client/websocket/instance.ts +0 -24
  78. package/client/websocket/remote-settings.ts +0 -47
  79. package/dist/runtime/index.d.ts +0 -6
  80. package/dist/runtime/index.d.ts.map +0 -1
  81. package/dist/runtime/index.js +0 -10
  82. package/dist/runtime/index.js.map +0 -1
  83. package/dist/websocket/hooks.d.ts +0 -114
  84. package/dist/websocket/hooks.d.ts.map +0 -1
  85. package/dist/websocket/hooks.js.map +0 -1
  86. package/dist/websocket/index.d.ts +0 -7
  87. package/dist/websocket/index.d.ts.map +0 -1
  88. package/dist/websocket/index.js +0 -7
  89. package/dist/websocket/index.js.map +0 -1
  90. package/dist/websocket/instance.d.ts +0 -5
  91. package/dist/websocket/instance.d.ts.map +0 -1
  92. package/dist/websocket/instance.js +0 -21
  93. package/dist/websocket/instance.js.map +0 -1
  94. package/dist/websocket/manager.d.ts +0 -131
  95. package/dist/websocket/manager.d.ts.map +0 -1
  96. package/dist/websocket/manager.js.map +0 -1
  97. package/dist/websocket/remote-settings.d.ts +0 -5
  98. package/dist/websocket/remote-settings.d.ts.map +0 -1
  99. package/dist/websocket/remote-settings.js +0 -47
  100. package/dist/websocket/remote-settings.js.map +0 -1
  101. package/dist/websocket/types.d.ts.map +0 -1
  102. package/dist/websocket/types.js.map +0 -1
  103. /package/client/{websocket → transport}/hooks.test.ts +0 -0
package/client/index.ts CHANGED
@@ -7,18 +7,19 @@ export {
7
7
  INBOX_RPC,
8
8
  SIDE_EVENT_PUSH,
9
9
  SIDE_EVENT_RPC,
10
- normalizeConsolePushMessage,
11
- normalizeConsolePushType,
12
10
  parseConsoleInboxEvent,
13
11
  type ConsoleInboxEvent,
14
12
  type ConsoleInboxEventKind,
13
+ type ConsoleInboxMessageRow,
15
14
  type ConsoleInboxNoticeRow,
16
15
  type ConsoleInboxNoticesQuery,
17
16
  type ConsoleInboxNoticesResult,
17
+ type ConsoleInboxRequestRow,
18
18
  type ConsoleEndpointPhase,
19
19
  type ConsoleEndpointSummary,
20
20
  type ConsoleEventActor,
21
21
  type ConsoleEventChannel,
22
+ type ConsoleEventParent,
22
23
  type EndpointManagementCapability,
23
24
  parseConsoleSseFrame,
24
25
  type ConsoleEventDelivery,
@@ -64,22 +65,32 @@ export {
64
65
  // Segment IM visibility (inbox vs agent panel)
65
66
  export { segmentsForImDelivery, segmentsForAgentPanel } from "./segments.js";
66
67
 
67
- // Console app singleton (pagemanager / registry style)
68
+ // Owner-scoped Console client and application registry
68
69
  export {
69
- app,
70
- type ConsoleApp,
70
+ ConsoleApp,
71
+ createConsoleApp,
71
72
  type AddRouteInput,
72
73
  type AddToolInput,
73
74
  type ConsoleRouteRecord,
74
75
  type RouteTreeNode,
75
76
  type ToolTreeNode,
77
+ type SidebarRenderer,
78
+ type ToolbarRenderer,
76
79
  type ConsoleRouteRenderer,
77
- } from "./app";
78
-
79
- // WebSocket (business data only)
80
- export * from "./websocket";
80
+ } from "./app.js";
81
+ export {
82
+ ConsoleClient,
83
+ createConsoleClient,
84
+ type ConsoleClientOptions,
85
+ } from './console-client.js';
86
+ export {
87
+ ConsoleClientProvider,
88
+ useConsoleClient,
89
+ type ConsoleClientProviderProps,
90
+ } from './console-client-context.js';
81
91
 
82
- export { configureConsole, getRuntimeEnv } from "./runtime/index.js";
92
+ // Owner-scoped REST/SSE transport
93
+ export * from "./transport/index.js";
83
94
 
84
95
  export {
85
96
  createRegistryStore,
@@ -94,17 +105,14 @@ export {
94
105
  getApiBase,
95
106
  getToken,
96
107
  resolveApiUrl,
97
- resolveWebSocketUrl,
98
108
  } from "./console-utils/remoteApi.js";
99
109
 
100
110
  export {
101
111
  fetchConsoleEntries,
102
- createPluginRegisterHostApi,
103
112
  getRegisterFn,
104
113
  loadConsoleEntries,
105
114
  registerConsolePluginsFromEntries,
106
115
  resolveEntryRegister,
107
- type CreatePluginRegisterHostApiOptions,
108
116
  type FetchConsoleEntriesOptions,
109
117
  type LoadConsoleEntriesOptions,
110
118
  } from "./bootstrap/loadConsoleEntries.js";
@@ -118,22 +118,4 @@ describe("applyConsoleEvent inbox record ids", () => {
118
118
  expect(rows[0]?.id).toBe('runtime-a:7');
119
119
  expect(rows[0]?.updatedAt).toBe(1700000000000);
120
120
  });
121
-
122
- it("reads legacy records that only carry endpointKey (camelCase)", async () => {
123
- // 旧版(DB v1 时代)记录只有 endpointKey 字段,升级后仍需能被列出
124
- const { idbPutInbox } = await import("./idb-store.js");
125
- const legacy = {
126
- id: "icqq:bot-1:message:1700000000000:legacy",
127
- adapter: "icqq",
128
- endpointKey: "bot-1",
129
- kind: "message",
130
- payload: {},
131
- updatedAt: 1700000000000,
132
- };
133
- await idbPutInbox(legacy as unknown as Parameters<typeof idbPutInbox>[0]);
134
-
135
- const rows = await idbListInbox("icqq", "bot-1", "message");
136
- expect(rows).toHaveLength(1);
137
- expect(rows[0].id).toBe(legacy.id);
138
- });
139
121
  });
@@ -5,8 +5,6 @@ const DB_VERSION = 2;
5
5
  const STORE_INBOX = "inbox";
6
6
  const STORE_PENDING = "pending";
7
7
 
8
- let inboxEventSeq = 0;
9
-
10
8
  export type InboxRecord = {
11
9
  id: string;
12
10
  adapter: string;
@@ -60,8 +58,7 @@ export async function idbListInbox(
60
58
  return all.filter(
61
59
  (r) =>
62
60
  r.adapter === adapter &&
63
- (r.endpoint_id === endpoint_id ||
64
- (r as { endpointKey?: string }).endpointKey === endpoint_id) &&
61
+ r.endpoint_id === endpoint_id &&
65
62
  r.kind === kind,
66
63
  );
67
64
  }
@@ -86,9 +83,9 @@ export async function applyConsoleEvent(event: {
86
83
  : null;
87
84
  await idbPutInbox({
88
85
  // Resumable Host events are idempotent across history/live redelivery.
89
- // Legacy/unsequenced sources retain collision-safe append ids.
86
+ // Unsequenced sources use a collision-safe append id without shared state.
90
87
  id: resumableId
91
- ?? `${parsed.adapter}:${parsed.endpointKey}:${parsed.type}:${updatedAt}:${inboxEventSeq++}:${Math.random().toString(36).slice(2, 8)}`,
88
+ ?? `${parsed.adapter}:${parsed.endpointKey}:${parsed.type}:${updatedAt}:${crypto.randomUUID()}`,
92
89
  adapter: parsed.adapter,
93
90
  endpoint_id: parsed.endpointKey,
94
91
  kind: parsed.kind,
@@ -6,10 +6,12 @@ describe('console segmentsForImDelivery', () => {
6
6
  const full = [
7
7
  { type: 'thinking', data: { text: 'plan' } },
8
8
  { type: 'text', data: { text: 'hi' } },
9
+ { type: 'share', data: { title: 'Example', url: 'https://example.com' } },
9
10
  { type: 'tool_call', data: { name: 'bash' } },
10
11
  ];
11
12
  expect(segmentsForImDelivery(full)).toEqual([
12
13
  { type: 'text', data: { text: 'hi' } },
14
+ { type: 'share', data: { title: 'Example', url: 'https://example.com' } },
13
15
  ]);
14
16
  expect(segmentsForAgentPanel(full)).toEqual(full);
15
17
  });
@@ -4,7 +4,7 @@ import type { MessageSegment } from './types.js';
4
4
  const IM_VISIBLE_TYPES = new Set([
5
5
  'text', 'mention', 'image', 'video', 'audio', 'voice', 'record', 'file',
6
6
  'face', 'reply', 'forward', 'link', 'dice', 'rps', 'markdown', 'html',
7
- 'qrcode', 'tts', 'keyboard', 'action',
7
+ 'qrcode', 'tts', 'keyboard', 'action', 'share',
8
8
  ]);
9
9
 
10
10
  const AI_ONLY_TYPES = new Set(['thinking', 'tool_call']);
@@ -6,8 +6,8 @@ vi.mock("../persistence/idb-store.js", () => ({
6
6
  applyConsoleEvent: (...args: unknown[]) => mockApplyConsoleEvent(...args),
7
7
  }));
8
8
 
9
- import { WebSocketManager } from "./manager.js";
10
- import { ConnectionState, type WebSocketMessage } from "./types.js";
9
+ import { ConsoleTransport } from "./console-transport.js";
10
+ import { ConnectionState } from "./types.js";
11
11
 
12
12
  function mockStorage(map: Record<string, string> = {}): Storage {
13
13
  return {
@@ -80,7 +80,7 @@ function mockConsoleTransport(sse: () => Response | Promise<Response>) {
80
80
  });
81
81
  }
82
82
 
83
- describe("WebSocketManager REST/SSE transport", () => {
83
+ describe("ConsoleTransport REST/SSE transport", () => {
84
84
  beforeEach(() => {
85
85
  vi.useFakeTimers();
86
86
  mockApplyConsoleEvent.mockClear();
@@ -97,7 +97,7 @@ describe("WebSocketManager REST/SSE transport", () => {
97
97
  const fetchMock = mockConsoleTransport(() => sseResponse(200));
98
98
  vi.stubGlobal("fetch", fetchMock);
99
99
 
100
- const manager = new WebSocketManager({ reconnectInterval: 100, maxReconnectAttempts: 3 });
100
+ const manager = new ConsoleTransport({ reconnectInterval: 100, maxReconnectAttempts: 3 });
101
101
  const states: boolean[] = [];
102
102
  manager.onConnectionChange((c) => states.push(c));
103
103
  manager.connect();
@@ -122,7 +122,7 @@ describe("WebSocketManager REST/SSE transport", () => {
122
122
  const fetchMock = mockConsoleTransport(() => sseResponse(200));
123
123
  vi.stubGlobal("fetch", fetchMock);
124
124
 
125
- const manager = new WebSocketManager();
125
+ const manager = new ConsoleTransport();
126
126
  manager.connect();
127
127
  await vi.advanceTimersByTimeAsync(0);
128
128
  await Promise.resolve();
@@ -144,7 +144,7 @@ describe("WebSocketManager REST/SSE transport", () => {
144
144
  });
145
145
  vi.stubGlobal("fetch", fetchMock);
146
146
 
147
- const manager = new WebSocketManager({
147
+ const manager = new ConsoleTransport({
148
148
  reconnectInterval: 100,
149
149
  maxReconnectAttempts: 5,
150
150
  });
@@ -172,13 +172,21 @@ describe("WebSocketManager REST/SSE transport", () => {
172
172
  const fetchMock = vi.fn(async () => ({
173
173
  ok: true,
174
174
  status: 200,
175
- json: async () => ({ success: true, data: { yaml: "http:\n port: 1\n" } }),
175
+ json: async () => ({
176
+ success: true,
177
+ data: {
178
+ source: "http:\n port: 1\n",
179
+ format: 'yaml',
180
+ revision: 'a'.repeat(64),
181
+ configKeys: [],
182
+ },
183
+ }),
176
184
  } as Response));
177
185
  vi.stubGlobal("fetch", fetchMock);
178
186
 
179
- const manager = new WebSocketManager();
180
- const data = await manager.getConfigYaml();
181
- expect(data.yaml).toContain("port");
187
+ const manager = new ConsoleTransport();
188
+ const data = await manager.getConfigSource();
189
+ expect(data.source).toContain("port");
182
190
  const init = fetchMock.mock.calls[0]![1] as RequestInit;
183
191
  expect((init.headers as Record<string, string>).Authorization).toBeUndefined();
184
192
  expect(init.method).toBe("POST");
@@ -193,31 +201,39 @@ describe("WebSocketManager REST/SSE transport", () => {
193
201
  } as Response));
194
202
  vi.stubGlobal("fetch", fetchMock);
195
203
 
196
- const manager = new WebSocketManager();
204
+ const manager = new ConsoleTransport();
197
205
  await manager.setConfig("sandbox", { endpoints: [] });
198
206
  const init = fetchMock.mock.calls[0]![1] as RequestInit;
199
207
  expect((init.headers as Record<string, string>).Authorization).toBe("Bearer t");
200
208
  });
201
209
 
202
- it("normalizes legacy endpoint push names and payload aliases before callbacks", async () => {
203
- installBrowserGlobals();
204
- const received: WebSocketMessage[] = [];
205
- const manager = new WebSocketManager({}, {
206
- onMessage: (message) => received.push(message),
210
+ it('exposes marketplace discovery and lifecycle methods for Console pages', async () => {
211
+ installBrowserGlobals(mockStorage({ zhin_api_token: 'token' }));
212
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => ({
213
+ ok: true,
214
+ status: 200,
215
+ json: async () => ({
216
+ success: true,
217
+ data: String(input).includes('/marketplace/search') ? { items: [] } : { ok: true },
218
+ }),
219
+ headers: init?.headers,
220
+ } as Response));
221
+ const manager = new ConsoleTransport({ fetch: fetchMock });
222
+
223
+ await manager.searchMarketplace({ keyword: 'telegram', official: true });
224
+ await manager.getMarketplacePlugin('@zhin.js/adapter-telegram');
225
+ await manager.getPluginUpdates();
226
+ await manager.setPluginEnabled('telegram', false);
227
+
228
+ expect(String(fetchMock.mock.calls[0]![0])).toContain('/pub/marketplace/search?q=telegram&official=true');
229
+ expect(String(fetchMock.mock.calls[1]![0])).toContain('%40zhin.js%2Fadapter-telegram');
230
+ expect(String(fetchMock.mock.calls[2]![0])).toContain('/api/marketplace/updates');
231
+ const lifecycleBody = JSON.parse(String((fetchMock.mock.calls[3]![1] as RequestInit).body));
232
+ expect(lifecycleBody).toMatchObject({
233
+ type: 'plugin:set-enabled',
234
+ instanceKey: 'telegram',
235
+ enabled: false,
207
236
  });
208
-
209
- (manager as unknown as { handleMessage(event: MessageEvent): void }).handleMessage({
210
- data: '{"type":"endpoint:message","data":{"$adapter":"sandbox","endpoint":"bot"}}',
211
- } as MessageEvent);
212
- await Promise.resolve();
213
-
214
- expect(received).toEqual([expect.objectContaining({
215
- type: "message.receive",
216
- data: expect.objectContaining({ adapter: "sandbox", endpointKey: "bot" }),
217
- })]);
218
- expect(mockApplyConsoleEvent).toHaveBeenCalledWith(expect.objectContaining({
219
- type: "message.receive",
220
- }));
221
237
  });
222
238
 
223
239
  it("stops reconnecting after maxReconnectAttempts", async () => {
@@ -225,7 +241,7 @@ describe("WebSocketManager REST/SSE transport", () => {
225
241
  const fetchMock = mockConsoleTransport(() => sseResponse(500, null));
226
242
  vi.stubGlobal("fetch", fetchMock);
227
243
 
228
- const manager = new WebSocketManager({
244
+ const manager = new ConsoleTransport({
229
245
  reconnectInterval: 10,
230
246
  maxReconnectAttempts: 2,
231
247
  });
@@ -270,7 +286,7 @@ describe("WebSocketManager REST/SSE transport", () => {
270
286
  ));
271
287
  vi.stubGlobal('fetch', fetchMock);
272
288
  const received: Array<{ eventId: number; delivery?: string }> = [];
273
- const manager = new WebSocketManager();
289
+ const manager = new ConsoleTransport();
274
290
  manager.onConsoleEvent('message.receive', (event) => received.push({ eventId: event.eventId, delivery: event.delivery }));
275
291
  manager.connect();
276
292
  await vi.advanceTimersByTimeAsync(0);
@@ -301,7 +317,7 @@ describe("WebSocketManager REST/SSE transport", () => {
301
317
  });
302
318
  vi.stubGlobal('fetch', fetchMock);
303
319
  const received: number[] = [];
304
- const manager = new WebSocketManager();
320
+ const manager = new ConsoleTransport();
305
321
  manager.onConsoleEvent('message.receive', (event) => received.push(event.eventId));
306
322
  const recover = manager as unknown as { recoverEventHistory(signal: AbortSignal): Promise<void> };
307
323
  await recover.recoverEventHistory(new AbortController().signal);
@@ -320,7 +336,7 @@ describe("WebSocketManager REST/SSE transport", () => {
320
336
  return { ...body, data: { ...body.data, gap: true, oldestAvailableEventId: 8 } };
321
337
  };
322
338
  vi.stubGlobal('fetch', vi.fn(async () => response));
323
- const manager = new WebSocketManager();
339
+ const manager = new ConsoleTransport();
324
340
  const gaps: unknown[] = [];
325
341
  manager.onConsoleEventRecoveryGap((gap) => gaps.push(gap));
326
342
  const recover = manager as unknown as { recoverEventHistory(signal: AbortSignal): Promise<void> };
@@ -340,10 +356,39 @@ describe("WebSocketManager REST/SSE transport", () => {
340
356
  data: { adapter: 'sandbox', endpointKey: 'bot', content: 'history' },
341
357
  };
342
358
  vi.stubGlobal('fetch', vi.fn(async () => eventHistoryResponse([historyEvent], 'runtime-a', 1)));
343
- const manager = new WebSocketManager();
359
+ const manager = new ConsoleTransport();
344
360
  const recover = manager as unknown as { recoverEventHistory(signal: AbortSignal): Promise<void> };
345
361
  await recover.recoverEventHistory(new AbortController().signal);
346
362
 
347
363
  expect(Object.keys(stored).filter((key) => key.includes('event-cursor'))).toEqual([]);
348
364
  });
365
+
366
+ it('makes disposal terminal and idempotent', () => {
367
+ const transport = new ConsoleTransport();
368
+ transport.dispose();
369
+ transport.dispose();
370
+
371
+ expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
372
+ expect(() => transport.connect()).toThrow('ConsoleTransport has been disposed');
373
+ expect(() => transport.onConnectionChange(() => {}))
374
+ .toThrow('ConsoleTransport has been disposed');
375
+ });
376
+
377
+ it('rejects a stale connection result after disposal', async () => {
378
+ installBrowserGlobals();
379
+ let resolveHistory!: (response: Response) => void;
380
+ const fetchMock = vi.fn(() => new Promise<Response>((resolve) => {
381
+ resolveHistory = resolve;
382
+ }));
383
+ const transport = new ConsoleTransport({ fetch: fetchMock });
384
+
385
+ transport.connect();
386
+ transport.dispose();
387
+ resolveHistory(eventHistoryResponse());
388
+ await Promise.resolve();
389
+ await Promise.resolve();
390
+
391
+ expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
392
+ expect(fetchMock).toHaveBeenCalledOnce();
393
+ });
349
394
  });