@citeark/agent 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. package/ui/arkgraph/style.css +272 -0
@@ -0,0 +1,810 @@
1
+ import { hasSessionEvents, createRuntimeStream } from '../terminal/events.mjs';
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, mkdir, readFile, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ import { buildCheckpointedWorkerScript } from "./gcp-checkpoint-worker.mjs";
7
+ import {
8
+ resolveAutoDlProviderEnvironment,
9
+ shellEnvironmentFile,
10
+ } from "./autodl-pro-job.mjs";
11
+ import {
12
+ autoDlRoutingProxySource,
13
+ buildAutoDlNetworkRunnerScript,
14
+ resolveAutoDlManagedNetwork,
15
+ } from "./autodl-pro-network.mjs";
16
+ import { createAutoDlGcsStageTransfer } from "./autodl-pro-staging.mjs";
17
+ import { captureProcess } from "../process.mjs";
18
+ import { createRemoteStageArchive } from "./stage-archive.mjs";
19
+ import {
20
+ CiteArkError,
21
+ pathExists,
22
+ readJson,
23
+ sha256File,
24
+ shellQuote,
25
+ } from "../util.mjs";
26
+
27
+ export async function executeAutoDlRemoteAttempt({
28
+ bundle,
29
+ lease,
30
+ config,
31
+ invocation,
32
+ resumeInvocation,
33
+ attempt,
34
+ stageArchive,
35
+ resultArchive,
36
+ checkpointPolicy,
37
+ restoreCheckpointOnStart = false,
38
+ reuseCompletedResultOnRestore = true,
39
+ completionRelativePath = "output/result.json",
40
+ completionHasEvidence = true,
41
+ secretManager,
42
+ stageTransfer,
43
+ stageClients,
44
+ assertActive = () => {},
45
+ progress = () => {},
46
+ onCheckpoint = () => {},
47
+ }) {
48
+ const safeRunId = safeIdentifier(bundle.runId);
49
+ const remoteDirectory = `/root/citeark/${safeRunId}`;
50
+ const remoteStage = `${remoteDirectory}/stage-a${attempt}.tar.gz`;
51
+ const remoteResult = `${remoteDirectory}/result-a${attempt}.tar.gz`;
52
+ const externalPrefix = `${remoteDirectory}/result-a${attempt}-object`;
53
+ const checkpointPrefix = `${remoteDirectory}/checkpoint-a1`;
54
+ const checkpointStatus = `${checkpointPrefix}-status.json`;
55
+ const remoteScript = `${remoteDirectory}/worker-a${attempt}.sh`;
56
+ const remoteEnvironment = `${remoteDirectory}/environment-a${attempt}.sh`;
57
+ const remoteNetworkRunner = `${remoteDirectory}/network-runner-a${attempt}.sh`;
58
+ const remoteRouter = `${remoteDirectory}/network-router-a${attempt}.mjs`;
59
+ const remoteTunnelKey = `${remoteDirectory}/liguse-tunnel-a${attempt}`;
60
+ const remoteKnownHosts = `${remoteDirectory}/liguse-known-hosts-a${attempt}`;
61
+ const remoteProviderRelay = `${remoteDirectory}/provider-pro-relay-a${attempt}.mjs`;
62
+ const providerRelay = bundle.runtimeAgent.reasoningMode === "pro";
63
+ const environment = await resolveAutoDlProviderEnvironment({
64
+ agent: bundle.runtimeAgent,
65
+ providerSecret: config.providerSecret,
66
+ credentialSource: config.credentialSource,
67
+ allowRemoteModelKey: config.allowRemoteModelKey,
68
+ secretManager,
69
+ });
70
+ const managedNetwork = await resolveAutoDlManagedNetwork({ config, secretManager });
71
+ const workerScript = buildCheckpointedWorkerScript({
72
+ runId: bundle.runId,
73
+ executor: "autodl-pro",
74
+ initialInvocation: invocation,
75
+ resumeInvocation,
76
+ attempt,
77
+ stagePath: remoteStage,
78
+ resultPath: remoteResult,
79
+ streamTrace: hasSessionEvents(),
80
+ checkpointPrefixPath: checkpointPrefix,
81
+ checkpointStatusPath: checkpointStatus,
82
+ externalObjectPrefixPath: externalPrefix,
83
+ restoreCheckpointPrefixPath: checkpointPrefix,
84
+ checkpointPolicy,
85
+ completionRelativePath,
86
+ completionHasEvidence,
87
+ providerRelay,
88
+ providerRelayPath: remoteProviderRelay,
89
+ restoreCheckpointOnStart,
90
+ reuseCompletedResultOnRestore,
91
+ });
92
+
93
+ await assertActive();
94
+ await assertRemotePath(remoteDirectory);
95
+ const prepared = await lease.ssh.exec(
96
+ `rm -rf /job && install -d -m 0711 /job ${shellQuote(remoteDirectory)}`,
97
+ { timeoutMs: 60_000 },
98
+ );
99
+ if (prepared.code !== 0) {
100
+ throw new CiteArkError(`无法准备 AutoDL 远程目录:${prepared.stderr.trim()}`);
101
+ }
102
+ const stageUpload = {
103
+ finalPath: remoteStage,
104
+ uploadPath: `${remoteStage}.uploading`,
105
+ localPath: stageArchive,
106
+ byteSize: (await lstat(stageArchive)).size,
107
+ digest: await sha256File(stageArchive),
108
+ };
109
+ progress(
110
+ `AutoDL ${bundle.runId}: 准备第 ${attempt} 次运行输入(${formatMiB(stageUpload.byteSize)} MiB)`,
111
+ );
112
+ const resolvedStageTransfer = stageTransfer ?? createAutoDlGcsStageTransfer({
113
+ config,
114
+ clients: stageClients,
115
+ });
116
+ const stageTransferRecord = await transferAutoDlStageArchive({
117
+ ssh: lease.ssh,
118
+ transfer: resolvedStageTransfer,
119
+ runId: bundle.runId,
120
+ attempt,
121
+ stageUpload,
122
+ progress,
123
+ });
124
+ const controlUploads = [
125
+ textUpload(remoteScript, `${workerScript}\n`, 0o700),
126
+ ...(providerRelay ? [textUpload(
127
+ remoteProviderRelay,
128
+ await readFile(
129
+ new URL("../../docker/claude-code/codex-pro-relay.mjs", import.meta.url),
130
+ "utf8",
131
+ ),
132
+ 0o700,
133
+ )] : []),
134
+ ...(managedNetwork ? [
135
+ textUpload(remoteRouter, await autoDlRoutingProxySource(), 0o700),
136
+ textUpload(remoteTunnelKey, `${managedNetwork.privateKey}\n`, 0o600),
137
+ textUpload(remoteKnownHosts, `${managedNetwork.knownHosts}\n`, 0o600),
138
+ textUpload(
139
+ remoteNetworkRunner,
140
+ buildAutoDlNetworkRunnerScript({
141
+ workerPath: remoteScript,
142
+ routerPath: remoteRouter,
143
+ keyPath: remoteTunnelKey,
144
+ knownHostsPath: remoteKnownHosts,
145
+ turboScript: config.networkTurboScript,
146
+ tunnel: managedNetwork,
147
+ providerBaseUrl: bundle.runtimeAgent?.api?.baseUrl,
148
+ }),
149
+ 0o700,
150
+ ),
151
+ ] : []),
152
+ ];
153
+ const controlUploadStartedAt = Date.now();
154
+ await uploadVerifiedFiles(lease.ssh, controlUploads, {
155
+ onRetry: (retry, error) => progress(
156
+ `AutoDL ${bundle.runId}: 控制文件上传连接中断(${safeMessage(error)}),准备第 ${retry} 次重试`,
157
+ ),
158
+ onResume: (upload) => progress(
159
+ `AutoDL ${bundle.runId}: 已确认并复用 ${path.posix.basename(upload.finalPath)}`,
160
+ ),
161
+ });
162
+ progress(
163
+ `AutoDL ${bundle.runId}: ${controlUploads.length} 个控制文件上传并校验完成(${formatSeconds(
164
+ (Date.now() - controlUploadStartedAt) / 1000,
165
+ )})`,
166
+ );
167
+ progress(`AutoDL ${bundle.runId}: 第 ${attempt} 次运行输入准备并校验完成`);
168
+
169
+ const startedAt = new Date().toISOString();
170
+ let checkpointInFlight = false;
171
+ let lastCheckpointSequence = null;
172
+ const executionAbort = new AbortController();
173
+ const checkpointTimer = setInterval(async () => {
174
+ if (checkpointInFlight) return;
175
+ checkpointInFlight = true;
176
+ try {
177
+ await assertActive();
178
+ const checkpoint = await readRemoteJson(lease.ssh, checkpointStatus);
179
+ if (
180
+ checkpoint?.schemaVersion === "0.1"
181
+ && typeof checkpoint.sequence === "string"
182
+ && checkpoint.sequence !== lastCheckpointSequence
183
+ ) {
184
+ lastCheckpointSequence = checkpoint.sequence;
185
+ await onCheckpoint(checkpoint);
186
+ progress(`AutoDL ${bundle.runId}: 已保存${checkpoint.kind === "full" ? "完整" : "关键"}检查点 ${checkpoint.sequence}`);
187
+ }
188
+ } catch (error) {
189
+ if (error?.processingLeaseLost === true) executionAbort.abort(error);
190
+ // A checkpoint is optional until the remote worker has created it.
191
+ } finally {
192
+ checkpointInFlight = false;
193
+ }
194
+ }, Math.max(5_000, checkpointPolicy.criticalIntervalSeconds * 1000));
195
+ checkpointTimer.unref();
196
+
197
+ const liveStream = hasSessionEvents() ? createRuntimeStream({ source: `${bundle.runId}:${attempt}`, runtime: bundle.runtimeAgent.runtime }) : null;
198
+ let remoteExecution;
199
+ let environmentUploadStarted = false;
200
+ try {
201
+ environmentUploadStarted = true;
202
+ const environmentUpload = textUpload(
203
+ remoteEnvironment,
204
+ shellEnvironmentFile(environment),
205
+ 0o600,
206
+ );
207
+ await uploadVerifiedFiles(lease.ssh, [environmentUpload], {
208
+ onRetry: (retry, error) => progress(
209
+ `AutoDL ${bundle.runId}: 环境文件上传连接中断(${safeMessage(error)}),准备第 ${retry} 次重试`,
210
+ ),
211
+ });
212
+ await assertActive();
213
+ progress(`AutoDL ${bundle.runId}: Agent 已在 ${lease.instanceUuid} 启动`);
214
+ const seconds = Math.ceil(bundle.executionEnvironment.timeoutMinutes * 60);
215
+ const executionScript = managedNetwork ? remoteNetworkRunner : remoteScript;
216
+ remoteExecution = await lease.ssh.exec(
217
+ [
218
+ "set -a",
219
+ `. ${shellQuote(remoteEnvironment)}`,
220
+ "set +a",
221
+ `timeout --signal=TERM --kill-after=30s ${seconds}s bash ${shellQuote(executionScript)}`,
222
+ ].join("; "),
223
+ {
224
+ timeoutMs: (seconds + 90) * 1000,
225
+ signal: executionAbort.signal,
226
+ onStdout: chunk => liveStream?.write(chunk),
227
+ onStderr: (text) => {
228
+ if (liveStream) { liveStream.stderr(text); return; }
229
+ const line = text.replaceAll(/\s+/g, " ").trim();
230
+ if (line) progress(`AutoDL ${bundle.runId}: ${line.slice(0, 400)}`);
231
+ },
232
+ },
233
+ );
234
+ } finally {
235
+ liveStream?.end();
236
+ clearInterval(checkpointTimer);
237
+ if (environmentUploadStarted) {
238
+ await removeRemoteNetworkFiles(lease.ssh, [
239
+ `${remoteEnvironment}.uploading`,
240
+ remoteEnvironment,
241
+ remoteTunnelKey,
242
+ remoteKnownHosts,
243
+ remoteNetworkRunner,
244
+ remoteRouter,
245
+ remoteProviderRelay,
246
+ ]);
247
+ }
248
+ }
249
+ const finishedAt = new Date().toISOString();
250
+ const timedOut = remoteExecution.timedOut || remoteExecution.code === 124;
251
+ if (!(await lease.ssh.exists(remoteResult))) {
252
+ const detail = safeRemoteFailureDetail(remoteExecution.stderr);
253
+ throw new CiteArkError(
254
+ `AutoDL 远程运行没有返回结果包(退出码 ${remoteExecution.code}${timedOut ? ",已超时" : ""})${detail ? `:${detail}` : ""}`,
255
+ );
256
+ }
257
+ await lease.ssh.getFile(remoteResult, resultArchive);
258
+ await extractAutoDlResultArchive(bundle, resultArchive);
259
+ const externalObjects = await restoreAutoDlExternalObjects({
260
+ bundle,
261
+ ssh: lease.ssh,
262
+ remoteDirectory,
263
+ maximumExternalBytes: config.maximumExternalBytes,
264
+ progress,
265
+ });
266
+ const durationSeconds = Math.max(
267
+ 0,
268
+ (Date.parse(finishedAt) - Date.parse(startedAt)) / 1000,
269
+ );
270
+ return {
271
+ record: {
272
+ attempt,
273
+ instanceUuid: lease.instanceUuid,
274
+ state: remoteExecution.code === 0 ? "SUCCEEDED" : timedOut ? "TIMED_OUT" : "FAILED",
275
+ startedAt,
276
+ finishedAt,
277
+ durationSeconds,
278
+ exitCode: remoteExecution.code,
279
+ signal: remoteExecution.signal,
280
+ timedOut,
281
+ checkpoint: lastCheckpointSequence,
282
+ stageTransfer: stageTransferRecord,
283
+ },
284
+ externalObjects,
285
+ };
286
+ }
287
+
288
+ export async function transferAutoDlStageArchive({
289
+ ssh,
290
+ transfer,
291
+ runId,
292
+ attempt,
293
+ stageUpload,
294
+ progress = () => {},
295
+ now = () => Date.now(),
296
+ }) {
297
+ let fallbackReason = null;
298
+ if (transfer) {
299
+ let published = null;
300
+ try {
301
+ const publishStartedAt = now();
302
+ published = await transfer.publish({
303
+ runId,
304
+ attempt,
305
+ filePath: stageUpload.localPath,
306
+ digest: stageUpload.digest,
307
+ byteSize: stageUpload.byteSize,
308
+ });
309
+ const publishSeconds = elapsedSeconds(publishStartedAt, now());
310
+ progress(
311
+ `AutoDL ${runId}: 输入包已暂存 GCS 并签名(${formatMiB(stageUpload.byteSize)} MiB,${formatSeconds(publishSeconds)})`,
312
+ );
313
+ const downloadStartedAt = now();
314
+ await downloadVerifiedAutoDlStage({
315
+ ssh,
316
+ url: published.url,
317
+ stageUpload,
318
+ onRetry: (nextAttempt, detail) => progress(
319
+ `AutoDL ${runId}: GCS 分块下载中断(${detail}),保留完整分块并进行第 ${nextAttempt} 次续传`,
320
+ ),
321
+ });
322
+ const downloadSeconds = elapsedSeconds(downloadStartedAt, now());
323
+ const bytesPerSecond = transferRate(stageUpload.byteSize, downloadSeconds);
324
+ progress(
325
+ `AutoDL ${runId}: GCS 直连下载并校验完成(${formatSeconds(downloadSeconds)},${formatRate(bytesPerSecond)})`,
326
+ );
327
+ return {
328
+ mode: "gcs-signed-url",
329
+ byteSize: stageUpload.byteSize,
330
+ publishSeconds,
331
+ transferSeconds: downloadSeconds,
332
+ bytesPerSecond,
333
+ fallbackReason: null,
334
+ };
335
+ } catch (error) {
336
+ fallbackReason = safeMessage(error);
337
+ progress(
338
+ `AutoDL ${runId}: GCS 直连输入失败(${fallbackReason}),回退到校验 SFTP`,
339
+ );
340
+ } finally {
341
+ if (published?.remove) {
342
+ await published.remove().catch((error) => progress(
343
+ `AutoDL ${runId}: GCS 临时输入清理失败,将由 bucket 生命周期回收(${safeMessage(error)})`,
344
+ ));
345
+ }
346
+ }
347
+ }
348
+
349
+ const uploadStartedAt = now();
350
+ await uploadVerifiedFiles(ssh, [stageUpload], {
351
+ onRetry: (retry, error) => progress(
352
+ `AutoDL ${runId}: SFTP 输入上传连接中断(${safeMessage(error)}),准备第 ${retry} 次重试`,
353
+ ),
354
+ onResume: () => progress(`AutoDL ${runId}: 已确认并复用完整 SFTP 输入包`),
355
+ });
356
+ const transferSeconds = elapsedSeconds(uploadStartedAt, now());
357
+ const bytesPerSecond = transferRate(stageUpload.byteSize, transferSeconds);
358
+ progress(
359
+ `AutoDL ${runId}: SFTP 输入上传并校验完成(${formatSeconds(transferSeconds)},${formatRate(bytesPerSecond)})`,
360
+ );
361
+ return {
362
+ mode: "sftp",
363
+ byteSize: stageUpload.byteSize,
364
+ publishSeconds: null,
365
+ transferSeconds,
366
+ bytesPerSecond,
367
+ fallbackReason,
368
+ };
369
+ }
370
+
371
+ export async function downloadVerifiedAutoDlStage({
372
+ ssh,
373
+ url,
374
+ stageUpload,
375
+ attempts = 3,
376
+ onRetry = () => {},
377
+ }) {
378
+ if (typeof url !== "string" || !url.startsWith("https://storage.googleapis.com/")) {
379
+ throw new TypeError("AutoDL GCS 输入 URL 无效");
380
+ }
381
+ if (!/^[a-f0-9]{64}$/.test(String(stageUpload?.digest ?? ""))) {
382
+ throw new TypeError("AutoDL GCS 输入摘要无效");
383
+ }
384
+ if (!Number.isSafeInteger(stageUpload?.byteSize) || stageUpload.byteSize <= 0) {
385
+ throw new TypeError("AutoDL GCS 输入大小无效");
386
+ }
387
+ const maximumSeconds = autoDlStageDownloadTimeoutSeconds(stageUpload.byteSize);
388
+ const chunkBytes = autoDlStageDownloadChunkBytes(stageUpload.byteSize);
389
+ const partsDirectory = `${stageUpload.uploadPath}.parts`;
390
+ const expectedLine = `${stageUpload.digest} ${stageUpload.uploadPath}`;
391
+ const downloadScript = [
392
+ "set -eu",
393
+ `url=${shellQuote(url)}`,
394
+ `destination=${shellQuote(stageUpload.uploadPath)}`,
395
+ `parts_directory=${shellQuote(partsDirectory)}`,
396
+ `byte_size=${stageUpload.byteSize}`,
397
+ `chunk_bytes=${chunkBytes}`,
398
+ "parallelism=4",
399
+ "rm -f -- \"$destination\"",
400
+ "mkdir -p -- \"$parts_directory\"",
401
+ "index=0",
402
+ "start=0",
403
+ "while [ \"$start\" -lt \"$byte_size\" ]; do",
404
+ " pids=''",
405
+ " launched=0",
406
+ " while [ \"$launched\" -lt \"$parallelism\" ] && [ \"$start\" -lt \"$byte_size\" ]; do",
407
+ " end=$((start + chunk_bytes - 1))",
408
+ " if [ \"$end\" -ge \"$byte_size\" ]; then end=$((byte_size - 1)); fi",
409
+ " part=$(printf '%08d' \"$index\")",
410
+ " part_path=\"$parts_directory/part-$part\"",
411
+ " partial_path=\"$part_path.partial\"",
412
+ " expected_size=$((end - start + 1))",
413
+ " if [ -f \"$part_path\" ] && [ \"$(stat -c '%s' -- \"$part_path\")\" = \"$expected_size\" ]; then",
414
+ " index=$((index + 1))",
415
+ " start=$((end + 1))",
416
+ " launched=$((launched + 1))",
417
+ " continue",
418
+ " fi",
419
+ " rm -f -- \"$part_path\" \"$partial_path\"",
420
+ " (",
421
+ [
422
+ " env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy",
423
+ "curl --noproxy '*' --fail --silent --show-error --location",
424
+ "--retry 3 --retry-all-errors --retry-delay 2 --connect-timeout 20",
425
+ "--speed-limit 4096 --speed-time 45",
426
+ `--max-time ${maximumSeconds}`,
427
+ "--range \"$start-$end\"",
428
+ "--output \"$partial_path\" \"$url\"",
429
+ ].join(" "),
430
+ " test \"$(stat -c '%s' -- \"$partial_path\")\" = \"$expected_size\"",
431
+ " mv -f -- \"$partial_path\" \"$part_path\"",
432
+ " ) &",
433
+ " pids=\"$pids $!\"",
434
+ " index=$((index + 1))",
435
+ " start=$((end + 1))",
436
+ " launched=$((launched + 1))",
437
+ " done",
438
+ " failed=0",
439
+ " for pid in $pids; do wait \"$pid\" || failed=1; done",
440
+ " test \"$failed\" -eq 0",
441
+ "done",
442
+ "cat \"$parts_directory\"/part-* > \"$destination\"",
443
+ `test "$(stat -c '%s' -- \"$destination\")" = ${shellQuote(String(stageUpload.byteSize))}`,
444
+ `printf '%s\\n' ${shellQuote(expectedLine)} | sha256sum --check --status -`,
445
+ `mv -f -- \"$destination\" ${shellQuote(stageUpload.finalPath)}`,
446
+ "rm -rf -- \"$parts_directory\"",
447
+ ].join("\n");
448
+ let lastDetail = "未知错误";
449
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
450
+ try {
451
+ const result = await ssh.exec(
452
+ `timeout --signal=TERM --kill-after=15s ${maximumSeconds}s bash -c ${shellQuote(downloadScript)}`,
453
+ { timeoutMs: (maximumSeconds + 30) * 1000 },
454
+ );
455
+ if (result.code === 0) return;
456
+ lastDetail = safeRemoteFailureDetail(result.stderr) || `退出码 ${result.code}`;
457
+ } catch (error) {
458
+ lastDetail = safeMessage(error);
459
+ }
460
+ if (attempt < attempts) onRetry(attempt + 1, lastDetail);
461
+ }
462
+ await ssh.exec(
463
+ `rm -rf -- ${shellQuote(stageUpload.uploadPath)} ${shellQuote(partsDirectory)}`,
464
+ { timeoutMs: 30_000 },
465
+ ).catch(() => undefined);
466
+ throw new CiteArkError(`AutoDL 无代理 GCS 下载或身份校验失败:${lastDetail}`);
467
+ }
468
+
469
+ export function autoDlStageDownloadTimeoutSeconds(byteSize) {
470
+ const size = Number.isSafeInteger(byteSize) && byteSize > 0 ? byteSize : 1;
471
+ const minimumBytesPerSecond = 64 * 1024;
472
+ return Math.min(
473
+ 30 * 60,
474
+ Math.max(5 * 60, Math.ceil(size / minimumBytesPerSecond) + 2 * 60),
475
+ );
476
+ }
477
+
478
+ export function autoDlStageDownloadChunkBytes(byteSize) {
479
+ const size = Number.isSafeInteger(byteSize) && byteSize > 0 ? byteSize : 1;
480
+ if (size <= 64 * 1024 ** 2) return 1024 ** 2;
481
+ if (size <= 512 * 1024 ** 2) return 4 * 1024 ** 2;
482
+ return 16 * 1024 ** 2;
483
+ }
484
+
485
+ export async function createAutoDlStageArchive(bundle, destination) {
486
+ await createRemoteStageArchive({
487
+ runDirectory: bundle.directories.run,
488
+ destination,
489
+ label: "AutoDL",
490
+ });
491
+ }
492
+
493
+ export async function extractAutoDlResultArchive(bundle, source) {
494
+ const result = await captureProcess(
495
+ "tar",
496
+ ["-xzf", source, "-C", bundle.directories.run],
497
+ );
498
+ if (result.code !== 0) {
499
+ throw new CiteArkError(`解压 AutoDL 运行结果失败:\n${result.stderr.trim()}`);
500
+ }
501
+ }
502
+
503
+ export async function restoreAutoDlExternalObjects({
504
+ bundle,
505
+ ssh,
506
+ remoteDirectory,
507
+ maximumExternalBytes = 8 * 1024 ** 3,
508
+ progress = () => {},
509
+ }) {
510
+ const manifestPath = path.join(bundle.directories.execution, "result-export.json");
511
+ if (!(await pathExists(manifestPath))) return [];
512
+ const manifest = await readJson(manifestPath);
513
+ const descriptors = Array.isArray(manifest.externalObjects)
514
+ ? manifest.externalObjects
515
+ : [];
516
+ const normalized = descriptors.map((descriptor) => {
517
+ const relative = descriptor?.path;
518
+ const object = descriptor?.object;
519
+ const digest = descriptor?.sha256;
520
+ const size = descriptor?.size;
521
+ if (
522
+ typeof relative !== "string"
523
+ || typeof object !== "string"
524
+ || path.posix.basename(object) !== object
525
+ || !/^[a-f0-9]{64}$/.test(String(digest))
526
+ || !Number.isSafeInteger(size)
527
+ || size <= 0
528
+ ) {
529
+ throw new CiteArkError("AutoDL 发布清单包含无效的大对象描述");
530
+ }
531
+ return { relative, object, digest: String(digest), size };
532
+ });
533
+ const totalBytes = normalized.reduce((total, descriptor) => total + descriptor.size, 0);
534
+ if (!Number.isSafeInteger(totalBytes) || totalBytes > maximumExternalBytes) {
535
+ throw new CiteArkError(
536
+ `AutoDL 外部证据合计 ${formatMiB(totalBytes)} MiB,超过协调器本地预算 ${formatMiB(maximumExternalBytes)} MiB`,
537
+ );
538
+ }
539
+ const restored = [];
540
+ for (const descriptor of normalized) {
541
+ const { relative, object, digest, size } = descriptor;
542
+ const destination = path.resolve(bundle.directories.run, relative);
543
+ if (!destination.startsWith(`${path.resolve(bundle.directories.run)}${path.sep}`)) {
544
+ throw new CiteArkError(`AutoDL 发布大对象路径逃逸:${relative}`);
545
+ }
546
+ await mkdir(path.dirname(destination), { recursive: true });
547
+ await downloadVerifiedExternalObject({
548
+ ssh,
549
+ remotePath: `${remoteDirectory}/${object}`,
550
+ destination,
551
+ relative,
552
+ digest,
553
+ size,
554
+ progress,
555
+ });
556
+ restored.push(object);
557
+ }
558
+ return restored;
559
+ }
560
+
561
+ export function autoDlExternalObjectTransferTimeoutMs(byteSize) {
562
+ const size = Number.isSafeInteger(byteSize) && byteSize > 0 ? byteSize : 1;
563
+ const minimumTransferBytesPerSecond = 64 * 1024;
564
+ const verificationMarginMs = 5 * 60_000;
565
+ return Math.min(
566
+ 4 * 60 * 60_000,
567
+ Math.max(
568
+ 30 * 60_000,
569
+ Math.ceil((size / minimumTransferBytesPerSecond) * 1000)
570
+ + verificationMarginMs,
571
+ ),
572
+ );
573
+ }
574
+
575
+ async function downloadVerifiedExternalObject({
576
+ ssh,
577
+ remotePath,
578
+ destination,
579
+ relative,
580
+ digest,
581
+ size,
582
+ progress,
583
+ attempts = 3,
584
+ }) {
585
+ const timeoutMs = autoDlExternalObjectTransferTimeoutMs(size);
586
+ let lastError;
587
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
588
+ let lastReportedDecile = -1;
589
+ try {
590
+ await rm(destination, { force: true });
591
+ progress(
592
+ `AutoDL 下载外部证据 ${relative}(${formatMiB(size)} MiB,第 ${attempt}/${attempts} 次)`,
593
+ );
594
+ await ssh.getFile(remotePath, destination, {
595
+ timeoutMs,
596
+ onProgress: ({ transferredBytes, totalBytes }) => {
597
+ const total = Number(totalBytes) > 0 ? Number(totalBytes) : size;
598
+ const decile = Math.min(
599
+ 10,
600
+ Math.floor((Number(transferredBytes) / total) * 10),
601
+ );
602
+ if (decile <= lastReportedDecile) return;
603
+ lastReportedDecile = decile;
604
+ progress(`AutoDL 外部证据 ${relative} 下载 ${decile * 10}%`);
605
+ },
606
+ });
607
+ const [observedDigest, stat] = await Promise.all([
608
+ sha256File(destination),
609
+ lstat(destination),
610
+ ]);
611
+ if (!stat.isFile() || stat.size !== size || observedDigest !== digest) {
612
+ throw new CiteArkError(`AutoDL 发布大对象身份校验失败:${relative}`);
613
+ }
614
+ progress(`AutoDL 外部证据 ${relative} 下载并校验完成`);
615
+ return;
616
+ } catch (error) {
617
+ lastError = error;
618
+ if (attempt < attempts) {
619
+ progress(
620
+ `AutoDL 外部证据 ${relative} 下载中断,将重新连接并进行第 ${attempt + 1}/${attempts} 次尝试`,
621
+ );
622
+ }
623
+ }
624
+ }
625
+ await rm(destination, { force: true });
626
+ throw new CiteArkError(
627
+ `AutoDL 外部证据下载重试后仍失败:${relative}`,
628
+ { cause: lastError },
629
+ );
630
+ }
631
+
632
+ function formatMiB(byteSize) {
633
+ return (byteSize / (1024 * 1024)).toFixed(1);
634
+ }
635
+
636
+ function elapsedSeconds(startedAt, finishedAt) {
637
+ return Math.max(0, (Number(finishedAt) - Number(startedAt)) / 1000);
638
+ }
639
+
640
+ function transferRate(byteSize, seconds) {
641
+ return seconds > 0 ? Math.round(byteSize / seconds) : null;
642
+ }
643
+
644
+ function formatRate(bytesPerSecond) {
645
+ if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "速度未知";
646
+ if (bytesPerSecond >= 1024 * 1024) {
647
+ return `${(bytesPerSecond / (1024 * 1024)).toFixed(1)} MiB/s`;
648
+ }
649
+ return `${(bytesPerSecond / 1024).toFixed(1)} KiB/s`;
650
+ }
651
+
652
+ function formatSeconds(seconds) {
653
+ return `${Number(seconds).toFixed(1)} 秒`;
654
+ }
655
+
656
+ async function readRemoteJson(ssh, remotePath) {
657
+ const bytes = await ssh.readFile(remotePath, { maximumBytes: 2 * 1024 * 1024 });
658
+ return JSON.parse(bytes.toString("utf8"));
659
+ }
660
+
661
+ async function assertRemotePath(value) {
662
+ if (!/^\/root\/citeark\/[a-z0-9-]{1,80}$/.test(value)) {
663
+ throw new TypeError(`AutoDL 远程目录无效:${value}`);
664
+ }
665
+ }
666
+
667
+ function textUpload(finalPath, content, mode) {
668
+ const bytes = Buffer.from(content, "utf8");
669
+ if (!bytes.length) throw new CiteArkError(`AutoDL 上传内容为空:${path.posix.basename(finalPath)}`);
670
+ return {
671
+ finalPath,
672
+ uploadPath: `${finalPath}.uploading`,
673
+ content,
674
+ mode,
675
+ byteSize: bytes.length,
676
+ digest: createHash("sha256").update(bytes).digest("hex"),
677
+ };
678
+ }
679
+
680
+ export async function assertRemoteUploads(ssh, uploads) {
681
+ if (!uploads.length) return;
682
+ const paths = uploads.map((upload) => shellQuote(upload.uploadPath));
683
+ const result = await ssh.exec(
684
+ [
685
+ `sha256sum -- ${paths.join(" ")}`,
686
+ `stat -c '%s %a' -- ${paths.join(" ")}`,
687
+ ].join(" && "),
688
+ { timeoutMs: 60_000 },
689
+ );
690
+ if (result.code !== 0) {
691
+ throw new CiteArkError("AutoDL 上传完整性检查无法读取全部暂存文件");
692
+ }
693
+ const lines = result.stdout.trim().split(/\r?\n/);
694
+ if (lines.length !== uploads.length * 2) {
695
+ throw new CiteArkError("AutoDL 上传完整性检查返回的文件数量不一致");
696
+ }
697
+ const digests = new Map(lines.slice(0, uploads.length).map((line) => {
698
+ const matched = line.match(/^([0-9a-f]{64})\s+(.+)$/);
699
+ if (!matched) throw new CiteArkError("AutoDL 上传摘要输出无效");
700
+ return [matched[2], matched[1]];
701
+ }));
702
+ const stats = lines.slice(uploads.length).map((line) => {
703
+ const matched = line.match(/^(\d+) (\d+)$/);
704
+ if (!matched) {
705
+ throw new CiteArkError("AutoDL 上传文件状态输出无效");
706
+ }
707
+ return { size: Number(matched[1]), mode: matched[2] };
708
+ });
709
+ for (const [index, upload] of uploads.entries()) {
710
+ const remote = stats[index];
711
+ if (
712
+ !remote
713
+ || remote.size !== upload.byteSize
714
+ || digests.get(upload.uploadPath) !== upload.digest
715
+ || (upload.mode && remote.mode !== upload.mode.toString(8))
716
+ ) {
717
+ throw new CiteArkError(
718
+ `AutoDL 上传完整性检查失败:${path.posix.basename(upload.finalPath)}`,
719
+ );
720
+ }
721
+ }
722
+ }
723
+
724
+ export async function uploadVerifiedFiles(ssh, uploads, {
725
+ attempts = 5,
726
+ retryDelayMs = 5_000,
727
+ onRetry = () => {},
728
+ onResume = () => {},
729
+ } = {}) {
730
+ let lastError;
731
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
732
+ try {
733
+ for (const upload of uploads) {
734
+ if (attempt > 1 && await remoteUploadIsVerified(ssh, upload)) {
735
+ onResume(upload);
736
+ continue;
737
+ }
738
+ if (upload.localPath) {
739
+ await ssh.putFile(upload.localPath, upload.uploadPath);
740
+ } else {
741
+ await ssh.writeFile(upload.uploadPath, upload.content, { mode: upload.mode });
742
+ }
743
+ }
744
+ await assertRemoteUploads(ssh, uploads);
745
+ await promoteRemoteUploads(ssh, uploads);
746
+ return;
747
+ } catch (error) {
748
+ lastError = error;
749
+ if (attempt >= attempts) break;
750
+ onRetry(attempt + 1, error);
751
+ if (retryDelayMs > 0) {
752
+ const backoffMs = Math.min(retryDelayMs * (2 ** (attempt - 1)), 30_000);
753
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
754
+ }
755
+ }
756
+ }
757
+ await removeRemoteNetworkFiles(ssh, [
758
+ ...uploads.flatMap((upload) => [upload.uploadPath, upload.finalPath]),
759
+ ]);
760
+ throw lastError;
761
+ }
762
+
763
+ async function remoteUploadIsVerified(ssh, upload) {
764
+ try {
765
+ await assertRemoteUploads(ssh, [upload]);
766
+ return true;
767
+ } catch (error) {
768
+ if (String(error?.message ?? "").startsWith("AutoDL 上传")) return false;
769
+ throw error;
770
+ }
771
+ }
772
+
773
+ async function promoteRemoteUploads(ssh, uploads) {
774
+ const command = uploads
775
+ .map((upload) => `mv -f -- ${shellQuote(upload.uploadPath)} ${shellQuote(upload.finalPath)}`)
776
+ .join(" && ");
777
+ const result = await ssh.exec(command, { timeoutMs: 60_000 });
778
+ if (result.code !== 0) {
779
+ throw new CiteArkError("AutoDL 已校验上传文件无法提交到运行目录");
780
+ }
781
+ }
782
+
783
+ function safeIdentifier(value) {
784
+ return String(value ?? "run")
785
+ .toLowerCase()
786
+ .replaceAll(/[^a-z0-9-]/g, "-")
787
+ .replaceAll(/^-+|-+$/g, "")
788
+ .slice(0, 80) || "run";
789
+ }
790
+
791
+ function safeRemoteFailureDetail(value) {
792
+ return String(value ?? "")
793
+ .replaceAll(/(password|token|secret|authorization|private[_ -]?key)\s*[=:]\s*\S+/gi, "$1=<redacted>")
794
+ .split(/\r?\n/)
795
+ .map((line) => line.trim())
796
+ .filter(Boolean)
797
+ .at(-1)
798
+ ?.slice(0, 500) ?? "";
799
+ }
800
+
801
+ function safeMessage(error) {
802
+ return safeRemoteFailureDetail(error instanceof Error ? error.message : String(error))
803
+ || "未知错误";
804
+ }
805
+
806
+ async function removeRemoteNetworkFiles(ssh, paths) {
807
+ await ssh.exec(["rm -f", ...paths.map(shellQuote)].join(" "), {
808
+ timeoutMs: 30_000,
809
+ }).catch(() => undefined);
810
+ }