@cnbcool/cnb-api-generate 2.13.0 → 2.14.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.
|
@@ -6,6 +6,7 @@ import { analyzeBuildTiming } from './build-timing';
|
|
|
6
6
|
import { formatParams } from './format-params';
|
|
7
7
|
import { formatOutput } from './format-output';
|
|
8
8
|
import { buildToolParams } from './build-tool-params';
|
|
9
|
+
import { skillsList } from './skills-list';
|
|
9
10
|
// @ts-ignore
|
|
10
11
|
import {getLoader} from '../../loader';
|
|
11
12
|
/**
|
|
@@ -113,6 +114,17 @@ export async function executeAction(
|
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
if (shortcut?.tool === '__skills-list__') {
|
|
118
|
+
const result = skillsList({
|
|
119
|
+
global: !!opts.global,
|
|
120
|
+
project: !!opts.project,
|
|
121
|
+
agent: (opts.agent as string) || undefined,
|
|
122
|
+
json: !!opts.json,
|
|
123
|
+
});
|
|
124
|
+
console.log(result);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
const formattedParams = formatParams(params);
|
|
117
129
|
|
|
118
130
|
const toolFunction = loadToolFunction(formattedParams.module, formattedParams.tool);
|
|
@@ -3,7 +3,7 @@ import { trimSummary } from './trim-summary';
|
|
|
3
3
|
import { helpData } from './help-data';
|
|
4
4
|
import { flatOptionsData } from './flat-options-data';
|
|
5
5
|
import { executeAction } from './execute-action';
|
|
6
|
-
import { ISSUE_SHORTCUTS, PR_SHORTCUTS, type ShortcutDefinition } from '../shortcuts';
|
|
6
|
+
import { ISSUE_SHORTCUTS, PR_SHORTCUTS, SKILLS_SHORTCUTS, type ShortcutDefinition } from '../shortcuts';
|
|
7
7
|
|
|
8
8
|
interface FlatOption {
|
|
9
9
|
optKey: string;
|
|
@@ -94,6 +94,7 @@ function registerToolOptions(toolCmd: Command, moduleName: string, toolName: str
|
|
|
94
94
|
function getShortcutsForModule(moduleName: string): ShortcutDefinition[] {
|
|
95
95
|
if (moduleName === 'issues') return ISSUE_SHORTCUTS;
|
|
96
96
|
if (moduleName === 'pulls') return PR_SHORTCUTS;
|
|
97
|
+
if (moduleName === 'skills') return SKILLS_SHORTCUTS;
|
|
97
98
|
return [];
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -186,4 +187,45 @@ export function registerModuleCommands(program: Command): void {
|
|
|
186
187
|
sub.help();
|
|
187
188
|
});
|
|
188
189
|
}
|
|
190
|
+
|
|
191
|
+
// ============================================================
|
|
192
|
+
// skills 模块:非 swagger 生成的独立 custom 模块,单独注册
|
|
193
|
+
// ============================================================
|
|
194
|
+
// 命令:cnb skills list [--json] [-g|--global] [-p|--project] [-a|--agent <agent>]
|
|
195
|
+
// 对应 quick-commands.json 中 realTool 为 __skills-list__ 的 custom 命令。
|
|
196
|
+
const skillShortcuts = SKILLS_SHORTCUTS;
|
|
197
|
+
if (skillShortcuts.length) {
|
|
198
|
+
const skillsSub = program
|
|
199
|
+
.command('skills')
|
|
200
|
+
.description(`skills 模块 (${skillShortcuts.length} tools)`)
|
|
201
|
+
.helpOption('-h, --help', '显示帮助文档')
|
|
202
|
+
.allowUnknownOption()
|
|
203
|
+
.allowExcessArguments();
|
|
204
|
+
|
|
205
|
+
for (const shortcut of skillShortcuts) {
|
|
206
|
+
const skillCmd = skillsSub
|
|
207
|
+
.command(shortcut.shortName)
|
|
208
|
+
.description(`[快捷] ${shortcut.description}`)
|
|
209
|
+
.option('-v, --verbose', '输出完整原始响应')
|
|
210
|
+
.helpOption('-h, --help', '显示帮助文档')
|
|
211
|
+
.showHelpAfterError(true)
|
|
212
|
+
.action(async (opts: Record<string, any>) => {
|
|
213
|
+
await executeAction('skills', shortcut.shortName, opts, skillsSub);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (shortcut.realTool === '__skills-list__') {
|
|
217
|
+
skillCmd
|
|
218
|
+
.option('--json', '输出 JSON 格式')
|
|
219
|
+
.option('-g, --global', '仅列出全局 scope 的 skills')
|
|
220
|
+
.option('-p, --project', '仅列出项目 scope 的 skills')
|
|
221
|
+
.option('-a, --agent <agent>', '按 agent 过滤');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
skillsSub
|
|
226
|
+
.argument('[tool]', '工具名称(支持快捷命令)')
|
|
227
|
+
.action(async (toolArg: string | undefined, _opts: Record<string, any>) => {
|
|
228
|
+
skillsSub.help();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
189
231
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cnb skills list` 自定义命令实现(方案 A)
|
|
3
|
+
*
|
|
4
|
+
* 背景:`skills list --json` 命令输出的 JSON 可能缺少 `description` 字段,
|
|
5
|
+
* 下游(如 NPC)无法直接把 skill 目录喂给 LLM。此前依赖在 default-npc
|
|
6
|
+
* 的 Dockerfile 中打补丁(`patches/skills-list-json-add-description.patch`)
|
|
7
|
+
* 给 `skills list --json` 补上 description,但该补丁强耦合 `skills` npm 包
|
|
8
|
+
* 的 `dist/cli.mjs` 内部结构,属于易碎件。
|
|
9
|
+
*
|
|
10
|
+
* 本命令不依赖补丁:内部调用原始 `skills list [scope] --json` 拿到 JSON,
|
|
11
|
+
* 对缺失 `description` 的条目,从各 `path` 指向的 SKILL.md frontmatter 解析
|
|
12
|
+
* description 回填,再输出——保证每个条目都包含 description,下游可无缝切换。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from 'child_process';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { parse as parseYaml } from 'yaml';
|
|
19
|
+
|
|
20
|
+
/** `skills list --json` 输出的单个条目结构(与 skills npm 包对齐) */
|
|
21
|
+
export interface SkillEntry {
|
|
22
|
+
name: string;
|
|
23
|
+
/** 可能缺失,需回填 */
|
|
24
|
+
description?: string;
|
|
25
|
+
path: string;
|
|
26
|
+
scope: string;
|
|
27
|
+
agents?: string[];
|
|
28
|
+
source?: string | null;
|
|
29
|
+
sourceUrl?: string | null;
|
|
30
|
+
sourceType?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 提取 SKILL.md 开头的 frontmatter 块(不含首尾 "---" 定界符)。
|
|
35
|
+
* 未找到(非 frontmatter 开头)时返回 null。
|
|
36
|
+
*/
|
|
37
|
+
function extractFrontmatter(content: string): string | null {
|
|
38
|
+
// 仅匹配开头为 "---" 的 frontmatter 块
|
|
39
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
40
|
+
return match ? match[1] : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 从 SKILL.md frontmatter 中解析 description 字段。
|
|
45
|
+
*
|
|
46
|
+
* frontmatter 形如:
|
|
47
|
+
* ---
|
|
48
|
+
* name: cnb-api
|
|
49
|
+
* description: CNB 平台交互命令...
|
|
50
|
+
* ---
|
|
51
|
+
*
|
|
52
|
+
* 使用正规 YAML 解析,因此支持多行写法(块标量 / 折行 / 续行):
|
|
53
|
+
* description: |
|
|
54
|
+
* CNB 平台交互命令...
|
|
55
|
+
* 支持代码仓库、Issue、PR、CI 读写
|
|
56
|
+
*
|
|
57
|
+
* 解析失败(文件不存在 / 无 frontmatter / 无 description / 非法 YAML)返回空串。
|
|
58
|
+
*/
|
|
59
|
+
export function parseDescriptionFromSkillMd(skillPath: string): string {
|
|
60
|
+
const mdPath = path.join(skillPath, 'SKILL.md');
|
|
61
|
+
let content: string;
|
|
62
|
+
try {
|
|
63
|
+
content = fs.readFileSync(mdPath, 'utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fm = extractFrontmatter(content);
|
|
69
|
+
if (fm === null) return '';
|
|
70
|
+
|
|
71
|
+
let parsed: unknown;
|
|
72
|
+
try {
|
|
73
|
+
parsed = parseYaml(fm);
|
|
74
|
+
} catch {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// YAML 解析结果若不是对象或没有 description 字段,返回空串
|
|
79
|
+
const desc =
|
|
80
|
+
parsed && typeof parsed === 'object'
|
|
81
|
+
? (parsed as Record<string, unknown>).description
|
|
82
|
+
: undefined;
|
|
83
|
+
if (typeof desc !== 'string') return '';
|
|
84
|
+
|
|
85
|
+
return desc.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SkillsListOptions {
|
|
89
|
+
/** 仅列出全局 scope(对应 skills list -g) */
|
|
90
|
+
global?: boolean;
|
|
91
|
+
/** 仅列出项目 scope(对应 skills list -p) */
|
|
92
|
+
project?: boolean;
|
|
93
|
+
/** 按 agent 过滤:仅保留 agents 中包含指定 agent 的条目 */
|
|
94
|
+
agent?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 运行原始 `skills list [scope] --json` 并返回解析后的 JSON 数组。
|
|
99
|
+
*
|
|
100
|
+
* scope 参数规则与 `skills list` 一致:
|
|
101
|
+
* - 指定 `-g` → `-g`
|
|
102
|
+
* - 指定 `-p` → `-p`
|
|
103
|
+
* - 都不指定 → 不传 scope(由 skills 默认:项目优先,否则全局)
|
|
104
|
+
*/
|
|
105
|
+
export function runSkillsListJson(opts: SkillsListOptions): SkillEntry[] {
|
|
106
|
+
const args = ['list'];
|
|
107
|
+
if (opts.global) args.push('-g');
|
|
108
|
+
if (opts.project) args.push('-p');
|
|
109
|
+
args.push('--json');
|
|
110
|
+
|
|
111
|
+
let stdout: string;
|
|
112
|
+
try {
|
|
113
|
+
stdout = execFileSync('skills', args, {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
116
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
117
|
+
});
|
|
118
|
+
} catch (e: any) {
|
|
119
|
+
const stderr = e?.stderr?.toString?.() || '';
|
|
120
|
+
throw new Error(
|
|
121
|
+
`执行 skills list 失败:${e?.message || String(e)}${stderr ? `\n${stderr}` : ''}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(stdout);
|
|
127
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
throw new Error(`解析 skills list --json 输出失败:${e?.message || String(e)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 对 `skills list --json` 输出做后处理:
|
|
135
|
+
* 1. 从 SKILL.md frontmatter 回填缺失的 description;
|
|
136
|
+
* 2. 按 agent 过滤(若指定 -a)。
|
|
137
|
+
*
|
|
138
|
+
* 返回的是全新数组/对象,不修改原始输入。
|
|
139
|
+
*/
|
|
140
|
+
export function decorateSkillEntries(
|
|
141
|
+
entries: SkillEntry[],
|
|
142
|
+
opts: SkillsListOptions,
|
|
143
|
+
): SkillEntry[] {
|
|
144
|
+
const result: SkillEntry[] = [];
|
|
145
|
+
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
// agent 过滤:保留 agents 中包含指定 agent 的条目
|
|
148
|
+
if (opts.agent) {
|
|
149
|
+
const agents = Array.isArray(entry.agents) ? entry.agents : [];
|
|
150
|
+
if (!agents.includes(opts.agent)) continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const decorated: SkillEntry = { ...entry };
|
|
154
|
+
|
|
155
|
+
// 回填 description:缺失或为空时从 path 下的 SKILL.md frontmatter 解析
|
|
156
|
+
if (!decorated.description || !decorated.description.trim()) {
|
|
157
|
+
decorated.description = parseDescriptionFromSkillMd(entry.path) || undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
result.push(decorated);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `cnb skills list` 主入口。
|
|
168
|
+
*
|
|
169
|
+
* 参数对齐 `skills list --json`:`-g/-p/-a/--json`。
|
|
170
|
+
* 无论原始 `skills list` 是否被打补丁,输出都保证每条含 description。
|
|
171
|
+
*/
|
|
172
|
+
export function skillsList(opts: SkillsListOptions & { json?: boolean }): string {
|
|
173
|
+
const raw = runSkillsListJson(opts);
|
|
174
|
+
const decorated = decorateSkillEntries(raw, opts);
|
|
175
|
+
|
|
176
|
+
// 与 `skills list --json` 保持一致的 JSON 输出(默认即 JSON,字段结构不变)
|
|
177
|
+
return JSON.stringify(decorated, null, 2);
|
|
178
|
+
}
|
package/client/shortcuts.ts
CHANGED
|
@@ -107,6 +107,9 @@ export const ISSUE_SHORTCUTS: ShortcutDefinition[] =
|
|
|
107
107
|
|
|
108
108
|
export const PR_SHORTCUTS: ShortcutDefinition[] = SHORTCUTS_CONFIG.pulls || [];
|
|
109
109
|
|
|
110
|
+
export const SKILLS_SHORTCUTS: ShortcutDefinition[] =
|
|
111
|
+
SHORTCUTS_CONFIG.skills || [];
|
|
112
|
+
|
|
110
113
|
// ============================================================
|
|
111
114
|
// --short 帮助输出
|
|
112
115
|
// ============================================================
|
|
@@ -220,6 +223,8 @@ ${prList}
|
|
|
220
223
|
// ============================================================
|
|
221
224
|
|
|
222
225
|
function buildAutoPath(moduleName: string, repoOnly: boolean): Record<string, string> {
|
|
226
|
+
// skills 模块不依赖 repo/number,无自动注入的 path 参数
|
|
227
|
+
if (moduleName === 'skills') return {};
|
|
223
228
|
const repo = process.env.CNB_REPO_SLUG || '';
|
|
224
229
|
if (repoOnly) return { repo };
|
|
225
230
|
const number = moduleName === 'issues'
|
|
@@ -243,7 +248,9 @@ export function resolveShortcut(
|
|
|
243
248
|
? ISSUE_SHORTCUTS
|
|
244
249
|
: moduleName === 'pulls'
|
|
245
250
|
? PR_SHORTCUTS
|
|
246
|
-
:
|
|
251
|
+
: moduleName === 'skills'
|
|
252
|
+
? SKILLS_SHORTCUTS
|
|
253
|
+
: null;
|
|
247
254
|
if (!shortcuts) return null;
|
|
248
255
|
|
|
249
256
|
const matched = shortcuts.find((s) => s.shortName === toolName);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cnbcool/cnb-api-generate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"main": "./built/index.js",
|
|
5
5
|
"module": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"prettier": "3.4.2",
|
|
50
50
|
"rimraf": "6.0.1",
|
|
51
51
|
"slashes": "^3.0.12",
|
|
52
|
-
"typescript": "5.9.3"
|
|
52
|
+
"typescript": "5.9.3",
|
|
53
|
+
"yaml": "^2.9.0"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@reduxjs/toolkit": "^1.9.5",
|
package/quick-commands.json
CHANGED
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
{ "shortName": "get-ci-timing", "realTool": "__get-ci-timing__", "description": "分析 CI 耗时瓶颈", "repoOnly": true, "custom": true, "dataTip": "--sn 构建号(可选)" },
|
|
34
34
|
{ "shortName": "get-imgs", "realTool": "get-pr-imgs", "description": "获取 PR 图片", "repoOnly": true, "dataTip": "--img-path 图片路径" },
|
|
35
35
|
{ "shortName": "get-files", "realTool": "get-pr-files", "description": "获取 PR 附件", "repoOnly": true, "dataTip": "--file-path 附件路径" }
|
|
36
|
+
],
|
|
37
|
+
"skills": [
|
|
38
|
+
{ "shortName": "list", "realTool": "__skills-list__", "description": "列出本地 skills", "custom": true, "dataTip": "--json -g -p -a agent" }
|
|
36
39
|
]
|
|
37
40
|
}
|
package/skills-template/SKILL.md
CHANGED
|
@@ -5,19 +5,40 @@ description: CNB 平台交互命令,支持代码仓库、Issue、PR、CI、制
|
|
|
5
5
|
|
|
6
6
|
# cnb-api
|
|
7
7
|
|
|
8
|
-
操作 CNB 平台资源的 CLI 工具。
|
|
9
|
-
|
|
10
8
|
## 快捷命令
|
|
11
9
|
|
|
12
10
|
<$QUICK_COMMANDS$>
|
|
13
11
|
|
|
14
12
|
注意事项:
|
|
15
13
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- **多行文本传参**:bash
|
|
19
|
-
-
|
|
20
|
-
-
|
|
14
|
+
- **参数自动识别**:Issue/PR 编号自动从环境变量识别,无需额外传递。
|
|
15
|
+
- **默认仅需摘要**:默认精简输出,加 `--verbose` 输出完整数据。
|
|
16
|
+
- **多行文本传参**:bash 参数为多行文本时用单引号,降低命令注入风险。
|
|
17
|
+
- **适用范围**:快捷命令仅限当前仓库的当前 Issue/PR,跨仓库或跨编号请参考 `更多 API`。
|
|
18
|
+
- **提及与召唤**:评论中直接 @npc 会召唤 npc;仅提及不召唤时,用反引号包裹 `@npc`。
|
|
19
|
+
|
|
20
|
+
## PR 相关规范
|
|
21
|
+
|
|
22
|
+
> 本节 PR 规范为默认约定,若项目另有规定,以项目规定为准。
|
|
23
|
+
|
|
24
|
+
### 标题
|
|
25
|
+
|
|
26
|
+
- **一行表达**:采用语义化提交格式。
|
|
27
|
+
- **只保留核心问题**:标题要简洁、可读、语义清晰。
|
|
28
|
+
- **可读性**:影响可读性的信息禁止写入标题。
|
|
29
|
+
- **禁止括号**:标题禁止出现括号,补充说明写描述区。
|
|
30
|
+
|
|
31
|
+
### 描述
|
|
32
|
+
|
|
33
|
+
- **关联引用**:需包含 `Ref: #<ISSUE_ID>`。
|
|
34
|
+
- **关联信息**:关联信息和补充说明应该写进描述区,比如 `Ref #xxx`、`cherry-pick #xxx`、版本号、日期、作者等。
|
|
35
|
+
- **cherry-pick**:目标版本写入 body 末尾。
|
|
36
|
+
|
|
37
|
+
### 提交流程
|
|
38
|
+
|
|
39
|
+
- **提交后立即结束**:创建/推送 PR 后马上结束,不做其他操作。
|
|
40
|
+
- **禁止轮询 CI 与评审**:不等待 CI 与评审状态;失败会自动唤起 NPC。
|
|
41
|
+
- **禁止合并/关闭**:AI 不执行合并/关闭操作,合并交由人工完成。
|
|
21
42
|
|
|
22
43
|
## 常用链接
|
|
23
44
|
|