dsh-openwrite 0.2.28 → 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.
- package/LICENSE +1 -1
- package/README.md +49 -8
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +1 -1
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +1 -1
- package/docs/COST_MEASUREMENT.md +2 -2
- package/docs/DEVELOPMENT.md +6 -11
- package/docs/DSH_STORE.md +14 -2
- package/docs/EDITOR_SAFETY_REPAIRS.md +2 -2
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +4 -4
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUE_123_FOLLOWUP.md +3 -2
- package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
- package/docs/LEARNING.md +1 -1
- package/docs/MOBILE_READINESS.md +62 -0
- package/docs/PLUGIN_MAINTENANCE.md +1 -3
- package/docs/RELEASE_ACCEPTANCE.md +20 -3
- package/docs/REVIEW_ROUND_22.md +67 -0
- package/docs/REVIEW_ROUND_23.md +48 -0
- package/docs/REVIEW_ROUND_25.md +51 -0
- package/docs/SELECTION_UPGRADE_REPAIRS.md +2 -2
- package/docs/TOOLS.md +181 -604
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +17 -21
- package/packages/openwrite-bridge/lib/client.d.ts +0 -1
- package/packages/openwrite-bridge/lib/client.js +90 -9
- package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +3 -1
- package/packages/openwrite-bridge/lib/dog-review.js +15 -3
- package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
- package/packages/openwrite-bridge/lib/domain.js +250 -29
- package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
- package/packages/openwrite-bridge/lib/host-llm.js +657 -0
- package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
- package/packages/openwrite-bridge/lib/host-web.js +166 -0
- package/packages/openwrite-bridge/lib/index.d.ts +4 -0
- package/packages/openwrite-bridge/lib/index.js +150 -6
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
- package/packages/openwrite-bridge/lib/managed-runtime.js +176 -34
- package/packages/openwrite-bridge/lib/tool-policy.js +37 -12
- package/packages/openwrite-bridge/lib/tools.js +1223 -1227
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +123207 -121534
- package/packages/studio-panel/lib/client.js.map +1 -1
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
- package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
- package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
- package/packages/studio-panel/package.json +5 -3
- package/plugin.mjs +159 -20
- package/preset-definition.mjs +144 -4
- package/presets/openwrite/agent.cordis.yml +7 -9
- package/presets/openwrite/ops-integration.mjs +28 -0
- package/presets/openwrite/preset.yml +2 -1
- package/presets/openwrite/skills/canon/SKILL.md +3 -3
- package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
- package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
- package/presets/openwrite/skills/export-book/SKILL.md +3 -3
- package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
- package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
- package/presets/openwrite/skills/learn/SKILL.md +3 -3
- package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
- package/presets/openwrite/skills/progress/SKILL.md +1 -1
- package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
- package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
- package/presets/openwrite/skills/style-system/SKILL.md +13 -6
- package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
- package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
- package/presets/openwrite/skills/world-query/SKILL.md +16 -11
- package/presets/openwrite/skills/write-next/SKILL.md +1 -1
- package/release/{openwrite-5.8.20-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -13
- package/release/source.json +21 -21
- package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
- package/scripts/maintenance.mjs +67 -7
- package/scripts/plugin-doctor.mjs +179 -0
- package/scripts/prepare.mjs +11 -2
- package/scripts/safe-upgrade.mjs +38 -5
- package/dog-tools.mjs +0 -13
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
- package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
- package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
- package/scripts/dog/delivery-stage.js +0 -32
- package/scripts/dog/import-chapter.js +0 -20
- package/scripts/dog/import-record.js +0 -35
- package/scripts/dog/review-dimension.js +0 -41
- package/scripts/dog/review-record.js +0 -42
- package/vendor/dsh-dog/LICENSE +0 -29
- package/vendor/dsh-dog/README.md +0 -114
- package/vendor/dsh-dog/UPSTREAM.md +0 -30
- package/vendor/dsh-dog/cordis.patch.yml +0 -14
- package/vendor/dsh-dog/lib/client.js +0 -2960
- package/vendor/dsh-dog/lib/client.js.map +0 -1
- package/vendor/dsh-dog/lib/core--byUPJL8.js +0 -2191
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
- package/vendor/dsh-dog/lib/core.d.ts +0 -2
- package/vendor/dsh-dog/lib/core.js +0 -2
- package/vendor/dsh-dog/lib/index.d.ts +0 -143
- package/vendor/dsh-dog/lib/index.js +0 -1230
- package/vendor/dsh-dog/package.json +0 -110
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
- 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
|
-
正文和正典使用 `
|
|
27
|
-
使用 `
|
|
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
|
-
通过 `
|
|
35
|
-
`
|
|
34
|
+
通过 `novel_task(action="create", type=chapter_review)` 启动的后台评审,也会在
|
|
35
|
+
`novel_task(action="get")` 读到完成态时生成同样的图;
|
|
36
36
|
图的顶层是上下文完整性、六个质量域、硬门禁和确定性聚合,六域下可展开原 37 项,
|
|
37
37
|
查询 criterion、证据、问题、覆盖率和继承状态。DoG 只查询已生成的 artifact,
|
|
38
38
|
不重新调用模型评审。
|
|
39
|
-
写章、评审和 `
|
|
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.
|
|
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
|
|
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,15 +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/
|
|
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
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",
|
|
22
|
+
"docs:check": "node scripts/gen-tools-md.mjs --check && node scripts/check-editor-coordinates.mjs && node scripts/check-tool-references.mjs",
|
|
23
23
|
"test:profiles": "node scripts/profile-smoke.mjs",
|
|
24
24
|
"prepare": "node scripts/prepare.mjs",
|
|
25
|
-
"test:managed": "node --test packages/openwrite-bridge/scripts/managed-runtime.test.mjs packages/openwrite-bridge/scripts/tool-policy.test.mjs packages/openwrite-bridge/scripts/followup-tools.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",
|
|
26
26
|
"test:release": "node scripts/release-smoke.mjs",
|
|
27
27
|
"test:client-boot": "node scripts/client-boot-smoke.mjs"
|
|
28
28
|
},
|
|
@@ -44,9 +44,8 @@
|
|
|
44
44
|
"./bridge": "./packages/openwrite-bridge/lib/index.js",
|
|
45
45
|
"./tools": "./packages/openwrite-bridge/lib/preset-tools.js",
|
|
46
46
|
"./panel": "./packages/studio-panel/lib/index.js",
|
|
47
|
-
"./dog": "./vendor/dsh-dog/lib/index.js",
|
|
48
47
|
"./package.json": "./package.json",
|
|
49
|
-
"./
|
|
48
|
+
"./ops-integration": "./presets/openwrite/ops-integration.mjs"
|
|
50
49
|
},
|
|
51
50
|
"dsh": {
|
|
52
51
|
"bundle": {
|
|
@@ -55,7 +54,7 @@
|
|
|
55
54
|
"displayName": "小说创作(OpenWrite)",
|
|
56
55
|
"pluginType": "suite",
|
|
57
56
|
"compatibility": {
|
|
58
|
-
"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",
|
|
59
58
|
"profiles": [
|
|
60
59
|
"web",
|
|
61
60
|
"desktop"
|
|
@@ -67,21 +66,19 @@
|
|
|
67
66
|
"0.1.5-alpha.1": "compatible",
|
|
68
67
|
"0.2.0-rc.2": "compatible",
|
|
69
68
|
"0.2.1-rc.2": "compatible",
|
|
70
|
-
"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"
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
75
|
"files": [
|
|
75
76
|
"plugin.mjs",
|
|
76
77
|
"preset-definition.mjs",
|
|
78
|
+
"companions.mjs",
|
|
77
79
|
"plugin.d.ts",
|
|
78
80
|
"cordis.patch.yml",
|
|
79
81
|
"packages/studio-panel/vendor",
|
|
80
|
-
"vendor/dsh-dog/package.json",
|
|
81
|
-
"vendor/dsh-dog/lib",
|
|
82
|
-
"vendor/dsh-dog/schemas",
|
|
83
|
-
"vendor/dsh-dog/LICENSE",
|
|
84
|
-
"vendor/dsh-dog/UPSTREAM.md",
|
|
85
82
|
"presets/openwrite",
|
|
86
83
|
"release/*.json",
|
|
87
84
|
"release/*.lock",
|
|
@@ -90,13 +87,12 @@
|
|
|
90
87
|
"README.md",
|
|
91
88
|
"LICENSE",
|
|
92
89
|
"THIRD_PARTY_NOTICES.md",
|
|
93
|
-
"dog-tools.mjs",
|
|
94
90
|
"packages/openwrite-bridge/lib",
|
|
95
91
|
"packages/openwrite-bridge/package.json",
|
|
96
92
|
"packages/studio-panel/lib",
|
|
97
93
|
"packages/studio-panel/package.json",
|
|
98
|
-
"scripts/dog",
|
|
99
94
|
"scripts/maintenance.mjs",
|
|
95
|
+
"scripts/plugin-doctor.mjs",
|
|
100
96
|
"scripts/safe-upgrade.mjs",
|
|
101
97
|
"packages/openwrite-bridge/LICENSE",
|
|
102
98
|
"packages/studio-panel/LICENSE",
|
|
@@ -111,15 +107,15 @@
|
|
|
111
107
|
"ajv": "^8.20.0",
|
|
112
108
|
"ajv-formats": "^3.0.1",
|
|
113
109
|
"fflate": "0.8.2",
|
|
110
|
+
"js-yaml": "4.3.1",
|
|
114
111
|
"proper-lockfile": "4.1.2",
|
|
115
|
-
"tar": "7.5.7"
|
|
116
|
-
"js-yaml": "4.3.1"
|
|
112
|
+
"tar": "7.5.7"
|
|
117
113
|
},
|
|
118
114
|
"peerDependencies": {
|
|
119
115
|
"@deepseek-ai/cordis": "4.0.2 || ~4.0.4",
|
|
120
|
-
"@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",
|
|
121
|
-
"@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",
|
|
122
|
-
"@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"
|
|
123
119
|
},
|
|
124
120
|
"bin": {
|
|
125
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
|
@@ -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 {
|
|
@@ -332,6 +332,8 @@ export async function materializeChapterDelivery(workspaceValue, chapterId, thre
|
|
|
332
332
|
root: {
|
|
333
333
|
kind: 'composite', title: `${chapterId} 章节交付`, constraint: 'hard', target: `${relativeDir}/delivery.json`,
|
|
334
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.
|
|
335
337
|
verifier: { mode: 'programmatic', script: 'delivery-stage' },
|
|
336
338
|
},
|
|
337
339
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdir, readdir, readFile, stat
|
|
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
|
-
|
|
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;
|