@cnbcool/cnb-api-generate 1.2.5 → 1.2.7
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/client/index.ts +1 -1
- package/client/utils/upload.ts +147 -19
- package/package.json +1 -1
package/client/index.ts
CHANGED
|
@@ -498,7 +498,7 @@ async function main() {
|
|
|
498
498
|
// 上传快捷命令:走完整上传流程(获取 URL → PUT 文件 → 返回结果)
|
|
499
499
|
let data: any;
|
|
500
500
|
if (shortcut?.upload) {
|
|
501
|
-
data = await handleUpload(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams);
|
|
501
|
+
data = await handleUpload(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams as { repo: string; number?: string });
|
|
502
502
|
} else {
|
|
503
503
|
if (formattedParams.data) {
|
|
504
504
|
toolsParam.push(formattedParams.data);
|
package/client/utils/upload.ts
CHANGED
|
@@ -8,27 +8,148 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import nodePath from 'path';
|
|
11
|
-
import { lookup as mimeLookup } from 'mime-types';
|
|
12
11
|
import type { ResolvedShortcut } from '../shortcuts';
|
|
13
12
|
|
|
13
|
+
/** 常见文件扩展名 → MIME 类型映射(仅用于上传场景,不引入外部依赖) */
|
|
14
|
+
const MIME_MAP: Record<string, string> = {
|
|
15
|
+
'.txt': 'text/plain',
|
|
16
|
+
'.html': 'text/html', '.htm': 'text/html',
|
|
17
|
+
'.css': 'text/css',
|
|
18
|
+
'.csv': 'text/csv',
|
|
19
|
+
'.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
20
|
+
'.json': 'application/json',
|
|
21
|
+
'.xml': 'application/xml',
|
|
22
|
+
'.pdf': 'application/pdf',
|
|
23
|
+
'.zip': 'application/zip',
|
|
24
|
+
'.gz': 'application/gzip', '.gzip': 'application/gzip',
|
|
25
|
+
'.tar': 'application/x-tar',
|
|
26
|
+
'.7z': 'application/x-7z-compressed',
|
|
27
|
+
'.rar': 'application/vnd.rar',
|
|
28
|
+
'.doc': 'application/msword',
|
|
29
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
30
|
+
'.xls': 'application/vnd.ms-excel',
|
|
31
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
32
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
33
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
34
|
+
'.png': 'image/png',
|
|
35
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
36
|
+
'.gif': 'image/gif',
|
|
37
|
+
'.svg': 'image/svg+xml',
|
|
38
|
+
'.webp': 'image/webp',
|
|
39
|
+
'.ico': 'image/x-icon',
|
|
40
|
+
'.bmp': 'image/bmp',
|
|
41
|
+
'.mp3': 'audio/mpeg',
|
|
42
|
+
'.wav': 'audio/wav',
|
|
43
|
+
'.ogg': 'audio/ogg',
|
|
44
|
+
'.mp4': 'video/mp4',
|
|
45
|
+
'.webm': 'video/webm',
|
|
46
|
+
'.avi': 'video/x-msvideo',
|
|
47
|
+
'.mov': 'video/quicktime',
|
|
48
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2',
|
|
49
|
+
'.ttf': 'font/ttf',
|
|
50
|
+
'.otf': 'font/otf',
|
|
51
|
+
'.md': 'text/markdown',
|
|
52
|
+
'.yaml': 'text/yaml', '.yml': 'text/yaml',
|
|
53
|
+
'.sh': 'application/x-sh',
|
|
54
|
+
'.py': 'text/x-python',
|
|
55
|
+
'.rb': 'text/x-ruby',
|
|
56
|
+
'.go': 'text/x-go',
|
|
57
|
+
'.rs': 'text/x-rust',
|
|
58
|
+
'.ts': 'text/typescript', '.tsx': 'text/typescript',
|
|
59
|
+
'.jsx': 'text/jsx',
|
|
60
|
+
'.java': 'text/x-java-source',
|
|
61
|
+
'.c': 'text/x-c', '.h': 'text/x-c',
|
|
62
|
+
'.cpp': 'text/x-c++src', '.hpp': 'text/x-c++hdr',
|
|
63
|
+
'.log': 'text/plain',
|
|
64
|
+
'.patch': 'text/x-diff', '.diff': 'text/x-diff',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function mimeLookup(filePath: string): string {
|
|
68
|
+
const ext = nodePath.extname(filePath).toLowerCase();
|
|
69
|
+
return MIME_MAP[ext] || 'application/octet-stream';
|
|
70
|
+
}
|
|
71
|
+
|
|
14
72
|
// 上传文件大小上限:100MB
|
|
15
73
|
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
16
74
|
|
|
75
|
+
/** 上传 API 的 path 参数 */
|
|
76
|
+
interface UploadPathParams {
|
|
77
|
+
repo: string;
|
|
78
|
+
number?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Issue 上传请求体 */
|
|
82
|
+
interface IssueUploadBody {
|
|
83
|
+
name: string;
|
|
84
|
+
size: number;
|
|
85
|
+
content_type: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Pull 上传请求体 */
|
|
89
|
+
interface PullUploadBody {
|
|
90
|
+
name: string;
|
|
91
|
+
size: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 上传 API 函数签名 */
|
|
95
|
+
type UploadApiFunction = (
|
|
96
|
+
params: UploadPathParams,
|
|
97
|
+
body: IssueUploadBody | PullUploadBody,
|
|
98
|
+
) => Promise<UploadApiResponse>;
|
|
99
|
+
|
|
100
|
+
/** 上传 API 响应 */
|
|
101
|
+
interface UploadApiResponse {
|
|
102
|
+
status?: number;
|
|
103
|
+
data?: {
|
|
104
|
+
upload_url?: string;
|
|
105
|
+
asset_link?: string;
|
|
106
|
+
assets?: { name?: string; path?: string };
|
|
107
|
+
token?: string;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 上传失败时的 data */
|
|
112
|
+
interface UploadErrorData {
|
|
113
|
+
error: string;
|
|
114
|
+
detail?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Issue 上传成功时的 data */
|
|
118
|
+
interface IssueUploadData {
|
|
119
|
+
asset_link?: string;
|
|
120
|
+
name: string;
|
|
121
|
+
size: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Pull 上传成功时的 data */
|
|
125
|
+
interface PullUploadData {
|
|
126
|
+
name: string;
|
|
127
|
+
path?: string;
|
|
128
|
+
size: number;
|
|
129
|
+
token?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** handleUpload 的返回值 */
|
|
133
|
+
interface UploadResult {
|
|
134
|
+
status: number;
|
|
135
|
+
data: UploadErrorData | IssueUploadData | PullUploadData;
|
|
136
|
+
}
|
|
137
|
+
|
|
17
138
|
/**
|
|
18
139
|
* 处理完整上传流程
|
|
19
140
|
* @param shortcut 解析后的快捷命令
|
|
20
141
|
* @param filePath 本地文件路径
|
|
21
142
|
* @param toolFunction 原始上传 API 函数(获取 upload_url)
|
|
22
|
-
* @param
|
|
143
|
+
* @param pathParams API 调用的 path 参数(repo 或 {repo, number})
|
|
23
144
|
*/
|
|
24
145
|
export async function handleUpload(
|
|
25
146
|
shortcut: ResolvedShortcut,
|
|
26
|
-
filePath:
|
|
27
|
-
toolFunction:
|
|
28
|
-
|
|
29
|
-
): Promise<
|
|
147
|
+
filePath: string | undefined,
|
|
148
|
+
toolFunction: UploadApiFunction,
|
|
149
|
+
pathParams: UploadPathParams,
|
|
150
|
+
): Promise<UploadResult> {
|
|
30
151
|
// 校验 file 参数
|
|
31
|
-
if (!filePath
|
|
152
|
+
if (!filePath) {
|
|
32
153
|
return { status: 400, data: { error: `上传命令需要指定文件路径,如: --data '{"file":"./path/to/file"}'` } };
|
|
33
154
|
}
|
|
34
155
|
|
|
@@ -52,7 +173,7 @@ export async function handleUpload(
|
|
|
52
173
|
return { status: 400, data: { error: `文件过大 (${(fileSize / 1024 / 1024).toFixed(1)}MB),上限 ${MAX_FILE_SIZE / 1024 / 1024}MB` } };
|
|
53
174
|
}
|
|
54
175
|
|
|
55
|
-
const contentType = mimeLookup(filePath)
|
|
176
|
+
const contentType = mimeLookup(filePath);
|
|
56
177
|
|
|
57
178
|
// 2. 调用上传 API 获取 upload_url
|
|
58
179
|
const isIssueUpload = shortcut.module === 'issues';
|
|
@@ -60,14 +181,14 @@ export async function handleUpload(
|
|
|
60
181
|
? { name: fileName, size: fileSize, content_type: contentType }
|
|
61
182
|
: { name: fileName, size: fileSize };
|
|
62
183
|
|
|
63
|
-
const uploadResponse = await toolFunction(
|
|
184
|
+
const uploadResponse = await toolFunction(pathParams, requestBody);
|
|
64
185
|
|
|
65
|
-
// 提取 upload_url
|
|
66
|
-
const responseData = uploadResponse?.data
|
|
186
|
+
// 提取 upload_url
|
|
187
|
+
const responseData = uploadResponse?.data;
|
|
67
188
|
const uploadUrl = responseData?.upload_url;
|
|
68
189
|
|
|
69
190
|
if (!uploadUrl) {
|
|
70
|
-
return uploadResponse; // 获取 URL 失败,直接返回原始错误
|
|
191
|
+
return uploadResponse as unknown as UploadResult; // 获取 URL 失败,直接返回原始错误
|
|
71
192
|
}
|
|
72
193
|
|
|
73
194
|
// 3. PUT 文件内容到 upload_url(流式读取,避免大文件撑爆内存)
|
|
@@ -78,15 +199,22 @@ export async function handleUpload(
|
|
|
78
199
|
'Content-Length': String(fileSize),
|
|
79
200
|
};
|
|
80
201
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
202
|
+
let putResponse: Response;
|
|
203
|
+
try {
|
|
204
|
+
putResponse = await fetch(uploadUrl, {
|
|
205
|
+
method: 'PUT',
|
|
206
|
+
headers: putHeaders,
|
|
207
|
+
body: fileStream as any,
|
|
208
|
+
// @ts-ignore duplex required for streaming body in Node.js fetch
|
|
209
|
+
duplex: 'half',
|
|
210
|
+
});
|
|
211
|
+
} catch (e) {
|
|
212
|
+
fileStream.destroy();
|
|
213
|
+
throw e;
|
|
214
|
+
}
|
|
88
215
|
|
|
89
216
|
if (!putResponse.ok) {
|
|
217
|
+
fileStream.destroy();
|
|
90
218
|
const errText = await putResponse.text().catch(() => '');
|
|
91
219
|
return {
|
|
92
220
|
status: putResponse.status,
|