dsh-mobile-plugin 0.5.20 → 0.5.21

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.
@@ -509,6 +509,7 @@ function registerControlPlaneRoutes(deps) {
509
509
  // /api/relay/panel 看宿主记的「配没配过」,自己决定先走引导还是直接进主页
510
510
  // /api/relay/panel/setup 强制走引导(想再看一遍流程时用)
511
511
  // /api/relay/panel/settings 强制进主页(演示 / 排查时跳过引导)
512
+ // /api/relay/panel/debug 强制进调试页(桌面端那颗手机图标点开的就是它)
512
513
  // 整页 HTML 不需要 token,但这几条仍然走 localOnly —— 它读的是本机运维界面,
513
514
  // 而局域网与中转那两条链路本来就不该看到它(它们带 dsh_pair/dsh_lan 凭据,会被挡掉)。
514
515
  const servePanel = (res) => {
@@ -527,6 +528,13 @@ function registerControlPlaneRoutes(deps) {
527
528
  registerRoute({ kind: 'exact', path: '/api/relay/panel', handler: localOnly('GET', async (_req, res) => servePanel(res), { allowDocumentNavigation: true }) })
528
529
  registerRoute({ kind: 'exact', path: '/api/relay/panel/setup', handler: localOnly('GET', async (_req, res) => servePanel(res), { allowDocumentNavigation: true }) })
529
530
  registerRoute({ kind: 'exact', path: '/api/relay/panel/settings', handler: localOnly('GET', async (_req, res) => servePanel(res), { allowDocumentNavigation: true }) })
531
+ // 调试页与上面几条吐同一份 HTML,分流同样在 React 侧(`React/src/Root.tsx` 认 `/debug`)。
532
+ //
533
+ // 为什么它也放开 `allowDocumentNavigation`:这一页唯一的入口是桌面端那颗手机图标弹的窗
534
+ // (同源 iframe),而社区桌面壳会把接住的链接原地打开、从自定义协议页跳过来必带
535
+ // `sec-fetch-site: cross-site` —— 与面板页同一条理由。页面本身仍是只读 HTML,
536
+ // 里面的 fetch 照样被 localOnly 与两小时窗口挡着。
537
+ registerRoute({ kind: 'exact', path: '/api/relay/panel/debug', handler: localOnly('GET', async (_req, res) => servePanel(res), { allowDocumentNavigation: true }) })
530
538
  // 桌面端画布页:整页吐同一份内联 HTML,并在 `</head>` 前补一句「这是桌面端」的标记 ——
531
539
  // 画布据此把路由从 BrowserRouter 换成 HashRouter(它被挂在 `/api/relay/canvas` 这种非根
532
540
  // 路径下,BrowserRouter 的 `/` 匹配不上会 404)。手机 WebView 那份不带这个标记,一个字不改。
@@ -1076,6 +1084,7 @@ const WEB_ACCESS_ROUTES = new Set([
1076
1084
  '/api/relay/panel',
1077
1085
  '/api/relay/panel/setup',
1078
1086
  '/api/relay/panel/settings',
1087
+ '/api/relay/panel/debug',
1079
1088
  '/api/relay/faults',
1080
1089
  // 配对二维码:里面是能派生整条链路密钥的一次性票据,所以它只在两小时窗口开着时给网页看
1081
1090
  // ——窗口由本机设置页亲手开,服务器部署没有「本机屏幕」可看,这条是它唯一的出口。
@@ -316,7 +316,7 @@ function restoreQueuedArchivedClearIds(ctx) {
316
316
  return queued
317
317
  }
318
318
 
319
- async function pruneOrphanedArchivedSessions(ctx) {
319
+ async function pruneOrphanedArchivedSessions(ctx, skip) {
320
320
  const workspaceList = ctx && ctx.apiProxy && ctx.apiProxy.workspace ? ctx.apiProxy.workspace.list : undefined
321
321
  if (typeof workspaceList !== 'function') return 0
322
322
  const rpcId = 'dsh-mobile-plugin-archive-prune'
@@ -325,7 +325,8 @@ async function pruneOrphanedArchivedSessions(ctx) {
325
325
  const archived = value && Array.isArray(value.archivedSessionIds)
326
326
  ? value.archivedSessionIds.filter((id) => typeof id === 'string' && id)
327
327
  : []
328
- const orphans = await archivedIdsWithoutLogs(archived)
328
+ const skipped = skip instanceof Set ? skip : new Set(Array.isArray(skip) ? skip : [])
329
+ const orphans = (await archivedIdsWithoutLogs(archived)).filter((id) => !skipped.has(id))
329
330
  if (orphans.length === 0) return 0
330
331
  const failed = await forgetArchivedSessions(ctx, orphans, rpcId)
331
332
  return orphans.length - failed.length
@@ -339,10 +340,12 @@ async function pruneOrphanedArchivedSessions(ctx) {
339
340
  * 就是一次未捕获的 ENOENT,宿主直接退出。
340
341
  * - **擦归档集合**:桌面端的会话列表是「连上宿主时拉的一份快照 + 之后的事件增量」,
341
342
  * 进程活着的时候擦集合,宿主会把「归档集合变了」广播给桌面端,桌面端就把它快照里
342
- * 那些其实已经没有日志、本来被归档藏着的行放回正常列表 —— 用户报的正是「清理后
343
- * 它们又回来了,重启 DSH 才真的消失」。启动时快照本来就从磁盘重新拉,擦掉不会放出
344
- * 任何行。留在集合里的死 id 不显示在任何地方:「已归档」取的是会话列表与归档集合
345
- * 的交集,没有日志就没有那一行。
343
+ * 那些其实已经没有日志、本来被归档藏着的行放回正常列表。所以只在启动时擦,而且**本次
344
+ * 刚删掉日志的 id 这一轮也先不擦**:宿主启动和删日志是并发的,桌面端的快照完全可能
345
+ * 在删完之前就取走、把这些行带在身上;这时擦集合照样会把它们放回正常列表(一次重启
346
+ * 之后它们又出现,再重启才消失)。留到下一次启动再擦,那时它们早已没有日志,任何新
347
+ * 快照里都不会再有这些行。留在集合里的死 id 依然不显示:「已归档」取的是会话列表与
348
+ * 归档集合的交集,没有日志就没有那一行。
346
349
  *
347
350
  * 用进程级标记而不是模块变量:代际热替换会重跑 apply,而桌面端那条快照挂在同一个进程上。
348
351
  */
@@ -352,9 +355,21 @@ const PRUNE_RETRY_MS = 500
352
355
  async function pruneOrphanedArchivedSessionsOnce(ctx, log) {
353
356
  if (globalThis[PRUNE_ONCE]) return
354
357
  globalThis[PRUNE_ONCE] = true
358
+ // 本次启动前盘上就已经没有数据的 id:它们早就不在任何新快照里,这一轮可以直接擦。
359
+ const alreadyDataLess = new Set(sessionIdsWithoutAnyData(readPendingArchivedClearIds()))
360
+ let skip = []
355
361
  // 删日志不碰宿主,先做:越早越好,别等宿主把 workspace 服务挂齐(那时桌面端可能已经连上)。
356
362
  try {
357
363
  const { deleted, kept } = await deleteQueuedArchivedSessions(log, ctx)
364
+ // 本次刚删掉日志的 id,这一轮先别擦归档集合,留到下一次启动再擦。
365
+ //
366
+ // 桌面端的会话列表是「启动时拉的一份快照 + 之后的事件增量」,宿主没有删除会话的接口,
367
+ // 插件在磁盘上删日志它看不见 —— 它快照里还留着这些行,本来靠「已归档」藏着。这时一擦
368
+ // 集合,宿主就广播「归档集合变了」,桌面端把这些其实已经没有日志的行放回正常列表,用户
369
+ // 看到的就是「重启一次它们又回来了」。留到下一次启动:那时它们早就没有日志,任何新快照
370
+ // 里都不会再有这些行,擦集合的广播也就放不出东西来。代价是这些空 id 在归档集合里多留
371
+ // 一次启动(「已归档」取会话列表与归档集合的交集,没有日志就看不见它)。
372
+ skip = deleted.filter((id) => !alreadyDataLess.has(id))
358
373
  if (deleted.length > 0) log?.(`archived clear queue: deleted ${deleted.length} session logs`)
359
374
  if (kept.length > 0) log?.(`archived clear queue: ${kept.length} session logs kept for the next start`)
360
375
  } catch (error) {
@@ -365,7 +380,7 @@ async function pruneOrphanedArchivedSessionsOnce(ctx, log) {
365
380
  // 的死 id 一直没人清(实测宿主重启过一次,315 个没有日志的 id 原样挂着)。
366
381
  for (let attempt = 1; attempt <= PRUNE_ATTEMPTS; attempt += 1) {
367
382
  try {
368
- const pruned = await pruneOrphanedArchivedSessions(ctx)
383
+ const pruned = await pruneOrphanedArchivedSessions(ctx, skip)
369
384
  if (pruned > 0) log?.(`archived session prune: dropped ${pruned} deleted session ids`)
370
385
  return
371
386
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
5
5
  "license": "MIT",
6
6
  "icon": "./icon.png",