# dsh-workflow-runner
🇬🇧 English · 🇨🇳 中文
把 ZCode 风格的 **dynamic-workflows** 语义搬进 [dsh][dsh] Web UI。
写一小段 JS workflow 脚本,使用顶层 await 友好的 facade
(`phase` / `log` / `report` / `agent(name).ask(prompt)` / `world.run(cmd, args)`),
插件把它跑在隔离的 Node 子进程里,实时把进度广播到聊天输入区按钮、每轮尾巴卡片和浮动监控点上。
[dsh]: https://github.com/deepseek-ai/dsh
> **目标宿主**:dsh **桌面版** profile,**V0.2.0-rc.2** 及以上版本。
> 与 [`dsh-agent-teams`][dteams] 完全独立 —— 不依赖它,不与之协作,卸了它本插件照常工作。
[dteams]: https://www.npmjs.com/package/@nanmicoder/dsh-agent-teams
## 你会拿到
- **三个 UI slot**(通过 cordis 注册):
- `conversation.composer.dock` —— `▶ Workflow` 按钮(点击在新窗口打开编辑器)
- `conversation.chat.turnTail` —— 每轮 assistant 尾巴下的被动监控卡
- `shell.overlay` —— 运行中时右上角的浮动小点
- **standalone Web 应用**(`/workflow/` 路径),含:
- 脚本选择器(三个样本)、文件路径、源码预览
- `▶ Run` / `◼ Stop` 按钮
- 实时 phase 进度、子代理活动、报告列表、事件日志
- **服务端运行时**:spawn 用户脚本为 Node 子进程,stdin/stdout 上跑 JSON-RPC,使用与 dsh `balance` 同样的 key 解析路径调 DeepSeek chat-completion,跑 shell 命令并有可控超时。
## 安装
把插件按 dsh bundle 标准装入。从 dsh 宿主(它跑在 WSL)执行,**desktop** profile:
```bash
dsh plugin --profile desktop add dsh-workflow-runner
```
这会装 npm 上的正式发布版。想跟这个仓库的最新 `main`:
```bash
git clone https://github.com/gamelongtim-ux/dsh-workflow-runner.git
cd dsh-workflow-runner
dsh plugin --profile desktop add file:"$PWD"
```
然后重启 `dsh web`。聊天输入区里能看到 `▶ Workflow` 按钮。
要求 **dsh 0.2.0-rc.2 及以上**——已声明为 `engines.dsh`,插件市场能据此显示宿主版本要求,而不是"未声明"。
> 📌 如果同版本 pnpm `add file:` 不重新拷贝(pnpm 12 的怪行为),改 `package.json` 里的 `version`,或者直接用 `update.cmd`。
## 把这个插件给其他 dsh 用户用
下游 dsh 用户在"添加插件"对话框里有三种装入路径,看你打算怎么分发来选:
| 路径 | 在 dsh "添加插件"对话框里粘贴 | 备注 |
| --- | --- | --- |
| **npm 包名** *(默认)* | `dsh-workflow-runner` | 自 **0.1.5** 起已发到 npmjs.org。国内走镜像源更快,且能锁版本 |
| **GitHub URL** | `https://github.com/gamelongtim-ux/dsh-workflow-runner` | 永远可用;拉的是最新 `main` HEAD,而不是已发布版本 |
| **本地路径** | `<本地 clone 的绝对路径>` | 想跟你的本地编辑同步改;搭配 `update.cmd` |
> dsh 桌面"添加插件"对话框里的"中国大陆镜像源"是**读端加速**;包本身还得能经 npmjs.org(或 GitHub)拿到。
### 版本兼容性
npm manifest 里用 `engines.dsh` 声明宿主版本要求,dsh 插件市场读这个字段(以及 lockstep 的 `@deepseek-ai/dsh-*` peer)来标注卡片是"兼容"还是"不兼容"。`dsh.compatibility.dshReleases` 也一并声明,作为面向作者的元数据。dsh 自身在加载时**只强制检查 `peerDependencies`**,所以本包刻意**不**声明任何 `@deepseek-ai/dsh-*` peer:它只需要 base bundle 提供的挂载点与服务,声明宿主 peer 范围反而可能误拒一个实际可用的宿主。
## 独立运行(不需要 dsh)
测脚本很好用:
```bash
node lib/standalone.js # 监听 127.0.0.1:7910
```
浏览器开 ,选个样本即可。
## 写一个 workflow
脚本是 **ES module** (`.mjs`),默认导出 `async function main()`:
```js
'use strict';
const { agent, world, phase, log, report } = globalThis.__dsh;
export default async function main() {
phase('Plan');
const draft = await agent('planner', {
system: 'You are a senior algorithm textbook author.',
}).ask('用三段话解释匈牙利算法');
phase('Critique');
const issues = await agent('critic', {
system: '列出 3 个最关键遗漏,每个 ≤25 字。',
}).ask(draft);
phase('Gate');
const wc = await world.run('node', ['-e',
'process.stdout.write(String(arguments[0].split(/\\s+/).filter(Boolean).length))',
'--', draft,
], { timeoutMs: 5000 });
if (wc.exitCode !== 0) log('gate 警告 exit=' + wc.exitCode, 'warn');
report({ draft, issues, wordCount: Number(wc.stdout) }, 'final');
phase('Done');
return { ok: true };
}
```
### Facade 参考
| 调用 | 返回 | 备注 |
| --- | --- | --- |
| `phase(name)` | `Promise<{ok:true}>` | 标记一个 phase。所有已完成的 phase 都在 UI 列出 |
| `log(message, level='info')` | `Promise<{ok:true}>` | 流到事件日志。`level` ∈ `info`, `warn`, `error`, `debug` |
| `report(item, tag=null)` | `Promise<{ok:true}>` | 在 Reports 面板露出结构化项。`item` 可以是任意可 JSON 序列化的值 |
| `agent(name, opts).ask(prompt)` | `Promise<{content, usage}>` | 按名创建(或复用)子代理;同名复用保留历史。`opts` ∈ `{system, model, temperature, maxTokens}` |
| `agent(name).close()` | `Promise<{closed:true}>` | (可选)提示;历史随 run 结束释放 |
| `world.run(cmd, args, opts)` | `Promise<{exitCode, signal, stdout, stderr, durationMs}>` | spawn 本地 shell 命令。**非零退出码是值,不抛异常**。`opts.cwd / env / timeoutMs` 可配 |
> 第一个参数 `agent(name, opts)` 可只写 `agent('planner')`;`opts` 可省。
## 样本
`samples/` 里有三份:
| 文件 | 模式 |
| --- | --- |
| `smoke.mjs` | phase + log + world.run + report —— 不调 LLM,可无 key 跑通 |
| `planner-critic.mjs` | `planner` → `critic` → `planner` 同名复用带 critique 的修订 |
| `fanout-summarize.mjs` | 三个并行 `summarizer-N`,再 `reducer` 汇总成 cheatsheet |
## 架构
```
┌────────────────────┐ stdio 行分隔 JSON ┌──────────────────────────┐
│ 用户 .mjs 脚本 │◀──── RPC + notify ─────▶│ Node 子进程 │
│ (subprocess) │ │ runner-bootstrap.mjs │
└────────────────────┘ └──────────┬───────────────┘
│
▼
┌──────────────────────┐
│ WorkflowRuntime │
│ (cordis 宿主) │
│ │
│ - agent 状态表 │
│ - 通过 ctx 调 LLM │
│ - shell 调度 │
│ - SSE 广播 │
└──────┬───────┬────────┘
│ │
┌──────────────────────────┘ └───────────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ webServer 路由 │ │ dsh 聊天 UI │
│ /workflow/api/* │ ─ SSE 事件流 ──▶ ┌────────────┐ │ (三个 slot: │
│ /workflow/ │ │ iframe │ │ composer, turn- │
└──────────────────┘ │ editor + │ │ tail, overlay) │
│ viewer │ └──────────────────┘
└────────────┘
```
LLM 子代理直接打 DeepSeek `/v1/chat/completions`,优先通过 `ctx.llm.resolveCallConfig(...)` 取,
回退到与 [`dsh-usage-dashboard` 的 balance 模块][usage-dash-balance] 完全一致的
`DEEPSEEK_API_KEY` / `$DSH_HOME/.credentials.yaml` 解析路径。
[usage-dash-balance]: https://github.com/gamelongtim-ux/dsh-usage-dashboard
## 排错
### "启用失败: dsh: warning: 1 entry did not activate dsh-workflow-runner (dsh-workflow-runner): failed to import"
在 **v0.1.3** 修复了——去掉了 `@deepseek-ai/schemastery` 这个 peer 依赖。dsh 的 cordis 加载器解析裸 import 时先在 bundle 自己的 `node_modules` 找,新装时 schemastery 提升到宿主 profile 不一定找得到。如果你装的是 v0.1.1 / v0.1.2,卸了重装 GitHub URL 那条,会拉到 v0.1.3,零三方依赖。
### 装完没看到 `▶ Workflow` 按钮
在 **v0.1.5** 修复了。根因是包里混进了一个多余的 `lib/package.json`:dsh 的 `dsh.client` 扫描从解析到的入口(`lib/plugin.js`)向上找最近的 manifest,命中了这个 shadow 文件,于是把客户端 bundle 路径算成 `lib/lib/client.js`——该文件不存在。浏览器半边就这样被**静默丢弃**(UI 上没有任何报错),而服务端半边照常工作(所以 `/workflow/api/*` 有响应、聊天区里却什么都没挂上)。如果你在 v0.1.4 或更早,从 npm 或 GitHub 重装到 v0.1.5;手动删掉那个 shadow 文件也有效。
其余情况按这个顺序排查:
1. **插件**页里有 `dsh-workflow-runner` 且**开关是开**的
2. 拨完开关后**重启 dsh**(`任务管理器` → `DeepSeek Harness.exe` → 结束任务 → 重开)。bundle 是在进程启动时加载,不是拨开关时。
3. 你在**新会话**里看(左下角 → 新会话),不是装插件之前打开的老会话
4. dsh 桌面版是 **V0.2.0-rc.2 或以上**——已声明为 `engines.dsh`;老版本可能没有 `conversation.composer.dock` 这个 slot
### 打开 `/workflow` 报 `ERR_TOO_MANY_REDIRECTS`
在 **v0.1.5** 修复了。`/workflow` 和 `/workflow/` 原本都 302 到 `/workflow/`,于是带斜杠那个自己跳自己,永远跳不完。修复后 `/workflow/` 直接返回 `index.html`,只有不带斜杠的 `/workflow` 会重定向。
### 页面能开,但 Load / Run / Stop 点了没反应
分两种情况,请先看 `runStatus` 那一行或弹窗提示:
**报了 `run failed: HTTP 401`,且表单里没有错误提示** —— 在 **v0.1.6** 修复。0.1.5 给 `/api/*` 加了防护,却把 `requestRejection()` 的两种拒绝当成了一回事。它们并不等价:
- **403** 是 Host/Origin fence 判定"跨站 / DNS rebinding",这是**真正挡住网页 drive-by 的那一层**,必须一律拒绝;
- **401** 表示 fence **已经通过**,只是请求没带 dsh 的浏览器会话 cookie。
编辑页是自挂的 `/workflow` 静态页,而 dsh 只在根路径 `/` 的 token 握手上才下发会话 cookie,所以这个页面永远拿不到 cookie。把 401 也当作拒绝,就等于让插件自己的界面永久不可用,而且**没有增加任何本地防护**——cookie 的签名密钥就在 `$DSH_HOME/.credentials.yaml` 里,本地进程本来就能自己签一个。如果你在 v0.1.5,请升级到 v0.1.6。
**报 404 / `not found` 或按钮完全没反应** —— 在 **v0.1.5** 修复。编辑页 `public/editor.js` 原本用根路径 `/api/*` 调接口,而在 dsh 下这些请求会打到 dsh 自己的 API 网关。现在接口基址从模块自身 URL 推导,所以 dsh 前缀路由和独立模式(站点根)都能用。
### `▶ Workflow` 打开后显示的是 dsh 登录页
那是**宿主自己的鉴权**,不是本插件的问题:dsh 把每个页面都放在进程启动令牌之后。请打开宿主启动时打印的那个带 `?token=…` 的网址,让壳子先拿到会话 cookie,然后从应用内的 `▶ Workflow` 按钮进入,不要用手写的裸地址。
### "Lockfile failed supply-chain policy check" / `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`
**不是本插件本身的问题**——`dsh-workflow-runner` 零三方运行时依赖。pnpm 验 lockfile 时**整个 profile 的所有条目**都过 `minimumReleaseAge`,刚发的包(默认窗口 24h)直接拒。错误信息里点名的那个包(比如 `dsh-workbuddy-connect@0.7.5 was published at 2026-10-14T17:23Z`)才是元凶。
两条路:
- **等**那个包超过策略窗口(通常 1-3 天)再重试
- **绕过 pnpm** 直接拷:
```powershell
$D = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-workflow-runner"
$S = "D:\ZcodeStorage\.zcode\workspace\default\dsh-workflow-runner"
New-Item -ItemType Directory -Force -Path $D | Out-Null
Copy-Item "$S\lib","$S\public","$S\samples","$S\package.json","$S\cordis.patch.yml" `
-Recurse -Force -Destination $D
```
拷完**重启 dsh**。如果"插件"页还是没出现,手填 `~/.dsh/profiles/desktop/cordis.patch.yml`:
```yaml
- insert:
- id: dsh-workflow-runner
name: dsh-workflow-runner
config: { path: /workflow }
```
## 显式不做
- TypeScript 类型检查 —— 只用 JS + JSDoc
- `hole()` 运行中人工接管 —— 没有交互模式
- 缓存复用 amend —— 改脚本要重跑
- Settings → Plugins 上的 Plugin configuration 卡片
- 运行历史持久化 —— 仅内存保存,关 dsh 即丢
## 协议
MIT