@helixlife-ai/xiantao 0.1.8 → 0.1.10

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
@@ -67,7 +67,7 @@ For machine callers, use `xt tool exec`, keep `--json`, pass an explicit profile
67
67
 
68
68
  For the common Xiantao tool product, `tool search`, `tool inspect`, `tool run`, `tool resolve`, and `tool menu` default `toolProductUuid` to `c0b6febb-52dd-4525-970a-61bbe9e263ff`. You can override it with `--toolProductUuid`, `XIANTAO_TOOL_PRODUCT_UUID`, or `~/.config/xtz/config.json` under `xiantao.toolProductUuid`.
69
69
 
70
- `xt tool search <keyword>` searches cached or 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. `xt tool resolve <tool_id>` remains available as the low-level UUID and route lookup. `--demo` reuses the demo file metadata returned by `tool fetch-all`; otherwise `tool run` uploads the local file first and therefore also needs upload auth. Run `xt login` first to refresh both tokens in one step.
70
+ `xt tool search <keyword>` searches cached or 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. `xt tool resolve <tool_id>` remains available as the low-level UUID and route lookup. `--demo` reuses the demo file metadata returned by `tool fetch-all`; otherwise `tool run` uploads the local file first and therefore also needs upload auth. Run `xt login` first to refresh both tokens in one step. When a tool exposes multiple downloadable results, `--download <path>` uses the output file extension to select the matching artifact.
71
71
 
72
72
  `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.
73
73
 
@@ -16,7 +16,7 @@ export default class PlotDownload extends XtzCommand {
16
16
  }),
17
17
  output: Flags.string({
18
18
  char: 'o',
19
- description: 'Destination path; defaults to the file name inferred from the URL',
19
+ description: 'Destination path; defaults to ~/Downloads/<file name>',
20
20
  }),
21
21
  };
22
22
  async run() {
@@ -135,30 +135,60 @@ export class ToolRunCommandBase extends XtzCommand {
135
135
  return lines.join('\n');
136
136
  }
137
137
  async downloadResult(auth, input) {
138
+ if (input.downloads.length > 0) {
139
+ const item = this.pickDownloadItem(input.downloads, input.downloadPath);
140
+ if (!item) {
141
+ throw new XtzError('当前模块返回多个可下载结果,请将 `--download` 路径改为带目标扩展名的文件名,或使用 `--interactive` 选择。');
142
+ }
143
+ const resolved = await resolveXiantaoDownload(auth, {
144
+ item,
145
+ moduleId: input.moduleId,
146
+ uuid: input.uuid,
147
+ });
148
+ return [await downloadToPath(resolved.url, input.downloadPath)];
149
+ }
138
150
  if (input.file) {
139
151
  return [await downloadToPath(input.file, input.downloadPath)];
140
152
  }
141
- const item = this.pickDownloadItem(input.downloads);
142
- if (!item) {
143
- throw new XtzError('当前模块未返回可下载文件。');
144
- }
145
- const resolved = await resolveXiantaoDownload(auth, {
146
- item,
147
- moduleId: input.moduleId,
148
- uuid: input.uuid,
149
- });
150
- return [await downloadToPath(resolved.url, input.downloadPath)];
153
+ throw new XtzError('当前模块未返回可下载文件。');
151
154
  }
152
- pickDownloadItem(downloads) {
153
- const report = downloads.find((item) => item.endpoint === 'public.output.download-report');
154
- if (report) {
155
- return report;
155
+ pickDownloadItem(downloads, downloadPath) {
156
+ const expectedDevice = this.inferDownloadDevice(downloadPath);
157
+ if (expectedDevice) {
158
+ const matched = downloads.filter((item) => this.normalizeDevice(item.device) === expectedDevice);
159
+ if (matched.length === 1) {
160
+ return matched[0];
161
+ }
162
+ }
163
+ const results = downloads.filter((item) => item.endpoint === 'public.output.download-result');
164
+ if (results.length === 1) {
165
+ return results[0];
156
166
  }
157
167
  if (downloads.length === 1) {
158
168
  return downloads[0];
159
169
  }
160
170
  return undefined;
161
171
  }
172
+ inferDownloadDevice(downloadPath) {
173
+ if (!downloadPath) {
174
+ return undefined;
175
+ }
176
+ const extension = downloadPath.trim().split('.').pop()?.toLowerCase();
177
+ if (!extension) {
178
+ return undefined;
179
+ }
180
+ return this.normalizeDevice(extension);
181
+ }
182
+ normalizeDevice(device) {
183
+ const normalized = device.trim().toLowerCase();
184
+ if (normalized === 'tif') {
185
+ return 'tiff';
186
+ }
187
+ if (normalized === 'jpg') {
188
+ return 'jpeg';
189
+ }
190
+ return normalized;
191
+ }
162
192
  async downloadSelectedResults(auth, input) {
163
193
  const paths = [];
164
194
  for (const item of input.downloads) {
@@ -1,5 +1,6 @@
1
- import { writeFile } from 'node:fs/promises';
1
+ import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import { writeFile } from 'node:fs/promises';
3
4
  import { XtzError } from './errors.js';
4
5
  export async function downloadToPath(url, outputPath) {
5
6
  let response;
@@ -12,18 +13,20 @@ export async function downloadToPath(url, outputPath) {
12
13
  if (!response.ok) {
13
14
  throw new XtzError(`下载失败 (${response.status})`);
14
15
  }
15
- const targetPath = path.resolve(outputPath ?? inferFileName(url));
16
+ const targetPath = outputPath
17
+ ? path.resolve(outputPath)
18
+ : inferDownloadPath(url);
16
19
  const buffer = Buffer.from(await response.arrayBuffer());
17
20
  await writeFile(targetPath, buffer);
18
21
  return targetPath;
19
22
  }
20
- function inferFileName(url) {
23
+ function inferDownloadPath(url) {
21
24
  try {
22
25
  const pathname = new URL(url).pathname;
23
26
  const name = decodeURIComponent(path.basename(pathname));
24
- return name || 'download.bin';
27
+ return path.join(os.homedir(), 'Downloads', name || 'download.bin');
25
28
  }
26
29
  catch {
27
- return 'download.bin';
30
+ return path.join(os.homedir(), 'Downloads', 'download.bin');
28
31
  }
29
32
  }
@@ -25,9 +25,9 @@ export async function raiseApiError(payload, options = {}) {
25
25
  }
26
26
  }
27
27
  if (tokenKind === 'xiantao') {
28
- throw new XtzError('您的仙桃网页授权已过期或无效,请重新运行 `xt login`。');
28
+ throw new XtzError('您的授权已过期或无效,请重新完成授权。。');
29
29
  }
30
- throw new XtzError('您的授权已过期或无效,请重新运行 `xt login`。');
30
+ throw new XtzError('您的授权已过期或无效,请重新完成授权。');
31
31
  }
32
32
  if (message.includes('无权限') || message.includes('权限')) {
33
33
  throw new XtzError('需购买仙桃高级版 https://www.xiantaozi.com/');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixlife-ai/xiantao",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CLI for Xiantao bioinformatics tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,53 +0,0 @@
1
- import { Flags } from '@oclif/core';
2
- import { XtzCommand } from '../../base-command.js';
3
- import { loginAuth, waitForAuthorizedToken } from '../../lib/auth.js';
4
- import { globalFlags } from '../../lib/flags.js';
5
- import { bindUserWithXiantaoToken } from '../../lib/xiantao-auth.js';
6
- import { getTokenPath, getXiantaoTokenPath, loadXiantaoToken, saveToken } from '../../lib/storage.js';
7
- export default class AuthBind extends XtzCommand {
8
- static description = 'Bind login remotely with a Xiantao web token without the localhost callback flow';
9
- static flags = {
10
- ...globalFlags,
11
- 'auth-key': Flags.string({
12
- char: 'k',
13
- description: 'Existing auth key returned by the upload authorization flow',
14
- }),
15
- 'token-key': Flags.string({
16
- description: 'Existing upload token key paired with --auth-key',
17
- }),
18
- 'xiantao-token': Flags.string({
19
- description: 'Bearer token for api.helixlife.net xiantao APIs; falls back to XIANTAO_TOKEN or the stored Xiantao web token',
20
- }),
21
- };
22
- async run() {
23
- this.warnIfLegacyAuthCommand();
24
- const { flags } = await this.parse(AuthBind);
25
- const agent = await this.getProfile(flags.profile);
26
- const token = flags['xiantao-token']?.trim() || process.env.XIANTAO_TOKEN?.trim() || await loadXiantaoToken(agent);
27
- if (!token) {
28
- this.error(`未找到 ${agent} 的仙桃网页 token,请传 --xiantao-token、设置 XIANTAO_TOKEN,或先准备 ${getXiantaoTokenPath(agent)}`, { exit: 1 });
29
- }
30
- const auth = flags['auth-key']
31
- ? {
32
- authKey: flags['auth-key'],
33
- token: flags['token-key']?.trim(),
34
- tokenPath: getTokenPath(agent),
35
- }
36
- : await loginAuth(agent, false);
37
- if (!auth.token) {
38
- this.error('传 --auth-key 时必须同时传 --token-key', { exit: 1 });
39
- }
40
- if (flags['auth-key']) {
41
- await saveToken(agent, auth.token);
42
- }
43
- const result = await bindUserWithXiantaoToken(agent, auth.authKey, token);
44
- await waitForAuthorizedToken(auth.token);
45
- this.print(flags, {
46
- agent,
47
- auth_key: auth.authKey,
48
- bound: true,
49
- token_path: auth.tokenPath,
50
- xiantao_token_path: result.tokenPath,
51
- }, `profile: ${agent}\nbound: true\nauth_key: ${auth.authKey}\ntoken: ${auth.tokenPath}\nxiantao_token: ${result.tokenPath}`);
52
- }
53
- }
@@ -1,3 +0,0 @@
1
- import AuthBind from './auth/bind.js';
2
- export default class Bind extends AuthBind {
3
- }