@yushaw/sanqian-sdk 0.2.6 → 0.2.8
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 +90 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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,54 @@ 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,
|
|
536
|
+
autoLaunchSanqian: true,
|
|
511
537
|
...config
|
|
512
538
|
};
|
|
513
539
|
this.discovery = new DiscoveryManager();
|
|
514
540
|
for (const tool of config.tools) {
|
|
515
541
|
this.toolHandlers.set(tool.name, tool.handler);
|
|
516
542
|
}
|
|
543
|
+
this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
|
|
544
|
+
if (this.launchedBySanqian) {
|
|
545
|
+
this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
this.connect().catch((err) => {
|
|
548
|
+
console.error("[SDK] Failed to connect after Sanqian launch:", err);
|
|
549
|
+
});
|
|
550
|
+
}, 0);
|
|
551
|
+
}
|
|
517
552
|
}
|
|
518
553
|
// ============================================
|
|
519
554
|
// Lifecycle
|
|
@@ -540,7 +575,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
540
575
|
this.connectionInfo = info;
|
|
541
576
|
const url = this.discovery.buildWebSocketUrl(info);
|
|
542
577
|
return new Promise((resolve, reject) => {
|
|
543
|
-
|
|
578
|
+
this.log(`Connecting to ${url}`);
|
|
544
579
|
this.ws = new import_ws.default(url);
|
|
545
580
|
const connectTimeout = setTimeout(() => {
|
|
546
581
|
reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
|
|
@@ -548,7 +583,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
548
583
|
}, 1e4);
|
|
549
584
|
this.ws.on("open", async () => {
|
|
550
585
|
clearTimeout(connectTimeout);
|
|
551
|
-
|
|
586
|
+
this.log("WebSocket connected");
|
|
552
587
|
this.state.connected = true;
|
|
553
588
|
this.state.reconnectAttempts = 0;
|
|
554
589
|
this.emit("connected");
|
|
@@ -560,7 +595,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
560
595
|
}
|
|
561
596
|
});
|
|
562
597
|
this.ws.on("close", (code, reason) => {
|
|
563
|
-
|
|
598
|
+
this.log(`WebSocket closed: ${code} ${reason.toString()}`);
|
|
564
599
|
this.handleDisconnect(reason.toString());
|
|
565
600
|
});
|
|
566
601
|
this.ws.on("error", (error) => {
|
|
@@ -573,7 +608,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
573
608
|
const message = JSON.parse(data.toString());
|
|
574
609
|
this.handleMessage(message);
|
|
575
610
|
} catch (e) {
|
|
576
|
-
|
|
611
|
+
this.warn("Failed to parse message:", e);
|
|
577
612
|
}
|
|
578
613
|
});
|
|
579
614
|
});
|
|
@@ -630,7 +665,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
630
665
|
this.state.registered = true;
|
|
631
666
|
this.startHeartbeat();
|
|
632
667
|
this.emit("registered");
|
|
633
|
-
|
|
668
|
+
this.log(`Registered as '${this.config.appName}'`);
|
|
634
669
|
} catch (e) {
|
|
635
670
|
this.state.registering = false;
|
|
636
671
|
throw e;
|
|
@@ -659,7 +694,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
659
694
|
this.handleChatStream(message);
|
|
660
695
|
break;
|
|
661
696
|
default:
|
|
662
|
-
|
|
697
|
+
this.warn(`Unknown message type: ${type}`);
|
|
663
698
|
}
|
|
664
699
|
}
|
|
665
700
|
handleChatStream(message) {
|
|
@@ -667,7 +702,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
667
702
|
if (!id) return;
|
|
668
703
|
const handler = this.streamHandlers.get(id);
|
|
669
704
|
if (!handler) {
|
|
670
|
-
|
|
705
|
+
this.warn(`No stream handler for message ${id}`);
|
|
671
706
|
return;
|
|
672
707
|
}
|
|
673
708
|
switch (event) {
|
|
@@ -706,11 +741,11 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
706
741
|
}
|
|
707
742
|
}
|
|
708
743
|
async handleToolCall(message) {
|
|
709
|
-
|
|
744
|
+
this.log(`handleToolCall received:`, JSON.stringify(message));
|
|
710
745
|
const { id, call_id, name, arguments: args } = message;
|
|
711
746
|
const msgId = id || call_id;
|
|
712
747
|
const toolName = name.includes(":") ? name.split(":")[1] : name;
|
|
713
|
-
|
|
748
|
+
this.log(`Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
|
|
714
749
|
const handler = this.toolHandlers.get(toolName);
|
|
715
750
|
this.emit("tool_call", { name: toolName, arguments: args });
|
|
716
751
|
if (!handler) {
|
|
@@ -718,19 +753,19 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
718
753
|
return;
|
|
719
754
|
}
|
|
720
755
|
try {
|
|
721
|
-
|
|
756
|
+
this.log(`Executing tool '${toolName}' with args:`, args);
|
|
722
757
|
const result = await Promise.race([
|
|
723
758
|
handler(args),
|
|
724
759
|
this.createTimeout(this.config.toolExecutionTimeout)
|
|
725
760
|
]);
|
|
726
|
-
|
|
761
|
+
this.log(`Tool '${toolName}' completed successfully`);
|
|
727
762
|
await this.sendToolResult(msgId, call_id, true, result);
|
|
728
763
|
} catch (e) {
|
|
729
764
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
730
765
|
const errorStack = e instanceof Error ? e.stack : void 0;
|
|
731
|
-
|
|
766
|
+
this.log(`Tool '${toolName}' failed:`, errorMessage);
|
|
732
767
|
if (errorStack) {
|
|
733
|
-
|
|
768
|
+
this.log(`Tool error stack:`, errorStack);
|
|
734
769
|
}
|
|
735
770
|
await this.sendToolResult(msgId, call_id, false, void 0, errorMessage);
|
|
736
771
|
}
|
|
@@ -744,7 +779,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
744
779
|
result,
|
|
745
780
|
error
|
|
746
781
|
};
|
|
747
|
-
|
|
782
|
+
this.log(`Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
|
|
748
783
|
this.send(message);
|
|
749
784
|
}
|
|
750
785
|
// ============================================
|
|
@@ -760,19 +795,48 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
760
795
|
}
|
|
761
796
|
this.pendingRequests.clear();
|
|
762
797
|
if (reason === "Client disconnect") {
|
|
763
|
-
|
|
798
|
+
this.log("Client disconnected intentionally, skipping auto-reconnect");
|
|
764
799
|
return;
|
|
765
800
|
}
|
|
766
801
|
if (reason === "Replaced by new connection") {
|
|
767
|
-
|
|
802
|
+
this.log("Connection replaced by newer one, skipping auto-reconnect");
|
|
768
803
|
return;
|
|
769
804
|
}
|
|
770
805
|
if (this.connectingPromise) {
|
|
771
|
-
|
|
806
|
+
this.log("Connection attempt already in progress, skipping auto-reconnect");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (!this.shouldAutoReconnect()) {
|
|
810
|
+
this.log("No components require reconnection (refCount=0), skipping auto-reconnect");
|
|
772
811
|
return;
|
|
773
812
|
}
|
|
774
813
|
this.scheduleReconnect();
|
|
775
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Check if auto-reconnect should be enabled
|
|
817
|
+
* Returns true if any component has requested persistent connection
|
|
818
|
+
*/
|
|
819
|
+
shouldAutoReconnect() {
|
|
820
|
+
return this.reconnectRefCount > 0;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Request persistent connection (enables auto-reconnect)
|
|
824
|
+
* Call this when a component needs the connection to stay alive (e.g., chat panel opens)
|
|
825
|
+
* Must be paired with releaseReconnect() when the component no longer needs it
|
|
826
|
+
*/
|
|
827
|
+
acquireReconnect() {
|
|
828
|
+
this.reconnectRefCount++;
|
|
829
|
+
this.log(`acquireReconnect: refCount=${this.reconnectRefCount}`);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Release persistent connection request
|
|
833
|
+
* Call this when a component no longer needs the connection (e.g., chat panel closes)
|
|
834
|
+
* When refCount reaches 0, auto-reconnect is disabled
|
|
835
|
+
*/
|
|
836
|
+
releaseReconnect() {
|
|
837
|
+
this.reconnectRefCount = Math.max(0, this.reconnectRefCount - 1);
|
|
838
|
+
this.log(`releaseReconnect: refCount=${this.reconnectRefCount}`);
|
|
839
|
+
}
|
|
776
840
|
scheduleReconnect() {
|
|
777
841
|
if (this.reconnectTimer) return;
|
|
778
842
|
const baseDelay = Math.min(
|
|
@@ -782,7 +846,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
782
846
|
);
|
|
783
847
|
const jitter = Math.random() * 500;
|
|
784
848
|
const delay = baseDelay + jitter;
|
|
785
|
-
|
|
849
|
+
this.log(`Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
|
|
786
850
|
this.reconnectTimer = setTimeout(async () => {
|
|
787
851
|
this.reconnectTimer = null;
|
|
788
852
|
this.state.reconnectAttempts++;
|
|
@@ -812,9 +876,9 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
812
876
|
this.heartbeatTimer = setInterval(() => {
|
|
813
877
|
if (this.heartbeatAckPending) {
|
|
814
878
|
this.missedHeartbeats++;
|
|
815
|
-
|
|
879
|
+
this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
|
|
816
880
|
if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
|
|
817
|
-
|
|
881
|
+
this.warn("Too many missed heartbeat acks, connection may be dead");
|
|
818
882
|
this.ws?.close(4e3, "Heartbeat timeout");
|
|
819
883
|
return;
|
|
820
884
|
}
|
|
@@ -850,7 +914,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
850
914
|
throw error;
|
|
851
915
|
}
|
|
852
916
|
const data = JSON.stringify(message);
|
|
853
|
-
|
|
917
|
+
this.log(`WebSocket send:`, data.substring(0, 200));
|
|
854
918
|
this.ws.send(data);
|
|
855
919
|
}
|
|
856
920
|
sendAndWait(message, id, timeout) {
|
|
@@ -911,7 +975,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
911
975
|
return;
|
|
912
976
|
}
|
|
913
977
|
if (this.connectingPromise) {
|
|
914
|
-
|
|
978
|
+
this.log("Connection already in progress, waiting...");
|
|
915
979
|
return this.connectingPromise;
|
|
916
980
|
}
|
|
917
981
|
this.connectingPromise = this.doFullConnect();
|
|
@@ -929,16 +993,16 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
929
993
|
* so there's no need for additional launch-in-progress tracking within a single instance.
|
|
930
994
|
*/
|
|
931
995
|
async doFullConnect() {
|
|
932
|
-
|
|
996
|
+
this.log("Starting full connection flow...");
|
|
933
997
|
let info = this.discovery.read();
|
|
934
998
|
if (!info) {
|
|
935
999
|
if (this.config.autoLaunchSanqian) {
|
|
936
|
-
|
|
1000
|
+
this.log("Sanqian not running, attempting to launch...");
|
|
937
1001
|
const launched = this.discovery.launchSanqian(this.config.sanqianPath);
|
|
938
1002
|
if (!launched) {
|
|
939
1003
|
throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
|
|
940
1004
|
}
|
|
941
|
-
|
|
1005
|
+
this.log("Sanqian launch initiated, waiting for startup...");
|
|
942
1006
|
info = await this.waitForSanqianStartup();
|
|
943
1007
|
} else {
|
|
944
1008
|
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
@@ -955,7 +1019,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
955
1019
|
while (Date.now() - startTime < timeout) {
|
|
956
1020
|
const info = this.discovery.read();
|
|
957
1021
|
if (info) {
|
|
958
|
-
|
|
1022
|
+
this.log("Sanqian started, connection info available");
|
|
959
1023
|
return info;
|
|
960
1024
|
}
|
|
961
1025
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|