dsh-openwrite 0.2.20 → 0.2.22

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
@@ -29,12 +29,12 @@
29
29
 
30
30
  ## 快速安装
31
31
 
32
- **0.2.20 标准插件包**兼容精确 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.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)。
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.20/dsh-openwrite-0.2.20.tgz
37
+ dsh plugin --profile web add -w https://github.com/LiPu-jpg/Openwrite/releases/download/v0.2.21/dsh-openwrite-0.2.21.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.21` 修复 [#79](https://github.com/LiPu-jpg/Openwrite/issues/79) 的 Windows Desktop 目录选择:宿主接口失败后使用桌面提供的系统文件夹对话框,覆盖侧栏进入与工作台内目录选择。Core 保持 5.8.10。排查与手填路径方式见 [安装指南](docs/INSTALL.md#windows-desktop-目录选择)。
68
+
67
69
  `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 为准。
68
70
 
69
71
  `0.2.19` 候选包修复两个加载/工具问题:部分安装下「OpenWrite 创作」预设从列表消失、DoG 工具不可用(dsh-dog 宿主包解析失败,[#74](https://github.com/LiPu-jpg/Openwrite/issues/74)),以及 `novel_*` 工具的对象/数组参数在模型输出 JSON 字符串时预览失败、`data` 字段丢失([#75](https://github.com/LiPu-jpg/Openwrite/issues/75))。正式可下载版本以 npm `latest` 和 GitHub Release 为准。升级后需停止并重新启动 dsh,再刷新浏览器页面。
@@ -84,7 +86,7 @@ dsh web
84
86
 
85
87
  `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 桌面端恢复链路尚未实机复测。
86
88
 
87
- **`dsh-openwrite@0.2.10` 不包含这项修复,请升级到 `0.2.13` 的 GitHub Release 包。** 不要通过给旧子包授权来代替升级:`dsh-openwrite` 的版本豁免不会覆盖 `@dsh-novel/studio-panel@<ver>` 和 `@dsh-external/dsh-dog@<ver>`,而旧客户端在 0.2.0-rc.2 上激活失败可能触发桌面端的 profile 恢复。
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 恢复。
88
90
 
89
91
  若已发生恢复,先关闭桌面端并备份当前 profile,再从自己的 `cordis.patch.yml.bak-*` 备份恢复 patch,同时恢复原 `package.json` 的 `dsh.profile.bundles` 列表。安装包和作品目录通常仍在;不要用出厂模板覆盖其他插件配置。详细排查见 [安装指南](docs/INSTALL.md#dsh-02-客户端兼容排查)。
90
92
 
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/INSTALL.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  安装已发布的版本;可下载产物、SHA-256 和实际平台结果以 [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/latest) 附件为准。验收方法见 [验收报告](RELEASE_ACCEPTANCE.md)。
4
4
 
5
+ ## Windows Desktop 目录选择
6
+
7
+ 若留空作品目录并点击“选择目录并进入”时出现 `directoryPicker.pick needs the native capability; the composed picker serves "browse"`,这是桌面宿主组合的目录选择能力与其接口要求不一致。OpenWrite 0.2.21 在宿主接口失败后调用桌面公开的 `__DSH_DESKTOP_PICK_DIRECTORY__`,覆盖侧栏进入、作品绑定、新建与恢复目录选择。取消目录对话框不会创建作品或会话;手填绝对路径仍可直接进入。
8
+
9
+ 没有桌面选择通道的宿主会保留原始错误。此时可在同一个输入框填写作品的完整绝对路径,例如 `A:\AI\我的小说`,点击“进入作品”。升级插件后需要关闭并重新打开 DSH Desktop,使客户端重新加载。
10
+
5
11
  ## 标准安装
6
12
 
7
13
  本次锁定 dsh `0.1.2-rc.1`,Node 24 为完整验收基线;Node 22.19 / 26 做兼容检查。安装不自动升级 dsh。先执行 `dsh --version` 核对宿主。
@@ -6,6 +6,29 @@
6
6
 
7
7
  新建作品的验收请求最多等待 60 秒(原为 15 秒),保留成功耗时和失败诊断;不重试写请求,超时仍失败。此调整容纳慢速运行器,不代表初始化耗时问题已被修复。
8
8
 
9
+ ## 0.2.21
10
+
11
+ [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 也一致。
12
+
13
+ 修复 [#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 未变。
14
+
15
+ `npm run check:plugin` 全部通过(298 项组件测试)。新增回归直接对正式 0.2.20 客户端复现同一错误,修复后的客户端通过;从本版主分支 CI 包提取客户端执行相同回归也通过。正式发布构建在打包前执行该回归。回归覆盖宿主优先、两种取消、首次进入、共享入口、缺失/无效桌面能力、桌面对话框失败和手填路径。
16
+
17
+ 本版原生 Windows 安装与浏览器检查运行在 Web profile;Windows Electron 系统文件夹对话框的实机复测未计为通过。目录通道的名称与返回契约已对照桌面上游源码,安装指南补充排查及手填路径方式。升级后需关闭并重新打开 DSH Desktop,重新加载客户端。真实模型调用为 0。
18
+
19
+ ## 0.2.20
20
+
21
+ [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 也一致。
22
+
23
+ 修复 [#76](https://github.com/LiPu-jpg/Openwrite/issues/76) 与 [#65](https://github.com/LiPu-jpg/Openwrite/issues/65),合并来源为引擎 [PR #77](https://github.com/LiPu-jpg/Openwrite/pull/77) 和插件 [PR #78](https://github.com/LiPu-jpg/Openwrite/pull/78)。捆绑 Core 5.8.10,wheel 来源提交 `18a500015f5d5da00f2f4d5ccd2325b084fed24b` 已包含在合并后的 native-core 分支;wheel SHA-256 为 `f121410300c7e7c55d594d0b8d7f4da41f8a17f0dfb5dc3b195e447b618bfd4d`。
24
+
25
+ - 生成后提交失败保留正文与恢复句柄;恢复提交复用已有草稿。写章返回持久化 task_id,忙碌延后、取消请求与终态分离,传输错误可追踪。
26
+ - 统一字数目标,补齐验收恢复、批量与字段级编辑、精确文本/换行保护、完整审稿覆盖与硬检查、正文结构及推理污染检查、审稿历史。
27
+ - persona 和所有技能入口跟随作者语言;运行环境与任务状态提供英文,Studio 错误附带英文说明和错误码。
28
+ - Core 全套 pytest 1397 passed / 32 skipped,专项回归 21 passed。本地 macOS arm64 真实候选包安装、生命周期及浏览器验收通过;正式包以以上主分支 CI 报告为准。真实模型写作未实测,不计为通过。
29
+
30
+ 恢复步骤与明确的范围见 [修复说明](ISSUE_76_REPAIRS.md)。跨章聚合与逾期伏笔硬阻断按 issue 允许的方案声明职责范围,本版未新增这些功能。
31
+
9
32
  ## 0.2.19
10
33
 
11
34
  [GitHub Release](https://github.com/LiPu-jpg/Openwrite/releases/tag/v0.2.19) 已发布,直接使用[完整 CI](https://github.com/LiPu-jpg/Openwrite/actions/runs/38167325137) 验收的同一份包。来源提交 `e0c1ab7`,npm 包 SHA-256 为 `97f89586e4c43071ebca67abd5af1df1ae42b637cf6637d4fc26ca31c133d784`。六组原生平台/Node、源码安装与 alpha 兼容共 8 份运行报告、三代宿主客户端 3 份报告全部通过;Release 四份附件的校验值已与 CI `validated-release` 产物逐一核对。npm `dsh-openwrite@0.2.19` 已发布到官方 registry(registry.npmjs.org),`latest` 已更新到 0.2.19;从官方 registry 无认证重新下载后,SHA-256 与上述 CI 包一致。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-openwrite",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "OpenWrite 长篇小说领域后端接入 DeepSeek Harness:统一创作 Agent 预设 + 原生创作工作台",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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) {