@yushaw/sanqian-sdk 0.2.6 → 0.2.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.
package/dist/index.d.mts CHANGED
@@ -28,6 +28,11 @@ interface SDKConfig {
28
28
  * If not provided, SDK will search in standard installation locations
29
29
  */
30
30
  sanqianPath?: string;
31
+ /**
32
+ * Enable debug logging (default: false)
33
+ * When enabled, SDK will output detailed logs to console
34
+ */
35
+ debug?: boolean;
31
36
  }
32
37
  interface ToolDefinition {
33
38
  /** Tool name (without app prefix) */
@@ -218,7 +223,17 @@ declare class SanqianSDK {
218
223
  private missedHeartbeats;
219
224
  private static readonly MAX_MISSED_HEARTBEATS;
220
225
  private connectingPromise;
226
+ private reconnectRefCount;
227
+ private launchedBySanqian;
221
228
  private eventListeners;
229
+ /**
230
+ * Log debug message (only when config.debug is true)
231
+ */
232
+ private log;
233
+ /**
234
+ * Log warning message (only when config.debug is true)
235
+ */
236
+ private warn;
222
237
  constructor(config: SDKConfig);
223
238
  /**
224
239
  * Connect to Sanqian
@@ -247,6 +262,23 @@ declare class SanqianSDK {
247
262
  private handleToolCall;
248
263
  private sendToolResult;
249
264
  private handleDisconnect;
265
+ /**
266
+ * Check if auto-reconnect should be enabled
267
+ * Returns true if any component has requested persistent connection
268
+ */
269
+ private shouldAutoReconnect;
270
+ /**
271
+ * Request persistent connection (enables auto-reconnect)
272
+ * Call this when a component needs the connection to stay alive (e.g., chat panel opens)
273
+ * Must be paired with releaseReconnect() when the component no longer needs it
274
+ */
275
+ acquireReconnect(): void;
276
+ /**
277
+ * Release persistent connection request
278
+ * Call this when a component no longer needs the connection (e.g., chat panel closes)
279
+ * When refCount reaches 0, auto-reconnect is disabled
280
+ */
281
+ releaseReconnect(): void;
250
282
  private scheduleReconnect;
251
283
  private stopReconnect;
252
284
  private startHeartbeat;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,11 @@ interface SDKConfig {
28
28
  * If not provided, SDK will search in standard installation locations
29
29
  */
30
30
  sanqianPath?: string;
31
+ /**
32
+ * Enable debug logging (default: false)
33
+ * When enabled, SDK will output detailed logs to console
34
+ */
35
+ debug?: boolean;
31
36
  }
32
37
  interface ToolDefinition {
33
38
  /** Tool name (without app prefix) */
@@ -218,7 +223,17 @@ declare class SanqianSDK {
218
223
  private missedHeartbeats;
219
224
  private static readonly MAX_MISSED_HEARTBEATS;
220
225
  private connectingPromise;
226
+ private reconnectRefCount;
227
+ private launchedBySanqian;
221
228
  private eventListeners;
229
+ /**
230
+ * Log debug message (only when config.debug is true)
231
+ */
232
+ private log;
233
+ /**
234
+ * Log warning message (only when config.debug is true)
235
+ */
236
+ private warn;
222
237
  constructor(config: SDKConfig);
223
238
  /**
224
239
  * Connect to Sanqian
@@ -247,6 +262,23 @@ declare class SanqianSDK {
247
262
  private handleToolCall;
248
263
  private sendToolResult;
249
264
  private handleDisconnect;
265
+ /**
266
+ * Check if auto-reconnect should be enabled
267
+ * Returns true if any component has requested persistent connection
268
+ */
269
+ private shouldAutoReconnect;
270
+ /**
271
+ * Request persistent connection (enables auto-reconnect)
272
+ * Call this when a component needs the connection to stay alive (e.g., chat panel opens)
273
+ * Must be paired with releaseReconnect() when the component no longer needs it
274
+ */
275
+ acquireReconnect(): void;
276
+ /**
277
+ * Release persistent connection request
278
+ * Call this when a component no longer needs the connection (e.g., chat panel closes)
279
+ * When refCount reaches 0, auto-reconnect is disabled
280
+ */
281
+ releaseReconnect(): void;
250
282
  private scheduleReconnect;
251
283
  private stopReconnect;
252
284
  private startHeartbeat;
package/dist/index.js CHANGED
@@ -501,19 +501,53 @@ var SanqianSDK = class _SanqianSDK {
501
501
  static MAX_MISSED_HEARTBEATS = 2;
502
502
  // Connection promise for deduplication (prevents multiple concurrent connect attempts)
503
503
  connectingPromise = null;
504
+ // Reconnect reference count - when > 0, auto-reconnect is enabled
505
+ // This allows multiple components to request persistent connections
506
+ reconnectRefCount = 0;
507
+ // Whether this app was launched by Sanqian (detected via environment variable)
508
+ launchedBySanqian = false;
504
509
  // Event listeners
505
510
  eventListeners = /* @__PURE__ */ new Map();
511
+ // ============================================
512
+ // Debug Logging
513
+ // ============================================
514
+ /**
515
+ * Log debug message (only when config.debug is true)
516
+ */
517
+ log(...args) {
518
+ if (this.config.debug) {
519
+ console.log("[SDK]", ...args);
520
+ }
521
+ }
522
+ /**
523
+ * Log warning message (only when config.debug is true)
524
+ */
525
+ warn(...args) {
526
+ if (this.config.debug) {
527
+ console.warn("[SDK]", ...args);
528
+ }
529
+ }
506
530
  constructor(config) {
507
531
  this.config = {
508
532
  reconnectInterval: 5e3,
509
533
  heartbeatInterval: 3e4,
510
534
  toolExecutionTimeout: 3e4,
535
+ debug: false,
511
536
  ...config
512
537
  };
513
538
  this.discovery = new DiscoveryManager();
514
539
  for (const tool of config.tools) {
515
540
  this.toolHandlers.set(tool.name, tool.handler);
516
541
  }
542
+ this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
543
+ if (this.launchedBySanqian) {
544
+ this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
545
+ setTimeout(() => {
546
+ this.connect().catch((err) => {
547
+ console.error("[SDK] Failed to connect after Sanqian launch:", err);
548
+ });
549
+ }, 0);
550
+ }
517
551
  }
518
552
  // ============================================
519
553
  // Lifecycle
@@ -540,7 +574,7 @@ var SanqianSDK = class _SanqianSDK {
540
574
  this.connectionInfo = info;
541
575
  const url = this.discovery.buildWebSocketUrl(info);
542
576
  return new Promise((resolve, reject) => {
543
- console.log(`[SDK] Connecting to ${url}`);
577
+ this.log(`Connecting to ${url}`);
544
578
  this.ws = new import_ws.default(url);
545
579
  const connectTimeout = setTimeout(() => {
546
580
  reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
@@ -548,7 +582,7 @@ var SanqianSDK = class _SanqianSDK {
548
582
  }, 1e4);
549
583
  this.ws.on("open", async () => {
550
584
  clearTimeout(connectTimeout);
551
- console.log("[SDK] WebSocket connected");
585
+ this.log("WebSocket connected");
552
586
  this.state.connected = true;
553
587
  this.state.reconnectAttempts = 0;
554
588
  this.emit("connected");
@@ -560,7 +594,7 @@ var SanqianSDK = class _SanqianSDK {
560
594
  }
561
595
  });
562
596
  this.ws.on("close", (code, reason) => {
563
- console.log(`[SDK] WebSocket closed: ${code} ${reason.toString()}`);
597
+ this.log(`WebSocket closed: ${code} ${reason.toString()}`);
564
598
  this.handleDisconnect(reason.toString());
565
599
  });
566
600
  this.ws.on("error", (error) => {
@@ -573,7 +607,7 @@ var SanqianSDK = class _SanqianSDK {
573
607
  const message = JSON.parse(data.toString());
574
608
  this.handleMessage(message);
575
609
  } catch (e) {
576
- console.error("[SDK] Failed to parse message:", e);
610
+ this.warn("Failed to parse message:", e);
577
611
  }
578
612
  });
579
613
  });
@@ -630,7 +664,7 @@ var SanqianSDK = class _SanqianSDK {
630
664
  this.state.registered = true;
631
665
  this.startHeartbeat();
632
666
  this.emit("registered");
633
- console.log(`[SDK] Registered as '${this.config.appName}'`);
667
+ this.log(`Registered as '${this.config.appName}'`);
634
668
  } catch (e) {
635
669
  this.state.registering = false;
636
670
  throw e;
@@ -659,7 +693,7 @@ var SanqianSDK = class _SanqianSDK {
659
693
  this.handleChatStream(message);
660
694
  break;
661
695
  default:
662
- console.warn(`[SDK] Unknown message type: ${type}`);
696
+ this.warn(`Unknown message type: ${type}`);
663
697
  }
664
698
  }
665
699
  handleChatStream(message) {
@@ -667,7 +701,7 @@ var SanqianSDK = class _SanqianSDK {
667
701
  if (!id) return;
668
702
  const handler = this.streamHandlers.get(id);
669
703
  if (!handler) {
670
- console.warn(`[SDK] No stream handler for message ${id}`);
704
+ this.warn(`No stream handler for message ${id}`);
671
705
  return;
672
706
  }
673
707
  switch (event) {
@@ -706,11 +740,11 @@ var SanqianSDK = class _SanqianSDK {
706
740
  }
707
741
  }
708
742
  async handleToolCall(message) {
709
- console.log(`[SDK] handleToolCall received:`, JSON.stringify(message));
743
+ this.log(`handleToolCall received:`, JSON.stringify(message));
710
744
  const { id, call_id, name, arguments: args } = message;
711
745
  const msgId = id || call_id;
712
746
  const toolName = name.includes(":") ? name.split(":")[1] : name;
713
- console.log(`[SDK] Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
747
+ this.log(`Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
714
748
  const handler = this.toolHandlers.get(toolName);
715
749
  this.emit("tool_call", { name: toolName, arguments: args });
716
750
  if (!handler) {
@@ -718,19 +752,19 @@ var SanqianSDK = class _SanqianSDK {
718
752
  return;
719
753
  }
720
754
  try {
721
- console.log(`[SDK] Executing tool '${toolName}' with args:`, args);
755
+ this.log(`Executing tool '${toolName}' with args:`, args);
722
756
  const result = await Promise.race([
723
757
  handler(args),
724
758
  this.createTimeout(this.config.toolExecutionTimeout)
725
759
  ]);
726
- console.log(`[SDK] Tool '${toolName}' completed successfully`);
760
+ this.log(`Tool '${toolName}' completed successfully`);
727
761
  await this.sendToolResult(msgId, call_id, true, result);
728
762
  } catch (e) {
729
763
  const errorMessage = e instanceof Error ? e.message : String(e);
730
764
  const errorStack = e instanceof Error ? e.stack : void 0;
731
- console.log(`[SDK] Tool '${toolName}' failed:`, errorMessage);
765
+ this.log(`Tool '${toolName}' failed:`, errorMessage);
732
766
  if (errorStack) {
733
- console.log(`[SDK] Tool error stack:`, errorStack);
767
+ this.log(`Tool error stack:`, errorStack);
734
768
  }
735
769
  await this.sendToolResult(msgId, call_id, false, void 0, errorMessage);
736
770
  }
@@ -744,7 +778,7 @@ var SanqianSDK = class _SanqianSDK {
744
778
  result,
745
779
  error
746
780
  };
747
- console.log(`[SDK] Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
781
+ this.log(`Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
748
782
  this.send(message);
749
783
  }
750
784
  // ============================================
@@ -760,19 +794,48 @@ var SanqianSDK = class _SanqianSDK {
760
794
  }
761
795
  this.pendingRequests.clear();
762
796
  if (reason === "Client disconnect") {
763
- console.log("[SDK] Client disconnected intentionally, skipping auto-reconnect");
797
+ this.log("Client disconnected intentionally, skipping auto-reconnect");
764
798
  return;
765
799
  }
766
800
  if (reason === "Replaced by new connection") {
767
- console.log("[SDK] Connection replaced by newer one, skipping auto-reconnect");
801
+ this.log("Connection replaced by newer one, skipping auto-reconnect");
768
802
  return;
769
803
  }
770
804
  if (this.connectingPromise) {
771
- console.log("[SDK] Connection attempt already in progress, skipping auto-reconnect");
805
+ this.log("Connection attempt already in progress, skipping auto-reconnect");
806
+ return;
807
+ }
808
+ if (!this.shouldAutoReconnect()) {
809
+ this.log("No components require reconnection (refCount=0), skipping auto-reconnect");
772
810
  return;
773
811
  }
774
812
  this.scheduleReconnect();
775
813
  }
814
+ /**
815
+ * Check if auto-reconnect should be enabled
816
+ * Returns true if any component has requested persistent connection
817
+ */
818
+ shouldAutoReconnect() {
819
+ return this.reconnectRefCount > 0;
820
+ }
821
+ /**
822
+ * Request persistent connection (enables auto-reconnect)
823
+ * Call this when a component needs the connection to stay alive (e.g., chat panel opens)
824
+ * Must be paired with releaseReconnect() when the component no longer needs it
825
+ */
826
+ acquireReconnect() {
827
+ this.reconnectRefCount++;
828
+ this.log(`acquireReconnect: refCount=${this.reconnectRefCount}`);
829
+ }
830
+ /**
831
+ * Release persistent connection request
832
+ * Call this when a component no longer needs the connection (e.g., chat panel closes)
833
+ * When refCount reaches 0, auto-reconnect is disabled
834
+ */
835
+ releaseReconnect() {
836
+ this.reconnectRefCount = Math.max(0, this.reconnectRefCount - 1);
837
+ this.log(`releaseReconnect: refCount=${this.reconnectRefCount}`);
838
+ }
776
839
  scheduleReconnect() {
777
840
  if (this.reconnectTimer) return;
778
841
  const baseDelay = Math.min(
@@ -782,7 +845,7 @@ var SanqianSDK = class _SanqianSDK {
782
845
  );
783
846
  const jitter = Math.random() * 500;
784
847
  const delay = baseDelay + jitter;
785
- console.log(`[SDK] Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
848
+ this.log(`Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
786
849
  this.reconnectTimer = setTimeout(async () => {
787
850
  this.reconnectTimer = null;
788
851
  this.state.reconnectAttempts++;
@@ -812,9 +875,9 @@ var SanqianSDK = class _SanqianSDK {
812
875
  this.heartbeatTimer = setInterval(() => {
813
876
  if (this.heartbeatAckPending) {
814
877
  this.missedHeartbeats++;
815
- console.warn(`[SDK] Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
878
+ this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
816
879
  if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
817
- console.error("[SDK] Too many missed heartbeat acks, connection may be dead");
880
+ this.warn("Too many missed heartbeat acks, connection may be dead");
818
881
  this.ws?.close(4e3, "Heartbeat timeout");
819
882
  return;
820
883
  }
@@ -850,7 +913,7 @@ var SanqianSDK = class _SanqianSDK {
850
913
  throw error;
851
914
  }
852
915
  const data = JSON.stringify(message);
853
- console.log(`[SDK] WebSocket send:`, data.substring(0, 200));
916
+ this.log(`WebSocket send:`, data.substring(0, 200));
854
917
  this.ws.send(data);
855
918
  }
856
919
  sendAndWait(message, id, timeout) {
@@ -911,7 +974,7 @@ var SanqianSDK = class _SanqianSDK {
911
974
  return;
912
975
  }
913
976
  if (this.connectingPromise) {
914
- console.log("[SDK] Connection already in progress, waiting...");
977
+ this.log("Connection already in progress, waiting...");
915
978
  return this.connectingPromise;
916
979
  }
917
980
  this.connectingPromise = this.doFullConnect();
@@ -929,16 +992,16 @@ var SanqianSDK = class _SanqianSDK {
929
992
  * so there's no need for additional launch-in-progress tracking within a single instance.
930
993
  */
931
994
  async doFullConnect() {
932
- console.log("[SDK] Starting full connection flow...");
995
+ this.log("Starting full connection flow...");
933
996
  let info = this.discovery.read();
934
997
  if (!info) {
935
998
  if (this.config.autoLaunchSanqian) {
936
- console.log("[SDK] Sanqian not running, attempting to launch...");
999
+ this.log("Sanqian not running, attempting to launch...");
937
1000
  const launched = this.discovery.launchSanqian(this.config.sanqianPath);
938
1001
  if (!launched) {
939
1002
  throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
940
1003
  }
941
- console.log("[SDK] Sanqian launch initiated, waiting for startup...");
1004
+ this.log("Sanqian launch initiated, waiting for startup...");
942
1005
  info = await this.waitForSanqianStartup();
943
1006
  } else {
944
1007
  throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
@@ -955,7 +1018,7 @@ var SanqianSDK = class _SanqianSDK {
955
1018
  while (Date.now() - startTime < timeout) {
956
1019
  const info = this.discovery.read();
957
1020
  if (info) {
958
- console.log("[SDK] Sanqian started, connection info available");
1021
+ this.log("Sanqian started, connection info available");
959
1022
  return info;
960
1023
  }
961
1024
  await new Promise((resolve) => setTimeout(resolve, pollInterval));