dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Annotate any web element — local or online — with DOM facts and your comments, straight into your DeepSeek Harness chat. Built-in sidebar browser, Codex-style review, no X-Frame-Options pain. / 直接在DeepSeek Harness中用“标记DOM”和你的“评论”来标注任何网页元素——无论是本地的还是在线的。内置侧边栏浏览器,无需担心X-Frame-Options
DSH 侧边栏标注插件 —— 在实时预览里点选元素、写批注,直接发进对话。 不用截图、不用描述"右边那个灰色的卡片"。
不需要装浏览器扩展。 这是一个 DSH 插件,装进 DSH 就能用,标注直接进对话。 (和浏览器扩展的区别)
https://github.com/user-attachments/assets/347d20cc-51c7-471d-8911-8a82407ea761
视频托管在 GitHub 附件上。看不了的话,仓库里的 assets/demo.mp4 是同一段。
dsh-annotate 是 DeepSeek Harness(DSH)的 Web 侧边栏插件。 你在实时页面预览里直接点选元素,写上"这里要改什么", 标注就作为结构化上下文随下一条消息发出去。
它解决的问题:你想让 AI 改某个界面元素,但说不清是哪一个。 截图加一句"右边那个灰色的卡片",AI 只能猜,猜错了再来一轮。 装上之后你点一下元素,AI 收到的就是这个元素的 CSS 选择器、真实文本和你的批注。
| 宿主 | DeepSeek Harness(DSH),Profile 必须是 web |
| 类型 | DSH 插件(不是独立应用,不是浏览器扩展) |
| 安装 | 加进 Web profile 的 dsh.profile.bundles,然后重启 DSH |
| Node | >= 20 |
| 平台 | Windows / macOS / Linux |
| 许可 | MIT |
| 依赖 | 无额外运行时依赖(@deepseek-ai/cordis 由 DSH 提供) |
| 网络 | 默认只预览回环地址与工作区文件;allowRemote 打开后可预览任意 http(s) 站点。代理监听 127.0.0.1 |
对口的使用场景:在网页上圈选/标注元素、把 UI 反馈给 AI、让 AI 精确定位 DOM 元素、
可视化地提修改意见、给前端改稿。
当前版本预览本地服务与工作区 HTML;访问在线网站由 allowRemote 配置项控制,
计划在后续版本开放。
不对口的:脱离 DSH 独立运行的标注工具(它必须装进 DSH 的 Web profile)、 跨域 iframe 内部的元素(浏览器的同源策略限制,不是本插件可以绕过的)、 自动改代码(它只负责把"改哪儿"说清楚,改是 AI 的事)。
给 AI 改界面,卡住的从来不是"改",是说清楚改哪儿。
截图圈一下,再打字"就是那个卡片,右边那个灰色的"—— 它还是得猜,猜错了再来一轮。
这个插件把这一步压缩成两次点击:
AI 拿到的是 .lb-hero 和它的真实文本,不是一句模糊描述。
它和浏览器截图标注工具的区别:发出去的是结构化数据,不是图片。 选择器、文本、批注分开成字段,AI 能直接照着定位,不需要"看图猜"。
nth-child、动态 class 是标注工具最容易翻车的地方。
生成的结果会先验证唯一且指向你点的那一个,不唯一时用元素文本消歧;
实在定位不了会明确告诉你,而不是默默丢掉、留下一个对不上的编号?query 和 #/hash),
而不是被代理分配的端口号,换页时跟着更新下面三张都是实际运行时的截图。
点工具栏的 发现 按钮,插件扫一遍本机实际在监听的端口,把正在跑的页面列出来,
连标题和技术栈一起。3000 上的 Vite、8000 上的 Django、5173 上的 Vue,直接点开,
不用回忆也不用去翻终端。
每个技术栈带自己的 logo —— 扫一眼就知道哪个是哪个,不用读文字。
工具栏的 颜色 里可以换标注色。这不只是好看:标记要同时满足"在任意页面内容上 是 2px 描边"、"是 6px 的圆点"、"是背后有白字的实心针"三种形态,还要在浅色和深色 主题下都够清楚。预设的 6 种全部通过 WCAG AA,选完立刻生效,不需要刷新。
左边是蓝色,右边换成橙色 —— 换的是标记的颜色,页面本身没有动。
标注不会混进你打的字里。它作为一条独立的上下文条目附在消息上,对话里显示成 可折叠的一行「N element attached」,点开才是内容。你自己打的字和它完全分开, 想删掉标注不影响消息正文。
这是它和"点一下写句评论"的工具最大的区别 —— 不写也是有效的意见:
| 怎么做 | 结果 | |
|---|---|---|
| 标注 | 点元素,输入框留空,保存 | 记录"就是这个元素",不带意见 |
| 批注 | 点元素,写一句说明,保存 | 记录元素 + 你的修改要求 |
留空不是取消。 点一下某个元素本身就是一条完整的意见,空评论会被原样保留为一条「标注」。 输入框上方的徽章随打字实时切换「标注 / 批注」,保存前就能看到它会变成哪一种。
这是一个 DSH 插件,不是一个浏览器扩展。
| dsh-annotate | 浏览器扩展类工具 | |
|---|---|---|
| 装在哪 | DSH 的 Web profile | Chrome / Edge 扩展商店 |
| 要装扩展吗 | 不用 | 要 |
| 标注送到哪 | 直接进 DSH 对话,AI 立刻能看到 | 复制粘贴,或另配一个服务 |
| 标注是什么格式 | 结构化数据(选择器 + 文本 + 批注) | 通常是截图 + 文字 |
| 页面在哪打开 | DSH 侧边栏的预览里 | 你浏览器当前的标签页 |
区别不在"能不能标注在线网站",而在标注之后怎么走。 这个插件把选区变成结构化字段直接送进对话,而不是给你一张截图让你自己解释。
当前版本的预览目标:本地服务(自动探测端口)和工作区里的 HTML 文件。
访问在线网站由配置项 allowRemote 控制,计划在后续版本开放
(见 已知限制)。
把下面这段整段复制发给你的 DSH:
帮我安装 dsh-annotate 插件:
1. 克隆 https://github.com/TheYoungChen/dsh-annotate 到我的插件目录
2. 把 "dsh-annotate" 加进 Web profile 的 dsh.profile.bundles
3. 告诉我怎么重启 Web 端
DSH 会读这个 README 自己完成剩下的步骤 —— 不需要你手动改配置文件。
第一步 —— 把包加进 Web profile 的 dsh.profile.bundles:
{
"dsh": {
"profile": {
"bundles": ["...", "dsh-annotate"]
}
}
}
第二步 —— 重启 Web 端。
必须在一个独立的终端里做。 agent 就跑在 DSH 里面,在会话里杀进程等于自杀:
Get-NetTCPConnection -LocalPort 3000 -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
cd <你的 deepseek-harness 目录>
pnpm dsh web --port 3000
重启完打开右侧边栏,就能看到「标注」页签。
cordis.patch.yml一般用不上。插件自带 cordis.patch.yml,通过 dsh.bundle.patch 自注册。
- insert:
- id: dsh-annotate
name: 'dsh-annotate'
config:
enabled: true
allowRemote: false
allowExternalFiles: true
只写
cordis.patch.yml不生效。 插件的客户端半边必须经 profile 的dsh.profile.bundles激活。只写在 patch 里,宿主半边会加载、客户端半边被静默跳过, 表现为侧边栏根本没有这个页签 —— 这是最常见的装不上的原因。
⌘⇧B.html 也能直接开标注不会出现在你打的字里。发送后它作为一条可折叠的「上下文注入」行出现在对话里, 默认收起,点开才看得到具体内容。
选择器优先语义化 class。 按优先级取第一个能唯一命中的:#id → [data-testid] →
唯一的 class → 位置链(最多 6 层,兜底)。位置链对人没有信息量,页面一改就失效;
.lb-hero 一眼就知道改哪儿。
标注不写进输入框。 早先的做法是把标注拼成一段文本追加到草稿后面 —— 用户看到的自己 的消息里混着一大坨 DOM 结构,既难看又容易被误删。现在标注走宿主的运行时上下文通道, 以自己的身份投递:你的消息就是你的消息,标注是旁边一条独立记录。
载荷刻意保持精简。 选择器只出现一次;坐标只在选择器可能歧义时才带上;没有任何装饰性 图标。在同一组真实标注上实测:602 字符 → 192 字符,减少 68%。
没有需求就不发。 标注是上下文而非消息,草稿为空时没有可附着的对象,此时会提示你先写 要求,而不是替你发一条空消息。
"附上了什么"和"屏幕上有什么"是两个问题,答案不再共用一个。 标注按会话存一份,
每条再记下自己是在哪个路由上标的。于是换路由时列表、计数、"已附加"全都原样保留,而标记针
只画在它当初所在的那个页面上 —— 按路由存整份列表会让切页面等于丢标注,按会话存而不管路由
又会让针飘到别的页面上。列表里有多个页面的标注时,每条会多显示一行它来自哪个路由;
交给模型的那段文本同理,只有当标注横跨多个路由时才会带上 page: 一行。
换了页面要记得回来。 侧边栏切到别的标签页时,标注面板整个被卸载,回来就是全新的空状态 —— 连"刚才看的是哪个页面"都不记得了。所以打开的地址按会话记在小容量存储里,面板重建时自动重开。 重开不能清空列表:面板的镜像逻辑会把"从 N 条变成 0 条"读成用户主动清空并上报, 宿主随即丢弃这批标注并推进 epoch,dock 又会去清空页面 —— 一次切标签页就能毁掉它本该保住的东西。 所以只有真正换到另一个页面才清空。
宿主是那个"还欠着"的一侧,所以面板重建时要问它。 面板被卸载后没有任何状态可比对,
光"沉默不上报"只能避免清空宿主,自己仍然是空的,而输入框上的胶囊还在计数 —— 这就是
"侧边栏里所有标注都消失了"的来源。因此 /pending 除了数量还回传列表本身,面板挂载时
认领它。跨页面的 restore 消息让页面把宿主手里的那份接回去,这样预览被回收重建
(新的 origin,页内存储是空的)之后,标注仍然回得来。
DSH 宿主
└─ lib/index.js 起本地代理(同 site、不同 origin)+ HTTP API + 注册运行时上下文
↓ 代理 ↑ 标注按会话存在这里
目标应用 │
lib/shim.js 改写 fetch/XHR/WebSocket,让子请求也走代理
lib/overlay.js 帧内:拾取、标记针、评论卡片、页内存储
↕ postMessage │
client.js 侧边栏:编号列表、排序、把标注报给宿主(只报原始字段,不拼文本)
标注从侧边栏到模型的完整路径:
侧边栏标注变化
└─ POST /__dsh-annotate/context { session, annotations[] } ← 只传原始字段
↓
宿主按 sessionId 存一份待发列表
↓ 下一轮对话组装 prompt 时
systemPrompt.context() 的 provider 求值
↓
以 plugin 身份产出快照消息(不是 user 消息)
↓ ↓
模型收到结构化上下文 界面显示一条可折叠的「上下文注入」行
↓
该轮 turn/start 后列表清空,不会重复出现在后续轮次
三个关键点:
localStorage / sessionStorage 和 harness 隔开,而目标应用自己的登录
cookie 能正常存取(同 site 才不会被浏览器的跨站限制拒收)。x-frame-options、content-security-policy(含 report-only)、
cross-origin-opener-policy、cross-origin-embedder-policy、cross-origin-resource-policy。
任何一个在,页面就是白屏。getBoundingClientRect() 重新定位,不存坐标。
元素被滚出容器或裁掉时,针直接隐藏,不会飘到别的组件上。allowRemote 关着时,任何非 localhost / 127.0.0.1 / ::1 的地址一律 403 ——
这个代理不会变成通往内网或云元数据地址的跳板。realpath 再和根目录比对,符号链接指向外面也会被拒;
. 开头的路径段直接 403。需要预览工作区外的文件时,显式打开 allowExternalFiles。Sec-Fetch-Site: cross-site 的请求会被拒。set-cookie 会加目标前缀并改写路径,两个预览之间不会串味。
注意 cookie 的作用域是 site(hostname)而不是 origin(含端口):所以预览之间、
以及预览与宿主之间共用同一份 cookie 存储。前缀保证了不同目标不会互相覆盖。这些是这类方案共同的硬限制,不是 bug:
SameSite=None; Secure 的 cookie:预览是 http://,这类 cookie 在非 HTTPS
下会被浏览器丢弃,所以要求它的站点登录不上frame-ancestors 被剥了,但页面内联脚本仍可能被 CSP 挡)127.0.0.1 / ::1 与工作区页面。
把配置里的 allowRemote 打开即可预览任意 http(s) 站点 —— 这个开关已经可用,
但默认关着,因为要安全地开放还需要 SSRF 加固(拦截云元数据地址 169.254.169.254
和内网私有网段),而那部分计划在后续版本补上。localhost:5173/a 和 localhost:5173/b 是两个
标签,各自有各自的标记 —— 预览身份用的是完整地址而不是 origin。这一点曾经是错的:
按 origin 复用会让第二个地址拿回第一个的预览,表现就是永远只有一个标签页,
而且根本开不出第二个。file:// 那一支上。localStorage 活不过预览本身,登录态可以。 这两件事的存储作用域不同,
之前把它们并成一句是错的:
localStorage / sessionStorage 按 origin 划分,预览每次新建都监听随机端口,
就是新的 origin —— 所以页内存储是空的,被回收(默认 5 分钟不动)后重开也一样。
这是隔离的代价,也是刻意的:被标注的页面读不到这个工作区的任何东西。set-cookie 仍会加目标前缀转发,不会串味。# 回归测试(52 项)
node scripts/preflight-activation.mjs # 激活链(真实 composeEntries)
node scripts/preflight-shapes.mjs # 注册形状对真实 slot key 校验
node scripts/preflight-profile.mjs # profile 里恰好一行 annotate
node scripts/check-boot.mjs # overlay 挂载(head/body 两种注入位置)
node scripts/check-overlay.mjs # 标记 / 批注 / 空标注保留 / 卡片停靠 / 编号连续 / 空白处不算选中
node scripts/check-client.mjs # 客户端接线
node scripts/check-client-dom.mjs # 真实 DOM 渲染
node scripts/check-layout.mjs # 空间分配、地址栏占工具栏那一行、detect 唯一入口、浮层不被对话盖住、批量清除不会被滚出视野
node scripts/check-filepreview.mjs # 文件预览入口 URL
node scripts/check-stacks.mjs # 技术栈指纹(9 种,含"认不出就不猜")
node scripts/check-selectors.mjs # 选择器在真实页面上唯一
node scripts/check-block.mjs # 模型实际收到的文本(跑真实渲染函数)
node scripts/check-context.mjs # 标注以 plugin 身份投递,不进用户消息
node scripts/check-host-apply.mjs # 宿主 apply() 不会把 DSH 带崩
node scripts/check-host-http.mjs # HTTP 边界:畸形输入不落库、不抛错
node scripts/check-preview-reuse.mjs # 预览复用不因 null target 抛错
node scripts/check-report-map.mjs # 标注上报与列表渲染的健壮性
node scripts/check-attach-flow.mjs # 附上流程:跨源消息不被误收
node scripts/check-handover.mjs # 附上后宿主仍持有全部标注(含反向验证)
node scripts/check-capsule.mjs # 胶囊在输入框上方、计数跟随宿主
node scripts/check-capsule-wiring.mjs # 三种 sessionId 传参都能渲染
node scripts/check-repeat-guard.mjs # 每 turn 只写一行;交付即释放;epoch 只推进一次
node scripts/check-delivery-order.mjs # 交付时刻释放(不是 turn/end)
node scripts/check-accent.mjs # 配色:变量作用域、对比度、胶囊不透明且有颜色
node scripts/check-interaction.mjs # Esc 退出标记;只有一个交接动作
node scripts/check-payload.mjs # 客户端不再拼文本(交接边界守卫)
node scripts/check-selector-target.mjs # 选择器以目标元素结尾,不是它的祖先
node scripts/check-duplicate-nodes.mjs # 同结构节点上的两个标注不会解析到同一个元素
node scripts/check-anchor-shape.mjs # 复现读者页面的形状:两条路径各自锚定自己的元素
node scripts/check-reported-flow.mjs # 端到端:开文件 → 标注 → 保存 → 再开同一文件
node scripts/check-readme.mjs # README 里的图片、链接与两个数字都不是手写的
node scripts/check-sidebar-crash.mjs # 侧边栏在异常输入下不崩
node scripts/check-structure.mjs # 三个源文件的结构完整(头部没被截掉、括号配平)
node scripts/check-detection.mjs # 端口清单含 Tauri;每个技术栈都有 logo
node scripts/check-target-gate.mjs # 预览目标白名单与 README 的承诺一致(含 SSRF 缺口)
node scripts/check-route-key.mjs # 换页不丢标注:列表按会话存、pin 按路由画,编号跨页不变
node scripts/check-address-chip.mjs # 地址栏显示完整真实地址(含 #/hash),过长从左侧截断
node scripts/check-chip-dom.mjs # 批注胶囊真的挂进 DOM:计数、展开、异常 props 不崩
node scripts/check-chip.mjs # 已发送气泡下方的批注胶囊:读回宿主写的块、只认自己那条消息、落在折叠窗口之外
node scripts/check-count-popup.mjs # 计数弹窗:批量清除可及、编辑后弹窗自己关掉、清空真的上报空批次
node scripts/check-delete-stickiness.mjs # 在页面上删掉最后一条标注后,它不会又被宿主塞回来
node scripts/check-host-asset-freshness.mjs # overlay 改动必须进到"当前这一次"响应里,不能等到重启宿主
node scripts/check-preview-login.mjs # 预览 iframe 与宿主同 site,登录会话 cookie 才留得住
node scripts/check-tabs.mjs # 多标签页:加页面而不是换页面、标记按页面分、关页面留标记、地址栏入住工具栏那一行
node scripts/check-preview-reuse.mjs # 预览复用身份:同应用不同路由、同目录不同文件都是两个预览
node scripts/check-tabs-dom.mjs # 真的渲染一遍面板数标签:点 + 能开出同应用的第二个页面;关一个剩一个
node scripts/check-release.mjs # 更新日志直接当发布说明:没有内部脚本名、没有"读者报告"式转述、不是空壳
# 变异检测前先自检:每个变异都带一个字面锚点,源码一改就可能失配;
# 失配时变异根本没注入,受检代码等于好代码,"通过"是假的
node scripts/check-mutation-anchors.mjs
# 真 cordis、真 bundle:会话服务晚到时胶囊仍要注册(这正是它曾经整个不显示的成因),
# 服务永不到时插件仍要 ACTIVE —— 否则 dsh web 的启动自检会判定该条未激活
node scripts/check-real-registration.mjs
# 变异检测:注入 92 个故障,每一个都必须被抓到
node scripts/check-preflight-power.mjs
# 对一个真实运行中的实例复查(默认 3099,先起好)
node scripts/check-preview-inline.mjs http://127.0.0.1:3099
# 直接问正在跑的那个进程(默认 3080):它实际送出的是哪一版 overlay
node scripts/check-live-host.mjs http://127.0.0.1:3080
# 正在跑的进程送出的 overlay 里,这一轮的改动到底在不在(换个版本号骗不过它)
node scripts/check-live-overlay.mjs http://127.0.0.1:3080
# 刷新在代理已被回收之后仍然可用(这正是 "localhost 拒绝连接" 那条)
node scripts/check-reload.mjs http://127.0.0.1:3080
# 冒烟 / 诊断
node scripts/smoke-host.mjs
node scripts/smoke-proxy.mjs
手工注入过变异之后(node scripts/mutate.mjs . <name>),用这个还原:
node scripts/mutate.mjs . restore
check-preflight-power.mjs 会故意注入故障来验证测试本身有效 ——
因为一套永远绿的测试等于没有测试。
每个新断言都必须在注入对应故障后确实失败,才算数。这条规则不是形式主义: 本仓库出现过两次「断言字符串存在」而不是「断言值正确」的假测试,以及一次竞态导致 同一个测试三次里失败一次 —— 那一次最终被改成确定性实现,而不是放宽断言。
CHANGELOG.md —— 每个版本用户可见的变化assets/README.md —— 截图与演示视频的来源,以及重新生成的步骤docs/compatibility.md —— DSH 版本兼容声明、依据,以及尚未完成的运行验收步骤docs/design-principles.md —— 设计原则docs/reference-element-facts.ts —— 元素信息采集参考实现(ARIA role 映射等),
当前版本未启用,保留供将来扩展| DSH 版本 | 状态 |
|---|---|
0.1.7-alpha.1 | 兼容 |
0.1.7-alpha.2 | 兼容 |
0.1.7-rc.1 | 兼容 |
Node.js >=20;平台 win32 / darwin / linux;Profile web。
兼容性声明的依据是 API 表面检查,可复现:
node scripts/check-compat.mjs 0.1.7-rc.1
它验证插件用到的服务与槽位在该版本中确实存在。这不等同于运行验收 ——
一次性 Profile 的安装/启动/卸载证据尚未采集,docs/compatibility.md 记录了具体步骤。