@dimina-kit/devtools 0.3.1-dev.20260517100756 → 0.3.1-dev.20260518054451

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.
Files changed (32) hide show
  1. package/README.md +18 -7
  2. package/dist/main/index.bundle.js +106 -27
  3. package/dist/main/ipc/session.js +9 -1
  4. package/dist/main/menu/index.js +11 -0
  5. package/dist/main/services/projects/thumbnail.d.ts +4 -0
  6. package/dist/main/services/projects/thumbnail.js +32 -0
  7. package/dist/main/services/workspace/workspace-service.d.ts +2 -0
  8. package/dist/main/services/workspace/workspace-service.js +20 -0
  9. package/dist/main/utils/sender-policy.d.ts +7 -5
  10. package/dist/main/utils/sender-policy.js +8 -6
  11. package/dist/preload/runtime/custom-apis.js +32 -3
  12. package/dist/preload/windows/simulator.js +26 -5
  13. package/dist/renderer/assets/index-BFdItKBK.js +46 -0
  14. package/dist/renderer/assets/{input-C5M5Wxn6.js → input-AH9UDYmj.js} +2 -2
  15. package/dist/renderer/assets/{ipc-transport-CUsl-fS2.js → ipc-transport-DNgBM6Qz.js} +2 -2
  16. package/dist/renderer/assets/ipc-transport-Dvmd5JZj.css +1 -0
  17. package/dist/renderer/assets/{popover-BSBPVO0o.js → popover-D1hoVYiY.js} +2 -2
  18. package/dist/renderer/assets/{select-B1tF3UAc.js → select-BgDHRj6r.js} +2 -2
  19. package/dist/renderer/assets/{settings-api-BjJalghB.js → settings-api-DBKlNKV3.js} +2 -2
  20. package/dist/renderer/assets/{settings-7w54H6p-.js → settings-m-tcmZy_.js} +2 -2
  21. package/dist/renderer/assets/{workbenchSettings-q2_-nc8t.js → workbenchSettings-CGacu-pI.js} +2 -2
  22. package/dist/renderer/entries/main/index.html +5 -5
  23. package/dist/renderer/entries/popover/index.html +5 -5
  24. package/dist/renderer/entries/settings/index.html +5 -5
  25. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  26. package/dist/shared/ipc-channels.d.ts +6 -0
  27. package/dist/shared/ipc-channels.js +19 -0
  28. package/dist/shared/ipc-schemas.d.ts +4 -0
  29. package/dist/shared/ipc-schemas.js +4 -0
  30. package/package.json +4 -4
  31. package/dist/renderer/assets/index-DW_0thr-.js +0 -46
  32. package/dist/renderer/assets/ipc-transport-CltRuvhi.css +0 -1
package/README.md CHANGED
@@ -379,26 +379,37 @@ launch({ preloadPath: '/absolute/path/to/my-preload.js' })
379
379
 
380
380
  > ⚠️ 使用本功能要求模拟器 preload 调用 `installCustomApisBridge()`(详见上方 "Preload Instrumentation")。内置 preload 已包含此调用;若你用了自定义 preload 又忘记调用,注册的 API 在小程序运行时**没有任何反应**,且不会报错。
381
381
 
382
+ ### 命名约定
383
+
384
+ `name` 是 dimina service 层 `invokeAPI(name, opts)` 用的**裸名**,不带 `wx.` 前缀。注册之后小程序里通过 `wx.<name>(...)`(以及你在 `apiNamespaces` 配的其它命名空间)调用都会命中:
385
+
386
+ | 小程序里写 | `registerSimulatorApi` 的 name |
387
+ | --- | --- |
388
+ | `wx.login(...)` | `'login'` |
389
+ | `wx.request(...)` | `'request'` |
390
+ | `wx.getStorage(...)` 覆盖内置 | `'getStorage'`(小心:这会**屏蔽** dimina 容器的实现) |
391
+
382
392
  ```typescript
383
393
  import { registerSimulatorApi } from '@dimina-kit/devtools/simulator-apis'
384
394
 
385
- const dispose = registerSimulatorApi('myCompany.login', async ({ username }) => {
395
+ // 提供 wx.login 的实现(dimina 容器默认未实现,注册后才能用)
396
+ const dispose = registerSimulatorApi('login', async ({ success }) => {
386
397
  // 主进程上下文:可调用内部 HTTP、读取凭证文件、访问原生模块
387
- return await callInternalAuth(username)
398
+ const code = await callInternalAuth()
399
+ return { code }
388
400
  })
389
401
 
390
402
  // 不再需要时取消注册(不传也行,进程退出会一起清理)
391
403
  dispose()
392
404
  ```
393
405
 
394
- 几点行为约定:
406
+ ### 行为约定
395
407
 
408
+ - **命中顺序**:小程序运行时按 `apiRegistry[name] → MiniApp 实例方法 → 扩展模块兜底` 查找,所以 `registerSimulatorApi` 注册的 name **优先于内置实现**——既能给"未实现"的 wx.* 补功能(如 `wx.login`),也能覆盖默认的 wx.* 行为(如 `wx.request`,dimina-kit 本身就在用这条路把 `request` 接到主进程 fetch)。
396
409
  - 同名重复注册:后注册的覆盖前者,静默替换。
397
410
  - `dispose()` 只会移除自己创建的那次注册——如果之后被别人覆盖了,旧的 disposer 是 no-op。
398
- - 调用未注册的 name:小程序侧拿到 reject,错误信息里带 name。
399
- - Handler 可同步或异步返回;返回值需可 JSON 序列化(IPC 限制)。
400
-
401
- 对小程序侧来说,这些 name 和内置 API(`wx.getStorage` 等)走同一个 `AppManager.apiRegistry`,没有区别。
411
+ - 调用未注册且容器内也没有的 name:小程序侧拿到 reject,错误信息里带 name。
412
+ - Handler 可同步或异步返回;**返回值需可 JSON 序列化**(IPC 限制)。函数、Symbol、Map/Set、循环引用都会丢失。
402
413
 
403
414
  ---
404
415
 
@@ -73,9 +73,9 @@ function setupCdpPort() {
73
73
  }
74
74
 
75
75
  // src/main/app/app.ts
76
- import { app as app11, BrowserWindow as BrowserWindow3, nativeImage } from "electron";
77
- import fs5 from "fs";
78
- import path8 from "path";
76
+ import { app as app12, BrowserWindow as BrowserWindow3, nativeImage } from "electron";
77
+ import fs6 from "fs";
78
+ import path9 from "path";
79
79
 
80
80
  // src/main/utils/paths.ts
81
81
  import fs2 from "fs";
@@ -441,7 +441,9 @@ var ProjectChannel = {
441
441
  GetPages: "project:getPages",
442
442
  GetCompileConfig: "project:getCompileConfig",
443
443
  SaveCompileConfig: "project:saveCompileConfig",
444
- Status: "project:status"
444
+ Status: "project:status",
445
+ CaptureThumbnail: "project:captureThumbnail",
446
+ GetThumbnail: "project:getThumbnail"
445
447
  };
446
448
  var ProjectsChannel = {
447
449
  List: "projects:list",
@@ -867,6 +869,9 @@ function createWindowService(mainWindow) {
867
869
  };
868
870
  }
869
871
 
872
+ // src/main/services/workspace/workspace-service.ts
873
+ import { webContents as webContents2 } from "electron";
874
+
870
875
  // src/main/services/projects/project-repository.ts
871
876
  import { app as app6 } from "electron";
872
877
  import path7 from "path";
@@ -1054,6 +1059,38 @@ function updateProjectSettings(projectPath, patch) {
1054
1059
  );
1055
1060
  }
1056
1061
 
1062
+ // src/main/services/projects/thumbnail.ts
1063
+ import { createHash } from "crypto";
1064
+ import fs4 from "fs";
1065
+ import path8 from "path";
1066
+ import { app as app7 } from "electron";
1067
+ function getThumbnailDir() {
1068
+ return path8.join(app7.getPath("userData"), "thumbnails");
1069
+ }
1070
+ function hashProjectPath(projectPath) {
1071
+ return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
1072
+ }
1073
+ function getThumbnailPath(projectPath) {
1074
+ return path8.join(getThumbnailDir(), `${hashProjectPath(projectPath)}.png`);
1075
+ }
1076
+ function saveThumbnail(projectPath, image) {
1077
+ const png = image.toPNG();
1078
+ const dir = getThumbnailDir();
1079
+ fs4.mkdirSync(dir, { recursive: true });
1080
+ const filePath = getThumbnailPath(projectPath);
1081
+ fs4.writeFileSync(filePath, png);
1082
+ return `data:image/png;base64,${png.toString("base64")}`;
1083
+ }
1084
+ function loadThumbnail(projectPath) {
1085
+ const filePath = getThumbnailPath(projectPath);
1086
+ try {
1087
+ const buf = fs4.readFileSync(filePath);
1088
+ return `data:image/png;base64,${buf.toString("base64")}`;
1089
+ } catch {
1090
+ return null;
1091
+ }
1092
+ }
1093
+
1057
1094
  // src/main/services/workspace/workspace-service.ts
1058
1095
  function createWorkspaceService(ctx) {
1059
1096
  let currentSession = null;
@@ -1143,6 +1180,21 @@ function createWorkspaceService(ctx) {
1143
1180
  getSession: () => currentSession,
1144
1181
  getProjectPath: () => currentProjectPath,
1145
1182
  hasActiveSession: () => currentSession !== null,
1183
+ async captureThumbnail(projectPath) {
1184
+ const simWcId = ctx.views.getSimulatorWebContentsId();
1185
+ if (!simWcId) return null;
1186
+ const wc = webContents2.fromId(simWcId);
1187
+ if (!wc || wc.isDestroyed()) return null;
1188
+ try {
1189
+ const image = await wc.capturePage();
1190
+ return saveThumbnail(projectPath, image);
1191
+ } catch {
1192
+ return null;
1193
+ }
1194
+ },
1195
+ getThumbnail(projectPath) {
1196
+ return loadThumbnail(projectPath);
1197
+ },
1146
1198
  getProjectPages: (projectPath) => getProjectPages(projectPath),
1147
1199
  getCompileConfig: (projectPath) => getCompileConfig(projectPath),
1148
1200
  saveCompileConfig: (projectPath, config) => saveCompileConfig(projectPath, config),
@@ -1208,6 +1260,17 @@ function installAppMenu(ctx) {
1208
1260
  { role: "quit" }
1209
1261
  ]
1210
1262
  },
1263
+ {
1264
+ label: "\u9879\u76EE",
1265
+ submenu: [
1266
+ {
1267
+ label: "\u6253\u5F00\u9879\u76EE",
1268
+ click: () => {
1269
+ ctx.notify.windowNavigateBack();
1270
+ }
1271
+ }
1272
+ ]
1273
+ },
1211
1274
  {
1212
1275
  label: "\u7F16\u8F91",
1213
1276
  submenu: [
@@ -1249,7 +1312,7 @@ function installAppMenu(ctx) {
1249
1312
  }
1250
1313
 
1251
1314
  // src/main/ipc/app.ts
1252
- import fs4 from "fs";
1315
+ import fs5 from "fs";
1253
1316
 
1254
1317
  // src/main/utils/ipc-registry.ts
1255
1318
  import { ipcMain } from "electron";
@@ -1355,7 +1418,7 @@ var IpcRegistry = class {
1355
1418
  // src/main/ipc/app.ts
1356
1419
  function registerAppIpc(ctx) {
1357
1420
  return new IpcRegistry(ctx.senderPolicy).handle(AppChannel.GetPreloadPath, () => {
1358
- return `file://${fs4.realpathSync(ctx.preloadPath)}`;
1421
+ return `file://${fs5.realpathSync(ctx.preloadPath)}`;
1359
1422
  }).handle(AppChannel.GetBranding, async () => {
1360
1423
  if (ctx.brandingProvider) return ctx.brandingProvider();
1361
1424
  return { appName: ctx.appName };
@@ -1424,6 +1487,8 @@ var SettingsProjectSettingsChangedSchema = z.tuple([
1424
1487
  })
1425
1488
  ]);
1426
1489
  var PanelSelectSchema = z.tuple([z.string().min(1).max(200)]);
1490
+ var ProjectCaptureThumbnailSchema = z.tuple([AbsolutePath]);
1491
+ var ProjectGetThumbnailSchema = z.tuple([AbsolutePath]);
1427
1492
 
1428
1493
  // src/main/services/simulator/custom-apis.ts
1429
1494
  function createSimulatorApiRegistry() {
@@ -1478,7 +1543,7 @@ function registerSimulatorIpc(ctx) {
1478
1543
  }
1479
1544
 
1480
1545
  // src/main/ipc/panels.ts
1481
- import { webContents as webContents2 } from "electron";
1546
+ import { webContents as webContents3 } from "electron";
1482
1547
  var BUILTIN_PANELS = [
1483
1548
  { id: "wxml", label: "WXML" },
1484
1549
  { id: "appdata", label: "AppData" },
@@ -1491,7 +1556,7 @@ function registerPanelsIpc(ctx) {
1491
1556
  const [expression] = validate(PanelChannel.Eval, PanelEvalSchema, args);
1492
1557
  const simWcId = ctx.views.getSimulatorWebContentsId();
1493
1558
  if (!ctx.workspace.hasActiveSession() || simWcId == null) return void 0;
1494
- const sim = webContents2.fromId(simWcId);
1559
+ const sim = webContents3.fromId(simWcId);
1495
1560
  if (!sim || sim.isDestroyed()) return void 0;
1496
1561
  try {
1497
1562
  return await sim.executeJavaScript(expression);
@@ -1596,6 +1661,20 @@ function registerSessionIpc(ctx) {
1596
1661
  return ctx.workspace.saveCompileConfig(projectPath, config);
1597
1662
  }).handle(ProjectChannel.Close, () => {
1598
1663
  return ctx.workspace.closeProject();
1664
+ }).handle(ProjectChannel.CaptureThumbnail, (_, ...args) => {
1665
+ const [projectPath] = validate(
1666
+ ProjectChannel.CaptureThumbnail,
1667
+ ProjectCaptureThumbnailSchema,
1668
+ args
1669
+ );
1670
+ return ctx.workspace.captureThumbnail(projectPath);
1671
+ }).handle(ProjectChannel.GetThumbnail, (_, ...args) => {
1672
+ const [projectPath] = validate(
1673
+ ProjectChannel.GetThumbnail,
1674
+ ProjectGetThumbnailSchema,
1675
+ args
1676
+ );
1677
+ return ctx.workspace.getThumbnail(projectPath);
1599
1678
  });
1600
1679
  }
1601
1680
  var sessionModule = {
@@ -1603,7 +1682,7 @@ var sessionModule = {
1603
1682
  };
1604
1683
 
1605
1684
  // src/main/ipc/settings.ts
1606
- import { app as app7 } from "electron";
1685
+ import { app as app8 } from "electron";
1607
1686
  function registerSettingsIpc(ctx) {
1608
1687
  return new IpcRegistry(ctx.senderPolicy).handle(WorkbenchSettingsChannel.Get, () => {
1609
1688
  return loadWorkbenchSettings();
@@ -1625,8 +1704,8 @@ function registerSettingsIpc(ctx) {
1625
1704
  applyTheme(theme);
1626
1705
  }).handle(WorkbenchSettingsChannel.GetCdpStatus, () => {
1627
1706
  const settings = loadWorkbenchSettings();
1628
- const switchValue = app7.commandLine.getSwitchValue("remote-debugging-port");
1629
- const implicitDevDefault = !app7.isPackaged && !settings.cdp.enabled && switchValue === "9222";
1707
+ const switchValue = app8.commandLine.getSwitchValue("remote-debugging-port");
1708
+ const implicitDevDefault = !app8.isPackaged && !settings.cdp.enabled && switchValue === "9222";
1630
1709
  return {
1631
1710
  configured: settings.cdp.enabled,
1632
1711
  port: settings.cdp.port,
@@ -1710,11 +1789,11 @@ var simulatorModule = {
1710
1789
  import { WebSocketServer } from "ws";
1711
1790
 
1712
1791
  // src/main/services/automation/exec.ts
1713
- import { webContents as webContents3 } from "electron";
1792
+ import { webContents as webContents4 } from "electron";
1714
1793
  function getSimulator(ctx) {
1715
1794
  const simWcId = ctx.views.getSimulatorWebContentsId();
1716
1795
  if (!ctx.workspace.hasActiveSession() || simWcId == null) return null;
1717
- const sim = webContents3.fromId(simWcId);
1796
+ const sim = webContents4.fromId(simWcId);
1718
1797
  if (!sim || sim.isDestroyed()) return null;
1719
1798
  return sim;
1720
1799
  }
@@ -2880,7 +2959,7 @@ function startMcpServer(resolvedCdpPort, mcpPort) {
2880
2959
  }
2881
2960
 
2882
2961
  // src/main/services/simulator-storage/index.ts
2883
- import { app as app8, webContents as wcStatic } from "electron";
2962
+ import { app as app9, webContents as wcStatic } from "electron";
2884
2963
  function isSimulatorWebview(wc) {
2885
2964
  if (wc.isDestroyed()) return false;
2886
2965
  if (wc.getType() !== "webview") return false;
@@ -3130,9 +3209,9 @@ function setupSimulatorStorage(host, options) {
3130
3209
  wcSubs.delete(wc);
3131
3210
  });
3132
3211
  };
3133
- app8.on("web-contents-created", onWcCreated);
3212
+ app9.on("web-contents-created", onWcCreated);
3134
3213
  registry.add(() => {
3135
- app8.removeListener("web-contents-created", onWcCreated);
3214
+ app9.removeListener("web-contents-created", onWcCreated);
3136
3215
  });
3137
3216
  registry.add(async () => {
3138
3217
  const subs = Array.from(wcSubs.values());
@@ -3193,7 +3272,7 @@ async function clearElementInspection() {
3193
3272
  }
3194
3273
 
3195
3274
  // src/main/services/update/update-manager.ts
3196
- import { app as app9, shell as shell3 } from "electron";
3275
+ import { app as app10, shell as shell3 } from "electron";
3197
3276
  var UpdateManager = class {
3198
3277
  checker;
3199
3278
  mainWindow;
@@ -3206,7 +3285,7 @@ var UpdateManager = class {
3206
3285
  constructor(opts) {
3207
3286
  this.checker = opts.checker;
3208
3287
  this.mainWindow = opts.mainWindow;
3209
- this.getCurrentVersion = opts.getCurrentVersion ?? (() => app9.getVersion());
3288
+ this.getCurrentVersion = opts.getCurrentVersion ?? (() => app10.getVersion());
3210
3289
  this.ipc = new IpcRegistry(opts.senderPolicy);
3211
3290
  this.registerIpc();
3212
3291
  this.startPeriodicCheck(opts.checkInterval ?? 60 * 60 * 1e3, opts.initialDelay ?? 5e3);
@@ -3247,7 +3326,7 @@ var UpdateManager = class {
3247
3326
  install() {
3248
3327
  if (this.downloadedPath) {
3249
3328
  shell3.openPath(this.downloadedPath);
3250
- app9.quit();
3329
+ app10.quit();
3251
3330
  }
3252
3331
  }
3253
3332
  /**
@@ -3280,7 +3359,7 @@ var UpdateManager = class {
3280
3359
  };
3281
3360
 
3282
3361
  // src/main/services/update/github-release-checker.ts
3283
- import { app as app10 } from "electron";
3362
+ import { app as app11 } from "electron";
3284
3363
 
3285
3364
  // src/main/app/app.ts
3286
3365
  var DEFAULT_MODULES = {
@@ -3329,7 +3408,7 @@ async function disposeContext(ctx) {
3329
3408
  function createConfiguredMainWindow(config, rendererDir2) {
3330
3409
  const mainWindow = createMainWindow({
3331
3410
  title: config.appName ?? "Dimina DevTools",
3332
- indexHtml: path8.join(rendererDir2, "entries/main/index.html"),
3411
+ indexHtml: path9.join(rendererDir2, "entries/main/index.html"),
3333
3412
  width: config.window?.width,
3334
3413
  height: config.window?.height,
3335
3414
  minWidth: config.window?.minWidth,
@@ -3384,7 +3463,7 @@ async function setupAutomation(instance) {
3384
3463
  function setupMcp() {
3385
3464
  const settings = loadWorkbenchSettings();
3386
3465
  if (!settings.mcp.enabled) return null;
3387
- const cdpPortSwitch = app11.commandLine.getSwitchValue("remote-debugging-port");
3466
+ const cdpPortSwitch = app12.commandLine.getSwitchValue("remote-debugging-port");
3388
3467
  const cdpPort2 = cdpPortSwitch ? parseInt(cdpPortSwitch, 10) : settings.cdp.port;
3389
3468
  return startMcpServer(cdpPort2, settings.mcp.port);
3390
3469
  }
@@ -3405,12 +3484,12 @@ function wireAppWindowEvents(config, instance) {
3405
3484
  });
3406
3485
  }
3407
3486
  function enableDevRendererAutoReload(rendererDir2) {
3408
- if (app11.isPackaged) {
3487
+ if (app12.isPackaged) {
3409
3488
  return toDisposable(() => {
3410
3489
  });
3411
3490
  }
3412
3491
  let reloadTimer = null;
3413
- const watcher = fs5.watch(rendererDir2, { recursive: true }, () => {
3492
+ const watcher = fs6.watch(rendererDir2, { recursive: true }, () => {
3414
3493
  if (reloadTimer) clearTimeout(reloadTimer);
3415
3494
  reloadTimer = setTimeout(() => {
3416
3495
  for (const win of BrowserWindow3.getAllWindows()) {
@@ -3432,7 +3511,7 @@ function createWorkbenchApp(config = {}) {
3432
3511
  setupCdpPort();
3433
3512
  async function setup() {
3434
3513
  if (setupPromise) return setupPromise;
3435
- setupPromise = app11.whenReady().then(async () => {
3514
+ setupPromise = app12.whenReady().then(async () => {
3436
3515
  applyTheme(loadWorkbenchSettings().theme);
3437
3516
  const rendererDir2 = config.rendererDir ?? rendererDir;
3438
3517
  const mainWindow = createConfiguredMainWindow(config, rendererDir2);
@@ -3496,7 +3575,7 @@ function createWorkbenchApp(config = {}) {
3496
3575
 
3497
3576
  // src/main/windows/settings-window/create.ts
3498
3577
  import { BrowserWindow as BrowserWindow4 } from "electron";
3499
- import path9 from "path";
3578
+ import path10 from "path";
3500
3579
  async function createSettingsWindow(parent, rendererDir2) {
3501
3580
  const win = new BrowserWindow4({
3502
3581
  width: 420,
@@ -3514,7 +3593,7 @@ async function createSettingsWindow(parent, rendererDir2) {
3514
3593
  }
3515
3594
  });
3516
3595
  applyNavigationHardening(win.webContents, rendererDir2);
3517
- await win.loadFile(path9.join(rendererDir2, "entries/workbench-settings/index.html"));
3596
+ await win.loadFile(path10.join(rendererDir2, "entries/workbench-settings/index.html"));
3518
3597
  return win;
3519
3598
  }
3520
3599
 
@@ -1,5 +1,5 @@
1
1
  import { ProjectChannel } from '../../shared/ipc-channels.js';
2
- import { ProjectGetCompileConfigSchema, ProjectGetPagesSchema, ProjectOpenSchema, ProjectSaveCompileConfigSchema, } from '../../shared/ipc-schemas.js';
2
+ import { ProjectCaptureThumbnailSchema, ProjectGetCompileConfigSchema, ProjectGetPagesSchema, ProjectGetThumbnailSchema, ProjectOpenSchema, ProjectSaveCompileConfigSchema, } from '../../shared/ipc-schemas.js';
3
3
  import { validate } from '../utils/ipc-schema.js';
4
4
  import { IpcRegistry } from '../utils/ipc-registry.js';
5
5
  export function registerSessionIpc(ctx) {
@@ -22,6 +22,14 @@ export function registerSessionIpc(ctx) {
22
22
  })
23
23
  .handle(ProjectChannel.Close, () => {
24
24
  return ctx.workspace.closeProject();
25
+ })
26
+ .handle(ProjectChannel.CaptureThumbnail, (_, ...args) => {
27
+ const [projectPath] = validate(ProjectChannel.CaptureThumbnail, ProjectCaptureThumbnailSchema, args);
28
+ return ctx.workspace.captureThumbnail(projectPath);
29
+ })
30
+ .handle(ProjectChannel.GetThumbnail, (_, ...args) => {
31
+ const [projectPath] = validate(ProjectChannel.GetThumbnail, ProjectGetThumbnailSchema, args);
32
+ return ctx.workspace.getThumbnail(projectPath);
25
33
  });
26
34
  }
27
35
  export const sessionModule = {
@@ -23,6 +23,17 @@ export function installAppMenu(ctx) {
23
23
  { role: 'quit' },
24
24
  ],
25
25
  },
26
+ {
27
+ label: '项目',
28
+ submenu: [
29
+ {
30
+ label: '打开项目',
31
+ click: () => {
32
+ ctx.notify.windowNavigateBack();
33
+ },
34
+ },
35
+ ],
36
+ },
26
37
  {
27
38
  label: '编辑',
28
39
  submenu: [
@@ -0,0 +1,4 @@
1
+ import { type NativeImage } from 'electron';
2
+ export declare function saveThumbnail(projectPath: string, image: NativeImage): string | null;
3
+ export declare function loadThumbnail(projectPath: string): string | null;
4
+ //# sourceMappingURL=thumbnail.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { app } from 'electron';
5
+ function getThumbnailDir() {
6
+ return path.join(app.getPath('userData'), 'thumbnails');
7
+ }
8
+ function hashProjectPath(projectPath) {
9
+ return createHash('sha256').update(projectPath).digest('hex').slice(0, 16);
10
+ }
11
+ function getThumbnailPath(projectPath) {
12
+ return path.join(getThumbnailDir(), `${hashProjectPath(projectPath)}.png`);
13
+ }
14
+ export function saveThumbnail(projectPath, image) {
15
+ const png = image.toPNG();
16
+ const dir = getThumbnailDir();
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ const filePath = getThumbnailPath(projectPath);
19
+ fs.writeFileSync(filePath, png);
20
+ return `data:image/png;base64,${png.toString('base64')}`;
21
+ }
22
+ export function loadThumbnail(projectPath) {
23
+ const filePath = getThumbnailPath(projectPath);
24
+ try {
25
+ const buf = fs.readFileSync(filePath);
26
+ return `data:image/png;base64,${buf.toString('base64')}`;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ //# sourceMappingURL=thumbnail.js.map
@@ -39,6 +39,8 @@ export interface WorkspaceService {
39
39
  } | null;
40
40
  getProjectPath(): string;
41
41
  hasActiveSession(): boolean;
42
+ captureThumbnail(projectPath: string): Promise<string | null>;
43
+ getThumbnail(projectPath: string): string | null;
42
44
  getProjectPages(projectPath: string): ProjectPages;
43
45
  getCompileConfig(projectPath: string): CompileConfig;
44
46
  saveCompileConfig(projectPath: string, config: CompileConfig): void;
@@ -1,6 +1,8 @@
1
+ import { webContents } from 'electron';
1
2
  import * as repo from '../projects/project-repository.js';
2
3
  import { clearSimulatorServicewechatReferer, setSimulatorServicewechatReferer, } from '../simulator/referer.js';
3
4
  import { loadWorkbenchSettings } from '../settings/index.js';
5
+ import { saveThumbnail, loadThumbnail } from '../projects/thumbnail.js';
4
6
  /** Build a workspace service bound to the given workbench context. */
5
7
  export function createWorkspaceService(ctx) {
6
8
  let currentSession = null;
@@ -96,6 +98,24 @@ export function createWorkspaceService(ctx) {
96
98
  getSession: () => currentSession,
97
99
  getProjectPath: () => currentProjectPath,
98
100
  hasActiveSession: () => currentSession !== null,
101
+ async captureThumbnail(projectPath) {
102
+ const simWcId = ctx.views.getSimulatorWebContentsId();
103
+ if (!simWcId)
104
+ return null;
105
+ const wc = webContents.fromId(simWcId);
106
+ if (!wc || wc.isDestroyed())
107
+ return null;
108
+ try {
109
+ const image = await wc.capturePage();
110
+ return saveThumbnail(projectPath, image);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ },
116
+ getThumbnail(projectPath) {
117
+ return loadThumbnail(projectPath);
118
+ },
99
119
  getProjectPages: (projectPath) => repo.getProjectPages(projectPath),
100
120
  getCompileConfig: (projectPath) => repo.getCompileConfig(projectPath),
101
121
  saveCompileConfig: (projectPath, config) => repo.saveCompileConfig(projectPath, config),
@@ -10,11 +10,13 @@ import type { SenderPolicy } from './ipc-registry.js';
10
10
  * - the settings overlay view (when open)
11
11
  * - the popover overlay view (when open)
12
12
  *
13
- * The simulator webview is intentionally NOT on this list: its preload
14
- * (`src/preload/windows/simulator.ts`) uses `ipcRenderer.sendToHost`
15
- * exclusively, never `invoke`/`send`, so it can't legitimately reach an
16
- * ipcMain handler. Including it would only widen the attack surface if
17
- * the guest were ever compromised.
13
+ * The simulator webview is intentionally NOT on this list. Anything it
14
+ * needs from main (currently just the custom-apis bridge — see
15
+ * `installCustomApisBridge` in `src/preload/runtime/custom-apis.ts` and the
16
+ * matching `useCustomApiProxy` host hook) proxies through the trusted
17
+ * main-window renderer via `ipcRenderer.sendToHost` + `<webview>.send`.
18
+ * Keeping the guest off this list contains the blast radius if the
19
+ * simulator content is ever compromised.
18
20
  *
19
21
  * Any other sender — including a stale/destroyed sender or an unknown
20
22
  * iframe — is rejected.
@@ -8,11 +8,13 @@
8
8
  * - the settings overlay view (when open)
9
9
  * - the popover overlay view (when open)
10
10
  *
11
- * The simulator webview is intentionally NOT on this list: its preload
12
- * (`src/preload/windows/simulator.ts`) uses `ipcRenderer.sendToHost`
13
- * exclusively, never `invoke`/`send`, so it can't legitimately reach an
14
- * ipcMain handler. Including it would only widen the attack surface if
15
- * the guest were ever compromised.
11
+ * The simulator webview is intentionally NOT on this list. Anything it
12
+ * needs from main (currently just the custom-apis bridge — see
13
+ * `installCustomApisBridge` in `src/preload/runtime/custom-apis.ts` and the
14
+ * matching `useCustomApiProxy` host hook) proxies through the trusted
15
+ * main-window renderer via `ipcRenderer.sendToHost` + `<webview>.send`.
16
+ * Keeping the guest off this list contains the blast radius if the
17
+ * simulator content is ever compromised.
16
18
  *
17
19
  * Any other sender — including a stale/destroyed sender or an unknown
18
20
  * iframe — is rejected.
@@ -35,7 +37,7 @@ export function createWorkbenchSenderPolicy(ctx) {
35
37
  const popoverViewId = ctx.views.getPopoverWebContentsId();
36
38
  if (popoverViewId != null && sender.id === popoverViewId)
37
39
  return true;
38
- // simulator preload uses sendToHost only; never reaches ipcMain handlers.
40
+ // simulator <webview> proxies through main-window renderer (see file header).
39
41
  return false;
40
42
  };
41
43
  }
@@ -1,9 +1,38 @@
1
1
  import { contextBridge, ipcRenderer } from 'electron';
2
- import { SimulatorCustomApiChannel } from '../../shared/ipc-channels.js';
2
+ import { SimulatorCustomApiBridgeChannel } from '../../shared/ipc-channels.js';
3
+ // The simulator <webview> is intentionally kept off the workbench sender-policy
4
+ // white-list, so it cannot reach `ipcMain.handle` directly. Instead the bridge
5
+ // asks the trusted main-window renderer to proxy the call: webview sends via
6
+ // `ipcRenderer.sendToHost`, host does the `ipcInvoke`, and posts the result
7
+ // back through `<webview>.send`. Requests and responses are correlated by id
8
+ // so concurrent invokes do not tangle.
3
9
  function buildBridge() {
10
+ let nextId = 1;
11
+ const pending = new Map();
12
+ ipcRenderer.on(SimulatorCustomApiBridgeChannel.Response, (_event, payload) => {
13
+ const entry = pending.get(payload.id);
14
+ if (!entry)
15
+ return;
16
+ pending.delete(payload.id);
17
+ if ('error' in payload) {
18
+ entry.reject(new Error(payload.error));
19
+ }
20
+ else {
21
+ entry.resolve(payload.result);
22
+ }
23
+ });
24
+ const send = (req) => {
25
+ return new Promise((resolve, reject) => {
26
+ pending.set(req.id, {
27
+ resolve: (value) => resolve(value),
28
+ reject,
29
+ });
30
+ ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
31
+ });
32
+ };
4
33
  return {
5
- list: () => ipcRenderer.invoke(SimulatorCustomApiChannel.List),
6
- invoke: (name, params) => ipcRenderer.invoke(SimulatorCustomApiChannel.Invoke, name, params),
34
+ list: () => send({ id: nextId++, op: 'list' }),
35
+ invoke: (name, params) => send({ id: nextId++, op: 'invoke', name, params }),
7
36
  };
8
37
  }
9
38
  export function installCustomApisBridge() {
@@ -187,9 +187,9 @@ var SimulatorChannel = {
187
187
  AppData: "simulator:appdata",
188
188
  AppDataAll: "simulator:appdata-all"
189
189
  };
190
- var SimulatorCustomApiChannel = {
191
- List: "simulator:custom-apis:list",
192
- Invoke: "simulator:custom-apis:invoke"
190
+ var SimulatorCustomApiBridgeChannel = {
191
+ Request: "simulator:custom-apis:bridge-request",
192
+ Response: "simulator:custom-apis:bridge-response"
193
193
  };
194
194
  var BridgeChannel = {
195
195
  WxmlRefreshRequest: "wxml:refresh:request",
@@ -199,9 +199,30 @@ var BridgeChannel = {
199
199
 
200
200
  // src/preload/runtime/custom-apis.ts
201
201
  function buildBridge() {
202
+ let nextId = 1;
203
+ const pending = /* @__PURE__ */ new Map();
204
+ import_electron2.ipcRenderer.on(SimulatorCustomApiBridgeChannel.Response, (_event, payload) => {
205
+ const entry = pending.get(payload.id);
206
+ if (!entry) return;
207
+ pending.delete(payload.id);
208
+ if ("error" in payload) {
209
+ entry.reject(new Error(payload.error));
210
+ } else {
211
+ entry.resolve(payload.result);
212
+ }
213
+ });
214
+ const send = (req) => {
215
+ return new Promise((resolve, reject) => {
216
+ pending.set(req.id, {
217
+ resolve: (value) => resolve(value),
218
+ reject
219
+ });
220
+ import_electron2.ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
221
+ });
222
+ };
202
223
  return {
203
- list: () => import_electron2.ipcRenderer.invoke(SimulatorCustomApiChannel.List),
204
- invoke: (name, params) => import_electron2.ipcRenderer.invoke(SimulatorCustomApiChannel.Invoke, name, params)
224
+ list: () => send({ id: nextId++, op: "list" }),
225
+ invoke: (name, params) => send({ id: nextId++, op: "invoke", name, params })
205
226
  };
206
227
  }
207
228
  function installCustomApisBridge() {