@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.
- package/LICENSE +202 -0
- package/README.md +128 -0
- package/data/dataset-source-registry.v1.json +300 -0
- package/dist/arkgraph/boot.js +6 -0
- package/dist/arkgraph/index.html +1 -0
- package/dist/arkgraph/viewer.css +1 -0
- package/dist/arkgraph/viewer.en.css +1 -0
- package/dist/arkgraph/viewer.en.js +49 -0
- package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
- package/dist/arkgraph/viewer.js +49 -0
- package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
- package/docker/claude-code/Dockerfile +97 -0
- package/docker/claude-code/codex-pro-relay.mjs +466 -0
- package/docker/claude-code/runtime-contract-check.mjs +79 -0
- package/docs/arkgraph-reading.md +79 -0
- package/docs/configuration.md +100 -0
- package/docs/integration.md +92 -0
- package/docs/maturity-plan.md +27 -0
- package/docs/npm-release.md +44 -0
- package/docs/paper-reading.md +40 -0
- package/docs/research-plan-granularity.md +27 -0
- package/docs/terminal.md +49 -0
- package/examples/toy-evaluation/compile-task.json +27 -0
- package/examples/toy-evaluation/paper.md +5 -0
- package/examples/toy-evaluation/repository/README.md +9 -0
- package/examples/toy-evaluation/repository/checkpoint.json +4 -0
- package/examples/toy-evaluation/repository/evaluate.py +17 -0
- package/examples/toy-evaluation/task.json +81 -0
- package/package.json +59 -0
- package/prompts/compile-research.md +58 -0
- package/prompts/execute-contract.md +72 -0
- package/prompts/execute-workspace-simple.md +51 -0
- package/prompts/execute-workspace.md +34 -0
- package/prompts/prepare-reproduction.md +82 -0
- package/prompts/repair-research.md +45 -0
- package/protocol/CAP.md +129 -0
- package/protocol/LICENSE +12 -0
- package/protocol/MAPPINGS.md +72 -0
- package/protocol/README.md +38 -0
- package/protocol/conformance-v2.0-alpha.1.json +36 -0
- package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
- package/protocol/examples/arkgraph/fixtures.mjs +49 -0
- package/protocol/examples/arkgraph/paper-free.json +291 -0
- package/protocol/examples/arkgraph/partial-failure.json +344 -0
- package/protocol/examples/arkgraph/training-evaluation.json +443 -0
- package/protocol/profiles/agent-trace.md +16 -0
- package/protocol/profiles/computational-run.md +16 -0
- package/protocol/profiles/core.md +15 -0
- package/protocol/profiles/public-bundle.md +18 -0
- package/protocol/profiles/reproduction.md +29 -0
- package/protocol/profiles/research-compilation.md +44 -0
- package/protocol/profiles/research-plan.md +39 -0
- package/protocol/profiles/restricted-evidence.md +15 -0
- package/runtime/bootstrap-autodl-runtime.sh +314 -0
- package/runtime/create-runtime-venv.sh +41 -0
- package/runtime/install-local-cpu-runtime.sh +23 -0
- package/runtime/install-scientific-runtime.sh +153 -0
- package/runtime/mineru/parse.py +62 -0
- package/runtime/mineru/requirements.txt +4 -0
- package/runtime/requirements-baseline.txt +38 -0
- package/schemas/cap/v2/activity.schema.json +47 -0
- package/schemas/cap/v2/agent.schema.json +32 -0
- package/schemas/cap/v2/assertion.schema.json +110 -0
- package/schemas/cap/v2/descriptor.schema.json +243 -0
- package/schemas/cap/v2/entity.schema.json +64 -0
- package/schemas/cap/v2/manifest.schema.json +67 -0
- package/schemas/cap/v2/relation.schema.json +82 -0
- package/schemas/compute-catalog.schema.json +63 -0
- package/schemas/compute-decision.schema.json +27 -0
- package/schemas/execution-contract.schema.json +1024 -0
- package/schemas/research-card.schema.json +30 -0
- package/schemas/research-inventory-draft.schema.json +366 -0
- package/schemas/research.schema.json +1044 -0
- package/schemas/result.schema.json +173 -0
- package/schemas/verification-policy.schema.json +47 -0
- package/schemas/verified-conclusion.schema.json +58 -0
- package/schemas/workspace-summary.schema.json +24 -0
- package/scripts/build-arkgraph-view.mjs +12 -0
- package/scripts/check-execution-feasibility.mjs +24 -0
- package/scripts/check-syntax.mjs +15 -0
- package/scripts/deterministic-asset-preparation.py +438 -0
- package/scripts/package-cap.mjs +23 -0
- package/scripts/package-local-agent.mjs +23 -0
- package/scripts/preview-arkgraph.mjs +25 -0
- package/scripts/replay-research-compiler-candidate.mjs +134 -0
- package/scripts/review-compiler-sources.mjs +44 -0
- package/scripts/run-asset-preparation.sh +17 -0
- package/scripts/run-research-plan.mjs +98 -0
- package/scripts/validate-asset-preparation.py +290 -0
- package/scripts/verify-local-runtime.mjs +57 -0
- package/scripts/verify-npm-package.mjs +57 -0
- package/src/adapters/paper2agent.mjs +107 -0
- package/src/assets/cache.mjs +159 -0
- package/src/assets/compute.mjs +98 -0
- package/src/assets/executor.mjs +145 -0
- package/src/assets/lifecycle.mjs +213 -0
- package/src/assets/manifest.mjs +242 -0
- package/src/assets/opportunistic-preparation.mjs +81 -0
- package/src/assets/plan.mjs +411 -0
- package/src/assets/prompts.mjs +29 -0
- package/src/assets/public-asset-probe.mjs +525 -0
- package/src/assets/qualification.mjs +119 -0
- package/src/assets/readiness.mjs +130 -0
- package/src/assets/reproduction-admission.mjs +355 -0
- package/src/assets/requirements.mjs +152 -0
- package/src/assets/source-grounding.mjs +341 -0
- package/src/assets/source-policy.mjs +118 -0
- package/src/autodl/client.mjs +260 -0
- package/src/autodl/ssh.mjs +380 -0
- package/src/autodl/tools.mjs +129 -0
- package/src/cap/redaction.mjs +38 -0
- package/src/cap/v2/archive.mjs +152 -0
- package/src/cap/v2/attestation.mjs +204 -0
- package/src/cap/v2/canonical-json.mjs +114 -0
- package/src/cap/v2/compilation-artifact.mjs +240 -0
- package/src/cap/v2/core.mjs +282 -0
- package/src/cap/v2/measurement-assessment-records.mjs +23 -0
- package/src/cap/v2/pipeline-artifact.mjs +922 -0
- package/src/cap/v2/read.mjs +41 -0
- package/src/cap/v2/reassessment-artifact.mjs +383 -0
- package/src/cap/v2/research-artifact.mjs +231 -0
- package/src/cap/v2/research-map-records.mjs +46 -0
- package/src/cap/v2/research-object-records.mjs +163 -0
- package/src/cap/v2/research-records.mjs +187 -0
- package/src/cap/v2/verify.mjs +642 -0
- package/src/cli.mjs +1146 -0
- package/src/compute/autodl-pro-compiler.mjs +347 -0
- package/src/compute/autodl-pro-executor.mjs +459 -0
- package/src/compute/autodl-pro-job.mjs +843 -0
- package/src/compute/autodl-pro-network.mjs +295 -0
- package/src/compute/autodl-pro-remote.mjs +810 -0
- package/src/compute/autodl-pro-staging.mjs +117 -0
- package/src/compute/campaign.mjs +110 -0
- package/src/compute/catalog.mjs +123 -0
- package/src/compute/checkpoint-protocol.mjs +154 -0
- package/src/compute/codex-account-lock.mjs +111 -0
- package/src/compute/codex-account-session.mjs +107 -0
- package/src/compute/compiler-profile.mjs +38 -0
- package/src/compute/compiler-router.mjs +23 -0
- package/src/compute/coordinator-recovery.mjs +210 -0
- package/src/compute/executor-router.mjs +29 -0
- package/src/compute/gcp-batch-compiler.mjs +685 -0
- package/src/compute/gcp-batch-executor.mjs +1215 -0
- package/src/compute/gcp-batch-failure.mjs +92 -0
- package/src/compute/gcp-batch-job.mjs +527 -0
- package/src/compute/gcp-batch-lifecycle.mjs +81 -0
- package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
- package/src/compute/local-codex-compiler.mjs +52 -0
- package/src/compute/measurement-hardware.mjs +128 -0
- package/src/compute/remote-attempt.mjs +226 -0
- package/src/compute/requirements.mjs +124 -0
- package/src/compute/research-phases.mjs +48 -0
- package/src/compute/scheduler.mjs +452 -0
- package/src/compute/shared-workloads.mjs +26 -0
- package/src/compute/stage-archive.mjs +79 -0
- package/src/contracts/campaign-contract.mjs +52 -0
- package/src/contracts/execution-contract.mjs +819 -0
- package/src/contracts/execution-mode.mjs +19 -0
- package/src/contracts/execution-timeouts.mjs +45 -0
- package/src/contracts/execution-workload.mjs +68 -0
- package/src/contracts/preflight-schema.mjs +25 -0
- package/src/contracts/public-contract.mjs +63 -0
- package/src/contracts/subject-tags.mjs +31 -0
- package/src/dashboard/data.mjs +898 -0
- package/src/dashboard/server.mjs +79 -0
- package/src/dashboard/static/dashboard.css +366 -0
- package/src/dashboard/static/dashboard.js +560 -0
- package/src/dashboard/static/index.html +85 -0
- package/src/deployment/community-policy.mjs +9 -0
- package/src/deployment/environment.mjs +112 -0
- package/src/deployment/guided.mjs +98 -0
- package/src/deployment/handoff.mjs +102 -0
- package/src/deployment/local-contract.mjs +31 -0
- package/src/deployment/local.mjs +100 -0
- package/src/deployment/prepare.mjs +46 -0
- package/src/deployment/recipe.mjs +108 -0
- package/src/deployment/supplement.mjs +51 -0
- package/src/deployment/terminal.mjs +43 -0
- package/src/diagnosis/renderer.mjs +75 -0
- package/src/diagnosis/target-failure.mjs +46 -0
- package/src/evidence/parser-registry.mjs +54 -0
- package/src/evidence/parsers/fasttext-classification.mjs +82 -0
- package/src/evidence/parsers/json-scalar.mjs +96 -0
- package/src/evidence/parsers/simcse-senteval.mjs +104 -0
- package/src/evidence/parsers/starspace-classification.mjs +78 -0
- package/src/evidence/registry.mjs +147 -0
- package/src/execution/runner-audit.mjs +473 -0
- package/src/gcp/auth.mjs +106 -0
- package/src/gcp/batch-client.mjs +120 -0
- package/src/gcp/resource-discovery.mjs +177 -0
- package/src/gcp/rest.mjs +82 -0
- package/src/gcp/secret-manager.mjs +34 -0
- package/src/gcp/signed-url.mjs +133 -0
- package/src/gcp/storage.mjs +220 -0
- package/src/graph/command.mjs +41 -0
- package/src/graph/execution.mjs +97 -0
- package/src/graph/model.mjs +37 -0
- package/src/graph/presentation.mjs +110 -0
- package/src/graph/query.mjs +159 -0
- package/src/graph/research-relations.mjs +69 -0
- package/src/graph/source-page.mjs +12 -0
- package/src/graph/source-preview.mjs +34 -0
- package/src/graph/validate.mjs +76 -0
- package/src/job.mjs +496 -0
- package/src/network/autodl-routing-proxy.mjs +462 -0
- package/src/network/egress-proxy.mjs +158 -0
- package/src/observability/event-contract.mjs +230 -0
- package/src/observability/pipeline-monitor.mjs +166 -0
- package/src/pipeline/orchestrator.mjs +1281 -0
- package/src/pipeline/recovery-error.mjs +11 -0
- package/src/pipeline/replay.mjs +304 -0
- package/src/pipeline/shared-execution.mjs +115 -0
- package/src/pipeline/stage-checkpoint.mjs +86 -0
- package/src/pipeline/stage-recovery.mjs +101 -0
- package/src/pipeline/targets.mjs +110 -0
- package/src/process.mjs +143 -0
- package/src/protocol.mjs +312 -0
- package/src/provider/codex-account.mjs +44 -0
- package/src/provider/codex-completion.mjs +49 -0
- package/src/provider/completion.mjs +292 -0
- package/src/provider/model-client.mjs +44 -0
- package/src/provider/model-route.mjs +29 -0
- package/src/provider/openrouter-readiness.mjs +189 -0
- package/src/provider/reader-bridge.mjs +35 -0
- package/src/provider/relay.mjs +263 -0
- package/src/provider/runtime-auth.mjs +40 -0
- package/src/public/cap.d.mts +90 -0
- package/src/public/cap.mjs +12 -0
- package/src/public/contracts.d.mts +2 -0
- package/src/public/host.mjs +171 -0
- package/src/public/operations.d.mts +11 -0
- package/src/public/presentation.d.mts +4 -0
- package/src/records/views.mjs +26 -0
- package/src/remote/command.mjs +178 -0
- package/src/remote/ssh.mjs +59 -0
- package/src/repository-origin.mjs +81 -0
- package/src/reproduction/evidence-feedback.mjs +96 -0
- package/src/reproduction/incomplete-initialization.mjs +25 -0
- package/src/reproduction/lifecycle.mjs +253 -0
- package/src/reproduction/plan.mjs +132 -0
- package/src/reproduction/prompts.mjs +70 -0
- package/src/reproduction/runner.mjs +188 -0
- package/src/reproduction/summary.mjs +130 -0
- package/src/reproduction/workspace-mode.mjs +7 -0
- package/src/research/automatic-admission.mjs +156 -0
- package/src/research/compiler-coverage.mjs +85 -0
- package/src/research/compiler-failure.mjs +24 -0
- package/src/research/compiler-normalization-guards.mjs +112 -0
- package/src/research/compiler-repair.mjs +3 -0
- package/src/research/compiler.mjs +853 -0
- package/src/research/continuation-selection.mjs +26 -0
- package/src/research/execution-graph-context.mjs +43 -0
- package/src/research/experiment-importance.mjs +15 -0
- package/src/research/inventory-handoff.mjs +104 -0
- package/src/research/inventory-revisions.mjs +32 -0
- package/src/research/mineru-local.mjs +73 -0
- package/src/research/paper-command.mjs +19 -0
- package/src/research/paper-markdown.mjs +180 -0
- package/src/research/paper-source-map.mjs +69 -0
- package/src/research/planning-policy.mjs +88 -0
- package/src/research/reference-materials.mjs +11 -0
- package/src/research/reproduction-scope.mjs +30 -0
- package/src/research/research-map.mjs +94 -0
- package/src/research/research-objects.mjs +88 -0
- package/src/research/source-discovery.mjs +646 -0
- package/src/research/source-observations.mjs +75 -0
- package/src/research/source-review-cli-mcp.mjs +26 -0
- package/src/research/source-review-input.mjs +209 -0
- package/src/research/source-review-local-codex.mjs +36 -0
- package/src/research/source-review-model.mjs +70 -0
- package/src/research/source-review.mjs +173 -0
- package/src/research/structure.mjs +3163 -0
- package/src/research-card/renderer.mjs +277 -0
- package/src/research-card/verified-conclusion.mjs +143 -0
- package/src/results/output-registry.mjs +183 -0
- package/src/runtime/claude-code.mjs +52 -0
- package/src/runtime/codex-capacity-retry.mjs +87 -0
- package/src/runtime/codex.mjs +64 -0
- package/src/runtime/config.mjs +157 -0
- package/src/runtime/final-output.mjs +40 -0
- package/src/runtime/index.mjs +21 -0
- package/src/runtime/local-codex.mjs +74 -0
- package/src/runtime/opencode.mjs +95 -0
- package/src/runtime/prompt.mjs +13 -0
- package/src/sandbox/docker.mjs +363 -0
- package/src/settings/command.mjs +297 -0
- package/src/settings/store.mjs +119 -0
- package/src/telemetry/pricing.mjs +68 -0
- package/src/telemetry/usage.mjs +265 -0
- package/src/terminal/events.mjs +97 -0
- package/src/terminal/input.mjs +40 -0
- package/src/terminal/plain.mjs +40 -0
- package/src/terminal/remote-stream.mjs +22 -0
- package/src/terminal/screen.mjs +214 -0
- package/src/terminal/transcript.mjs +69 -0
- package/src/util.mjs +107 -0
- package/src/verification/ai-assessor.mjs +534 -0
- package/src/verification/claim-evaluator.mjs +242 -0
- package/src/verification/evidence-context.mjs +165 -0
- package/src/verification/evidence-reader.mjs +95 -0
- package/src/verification/integrity.mjs +570 -0
- package/src/verification/tolerance.mjs +32 -0
- package/src/workloads/cpu-research-preparation.mjs +56 -0
- package/src/workloads/definition.mjs +74 -0
- package/src/workloads/phase-aware-reproduction.mjs +46 -0
- package/src/workloads/reproduction.mjs +85 -0
- package/src/workspace/command.mjs +242 -0
- package/src/workspace/control.mjs +49 -0
- package/src/workspace/entry.mjs +28 -0
- package/src/workspace/input.mjs +93 -0
- package/src/workspace/interactive.mjs +94 -0
- package/src/workspace/jobs.mjs +418 -0
- package/src/workspace/session.mjs +97 -0
- package/src/workspace/worker.mjs +137 -0
- package/ui/arkgraph/ambient-motion.mjs +10 -0
- package/ui/arkgraph/app.jsx +153 -0
- package/ui/arkgraph/boot.js +6 -0
- package/ui/arkgraph/camera-motion.mjs +20 -0
- package/ui/arkgraph/context-reveal.mjs +39 -0
- package/ui/arkgraph/details.css +3 -0
- package/ui/arkgraph/entry.jsx +28 -0
- package/ui/arkgraph/experiment-curves.mjs +17 -0
- package/ui/arkgraph/experiment-selection.mjs +15 -0
- package/ui/arkgraph/experiment-style.css +26 -0
- package/ui/arkgraph/experiment-ui.jsx +32 -0
- package/ui/arkgraph/frame.html +1 -0
- package/ui/arkgraph/graph-gestures.mjs +62 -0
- package/ui/arkgraph/label-layout.mjs +57 -0
- package/ui/arkgraph/locales/en.json +229 -0
- package/ui/arkgraph/locales/source-types.json +15 -0
- package/ui/arkgraph/localization-build.mjs +27 -0
- package/ui/arkgraph/material-build.mjs +23 -0
- package/ui/arkgraph/material-colors.mjs +39 -0
- package/ui/arkgraph/material-style.css +15 -0
- package/ui/arkgraph/open-graph.jsx +326 -0
- package/ui/arkgraph/outline.jsx +49 -0
- package/ui/arkgraph/package-lock.json +888 -0
- package/ui/arkgraph/package.json +17 -0
- package/ui/arkgraph/reading-layout.mjs +130 -0
- package/ui/arkgraph/reading-presentation.mjs +73 -0
- package/ui/arkgraph/record-detail.css +51 -0
- package/ui/arkgraph/record-details.jsx +29 -0
- package/ui/arkgraph/research-types.mjs +31 -0
- package/ui/arkgraph/selection-mark.jsx +6 -0
- package/ui/arkgraph/soft-spine.mjs +26 -0
- package/ui/arkgraph/steering-style.css +187 -0
- package/ui/arkgraph/style.css +272 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
|
|
3
|
+
import { CiteArkError } from "../util.mjs";
|
|
4
|
+
|
|
5
|
+
const RETRYABLE_HTTP_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
6
|
+
const DEFAULT_BASE_URL = "https://api.autodl.com";
|
|
7
|
+
|
|
8
|
+
export class AutoDlApiError extends CiteArkError {
|
|
9
|
+
constructor(message, { status = null, code = null, retryable = false, cause } = {}) {
|
|
10
|
+
super(message, { cause });
|
|
11
|
+
this.name = "AutoDlApiError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.retryable = retryable;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AutoDlProClient {
|
|
19
|
+
constructor({
|
|
20
|
+
token,
|
|
21
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
22
|
+
transport = nodeHttpsJsonRequest,
|
|
23
|
+
sleep = defaultSleep,
|
|
24
|
+
timeoutMs = 30_000,
|
|
25
|
+
maxAttempts = 5,
|
|
26
|
+
} = {}) {
|
|
27
|
+
this.token = required(token, "token");
|
|
28
|
+
this.baseUrl = required(baseUrl, "baseUrl").replace(/\/$/, "");
|
|
29
|
+
this.transport = transport;
|
|
30
|
+
this.sleep = sleep;
|
|
31
|
+
this.timeoutMs = positiveInteger(timeoutMs, "timeoutMs");
|
|
32
|
+
this.maxAttempts = positiveInteger(maxAttempts, "maxAttempts");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createInstance(input) {
|
|
36
|
+
const data = await this.#request("POST", "/api/v1/dev/instance/pro/create", {
|
|
37
|
+
data_center_list: stringArray(input.dataCenterList),
|
|
38
|
+
req_gpu_amount: positiveInteger(input.gpuCount ?? 1, "gpuCount"),
|
|
39
|
+
expand_system_disk_by_gb: nonNegativeInteger(
|
|
40
|
+
input.expandSystemDiskGb ?? 0,
|
|
41
|
+
"expandSystemDiskGb",
|
|
42
|
+
),
|
|
43
|
+
gpu_spec_uuid: required(input.gpuSpecUuid, "gpuSpecUuid"),
|
|
44
|
+
image_uuid: required(input.imageUuid, "imageUuid"),
|
|
45
|
+
cuda_v_from: positiveInteger(input.cudaVersionFrom ?? 118, "cudaVersionFrom"),
|
|
46
|
+
instance_name: optional(input.instanceName),
|
|
47
|
+
start_command: optional(input.startCommand),
|
|
48
|
+
});
|
|
49
|
+
return required(data, "createInstance.data");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getInstance(instanceUuid) {
|
|
53
|
+
return await this.#request("GET", "/api/v1/dev/instance/pro/snapshot", {
|
|
54
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getStatus(instanceUuid) {
|
|
59
|
+
return await this.#request("GET", "/api/v1/dev/instance/pro/status", {
|
|
60
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async listInstances({ pageIndex = 1, pageSize = 100 } = {}) {
|
|
65
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/list", {
|
|
66
|
+
page_index: positiveInteger(pageIndex, "pageIndex"),
|
|
67
|
+
page_size: positiveInteger(pageSize, "pageSize"),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async saveInstanceImage(instanceUuid, imageName) {
|
|
72
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/image/save", {
|
|
73
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
74
|
+
image_name: required(imageName, "imageName"),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async listPrivateImages({ pageIndex = 1, pageSize = 100 } = {}) {
|
|
79
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/image/private/list", {
|
|
80
|
+
page_index: positiveInteger(pageIndex, "pageIndex"),
|
|
81
|
+
page_size: positiveInteger(pageSize, "pageSize"),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async powerOn(instanceUuid, { startCommand } = {}) {
|
|
86
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/power_on", {
|
|
87
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
88
|
+
payload: "gpu",
|
|
89
|
+
start_command: optional(startCommand),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async powerOff(instanceUuid) {
|
|
94
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/power_off", {
|
|
95
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async release(instanceUuid) {
|
|
100
|
+
return await this.#request("POST", "/api/v1/dev/instance/pro/release", {
|
|
101
|
+
instance_uuid: required(instanceUuid, "instanceUuid"),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async waitForStatus(instanceUuid, acceptedStatuses, {
|
|
106
|
+
timeoutMs = 15 * 60_000,
|
|
107
|
+
pollIntervalMs = 5_000,
|
|
108
|
+
onStatus = () => {},
|
|
109
|
+
} = {}) {
|
|
110
|
+
const accepted = new Set(
|
|
111
|
+
(Array.isArray(acceptedStatuses) ? acceptedStatuses : [acceptedStatuses])
|
|
112
|
+
.map((value) => String(value).trim().toLowerCase())
|
|
113
|
+
.filter(Boolean),
|
|
114
|
+
);
|
|
115
|
+
if (!accepted.size) throw new TypeError("AutoDL acceptedStatuses 不能为空");
|
|
116
|
+
const deadline = Date.now() + positiveInteger(timeoutMs, "timeoutMs");
|
|
117
|
+
let previous = null;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const status = String(await this.getStatus(instanceUuid)).trim().toLowerCase();
|
|
120
|
+
if (status !== previous) {
|
|
121
|
+
previous = status;
|
|
122
|
+
await onStatus(status);
|
|
123
|
+
}
|
|
124
|
+
if (accepted.has(status)) return status;
|
|
125
|
+
if (new Set(["error", "failed", "released"]).has(status)) {
|
|
126
|
+
throw new AutoDlApiError(`AutoDL 实例 ${instanceUuid} 进入异常状态:${status}`);
|
|
127
|
+
}
|
|
128
|
+
await this.sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
|
|
129
|
+
}
|
|
130
|
+
throw new AutoDlApiError(
|
|
131
|
+
`等待 AutoDL 实例 ${instanceUuid} 进入 ${[...accepted].join("/")} 超时`,
|
|
132
|
+
{ retryable: true },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async #request(method, pathname, body) {
|
|
137
|
+
let lastError;
|
|
138
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
|
|
139
|
+
try {
|
|
140
|
+
const requestUrl = new URL(`${this.baseUrl}${pathname}`);
|
|
141
|
+
const requestBody = method === "GET" ? undefined : body;
|
|
142
|
+
if (method === "GET") {
|
|
143
|
+
for (const [key, value] of Object.entries(body ?? {})) {
|
|
144
|
+
if (value !== undefined && value !== null) {
|
|
145
|
+
requestUrl.searchParams.set(key, String(value));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const response = await this.transport({
|
|
150
|
+
url: requestUrl.toString(),
|
|
151
|
+
method,
|
|
152
|
+
headers: {
|
|
153
|
+
authorization: this.token,
|
|
154
|
+
...(requestBody === undefined ? {} : { "content-type": "application/json" }),
|
|
155
|
+
},
|
|
156
|
+
body: requestBody,
|
|
157
|
+
timeoutMs: this.timeoutMs,
|
|
158
|
+
});
|
|
159
|
+
const payload = isRecord(response.body) ? response.body : {};
|
|
160
|
+
const apiCode = typeof payload.code === "string" ? payload.code : null;
|
|
161
|
+
if (response.status >= 200 && response.status < 300 && apiCode === "Success") {
|
|
162
|
+
return payload.data;
|
|
163
|
+
}
|
|
164
|
+
const retryable = RETRYABLE_HTTP_STATUS.has(response.status)
|
|
165
|
+
|| /busy|timeout|temporar|稍后|繁忙|库存/i.test(String(payload.msg ?? ""));
|
|
166
|
+
throw new AutoDlApiError(
|
|
167
|
+
`AutoDL API ${pathname} 失败:${String(payload.msg || apiCode || `HTTP ${response.status}`).slice(0, 500)}`,
|
|
168
|
+
{ status: response.status, code: apiCode, retryable },
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
lastError = error;
|
|
172
|
+
const retryable = error instanceof AutoDlApiError ? error.retryable : true;
|
|
173
|
+
if (!retryable || attempt >= this.maxAttempts) break;
|
|
174
|
+
await this.sleep(backoffMilliseconds(attempt));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (lastError instanceof AutoDlApiError) throw lastError;
|
|
178
|
+
throw new AutoDlApiError(
|
|
179
|
+
`AutoDL API 请求失败:${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
180
|
+
{ retryable: true, cause: lastError },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function nodeHttpsJsonRequest({ url, method, headers, body, timeoutMs }) {
|
|
186
|
+
const serialized = body === undefined ? null : JSON.stringify(body ?? {});
|
|
187
|
+
return await new Promise((resolve, reject) => {
|
|
188
|
+
const request = https.request(url, {
|
|
189
|
+
method,
|
|
190
|
+
headers: {
|
|
191
|
+
...headers,
|
|
192
|
+
...(serialized === null ? {} : { "content-length": Buffer.byteLength(serialized) }),
|
|
193
|
+
},
|
|
194
|
+
timeout: timeoutMs,
|
|
195
|
+
}, (response) => {
|
|
196
|
+
const chunks = [];
|
|
197
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
198
|
+
response.on("end", () => {
|
|
199
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
200
|
+
let parsed = {};
|
|
201
|
+
try {
|
|
202
|
+
parsed = text ? JSON.parse(text) : {};
|
|
203
|
+
} catch {
|
|
204
|
+
parsed = { msg: text.slice(0, 500) };
|
|
205
|
+
}
|
|
206
|
+
resolve({ status: response.statusCode ?? 0, body: parsed });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
request.on("timeout", () => request.destroy(new Error("AutoDL API 请求超时")));
|
|
210
|
+
request.on("error", reject);
|
|
211
|
+
request.end(serialized ?? undefined);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function required(value, field) {
|
|
216
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
217
|
+
throw new TypeError(`AutoDL 缺少 ${field}`);
|
|
218
|
+
}
|
|
219
|
+
return value.trim();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function optional(value) {
|
|
223
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stringArray(value) {
|
|
227
|
+
if (value === undefined || value === null) return undefined;
|
|
228
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) {
|
|
229
|
+
throw new TypeError("AutoDL dataCenterList 必须是非空字符串数组");
|
|
230
|
+
}
|
|
231
|
+
return value.map((item) => item.trim());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function positiveInteger(value, field) {
|
|
235
|
+
const numeric = Number(value);
|
|
236
|
+
if (!Number.isInteger(numeric) || numeric <= 0) {
|
|
237
|
+
throw new TypeError(`AutoDL ${field} 必须是正整数`);
|
|
238
|
+
}
|
|
239
|
+
return numeric;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function nonNegativeInteger(value, field) {
|
|
243
|
+
const numeric = Number(value);
|
|
244
|
+
if (!Number.isInteger(numeric) || numeric < 0) {
|
|
245
|
+
throw new TypeError(`AutoDL ${field} 必须是非负整数`);
|
|
246
|
+
}
|
|
247
|
+
return numeric;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isRecord(value) {
|
|
251
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function backoffMilliseconds(attempt) {
|
|
255
|
+
return Math.min(15_000, 500 * 2 ** Math.max(0, attempt - 1));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function defaultSleep(milliseconds) {
|
|
259
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
260
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import ssh2 from "ssh2";
|
|
2
|
+
|
|
3
|
+
import { CiteArkError } from "../util.mjs";
|
|
4
|
+
|
|
5
|
+
const { Client } = ssh2;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 2 * 60_000;
|
|
8
|
+
const DEFAULT_TRANSFER_TIMEOUT_MS = 30 * 60_000;
|
|
9
|
+
const DEFAULT_TRANSFER_CONCURRENCY = 4;
|
|
10
|
+
|
|
11
|
+
export class AutoDlSshClient {
|
|
12
|
+
constructor({
|
|
13
|
+
host,
|
|
14
|
+
port,
|
|
15
|
+
username = "root",
|
|
16
|
+
password,
|
|
17
|
+
readyTimeoutMs = 60_000,
|
|
18
|
+
operationTimeoutMs = DEFAULT_OPERATION_TIMEOUT_MS,
|
|
19
|
+
transferTimeoutMs = DEFAULT_TRANSFER_TIMEOUT_MS,
|
|
20
|
+
transferConcurrency = DEFAULT_TRANSFER_CONCURRENCY,
|
|
21
|
+
clientFactory = () => new Client(),
|
|
22
|
+
} = {}) {
|
|
23
|
+
this.host = required(host, "host");
|
|
24
|
+
this.port = positiveInteger(port, "port");
|
|
25
|
+
this.username = required(username, "username");
|
|
26
|
+
this.password = required(password, "password");
|
|
27
|
+
this.readyTimeoutMs = positiveInteger(readyTimeoutMs, "readyTimeoutMs");
|
|
28
|
+
this.operationTimeoutMs = positiveInteger(operationTimeoutMs, "operationTimeoutMs");
|
|
29
|
+
this.transferTimeoutMs = positiveInteger(transferTimeoutMs, "transferTimeoutMs");
|
|
30
|
+
this.transferConcurrency = positiveInteger(
|
|
31
|
+
transferConcurrency,
|
|
32
|
+
"transferConcurrency",
|
|
33
|
+
);
|
|
34
|
+
this.clientFactory = clientFactory;
|
|
35
|
+
this.client = null;
|
|
36
|
+
this.connected = false;
|
|
37
|
+
this.connecting = null;
|
|
38
|
+
this.pendingOperations = new Set();
|
|
39
|
+
this.fingerprint = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async connect() {
|
|
43
|
+
if (this.client && this.connected) return this;
|
|
44
|
+
if (this.connecting) {
|
|
45
|
+
await this.connecting;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
const client = this.clientFactory();
|
|
49
|
+
this.client = client;
|
|
50
|
+
const connecting = new Promise((resolve, reject) => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
const finish = (callback, value) => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
callback(value);
|
|
56
|
+
};
|
|
57
|
+
const disconnect = (message, cause) => {
|
|
58
|
+
const error = new CiteArkError(message, { cause });
|
|
59
|
+
this.#handleDisconnect(client, error);
|
|
60
|
+
finish(reject, error);
|
|
61
|
+
};
|
|
62
|
+
client.once("ready", () => {
|
|
63
|
+
if (this.client !== client) {
|
|
64
|
+
finish(reject, new CiteArkError("AutoDL SSH 连接在就绪前已失效"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.connected = true;
|
|
68
|
+
finish(resolve);
|
|
69
|
+
});
|
|
70
|
+
client.on("error", (error) => disconnect(
|
|
71
|
+
`AutoDL SSH 连接错误:${safeMessage(error)}`,
|
|
72
|
+
error,
|
|
73
|
+
));
|
|
74
|
+
client.once("end", () => disconnect("AutoDL SSH 连接已由远端结束"));
|
|
75
|
+
client.once("close", () => disconnect("AutoDL SSH 连接已关闭"));
|
|
76
|
+
try {
|
|
77
|
+
client.connect({
|
|
78
|
+
host: this.host,
|
|
79
|
+
port: this.port,
|
|
80
|
+
username: this.username,
|
|
81
|
+
password: this.password,
|
|
82
|
+
readyTimeout: this.readyTimeoutMs,
|
|
83
|
+
keepaliveInterval: 15_000,
|
|
84
|
+
keepaliveCountMax: 4,
|
|
85
|
+
hostHash: "sha256",
|
|
86
|
+
hostVerifier: (fingerprint) => {
|
|
87
|
+
if (!this.fingerprint) {
|
|
88
|
+
this.fingerprint = fingerprint;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return this.fingerprint === fingerprint;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
disconnect(
|
|
96
|
+
`无法连接 AutoDL SSH 节点:${safeMessage(error)}`,
|
|
97
|
+
error,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
this.connecting = connecting;
|
|
102
|
+
try {
|
|
103
|
+
await connecting;
|
|
104
|
+
return this;
|
|
105
|
+
} finally {
|
|
106
|
+
if (this.connecting === connecting) this.connecting = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async exec(command, {
|
|
111
|
+
timeoutMs = 0,
|
|
112
|
+
signal,
|
|
113
|
+
onStdout = () => {},
|
|
114
|
+
onStderr = () => {},
|
|
115
|
+
maximumCaptureCharacters = 16_000,
|
|
116
|
+
} = {}) {
|
|
117
|
+
const operationTimeoutMs = timeoutMs > 0 ? timeoutMs + 45_000 : null;
|
|
118
|
+
return await this.#runOperation("AutoDL SSH 远程命令", async (client) => {
|
|
119
|
+
return await new Promise((resolve, reject) => {
|
|
120
|
+
if (signal?.aborted) {
|
|
121
|
+
reject(signal.reason instanceof Error
|
|
122
|
+
? signal.reason
|
|
123
|
+
: new CiteArkError("AutoDL SSH 远程命令已取消"));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
client.exec(command, (error, stream) => {
|
|
127
|
+
if (error) {
|
|
128
|
+
reject(new CiteArkError(`无法启动 AutoDL 远程命令:${safeMessage(error)}`, { cause: error }));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let stdout = "";
|
|
132
|
+
let stderr = "";
|
|
133
|
+
let timedOut = false;
|
|
134
|
+
let timer;
|
|
135
|
+
let abortTimer;
|
|
136
|
+
const abort = () => {
|
|
137
|
+
if (timer) clearTimeout(timer);
|
|
138
|
+
stream.signal("TERM");
|
|
139
|
+
abortTimer = setTimeout(() => stream.close(), 30_000);
|
|
140
|
+
abortTimer.unref();
|
|
141
|
+
reject(signal?.reason instanceof Error
|
|
142
|
+
? signal.reason
|
|
143
|
+
: new CiteArkError("AutoDL SSH 远程命令已取消"));
|
|
144
|
+
};
|
|
145
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
146
|
+
const append = (current, chunk) => {
|
|
147
|
+
const combined = `${current}${chunk}`;
|
|
148
|
+
return combined.slice(-maximumCaptureCharacters);
|
|
149
|
+
};
|
|
150
|
+
stream.on("data", (chunk) => {
|
|
151
|
+
const text = chunk.toString();
|
|
152
|
+
stdout = append(stdout, text);
|
|
153
|
+
onStdout(text);
|
|
154
|
+
});
|
|
155
|
+
stream.stderr.on("data", (chunk) => {
|
|
156
|
+
const text = chunk.toString();
|
|
157
|
+
stderr = append(stderr, text);
|
|
158
|
+
onStderr(text);
|
|
159
|
+
});
|
|
160
|
+
stream.once("error", (streamError) => {
|
|
161
|
+
if (timer) clearTimeout(timer);
|
|
162
|
+
if (abortTimer) clearTimeout(abortTimer);
|
|
163
|
+
signal?.removeEventListener("abort", abort);
|
|
164
|
+
reject(new CiteArkError(`AutoDL SSH 数据流中断:${safeMessage(streamError)}`, {
|
|
165
|
+
cause: streamError,
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
stream.once("close", (code, remoteSignal) => {
|
|
169
|
+
if (timer) clearTimeout(timer);
|
|
170
|
+
if (abortTimer) clearTimeout(abortTimer);
|
|
171
|
+
signal?.removeEventListener("abort", abort);
|
|
172
|
+
resolve({
|
|
173
|
+
code: Number.isInteger(code) ? code : 1,
|
|
174
|
+
signal: remoteSignal ?? null,
|
|
175
|
+
stdout,
|
|
176
|
+
stderr,
|
|
177
|
+
timedOut,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
if (timeoutMs > 0) {
|
|
181
|
+
timer = setTimeout(() => {
|
|
182
|
+
timedOut = true;
|
|
183
|
+
stream.signal("TERM");
|
|
184
|
+
setTimeout(() => stream.close(), 30_000).unref();
|
|
185
|
+
}, timeoutMs);
|
|
186
|
+
timer.unref();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}, operationTimeoutMs);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async putFile(localPath, remotePath, { timeoutMs = this.transferTimeoutMs } = {}) {
|
|
194
|
+
return await this.#runSftpOperation("AutoDL SFTP 上传", timeoutMs, async (sftp) => {
|
|
195
|
+
await callbackPromise((done) => sftp.fastPut(localPath, remotePath, {
|
|
196
|
+
concurrency: this.transferConcurrency,
|
|
197
|
+
}, done));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async getFile(remotePath, localPath, {
|
|
202
|
+
timeoutMs = this.transferTimeoutMs,
|
|
203
|
+
onProgress,
|
|
204
|
+
} = {}) {
|
|
205
|
+
return await this.#runSftpOperation("AutoDL SFTP 下载", timeoutMs, async (sftp) => {
|
|
206
|
+
const options = { concurrency: this.transferConcurrency };
|
|
207
|
+
if (typeof onProgress === "function") {
|
|
208
|
+
options.step = (transferredBytes, chunkBytes, totalBytes) => {
|
|
209
|
+
onProgress({ transferredBytes, chunkBytes, totalBytes });
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
await callbackPromise((done) => sftp.fastGet(
|
|
213
|
+
remotePath,
|
|
214
|
+
localPath,
|
|
215
|
+
options,
|
|
216
|
+
done,
|
|
217
|
+
));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async readFile(remotePath, {
|
|
222
|
+
maximumBytes = 4 * 1024 * 1024,
|
|
223
|
+
timeoutMs = this.operationTimeoutMs,
|
|
224
|
+
} = {}) {
|
|
225
|
+
return await this.#runSftpOperation("AutoDL SFTP 读取", timeoutMs, async (sftp) => {
|
|
226
|
+
const attributes = await callbackPromise((done) => sftp.stat(remotePath, done));
|
|
227
|
+
if (Number(attributes?.size) > maximumBytes) {
|
|
228
|
+
throw new CiteArkError(`AutoDL 远程文件超过读取上限:${remotePath}`);
|
|
229
|
+
}
|
|
230
|
+
const bytes = await callbackPromise((done) => sftp.readFile(remotePath, done));
|
|
231
|
+
if (bytes.length > maximumBytes) {
|
|
232
|
+
throw new CiteArkError(`AutoDL 远程文件超过读取上限:${remotePath}`);
|
|
233
|
+
}
|
|
234
|
+
return bytes;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async writeFile(remotePath, content, {
|
|
239
|
+
mode = 0o600,
|
|
240
|
+
timeoutMs = this.operationTimeoutMs,
|
|
241
|
+
} = {}) {
|
|
242
|
+
return await this.#runSftpOperation("AutoDL SFTP 写入", timeoutMs, async (sftp) => {
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
const stream = sftp.createWriteStream(remotePath, { mode });
|
|
245
|
+
stream.once("error", reject);
|
|
246
|
+
stream.once("close", resolve);
|
|
247
|
+
stream.end(content);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async exists(remotePath, { timeoutMs = this.operationTimeoutMs } = {}) {
|
|
253
|
+
return await this.#runSftpOperation("AutoDL SFTP 状态检查", timeoutMs, async (sftp) => {
|
|
254
|
+
return await new Promise((resolve, reject) => {
|
|
255
|
+
sftp.stat(remotePath, (error) => {
|
|
256
|
+
if (!error) return resolve(true);
|
|
257
|
+
if (error.code === 2 || error.code === "ENOENT") return resolve(false);
|
|
258
|
+
reject(error);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async list(remotePath, { timeoutMs = this.operationTimeoutMs } = {}) {
|
|
265
|
+
return await this.#runSftpOperation("AutoDL SFTP 目录读取", timeoutMs, async (sftp) => {
|
|
266
|
+
return await callbackPromise((done) => sftp.readdir(remotePath, done));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
close() {
|
|
271
|
+
const client = this.client;
|
|
272
|
+
const error = new CiteArkError("AutoDL SSH 客户端已关闭");
|
|
273
|
+
this.#handleDisconnect(client, error);
|
|
274
|
+
client?.end();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async #runSftpOperation(label, timeoutMs, operation) {
|
|
278
|
+
return await this.#runOperation(label, async (client) => {
|
|
279
|
+
const sftp = await callbackPromise((done) => client.sftp(done));
|
|
280
|
+
try {
|
|
281
|
+
return await operation(sftp);
|
|
282
|
+
} finally {
|
|
283
|
+
sftp.end();
|
|
284
|
+
}
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async #runOperation(label, operation, timeoutMs) {
|
|
289
|
+
await this.connect();
|
|
290
|
+
const client = this.client;
|
|
291
|
+
if (!client || !this.connected) {
|
|
292
|
+
throw new CiteArkError(`${label}无法开始:SSH 连接不可用`);
|
|
293
|
+
}
|
|
294
|
+
const boundedTimeoutMs = timeoutMs === null
|
|
295
|
+
? null
|
|
296
|
+
: positiveInteger(timeoutMs ?? this.operationTimeoutMs, "timeoutMs");
|
|
297
|
+
return await new Promise((resolve, reject) => {
|
|
298
|
+
let settled = false;
|
|
299
|
+
let timer;
|
|
300
|
+
const finish = (callback, value) => {
|
|
301
|
+
if (settled) return;
|
|
302
|
+
settled = true;
|
|
303
|
+
if (timer) clearTimeout(timer);
|
|
304
|
+
this.pendingOperations.delete(fail);
|
|
305
|
+
callback(value);
|
|
306
|
+
};
|
|
307
|
+
const fail = (error) => finish(reject, error);
|
|
308
|
+
this.pendingOperations.add(fail);
|
|
309
|
+
if (boundedTimeoutMs !== null) {
|
|
310
|
+
timer = setTimeout(() => {
|
|
311
|
+
const error = new CiteArkError(
|
|
312
|
+
`${label}超时(${Math.ceil(boundedTimeoutMs / 1000)} 秒)`,
|
|
313
|
+
);
|
|
314
|
+
this.#handleDisconnect(client, error);
|
|
315
|
+
client.destroy();
|
|
316
|
+
}, boundedTimeoutMs);
|
|
317
|
+
}
|
|
318
|
+
Promise.resolve()
|
|
319
|
+
.then(() => operation(client))
|
|
320
|
+
.then((value) => finish(resolve, value), fail);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#handleDisconnect(client, error) {
|
|
325
|
+
if (!client || this.client !== client) return;
|
|
326
|
+
this.client = null;
|
|
327
|
+
this.connected = false;
|
|
328
|
+
const pending = [...this.pendingOperations];
|
|
329
|
+
this.pendingOperations.clear();
|
|
330
|
+
for (const reject of pending) reject(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function parseAutoDlSshConnection(snapshot) {
|
|
335
|
+
const explicitHost = text(snapshot?.proxy_host);
|
|
336
|
+
const explicitPort = Number(snapshot?.ssh_port);
|
|
337
|
+
const command = text(snapshot?.ssh_command);
|
|
338
|
+
const matched = command?.match(/^ssh\s+-p\s+(\d+)\s+([^@\s]+)@([^\s]+)$/i);
|
|
339
|
+
const host = explicitHost ?? matched?.[3] ?? null;
|
|
340
|
+
const port = Number.isInteger(explicitPort) && explicitPort > 0
|
|
341
|
+
? explicitPort
|
|
342
|
+
: Number(matched?.[1]);
|
|
343
|
+
const username = matched?.[2] ?? "root";
|
|
344
|
+
const password = text(snapshot?.root_password);
|
|
345
|
+
if (!host || !Number.isInteger(port) || port <= 0 || !password) {
|
|
346
|
+
throw new CiteArkError("AutoDL 实例尚未返回完整 SSH 连接信息");
|
|
347
|
+
}
|
|
348
|
+
return { host, port, username, password };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function callbackPromise(register) {
|
|
352
|
+
return await new Promise((resolve, reject) => {
|
|
353
|
+
register((error, value) => error ? reject(error) : resolve(value));
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function required(value, field) {
|
|
358
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
359
|
+
throw new TypeError(`AutoDL SSH 缺少 ${field}`);
|
|
360
|
+
}
|
|
361
|
+
return value.trim();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function positiveInteger(value, field) {
|
|
365
|
+
const numeric = Number(value);
|
|
366
|
+
if (!Number.isInteger(numeric) || numeric <= 0) {
|
|
367
|
+
throw new TypeError(`AutoDL SSH ${field} 必须是正整数`);
|
|
368
|
+
}
|
|
369
|
+
return numeric;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function text(value) {
|
|
373
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function safeMessage(error) {
|
|
377
|
+
return String(error instanceof Error ? error.message : error)
|
|
378
|
+
.replaceAll(/(password|token|secret)\s*[=:]\s*\S+/gi, "$1=<redacted>")
|
|
379
|
+
.slice(0, 500);
|
|
380
|
+
}
|