dsh-paste-chip
长文本粘贴自动折叠成可展开的胶囊:悬停看正文与出处,发送时按原位展开;其他插件写入的长文本同样接管。
安装
dsh plugin --profile web add dsh-paste-chip
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
dsh-paste-chip
<p>
<a href="https://www.npmjs.com/package/dsh-paste-chip"><img alt="npm" src="https://img.shields.io/npm/v/dsh-paste-chip?color=2f81f7&label=npm"></a>
<a href="https://www.npmjs.com/package/dsh-paste-chip"><img alt="downloads" src="https://img.shields.io/npm/dw/dsh-paste-chip?color=3fb950"></a>
<a href="https://github.com/zengxiaolou/dsh-paste-chip/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/dsh-paste-chip?color=bf8700"></a>
<img alt="dsh plugin" src="https://img.shields.io/badge/DSH-plugin-7aa2ff">
<a href="https://github.com/zengxiaolou/dsh-paste-chip/stargazers"><img alt="stars" src="https://img.shields.io/github/stars/zengxiaolou/dsh-paste-chip?color=8250df"></a>
</p>
把超长粘贴折叠成一张可展开的引用胶囊,避免长草稿把输入框拖死。
- 粘贴阈值:≥400 字符 或 ≥12 行 折叠成一张胶囊
- 外部渠道(标注浏览器 / 划词注释 / 任何调用 API 的插件):不看长度,一律成胶囊
- 上限:20 万字符;超过直接拒绝并提示改用附件(超长正文进模型上下文也没有意义)
- 胶囊显示
粘贴「会议纪要:本周三…」· 1.2k 字(英文Pasted「…」· 1.2k chars)——带正文前缀,两个同字数的胶囊也能分辨;整块删除、整块撤销 - 发送时展开回完整正文;正文找不到就明确拦住发送,绝不把标签当正文发出去
为什么需要它
DSH 的输入框每敲一个键都在做全量工作:重算整篇投影、把整段草稿写进 localStorage。草稿一旦到几十万字,就会一卡一顿,极端情况直接把界面拖死。
这个插件把「长文本」从草稿里搬走:
没有它 · 有它
12k 字粘贴后草稿就 12k 字,每键写盘 12k+ · 草稿里只有一张 24px 高的胶囊,每键写盘回到标签大小
正文和你说的话混在一起,找不着边界 · 正文独立存放,发送时按胶囊所在位置展开
想确认粘了什么,得在几千字里翻 · 悬停或点击就能看正文前缀与上下文
为什么不改 core
DSH 的客户端是打包在 app.asar 里的,改 core 要连带改 ElectronAsarIntegrity 哈希并重签,且下一个版本会被覆盖。本插件只走三条已有公开通道:
能力 · 通道
插入胶囊 · 作用域事件 slash/input-insert-reference(内置 slash 插件插 chip 用的就是它)
拿稳定锚点 · InputActions.captureInsertion() → 带 draftRev 的 TokenSpan
发送时展开 · 注册 InputTriggerSource,实现 codec.serialize(ref)
正文不放进 ref id(core 版把 base64 塞进 ref,中文要放大 4 倍),而是按短 id 存在插件自己的 localStorage(前缀 dsh-paste-chip.v1.,最多 12 条 / 2MB,超了淘汰最旧的)。草稿里只有一个短 id,每次按键的持久化体积回到标签大小。
工作方式
1. 在 document 的捕获阶段监听 paste,只处理本插件所属 composer 的编辑器
2. 短文本直接放行给 core,不干预
3. 长文本:preventDefault() → 正文入库 → actx.bail(...) 插入胶囊
4. 插入失败:清掉刚写的正文,退回纯文本粘贴并提示——不吞内容
安装
以本地目录方式装进 profile(路径换成你自己的):
1. ~/.dsh/profiles/<profile>/package.json → dependencies 加 "dsh-paste-chip": "link:/path/to/dsh-paste-chip"
2. 同一个 package.json → dsh.profile.bundles 数组末尾加 "dsh-paste-chip" ← 真正的加载入口
3. ~/.dsh/profiles/<profile>/cordis.patch.yml → 末尾加 - id: dsh-paste-chip
然后在 profile 目录跑一次 pnpm install(需要 Node ≥22.13),再重启 DSH 客户端。
宿主启动时按 dsh.profile.bundles 的顺序叠 bundle 层,再叠 cordis.patch.yml;只加依赖和 patch 行不会加载插件。
客户端半的改动刷新页面就会重新加载;宿主半(lib/index.js)改了才需要重启。
可配置项
全部阈值都能改,两种方式二选一:
A. cordis.patch.yml(profile 里那一行加上 config)
- id: dsh-paste-chip
config:
collapseChars: 600 # 粘贴折叠阈值(字符)
collapseLines: 15 # 粘贴折叠阈值(行)
previewChars: 500 # 悬停预览多少字
B. 设置页(装了 @deepseek-ai/dsh-settings 时自动出现「dsh-paste-chip」分区)
键 · 默认 · 范围 · 作用
collapseChars · 400 · 100–10000 · 粘贴折叠阈值(字符)
collapseLines · 12 · 3–200 · 粘贴折叠阈值(行)
maxChars · 200000 · 1000–2000000 · 单段正文上限,超过拒绝
externalMinChars · 8 · 1–200 · 外部渠道插入的兜底长度,低于它视为手打
warmupMs · 1200 · 0–10000 · 挂载后预热窗口,期间不接管(防恢复草稿误折叠)
inputGraceMs · 500 · 0–3000 · 用户输入后的宽限期
previewChars · 320 · 80–2000 · 悬停浮层预览字数
contextChars · 40 · 0–200 · 悬停浮层上下文各取多少字(0 = 关闭)
hoverDelayMs · 180 · 0–1000 · 悬停多久弹出预览
leaveGraceMs · 60 · 0–1000 · 移出后多久收起
storeMaxEntries · 12 · 1–100 · 正文最多存几条,超出淘汰最旧
storeMaxBytes · 2097152 · 64KB–8MB · 正文存储总字节上限
实现方式:宿主半把这些值汇总到 GET /api/dsh-paste-chip/config,客户端启动时读一次并覆盖默认值;读不到就用同一份默认值,越界一律夹到边界(客户端与宿主各夹一次)。默认值写在 lib/client.js 的 DEFAULT_SETTINGS 与 lib/index.js 的 DEFAULTS,两边必须保持一致。
给其他插件用的主动接法(可选)
默认是被动接法:任何插件用 setDraft / insertText 写进草稿的长文本,都会被差异观察器接住,对方不用改一行代码。
需要更精确控制时(原子插入、草稿里已有胶囊、不想晚一帧),生产者调一行服务:
// 在会话槽组件里:span 必须和当前 revision 一致
const span = props.inputActions.captureInsertion()
const result = ctx.pasteChip.insert(body, { sessionId, span, label: '注释 · 3 条' })
// result: { ok: true, ref, label } | { ok: false, reason }
失败原因(都不静默):empty、too-long(超 20 万字)、span-required(没给 span)、no-session(会话 actx 解析不到)、bail-refused(插入被拒,正文已清理)。
服务版本:ctx.pasteChip.version。
预览开关的事件(v0.1.22)
常驻预览是谁打开的、谁关心它被关掉,插件自己不知道。所以给一个订阅口:
const off = ctx.get('pasteChip').watchPreview((event) => {
// { phase: 'pin', ref }
// { phase: 'unpin', ref, reason: 'close' | 'dismiss' | 'switch' | 'removed' }
})
reason 是这套 API 的全部价值,别混用:
reason · 什么动作 · 联动方一般该做什么
close · 点了卡片右上角的 ✕ · 这是唯一的「用户主动关闭」,可以据此取消对应动作
dismiss · Esc / 点浮层外部 · 收起预览而已,不要当成取消
switch · 点了另一枚胶囊,预览换过去 · 同上,旧的那条只是被替换
removed · 外部插件调了 closePreview(ref) · 胶囊已经没了,卡片顺手收掉
ctx.pasteChip.closePreview(ref):只收指定 ref 的常驻预览(不是当前预览就返回 false,不误伤别人),广播 reason: 'removed'。
服务必须用 ctx.get('pasteChip') 取。cordis 上下文里直接读没在 inject 里声明的服务会抛
cannot get property ... without inject,属性访问永远是死的。
非粘贴渠道
标注浏览器、划词询问这类插件是用 setDraft / insertText 写草稿的,不触发 paste,所以另有一条通道:比对前后两次草稿,认出「这一次新塞进来的那一段」。
这条通道没有字数/行数阈值:凡是外部渠道传进来的非空白文本,一律成胶囊。 粘贴仍保留 400 字阈值(短粘贴保持原文,避免粘个链接也变胶囊)。
怎么区分「外部传进来的」和「用户自己打的」:真实键入一定先触发 beforeinput,外部 setDraft 不会。所以 beforeinput 之后 500ms 内的草稿变化算用户输入,不接管;超出这个窗口且不是用户输入的插入,一律折叠。
三道保护,缺一个都会误伤:
保护 · 为什么
挂载后 1200ms 预热窗口内不接管 · 会话切换/页面加载会"恢复"草稿,那段文本和外部插入长得一模一样
用户输入事件后 500ms 内不接管 · beforeinput / input / keydown / compositionstart / compositionend 任一命中都算。只监听 beforeinput 不够:输入法组合提交有时不发它,实测漏了 7 次,产出「粘贴文本 · 1 字」这种胶囊
少于 8 个字的插入不接管 · 误判兜底,不是产品阈值。手打/输入法提交一次就是 1~3 字;真实外部渠道塞的是整段注释。日志里 7 次误折叠全是 1 字
空白插入不接管 · 纯换行/空格没有折叠价值
草稿里已有胶囊时跳过 · 有 chip 时 clipboard 偏移与 detect 偏移不再一致,硬插会错位替换
只处理纯插入(removed === 0) · 替换、删除的量算不准,不接管
事件:insertion.detected / insertion.collapsed / insertion.skip(带原因)/ `insertion.warmup
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:8,最后更新 2026-10-26