Skip to content

Releases

1.6.0

Choose a tag to compare

@Mzy123l Mzy123l released this 05 Nov 20:48

1.6.0

手机布局改为「官方基线 + 我们只做三件事」。之前那套自建布局(覆盖 grid-template-columns、自建 #dsh-ra-scrim 遮罩、把侧栏改成定浮层)与 DSH 0.2.2 的官方布局抢列宽,是「右侧面板收不起来」「抽屉里左滑反而弹右栏」这一串 bug 的根源。

手机布局

  • 布局全部交给官方 @deepseek-ai/dsh-client-ui-layout。< 768px 时官方自己就是「无轨道 + 模态抽屉 + 右栏全屏浮层」,列宽由它按内联 style 算,插件一条都不覆盖。
    删掉了:grid-template-columns / grid-column 钉列、自建遮罩、data-ra-rail 零宽收起态。
  • 保留我们自己的三样:手势、返回键、输入区样式。
  • 手势与返回键改为点官方按钮(data-sidebar-opener / -toggle / -right-expand / -right-toggle)。1.5.0 按中文 aria-label 找按钮,切成英文后一个都点不到,抽屉就卡在展开态 —— 这次一并修掉。
    另外修了「右栏展开时滑空白处收不起来」:两个浮层都盖满整屏时不再做横向归属判断。
  • 左上角图标换成官方鲸鱼:保留官方那枚 data-sidebar-opener 按钮本身(点击行为不变),只把里面的 <svg> 换成 FishLogo 的路径(viewBox 0 0 23.16 17.04,单条 path)。
  • 输入区:字号 15px、行高 21px;右组(积分 / 模型 / 发送)靠右对齐;正文左右留白各 28px。

多语言

  • 设置页支持 DSH 可切换的全部 8 种语言:zh / en / ja / es / fr / de / ru / pt(清单对应官方 dsh-client-locale/locale/ 目录)。
  • 语言跟随 DSH 即时切换、不用刷新;查不到的条目回落到中文原文,不会渲染成空白。
  • 缓存统计那类内联诊断文案仍保持中英双语。

修复

  • 手机端「选择工作区目录」不可用:directory-picker-auto 在停用前已挂上 native 后端并占用了 directoryPicker 服务名,导致 browse 后端注册失败且不会重试,directoryPickerController 因此不激活。插件在启动稳定后检测到这条失败条目就重挂一次 browse。
  • 文件预览分页调小:workspace-files 的 maxLines 从 5000 降到 1000、maxBytes 从 2MB 降到 512KB。手机上打开大文件原本会一次性往 DOM 塞 5000 行并做高亮,主线程长时间阻塞,表现是「整个页面都卡」。

设置页

  • 移除「启用」「远程UI布局」「收起态图标栏」三项(配置字段本身保留,老配置仍可读)。

安卓外壳

  • 设置 / 刷新两枚按钮从左侧图标栏下方移到对话框(输入框)右侧上方。
  • targetSdk 从 35 降到 34:Android 15 对 targetSdk >= 35 的应用强制 edge-to-edge,内容会画到状态栏下面(时间/电量压在标题上)。降到 34 即避开。
  • versionCode 17 / versionName 1.6.0,签名不变,可直接覆盖安装。

测试

Host 侧 169 条、客户端 32 条、i18n 28 条、配置契约 13 条,全部通过。

升级说明

index.js / client.js / bundle patch 都改了,重启一次 DSH 生效。APK 需要重装(见下方附件)。


完整更新日志:README.md#160

v1.5.0:配置只认每用户 ~/.dsh/remote-access-config.json

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 17:40

v1.5.0

配置彻底分离:每个 Windows 用户只认自己的 $DSH_HOME/remote-access-config.json,
插件不再读也不写共享 profile(cordis.patch.yml)。

为什么改

1.4.2 起虽然把配置存到了本用户 JSON,但仍然是「本地优先、共享兜底」:设置页保存写的还是
profile 的 cordis.patch.yml,启动时还要拿它当基线。结果就是配置并没有真正分离 ——
多个用户共用一份 profile(.dsh\profiles\desktop 做成 junction)时,任何一次保存仍可能
碰到共享配置。

1.5.0 把这层关系整个去掉:

  • 插件不导出 Config、不做 DSH 配置表单;bundle patch 的插件行不带 config;
  • 启动时只读 $DSH_HOME/remote-access-config.json(不存在就按代码默认值建一份);
    DSH 传进来的 config 一律忽略;
  • 设置页(本机与手机)都走本插件自己的 /__remote_access__/config:
    GET 读、POST { patch, revision } 写,写盘完成后先回执、下一拍再重挂监听
    (否则重挂会 closeAllConnections(),把这次保存的连接自己掐掉);
  • 错密码拉黑直接写这份 JSON 的 denyCidrs,不再经过 configEditor;
  • 端口被占用仍会自动换空闲端口,并把实际端口写回本文件。

升级注意

  1. 升级后重启一次 DSH(index.js / client.js 都改了);
  2. 1.4.x 存在 profile 里的参数(端口 / 网段 / 密码)不会被读取:到「设置 → 远程访问」
    重新填一遍,或直接编辑本用户 JSON;
  3. 1.4.2–1.4.4 用户的本用户 JSON 已经存在,里面就是上次的生效值,通常不用改;
  4. 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(versionCode 16)。

文件形态

{
  "version": 1,
  "note": "本文件是当前 Windows 用户自己的远程访问配置(唯一配置源)……",
  "updatedAt": "2026-10-19T00:00:00.000Z",
  "config": { "port": 19388, "allowCidrs": ["100.64.0.0/10"], "……": "……" }
}

未知键会被忽略;也可以只写一个扁平的配置对象(不包 config),插件照常读。

测试

  • tools/test-remote-access.mjs 169/169(含:配置端点 GET/POST/409/405/415、
    先回执再重挂、DSH config 被忽略、首次启动默认值、坏文件备份重建、端口退让与记住端口)
  • tools/test-client.mjs 32/32(只走插件端点:读取 / 只提交改动字段 / revision / 409 /
    读失败提示;布局与 UI 投影;本机页面不上报配置写入)
  • tools/check-config-schema.mjs 13/13(静态钉住:无 Config、无 configEditor /
    settings.describe、bundle patch 不带 config、client 不碰 configForms)
  • tools/check-dsh-compat.mjs 15/15
  • 全部测试使用临时 $DSH_HOME,不会碰开发机的真实配置。

v1.4.4:引导字段不参与本用户配置差异(去掉无谓写盘)

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 16:44

v1.4.4

去掉一处无谓写盘 / 日志噪音(功能与 1.4.3 相同)。

修了什么

localConfigFile 是启动期引导字段(决定本用户配置文件在哪),但它被算进了
「本用户配置 vs 共享 profile」的差异比较里。于是每次 app-boot/config-reload
(任何插件保存设置都会发)都会误报一条:

设置已同步到本用户配置(localConfigFile)→ C:\Users\<你>\.dsh\remote-access-config.json

并把本用户配置文件白写一遍 —— 内容没变,只是 updatedAt 变了。

现在:

  • localConfigFile 不参与差异比较(BOOTSTRAP_KEYS);
  • 首次种子改为归一化后的完整配置,第一次热重载也不会再把 listen 之类的默认值
    当成「刚保存的改动」;
  • 没有真实改动时,config-reload 不写文件、不打那条日志。

测试

  • 新增:diffConfigKeys 忽略引导字段;config-reload 没有改动时 updatedAt 不变;
  • tools/test-remote-access.mjs 183/183
  • tools/test-client.mjs 123/123
  • tools/check-config-schema.mjs 12/12
  • tools/check-dsh-compat.mjs 15/15

说明

  • 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(versionCode 16);
  • index.js 改了,重启一次 DSH 生效;
  • 1.4.2/1.4.3 的功能(本用户配置、端口退让、热重载沿用引导字段)完全保留。

v1.4.3:热重载沿用启动期 localConfigFile(内部修复)

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 16:37

v1.4.3

内部修复(功能与 1.4.2 相同):热重载不再重新推导本用户配置路径。

修了什么

app-boot/config-reload 的处理(reapply())以前会用「刚从 settings 读到的配置」重新推导
<DSH_HOME>/remote-access-config.json 的路径。真实 DSH 里这个字段总会出现在 settings 视图里,
所以对用户行为与 1.4.2 一致;但当视图缺少 localConfigFile 时(例如测试桩),归一化会填回默认值,
于是把配置写进了真实的 $DSH_HOME —— 1.4.2 的测试就是这样污染了开发机的 .dsh。

现在 localConfigFile 被当作启动期引导字段:

  • 路径只在 apply() 时确定一次;
  • 热重载沿用这份路径,不再从 settings 视图重新推导;
  • 它也不参与「本用户配置 vs 共享 profile」的差异比较。

测试

  • 新增回归:settings 视图不带 localConfigFile 时,触发 config-reload 不得创建
    $DSH_HOME/remote-access-config.json;
  • tools/test-remote-access.mjs 181/181
  • tools/test-client.mjs 123/123
  • tools/check-config-schema.mjs 12/12
  • tools/check-dsh-compat.mjs 15/15

说明

  • 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(versionCode 16);
  • index.js 改了,重启一次 DSH 生效;
  • 1.4.2 的功能(本用户配置、端口退让)完全保留。

v1.4.2:配置改为每用户一份(修多用户共用 profile 抢端口)

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 16:21

v1.4.2

配置改为「每个 Windows 用户各一份」:多个人共用一份 DSH profile 时,不再抢同一个监听端口。

根因

多个 Windows 用户共用一份 DSH 的常见做法,是把 .dsh\profiles\desktop 做成
junction / 符号链接指到同一个目录。这样 node_modules 与 cordis.patch.yml
都是共享的 —— 包括里面的 port。两个人同时开 DSH 时,第二个实例必然失败:

[error] 监听 100.64.0.5 失败: listen EADDRINUSE: address already in use 100.64.0.5:19388

于是「插件明明启用了,手机却连不上」;而先启动的那个实例仍占着端口,
后启动的实例连状态文件都写不出可用网址。

修复

  1. 新增本用户配置 $DSH_HOME/remote-access-config.json(默认 %USERPROFILE%\.dsh,
    权限 0600),启动时本地优先于共享 profile:
    • 首次启动以共享 profile 为种子建一份;
    • 设置页保存仍写共享 profile,但宿主只把「这次变了的键」同步进本用户配置再生效 ——
      改端口只影响自己,不会把别人的端口一起改掉;
    • 本用户手改的其它字段,不会被「别的字段保存」顺手覆盖。
  2. 端口被占用时退让:换一个系统空闲端口继续服务(而不是整个远程入口打不开),
    并把实际端口写回本用户配置 —— 下次启动直接用它,手机上的网址也稳定。
  3. 状态文件新增「本用户配置」一行;设置页底部会写明生效的是哪个文件。
  4. 新 Host 配置字段 localConfigFile:'' = 默认路径,off = 关掉这一层(全部走共享 profile),
    其它值 = 显式路径。
  5. 删掉本用户配置文件 = 下次启动重新以共享 profile 为种子(想整体重置就用这招)。

升级注意

  1. 升级到 1.4.2 并重启一次 DSH(index.js 的改动要重启才加载);
  2. 第一次启动会自动建好本用户配置,不需要手写;
  3. 如果状态文件里出现「端口 … 已被占用 → 本次改用 …」,说明发生了退让,
    实际网址以状态文件里「远程访问网址」那一行为准;
  4. 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(versionCode 16)。

测试

  • tools/test-remote-access.mjs 180/180(新增 20 条:本用户配置路径 / 合并 / 差异检测、
    首次启动种子、重启后本地优先、off 时完全不建文件、坏文件先备份再重建、
    设置页保存只同步变了的键、保存即重启本行时也能采纳、端口被占用时退让并把端口写回、
    重启后仍用记住的端口)
  • tools/test-client.mjs 123/123
  • tools/check-config-schema.mjs 12/12(32 个字段,含新增的 localConfigFile)
  • tools/check-dsh-compat.mjs 15/15

测试默认用 localConfigFile: 'off' 隔离。这一版的热重载路径有个测试隔离漏洞
(settings 桩缺该字段时会回落到默认路径),已在 1.4.3 修复并补了回归测试。

v1.4.1:修 DSH 0.2.1 loader id 冲突(启动崩溃 / 热启用失败)

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 04:01

修复 DSH 0.2.1 启动崩溃:插件 loader 行 id 与官方新增的 remote-access 行重名。

如果你在 1.4.0 上遇到 desktop welcome: Web request failed / “应用无法启动”,这一版就是修复。
升级后需要把旧 cordis.patch.yml 里 id: remote-access 的 config: 挪到
id: remote-access-cidr 下面;不挪则参数回默认值,但不会再撞官方行。

根因

DSH 0.2.1 的 @deepseek-ai/dsh-base 新增了一行:

- id: remote-access
  name: '@deepseek-ai/dsh-api-remote-access'

本插件 1.4.0 的 bundle patch 也用了同一个 loader id:

- id: remote-access
  name: 'dsh-plugin-remote-access'

loader 行 id 必须全局唯一。重名后官方的 remoteAccess 服务不挂载,
typert-gateway / settings-controller 等依赖它的行一直 pending,
桌面壳启动时 settings/describe 请求失败,于是报:

desktop welcome: Web request failed

插件自己的诊断里能看到它已 apply、已监听,但宿主核心服务并没有起齐——这两件事并不矛盾。

修复

  • 插件行、设置命名空间、Config 的 ROW_ID 与 client.js 的 NS 全部改为
    remote-access-cidr(旧包名式的名字,和官方行彻底分开);
  • cordis.patch.yml 的插入行改为 id: remote-access-cidr;
  • tools/check-dsh-compat.mjs 增加检查:读取 0.2.1 官方 dsh-base / dsh-web-app
    的 patch,断言插件行 id 不与官方行重名;
  • 测试夹具中的命名空间同步改为 remote-access-cidr。

这也解释了插件页「启用」时的
session-controller: file-upload: Agent resolver is already registered:官方
remoteAccess 服务没挂载,session-controller 在热启用时被反复激活。id 分开后,
官方服务与插件行各自独立,热启用不再撞这条链。

升级注意

  1. 升级到 1.4.1(第一次装/升级仍建议冷启动一次,让 loader 行换成新 id);
  2. 打开 $DSH_HOME/profiles/desktop/cordis.patch.yml,把旧 id: remote-access 下的
    整段 config: 复制到 id: remote-access-cidr 下(没有这一段就用默认值);
  3. 冷启动后,插件页的启用 / 停用走热重载即可,不再需要每次重启;
  4. 启动后到「设置 → 远程访问」确认能看到表单。

测试

  • Node 22 与 Node 24.21(桌面端内置 Electron Node)双跑:

    • tools/test-remote-access.mjs 160/160
    • tools/test-client.mjs 123/123
    • tools/check-config-schema.mjs 12/12
    • tools/check-dsh-compat.mjs 15/15(含 loader id 唯一性检查)
  • 用修好 id 的本地包跑 dsh --profile test --dump-config 实测:组合里
    官方 id: remote-access(@deepseek-ai/dsh-api-remote-access)与插件
    id: remote-access-cidr(dsh-plugin-remote-access)并存,不再重名。

  • 热启用/热停用实测:在临时 profile(@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app,
    1.4.1 已安装但未启用)里启动真实 DSH 0.2.1 Host,通过
    pluginManager/setBundleEnabled 依次热启用、热停用,两次都返回:

    { "application": "applied", "warnings": [] }

    不再出现 session-controller: file-upload: Agent resolver is already registered。

  • 真实桌面端热加载:在当前 DSH 0.2.1 桌面端里把 1.4.1 文件装进 profile、
    把 bundle 加进 dsh.profile.bundles 后,HMR 直接热加载插件:状态文件出现
    apply / 监听 / 上游自检,进程未崩、无需重启。

1.4.1 已在本机 DSH 0.2.1 桌面端热加载验证。发布:npm dsh-plugin-remote-access@1.4.1
与 GitHub Release v1.4.1。
APK 外壳代码这一版没有功能改动,但版本号已对齐:1.4.1(versionCode 16),
安装包为 dsh-plugin-remote-access-1.4.1.apk,与旧版同一把自签钥匙,可直接覆盖安装。

v1.4.0:适配 DSH 0.2.1(端口 / HTTPS / 远程 UI 不写宿主)

Choose a tag to compare

@Mzy123l Mzy123l released this 18 Oct 02:46

适配 DSH 0.2.1:端口不再写死、支持原生 HTTPS,远程外观/字号只改手机不写电脑设置,并按 dshmarket 要求声明版本要求。

版本要求:engines.dsh = ^0.2.1-rc.1(覆盖 0.2.1-rc.1 / 0.2.1-rc.2 与后续 0.2.x)。
DSH 0.2.0 及更早请继续用 1.3.8 —— 这一版用到了 0.2.1 的 webServer.getOrigin()、
远程设置持久化与 theme.setFontSize(role, px) 等行为。

插件与 APK 不总是同号:这一版只动网页半区与宿主半区,APK 仍是 1.3.8,不用重装。
宿主半区有改动(index.js),升级后重启一次 DSH。

适配 DSH 0.2.1

桌面端改用系统分配端口:上游自动跟随

0.2.1 的桌面端不再固定 19387,upstream 写死端口会连不上。现在:

  • upstream: auto 优先读 ctx.webServer.getOrigin(),一次拿到协议 + 真实监听端口;
  • cordis.patch.yml 的默认值从 http://127.0.0.1:19387 改为 auto;
  • 读不到 getOrigin() 时,退回 webServer.port / protocol / 环境变量;
  • 状态文件里会写明最终选中的上游协议与端口,排查时不用猜。

支持 0.2.1 的原生 HTTPS

0.2.1 的 Web 可以自己开 HTTPS(--tls-cert / --tls-key)。本版把整条链路都改成按上游协议选客户端:

  • 普通 HTTP 代理、缓存探针、401 补票重试都用 https.request;
  • WebSocket 透传改用 tls.connect;
  • Host / Origin 按实际协议改写(https:// 而不是永远 http://);
  • 只连本机回环,因此接受 DSH 的自签证书(rejectUnauthorized: false)。

远程「外观 / 字号」只投影本页,不写电脑设置

0.2.1 的 ui-settings 取消了「非回环页面 → memory」的降级:ConfigFormController 不再有
persistence / mode,远程页面调官方 setter 会直接写宿主设置。沿用 1.3.8 的做法,
手机会把电脑的主题/字号一起改掉。本版改为:

  • 外观:只设置本页 root 的 color-scheme、data-ds-theme-source、body 的 data-ds-dark-theme;
    不调用 theme.setTheme;
  • 字号:只写 body 的 --dsh-content-font-size / --dsh-code-font-size / --dsh-terminal-font-size;
    不调用 theme.setFontSize;
  • 电脑端改主题会广播 theme/change,手机页在官方 presenter 之后重新投影自己的外观;
  • 控制器退出时把本页恢复成宿主当前的主题/字号,仍然不触发写盘;
  • 0.2.0 上继续走官方 setter(本页生效),theme.setFontSize 的单参/双参签名都兼容。

「工作步骤展示 / 显示代码工作视图 / 性能与用量」改为跟随电脑

0.2.1 只提供「写宿主」的公开入口,没有「只改本页」的路径。继续调用就等于替你改电脑,
所以本版不再远程覆盖这三项:它们跟随电脑设置,设置页「UI 设置」的状态行会如实写明。
0.2.0 上仍然照旧生效。想改这三项,请到电脑的官方「通用设置」里改。

按 dshmarket 要求补齐清单

  • package.json 增加 engines.dsh = ^0.2.1-rc.1:dshmarket 的「按宿主发现」与安装前兼容性检查都读它;
    已用 DSH 0.2.1 自己的 evaluatePluginCompatibility 实测通过(运行时 0.2.1-rc.2)。
  • 增加 ./icon.svg 导出,配合 0.2.1「显示文本与图标走子路径导出」的约定。
  • 更新 locale/{zh,en}.json 与 meta.description,写明 0.2.1 适配与远程 UI 的新行为。
  • 新增 tools/check-dsh-compat.mjs:用安装目录里的 dsh-app-boot 与
    dsh-plugin-market-index 校验 engines.dsh、bundle patch、客户端面、locale/icon 与市场索引形状。

测试

全部在 Node 22(开发机)与 Node 24.21(DSH 桌面端内置 Electron 的 Node) 下跑过:

node --check index.js
node --check client.js
node tools/test-remote-access.mjs   # 160/160
node tools/test-client.mjs          # 123/123
node tools/check-config-schema.mjs  # 12/12
node tools/check-dsh-compat.mjs     # 13/13

新增覆盖:

  • parseUpstream 保留 http: / https: 协议;resolveUpstream 从 getOrigin() 读到 0.2.1 的随机端口,
    并跟随 webServer.protocol=https;
  • 0.2.1 的 UI 设置:一个会写宿主的官方入口都不调用;外观与字号真的落到本页 DOM;
    电脑端 theme/change 之后手机仍保留自己的外观;出厂值 default 也走本页投影;
  • 0.2.0 的旧路径(单参 setFontSize、五个官方入口)保持原样通过。

没有在运行中的 DSH 桌面端里启动插件做真机验证(按要求先不启动,避免把 DSH 带崩)。
发布后建议在 DSH 0.2.1 里安装后:

  1. 打开「设置 → 远程访问」,确认「本页客户端版本 1.4.0」;
  2. 手机上打开远程页,确认外观/字号生效、电脑设置不被改;
  3. 查看 <DSH_HOME>/remote-access-url.txt 里的上游协议与端口是否与本机 DSH 一致。

安装

dsh plugin --profile desktop add dsh-plugin-remote-access
# 或在 DSH 插件页填 dsh-plugin-remote-access

升级后重启一次 DSH。

DSH 安卓外壳 1.3.8

Choose a tag to compare

@Mzy123l Mzy123l released this 16 Oct 08:18

手机端四件事:一轮结束也会提醒你、回车不再发送、大资源不再每次重传、设置页整页整理。

这一版插件与 APK 都是 1.3.8(versionCode 13 → 14 → 15)—— 结束通知要外壳认识新的载荷,
所以 APK 需要重装;index.js 也改了(事件监听、缓存头、自检端点)→ 要重启一次 DSH。

修:同一条消息弹两次 / 切回后台又弹

两条链路各有一个坑,都补掉了:

  • 宿主:agent/status 不保证「running → idle」只出现一次(维护 / 压缩这类中间态也会映射成 idle),
    于是一次消息报了两次「结束」。现在规矩是「一次 running 只兑现一次结束」,
    没有 running 过的 idle 一律忽略。
  • 外壳:App 切到前台时那条前台服务会被停掉,切回后台是新实例 —— 去重状态只放在内存里,
    同一条结束(或同一条待答)就会被当成新的再弹一次。现在去重键(结束 = <宿主实例>:<编号>,
    提问 = 内容指纹)持久化在 SharedPreferences 里;宿主重启后编号从头数也不会误判
    (靠载荷里的 boot 实例标记区分)。
  • 「提问」那条按你的要求改了:切前台不再撤回通知与指纹(原来撤回时会把指纹一起清掉,
    等于「从没提醒过」,回来自然又弹),所以切前台再切回后台不再重复弹;
    只有「问题答完了」或「关掉通知开关」才撤回。你手动划掉那条通知也不会再弹;新问题照旧会响。

一轮结束时通知手机(正常 / 异常)

原来只有「模型提问」会提醒你;现在跑完一轮也会弹一条,走独立的「任务结束」通道
(与「模型提问」分开,想静音哪一类都行):

  • 正常跑完:这一轮跑完了 · 用时约 3 分 12 秒;
  • 异常结束:带上分类 —— 依据是 DSH 自己的错误码,不是猜的:
    MALFORMED_RESPONSE(模型输出格式错)、QUOTA / ACCOUNT_QUOTA(余额 / 额度不足)、
    AUTH / INVALID_CREDENTIAL(凭据失效)、RATE_LIMIT(被限流)、CONTEXT_WINDOW_EXCEEDED(上下文超长)、
    SERVER(服务端错误),网络那一族看 Node 的 errno;认不出来就老实说「出错」并带上原文。
  • 触发条件与「提问」完全一致:电脑窗口不可见 + 你没在看手机(两个开关互不影响:notifyOnFinish);
  • 结束判定用 DSH 的事件 agent/status(running → idle),带 2 秒防抖(循环里的维护 / 压缩中间态
    不会被误报),并且只认主流程 —— 子任务跑完不打扰你;
  • 刚跑完那一条有 5 分钟新鲜窗口:在页面上看着它跑完、然后切出去,照样收得到;
  • 正文只写结论与用时(异常写分类与原因),绝不带模型回答内容(可能很长,也不该出现在锁屏上);
  • 设置页页脚的自检现在两个通道分开报:提问通道=4、结束通道=4(哪个被调低一眼看出来)。

手机端回车 = 换行,点「发送」才发送

手机上的回车键本来就小,官方语义下它直接发送,想写两行只能去凑 Shift+Enter。现在:

  • 手机布局或触摸设备上的主输入框里,裸回车插一个换行,只有点发送按钮才真正发送;
  • Ctrl/Cmd+Enter 仍然是发送,Shift/Alt+Enter 保持原样;
  • 输入法合成中(含刚合成完 60ms 内)一律放行 —— 中文输入法用回车确认候选词,吃掉它等于打不了字;
  • 别的输入框(设置页、排队消息的行内编辑器)一律不碰,本机(电脑)页面完全不受影响;
  • 不想要这个行为:设置 →「远程访问」→ 关掉「手机端回车=换行」。

实现方式是在捕获阶段把这一个按键吃掉(官方那条「回车发送」根本收不到),再自己往编辑器里插换行;
三条路依次试(合成 beforeinput/insertLineBreak → execCommand('insertLineBreak') → 合成 Shift+Enter),
三条都不成时也什么都不做,绝不退回「发送」。

手机端资源缓存:壁纸那类大文件不再每次重传

「每次进去都要重下十几 MB」的根因在上游:

  • /assets/* 那两个 Vite bundle(实测 1.42 MB)上游只发 content-type,没有 cache-control / etag / last-modified;
  • 壁纸那类文件族(几十 MB 的图 / 视频)同样什么缓存口径都不发,也没有 ETag;
  • 于是浏览器既没有 freshness、也没有校验器,只能整份重下(APK 侧并没有禁用缓存,不是它的锅)。

这一版由代理把响应头喂对,并把「条件请求怎么判」接过来:

  • 内容寻址(?rev=…、index-5SrrfWpU.js 这种哈希文件名、64 位 hex 的附件 id)→ immutable,一年不再问。
    判定改成通用规则(URL 里真有内容哈希),不再写死 /plugins/、/assets/ 两个前缀 ——
    顺带修掉「非哈希的插件文件被钉一年」这个隐患。
  • 上游已给校验器(ETag / Last-Modified)→ 只补新鲜期,校验器原样保留,条件请求仍由上游回答。
  • 上游什么都没给的大静态文件 → 补 private, max-age=10 分钟, stale-while-revalidate 与自铸的弱校验器:
    期内手机一次请求都不发;过期后先出旧图、后台只做一次头部级核对,对得上就回 304(零字节)。

校验器用「长度 + 类型 + 首/尾各 64 KB 片段」(可关),不整份哈希、不落盘,
所以「换了图但字节数一模一样」也不会被误判成没变;≤256 KB 的小文件直接缓冲整份算精确哈希。

宿主不落任何正文,也不读宿主的文件系统:资源在哪个盘、哪个 Windows 用户的空间、
是不是软/硬链接搭出来的共用目录,对这套逻辑都透明(只看 URL 与上游响应头),
因此也不会出现「A 用户的缓存被 B 用户读到」。缓存容量交给手机自己的 Chromium LRU,我们不做第二份账。

不碰的范围(默认):no-store / no-cache / HTML 导航 / JSON 接口 / 小块二进制 / 带 Range 的请求。

流量账(1.3.8 加的排查工具):「缓存」那一组里除了口径与计数,还会列出本轮按 URL 的字节数 ——
送出多少、其中多少「没有缓存口径」(= 下次还会重传)、每个 WebSocket 通道各推了多少,
并给每一行附一句为什么没带缓存口径(上游写了 no-store / 带 Range 的分段请求 /
类型不在白名单(…) …)。配一个「清零流量账」按钮:先清零 → 完全退出 App → 重新打开 → 回来看这几行。
同一份数据也在状态文件里,接口是 GET /__remote_access__/cache(?reset=1 清零)。

两个「逆着上游意愿」的开关(默认都关,实测里最大那两笔就出在这儿):

开关 治什么 代价
cacheOverrideNoStore 上游给实时抓帧/预览图写 no-store,可那一张实测 4.03 MB、每次进页面都重来。开着时只对图片/音频/视频、且确实够大(探针读完 ≥64 KB,分块传输没长度头也算数)的响应补新鲜期 + 弱校验器 最多看到「新鲜期」这么久之前的旧帧;实时小端点(如每秒一次的频谱,JSON)不碰
cacheFullRange 媒体元素取音轨发 Range: bytes=0-,上游回覆盖整份的 206,而浏览器永远不缓存 206 —— 实测一次进页面重下 3.8 MB × 2。开着时把这一种改写成可缓存的整份 200(字节一模一样) 个别播放器可能失去拖动定位;中间分段(bytes=100-200)一律原样透传

有些流量是任何 HTTP 缓存都救不了的,别指望它:聊天里的图片/附件走 RPC(readAttachment,
WS /api/remote.mux),客户端只把它缓存在页面内存里 —— 所以「从最近任务划掉 App」=整页重载=图片重来,
而只是退到后台再回来不会重载、也就不重取;实时轮询(动态壁纸的音频频谱,实测约 8 次/秒)缓存它只会把动画弄坏。

可调项(都在设置页「远程访问」里)

项 默认 说明
手机端回车=换行 开 关掉即恢复官方「回车即发送」
启用手机端缓存 开 总开关;关掉后一个缓存头都不加
新鲜期(分钟) 10 1–1440;期内手机零请求,过期后先出旧的、后台核对一次
过期时取首/尾片段核对 开 治「换了图但字节数一模一样」;关掉就只按长度+类型核对
no-store 的大媒体也缓存 关 治「实时抓帧一次 4 MB、每次进页面都重来」
整段 Range 当整份缓存 关 治「音轨每次重下 3.8 MB × 2」

测试

  • Host:154 条(缓存整节 —— 三类判定 / 304 / 片段探针 / 同长度换图 / no-store 与接口不碰 /
    Range 透传 / 两个开关的开关两态 / 分块抓帧(无 content-length) / 实时小端点不碰 /
    整段 Range 改写成 200 且中间分段不动 / 流量账与清零 / 自检端点;结束通知整节 —— 主 agent 才报 /
    子 agent 不打扰 / idle 2 秒防抖 / 正常结束 / 结束推 rev / 错误码逐条分类 / 认不出来老实说 /
    状态文件结论 / 开关关掉时端点如实回答 / 重复触发收紧三条(没有 running 的 idle 不报、
    running→idle→idle 只兑现一条、载荷带 boot 实例标记))。
  • 客户端:118 条(回车拦截 20 条,含「异步提交只插一个」与「execCommand 只调一次」两条回归;
    缓存子菜单、流量账与设置页分节/字段说明 9 条;页脚自检改成两个通道分开报)。
  • node --check、tools/check-config-schema.mjs 12 条全过。

DSH 安卓外壳 1.3.6

Choose a tag to compare

@Mzy123l Mzy123l released this 14 Oct 17:46

手机端 1.3.6:「去开启横幅」按钮改成只在真检测出问题时才出现

1.3.5 把它无条件挂在每条通知上,横幅已经开了还一直显示 —— 自然会问「这不是已经开了吗」。
现在只在下面两种情况之一才出现:

  • 通知权限没开(areNotificationsEnabled() 为假);
  • 「模型提问」通道的重要性被调低(小米的「悬浮通知 / 横幅通知」开关就映射成通道重要性)。

一切正常时,那条通知上不再挂任何按钮。常驻的「后台待命」那条早就是这个条件,两条现在一致。

设置页页脚里手机端那句也不再打勾(手机端 App 1.3.6:通知已开、横幅通道=高)——
「通道=高」只是必要条件,厂商那层(小米的「后台弹出界面」等)系统不交给应用判断。

同一把自签钥匙(CN=DSH Remote),可直接覆盖安装(versionCode 12 → 13)。

DSH 安卓外壳 1.3.5

Choose a tag to compare

@Mzy123l Mzy123l released this 14 Oct 17:34

手机端 1.3.5:通知发出去了却不弹横幅时,把路指明白(小米 / Redmi 必看)

真机上查到的现象:通知栏里两条都在(带问题文字那条也发了),屏幕上就是不弹 —— 不是链路问题,
是厂商那一层没放行。这一版能做的是把路指明白、把状态摊开:

  • 通知上多一个**「去开启横幅」按钮**(带问题那条与常驻那条都带):一点直接跳到「模型提问」通道的
    设置页,小米的「悬浮通知 / 横幅通知」开关就在那一页;
  • 手机每次长轮询顺带把自己的通知环境报给电脑(App 版本 / 通知开没开 / 通道重要性),
    设置页底部于是能写:手机端 App 1.3.5:通知已开、横幅通道=高 ✓,或直接说出差哪一项;
  • 手机端检测到「通知权限没开」或「通道重要性被调低」时,常驻通知直接换成对应说明并带上那个按钮。

小米 / Redmi(MIUI、HyperOS)要打开的三处(按重要性):

  1. 设置 → 应用设置 → 应用管理 → DSH → 权限管理 → 后台弹出界面(最关键:App 在后台时 MIUI 靠它决定准不准弹窗);
  2. 设置 → 应用设置 → 应用管理 → DSH → 通知管理 → 通知类别 → 模型提问 → 横幅 / 悬浮通知;
  3. 锁屏通知(设置 → 通知与控制中心 → 锁屏通知 → 允许 DSH)。

同一把自签钥匙(CN=DSH Remote),可直接覆盖安装(versionCode 11 → 12)。
横幅只跟 APK 与手机设置有关;电脑那端更新到 1.3.5 并重启一次 DSH 后,设置页底部会多出手机端那一行自报。