@cnbcool/cnb-api-generate 2.5.1 → 2.6.0

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.
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateShortcutsConfig = generateShortcutsConfig;
7
+ const debug_1 = __importDefault(require("debug"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const get_skills_codegen_target_1 = require("../utils/get-skills-codegen-target");
11
+ const logger = (0, debug_1.default)('csg:shortcuts-config');
12
+ /**
13
+ * 把解析后的快捷命令配置写入 client 运行时可读取的 JSON。
14
+ * 文件路径为 `<skillScriptDir>/core/shortcuts.config.json`,
15
+ * 与编译后的 `core/shortcuts.js` 同级,运行时通过相对路径读取。
16
+ *
17
+ * 写入的字段就是 ResolvedQuickCommand 透传后的内容(包含 module 冗余字段,
18
+ * 以及配置里给的 shortName / realTool / description / repoOnly / upload /
19
+ * custom / autoData / dataTip);下游 client/shortcuts.ts 只用其中的
20
+ * 运行时字段,文档相关字段被忽略也不影响。
21
+ */
22
+ async function generateShortcutsConfig(byModule) {
23
+ const skillScriptDir = (0, get_skills_codegen_target_1.getSkillScriptCodegenTarget)();
24
+ const skillScriptCoreDir = path_1.default.join(skillScriptDir, 'core');
25
+ if (!fs_1.default.existsSync(skillScriptCoreDir)) {
26
+ fs_1.default.mkdirSync(skillScriptCoreDir, { recursive: true });
27
+ }
28
+ const targetFile = path_1.default.join(skillScriptCoreDir, 'shortcuts.config.json');
29
+ fs_1.default.writeFileSync(targetFile, JSON.stringify(byModule, null, 2));
30
+ const total = Object.values(byModule).reduce((n, arr) => n + arr.length, 0);
31
+ logger(`wrote ${targetFile} (${Object.keys(byModule).length} modules, ${total} commands)`);
32
+ }
@@ -10,8 +10,14 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const get_skills_codegen_target_1 = require("../utils/get-skills-codegen-target");
11
11
  const get_config_1 = require("../utils/get-config");
12
12
  const skills_1 = require("../constants/skills");
13
+ const render_quick_commands_md_1 = require("./render-quick-commands-md");
13
14
  const logger = (0, debug_1.default)('csg:skills');
14
- async function generateSkillMd(helpData, requestMap) {
15
+ async function generateSkillMd(
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
+ _helpData,
18
+ // requestMap 由调用方传入但当前未使用,保留以维持稳定签名
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ _requestMap, quickCommandsByModule) {
15
21
  const skillMDTempateDir = path_1.default.join(__dirname, '../../skills-template');
16
22
  const skillMDDir = path_1.default.join((0, get_skills_codegen_target_1.getSkillCodegenTarget)());
17
23
  // 复制skills.md
@@ -19,7 +25,16 @@ async function generateSkillMd(helpData, requestMap) {
19
25
  const targetSkillMD = path_1.default.join(skillMDDir, 'SKILL.md');
20
26
  const skillMDContent = fs_1.default.readFileSync(SKILL_MD, 'utf8');
21
27
  const config = (0, get_config_1.getConfig)();
22
- const newSkillMDContent = skillMDContent.replaceAll('<$CNB_CLI_CMD$>', config.skillsDev ? skills_1.CNB_CLI_DEV_CMD : skills_1.CNB_CLI_PROD_CMD);
28
+ const cliCmd = config.skillsDev ? skills_1.CNB_CLI_DEV_CMD : skills_1.CNB_CLI_PROD_CMD;
29
+ // 仅替换两类占位符:
30
+ // - <$QUICK_COMMANDS$>:配置驱动的快捷命令清单(标题/注意事项写在模板里即可)
31
+ // - <$CNB_CLI_CMD$> :CLI 命令前缀
32
+ const quickCommandsMd = (0, render_quick_commands_md_1.renderQuickCommandsMarkdown)(quickCommandsByModule, cliCmd);
33
+ const total = Object.values(quickCommandsByModule).reduce((n, arr) => n + arr.length, 0);
34
+ logger(`quick commands rendered: ${Object.keys(quickCommandsByModule).length} modules, ${total} commands`);
35
+ const newSkillMDContent = skillMDContent
36
+ .replaceAll('<$QUICK_COMMANDS$>', quickCommandsMd)
37
+ .replaceAll('<$CNB_CLI_CMD$>', cliCmd);
23
38
  fs_1.default.writeFileSync(targetSkillMD, newSkillMDContent);
24
39
  logger(`generate ${SKILL_MD} to ${targetSkillMD}`);
25
40
  }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderQuickCommandsMarkdown = renderQuickCommandsMarkdown;
4
+ /**
5
+ * 把解析后的快捷命令渲染为 markdown 文本,对应模板里的 <$QUICK_COMMANDS$> 占位符。
6
+ *
7
+ * 仅渲染命令列表本身——"## 快捷命令" 标题、"## 注意事项" 等静态文案直接
8
+ * 写在 skills-template/SKILL.md 模板里,没必要再加占位符。
9
+ *
10
+ * 渲染规则:
11
+ * - 每个模块先输出 `<moduleName>:` 标题行
12
+ * - 每条命令渲染为 ``- `<cliCmd> <module> <shortName>[ <dataTip>]` — <description>`` 的列表项
13
+ * - 没有任何模块/命令时返回空字符串(让模板里的占位符消失)
14
+ */
15
+ function renderQuickCommandsMarkdown(byModule, cliCmd) {
16
+ const moduleNames = Object.keys(byModule);
17
+ if (!moduleNames.length)
18
+ return '';
19
+ const lines = [];
20
+ for (const moduleName of moduleNames) {
21
+ const items = byModule[moduleName];
22
+ if (!items.length)
23
+ continue;
24
+ lines.push(`${moduleName}:`);
25
+ for (const cmd of items) {
26
+ const dataPart = cmd.dataTip ? ` ${cmd.dataTip}` : '';
27
+ const cmdLine = `${cliCmd} ${moduleName} ${cmd.shortName}${dataPart}`;
28
+ lines.push(`- \`${cmdLine}\` — ${cmd.description}`);
29
+ }
30
+ lines.push('');
31
+ }
32
+ // 去掉末尾多余空行
33
+ while (lines.length && lines[lines.length - 1] === '') {
34
+ lines.pop();
35
+ }
36
+ return lines.join('\n');
37
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveQuickCommands = resolveQuickCommands;
7
+ const debug_1 = __importDefault(require("debug"));
8
+ const get_config_1 = require("../utils/get-config");
9
+ const logger = (0, debug_1.default)('csg:quick-commands');
10
+ /**
11
+ * 把 cag.config.js 的 quickCommands 配置 + helpData 元数据,
12
+ * 解析为下游(client 产物 / SKILL.md 渲染)共用的结构化数据。
13
+ *
14
+ * 关键校验:
15
+ * - 非 custom 命令的 realTool 必须能在 helpData.modulesHelp 中命中,
16
+ * 否则立即抛错,防止 swagger 改名/下线后产物默默漂移。
17
+ * - 校验时按"任一 category 命中"匹配(realTool 在 swagger 里全局唯一即可,
18
+ * 不强制要求 module 名等于 swagger category)。
19
+ */
20
+ function resolveQuickCommands(helpData) {
21
+ const { quickCommands } = (0, get_config_1.getConfig)();
22
+ if (!quickCommands)
23
+ return {};
24
+ const { modulesHelp } = helpData;
25
+ // 建一个 realTool → true 的全局集合,便于 O(1) 校验
26
+ const allTools = new Set();
27
+ for (const category of Object.values(modulesHelp)) {
28
+ for (const filename of Object.keys(category)) {
29
+ allTools.add(filename);
30
+ }
31
+ }
32
+ const result = {};
33
+ for (const [moduleName, items] of Object.entries(quickCommands)) {
34
+ if (!Array.isArray(items)) {
35
+ throw new Error(`[quickCommands] 模块 "${moduleName}" 的值必须是数组,请检查 cag.config.js。`);
36
+ }
37
+ const resolved = [];
38
+ for (const item of items) {
39
+ if (!(item === null || item === void 0 ? void 0 : item.shortName) || !(item === null || item === void 0 ? void 0 : item.realTool) || !(item === null || item === void 0 ? void 0 : item.description)) {
40
+ throw new Error(`[quickCommands] 模块 "${moduleName}" 下存在缺失必填字段(shortName / realTool / description)的命令:` +
41
+ JSON.stringify(item));
42
+ }
43
+ // custom 命令跳过 swagger 校验
44
+ if (!item.custom && !allTools.has(item.realTool)) {
45
+ throw new Error(`[quickCommands] 命令 "${moduleName} ${item.shortName}" 的 realTool="${item.realTool}" ` +
46
+ `在 swagger 生成结果中不存在。请检查 cag.config.js 是否与最新 swagger 同步,` +
47
+ `或对自定义命令显式标记 custom: true。`);
48
+ }
49
+ resolved.push({ ...item, module: moduleName });
50
+ logger(`resolved: ${moduleName} ${item.shortName} -> ${item.realTool}`);
51
+ }
52
+ result[moduleName] = resolved;
53
+ }
54
+ return result;
55
+ }
package/built/skills.js CHANGED
@@ -13,6 +13,8 @@ const generate_skill_cli_scripts_1 = require("./skills-tools/generate-skill-cli-
13
13
  const generate_skill_md_1 = require("./skills-tools/generate-skill-md");
14
14
  const generate_config_1 = require("./skills-tools/generate-config");
15
15
  const printer_skills_client_loader_1 = require("./codegen/printer/printer-skills-client-loader");
16
+ const resolve_quick_commands_1 = require("./skills-tools/resolve-quick-commands");
17
+ const generate_shortcuts_config_1 = require("./skills-tools/generate-shortcuts-config");
16
18
  const logger = (0, debug_1.default)('csg:main');
17
19
  async function start() {
18
20
  process.env.CNB_GENERATE_MODE = 'skills';
@@ -31,8 +33,12 @@ async function start() {
31
33
  const helpData = (0, generate_skill_cli_help_1.generateSkillCliHelp)(requestMap, defintionsMap);
32
34
  // 编译core
33
35
  await (0, printer_skills_client_core_1.printerSkillsClientCore)(helpData);
34
- // 编译skills.md
35
- await (0, generate_skill_md_1.generateSkillMd)(helpData, requestMap);
36
+ // 解析快捷命令配置(cag.config.js → 校验 → 结构化),同时驱动 client 运行时数据与 SKILL.md 文档
37
+ const quickCommandsByModule = (0, resolve_quick_commands_1.resolveQuickCommands)(helpData);
38
+ // 写入 client 运行时配置(core/shortcuts.config.json),替代 client/shortcuts.ts 中的硬编码常量
39
+ await (0, generate_shortcuts_config_1.generateShortcutsConfig)(quickCommandsByModule);
40
+ // 编译skills.md(使用同一份解析结果渲染 <$QUICK_COMMANDS$>)
41
+ await (0, generate_skill_md_1.generateSkillMd)(helpData, requestMap, quickCommandsByModule);
36
42
  // 生成config文件
37
43
  await (0, generate_config_1.generateConfig)();
38
44
  }
package/client/core.ts CHANGED
@@ -135,6 +135,8 @@ async function clientFetch(data: any): Promise<any> {
135
135
  headers: {
136
136
  Authorization: `Bearer ${token}`,
137
137
  Accept: 'application/vnd.cnb.api+json',
138
+ // 当请求体存在时,附加 Content-Type,避免后端无法识别 JSON body
139
+ ...(data.data ? { 'Content-Type': 'application/vnd.cnb.api+json' } : {}),
138
140
  ...(data?.header || {}),
139
141
  },
140
142
  });
@@ -31,7 +31,10 @@ export async function fetchResponseHandler(fetchOriginParams: Record<string, any
31
31
  try {
32
32
  const converterExample = loadPlugin(converter);
33
33
  if (converterExample && typeof converterExample === 'function') {
34
- return handler(converterExample, fetchOriginParams, response.data);
34
+ // handler 返回的是处理后的 data,应回填到 response.data
35
+ // 然后返回完整的 response 对象(保留 status/trace/header/page 等包装字段)
36
+ const convertedData = await handler(converterExample, fetchOriginParams, response.data);
37
+ return { ...response, data: convertedData };
35
38
  }
36
39
  } catch (e) {
37
40
  console.error(`converter ${converter} not found`);
@@ -70,7 +70,10 @@ export function formatParams(
70
70
 
71
71
  for (const [key, value] of Object.entries(params)) {
72
72
  if (reservedKeys.has(key)) continue;
73
- // boolean 值需要特殊处理:嵌套对象的 boolean 子字段不能跳过
73
+ // boolean 值需要特殊处理:
74
+ // 1) 嵌套对象的 boolean 子字段应展开到 nestedObjectCollector
75
+ // 2) 顶层 body 中的 boolean 字段应直接写入 formatted.data
76
+ // 3) 其余未知 boolean 字段才跳过
74
77
  if (typeof value === 'boolean') {
75
78
  const nestedMapping = nestedFieldMap.get(key);
76
79
  if (nestedMapping && nestedMapping.leafSchema?.type === 'boolean') {
@@ -88,6 +91,23 @@ export function formatParams(
88
91
  target = target[subPathKeys[pi]];
89
92
  }
90
93
  target[subPathKeys[subPathKeys.length - 1]] = value;
94
+ continue;
95
+ }
96
+ // 顶层 body boolean 字段:直接写入 formatted.data
97
+ if (bodyProps[key] && bodyProps[key].type === 'boolean') {
98
+ if (!formatted.data) formatted.data = {};
99
+ formatted.data[key] = value;
100
+ continue;
101
+ }
102
+ // body 字段与 path/query 冲突时会被加 `d-` 前缀(详见 flatten-*-options.ts);
103
+ // `d_` 形式与同文件下方非 boolean 分支保持一致兼容
104
+ if (key.startsWith('d-') || key.startsWith('d_')) {
105
+ const originalKey = key.replace(/^d[-_]/, '');
106
+ if (bodyProps[originalKey] && bodyProps[originalKey].type === 'boolean') {
107
+ if (!formatted.data) formatted.data = {};
108
+ formatted.data[originalKey] = value;
109
+ continue;
110
+ }
91
111
  }
92
112
  continue;
93
113
  }
@@ -28,6 +28,8 @@ export interface ShortcutDefinition {
28
28
  autoData?: Record<string, any>;
29
29
  /** --data 的使用提示(展示用) */
30
30
  dataTip?: string;
31
+ /** 上传命令的资源种类(file/image),仅 upload 命令使用,默认 'file' */
32
+ kind?: 'file' | 'image';
31
33
  }
32
34
 
33
35
  export interface ResolvedShortcut {
@@ -41,6 +43,8 @@ export interface ResolvedShortcut {
41
43
  autoData: Record<string, any> | null;
42
44
  /** 是否为上传命令 */
43
45
  upload: boolean;
46
+ /** 上传命令的资源种类(file/image),仅 upload 时有意义 */
47
+ kind?: 'file' | 'image';
44
48
  }
45
49
 
46
50
  // ============================================================
@@ -69,36 +73,39 @@ function getPRNumber(): string {
69
73
  // 快捷命令定义
70
74
  // ============================================================
71
75
 
72
- export const ISSUE_SHORTCUTS: ShortcutDefinition[] = [
73
- { shortName: 'get', realTool: 'get-issue', description: '获取详情' },
74
- { shortName: 'list-comments', realTool: 'list-issue-comments', description: '获取评论列表' },
75
- { shortName: 'comment', realTool: 'post-issue-comment', description: '评论', dataTip: "--body 内容" },
76
- { shortName: 'close', realTool: 'update-issue', description: '关闭', autoData: { state: 'closed', state_reason: 'completed' } },
77
- { shortName: 'open', realTool: 'update-issue', description: '打开', autoData: { state: 'open', state_reason: 'reopened' } },
78
- { shortName: 'list-labels', realTool: 'list-issue-labels', description: '查看标签' },
79
- { shortName: 'add-labels', realTool: 'post-issue-labels', description: '添加标签', dataTip: "--labels bug --labels feature" },
80
- { shortName: 'list-assignees', realTool: 'list-issue-assignees', description: '查看处理人' },
81
- { shortName: 'add-assignees', realTool: 'post-issue-assignees', description: '添加处理人', dataTip: "--assignees username" },
82
- { shortName: 'upload-file', realTool: 'post-issue-file-asset-upload-url', description: '上传文件', upload: true, dataTip: "--file 文件路径" },
83
- { shortName: 'upload-image', realTool: 'post-issue-image-asset-upload-url', description: '上传图片', upload: true, dataTip: "--file 图片路径" },
84
- ];
85
-
86
- export const PR_SHORTCUTS: ShortcutDefinition[] = [
87
- { shortName: 'get', realTool: 'get-pull', description: '获取详情' },
88
- { shortName: 'list-files', realTool: 'list-pull-files', description: '获取文件变更' },
89
- { shortName: 'list-commits', realTool: 'list-pull-commits', description: '获取提交记录' },
90
- { shortName: 'list-comments', realTool: 'list-pull-comments', description: '获取评论列表' },
91
- { shortName: 'comment', realTool: 'post-pull-comment', description: '评论', dataTip: "--body 内容" },
92
- { shortName: 'list-labels', realTool: 'list-pull-labels', description: '查看标签' },
93
- { shortName: 'add-labels', realTool: 'post-pull-labels', description: '添加标签', dataTip: "--labels ready --labels approved" },
94
- { shortName: 'check-status', realTool: 'list-pull-commit-statuses', description: '查看 CI 状态' },
95
- { shortName: 'list-reviews', realTool: 'list-pull-reviews', description: '查看评审列表' },
96
- { shortName: 'list-assignees', realTool: 'list-pull-assignees', description: '查看处理人' },
97
- { shortName: 'upload-file', realTool: 'upload-files', description: '上传文件', repoOnly: true, upload: true, dataTip: "--file 文件路径" },
98
- { shortName: 'upload-image', realTool: 'upload-imgs', description: '上传图片', repoOnly: true, upload: true, dataTip: "--file 图片路径" },
99
- { shortName: 'get-ci-logs', realTool: '__get-ci-logs__', description: '获取 CI 失败日志', repoOnly: true, custom: true, dataTip: "--sn 构建号(可选)" },
100
- { shortName: 'get-ci-timing', realTool: '__get-ci-timing__', description: '分析 CI 耗时瓶颈', repoOnly: true, custom: true, dataTip: "--sn 构建号(可选)" },
101
- ];
76
+ // ============================================================
77
+ // 快捷命令定义
78
+ // ============================================================
79
+ //
80
+ // 数据由 csg 构建期从 cag.config.js quickCommands 字段派生,
81
+ // 写入同级目录下的 shortcuts.config.json。本文件仅负责加载并暴露给
82
+ // 其它模块(register-modules.ts / execute-action.ts / utils/upload.ts)。
83
+ //
84
+ // 编译产物中本文件位于 <skillScriptDir>/core/shortcuts.js,
85
+ // JSON 文件位于 <skillScriptDir>/core/shortcuts.config.json,二者同级。
86
+ // 源码态时 client/shortcuts.config.json 不存在(只在 csg 构建后生成),
87
+ // 因此用 try/catch 兜底,源码态返回空数组以便单元测试与类型检查。
88
+
89
+ interface ShortcutsConfigJson {
90
+ [moduleName: string]: ShortcutDefinition[];
91
+ }
92
+
93
+ function loadShortcutsConfig(): ShortcutsConfigJson {
94
+ try {
95
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
96
+ return require('./shortcuts.config.json') as ShortcutsConfigJson;
97
+ } catch {
98
+ // 源码/测试态没有产物 JSON,返回空配置。
99
+ return {};
100
+ }
101
+ }
102
+
103
+ const SHORTCUTS_CONFIG = loadShortcutsConfig();
104
+
105
+ export const ISSUE_SHORTCUTS: ShortcutDefinition[] =
106
+ SHORTCUTS_CONFIG.issues || [];
107
+
108
+ export const PR_SHORTCUTS: ShortcutDefinition[] = SHORTCUTS_CONFIG.pulls || [];
102
109
 
103
110
  // ============================================================
104
111
  // --short 帮助输出
@@ -267,6 +274,7 @@ export function resolveShortcut(
267
274
  autoPath,
268
275
  autoData,
269
276
  upload: !!matched.upload,
277
+ kind: matched.kind,
270
278
  };
271
279
  }
272
280
 
@@ -72,42 +72,57 @@ function mimeLookup(filePath: string): string {
72
72
  // 上传文件大小上限:100MB
73
73
  const MAX_FILE_SIZE = 100 * 1024 * 1024;
74
74
 
75
- /** 上传 API 的 path 参数 */
75
+ /** 上传 API 的 path 参数(issue 走 asset-group 流程时只需要 repo) */
76
76
  interface UploadPathParams {
77
77
  repo: string;
78
78
  number?: string;
79
79
  }
80
80
 
81
- /** Issue 上传请求体 */
82
- interface IssueUploadBody {
81
+ /** Issue asset-group 单条附件的描述(请求体里的元素) */
82
+ interface AssetItem {
83
83
  name: string;
84
84
  size: number;
85
85
  content_type: string;
86
86
  }
87
87
 
88
+ /** Issue asset-group 创建请求体 */
89
+ interface IssueAssetGroupBody {
90
+ file_assets?: AssetItem[];
91
+ image_assets?: AssetItem[];
92
+ }
93
+
88
94
  /** Pull 上传请求体 */
89
95
  interface PullUploadBody {
90
96
  name: string;
91
97
  size: number;
92
98
  }
93
99
 
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
- };
100
+ /** Issue asset-group 接口响应(与 swagger 一致;只列出我们用到的字段) */
101
+ interface AssetUploadEntry {
102
+ name?: string;
103
+ path?: string;
104
+ asset_link?: string;
105
+ upload_url?: string;
106
+ }
107
+ interface IssueAssetGroupResponseData {
108
+ asset_group_id?: string;
109
+ file_upload_urls?: AssetUploadEntry[];
110
+ image_upload_urls?: AssetUploadEntry[];
111
+ }
112
+
113
+ /** Pull 上传 API 响应 */
114
+ interface PullUploadResponseData {
115
+ upload_url?: string;
116
+ assets?: { name?: string; path?: string };
117
+ token?: string;
109
118
  }
110
119
 
120
+ /** 上传 API 函数签名(issue 与 pull 分别由对应的 swagger 函数承担,统一为 (firstArg, body)) */
121
+ type UploadApiFunction = (
122
+ firstArg: UploadPathParams | string,
123
+ body: IssueAssetGroupBody | PullUploadBody,
124
+ ) => Promise<{ status?: number; data?: IssueAssetGroupResponseData | PullUploadResponseData }>;
125
+
111
126
  /** 上传失败时的 data */
112
127
  interface UploadErrorData {
113
128
  error: string;
@@ -117,6 +132,7 @@ interface UploadErrorData {
117
132
  /** Issue 上传成功时的 data */
118
133
  interface IssueUploadData {
119
134
  asset_link?: string;
135
+ asset_group_id?: string;
120
136
  name: string;
121
137
  size: number;
122
138
  }
@@ -137,10 +153,21 @@ interface UploadResult {
137
153
 
138
154
  /**
139
155
  * 处理完整上传流程
140
- * @param shortcut 解析后的快捷命令
141
- * @param filePath 本地文件路径
142
- * @param toolFunction 原始上传 API 函数(获取 upload_url)
143
- * @param pathParams API 调用的 path 参数(repo 或 {repo, number})
156
+ *
157
+ * Issue 路径(走 asset-group 一次性接口):
158
+ * 1. POST /{repo}/-/issues/asset-groups 携带 file_assets image_assets
159
+ * 服务端创建 asset_group 并返回 upload_url + asset_link
160
+ * 2. PUT 文件流到 upload_url
161
+ * 3. 返回 asset_link 给调用方拼到评论或 issue 描述里
162
+ *
163
+ * Pull 路径(保留原有的 upload-files / upload-imgs 一步上传):
164
+ * 1. POST /{repo}/upload-files(或 imgs) 拿 upload_url
165
+ * 2. PUT 文件流到 upload_url
166
+ *
167
+ * @param shortcut 解析后的快捷命令;shortcut.kind 决定 issue 上传走 file 还是 image 通道
168
+ * @param filePath 本地文件路径
169
+ * @param toolFunction 原始上传 API 函数
170
+ * @param pathParams API 调用的 path 参数(issue 只需 repo;pull 也只需 repo)
144
171
  */
145
172
  export async function handleUpload(
146
173
  shortcut: ResolvedShortcut,
@@ -177,21 +204,46 @@ export async function handleUpload(
177
204
 
178
205
  // 2. 调用上传 API 获取 upload_url
179
206
  const isIssueUpload = shortcut.module === 'issues';
180
- const requestBody = isIssueUpload
181
- ? { name: fileName, size: fileSize, content_type: contentType }
182
- : { name: fileName, size: fileSize };
207
+ const isImageKind = shortcut.kind === 'image';
208
+
209
+ let uploadUrl: string | undefined;
210
+ let issueAssetLink: string | undefined;
211
+ let issueAssetGroupId: string | undefined;
212
+ let pullResponseData: PullUploadResponseData | undefined;
213
+
214
+ if (isIssueUpload) {
215
+ // Issue: 调 post-asset-group,一次拿 asset_group_id + upload_url + asset_link
216
+ // 函数签名:(repo: string, { file_assets | image_assets }, ...)
217
+ const assetItem: AssetItem = { name: fileName, size: fileSize, content_type: contentType };
218
+ const requestBody: IssueAssetGroupBody = isImageKind
219
+ ? { image_assets: [assetItem] }
220
+ : { file_assets: [assetItem] };
221
+
222
+ const apiResponse = await toolFunction(pathParams.repo, requestBody);
223
+ const data = apiResponse?.data as IssueAssetGroupResponseData | undefined;
183
224
 
184
- // Issue API 签名: fn({ repo, number }, body)
185
- // PR API 签名: fn(repo, body)
186
- const apiFirstArg: any = isIssueUpload ? pathParams : pathParams.repo;
187
- const uploadResponse = await toolFunction(apiFirstArg, requestBody);
225
+ // 服务端可能返回 201/200,从对应数组里取第一条 upload 描述
226
+ const entries = isImageKind ? data?.image_upload_urls : data?.file_upload_urls;
227
+ const entry = entries?.[0];
228
+ uploadUrl = entry?.upload_url;
229
+ issueAssetLink = entry?.asset_link;
230
+ issueAssetGroupId = data?.asset_group_id;
188
231
 
189
- // 提取 upload_url
190
- const responseData = uploadResponse?.data;
191
- const uploadUrl = responseData?.upload_url;
232
+ if (!uploadUrl) {
233
+ // URL 失败:把原始响应作为错误返回,便于排查
234
+ return apiResponse as unknown as UploadResult;
235
+ }
236
+ } else {
237
+ // Pull: 沿用原有"一步走"上传接口
238
+ // 函数签名:(repo: string, { name, size }, ...)
239
+ const requestBody: PullUploadBody = { name: fileName, size: fileSize };
240
+ const apiResponse = await toolFunction(pathParams.repo, requestBody);
241
+ pullResponseData = apiResponse?.data as PullUploadResponseData | undefined;
242
+ uploadUrl = pullResponseData?.upload_url;
192
243
 
193
- if (!uploadUrl) {
194
- return uploadResponse as unknown as UploadResult; // 获取 URL 失败,直接返回原始错误
244
+ if (!uploadUrl) {
245
+ return apiResponse as unknown as UploadResult;
246
+ }
195
247
  }
196
248
 
197
249
  // 3. PUT 文件内容到 upload_url(流式读取,避免大文件撑爆内存)
@@ -230,7 +282,8 @@ export async function handleUpload(
230
282
  return {
231
283
  status: 200,
232
284
  data: {
233
- asset_link: responseData.asset_link,
285
+ asset_link: issueAssetLink,
286
+ asset_group_id: issueAssetGroupId,
234
287
  name: fileName,
235
288
  size: fileSize,
236
289
  },
@@ -240,10 +293,10 @@ export async function handleUpload(
240
293
  return {
241
294
  status: 200,
242
295
  data: {
243
- name: responseData.assets?.name || fileName,
244
- path: responseData.assets?.path,
296
+ name: pullResponseData?.assets?.name || fileName,
297
+ path: pullResponseData?.assets?.path,
245
298
  size: fileSize,
246
- token: responseData.token,
299
+ token: pullResponseData?.token,
247
300
  },
248
301
  };
249
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -9,33 +9,7 @@ description: CNB 平台交互命令,支持仓库、Issue、PR、流水线、
9
9
 
10
10
  ## 快捷命令
11
11
 
12
- issues:
13
- - `<$CNB_CLI_CMD$> issues get` — 获取当前 Issue 详情
14
- - `<$CNB_CLI_CMD$> issues list-comments` — 列出当前 Issue 评论
15
- - `<$CNB_CLI_CMD$> issues comment --body '内容'` — 发表评论到当前 Issue
16
- - `<$CNB_CLI_CMD$> issues close` — 关闭当前 Issue
17
- - `<$CNB_CLI_CMD$> issues open` — 打开当前 Issue
18
- - `<$CNB_CLI_CMD$> issues list-labels` — 列出当前 Issue 标签
19
- - `<$CNB_CLI_CMD$> issues add-labels --labels bug --labels feature` — 添加标签到当前 Issue
20
- - `<$CNB_CLI_CMD$> issues list-assignees` — 查看当前 Issue 处理人
21
- - `<$CNB_CLI_CMD$> issues add-assignees --assignees username` — 添加处理人到当前 Issue
22
- - `<$CNB_CLI_CMD$> issues upload-file --file 文件路径` — 上传文件到当前 Issue
23
- - `<$CNB_CLI_CMD$> issues upload-image --file 图片路径` — 上传图片到当前 Issue
24
-
25
- pulls:
26
- - `<$CNB_CLI_CMD$> pulls get` — 获取当前 PR 详情
27
- - `<$CNB_CLI_CMD$> pulls list-files` — 列出当前 PR 变更文件
28
- - `<$CNB_CLI_CMD$> pulls list-commits` — 列出当前 PR 提交记录
29
- - `<$CNB_CLI_CMD$> pulls list-comments` — 列出当前 PR 评论
30
- - `<$CNB_CLI_CMD$> pulls comment --body '内容'` — 发表 PR 评论
31
- - `<$CNB_CLI_CMD$> pulls list-labels` — 列出当前 PR 标签
32
- - `<$CNB_CLI_CMD$> pulls add-labels --labels ready --labels approved` — 添加标签到当前 PR
33
- - `<$CNB_CLI_CMD$> pulls check-status` — 获取当前 PR 的 CI 状态
34
- - `<$CNB_CLI_CMD$> pulls get-ci-logs` — 获取当前 PR 的 CI 构建日志
35
- - `<$CNB_CLI_CMD$> pulls list-reviews` — 查看当前当前的评审列表
36
- - `<$CNB_CLI_CMD$> pulls list-assignees` — 查看当前 PR 处理人
37
- - `<$CNB_CLI_CMD$> pulls upload-file --file 文件路径` — 上传文件到当前 PR
38
- - `<$CNB_CLI_CMD$> pulls upload-image --file 图片路径` — 上传图片到当前 PR
12
+ <$QUICK_COMMANDS$>
39
13
 
40
14
  注意事项:
41
15
  - **链接结构**:Issue 链接格式为 `<host>/<slug>/-/issues/<number>`,PR 链接格式为 `<host>/<slug>/-/pulls/<number>`。在生成或引用链接时请遵循此结构。