![]() 玄同 765 AI Engineer | ▍深耕领域 AI Agent 架构设计 Agent Harness 工程 Context Engineering 多智能体协同 & RAG |
| 「 让 AI 交互更智能,让技术落地更高效 」 | 欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能! | |
AI 长回复没法逐段批注?开源划词引用与行内评论胶囊插件实战(已获官方收录)
📚 《DeepSeek Harness (DSH) 插件工程与 Agent 运行时实战》连载专栏
在日常使用 AI Coding Agent 进行长代码重构、架构方案审阅或复杂排障时,开发者经常面临一个高频交互痛点:当模型一次性输出了上百行代码或十几条技术建议时,如果你想针对其中 3~5 处细节分别提出修改意见,往往需要反复在消息流与底部输入框之间来回滚动、手动复制粘贴代码片段、手打 Markdown 引用符号,再逐条写下修改说明。
为了彻底消除这种「复制—滚动—粘贴—排版」的上下文切换损耗,我开源发布了 dsh-selection-quote v0.2.0(划词引用与行内评论胶囊插件),现已被 DeepSeek Harness 官方精选插件列表(awesome-dsh-plugin)正式收录。它将 划词右键菜单(拷贝 (Copy) / 添加到对话 ⌘L)、CSS.highlights 原文持续蓝色高亮、蓝色水滴序号角标(1、2…)+ 圆角胶囊评论框,以及 输入框与已发送消息的「💬 N 条注释」胶囊及悬浮详情卡片 无缝融合进原生会话界面中:
1. 官方收录与开源仓库地址(MIT 协议,开箱即用):
- 官方插件市场收录(
awesome-dsh-plugin):https://github.com/awesome-dsh-plugin/awesome-dsh-plugin(可在dsh-market插件市场搜索dsh-selection-quote一键安装,或执行dsh plugin --profile desktop add dsh-selection-quote) - GitHub 开源仓库:
https://github.com/xt765/dsh-selection-quote - Gitee 国内镜像:
https://gitee.com/xt765/dsh-selection-quote - npm 官方包地址:
https://www.npmjs.com/package/dsh-selection-quote - 同系列深度实战推荐:建议结合阅读 《AI Agent 连续调工具闷头当哑巴?开源阶段性结论守卫插件实战(已获官方收录)》 与 《【开源发布】MCP Document Reader:让你的 AI 助手真正读懂需求文档!》。
2. 读完本文你将彻底掌握以下 4 项核心工程能力:
- 会话级 UI 插槽(Session-Scoped Slots)精准挂载:深入理解
@deepseek-ai/dsh-client-ui-conversation提供的"conversation.input.overlay"与"conversation.composer.dock"两大原生插槽机制; - DOM 选区边界隔离与视口自适应翻转算法:如何精确识别当前会话消息流选区,自动排除底部 Composer 输入框自身选区,并在视口顶部边缘实现浮动胶囊条的自动翻转;
SessionInputShell乐观并发草稿写入(CAS Insertion):如何调用ctx.conversation.input.shell(sessionId)的captureInsertion()与insertText()实现光标处无损注入与自动聚焦;- 多片段 PR Review 式批注队列与结构化提示词组装:如何用外部快照存储(
createSnapshotStore)管理多条行内批注,并一键汇总为大模型最易精准理解的结构化指令。
文章目录
一、为什么 AI 对话界面急需「PR Review 式划词批注」?(原理拆解篇)
1.1 从一个生活中的比喻说起:对讲机逐句喊话 vs 图纸红线批注单
我们可以把开发者与 Coding Agent 的多轮协作,比作建筑师与施工队长核对施工蓝图的过程:
- 传统聊天框模式(对讲机逐句喊话):
施工队长交给你一张包含 20 个房间管线设计的整版图纸(模型输出的长篇回复或长代码块)。你发现其中 3 个房间的插座位置不对、2 个阀门型号有误。在没有划词批注工具时,你只能把图纸翻上去抄下第 42 行代码,再滚到底部输入框粘贴,手敲一句“这里端口不对”;接着再滚回上面找第 108 行代码,再滚下来粘贴……不仅手忙脚乱,还极易在复制过程中漏掉上下文或不小心按回车提前发送了半截话。 - Cursor + GitHub PR Review 模式(图纸红线贴条 + 一键汇总审阅单):
你在阅读模型回复的过程中,鼠标划过第一处问题代码,上方立刻浮现一枚轻巧的胶囊条[ Add to Chat ⌘L | 写批注 ⇧⌘L ]。如果是简单追问,直接按⌘L就能把选中代码作为引用块送入底部输入框;如果是多处代码审查,按下⇧⌘L原地展开小巧的批注卡片,写下一句修改意见并按Enter,这张批注就自动收进底部输入框正上方的**「引用与批注汇总栏」**。等你从头到尾审完 5 处细节,点击一次 「一键汇总插入输入框」,5 条「原文引用 + 对应修改意见」瞬间整齐排列在输入框草稿中,一次发送即可让 Agent 精准完成全部修改。
下面通过一张双子图架构对比,直观展示两种交互链路在操作步数与上下文连贯性上的本质差异:
我们先来看看 dsh-selection-quote v0.2.0 的实机运行效果。首先,在消息流中普通划选文字时不会强行弹窗或抢夺焦点(完全保留原生 ⌘C 复制习惯);当你在选中文字上点击鼠标右键时,弹出毛玻璃右键菜单,提供 拷贝 (Copy) (⌘C) 与 添加到对话 (⌘L) 两个选项:

点击 「添加到对话」(或按下 ⌘L)后,借助浏览器原生 CSS.highlights 渲染管线,被选中的原文持续保持蓝色高亮状态,首行右上角自动锚定一枚蓝色水滴序号角标 1,正上方弹出圆角胶囊评论框(输入评论后按 Enter 或点击右侧圆形 ✓ 即可确认):

确认添加后,底部输入框内部左上方会内嵌显示 💬 1 条注释 × 胶囊(支持不额外打字直接按 Enter 或点击发送按钮发出!);鼠标悬停或点击该胶囊时,正上方弹出白色圆角详情卡片,按序号清晰展示 所选文本: 与 用户评论:,并提供铅笔编辑与垃圾桶删除按钮:

更进一步,当你按下回车将消息发送出去后,借助 uiConversation.userText 规则与 "conversation.user.text.node" 插槽,已发送的用户消息气泡同样会自动渲染为干净的 💬 N 条注释 胶囊(悬停或点击即可随时回看每条注释的所选文本与评论):

1.2 主流 AI 划词与局部引用方案全景对比:为什么原生插槽架构更胜一筹?
在 Web 与桌面端 AI 客户端中,实现「划词引用与行内批注」通常有三种技术路线。下表从 DOM 隔离精度、多会话状态隔离、输入框草稿同步方式以及多点批注汇总能力四个维度进行客观对比:
| 方案维度 | 方案 A:全局 document 脚本粗暴注入 | 方案 B:模拟剪贴板 + execCommand 粘贴 | 方案 C:DSH 原生会话插槽 + SessionInputShell(本文方案) |
|---|---|---|---|
| 选区监听作用范围 | 监听全局 document,容易在用户在底部输入框改字或改设置表单时误弹窗 | 监听全局 mouseup,需依赖脆弱的 CSS 类名黑名单过滤输入框 | 基于 "conversation.input.overlay" 探针定位 [data-conversation-content],精准排除 [data-composer-card] |
| 多会话状态隔离 | 全局单例变量,切换会话标签页后批注列表串台污染 | 无状态队列,仅支持单次即时粘贴 | 按 sessionId 独立维护 createSnapshotStore 实例,多会话切换零串台 |
| 草稿写入可靠性 | 直接篡改 textarea.value 或 innerHTML,导致 React 内部受控状态失步 | 覆盖用户系统剪贴板原有内容,体验侵入性强 | 调用官方 ctx.conversation.input.shell(sessionId) 的 captureInsertion + insertText / setDraft |
| 多片段批注汇总 | 不支持,每次划词只能立即插入单条文本 | 不支持,无法暂存多处修改意见统一编排 | 原生挂载 "conversation.composer.dock" 汇总栏,支持内联编辑与一键批量合成结构化提示词 |
| 主题与视口自适应 | 固定亮色或暗色样式,在窗口顶部划词时工具条被裁剪出屏幕 | 仅快捷键触发,无可视化胶囊条与行内批注输入框 | 自适应 CSS 变量与深浅色毛玻璃,内置视口顶部阈值检测与上下自动翻转(above / below) |
工程选型建议:
- 如果你只是在普通静态网页里做简单的划词高亮,方案 A 即可满足;
- 但在像 DeepSeek Harness 这样采用 ProseMirror / 富文本 ContentEditable 输入框 + 多会话并发状态树 的现代 Agent 客户端中,直接修改 DOM 文本会导致编辑器内部状态机丢失修改记录(按下发送时内容为空)。因此,必须通过 DSH 官方暴露的
"conversation.input.overlay"、"conversation.composer.dock"插槽与SessionInputShell接口实现原生级集成。
二、基础篇:3 分钟搞定 DSH 会话插槽架构与结构化批注协议
2.1 DeepSeek Harness 客户端插件与双插槽协同原理
在 DeepSeek Harness 的前端架构(@deepseek-ai/dsh-client-ui-conversation)中,每一个聊天会话面板(SessionPane)都预留了强类型的会话级插槽(Session-Scoped Slots)。dsh-selection-quote 巧妙组合了其中两个核心插槽:
插槽名称 (slot.name) | 插槽类型 (kind) | 作用域 (scope) | 宿主 DOM 挂载位置 | 在 dsh-selection-quote 中的职责 |
|---|---|---|---|---|
"conversation.input.overlay" | "list" | "session" | 位于底部 Composer 卡片的 .overlayAnchor 锚点内 | 渲染隐藏的 DOM 探针节点以反查当前会话的 [data-conversation-content] 容器,并通过 ReactDOM.createPortal 向 document.body 挂载浮动胶囊条与批注卡片 |
"conversation.composer.dock" | "list" | "session" | 位于底部输入框区域([data-composer-seat])内 | 渲染底部输入框正上方的「引用与批注汇总栏」;当队列为空时返回 null(零高度),非空时通过 Flex order: -1 置顶在输入框正上方 |
"plugins.bundle.config" | "single" | "global" | 位于「设置 → 插件管理」详情页 | 结合宿主端导出的 Config(Schemastery Schema),暴露 enabled、addToChatMode 与 commentLabel 配置项 |
2.2 结构化引用与多段批注的协议建模与算法验证
为什么将多条「代码引用 + 行内批注」汇总发送给大模型时,排版格式会直接影响 Agent 修改代码的准确率?
当模型收到一段没有任何标记的混杂文本时,它很难区分哪句话是它上一轮输出的原始代码、哪句话是你新提出的修改要求。为此,我们在插件中设计了严格的多行引用块规整与键值标注协议:
- 原始选区中的每一行非空文本自动加上前缀引用符,空白行保留结构占位,确保多行代码缩进丝毫不乱;
- 紧跟在引用块下方的一行使用加粗标签标注用户的具体批注;
- 多条批注之间以双换行符分隔,末尾预留双换行以便用户在输入框底部继续补充总结性指令。
为了便于在 Python 后端或评测流水线中复用同一套格式化与位置计算逻辑,下面提供完整可运行的 Python 协议实现与单元测试代码(通过 ast.parse 严格校验):
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
import time
@dataclass(frozen=True)
class SelectionAnchorBox:
"""表示浏览器选区 Range.getBoundingClientRect() 返回的几何矩形边界。"""
top: float
left: float
bottom: float
right: float
width: float
height: float
@dataclass
class StagedReviewNote:
"""表示暂存在输入框上方批注汇总栏中的单条引用与批注条目。"""
note_id: str
raw_excerpt: str
review_remark: str = ""
created_at_ms: int = field(default_factory=lambda: int(time.time() * 1000))
class QuoteMarkdownBuilder:
"""负责将划词选区与行内批注转换为结构化 Markdown 提示词的构建器。"""
def __init__(self, remark_tag: str = "批注", max_preview_chars: int = 120) -> None:
self.remark_tag: str = remark_tag.strip() or "批注"
self.max_preview_chars: int = max(20, max_preview_chars)
@staticmethod
def normalize_line_breaks(raw_text: str) -> str:
"""统一 CRLF/CR 换行符并去除首尾多余空白。"""
if not isinstance(raw_text, str):
return ""
return raw_text.replace("\r\n", "\n").replace("\r", "\n").strip()
def build_quote_block(self, raw_text: str) -> str:
"""将单行或多行文本转换为标准 Markdown 引用块。"""
cleaned = self.normalize_line_breaks(raw_text)
if not cleaned:
return ""
quoted_lines: List[str] = []
for line in cleaned.split("\n"):
quoted_lines.append(f"> {line}" if line.strip() else ">")
return "\n".join(quoted_lines)
def build_Annotated_snippet(self, excerpt: str, remark: str = "") -> str:
"""将单条选中文本与批注说明组合成成对的审阅段落。"""
block = self.build_quote_block(excerpt)
if not block:
return ""
cleaned_remark = self.normalize_line_breaks(remark)
if not cleaned_remark:
return block
return f"{block}\n**{self.remark_tag}**:{cleaned_remark}"
def compile_batch_payload(
self, notes: List[StagedReviewNote], append_trailing_gap: bool = True
) -> str:
"""将批注汇总栏中的全部条目一次性合成为写入输入框草稿的完整负载。"""
sections: List[str] = []
for item in notes:
rendered = self.build_Annotated_snippet(item.raw_excerpt, item.review_remark)
if rendered:
sections.append(rendered)
if not sections:
return ""
joined = "\n\n".join(sections)
return f"{joined}\n\n" if append_trailing_gap else joined
def summarize_excerpt(self, raw_text: str) -> str:
"""生成用于浮动批注卡片顶部与汇总栏展示的紧凑单行摘要。"""
collapsed = " ".join(self.normalize_line_breaks(raw_text).split())
if len(collapsed) <= self.max_preview_chars:
return collapsed
return f"{collapsed[: self.max_preview_chars - 1]}…"
if __name__ == "__main__":
builder = QuoteMarkdownBuilder(remark_tag="批注", max_preview_chars=48)
sample_notes = [
StagedReviewNote(
note_id="sq-1",
raw_excerpt="const pool = createConnectionPool({ port: 8080, maxRetries: 0 });",
review_remark="端口应支持从配置注入,且 maxRetries 建议改为 3 次并配合指数退避",
),
StagedReviewNote(
note_id="sq-2",
raw_excerpt="catch (err) {\n return null;\n}",
review_remark="不要静默吞掉连接异常,应抛出带上下文的 GatewayConnectionError",
),
]
batch_output = builder.compile_batch_payload(sample_notes)
print(batch_output)
三、进阶篇:划词浮动胶囊条、视口翻转算法与双插槽核心实战
3.1 多阶段运行时交互闭环拆解
当用户在 DeepSeek Harness 会话窗口中按下鼠标左键划选文字时,插件内部会经历 「DOM 容器溯源校验 → 视口防溢出坐标解算 → 胶囊条/批注卡状态切换 → 汇总队列与 SessionInputShell 草稿注入」 四个紧密衔接的阶段。
下图展示了 dsh-selection-quote 在浏览器端与 DSH 会话控制器之间的完整数据流转架构:
3.2 为什么必须设计「DOM 探针溯源」与「视口顶部翻转算法」?
在实际工程落地中,有两个极易踩坑的边界场景:
-
底部输入框与自身弹层的选区误触发问题:
当用户在底部聊天输入框([data-composer-card])里用鼠标选中一段自己刚写的草稿准备删除时,或者在展开的「写批注卡片」内部的<textarea>里选中文字修改时,浏览器同样会触发全局mouseup事件且window.getSelection().toString()非空。如果不做严格的祖先节点过滤,浮动胶囊条就会荒谬地弹在输入框或者批注卡片自己的头上!
因此,我们在isValidMessageSelection中设计了双重校验:- 黑名单拦截:一旦选区的
startContainer、endContainer或commonAncestorContainer命中[data-composer-seat]、[data-composer-card]、[data-dsh-quote-toolbar]、[data-dsh-quote-dock]、[contenteditable="true"]、textarea或input,立即判定为无效选区; - 白名单归属:利用挂载在
"conversation.input.overlay"中的隐藏探针<span data-dsh-selection-quote-probe={sessionId} />,通过probeRef.current.closest("[data-conversation-content]")锁定当前会话的专属 DOM 树,要求选区起止节点必须 100% 包含在该会话容器内部。
- 黑名单拦截:一旦选区的
-
窗口顶部与左右边缘的防裁剪翻转算法(
computeToolbarPosition):
默认情况下,浮动胶囊条(高度约44px)与展开后的批注输入卡片(高度约200px)采用transform: translate(-50%, -100%)渲染在选区第一行的正上方。但当用户滚动页面,使选中的代码正好位于窗口最顶部(例如rect.top = 30px)时,向上展开批注卡片会导致卡片上半部分直接超出视口顶部而无法点击!
为此,我们根据当前所处模式动态计算所需顶部净空高度(胶囊态requiredTopSpace = 54px,批注卡片态requiredTopSpace = 210px):当rect.top < requiredTopSpace时,自动将placement切换为"below",使卡片丝滑翻转到选区正下方(rect.bottom + 10px),同时对水平中心坐标left施加[margin + halfWidth, viewportWidth - margin - halfWidth]区间钳制,彻底杜绝任何方向的溢出裁剪。
下面给出视口坐标解算器与会话级状态管理器的完整 Python 算法仿真实现(可直接运行验证各种边界坐标下的翻转行为):
from dataclasses import dataclass, replace
from typing import Dict, List, Literal, Optional
PlacementMode = Literal["above", "below"]
ToolbarViewMode = Literal["pill", "comment"]
@dataclass(frozen=True)
class ViewportSize:
"""浏览器可视区域宽高尺寸。"""
width: int = 1280
height: int = 800
@dataclass(frozen=True)
class FloatingAnchorLayout:
"""解算后的浮动胶囊条或批注卡片定位参数。"""
left: int
top: int
placement: PlacementMode
mode: ToolbarViewMode
@dataclass(frozen=True)
class SessionToolbarSnapshot:
"""单个会话的划词浮动层与批注队列不可变状态快照。"""
visible: bool = False
mode: ToolbarViewMode = "pill"
selected_text: str = ""
comment_draft: str = ""
layout: Optional[FloatingAnchorLayout] = None
queued_count: int = 0
class ToolbarPositionSolver:
"""负责计算选区浮动胶囊条与批注输入卡片的防溢出坐标与上下翻转方向。"""
PILL_HALF_WIDTH: int = 140
COMMENT_HALF_WIDTH: int = 190
PILL_TOP_CLEARANCE: int = 54
COMMENT_TOP_CLEARANCE: int = 210
VIEWPORT_MARGIN: int = 16
VERTICAL_GAP: int = 10
@classmethod
def solve(
cls,
rect_top: float,
rect_left: float,
rect_bottom: float,
rect_width: float,
viewport: ViewportSize,
mode: ToolbarViewMode = "pill",
) -> FloatingAnchorLayout:
"""根据选区矩形与视口尺寸,计算最佳锚点坐标与翻转方向。"""
half_width = cls.COMMENT_HALF_WIDTH if mode == "comment" else cls.PILL_HALF_WIDTH
required_top = (
cls.COMMENT_TOP_CLEARANCE if mode == "comment" else cls.PILL_TOP_CLEARANCE
)
raw_center_x = rect_left + (rect_width / 2.0)
min_x = float(cls.VIEWPORT_MARGIN + half_width)
max_x = float(max(min_x, viewport.width - cls.VIEWPORT_MARGIN - half_width))
clamped_left = int(round(min(max(raw_center_x, min_x), max_x)))
if rect_top >= required_top:
top_pos = int(round(max(float(cls.VIEWPORT_MARGIN), rect_top - cls.VERTICAL_GAP)))
return FloatingAnchorLayout(
left=clamped_left,
top=top_pos,
placement="above",
mode=mode,
)
bottom_pos = int(
round(min(float(viewport.height - cls.VIEWPORT_MARGIN), rect_bottom + cls.VERTICAL_GAP))
)
return FloatingAnchorLayout(
left=clamped_left,
top=bottom_pos,
placement="below",
mode=mode,
)
class MultiSessionQuoteRegistry:
"""管理多个并发会话的划词状态与待插入批注队列。"""
def __init__(self, max_items_per_session: int = 20) -> None:
self.max_items_per_session: int = max(1, max_items_per_session)
self._snapshots: Dict[str, SessionToolbarSnapshot] = {}
self._queues: Dict[str, List[Dict[str, str]]] = {}
def activate_pill(
self,
session_id: str,
selected_text: str,
rect: Tuple[float, float, float, float],
viewport: ViewportSize,
) -> SessionToolbarSnapshot:
"""当用户在消息流完成划词时,激活胶囊态工具条。"""
top, left, bottom, width = rect
layout = ToolbarPositionSolver.solve(top, left, bottom, width, viewport, mode="pill")
queue = self._queues.setdefault(session_id, [])
snap = SessionToolbarSnapshot(
visible=True,
mode="pill",
selected_text=selected_text.strip(),
comment_draft="",
layout=layout,
queued_count=len(queue),
)
self._snapshots[session_id] = snap
return snap
def expand_to_comment_card(
self,
session_id: str,
rect: Tuple[float, float, float, float],
viewport: ViewportSize,
) -> SessionToolbarSnapshot:
"""当用户点击写批注或按下 Shift+Cmd+L 时,原地切换为批注卡片并重算顶部净空。"""
prev = self._snapshots.get(session_id, SessionToolbarSnapshot())
top, left, bottom, width = rect
layout = ToolbarPositionSolver.solve(top, left, bottom, width, viewport, mode="comment")
snap = replace(prev, visible=True, mode="comment", layout=layout)
self._snapshots[session_id] = snap
return snap
if __name__ == "__main__":
vp = ViewportSize(width=1280, height=800)
registry = MultiSessionQuoteRegistry()
pill_state = registry.activate_pill("sess-01", "listen 8080;", (35.0, 420.0, 55.0, 140.0), vp)
card_state = registry.expand_to_comment_card("sess-01", (35.0, 420.0, 55.0, 140.0), vp)
print("Pill placement at top=35:", pill_state.layout)
print("Comment card placement at top=35 (auto-flipped):", card_state.layout)
四、高阶技巧:SessionInputShell 乐观并发草稿写入与焦点恢复闭环
4.1 为什么不能直接修改 DOM,而要用 captureInsertion + insertText?
在 DeepSeek Harness 客户端中,底部输入框由 SessionInputShell 统一托管,支持富文本标签(如 @file 引用胶囊、技能徽标)、历史草稿持久化和多端同步。SessionInputShell 内部维护了一套基于版本号的快照投影(shell.projection.clipboardText)与光标选区捕获机制(shell.actions.captureInsertion()):
- 当底部输入框为空(
!hasExisting)时:
优先调用const span = shell.actions.captureInsertion()获取当前光标位置令牌,再调用shell.actions.insertText(textWithTrailingNewlines, span)。如果编辑器选区有效,文本会直接以原子事务写入编辑器; - 当底部输入框已有草稿文本,或编辑器处于失焦状态导致
insertText返回false时:
自动回退到shell.setDraft(payload),读取当前草稿shell.projection.clipboardText,去除末尾多余空白后以双换行\n\n拼接新插入的引用或汇总批注块,保证用户此前已经在输入框里写了一半的文字绝不会被覆盖丢失; - 自动聚焦与光标置尾:
写入完成后,立即调用shell.focus()并定位当前会话[data-composer-card] [contenteditable="true"]节点,通过Range.selectNodeContents(editor)+range.collapse(false)将输入光标精准停靠在新插入引用块下方的空白行上,用户双手无需离开键盘即可直接打字提问。
下面通过时序图展示从按下快捷键到 SessionInputShell 完成草稿注入与光标置尾的完整交互过程:
4.2 SessionInputShell 乐观并发草稿合并器完整仿真代码
下面给出 SessionInputShell 乐观并发(CAS)选区插入与非破坏性草稿追加器的完整 Python 实现与自测用例:
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class InsertionSpanToken:
"""表示编辑器在某一文档版本下捕获的光标选区 CAS 令牌。"""
doc_revision: int
cursor_start: int
cursor_end: int
class ComposerShellSimulator:
"""模拟 DeepSeek Harness 的 SessionInputShell 草稿状态机与 CAS 插入接口。"""
def __init__(self, initial_draft: str = "") -> None:
self._draft: str = initial_draft
self._revision: int = 1
self._cursor_offset: int = len(initial_draft)
self.is_focused: bool = False
@property
def clipboard_text(self) -> str:
"""返回当前输入框的纯文本投影。"""
return self._draft
def capture_insertion(self) -> InsertionSpanToken:
"""捕获当前文档版本号与光标区间。"""
return InsertionSpanToken(
doc_revision=self._revision,
cursor_start=self._cursor_offset,
cursor_end=self._cursor_offset,
)
def insert_text(self, text: str, token: Optional[InsertionSpanToken] = None) -> bool:
"""校验 CAS 版本令牌并在光标处原子插入文本。"""
if token is None or token.doc_revision != self._revision:
return False
before = self._draft[: token.cursor_start]
after = self._draft[token.cursor_end :]
self._draft = f"{before}{text}{after}"
self._revision += 1
self._cursor_offset = len(before) + len(text)
return True
def set_draft(self, next_draft: str) -> None:
"""全量替换草稿文本并推进文档版本号。"""
self._draft = next_draft
self._revision += 1
self._cursor_offset = len(next_draft)
def focus(self) -> None:
"""聚焦底部输入框并将光标移至草稿末尾。"""
self.is_focused = True
self._cursor_offset = len(self._draft)
def inject_formatted_markdown_into_shell(
shell: ComposerShellSimulator, raw_markdown: str
) -> bool:
"""向 SessionInputShell 安全写入格式化后的引用块或批量批注,保留用户已有草稿。"""
normalized = raw_markdown.replace("\r\n", "\n").replace("\r", "\n").strip()
if not normalized:
return False
payload_with_gap = f"{normalized}\n\n"
existing_text = shell.clipboard_text.rstrip()
has_existing = len(existing_text) > 0
inserted = False
if not has_existing:
span_token = shell.capture_insertion()
inserted = shell.insert_text(payload_with_gap, span_token)
if not inserted:
merged_draft = (
f"{existing_text}\n\n{payload_with_gap}" if has_existing else payload_with_gap
)
shell.set_draft(merged_draft)
inserted = True
if inserted:
shell.focus()
return inserted
if __name__ == "__main__":
sim_shell = ComposerShellSimulator()
ok_first = inject_formatted_markdown_into_shell(sim_shell, "> listen 8080;")
ok_second = inject_formatted_markdown_into_shell(
sim_shell, "> maxRetries: 0\n**批注**:请改为指数退避重试 3 次"
)
assert ok_first and ok_second and sim_shell.is_focused
print("Merged Composer Draft:\n" + repr(sim_shell.clipboard_text))
五、实战避坑指南:DSH 客户端插槽开发高频报错与一键解决速查表
在开发与调试 DeepSeek Harness 客户端插件时,以下 5 个高频问题是开发者最容易遇到的拦路虎。我们将其整理为速查表格,方便按图索骥:
| 序号 | 遇到的报错或异常现象 | 背后的真正原因 | 一句话解决办法 |
|---|---|---|---|
| 01 | 点击浮动胶囊条按钮时,按钮点击事件没触发,胶囊条却直接消失了 | 浏览器在点击按钮时先触发了全局 mousedown 监听器,导致选区失焦并先一步调用了 hideToolbar() | 在浮动工具条根容器与按钮的 onMouseDown 中调用 e.preventDefault() 与 e.stopPropagation(),并在全局 mousedown 中放行 toolbarRef.current.contains(e.target) |
| 02 | 在批注输入框使用中文拼音输入法按 Enter 选词时,批注卡片直接被提交关闭了 | 未检查 IME 组合输入状态(isComposing),导致输入法上屏回车被误判为提交快捷键 | 在 onKeyDown 开头增加 if (e.nativeEvent?.isComposing) return; 门禁判断 |
| 03 | "conversation.composer.dock" 挂载后,批注汇总栏与底部的 ContextMeter 挤在同一行导致样式错乱 | DSH 默认的 .yhfFVG_dock 容器采用单行水平 Flex 布局且位于输入框卡片下方 | 为父容器注入 [data-composer-seat] > div:has(> [data-dsh-quote-dock]) { order: -1; flex-wrap: wrap !important; width: 100% !important; },让汇总栏独占一行置顶于输入框正上方 |
| 04 | 在展开的批注输入框 <textarea> 内用鼠标选中文字想修改时,选区被替换成了批注文字自己 | 全局 mouseup 选区同步函数未判断当前是否已处于 mode === "comment" 状态 | 在 syncFromDomSelection 开头判断 if (currentToolbar.visible && currentToolbar.mode === "comment") return;,锁定原引用文本不被覆盖 |
| 05 | 执行 dsh plugin add 安装插件时提示缺少 dsh.bundle 清单 | package.json 仅声明了 dsh.client 而遗漏了 dsh.bundle.patch 字段 | 在 package.json 中同时声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "entry": "./lib/client.js", "platform": "web" } } |
结语
通过 dsh-selection-quote 插件的设计与实现,我们不仅为 DeepSeek Harness 补齐了 Cursor 式的 ⌘L 一键划词引用 与 GitHub PR Review 式的 ⇧⌘L 多点行内批注汇总 能力,更完整走通了 DSH 客户端插件从会话级插槽("conversation.input.overlay" / "conversation.composer.dock")挂载、DOM 选区边界隔离、视口防溢出翻转解算,到 SessionInputShell 乐观并发草稿写入的全套工程范式。
目前 dsh-selection-quote(v0.2.0)已在 GitHub、Gitee 与 npm 官方仓库全面开源上线,欢迎大家直接安装体验或 Star 支持:
- GitHub 仓库:
https://github.com/xt765/dsh-selection-quote - Gitee 仓库:
https://gitee.com/xt765/dsh-selection-quote - npm 包主页:
https://www.npmjs.com/package/dsh-selection-quote
在下一篇 DeepSeek Harness 插件实战文章中,我们将继续探索会话上下文窗口可视化监控与自动摘要压缩插件的开发,敬请期待!








被折叠的 条评论
为什么被折叠?



