@dimina-kit/devtools 0.3.1-dev.20260515105504 → 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 (34) hide show
  1. package/README.md +84 -64
  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/index.d.ts +2 -0
  12. package/dist/preload/index.js +1 -0
  13. package/dist/preload/runtime/custom-apis.js +32 -3
  14. package/dist/preload/windows/simulator.js +26 -5
  15. package/dist/renderer/assets/index-BFdItKBK.js +46 -0
  16. package/dist/renderer/assets/{input-C5M5Wxn6.js → input-AH9UDYmj.js} +2 -2
  17. package/dist/renderer/assets/{ipc-transport-CUsl-fS2.js → ipc-transport-DNgBM6Qz.js} +2 -2
  18. package/dist/renderer/assets/ipc-transport-Dvmd5JZj.css +1 -0
  19. package/dist/renderer/assets/{popover-BSBPVO0o.js → popover-D1hoVYiY.js} +2 -2
  20. package/dist/renderer/assets/{select-B1tF3UAc.js → select-BgDHRj6r.js} +2 -2
  21. package/dist/renderer/assets/{settings-api-BjJalghB.js → settings-api-DBKlNKV3.js} +2 -2
  22. package/dist/renderer/assets/{settings-7w54H6p-.js → settings-m-tcmZy_.js} +2 -2
  23. package/dist/renderer/assets/{workbenchSettings-q2_-nc8t.js → workbenchSettings-CGacu-pI.js} +2 -2
  24. package/dist/renderer/entries/main/index.html +5 -5
  25. package/dist/renderer/entries/popover/index.html +5 -5
  26. package/dist/renderer/entries/settings/index.html +5 -5
  27. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  28. package/dist/shared/ipc-channels.d.ts +6 -0
  29. package/dist/shared/ipc-channels.js +19 -0
  30. package/dist/shared/ipc-schemas.d.ts +4 -0
  31. package/dist/shared/ipc-schemas.js +4 -0
  32. package/package.json +4 -4
  33. package/dist/renderer/assets/index-DW_0thr-.js +0 -46
  34. package/dist/renderer/assets/ipc-transport-CltRuvhi.css +0 -1
package/README.md CHANGED
@@ -83,6 +83,7 @@ import { app, globalShortcut } from 'electron'
83
83
  import path from 'path'
84
84
  import { createMainWindow } from '@dimina-kit/devtools/create-window'
85
85
  import { createWorkbenchContext } from '@dimina-kit/devtools/context'
86
+ import { registerAppIpc } from '@dimina-kit/devtools'
86
87
  import { registerSimulatorIpc } from '@dimina-kit/devtools/ipc-simulator'
87
88
  import { registerPanelsIpc } from '@dimina-kit/devtools/ipc-panels'
88
89
  import { registerToolbarIpc } from '@dimina-kit/devtools/ipc-toolbar'
@@ -95,7 +96,7 @@ import { rendererDir, defaultPreloadPath } from '@dimina-kit/devtools/paths'
95
96
  app.whenReady().then(() => {
96
97
  const mainWindow = createMainWindow({
97
98
  title: 'My DevTools',
98
- indexHtml: path.join(rendererDir, 'index.html'),
99
+ indexHtml: path.join(rendererDir, 'entries/main/index.html'),
99
100
  })
100
101
 
101
102
  const ctx = createWorkbenchContext({
@@ -105,6 +106,9 @@ app.whenReady().then(() => {
105
106
  rendererDir,
106
107
  })
107
108
 
109
+ // registerAppIpc 必装:renderer 启动时调用 app:getPreloadPath / app:getBranding,
110
+ // 缺它会卡在加载阶段。
111
+ registerAppIpc(ctx)
108
112
  registerProjectsIpc(ctx)
109
113
  registerSessionIpc(ctx)
110
114
  registerSimulatorIpc(ctx)
@@ -112,6 +116,11 @@ app.whenReady().then(() => {
112
116
  registerToolbarIpc(ctx)
113
117
  registerPopoverIpc(ctx)
114
118
 
119
+ // Storage / 元素选区面板的后端 handler 由 setupSimulatorStorage 单独装。
120
+ // 该函数当前**未列入稳定公共 API**(在 `@dimina-kit/devtools` 之外)。
121
+ // 若需要这两个面板,建议改用上方的 `createWorkbenchApp({ onSetup })` 路线,
122
+ // 框架会自动注册;模块组装路线下你只能用不依赖它们的精简面板集。
123
+
115
124
  // 添加自定义 IPC — 通过 ctx.workspace 访问会话状态
116
125
  ipcMain.handle('my:action', () => {
117
126
  console.log('current project:', ctx.workspace.getProjectPath())
@@ -197,19 +206,20 @@ src/
197
206
  types.ts
198
207
  instrumentation/ # 注入到小程序 runtime 的探针
199
208
  console.ts # 控制台拦截
200
- storage.ts # localStorage 拦截
201
209
  app-data.ts # Worker setData 拦截
202
- wxml.ts # Vue 组件树提取
210
+ wxml.ts # Vue 组件树提取 + WXML 高亮 overlay
211
+ disposable.ts # instrumentation 共用的清理工具
203
212
  runtime/
204
- bridge.ts # installSimulatorBridge
205
- host.ts
213
+ bridge.ts # installSimulatorBridge → window.__simulatorData
214
+ custom-apis.ts # installCustomApisBridge → window.__diminaCustomApis
215
+ host.ts # sendToHost 推送封装
206
216
 
207
217
  renderer/
208
218
  entries/ # 各窗口 HTML + React 挂载点
209
219
  main/ popover/ settings/ workbench-settings/
210
220
  modules/ # 先按窗口分,再按区域
211
221
  main/
212
- app/ # 主窗口 App
222
+ main.tsx # 主窗口 React
213
223
  features/ # 主窗口内的业务区域
214
224
  project-runtime/ # 项目视图 + 工具栏 + 右侧面板切换
215
225
  right-panel/ # WXML / AppData / Storage 面板
@@ -261,6 +271,8 @@ src/
261
271
  | `onSetup` | `(instance) => void` | — | 窗口和 context 创建后的回调,用于注册自定义 IPC |
262
272
  | `onBeforeClose` | `(instance) => void` | — | 窗口关闭前的回调,session 关闭由框架自动处理 |
263
273
  | `window` | `WorkbenchWindowConfig` | — | 窗口尺寸覆盖 |
274
+ | `updateChecker` | `UpdateChecker` | — | 自定义更新检查器;提供后启用"检查更新"功能 |
275
+ | `updateOptions` | `{ checkInterval?, initialDelay?, getCurrentVersion? }` | — | 仅当 `updateChecker` 提供时生效,默认 1h / 5s |
264
276
 
265
277
  ### 内置面板 ID
266
278
 
@@ -298,37 +310,56 @@ const myAdapter: CompilationAdapter = {
298
310
 
299
311
  ## Preload Instrumentation
300
312
 
301
- 模拟器 webview 注入 preload 脚本在全局安装 instrumentation。内置 preload(`preload/windows/simulator.ts`)组装了以下模块:
313
+ 模拟器 webview 注入 preload 脚本,在 webview 全局上安装两类东西:**桥接**(把数据通道暴露到 webview)+ **instrumentation**(监听运行时事件并写入桥接)。内置 preload 见 `src/preload/windows/simulator.ts`。
314
+
315
+ ### 必装:桥接
316
+
317
+ | 函数 | 作用 | 不装会怎样 |
318
+ | --- | --- | --- |
319
+ | `installSimulatorBridge()` | 暴露 `window.__simulatorData`,承载 `highlightElement` / `unhighlightElement` / `getAppdata` / `getWxml` / `getStorageSnapshot` 等只读 API(供主进程 `executeJavaScript` 拉取) | 元素选区 overlay 不显示;automation `Page.getData` 返回空对象;MCP `simulator_get_overview` 在 hints 中追加 `simulator bridge not ready (window.__simulatorData missing)`。注:Storage 面板走主进程 CDP,不受影响;Console/AppData/WXML 推送走 `sendToHost`,也不受影响 |
320
+ | `installCustomApisBridge()` | 暴露 `window.__diminaCustomApis`,承载 `list` / `invoke`,让模拟器侧把主进程注册的 API 代理回小程序 runtime | `registerSimulatorApi` 注册的业务 API 在小程序里调用全部 no-op(参见下方 "Simulator 自定义 API") |
321
+ | `setupApiCompatHook()` | 兼容上游 API 名称变更 | 部分 API 调用兼容性问题 |
322
+
323
+ 下游写自定义 preload 时,这三项原则上都要装。若不需要 `registerSimulatorApi`,`installCustomApisBridge` 可省略。
302
324
 
303
- | Instrumentation | 捕获内容 |
304
- | ----------------------------------------- | ----------------------------------------------- |
305
- | Console (`installConsoleInstrumentation`) | `console.*`、`window.onerror`、未捕获 rejection |
306
- | Storage (`installStorageInstrumentation`) | `localStorage.setItem/removeItem`、全量快照 |
307
- | AppData (`installAppDataInstrumentation`) | Worker `type='u'` 消息、setData 调用 |
308
- | WXML (`installWxmlInstrumentation`) | Vue 组件树提取、MutationObserver 自动更新 |
325
+ ### 按需:instrumentation
309
326
 
310
- 所有 install\* 函数从 `@dimina-kit/devtools/preload` 统一导出,调用后通过 `installSimulatorBridge` 暴露的内部桥接通道把数据推到宿主。面板打开时调用 `panel:eval` 通过 `executeJavaScript` 拉取当前状态。
327
+ | Instrumentation | 捕获内容 | 数据通道 |
328
+ | --- | --- | --- |
329
+ | `installConsoleInstrumentation` | `console.*`、`window.onerror`、未捕获 rejection | IPC sendToHost |
330
+ | `installAppDataInstrumentation` | Worker `type='u'` 消息、setData 调用 | IPC sendToHost + 写入 bridge state |
331
+ | `installWxmlInstrumentation` | Vue 组件树提取、MutationObserver 自动更新 | IPC sendToHost + 写入 bridge state |
332
+
333
+ > Storage 面板的数据由主进程通过 CDP `DOMStorage` 域抓取(`src/main/services/simulator-storage`),**不需要 preload 侧 instrumentation**。
334
+
335
+ 各面板的数据流详见下方 "数据流" 章节。
311
336
 
312
337
  ### 自定义 Preload
313
338
 
314
- 按需组合或追加自定义逻辑(`src/preload/windows/simulator.ts` 作为参考):
339
+ 参考 `src/preload/windows/simulator.ts`:
315
340
 
316
341
  ```typescript
317
342
  // my-preload.ts
318
343
  import {
319
344
  installSimulatorBridge,
345
+ installCustomApisBridge,
320
346
  installConsoleInstrumentation,
321
- installStorageInstrumentation,
347
+ installAppDataInstrumentation,
348
+ installWxmlInstrumentation,
322
349
  setupApiCompatHook,
323
350
  } from '@dimina-kit/devtools/preload'
324
- // 不需要 WXML/AppData,不导入
325
351
 
352
+ // 1. 兼容 hook + 桥接(必装,且应在 instrumentation 之前)
326
353
  setupApiCompatHook()
327
354
  installSimulatorBridge()
355
+ installCustomApisBridge() // 若用到 registerSimulatorApi,必装
356
+
357
+ // 2. instrumentation(按需)
328
358
  installConsoleInstrumentation()
329
- installStorageInstrumentation()
359
+ installAppDataInstrumentation()
360
+ installWxmlInstrumentation()
330
361
 
331
- // 自定义 hook
362
+ // 3. 自定义 hook
332
363
  window.addEventListener('error', (e) => {
333
364
  console.error('[my-preload]', e.message)
334
365
  })
@@ -346,26 +377,39 @@ launch({ preloadPath: '/absolute/path/to/my-preload.js' })
346
377
 
347
378
  下游可以通过 `registerSimulatorApi` 把业务 API 注入到模拟器内运行的小程序。Handler 在 Electron **主进程**执行,能用任何 Node 能力(fs、net、原生模块、持久状态);模拟器侧拿到 `wx.<name>(params)` 调用后通过 IPC 转发到主进程,await 返回结果,handler 抛错则原型回传给小程序。
348
379
 
380
+ > ⚠️ 使用本功能要求模拟器 preload 调用 `installCustomApisBridge()`(详见上方 "Preload Instrumentation")。内置 preload 已包含此调用;若你用了自定义 preload 又忘记调用,注册的 API 在小程序运行时**没有任何反应**,且不会报错。
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
+
349
392
  ```typescript
350
393
  import { registerSimulatorApi } from '@dimina-kit/devtools/simulator-apis'
351
394
 
352
- const dispose = registerSimulatorApi('myCompany.login', async ({ username }) => {
395
+ // 提供 wx.login 的实现(dimina 容器默认未实现,注册后才能用)
396
+ const dispose = registerSimulatorApi('login', async ({ success }) => {
353
397
  // 主进程上下文:可调用内部 HTTP、读取凭证文件、访问原生模块
354
- return await callInternalAuth(username)
398
+ const code = await callInternalAuth()
399
+ return { code }
355
400
  })
356
401
 
357
402
  // 不再需要时取消注册(不传也行,进程退出会一起清理)
358
403
  dispose()
359
404
  ```
360
405
 
361
- 几点行为约定:
406
+ ### 行为约定
362
407
 
408
+ - **命中顺序**:小程序运行时按 `apiRegistry[name] → MiniApp 实例方法 → 扩展模块兜底` 查找,所以 `registerSimulatorApi` 注册的 name **优先于内置实现**——既能给"未实现"的 wx.* 补功能(如 `wx.login`),也能覆盖默认的 wx.* 行为(如 `wx.request`,dimina-kit 本身就在用这条路把 `request` 接到主进程 fetch)。
363
409
  - 同名重复注册:后注册的覆盖前者,静默替换。
364
410
  - `dispose()` 只会移除自己创建的那次注册——如果之后被别人覆盖了,旧的 disposer 是 no-op。
365
- - 调用未注册的 name:小程序侧拿到 reject,错误信息里带 name。
366
- - Handler 可同步或异步返回;返回值需可 JSON 序列化(IPC 限制)。
367
-
368
- 对小程序侧来说,这些 name 和内置 API(`wx.getStorage` 等)走同一个 `AppManager.apiRegistry`,没有区别。
411
+ - 调用未注册且容器内也没有的 name:小程序侧拿到 reject,错误信息里带 name。
412
+ - Handler 可同步或异步返回;**返回值需可 JSON 序列化**(IPC 限制)。函数、Symbol、Map/Set、循环引用都会丢失。
369
413
 
370
414
  ---
371
415
 
@@ -392,12 +436,14 @@ dispose()
392
436
  getRendererDir, getPreloadDir, getRendererHtml
393
437
  @dimina-kit/devtools/simulator-apis registerSimulatorApi(name, handler) →
394
438
  注入由主进程托管的小程序 API(详见上方"Simulator 自定义 API")
395
- @dimina-kit/devtools/preload installConsoleInstrumentation,
396
- installStorageInstrumentation,
439
+ @dimina-kit/devtools/preload installSimulatorBridge,
440
+ installCustomApisBridge,
441
+ installConsoleInstrumentation,
397
442
  installAppDataInstrumentation, sendAllAppData,
398
443
  installWxmlInstrumentation, sendWxmlTree,
399
- setupWxmlObserver, installSimulatorBridge,
444
+ setupWxmlObserver,
400
445
  setupApiCompatHook
446
+ (storage 面板数据由主进程 CDP 抓取,无 preload 侧 hook)
401
447
  ```
402
448
 
403
449
  ### Experimental exports (v0.x — signatures may change in minor versions)
@@ -414,18 +460,10 @@ dispose()
414
460
  @dimina-kit/devtools/ipc-popover registerPopoverIpc(ctx)
415
461
  @dimina-kit/devtools/ipc-settings registerSettingsIpc(ctx)
416
462
  @dimina-kit/devtools/ipc-projects registerProjectsIpc(ctx)
417
- @dimina-kit/devtools/ipc-session registerSessionIpc(ctx), sendStatus(ctx, status, msg)
463
+ @dimina-kit/devtools/ipc-session registerSessionIpc(ctx), sessionModule
418
464
  @dimina-kit/devtools/workbench-settings loadWorkbenchSettings(), saveWorkbenchSettings(), applyTheme()
419
465
  ```
420
466
 
421
- > 注:以下子路径在过去版本曾被导出,已收敛到根 barrel 或内部实现,请改从 `@dimina-kit/devtools` 根入口导入(`createViewManager`、`registerAppIpc`、`simulatorDir`、`Project` 等类型):
422
- >
423
- > - `@dimina-kit/devtools/view-manager` → `import { createViewManager, type ViewManager } from '@dimina-kit/devtools'`
424
- > - `@dimina-kit/devtools/ipc-app` → `import { registerAppIpc } from '@dimina-kit/devtools'`
425
- > - `@dimina-kit/devtools/projects` → 通过 `ctx.workspace.listProjects()` 等服务方法访问;`Project` 类型从 `@dimina-kit/devtools` 根入口导入
426
- > - `@dimina-kit/devtools/layout` → `import { setHeaderHeight } from '@dimina-kit/devtools'`(其余布局细节回归内部实现)
427
- > - `@dimina-kit/devtools/simulator-dir` → `import { simulatorDir } from '@dimina-kit/devtools/paths'`
428
-
429
467
  ---
430
468
 
431
469
  ## WorkbenchContext
@@ -452,15 +490,10 @@ interface WorkbenchContext {
452
490
  // ── Internal lifecycle / security (host 不直接消费) ──
453
491
  registry: DisposableRegistry // 框架在创建时统一聚合 dispose
454
492
  senderPolicy: SenderPolicy // IpcRegistry 自动校验 sender 白名单
455
-
456
- /** @deprecated 用 ctx.windows.mainWindow */
457
- mainWindow: BrowserWindow
458
- /** @deprecated 用 ctx.windows.settingsWindow / setSettingsWindow */
459
- workbenchSettingsWindow: BrowserWindow | null
460
493
  }
461
494
  ```
462
495
 
463
- 会话和视图状态不再作为 ctx 的直接字段暴露,而是封装在对应 service 的私有闭包中:
496
+ 会话和视图状态封装在对应 service 的私有闭包中,通过方法访问:
464
497
 
465
498
  | 旧写法 | 新写法 |
466
499
  | ---------------------------- | ----------------------------------------------------------------- |
@@ -500,28 +533,15 @@ flowchart TB
500
533
 
501
534
  ### 数据流
502
535
 
503
- 内置面板采用**拉取式**数据流,避免长期订阅:
536
+ 按面板拆分,分**三条独立路径**:
504
537
 
505
- ```mermaid
506
- sequenceDiagram
507
- autonumber
508
- actor User as 用户
509
- participant UI as 面板 UI (React)
510
- participant R as renderer
511
- participant M as main<br/>ipc/panels.ts
512
- participant P as preload<br/>instrumentation
513
-
514
- User->>UI: 打开面板 / 刷新
515
- UI->>R: invokeMain('panel:eval', expression)
516
- R->>M: IPC: panel:eval
517
- M->>P: webContents.executeJavaScript(expression)
518
- Note over P: window 上已挂好取值函数
519
- P-->>M: 结构化数据
520
- M-->>R: 返回结果
521
- R-->>UI: 渲染
522
- ```
538
+ | 面板 / 场景 | 模式 | 路径 |
539
+ | --- | --- | --- |
540
+ | Console / AppData / WXML 树 | 推送 | preload instrumentation 通过 `runtime/host.ts:sendToHost` 经 webview `ipcRenderer.sendToHost` 推到宿主 → renderer 监听 `ipc-message` 分发 |
541
+ | Storage | 拉取 + 推送 | renderer `ipcInvoke(SimulatorStorageChannel.GetSnapshot)` → 主进程 CDP `DOMStorage` 域;变更事件由主进程通过 `SimulatorStorageChannel.Event` 推回 renderer |
542
+ | 元素选区 / `Page.getData` / MCP overview | 拉取 | 主进程 `webContents.executeJavaScript(...)` 或 `Runtime.evaluate(...)` 读 `window.__simulatorData.*`(由 `installSimulatorBridge` 暴露) |
523
543
 
524
- Console 输出等推送场景仍由 preload 通过 `installSimulatorBridge()` 经由 webview ipcRenderer 送至宿主,再由 renderer 分发到相应面板。
544
+ > `src/main/ipc/panels.ts` 暴露 `panel:eval` handler 供外部 host 自定义面板复用,内置 renderer 不消费它。
525
545
 
526
546
  ---
527
547
 
@@ -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