@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.mjs
CHANGED
|
@@ -458,19 +458,54 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
458
458
|
static MAX_MISSED_HEARTBEATS = 2;
|
|
459
459
|
// Connection promise for deduplication (prevents multiple concurrent connect attempts)
|
|
460
460
|
connectingPromise = null;
|
|
461
|
+
// Reconnect reference count - when > 0, auto-reconnect is enabled
|
|
462
|
+
// This allows multiple components to request persistent connections
|
|
463
|
+
reconnectRefCount = 0;
|
|
464
|
+
// Whether this app was launched by Sanqian (detected via environment variable)
|
|
465
|
+
launchedBySanqian = false;
|
|
461
466
|
// Event listeners
|
|
462
467
|
eventListeners = /* @__PURE__ */ new Map();
|
|
468
|
+
// ============================================
|
|
469
|
+
// Debug Logging
|
|
470
|
+
// ============================================
|
|
471
|
+
/**
|
|
472
|
+
* Log debug message (only when config.debug is true)
|
|
473
|
+
*/
|
|
474
|
+
log(...args) {
|
|
475
|
+
if (this.config.debug) {
|
|
476
|
+
console.log("[SDK]", ...args);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Log warning message (only when config.debug is true)
|
|
481
|
+
*/
|
|
482
|
+
warn(...args) {
|
|
483
|
+
if (this.config.debug) {
|
|
484
|
+
console.warn("[SDK]", ...args);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
463
487
|
constructor(config) {
|
|
464
488
|
this.config = {
|
|
465
489
|
reconnectInterval: 5e3,
|
|
466
490
|
heartbeatInterval: 3e4,
|
|
467
491
|
toolExecutionTimeout: 3e4,
|
|
492
|
+
debug: false,
|
|
493
|
+
autoLaunchSanqian: true,
|
|
468
494
|
...config
|
|
469
495
|
};
|
|
470
496
|
this.discovery = new DiscoveryManager();
|
|
471
497
|
for (const tool of config.tools) {
|
|
472
498
|
this.toolHandlers.set(tool.name, tool.handler);
|
|
473
499
|
}
|
|
500
|
+
this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
|
|
501
|
+
if (this.launchedBySanqian) {
|
|
502
|
+
this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
|
|
503
|
+
setTimeout(() => {
|
|
504
|
+
this.connect().catch((err) => {
|
|
505
|
+
console.error("[SDK] Failed to connect after Sanqian launch:", err);
|
|
506
|
+
});
|
|
507
|
+
}, 0);
|
|
508
|
+
}
|
|
474
509
|
}
|
|
475
510
|
// ============================================
|
|
476
511
|
// Lifecycle
|
|
@@ -497,7 +532,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
497
532
|
this.connectionInfo = info;
|
|
498
533
|
const url = this.discovery.buildWebSocketUrl(info);
|
|
499
534
|
return new Promise((resolve, reject) => {
|
|
500
|
-
|
|
535
|
+
this.log(`Connecting to ${url}`);
|
|
501
536
|
this.ws = new WebSocket(url);
|
|
502
537
|
const connectTimeout = setTimeout(() => {
|
|
503
538
|
reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
|
|
@@ -505,7 +540,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
505
540
|
}, 1e4);
|
|
506
541
|
this.ws.on("open", async () => {
|
|
507
542
|
clearTimeout(connectTimeout);
|
|
508
|
-
|
|
543
|
+
this.log("WebSocket connected");
|
|
509
544
|
this.state.connected = true;
|
|
510
545
|
this.state.reconnectAttempts = 0;
|
|
511
546
|
this.emit("connected");
|
|
@@ -517,7 +552,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
517
552
|
}
|
|
518
553
|
});
|
|
519
554
|
this.ws.on("close", (code, reason) => {
|
|
520
|
-
|
|
555
|
+
this.log(`WebSocket closed: ${code} ${reason.toString()}`);
|
|
521
556
|
this.handleDisconnect(reason.toString());
|
|
522
557
|
});
|
|
523
558
|
this.ws.on("error", (error) => {
|
|
@@ -530,7 +565,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
530
565
|
const message = JSON.parse(data.toString());
|
|
531
566
|
this.handleMessage(message);
|
|
532
567
|
} catch (e) {
|
|
533
|
-
|
|
568
|
+
this.warn("Failed to parse message:", e);
|
|
534
569
|
}
|
|
535
570
|
});
|
|
536
571
|
});
|
|
@@ -587,7 +622,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
587
622
|
this.state.registered = true;
|
|
588
623
|
this.startHeartbeat();
|
|
589
624
|
this.emit("registered");
|
|
590
|
-
|
|
625
|
+
this.log(`Registered as '${this.config.appName}'`);
|
|
591
626
|
} catch (e) {
|
|
592
627
|
this.state.registering = false;
|
|
593
628
|
throw e;
|
|
@@ -616,7 +651,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
616
651
|
this.handleChatStream(message);
|
|
617
652
|
break;
|
|
618
653
|
default:
|
|
619
|
-
|
|
654
|
+
this.warn(`Unknown message type: ${type}`);
|
|
620
655
|
}
|
|
621
656
|
}
|
|
622
657
|
handleChatStream(message) {
|
|
@@ -624,7 +659,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
624
659
|
if (!id) return;
|
|
625
660
|
const handler = this.streamHandlers.get(id);
|
|
626
661
|
if (!handler) {
|
|
627
|
-
|
|
662
|
+
this.warn(`No stream handler for message ${id}`);
|
|
628
663
|
return;
|
|
629
664
|
}
|
|
630
665
|
switch (event) {
|
|
@@ -663,11 +698,11 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
663
698
|
}
|
|
664
699
|
}
|
|
665
700
|
async handleToolCall(message) {
|
|
666
|
-
|
|
701
|
+
this.log(`handleToolCall received:`, JSON.stringify(message));
|
|
667
702
|
const { id, call_id, name, arguments: args } = message;
|
|
668
703
|
const msgId = id || call_id;
|
|
669
704
|
const toolName = name.includes(":") ? name.split(":")[1] : name;
|
|
670
|
-
|
|
705
|
+
this.log(`Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
|
|
671
706
|
const handler = this.toolHandlers.get(toolName);
|
|
672
707
|
this.emit("tool_call", { name: toolName, arguments: args });
|
|
673
708
|
if (!handler) {
|
|
@@ -675,19 +710,19 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
675
710
|
return;
|
|
676
711
|
}
|
|
677
712
|
try {
|
|
678
|
-
|
|
713
|
+
this.log(`Executing tool '${toolName}' with args:`, args);
|
|
679
714
|
const result = await Promise.race([
|
|
680
715
|
handler(args),
|
|
681
716
|
this.createTimeout(this.config.toolExecutionTimeout)
|
|
682
717
|
]);
|
|
683
|
-
|
|
718
|
+
this.log(`Tool '${toolName}' completed successfully`);
|
|
684
719
|
await this.sendToolResult(msgId, call_id, true, result);
|
|
685
720
|
} catch (e) {
|
|
686
721
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
687
722
|
const errorStack = e instanceof Error ? e.stack : void 0;
|
|
688
|
-
|
|
723
|
+
this.log(`Tool '${toolName}' failed:`, errorMessage);
|
|
689
724
|
if (errorStack) {
|
|
690
|
-
|
|
725
|
+
this.log(`Tool error stack:`, errorStack);
|
|
691
726
|
}
|
|
692
727
|
await this.sendToolResult(msgId, call_id, false, void 0, errorMessage);
|
|
693
728
|
}
|
|
@@ -701,7 +736,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
701
736
|
result,
|
|
702
737
|
error
|
|
703
738
|
};
|
|
704
|
-
|
|
739
|
+
this.log(`Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
|
|
705
740
|
this.send(message);
|
|
706
741
|
}
|
|
707
742
|
// ============================================
|
|
@@ -717,19 +752,48 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
717
752
|
}
|
|
718
753
|
this.pendingRequests.clear();
|
|
719
754
|
if (reason === "Client disconnect") {
|
|
720
|
-
|
|
755
|
+
this.log("Client disconnected intentionally, skipping auto-reconnect");
|
|
721
756
|
return;
|
|
722
757
|
}
|
|
723
758
|
if (reason === "Replaced by new connection") {
|
|
724
|
-
|
|
759
|
+
this.log("Connection replaced by newer one, skipping auto-reconnect");
|
|
725
760
|
return;
|
|
726
761
|
}
|
|
727
762
|
if (this.connectingPromise) {
|
|
728
|
-
|
|
763
|
+
this.log("Connection attempt already in progress, skipping auto-reconnect");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!this.shouldAutoReconnect()) {
|
|
767
|
+
this.log("No components require reconnection (refCount=0), skipping auto-reconnect");
|
|
729
768
|
return;
|
|
730
769
|
}
|
|
731
770
|
this.scheduleReconnect();
|
|
732
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Check if auto-reconnect should be enabled
|
|
774
|
+
* Returns true if any component has requested persistent connection
|
|
775
|
+
*/
|
|
776
|
+
shouldAutoReconnect() {
|
|
777
|
+
return this.reconnectRefCount > 0;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Request persistent connection (enables auto-reconnect)
|
|
781
|
+
* Call this when a component needs the connection to stay alive (e.g., chat panel opens)
|
|
782
|
+
* Must be paired with releaseReconnect() when the component no longer needs it
|
|
783
|
+
*/
|
|
784
|
+
acquireReconnect() {
|
|
785
|
+
this.reconnectRefCount++;
|
|
786
|
+
this.log(`acquireReconnect: refCount=${this.reconnectRefCount}`);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Release persistent connection request
|
|
790
|
+
* Call this when a component no longer needs the connection (e.g., chat panel closes)
|
|
791
|
+
* When refCount reaches 0, auto-reconnect is disabled
|
|
792
|
+
*/
|
|
793
|
+
releaseReconnect() {
|
|
794
|
+
this.reconnectRefCount = Math.max(0, this.reconnectRefCount - 1);
|
|
795
|
+
this.log(`releaseReconnect: refCount=${this.reconnectRefCount}`);
|
|
796
|
+
}
|
|
733
797
|
scheduleReconnect() {
|
|
734
798
|
if (this.reconnectTimer) return;
|
|
735
799
|
const baseDelay = Math.min(
|
|
@@ -739,7 +803,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
739
803
|
);
|
|
740
804
|
const jitter = Math.random() * 500;
|
|
741
805
|
const delay = baseDelay + jitter;
|
|
742
|
-
|
|
806
|
+
this.log(`Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
|
|
743
807
|
this.reconnectTimer = setTimeout(async () => {
|
|
744
808
|
this.reconnectTimer = null;
|
|
745
809
|
this.state.reconnectAttempts++;
|
|
@@ -769,9 +833,9 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
769
833
|
this.heartbeatTimer = setInterval(() => {
|
|
770
834
|
if (this.heartbeatAckPending) {
|
|
771
835
|
this.missedHeartbeats++;
|
|
772
|
-
|
|
836
|
+
this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
|
|
773
837
|
if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
|
|
774
|
-
|
|
838
|
+
this.warn("Too many missed heartbeat acks, connection may be dead");
|
|
775
839
|
this.ws?.close(4e3, "Heartbeat timeout");
|
|
776
840
|
return;
|
|
777
841
|
}
|
|
@@ -807,7 +871,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
807
871
|
throw error;
|
|
808
872
|
}
|
|
809
873
|
const data = JSON.stringify(message);
|
|
810
|
-
|
|
874
|
+
this.log(`WebSocket send:`, data.substring(0, 200));
|
|
811
875
|
this.ws.send(data);
|
|
812
876
|
}
|
|
813
877
|
sendAndWait(message, id, timeout) {
|
|
@@ -868,7 +932,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
868
932
|
return;
|
|
869
933
|
}
|
|
870
934
|
if (this.connectingPromise) {
|
|
871
|
-
|
|
935
|
+
this.log("Connection already in progress, waiting...");
|
|
872
936
|
return this.connectingPromise;
|
|
873
937
|
}
|
|
874
938
|
this.connectingPromise = this.doFullConnect();
|
|
@@ -886,16 +950,16 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
886
950
|
* so there's no need for additional launch-in-progress tracking within a single instance.
|
|
887
951
|
*/
|
|
888
952
|
async doFullConnect() {
|
|
889
|
-
|
|
953
|
+
this.log("Starting full connection flow...");
|
|
890
954
|
let info = this.discovery.read();
|
|
891
955
|
if (!info) {
|
|
892
956
|
if (this.config.autoLaunchSanqian) {
|
|
893
|
-
|
|
957
|
+
this.log("Sanqian not running, attempting to launch...");
|
|
894
958
|
const launched = this.discovery.launchSanqian(this.config.sanqianPath);
|
|
895
959
|
if (!launched) {
|
|
896
960
|
throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
|
|
897
961
|
}
|
|
898
|
-
|
|
962
|
+
this.log("Sanqian launch initiated, waiting for startup...");
|
|
899
963
|
info = await this.waitForSanqianStartup();
|
|
900
964
|
} else {
|
|
901
965
|
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
@@ -912,7 +976,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
912
976
|
while (Date.now() - startTime < timeout) {
|
|
913
977
|
const info = this.discovery.read();
|
|
914
978
|
if (info) {
|
|
915
|
-
|
|
979
|
+
this.log("Sanqian started, connection info available");
|
|
916
980
|
return info;
|
|
917
981
|
}
|
|
918
982
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|