gaoding-cli 1.0.0-alpha.5
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/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SECURITY.md +34 -0
- package/dist/bin/gd-cli.d.ts +2 -0
- package/dist/bin/gd-cli.js +10 -0
- package/dist/src/api/app-runner.d.ts +33 -0
- package/dist/src/api/app-runner.js +1177 -0
- package/dist/src/api/dam-client.d.ts +111 -0
- package/dist/src/api/dam-client.js +897 -0
- package/dist/src/api/direct-auth.d.ts +3 -0
- package/dist/src/api/direct-auth.js +11 -0
- package/dist/src/api/direct-client.d.ts +27 -0
- package/dist/src/api/direct-client.js +151 -0
- package/dist/src/api/direct-tools.d.ts +43 -0
- package/dist/src/api/direct-tools.js +1155 -0
- package/dist/src/api/hub-client.d.ts +20 -0
- package/dist/src/api/hub-client.js +99 -0
- package/dist/src/api/sso-client.d.ts +60 -0
- package/dist/src/api/sso-client.js +140 -0
- package/dist/src/api/task-poller.d.ts +38 -0
- package/dist/src/api/task-poller.js +72 -0
- package/dist/src/api/tool-invoker.d.ts +12 -0
- package/dist/src/api/tool-invoker.js +11 -0
- package/dist/src/api/usage-client.d.ts +39 -0
- package/dist/src/api/usage-client.js +179 -0
- package/dist/src/auth/credential-store-v2.d.ts +12 -0
- package/dist/src/auth/credential-store-v2.js +59 -0
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +191 -0
- package/dist/src/commands/dam.d.ts +2 -0
- package/dist/src/commands/dam.js +293 -0
- package/dist/src/commands/models.d.ts +2 -0
- package/dist/src/commands/models.js +78 -0
- package/dist/src/commands/org.d.ts +2 -0
- package/dist/src/commands/org.js +137 -0
- package/dist/src/commands/skills.d.ts +2 -0
- package/dist/src/commands/skills.js +213 -0
- package/dist/src/commands/tools.d.ts +4 -0
- package/dist/src/commands/tools.js +383 -0
- package/dist/src/commands/update.d.ts +2 -0
- package/dist/src/commands/update.js +167 -0
- package/dist/src/commands/usage.d.ts +7 -0
- package/dist/src/commands/usage.js +43 -0
- package/dist/src/commands/workflows.d.ts +3 -0
- package/dist/src/commands/workflows.js +398 -0
- package/dist/src/core/auth/auth-provider.d.ts +8 -0
- package/dist/src/core/auth/auth-provider.js +12 -0
- package/dist/src/core/auth/device-oauth-provider.d.ts +12 -0
- package/dist/src/core/auth/device-oauth-provider.js +124 -0
- package/dist/src/core/auth/org-manager.d.ts +26 -0
- package/dist/src/core/auth/org-manager.js +224 -0
- package/dist/src/core/auth/signature.d.ts +19 -0
- package/dist/src/core/auth/signature.js +33 -0
- package/dist/src/core/auth/types.d.ts +52 -0
- package/dist/src/core/auth/types.js +3 -0
- package/dist/src/core/output/cli-error.d.ts +5 -0
- package/dist/src/core/output/cli-error.js +8 -0
- package/dist/src/core/output/detector.d.ts +5 -0
- package/dist/src/core/output/detector.js +14 -0
- package/dist/src/core/output/engine.d.ts +32 -0
- package/dist/src/core/output/engine.js +77 -0
- package/dist/src/core/output/envelope.d.ts +40 -0
- package/dist/src/core/output/envelope.js +119 -0
- package/dist/src/core/output/index.d.ts +4 -0
- package/dist/src/core/output/index.js +4 -0
- package/dist/src/core/output/live-display.d.ts +15 -0
- package/dist/src/core/output/live-display.js +111 -0
- package/dist/src/core/output/result-card.d.ts +18 -0
- package/dist/src/core/output/result-card.js +39 -0
- package/dist/src/core/prompt/index.d.ts +31 -0
- package/dist/src/core/prompt/index.js +36 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +31 -0
- package/dist/src/middleware/auth.d.ts +3 -0
- package/dist/src/middleware/auth.js +74 -0
- package/dist/src/middleware/error-handler.d.ts +3 -0
- package/dist/src/middleware/error-handler.js +431 -0
- package/dist/src/middleware/interactive-params.d.ts +14 -0
- package/dist/src/middleware/interactive-params.js +53 -0
- package/dist/src/middleware/output.d.ts +14 -0
- package/dist/src/middleware/output.js +546 -0
- package/dist/src/models/model-catalog.d.ts +26 -0
- package/dist/src/models/model-catalog.js +216 -0
- package/dist/src/program.d.ts +2 -0
- package/dist/src/program.js +152 -0
- package/dist/src/registry/agent-contracts.d.ts +43 -0
- package/dist/src/registry/agent-contracts.js +483 -0
- package/dist/src/registry/app-loader.d.ts +2 -0
- package/dist/src/registry/app-loader.js +70 -0
- package/dist/src/registry/direct-registry.d.ts +11 -0
- package/dist/src/registry/direct-registry.js +234 -0
- package/dist/src/registry/registry-cache.d.ts +10 -0
- package/dist/src/registry/registry-cache.js +52 -0
- package/dist/src/registry/registry-client.d.ts +28 -0
- package/dist/src/registry/registry-client.js +50 -0
- package/dist/src/registry/types.d.ts +69 -0
- package/dist/src/registry/types.js +1 -0
- package/dist/src/tty/auth-presenter.d.ts +3 -0
- package/dist/src/tty/auth-presenter.js +21 -0
- package/dist/src/update/install-source.d.ts +19 -0
- package/dist/src/update/install-source.js +139 -0
- package/dist/src/update/notifier.d.ts +2 -0
- package/dist/src/update/notifier.js +59 -0
- package/dist/src/update/update-checker.d.ts +22 -0
- package/dist/src/update/update-checker.js +110 -0
- package/dist/src/update/version.d.ts +9 -0
- package/dist/src/update/version.js +52 -0
- package/dist/src/utils/agent-skill-installer.d.ts +57 -0
- package/dist/src/utils/agent-skill-installer.js +389 -0
- package/dist/src/utils/capability-metadata.d.ts +18 -0
- package/dist/src/utils/capability-metadata.js +88 -0
- package/dist/src/utils/capability-presenter.d.ts +4 -0
- package/dist/src/utils/capability-presenter.js +190 -0
- package/dist/src/utils/config.d.ts +16 -0
- package/dist/src/utils/config.js +54 -0
- package/dist/src/utils/credential-permissions.d.ts +2 -0
- package/dist/src/utils/credential-permissions.js +22 -0
- package/dist/src/utils/ecommerce-input-guidance.d.ts +15 -0
- package/dist/src/utils/ecommerce-input-guidance.js +85 -0
- package/dist/src/utils/endpoints.d.ts +52 -0
- package/dist/src/utils/endpoints.js +258 -0
- package/dist/src/utils/enrich-upload-params.d.ts +6 -0
- package/dist/src/utils/enrich-upload-params.js +11 -0
- package/dist/src/utils/entitlement.d.ts +17 -0
- package/dist/src/utils/entitlement.js +146 -0
- package/dist/src/utils/gd-home.d.ts +7 -0
- package/dist/src/utils/gd-home.js +26 -0
- package/dist/src/utils/help-verify.d.ts +9 -0
- package/dist/src/utils/help-verify.js +41 -0
- package/dist/src/utils/input-json.d.ts +3 -0
- package/dist/src/utils/input-json.js +61 -0
- package/dist/src/utils/list-output-format.d.ts +10 -0
- package/dist/src/utils/list-output-format.js +34 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +19 -0
- package/dist/src/utils/matting-compose.d.ts +10 -0
- package/dist/src/utils/matting-compose.js +45 -0
- package/dist/src/utils/open-browser.d.ts +1 -0
- package/dist/src/utils/open-browser.js +4 -0
- package/dist/src/utils/output-path.d.ts +5 -0
- package/dist/src/utils/output-path.js +70 -0
- package/dist/src/utils/redact.d.ts +3 -0
- package/dist/src/utils/redact.js +96 -0
- package/dist/src/utils/reference-image.d.ts +8 -0
- package/dist/src/utils/reference-image.js +43 -0
- package/dist/src/utils/resolve-app-local-images.d.ts +8 -0
- package/dist/src/utils/resolve-app-local-images.js +84 -0
- package/dist/src/utils/sensitive-path.d.ts +6 -0
- package/dist/src/utils/sensitive-path.js +69 -0
- package/dist/src/utils/settings-store.d.ts +42 -0
- package/dist/src/utils/settings-store.js +50 -0
- package/dist/src/utils/sleep.d.ts +1 -0
- package/dist/src/utils/sleep.js +3 -0
- package/dist/src/utils/spinner.d.ts +2 -0
- package/dist/src/utils/spinner.js +4 -0
- package/dist/src/utils/static-help.d.ts +2 -0
- package/dist/src/utils/static-help.js +3 -0
- package/dist/src/utils/tool-display.d.ts +10 -0
- package/dist/src/utils/tool-display.js +55 -0
- package/dist/src/utils/transient-network-error.d.ts +4 -0
- package/dist/src/utils/transient-network-error.js +40 -0
- package/dist/src/utils/url-safety.d.ts +6 -0
- package/dist/src/utils/url-safety.js +67 -0
- package/dist/src/utils/write-app-deliverables.d.ts +18 -0
- package/dist/src/utils/write-app-deliverables.js +386 -0
- package/package.json +91 -0
- package/skills/gd-cli/SKILL.md +57 -0
- package/skills/gd-cli/references/auth.md +31 -0
- package/skills/gd-cli/references/deliverables.md +22 -0
- package/skills/gd-cli/references/entitlement.md +21 -0
- package/skills/gd-cli/references/errors.md +19 -0
- package/skills/gd-cli/references/org.md +18 -0
- package/skills/gd-cli/references/sop.md +25 -0
- package/skills/gd-cli/references/tools.md +29 -0
- package/skills/gd-cli/references/update.md +16 -0
- package/skills/gd-cli/references/workflows.md +22 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, resolve } from "node:path";
|
|
4
|
+
import OSS from "ali-oss";
|
|
5
|
+
import sharp from "sharp";
|
|
6
|
+
import { DirectApiClient, DirectApiError, unwrapEnvelope } from "./direct-client.js";
|
|
7
|
+
import { isDebug, isVerbose } from "../utils/config.js";
|
|
8
|
+
import { assertSafeUploadPath } from "../utils/sensitive-path.js";
|
|
9
|
+
import { assertSafeHttpUrl } from "../utils/url-safety.js";
|
|
10
|
+
import { redactSensitiveText } from "../utils/redact.js";
|
|
11
|
+
const ANALYSIS_STATUS = {
|
|
12
|
+
NOT_START: 0,
|
|
13
|
+
PROCESSING: 1,
|
|
14
|
+
SUCCESS: 2,
|
|
15
|
+
FAILED: 3,
|
|
16
|
+
NOT_SUPPORT: 4,
|
|
17
|
+
};
|
|
18
|
+
const DEFAULT_ANALYSIS_TIMEOUT_MS = 180_000;
|
|
19
|
+
const DEFAULT_ANALYSIS_INTERVAL_MS = 3_000;
|
|
20
|
+
export class DamClient {
|
|
21
|
+
client;
|
|
22
|
+
constructor(client = new DirectApiClient()) {
|
|
23
|
+
this.client = client;
|
|
24
|
+
}
|
|
25
|
+
async createAssetId() {
|
|
26
|
+
const response = await this.client.post("/tb-dam/asset/id");
|
|
27
|
+
const data = unwrapEnvelope(response);
|
|
28
|
+
if (!data.id) {
|
|
29
|
+
throw new Error("创建 DAM content_id 失败:接口没有返回 id");
|
|
30
|
+
}
|
|
31
|
+
return data.id;
|
|
32
|
+
}
|
|
33
|
+
async uploadLocalAsset(file, options = {}) {
|
|
34
|
+
const filePath = assertSafeUploadPath(resolve(process.cwd(), file), {
|
|
35
|
+
allowSensitivePath: options.allowSensitivePath,
|
|
36
|
+
context: "上传文件",
|
|
37
|
+
});
|
|
38
|
+
await access(filePath);
|
|
39
|
+
const fileBuffer = await readFile(filePath);
|
|
40
|
+
const format = inferFormat(filePath, fileBuffer);
|
|
41
|
+
const mimeType = inferMimeType(format);
|
|
42
|
+
const fileStat = await stat(filePath);
|
|
43
|
+
const contentId = options.register ? await this.createAssetId() : undefined;
|
|
44
|
+
const token = await this.createUploadToken(format, contentId);
|
|
45
|
+
await putObjectWithFallback(token, filePath, mimeType);
|
|
46
|
+
const rawUrl = `${normalizeHost(token.host)}/${token.object_name}`;
|
|
47
|
+
const storageId = contentId ?? token.object_name;
|
|
48
|
+
const uploaded = {
|
|
49
|
+
id: storageId,
|
|
50
|
+
storage_id: storageId,
|
|
51
|
+
file_path: filePath,
|
|
52
|
+
filename: basename(filePath),
|
|
53
|
+
format,
|
|
54
|
+
mime_type: mimeType,
|
|
55
|
+
object_name: token.object_name,
|
|
56
|
+
auth_key: token.auth_key,
|
|
57
|
+
url: appendAuthKey(rawUrl, token.auth_key),
|
|
58
|
+
source: "cli",
|
|
59
|
+
};
|
|
60
|
+
if (!options.register)
|
|
61
|
+
return uploaded;
|
|
62
|
+
if (!contentId) {
|
|
63
|
+
throw new Error("DAM 入库失败:缺少 content_id");
|
|
64
|
+
}
|
|
65
|
+
const repositoryId = options.repositoryId ?? (await this.resolveDefaultRepositoryId());
|
|
66
|
+
if (!repositoryId) {
|
|
67
|
+
throw new Error(`DAM 入库失败:缺少 repository_id。对象已上传到存储,storage_id=${contentId}, object_name=${token.object_name}`);
|
|
68
|
+
}
|
|
69
|
+
const preview = await readPreviewInfo(filePath, mimeType, uploaded.url, format);
|
|
70
|
+
const registration = await this.registerAsset({
|
|
71
|
+
contentId,
|
|
72
|
+
repositoryId,
|
|
73
|
+
folderId: options.folderId ?? "0",
|
|
74
|
+
filePath,
|
|
75
|
+
filename: basename(filePath),
|
|
76
|
+
title: options.title,
|
|
77
|
+
mimeType,
|
|
78
|
+
format,
|
|
79
|
+
url: uploaded.url,
|
|
80
|
+
size: fileStat.size,
|
|
81
|
+
md5: hash("md5", fileBuffer),
|
|
82
|
+
sha1: hash("sha1", fileBuffer),
|
|
83
|
+
tagIds: options.tagIds,
|
|
84
|
+
preview,
|
|
85
|
+
});
|
|
86
|
+
const registeredAssetId = extractRegisteredAssetId(registration) ?? contentId;
|
|
87
|
+
const analysis = options.waitAnalysis
|
|
88
|
+
? await this.waitForAssetAnalysis(registeredAssetId, normalizePositiveInteger(options.analysisTimeoutMs, DEFAULT_ANALYSIS_TIMEOUT_MS), normalizePositiveInteger(options.analysisIntervalMs, DEFAULT_ANALYSIS_INTERVAL_MS))
|
|
89
|
+
: undefined;
|
|
90
|
+
const assetInfo = analysis?.assetInfo ?? extractAssetInfo(registration);
|
|
91
|
+
const cover = coverFromAssetInfo(assetInfo, preview);
|
|
92
|
+
return {
|
|
93
|
+
...uploaded,
|
|
94
|
+
id: registeredAssetId,
|
|
95
|
+
registered_asset_id: registeredAssetId,
|
|
96
|
+
registered: true,
|
|
97
|
+
registration,
|
|
98
|
+
cover_url: cover?.url,
|
|
99
|
+
cover,
|
|
100
|
+
asset_info: assetInfo,
|
|
101
|
+
analysis_status: extractAnalysisStatus(assetInfo),
|
|
102
|
+
analysis_fail_msg: extractString(assetInfo, ["analysis_fail_msg", "analysis_error", "message"]),
|
|
103
|
+
analysis_timed_out: analysis?.timedOut,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async getAsset(idOrUrl) {
|
|
107
|
+
if (/^https?:\/\//.test(idOrUrl)) {
|
|
108
|
+
const safeUrl = assertSafeHttpUrl(idOrUrl, { context: "DAM URL" });
|
|
109
|
+
return {
|
|
110
|
+
status: "unsupported_api",
|
|
111
|
+
message: "DAM 按 URL 查询接口尚未确认,暂不把 URL 查询视为已实现能力",
|
|
112
|
+
query: safeUrl,
|
|
113
|
+
expected_contract: "GET /tb-dam/dam/asset/by-url?url=... 或 POST /tb-dam/asset/search 支持 origin_url 精确查询",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return this.client.get(`/dam/asset/${encodeURIComponent(idOrUrl)}`);
|
|
117
|
+
}
|
|
118
|
+
async deleteAsset(assetId, options = {}) {
|
|
119
|
+
const repositoryId = options.repositoryId ?? (await this.resolveDefaultRepositoryId());
|
|
120
|
+
if (!repositoryId) {
|
|
121
|
+
throw new Error("DAM 删除失败:缺少 repository_id,请传 --repository-id 或配置 GD_DAM_REPOSITORY_ID");
|
|
122
|
+
}
|
|
123
|
+
const body = {
|
|
124
|
+
repository_id: repositoryId,
|
|
125
|
+
file_ids: [assetId],
|
|
126
|
+
folder_ids: [],
|
|
127
|
+
};
|
|
128
|
+
const recycle = await this.client.post("/dam/recycle-bin", body);
|
|
129
|
+
if (!options.permanent)
|
|
130
|
+
return recycle;
|
|
131
|
+
return this.client.post("/dam/recycle-bin/delete", body);
|
|
132
|
+
}
|
|
133
|
+
async searchAssets(keyword, options = {}) {
|
|
134
|
+
const query = keyword.trim();
|
|
135
|
+
if (!query && !options.allowEmptyKeyword && !options.folderId) {
|
|
136
|
+
throw new Error("DAM 搜索关键词不能为空;如需查看最近素材请使用 gd-cli dam list");
|
|
137
|
+
}
|
|
138
|
+
const repositoryIds = await this.resolveSearchRepositoryIds(options);
|
|
139
|
+
const payload = buildSearchPayload(query, repositoryIds, options);
|
|
140
|
+
try {
|
|
141
|
+
const response = await this.client.post("/dam/asset/v2/search/simple", payload);
|
|
142
|
+
return normalizeSearchResponse(response, {
|
|
143
|
+
endpoint: "/dam/asset/v2/search/simple",
|
|
144
|
+
query,
|
|
145
|
+
repositoryIds,
|
|
146
|
+
payload,
|
|
147
|
+
includeDebug: options.raw === true || isVerbose() || isDebug(),
|
|
148
|
+
includeRaw: options.raw === true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!shouldFallbackDamSearch(error))
|
|
153
|
+
throw error;
|
|
154
|
+
const response = await this.client.post("/dam/asset/search", payload);
|
|
155
|
+
return normalizeSearchResponse(response, {
|
|
156
|
+
endpoint: "/dam/asset/search",
|
|
157
|
+
query,
|
|
158
|
+
repositoryIds,
|
|
159
|
+
payload,
|
|
160
|
+
includeDebug: options.raw === true || isVerbose() || isDebug(),
|
|
161
|
+
includeRaw: options.raw === true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async resolveDefaultRepositoryId() {
|
|
166
|
+
const explicit = process.env.GD_DAM_REPOSITORY_ID?.trim();
|
|
167
|
+
if (explicit)
|
|
168
|
+
return explicit;
|
|
169
|
+
const repositories = await this.listRepositories();
|
|
170
|
+
return repositories.find((repo) => repo.type === 1 && repo.repository_id)?.repository_id ?? repositories.find((repo) => repo.repository_id)?.repository_id;
|
|
171
|
+
}
|
|
172
|
+
async resolveSearchRepositoryIds(options) {
|
|
173
|
+
if (options.repositoryId)
|
|
174
|
+
return [options.repositoryId];
|
|
175
|
+
if (options.allRepositories) {
|
|
176
|
+
const ids = (await this.listRepositories())
|
|
177
|
+
.map((repo) => repo.repository_id)
|
|
178
|
+
.filter((id) => Boolean(id));
|
|
179
|
+
if (ids.length === 0) {
|
|
180
|
+
throw new Error("DAM 搜索失败:当前组织没有可访问的资源库");
|
|
181
|
+
}
|
|
182
|
+
return ids;
|
|
183
|
+
}
|
|
184
|
+
const repositoryId = await this.resolveDefaultRepositoryId();
|
|
185
|
+
if (!repositoryId) {
|
|
186
|
+
throw new Error("DAM 搜索失败:缺少 repository_id,请传 --repository-id 或配置 GD_DAM_REPOSITORY_ID");
|
|
187
|
+
}
|
|
188
|
+
return [repositoryId];
|
|
189
|
+
}
|
|
190
|
+
async listRepositories() {
|
|
191
|
+
const response = await this.client.get("/tb-dam/repositories");
|
|
192
|
+
const repositories = unwrapEnvelope(response);
|
|
193
|
+
return Array.isArray(repositories) ? repositories : [];
|
|
194
|
+
}
|
|
195
|
+
async registerAsset(input) {
|
|
196
|
+
const payload = {
|
|
197
|
+
folder_id: input.folderId,
|
|
198
|
+
repository_id: input.repositoryId,
|
|
199
|
+
second_transmission: false,
|
|
200
|
+
storage_format: toDamStorageFormat(input.format),
|
|
201
|
+
category_ids: [],
|
|
202
|
+
asset_info: {
|
|
203
|
+
id: input.contentId,
|
|
204
|
+
origin_url: input.url,
|
|
205
|
+
size: input.size,
|
|
206
|
+
title: input.title ?? stripExtension(input.filename),
|
|
207
|
+
md5: input.md5,
|
|
208
|
+
sha1: input.sha1,
|
|
209
|
+
mime_type: input.mimeType,
|
|
210
|
+
format: input.format,
|
|
211
|
+
preview: input.preview ?? {},
|
|
212
|
+
},
|
|
213
|
+
...(input.tagIds?.length ? { tag_ids: input.tagIds } : {}),
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
return await this.client.post("/dam/asset/createMaterial", payload);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const existing = await this.findExistingAsset(input.repositoryId, stripExtension(input.filename));
|
|
220
|
+
if (existing)
|
|
221
|
+
return existing;
|
|
222
|
+
throw new Error(`DAM 入库失败。对象已上传到存储,storage_id=${input.contentId}。` +
|
|
223
|
+
` /dam/asset/createMaterial: ${errorMessage(error)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async waitForAssetAnalysis(assetId, timeoutMs, intervalMs) {
|
|
227
|
+
const deadline = Date.now() + timeoutMs;
|
|
228
|
+
let lastAssetInfo;
|
|
229
|
+
while (Date.now() <= deadline) {
|
|
230
|
+
const assetInfo = await this.fetchAnalysisAsset(assetId);
|
|
231
|
+
if (assetInfo) {
|
|
232
|
+
lastAssetInfo = assetInfo;
|
|
233
|
+
const status = extractAnalysisStatus(assetInfo);
|
|
234
|
+
if (isTerminalAnalysisStatus(status)) {
|
|
235
|
+
return { assetInfo };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
239
|
+
}
|
|
240
|
+
return { assetInfo: lastAssetInfo, timedOut: true };
|
|
241
|
+
}
|
|
242
|
+
async fetchAnalysisAsset(assetId) {
|
|
243
|
+
const response = await this.client.get("/dam/asset/batch-status", {
|
|
244
|
+
query: { ids: assetId },
|
|
245
|
+
});
|
|
246
|
+
return firstRecordFromResponse(response);
|
|
247
|
+
}
|
|
248
|
+
async findExistingAsset(repositoryId, title) {
|
|
249
|
+
const response = await this.client.post("/dam/asset/v2/search/simple", {
|
|
250
|
+
repository_ids: [repositoryId],
|
|
251
|
+
keyword: title,
|
|
252
|
+
page: 1,
|
|
253
|
+
page_size: 10,
|
|
254
|
+
});
|
|
255
|
+
const raw = response;
|
|
256
|
+
const result = (Array.isArray(raw) ? raw : raw.data ? raw : raw.data);
|
|
257
|
+
const items = Array.isArray(result) ? result : result?.data;
|
|
258
|
+
return items?.find((item) => item.title === title) ?? items?.[0];
|
|
259
|
+
}
|
|
260
|
+
async createUploadToken(format, contentId) {
|
|
261
|
+
const response = await this.client.post("/tb-dam/asset/upload/tokens", contentId ? { content_id: contentId, format } : { format });
|
|
262
|
+
return unwrapEnvelope(response);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function buildSearchPayload(keyword, repositoryIds, options) {
|
|
266
|
+
const pageSize = clampPositiveInteger(options.pageSize, 20, 100);
|
|
267
|
+
const page = clampPositiveInteger(options.page, 1, Number.MAX_SAFE_INTEGER);
|
|
268
|
+
const payload = {
|
|
269
|
+
keyword,
|
|
270
|
+
repository_ids: repositoryIds,
|
|
271
|
+
folder_ids: options.folderId ? [options.folderId] : [],
|
|
272
|
+
include_child_folder: options.includeChildFolder !== false,
|
|
273
|
+
recycle: false,
|
|
274
|
+
page_size: pageSize,
|
|
275
|
+
page_num: page,
|
|
276
|
+
page,
|
|
277
|
+
sort: ["-top_time", "-updated_at"],
|
|
278
|
+
};
|
|
279
|
+
if (options.cursor) {
|
|
280
|
+
payload.query_id = options.cursor;
|
|
281
|
+
payload.cursor = options.cursor;
|
|
282
|
+
}
|
|
283
|
+
const contentFormats = contentFormatsForType(options.type);
|
|
284
|
+
if (contentFormats.length)
|
|
285
|
+
payload.content_format_list = contentFormats;
|
|
286
|
+
if (options.format?.length)
|
|
287
|
+
payload.format_list = options.format;
|
|
288
|
+
if (options.tagIds?.length)
|
|
289
|
+
payload.tag_ids = options.tagIds;
|
|
290
|
+
return payload;
|
|
291
|
+
}
|
|
292
|
+
function normalizeSearchResponse(response, context) {
|
|
293
|
+
const container = extractSearchContainer(response);
|
|
294
|
+
const rawItems = container.items;
|
|
295
|
+
const page = numberFromUnknown(context.payload.page_num) ?? 1;
|
|
296
|
+
const pageSize = numberFromUnknown(context.payload.page_size) ?? 20;
|
|
297
|
+
const total = container.total ?? rawItems.length;
|
|
298
|
+
const finish = container.finish ?? page * pageSize >= total;
|
|
299
|
+
const nextCursor = container.queryId && !finish ? container.queryId : undefined;
|
|
300
|
+
return {
|
|
301
|
+
status: "completed",
|
|
302
|
+
query: context.query,
|
|
303
|
+
repository_ids: context.repositoryIds,
|
|
304
|
+
page,
|
|
305
|
+
page_size: pageSize,
|
|
306
|
+
total,
|
|
307
|
+
finish,
|
|
308
|
+
next_cursor: nextCursor,
|
|
309
|
+
assets: rawItems.map(normalizeSearchAsset).filter((item) => item.asset_id),
|
|
310
|
+
...(context.includeDebug
|
|
311
|
+
? {
|
|
312
|
+
debug: {
|
|
313
|
+
source: {
|
|
314
|
+
endpoint: context.endpoint,
|
|
315
|
+
...(context.includeRaw ? { raw: response } : {}),
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function extractSearchContainer(value) {
|
|
323
|
+
const candidates = nestedCandidates(value);
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
if (Array.isArray(candidate)) {
|
|
326
|
+
return { items: candidate.filter(isRecord) };
|
|
327
|
+
}
|
|
328
|
+
if (!isRecord(candidate))
|
|
329
|
+
continue;
|
|
330
|
+
const items = arrayField(candidate, "data") ?? arrayField(candidate, "list") ?? arrayField(candidate, "items");
|
|
331
|
+
if (items) {
|
|
332
|
+
return {
|
|
333
|
+
items: items.filter(isRecord),
|
|
334
|
+
total: numberFromUnknown(candidate.total) ?? numberFromUnknown(candidate.count),
|
|
335
|
+
finish: booleanFromUnknown(candidate.finish),
|
|
336
|
+
queryId: stringFromUnknown(candidate.query_id) ?? stringFromUnknown(candidate.cursor),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return { items: [] };
|
|
341
|
+
}
|
|
342
|
+
function nestedCandidates(value) {
|
|
343
|
+
const out = [value];
|
|
344
|
+
let current = value;
|
|
345
|
+
for (let i = 0; i < 3; i += 1) {
|
|
346
|
+
if (!isRecord(current) || current.data === undefined)
|
|
347
|
+
break;
|
|
348
|
+
current = current.data;
|
|
349
|
+
out.push(current);
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
function normalizeSearchAsset(item) {
|
|
354
|
+
const originInfo = recordField(item, "origin_info") ?? recordField(item, "originInfo");
|
|
355
|
+
const preview = recordField(item, "preview") ?? recordField(item, "preview_info") ?? recordField(item, "previewInfo");
|
|
356
|
+
const folderInfo = recordField(item, "folder_info") ?? recordField(item, "folderInfo");
|
|
357
|
+
const format = stringFromUnknown(originInfo?.format) ??
|
|
358
|
+
stringFromUnknown(item.format) ??
|
|
359
|
+
stringFromUnknown(item.storage_format);
|
|
360
|
+
const url = stringFromUnknown(originInfo?.url) ??
|
|
361
|
+
stringFromUnknown(item.origin_url) ??
|
|
362
|
+
stringFromUnknown(item.url);
|
|
363
|
+
const coverUrl = stringFromUnknown(preview?.url) ??
|
|
364
|
+
stringFromUnknown(item.cover_url) ??
|
|
365
|
+
stringFromUnknown(item.preview_url);
|
|
366
|
+
const assetId = stringFromUnknown(item.asset_id) ??
|
|
367
|
+
stringFromUnknown(item.id) ??
|
|
368
|
+
stringFromUnknown(item.content_id) ??
|
|
369
|
+
"";
|
|
370
|
+
return removeUndefined({
|
|
371
|
+
asset_id: assetId,
|
|
372
|
+
title: stringFromUnknown(item.title) ?? stringFromUnknown(item.name),
|
|
373
|
+
format,
|
|
374
|
+
type: inferAssetType(item, format),
|
|
375
|
+
size: numberFromUnknown(originInfo?.size) ?? numberFromUnknown(item.size),
|
|
376
|
+
url,
|
|
377
|
+
cover_url: coverUrl,
|
|
378
|
+
width: numberFromUnknown(preview?.width),
|
|
379
|
+
height: numberFromUnknown(preview?.height),
|
|
380
|
+
repository_id: stringFromUnknown(item.repository_id),
|
|
381
|
+
folder_id: stringFromUnknown(item.folder_id) ?? stringFromUnknown(folderInfo?.id),
|
|
382
|
+
folder_name: stringFromUnknown(folderInfo?.title) ?? stringFromUnknown(folderInfo?.name),
|
|
383
|
+
tags: stringArrayFromTags(item.tags),
|
|
384
|
+
tag_ids: idArrayFromTags(item.tags) ?? idArrayFromUnknown(item.tag_ids),
|
|
385
|
+
analysis_status: numberFromUnknown(item.analysis_result) ??
|
|
386
|
+
numberFromUnknown(item.analysis_status) ??
|
|
387
|
+
stringFromUnknown(item.analysis_result) ??
|
|
388
|
+
stringFromUnknown(item.analysis_status),
|
|
389
|
+
created_at: dateStringFromUnknown(item.created_at ?? item.create_time ?? item.created_time),
|
|
390
|
+
updated_at: dateStringFromUnknown(item.updated_at ?? item.update_time ?? item.updated_time),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function shouldFallbackDamSearch(error) {
|
|
394
|
+
if (error instanceof DirectApiError) {
|
|
395
|
+
return [404, 405, 501].includes(error.statusCode);
|
|
396
|
+
}
|
|
397
|
+
return /not found|unsupported|method not allowed/i.test(errorMessage(error));
|
|
398
|
+
}
|
|
399
|
+
function contentFormatsForType(type) {
|
|
400
|
+
const normalized = type?.trim().toLowerCase();
|
|
401
|
+
if (!normalized)
|
|
402
|
+
return [];
|
|
403
|
+
const map = {
|
|
404
|
+
image: ["gdpic", "gdimage"],
|
|
405
|
+
video: ["gdvideo"],
|
|
406
|
+
audio: ["gdaudio"],
|
|
407
|
+
font: ["gdfont"],
|
|
408
|
+
template: ["gdtemplate"],
|
|
409
|
+
file: ["gdfile", "gdimage", "gdslide"],
|
|
410
|
+
};
|
|
411
|
+
return map[normalized] ?? [normalized];
|
|
412
|
+
}
|
|
413
|
+
function inferAssetType(item, format) {
|
|
414
|
+
const contentFormat = stringFromUnknown(item.content_format) ??
|
|
415
|
+
stringFromUnknown(item.storage_format) ??
|
|
416
|
+
stringFromUnknown(item.type);
|
|
417
|
+
const normalized = contentFormat?.toLowerCase();
|
|
418
|
+
if (normalized?.includes("video"))
|
|
419
|
+
return "video";
|
|
420
|
+
if (normalized?.includes("audio"))
|
|
421
|
+
return "audio";
|
|
422
|
+
if (normalized?.includes("font"))
|
|
423
|
+
return "font";
|
|
424
|
+
if (normalized?.includes("template"))
|
|
425
|
+
return "template";
|
|
426
|
+
if (normalized?.includes("pic") || normalized?.includes("image"))
|
|
427
|
+
return "image";
|
|
428
|
+
const ext = format?.toLowerCase();
|
|
429
|
+
if (!ext)
|
|
430
|
+
return undefined;
|
|
431
|
+
if (["png", "jpg", "jpeg", "gif", "webp", "svg", "heic", "avif", "bmp"].includes(ext))
|
|
432
|
+
return "image";
|
|
433
|
+
if (["mp4", "mov", "webm", "avi", "mkv"].includes(ext))
|
|
434
|
+
return "video";
|
|
435
|
+
if (["mp3", "wav", "ogg", "flac"].includes(ext))
|
|
436
|
+
return "audio";
|
|
437
|
+
if (["ttf", "otf"].includes(ext))
|
|
438
|
+
return "font";
|
|
439
|
+
return "file";
|
|
440
|
+
}
|
|
441
|
+
function isRecord(value) {
|
|
442
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
443
|
+
}
|
|
444
|
+
function recordField(record, key) {
|
|
445
|
+
const value = record[key];
|
|
446
|
+
return isRecord(value) ? value : undefined;
|
|
447
|
+
}
|
|
448
|
+
function arrayField(record, key) {
|
|
449
|
+
const value = record[key];
|
|
450
|
+
return Array.isArray(value) ? value : undefined;
|
|
451
|
+
}
|
|
452
|
+
function stringFromUnknown(value) {
|
|
453
|
+
if (typeof value === "string" && value.trim())
|
|
454
|
+
return value.trim();
|
|
455
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
456
|
+
return String(value);
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
function numberFromUnknown(value) {
|
|
460
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
461
|
+
return value;
|
|
462
|
+
if (typeof value === "string" && value.trim()) {
|
|
463
|
+
const parsed = Number(value);
|
|
464
|
+
if (Number.isFinite(parsed))
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
function booleanFromUnknown(value) {
|
|
470
|
+
if (typeof value === "boolean")
|
|
471
|
+
return value;
|
|
472
|
+
if (value === "true")
|
|
473
|
+
return true;
|
|
474
|
+
if (value === "false")
|
|
475
|
+
return false;
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
function stringArrayFromTags(value) {
|
|
479
|
+
if (!Array.isArray(value))
|
|
480
|
+
return undefined;
|
|
481
|
+
const out = value
|
|
482
|
+
.map((item) => isRecord(item)
|
|
483
|
+
? stringFromUnknown(item.title) ?? stringFromUnknown(item.name)
|
|
484
|
+
: stringFromUnknown(item))
|
|
485
|
+
.filter((item) => Boolean(item));
|
|
486
|
+
return out.length ? out : undefined;
|
|
487
|
+
}
|
|
488
|
+
function idArrayFromTags(value) {
|
|
489
|
+
if (!Array.isArray(value))
|
|
490
|
+
return undefined;
|
|
491
|
+
const out = value
|
|
492
|
+
.map((item) => (isRecord(item) ? item.id ?? item.tag_id : undefined))
|
|
493
|
+
.filter((item) => typeof item === "string" || typeof item === "number");
|
|
494
|
+
return out.length ? out : undefined;
|
|
495
|
+
}
|
|
496
|
+
function idArrayFromUnknown(value) {
|
|
497
|
+
if (!Array.isArray(value))
|
|
498
|
+
return undefined;
|
|
499
|
+
const out = value.filter((item) => typeof item === "string" || typeof item === "number");
|
|
500
|
+
return out.length ? out : undefined;
|
|
501
|
+
}
|
|
502
|
+
function dateStringFromUnknown(value) {
|
|
503
|
+
if (typeof value === "string" && value.trim())
|
|
504
|
+
return value.trim();
|
|
505
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
506
|
+
return undefined;
|
|
507
|
+
const millis = value < 10_000_000_000 ? value * 1000 : value;
|
|
508
|
+
const date = new Date(millis);
|
|
509
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
510
|
+
}
|
|
511
|
+
function removeUndefined(record) {
|
|
512
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
513
|
+
}
|
|
514
|
+
function clampPositiveInteger(value, fallback, max) {
|
|
515
|
+
if (!Number.isInteger(value) || value === undefined || value <= 0)
|
|
516
|
+
return fallback;
|
|
517
|
+
return Math.min(value, max);
|
|
518
|
+
}
|
|
519
|
+
async function putObjectWithFallback(token, filePath, mimeType) {
|
|
520
|
+
let lastError;
|
|
521
|
+
for (const endpoint of resolveUploadEndpoints(token)) {
|
|
522
|
+
for (const authorizationV4 of [false, true]) {
|
|
523
|
+
try {
|
|
524
|
+
await putObjectWithRetry(token, endpoint, filePath, mimeType, authorizationV4);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
lastError = error;
|
|
529
|
+
if (!shouldTryNextOssAuthOrEndpoint(error))
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
535
|
+
}
|
|
536
|
+
async function putObject(token, endpoint, filePath, mimeType, authorizationV4) {
|
|
537
|
+
if (isHuaweiObsEndpoint(endpoint)) {
|
|
538
|
+
await putObsObject(token, endpoint, filePath, mimeType);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const oss = new OSS({
|
|
542
|
+
region: token.region,
|
|
543
|
+
bucket: token.bucket_name,
|
|
544
|
+
accessKeyId: token.access_key_id,
|
|
545
|
+
accessKeySecret: token.access_key_secret,
|
|
546
|
+
stsToken: token.security_token,
|
|
547
|
+
endpoint,
|
|
548
|
+
secure: true,
|
|
549
|
+
authorizationV4,
|
|
550
|
+
refreshSTSToken: async () => ({
|
|
551
|
+
accessKeyId: token.access_key_id,
|
|
552
|
+
accessKeySecret: token.access_key_secret,
|
|
553
|
+
stsToken: token.security_token,
|
|
554
|
+
}),
|
|
555
|
+
refreshSTSTokenInterval: 60 * 60 * 1000,
|
|
556
|
+
});
|
|
557
|
+
await oss.put(token.object_name, filePath, {
|
|
558
|
+
headers: {
|
|
559
|
+
"Content-Type": mimeType,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function putObsObject(token, endpoint, filePath, mimeType) {
|
|
564
|
+
const endpointUrl = new URL(endpoint);
|
|
565
|
+
const objectPath = encodeObjectPath(token.object_name);
|
|
566
|
+
const resource = `/${token.bucket_name}/${token.object_name}`;
|
|
567
|
+
const uploadUrl = `${endpointUrl.protocol}//${token.bucket_name}.${endpointUrl.host}/${objectPath}`;
|
|
568
|
+
const date = new Date().toUTCString();
|
|
569
|
+
const canonicalObsHeaders = `x-obs-security-token:${token.security_token}\n`;
|
|
570
|
+
const stringToSign = [
|
|
571
|
+
"PUT",
|
|
572
|
+
"",
|
|
573
|
+
mimeType,
|
|
574
|
+
date,
|
|
575
|
+
`${canonicalObsHeaders}${resource}`,
|
|
576
|
+
].join("\n");
|
|
577
|
+
const signature = crypto
|
|
578
|
+
.createHmac("sha1", token.access_key_secret)
|
|
579
|
+
.update(stringToSign)
|
|
580
|
+
.digest("base64");
|
|
581
|
+
const body = await readFile(filePath);
|
|
582
|
+
const response = await fetch(uploadUrl, {
|
|
583
|
+
method: "PUT",
|
|
584
|
+
headers: {
|
|
585
|
+
Authorization: `OBS ${token.access_key_id}:${signature}`,
|
|
586
|
+
Date: date,
|
|
587
|
+
"Content-Type": mimeType,
|
|
588
|
+
"Content-Length": String(body.byteLength),
|
|
589
|
+
"x-obs-security-token": token.security_token,
|
|
590
|
+
},
|
|
591
|
+
body,
|
|
592
|
+
});
|
|
593
|
+
if (!response.ok) {
|
|
594
|
+
const text = await response.text().catch(() => "");
|
|
595
|
+
throw new Error(redactSensitiveText(`OBS 上传失败 [${response.status}] ${uploadUrl}: ${text || response.statusText}`));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function putObjectWithRetry(token, endpoint, filePath, mimeType, authorizationV4) {
|
|
599
|
+
const maxAttempts = 2;
|
|
600
|
+
let lastError;
|
|
601
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
602
|
+
try {
|
|
603
|
+
await putObject(token, endpoint, filePath, mimeType, authorizationV4);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
lastError = error;
|
|
608
|
+
if (!isTransientOssUploadError(error) || attempt === maxAttempts - 1) {
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
await sleep(300 * (attempt + 1));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
615
|
+
}
|
|
616
|
+
function resolveUploadEndpoints(token) {
|
|
617
|
+
// Some DAM tokens include CDN/static domains in accelerate_endpoint. Those
|
|
618
|
+
// domains can serve reads but may reject PUT, so prefer the canonical OSS
|
|
619
|
+
// endpoint and keep the others as fallbacks.
|
|
620
|
+
return uniqueStrings([
|
|
621
|
+
token.endpoint,
|
|
622
|
+
token.standby_endpoint,
|
|
623
|
+
token.accelerate_endpoint,
|
|
624
|
+
]).map(normalizeEndpoint);
|
|
625
|
+
}
|
|
626
|
+
function isHuaweiObsEndpoint(endpoint) {
|
|
627
|
+
return /(^|\.)obs\.[^.]+\.myhuaweicloud\.com/i.test(new URL(endpoint).host);
|
|
628
|
+
}
|
|
629
|
+
function encodeObjectPath(objectName) {
|
|
630
|
+
return objectName.split("/").map(encodeURIComponent).join("/");
|
|
631
|
+
}
|
|
632
|
+
function shouldTryNextOssAuthOrEndpoint(error) {
|
|
633
|
+
return isUnsupportedAuthorizationType(error) || isTransientOssUploadError(error);
|
|
634
|
+
}
|
|
635
|
+
function isUnsupportedAuthorizationType(error) {
|
|
636
|
+
return /Unsupported Authorization Type/i.test(errorMessage(error));
|
|
637
|
+
}
|
|
638
|
+
function isTransientOssUploadError(error) {
|
|
639
|
+
const message = errorMessage(error);
|
|
640
|
+
return (/socket hang up/i.test(message) ||
|
|
641
|
+
/ECONNRESET/i.test(message) ||
|
|
642
|
+
/ETIMEDOUT/i.test(message) ||
|
|
643
|
+
/EPIPE/i.test(message));
|
|
644
|
+
}
|
|
645
|
+
function errorMessage(error) {
|
|
646
|
+
if (!error || typeof error !== "object")
|
|
647
|
+
return String(error);
|
|
648
|
+
return ("message" in error && typeof error.message === "string"
|
|
649
|
+
? error.message
|
|
650
|
+
: String(error));
|
|
651
|
+
}
|
|
652
|
+
function hash(algorithm, body) {
|
|
653
|
+
return crypto.createHash(algorithm).update(body).digest("hex");
|
|
654
|
+
}
|
|
655
|
+
function stripExtension(filename) {
|
|
656
|
+
return filename.replace(/\.[^.]+$/, "");
|
|
657
|
+
}
|
|
658
|
+
function extractRegisteredAssetId(value) {
|
|
659
|
+
const data = unwrapEnvelope(value);
|
|
660
|
+
if (!data || typeof data !== "object")
|
|
661
|
+
return undefined;
|
|
662
|
+
const record = data;
|
|
663
|
+
const candidates = [
|
|
664
|
+
record.id,
|
|
665
|
+
record.asset_id,
|
|
666
|
+
record.content_id,
|
|
667
|
+
record.asset?.id,
|
|
668
|
+
record.asset?.asset_id,
|
|
669
|
+
];
|
|
670
|
+
const found = candidates.find((item) => typeof item === "string" || typeof item === "number");
|
|
671
|
+
return found === undefined ? undefined : String(found);
|
|
672
|
+
}
|
|
673
|
+
export function toDamStorageFormat(format) {
|
|
674
|
+
const normalized = format.toLowerCase();
|
|
675
|
+
if ([
|
|
676
|
+
"png",
|
|
677
|
+
"jpg",
|
|
678
|
+
"jpeg",
|
|
679
|
+
"gif",
|
|
680
|
+
"svg",
|
|
681
|
+
"webp",
|
|
682
|
+
"jfif",
|
|
683
|
+
"bmp",
|
|
684
|
+
"tiff",
|
|
685
|
+
"tif",
|
|
686
|
+
"heic",
|
|
687
|
+
"avif",
|
|
688
|
+
].includes(normalized)) {
|
|
689
|
+
return "gdpic";
|
|
690
|
+
}
|
|
691
|
+
if ([
|
|
692
|
+
"mp4",
|
|
693
|
+
"avi",
|
|
694
|
+
"flv",
|
|
695
|
+
"mov",
|
|
696
|
+
"3gp",
|
|
697
|
+
"m3u8",
|
|
698
|
+
"mpg",
|
|
699
|
+
"asf",
|
|
700
|
+
"wmv",
|
|
701
|
+
"mkv",
|
|
702
|
+
"ts",
|
|
703
|
+
"webm",
|
|
704
|
+
"mxf",
|
|
705
|
+
].includes(normalized)) {
|
|
706
|
+
return "gdvideo";
|
|
707
|
+
}
|
|
708
|
+
if (["mp3", "wav", "ogg", "flac"].includes(normalized))
|
|
709
|
+
return "gdaudio";
|
|
710
|
+
if (["ttf", "otf"].includes(normalized))
|
|
711
|
+
return "gdfont";
|
|
712
|
+
if (["psd", "ai", "sketch", "pdf"].includes(normalized))
|
|
713
|
+
return "gdimage";
|
|
714
|
+
if (["ppt", "pptx"].includes(normalized))
|
|
715
|
+
return "gdslide";
|
|
716
|
+
return normalized;
|
|
717
|
+
}
|
|
718
|
+
async function readPreviewInfo(filePath, mimeType, url, format) {
|
|
719
|
+
if (!mimeType.startsWith("image/"))
|
|
720
|
+
return {};
|
|
721
|
+
try {
|
|
722
|
+
const metadata = await sharp(filePath).metadata();
|
|
723
|
+
return Object.fromEntries(Object.entries({
|
|
724
|
+
url,
|
|
725
|
+
width: metadata.width,
|
|
726
|
+
height: metadata.height,
|
|
727
|
+
format,
|
|
728
|
+
}).filter(([, value]) => value !== undefined));
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return { url, format };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function firstRecordFromResponse(value) {
|
|
735
|
+
const data = unwrapEnvelope(value);
|
|
736
|
+
if (Array.isArray(data))
|
|
737
|
+
return firstRecord(data);
|
|
738
|
+
if (!data || typeof data !== "object")
|
|
739
|
+
return undefined;
|
|
740
|
+
const record = data;
|
|
741
|
+
if (Array.isArray(record.data))
|
|
742
|
+
return firstRecord(record.data);
|
|
743
|
+
return firstRecord([record]);
|
|
744
|
+
}
|
|
745
|
+
function firstRecord(values) {
|
|
746
|
+
const found = values.find((item) => item && typeof item === "object");
|
|
747
|
+
return found;
|
|
748
|
+
}
|
|
749
|
+
function extractAssetInfo(value) {
|
|
750
|
+
const data = unwrapEnvelope(value);
|
|
751
|
+
if (!data || typeof data !== "object")
|
|
752
|
+
return undefined;
|
|
753
|
+
const record = data;
|
|
754
|
+
const candidates = [
|
|
755
|
+
record.asset_info,
|
|
756
|
+
record.asset,
|
|
757
|
+
record,
|
|
758
|
+
];
|
|
759
|
+
return candidates.find((item) => Boolean(item && typeof item === "object"));
|
|
760
|
+
}
|
|
761
|
+
function coverFromAssetInfo(assetInfo, fallbackPreview) {
|
|
762
|
+
const previewInfo = getRecord(assetInfo, "preview_info") ?? getRecord(assetInfo, "preview");
|
|
763
|
+
const sourcePreviewInfo = getRecord(assetInfo, "source_preview_info");
|
|
764
|
+
const fallback = fallbackPreview && Object.keys(fallbackPreview).length ? fallbackPreview : undefined;
|
|
765
|
+
const coverSource = previewInfo ?? sourcePreviewInfo ?? fallback;
|
|
766
|
+
if (!coverSource)
|
|
767
|
+
return undefined;
|
|
768
|
+
const url = extractString(coverSource, ["url", "origin_url"]);
|
|
769
|
+
const width = extractNumber(coverSource, "width");
|
|
770
|
+
const height = extractNumber(coverSource, "height");
|
|
771
|
+
const duration = extractNumber(coverSource, "duration");
|
|
772
|
+
if (!url && width === undefined && height === undefined && duration === undefined)
|
|
773
|
+
return undefined;
|
|
774
|
+
return Object.fromEntries(Object.entries({ url, width, height, duration }).filter(([, value]) => value !== undefined));
|
|
775
|
+
}
|
|
776
|
+
function getRecord(record, key) {
|
|
777
|
+
const value = record?.[key];
|
|
778
|
+
return value && typeof value === "object" ? value : undefined;
|
|
779
|
+
}
|
|
780
|
+
function extractAnalysisStatus(record) {
|
|
781
|
+
const value = record?.analysis_status;
|
|
782
|
+
if (typeof value === "number" || typeof value === "string")
|
|
783
|
+
return value;
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
function isTerminalAnalysisStatus(status) {
|
|
787
|
+
const normalized = typeof status === "string" ? Number(status) : status;
|
|
788
|
+
return (normalized === ANALYSIS_STATUS.SUCCESS ||
|
|
789
|
+
normalized === ANALYSIS_STATUS.FAILED ||
|
|
790
|
+
normalized === ANALYSIS_STATUS.NOT_SUPPORT);
|
|
791
|
+
}
|
|
792
|
+
function extractString(record, keys) {
|
|
793
|
+
for (const key of keys) {
|
|
794
|
+
const value = record?.[key];
|
|
795
|
+
if (typeof value === "string" && value.trim())
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
function extractNumber(record, key) {
|
|
801
|
+
const value = record[key];
|
|
802
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
803
|
+
return value;
|
|
804
|
+
if (typeof value === "string" && value.trim()) {
|
|
805
|
+
const parsed = Number(value);
|
|
806
|
+
if (Number.isFinite(parsed))
|
|
807
|
+
return parsed;
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
function normalizePositiveInteger(value, fallback) {
|
|
812
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0)
|
|
813
|
+
return value;
|
|
814
|
+
return fallback;
|
|
815
|
+
}
|
|
816
|
+
function uniqueStrings(values) {
|
|
817
|
+
const set = new Set();
|
|
818
|
+
for (const value of values) {
|
|
819
|
+
if (value?.trim())
|
|
820
|
+
set.add(value.trim());
|
|
821
|
+
}
|
|
822
|
+
return [...set];
|
|
823
|
+
}
|
|
824
|
+
function sleep(ms) {
|
|
825
|
+
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
826
|
+
}
|
|
827
|
+
function normalizeEndpoint(endpoint) {
|
|
828
|
+
if (!endpoint)
|
|
829
|
+
throw new Error("上传凭证缺少 endpoint");
|
|
830
|
+
return endpoint.startsWith("http") ? endpoint : `https://${endpoint}`;
|
|
831
|
+
}
|
|
832
|
+
function normalizeHost(host) {
|
|
833
|
+
const normalized = host.startsWith("http") ? host : `https://${host}`;
|
|
834
|
+
return normalized.replace(/\/$/, "");
|
|
835
|
+
}
|
|
836
|
+
function appendAuthKey(url, authKey) {
|
|
837
|
+
if (!authKey)
|
|
838
|
+
return url;
|
|
839
|
+
const parsed = new URL(url);
|
|
840
|
+
if (!parsed.searchParams.has("auth_key")) {
|
|
841
|
+
parsed.searchParams.set("auth_key", authKey);
|
|
842
|
+
}
|
|
843
|
+
return parsed.toString();
|
|
844
|
+
}
|
|
845
|
+
function inferFormat(filePath, buffer) {
|
|
846
|
+
const detected = buffer ? inferFormatFromBuffer(buffer) : undefined;
|
|
847
|
+
if (detected)
|
|
848
|
+
return detected;
|
|
849
|
+
const format = extname(filePath).replace(/^\./, "").toLowerCase();
|
|
850
|
+
if (!format)
|
|
851
|
+
throw new Error(`无法从文件名推断格式:${filePath}`);
|
|
852
|
+
return format === "jpg" ? "jpeg" : format;
|
|
853
|
+
}
|
|
854
|
+
function inferFormatFromBuffer(buffer) {
|
|
855
|
+
if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
856
|
+
return "jpeg";
|
|
857
|
+
}
|
|
858
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
859
|
+
return "png";
|
|
860
|
+
}
|
|
861
|
+
if (buffer.length >= 6 && buffer.subarray(0, 3).toString("ascii") === "GIF") {
|
|
862
|
+
return "gif";
|
|
863
|
+
}
|
|
864
|
+
if (buffer.length >= 12 &&
|
|
865
|
+
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
866
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
867
|
+
return "webp";
|
|
868
|
+
}
|
|
869
|
+
if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
|
|
870
|
+
const brand = buffer.subarray(8, 12).toString("ascii");
|
|
871
|
+
if (brand === "avif" || brand === "avis")
|
|
872
|
+
return "avif";
|
|
873
|
+
if (brand === "heic" || brand === "heix" || brand === "hevc" || brand === "hevx")
|
|
874
|
+
return "heic";
|
|
875
|
+
}
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
function inferMimeType(format) {
|
|
879
|
+
const map = {
|
|
880
|
+
jpeg: "image/jpeg",
|
|
881
|
+
png: "image/png",
|
|
882
|
+
gif: "image/gif",
|
|
883
|
+
webp: "image/webp",
|
|
884
|
+
avif: "image/avif",
|
|
885
|
+
svg: "image/svg+xml",
|
|
886
|
+
heic: "image/heic",
|
|
887
|
+
heif: "image/heif",
|
|
888
|
+
bmp: "image/bmp",
|
|
889
|
+
mp4: "video/mp4",
|
|
890
|
+
webm: "video/webm",
|
|
891
|
+
mov: "video/quicktime",
|
|
892
|
+
m4v: "video/x-m4v",
|
|
893
|
+
avi: "video/x-msvideo",
|
|
894
|
+
mkv: "video/x-matroska",
|
|
895
|
+
};
|
|
896
|
+
return map[format] ?? `application/${format}`;
|
|
897
|
+
}
|