@minato-aqukin/autodl-cli 0.1.0
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 +21 -0
- package/README.md +415 -0
- package/README.zh-CN.md +383 -0
- package/dist/app-4E2WPQ3C.js +36151 -0
- package/dist/chunk-2TWWLI3Y.js +134 -0
- package/dist/chunk-5RZ4Q7T4.js +1458 -0
- package/dist/chunk-6QR4ZYQR.js +1432 -0
- package/dist/chunk-CWCG3MOC.js +433 -0
- package/dist/chunk-NBWWTYSL.js +46 -0
- package/dist/cli.js +1058 -0
- package/dist/index.d.ts +948 -0
- package/dist/index.js +188 -0
- package/dist/ink-devtools-QXLFHNC2.js +4 -0
- package/dist/tui-AARJCHRA.js +16 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __nodeCreateRequire } from "node:module";
|
|
3
|
+
const require = __nodeCreateRequire(import.meta.url);
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
11
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
12
|
+
}) : x)(function(x) {
|
|
13
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
14
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
|
+
});
|
|
16
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
+
for (let key of __getOwnPropNames(from))
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
+
}
|
|
29
|
+
return to;
|
|
30
|
+
};
|
|
31
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
+
mod
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
// src/core/errors.ts
|
|
41
|
+
var ExitCode = {
|
|
42
|
+
OK: 0,
|
|
43
|
+
GENERIC: 1,
|
|
44
|
+
USAGE: 2,
|
|
45
|
+
AUTH: 3,
|
|
46
|
+
NOT_FOUND: 4,
|
|
47
|
+
BUDGET: 5,
|
|
48
|
+
NO_STOCK: 6,
|
|
49
|
+
TIMEOUT: 7,
|
|
50
|
+
SSH: 8
|
|
51
|
+
};
|
|
52
|
+
var AutoDLError = class extends Error {
|
|
53
|
+
code;
|
|
54
|
+
exitCode;
|
|
55
|
+
hint;
|
|
56
|
+
requestId;
|
|
57
|
+
details;
|
|
58
|
+
constructor(message, options2 = {}) {
|
|
59
|
+
super(message, options2.cause !== void 0 ? { cause: options2.cause } : void 0);
|
|
60
|
+
this.name = "AutoDLError";
|
|
61
|
+
this.code = options2.code ?? "GENERIC";
|
|
62
|
+
this.exitCode = options2.exitCode ?? ExitCode.GENERIC;
|
|
63
|
+
this.hint = options2.hint;
|
|
64
|
+
this.requestId = options2.requestId;
|
|
65
|
+
this.details = options2.details;
|
|
66
|
+
}
|
|
67
|
+
toJSON() {
|
|
68
|
+
return {
|
|
69
|
+
code: this.code,
|
|
70
|
+
message: this.message,
|
|
71
|
+
...this.hint ? { hint: this.hint } : {},
|
|
72
|
+
...this.requestId ? { requestId: this.requestId } : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var AuthError = class extends AutoDLError {
|
|
77
|
+
constructor(message, options2 = {}) {
|
|
78
|
+
super(message, { code: "AUTH_INVALID", exitCode: ExitCode.AUTH, ...options2 });
|
|
79
|
+
this.name = "AuthError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var NotFoundError = class extends AutoDLError {
|
|
83
|
+
constructor(message, options2 = {}) {
|
|
84
|
+
super(message, { code: "NOT_FOUND", exitCode: ExitCode.NOT_FOUND, ...options2 });
|
|
85
|
+
this.name = "NotFoundError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var UsageError = class extends AutoDLError {
|
|
89
|
+
constructor(message, options2 = {}) {
|
|
90
|
+
super(message, { code: "USAGE", exitCode: ExitCode.USAGE, ...options2 });
|
|
91
|
+
this.name = "UsageError";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var BudgetError = class extends AutoDLError {
|
|
95
|
+
constructor(message, options2 = {}) {
|
|
96
|
+
super(message, { code: "GUARD_BLOCKED", exitCode: ExitCode.BUDGET, ...options2 });
|
|
97
|
+
this.name = "BudgetError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var NoStockError = class extends AutoDLError {
|
|
101
|
+
constructor(message, options2 = {}) {
|
|
102
|
+
super(message, { code: "NO_STOCK", exitCode: ExitCode.NO_STOCK, ...options2 });
|
|
103
|
+
this.name = "NoStockError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var TimeoutError = class extends AutoDLError {
|
|
107
|
+
constructor(message, options2 = {}) {
|
|
108
|
+
super(message, { code: "TIMEOUT", exitCode: ExitCode.TIMEOUT, ...options2 });
|
|
109
|
+
this.name = "TimeoutError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var SSHError = class extends AutoDLError {
|
|
113
|
+
constructor(message, options2 = {}) {
|
|
114
|
+
super(message, { code: "SSH_FAILED", exitCode: ExitCode.SSH, ...options2 });
|
|
115
|
+
this.name = "SSHError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function toAutoDLError(err) {
|
|
119
|
+
if (err instanceof AutoDLError) return err;
|
|
120
|
+
if (err instanceof Error) {
|
|
121
|
+
return new AutoDLError(err.message, { cause: err });
|
|
122
|
+
}
|
|
123
|
+
return new AutoDLError(String(err));
|
|
124
|
+
}
|
|
125
|
+
function isAuthError(err) {
|
|
126
|
+
return err instanceof AutoDLError && (err.code === "AUTH_INVALID" || err.code === "AUTH_MISSING");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/config/store.ts
|
|
130
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
131
|
+
import { homedir } from "os";
|
|
132
|
+
import { dirname, join } from "path";
|
|
133
|
+
function configDir() {
|
|
134
|
+
const override = process.env.AUTODL_CONFIG_DIR;
|
|
135
|
+
if (override) return override;
|
|
136
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
137
|
+
return join(xdg?.trim() ? xdg : join(homedir(), ".config"), "autodl-cli");
|
|
138
|
+
}
|
|
139
|
+
var configPath = () => join(configDir(), "config.json");
|
|
140
|
+
function readConfig() {
|
|
141
|
+
const file = configPath();
|
|
142
|
+
if (!existsSync(file)) return {};
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
145
|
+
} catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function writeConfig(config) {
|
|
150
|
+
const file = configPath();
|
|
151
|
+
mkdirSync(dirname(file), { recursive: true, mode: 448 });
|
|
152
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
153
|
+
`, { mode: 384 });
|
|
154
|
+
chmodSync(file, 384);
|
|
155
|
+
}
|
|
156
|
+
function updateConfig(patch) {
|
|
157
|
+
const next = { ...readConfig(), ...patch };
|
|
158
|
+
writeConfig(next);
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
function clearToken() {
|
|
162
|
+
const config = readConfig();
|
|
163
|
+
delete config.token;
|
|
164
|
+
if (Object.keys(config).length === 0) {
|
|
165
|
+
if (existsSync(configPath())) rmSync(configPath());
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
writeConfig(config);
|
|
169
|
+
}
|
|
170
|
+
function resolveToken(explicit) {
|
|
171
|
+
if (explicit?.trim()) return { token: explicit.trim(), source: "flag" };
|
|
172
|
+
const fromEnv = process.env.AUTODL_TOKEN;
|
|
173
|
+
if (fromEnv?.trim()) return { token: fromEnv.trim(), source: "env" };
|
|
174
|
+
const fromConfig = readConfig().token;
|
|
175
|
+
if (fromConfig?.trim()) return { token: fromConfig.trim(), source: "config" };
|
|
176
|
+
throw new AuthError("\u672A\u914D\u7F6E AutoDL \u5F00\u53D1\u8005 Token", {
|
|
177
|
+
code: "AUTH_MISSING",
|
|
178
|
+
hint: "\u8FD0\u884C `autodl login` \u4EA4\u4E92\u5199\u5165\uFF0C\u6216\u8BBE\u7F6E AUTODL_TOKEN \u73AF\u5883\u53D8\u91CF\u3002Token \u4F4D\u7F6E\uFF1AAutoDL \u63A7\u5236\u53F0 \u2192 \u8BBE\u7F6E \u2192 \u5F00\u53D1\u8005 Token\uFF08\u9700\u5148\u5B8C\u6210\u5B9E\u540D\u8BA4\u8BC1\uFF09\u3002"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function tryResolveToken(explicit) {
|
|
182
|
+
try {
|
|
183
|
+
return resolveToken(explicit);
|
|
184
|
+
} catch {
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function resolveBaseUrl(explicit) {
|
|
189
|
+
return explicit ?? process.env.AUTODL_BASE_URL ?? readConfig().baseUrl;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/core/client.ts
|
|
193
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
194
|
+
import { request as httpsRequest } from "https";
|
|
195
|
+
var DEFAULT_BASE_URL = "https://api.autodl.com";
|
|
196
|
+
function redactToken(token) {
|
|
197
|
+
if (token.length <= 8) return "****";
|
|
198
|
+
return `${token.slice(0, 4)}\u2026${token.slice(-4)}`;
|
|
199
|
+
}
|
|
200
|
+
function sleep(ms) {
|
|
201
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
202
|
+
}
|
|
203
|
+
function backoffDelay(attempt, baseMs) {
|
|
204
|
+
const base = Math.min(baseMs * 2 ** attempt, 15e3);
|
|
205
|
+
return Math.round(base * (0.5 + Math.random() * 0.5));
|
|
206
|
+
}
|
|
207
|
+
function isRetriableStatus(status) {
|
|
208
|
+
return status === 408 || status === 429 || status >= 500;
|
|
209
|
+
}
|
|
210
|
+
var getBodyFallback = /* @__PURE__ */ new Set();
|
|
211
|
+
var AutoDLClient = class {
|
|
212
|
+
token;
|
|
213
|
+
baseUrl;
|
|
214
|
+
timeoutMs;
|
|
215
|
+
maxRetries;
|
|
216
|
+
retryBaseDelayMs;
|
|
217
|
+
onDebug;
|
|
218
|
+
fetchImpl;
|
|
219
|
+
constructor(options2) {
|
|
220
|
+
if (!options2.token) {
|
|
221
|
+
throw new AuthError("\u672A\u914D\u7F6E AutoDL \u5F00\u53D1\u8005 Token", {
|
|
222
|
+
code: "AUTH_MISSING",
|
|
223
|
+
hint: "\u8FD0\u884C `autodl login`\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF AUTODL_TOKEN\u3002Token \u5728 AutoDL \u63A7\u5236\u53F0 \u2192 \u8BBE\u7F6E \u2192 \u5F00\u53D1\u8005 Token \u83B7\u53D6\u3002"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
this.token = options2.token;
|
|
227
|
+
this.baseUrl = (options2.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
228
|
+
this.timeoutMs = options2.timeoutMs ?? 3e4;
|
|
229
|
+
this.maxRetries = options2.maxRetries ?? 4;
|
|
230
|
+
this.retryBaseDelayMs = options2.retryBaseDelayMs ?? 1e3;
|
|
231
|
+
this.onDebug = options2.onDebug;
|
|
232
|
+
this.fetchImpl = options2.fetchImpl ?? globalThis.fetch;
|
|
233
|
+
}
|
|
234
|
+
get maskedToken() {
|
|
235
|
+
return redactToken(this.token);
|
|
236
|
+
}
|
|
237
|
+
async get(path, body, extra) {
|
|
238
|
+
return this.request({ method: "GET", path, body, ...extra });
|
|
239
|
+
}
|
|
240
|
+
async post(path, body, extra) {
|
|
241
|
+
return this.request({ method: "POST", path, body, ...extra });
|
|
242
|
+
}
|
|
243
|
+
async put(path, body, extra) {
|
|
244
|
+
return this.request({ method: "PUT", path, body, ...extra });
|
|
245
|
+
}
|
|
246
|
+
async delete(path, body, extra) {
|
|
247
|
+
return this.request({ method: "DELETE", path, body, ...extra });
|
|
248
|
+
}
|
|
249
|
+
async request(options2) {
|
|
250
|
+
const retries = options2.maxRetries ?? this.maxRetries;
|
|
251
|
+
let lastError;
|
|
252
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
253
|
+
try {
|
|
254
|
+
return await this.attempt(options2);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
lastError = err;
|
|
257
|
+
const retriable = err instanceof AutoDLError && err.details?.retriable === true;
|
|
258
|
+
if (!retriable || attempt === retries) break;
|
|
259
|
+
const delay2 = backoffDelay(attempt, this.retryBaseDelayMs);
|
|
260
|
+
this.debug(`\u91CD\u8BD5 ${options2.method} ${options2.path}\uFF08\u7B2C ${attempt + 1} \u6B21\uFF0C${delay2}ms \u540E\uFF09`);
|
|
261
|
+
await sleep(delay2);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw lastError;
|
|
265
|
+
}
|
|
266
|
+
debug(message) {
|
|
267
|
+
this.onDebug?.(message);
|
|
268
|
+
}
|
|
269
|
+
async attempt(options2) {
|
|
270
|
+
const { method, path, body } = options2;
|
|
271
|
+
const timeoutMs = options2.timeoutMs ?? this.timeoutMs;
|
|
272
|
+
const useGetBody = method === "GET" && getBodyFallback.has(path);
|
|
273
|
+
let envelope;
|
|
274
|
+
if (useGetBody) {
|
|
275
|
+
envelope = await this.rawGetWithBody(path, body ?? {}, timeoutMs);
|
|
276
|
+
} else {
|
|
277
|
+
envelope = await this.fetchJson(method, path, body, timeoutMs);
|
|
278
|
+
}
|
|
279
|
+
if (envelope.code === "Success") {
|
|
280
|
+
return envelope.data;
|
|
281
|
+
}
|
|
282
|
+
if (method === "GET" && !useGetBody && looksLikeMissingParam(envelope.msg)) {
|
|
283
|
+
getBodyFallback.add(path);
|
|
284
|
+
this.debug(`${path} \u9700\u8981 GET \u643A\u5E26 body\uFF0C\u5207\u6362\u4F20\u8F93\u65B9\u5F0F\u540E\u91CD\u8BD5`);
|
|
285
|
+
const retried = await this.rawGetWithBody(path, body ?? {}, timeoutMs);
|
|
286
|
+
if (retried.code === "Success") return retried.data;
|
|
287
|
+
throw mapEnvelopeError(retried.code, retried.msg, retried.request_id);
|
|
288
|
+
}
|
|
289
|
+
throw mapEnvelopeError(envelope.code, envelope.msg, envelope.request_id);
|
|
290
|
+
}
|
|
291
|
+
async fetchJson(method, path, body, timeoutMs) {
|
|
292
|
+
const url = new URL(this.baseUrl + path);
|
|
293
|
+
let payload;
|
|
294
|
+
if (method === "GET") {
|
|
295
|
+
for (const [key, value] of Object.entries(body ?? {})) {
|
|
296
|
+
if (value === void 0 || value === null) continue;
|
|
297
|
+
url.searchParams.set(
|
|
298
|
+
key,
|
|
299
|
+
typeof value === "object" ? JSON.stringify(value) : String(value)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
} else if (body !== void 0) {
|
|
303
|
+
payload = JSON.stringify(body);
|
|
304
|
+
}
|
|
305
|
+
this.debug(`${method} ${url.pathname}${url.search} token=${this.maskedToken}`);
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
308
|
+
let response;
|
|
309
|
+
try {
|
|
310
|
+
response = await this.fetchImpl(url, {
|
|
311
|
+
method,
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: this.token,
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
Accept: "application/json",
|
|
316
|
+
"User-Agent": "autodl-cli"
|
|
317
|
+
},
|
|
318
|
+
...payload !== void 0 ? { body: payload } : {},
|
|
319
|
+
signal: controller.signal
|
|
320
|
+
});
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (controller.signal.aborted) {
|
|
323
|
+
throw new TimeoutError(`\u8BF7\u6C42\u8D85\u65F6\uFF1A${method} ${path}`, {
|
|
324
|
+
details: { retriable: true },
|
|
325
|
+
cause: err
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
throw new AutoDLError(`\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF1A${method} ${path}`, {
|
|
329
|
+
code: "NETWORK",
|
|
330
|
+
details: { retriable: true },
|
|
331
|
+
cause: err
|
|
332
|
+
});
|
|
333
|
+
} finally {
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
}
|
|
336
|
+
return this.readEnvelope(response.status, await response.text(), method, path);
|
|
337
|
+
}
|
|
338
|
+
/** GET with a JSON body — legal HTTP/1.1, but fetch refuses, so drop to node:https. */
|
|
339
|
+
rawGetWithBody(path, body, timeoutMs) {
|
|
340
|
+
const url = new URL(this.baseUrl + path);
|
|
341
|
+
const payload = Buffer2.from(JSON.stringify(body));
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const req = httpsRequest(
|
|
344
|
+
{
|
|
345
|
+
hostname: url.hostname,
|
|
346
|
+
port: url.port || 443,
|
|
347
|
+
path: url.pathname + url.search,
|
|
348
|
+
method: "GET",
|
|
349
|
+
headers: {
|
|
350
|
+
Authorization: this.token,
|
|
351
|
+
"Content-Type": "application/json",
|
|
352
|
+
Accept: "application/json",
|
|
353
|
+
"User-Agent": "autodl-cli",
|
|
354
|
+
"Content-Length": payload.byteLength
|
|
355
|
+
},
|
|
356
|
+
timeout: timeoutMs
|
|
357
|
+
},
|
|
358
|
+
(res) => {
|
|
359
|
+
const chunks = [];
|
|
360
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
361
|
+
res.on("end", () => {
|
|
362
|
+
try {
|
|
363
|
+
resolve(
|
|
364
|
+
this.readEnvelope(
|
|
365
|
+
res.statusCode ?? 0,
|
|
366
|
+
Buffer2.concat(chunks).toString("utf8"),
|
|
367
|
+
"GET",
|
|
368
|
+
path
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
reject(err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
req.on("timeout", () => {
|
|
378
|
+
req.destroy();
|
|
379
|
+
reject(new TimeoutError(`\u8BF7\u6C42\u8D85\u65F6\uFF1AGET ${path}`, { details: { retriable: true } }));
|
|
380
|
+
});
|
|
381
|
+
req.on(
|
|
382
|
+
"error",
|
|
383
|
+
(err) => reject(
|
|
384
|
+
new AutoDLError(`\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF1AGET ${path}`, {
|
|
385
|
+
code: "NETWORK",
|
|
386
|
+
details: { retriable: true },
|
|
387
|
+
cause: err
|
|
388
|
+
})
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
req.end(payload);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
readEnvelope(status, text, method, path) {
|
|
395
|
+
if (status === 401 || status === 403) {
|
|
396
|
+
throw new AuthError(`Token \u65E0\u6548\u6216\u5DF2\u5931\u6548\uFF08HTTP ${status}\uFF09`, {
|
|
397
|
+
hint: "\u91CD\u65B0\u8FD0\u884C `autodl login` \u5199\u5165\u65B0\u7684\u5F00\u53D1\u8005 Token\u3002"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (isRetriableStatus(status)) {
|
|
401
|
+
throw new AutoDLError(`AutoDL \u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF08HTTP ${status}\uFF09\uFF1A${method} ${path}`, {
|
|
402
|
+
code: "API_ERROR",
|
|
403
|
+
details: { retriable: true, status }
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
let parsed;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(text);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
throw new AutoDLError(`\u65E0\u6CD5\u89E3\u6790 AutoDL \u54CD\u5E94\uFF08HTTP ${status}\uFF09`, {
|
|
411
|
+
code: "API_ERROR",
|
|
412
|
+
details: { status, snippet: text.slice(0, 200) },
|
|
413
|
+
cause: err
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (status >= 400 && parsed.code === void 0) {
|
|
417
|
+
throw new AutoDLError(`AutoDL \u8FD4\u56DE HTTP ${status}\uFF1A${method} ${path}`, {
|
|
418
|
+
code: "API_ERROR",
|
|
419
|
+
details: { status }
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return parsed;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
function looksLikeMissingParam(msg) {
|
|
426
|
+
const m = (msg ?? "").toLowerCase();
|
|
427
|
+
return m.includes("param") || m.includes("bind") || m.includes("required") || msg.includes("\u53C2\u6570") || msg.includes("\u5FC5\u586B");
|
|
428
|
+
}
|
|
429
|
+
function mapEnvelopeError(code, msg, requestId) {
|
|
430
|
+
const text = `${code} ${msg}`;
|
|
431
|
+
const lower = text.toLowerCase();
|
|
432
|
+
const message = msg?.trim() ? msg.trim() : `AutoDL \u8FD4\u56DE\u9519\u8BEF\uFF1A${code}`;
|
|
433
|
+
const opts = { requestId, details: { apiCode: code } };
|
|
434
|
+
if (lower.includes("token") || lower.includes("auth") || lower.includes("unauthor")) {
|
|
435
|
+
return new AuthError(message, {
|
|
436
|
+
...opts,
|
|
437
|
+
hint: "\u786E\u8BA4 Token \u672A\u8FC7\u671F\u4E14\u8D26\u53F7\u5DF2\u5B8C\u6210\u5B9E\u540D\u8BA4\u8BC1\u3002"
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (text.includes("\u5B9E\u540D")) {
|
|
441
|
+
return new AuthError(message, {
|
|
442
|
+
...opts,
|
|
443
|
+
hint: "\u5B98\u65B9\u5F00\u653E API \u8981\u6C42\u5B8C\u6210\u4E2A\u4EBA\u6216\u4F01\u4E1A\u5B9E\u540D\u8BA4\u8BC1\u540E\u624D\u80FD\u8C03\u7528\u3002"
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (text.includes("\u4F59\u989D") || lower.includes("balance") || lower.includes("insufficient fund")) {
|
|
447
|
+
return new BudgetError(message, { ...opts, hint: "\u8BF7\u5148\u5145\u503C\uFF0C\u6216\u964D\u4F4E\u6240\u9700 GPU \u89C4\u683C\u3002" });
|
|
448
|
+
}
|
|
449
|
+
if (text.includes("\u5E93\u5B58") || text.includes("\u65E0\u53EF\u7528") || text.includes("\u8D44\u6E90\u4E0D\u8DB3") || text.includes("\u552E\u7F44") || lower.includes("no stock") || lower.includes("sold out") || lower.includes("no available")) {
|
|
450
|
+
return new NoStockError(message, {
|
|
451
|
+
...opts,
|
|
452
|
+
hint: "\u5B98\u65B9 API \u6CA1\u6709\u5E93\u5B58\u67E5\u8BE2\u63A5\u53E3\uFF0C\u53EA\u80FD\u6362 GPU \u89C4\u683C\u6216\u5730\u533A\u91CD\u8BD5\u3002"
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (text.includes("\u4E0D\u5B58\u5728") || lower.includes("not found") || lower.includes("no such")) {
|
|
456
|
+
return new NotFoundError(message, opts);
|
|
457
|
+
}
|
|
458
|
+
return new AutoDLError(message, { ...opts, code: "API_ERROR" });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/core/duration.ts
|
|
462
|
+
var PATTERN = /^(\d+(?:\.\d+)?)(s|m|h|d)?$/i;
|
|
463
|
+
var UNIT_SECONDS = {
|
|
464
|
+
s: 1,
|
|
465
|
+
m: 60,
|
|
466
|
+
h: 3600,
|
|
467
|
+
d: 86400
|
|
468
|
+
};
|
|
469
|
+
function parseDuration(input, { bareUnit = "m" } = {}) {
|
|
470
|
+
const match = PATTERN.exec(input.trim());
|
|
471
|
+
if (!match) {
|
|
472
|
+
throw new UsageError(`\u65E0\u6CD5\u89E3\u6790\u65F6\u957F "${input}"`, {
|
|
473
|
+
hint: "\u652F\u6301\u7684\u683C\u5F0F\uFF1A30s\u300145m\u30012h\u30011d\uFF0C\u6216\u7EAF\u6570\u5B57\uFF08\u9ED8\u8BA4\u6309\u5206\u949F\uFF09"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const value = Number(match[1]);
|
|
477
|
+
const unit = (match[2] ?? bareUnit).toLowerCase();
|
|
478
|
+
const factor = UNIT_SECONDS[unit];
|
|
479
|
+
if (factor === void 0) {
|
|
480
|
+
throw new UsageError(`\u4E0D\u652F\u6301\u7684\u65F6\u957F\u5355\u4F4D "${unit}"`, { hint: "\u652F\u6301 s / m / h / d" });
|
|
481
|
+
}
|
|
482
|
+
const seconds = Math.round(value * factor);
|
|
483
|
+
if (seconds <= 0) {
|
|
484
|
+
throw new UsageError(`\u65F6\u957F\u5FC5\u987B\u5927\u4E8E 0\uFF0C\u6536\u5230 "${input}"`);
|
|
485
|
+
}
|
|
486
|
+
return seconds;
|
|
487
|
+
}
|
|
488
|
+
function formatDuration(seconds) {
|
|
489
|
+
if (seconds < 60) return `${seconds}s`;
|
|
490
|
+
const d = Math.floor(seconds / 86400);
|
|
491
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
492
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
493
|
+
const parts = [];
|
|
494
|
+
if (d) parts.push(`${d}d`);
|
|
495
|
+
if (h) parts.push(`${h}h`);
|
|
496
|
+
if (m) parts.push(`${m}m`);
|
|
497
|
+
return parts.join("") || `${seconds}s`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/core/money.ts
|
|
501
|
+
var MILLI_PER_YUAN = 1e3;
|
|
502
|
+
function milliToYuan(milli) {
|
|
503
|
+
if (milli === null || milli === void 0 || Number.isNaN(milli)) return 0;
|
|
504
|
+
return Math.round(milli / MILLI_PER_YUAN * 100) / 100;
|
|
505
|
+
}
|
|
506
|
+
function yuanToMilli(yuan) {
|
|
507
|
+
return Math.round(yuan * MILLI_PER_YUAN);
|
|
508
|
+
}
|
|
509
|
+
function formatYuan(yuan) {
|
|
510
|
+
return `\xA5${yuan.toFixed(2)}`;
|
|
511
|
+
}
|
|
512
|
+
function formatRate(yuanPerHour, unit = "\u65F6") {
|
|
513
|
+
return `${formatYuan(yuanPerHour)}/${unit}`;
|
|
514
|
+
}
|
|
515
|
+
function estimateCost(yuanPerHour, seconds) {
|
|
516
|
+
const raw = yuanPerHour * seconds / 3600;
|
|
517
|
+
if (raw <= 0) return 0;
|
|
518
|
+
return Math.max(0.01, Math.round(raw * 100) / 100);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/core/schemas.ts
|
|
522
|
+
import { z } from "zod";
|
|
523
|
+
var nullTime = z.object({ Time: z.string(), Valid: z.boolean() }).transform((v) => v.Valid ? v.Time : null);
|
|
524
|
+
var maybeTime = z.union([nullTime, z.string(), z.null()]).transform((v) => v ?? null);
|
|
525
|
+
var instanceStatusSchema = z.enum([
|
|
526
|
+
"creating",
|
|
527
|
+
"starting",
|
|
528
|
+
"running",
|
|
529
|
+
"shutting_down",
|
|
530
|
+
"shutdown",
|
|
531
|
+
"releasing",
|
|
532
|
+
"released",
|
|
533
|
+
"failed"
|
|
534
|
+
]);
|
|
535
|
+
var looseStatusSchema = z.string();
|
|
536
|
+
var rawInstanceSchema = z.object({
|
|
537
|
+
uuid: z.string(),
|
|
538
|
+
name: z.string().nullish(),
|
|
539
|
+
status: z.string(),
|
|
540
|
+
sub_status: z.string().nullish(),
|
|
541
|
+
machine_id: z.string().nullish(),
|
|
542
|
+
machine_alias: z.string().nullish(),
|
|
543
|
+
region_sign: z.string().nullish(),
|
|
544
|
+
region_name: z.string().nullish(),
|
|
545
|
+
charge_type: z.string().nullish(),
|
|
546
|
+
start_mode: z.string().nullish(),
|
|
547
|
+
req_gpu_amount: z.number().nullish(),
|
|
548
|
+
gpu_spec_uuid: z.string().nullish(),
|
|
549
|
+
created_at: maybeTime.nullish(),
|
|
550
|
+
status_at: maybeTime.nullish(),
|
|
551
|
+
started_at: maybeTime.nullish(),
|
|
552
|
+
stopped_at: maybeTime.nullish(),
|
|
553
|
+
expired_at: maybeTime.nullish(),
|
|
554
|
+
timed_shutdown_at: maybeTime.nullish()
|
|
555
|
+
}).passthrough();
|
|
556
|
+
function normalizeInstance(raw) {
|
|
557
|
+
const v = rawInstanceSchema.parse(raw);
|
|
558
|
+
return {
|
|
559
|
+
uuid: v.uuid,
|
|
560
|
+
name: v.name ?? null,
|
|
561
|
+
status: v.status,
|
|
562
|
+
subStatus: v.sub_status || null,
|
|
563
|
+
machineId: v.machine_id || null,
|
|
564
|
+
regionSign: v.region_sign || null,
|
|
565
|
+
regionName: v.region_name || null,
|
|
566
|
+
chargeType: v.charge_type || null,
|
|
567
|
+
startMode: v.start_mode || null,
|
|
568
|
+
gpuSpec: v.gpu_spec_uuid || null,
|
|
569
|
+
gpuNum: v.req_gpu_amount ?? 0,
|
|
570
|
+
createdAt: v.created_at ?? null,
|
|
571
|
+
startedAt: v.started_at ?? null,
|
|
572
|
+
stoppedAt: v.stopped_at ?? null,
|
|
573
|
+
expiredAt: v.expired_at ?? null,
|
|
574
|
+
timedShutdownAt: v.timed_shutdown_at ?? null
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
var usageInfoSchema = z.object({
|
|
578
|
+
container_id: z.string().nullish(),
|
|
579
|
+
cpu_usage_percent: z.number().nullish(),
|
|
580
|
+
mem_usage_percent: z.number().nullish(),
|
|
581
|
+
mem_usage: z.number().nullish(),
|
|
582
|
+
mem_limit: z.number().nullish(),
|
|
583
|
+
root_fs_used_size: z.number().nullish(),
|
|
584
|
+
root_fs_total_size: z.number().nullish(),
|
|
585
|
+
data_disk_used_size: z.number().nullish(),
|
|
586
|
+
data_disk_total_size: z.number().nullish()
|
|
587
|
+
}).passthrough();
|
|
588
|
+
var rawSnapshotSchema = z.object({
|
|
589
|
+
region_sign: z.string().nullish(),
|
|
590
|
+
payg_price: z.number().nullish(),
|
|
591
|
+
origin_pay_price: z.number().nullish(),
|
|
592
|
+
snapshot_gpu_alias_name: z.string().nullish(),
|
|
593
|
+
chip_corp: z.string().nullish(),
|
|
594
|
+
cpu_arch: z.string().nullish(),
|
|
595
|
+
ssh_command: z.string().nullish(),
|
|
596
|
+
proxy_host: z.string().nullish(),
|
|
597
|
+
root_password: z.string().nullish(),
|
|
598
|
+
ssh_port: z.number().nullish(),
|
|
599
|
+
jupyter_token: z.string().nullish(),
|
|
600
|
+
jupyter_domain: z.string().nullish(),
|
|
601
|
+
expand_system_disk_size: z.number().nullish(),
|
|
602
|
+
system_init_disk_size: z.number().nullish(),
|
|
603
|
+
usage_info: usageInfoSchema.nullish()
|
|
604
|
+
}).passthrough();
|
|
605
|
+
var SERVICE_DOMAIN = /^service_(\d+)_domain$/;
|
|
606
|
+
function normalizeSnapshot(raw) {
|
|
607
|
+
const v = rawSnapshotSchema.parse(raw);
|
|
608
|
+
const bag = raw;
|
|
609
|
+
const services = [];
|
|
610
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
611
|
+
const match = SERVICE_DOMAIN.exec(key);
|
|
612
|
+
if (!match || typeof value !== "string" || !value) continue;
|
|
613
|
+
const port = Number(match[1]);
|
|
614
|
+
const protocol = bag[`service_${port}_port_protocol`];
|
|
615
|
+
services.push({
|
|
616
|
+
port,
|
|
617
|
+
domain: value,
|
|
618
|
+
protocol: typeof protocol === "string" && protocol ? protocol : null
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
services.sort((a, b) => a.port - b.port);
|
|
622
|
+
const usage = v.usage_info ?? {};
|
|
623
|
+
return {
|
|
624
|
+
regionSign: v.region_sign || null,
|
|
625
|
+
gpuAlias: v.snapshot_gpu_alias_name || null,
|
|
626
|
+
chipCorp: v.chip_corp || null,
|
|
627
|
+
cpuArch: v.cpu_arch || null,
|
|
628
|
+
priceYuanPerHour: milliToYuan(v.payg_price),
|
|
629
|
+
originalPriceYuanPerHour: milliToYuan(v.origin_pay_price),
|
|
630
|
+
ssh: {
|
|
631
|
+
command: v.ssh_command || null,
|
|
632
|
+
host: v.proxy_host || null,
|
|
633
|
+
port: v.ssh_port ?? null,
|
|
634
|
+
user: "root",
|
|
635
|
+
password: v.root_password || null
|
|
636
|
+
},
|
|
637
|
+
jupyter: { token: v.jupyter_token || null, domain: v.jupyter_domain || null },
|
|
638
|
+
services,
|
|
639
|
+
disk: {
|
|
640
|
+
systemInitBytes: v.system_init_disk_size ?? 0,
|
|
641
|
+
systemExpandBytes: v.expand_system_disk_size ?? 0
|
|
642
|
+
},
|
|
643
|
+
usage: {
|
|
644
|
+
cpuPercent: usage.cpu_usage_percent ?? null,
|
|
645
|
+
memPercent: usage.mem_usage_percent ?? null,
|
|
646
|
+
memUsedBytes: usage.mem_usage ?? null,
|
|
647
|
+
memLimitBytes: usage.mem_limit ?? null,
|
|
648
|
+
rootFsUsedBytes: usage.root_fs_used_size ?? null,
|
|
649
|
+
rootFsTotalBytes: usage.root_fs_total_size ?? null,
|
|
650
|
+
dataDiskUsedBytes: usage.data_disk_used_size ?? null,
|
|
651
|
+
dataDiskTotalBytes: usage.data_disk_total_size ?? null
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
var rawBalanceSchema = z.object({
|
|
656
|
+
assets: z.number().nullish(),
|
|
657
|
+
accumulate: z.number().nullish(),
|
|
658
|
+
voucher_balance: z.number().nullish()
|
|
659
|
+
}).passthrough();
|
|
660
|
+
function normalizeBalance(raw) {
|
|
661
|
+
const v = rawBalanceSchema.parse(raw);
|
|
662
|
+
return {
|
|
663
|
+
balanceYuan: milliToYuan(v.assets),
|
|
664
|
+
accumulatedYuan: milliToYuan(v.accumulate),
|
|
665
|
+
voucherYuan: milliToYuan(v.voucher_balance)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
var rawImageSchema = z.object({
|
|
669
|
+
image_uuid: z.string(),
|
|
670
|
+
name: z.string().nullish(),
|
|
671
|
+
image_name: z.string().nullish(),
|
|
672
|
+
status: z.string().nullish(),
|
|
673
|
+
image_size: z.number().nullish(),
|
|
674
|
+
create_at: z.string().nullish(),
|
|
675
|
+
created_at: z.string().nullish()
|
|
676
|
+
}).passthrough();
|
|
677
|
+
function normalizeImage(raw) {
|
|
678
|
+
const v = rawImageSchema.parse(raw);
|
|
679
|
+
return {
|
|
680
|
+
imageUuid: v.image_uuid,
|
|
681
|
+
name: v.name ?? v.image_name ?? "",
|
|
682
|
+
status: v.status ?? null,
|
|
683
|
+
sizeBytes: v.image_size ?? 0,
|
|
684
|
+
createdAt: v.create_at ?? v.created_at ?? null
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
var paginationSchema = z.object({
|
|
688
|
+
page_index: z.number().nullish(),
|
|
689
|
+
page_size: z.number().nullish(),
|
|
690
|
+
max_page: z.number().nullish(),
|
|
691
|
+
result_total: z.number().nullish()
|
|
692
|
+
}).passthrough();
|
|
693
|
+
function normalizePagination(raw) {
|
|
694
|
+
const v = paginationSchema.parse(raw ?? {});
|
|
695
|
+
return {
|
|
696
|
+
pageIndex: v.page_index ?? 1,
|
|
697
|
+
pageSize: v.page_size ?? 0,
|
|
698
|
+
maxPage: v.max_page ?? 1,
|
|
699
|
+
total: v.result_total ?? 0
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function redactSnapshot(snapshot) {
|
|
703
|
+
return {
|
|
704
|
+
...snapshot,
|
|
705
|
+
ssh: { ...snapshot.ssh, password: snapshot.ssh.password ? "***" : null },
|
|
706
|
+
jupyter: { ...snapshot.jupyter, token: snapshot.jupyter.token ? "***" : null }
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/core/endpoints/instance.ts
|
|
711
|
+
var MAX_GPU_NUM = 4;
|
|
712
|
+
var MAX_DISK_GB = 500;
|
|
713
|
+
function validateCreateInput(input) {
|
|
714
|
+
if (!Number.isInteger(input.gpuNum) || input.gpuNum < 1 || input.gpuNum > MAX_GPU_NUM) {
|
|
715
|
+
throw new UsageError(`GPU \u6570\u91CF\u5FC5\u987B\u662F 1-${MAX_GPU_NUM} \u4E4B\u95F4\u7684\u6574\u6570\uFF0C\u6536\u5230 ${input.gpuNum}`);
|
|
716
|
+
}
|
|
717
|
+
const disk = input.expandSystemDiskGb ?? 0;
|
|
718
|
+
if (!Number.isInteger(disk) || disk < 0 || disk > MAX_DISK_GB) {
|
|
719
|
+
throw new UsageError(`\u7CFB\u7EDF\u76D8\u6269\u5BB9\u5FC5\u987B\u662F 0-${MAX_DISK_GB} GB \u4E4B\u95F4\u7684\u6574\u6570\uFF0C\u6536\u5230 ${disk}`);
|
|
720
|
+
}
|
|
721
|
+
if (!input.imageUuid) {
|
|
722
|
+
throw new UsageError("\u5FC5\u987B\u6307\u5B9A\u955C\u50CF\uFF08--image\uFF09");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function createInstance(client, input) {
|
|
726
|
+
validateCreateInput(input);
|
|
727
|
+
const payload = {
|
|
728
|
+
req_gpu_amount: input.gpuNum,
|
|
729
|
+
gpu_spec_uuid: input.gpuSpec,
|
|
730
|
+
image_uuid: input.imageUuid,
|
|
731
|
+
cuda_v_from: input.cudaFrom,
|
|
732
|
+
expand_system_disk_by_gb: input.expandSystemDiskGb ?? 0
|
|
733
|
+
};
|
|
734
|
+
if (input.regions?.length) payload.data_center_list = input.regions;
|
|
735
|
+
if (input.name) payload.instance_name = input.name;
|
|
736
|
+
if (input.startCommand) payload.start_command = input.startCommand;
|
|
737
|
+
const uuid = await client.post("/api/v1/dev/instance/pro/create", payload, {
|
|
738
|
+
maxRetries: 0
|
|
739
|
+
});
|
|
740
|
+
return uuid;
|
|
741
|
+
}
|
|
742
|
+
async function listInstancesPage(client, options2 = {}) {
|
|
743
|
+
const data = await client.post(
|
|
744
|
+
"/api/v1/dev/instance/pro/list",
|
|
745
|
+
{ page_index: options2.pageIndex ?? 1, page_size: options2.pageSize ?? 50 }
|
|
746
|
+
);
|
|
747
|
+
const list = Array.isArray(data?.list) ? data.list : [];
|
|
748
|
+
return {
|
|
749
|
+
instances: list.map(normalizeInstance),
|
|
750
|
+
pagination: normalizePagination(data)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async function listAllInstances(client) {
|
|
754
|
+
const pageSize = 100;
|
|
755
|
+
const first = await listInstancesPage(client, { pageIndex: 1, pageSize });
|
|
756
|
+
const all = [...first.instances];
|
|
757
|
+
for (let page = 2; page <= first.pagination.maxPage; page++) {
|
|
758
|
+
const next = await listInstancesPage(client, { pageIndex: page, pageSize });
|
|
759
|
+
all.push(...next.instances);
|
|
760
|
+
if (next.instances.length === 0) break;
|
|
761
|
+
}
|
|
762
|
+
return all;
|
|
763
|
+
}
|
|
764
|
+
async function getInstanceStatus(client, uuid) {
|
|
765
|
+
return client.get("/api/v1/dev/instance/pro/status", { instance_uuid: uuid });
|
|
766
|
+
}
|
|
767
|
+
async function getInstanceSnapshot(client, uuid) {
|
|
768
|
+
const data = await client.get("/api/v1/dev/instance/pro/snapshot", {
|
|
769
|
+
instance_uuid: uuid
|
|
770
|
+
});
|
|
771
|
+
return normalizeSnapshot(data);
|
|
772
|
+
}
|
|
773
|
+
async function findInstance(client, uuid) {
|
|
774
|
+
const all = await listAllInstances(client);
|
|
775
|
+
const found = all.find((instance) => instance.uuid === uuid);
|
|
776
|
+
if (!found) {
|
|
777
|
+
throw new NotFoundError(`\u672A\u627E\u5230\u5B9E\u4F8B ${uuid}`, {
|
|
778
|
+
hint: "\u8FD0\u884C `autodl ls` \u67E5\u770B\u5F53\u524D\u8D26\u53F7\u4E0B\u7684\u5B9E\u4F8B\u3002"
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return found;
|
|
782
|
+
}
|
|
783
|
+
async function powerOnInstance(client, uuid, options2 = {}) {
|
|
784
|
+
const payload = { instance_uuid: uuid, payload: "gpu" };
|
|
785
|
+
if (options2.startCommand) payload.start_command = options2.startCommand;
|
|
786
|
+
await client.post("/api/v1/dev/instance/pro/power_on", payload, { maxRetries: 0 });
|
|
787
|
+
}
|
|
788
|
+
async function powerOffInstance(client, uuid) {
|
|
789
|
+
await client.post("/api/v1/dev/instance/pro/power_off", { instance_uuid: uuid });
|
|
790
|
+
}
|
|
791
|
+
async function releaseInstance(client, uuid) {
|
|
792
|
+
await client.post("/api/v1/dev/instance/pro/release", { instance_uuid: uuid }, { maxRetries: 0 });
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/output/format.ts
|
|
796
|
+
import Table from "cli-table3";
|
|
797
|
+
import pc from "picocolors";
|
|
798
|
+
var options = { json: false, color: true, verbose: false, quiet: false };
|
|
799
|
+
function configureOutput(next) {
|
|
800
|
+
options = { ...options, ...next };
|
|
801
|
+
if (!options.color) {
|
|
802
|
+
process.env.NO_COLOR = "1";
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
var isJson = () => options.json;
|
|
806
|
+
var isVerbose = () => options.verbose;
|
|
807
|
+
function note(message) {
|
|
808
|
+
if (options.json || options.quiet) return;
|
|
809
|
+
process.stderr.write(`${message}
|
|
810
|
+
`);
|
|
811
|
+
}
|
|
812
|
+
function success(message) {
|
|
813
|
+
if (options.json || options.quiet) return;
|
|
814
|
+
process.stderr.write(`${pc.green("\u2714")} ${message}
|
|
815
|
+
`);
|
|
816
|
+
}
|
|
817
|
+
function warn(message) {
|
|
818
|
+
if (options.json || options.quiet) return;
|
|
819
|
+
process.stderr.write(`${pc.yellow("!")} ${message}
|
|
820
|
+
`);
|
|
821
|
+
}
|
|
822
|
+
function debug(message) {
|
|
823
|
+
if (!options.verbose || options.quiet) return;
|
|
824
|
+
process.stderr.write(`${pc.dim(`[debug] ${message}`)}
|
|
825
|
+
`);
|
|
826
|
+
}
|
|
827
|
+
function emit(data, renderHuman) {
|
|
828
|
+
if (options.json) {
|
|
829
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data }, null, 2)}
|
|
830
|
+
`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
renderHuman();
|
|
834
|
+
}
|
|
835
|
+
function emitError(error) {
|
|
836
|
+
if (options.json) {
|
|
837
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: error.toJSON() }, null, 2)}
|
|
838
|
+
`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
process.stderr.write(`${pc.red("\u2716")} ${error.message}
|
|
842
|
+
`);
|
|
843
|
+
if (error.hint) process.stderr.write(` ${pc.dim(error.hint)}
|
|
844
|
+
`);
|
|
845
|
+
if (error.requestId) process.stderr.write(` ${pc.dim(`request_id: ${error.requestId}`)}
|
|
846
|
+
`);
|
|
847
|
+
if (options.verbose && error.cause instanceof Error && error.cause.stack) {
|
|
848
|
+
process.stderr.write(`${pc.dim(error.cause.stack)}
|
|
849
|
+
`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function table(head, rows) {
|
|
853
|
+
const t2 = new Table({
|
|
854
|
+
head: head.map((h) => pc.bold(h)),
|
|
855
|
+
style: { head: [], border: [] },
|
|
856
|
+
chars: {
|
|
857
|
+
top: "\u2500",
|
|
858
|
+
"top-mid": "\u252C",
|
|
859
|
+
"top-left": "\u250C",
|
|
860
|
+
"top-right": "\u2510",
|
|
861
|
+
bottom: "\u2500",
|
|
862
|
+
"bottom-mid": "\u2534",
|
|
863
|
+
"bottom-left": "\u2514",
|
|
864
|
+
"bottom-right": "\u2518",
|
|
865
|
+
left: "\u2502",
|
|
866
|
+
"left-mid": "\u251C",
|
|
867
|
+
mid: "\u2500",
|
|
868
|
+
"mid-mid": "\u253C",
|
|
869
|
+
right: "\u2502",
|
|
870
|
+
"right-mid": "\u2524",
|
|
871
|
+
middle: "\u2502"
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
for (const row of rows) t2.push(row.map((cell) => String(cell)));
|
|
875
|
+
return t2.toString();
|
|
876
|
+
}
|
|
877
|
+
function printTable(head, rows) {
|
|
878
|
+
process.stdout.write(`${table(head, rows)}
|
|
879
|
+
`);
|
|
880
|
+
}
|
|
881
|
+
function printKeyValues(pairs) {
|
|
882
|
+
const width = Math.max(...pairs.map(([key]) => stringWidth(key)));
|
|
883
|
+
for (const [key, value] of pairs) {
|
|
884
|
+
const pad = " ".repeat(Math.max(0, width - stringWidth(key)));
|
|
885
|
+
process.stdout.write(`${pc.dim(key)}${pad} ${value}
|
|
886
|
+
`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function stringWidth(input) {
|
|
890
|
+
let width = 0;
|
|
891
|
+
for (const char of input) {
|
|
892
|
+
const code = char.codePointAt(0) ?? 0;
|
|
893
|
+
width += code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 ? 2 : 1;
|
|
894
|
+
}
|
|
895
|
+
return width;
|
|
896
|
+
}
|
|
897
|
+
function colorStatus(status) {
|
|
898
|
+
switch (status) {
|
|
899
|
+
case "running":
|
|
900
|
+
return pc.green(status);
|
|
901
|
+
case "starting":
|
|
902
|
+
case "creating":
|
|
903
|
+
return pc.cyan(status);
|
|
904
|
+
case "shutting_down":
|
|
905
|
+
case "shutdown":
|
|
906
|
+
return pc.dim(status);
|
|
907
|
+
case "failed":
|
|
908
|
+
case "released":
|
|
909
|
+
return pc.red(status);
|
|
910
|
+
default:
|
|
911
|
+
return status;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function formatBytes(bytes) {
|
|
915
|
+
if (!bytes) return "-";
|
|
916
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
917
|
+
let value = bytes;
|
|
918
|
+
let index = 0;
|
|
919
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
920
|
+
value /= 1024;
|
|
921
|
+
index++;
|
|
922
|
+
}
|
|
923
|
+
return `${value.toFixed(value >= 100 || index === 0 ? 0 : 1)}${units[index]}`;
|
|
924
|
+
}
|
|
925
|
+
function formatTime(iso) {
|
|
926
|
+
if (!iso) return "-";
|
|
927
|
+
const date = new Date(iso);
|
|
928
|
+
if (Number.isNaN(date.getTime())) return "-";
|
|
929
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
930
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/core/waiters.ts
|
|
934
|
+
var TERMINAL_FAILURES = /* @__PURE__ */ new Set(["failed", "released", "releasing"]);
|
|
935
|
+
function sleep2(ms, signal) {
|
|
936
|
+
return new Promise((resolve, reject) => {
|
|
937
|
+
const timer = setTimeout(resolve, ms);
|
|
938
|
+
signal?.addEventListener(
|
|
939
|
+
"abort",
|
|
940
|
+
() => {
|
|
941
|
+
clearTimeout(timer);
|
|
942
|
+
reject(new AutoDLError("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
|
|
943
|
+
},
|
|
944
|
+
{ once: true }
|
|
945
|
+
);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
async function waitForStatus(client, uuid, targets, options2 = {}) {
|
|
949
|
+
const timeoutMs = options2.timeoutMs ?? 10 * 60 * 1e3;
|
|
950
|
+
const intervalMs = options2.intervalMs ?? 5e3;
|
|
951
|
+
const wanted = new Set(targets);
|
|
952
|
+
const startedAt = Date.now();
|
|
953
|
+
for (; ; ) {
|
|
954
|
+
const status = await getInstanceStatus(client, uuid);
|
|
955
|
+
const elapsed = Date.now() - startedAt;
|
|
956
|
+
options2.onPoll?.(status, elapsed);
|
|
957
|
+
if (wanted.has(status)) return status;
|
|
958
|
+
if (TERMINAL_FAILURES.has(status) && !wanted.has(status)) {
|
|
959
|
+
throw new AutoDLError(
|
|
960
|
+
`\u5B9E\u4F8B ${uuid} \u8FDB\u5165\u4E86 "${status}" \u72B6\u6001\uFF0C\u65E0\u6CD5\u7EE7\u7EED\u7B49\u5F85 ${targets.join("/")}`,
|
|
961
|
+
{
|
|
962
|
+
details: { status }
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (elapsed >= timeoutMs) {
|
|
967
|
+
throw new TimeoutError(
|
|
968
|
+
`\u7B49\u5F85\u5B9E\u4F8B ${uuid} \u53D8\u4E3A ${targets.join("/")} \u8D85\u65F6\uFF08\u5F53\u524D\u72B6\u6001 "${status}"\uFF09`,
|
|
969
|
+
{ hint: "\u53EF\u4EE5\u7A0D\u540E\u8FD0\u884C `autodl info <id>` \u67E5\u770B\u5B9E\u4F8B\u662F\u5426\u5DF2\u5C31\u7EEA\u3002", details: { status } }
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
await sleep2(intervalMs, options2.signal);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
var waitForRunning = (client, uuid, options2) => waitForStatus(client, uuid, ["running"], options2);
|
|
976
|
+
var waitForShutdown = (client, uuid, options2) => waitForStatus(client, uuid, ["shutdown"], options2);
|
|
977
|
+
|
|
978
|
+
// src/ssh/credentials.ts
|
|
979
|
+
import { Client } from "ssh2";
|
|
980
|
+
|
|
981
|
+
// src/output/i18n.ts
|
|
982
|
+
var MESSAGES = {
|
|
983
|
+
zh: {
|
|
984
|
+
"account.balance": "\u4F59\u989D",
|
|
985
|
+
"account.accumulated": "\u7D2F\u8BA1\u6D88\u8D39",
|
|
986
|
+
"account.voucher": "\u4EE3\u91D1\u5238",
|
|
987
|
+
"login.prompt": "\u8BF7\u7C98\u8D34 AutoDL \u5F00\u53D1\u8005 Token",
|
|
988
|
+
"login.hint": "\u63A7\u5236\u53F0 \u2192 \u8BBE\u7F6E \u2192 \u5F00\u53D1\u8005 Token\uFF08\u9700\u5DF2\u5B8C\u6210\u5B9E\u540D\u8BA4\u8BC1\uFF09",
|
|
989
|
+
"login.verifying": "\u6B63\u5728\u9A8C\u8BC1 Token\u2026",
|
|
990
|
+
"login.success": "Token \u5DF2\u9A8C\u8BC1\u5E76\u4FDD\u5B58\u5230",
|
|
991
|
+
"login.cancelled": "\u5DF2\u53D6\u6D88",
|
|
992
|
+
"logout.done": "\u5DF2\u6E05\u9664\u672C\u5730 Token",
|
|
993
|
+
"instance.none": "\u5F53\u524D\u8D26\u53F7\u4E0B\u6CA1\u6709\u5B9E\u4F8B",
|
|
994
|
+
"instance.creating": "\u6B63\u5728\u521B\u5EFA\u5B9E\u4F8B\u2026",
|
|
995
|
+
"instance.created": "\u5B9E\u4F8B\u5DF2\u521B\u5EFA",
|
|
996
|
+
"instance.waiting": "\u7B49\u5F85\u5B9E\u4F8B\u5C31\u7EEA\u2026",
|
|
997
|
+
"instance.ready": "\u5B9E\u4F8B\u5DF2\u5C31\u7EEA",
|
|
998
|
+
"instance.poweringOn": "\u6B63\u5728\u5F00\u673A\u2026",
|
|
999
|
+
"instance.poweringOff": "\u6B63\u5728\u5173\u673A\u2026",
|
|
1000
|
+
"instance.released": "\u5B9E\u4F8B\u5DF2\u91CA\u653E",
|
|
1001
|
+
"instance.confirmRelease": "\u91CA\u653E\u540E\u5B9E\u4F8B\u6570\u636E\u5C06\u88AB\u6C38\u4E45\u6E05\u7A7A\uFF0C\u786E\u8BA4\u91CA\u653E",
|
|
1002
|
+
"guard.armed": "\u5DF2\u8BBE\u7F6E\u5230\u671F\u81EA\u52A8\u5173\u673A",
|
|
1003
|
+
"guard.armFailed": "\u5B9E\u4F8B\u5185\u5B9A\u65F6\u5173\u673A\u672A\u80FD\u8BBE\u7F6E\uFF0C\u5DF2\u6539\u4E3A\u4EC5\u4F9D\u8D56\u672C\u5730\u53F0\u8D26",
|
|
1004
|
+
"guard.sweptOne": "\u5DF2\u81EA\u52A8\u5173\u95ED\u8D85\u65F6\u5B9E\u4F8B",
|
|
1005
|
+
"guard.blocked": "\u4F59\u989D\u4F4E\u4E8E\u9608\u503C\uFF0C\u5DF2\u963B\u6B62\u521B\u5EFA\u5B9E\u4F8B",
|
|
1006
|
+
"ssh.connecting": "\u6B63\u5728\u8FDE\u63A5\u2026",
|
|
1007
|
+
"ssh.refreshing": "SSH \u51ED\u8BC1\u53EF\u80FD\u5DF2\u53D8\u5316\uFF0C\u6B63\u5728\u91CD\u65B0\u83B7\u53D6\u2026",
|
|
1008
|
+
"run.syncing": "\u6B63\u5728\u540C\u6B65\u6587\u4EF6\u2026",
|
|
1009
|
+
"run.executing": "\u6B63\u5728\u8FDC\u7A0B\u6267\u884C\u2026",
|
|
1010
|
+
"run.pulling": "\u6B63\u5728\u56DE\u4F20\u4EA7\u7269\u2026",
|
|
1011
|
+
"run.finished": "\u6267\u884C\u5B8C\u6210",
|
|
1012
|
+
"run.cleanup": "\u6B63\u5728\u6536\u5C3E\u2026",
|
|
1013
|
+
"common.cancelled": "\u5DF2\u53D6\u6D88",
|
|
1014
|
+
"common.yes": "\u662F",
|
|
1015
|
+
"common.no": "\u5426"
|
|
1016
|
+
},
|
|
1017
|
+
en: {
|
|
1018
|
+
"account.balance": "Balance",
|
|
1019
|
+
"account.accumulated": "Total spend",
|
|
1020
|
+
"account.voucher": "Vouchers",
|
|
1021
|
+
"login.prompt": "Paste your AutoDL developer token",
|
|
1022
|
+
"login.hint": "Console \u2192 Settings \u2192 Developer Token (requires identity verification)",
|
|
1023
|
+
"login.verifying": "Verifying token\u2026",
|
|
1024
|
+
"login.success": "Token verified and saved to",
|
|
1025
|
+
"login.cancelled": "Cancelled",
|
|
1026
|
+
"logout.done": "Local token cleared",
|
|
1027
|
+
"instance.none": "No instances on this account",
|
|
1028
|
+
"instance.creating": "Creating instance\u2026",
|
|
1029
|
+
"instance.created": "Instance created",
|
|
1030
|
+
"instance.waiting": "Waiting for the instance to be ready\u2026",
|
|
1031
|
+
"instance.ready": "Instance ready",
|
|
1032
|
+
"instance.poweringOn": "Powering on\u2026",
|
|
1033
|
+
"instance.poweringOff": "Powering off\u2026",
|
|
1034
|
+
"instance.released": "Instance released",
|
|
1035
|
+
"instance.confirmRelease": "Releasing wipes all data permanently. Release",
|
|
1036
|
+
"guard.armed": "Auto-shutdown timer armed",
|
|
1037
|
+
"guard.armFailed": "Could not arm the in-instance timer; falling back to the local ledger",
|
|
1038
|
+
"guard.sweptOne": "Auto-stopped an instance past its TTL",
|
|
1039
|
+
"guard.blocked": "Balance below threshold; instance creation blocked",
|
|
1040
|
+
"ssh.connecting": "Connecting\u2026",
|
|
1041
|
+
"ssh.refreshing": "SSH credentials may have rotated, refreshing\u2026",
|
|
1042
|
+
"run.syncing": "Syncing files\u2026",
|
|
1043
|
+
"run.executing": "Running remotely\u2026",
|
|
1044
|
+
"run.pulling": "Downloading artefacts\u2026",
|
|
1045
|
+
"run.finished": "Done",
|
|
1046
|
+
"run.cleanup": "Cleaning up\u2026",
|
|
1047
|
+
"common.cancelled": "Cancelled",
|
|
1048
|
+
"common.yes": "yes",
|
|
1049
|
+
"common.no": "no"
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
var current = "zh";
|
|
1053
|
+
function resolveLang(explicit) {
|
|
1054
|
+
const candidate = (explicit ?? process.env.AUTODL_LANG ?? "").toLowerCase();
|
|
1055
|
+
if (candidate.startsWith("en")) return "en";
|
|
1056
|
+
if (candidate.startsWith("zh")) return "zh";
|
|
1057
|
+
return "zh";
|
|
1058
|
+
}
|
|
1059
|
+
function setLang(lang) {
|
|
1060
|
+
current = lang;
|
|
1061
|
+
}
|
|
1062
|
+
function t(key) {
|
|
1063
|
+
return MESSAGES[current][key] ?? MESSAGES.zh[key] ?? key;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/ssh/credentials.ts
|
|
1067
|
+
async function getCredentials(client, uuid, options2 = {}) {
|
|
1068
|
+
let status = await getInstanceStatus(client, uuid);
|
|
1069
|
+
if (status !== "running") {
|
|
1070
|
+
if (!options2.autoStart) {
|
|
1071
|
+
throw new SSHError(`\u5B9E\u4F8B ${uuid} \u5F53\u524D\u72B6\u6001\u4E3A "${status}"\uFF0C\u65E0\u6CD5\u5EFA\u7ACB SSH \u8FDE\u63A5`, {
|
|
1072
|
+
hint: "\u5148\u8FD0\u884C `autodl start <id>`\uFF0C\u6216\u52A0 --start \u8BA9\u547D\u4EE4\u81EA\u52A8\u5F00\u673A\u3002",
|
|
1073
|
+
details: { status }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (status === "shutting_down") {
|
|
1077
|
+
note("\u5B9E\u4F8B\u6B63\u5728\u5173\u673A\u4E2D\uFF0C\u7B49\u5F85\u5173\u673A\u5B8C\u6210\u540E\u518D\u5F00\u673A\u2026");
|
|
1078
|
+
status = await waitForShutdown(client, uuid, {
|
|
1079
|
+
...options2.waitTimeoutMs !== void 0 ? { timeoutMs: options2.waitTimeoutMs } : {},
|
|
1080
|
+
...options2.pollIntervalMs !== void 0 ? { intervalMs: options2.pollIntervalMs } : {},
|
|
1081
|
+
...options2.signal ? { signal: options2.signal } : {}
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (status === "shutdown") {
|
|
1085
|
+
note(t("instance.poweringOn"));
|
|
1086
|
+
await powerOnInstance(client, uuid);
|
|
1087
|
+
}
|
|
1088
|
+
status = await waitForRunning(client, uuid, {
|
|
1089
|
+
...options2.waitTimeoutMs !== void 0 ? { timeoutMs: options2.waitTimeoutMs } : {},
|
|
1090
|
+
...options2.pollIntervalMs !== void 0 ? { intervalMs: options2.pollIntervalMs } : {},
|
|
1091
|
+
...options2.signal ? { signal: options2.signal } : {},
|
|
1092
|
+
onPoll: (s) => debug(`\u5B9E\u4F8B ${uuid} \u72B6\u6001\uFF1A${s}`)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const snapshot = await getInstanceSnapshot(client, uuid);
|
|
1096
|
+
const { host, port, password } = snapshot.ssh;
|
|
1097
|
+
if (!host || !port || !password) {
|
|
1098
|
+
throw new SSHError(`\u5B9E\u4F8B ${uuid} \u5C1A\u672A\u8FD4\u56DE\u5B8C\u6574\u7684 SSH \u4FE1\u606F`, {
|
|
1099
|
+
hint: "\u5B9E\u4F8B\u53EF\u80FD\u4ECD\u5728\u542F\u52A8\u4E2D\uFF0C\u7A0D\u7B49\u7247\u523B\u540E\u91CD\u8BD5\u3002",
|
|
1100
|
+
details: { host, port, hasPassword: Boolean(password) }
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
return { uuid, host, port, user: "root", password };
|
|
1104
|
+
}
|
|
1105
|
+
var RETRY_DELAYS_MS = [2e3, 5e3, 8e3];
|
|
1106
|
+
function delay(ms) {
|
|
1107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1108
|
+
}
|
|
1109
|
+
function connectOnce(creds, options2) {
|
|
1110
|
+
return new Promise((resolve, reject) => {
|
|
1111
|
+
const conn = new Client();
|
|
1112
|
+
const settle = (err) => {
|
|
1113
|
+
conn.removeAllListeners("ready");
|
|
1114
|
+
conn.removeAllListeners("error");
|
|
1115
|
+
if (err) reject(err);
|
|
1116
|
+
else resolve(conn);
|
|
1117
|
+
};
|
|
1118
|
+
conn.once("ready", () => settle());
|
|
1119
|
+
conn.once("error", (err) => {
|
|
1120
|
+
conn.end();
|
|
1121
|
+
settle(err);
|
|
1122
|
+
});
|
|
1123
|
+
conn.connect({
|
|
1124
|
+
host: creds.host,
|
|
1125
|
+
port: creds.port,
|
|
1126
|
+
username: creds.user,
|
|
1127
|
+
password: creds.password,
|
|
1128
|
+
readyTimeout: options2.connectTimeoutMs ?? 3e4,
|
|
1129
|
+
keepaliveInterval: options2.keepaliveIntervalMs ?? 15e3,
|
|
1130
|
+
// AutoDL's proxy hosts reuse addresses across instances, so a known_hosts
|
|
1131
|
+
// check would fail constantly and teach users to ignore warnings.
|
|
1132
|
+
algorithms: void 0
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
async function withSSH(client, uuid, fn, options2 = {}) {
|
|
1137
|
+
let lastError;
|
|
1138
|
+
const attempts = Math.max(1, options2.connectAttempts ?? 3);
|
|
1139
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1140
|
+
if (attempt > 0) {
|
|
1141
|
+
note(t("ssh.refreshing"));
|
|
1142
|
+
await delay(RETRY_DELAYS_MS[attempt - 1] ?? RETRY_DELAYS_MS.at(-1) ?? 5e3);
|
|
1143
|
+
}
|
|
1144
|
+
const creds = await getCredentials(client, uuid, options2);
|
|
1145
|
+
let conn;
|
|
1146
|
+
try {
|
|
1147
|
+
conn = await connectOnce(creds, options2);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
lastError = err;
|
|
1150
|
+
debug(`SSH \u8FDE\u63A5 ${creds.host}:${creds.port} \u5931\u8D25\uFF1A${err.message}`);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
return await fn(conn, creds);
|
|
1155
|
+
} finally {
|
|
1156
|
+
conn.end();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
throw new SSHError(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u5B9E\u4F8B ${uuid}\uFF08\u5DF2\u5C1D\u8BD5 ${attempts} \u6B21\uFF09`, {
|
|
1160
|
+
hint: "\u5B9E\u4F8B\u53EF\u80FD\u4ECD\u5728\u542F\u52A8\u4E2D\uFF0C\u7A0D\u540E\u91CD\u8BD5\uFF1B\u6216\u8FD0\u884C `autodl info <id>` \u624B\u52A8\u6838\u5BF9 SSH \u7AEF\u53E3\u3002",
|
|
1161
|
+
cause: lastError
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
function shellQuote(value) {
|
|
1165
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/ssh/exec.ts
|
|
1169
|
+
function buildCommand(command, options2) {
|
|
1170
|
+
const parts = [];
|
|
1171
|
+
for (const [key, value] of Object.entries(options2.env ?? {})) {
|
|
1172
|
+
parts.push(`export ${key}=${JSON.stringify(value)}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (options2.cwd) parts.push(`cd ${JSON.stringify(options2.cwd)}`);
|
|
1175
|
+
parts.push(command);
|
|
1176
|
+
const inner = parts.join(" && ");
|
|
1177
|
+
return options2.loginShell === false ? inner : `bash -lc ${shellQuote(inner)}`;
|
|
1178
|
+
}
|
|
1179
|
+
function execOnConnection(conn, command, options2 = {}) {
|
|
1180
|
+
const capture = options2.capture ?? true;
|
|
1181
|
+
const full = buildCommand(command, options2);
|
|
1182
|
+
return new Promise((resolve, reject) => {
|
|
1183
|
+
conn.exec(full, { pty: options2.pty ?? false }, (err, stream) => {
|
|
1184
|
+
if (err) {
|
|
1185
|
+
reject(new SSHError(`\u8FDC\u7A0B\u547D\u4EE4\u542F\u52A8\u5931\u8D25\uFF1A${err.message}`, { cause: err }));
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
let stdout = "";
|
|
1189
|
+
let stderr = "";
|
|
1190
|
+
let settled = false;
|
|
1191
|
+
const timer = options2.timeoutMs ? setTimeout(() => {
|
|
1192
|
+
settled = true;
|
|
1193
|
+
stream.close();
|
|
1194
|
+
reject(
|
|
1195
|
+
new TimeoutError(`\u8FDC\u7A0B\u547D\u4EE4\u8D85\u8FC7 ${options2.timeoutMs}ms \u672A\u7ED3\u675F\uFF0C\u5DF2\u7EC8\u6B62`, {
|
|
1196
|
+
details: { command }
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
}, options2.timeoutMs) : void 0;
|
|
1200
|
+
stream.on("data", (chunk) => {
|
|
1201
|
+
if (capture) stdout += chunk.toString("utf8");
|
|
1202
|
+
options2.stdout?.write(chunk);
|
|
1203
|
+
});
|
|
1204
|
+
stream.stderr.on("data", (chunk) => {
|
|
1205
|
+
if (capture) stderr += chunk.toString("utf8");
|
|
1206
|
+
options2.stderr?.write(chunk);
|
|
1207
|
+
});
|
|
1208
|
+
stream.on("close", (code, signal) => {
|
|
1209
|
+
if (timer) clearTimeout(timer);
|
|
1210
|
+
if (settled) return;
|
|
1211
|
+
resolve({ exitCode: code ?? null, signal: signal ?? null, stdout, stderr });
|
|
1212
|
+
});
|
|
1213
|
+
stream.on("error", (streamErr) => {
|
|
1214
|
+
if (timer) clearTimeout(timer);
|
|
1215
|
+
if (settled) return;
|
|
1216
|
+
reject(new SSHError(`\u8FDC\u7A0B\u547D\u4EE4\u6267\u884C\u51FA\u9519\uFF1A${streamErr.message}`, { cause: streamErr }));
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
async function execCommand(client, uuid, command, options2 = {}) {
|
|
1222
|
+
return withSSH(client, uuid, (conn) => execOnConnection(conn, command, options2), options2);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/config/state.ts
|
|
1226
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1227
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1228
|
+
var EMPTY = { version: 1, tracked: {} };
|
|
1229
|
+
var statePath = () => join2(configDir(), "state.json");
|
|
1230
|
+
function readState() {
|
|
1231
|
+
const file = statePath();
|
|
1232
|
+
if (!existsSync2(file)) return { ...EMPTY, tracked: {} };
|
|
1233
|
+
try {
|
|
1234
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
1235
|
+
return { version: 1, tracked: parsed.tracked ?? {} };
|
|
1236
|
+
} catch {
|
|
1237
|
+
return { ...EMPTY, tracked: {} };
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function writeState(state) {
|
|
1241
|
+
const file = statePath();
|
|
1242
|
+
mkdirSync2(dirname2(file), { recursive: true, mode: 448 });
|
|
1243
|
+
writeFileSync2(file, `${JSON.stringify(state, null, 2)}
|
|
1244
|
+
`, { mode: 384 });
|
|
1245
|
+
}
|
|
1246
|
+
function trackInstance(entry) {
|
|
1247
|
+
const state = readState();
|
|
1248
|
+
state.tracked[entry.uuid] = entry;
|
|
1249
|
+
writeState(state);
|
|
1250
|
+
}
|
|
1251
|
+
function untrackInstance(uuid) {
|
|
1252
|
+
const state = readState();
|
|
1253
|
+
if (state.tracked[uuid]) {
|
|
1254
|
+
delete state.tracked[uuid];
|
|
1255
|
+
writeState(state);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function listTracked() {
|
|
1259
|
+
return Object.values(readState().tracked);
|
|
1260
|
+
}
|
|
1261
|
+
function listExpired(now = Date.now()) {
|
|
1262
|
+
return listTracked().filter((entry) => entry.expiresAt <= now);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/guard/ttl.ts
|
|
1266
|
+
var PID_FILE = "/tmp/.autodl-cli-ttl.pid";
|
|
1267
|
+
function buildTTLSnippet(seconds) {
|
|
1268
|
+
return `(sleep ${seconds} && /usr/bin/shutdown -h now) >/dev/null 2>&1 &`;
|
|
1269
|
+
}
|
|
1270
|
+
function composeStartCommand(ttlSeconds, userCommand) {
|
|
1271
|
+
const parts = [];
|
|
1272
|
+
if (ttlSeconds && ttlSeconds > 0) parts.push(buildTTLSnippet(ttlSeconds));
|
|
1273
|
+
if (userCommand?.trim()) parts.push(userCommand.trim());
|
|
1274
|
+
return parts.length ? parts.join(" ") : void 0;
|
|
1275
|
+
}
|
|
1276
|
+
async function armTTLOverSSH(client, uuid, seconds) {
|
|
1277
|
+
const command = [
|
|
1278
|
+
`if [ -f ${PID_FILE} ]; then kill "$(cat ${PID_FILE})" 2>/dev/null || true; fi`,
|
|
1279
|
+
`(sleep ${seconds} && /usr/bin/shutdown -h now) >/dev/null 2>&1 &`,
|
|
1280
|
+
`echo $! > ${PID_FILE}`
|
|
1281
|
+
].join("; ");
|
|
1282
|
+
try {
|
|
1283
|
+
const result = await execCommand(client, uuid, command, { capture: true, timeoutMs: 3e4 });
|
|
1284
|
+
if (result.exitCode !== 0) {
|
|
1285
|
+
debug(`\u5B9A\u65F6\u5173\u673A\u547D\u4EE4\u9000\u51FA\u7801 ${result.exitCode}\uFF1A${result.stderr.trim()}`);
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
debug(`\u5B9A\u65F6\u5173\u673A\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF1A${err.message}`);
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async function disarmTTLOverSSH(client, uuid) {
|
|
1295
|
+
const command = `if [ -f ${PID_FILE} ]; then kill "$(cat ${PID_FILE})" 2>/dev/null; rm -f ${PID_FILE}; fi; /usr/bin/shutdown -c 2>/dev/null || true`;
|
|
1296
|
+
try {
|
|
1297
|
+
await execCommand(client, uuid, command, { capture: false, timeoutMs: 3e4 });
|
|
1298
|
+
return true;
|
|
1299
|
+
} catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function recordTTL(input) {
|
|
1304
|
+
const now = Date.now();
|
|
1305
|
+
trackInstance({
|
|
1306
|
+
uuid: input.uuid,
|
|
1307
|
+
...input.name ? { name: input.name } : {},
|
|
1308
|
+
ttlSeconds: input.ttlSeconds,
|
|
1309
|
+
expiresAt: now + input.ttlSeconds * 1e3,
|
|
1310
|
+
createdAt: now,
|
|
1311
|
+
inInstanceTimer: input.inInstanceTimer
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async function sweepExpired(client) {
|
|
1315
|
+
const result = { stopped: [], alreadyStopped: [], failed: [] };
|
|
1316
|
+
const expired = listExpired();
|
|
1317
|
+
if (expired.length === 0) return result;
|
|
1318
|
+
for (const entry of expired) {
|
|
1319
|
+
try {
|
|
1320
|
+
const status = await getInstanceStatus(client, entry.uuid);
|
|
1321
|
+
if (status === "running" || status === "starting") {
|
|
1322
|
+
await powerOffInstance(client, entry.uuid);
|
|
1323
|
+
const after = await getInstanceStatus(client, entry.uuid).catch(() => status);
|
|
1324
|
+
if (after === "running" || after === "starting") {
|
|
1325
|
+
result.failed.push({ uuid: entry.uuid, reason: `\u5173\u673A\u672A\u751F\u6548\uFF08\u72B6\u6001\u4ECD\u4E3A ${after}\uFF09` });
|
|
1326
|
+
warn(
|
|
1327
|
+
`${entry.name ?? entry.uuid} \u5173\u673A\u672A\u751F\u6548\uFF08\u72B6\u6001 ${after}\uFF09\uFF0C\u4FDD\u7559\u5728\u53F0\u8D26\u4E2D\uFF0C\u4E0B\u6B21\u547D\u4EE4\u4F1A\u91CD\u8BD5`
|
|
1328
|
+
);
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
result.stopped.push(entry.uuid);
|
|
1332
|
+
warn(
|
|
1333
|
+
`${t("guard.sweptOne")}\uFF1A${entry.name ?? entry.uuid}\uFF08TTL ${formatDuration(entry.ttlSeconds)}\uFF09`
|
|
1334
|
+
);
|
|
1335
|
+
} else {
|
|
1336
|
+
result.alreadyStopped.push(entry.uuid);
|
|
1337
|
+
}
|
|
1338
|
+
untrackInstance(entry.uuid);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
const reason = err.message;
|
|
1341
|
+
result.failed.push({ uuid: entry.uuid, reason });
|
|
1342
|
+
debug(`\u6E05\u7406\u8D85\u65F6\u5B9E\u4F8B ${entry.uuid} \u5931\u8D25\uFF1A${reason}`);
|
|
1343
|
+
if (/不存在|not found/i.test(reason)) untrackInstance(entry.uuid);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export {
|
|
1350
|
+
__require,
|
|
1351
|
+
__commonJS,
|
|
1352
|
+
__export,
|
|
1353
|
+
__toESM,
|
|
1354
|
+
ExitCode,
|
|
1355
|
+
AutoDLError,
|
|
1356
|
+
AuthError,
|
|
1357
|
+
NotFoundError,
|
|
1358
|
+
UsageError,
|
|
1359
|
+
BudgetError,
|
|
1360
|
+
NoStockError,
|
|
1361
|
+
TimeoutError,
|
|
1362
|
+
SSHError,
|
|
1363
|
+
toAutoDLError,
|
|
1364
|
+
isAuthError,
|
|
1365
|
+
configPath,
|
|
1366
|
+
readConfig,
|
|
1367
|
+
writeConfig,
|
|
1368
|
+
updateConfig,
|
|
1369
|
+
clearToken,
|
|
1370
|
+
resolveToken,
|
|
1371
|
+
tryResolveToken,
|
|
1372
|
+
resolveBaseUrl,
|
|
1373
|
+
DEFAULT_BASE_URL,
|
|
1374
|
+
redactToken,
|
|
1375
|
+
AutoDLClient,
|
|
1376
|
+
untrackInstance,
|
|
1377
|
+
listTracked,
|
|
1378
|
+
parseDuration,
|
|
1379
|
+
formatDuration,
|
|
1380
|
+
milliToYuan,
|
|
1381
|
+
yuanToMilli,
|
|
1382
|
+
formatYuan,
|
|
1383
|
+
formatRate,
|
|
1384
|
+
estimateCost,
|
|
1385
|
+
normalizeInstance,
|
|
1386
|
+
normalizeSnapshot,
|
|
1387
|
+
normalizeBalance,
|
|
1388
|
+
normalizeImage,
|
|
1389
|
+
normalizePagination,
|
|
1390
|
+
redactSnapshot,
|
|
1391
|
+
createInstance,
|
|
1392
|
+
listInstancesPage,
|
|
1393
|
+
listAllInstances,
|
|
1394
|
+
getInstanceStatus,
|
|
1395
|
+
getInstanceSnapshot,
|
|
1396
|
+
findInstance,
|
|
1397
|
+
powerOnInstance,
|
|
1398
|
+
powerOffInstance,
|
|
1399
|
+
releaseInstance,
|
|
1400
|
+
configureOutput,
|
|
1401
|
+
isJson,
|
|
1402
|
+
isVerbose,
|
|
1403
|
+
note,
|
|
1404
|
+
success,
|
|
1405
|
+
warn,
|
|
1406
|
+
debug,
|
|
1407
|
+
emit,
|
|
1408
|
+
emitError,
|
|
1409
|
+
printTable,
|
|
1410
|
+
printKeyValues,
|
|
1411
|
+
stringWidth,
|
|
1412
|
+
colorStatus,
|
|
1413
|
+
formatBytes,
|
|
1414
|
+
formatTime,
|
|
1415
|
+
resolveLang,
|
|
1416
|
+
setLang,
|
|
1417
|
+
t,
|
|
1418
|
+
waitForStatus,
|
|
1419
|
+
waitForRunning,
|
|
1420
|
+
waitForShutdown,
|
|
1421
|
+
getCredentials,
|
|
1422
|
+
withSSH,
|
|
1423
|
+
execOnConnection,
|
|
1424
|
+
execCommand,
|
|
1425
|
+
buildTTLSnippet,
|
|
1426
|
+
composeStartCommand,
|
|
1427
|
+
armTTLOverSSH,
|
|
1428
|
+
disarmTTLOverSSH,
|
|
1429
|
+
recordTTL,
|
|
1430
|
+
sweepExpired
|
|
1431
|
+
};
|
|
1432
|
+
//# sourceMappingURL=chunk-6QR4ZYQR.js.map
|