dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 极简插件:把对话里选中的一段文字,作为「引用上下文」随你的下一条消息送进对话。
选中任意文字块(assistant / 你自己 / tool 输出)→ 松手弹出菜单 →「添加到对话」→ 那段文字先以卡片排在输入框里(可移除)→ 你按下发送,卡片立即消失,引文作为一条带标签的**「引用上下文」折叠行**出现在对话里,用完即走。
source.kind 投递,再由插件注册的 Conversation definition 渲染成一条可区分的引用行。这样你一眼能分辨"这是我引用的"与"这是我自己说的",见 docs/adr/0004-quote-as-own-chat-node.md。source.kind 分类每一条 user/message——kind: 'user' 会变成你无法改造的用户气泡;其它任何 kind 会变成被 isVisibleChatNode 滤掉的 context 节点。插件只能靠自己的 node kind 逃出这两条路。Ctrl+Alt+P 打开工作区文件)→ 卡片副标题显示文件名 → 注入给模型时带上绝对路径,形如 Quoted context from file E:\…\docs\README.md (1 line) follows.dsh-quote,菜单文案「添加到对话」引用只在下一个真正带用户文字的回合投递。agent 正卡在长工具循环里时,那个回合还没开始,引用会如实等待——但 composer 里的卡片此时已经随发送消失了,不会挂在那里。诊断这类问题用
DSH_QUOTE_TRACE=<文件>。
已知边界(宿主行为,插件无法控制):引文行会随回合结束被宿主收进「回合过程」折叠组。回合进行中它露在外面;回合结束后需要展开宿主的折叠组才能看到。原因见 ADR-0004。
CONTEXT.mddocs/adr/0001-quote-as-injected-context.md、docs/adr/0002-quote-visibility.md、docs/adr/0003-quote-as-user-message.md、docs/adr/0004-quote-as-own-chat-node.md、docs/adr/0005-sidebar-file-quote.mdconversation.input.overlay(src/client/index.tsx 注册 QuoteDock)。该槽渲染在 composer 卡片内部顶端的零高锚点上,所以引用卡片能像附件一样压在输入框里;conversation.input.dock 是卡片上方的全宽槽位,conversation.input.attachments 是被官方图片附件占用的 single 槽,两者都不合适。mouseup 捕获监听(selectionchange / scroll 只负责收起);选区非空且锚点命中对话行([data-chat-flow-key])或侧边栏文件预览([data-textpreview-url])时 → window.getSelection().toString() + 该锚点的来源信息(src/client/quote-dock.tsx)。归属由拖拽起点决定,跨边界选区归给它开始的那一侧。src/client/file-source.ts):先 closest('[data-textpreview-url]') 定位容器,再在该容器子树内取绝对路径形状的 title。不向容器外攀爬——那会爬到标签壳并可能匹配到无关文件。地址形如 dsh-resource://file/session/<会话>/<相对路径>。createPortal 挂到 <body>。留在 composer 内部时,z-index 无法跨越祖先的层叠上下文,右侧边栏一打开就拦截菜单上的所有点击。QuoteDock 经自有 HTTP API(src/client/api.ts)把引文交给 host;每条待生效引用渲染为一张附件卡片(data-dsh-quote-rail),副标题是来源行类型,hover 出移除按钮。卡片标题是有界预览(quotePreview),全文在 title 属性里。QuoteDock 在提交按键(裸 Enter / 发送按钮)上调用 POST /claim,然后显示宿主报告的状态。客户端不自己决定什么已发送 —— 早期版本用"草稿变空"作为发送信号,无法区分「发送」与「退格删除」,导致删除草稿时卡片永久消失而引文仍被静默注入。quoteLimitError 在客户端(即时反馈)与宿主(权威,413)各判一次;拒绝原因显示在 composer 卡片内的 data-dsh-quote-notice 里。src/quote-text.ts):零依赖叶子,host 与 client 共用。quoteSummary(80,折叠行)/ quotePreview(240,卡片)/ quoteFrame(模型输入,头部 + 逐字正文)/ quoteLimitError(上限)。src/index.ts)agent/pre-step 一次性折叠(src/quote-fold.ts),仅跟随真实用户回合,投递后清空(src/quote-store.ts)。引文由 src/quote-context.ts 构造成一条带私有 source.kind(quote-context)的 user-role 消息,附 form: 'notice' 与有界 summary,插在该步最后一条真实用户消息之前。src/client/quote-row.ts 注册一个 Conversation definition,认领 source.kind === 'quote-context' 的 append-surface user/message 事件,产出 quote 节点;src/client/quote-row-view.tsx 作为 conversation.chat.node 的 key: 'quote' 渲染器,画成「引用上下文」+ 摘要的可展开行。
isVisibleChatNode 是 kind 黑名单(只排 system-prompt / context / permission command),自定义 kind 默认可见;ChatNodeDataMap 是公开的插件合并面;ctx.uiConversation.events.register 是官方自用的注册口。messageDefinition 也认领同一事件,但它的副本投影成 context 节点被滤掉,只有我们的 quote 节点可见(实测 user bubble 0 / context row 0 / quote row 1)。slots 同款,走结构化 context 面注册,不引入任何 @deepseek-ai 值导入。dsh plugin --profile <name> add <dsh-quote 本地路径或包名>
装完硬刷新浏览器。client 改动热加载;host 改动需重启 dsh web。
pnpm typecheck
pnpm build
pnpm test
pnpm verify # typecheck + test + 安装面校验(pnpm pack 后装进一次性 pnpm 工程)
真实浏览器验收(对着一个真跑着的 dsh --profile web 服务):
dsh --profile web --port 3099 --no-open # 另开一个测试实例,别动你在用的那个
pnpm browser-test "http://127.0.0.1:3099/?token=<上一步打印的 token>"
断言覆盖三条需求:bundle 进入 __DSH_BOOT__、dock 挂载并轮询自有 API、conversation.input.overlay 落在 composer 卡片内、host HTTP 桥往返、真实鼠标划选弹出菜单、卡片入 rail、发送后卡片立即消失且 60 秒内不复活、引文在转录区渲染为自己的 quote 行(非用户气泡、非被滤的 context)、一次性队列排空。脚本会自行处理宿主首启的「内测声明」弹窗(与插件无关)。
注:「引文行是否在屏幕上」取决于宿主的回合过程折叠组是否展开,而后者由回合状态(
turnClosed)决定。测试把行已挂载、几何正常、归属宿主折叠作为硬断言,把「是否可见」作为上报项而非门禁 —— 否则一个无模型凭据的 profile(回合瞬时失败关闭)会让套件因插件既未造成、也无法修复的原因失败。
运行时兼容:本插件面向 @deepseek-ai/dsh 0.1.7-rc.2。peer/dev 范围钉在 ^0.1.7-rc.2 / 0.1.7-rc.2。
架构约束:
@deepseek-ai 模块(client 纯度门);与 DSH 交互只走公开 slot / uiConversation 注册口 / 自有 HTTP 桥。host/client 共享的字符串常量必须放零依赖叶子(如 src/source-kind.ts),不能从 host 模块导出给 client 用。peerDependencies,绝不能进 dependencies。profile 是独立的 hoisted pnpm 工程,而宿主自己的包在全局 dsh 那棵树里,插件往上找 node_modules 走不到——声明成 dependencies 就会被就地再装一份,插件的裸 import 绑到第二份实例上。表现是 profile 照常启动、每条对话在第一次工具调用处静默死掉,日志里没有"模块重复"提示。link: 安装只放软链所以看不出问题,file:<tgz>(桌面分发用的形态)才会暴露;唯一例外是插件要 spawn 成独立进程用的包。pnpm verify 与 tests/dependency-face.spec.ts 双向拦住这个回归。docs/adr/0001-quote-as-injected-context.md、docs/adr/0003-quote-as-user-message.md、docs/adr/0004-quote-as-own-chat-node.md。