@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.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
|
// ============================================
|
|
@@ -716,16 +750,49 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
716
750
|
pending.reject(createSDKError("DISCONNECTED" /* DISCONNECTED */));
|
|
717
751
|
}
|
|
718
752
|
this.pendingRequests.clear();
|
|
753
|
+
if (reason === "Client disconnect") {
|
|
754
|
+
this.log("Client disconnected intentionally, skipping auto-reconnect");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
719
757
|
if (reason === "Replaced by new connection") {
|
|
720
|
-
|
|
758
|
+
this.log("Connection replaced by newer one, skipping auto-reconnect");
|
|
721
759
|
return;
|
|
722
760
|
}
|
|
723
761
|
if (this.connectingPromise) {
|
|
724
|
-
|
|
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");
|
|
725
767
|
return;
|
|
726
768
|
}
|
|
727
769
|
this.scheduleReconnect();
|
|
728
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
|
+
}
|
|
729
796
|
scheduleReconnect() {
|
|
730
797
|
if (this.reconnectTimer) return;
|
|
731
798
|
const baseDelay = Math.min(
|
|
@@ -735,7 +802,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
735
802
|
);
|
|
736
803
|
const jitter = Math.random() * 500;
|
|
737
804
|
const delay = baseDelay + jitter;
|
|
738
|
-
|
|
805
|
+
this.log(`Scheduling reconnect in ${Math.round(delay)}ms (attempt ${this.state.reconnectAttempts + 1})`);
|
|
739
806
|
this.reconnectTimer = setTimeout(async () => {
|
|
740
807
|
this.reconnectTimer = null;
|
|
741
808
|
this.state.reconnectAttempts++;
|
|
@@ -765,9 +832,9 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
765
832
|
this.heartbeatTimer = setInterval(() => {
|
|
766
833
|
if (this.heartbeatAckPending) {
|
|
767
834
|
this.missedHeartbeats++;
|
|
768
|
-
|
|
835
|
+
this.warn(`Missed heartbeat ack (${this.missedHeartbeats}/${_SanqianSDK.MAX_MISSED_HEARTBEATS})`);
|
|
769
836
|
if (this.missedHeartbeats >= _SanqianSDK.MAX_MISSED_HEARTBEATS) {
|
|
770
|
-
|
|
837
|
+
this.warn("Too many missed heartbeat acks, connection may be dead");
|
|
771
838
|
this.ws?.close(4e3, "Heartbeat timeout");
|
|
772
839
|
return;
|
|
773
840
|
}
|
|
@@ -803,7 +870,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
803
870
|
throw error;
|
|
804
871
|
}
|
|
805
872
|
const data = JSON.stringify(message);
|
|
806
|
-
|
|
873
|
+
this.log(`WebSocket send:`, data.substring(0, 200));
|
|
807
874
|
this.ws.send(data);
|
|
808
875
|
}
|
|
809
876
|
sendAndWait(message, id, timeout) {
|
|
@@ -864,7 +931,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
864
931
|
return;
|
|
865
932
|
}
|
|
866
933
|
if (this.connectingPromise) {
|
|
867
|
-
|
|
934
|
+
this.log("Connection already in progress, waiting...");
|
|
868
935
|
return this.connectingPromise;
|
|
869
936
|
}
|
|
870
937
|
this.connectingPromise = this.doFullConnect();
|
|
@@ -882,16 +949,16 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
882
949
|
* so there's no need for additional launch-in-progress tracking within a single instance.
|
|
883
950
|
*/
|
|
884
951
|
async doFullConnect() {
|
|
885
|
-
|
|
952
|
+
this.log("Starting full connection flow...");
|
|
886
953
|
let info = this.discovery.read();
|
|
887
954
|
if (!info) {
|
|
888
955
|
if (this.config.autoLaunchSanqian) {
|
|
889
|
-
|
|
956
|
+
this.log("Sanqian not running, attempting to launch...");
|
|
890
957
|
const launched = this.discovery.launchSanqian(this.config.sanqianPath);
|
|
891
958
|
if (!launched) {
|
|
892
959
|
throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
|
|
893
960
|
}
|
|
894
|
-
|
|
961
|
+
this.log("Sanqian launch initiated, waiting for startup...");
|
|
895
962
|
info = await this.waitForSanqianStartup();
|
|
896
963
|
} else {
|
|
897
964
|
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
@@ -908,7 +975,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
908
975
|
while (Date.now() - startTime < timeout) {
|
|
909
976
|
const info = this.discovery.read();
|
|
910
977
|
if (info) {
|
|
911
|
-
|
|
978
|
+
this.log("Sanqian started, connection info available");
|
|
912
979
|
return info;
|
|
913
980
|
}
|
|
914
981
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|