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