@cnbcool/cnb-api-generate 2.5.0 → 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.
- package/built/skills-tools/generate-shortcuts-config.js +32 -0
- package/built/skills-tools/generate-skill-md.js +17 -2
- package/built/skills-tools/render-quick-commands-md.js +37 -0
- package/built/skills-tools/resolve-quick-commands.js +55 -0
- package/built/skills.js +8 -2
- package/client/core.ts +2 -0
- package/client/fetch-response-handler.ts +4 -1
- package/client/index.ts +2 -0
- package/client/lib/format-params.ts +21 -1
- package/client/lib/git-credential.ts +182 -0
- package/client/shortcuts.ts +38 -30
- package/client/utils/get-token-env-key.ts +7 -0
- package/client/utils/is-workbuddy-sandbox.ts +3 -0
- package/client/utils/refresh-token.ts +1 -1
- package/client/utils/resolve-token.ts +34 -0
- package/client/utils/upload.ts +91 -38
- package/package.json +1 -1
- package/skills-template/SKILL.md +1 -27
|
@@ -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(
|
|
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
|
|
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
|
-
//
|
|
35
|
-
|
|
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
|
-
|
|
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`);
|
package/client/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { registerFallbackAction } from './lib/register-fallback';
|
|
|
7
7
|
import { registerLoginCommand } from './lib/login';
|
|
8
8
|
import { registerLogoutCommand } from './lib/logout';
|
|
9
9
|
import { registerStatusCommand } from './lib/status';
|
|
10
|
+
import { registerGitCredentialCommand } from './lib/git-credential';
|
|
10
11
|
|
|
11
12
|
// ============================================================
|
|
12
13
|
// Commander 程序定义
|
|
@@ -30,6 +31,7 @@ program
|
|
|
30
31
|
registerLoginCommand(program);
|
|
31
32
|
registerLogoutCommand(program);
|
|
32
33
|
registerStatusCommand(program);
|
|
34
|
+
registerGitCredentialCommand(program);
|
|
33
35
|
registerModuleCommands(program);
|
|
34
36
|
registerFallbackAction(program);
|
|
35
37
|
|
|
@@ -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
|
|
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
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveToken } from '../utils/resolve-token';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 用作 git credential helper。
|
|
6
|
+
*
|
|
7
|
+
* 在 .gitconfig 中配置:
|
|
8
|
+
* [credential]
|
|
9
|
+
* helper = "<cli> git-credential"
|
|
10
|
+
* useHttpPath = true
|
|
11
|
+
*
|
|
12
|
+
* git 在执行需要凭据的操作时,会以 `get`、`store`、`erase` 之一作为 action 调用本命令,
|
|
13
|
+
* 并通过 stdin 传入若干 `key=value` 形式的字段(protocol/host/path 等)。
|
|
14
|
+
* 我们仅处理 `get`:根据 host 白名单校验后,从后端获取凭据,按 git-credential
|
|
15
|
+
* 协议把 `username=...` / `password=...` 写到 stdout;`store` / `erase` 直接忽略。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface GitCredentialInput {
|
|
19
|
+
protocol?: string;
|
|
20
|
+
host?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
[key: string]: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Credential {
|
|
26
|
+
username: string;
|
|
27
|
+
password: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ALLOWED_HOSTS = ['cnb.cool', 'cnb.woa.com'];
|
|
31
|
+
|
|
32
|
+
/** git-credential get 等待 stdin 的最长时间,避免 git 无限挂起。 */
|
|
33
|
+
const GET_INPUT_TIMEOUT_MS = 8000;
|
|
34
|
+
|
|
35
|
+
export function registerGitCredentialCommand(program: Command): void {
|
|
36
|
+
program
|
|
37
|
+
.command('git-credential <action>')
|
|
38
|
+
.description('作为 git credential helper,向 git 提供 CNB 凭据(仅供 git 内部调用)')
|
|
39
|
+
.allowUnknownOption()
|
|
40
|
+
.allowExcessArguments()
|
|
41
|
+
.helpOption('-h, --help', '显示帮助文档')
|
|
42
|
+
.action(async (action: string, _opts, cmd: Command) => {
|
|
43
|
+
// 透传给用户:剩余的位置参数都视为自定义参数
|
|
44
|
+
const customArgs = cmd.args.slice(0, -1);
|
|
45
|
+
if (customArgs.length > 0) {
|
|
46
|
+
console.error(`[git-credential]: custom args: ${customArgs.join(' ')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// store / erase:保持与原 helper 一致,忽略并退出 0
|
|
50
|
+
if (action === 'store' || action === 'erase') {
|
|
51
|
+
console.error(`[git-credential]: ${action} ignored`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (action !== 'get') {
|
|
56
|
+
console.error(`[git-credential]: unknown action: ${action}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// get:从 stdin 读取字段并输出凭据
|
|
61
|
+
try {
|
|
62
|
+
const input = await readStdin(GET_INPUT_TIMEOUT_MS);
|
|
63
|
+
const data = parseText(input, '=') as GitCredentialInput;
|
|
64
|
+
normalizePath(data);
|
|
65
|
+
|
|
66
|
+
await auth(data);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
console.error(err?.message ? err.message : String(err));
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 校验白名单并向后端获取凭据,按照 git-credential 协议输出 username/password。
|
|
77
|
+
*/
|
|
78
|
+
async function auth(data: GitCredentialInput): Promise<void> {
|
|
79
|
+
const { host } = data;
|
|
80
|
+
console.error(
|
|
81
|
+
`[git-credential]: for ${data.protocol}://${data.host}/${data.path ?? ''}`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 必须校验域名,否则传入第三方域名就会被盗密码
|
|
85
|
+
if (!host || !ALLOWED_HOSTS.includes(host)) {
|
|
86
|
+
throw new Error(`unknown host: ${host}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const credential = await fetchCredential(data);
|
|
90
|
+
|
|
91
|
+
// git-credential 协议:通过 stdout 输出 key=value
|
|
92
|
+
process.stdout.write(`username=${credential.username}\n`);
|
|
93
|
+
process.stdout.write(`password=${credential.password}\n`);
|
|
94
|
+
|
|
95
|
+
console.error('[git-credential]: done');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchCredential(_data: GitCredentialInput): Promise<Credential> {
|
|
99
|
+
// 使用 resolveToken 获取 access_token 作为 password,username 固定为 cnb
|
|
100
|
+
const token = await resolveToken();
|
|
101
|
+
return {
|
|
102
|
+
username: 'cnb',
|
|
103
|
+
password: token,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* git-credential 协议中 path 通常包含仓库目录,需要规范化。
|
|
109
|
+
*/
|
|
110
|
+
function normalizePath(data: GitCredentialInput): void {
|
|
111
|
+
if (!data.path) return;
|
|
112
|
+
if (data.path.endsWith('.git')) {
|
|
113
|
+
data.path = data.path.slice(0, -4);
|
|
114
|
+
} else if (data.path.endsWith('.git/info/lfs')) {
|
|
115
|
+
data.path = data.path.slice(0, -13);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 从 stdin 收集所有数据。
|
|
121
|
+
*
|
|
122
|
+
* 同原始 helper 一致:必须设置超时,否则 git 在拿不到响应时会让用户输入账号密码,
|
|
123
|
+
* 进而把进程挂起。同时支持单字节回车(0x0a)作为提前结束的信号。
|
|
124
|
+
*/
|
|
125
|
+
function readStdin(timeoutMs: number): Promise<string> {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const buff: Buffer[] = [];
|
|
128
|
+
let settled = false;
|
|
129
|
+
|
|
130
|
+
const finish = (): void => {
|
|
131
|
+
if (settled) return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
resolve(Buffer.concat(buff).toString('utf-8'));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const timer = setTimeout(() => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
settled = true;
|
|
140
|
+
console.error('[git-credential]: timeout occurred');
|
|
141
|
+
reject(new Error('stdin timeout'));
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
|
|
144
|
+
process.stdin.on('data', (chunk: Buffer) => {
|
|
145
|
+
buff.push(chunk);
|
|
146
|
+
// git 会在最后发送一个空行(仅一个 \n)作为结束标志
|
|
147
|
+
if (chunk.length === 1 && chunk[0] === 0x0a) {
|
|
148
|
+
finish();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
process.stdin.on('end', () => {
|
|
153
|
+
finish();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
process.stdin.on('error', (err) => {
|
|
157
|
+
if (settled) return;
|
|
158
|
+
settled = true;
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
reject(err);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 把 git 通过 stdin 传入的多行 key=value 文本解析为对象。
|
|
167
|
+
*/
|
|
168
|
+
function parseText(content: string, separator: string): Record<string, string> {
|
|
169
|
+
return content
|
|
170
|
+
.split(/(?:\r\n|\r|\n)/)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map((line) => {
|
|
173
|
+
const idx = line.indexOf(separator);
|
|
174
|
+
const key = idx >= 0 ? line.substring(0, idx).trim() : line.trim();
|
|
175
|
+
const value = idx >= 0 ? line.substring(idx + 1).trim() : '';
|
|
176
|
+
return { key, value };
|
|
177
|
+
})
|
|
178
|
+
.reduce<Record<string, string>>((acc, { key, value }) => {
|
|
179
|
+
if (key) acc[key] = value;
|
|
180
|
+
return acc;
|
|
181
|
+
}, {});
|
|
182
|
+
}
|
package/client/shortcuts.ts
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
|
@@ -10,7 +10,7 @@ export async function refreshAccessToken(store: TokenStore): Promise<string> {
|
|
|
10
10
|
throw new Error('no refresh_token');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const platformURL = store.platform_url || 'https://cnb
|
|
13
|
+
const platformURL = store.platform_url || 'https://cnb.cool';
|
|
14
14
|
const clientID = store.client_id || process.env.OAUTH2_CLIENT_ID || 'cnb_cli';
|
|
15
15
|
const tokenURL = `${platformURL.replace(/\/+$/, '')}/oauth2/token`;
|
|
16
16
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getTokenEnvKey } from './get-token-env-key';
|
|
2
|
+
import { isWorkBuddySandbox } from './is-workbuddy-sandbox';
|
|
1
3
|
import { loadToken } from './load-token';
|
|
2
4
|
import { refreshAccessToken } from './refresh-token';
|
|
3
5
|
|
|
@@ -9,6 +11,38 @@ import { refreshAccessToken } from './refresh-token';
|
|
|
9
11
|
* 刷新失败或无 refresh_token 时终止进程并提示用户重新登录。
|
|
10
12
|
*/
|
|
11
13
|
export async function resolveToken(): Promise<string> {
|
|
14
|
+
// WorkBuddy 沙箱环境,从远程接口获取 token
|
|
15
|
+
if (isWorkBuddySandbox()) {
|
|
16
|
+
const tokenURL = process.env[getTokenEnvKey()];
|
|
17
|
+
if (!tokenURL) {
|
|
18
|
+
console.error(`未找到环境变量 ${getTokenEnvKey()},无法在 WorkBuddy 沙箱环境中获取 token。`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const resp = await fetch(tokenURL, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
headers: { Accept: 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!resp.ok) {
|
|
29
|
+
console.error(`从 WorkBuddy 获取 token 失败,HTTP 状态码: ${resp.status} ${resp.statusText}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const body: any = await resp.json();
|
|
34
|
+
if (body?.code !== 0 || !body?.data?.access_token) {
|
|
35
|
+
console.error(`从 WorkBuddy 获取 token 失败,响应内容异常: ${JSON.stringify(body)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return body.data.access_token as string;
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
console.error(`从 WorkBuddy 获取 token 失败: ${err?.message || err}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
// 环境变量优先 CodeBuddy
|
|
13
47
|
if (process.env.CNB_TOKEN_FOR_CODEBUDDY) {
|
|
14
48
|
if (!process.env.CNB_NPC_SLUG && process.env.CNB_NPC_NAME === 'CodeBuddy') {
|
package/client/utils/upload.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
interface
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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:
|
|
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:
|
|
244
|
-
path:
|
|
296
|
+
name: pullResponseData?.assets?.name || fileName,
|
|
297
|
+
path: pullResponseData?.assets?.path,
|
|
245
298
|
size: fileSize,
|
|
246
|
-
token:
|
|
299
|
+
token: pullResponseData?.token,
|
|
247
300
|
},
|
|
248
301
|
};
|
|
249
302
|
}
|
package/package.json
CHANGED
package/skills-template/SKILL.md
CHANGED
|
@@ -9,33 +9,7 @@ description: CNB 平台交互命令,支持仓库、Issue、PR、流水线、
|
|
|
9
9
|
|
|
10
10
|
## 快捷命令
|
|
11
11
|
|
|
12
|
-
|
|
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>`。在生成或引用链接时请遵循此结构。
|