helixlife-expert-cli 0.3.2 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +3 -3
  2. package/cli/dist/modules/analysis/api.d.ts +28 -0
  3. package/cli/dist/modules/analysis/api.d.ts.map +1 -0
  4. package/cli/dist/modules/analysis/api.js +265 -0
  5. package/cli/dist/modules/analysis/api.js.map +1 -0
  6. package/cli/dist/modules/analysis/bio.d.ts +159 -0
  7. package/cli/dist/modules/analysis/bio.d.ts.map +1 -0
  8. package/cli/dist/modules/analysis/bio.js +275 -0
  9. package/cli/dist/modules/analysis/bio.js.map +1 -0
  10. package/cli/dist/modules/analysis/catalog.d.ts +22 -0
  11. package/cli/dist/modules/analysis/catalog.d.ts.map +1 -0
  12. package/cli/dist/modules/analysis/catalog.js +126 -0
  13. package/cli/dist/modules/analysis/catalog.js.map +1 -0
  14. package/cli/dist/modules/analysis/commands.d.ts +15 -0
  15. package/cli/dist/modules/analysis/commands.d.ts.map +1 -0
  16. package/cli/dist/modules/analysis/commands.js +475 -0
  17. package/cli/dist/modules/analysis/commands.js.map +1 -0
  18. package/cli/dist/modules/analysis/constants.d.ts +14 -0
  19. package/cli/dist/modules/analysis/constants.d.ts.map +1 -1
  20. package/cli/dist/modules/analysis/constants.js +15 -0
  21. package/cli/dist/modules/analysis/constants.js.map +1 -1
  22. package/cli/dist/modules/analysis/errors.d.ts +12 -0
  23. package/cli/dist/modules/analysis/errors.d.ts.map +1 -0
  24. package/cli/dist/modules/analysis/errors.js +96 -0
  25. package/cli/dist/modules/analysis/errors.js.map +1 -0
  26. package/cli/dist/modules/analysis/files.d.ts +8 -0
  27. package/cli/dist/modules/analysis/files.d.ts.map +1 -0
  28. package/cli/dist/modules/analysis/files.js +59 -0
  29. package/cli/dist/modules/analysis/files.js.map +1 -0
  30. package/cli/dist/modules/analysis/flags.d.ts +27 -0
  31. package/cli/dist/modules/analysis/flags.d.ts.map +1 -0
  32. package/cli/dist/modules/analysis/flags.js +58 -0
  33. package/cli/dist/modules/analysis/flags.js.map +1 -0
  34. package/cli/dist/modules/analysis/index.d.ts.map +1 -1
  35. package/cli/dist/modules/analysis/index.js +154 -8
  36. package/cli/dist/modules/analysis/index.js.map +1 -1
  37. package/cli/dist/modules/analysis/inputs.d.ts +36 -0
  38. package/cli/dist/modules/analysis/inputs.d.ts.map +1 -0
  39. package/cli/dist/modules/analysis/inputs.js +120 -0
  40. package/cli/dist/modules/analysis/inputs.js.map +1 -0
  41. package/cli/dist/modules/analysis/json.d.ts +7 -0
  42. package/cli/dist/modules/analysis/json.d.ts.map +1 -0
  43. package/cli/dist/modules/analysis/json.js +30 -0
  44. package/cli/dist/modules/analysis/json.js.map +1 -0
  45. package/cli/dist/modules/analysis/overrides.d.ts +12 -0
  46. package/cli/dist/modules/analysis/overrides.d.ts.map +1 -0
  47. package/cli/dist/modules/analysis/overrides.js +185 -0
  48. package/cli/dist/modules/analysis/overrides.js.map +1 -0
  49. package/cli/dist/modules/analysis/prepare.d.ts +47 -0
  50. package/cli/dist/modules/analysis/prepare.d.ts.map +1 -0
  51. package/cli/dist/modules/analysis/prepare.js +941 -0
  52. package/cli/dist/modules/analysis/prepare.js.map +1 -0
  53. package/cli/dist/modules/analysis/schema.d.ts +5 -0
  54. package/cli/dist/modules/analysis/schema.d.ts.map +1 -0
  55. package/cli/dist/modules/analysis/schema.js +39 -0
  56. package/cli/dist/modules/analysis/schema.js.map +1 -0
  57. package/package.json +1 -1
@@ -0,0 +1,941 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { ANALYSIS_AGENT_NAME, ANALYSIS_HISTORY_UUID } from "./constants.js";
3
+ import { resolveToolProductUuid, uploadLocalFile } from "./api.js";
4
+ import { fetchToolMenuOrFallback, resolveToolByCode, } from "./catalog.js";
5
+ import { buildSubmitArgsData, fetchAllArgs, fetchCloudPage, fetchHistoryDownloads, fetchHistoryPage, findHistoryByFuid, historyStatus, isHistoryFailed, isHistoryFinished, resolveDownloadUrl, submitModule, updateArgs, verifyUpload, } from "./bio.js";
6
+ import { AnalysisChoiceError, AnalysisError } from "./errors.js";
7
+ import { downloadArtifact, downloadToPath } from "./files.js";
8
+ import { findArgsDataType, findDefaultCloud, findDefaultRecord, findRecordField, listUploadFields, } from "./inputs.js";
9
+ import { asObject, deepMerge, isJsonObject, textOf } from "./json.js";
10
+ import { applySpecificOverrides, assertOverrideSides, loadArgsMainConfig, parseSetOverrides, } from "./overrides.js";
11
+ import { resolveUserPath } from "../../core/paths.js";
12
+ import { buildArgsMainFromSchema, buildArgsSpecificFromSchema } from "./schema.js";
13
+ export async function prepareTool(tool, flags) {
14
+ validateInputFlags(flags);
15
+ const productUuid = resolveToolProductUuid(flags.toolProductUuid);
16
+ const module = await resolveModule(tool, flags, productUuid);
17
+ const fetched = await fetchAllArgs(module.moduleId, module.uuid);
18
+ const mode = resolveInputMode(fetched, flags);
19
+ const config = await loadArgsMainConfig(module.moduleId, flags.configPath);
20
+ const setOverrides = deepMerge(config, parseSetOverrides(flags.sets));
21
+ let schema = fetched.args_main;
22
+ let specificSchema = fetched.args_specific;
23
+ let argsMain = deepMerge(buildArgsMainFromSchema(schema), setOverrides);
24
+ let argsSpecific = applySpecificOverrides(buildArgsSpecificFromSchema(specificSchema), flags.specifics);
25
+ let prepared = await resolvePreparedInput(fetched, flags, module, mode, argsMain);
26
+ if (prepared.schema.length > 0) {
27
+ schema = prepared.schema;
28
+ argsMain = deepMerge(buildArgsMainFromSchema(schema), setOverrides);
29
+ }
30
+ if (prepared.input.kind === "cloud") {
31
+ const updated = await updateArgs({
32
+ argsData: buildSubmitArgsData(prepared.duid, prepared.input),
33
+ argsKey: prepared.input.cloud.key,
34
+ argsMain,
35
+ argsSpecific,
36
+ moduleId: module.moduleId,
37
+ uuid: module.uuid,
38
+ });
39
+ const cloud = findDefaultCloud(updated.args_data);
40
+ if (cloud) {
41
+ prepared = {
42
+ ...prepared,
43
+ input: {
44
+ kind: "cloud",
45
+ cloud: { key: cloud.key, label: cloud.label, record: normalizeCloudRecord(cloud.record) },
46
+ },
47
+ };
48
+ }
49
+ schema = updated.args_main.length > 0 ? updated.args_main : schema;
50
+ specificSchema = updated.args_specific;
51
+ argsMain = deepMerge(buildArgsMainFromSchema(schema), setOverrides);
52
+ argsSpecific = applySpecificOverrides(buildArgsSpecificFromSchema(specificSchema), flags.specifics);
53
+ }
54
+ assertOverrideSides({
55
+ sets: flags.sets,
56
+ specifics: flags.specifics,
57
+ mainSchema: schema,
58
+ specificSchema,
59
+ });
60
+ return {
61
+ agent: ANALYSIS_AGENT_NAME,
62
+ argsMain,
63
+ argsSpecific,
64
+ argsSpecificSchema: specificSchema,
65
+ duid: prepared.duid,
66
+ input: prepared.input,
67
+ inputMode: mode,
68
+ inputSummary: summarizeInput(prepared.input),
69
+ moduleId: module.moduleId,
70
+ notice: fetched.notice,
71
+ route: module.route,
72
+ schema,
73
+ toolProductUuid: module.toolProductUuid,
74
+ uuid: module.uuid,
75
+ };
76
+ }
77
+ export function preparedPayload(run) {
78
+ return {
79
+ agent: run.agent,
80
+ args_main: run.schema,
81
+ args_main_defaults: buildArgsMainFromSchema(run.schema),
82
+ args_main_prepared: run.argsMain,
83
+ args_specific: run.argsSpecificSchema,
84
+ args_specific_defaults: buildArgsSpecificFromSchema(run.argsSpecificSchema),
85
+ args_specific_prepared: run.argsSpecific,
86
+ duid: run.duid,
87
+ ...run.inputSummary,
88
+ mode: run.inputMode,
89
+ module_id: run.moduleId,
90
+ notice: run.notice,
91
+ route: run.route,
92
+ tool_product_uuid: run.toolProductUuid,
93
+ uuid: run.uuid,
94
+ };
95
+ }
96
+ export async function executeTool(tool, flags) {
97
+ validateDownloadFlags(flags);
98
+ const prepared = await prepareTool(tool, flags);
99
+ const submittedAt = Date.now();
100
+ const result = await submitModule({
101
+ moduleId: prepared.moduleId,
102
+ uuid: prepared.uuid,
103
+ route: prepared.route,
104
+ argsMain: prepared.argsMain,
105
+ argsSpecific: prepared.argsSpecific,
106
+ argsData: buildSubmitArgsData(prepared.duid, prepared.input),
107
+ });
108
+ if (result.asyncSubmission) {
109
+ return resolveAsyncRun(prepared, flags, submittedAt, result.message);
110
+ }
111
+ const paths = flags.download
112
+ ? await downloadResult({
113
+ moduleId: prepared.moduleId,
114
+ uuid: prepared.uuid,
115
+ downloads: result.downloads,
116
+ file: result.file,
117
+ downloadPath: flags.download,
118
+ downloadItem: flags.downloadItem,
119
+ downloadLabel: flags.downloadLabel,
120
+ })
121
+ : [];
122
+ return directPayload(prepared, result, paths);
123
+ }
124
+ async function resolveAsyncRun(prepared, flags, submittedAt, message) {
125
+ const timeout = flags.download ? 60_000 : 12_000;
126
+ const record = await waitForLatestHistory(prepared.moduleId, submittedAt, timeout);
127
+ const summary = record ? toHistorySummary(record, prepared.moduleId) : null;
128
+ if (!flags.download || !textOf(record?.fuid)) {
129
+ const note = flags.download && !textOf(record?.fuid) ? `${message},但暂未定位到历史记录。` : message;
130
+ return historyPayload(prepared, summary, note, "", [], []);
131
+ }
132
+ const fuid = textOf(record?.fuid);
133
+ const finished = await waitForHistoryCompletion(fuid, 60_000);
134
+ const finishedSummary = finished ? toHistorySummary(finished, prepared.moduleId) : summary;
135
+ if (!textOf(finished?.fuid)) {
136
+ const note = `${message},但等待历史记录完成超时。`;
137
+ return historyPayload(prepared, finishedSummary, note, "", [], []);
138
+ }
139
+ const downloaded = await downloadHistoryWithRetry(fuid, prepared.moduleId, flags);
140
+ return historyPayload(prepared, finishedSummary, message, downloaded.body, downloaded.downloads, downloaded.paths);
141
+ }
142
+ async function downloadHistoryWithRetry(fuid, moduleId, flags) {
143
+ let last;
144
+ for (let attempt = 0; attempt < 4; attempt += 1) {
145
+ const viewed = await fetchHistoryDownloads(fuid);
146
+ try {
147
+ const paths = await downloadResult({
148
+ moduleId,
149
+ uuid: ANALYSIS_HISTORY_UUID,
150
+ downloads: viewed.downloads,
151
+ downloadPath: flags.download,
152
+ downloadItem: flags.downloadItem,
153
+ downloadLabel: flags.downloadLabel,
154
+ });
155
+ return { body: viewed.body, downloads: viewed.downloads, paths };
156
+ }
157
+ catch (error) {
158
+ if (!(error instanceof AnalysisError) || !isRetryableDownload(error) || attempt === 3) {
159
+ throw error;
160
+ }
161
+ last = error;
162
+ await sleep(3000);
163
+ }
164
+ }
165
+ throw last ?? new AnalysisError("下载失败,请稍后重试");
166
+ }
167
+ function directPayload(prepared, result, paths) {
168
+ // 路径与下载项放在 args_main / body 之前:宿主会截断长 stdout
169
+ return {
170
+ artifact: downloadArtifact(paths[0]),
171
+ download_path: paths[0],
172
+ download_paths: paths,
173
+ downloads: result.downloads,
174
+ result_source: "direct",
175
+ module_id: prepared.moduleId,
176
+ route: prepared.route,
177
+ submission_message: result.message,
178
+ agent: prepared.agent,
179
+ duid: prepared.duid,
180
+ file: result.file ?? null,
181
+ history: null,
182
+ history_hint: null,
183
+ ...prepared.inputSummary,
184
+ mode: prepared.inputMode,
185
+ notice: prepared.notice,
186
+ tool_product_uuid: prepared.toolProductUuid,
187
+ uuid: prepared.uuid,
188
+ args_specific: prepared.argsSpecific,
189
+ args_main: prepared.argsMain,
190
+ body: result.body,
191
+ };
192
+ }
193
+ function historyPayload(prepared, history, message, body, downloads, paths) {
194
+ return {
195
+ artifact: downloadArtifact(paths[0]),
196
+ download_path: paths[0],
197
+ download_paths: paths,
198
+ downloads,
199
+ result_source: "history",
200
+ history,
201
+ history_hint: historyHint(history),
202
+ module_id: prepared.moduleId,
203
+ route: prepared.route,
204
+ submission_message: message,
205
+ agent: prepared.agent,
206
+ duid: prepared.duid,
207
+ file: null,
208
+ ...prepared.inputSummary,
209
+ mode: prepared.inputMode,
210
+ notice: prepared.notice,
211
+ tool_product_uuid: prepared.toolProductUuid,
212
+ uuid: prepared.uuid,
213
+ args_specific: prepared.argsSpecific,
214
+ args_main: prepared.argsMain,
215
+ body,
216
+ };
217
+ }
218
+ export function historyHint(history) {
219
+ const fuid = textOf(history?.fuid);
220
+ return fuid
221
+ ? `可使用 \`helixlife-expert analysis history download ${fuid} --device pdf\` 或 \`helixlife-expert analysis history list\` 查看结果`
222
+ : "可使用 `helixlife-expert analysis history list` 查看任务进度";
223
+ }
224
+ export async function downloadResult(input) {
225
+ if (input.downloads.length > 0) {
226
+ const item = pickDownloadItem(input.downloads, {
227
+ item: input.downloadItem,
228
+ label: input.downloadLabel,
229
+ outputPath: input.downloadPath,
230
+ });
231
+ if (!item) {
232
+ throw downloadChoiceError(input.downloads, "当前结果包含多个可下载文件。分析已经完成,请用 `--downloadLabel <名称>` 选择后再保存。第 1 项通常是整份报告 zip,不要用它代替图片或表格。");
233
+ }
234
+ let resolved;
235
+ try {
236
+ resolved = await resolveDownloadUrl({
237
+ item,
238
+ moduleId: input.moduleId,
239
+ uuid: input.uuid,
240
+ });
241
+ }
242
+ catch (error) {
243
+ if (error instanceof AnalysisError && error.message === "未获取到下载链接") {
244
+ throw downloadChoiceError(input.downloads, `未获取到「${item.label}」(${item.device}) 的下载链接。分析已经完成,请改用 --downloadLabel 选择 pdf、tiff 或表格。第 1 项整份报告 zip 经常没有链接。`);
245
+ }
246
+ throw error;
247
+ }
248
+ return [await downloadToPath(resolved.url, input.downloadPath)];
249
+ }
250
+ if (input.file) {
251
+ return [await downloadToPath(input.file, input.downloadPath)];
252
+ }
253
+ throw new AnalysisError("当前模块未返回可下载文件。");
254
+ }
255
+ export function pickDownloadItem(items, options) {
256
+ if (options.item !== undefined) {
257
+ return pickByIndex(items, options.item);
258
+ }
259
+ if (options.label) {
260
+ return pickByLabel(items, options.label);
261
+ }
262
+ const device = normalizeDevice(options.device ?? inferDevice(options.outputPath));
263
+ if (device) {
264
+ const matched = items.filter((item) => normalizeDevice(item.device) === device);
265
+ if (matched.length === 1)
266
+ return matched[0];
267
+ if (matched.length > 1) {
268
+ throw downloadChoiceError(items, `下载格式 ${device} 匹配到多个结果。分析已经完成,请用 \`--downloadItem <n>\` 或 \`--downloadLabel <名称>\` 选择后再保存,不要重新检索工具。`);
269
+ }
270
+ throw downloadChoiceError(items, `没有 ${device} 格式的产物。分析已经完成,请按下列名称用 \`--downloadItem <n>\` 或 \`--downloadLabel <名称>\` 保存,不要重新检索工具。`);
271
+ }
272
+ const preferred = items.filter((item) => item.endpoint === "public.output.download-result");
273
+ if (preferred.length === 1)
274
+ return preferred[0];
275
+ if (items.length === 1)
276
+ return items[0];
277
+ return undefined;
278
+ }
279
+ function resolveInputMode(fetched, flags) {
280
+ if (flags.demo) {
281
+ if (hasDemo(fetched))
282
+ return "demo";
283
+ if (hasRecord(fetched))
284
+ return "record";
285
+ if (hasCloud(fetched))
286
+ return "cloud";
287
+ return "demo";
288
+ }
289
+ if (flags.files.length > 0)
290
+ return "upload";
291
+ if (recordRequested(flags))
292
+ return "record";
293
+ if (cloudRequested(flags, fetched))
294
+ return "cloud";
295
+ if (hasRecord(fetched))
296
+ return "record";
297
+ if (hasCloud(fetched))
298
+ return "cloud";
299
+ if (listUploadFields(fetched.args_data).length > 0)
300
+ return "upload";
301
+ return "none";
302
+ }
303
+ async function resolvePreparedInput(fetched, flags, module, mode, argsMain) {
304
+ if (mode === "cloud") {
305
+ return resolveCloudInput(fetched, flags, module);
306
+ }
307
+ if (mode === "demo") {
308
+ return resolveDemoInput(fetched, module.moduleId);
309
+ }
310
+ if (mode === "record") {
311
+ return resolveRecordInput(fetched, flags, module.moduleId);
312
+ }
313
+ if (mode === "none") {
314
+ return { duid: textOf(fetched.id.duid), input: { kind: "none" }, schema: [] };
315
+ }
316
+ return uploadAndVerify(flags.files, fetched.args_data, module, argsMain);
317
+ }
318
+ function resolveDemoInput(fetched, moduleId) {
319
+ const duid = textOf(fetched.id.duid);
320
+ if (!duid) {
321
+ throw new AnalysisError(`模块 ${moduleId} 未返回 demo duid,无法使用 \`--demo\`。`);
322
+ }
323
+ const fields = listUploadFields(fetched.args_data);
324
+ if (!fields.length || fields.some((field) => !field.defaultUpload)) {
325
+ throw new AnalysisError(`模块 ${moduleId} 未返回可用的 demo 文件信息,无法使用 \`--demo\`。`);
326
+ }
327
+ return {
328
+ duid,
329
+ input: {
330
+ kind: "upload",
331
+ uploads: fields.map((field) => ({
332
+ key: field.key,
333
+ label: field.label,
334
+ upload: {
335
+ fileName: field.defaultUpload.fileName,
336
+ fileOss: field.defaultUpload.fileOss,
337
+ fileSize: field.defaultUpload.fileSize,
338
+ },
339
+ })),
340
+ },
341
+ schema: [],
342
+ };
343
+ }
344
+ async function resolveCloudInput(fetched, flags, module) {
345
+ const cloud = findDefaultCloud(fetched.args_data);
346
+ const key = flags.cloudKey?.trim() || cloud?.key || "";
347
+ if (!key) {
348
+ throw new AnalysisError(`模块 ${module.moduleId} 未返回可用的云端数据参数,请显式传入 \`--cloudKey\`.`);
349
+ }
350
+ const record = await resolveCloudRecord(flags, {
351
+ argsKey: key,
352
+ defaultRecord: cloud?.record,
353
+ moduleId: module.moduleId,
354
+ uuid: module.uuid,
355
+ });
356
+ if (!record) {
357
+ throw new AnalysisError(`模块 ${module.moduleId} 未返回默认云端数据,请传入 \`--cloudRecordJson\` 或 \`--cloudRecordFile\`.`);
358
+ }
359
+ return {
360
+ duid: textOf(fetched.id.duid) || "cloud",
361
+ input: {
362
+ kind: "cloud",
363
+ cloud: { key, label: cloud?.label ?? "", record: normalizeCloudRecord(record) },
364
+ },
365
+ schema: [],
366
+ };
367
+ }
368
+ async function resolveRecordInput(fetched, flags, moduleId) {
369
+ const duid = textOf(fetched.id.duid);
370
+ if (!duid) {
371
+ throw new AnalysisError(`模块 ${moduleId} 未返回 record duid,无法使用历史记录输入。`);
372
+ }
373
+ const argsData = fetched.args_data;
374
+ const cloudKeyAsRecord = flags.cloudKey && findArgsDataType(argsData, flags.cloudKey) === "XTRecord"
375
+ ? flags.cloudKey
376
+ : "";
377
+ const fallback = findDefaultRecord(argsData);
378
+ const key = flags.recordKey?.trim() || cloudKeyAsRecord || fallback?.key || "";
379
+ if (!key) {
380
+ throw new AnalysisError(`模块 ${moduleId} 未识别到 XTRecord 字段,请显式传入 \`--recordKey\` 或改用其他输入方式。`);
381
+ }
382
+ const field = findRecordField(argsData, key);
383
+ if (!field) {
384
+ throw new AnalysisError(`模块 ${moduleId} 未找到 XTRecord 字段 ${key}。`);
385
+ }
386
+ const picked = await pickRecord(field.options, flags, {
387
+ moduleId,
388
+ recordKey: key,
389
+ defaultRecord: fallback?.key === key ? fallback : null,
390
+ });
391
+ return {
392
+ duid,
393
+ input: {
394
+ kind: "record",
395
+ record: {
396
+ key,
397
+ label: field.label || (fallback?.key === key ? fallback.label : ""),
398
+ ...picked,
399
+ },
400
+ },
401
+ schema: [],
402
+ };
403
+ }
404
+ async function uploadAndVerify(files, argsData, module, argsMain) {
405
+ const fields = listUploadFields(argsData);
406
+ const selected = resolveUploadInputs(files, fields, module.moduleId);
407
+ const uploads = [];
408
+ for (const item of selected) {
409
+ const uploaded = await uploadLocalFile(item.filePath);
410
+ uploads.push({ key: item.key, label: item.label, upload: uploaded });
411
+ }
412
+ const verified = await verifyUpload({
413
+ argsMain,
414
+ moduleId: module.moduleId,
415
+ uploads,
416
+ uuid: module.uuid,
417
+ });
418
+ return {
419
+ duid: verified.duid,
420
+ input: { kind: "upload", uploads },
421
+ schema: verified.schema,
422
+ };
423
+ }
424
+ export function resolveUploadInputs(files, fields, moduleId) {
425
+ if (!fields.length) {
426
+ throw new AnalysisError(`模块 ${moduleId} 未检测到可上传输入,不能使用 \`--file\`。`);
427
+ }
428
+ if (!files.length) {
429
+ throw new AnalysisError(fields.length === 1
430
+ ? "通用模式请传入 `--file <xlsx>`,或改用 `--demo`。"
431
+ : `模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${fields.map((field) => field.key).join(", ")}`);
432
+ }
433
+ const only = fields[0];
434
+ if (fields.length === 1 && files.length === 1) {
435
+ const parsed = parseFileInput(files[0]);
436
+ if (parsed.key && parsed.key !== only.key) {
437
+ throw new AnalysisError(`模块 ${moduleId} 仅支持上传字段 ${only.key},收到 ${parsed.key}。`);
438
+ }
439
+ return [{ filePath: parsed.path, key: only.key, label: only.label }];
440
+ }
441
+ const byKey = new Map(fields.map((field) => [field.key, field]));
442
+ const chosen = new Map();
443
+ for (const raw of files) {
444
+ const parsed = parseFileInput(raw);
445
+ if (!parsed.key) {
446
+ if (fields.length === 1) {
447
+ if (chosen.has(only.key)) {
448
+ throw new AnalysisError(`模块 ${moduleId} 仅需要 1 个上传文件,收到重复输入。`);
449
+ }
450
+ chosen.set(only.key, parsed.path);
451
+ continue;
452
+ }
453
+ throw new AnalysisError(`模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${fields.map((field) => field.key).join(", ")}`);
454
+ }
455
+ const field = byKey.get(parsed.key);
456
+ if (!field) {
457
+ throw new AnalysisError(`模块 ${moduleId} 不存在上传字段 ${parsed.key}。可用字段: ${fields.map((item) => item.key).join(", ")}`);
458
+ }
459
+ if (chosen.has(field.key)) {
460
+ throw new AnalysisError(`上传字段 ${field.key} 重复传入。`);
461
+ }
462
+ chosen.set(field.key, parsed.path);
463
+ }
464
+ const missing = fields.filter((field) => field.required && !chosen.has(field.key));
465
+ if (missing.length > 0) {
466
+ throw new AnalysisError(`模块 ${moduleId} 缺少必填上传字段: ${missing.map((field) => field.key).join(", ")}`);
467
+ }
468
+ return fields.flatMap((field) => {
469
+ const path = chosen.get(field.key);
470
+ return path ? [{ filePath: path, key: field.key, label: field.label }] : [];
471
+ });
472
+ }
473
+ async function resolveModule(tool, flags, toolProductUuid) {
474
+ const moduleId = tool?.trim() ?? "";
475
+ const route = flags.route?.trim();
476
+ const uuid = flags.uuid?.trim();
477
+ if (!moduleId) {
478
+ throw new AnalysisError("请传入 `module_id`。");
479
+ }
480
+ if (route && uuid) {
481
+ return { moduleId, route, toolProductUuid, uuid };
482
+ }
483
+ const menuUuid = flags.menuUuid?.trim();
484
+ if (menuUuid) {
485
+ const menu = await fetchToolMenuOrFallback(menuUuid, toolProductUuid);
486
+ const menuCode = textOf(menu.code);
487
+ if (menuCode && menuCode !== moduleId) {
488
+ throw new AnalysisError(`菜单模块 code 为 ${menuCode},与传入的 module_id ${moduleId} 不一致。`);
489
+ }
490
+ return {
491
+ moduleId,
492
+ route: route || requireRoute(menu),
493
+ toolProductUuid,
494
+ uuid: uuid || menuUuid,
495
+ };
496
+ }
497
+ const resolved = await resolveToolByCode(toolProductUuid, moduleId);
498
+ return {
499
+ moduleId: resolved.code,
500
+ route: route || resolved.route,
501
+ toolProductUuid,
502
+ uuid: uuid || resolved.uuid,
503
+ };
504
+ }
505
+ async function resolveCloudRecord(flags, context) {
506
+ if (flags.cloudRecordJson) {
507
+ return parseJsonObject(flags.cloudRecordJson, "--cloudRecordJson");
508
+ }
509
+ if (flags.cloudRecordFile) {
510
+ const raw = await readFile(resolveUserPath(flags.cloudRecordFile), "utf8");
511
+ return parseJsonObject(raw, "--cloudRecordFile");
512
+ }
513
+ if (flags.cloudFilter?.trim() || flags.cloudDataId?.trim() || typeof flags.cloudIndex === "number") {
514
+ return resolveCloudByQuery(flags, context);
515
+ }
516
+ return context.defaultRecord;
517
+ }
518
+ async function resolveCloudByQuery(flags, context) {
519
+ const records = await fetchAllCloudRecords(flags, context);
520
+ if (records.length === 0) {
521
+ const filter = flags.cloudFilter?.trim();
522
+ throw new AnalysisError(filter ? `未找到匹配云端数据: ${filter}` : "当前模块没有可选的云端数据记录。");
523
+ }
524
+ const dataId = flags.cloudDataId?.trim();
525
+ if (dataId) {
526
+ const found = records.find((record) => textOf(record.data_id) === dataId);
527
+ if (!found) {
528
+ throw new AnalysisError(`未找到 data_id=${dataId} 对应的云端数据。`);
529
+ }
530
+ return found;
531
+ }
532
+ if (typeof flags.cloudIndex === "number") {
533
+ const found = records[flags.cloudIndex];
534
+ if (!found) {
535
+ throw new AnalysisError(`云端数据索引超出范围: ${flags.cloudIndex},当前共有 ${records.length} 条。`);
536
+ }
537
+ return found;
538
+ }
539
+ if (records.length === 1)
540
+ return records[0];
541
+ throw new AnalysisChoiceError(cloudSelectionHint(records, flags.cloudFilter?.trim()), {
542
+ choices: records.map((record, index) => ({
543
+ data_id: textOf(record.data_id),
544
+ index,
545
+ label: textOf(record.label) || textOf(record.data_id),
546
+ })),
547
+ filter: flags.cloudFilter?.trim() ?? "",
548
+ selection_type: "cloud_record",
549
+ });
550
+ }
551
+ async function fetchAllCloudRecords(flags, context) {
552
+ const first = await fetchCloudPage({
553
+ argsKey: context.argsKey,
554
+ currentPage: 1,
555
+ filter: flags.cloudFilter?.trim() ?? "",
556
+ moduleId: context.moduleId,
557
+ selectedRecord: context.defaultRecord,
558
+ uuid: context.uuid,
559
+ });
560
+ const records = first.records.map(normalizeCloudRecord);
561
+ for (let page = first.currentPage + 1; page <= first.lastPage; page += 1) {
562
+ const next = await fetchCloudPage({
563
+ argsKey: context.argsKey,
564
+ currentPage: page,
565
+ filter: flags.cloudFilter?.trim() ?? "",
566
+ moduleId: context.moduleId,
567
+ selectedRecord: context.defaultRecord,
568
+ uuid: context.uuid,
569
+ });
570
+ records.push(...next.records.map(normalizeCloudRecord));
571
+ }
572
+ return records;
573
+ }
574
+ async function pickRecord(options, flags, context) {
575
+ if (flags.recordJson) {
576
+ return toRecordSelection(parseJsonObject(flags.recordJson, "--recordJson"));
577
+ }
578
+ if (flags.recordFile) {
579
+ return toRecordSelection(parseJsonObject(await readFile(resolveUserPath(flags.recordFile), "utf8"), "--recordFile"));
580
+ }
581
+ if (flags.cloudRecordJson && !flags.recordJson) {
582
+ return toRecordSelection(parseJsonObject(flags.cloudRecordJson, "--cloudRecordJson"));
583
+ }
584
+ if (flags.cloudRecordFile && !flags.recordFile) {
585
+ return toRecordSelection(parseJsonObject(await readFile(resolveUserPath(flags.cloudRecordFile), "utf8"), "--cloudRecordFile"));
586
+ }
587
+ const recordId = flags.recordId?.trim();
588
+ if (recordId) {
589
+ const found = options.find((item) => textOf(item.record_id) === recordId);
590
+ if (!found) {
591
+ throw new AnalysisError(`模块 ${context.moduleId} 未找到 record_id=${recordId} 的历史记录。${recordHint(options)}`);
592
+ }
593
+ return toRecordSelection(found);
594
+ }
595
+ if (options.length > 1) {
596
+ throw new AnalysisChoiceError(`模块 ${context.moduleId} 存在多个历史记录,请使用 \`--recordId\` 或 \`--recordJson\` 指定。\n${recordHint(options)}`, {
597
+ choices: options.map((item, index) => ({
598
+ index,
599
+ module_id: textOf(item.module_id),
600
+ module_name: textOf(item.module_name),
601
+ record_id: textOf(item.record_id),
602
+ record_name: textOf(item.record_name),
603
+ time: textOf(item.time),
604
+ })),
605
+ selection_type: "history_record",
606
+ });
607
+ }
608
+ if (options.length === 1)
609
+ return toRecordSelection(options[0]);
610
+ if (context.defaultRecord)
611
+ return context.defaultRecord;
612
+ throw new AnalysisError(`模块 ${context.moduleId} 的 XTRecord 字段 ${context.recordKey} 没有可用记录,请传入 \`--recordJson\`.`);
613
+ }
614
+ function toRecordSelection(record) {
615
+ const recordId = textOf(record.record_id);
616
+ if (!recordId) {
617
+ throw new AnalysisError("历史记录缺少 record_id,请检查传入的 JSON。");
618
+ }
619
+ return {
620
+ moduleId: textOf(record.module_id),
621
+ moduleName: textOf(record.module_name),
622
+ recordId,
623
+ recordName: textOf(record.record_name),
624
+ time: textOf(record.time),
625
+ };
626
+ }
627
+ function summarizeInput(input) {
628
+ if (input.kind === "cloud") {
629
+ return {
630
+ file_name: null,
631
+ file_oss: null,
632
+ file_size: null,
633
+ input_cloud: {
634
+ data_filter: readOptionValue(input.cloud.record.data_filter),
635
+ data_id: textOf(input.cloud.record.data_id),
636
+ data_process: readOptionValue(input.cloud.record.data_process),
637
+ key: input.cloud.key,
638
+ label: input.cloud.label,
639
+ record: structuredClone(input.cloud.record),
640
+ record_label: textOf(input.cloud.record.label),
641
+ },
642
+ input_record: null,
643
+ input_uploads: null,
644
+ };
645
+ }
646
+ if (input.kind === "none") {
647
+ return {
648
+ file_name: null,
649
+ file_oss: null,
650
+ file_size: null,
651
+ input_cloud: null,
652
+ input_record: null,
653
+ input_uploads: null,
654
+ };
655
+ }
656
+ if (input.kind === "record") {
657
+ return {
658
+ file_name: null,
659
+ file_oss: null,
660
+ file_size: null,
661
+ input_cloud: null,
662
+ input_record: {
663
+ label: input.record.label,
664
+ module_id: input.record.moduleId,
665
+ module_name: input.record.moduleName,
666
+ record_id: input.record.recordId,
667
+ record_name: input.record.recordName,
668
+ time: input.record.time,
669
+ },
670
+ input_uploads: null,
671
+ };
672
+ }
673
+ const uploads = input.uploads.map((item) => ({
674
+ file_name: item.upload.fileName,
675
+ file_oss: item.upload.fileOss,
676
+ file_size: item.upload.fileSize,
677
+ key: item.key,
678
+ label: item.label,
679
+ }));
680
+ const single = uploads.length === 1 ? uploads[0] : null;
681
+ return {
682
+ file_name: single?.file_name ?? null,
683
+ file_oss: single?.file_oss ?? null,
684
+ file_size: single?.file_size ?? null,
685
+ input_cloud: null,
686
+ input_record: null,
687
+ input_uploads: uploads,
688
+ };
689
+ }
690
+ function validateInputFlags(flags) {
691
+ const explicitCloud = !!(flags.cloudRecordJson || flags.cloudRecordFile);
692
+ const queriedCloud = !!(flags.cloudDataId?.trim() ||
693
+ flags.cloudFilter?.trim() ||
694
+ typeof flags.cloudIndex === "number");
695
+ const explicitRecord = !!(flags.recordJson || flags.recordFile);
696
+ const recordMode = explicitRecord || !!flags.recordKey?.trim() || !!flags.recordId?.trim();
697
+ if (flags.cloudRecordJson && flags.cloudRecordFile) {
698
+ throw new AnalysisError("`--cloudRecordJson` 与 `--cloudRecordFile` 不能同时使用。");
699
+ }
700
+ if (flags.recordJson && flags.recordFile) {
701
+ throw new AnalysisError("`--recordJson` 与 `--recordFile` 不能同时使用。");
702
+ }
703
+ if (explicitCloud && queriedCloud) {
704
+ throw new AnalysisError("显式云端记录参数不能与 `--cloudFilter` / `--cloudDataId` / `--cloudIndex` 同时使用。");
705
+ }
706
+ if (recordMode && queriedCloud) {
707
+ throw new AnalysisError("历史记录参数不能与 `--cloudFilter` / `--cloudDataId` / `--cloudIndex` 同时使用。");
708
+ }
709
+ if (explicitRecord && explicitCloud) {
710
+ throw new AnalysisError("`--recordJson` / `--recordFile` 不能与 `--cloudRecordJson` / `--cloudRecordFile` 同时使用。");
711
+ }
712
+ if (flags.demo && (explicitCloud || queriedCloud || recordMode)) {
713
+ throw new AnalysisError("`--demo` 不能与云端数据/历史记录参数同时使用。");
714
+ }
715
+ if (flags.files.length > 0 && (explicitCloud || queriedCloud || recordMode)) {
716
+ throw new AnalysisError("`--file` 不能与云端数据/历史记录参数同时使用。");
717
+ }
718
+ if (flags.demo && flags.files.length > 0) {
719
+ throw new AnalysisError("`--demo` 与 `--file` 不能同时使用。");
720
+ }
721
+ }
722
+ function validateDownloadFlags(flags) {
723
+ if (flags.downloadItem !== undefined && flags.downloadLabel) {
724
+ throw new AnalysisError("`--downloadItem` 与 `--downloadLabel` 不能同时使用。");
725
+ }
726
+ if ((flags.downloadItem !== undefined || flags.downloadLabel) && !flags.download) {
727
+ throw new AnalysisError("`--downloadItem` 和 `--downloadLabel` 仅可与 `--download` 一起使用。");
728
+ }
729
+ }
730
+ function cloudRequested(flags, fetched) {
731
+ const key = flags.cloudKey?.trim() || "";
732
+ const keyIsRecord = !!(key && findArgsDataType(fetched.args_data, key) === "XTRecord");
733
+ return !!(flags.cloudDataId?.trim() ||
734
+ flags.cloudFilter?.trim() ||
735
+ typeof flags.cloudIndex === "number" ||
736
+ (key && !keyIsRecord) ||
737
+ (flags.cloudRecordFile && !keyIsRecord) ||
738
+ (flags.cloudRecordJson && !keyIsRecord));
739
+ }
740
+ function recordRequested(flags) {
741
+ return !!(flags.recordKey?.trim() || flags.recordId?.trim() || flags.recordJson || flags.recordFile);
742
+ }
743
+ function hasDemo(fetched) {
744
+ if (!textOf(fetched.id.duid))
745
+ return false;
746
+ const fields = listUploadFields(fetched.args_data);
747
+ return fields.length > 0 && fields.every((field) => !!field.defaultUpload);
748
+ }
749
+ function hasRecord(fetched) {
750
+ return !!(textOf(fetched.id.duid) && findDefaultRecord(fetched.args_data));
751
+ }
752
+ function hasCloud(fetched) {
753
+ return !!findDefaultCloud(fetched.args_data);
754
+ }
755
+ function normalizeCloudRecord(record) {
756
+ const copy = {};
757
+ for (const [key, value] of Object.entries(record)) {
758
+ if (value !== undefined)
759
+ copy[key] = structuredClone(value);
760
+ }
761
+ if (!textOf(copy.type))
762
+ copy.type = "cloud";
763
+ return copy;
764
+ }
765
+ function parseJsonObject(raw, flag) {
766
+ let parsed;
767
+ try {
768
+ parsed = JSON.parse(raw);
769
+ }
770
+ catch {
771
+ throw new AnalysisError(`${flag} 不是合法 JSON。`);
772
+ }
773
+ if (!isJsonObject(parsed)) {
774
+ throw new AnalysisError(`${flag} 必须是 JSON 对象。`);
775
+ }
776
+ return parsed;
777
+ }
778
+ function parseFileInput(raw) {
779
+ const value = raw.trim();
780
+ if (!value)
781
+ throw new AnalysisError("`--file` 不能为空。");
782
+ const splitAt = value.indexOf("=");
783
+ if (splitAt <= 0)
784
+ return { path: value };
785
+ const key = value.slice(0, splitAt).trim();
786
+ const path = value.slice(splitAt + 1).trim();
787
+ if (!key || !path)
788
+ throw new AnalysisError("多上传模块请使用 `--file key=path`。");
789
+ return { key, path };
790
+ }
791
+ function requireRoute(menu) {
792
+ const route = textOf(menu.route);
793
+ if (!route) {
794
+ throw new AnalysisError("菜单未返回可用 route,请改用 `--route` 显式指定。");
795
+ }
796
+ return route;
797
+ }
798
+ function readOptionValue(value) {
799
+ const object = asObject(value);
800
+ const text = textOf(object?.value);
801
+ return text || null;
802
+ }
803
+ function pickByIndex(items, index) {
804
+ if (!Number.isInteger(index) || index < 1 || index > items.length) {
805
+ throw new AnalysisError(`下载项序号 ${index} 无效,可选范围为 1-${items.length}。\n${multipleDownloadsMessage(items)}`);
806
+ }
807
+ return items[index - 1];
808
+ }
809
+ function pickByLabel(items, label) {
810
+ const wanted = label.trim().toLowerCase();
811
+ if (!wanted)
812
+ return undefined;
813
+ const matched = items.filter((item) => item.label.trim().toLowerCase() === wanted);
814
+ if (matched.length === 1)
815
+ return matched[0];
816
+ if (matched.length > 1) {
817
+ throw downloadChoiceError(items, `下载项名称 ${label} 匹配到多个结果。分析已经完成,请改用 \`--downloadItem <n>\` 精确选择,不要重新检索工具。`);
818
+ }
819
+ return undefined;
820
+ }
821
+ function inferDevice(path) {
822
+ const ext = path?.trim().split(".").pop()?.toLowerCase();
823
+ return ext ? normalizeDevice(ext) : undefined;
824
+ }
825
+ function normalizeDevice(value) {
826
+ const device = value?.trim().toLowerCase();
827
+ if (!device)
828
+ return undefined;
829
+ if (device === "tif")
830
+ return "tiff";
831
+ if (device === "jpg")
832
+ return "jpeg";
833
+ return device;
834
+ }
835
+ function downloadChoiceError(items, lead) {
836
+ return new AnalysisChoiceError(`${lead}\n${multipleDownloadsMessage(items)}`, {
837
+ choices: items.map((item, index) => ({
838
+ device: item.device,
839
+ endpoint: item.endpoint,
840
+ fuid: item.fuid,
841
+ index: index + 1,
842
+ label: item.label,
843
+ })),
844
+ selection_type: "download",
845
+ });
846
+ }
847
+ function multipleDownloadsMessage(items) {
848
+ const lines = [
849
+ "当前结果包含多个可下载文件,请使用 `--downloadItem`、`--downloadLabel`、`--item`、`--label` 或带目标扩展名的路径明确选择。",
850
+ "可选下载项:",
851
+ ];
852
+ items.forEach((item, index) => {
853
+ lines.push(`${index + 1}. ${item.label} (device=${item.device}, endpoint=${item.endpoint})`);
854
+ });
855
+ return lines.join("\n");
856
+ }
857
+ function recordHint(options) {
858
+ if (options.length === 0)
859
+ return "";
860
+ const lines = ["可选历史记录:"];
861
+ options.forEach((item, index) => {
862
+ const id = textOf(item.record_id);
863
+ const name = [textOf(item.record_name), textOf(item.time)].filter(Boolean).join(" ");
864
+ lines.push(` [${index}] ${id}${name ? ` (${name})` : ""}`);
865
+ });
866
+ return lines.join("\n");
867
+ }
868
+ function cloudSelectionHint(records, filter) {
869
+ const lines = [
870
+ filter ? `匹配到多个云端数据: ${filter}` : "匹配到多个云端数据",
871
+ "请使用 `--cloudIndex` 或 `--cloudDataId` 指定其中一条:",
872
+ ];
873
+ records.forEach((record, index) => {
874
+ const id = textOf(record.data_id);
875
+ const label = textOf(record.label);
876
+ lines.push(` [${index}] ${label || id}`);
877
+ });
878
+ return lines.join("\n");
879
+ }
880
+ function toHistorySummary(record, moduleId) {
881
+ const fuid = textOf(record.fuid);
882
+ if (!fuid)
883
+ return null;
884
+ return {
885
+ fuid,
886
+ id: typeof record.id === "number" ? record.id : null,
887
+ module_id: textOf(record.module_id) || moduleId,
888
+ module_name: textOf(record.module_name),
889
+ name: textOf(record.name),
890
+ status: historyStatus(record),
891
+ time: textOf(record.time),
892
+ type: textOf(record.type),
893
+ };
894
+ }
895
+ async function waitForLatestHistory(moduleId, submittedAt, timeoutMs) {
896
+ const deadline = Date.now() + timeoutMs;
897
+ const earliest = submittedAt - 120_000;
898
+ while (Date.now() <= deadline) {
899
+ const page = await fetchHistoryPage(1);
900
+ const match = page.records
901
+ .filter((record) => textOf(record.module_id) === moduleId)
902
+ .sort((left, right) => parseTime(right.time) - parseTime(left.time))
903
+ .find((record) => parseTime(record.time) >= earliest);
904
+ if (match)
905
+ return match;
906
+ await sleep(3000);
907
+ }
908
+ return null;
909
+ }
910
+ async function waitForHistoryCompletion(fuid, timeoutMs) {
911
+ const deadline = Date.now() + timeoutMs;
912
+ while (Date.now() <= deadline) {
913
+ const record = await findHistoryByFuid(fuid);
914
+ if (!record) {
915
+ await sleep(3000);
916
+ continue;
917
+ }
918
+ const status = historyStatus(record);
919
+ if (isHistoryFinished(status))
920
+ return record;
921
+ if (isHistoryFailed(status)) {
922
+ throw new AnalysisError(`历史记录任务执行失败:${status}`);
923
+ }
924
+ await sleep(3000);
925
+ }
926
+ return null;
927
+ }
928
+ function parseTime(value) {
929
+ const text = textOf(value);
930
+ if (!text)
931
+ return 0;
932
+ const parsed = Date.parse(text.replace(" ", "T"));
933
+ return Number.isNaN(parsed) ? 0 : parsed;
934
+ }
935
+ function isRetryableDownload(error) {
936
+ return error.message.includes("下载失败") || error.message.includes("未获取到下载链接");
937
+ }
938
+ function sleep(ms) {
939
+ return new Promise((resolve) => setTimeout(resolve, ms));
940
+ }
941
+ //# sourceMappingURL=prepare.js.map