@yushaw/sanqian-sdk 0.2.9 → 0.2.11

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.
@@ -0,0 +1,1058 @@
1
+ // src/client.browser.ts
2
+ import WebSocket from "isomorphic-ws";
3
+
4
+ // src/errors.ts
5
+ var SANQIAN_WEBSITE = "https://sanqian.io";
6
+ var SDKErrorCode = /* @__PURE__ */ ((SDKErrorCode2) => {
7
+ SDKErrorCode2["NOT_INSTALLED"] = "NOT_INSTALLED";
8
+ SDKErrorCode2["NOT_RUNNING"] = "NOT_RUNNING";
9
+ SDKErrorCode2["CONNECTION_TIMEOUT"] = "CONNECTION_TIMEOUT";
10
+ SDKErrorCode2["STARTUP_TIMEOUT"] = "STARTUP_TIMEOUT";
11
+ SDKErrorCode2["REGISTRATION_FAILED"] = "REGISTRATION_FAILED";
12
+ SDKErrorCode2["REQUEST_TIMEOUT"] = "REQUEST_TIMEOUT";
13
+ SDKErrorCode2["REQUEST_FAILED"] = "REQUEST_FAILED";
14
+ SDKErrorCode2["DISCONNECTED"] = "DISCONNECTED";
15
+ SDKErrorCode2["WEBSOCKET_ERROR"] = "WEBSOCKET_ERROR";
16
+ SDKErrorCode2["AGENT_NOT_FOUND"] = "AGENT_NOT_FOUND";
17
+ SDKErrorCode2["CONVERSATION_NOT_FOUND"] = "CONVERSATION_NOT_FOUND";
18
+ SDKErrorCode2["TOOL_NOT_FOUND"] = "TOOL_NOT_FOUND";
19
+ SDKErrorCode2["TOOL_EXECUTION_TIMEOUT"] = "TOOL_EXECUTION_TIMEOUT";
20
+ return SDKErrorCode2;
21
+ })(SDKErrorCode || {});
22
+ var ErrorMessages = {
23
+ ["NOT_INSTALLED" /* NOT_INSTALLED */]: {
24
+ en: `Sanqian is not installed on this computer.`,
25
+ zh: `Sanqian \u5C1A\u672A\u5B89\u88C5\u5728\u6B64\u7535\u8111\u4E0A\u3002`,
26
+ hint: {
27
+ en: `Please download and install Sanqian from ${SANQIAN_WEBSITE}`,
28
+ zh: `\u8BF7\u8BBF\u95EE ${SANQIAN_WEBSITE} \u4E0B\u8F7D\u5B89\u88C5 Sanqian`
29
+ }
30
+ },
31
+ ["NOT_RUNNING" /* NOT_RUNNING */]: {
32
+ en: `Sanqian is not running.`,
33
+ zh: `Sanqian \u672A\u5728\u8FD0\u884C\u3002`,
34
+ hint: {
35
+ en: `Please start Sanqian first, or enable autoLaunchSanqian option in SDK config.`,
36
+ zh: `\u8BF7\u5148\u542F\u52A8 Sanqian\uFF0C\u6216\u5728 SDK \u914D\u7F6E\u4E2D\u542F\u7528 autoLaunchSanqian \u9009\u9879\u3002`
37
+ }
38
+ },
39
+ ["CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */]: {
40
+ en: `Failed to connect to Sanqian (connection timeout).`,
41
+ zh: `\u8FDE\u63A5 Sanqian \u5931\u8D25\uFF08\u8FDE\u63A5\u8D85\u65F6\uFF09\u3002`,
42
+ hint: {
43
+ en: `Please check if Sanqian is running properly. If the problem persists, try restarting Sanqian.`,
44
+ zh: `\u8BF7\u68C0\u67E5 Sanqian \u662F\u5426\u6B63\u5E38\u8FD0\u884C\u3002\u5982\u95EE\u9898\u6301\u7EED\uFF0C\u8BF7\u5C1D\u8BD5\u91CD\u542F Sanqian\u3002`
45
+ }
46
+ },
47
+ ["STARTUP_TIMEOUT" /* STARTUP_TIMEOUT */]: {
48
+ en: `Sanqian failed to start within 2 minutes.`,
49
+ zh: `Sanqian \u5728 2 \u5206\u949F\u5185\u672A\u80FD\u542F\u52A8\u3002`,
50
+ hint: {
51
+ en: `Please try starting Sanqian manually. If it fails to start, reinstall from ${SANQIAN_WEBSITE}`,
52
+ zh: `\u8BF7\u5C1D\u8BD5\u624B\u52A8\u542F\u52A8 Sanqian\u3002\u5982\u4ECD\u65E0\u6CD5\u542F\u52A8\uFF0C\u8BF7\u4ECE ${SANQIAN_WEBSITE} \u91CD\u65B0\u5B89\u88C5\u3002`
53
+ }
54
+ },
55
+ ["REGISTRATION_FAILED" /* REGISTRATION_FAILED */]: {
56
+ en: `Failed to register with Sanqian.`,
57
+ zh: `\u5411 Sanqian \u6CE8\u518C\u5931\u8D25\u3002`,
58
+ hint: {
59
+ en: `Please check your SDK configuration (appName, tools). If the problem persists, try restarting Sanqian.`,
60
+ zh: `\u8BF7\u68C0\u67E5 SDK \u914D\u7F6E\uFF08appName\u3001tools\uFF09\u3002\u5982\u95EE\u9898\u6301\u7EED\uFF0C\u8BF7\u5C1D\u8BD5\u91CD\u542F Sanqian\u3002`
61
+ }
62
+ },
63
+ ["REQUEST_TIMEOUT" /* REQUEST_TIMEOUT */]: {
64
+ en: `Request timed out.`,
65
+ zh: `\u8BF7\u6C42\u8D85\u65F6\u3002`,
66
+ hint: {
67
+ en: `The operation took too long. Please try again.`,
68
+ zh: `\u64CD\u4F5C\u8017\u65F6\u8FC7\u957F\uFF0C\u8BF7\u91CD\u8BD5\u3002`
69
+ }
70
+ },
71
+ ["REQUEST_FAILED" /* REQUEST_FAILED */]: {
72
+ en: `Request failed.`,
73
+ zh: `\u8BF7\u6C42\u5931\u8D25\u3002`,
74
+ hint: {
75
+ en: `Please check the error details and try again.`,
76
+ zh: `\u8BF7\u68C0\u67E5\u9519\u8BEF\u8BE6\u60C5\u540E\u91CD\u8BD5\u3002`
77
+ }
78
+ },
79
+ ["DISCONNECTED" /* DISCONNECTED */]: {
80
+ en: `Disconnected from Sanqian.`,
81
+ zh: `\u4E0E Sanqian \u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00\u3002`,
82
+ hint: {
83
+ en: `The SDK will automatically reconnect. If problems persist, check if Sanqian is still running.`,
84
+ zh: `SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\u3002\u5982\u95EE\u9898\u6301\u7EED\uFF0C\u8BF7\u68C0\u67E5 Sanqian \u662F\u5426\u4ECD\u5728\u8FD0\u884C\u3002`
85
+ }
86
+ },
87
+ ["WEBSOCKET_ERROR" /* WEBSOCKET_ERROR */]: {
88
+ en: `WebSocket connection error.`,
89
+ zh: `WebSocket \u8FDE\u63A5\u9519\u8BEF\u3002`,
90
+ hint: {
91
+ en: `Please check your network and firewall settings. Sanqian uses local WebSocket on port shown in settings.`,
92
+ zh: `\u8BF7\u68C0\u67E5\u7F51\u7EDC\u548C\u9632\u706B\u5899\u8BBE\u7F6E\u3002Sanqian \u4F7F\u7528\u672C\u5730 WebSocket\uFF0C\u7AEF\u53E3\u53EF\u5728\u8BBE\u7F6E\u4E2D\u67E5\u770B\u3002`
93
+ }
94
+ },
95
+ ["AGENT_NOT_FOUND" /* AGENT_NOT_FOUND */]: {
96
+ en: `Agent not found.`,
97
+ zh: `\u627E\u4E0D\u5230\u8BE5 Agent\u3002`,
98
+ hint: {
99
+ en: `Please check the agent ID. Use listAgents() to see available agents.`,
100
+ zh: `\u8BF7\u68C0\u67E5 Agent ID\u3002\u4F7F\u7528 listAgents() \u67E5\u770B\u53EF\u7528\u7684 Agent\u3002`
101
+ }
102
+ },
103
+ ["CONVERSATION_NOT_FOUND" /* CONVERSATION_NOT_FOUND */]: {
104
+ en: `Conversation not found.`,
105
+ zh: `\u627E\u4E0D\u5230\u8BE5\u5BF9\u8BDD\u3002`,
106
+ hint: {
107
+ en: `The conversation may have been deleted. Start a new conversation with startConversation().`,
108
+ zh: `\u8BE5\u5BF9\u8BDD\u53EF\u80FD\u5DF2\u88AB\u5220\u9664\u3002\u4F7F\u7528 startConversation() \u5F00\u59CB\u65B0\u5BF9\u8BDD\u3002`
109
+ }
110
+ },
111
+ ["TOOL_NOT_FOUND" /* TOOL_NOT_FOUND */]: {
112
+ en: `Tool not found.`,
113
+ zh: `\u627E\u4E0D\u5230\u8BE5\u5DE5\u5177\u3002`,
114
+ hint: {
115
+ en: `Please check that the tool is registered in your SDK config.`,
116
+ zh: `\u8BF7\u68C0\u67E5\u8BE5\u5DE5\u5177\u662F\u5426\u5DF2\u5728 SDK \u914D\u7F6E\u4E2D\u6CE8\u518C\u3002`
117
+ }
118
+ },
119
+ ["TOOL_EXECUTION_TIMEOUT" /* TOOL_EXECUTION_TIMEOUT */]: {
120
+ en: `Tool execution timed out.`,
121
+ zh: `\u5DE5\u5177\u6267\u884C\u8D85\u65F6\u3002`,
122
+ hint: {
123
+ en: `The tool took too long to execute. Consider increasing toolExecutionTimeout in SDK config.`,
124
+ zh: `\u5DE5\u5177\u6267\u884C\u65F6\u95F4\u8FC7\u957F\u3002\u53EF\u5728 SDK \u914D\u7F6E\u4E2D\u589E\u52A0 toolExecutionTimeout\u3002`
125
+ }
126
+ }
127
+ };
128
+ var SanqianSDKError = class extends Error {
129
+ code;
130
+ messageZh;
131
+ hint;
132
+ hintZh;
133
+ constructor(code, details) {
134
+ const msg = ErrorMessages[code];
135
+ const fullMessage = details ? `${msg.en} ${details}` : msg.en;
136
+ super(fullMessage);
137
+ this.name = "SanqianSDKError";
138
+ this.code = code;
139
+ this.messageZh = details ? `${msg.zh} ${details}` : msg.zh;
140
+ this.hint = msg.hint?.en;
141
+ this.hintZh = msg.hint?.zh;
142
+ }
143
+ /**
144
+ * Get full error message with hint (English)
145
+ */
146
+ getFullMessage() {
147
+ return this.hint ? `${this.message}
148
+ ${this.hint}` : this.message;
149
+ }
150
+ /**
151
+ * Get full error message with hint (Chinese)
152
+ */
153
+ getFullMessageZh() {
154
+ return this.hintZh ? `${this.messageZh}
155
+ ${this.hintZh}` : this.messageZh;
156
+ }
157
+ /**
158
+ * Get bilingual error message
159
+ */
160
+ getBilingualMessage() {
161
+ return `${this.getFullMessage()}
162
+
163
+ ${this.getFullMessageZh()}`;
164
+ }
165
+ };
166
+ function createSDKError(code, details) {
167
+ return new SanqianSDKError(code, details);
168
+ }
169
+
170
+ // src/client.browser.ts
171
+ var SanqianSDK = class _SanqianSDK {
172
+ config;
173
+ ws = null;
174
+ connectionInfo = null;
175
+ state = {
176
+ connected: false,
177
+ registering: false,
178
+ registered: false,
179
+ reconnectAttempts: 0
180
+ };
181
+ // Tool handlers by name
182
+ toolHandlers = /* @__PURE__ */ new Map();
183
+ // Pending request futures
184
+ pendingRequests = /* @__PURE__ */ new Map();
185
+ // Timers
186
+ heartbeatTimer = null;
187
+ reconnectTimer = null;
188
+ heartbeatAckPending = false;
189
+ missedHeartbeats = 0;
190
+ static MAX_MISSED_HEARTBEATS = 2;
191
+ // Connection promise for deduplication
192
+ connectingPromise = null;
193
+ // Reconnect reference count
194
+ reconnectRefCount = 0;
195
+ // Event listeners
196
+ eventListeners = /* @__PURE__ */ new Map();
197
+ // ============================================
198
+ // Debug Logging
199
+ // ============================================
200
+ log(...args) {
201
+ if (this.config.debug) {
202
+ console.log("[SDK]", ...args);
203
+ }
204
+ }
205
+ warn(...args) {
206
+ if (this.config.debug) {
207
+ console.warn("[SDK]", ...args);
208
+ }
209
+ }
210
+ constructor(config) {
211
+ if (!config.connectionInfo) {
212
+ throw new SanqianSDKError(
213
+ "NOT_RUNNING" /* NOT_RUNNING */,
214
+ "Browser build requires connectionInfo in config. Use the Node.js build for auto-discovery, or provide connectionInfo manually."
215
+ );
216
+ }
217
+ this.config = {
218
+ reconnectInterval: 5e3,
219
+ heartbeatInterval: 3e4,
220
+ toolExecutionTimeout: 3e4,
221
+ debug: false,
222
+ autoLaunchSanqian: false,
223
+ // Not supported in browser
224
+ ...config
225
+ };
226
+ for (const tool of config.tools) {
227
+ this.toolHandlers.set(tool.name, tool.handler);
228
+ }
229
+ }
230
+ /**
231
+ * Build WebSocket URL from connection info
232
+ */
233
+ buildWebSocketUrl(info) {
234
+ const wsPath = info.ws_path || "/ws/apps";
235
+ return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
236
+ }
237
+ // ============================================
238
+ // Lifecycle
239
+ // ============================================
240
+ async connect() {
241
+ return this.ensureReady();
242
+ }
243
+ async connectWithInfo(info) {
244
+ this.connectionInfo = info;
245
+ const url = this.buildWebSocketUrl(info);
246
+ return new Promise((resolve, reject) => {
247
+ this.log(`Connecting to ${url}`);
248
+ this.ws = new WebSocket(url);
249
+ const connectTimeout = setTimeout(() => {
250
+ reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
251
+ this.ws?.close();
252
+ }, 1e4);
253
+ this.ws.onopen = async () => {
254
+ clearTimeout(connectTimeout);
255
+ this.log("WebSocket connected");
256
+ this.state.connected = true;
257
+ this.state.reconnectAttempts = 0;
258
+ this.emit("connected");
259
+ try {
260
+ await this.register();
261
+ resolve();
262
+ } catch (e) {
263
+ reject(e);
264
+ }
265
+ };
266
+ this.ws.onclose = (event) => {
267
+ const reasonRaw = event.reason;
268
+ const reason = typeof reasonRaw === "string" ? reasonRaw : reasonRaw?.toString() || "";
269
+ this.log(`WebSocket closed: ${event.code} ${reason}`);
270
+ this.handleDisconnect(reason);
271
+ };
272
+ this.ws.onerror = (event) => {
273
+ const error = event.error || new Error("WebSocket error");
274
+ console.error("[SDK] WebSocket error:", error);
275
+ this.state.lastError = error;
276
+ this.emit("error", error);
277
+ };
278
+ this.ws.onmessage = (event) => {
279
+ try {
280
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
281
+ const message = JSON.parse(data);
282
+ this.handleMessage(message);
283
+ } catch (e) {
284
+ this.warn("Failed to parse message:", e);
285
+ }
286
+ };
287
+ });
288
+ }
289
+ async disconnect() {
290
+ this.stopHeartbeat();
291
+ this.stopReconnect();
292
+ if (this.ws) {
293
+ this.ws.close(1e3, "Client disconnect");
294
+ this.ws = null;
295
+ }
296
+ this.state = {
297
+ connected: false,
298
+ registering: false,
299
+ registered: false,
300
+ reconnectAttempts: 0
301
+ };
302
+ }
303
+ // ============================================
304
+ // Registration
305
+ // ============================================
306
+ async register() {
307
+ this.state.registering = true;
308
+ const msgId = this.generateId();
309
+ const message = {
310
+ id: msgId,
311
+ type: "register",
312
+ app: {
313
+ name: this.config.appName,
314
+ version: this.config.appVersion,
315
+ display_name: this.config.displayName,
316
+ launch_command: this.config.launchCommand
317
+ },
318
+ tools: this.config.tools.map((t) => ({
319
+ name: `${this.config.appName}:${t.name}`,
320
+ description: t.description,
321
+ parameters: t.parameters,
322
+ searchable: t.searchable ?? true
323
+ }))
324
+ };
325
+ try {
326
+ const response = await this.sendAndWait(
327
+ message,
328
+ msgId,
329
+ 1e4
330
+ );
331
+ if (!response.success) {
332
+ throw createSDKError("REGISTRATION_FAILED" /* REGISTRATION_FAILED */, response.error);
333
+ }
334
+ this.state.registering = false;
335
+ this.state.registered = true;
336
+ this.startHeartbeat();
337
+ this.emit("registered");
338
+ this.log(`Registered as '${this.config.appName}'`);
339
+ } catch (e) {
340
+ this.state.registering = false;
341
+ throw e;
342
+ }
343
+ }
344
+ // ============================================
345
+ // Message Handling
346
+ // ============================================
347
+ handleMessage(message) {
348
+ const { id, type } = message;
349
+ if (id && this.pendingRequests.has(id)) {
350
+ const pending = this.pendingRequests.get(id);
351
+ this.pendingRequests.delete(id);
352
+ pending.resolve(message);
353
+ return;
354
+ }
355
+ switch (type) {
356
+ case "tool_call":
357
+ this.handleToolCall(message);
358
+ break;
359
+ case "heartbeat_ack":
360
+ this.heartbeatAckPending = false;
361
+ this.missedHeartbeats = 0;
362
+ break;
363
+ case "chat_stream":
364
+ this.handleChatStream(message);
365
+ break;
366
+ default:
367
+ this.warn(`Unknown message type: ${type}`);
368
+ }
369
+ }
370
+ handleChatStream(message) {
371
+ const {
372
+ id,
373
+ event,
374
+ content,
375
+ tool_call,
376
+ tool_result,
377
+ conversation_id,
378
+ title,
379
+ usage,
380
+ error
381
+ } = message;
382
+ if (!id) return;
383
+ const handler = this.streamHandlers.get(id);
384
+ if (!handler) {
385
+ this.warn(`No stream handler for message ${id}`);
386
+ return;
387
+ }
388
+ switch (event) {
389
+ case "text":
390
+ handler.onEvent({ type: "text", content });
391
+ break;
392
+ case "tool_call":
393
+ handler.onEvent({ type: "tool_call", tool_call });
394
+ break;
395
+ case "tool_result":
396
+ if (tool_result) {
397
+ handler.onEvent({
398
+ type: "tool_call",
399
+ tool_call: {
400
+ id: tool_result.call_id,
401
+ type: "function",
402
+ function: {
403
+ name: "tool_result",
404
+ arguments: JSON.stringify(tool_result)
405
+ }
406
+ }
407
+ });
408
+ }
409
+ break;
410
+ case "done":
411
+ handler.onDone({
412
+ message: message.message || { role: "assistant", content: "" },
413
+ conversationId: conversation_id || "",
414
+ title,
415
+ usage
416
+ });
417
+ break;
418
+ case "error":
419
+ handler.onError(new Error(error || "Unknown stream error"));
420
+ break;
421
+ }
422
+ }
423
+ async handleToolCall(message) {
424
+ this.log(`handleToolCall received:`, JSON.stringify(message));
425
+ const { id, call_id, name, arguments: args } = message;
426
+ const msgId = id || call_id;
427
+ const toolName = name.includes(":") ? name.split(":")[1] : name;
428
+ this.log(
429
+ `Looking for handler: '${toolName}', available handlers:`,
430
+ Array.from(this.toolHandlers.keys())
431
+ );
432
+ const handler = this.toolHandlers.get(toolName);
433
+ this.emit("tool_call", { name: toolName, arguments: args });
434
+ if (!handler) {
435
+ await this.sendToolResult(
436
+ msgId,
437
+ call_id,
438
+ false,
439
+ void 0,
440
+ `Tool '${toolName}' not found`
441
+ );
442
+ return;
443
+ }
444
+ try {
445
+ this.log(`Executing tool '${toolName}' with args:`, args);
446
+ const result = await Promise.race([
447
+ handler(args),
448
+ this.createTimeout(this.config.toolExecutionTimeout)
449
+ ]);
450
+ this.log(`Tool '${toolName}' completed successfully`);
451
+ await this.sendToolResult(msgId, call_id, true, result);
452
+ } catch (e) {
453
+ const errorMessage = e instanceof Error ? e.message : String(e);
454
+ const errorStack = e instanceof Error ? e.stack : void 0;
455
+ this.log(`Tool '${toolName}' failed:`, errorMessage);
456
+ if (errorStack) {
457
+ this.log(`Tool error stack:`, errorStack);
458
+ }
459
+ await this.sendToolResult(msgId, call_id, false, void 0, errorMessage);
460
+ }
461
+ }
462
+ async sendToolResult(id, callId, success, result, error) {
463
+ const message = {
464
+ id,
465
+ type: "tool_result",
466
+ call_id: callId,
467
+ success,
468
+ result,
469
+ error
470
+ };
471
+ this.log(
472
+ `Sending tool_result for ${callId}:`,
473
+ success ? "success" : `error: ${error}`
474
+ );
475
+ this.send(message);
476
+ }
477
+ // ============================================
478
+ // Connection Management
479
+ // ============================================
480
+ handleDisconnect(reason) {
481
+ this.stopHeartbeat();
482
+ this.state.connected = false;
483
+ this.state.registered = false;
484
+ this.emit("disconnected", reason);
485
+ for (const [, pending] of this.pendingRequests) {
486
+ pending.reject(createSDKError("DISCONNECTED" /* DISCONNECTED */));
487
+ }
488
+ this.pendingRequests.clear();
489
+ if (reason === "Client disconnect") {
490
+ this.log("Client disconnected intentionally, skipping auto-reconnect");
491
+ return;
492
+ }
493
+ if (reason === "Replaced by new connection") {
494
+ this.log("Connection replaced by newer one, skipping auto-reconnect");
495
+ return;
496
+ }
497
+ if (this.connectingPromise) {
498
+ this.log(
499
+ "Connection attempt already in progress, skipping auto-reconnect"
500
+ );
501
+ return;
502
+ }
503
+ if (!this.shouldAutoReconnect()) {
504
+ this.log(
505
+ "No components require reconnection (refCount=0), skipping auto-reconnect"
506
+ );
507
+ return;
508
+ }
509
+ this.scheduleReconnect();
510
+ }
511
+ shouldAutoReconnect() {
512
+ return this.reconnectRefCount > 0;
513
+ }
514
+ acquireReconnect() {
515
+ this.reconnectRefCount++;
516
+ this.log(`acquireReconnect: refCount=${this.reconnectRefCount}`);
517
+ }
518
+ releaseReconnect() {
519
+ this.reconnectRefCount = Math.max(0, this.reconnectRefCount - 1);
520
+ this.log(`releaseReconnect: refCount=${this.reconnectRefCount}`);
521
+ }
522
+ scheduleReconnect() {
523
+ if (this.reconnectTimer) return;
524
+ const baseDelay = Math.min(
525
+ 500 * Math.pow(2, this.state.reconnectAttempts),
526
+ 5e3
527
+ );
528
+ const jitter = Math.random() * 500;
529
+ const delay = baseDelay + jitter;
530
+ this.log(
531
+ `Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`
532
+ );
533
+ this.reconnectTimer = setTimeout(async () => {
534
+ this.reconnectTimer = null;
535
+ this.state.reconnectAttempts++;
536
+ try {
537
+ await this.ensureReady();
538
+ } catch (e) {
539
+ console.error("[SDK] Reconnect failed:", e);
540
+ if (!this.isConnected()) {
541
+ this.scheduleReconnect();
542
+ }
543
+ }
544
+ }, delay);
545
+ }
546
+ stopReconnect() {
547
+ if (this.reconnectTimer) {
548
+ clearTimeout(this.reconnectTimer);
549
+ this.reconnectTimer = null;
550
+ }
551
+ }
552
+ // ============================================
553
+ // Heartbeat
554
+ // ============================================
555
+ startHeartbeat() {
556
+ this.stopHeartbeat();
557
+ this.missedHeartbeats = 0;
558
+ this.heartbeatAckPending = false;
559
+ this.heartbeatTimer = setInterval(() => {
560
+ if (this.heartbeatAckPending) {
561
+ this.missedHeartbeats++;
562
+ this.warn(
563
+ `Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`
564
+ );
565
+ if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
566
+ this.warn("Too many missed heartbeat acks, connection may be dead");
567
+ this.ws?.close(4e3, "Heartbeat timeout");
568
+ return;
569
+ }
570
+ }
571
+ const message = {
572
+ type: "heartbeat",
573
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
574
+ };
575
+ this.heartbeatAckPending = true;
576
+ try {
577
+ this.send(message);
578
+ } catch (e) {
579
+ console.error("[SDK] Heartbeat send failed:", e);
580
+ this.ws?.close(4e3, "Heartbeat send failed");
581
+ }
582
+ }, this.config.heartbeatInterval);
583
+ }
584
+ stopHeartbeat() {
585
+ if (this.heartbeatTimer) {
586
+ clearInterval(this.heartbeatTimer);
587
+ this.heartbeatTimer = null;
588
+ }
589
+ }
590
+ // ============================================
591
+ // Communication
592
+ // ============================================
593
+ send(message) {
594
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
595
+ const error = new Error(
596
+ `WebSocket not open (state: ${this.ws?.readyState ?? "null"}), cannot send message`
597
+ );
598
+ console.error(`[SDK] ${error.message}:`, message);
599
+ throw error;
600
+ }
601
+ const data = JSON.stringify(message);
602
+ this.log(`WebSocket send:`, data.substring(0, 200));
603
+ this.ws.send(data);
604
+ }
605
+ sendAndWait(message, id, timeout) {
606
+ return new Promise((resolve, reject) => {
607
+ const timer = setTimeout(() => {
608
+ this.pendingRequests.delete(id);
609
+ reject(createSDKError("REQUEST_TIMEOUT" /* REQUEST_TIMEOUT */));
610
+ }, timeout);
611
+ this.pendingRequests.set(id, {
612
+ resolve: (value) => {
613
+ clearTimeout(timer);
614
+ resolve(value);
615
+ },
616
+ reject: (error) => {
617
+ clearTimeout(timer);
618
+ reject(error);
619
+ }
620
+ });
621
+ try {
622
+ this.send(message);
623
+ } catch (e) {
624
+ clearTimeout(timer);
625
+ this.pendingRequests.delete(id);
626
+ reject(e);
627
+ }
628
+ });
629
+ }
630
+ // ============================================
631
+ // Public API
632
+ // ============================================
633
+ getState() {
634
+ return { ...this.state };
635
+ }
636
+ isConnected() {
637
+ return this.state.connected && this.state.registered;
638
+ }
639
+ async ensureReady() {
640
+ if (this.isConnected()) {
641
+ return;
642
+ }
643
+ if (this.connectingPromise) {
644
+ this.log("Connection already in progress, waiting...");
645
+ return this.connectingPromise;
646
+ }
647
+ this.connectingPromise = this.doFullConnect();
648
+ try {
649
+ await this.connectingPromise;
650
+ } finally {
651
+ this.connectingPromise = null;
652
+ }
653
+ }
654
+ async doFullConnect() {
655
+ this.log("Starting connection (browser mode)...");
656
+ const info = this.config.connectionInfo;
657
+ await this.connectWithInfo(info);
658
+ }
659
+ async updateTools(tools) {
660
+ this.toolHandlers.clear();
661
+ for (const tool of tools) {
662
+ this.toolHandlers.set(tool.name, tool.handler);
663
+ }
664
+ if (this.isConnected()) {
665
+ const msgId = this.generateId();
666
+ const message = {
667
+ id: msgId,
668
+ type: "tools_update",
669
+ tools: tools.map((t) => ({
670
+ name: `${this.config.appName}:${t.name}`,
671
+ description: t.description,
672
+ parameters: t.parameters,
673
+ searchable: t.searchable ?? true
674
+ }))
675
+ };
676
+ await this.sendAndWait(message, msgId, 5e3);
677
+ }
678
+ }
679
+ // ============================================
680
+ // Private Agent API
681
+ // ============================================
682
+ async createAgent(config) {
683
+ await this.ensureReady();
684
+ const msgId = this.generateId();
685
+ const message = {
686
+ id: msgId,
687
+ type: "create_agent",
688
+ agent: config
689
+ };
690
+ const response = await this.sendAndWait(
691
+ message,
692
+ msgId,
693
+ 1e4
694
+ );
695
+ if (!response.success) {
696
+ throw createSDKError("AGENT_NOT_FOUND" /* AGENT_NOT_FOUND */, response.error);
697
+ }
698
+ if (response.agent) {
699
+ return response.agent;
700
+ }
701
+ return {
702
+ agent_id: response.agent_id,
703
+ name: config.name,
704
+ description: config.description,
705
+ tools: config.tools || []
706
+ };
707
+ }
708
+ async listAgents() {
709
+ await this.ensureReady();
710
+ const msgId = this.generateId();
711
+ const message = {
712
+ id: msgId,
713
+ type: "list_agents"
714
+ };
715
+ const response = await this.sendAndWait(
716
+ message,
717
+ msgId,
718
+ 1e4
719
+ );
720
+ if (response.error) {
721
+ throw new Error(response.error);
722
+ }
723
+ return response.agents;
724
+ }
725
+ async deleteAgent(agentId) {
726
+ await this.ensureReady();
727
+ const msgId = this.generateId();
728
+ const message = {
729
+ id: msgId,
730
+ type: "delete_agent",
731
+ agent_id: agentId
732
+ };
733
+ const response = await this.sendAndWait(
734
+ message,
735
+ msgId,
736
+ 1e4
737
+ );
738
+ if (!response.success) {
739
+ throw createSDKError("AGENT_NOT_FOUND" /* AGENT_NOT_FOUND */, response.error);
740
+ }
741
+ }
742
+ async updateAgent(agentId, updates) {
743
+ await this.ensureReady();
744
+ const msgId = this.generateId();
745
+ const message = {
746
+ id: msgId,
747
+ type: "update_agent",
748
+ agent_id: agentId,
749
+ updates
750
+ };
751
+ const response = await this.sendAndWait(
752
+ message,
753
+ msgId,
754
+ 1e4
755
+ );
756
+ if (!response.success || !response.agent) {
757
+ throw createSDKError("AGENT_NOT_FOUND" /* AGENT_NOT_FOUND */, response.error);
758
+ }
759
+ return response.agent;
760
+ }
761
+ // ============================================
762
+ // Conversation API
763
+ // ============================================
764
+ async listConversations(options) {
765
+ await this.ensureReady();
766
+ const msgId = this.generateId();
767
+ const message = {
768
+ id: msgId,
769
+ type: "list_conversations",
770
+ agent_id: options?.agentId,
771
+ limit: options?.limit,
772
+ offset: options?.offset
773
+ };
774
+ const response = await this.sendAndWait(
775
+ message,
776
+ msgId,
777
+ 1e4
778
+ );
779
+ if (response.error) {
780
+ throw new Error(response.error);
781
+ }
782
+ return {
783
+ conversations: response.conversations,
784
+ total: response.total
785
+ };
786
+ }
787
+ async getConversation(conversationId, options) {
788
+ await this.ensureReady();
789
+ const msgId = this.generateId();
790
+ const message = {
791
+ id: msgId,
792
+ type: "get_conversation",
793
+ conversation_id: conversationId,
794
+ include_messages: options?.includeMessages ?? true,
795
+ message_limit: options?.messageLimit,
796
+ message_offset: options?.messageOffset
797
+ };
798
+ const response = await this.sendAndWait(
799
+ message,
800
+ msgId,
801
+ 1e4
802
+ );
803
+ if (!response.success || !response.conversation) {
804
+ throw createSDKError("CONVERSATION_NOT_FOUND" /* CONVERSATION_NOT_FOUND */, response.error);
805
+ }
806
+ return response.conversation;
807
+ }
808
+ async deleteConversation(conversationId) {
809
+ await this.ensureReady();
810
+ const msgId = this.generateId();
811
+ const message = {
812
+ id: msgId,
813
+ type: "delete_conversation",
814
+ conversation_id: conversationId
815
+ };
816
+ const response = await this.sendAndWait(
817
+ message,
818
+ msgId,
819
+ 1e4
820
+ );
821
+ if (!response.success) {
822
+ throw createSDKError("CONVERSATION_NOT_FOUND" /* CONVERSATION_NOT_FOUND */, response.error);
823
+ }
824
+ }
825
+ // ============================================
826
+ // Chat API
827
+ // ============================================
828
+ streamHandlers = /* @__PURE__ */ new Map();
829
+ async chat(agentId, messages, options) {
830
+ await this.ensureReady();
831
+ const msgId = this.generateId();
832
+ const message = {
833
+ id: msgId,
834
+ type: "chat",
835
+ agent_id: agentId,
836
+ messages,
837
+ conversation_id: options?.conversationId,
838
+ stream: false,
839
+ remote_tools: options?.remoteTools?.map((t) => ({
840
+ name: t.name,
841
+ description: t.description,
842
+ parameters: t.parameters
843
+ }))
844
+ };
845
+ const response = await this.sendAndWait(
846
+ message,
847
+ msgId,
848
+ 6e5
849
+ );
850
+ if (!response.success) {
851
+ const errorLower = (response.error || "").toLowerCase();
852
+ if (errorLower.includes("agent") && errorLower.includes("not found")) {
853
+ throw createSDKError("AGENT_NOT_FOUND" /* AGENT_NOT_FOUND */, response.error);
854
+ }
855
+ throw createSDKError("REQUEST_FAILED" /* REQUEST_FAILED */, response.error);
856
+ }
857
+ return {
858
+ message: response.message,
859
+ conversationId: response.conversation_id,
860
+ title: response.title,
861
+ usage: response.usage
862
+ };
863
+ }
864
+ async *chatStream(agentId, messages, options) {
865
+ await this.ensureReady();
866
+ const msgId = this.generateId();
867
+ const message = {
868
+ id: msgId,
869
+ type: "chat",
870
+ agent_id: agentId,
871
+ messages,
872
+ conversation_id: options?.conversationId,
873
+ stream: true,
874
+ remote_tools: options?.remoteTools?.map((t) => ({
875
+ name: t.name,
876
+ description: t.description,
877
+ parameters: t.parameters
878
+ }))
879
+ };
880
+ const eventQueue = [];
881
+ let done = false;
882
+ let error = null;
883
+ let resolveNext = null;
884
+ this.streamHandlers.set(msgId, {
885
+ onEvent: (event) => {
886
+ eventQueue.push(event);
887
+ resolveNext?.();
888
+ },
889
+ onDone: (response) => {
890
+ eventQueue.push({
891
+ type: "done",
892
+ conversationId: response.conversationId,
893
+ title: response.title
894
+ });
895
+ done = true;
896
+ resolveNext?.();
897
+ },
898
+ onError: (e) => {
899
+ error = e;
900
+ resolveNext?.();
901
+ }
902
+ });
903
+ try {
904
+ this.send(message);
905
+ while (!done && !error) {
906
+ if (eventQueue.length > 0) {
907
+ yield eventQueue.shift();
908
+ } else {
909
+ await new Promise((resolve) => {
910
+ resolveNext = resolve;
911
+ });
912
+ resolveNext = null;
913
+ }
914
+ }
915
+ while (eventQueue.length > 0) {
916
+ yield eventQueue.shift();
917
+ }
918
+ if (error) {
919
+ throw error;
920
+ }
921
+ } finally {
922
+ this.streamHandlers.delete(msgId);
923
+ }
924
+ }
925
+ startConversation(agentId) {
926
+ return new Conversation(this, agentId);
927
+ }
928
+ // ============================================
929
+ // Events
930
+ // ============================================
931
+ on(event, listener) {
932
+ if (!this.eventListeners.has(event)) {
933
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
934
+ }
935
+ this.eventListeners.get(event).add(listener);
936
+ return this;
937
+ }
938
+ off(event, listener) {
939
+ this.eventListeners.get(event)?.delete(listener);
940
+ return this;
941
+ }
942
+ once(event, listener) {
943
+ const onceWrapper = ((...args) => {
944
+ this.off(event, onceWrapper);
945
+ listener(...args);
946
+ });
947
+ return this.on(event, onceWrapper);
948
+ }
949
+ /**
950
+ * Remove all event listeners
951
+ *
952
+ * Call this to prevent memory leaks when disposing the SDK instance.
953
+ * If event is specified, only removes listeners for that event.
954
+ */
955
+ removeAllListeners(event) {
956
+ if (event) {
957
+ this.eventListeners.delete(event);
958
+ } else {
959
+ this.eventListeners.clear();
960
+ }
961
+ return this;
962
+ }
963
+ emit(event, ...args) {
964
+ const listeners = this.eventListeners.get(event);
965
+ if (listeners) {
966
+ for (const listener of listeners) {
967
+ try {
968
+ listener(...args);
969
+ } catch (e) {
970
+ console.error(`[SDK] Event listener error for '${event}':`, e);
971
+ }
972
+ }
973
+ }
974
+ }
975
+ // ============================================
976
+ // Utilities
977
+ // ============================================
978
+ generateId() {
979
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
980
+ }
981
+ createTimeout(ms) {
982
+ return new Promise((_, reject) => {
983
+ setTimeout(
984
+ () => reject(createSDKError("TOOL_EXECUTION_TIMEOUT" /* TOOL_EXECUTION_TIMEOUT */)),
985
+ ms
986
+ );
987
+ });
988
+ }
989
+ };
990
+ var Conversation = class {
991
+ sdk;
992
+ agentId;
993
+ _conversationId = null;
994
+ constructor(sdk, agentId, conversationId) {
995
+ this.sdk = sdk;
996
+ this.agentId = agentId;
997
+ this._conversationId = conversationId || null;
998
+ }
999
+ get id() {
1000
+ return this._conversationId;
1001
+ }
1002
+ async send(content, options) {
1003
+ const response = await this.sdk.chat(
1004
+ this.agentId,
1005
+ [{ role: "user", content }],
1006
+ {
1007
+ conversationId: this._conversationId || void 0,
1008
+ remoteTools: options?.remoteTools
1009
+ }
1010
+ );
1011
+ if (response.conversationId && !this._conversationId) {
1012
+ this._conversationId = response.conversationId;
1013
+ }
1014
+ return response;
1015
+ }
1016
+ async *sendStream(content, options) {
1017
+ const stream = this.sdk.chatStream(
1018
+ this.agentId,
1019
+ [{ role: "user", content }],
1020
+ {
1021
+ conversationId: this._conversationId || void 0,
1022
+ remoteTools: options?.remoteTools
1023
+ }
1024
+ );
1025
+ for await (const event of stream) {
1026
+ if (event.type === "done" && event.conversationId && !this._conversationId) {
1027
+ this._conversationId = event.conversationId;
1028
+ }
1029
+ yield event;
1030
+ }
1031
+ }
1032
+ async delete() {
1033
+ if (!this._conversationId) {
1034
+ throw new Error("No conversation to delete");
1035
+ }
1036
+ await this.sdk.deleteConversation(this._conversationId);
1037
+ this._conversationId = null;
1038
+ }
1039
+ async getDetails(options) {
1040
+ if (!this._conversationId) {
1041
+ throw new Error("No conversation to get details for");
1042
+ }
1043
+ return this.sdk.getConversation(this._conversationId, {
1044
+ includeMessages: true,
1045
+ messageLimit: options?.messageLimit
1046
+ });
1047
+ }
1048
+ };
1049
+ export {
1050
+ Conversation,
1051
+ ErrorMessages,
1052
+ SANQIAN_WEBSITE,
1053
+ SDKErrorCode,
1054
+ SanqianSDK,
1055
+ SanqianSDKError,
1056
+ createSDKError
1057
+ };
1058
+ //# sourceMappingURL=index.browser.mjs.map