@yushaw/sanqian-sdk 0.2.9 → 0.2.11
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.browser.d.mts +430 -0
- package/dist/index.browser.mjs +1058 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +358 -295
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -4
package/dist/index.mjs
CHANGED
|
@@ -1,272 +1,293 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
// src/discovery.ts
|
|
12
|
+
var discovery_exports = {};
|
|
13
|
+
__export(discovery_exports, {
|
|
14
|
+
DiscoveryManager: () => DiscoveryManager
|
|
15
|
+
});
|
|
5
16
|
import { existsSync, readFileSync, watch } from "fs";
|
|
6
17
|
import { homedir, platform } from "os";
|
|
7
18
|
import { join } from "path";
|
|
8
19
|
import { spawn, execSync } from "child_process";
|
|
9
|
-
var DiscoveryManager
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* Returns null if:
|
|
29
|
-
* - File doesn't exist
|
|
30
|
-
* - File is invalid JSON
|
|
31
|
-
* - Process is not running
|
|
32
|
-
*/
|
|
33
|
-
read() {
|
|
34
|
-
const filePath = this.getConnectionFilePath();
|
|
35
|
-
if (!existsSync(filePath)) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
const content = readFileSync(filePath, "utf-8");
|
|
40
|
-
const info = JSON.parse(content);
|
|
41
|
-
if (!info.port || !info.token || !info.pid) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
if (info.executable) {
|
|
45
|
-
this.cachedExecutable = info.executable;
|
|
46
|
-
}
|
|
47
|
-
if (!this.isProcessRunning(info.pid)) {
|
|
48
|
-
return null;
|
|
20
|
+
var DiscoveryManager;
|
|
21
|
+
var init_discovery = __esm({
|
|
22
|
+
"src/discovery.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
DiscoveryManager = class {
|
|
25
|
+
connectionInfo = null;
|
|
26
|
+
watcher = null;
|
|
27
|
+
onChange = null;
|
|
28
|
+
pollInterval = null;
|
|
29
|
+
/**
|
|
30
|
+
* Cached executable path from last successful connection.json read.
|
|
31
|
+
* Persists even when Sanqian is not running, for use in auto-launch.
|
|
32
|
+
*/
|
|
33
|
+
cachedExecutable = null;
|
|
34
|
+
/**
|
|
35
|
+
* Get the path to connection.json
|
|
36
|
+
*/
|
|
37
|
+
getConnectionFilePath() {
|
|
38
|
+
return join(homedir(), ".sanqian", "runtime", "connection.json");
|
|
49
39
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Read and validate connection info
|
|
42
|
+
*
|
|
43
|
+
* Returns null if:
|
|
44
|
+
* - File doesn't exist
|
|
45
|
+
* - File is invalid JSON
|
|
46
|
+
* - Process is not running
|
|
47
|
+
*/
|
|
48
|
+
read() {
|
|
49
|
+
const filePath = this.getConnectionFilePath();
|
|
50
|
+
if (!existsSync(filePath)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(filePath, "utf-8");
|
|
55
|
+
const info = JSON.parse(content);
|
|
56
|
+
if (!info.port || !info.token || !info.pid) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (info.executable) {
|
|
60
|
+
this.cachedExecutable = info.executable;
|
|
68
61
|
}
|
|
69
|
-
this.
|
|
62
|
+
if (!this.isProcessRunning(info.pid)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
this.connectionInfo = info;
|
|
66
|
+
return info;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
70
69
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
this.onChange?.(newInfo);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Start watching for connection file changes
|
|
73
|
+
*/
|
|
74
|
+
startWatching(onChange) {
|
|
75
|
+
this.onChange = onChange;
|
|
76
|
+
const dir = join(homedir(), ".sanqian", "runtime");
|
|
77
|
+
if (!existsSync(dir)) {
|
|
78
|
+
this.pollInterval = setInterval(() => {
|
|
79
|
+
if (existsSync(dir)) {
|
|
80
|
+
if (this.pollInterval) {
|
|
81
|
+
clearInterval(this.pollInterval);
|
|
82
|
+
this.pollInterval = null;
|
|
83
|
+
}
|
|
84
|
+
this.setupWatcher(dir);
|
|
87
85
|
}
|
|
88
|
-
},
|
|
86
|
+
}, 2e3);
|
|
87
|
+
return;
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Stop watching
|
|
97
|
-
*/
|
|
98
|
-
stopWatching() {
|
|
99
|
-
if (this.pollInterval) {
|
|
100
|
-
clearInterval(this.pollInterval);
|
|
101
|
-
this.pollInterval = null;
|
|
102
|
-
}
|
|
103
|
-
this.watcher?.close();
|
|
104
|
-
this.watcher = null;
|
|
105
|
-
this.onChange = null;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Check if a process is running by PID
|
|
109
|
-
*
|
|
110
|
-
* Cross-platform implementation:
|
|
111
|
-
* - macOS/Linux: process.kill(pid, 0) works correctly
|
|
112
|
-
* - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
|
|
113
|
-
* so we use tasklist command for reliable checking
|
|
114
|
-
*/
|
|
115
|
-
isProcessRunning(pid) {
|
|
116
|
-
try {
|
|
117
|
-
if (platform() === "win32") {
|
|
89
|
+
this.setupWatcher(dir);
|
|
90
|
+
}
|
|
91
|
+
setupWatcher(dir) {
|
|
118
92
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
93
|
+
this.watcher = watch(dir, (event, filename) => {
|
|
94
|
+
if (filename === "connection.json") {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
const newInfo = this.read();
|
|
97
|
+
const oldInfoStr = JSON.stringify(this.connectionInfo);
|
|
98
|
+
const newInfoStr = JSON.stringify(newInfo);
|
|
99
|
+
if (oldInfoStr !== newInfoStr) {
|
|
100
|
+
this.connectionInfo = newInfo;
|
|
101
|
+
this.onChange?.(newInfo);
|
|
102
|
+
}
|
|
103
|
+
}, 100);
|
|
104
|
+
}
|
|
122
105
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error("Failed to watch connection file:", e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Stop watching
|
|
112
|
+
*/
|
|
113
|
+
stopWatching() {
|
|
114
|
+
if (this.pollInterval) {
|
|
115
|
+
clearInterval(this.pollInterval);
|
|
116
|
+
this.pollInterval = null;
|
|
117
|
+
}
|
|
118
|
+
this.watcher?.close();
|
|
119
|
+
this.watcher = null;
|
|
120
|
+
this.onChange = null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if a process is running by PID
|
|
124
|
+
*
|
|
125
|
+
* Cross-platform implementation:
|
|
126
|
+
* - macOS/Linux: process.kill(pid, 0) works correctly
|
|
127
|
+
* - Windows: process.kill(pid, 0) can give false positives due to PID reuse,
|
|
128
|
+
* so we use tasklist command for reliable checking
|
|
129
|
+
*/
|
|
130
|
+
isProcessRunning(pid) {
|
|
131
|
+
try {
|
|
132
|
+
if (platform() === "win32") {
|
|
133
|
+
try {
|
|
134
|
+
const result = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
137
|
+
});
|
|
138
|
+
const trimmed = result.trim();
|
|
139
|
+
if (!trimmed || trimmed.toUpperCase().startsWith("INFO")) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return new RegExp(`\\b${pid}\\b`).test(trimmed);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
126
149
|
}
|
|
127
|
-
return new RegExp(`\\b${pid}\\b`).test(trimmed);
|
|
128
150
|
} catch {
|
|
129
151
|
return false;
|
|
130
152
|
}
|
|
131
|
-
} else {
|
|
132
|
-
process.kill(pid, 0);
|
|
133
|
-
return true;
|
|
134
153
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* Get cached connection info (may be stale)
|
|
141
|
-
*/
|
|
142
|
-
getCached() {
|
|
143
|
-
return this.connectionInfo;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Build WebSocket URL from connection info
|
|
147
|
-
*/
|
|
148
|
-
buildWebSocketUrl(info) {
|
|
149
|
-
return `ws://127.0.0.1:${info.port}${info.ws_path}?token=${info.token}`;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Build HTTP base URL from connection info
|
|
153
|
-
*/
|
|
154
|
-
buildHttpUrl(info) {
|
|
155
|
-
return `http://127.0.0.1:${info.port}`;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Find Sanqian executable path
|
|
159
|
-
* Searches in standard installation locations for each platform
|
|
160
|
-
*/
|
|
161
|
-
findSanqianPath(customPath) {
|
|
162
|
-
if (customPath) {
|
|
163
|
-
if (existsSync(customPath)) {
|
|
164
|
-
return customPath;
|
|
154
|
+
/**
|
|
155
|
+
* Get cached connection info (may be stale)
|
|
156
|
+
*/
|
|
157
|
+
getCached() {
|
|
158
|
+
return this.connectionInfo;
|
|
165
159
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
searchPaths.push(
|
|
173
|
-
// Production: installed app
|
|
174
|
-
"/Applications/Sanqian.app/Contents/MacOS/Sanqian",
|
|
175
|
-
join(homedir(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
|
|
176
|
-
// Development: electron-builder output
|
|
177
|
-
join(homedir(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
|
|
178
|
-
join(homedir(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
|
|
179
|
-
);
|
|
180
|
-
} else if (os === "win32") {
|
|
181
|
-
const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
182
|
-
const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
183
|
-
const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
184
|
-
searchPaths.push(
|
|
185
|
-
// Production: NSIS installer uses lowercase directory name from package.json "name"
|
|
186
|
-
join(localAppData, "Programs", "sanqian", "Sanqian.exe"),
|
|
187
|
-
// Legacy/alternative paths with uppercase
|
|
188
|
-
join(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
|
|
189
|
-
join(programFiles, "Sanqian", "Sanqian.exe"),
|
|
190
|
-
join(programFilesX86, "Sanqian", "Sanqian.exe"),
|
|
191
|
-
// Development: electron-builder output
|
|
192
|
-
join(homedir(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
|
|
193
|
-
);
|
|
194
|
-
} else {
|
|
195
|
-
searchPaths.push(
|
|
196
|
-
"/usr/bin/sanqian",
|
|
197
|
-
"/usr/local/bin/sanqian",
|
|
198
|
-
join(homedir(), ".local/bin/sanqian"),
|
|
199
|
-
"/opt/Sanqian/sanqian",
|
|
200
|
-
// Development
|
|
201
|
-
join(homedir(), "dev/sanqian/dist/linux-unpacked/sanqian")
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
for (const path of searchPaths) {
|
|
205
|
-
if (existsSync(path)) {
|
|
206
|
-
return path;
|
|
160
|
+
/**
|
|
161
|
+
* Build WebSocket URL from connection info
|
|
162
|
+
*/
|
|
163
|
+
buildWebSocketUrl(info) {
|
|
164
|
+
const wsPath = info.ws_path || "/ws/apps";
|
|
165
|
+
return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
|
|
207
166
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Build HTTP base URL from connection info
|
|
169
|
+
*/
|
|
170
|
+
buildHttpUrl(info) {
|
|
171
|
+
return `http://127.0.0.1:${info.port}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Find Sanqian executable path
|
|
175
|
+
* Searches in standard installation locations for each platform
|
|
176
|
+
*/
|
|
177
|
+
findSanqianPath(customPath) {
|
|
178
|
+
if (customPath) {
|
|
179
|
+
if (existsSync(customPath)) {
|
|
180
|
+
return customPath;
|
|
181
|
+
}
|
|
182
|
+
console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const os = platform();
|
|
186
|
+
const searchPaths = [];
|
|
187
|
+
if (os === "darwin") {
|
|
188
|
+
searchPaths.push(
|
|
189
|
+
// Production: installed app
|
|
190
|
+
"/Applications/Sanqian.app/Contents/MacOS/Sanqian",
|
|
191
|
+
join(homedir(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
|
|
192
|
+
// Development: electron-builder output
|
|
193
|
+
join(homedir(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
|
|
194
|
+
join(homedir(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
|
|
195
|
+
);
|
|
196
|
+
} else if (os === "win32") {
|
|
197
|
+
const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
198
|
+
const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
199
|
+
const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
200
|
+
searchPaths.push(
|
|
201
|
+
// Production: NSIS installer uses lowercase directory name from package.json "name"
|
|
202
|
+
join(localAppData, "Programs", "sanqian", "Sanqian.exe"),
|
|
203
|
+
// Legacy/alternative paths with uppercase
|
|
204
|
+
join(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
|
|
205
|
+
join(programFiles, "Sanqian", "Sanqian.exe"),
|
|
206
|
+
join(programFilesX86, "Sanqian", "Sanqian.exe"),
|
|
207
|
+
// Development: electron-builder output
|
|
208
|
+
join(homedir(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
searchPaths.push(
|
|
212
|
+
"/usr/bin/sanqian",
|
|
213
|
+
"/usr/local/bin/sanqian",
|
|
214
|
+
join(homedir(), ".local/bin/sanqian"),
|
|
215
|
+
"/opt/Sanqian/sanqian",
|
|
216
|
+
// Development
|
|
217
|
+
join(homedir(), "dev/sanqian/dist/linux-unpacked/sanqian")
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
for (const path of searchPaths) {
|
|
221
|
+
if (existsSync(path)) {
|
|
222
|
+
return path;
|
|
254
223
|
}
|
|
255
|
-
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
256
226
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Launch Sanqian in hidden/tray mode
|
|
229
|
+
* Returns true if launch was initiated successfully
|
|
230
|
+
*
|
|
231
|
+
* Priority for finding Sanqian executable:
|
|
232
|
+
* 1. customPath parameter (if provided)
|
|
233
|
+
* 2. Cached executable from connection.json (most reliable)
|
|
234
|
+
* 3. Search in standard installation locations (fallback)
|
|
235
|
+
*/
|
|
236
|
+
launchSanqian(customPath) {
|
|
237
|
+
let sanqianPath = null;
|
|
238
|
+
if (customPath) {
|
|
239
|
+
sanqianPath = this.findSanqianPath(customPath);
|
|
240
|
+
} else if (this.cachedExecutable && existsSync(this.cachedExecutable)) {
|
|
241
|
+
sanqianPath = this.cachedExecutable;
|
|
242
|
+
console.log(`[SDK] Using cached executable: ${sanqianPath}`);
|
|
243
|
+
} else {
|
|
244
|
+
sanqianPath = this.findSanqianPath();
|
|
245
|
+
}
|
|
246
|
+
if (!sanqianPath) {
|
|
247
|
+
console.error("[SDK] Sanqian executable not found");
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
|
|
251
|
+
try {
|
|
252
|
+
const os = platform();
|
|
253
|
+
if (os === "darwin") {
|
|
254
|
+
const appPath = sanqianPath.replace(
|
|
255
|
+
"/Contents/MacOS/Sanqian",
|
|
256
|
+
""
|
|
257
|
+
);
|
|
258
|
+
spawn("open", ["-g", "-a", appPath, "--args", "--hidden"], {
|
|
259
|
+
detached: true,
|
|
260
|
+
stdio: "ignore"
|
|
261
|
+
}).unref();
|
|
262
|
+
} else {
|
|
263
|
+
spawn(sanqianPath, ["--hidden"], {
|
|
264
|
+
detached: true,
|
|
265
|
+
stdio: "ignore",
|
|
266
|
+
// On Windows, hide the console window
|
|
267
|
+
...os === "win32" && {
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
shell: false
|
|
270
|
+
}
|
|
271
|
+
}).unref();
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error("[SDK] Failed to launch Sanqian:", e);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Check if Sanqian is running
|
|
281
|
+
*/
|
|
282
|
+
isSanqianRunning() {
|
|
283
|
+
return this.read() !== null;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
268
286
|
}
|
|
269
|
-
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// src/client.ts
|
|
290
|
+
import WebSocket from "isomorphic-ws";
|
|
270
291
|
|
|
271
292
|
// src/errors.ts
|
|
272
293
|
var SANQIAN_WEBSITE = "https://sanqian.io";
|
|
@@ -437,7 +458,8 @@ function createSDKError(code, details) {
|
|
|
437
458
|
// src/client.ts
|
|
438
459
|
var SanqianSDK = class _SanqianSDK {
|
|
439
460
|
config;
|
|
440
|
-
|
|
461
|
+
// DiscoveryManager is only initialized in Node.js environments (when connectionInfo is not provided)
|
|
462
|
+
discovery = null;
|
|
441
463
|
ws = null;
|
|
442
464
|
connectionInfo = null;
|
|
443
465
|
state = {
|
|
@@ -493,11 +515,10 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
493
515
|
autoLaunchSanqian: true,
|
|
494
516
|
...config
|
|
495
517
|
};
|
|
496
|
-
this.discovery = new DiscoveryManager();
|
|
497
518
|
for (const tool of config.tools) {
|
|
498
519
|
this.toolHandlers.set(tool.name, tool.handler);
|
|
499
520
|
}
|
|
500
|
-
this.launchedBySanqian = typeof process !== "undefined" && process.env?.SANQIAN_NO_RECONNECT === "1";
|
|
521
|
+
this.launchedBySanqian = typeof process !== "undefined" && (process.env?.SANQIAN_LAUNCHED === "1" || process.env?.SANQIAN_NO_RECONNECT === "1");
|
|
501
522
|
if (this.launchedBySanqian) {
|
|
502
523
|
this.log("Detected launch by Sanqian, will connect immediately without auto-reconnect");
|
|
503
524
|
setTimeout(() => {
|
|
@@ -507,6 +528,26 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
507
528
|
}, 0);
|
|
508
529
|
}
|
|
509
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Build WebSocket URL from connection info
|
|
533
|
+
* This is inlined to avoid importing DiscoveryManager in browser environments
|
|
534
|
+
*/
|
|
535
|
+
buildWebSocketUrl(info) {
|
|
536
|
+
const wsPath = info.ws_path || "/ws/apps";
|
|
537
|
+
return `ws://127.0.0.1:${info.port}${wsPath}?token=${info.token}`;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Lazily initialize DiscoveryManager (only in Node.js environments)
|
|
541
|
+
* This uses dynamic import to avoid bundling Node.js APIs in browser builds
|
|
542
|
+
*/
|
|
543
|
+
async getDiscovery() {
|
|
544
|
+
if (this.discovery) {
|
|
545
|
+
return this.discovery;
|
|
546
|
+
}
|
|
547
|
+
const { DiscoveryManager: DiscoveryManager2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
548
|
+
this.discovery = new DiscoveryManager2();
|
|
549
|
+
return this.discovery;
|
|
550
|
+
}
|
|
510
551
|
// ============================================
|
|
511
552
|
// Lifecycle
|
|
512
553
|
// ============================================
|
|
@@ -530,7 +571,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
530
571
|
*/
|
|
531
572
|
async connectWithInfo(info) {
|
|
532
573
|
this.connectionInfo = info;
|
|
533
|
-
const url = this.
|
|
574
|
+
const url = this.buildWebSocketUrl(info);
|
|
534
575
|
return new Promise((resolve, reject) => {
|
|
535
576
|
this.log(`Connecting to ${url}`);
|
|
536
577
|
this.ws = new WebSocket(url);
|
|
@@ -538,7 +579,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
538
579
|
reject(createSDKError("CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */));
|
|
539
580
|
this.ws?.close();
|
|
540
581
|
}, 1e4);
|
|
541
|
-
this.ws.
|
|
582
|
+
this.ws.onopen = async () => {
|
|
542
583
|
clearTimeout(connectTimeout);
|
|
543
584
|
this.log("WebSocket connected");
|
|
544
585
|
this.state.connected = true;
|
|
@@ -550,24 +591,28 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
550
591
|
} catch (e) {
|
|
551
592
|
reject(e);
|
|
552
593
|
}
|
|
553
|
-
}
|
|
554
|
-
this.ws.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
594
|
+
};
|
|
595
|
+
this.ws.onclose = (event) => {
|
|
596
|
+
const reasonRaw = event.reason;
|
|
597
|
+
const reason = typeof reasonRaw === "string" ? reasonRaw : reasonRaw?.toString() || "";
|
|
598
|
+
this.log(`WebSocket closed: ${event.code} ${reason}`);
|
|
599
|
+
this.handleDisconnect(reason);
|
|
600
|
+
};
|
|
601
|
+
this.ws.onerror = (event) => {
|
|
602
|
+
const error = event.error || new Error("WebSocket error");
|
|
559
603
|
console.error("[SDK] WebSocket error:", error);
|
|
560
604
|
this.state.lastError = error;
|
|
561
605
|
this.emit("error", error);
|
|
562
|
-
}
|
|
563
|
-
this.ws.
|
|
606
|
+
};
|
|
607
|
+
this.ws.onmessage = (event) => {
|
|
564
608
|
try {
|
|
565
|
-
const
|
|
609
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
610
|
+
const message = JSON.parse(data);
|
|
566
611
|
this.handleMessage(message);
|
|
567
612
|
} catch (e) {
|
|
568
613
|
this.warn("Failed to parse message:", e);
|
|
569
614
|
}
|
|
570
|
-
}
|
|
615
|
+
};
|
|
571
616
|
});
|
|
572
617
|
}
|
|
573
618
|
/**
|
|
@@ -576,7 +621,7 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
576
621
|
async disconnect() {
|
|
577
622
|
this.stopHeartbeat();
|
|
578
623
|
this.stopReconnect();
|
|
579
|
-
this.discovery
|
|
624
|
+
this.discovery?.stopWatching();
|
|
580
625
|
if (this.ws) {
|
|
581
626
|
this.ws.close(1e3, "Client disconnect");
|
|
582
627
|
this.ws = null;
|
|
@@ -952,18 +997,25 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
952
997
|
*/
|
|
953
998
|
async doFullConnect() {
|
|
954
999
|
this.log("Starting full connection flow...");
|
|
955
|
-
let info =
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1000
|
+
let info = null;
|
|
1001
|
+
if (this.config.connectionInfo) {
|
|
1002
|
+
this.log("Using pre-configured connection info (browser mode)");
|
|
1003
|
+
info = this.config.connectionInfo;
|
|
1004
|
+
} else {
|
|
1005
|
+
const discovery = await this.getDiscovery();
|
|
1006
|
+
info = discovery.read();
|
|
1007
|
+
if (!info) {
|
|
1008
|
+
if (this.config.autoLaunchSanqian) {
|
|
1009
|
+
this.log("Sanqian not running, attempting to launch...");
|
|
1010
|
+
const launched = discovery.launchSanqian(this.config.sanqianPath);
|
|
1011
|
+
if (!launched) {
|
|
1012
|
+
throw createSDKError("NOT_INSTALLED" /* NOT_INSTALLED */);
|
|
1013
|
+
}
|
|
1014
|
+
this.log("Sanqian launch initiated, waiting for startup...");
|
|
1015
|
+
info = await this.waitForSanqianStartup();
|
|
1016
|
+
} else {
|
|
1017
|
+
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
962
1018
|
}
|
|
963
|
-
this.log("Sanqian launch initiated, waiting for startup...");
|
|
964
|
-
info = await this.waitForSanqianStartup();
|
|
965
|
-
} else {
|
|
966
|
-
throw createSDKError("NOT_RUNNING" /* NOT_RUNNING */);
|
|
967
1019
|
}
|
|
968
1020
|
}
|
|
969
1021
|
await this.connectWithInfo(info);
|
|
@@ -974,8 +1026,9 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
974
1026
|
async waitForSanqianStartup(timeout = 12e4) {
|
|
975
1027
|
const startTime = Date.now();
|
|
976
1028
|
const pollInterval = 500;
|
|
1029
|
+
const discovery = await this.getDiscovery();
|
|
977
1030
|
while (Date.now() - startTime < timeout) {
|
|
978
|
-
const info =
|
|
1031
|
+
const info = discovery.read();
|
|
979
1032
|
if (info) {
|
|
980
1033
|
this.log("Sanqian started, connection info available");
|
|
981
1034
|
return info;
|
|
@@ -1324,6 +1377,20 @@ var SanqianSDK = class _SanqianSDK {
|
|
|
1324
1377
|
});
|
|
1325
1378
|
return this.on(event, onceWrapper);
|
|
1326
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Remove all event listeners
|
|
1382
|
+
*
|
|
1383
|
+
* Call this to prevent memory leaks when disposing the SDK instance.
|
|
1384
|
+
* If event is specified, only removes listeners for that event.
|
|
1385
|
+
*/
|
|
1386
|
+
removeAllListeners(event) {
|
|
1387
|
+
if (event) {
|
|
1388
|
+
this.eventListeners.delete(event);
|
|
1389
|
+
} else {
|
|
1390
|
+
this.eventListeners.clear();
|
|
1391
|
+
}
|
|
1392
|
+
return this;
|
|
1393
|
+
}
|
|
1327
1394
|
emit(event, ...args) {
|
|
1328
1395
|
const listeners = this.eventListeners.get(event);
|
|
1329
1396
|
if (listeners) {
|
|
@@ -1424,6 +1491,9 @@ var Conversation = class {
|
|
|
1424
1491
|
});
|
|
1425
1492
|
}
|
|
1426
1493
|
};
|
|
1494
|
+
|
|
1495
|
+
// src/index.ts
|
|
1496
|
+
init_discovery();
|
|
1427
1497
|
export {
|
|
1428
1498
|
Conversation,
|
|
1429
1499
|
DiscoveryManager,
|