@helixlife-ai/xiantao 0.1.17 → 0.1.19
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/README.md +22 -2
- package/dist/commands/history/download.js +54 -4
- package/dist/commands/plot/run.js +223 -65
- package/dist/commands/tool/exec.js +6 -11
- package/dist/commands/tool/inspect.js +50 -19
- package/dist/commands/tool/prepare.js +11 -14
- package/dist/lib/tool-inputs.js +51 -0
- package/dist/lib/xiantao-plot.js +35 -19
- package/dist/xtz-help.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,9 +37,27 @@ This starts a menu-driven path for choosing a tool, selecting demo data or a loc
|
|
|
37
37
|
xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json
|
|
38
38
|
xt tool exec violin_flat --file ./data.xlsx --profile opencode --json
|
|
39
39
|
xt tool exec violin_flat --file ./data.xlsx --profile opencode --json --download ./violin.pdf
|
|
40
|
+
|
|
41
|
+
xt tool exec gsea_analysis --demo --profile opencode --json
|
|
42
|
+
xt tool prepare gsea_plot --profile opencode --json
|
|
43
|
+
xt tool exec gsea_plot --profile opencode --json --download ./gsea_plot.pdf
|
|
44
|
+
xt tool exec gsea_customize --file gene_set=./gene_set.gmt --file data=./rank.xlsx --profile opencode --json
|
|
40
45
|
```
|
|
41
46
|
|
|
42
|
-
For machine callers, use `xt tool prepare` to fetch the
|
|
47
|
+
For machine callers, use `xt tool prepare` to fetch the final schema, then `xt tool exec` to submit. Keep `--json`, pass an explicit profile with `--profile`, and avoid interactive-only flows.
|
|
48
|
+
The examples below illustrate command shapes, not a fixed allowlist of tools. Apply the same patterns to any tool with the same input mode.
|
|
49
|
+
|
|
50
|
+
`xt tool prepare` and `xt tool exec` support three input patterns:
|
|
51
|
+
- upload modules: pass `--file <xlsx>`
|
|
52
|
+
- multi-upload modules: repeat `--file key=path`, for example `--file gene_set=./gene_set.gmt --file data=./rank.xlsx`
|
|
53
|
+
- demo-capable upload modules: pass `--demo`
|
|
54
|
+
- history-record modules such as `gsea_plot`: pass neither `--file` nor `--demo`; the CLI reuses the default `XTRecord` returned by `tool inspect` / `tool prepare`
|
|
55
|
+
|
|
56
|
+
Current operational notes for agent-style callers:
|
|
57
|
+
- serialize `xt tool exec` submits per profile; Xiantao may reject overlapping or too-frequent submits with `NETWORK_ERROR`
|
|
58
|
+
- `tool inspect` now includes the full `args_data` payload in JSON output and summarizes every detected `XTUpload` field
|
|
59
|
+
- for multi-upload modules, pass keyed uploads in the same field names returned by `tool inspect`
|
|
60
|
+
- `gsea_customize` has been validated through `tool prepare` and `tool exec` with repeated `--file gene_set=... --file data=...`
|
|
43
61
|
|
|
44
62
|
For agent workflows, do not use `xt tool run`. Treat `xt tool run` as the human-first path and `xt tool exec` as the machine path.
|
|
45
63
|
|
|
@@ -84,7 +102,9 @@ For the common Xiantao tool product, `tool search`, `tool inspect`, `tool run`,
|
|
|
84
102
|
|
|
85
103
|
The CLI also loads a project-root `.env` file on startup. You can set `XIANTAO_BASE_URL=https://agent.helixlife.cn` there to switch the API base URL without prefixing each command manually.
|
|
86
104
|
|
|
87
|
-
`xt tool search <keyword>` searches remote tool menus by tool id, title, path, and route. `xt tool inspect <tool_id>` prints resolved metadata together with the dynamic `args_main` schema
|
|
105
|
+
`xt tool search <keyword>` searches remote tool menus by tool id, title, path, and route. `xt tool inspect <tool_id>` prints resolved metadata together with the dynamic `args_main` schema, the full `args_data` payload in JSON mode, and the detected input source summary, including per-field `XTUpload` defaults and `XTRecord` defaults. `xt tool run <tool_id>` runs a Xiantao bioinformatics tool in the human-first path. `xt tool exec <tool_id>` is the agent-safe submit command and also supports `--download <path>` when a local artifact is needed. `xt tool resolve <tool_id>` remains available as the low-level UUID and route lookup. `xt tool menus` returns a concise catalog with each tool title, `module_id`, `path`, and `route`, which is useful when `tool search` finds nothing helpful or when you want to shortlist tools for recommendation. `--demo` reuses the demo file metadata returned by `tool fetch-all`; `--file` uploads a local file first; multi-upload modules accept repeated `--file key=path`; for `XTRecord` modules the CLI can submit directly from the default history record returned by `fetch-all`. Run `xt login` first to authorize the stored token. When a tool exposes multiple downloadable results, `--download <path>` uses the output file extension to select the matching artifact.
|
|
106
|
+
|
|
107
|
+
For the current GSEA tool family, `gsea_plot`, `gsea_plot2`, `gsea_bar`, `gsea_point`, `gsea_ridge`, `gsea_emapplot`, `gsea_tree`, and `gsea_path_cluster` are record-backed visual modules that reuse the default `gsea_analysis` history record. `gsea_analysis` itself is an upload/demo analysis module. `gsea_customize` is a custom gene-set upload module whose frontend expects two uploads, `gene_set` and `data`; submit it with `--file gene_set=... --file data=...`. A real CLI submit path has been verified with the sample files in [`docs/GSEA-基因集.csv`](/Users/chenyunjie/Desktop/xtz-cli/xiantao/docs/GSEA-基因集.csv) and [`docs/GSEA-分子.xlsx`](/Users/chenyunjie/Desktop/xtz-cli/xiantao/docs/GSEA-分子.xlsx).
|
|
88
108
|
|
|
89
109
|
`xt tool run <tool_id> ... --interactive` prompts for dynamic `args_main` values before the final submit, prints reusable `--set` flags, and supports `<` for the previous item plus `/skip` for the current section.
|
|
90
110
|
|
|
@@ -20,6 +20,12 @@ export default class HistoryDownload extends XtzCommand {
|
|
|
20
20
|
device: Flags.string({
|
|
21
21
|
description: 'Preferred device/format, e.g. pdf, xlsx, zip',
|
|
22
22
|
}),
|
|
23
|
+
item: Flags.integer({
|
|
24
|
+
description: 'Select a download item by its 1-based index from the listed results',
|
|
25
|
+
}),
|
|
26
|
+
label: Flags.string({
|
|
27
|
+
description: 'Select a download item by its label',
|
|
28
|
+
}),
|
|
23
29
|
output: Flags.string({
|
|
24
30
|
char: 'o',
|
|
25
31
|
description: 'Destination path; defaults to ~/Downloads/<file name>',
|
|
@@ -33,13 +39,21 @@ export default class HistoryDownload extends XtzCommand {
|
|
|
33
39
|
const agent = await this.getProfile(flags.profile);
|
|
34
40
|
const auth = await getAuthContext(agent, flags.token);
|
|
35
41
|
const record = await findXiantaoHistoryRecordByFuid(auth, args.fuid);
|
|
42
|
+
if (flags.item !== undefined && flags.label) {
|
|
43
|
+
throw new XtzError('`--item` 与 `--label` 不能同时使用。');
|
|
44
|
+
}
|
|
36
45
|
if (!record?.module_id?.trim()) {
|
|
37
46
|
throw new XtzError(`未找到 fuid 为 ${args.fuid} 的历史记录。`);
|
|
38
47
|
}
|
|
39
48
|
const result = await fetchXiantaoHistoryDownloadButtons(auth, { fuid: args.fuid });
|
|
40
|
-
const item = this.pickDownloadItem(result.downloads,
|
|
49
|
+
const item = this.pickDownloadItem(result.downloads, {
|
|
50
|
+
device: flags.device,
|
|
51
|
+
item: flags.item,
|
|
52
|
+
label: flags.label,
|
|
53
|
+
outputPath: flags.output,
|
|
54
|
+
});
|
|
41
55
|
if (!item) {
|
|
42
|
-
throw new XtzError(
|
|
56
|
+
throw new XtzError(this.buildMultipleDownloadsMessage(result.downloads));
|
|
43
57
|
}
|
|
44
58
|
const resolved = await resolveXiantaoDownload(auth, {
|
|
45
59
|
item,
|
|
@@ -62,8 +76,14 @@ export default class HistoryDownload extends XtzCommand {
|
|
|
62
76
|
profile: agent,
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
|
-
pickDownloadItem(downloads,
|
|
66
|
-
|
|
79
|
+
pickDownloadItem(downloads, input) {
|
|
80
|
+
if (input.item !== undefined) {
|
|
81
|
+
return this.pickDownloadItemByIndex(downloads, input.item);
|
|
82
|
+
}
|
|
83
|
+
if (input.label) {
|
|
84
|
+
return this.pickDownloadItemByLabel(downloads, input.label);
|
|
85
|
+
}
|
|
86
|
+
const normalizedExpectedDevice = this.normalizeDevice(input.device ?? this.inferDeviceFromOutputPath(input.outputPath));
|
|
67
87
|
if (normalizedExpectedDevice) {
|
|
68
88
|
const matched = downloads.filter((item) => this.normalizeDevice(item.device) === normalizedExpectedDevice);
|
|
69
89
|
if (matched.length === 1) {
|
|
@@ -79,6 +99,26 @@ export default class HistoryDownload extends XtzCommand {
|
|
|
79
99
|
}
|
|
80
100
|
return undefined;
|
|
81
101
|
}
|
|
102
|
+
pickDownloadItemByIndex(downloads, itemIndex) {
|
|
103
|
+
if (!Number.isInteger(itemIndex) || itemIndex < 1 || itemIndex > downloads.length) {
|
|
104
|
+
throw new XtzError(`下载项序号 ${itemIndex} 无效,可选范围为 1-${downloads.length}。\n${this.buildMultipleDownloadsMessage(downloads)}`);
|
|
105
|
+
}
|
|
106
|
+
return downloads[itemIndex - 1];
|
|
107
|
+
}
|
|
108
|
+
pickDownloadItemByLabel(downloads, expectedLabel) {
|
|
109
|
+
const normalizedExpectedLabel = expectedLabel.trim().toLowerCase();
|
|
110
|
+
if (!normalizedExpectedLabel) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const matched = downloads.filter((item) => item.label.trim().toLowerCase() === normalizedExpectedLabel);
|
|
114
|
+
if (matched.length === 1) {
|
|
115
|
+
return matched[0];
|
|
116
|
+
}
|
|
117
|
+
if (matched.length > 1) {
|
|
118
|
+
throw new XtzError(`下载项名称 ${expectedLabel} 匹配到多个结果,请改用 \`--item\` 精确选择。\n${this.buildMultipleDownloadsMessage(downloads)}`);
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
82
122
|
inferDeviceFromOutputPath(outputPath) {
|
|
83
123
|
const extension = outputPath?.trim().split('.').pop()?.toLowerCase();
|
|
84
124
|
return extension ? this.normalizeDevice(extension) : undefined;
|
|
@@ -96,4 +136,14 @@ export default class HistoryDownload extends XtzCommand {
|
|
|
96
136
|
}
|
|
97
137
|
return normalized;
|
|
98
138
|
}
|
|
139
|
+
buildMultipleDownloadsMessage(downloads) {
|
|
140
|
+
const lines = [
|
|
141
|
+
'当前历史记录返回多个可下载结果,请传入 `--item`、`--label`、`--device`,或将 `--output` 改为带目标扩展名的文件名。',
|
|
142
|
+
'可选下载项:',
|
|
143
|
+
];
|
|
144
|
+
downloads.forEach((item, index) => {
|
|
145
|
+
lines.push(`${index + 1}. ${item.label} (device=${item.device}, endpoint=${item.endpoint})`);
|
|
146
|
+
});
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
99
149
|
}
|
|
@@ -6,16 +6,26 @@ import { downloadToPath } from '../../lib/download.js';
|
|
|
6
6
|
import { XtzError } from '../../lib/errors.js';
|
|
7
7
|
import { globalFlags } from '../../lib/flags.js';
|
|
8
8
|
import { fetchXiantaoHistoryDownloadButtons, getXiantaoHistoryRecordStatus, waitForLatestXiantaoHistoryRecord, waitForXiantaoHistoryRecordCompletion, } from '../../lib/history.js';
|
|
9
|
-
import { deepMerge
|
|
9
|
+
import { deepMerge } from '../../lib/json.js';
|
|
10
10
|
import { confirmPlotArgsMain, promptPlotArgsMain, promptPlotFilePath, promptPlotInputSource, promptPlotMenuSelection, promptPlotResultSelection, } from '../../lib/plot-interactive.js';
|
|
11
11
|
import { uploadFile } from '../../lib/plot.js';
|
|
12
12
|
import { parseSetOverrides, plotOverrideFlags } from '../../lib/plot-options.js';
|
|
13
13
|
import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
|
|
14
14
|
import { loadPlotArgsMainConfig, resolveXiantaoToolProductUuid } from '../../lib/storage.js';
|
|
15
|
-
import {
|
|
15
|
+
import { findDefaultXiantaoRecord, listXiantaoUploadFields } from '../../lib/tool-inputs.js';
|
|
16
|
+
import { resolveXiantaoDownload, submitXiantaoPlot, verifyXiantaoPlotUpload, } from '../../lib/xiantao-plot.js';
|
|
16
17
|
import { fetchAllPlotArgs, fetchToolMenu, fetchToolMenus, resolveToolMenuByCode } from '../../lib/xiantao.js';
|
|
17
18
|
export class ToolRunCommandBase extends XtzCommand {
|
|
18
|
-
|
|
19
|
+
resolveToolFileInputs(positionalFile, flagFiles) {
|
|
20
|
+
if (positionalFile && flagFiles && flagFiles.length > 0) {
|
|
21
|
+
throw new XtzError('FILE 与 `--file` 不能同时使用。');
|
|
22
|
+
}
|
|
23
|
+
if (positionalFile) {
|
|
24
|
+
return [positionalFile];
|
|
25
|
+
}
|
|
26
|
+
return flagFiles ?? [];
|
|
27
|
+
}
|
|
28
|
+
async prepareToolRun(requestedModuleId, fileInputs, flags) {
|
|
19
29
|
const agent = await this.getProfile(flags.profile);
|
|
20
30
|
const auth = await getAuthContext(agent, flags.token);
|
|
21
31
|
const resolved = await this.resolveModule(requestedModuleId, flags, auth);
|
|
@@ -23,41 +33,45 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
23
33
|
const fetchAllResult = await fetchAllPlotArgs(auth, { moduleId, uuid });
|
|
24
34
|
const inputMode = await this.resolveInputMode(fetchAllResult, {
|
|
25
35
|
demo: flags.demo,
|
|
26
|
-
|
|
36
|
+
fileInputs,
|
|
27
37
|
interactive: flags.interactive,
|
|
28
38
|
});
|
|
29
39
|
const configArgsMain = await loadPlotArgsMainConfig(moduleId, flags.config);
|
|
30
40
|
const overrides = deepMerge(configArgsMain, parseSetOverrides(flags.set));
|
|
31
41
|
let schema = fetchAllResult.args_main ?? [];
|
|
32
42
|
let argsMain = deepMerge(buildArgsMainFromSchema(schema), overrides);
|
|
33
|
-
const
|
|
43
|
+
const preparedInput = inputMode.mode === 'demo'
|
|
34
44
|
? this.resolveDemoInput(fetchAllResult, moduleId)
|
|
35
|
-
:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
: inputMode.mode === 'record'
|
|
46
|
+
? this.resolveRecordInput(fetchAllResult, moduleId)
|
|
47
|
+
: await this.uploadAndVerify(agent, inputMode.fileInputs ?? [], {
|
|
48
|
+
argsMain,
|
|
49
|
+
argsData: fetchAllResult.args_data ?? [],
|
|
50
|
+
moduleId,
|
|
51
|
+
uuid,
|
|
52
|
+
}, auth);
|
|
53
|
+
if (preparedInput.schema.length) {
|
|
54
|
+
schema = preparedInput.schema;
|
|
42
55
|
argsMain = deepMerge(buildArgsMainFromSchema(schema), overrides);
|
|
43
56
|
}
|
|
44
57
|
return {
|
|
45
58
|
agent,
|
|
46
59
|
argsMain,
|
|
47
60
|
auth,
|
|
48
|
-
duid,
|
|
61
|
+
duid: preparedInput.duid,
|
|
62
|
+
input: preparedInput.input,
|
|
49
63
|
inputMode: inputMode.mode,
|
|
64
|
+
inputSummary: this.summarizePreparedInput(preparedInput.input),
|
|
50
65
|
moduleId,
|
|
51
66
|
notice: fetchAllResult.notice ?? '',
|
|
52
67
|
route,
|
|
53
68
|
schema,
|
|
54
69
|
toolProductUuid,
|
|
55
|
-
upload,
|
|
56
70
|
uuid,
|
|
57
71
|
};
|
|
58
72
|
}
|
|
59
|
-
async executeToolRun(requestedModuleId,
|
|
60
|
-
const prepared = await this.prepareToolRun(requestedModuleId,
|
|
73
|
+
async executeToolRun(requestedModuleId, fileInputs, flags) {
|
|
74
|
+
const prepared = await this.prepareToolRun(requestedModuleId, fileInputs, flags);
|
|
61
75
|
let argsMain = prepared.argsMain;
|
|
62
76
|
if (flags.interactive) {
|
|
63
77
|
argsMain = await promptPlotArgsMain(prepared.schema ?? [], argsMain);
|
|
@@ -67,9 +81,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
67
81
|
const result = await submitXiantaoPlot(prepared.auth, {
|
|
68
82
|
argsMain,
|
|
69
83
|
duid: prepared.duid,
|
|
84
|
+
input: prepared.input,
|
|
70
85
|
moduleId: prepared.moduleId,
|
|
71
86
|
route: prepared.route,
|
|
72
|
-
upload: prepared.upload,
|
|
73
87
|
uuid: prepared.uuid,
|
|
74
88
|
});
|
|
75
89
|
if (result.asyncSubmission) {
|
|
@@ -105,11 +119,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
105
119
|
downloads: result.downloads,
|
|
106
120
|
duid: prepared.duid,
|
|
107
121
|
file: result.file ?? null,
|
|
108
|
-
file_name: prepared.upload.fileName,
|
|
109
|
-
file_oss: prepared.upload.fileOss,
|
|
110
|
-
file_size: prepared.upload.fileSize,
|
|
111
122
|
history: null,
|
|
112
123
|
history_hint: null,
|
|
124
|
+
...prepared.inputSummary,
|
|
113
125
|
mode: prepared.inputMode,
|
|
114
126
|
module_id: prepared.moduleId,
|
|
115
127
|
notice: prepared.notice,
|
|
@@ -141,11 +153,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
141
153
|
downloads: [],
|
|
142
154
|
duid: prepared.duid,
|
|
143
155
|
file: null,
|
|
144
|
-
file_name: prepared.upload.fileName,
|
|
145
|
-
file_oss: prepared.upload.fileOss,
|
|
146
|
-
file_size: prepared.upload.fileSize,
|
|
147
156
|
history: recordedHistory,
|
|
148
157
|
history_hint: this.buildHistoryHint(recordedHistory),
|
|
158
|
+
...prepared.inputSummary,
|
|
149
159
|
mode: prepared.inputMode,
|
|
150
160
|
module_id: prepared.moduleId,
|
|
151
161
|
notice: prepared.notice,
|
|
@@ -169,11 +179,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
169
179
|
downloads: [],
|
|
170
180
|
duid: prepared.duid,
|
|
171
181
|
file: null,
|
|
172
|
-
file_name: prepared.upload.fileName,
|
|
173
|
-
file_oss: prepared.upload.fileOss,
|
|
174
|
-
file_size: prepared.upload.fileSize,
|
|
175
182
|
history: recordedHistory,
|
|
176
183
|
history_hint: this.buildHistoryHint(recordedHistory),
|
|
184
|
+
...prepared.inputSummary,
|
|
177
185
|
mode: prepared.inputMode,
|
|
178
186
|
module_id: prepared.moduleId,
|
|
179
187
|
notice: prepared.notice,
|
|
@@ -203,11 +211,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
203
211
|
downloads: [],
|
|
204
212
|
duid: prepared.duid,
|
|
205
213
|
file: null,
|
|
206
|
-
file_name: prepared.upload.fileName,
|
|
207
|
-
file_oss: prepared.upload.fileOss,
|
|
208
|
-
file_size: prepared.upload.fileSize,
|
|
209
214
|
history: finalHistory,
|
|
210
215
|
history_hint: this.buildHistoryHint(finalHistory),
|
|
216
|
+
...prepared.inputSummary,
|
|
211
217
|
mode: prepared.inputMode,
|
|
212
218
|
module_id: prepared.moduleId,
|
|
213
219
|
notice: prepared.notice,
|
|
@@ -235,11 +241,9 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
235
241
|
downloads,
|
|
236
242
|
duid: prepared.duid,
|
|
237
243
|
file: null,
|
|
238
|
-
file_name: prepared.upload.fileName,
|
|
239
|
-
file_oss: prepared.upload.fileOss,
|
|
240
|
-
file_size: prepared.upload.fileSize,
|
|
241
244
|
history: finalHistory,
|
|
242
245
|
history_hint: this.buildHistoryHint(finalHistory),
|
|
246
|
+
...prepared.inputSummary,
|
|
243
247
|
mode: prepared.inputMode,
|
|
244
248
|
module_id: prepared.moduleId,
|
|
245
249
|
notice: prepared.notice,
|
|
@@ -431,18 +435,21 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
431
435
|
if (input.demo) {
|
|
432
436
|
return { mode: 'demo' };
|
|
433
437
|
}
|
|
434
|
-
if (input.
|
|
438
|
+
if (input.fileInputs.length > 0) {
|
|
435
439
|
return {
|
|
436
|
-
|
|
440
|
+
fileInputs: input.fileInputs,
|
|
437
441
|
mode: 'upload',
|
|
438
442
|
};
|
|
439
443
|
}
|
|
444
|
+
if (this.hasRecordInput(fetchAllResult)) {
|
|
445
|
+
return { mode: 'record' };
|
|
446
|
+
}
|
|
440
447
|
if (!input.interactive) {
|
|
441
448
|
return { mode: 'upload' };
|
|
442
449
|
}
|
|
443
450
|
if (!this.hasDemoInput(fetchAllResult)) {
|
|
444
451
|
return {
|
|
445
|
-
|
|
452
|
+
fileInputs: [await promptPlotFilePath()],
|
|
446
453
|
mode: 'upload',
|
|
447
454
|
};
|
|
448
455
|
}
|
|
@@ -451,7 +458,7 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
451
458
|
return { mode };
|
|
452
459
|
}
|
|
453
460
|
return {
|
|
454
|
-
|
|
461
|
+
fileInputs: [await promptPlotFilePath()],
|
|
455
462
|
mode,
|
|
456
463
|
};
|
|
457
464
|
}
|
|
@@ -460,22 +467,25 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
460
467
|
if (!duid) {
|
|
461
468
|
throw new XtzError(`模块 ${moduleId} 未返回 demo duid,无法使用 \`--demo\`。`);
|
|
462
469
|
}
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
|
|
466
|
-
const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
|
|
467
|
-
const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
|
|
468
|
-
if (!fileName || !fileOss || typeof fileSize !== 'number') {
|
|
470
|
+
const uploadFields = listXiantaoUploadFields(fetchAllResult.args_data ?? []);
|
|
471
|
+
if (!uploadFields.length || uploadFields.some((field) => !field.defaultUpload)) {
|
|
469
472
|
throw new XtzError(`模块 ${moduleId} 未返回可用的 demo 文件信息,无法使用 \`--demo\`。`);
|
|
470
473
|
}
|
|
471
474
|
return {
|
|
472
475
|
duid,
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
476
|
+
input: {
|
|
477
|
+
kind: 'upload',
|
|
478
|
+
uploads: uploadFields.map((field) => ({
|
|
479
|
+
key: field.key,
|
|
480
|
+
label: field.label,
|
|
481
|
+
upload: {
|
|
482
|
+
fileName: field.defaultUpload.fileName,
|
|
483
|
+
fileOss: field.defaultUpload.fileOss,
|
|
484
|
+
fileSize: field.defaultUpload.fileSize,
|
|
485
|
+
},
|
|
486
|
+
})),
|
|
478
487
|
},
|
|
488
|
+
schema: [],
|
|
479
489
|
};
|
|
480
490
|
}
|
|
481
491
|
hasDemoInput(fetchAllResult) {
|
|
@@ -483,31 +493,180 @@ export class ToolRunCommandBase extends XtzCommand {
|
|
|
483
493
|
if (!duid) {
|
|
484
494
|
return false;
|
|
485
495
|
}
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
|
|
489
|
-
const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
|
|
490
|
-
const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
|
|
491
|
-
return Boolean(fileName && fileOss && typeof fileSize === 'number');
|
|
496
|
+
const uploadFields = listXiantaoUploadFields(fetchAllResult.args_data ?? []);
|
|
497
|
+
return uploadFields.length > 0 && uploadFields.every((field) => Boolean(field.defaultUpload));
|
|
492
498
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
499
|
+
resolveRecordInput(fetchAllResult, moduleId) {
|
|
500
|
+
const duid = fetchAllResult.id?.duid?.trim();
|
|
501
|
+
if (!duid) {
|
|
502
|
+
throw new XtzError(`模块 ${moduleId} 未返回 record duid,无法使用历史记录输入。`);
|
|
496
503
|
}
|
|
497
|
-
const
|
|
498
|
-
|
|
504
|
+
const record = findDefaultXiantaoRecord(fetchAllResult.args_data ?? []);
|
|
505
|
+
if (!record) {
|
|
506
|
+
throw new XtzError(`模块 ${moduleId} 未返回可用的数据记录,无法直接运行。`);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
duid,
|
|
510
|
+
input: {
|
|
511
|
+
kind: 'record',
|
|
512
|
+
record: {
|
|
513
|
+
key: record.key,
|
|
514
|
+
label: record.label,
|
|
515
|
+
moduleId: record.moduleId,
|
|
516
|
+
moduleName: record.moduleName,
|
|
517
|
+
recordId: record.recordId,
|
|
518
|
+
recordName: record.recordName,
|
|
519
|
+
time: record.time,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
schema: [],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
hasRecordInput(fetchAllResult) {
|
|
526
|
+
return Boolean(fetchAllResult.id?.duid?.trim() && findDefaultXiantaoRecord(fetchAllResult.args_data ?? []));
|
|
527
|
+
}
|
|
528
|
+
async uploadAndVerify(agent, fileInputs, input, auth) {
|
|
529
|
+
const uploads = await this.uploadFiles(agent, fileInputs, input.argsData ?? [], input.moduleId);
|
|
499
530
|
const verified = await verifyXiantaoPlotUpload(auth, {
|
|
500
531
|
argsMain: input.argsMain,
|
|
501
532
|
moduleId: input.moduleId,
|
|
502
|
-
|
|
533
|
+
uploads,
|
|
503
534
|
uuid: input.uuid,
|
|
504
535
|
});
|
|
505
536
|
return {
|
|
506
537
|
duid: verified.duid,
|
|
538
|
+
input: {
|
|
539
|
+
kind: 'upload',
|
|
540
|
+
uploads,
|
|
541
|
+
},
|
|
507
542
|
schema: verified.schema,
|
|
508
|
-
upload,
|
|
509
543
|
};
|
|
510
544
|
}
|
|
545
|
+
summarizePreparedInput(input) {
|
|
546
|
+
if (input.kind === 'record') {
|
|
547
|
+
return {
|
|
548
|
+
file_name: null,
|
|
549
|
+
file_oss: null,
|
|
550
|
+
file_size: null,
|
|
551
|
+
input_record: {
|
|
552
|
+
label: input.record.label,
|
|
553
|
+
module_id: input.record.moduleId,
|
|
554
|
+
module_name: input.record.moduleName,
|
|
555
|
+
record_id: input.record.recordId,
|
|
556
|
+
record_name: input.record.recordName,
|
|
557
|
+
time: input.record.time,
|
|
558
|
+
},
|
|
559
|
+
input_uploads: null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const inputUploads = input.uploads.map((item) => ({
|
|
563
|
+
file_name: item.upload.fileName,
|
|
564
|
+
file_oss: item.upload.fileOss,
|
|
565
|
+
file_size: item.upload.fileSize,
|
|
566
|
+
key: item.key,
|
|
567
|
+
label: item.label,
|
|
568
|
+
}));
|
|
569
|
+
const singularUpload = inputUploads.length === 1 ? inputUploads[0] : null;
|
|
570
|
+
return {
|
|
571
|
+
file_name: singularUpload?.file_name ?? null,
|
|
572
|
+
file_oss: singularUpload?.file_oss ?? null,
|
|
573
|
+
file_size: singularUpload?.file_size ?? null,
|
|
574
|
+
input_record: null,
|
|
575
|
+
input_uploads: inputUploads,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async uploadFiles(agent, fileInputs, argsData, moduleId) {
|
|
579
|
+
const uploadFields = listXiantaoUploadFields(argsData ?? []);
|
|
580
|
+
const uploadSpecs = this.resolveUploadInputs(fileInputs, uploadFields, moduleId);
|
|
581
|
+
const uploadAuth = await getAuthContext(agent);
|
|
582
|
+
const uploads = [];
|
|
583
|
+
for (const item of uploadSpecs) {
|
|
584
|
+
const upload = await uploadFile(uploadAuth, item.filePath);
|
|
585
|
+
uploads.push({
|
|
586
|
+
key: item.key,
|
|
587
|
+
label: item.label,
|
|
588
|
+
upload,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return uploads;
|
|
592
|
+
}
|
|
593
|
+
resolveUploadInputs(fileInputs, uploadFields, moduleId) {
|
|
594
|
+
if (!uploadFields.length) {
|
|
595
|
+
throw new XtzError(`模块 ${moduleId} 未检测到可上传输入,不能使用 \`--file\`。`);
|
|
596
|
+
}
|
|
597
|
+
if (!fileInputs.length) {
|
|
598
|
+
if (uploadFields.length === 1) {
|
|
599
|
+
throw new XtzError('通用模式请传入 FILE / `--file <xlsx>`,或改用 `--demo`。');
|
|
600
|
+
}
|
|
601
|
+
throw new XtzError(`模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${uploadFields.map((field) => field.key).join(', ')}`);
|
|
602
|
+
}
|
|
603
|
+
const [singleUploadField] = uploadFields;
|
|
604
|
+
if (uploadFields.length === 1 && fileInputs.length === 1) {
|
|
605
|
+
const parsed = this.parseUploadFileInput(fileInputs[0]);
|
|
606
|
+
if (parsed.key && parsed.key !== singleUploadField.key) {
|
|
607
|
+
throw new XtzError(`模块 ${moduleId} 仅支持上传字段 ${singleUploadField.key},收到 ${parsed.key}。`);
|
|
608
|
+
}
|
|
609
|
+
return [{
|
|
610
|
+
filePath: parsed.path,
|
|
611
|
+
key: singleUploadField.key,
|
|
612
|
+
label: singleUploadField.label,
|
|
613
|
+
}];
|
|
614
|
+
}
|
|
615
|
+
const fieldsByKey = new Map(uploadFields.map((field) => [field.key, field]));
|
|
616
|
+
const assignments = new Map();
|
|
617
|
+
for (const rawValue of fileInputs) {
|
|
618
|
+
const parsed = this.parseUploadFileInput(rawValue);
|
|
619
|
+
if (!parsed.key) {
|
|
620
|
+
if (uploadFields.length === 1) {
|
|
621
|
+
if (assignments.has(singleUploadField.key)) {
|
|
622
|
+
throw new XtzError(`模块 ${moduleId} 仅需要 1 个上传文件,收到重复输入。`);
|
|
623
|
+
}
|
|
624
|
+
assignments.set(singleUploadField.key, parsed.path);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
throw new XtzError(`模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${uploadFields.map((field) => field.key).join(', ')}`);
|
|
628
|
+
}
|
|
629
|
+
const field = fieldsByKey.get(parsed.key);
|
|
630
|
+
if (!field) {
|
|
631
|
+
throw new XtzError(`模块 ${moduleId} 不存在上传字段 ${parsed.key}。可用字段: ${uploadFields.map((item) => item.key).join(', ')}`);
|
|
632
|
+
}
|
|
633
|
+
if (assignments.has(field.key)) {
|
|
634
|
+
throw new XtzError(`上传字段 ${field.key} 重复传入。`);
|
|
635
|
+
}
|
|
636
|
+
assignments.set(field.key, parsed.path);
|
|
637
|
+
}
|
|
638
|
+
const missingFields = uploadFields.filter((field) => field.required && !assignments.has(field.key));
|
|
639
|
+
if (missingFields.length > 0) {
|
|
640
|
+
throw new XtzError(`模块 ${moduleId} 缺少必填上传字段: ${missingFields.map((field) => field.key).join(', ')}`);
|
|
641
|
+
}
|
|
642
|
+
return uploadFields.flatMap((field) => {
|
|
643
|
+
const filePath = assignments.get(field.key);
|
|
644
|
+
if (!filePath) {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
return [{
|
|
648
|
+
filePath,
|
|
649
|
+
key: field.key,
|
|
650
|
+
label: field.label,
|
|
651
|
+
}];
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
parseUploadFileInput(input) {
|
|
655
|
+
const normalized = input.trim();
|
|
656
|
+
if (!normalized) {
|
|
657
|
+
throw new XtzError('`--file` 不能为空。');
|
|
658
|
+
}
|
|
659
|
+
const separatorIndex = normalized.indexOf('=');
|
|
660
|
+
if (separatorIndex <= 0) {
|
|
661
|
+
return { path: normalized };
|
|
662
|
+
}
|
|
663
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
664
|
+
const path = normalized.slice(separatorIndex + 1).trim();
|
|
665
|
+
if (!key || !path) {
|
|
666
|
+
throw new XtzError('多上传模块请使用 `--file key=path`。');
|
|
667
|
+
}
|
|
668
|
+
return { key, path };
|
|
669
|
+
}
|
|
511
670
|
async resolveModule(requestedModuleId, flags, auth) {
|
|
512
671
|
const moduleId = requestedModuleId?.trim();
|
|
513
672
|
const explicitRoute = flags.route?.trim();
|
|
@@ -605,7 +764,8 @@ export default class PlotRun extends ToolRunCommandBase {
|
|
|
605
764
|
description: 'Use the module demo data returned by fetch-all instead of uploading a local file',
|
|
606
765
|
}),
|
|
607
766
|
file: Flags.string({
|
|
608
|
-
description: 'Path to the local xlsx file;
|
|
767
|
+
description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
|
|
768
|
+
multiple: true,
|
|
609
769
|
}),
|
|
610
770
|
download: Flags.string({
|
|
611
771
|
description: 'Download the generated file or preferred analysis result to this local path after submission',
|
|
@@ -636,10 +796,8 @@ export default class PlotRun extends ToolRunCommandBase {
|
|
|
636
796
|
if (flags.interactive && flags.json) {
|
|
637
797
|
throw new XtzError('`--interactive` 不能与 `--json` 同时使用。');
|
|
638
798
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
const result = await this.executeToolRun(args.tool, args.file ?? flags.file, flags);
|
|
799
|
+
const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
|
|
800
|
+
const result = await this.executeToolRun(args.tool, fileInputs, flags);
|
|
643
801
|
this.print(flags, result.data, result.text, { profile: result.data.agent });
|
|
644
802
|
}
|
|
645
803
|
}
|
|
@@ -6,7 +6,7 @@ import { plotOverrideFlags } from '../../lib/plot-options.js';
|
|
|
6
6
|
import { ToolRunCommandBase } from '../plot/run.js';
|
|
7
7
|
export default class ToolExec extends ToolRunCommandBase {
|
|
8
8
|
static description = 'Execute a Xiantao bioinformatics tool in agent mode';
|
|
9
|
-
static usage = 'tool exec <tool>
|
|
9
|
+
static usage = 'tool exec <tool> [--file <value> ... | --demo] --json [--download <path>]';
|
|
10
10
|
static args = {
|
|
11
11
|
tool: Args.string({
|
|
12
12
|
description: 'Tool id, e.g. violin_flat',
|
|
@@ -27,7 +27,8 @@ export default class ToolExec extends ToolRunCommandBase {
|
|
|
27
27
|
description: 'Download the generated file or preferred analysis result to this local path after submission',
|
|
28
28
|
}),
|
|
29
29
|
file: Flags.string({
|
|
30
|
-
description: 'Path to the local xlsx file',
|
|
30
|
+
description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
|
|
31
|
+
multiple: true,
|
|
31
32
|
}),
|
|
32
33
|
interactive: Flags.boolean({
|
|
33
34
|
default: false,
|
|
@@ -59,17 +60,11 @@ export default class ToolExec extends ToolRunCommandBase {
|
|
|
59
60
|
if (!flags.json) {
|
|
60
61
|
throw new XtzError('`xt tool exec` 仅支持 `--json` 输出。');
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (flags.demo && flags.file) {
|
|
63
|
+
const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
|
|
64
|
+
if (flags.demo && fileInputs.length > 0) {
|
|
66
65
|
throw new XtzError('`--demo` 与 `--file` 不能同时使用。');
|
|
67
66
|
}
|
|
68
|
-
const
|
|
69
|
-
if (!flags.demo && !filePath) {
|
|
70
|
-
throw new XtzError('`xt tool exec` 需要 `--file`,或显式传入 `--demo`。');
|
|
71
|
-
}
|
|
72
|
-
const result = await this.executeToolRun(args.tool, filePath, {
|
|
67
|
+
const result = await this.executeToolRun(args.tool, fileInputs, {
|
|
73
68
|
...flags,
|
|
74
69
|
interactive: false,
|
|
75
70
|
});
|
|
@@ -3,8 +3,8 @@ import { XtzCommand } from '../../base-command.js';
|
|
|
3
3
|
import { getAuthContext } from '../../lib/auth.js';
|
|
4
4
|
import { globalFlags } from '../../lib/flags.js';
|
|
5
5
|
import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
|
|
6
|
-
import { isJsonObject } from '../../lib/json.js';
|
|
7
6
|
import { resolveXiantaoToolProductUuid } from '../../lib/storage.js';
|
|
7
|
+
import { findDefaultXiantaoRecord, listXiantaoUploadFields } from '../../lib/tool-inputs.js';
|
|
8
8
|
import { fetchAllPlotArgs, resolveToolMenuByCode } from '../../lib/xiantao.js';
|
|
9
9
|
export default class ToolInspect extends XtzCommand {
|
|
10
10
|
static description = 'Inspect a Xiantao tool and its dynamic inputs';
|
|
@@ -38,6 +38,7 @@ export default class ToolInspect extends XtzCommand {
|
|
|
38
38
|
const summary = this.summarizeSchema(schema);
|
|
39
39
|
const output = {
|
|
40
40
|
agent,
|
|
41
|
+
args_data: fetchAllResult.args_data ?? [],
|
|
41
42
|
args_main: schema,
|
|
42
43
|
args_main_defaults: buildArgsMainFromSchema(schema),
|
|
43
44
|
duid: fetchAllResult.id?.duid ?? '',
|
|
@@ -64,8 +65,17 @@ export default class ToolInspect extends XtzCommand {
|
|
|
64
65
|
lines.push(`notice: ${notice}`);
|
|
65
66
|
}
|
|
66
67
|
lines.push('input:');
|
|
67
|
-
lines.push(` upload: ${inputs.upload_available ?
|
|
68
|
-
|
|
68
|
+
lines.push(` upload: ${inputs.upload_available ? `${inputs.upload_fields.length} fields` : 'not detected'}`);
|
|
69
|
+
for (const field of inputs.upload_fields) {
|
|
70
|
+
const suffixes = [
|
|
71
|
+
field.label && field.label !== field.key ? field.label : undefined,
|
|
72
|
+
field.required ? 'required' : undefined,
|
|
73
|
+
field.demo_upload ? this.formatDemoSummary(field.demo_upload) : 'no demo default',
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
lines.push(` - ${field.key}${suffixes.length > 0 ? ` - ${suffixes.join(', ')}` : ''}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(` demo: ${inputs.demo_available ? 'available' : 'unavailable'}`);
|
|
78
|
+
lines.push(` record: ${inputs.record_available ? this.formatRecordSummary(inputs.record_default) : 'unavailable'}`);
|
|
69
79
|
if (schema.length === 0) {
|
|
70
80
|
lines.push('args_main: none');
|
|
71
81
|
return lines.join('\n');
|
|
@@ -90,22 +100,40 @@ export default class ToolInspect extends XtzCommand {
|
|
|
90
100
|
return lines.join('\n');
|
|
91
101
|
}
|
|
92
102
|
summarizeInputs(argsData, duid) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
103
|
+
const uploadFields = listXiantaoUploadFields(argsData);
|
|
104
|
+
const record = findDefaultXiantaoRecord(argsData);
|
|
105
|
+
const demoAvailable = Boolean(duid.trim() && uploadFields.length > 0 && uploadFields.every((field) => Boolean(field.defaultUpload)));
|
|
106
|
+
const recordAvailable = Boolean(duid.trim() && record);
|
|
107
|
+
const recordDefault = recordAvailable && record
|
|
108
|
+
? {
|
|
109
|
+
key: record.key,
|
|
110
|
+
label: record.label,
|
|
111
|
+
module_id: record.moduleId,
|
|
112
|
+
module_name: record.moduleName,
|
|
113
|
+
record_id: record.recordId,
|
|
114
|
+
record_name: record.recordName,
|
|
115
|
+
time: record.time,
|
|
116
|
+
}
|
|
117
|
+
: null;
|
|
99
118
|
return {
|
|
100
119
|
demo_available: demoAvailable,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
record_available: recordAvailable,
|
|
121
|
+
record_default: recordDefault,
|
|
122
|
+
upload_available: uploadFields.length > 0,
|
|
123
|
+
upload_fields: uploadFields.map((field) => ({
|
|
124
|
+
demo_upload: field.defaultUpload
|
|
125
|
+
? {
|
|
126
|
+
file_name: field.defaultUpload.fileName,
|
|
127
|
+
file_oss: field.defaultUpload.fileOss,
|
|
128
|
+
file_size: field.defaultUpload.fileSize,
|
|
129
|
+
key: field.key,
|
|
130
|
+
label: field.label,
|
|
131
|
+
}
|
|
132
|
+
: null,
|
|
133
|
+
key: field.key,
|
|
134
|
+
label: field.label,
|
|
135
|
+
required: field.required,
|
|
136
|
+
})),
|
|
109
137
|
};
|
|
110
138
|
}
|
|
111
139
|
summarizeSchema(schema) {
|
|
@@ -117,10 +145,13 @@ export default class ToolInspect extends XtzCommand {
|
|
|
117
145
|
};
|
|
118
146
|
}
|
|
119
147
|
formatDemoSummary(demoUpload) {
|
|
120
|
-
|
|
148
|
+
return `${demoUpload.file_name} (${this.formatFileSize(demoUpload.file_size)})`;
|
|
149
|
+
}
|
|
150
|
+
formatRecordSummary(record) {
|
|
151
|
+
if (!record) {
|
|
121
152
|
return 'available';
|
|
122
153
|
}
|
|
123
|
-
return `${
|
|
154
|
+
return `${record.module_id || '-'} ${record.record_id}${record.time ? ` (${record.time})` : ''}`;
|
|
124
155
|
}
|
|
125
156
|
formatFileSize(fileSize) {
|
|
126
157
|
if (fileSize < 1024) {
|
|
@@ -7,7 +7,7 @@ import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
|
|
|
7
7
|
import { ToolRunCommandBase } from '../plot/run.js';
|
|
8
8
|
export default class ToolPrepare extends ToolRunCommandBase {
|
|
9
9
|
static description = 'Prepare a Xiantao bioinformatics tool in agent mode and return the final dynamic schema';
|
|
10
|
-
static usage = 'tool prepare <tool>
|
|
10
|
+
static usage = 'tool prepare <tool> [--file <value> ... | --demo] --json';
|
|
11
11
|
static args = {
|
|
12
12
|
tool: Args.string({
|
|
13
13
|
description: 'Tool id, e.g. violin_flat',
|
|
@@ -25,7 +25,8 @@ export default class ToolPrepare extends ToolRunCommandBase {
|
|
|
25
25
|
description: 'Use the module demo data returned by fetch-all instead of uploading a local file',
|
|
26
26
|
}),
|
|
27
27
|
file: Flags.string({
|
|
28
|
-
description: 'Path to the local xlsx file',
|
|
28
|
+
description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
|
|
29
|
+
multiple: true,
|
|
29
30
|
}),
|
|
30
31
|
interactive: Flags.boolean({
|
|
31
32
|
default: false,
|
|
@@ -57,17 +58,11 @@ export default class ToolPrepare extends ToolRunCommandBase {
|
|
|
57
58
|
if (!flags.json) {
|
|
58
59
|
throw new XtzError('`xt tool prepare` 仅支持 `--json` 输出。');
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
if (flags.demo && flags.file) {
|
|
61
|
+
const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
|
|
62
|
+
if (flags.demo && fileInputs.length > 0) {
|
|
64
63
|
throw new XtzError('`--demo` 与 `--file` 不能同时使用。');
|
|
65
64
|
}
|
|
66
|
-
const
|
|
67
|
-
if (!flags.demo && !filePath) {
|
|
68
|
-
throw new XtzError('`xt tool prepare` 需要 `--file`,或显式传入 `--demo`。');
|
|
69
|
-
}
|
|
70
|
-
const prepared = await this.prepareToolRun(args.tool, filePath, {
|
|
65
|
+
const prepared = await this.prepareToolRun(args.tool, fileInputs, {
|
|
71
66
|
...flags,
|
|
72
67
|
interactive: false,
|
|
73
68
|
});
|
|
@@ -77,9 +72,11 @@ export default class ToolPrepare extends ToolRunCommandBase {
|
|
|
77
72
|
args_main: prepared.schema ?? [],
|
|
78
73
|
args_main_defaults: buildArgsMainFromSchema(prepared.schema),
|
|
79
74
|
duid: prepared.duid,
|
|
80
|
-
file_name: prepared.
|
|
81
|
-
file_oss: prepared.
|
|
82
|
-
file_size: prepared.
|
|
75
|
+
file_name: prepared.inputSummary.file_name,
|
|
76
|
+
file_oss: prepared.inputSummary.file_oss,
|
|
77
|
+
file_size: prepared.inputSummary.file_size,
|
|
78
|
+
input_record: prepared.inputSummary.input_record,
|
|
79
|
+
input_uploads: prepared.inputSummary.input_uploads,
|
|
83
80
|
mode: prepared.inputMode,
|
|
84
81
|
module_id: prepared.moduleId,
|
|
85
82
|
notice: prepared.notice,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isJsonObject } from './json.js';
|
|
2
|
+
export function listXiantaoUploadFields(argsData) {
|
|
3
|
+
return argsData.flatMap((item) => {
|
|
4
|
+
if (!isJsonObject(item) || item.type !== 'XTUpload') {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const key = typeof item.key === 'string' ? item.key.trim() : '';
|
|
8
|
+
if (!key) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const defaultValue = isJsonObject(item.default_value) ? item.default_value : undefined;
|
|
12
|
+
const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
|
|
13
|
+
const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
|
|
14
|
+
const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
|
|
15
|
+
return [{
|
|
16
|
+
defaultUpload: fileName && fileOss && typeof fileSize === 'number'
|
|
17
|
+
? {
|
|
18
|
+
fileName,
|
|
19
|
+
fileOss,
|
|
20
|
+
fileSize,
|
|
21
|
+
}
|
|
22
|
+
: null,
|
|
23
|
+
key,
|
|
24
|
+
label: typeof item.label === 'string' ? item.label.trim() : '',
|
|
25
|
+
required: item.required === true,
|
|
26
|
+
}];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function findDefaultXiantaoRecord(argsData) {
|
|
30
|
+
for (const item of argsData) {
|
|
31
|
+
if (!isJsonObject(item) || item.type !== 'XTRecord') {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const key = typeof item.key === 'string' ? item.key.trim() : '';
|
|
35
|
+
const defaultValue = isJsonObject(item.default_value) ? item.default_value : undefined;
|
|
36
|
+
const recordId = typeof defaultValue?.record_id === 'string' ? defaultValue.record_id.trim() : '';
|
|
37
|
+
if (!key || !recordId) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
label: typeof item.label === 'string' ? item.label.trim() : '',
|
|
43
|
+
moduleId: typeof defaultValue?.module_id === 'string' ? defaultValue.module_id.trim() : '',
|
|
44
|
+
moduleName: typeof defaultValue?.module_name === 'string' ? defaultValue.module_name.trim() : '',
|
|
45
|
+
recordId,
|
|
46
|
+
recordName: typeof defaultValue?.record_name === 'string' ? defaultValue.record_name.trim() : '',
|
|
47
|
+
time: typeof defaultValue?.time === 'string' ? defaultValue.time.trim() : '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
package/dist/lib/xiantao-plot.js
CHANGED
|
@@ -14,14 +14,7 @@ export async function verifyXiantaoPlotUpload(auth, input) {
|
|
|
14
14
|
},
|
|
15
15
|
args_data: {
|
|
16
16
|
duid: '',
|
|
17
|
-
info:
|
|
18
|
-
main: {
|
|
19
|
-
file_oss: getXiantaoFileOss(input.upload),
|
|
20
|
-
file_name: input.upload.fileName,
|
|
21
|
-
file_size: getXiantaoFileSize(input.upload),
|
|
22
|
-
type: 'upload',
|
|
23
|
-
},
|
|
24
|
-
},
|
|
17
|
+
info: buildUploadInfo(input.uploads),
|
|
25
18
|
},
|
|
26
19
|
args_main: input.argsMain,
|
|
27
20
|
},
|
|
@@ -52,17 +45,7 @@ export async function submitXiantaoPlot(auth, input) {
|
|
|
52
45
|
module_id: input.moduleId,
|
|
53
46
|
uuid: input.uuid,
|
|
54
47
|
},
|
|
55
|
-
args_data:
|
|
56
|
-
duid: input.duid,
|
|
57
|
-
info: {
|
|
58
|
-
main: {
|
|
59
|
-
file_oss: getXiantaoFileOss(input.upload),
|
|
60
|
-
file_name: input.upload.fileName,
|
|
61
|
-
file_size: getXiantaoFileSize(input.upload),
|
|
62
|
-
type: 'upload',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
48
|
+
args_data: buildSubmitArgsData(input.duid, input.input),
|
|
66
49
|
args_main: input.argsMain,
|
|
67
50
|
},
|
|
68
51
|
}),
|
|
@@ -163,6 +146,39 @@ function getXiantaoFileOss(upload) {
|
|
|
163
146
|
function getXiantaoFileSize(upload) {
|
|
164
147
|
return upload.fileSizeForXiantao ?? upload.fileSize;
|
|
165
148
|
}
|
|
149
|
+
function buildSubmitArgsData(duid, input) {
|
|
150
|
+
if (input.kind === 'record') {
|
|
151
|
+
return {
|
|
152
|
+
duid,
|
|
153
|
+
info: {
|
|
154
|
+
[input.record.key]: {
|
|
155
|
+
label: input.record.label,
|
|
156
|
+
module_id: input.record.moduleId,
|
|
157
|
+
module_name: input.record.moduleName,
|
|
158
|
+
record_id: input.record.recordId,
|
|
159
|
+
record_name: input.record.recordName,
|
|
160
|
+
time: input.record.time,
|
|
161
|
+
type: 'record',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
duid,
|
|
168
|
+
info: buildUploadInfo(input.uploads),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function buildUploadInfo(uploads) {
|
|
172
|
+
return Object.fromEntries(uploads.map((item) => [
|
|
173
|
+
item.key,
|
|
174
|
+
{
|
|
175
|
+
file_oss: getXiantaoFileOss(item.upload),
|
|
176
|
+
file_name: item.upload.fileName,
|
|
177
|
+
file_size: getXiantaoFileSize(item.upload),
|
|
178
|
+
type: 'upload',
|
|
179
|
+
},
|
|
180
|
+
]));
|
|
181
|
+
}
|
|
166
182
|
function isAsyncSubmissionMessage(message) {
|
|
167
183
|
return Boolean(message?.includes('任务提交成功'));
|
|
168
184
|
}
|
package/dist/xtz-help.js
CHANGED
|
@@ -6,7 +6,7 @@ export default class XtzHelp extends Help {
|
|
|
6
6
|
this.log(this.section('MAIN FLOWS', [
|
|
7
7
|
['First use', '$ xt login\n$ xt tool search violin\n$ xt tool inspect violin_flat\n$ xt tool run violin_flat ./data.xlsx'],
|
|
8
8
|
['Interactive tool run', '$ xt tool run --interactive'],
|
|
9
|
-
['Agent automation', '$ xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile opencode --json'],
|
|
9
|
+
['Agent automation', '$ xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec gsea_customize --file gene_set=./gene_set.gmt --file data=./rank.xlsx --profile opencode --json'],
|
|
10
10
|
]));
|
|
11
11
|
this.log('');
|
|
12
12
|
this.log(this.section('COMMON COMMANDS', this.renderCommandItems([
|