dsh-image-fit
给 DeepSeek Harness(DSH) 用的插件:输入栏多一个「长图适配」开关,把因为规格送不进去的图片自动切成能送进去的尺寸,再交给 DSH 的附件通道。
长截图切成方形图块
它解决什么问题
DSH 对单张图片有硬限额:长边 ≤ 8192、体积 ≤ 20 MiB、像素 ≤ 6400 万。超了直接拒收。
还有一条更隐蔽的限制:即使没超限额,服务端也会做归一化——超过 4.19M 像素的图会被压到长边 2048。一张 1080×7250 的手机长截图压完只剩 305×2048,字全糊,等于没发。
这两类都属于「规格问题」而不是「图片有问题」,插件只在命中时才动手,其余图片原样放行。
怎么用
装好并重启 DSH 后,输入框左侧会多出一个开关:
● 长图适配 ← 开:命中规则的图片会被接管处理
○ 长图适配 ← 关:全部按原样交给宿主
状态存在 localStorage(键 dsh-image-fit.enabled),默认开启。开着的时候,把图片拖进输入框 / 直接粘贴 / 用回形针选择,插件会在事件到达宿主之前截下来:
createImageBitmap 读尺寸(自动处理手机照片的 EXIF 方向);
- 按下面的表格判定要不要处理;
- 需要就切块或缩小,每块都是 ≤2048 边长、≤4.19M 像素、≤4 MiB 的 JPEG;
- 把结果塞进当前消息,投递分两条通道:
- 首选
conversation.createDrafts() 加 shell.addAttachments(),有明确回执;
- 这套私有接口拿不到或报错时,退回兜底通道:给输入框自带的隐藏
<input type="file"> 赋值并触发 change,等于替用户「选了一次文件」,走宿主自己的上传流程;
- 两条都不行就在输入框上方提示失败,不静默吞掉你的图片;
- 成功后提示一句「1080×7250 切成 7 张 1080×1080」。
尺寸本来就合规的图片也会被接管,但只是原样重新投递,效果和没拦一样。
判定规则
| 情况 | 处理 | 原因 |
|---|
| 超 20 MiB / 6400 万像素 / 长边 8192 | 长条(长短边比 ≥ 2.5)→ 切块;其余 → 整体缩小 | 硬限额,原图必然被拒 |
| 没超硬限额,但 > 4.19M 像素且长短边比 ≥ 2.5 | 切块 | 会被归一化压到长边 2048,字会糊 |
| 其余(普通照片、小图、方图) | 不动 | 宿主自己处理得更好 |
GIF 动图只在触发硬限额时整体缩小,不切块(切了就成静态图了)。
切块规则
- 块是正方形,边长从
min(短边, 2048) 起算;
- 块数超过 20 张(宿主单条消息的图片上限)就按比例缩小边长重算,直到装得下;
- 每块都是 ≤2048 边长、≤4.19M 像素、无 EXIF 的 JPEG,正好命中服务端归一化的透传条件,不会被二次压缩;
- 起点均匀铺开:首块贴 0、末块贴底,相邻块留少量重叠,保证一个像素都不丢,一行字不会被切两半;
- 编码质量阶梯
[0.92, 0.85, 0.78, 0.7, 0.6, 0.5],逐级降到 ≤ 4 MiB。
例子:1080×7250 的截图 → 7 张 1080×1080,重叠约 52px;500×200000 的极长图 → 20 张,整体缩到 0.197 倍。
安装
这是一个 bundle 级的 DSH 插件(dsh.bundle 加 cordis.patch.yml),要求 DSH 0.2.x 桌面版。
方式一:在 profile 里登记
把本仓库放到任意目录,然后在你的 profile($DSH_HOME/profiles/<name>/)里:
package.json 的 dependencies 加一条:"dsh-image-fit": "link:<本仓库绝对路径>"(Windows 下用正斜杠);
dsh.profile.bundles 数组末尾加 "dsh-image-fit";
- 在 profile 的
node_modules 下建一个指向本仓库的链接(Windows:mklink /J node_modules/dsh-image-fit <本仓库绝对路径>,或直接用 pnpm link)。
方式二:plugin_manager
如果你的 DSH 会话带 plugin_manager 工具,直接 action="install_bundle"、target 指向本仓库绝对路径即可,它会代为写入 profile。
装完必须重启 DSH 应用:宿主只在启动时扫描 bundle 列表,客户端 HMR 只覆盖已经在跑的插件,不会自动加载新增的 bundle。
对宿主私有接口的依赖
插件用到两处非公开的宿主接口,都做了降级:
conversation.input.for(scope) 到 shell.addAttachments(ids):拿到会话 shell 就直接把附件塞进草稿;
conversation.createDrafts(sessionId, files):让宿主自己创建附件草稿(它会顺带做尺寸探测)。
任何一处不可用都会退回隐藏 input 的兜底通道,所以宿主升级后最差是效果变差(走宿主自己的限额提示),不会报错崩掉。
文件结构
package.json bundle 清单(dsh.bundle / dsh.client 字段)
cordis.patch.yml 插入条目(id、name 都是 dsh-image-fit)
index.js node 半边,空 apply(纯浏览器插件,只为让 dsh.client 被扫描到)
client.js 全部逻辑:切块算法 + 事件拦截 + 开关按钮
test/plan.test.mjs 从 client.js 里切出切块代码直接求值,测规划 / 像素覆盖 / 判定
test/entry.test.mjs 假 React + 假 DOM 渲染真组件,测插槽注册 / 开关 / 事件拦截 / 两条投递通道的回退
test/make-browser-test.mjs 从 client.js 里原样切出切图代码,生成 test/carve.html
test/carve-run.js 浏览器里跑的端到端断言(页面回传结果)
测试
node test/plan.test.mjs # 切块规划、零空隙覆盖、判定规则
node test/entry.test.mjs # 组件渲染与事件拦截
node test/make-browser-test.mjs # 生成 test/carve.html(改过 client.js 之后要重跑)
test/carve.html 也可以直接用浏览器打开,断言结果打印在页面上并写进 document.title(PASSED / FAILED)。
实测结果:
plan.test.mjs、entry.test.mjs 全 PASS;
- 真 Chromium(headless):1080×7250 → 7 张 1080×1080、全是 JPEG、每张 ≤4 MiB、每张中心像素与源图对应位置一致、85 ms 完成;6000×4000 → 缩成 2048×1365;1080×29000 → 15 张、每张 ≤2048 且 ≤4 MiB。
已知限制
- 每条消息最多 20 张:超长图会整体缩小到刚好 20 张,宁可小一点,不丢内容;
- 粘贴时如果剪贴板里同时有文字,插件整体放行,不抢你的文字;
- 拦截只装在当前输入框卡片上(
[data-composer-card]);宿主 DOM 结构变了就退化成不拦截,不会误伤;
- 只在 Chromium 系(
createImageBitmap 加 OffscreenCanvas)下做过验证。
License
MIT © Chen-Zhenghao