@yushaw/sanqian-sdk 0.2.5 → 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 +92 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -25
- 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
|
// ============================================
|
|
@@ -759,16 +793,49 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
759
793
|
pending.reject(createSDKError("DISCONNECTED" /* DISCONNECTED */));
|
|
760
794
|
}
|
|
761
795
|
this.pendingRequests.clear();
|
|
796
|
+
if (reason === "Client disconnect") {
|
|
797
|
+
this.log("Client disconnected intentionally, skipping auto-reconnect");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
762
800
|
if (reason === "Replaced by new connection") {
|
|
763
|
-
|
|
801
|
+
this.log("Connection replaced by newer one, skipping auto-reconnect");
|
|
764
802
|
return;
|
|
765
803
|
}
|
|
766
804
|
if (this.connectingPromise) {
|
|
767
|
-
|
|
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");
|
|
768
810
|
return;
|
|
769
811
|
}
|
|
770
812
|
this.scheduleReconnect();
|
|
771
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
|
+
}
|
|
772
839
|
scheduleReconnect() {
|
|
773
840
|
if (this.reconnectTimer) return;
|
|
774
841
|
const baseDelay = Math.min(
|
|
@@ -778,7 +845,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
778
845
|
);
|
|
779
846
|
const jitter = Math.random() * 500;
|
|
780
847
|
const delay = baseDelay + jitter;
|
|
781
|
-
|
|
848
|
+
this.log(`Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
|
|
782
849
|
this.reconnectTimer = setTimeout(async () => {
|
|
783
850
|
this.reconnectTimer = null;
|
|
784
851
|
this.state.reconnectAttempts++;
|
|
@@ -808,9 +875,9 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
808
875
|
this.heartbeatTimer = setInterval(() => {
|
|
809
876
|
if (this.heartbeatAckPending) {
|
|
810
877
|
this.missedHeartbeats++;
|
|
811
|
-
|
|
878
|
+
this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
|
|
812
879
|
if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
|
|
813
|
-
|
|
880
|
+
this.warn("Too many missed heartbeat acks, connection may be dead");
|
|
814
881
|
this.ws?.close(4e3, "Heartbeat timeout");
|
|
815
882
|
return;
|
|
816
883
|
}
|
|
@@ -846,7 +913,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
846
913
|
throw error;
|
|
847
914
|
}
|
|
848
915
|
const data = JSON.stringify(message);
|
|
849
|
-
|
|
916
|
+
this.log(`WebSocket send:`, data.substring(0, 200));
|
|
850
917
|
this.ws.send(data);
|
|
851
918
|
}
|
|
852
919
|
sendAndWait(message, id, timeout) {
|
|
@@ -907,7 +974,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
907
974
|
return;
|
|
908
975
|
}
|
|
909
976
|
if (this.connectingPromise) {
|
|
910
|
-
|
|
977
|
+
this.log("Connection already in progress, waiting...");
|
|
911
978
|
return this.connectingPromise;
|
|
912
979
|
}
|
|
913
980
|
this.connectingPromise = this.doFullConnect();
|
|
@@ -925,16 +992,16 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
925
992
|
* so there's no need for additional launch-in-progress tracking within a single instance.
|
|
926
993
|
*/
|
|
927
994
|
async doFullConnect() {
|
|
928
|
-
|
|
995
|
+
this.log("Starting full connection flow...");
|
|
929
996
|
let info = this.discovery.read();
|
|
930
997
|
if (!info) {
|
|
931
998
|
if (this.config.autoLaunchSanqian) {
|
|
932
|
-
|
|
999
|
+
this.log("Sanqian not running, attempting to launch...");
|
|
933
1000
|
const launched = this.discovery.launchSanqian(this.config.sanqianPath);
|
|
934
1001
|
if (!launched) {
|
|
935
1002
|
throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
|
|
936
1003
|
}
|
|
937
|
-
|
|
1004
|
+
this.log("Sanqian launch initiated, waiting for startup...");
|
|
938
1005
|
info = await this.waitForSanqianStartup();
|
|
939
1006
|
} else {
|
|
940
1007
|
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
@@ -951,7 +1018,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
951
1018
|
while (Date.now() - startTime < timeout) {
|
|
952
1019
|
const info = this.discovery.read();
|
|
953
1020
|
if (info) {
|
|
954
|
-
|
|
1021
|
+
this.log("Sanqian started, connection info available");
|
|
955
1022
|
return info;
|
|
956
1023
|
}
|
|
957
1024
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|