Vai al contenuto principale
D

dsh-plugin-middle-panel

darkhe55/dsh-plugin-middle-panel

Middle canvas panel for DeepSeek Harness: shows deliverables between the workspace list and the conversation, and sends numbered annotations back to the chat.

Installazione

dsh plugin --profile web add github:darkhe55/dsh-plugin-middle-panel

README

画布面板 / Canvas panel

在 DeepSeek Harness Web GUI 里把中栏变成 [画布 | 对话] 两栏:左边工作区列表不动,中间是交付物画布,右边是原生对话。画布支持文本划选、HTML 元素选取、图片区域框选与 SVG 图层选取,标注按编号回传到输入框。

┌──────────┬──────────────────────────────────────┬──────────────────┐
│ 工作区   │  画布                                │  对话            │
│          │  ┌────────────────────────────────┐  │                  │
│ ▸ 目录   │  │ 编辑  渲染 | 原文  A− A+  🔍    │  │  用户 / 模型     │
│ ▸ 文件   │  ├────────────────────────────────┤  │  消息            │
│          │  │   交付物(文本 / 网页 / 图 / 3D)│  │                  │
│          │  ├────────────────────────────────┤  │  ┌────────────┐  │
│          │  │ 3. notes.md · 第 12-14 行 ✓ ↗ × │  │  │ 输入框     │  │
│          │  │    > 这段话需要改写             │  │  └────────────┘  │
│          │  └────────────────────────────────┘  │                  │
└──────────┴──────────────────────────────────────┴──────────────────┘

包名 dsh-plugin-middle-panel,以 bundle 形式装在当前 profile(desktop)里。

发布流程

分支推送只跑测试,不动 registry;发布只由打标签触发。

# 1) 改版本 + 写 CHANGELOG,提交并推送
git commit -am "release: 0.5.6" && git push

# 2) 打标签并推送,这一步才会发布
git tag v0.5.6 && git push origin v0.5.6

.github/workflows/publish.yml 会依次:校验标签与 package.json 一致 → 若该版本已存在则跳过 → 跑全部测试 → 校验 client.js 与 src/parts 同源 → npm publish --provenance。

两种凭据方式,二选一:

方式配置是否需要 token
可信发布(推荐)npm 包页面 → Trusted Publisher 绑定 Darkhe55/dsh-plugin-middle-panel + publish.yml不需要,走 OIDC
仓库 secretSettings → Secrets → NPM_TOKEN(Granular,不要开 bypass 2FA)需要

手动触发也可以在 Actions → Release → Run workflow 里选 stage(只暂存,需人工 2FA 审批)或 publish。

本地等价命令:npm run guard -- --tag v0.5.6(只检查是否该发)、npm run release:stage、npm run release:approve -- <stage-id>、npm run release(直接发布)。

安装

已发布到 npm:dsh-plugin-middle-panel(以 npm view dsh-plugin-middle-panel version 为准)。

  • 按包名安装(官方 npm 源,或任意镜像源,二者等价):
    dsh plugin --profile desktop add dsh-plugin-middle-panel
    # 镜像源
    dsh plugin --profile desktop add dsh-plugin-middle-panel --registry https://registry.npmmirror.com
    
    安装后重启 DSH;首次打开若界面无变化,Ctrl+Shift+R 硬刷新一次。
  • 从市场(Discover)安装:在 Plugins → Discover 添加索引源 https://raw.githubusercontent.com/Darkhe55/dsh-plugin-middle-panel/main/market-index.json, 搜索「画布面板」。该索引条目指向 npm 源,安装参数为 dsh-plugin-middle-panel@0.4.0。
  • 从 Git 直接安装(不依赖 npm):
    plugin_manager install_bundle
    target: git+https://github.com/Darkhe55/dsh-plugin-middle-panel.git#<commit>
    
  • 本地开发:install_bundle 指向本目录。

兼容 engines.dsh: >=0.2.2-rc.1 <0.3.0,无运行时依赖、无 postinstall。已在 0.2.2-rc.3 上实测:宿主路由、main slot 占用、canvas 工具注册与保存接口冒烟均通过。

用法

  1. 会话头部右侧的 画布 按钮,或让模型调用 canvas 工具(模型调用会自动展开面板):
    { "path": "demo/hero.html", "view": "auto", "note": "配色按设计稿改了" }
    
  2. 面板打开交付物后:
    • 文本:划选 → 浮出的「标记第 N 行」;
    • HTML:悬停高亮、点一下选中元素(选择器 + 文本);
    • 图片:先点「框选区域」再拖矩形,可复制或保存为 <文件名>.crop.png;Shift 悬停取色,点「标尺」量两点像素距离;SVG 还会列出可点选的图层;
    • 三维:点三角面生成标注,标注自带顶点、法线、面积与方位;「测距 / 测角」量尺寸。
  3. 每条标注可写一句意见,右上角 发送标注 (N) 会把下面这种文本填进输入框,用户补一句话即可发送:
以下是我在画布面板中的标注(共 2 条),请按编号逐条处理:

### `demo/notes.md`

1. `demo/notes.md` — 第 5-6 行
   > - 第一条:这句话写得太平。
   意见:这里改成更具体的说法

### `demo/sample.dae`

2. `demo/sample.dae` — 三角面 #0(共 2 面)
   > 0.888, 0.601, 0.000
   > 顶点 (0.000, 0.000, 0.000) (1.000, 0.000, 0.000) (1.000, 1.000, 0.000)
   > 法线 (0.000, 0.000, 1.000) · 面积 0.500 · 位于模型 中·中·前 · 该模型共 2 个三角面
   意见:这个面要加厚

处理完的条目标为 ✓:编号与意见都留着,但不再参与下一次发送,方便对齐「哪些还没改」。

实现要点

  • 面板注册为全局 main key canvas(ctx.layout.selectPanel('canvas'))。
  • 右半是 原生对话的嵌入式出现:props.renderFactorySlot('conversation.content', {})。框架没有第四列,root 槽明确禁止注册,这是唯一不 fork 框架的中间布局。
  • 会话身份与输入框 API 由一个零高度桥接组件从 conversation.composer.dock 拿:sessionId + inputActions(captureInsertion / insertText,失败回退 setDraft,再回退剪贴板)。
  • 文件读取走 remote.workspaceFiles,注意客户端 Remote 返回的是 { ok, value } 信封,必须解包;remote.workspaceFiles 也必须写进客户端 inject。
  • HTML 预览:读文本 → 注入 picker 脚本 → Blob iframe(sandbox="allow-scripts",不给 same-origin),选取结果经 postMessage 回传。
  • 标注只存客户端(localStorage,按 session 分键);发送后才进入 session log,符合“log 是唯一真相”。

检视能力

  • 定格聚光:Alt+S 暂停后画面定格,此时鼠标在同一元素上停 0.5 秒,其余元素淡到 45%,被悬停元素及其子树保持清晰;移开或换元素立即恢复,过渡 180ms cubic-bezier(.33,0,.2,1)。播放状态下不做任何淡化,页面按自己的交互跑。
  • 动画冻结 + 时间轴:暂停 / 继续(Alt+S,单手)、时间轴滑轨、±1 帧(16ms,Shift 100ms)。HTML 走 document.getAnimations()(CSS 动画 / 过渡 / WAAPI)+ SVG SMIL,并接管 requestAnimationFrame 让 JS 帧循环也能停住、+ 可逐帧推进。
  • 快捷键归属:只有鼠标悬停在面板上、或焦点在面板内时才响应;在会话输入框、任意 input/textarea/select/contenteditable 或输入法组合期间一律不拦截。
  • SVG 内联渲染:不再用 <img>,因此可悬停聚光、点元素生成图层标注、暂停/拖动 SMIL 时间线。插入前剥掉 <script>、on* 事件与 javascript: 链接。
  • 标注 ↔ 视图联动:鼠标停在标注栏的编号或意见框上,视图里对应的目标立即高亮、其余内容淡到 40%;移出即恢复。覆盖文本行、HTML 元素、SVG 图层、图片区域、docx 段落、xlsx/csv 单元格、pptx 页、DXF 图元、三维三角面、PSD 像素与 sketch 结构行。

已知边界:JS 帧无法回退(requestAnimationFrame 没有历史);页面若在启动时缓存了原生 requestAnimationFrame 引用则不吃接管;时间轴范围取运行中动画的最长有限 endTime,无限循环动画用随播放时间增长的窗口。

文件格式支持

类型格式渲染方式
网页.html .htm沙箱 iframe(注入元素选取与动效控制)
文档.md .txtMarkdown 渲染 / 纯文本,可编辑保存
图片.png .jpg .webp .gif .bmp .ico .avif位图缩放 + 框选区域标注
矢量.svg沙箱 iframe 内渲染(sandbox="allow-scripts",不透明来源),可悬停聚光、点选图层、暂停 SMIL
PDF.pdf运行时内置阅读器(不可用时提示路径与大小)
Office.docx .xlsx .pptx解 OOXML:段落 / 表格(≤3 表 × 300 行)/ 分页文本
表格.csv .tsv自解析渲染表格(支持引号内逗号换行)
三维.stl .obj .ply .off .3mf .dae .glb .gltfWebGL 三角面渲染:Blinn-Phong 双光源 + 雾、面/边可独立开关(边可切消隐或透视)、坐标轴、包围盒统计;左键旋转 / 右键平移 / 滚轮缩放 / F 复位取景;标注带面片信息,重新导出后按重心自动找回
视频.mp4 .m4v .webm .mov .ogv .ogg时间区间标注:拖进度条选段生成 00:12.4–00:18.7,暂停后可框选画面区域;播放/暂停、逐帧(← →)、倍速、点标注时间码跳转
图纸.dxf解析 LINE/CIRCLE/ARC/LWPOLYLINE/TEXT/POINT 渲染 SVG,可缩放
其他.doc .xls .ppt .step .iges .dwg .fbx 等明确提示需转换的格式(不显示二进制乱码)
视频(不可播).mkv .avi .wmv .flv .mpg .mpeg .3gp .ts明确提示「浏览器无法直接播放 .mkv,可播 mp4/webm/mov/m4v/ogv,请先转码」

视频标注

视频在画布面板里可以直接看、直接标。

操作结果
点进度条跳转到该时刻
输入后点「跳到 xx」输入与当前位置不同时右侧出现,点击直接定位
在时间框输入后回车跳转,支持 12.4 / 01:23.4 / 01:02:03.5;格式不对或超出片长会提示
未播放时悬停进度条画面预览该帧,移出后回到原位置
拖动选段画面跟随指针,浮出「起–止 · 时长」提示,便于确认范围
Alt + 滚轮时间轴缩放,以指针为锚点;最深 1 秒窗口并显示帧级刻度,反向滚回全部
Shift + 滚轮放大后左右平移时间轴窗口
右键时间轴只弹出菜单,不移动播放头(左键才会定位);菜单含插入入点 / 插入出点 / 清除入出点(也可用 I、O)
标记按钮随入出点变化:标记此刻 / 标记入点 / 标记出点 / 标记区间
单击进度条只跳转;移开鼠标不会回退到预览前的位置
拖动进度条选中一段时间 → 生成一条区间标注 00:12.4–00:18.7
单击不拖只打一个时间点
暂停后点「框选画面」在画面上框一块区域,附在该时刻(显示为「· 画面区域」)
空格播放 / 暂停
← →逐帧步进(1/30 秒);按住 Shift 为 1 秒
点标注栏里的时间码跳回该时刻

同一段时间重复标注不会产生新编号——起止时间都在 0.2 秒内视为同一处,只刷新原有那条的引用。

发送时时间码随编号一起进入输入框,形如:

1. `demo/clip.mp4` — 00:12.4–00:18.7
   意见:这段转场太慢

并排对比

画布面板可以拆成两个窗格,把两个交付物放在一起看。

操作结果
标签栏的 ⧉ / Alt+\进入并排,第二个窗格填入另一个已打开的交付物
拖动中间分隔条调比例,松手吸附到 30/40/50/60/70,双击复位 50/50
点击窗格 / Alt+1 Alt+2切换焦点(激活窗格有 1px 蓝色内描边)
点击另一个标签替换非激活窗格,不会越开越多
标签栏的 ↔ / ↕手动切换左右 / 上下排列(宽度足够时才生效,窄于 860px 一律上下)
Esc退出并排,保留当前窗格

排列方式按内容与宽度自动决定:

面板宽度行为
≥ 860px左右并排(默认),可用 Alt+方向键 在分隔条上微调
600–860px自动改为上下排列,避免文字被挤成窄条
< 600px 或高度 < 500px放弃并排,回到单栏

不会形变:HTML 与 PDF 在窄窗格里按固定逻辑宽度(1280 / 900)整体等比缩放,页面自身的版式不变;图片用 contain;SVG 等比居中;三维每帧按当前宽高比重算投影;表格保持字号并允许横向滚动;只有文本会正常重排。

标注仍然属于文件而不是窗格:并排时两侧的标注一起发送,工具栏会提示「另有 N 条在另一侧」。

安全模型

面现状
外部网络零:插件不访问任何第三方域名,唯一请求是本机路由(/dsh-canvas/save、/dsh-canvas/file)
文件读取走 DSH 进程内通道 remote.workspaceFiles;HTTP 路由只返回元数据(大小、mtime),不返回任何字节
文件写入仅限该会话工作目录的真实路径内:先 realpath 最近的已存在祖先、再拼接缺失尾段,用 path.relative 判断,符号链接与 Windows junction 一律拒绝;显式 workspace-write 策略;文本 ≤ 4 MiB(按 UTF-8 字节)
请求体读取时边读边计字节,超过 6 MiB 立即中断并 destroy,不把超大请求读进内存;文本按 Buffer.byteLength 判断,base64 先解码再判实际字节
路由来源仅接受 dsh-app://app、桌面端的不透明来源,以及与本服务同端口的 loopback 页面;其它本机端口的页面一律拒绝
预览隔离HTML 与 SVG 预览都运行在 sandbox="allow-scripts" 的 iframe(不透明来源,无 allow-same-origin),注入脚本只能读到该预览自身,拿不到本页的标注与保存路由
本地留存localStorage 保存打开的路径、标注(含 ≤240 字符原文引用)与面板尺寸;不上传,可随时「清空」

两级上限:JSON 请求体 6 MiB、最终文件 4 MiB。分开是因为 base64 会把文件撑大约 4/3,用同一个数字会误伤合法文件;而只看 text.length 又会把 400 万个中文字符(12 MB)当成 4 MB 放行。

已知残余风险:桌面端页面以不透明来源发起请求,因此无法用来源区分"同一台机器上的恶意页面";两条路由仍以 sessionId 作为凭据(UUID 级)。若需要更强的隔离,可再加"进程内下发的一次性令牌",但那需要宿主暴露客户端↔主进程的服务通道。

性能取舍

  • Markdown 只解析一次:解析结果按文本 memo,悬停只对已解析的块套一次 class,移动鼠标不再重跑解析器。
  • 悬停有独立订阅通道:hoverId 不再触发全量 notify,只有标注栏和当前视图会重渲染。
  • 文本行缓存:行元素按「内容 / 搜索词 / 高亮区间」memo;搜索输入走 useDeferredValue,打字不被上万行高亮阻塞。
  • 三维描边按需分配:边的顶点列表占 72 B/三角,仅在「边」开启时创建;面数超过 40 万且显示面时省略描边(此时描边本就看不清),切到线框仍显示全部边。
  • 预览与编辑阈值分离:16 MB 以内可只读查看,4 MB 以内才允许内联编辑——草稿、基线与每次快照都会复制整串文本。
  • 三维重定位先验后扫:先比标注原索引处的重心(O(1)),不一致才全量扫描,且超过 150 万面只标记不扫描。
  • npm test:13 组、116 项断言,覆盖 Markdown、Collada、标注重定位(文本与三维)、冲突判定、发送语义与控件契约。

最近的变化

0.5.0 · 并排对比与视频标注

  • 并排对比:两窗格 + 可拖分隔条(吸附 / 双击复位)、焦点描边、标签栏联动、断点自动上下、Alt+\\ Alt+1/2 Esc。
  • 防形变:HTML/PDF 按固定逻辑宽度整体等比缩放,图片保持固有比例,三维按宽高比重算投影。
  • 视频标注:拖进度条选段生成 00:12.4–00:18.7、暂停后框选画面区域、点时间码跳回该时刻;只有聚焦的窗格出声。
  • 一次调用打开多个产物:canvas 的 paths 参数,每个文件一个标签。

0.4.1 · SVG 彻底沙箱化

  • SVG 移入沙箱 iframe:此前为了点选图层而内联注入页面,等于让工作区里的 SVG 与本页面同源——恶意文件可以读标注、带 sessionId 调用保存路由。现在与 HTML 预览一样跑在 sandbox="allow-scripts"(无 allow-same-origin)里,点选、聚光、SMIL 控制改由 postMessage 驱动,消息回传校验 event.source === frame.contentWindow。
  • 清洗器作为第二道防线加固:除 <script> 与 on* 之外,还移除 foreignObject / iframe / object / embed / handler,href/src 只允许 #fragment 与 data:image/,并删除可改写危险属性的 SMIL 节点与 @import 远程样式的 <style>。

0.4.0 · 标得准、发得清、找得回

  • 修复保存冲突从未生效:元数据路由把沙箱句柄当路径 stat,每次都抛错,于是客户端拿不到 mtime、服务端也没有保存前快照,外部修改被静默覆盖。现在改为 stat 受限后的绝对路径,并附真实环境探针 test/live-probe.cjs。
  • 修复桌面端连不上服务:客户端把 http://127.0.0.1:1296 写死,服务换端口后每次保存要等 2 秒再报 Failed to fetch;改为同源相对路径(桌面壳会把非静态路径转发给本机服务)。
  • 修复 Collada 解析不出三角形:补全 vertices → source → float_array 两级引用,并支持 polylist / polygons / tristrips / trifans、命名空间前缀与交错步长。
  • 三维标注带面片信息(顶点 / 法线 / 面积 / 方位 / 总面数),重新导出后按重心自动重定位,几何真变了才标 ⚠。
  • 评审回执:点编号标为已处理(✓),保留编号与意见,但不再参与发送。
  • 按文件分组发送:多文件评审批次不再散乱。
  • 控件语义分层:动作(primary / default / quiet)与开关(aria-pressed)分开;开关改径向渐变(关闭红、打开绿,圆角更大),主按钮为淡色底 + 双向同心波纹。
  • 图片取色 / 标尺 / 裁剪落盘;搜索上一个 / 下一个;常用意见模板;复制操作浮动提示;标注行支持键盘(Delete / Enter)。
  • 移除三维导出 OBJ:格式控制属于专门的工具,不该由预览器顺手写文件。

0.3.0 · 标注与渲染

  • 标注导出 / 导入:存成 JSON 交接或进版本库;导入按定位串复用原编号。
  • 内容指纹重定位:文件改动后按前后文找回标注并更新行号(↻),找不到才标 ⚠。
  • 精确高亮:悬停标注只高亮选中的字(CSS Highlights),不再整行涂底色。
  • 标注栏分组:按文件分组、只看当前文件、跨文件搜索;每条可复制 / 单发 / 删除。
  • Ctrl+Enter 发送,可选「发后清空」。
  • Markdown 渲染补全:表格、任务列表、图片占位、删除线;修复结尾段落被吞。
  • 三维测距 / 测角;渲染诊断默认隐藏。
  • 窄面板自适应:次要按钮折叠进「⋯」,不再溢出。
  • 预览注入开关:帮助面板可关闭,不信任的 HTML 也能安全打开。
  • 构建:源码在 src/client.js,npm run build 生成 client.js(−26%)。

0.2.1 · 安全修复

  • /dsh-canvas/file 只返回元数据(大小、mtime),不再回传文件字节;文件内容只经进程内通道读取。
  • loopback 来源必须与本服务同端口,其它本机端口一律拒绝。
  • 移除未使用的范围读取客户端代码。
  • 新增「安全模型」章节。
  • 三维:面/边独立开关(线框可切消隐或透视),修掉共面边被深度测试剔除的问题。
  • 快捷键:暂停/步进改为 Alt+S / Alt+A / Alt+D(单手,避开输入法空格冲突)。

0.2.0 及更早

  • 面板头部 打开:工作区文件浏览(进目录、点文件即开),空状态也有「打开文件…」。
  • 文本视图支持 编辑 → 保存(Ctrl+S),写回走 Host 路由,限定在会话自己的工作目录内。
  • .md 默认 渲染 显示,可切「原文」;切换保持阅读位置、不抢焦点。
  • 同一文件只保留一个标签(路径归一化);同一位置重复标注复用原编号。
  • 嵌入式对话的 输入框吸底:官方吸底依赖 [data-conversation-composer-overlay],嵌入式出现拿不到该标记,插件补上标记并让视图区成为滚动容器、按输入框高度留白。

已知限制

  • 面板打开时 右侧栏不可用(框架只在对话占据主栏时渲染右栏),点「回到对话」恢复。
  • 嵌入的对话没有会话头部(标题 / 视图 tab / 右栏开关);面板顶部只保留「画布 · 路径 · 发送标注 · 回到对话」。
  • HTML 预览不做资源打包:相对路径的 CSS/JS/图片在 Blob 里加载不到,内联样式与 data URI 正常。
  • 图片没有“真图层”:SVG 走真实图层树,位图只能框选区域(不做 PSD、不做自动分层)。
  • 保存只能写回会话自己的工作目录内的文件;保存走插件自己的 Host 路由(Origin 白名单 + 目录约束 + 4 MiB 上限)。
  • 标注跨设备不同步;换浏览器或清 localStorage 会丢。
  • PDF 只能页级标注:预览走运行时内置阅读器(进程外渲染),拿不到页内点击坐标;页内区域标注需要自带渲染器(pdf.js,约 1 MB),与「零第三方网络依赖」的取舍冲突。
  • 界面仅中文:locale/*.json 目前只有市场元数据,不含 UI 文案。
  • 桌面端页面 origin 是 dsh-app://app/,浏览器上报 Origin: null,所以保存路由对“不透明来源”只放行能解析到真实会话工作目录的请求。

开发

  • 改客户端:硬刷新页面(Ctrl+Shift+R)。
  • 改 Host 半(index.js):plugin_manager 对 include:dsh-canvas-panel 禁用再启用,即可热重载。
  • 重新安装:plugin_manager install_bundle 指向本目录(已安装时先 remove_bundle)。

PDF 源码布局

src/client.js 只是加载壳,正文按职责放在 src/parts/,由 scripts/build.cjs 按文件名顺序拼接后再做去注释/缩进变换:

片段职责
00-head.jsstore、通用工具、非 PDF 视图
20-pdf-vendor.jspdf.js 动态加载与 vendor 路径
30-pdf-text.js页面文本缓存
35-pdf-selection.js视觉顺序取词
40-pdf-metrics.js测量字体与累积宽度表
45-pdf-ink.js渲染像素墨迹测量
60-pdf-page.js单页渲染、文字层、覆盖层
70-pdf-thumb.js缩略图
80-pdf-viewer.js文档、缩放、搜索、选区编排
85-pdf-wrapper.js失败回退
90-tail.js其余视图与导出

测试通过 test/_source.cjs 读取同一份拼接结果,不要直接读 src/client.js。

坐标约定

数据唯一单位产生处消费处
标注 rect / bands页面比例 0–1选词、框选覆盖层按 ×100 + '%' 渲染
搜索命中 rects页面比例 0–1搜索映射同上
实时选区 live页面比例 0–1getClientRects同上
span 的 left/top/fontSize/widthviewport CSS px文字层仅文字层内部
canvas 位图设备像素pdf.js render绘制与像素测量

跨层只允许通过 Geometry 风格的比例换算,禁止直接写 / box.width。

Plugin correlati