dsh-pocket 2.1.4 → 2.3.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/README.en.md CHANGED
@@ -132,6 +132,7 @@ Note: in named-tunnel mode the public PIN is **not auto-rotated** (the address i
132
132
  | `ERR_PNPM_ADDING_TO_ROOT` | pnpm 9 workspace-root restriction: append `-w` (`--workspace-root`) to install/update commands |
133
133
  | Nothing changed after install/update | **You must restart `dsh web`**; the running process still loads the old code |
134
134
  | `listen EADDRINUSE ... :3081` | A stale dsh-pocket process holds the port: macOS/Linux `lsof -ti :3081 \| xargs kill -9`; Windows `netstat -ano \| findstr :3081` (find the LISTENING PID) → `taskkill /PID <PID> /F`, then retry |
135
+ | Want a different port (issue #70) | Plugin mode: write `"proxyPort": 3082` into `$DSH_HOME/dsh-pocket/settings.json` and restart `dsh web`. CLI mode: `dsh-pocket --port 3082`. If the port is taken you'll get `EADDRINUSE` — kill the old process or pick another one |
135
136
  | Version stuck below 1.x | `^0.x` ranges never jump to 1.x: update with `--latest` (`dsh plugin --profile web update dsh-pocket --latest -w`) |
136
137
  | Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
137
138
  | After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it: macOS/Linux `lsof -ti :3080 \| xargs kill -9`; Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
package/README.md CHANGED
@@ -132,6 +132,7 @@ npx @deepseek-ai/dsh web
132
132
  | `ERR_PNPM_ADDING_TO_ROOT` | pnpm 9 对 workspace 根的限制:安装/更新命令**末尾加 `-w`**(`--workspace-root`) |
133
133
  | 装完/更新了但界面没变化 | **必须重启 `dsh web`** 才生效;运行中的进程仍加载旧代码 |
134
134
  | `listen EADDRINUSE ... :3081` | 旧 dsh-pocket 进程还占着端口:macOS/Linux `lsof -ti :3081 \| xargs kill -9`;Windows `netstat -ano \| findstr :3081`(找 LISTENING 的 PID)→ `taskkill /PID <PID> /F`,后重试 |
135
+ | 想换端口(issue #70) | 插件模式:在 `$DSH_HOME/dsh-pocket/settings.json` 写 `"proxyPort": 3082` 后重启 `dsh web`。CLI 模式:`dsh-pocket --port 3082`。端口被占会报 `EADDRINUSE`,杀掉旧进程或换一个端口 |
135
136
  | 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
136
137
  | 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
137
138
  | 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:macOS/Linux `lsof -ti :3080 \| xargs kill -9`;Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
package/client/client.js CHANGED
@@ -1231,8 +1231,44 @@ var en = {
1231
1231
  "files": "Files"
1232
1232
  };
1233
1233
 
1234
+ // client/mobile/layout-mode.mjs
1235
+ function resolveLayout({ urlValue, stored, narrowMatch }) {
1236
+ const url = String(urlValue ?? "").trim();
1237
+ if (url === "desktop") return "desktop";
1238
+ if (url === "mobile") return "mobile";
1239
+ if (stored === "desktop" || stored === "mobile") return stored;
1240
+ return narrowMatch ? "mobile" : "desktop";
1241
+ }
1242
+ function persistLayoutFromUrl(urlValue) {
1243
+ if (typeof localStorage === "undefined") return "";
1244
+ const v = String(urlValue ?? "").trim();
1245
+ try {
1246
+ if (v === "desktop" || v === "mobile") localStorage.setItem("dsh-pocket.layout", v);
1247
+ else if (v === "auto" || v === "") localStorage.removeItem("dsh-pocket.layout");
1248
+ } catch {
1249
+ }
1250
+ try {
1251
+ const s = localStorage.getItem("dsh-pocket.layout");
1252
+ return s === "desktop" || s === "mobile" ? s : "";
1253
+ } catch {
1254
+ return "";
1255
+ }
1256
+ }
1257
+
1234
1258
  // client/mobile/mobile-apply.tsx
1235
1259
  function mobileApply(ctx) {
1260
+ const urlValue = new URL(window.location.href).searchParams.get("dsh-layout") ?? "";
1261
+ const narrowMQ = window.matchMedia("(max-width: 1023px)");
1262
+ const stored = persistLayoutFromUrl(urlValue);
1263
+ const layout = resolveLayout({ urlValue, stored, narrowMatch: narrowMQ.matches });
1264
+ document.body?.setAttribute("data-dsh-pocket-layout", layout);
1265
+ if (layout === "desktop") return;
1266
+ let narrow = narrowMQ;
1267
+ if (layout === "mobile") {
1268
+ narrow = { matches: true, addEventListener: () => {
1269
+ }, removeEventListener: () => {
1270
+ } };
1271
+ }
1236
1272
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-mobile-nav: dictionaries");
1237
1273
  ctx.effect(() => {
1238
1274
  const tag = document.createElement("style");
@@ -1245,7 +1281,6 @@ function mobileApply(ctx) {
1245
1281
  };
1246
1282
  }, "dsh-mobile-nav: styles");
1247
1283
  ctx.effect(() => {
1248
- const narrow = window.matchMedia("(max-width: 1023px)");
1249
1284
  const viewport = document.querySelector('meta[name="viewport"]');
1250
1285
  const originalViewport = viewport?.content ?? "";
1251
1286
  const themeMeta = document.createElement("meta");
@@ -1277,7 +1312,6 @@ function mobileApply(ctx) {
1277
1312
  };
1278
1313
  }, "dsh-mobile-nav: status bar theme + viewport + zoom guard");
1279
1314
  ctx.effect(() => {
1280
- const narrow = window.matchMedia("(max-width: 1023px)");
1281
1315
  if (!narrow.matches) return () => {
1282
1316
  };
1283
1317
  const onChevronClick = (event) => {
@@ -1289,7 +1323,6 @@ function mobileApply(ctx) {
1289
1323
  return () => document.removeEventListener("click", onChevronClick, true);
1290
1324
  }, "dsh-mobile-nav: aionui explorer close marker");
1291
1325
  ctx.effect(() => {
1292
- const narrow = window.matchMedia("(max-width: 1023px)");
1293
1326
  if (!narrow.matches) return () => {
1294
1327
  };
1295
1328
  const frame = () => document.querySelector('[data-mobile-nav="frame"]');
@@ -1307,7 +1340,6 @@ function mobileApply(ctx) {
1307
1340
  };
1308
1341
  }, "dsh-mobile-nav: explorer availability (issue #48)");
1309
1342
  ctx.effect(() => {
1310
- const narrow = window.matchMedia("(max-width: 1023px)");
1311
1343
  if (!narrow.matches) return () => {
1312
1344
  };
1313
1345
  const frame = () => document.querySelector('[data-mobile-nav="frame"]');
@@ -1332,7 +1364,6 @@ function mobileApply(ctx) {
1332
1364
  };
1333
1365
  }, "dsh-mobile-nav: preview sheet open marker");
1334
1366
  ctx.effect(() => {
1335
- const narrow = window.matchMedia("(max-width: 1023px)");
1336
1367
  if (!narrow.matches) return () => {
1337
1368
  };
1338
1369
  const moveTps = (stats) => {
@@ -1366,7 +1397,6 @@ function mobileApply(ctx) {
1366
1397
  };
1367
1398
  }, "dsh-mobile-nav: stats line marker");
1368
1399
  ctx.effect(() => {
1369
- const narrow = window.matchMedia("(max-width: 1023px)");
1370
1400
  if (!narrow.matches) return () => {
1371
1401
  };
1372
1402
  const cols = ["[data-aionui-explorer-col]", "[data-aionui-preview-col]"];
@@ -0,0 +1,46 @@
1
+ // 布局模式判定(issue #74:宽屏手机/Pad 可选电脑布局)
2
+ //
3
+ // 优先级:URL 参数 (?dsh-layout=desktop|mobile|auto) > localStorage > 默认 auto。
4
+ // - 'auto' 不写 localStorage,仅按 matchMedia 走;
5
+ // - 显式 'desktop' / 'mobile' 同步到 localStorage,下次无 URL 参数也走同样布局。
6
+ //
7
+ // 纯函数,便于单测;mobile-apply.tsx 读 DOM/localStorage 喂进来。
8
+
9
+ /** @typedef {'mobile' | 'desktop'} ForcedLayout */
10
+
11
+ /** 解析输入。 */
12
+ export function resolveLayout({ urlValue, stored, narrowMatch }) {
13
+ const url = String(urlValue ?? '').trim();
14
+ if (url === 'desktop') return 'desktop';
15
+ if (url === 'mobile') return 'mobile';
16
+ // 'auto' / 空 / 非法值 → 用 localStorage / matchMedia
17
+ if (stored === 'desktop' || stored === 'mobile') return stored;
18
+ return narrowMatch ? 'mobile' : 'desktop';
19
+ }
20
+
21
+ /** 同步 localStorage 的副作用(在 mobileApply 入口跑一次)。返回最终存储值。 */
22
+ export function persistLayoutFromUrl(urlValue) {
23
+ if (typeof localStorage === 'undefined') return '';
24
+ const v = String(urlValue ?? '').trim();
25
+ try {
26
+ if (v === 'desktop' || v === 'mobile') localStorage.setItem('dsh-pocket.layout', v);
27
+ else if (v === 'auto' || v === '') localStorage.removeItem('dsh-pocket.layout');
28
+ } catch { /* 隐私模式/无 storage → 静默 */ }
29
+ try {
30
+ const s = localStorage.getItem('dsh-pocket.layout');
31
+ return s === 'desktop' || s === 'mobile' ? s : '';
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ /** 读 localStorage 当前的 layout 值('desktop' | 'mobile' | '')。 */
38
+ export function readStoredLayout() {
39
+ if (typeof localStorage === 'undefined') return '';
40
+ try {
41
+ const v = localStorage.getItem('dsh-pocket.layout');
42
+ return v === 'desktop' || v === 'mobile' ? v : '';
43
+ } catch {
44
+ return '';
45
+ }
46
+ }
@@ -6,6 +6,7 @@ import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
6
6
  import { MOBILE_CSS } from './mobile.css.ts'
7
7
  import { NS, en, zh } from './locales.ts'
8
8
  import type { MobileNavKey } from './locales.ts'
9
+ import { resolveLayout, persistLayoutFromUrl } from './layout-mode.mjs'
9
10
 
10
11
  declare module '@deepseek-ai/dsh-client-ui-slots' {
11
12
  interface LocaleNamespaceMap {
@@ -23,6 +24,22 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
23
24
  * @param ctx - client root context.
24
25
  */
25
26
  export function mobileApply(ctx): void {
27
+ // 布局模式(issue #74):URL 参数 > localStorage > auto(=matchMedia)。
28
+ // desktop 模式(宽屏手机/平板强制电脑布局)下整段 mobile 效果都不挂——
29
+ // 不加 styles、不挂 slots、不跑 effects,直接走 DSH 原生桌面 UI。
30
+ const urlValue = new URL(window.location.href).searchParams.get('dsh-layout') ?? '';
31
+ const narrowMQ = window.matchMedia('(max-width: 1023px)');
32
+ const stored = persistLayoutFromUrl(urlValue);
33
+ const layout = resolveLayout({ urlValue, stored, narrowMatch: narrowMQ.matches });
34
+ document.body?.setAttribute('data-dsh-pocket-layout', layout);
35
+ if (layout === 'desktop') return;
36
+ // 强制 mobile:narrow 永远 true;宽度变化不再切换(用户已显式选 mobile)
37
+ // auto 模式:narrow 是真实的 matchMedia,宽度变化会触发 effect 挂载/卸载
38
+ let narrow: MediaQueryList = narrowMQ;
39
+ if (layout === 'mobile') {
40
+ narrow = { matches: true, addEventListener: () => {}, removeEventListener: () => {} } as MediaQueryList;
41
+ }
42
+
26
43
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-mobile-nav: dictionaries')
27
44
 
28
45
  ctx.effect(() => {
@@ -52,7 +69,6 @@ export function mobileApply(ctx): void {
52
69
  // zoom; modern browsers are covered by the stylesheet's
53
70
  // touch-action: manipulation (which keeps pan and pinch zoom).
54
71
  ctx.effect(() => {
55
- const narrow = window.matchMedia('(max-width: 1023px)')
56
72
  const viewport = document.querySelector<HTMLMetaElement>('meta[name="viewport"]')
57
73
  const originalViewport = viewport?.content ?? ''
58
74
  const themeMeta = document.createElement('meta')
@@ -96,7 +112,6 @@ export function mobileApply(ctx): void {
96
112
  // sheet's own collapse chevron is tapped, so closing is symmetric with
97
113
  // opening.
98
114
  ctx.effect(() => {
99
- const narrow = window.matchMedia('(max-width: 1023px)')
100
115
  if (!narrow.matches) return () => {}
101
116
  const onChevronClick = (event: MouseEvent) => {
102
117
  const target = event.target as HTMLElement | null
@@ -115,7 +130,6 @@ export function mobileApply(ctx): void {
115
130
  // `data-mobile-nav-explorer="1|0"` so the stylesheet can hide the entries
116
131
  // on hosts without it (dsh-web-ui installs keep the feature).
117
132
  ctx.effect(() => {
118
- const narrow = window.matchMedia('(max-width: 1023px)')
119
133
  if (!narrow.matches) return () => {}
120
134
  const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
121
135
  const check = () => {
@@ -141,7 +155,6 @@ export function mobileApply(ctx): void {
141
155
  // whenever the suite hides the column again (collapse chevron / tab
142
156
  // close), so a restored-but-unwanted sheet never appears.
143
157
  ctx.effect(() => {
144
- const narrow = window.matchMedia('(max-width: 1023px)')
145
158
  if (!narrow.matches) return () => {}
146
159
  const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
147
160
  const onTap = (event: MouseEvent) => {
@@ -173,7 +186,6 @@ export function mobileApply(ctx): void {
173
186
  // marked row out as ONE horizontally scrolling line with every metric
174
187
  // reachable.
175
188
  ctx.effect(() => {
176
- const narrow = window.matchMedia('(max-width: 1023px)')
177
189
  if (!narrow.matches) return () => {}
178
190
  // The composer root renders the TPS readout ("TPS 89.4 tok/s") as its
179
191
  // own row BELOW the status strip; fold it into the strip so every
@@ -218,7 +230,6 @@ export function mobileApply(ctx): void {
218
230
  // with the Web Animations API each time a column turns visible, then
219
231
  // leave the resting state to the stylesheet.
220
232
  ctx.effect(() => {
221
- const narrow = window.matchMedia('(max-width: 1023px)')
222
233
  if (!narrow.matches) return () => {}
223
234
  const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]']
224
235
  const seen = new Map<string, boolean>()
package/lib/index.js CHANGED
@@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs';
21
21
  import { installPocketRpc } from './web-rpc.js';
22
22
  import { restartHost } from './restart.js';
23
23
  import { advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
24
- import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings } from './settings.mjs';
24
+ import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort } from './settings.mjs';
25
25
 
26
26
  const name = 'dsh-pocket';
27
27
  const inject = ['connection', 'webServer'];
@@ -246,7 +246,9 @@ export function apply(ctx, config = {}, internals = {}) {
246
246
 
247
247
  const service = internals.service ?? createPocketService({
248
248
  dshPort,
249
- port: internals.port ?? config.port ?? 3081,
249
+ // 端口(issue #70):插件模式优先级 settings.proxyPort > cordis patch config.port > 3081
250
+ // 3081 被占时 createPocketService 内部 EADDRINUSE 抛错,按 DSH 插件启动失败处理(用户改端口重启即可)。
251
+ port: internals.port ?? config.port ?? (proxyPort() || 3081),
250
252
  home: internals.home,
251
253
  internals,
252
254
  getLanIpOverride: () => lanIpOverride(),
package/lib/settings.mjs CHANGED
@@ -192,3 +192,23 @@ export function setTunnelHostname(value) {
192
192
  writeSettings(s);
193
193
  return v;
194
194
  }
195
+
196
+ // ---------- 代理端口(issue #70) ----------
197
+ // 局域网代理的监听端口(默认 3081)。插件模式下唯一改法就是写 settings.json 的
198
+ // proxyPort 字段(CLI 模式可用 dsh-pocket --port)。允许范围 1-65535;
199
+ // 端口已被占用时 dsh web 启动会直接抛 EADDRINUSE(保持原行为),不必在 setter 校验。
200
+ /** 当前代理端口(0 = 用默认 3081)。 */
201
+ export function proxyPort() {
202
+ const v = Number(readSettings().proxyPort);
203
+ return Number.isInteger(v) && v >= 1 && v <= 65535 ? v : 0;
204
+ }
205
+
206
+ /** 设置代理端口(持久化)。空/0/非法值清除,回退默认 3081。 */
207
+ export function setProxyPort(value) {
208
+ const n = Number(value);
209
+ const s = readSettings();
210
+ if (Number.isInteger(n) && n >= 1 && n <= 65535) s.proxyPort = n;
211
+ else delete s.proxyPort;
212
+ writeSettings(s);
213
+ return proxyPort();
214
+ }
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.1.4"
83
+ "version": "2.3.0"
84
84
  }