@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 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 upload-dependent final schema, then `xt tool exec` to submit. Keep `--json`, pass an explicit profile with `--profile`, and avoid interactive-only flows.
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 used by the tool. `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`; otherwise `tool run` uploads the local file first and therefore also needs token auth. 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.
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, flags.device, flags.output);
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('当前历史记录返回多个可下载结果,请传入 `--device`,或将 `--output` 改为带目标扩展名的文件名。');
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, expectedDevice, outputPath) {
66
- const normalizedExpectedDevice = this.normalizeDevice(expectedDevice ?? this.inferDeviceFromOutputPath(outputPath));
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, isJsonObject } from '../../lib/json.js';
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 { resolveXiantaoDownload, submitXiantaoPlot, verifyXiantaoPlotUpload } from '../../lib/xiantao-plot.js';
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
- async prepareToolRun(requestedModuleId, filePath, flags) {
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
- filePath,
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 { duid, upload, schema: verifiedSchema } = inputMode.mode === 'demo'
43
+ const preparedInput = inputMode.mode === 'demo'
34
44
  ? this.resolveDemoInput(fetchAllResult, moduleId)
35
- : await this.uploadAndVerify(agent, inputMode.filePath, {
36
- argsMain,
37
- moduleId,
38
- uuid,
39
- }, auth);
40
- if (verifiedSchema.length) {
41
- schema = verifiedSchema;
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, filePath, flags) {
60
- const prepared = await this.prepareToolRun(requestedModuleId, filePath, flags);
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.filePath) {
438
+ if (input.fileInputs.length > 0) {
435
439
  return {
436
- filePath: input.filePath,
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
- filePath: await promptPlotFilePath(),
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
- filePath: await promptPlotFilePath(),
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 uploadField = (fetchAllResult.args_data ?? []).find((item) => isJsonObject(item) && item.type === 'XTUpload');
464
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
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
- schema: [],
474
- upload: {
475
- fileName,
476
- fileOss,
477
- fileSize,
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 uploadField = (fetchAllResult.args_data ?? []).find((item) => isJsonObject(item) && item.type === 'XTUpload');
487
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
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
- async uploadAndVerify(agent, filePath, input, auth) {
494
- if (!filePath) {
495
- throw new XtzError('通用模式请传入 FILE,或改用 `--demo`。');
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 uploadAuth = await getAuthContext(agent);
498
- const upload = await uploadFile(uploadAuth, filePath);
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
- upload,
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; optional in --interactive mode',
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
- if (args.file && flags.file) {
640
- throw new XtzError('FILE `--file` 不能同时使用。');
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> (--file <value> | --demo) --json [--download <path>]';
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
- if (args.file && flags.file) {
63
- throw new XtzError('FILE `--file` 不能同时使用。');
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 filePath = args.file ?? flags.file;
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 ? 'local xlsx' : 'not detected'}`);
68
- lines.push(` demo: ${inputs.demo_available ? this.formatDemoSummary(inputs.demo_upload) : 'unavailable'}`);
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 uploadField = argsData.find((item) => isJsonObject(item) && item.type === 'XTUpload');
94
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
95
- const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
96
- const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
97
- const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
98
- const demoAvailable = Boolean(duid.trim() && fileName && fileOss && typeof fileSize === 'number');
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
- demo_upload: demoAvailable
102
- ? {
103
- file_name: fileName,
104
- file_oss: fileOss,
105
- file_size: fileSize,
106
- }
107
- : null,
108
- upload_available: Boolean(uploadField),
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
- if (!demoUpload) {
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 `${demoUpload.file_name} (${this.formatFileSize(demoUpload.file_size)})`;
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> (--file <value> | --demo) --json';
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
- if (args.file && flags.file) {
61
- throw new XtzError('FILE `--file` 不能同时使用。');
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 filePath = args.file ?? flags.file;
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.upload.fileName,
81
- file_oss: prepared.upload.fileOss,
82
- file_size: prepared.upload.fileSize,
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
+ }
@@ -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([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixlife-ai/xiantao",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for Xiantao bioinformatics tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",