@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 +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +89 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
765
|
+
this.log(`Tool '${toolName}' failed:`, errorMessage);
|
|
732
766
|
if (errorStack) {
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
+
this.log("Client disconnected intentionally, skipping auto-reconnect");
|
|
764
798
|
return;
|
|
765
799
|
}
|
|
766
800
|
if (reason === "Replaced by new connection") {
|
|
767
|
-
|
|
801
|
+
this.log("Connection replaced by newer one, skipping auto-reconnect");
|
|
768
802
|
return;
|
|
769
803
|
}
|
|
770
804
|
if (this.connectingPromise) {
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
+
this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
|
|
816
879
|
if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
|
|
817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
+
this.log("Sanqian started, connection info available");
|
|
959
1022
|
return info;
|
|
960
1023
|
}
|
|
961
1024
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|