@dimina-kit/devtools 0.4.0-dev.20260616102751 → 0.4.0-dev.20260618090552

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 (40) hide show
  1. package/README.md +25 -24
  2. package/dist/main/index.bundle.js +212 -12
  3. package/dist/main/ipc/bridge-router.js +44 -1
  4. package/dist/main/runtime/devtools-backend.d.ts +1 -1
  5. package/dist/main/runtime/devtools-backend.js +1 -1
  6. package/dist/main/runtime/miniapp-runtime.d.ts +2 -2
  7. package/dist/main/services/console-forward/index.js +8 -1
  8. package/dist/main/services/elements-forward/index.js +31 -7
  9. package/dist/main/services/layout/index.d.ts +12 -0
  10. package/dist/main/services/layout/index.js +14 -2
  11. package/dist/main/services/projects/types.d.ts +1 -1
  12. package/dist/main/services/projects/types.js +1 -1
  13. package/dist/main/services/service-console/console-api.d.ts +78 -0
  14. package/dist/main/services/service-console/console-api.js +104 -0
  15. package/dist/main/services/service-console/index.d.ts +47 -0
  16. package/dist/main/services/service-console/index.js +139 -0
  17. package/dist/main/services/update/update-manager.d.ts +1 -1
  18. package/dist/main/services/update/update-manager.js +1 -1
  19. package/dist/main/services/views/view-manager.d.ts +7 -0
  20. package/dist/main/services/views/view-manager.js +53 -0
  21. package/dist/main/services/workbench-context.d.ts +7 -6
  22. package/dist/renderer/assets/index-DmgWoK8N.js +50 -0
  23. package/dist/renderer/assets/{input-B8NDCYv3.js → input-DiCxYlqY.js} +2 -2
  24. package/dist/renderer/assets/{ipc-transport-DhrajiC5.js → ipc-transport-DSBmaWpJ.js} +1 -1
  25. package/dist/renderer/assets/ipc-transport-DZxf2YoM.css +1 -0
  26. package/dist/renderer/assets/{popover-DbZOg03s.js → popover-CyL_uffj.js} +2 -2
  27. package/dist/renderer/assets/{select-pW3Ysc9T.js → select-BSwvDMtA.js} +2 -2
  28. package/dist/renderer/assets/settings-Ch8XbFxf.js +2 -0
  29. package/dist/renderer/assets/{settings-api-BSKzKiWd.js → settings-api-e9ILe2JO.js} +2 -2
  30. package/dist/renderer/assets/{workbenchSettings-BGxjl25x.js → workbenchSettings-DZsHf1g_.js} +2 -2
  31. package/dist/renderer/entries/main/index.html +6 -6
  32. package/dist/renderer/entries/popover/index.html +5 -5
  33. package/dist/renderer/entries/settings/index.html +11 -5
  34. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  35. package/dist/service-host/preload.cjs +13 -15
  36. package/dist/service-host/sourcemap-rewrite.cjs +21 -12
  37. package/package.json +4 -4
  38. package/dist/renderer/assets/index-Z4Rh52G8.js +0 -50
  39. package/dist/renderer/assets/ipc-transport-9agi76dX.css +0 -1
  40. package/dist/renderer/assets/settings-CZJOHt8b.js +0 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage/编译 面板、编译配置等功能。
4
4
 
5
- 下游 host 通过 `launch(config)` 集成并定制 devtools(零配置直接 `launch()`,配置驱动 `launch({...})`;见下方「两种用法」)。两种用法都经领域中立的 [`@dimina-kit/electron-deck`](../electron-deck) 框架编排——框架接管 Electron 进程生命周期(whenReady / will-quit)、wire/trust 原语,devtools 作为 `RuntimeBackend` 注入完整运行时(见 [`framework-extraction-v2.md`](../electron-deck/docs/framework-extraction-v2.md))。
5
+ 下游 host 通过 `launch(config)` 集成并定制 devtools(零配置直接 `launch()`,配置驱动 `launch({...})`;见下方「两种用法」)。两种用法都经领域中立的 [`@dimina-kit/electron-deck`](../electron-deck) 框架编排——框架接管 Electron 进程生命周期(whenReady / will-quit)、wire/trust 原语,devtools 作为 `RuntimeBackend` 注入完整运行时(见 [`electron-deck 架构`](../electron-deck/docs/architecture.md))。
6
6
 
7
7
  ---
8
8
 
@@ -195,7 +195,8 @@ src/
195
195
  | `menuBuilder` | `(mainWindow, menuContext: MenuContext) => void` | 内置菜单 | 自定义菜单构建器;`menuContext` 为手写窄契约 `MenuContext`(`appName` + workspace 窄集 + `openSettings` + `notify.{projectStatus, windowNavigateBack}`),不含内部管线 |
196
196
  | `onSetup` | `(instance) => void` | — | 窗口和 context 创建后的回调,用于注册 Contribution(见下文)|
197
197
  | `onBeforeClose` | `(instance) => void` | — | 窗口关闭前的回调,session 关闭由框架自动处理 |
198
- | `window` | `WorkbenchWindowConfig` | — | 窗口尺寸覆盖 |
198
+ | `onBeforeOpenProject` | `(projectPath: string) => void \| Promise<void>` | — | 打开项目前、任何副作用(旧会话拆除 / 编译 / dev-server)之前运行的声明式权限门控。抛错即否决:`openProject` 返回 `{ success: false, error }`,当前活动会话保持不动、适配器不被调用,且框架自动把 error 透到状态条(`notify.projectStatus`)。覆盖 IPC / 菜单 / 内部直调三个打开入口 |
199
+ | `window` | `WorkbenchWindowConfig`(`width`/`height`/`autoShow?`) | — | 主窗口尺寸覆盖;`autoShow`(默认 `true`)控制 `ready-to-show` 是否自动显示窗口。`autoShow: false` 时窗口创建即隐藏(test / 非 test 一致),由 host 在登录通过后自行 `instance.mainWindow.show()` |
199
200
  | `updateChecker` | `UpdateChecker` | — | 自定义更新检查器;提供后启用"检查更新"功能 |
200
201
  | `updateOptions` | `{ checkInterval?, initialDelay?, getCurrentVersion? }` | — | 仅当 `updateChecker` 提供时生效,默认 1h / 5s |
201
202
 
@@ -221,11 +222,11 @@ src/
221
222
 
222
223
  ## Embedding & Extending the Project Panel
223
224
 
224
- 宿主(如 qdmp)通过 `launch({...})` 嵌入 devtools 时,可对项目面板做三个正交扩展:
225
+ 下游宿主通过 `launch({...})` 嵌入 devtools 时,可对项目面板做三个正交扩展:
225
226
 
226
227
  | 扩展点 | 用途 |
227
228
  | --- | --- |
228
- | `projectsProvider` | 接管项目列表存储 —— 替换默认 `<userData>/dimina-projects.json`,对接 qdmp 后台 / IDE workspace / 远端工程库 |
229
+ | `projectsProvider` | 接管项目列表存储 —— 替换默认 `<userData>/dimina-projects.json`,对接下游宿主后台 / IDE workspace / 远端工程库 |
229
230
  | `projectTemplates` + `builtinTemplates` | 注入/覆盖/裁剪"新建项目"模板列表(同 id 覆盖内置;`builtinTemplates` 控制内置策略) |
230
231
  | `customCreateProjectDialog` | 用宿主自家页面替换内置"新建项目"对话框(main 进程 hook,可 `new BrowserWindow` 加载任意 URL,通过 IPC/postMessage 回传结果) |
231
232
 
@@ -233,8 +234,8 @@ src/
233
234
 
234
235
  | 场景 | 需要的扩展点 |
235
236
  | --- | --- |
236
- | 项目列表来自 qdmp 后台 / 团队工程库 | `projectsProvider` |
237
- | 仅替换可选模板(e.g. 只提供 taro / qdmp 自家脚手架) | `projectTemplates` + `builtinTemplates` |
237
+ | 项目列表来自下游宿主后台 / 团队工程库 | `projectsProvider` |
238
+ | 仅替换可选模板(e.g. 只提供 taro / 宿主自家脚手架) | `projectTemplates` + `builtinTemplates` |
238
239
  | 创建项目流程要走宿主自己的 wizard / 登录态 | `customCreateProjectDialog` |
239
240
 
240
241
  ### 最小示例
@@ -246,51 +247,51 @@ import { BrowserWindow } from 'electron'
246
247
 
247
248
  const provider: ProjectsProvider = {
248
249
  async listProjects() {
249
- return await qdmp.api.listProjects()
250
+ return await host.api.listProjects()
250
251
  },
251
252
  async validateProjectDir(dirPath) {
252
- return (await qdmp.api.isMiniApp(dirPath)) ? null : '不是合法的小程序工程'
253
+ return (await host.api.isMiniApp(dirPath)) ? null : '不是合法的小程序工程'
253
254
  },
254
255
  async addProject(dirPath) {
255
- return await qdmp.api.addProject(dirPath)
256
+ return await host.api.addProject(dirPath)
256
257
  },
257
258
  async removeProject(dirPath) {
258
- await qdmp.api.removeProject(dirPath)
259
+ await host.api.removeProject(dirPath)
259
260
  },
260
261
  async updateLastOpened(dirPath) {
261
- await qdmp.api.touchLastOpened(dirPath)
262
+ await host.api.touchLastOpened(dirPath)
262
263
  },
263
264
  async getCompileConfig(dirPath) {
264
- return await qdmp.api.getCompileConfig(dirPath)
265
+ return await host.api.getCompileConfig(dirPath)
265
266
  },
266
267
  async saveCompileConfig(dirPath, cfg) {
267
- await qdmp.api.saveCompileConfig(dirPath, cfg)
268
+ await host.api.saveCompileConfig(dirPath, cfg)
268
269
  },
269
270
  // 缩略图走云端存储——dataUrl 形如 'data:image/png;base64,...'
270
271
  async saveThumbnail(dirPath, imageDataUrl) {
271
- await qdmp.api.uploadThumbnail(dirPath, imageDataUrl)
272
+ await host.api.uploadThumbnail(dirPath, imageDataUrl)
272
273
  },
273
274
  async getThumbnail(dirPath) {
274
- return await qdmp.api.fetchThumbnail(dirPath)
275
+ return await host.api.fetchThumbnail(dirPath)
275
276
  },
276
277
  }
277
278
 
278
- const qdmpBlank: ProjectTemplate = {
279
+ const hostBlank: ProjectTemplate = {
279
280
  id: 'blank', // 同 id 覆盖内置 blank
280
- name: 'qdmp 空白工程',
281
- description: '由 qdmp 维护的官方空白模板',
281
+ name: '宿主空白工程',
282
+ description: '由下游宿主维护的官方空白模板',
282
283
  source: { type: 'directory', path: '/abs/path/to/template' },
283
284
  }
284
285
 
285
286
  launch({
286
287
  projectsProvider: provider,
287
- projectTemplates: [qdmpBlank],
288
+ projectTemplates: [hostBlank],
288
289
  builtinTemplates: ['taro-todo'], // 只保留 taro-todo,干掉默认 blank
289
290
  customCreateProjectDialog: async ({ parentWindow }) => {
290
291
  const win = new BrowserWindow({ parent: parentWindow, modal: true, width: 720, height: 520 })
291
- await win.loadURL('https://qdmp.example.com/devtools/new-project')
292
+ await win.loadURL('https://host.example.com/devtools/new-project')
292
293
  return await new Promise((resolve) => {
293
- win.webContents.ipc.once('qdmp:create-project:done', (_e, payload) => {
294
+ win.webContents.ipc.once('host:create-project:done', (_e, payload) => {
294
295
  win.close()
295
296
  // payload 三选一(CustomCreateProjectDialogResult):
296
297
  // null → 用户取消
@@ -323,7 +324,7 @@ devtools 自带两个 `source`-style 模板,位于 `packages/devtools/template
323
324
  - `blank` — 最小空白小程序骨架(pages/index + app.js/json/wxss + project.config.json,约 8 个文件)
324
325
  - `taro-todo` — 复制自 `dimina/fe/example/taro-todo`,作为 Taro 编译产物的端到端演示工程
325
326
 
326
- `builtinTemplates: 'none'` 排除全部内置;`builtinTemplates: ['taro-todo']` 仅保留指定 id;`projectTemplates` 同 id 注入会覆盖(例如上文 `qdmpBlank` 覆盖了内置 `blank`)。
327
+ `builtinTemplates: 'none'` 排除全部内置;`builtinTemplates: ['taro-todo']` 仅保留指定 id;`projectTemplates` 同 id 注入会覆盖(例如上文 `hostBlank` 覆盖了内置 `blank`)。
327
328
 
328
329
  ### Provider 部分实现 / fallback 行为
329
330
 
@@ -547,7 +548,7 @@ instance.context.views.hostToolbar.send('host:cmd', { theme: 'dark' }) // false
547
548
 
548
549
  ## MiniappRuntime(宿主集成推荐契约)
549
550
 
550
- 下游宿主(如 qdmp)集成时,推荐依赖手写的稳定契约 `MiniappRuntime`,而不是整个 `WorkbenchContext`——后者携带全部内部 service 类型,内部重构会直接破坏下游编译。`asMiniappRuntime(ctx)` 恒等返回(typed view,非拷贝),宿主对 `runtime.workspace.openProject` 的 monkey-patch 会落在真实对象上:
551
+ 下游宿主集成时,推荐依赖手写的稳定契约 `MiniappRuntime`,而不是整个 `WorkbenchContext`——后者携带全部内部 service 类型,内部重构会直接破坏下游编译。`asMiniappRuntime(ctx)` 恒等返回(typed view,非拷贝),宿主对 `runtime.workspace.openProject` 的 monkey-patch 会落在真实对象上:
551
552
 
552
553
  ```typescript
553
554
  import { asMiniappRuntime, type MiniappRuntime } from '@dimina-kit/devtools'
@@ -679,7 +680,7 @@ pnpm test # 运行测试
679
680
  本工具仅用于本地开发调试。当前的安全配置:
680
681
 
681
682
  - **Workbench 窗口**(main / settings / popover overlay)均启用 `contextIsolation` 并禁用 `nodeIntegration`;renderer 通过 main preload 经 `contextBridge` 拿到 `window.devtools.ipc`,每个 IPC handler 经 sender whitelist 校验调用方
682
- - **Simulator 本体** 是主进程顶层 `WebContentsView`(`nativeSimulatorView`,`partition='persist:simulator'`、`nodeIntegration=false`),其 preload 由 `webPreferences.preload = cjsSiblingPreloadPath(ctx.preloadPath)` 在创建时(`attachNativeSimulator`)固定;session 层的 `configureSimulatorSession()` 只设 Referer/CORS 头,不注册 frame preload。**小程序的每页代码** 运行在该 WCV 内部 DeviceShell 托管的 render-host `<webview>` guest 里(同 `persist:simulator` partition、`nodeIntegration=false`),承载逐页沙箱;guest contextIsolation/sandbox 由主进程 `will-attach-webview` 钉死,渲染进程无法替换
683
+ - **Simulator 本体** 是主进程顶层 `WebContentsView`(`nativeSimulatorView`,per-project `persist:miniapp-<key>` partition、无 appId 时回落共享 `persist:simulator`、`nodeIntegration=false`),其 preload 由 `webPreferences.preload = cjsSiblingPreloadPath(ctx.preloadPath)` 在创建时(`attachNativeSimulator`)固定。**小程序的每页代码** 运行在该 WCV 内部 DeviceShell 托管的 render-host `<webview>` guest 里(`nodeIntegration=false`),承载逐页沙箱;guest 不带静态 `partition` 属性,由主进程 `will-attach-webview` 钉到同一 per-project partition,并钉死 contextIsolation/sandbox,渲染进程无法替换
683
684
  - **renderer 入口** 启用了保守 CSP;`will-navigate` / `setWindowOpenHandler` 屏蔽外部跳转
684
685
 
685
686
  仍然不要在 devtools 窗口中加载不受信任的远程内容;本工具不打算在非本地环境部署。
@@ -1276,7 +1276,7 @@ function buildDocumentUpdatedScript() {
1276
1276
  return buildDispatchScript(msg);
1277
1277
  }
1278
1278
  var DRAIN_INTERVAL_MS = 150;
1279
- var INSTALL_POLL_TRIES = 80;
1279
+ var INSTALL_POLL_TRIES = 200;
1280
1280
  var INSTALL_POLL_INTERVAL_MS = 50;
1281
1281
  function installElementsForward(deps) {
1282
1282
  const { devtoolsWc, bridge } = deps;
@@ -1503,6 +1503,14 @@ function installElementsForward(deps) {
1503
1503
  const unsubscribeRenderEvents = bridge.onRenderEvent(onRenderEvent);
1504
1504
  const onReady = () => {
1505
1505
  if (disposed) return;
1506
+ if (installTimer) {
1507
+ clearInterval(installTimer);
1508
+ installTimer = null;
1509
+ }
1510
+ if (drainTimer) {
1511
+ clearInterval(drainTimer);
1512
+ drainTimer = null;
1513
+ }
1506
1514
  let tries = 0;
1507
1515
  installTimer = setInterval(() => {
1508
1516
  tries++;
@@ -1537,11 +1545,8 @@ function installElementsForward(deps) {
1537
1545
  });
1538
1546
  }, DRAIN_INTERVAL_MS);
1539
1547
  };
1540
- if (devtoolsWc.isLoading()) {
1541
- devtoolsWc.once("dom-ready", onReady);
1542
- } else {
1543
- onReady();
1544
- }
1548
+ devtoolsWc.on("dom-ready", onReady);
1549
+ onReady();
1545
1550
  let devtoolsClosedSub;
1546
1551
  try {
1547
1552
  if (deps.connections) devtoolsClosedSub = deps.connections.acquire(devtoolsWc).on("closed", stop);
@@ -1557,10 +1562,160 @@ function installElementsForward(deps) {
1557
1562
  devtoolsClosedSub?.dispose();
1558
1563
  } catch {
1559
1564
  }
1565
+ try {
1566
+ devtoolsWc.removeListener("dom-ready", onReady);
1567
+ } catch {
1568
+ }
1560
1569
  stop();
1561
1570
  };
1562
1571
  }
1563
1572
 
1573
+ // src/main/services/service-console/console-api.ts
1574
+ var RENDER_FORWARD_SOURCE_URL = "dimina://render-console-forward";
1575
+ var KNOWN_LEVELS = /* @__PURE__ */ new Set(["log", "warn", "error", "info", "debug"]);
1576
+ function mapConsoleApiType(type) {
1577
+ if (type === "warning") return "warn";
1578
+ if (type && KNOWN_LEVELS.has(type)) return type;
1579
+ return "log";
1580
+ }
1581
+ var BIGINT_LITERAL = /^-?\d+n$/;
1582
+ function remoteObjectToValue(ro) {
1583
+ if (!ro) return ro;
1584
+ if ("value" in ro) return ro.value;
1585
+ if (typeof ro.unserializableValue === "string") {
1586
+ const u = ro.unserializableValue;
1587
+ if (u === "Infinity") return Infinity;
1588
+ if (u === "-Infinity") return -Infinity;
1589
+ if (u === "NaN") return NaN;
1590
+ if (u === "-0") return -0;
1591
+ if (BIGINT_LITERAL.test(u)) return u;
1592
+ return u;
1593
+ }
1594
+ switch (ro.type) {
1595
+ case "undefined":
1596
+ return void 0;
1597
+ case "function":
1598
+ return ro.description ?? "[Function]";
1599
+ case "symbol":
1600
+ return ro.description ?? "[Symbol]";
1601
+ case "object":
1602
+ if (ro.subtype === "null") return null;
1603
+ return ro.description ?? "[Object]";
1604
+ default:
1605
+ return ro.description ?? "[Unknown]";
1606
+ }
1607
+ }
1608
+ function needsDeepFetch(ro) {
1609
+ if (!ro) return false;
1610
+ if ("value" in ro) return false;
1611
+ return ro.type === "object" && ro.subtype !== "null" && typeof ro.objectId === "string";
1612
+ }
1613
+ function isRenderForwardEvent(params, sentinelUrl) {
1614
+ const top = params?.stackTrace?.callFrames?.[0]?.url;
1615
+ return typeof top === "string" && top === sentinelUrl;
1616
+ }
1617
+
1618
+ // src/main/services/service-console/index.ts
1619
+ function installServiceConsoleForward(deps) {
1620
+ const { serviceWc, emit, connections } = deps;
1621
+ let disposed = false;
1622
+ let selfAttached = false;
1623
+ let tail = Promise.resolve();
1624
+ let closedSub;
1625
+ function usableDebugger() {
1626
+ try {
1627
+ if (serviceWc.isDestroyed()) return false;
1628
+ if (serviceWc.debugger.isAttached()) return true;
1629
+ } catch {
1630
+ return false;
1631
+ }
1632
+ try {
1633
+ serviceWc.debugger.attach("1.3");
1634
+ selfAttached = true;
1635
+ return true;
1636
+ } catch {
1637
+ try {
1638
+ return serviceWc.debugger.isAttached();
1639
+ } catch {
1640
+ return false;
1641
+ }
1642
+ }
1643
+ }
1644
+ async function resolveArg(ro) {
1645
+ if (!needsDeepFetch(ro)) return remoteObjectToValue(ro);
1646
+ try {
1647
+ const res = await serviceWc.debugger.sendCommand("Runtime.callFunctionOn", {
1648
+ objectId: ro.objectId,
1649
+ functionDeclaration: "function () { return this }",
1650
+ returnByValue: true
1651
+ });
1652
+ if (res && res.result && "value" in res.result) return res.result.value;
1653
+ } catch {
1654
+ }
1655
+ return remoteObjectToValue(ro);
1656
+ }
1657
+ async function handleConsoleApi(params) {
1658
+ if (disposed) return;
1659
+ if (isRenderForwardEvent(params, RENDER_FORWARD_SOURCE_URL)) return;
1660
+ const level = mapConsoleApiType(params.type);
1661
+ const rawArgs = Array.isArray(params.args) ? params.args : [];
1662
+ let args;
1663
+ try {
1664
+ args = await Promise.all(rawArgs.map((a) => resolveArg(a)));
1665
+ } catch {
1666
+ args = rawArgs.map((a) => remoteObjectToValue(a));
1667
+ }
1668
+ if (disposed) return;
1669
+ emit({ source: "service", level, args, ts: Date.now() });
1670
+ }
1671
+ const onMessage = (_event, method, params) => {
1672
+ if (disposed) return;
1673
+ if (method !== "Runtime.consoleAPICalled") return;
1674
+ const p = params;
1675
+ tail = tail.then(() => handleConsoleApi(p)).catch(() => {
1676
+ });
1677
+ };
1678
+ function stop() {
1679
+ if (disposed) return;
1680
+ disposed = true;
1681
+ try {
1682
+ closedSub?.dispose();
1683
+ } catch {
1684
+ }
1685
+ try {
1686
+ serviceWc.debugger.removeListener("message", onMessage);
1687
+ } catch {
1688
+ }
1689
+ if (selfAttached) {
1690
+ try {
1691
+ if (!serviceWc.isDestroyed() && serviceWc.debugger.isAttached()) serviceWc.debugger.detach();
1692
+ } catch {
1693
+ }
1694
+ selfAttached = false;
1695
+ }
1696
+ }
1697
+ if (!usableDebugger()) {
1698
+ disposed = true;
1699
+ return { stop() {
1700
+ } };
1701
+ }
1702
+ try {
1703
+ serviceWc.debugger.on("message", onMessage);
1704
+ } catch {
1705
+ stop();
1706
+ return { stop() {
1707
+ } };
1708
+ }
1709
+ serviceWc.debugger.sendCommand("Runtime.enable").catch(() => {
1710
+ });
1711
+ try {
1712
+ if (connections) closedSub = connections.acquire(serviceWc).on("closed", stop);
1713
+ else serviceWc.once("destroyed", stop);
1714
+ } catch {
1715
+ }
1716
+ return { stop };
1717
+ }
1718
+
1564
1719
  // src/main/services/layout/index.ts
1565
1720
  function computeNativeSimulatorViewParams(rect, zoomPercent) {
1566
1721
  const zoomFactor = zoomPercent / 100;
@@ -1574,12 +1729,11 @@ function computeNativeSimulatorViewParams(rect, zoomPercent) {
1574
1729
  zoomFactor
1575
1730
  };
1576
1731
  }
1577
- var SETTINGS_W = 320;
1578
1732
  function computeSettingsBounds(contentWidth, contentHeight, headerHeight) {
1579
1733
  return {
1580
- x: Math.max(0, contentWidth - SETTINGS_W),
1734
+ x: 0,
1581
1735
  y: headerHeight,
1582
- width: Math.min(SETTINGS_W, Math.max(1, contentWidth)),
1736
+ width: Math.max(1, contentWidth),
1583
1737
  height: Math.max(1, contentHeight - headerHeight)
1584
1738
  };
1585
1739
  }
@@ -1884,6 +2038,7 @@ function createViewManager(ctx) {
1884
2038
  let nativeDevtoolsSourceWc = null;
1885
2039
  let unsubscribeNativeRenderEvents = null;
1886
2040
  let stopElementsForward = null;
2041
+ let stopServiceConsole = null;
1887
2042
  let nativeDevtoolsRetryTimer = null;
1888
2043
  let nativeDevtoolsRetryToken = 0;
1889
2044
  let simulatorBoundsOverride = null;
@@ -1931,6 +2086,7 @@ function createViewManager(ctx) {
1931
2086
  if (!simulatorViewAdded) {
1932
2087
  ctx.windows.mainWindow.contentView.addChildView(simulatorView);
1933
2088
  simulatorViewAdded = true;
2089
+ raiseTopOverlays();
1934
2090
  }
1935
2091
  simulatorView.setBounds(bounds);
1936
2092
  }
@@ -2079,6 +2235,11 @@ function createViewManager(ctx) {
2079
2235
  function closeNativeDevtoolsSource() {
2080
2236
  const source = nativeDevtoolsSourceWc;
2081
2237
  nativeDevtoolsSourceWc = null;
2238
+ try {
2239
+ stopServiceConsole?.();
2240
+ } catch {
2241
+ }
2242
+ stopServiceConsole = null;
2082
2243
  if (!source || source.isDestroyed()) return;
2083
2244
  try {
2084
2245
  if (source.isDevToolsOpened()) {
@@ -2192,6 +2353,15 @@ function createViewManager(ctx) {
2192
2353
  next.openDevTools({ mode: "detach", activate: false });
2193
2354
  wireOpenInEditor(next, simulatorView.webContents);
2194
2355
  customizeDevtoolsTabs(simulatorView.webContents);
2356
+ try {
2357
+ stopServiceConsole?.();
2358
+ } catch {
2359
+ }
2360
+ stopServiceConsole = installServiceConsoleForward({
2361
+ serviceWc: next,
2362
+ connections: ctx.connections,
2363
+ emit: (entry) => ctx.consoleForwarder?.emit(entry)
2364
+ }).stop;
2195
2365
  return true;
2196
2366
  } catch {
2197
2367
  if (nativeDevtoolsSourceWc?.id === next.id) {
@@ -2488,6 +2658,7 @@ function createViewManager(ctx) {
2488
2658
  }
2489
2659
  });
2490
2660
  applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
2661
+ settingsView.setBackgroundColor("#00000000");
2491
2662
  await settingsView.webContents.loadFile(
2492
2663
  path7.join(ctx.rendererDir, "entries/settings/index.html")
2493
2664
  );
@@ -2507,6 +2678,12 @@ function createViewManager(ctx) {
2507
2678
  settingsViewAdded = false;
2508
2679
  }
2509
2680
  }
2681
+ function raiseTopOverlays() {
2682
+ if (ctx.windows.mainWindow.isDestroyed()) return;
2683
+ const cv = ctx.windows.mainWindow.contentView;
2684
+ if (settingsView && settingsViewAdded) cv.addChildView(settingsView);
2685
+ if (popoverView) cv.addChildView(popoverView);
2686
+ }
2510
2687
  function showPopover(data) {
2511
2688
  hidePopover();
2512
2689
  const popover = new WebContentsView2({
@@ -2572,6 +2749,7 @@ function createViewManager(ctx) {
2572
2749
  if (!nativeSimulatorViewAdded) {
2573
2750
  ctx.windows.mainWindow.contentView.addChildView(nativeSimulatorView);
2574
2751
  nativeSimulatorViewAdded = true;
2752
+ raiseTopOverlays();
2575
2753
  }
2576
2754
  nativeSimulatorView.setBounds(p.bounds);
2577
2755
  const simWc = nativeSimulatorView.webContents;
@@ -4301,7 +4479,8 @@ var FORWARDABLE_LEVELS = /* @__PURE__ */ new Set(["log", "warn", "error", "info"
4301
4479
  function buildForwardScript(level, args) {
4302
4480
  const method = FORWARDABLE_LEVELS.has(level) ? level : "log";
4303
4481
  const argsJson = JSON.stringify(args ?? []);
4304
- return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[\u89C6\u56FE]',...a)}catch(_){}})()`;
4482
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[\u89C6\u56FE]',...a)}catch(_){}})()
4483
+ //# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
4305
4484
  }
4306
4485
  function createConsoleForwarder(bridge) {
4307
4486
  const sinks = /* @__PURE__ */ new Set();
@@ -4836,6 +5015,27 @@ function findSimulatorWebContents() {
4836
5015
  }
4837
5016
 
4838
5017
  // src/main/ipc/bridge-router.ts
5018
+ function rewriteSourceMappingUrl(source, scriptUrl) {
5019
+ if (typeof source !== "string" || !source) return source;
5020
+ const re = /(^|\n)[ \t]*\/\/[#@][ \t]*sourceMappingURL=([^\n]*)/g;
5021
+ let lastIndex = -1;
5022
+ let lastValue = "";
5023
+ let m;
5024
+ while ((m = re.exec(source)) !== null) {
5025
+ lastIndex = m.index + m[1].length;
5026
+ lastValue = (m[2] || "").trim();
5027
+ }
5028
+ if (lastIndex < 0 || !lastValue) return source;
5029
+ if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(lastValue) || /^data:/i.test(lastValue)) return source;
5030
+ let absolute;
5031
+ try {
5032
+ absolute = new URL(lastValue, scriptUrl).toString();
5033
+ } catch {
5034
+ return source;
5035
+ }
5036
+ return `${source.slice(0, lastIndex)}
5037
+ //# sourceMappingURL=${absolute}`;
5038
+ }
4839
5039
  var STACK_ID = "stack_0";
4840
5040
  var PREWARM_MAX_POOL_SIZE = 4;
4841
5041
  function resolvePrewarmPoolSize() {
@@ -5365,7 +5565,7 @@ async function injectLogicBundle(ap) {
5365
5565
  try {
5366
5566
  const res = await fetch(logicUrl);
5367
5567
  if (!res.ok) throw new Error(`logic.js fetch ${res.status} at ${logicUrl}`);
5368
- const logicContent = await res.text();
5568
+ const logicContent = rewriteSourceMappingUrl(await res.text(), logicUrl);
5369
5569
  await ap.serviceWc.executeJavaScript(`${logicContent}
5370
5570
  //# sourceURL=${logicUrl}`, true);
5371
5571
  } catch (error) {
@@ -8702,7 +8902,7 @@ var UpdateManager = class {
8702
8902
  downloadedPath = null;
8703
8903
  /**
8704
8904
  * Whether this instance has already emitted `Available` once. The periodic
8705
- * check fires forever (interval default 1h), and downstream UI (qdmp shell
8905
+ * check fires forever (interval default 1h), and downstream UI (a downstream host shell
8706
8906
  * renders a native toast / Notification) is not idempotent — every event
8707
8907
  * stacks. We dedupe at the source: at most one Available per instance
8708
8908
  * lifetime, regardless of how the upstream `info.version` shifts. The user
@@ -11,6 +11,46 @@ import { ServiceHostPool } from '../services/service-host-pool/pool.js';
11
11
  import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from '../services/views/miniapp-partition.js';
12
12
  import { createConsoleForwarder } from '../services/console-forward/index.js';
13
13
  import { STORAGE_API_NAMES } from '../services/simulator-storage/index.js';
14
+ // The compiled `logic.js` ships a RELATIVE `//# sourceMappingURL=logic.js.map`.
15
+ // `injectLogicBundle` loads it via `executeJavaScript`, which gives the injected
16
+ // script no base URL of its own — so DevTools resolves that relative map against
17
+ // the service-host DOCUMENT and 404s, leaving console frames / Sources links
18
+ // pointing at the compiled bundle instead of the developer's source. This rewrite
19
+ // turns the directive absolute.
20
+ //
21
+ // Inlined (not imported from `service-host/sourcemap-rewrite.cjs`) ON PURPOSE:
22
+ // the shipped main entry is the FLAT esbuild bundle (`dist/main/index.bundle.js`,
23
+ // see electron-builder.yml `extraMetadata.main`). A runtime `createRequire(...)`
24
+ // of the relative `.cjs` resolved against `import.meta.url`, which is the bundle
25
+ // at `dist/main/` (one level shallower than the tsc tree `dist/main/ipc/`), so
26
+ // the path 404'd and the PACKAGED app crashed on load. Bundling the logic inline
27
+ // is bundler-proof. KEEP IN SYNC with `service-host/sourcemap-rewrite.cjs` (the
28
+ // copied-not-bundled service-host preload still requires that one).
29
+ function rewriteSourceMappingUrl(source, scriptUrl) {
30
+ if (typeof source !== 'string' || !source)
31
+ return source;
32
+ const re = /(^|\n)[ \t]*\/\/[#@][ \t]*sourceMappingURL=([^\n]*)/g;
33
+ let lastIndex = -1;
34
+ let lastValue = '';
35
+ let m;
36
+ while ((m = re.exec(source)) !== null) {
37
+ lastIndex = m.index + m[1].length;
38
+ lastValue = (m[2] || '').trim();
39
+ }
40
+ if (lastIndex < 0 || !lastValue)
41
+ return source;
42
+ // Already absolute (scheme://, protocol-relative //host, or data:)? Leave it.
43
+ if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(lastValue) || /^data:/i.test(lastValue))
44
+ return source;
45
+ let absolute;
46
+ try {
47
+ absolute = new URL(lastValue, scriptUrl).toString();
48
+ }
49
+ catch {
50
+ return source;
51
+ }
52
+ return `${source.slice(0, lastIndex)}\n//# sourceMappingURL=${absolute}`;
53
+ }
14
54
  const STACK_ID = 'stack_0';
15
55
  /** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/doc §3.3. */
16
56
  const PREWARM_MAX_POOL_SIZE = 4;
@@ -690,7 +730,10 @@ async function injectLogicBundle(ap) {
690
730
  const res = await fetch(logicUrl);
691
731
  if (!res.ok)
692
732
  throw new Error(`logic.js fetch ${res.status} at ${logicUrl}`);
693
- const logicContent = await res.text();
733
+ // Rewrite the relative `sourceMappingURL` to an absolute dev-server URL
734
+ // BEFORE injecting — `executeJavaScript` gives the script no base URL, so a
735
+ // relative map would 404 and break sourcemapped console frames / Sources.
736
+ const logicContent = rewriteSourceMappingUrl(await res.text(), logicUrl);
694
737
  await ap.serviceWc.executeJavaScript(`${logicContent}\n//# sourceURL=${logicUrl}`, true);
695
738
  }
696
739
  catch (error) {
@@ -11,7 +11,7 @@ import type { WorkbenchAppConfig } from '../../shared/types.js';
11
11
  * v2 scope: the backend owns window creation (the framework skips its own when a
12
12
  * backend is present), so `assemble` ignores the framework `runtime` and builds
13
13
  * the real devtools main window itself. Trust/frame unification and surfacing
14
- * the runtime facade are deferred (see framework-extraction-v2.md §7).
14
+ * the runtime facade are deferred.
15
15
  *
16
16
  * NOT migrating onto deck's high-level host API (Window facade / runtime.view /
17
17
  * DeckSession / grants) is a DELIBERATE, reviewed decision — not a backlog item.
@@ -11,7 +11,7 @@ import { registerAppLifecycle } from '../app/lifecycle.js';
11
11
  * v2 scope: the backend owns window creation (the framework skips its own when a
12
12
  * backend is present), so `assemble` ignores the framework `runtime` and builds
13
13
  * the real devtools main window itself. Trust/frame unification and surfacing
14
- * the runtime facade are deferred (see framework-extraction-v2.md §7).
14
+ * the runtime facade are deferred.
15
15
  *
16
16
  * NOT migrating onto deck's high-level host API (Window facade / runtime.view /
17
17
  * DeckSession / grants) is a DELIBERATE, reviewed decision — not a backlog item.
@@ -6,7 +6,7 @@
6
6
  * would drag every nested internal service type (ViewManager,
7
7
  * BridgeRouterHandle, SimulatorApiRegistry, Electron WebContents…) onto the
8
8
  * public semver face, so any internal refactor of those services becomes an
9
- * unreviewed breaking change for downstream hosts (qdmp). Instead this module
9
+ * unreviewed breaking change for downstream hosts. Instead this module
10
10
  * names ONLY the audited downstream consumption surface, with structural DTOs
11
11
  * and zero Electron types — non-Electron consumers can compile against it.
12
12
  *
@@ -171,7 +171,7 @@ export interface MiniappWorkspace {
171
171
  } | null;
172
172
  }
173
173
  /**
174
- * The stable miniapp-kernel surface a downstream host (qdmp) consumes.
174
+ * The stable miniapp-kernel surface a downstream host consumes.
175
175
  * Compiler-enforced and versionable: widening it back toward internal
176
176
  * plumbing is a deliberate semver decision, not an accident of projection.
177
177
  */
@@ -1,4 +1,5 @@
1
1
  import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
2
+ import { RENDER_FORWARD_SOURCE_URL } from '../service-console/console-api.js';
2
3
  /** Console levels we re-emit into the service host. Anything else maps to 'log'. */
3
4
  const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
4
5
  /**
@@ -16,7 +17,13 @@ function buildForwardScript(level, args) {
16
17
  const argsJson = JSON.stringify(args ?? []);
17
18
  // `console[method]` (not `console.log`) preserves the original level so the
18
19
  // DevTools severity filter still works on the forwarded line.
19
- return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()`;
20
+ //
21
+ // The trailing `//# sourceURL` stamps this injected line with a sentinel URL.
22
+ // The service-console CDP capture (services/service-console) attached to THIS
23
+ // service host would otherwise re-capture this `console[...]` call as a fresh
24
+ // service entry and re-broadcast it to automation (duplicate). It skips any
25
+ // `consoleAPICalled` whose top frame URL === RENDER_FORWARD_SOURCE_URL.
26
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
20
27
  }
21
28
  export function createConsoleForwarder(bridge) {
22
29
  const sinks = new Set();
@@ -173,7 +173,11 @@ function buildDocumentUpdatedScript() {
173
173
  }
174
174
  const DRAIN_INTERVAL_MS = 150;
175
175
  /** Bounded retry for the front-end hook install (front-end boots asynchronously). */
176
- const INSTALL_POLL_TRIES = 80;
176
+ // Per-load install poll window. Must outlast the gap between a devtools
177
+ // front-end `dom-ready` and `InspectorFrontendHost` becoming available on a
178
+ // cold/slow open (pool cold-start). 200×50ms = 10s — generous, and each future
179
+ // front-end load re-arms a fresh window anyway (see the `dom-ready` listener).
180
+ const INSTALL_POLL_TRIES = 200;
177
181
  const INSTALL_POLL_INTERVAL_MS = 50;
178
182
  /**
179
183
  * Install Elements forwarding on a DevTools front-end host wc. Returns a disposer
@@ -504,6 +508,17 @@ export function installElementsForward(deps) {
504
508
  const onReady = () => {
505
509
  if (disposed)
506
510
  return;
511
+ // Re-arm safe: a devtools front-end (re)load re-runs this, so clear any
512
+ // in-flight timers from a prior attempt before starting fresh ones — without
513
+ // this a reload would stack intervals (leak) and double-drain.
514
+ if (installTimer) {
515
+ clearInterval(installTimer);
516
+ installTimer = null;
517
+ }
518
+ if (drainTimer) {
519
+ clearInterval(drainTimer);
520
+ drainTimer = null;
521
+ }
507
522
  let tries = 0;
508
523
  installTimer = setInterval(() => {
509
524
  tries++;
@@ -550,12 +565,17 @@ export function installElementsForward(deps) {
550
565
  .catch(() => { });
551
566
  }, DRAIN_INTERVAL_MS);
552
567
  };
553
- if (devtoolsWc.isLoading()) {
554
- devtoolsWc.once('dom-ready', onReady);
555
- }
556
- else {
557
- onReady();
558
- }
568
+ // Re-install on EVERY devtools front-end load, not once. The hook lives in the
569
+ // front-end's `globalThis` (`__diminaElementsHookInstalled`), so any front-end
570
+ // (re)load wipes it — most importantly a service-host pool SWAP, which re-opens
571
+ // DevTools (`pointNativeDevtoolsAtServiceWc`) and re-bootstraps the front-end.
572
+ // A single-shot `once('dom-ready')` left the hook uninstalled after such a
573
+ // reload, so Elements silently fell back to the native service-host DOM. A
574
+ // PERSISTENT listener re-runs the (idempotent — the sentinel returns 'already')
575
+ // install on each load; the immediate call covers an already-loaded wc and the
576
+ // first `openDevTools` whose `dom-ready` may have fired before this ran.
577
+ devtoolsWc.on('dom-ready', onReady);
578
+ onReady();
559
579
  let devtoolsClosedSub;
560
580
  try {
561
581
  // Devtools front-end wc is never pool-reset → use `'closed'` (fires only on
@@ -577,6 +597,10 @@ export function installElementsForward(deps) {
577
597
  devtoolsClosedSub?.dispose();
578
598
  }
579
599
  catch { /* already gone */ }
600
+ try {
601
+ devtoolsWc.removeListener('dom-ready', onReady);
602
+ }
603
+ catch { /* already gone */ }
580
604
  stop();
581
605
  };
582
606
  }