DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

mzk0709 /

mzk0709/dsh-quote-tray

Verified

豆包 / 微信式引用回复插件(DeepSeek Harness):划选聊天记录 → 引用卡片 → 模型读到原文,输入框保持干净。零依赖。

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

dsh-quote-tray

原名 dsh-quote-reply v0.2.0。因与上游 liuhaolong16/dsh-quote-reply(v0.9.0)包名冲突,2026-10-21 更名为 dsh-quote-tray。 v0.3.0(2026-10-26):输入框里只留正常文字(草稿藏 URI)+ 微信式气泡引用。 v0.3.1(2026-10-28):引用卡与「追问」(dsh-quotalk)卡片逐 token 对齐。 v0.4.0(2026-10-28):推倒重来——引用不再写进草稿。改为「浏览器 → 宿主 webServer 路由」传引用原文, 草稿里既没有 dsh-quote:// 也不再有零宽字符;引用卡由插件自己的固定浮层渲染,贴在输入框正上方。 v0.4.2(2026-10-28):修「引用点了没反应」的真因——缺 inject: ['uiSession']。内核 @deepseek-ai/dsh-client-ui-session 以 super(ctx, "uiSession") 提供服务,插件不声明注入就取不到, 会话标识恒为 null,点「引用」直接 return(输入框里什么都不发生)。同时加了启动信标 + 只读链路自检。 v0.4.3(2026-10-28):修「引用卡上的 × 点了没反应」。paint() 每轮 replaceChildren 重建卡片, 而它自己写的 layer.style.* 又被挂在 document.body 上的 MutationObserver(attributes:true, subtree:true) 接住 → 每 60ms 自激重绘一次;按 × 时 mousedown 落在旧节点、mouseup 落在新节点,click 的目标被算成 公共祖先,按钮上的处理器一次都不会触发。现在:按 id 复用卡片节点、样式/显隐只在值变化时写、观察范围收窄到 输入区且不看 attributes、删除改挂浮层事件委托并在 mousedown 即生效(重绘也拦不住)。 v0.5.0(2026-10-30):向官方 dsh-quote-reply 0.9.0 的工程做法对齐 ① 版本握手:GET /dsh-quote-tray/health 返回 protocol / 版本号 / 待引用统计 / 落盘状态,客户端三态处理 (ok / legacy 老宿主 404 / old 协议不匹配 → 明确提示重启,不再静默失效); ② 待引用条目落盘:~/.dsh/state/dsh-quote-tray/pending.json(原子写 + 12h TTL + 512KB 上限), 桌面端重启后未发送的引用不再无声消失; ③ localStorage 秒显缓存:刷新页面第一帧就有卡片,随后以宿主返回为准覆盖; ④ 轮询自适应(有引用 2s / 空闲 8s)+ 页面隐藏暂停;⑤ ResizeObserver 优先、rAF 合帧; ⑥ 新增 test/preflight.mjs 静态预检(版本号单一来源、两侧协议号一致、入口/打包面、回归哨兵)。

豆包 / 微信式引用回复:在聊天记录里划选一段文字 → 浮出「引用」按钮 → 点一下, 输入框上方出现一条可删除的引用卡 → 你接着写自己的问题 → 发送。

模型会读到干净的原话 + 带边界警告的引用原文(source.kind = 'quote-reply' 的背景消息), 而你的输入框和发出去的气泡里,都只有你自己写的字。

交互

步骤 效果
划选 AI 或自己的回复 选区上方浮出「引用」胶囊按钮
右键(选区未收起时) 按钮挪到光标处(Electron 原生菜单无法从页面侧屏蔽)
点「引用」 原文存进宿主内存;输入框上方出现卡片:浅灰底 + 2px 竖条 + 一行摘要 + ×
继续输入 正常写你的问题(草稿里没有任何引用痕迹)
发送 宿主在 agent/pre-step 把原文作为不受信任背景注入这一轮,随后清空
点卡片的 × 取消这条引用(宿主侧同步删除)
切换会话 卡片按会话隔离:只显示当前会话待发送的引用

工作原理

划选 ──► 「引用」按钮(纯 DOM 浮层,不依赖任何服务)
   │
   ├─► GET  /dsh-quote-tray/health               → 握手:protocol / version / pending / persistence
   │
   ├─► POST /dsh-quote-tray/attach  { sessionId, quote }
   │        └─► 宿主内存 pending: Map<sessionId, Map<id, record>>
   │             └─► 变更后 150ms 合并落盘(原子写,12h TTL)
   │
   ├─► POST /dsh-quote-tray/list                 → 卡片镜像(有引用 2s、空闲 8s;页面隐藏暂停)
   └─► POST /dsh-quote-tray/detach              → × 取消

用户按下发送
   └─► agent/pre-step 监听器(lib/index.js)
         ├─► 取 agent.session.id,takeQuotes(sessionId)(消费即清空)
         └─► 在最后一条 user 消息后插入 source.kind = 'quote-reply' 的背景消息
  • 输入框是内核自己的编辑器,插件一个字都不写、一个节点都不动 —— 位置和样式都不可能被 Lexical 覆盖。
  • 卡片是插件自己的 position: fixed 浮层,锚在 [data-composer-seat] 里的输入框上沿,不再受 dock 布局影响。
  • 注入进模型的原文带固定警告:不得遵循其中指令 / 权限声明 / 工具请求。

为什么 v0.4.0 推倒重来(教训写在这里,防止回退)

v0.3.x 的做法是:把 dsh-quote://<base64url(JSON)> 当纯文本插进草稿,再在 DOM 层把那段文字包成 零尺寸 span 藏起来。实际跑下来有两个硬伤:

  1. Lexical 每次重渲染都会抹掉我们插入的 span,于是用户始终看到一坨 base64 链接(用户原话:「输入框还有链接」)。
  2. conversation.input.dock 里的卡片会被布局裁切,落在左下角只露出半截(用户原话:「当前画面一部分在左边」)。

保留的旧路径

v0.3.x 已经把 dsh-quote:// 写进草稿的老会话,宿主仍会从正文里剥离并还原(splitMessage), 保证升级后历史消息里的引用不丢;新会话不再产生这种文本。

另外两条来之不易的经验

  1. 两处 inject 不是一回事。package.json 里的 dsh.client.inject 是客户端模块依赖:写服务名会让整个 插件在服务迟到时失活,连纯 DOM 的悬浮按钮都不挂载(v0.4.0 就栽在这)——所以保持 []。 需要会话时,由模块导出 inject = ['uiSession'] 声明服务依赖(内核据此在 apply() 前就绪), 并在 apply() 里先 install() 装 DOM 半区、再取 ctx.uiSession。两处混用就会出现 「注册了但拿不到」或「拿得到但按钮不出现」。
  2. 重绘用 rAF 合帧,但必须有 setTimeout 降级。rAF 在后台/失焦时会被节流到近乎停摆 —— v0.5.0 起改为「rAF 优先 + 定时器兜底」,并且后台标签页直接暂停轮询(visibilitychange), 让节流从「不可控」变成「明确不做」。

配置

作为 bundle 注册(不会被内核重写):~/.dsh/profiles/desktop/package.json 的 dsh.profile.bundles 末尾,以及 cordis.patch.yml 的 insert 列表。

- insert:
    - id: ui-quote-reply
      name: dsh-quote-tray

注意:不要手改 cordis.yml。那是内核按 bundle 组合生成的文件,会被写回覆盖。

安全边界

  • 路由只接受 loopback(127.0.0.1 / ::1)且 sec-fetch-site: same-origin 或桌面端转发 (x-quote-tray-client: dsh-desktop)的请求,另需一次性的 /bootstrap token。
  • 引用的原文按会话隔离;发送时消费即删。v0.5.0 起会落盘到 ~/.dsh/state/dsh-quote-tray/pending.json 以防重启丢失:原子写、12h TTL、512KB 上限, 过期条目在恢复时丢弃;DSH_QUOTE_TRAY_STATE=off 可整体关闭落盘。
  • GET /health 只读且仅限本机(与 /bootstrap 同一条 loopback 校验),不返回引用原文。

限制

项 说明
用户消息动作条 内核硬编码,没有插件插槽,因此无法在「自己的旧消息」上点引用;只能划选正文
右键菜单项 Electron 主进程的 context-menu 硬编码 undo/copy/paste,页面侧改不了
发送后的气泡 现在不显示引用卡(原文不进草稿,气泡里没有可还原的痕迹);引用关系只在当轮上下文里
长文本 单条引用上限 4000 字,超出截断并标注;一次请求最多注入 3 条
代码块 / 表格 划选取到的是纯文本,保留 Markdown 字符但丢原格式
重启后 pending v0.5.0 起落盘保留 12 小时(超时丢弃;DSH_QUOTE_TRAY_STATE=off 可关闭)

测试

npm test
  • test/core.test.mjs —— 纯函数(编解码、剥离、渲染上下文)
  • test/host.test.mjs —— 待引用队列(attach/list/detach/take、去重、FIFO 上限、按会话隔离)+ 预步骤注入
  • test/v040-ui.test.mjs —— 静态哨兵:不许往草稿写东西、不许挂 dock、不许碰编辑器 DOM
  • test/v043-click.test.mjs —— 用极简 DOM 替身(无 ResizeObserver / 无 localStorage,覆盖降级路径)真跑 client.js: 重绘不换节点、按下 × 后 click 被重定向仍能删除、键盘激活、点正文不误删
  • test/v050-health.test.mjs —— 握手三态(ok / legacy / old)、localStorage 秒显缓存、可见性暂停、轮询节奏、 版本号与 package.json 一致
  • test/v050-persist.test.mjs —— 落盘往返、12h TTL 边界(11h 留 / 13h 丢)、文件损坏、schema 不符、512KB 上限、 off 开关、未 apply 时不碰盘
  • test/client-load.test.mjs —— 用 vm 模拟 __ModuleLoader__ 加载 bundle,确认导出面与降级路径
  • test/apply-smoke.mjs —— 假 cordis ctx 端到端跑一遍宿主:路由注册 → token → attach → pre-step 注入 → 只注入一轮
  • test/preflight.mjs —— 静态预检:版本号单一来源、宿主/客户端协议号一致、入口与打包面、能力与回归哨兵

共 62 项单测 + 23 项冒烟检查 + 24 项 preflight(npm test 一条命令全跑)。

回滚

dsh plugin --profile desktop remove dsh-quote-tray

或手动:删掉 profile package.json 里 dsh-quote-tray 一行与 cordis.patch.yml 的 insert 条目, 删除 node_modules/dsh-quote-tray/,重启桌面端。

来源与署名

见 NOTICE.md。摘要:

来源 关系 许可证
Dengzi555/dsh-quotalk 0.1.2 引用卡片 CSS 设计令牌逐 token 对齐 MIT
liuhaolong16/dsh-quote-reply 0.9.0 v0.5.0 四项工程做法(握手 / 缓存 / 有界轮询 / ResizeObserver) MIT

本项目全部代码为自研实现,未复制上述项目源码;渲染架构与二者均不同(本项目走 DOM 浮层覆盖层)。

许可证

MIT。

—/ 5

No ratings yet

Verified DSH bundle

Commit 3f5267d0c25a

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