@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.
- package/dist/commands/generate-skills.js +5 -0
- package/dist/commands/services/kb/config.js +12 -12
- package/dist/commands/services/kb/descriptions.js +2 -0
- package/dist/commands/services/kb/handler.d.ts +6 -0
- package/dist/commands/services/kb/handler.js +52 -6
- package/dist/commands/services/kb/index.js +38 -1
- package/dist/commands/services/kb/params.js +7 -0
- package/dist/commands/services/kb/rpc.js +19 -2
- package/dist/commands/services/kb/skill-metadata.d.ts +5 -0
- package/dist/commands/services/kb/skill-metadata.js +15 -0
- package/dist/commands/services/kb/types.d.ts +12 -0
- package/dist/commands/upgrade.d.ts +4 -0
- package/dist/commands/upgrade.js +40 -6
- package/dist/core/http.d.ts +40 -2
- package/dist/core/http.js +30 -15
- package/package.json +1 -1
|
@@ -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-
|
|
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-
|
|
8
|
+
beta: 'https://kanboard-staging.miyachat.com',
|
|
9
9
|
prod: 'https://kanboard.miyachat.com',
|
|
10
10
|
};
|
|
11
11
|
const coGateways = {
|
|
12
|
-
test: 'https://kanboard
|
|
13
|
-
pre: 'https://kanboard
|
|
14
|
-
staging: 'https://kanboard
|
|
15
|
-
beta: 'https://kanboard
|
|
16
|
-
prod: 'https://kanboard.
|
|
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
|
|
20
|
-
pre: 'https://kanboard
|
|
21
|
-
staging: 'https://kanboard
|
|
22
|
-
beta: 'https://kanboard
|
|
23
|
-
prod: 'https://kanboard.
|
|
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
|
-
|
|
93
|
-
|
|
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 --
|
|
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:
|
|
9
|
+
id: requestId,
|
|
9
10
|
params,
|
|
10
11
|
};
|
|
11
|
-
const
|
|
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,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
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
27
|
-
message:
|
|
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) {
|
package/dist/core/http.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
export interface
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|