@hangox/mg-cli 1.0.0 → 1.0.2
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/cli.js +1580 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon-runner.js +794 -0
- package/dist/daemon-runner.js.map +1 -0
- package/dist/index-DNrszrq9.d.ts +568 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +950 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +689 -0
- package/dist/server.js.map +1 -0
- package/package.json +5 -1
- package/.eslintrc.cjs +0 -26
- package/CLAUDE.md +0 -43
- package/src/cli/client.ts +0 -266
- package/src/cli/commands/execute-code.ts +0 -59
- package/src/cli/commands/export-image.ts +0 -193
- package/src/cli/commands/get-all-nodes.ts +0 -81
- package/src/cli/commands/get-all-pages.ts +0 -118
- package/src/cli/commands/get-node-by-id.ts +0 -83
- package/src/cli/commands/get-node-by-link.ts +0 -105
- package/src/cli/commands/server.ts +0 -130
- package/src/cli/index.ts +0 -33
- package/src/index.ts +0 -9
- package/src/server/connection-manager.ts +0 -211
- package/src/server/daemon-runner.ts +0 -22
- package/src/server/daemon.ts +0 -211
- package/src/server/index.ts +0 -8
- package/src/server/logger.ts +0 -117
- package/src/server/request-handler.ts +0 -192
- package/src/server/websocket-server.ts +0 -297
- package/src/shared/constants.ts +0 -90
- package/src/shared/errors.ts +0 -131
- package/src/shared/index.ts +0 -8
- package/src/shared/types.ts +0 -227
- package/src/shared/utils.ts +0 -352
- package/tests/unit/shared/constants.test.ts +0 -66
- package/tests/unit/shared/errors.test.ts +0 -82
- package/tests/unit/shared/utils.test.ts +0 -208
- package/tsconfig.json +0 -22
- package/tsup.config.ts +0 -33
- package/vitest.config.ts +0 -22
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server/daemon.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/shared/utils.ts
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
10
|
+
import { dirname, resolve, isAbsolute } from "path";
|
|
11
|
+
|
|
12
|
+
// src/shared/constants.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var DEFAULT_PORT = 9527;
|
|
16
|
+
var PORT_RANGE_START = 9527;
|
|
17
|
+
var PORT_RANGE_END = 9536;
|
|
18
|
+
var CONFIG_DIR = join(homedir(), ".mg-plugin");
|
|
19
|
+
var SERVER_INFO_FILE = join(CONFIG_DIR, "server.json");
|
|
20
|
+
var LOG_DIR = join(CONFIG_DIR, "logs");
|
|
21
|
+
var SERVER_LOG_FILE = join(LOG_DIR, "server.log");
|
|
22
|
+
var HEARTBEAT_INTERVAL = 3e4;
|
|
23
|
+
var HEARTBEAT_TIMEOUT = 9e4;
|
|
24
|
+
var REQUEST_TIMEOUT = 3e4;
|
|
25
|
+
|
|
26
|
+
// src/shared/utils.ts
|
|
27
|
+
function ensureDir(dir) {
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function ensureConfigDir() {
|
|
33
|
+
ensureDir(CONFIG_DIR);
|
|
34
|
+
ensureDir(LOG_DIR);
|
|
35
|
+
}
|
|
36
|
+
function readServerInfo() {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(SERVER_INFO_FILE)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const content = readFileSync(SERVER_INFO_FILE, "utf-8");
|
|
42
|
+
return JSON.parse(content);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeServerInfo(info) {
|
|
48
|
+
ensureConfigDir();
|
|
49
|
+
writeFileSync(SERVER_INFO_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
function deleteServerInfo() {
|
|
52
|
+
try {
|
|
53
|
+
if (existsSync(SERVER_INFO_FILE)) {
|
|
54
|
+
unlinkSync(SERVER_INFO_FILE);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isProcessRunning(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function generateId() {
|
|
68
|
+
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
69
|
+
}
|
|
70
|
+
function getCurrentISOTime() {
|
|
71
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
}
|
|
73
|
+
function formatLogTime(date = /* @__PURE__ */ new Date()) {
|
|
74
|
+
const year = date.getFullYear();
|
|
75
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
76
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
77
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
78
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
79
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
80
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/shared/errors.ts
|
|
84
|
+
var ErrorNames = {
|
|
85
|
+
["E001" /* CONNECTION_FAILED */]: "CONNECTION_FAILED",
|
|
86
|
+
["E002" /* CONNECTION_TIMEOUT */]: "CONNECTION_TIMEOUT",
|
|
87
|
+
["E003" /* NO_PAGE_CONNECTED */]: "NO_PAGE_CONNECTED",
|
|
88
|
+
["E004" /* PAGE_NOT_FOUND */]: "PAGE_NOT_FOUND",
|
|
89
|
+
["E005" /* NODE_NOT_FOUND */]: "NODE_NOT_FOUND",
|
|
90
|
+
["E006" /* NO_SELECTION */]: "NO_SELECTION",
|
|
91
|
+
["E007" /* MG_UNAVAILABLE */]: "MG_UNAVAILABLE",
|
|
92
|
+
["E008" /* EXPORT_FAILED */]: "EXPORT_FAILED",
|
|
93
|
+
["E009" /* FILE_WRITE_FAILED */]: "FILE_WRITE_FAILED",
|
|
94
|
+
["E010" /* INVALID_LINK */]: "INVALID_LINK",
|
|
95
|
+
["E011" /* INVALID_PARAMS */]: "INVALID_PARAMS",
|
|
96
|
+
["E012" /* REQUEST_TIMEOUT */]: "REQUEST_TIMEOUT",
|
|
97
|
+
["E013" /* PORT_EXHAUSTED */]: "PORT_EXHAUSTED",
|
|
98
|
+
["E014" /* SERVER_DISCOVERY_FAILED */]: "SERVER_DISCOVERY_FAILED",
|
|
99
|
+
["E015" /* SERVER_START_FAILED */]: "SERVER_START_FAILED",
|
|
100
|
+
["E016" /* SERVER_ALREADY_RUNNING */]: "SERVER_ALREADY_RUNNING",
|
|
101
|
+
["E017" /* CONNECTION_LOST */]: "CONNECTION_LOST",
|
|
102
|
+
["E099" /* UNKNOWN_ERROR */]: "UNKNOWN_ERROR"
|
|
103
|
+
};
|
|
104
|
+
var ErrorMessages = {
|
|
105
|
+
["E001" /* CONNECTION_FAILED */]: "\u65E0\u6CD5\u8FDE\u63A5\u5230 MG Server",
|
|
106
|
+
["E002" /* CONNECTION_TIMEOUT */]: "\u8FDE\u63A5\u8D85\u65F6",
|
|
107
|
+
["E003" /* NO_PAGE_CONNECTED */]: "\u6CA1\u6709 MasterGo \u9875\u9762\u8FDE\u63A5\u5230 Server",
|
|
108
|
+
["E004" /* PAGE_NOT_FOUND */]: "\u672A\u627E\u5230\u5339\u914D\u7684\u9875\u9762",
|
|
109
|
+
["E005" /* NODE_NOT_FOUND */]: "\u8282\u70B9\u4E0D\u5B58\u5728",
|
|
110
|
+
["E006" /* NO_SELECTION */]: "\u6CA1\u6709\u9009\u4E2D\u4EFB\u4F55\u8282\u70B9",
|
|
111
|
+
["E007" /* MG_UNAVAILABLE */]: "mg \u5BF9\u8C61\u4E0D\u53EF\u7528",
|
|
112
|
+
["E008" /* EXPORT_FAILED */]: "\u5BFC\u51FA\u56FE\u7247\u5931\u8D25",
|
|
113
|
+
["E009" /* FILE_WRITE_FAILED */]: "\u6587\u4EF6\u5199\u5165\u5931\u8D25",
|
|
114
|
+
["E010" /* INVALID_LINK */]: "\u65E0\u6548\u7684 mgp:// \u94FE\u63A5\u683C\u5F0F",
|
|
115
|
+
["E011" /* INVALID_PARAMS */]: "\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
116
|
+
["E012" /* REQUEST_TIMEOUT */]: "\u8BF7\u6C42\u8D85\u65F6",
|
|
117
|
+
["E013" /* PORT_EXHAUSTED */]: "\u6240\u6709\u5907\u9009\u7AEF\u53E3\u5747\u88AB\u5360\u7528",
|
|
118
|
+
["E014" /* SERVER_DISCOVERY_FAILED */]: "\u65E0\u6CD5\u53D1\u73B0 Server (\u7AEF\u53E3\u626B\u63CF\u5931\u8D25)",
|
|
119
|
+
["E015" /* SERVER_START_FAILED */]: "\u81EA\u52A8\u542F\u52A8 Server \u5931\u8D25",
|
|
120
|
+
["E016" /* SERVER_ALREADY_RUNNING */]: "Server \u5DF2\u5728\u8FD0\u884C\u4E2D",
|
|
121
|
+
["E017" /* CONNECTION_LOST */]: "\u8FDE\u63A5\u65AD\u5F00",
|
|
122
|
+
["E099" /* UNKNOWN_ERROR */]: "\u672A\u77E5\u9519\u8BEF"
|
|
123
|
+
};
|
|
124
|
+
var MGError = class extends Error {
|
|
125
|
+
/** 错误码 */
|
|
126
|
+
code;
|
|
127
|
+
/** 错误名称 */
|
|
128
|
+
errorName;
|
|
129
|
+
/** 额外详情 */
|
|
130
|
+
details;
|
|
131
|
+
constructor(code, message, details) {
|
|
132
|
+
super(message || ErrorMessages[code]);
|
|
133
|
+
this.name = "MGError";
|
|
134
|
+
this.code = code;
|
|
135
|
+
this.errorName = ErrorNames[code];
|
|
136
|
+
this.details = details;
|
|
137
|
+
}
|
|
138
|
+
/** 转换为 JSON 格式 */
|
|
139
|
+
toJSON() {
|
|
140
|
+
return {
|
|
141
|
+
code: this.code,
|
|
142
|
+
name: this.errorName,
|
|
143
|
+
message: this.message,
|
|
144
|
+
details: this.details
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** 格式化输出 */
|
|
148
|
+
toString() {
|
|
149
|
+
return `\u9519\u8BEF [${this.code}]: ${this.message}`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/server/websocket-server.ts
|
|
154
|
+
import { WebSocketServer } from "ws";
|
|
155
|
+
|
|
156
|
+
// src/server/logger.ts
|
|
157
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
158
|
+
import { dirname as dirname2 } from "path";
|
|
159
|
+
var levelPriority = {
|
|
160
|
+
["DEBUG" /* DEBUG */]: 0,
|
|
161
|
+
["INFO" /* INFO */]: 1,
|
|
162
|
+
["WARN" /* WARN */]: 2,
|
|
163
|
+
["ERROR" /* ERROR */]: 3
|
|
164
|
+
};
|
|
165
|
+
var Logger = class {
|
|
166
|
+
options;
|
|
167
|
+
constructor(options = {}) {
|
|
168
|
+
this.options = {
|
|
169
|
+
console: options.console ?? true,
|
|
170
|
+
file: options.file ?? false,
|
|
171
|
+
filePath: options.filePath ?? SERVER_LOG_FILE,
|
|
172
|
+
minLevel: options.minLevel ?? "INFO" /* INFO */
|
|
173
|
+
};
|
|
174
|
+
if (this.options.file) {
|
|
175
|
+
const dir = dirname2(this.options.filePath);
|
|
176
|
+
if (!existsSync2(dir)) {
|
|
177
|
+
mkdirSync2(dir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 记录日志
|
|
183
|
+
*/
|
|
184
|
+
log(level, message, ...args2) {
|
|
185
|
+
if (levelPriority[level] < levelPriority[this.options.minLevel]) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const timestamp = formatLogTime();
|
|
189
|
+
const formattedMessage = `[${timestamp}] [${level}] ${message}`;
|
|
190
|
+
if (this.options.console) {
|
|
191
|
+
const consoleMethod = level === "ERROR" /* ERROR */ ? console.error : console.log;
|
|
192
|
+
if (args2.length > 0) {
|
|
193
|
+
consoleMethod(formattedMessage, ...args2);
|
|
194
|
+
} else {
|
|
195
|
+
consoleMethod(formattedMessage);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this.options.file) {
|
|
199
|
+
try {
|
|
200
|
+
const fileMessage = args2.length > 0 ? `${formattedMessage} ${JSON.stringify(args2)}
|
|
201
|
+
` : `${formattedMessage}
|
|
202
|
+
`;
|
|
203
|
+
appendFileSync(this.options.filePath, fileMessage);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (this.options.console) {
|
|
206
|
+
console.error("\u65E5\u5FD7\u6587\u4EF6\u5199\u5165\u5931\u8D25:", error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
debug(message, ...args2) {
|
|
212
|
+
this.log("DEBUG" /* DEBUG */, message, ...args2);
|
|
213
|
+
}
|
|
214
|
+
info(message, ...args2) {
|
|
215
|
+
this.log("INFO" /* INFO */, message, ...args2);
|
|
216
|
+
}
|
|
217
|
+
warn(message, ...args2) {
|
|
218
|
+
this.log("WARN" /* WARN */, message, ...args2);
|
|
219
|
+
}
|
|
220
|
+
error(message, ...args2) {
|
|
221
|
+
this.log("ERROR" /* ERROR */, message, ...args2);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
function createLogger(options) {
|
|
225
|
+
return new Logger(options);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/server/connection-manager.ts
|
|
229
|
+
var ConnectionManager = class {
|
|
230
|
+
logger;
|
|
231
|
+
/** Provider 连接(按页面 URL 索引) */
|
|
232
|
+
providers = /* @__PURE__ */ new Map();
|
|
233
|
+
/** Consumer 连接 */
|
|
234
|
+
consumers = /* @__PURE__ */ new Map();
|
|
235
|
+
/** 所有连接(按 ID 索引) */
|
|
236
|
+
allConnections = /* @__PURE__ */ new Map();
|
|
237
|
+
/** 心跳检查定时器 */
|
|
238
|
+
heartbeatTimer = null;
|
|
239
|
+
constructor(logger) {
|
|
240
|
+
this.logger = logger;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 启动心跳检查
|
|
244
|
+
*/
|
|
245
|
+
startHeartbeatCheck(interval = 3e4) {
|
|
246
|
+
if (this.heartbeatTimer) {
|
|
247
|
+
clearInterval(this.heartbeatTimer);
|
|
248
|
+
}
|
|
249
|
+
this.heartbeatTimer = setInterval(() => {
|
|
250
|
+
this.checkHeartbeats();
|
|
251
|
+
}, interval);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 停止心跳检查
|
|
255
|
+
*/
|
|
256
|
+
stopHeartbeatCheck() {
|
|
257
|
+
if (this.heartbeatTimer) {
|
|
258
|
+
clearInterval(this.heartbeatTimer);
|
|
259
|
+
this.heartbeatTimer = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 检查所有连接的心跳
|
|
264
|
+
*/
|
|
265
|
+
checkHeartbeats() {
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
for (const [id, ws] of this.allConnections) {
|
|
268
|
+
const lastActive = ws.connectionInfo.lastActiveAt.getTime();
|
|
269
|
+
const elapsed = now - lastActive;
|
|
270
|
+
if (elapsed > HEARTBEAT_TIMEOUT) {
|
|
271
|
+
this.logger.warn(`\u8FDE\u63A5 ${id} \u5FC3\u8DF3\u8D85\u65F6\uFF0C\u5173\u95ED\u8FDE\u63A5`);
|
|
272
|
+
this.removeConnection(ws);
|
|
273
|
+
ws.terminate();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 添加连接
|
|
279
|
+
*/
|
|
280
|
+
addConnection(ws, type, pageUrl, pageId) {
|
|
281
|
+
const connectionId = generateId();
|
|
282
|
+
const now = /* @__PURE__ */ new Date();
|
|
283
|
+
const connectionInfo = {
|
|
284
|
+
id: connectionId,
|
|
285
|
+
type,
|
|
286
|
+
pageUrl,
|
|
287
|
+
pageId,
|
|
288
|
+
connectedAt: now,
|
|
289
|
+
lastActiveAt: now
|
|
290
|
+
};
|
|
291
|
+
const managedWs = ws;
|
|
292
|
+
managedWs.connectionId = connectionId;
|
|
293
|
+
managedWs.connectionInfo = connectionInfo;
|
|
294
|
+
managedWs.isAlive = true;
|
|
295
|
+
this.allConnections.set(connectionId, managedWs);
|
|
296
|
+
if (type === "provider" /* PROVIDER */ && pageUrl) {
|
|
297
|
+
const existing = this.providers.get(pageUrl);
|
|
298
|
+
if (existing) {
|
|
299
|
+
this.logger.info(`\u9875\u9762 ${pageUrl} \u5DF2\u6709\u8FDE\u63A5\uFF0C\u66FF\u6362\u4E3A\u65B0\u8FDE\u63A5`);
|
|
300
|
+
this.removeConnection(existing);
|
|
301
|
+
existing.close();
|
|
302
|
+
}
|
|
303
|
+
this.providers.set(pageUrl, managedWs);
|
|
304
|
+
this.logger.info(`Provider \u8FDE\u63A5: ${pageUrl}`);
|
|
305
|
+
} else if (type === "consumer" /* CONSUMER */) {
|
|
306
|
+
this.consumers.set(connectionId, managedWs);
|
|
307
|
+
this.logger.info(`Consumer \u8FDE\u63A5: ${connectionId}`);
|
|
308
|
+
}
|
|
309
|
+
return managedWs;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 移除连接
|
|
313
|
+
*/
|
|
314
|
+
removeConnection(ws) {
|
|
315
|
+
const { connectionId, connectionInfo } = ws;
|
|
316
|
+
this.allConnections.delete(connectionId);
|
|
317
|
+
if (connectionInfo.type === "provider" /* PROVIDER */ && connectionInfo.pageUrl) {
|
|
318
|
+
this.providers.delete(connectionInfo.pageUrl);
|
|
319
|
+
this.logger.info(`Provider \u65AD\u5F00: ${connectionInfo.pageUrl}`);
|
|
320
|
+
} else if (connectionInfo.type === "consumer" /* CONSUMER */) {
|
|
321
|
+
this.consumers.delete(connectionId);
|
|
322
|
+
this.logger.info(`Consumer \u65AD\u5F00: ${connectionId}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 更新连接活跃时间
|
|
327
|
+
*/
|
|
328
|
+
updateLastActive(ws) {
|
|
329
|
+
ws.connectionInfo.lastActiveAt = /* @__PURE__ */ new Date();
|
|
330
|
+
ws.isAlive = true;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 根据页面 URL 查找 Provider
|
|
334
|
+
*/
|
|
335
|
+
findProviderByPageUrl(pageUrl) {
|
|
336
|
+
return this.providers.get(pageUrl);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 获取第一个可用的 Provider
|
|
340
|
+
*/
|
|
341
|
+
getFirstProvider() {
|
|
342
|
+
const iterator = this.providers.values();
|
|
343
|
+
const first = iterator.next();
|
|
344
|
+
return first.value;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 获取所有 Provider 信息
|
|
348
|
+
*/
|
|
349
|
+
getAllProviders() {
|
|
350
|
+
return Array.from(this.providers.values()).map((ws) => ws.connectionInfo);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 获取连接统计
|
|
354
|
+
*/
|
|
355
|
+
getStats() {
|
|
356
|
+
return {
|
|
357
|
+
providers: this.providers.size,
|
|
358
|
+
consumers: this.consumers.size,
|
|
359
|
+
total: this.allConnections.size
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 获取所有已连接的页面 URL
|
|
364
|
+
*/
|
|
365
|
+
getConnectedPageUrls() {
|
|
366
|
+
return Array.from(this.providers.keys());
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 关闭所有连接
|
|
370
|
+
*/
|
|
371
|
+
closeAll() {
|
|
372
|
+
this.stopHeartbeatCheck();
|
|
373
|
+
for (const ws of this.allConnections.values()) {
|
|
374
|
+
ws.close();
|
|
375
|
+
}
|
|
376
|
+
this.providers.clear();
|
|
377
|
+
this.consumers.clear();
|
|
378
|
+
this.allConnections.clear();
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/server/request-handler.ts
|
|
383
|
+
var RequestHandler = class {
|
|
384
|
+
logger;
|
|
385
|
+
connectionManager;
|
|
386
|
+
/** 待处理的请求 */
|
|
387
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
388
|
+
constructor(connectionManager, logger) {
|
|
389
|
+
this.connectionManager = connectionManager;
|
|
390
|
+
this.logger = logger;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 处理 Consumer 请求
|
|
394
|
+
*/
|
|
395
|
+
async handleRequest(consumer, message) {
|
|
396
|
+
const requestId = message.id || generateId();
|
|
397
|
+
const { type, pageUrl, params } = message;
|
|
398
|
+
this.logger.info(`\u6536\u5230\u8BF7\u6C42: ${type} (${requestId})`, { pageUrl });
|
|
399
|
+
let provider;
|
|
400
|
+
if (pageUrl) {
|
|
401
|
+
provider = this.connectionManager.findProviderByPageUrl(pageUrl);
|
|
402
|
+
if (!provider) {
|
|
403
|
+
this.sendError(consumer, requestId, "E004" /* PAGE_NOT_FOUND */, `\u672A\u627E\u5230\u9875\u9762: ${pageUrl}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
provider = this.connectionManager.getFirstProvider();
|
|
408
|
+
if (!provider) {
|
|
409
|
+
this.sendError(consumer, requestId, "E003" /* NO_PAGE_CONNECTED */, "\u6CA1\u6709\u9875\u9762\u8FDE\u63A5\u5230 Server");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const timer = setTimeout(() => {
|
|
414
|
+
this.handleTimeout(requestId);
|
|
415
|
+
}, REQUEST_TIMEOUT);
|
|
416
|
+
this.pendingRequests.set(requestId, {
|
|
417
|
+
id: requestId,
|
|
418
|
+
consumer,
|
|
419
|
+
timer,
|
|
420
|
+
timestamp: Date.now()
|
|
421
|
+
});
|
|
422
|
+
const forwardMessage = {
|
|
423
|
+
id: requestId,
|
|
424
|
+
type,
|
|
425
|
+
pageUrl: pageUrl || provider.connectionInfo.pageUrl,
|
|
426
|
+
params,
|
|
427
|
+
timestamp: Date.now()
|
|
428
|
+
};
|
|
429
|
+
try {
|
|
430
|
+
provider.send(JSON.stringify(forwardMessage));
|
|
431
|
+
this.logger.info(`\u8BF7\u6C42\u8F6C\u53D1: ${type} -> ${provider.connectionInfo.pageUrl}`);
|
|
432
|
+
} catch {
|
|
433
|
+
this.cleanupRequest(requestId);
|
|
434
|
+
this.sendError(consumer, requestId, "E001" /* CONNECTION_FAILED */, "\u8F6C\u53D1\u8BF7\u6C42\u5931\u8D25");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 处理 Provider 响应
|
|
439
|
+
*/
|
|
440
|
+
handleResponse(response) {
|
|
441
|
+
const { id } = response;
|
|
442
|
+
const pending = this.pendingRequests.get(id);
|
|
443
|
+
if (!pending) {
|
|
444
|
+
this.logger.warn(`\u6536\u5230\u672A\u77E5\u8BF7\u6C42\u7684\u54CD\u5E94: ${id}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.cleanupRequest(id);
|
|
448
|
+
try {
|
|
449
|
+
pending.consumer.send(JSON.stringify(response));
|
|
450
|
+
this.logger.info(
|
|
451
|
+
`\u54CD\u5E94\u8FD4\u56DE: ${id} (${response.success ? "\u6210\u529F" : "\u5931\u8D25"})`
|
|
452
|
+
);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.logger.error(`\u54CD\u5E94\u8F6C\u53D1\u5931\u8D25: ${id}`, error);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 处理请求超时
|
|
459
|
+
*/
|
|
460
|
+
handleTimeout(requestId) {
|
|
461
|
+
const pending = this.pendingRequests.get(requestId);
|
|
462
|
+
if (!pending) return;
|
|
463
|
+
this.logger.warn(`\u8BF7\u6C42\u8D85\u65F6: ${requestId}`);
|
|
464
|
+
this.cleanupRequest(requestId);
|
|
465
|
+
this.sendError(pending.consumer, requestId, "E012" /* REQUEST_TIMEOUT */, "\u8BF7\u6C42\u8D85\u65F6");
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 清理请求
|
|
469
|
+
*/
|
|
470
|
+
cleanupRequest(requestId) {
|
|
471
|
+
const pending = this.pendingRequests.get(requestId);
|
|
472
|
+
if (pending) {
|
|
473
|
+
clearTimeout(pending.timer);
|
|
474
|
+
this.pendingRequests.delete(requestId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* 发送错误响应
|
|
479
|
+
*/
|
|
480
|
+
sendError(consumer, requestId, code, message) {
|
|
481
|
+
const error = new MGError(code, message);
|
|
482
|
+
const response = {
|
|
483
|
+
id: requestId,
|
|
484
|
+
type: "error" /* ERROR */,
|
|
485
|
+
success: false,
|
|
486
|
+
data: null,
|
|
487
|
+
error: error.toJSON()
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
consumer.send(JSON.stringify(response));
|
|
491
|
+
} catch (err) {
|
|
492
|
+
this.logger.error(`\u53D1\u9001\u9519\u8BEF\u54CD\u5E94\u5931\u8D25: ${requestId}`, err);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 清理特定连接的所有待处理请求
|
|
497
|
+
*/
|
|
498
|
+
cleanupConnectionRequests(connectionId) {
|
|
499
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
500
|
+
if (pending.consumer.connectionId === connectionId) {
|
|
501
|
+
this.cleanupRequest(requestId);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 清理所有待处理请求
|
|
507
|
+
*/
|
|
508
|
+
cleanupAll() {
|
|
509
|
+
for (const [requestId] of this.pendingRequests) {
|
|
510
|
+
this.cleanupRequest(requestId);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/server/websocket-server.ts
|
|
516
|
+
var MGServer = class {
|
|
517
|
+
wss = null;
|
|
518
|
+
logger;
|
|
519
|
+
connectionManager;
|
|
520
|
+
requestHandler;
|
|
521
|
+
port;
|
|
522
|
+
isRunning = false;
|
|
523
|
+
constructor(options = {}) {
|
|
524
|
+
this.port = options.port || DEFAULT_PORT;
|
|
525
|
+
this.logger = options.logger || createLogger();
|
|
526
|
+
this.connectionManager = new ConnectionManager(this.logger);
|
|
527
|
+
this.requestHandler = new RequestHandler(this.connectionManager, this.logger);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 启动服务器
|
|
531
|
+
*/
|
|
532
|
+
async start() {
|
|
533
|
+
if (this.isRunning) {
|
|
534
|
+
throw new MGError("E016" /* SERVER_ALREADY_RUNNING */, "Server \u5DF2\u5728\u8FD0\u884C\u4E2D");
|
|
535
|
+
}
|
|
536
|
+
const port2 = await this.findAvailablePort();
|
|
537
|
+
return new Promise((resolve2, reject) => {
|
|
538
|
+
this.wss = new WebSocketServer({ port: port2 });
|
|
539
|
+
this.wss.on("listening", () => {
|
|
540
|
+
this.port = port2;
|
|
541
|
+
this.isRunning = true;
|
|
542
|
+
this.logger.info(`Server \u542F\u52A8\u6210\u529F\uFF0C\u76D1\u542C\u7AEF\u53E3: ${port2}`);
|
|
543
|
+
this.connectionManager.startHeartbeatCheck(HEARTBEAT_INTERVAL);
|
|
544
|
+
resolve2(port2);
|
|
545
|
+
});
|
|
546
|
+
this.wss.on("error", (error) => {
|
|
547
|
+
this.logger.error("Server \u9519\u8BEF:", error);
|
|
548
|
+
reject(error);
|
|
549
|
+
});
|
|
550
|
+
this.wss.on("connection", (ws, request) => {
|
|
551
|
+
this.handleConnection(ws, request);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 查找可用端口
|
|
557
|
+
*/
|
|
558
|
+
async findAvailablePort() {
|
|
559
|
+
for (let port2 = PORT_RANGE_START; port2 <= PORT_RANGE_END; port2++) {
|
|
560
|
+
const available = await this.isPortAvailable(port2);
|
|
561
|
+
if (available) {
|
|
562
|
+
return port2;
|
|
563
|
+
}
|
|
564
|
+
this.logger.debug(`\u7AEF\u53E3 ${port2} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u4E0B\u4E00\u4E2A`);
|
|
565
|
+
}
|
|
566
|
+
throw new MGError(
|
|
567
|
+
"E013" /* PORT_EXHAUSTED */,
|
|
568
|
+
`\u7AEF\u53E3 ${PORT_RANGE_START}-${PORT_RANGE_END} \u5747\u88AB\u5360\u7528`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 检查端口是否可用
|
|
573
|
+
*/
|
|
574
|
+
isPortAvailable(port2) {
|
|
575
|
+
return new Promise((resolve2) => {
|
|
576
|
+
const testServer = new WebSocketServer({ port: port2 });
|
|
577
|
+
testServer.on("listening", () => {
|
|
578
|
+
testServer.close();
|
|
579
|
+
resolve2(true);
|
|
580
|
+
});
|
|
581
|
+
testServer.on("error", () => {
|
|
582
|
+
resolve2(false);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 处理新连接
|
|
588
|
+
*/
|
|
589
|
+
handleConnection(ws, _request) {
|
|
590
|
+
this.logger.info("\u65B0\u8FDE\u63A5\u5EFA\u7ACB");
|
|
591
|
+
const registerTimeout = setTimeout(() => {
|
|
592
|
+
this.logger.warn("\u8FDE\u63A5\u6CE8\u518C\u8D85\u65F6\uFF0C\u5173\u95ED\u8FDE\u63A5");
|
|
593
|
+
ws.close();
|
|
594
|
+
}, 5e3);
|
|
595
|
+
ws.on("message", (data) => {
|
|
596
|
+
try {
|
|
597
|
+
const message = JSON.parse(data.toString());
|
|
598
|
+
if (message.type === "register" /* REGISTER */) {
|
|
599
|
+
clearTimeout(registerTimeout);
|
|
600
|
+
this.handleRegister(ws, message);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const managedWs = ws;
|
|
604
|
+
if (!managedWs.connectionId) {
|
|
605
|
+
this.logger.warn("\u672A\u6CE8\u518C\u7684\u8FDE\u63A5\u53D1\u9001\u6D88\u606F\uFF0C\u5FFD\u7565");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.handleMessage(managedWs, message);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this.logger.error("\u6D88\u606F\u89E3\u6790\u5931\u8D25:", error);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
ws.on("close", () => {
|
|
614
|
+
clearTimeout(registerTimeout);
|
|
615
|
+
const managedWs = ws;
|
|
616
|
+
if (managedWs.connectionId) {
|
|
617
|
+
this.requestHandler.cleanupConnectionRequests(managedWs.connectionId);
|
|
618
|
+
this.connectionManager.removeConnection(managedWs);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
ws.on("error", (error) => {
|
|
622
|
+
this.logger.error("WebSocket \u9519\u8BEF:", error);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* 处理注册消息
|
|
627
|
+
*/
|
|
628
|
+
handleRegister(ws, message) {
|
|
629
|
+
const { connectionType, pageUrl, pageId } = message.data;
|
|
630
|
+
const managedWs = this.connectionManager.addConnection(
|
|
631
|
+
ws,
|
|
632
|
+
connectionType,
|
|
633
|
+
pageUrl,
|
|
634
|
+
pageId
|
|
635
|
+
);
|
|
636
|
+
const ack = {
|
|
637
|
+
id: message.id || "",
|
|
638
|
+
type: "register_ack" /* REGISTER_ACK */,
|
|
639
|
+
success: true,
|
|
640
|
+
data: {
|
|
641
|
+
connectionId: managedWs.connectionId,
|
|
642
|
+
pageUrl
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
ws.send(JSON.stringify(ack));
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* 处理消息
|
|
649
|
+
*/
|
|
650
|
+
handleMessage(ws, message) {
|
|
651
|
+
this.connectionManager.updateLastActive(ws);
|
|
652
|
+
switch (message.type) {
|
|
653
|
+
case "ping" /* PING */:
|
|
654
|
+
this.handlePing(ws, message);
|
|
655
|
+
break;
|
|
656
|
+
case "response" /* RESPONSE */:
|
|
657
|
+
case "error" /* ERROR */:
|
|
658
|
+
this.requestHandler.handleResponse(message);
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
if (ws.connectionInfo.type === "consumer" /* CONSUMER */) {
|
|
662
|
+
this.requestHandler.handleRequest(ws, message);
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* 处理心跳
|
|
669
|
+
*/
|
|
670
|
+
handlePing(ws, message) {
|
|
671
|
+
const pong = {
|
|
672
|
+
type: "pong" /* PONG */,
|
|
673
|
+
timestamp: message.timestamp || Date.now()
|
|
674
|
+
};
|
|
675
|
+
ws.send(JSON.stringify(pong));
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 停止服务器
|
|
679
|
+
*/
|
|
680
|
+
async stop() {
|
|
681
|
+
if (!this.isRunning || !this.wss) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.logger.info("\u6B63\u5728\u505C\u6B62 Server...");
|
|
685
|
+
this.requestHandler.cleanupAll();
|
|
686
|
+
this.connectionManager.closeAll();
|
|
687
|
+
return new Promise((resolve2) => {
|
|
688
|
+
this.wss.close(() => {
|
|
689
|
+
this.isRunning = false;
|
|
690
|
+
this.wss = null;
|
|
691
|
+
this.logger.info("Server \u5DF2\u505C\u6B62");
|
|
692
|
+
resolve2();
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* 获取运行状态
|
|
698
|
+
*/
|
|
699
|
+
getStatus() {
|
|
700
|
+
return {
|
|
701
|
+
running: this.isRunning,
|
|
702
|
+
port: this.port,
|
|
703
|
+
stats: this.connectionManager.getStats(),
|
|
704
|
+
connectedPages: this.connectionManager.getConnectedPageUrls()
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 获取端口
|
|
709
|
+
*/
|
|
710
|
+
getPort() {
|
|
711
|
+
return this.port;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* 是否运行中
|
|
715
|
+
*/
|
|
716
|
+
isServerRunning() {
|
|
717
|
+
return this.isRunning;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
function createServer(options) {
|
|
721
|
+
return new MGServer(options);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/server/daemon.ts
|
|
725
|
+
function isServerRunning() {
|
|
726
|
+
const info = readServerInfo();
|
|
727
|
+
if (!info) {
|
|
728
|
+
return { running: false, info: null };
|
|
729
|
+
}
|
|
730
|
+
if (!isProcessRunning(info.pid)) {
|
|
731
|
+
deleteServerInfo();
|
|
732
|
+
return { running: false, info: null };
|
|
733
|
+
}
|
|
734
|
+
return { running: true, info };
|
|
735
|
+
}
|
|
736
|
+
async function startServerForeground(port2) {
|
|
737
|
+
const { running, info } = isServerRunning();
|
|
738
|
+
if (running && info) {
|
|
739
|
+
throw new MGError(
|
|
740
|
+
"E016" /* SERVER_ALREADY_RUNNING */,
|
|
741
|
+
`Server \u5DF2\u5728\u8FD0\u884C\u4E2D (PID: ${info.pid}, \u7AEF\u53E3: ${info.port})`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
ensureConfigDir();
|
|
745
|
+
const logger = createLogger({
|
|
746
|
+
console: true,
|
|
747
|
+
file: true,
|
|
748
|
+
minLevel: "INFO" /* INFO */
|
|
749
|
+
});
|
|
750
|
+
const server = createServer({
|
|
751
|
+
port: port2 || DEFAULT_PORT,
|
|
752
|
+
logger
|
|
753
|
+
});
|
|
754
|
+
const cleanup = async () => {
|
|
755
|
+
console.log("\n\u6B63\u5728\u505C\u6B62 Server...");
|
|
756
|
+
await server.stop();
|
|
757
|
+
deleteServerInfo();
|
|
758
|
+
process.exit(0);
|
|
759
|
+
};
|
|
760
|
+
process.on("SIGINT", cleanup);
|
|
761
|
+
process.on("SIGTERM", cleanup);
|
|
762
|
+
try {
|
|
763
|
+
const actualPort = await server.start();
|
|
764
|
+
writeServerInfo({
|
|
765
|
+
port: actualPort,
|
|
766
|
+
pid: process.pid,
|
|
767
|
+
startedAt: getCurrentISOTime()
|
|
768
|
+
});
|
|
769
|
+
console.log(`
|
|
770
|
+
MG Server \u542F\u52A8\u6210\u529F`);
|
|
771
|
+
console.log(`\u76D1\u542C\u7AEF\u53E3: ${actualPort}`);
|
|
772
|
+
console.log(`\u8FDB\u7A0B PID: ${process.pid}`);
|
|
773
|
+
console.log(`\u8FD0\u884C\u6A21\u5F0F: \u524D\u53F0`);
|
|
774
|
+
console.log(`
|
|
775
|
+
\u6309 Ctrl+C \u505C\u6B62...`);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
logger.error("Server \u542F\u52A8\u5931\u8D25:", error);
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/server/daemon-runner.ts
|
|
783
|
+
var args = process.argv.slice(2);
|
|
784
|
+
var port;
|
|
785
|
+
for (let i = 0; i < args.length; i++) {
|
|
786
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
787
|
+
port = parseInt(args[i + 1], 10);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
startServerForeground(port).catch((error) => {
|
|
791
|
+
console.error("\u5B88\u62A4\u8FDB\u7A0B\u542F\u52A8\u5931\u8D25:", error);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
});
|
|
794
|
+
//# sourceMappingURL=daemon-runner.js.map
|