dsh-in-hand 1.7.2 → 1.7.4

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
@@ -502,9 +502,16 @@ nonce = crypto.randomBytes(16) # base64url 后 22 个字符
502
502
  ```
503
503
 
504
504
  配了 `lanUrl` 就把 `entries` 的局域网条目换成它,不再自动探测内网 IP;没配保持现状。
505
+ `lanUrl` 接受 `http://192.168.1.5:3099`,也接受入口的完整地址
506
+ `http://192.168.1.5:3099/taskwatch`(只吃掉这一个后缀,entries / ticket 还会自己再拼
507
+ `/taskwatch`);其它路径一律按未配置处理,**并在启动时打一条告警**(v1.7.3 之前是静默丢弃)。
505
508
  **插件自己的路由没有令牌门**(授权由中继那层负责),所以建议把 `lanUrl` 指到有鉴权的
506
509
  中继,而不是直接指回插件宿主。
507
510
 
511
+ 票据接口的请求体字段就是 `to`:页面发 `{"to":"remote"}` / `{"to":"lan"}`,
512
+ 服务端读 `body.to`(缺省 / 未知值都按 `lan` 处理,不会误签远端票)。这条契约由
513
+ `scripts/check-contract.mjs` 钉住,改页面或改服务端时必须同步。
514
+
508
515
  没配 `publicUrl` 时只报局域网入口;`ticket` 的远端请求明确回 `409 no-public-entry`。
509
516
  宿主这层仍然不做令牌校验(401 由中继负责),但票据是写路由,所以和中继的写路由同一套
510
517
  门禁:`POST` + `application/json` + 同源(`Origin` / `Sec-Fetch-Site`)校验。
@@ -514,6 +521,10 @@ nonce = crypto.randomBytes(16) # base64url 后 22 个字符
514
521
  (上次成功的地址记在 `localStorage['tw-remote-url']`)。500 / 502 / 网络错误只提示一次
515
522
  (同一句 30 秒内不重复弹),不会变成重试风暴。
516
523
 
524
+ > **升级后公网换路仍是 401?** 先看 3099 上的中继进程是不是新代码:它在**启动时**
525
+ > 一次性加载路由与票据密钥,不会热更新。带令牌调一次 `POST /taskwatch/ticket`,
526
+ > 返回的 `t` 不是 `v2.` 形状就说明还在跑旧进程 —— 重启中继即可,插件这侧无需改动。
527
+
517
528
  > 中继那边另有独立实现,见中继仓库 `dsh-in-hand-relay`(该仓库尚未创建,此处只写仓库名)。
518
529
  > 中继没升级时,页面在宿主上仍能正常用,只是没有远端切换。
519
530
 
package/lib/index.js CHANGED
@@ -209,10 +209,19 @@ export function lanAddresses(ifaces, includeVirtual = false) {
209
209
  return out
210
210
  }
211
211
 
212
- /** 局域网入口也可以指向别处(通常是中继的局域网入口)。只接受纯 origin:
213
- * 带路径的值和 publicUrl 一样会让 302 / 入口清单对不上,一律当作没配。 */
212
+ /** 局域网入口也可以指向别处(通常是中继的局域网入口)。
213
+ *
214
+ * v1.7.3 之前这里和 publicUrl 共用同一个"只收纯 origin"的归一化函数,结果很自然
215
+ * 会写出来的 `http://10.0.51.18:3099/taskwatch`(入口的完整 URL)被**静默丢弃**,
216
+ * 再回落到自动探测内网 IP —— 用户看到的就是"配了 lanUrl 却没生效"。
217
+ * 现在允许且只允许入口自己的那个 `/taskwatch` 后缀:entries / ticket 还会再拼一次,
218
+ * 不把后缀吃掉就会拼成 /taskwatch/taskwatch。其它路径仍然当作没配(返回 ''),
219
+ * 调用方会打一条告警,不再无声无息。 */
214
220
  export function normalizeLanUrl(raw) {
215
- return normalizePublicUrl(raw)
221
+ const text = typeof raw === 'string' ? raw.trim() : ''
222
+ if (!text) return ''
223
+ const trimmed = text.replace(/\/+$/, '').replace(/\/taskwatch$/, '')
224
+ return normalizePublicUrl(trimmed)
216
225
  }
217
226
 
218
227
  /** 票据存储:60 秒、一次性。单独抽出来的原因是"过期"要能被测试钉住 ——
@@ -1875,11 +1884,21 @@ export function apply(ctx, config) {
1875
1884
  const settings = config && typeof config === 'object' ? config : {}
1876
1885
  // 公网地址只从配置来:插件配置项 publicUrl,其次环境变量(与 DSH_TASKWATCH_ROOTS
1877
1886
  // 同一习惯)。没有配置就只报局域网入口 —— 代码里绝不写死任何域名。
1878
- const publicUrl = normalizePublicUrl(
1879
- asText(settings.publicUrl) || asText(process.env.DSH_TASKWATCH_PUBLIC_URL),
1880
- )
1887
+ const rawPublicUrl = asText(settings.publicUrl) || asText(process.env.DSH_TASKWATCH_PUBLIC_URL)
1888
+ const publicUrl = normalizePublicUrl(rawPublicUrl)
1881
1889
  // 局域网入口可以指到别处(通常是有令牌门的中继)。没配就沿用自动探测内网 IP。
1882
- const lanUrl = normalizeLanUrl(asText(settings.lanUrl) || asText(process.env.DSH_TASKWATCH_LAN_URL))
1890
+ const rawLanUrl = asText(settings.lanUrl) || asText(process.env.DSH_TASKWATCH_LAN_URL)
1891
+ const lanUrl = normalizeLanUrl(rawLanUrl)
1892
+ // 配了却归一化成空,以前是**静默**回落 —— 这正是"lanUrl 配了不生效"最难查的地方。
1893
+ // 只写键名与预期形态,绝不回显值(URL 里可能带凭据)。
1894
+ if (rawPublicUrl && !publicUrl) {
1895
+ console.error('dsh-in-hand: publicUrl 已配置但无效,按未配置处理'
1896
+ + '(需要纯 origin,例如 https://dsh.example.com,不能带路径)')
1897
+ }
1898
+ if (rawLanUrl && !lanUrl) {
1899
+ console.error('dsh-in-hand: lanUrl 已配置但无效,按未配置处理'
1900
+ + '(需要 origin,例如 http://192.168.1.5:3099;只允许带 /taskwatch 后缀)')
1901
+ }
1883
1902
  // 票据 v2:密钥解析顺序在 lib/ticket.js 里与中继保持一致;文件权限也在那里收紧。
1884
1903
  // **按需初始化**(第一次签发/兑换时才解析)—— 只读监控、不换路的会话不该在启动时
1885
1904
  // 就去碰用户主目录,测试里也能避免无意写入真实 ~/.dsh。
package/lib/sw.js CHANGED
@@ -44,7 +44,7 @@ const SHELL = ['/taskwatch', '/taskwatch/icon-192.png', '/taskwatch/icon-512.png
44
44
  // 那次重载是 cache-first 时代的补丁(旧外壳永远不会自己变新)。现在页面已经是
45
45
  // **网络优先**(见下面 fetch 里的注释),每次导航本来就取新版,这个补丁既没必要、
46
46
  // 又足以把整站打死。别再加回来。
47
- const CACHE = 'taskwatch-v13'
47
+ const CACHE = 'taskwatch-v14'
48
48
 
49
49
  const JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-in-hand",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "掌上 DSH:DeepSeek Harness 的手机端,不用装 App——会话、实时对话、交付文件、后台任务与待批准。 / DSH in hand: the phone client for DeepSeek Harness with no app to install.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -33,7 +33,7 @@
33
33
  "node": ">=20"
34
34
  },
35
35
  "scripts": {
36
- "check": "node scripts/check-bundle.mjs && node scripts/check-files.mjs && node scripts/check-uploads.mjs && node scripts/check-queue.mjs && node scripts/check-entries.mjs && node scripts/check-ticket-hmac.mjs",
36
+ "check": "node scripts/check-bundle.mjs && node scripts/check-files.mjs && node scripts/check-uploads.mjs && node scripts/check-queue.mjs && node scripts/check-entries.mjs && node scripts/check-ticket-hmac.mjs && node scripts/check-contract.mjs",
37
37
  "demo": "node scripts/make-demo.mjs",
38
38
  "icons": "node scripts/make-icons.mjs"
39
39
  },