dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
为回答里的命令行代码框加「运行」按钮,通过宿主 shell 服务执行该命令,输出显示在右下角浮动面板,或每个运行一个底部面板标签页。
给 DSH 回答里的每一个命令行代码框加一个「运行」按钮。 点一下命令就在宿主机上执行,stdout/stderr 实时流进右下角的浮动面板(默认行为)。不需要任何可选插件:另有一个 Panel 模式可以把每个运行送进底部工作台标签页,而它才是唯一用到 dsh-better-sidebar 的模式 —— 见三种输出位置。
状态:
0.1.0—— 可工作的插件包,纯手写(无打包器、无 TypeScript 构建)。已在真实 DSH 上验证(
0.1.5-rc.2):运行按钮能在会话自己的工作目录里执行命令、实时输出、并停止正在运行的进程。两种输出形态都端到端跑过 ——Dock模式在右下角堆叠卡片;装了dsh-better-sidebar时Panel模式每个运行一个底部工作台标签页。
English · 中文
DSH 把回答渲染成 Markdown,而代码框只提供一个操作:复制。每次回答里出现想试的命令(一段 git、一个 pnpm 脚本、一条诊断命令),你都得复制 → 切终端 → 粘贴 → 回车。
这个插件补上缺的那个动词。语言是命令行的代码框会在「复制」旁边多一个 ▶ 运行。点它之后:
▶ 运行 → ■ 停止 → ✓ 运行(退出码 0)/ ✕ 运行(非 0 或被终止);× 丢弃该运行;三种模式,可在运行条上切换(Dock / Panel / Off),选择会被记住:
| 模式 | 输出去哪 | 需要额外装东西吗 |
|---|---|---|
| Dock(默认) | 右下角固定堆叠,每个运行一张浮动卡片 | 不需要 |
| Panel | 每个运行一个底部工作台标签页 —— 就是终端所在的那个面板 | dsh-better-sidebar |
| Off | 不渲染输出;按钮仍反映状态 | 不需要 |
Panel 是通过 dsh-better-sidebar 公开的 ctx.betterSidebar 服务提供的(registerTab + openTab({ target: 'bottom' })),与它内置的终端 / git / 任务标签页用的是同一个扩展点。该插件是可选依赖:没装时 Panel 会被置灰,Dock 仍是默认,所以这个插件不依赖任何额外东西就能完整使用。
卡片刻意不锚定在发起它的代码框上。锚定看起来更整齐,直到该代码框滚出虚拟化列表、锚点查不到,同一个运行就会同时出现在两处。固定的右下角面板没有这种失效模式。
当代码框的 info string 是命令行语言时才会出现按钮:
| 语言标记 | 按钮文字 |
|---|---|
bash、sh、zsh、shell、shellscript | ▶ Run |
console | ▶ console |
powershell、pwsh、ps1 | ▶ PowerShell |
cmd、bat、batch、dos | ▶ cmd |
其他代码框(js、python、json 等)保持原样:执行它们需要自行拼装解释器调用,那是另一个功能。
这是一个由用户主动触发的执行入口,并且刻意不比 Agent 本身更强:
ctx.sandboxPolicy.resolve)与工作目录;点运行能到的范围不会超过该会话里 Agent 能到的范围。401。本插件已发布到 npm,也已收录进 DSH 社区插件目录,所以有四种安装途径。前两种是绝大多数人需要的。
设置 → 插件市场 → 搜 dsh-run-button → 安装。 市场读的是插件目录,它会优先用 npm 包、否则用预构建 tarball——两种情况都是几秒装完,用户机器不需要任何本地工具链。
dsh 命令行dsh plugin --profile web add dsh-run-button
这条会从 npm registry 解析 dsh-run-button,写入 profile 的依赖与 dsh.profile.bundles 列表,建立链接并挂载。卸载:
dsh plugin --profile web remove dsh-run-button
如果只想钉版本、或从仓库离线安装,用下面任一条:
# 最新 Release 上的预构建 tarball
dsh plugin --profile web add "https://github.com/EdwardXiao-bit/dsh-run-button/releases/latest/download/dsh-run-button.tgz"
# 直接从仓库源码装
dsh plugin --profile web add github:EdwardXiao-bit/dsh-run-button
裸跑
npm install dsh-run-button对 DSH 来说不算安装。 它只是把包取到node_modules,而 DSH 只加载出现在 profilebundles列表里的插件(或自带 bundle patch 的包),所以什么都不会被挂载。要手动装就写进 profile:
{
"dependencies": { "dsh-run-button": "^0.1.0" },
"dsh": { "profile": { "bundles": ["...", "dsh-run-button"] } }
}
# 把包 junction 进 profile 的 node_modules 并挂载
dsh dev inject <path-to>/dsh-run-button
然后重启 DSH(客户端半边只需刷新页面)。
bash/powershell 代码框。Panel 时改为每个运行一个底部工作台标签页(那个标签页有 Stop、Copy、Close 和一个 Clear finished 式的列表)。一个 Cordis 插件包,两个半边:
dsh-run-button/
├── package.json dsh.bundle.patch + dsh.client {platform:"web"}
├── cordis.patch.yml profile 行:- insert: [{id: run-button, name: dsh-run-button}]
├── lib/index.js HOST — ESM Cordis 插件
├── lib/client.js CLIENT — 经典脚本 bundle(window.__ModuleLoader__)
├── scripts/check-client.mjs 浏览器 bundle 的解析闸门 + 未声明全局审计
└── scripts/simulate-client.mjs 在真实 DOM 上挂载并走完整链路
Host 半边(lib/index.js)挂一条专用回环 RPC 通道 /dsh-run-button,端点 info、start、output、input、kill。start 解析会话的 cwd 与沙箱策略,然后用内置 shell 服务(ctx.shell.resolve → ctx.shell.start)启动命令并持有后台进程句柄。output 读取增量 delta(readOutput() 不会重复吐已读内容),返回 JSON 安全的视图:状态、退出码、信号、cwd、沙箱模式与累积输出。所有副作用都是 ctx.effect,其中包含卸载时杀掉活动进程。
Client 半边(lib/client.js)做三件事:
MutationObserver 扫描会话区里的 [data-code-block-banner],读出语言与 <pre> 文本,把运行按钮与 cwd 小标签追加为 banner 动作行的尾部子节点 —— React 的 reconciler 从不枚举 DOM 子节点,因此尾部额外节点能在重渲染中存活。由于记录超过 100 条时列表会虚拟化,扫描在 mutation 与滚动时各跑一次。connection.rpc.call 每 180 ms 轮询 output 并通知订阅者;React 视图都从这一份存储渲染,所以按钮、运行条与输出面板三者始终一致。Panel 模式下改为通过 ctx.betterSidebar.registerTab(...) 注册 per-run 标签页,并用 openTab({ target: 'bottom' }) 打开。标签页本体就是一个普通 React 组件,接收标准的 TabComponentProps。运行条注册在会话 Slot conversation.input.dock。
两个半边都是手写纯 JavaScript:
lib/index.js 就是普通 ESM —— Cordis loader 直接 import(不需要 tsconfig/tsdown)。lib/client.js 是经典脚本(不是 ES module),采用浏览器内核唯一接受的注册格式:window.__ModuleLoader__.load({
id: "dsh-run-button", // 必须等于 package.json 的 name
factory: (require) => { var module = {exports:{}}; var exports = module.exports; /* … */ return module.exports }
})
require 只能命中 shell 冻结的基线表(react、react/jsx-runtime、react-dom、@deepseek-ai/cordis 等),所以这个插件除 react 外不依赖任何东西。
发布前跑这两条闸门:
node scripts/check-client.mjs # 解析 + 物化 factory;并审计未声明的环境全局
node scripts/simulate-client.mjs # 在真实 DOM 上挂载,走完整链路
simulate-client.mjs 是关键那条。它像 shell 一样求值 bundle(window.__ModuleLoader__,再给一个类 Cordis 的 ctx),在 happy-dom 文档上挂载,渲染真实的代码框标记,然后断言:
run/start 与 output 确实在 RPC 通道上被调用;runId —— per-run 配对是最容易悄悄退化的地方;它之所以存在:早先的 check-client.mjs 只证明 bundle 能解析,不跑 apply(),于是未声明的全局变量(styles)顺利通过,把整页变成 "Failed to load plugins"。那两个上线的 bug 都是这个 harness 在编写过程中自己抓出来的 —— 这就是保留它的理由。
lib/client.js 是手写纯 JS,没有类型检查兜底:臆造 API、漏填必填字段都不会在构建期报错,只会变成整页白屏或崩掉的侧栏标签页。以下契约摘自宿主的实际定义。
动态 Cordis 插件沙箱会给出 styles 与 harness 这两个 builtin。而一个真正发布的包 bundle 两个都没有 —— 引用其中一个就会在 apply() 里抛错,而抛错的 apply() 会把整个 composition 拖垮("Failed to load plugins"),不只是这个插件。因此每个浏览器全局都通过 window.<name> 访问,并且 check-client.mjs 会在裸引用再次出现时让构建失败。
apply() 同时包在 try/catch 里:失败时会报告、回滚已挂载部分并返回,所以这个插件永远不可能是页面停止渲染的原因。
styles 服务客户端内核不提供 styles 服务,ctx.get("styles") 也拿不到东西。全局样式自己插 <style>,并在 teardown 里对称移除(apply() 可能被反复调用):
var tag = document.createElement("style");
tag.id = PREFIX + "-styles";
tag.textContent = CSS; // 本项目的 CSS 是字符串(数组 .join("") 得到),不是数组
document.head.appendChild(tag);
ctx.betterSidebar.registerTab():component 是必填TabDescriptor(定义见 dsh-better-sidebar/src/client/service.ts):
| 字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 唯一 id,同时是 SidebarTab.type |
title | ✅ | string | (() => string) |
component | ✅ | (props: TabComponentProps) => ReactNode。漏传即 createElement(undefined) → React #130 |
description | 新标签页列表里的一行说明 | |
icon | ReactNode | ((size: number) => ReactNode) | |
order | 菜单排序,默认 100 | |
hidden | 不在 + 菜单里出现 | |
available | (ctx, scope, state) => boolean | |
single / dedupeKey | 单实例 / 自定义去重 | |
createTab | 自定义开页(铸 id + 状态补丁) | |
urlTarget | 接管外部链接点击 | |
settings / badge | 设置页开关 / 标签页角标 | |
onOpen / onActivate / onClose | 生命周期回调 |
component 收到的 TabComponentProps:ctx、store、scope、tab、visible(是否活动且面板展开 —— 不 live 时应暂停轮询)。
函数声明会提升,所以 component: RunTab 可以写在 function RunTab() 之前。
dsh-better-sidebar 的 RenderBoundary 有两个作用域:ROOT(整个侧栏外壳,index.tsx)与 PER-TAB(单个标签页,Sidebar.tsx 的 TabContent)。报错前缀统一是 dsh-better-sidebar: <message>。看到这个前缀先怀疑 run-button 注册的标签页,而不是侧栏本身 —— 外壳和其他标签页通常是好的。
无需配置。可调项:
| 旋钮 | 位置 | 默认 |
|---|---|---|
| 轮询间隔 | lib/client.js 的 POLL_MS | 180 ms |
| 并发运行上限 | lib/index.js 的 MAX_LIVE_RUNS | 16 |
| 单条流上限 | lib/index.js 的 MAX_STREAM_BYTES | 256 KB |
| 完成后保留时长 | lib/index.js 的 RUN_TTL_MS | 10 分钟 |
| 识别语言 | lib/client.js 的 SHELL_LANGUAGES | 见上表 |
js、python 等按设计不在范围内)。sessionStorage),但超过 TTL 或 DSH 重启后宿主机就忘了。input(stdin)在宿主端已实现,界面尚未暴露。MIT —— 见 LICENSE。
dsh-smooth-stream 同样会接入代码块 banner —— 但它是为了流式动画与滚动控制,不是为了执行代码。两者不重叠:Smooth Stream 改变输出如何呈现,Run Button 增加的是会话区本来没有的操作入口。两者可以共存(已在同一个 profile 里一起验证过)。