dsh-remote-mobile 1.4.3 → 1.6.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/lib/index.js CHANGED
@@ -15,8 +15,8 @@ import { SessionStore } from './auth/token.js';
15
15
  import { createGlobalAuthGate } from './auth/gate.js';
16
16
  import { createRoutes } from './routes/api.js';
17
17
  import { StyleSnippetStore } from './styles/style-snippets.js';
18
- import { getLocalTailscaleIp } from './auth/tailscale.js';
19
- import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, } from './bridge/compat.js';
18
+ import { getLocalTailscaleIp, getLocalLanIp } from './auth/tailscale.js';
19
+ import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, } from './bridge/compat.js';
20
20
  /** Cordis 插件名称标识 */
21
21
  export const name = 'dsh-remote-mobile';
22
22
  /** DSH 设置命名空间 */
@@ -98,18 +98,129 @@ export function apply(ctx, config = {}) {
98
98
  // 4. 向 Cordis 容器桥接注册 remoteWebUiPairing 服务(延迟裁决:与其他远程接入
99
99
  // 类插件共存时自动让出,保证启动不冲突,详见 compat.ts)
100
100
  registerRemoteWebUiPairingBridge(ctx, store);
101
- // 5. 延迟补丁与服务启动日志输出
102
- setTimeout(() => {
103
- if (ctx.webServer && ctx.webServer.server) {
104
- patchHttpServerWithVirtualizer(ctx.webServer.server, gateMiddleware, store, styleStore);
101
+ // 5. 透明接管 DSH 底座 connection 鉴权服务(短路官方 Token 401 阻断)
102
+ /**
103
+ * connection 鉴权接管采用三点冗余策略:
104
+ * 1) 插件 apply 时立即尝试 —— 覆盖 connection 先于本插件就绪的场景
105
+ * 2) ctx.inject 动态注入 —— 覆盖 connection 后于本插件就绪的场景
106
+ * 3) printReadyBanner 中兜底 —— 覆盖上述两者都未捕获到的极端时序
107
+ *
108
+ * takeOverConnectionAuth 内部通过 __dsh_rm_auth_takeover__ 标记保证幂等,
109
+ * 同一 connection 实例不会被重复接管。
110
+ */
111
+ const attachConnectionAuth = (conn) => {
112
+ if (conn) {
113
+ takeOverConnectionAuth(conn, store);
114
+ }
115
+ };
116
+ // 1) 优先通过 ctx.get 安全获取(未注入时不抛错返回 undefined)
117
+ try {
118
+ const conn = typeof ctx.get === 'function' ? ctx.get('connection') : undefined;
119
+ if (conn)
120
+ attachConnectionAuth(conn);
121
+ }
122
+ catch (e) {
123
+ ctx.logger?.debug?.('[dsh-remote-mobile] 尝试直接获取 connection 失败:', e);
124
+ }
125
+ // 2) 声明动态子注入,在 connection 服务就绪时接管
126
+ if (typeof ctx.inject === 'function') {
127
+ try {
128
+ ctx.inject(['connection'], (connCtx) => {
129
+ try {
130
+ // 统一通过 get 获取服务实例,避免属性访问与方法获取返回不同对象
131
+ const conn = typeof connCtx?.get === 'function' ? connCtx.get('connection') : connCtx?.connection;
132
+ if (conn)
133
+ attachConnectionAuth(conn);
134
+ }
135
+ catch (e) {
136
+ ctx.logger?.debug?.('[dsh-remote-mobile] inject 接收 connection 注入失败:', e);
137
+ }
138
+ });
139
+ }
140
+ catch (e) {
141
+ ctx.logger?.debug?.('[dsh-remote-mobile] 注册 connection 动态注入失败:', e);
142
+ }
143
+ }
144
+ // 6. 服务就绪时输出提示横幅与接入地址
145
+ let printed = false;
146
+ let disposed = false;
147
+ let timer1 = null;
148
+ let timer2 = null;
149
+ try {
150
+ ctx.on?.('dispose', () => {
151
+ disposed = true;
152
+ if (timer1)
153
+ clearTimeout(timer1);
154
+ if (timer2)
155
+ clearTimeout(timer2);
156
+ });
157
+ }
158
+ catch { }
159
+ const printReadyBanner = () => {
160
+ // 生命周期守卫:若上下文已废弃/进入 dispose,放弃回调
161
+ if (disposed || printed)
162
+ return;
163
+ printed = true;
164
+ try {
165
+ const webServer = (typeof ctx.get === 'function' ? ctx.get('webServer') : undefined) || ctx.webServer;
166
+ if (webServer?.server) {
167
+ patchHttpServerWithVirtualizer(webServer.server, gateMiddleware, store, styleStore);
168
+ }
169
+ try {
170
+ const conn = typeof ctx.get === 'function' ? ctx.get('connection') : undefined;
171
+ if (conn)
172
+ attachConnectionAuth(conn);
173
+ }
174
+ catch (e) {
175
+ ctx.logger?.debug?.('[dsh-remote-mobile] 就绪回调获取 connection 失败:', e);
176
+ }
177
+ const tsIp = getLocalTailscaleIp();
178
+ const lanIp = getLocalLanIp();
179
+ const port = webServer?.port ?? 3080;
180
+ const opts = store.getOptions();
181
+ const divider = '─'.repeat(72);
182
+ console.log(`\n┌${divider}`);
183
+ console.log(`│ [dsh-remote-mobile] 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`);
184
+ console.log(`│ 远程安全网关已就绪,支持手机浏览器访问:`);
185
+ console.log(`│ • 本机回环: http://127.0.0.1:${port}`);
186
+ if (lanIp && lanIp !== '127.0.0.1') {
187
+ console.log(`│ • 局域网: http://${lanIp}:${port} (同 Wi-Fi 局域网直连)`);
188
+ }
189
+ if (tsIp) {
190
+ console.log(`│ • 虚拟私网: http://${tsIp}:${port} (Tailscale 私网异地直连)`);
191
+ }
192
+ console.log(`│ 访问与配置说明:`);
193
+ console.log(`│ - 手机/远程设备打开对应地址即可直接访问`);
194
+ console.log(`│ - 在本机打开 Web 界面 ->「设置」->「远程与移动端」进行安全管理:`);
195
+ console.log(`│ 可开启免密直连(局域网/Tailscale)、查看 6 位临时配对码或设置长期密码`);
196
+ console.log(`│ - 项目文档: https://github.com/IceApriler/dsh-remote-mobile`);
197
+ console.log(`└${divider}\n`);
198
+ ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (上下文虚拟化桥接就绪,样式片段: ${styleStore.list().length} 个)`);
199
+ if (tsIp) {
200
+ ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:${port} (免密直连: ${opts.allowTailscale ? '已开启' : '已关闭'})`);
201
+ }
105
202
  }
106
- const tsIp = getLocalTailscaleIp();
107
- const opts = store.getOptions();
108
- ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (上下文虚拟化桥接就绪,样式片段: ${styleStore.list().length} 个)`);
109
- if (tsIp) {
110
- ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:3080 (免密直连: ${opts.allowTailscale ? '已开启' : '已关闭'})`);
203
+ catch (e) {
204
+ // 上下文若在并发或极端时序下重载,静默退出,防止未捕获异常导致主进程崩溃
111
205
  }
112
- }, 300);
206
+ };
207
+ // 1) 优先在 loader 插件树就绪后紧跟官方提示打印
208
+ try {
209
+ const loader = typeof ctx.get === 'function' ? ctx.get('loader') : undefined;
210
+ const settled = loader?.await?.();
211
+ if (settled && typeof settled.then === 'function') {
212
+ settled.then(() => {
213
+ timer1 = setTimeout(printReadyBanner, 120);
214
+ }).catch((e) => {
215
+ ctx.logger?.debug?.('[dsh-remote-mobile] loader await 异常:', e);
216
+ });
217
+ }
218
+ }
219
+ catch (e) {
220
+ ctx.logger?.debug?.('[dsh-remote-mobile] 获取 loader 失败:', e);
221
+ }
222
+ // 2) 兜底定时器(避免无 loader 环境时不输出)
223
+ timer2 = setTimeout(printReadyBanner, 1500);
113
224
  }
114
225
  // 导出所有子模块类型与核心工具
115
226
  export * from './auth/tailscale.js';
@@ -116,6 +116,40 @@ const PRESET_SIDEBAR_CSS = `
116
116
  background: rgb(0 0 0 / 24%);
117
117
  }
118
118
 
119
+ /* 展开态自适应宽度:支持 360px 窄屏与大屏手机灵活伸缩,覆盖官方内联 280px */
120
+ [class*="_sidebarCol"]:not(:has([class*="_collapsed"])) {
121
+ width: min(85vw, 320px) !important;
122
+ max-width: 85vw !important;
123
+ }
124
+
125
+ /* 条目触控热区友好化:行高 44px + 次级操作按钮 36px,防误触相邻会话并易于点击更多操作 */
126
+ [class*="_sidebarCol"] [role="treeitem"],
127
+ [class*="_sidebarCol"] [data-dsh-part="sidebar-entry"] {
128
+ min-height: 44px !important;
129
+ }
130
+ [class*="_sidebarCol"] [role="treeitem"] button,
131
+ [class*="_sidebarCol"] [role="treeitem"] [role="button"] {
132
+ min-width: 36px !important;
133
+ min-height: 36px !important;
134
+ }
135
+
136
+ /* 触屏设备没有 hover:工作区/会话右侧操作按钮(更多操作 ⋯、新建会话 + 等)
137
+ 在 PC 依赖鼠标悬停显现,移动端必须常显,保证触屏用户可随时直接交互 */
138
+ [class*="_sidebarCol"] [class*="_rowActions"] {
139
+ opacity: 1 !important;
140
+ visibility: visible !important;
141
+ display: flex !important;
142
+ align-items: center !important;
143
+ flex-shrink: 0 !important;
144
+ }
145
+
146
+ /* 标题容器弹性自适应并防文字挤出视野,超长文本自然截断不破坏按钮布局 */
147
+ [class*="_sidebarCol"] [class*="_projectText"],
148
+ [class*="_sidebarCol"] [class*="_sessionText"] {
149
+ min-width: 0 !important;
150
+ flex: 1 1 auto !important;
151
+ }
152
+
119
153
  /* 内层根节点铺满抽屉:覆盖官方响应式脚本写入的内联 width,
120
154
  消除「外层 ~320 / 内层内联 280」的宽度差与右缘空白条 */
121
155
  [class*="_sidebarCol"] > [data-slot="sidebar"] > div {
@@ -144,15 +178,16 @@ const PRESET_SIDEBAR_CSS = `
144
178
  display: none !important;
145
179
  }
146
180
 
147
- /* 折叠状态下,Logo 按钮悬浮在屏幕左上角作为展开把手(与 dsh-draggable-nav 拖拽脚本联动) */
181
+ /* 折叠状态下,Logo 按钮悬浮在屏幕左上角作为展开把手(与 dsh-draggable-nav 拖拽脚本联动)
182
+ 放大到 44px 触控尺寸,并避让刘海屏/灵动岛顶部安全区 */
148
183
  [class*="_sidebarCol"] [class*="_collapsed"] [class*="_logoRow"],
149
184
  [class*="_frame"][data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_logoRow"] {
150
185
  position: fixed !important;
151
- top: 10px !important;
152
- left: 10px !important;
153
- width: 40px !important;
154
- height: 40px !important;
155
- border-radius: 10px !important;
186
+ top: max(12px, env(safe-area-inset-top)) !important;
187
+ left: 12px !important;
188
+ width: 44px !important;
189
+ height: 44px !important;
190
+ border-radius: 12px !important;
156
191
  background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.95)) !important;
157
192
  backdrop-filter: blur(12px) !important;
158
193
  -webkit-backdrop-filter: blur(12px) !important;
@@ -197,8 +232,17 @@ const PRESET_SIDEBAR_CSS = `
197
232
  min-height: 0 !important;
198
233
  }
199
234
  }
235
+
236
+ /* 8. 兼容处理:隐藏生态插件中重复挂载的小鲸鱼悬浮按钮 (#dshRemoteWhale) 避免入口与手势冲突 */
237
+ #dshRemoteWhale,
238
+ button[id="dshRemoteWhale"],
239
+ [data-dsh-plugin="remote-web-ui"] {
240
+ display: none !important;
241
+ pointer-events: none !important;
242
+ visibility: hidden !important;
243
+ }
200
244
  `;
201
- /** 设置面板预设:官方设置弹窗整体微缩 + 遮罩锁滚动 + 内容横向滑动 */
245
+ /** 设置面板预设:官方设置弹窗上下堆叠 + 顶部导航横滑 + 内容纵向自然滚览 */
202
246
  const PRESET_SETTINGS_CSS = `
203
247
  /* ===== dsh-remote-mobile · 设置面板适配 ===== */
204
248
  /* 注:窄屏媒体查询由注入层按端包裹 */
@@ -217,38 +261,9 @@ const PRESET_SETTINGS_CSS = `
217
261
  overscroll-behavior: contain !important;
218
262
  }
219
263
 
220
- /* 移动端设置弹窗顶部固定提示:内容较宽(530px 等比微缩)需左右滑动浏览。
221
- 挂在 overlay 容器 ::before 上(absolute 定位相对 fixed overlay),
222
- 不随弹窗内容滚动,也不参与 flex 居中布局。内测声明等通用 Modal
223
- (_root 容器,不在本选择器集合内)不会出现此提示。 */
224
- [data-slot*="overlay"]:has([role="dialog"])::before,
225
- [class*="_overlay"]:has([role="dialog"])::before,
226
- [class*="_modalWrapper"]:has([role="dialog"])::before {
227
- content: "↔ 左右滑动浏览设置";
228
- position: absolute;
229
- top: 16px;
230
- left: 50%;
231
- transform: translateX(-50%);
232
- z-index: 100006 !important;
233
- box-sizing: border-box;
234
- white-space: nowrap;
235
- max-width: calc(100vw - 32px);
236
- overflow: hidden;
237
- text-overflow: ellipsis;
238
- background: rgba(0, 0, 0, 0.62);
239
- color: #ffffff;
240
- font-size: 12px;
241
- line-height: 18px;
242
- border-radius: 999px;
243
- padding: 4px 14px;
244
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
245
- pointer-events: none;
246
- text-align: center;
247
- }
248
-
249
264
  /* 弹窗卡片:仅命中设置弹窗——外层 overlay 容器内含 role=dialog(设置弹窗由
250
265
  data-slot overlay / overlay 类包裹)。内测声明等通用 Modal 直接挂在 body、
251
- 无 data-slot overlay,天然排除,不会被 94vw / 530px 覆写。 */
266
+ 无 data-slot overlay,天然排除。卡片设为纵向滚动,取消横向滑动。 */
252
267
  [data-slot*="overlay"]:has([role="dialog"]) [role="dialog"],
253
268
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"],
254
269
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"],
@@ -260,25 +275,22 @@ const PRESET_SETTINGS_CSS = `
260
275
  [class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"],
261
276
  [class*="_settingsModal"] {
262
277
  position: relative !important;
263
- width: 94vw !important;
264
- max-width: 94vw !important;
265
- height: 86vh !important;
278
+ width: 96vw !important;
279
+ max-width: 96vw !important;
266
280
  height: 86dvh !important;
267
- max-height: 88vh !important;
268
- max-height: 88dvh !important;
281
+ max-height: 86dvh !important;
269
282
  margin: auto !important;
270
283
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
271
- border-radius: 16px !important;
284
+ border-radius: 14px !important;
272
285
  background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
273
- overflow-x: auto !important;
274
- overflow-y: hidden !important;
286
+ overflow-x: hidden !important;
287
+ overflow-y: auto !important;
275
288
  -webkit-overflow-scrolling: touch !important;
276
289
  overscroll-behavior: contain !important;
277
- font-size: 13.5px !important;
290
+ font-size: 14px !important;
278
291
  }
279
292
 
280
- /* 弹窗内部等比微缩:dialog 的全部直接子元素(如设置弹窗的 nav 左侧按钮列表 +
281
- content 右侧内容)统一 zoom 0.88,保证整窗一致缩放。 */
293
+ /* 取消 zoom 微缩,恢复正常比例 */
282
294
  [data-slot*="overlay"]:has([role="dialog"]) [role="dialog"] > *,
283
295
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"] > *,
284
296
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"] > *,
@@ -289,12 +301,10 @@ const PRESET_SETTINGS_CSS = `
289
301
  [class*="_modalWrapper"]:has([role="dialog"]) [class*="_dialog"] > *,
290
302
  [class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"] > *,
291
303
  [class*="_settingsModal"] > * {
292
- zoom: 0.88 !important;
304
+ zoom: 1 !important;
293
305
  }
294
306
 
295
- /* 弹窗右侧内容主体(直接 div 子元素):固定最小宽度并铺满高度,单屏显示更多
296
- 内容且字号更精致。左侧导航列表不强制宽度,保持官方窄宽。
297
- 同样限定在设置弹窗上下文内,避免波及通用 Modal。 */
307
+ /* 取消 530px 定宽:主体内容 div 铺满宽度,高度自然自适应 */
298
308
  [data-slot*="overlay"]:has([role="dialog"]) [role="dialog"] > div,
299
309
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"] > div,
300
310
  [data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"] > div,
@@ -305,9 +315,41 @@ const PRESET_SETTINGS_CSS = `
305
315
  [class*="_modalWrapper"]:has([role="dialog"]) [class*="_dialog"] > div,
306
316
  [class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"] > div,
307
317
  [class*="_settingsModal"] > div {
308
- min-width: 530px !important;
309
- width: 530px !important;
310
- height: 100% !important;
318
+ min-width: 0 !important;
319
+ width: 100% !important;
320
+ height: auto !important;
321
+ }
322
+
323
+ /* 上下堆叠:只命中带 nav 的设置类弹窗,通用 Modal 不受影响 */
324
+ [role="dialog"]:has(nav) {
325
+ flex-direction: column !important;
326
+ }
327
+ [role="dialog"]:has(nav) > nav {
328
+ width: 100% !important;
329
+ flex: none !important;
330
+ }
331
+ [role="dialog"] [class*="_navTitle"] {
332
+ display: none !important;
333
+ }
334
+ [role="dialog"] [class*="_navList"] {
335
+ display: flex !important;
336
+ flex-direction: row !important;
337
+ overflow-x: auto !important;
338
+ overflow-y: hidden !important;
339
+ gap: 4px !important;
340
+ padding: 4px 2px !important;
341
+ -webkit-overflow-scrolling: touch !important;
342
+ }
343
+ [role="dialog"] [class*="_navList"] > * {
344
+ flex: none !important;
345
+ white-space: nowrap !important;
346
+ }
347
+ /* 移动端收紧官方内容区的厚 padding(原 24px/32px),释放横向显示空间 */
348
+ [role="dialog"] [class*="_content"] {
349
+ width: 100% !important;
350
+ min-width: 0 !important;
351
+ flex: 1 1 auto !important;
352
+ padding: 10px 4px !important;
311
353
  }
312
354
 
313
355
  /* 重置插件市场等弹窗在窄屏隐藏左侧 nav 的行为:保持导航列表可见。
@@ -616,8 +658,8 @@ export const BUILTIN_PRESETS = [
616
658
  id: 'preset-settings',
617
659
  name: { zh: '设置面板适配', en: 'Settings Panel' },
618
660
  description: {
619
- zh: '官方设置弹窗居中微缩显示更多内容,遮罩锁死滚动,内部支持左右横滑。',
620
- en: 'Centers and scales down the settings dialog, locks overlay scroll, and enables horizontal panning.',
661
+ zh: '官方设置弹窗上下堆叠自适应浏览:顶部导航横滑切换,内容纵向自然滚览,告别横向截断。',
662
+ en: 'Stacks settings dialog vertically: horizontal scrollable nav tabs at top, vertical flowing content.',
621
663
  },
622
664
  defaultPcEnabled: false,
623
665
  defaultMobileEnabled: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-mobile",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "DeepSeek Harness 远程与移动端安全网关插件:零修改 DSH 底层代码安全开放局域网与 Tailscale 连接 | DeepSeek Harness (DSH) Remote & Mobile Security Guard: safely opens Tailscale/LAN with zero core modifications, QR scan auth, RSA encryption & brute-force defense.",
5
5
  "keywords": [
6
6
  "dsh",