@hangox/mg-cli 1.0.6 → 1.0.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.js CHANGED
@@ -34,6 +34,7 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
34
34
  MessageType2["REGISTER"] = "register";
35
35
  MessageType2["REGISTER_ACK"] = "register_ack";
36
36
  MessageType2["GET_NODE_BY_ID"] = "get_node_by_id";
37
+ MessageType2["GET_PAGE_BY_ID"] = "get_page_by_id";
37
38
  MessageType2["GET_ALL_NODES"] = "get_all_nodes";
38
39
  MessageType2["GET_SELECTION"] = "get_selection";
39
40
  MessageType2["EXPORT_IMAGE"] = "export_image";
@@ -225,27 +226,44 @@ function parseMgpLink(link) {
225
226
  const queryString = urlPart.slice(questionMarkIndex + 1);
226
227
  const params = new URLSearchParams(queryString);
227
228
  const encodedNodeId = params.get("nodeId");
228
- if (!encodedNodeId) {
229
+ const encodedPageId = params.get("pageId");
230
+ if (!encodedNodeId && !encodedPageId) {
229
231
  return null;
230
232
  }
231
- const nodeId = decodeURIComponent(encodedNodeId);
232
- if (!/^(\d+:\d+)(\/\d+:\d+)*$/.test(nodeId)) {
233
+ if (encodedNodeId && encodedPageId) {
233
234
  return null;
234
235
  }
235
- const encodedNodePath = params.get("nodePath");
236
- let nodePath;
237
- if (encodedNodePath) {
238
- const decodedNodePath = decodeURIComponent(encodedNodePath);
239
- nodePath = decodedNodePath.split("/").filter(Boolean);
240
- if (!nodePath.every((segment) => /^\d+:\d+$/.test(segment))) {
236
+ if (encodedNodeId) {
237
+ const nodeId = decodeURIComponent(encodedNodeId);
238
+ if (!/^(\d+:\d+)(\/\d+:\d+)*$/.test(nodeId)) {
241
239
  return null;
242
240
  }
241
+ const encodedNodePath = params.get("nodePath");
242
+ let nodePath;
243
+ if (encodedNodePath) {
244
+ const decodedNodePath = decodeURIComponent(encodedNodePath);
245
+ nodePath = decodedNodePath.split("/").filter(Boolean);
246
+ if (!nodePath.every((segment) => /^\d+:\d+$/.test(segment))) {
247
+ return null;
248
+ }
249
+ }
250
+ return {
251
+ pageUrl,
252
+ nodeId,
253
+ nodePath
254
+ };
243
255
  }
244
- return {
245
- pageUrl,
246
- nodeId,
247
- nodePath
248
- };
256
+ if (encodedPageId) {
257
+ const pageId = decodeURIComponent(encodedPageId);
258
+ if (!/^\d+:\d+$/.test(pageId)) {
259
+ return null;
260
+ }
261
+ return {
262
+ pageUrl,
263
+ pageId
264
+ };
265
+ }
266
+ return null;
249
267
  } catch {
250
268
  return null;
251
269
  }
@@ -260,6 +278,11 @@ function generateMgpLink(pageUrl, nodeId, nodePath) {
260
278
  }
261
279
  return link;
262
280
  }
281
+ function generatePageLink(pageUrl, pageId) {
282
+ const normalizedUrl = normalizePageUrl(pageUrl);
283
+ const encodedPageId = encodeURIComponent(pageId);
284
+ return `mgp://${normalizedUrl}?pageId=${encodedPageId}`;
285
+ }
263
286
  function formatFileSize(bytes) {
264
287
  if (bytes < 1024) {
265
288
  return `${bytes} \u5B57\u8282`;
@@ -557,7 +580,7 @@ function createLogger(options) {
557
580
  // src/server/connection-manager.ts
558
581
  var ConnectionManager = class {
559
582
  logger;
560
- /** Provider 连接(按页面 URL 索引) */
583
+ /** Provider 连接(按页面 URL 索引,支持同 URL 多个连接) */
561
584
  providers = /* @__PURE__ */ new Map();
562
585
  /** Consumer 连接 */
563
586
  consumers = /* @__PURE__ */ new Map();
@@ -593,7 +616,8 @@ var ConnectionManager = class {
593
616
  */
594
617
  checkHeartbeats() {
595
618
  const now = Date.now();
596
- for (const [id, ws] of this.allConnections) {
619
+ const entries = Array.from(this.allConnections.entries());
620
+ for (const [id, ws] of entries) {
597
621
  const lastActive = ws.connectionInfo.lastActiveAt.getTime();
598
622
  const elapsed = now - lastActive;
599
623
  if (elapsed > HEARTBEAT_TIMEOUT) {
@@ -623,14 +647,10 @@ var ConnectionManager = class {
623
647
  managedWs.isAlive = true;
624
648
  this.allConnections.set(connectionId, managedWs);
625
649
  if (type === "provider" /* PROVIDER */ && pageUrl) {
626
- const existing = this.providers.get(pageUrl);
627
- if (existing) {
628
- this.logger.info(`\u9875\u9762 ${pageUrl} \u5DF2\u6709\u8FDE\u63A5\uFF0C\u66FF\u6362\u4E3A\u65B0\u8FDE\u63A5`);
629
- this.removeConnection(existing);
630
- existing.close();
631
- }
632
- this.providers.set(pageUrl, managedWs);
633
- this.logger.info(`Provider \u8FDE\u63A5: ${pageUrl}`);
650
+ const existing = this.providers.get(pageUrl) || [];
651
+ existing.push(managedWs);
652
+ this.providers.set(pageUrl, existing);
653
+ this.logger.info(`Provider \u8FDE\u63A5: ${pageUrl} (\u5F53\u524D\u8BE5\u9875\u9762\u8FDE\u63A5\u6570: ${existing.length})`);
634
654
  } else if (type === "consumer" /* CONSUMER */) {
635
655
  this.consumers.set(connectionId, managedWs);
636
656
  this.logger.info(`Consumer \u8FDE\u63A5: ${connectionId}`);
@@ -644,8 +664,17 @@ var ConnectionManager = class {
644
664
  const { connectionId, connectionInfo } = ws;
645
665
  this.allConnections.delete(connectionId);
646
666
  if (connectionInfo.type === "provider" /* PROVIDER */ && connectionInfo.pageUrl) {
647
- this.providers.delete(connectionInfo.pageUrl);
648
- this.logger.info(`Provider \u65AD\u5F00: ${connectionInfo.pageUrl}`);
667
+ const connections = this.providers.get(connectionInfo.pageUrl);
668
+ if (connections) {
669
+ const index = connections.findIndex((c) => c.connectionId === connectionId);
670
+ if (index !== -1) {
671
+ connections.splice(index, 1);
672
+ if (connections.length === 0) {
673
+ this.providers.delete(connectionInfo.pageUrl);
674
+ }
675
+ }
676
+ }
677
+ this.logger.info(`Provider \u65AD\u5F00: ${connectionInfo.pageUrl} (\u8FDE\u63A5ID: ${connectionId})`);
649
678
  } else if (connectionInfo.type === "consumer" /* CONSUMER */) {
650
679
  this.consumers.delete(connectionId);
651
680
  this.logger.info(`Consumer \u65AD\u5F00: ${connectionId}`);
@@ -659,31 +688,48 @@ var ConnectionManager = class {
659
688
  ws.isAlive = true;
660
689
  }
661
690
  /**
662
- * 根据页面 URL 查找 Provider
691
+ * 根据页面 URL 查找 Provider(返回第一个可用的连接)
663
692
  */
664
693
  findProviderByPageUrl(pageUrl) {
665
- return this.providers.get(pageUrl);
694
+ const connections = this.providers.get(pageUrl);
695
+ return connections?.[0];
666
696
  }
667
697
  /**
668
698
  * 获取第一个可用的 Provider
669
699
  */
670
700
  getFirstProvider() {
671
- const iterator = this.providers.values();
672
- const first = iterator.next();
673
- return first.value;
701
+ const allConnections = Array.from(this.providers.values());
702
+ for (const connections of allConnections) {
703
+ if (connections.length > 0) {
704
+ return connections[0];
705
+ }
706
+ }
707
+ return void 0;
674
708
  }
675
709
  /**
676
710
  * 获取所有 Provider 信息
677
711
  */
678
712
  getAllProviders() {
679
- return Array.from(this.providers.values()).map((ws) => ws.connectionInfo);
713
+ const result = [];
714
+ const allConnections = Array.from(this.providers.values());
715
+ for (const connections of allConnections) {
716
+ for (const ws of connections) {
717
+ result.push(ws.connectionInfo);
718
+ }
719
+ }
720
+ return result;
680
721
  }
681
722
  /**
682
723
  * 获取连接统计
683
724
  */
684
725
  getStats() {
726
+ let providerCount = 0;
727
+ const allConnections = Array.from(this.providers.values());
728
+ for (const connections of allConnections) {
729
+ providerCount += connections.length;
730
+ }
685
731
  return {
686
- providers: this.providers.size,
732
+ providers: providerCount,
687
733
  consumers: this.consumers.size,
688
734
  total: this.allConnections.size
689
735
  };
@@ -699,7 +745,8 @@ var ConnectionManager = class {
699
745
  */
700
746
  closeAll() {
701
747
  this.stopHeartbeatCheck();
702
- for (const ws of this.allConnections.values()) {
748
+ const allWs = Array.from(this.allConnections.values());
749
+ for (const ws of allWs) {
703
750
  ws.close();
704
751
  }
705
752
  this.providers.clear();
@@ -853,24 +900,11 @@ function getVersion() {
853
900
  try {
854
901
  const currentFile = fileURLToPath(import.meta.url);
855
902
  const currentDir = dirname3(currentFile);
856
- const versionFilePaths = [
857
- join2(currentDir, "..", "VERSION"),
858
- // dist/xxx.js -> ../VERSION
859
- join2(currentDir, "..", "..", "VERSION")
860
- // src/shared/version.ts -> ../../VERSION
861
- ];
862
- for (const versionFilePath of versionFilePaths) {
863
- if (existsSync3(versionFilePath)) {
864
- const version = readFileSync2(versionFilePath, "utf-8").trim();
865
- if (version) {
866
- cachedVersion = version;
867
- return cachedVersion;
868
- }
869
- }
870
- }
871
903
  const packageJsonPaths = [
872
904
  join2(currentDir, "..", "package.json"),
905
+ // dist/xxx.js -> ../package.json
873
906
  join2(currentDir, "..", "..", "package.json")
907
+ // src/shared/version.ts -> ../../package.json
874
908
  ];
875
909
  for (const packageJsonPath of packageJsonPaths) {
876
910
  if (existsSync3(packageJsonPath)) {
@@ -888,6 +922,13 @@ function getVersion() {
888
922
  return cachedVersion;
889
923
  }
890
924
  }
925
+ var DEV_VERSION = "9.9.9";
926
+ function isVersionMatch(version1, version2) {
927
+ if (version1 === DEV_VERSION || version2 === DEV_VERSION) {
928
+ return true;
929
+ }
930
+ return version1 === version2;
931
+ }
891
932
 
892
933
  // src/server/websocket-server.ts
893
934
  var MGServer = class {
@@ -1134,6 +1175,247 @@ var MGServer = class {
1134
1175
  function createServer(options) {
1135
1176
  return new MGServer(options);
1136
1177
  }
1178
+
1179
+ // src/cli/client.ts
1180
+ import WebSocket2 from "ws";
1181
+
1182
+ // src/server/daemon.ts
1183
+ import { spawn } from "child_process";
1184
+ import { fileURLToPath as fileURLToPath2 } from "url";
1185
+ import { dirname as dirname4, join as join3 } from "path";
1186
+ function isServerRunning() {
1187
+ const info = readServerInfo();
1188
+ if (!info) {
1189
+ return { running: false, info: null };
1190
+ }
1191
+ if (!isProcessRunning(info.pid)) {
1192
+ deleteServerInfo();
1193
+ return { running: false, info: null };
1194
+ }
1195
+ return { running: true, info };
1196
+ }
1197
+ async function startServerDaemon(port) {
1198
+ const { running, info } = isServerRunning();
1199
+ if (running && info) {
1200
+ throw new MGError(
1201
+ "E016" /* SERVER_ALREADY_RUNNING */,
1202
+ `Server \u5DF2\u5728\u8FD0\u884C\u4E2D (PID: ${info.pid}, \u7AEF\u53E3: ${info.port})`
1203
+ );
1204
+ }
1205
+ ensureConfigDir();
1206
+ const currentFile = fileURLToPath2(import.meta.url);
1207
+ const currentDir = dirname4(currentFile);
1208
+ const serverScript = join3(currentDir, "daemon-runner.js");
1209
+ const args = ["--foreground"];
1210
+ if (port) {
1211
+ args.push("--port", String(port));
1212
+ }
1213
+ const child = spawn(process.execPath, [serverScript, ...args], {
1214
+ detached: true,
1215
+ stdio: "ignore",
1216
+ env: {
1217
+ ...process.env,
1218
+ MG_DAEMON: "1"
1219
+ }
1220
+ });
1221
+ child.unref();
1222
+ const startTime = Date.now();
1223
+ while (Date.now() - startTime < SERVER_START_TIMEOUT) {
1224
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
1225
+ const { running: running2, info: info2 } = isServerRunning();
1226
+ if (running2 && info2) {
1227
+ return info2;
1228
+ }
1229
+ }
1230
+ throw new MGError("E015" /* SERVER_START_FAILED */, "Server \u542F\u52A8\u8D85\u65F6");
1231
+ }
1232
+
1233
+ // src/cli/client.ts
1234
+ var MGClient = class {
1235
+ ws = null;
1236
+ options;
1237
+ constructor(options = {}) {
1238
+ this.options = options;
1239
+ }
1240
+ /**
1241
+ * 连接到 Server
1242
+ */
1243
+ async connect() {
1244
+ const serverInfo = readServerInfo();
1245
+ if (serverInfo) {
1246
+ if (isProcessRunning(serverInfo.pid)) {
1247
+ const currentVersion = getVersion();
1248
+ if (!isVersionMatch(currentVersion, serverInfo.version)) {
1249
+ console.warn(`\u26A0\uFE0F \u7248\u672C\u4E0D\u5339\u914D: CLI ${currentVersion} vs Server ${serverInfo.version}`);
1250
+ console.warn("\u63D0\u793A: \u5982\u9700\u5BF9\u9F50\u7248\u672C\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C `npx -y @hangox/mg-cli@latest server restart`");
1251
+ }
1252
+ try {
1253
+ await this.tryConnect(serverInfo.port);
1254
+ return;
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ }
1259
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
1260
+ try {
1261
+ await this.tryConnect(port);
1262
+ return;
1263
+ } catch {
1264
+ }
1265
+ }
1266
+ if (!this.options.noAutoStart) {
1267
+ console.log("Server \u672A\u8FD0\u884C\uFF0C\u6B63\u5728\u81EA\u52A8\u542F\u52A8...");
1268
+ try {
1269
+ const info = await startServerDaemon();
1270
+ console.log(`Server \u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3: ${info.port}`);
1271
+ await this.waitForServer(info.port);
1272
+ return;
1273
+ } catch (error) {
1274
+ throw new MGError(
1275
+ "E015" /* SERVER_START_FAILED */,
1276
+ `\u81EA\u52A8\u542F\u52A8 Server \u5931\u8D25: ${error instanceof Error ? error.message : error}`
1277
+ );
1278
+ }
1279
+ }
1280
+ throw new MGError("E001" /* CONNECTION_FAILED */, ErrorMessages["E001" /* CONNECTION_FAILED */]);
1281
+ }
1282
+ /**
1283
+ * 尝试连接指定端口
1284
+ */
1285
+ tryConnect(port) {
1286
+ return new Promise((resolve2, reject) => {
1287
+ const ws = new WebSocket2(`ws://localhost:${port}`);
1288
+ const timer = setTimeout(() => {
1289
+ ws.close();
1290
+ reject(new Error("\u8FDE\u63A5\u8D85\u65F6"));
1291
+ }, PORT_SCAN_TIMEOUT);
1292
+ ws.on("open", () => {
1293
+ clearTimeout(timer);
1294
+ this.ws = ws;
1295
+ this.register();
1296
+ resolve2();
1297
+ });
1298
+ ws.on("error", (error) => {
1299
+ clearTimeout(timer);
1300
+ reject(error);
1301
+ });
1302
+ });
1303
+ }
1304
+ /**
1305
+ * 等待 Server 就绪
1306
+ */
1307
+ async waitForServer(port) {
1308
+ const startTime = Date.now();
1309
+ const interval = 500;
1310
+ while (Date.now() - startTime < SERVER_START_TIMEOUT) {
1311
+ try {
1312
+ await this.tryConnect(port);
1313
+ return;
1314
+ } catch {
1315
+ await new Promise((r) => setTimeout(r, interval));
1316
+ }
1317
+ }
1318
+ throw new Error("\u7B49\u5F85 Server \u542F\u52A8\u8D85\u65F6");
1319
+ }
1320
+ /**
1321
+ * 注册为 Consumer
1322
+ */
1323
+ register() {
1324
+ if (!this.ws) return;
1325
+ const message = {
1326
+ type: "register" /* REGISTER */,
1327
+ data: {
1328
+ connectionType: "consumer" /* CONSUMER */
1329
+ },
1330
+ timestamp: Date.now()
1331
+ };
1332
+ this.ws.send(JSON.stringify(message));
1333
+ }
1334
+ /**
1335
+ * 发送请求并等待响应
1336
+ */
1337
+ async request(type, params, pageUrl) {
1338
+ if (!this.ws) {
1339
+ throw new MGError("E001" /* CONNECTION_FAILED */, "\u672A\u8FDE\u63A5\u5230 Server");
1340
+ }
1341
+ const requestId = generateId();
1342
+ const message = {
1343
+ id: requestId,
1344
+ type,
1345
+ params,
1346
+ pageUrl,
1347
+ timestamp: Date.now()
1348
+ };
1349
+ return new Promise((resolve2, reject) => {
1350
+ const timer = setTimeout(() => {
1351
+ reject(new MGError("E012" /* REQUEST_TIMEOUT */, ErrorMessages["E012" /* REQUEST_TIMEOUT */]));
1352
+ }, REQUEST_TIMEOUT);
1353
+ const messageHandler = (data) => {
1354
+ try {
1355
+ const response = JSON.parse(data.toString());
1356
+ if (response.id === requestId) {
1357
+ clearTimeout(timer);
1358
+ this.ws?.off("message", messageHandler);
1359
+ if (response.success) {
1360
+ resolve2(response.data);
1361
+ } else {
1362
+ const error = response.error;
1363
+ reject(
1364
+ new MGError(
1365
+ error?.code || "E099" /* UNKNOWN_ERROR */,
1366
+ error?.message || "\u672A\u77E5\u9519\u8BEF"
1367
+ )
1368
+ );
1369
+ }
1370
+ }
1371
+ } catch {
1372
+ }
1373
+ };
1374
+ this.ws.on("message", messageHandler);
1375
+ this.ws.send(JSON.stringify(message));
1376
+ });
1377
+ }
1378
+ /**
1379
+ * 带重试的请求
1380
+ */
1381
+ async requestWithRetry(type, params, pageUrl) {
1382
+ if (this.options.noRetry) {
1383
+ return this.request(type, params, pageUrl);
1384
+ }
1385
+ let lastError = null;
1386
+ for (let attempt = 0; attempt <= MAX_RETRY_COUNT; attempt++) {
1387
+ try {
1388
+ return await this.request(type, params, pageUrl);
1389
+ } catch (error) {
1390
+ lastError = error instanceof Error ? error : new Error(String(error));
1391
+ if (error instanceof MGError) {
1392
+ const retryable = ["E017" /* CONNECTION_LOST */, "E012" /* REQUEST_TIMEOUT */];
1393
+ if (!retryable.includes(error.code)) {
1394
+ throw error;
1395
+ }
1396
+ }
1397
+ if (attempt < MAX_RETRY_COUNT) {
1398
+ const delay = RETRY_INTERVALS[attempt] || RETRY_INTERVALS[RETRY_INTERVALS.length - 1];
1399
+ await new Promise((r) => setTimeout(r, delay));
1400
+ try {
1401
+ await this.connect();
1402
+ } catch {
1403
+ }
1404
+ }
1405
+ }
1406
+ }
1407
+ throw lastError || new MGError("E099" /* UNKNOWN_ERROR */, "\u8BF7\u6C42\u5931\u8D25");
1408
+ }
1409
+ /**
1410
+ * 关闭连接
1411
+ */
1412
+ close() {
1413
+ if (this.ws) {
1414
+ this.ws.close();
1415
+ this.ws = null;
1416
+ }
1417
+ }
1418
+ };
1137
1419
  export {
1138
1420
  CONFIG_DIR,
1139
1421
  ConnectionManager,
@@ -1153,6 +1435,7 @@ export {
1153
1435
  Logger,
1154
1436
  MAX_PORT_ATTEMPTS,
1155
1437
  MAX_RETRY_COUNT,
1438
+ MGClient,
1156
1439
  MGError,
1157
1440
  MGServer,
1158
1441
  MessageType,
@@ -1185,6 +1468,7 @@ export {
1185
1468
  formatLogTime,
1186
1469
  generateId,
1187
1470
  generateMgpLink,
1471
+ generatePageLink,
1188
1472
  getCurrentISOTime,
1189
1473
  isDesignPageUrl,
1190
1474
  isProcessRunning,