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.
- package/lib/client.js +318 -27
- package/lib/mobile/monitor.js +5 -2
- package/lib/mobile/session-flags.js +61 -8
- package/lib/panel/index.html +11 -10
- package/lib/routes/control-plane.js +9 -0
- package/lib/session-cleanup.cjs +22 -7
- package/package.json +1 -1
|
@@ -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
|
// ——窗口由本机设置页亲手开,服务器部署没有「本机屏幕」可看,这条是它唯一的出口。
|
package/lib/session-cleanup.cjs
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
344
|
-
*
|
|
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.
|
|
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",
|