@meet-im/lxcli 0.0.3 → 0.0.5

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.
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import { outputCliError } from '../core/error.js';
5
5
  import { logger } from '../core/logger.js';
6
6
  import { PARAM_DEFS, METHOD_DESCRIPTIONS } from './services/kb/descriptions.js';
7
+ import { METHOD_SKILL_METADATA } from './services/kb/skill-metadata.js';
7
8
  const API_ROOT = path.resolve('api');
8
9
  const DEFAULT_SKILLS_DIR = path.resolve('api/skills');
9
10
  const DEFAULT_OUTPUT = 'api/docs/skills.md';
@@ -157,6 +158,7 @@ async function readPackageVersion() {
157
158
  function renderSkillMd(method, version) {
158
159
  const description = METHOD_DESCRIPTIONS[method];
159
160
  const params = PARAM_DEFS[method] || [];
161
+ const metadata = METHOD_SKILL_METADATA[method];
160
162
  const skillName = `lxcli-kb-${method}`;
161
163
  const hasParams = params.length > 0;
162
164
  // 快捷参数列表
@@ -223,6 +225,9 @@ metadata:
223
225
  lines.push(renderParamsTable(params));
224
226
  }
225
227
  lines.push('## Examples', '', '```bash', shortcutUsage || `lxcli kb ${method}`, '```', '');
228
+ if (metadata?.notes?.length) {
229
+ lines.push('## Notes', '', ...metadata.notes.map((note) => `- ${note}`), '');
230
+ }
226
231
  return lines.join('\n');
227
232
  }
228
233
  async function generateServiceSkills(skillsDir, serviceName) {
@@ -2,25 +2,25 @@
2
2
  // kanboard gateway 配置(按企业、按环境)
3
3
  // 实际地址需要根据部署环境配置
4
4
  const meetGateways = {
5
- test: 'https://kanboard-test.miyachat.com',
5
+ test: 'https://kanboard-staging.miyachat.com',
6
6
  pre: 'https://kanboard-pre.miyachat.com',
7
7
  staging: 'https://kanboard-staging.miyachat.com',
8
- beta: 'https://kanboard-beta.miyachat.com',
8
+ beta: 'https://kanboard-staging.miyachat.com',
9
9
  prod: 'https://kanboard.miyachat.com',
10
10
  };
11
11
  const coGateways = {
12
- test: 'https://kanboard-test.co.example.com',
13
- pre: 'https://kanboard-pre.co.example.com',
14
- staging: 'https://kanboard-staging.co.example.com',
15
- beta: 'https://kanboard-beta.co.example.com',
16
- prod: 'https://kanboard.co.example.com',
12
+ test: 'https://kanboard.mecord668.com',
13
+ pre: 'https://kanboard.mecord668.com',
14
+ staging: 'https://kanboard.mecord668.com',
15
+ beta: 'https://kanboard.mecord668.com',
16
+ prod: 'https://kanboard.mecord668.com',
17
17
  };
18
18
  const skyGateways = {
19
- test: 'https://kanboard-test.sky.example.com',
20
- pre: 'https://kanboard-pre.sky.example.com',
21
- staging: 'https://kanboard-staging.sky.example.com',
22
- beta: 'https://kanboard-beta.sky.example.com',
23
- prod: 'https://kanboard.sky.example.com',
19
+ test: 'https://kanboard.2tianxin.com',
20
+ pre: 'https://kanboard.2tianxin.com',
21
+ staging: 'https://kanboard.2tianxin.com',
22
+ beta: 'https://kanboard.2tianxin.com',
23
+ prod: 'https://kanboard.2tianxin.com',
24
24
  };
25
25
  export const kbConfig = {
26
26
  name: 'kb',
@@ -20,7 +20,9 @@ export const METHOD_DESCRIPTIONS = {
20
20
  getAllComments: '获取评论列表',
21
21
  createComment: '对需求增加评论',
22
22
  createProjectFile: '上传附件到项目',
23
+ downloadProjectFile: '下载项目附件',
23
24
  createTaskFile: '上传附件到需求',
25
+ downloadTaskFile: '下载需求附件',
24
26
  };
25
27
  // 重新导出 params.ts 的类型和定义,方便其他模块使用
26
28
  export { PARAM_DEFS } from './params.js';
@@ -4,9 +4,15 @@ export interface KbCommandOptions {
4
4
  task_id?: string;
5
5
  title?: string;
6
6
  project_id?: string;
7
+ file_id?: string;
8
+ owner_id?: string;
9
+ category_id?: string;
7
10
  query?: string;
11
+ user_id?: string;
12
+ content?: string;
8
13
  user?: string;
9
14
  gateway?: string;
10
15
  file?: string;
16
+ out?: string;
11
17
  }
12
18
  export declare function createKbAction(method: Method): (options: KbCommandOptions) => Promise<void>;
@@ -1,4 +1,4 @@
1
- import { readFile } from 'fs/promises';
1
+ import { readFile, writeFile } from 'fs/promises';
2
2
  import { basename } from 'path';
3
3
  import { getCurrentUser, getGatewayForUser } from '../../../core/auth.js';
4
4
  import { outputCliError } from '../../../core/error.js';
@@ -87,13 +87,15 @@ export function createKbAction(method) {
87
87
  }
88
88
  params = { task_id: taskId };
89
89
  }
90
- else if (method === 'createTask' && options.title && options.project_id) {
90
+ else if (method === 'createTask' && options.title && options.project_id && options.owner_id && options.category_id) {
91
91
  const projectId = parseTaskId(options.project_id);
92
- if (projectId === null) {
93
- outputCliError('PARAM_INVALID', '--project_id must be a positive integer');
92
+ const ownerId = parseTaskId(options.owner_id);
93
+ const categoryId = parseTaskId(options.category_id);
94
+ if (projectId === null || ownerId === null || categoryId === null) {
95
+ outputCliError('PARAM_INVALID', '--project_id, --owner_id and --category_id must be positive integers');
94
96
  return;
95
97
  }
96
- params = { title: options.title, project_id: projectId };
98
+ params = { title: options.title, project_id: projectId, owner_id: ownerId, category_id: categoryId };
97
99
  }
98
100
  else if (method === 'searchTasks' && options.project_id && options.query) {
99
101
  const projectId = parseTaskId(options.project_id);
@@ -127,6 +129,24 @@ export function createKbAction(method) {
127
129
  }
128
130
  params = { task_id: taskId };
129
131
  }
132
+ else if (method === 'createComment' && options.task_id && options.user_id && options.content) {
133
+ const taskId = parseTaskId(options.task_id);
134
+ const userId = options.user_id === '0' ? 0 : parseTaskId(options.user_id);
135
+ if (taskId === null || userId === null) {
136
+ outputCliError('PARAM_INVALID', '--task_id and --user_id must be non-negative integers');
137
+ return;
138
+ }
139
+ params = { task_id: taskId, user_id: userId, content: options.content };
140
+ }
141
+ else if (method === 'downloadProjectFile' && options.project_id && options.file_id) {
142
+ const projectId = parseTaskId(options.project_id);
143
+ const fileId = parseTaskId(options.file_id);
144
+ if (projectId === null || fileId === null) {
145
+ outputCliError('PARAM_INVALID', '--project_id and --file_id must be positive integers');
146
+ return;
147
+ }
148
+ params = { project_id: projectId, file_id: fileId };
149
+ }
130
150
  else if (method === 'createProjectFile' && options.file && options.project_id) {
131
151
  const projectId = parseTaskId(options.project_id);
132
152
  if (projectId === null) {
@@ -144,6 +164,14 @@ export function createKbAction(method) {
144
164
  return;
145
165
  }
146
166
  }
167
+ else if (method === 'downloadTaskFile' && options.file_id) {
168
+ const fileId = parseTaskId(options.file_id);
169
+ if (fileId === null) {
170
+ outputCliError('PARAM_INVALID', '--file_id must be a positive integer');
171
+ return;
172
+ }
173
+ params = { file_id: fileId };
174
+ }
147
175
  else if (method === 'createTaskFile' && options.file && options.task_id && options.project_id) {
148
176
  const taskId = parseTaskId(options.task_id);
149
177
  const projectId = parseTaskId(options.project_id);
@@ -169,12 +197,15 @@ export function createKbAction(method) {
169
197
  else {
170
198
  const shortcuts = {
171
199
  getTask: '--task_id',
172
- createTask: '--title and --project_id',
200
+ createTask: '--title, --project_id, --owner_id and --category_id',
173
201
  searchTasks: '--project_id and --query',
174
202
  getAllActiveIterations: '--project_id',
175
203
  getAllCategories: '--project_id',
176
204
  getAllComments: '--task_id',
205
+ createComment: '--task_id, --user_id and --content',
206
+ downloadProjectFile: '--project_id and --file_id',
177
207
  createProjectFile: '--file and --project_id',
208
+ downloadTaskFile: '--file_id',
178
209
  createTaskFile: '--file, --task_id and --project_id',
179
210
  };
180
211
  outputCliError('PARAM_INVALID', shortcuts[method]
@@ -183,6 +214,21 @@ export function createKbAction(method) {
183
214
  return;
184
215
  }
185
216
  const result = await jsonRpcCall(gateway, method, params, user.token);
217
+ if ((method === 'downloadProjectFile' || method === 'downloadTaskFile') && options.out) {
218
+ if (typeof result !== 'string') {
219
+ outputCliError('PARAM_INVALID', `${method} result must be a Base64 string`);
220
+ return;
221
+ }
222
+ try {
223
+ const buffer = Buffer.from(result, 'base64');
224
+ await writeFile(options.out, buffer);
225
+ outputJson({ success: true, path: options.out, bytes: buffer.length });
226
+ }
227
+ catch (error) {
228
+ outputCliError('PARAM_INVALID', `Failed to write file: ${options.out}`);
229
+ }
230
+ return;
231
+ }
186
232
  outputJson(result);
187
233
  };
188
234
  }
@@ -38,13 +38,15 @@ const kbService = {
38
38
  .option('--user <user>', 'Specify user')
39
39
  .option('--gateway <url>', 'Override kanboard gateway')
40
40
  .action(createKbAction('getTask'));
41
- // createTask 支持快捷参数 --title / --project_id,且 --params 仍然可用
41
+ // createTask 支持快捷参数 --title / --project_id / --owner_id / --category_id,且 --params 仍然可用
42
42
  cmd
43
43
  .command('createTask')
44
44
  .description(METHOD_DESCRIPTIONS.createTask)
45
45
  .option('--params <json>', 'JSON/JSON5 params; CLI fills jsonrpc/method/id')
46
46
  .option('--title <title>', 'Task title (shortcut)')
47
47
  .option('--project_id <id>', 'Project ID (shortcut)')
48
+ .option('--owner_id <id>', 'Owner user ID (shortcut)')
49
+ .option('--category_id <id>', 'Category ID (shortcut)')
48
50
  .option('--user <user>', 'Specify user')
49
51
  .option('--gateway <url>', 'Override kanboard gateway')
50
52
  .action(createKbAction('createTask'));
@@ -85,6 +87,17 @@ const kbService = {
85
87
  .option('--user <user>', 'Specify user')
86
88
  .option('--gateway <url>', 'Override kanboard gateway')
87
89
  .action(createKbAction('getAllComments'));
90
+ // createComment 支持快捷参数 --task_id / --user_id / --content
91
+ cmd
92
+ .command('createComment')
93
+ .description(METHOD_DESCRIPTIONS.createComment)
94
+ .option('--params <json>', 'JSON/JSON5 params; CLI fills jsonrpc/method/id')
95
+ .option('--task_id <id>', 'Task ID (shortcut)')
96
+ .option('--user_id <id>', 'Comment author user ID (shortcut)')
97
+ .option('--content <content>', 'Comment content (shortcut)')
98
+ .option('--user <user>', 'Specify user')
99
+ .option('--gateway <url>', 'Override kanboard gateway')
100
+ .action(createKbAction('createComment'));
88
101
  for (const method of METHODS.filter((item) => item !== 'getMe' &&
89
102
  item !== 'getMyProjects' &&
90
103
  item !== 'getTask' &&
@@ -93,7 +106,10 @@ const kbService = {
93
106
  item !== 'getAllActiveIterations' &&
94
107
  item !== 'getAllCategories' &&
95
108
  item !== 'getAllComments' &&
109
+ item !== 'createComment' &&
110
+ item !== 'downloadProjectFile' &&
96
111
  item !== 'createTaskFile' &&
112
+ item !== 'downloadTaskFile' &&
97
113
  item !== 'createProjectFile')) {
98
114
  cmd
99
115
  .command(method)
@@ -113,6 +129,17 @@ const kbService = {
113
129
  .option('--user <user>', 'Specify user')
114
130
  .option('--gateway <url>', 'Override kanboard gateway')
115
131
  .action(createKbAction('createProjectFile'));
132
+ // downloadProjectFile 支持快捷参数 --project_id / --file_id,可选 --out 落盘
133
+ cmd
134
+ .command('downloadProjectFile')
135
+ .description(METHOD_DESCRIPTIONS.downloadProjectFile)
136
+ .option('--params <json>', 'JSON/JSON5 params; CLI fills jsonrpc/method/id')
137
+ .option('--project_id <id>', 'Project ID (shortcut)')
138
+ .option('--file_id <id>', 'Project file ID (shortcut)')
139
+ .option('--out <path>', 'Write decoded file to path')
140
+ .option('--user <user>', 'Specify user')
141
+ .option('--gateway <url>', 'Override kanboard gateway')
142
+ .action(createKbAction('downloadProjectFile'));
116
143
  // createTaskFile 支持快捷参数 --file / --task_id / --project_id,自动转 base64
117
144
  cmd
118
145
  .command('createTaskFile')
@@ -124,6 +151,16 @@ const kbService = {
124
151
  .option('--user <user>', 'Specify user')
125
152
  .option('--gateway <url>', 'Override kanboard gateway')
126
153
  .action(createKbAction('createTaskFile'));
154
+ // downloadTaskFile 支持快捷参数 --file_id,可选 --out 落盘
155
+ cmd
156
+ .command('downloadTaskFile')
157
+ .description(METHOD_DESCRIPTIONS.downloadTaskFile)
158
+ .option('--params <json>', 'JSON/JSON5 params; CLI fills jsonrpc/method/id')
159
+ .option('--file_id <id>', 'Task file ID (shortcut)')
160
+ .option('--out <path>', 'Write decoded file to path')
161
+ .option('--user <user>', 'Specify user')
162
+ .option('--gateway <url>', 'Override kanboard gateway')
163
+ .action(createKbAction('downloadTaskFile'));
127
164
  },
128
165
  };
129
166
  export default kbService;
@@ -77,10 +77,17 @@ export const PARAM_DEFS = {
77
77
  { name: 'filename', type: 'string', required: true, description: '文件名' },
78
78
  { name: 'blob', type: 'string', required: true, description: '文件内容 Base64 编码' },
79
79
  ],
80
+ downloadProjectFile: [
81
+ { name: 'project_id', type: 'integer', required: true, description: '项目 ID' },
82
+ { name: 'file_id', type: 'integer', required: true, description: '项目附件 ID' },
83
+ ],
80
84
  createTaskFile: [
81
85
  { name: 'project_id', type: 'integer', required: true, description: '项目 ID' },
82
86
  { name: 'task_id', type: 'integer', required: true, description: '需求/bug ID' },
83
87
  { name: 'filename', type: 'string', required: true, description: '文件名' },
84
88
  { name: 'blob', type: 'string', required: true, description: '文件内容 Base64 编码' },
85
89
  ],
90
+ downloadTaskFile: [
91
+ { name: 'file_id', type: 'integer', required: true, description: '需求附件 ID' },
92
+ ],
86
93
  };
@@ -2,13 +2,14 @@ import { outputCliError } from '../../../core/error.js';
2
2
  import { request } from '../../../core/http.js';
3
3
  import { getNextRequestId } from './utils.js';
4
4
  export async function jsonRpcCall(gateway, method, params, pat) {
5
+ const requestId = getNextRequestId();
5
6
  const requestBody = {
6
7
  jsonrpc: '2.0',
7
8
  method,
8
- id: getNextRequestId(),
9
+ id: requestId,
9
10
  params,
10
11
  };
11
- const response = await request({
12
+ const result = await request({
12
13
  gateway,
13
14
  path: '/jsonrpc.php',
14
15
  method: 'POST',
@@ -16,11 +17,26 @@ export async function jsonRpcCall(gateway, method, params, pat) {
16
17
  headers: {
17
18
  Authorization: `meet ${pat}`,
18
19
  },
20
+ throwOnError: false,
19
21
  });
22
+ // HTTP 层错误
23
+ if (result.error) {
24
+ outputCliError('NETWORK_ERROR', result.error.message, {
25
+ code: result.error.code,
26
+ details: result.error.details,
27
+ requestId,
28
+ });
29
+ return undefined;
30
+ }
31
+ const response = result.data;
32
+ // JSON-RPC 层错误
20
33
  if (response.error) {
21
34
  outputCliError('NETWORK_ERROR', response.error.message, {
22
35
  code: response.error.code,
23
36
  data: response.error.data,
37
+ requestId,
38
+ // 服务端返回的 id(可能为 null)
39
+ responseId: response.id,
24
40
  });
25
41
  return undefined;
26
42
  }
@@ -28,6 +44,7 @@ export async function jsonRpcCall(gateway, method, params, pat) {
28
44
  outputCliError('NETWORK_ERROR', 'No result returned from Kanboard API', {
29
45
  method,
30
46
  params,
47
+ requestId,
31
48
  });
32
49
  return undefined;
33
50
  }
@@ -0,0 +1,5 @@
1
+ import type { Method } from './descriptions.js';
2
+ export interface MethodSkillMetadata {
3
+ notes?: string[];
4
+ }
5
+ export declare const METHOD_SKILL_METADATA: Partial<Record<Method, MethodSkillMetadata>>;
@@ -0,0 +1,15 @@
1
+ export const METHOD_SKILL_METADATA = {
2
+ downloadProjectFile: {
3
+ notes: [
4
+ '当附件链接包含 `project_id` 时,使用 `downloadProjectFile`。',
5
+ '例如:`/FileViewer/browser?project_id=4&file_id=168`。',
6
+ '例如:`/?controller=FileViewerController&action=browser&project_id=4&file_id=168`。',
7
+ ],
8
+ },
9
+ downloadTaskFile: {
10
+ notes: [
11
+ '当附件链接包含 `task_id` 时,使用 `downloadTaskFile`。',
12
+ '例如:`?controller=FileViewerController&action=browser&task_id=2000757&file_id=3870`。',
13
+ ],
14
+ },
15
+ };
@@ -33,6 +33,7 @@ export interface KanboardTask {
33
33
  date_due?: string;
34
34
  date_started?: string;
35
35
  priority: string;
36
+ priority_name?: string;
36
37
  score: string;
37
38
  reference: string;
38
39
  url: string;
@@ -43,6 +44,17 @@ export interface KanboardTask {
43
44
  };
44
45
  tags?: string[];
45
46
  }
47
+ export interface KanboardSubtask {
48
+ id: string;
49
+ title: string;
50
+ status: string;
51
+ status_name?: string;
52
+ time_estimated: string;
53
+ time_spent: string;
54
+ task_id: string;
55
+ user_id: string;
56
+ position: string;
57
+ }
46
58
  export interface GetTaskParams {
47
59
  task_id: number;
48
60
  }
@@ -1,2 +1,6 @@
1
1
  import { Command } from 'commander';
2
+ import { spawn } from 'child_process';
3
+ export declare const upgradeDeps: {
4
+ spawn: typeof spawn;
5
+ };
2
6
  export declare function registerUpgradeCommand(program: Command): void;
@@ -1,8 +1,12 @@
1
1
  // src/commands/upgrade.ts
2
+ import { spawn } from 'child_process';
2
3
  import { outputCliError } from '../core/error.js';
3
4
  import { outputJson } from '../utils/index.js';
4
5
  const PACKAGE_NAME = '@meet-im/lxcli';
5
6
  const NPM_REGISTRY = 'https://registry.npmjs.org';
7
+ export const upgradeDeps = {
8
+ spawn,
9
+ };
6
10
  async function getLatestVersion() {
7
11
  const res = await fetch(`${NPM_REGISTRY}/${PACKAGE_NAME}/latest`);
8
12
  if (!res.ok) {
@@ -11,22 +15,52 @@ async function getLatestVersion() {
11
15
  const data = await res.json();
12
16
  return data.version;
13
17
  }
18
+ async function runNpmUpgrade() {
19
+ const command = process.platform === 'win32' ? 'cmd.exe' : 'npm';
20
+ const args = process.platform === 'win32'
21
+ ? ['/c', 'npm', 'install', '-g', `${PACKAGE_NAME}@latest`]
22
+ : ['install', '-g', `${PACKAGE_NAME}@latest`];
23
+ await new Promise((resolve, reject) => {
24
+ const child = upgradeDeps.spawn(command, args, {
25
+ stdio: 'inherit',
26
+ });
27
+ child.on('error', reject);
28
+ child.on('exit', (code) => {
29
+ if (code === 0) {
30
+ resolve();
31
+ return;
32
+ }
33
+ reject(new Error(`npm install exited with code ${code ?? 'unknown'}`));
34
+ });
35
+ });
36
+ }
14
37
  export function registerUpgradeCommand(program) {
15
38
  program
16
39
  .command('upgrade')
17
40
  .description('Check for updates and show upgrade instructions')
18
- .action(async () => {
41
+ .option('--check', 'Only check for updates without installing')
42
+ .action(async (options) => {
19
43
  try {
20
44
  const currentVersion = program.version() || '1.0.0';
21
45
  const latestVersion = await getLatestVersion();
22
46
  const isLatest = currentVersion === latestVersion;
47
+ if (options.check || isLatest) {
48
+ outputJson({
49
+ currentVersion,
50
+ latestVersion,
51
+ isLatest,
52
+ message: isLatest
53
+ ? 'Already on the latest version'
54
+ : `Run "npm install -g ${PACKAGE_NAME}@latest" to upgrade`,
55
+ });
56
+ return;
57
+ }
58
+ await runNpmUpgrade();
23
59
  outputJson({
24
- currentVersion,
60
+ previousVersion: currentVersion,
25
61
  latestVersion,
26
- isLatest,
27
- message: isLatest
28
- ? 'Already on the latest version'
29
- : `Run "npm install -g ${PACKAGE_NAME}@latest" to upgrade`,
62
+ upgraded: true,
63
+ message: `Upgraded ${PACKAGE_NAME} to ${latestVersion}`,
30
64
  });
31
65
  }
32
66
  catch (error) {
@@ -1,4 +1,16 @@
1
- export interface RequestOptions {
1
+ export interface RequestResult<T> {
2
+ /** HTTP 状态码 */
3
+ status: number;
4
+ /** 响应数据(成功时) */
5
+ data?: T;
6
+ /** 错误信息(失败时) */
7
+ error?: {
8
+ code: number;
9
+ message: string;
10
+ details?: unknown;
11
+ };
12
+ }
13
+ export interface RequestOptionsWithThrow {
2
14
  /** gateway 地址(必填) */
3
15
  gateway: string;
4
16
  /** 请求路径(不含域名) */
@@ -15,7 +27,30 @@ export interface RequestOptions {
15
27
  userAgent?: string;
16
28
  /** 最大重试次数,默认 3 */
17
29
  maxRetries?: number;
30
+ /** 错误时抛出异常而非直接退出,默认 true */
31
+ throwOnError?: true;
18
32
  }
33
+ export interface RequestOptionsWithoutThrow {
34
+ /** gateway 地址(必填) */
35
+ gateway: string;
36
+ /** 请求路径(不含域名) */
37
+ path: string;
38
+ /** HTTP 方法,默认 GET */
39
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
40
+ /** URL query 参数 */
41
+ query?: Record<string, string | number | boolean>;
42
+ /** 请求体 */
43
+ body?: Record<string, unknown>;
44
+ /** 自定义 headers(会覆盖默认 headers) */
45
+ headers?: Record<string, string>;
46
+ /** User-Agent,默认 lxcli/版本号 */
47
+ userAgent?: string;
48
+ /** 最大重试次数,默认 3 */
49
+ maxRetries?: number;
50
+ /** 错误时返回 RequestResult 而非退出 */
51
+ throwOnError: false;
52
+ }
53
+ export type RequestOptions = RequestOptionsWithThrow | RequestOptionsWithoutThrow;
19
54
  /**
20
55
  * 通用请求函数
21
56
  *
@@ -24,5 +59,8 @@ export interface RequestOptions {
24
59
  * 支持自动重试:
25
60
  * - 429 (rate limit) 响应时重试,尊重 Retry-After header
26
61
  * - 网络错误时重试,使用指数退避
62
+ *
63
+ * 当 throwOnError 为 false 时,返回 RequestResult<T> 而非直接退出。
27
64
  */
28
- export declare function request<T>(options: RequestOptions): Promise<T>;
65
+ export declare function request<T>(options: RequestOptionsWithThrow): Promise<T>;
66
+ export declare function request<T>(options: RequestOptionsWithoutThrow): Promise<RequestResult<T>>;
package/dist/core/http.js CHANGED
@@ -40,17 +40,8 @@ function joinUrl(gateway, path) {
40
40
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
41
41
  return `${normalizedGateway}${normalizedPath}`;
42
42
  }
43
- /**
44
- * 通用请求函数
45
- *
46
- * 支持灵活传递 gateway、路径、方法、参数、UA 等。
47
- *
48
- * 支持自动重试:
49
- * - 429 (rate limit) 响应时重试,尊重 Retry-After header
50
- * - 网络错误时重试,使用指数退避
51
- */
52
43
  export async function request(options) {
53
- const { gateway, path, method = 'GET', query, body, headers = {}, userAgent = `lxcli/${pkg.version}`, maxRetries = MAX_RETRIES, } = options;
44
+ const { gateway, path, method = 'GET', query, body, headers = {}, userAgent = `lxcli/${pkg.version}`, maxRetries = MAX_RETRIES, throwOnError = true, } = options;
54
45
  // 构建 URL(gateway + path + query)
55
46
  let url = joinUrl(gateway, path);
56
47
  if (query && Object.keys(query).length > 0) {
@@ -65,6 +56,13 @@ export async function request(options) {
65
56
  };
66
57
  const requestBody = body ? JSON.stringify(body) : undefined;
67
58
  let lastError = null;
59
+ // 辅助函数:处理错误响应
60
+ const makeErrorResult = (status, code, message, details) => {
61
+ if (throwOnError) {
62
+ outputApiError(code, message, details, status);
63
+ }
64
+ return { status, error: { code, message, details } };
65
+ };
68
66
  // 发送请求(带重试)
69
67
  for (let attempt = 0; attempt < maxRetries; attempt++) {
70
68
  try {
@@ -94,19 +92,32 @@ export async function request(options) {
94
92
  await sleep(delay);
95
93
  continue;
96
94
  }
97
- outputApiError(errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details, response.status);
95
+ const result = makeErrorResult(response.status, errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details);
96
+ if (!throwOnError)
97
+ return result;
98
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
98
99
  }
99
100
  let responseData;
100
101
  try {
101
102
  responseData = parseJsonResponse(responseText);
102
103
  }
103
104
  catch {
104
- outputApiError(response.status, `Invalid JSON response: ${responseText}`, undefined, response.status);
105
+ const result = makeErrorResult(response.status, response.status, `Invalid JSON response: ${responseText}`);
106
+ if (!throwOnError)
107
+ return result;
108
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
105
109
  }
106
110
  logger.debug('Response', { status: response.status, data: responseData, attempt });
107
111
  if (!response.ok) {
108
112
  const errorResponse = responseData;
109
- outputApiError(errorResponse.error?.code || response.status, errorResponse.error?.message || 'Request failed', errorResponse.error?.details, response.status);
113
+ const result = makeErrorResult(response.status, errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details);
114
+ if (!throwOnError)
115
+ return result;
116
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
117
+ }
118
+ // throwOnError 为 false 时返回完整结果
119
+ if (!throwOnError) {
120
+ return { status: response.status, data: responseData };
110
121
  }
111
122
  return responseData;
112
123
  }
@@ -125,7 +136,9 @@ export async function request(options) {
125
136
  }
126
137
  if (isNetworkError) {
127
138
  logger.error('Request failed', error);
128
- outputApiError(500, error.message, undefined, 500);
139
+ const result = makeErrorResult(500, 500, error.message);
140
+ if (!throwOnError)
141
+ return result;
129
142
  }
130
143
  throw error;
131
144
  }
@@ -133,7 +146,9 @@ export async function request(options) {
133
146
  // 所有重试都失败了
134
147
  if (lastError) {
135
148
  logger.error('All retries failed', lastError);
136
- outputApiError(500, lastError.message, undefined, 500);
149
+ const result = makeErrorResult(500, 500, lastError.message);
150
+ if (!throwOnError)
151
+ return result;
137
152
  }
138
153
  // 不应该到达这里,但 TypeScript 需要返回值
139
154
  throw new Error('Request failed after all retries');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/lxcli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "连续科技命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",