@helixlife-ai/xiantao 0.1.2 → 0.1.3

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
@@ -15,13 +15,13 @@ Install with `npm i -g @helixlife-ai/xiantao` and run it as `xt`.
15
15
  ### First Use
16
16
 
17
17
  ```bash
18
- xt tool login
18
+ xt login
19
19
  xt tool search violin
20
20
  xt tool inspect violin_flat
21
21
  xt tool run violin_flat ./data.xlsx
22
22
  ```
23
23
 
24
- `xt tool login` is the recommended first-use entrypoint. It signs in to the Xiantao web APIs and automatically backfills the upload token when it is missing. `xt login` does the inverse: it signs in for upload-related APIs and backfills the Xiantao web token when needed.
24
+ `xt login` is the recommended first-use entrypoint. It refreshes both the upload token and the Xiantao web token for the current profile. `xt tool login` is kept as an equivalent entrypoint under the `tool` topic.
25
25
 
26
26
  ### Interactive Tool Run
27
27
 
@@ -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, which `xt tool login` or `xt login` will backfill automatically when missing.
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.
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
 
@@ -1,11 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { XtzCommand } from '../../base-command.js';
3
- import { loginAuth } from '../../lib/auth.js';
4
3
  import { globalFlags } from '../../lib/flags.js';
4
+ import { loginAll } from '../../lib/login-flow.js';
5
5
  import { getTokenPath, getXiantaoTokenPath } from '../../lib/storage.js';
6
- import { ensureXiantaoLogin } from '../../lib/xiantao-auth.js';
7
6
  export default class AuthLogin extends XtzCommand {
8
- static description = 'Get an authorization link for the current profile, save the token locally, and backfill the Xiantao web token when missing';
7
+ static description = 'Sign in for the current profile and refresh both the upload token and the Xiantao web token';
9
8
  static flags = {
10
9
  ...globalFlags,
11
10
  open: Flags.boolean({
@@ -18,28 +17,35 @@ export default class AuthLogin extends XtzCommand {
18
17
  this.warnIfLegacyAuthCommand();
19
18
  const { flags } = await this.parse(AuthLogin);
20
19
  const agent = await this.getProfile(flags.profile);
21
- const authResult = await loginAuth(agent, flags.open);
22
- const xiantaoResult = await ensureXiantaoLogin(agent);
20
+ const result = await loginAll(agent, flags.open, ({ auth, phase }) => {
21
+ if (flags.json) {
22
+ return;
23
+ }
24
+ if (phase === 'auth_started') {
25
+ this.log(auth.browserOpened ? '浏览器已打开上传授权链接,等待授权完成...' : `请先在浏览器中打开上传授权链接: ${auth.linkUrl}`);
26
+ return;
27
+ }
28
+ this.log('上传授权已确认,继续仙桃网页登录...');
29
+ });
23
30
  this.print(flags, {
24
31
  agent,
25
- browser_opened: authResult.browserOpened,
26
- link_url: authResult.linkUrl,
27
- token_path: authResult.tokenPath,
32
+ browser_opened: result.auth.browserOpened,
33
+ link_url: result.auth.linkUrl,
34
+ token_path: result.auth.tokenPath,
28
35
  xiantao: {
29
- browser_opened: xiantaoResult.browserOpened,
30
- callback_url: xiantaoResult.callbackUrl,
31
- jump_url: xiantaoResult.jumpUrl,
32
- supplemented: xiantaoResult.created,
33
- token_path: xiantaoResult.tokenPath,
36
+ browser_opened: result.xiantao.browserOpened,
37
+ callback_url: result.xiantao.callbackUrl,
38
+ jump_url: result.xiantao.jumpUrl,
39
+ token_path: result.xiantao.tokenPath,
34
40
  },
35
41
  }, [
36
42
  `profile: ${agent}`,
37
43
  `token: ${getTokenPath(agent)}`,
38
44
  `xiantao_token: ${getXiantaoTokenPath(agent)}`,
39
- authResult.browserOpened
45
+ result.xiantao.browserOpened ? '仙桃网页登录成功,token 已刷新。' : `请在浏览器中打开: ${result.xiantao.jumpUrl}`,
46
+ result.auth.browserOpened
40
47
  ? '浏览器已打开授权链接,请完成授权后运行 `xt status`。'
41
- : `请在浏览器中打开: ${authResult.linkUrl}`,
42
- xiantaoResult.created ? '已自动补齐仙桃网页登录 token。' : '仙桃网页登录 token 已存在,跳过补齐。',
48
+ : `请在浏览器中打开: ${result.auth.linkUrl}`,
43
49
  ].join('\n'));
44
50
  }
45
51
  }
@@ -1,41 +1,51 @@
1
+ import { Flags } from '@oclif/core';
1
2
  import { XtzCommand } from '../../base-command.js';
2
- import { ensureAuthLogin } from '../../lib/auth.js';
3
3
  import { globalFlags } from '../../lib/flags.js';
4
+ import { loginAll } from '../../lib/login-flow.js';
4
5
  import { getTokenPath, getXiantaoTokenPath } from '../../lib/storage.js';
5
- import { loginXiantao } from '../../lib/xiantao-auth.js';
6
6
  export default class PlotLogin extends XtzCommand {
7
- static description = 'Sign in to Xiantao web APIs for the current profile, save the returned token locally, and backfill the upload token when missing';
7
+ static description = 'Sign in for the current profile and refresh both the upload token and the Xiantao web token';
8
8
  static flags = {
9
9
  ...globalFlags,
10
+ open: Flags.boolean({
11
+ allowNo: true,
12
+ default: true,
13
+ description: 'Open the authorization link in the browser',
14
+ }),
10
15
  };
11
16
  async run() {
12
17
  this.warnIfLegacyPlotCommand();
13
18
  const { flags } = await this.parse(PlotLogin);
14
19
  const agent = await this.getProfile(flags.profile);
15
- const result = await loginXiantao(agent);
16
- const authResult = await ensureAuthLogin(agent);
20
+ const result = await loginAll(agent, flags.open, ({ auth, phase }) => {
21
+ if (flags.json) {
22
+ return;
23
+ }
24
+ if (phase === 'auth_started') {
25
+ this.log(auth.browserOpened ? '浏览器已打开上传授权链接,等待授权完成...' : `请先在浏览器中打开上传授权链接: ${auth.linkUrl}`);
26
+ return;
27
+ }
28
+ this.log('上传授权已确认,继续仙桃网页登录...');
29
+ });
17
30
  this.print(flags, {
18
31
  agent,
19
- browser_opened: result.browserOpened,
20
- callback_url: result.callbackUrl,
21
- jump_url: result.jumpUrl,
22
- token_path: result.tokenPath,
23
- auth: {
24
- browser_opened: authResult.browserOpened,
25
- link_url: authResult.linkUrl,
26
- supplemented: authResult.created,
27
- token_path: authResult.tokenPath,
32
+ browser_opened: result.auth.browserOpened,
33
+ link_url: result.auth.linkUrl,
34
+ token_path: result.auth.tokenPath,
35
+ xiantao: {
36
+ browser_opened: result.xiantao.browserOpened,
37
+ callback_url: result.xiantao.callbackUrl,
38
+ jump_url: result.xiantao.jumpUrl,
39
+ token_path: result.xiantao.tokenPath,
28
40
  },
29
41
  }, [
30
42
  `profile: ${agent}`,
31
43
  `token: ${getTokenPath(agent)}`,
32
44
  `xiantao_token: ${getXiantaoTokenPath(agent)}`,
33
- result.browserOpened ? '仙桃网页登录成功,token 已保存。' : `请在浏览器中打开: ${result.jumpUrl}`,
34
- authResult.created
35
- ? authResult.browserOpened
36
- ? '已自动补齐上传授权 token,请完成浏览器授权后运行 `xt status`。'
37
- : `已自动补齐上传授权 token,请在浏览器中打开: ${authResult.linkUrl}`
38
- : '上传授权 token 已存在,跳过补齐。',
45
+ result.xiantao.browserOpened ? '仙桃网页登录成功,token 已刷新。' : `请在浏览器中打开: ${result.xiantao.jumpUrl}`,
46
+ result.auth.browserOpened
47
+ ? '浏览器已打开授权链接,请完成授权后运行 `xt status`。'
48
+ : `请在浏览器中打开: ${result.auth.linkUrl}`,
39
49
  ].join('\n'));
40
50
  }
41
51
  }
package/dist/lib/auth.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { AUTH_URL_ENDPOINT, CHECK_TOKEN_ENDPOINT } from './constants.js';
2
+ import { XtzError } from './errors.js';
2
3
  import { openUrl } from './open-url.js';
3
4
  import { requestJson } from './http.js';
4
- import { getTokenPath, loadToken, requireToken, saveToken } from './storage.js';
5
+ import { requireToken, saveToken } from './storage.js';
5
6
  export async function fetchAuthLink(agent) {
6
7
  const response = await requestJson(AUTH_URL_ENDPOINT);
7
8
  const link = new URL(response.data.link_url);
@@ -22,22 +23,6 @@ export async function loginAuth(agent, openBrowser = true) {
22
23
  tokenPath,
23
24
  };
24
25
  }
25
- export async function ensureAuthLogin(agent, openBrowser = true) {
26
- const token = await loadToken(agent);
27
- if (token) {
28
- return {
29
- browserOpened: false,
30
- created: false,
31
- token,
32
- tokenPath: getTokenPath(agent),
33
- };
34
- }
35
- const result = await loginAuth(agent, openBrowser);
36
- return {
37
- ...result,
38
- created: true,
39
- };
40
- }
41
26
  export async function getAuthContext(agent) {
42
27
  const token = await requireToken(agent);
43
28
  return { agent, token };
@@ -49,3 +34,40 @@ export async function checkToken(agent, token) {
49
34
  const response = await requestJson(url.toString(), {}, { agent });
50
35
  return response.data;
51
36
  }
37
+ export async function waitForAuthorizedToken(token, options = {}) {
38
+ const timeoutMs = options.timeoutMs ?? 2 * 60 * 1000;
39
+ const intervalMs = options.intervalMs ?? 1500;
40
+ const deadline = Date.now() + timeoutMs;
41
+ let lastMessage = '';
42
+ while (Date.now() < deadline) {
43
+ const result = await pollAuthorizedToken(token);
44
+ if (result.success) {
45
+ return;
46
+ }
47
+ lastMessage = result.message?.trim() || lastMessage;
48
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
49
+ }
50
+ throw new XtzError(lastMessage ? `等待上传授权完成超时: ${lastMessage}` : '等待上传授权完成超时,请重新运行 `xt login`。');
51
+ }
52
+ async function pollAuthorizedToken(token) {
53
+ const url = new URL(CHECK_TOKEN_ENDPOINT);
54
+ url.searchParams.set('token_key', token);
55
+ let response;
56
+ try {
57
+ response = await fetch(url.toString());
58
+ }
59
+ catch {
60
+ throw new XtzError('网络请求失败,请稍后重试。');
61
+ }
62
+ const body = await response.text();
63
+ try {
64
+ const payload = JSON.parse(body);
65
+ return {
66
+ message: payload.message,
67
+ success: response.ok && payload.success !== false,
68
+ };
69
+ }
70
+ catch {
71
+ throw new XtzError('服务返回了无法解析的响应。');
72
+ }
73
+ }
@@ -19,9 +19,9 @@ export async function raiseApiError(payload, options = {}) {
19
19
  }
20
20
  }
21
21
  if (tokenKind === 'xiantao') {
22
- throw new XtzError('您的仙桃网页授权已过期或无效,请重新运行 `xt tool login` 或 `xt login`。');
22
+ throw new XtzError('您的仙桃网页授权已过期或无效,请重新运行 `xt login`。');
23
23
  }
24
- throw new XtzError('您的授权已过期或无效,请重新运行 `xt login` 或 `xt tool login`。');
24
+ throw new XtzError('您的授权已过期或无效,请重新运行 `xt login`。');
25
25
  }
26
26
  if (message.includes('无权限') || message.includes('权限')) {
27
27
  throw new XtzError('需购买仙桃高级版 https://www.xiantaozi.com/');
@@ -0,0 +1,13 @@
1
+ import { loginAuth, waitForAuthorizedToken } from './auth.js';
2
+ import { loginXiantao } from './xiantao-auth.js';
3
+ export async function loginAll(agent, openAuthBrowser = true, onProgress) {
4
+ const auth = await loginAuth(agent, openAuthBrowser);
5
+ onProgress?.({ auth, phase: 'auth_started' });
6
+ await waitForAuthorizedToken(auth.token);
7
+ onProgress?.({ auth, phase: 'auth_confirmed' });
8
+ const xiantao = await loginXiantao(agent);
9
+ return {
10
+ auth,
11
+ xiantao,
12
+ };
13
+ }
@@ -57,14 +57,14 @@ export async function requireToken(agent) {
57
57
  if (token) {
58
58
  return token;
59
59
  }
60
- throw new XtzError(`未找到 ${agent} 的 token,请先运行 \`xt login --profile ${agent}\` 或 \`xt tool login --profile ${agent}\``);
60
+ throw new XtzError(`未找到 ${agent} 的 token,请先运行 \`xt login --profile ${agent}\``);
61
61
  }
62
62
  export async function requireXiantaoToken(agent) {
63
63
  const token = await loadXiantaoToken(agent);
64
64
  if (token) {
65
65
  return token;
66
66
  }
67
- throw new XtzError(`未找到 ${agent} 的仙桃网页 token,请先运行 \`xt tool login --profile ${agent}\` 或 \`xt login --profile ${agent}\``);
67
+ throw new XtzError(`未找到 ${agent} 的仙桃网页 token,请先运行 \`xt login --profile ${agent}\``);
68
68
  }
69
69
  export async function deleteToken(agent) {
70
70
  const tokenPath = getTokenPath(agent);
@@ -2,7 +2,7 @@ import http from 'node:http';
2
2
  import { PASSPORT_JUMP_ENDPOINT } from './constants.js';
3
3
  import { openUrl } from './open-url.js';
4
4
  import { XtzError } from './errors.js';
5
- import { getXiantaoTokenPath, loadXiantaoToken, requireXiantaoToken, saveXiantaoToken } from './storage.js';
5
+ import { loadXiantaoToken, requireXiantaoToken, saveXiantaoToken } from './storage.js';
6
6
  export async function getXiantaoAuthContext(agent, tokenOverride) {
7
7
  const token = tokenOverride?.trim() || process.env.XIANTAO_TOKEN?.trim() || await requireXiantaoToken(agent);
8
8
  return { agent, token };
@@ -27,22 +27,6 @@ export async function loginXiantao(agent) {
27
27
  tokenPath,
28
28
  };
29
29
  }
30
- export async function ensureXiantaoLogin(agent) {
31
- const token = await loadXiantaoToken(agent);
32
- if (token) {
33
- return {
34
- browserOpened: false,
35
- created: false,
36
- token,
37
- tokenPath: getXiantaoTokenPath(agent),
38
- };
39
- }
40
- const result = await loginXiantao(agent);
41
- return {
42
- ...result,
43
- created: true,
44
- };
45
- }
46
30
  async function createLoginSession() {
47
31
  let resolveToken;
48
32
  let rejectToken;
package/dist/xtz-help.js CHANGED
@@ -4,14 +4,14 @@ export default class XtzHelp extends Help {
4
4
  this.log(this.formatRoot());
5
5
  this.log('');
6
6
  this.log(this.section('MAIN FLOWS', [
7
- ['First use', '$ xt tool login\n$ xt tool search violin\n$ xt tool inspect violin_flat\n$ xt tool run violin_flat ./data.xlsx'],
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
9
  ['Agent automation', '$ xt tool:prepare violin_flat --file ./data.xlsx --profile openclaw --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile openclaw --json'],
10
10
  ]));
11
11
  this.log('');
12
12
  this.log(this.section('COMMON COMMANDS', this.renderCommandItems([
13
13
  {
14
- description: 'Sign in for upload-related APIs and backfill Xiantao web auth if missing',
14
+ description: 'Sign in for the current profile and refresh both auth tokens',
15
15
  id: 'login',
16
16
  },
17
17
  {
@@ -23,7 +23,7 @@ export default class XtzHelp extends Help {
23
23
  id: 'logout',
24
24
  },
25
25
  {
26
- description: 'Sign in for Xiantao web APIs and backfill upload auth if missing',
26
+ description: 'Alias of xt login under the tool topic',
27
27
  id: 'tool:login',
28
28
  },
29
29
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixlife-ai/xiantao",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for Xiantao bioinformatics tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@
55
55
  }
56
56
  },
57
57
  "dependencies": {
58
- "@helixlife-ai/xiantao": "^0.1.2",
58
+ "@helixlife-ai/xiantao": "^0.1.3",
59
59
  "@oclif/core": "^4.9.0",
60
60
  "@oclif/plugin-autocomplete": "^3.2.41"
61
61
  },