qydxd1/dsh-page-zoom

dsh-page-zoom

浏览器式的整页缩放,给 DeepSeek Harness 桌面版用:按住 Ctrl 滚动滚轮(触控板双指捏合同理)即可像浏览器一样缩放整个界面,Ctrl + 0 回到 100%。

미분류accessibilitybrowser-zoomcordis

dsh-page-zoom

浏览器式的整页缩放,给 DeepSeek Harness 桌面版用:按住 Ctrl 滚动滚轮(触控板双指捏合同理)即可像浏览器一样缩放整个界面,Ctrl + 0 回到 100%。

网页版(浏览器打开的 Harness 地址)不受影响——浏览器自己已经有缩放快捷键,插件只在桌面窗口里生效(靠 window.dshDesktop 是否存在来判断)。

安装

这是一个 Harness bundle(客户端插件),装进你的 profile 即可:

plugin_manager → action: install_bundle → target: <这个目录的绝对路径>

装完在「插件」列表里能看到 Page zoom / 页面缩放,界面顶部按 Ctrl + 滚轮就会出百分比胶囊。

  • 需要桌面版(@deepseek-ai/dsh-desktop);开发时验证过 0.2.1-rc.2。
  • 它通过 dsh.client.inject 依赖 @deepseek-ai/dsh-client-ui-layout(百分比胶囊挂在 shell.overlay 槽位上),这是 Harness 自带的包。
  • 从本机目录 link 安装时,别删这个目录,删了插件就断了;想彻底移除:插件管理里禁用并移除 dsh-page-zoom。

为什么不是在窗口层面做

桌面壳(安装目录 resources/app.asar 里的 lib/main.js)是只读打包的安装产物,而且完全没有暴露缩放接口(既没有 webContents.setZoomFactor,也没有 IPC 通道),改它等于改安装目录、还会被下次更新覆盖。所以缩放放在页面里做:

  • 给 <body> 设 CSS zoom —— 这是真正的重排缩放(文字清晰、布局跟着变),不是 transform: scale 那种位图放大;
  • 同时把 <html> 和 <body> 的宽高设成「视口 ÷ 缩放倍数」,让 html → body → #root → 框架 这条 100% 高度链在放大后仍然正好填满窗口。<html> 那一步不能省:它默认还是整屏高度,会撑出竖向滚动条,界面底部被裁掉;
  • 给 <body> 加 contain: layout,把 position: fixed 的浮层(弹窗、toast、菜单)锚定到 body 上,这样它们随页面一起缩放,任何倍数下都正好盖住可见区域;
  • 缩放期间按 Chromium 原生页面缩放的口径校正「测量面」:getBoundingClientRect() / getClientRects()(含 Range)的返回值除以倍数,innerWidth / innerHeight 和 documentElement.clientWidth/Height 也除以倍数。CSS zoom 默认把这些量乘进了视觉尺寸,而 Harness 用它量列宽、算浮层定位和拖拽;不校正的话列宽会被算成「放大后的视觉宽度」,右侧面板、菜单、拖拽把手都会错位。
  • 指针坐标也一样要校正,否则「自己量矩形、再拿指针去比对」的浮层(桌面挂件正是这种写法)会把一次真实的点击算成「点在框外」而忽略掉:MouseEvent 的 clientX/clientY、pageX/pageY、offsetX/offsetY、x/y、movementX/movementY 除以倍数;document.elementFromPoint() / elementsFromPoint() 在内部先乘回倍数再问浏览器,调用方照旧传布局像素。WheelEvent 的 deltaX/deltaY 不换算——它描述滚轮走了多远,不是文档滚了多远(详见下面的滚轮口径)。
  • visualViewport 的 width/height/offsetLeft/offsetTop/pageLeft/pageTop 同样除以倍数:底部工作台(dsh-better-sidebar)按 visualViewport.height 算展开高度,不换算的话高度会按视觉像素算出来、再被 CSS zoom 放大一遍,面板就会比实际可用空间高一截、跟内容错位。
  • 视口单位(vh/dvh/vw)在 zoom 子树里仍然按真实窗口解析,于是「按视口高度定尺寸」的盒子会被再放大一遍、伸出窗口下沿。插件注入一小段样式把它们重新指向布局视口(--page-zoom-vh 每次缩放时由插件写入):底部工作台宿主 [data-dsh-panel-host]{height:100vh} → 100%,Harness 弹窗的 100dvh 上限与可滚动区的 100vh 上限 → 同一个变量的 calc()。100% 时这些值与原生一致,等于没改。
  • 例外:原生视图(设置 → 账号 → 「查询用量」/「充值」打开的 DeepSeek 平台页)不是网页里的一层 DOM,而是桌面壳用 WebContentsView 按窗口 DIP 坐标盖上去的。它量的是占位元素的 getBoundingClientRect(),这个边界必须拿视觉尺寸,所以插件对「带 role="dialog" aria-modal="true" 且首个子元素是 header[data-window-drag] 的浮层」内部元素不做除法,让原生视图盖在正确的位置和大小上。不这么做的话,平台页会被摆成布局尺寸:只盖住窗口左上角一小块,返回栏也被压住,看起来就是「显示不完整」。
  • 原生视图的位置靠占位元素上的 ResizeObserver 上报,而换缩放倍数并不会改变占位元素的尺寸 —— observer 不触发,原生视图就停在上一个倍数的框里。插件每次改倍数(以及窗口 resize)时把占位元素临时撑高 0.5px、下一帧再还原,让 observer 连发两次、把当前边界重新报给桌面壳。

这是浏览器「页面缩放」的等效实现:布局按缩放后的 CSS 尺寸重排,测量出来的是布局尺寸。原生 webContents.setZoomFactor 拿不到,才用这套等价手段。

交互

操作效果
Ctrl + 滚轮 / 双指捏合按档位放大、缩小
Ctrl + Shift + = / -键盘放大、缩小(没有滚轮时用)
Ctrl + 0回到 100%
顶部短暂出现的胶囊− / 百分比 / + / ↺,1.8 秒后自动消失

档位是浏览器那套常用值:50% 67% 75% 80% 90% 100% 110% 125% 150% 175% 200% 250% 300%。

滚轮的口径跟浏览器一致:一个物理格 = 一个档位(Chromium 一格约 100px,阈值 50px 以上直接算一格,剩余量丢弃);触控板捏合和线性滚轮(deltaMode=1/3)是小步长连续事件,累加满一格才走一档,所以捏合是平滑推进而不是一格一格蹦。WheelEvent 的 deltaX/deltaY 不做倍数换算——它描述的是滚轮/触控板走了多远,不是文档滚了多远;换算过的话高倍率下(300% 时一格只剩 33px)一格会被误判成捏合。

Ctrl + = / -(不带 Shift)故意不接管:那是 Harness 自己的字号步进(文本/代码/终端字号 +1px),两边各管各的。

持久化

缩放倍数存在 localStorage 的 dsh.page-zoom.v1(origin 是 dsh-app://app),下次打开桌面版自动恢复;因为恢复值不是 100% 时会先亮一下胶囊,不会让人莫名其妙。

测试

自检页在 test/selftest.html(加载真实 client.js,28 条断言覆盖:尺寸链、滚动条、测量面、指针坐标、elementFromPoint、visualViewport、vh 宿主、原生视图 bounds、滚轮灵敏度、原生占位元素重报、复位与卸载还原):

chrome --headless=new --disable-gpu --user-data-dir=<临时目录> \
  --window-size=1280,820 --virtual-time-budget=8000 --dump-dom \
  "file://<repo>/test/selftest.html" | grep -o '<title>[^<]*</title>'

tools/cdp-zoom-hit.mjs 是更严格的一套 CDP 回归(真实鼠标/滚轮输入、elementFromPoint、原生视图 bounds)。它需要占一个本地调试端口,某些环境会拒绝 bind()(WSAEACCES),那时用自检页即可。

已知边界

  • 应用里只有 3 处 100vh/100dvh/100vw 的写法(确认弹窗、可滚动区、toast 的最大宽度)。它们是「视口单位不受 CSS zoom 影响」的老问题,插件用一小段样式把它们重新指向布局视口;只在内容特别高的弹窗上可能顶出窗口下沿,一般弹窗不受影响。
  • 桌面窗口在 win32 下用的是自绘标题栏,框架顶部有一段 40px 的标题栏内边距,它会跟着页面缩放(视觉上标题区会变高一点);系统画的那三个窗口按钮仍是物理尺寸。这是整页缩放的正常表现。
  • 缩放只作用于桌面窗口;网页版就是浏览器原生缩放。
  • 插件会临时改写 Element / Range / MouseEvent / VisualViewport 的原型 getter,卸载时全部还原。如果别的插件也硬编码了 vh/vw 尺寸或不经过 getBoundingClientRect() 的定位计算,需要各自按同样思路适配。

文件

文件作用
client.js客户端一半:缩放引擎 + 测量/坐标校正 + 百分比胶囊
index.js宿主一半:空实现,仅让 bundle patch 能挂载
cordis.patch.yml把 dsh-page-zoom 插进 profile
locale/en.json、locale/zh.json插件管理里显示的标题和说明
icon.svg插件图标
test/selftest.html自检页(见上)
test/hit2.htmlCDP 回归用的命中测试页
tools/cdp-zoom-hit.mjsCDP 回归脚本(见上)
LICENSEMIT

开发

  • 改完 client.js 的生效方式:win32 版没有「重新加载页面」快捷键,所以要么重启桌面版,要么在插件管理里把 page-zoom 禁用再启用(宿主会重新下发客户端代码)。
  • 自检页和 CDP 脚本都按仓库内的相对路径找 client.js,换机器、换目录都能跑;CDP 脚本用 CHROME=<path> 指定浏览器,跑完会自己删掉临时 profile。
  • 只用 dsh client 体系的槽位与主题令牌;client.js 里所有 DOM 操作都发生在 <body>/<head> 上的缩放宿主与胶囊上,不往 document.body 追加业务节点。
  • 提交前至少跑一遍:node --check client.js + 自检页(28/28)。CDP 脚本需要能监听本地端口,沙箱里常被拒(WSAEACCES),属已知限制。