dsh-workflow-map 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -6
- package/README.en.md +76 -28
- package/README.md +74 -27
- package/client.js +163 -19
- package/cordis.patch.yml +3 -3
- package/docs/OPEN-SOURCE-GUIDE.md +8 -8
- package/docs/host-compatibility.md +320 -0
- package/docs/screenshots/01-main-dark.png +0 -0
- package/docs/screenshots/02-main-light.png +0 -0
- package/docs/screenshots/03-running-step.png +0 -0
- package/docs/screenshots/04-step-detail.png +0 -0
- package/docs/screenshots/05-hot-inject.png +0 -0
- package/docs/screenshots/06-settings.png +0 -0
- package/package.json +20 -3
- package/src/client/ProgressUnder.js +1 -1
- package/src/client/StepColumn.js +4 -1
- package/src/client/panel.js +97 -4
- package/src/client/pulse.js +11 -1
- package/src/client/store.js +23 -1
- package/src/client/theme.js +27 -11
- package/src/fold.mjs +210 -19
- package/src/orchestrator.mjs +43 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# 宿主兼容性核验(DSH `0.2.1-rc.1` – `0.2.1-rc.5`)
|
|
2
|
+
|
|
3
|
+
本文件回答一个问题:本插件声明的宿主范围,在 `0.2.1` 全系列(`rc.1` 至 `rc.5`)里是否真的有依据。
|
|
4
|
+
|
|
5
|
+
结论以 `package.json` 的 `engines.dsh` 为唯一声明口径;本文件记录该口径的判据、核验方法与逐条证据,供使用者自行复核。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、声明
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20",
|
|
14
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0-0"
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
区间含义:
|
|
19
|
+
|
|
20
|
+
- 起点 `>=0.2.1-rc.1`:`0.2.1` 元组上的第一个预发布版本,带显式预发布标签。
|
|
21
|
+
- 上界 `<0.3.0-0`:排除 `0.3.0` 及其预发布(`0.3.0-0` 是 `0.3.0` 预发布的最小值),即不声称兼容下一 minor。
|
|
22
|
+
|
|
23
|
+
本插件不使用 `dsh.host` 之类的自定义元数据字段:宿主包由 `peerDependencies` 声明(均为可选 peer),版本闸门由 `engines.dsh` 单独决定。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 二、`engines.dsh` 由谁判定:两种判据
|
|
28
|
+
|
|
29
|
+
同一个区间字符串会被两套判据读取,结论不完全相同,声明必须同时满足两者。
|
|
30
|
+
|
|
31
|
+
### 2.1 DSH 自身的兼容性判定
|
|
32
|
+
|
|
33
|
+
`@deepseek-ai/dsh-app-boot` 的 `evaluatePluginCompatibility()` 是安装与启用期的判定入口:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// @deepseek-ai/dsh-app-boot lib/index.js:252-284(节选,省略参数校验与回落分支的具体实现)
|
|
37
|
+
function satisfies(runtimeVersion, range) {
|
|
38
|
+
return range.trim() !== "" && semver.satisfies(runtimeVersion, range, { includePrerelease: true });
|
|
39
|
+
}
|
|
40
|
+
function unmetRequirements(fields, runtimeVersion) {
|
|
41
|
+
const engines = Object.hasOwn(fields, "engines") ? objectOf$1(fields.engines, "Plugin manifest engines") : {};
|
|
42
|
+
if (Object.hasOwn(engines, "dsh")) {
|
|
43
|
+
const range = engines.dsh;
|
|
44
|
+
if (typeof range !== "string") throw new Error("Plugin manifest engines.dsh must be a string");
|
|
45
|
+
return satisfies(runtimeVersion, range) ? {} : { "engines.dsh": range };
|
|
46
|
+
}
|
|
47
|
+
// 没有 engines.dsh 时,回落到逐个 @deepseek-ai/dsh / @deepseek-ai/dsh-* peer 的 range 判定
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`@deepseek-ai/dsh-plugin-manager` 在三个位置调用它,未满足即抛 `incompatible-version`:
|
|
52
|
+
|
|
53
|
+
| 位置 | 场景 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `lib/types/index.js:368` | 列出 bundle 时逐个核对 |
|
|
56
|
+
| `lib/types/index.js:660` | 安装(`add`)前核对 |
|
|
57
|
+
| `lib/types/index.js:857` | 启用(`enable`)前核对 |
|
|
58
|
+
|
|
59
|
+
同一包对外的 `requirements` 字段说明也把键名写死为 `engines.dsh`(`lib/typert.host.js:59`):
|
|
60
|
+
|
|
61
|
+
> "Only the requirements the running version does not satisfy, keyed `engines.dsh` or by DSH peer package name."
|
|
62
|
+
|
|
63
|
+
### 2.2 npm / pnpm 自身的 engines 检查
|
|
64
|
+
|
|
65
|
+
npm 与 pnpm 用不带 `includePrerelease` 的默认 semver 语义检查 `engines`。
|
|
66
|
+
|
|
67
|
+
### 2.3 两者对同一区间的判定(semver 7.8.5 实测)
|
|
68
|
+
|
|
69
|
+
| 运行时版本 | `>=0.2.1-rc.1 <0.3.0-0`(npm 默认) | 同区间(DSH 闸门,`includePrerelease`) | `>=0.1.0-rc.1 <0.3.0-0`(npm 默认) |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `0.2.1-alpha.5` | 不通过 | 不通过 | 不通过 |
|
|
72
|
+
| `0.2.1-rc.1` | 通过 | 通过 | **不通过** |
|
|
73
|
+
| `0.2.1-rc.2` | 通过 | 通过 | **不通过** |
|
|
74
|
+
| `0.2.1-rc.3` | 通过 | 通过 | **不通过** |
|
|
75
|
+
| `0.2.1-rc.4` | 通过 | 通过 | **不通过** |
|
|
76
|
+
| `0.2.1-rc.5` | 通过 | 通过 | **不通过** |
|
|
77
|
+
| `0.2.1` | 通过 | 通过 | 通过 |
|
|
78
|
+
| `0.2.2-rc.1` | 不通过 | 通过 | 不通过 |
|
|
79
|
+
| `0.3.0-0` | 不通过 | 不通过 | 不通过 |
|
|
80
|
+
| `0.3.0` | 不通过 | 不通过 | 不通过 |
|
|
81
|
+
|
|
82
|
+
由此得到两个写法上的硬要求:
|
|
83
|
+
|
|
84
|
+
1. **起点必须落在 `0.2.1` 元组上。** 默认 semver 只允许「与区间内某个比较符同元组」的预发布版本参与匹配;写成 `>=0.1.0-rc.1`(元组 `0.1.0`)时,`0.2.1-rc.x` 全部被判为不匹配。所以在 npm/pnpm 的默认判据下,`>=0.1.0-rc.1 <0.3.0-0` 会静默排除全部 `0.2.1` 预发布版本。
|
|
85
|
+
2. **上界取 `<0.3.0-0`。** 写成 `<0.3.0` 会在 DSH 闸门(`includePrerelease`)下把 `0.3.0-rc.x` 也算作通过,与「不声称兼容下一 minor」冲突。
|
|
86
|
+
|
|
87
|
+
DSH 闸门带 `includePrerelease`,因此 `0.2.2-rc.x` 这类后续 `0.2.x` 预发布也会通过。这是有意接受的结果:声明覆盖整个 `0.2.x` 预发布链,而不是逐版本列举。
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 三、逐版本核验表
|
|
92
|
+
|
|
93
|
+
| 版本 | 取样方式 | 结论 |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `0.2.1-rc.1` | npm 闭包元数据(206 包)+ 9 个契约点的逐文件 SHA / 锚点窗口 | 契约面与 `rc.5` 一致;闭包比 `rc.3` 少 3 个包(见 §4.3) |
|
|
96
|
+
| `0.2.1-rc.2` | 同上 | 同上 |
|
|
97
|
+
| `0.2.1-rc.3` | 同上 + 核验机实机运行(插件加载、面板注入、视图路由) | 基线版本 |
|
|
98
|
+
| `0.2.1-rc.4` | npm 闭包元数据 + 5 个变动包逐文件 SHA + 契约锚点窗口 | 契约面无变化 |
|
|
99
|
+
| `0.2.1-rc.5` | npm 闭包元数据 + 逐文件比对 + **隔离 `DSH_HOME` 实机运行**(`--dump-config` 解析、web 宿主启动、`/dsh-workflow/*` 三种端点实测,见 §3.1) | 契约面无变化,且宿主半在 `rc.5` 上实机应答 |
|
|
100
|
+
|
|
101
|
+
`rc.4` 的结论来自与 `rc.3` 的逐文件比对,不是实机运行结论(见 §8)。`rc.5` 已补做隔离实机运行,记录见下节。
|
|
102
|
+
|
|
103
|
+
### 3.1 `rc.5` 隔离实机运行记录(2026-10-30)
|
|
104
|
+
|
|
105
|
+
核验机内核是 `rc.3`,因此不升级本机环境:在 `%TEMP%\wfmap-rc5-run\` 另起一套 `npm i @deepseek-ai/dsh@0.2.1-rc.5`,`DSH_HOME=%TEMP%\wfmap-rc5-run\home`(隔离),新 profile `wfmap5` 的 `dsh.profile.bundles` = `dsh-base` + `dsh-web-app` + `dsh-workflow-map`(后者以 `link:` 指向本仓库的工作副本,未改动源文件)。全程不写本机 `~/.dsh`、不动 `desktop` profile。
|
|
106
|
+
|
|
107
|
+
**A 级 —— `rc.5` 的 loader 解析本插件 manifest 与 patch:通过**
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
node <run>\node_modules\@deepseek-ai\dsh\lib\bin.js --profile wfmap5 --dump-config
|
|
111
|
+
# 退出码 0;stdout 1234 行;stderr 0 行
|
|
112
|
+
# 末尾三行即本插件层:
|
|
113
|
+
# # == dsh-workflow-map
|
|
114
|
+
# - id: workflow-map
|
|
115
|
+
# name: dsh-workflow-map
|
|
116
|
+
# stdout 全文无 unknown / invalid / unresolved / warn / error 类告警
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**B 级 —— `rc.5` 宿主上路由实机应答:通过**
|
|
120
|
+
|
|
121
|
+
同一套 `rc.5` 起 web 宿主(`--profile wfmap5 --port 55999 --no-open --host 127.0.0.1`),逐端点实测(响应头均为 `content-type: application/json; charset=utf-8`,与 `src/route.js` 的契约一致):
|
|
122
|
+
|
|
123
|
+
| 请求 | 状态 | 响应体(原文) | 说明 |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| `GET /dsh-workflow/state?sessionId=none` | `404` | `{"ok":false,"error":"unknown session"}` | 前缀路由已挂上;`sessions` 服务在 `rc.5` 上可用(否则会是 503) |
|
|
126
|
+
| `GET /dsh-workflow/state`(无 `sessionId`) | `400` | `{"ok":false,"error":"sessionId is required"}` | 参数校验分支 |
|
|
127
|
+
| `POST /dsh-workflow/steer` | `404` | `{"ok":false,"error":"unknown session"}` | 热注入通道已挂上 |
|
|
128
|
+
| `POST /dsh-workflow/unsteer` | `404` | `{"ok":false,"error":"unknown session"}` | 撤回通道已挂上 |
|
|
129
|
+
| `GET /dsh-workflow/nope` | `404` | `{"ok":false,"error":"not found","path":"/dsh-workflow/state \| /dsh-workflow/steer \| /dsh-workflow/unsteer"}` | 前缀内部路由表,证明命中的是本插件的 handler,而非宿主兜底 404 |
|
|
130
|
+
|
|
131
|
+
对照:同一宿主上 `GET /` 返回 `401 dsh web authentication required; reopen the URL printed by dsh web.`(`dsh-client-connection` 的页面鉴权闸门,`lib/index.js:576`),属宿主行为,不影响插件路由。
|
|
132
|
+
|
|
133
|
+
**两个会把人带偏的坑(实测)**
|
|
134
|
+
|
|
135
|
+
1. **系统代理会把 `127.0.0.1` 也拦走。** 本机代理 `127.0.0.1:26561` 的 `ProxyOverride` 不含 `localhost`/`127.0.0.1`,于是 `Invoke-WebRequest http://127.0.0.1:55999/...` 全部返回 **404 且空体**——看着像「插件没挂」。改用裸 socket 或 `-NoProxy` 后立刻看到 `401`(页面)与本插件的 JSON(路由)。**结论:HTTP 404 不能单独作为「路由未注册」的证据。**
|
|
136
|
+
2. **`--profile <name>` 后面不能再跟 `web`。** `dsh --profile wfmap5 web` 报 `too many arguments. Expected 0 arguments but got 1: web`(`web` 是 profile 名,不是子命令);正确写法是 `dsh --profile wfmap5 --port 55999 --no-open --host 127.0.0.1`。
|
|
137
|
+
|
|
138
|
+
**复跑命令**(升级到 `rc.5` 后任选一个临时目录执行,不动本机环境):
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
$run="$env:TEMP\wfmap-rc5-run"; $env:DSH_HOME="$run\home"
|
|
142
|
+
node "$run\node_modules\@deepseek-ai\dsh\lib\bin.js" --profile wfmap5 --port 55999 --no-open --host 127.0.0.1
|
|
143
|
+
# 另开窗口(-NoProxy 是关键,否则会被系统代理拦成假 404):
|
|
144
|
+
Invoke-WebRequest 'http://127.0.0.1:55999/dsh-workflow/state?sessionId=none' -SkipHttpErrorCheck -NoProxy
|
|
145
|
+
# 期望:404 + {"ok":false,"error":"unknown session"}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 四、闭包级别比对
|
|
151
|
+
|
|
152
|
+
### 4.1 方法
|
|
153
|
+
|
|
154
|
+
以 `@deepseek-ai/dsh-base` 与 `@deepseek-ai/dsh-web-app` 的依赖并集为闭包,共 **206 个包名**。对每个包从 npm registry 取 `dist.unpackedSize` 与 `dist.fileCount`,逐版本比较;任一版本组合不同即进入人工取证。
|
|
155
|
+
|
|
156
|
+
用 registry 元数据做初筛、再用逐文件 SHA-256 取证,是因为元数据能覆盖全部 206 个包,而逐文件下载只对筛出的少数包做。
|
|
157
|
+
|
|
158
|
+
### 4.2 差异分布
|
|
159
|
+
|
|
160
|
+
206 个包中有 **24 个**在 `rc.1`–`rc.5` 之间存在差异,全部集中在三段边界上:
|
|
161
|
+
|
|
162
|
+
| 变化边界 | 包数 | 包 |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `rc.1 → rc.2` | 5 | `dsh-web`、`dsh-web-search-deepseek`、`dsh-web-deepseek-api`、`dsh-client-ui-deepseek-model-label-1017`、`dsh-client-ui-settings-web-search` |
|
|
165
|
+
| `rc.1 → rc.2` 且 `rc.2 → rc.3` | 1 | `dsh-agent-preset` |
|
|
166
|
+
| `rc.2 → rc.3` | 13 | `dsh-llm-deepseek-api-key`、`dsh-api-session-controller`、`dsh-api-remotes`、`dsh-client-ui-entry-customization`、`dsh-client-ui-plugin-manager`、`dsh-client-ui-settings-general`、`dsh-client-ui-agent-preset`、`dsh-client-ui-sidebar-right`、`dsh-client-ui-workspace`、`dsh-client-ui-sidebar`、`dsh-tool-cordis`、`dsh-web-frontend`、`dsh-client-product-analytics` |
|
|
167
|
+
| `rc.2 → rc.3` 且 `rc.3 → rc.4` | 2 | `dsh-llm-deepseek-account`、`dsh-client-ui-conversation` |
|
|
168
|
+
| `rc.3 → rc.4` | 1 | `dsh-mcp-resources` |
|
|
169
|
+
| `rc.4 → rc.5` | 1 | `dsh-client-ui-chat` |
|
|
170
|
+
| 三个边界都有 | 1 | `dsh-cordis-client-runner` |
|
|
171
|
+
|
|
172
|
+
其中 `dsh-client-ui-sidebar` 与 `dsh-client-ui-conversation` 属于本插件的契约面,其锚点级比对见 §5。
|
|
173
|
+
|
|
174
|
+
### 4.3 版本缺席
|
|
175
|
+
|
|
176
|
+
| 包 | 缺席版本 | 说明 |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `dsh-web-deepseek-api` | `rc.1` | 首次发布于 `rc.2` |
|
|
179
|
+
| `dsh-client-ui-deepseek-model-label-1017` | `rc.1` | 首次发布于 `rc.2` |
|
|
180
|
+
| `dsh-client-ui-entry-customization` | `rc.1`、`rc.2` | 首次发布于 `rc.3` |
|
|
181
|
+
| `cordis-plugin-timer`、`schemastery` | 全部 | 第三方依赖,不随 DSH 的 `rc` 编号发布 |
|
|
182
|
+
|
|
183
|
+
`rc.1` 的 profile 因此比 `rc.3` 少 3 个客户端包。这 3 个包都与本插件的契约面无关(模型标签角标、入口定制、API 通道包),缺失不影响本插件加载。
|
|
184
|
+
|
|
185
|
+
### 4.4 `rc.3 → rc.5` 之间真正改动的 5 个包
|
|
186
|
+
|
|
187
|
+
| 包 | `rc.3` | `rc.4` | `rc.5` | 改动文件 | 是否触及本插件契约 |
|
|
188
|
+
|---|---|---|---|---|---|
|
|
189
|
+
| `dsh-llm-deepseek-account` | 35661 B / 10 | 35735 B / 10 | 35735 B / 10 | `lib/index.js`、三个 README、`package.json` | 否:`lib/index.js` 只改 1 行,日志级别 `console.info` → `console.error` |
|
|
190
|
+
| `dsh-mcp-resources` | 30771 B / 11 | 30844 B / 11 | 30844 B / 11 | 三个 README、`package.json` | 否:无代码改动 |
|
|
191
|
+
| `dsh-client-ui-chat` | 857882 B / 106 | 同 `rc.3` | 859933 B / 107 | `lib/client.js`、3 个 `*.d.ts`、新增 `reasoning-text.d.ts`、README、`package.json` | 否:改动集中在推理正文渲染(新增 `conversation.chat.reasoning.content` 槽、`ReasoningBodyOwnerProps.text` 拆成 `rawText` + 显示文本) |
|
|
192
|
+
| `dsh-client-ui-conversation` | 1130507 B / 89 | 1131117 B / 89 | 同 `rc.4` | `lib/client.js`、`locales.d.ts`、README、`package.json` | 否:92 个 bundle 分区中仅 `locales.js` 与 `skeleton/ConversationSession.js` 变化 |
|
|
193
|
+
| `dsh-cordis-client-runner` | 483933 B / 20 | 同 `rc.3` | 484021 B / 20 | `lib/client.js`、`package.json` | 否:12 行变化全部是槽文档文本与 `sources` 源行号(`slots.ts:441` → `:447`) |
|
|
194
|
+
|
|
195
|
+
`dsh-client-ui-conversation` 的改动按 bundle 内 `//#region` 分区逐一比对:`lib/types/client/locales.js`(词条增删)与 `lib/types/client/skeleton/ConversationSession.js`(输入框手势与文件选择绑定重构)。本插件依赖的槽注册、DOM 属性都不在这两个分区内。
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 五、契约面逐项核对
|
|
200
|
+
|
|
201
|
+
本插件只依赖九个契约点。下表每个点都给出提供方、用法与 `rc.1` ↔ `rc.5` 的比对证据。
|
|
202
|
+
|
|
203
|
+
| 契约点 | 提供方 | 本插件用法 | `rc.1` ↔ `rc.5` 证据 | 结论 |
|
|
204
|
+
|---|---|---|---|---|
|
|
205
|
+
| `sidebar.panellist` 槽 | `dsh-client-ui-sidebar` | 注册左栏面板项(`kind: 'list'`) | 槽声明在 `rc.1` 与 `rc.3` 逐字节相同:`kind: 'list'; scope: 'root'; owner: SidebarPanelIconOwnerProps` | 未变 |
|
|
206
|
+
| `main` 槽 | `dsh-client-ui-layout` | 注册主内容面板(`kind: 'keyed'`) | 整包除 `package.json` 外逐文件 SHA-256 相同(`lib/client.js` = `686fb83c91c8ff8d…`) | 未变 |
|
|
207
|
+
| `conversation.view` 槽 | `dsh-client-ui-conversation` | 注册会话标签栏视图 | 4 处锚点窗口(`DefaultConversationViews` 的 `renderSlot`、`apply.js` 的 `entries` / `subscribe` / `children`)在 `rc.1`/`rc.3`/`rc.4`/`rc.5` 逐字节相同 | 未变 |
|
|
208
|
+
| 输入框座位 DOM | `dsh-client-ui-conversation` | 选中本插件视图时隐藏输入框 | `data-composer-seat`(1 处)、`data-conversation-content`(1 处)、`data-conversation-scroll`(3 处)的上下文窗口逐字节相同 | 未变 |
|
|
209
|
+
| `sessionProjections` 服务 | `dsh-session-projection` | 注册会话投影 | `lib/index.js` SHA-256 `7dc2b212819baf4f…` 相同 | 未变 |
|
|
210
|
+
| `sessions` 服务 | `dsh-session` | 按会话号取会话 | `lib/index.js` SHA-256 `2f5dd07cb5c744a2…` 相同 | 未变 |
|
|
211
|
+
| `webServer` 服务 | `dsh-host-webserver` | 注册面板数据路由 | `lib/index.js` SHA-256 `08907285802f0edd…` 相同 | 未变 |
|
|
212
|
+
| `agents` 服务 | `dsh-agent-loop` | 读运行中的 agent 状态 | `lib/index.js` SHA-256 `cf57e3ecd487ac5c…` 相同 | 未变 |
|
|
213
|
+
| 会话事件 / JSONL 格式 | `dsh-session-persistence-jsonl` | 折叠会话事件为步骤序列 | `lib/worker.cjs` SHA-256 `4b0f235e772792d7…` 相同 | 未变 |
|
|
214
|
+
|
|
215
|
+
输入框座位的实测嵌套(`dsh-client-ui-conversation` `rc.5` 的 `lib/client.js`):
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
div[data-conversation-content]
|
|
219
|
+
└─ div[data-conversation-scroll]
|
|
220
|
+
├─ Views ← 选中的 conversation.view 占据位
|
|
221
|
+
└─ div[data-composer-seat]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
本插件依赖的 DOM 只有这一条父子链与两个 `data-*` 属性,不依赖 class 名或内部组件。
|
|
225
|
+
|
|
226
|
+
§4.2 表里其余 22 个变动包与本插件的九个契约点没有交集:不注册 `conversation.chat.*` 系列槽、不读设置页组件、不调用搜索工具、不依赖 API 网关包。
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 六、安装闸门生效性
|
|
231
|
+
|
|
232
|
+
结论:`engines.dsh` 不是装饰性字段,安装与启用期都会被读取。
|
|
233
|
+
|
|
234
|
+
| 环节 | 证据 |
|
|
235
|
+
|---|---|
|
|
236
|
+
| 判定实现 | `dsh-app-boot` 的 `evaluatePluginCompatibility()` → `unmetRequirements()`(§2.1 引文) |
|
|
237
|
+
| 判定实现版本一致 | `dsh-app-boot` `rc.1` 与 `rc.5` 除 `package.json` 外逐文件 SHA-256 相同(`lib/index.js` = `5135179ac810e4f6…`) |
|
|
238
|
+
| 安装期调用 | `dsh-plugin-manager` 三处调用(§2.1 表),未满足抛 `incompatible-version`,并在 `requirements` 里给出 `engines.dsh` 键 |
|
|
239
|
+
| 调用方版本一致 | `dsh-plugin-manager` `rc.3` 与 `rc.5` 除 `package.json` 外逐文件 SHA-256 相同 |
|
|
240
|
+
|
|
241
|
+
由此可推出两条可验证的后果:
|
|
242
|
+
|
|
243
|
+
1. 在 `0.2.1-rc.1`–`0.2.1-rc.5` 任一内核上安装或启用,本插件的 `engines.dsh` 判定通过,不会有 `incompatible-version`。
|
|
244
|
+
2. 在 `0.3.0` 及以后的内核上,`engines.dsh` 判定不通过(§2.3),安装或启用会被上述任一调用点拦下并给出明确的 `engines.dsh: ">=0.2.1-rc.1 <0.3.0-0"` 说明。这属于预期行为:不兼容时应显式失败,而不是勉强加载。
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 七、可复跑命令
|
|
249
|
+
以下命令只需 Node.js 20 以上。前两条只需 npm registry 访问;第三条需要环境里能解析到 `semver`(`npm i semver`,或使用 DSH 安装目录自带的副本)。
|
|
250
|
+
|
|
251
|
+
**1) 闭包元数据对比(206 包 × 5 个版本)**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
node -e '
|
|
255
|
+
const { readFileSync } = require("node:fs");
|
|
256
|
+
const names = readFileSync("closure-names.txt", "utf8").split(/\r?\n/).filter(Boolean);
|
|
257
|
+
const V = ["0.2.1-rc.1","0.2.1-rc.2","0.2.1-rc.3","0.2.1-rc.4","0.2.1-rc.5"];
|
|
258
|
+
let i = 0; const rows = [];
|
|
259
|
+
(async () => {
|
|
260
|
+
await Promise.all(Array.from({ length: 8 }, async () => {
|
|
261
|
+
while (i < names.length) {
|
|
262
|
+
const name = names[i++];
|
|
263
|
+
const p = await (await fetch("https://registry.npmjs.org/" + name.replace("/", "%2f"))).json();
|
|
264
|
+
const pick = (v) => { const d = p.versions?.[v]?.dist; return d ? [d.unpackedSize, d.fileCount] : null; };
|
|
265
|
+
const row = V.map((v) => JSON.stringify(pick(v)));
|
|
266
|
+
if (new Set(row).size > 1) rows.push({ name, row });
|
|
267
|
+
}
|
|
268
|
+
}));
|
|
269
|
+
console.log(rows.length, JSON.stringify(rows, null, 1));
|
|
270
|
+
})();
|
|
271
|
+
'
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`closure-names.txt` 是 `dsh-base` 与 `dsh-web-app` 依赖并集的包名清单(每行一个)。换用别的清单会得到别的闭包,比较结果随之变化。
|
|
275
|
+
|
|
276
|
+
**2) 单包逐文件 SHA-256 比对**
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# 取两个版本的 tarball,解包后逐文件比对
|
|
280
|
+
npm pack @deepseek-ai/dsh-session-projection@0.2.1-rc.1
|
|
281
|
+
npm pack @deepseek-ai/dsh-session-projection@0.2.1-rc.5
|
|
282
|
+
tar -xzf deepseek-ai-dsh-session-projection-0.2.1-rc.1.tgz -C rc1
|
|
283
|
+
tar -xzf deepseek-ai-dsh-session-projection-0.2.1-rc.5.tgz -C rc5
|
|
284
|
+
diff -r rc1/package rc5/package # 期望:仅 package.json 不同
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**3) 区间判定矩阵**
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
node -e '
|
|
291
|
+
const semver = require("semver");
|
|
292
|
+
const ranges = [">=0.2.1-rc.1 <0.3.0-0", ">=0.1.0-rc.1 <0.3.0-0"];
|
|
293
|
+
const versions = ["0.2.1-alpha.5","0.2.1-rc.1","0.2.1-rc.5","0.2.1","0.2.2-rc.1","0.3.0-0","0.3.0"];
|
|
294
|
+
for (const r of ranges) console.log(r, versions.map((v) => [v, semver.satisfies(v, r), semver.satisfies(v, r, { includePrerelease: true })]));
|
|
295
|
+
'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 八、限制声明
|
|
301
|
+
|
|
302
|
+
- **动态核验范围**:截至 2026-10-30,`0.2.1-rc.3` 是本机实机运行版本;`0.2.1-rc.5` 已在隔离 `DSH_HOME` 的临时区实机启动并实测路由应答(§3.1),但不覆盖「真实会话内的投影内容」——`rc.5` 上没有跑过带会话的完整回合,`/state` 只验到「按契约返回 `unknown session`」。`rc.4` 仍只做了静态比对(闭包元数据、逐文件 SHA-256、契约锚点窗口),未实机启动。
|
|
303
|
+
- **比对对象**:只比对 npm 上已发布的产物,不覆盖未发布的仓库提交。
|
|
304
|
+
- **闭包口径**:206 个包名按 `dsh-base` 与 `dsh-web-app` 的依赖并集确定;更换清单会改变闭包边界。
|
|
305
|
+
- **第三方依赖**:闭包内含 `cordis`、`schemastery`、`zod` 等第三方包,它们不随 DSH 的 `rc` 编号发布,本文件不对其版本承诺做任何声明。
|
|
306
|
+
- **上界外版本**:`0.3.0` 及以后未做任何核验,声明上也不覆盖。
|
|
307
|
+
- **判定语义**:§2.3 的矩阵来自 semver 7.8.5;换用其他实现时 `includePrerelease` 的细节可能不同。
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 九、复核记录
|
|
312
|
+
|
|
313
|
+
| 项 | 取值 |
|
|
314
|
+
|---|---|
|
|
315
|
+
| 复核日期 | 2026-10-30 |
|
|
316
|
+
| 数据来源 | `registry.npmjs.org` 发布产物 |
|
|
317
|
+
| semver 实现 | 7.8.5(`satisfies`,分别带与不带 `includePrerelease`) |
|
|
318
|
+
| 闭包规模 | 206 个包名,24 个存在版本间差异 |
|
|
319
|
+
| 逐文件比对包 | `dsh-app-boot`、`dsh-session`、`dsh-session-projection`、`dsh-host-webserver`、`dsh-agent-loop`、`dsh-session-persistence-jsonl`、`dsh-plugin-manager`、`dsh-client-ui-layout`、`dsh-client-ui-sidebar`、`dsh-client-ui-conversation`、`dsh-client-ui-chat`、`dsh-cordis-client-runner`、`dsh-llm-deepseek-account`、`dsh-mcp-resources` |
|
|
320
|
+
| 实机运行内核 | `0.2.1-rc.3`(本机内核);`0.2.1-rc.5`(隔离 `DSH_HOME` 临时区,仅路由与 loader 层) |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workflow-map",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "把当前会话渲染成一张可交互的分步工作流地图:会话标签栏、左栏菜单与主内容区共用同一份面板,逐步显示状态、工具与文件类型明细、进度条,并支持热注入(steer)与「需确认」标注。宿主半折叠会话事件、读取按步执行(PAE)计划,经本地 HTTP 视图供数据;浏览器半渲染面板。",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"deepseek-harness",
|
|
10
|
+
"deepseek-harness-plugin",
|
|
10
11
|
"dsh",
|
|
12
|
+
"dsh-0.2.1",
|
|
11
13
|
"cordis",
|
|
12
14
|
"plugin",
|
|
13
15
|
"bundle",
|
|
@@ -24,7 +26,20 @@
|
|
|
24
26
|
"url": "https://github.com/Joz739/dsh-workflow-map/issues"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
|
-
"node": ">=20"
|
|
29
|
+
"node": ">=20",
|
|
30
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0-0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@deepseek-ai/dsh-agent-loop": ">=0.2.1-rc.1 <0.3.0-0",
|
|
34
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.1 <0.3.0-0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"@deepseek-ai/dsh-agent-loop": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"@deepseek-ai/dsh-llm": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
28
43
|
},
|
|
29
44
|
"exports": {
|
|
30
45
|
".": "./index.js",
|
|
@@ -45,8 +60,10 @@
|
|
|
45
60
|
"CHANGELOG.md",
|
|
46
61
|
"LICENSE",
|
|
47
62
|
"docs/OPEN-SOURCE-GUIDE.md",
|
|
63
|
+
"docs/host-compatibility.md",
|
|
48
64
|
"docs/screenshot.png",
|
|
49
65
|
"docs/screenshot-detail.png",
|
|
66
|
+
"docs/screenshots",
|
|
50
67
|
"package.json"
|
|
51
68
|
],
|
|
52
69
|
"scripts": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* "0 seconds ago" would be a fabricated number, which is exactly what this plugin
|
|
25
25
|
* refuses to print.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* The projected REMAINING half is no longer part of this rail. It used to be shipped
|
|
28
28
|
* by the host as a derived millisecond field (median of the finished steps, hard-bounded) and
|
|
29
29
|
* rendered as "remaining ~N minutes"; the host no longer computes it and this rail no longer renders it.
|
|
30
30
|
* One line, one fact, no consumer-less field — and a payload that still carries that field (an
|
package/src/client/StepColumn.js
CHANGED
|
@@ -155,8 +155,11 @@ function wfHasKey(object, key) {
|
|
|
155
155
|
*/
|
|
156
156
|
function wfDeriveStepStatus(row, plan, todoIndex, currentPlanIndex) {
|
|
157
157
|
var planIndex = row && wfFinite(row.planIndex) !== null ? Math.round(row.planIndex) : null;
|
|
158
|
+
// A finished plan (completed / aborted) is history: its statuses and anchor
|
|
159
|
+
// cursor describe a turn that is over, so the todo table and the host decide.
|
|
160
|
+
var live = plan && plan.terminal !== true;
|
|
158
161
|
|
|
159
|
-
if (
|
|
162
|
+
if (live && planIndex !== null) {
|
|
160
163
|
// 1. PAE's own statuses are the published truth.
|
|
161
164
|
var label = wfHasKey(plan.statuses, String(planIndex)) ? plan.statuses[String(planIndex)] : null;
|
|
162
165
|
var fromPae = wfCanonicalStatus(label);
|
package/src/client/panel.js
CHANGED
|
@@ -34,6 +34,9 @@ var DICT_ZH = {
|
|
|
34
34
|
"panel.planning.note": "计划尚未提交,提交后会显示分步。",
|
|
35
35
|
"panel.collapse": "收起",
|
|
36
36
|
"panel.expand": "展开",
|
|
37
|
+
"panel.refresh": "刷新",
|
|
38
|
+
"panel.refresh.hint": "立即重新读取这一步的状态(自动刷新每 0.8 秒一次,这里是不等它的手动兜底)",
|
|
39
|
+
"panel.refresh.done": "已刷新",
|
|
37
40
|
"panel.settings": "设置",
|
|
38
41
|
"panel.steps": "总任务分步",
|
|
39
42
|
"panel.flow": "分步流程",
|
|
@@ -244,6 +247,9 @@ var DICT_EN = {
|
|
|
244
247
|
"panel.planning.note": "The plan has not been submitted yet; steps appear after it is.",
|
|
245
248
|
"panel.collapse": "Collapse",
|
|
246
249
|
"panel.expand": "Expand",
|
|
250
|
+
"panel.refresh": "Refresh",
|
|
251
|
+
"panel.refresh.hint": "Read this step's state again now (auto-refresh runs every 0.8 s; this is the manual fallback)",
|
|
252
|
+
"panel.refresh.done": "Refreshed",
|
|
247
253
|
"panel.settings": "Settings",
|
|
248
254
|
"panel.steps": "Task steps",
|
|
249
255
|
"panel.flow": "Step flow",
|
|
@@ -523,6 +529,10 @@ function wfUseSnap(sessionId) {
|
|
|
523
529
|
});
|
|
524
530
|
var snap = pair[0];
|
|
525
531
|
var setSnap = pair[1];
|
|
532
|
+
// The running loop's own tick, so the header's refresh button can pull once on
|
|
533
|
+
// demand without waiting for the interval. A ref, not state: swapping the function
|
|
534
|
+
// must not re-render, and a click must never restart the effect.
|
|
535
|
+
var tickRef = React.useRef(null);
|
|
526
536
|
|
|
527
537
|
React.useEffect(
|
|
528
538
|
function () {
|
|
@@ -535,6 +545,9 @@ function wfUseSnap(sessionId) {
|
|
|
535
545
|
var timer = 0;
|
|
536
546
|
var controller = null;
|
|
537
547
|
var etag = "";
|
|
548
|
+
// How many requests are in the air. A replaced request resolves as -1 and must
|
|
549
|
+
// not re-arm while the newer one is still going, or two loops would run.
|
|
550
|
+
var inflight = 0;
|
|
538
551
|
|
|
539
552
|
setSnap({ status: "loading", view: null, rev: null, error: "" });
|
|
540
553
|
|
|
@@ -559,11 +572,20 @@ function wfUseSnap(sessionId) {
|
|
|
559
572
|
}
|
|
560
573
|
controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
561
574
|
var signal = controller ? controller.signal : undefined;
|
|
575
|
+
inflight += 1;
|
|
562
576
|
wfPollOnce(window.fetch, wfPollUrl(sessionId), etag, signal).then(function (result) {
|
|
577
|
+
inflight -= 1;
|
|
563
578
|
if (stopped) return;
|
|
564
|
-
// -1 = this request was aborted
|
|
565
|
-
//
|
|
566
|
-
|
|
579
|
+
// -1 = this request was aborted. A tick aborts the request it replaces, so this
|
|
580
|
+
// is the ORDINARY path on a slow link, not an exception: returning here without
|
|
581
|
+
// re-arming stopped the loop for good the first time a poll outlived its tick —
|
|
582
|
+
// the map then froze while the session kept working, which is the reported
|
|
583
|
+
// "the map does not update by itself". Only the deliberate stop (stopped) ends it.
|
|
584
|
+
if (result.status === -1) {
|
|
585
|
+
// A replaced request: re-arm only when no replacement is still in the air.
|
|
586
|
+
if (wfPollContinues(result, inflight)) schedule(delay);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
567
589
|
if (result.status === 304) {
|
|
568
590
|
setSnap(function (prev) {
|
|
569
591
|
return prev.view ? { status: "live", view: prev.view, rev: prev.rev, error: "" } : prev;
|
|
@@ -596,10 +618,12 @@ function wfUseSnap(sessionId) {
|
|
|
596
618
|
tick();
|
|
597
619
|
}
|
|
598
620
|
|
|
621
|
+
tickRef.current = tick;
|
|
599
622
|
tick();
|
|
600
623
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
601
624
|
return function () {
|
|
602
625
|
stopped = true;
|
|
626
|
+
tickRef.current = null;
|
|
603
627
|
window.clearTimeout(timer);
|
|
604
628
|
if (controller && typeof controller.abort === "function") {
|
|
605
629
|
try {
|
|
@@ -614,7 +638,18 @@ function wfUseSnap(sessionId) {
|
|
|
614
638
|
[sessionId]
|
|
615
639
|
);
|
|
616
640
|
|
|
617
|
-
|
|
641
|
+
// One object, not two returns: every caller already reads the snapshot field by field,
|
|
642
|
+
// so the refresh hook rides along without changing the shape they destructure.
|
|
643
|
+
return {
|
|
644
|
+
status: snap.status,
|
|
645
|
+
view: snap.view,
|
|
646
|
+
rev: snap.rev,
|
|
647
|
+
error: snap.error,
|
|
648
|
+
refresh: function () {
|
|
649
|
+
var pull = tickRef.current;
|
|
650
|
+
if (typeof pull === "function") pull();
|
|
651
|
+
},
|
|
652
|
+
};
|
|
618
653
|
}
|
|
619
654
|
|
|
620
655
|
/** The stylesheet lives exactly as long as the mount. */
|
|
@@ -634,6 +669,25 @@ function wfUseStyle() {
|
|
|
634
669
|
}, []);
|
|
635
670
|
}
|
|
636
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Clear a "just happened" stamp after a moment, so the button returns to its own
|
|
674
|
+
* glyph. One timer, cleared on unmount and on a second click (the stamp changes).
|
|
675
|
+
*/
|
|
676
|
+
function wfUseFlashClear(stamp, clear) {
|
|
677
|
+
React.useEffect(
|
|
678
|
+
function () {
|
|
679
|
+
if (!(stamp > 0)) return undefined;
|
|
680
|
+
var timer = window.setTimeout(function () {
|
|
681
|
+
clear();
|
|
682
|
+
}, 1200);
|
|
683
|
+
return function () {
|
|
684
|
+
window.clearTimeout(timer);
|
|
685
|
+
};
|
|
686
|
+
},
|
|
687
|
+
[stamp]
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
637
691
|
/** Alt+W toggles the collapsed state; the listener is removed on unmount. */
|
|
638
692
|
function wfUseAltW(updatePrefs) {
|
|
639
693
|
React.useEffect(function () {
|
|
@@ -1026,6 +1080,15 @@ function WorkflowMapPanel(props) {
|
|
|
1026
1080
|
var prefs = prefsHandle.prefs;
|
|
1027
1081
|
var updatePrefs = prefsHandle.update;
|
|
1028
1082
|
var snap = wfUseSnap(sessionId);
|
|
1083
|
+
// When the header's refresh was last clicked; drives the one-word acknowledgement
|
|
1084
|
+
// that clears itself. Deliberately NOT the poll's own state: a click must not look
|
|
1085
|
+
// like a data change.
|
|
1086
|
+
var refreshedPair = React.useState(0);
|
|
1087
|
+
var refreshed = refreshedPair[0];
|
|
1088
|
+
var setRefreshed = refreshedPair[1];
|
|
1089
|
+
wfUseFlashClear(refreshed, function () {
|
|
1090
|
+
setRefreshed(0);
|
|
1091
|
+
});
|
|
1029
1092
|
var settingsPair = React.useState(false);
|
|
1030
1093
|
var settingsOpen = settingsPair[0];
|
|
1031
1094
|
var setSettingsOpen = settingsPair[1];
|
|
@@ -1124,6 +1187,13 @@ function WorkflowMapPanel(props) {
|
|
|
1124
1187
|
onToggleSettings: function () {
|
|
1125
1188
|
setSettingsOpen(!settingsOpen);
|
|
1126
1189
|
},
|
|
1190
|
+
// The manual fallback: one pull, now, without waiting for the interval and without
|
|
1191
|
+
// touching the loop. It reads the same endpoint with the same validator, so a click
|
|
1192
|
+
// is a normal poll and never a second data path.
|
|
1193
|
+
onRefresh: function () {
|
|
1194
|
+
setRefreshed(Date.now());
|
|
1195
|
+
if (typeof snap.refresh === "function") snap.refresh();
|
|
1196
|
+
},
|
|
1127
1197
|
// Clicking a main-flow unit: it becomes the SELECTED step (store.js builds its detail) and the
|
|
1128
1198
|
// right page opens on it. It deliberately does not expand anything in place.
|
|
1129
1199
|
// right page opens on it. It deliberately does not expand anything in place.
|
|
@@ -1202,6 +1272,7 @@ function WorkflowMapPanel(props) {
|
|
|
1202
1272
|
|
|
1203
1273
|
return wfRenderPanel(model, prefs, say, actions, {
|
|
1204
1274
|
settingsOpen: settingsOpen,
|
|
1275
|
+
refreshed: refreshed,
|
|
1205
1276
|
status: snap.status,
|
|
1206
1277
|
error: snap.error,
|
|
1207
1278
|
rev: snap.rev,
|
|
@@ -1242,6 +1313,9 @@ function wfRenderPanel(model, prefs, say, actions, ui) {
|
|
|
1242
1313
|
var pendingCount = wfPendingInjectCount(model.injects);
|
|
1243
1314
|
var extras = ui || {};
|
|
1244
1315
|
var fire = actions || {};
|
|
1316
|
+
// The refresh button's one-second acknowledgement, handed in by the component: the
|
|
1317
|
+
// renderer stays pure, so a self-test can drive it with a fixed stamp.
|
|
1318
|
+
var refreshed = wfFinite(extras.refreshed) === null ? 0 : extras.refreshed;
|
|
1245
1319
|
// the two attributes the living layer needs. `motion` comes from the system's
|
|
1246
1320
|
// prefers-reduced-motion preference (pulse.js), `idle` from the model's running flag.
|
|
1247
1321
|
// Both live on .wf-root so the capsule is covered as well as the panel.
|
|
@@ -1341,6 +1415,24 @@ function wfRenderPanel(model, prefs, say, actions, ui) {
|
|
|
1341
1415
|
h(
|
|
1342
1416
|
"div",
|
|
1343
1417
|
{ className: "wf-btns", key: "btns" },
|
|
1418
|
+
// First in the row: it is the escape hatch when the automatic poll is not
|
|
1419
|
+
// delivering, so it must not be the hard one to find.
|
|
1420
|
+
h(
|
|
1421
|
+
"button",
|
|
1422
|
+
{
|
|
1423
|
+
type: "button",
|
|
1424
|
+
key: "refresh",
|
|
1425
|
+
className: refreshed > 0 ? "wf-btn wf-btn-ok" : "wf-btn",
|
|
1426
|
+
"data-dsh-workflow-map": "refresh",
|
|
1427
|
+
"data-refreshed": refreshed > 0 ? "true" : undefined,
|
|
1428
|
+
title: say("panel.refresh.hint"),
|
|
1429
|
+
"aria-label": say("panel.refresh"),
|
|
1430
|
+
onClick: function () {
|
|
1431
|
+
wfFire(fire.onRefresh);
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
refreshed > 0 ? say("panel.refresh.done") : "↻"
|
|
1435
|
+
),
|
|
1344
1436
|
h(
|
|
1345
1437
|
"button",
|
|
1346
1438
|
{
|
|
@@ -1811,6 +1903,7 @@ var __test = {
|
|
|
1811
1903
|
sessionIdFromSnapshot: wfSessionIdFromSnapshot,
|
|
1812
1904
|
pollUrl: wfPollUrl,
|
|
1813
1905
|
pollOnce: wfPollOnce,
|
|
1906
|
+
pollContinues: wfPollContinues,
|
|
1814
1907
|
parsePrefs: wfParsePrefs,
|
|
1815
1908
|
sanitizePrefs: wfSanitizePrefs,
|
|
1816
1909
|
applyPrefsPatch: wfApplyPrefsPatch,
|
package/src/client/pulse.js
CHANGED
|
@@ -53,7 +53,17 @@ function wfMotionMode(matchMediaFn) {
|
|
|
53
53
|
* working session as idle (which would make the curve and the frame look broken).
|
|
54
54
|
*/
|
|
55
55
|
function wfSessionRunning(view, activeNodeId, nowMs) {
|
|
56
|
-
|
|
56
|
+
// The clock is the second witness. view.running is the turn flag, and a turn that never
|
|
57
|
+
// received its turn/end event (interrupt, crash, a compacted session) leaves it true forever.
|
|
58
|
+
// Once the last folded event is older than the idle window the session is not working,
|
|
59
|
+
// whatever the flag says.
|
|
60
|
+
if (view && typeof view.running === "boolean") {
|
|
61
|
+
if (view.running !== true) return false;
|
|
62
|
+
var flagAsOf = wfFinite(view.asOf);
|
|
63
|
+
var flagNow = wfFinite(nowMs);
|
|
64
|
+
if (flagAsOf !== null && flagAsOf > 0 && flagNow !== null && flagNow - flagAsOf >= WF_IDLE_AFTER_MS) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
57
67
|
if (typeof activeNodeId === "string" && activeNodeId !== "") return true;
|
|
58
68
|
var asOf = wfFinite(view && view.asOf);
|
|
59
69
|
if (asOf === null || asOf <= 0) return false;
|