@cnbcool/cnb-api-generate 1.2.8 → 2.1.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.
Files changed (37) hide show
  1. package/built/codegen/printer/printer-skills-client-core.js +10 -0
  2. package/built/utils/clean-array-desc.js +9 -0
  3. package/built/utils/collect-used-keys.js +16 -0
  4. package/built/utils/flat-option.js +2 -0
  5. package/built/utils/flatten-array-object-options.js +35 -0
  6. package/built/utils/flatten-nested-object-options.js +40 -0
  7. package/built/utils/flatten-tool-options.js +108 -0
  8. package/built/utils/generate-flat-options.js +50 -0
  9. package/built/utils/is-array-of-objects.js +10 -0
  10. package/built/utils/is-nested-object.js +9 -0
  11. package/built/utils/option-value-flag.js +14 -0
  12. package/built/utils/to-display-key.js +10 -0
  13. package/built/utils/trim-summary.js +12 -0
  14. package/client/index.ts +29 -504
  15. package/client/lib/execute-action.ts +126 -0
  16. package/client/lib/extra-help.ts +15 -0
  17. package/client/lib/flat-options-data.ts +13 -0
  18. package/client/lib/format-output.ts +79 -0
  19. package/client/lib/format-params.ts +220 -0
  20. package/client/lib/help-data.ts +13 -0
  21. package/client/lib/key-mapping-data.ts +13 -0
  22. package/client/lib/parsers.ts +130 -0
  23. package/client/lib/print-json.ts +12 -0
  24. package/client/lib/register-fallback.ts +14 -0
  25. package/client/lib/register-modules.ts +121 -0
  26. package/client/lib/summary-extractors.ts +189 -0
  27. package/client/lib/trim-summary.ts +11 -0
  28. package/client/shortcuts.ts +10 -198
  29. package/client/utils/build-nested-field-map.ts +38 -0
  30. package/client/utils/flat-key-from-segments.ts +8 -0
  31. package/client/utils/match-array-object-field.ts +19 -0
  32. package/client/utils/restore-original-keys.ts +23 -0
  33. package/package.json +3 -2
  34. package/skills-template/SKILL.md +9 -10
  35. package/client/modules.help.ts +0 -49
  36. package/client/schemaToJson.ts +0 -26
  37. package/client/tools.help.ts +0 -124
@@ -0,0 +1,79 @@
1
+ import { printJSON } from './print-json';
2
+ import { summarizeResponse } from './summary-extractors';
3
+
4
+ /**
5
+ * 精简响应对象(非 verbose 模式使用)
6
+ * - 去掉 trace(仅错误时保留)
7
+ * - 去掉 header(原始 x-cnb-* 头)
8
+ * - 仅列表 API 时保留分页信息
9
+ */
10
+ export function compactResponse(response: any): any {
11
+ if (!response || typeof response !== 'object') return response;
12
+
13
+ const compact: any = { status: response.status };
14
+
15
+ // 仅错误时保留 trace
16
+ if (response.status >= 300 && response.trace) {
17
+ compact.trace = response.trace;
18
+ }
19
+
20
+ // 仅列表 API 时保留分页信息
21
+ if (response.total != null) {
22
+ compact.page = response.page;
23
+ compact.pageSize = response.pageSize;
24
+ compact.total = response.total;
25
+ compact.totalPages = response.totalPages;
26
+ }
27
+
28
+ compact.data = response.data;
29
+ return compact;
30
+ }
31
+
32
+ /**
33
+ * 判断是否是标准响应结构(有 status + data 字段)
34
+ * cag.config.js 中的 responseConverter 可能返回裸 data(不含 status),需要区分处理
35
+ */
36
+ export function isStandardResponse(response: any): boolean {
37
+ return response && typeof response === 'object' && typeof response.status === 'number' && 'data' in response;
38
+ }
39
+
40
+ /**
41
+ * 格式化 CLI 输出
42
+ * - verbose 模式:完整 JSON(含 trace、header 等全部字段)
43
+ * - 快捷命令默认:只输出核心摘要字段
44
+ * - 非快捷命令默认:精简 JSON(去掉 trace、header,保留完整 data)
45
+ * @param response 原始响应
46
+ * @param verbose 是否 verbose 模式
47
+ * @param summary 是否 summary 模式
48
+ * @param toolKey 当前 tool 标识,格式为 "module/tool",用于匹配摘要配置
49
+ */
50
+ export function formatOutput(response: any, verbose: boolean, summary: boolean, toolKey: string): string {
51
+ // --verbose 模式:输出完整原始信息
52
+ if (verbose) {
53
+ return printJSON(response, verbose);
54
+ }
55
+
56
+ // converter 可能返回裸 data(没有标准 {status, data} 结构),直接输出
57
+ if (!isStandardResponse(response)) {
58
+ if (summary) {
59
+ const summarized = summarizeResponse(response, toolKey);
60
+ if (summarized !== null) {
61
+ return printJSON(summarized);
62
+ }
63
+ }
64
+ return printJSON(response);
65
+ }
66
+
67
+ // 精简响应
68
+ const compact = compactResponse(response);
69
+
70
+ // summary 模式:对特定 tool 应用摘要提取(仅成功响应)
71
+ if (summary && compact.status >= 200 && compact.status < 300) {
72
+ const summarized = summarizeResponse(compact.data, toolKey);
73
+ if (summarized !== null) {
74
+ compact.data = summarized;
75
+ }
76
+ }
77
+
78
+ return printJSON(compact);
79
+ }
@@ -0,0 +1,220 @@
1
+ import { helpData } from './help-data';
2
+ import { tryParseJSON } from './parsers';
3
+ import { buildNestedFieldMap } from '../utils/build-nested-field-map';
4
+ import type { NestedFieldMapping } from '../utils/build-nested-field-map';
5
+ import { restoreOriginalKeys } from '../utils/restore-original-keys';
6
+ import { matchArrayObjectField } from '../utils/match-array-object-field';
7
+
8
+ /**
9
+ * 获取 tool 的参数定义(用于自动分发 --key value 到 path/query)
10
+ */
11
+ export function getToolParamDefs(moduleName: string, toolName: string) {
12
+ const toolHelp = helpData.modulesHelp?.[moduleName]?.[toolName];
13
+ if (!toolHelp) return null;
14
+ return toolHelp.help?.parameter || {};
15
+ }
16
+
17
+ /**
18
+ * 格式化参数
19
+ * 支持新的 --key value 扁平格式,同时向后兼容旧的 --path/--query JSON 格式。
20
+ * 根据 help.json 中的参数定义,自动将扁平参数分发到 path 或 query。
21
+ * @param params 原始参数对象
22
+ * @returns 格式化后的参数对象,包含 module, tool, path, query, data 等
23
+ */
24
+ export function formatParams(
25
+ params: Record<string, string | string[] | boolean | undefined>,
26
+ ): Record<string, any> {
27
+ console.log(JSON.stringify(params))
28
+ // 先将 CLI 带 '-' 的参数名还原为原始 '_'/'@' 参数名
29
+ params = restoreOriginalKeys(params);
30
+ console.log(params)
31
+ const formatted: Record<string, any> = {
32
+ module: params.module,
33
+ tool: params.tool,
34
+ };
35
+
36
+ // 保留控制标志
37
+ if (params.help) formatted.help = true;
38
+ if (params.short) formatted.short = true;
39
+ if (params.verbose) formatted.verbose = true;
40
+
41
+ // 新格式:将其他 --key value 根据 help.json 自动分发到 path/query
42
+ const paramDefs = getToolParamDefs(
43
+ params.module as string,
44
+ params.tool as string,
45
+ );
46
+
47
+ if (paramDefs) {
48
+ const pathDef = paramDefs.path || {};
49
+ const queryDef = paramDefs.query || {};
50
+ const bodyProps = paramDefs.body?.schema?.properties || {};
51
+
52
+ const reservedKeys = new Set([
53
+ 'module', 'tool', 'help', 'short', 'verbose',
54
+ 'path', 'query', 'data', 'h', 'v',
55
+ ]);
56
+
57
+ // 收集 array<object> 展开字段的临时存储:{ arrayName: { propName: string[] } }
58
+ const arrayObjectCollector: Record<string, Record<string, string[]>> = {};
59
+
60
+ // 收集嵌套对象展开字段的临时存储(深层嵌套结构):{ rootObjectKey: { nested: { path: value } } }
61
+ const nestedObjectCollector: Record<string, Record<string, any>> = {};
62
+
63
+ // 预建所有嵌套对象字段的 camelCased flat key → pathKeys 映射表
64
+ const nestedFieldMap = new Map<string, NestedFieldMapping>();
65
+ for (const [rootKey, rootProp] of Object.entries(bodyProps) as [string, any][]) {
66
+ if (rootProp.type === 'object' && rootProp.properties && Object.keys(rootProp.properties).length > 0) {
67
+ const subMap = buildNestedFieldMap(rootProp.properties, rootKey, [rootKey]);
68
+ for (const [k, v] of subMap) {
69
+ nestedFieldMap.set(k, v);
70
+ }
71
+ }
72
+ }
73
+
74
+ for (const [key, value] of Object.entries(params)) {
75
+ if (reservedKeys.has(key)) continue;
76
+ // boolean 值需要特殊处理:嵌套对象的 boolean 子字段不能跳过
77
+ if (typeof value === 'boolean') {
78
+ const nestedMapping = nestedFieldMap.get(key);
79
+ if (nestedMapping && nestedMapping.leafSchema?.type === 'boolean') {
80
+ const { pathKeys } = nestedMapping;
81
+ const rootKey = pathKeys[0];
82
+ if (!nestedObjectCollector[rootKey]) {
83
+ nestedObjectCollector[rootKey] = {};
84
+ }
85
+ const subPathKeys = pathKeys.slice(1);
86
+ let target = nestedObjectCollector[rootKey];
87
+ for (let pi = 0; pi < subPathKeys.length - 1; pi++) {
88
+ if (!target[subPathKeys[pi]] || typeof target[subPathKeys[pi]] !== 'object') {
89
+ target[subPathKeys[pi]] = {};
90
+ }
91
+ target = target[subPathKeys[pi]];
92
+ }
93
+ target[subPathKeys[subPathKeys.length - 1]] = value;
94
+ }
95
+ continue;
96
+ }
97
+
98
+ if (pathDef[key]) {
99
+ // 归入 path
100
+ if (!formatted.path) formatted.path = {};
101
+ formatted.path[key] = value;
102
+ } else if (queryDef[key]) {
103
+ // 归入 query
104
+ if (!formatted.query) formatted.query = {};
105
+ const paramType = queryDef[key].type;
106
+ if (paramType === 'number' && typeof value === 'string' && !isNaN(Number(value))) {
107
+ formatted.query[key] = Number(value);
108
+ } else {
109
+ formatted.query[key] = value;
110
+ }
111
+ } else if (bodyProps[key]) {
112
+ // body 字段(无冲突,直接用原 key)
113
+ if (!formatted.data) formatted.data = {};
114
+ formatted.data[key] = Array.isArray(value) ? value : tryParseJSON(value as string);
115
+ } else {
116
+ // 处理 d- 前缀(冲突时 CLI 以 d- 前缀传入)
117
+ const stripped = (key.startsWith('d-') || key.startsWith('d_'))
118
+ ? key.replace(/^d[-_]/, '')
119
+ : key;
120
+
121
+ // 尝试匹配嵌套对象展开字段(通过预建映射表,兼容 Commander camelCase)
122
+ const nestedMapping = nestedFieldMap.get(stripped);
123
+ // 尝试匹配 array<object> 展开字段
124
+ const arrayMatch = matchArrayObjectField(stripped, bodyProps);
125
+
126
+ if (nestedMapping) {
127
+ // 匹配到嵌套对象叶子字段
128
+ const { pathKeys, leafSchema } = nestedMapping;
129
+ const rootKey = pathKeys[0];
130
+ if (!nestedObjectCollector[rootKey]) {
131
+ nestedObjectCollector[rootKey] = {};
132
+ }
133
+ // 类型转换
134
+ let convertedValue: any = value;
135
+ if (leafSchema?.type === 'number' && typeof value === 'string' && !isNaN(Number(value))) {
136
+ convertedValue = Number(value);
137
+ } else if (leafSchema?.type === 'boolean') {
138
+ convertedValue = value === 'true' || (value as any) === true;
139
+ } else if (typeof value === 'string') {
140
+ convertedValue = tryParseJSON(value);
141
+ }
142
+ // 用 pathKeys(去掉根 key)深度设置到 collector 中
143
+ const subPathKeys = pathKeys.slice(1);
144
+ let target = nestedObjectCollector[rootKey];
145
+ for (let pi = 0; pi < subPathKeys.length - 1; pi++) {
146
+ if (!target[subPathKeys[pi]] || typeof target[subPathKeys[pi]] !== 'object') {
147
+ target[subPathKeys[pi]] = {};
148
+ }
149
+ target = target[subPathKeys[pi]];
150
+ }
151
+ target[subPathKeys[subPathKeys.length - 1]] = convertedValue;
152
+ } else if (arrayMatch) {
153
+ // 收集到 collector 中,后续统一 zip
154
+ if (!arrayObjectCollector[arrayMatch.arrayKey]) {
155
+ arrayObjectCollector[arrayMatch.arrayKey] = {};
156
+ }
157
+ const vals = Array.isArray(value) ? value : [value as string];
158
+ arrayObjectCollector[arrayMatch.arrayKey][arrayMatch.subKey] = vals;
159
+ } else if (key.startsWith('d-') || key.startsWith('d_')) {
160
+ // body 字段(有冲突,CLI 中以 d- 或 d_ 前缀传入)
161
+ const originalKey = key.replace(/^d[-_]/, '');
162
+ if (bodyProps[originalKey]) {
163
+ if (!formatted.data) formatted.data = {};
164
+ formatted.data[originalKey] = Array.isArray(value) ? value : tryParseJSON(value as string);
165
+ } else {
166
+ if (!formatted.query) formatted.query = {};
167
+ formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
168
+ }
169
+ } else {
170
+ // 未知参数,尝试放入 query(兼容)
171
+ if (!formatted.query) formatted.query = {};
172
+ formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
173
+ }
174
+ }
175
+ }
176
+
177
+ // 将 array<object> 展开字段按索引 zip 合并为对象数组
178
+ for (const [arrayKey, subProps] of Object.entries(arrayObjectCollector)) {
179
+ if (!formatted.data) formatted.data = {};
180
+ const subKeys = Object.keys(subProps);
181
+ const maxLen = Math.max(...subKeys.map(k => subProps[k].length));
182
+ const result: Record<string, any>[] = [];
183
+ for (let i = 0; i < maxLen; i++) {
184
+ const obj: Record<string, any> = {};
185
+ for (const subKey of subKeys) {
186
+ const val = subProps[subKey][i];
187
+ if (val !== undefined) {
188
+ const itemProp = bodyProps[arrayKey]?.items?.properties?.[subKey];
189
+ if (itemProp?.type === 'number' && !isNaN(Number(val))) {
190
+ obj[subKey] = Number(val);
191
+ } else {
192
+ obj[subKey] = val;
193
+ }
194
+ }
195
+ }
196
+ result.push(obj);
197
+ }
198
+ formatted.data[arrayKey] = result;
199
+ }
200
+
201
+ // 将嵌套对象展开字段组装回嵌套结构
202
+ for (const [objectKey, subProps] of Object.entries(nestedObjectCollector)) {
203
+ if (!formatted.data) formatted.data = {};
204
+ formatted.data[objectKey] = subProps;
205
+ }
206
+ }
207
+
208
+ // 当没有传递 query 时,要判断当前 tool 是否支持 query
209
+ if (formatted.query === undefined) {
210
+ const paramDefs2 = getToolParamDefs(
211
+ formatted.module,
212
+ formatted.tool,
213
+ );
214
+ if (paramDefs2?.query) {
215
+ formatted.query = {};
216
+ }
217
+ }
218
+
219
+ return formatted;
220
+ }
@@ -0,0 +1,13 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const helpFileContent = fs.readFileSync(
5
+ path.join(__dirname, '../help.json'),
6
+ 'utf8',
7
+ );
8
+ if (!helpFileContent) {
9
+ console.error('help.json not found');
10
+ process.exit(1);
11
+ }
12
+
13
+ export const helpData = JSON.parse(helpFileContent);
@@ -0,0 +1,13 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const keyMappingFilePath = path.join(__dirname, '../key-mapping.json');
5
+
6
+ let data: Record<string, Record<string, Record<string, string>>> = {};
7
+
8
+ if (fs.existsSync(keyMappingFilePath)) {
9
+ const content = fs.readFileSync(keyMappingFilePath, 'utf8');
10
+ data = JSON.parse(content);
11
+ }
12
+
13
+ export const keyMappingData = data;
@@ -0,0 +1,130 @@
1
+ import fs from 'fs';
2
+
3
+ /**
4
+ * 尝试解析JSON字符串
5
+ * 先将真实控制字符转义为 JSON 合法形式,处理 shell/AI 传入的原始换行等情况
6
+ * @param str 要解析的字符串
7
+ * @returns 解析后的对象或原始字符串
8
+ */
9
+ export function tryParseJSON(str: string | boolean | undefined): any {
10
+ if (typeof str !== 'string') return str;
11
+
12
+ const escaped = str.replace(/[\x00-\x1F\x7F]/g, (ch) => {
13
+ const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t', '\b': '\\b', '\f': '\\f' };
14
+ return map[ch] || '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
15
+ });
16
+ try {
17
+ return JSON.parse(escaped);
18
+ } catch (error) {
19
+ return str;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 尝试从文件引用或 stdin 读取内容(类似 curl 的 @file / @- 语法)
25
+ */
26
+ export function tryReadFileRef(str: string | boolean | undefined) {
27
+ if (typeof str !== 'string' || !str.startsWith('@')) return str;
28
+
29
+ const ref = str.slice(1);
30
+
31
+ // @- 表示从 stdin 读取
32
+ if (ref === '-') {
33
+ try {
34
+ return fs.readFileSync(0, 'utf8').trim();
35
+ } catch (e) {
36
+ console.error('从 stdin 读取失败:', e.message);
37
+ return str;
38
+ }
39
+ }
40
+
41
+ // @/path/to/file 表示从文件读取
42
+ if (fs.existsSync(ref)) {
43
+ return fs.readFileSync(ref, 'utf8').trim();
44
+ }
45
+
46
+ return str;
47
+ }
48
+
49
+ /**
50
+ * 解析 commander 未识别的未知选项
51
+ * commander 开启 allowUnknownOption 后,未知选项会残留在 process.argv 中,
52
+ * 这里手动解析它们,行为与原 parseArguments 对未知 --key value 的处理一致。
53
+ */
54
+ export function parseUnknownOptions(
55
+ params: Record<string, string | boolean | undefined>,
56
+ ): void {
57
+ const knownLongOptions = new Set([
58
+ 'help', 'short', 'verbose', 'path', 'query', 'data',
59
+ ]);
60
+
61
+ // 获取 commander 解析后的剩余参数(未被识别的选项)
62
+ const rawArgs = process.argv.slice(2);
63
+ // 跳过已被 commander 消费的位置参数和已知选项,手动扫描未知选项
64
+ let i = 0;
65
+ while (i < rawArgs.length) {
66
+ const arg = rawArgs[i];
67
+
68
+ if (arg.startsWith('--')) {
69
+ const fullKey = arg.slice(2);
70
+
71
+ // 处理 --key=value 格式
72
+ if (fullKey.includes('=')) {
73
+ const [key, ...valueParts] = fullKey.split('=');
74
+ if (!knownLongOptions.has(key) && key !== 'module' && key !== 'tool') {
75
+ params[key] = valueParts.join('=');
76
+ }
77
+ i++;
78
+ continue;
79
+ }
80
+
81
+ const key = fullKey;
82
+
83
+ // 已知选项跳过(commander 已处理)
84
+ if (knownLongOptions.has(key)) {
85
+ // 如果是带值的已知选项(path/query/data),跳过值
86
+ if (['path', 'query', 'data'].includes(key) && i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
87
+ i += 2;
88
+ } else {
89
+ i++;
90
+ }
91
+ continue;
92
+ }
93
+
94
+ // 未知选项:检查下一个参数是否是值
95
+ const nextArg = rawArgs[i + 1];
96
+ const isNextArgValue =
97
+ i + 1 < rawArgs.length &&
98
+ !nextArg.startsWith('--') &&
99
+ !nextArg.startsWith('-');
100
+
101
+ if (isNextArgValue) {
102
+ params[key] = nextArg;
103
+ i += 2;
104
+ } else {
105
+ params[key] = true;
106
+ i++;
107
+ }
108
+ } else if (arg.startsWith('-') && arg.length > 1 && !/^-?\d+$/.test(arg)) {
109
+ // 短参数
110
+ const key = arg.slice(1);
111
+
112
+ const nextArg = rawArgs[i + 1];
113
+ const isNextArgValue =
114
+ i + 1 < rawArgs.length &&
115
+ !nextArg.startsWith('--') &&
116
+ !nextArg.startsWith('-');
117
+
118
+ if (isNextArgValue && key.length === 1) {
119
+ params[key] = nextArg;
120
+ i += 2;
121
+ } else {
122
+ params[key] = true;
123
+ i++;
124
+ }
125
+ } else {
126
+ // 位置参数,跳过(已被 commander 消费)
127
+ i++;
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 将数据转换为格式化的 JSON 字符串
3
+ * @param data 要序列化的数据(可以是任意类型)缩进)
4
+ * @param verbose 是否 verbose 模式
5
+ * @returns 格式化的 JSON 字符串,如果序列化失败则返回错误信息的 JSON
6
+ */
7
+ export function printJSON(
8
+ data: Record<string, any> | Array<any>,
9
+ verbose: boolean = false
10
+ ): string {
11
+ return verbose ? JSON.stringify(data, null, 2) : JSON.stringify(data);
12
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from 'commander';
2
+ import { executeAction } from './execute-action';
3
+
4
+ /**
5
+ * 顶层 fallback action:处理快捷命令等未被子命令匹配的情况
6
+ */
7
+ export function registerFallbackAction(program: Command): void {
8
+ program
9
+ .argument('[module]', '模块名称')
10
+ .argument('[tool]', '工具名称')
11
+ .action(async (moduleArg: string | undefined, toolArg: string | undefined, opts: Record<string, any>) => {
12
+ await executeAction(moduleArg, toolArg, opts, program);
13
+ });
14
+ }
@@ -0,0 +1,121 @@
1
+ import { Command, Option } from 'commander';
2
+ import { trimSummary } from './trim-summary';
3
+ import { helpData } from './help-data';
4
+ import { flatOptionsData } from './flat-options-data';
5
+ import { executeAction } from './execute-action';
6
+ import { resolveShortcut } from '../shortcuts';
7
+
8
+ interface FlatOption {
9
+ optKey: string;
10
+ valuePlaceholder: string;
11
+ description: string;
12
+ required: boolean;
13
+ isArray: boolean;
14
+ choices?: string[];
15
+ defaultValue?: any;
16
+ source: 'path' | 'query' | 'body';
17
+ isArrayObject?: boolean;
18
+ isNestedField?: boolean;
19
+ }
20
+
21
+ /**
22
+ * 为单个 tool 子命令注册 Commander options
23
+ * 直接读取预生成的 flat-options.json 中已扁平化的参数定义,无需运行时计算
24
+ */
25
+ function registerToolOptions(toolCmd: Command, moduleName: string, toolName: string): void {
26
+ const moduleOptions = flatOptionsData[moduleName];
27
+ if (!moduleOptions) return;
28
+ const toolOptions: Record<string, FlatOption> = moduleOptions[toolName];
29
+ if (!toolOptions) return;
30
+
31
+ for (const [, opt] of Object.entries(toolOptions) as [string, FlatOption][]) {
32
+ const option = new Option(
33
+ `--${opt.optKey} ${opt.valuePlaceholder}`.trim(),
34
+ opt.description,
35
+ );
36
+
37
+ if (opt.required && opt.source === 'path') {
38
+ option.makeOptionMandatory();
39
+ }
40
+
41
+ if (opt.choices) {
42
+ option.choices(opt.choices);
43
+ }
44
+
45
+ if (opt.defaultValue !== undefined) {
46
+ option.default(opt.defaultValue);
47
+ }
48
+
49
+ if (opt.required && opt.source === 'body' && !opt.isArrayObject && !opt.isNestedField) {
50
+ option.makeOptionMandatory();
51
+ }
52
+
53
+ if (opt.isArray || opt.isArrayObject) {
54
+ option.argParser((val: string, prev: string[] | undefined) => {
55
+ return (prev || []).concat(val);
56
+ });
57
+ option.default(undefined);
58
+ }
59
+
60
+ toolCmd.addOption(option);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 为每个模块注册子命令,使 cnb <module> -h 显示模块帮助
66
+ * 每个 tool 的参数通过 Commander 的 option 系统注册,--help 自动展示参数说明
67
+ */
68
+ export function registerModuleCommands(program: Command): void {
69
+ const modulesHelp = helpData.modulesHelp || {};
70
+
71
+ for (const [moduleName, tools] of Object.entries(modulesHelp) as [string, Record<string, any>][]) {
72
+ const toolEntries = Object.values(tools);
73
+ const sub = program
74
+ .command(moduleName)
75
+ .description(`${moduleName} 模块 (${toolEntries.length} tools)`)
76
+ .helpOption('-h, --help', '显示帮助文档');
77
+
78
+ // 为每个 tool 注册子命令
79
+ for (const [toolName, toolInfo] of Object.entries(tools) as [string, any][]) {
80
+ const summary = trimSummary(toolInfo.summary || '');
81
+ const toolCmd = sub
82
+ .command(toolName)
83
+ .description(summary)
84
+ .option('-v, --verbose', '输出完整原始响应')
85
+ .helpOption('-h, --help', '显示帮助文档')
86
+ .showHelpAfterError(true)
87
+ .action(async (opts: Record<string, any>) => {
88
+ await executeAction(moduleName, toolName, opts, sub);
89
+ });
90
+
91
+ // 添加权限信息到帮助文本
92
+ if (toolInfo.permission) {
93
+ toolCmd.addHelpText('before', `权限: ${toolInfo.permission}\n`);
94
+ }
95
+
96
+ // 根据 flat-options.json 注册参数选项
97
+ registerToolOptions(toolCmd, moduleName, toolName);
98
+ }
99
+
100
+ // 允许未匹配到子命令时也能正常执行(不报错)
101
+ sub.allowUnknownOption();
102
+ sub.allowExcessArguments();
103
+
104
+ // 模块级 action:处理未匹配到子命令的情况
105
+ // 如果输入了快捷命令名(如 cnb issues get),Commander 匹配不到真实 tool 子命令,
106
+ // 会走到这里,此时尝试 resolveShortcut 将其转换为真实 tool
107
+ sub
108
+ .argument('[tool]', '工具名称(支持快捷命令)')
109
+ .action(async (toolArg: string | undefined, opts: Record<string, any>) => {
110
+ if (toolArg) {
111
+ const shortcut = resolveShortcut(moduleName, toolArg);
112
+ if (shortcut) {
113
+ // 快捷命令匹配成功,走 executeAction 流程
114
+ await executeAction(moduleName, toolArg, opts, sub);
115
+ return;
116
+ }
117
+ }
118
+ sub.help();
119
+ });
120
+ }
121
+ }