deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
在 Agent 对话中把模型输出的 HTML / Mermaid 渲染成可交互卡片的产品线。宿主无关:同一套 core 与浏览器 UI 套件,接不同 Agent。
卡片原位替换原来的源码块:客户端观察会话 DOM、按围栏认领 html / mermaid 代码块(v0.6 起免 marker,围栏闭合即出卡),其余内容(散文、推理、图片、其它代码块)仍交回宿主自己的渲染管线。
卡片 chrome(v0.4):唯一一个悬浮工具栏钉在卡片右上角,hover / 键盘聚焦时浮现(源码态常显)。预览态 [↓][⧉][⛶ 放大] │ [‹› 查看源码],源码态 [语言][⧉ 复制] │ [◉ 预览],失败态 [错误码][⧉ 复制][↻ 重试]。源码态直接用宿主代码渲染器(shiki 高亮、<pre> 排版都是宿主的),宿主自带的 banner 折起来、语言名挪进胶囊——所以两种视图都只有一个工具栏。工具栏文案跟随会话语言。
放大查看(v0.7)落在宿主的右栏面板:点 ⛶ 打开右栏的一个标签页(标签名取自围栏语言,如 HTML),面板里是同一张卡片的预览 / 源码 / 复制 / 导出,分栏与浮动由宿主提供。宿主没有右栏面板时这个入口不出现——面板是宿主的面板,插件不自己造(早先塞进宿主表单对话框的做法实测被裁掉且无处可滚,见 CHANGELOG 0.7.0)。
按钮文案永远等于它真实会做的事,没有格式菜单:
| 卡片状态 | ↓ 下载 |
⧉ 复制 |
|---|---|---|
可栅格化(export.raster,当前是 mermaid) |
下载图片(PNG,尺寸 ≥ 屏幕密度) | 复制图片到剪贴板(PNG) |
仅矢量(export.vector,当前是 html) |
下载 SVG(沙箱 DOM 快照,矢量、可缩放) | 复制源码文本 |
| 尚无产物(快照未到/超限) | 下载 HTML 源文件(自包含沙箱文档) | 复制源码文本 |
导出目标由 action-target.ts 的两个纯函数裁定(primaryDownload / previewCopyStrategy),按钮从中取名,所以不会出现"写着下载图片、其实在下 HTML",也不会出现点了必然失败的图片动作。
分辨率:产物先经 core 的 normalizeSvgSize 把尺寸钉在自己声明的 viewBox 上,再按 max(config.export.scale, devicePixelRatio) 栅格化——响应式 SVG(mermaid 默认 width="100%"、无 height)作为 <img> 只有 300px 固有宽,不这么做导出的图会明显发糊(实测:卡片屏上 1484 设备像素,旧实现导出 600px 宽)。
HTML 卡为什么只有 SVG:Chromium 禁止含 foreignObject 的 SVG 图片读回 canvas(实测见 core/spec/html-artifact.md),所以 HTML 快照既栅格化不成 PNG 也写不进剪贴板。这是浏览器规则,与沙箱无关。
实机截图:预览态(mermaid 卡) · 源码态(HTML 卡) · 放大后的右栏面板 · 面板里的源码态。
core/spec(语言中立契约:协议 / schema / 错误码 / golden fixtures)
↓
core/ts(纯逻辑:fence 扫描 / gate / 切分计划 / 视图状态机 / 沙箱文档 / 报文校验;零宿主、零 DOM)
↓
platforms/web(浏览器 UI 套件:React 组件 + DOM 胶水;宿主差异经 WebPlatformPorts 注入;零宿主 SDK)
↓
adapters/dsh(DSH 接线:config wire + 围栏认领通道 + 资产路由 + 端口实现;零业务逻辑)
依赖单向;core 与 platforms 都不认识任何宿主。
| 能力 | 说明 |
|---|---|
scan |
按 fence 协议切分可视围栏(v0.6 起所有配置语言免 marker;render.mermaidRequiresMarker 可单独收紧 mermaid),并返回围栏的行区间 |
meta |
meta 键值解析 + HTML 实体反转义(title/preview/height/theme/allow) |
gate |
顺序短路判定:unsupported_language → fence_incomplete → source_too_large → render_disabled → too_many_blocks |
index |
组合 scan+gate 产出宿主中立的可视化索引(看不到消息正文的宿主形态) |
plan-visual |
把一条消息的 markdown 切成"文本段 / 卡片段"并随段带上源码,供原位替换(带 UI 的宿主形态) |
view-machine |
预览/源码双视图纯 reducer,含失败记忆 |
sandbox-doc |
逐字节确定的沙箱文档 + sandbox 属性矩阵 + CSP(安全单源) |
capture-validate |
捕获报文形状 / 在途 id / dataURL 前缀白名单 / 长度上限校验 |
svg-size |
把 SVG 产物的尺寸钉在其 viewBox 上(导出分辨率不再取决于作者怎么写) |
html-artifact |
沙箱 DOM 快照 → foreignObject SVG 产物(HTML 卡的导出形态) |
naming |
导出文件名(slug + 时钟端口时间戳) |
visualize_present(可选工具) |
把片段渲染成自包含文件并回显路径;默认关闭 |
index 与 plan-visual 是同一套 scan+gate 的两种产物;对同一 fence 产出的 IndexItem 逐字节一致。
宿主
| 能力 | dsh | octop | pi |
|---|---|---|---|
| 原位替换(卡片取代源码块) | ✅ DOM 围栏认领(观察 + 旁挂) | ⚠ 需宿主有等价的节点渲染器扩展点 | ❌ |
| 已有会话追溯 | ✅ 读消息正文即可,无索引/无重放依赖 | ⚠ 视宿主能否读到正文 | ❌ |
| 预览 / 源码双视图 | ✅ | ✅(组件经端口注入) | ❌ 终端 |
| 源码态复用宿主高亮 | ✅ 宿主 CodeBlock(shiki) |
⚠ 视宿主有无代码渲染器 | — |
| 工具栏位置与文案 | ✅ 悬浮于卡片右上角,文案跟随会话语言 | ⚠ 视宿主 | — |
| 放大查看 | ✅ 宿主右栏标签页(宿主没有右栏 ⇒ 入口不出现) | ⚠ 视宿主有无面板 | ❌ |
| 图片导出 | ✅ PNG(mermaid,≥ 屏幕密度)/ SVG(html) | ⚠ 视 webview 桥能力 | ❌ |
| 配置跟随宿主 config | ✅ wire 送归一化 config | ⚠ 需等价通道 | — |
visualize_present 工具 |
✅(可选开关) | ✅ | ⚠ 降级为"写文件 + 路径回显" |
第一期只实现 dsh;octop / pi 仅为规划位,未实现。
渲染语言
| language | marker 要求 | 导出 | 沙箱 |
|---|---|---|---|
html |
免 marker(v0.6 起全部认领) | SVG(沙箱 DOM 快照 → foreignObject;PNG 受 Chromium 限制不可得)+ 自包含 .html 文档 |
opaque-origin iframe |
mermaid |
免 marker(render.mermaidRequiresMarker 可改严) |
PNG + SVG | 渲染出的 SVG 同样注入沙箱;引擎为独立懒加载资产(首张卡时经插件自有路由加载) |
新增一种渲染语言 = 动 core/spec + core/ts + platforms/web/src/registry.ts 一行,adapters/ 零改动。
三种方式,按场景选一种即可。推荐方式一:一条命令、零构建。
dsh plugin --profile <profile> add \
"@slothtron/visualize-dsh@git+https://github.com/Slothtron/visualize.git#master&path:adapters/dsh"
dsh --profile <profile> --dump-config | grep -A3 visualize # 应出现 id: visualize 一行
要可复现就把 master 换成 commit(git rev-parse HEAD)或 tag;日常跟分支用 master 最省事。升级=换 ref 重装
(dsh plugin --profile <profile> add "…@git+…#<新 ref>&path:adapters/dsh"),回滚=装回旧 ref。
写法要点(都实测过)
| 要点 | 说明 |
|---|---|
&path:adapters/dsh 用冒号 |
写成 &path= 会被当成 ref 的一部分,报 Could not resolve path=adapters/dsh to a commit |
| 整串必须加引号 | 里面有 # 与 &,不加引号会被 shell 截断或后台化 |
| ref 写实 | 只写 #path:adapters/dsh 会跟随默认分支 HEAD |
前置条件与常见报错
| 现象 | 原因 | 处理 |
|---|---|---|
Could not resolve <ref> to a commit of <url> |
ref 解析不到:写错,或该 commit/tag 还没 push | 用 git rev-parse HEAD 取真值并确认已 push |
could not read Username for 'https://github.com' |
私有库或未登录而走 HTTPS | 配 credential helper,或改用 git+ssh://git@github.com/Slothtron/visualize.git#… |
安装时提示 git-hosted plugins build on install via their prepare script … allowBuilds |
这是 dsh 对所有 git 依赖的通用提示 | 本包没有 prepare 脚本,可忽略;先解决上面两类真实错误 |
为什么安装时不需要构建:lib/ 走提交入库策略(AGENTS §3 的二选一,见 ./.gitignore 的说明),
git 依赖里直接带着构建产物(core 与 platforms/web 已内联进 lib/client.js),所以不跑 prepare、
也不需要 pnpm 的构建授权。代价是产物新鲜度:pnpm run verify 里的 verify:lib 会断言"重新构建后产物
与提交内容一致",改了源码必须把重新构建的 lib/ 一起提交。
git clone https://github.com/Slothtron/visualize.git && cd visualize
pnpm install && pnpm run verify && pnpm run build
cd adapters/dsh && npm pack --pack-destination /tmp
dsh plugin --profile <profile> add "@slothtron/visualize-dsh@file:/tmp/slothtron-visualize-dsh-<版本>.tgz"
注意:同一路径的 tarball 重装时 pnpm 可能跳过解析(
Lockfile is up to date, resolution step is skipped), 导致装的还是旧内容——换一个新文件名再装即可。
dsh plugin --profile <profile> add @slothtron/visualize-dsh
配置分片由 core 唯一定义(core/ts/src/config.ts);适配器只做映射,不得新增第二份默认值。非法配置在加载期以 invalid_config(附字段路径)失败。
| 分片 | 键 | 默认值 |
|---|---|---|
render |
enabled |
true |
languages |
["html", "mermaid"] |
|
mermaidRequiresMarker |
false |
|
presentTool |
false |
|
limits |
maxSourceBytes |
262144(256 KiB) |
maxBlocksPerMessage |
8 |
|
maxIndexedItems |
512 |
|
maxConcurrentIframes |
4 |
|
maxCaptureBytes |
16777216(16 MiB) |
|
sandbox |
csp |
default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'unsafe-inline' |
allowAssets |
false(置真只把 img-src 放宽为 data: https:) |
|
export |
formats |
["png", "svg", "html"] |
scale |
2 |
|
maxPixels |
4096 |
|
timeoutMs |
5000 |
|
view |
defaultView |
"preview" |
pnpm install
pnpm run verify # build + typecheck + test + verify-contract + dependency-lint + pack-check + verify:lib
pnpm run verify 是单一入口,等价的 POSIX 包装是 ci/verify.sh。分步:
| 命令 | 作用 |
|---|---|
pnpm run build |
三包构建(adapter 内联 core + platform,产物自包含) |
pnpm run typecheck |
tsc --noEmit(core 无 DOM lib,platform 有) |
pnpm run test |
vitest(core 纯 node、platform jsdom、adapter node) |
pnpm run verify-contract |
跑 core/spec/fixtures,含 sandbox-doc 逐字节 golden |
pnpm run dependency-lint |
强制依赖方向:core 零宿主零 DOM、platforms 零宿主 SDK、adapters 语言无关 |
pnpm run pack-check |
npm pack --dry-run 只含白名单(含 lib/client.js) |
pnpm run verify:lib |
lib/ 入库策略的护栏:重新构建后产物必须与提交内容一致(见 .gitignore) |
实机验收(需先起一个装好插件的 dsh 服务;脚本不依赖任何 npm 安装):
node scripts/acceptance-card.mjs "http://127.0.0.1:18765/?token=<token>" /tmp "<会话标题>" [截图前缀]
# 指向含卡片的会话:驱动工具栏、解码导出的图、逐条断言并在失败时以非零码退出
node scripts/probe-export.mjs "http://127.0.0.1:18765/?token=<token>" # 导出分辨率数值诊断(不出图不断言)
node scripts/probe-html-artifact.mjs "…" # foreignObject 快照为何只能出 SVG
端到端:装插件 → 让模型输出一个 ```html 或 ```mermaid 围栏(免 marker)→ 该围栏原位变成卡片(原文不再以代码块出现,但可在卡片的源码视图里带高亮查看/复制/导出)→ 工具栏切换视图 → 导出。
core/spec/fixtures/sandbox-doc/*.expected.html 是安全契约的字节级真相。修改沙箱文档后:
node scripts/verify-contract.mjs --update # 重新钉 golden
该命令会打印醒目警告;必须逐字节审阅 diff 后再提交,不得用它掩盖非预期的漂移。
唯一入口是 ci/verify.sh;它不含任何托管方专有内容,换托管方时只需让流水线调用它。
sh ci/verify.sh
| 托管方 | 接法 |
|---|---|
| GitHub Actions | run: sh ci/verify.sh(workflow 由托管方自行添加) |
| GitLab CI | script: [ "sh ci/verify.sh" ] |
| Jenkins | sh 'sh ci/verify.sh' |
| 内部流水线 | 调用同一脚本 |
可选的最新宿主探针(无 matrix 支持时用):
VERIFY_LATEST_HARNESS=1 pnpm run verify:latest-harness
sandbox="allow-scripts",禁止 allow-same-origin),绝不进宿主 React 树、绝不用 dangerouslySetInnerHTML。default-src 'none';allow="assets" 只放宽 img-src。CSP 串、sandbox 属性矩阵、bootstrap 文档是 core 的 golden fixture,适配器无权放宽。event.source === iframe.contentWindow,core 校验报文形状与在途 id,源取回校验 ref 命中当前索引。visualize_present 例外,且仅在显式开启时写文件);fixtures 全假值。M0 运行时疑点已全部实机闭环(dsh 0.1.5-rc.2 / Node 26):
wire 的第三方 projection key 是否真出现在 useProjection props 中wire 则恒 undefined)。assistant/message 事件上的 seq / 载荷字段event.seq + event.data.message.content[]。visualize)。tools 服务注册形态visualize_present 显式降级并告警。剩余边界:
SourcePort"路径(buildIndex 与 SourcePort 已为此保留)。data-code-block-banner / data-code-block-content);宿主改名只会让 banner 重新出现,不影响功能。<style>,退化为"胶囊常显 + banner 可见 + 源码顶部无内边距",控件仍可用(结构在 inline style 里)。mermaid);html 卡显示"下载 SVG"/"复制源码"——按钮文案如实反映它真正会做的事,原因见 core/spec/html-artifact.md。实测细节、降级清单与 9 条集成缺陷记录见 adapters/dsh/README.md。
visualize/
├── core/spec/ # 语言中立契约 + golden fixtures
├── core/ts/ # @slothtron/visualize-core
├── platforms/web/ # @slothtron/visualize-web
├── adapters/dsh/ # @slothtron/visualize-dsh
├── scripts/ # verify-contract / verify-lib-fresh / dependency-lint / pack-check
│ # / acceptance-card / probe-export / probe-html-artifact
├── ci/verify.sh # 单一 verify 入口(POSIX)
├── .github/workflows/ # verify 门禁(GitHub 托管时生效;其它托管方调 ci/verify.sh)
├── docs/acceptance/ # 实机验收截图(**不放设计文档**)
└── tests/contract/ # 契约测试说明(实现即 scripts/verify-contract.mjs)
各包 lib/ 属于提交入库的构建产物(策略与理由见 ./.gitignore);sourcemap 不入库。
| 类型 | 位置 |
|---|---|
| 能力契约与协议(实现与测试的单源) | core/spec/(在本仓,随代码演进) |
| 安装 / 配置 / 验证 / 支持矩阵 | 本 README 与三处包 README(在本仓) |
| 实机验收截图 | docs/acceptance/(在本仓,是证据不是设计) |
| 设计稿与开发计划 | 不在本仓:plugins/docs/visualize-design.md、plugins/docs/visualize-dev-plan.md、plugins/docs/visualize-dsh-design.md |
设计方案文档刻意与代码仓库分离:仓库里禁止出现设计文档(.gitignore 也对这几个路径做了兜底)。要改设计请改仓外那几份,再在 CHANGELOG.md 与本 README 里引用结论。
MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。