Repository navigation
Releases
1.6.0
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的路径(viewBox0 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 即避开。versionCode17 /versionName1.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
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; - 端口被占用仍会自动换空闲端口,并把实际端口写回本文件。
升级注意
- 升级后重启一次 DSH(
index.js/client.js都改了); - 1.4.x 存在 profile 里的参数(端口 / 网段 / 密码)不会被读取:到「设置 → 远程访问」
重新填一遍,或直接编辑本用户 JSON; - 1.4.2–1.4.4 用户的本用户 JSON 已经存在,里面就是上次的生效值,通常不用改;
- 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(
versionCode16)。
文件形态
{
"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.mjs169/169(含:配置端点 GET/POST/409/405/415、
先回执再重挂、DSH config 被忽略、首次启动默认值、坏文件备份重建、端口退让与记住端口)tools/test-client.mjs32/32(只走插件端点:读取 / 只提交改动字段 / revision / 409 /
读失败提示;布局与 UI 投影;本机页面不上报配置写入)tools/check-config-schema.mjs13/13(静态钉住:无Config、无configEditor/
settings.describe、bundle patch 不带config、client 不碰configForms)tools/check-dsh-compat.mjs15/15- 全部测试使用临时
$DSH_HOME,不会碰开发机的真实配置。
v1.4.4:引导字段不参与本用户配置差异(去掉无谓写盘)
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.mjs183/183tools/test-client.mjs123/123tools/check-config-schema.mjs12/12tools/check-dsh-compat.mjs15/15
说明
- 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(
versionCode16); index.js改了,重启一次 DSH 生效;- 1.4.2/1.4.3 的功能(本用户配置、端口退让、热重载沿用引导字段)完全保留。
v1.4.3:热重载沿用启动期 localConfigFile(内部修复)
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.mjs181/181tools/test-client.mjs123/123tools/check-config-schema.mjs12/12tools/check-dsh-compat.mjs15/15
说明
- 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(
versionCode16); index.js改了,重启一次 DSH 生效;- 1.4.2 的功能(本用户配置、端口退让)完全保留。
v1.4.2:配置改为每用户一份(修多用户共用 profile 抢端口)
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
于是「插件明明启用了,手机却连不上」;而先启动的那个实例仍占着端口,
后启动的实例连状态文件都写不出可用网址。
修复
- 新增本用户配置
$DSH_HOME/remote-access-config.json(默认%USERPROFILE%\.dsh,
权限0600),启动时本地优先于共享 profile:- 首次启动以共享 profile 为种子建一份;
- 设置页保存仍写共享 profile,但宿主只把「这次变了的键」同步进本用户配置再生效 ——
改端口只影响自己,不会把别人的端口一起改掉; - 本用户手改的其它字段,不会被「别的字段保存」顺手覆盖。
- 端口被占用时退让:换一个系统空闲端口继续服务(而不是整个远程入口打不开),
并把实际端口写回本用户配置 —— 下次启动直接用它,手机上的网址也稳定。 - 状态文件新增「本用户配置」一行;设置页底部会写明生效的是哪个文件。
- 新 Host 配置字段
localConfigFile:''= 默认路径,off= 关掉这一层(全部走共享 profile),
其它值 = 显式路径。 - 删掉本用户配置文件 = 下次启动重新以共享 profile 为种子(想整体重置就用这招)。
升级注意
- 升级到 1.4.2 并重启一次 DSH(
index.js的改动要重启才加载); - 第一次启动会自动建好本用户配置,不需要手写;
- 如果状态文件里出现「端口 … 已被占用 → 本次改用 …」,说明发生了退让,
实际网址以状态文件里「远程访问网址」那一行为准; - 这一版没有重发 APK:外壳代码没动,仍是 1.4.1(
versionCode16)。
测试
tools/test-remote-access.mjs180/180(新增 20 条:本用户配置路径 / 合并 / 差异检测、
首次启动种子、重启后本地优先、off时完全不建文件、坏文件先备份再重建、
设置页保存只同步变了的键、保存即重启本行时也能采纳、端口被占用时退让并把端口写回、
重启后仍用记住的端口)tools/test-client.mjs123/123tools/check-config-schema.mjs12/12(32 个字段,含新增的localConfigFile)tools/check-dsh-compat.mjs15/15
测试默认用
localConfigFile: 'off'隔离。这一版的热重载路径有个测试隔离漏洞
(settings 桩缺该字段时会回落到默认路径),已在 1.4.3 修复并补了回归测试。
v1.4.1:修 DSH 0.2.1 loader id 冲突(启动崩溃 / 热启用失败)
修复 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.4.1(第一次装/升级仍建议冷启动一次,让 loader 行换成新 id);
- 打开
$DSH_HOME/profiles/desktop/cordis.patch.yml,把旧id: remote-access下的
整段config:复制到id: remote-access-cidr下(没有这一段就用默认值); - 冷启动后,插件页的启用 / 停用走热重载即可,不再需要每次重启;
- 启动后到「设置 → 远程访问」确认能看到表单。
测试
-
Node 22 与 Node 24.21(桌面端内置 Electron Node)双跑:
tools/test-remote-access.mjs160/160tools/test-client.mjs123/123tools/check-config-schema.mjs12/12tools/check-dsh-compat.mjs15/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 Releasev1.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 不写宿主)
适配 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.4.0」;
- 手机上打开远程页,确认外观/字号生效、电脑设置不被改;
- 查看
<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
手机端四件事:一轮结束也会提醒你、回车不再发送、大资源不再每次重传、设置页整页整理。
这一版插件与 APK 都是 1.3.8(
versionCode13 → 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.mjs12 条全过。
DSH 安卓外壳 1.3.6
手机端 1.3.6:「去开启横幅」按钮改成只在真检测出问题时才出现
1.3.5 把它无条件挂在每条通知上,横幅已经开了还一直显示 —— 自然会问「这不是已经开了吗」。
现在只在下面两种情况之一才出现:
- 通知权限没开(
areNotificationsEnabled()为假); - 「模型提问」通道的重要性被调低(小米的「悬浮通知 / 横幅通知」开关就映射成通道重要性)。
一切正常时,那条通知上不再挂任何按钮。常驻的「后台待命」那条早就是这个条件,两条现在一致。
设置页页脚里手机端那句也不再打勾(手机端 App 1.3.6:通知已开、横幅通道=高)——
「通道=高」只是必要条件,厂商那层(小米的「后台弹出界面」等)系统不交给应用判断。
同一把自签钥匙(CN=DSH Remote),可直接覆盖安装(versionCode 12 → 13)。
DSH 安卓外壳 1.3.5
手机端 1.3.5:通知发出去了却不弹横幅时,把路指明白(小米 / Redmi 必看)
真机上查到的现象:通知栏里两条都在(带问题文字那条也发了),屏幕上就是不弹 —— 不是链路问题,
是厂商那一层没放行。这一版能做的是把路指明白、把状态摊开:
- 通知上多一个**「去开启横幅」按钮**(带问题那条与常驻那条都带):一点直接跳到「模型提问」通道的
设置页,小米的「悬浮通知 / 横幅通知」开关就在那一页; - 手机每次长轮询顺带把自己的通知环境报给电脑(App 版本 / 通知开没开 / 通道重要性),
设置页底部于是能写:手机端 App 1.3.5:通知已开、横幅通道=高 ✓,或直接说出差哪一项; - 手机端检测到「通知权限没开」或「通道重要性被调低」时,常驻通知直接换成对应说明并带上那个按钮。
小米 / Redmi(MIUI、HyperOS)要打开的三处(按重要性):
- 设置 → 应用设置 → 应用管理 → DSH → 权限管理 → 后台弹出界面(最关键:App 在后台时 MIUI 靠它决定准不准弹窗);
- 设置 → 应用设置 → 应用管理 → DSH → 通知管理 → 通知类别 → 模型提问 → 横幅 / 悬浮通知;
- 锁屏通知(设置 → 通知与控制中心 → 锁屏通知 → 允许 DSH)。
同一把自签钥匙(CN=DSH Remote),可直接覆盖安装(versionCode 11 → 12)。
横幅只跟 APK 与手机设置有关;电脑那端更新到 1.3.5 并重启一次 DSH 后,设置页底部会多出手机端那一行自报。