dsh-foreground-stream
DeepSeek Harness Web bundle 插件:前台命令执行(bash / pwsh 工具、非后台模式)期间的实时输出流式展示——对话页输入区上方出现一条"命令输出流条",随命令执行实时滚动显示 stdout/stderr,而不是等命令结束后才一次性看到。
- 不修改任何官方工具包:
tool/result内容与执行路径逐字节不变,模型语义零改动。 - 只旁路展示:命令完成后官方工具卡片照常呈现最终结果;本条只负责"执行期间"的增量画面。
- 纯内存缓冲,进程重启即清,不做任何跨会话持久化。
安装
从 GitHub 一键添加(推上仓库后)
包已按可分发结构打包(dependencies 声明了 host 依赖、files/.gitignore 排除了开发期 junction):
# 在任意位置执行(git 需走本地代理 127.0.0.1:7800):
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add github:<owner>/<repo>
# 或带版本:add github:<owner>/<repo>#<tag|commit>dsh plugin 会自动把声明了 dsh.bundle.patch 的包追加进 dsh.profile.bundles(自动 reconcile)。
安装后必做一步(settings 白名单补丁,幂等脚本):
node "G:\Deepseek Harness Desktop\data\dsh\plugins\dsh-foreground-stream\scripts\patch-allowlist.mjs"
# 或:npm run patch:allowlist> 原因:Web 客户端读写 settings 命名空间受 harness dsh-host-apiproxy 的 > WEB_SETTINGS_NAMESPACES 硬编码白名单过滤,插件侧无注册逃生通道;本脚本把 > dsh-foreground-stream 幂等加入该白名单。harness 升级/重装会覆盖补丁,需重跑。
然后重启 DSH web 宿主(Host half 改动不热更新;Client 刷新页面即可)。
本地目录安装(开发中)
# 在插件目录(含 package.json 的目录)下执行:
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add .> 等价于 pnpm 在 profile 目录里 add file:<插件绝对路径>。Windows 路径含空格时 > CLI 可能拆参,可改为:在 profile 目录 pnpm add "file:<插件绝对路径>",再手动把包名 > 追加进 dsh.profile.bundles(profiles/web/package.json)。
验证安装:
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" --profile web --dump-config | Select-String -Pattern 'dsh-foreground-stream'应出现:
- id: dsh-foreground-stream
name: dsh-foreground-stream重启后如何确认效果
1. 重启 DSH web(新组合在启动时加载)。 2. 让 agent 跑一条长时间前台命令,例如:
- pwsh(Windows):1..1000 | ForEach-Object { Write-Output "line $_"; Start-Sleep -Milliseconds 200 }
- bash(POSIX):for i in $(seq 1 1000); do echo "line $i"; sleep 0.2; done - 或 ping -t 8.8.8.8(需注意默认 timeout)。 3. 观察对话页输入框上方:出现"pwsh · <命令>"条,命令输出随执行逐行滚动; 命令结束显示"命令完成 · 退出码 N"约 4 秒后自动收起,官方工具卡片照常呈现最终结果。
设计说明
为什么是"镜像旁路"而不是改官方包
官方链路(已读源码确认):
dsh-tool-bash/pwsh.execute → ctx.shell.run(spec) → 执行器 runArgv
→ ctx.subprocess.spawn(spec) (stdio 恒为 collect 模式,无增量回调)
→ await handle.done → readFrom(0) 一次性汇总dsh-subprocess 服务契约明确:collect-mode reader 是 offset-based 非消耗式的 (readFrom(fromByte) 返回 { text, nextOffset, lossy, spillPath },独立读者互不消耗)。 因此插件用自己的游标增量读取 handle.collected.stdout/stderr,官方最终 readFrom(0) 完全不受影响——这就是 task-status "镜像补丁保证零竞争" 哲学的同一个缝。
callId 关联(把输出挂到具体工具调用)
- agent-loop 在 execute 之前同步 append
tool/call({turn, step, callId, name, arguments}),
经 session/event 派发 → 插件记录"当前前台命令"并建立 per-callId 缓冲。
- 真实事件形态(源码 + 实测确认,勿想当然):
- tool/call 的 arguments 是模型侧原始 JSON 字符串(agent-loop 直接落 block.arguments,只有 exec.arguments 经过 parseArguments)——插件 先 JSON.parse 再读 command / run_in_background。 - tool/result 的 message.content 是嵌套形态 [{ type:'tool-result', content:[{type:'text', text}] }] (createToolResultMessage 包装)——退出码提取需穿透包装层。
- 前台 shell 调用在 agent-loop 中严格串行(
startCallawait dispatch 直到进程结束),
因此"当前前台命令"是单槽;subprocess.spawn 包装器在 spawn 时校验 argv 包含该命令字符串(bash:["bash","-c",cmd];pwsh:["pwsh",...,"-Command",<preamble>cmd]; win32 sandbox 的 runner argv 同样内嵌原 argv)才挂 tap——MCP / skill / worker 等 其他工具的 spawn 因 argv 不匹配而绝不会被误挂。
tool/result(message.source.callId)收尾:running=false、尽力恢复退出码/信号、
清 pending。
数据通道与节流
- Host 注册只读路由
GET /plugins/dsh-foreground-stream/active?sessionId=...
(流条用,返回当前/最近命令 + 全文 + dock 配置位)与 GET /plugins/dsh-foreground-stream/output?callId=...(历史卡片实时轮询用); 客户端整段替换渲染(tail 效果)。
- 增量 tap 统一 120ms 心跳 drain(不随输出量放大);缓冲 64KB 保尾截断;
完成后保留 120s 供展示,之后清理;缓冲条目上限 100。
- 客户端运行中 400ms / 空闲 1s 轮询;输出区 max-height 180px 滚动 + 自动跟随底部
(用户上翻暂停跟随,回底恢复);ANSI 转义防御性清洗。
- 长命令行展开:命令行超过 64 字符(或含换行)时,流条标题变为可点击
(带 ▾/▴ 指示与 aria-expanded),点击展开完整命令(等宽字体块、 max-height 120px 可滚动),再点收起;调用切换时自动复位。短命令保持单行省略。
- 历史卡片实时输出:运行中的前台命令,其对话历史卡片自动展开终端面板,
面板输出区按 callId 轮询 /plugins/dsh-foreground-stream/output 实时更新 (400ms/次),无需底部流条即可在记录里直接看到命令输出;命令结束后停止 轮询,改由官方最终结果呈现。
- 自动展开/收起(
config.autoExpand,默认开):运行中的新命令面板自动展开、
运行完成后自动收起;用户手动展开/收起后不再自动干涉(手动状态优先)。
- 复制命令按钮:历史卡片终端面板内命令行右侧有复制按钮,悬停卡片时显示,
点击一键复制完整命令行(短暂显示 ✓)。
- 面板展开/收起动画(
config.animMs,默认 150ms):终端面板展开播
dshfs-panel-in、收起先播 dshfs-panel-out 再卸载(opacity + 轻微位移); 时长由设置项"面板动画速度"控制(关闭 0 / 快 80 / 标准 150 / 慢 300), animMs: 0 时无动画、即时展开/收起。
- WebUI 设置栏目(命令输出):
通用设置 → 外观 → 命令输出,两个开关——
"显示底部命令流条"(showDock)、"命令面板自动展开/收起"(autoExpand) 与动画速度下拉(animMs)。 值存 Host settings 命名空间 dsh-foreground-stream(settings.yaml 持久化), 即时生效(流条/卡片随下一次轮询应用),无需改组合行配置。 注册方式:Host 在 apply 时经 ctx.get('settings') 直连注册,服务未就绪时 500ms 轮询重试(最多 60s)——不依赖嵌套 ctx.inject(真实宿主上该路径 对部分插件不触发)。诊断:GET /plugins/dsh-foreground-stream/settings-state 返回 { state, source, registered }。
- ⚠️ harness 依赖:Web 客户端能否读写某命名空间由
dsh-host-apiproxy
的 WEB_SETTINGS_NAMESPACES 硬编码白名单决定(代码注释明说:未列入即 settings-not-exposed,即使命名空间已注册)。本插件已把 dsh-foreground-stream 加入该白名单(dependencies/dsh/node_modules/ @deepseek-ai/dsh-host-apiproxy/lib/index.js)。升级/重装 harness 会覆盖 该修改,需重新打补丁,否则设置栏目会退回"设置服务暂不可用"。
- 组合行配置作为默认层:
cordis.patch.yml的config.showDock/
config.autoExpand 仍是 base 默认值;UI 里手动开关后以 settings 用户值为准。
``yaml - id: dsh-foreground-stream config: showDock: false # base 默认层(UI 可再覆盖) autoExpand: false ``
槽位选择
| 候选槽 | 结论 |
|---|---|
conversation.input.dock(list)✅ 流条 | 官方既有 additive 座位(task-status 同款),对 bash/pwsh/任何 shell 工具统一生效;负责"执行期间"的实时滚动展示 |
tool.call.toolview(keyed)✅ 历史卡片 | 接管 bash + pwsh 两个 key(priority: -1 最低者胜出,替换官方 bash-toolview-sample 与 pwsh 的通用回退行),原版面板形态:标题行 = 命令执行图标(悬停/已展开变箭头)+ 工具名标题(Bash/PowerShell)· 描述摘要 + 状态;点击行展开终端面板,面板内命令行单行省略、点击命令行本身原位展开为完整多行(无第二面板),下方为输出(等宽、滚动)。数据取自 block 的 callView/resultView(card:'terminal'),缺失回退 parseArgs(argsRaw)/resultText |
conversation.details.tool(single) | 接管 = 渲染所有工具的输出详情面板,太重,未采用 |
settings.general.item(list)✅ 设置栏目 | 通用设置页的"命令输出"行(id command-output,order 15,紧随"外观"行):标题 + 两个开关(showDock / autoExpand),经 settingsScope 绑定 Host 命名空间读写 |
> 两处都只读展示:官方 execute 行为与模型可见的 tool/result 内容零改动。
局限
- 只覆盖经
ctx.shell的前台 bash/pwsh 工具调用;run_code内部子调度、终端
(PTY)路径、tool-bash-persistent 不经此链路,无流式旁路(不影响其正常执行)。
- 输出缓冲是"插件视角的镜像",与官方最终 tool/result 在超长输出截断边界上可能有
细微差异(各用各的 64KB 上限),但展示用途完全够用。
开发与自检
node --check lib/index.mjs # Host half 语法
node --check lib/client.js # Client half(浏览器 bundle)语法
node scripts/smoke.mjs # Host 逻辑冒烟:补丁/关联/增量/跳过/隔离/dispose文件
package.json # dsh.bundle.patch + dsh.client(web platform + inject)
cordis.patch.yml # 挂载行:- id: dsh-foreground-stream
lib/index.mjs # Host half:镜像 spawn 补丁 + session/event 关联 + /active 路由
lib/client.js # Client half:dock 流条 + 历史命令卡片(手写 ModuleLoader bundle)
scripts/smoke.mjs # Host 逻辑冒烟:补丁/关联/增量/跳过/隔离/dispose
scripts/smoke-client.mjs # Client 冒烟:bundle 结构/槽注册/展开交互/卡片渲染
README.md> Client half 是手写 plain-JS window.__ModuleLoader__.load({ id, factory }) bundle > (与 dsh-message-edit 同款格式),无需 tsdown 构建步骤,提交的产物即源码; > 安装时无 build 脚本,pnpm 不会触发 allowBuilds 拦截。