@cnbcool/cnb-api-generate 2.13.0 → 2.14.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.
- package/client/lib/execute-action.ts +12 -0
- package/client/lib/git-credential.ts +8 -6
- package/client/lib/login.ts +3 -2
- package/client/lib/register-modules.ts +43 -1
- package/client/lib/skills-list.ts +178 -0
- package/client/shortcuts.ts +8 -1
- package/client/utils/resolve-platform-host.ts +64 -0
- package/package.json +3 -2
- package/quick-commands.json +3 -0
- package/skills-template/SKILL.md +28 -7
|
@@ -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);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { resolveToken } from '../utils/resolve-token';
|
|
3
|
+
import { getGitRemoteHosts } from '../utils/resolve-platform-host';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 用作 git credential helper。
|
|
@@ -11,7 +12,7 @@ import { resolveToken } from '../utils/resolve-token';
|
|
|
11
12
|
*
|
|
12
13
|
* git 在执行需要凭据的操作时,会以 `get`、`store`、`erase` 之一作为 action 调用本命令,
|
|
13
14
|
* 并通过 stdin 传入若干 `key=value` 形式的字段(protocol/host/path 等)。
|
|
14
|
-
* 我们仅处理 `get
|
|
15
|
+
* 我们仅处理 `get`:校验 host 属于当前仓库 git remote 的域名后,从后端获取凭据,按 git-credential
|
|
15
16
|
* 协议把 `username=...` / `password=...` 写到 stdout;`store` / `erase` 直接忽略。
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -27,8 +28,6 @@ interface Credential {
|
|
|
27
28
|
password: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const ALLOWED_HOSTS = ['cnb.cool', 'cnb.woa.com'];
|
|
31
|
-
|
|
32
31
|
/** git-credential get 等待 stdin 的最长时间,避免 git 无限挂起。 */
|
|
33
32
|
const GET_INPUT_TIMEOUT_MS = 8000;
|
|
34
33
|
|
|
@@ -73,7 +72,7 @@ export function registerGitCredentialCommand(program: Command): void {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
|
-
*
|
|
75
|
+
* 校验 remote 域名并向后端获取凭据,按照 git-credential 协议输出 username/password。
|
|
77
76
|
*/
|
|
78
77
|
async function auth(data: GitCredentialInput): Promise<void> {
|
|
79
78
|
const { host } = data;
|
|
@@ -81,8 +80,11 @@ async function auth(data: GitCredentialInput): Promise<void> {
|
|
|
81
80
|
`[git-credential]: for ${data.protocol}://${data.host}/${data.path ?? ''}`,
|
|
82
81
|
);
|
|
83
82
|
|
|
84
|
-
//
|
|
85
|
-
|
|
83
|
+
// 只向当前仓库 git remote 的域名提供凭据;remote 由用户配置(含子域名、api 子域),
|
|
84
|
+
// git-credential 调用链天然与仓库绑定,非本仓库 remote 的域名一律拒绝,
|
|
85
|
+
// 避免第三方域名盗取密码
|
|
86
|
+
const remoteHosts = getGitRemoteHosts();
|
|
87
|
+
if (!host || remoteHosts.length === 0 || !remoteHosts.includes(host)) {
|
|
86
88
|
throw new Error(`unknown host: ${host}`);
|
|
87
89
|
}
|
|
88
90
|
|
package/client/lib/login.ts
CHANGED
|
@@ -9,19 +9,20 @@ import {
|
|
|
9
9
|
type TokenStore,
|
|
10
10
|
} from '../utils/device-auth';
|
|
11
11
|
import { tryOpenBrowser } from '../utils/open-browser';
|
|
12
|
+
import { resolvePlatformUrl } from '../utils/resolve-platform-host';
|
|
12
13
|
|
|
13
14
|
export function registerLoginCommand(program: Command): void {
|
|
14
15
|
program
|
|
15
16
|
.command('login')
|
|
16
17
|
.description('通过 OAuth2 设备授权流登录 CNB,获取并保存 access_token')
|
|
17
18
|
.option('--client-id <string>', 'OAuth2 client_id', process.env.OAUTH2_CLIENT_ID || 'cnb_cli')
|
|
18
|
-
.option('--woa', '使用内网环境 (https://cnb.woa.com)', false)
|
|
19
19
|
.option('--debug', '打印调试信息', false)
|
|
20
20
|
.helpOption('-h, --help', '显示帮助文档')
|
|
21
21
|
.action(async (opts) => {
|
|
22
22
|
const cfg: LoginConfig = {
|
|
23
23
|
clientID: opts.clientId,
|
|
24
|
-
|
|
24
|
+
// 从 git remote -v 动态识别平台域名,兼容不同部署(公开 / 内网 / 自定义域名)
|
|
25
|
+
platformURL: resolvePlatformUrl(),
|
|
25
26
|
debug: opts.debug,
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -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);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从当前仓库的 `git remote -v` 动态识别平台域名。
|
|
5
|
+
*
|
|
6
|
+
* 统一以 git remote 解析为准,不再维护静态域名白名单,
|
|
7
|
+
* 天然兼容不同平台部署(公开 / 内网 / 自定义域名)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 执行 `git remote -v` 并解析出所有 remote 的 host(去重)。
|
|
12
|
+
* 仅在当前目录是 git 仓库时返回非空数组;解析失败或非 git 仓库时返回空数组。
|
|
13
|
+
* @returns 去重后的 host 列表,如 ['cnb.cool']
|
|
14
|
+
*/
|
|
15
|
+
export function getGitRemoteHosts(): string[] {
|
|
16
|
+
try {
|
|
17
|
+
const out = execSync('git remote -v', {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
20
|
+
});
|
|
21
|
+
const hosts = new Set<string>();
|
|
22
|
+
for (const line of out.split('\n')) {
|
|
23
|
+
// 每行形如:origin\thttps://cnb.cool/cnb/skills/cnb-skill.git (fetch)
|
|
24
|
+
const url = line.split(/\s+/)[1];
|
|
25
|
+
if (!url) continue;
|
|
26
|
+
const host = parseUrlHost(url);
|
|
27
|
+
if (host) hosts.add(host);
|
|
28
|
+
}
|
|
29
|
+
return [...hosts];
|
|
30
|
+
} catch {
|
|
31
|
+
// 非 git 仓库或 git 不可用等场景,静默返回空数组
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 从 remote URL 中解析出 host。
|
|
38
|
+
* 兼容 https://、http://、ssh:// 以及 scp-like 形式(git@host:path)。
|
|
39
|
+
* @param url
|
|
40
|
+
* @returns host,解析失败返回 null
|
|
41
|
+
*/
|
|
42
|
+
function parseUrlHost(url: string): string | null {
|
|
43
|
+
try {
|
|
44
|
+
// scp-like: git@github.com:org/repo.git
|
|
45
|
+
if (/^[^@/]+@[^:/]+:/.test(url) && !/^[a-z]+:\/\//i.test(url)) {
|
|
46
|
+
const host = url.slice(url.indexOf('@') + 1, url.indexOf(':'));
|
|
47
|
+
return host || null;
|
|
48
|
+
}
|
|
49
|
+
return new URL(url).hostname || null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析当前平台的 base URL(形如 https://cnb.cool)。
|
|
57
|
+
* 优先取 git remote 中的 host 拼成 https://{host};
|
|
58
|
+
* 无 remote 或解析失败时兜底返回 https://cnb.cool。
|
|
59
|
+
* @returns 平台 base URL
|
|
60
|
+
*/
|
|
61
|
+
export function resolvePlatformUrl(): string {
|
|
62
|
+
const host = getGitRemoteHosts()[0];
|
|
63
|
+
return host ? `https://${host}` : 'https://cnb.cool';
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cnbcool/cnb-api-generate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
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
|
|