dsh-openwrite 0.2.27 → 0.2.33

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.
Files changed (130) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -6
  3. package/THIRD_PARTY_NOTICES.md +4 -3
  4. package/companions.mjs +508 -0
  5. package/cordis.patch.yml +0 -2
  6. package/docs/CLOSED_ISSUE_FOLLOWUPS.md +5 -3
  7. package/docs/COMPANION_PLUGINS.md +142 -0
  8. package/docs/CONTEXT_SLOTS.md +60 -0
  9. package/docs/COST_MEASUREMENT.md +2 -2
  10. package/docs/DEVELOPMENT.md +6 -11
  11. package/docs/DSH_STORE.md +14 -2
  12. package/docs/EDITOR_SAFETY_REPAIRS.md +17 -0
  13. package/docs/INSTALL.md +112 -4
  14. package/docs/ISSUES_121_123.md +97 -0
  15. package/docs/ISSUES_140_142.md +58 -0
  16. package/docs/ISSUES_149_150_151.md +77 -0
  17. package/docs/ISSUES_98_100.md +2 -0
  18. package/docs/ISSUE_123_FOLLOWUP.md +55 -0
  19. package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
  20. package/docs/LEARNING.md +1 -1
  21. package/docs/MOBILE_READINESS.md +62 -0
  22. package/docs/PLUGIN_MAINTENANCE.md +1 -3
  23. package/docs/RELEASE_ACCEPTANCE.md +90 -3
  24. package/docs/REVIEW_ROUND_22.md +67 -0
  25. package/docs/REVIEW_ROUND_23.md +48 -0
  26. package/docs/REVIEW_ROUND_25.md +51 -0
  27. package/docs/SELECTION_UPGRADE_REPAIRS.md +16 -0
  28. package/docs/TOOLS.md +933 -0
  29. package/docs/WORKFLOWS.md +5 -5
  30. package/package.json +18 -21
  31. package/packages/openwrite-bridge/lib/client.d.ts +0 -1
  32. package/packages/openwrite-bridge/lib/client.js +90 -9
  33. package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
  34. package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
  35. package/packages/openwrite-bridge/lib/dog-delivery.js +10 -4
  36. package/packages/openwrite-bridge/lib/dog-review.js +15 -3
  37. package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
  38. package/packages/openwrite-bridge/lib/domain.js +250 -29
  39. package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
  40. package/packages/openwrite-bridge/lib/host-llm.js +657 -0
  41. package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
  42. package/packages/openwrite-bridge/lib/host-web.js +166 -0
  43. package/packages/openwrite-bridge/lib/index.d.ts +4 -0
  44. package/packages/openwrite-bridge/lib/index.js +150 -6
  45. package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
  46. package/packages/openwrite-bridge/lib/managed-runtime.js +179 -35
  47. package/packages/openwrite-bridge/lib/tool-policy.js +39 -12
  48. package/packages/openwrite-bridge/lib/tools.js +1238 -1219
  49. package/packages/openwrite-bridge/package.json +7 -5
  50. package/packages/studio-panel/lib/client.js +116667 -114773
  51. package/packages/studio-panel/lib/client.js.map +1 -1
  52. package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
  53. package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
  54. package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
  55. package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
  56. package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
  57. package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
  58. package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
  59. package/packages/studio-panel/lib/types/client/VditorBody.d.ts +8 -2
  60. package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
  61. package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
  62. package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
  63. package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
  64. package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
  65. package/packages/studio-panel/lib/types/client/manuscript-annotations.d.ts +2 -1
  66. package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +2 -1
  67. package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +18 -5
  68. package/packages/studio-panel/lib/types/client/selection-coordinates.d.ts +15 -0
  69. package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
  70. package/packages/studio-panel/package.json +5 -3
  71. package/plugin.mjs +159 -20
  72. package/preset-definition.mjs +144 -4
  73. package/presets/openwrite/agent.cordis.yml +7 -9
  74. package/presets/openwrite/ops-integration.mjs +28 -0
  75. package/presets/openwrite/preset.yml +2 -1
  76. package/presets/openwrite/skills/canon/SKILL.md +3 -3
  77. package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
  78. package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
  79. package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
  80. package/presets/openwrite/skills/export-book/SKILL.md +3 -3
  81. package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
  82. package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
  83. package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
  84. package/presets/openwrite/skills/learn/SKILL.md +3 -3
  85. package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
  86. package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
  87. package/presets/openwrite/skills/progress/SKILL.md +1 -1
  88. package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
  89. package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
  90. package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
  91. package/presets/openwrite/skills/style-system/SKILL.md +13 -6
  92. package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
  93. package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
  94. package/presets/openwrite/skills/world-query/SKILL.md +16 -11
  95. package/presets/openwrite/skills/write-next/SKILL.md +1 -1
  96. package/release/{openwrite-5.8.16-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
  97. package/release/runtime-manifest.json +4 -12
  98. package/release/source.json +21 -21
  99. package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
  100. package/scripts/maintenance.mjs +67 -7
  101. package/scripts/plugin-doctor.mjs +179 -0
  102. package/scripts/prepare.mjs +11 -2
  103. package/scripts/safe-upgrade.mjs +141 -13
  104. package/dog-tools.mjs +0 -11
  105. package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
  106. package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
  107. package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
  108. package/scripts/dog/delivery-stage.js +0 -32
  109. package/scripts/dog/import-chapter.js +0 -20
  110. package/scripts/dog/import-record.js +0 -35
  111. package/scripts/dog/review-dimension.js +0 -41
  112. package/scripts/dog/review-record.js +0 -42
  113. package/vendor/dsh-dog/LICENSE +0 -29
  114. package/vendor/dsh-dog/README.md +0 -114
  115. package/vendor/dsh-dog/UPSTREAM.md +0 -30
  116. package/vendor/dsh-dog/cordis.patch.yml +0 -14
  117. package/vendor/dsh-dog/lib/client.js +0 -2960
  118. package/vendor/dsh-dog/lib/client.js.map +0 -1
  119. package/vendor/dsh-dog/lib/core-BAlgZpLq.js +0 -2074
  120. package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
  121. package/vendor/dsh-dog/lib/core.d.ts +0 -2
  122. package/vendor/dsh-dog/lib/core.js +0 -2
  123. package/vendor/dsh-dog/lib/index.d.ts +0 -143
  124. package/vendor/dsh-dog/lib/index.js +0 -1279
  125. package/vendor/dsh-dog/package.json +0 -110
  126. package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
  127. package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
  128. package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
  129. package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
  130. package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
package/docs/WORKFLOWS.md CHANGED
@@ -23,20 +23,20 @@ dsh Workspace 是唯一的工作区身份:agent 工具调用按会话的不可
23
23
 
24
24
  1. 在 **OpenWrite 创作** 会话里先收敛灵感、人物、世界观与大纲(所有写入经 OpenWrite
25
25
  修订门控,重要改动先出 diff 再确认),再在同一会话中写正文。
26
- 正文和正典使用 `novel_document_change_plan`;大纲、资产、创作重点、伏笔和写作目标
27
- 使用 `novel_structured_change_plan`。两者都由服务端保存不可变预览、在确认时重验源
26
+ 正文和正典使用 `novel_change_plan(action="document")`;大纲、资产、创作重点、伏笔和写作目标
27
+ 使用 `novel_change_plan(action="structured")`。两者都由服务端保存不可变预览、在确认时重验源
28
28
  revision,并返回只对已确认结果有效的安全撤销 token。
29
29
  正文区按 canonical reading order 切章和连续审读,计划未写章节保留在大纲中但不会被
30
30
  当作可读正文;搜索结果只有在 document ID 与完整 revision 都仍匹配时才开放改单行。
31
31
  2. 写正文时执行 `novel_context_preview` 预检上下文包 →
32
32
  `novel_write_chapter` 写章 → `novel_review_chapter` 六域累加评审。
33
33
  每次评审同时生成 `data/novels/{id}/data/dog/reviews/{chapter}/dog-graph.json`;
34
- 通过 `novel_task_create(type=chapter_review)` 启动的后台评审,也会在
35
- `novel_task_get` 读到完成态时生成同样的图;
34
+ 通过 `novel_task(action="create", type=chapter_review)` 启动的后台评审,也会在
35
+ `novel_task(action="get")` 读到完成态时生成同样的图;
36
36
  图的顶层是上下文完整性、六个质量域、硬门禁和确定性聚合,六域下可展开原 37 项,
37
37
  查询 criterion、证据、问题、覆盖率和继承状态。DoG 只查询已生成的 artifact,
38
38
  不重新调用模型评审。
39
- 写章、评审和 `novel_revision_*` 操作还会维护
39
+ 写章、评审和 `novel_revision` 操作还会维护
40
40
  `data/novels/{id}/data/dog/deliveries/{chapter}/dog-graph.json`:它把正文、
41
41
  `writing → review → revision → application → rereview → closure` 六阶段串成章节交付总图;
42
42
  正文 SHA 改变后旧评审立即 stale,修订应用后必须复评通过才算交付。
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "dsh-openwrite",
3
- "version": "0.2.27",
3
+ "version": "0.2.33",
4
4
  "description": "OpenWrite 长篇小说领域后端接入 DeepSeek Harness:统一创作 Agent 预设 + 原生创作工作台",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "npm --prefix packages/openwrite-bridge run build && npm --prefix packages/studio-panel run build && npm --prefix vendor/dsh-dog run build",
7
+ "build": "npm --prefix packages/openwrite-bridge run build && npm --prefix packages/studio-panel run build",
8
8
  "test:dog": "npm --prefix packages/openwrite-bridge run build && .venv/bin/python conductor/test_dog_artifacts.py",
9
9
  "test:epochs": "npm --prefix packages/studio-panel run test:epochs",
10
10
  "test:components": "npm --prefix packages/studio-panel run test:components",
@@ -14,14 +14,15 @@
14
14
  "install-dsh": "bash scripts/install.sh",
15
15
  "dev": "bash scripts/dev.sh",
16
16
  "doctor": "node scripts/plugin-doctor.mjs",
17
- "test:maintenance": "node --test scripts/plugin-doctor.test.mjs scripts/lifecycle.test.mjs packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs scripts/maintenance.test.mjs scripts/preset-lifecycle.test.mjs scripts/safe-upgrade.test.mjs",
17
+ "test:maintenance": "node --test scripts/plugin-doctor.test.mjs scripts/lifecycle.test.mjs packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs scripts/maintenance.test.mjs scripts/preset-lifecycle.test.mjs scripts/companions.test.mjs scripts/host-model.test.mjs scripts/safe-upgrade.test.mjs",
18
18
  "check": "npm run check:plugin && npm run test:contracts",
19
19
  "test:lifecycle": "node --test scripts/lifecycle.test.mjs",
20
20
  "test:runtime": "node --test packages/openwrite-bridge/scripts/runtime-lifecycle.test.mjs",
21
- "check:plugin": "npm run build && npm run doctor && npm run test:maintenance && npm run test:profiles && npm run test:preset && npm --prefix packages/openwrite-bridge run smoke && npm --prefix packages/studio-panel run smoke && npm run test:epochs && npm run test:components",
21
+ "check:plugin": "npm run build && npm run docs:check && npm run doctor && npm run test:maintenance && npm run test:profiles && npm run test:preset && npm --prefix packages/openwrite-bridge run smoke && npm --prefix packages/studio-panel run smoke && npm run test:epochs && npm run test:components",
22
+ "docs:check": "node scripts/gen-tools-md.mjs --check && node scripts/check-editor-coordinates.mjs && node scripts/check-tool-references.mjs",
22
23
  "test:profiles": "node scripts/profile-smoke.mjs",
23
24
  "prepare": "node scripts/prepare.mjs",
24
- "test:managed": "node --test packages/openwrite-bridge/scripts/managed-runtime.test.mjs packages/openwrite-bridge/scripts/tool-policy.test.mjs",
25
+ "test:managed": "node --test packages/openwrite-bridge/scripts/host-web.test.mjs packages/openwrite-bridge/scripts/host-llm.test.mjs packages/openwrite-bridge/scripts/managed-runtime.test.mjs packages/openwrite-bridge/scripts/system-python.test.mjs packages/openwrite-bridge/scripts/tool-policy.test.mjs packages/openwrite-bridge/scripts/followup-tools.test.mjs packages/openwrite-bridge/scripts/proxy-allowlist.test.mjs",
25
26
  "test:release": "node scripts/release-smoke.mjs",
26
27
  "test:client-boot": "node scripts/client-boot-smoke.mjs"
27
28
  },
@@ -43,9 +44,8 @@
43
44
  "./bridge": "./packages/openwrite-bridge/lib/index.js",
44
45
  "./tools": "./packages/openwrite-bridge/lib/preset-tools.js",
45
46
  "./panel": "./packages/studio-panel/lib/index.js",
46
- "./dog": "./vendor/dsh-dog/lib/index.js",
47
47
  "./package.json": "./package.json",
48
- "./dog-tools": "./dog-tools.mjs"
48
+ "./ops-integration": "./presets/openwrite/ops-integration.mjs"
49
49
  },
50
50
  "dsh": {
51
51
  "bundle": {
@@ -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 || 0.2.1-rc.2 || 0.2.1-rc.3",
57
+ "dsh": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5",
58
58
  "profiles": [
59
59
  "web",
60
60
  "desktop"
@@ -66,21 +66,19 @@
66
66
  "0.1.5-alpha.1": "compatible",
67
67
  "0.2.0-rc.2": "compatible",
68
68
  "0.2.1-rc.2": "compatible",
69
- "0.2.1-rc.3": "compatible"
69
+ "0.2.1-rc.3": "compatible",
70
+ "0.2.1-rc.4": "compatible",
71
+ "0.2.1-rc.5": "compatible"
70
72
  }
71
73
  }
72
74
  },
73
75
  "files": [
74
76
  "plugin.mjs",
75
77
  "preset-definition.mjs",
78
+ "companions.mjs",
76
79
  "plugin.d.ts",
77
80
  "cordis.patch.yml",
78
81
  "packages/studio-panel/vendor",
79
- "vendor/dsh-dog/package.json",
80
- "vendor/dsh-dog/lib",
81
- "vendor/dsh-dog/schemas",
82
- "vendor/dsh-dog/LICENSE",
83
- "vendor/dsh-dog/UPSTREAM.md",
84
82
  "presets/openwrite",
85
83
  "release/*.json",
86
84
  "release/*.lock",
@@ -89,13 +87,12 @@
89
87
  "README.md",
90
88
  "LICENSE",
91
89
  "THIRD_PARTY_NOTICES.md",
92
- "dog-tools.mjs",
93
90
  "packages/openwrite-bridge/lib",
94
91
  "packages/openwrite-bridge/package.json",
95
92
  "packages/studio-panel/lib",
96
93
  "packages/studio-panel/package.json",
97
- "scripts/dog",
98
94
  "scripts/maintenance.mjs",
95
+ "scripts/plugin-doctor.mjs",
99
96
  "scripts/safe-upgrade.mjs",
100
97
  "packages/openwrite-bridge/LICENSE",
101
98
  "packages/studio-panel/LICENSE",
@@ -110,15 +107,15 @@
110
107
  "ajv": "^8.20.0",
111
108
  "ajv-formats": "^3.0.1",
112
109
  "fflate": "0.8.2",
110
+ "js-yaml": "4.3.1",
113
111
  "proper-lockfile": "4.1.2",
114
- "tar": "7.5.7",
115
- "js-yaml": "4.3.1"
112
+ "tar": "7.5.7"
116
113
  },
117
114
  "peerDependencies": {
118
115
  "@deepseek-ai/cordis": "4.0.2 || ~4.0.4",
119
- "@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 || 0.2.1-rc.3",
120
- "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3",
121
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3"
116
+ "@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 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5",
117
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5",
118
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5"
122
119
  },
123
120
  "bin": {
124
121
  "openwrite-maintenance": "scripts/maintenance.mjs",
@@ -36,7 +36,6 @@ export interface WorkspaceContext {
36
36
  rootCallId?: string;
37
37
  toolName?: string;
38
38
  }
39
- /** A normalized Studio failure: HTTP status plus the server's machine-readable code. */
40
39
  export declare class StudioError extends Error {
41
40
  readonly status: number;
42
41
  readonly code: string;
@@ -21,6 +21,8 @@ export function workspaceRootHeaders(root, lowercase = false) {
21
21
  : { [name]: root };
22
22
  }
23
23
  /** A normalized Studio failure: HTTP status plus the server's machine-readable code. */
24
+ /** Hard ceiling for one download (export / project archive). */
25
+ const MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
24
26
  export class StudioError extends Error {
25
27
  status;
26
28
  code;
@@ -176,7 +178,47 @@ export class StudioClient {
176
178
  // Error downloads are still JSON payloads.
177
179
  await this.readJson(response);
178
180
  }
179
- const content = new Uint8Array(await response.arrayBuffer());
181
+ // Bound the buffer: an export or project archive is read fully into memory,
182
+ // so an unbounded body is an OOM vector against the host process. Reject on
183
+ // a declared oversize, and on a stream that exceeds the cap mid-read.
184
+ const declared = Number(response.headers.get('content-length') ?? '');
185
+ if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
186
+ await response.body?.cancel();
187
+ throw new StudioError(`Studio download is too large (${declared} bytes)`, 502, 'DOWNLOAD_TOO_LARGE', { limit: MAX_DOWNLOAD_BYTES });
188
+ }
189
+ const reader = response.body?.getReader();
190
+ if (reader === undefined) {
191
+ const content = new Uint8Array(await response.arrayBuffer());
192
+ if (content.byteLength > MAX_DOWNLOAD_BYTES) {
193
+ throw new StudioError(`Studio download is too large (${content.byteLength} bytes)`, 502, 'DOWNLOAD_TOO_LARGE', { limit: MAX_DOWNLOAD_BYTES });
194
+ }
195
+ return {
196
+ filename: parseDispositionFilename(response.headers.get('content-disposition')) ?? 'export.bin',
197
+ contentType: response.headers.get('content-type') ?? 'application/octet-stream',
198
+ content,
199
+ };
200
+ }
201
+ const chunks = [];
202
+ let total = 0;
203
+ for (;;) {
204
+ const { done, value } = await reader.read();
205
+ if (done !== undefined && done)
206
+ break;
207
+ if (value === undefined)
208
+ continue;
209
+ total += value.byteLength;
210
+ if (total > MAX_DOWNLOAD_BYTES) {
211
+ await reader.cancel().catch(() => { });
212
+ throw new StudioError(`Studio download exceeded ${MAX_DOWNLOAD_BYTES} bytes`, 502, 'DOWNLOAD_TOO_LARGE', { limit: MAX_DOWNLOAD_BYTES });
213
+ }
214
+ chunks.push(value);
215
+ }
216
+ const content = new Uint8Array(total);
217
+ let offset = 0;
218
+ for (const chunk of chunks) {
219
+ content.set(chunk, offset);
220
+ offset += chunk.byteLength;
221
+ }
180
222
  return {
181
223
  filename: parseDispositionFilename(response.headers.get('content-disposition')) ?? 'export.bin',
182
224
  contentType: response.headers.get('content-type') ?? 'application/octet-stream',
@@ -185,17 +227,32 @@ export class StudioClient {
185
227
  }
186
228
  /** One fetch honoring both the caller's abort signal and the configured timeout. */
187
229
  async request(url, init, signal) {
230
+ // Arm the deadline only once the connection is known. The runtime bootstrap
231
+ // (`connect()`) is deliberately uncapped — a 23 MB archive over a slow link
232
+ // needs minutes — so a timeout started before it turns every cold-start
233
+ // first call into a spurious network error carrying a misleading "the
234
+ // server may have accepted the request" hint.
235
+ const connection = await this.connect?.();
188
236
  const signals = [AbortSignal.timeout(this.timeoutMs)];
189
237
  if (signal !== undefined)
190
238
  signals.push(signal);
191
239
  try {
192
- const connection = await this.connect?.();
193
240
  const target = connection ? new URL(url.pathname + url.search, connection.baseUrl) : url;
194
241
  const headers = connection?.token ? { ...init.headers, Authorization: `Bearer ${connection.token}` } : init.headers;
195
242
  const response = await fetch(target, { ...init, headers, signal: AbortSignal.any(signals) });
196
243
  const actual = response.headers.get('X-OpenWrite-Project-Path');
197
244
  if (response.ok && actual && this.context?.workspaceRoot) {
198
- const expected = realpathSync(this.context.workspaceRoot);
245
+ let expected;
246
+ try {
247
+ expected = realpathSync(this.context.workspaceRoot);
248
+ }
249
+ catch {
250
+ // The root was deleted or moved mid-session. Letting ENOENT reach the
251
+ // transport handler would report it as NETWORK_ERROR and write a
252
+ // diagnostic blaming the network.
253
+ await response.body?.cancel();
254
+ throw new StudioError('The requested workspace root no longer exists', 400, 'WORKSPACE_ROOT_INVALID', { requested_path: this.context.workspaceRoot });
255
+ }
199
256
  const received = decodeURIComponent(actual);
200
257
  if (!workspaceRootsEqual(received, expected)) {
201
258
  await response.body?.cancel();
@@ -210,23 +267,31 @@ export class StudioClient {
210
267
  if (error instanceof UnsupportedRuntimePlatformError) {
211
268
  throw new StudioError(error.message, error.status, error.code, { platform: error.platform });
212
269
  }
270
+ // The caller's own abort is a cancellation, not a failure; it must not be
271
+ // rewritten into a StudioError that looks like a transport problem, and it
272
+ // must not race the client's own timeout for the same outcome.
273
+ if (signal?.aborted)
274
+ throw error;
275
+ // Distinguish the deadline we imposed from "the backend is unreachable".
276
+ // Both are ambiguous about whether the server applied a write, but only
277
+ // the timeout carries that meaning, and collapsing them makes an agent
278
+ // retry a write that may already have landed.
279
+ const timedOut = error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError');
213
280
  if (this.context?.workspaceRoot) {
214
281
  try {
215
282
  const root = join(this.context.workspaceRoot, '.openwrite', 'transport-errors');
216
283
  await mkdir(root, { recursive: true });
217
284
  await writeFile(join(root, `${randomUUID()}.json`), JSON.stringify({
218
- source: 'transport', code: signal?.aborted ? 'REQUEST_ABORTED' : 'NETWORK_ERROR',
285
+ source: 'transport', code: timedOut ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR',
219
286
  created_at: new Date().toISOString(), path: url.pathname,
220
287
  tool_name: this.context.toolName ?? '', tool_call_id: this.context.toolCallId ?? '',
221
288
  }));
222
289
  }
223
290
  catch { /* A diagnostic failure must not hide the transport failure. */ }
224
291
  }
225
- if (signal?.aborted)
226
- throw error;
227
292
  const cause = error instanceof Error ? error.cause : undefined;
228
293
  const detail = cause instanceof Error ? ` (${cause.message})` : cause === undefined ? '' : ` (${String(cause)})`;
229
- throw new StudioError(`Studio request failed: ${error instanceof Error ? error.message : String(error)}${detail}`, 0, 'NETWORK_ERROR');
294
+ throw new StudioError(`Studio request failed: ${error instanceof Error ? error.message : String(error)}${detail}`, 0, timedOut ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR');
230
295
  }
231
296
  }
232
297
  /**
@@ -239,7 +304,14 @@ export class StudioClient {
239
304
  try {
240
305
  parsed = await response.json();
241
306
  }
242
- catch {
307
+ catch (error) {
308
+ // The abort signal applies to the body stream too, so a mid-body abort or
309
+ // a truncated body would otherwise surface as INVALID_RESPONSE with the
310
+ // original success status — an error code on a 2xx, which defeats any
311
+ // caller that keys off `status`.
312
+ if (error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
313
+ throw new StudioError(`Studio response body was not fully received: ${error.message}`, 0, 'REQUEST_TIMEOUT');
314
+ }
243
315
  throw new StudioError(`Studio returned HTTP ${response.status} with a non-JSON body`, response.status, 'INVALID_RESPONSE');
244
316
  }
245
317
  const body = asJson(parsed);
@@ -248,7 +320,16 @@ export class StudioClient {
248
320
  if (error !== undefined && error !== null && error !== false && error !== '') {
249
321
  const nested = typeof error === 'object' && !Array.isArray(error) ? error : {};
250
322
  const code = typeof body['code'] === 'string' ? body['code'] : typeof nested['code'] === 'string' ? nested['code'] : 'STUDIO_ERROR';
251
- const message = typeof body['message'] === 'string' ? body['message'] : typeof error === 'string' ? error : typeof nested['message'] === 'string' ? nested['message'] : JSON.stringify(error);
323
+ // Core sends the generic "Request failed (CODE, HTTP n)" envelope in
324
+ // `message` and the actionable reason in `error`. Prefer the message
325
+ // only when it carries real detail; otherwise surface `error`.
326
+ const envelope = typeof body['message'] === 'string' ? body['message'].trim() : '';
327
+ const genericEnvelope = envelope !== '' && /^Request failed \([^)]*\)$/.test(envelope);
328
+ const errorText = typeof error === 'string' && error !== '' ? error : undefined;
329
+ const nestedMessage = typeof nested['message'] === 'string' && nested['message'] !== '' ? nested['message'] : undefined;
330
+ const message = envelope !== '' && !genericEnvelope
331
+ ? envelope
332
+ : errorText ?? nestedMessage ?? (envelope !== '' ? envelope : JSON.stringify(error));
252
333
  const details = body['details'] ?? nested['details'] ?? null;
253
334
  const failure = new StudioError(message, response.status, code, details);
254
335
  failure.requestId = typeof body['request_id'] === 'string' ? body['request_id'] : null;
@@ -729,7 +729,8 @@ const SCHEMAS = {
729
729
  "auth_source": {
730
730
  "enum": [
731
731
  "api_key",
732
- "opencode_console"
732
+ "opencode_console",
733
+ "host_relay"
733
734
  ]
734
735
  },
735
736
  "capabilities": {
@@ -1,4 +1,5 @@
1
1
  import { type JsonValue } from './client.js';
2
+ export declare function atomicWrite(path: string, value: unknown): Promise<void>;
2
3
  /**
3
4
  * Anti-drift guard (docs/WORKSPACE_CONTEXT_CONTRACT.md §7): the workspace
4
5
  * payload's project root must canonicalize to the current tool context root.
@@ -43,7 +43,7 @@ async function readJson(path) {
43
43
  }
44
44
  return value;
45
45
  }
46
- async function atomicWrite(path, value) {
46
+ export async function atomicWrite(path, value) {
47
47
  await mkdir(dirname(path), { recursive: true });
48
48
  const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}`);
49
49
  try {
@@ -216,7 +216,7 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
216
216
  });
217
217
  let reviewStage;
218
218
  if (Object.keys(review).length === 0) {
219
- reviewStage = stage(chapterId, 'review', 'inconclusive', 'missing', {});
219
+ reviewStage = stage(chapterId, 'review', 'inconclusive', 'not_reviewed', {});
220
220
  }
221
221
  else if (!reviewCurrent) {
222
222
  reviewStage = stage(chapterId, 'review', 'inconclusive', reviewStale ? 'stale' : 'unverifiable', {
@@ -225,9 +225,13 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
225
225
  }
226
226
  else {
227
227
  const v2 = record(review['review_v2']);
228
- reviewStage = stage(chapterId, 'review', reviewPassed ? 'pass' : ['inconclusive', 'stale'].includes(currentDeliveryStatus) ? 'inconclusive' : 'fail', 'current', {
228
+ const diagnostics = record(v2['delivery_diagnostics']);
229
+ const reviewState = !reviewPassed && currentDeliveryStatus === 'inconclusive' ? String(diagnostics['code'] ?? v2['delivery_reason'] ?? 'review_inconclusive') : 'current';
230
+ reviewStage = stage(chapterId, 'review', reviewPassed ? 'pass' : ['inconclusive', 'stale'].includes(currentDeliveryStatus) ? 'inconclusive' : 'fail', reviewState, {
229
231
  score: review['score'] ?? null, threshold: effectiveThreshold, passedGate: reviewPassed,
230
- qualityScore: v2['quality_score'] ?? review['score'] ?? null, coverage: v2['coverage'] ?? 1,
232
+ qualityScore: hasV2 ? v2['quality_score'] ?? null : review['score'] ?? null, coverage: v2['coverage'] ?? 1,
233
+ scoreValid: v2['score_valid'] ?? (hasV2 ? v2['quality_score'] != null : review['score'] != null),
234
+ scopeStatus: v2['scope_status'] ?? 'full', deliveryDiagnostics: diagnostics,
231
235
  gateStatus: v2['gate_status'] ?? (hardIssueIds.length > 0 ? 'blocked' : 'pass'),
232
236
  deliveryStatus: currentDeliveryStatus,
233
237
  issueIds, hardIssueIds, sourceRevision: reviewSourceRevision,
@@ -328,6 +332,8 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
328
332
  root: {
329
333
  kind: 'composite', title: `${chapterId} 章节交付`, constraint: 'hard', target: `${relativeDir}/delivery.json`,
330
334
  completion: { op: 'all', items: Object.keys(stages).map(id => ({ op: 'ref', id })) },
335
+ // Inert contract metadata naming the verification stage kind (see the note
336
+ // in dog-review.ts): not resolved or executed by this plugin.
331
337
  verifier: { mode: 'programmatic', script: 'delivery-stage' },
332
338
  },
333
339
  };
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
3
3
  import { join, relative } from 'node:path';
4
4
  import { validateReviewV2Decision } from './contracts-generated.js';
5
- import { assertContained, assertWorkspaceRootMatch } from './dog-delivery.js';
5
+ import { assertContained, assertWorkspaceRootMatch, atomicWrite } from './dog-delivery.js';
6
6
  export const DOG_REVIEW_DIMENSIONS = {
7
7
  1: 'OOC检查', 2: '时间线检查', 3: '设定冲突', 4: '战力崩坏', 5: '数值检查',
8
8
  6: '伏笔检查', 7: '节奏检查', 8: '文风检查', 9: '信息越界', 10: '词汇疲劳',
@@ -146,6 +146,11 @@ export function instantiateDogReviewFramework(frameworkValue, chapterId, relativ
146
146
  title: titleTemplate.replaceAll('{chapter_id}', chapterId),
147
147
  constraint: template['constraint'],
148
148
  target: `${prefix}/${artifact}`,
149
+ // Inert, server-declared metadata: the framework names a `script` such as
150
+ // "review-record"/"review-dimension" as a verification *stage kind*. The
151
+ // `scripts/dog/*.js` files that once implemented those names were removed
152
+ // with the vendored DoG engine; nothing in this plugin resolves or executes
153
+ // the value, so it is forwarded verbatim for contract compatibility.
149
154
  verifier: template['verifier'],
150
155
  };
151
156
  if (template['completion'] !== undefined)
@@ -393,6 +398,11 @@ export async function materializeDogReview(response, chapterId, threshold, expec
393
398
  catch {
394
399
  throw new Error(`invalid project root: ${root}`);
395
400
  }
401
+ // Contain the derived path before touching the filesystem: `novelId` comes
402
+ // straight from the Studio payload. dog-delivery.ts asserts first and reads
403
+ // second; doing it the other way round walks outside the project root first.
404
+ const reviewDir = join(root, 'data', 'novels', novelId, 'data', 'dog', 'reviews', chapterId);
405
+ assertContained(reviewDir, root);
396
406
  const { manifest, dimensionRecords } = buildDogReviewBundle(review, chapterId, threshold);
397
407
  // The artifact records retain the exact blueprint identity so a historical
398
408
  // review can always be explained against the topology that produced it.
@@ -417,7 +427,9 @@ export async function materializeDogReview(response, chapterId, threshold, expec
417
427
  const directory = join(root, relativeDir);
418
428
  assertContained(directory, root);
419
429
  await mkdir(directory, { recursive: true });
420
- const write = async (name, value) => writeFile(join(directory, name), `${JSON.stringify(value, null, 2)}\n`, 'utf8');
430
+ // Atomic per-file writes: a crash mid-run must not leave a truncated JSON that
431
+ // a later read classifies as "DoG artifact corrupt JSON" (dog-delivery.ts:38).
432
+ const write = (name, value) => atomicWrite(join(directory, name), value);
421
433
  await write('review.json', manifest);
422
434
  await write('context.json', {
423
435
  schemaVersion: 'dsh-novel.review.context.v2', recordType: 'review-context', chapterId,
@@ -6,6 +6,23 @@ export declare const CONFIG_ROUTE = "/studio-panel/config.json";
6
6
  export declare const API_PROXY_ROUTE = "/studio-panel/api";
7
7
  export declare const EVENTS_ROUTE = "/studio-panel/events";
8
8
  export declare const INVALIDATION_ROUTE = "/studio-panel/invalidation.json";
9
+ /** Every browser write path the proxy forwards. Exported for the regression test. */
10
+ export declare const WRITABLE_PATHS: Set<string>;
11
+ export declare const WRITABLE_PATH_RE: RegExp;
12
+ /**
13
+ * Paths whose payload is global, not project-scoped.
14
+ *
15
+ * The model profile library lives outside every project — Studio keeps it in
16
+ * the user's preferences directory (`model-profiles.json`), and every model
17
+ * route is registered with `requires_project=False`. The host settings dialog
18
+ * is the one surface that is opened *without* a bound workspace, so these
19
+ * paths must be reachable without a workspace id: the browser sends no
20
+ * `X-Dsh-Workspace-Id` and the proxy forwards the call without a project
21
+ * root, exactly as the contract's project-free routes expect.
22
+ *
23
+ * Every other proxied path stays fail-closed on a missing workspace id.
24
+ */
25
+ export declare const GLOBAL_PATHS: Set<string>;
9
26
  declare module '@deepseek-ai/cordis' {
10
27
  interface Context {
11
28
  novelDomain: NovelDomainService;
@@ -37,6 +54,8 @@ export declare class NovelDomainService extends Service {
37
54
  private readonly connect;
38
55
  readonly baseUrl: string;
39
56
  private readonly resolveWorkspace;
57
+ /** Undelivered SSE bytes per stream, for backpressure (see notifyMutation). */
58
+ private readonly streamPending;
40
59
  /** Per-root invalidation state, keyed by canonical workspace root ('' = legacy). */
41
60
  private readonly roots;
42
61
  /** Open SSE connections bound to the root key they subscribed to. */
@@ -63,6 +82,14 @@ export declare class NovelDomainService extends Service {
63
82
  * closed per the contract: missing id → 400 WORKSPACE_CONTEXT_MISSING, no
64
83
  * registry → 503 WORKSPACE_REGISTRY_UNAVAILABLE, unknown id → 400 WORKSPACE_UNKNOWN.
65
84
  */
85
+ /**
86
+ * The workspace a browser view belongs to, for invalidation only.
87
+ *
88
+ * An unknown or absent workspace id resolves to the global bucket rather
89
+ * than to a rejection: the payload of a global path does not depend on the
90
+ * workspace, so a stale id must not turn a readable answer into an error.
91
+ */
92
+ private viewerRoot;
66
93
  private resolveBrowserWorkspace;
67
94
  private static sendRejection;
68
95
  registerWebRoutes(ctx: Context): void;