@gaoding/cli 0.0.0 → 1.0.0-alpha.25
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 +40 -3
- package/contracts/operations/agent.send/input.schema.json +353 -0
- package/contracts/operations/agent.send/output.schema.json +271 -0
- package/contracts/operations/auth.status/input.schema.json +7 -0
- package/contracts/operations/auth.status/output.schema.json +132 -0
- package/contracts/operations/dam.copy/input.schema.json +19 -0
- package/contracts/operations/dam.copy/output.schema.json +19 -0
- package/contracts/operations/dam.delete/input.schema.json +23 -0
- package/contracts/operations/dam.delete/output.schema.json +6 -0
- package/contracts/operations/dam.download/input.schema.json +14 -0
- package/contracts/operations/dam.download/output.schema.json +25 -0
- package/contracts/operations/dam.folder.list/input.schema.json +15 -0
- package/contracts/operations/dam.folder.list/output.schema.json +37 -0
- package/contracts/operations/dam.get/input.schema.json +16 -0
- package/contracts/operations/dam.get/output.schema.json +46 -0
- package/contracts/operations/dam.list/input.schema.json +44 -0
- package/contracts/operations/dam.list/output.schema.json +71 -0
- package/contracts/operations/dam.move/input.schema.json +19 -0
- package/contracts/operations/dam.move/output.schema.json +18 -0
- package/contracts/operations/dam.recycle.list/input.schema.json +13 -0
- package/contracts/operations/dam.recycle.list/output.schema.json +34 -0
- package/contracts/operations/dam.recycle.restore/input.schema.json +14 -0
- package/contracts/operations/dam.recycle.restore/output.schema.json +6 -0
- package/contracts/operations/dam.rename/input.schema.json +18 -0
- package/contracts/operations/dam.rename/output.schema.json +18 -0
- package/contracts/operations/dam.repository.list/input.schema.json +8 -0
- package/contracts/operations/dam.repository.list/output.schema.json +52 -0
- package/contracts/operations/dam.search/input.schema.json +46 -0
- package/contracts/operations/dam.search/output.schema.json +71 -0
- package/contracts/operations/dam.tag.list/input.schema.json +12 -0
- package/contracts/operations/dam.tag.list/output.schema.json +26 -0
- package/contracts/operations/dam.upload/input.schema.json +31 -0
- package/contracts/operations/dam.upload/output.schema.json +46 -0
- package/contracts/operations/editor.apply/input.schema.json +134 -0
- package/contracts/operations/editor.apply/output.schema.json +15 -0
- package/contracts/operations/editor.connect/input.schema.json +15 -0
- package/contracts/operations/editor.connect/output.schema.json +17 -0
- package/contracts/operations/editor.disconnect/input.schema.json +8 -0
- package/contracts/operations/editor.disconnect/output.schema.json +14 -0
- package/contracts/operations/editor.save/input.schema.json +8 -0
- package/contracts/operations/editor.save/output.schema.json +23 -0
- package/contracts/operations/editor.screenshot/input.schema.json +8 -0
- package/contracts/operations/editor.screenshot/output.schema.json +16 -0
- package/contracts/operations/editor.snapshot/input.schema.json +8 -0
- package/contracts/operations/editor.snapshot/output.schema.json +113 -0
- package/contracts/operations/model.get/input.schema.json +18 -0
- package/contracts/operations/model.get/output.schema.json +191 -0
- package/contracts/operations/model.list/input.schema.json +15 -0
- package/contracts/operations/model.list/output.schema.json +111 -0
- package/contracts/operations/org.current/input.schema.json +7 -0
- package/contracts/operations/org.current/output.schema.json +49 -0
- package/contracts/operations/org.list/input.schema.json +7 -0
- package/contracts/operations/org.list/output.schema.json +71 -0
- package/contracts/operations/tool.call/input.schema.json +35 -0
- package/contracts/operations/tool.call/output.schema.json +129 -0
- package/contracts/operations/tool.list/input.schema.json +7 -0
- package/contracts/operations/tool.list/output.schema.json +45 -0
- package/dist/bin/gd-cli.js +60 -0
- package/dist/bin/postinstall.js +42 -0
- package/dist/src/bootstrap/create-cli.js +112 -0
- package/dist/src/bootstrap/create-runtime.js +290 -0
- package/dist/src/bootstrap/validators.js +157 -0
- package/dist/src/cli/action-binding.js +109 -0
- package/dist/src/cli/agent-commands.js +49 -0
- package/dist/src/cli/auth-commands.js +55 -0
- package/dist/src/cli/dam-commands.js +469 -0
- package/dist/src/cli/editor-commands.js +73 -0
- package/dist/src/cli/errors.js +157 -0
- package/dist/src/cli/model-commands.js +61 -0
- package/dist/src/cli/org-commands.js +58 -0
- package/dist/src/cli/presenter.js +105 -0
- package/dist/src/cli/prompt.js +30 -0
- package/dist/src/cli/tool-commands.js +77 -0
- package/dist/src/cli/update-command.js +14 -0
- package/dist/src/contracts/schema.js +4 -0
- package/dist/src/features/agent/creative-agent-adapter.js +36 -0
- package/dist/src/features/agent/creative-protocol.js +432 -0
- package/dist/src/features/agent/creative-stream.js +127 -0
- package/dist/src/features/agent/use-cases.js +150 -0
- package/dist/src/features/auth/access-policy.js +101 -0
- package/dist/src/features/auth/credential-store.js +62 -0
- package/dist/src/features/auth/sso-service.js +194 -0
- package/dist/src/features/auth/state.js +129 -0
- package/dist/src/features/auth/use-cases.js +136 -0
- package/dist/src/features/dam/asset-projection.js +278 -0
- package/dist/src/features/dam/dam-api-adapter.js +562 -0
- package/dist/src/features/dam/file-downloader.js +242 -0
- package/dist/src/features/dam/object-storage.js +193 -0
- package/dist/src/features/dam/operation-executor.js +72 -0
- package/dist/src/features/dam/registered-uploader.js +247 -0
- package/dist/src/features/dam/repository-catalog.js +127 -0
- package/dist/src/features/dam/storage-upload.js +141 -0
- package/dist/src/features/dam/transient-uploader.js +17 -0
- package/dist/src/features/dam/use-cases.js +470 -0
- package/dist/src/features/editor/bridge-client.js +109 -0
- package/dist/src/features/editor/bridge-process.js +222 -0
- package/dist/src/features/editor/bridge-server.js +337 -0
- package/dist/src/features/editor/protocol.js +1 -0
- package/dist/src/features/editor/public-url.js +36 -0
- package/dist/src/features/editor/session-state.js +78 -0
- package/dist/src/features/editor/session.js +207 -0
- package/dist/src/features/editor/use-cases.js +88 -0
- package/dist/src/features/org/org-service.js +76 -0
- package/dist/src/features/org/use-cases.js +156 -0
- package/dist/src/features/skill/bundled-skills.js +55 -0
- package/dist/src/features/skill/installer.js +271 -0
- package/dist/src/features/tool/catalog.js +83 -0
- package/dist/src/features/tool/dynamic-schema.js +128 -0
- package/dist/src/features/tool/mns-catalog-adapter.js +265 -0
- package/dist/src/features/tool/tool-api-adapter.js +352 -0
- package/dist/src/features/tool/use-cases.js +177 -0
- package/dist/src/features/update/latest-version.js +37 -0
- package/dist/src/features/update/update-notification.js +262 -0
- package/dist/src/features/update/update-service.js +183 -0
- package/dist/src/platform/json-input.js +67 -0
- package/dist/src/platform/local-json-file.js +59 -0
- package/dist/src/platform/open-browser.js +8 -0
- package/dist/src/platform/redact.js +80 -0
- package/dist/src/platform/remote-error-evidence.js +80 -0
- package/dist/src/platform/remote-protocol.js +10 -0
- package/dist/src/platform/safe-upload-file.js +146 -0
- package/dist/src/platform/signature.js +20 -0
- package/dist/src/platform/signed-http-transport.js +143 -0
- package/dist/src/platform/url-safety.js +100 -0
- package/dist/src/telemetry/invocation.js +156 -0
- package/dist/src/telemetry/sls-sink.js +31 -0
- package/package.json +54 -8
- package/skills/gd-cli/SKILL.md +15 -0
- package/skills/gd-cli/references/auth-org.md +9 -0
- package/skills/gd-cli/references/creation.md +115 -0
- package/skills/gd-cli/references/dam.md +52 -0
- package/skills/gd-cli/references/editor.md +254 -0
- package/skills/gd-cli/references/errors.md +13 -0
- package/skills/gd-cli/references/update.md +13 -0
- package/index.js +0 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { access, mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { extname, resolve } from "node:path";
|
|
4
|
+
import { parseSafeRemoteAssetUrl } from "../../platform/url-safety.js";
|
|
5
|
+
export class DamDownloadError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("素材下载失败。");
|
|
8
|
+
this.name = "DamDownloadError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class DamDownloadDestinationExistsError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("目标文件已存在,未覆盖任何文件。");
|
|
14
|
+
this.name = "DamDownloadDestinationExistsError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
|
|
18
|
+
const maximumRedirects = 5;
|
|
19
|
+
const maximumFilenameLength = 180;
|
|
20
|
+
const nodeFileSystem = {
|
|
21
|
+
async mkdir(path, options) {
|
|
22
|
+
await mkdir(path, options);
|
|
23
|
+
},
|
|
24
|
+
async open(path, flags, mode) {
|
|
25
|
+
const handle = await open(path, flags, mode);
|
|
26
|
+
return {
|
|
27
|
+
write: async (data) => handle.write(data),
|
|
28
|
+
sync: async () => handle.sync(),
|
|
29
|
+
close: async () => handle.close()
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
access,
|
|
33
|
+
rename,
|
|
34
|
+
async rm(path, options) {
|
|
35
|
+
await rm(path, options);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function safeDownloadUrl(value) {
|
|
39
|
+
try {
|
|
40
|
+
const url = parseSafeRemoteAssetUrl(value);
|
|
41
|
+
if (url.protocol !== "https:")
|
|
42
|
+
throw new Error("HTTPS required");
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new DamDownloadError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function decodedFilename(value) {
|
|
50
|
+
const encoded = /(?:^|;)\s*filename\*\s*=\s*(?:UTF-8'')?("?)([^;"]*)\1/iu.exec(value);
|
|
51
|
+
if (encoded?.[2] !== undefined) {
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(encoded[2].trim());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const plain = /(?:^|;)\s*filename\s*=\s*(?:"([^"]*)"|([^;]*))/iu.exec(value);
|
|
60
|
+
return plain?.[1]?.trim() ?? plain?.[2]?.trim();
|
|
61
|
+
}
|
|
62
|
+
function safeFormat(value) {
|
|
63
|
+
const format = value?.trim().replace(/^\.+/u, "").replace(/[^a-zA-Z0-9]+/gu, "");
|
|
64
|
+
return format === undefined || format === "" ? "bin" : format.toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
function truncateFilename(value) {
|
|
67
|
+
if (value.length <= maximumFilenameLength)
|
|
68
|
+
return value;
|
|
69
|
+
const extension = extname(value);
|
|
70
|
+
const extensionLength = Math.min(extension.length, 20);
|
|
71
|
+
const safeExtension = extension.slice(0, extensionLength);
|
|
72
|
+
const stemLength = maximumFilenameLength - safeExtension.length;
|
|
73
|
+
return `${value.slice(0, Math.max(1, stemLength))}${safeExtension}`;
|
|
74
|
+
}
|
|
75
|
+
function safeFilename(candidate, fallback, format) {
|
|
76
|
+
const raw = (candidate === undefined || candidate.trim() === "") ? fallback : candidate;
|
|
77
|
+
const segments = raw.normalize("NFKC").replace(/[\u0000-\u001f\u007f]/gu, "")
|
|
78
|
+
.split(/[\\/]+/u);
|
|
79
|
+
let filename = (segments.at(-1) ?? "").trim().replace(/^\.+/u, "");
|
|
80
|
+
if (filename === "" || filename === "." || filename === "..")
|
|
81
|
+
filename = "asset";
|
|
82
|
+
if (extname(filename) === "")
|
|
83
|
+
filename = `${filename}.${format}`;
|
|
84
|
+
return truncateFilename(filename);
|
|
85
|
+
}
|
|
86
|
+
function withIndex(filename, index, total) {
|
|
87
|
+
if (total === 1)
|
|
88
|
+
return filename;
|
|
89
|
+
const extension = extname(filename);
|
|
90
|
+
const stem = extension === "" ? filename : filename.slice(0, -extension.length);
|
|
91
|
+
return truncateFilename(`${stem}-${index + 1}${extension}`);
|
|
92
|
+
}
|
|
93
|
+
async function fetchDownload(initial, fetch, signal) {
|
|
94
|
+
let url = safeDownloadUrl(initial);
|
|
95
|
+
for (let redirects = 0; redirects <= maximumRedirects; redirects += 1) {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
response = await fetch(url, { method: "GET", redirect: "manual", signal });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
throw signal.reason;
|
|
104
|
+
throw new DamDownloadError();
|
|
105
|
+
}
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
if (!redirectStatuses.has(response.status)) {
|
|
108
|
+
if (!response.ok || response.body === null)
|
|
109
|
+
throw new DamDownloadError();
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
const location = response.headers.get("location");
|
|
113
|
+
if (location === null || redirects === maximumRedirects)
|
|
114
|
+
throw new DamDownloadError();
|
|
115
|
+
try {
|
|
116
|
+
url = safeDownloadUrl(new URL(location, url).href);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new DamDownloadError();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await response.body?.cancel().catch(() => undefined);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new DamDownloadError();
|
|
126
|
+
}
|
|
127
|
+
async function writeAll(handle, data) {
|
|
128
|
+
let offset = 0;
|
|
129
|
+
while (offset < data.byteLength) {
|
|
130
|
+
const { bytesWritten } = await handle.write(data.subarray(offset));
|
|
131
|
+
if (!Number.isInteger(bytesWritten) || bytesWritten <= 0)
|
|
132
|
+
throw new DamDownloadError();
|
|
133
|
+
offset += bytesWritten;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isMissingFile(error) {
|
|
137
|
+
return error !== null
|
|
138
|
+
&& typeof error === "object"
|
|
139
|
+
&& "code" in error
|
|
140
|
+
&& error.code === "ENOENT";
|
|
141
|
+
}
|
|
142
|
+
export function createDamFileDownloader(options = {}) {
|
|
143
|
+
const dependencies = {
|
|
144
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
145
|
+
fileSystem: options.fileSystem ?? nodeFileSystem,
|
|
146
|
+
randomId: options.randomId ?? randomUUID,
|
|
147
|
+
cwd: options.cwd ?? process.cwd
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
async download(input) {
|
|
151
|
+
input.signal.throwIfAborted();
|
|
152
|
+
if (input.urls.length === 0)
|
|
153
|
+
throw new DamDownloadError();
|
|
154
|
+
const outputDirectory = resolve(dependencies.cwd(), input.outputDirectory);
|
|
155
|
+
const pending = [];
|
|
156
|
+
const createdFinalPaths = [];
|
|
157
|
+
try {
|
|
158
|
+
await dependencies.fileSystem.mkdir(outputDirectory, { recursive: true });
|
|
159
|
+
for (const [index, rawUrl] of input.urls.entries()) {
|
|
160
|
+
input.signal.throwIfAborted();
|
|
161
|
+
const response = await fetchDownload(rawUrl, dependencies.fetch, input.signal);
|
|
162
|
+
const format = safeFormat(input.fallbackFormat);
|
|
163
|
+
const fallback = safeFilename(input.fallbackTitle, "asset", format);
|
|
164
|
+
const disposition = response.headers.get("content-disposition");
|
|
165
|
+
const filename = withIndex(safeFilename(disposition === null ? undefined : decodedFilename(disposition), fallback, format), index, input.urls.length);
|
|
166
|
+
const finalPath = resolve(outputDirectory, filename);
|
|
167
|
+
if (!finalPath.startsWith(`${outputDirectory}/`) && finalPath !== outputDirectory) {
|
|
168
|
+
throw new DamDownloadError();
|
|
169
|
+
}
|
|
170
|
+
const temporaryPath = resolve(outputDirectory, `.part-${dependencies.randomId()}-${index + 1}`);
|
|
171
|
+
const handle = await dependencies.fileSystem.open(temporaryPath, "wx", 0o600);
|
|
172
|
+
const item = {
|
|
173
|
+
temporaryPath,
|
|
174
|
+
finalPath,
|
|
175
|
+
handle,
|
|
176
|
+
open: true,
|
|
177
|
+
bytes: 0,
|
|
178
|
+
sha256: ""
|
|
179
|
+
};
|
|
180
|
+
pending.push(item);
|
|
181
|
+
const hash = createHash("sha256");
|
|
182
|
+
const reader = response.body.getReader();
|
|
183
|
+
try {
|
|
184
|
+
while (true) {
|
|
185
|
+
input.signal.throwIfAborted();
|
|
186
|
+
const chunk = await reader.read();
|
|
187
|
+
if (chunk.done)
|
|
188
|
+
break;
|
|
189
|
+
await writeAll(handle, chunk.value);
|
|
190
|
+
hash.update(chunk.value);
|
|
191
|
+
item.bytes += chunk.value.byteLength;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
reader.releaseLock();
|
|
196
|
+
}
|
|
197
|
+
item.sha256 = hash.digest("hex");
|
|
198
|
+
}
|
|
199
|
+
for (const item of pending) {
|
|
200
|
+
try {
|
|
201
|
+
await dependencies.fileSystem.access(item.finalPath);
|
|
202
|
+
throw new DamDownloadDestinationExistsError();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof DamDownloadDestinationExistsError)
|
|
206
|
+
throw error;
|
|
207
|
+
if (!isMissingFile(error))
|
|
208
|
+
throw new DamDownloadError();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const item of pending) {
|
|
212
|
+
await item.handle.sync();
|
|
213
|
+
await item.handle.close();
|
|
214
|
+
item.open = false;
|
|
215
|
+
}
|
|
216
|
+
for (const item of pending) {
|
|
217
|
+
await dependencies.fileSystem.rename(item.temporaryPath, item.finalPath);
|
|
218
|
+
createdFinalPaths.push(item.finalPath);
|
|
219
|
+
}
|
|
220
|
+
return pending.map((item) => ({
|
|
221
|
+
path: item.finalPath,
|
|
222
|
+
bytes: item.bytes,
|
|
223
|
+
sha256: item.sha256
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
await Promise.all(pending.map(async (item) => {
|
|
228
|
+
if (item.open)
|
|
229
|
+
await item.handle.close().catch(() => undefined);
|
|
230
|
+
await dependencies.fileSystem.rm(item.temporaryPath, { force: true }).catch(() => undefined);
|
|
231
|
+
}));
|
|
232
|
+
await Promise.all(createdFinalPaths.map((path) => (dependencies.fileSystem.rm(path, { force: true }).catch(() => undefined))));
|
|
233
|
+
if (input.signal.aborted)
|
|
234
|
+
throw input.signal.reason;
|
|
235
|
+
if (error instanceof DamDownloadDestinationExistsError
|
|
236
|
+
|| error instanceof DamDownloadError)
|
|
237
|
+
throw error;
|
|
238
|
+
throw new DamDownloadError();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import OSS from "ali-oss";
|
|
5
|
+
export class ObjectStorageUploadError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("素材上传失败。");
|
|
8
|
+
this.name = "ObjectStorageUploadError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function normalizeEndpoint(value) {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
14
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
15
|
+
throw new Error("unsupported protocol");
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new ObjectStorageUploadError();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function endpointOrigin(value) {
|
|
24
|
+
return normalizeEndpoint(value).origin;
|
|
25
|
+
}
|
|
26
|
+
function endpointOrder(token) {
|
|
27
|
+
return [...new Set([
|
|
28
|
+
token.endpoint,
|
|
29
|
+
token.standbyEndpoint,
|
|
30
|
+
token.accelerateEndpoint
|
|
31
|
+
].filter((value) => value !== undefined).map(endpointOrigin))];
|
|
32
|
+
}
|
|
33
|
+
function errorText(error) {
|
|
34
|
+
if (!(error instanceof Error))
|
|
35
|
+
return "";
|
|
36
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : "";
|
|
37
|
+
return `${code} ${error.message}`;
|
|
38
|
+
}
|
|
39
|
+
function isTransient(error) {
|
|
40
|
+
const text = errorText(error);
|
|
41
|
+
return text.includes("socket hang up")
|
|
42
|
+
|| text.includes("ECONNRESET")
|
|
43
|
+
|| text.includes("ETIMEDOUT")
|
|
44
|
+
|| text.includes("EPIPE");
|
|
45
|
+
}
|
|
46
|
+
function isUnsupportedAuthorization(error) {
|
|
47
|
+
return errorText(error).includes("Unsupported Authorization Type");
|
|
48
|
+
}
|
|
49
|
+
function sleepWithSignal(milliseconds, signal) {
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
signal.removeEventListener("abort", onAbort);
|
|
54
|
+
resolve();
|
|
55
|
+
}, milliseconds);
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
reject(signal.reason);
|
|
59
|
+
};
|
|
60
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const defaultPutOss = async (input) => {
|
|
64
|
+
input.signal.throwIfAborted();
|
|
65
|
+
const client = new OSS({
|
|
66
|
+
accessKeyId: input.token.accessKeyId,
|
|
67
|
+
accessKeySecret: input.token.accessKeySecret,
|
|
68
|
+
stsToken: input.token.securityToken,
|
|
69
|
+
// DAM issues one-shot credentials without a refresh endpoint.
|
|
70
|
+
refreshSTSTokenInterval: Number.MAX_SAFE_INTEGER,
|
|
71
|
+
endpoint: input.endpoint,
|
|
72
|
+
region: input.token.region,
|
|
73
|
+
bucket: input.token.bucketName,
|
|
74
|
+
secure: true,
|
|
75
|
+
authorizationV4: input.authorizationV4
|
|
76
|
+
});
|
|
77
|
+
await client.put(input.token.objectName, input.filePath, {
|
|
78
|
+
headers: { "Content-Type": input.mediaType }
|
|
79
|
+
});
|
|
80
|
+
input.signal.throwIfAborted();
|
|
81
|
+
};
|
|
82
|
+
async function attemptOss(input, dependencies) {
|
|
83
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
84
|
+
input.signal.throwIfAborted();
|
|
85
|
+
try {
|
|
86
|
+
await dependencies.putOss(input);
|
|
87
|
+
input.signal.throwIfAborted();
|
|
88
|
+
return "success";
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (input.signal.aborted)
|
|
92
|
+
throw input.signal.reason;
|
|
93
|
+
if (!input.authorizationV4 && isUnsupportedAuthorization(error)) {
|
|
94
|
+
return "unsupported-authorization";
|
|
95
|
+
}
|
|
96
|
+
if (!isTransient(error))
|
|
97
|
+
throw new ObjectStorageUploadError();
|
|
98
|
+
if (attempt === 1)
|
|
99
|
+
return "transient-exhausted";
|
|
100
|
+
await dependencies.sleep(300, input.signal);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return "transient-exhausted";
|
|
104
|
+
}
|
|
105
|
+
function isHuaweiObs(endpoint) {
|
|
106
|
+
const hostname = normalizeEndpoint(endpoint).hostname;
|
|
107
|
+
return /(^|\.)obs\.[^.]+\.myhuaweicloud\.com$/i.test(hostname);
|
|
108
|
+
}
|
|
109
|
+
function encodedObjectPath(objectName) {
|
|
110
|
+
return objectName.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
111
|
+
}
|
|
112
|
+
async function uploadObs(input, dependencies) {
|
|
113
|
+
input.signal.throwIfAborted();
|
|
114
|
+
const file = await stat(input.filePath);
|
|
115
|
+
input.signal.throwIfAborted();
|
|
116
|
+
const endpoint = normalizeEndpoint(input.endpoint);
|
|
117
|
+
const objectName = input.token.objectName.replace(/^\/+/, "");
|
|
118
|
+
endpoint.hostname = `${input.token.bucketName}.${endpoint.hostname}`;
|
|
119
|
+
endpoint.pathname = `/${encodedObjectPath(objectName)}`;
|
|
120
|
+
const date = dependencies.now().toUTCString();
|
|
121
|
+
const stringToSign = [
|
|
122
|
+
"PUT",
|
|
123
|
+
"",
|
|
124
|
+
input.mediaType,
|
|
125
|
+
date,
|
|
126
|
+
`x-obs-security-token:${input.token.securityToken}`,
|
|
127
|
+
`/${input.token.bucketName}/${objectName}`
|
|
128
|
+
].join("\n");
|
|
129
|
+
const signature = createHmac("sha1", input.token.accessKeySecret)
|
|
130
|
+
.update(stringToSign)
|
|
131
|
+
.digest("base64");
|
|
132
|
+
const stream = createReadStream(input.filePath);
|
|
133
|
+
try {
|
|
134
|
+
const response = await dependencies.fetch(endpoint, {
|
|
135
|
+
method: "PUT",
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: `OBS ${input.token.accessKeyId}:${signature}`,
|
|
138
|
+
Date: date,
|
|
139
|
+
"Content-Type": input.mediaType,
|
|
140
|
+
"Content-Length": String(file.size),
|
|
141
|
+
"x-obs-security-token": input.token.securityToken
|
|
142
|
+
},
|
|
143
|
+
body: stream,
|
|
144
|
+
signal: input.signal,
|
|
145
|
+
duplex: "half"
|
|
146
|
+
});
|
|
147
|
+
input.signal.throwIfAborted();
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
stream.destroy();
|
|
150
|
+
throw new ObjectStorageUploadError();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
stream.destroy();
|
|
155
|
+
if (input.signal.aborted)
|
|
156
|
+
throw input.signal.reason;
|
|
157
|
+
if (error instanceof ObjectStorageUploadError)
|
|
158
|
+
throw error;
|
|
159
|
+
throw new ObjectStorageUploadError();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function createObjectStorageUploader(options = {}) {
|
|
163
|
+
const dependencies = {
|
|
164
|
+
putOss: options.putOss ?? defaultPutOss,
|
|
165
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
166
|
+
sleep: options.sleep ?? sleepWithSignal,
|
|
167
|
+
now: options.now ?? (() => new Date())
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
async upload(input) {
|
|
171
|
+
input.signal.throwIfAborted();
|
|
172
|
+
const endpoints = endpointOrder(input.token);
|
|
173
|
+
if (endpoints.length === 0)
|
|
174
|
+
throw new ObjectStorageUploadError();
|
|
175
|
+
if (isHuaweiObs(endpoints[0])) {
|
|
176
|
+
await uploadObs({ ...input, endpoint: endpoints[0] }, dependencies);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const endpoint of endpoints) {
|
|
180
|
+
const base = { ...input, endpoint, authorizationV4: false };
|
|
181
|
+
const v1 = await attemptOss(base, dependencies);
|
|
182
|
+
if (v1 === "success")
|
|
183
|
+
return;
|
|
184
|
+
if (v1 !== "unsupported-authorization")
|
|
185
|
+
continue;
|
|
186
|
+
const v4 = await attemptOss({ ...base, authorizationV4: true }, dependencies);
|
|
187
|
+
if (v4 === "success")
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw new ObjectStorageUploadError();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class DamOperationFailedError extends Error {
|
|
2
|
+
operation;
|
|
3
|
+
constructor(operation) {
|
|
4
|
+
super("DAM 操作执行失败。");
|
|
5
|
+
this.name = "DamOperationFailedError";
|
|
6
|
+
this.operation = operation;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class DamOperationUnconfirmedError extends Error {
|
|
10
|
+
operation;
|
|
11
|
+
constructor(operation) {
|
|
12
|
+
super("DAM 操作已提交,但未能确认最终结果。");
|
|
13
|
+
this.name = "DamOperationUnconfirmedError";
|
|
14
|
+
this.operation = operation;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function copiedEntryId(resultIds) {
|
|
18
|
+
const unique = [...new Set((resultIds ?? []).filter((value) => value.trim() !== ""))];
|
|
19
|
+
return unique.length === 1 ? unique[0] : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function createDamOperationExecutor(dependencies) {
|
|
22
|
+
return {
|
|
23
|
+
async execute(input) {
|
|
24
|
+
input.signal.throwIfAborted();
|
|
25
|
+
const startedAt = dependencies.now();
|
|
26
|
+
const submission = await input.submit();
|
|
27
|
+
input.signal.throwIfAborted();
|
|
28
|
+
const taskId = submission.task_id;
|
|
29
|
+
if (taskId === undefined) {
|
|
30
|
+
const copied = input.operation === "copy"
|
|
31
|
+
? copiedEntryId(submission.result_ids)
|
|
32
|
+
: undefined;
|
|
33
|
+
return copied === undefined ? {} : { copied_entry_id: copied };
|
|
34
|
+
}
|
|
35
|
+
dependencies.annotate({ task_id: taskId });
|
|
36
|
+
let delayMilliseconds = 1_000;
|
|
37
|
+
const deadline = startedAt + 180_000;
|
|
38
|
+
while (true) {
|
|
39
|
+
input.signal.throwIfAborted();
|
|
40
|
+
if (dependencies.now() >= deadline) {
|
|
41
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
42
|
+
}
|
|
43
|
+
const task = await input.getTask(taskId, input.signal);
|
|
44
|
+
input.signal.throwIfAborted();
|
|
45
|
+
if (task === null)
|
|
46
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
47
|
+
if (task.status === 2) {
|
|
48
|
+
if (task.fail_count !== null && task.fail_count > 0) {
|
|
49
|
+
throw new DamOperationFailedError(input.operation);
|
|
50
|
+
}
|
|
51
|
+
if (input.operation !== "copy" || input.inspectResult === undefined)
|
|
52
|
+
return {};
|
|
53
|
+
const result = await input.inspectResult(taskId, input.signal);
|
|
54
|
+
input.signal.throwIfAborted();
|
|
55
|
+
const copied = copiedEntryId(result.result_ids);
|
|
56
|
+
return copied === undefined ? {} : { copied_entry_id: copied };
|
|
57
|
+
}
|
|
58
|
+
if (task.status === 3)
|
|
59
|
+
throw new DamOperationFailedError(input.operation);
|
|
60
|
+
if (task.status !== 0 && task.status !== 1) {
|
|
61
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
62
|
+
}
|
|
63
|
+
const remaining = deadline - dependencies.now();
|
|
64
|
+
if (remaining <= 0)
|
|
65
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
66
|
+
await dependencies.sleep(Math.min(delayMilliseconds, remaining), input.signal);
|
|
67
|
+
input.signal.throwIfAborted();
|
|
68
|
+
delayMilliseconds = Math.min(delayMilliseconds + 500, 3_000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|