dsh-openwrite 0.2.8
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 +201 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/assets/logo-dark.svg +31 -0
- package/assets/logo-light.svg +31 -0
- package/cordis.patch.yml +11 -0
- package/docs/BENCHMARK_TASKS.md +54 -0
- package/docs/COST_MEASUREMENT.md +39 -0
- package/docs/DEVELOPMENT.md +65 -0
- package/docs/DSH_STORE.md +58 -0
- package/docs/FRAMEWORK_ADVANTAGES.md +288 -0
- package/docs/IMPROVEMENT_PLAN.md +252 -0
- package/docs/INSTALL.md +99 -0
- package/docs/LEARNING.md +68 -0
- package/docs/OPEN_SOURCE_NOVEL_AUDIT.md +191 -0
- package/docs/PLUGIN_MAINTENANCE.md +54 -0
- package/docs/PLUGIN_MAINTENANCE_RC7.md +157 -0
- package/docs/QA_20260908.md +103 -0
- package/docs/QA_20260909_BENCHMARK.md +24 -0
- package/docs/RELEASE_ACCEPTANCE.md +116 -0
- package/docs/REVIEW_DAG_FRAMEWORK.md +82 -0
- package/docs/TARGETED_NOVEL_PROJECT_REVIEW.md +167 -0
- package/docs/WORKFLOWS.md +74 -0
- package/docs/WORKSPACE_CONTEXT_CONTRACT.md +125 -0
- package/docs/WORKSPACE_INTEGRATION_GOAL.md +278 -0
- package/docs/research/autosave-race-repro.mjs +133 -0
- package/docs/research/framework-stats-2026-09-05.json +116 -0
- package/dog-tools.mjs +11 -0
- package/package.json +132 -0
- package/packages/openwrite-bridge/LICENSE +201 -0
- package/packages/openwrite-bridge/cordis.patch.yml +10 -0
- package/packages/openwrite-bridge/lib/client.d.ts +95 -0
- package/packages/openwrite-bridge/lib/client.js +193 -0
- package/packages/openwrite-bridge/lib/contracts-generated.d.ts +367 -0
- package/packages/openwrite-bridge/lib/contracts-generated.js +1764 -0
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +12 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +362 -0
- package/packages/openwrite-bridge/lib/dog-review.d.ts +50 -0
- package/packages/openwrite-bridge/lib/dog-review.js +470 -0
- package/packages/openwrite-bridge/lib/domain.d.ts +70 -0
- package/packages/openwrite-bridge/lib/domain.js +496 -0
- package/packages/openwrite-bridge/lib/index.d.ts +17 -0
- package/packages/openwrite-bridge/lib/index.js +89 -0
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +95 -0
- package/packages/openwrite-bridge/lib/managed-runtime.js +510 -0
- package/packages/openwrite-bridge/lib/preset-tools.d.ts +13 -0
- package/packages/openwrite-bridge/lib/preset-tools.js +18 -0
- package/packages/openwrite-bridge/lib/tool-policy.d.ts +6 -0
- package/packages/openwrite-bridge/lib/tool-policy.js +56 -0
- package/packages/openwrite-bridge/lib/tools.d.ts +24 -0
- package/packages/openwrite-bridge/lib/tools.js +2404 -0
- package/packages/openwrite-bridge/package.json +67 -0
- package/packages/studio-panel/LICENSE +21 -0
- package/packages/studio-panel/README.md +51 -0
- package/packages/studio-panel/cordis.patch.yml +5 -0
- package/packages/studio-panel/lib/client.js +158087 -0
- package/packages/studio-panel/lib/client.js.map +1 -0
- package/packages/studio-panel/lib/index.js +50 -0
- package/packages/studio-panel/lib/types/client/AssetEditor.d.ts +122 -0
- package/packages/studio-panel/lib/types/client/AssetsView.d.ts +43 -0
- package/packages/studio-panel/lib/types/client/BenchmarkView.d.ts +9 -0
- package/packages/studio-panel/lib/types/client/ContinuousReader.d.ts +13 -0
- package/packages/studio-panel/lib/types/client/CreationView.d.ts +6 -0
- package/packages/studio-panel/lib/types/client/DagCanvas.d.ts +11 -0
- package/packages/studio-panel/lib/types/client/DiscardDraftDialog.d.ts +8 -0
- package/packages/studio-panel/lib/types/client/DomainToolCard.d.ts +9 -0
- package/packages/studio-panel/lib/types/client/GraphView.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/HeaderChrome.d.ts +18 -0
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +13 -0
- package/packages/studio-panel/lib/types/client/LibraryView.d.ts +6 -0
- package/packages/studio-panel/lib/types/client/MarkdownText.d.ts +3 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +9 -0
- package/packages/studio-panel/lib/types/client/OperationsView.d.ts +6 -0
- package/packages/studio-panel/lib/types/client/OutlinePlans.d.ts +37 -0
- package/packages/studio-panel/lib/types/client/OutlineView.d.ts +29 -0
- package/packages/studio-panel/lib/types/client/ResearchView.d.ts +7 -0
- package/packages/studio-panel/lib/types/client/ReviewCard.d.ts +28 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +55 -0
- package/packages/studio-panel/lib/types/client/SearchView.d.ts +33 -0
- package/packages/studio-panel/lib/types/client/TasksView.d.ts +11 -0
- package/packages/studio-panel/lib/types/client/TransferWorkspaces.d.ts +19 -0
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +62 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +55 -0
- package/packages/studio-panel/lib/types/client/WorkbenchStore.d.ts +121 -0
- package/packages/studio-panel/lib/types/client/api.d.ts +72 -0
- package/packages/studio-panel/lib/types/client/asset-drafts.d.ts +20 -0
- package/packages/studio-panel/lib/types/client/benchmark-options.d.ts +43 -0
- package/packages/studio-panel/lib/types/client/draft-store.d.ts +26 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +517 -0
- package/packages/studio-panel/lib/types/client/index.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +15 -0
- package/packages/studio-panel/lib/types/client/manuscript-annotations.d.ts +59 -0
- package/packages/studio-panel/lib/types/client/manuscript-markers.d.ts +61 -0
- package/packages/studio-panel/lib/types/client/manuscript-mentions.d.ts +30 -0
- package/packages/studio-panel/lib/types/client/manuscript-overlay-dom.d.ts +7 -0
- package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +42 -0
- package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +59 -0
- package/packages/studio-panel/lib/types/client/model-view-memory.d.ts +17 -0
- package/packages/studio-panel/lib/types/client/storage.d.ts +8 -0
- package/packages/studio-panel/lib/types/client/workbench-epochs.d.ts +16 -0
- package/packages/studio-panel/lib/types/client/workspace-context.d.ts +52 -0
- package/packages/studio-panel/lib/types/client/writing-scope.d.ts +7 -0
- package/packages/studio-panel/lib/types/index.d.ts +5 -0
- package/packages/studio-panel/package.json +105 -0
- package/packages/studio-panel/vendor/vditor/LICENSE +21 -0
- package/packages/studio-panel/vendor/vditor/dist/css/content-theme/dark.css +123 -0
- package/packages/studio-panel/vendor/vditor/dist/css/content-theme/light.css +101 -0
- package/packages/studio-panel/vendor/vditor/dist/index.css +1823 -0
- package/packages/studio-panel/vendor/vditor/dist/index.min.js +1 -0
- package/packages/studio-panel/vendor/vditor/dist/js/i18n/zh_CN.js +86 -0
- package/packages/studio-panel/vendor/vditor/dist/js/icons/ant.js +174 -0
- package/packages/studio-panel/vendor/vditor/dist/js/lute/lute.min.js +72 -0
- package/plugin.d.ts +3 -0
- package/plugin.mjs +80 -0
- package/presets/README.md +32 -0
- package/presets/openwrite/agent.cordis.yml +317 -0
- package/presets/openwrite/preset.yml +3 -0
- package/presets/openwrite/skills/canon/SKILL.md +15 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +166 -0
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +40 -0
- package/presets/openwrite/skills/dog-import-query/SKILL.md +27 -0
- package/presets/openwrite/skills/dog-review-query/SKILL.md +40 -0
- package/presets/openwrite/skills/export-book/SKILL.md +14 -0
- package/presets/openwrite/skills/foreshadow/SKILL.md +14 -0
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +125 -0
- package/presets/openwrite/skills/learn/SKILL.md +24 -0
- package/presets/openwrite/skills/novel-creator/SKILL.md +88 -0
- package/presets/openwrite/skills/novel-creator/templates/beat_templates.yaml +179 -0
- package/presets/openwrite/skills/novel-creator/templates/example_usage.yaml +74 -0
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +85 -0
- package/presets/openwrite/skills/oh-story-deslop/LICENSE +21 -0
- package/presets/openwrite/skills/oh-story-deslop/SKILL.md +36 -0
- package/presets/openwrite/skills/oh-story-deslop/agents/openai.yaml +4 -0
- package/presets/openwrite/skills/oh-story-deslop/references/method.md +22 -0
- package/presets/openwrite/skills/oh-story-long-analyze/LICENSE +21 -0
- package/presets/openwrite/skills/oh-story-long-analyze/SKILL.md +37 -0
- package/presets/openwrite/skills/oh-story-long-analyze/agents/openai.yaml +4 -0
- package/presets/openwrite/skills/oh-story-long-analyze/references/method.md +22 -0
- package/presets/openwrite/skills/oh-story-long-scan/LICENSE +21 -0
- package/presets/openwrite/skills/oh-story-long-scan/SKILL.md +36 -0
- package/presets/openwrite/skills/oh-story-long-scan/agents/openai.yaml +4 -0
- package/presets/openwrite/skills/oh-story-long-scan/references/method.md +21 -0
- package/presets/openwrite/skills/oh-story-long-write/LICENSE +21 -0
- package/presets/openwrite/skills/oh-story-long-write/SKILL.md +37 -0
- package/presets/openwrite/skills/oh-story-long-write/agents/openai.yaml +4 -0
- package/presets/openwrite/skills/oh-story-long-write/references/method.md +33 -0
- package/presets/openwrite/skills/oh-story-review/LICENSE +21 -0
- package/presets/openwrite/skills/oh-story-review/SKILL.md +36 -0
- package/presets/openwrite/skills/oh-story-review/agents/openai.yaml +4 -0
- package/presets/openwrite/skills/oh-story-review/references/method.md +25 -0
- package/presets/openwrite/skills/progress/SKILL.md +16 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +17 -0
- package/presets/openwrite/skills/revise-span/SKILL.md +16 -0
- package/presets/openwrite/skills/style-system/SKILL.md +135 -0
- package/presets/openwrite/skills/style-system/prompts/analyze_style.md +214 -0
- package/presets/openwrite/skills/style-system/prompts/batch_extract_style.md +66 -0
- package/presets/openwrite/skills/style-system/prompts/compose_style.md +83 -0
- package/presets/openwrite/skills/style-system/prompts/extract_style.md +84 -0
- package/presets/openwrite/skills/style-system/prompts/initialize_style.md +52 -0
- package/presets/openwrite/skills/style-system/templates/analysis_report.yaml +78 -0
- package/presets/openwrite/skills/style-system/templates/composed_style.md +96 -0
- package/presets/openwrite/skills/style-system/templates/extraction_report.yaml +80 -0
- package/presets/openwrite/skills/style-system/templates/fingerprint.yaml +61 -0
- package/presets/openwrite/skills/style-system/tools/README.md +33 -0
- package/presets/openwrite/skills/truth-validation/SKILL.md +119 -0
- package/presets/openwrite/skills/workflow-manager/SKILL.md +81 -0
- package/presets/openwrite/skills/world-query/SKILL.md +45 -0
- package/presets/openwrite/skills/write-next/SKILL.md +17 -0
- package/release/core-requirements.in +12 -0
- package/release/licenses/@deepseek-ai__cosmokit@1.8.3/LICENSE +21 -0
- package/release/licenses/@deepseek-ai__schemastery@3.18.2/LICENSE +21 -0
- package/release/licenses/@isaacs__fs-minipass@4.0.1/LICENSE +15 -0
- package/release/licenses/@standard-schema__spec@1.1.0/LICENSE +21 -0
- package/release/licenses/@types__d3-color@3.1.3/LICENSE +21 -0
- package/release/licenses/@types__d3-drag@3.0.7/LICENSE +21 -0
- package/release/licenses/@types__d3-interpolate@3.0.4/LICENSE +21 -0
- package/release/licenses/@types__d3-selection@3.0.11/LICENSE +21 -0
- package/release/licenses/@types__d3-transition@3.0.9/LICENSE +21 -0
- package/release/licenses/@types__d3-zoom@3.0.8/LICENSE +21 -0
- package/release/licenses/@xyflow__react@12.11.6/LICENSE +21 -0
- package/release/licenses/@xyflow__system@0.0.82/LICENSE +21 -0
- package/release/licenses/ajv-formats@3.0.1/LICENSE +21 -0
- package/release/licenses/ajv@8.20.0/LICENSE +22 -0
- package/release/licenses/chownr@3.0.0/LICENSE.md +63 -0
- package/release/licenses/classcat@5.0.5/LICENSE.md +7 -0
- package/release/licenses/commander@7.2.0/LICENSE +22 -0
- package/release/licenses/d3-array@3.2.4/LICENSE +13 -0
- package/release/licenses/d3-axis@3.0.0/LICENSE +13 -0
- package/release/licenses/d3-brush@3.0.0/LICENSE +13 -0
- package/release/licenses/d3-chord@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-color@3.1.0/LICENSE +13 -0
- package/release/licenses/d3-contour@4.0.2/LICENSE +13 -0
- package/release/licenses/d3-delaunay@6.0.4/LICENSE +14 -0
- package/release/licenses/d3-dispatch@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-drag@3.0.0/LICENSE +13 -0
- package/release/licenses/d3-dsv@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-ease@3.0.1/LICENSE +28 -0
- package/release/licenses/d3-fetch@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-force@3.0.0/LICENSE +13 -0
- package/release/licenses/d3-format@3.1.2/LICENSE +13 -0
- package/release/licenses/d3-geo@3.1.1/LICENSE +34 -0
- package/release/licenses/d3-hierarchy@3.1.2/LICENSE +13 -0
- package/release/licenses/d3-interpolate@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-path@3.1.0/LICENSE +13 -0
- package/release/licenses/d3-polygon@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-quadtree@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-random@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-scale-chromatic@3.1.0/LICENSE +28 -0
- package/release/licenses/d3-scale@4.0.2/LICENSE +13 -0
- package/release/licenses/d3-selection@3.0.0/LICENSE +13 -0
- package/release/licenses/d3-shape@3.2.0/LICENSE +13 -0
- package/release/licenses/d3-time-format@4.1.0/LICENSE +13 -0
- package/release/licenses/d3-time@3.1.0/LICENSE +13 -0
- package/release/licenses/d3-timer@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-transition@3.0.1/LICENSE +13 -0
- package/release/licenses/d3-zoom@3.0.0/LICENSE +13 -0
- package/release/licenses/d3@7.9.0/LICENSE +13 -0
- package/release/licenses/delaunator@5.1.0/LICENSE +15 -0
- package/release/licenses/elkjs@0.12.0/LICENSE.md +263 -0
- package/release/licenses/fast-deep-equal@3.1.3/LICENSE +21 -0
- package/release/licenses/fast-uri@3.1.7/LICENSE +30 -0
- package/release/licenses/fflate@0.8.2/LICENSE +21 -0
- package/release/licenses/graceful-fs@4.2.11/LICENSE +15 -0
- package/release/licenses/iconv-lite@0.6.3/LICENSE +21 -0
- package/release/licenses/internmap@2.0.3/LICENSE +13 -0
- package/release/licenses/json-schema-traverse@1.0.0/LICENSE +21 -0
- package/release/licenses/lucide-react@0.468.0/LICENSE +15 -0
- package/release/licenses/minipass@7.1.3/LICENSE.md +55 -0
- package/release/licenses/minizlib@3.1.0/LICENSE +26 -0
- package/release/licenses/proper-lockfile@4.1.2/LICENSE +21 -0
- package/release/licenses/require-from-string@2.0.2/license +21 -0
- package/release/licenses/retry@0.12.0/License +21 -0
- package/release/licenses/robust-predicates@3.0.3/LICENSE +24 -0
- package/release/licenses/rw@1.3.3/LICENSE +26 -0
- package/release/licenses/safer-buffer@2.1.2/LICENSE +21 -0
- package/release/licenses/signal-exit@3.0.7/LICENSE.txt +16 -0
- package/release/licenses/tar@7.5.7/LICENSE.md +55 -0
- package/release/licenses/use-sync-external-store@1.6.0/LICENSE +21 -0
- package/release/licenses/yallist@5.0.0/LICENSE.md +63 -0
- package/release/licenses/zustand@4.5.7/LICENSE +21 -0
- package/release/openwrite-5.8.1-py3-none-any.whl +0 -0
- package/release/platform-overrides.in +5 -0
- package/release/python-constraints.in +96 -0
- package/release/requirements.lock +2927 -0
- package/release/runtime-manifest.json +98 -0
- package/release/sdk-compatibility.json +240 -0
- package/release/source.json +859 -0
- package/release/wheels/cryptography-50.0.1-cp312-abi3-macosx_13_0_x86_64.whl +0 -0
- package/release/wheels/licenses/LICENSE.txt +177 -0
- package/release/wheels/provenance.json +15 -0
- package/scripts/dog/delivery-stage.js +32 -0
- package/scripts/dog/import-chapter.js +20 -0
- package/scripts/dog/import-record.js +35 -0
- package/scripts/dog/review-dimension.js +41 -0
- package/scripts/dog/review-record.js +42 -0
- package/scripts/maintenance.mjs +179 -0
- package/scripts/prepare.mjs +21 -0
- package/vendor/dsh-dog/LICENSE +29 -0
- package/vendor/dsh-dog/README.md +114 -0
- package/vendor/dsh-dog/UPSTREAM.md +22 -0
- package/vendor/dsh-dog/cordis.patch.yml +14 -0
- package/vendor/dsh-dog/lib/client.js +2898 -0
- package/vendor/dsh-dog/lib/client.js.map +1 -0
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +495 -0
- package/vendor/dsh-dog/lib/core-DdfMcrB4.js +2074 -0
- package/vendor/dsh-dog/lib/core.d.ts +2 -0
- package/vendor/dsh-dog/lib/core.js +2 -0
- package/vendor/dsh-dog/lib/index.d.ts +133 -0
- package/vendor/dsh-dog/lib/index.js +967 -0
- package/vendor/dsh-dog/package.json +97 -0
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +139 -0
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +87 -0
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +259 -0
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +105 -0
- package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +40 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
import { StudioClient, workspaceRootHeaders } from './client.js';
|
|
3
|
+
import { workspaceContextFromExec } from './tools.js';
|
|
4
|
+
export const CONFIG_ROUTE = '/studio-panel/config.json';
|
|
5
|
+
export const API_PROXY_ROUTE = '/studio-panel/api';
|
|
6
|
+
export const EVENTS_ROUTE = '/studio-panel/events';
|
|
7
|
+
export const INVALIDATION_ROUTE = '/studio-panel/invalidation.json';
|
|
8
|
+
const PROXY_TIMEOUT_MS = 60_000;
|
|
9
|
+
/** Cap for the best-effort upstream context_epoch read; never blocks a snapshot longer. */
|
|
10
|
+
const CONTEXT_EPOCH_TIMEOUT_MS = 5_000;
|
|
11
|
+
const WRITABLE_PATHS = new Set([
|
|
12
|
+
'assets',
|
|
13
|
+
'assets/update',
|
|
14
|
+
'assets/package/import',
|
|
15
|
+
'outline/edit',
|
|
16
|
+
'rolling-plans',
|
|
17
|
+
'narrative-forecasts',
|
|
18
|
+
'sync',
|
|
19
|
+
'import/preview',
|
|
20
|
+
'import',
|
|
21
|
+
'manuscript-imports/prepare',
|
|
22
|
+
'manuscript-imports/structure',
|
|
23
|
+
'manuscript-imports/confirm',
|
|
24
|
+
'manuscript-imports/run',
|
|
25
|
+
'manuscript-imports/discard',
|
|
26
|
+
'project-archives/create',
|
|
27
|
+
'project-archives/restore/preview',
|
|
28
|
+
'project-archives/restore',
|
|
29
|
+
'reading-order/move',
|
|
30
|
+
'scenes/migration/apply',
|
|
31
|
+
'scenes/migration/rollback',
|
|
32
|
+
'scenes/metadata',
|
|
33
|
+
'scenes/move',
|
|
34
|
+
'tasks',
|
|
35
|
+
'benchmarks',
|
|
36
|
+
'document',
|
|
37
|
+
'document/change-plan',
|
|
38
|
+
'structured/change-plan',
|
|
39
|
+
'manuscript-editing',
|
|
40
|
+
'manuscript/acceptance/baseline',
|
|
41
|
+
'manuscript/acceptance/external',
|
|
42
|
+
'manuscript/acceptance/reconcile',
|
|
43
|
+
'manuscript/acceptance/ack',
|
|
44
|
+
'project/init',
|
|
45
|
+
'model',
|
|
46
|
+
'model/test',
|
|
47
|
+
'model/embedding/test',
|
|
48
|
+
'model/embedding',
|
|
49
|
+
'model/embedding/select',
|
|
50
|
+
'model/embedding/delete',
|
|
51
|
+
'model/profiles',
|
|
52
|
+
'model/profiles/delete',
|
|
53
|
+
'model/routes',
|
|
54
|
+
'chapter/delete-batch',
|
|
55
|
+
'research/settings',
|
|
56
|
+
]);
|
|
57
|
+
const WRITABLE_PATH_RE = /^(?:tasks\/[A-Za-z0-9_-]+\/(?:cancel|retry|confirm)|revisions\/(?:selection|from-review|rev_[A-Za-z0-9_-]+\/(?:apply|reject|regenerate)))$/;
|
|
58
|
+
/** Mutation/SSE state bucket key for context-less (legacy) mutations. */
|
|
59
|
+
const LEGACY_ROOT_KEY = '';
|
|
60
|
+
function sendJson(res, status, payload) {
|
|
61
|
+
const body = Buffer.from(JSON.stringify(payload));
|
|
62
|
+
res.writeHead(status, {
|
|
63
|
+
'content-type': 'application/json',
|
|
64
|
+
'cache-control': 'no-cache',
|
|
65
|
+
'content-length': String(body.length),
|
|
66
|
+
});
|
|
67
|
+
res.end(body);
|
|
68
|
+
}
|
|
69
|
+
async function readBody(req) {
|
|
70
|
+
const iterator = req[Symbol.asyncIterator];
|
|
71
|
+
if (iterator === undefined)
|
|
72
|
+
return Buffer.alloc(0);
|
|
73
|
+
const chunks = [];
|
|
74
|
+
for await (const chunk of iterator.call(req)) {
|
|
75
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
76
|
+
}
|
|
77
|
+
return Buffer.concat(chunks);
|
|
78
|
+
}
|
|
79
|
+
function resourceForPath(path) {
|
|
80
|
+
if (path.startsWith('/api/manuscript-imports'))
|
|
81
|
+
return 'tasks';
|
|
82
|
+
if (path.startsWith('/api/project-archives'))
|
|
83
|
+
return 'workspace';
|
|
84
|
+
if (path.startsWith('/api/document') || path.startsWith('/api/write') || path.startsWith('/api/manuscript/acceptance'))
|
|
85
|
+
return 'manuscript';
|
|
86
|
+
if (path.startsWith('/api/outline'))
|
|
87
|
+
return 'outline';
|
|
88
|
+
if (path.startsWith('/api/scenes'))
|
|
89
|
+
return 'outline';
|
|
90
|
+
if (path.startsWith('/api/assets'))
|
|
91
|
+
return 'assets';
|
|
92
|
+
if (path.startsWith('/api/tasks') || path.startsWith('/api/review'))
|
|
93
|
+
return 'tasks';
|
|
94
|
+
if (path.startsWith('/api/benchmarks'))
|
|
95
|
+
return 'benchmark';
|
|
96
|
+
if (path.startsWith('/api/model'))
|
|
97
|
+
return 'models';
|
|
98
|
+
if (path.startsWith('/api/research'))
|
|
99
|
+
return 'research';
|
|
100
|
+
if (path.startsWith('/api/revisions'))
|
|
101
|
+
return 'revisions';
|
|
102
|
+
return 'workspace';
|
|
103
|
+
}
|
|
104
|
+
function compactTaskList(bytes, pathPart, method) {
|
|
105
|
+
if (method !== 'GET' || pathPart !== 'tasks')
|
|
106
|
+
return bytes;
|
|
107
|
+
try {
|
|
108
|
+
const envelope = JSON.parse(bytes.toString('utf8'));
|
|
109
|
+
const data = envelope['data'];
|
|
110
|
+
if (data === undefined || !Array.isArray(data['tasks']))
|
|
111
|
+
return bytes;
|
|
112
|
+
const tasks = data['tasks'].map((raw) => {
|
|
113
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
114
|
+
return raw;
|
|
115
|
+
const task = raw;
|
|
116
|
+
const result = task['result'] !== null && typeof task['result'] === 'object' && !Array.isArray(task['result'])
|
|
117
|
+
? task['result']
|
|
118
|
+
: null;
|
|
119
|
+
const reviewV2 = result?.['review_v2'] !== null && typeof result?.['review_v2'] === 'object' && !Array.isArray(result?.['review_v2'])
|
|
120
|
+
? result['review_v2']
|
|
121
|
+
: null;
|
|
122
|
+
const reviewResult = task['type'] === 'chapter_review' && result !== null
|
|
123
|
+
? {
|
|
124
|
+
score: result['score'],
|
|
125
|
+
passed: result['passed'],
|
|
126
|
+
issues: result['issues'],
|
|
127
|
+
summary: result['summary'],
|
|
128
|
+
review_v2: reviewV2 === null ? undefined : {
|
|
129
|
+
schema_version: reviewV2['schema_version'],
|
|
130
|
+
execution_status: reviewV2['execution_status'],
|
|
131
|
+
quality_score: reviewV2['quality_score'],
|
|
132
|
+
coverage: reviewV2['coverage'],
|
|
133
|
+
gate_status: reviewV2['gate_status'],
|
|
134
|
+
delivery_status: reviewV2['delivery_status'],
|
|
135
|
+
production_gate_status: reviewV2['production_gate_status'],
|
|
136
|
+
freshness_status: reviewV2['freshness_status'],
|
|
137
|
+
source_revision: reviewV2['source_revision'],
|
|
138
|
+
current_source_revision: reviewV2['current_source_revision'],
|
|
139
|
+
},
|
|
140
|
+
issue_details: Array.isArray(result['issue_details'])
|
|
141
|
+
? result['issue_details'].map((rawIssue) => {
|
|
142
|
+
const issue = rawIssue !== null && typeof rawIssue === 'object' && !Array.isArray(rawIssue)
|
|
143
|
+
? rawIssue
|
|
144
|
+
: {};
|
|
145
|
+
return {
|
|
146
|
+
severity: issue['severity'],
|
|
147
|
+
review_severity: issue['review_severity'],
|
|
148
|
+
revision_priority: issue['revision_priority'],
|
|
149
|
+
legacy_severity: issue['legacy_severity'],
|
|
150
|
+
dimension: issue['dimension'],
|
|
151
|
+
category: issue['category'],
|
|
152
|
+
summary: issue['summary'] ?? issue['description'],
|
|
153
|
+
};
|
|
154
|
+
})
|
|
155
|
+
: [],
|
|
156
|
+
}
|
|
157
|
+
: null;
|
|
158
|
+
const benchmarkResult = task['type'] === 'model_benchmark' && result !== null
|
|
159
|
+
? {
|
|
160
|
+
run_id: result['run_id'], status: result['status'], artifact_path: result['artifact_path'],
|
|
161
|
+
context_hash: result['context_hash'], summary: result['summary'],
|
|
162
|
+
}
|
|
163
|
+
: null;
|
|
164
|
+
return {
|
|
165
|
+
task_id: task['task_id'],
|
|
166
|
+
type: task['type'],
|
|
167
|
+
status: task['status'],
|
|
168
|
+
phase: task['phase'],
|
|
169
|
+
phase_index: task['phase_index'],
|
|
170
|
+
progress: task['progress'],
|
|
171
|
+
chapter_id: task['chapter_id'],
|
|
172
|
+
input_summary: task['input_summary'],
|
|
173
|
+
error: task['error'],
|
|
174
|
+
retryable: task['retryable'],
|
|
175
|
+
attempt: task['attempt'],
|
|
176
|
+
created_at: task['created_at'],
|
|
177
|
+
updated_at: task['updated_at'],
|
|
178
|
+
started_at: task['started_at'],
|
|
179
|
+
completed_at: task['completed_at'],
|
|
180
|
+
result_ref: task['result_ref'],
|
|
181
|
+
schema_version: task['schema_version'],
|
|
182
|
+
result: reviewResult ?? benchmarkResult,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
return Buffer.from(JSON.stringify({ ...envelope, data: { ...data, tasks } }));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return bytes;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Read a single-valued request header (node lowercases header names). */
|
|
192
|
+
function headerValue(req, name) {
|
|
193
|
+
const value = req.headers?.[name];
|
|
194
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
195
|
+
}
|
|
196
|
+
/** Read the `?workspace=` query parameter of a route URL. */
|
|
197
|
+
function workspaceParam(req) {
|
|
198
|
+
const value = new URL(req.url ?? '/', 'http://localhost').searchParams.get('workspace');
|
|
199
|
+
return value === null || value === '' ? undefined : value;
|
|
200
|
+
}
|
|
201
|
+
/** Single host-side boundary shared by agent tools, browser proxy and live invalidation. */
|
|
202
|
+
export class NovelDomainService extends Service {
|
|
203
|
+
toolOptions;
|
|
204
|
+
client;
|
|
205
|
+
authorizeRequest;
|
|
206
|
+
connect;
|
|
207
|
+
baseUrl;
|
|
208
|
+
resolveWorkspace;
|
|
209
|
+
/** Per-root invalidation state, keyed by canonical workspace root ('' = legacy). */
|
|
210
|
+
roots = new Map();
|
|
211
|
+
/** Open SSE connections bound to the root key they subscribed to. */
|
|
212
|
+
streams = new Map();
|
|
213
|
+
constructor(ctx, options) {
|
|
214
|
+
super(ctx, 'novelDomain');
|
|
215
|
+
this.toolOptions = { timeoutMs: options.timeoutMs, ...(options.outputDir ? { outputDir: options.outputDir } : {}) };
|
|
216
|
+
this.connect = options.connect;
|
|
217
|
+
this.authorizeRequest = options.authorizeRequest;
|
|
218
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
219
|
+
this.resolveWorkspace = options.resolveWorkspace;
|
|
220
|
+
this.client = new StudioClient({
|
|
221
|
+
connect: options.connect,
|
|
222
|
+
baseUrl: this.baseUrl,
|
|
223
|
+
timeoutMs: options.timeoutMs,
|
|
224
|
+
onMutation: (path, context) => this.notifyMutation(path, context),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Factory for the tool surface: every tool execution derives its own
|
|
229
|
+
* request-scoped client from the dsh session context (fail closed).
|
|
230
|
+
*/
|
|
231
|
+
clientFactory() {
|
|
232
|
+
return exec => this.client.scoped(workspaceContextFromExec(exec));
|
|
233
|
+
}
|
|
234
|
+
/** Record a mutation under its root and broadcast only to that root's SSE connections. */
|
|
235
|
+
notifyMutation(path, context) {
|
|
236
|
+
const root = context?.workspaceRoot ?? LEGACY_ROOT_KEY;
|
|
237
|
+
const state = this.roots.get(root) ?? { revision: 0, lastMutation: { revision: 0, resource: 'workspace', path: '' } };
|
|
238
|
+
state.revision += 1;
|
|
239
|
+
state.lastMutation = { revision: state.revision, resource: resourceForPath(path), path };
|
|
240
|
+
this.roots.set(root, state);
|
|
241
|
+
const event = JSON.stringify({ workspace_root: root, ...state.lastMutation });
|
|
242
|
+
for (const [stream, key] of this.streams) {
|
|
243
|
+
if (key === root)
|
|
244
|
+
stream.write(`event: invalidate\ndata: ${event}\n\n`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Snapshot of one root's invalidation state (zero revision when never mutated). */
|
|
248
|
+
snapshotFor(root) {
|
|
249
|
+
const state = this.roots.get(root);
|
|
250
|
+
if (state === undefined)
|
|
251
|
+
return { revision: 0, resource: null, path: null, workspace_root: root };
|
|
252
|
+
return { workspace_root: root, ...state.lastMutation };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Best-effort read of the upstream per-root context epoch (the only signal
|
|
256
|
+
* that background task transitions bump). ANY failure — network, non-2xx,
|
|
257
|
+
* missing/invalid field — degrades to null so the caller omits the field;
|
|
258
|
+
* this helper never throws and never breaks a snapshot.
|
|
259
|
+
*/
|
|
260
|
+
async fetchContextEpoch(root, id, signal) {
|
|
261
|
+
try {
|
|
262
|
+
const timeout = AbortSignal.timeout(CONTEXT_EPOCH_TIMEOUT_MS);
|
|
263
|
+
const payload = await this.client
|
|
264
|
+
.scoped({ workspaceRoot: root, workspaceId: id })
|
|
265
|
+
.getJson('/api/workspace/context', {}, signal === undefined ? timeout : AbortSignal.any([timeout, signal]));
|
|
266
|
+
const epoch = payload?.['context_epoch'];
|
|
267
|
+
return typeof epoch === 'number' && Number.isFinite(epoch) ? epoch : null;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a browser-supplied workspace id through the host registry, failing
|
|
275
|
+
* closed per the contract: missing id → 400 WORKSPACE_CONTEXT_MISSING, no
|
|
276
|
+
* registry → 503 WORKSPACE_REGISTRY_UNAVAILABLE, unknown id → 400 WORKSPACE_UNKNOWN.
|
|
277
|
+
*/
|
|
278
|
+
resolveBrowserWorkspace(id) {
|
|
279
|
+
if (id === undefined) {
|
|
280
|
+
return { rejection: { status: 400, code: 'WORKSPACE_CONTEXT_MISSING', message: 'missing workspace id (x-dsh-workspace-id header or ?workspace= param)' } };
|
|
281
|
+
}
|
|
282
|
+
if (this.resolveWorkspace === undefined) {
|
|
283
|
+
return { rejection: { status: 503, code: 'WORKSPACE_REGISTRY_UNAVAILABLE', message: 'host workspaceRegistry service is not available' } };
|
|
284
|
+
}
|
|
285
|
+
const root = this.resolveWorkspace(id);
|
|
286
|
+
if (root === null) {
|
|
287
|
+
return { rejection: { status: 503, code: 'WORKSPACE_REGISTRY_UNAVAILABLE', message: 'host workspaceRegistry service is not available' } };
|
|
288
|
+
}
|
|
289
|
+
if (root === undefined) {
|
|
290
|
+
return { rejection: { status: 400, code: 'WORKSPACE_UNKNOWN', message: `workspace id is not registered: ${id}` } };
|
|
291
|
+
}
|
|
292
|
+
return { root, id };
|
|
293
|
+
}
|
|
294
|
+
static sendRejection(res, rejection) {
|
|
295
|
+
sendJson(res, rejection.status, { error: rejection.message, code: rejection.code });
|
|
296
|
+
}
|
|
297
|
+
registerWebRoutes(ctx) {
|
|
298
|
+
const register = route => ctx.webServer.register({ ...route, handler: (req, res) => {
|
|
299
|
+
const rejection = this.authorizeRequest?.(req.headers);
|
|
300
|
+
if (rejection !== undefined) {
|
|
301
|
+
sendJson(res, rejection, { error: 'dsh browser authentication required' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
return route.handler(req, res);
|
|
305
|
+
} });
|
|
306
|
+
// These responses belong to this webServer injection scope. Removing a
|
|
307
|
+
// route does not close requests already handled by it; end them explicitly
|
|
308
|
+
// so EventSource reconnects to the newly mounted plugin after a reload.
|
|
309
|
+
const responses = new Set();
|
|
310
|
+
const lifetime = new AbortController();
|
|
311
|
+
let disposed = false;
|
|
312
|
+
ctx.effect(() => () => {
|
|
313
|
+
disposed = true;
|
|
314
|
+
lifetime.abort();
|
|
315
|
+
for (const response of responses) {
|
|
316
|
+
this.streams.delete(response);
|
|
317
|
+
try {
|
|
318
|
+
response.end();
|
|
319
|
+
}
|
|
320
|
+
catch { /* already disconnected */ }
|
|
321
|
+
}
|
|
322
|
+
responses.clear();
|
|
323
|
+
}, 'novel-domain: close live invalidation responses');
|
|
324
|
+
ctx.effect(() => register({
|
|
325
|
+
kind: 'exact',
|
|
326
|
+
path: CONFIG_ROUTE,
|
|
327
|
+
handler: (req, res) => {
|
|
328
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
329
|
+
res.writeHead(405);
|
|
330
|
+
res.end();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-cache' });
|
|
334
|
+
res.end(JSON.stringify({ studioUrl: this.baseUrl }));
|
|
335
|
+
},
|
|
336
|
+
}), 'novel-domain: config route');
|
|
337
|
+
ctx.effect(() => register({
|
|
338
|
+
kind: 'prefix',
|
|
339
|
+
path: API_PROXY_ROUTE,
|
|
340
|
+
handler: this.createProxyHandler(lifetime.signal, responses),
|
|
341
|
+
}), 'novel-domain: API proxy');
|
|
342
|
+
ctx.effect(() => register({
|
|
343
|
+
kind: 'exact',
|
|
344
|
+
path: INVALIDATION_ROUTE,
|
|
345
|
+
handler: async (req, res) => {
|
|
346
|
+
if (disposed) {
|
|
347
|
+
sendJson(res, 503, { error: 'novel-domain is reloading', code: 'NOVEL_DOMAIN_DISPOSED' });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
351
|
+
res.writeHead(405);
|
|
352
|
+
res.end();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const resolved = this.resolveBrowserWorkspace(workspaceParam(req));
|
|
356
|
+
if ('rejection' in resolved) {
|
|
357
|
+
NovelDomainService.sendRejection(res, resolved.rejection);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const snapshot = this.snapshotFor(resolved.root);
|
|
361
|
+
// Merge the upstream context epoch (omitted when unreadable): this is
|
|
362
|
+
// how polling clients observe background task transitions that never
|
|
363
|
+
// touch the bridge-local revision.
|
|
364
|
+
responses.add(res);
|
|
365
|
+
const contextEpoch = await this.fetchContextEpoch(resolved.root, resolved.id, lifetime.signal);
|
|
366
|
+
responses.delete(res);
|
|
367
|
+
if (disposed)
|
|
368
|
+
return;
|
|
369
|
+
sendJson(res, 200, contextEpoch === null ? snapshot : { ...snapshot, context_epoch: contextEpoch });
|
|
370
|
+
},
|
|
371
|
+
}), 'novel-domain: invalidation snapshot');
|
|
372
|
+
ctx.effect(() => register({
|
|
373
|
+
kind: 'exact',
|
|
374
|
+
path: EVENTS_ROUTE,
|
|
375
|
+
handler: async (req, res) => {
|
|
376
|
+
if (disposed) {
|
|
377
|
+
sendJson(res, 503, { error: 'novel-domain is reloading', code: 'NOVEL_DOMAIN_DISPOSED' });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (req.method !== 'GET') {
|
|
381
|
+
res.writeHead(405);
|
|
382
|
+
res.end();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const resolved = this.resolveBrowserWorkspace(workspaceParam(req));
|
|
386
|
+
if ('rejection' in resolved) {
|
|
387
|
+
NovelDomainService.sendRejection(res, resolved.rejection);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
res.writeHead(200, {
|
|
391
|
+
'content-type': 'text/event-stream',
|
|
392
|
+
'cache-control': 'no-cache',
|
|
393
|
+
connection: 'keep-alive',
|
|
394
|
+
});
|
|
395
|
+
responses.add(res);
|
|
396
|
+
this.streams.set(res, resolved.root);
|
|
397
|
+
const dispose = () => { this.streams.delete(res); responses.delete(res); };
|
|
398
|
+
res.on?.('close', dispose);
|
|
399
|
+
res.on?.('error', dispose);
|
|
400
|
+
// The ready event carries the upstream context epoch when it is
|
|
401
|
+
// readable; the stream waits for the fetch to settle (capped at
|
|
402
|
+
// CONTEXT_EPOCH_TIMEOUT_MS), never longer. A client gone by then
|
|
403
|
+
// simply never gets the ready event — polling carries epochs anyway.
|
|
404
|
+
const revision = this.roots.get(resolved.root)?.revision ?? 0;
|
|
405
|
+
const contextEpoch = await this.fetchContextEpoch(resolved.root, resolved.id, lifetime.signal);
|
|
406
|
+
if (disposed || !this.streams.has(res))
|
|
407
|
+
return;
|
|
408
|
+
const ready = { workspace_root: resolved.root, revision };
|
|
409
|
+
if (contextEpoch !== null)
|
|
410
|
+
ready['context_epoch'] = contextEpoch;
|
|
411
|
+
res.write(`event: ready\ndata: ${JSON.stringify(ready)}\n\n`);
|
|
412
|
+
},
|
|
413
|
+
}), 'novel-domain: invalidation stream');
|
|
414
|
+
}
|
|
415
|
+
createProxyHandler(lifetime, responses) {
|
|
416
|
+
return async (req, res) => {
|
|
417
|
+
if (lifetime.aborted) {
|
|
418
|
+
sendJson(res, 503, { error: 'OpenWrite 已停止' });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const sub = (req.url ?? '').slice(API_PROXY_ROUTE.length);
|
|
422
|
+
if (sub === '' || !sub.startsWith('/')) {
|
|
423
|
+
sendJson(res, 404, { error: 'novel-domain proxy: missing API path' });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Every browser request carries only a workspace_id; the trusted host
|
|
427
|
+
// registry resolves it to the canonical root (contract §2.2).
|
|
428
|
+
const resolved = this.resolveBrowserWorkspace(headerValue(req, 'x-dsh-workspace-id'));
|
|
429
|
+
if ('rejection' in resolved) {
|
|
430
|
+
NovelDomainService.sendRejection(res, resolved.rejection);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const method = req.method ?? 'GET';
|
|
434
|
+
const pathPart = sub.split('?')[0]?.slice(1) ?? '';
|
|
435
|
+
const isWrite = method === 'POST' || method === 'PUT';
|
|
436
|
+
if (!isWrite && method !== 'GET' && method !== 'HEAD') {
|
|
437
|
+
sendJson(res, 405, { error: 'novel-domain proxy allows only GET/HEAD and allowlisted writes' });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (isWrite && !WRITABLE_PATHS.has(pathPart) && !WRITABLE_PATH_RE.test(pathPart)) {
|
|
441
|
+
sendJson(res, 405, { error: `novel-domain proxy: write path "${pathPart}" is not allowlisted` });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
responses.add(res);
|
|
445
|
+
try {
|
|
446
|
+
const headers = {
|
|
447
|
+
accept: 'application/json',
|
|
448
|
+
...workspaceRootHeaders(resolved.root, true),
|
|
449
|
+
'x-openwrite-workspace-id': resolved.id,
|
|
450
|
+
};
|
|
451
|
+
const sessionId = headerValue(req, 'x-dsh-session-id');
|
|
452
|
+
if (sessionId !== undefined)
|
|
453
|
+
headers['x-openwrite-session-id'] = sessionId;
|
|
454
|
+
let body;
|
|
455
|
+
if (isWrite) {
|
|
456
|
+
body = await readBody(req);
|
|
457
|
+
headers['content-type'] = 'application/json';
|
|
458
|
+
headers['x-openwrite-studio'] = '1';
|
|
459
|
+
}
|
|
460
|
+
const connection = await this.connect?.();
|
|
461
|
+
if (connection?.token)
|
|
462
|
+
headers['authorization'] = `Bearer ${connection.token}`;
|
|
463
|
+
const upstream = await fetch(new URL(`/api${sub}`, connection?.baseUrl ?? this.baseUrl), {
|
|
464
|
+
method,
|
|
465
|
+
headers,
|
|
466
|
+
...(body !== undefined ? { body: new Uint8Array(body) } : {}),
|
|
467
|
+
signal: AbortSignal.any([lifetime, AbortSignal.timeout(PROXY_TIMEOUT_MS)]),
|
|
468
|
+
});
|
|
469
|
+
if (lifetime.aborted)
|
|
470
|
+
return;
|
|
471
|
+
const upstreamBytes = Buffer.from(await upstream.arrayBuffer());
|
|
472
|
+
const bytes = compactTaskList(upstreamBytes, pathPart, method);
|
|
473
|
+
const responseHeaders = {
|
|
474
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
475
|
+
'cache-control': 'no-cache',
|
|
476
|
+
'content-length': String(bytes.length),
|
|
477
|
+
};
|
|
478
|
+
const disposition = upstream.headers.get('content-disposition');
|
|
479
|
+
if (disposition !== null)
|
|
480
|
+
responseHeaders['content-disposition'] = disposition;
|
|
481
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
482
|
+
res.end(bytes);
|
|
483
|
+
if (isWrite && upstream.ok) {
|
|
484
|
+
this.notifyMutation(`/api/${pathPart}`, { workspaceRoot: resolved.root, workspaceId: resolved.id });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (!lifetime.aborted)
|
|
489
|
+
sendJson(res, 502, { error: '写作环境暂不可用,请打开环境与诊断重试' });
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
responses.delete(res);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
+
export declare const name = "@dsh-novel/openwrite-bridge";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export interface Config {
|
|
6
|
+
mode?: 'managed' | 'external';
|
|
7
|
+
runtimeRoot?: string;
|
|
8
|
+
artifacts?: string;
|
|
9
|
+
/** Base URL of the local OpenWrite Studio server. */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** Per-tool cooperative timeout in ms; chapter writing/review can take minutes. */
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
/** Directory `novel_export` saves exported files into. */
|
|
14
|
+
outputDir: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const Config: Schema<Config>;
|
|
17
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dsh-novel/openwrite-bridge`: a dsh plugin registering `novel_*` tools that
|
|
3
|
+
* call the local OpenWrite Studio HTTP action surface. dsh owns agent
|
|
4
|
+
* orchestration; OpenWrite owns the novel domain logic; this package is a thin
|
|
5
|
+
* bridge over the HTTP contract only.
|
|
6
|
+
*/
|
|
7
|
+
import { tmpdir, homedir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { ManagedRuntime } from './managed-runtime.js';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
12
|
+
import { NovelDomainService } from './domain.js';
|
|
13
|
+
export const name = '@dsh-novel/openwrite-bridge';
|
|
14
|
+
export const inject = [];
|
|
15
|
+
export const Config = Schema.object({
|
|
16
|
+
mode: Schema.union(['managed', 'external']),
|
|
17
|
+
runtimeRoot: Schema.string(),
|
|
18
|
+
artifacts: Schema.string(),
|
|
19
|
+
baseUrl: Schema.string().default('http://127.0.0.1:4567'),
|
|
20
|
+
timeoutMs: Schema.number().default(600000),
|
|
21
|
+
outputDir: Schema.string().default(join(tmpdir(), 'openwrite-exports')),
|
|
22
|
+
});
|
|
23
|
+
export function apply(ctx, config) {
|
|
24
|
+
// The web host process mounts ctx.workspaceRegistry. Resolve it lazily on
|
|
25
|
+
// every call: plugin start order must not strand the lookup, an absent
|
|
26
|
+
// registry (null) fails closed in the domain, and ctx.inject(['webServer',
|
|
27
|
+
// ...]) would silently never fire if we inject-required a second service.
|
|
28
|
+
const resolveWorkspace = (workspaceId) => {
|
|
29
|
+
const registry = ctx.get('workspaceRegistry');
|
|
30
|
+
if (registry === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
return registry.get(workspaceId)?.path;
|
|
33
|
+
};
|
|
34
|
+
// A supplied legacy baseUrl remains external; the standard bundle selects managed explicitly.
|
|
35
|
+
const runtime = config.mode === 'managed' ? new ManagedRuntime(config.runtimeRoot ?? join(process.env['DSH_HOME']?.trim() || join(homedir(), '.dsh'), 'openwrite'), config.artifacts ?? fileURLToPath(new URL('../../../release/', import.meta.url))) : undefined;
|
|
36
|
+
const authorizeRequest = (headers) => {
|
|
37
|
+
const connection = ctx.get('connection');
|
|
38
|
+
return connection ? connection.requestRejection({ headers }) : 503;
|
|
39
|
+
};
|
|
40
|
+
const domain = new NovelDomainService(ctx, { authorizeRequest, baseUrl: config.baseUrl, timeoutMs: config.timeoutMs, outputDir: config.outputDir, resolveWorkspace,
|
|
41
|
+
...(runtime ? { connect: () => runtime.ensure() } : {}),
|
|
42
|
+
});
|
|
43
|
+
ctx.effect(() => () => runtime?.dispose(), 'openwrite: owned runtime cleanup');
|
|
44
|
+
ctx.inject(['webServer'], webCtx => webCtx.effect(() => webCtx.webServer.register({
|
|
45
|
+
kind: 'exact', path: '/studio-panel/runtime', handler: async (req, res) => {
|
|
46
|
+
const respond = (code, data) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)); };
|
|
47
|
+
const rejection = authorizeRequest(req.headers);
|
|
48
|
+
if (rejection !== undefined) {
|
|
49
|
+
respond(rejection, { error: 'dsh browser authentication required' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === 'GET') {
|
|
53
|
+
respond(200, runtime?.status() ?? { phase: 'ready', message: '使用外部写作环境' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (req.method !== 'POST' || req.headers['x-openwrite-studio'] !== '1') {
|
|
57
|
+
respond(405, { error: '操作不允许' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > 1024) {
|
|
65
|
+
respond(413, { error: '请求过大' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
;
|
|
69
|
+
chunks.push(Buffer.from(chunk));
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const action = JSON.parse(Buffer.concat(chunks).toString()).action;
|
|
73
|
+
if (action === 'cancel')
|
|
74
|
+
await runtime?.cancel();
|
|
75
|
+
else if (action === 'prepare' || action === 'retry')
|
|
76
|
+
void runtime?.ensure().catch(() => { });
|
|
77
|
+
else {
|
|
78
|
+
respond(400, { error: '未知操作' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
respond(202, runtime?.status() ?? { phase: 'ready' });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
respond(400, { error: '请求无效' });
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}), 'openwrite: runtime controls'));
|
|
88
|
+
ctx.inject(['webServer'], webCtx => domain.registerWebRoutes(webCtx));
|
|
89
|
+
}
|