dsh-in-hand 1.7.4 → 1.7.6

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.md CHANGED
@@ -227,6 +227,31 @@ PDF / ZIP / MP4)。这是**按内容识别**,不是"按内容放行" ——
227
227
  进行中 / 完成 / 失败三态。这是"看得懂进度"最关键的一处。
228
228
  - **逐字输出只改一个节点**。整页重建会让长会话每来一个字都重排一次。
229
229
 
230
+ ### 对话排版与子代理二级页(v1.7.6)
231
+
232
+ 手机只有 360–390px 宽,对话里最容易坏的是三样东西:表格、长代码、长 URL。v1.7.6
233
+ 把表格渲染成真表格并放进可横滑的 `.tablewrap`,代码块自己横滑,长 token 在分隔符后
234
+ 断行;页面级 `scrollWidth` 在 390 和 360 下都等于视口宽。助手正文、流式回复和工具卡
235
+ 统一 100% 宽、左对齐;用户消息仍靠右。
236
+
237
+ 子代理不再混在主对话里:工具条出现「子代理 N」,点进去是独立列表,再点开只读快照;
238
+ 返回回到原来的根会话。没有子代理时入口不出现。
239
+
240
+ | 改前(表格是竖线文本) | 改后(真表格 / 可横滑代码) |
241
+ | --- | --- |
242
+ | ![改前](docs/screenshots/v1.7.6-before.png) | ![改后](docs/screenshots/v1.7.6-light.png) |
243
+
244
+ 暗色与子代理列表:
245
+
246
+ ![暗色](docs/screenshots/v1.7.6-dark.png)
247
+
248
+ ![子代理列表](docs/screenshots/v1.7.6-subagents.png)
249
+
250
+ 截图是 `docs/demo.html` 的真实渲染(Chrome headless + CDP,DPR2),子代理列表用演示数据;
251
+ 流式截图里的文字是注入的 mock。回归自检:`npm run check` 里的
252
+ `scripts/check-mobile.mjs` 会在 390 / 360 两个宽度下跑 `window.twSelfCheck()`、切会话
253
+ 残留断言与左基线断言。
254
+
230
255
  ### 页面为什么不做整页重建
231
256
 
232
257
  页面每 3 秒轮询一次,但**数据没变就完全不碰 DOM**,只刷新右上角时间戳。首版是
@@ -330,6 +355,42 @@ dsh plugin --profile web add <你的绝对路径>/dsh-in-hand
330
355
  (由 profile 的 `node_modules` 解析),而 profile 自带补丁里若写路径,必须是绝对路径
331
356
  (相对路径是相对该补丁文件解析的,跨盘会失败)。
332
357
 
358
+ ## DSH Web 端口固定为 3080(部署前提)
359
+
360
+ 中继的上游默认就是 `http://127.0.0.1:3080`(`server.mjs` 里的 `RELAY_UPSTREAM`),
361
+ 所以 **DSH 的 Web 端口必须是 3080**,否则 `/taskwatch/data` 只会回 502,手机页显示
362
+ 「对话功能不可用」。桌面版默认不是这样:端口由 `app.asar` 决定,而那里把 `--port 0`
363
+ 写死了(OS 每次启动随机挑一个空闲端口),并且 **没有** `DSH_PORT` / `DSH_WEB_PORT`
364
+ 这类环境变量入口、配置文件里也没有端口字段 —— 环境变量与 CLI 参数这条路在桌面版上
365
+ 都是堵死的。
366
+
367
+ 唯一「不改安装目录、升级不丢」的入口,是用户补丁层覆盖 web-app 自带的 `webserver` 行:
368
+
369
+ ```yaml
370
+ # ~/.dsh/profiles/desktop/cordis.patch.yml
371
+ - id: webserver
372
+ name: "@deepseek-ai/dsh-host-webserver"
373
+ config:
374
+ host: "127.0.0.1"
375
+ port: 3080 # 原来是 !!js ctx.webStartup.port ?? 3080,桌面版传进来的是 0
376
+ tls: !!js ctx.webStartup.tls
377
+ compression: gzip
378
+ compressionLevel: 1
379
+ compressionThresholdBytes: 1024
380
+ ```
381
+
382
+ 三个要点:
383
+
384
+ - 补丁层是**整段替换** `config`,不是深合并。少写一个字段(上面那四个除 `port` 和
385
+ `host` 之外的字段)等于把它删掉,所以必须原样写回。
386
+ - 这个 profile 的补丁文件是 **live** 生效的:保存后 DSH 立刻在新端口重新监听,
387
+ 已经打开的窗口会断线 —— 重开一次客户端即可,会话不丢。
388
+ - **改端口时三处必须一起改**:上面这行 `port`、`~/.dsh/autostart/services-watchdog.ps1`
389
+ 里给中继设的 `RELAY_UPSTREAM`、以及本文档这一节。中继的 `UPSTREAM` 是启动时读取的
390
+ 模块常量,改环境变量不会热生效,必须重启一次中继。
391
+ - 3080 若被别的程序占着,桌面版会 bind 失败、Web 界面起不来;先 `curl http://127.0.0.1:3080/`
392
+ 看是谁在应答。
393
+
333
394
  ## 在外网 / 手机上访问(需要你自己叠一层)
334
395
 
335
396
  **本插件自己不做鉴权,只监听回环。** 它提供的是「可被安全转发的只读源」,转发这一层
@@ -451,11 +512,22 @@ PWA 外壳由中继本地提供(而不是转发上游),这样它不随上
451
512
 
452
513
  这就是手机页右上角那个双入口胶囊只能"事后切"、不能"事先测"的原因:
453
514
 
454
- - 胶囊上显示当前走的是哪条路(远端 / 局域网),点一下在 **自动 → 固定局域网 →
455
- 固定远端** 之间循环,选择记在 `localStorage`。
456
- - **自动**只用两个信号:这条路刚才通不通、你上次选了哪条。远端路上
457
- `GET /taskwatch/upstream-health` 体检失败(隧道或 DSH 挂了)就切局域网(若可用);
458
- 局域网路上请求失败(手机离开了 Wi-Fi)就切回远端。能用的那条路不会被主动换掉。
515
+ - 胶囊显示当前走的是哪条路(远端 / 局域网)。**点开是一张菜单**,列出两条入口:
516
+ 当前那条标「当前」,另一条标「切换」—— 点另一条 = 签一张票 + 跳一次。
517
+ - **没有任何自动跳转**(v1.7.5 起)。页面加载时只读一次入口清单、只渲染胶囊,
518
+ 不允许因为入口判断而导航;也没有定时体检。自动模式被删除的原因写在下面。
519
+ - 两道保险:跳转前在 `sessionStorage['tw-route-jump']` 写时间戳,8 秒内重复点击
520
+ 只给静态提示、不签票也不跳;URL 带 `?t=` 落地时立刻 `history.replaceState`
521
+ 清掉参数,并把入口锁成当前项(`sessionStorage['tw-route-lock']`)。
522
+
523
+ > **为什么不再"自动切"**:v1.6.0 – v1.7.4 的自动模式用「这条路刚才通不通」猜方向,
524
+ > 而 `apply()` 在初始加载与每 12 秒的体检后都会重新判断,判断出「该换」就改地址。
525
+ > 两条路的信号一旦互相矛盾(一条体检失败、另一条请求报错),页面就会在两个入口之间
526
+ > 无限往返,手机看到的是**地址栏来回变、整页反复刷新**。猜错的代价远大于省下的一次
527
+ > 点击,所以自动只保留在人的脑子里:菜单列出两条路,当前是哪条、另一条在哪,由用户点。
528
+ > `GET /taskwatch/upstream-health` 仍在中继侧保留,但页面不再请求它。
529
+
530
+ ![双入口菜单(390 宽,真实渲染)](docs/route-entries-390.png)
459
531
 
460
532
  ### 换路为什么不重输配对码
461
533
 
@@ -472,8 +544,8 @@ PWA 外壳由中继本地提供(而不是转发上游),这样它不随上
472
544
  **v1.7.1 起,前两条插件宿主也自己实现了**(同一套语义):`GET /taskwatch/entries`
473
545
  回答「现在有哪几条路」,`POST /taskwatch/ticket` 给一张换路票据。
474
546
  所以从宿主页面(例如 `http://127.0.0.1:52676/taskwatch`)点「去远端」不再拿到 405 / 404。
475
- `GET /taskwatch/upstream-health` 仍只有中继实现;宿主页面拿到它的 404 时按「未知」处理,
476
- 并停掉探测 —— **不会**把「没有体检接口」误判成「这条路断了」。
547
+ `GET /taskwatch/upstream-health` 仍只有中继实现,但 **v1.7.5 起页面不再请求它**:
548
+ 自动体检切换已删除,来源也不可靠 —— 中继经隧道看到的来源永远是 `127.0.0.1`。
477
549
 
478
550
  #### 票据格式(v1.7.2)
479
551
 
@@ -518,8 +590,8 @@ nonce = crypto.randomBytes(16) # base64url 后 22 个字符
518
590
 
519
591
  页面侧遇到这两条接口的 **404 / 405** 会进入**静态降级**:不重试、不弹错,胶囊按
520
592
  「只有一个入口」隐藏;设置 / 任务详情里留一行说明与一个手动「去远端」链接
521
- (上次成功的地址记在 `localStorage['tw-remote-url']`)。500 / 502 / 网络错误只提示一次
522
- (同一句 30 秒内不重复弹),不会变成重试风暴。
593
+ (上次成功的地址记在 `localStorage['tw-remote-url']`)。其它错误只在入口菜单里
594
+ 显示一行静态提示(切菜单即清),不会自动重试,更不会因此导航。
523
595
 
524
596
  > **升级后公网换路仍是 401?** 先看 3099 上的中继进程是不是新代码:它在**启动时**
525
597
  > 一次性加载路由与票据密钥,不会热更新。带令牌调一次 `POST /taskwatch/ticket`,
@@ -538,6 +610,10 @@ nonce = crypto.randomBytes(16) # base64url 后 22 个字符
538
610
  | 中继回的 502,响应头带 `x-taskwatch-hop: relay` | 中继 → DSH 上游 | 中继自己发的:隧道是通的,但上游 DSH 不可达(回环端口没在监听,或插件没挂载) |
539
611
  | 手机上**反复**看到 502 | 手机的 service worker | v1.6.0 之前的 SW 会把那次 502 存成外壳,之后离线导航一直吃它,而服务端早已恢复 —— v1.6.0 起只缓存 2xx,并把 `CACHE` 升到 v13 清掉已存的 502 |
540
612
 
613
+ > 2026-10-21 起 DSH 的 Web 端口固定在 3080(见「DSH Web 端口固定为 3080」一节)。
614
+ > 中继那种 502 基本只剩两种情况:DSH 没在 3080 上(客户端没开,或 3080 被别的程序占用
615
+ > 导致 bind 失败),或者中继进程还拿着旧端口的上游(`UPSTREAM` 启动时读取,必须重启中继)。
616
+
541
617
  ## 安全边界
542
618
 
543
619
  - **写入口等于一台终端**。一旦放行 `/taskwatch/chat/send`,能打开这个页面的人就能在