DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

qgynisc /

qgynisc/dsh-inline-pastes

Verified

DeepSeek Harness Web GUI 插件:粘贴图片即在文字中插入 image-1.png 内联胶囊,悬浮名字预览缩略图,自动编号(文字中插图 / hover 预览)

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@65f5defe

dsh-inline-pastes

npm test license release

简体中文 | English

给 DeepSeek Harness Desktop(内置 Web GUI) 加四件事:

  1. 文字中插图 —— 粘贴图片时,在光标处插入一个内联胶囊 [图标] pic-1,图片本身仍按官方管线作为附件随本条消息一起发出(模型照样收到图)。
  2. 悬浮预览 —— 鼠标停在这个名字上(输入框里的内联胶囊、已发送气泡里的同名文字)弹出缩略图预览卡,带尺寸与体积;移开即收,Esc 也收。
  3. 自动编号 —— 名字自动编排:pic-1.png、pic-2.png ……,按媒体大类分前缀、扩展名跟真实 MIME 走,为 mp3 / pdf 等未来类型预留。
  4. 设置页 —— 「设置 → 混合排版」一页管全部:命名前缀、胶囊显示短名/完整名、缩略图角标三档大小、界面语言(跟随 DSH / 中文 / English)、以及总开关、粘贴接管、悬浮预览、发送前自检等全部行为开关,改完立刻生效。

安装

客户端半边只用 Web 客户端 API(conversation / sessions / uiSession / inputTriggers)和 DOM, 没有依赖桌面专属桥(如 Electron preload 注入的 __DSH_HOST_PATHS__), 所以 DSH Desktop 与 dsh web(浏览器)下同样可用。

方式一:克隆到本地,用仓库里的脚本(本机实测过)

git clone git@github.com:qgynisc/dsh-inline-pastes.git
cd dsh-inline-pastes
npm run build                # 可选:lib/ 已随仓库入库,构建产物就是加载器要的那份
npm run install:desktop      # 装进 desktop profile(自动备份 package.json / pnpm-lock.yaml)
# 别的 profile:node scripts/install.mjs --profile web

装完重启 DeepSeek Harness(客户端插件要重新加载 bundle 才会生效)。

方式二:用 DSH 自带的插件安装命令

dsh plugin --profile desktop add git@github.com:qgynisc/dsh-inline-pastes.git

卸载:npm run uninstall:desktop,或直接删掉 profile 里 dsh.profile.bundles 的 @qgynisc/dsh-inline-pastes 一行与 dependencies 里的对应条目。

验证装好了没:重启后浏览器控制台里 __dshInlinePastes.version 应等于插件版本号。

实际效果

① 文字中插图 —— 粘贴图片,胶囊落在光标处,图片照常作为附件随消息一起发出:

在文字中插入图片胶囊:1、功能测试 image-1.png / 2、功能测试 image-2.png / 3、实现了多行插入图片,与语义相配合 image-3.png

(截图上方那块深色浮层是 macOS 的文件提示,与本插件无关。另外这两张截图拍的是默认前缀还是 image 的时候,现在默认是 pic,且胶囊默认只显示短名 pic-1 —— 见下面的设置页。)

② 悬浮预览 —— 鼠标停在胶囊里的名字上,弹出预览卡(缩略图 + 名字 + 尺寸 · 体积),移开即收:

悬浮名字 image-1.png 时弹出预览卡:卡里是图片本身的缩略图,页脚显示 image-1.png 与 1490×655 · 346 KB;下方输入框里的同名胶囊处于悬浮高亮


1、功能1  [🖼 pic-1]
2、功能2  [🖼 pic-2]
3、修改bug如图:[🖼 pic-3],把它变成可自动定位。

设置页:设置 → 混合排版

装好后重启 DeepSeek Harness,设置里会多出一行 混合排版(英文界面下是 Mixed layout)。面板内容:

项 默认 说明
界面语言 跟随 DSH 跟随 DSH 的界面语言(locale 服务),也可以强制中文 / English —— 面板文案与左侧导航名一起变
命名前缀 pic 可选 pic / image,或自定义(A-Za-z0-9_-,以字母数字开头)
胶囊显示 短名 pic-1 或完整名 pic-1.png;只影响胶囊里显示什么(见下节)
缩略图角标 中 关 / 小(12px)/ 中(14px)/ 大(18px)
启用插件 开 关掉后完全退回官方行为
接管图片粘贴 开 关掉后图片只进官方附件轨
名字悬浮预览 开 悬浮预览卡总开关
预览卡显示尺寸与体积 开
发送前自检 开
自检不通过时拦一次回车 开 关掉 = 只提示不拦
恢复默认设置 — 全部回到出厂默认(不动编号计数)
  • 设置按设备存在 localStorage(dsh-inline-pastes:settings:v1),不跟会话/账号同步;换浏览器或清站点数据就回到默认值。
  • 改完立刻生效:角标马上换大小/消失,胶囊从下一张图开始按新名字显示;「启用插件」关掉与重新打开都是立刻生效(关着的时候设置页依然在,随时能开回来)。
  • 万一拿不到 React / slots / locale(很旧的 DSH、或非 Web 客户端),设置页整块不出现,粘贴、胶囊、预览、角标照常工作。

胶囊里的短名只是「显示别名」

「胶囊显示」默认短名:胶囊里写 pic-1,但同一条引用在发送时由 codec 重新序列化成完整文件名 pic-1.png。四处对照:

位置 看到的名字
输入框里的胶囊 pic-1(短,省地方)
发送出去的文本 / 已发送气泡 pic-1.png
附件名 / 模型侧 handle 行 pic-1.png

模型侧的一一对应完全不受影响(文字里与图头上都是 pic-1.png,见下一节)。代价是:输入框里看到短名,发出去之后气泡里是完整名;想要两处一致,把「胶囊显示」切成「完整文件名」即可。

悬浮预览两种写法都认:在输入框悬浮短名、在气泡里悬浮完整名,都弹同一张图。


多张图片时,名字和图片怎么对应(不会乱)

模型侧是「贴名字」,不是「靠数顺序」。 DSH 给模型发图时,会在每张图前面紧挨着插一行文字 (@deepseek-ai/dsh-llm 的 requestImageHandleText,DeepSeek / pi-ai 两个适配器都走它):

Image "pic-1.png" (sha256:abc…); request preview 1972x397px. …
<图片本体>
Image "pic-2.png" (sha256:def…); request preview 1200x800px. …
<图片本体>

所以模型看文字里的 pic-1.png 时,同一条消息里那张图头上就写着同一个名字,一一对应; 即使中间混进拖拽/+ 按钮加进来的图片(它们用自己的原文件名,同样带 handle 行),也不会串。

界面侧:DSH 把一条消息的所有图片排在气泡上方的附件行里,文字在气泡里 —— 所以人眼看到的是 「上面一排缩略图 + 下面文字里的名字」,靠名字对齐(缩略图本身不显示名字,只有 hover 的无障碍标签)。

本插件的三条保证:

  1. 同一条消息内:粘贴顺序 = 编号顺序 = 附件顺序(一次粘多张也按剪贴板顺序);
  2. 把胶囊从文字里删掉、但 dock 缩略图还留着时,编号不会重开(否则一条消息会出现两个 pic-1);
  3. 只有发送之后(草稿与附件都清空)才从 pic-1 重新开始。

两个仍然要注意的:

  • 发送前从 dock 里删掉中间某张缩略图、文字里的胶囊却没删,这条消息的编号就会与图片个数错开一格。 要么删缩略图时把胶囊一起删,要么发送前扫一眼;
  • 编号按消息重开(这是你要的),所以不同消息里会出现同名的 pic-1.png。同一条消息内不会歧义 (handle 行贴着图),但如果跨消息引用,名字不唯一。想要全会话唯一,把 CONFIG.counterScope 改回 'session'。

命名规则

粘贴内容 生成的名字
PNG 截图 pic-1.png、pic-2.png …
WebP / JPEG / GIF pic-1.webp / pic-1.jpg / pic-1.gif
MP3 / WAV audio-1.mp3 / audio-1.wav
MP4 / MOV video-1.mp4
PDF pdf-1.pdf
docx / xlsx / pptx doc-1.docx
txt / md / json text-1.md
zip / tar / 7z zip-1.zip
其他 file-1.<真实扩展名>
  • 序号按前缀分别计数(pic-1 与 audio-1 各数各的)。
  • 每条消息都从 pic-1.png 重新开始;同一条消息里连续粘则接着排(pic-1 → pic-2 → pic-3)。 判据是「当前草稿里还没有本插件的胶囊」,所以先打字再插图(草稿非空)同样算新消息;发送后草稿清空,下一条消息自然又从 1 开始。
  • 想改成「整个会话连续排」:src/client.js 的 DEFAULTS.counterScope 设成 'session' 再 npm run build(这一项设置页暂未开放)。
  • 计数持久化在 localStorage(按会话 id 分键,只留最近 100 个会话):刷新页面/重启应用后,同一条还没发送的消息接着往下排,不会把 pic-1.png 又发一遍。
  • v1 只接管图片粘贴(image/png|jpeg|webp|gif)。音频/文档的命名能力已就位,接的时候只需在 handlePaste 的入口放行对应 MIME 即可。

配置

首选方式:设置 → 混合排版(见上面的设置页一节),改完立刻生效、按设备存 localStorage。 代码里的默认值在 src/client.js 顶部的 DEFAULTS(改完 npm run build):设置页没开放的项、或想换出厂默认时改这里。

键 默认 设置页 说明
enabled true ✅ 总开关
interceptPaste true ✅ 关掉就退回官方行为(图片只进附件轨)
hoverPreview true ✅ 悬浮预览卡总开关
startIndex 1 — 序号起点
counterScope 'message' — 'message' 每条消息重开 / 'session' 整个会话连续排
hoverDelayMs 140 — 悬浮多久弹预览
prefixes { image: 'pic', … } ✅(图片) 各媒体大类的前缀
chipDisplay 'short' ✅ 胶囊显示短名 pic-1 还是完整名 pic-1.png
badgeSize 'md' ✅ 缩略图角标档位:off / sm / md / lg
badgeScanMinIntervalMs 80 — 角标重扫的最小间隔(流式输出时 MutationObserver 很吵,节流用)
maxRemembered 600 — 预览记忆上限(超出淘汰最早的并释放 object URL)
showMeta true ✅ 预览卡是否显示「宽×高 · 体积」
auditBeforeSend true ✅ 发送前自检:名字与图片对不上时在输入框上方提示
blockOnAuditFailure true ✅ 自检不通过时第一次回车拦一下(再按一次放行);false = 只提示不拦
lang 'auto' ✅ 本插件自己那套文案的语言:auto 跟随 DSH / zh / en

设置存在 localStorage 的 dsh-inline-pastes:settings:v1(与编号计数器分开存,「恢复默认设置」不会清掉编号)。 控制台里也能改:__dshInlinePastes.setSettings({ badgeSize: 'lg' })、__dshInlinePastes.resetSettings()。

胶囊必须注册 codec(v0.3.1 修的真实 bug)

官方在发送时不是读节点上缓存的文本,而是按「胶囊 source → codec」重新序列化一次: ui-conversation 的 sinkSerialized 对每个引用调 inputTriggers.serializeReference(source, ref), owner 缺失或没有 codec 就直接 reject,草稿被弹回并报

slash: no serializer for reference source "inline-paste"

所以本插件用 ctx.inputTriggers.registerSource({ trigger, name: 'inline-paste', codec }) 注册了一个 只带 codec 的 source:触发符用不可输入的 \u0000inline-paste(核心的触发器检测写死只认 @ 和 /, 所以它永远不会出现在任何菜单里),codec.serialize(ref) 把隐藏 ref(dsh-inline-paste:<文件名>)还原成文件名。 拿不到 inputTriggers 时,插件退回纯文本插入:宁可少个胶囊,也不能让发送被拦。

缩略图上的序号角标

dock 的附件轨和已发送气泡里的图片行,每张缩略图左下角都会显示一个序号小圆点 (pic-3.png → 3),悬浮缩略图还能看到全名(原生 title)。开着大图看的时候也能一眼知道 「这张是第几张」。

大小可调:设置页的「缩略图角标」有 关 / 小(12px)/ 中(14px,默认)/ 大(18px) 四档, 改完立刻生效(设置页里还给了一个实时样例)。档位只换 CSS 类(.dsh-ip-badge--sm|md|lg), 不重挂节点。

序号是"读"出来的,不是数出来的:官方两处的缩略图 alt 填的都是文件名 (dock 是 attachment.file.name,气泡里是持久附件 ref 的 name —— 也就是发给模型时那行 Image "pic-1.png" … 的来源),所以即使你删过其中某张图、顺序变了,角标也不会错位。

实现方式与代价(唯一一处 DOM 注入):官方的 conversation.input.attachments 声明是 kind: "single",slots.register 遇到第二个注册会直接抛错;想接管就得把它顶掉、自己重写整条 附件轨(缩略图 / 删除 / 拖拽 / 大图 / 上传进度),不值得。所以这里是把角标节点挂进缩略图的 <button> 内侧(overflow:hidden 之内、左下角避开右上角的删除 ×),配合 MutationObserver 自愈:React 重渲染把它冲掉后会自动再挂一次。

这是本插件唯一一处往 React 管理的 DOM 里塞节点。失效的表现只是角标消失,不影响粘贴、 胶囊、发送、预览;一键关掉:设置页的「缩略图角标 → 关」。

发送前自检(保险)

什么时候提示:本条消息的草稿里,出现下面两种「对不上」时,输入框上方会挂一条提示 (走官方 composer 的 notice 位,shell.notify('info', …),不往 React 管的 DOM 里塞节点):

  • 文字里点了名、但本条消息里没有这张图(比如你把 dock 里的缩略图删了、文字里的胶囊还在) → 图片名自检:pic-1.png 在本条消息里找不到对应图片(再按一次回车仍可发送)
  • 同一个名字在本条消息里出现两次

拦住的分寸:自检不通过时,第一次回车会被拦下(提示已经挂在输入框上方), 再按一次回车照常发送 —— 这是保险,不是门禁,绝不静默改你的内容,也不会把你卡死。 自检通过、Shift+Enter 换行、输入法组合中、Alt/AltGraph、不在输入框里 —— 一律不碰。

查什么、不查什么:只查「文字里提到的名字是否真有那张图、且不重复」。官方给模型发图时会把名字贴在图上 (见上一节),所以「有图但文字没点名」不算错,图照样带着名字发出去,不报。

要关掉:CONFIG.auditBeforeSend = false(连提示一起关),或 CONFIG.blockOnAuditFailure = false(只提示不拦)。

粘接管边界(这是本插件最看重的纪律)

只在全部满足时才接管,否则一律原样交还官方 intakeFiles:

  • 事件发生在 composer 内(认 [data-input-scroll] 标记);
  • 剪贴板里的文件全是官方支持的图片类型(混进一个 pdf 就整批放行);
  • 已有打开且保留的会话(uiSession.current);
  • 输入框处于 plain / claimed 相位(提交中、裁决中不动);
  • 没超出会话的 imageLimits(超了就交给官方弹它自己的提示)。

接管后任何一步失败(哪怕一个胶囊都没插进去)都会释放草稿并 return false,绝不 preventDefault。 上一代同类插件(@qithird/dsh-paste-input-plus)就是无脑在 window 捕获阶段拦 paste,又因为 DSH 0.2.0-rc.2 起 sessions.list 快照没有 current 字段而读不到会话,结果既没插成胶囊、 又把官方图片粘贴整个堵死。本插件因此:

  • 当前会话只从 uiSession.current.getSnapshot() 取(老快照字段只作兜底);
  • handlePaste 全程 try/catch,拦截器捕获异常后不拦事件。

目录结构

src/host.js       宿主半边(占位:只为让 dsh-client-modules 能按包名找到 client bundle)
src/client.js     浏览器半边(设置存储 / 命名规划 / 粘贴接管 / 胶囊插入 / 悬浮预览 / 角标)
src/panel.js      设置页(React 面板 + 中英词库 + settings.section 注册;构建时内联进 client.js)
src/styles.css    预览卡 + 角标 + 设置页样式(构建时注入 client bundle)
scripts/build.mjs         包装成 window.__ModuleLoader__.load({ id, factory }) 产物(含上面两个锚点替换)
scripts/verify-browser.mjs 真 Chrome 端到端验证(静态服务器 + harness 回执)
scripts/preview-settings.mjs 设置页离线预览出图(无头 Chrome 截图 → docs/settings.png)
scripts/install.mjs       装进 / 卸出某个 DSH profile(自动备份 package.json / pnpm-lock.yaml)
tests/unit/*.test.mjs     单测(直接测构建产物本身)
tests/harness/*           浏览器 harness(假 ctx + 真 ClipboardEvent / elementFromPoint)
tests/helpers/react-shim.js 极小 React 替身(让设置页组件能在单测与 harness 里渲染成一棵树)

开发

npm run build          # → lib/index.js、lib/client.js
npm test               # 构建 + 单元测试 + 真 Chrome 端到端(需要本机有 Chrome)
npm run test:ci        # CI 跑的同一条:构建 + 单测 + 用临时 profile 做加载器解析链自检
npm run verify:browser # 只跑浏览器层(需要本机 Chrome;可用 --chrome <path> 指定)
npm run preview:settings # 设置页离线预览出图 → docs/settings.png(无需装 DSH、无需重启)
node scripts/verify-install.mjs --simulate   # 干净机器/CI 上没有真 profile 时,临时造一个再自检
npm run install:desktop   # 装进 desktop profile(当前桌面端用的就是它)
npm run uninstall:desktop # 卸出

构建产物形态与官方 client 包一致(window.__ModuleLoader__.load({ id, factory })), id 必须等于 package.json 的 name,否则 dsh-client-modules 会报 "loaded without registering"。

关于 require:构建脚本只放行 require('react')(设置页要用)。React 属于 shell 的 基线模块表 PLATFORM_MODULES(官方 dsh-client-modules 文档原话:the shell seeds a frozen module table — React, Cordis, and static UI libraries),所以它不需要在 dsh.client.external 里声明;其余任何模块解析仍然会被构建拒掉。拿不到 React 时设置页整块跳过,不影响其它功能。

名字会重复出现,预览怎么不串台(v0.3 起)

编号按消息重开之后,同一个会话里会出现多条消息都叫 pic-1.png。悬浮预览因此优先到「那条消息自己的附件行」里按序号取图([data-chat-node-key] → [data-message-attachments] img 的第 N 张),而不是查全局名字表:

  • 每条消息都预览自己那张图,不会串到最新那条消息;
  • 顺带把「刷新页面后历史消息没有预览」这个 v1 限制也解决了大半——用的是会话授权的持久图片地址,只要那张图已经在气泡里渲染出来就能预览。

只有输入框草稿里的名字(还没发送、没有消息 DOM 可依)才回退到本插件的内存登记表。

持续集成

.github/workflows/test.yml 在每次 push / PR 跑 Ubuntu × Node 20/22/24 三档,执行 npm run test:ci(构建 → 单测 → 临时 profile 的加载器解析链自检)。

它专门盯两类本机抓不到的问题:

  • 跨平台:macOS 文件系统大小写不敏感、Ubuntu 敏感 —— 导入路径大小写写错只有 Linux 会炸;
  • 跨 Node 版本:package.json 声明 engines >= 20,而开发机只在 Node 26 上跑过。

(它抓不到「DSH 升级改坏了官方契约」—— 单测用的是假 ctx,不是真 DSH;这类只能实测。)

已知限制

  • 输入框里的短名与气泡里的完整名不一致(默认行为):胶囊显示 pic-1,发出去之后气泡里是 pic-1.png。 这是「显示别名」方案的代价,模型侧不受影响;想两处一致就把「胶囊显示」切成完整名。
  • 如果某条消息里的图片被手动删过(附件序号不连续),「按序号取第 N 张」会偏;此时输入框里的胶囊仍然准确(走登记表)。
  • 气泡里的图片还在加载中(缩略图转圈)时,那一条的悬浮预览不会弹。
  • 点击胶囊目前没有动作(appearance: 'file' 会让光标变手型,但不会打开大图查看器)。官方 conversation.message.images 是单占用 slot,抢过来会顶掉原生图片渲染,所以没有走那条路。
  • 胶囊里的图标沿用官方 appearance: 'file' 的文件图标;想换成图片图标可以用 CSS 覆盖 [data-composer-chip='inline-paste'] svg(见 src/styles.css)。

调试钩子

插件在浏览器里暴露了一个只读诊断对象(浏览器控制台可用):

__dshInlinePastes.version              // 插件版本
__dshInlinePastes.settings             // 当前设置(活对象,与设置页同一份)
__dshInlinePastes.setSettings({...})   // 改设置(等价于在设置页上点一下,立刻生效)
__dshInlinePastes.resetSettings()      // 恢复默认设置
__dshInlinePastes.useChips()           // 胶囊模式是否可用(false = 已退回纯文本插入)
__dshInlinePastes.auditNow()           // 立刻跑一次发送前自检,返回 {ok, duplicates, dangling, message}
__dshInlinePastes.debug.lastPaste      // 最近一次粘贴:{ sessionId, phase, files, inserted, names }
__dshInlinePastes.currentSession()     // 插件此刻认为的当前会话 id
__dshInlinePastes.remembered()         // 已记住多少张预览图
__dshInlinePastes.has('pic-1.png')   // 某个名字能不能预览
__dshInlinePastes.forgetSession(id)    // 清掉某会话的计数(下次从 pic-1 重开)

判断「编号是不是真的按消息重开」:粘一张(names: ['pic-1.png'])→ 再粘一张(['pic-2.png']) → 发送 → 再粘一张(又回到 ['pic-1.png'])。

排错

  • 插件没生效:先在设置里确认行 inline-pastes 已加载;再看浏览器控制台有没有 [dsh-inline-pastes] ... 的告警(插件任何一步失败都会打在这里,并且不会拦你的粘贴)。
  • 粘贴后没有胶囊:把设置页的「接管图片粘贴」临时关掉对比一次——如果关掉后官方行为正常, 说明是接管条件没过(多半是没打开会话,或剪贴板里混了非图片文件)。
  • 设置里找不到「混合排版」:客户端插件只在 DSH 启动时加载,装完/升级完要重启; 重启后还没有就看控制台有没有 react unavailable / slots service unavailable 这两条告警 ——有的话说明当前 DSH 没提供 React 或 slots 服务,此时设置页会整块跳过(其它功能不受影响)。
  • 预览卡不出现:悬浮要停在名字本体上(胶囊或文字),且该名字必须是本次页面生命周期内粘贴的。
  • 设置 → 插件里的卡片只显示 dsh-inline-pastes、少了 @qgynisc,也没有描述/图标: 这个包在 0.5.0 之前叫 dsh-inline-pastes(没有 npm scope),那时候装进 profile 的键就是不带 scope 的。DSH 是按 profile 清单里的键名去解析插件元数据的(<键名>/package.json), 键名与包名不一致时拿不到 title / description / icon,卡片只能回退显示那个键名 (注意行 inline-pastes 的 module name 一直是带 scope 的,只有「包」这一层会掉)。 重跑 npm run install:desktop 即可:新版 scripts/install.mjs 会把旧键从 dependencies 与 dsh.profile.bundles 迁到 @qgynisc/dsh-inline-pastes,并清掉 node_modules/ 里残留的旧软链, 之后重启 DeepSeek Harness 卡片就带上 scope 了。
—/ 5

No ratings yet

Verified DSH bundle

Commit 65f5defe77ae

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout