@cnbcool/cnb-cli 1.0.8 → 1.1.1

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.
@@ -10,6 +10,7 @@ var _path = _interopRequireDefault(require("path"));
10
10
  var _modules = require("./modules.help");
11
11
  var _tools = require("./tools.help");
12
12
  var _shortcuts = require("./shortcuts");
13
+ var _upload = require("./utils/upload");
13
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
15
  const helpFileContent = _fs.default.readFileSync(_path.default.join(__dirname, 'help.json'), 'utf8');
15
16
  if (!helpFileContent) {
@@ -370,24 +371,13 @@ async function main() {
370
371
  // 尝试解析快捷命令
371
372
  const shortcut = (0, _shortcuts.resolveShortcut)(params.module, params.tool);
372
373
  if (shortcut) {
374
+ params.module = shortcut.module;
373
375
  params.tool = shortcut.tool;
374
376
 
375
- // 自动注入 path 参数(兼容新旧格式)
376
- // 新格式:用户可能直接传了 --repo xxx,检查扁平参数是否已存在
377
- const autoPathKeys = Object.keys(shortcut.autoPath);
378
- const hasPathAlready = params.path || autoPathKeys.some(k => params[k]);
379
- if (!hasPathAlready) {
380
- if (autoPathKeys.length > 0) {
381
- // 新格式:直接注入为扁平参数
382
- for (const [key, value] of Object.entries(shortcut.autoPath)) {
383
- if (!params[key]) {
384
- params[key] = value;
385
- }
386
- }
387
- } else {
388
- const envHint = params.module === 'issues' ? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID' : 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
389
- console.error(`快捷命令需要环境变量 ${envHint},或手动传参数。\n` + `提示:运行 ${"cnb" || 'cnb'} --short 查看快捷命令详情。`);
390
- process.exit(1);
377
+ // 自动注入 path 参数
378
+ for (const [key, value] of Object.entries(shortcut.autoPath)) {
379
+ if (!params[key]) {
380
+ params[key] = value;
391
381
  }
392
382
  }
393
383
 
@@ -452,10 +442,17 @@ async function main() {
452
442
  if (pathAndQueryParams) {
453
443
  toolsParam.push(pathAndQueryParams);
454
444
  }
455
- if (formattedParams.data) {
456
- toolsParam.push(formattedParams.data);
445
+
446
+ // 上传快捷命令:走完整上传流程(获取 URL → PUT 文件 → 返回结果)
447
+ let data;
448
+ if (shortcut?.upload) {
449
+ data = await (0, _upload.handleUpload)(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams);
450
+ } else {
451
+ if (formattedParams.data) {
452
+ toolsParam.push(formattedParams.data);
453
+ }
454
+ data = await toolFunction(...toolsParam);
457
455
  }
458
- const data = await toolFunction(...toolsParam);
459
456
  const toolKey = `${formattedParams.module}/${formattedParams.tool}`;
460
457
  // 快捷命令默认输出摘要,--verbose 时输出全部信息
461
458
  const isVerbose = !!formattedParams.verbose;
@@ -29,11 +29,18 @@ function showModuleHelp(helpData, moduleName) {
29
29
  process.exit(1);
30
30
  }
31
31
  const cliCmd = "cnb" || 'cnb';
32
+ const entries = Object.values(moduleHelpData).map(info => ({
33
+ name: info.filename,
34
+ summary: trimSummary(info.summary || '')
35
+ }));
36
+ const maxNameLen = Math.max(...entries.map(e => e.name.length));
37
+ const pad = maxNameLen + 2;
32
38
  let toolListMsg = '';
33
- for (const [, info] of Object.entries(moduleHelpData)) {
34
- const name = info.filename;
35
- const summary = info.summary;
36
- toolListMsg += ` ${name.padEnd(30)} ${summary}\n`;
39
+ for (const {
40
+ name,
41
+ summary
42
+ } of entries) {
43
+ toolListMsg += ` ${name.padEnd(pad)} ${summary}\n`;
37
44
  }
38
45
  const helpMsg = `
39
46
  ${moduleName} 模块
@@ -88,6 +88,18 @@ const ISSUE_SHORTCUTS = [{
88
88
  realTool: 'post-issue-assignees',
89
89
  description: '添加处理人',
90
90
  dataTip: "--data '{\"assignees\":[\"username\"]}'"
91
+ }, {
92
+ shortName: 'upload-file',
93
+ realTool: 'post-issue-file-asset-upload-url',
94
+ description: '上传文件',
95
+ upload: true,
96
+ dataTip: "--data '{\"file\":\"文件路径\"}'"
97
+ }, {
98
+ shortName: 'upload-image',
99
+ realTool: 'post-issue-image-asset-upload-url',
100
+ description: '上传图片',
101
+ upload: true,
102
+ dataTip: "--data '{\"file\":\"图片路径\"}'"
91
103
  }];
92
104
  const PR_SHORTCUTS = [{
93
105
  shortName: 'get',
@@ -131,16 +143,50 @@ const PR_SHORTCUTS = [{
131
143
  shortName: 'list-assignees',
132
144
  realTool: 'list-pull-assignees',
133
145
  description: '查看处理人'
146
+ }, {
147
+ shortName: 'upload-file',
148
+ realTool: 'upload-files',
149
+ description: '上传文件',
150
+ repoOnly: true,
151
+ upload: true,
152
+ dataTip: "--data '{\"file\":\"文件路径\"}'"
153
+ }, {
154
+ shortName: 'upload-image',
155
+ realTool: 'upload-imgs',
156
+ description: '上传图片',
157
+ repoOnly: true,
158
+ upload: true,
159
+ dataTip: "--data '{\"file\":\"图片路径\"}'"
134
160
  }];
135
161
 
136
162
  // ============================================================
137
163
  // --short 帮助输出
138
164
  // ============================================================
139
165
 
166
+ /** 计算字符串的显示宽度(中文/全角占2,英文/半角占1) */
167
+ function displayWidth(str) {
168
+ let width = 0;
169
+ for (const ch of str) {
170
+ const code = ch.codePointAt(0) || 0;
171
+ // CJK、全角字符占2宽度
172
+ width += code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a || code >= 0x2e80 && code <= 0x3247 || code >= 0x3250 && code <= 0x4dbf || code >= 0x4e00 && code <= 0xa4c6 || code >= 0xa960 && code <= 0xa97c || code >= 0xac00 && code <= 0xd7a3 || code >= 0xf900 && code <= 0xfaff || code >= 0xfe10 && code <= 0xfe19 || code >= 0xfe30 && code <= 0xfe6b || code >= 0xff01 && code <= 0xff60 || code >= 0xffe0 && code <= 0xffe6 || code >= 0x1f000 && code <= 0x1fbff || code >= 0x20000 && code <= 0x2fffd || code >= 0x30000 && code <= 0x3fffd) ? 2 : 1;
173
+ }
174
+ return width;
175
+ }
176
+
177
+ /** 用全角空格将字符串补齐到指定显示宽度 */
178
+ function padToWidth(str, targetWidth) {
179
+ const diff = targetWidth - displayWidth(str);
180
+ if (diff <= 0) return str;
181
+ // 每个全角空格占2宽度
182
+ const fullSpaces = Math.floor(diff / 2);
183
+ const halfSpace = diff % 2 === 1 ? ' ' : '';
184
+ return str + ' '.repeat(fullSpaces) + halfSpace;
185
+ }
140
186
  function formatShortcutList(shortcuts, cliCmd, moduleName) {
141
- const maxDescLen = Math.max(...shortcuts.map(s => s.description.length));
187
+ const maxWidth = Math.max(...shortcuts.map(s => displayWidth(s.description)));
142
188
  return shortcuts.map(s => {
143
- const desc = s.description.padEnd(maxDescLen + 2, ' '); // 全角空格对齐
189
+ const desc = padToWidth(s.description, maxWidth + 2);
144
190
  const cmd = `${cliCmd} ${moduleName} ${s.shortName}`;
145
191
  const dataPart = s.dataTip ? ` ${s.dataTip}` : '';
146
192
  return ` ${desc} ${cmd}${dataPart}`;
@@ -150,50 +196,43 @@ function showShort() {
150
196
  const ctx = detectEventContext();
151
197
  const cliCmd = "cnb" || 'cnb';
152
198
  const repo = getRepo();
153
- if (ctx === 'issue') {
154
- const number = getIssueNumber();
155
- const list = formatShortcutList(ISSUE_SHORTCUTS, cliCmd, 'issues');
156
- console.log(`
157
- 📋 当前场景: Issue 事件 (Issue #${number})
158
- 仓库: ${repo}
159
-
160
- 常用快捷命令(path 参数已从环境变量自动获取):
161
- ${list}
162
-
163
- 提示: 以上命令自动使用环境变量 CNB_REPO_SLUG 和 CNB_ISSUE_IID,无需手动传 --path
164
- 默认只输出摘要信息,添加 --verbose 可输出全部信息
165
- `);
166
- } else if (ctx === 'pull_request') {
167
- const number = getPRNumber();
168
- const list = formatShortcutList(PR_SHORTCUTS, cliCmd, 'pulls');
199
+ if (ctx === 'issue' || ctx === 'pull_request') {
200
+ const isIssue = ctx === 'issue';
201
+ const number = isIssue ? getIssueNumber() : getPRNumber();
202
+ const emoji = isIssue ? '📋' : '🔀';
203
+ const label = isIssue ? 'Issue' : 'Pull Request';
204
+ const tag = isIssue ? 'Issue' : 'PR';
205
+ const moduleName = isIssue ? 'issues' : 'pulls';
206
+ const shortcuts = isIssue ? ISSUE_SHORTCUTS : PR_SHORTCUTS;
207
+ const envVars = isIssue ? 'CNB_REPO_SLUG 和 CNB_ISSUE_IID' : 'CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID';
208
+ const list = formatShortcutList(shortcuts, cliCmd, moduleName);
169
209
  console.log(`
170
- 🔀 当前场景: Pull Request 事件 (PR #${number})
210
+ ${emoji} 当前场景: ${label} 事件 (${tag} #${number})
171
211
  仓库: ${repo}
172
212
 
173
213
  常用快捷命令(path 参数已从环境变量自动获取):
174
214
  ${list}
175
215
 
176
- 提示: 以上命令自动使用环境变量 CNB_REPO_SLUG 和 CNB_PULL_REQUEST_IID,无需手动传 --path
216
+ 提示: ${tag} 相关命令会自动使用 ${envVars}
177
217
  默认只输出摘要信息,添加 --verbose 可输出全部信息
178
218
  `);
179
219
  } else {
180
- // 未知上下文,两组都显示
181
220
  const issueList = formatShortcutList(ISSUE_SHORTCUTS, cliCmd, 'issues');
182
221
  const prList = formatShortcutList(PR_SHORTCUTS, cliCmd, 'pulls');
183
222
  console.log(`
184
223
  ⚠️ 未检测到 Issue/PR 事件上下文
185
224
  (CNB_ISSUE_IID 和 CNB_PULL_REQUEST_IID 均未设置)
186
225
 
187
- 📋 Issue 场景常用命令(需要设置 CNB_ISSUE_IID 环境变量):
226
+ 📋 Issue 场景常用命令:
188
227
  ${issueList}
189
228
 
190
- 🔀 PR 场景常用命令(需要设置 CNB_PULL_REQUEST_IID 环境变量):
229
+ 🔀 PR 场景常用命令:
191
230
  ${prList}
192
231
 
193
- 提示: 快捷命令需要以下环境变量:
232
+ 提示: 快捷命令可使用以下环境变量:
194
233
  CNB_REPO_SLUG - 仓库路径
195
- CNB_ISSUE_IID - Issue 编号 (Issue 事件)
196
- CNB_PULL_REQUEST_IID - PR 编号 (PR 事件)
234
+ CNB_ISSUE_IID - Issue 编号(Issue 相关快捷命令)
235
+ CNB_PULL_REQUEST_IID - PR 编号(PR 相关快捷命令)
197
236
 
198
237
  默认只输出摘要信息,添加 --verbose 可输出全部信息
199
238
  `);
@@ -204,50 +243,35 @@ ${prList}
204
243
  // 快捷命令解析
205
244
  // ============================================================
206
245
 
246
+ function buildAutoPath(moduleName, repoOnly) {
247
+ const repo = process.env.CNB_REPO_SLUG || '';
248
+ if (repoOnly) return {
249
+ repo
250
+ };
251
+ const number = moduleName === 'issues' ? process.env.CNB_ISSUE_IID || '' : process.env.CNB_PULL_REQUEST_IID || '';
252
+ return {
253
+ repo,
254
+ number
255
+ };
256
+ }
257
+
207
258
  /**
208
259
  * 尝试将用户输入的 module + tool 解析为快捷命令
209
260
  *
210
- * 即使环境变量未设置,也会完成 shortName → realTool 的映射,
211
- * 只是 autoPath 为 null,调用方需要用户手动传 --path。
212
- *
213
261
  * @returns 解析结果,如果不是快捷命令则返回 null
214
262
  */
215
263
  function resolveShortcut(moduleName, toolName) {
216
264
  if (!moduleName || !toolName) return null;
217
-
218
- // 根据 module 名确定对应的快捷命令表
219
- let shortcuts = null;
220
- let autoPath = null;
221
- if (moduleName === 'issues') {
222
- shortcuts = ISSUE_SHORTCUTS;
223
- const repo = process.env.CNB_REPO_SLUG;
224
- const number = process.env.CNB_ISSUE_IID;
225
- if (repo && number) {
226
- autoPath = {
227
- repo,
228
- number
229
- };
230
- }
231
- } else if (moduleName === 'pulls') {
232
- shortcuts = PR_SHORTCUTS;
233
- const repo = process.env.CNB_REPO_SLUG;
234
- const number = process.env.CNB_PULL_REQUEST_IID;
235
- if (repo && number) {
236
- autoPath = {
237
- repo,
238
- number
239
- };
240
- }
241
- }
265
+ const shortcuts = moduleName === 'issues' ? ISSUE_SHORTCUTS : moduleName === 'pulls' ? PR_SHORTCUTS : null;
242
266
  if (!shortcuts) return null;
243
-
244
- // 在快捷命令表中查找匹配的 shortName
245
267
  const matched = shortcuts.find(s => s.shortName === toolName);
246
268
  if (!matched) return null;
247
269
  return {
270
+ module: moduleName,
248
271
  tool: matched.realTool,
249
- autoPath: autoPath || {},
250
- autoData: matched.autoData || null
272
+ autoPath: buildAutoPath(moduleName, !!matched.repoOnly),
273
+ autoData: matched.autoData || null,
274
+ upload: !!matched.upload
251
275
  };
252
276
  }
253
277
 
@@ -263,41 +287,151 @@ function resolveShortcut(moduleName, toolName) {
263
287
  * - 数组响应:对 data 中每个元素应用提取函数
264
288
  */
265
289
  const SUMMARY_EXTRACTORS = {
266
- // Issue 详情摘要:只保留标题和正文
290
+ // Issue 详情摘要:保留标题、状态、标签和正文
291
+ // 过滤: number, state_reason, assignees, author, created_at, updated_at 等
267
292
  'issues/get-issue': item => ({
268
293
  title: item.title,
294
+ state: item.state,
295
+ labels: item.labels?.map?.(l => l.name).filter(Boolean) || [],
269
296
  body: item.body
270
297
  }),
271
298
  // Issue 更新摘要:只保留状态变更结果
299
+ // 过滤: title, body, labels, assignees, author, created_at, updated_at 等
272
300
  'issues/update-issue': item => ({
273
301
  number: item.number,
274
302
  state: item.state,
275
303
  state_reason: item.state_reason
276
304
  }),
277
- // Issue 添加处理人摘要:只保留处理人列表
305
+ // Issue 评论列表摘要:只保留作者用户名、内容和创建时间
306
+ // 过滤: author 完整对象(avatar/email/freeze/is_npc/nickname), reactions, updated_at
307
+ 'issues/list-issue-comments': item => ({
308
+ id: item.id,
309
+ author: item.author?.username,
310
+ body: item.body,
311
+ created_at: item.created_at
312
+ }),
313
+ // Issue 发表评论摘要:只保留评论 ID 和创建时间(body 已在请求中传入,无需重复)
314
+ // 过滤: body(请求已传入), author 完整对象(avatar/email/freeze/is_npc/nickname/username), reactions, updated_at
315
+ 'issues/post-issue-comment': item => ({
316
+ id: item.id,
317
+ created_at: item.created_at
318
+ }),
319
+ // Issue 标签列表摘要:保留名称、颜色和描述
320
+ // 过滤: id
321
+ 'issues/list-issue-labels': item => ({
322
+ name: item.name,
323
+ color: item.color,
324
+ description: item.description
325
+ }),
326
+ // Issue 添加标签摘要:只保留 ID 和颜色(name 已在请求中传入,无需重复)
327
+ // 过滤: name(请求已传入), description
328
+ 'issues/post-issue-labels': item => ({
329
+ id: item.id,
330
+ color: item.color
331
+ }),
332
+ // Issue 处理人列表摘要:保留用户名和昵称
333
+ // 过滤: avatar, email, freeze, is_npc
334
+ 'issues/list-issue-assignees': item => ({
335
+ username: item.username,
336
+ nickname: item.nickname
337
+ }),
338
+ // Issue 添加处理人摘要:保留处理人用户名和昵称列表
339
+ // 过滤: assignees 中每个用户的 avatar/email/freeze/is_npc,以及 issue 其他字段
278
340
  'issues/post-issue-assignees': item => ({
279
341
  number: item.number,
280
- assignees: item.assignees?.map?.(a => a.username).filter(Boolean) || []
342
+ assignees: item.assignees?.map?.(a => ({
343
+ username: a.username,
344
+ nickname: a.nickname
345
+ })).filter(a => a.username) || []
281
346
  }),
282
- // PR 详情摘要:只保留标题、状态、正文和分支信息
347
+ // PR 详情摘要:保留标题、状态、标签、正文和分支信息
348
+ // 过滤: number, author, assignees, reviewers, created_at, updated_at, base/head 完整对象等
283
349
  'pulls/get-pull': item => ({
284
350
  title: item.title,
285
351
  state: item.state,
352
+ labels: item.labels?.map?.(l => l.name).filter(Boolean) || [],
286
353
  base: item.base?.ref,
287
354
  head: item.head?.ref,
288
355
  body: item.body
289
356
  }),
290
- // PR 文件列表摘要:只保留文件名、变更状态和增删行数
357
+ // PR 文件列表摘要:保留文件名、sha、变更状态和增删行数
358
+ // 过滤: patch, blob_url, raw_url, contents_url, previous_filename
291
359
  'pulls/list-pull-files': item => ({
292
360
  filename: item.filename,
361
+ sha: item.sha,
293
362
  status: item.status,
294
363
  additions: item.additions,
295
364
  deletions: item.deletions
296
365
  }),
297
366
  // PR 提交列表摘要:只保留 sha 前 8 位和 commit message
367
+ // 过滤: sha 完整值, commit 完整对象(author/committer/tree/verification), parents, url 等
298
368
  'pulls/list-pull-commits': item => ({
299
369
  sha: typeof item.sha === 'string' ? item.sha.substring(0, 8) : item.sha,
300
370
  message: item.commit?.message
371
+ }),
372
+ // PR 评论列表摘要:保留作者用户名和昵称、内容和创建时间
373
+ // 过滤: author 完整对象(avatar/email/freeze/is_npc), reactions, updated_at
374
+ 'pulls/list-pull-comments': item => ({
375
+ id: item.id,
376
+ author: item.author?.username,
377
+ nickname: item.author?.nickname,
378
+ body: item.body,
379
+ created_at: item.created_at
380
+ }),
381
+ // PR 发表评论摘要:只保留评论 ID 和创建时间(body 已在请求中传入,无需重复)
382
+ // 过滤: body(请求已传入), author 完整对象(avatar/email/freeze/is_npc/nickname/username), reactions, updated_at
383
+ 'pulls/post-pull-comment': item => ({
384
+ id: item.id,
385
+ created_at: item.created_at
386
+ }),
387
+ // PR 标签列表摘要:保留 ID、名称、颜色和描述
388
+ // 过滤: 无
389
+ 'pulls/list-pull-labels': item => ({
390
+ id: item.id,
391
+ name: item.name,
392
+ color: item.color,
393
+ description: item.description
394
+ }),
395
+ // PR 添加标签摘要:只保留 ID 和颜色(name 已在请求中传入,无需重复)
396
+ // 过滤: name(请求已传入), description
397
+ 'pulls/post-pull-labels': item => ({
398
+ id: item.id,
399
+ color: item.color
400
+ }),
401
+ // PR CI 状态摘要:保留整体状态和各检查项的核心信息(sha 截断为前 8 位)
402
+ // 过滤: sha 完整值, statuses 中每项的 target_url/created_at/updated_at
403
+ 'pulls/list-pull-commit-statuses': item => ({
404
+ sha: typeof item.sha === 'string' ? item.sha.substring(0, 8) : item.sha,
405
+ state: item.state,
406
+ statuses: item.statuses?.map?.(s => ({
407
+ context: s.context,
408
+ state: s.state,
409
+ description: s.description
410
+ })) || []
411
+ }),
412
+ // PR 评审列表摘要:保留作者用户名和昵称、状态和内容
413
+ // 过滤: author 完整对象(avatar/email/freeze/is_npc), created_at, updated_at
414
+ 'pulls/list-pull-reviews': item => ({
415
+ id: item.id,
416
+ author: item.author?.username,
417
+ nickname: item.author?.nickname,
418
+ state: item.state,
419
+ body: item.body
420
+ }),
421
+ // PR 处理人列表摘要:保留用户名和昵称
422
+ // 过滤: avatar, email, freeze, is_npc
423
+ 'pulls/list-pull-assignees': item => ({
424
+ username: item.username,
425
+ nickname: item.nickname
426
+ }),
427
+ // PR 添加处理人摘要:保留处理人用户名和昵称列表
428
+ // 过滤: assignees 中每个用户的 avatar/email/freeze/is_npc,以及 PR 其他字段
429
+ 'pulls/post-pull-assignees': item => ({
430
+ number: item.number,
431
+ assignees: item.assignees?.map?.(a => ({
432
+ username: a.username,
433
+ nickname: a.nickname
434
+ })).filter(a => a.username) || []
301
435
  })
302
436
  };
303
437
 
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.handleUpload = handleUpload;
7
+ var _fs = _interopRequireDefault(require("fs"));
8
+ var _path = _interopRequireDefault(require("path"));
9
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ /**
11
+ * 完整上传流程
12
+ * 1. 读取本地文件 → 获取 name/size
13
+ * 2. 调用上传 API → 获取 upload_url
14
+ * 3. PUT 文件内容到 upload_url(流式上传)
15
+ * 4. 返回最终结果
16
+ */
17
+
18
+ /** 常见文件扩展名 → MIME 类型映射(仅用于上传场景,不引入外部依赖) */
19
+ const MIME_MAP = {
20
+ '.txt': 'text/plain',
21
+ '.html': 'text/html',
22
+ '.htm': 'text/html',
23
+ '.css': 'text/css',
24
+ '.csv': 'text/csv',
25
+ '.js': 'application/javascript',
26
+ '.mjs': 'application/javascript',
27
+ '.json': 'application/json',
28
+ '.xml': 'application/xml',
29
+ '.pdf': 'application/pdf',
30
+ '.zip': 'application/zip',
31
+ '.gz': 'application/gzip',
32
+ '.gzip': 'application/gzip',
33
+ '.tar': 'application/x-tar',
34
+ '.7z': 'application/x-7z-compressed',
35
+ '.rar': 'application/vnd.rar',
36
+ '.doc': 'application/msword',
37
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
38
+ '.xls': 'application/vnd.ms-excel',
39
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
+ '.ppt': 'application/vnd.ms-powerpoint',
41
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
42
+ '.png': 'image/png',
43
+ '.jpg': 'image/jpeg',
44
+ '.jpeg': 'image/jpeg',
45
+ '.gif': 'image/gif',
46
+ '.svg': 'image/svg+xml',
47
+ '.webp': 'image/webp',
48
+ '.ico': 'image/x-icon',
49
+ '.bmp': 'image/bmp',
50
+ '.mp3': 'audio/mpeg',
51
+ '.wav': 'audio/wav',
52
+ '.ogg': 'audio/ogg',
53
+ '.mp4': 'video/mp4',
54
+ '.webm': 'video/webm',
55
+ '.avi': 'video/x-msvideo',
56
+ '.mov': 'video/quicktime',
57
+ '.woff': 'font/woff',
58
+ '.woff2': 'font/woff2',
59
+ '.ttf': 'font/ttf',
60
+ '.otf': 'font/otf',
61
+ '.md': 'text/markdown',
62
+ '.yaml': 'text/yaml',
63
+ '.yml': 'text/yaml',
64
+ '.sh': 'application/x-sh',
65
+ '.py': 'text/x-python',
66
+ '.rb': 'text/x-ruby',
67
+ '.go': 'text/x-go',
68
+ '.rs': 'text/x-rust',
69
+ '.ts': 'text/typescript',
70
+ '.tsx': 'text/typescript',
71
+ '.jsx': 'text/jsx',
72
+ '.java': 'text/x-java-source',
73
+ '.c': 'text/x-c',
74
+ '.h': 'text/x-c',
75
+ '.cpp': 'text/x-c++src',
76
+ '.hpp': 'text/x-c++hdr',
77
+ '.log': 'text/plain',
78
+ '.patch': 'text/x-diff',
79
+ '.diff': 'text/x-diff'
80
+ };
81
+ function mimeLookup(filePath) {
82
+ const ext = _path.default.extname(filePath).toLowerCase();
83
+ return MIME_MAP[ext] || 'application/octet-stream';
84
+ }
85
+
86
+ // 上传文件大小上限:100MB
87
+ const MAX_FILE_SIZE = 100 * 1024 * 1024;
88
+
89
+ /** 上传 API 的 path 参数 */
90
+
91
+ /** Issue 上传请求体 */
92
+
93
+ /** Pull 上传请求体 */
94
+
95
+ /** 上传 API 函数签名 */
96
+
97
+ /** 上传 API 响应 */
98
+
99
+ /** 上传失败时的 data */
100
+
101
+ /** Issue 上传成功时的 data */
102
+
103
+ /** Pull 上传成功时的 data */
104
+
105
+ /** handleUpload 的返回值 */
106
+
107
+ /**
108
+ * 处理完整上传流程
109
+ * @param shortcut 解析后的快捷命令
110
+ * @param filePath 本地文件路径
111
+ * @param toolFunction 原始上传 API 函数(获取 upload_url)
112
+ * @param pathParams API 调用的 path 参数(repo 或 {repo, number})
113
+ */
114
+ async function handleUpload(shortcut, filePath, toolFunction, pathParams) {
115
+ // 校验 file 参数
116
+ if (!filePath) {
117
+ return {
118
+ status: 400,
119
+ data: {
120
+ error: `上传命令需要指定文件路径,如: --data '{"file":"./path/to/file"}'`
121
+ }
122
+ };
123
+ }
124
+
125
+ // 1. 读取本地文件信息
126
+ if (!_fs.default.existsSync(filePath)) {
127
+ return {
128
+ status: 400,
129
+ data: {
130
+ error: `文件不存在: ${filePath}`
131
+ }
132
+ };
133
+ }
134
+ const stat = _fs.default.statSync(filePath);
135
+ if (!stat.isFile()) {
136
+ return {
137
+ status: 400,
138
+ data: {
139
+ error: `不是文件: ${filePath}`
140
+ }
141
+ };
142
+ }
143
+ const fileName = _path.default.basename(filePath);
144
+ const fileSize = stat.size;
145
+ if (fileSize === 0) {
146
+ return {
147
+ status: 400,
148
+ data: {
149
+ error: '文件为空'
150
+ }
151
+ };
152
+ }
153
+ if (fileSize > MAX_FILE_SIZE) {
154
+ return {
155
+ status: 400,
156
+ data: {
157
+ error: `文件过大 (${(fileSize / 1024 / 1024).toFixed(1)}MB),上限 ${MAX_FILE_SIZE / 1024 / 1024}MB`
158
+ }
159
+ };
160
+ }
161
+ const contentType = mimeLookup(filePath);
162
+
163
+ // 2. 调用上传 API 获取 upload_url
164
+ const isIssueUpload = shortcut.module === 'issues';
165
+ const requestBody = isIssueUpload ? {
166
+ name: fileName,
167
+ size: fileSize,
168
+ content_type: contentType
169
+ } : {
170
+ name: fileName,
171
+ size: fileSize
172
+ };
173
+ const uploadResponse = await toolFunction(pathParams, requestBody);
174
+
175
+ // 提取 upload_url
176
+ const responseData = uploadResponse?.data;
177
+ const uploadUrl = responseData?.upload_url;
178
+ if (!uploadUrl) {
179
+ return uploadResponse; // 获取 URL 失败,直接返回原始错误
180
+ }
181
+
182
+ // 3. PUT 文件内容到 upload_url(流式读取,避免大文件撑爆内存)
183
+ const fileStream = _fs.default.createReadStream(filePath);
184
+ const putHeaders = {
185
+ 'Content-Type': contentType,
186
+ 'Content-Length': String(fileSize)
187
+ };
188
+ let putResponse;
189
+ try {
190
+ putResponse = await fetch(uploadUrl, {
191
+ method: 'PUT',
192
+ headers: putHeaders,
193
+ body: fileStream,
194
+ // @ts-ignore duplex required for streaming body in Node.js fetch
195
+ duplex: 'half'
196
+ });
197
+ } catch (e) {
198
+ fileStream.destroy();
199
+ throw e;
200
+ }
201
+ if (!putResponse.ok) {
202
+ fileStream.destroy();
203
+ const errText = await putResponse.text().catch(() => '');
204
+ return {
205
+ status: putResponse.status,
206
+ data: {
207
+ error: `文件上传失败: ${putResponse.statusText}`,
208
+ detail: errText
209
+ }
210
+ };
211
+ }
212
+
213
+ // 4. 返回最终结果
214
+ if (isIssueUpload) {
215
+ return {
216
+ status: 200,
217
+ data: {
218
+ asset_link: responseData.asset_link,
219
+ name: fileName,
220
+ size: fileSize
221
+ }
222
+ };
223
+ }
224
+ return {
225
+ status: 200,
226
+ data: {
227
+ name: responseData.assets?.name || fileName,
228
+ path: responseData.assets?.path,
229
+ size: fileSize,
230
+ token: responseData.token
231
+ }
232
+ };
233
+ }