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,95 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
interface Download {
|
|
4
|
+
url: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
executable: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RuntimeManifest {
|
|
9
|
+
schema: number;
|
|
10
|
+
dsh: string;
|
|
11
|
+
python_version: string;
|
|
12
|
+
core_version: string;
|
|
13
|
+
contract_version: number;
|
|
14
|
+
platforms: Record<string, {
|
|
15
|
+
uv: Download;
|
|
16
|
+
python: Download;
|
|
17
|
+
}>;
|
|
18
|
+
wheel: {
|
|
19
|
+
file: string;
|
|
20
|
+
sha256: string;
|
|
21
|
+
};
|
|
22
|
+
requirements: {
|
|
23
|
+
file: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
};
|
|
26
|
+
dependency_wheels?: Array<{
|
|
27
|
+
file: string;
|
|
28
|
+
sha256: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export type RuntimePhase = 'idle' | 'waiting' | 'downloading' | 'installing' | 'starting' | 'ready' | 'recovering' | 'cancelled' | 'error' | 'stopped' | 'uninstalled';
|
|
32
|
+
export interface RuntimeStatus {
|
|
33
|
+
phase: RuntimePhase;
|
|
34
|
+
message: string;
|
|
35
|
+
downloadedBytes?: number;
|
|
36
|
+
totalBytes?: number;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface BackendConnection {
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
token?: string;
|
|
42
|
+
}
|
|
43
|
+
export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
44
|
+
export interface RuntimeOptions {
|
|
45
|
+
spawn?: SpawnFn;
|
|
46
|
+
delay?: typeof delay;
|
|
47
|
+
restartLimit?: number;
|
|
48
|
+
backoffMs?: number[];
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_RESTART_LIMIT = 5;
|
|
51
|
+
export declare const DEFAULT_BACKOFF_MS: number[];
|
|
52
|
+
export declare function sha256(file: string): Promise<string>;
|
|
53
|
+
/** Diagnostics must not echo provider credentials or authenticated download URLs. */
|
|
54
|
+
export declare function sanitizeDiagnostic(message: string): string;
|
|
55
|
+
/** Windows needs SystemRoot/PATH to start; proxy and CA vars stay when set. Credential names never copy. */
|
|
56
|
+
export declare function buildChildEnv(source: NodeJS.ProcessEnv, extra?: Record<string, string | undefined>): NodeJS.ProcessEnv;
|
|
57
|
+
/** Stop only a child we spawned. POSIX groups and Windows trees include grandchildren. */
|
|
58
|
+
export declare function stopOwnedProcess(child: ChildProcess): Promise<void>;
|
|
59
|
+
/** All artifacts and children belong to this instance; author files never enter this tree. */
|
|
60
|
+
export declare class ManagedRuntime {
|
|
61
|
+
readonly root: string;
|
|
62
|
+
readonly artifacts: string;
|
|
63
|
+
private state;
|
|
64
|
+
private pending?;
|
|
65
|
+
private controller?;
|
|
66
|
+
private child?;
|
|
67
|
+
private connection?;
|
|
68
|
+
private closed;
|
|
69
|
+
private allowRevive;
|
|
70
|
+
private generation;
|
|
71
|
+
private restartAttempts;
|
|
72
|
+
private recovered;
|
|
73
|
+
private readonly spawnFn;
|
|
74
|
+
private readonly wait;
|
|
75
|
+
private readonly restartLimit;
|
|
76
|
+
private readonly backoffMs;
|
|
77
|
+
constructor(root: string, artifacts: string, options?: RuntimeOptions);
|
|
78
|
+
status(): RuntimeStatus;
|
|
79
|
+
private update;
|
|
80
|
+
ensure(): Promise<BackendConnection>;
|
|
81
|
+
cancel(): Promise<void>;
|
|
82
|
+
dispose(): Promise<void>;
|
|
83
|
+
private exitDiagnostic;
|
|
84
|
+
private handleReadyExit;
|
|
85
|
+
private live;
|
|
86
|
+
private queueRestart;
|
|
87
|
+
private recover;
|
|
88
|
+
private download;
|
|
89
|
+
private extract;
|
|
90
|
+
private command;
|
|
91
|
+
private lock;
|
|
92
|
+
private prepare;
|
|
93
|
+
private start;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { x as untar } from 'tar';
|
|
9
|
+
import { unzipSync } from 'fflate';
|
|
10
|
+
import { lock as acquireLock } from 'proper-lockfile';
|
|
11
|
+
export const DEFAULT_RESTART_LIMIT = 5;
|
|
12
|
+
export const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000];
|
|
13
|
+
const CREDENTIAL_ENV = /(?:API[_-]?KEY|ACCESS[_-]?TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTHORIZATION)/i;
|
|
14
|
+
const ALLOWED_ENV = new Set([
|
|
15
|
+
'PATH', 'Path', 'PATHEXT',
|
|
16
|
+
'HOME', 'USER', 'USERNAME', 'LOGNAME',
|
|
17
|
+
'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
|
|
18
|
+
'TEMP', 'TMP', 'TMPDIR',
|
|
19
|
+
'SYSTEMROOT', 'SystemRoot', 'WINDIR', 'windir', 'COMSPEC', 'ComSpec',
|
|
20
|
+
'SystemDrive', 'PROGRAMDATA', 'ProgramData', 'ProgramFiles', 'PROGRAMFILES', 'ProgramFiles(x86)',
|
|
21
|
+
'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'OS',
|
|
22
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LANGUAGE', 'TZ',
|
|
23
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'all_proxy',
|
|
24
|
+
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE',
|
|
25
|
+
]);
|
|
26
|
+
export async function sha256(file) {
|
|
27
|
+
const hash = createHash('sha256');
|
|
28
|
+
for await (const chunk of createReadStream(file))
|
|
29
|
+
hash.update(chunk);
|
|
30
|
+
return hash.digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/** Diagnostics must not echo provider credentials or authenticated download URLs. */
|
|
33
|
+
export function sanitizeDiagnostic(message) {
|
|
34
|
+
return message.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[redacted]@')
|
|
35
|
+
.replace(/([?&](?:token|key|signature|credential|password|secret)=)[^&\s]+/gi, '$1[redacted]')
|
|
36
|
+
.replace(/((?:api[_-]?key|authorization|password|secret|token)\s*[:=]\s*)[^\s,;]+/gi, '$1[redacted]')
|
|
37
|
+
.replace(/\b[a-f0-9]{32,}\b/gi, '[redacted]');
|
|
38
|
+
}
|
|
39
|
+
/** Windows needs SystemRoot/PATH to start; proxy and CA vars stay when set. Credential names never copy. */
|
|
40
|
+
export function buildChildEnv(source, extra = {}) {
|
|
41
|
+
const env = {};
|
|
42
|
+
for (const [key, value] of Object.entries(source)) {
|
|
43
|
+
if (value === undefined || CREDENTIAL_ENV.test(key))
|
|
44
|
+
continue;
|
|
45
|
+
const allowed = ALLOWED_ENV.has(key)
|
|
46
|
+
|| (process.platform === 'win32' && [...ALLOWED_ENV].some(name => name.toLowerCase() === key.toLowerCase()));
|
|
47
|
+
if (allowed)
|
|
48
|
+
env[key] = value;
|
|
49
|
+
}
|
|
50
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
51
|
+
if (value === undefined || CREDENTIAL_ENV.test(key))
|
|
52
|
+
continue;
|
|
53
|
+
env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
57
|
+
/** Stop only a child we spawned. POSIX groups and Windows trees include grandchildren. */
|
|
58
|
+
export async function stopOwnedProcess(child) {
|
|
59
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
60
|
+
return;
|
|
61
|
+
const exited = new Promise(done => child.once('exit', () => done()));
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
await new Promise(done => {
|
|
64
|
+
const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
65
|
+
killer.once('error', () => { child.kill(); done(); });
|
|
66
|
+
killer.once('exit', () => done());
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
try {
|
|
71
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
child.kill('SIGTERM');
|
|
75
|
+
}
|
|
76
|
+
await Promise.race([exited, delay(3000)]);
|
|
77
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
child.kill('SIGKILL');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await exited;
|
|
87
|
+
}
|
|
88
|
+
/** All artifacts and children belong to this instance; author files never enter this tree. */
|
|
89
|
+
export class ManagedRuntime {
|
|
90
|
+
root;
|
|
91
|
+
artifacts;
|
|
92
|
+
state = { phase: 'idle', message: '首次使用将准备写作环境' };
|
|
93
|
+
pending;
|
|
94
|
+
controller;
|
|
95
|
+
child;
|
|
96
|
+
connection;
|
|
97
|
+
closed = false;
|
|
98
|
+
allowRevive = true;
|
|
99
|
+
generation = 0;
|
|
100
|
+
restartAttempts = 0;
|
|
101
|
+
recovered = false;
|
|
102
|
+
spawnFn;
|
|
103
|
+
wait;
|
|
104
|
+
restartLimit;
|
|
105
|
+
backoffMs;
|
|
106
|
+
constructor(root, artifacts, options = {}) {
|
|
107
|
+
this.root = root;
|
|
108
|
+
this.artifacts = artifacts;
|
|
109
|
+
this.spawnFn = options.spawn ?? spawn;
|
|
110
|
+
this.wait = options.delay ?? delay;
|
|
111
|
+
this.restartLimit = options.restartLimit ?? DEFAULT_RESTART_LIMIT;
|
|
112
|
+
this.backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
113
|
+
}
|
|
114
|
+
status() { return { ...this.state }; }
|
|
115
|
+
update(phase, message, extra = {}) {
|
|
116
|
+
this.state = { phase, message, ...extra };
|
|
117
|
+
}
|
|
118
|
+
ensure() {
|
|
119
|
+
if (this.closed)
|
|
120
|
+
return Promise.reject(new Error('OpenWrite 已卸载'));
|
|
121
|
+
if (this.connection)
|
|
122
|
+
return Promise.resolve(this.connection);
|
|
123
|
+
if (this.pending)
|
|
124
|
+
return this.pending;
|
|
125
|
+
this.allowRevive = true;
|
|
126
|
+
if (this.state.phase === 'error' || this.state.phase === 'cancelled' || this.state.phase === 'stopped') {
|
|
127
|
+
this.restartAttempts = 0;
|
|
128
|
+
this.recovered = false;
|
|
129
|
+
}
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
this.controller = controller;
|
|
132
|
+
this.pending = this.prepare(controller.signal).catch(error => {
|
|
133
|
+
this.state = { phase: controller.signal.aborted ? 'cancelled' : 'error',
|
|
134
|
+
message: controller.signal.aborted ? '准备已取消,可重试' : '写作环境未就绪,可重试',
|
|
135
|
+
error: controller.signal.aborted ? undefined : sanitizeDiagnostic(String(error instanceof Error ? error.message : error)) };
|
|
136
|
+
throw error;
|
|
137
|
+
}).finally(() => { this.pending = undefined; this.controller = undefined; });
|
|
138
|
+
return this.pending;
|
|
139
|
+
}
|
|
140
|
+
async cancel() {
|
|
141
|
+
this.allowRevive = false;
|
|
142
|
+
this.controller?.abort();
|
|
143
|
+
await this.pending?.catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
async dispose() {
|
|
146
|
+
this.closed = true;
|
|
147
|
+
this.allowRevive = false;
|
|
148
|
+
this.generation += 1;
|
|
149
|
+
await this.cancel();
|
|
150
|
+
if (this.child) {
|
|
151
|
+
this.child.stdin?.end();
|
|
152
|
+
await stopOwnedProcess(this.child);
|
|
153
|
+
this.child = undefined;
|
|
154
|
+
}
|
|
155
|
+
this.connection = undefined;
|
|
156
|
+
this.update('uninstalled', 'OpenWrite 已卸载,作品和配置已保留');
|
|
157
|
+
}
|
|
158
|
+
exitDiagnostic(code, signalName, stderr = '') {
|
|
159
|
+
return sanitizeDiagnostic(`exit ${code ?? 'none'} signal ${signalName ?? 'none'}${stderr ? `: ${stderr}` : ''}`);
|
|
160
|
+
}
|
|
161
|
+
handleReadyExit(generation, child, code, signalName, stderr) {
|
|
162
|
+
if (this.generation !== generation)
|
|
163
|
+
return;
|
|
164
|
+
if (this.child === child)
|
|
165
|
+
this.child = undefined;
|
|
166
|
+
this.connection = undefined;
|
|
167
|
+
if (this.closed)
|
|
168
|
+
return;
|
|
169
|
+
const error = this.exitDiagnostic(code, signalName, stderr);
|
|
170
|
+
if (!this.allowRevive) {
|
|
171
|
+
this.update('cancelled', '准备已取消,可重试', { error });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.restartAttempts += 1;
|
|
175
|
+
if (this.restartAttempts > this.restartLimit) {
|
|
176
|
+
this.allowRevive = false;
|
|
177
|
+
this.update('error', '写作后端多次异常退出,已停止自动恢复,可手动重试', { error });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.recovered = true;
|
|
181
|
+
this.update('recovering', `写作后端异常退出,正在恢复(${this.restartAttempts}/${this.restartLimit})`, { error });
|
|
182
|
+
// Auto-recovery has no caller; never let a rejected prepare become an unhandled rejection that kills dsh.
|
|
183
|
+
void this.queueRestart().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
live() { return Boolean(this.connection && this.child); }
|
|
186
|
+
queueRestart() {
|
|
187
|
+
if (this.pending) {
|
|
188
|
+
const prior = this.pending;
|
|
189
|
+
let assigned;
|
|
190
|
+
const chained = prior.then(connection => {
|
|
191
|
+
if (this.closed)
|
|
192
|
+
throw new Error('OpenWrite 已卸载');
|
|
193
|
+
if (this.live())
|
|
194
|
+
return connection;
|
|
195
|
+
if (!this.allowRevive)
|
|
196
|
+
throw new Error('准备已取消,可重试');
|
|
197
|
+
if (this.pending === assigned)
|
|
198
|
+
this.pending = undefined;
|
|
199
|
+
return this.queueRestart();
|
|
200
|
+
}, error => {
|
|
201
|
+
if (this.closed || !this.allowRevive)
|
|
202
|
+
throw error;
|
|
203
|
+
if (this.live())
|
|
204
|
+
return this.connection;
|
|
205
|
+
if (this.pending === assigned)
|
|
206
|
+
this.pending = undefined;
|
|
207
|
+
return this.queueRestart();
|
|
208
|
+
});
|
|
209
|
+
assigned = chained.finally(() => {
|
|
210
|
+
if (this.pending === assigned)
|
|
211
|
+
this.pending = undefined;
|
|
212
|
+
});
|
|
213
|
+
this.pending = assigned;
|
|
214
|
+
return assigned;
|
|
215
|
+
}
|
|
216
|
+
const task = this.recover();
|
|
217
|
+
const assigned = task.finally(() => {
|
|
218
|
+
if (this.pending === assigned) {
|
|
219
|
+
this.pending = undefined;
|
|
220
|
+
this.controller = undefined;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
this.pending = assigned;
|
|
224
|
+
return assigned;
|
|
225
|
+
}
|
|
226
|
+
async recover() {
|
|
227
|
+
let lastError;
|
|
228
|
+
while (!this.closed && this.allowRevive) {
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
this.controller = controller;
|
|
231
|
+
const wait = this.backoffMs[Math.min(Math.max(this.restartAttempts, 1) - 1, this.backoffMs.length - 1)] ?? 1_000;
|
|
232
|
+
try {
|
|
233
|
+
await this.wait(wait, undefined, { signal: controller.signal });
|
|
234
|
+
if (this.closed || !this.allowRevive)
|
|
235
|
+
break;
|
|
236
|
+
return await this.prepare(controller.signal);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
lastError = error;
|
|
240
|
+
if (this.closed || !this.allowRevive || controller.signal.aborted)
|
|
241
|
+
break;
|
|
242
|
+
this.restartAttempts += 1;
|
|
243
|
+
const diagnostic = sanitizeDiagnostic(String(error instanceof Error ? error.message : error));
|
|
244
|
+
if (this.restartAttempts > this.restartLimit) {
|
|
245
|
+
this.allowRevive = false;
|
|
246
|
+
this.update('error', '写作后端多次异常退出,已停止自动恢复,可手动重试', { error: diagnostic });
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
this.update('recovering', `写作后端异常退出,正在恢复(${this.restartAttempts}/${this.restartLimit})`, { error: diagnostic });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (this.closed)
|
|
253
|
+
throw new Error('OpenWrite 已卸载');
|
|
254
|
+
this.update('cancelled', '准备已取消,可重试');
|
|
255
|
+
throw lastError instanceof Error ? lastError : new Error('准备已取消,可重试');
|
|
256
|
+
}
|
|
257
|
+
async download(item, signal) {
|
|
258
|
+
const cache = join(this.root, 'cache');
|
|
259
|
+
await mkdir(cache, { recursive: true });
|
|
260
|
+
const destination = join(cache, item.sha256);
|
|
261
|
+
if (await sha256(destination).catch(() => '') === item.sha256)
|
|
262
|
+
return destination;
|
|
263
|
+
const partial = destination + '.' + randomUUID() + '.partial';
|
|
264
|
+
this.update('downloading', '正在下载并校验写作环境');
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(item.url, { signal: AbortSignal.any([signal, AbortSignal.timeout(600_000)]) });
|
|
267
|
+
if (!response.ok || !response.body)
|
|
268
|
+
throw new Error(`下载失败:HTTP ${response.status}`);
|
|
269
|
+
const size = Number(response.headers.get('content-length')) || undefined;
|
|
270
|
+
const file = await open(partial, 'wx', 0o600);
|
|
271
|
+
let received = 0;
|
|
272
|
+
try {
|
|
273
|
+
for await (const chunk of response.body) {
|
|
274
|
+
signal.throwIfAborted();
|
|
275
|
+
received += chunk.length;
|
|
276
|
+
if (received > 1024 ** 3)
|
|
277
|
+
throw new Error('下载超过允许大小');
|
|
278
|
+
await file.write(chunk);
|
|
279
|
+
this.state = { phase: 'downloading', message: '正在下载并校验写作环境', downloadedBytes: received, totalBytes: size };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
await file.close();
|
|
284
|
+
}
|
|
285
|
+
if (await sha256(partial) !== item.sha256)
|
|
286
|
+
throw new Error('下载校验失败,请重试');
|
|
287
|
+
await rename(partial, destination);
|
|
288
|
+
return destination;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await rm(partial, { force: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async extract(item, signal) {
|
|
295
|
+
const dir = join(this.root, 'binaries', item.sha256);
|
|
296
|
+
const executable = join(dir, item.executable);
|
|
297
|
+
const marker = join(dir, '.complete');
|
|
298
|
+
if (await readFile(marker, 'utf8').catch(() => '') === item.sha256)
|
|
299
|
+
return executable;
|
|
300
|
+
const archive = await this.download(item, signal);
|
|
301
|
+
await rm(dir, { recursive: true, force: true });
|
|
302
|
+
await mkdir(dir, { recursive: true });
|
|
303
|
+
if (item.url.endsWith('.zip')) {
|
|
304
|
+
for (const [name, bytes] of Object.entries(unzipSync(await readFile(archive)))) {
|
|
305
|
+
const target = resolve(dir, name);
|
|
306
|
+
if (isAbsolute(name) || !target.startsWith(dir + sep))
|
|
307
|
+
throw new Error('压缩包路径无效');
|
|
308
|
+
if (name.endsWith('/'))
|
|
309
|
+
await mkdir(target, { recursive: true });
|
|
310
|
+
else {
|
|
311
|
+
await mkdir(dirname(target), { recursive: true });
|
|
312
|
+
await writeFile(target, bytes);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else
|
|
317
|
+
await untar({ file: archive, cwd: dir, strict: true, preservePaths: false });
|
|
318
|
+
signal.throwIfAborted();
|
|
319
|
+
if (process.platform !== 'win32')
|
|
320
|
+
await chmod(executable, 0o755);
|
|
321
|
+
await writeFile(marker, item.sha256);
|
|
322
|
+
return executable;
|
|
323
|
+
}
|
|
324
|
+
async command(executable, args, signal) {
|
|
325
|
+
signal.throwIfAborted();
|
|
326
|
+
const child = this.spawnFn(executable, args, { cwd: this.root, detached: process.platform !== 'win32', windowsHide: true,
|
|
327
|
+
env: buildChildEnv(process.env, { UV_CACHE_DIR: join(this.root, 'cache', 'uv'), UV_PYTHON_DOWNLOADS: 'never', UV_NO_CONFIG: '1' }),
|
|
328
|
+
stdio: ['ignore', 'ignore', 'pipe'] });
|
|
329
|
+
let failure = '';
|
|
330
|
+
child.stderr?.on('data', bytes => { failure = (failure + String(bytes)).slice(-2000); });
|
|
331
|
+
const abort = () => { void stopOwnedProcess(child); };
|
|
332
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
333
|
+
try {
|
|
334
|
+
await new Promise((done, reject) => {
|
|
335
|
+
child.once('error', reject);
|
|
336
|
+
child.once('exit', code => code === 0 ? done() : reject(new Error(`环境准备失败 (${code}):${failure}`)));
|
|
337
|
+
});
|
|
338
|
+
signal.throwIfAborted();
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
signal.removeEventListener('abort', abort);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async lock(signal) {
|
|
345
|
+
for (;;) {
|
|
346
|
+
signal.throwIfAborted();
|
|
347
|
+
try {
|
|
348
|
+
return await acquireLock(this.root, {
|
|
349
|
+
realpath: false, stale: 30_000, update: 5_000, retries: 0,
|
|
350
|
+
onCompromised: () => this.controller?.abort(),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (error.code !== 'ELOCKED')
|
|
355
|
+
throw error;
|
|
356
|
+
this.update('waiting', '另一个 OpenWrite 正在准备环境,等待完成');
|
|
357
|
+
await this.wait(250, undefined, { signal });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async prepare(signal) {
|
|
362
|
+
const manifest = JSON.parse(await readFile(join(this.artifacts, 'runtime-manifest.json'), 'utf8'));
|
|
363
|
+
const platform = manifest.platforms[`${process.platform}-${process.arch}`];
|
|
364
|
+
if (!platform)
|
|
365
|
+
throw new Error(`暂不支持 ${process.platform}/${process.arch}`);
|
|
366
|
+
if (!manifest.wheel || !manifest.requirements)
|
|
367
|
+
throw new Error('发布包缺少 Core 或依赖校验清单');
|
|
368
|
+
const wheel = join(this.artifacts, manifest.wheel.file);
|
|
369
|
+
const requirements = join(this.artifacts, manifest.requirements.file);
|
|
370
|
+
if (await sha256(wheel) !== manifest.wheel.sha256 || await sha256(requirements) !== manifest.requirements.sha256) {
|
|
371
|
+
throw new Error('发布包校验失败,请重新安装');
|
|
372
|
+
}
|
|
373
|
+
for (const artifact of manifest.dependency_wheels ?? []) {
|
|
374
|
+
if (await sha256(join(this.artifacts, artifact.file)) !== artifact.sha256)
|
|
375
|
+
throw new Error('平台依赖校验失败,请重新安装');
|
|
376
|
+
}
|
|
377
|
+
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
378
|
+
const unlock = await this.lock(signal);
|
|
379
|
+
let python;
|
|
380
|
+
try {
|
|
381
|
+
const generation = createHash('sha256').update(JSON.stringify(manifest)).digest('hex').slice(0, 24);
|
|
382
|
+
const destination = join(this.root, 'environments', generation);
|
|
383
|
+
python = join(destination, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
|
|
384
|
+
if (await readFile(join(destination, '.complete'), 'utf8').catch(() => '') !== generation) {
|
|
385
|
+
const uv = await this.extract(platform.uv, signal);
|
|
386
|
+
const basePython = await this.extract(platform.python, signal);
|
|
387
|
+
this.update('installing', '正在安装隔离依赖,首次使用可能需要几分钟');
|
|
388
|
+
await rm(destination, { recursive: true, force: true });
|
|
389
|
+
try {
|
|
390
|
+
await this.command(uv, ['venv', '--python', basePython, destination], signal);
|
|
391
|
+
await this.command(uv, ['pip', 'sync', '--python', python, '--require-hashes', '--only-binary', ':all:',
|
|
392
|
+
...(manifest.dependency_wheels?.length ? ['--find-links', join(this.artifacts, 'wheels')] : []), requirements], signal);
|
|
393
|
+
await this.command(uv, ['pip', 'install', '--python', python, '--no-deps', wheel], signal);
|
|
394
|
+
await this.command(python, ['-I', '-c', 'from tools.studio_http import health_payload; from tools.studio import create_server; assert health_payload()["contract_version"] == 1'], signal);
|
|
395
|
+
await writeFile(join(destination, '.complete'), generation);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
await rm(destination, { recursive: true, force: true });
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
signal.throwIfAborted();
|
|
403
|
+
this.update(this.recovered ? 'recovering' : 'starting', this.recovered ? '正在恢复写作环境' : '正在启动写作环境');
|
|
404
|
+
const connection = await this.start(python, manifest, signal);
|
|
405
|
+
const pending = join(this.root, 'active.' + randomUUID() + '.json');
|
|
406
|
+
try {
|
|
407
|
+
await writeFile(pending, JSON.stringify({ generation, coreVersion: manifest.core_version }));
|
|
408
|
+
await rename(pending, join(this.root, 'active.json'));
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.connection = undefined;
|
|
412
|
+
if (this.child)
|
|
413
|
+
await stopOwnedProcess(this.child);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
await rm(pending, { force: true });
|
|
418
|
+
}
|
|
419
|
+
return connection;
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await unlock();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async start(python, manifest, signal) {
|
|
426
|
+
signal.throwIfAborted();
|
|
427
|
+
const token = randomBytes(32).toString('hex');
|
|
428
|
+
const generation = ++this.generation;
|
|
429
|
+
// Windows defaults redirected stdout to its legacy locale encoding. Core
|
|
430
|
+
// initialization logs and manuscript paths are Unicode; -I ignores Python
|
|
431
|
+
// environment options, so set UTF-8 explicitly on the interpreter as well.
|
|
432
|
+
const child = this.spawnFn(python, ['-I', '-X', 'utf8', '-u', '-m', 'tools.managed_runtime'], {
|
|
433
|
+
cwd: this.root, detached: process.platform !== 'win32', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
434
|
+
env: buildChildEnv(process.env, { PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' }),
|
|
435
|
+
});
|
|
436
|
+
this.child = child;
|
|
437
|
+
let stderrTail = '';
|
|
438
|
+
child.stderr?.on('data', bytes => { stderrTail = (stderrTail + String(bytes)).slice(-500); });
|
|
439
|
+
const lines = createInterface({ input: child.stdout });
|
|
440
|
+
let timer;
|
|
441
|
+
let started = false;
|
|
442
|
+
child.once('exit', (code, signalName) => {
|
|
443
|
+
if (!started)
|
|
444
|
+
return;
|
|
445
|
+
this.handleReadyExit(generation, child, code, signalName, stderrTail);
|
|
446
|
+
});
|
|
447
|
+
try {
|
|
448
|
+
const ready = new Promise((done, reject) => {
|
|
449
|
+
child.once('error', reject);
|
|
450
|
+
child.once('exit', (code, signalName) => {
|
|
451
|
+
if (started)
|
|
452
|
+
return;
|
|
453
|
+
reject(new Error(`写作后端在启动时退出 (${code ?? 'none'}/${signalName ?? 'none'})`));
|
|
454
|
+
});
|
|
455
|
+
timer = setTimeout(() => reject(new Error('写作后端启动超时')), 120_000);
|
|
456
|
+
lines.on('line', line => {
|
|
457
|
+
try {
|
|
458
|
+
const message = JSON.parse(line);
|
|
459
|
+
if (Number.isInteger(message.port) && message.port > 0 && message.port < 65536)
|
|
460
|
+
done(message.port);
|
|
461
|
+
}
|
|
462
|
+
catch { /* non-protocol startup logging */ }
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
const abort = () => { void stopOwnedProcess(child); };
|
|
466
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
467
|
+
let port;
|
|
468
|
+
try {
|
|
469
|
+
child.stdin.write(JSON.stringify({ token, state_dir: join(this.root, 'state') }) + '\n');
|
|
470
|
+
port = await ready;
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
signal.removeEventListener('abort', abort);
|
|
474
|
+
}
|
|
475
|
+
signal.throwIfAborted();
|
|
476
|
+
const connection = { baseUrl: `http://127.0.0.1:${port}`, token };
|
|
477
|
+
const health = await fetch(connection.baseUrl + '/api/health', {
|
|
478
|
+
headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
479
|
+
});
|
|
480
|
+
const result = await health.json();
|
|
481
|
+
if (!health.ok || result.core_version !== manifest.core_version || result.contract_version !== manifest.contract_version) {
|
|
482
|
+
throw new Error('写作后端版本或接口不匹配,请重新安装');
|
|
483
|
+
}
|
|
484
|
+
this.connection = connection;
|
|
485
|
+
started = true;
|
|
486
|
+
if (!this.recovered)
|
|
487
|
+
this.restartAttempts = 0;
|
|
488
|
+
this.update('ready', this.recovered
|
|
489
|
+
? '写作环境已恢复;进行中的写作或评审任务不会自动重试'
|
|
490
|
+
: '写作环境已就绪');
|
|
491
|
+
return connection;
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
if (this.generation === generation) {
|
|
495
|
+
this.generation += 1;
|
|
496
|
+
this.connection = undefined;
|
|
497
|
+
if (this.child === child)
|
|
498
|
+
this.child = undefined;
|
|
499
|
+
}
|
|
500
|
+
await stopOwnedProcess(child);
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
if (timer)
|
|
505
|
+
clearTimeout(timer);
|
|
506
|
+
lines.close();
|
|
507
|
+
child.stdout?.resume();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
export declare const name = "@dsh-novel/openwrite-tools";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
6
|
+
presentation: z<"native" | "ptc" | "both", "native" | "ptc" | "both">;
|
|
7
|
+
}>, Schemastery.ObjectT<{
|
|
8
|
+
presentation: z<"native" | "ptc" | "both", "native" | "ptc" | "both">;
|
|
9
|
+
}>>;
|
|
10
|
+
/** This row belongs in an Agent preset, never in the host bundle. */
|
|
11
|
+
export declare function apply(ctx: Context, config?: {
|
|
12
|
+
presentation?: 'native' | 'ptc' | 'both';
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { registerNovelTools } from './tools.js';
|
|
5
|
+
import { novelPolicyDenial } from './tool-policy.js';
|
|
6
|
+
export const name = '@dsh-novel/openwrite-tools';
|
|
7
|
+
export const inject = ['novelDomain', 'tools'];
|
|
8
|
+
export const Config = z.object({ presentation: z.union(['native', 'ptc', 'both']).default('native') });
|
|
9
|
+
/** This row belongs in an Agent preset, never in the host bundle. */
|
|
10
|
+
export function apply(ctx, config = {}) {
|
|
11
|
+
if (config.presentation && config.presentation !== 'native')
|
|
12
|
+
ctx.effect(() => ctx.tools.presentAs(config.presentation), 'openwrite: tool presentation');
|
|
13
|
+
ctx.effect(() => ctx.tools.guard(execution => novelPolicyDenial(ctx, execution)), 'openwrite: execution policy');
|
|
14
|
+
registerNovelTools(ctx, ctx.novelDomain.clientFactory(), {
|
|
15
|
+
timeoutMs: ctx.novelDomain.toolOptions.timeoutMs,
|
|
16
|
+
outputDir: ctx.novelDomain.toolOptions.outputDir ?? join(tmpdir(), 'openwrite-exports'),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
export type NovelEffect = 'read' | 'write' | 'generate';
|
|
4
|
+
/** Unknown and preview operations fail closed: previews can persist confirmation records. */
|
|
5
|
+
export declare function novelEffect(name: string, args: unknown): NovelEffect;
|
|
6
|
+
export declare function novelPolicyDenial(ctx: Context, execution: Readonly<ToolExecution>): string | undefined;
|