dsh-openwrite 0.2.21 → 0.2.23

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/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <p align="center">在 DeepSeek Harness 中规划、写作、审稿和管理长篇小说。</p>
11
11
 
12
12
  <p align="center">
13
- <a href="package.json"><img src="https://img.shields.io/badge/dsh-0.1.2--rc.1-2563eb" alt="dsh 0.1.2-rc.1"></a>
13
+ <a href="package.json"><img src="https://img.shields.io/badge/dsh-0.2.1--rc.2-2563eb" alt="dsh 0.2.1-rc.2"></a>
14
14
  <a href="package.json"><img src="https://img.shields.io/badge/Node-%E2%89%A522.19-15803d" alt="Node >= 22.19"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-0f766e" alt="Apache-2.0"></a>
16
16
  <a href="https://dsh-plugin.org/plugins/lipu-jpg/openwrite"><img src="https://dsh-plugin.org/badges/listed.svg" alt="Listed on dsh-plugin.org"></a>
@@ -29,12 +29,12 @@
29
29
 
30
30
  ## 快速安装
31
31
 
32
- **0.2.21 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2**。三个版本均已完成 Web 工作台渲染与 DoG 数据读取验证,Windows Electron 桌面端启动/恢复链路尚未实机复测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。默认使用 npm `latest` 渠道;商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
32
+ **0.2.23 标准插件包**兼容精确 dsh 版本 **0.1.2-rc.1 / 0.1.5-alpha.1 / 0.2.0-rc.2 / 0.2.1-rc.2**。四个版本均已完成 Web 工作台渲染与 DoG 数据读取验证,Windows Electron 桌面端启动/恢复链路尚未实机复测。可下载版本、各平台验收结果和 SHA-256 以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准;验收方法见 [发布验收](docs/RELEASE_ACCEPTANCE.md)。默认使用 npm `latest` 渠道;商城所需的精确预览版兼容性、验证范围和安装限制见 [DSH STORE 接入](docs/DSH_STORE.md)。
33
33
 
34
34
  npm 包和对应 GitHub Release 使用同一份验收产物。也可安装精确版本的 Release 包:
35
35
 
36
36
  ```sh
37
- dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.21/dsh-openwrite-0.2.21.tgz
37
+ dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.23/dsh-openwrite-0.2.23.tgz
38
38
  dsh web
39
39
  ```
40
40
 
@@ -64,6 +64,8 @@ dsh web
64
64
 
65
65
  ### dsh 0.2 桌面端升级
66
66
 
67
+ `0.2.23` 修复 [#83](https://github.com/LiPu-jpg/Openwrite/issues/83)/[#84](https://github.com/LiPu-jpg/Openwrite/issues/84)/[#85](https://github.com/LiPu-jpg/Openwrite/issues/85):根包与全部内嵌子包的兼容声明加入精确宿主 `0.2.1-rc.2`,安装和激活不再被宿主版本闸门拒绝,无需版本豁免或手工编辑 `compatibility.json`。已在 `0.2.1-rc.2` 完成 Web 客户端验收(启动、OpenWrite 入口、工作台三页签、DoG 面板与其他插件共存),`0.1.2-rc.1`、`0.2.0-rc.2` 回归通过;Windows Electron 桌面端未实机复测。升级后停止并重新启动 dsh,再刷新浏览器页面。
68
+
67
69
  `0.2.21` 修复 [#79](https://github.com/LiPu-jpg/Openwrite/issues/79) 的 Windows Desktop 目录选择:宿主接口失败后使用桌面提供的系统文件夹对话框,覆盖侧栏进入与工作台内目录选择。Core 保持 5.8.10。排查与手填路径方式见 [安装指南](docs/INSTALL.md#windows-desktop-目录选择)。
68
70
 
69
71
  `0.2.20` 捆绑 Core 5.8.10,修复 [#76](https://github.com/LiPu-jpg/Openwrite/issues/76) 的草稿恢复、任务状态、编辑和审稿契约,并补充 [#65](https://github.com/LiPu-jpg/Openwrite/issues/65) 的英文使用支持。行为与范围见 [修复说明](docs/ISSUE_76_REPAIRS.md)。正式可下载版本以 npm `latest` 和 GitHub Release 为准。
@@ -86,7 +88,7 @@ dsh web
86
88
 
87
89
  `0.2.11` 修复 [#45](https://github.com/LiPu-jpg/Openwrite/issues/45),适配精确宿主版本 `0.2.0-rc.2`:工作台兼容新的 turn-tail list 槽位,DoG 调试面板使用新的 `sessionStatus`;根包和所有子包的兼容声明同步更新。已验证 0.2.0-rc.2 的 Web 客户端启动、OpenWrite 入口、DoG 面板及其他插件共存;Windows Electron 桌面端恢复链路尚未实机复测。
88
90
 
89
- **`dsh-openwrite@0.2.10` 不包含这项修复,请升级到 `0.2.21` 的 GitHub Release 包。** 不要通过给旧子包授权来代替升级:`dsh-openwrite` 的版本豁免不会覆盖 `@dsh-novel/studio-panel@<ver>` 和 `@dsh-external/dsh-dog@<ver>`,而旧客户端在 0.2.0-rc.2 上激活失败可能触发桌面端的 profile 恢复。
91
+ **`dsh-openwrite@0.2.10` 不包含这项修复,请升级到 `0.2.23` 或更高的已验收版本。** 不要通过给旧子包授权来代替升级:`dsh-openwrite` 的版本豁免不会覆盖 `@dsh-novel/studio-panel@<ver>` 和 `@dsh-external/dsh-dog@<ver>`,而旧客户端在 0.2 宿主上激活失败可能触发桌面端的 profile 恢复。
90
92
 
91
93
  若已发生恢复,先关闭桌面端并备份当前 profile,再从自己的 `cordis.patch.yml.bak-*` 备份恢复 patch,同时恢复原 `package.json` 的 `dsh.profile.bundles` 列表。安装包和作品目录通常仍在;不要用出厂模板覆盖其他插件配置。详细排查见 [安装指南](docs/INSTALL.md#dsh-02-客户端兼容排查)。
92
94
 
package/cordis.patch.yml CHANGED
@@ -1,12 +1,3 @@
1
- # 0.2's carrier-neutral Connection does not require webServer itself, but its
2
- # custom RPC channel registrar reads that service from its declaring context.
3
- # OpenWrite targets Web/Desktop, where the carrier is present; declare it here
4
- # so authenticated DoG RPC channels can register without changing transport.
5
- - id: connection
6
- inject:
7
- - webRuntime
8
- - webServer
9
-
10
1
  - insert:
11
2
  - id: openwrite-bundle
12
3
  name: dsh-openwrite
package/docs/DSH_STORE.md CHANGED
@@ -23,7 +23,7 @@ OpenWrite 是一个完整的小说创作插件套件。商城安装目标是仓
23
23
 
24
24
  alpha 浏览器验收进一步发现 persona 配置由 `text` 改成了必填 `prefix`;预设使用 YAML 锚点将同一份身份文本提供给两个字段,并以两版实际 Config / apply 验证身份只注册一次。
25
25
 
26
- 默认宿主仍是 `0.1.2-rc.1`。商城当前滚动窗口还包含 alpha,所以额外验证 `0.1.5-alpha.1`,并保留 `0.1.3-alpha.1`、`0.1.3-alpha.2` 为未知。这不自动升级用户宿主,也不承诺整个 alpha 范围都兼容。
26
+ 构建基线仍是 `0.1.2-rc.1`。兼容声明覆盖精确版本 `0.1.2-rc.1`、`0.1.5-alpha.1`、`0.2.0-rc.2`、`0.2.1-rc.2`,并分别保留已完成的 Web 客户端验收;`0.1.3-alpha.1`、`0.1.3-alpha.2` 保持未知。这不自动升级用户宿主,也不承诺整个 alpha 或 0.2 范围都兼容。`0.2.23` 对 `0.2.1-rc.2` 的本机验证覆盖安装闸门、两个客户端激活、预设选择、工作台入口与三页签、DoG 面板及其他插件共存;完整平台矩阵和发布产物仍以下述 CI 报告为准。
27
27
 
28
28
  2026-09-09,从官方 npm Registry 直接安装 `@deepseek-ai/dsh@0.1.5-alpha.1` 时,上游范围依赖选中了正在发布的 alpha.2,其中 `@deepseek-ai/dsh-tool-pwsh-persistent@^0.1.5-alpha.2` 当时缺失,安装失败(ETARGET)。兼容测试使用 npm 的 `--before=2026-09-09T00:00:00Z` 解析 alpha.1 发布时的依赖,并保留解析锁。没有修改官方包源码或用 replacements/overrides 冒充官方组件。此证据仅适用于报告中的宿主依赖图,不代表后续浮动依赖安装也通过。
29
29
 
package/docs/INSTALL.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  若留空作品目录并点击“选择目录并进入”时出现 `directoryPicker.pick needs the native capability; the composed picker serves "browse"`,这是桌面宿主组合的目录选择能力与其接口要求不一致。OpenWrite 0.2.21 在宿主接口失败后调用桌面公开的 `__DSH_DESKTOP_PICK_DIRECTORY__`,覆盖侧栏进入、作品绑定、新建与恢复目录选择。取消目录对话框不会创建作品或会话;手填绝对路径仍可直接进入。
8
8
 
9
- 没有桌面选择通道的宿主会保留原始错误。此时可在同一个输入框填写作品的完整绝对路径,例如 `A:\AI\我的小说`,点击“进入作品”。升级插件后需要停止并重新启动 dsh,使客户端重新加载。
9
+ 没有桌面选择通道的宿主会保留原始错误。此时可在同一个输入框填写作品的完整绝对路径,例如 `A:\AI\我的小说`,点击“进入作品”。升级插件后需要关闭并重新打开 DSH Desktop,使客户端重新加载。
10
10
 
11
11
  ## 标准安装
12
12
 
@@ -127,6 +127,8 @@ bridge 的高级 `mode: external` / `baseUrl` 配置继续支持外部 Core,
127
127
 
128
128
  `dsh-openwrite@0.2.10` 的旧工作台按 0.1 接口构建,不能通过版本豁免获得 0.2.0-rc.2 的客户端兼容性。根包豁免不覆盖内嵌子包:`@dsh-novel/studio-panel@<ver>`、`@dsh-external/dsh-dog@<ver>` 各自经过宿主版本检查。优先升级到包含 #45 修复的包;修复后的精确 `0.2.0-rc.2` 声明覆盖根包和所有子包,不需要手工编辑 `compatibility.json`。
129
129
 
130
+ `dsh-openwrite@0.2.23` 起,根包和全部内嵌子包的声明同时覆盖精确宿主 `0.2.1-rc.2`,安装和激活不需要版本豁免。宿主判断的是 `package.json` 的 `peerDependencies`(`@deepseek-ai/dsh-*`),不是 `dsh.compatibility` 字段;因此只改 `dsh.compatibility` 字符串并不能让 0.2.22 在 0.2.1-rc.2 上加载。若安装日志出现 `plugin does not support the DSH version in use`,请直接升级到 `0.2.23` 或更高的已验收版本,不要手工改版本号或给旧子包授权。本版已在 `0.2.1-rc.2` 完成 Web 启动、OpenWrite 入口、工作台三页签、DoG 面板与其他插件共存的验证;`0.1.2-rc.1`、`0.2.0-rc.2` 回归通过,Windows Electron 桌面端仍未实机复测。
131
+
130
132
  本修复同时处理 `conversation.chat.turnTail` 从 chain 到 list 的变化,以及 `uiSession.pendingInteractions` 到 `uiSession.sessionStatus` 的变化。客户端初始化或延迟槽位注册失败时,插件回滚对应注册并在浏览器控制台输出 `[OpenWrite/Studio]` / `[OpenWrite/DoG]` 和原始异常,避免让这类插件初始化异常升级为宿主 web boot 失败。此修复不更改桌面端自身的 profile 恢复策略。
131
133
 
132
134
  如果旧版本已经触发桌面端恢复,关闭桌面端后备份整个 profile,找到恢复前的 `cordis.patch.yml.bak-*`,恢复 patch 和原 `package.json` 中的 `dsh.profile.bundles`。这两项需一起核对,才能重新加载其他插件。保留已安装依赖和作品目录,不要用空 profile 替换完整配置。
@@ -135,8 +137,9 @@ bridge 的高级 `mode: external` / `baseUrl` 配置继续支持外部 Core,
135
137
 
136
138
  ```sh
137
139
  npm run test:client-boot
140
+ node scripts/client-boot-smoke.mjs --host-cli=/absolute/path/to/dsh-0.2.1-rc.2/node_modules/@deepseek-ai/dsh/lib/bin.js
138
141
  node scripts/client-boot-smoke.mjs --host-cli=/absolute/path/to/dsh-0.2.0-rc.2/node_modules/@deepseek-ai/dsh/lib/bin.js
139
- node scripts/client-boot-smoke.mjs dsh-openwrite-<version>.tgz --host-cli=/absolute/path/to/dsh-0.2.0-rc.2/node_modules/@deepseek-ai/dsh/lib/bin.js
142
+ node scripts/client-boot-smoke.mjs dsh-openwrite-<version>.tgz --host-cli=/absolute/path/to/dsh-0.2.1-rc.2/node_modules/@deepseek-ai/dsh/lib/bin.js
140
143
  ```
141
144
 
142
145
  脚本需要 Playwright Chromium,创建临时 Web profile,检查无豁免版本闸门、两个客户端加载、真实预设选择、会话导航、三个工作台页签出现和创作视图激活、DoG 面板及其他插件共存,后端就绪与未初始化响应由测试夹具提供,不准备 Python 环境、不调用模型;实际安装与后端动作由独立发布验收覆盖。它验证 Web 客户端兼容性;Windows Electron 的原生启动和恢复流程仍须在对应平台完成发布验收。
@@ -6,6 +6,28 @@
6
6
 
7
7
  新建作品的验收请求最多等待 60 秒(原为 15 秒),保留成功耗时和失败诊断;不重试写请求,超时仍失败。此调整容纳慢速运行器,不代表初始化耗时问题已被修复。
8
8
 
9
+ ## 0.2.23(候选)
10
+
11
+ 修复 [#83](https://github.com/LiPu-jpg/Openwrite/issues/83)/[#84](https://github.com/LiPu-jpg/Openwrite/issues/84)/[#85](https://github.com/LiPu-jpg/Openwrite/issues/85):根包与全部内嵌子包的 `peerDependencies`、`dsh.compatibility.dsh` 和 `dshReleases` 加入精确宿主 `0.2.1-rc.2`。宿主版本闸门读取的是 dsh peer 范围,因此只修改展示用的 `dsh.compatibility` 不能让旧包通过;本版四个包同步声明后,安装与激活均不需要版本豁免。
12
+
13
+ 本机 macOS arm64 / Node v24.15 已使用官方 registry 的精确 `@deepseek-ai/dsh@0.2.1-rc.2` 完成候选包验证:tarball 经 pnpm 安装进隔离 profile,宿主启动不再出现 `incompatible with dsh` / `skipping profile bundle`;浏览器中两个客户端入口激活,OpenWrite 入口和作品启动器可用。`client-boot-smoke` 在 `0.2.1-rc.2` 上通过版本闸门、预设选择、工作台三页签、DoG 面板与其他插件共存检查;`0.1.2-rc.1`、`0.2.0-rc.2` 回归通过。真实模型调用为 0。
14
+
15
+ 完整六平台/Node、源码安装、CI 产物及 npm 发布尚未由本提交的 `Release artifact validation` 运行确认。在这些报告和同字节产物通过前,0.2.23 只作为候选版本,不计为已发布;Windows Electron 桌面端原生启动与恢复仍须对应平台实机验收。
16
+
17
+ ## 0.2.22
18
+
19
+ [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.22) 与 npm `dsh-openwrite@0.2.22` 已发布,npm `latest` 已更新到 0.2.22。使用[主分支完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/39084916927) 验收的原始产物,来源提交 `58209eabbaf0be1f54f562eab4bb5ca12a46bc57`,npm 包 SHA-256 为 `2f20f5e2326707d65119d549b08382dd2e83af481ac60e6ac6f1ee668b446683`。8 份安装报告与 3 份宿主客户端报告全部通过;GitHub 四份附件逐一与 CI 原文件比对一致,官方 npm registry 无认证下载包的字节与 SHA-256 也一致。修复 [#81](https://github.com/LiPu-jpg/Openwrite/issues/81)(bundle patch 覆写核心 connection 行与其他 bundle 冲突导致整机无法启动;DoG 调试通道改由插件自托管于 Web 载体)与 [#82](https://github.com/LiPu-jpg/Openwrite/issues/82)(受限网络下运行时下载必然超时、依赖索引不可换源)。两个 issue 的报告者均已随 issue 关闭获得修复说明。
20
+
21
+ ## 0.2.21
22
+
23
+ [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.21) 与 npm `dsh-openwrite@0.2.21` 已发布,npm `latest` 已更新到 0.2.21。使用[主分支完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38444848871) 验收的原始产物,来源提交 `4e5f88aac8b14c2402c509b2282810cc85840129`,npm 包 SHA-256 为 `b53e3638308185b5890207bc01806641a966620915b99102f5cb35c462b1daee`。8 份安装报告与 3 份宿主客户端报告全部通过;GitHub 四份附件逐一与 CI 原文件比对一致,官方 npm registry 无认证下载包的字节与 SHA-256 也一致。
24
+
25
+ 修复 [#79](https://github.com/LiPu-jpg/Openwrite/issues/79),合并来源 [PR #80](https://github.com/LiPu-jpg/Openwrite/pull/80)。Windows Desktop 宿主目录接口失败后,插件使用桌面公开的系统目录选择通道;侧栏进入和工作台内所有目录选择共用入口。宿主取消不触发第二个对话框,桌面取消不创建作品/会话,手填路径跳过选择器,没有桌面能力时保留原始错误。Core 保持 5.8.10,wheel 与 requirements.lock 未变。
26
+
27
+ `npm run check:plugin` 全部通过(298 项组件测试)。新增回归直接对正式 0.2.20 客户端复现同一错误,修复后的客户端通过;从本版主分支 CI 包提取客户端执行相同回归也通过。正式发布构建在打包前执行该回归。回归覆盖宿主优先、两种取消、首次进入、共享入口、缺失/无效桌面能力、桌面对话框失败和手填路径。
28
+
29
+ 本版原生 Windows 安装与浏览器检查运行在 Web profile;Windows Electron 系统文件夹对话框的实机复测未计为通过。目录通道的名称与返回契约已对照桌面上游源码,安装指南补充排查及手填路径方式。升级后需关闭并重新打开 DSH Desktop,重新加载客户端。真实模型调用为 0。
30
+
9
31
  ## 0.2.20
10
32
 
11
33
  [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.20) 与 npm `dsh-openwrite@0.2.20` 已发布,npm `latest` 已更新到 0.2.20。使用[主分支完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38296670387) 验收的同一份原始产物,来源提交 `4d54f3c619f853a4243cd60e67ef6c8ba8d336af`,npm 包 SHA-256 为 `486c20fbd36909e32cc6cac1fd1e734872abec53da749c0860f8e5e1d4cf6cda`。六组原生平台/Node、源码安装与 alpha 兼容共 8 份运行报告、三代宿主客户端 3 份报告全部通过;GitHub 四份附件逐一与 CI 原文件比对一致,从官方 npm registry 无认证重新下载后,包字节及 SHA-256 也一致。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-openwrite",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "OpenWrite 长篇小说领域后端接入 DeepSeek Harness:统一创作 Agent 预设 + 原生创作工作台",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -54,7 +54,7 @@
54
54
  "displayName": "小说创作(OpenWrite)",
55
55
  "pluginType": "suite",
56
56
  "compatibility": {
57
- "dsh": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
57
+ "dsh": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
58
58
  "profiles": [
59
59
  "web",
60
60
  "desktop"
@@ -64,7 +64,8 @@
64
64
  "0.1.3-alpha.1": "unknown",
65
65
  "0.1.3-alpha.2": "unknown",
66
66
  "0.1.5-alpha.1": "compatible",
67
- "0.2.0-rc.2": "compatible"
67
+ "0.2.0-rc.2": "compatible",
68
+ "0.2.1-rc.2": "compatible"
68
69
  }
69
70
  }
70
71
  },
@@ -113,9 +114,9 @@
113
114
  },
114
115
  "peerDependencies": {
115
116
  "@deepseek-ai/cordis": "4.0.2 || ~4.0.4",
116
- "@deepseek-ai/dsh-home-paths": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
117
- "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
118
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2"
117
+ "@deepseek-ai/dsh-home-paths": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
118
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
119
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2"
119
120
  },
120
121
  "bin": {
121
122
  "openwrite-maintenance": "scripts/maintenance.mjs"
@@ -104,6 +104,23 @@ export declare class ManagedRuntime {
104
104
  private queueRestart;
105
105
  private recover;
106
106
  private download;
107
+ /**
108
+ * Restricted networks throttle single connections to GitHub releases to a
109
+ * few dozen KB/s, so a fixed total timeout can never finish a 23 MB runtime
110
+ * archive (OpenWrite issue #82). Try the origin first, then optional mirror
111
+ * prefixes; every candidate is verified against the manifest checksum, so a
112
+ * mirror cannot substitute content. OPENWRITE_DOWNLOAD_MIRRORS overrides the
113
+ * prefix list (empty disables mirrors); the slow-start window uses
114
+ * OPENWRITE_DOWNLOAD_SLOW_MS / OPENWRITE_DOWNLOAD_SLOW_BYTES.
115
+ */
116
+ private downloadInto;
117
+ /**
118
+ * One download attempt against a single source. A source that stays below
119
+ * the byte threshold through the grace window is abandoned for the next
120
+ * candidate, and a transfer that stops progressing is cut by an idle
121
+ * timeout — there is deliberately no total-duration cap.
122
+ */
123
+ private downloadFrom;
107
124
  private extract;
108
125
  private command;
109
126
  private lock;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { chmod, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { chmod, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { setTimeout as delay } from 'node:timers/promises';
@@ -12,6 +12,8 @@ export const DEFAULT_RESTART_LIMIT = 5;
12
12
  export const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000];
13
13
  /** Budget for the per-request liveness probe of a previously-ready backend. */
14
14
  const HEALTH_PROBE_TIMEOUT_MS = 1_500;
15
+ /** A download with no progress for this long is cut and retried on the next source. */
16
+ const DOWNLOAD_IDLE_TIMEOUT_MS = 30_000;
15
17
  /** The backend stderr log on disk keeps only the newest chunk. */
16
18
  const STDERR_LOG_LIMIT = 64 * 1024;
17
19
  /**
@@ -63,6 +65,13 @@ const ALLOWED_ENV = new Set([
63
65
  'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LANGUAGE', 'TZ',
64
66
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'all_proxy',
65
67
  'SSL_CERT_FILE', 'SSL_CERT_DIR', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE',
68
+ // Package-index overrides let authors on restricted networks point the
69
+ // installer at a mirror (OpenWrite issue #82). uv reads UV_* variables only;
70
+ // PIP_* covers pip itself. UV_NO_CONFIG stays set so stray config files
71
+ // cannot change the locked dependency set — environment variables still
72
+ // apply, which is exactly the supported escape hatch.
73
+ 'UV_DEFAULT_INDEX', 'UV_INDEX_URL', 'UV_INDEX', 'UV_HTTP_TIMEOUT',
74
+ 'PIP_INDEX_URL', 'PIP_EXTRA_INDEX_URL', 'PIP_TRUSTED_HOST', 'PIP_DEFAULT_TIMEOUT',
66
75
  ]);
67
76
  export async function sha256(file) {
68
77
  const hash = createHash('sha256');
@@ -364,19 +373,86 @@ export class ManagedRuntime {
364
373
  const destination = join(cache, item.sha256);
365
374
  if (await sha256(destination).catch(() => '') === item.sha256)
366
375
  return destination;
376
+ // A host killed mid-download leaves .partial fragments that are never
377
+ // resumed (observed in OpenWrite issue #82); clear them before starting.
378
+ for (const entry of await readdir(cache))
379
+ if (entry.endsWith('.partial'))
380
+ await rm(join(cache, entry), { force: true });
367
381
  const partial = destination + '.' + randomUUID() + '.partial';
368
382
  this.update('downloading', 'Downloading and verifying the writing environment');
369
383
  try {
370
- const response = await fetch(item.url, { signal: AbortSignal.any([signal, AbortSignal.timeout(600_000)]) });
384
+ await this.downloadInto(item, partial, signal);
385
+ await rename(partial, destination);
386
+ return destination;
387
+ }
388
+ finally {
389
+ await rm(partial, { force: true });
390
+ }
391
+ }
392
+ /**
393
+ * Restricted networks throttle single connections to GitHub releases to a
394
+ * few dozen KB/s, so a fixed total timeout can never finish a 23 MB runtime
395
+ * archive (OpenWrite issue #82). Try the origin first, then optional mirror
396
+ * prefixes; every candidate is verified against the manifest checksum, so a
397
+ * mirror cannot substitute content. OPENWRITE_DOWNLOAD_MIRRORS overrides the
398
+ * prefix list (empty disables mirrors); the slow-start window uses
399
+ * OPENWRITE_DOWNLOAD_SLOW_MS / OPENWRITE_DOWNLOAD_SLOW_BYTES.
400
+ */
401
+ async downloadInto(item, partial, signal) {
402
+ const grace = Number(process.env['OPENWRITE_DOWNLOAD_SLOW_MS'] ?? 12_000);
403
+ const threshold = Number(process.env['OPENWRITE_DOWNLOAD_SLOW_BYTES'] ?? 524_288);
404
+ const prefixes = (process.env['OPENWRITE_DOWNLOAD_MIRRORS'] ?? 'https://gh-proxy.com/')
405
+ .split(',').map(prefix => prefix.trim()).filter(Boolean);
406
+ const candidates = [item.url, ...prefixes.map(prefix => prefix + item.url)];
407
+ const failures = [];
408
+ for (const candidate of candidates) {
409
+ signal.throwIfAborted();
410
+ try {
411
+ await this.downloadFrom(candidate, partial, signal, grace, threshold);
412
+ if (await sha256(partial) !== item.sha256)
413
+ throw new Error('checksum mismatch');
414
+ return;
415
+ }
416
+ catch (error) {
417
+ signal.throwIfAborted();
418
+ let host = candidate;
419
+ try {
420
+ host = new URL(candidate).host;
421
+ }
422
+ catch { /* keep the raw candidate */ }
423
+ failures.push(`${host}: ${error instanceof Error ? error.message : String(error)}`);
424
+ }
425
+ }
426
+ throw new Error(`Download failed (${failures.join('; ')}); retry`);
427
+ }
428
+ /**
429
+ * One download attempt against a single source. A source that stays below
430
+ * the byte threshold through the grace window is abandoned for the next
431
+ * candidate, and a transfer that stops progressing is cut by an idle
432
+ * timeout — there is deliberately no total-duration cap.
433
+ */
434
+ async downloadFrom(url, partial, signal, grace, threshold) {
435
+ const controller = new AbortController();
436
+ const started = Date.now();
437
+ let received = 0;
438
+ let lastChunkAt = started;
439
+ const watchdog = setInterval(() => {
440
+ if (Date.now() - lastChunkAt > DOWNLOAD_IDLE_TIMEOUT_MS)
441
+ controller.abort(new Error(`stalled (no data for ${DOWNLOAD_IDLE_TIMEOUT_MS}ms)`));
442
+ else if (received < threshold && Date.now() - started > grace)
443
+ controller.abort(new Error(`too slow (${received} of ${threshold} bytes within ${grace}ms)`));
444
+ }, 250);
445
+ try {
446
+ const response = await fetch(url, { signal: AbortSignal.any([signal, controller.signal]) });
371
447
  if (!response.ok || !response.body)
372
- throw new Error(`Download failed: HTTP ${response.status}`);
448
+ throw new Error(`HTTP ${response.status}`);
373
449
  const size = Number(response.headers.get('content-length')) || undefined;
374
- const file = await open(partial, 'wx', 0o600);
375
- let received = 0;
450
+ const file = await open(partial, 'w', 0o600);
376
451
  try {
377
452
  for await (const chunk of response.body) {
378
453
  signal.throwIfAborted();
379
454
  received += chunk.length;
455
+ lastChunkAt = Date.now();
380
456
  if (received > 1024 ** 3)
381
457
  throw new Error('Download exceeds the allowed size');
382
458
  await file.write(chunk);
@@ -386,13 +462,9 @@ export class ManagedRuntime {
386
462
  finally {
387
463
  await file.close();
388
464
  }
389
- if (await sha256(partial) !== item.sha256)
390
- throw new Error('Download checksum failed; retry');
391
- await rename(partial, destination);
392
- return destination;
393
465
  }
394
466
  finally {
395
- await rm(partial, { force: true });
467
+ clearInterval(watchdog);
396
468
  }
397
469
  }
398
470
  async extract(item, signal) {
@@ -28,11 +28,12 @@
28
28
  "patch": "./cordis.patch.yml"
29
29
  },
30
30
  "compatibility": {
31
- "dsh": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
31
+ "dsh": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
32
32
  "dshReleases": {
33
33
  "0.1.2-rc.1": "compatible",
34
34
  "0.1.5-alpha.1": "compatible",
35
- "0.2.0-rc.2": "compatible"
35
+ "0.2.0-rc.2": "compatible",
36
+ "0.2.1-rc.2": "compatible"
36
37
  }
37
38
  }
38
39
  },
@@ -44,9 +45,9 @@
44
45
  },
45
46
  "peerDependencies": {
46
47
  "@deepseek-ai/cordis": "4.0.2 || ~4.0.4",
47
- "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
48
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2",
49
- "@deepseek-ai/dsh-workspace": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2"
48
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
49
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2",
50
+ "@deepseek-ai/dsh-workspace": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2"
50
51
  },
51
52
  "peerDependenciesMeta": {
52
53
  "@deepseek-ai/dsh-host-webserver": {