dsh-remote-mobile 1.2.1 → 1.4.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.0] - 2026-08-24
9
+
10
+ ### Compatibility (通用插件共存保护)
11
+ - **修复:与其他远程/Web 接入类插件共存时启动致命崩溃**。`remoteWebUiPairing` 是远程/Web 接入类插件的通用配对共享服务名,任何其他插件先注册同名服务后,本插件再注册必然抛错;而 loader 以 `Promise.allSettled` 并发激活全部 entry,任一失败即回滚整棵插件树并使进程退出(市场安装后直接启动即崩,此前只能手动禁用对方解决)。现改为**延迟裁决**:等待激活窗口结束后检测服务名归属——已被占用则主动让出(不注册、不抛错),无人注册才接管,保证共存时必定可正常启动。
12
+ - **新增:共存状态可见化**。裁决结果通过 `/api/remote-mobile/status` 的 `pairingBridgeMode`(`active`/`yielded`/`pending`)、`pairingBridgeConflict` 与 `pairingBridgeConflictId` 下发(基于 Cordis 公开运行时结构尽力定位占用方包名与 entry id,无法识别时前端回退通用文案);设置页顶部在共存时展示可关闭的警示横幅(中英文),支持一键复制「诊断与修复报告」(含精确 entry id 与可直接执行的修复步骤,可直接粘贴给 AI 助手处理);host 日志同步输出 warn。判定逻辑完全通用,不针对任何特定第三方插件。
13
+ - **文档:澄清 EADDRINUSE 成因**。启动报错 `listen EADDRINUSE: address already in use 0.0.0.0:3080` 是端口被其他进程占用(典型:上一个 dsh 实例未退出、桌面快捷方式隐藏实例),与安装哪个插件无关,README FAQ 新增排查步骤(Q0)。
14
+ - **配置说明调整**:「禁用其他远程接入插件」由必选改为推荐项——共存不再导致启动失败;保留其一仍是最佳实践(避免功能重复与入口混乱)。
15
+
16
+ ### Fixed (缺陷修复)
17
+ - **修复:手动切换扫码页签后被强制切回 Tailscale**。设置面板每 3 秒轮询一次状态,只要本机存在 Tailscale IP 就会强制把扫码页签切回 Tailscale,用户手动选中的「局域网扫码」数秒内被覆盖。现记录用户的手动选择:自动预选仅在用户从未手动切换时生效。
18
+ - **修复:移动端样式片段启用后设置弹窗被困在侧边栏内**。设置面板内联渲染于侧边栏 DOM 中,其全屏遮罩层为 `position:fixed`;而官方在侧边栏列上遗留的恒等 transform(`matrix(1,0,0,1,0,0)`)会把 fixed 后代的包含块困在该列内,叠加官方 `overflow:hidden` 后弹窗被裁剪成抽屉宽度(表现为「弹窗出现在侧边栏里面」)。`preset-sidebar` 现显式清除侧边栏列上全部包含块触发属性(`transform` / `filter` / `backdrop-filter` / `perspective` / `will-change`),并将 body 直挂的模态 portal 容器统一抬升层级,两类弹窗均恢复正常全屏居中。
19
+ - **修复:移动端侧边栏抽屉内外层宽度不一致**。官方响应式脚本会给内层根节点写入固定内联宽度(如 `280px`),与外层抽屉实际宽度(官方 `min(88vw, 320px)`)不一致,在抽屉右缘留下约 40px 无法交互的空白条;现强制内层根节点铺满抽屉(`width:100% !important` 覆盖内联样式),内外对齐无死区。
20
+ - **修复:移动端侧边栏抽屉层级过高压住官方弹层**。抽屉原采用 `z-index:99999`,高于官方模态弹窗(1000)与下拉菜单(1100,portal 到 body),导致设置面板的操作菜单弹出后被盖在抽屉之下无法交互。现整体下调:抽屉 `900`、悬浮把手 `901`——仍稳定覆盖正文内容,同时让位官方全部弹层。
21
+ - **修复:官方 frame::after 全屏点击遮罩罩住抽屉内容**。抽屉降层级后,官方窄屏抽屉的全屏遮罩(`[data-dsh-frame]::after`,fixed、rgba 黑纱、`z-index:1050`、可拦截点击)反过来压在抽屉之上;现将其压制到 `899`——低于抽屉、高于正文,暗化背景与「点外部收回抽屉」能力保留。
22
+ - **修复:移动端抽屉内点击次级操作图标(更多操作 ⋯ / 展开箭头等)后抽屉被误关**。生态插件(如 @linxin666/dsh-web-ui-all)的「点条目自动收回抽屉」增强会把条目内 `role=treeitem` 下的所有点击一律视为选中,下一帧程序化点击收起按钮,导致 ⋯ 菜单未弹出抽屉先被收起。现注入防误收起守卫:记录对条目内按钮/箭头的触摸,900ms 内的程序化收起调用被忽略;真实点击收起按钮、点条目主体回主区的官方行为均保持不变。
23
+ - **修复:触屏点击展开把手后官方 Tooltip 气泡永久滞留**(黑底「收起侧边栏」)。官方气泡显示依赖 `mouseenter`/`focus`、消失只认 `mouseleave`/`blur`,触屏点击后这两个事件永远不会到来;现移动端 UA 下整体隐藏官方 Tooltip 气泡(按钮均保留 `aria-label`,可访问性不受影响),PC 窄窗口悬停提示不受影响。
24
+
25
+ ---
26
+
27
+ ## [1.3.1] - 2026-08-24
28
+
29
+ ### Security Hardening (安全加固)
30
+ - **回环 CSRF / drive-by 防御**:变更类插件 API(非 GET 的 `/api/remote-mobile/*`)在门禁层校验浏览器同源信号——`Origin` 与 Host 不一致、`Origin: null` 或 `Sec-Fetch-Site: cross-site` 的写请求直接返回 403。恶意网页无法再驱使已登录用户的浏览器(现代浏览器对跨站请求必携带 `Origin` / `Sec-Fetch-Site` 信号)向 `127.0.0.1` 发起免认证的跨站写操作(如开启局域网免密、清除密码、注销设备)。curl / 本机脚本等不带浏览器信号的客户端保持完全兼容;本机面板与手机端均为同源请求,行为不变。
31
+ - **回环上下文不再洗白**:Host / Origin / Sec-Fetch-Site 虚拟化改写仅作用于外部来源流量;本机回环 socket(127.0.0.1 / ::1)保留原始头,仅将 `localhost` / `[::1]` 变体归一化为 `127.0.0.1`;`socket.remoteAddress` 的统一虚拟化仍保留(仅对外部来源流量产生实际效果——真实回环请求本就是 `127.0.0.1`,该改写为 no-op)。此前对全部请求统一改写会顺带拆掉底层 WebServer 自带的 DNS-Rebinding 防护,恶意域名解析到 127.0.0.1 时可借道读写本机接口,现已恢复该防护对外部域名的效力(外部合法流量的虚拟化放行不受影响)。
32
+ - **SSE 事件流收紧**:移除 `/api/remote-mobile/events` 响应中的 `Access-Control-Allow-Origin: *`。前端 Bundle 与接口同源,无需 CORS 头;该头曾允许任意网页通过 EventSource 持续读取设备接入与安全告警事件流。
33
+ - **devices.json 权限收紧为 0o600**:会话持久化文件内含 365 天长效访问 Token(即完整凭证),落盘权限与 `rsa-keys.json` 对齐,仅当前用户可读写(已有旧文件会在下次写盘时自动修正)。
34
+
35
+ ---
36
+
37
+ ## [1.3.0] - 2026-08-23
38
+
39
+ ### Mobile Style Snippets (移动端样式片段 / 样式小插件)
40
+ - **全新模块 `src/styles/style-snippets.ts`**:插件内部独立的「样式小插件」注册表,将 DSH 界面样式拆分为可独立启停的 CSS 片段。
41
+ - **按界面区域划分的三段内置预设**:`preset-sidebar` 侧边栏抽屉导航(折叠 0 宽度 + 可拖拽悬浮把手)、`preset-settings` 设置面板适配(弹窗居中微缩 + 遮罩锁滚动)、`preset-main` 对话正文**高密度排版**(小字号 12.5px + 紧凑行距 + 收紧边距,每行展示更多内容;字号规则基于稳定 HTML 元素与 CSS Modules localName 后缀,不做布局缩放——zoom 会挤压 1fr 网格轨道产生右侧空白甚至破版,已弃用),默认移动端启用、PC 关闭。
42
+ - **PC / 移动端分别启停**:每个片段(内置与自定义)均支持 `pcEnabled` / `mobileEnabled` 双开关,注入层按视口宽度档选择对应端生效(窄屏 ≤900px / 宽屏 >900px,详见下条);存储升级为 v2(`presetStates` + 双端字段)并自动迁移 v1 旧数据。
43
+ - **内置预设状态分组齐全落盘**:`presetStates` 恒含 `preset-sidebar` / `preset-settings` / `preset-main` 三个分组,每组 `pc/mobile` 写当前有效值(用户覆盖 ?? 代码默认值),文件自包含、三组一目了然。
44
+ - **设置面板页签分组导航**:远程与移动端设置按「接入 / 设备与安全 / 样式覆写 / 本地数据」四个页签分组,消除长页面;全组保持挂载、仅 display 切换,切页不重置配对码/表单状态,页签选择 localStorage 记忆。
45
+ - **PC / 移动端开关改为按视口宽度生效(不再按设备 UA 过滤)**:`mobileEnabled` 的片段包 `@media (max-width: 900px)`(窄视口生效,**PC 浏览器拉小窗口同样生效**),`pcEnabled` 包 `@media (min-width: 901px)`(宽视口生效),两端都开则全宽度;内置预设 CSS 不再写死媒体查询外壳,由注入层按开关包装;修复了此前「只勾移动端时 PC 拉小屏幕不生效」的问题。
46
+ - **用户自定义片段**:支持在设置面板(设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段)或 API(`GET/POST /api/remote-mobile/styles` 等)中动态新增/编辑/删除/启停,持久化于 `~/.dsh/remote-mobile/style-snippets.json`,无需修改插件代码、无需重启。
47
+ - **按 UA 注入 + `data-dsh-mobile` 标记**:网关响应层按 User-Agent 判定移动端,向主工作区 index.html 与 /auth 登录页注入 `<style id="dsh-remote-mobile-style-snippets">`,并给 `<html>` 打上 `data-dsh-mobile` 属性作为稳定作用域选择器钩子;桌面端启用片段时同样按需注入(建议片段不依赖 `data-dsh-mobile` 作用域)。
48
+ - **悬浮把手拖拽脚本(全端注入 + 折叠状态自守卫)**:`dsh-draggable-nav` 支持拖动侧边栏展开把手并记忆位置,配合 `preset-sidebar` 使用;脚本对所有请求注入,运行时仅在侧边栏处于折叠状态时生效(桌面端未折叠时不工作,PC 窄窗口拉出折叠侧边栏时同样可拖动)。
49
+ - **存储卡片展示真实跨平台绝对路径**:设置面板「本地数据与密钥持久化存储说明」中的三个文件路径(devices.json / rsa-keys.json / style-snippets.json)改由 `/api/remote-mobile/status` 动态下发(服务端用 `join(homedir(), ...)` 跨平台拼接),不再硬编码 `~/.dsh/...` 形式。
50
+ - **构建脚本健壮性**:`scripts/build-client.js` 自动同步到 DSH 运行目录失败时仅告警不再中断构建。
51
+ - **修复:`beforeExit` 兜底落盘真正生效**:`StyleSnippetStore` 构造函数补充 `activeStores.add(this)`(与 `SessionStore` 保持一致),此前文件末尾的遍历集合恒为空(死代码)。
52
+ - **修复:样式路由双注册冲突**:`/api/remote-mobile/styles` 收敛为「单 path 内按 method 分发」,规避 webserver 精确路由同 path 重复注册冲突。
53
+ - **修复:自定义片段 id 冲突**:拒绝使用内置预设 id / `preset-` 保留前缀作为自定义片段 id,避免列表、启停与删除语义歧义。
54
+ - **修复:内置预设名称本地化**:`GET /api/remote-mobile/styles` 按请求语言返回预设名称/描述,英文界面不再显示中文。
55
+ - **加固:`Content-Length` 防截断守卫**:注入 `<style>`/Polyfill 改写响应体后、且响应头尚未发出时移除显式 `Content-Length`,交由 Node 按最终 body 重新计算,规避 `ERR_CONTENT_LENGTH_MISMATCH` 截断风险(`writeHead` 已发出头部属不可挽救场景)。
56
+ - **加固:`</style` 转义**:`buildMobileStyleTag` 将片段中的 `</style` 转义为 `</style` 的反斜杠形式(`<\/style`),防止用户 CSS 意外/恶意提前闭合 `<style>` 标签(与 dsh-host-webserver 对 `IndexInjection` `'style'` 行「text 不得包含 `</style`」的约束一致)。
57
+
58
+ ---
59
+
60
+ ## [1.2.1] - 2026-08-23
61
+
62
+ ### Documentation & UI Polish (文档与界面微调)
63
+ - **名称规范化**:全项目与 npm 包元数据统一将 DSH 全称规范为 **DeepSeek Harness**。
64
+ - **配置说明优化**:完善 README 中关于禁用内置远程插件的注释说明(防止功能冲突)。
65
+ - **设置界面精简**:控制台设置页 GitHub 仓库徽标文字精简为 `dsh-remote-mobile`。
66
+
67
+ ---
68
+
8
69
  ## [1.2.0] - 2026-08-22
9
70
 
10
71
  ### Security & Cryptography (安全与加密)
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # dsh-remote-mobile
4
4
 
5
- **DeepSeek Harness (DSH) 远程移动端与局域网安全接入插件**
5
+ **DeepSeek Harness (DSH) 远程与移动端安全网关插件**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/dsh-remote-mobile.svg?style=flat-square&color=3b82f6)](https://www.npmjs.com/package/dsh-remote-mobile)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/dsh-remote-mobile.svg?style=flat-square&color=22c55e)](https://www.npmjs.com/package/dsh-remote-mobile)
@@ -10,7 +10,7 @@
10
10
  [![license](https://img.shields.io/npm/l/dsh-remote-mobile.svg?style=flat-square&color=10b981)](https://github.com/IceApriler/dsh-remote-mobile/blob/master/LICENSE)
11
11
 
12
12
  <p align="center">
13
- <b>突破本地限制 · 扫码直连 · 工作区全功能复用 · 传输层加密</b>
13
+ <b>零改动底层代码 · 突破本地限制 · 扫码直连 · 工作区全功能复用 · 传输层加密</b>
14
14
  </p>
15
15
 
16
16
  [English Documentation](./README_EN.md) · [简体中文](./README.md)
@@ -43,10 +43,11 @@ DSH 核心服务出于安全考虑默认仅监听本地回环地址(`127.0.0.1
43
43
  ## ⚡ 核心优势
44
44
 
45
45
  * 🚀 **突破网络壁垒,工作区全功能复用**:解决局域网和 Tailscale 无法访问 DSH Web 的问题。在移动端或外部设备访问时,**支持新建工作区、切换工作区、执行终端命令等桌面端核心功能**。
46
- * 📱 **手机端与 Web 同源复用**:手机端直接复用 DSH Web 官方同源底座与完整生态插件(如 `dsh-pet` 宠物、任务看板等),后端与服务逻辑保持一致,后期只需按需适配移动端响应式样式即可,无需单独维护手机端后台。
46
+ * 📱 **手机端专属适配与同源复用**:直接复用 DSH Web 官方同源底座与生态插件(如 `dsh-pet` 宠物、任务看板等),无需单独维护手机端后台。插件已针对移动端做好了基础响应式适配(如侧边栏抽屉折叠、设置弹窗居中、对话正文字号与行距精细调小以提升一屏信息量),并内置**样式片段覆写功能**,支持开箱即用或按需注入自定义 CSS 样式小插件。
47
47
  * 🛡️ **安全门禁与传输加密**:内置 RSA 非对称公钥加密、`scrypt` 加盐慢哈希密码落盘、连续输错自动熔断锁定 IP,拦截公网/局域网未经授权的访问。
48
48
  * 📲 **扫码快速配对**:自动识别并生成 Tailscale CGNAT 与局域网专属访问链接与二维码,手机扫码即可完成长效授权。
49
49
  * 🔄 **SSE 实时状态推送**:设备上线、下线、注销、IP 锁定等安全事件即时推送,无需前端轮询。
50
+ * 🤝 **插件通用共存保护**:与其他远程/Web 接入类插件并存时自动让出共享服务、保证正常启动,设置页提供冲突警示横幅与一键复制的「诊断与修复报告」。
50
51
 
51
52
  ---
52
53
 
@@ -64,7 +65,7 @@ dsh plugin --profile web add dsh-remote-mobile
64
65
  ---
65
66
 
66
67
  <span id="config"></span>
67
- ### 2. 必备前置配置(开放外部监听)
68
+ ### 2. 前置配置(开放外部监听)
68
69
 
69
70
  由于 DSH 默认仅监听 `127.0.0.1`,为了使 Tailscale 私网或局域网设备能够正常连通,请确保在 `~/.dsh/profiles/web/cordis.patch.yml` 中包含以下配置:
70
71
 
@@ -77,7 +78,10 @@ dsh plugin --profile web add dsh-remote-mobile
77
78
  host: '0.0.0.0'
78
79
  port: 3080
79
80
 
80
- # 2. 禁用 @linxin666/dsh-web-ui-all 内置的远程插件(必选:防止功能冲突)
81
+ # 2. (推荐)只保留一款远程/Web 接入类插件,避免功能重复与入口混乱
82
+ # 本插件与其他远程接入类插件共存时也能正常启动:检测到配对共享服务被占用
83
+ # 会自动让出,并在设置页顶部展示警示横幅说明详情。
84
+ # 示例:禁用 @linxin666/dsh-web-ui-all 内置的远程插件
81
85
  - id: web-ui-remote-web-ui
82
86
  disabled: true
83
87
  ```
@@ -94,6 +98,8 @@ dsh web --no-open
94
98
 
95
99
  启动后,在电脑浏览器打开 DSH Web 控制台,进入 **设置 ⚙️ -> 远程与移动端**,使用手机微信或系统相机扫描二维码即可立即开启移动端接入!
96
100
 
101
+ > 设置面板按 **「接入 / 设备与安全 / 样式覆写 / 本地数据」** 四个页签分组显示,避免长页面;页签选择会被记住,切换不重置配对码等状态。
102
+
97
103
  ---
98
104
 
99
105
  <span id="preview"></span>
@@ -101,17 +107,19 @@ dsh web --no-open
101
107
 
102
108
  ### PC 端 DSH 插件控制面板
103
109
 
104
- | 网络接入与扫码配对 | 长期密码、设备会话与 IP 防暴破审计 |
110
+ | 网络接入与扫码配对 | 设备与安全 |
105
111
  | :---: | :---: |
106
- | ![PC 设置面板 - 网络与配对码](images/pc-dsh-setting-1.png) | ![PC 设置面板 - 密码与设备管理](images/pc-dsh-setting-2.png) |
112
+ | ![PC 设置面板 - 网络接入与扫码配对](images/pc-dsh-setting-1.png) | ![PC 设置面板 - 设备与安全](images/pc-dsh-setting-2.png) |
113
+ | **样式片段覆写** | **本地数据存储** |
114
+ | ![PC 设置面板 - 样式片段覆写](images/pc-dsh-setting-3.png) | ![PC 设置面板 - 本地数据存储](images/pc-dsh-setting-4.png) |
107
115
 
108
116
  ---
109
117
 
110
118
  ### 手机端实机演示(同源免登与工作区全功能)
111
119
 
112
- | 手机扫码授权登录页 | 手机端完整控制 DSH 工作区与操作 |
113
- | :---: | :---: |
114
- | ![手机端授权登录页](images/mobile-auth.jpg) | ![手机端控制 DSH 工作区](images/mobile-dsh.jpg) |
120
+ | 登录 | 对话列表字号缩放 | 侧边栏移动端样式 | 官方轨迹查看 |
121
+ | :---: | :---: | :---: | :---: |
122
+ | ![登录](images/mobile-auth.jpg) | ![对话列表字号缩放](images/mobile-dsh-1.jpg) | ![侧边栏移动端样式](images/mobile-dsh-2.jpg) | ![官方轨迹查看](images/mobile-dsh-3.jpg) |
115
123
 
116
124
  ---
117
125
 
@@ -138,14 +146,31 @@ dsh web --no-open
138
146
  - 访问审计与锁定状态持久化落盘,服务重启后自动恢复;
139
147
  - 支持管理员在管理面板中一键解锁指定 IP。
140
148
  - **智能静态资源放行机制**:安全门禁内置智能前端静态扩展名识别(放行 `.js`、`.css`、`.png`、`.svg`、`.woff2` 等 20+ 种合法资源),彻底拦截无扩展名或动态管理 API 请求(如 `/plugins/xxx/admin`),确保第三方生态插件前端正常展示的同时严密保护后台动态接口,无需用户在设置界面手动维护白名单。
141
- - **高可靠原子持久化与防抖**:采用 500ms 写盘防抖节流保护磁盘 I/O,配合 `beforeExit` 进程退出 Flush 保证数据不丢失;配置文件采用临时文件原子替换(`renameSync`),防止异常中断损坏数据。
149
+ - **高可靠原子持久化与防抖**:采用毫秒级写盘防抖节流保护磁盘 I/O(会话数据 500ms、样式片段 300ms),配合 `beforeExit` 进程退出 Flush 保证数据不丢失;`settings.yaml` 与 `style-snippets.json` 采用临时文件原子替换(`renameSync`),防止异常中断损坏数据。
142
150
  - **真实 IP 安全提取**:仅信任底层 Socket 真实连接地址,防范伪造的 `X-Forwarded-For` 欺骗攻击。
151
+ - **本机回环 CSRF 防御**:插件的变更类 API 会校验浏览器同源信号(`Origin` / `Sec-Fetch-Site`),恶意网页驱使浏览器向 `127.0.0.1` 发起的跨站写请求将被直接拒绝;curl / 本机脚本等无浏览器信号的客户端不受影响。
152
+ - **回环上下文不洗白**:上下文虚拟化仅作用于外部来源流量;本机回环请求保留原始 Host / Origin,使 DSH 底层自带的 DNS-Rebinding 与同源校验对外部域名继续生效。SSE 事件流仅限同源连接,不开放跨域读取。
143
153
 
144
154
  ### 4. 实时状态同步与国际化
145
155
  - **SSE 实时事件推送**:基于 Server-Sent Events 实现新设备接入、设备重连、会话撤销及安全告警的实时通知,内置连接双向关闭监听与幂等清理。
146
156
  - **全界面中英文双语自适应**:根据 DSH 全局语言偏好与浏览器环境,动态自适应中英文面板、提示及手机端界面。
147
157
  - **非 HTTPS 兼容补丁**:自动注入 `crypto.randomUUID` Polyfill,解决移动端浏览器在 HTTP 非安全上下文下缺少原生 API 的报错。
148
158
 
159
+ ### 5. 移动端样式片段(样式小插件)
160
+
161
+ DSH Web 界面在手机上仍有不少沿袭桌面端的样式问题,插件内置「移动端样式片段」模块,把移动端适配拆分为可独立启停的 CSS 片段:
162
+
163
+ - **按界面区域划分的三段内置预设**:`preset-sidebar` 侧边栏抽屉导航(折叠 0 宽度、可拖拽悬浮把手)、`preset-settings` 设置面板适配(弹窗居中微缩、遮罩锁滚动)、`preset-main` 对话正文**高密度排版**(小字号 12.5px + 紧凑行距 + 收紧边距 → 每行展示更多内容;基于稳定 HTML 元素与 localName 后缀,不做布局缩放,无右侧留白),默认移动端启用、PC 关闭;
164
+ - **PC / 移动端分别启停(按视口宽度判定,与设备无关)**:每段预设和每个自定义片段都有独立的「🖥️ PC」「📱 移动端」两个开关;「移动端」= 窄视口(≤900px)生效——**PC 浏览器拉小窗口也会生效**,「PC」= 宽视口(>900px)生效,两端都开 = 全宽度生效;
165
+ - **用户自定义(样式小插件)**:在 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段** 中粘贴自己的 CSS 即可新增片段,支持编辑/启停/删除,持久化于 `~/.dsh/remote-mobile/style-snippets.json`,保存后下一次页面加载即生效,无需重启;
166
+ - **按 UA 打标记 + 按宽度生效**:样式按视口宽度档生效(见上),与设备 UA 无关;移动端 UA 请求额外给 `<html>` 打上 `data-dsh-mobile="1"` 标记作为作用域钩子,并注入可拖拽的展开把手脚本(全端注入,运行时仅在侧边栏折叠时生效)。
167
+
168
+ ### 6. 插件通用共存保护
169
+
170
+ - **启动零冲突**:`remoteWebUiPairing` 配对共享服务被其他远程/Web 接入类插件占用时,本插件自动让出(延迟裁决:等待激活窗口结束后检测服务名归属),彻底避免服务重名导致的整树回滚致命崩溃——从插件市场安装后可直接启动,无需任何手动配置;
171
+ - **冲突状态可见化**:检测到共存时,设置页顶部展示可关闭的警示横幅(中英文),说明让出行为与「只保留其一」的建议;处理完成后刷新页面即不再出现;
172
+ - **一键诊断报告**:横幅内 **「📋 复制诊断与修复信息」** 按钮可复制完整诊断报告——运行时自动识别占用方插件包名与 loader entry id,报告含精确到行的修复配置与命令(禁用对方 / 卸载本插件二选一),粘贴给 AI 助手即可按步骤处理。
173
+
149
174
  ---
150
175
 
151
176
  <span id="install"></span>
@@ -214,6 +239,33 @@ dsh-remote-mobile:
214
239
  lockDurationMs: 900000 # number,默认 900000 (15分钟):IP 锁定持续时间(毫秒)
215
240
  ```
216
241
 
242
+ ### 🎨 移动端样式片段(可选)
243
+
244
+ 自定义样式片段(样式小插件)与启停状态持久化于 `~/.dsh/remote-mobile/style-snippets.json`,可在设置面板图形化管理,也可直接编辑该文件:
245
+
246
+ ```json
247
+ {
248
+ "version": 2,
249
+ "presetStates": {
250
+ "preset-sidebar": { "pc": false, "mobile": true },
251
+ "preset-settings": { "pc": false, "mobile": true },
252
+ "preset-main": { "pc": true, "mobile": true }
253
+ },
254
+ "custom": [
255
+ {
256
+ "id": "custom-xxx",
257
+ "name": "我的样式小插件",
258
+ "css": "html[data-dsh-mobile] .我的选择器 { ... }",
259
+ "pcEnabled": false,
260
+ "mobileEnabled": true
261
+ }
262
+ ],
263
+ "customOrder": ["custom-xxx"]
264
+ }
265
+ ```
266
+
267
+ > 所有启用片段会拼接为单个 `<style>` 注入主页面与 `/auth` 登录页,**生效与否由视口宽度决定**:`mobileEnabled` 的片段包在 `@media (max-width: 900px)`(≤900px 生效,PC 拉小窗口同样生效),`pcEnabled` 的片段包在 `@media (min-width: 901px)`(宽视口生效),两端都开则全宽度生效(v1 旧格式自动迁移,`presetStates` 按三个预设分组齐全落盘)。注意:PC 开启侧边栏抽屉预设会改变桌面布局,如不想用可关闭该预设的 PC 开关。
268
+
217
269
  ---
218
270
 
219
271
  ## 📂 本地文件存储位置
@@ -221,14 +273,27 @@ dsh-remote-mobile:
221
273
  | 文件路径 | 说明 | 安全级别 |
222
274
  |---|---|---|
223
275
  | `~/.dsh/settings.yaml` | 全局安全策略与免密开关配置 | 用户级读写 |
224
- | `~/.dsh/remote-mobile/devices.json` | 已授权设备会话、IP 访问计数与安全审计数据 | 本地落盘持久化 |
276
+ | `~/.dsh/remote-mobile/devices.json` | 已授权设备会话、IP 访问计数与安全审计数据(内含长效 Token,权限 `0o600`) | 本地落盘,仅当前用户可读写 |
225
277
  | `~/.dsh/remote-mobile/rsa-keys.json` | 服务端 RSA 密钥对文件(含公钥与私钥) | 本地落盘,权限 `0o600`(仅当前用户可读写) |
278
+ | `~/.dsh/remote-mobile/style-snippets.json` | 移动端样式片段(内置预设启停状态 + 用户自定义 CSS 小插件) | 本地落盘持久化 |
226
279
 
227
280
  ---
228
281
 
229
282
  <span id="faq"></span>
230
283
  ## ❓ 常见问题 (FAQ)
231
284
 
285
+ <details>
286
+ <summary><b>Q0: 启动报错 <code>listen EADDRINUSE: address already in use 0.0.0.0:3080</code>?</b></summary>
287
+
288
+ **答**:这是 **3080 端口被其他进程占用**(绝大多数是上一个 `dsh web` 实例尚未完全退出,例如桌面快捷方式拉起的隐藏实例、或重启时新旧进程交叠),**与安装了哪个插件无关**。排查步骤:
289
+
290
+ 1. 查找占用端口的进程:`lsof -nP -iTCP:3080`(Windows 用 `netstat -ano | findstr 3080`);
291
+ 2. 结束旧实例(`kill <PID>` / 任务管理器),再重新启动 `dsh web`;
292
+ 3. 注意:桌面快捷方式(dsh-desktop-launcher 生成的实例)与终端手动启动的实例不能同时运行。
293
+
294
+ 另:若日志显示的是 `service "remoteWebUiPairing" has been registered` 类错误,请升级本插件至最新版本——新版已内置通用共存保护,与其他远程接入类插件并存时可正常启动并自动让出服务,并在设置面板顶部提示冲突详情。
295
+ </details>
296
+
232
297
  <details>
233
298
  <summary><b>Q1: 手机扫码后提示「连接被拒绝」或无法打开页面?</b></summary>
234
299
 
@@ -252,6 +317,22 @@ dsh-remote-mobile:
252
317
  **答**:**支持**。本插件通过请求隔离技术,使手机端与 PC 桌面端拥有一致的工作区管理与执行能力。
253
318
  </details>
254
319
 
320
+ <details>
321
+ <summary><b>Q4: 手机端样式还是不满意,想完全自己定制?</b></summary>
322
+
323
+ **答**:打开 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段**,先尝试启停三段内置预设(侧边栏 / 设置面板 / 正文,每段都有独立的 PC 与移动端开关);还不够就在「自定义片段」里粘贴自己的 CSS(例如 `html[data-dsh-mobile] .xxx { ... }`)。保存后手机端刷新页面立即可见,改动全部落在 `~/.dsh/remote-mobile/style-snippets.json`,升级插件不会丢失。
324
+ </details>
325
+
326
+ <details>
327
+ <summary><b>Q5: 设置页顶部出现黄色横幅「⚠️ 检测到其他远程接入插件」是什么意思?</b></summary>
328
+
329
+ **答**:说明当前有另一款远程/Web 接入类插件与本插件同时启用,且对方先占用了配对共享服务。为避免启动冲突,本插件已**自动让出**该服务(自身的安全门禁与远程接入不受影响),但两套远程功能并存可能出现重复入口。建议只保留其一:
330
+
331
+ 1. 点击横幅中的 **「📋 复制诊断与修复信息」**,将报告粘贴给 AI 助手按步骤处理(报告已自动识别占用方包名与条目 id,并给出精确的修复配置);
332
+ 2. 推荐保留本插件时:按报告指引在 `~/.dsh/profiles/web/cordis.patch.yml` 末尾追加对应的 `disabled: true` 两行,然后重启 DSH;
333
+ 3. 横幅可点击 ✕ 临时关闭;冲突解除后刷新页面即不再出现。
334
+ </details>
335
+
255
336
  ---
256
337
 
257
338
  ## 📄 开源许可证
package/README_EN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # dsh-remote-mobile
4
4
 
5
- **DeepSeek Harness (DSH) Mobile & Remote Access Security Guard Plugin**
5
+ **DeepSeek Harness (DSH) Remote & Mobile Security Guard Plugin**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/dsh-remote-mobile.svg?style=flat-square&color=3b82f6)](https://www.npmjs.com/package/dsh-remote-mobile)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/dsh-remote-mobile.svg?style=flat-square&color=22c55e)](https://www.npmjs.com/package/dsh-remote-mobile)
@@ -10,7 +10,7 @@
10
10
  [![license](https://img.shields.io/npm/l/dsh-remote-mobile.svg?style=flat-square&color=10b981)](https://github.com/IceApriler/dsh-remote-mobile/blob/master/LICENSE)
11
11
 
12
12
  <p align="center">
13
- <b>Break Loopback Limits · QR Quick Pairing · Full Workspace Parity · Transport-Layer Encryption</b>
13
+ <b>Zero Core Modifications · Break Loopback Limits · QR Quick Pairing · Full Workspace Parity · Transport Encryption</b>
14
14
  </p>
15
15
 
16
16
  English Documentation · [简体中文](./README.md)
@@ -43,10 +43,11 @@ By default, DSH strictly listens only to the local loopback address (`127.0.0.1`
43
43
  ## ⚡ Key Advantages
44
44
 
45
45
  * 🚀 **Break Network Barriers with Full Workspace Parity**: Solves the core issue of accessing DSH Web over LAN and Tailscale. When accessed on mobile devices, **creating workspaces, switching workspaces, and executing terminal commands are supported** without feature degradation.
46
- * 📱 **Unified Codebase with Web**: Mobile devices share the same DSH Web core runtime and ecosystem plugins (such as `dsh-pet`, task boards, etc.). No duplicate backend maintenance is needed; future enhancements only require standard responsive styling adjustments.
46
+ * 📱 **Dedicated Mobile Adaptations & Same-Origin Reuse**: Directly reuses the official DSH Web base and complete plugin ecosystem (such as `dsh-pet`, task boards, etc.) without maintaining a separate mobile backend. Includes out-of-the-box mobile adaptations (sidebar drawer toggle, centered settings modal, and compact chat font scaling for higher information density), alongside a built-in **Style Snippet Override system** to toggle presets or inject custom CSS snippets on demand.
47
47
  * 🛡️ **Security Gateway & Transport Encryption**: Built-in client-side RSA asymmetric public key encryption, `scrypt` salted slow-hashing password persistence, and automated IP lockout upon consecutive brute-force failures, blocking unauthorized access over public/LAN networks.
48
48
  * 📲 **Quick QR Pairing**: Auto-detects Tailscale CGNAT and LAN IP addresses to generate dedicated pairing QR codes. Scan the code to authenticate and obtain persistent credentials.
49
49
  * 🔄 **SSE Real-Time Push**: Device connection, reconnection, revocation, and security alert events are pushed in real time via Server-Sent Events without polling.
50
+ * 🤝 **Generic Plugin Coexistence Protection**: Starts safely alongside other remote/Web access plugins by automatically yielding the shared service; the settings panel shows a conflict banner with a one-click copyable diagnosis & fix report.
50
51
 
51
52
  ---
52
53
 
@@ -64,7 +65,7 @@ dsh plugin --profile web add dsh-remote-mobile
64
65
  ---
65
66
 
66
67
  <span id="config"></span>
67
- ### 2. Required Configuration (Open External Listening)
68
+ ### 2. Configuration (Open External Listening)
68
69
 
69
70
  Because DSH defaults to `127.0.0.1`, ensure your `~/.dsh/profiles/web/cordis.patch.yml` includes the following configuration to allow external connectivity:
70
71
 
@@ -77,7 +78,11 @@ Because DSH defaults to `127.0.0.1`, ensure your `~/.dsh/profiles/web/cordis.pat
77
78
  host: '0.0.0.0'
78
79
  port: 3080
79
80
 
80
- # 2. Disable built-in remote plugin in @linxin666/dsh-web-ui-all (Required: prevent feature conflict)
81
+ # 2. (Recommended) Keep only ONE remote/Web access plugin to avoid duplicated entries.
82
+ # This plugin starts normally alongside other remote-access plugins: when it
83
+ # detects the shared pairing service is already taken by another plugin, it
84
+ # yields automatically and shows a warning banner at the top of its settings panel.
85
+ # Example: disable the legacy remote plugin bundled in @linxin666/dsh-web-ui-all
81
86
  - id: web-ui-remote-web-ui
82
87
  disabled: true
83
88
  ```
@@ -101,17 +106,19 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
101
106
 
102
107
  ### Desktop DSH Plugin Control Panel
103
108
 
104
- | Network Access & QR Pairing | Persistent Passwords, Device Sessions & IP Audit |
109
+ | Network Access & QR Pairing | Devices & Security |
105
110
  | :---: | :---: |
106
- | ![PC Settings - Network & QR](images/pc-dsh-setting-1.png) | ![PC Settings - Passwords & Devices](images/pc-dsh-setting-2.png) |
111
+ | ![PC Settings - Network & QR](images/pc-dsh-setting-1.png) | ![PC Settings - Devices & Security](images/pc-dsh-setting-2.png) |
112
+ | **Style Snippets Override** | **Local Data Storage** |
113
+ | ![PC Settings - Style Snippets](images/pc-dsh-setting-3.png) | ![PC Settings - Local Data Storage](images/pc-dsh-setting-4.png) |
107
114
 
108
115
  ---
109
116
 
110
117
  ### Mobile Live Demonstration (Same-Origin Auth & Full Workspace Control)
111
118
 
112
- | Mobile QR Auth & Login Page | Full DSH Workspace Control on Mobile |
113
- | :---: | :---: |
114
- | ![Mobile Auth Page](images/mobile-auth.jpg) | ![Mobile Workspace Control](images/mobile-dsh.jpg) |
119
+ | Login & Auth | Chat Font Scaling | Mobile Sidebar Styling | Official Trajectory / Trace |
120
+ | :---: | :---: | :---: | :---: |
121
+ | ![Login & Auth](images/mobile-auth.jpg) | ![Chat Font Scaling](images/mobile-dsh-1.jpg) | ![Mobile Sidebar Styling](images/mobile-dsh-2.jpg) | ![Official Trajectory / Trace](images/mobile-dsh-3.jpg) |
115
122
 
116
123
  ---
117
124
 
@@ -138,14 +145,31 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
138
145
  - Audit logs and lockout states persist to disk and restore across restarts;
139
146
  - Administrators can manually unlock blocked IPs with one click in the panel.
140
147
  - **Intelligent Static Asset Whitelisting**: The authentication gate automatically allows verified frontend asset extensions (`.js`, `.css`, `.png`, `.svg`, `.woff2`, etc., 20+ types) while strictly blocking extensionless or dynamic API routes (e.g. `/plugins/xxx/admin`), ensuring smooth third-party plugin rendering without manual whitelist configuration.
141
- - **Resilient Atomic Persistence & Debounce**: 500ms debounce write-throttling to safeguard disk I/O, coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for YAML configuration.
148
+ - **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.yaml` and `style-snippets.json`.
142
149
  - **Real Socket IP Extraction**: Relies strictly on underlying Socket connection addresses, preventing spoofed `X-Forwarded-For` header attacks.
150
+ - **Loopback CSRF Defense**: Mutating plugin APIs validate browser same-origin signals (`Origin` / `Sec-Fetch-Site`). Cross-site write requests to `127.0.0.1` driven by malicious webpages are rejected outright; non-browser clients (curl / local scripts) are unaffected.
151
+ - **No Context Laundering for Loopback**: Context virtualization applies only to external traffic. Loopback requests keep their original Host / Origin, so the built-in DNS-rebinding and same-origin checks of DSH keep working against external domains. The SSE event stream accepts same-origin connections only, with no cross-origin reads.
143
152
 
144
153
  ### 4. Real-Time Sync & Internationalization
145
154
  - **SSE Real-Time Push**: Push notifications for new device pairing, reconnections, revocations, and security alerts via Server-Sent Events, with bidirectional close listeners and idempotent cleanup.
146
155
  - **Adaptive Bilingual UI**: Automatically switches between English and Simplified Chinese based on DSH settings and browser language preferences.
147
156
  - **Non-HTTPS Compatibility Patch**: Injects `crypto.randomUUID` Polyfill into HTML templates to fix missing native Web Crypto APIs in HTTP non-secure mobile browser contexts.
148
157
 
158
+ ### 5. Mobile Style Snippets (style mini-plugins)
159
+
160
+ The DSH web UI still carries many desktop-oriented styles on phones. This plugin ships a built-in **Mobile Style Snippets** module that splits mobile adaptation into toggleable CSS snippets:
161
+
162
+ - **Three area-based built-in presets**: `preset-sidebar` (collapsed sidebar becomes a 0-width drawer with a draggable floating toggle), `preset-settings` (centered/scaled settings dialog with locked overlay scroll), `preset-main` (dense conversation typography with scrollable code blocks). Mobile on / PC off by default;
163
+ - **Separate PC / Mobile toggles (viewport-width based, device-independent)**: every preset and custom snippet has independent PC and Mobile switches; "Mobile" = applies at narrow viewports (≤900px) — **a PC browser with a narrow window gets it too**; "PC" = applies at wide viewports (>900px); both on = applies at all widths;
164
+ - **Custom snippets (your own mini-plugins)**: paste your CSS in **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets** to add a snippet, with edit/toggle/delete support. Persisted to `~/.dsh/remote-mobile/style-snippets.json`; changes apply on the next page load without restart;
165
+ - **UA-based marker + width-based styling**: styles apply by viewport-width band (see above), independent of device UA; mobile UA requests additionally add `data-dsh-mobile="1"` to `<html>` as a scope hook, and the draggable toggle script is injected for all clients (it only activates while the sidebar is collapsed).
166
+
167
+ ### 6. Generic Plugin Coexistence Protection
168
+
169
+ - **Conflict-free startup**: when the shared pairing service `remoteWebUiPairing` is already registered by another remote/Web access plugin, this plugin yields automatically (deferred arbitration: it waits until the activation window settles, then checks who owns the service name), completely avoiding the fatal whole-tree rollback caused by service-name collisions — installing from the plugin market works out of the box with zero manual configuration;
170
+ - **Visible coexistence status**: when a conflict is detected, a dismissible warning banner (Chinese/English) appears at the top of the settings panel, explaining the yield behavior and the "keep only one" recommendation; refresh after resolving and it disappears;
171
+ - **One-click diagnosis report**: the **"📋 Copy diagnosis & fix"** button in the banner copies a full report — the conflicting plugin's package name and loader entry id are auto-detected at runtime, and the report includes line-exact fix configuration and commands (disable the other / remove this one), ready to paste to an AI assistant.
172
+
149
173
  ---
150
174
 
151
175
  <span id="install"></span>
@@ -214,6 +238,33 @@ dsh-remote-mobile:
214
238
  lockDurationMs: 900000 # number, default 900000 (15 mins): IP lockout duration in ms
215
239
  ```
216
240
 
241
+ ### 🎨 Mobile Style Snippets (optional)
242
+
243
+ Custom snippets (style mini-plugins) and their enabled states are persisted in `~/.dsh/remote-mobile/style-snippets.json`. Manage them from the settings panel or edit the file directly:
244
+
245
+ ```json
246
+ {
247
+ "version": 2,
248
+ "presetStates": {
249
+ "preset-sidebar": { "pc": false, "mobile": true },
250
+ "preset-settings": { "pc": false, "mobile": true },
251
+ "preset-main": { "pc": true, "mobile": true }
252
+ },
253
+ "custom": [
254
+ {
255
+ "id": "custom-xxx",
256
+ "name": "My style mini-plugin",
257
+ "css": "html[data-dsh-mobile] .my-selector { ... }",
258
+ "pcEnabled": false,
259
+ "mobileEnabled": true
260
+ }
261
+ ],
262
+ "customOrder": ["custom-xxx"]
263
+ }
264
+ ```
265
+
266
+ > Enabled snippets are merged into a single `<style>` injected into the main page and the `/auth` login page; **effectiveness is decided by viewport width**: snippets with `mobileEnabled` are wrapped in `@media (max-width: 900px)` (apply at ≤900px — a narrow PC window gets them too), `pcEnabled` in `@media (min-width: 901px)`, and both on means all widths (v1 files migrate automatically; `presetStates` always persists all three preset groups with current effective values). Note: enabling the Sidebar Drawer preset on PC changes the desktop layout — turn its PC switch off if undesired.
267
+
217
268
  ---
218
269
 
219
270
  ## 📂 Local File Storage Locations
@@ -221,14 +272,27 @@ dsh-remote-mobile:
221
272
  | Path | Description | Security Level |
222
273
  |---|---|---|
223
274
  | `~/.dsh/settings.yaml` | Global security policies & bypass toggles | User R/W |
224
- | `~/.dsh/remote-mobile/devices.json` | Authorized devices, sessions & IP audit statistics | Local persistence |
275
+ | `~/.dsh/remote-mobile/devices.json` | Authorized devices, sessions & IP audit statistics (contains long-lived tokens, `0o600`) | Local storage, restricted to current user |
225
276
  | `~/.dsh/remote-mobile/rsa-keys.json` | Server RSA keypair (public & private) | Local storage, `0o600` (Restricted to current user) |
277
+ | `~/.dsh/remote-mobile/style-snippets.json` | Mobile style snippets (built-in toggles + custom CSS mini-plugins) | Local persistent storage |
226
278
 
227
279
  ---
228
280
 
229
281
  <span id="faq"></span>
230
282
  ## ❓ FAQ
231
283
 
284
+ <details>
285
+ <summary><b>Q0: Startup fails with <code>listen EADDRINUSE: address already in use 0.0.0.0:3080</code>?</b></summary>
286
+
287
+ **Answer**: This means **port 3080 is held by another process** (in most cases a previous `dsh web` instance has not fully exited — e.g. a hidden instance launched by a desktop shortcut, or overlapping old/new processes during restart). It is **unrelated to which plugins are installed**. Troubleshooting:
288
+
289
+ 1. Find the process holding the port: `lsof -nP -iTCP:3080` (Windows: `netstat -ano | findstr 3080`);
290
+ 2. Terminate the old instance (`kill <PID>` / Task Manager) and start `dsh web` again;
291
+ 3. Note: an instance launched via the desktop shortcut (dsh-desktop-launcher) and one started manually in a terminal cannot run at the same time.
292
+
293
+ If your log instead shows `service "remoteWebUiPairing" has been registered`, please upgrade this plugin to the latest version — newer versions ship with generic coexistence protection and can start normally alongside other remote-access plugins by yielding the service automatically, with a banner explaining the details in the settings panel.
294
+ </details>
295
+
232
296
  <details>
233
297
  <summary><b>Q1: Getting "Connection Refused" or cannot open page when scanning on mobile?</b></summary>
234
298
 
@@ -246,6 +310,22 @@ dsh-remote-mobile:
246
310
  * **LAN Bypass**: Inherent risks. Anyone connected to your Wi-Fi (including guests or unauthorized devices) can control your workspace. **Never enable this on untrusted networks**.
247
311
  </details>
248
312
 
313
+ <details>
314
+ <summary><b>Q4: Not happy with the mobile styles and want full control?</b></summary>
315
+
316
+ **A**: Open **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets**. First try toggling the three area presets (sidebar / settings / conversation), each with independent PC and Mobile switches; if that is not enough, paste your own CSS into the "Custom Snippets" area (e.g. `html[data-dsh-mobile] .xxx { ... }`). Refresh the mobile page to see the effect immediately. Everything lives in `~/.dsh/remote-mobile/style-snippets.json` and survives plugin upgrades.
317
+ </details>
318
+
319
+ <details>
320
+ <summary><b>Q5: What does the yellow banner "⚠️ Another remote-access plugin detected" at the top of the settings panel mean?</b></summary>
321
+
322
+ **Answer**: It means another remote/Web access plugin is enabled alongside this one and registered the shared pairing service first. To keep startup safe, this plugin has **automatically yielded** that service (its own security gateway and remote access are unaffected), but running two remote-access features side by side may cause duplicated entries. Keeping only one is recommended:
323
+
324
+ 1. Click **"📋 Copy diagnosis & fix"** in the banner and paste the report to an AI assistant (the report already identifies the conflicting plugin's package name and entry id, with exact fix configuration);
325
+ 2. To keep this plugin: follow the report to append the `disabled: true` lines for the other plugin at the end of `~/.dsh/profiles/web/cordis.patch.yml`, then restart DSH;
326
+ 3. The banner can be temporarily dismissed with ✕; it disappears after a refresh once the conflict is resolved.
327
+ </details>
328
+
249
329
  <details>
250
330
  <summary><b>Q3: Does mobile support creating and switching workspaces normally?</b></summary>
251
331
 
@@ -1,8 +1,8 @@
1
1
  import { generateKeyPairSync, privateDecrypt, constants, randomBytes, createHash, scryptSync, timingSafeEqual } from 'node:crypto';
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { dirname } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
- export const RSA_KEY_FILE = `${homedir()}/.dsh/remote-mobile/rsa-keys.json`;
5
+ export const RSA_KEY_FILE = join(homedir(), '.dsh', 'remote-mobile', 'rsa-keys.json');
6
6
  let keyPair = null;
7
7
  function loadOrCreateKeyPair() {
8
8
  try {
@@ -4,6 +4,21 @@ import type { SessionStore } from './token.js';
4
4
  * 判断请求是否来自本机回环
5
5
  */
6
6
  export declare function isLoopbackRequest(req: IncomingMessage): boolean;
7
+ /**
8
+ * 判断请求是否携带浏览器跨站信号(用于回环 CSRF / drive-by 防御)。
9
+ *
10
+ * Origin 与 Sec-Fetch-Site 由浏览器强制填写,页面脚本无法伪造或隐藏;
11
+ * 恶意网页驱使浏览器向本机回环发起的写请求必然携带这些信号。
12
+ * 不带这些头的客户端(curl / 本机脚本 / 旧内核 WebView)不受影响,保持向后兼容。
13
+ *
14
+ * 说明(有意取舍,非缺陷):
15
+ * - 仅比较 host:port,不比较 scheme:插件主要运行于 HTTP 局域网/回环场景,
16
+ * 同 host:port 下出现 HTTPS 恶意来源的现实可能性极低;
17
+ * - 无 Origin 时仅将 Sec-Fetch-Site: cross-site 判为跨站,same-site 不拦截:
18
+ * 回环场景无域名层级概念,子域场景不适用;收紧为「非 same-origin 即拦截」
19
+ * 会误伤不发送这些头的旧内核 WebView,与向后兼容目标冲突。
20
+ */
21
+ export declare function hasCrossSiteSignal(req: IncomingMessage): boolean;
7
22
  /**
8
23
  * 判断是否为无需鉴权的公开静态/登录路径
9
24
  */
package/lib/auth/gate.js CHANGED
@@ -7,6 +7,37 @@ export function isLoopbackRequest(req) {
7
7
  return ip === '127.0.0.1' || ip === '::1' || ip === 'localhost';
8
8
  }
9
9
  const STATIC_ASSET_EXT_REGEX = /\.(js|mjs|cjs|css|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|eot|otf|map|json|txt|wasm)$/i;
10
+ /** 插件私有变更类 API 前缀(非 GET 请求需做跨站信号校验) */
11
+ const MUTATION_API_PREFIX = '/api/remote-mobile/';
12
+ /**
13
+ * 判断请求是否携带浏览器跨站信号(用于回环 CSRF / drive-by 防御)。
14
+ *
15
+ * Origin 与 Sec-Fetch-Site 由浏览器强制填写,页面脚本无法伪造或隐藏;
16
+ * 恶意网页驱使浏览器向本机回环发起的写请求必然携带这些信号。
17
+ * 不带这些头的客户端(curl / 本机脚本 / 旧内核 WebView)不受影响,保持向后兼容。
18
+ *
19
+ * 说明(有意取舍,非缺陷):
20
+ * - 仅比较 host:port,不比较 scheme:插件主要运行于 HTTP 局域网/回环场景,
21
+ * 同 host:port 下出现 HTTPS 恶意来源的现实可能性极低;
22
+ * - 无 Origin 时仅将 Sec-Fetch-Site: cross-site 判为跨站,same-site 不拦截:
23
+ * 回环场景无域名层级概念,子域场景不适用;收紧为「非 same-origin 即拦截」
24
+ * 会误伤不发送这些头的旧内核 WebView,与向后兼容目标冲突。
25
+ */
26
+ export function hasCrossSiteSignal(req) {
27
+ const origin = req.headers.origin;
28
+ if (typeof origin === 'string' && origin) {
29
+ if (origin === 'null')
30
+ return true; // 沙箱 iframe 等场景的不透明 Origin,一律视为跨站
31
+ try {
32
+ const originHost = new URL(origin).host;
33
+ return originHost !== '' && originHost !== (req.headers.host || '');
34
+ }
35
+ catch {
36
+ return true; // 无法解析的 Origin 视为不可信
37
+ }
38
+ }
39
+ return req.headers['sec-fetch-site'] === 'cross-site';
40
+ }
10
41
  /**
11
42
  * 判断是否为无需鉴权的公开静态/登录路径
12
43
  */
@@ -36,6 +67,24 @@ export function createGlobalAuthGate(store) {
36
67
  next();
37
68
  return true;
38
69
  };
70
+ // 0. 变更类插件 API 的跨站写请求防御(先于回环放行,且运行在上下文虚拟化改写之前,
71
+ // 保证 Origin / Sec-Fetch-Site 为浏览器原始值):本机回环虽免认证,但恶意网页驱使
72
+ // 浏览器发起的跨站写请求(drive-by)必然携带跨站信号,在此直接拒绝。
73
+ const pathname = (req.url || '').split('?')[0];
74
+ if (pathname.startsWith(MUTATION_API_PREFIX) &&
75
+ req.method !== 'GET' &&
76
+ req.method !== 'HEAD' &&
77
+ hasCrossSiteSignal(req)) {
78
+ res.writeHead(403, {
79
+ 'Content-Type': 'application/json; charset=utf-8',
80
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
81
+ });
82
+ res.end(JSON.stringify({
83
+ error: 'Forbidden',
84
+ message: '已拦截来自其他网站的跨站写请求。',
85
+ }));
86
+ return false;
87
+ }
39
88
  // 1. 本机 127.0.0.1 回环无条件放行(保证本机 PC 始终可用)
40
89
  if (isLoopbackRequest(req)) {
41
90
  return pass();
package/lib/auth/token.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { randomBytes, randomInt } from 'node:crypto';
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
3
- import { dirname } from 'node:path';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { EventEmitter } from 'node:events';
6
6
  import { hashSecret, verifySecretHash } from './crypto.js';
7
7
  import { isTailscaleIp, isLanIp } from './tailscale.js';
8
8
  export const AUTH_COOKIE_NAME = 'dsh_mobile_token';
9
- export const DEFAULT_PERSIST_FILE = `${homedir()}/.dsh/remote-mobile/devices.json`;
10
- export const GLOBAL_SETTINGS_FILE = `${homedir()}/.dsh/settings.yaml`;
9
+ export const DEFAULT_PERSIST_FILE = join(homedir(), '.dsh', 'remote-mobile', 'devices.json');
10
+ export const GLOBAL_SETTINGS_FILE = join(homedir(), '.dsh', 'settings.yaml');
11
11
  /**
12
12
  * 从 ~/.dsh/settings.yaml 中安全读取 dsh-remote-mobile 命名空间的最新配置
13
13
  */
@@ -872,6 +872,11 @@ export class SessionStore extends EventEmitter {
872
872
  ipStats: Array.from(this.ipStats.values()),
873
873
  };
874
874
  writeFileSync(this.persistPath, JSON.stringify(payload, null, 2), 'utf8');
875
+ // 会话文件内含长效 Token(即完整凭证),权限与 rsa-keys.json 对齐收紧为仅当前用户可读写
876
+ try {
877
+ chmodSync(this.persistPath, 0o600);
878
+ }
879
+ catch { }
875
880
  }
876
881
  catch { }
877
882
  }