@lingjingai/lj-awb-cli-pre 0.3.15

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 (49) hide show
  1. package/README.md +335 -0
  2. package/build/_shared.mjs +130 -0
  3. package/build/build.mjs +50 -0
  4. package/build/pre-publish.mjs +57 -0
  5. package/build/pre.mjs +42 -0
  6. package/build/prod.mjs +52 -0
  7. package/install.mjs +53 -0
  8. package/package.json +44 -0
  9. package/packages/awb-cli/README.md +19 -0
  10. package/packages/awb-cli/bin/lj-awb +19 -0
  11. package/packages/awb-cli/bin/lj-awb.js +11 -0
  12. package/packages/awb-cli/package.json +18 -0
  13. package/packages/awb-core/README.md +12 -0
  14. package/packages/awb-core/package.json +21 -0
  15. package/packages/awb-core/src/api.js +349 -0
  16. package/packages/awb-core/src/artifact.js +936 -0
  17. package/packages/awb-core/src/auth.js +80 -0
  18. package/packages/awb-core/src/commands.js +1321 -0
  19. package/packages/awb-core/src/common.js +508 -0
  20. package/packages/awb-core/src/output.js +1189 -0
  21. package/packages/awb-core/src/services.js +3811 -0
  22. package/packages/awb-core/src/standalone.js +1213 -0
  23. package/skills/lj-awb/SKILL.md +160 -0
  24. package/skills/lj-awb/VERSION +1 -0
  25. package/skills/lj-awb/compat.json +6 -0
  26. package/skills/lj-awb/modules/account.md +30 -0
  27. package/skills/lj-awb/modules/artifact/asset.md +64 -0
  28. package/skills/lj-awb/modules/artifact/clip.md +65 -0
  29. package/skills/lj-awb/modules/artifact/script.md +37 -0
  30. package/skills/lj-awb/modules/artifact/video.md +65 -0
  31. package/skills/lj-awb/modules/artifact.md +65 -0
  32. package/skills/lj-awb/modules/asset.md +53 -0
  33. package/skills/lj-awb/modules/auth.md +30 -0
  34. package/skills/lj-awb/modules/create-contract.md +118 -0
  35. package/skills/lj-awb/modules/credits.md +28 -0
  36. package/skills/lj-awb/modules/evals.md +186 -0
  37. package/skills/lj-awb/modules/image.md +75 -0
  38. package/skills/lj-awb/modules/model.md +110 -0
  39. package/skills/lj-awb/modules/project.md +30 -0
  40. package/skills/lj-awb/modules/subject.md +32 -0
  41. package/skills/lj-awb/modules/task-manual.md +185 -0
  42. package/skills/lj-awb/modules/task.md +62 -0
  43. package/skills/lj-awb/modules/upload.md +33 -0
  44. package/skills/lj-awb/modules/video.md +102 -0
  45. package/skills/lj-awb/modules/workflows.md +482 -0
  46. package/skills/lj-awb/references/error-codes.md +102 -0
  47. package/skills/lj-awb/references/model-options-read.md +49 -0
  48. package/skills/lj-awb/references/output-fields.md +113 -0
  49. package/skills/lj-awb/scripts/resolve-lj-awb-cmd.sh +10 -0
@@ -0,0 +1,1213 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { LingjingAwbCliError, createEnvelope, toBool, toInt } from './common.js';
5
+ import { registerAwbCommands } from './commands.js';
6
+ import { formatTextError, formatTextOutput, normalizeJsonData } from './output.js';
7
+
8
+ function normalizeKey(value) {
9
+ return String(value || '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
10
+ }
11
+
12
+ function formatOptionName(value) {
13
+ return `--${String(value || '').replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`;
14
+ }
15
+
16
+ const OPTION_SYNONYMS = {
17
+ timeout: ['waitSeconds'],
18
+ deadline: ['waitSeconds'],
19
+ maxWait: ['waitSeconds'],
20
+ wait: ['waitSeconds'],
21
+ parallel: ['concurrency'],
22
+ workers: ['concurrency'],
23
+ jobs: ['concurrency'],
24
+ threads: ['concurrency'],
25
+ output: ['format'],
26
+ json: ['format'],
27
+ input: ['inputFile'],
28
+ in: ['inputFile'],
29
+ dry: ['dryRun'],
30
+ force: ['yes'],
31
+ };
32
+
33
+ function levenshtein(a, b) {
34
+ if (a === b) return 0;
35
+ const m = a.length;
36
+ const n = b.length;
37
+ if (!m) return n;
38
+ if (!n) return m;
39
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
40
+ let curr = new Array(n + 1);
41
+ for (let i = 1; i <= m; i++) {
42
+ curr[0] = i;
43
+ for (let j = 1; j <= n; j++) {
44
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
45
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
46
+ }
47
+ [prev, curr] = [curr, prev];
48
+ }
49
+ return prev[n];
50
+ }
51
+
52
+ function suggestSimilarOptions(unknownKey, allowedKeys) {
53
+ const scored = new Map();
54
+ const record = (key, score) => {
55
+ if (!scored.has(key) || scored.get(key) > score) scored.set(key, score);
56
+ };
57
+ const synonyms = OPTION_SYNONYMS[unknownKey] || [];
58
+ for (const syn of synonyms) {
59
+ if (allowedKeys.includes(syn)) record(syn, -1);
60
+ }
61
+ for (const allowed of allowedKeys) {
62
+ const a = unknownKey.toLowerCase();
63
+ const b = allowed.toLowerCase();
64
+ if (b.includes(a) || a.includes(b)) {
65
+ record(allowed, 0);
66
+ continue;
67
+ }
68
+ const d = levenshtein(a, b);
69
+ const threshold = Math.max(2, Math.ceil(Math.min(a.length, b.length) / 3));
70
+ if (d <= threshold) record(allowed, d);
71
+ }
72
+ return [...scored.entries()]
73
+ .sort((x, y) => x[1] - y[1])
74
+ .slice(0, 3)
75
+ .map(([key]) => formatOptionName(key));
76
+ }
77
+
78
+ function commandPrefix() {
79
+ return process.env.LINGJING_AWB_COMMAND_PREFIX || 'lj-awb';
80
+ }
81
+
82
+ function renderCliText(value) {
83
+ return String(value ?? '').replace(/\blj-awb\b/g, commandPrefix());
84
+ }
85
+
86
+ function shellArg(value) {
87
+ const text = String(value ?? '');
88
+ return /^[A-Za-z0-9_./:=@,+-]+$/.test(text) ? text : JSON.stringify(text);
89
+ }
90
+
91
+ function appendCommandOption(parts, key, value) {
92
+ if (value === undefined || value === null || value === '') return;
93
+ if (Array.isArray(value)) {
94
+ for (const item of value) appendCommandOption(parts, key, item);
95
+ return;
96
+ }
97
+ const flag = formatOptionName(key);
98
+ if (value === true) {
99
+ parts.push(flag);
100
+ return;
101
+ }
102
+ parts.push(flag, shellArg(value));
103
+ }
104
+
105
+ function commandInvocation(commandName, kwargs = {}, options = {}) {
106
+ const omit = new Set(options.omit || []);
107
+ const sensitive = new Set(['accessKey']);
108
+ const next = { ...kwargs, ...(options.set || {}) };
109
+ const parts = [commandPrefix(), ...String(commandName || '').split(' ').filter(Boolean)];
110
+ for (const [key, value] of Object.entries(next)) {
111
+ if (omit.has(key) || sensitive.has(key)) continue;
112
+ appendCommandOption(parts, key, value);
113
+ }
114
+ return parts.join(' ');
115
+ }
116
+
117
+ function buildOutputContext(command, kwargs = {}) {
118
+ const optionKeys = new Set((command.args || []).map((arg) => normalizeKey(arg.name)));
119
+ const canSuggestDryRunExecution = command.name !== 'auth login';
120
+ const withoutDryRun = { omit: ['dryRun'] };
121
+ const isModelList = command.name === 'model image-models' || command.name === 'model video-models';
122
+ const modelListAll = isModelList && toBool(kwargs.all);
123
+ const modelListLimit = isModelList
124
+ ? (modelListAll ? Number.POSITIVE_INFINITY : Math.max(1, toInt(kwargs.limit, 8)))
125
+ : undefined;
126
+ const modelListMoreCommand = isModelList && !modelListAll
127
+ ? commandInvocation(command.name, kwargs, { omit: ['limit'], set: { all: true } })
128
+ : null;
129
+ return {
130
+ jsonCommand: `${commandInvocation(command.name, kwargs)} -f json`,
131
+ executeCommand: canSuggestDryRunExecution && toBool(kwargs.dryRun) ? commandInvocation(command.name, kwargs, withoutDryRun) : null,
132
+ confirmCommand: canSuggestDryRunExecution && toBool(kwargs.dryRun) && optionKeys.has('yes')
133
+ ? commandInvocation(command.name, kwargs, { omit: ['dryRun'], set: { yes: true } })
134
+ : null,
135
+ ...(isModelList ? { listLimit: modelListLimit, moreCommand: modelListMoreCommand } : {}),
136
+ };
137
+ }
138
+
139
+ function assignKwarg(kwargs, key, value) {
140
+ if (kwargs[key] === undefined) {
141
+ kwargs[key] = value;
142
+ return;
143
+ }
144
+ if (Array.isArray(kwargs[key])) {
145
+ kwargs[key].push(value);
146
+ return;
147
+ }
148
+ kwargs[key] = [kwargs[key], value];
149
+ }
150
+
151
+ function parseArgv(argv) {
152
+ const commandParts = [];
153
+ const kwargs = {};
154
+ let format = 'text';
155
+
156
+ for (let index = 0; index < argv.length; index += 1) {
157
+ const token = argv[index];
158
+ if (token === '-f' || token === '--format') {
159
+ format = argv[index + 1] || 'json';
160
+ index += 1;
161
+ continue;
162
+ }
163
+ if (token === '--json') {
164
+ format = 'json';
165
+ continue;
166
+ }
167
+ if (token.startsWith('--')) {
168
+ const raw = token.slice(2);
169
+ const eqIndex = raw.indexOf('=');
170
+ const keyPart = eqIndex >= 0 ? raw.slice(0, eqIndex) : raw;
171
+ const inlineValue = eqIndex >= 0 ? raw.slice(eqIndex + 1) : undefined;
172
+ const key = normalizeKey(keyPart);
173
+ if (inlineValue !== undefined) {
174
+ assignKwarg(kwargs, key, inlineValue);
175
+ } else if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
176
+ assignKwarg(kwargs, key, argv[index + 1]);
177
+ index += 1;
178
+ } else {
179
+ assignKwarg(kwargs, key, true);
180
+ }
181
+ continue;
182
+ }
183
+ commandParts.push(token);
184
+ }
185
+
186
+ return { commandName: commandParts.join(' '), kwargs, format };
187
+ }
188
+
189
+ async function readVersion() {
190
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
191
+ const packagePath = path.join(currentDir, '..', 'package.json');
192
+ const pkg = JSON.parse(await fs.readFile(packagePath, 'utf8'));
193
+ return pkg.version || 'unknown';
194
+ }
195
+
196
+ const VIRTUAL_COMMANDS = [
197
+ {
198
+ name: 'schema',
199
+ virtual: 'schema',
200
+ description: [
201
+ '输出机器可读命令 schema,供 Agent / 脚本理解命令、参数和示例',
202
+ '',
203
+ 'Examples:',
204
+ ' lj-awb schema -f json',
205
+ ' lj-awb schema --domain video -f json',
206
+ ' lj-awb schema --domain image --command create -f json',
207
+ '',
208
+ 'Hint: Agent 应优先读取 schema,而不是解析自然语言 help。',
209
+ ].join('\n'),
210
+ args: [
211
+ { name: 'domain', valueName: 'name', description: '按命令域过滤,如 image / video / task / system' },
212
+ { name: 'command', valueName: 'name', description: '按子命令过滤,如 create / wait / doctor' },
213
+ ],
214
+ },
215
+ ];
216
+
217
+ const RENAMED_COMMAND_HINTS = {
218
+ 'workspace me': 'account info',
219
+ 'workspace teams': 'account teams',
220
+ 'workspace team-select': 'account switch-team',
221
+ 'workspace project-groups': 'project list',
222
+ 'workspace project-group-current': 'project current',
223
+ 'workspace project-group-select': 'project use',
224
+ 'workspace project-group-users': 'project users',
225
+ 'workspace project-group-create': 'project create',
226
+ 'workspace project-group-update': 'project update',
227
+ 'workspace project-group-ensure': 'project ensure',
228
+ 'billing points': 'credits balance',
229
+ 'billing usage-summary': 'credits usage',
230
+ };
231
+
232
+ const MODEL_LIST_GUESS_HINT = `按任务类型分开查询:生图候选用 ${'`'}lj-awb model image-models${'`'},生视频候选用 ${'`'}lj-awb model video-models${'`'}。运行 lj-awb model -h 查看 model 域全部命令。`;
233
+ const COMMAND_GUESS_HINTS = {
234
+ 'model list': MODEL_LIST_GUESS_HINT,
235
+ 'model models': MODEL_LIST_GUESS_HINT,
236
+ 'model ls': MODEL_LIST_GUESS_HINT,
237
+ 'model image': MODEL_LIST_GUESS_HINT,
238
+ 'model video': MODEL_LIST_GUESS_HINT,
239
+ 'models': MODEL_LIST_GUESS_HINT,
240
+ };
241
+
242
+ function buildRegistry() {
243
+ const commands = [];
244
+ registerAwbCommands((spec) => commands.push(spec));
245
+ return [...commands, ...VIRTUAL_COMMANDS];
246
+ }
247
+
248
+ function commandGroup(command) {
249
+ const parts = String(command.name || '').split(' ').filter(Boolean);
250
+ return parts.length === 1 ? 'system' : parts[0];
251
+ }
252
+
253
+ function commandSubcommand(command) {
254
+ const parts = String(command.name || '').split(' ').filter(Boolean);
255
+ return parts.length === 1 ? parts[0] : parts.slice(1).join(' ');
256
+ }
257
+
258
+ function isCommandGroupName(commandName, commands) {
259
+ return Boolean(commandName && commands.some((command) => commandGroup(command) === commandName));
260
+ }
261
+
262
+ function commandSubgroupTokens(command) {
263
+ const parts = String(command.name || '').split(' ').filter(Boolean);
264
+ if (parts.length < 3) return null;
265
+ return [parts[0], parts[1]];
266
+ }
267
+
268
+ function isCommandSubgroupName(commandName, commands) {
269
+ const parts = String(commandName || '').split(' ').filter(Boolean);
270
+ if (parts.length !== 2) return false;
271
+ const [group, subgroup] = parts;
272
+ if (!SUBGROUP_DESCRIPTIONS[group]?.[subgroup]) return false;
273
+ return commands.some((command) => {
274
+ const tokens = commandSubgroupTokens(command);
275
+ return tokens && tokens[0] === group && tokens[1] === subgroup;
276
+ });
277
+ }
278
+
279
+ function resolveCommandName(commandName, commands) {
280
+ const exact = commands.find((command) => command.name === commandName);
281
+ if (exact) return commandName;
282
+ const parts = String(commandName || '').split(' ').filter(Boolean);
283
+ if (parts[0] === 'system' && parts.length > 1) {
284
+ const rootCommandName = parts.slice(1).join(' ');
285
+ const rootCommand = commands.find((command) => command.name === rootCommandName && commandGroup(command) === 'system');
286
+ if (rootCommand) return rootCommand.name;
287
+ }
288
+ return commandName;
289
+ }
290
+
291
+ const GROUP_DESCRIPTIONS = {
292
+ system: '系统:环境体检和机器可读命令 schema',
293
+ auth: '认证:登录、查看和清空本地 access key',
294
+ account: '账号:查看账号和团队',
295
+ project: '项目:查看、切换、创建和更新项目组',
296
+ credits: '积分:查询可扣积分余额、项目组预算和用量统计',
297
+ model: '模型:查询生图 / 生视频模型、参数约束和创建用法',
298
+ upload: '上传:把本地文件上传为平台可访问素材',
299
+ image: '图片:估价、提交、批量提交和查询生图任务',
300
+ video: '视频:估价、提交、批量提交、查询和去字幕',
301
+ task: '任务:任务列表、等待和本地任务台账',
302
+ asset: '资产:素材库匹配、素材组和素材注册',
303
+ artifact: '最终产物:剧本、资产、视频、剪辑 CRUD 与本地 JSON 导入',
304
+ subject: '主体:发布和查询可复用主体资产',
305
+ };
306
+
307
+ const SUBGROUP_DESCRIPTIONS = {
308
+ artifact: {
309
+ script: '剧本:document / asset / state / speaker / episode / scene / action 行 CRUD 与 script.json 批量导入',
310
+ asset: '资产:角色 / 道具 / 场景 及其状态 CRUD,actors/props/locations JSON 批量导入',
311
+ video: '视频:集 / 场 / clip 三级最终产物 CRUD 与 storyboard 批量导入',
312
+ clip: '剪辑:按集组织的剪辑宽表 CRUD 与状态更新',
313
+ },
314
+ };
315
+
316
+ const SUBGROUP_EXAMPLES = {
317
+ 'artifact script': [
318
+ 'lj-awb artifact script import --project-id <projectId> --input-file 1_script/output/script.json --dry-run',
319
+ 'lj-awb artifact script get --project-id <projectId>',
320
+ 'lj-awb artifact script rows --project-id <projectId> --row-kind scene',
321
+ ],
322
+ 'artifact asset': [
323
+ 'lj-awb artifact asset import --project-id <projectId> --input-dir 2_asset/output --dry-run',
324
+ 'lj-awb artifact asset actors --project-id <projectId> --include-states true',
325
+ 'lj-awb artifact asset props --project-id <projectId>',
326
+ ],
327
+ 'artifact video': [
328
+ 'lj-awb artifact video import-storyboard --project-id <projectId> --input-file 3_footage/output/ep001/ep001_storyboard.json --dry-run',
329
+ 'lj-awb artifact video episodes --project-id <projectId>',
330
+ 'lj-awb artifact video clips --project-id <projectId> --episode-id ep_001 --scene-id scn_001',
331
+ ],
332
+ 'artifact clip': [
333
+ 'lj-awb artifact clip upsert-episode --project-id <projectId> --input-file 4_clip/output/ep001_clip_output.json --dry-run',
334
+ 'lj-awb artifact clip episodes --project-id <projectId>',
335
+ 'lj-awb artifact clip episode-by-id --project-id <projectId> --episode-id ep_001',
336
+ ],
337
+ };
338
+
339
+ const GROUP_EXAMPLES = {
340
+ system: [
341
+ 'lj-awb doctor',
342
+ 'lj-awb doctor --verify',
343
+ 'lj-awb schema -f json',
344
+ 'lj-awb schema --domain video -f json',
345
+ ],
346
+ auth: [
347
+ 'lj-awb auth status',
348
+ 'lj-awb auth verify',
349
+ 'lj-awb auth login --access-key <access-key>',
350
+ 'LINGJING_AWB_ACCESS_KEY=<access-key> lj-awb auth login',
351
+ 'lj-awb auth clear --dry-run',
352
+ ],
353
+ account: [
354
+ 'lj-awb account info',
355
+ 'lj-awb account teams',
356
+ 'lj-awb account switch-team --group-id <groupId> --yes',
357
+ ],
358
+ project: [
359
+ 'lj-awb project list',
360
+ 'lj-awb project current',
361
+ 'lj-awb project use --project-group-no <no> --yes',
362
+ 'lj-awb project create --name "CLI 测试项目" --point 1000 --yes',
363
+ ],
364
+ credits: [
365
+ 'lj-awb credits balance',
366
+ 'lj-awb credits usage --project-group-no <no> --last-hours 24',
367
+ ],
368
+ model: [
369
+ 'lj-awb model image-models --model Banana',
370
+ 'lj-awb model video-models --model Seedance',
371
+ 'lj-awb model input-guide',
372
+ 'lj-awb model options --model-group-code <code>',
373
+ 'lj-awb model create-spec --model-group-code <code>',
374
+ ],
375
+ upload: [
376
+ 'lj-awb upload files --files ./a.png,./b.mp4 --dry-run',
377
+ ],
378
+ image: [
379
+ 'lj-awb image fee --model-group-code <code> --prompt "一只小狗"',
380
+ 'lj-awb image create --model-group-code <code> --prompt "一只小狗" --dry-run',
381
+ 'lj-awb image status --task-id <id>',
382
+ ],
383
+ video: [
384
+ 'lj-awb video fee --model-group-code <code> --prompt "雨夜奔跑" --duration 5',
385
+ 'lj-awb video create --model-group-code <code> --prompt "镜头推进" --resource image:first_frame=./actor.png --dry-run',
386
+ 'lj-awb video create --model-group-code <code> --prompt "镜头推进" --resource image:first_frame=asset:<assetId> --dry-run',
387
+ 'lj-awb video subtitle-status --remote-task-id <id>',
388
+ ],
389
+ task: [
390
+ 'lj-awb task list --task-type IMAGE_CREATE --project-group-no <no>',
391
+ 'lj-awb task wait --task-id <id> --task-type IMAGE_CREATE --wait-seconds 180',
392
+ 'lj-awb task records --task-record-file .awb/tasks.jsonl',
393
+ ],
394
+ asset: [
395
+ 'lj-awb asset match-actor --description "十八岁少女,古风" --tags-json \'{"gender":"女"}\'',
396
+ 'lj-awb asset group-create --name "女主素材组" --dry-run',
397
+ ],
398
+ artifact: [
399
+ 'lj-awb artifact script import --project-id <projectId> --input-file 1_script/output/script.json --dry-run',
400
+ 'lj-awb artifact asset import --project-id <projectId> --input-dir 2_asset/output --dry-run',
401
+ 'lj-awb artifact video import-storyboard --project-id <projectId> --input-file 3_footage/output/ep001/ep001_storyboard.json --dry-run',
402
+ 'lj-awb artifact clip upsert-episode --project-id <projectId> --input-file 4_clip/output/ep001_clip_output.json --dry-run',
403
+ ],
404
+ subject: [
405
+ 'lj-awb subject list --name "女主"',
406
+ 'lj-awb subject publish --name 女主 --resource primary:./three-view.png --dry-run',
407
+ ],
408
+ };
409
+
410
+ const COMMAND_REQUIRED_OPTIONS = {
411
+ 'account switch-team': ['groupId'],
412
+ 'project create': ['name'],
413
+ 'project ensure': ['name'],
414
+ 'model options': ['modelGroupCode'],
415
+ 'model create-spec': ['modelGroupCode'],
416
+ 'image fee': ['modelGroupCode', 'prompt'],
417
+ 'image create': ['modelGroupCode', 'prompt'],
418
+ 'image create-batch': ['inputFile', 'modelGroupCode'],
419
+ 'image status': ['taskId'],
420
+ 'video fee': ['modelGroupCode'],
421
+ 'video create': ['modelGroupCode'],
422
+ 'video create-batch': ['inputFile', 'modelGroupCode'],
423
+ 'video status': ['taskId'],
424
+ 'video subtitle-remove': ['videoUrl'],
425
+ 'task wait': ['taskId', 'taskType'],
426
+ 'asset match-actor': ['description'],
427
+ 'asset group': ['groupId'],
428
+ 'asset group-create': ['name'],
429
+ 'asset group-update': ['groupId'],
430
+ 'asset register': ['groupId', 'name'],
431
+ 'artifact script row': ['rowKind', 'entityKey'],
432
+ 'artifact script children': ['parentKey'],
433
+ 'artifact script delete-row': ['rowKind', 'entityKey'],
434
+ 'artifact asset actor': ['actorKey'],
435
+ 'artifact asset prop': ['propKey'],
436
+ 'artifact asset location': ['locationKey'],
437
+ 'artifact asset upsert-actor-state': ['actorKey'],
438
+ 'artifact asset upsert-prop-state': ['propKey'],
439
+ 'artifact asset upsert-location-state': ['locationKey'],
440
+ 'artifact asset delete-actor': ['actorKey'],
441
+ 'artifact asset delete-prop': ['propKey'],
442
+ 'artifact asset delete-location': ['locationKey'],
443
+ 'artifact asset delete-actor-state': ['actorKey', 'stateKey'],
444
+ 'artifact asset delete-prop-state': ['propKey', 'stateKey'],
445
+ 'artifact asset delete-location-state': ['locationKey', 'stateKey'],
446
+ 'artifact video episode': ['episodeId'],
447
+ 'artifact video scenes': ['episodeId'],
448
+ 'artifact video scene': ['episodeId', 'sceneId'],
449
+ 'artifact video clips': ['episodeId', 'sceneId'],
450
+ 'artifact video clip': ['episodeId', 'sceneId', 'clipId'],
451
+ 'artifact video upsert-scene': ['episodeId'],
452
+ 'artifact video upsert-clip': ['episodeId', 'sceneId'],
453
+ 'artifact video update-clip-urls': ['episodeId', 'sceneId', 'clipId', 'videoUrlsJson'],
454
+ 'artifact video delete-episode': ['episodeId'],
455
+ 'artifact video delete-scene': ['episodeId', 'sceneId'],
456
+ 'artifact video delete-clip': ['episodeId', 'sceneId', 'clipId'],
457
+ 'artifact video import-storyboard': ['inputFile'],
458
+ 'artifact clip episode': ['videoEpisodeId'],
459
+ 'artifact clip episode-by-id': ['episodeId'],
460
+ 'artifact clip update-status': ['videoEpisodeId', 'status'],
461
+ 'artifact clip delete-episode': ['videoEpisodeId'],
462
+ 'subject publish': ['name'],
463
+ 'subject wait': ['elementId'],
464
+ 'subject publish-batch': ['inputFile'],
465
+ };
466
+
467
+ const COMMAND_REQUIRED_ANY_OPTIONS = {
468
+ 'project update': [['name', 'point']],
469
+ 'upload files': [['file', 'files', 'filesJson']],
470
+ 'video fee': [['prompt', 'resource', 'resourcesJson']],
471
+ 'video create': [['prompt', 'resource', 'resourcesJson']],
472
+ 'asset group-update': [['name', 'description', 'projectName']],
473
+ 'asset register': [['file', 'url', 'backendPath']],
474
+ 'artifact script upsert-row': [['bodyJson', 'inputFile']],
475
+ 'artifact asset upsert-actor': [['bodyJson', 'inputFile']],
476
+ 'artifact asset upsert-prop': [['bodyJson', 'inputFile']],
477
+ 'artifact asset upsert-location': [['bodyJson', 'inputFile']],
478
+ 'artifact asset upsert-actor-state': [['bodyJson', 'inputFile']],
479
+ 'artifact asset upsert-prop-state': [['bodyJson', 'inputFile']],
480
+ 'artifact asset upsert-location-state': [['bodyJson', 'inputFile']],
481
+ 'artifact video upsert-episode': [['bodyJson', 'inputFile']],
482
+ 'artifact video upsert-scene': [['bodyJson', 'inputFile']],
483
+ 'artifact video upsert-clip': [['bodyJson', 'inputFile']],
484
+ 'artifact clip upsert-episode': [['bodyJson', 'inputFile']],
485
+ 'artifact clip upsert-batch': [['bodyJson', 'inputFile']],
486
+ 'subject publish': [['resource']],
487
+ };
488
+
489
+ const ARTIFACT_WRITE_COMMANDS = [
490
+ 'artifact script upsert-row',
491
+ 'artifact script delete-row',
492
+ 'artifact script import',
493
+ 'artifact asset upsert-actor',
494
+ 'artifact asset upsert-prop',
495
+ 'artifact asset upsert-location',
496
+ 'artifact asset upsert-actor-state',
497
+ 'artifact asset upsert-prop-state',
498
+ 'artifact asset upsert-location-state',
499
+ 'artifact asset delete-actor',
500
+ 'artifact asset delete-prop',
501
+ 'artifact asset delete-location',
502
+ 'artifact asset delete-actor-state',
503
+ 'artifact asset delete-prop-state',
504
+ 'artifact asset delete-location-state',
505
+ 'artifact asset import',
506
+ 'artifact video upsert-episode',
507
+ 'artifact video upsert-scene',
508
+ 'artifact video upsert-clip',
509
+ 'artifact video update-clip-urls',
510
+ 'artifact video delete-episode',
511
+ 'artifact video delete-scene',
512
+ 'artifact video delete-clip',
513
+ 'artifact video import-storyboard',
514
+ 'artifact clip upsert-episode',
515
+ 'artifact clip upsert-batch',
516
+ 'artifact clip update-status',
517
+ 'artifact clip delete-episode',
518
+ ];
519
+
520
+ const ARTIFACT_DELETE_COMMANDS = ARTIFACT_WRITE_COMMANDS.filter((name) => name.includes(' delete-'));
521
+
522
+ const CONFIRMATION_COMMANDS = new Set([
523
+ 'auth clear',
524
+ 'account switch-team',
525
+ 'project use',
526
+ 'project create',
527
+ 'project update',
528
+ 'project ensure',
529
+ 'image create',
530
+ 'image create-batch',
531
+ 'video create',
532
+ 'video create-batch',
533
+ 'video subtitle-remove',
534
+ 'asset group-create',
535
+ 'asset group-update',
536
+ 'asset register',
537
+ ...ARTIFACT_WRITE_COMMANDS,
538
+ 'subject publish',
539
+ 'subject publish-batch',
540
+ ]);
541
+
542
+ const COST_COMMANDS = new Set([
543
+ 'image create',
544
+ 'image create-batch',
545
+ 'video create',
546
+ 'video create-batch',
547
+ ]);
548
+
549
+ const REMOTE_WRITE_COMMANDS = new Set([
550
+ 'account switch-team',
551
+ 'project use',
552
+ 'project create',
553
+ 'project update',
554
+ 'project ensure',
555
+ 'upload files',
556
+ 'image create',
557
+ 'image create-batch',
558
+ 'video create',
559
+ 'video create-batch',
560
+ 'video subtitle-remove',
561
+ 'asset group-create',
562
+ 'asset group-update',
563
+ 'asset register',
564
+ ...ARTIFACT_WRITE_COMMANDS,
565
+ 'subject publish',
566
+ 'subject publish-batch',
567
+ ]);
568
+
569
+ const LOCAL_STATE_WRITE_COMMANDS = new Set([
570
+ 'auth login',
571
+ 'auth clear',
572
+ 'project use',
573
+ 'project create',
574
+ 'project ensure',
575
+ ]);
576
+
577
+ const DESTRUCTIVE_COMMANDS = new Set(['auth clear', ...ARTIFACT_DELETE_COMMANDS]);
578
+ const LONG_RUNNING_COMMANDS = new Set(['task wait', 'task record-poll', 'subject wait']);
579
+ const NETWORK_NONE_COMMANDS = new Set(['schema', 'auth status', 'auth clear', 'model input-guide', 'task records']);
580
+ const NETWORK_CONDITIONAL_COMMANDS = new Set(['doctor', 'auth login']);
581
+
582
+ const OUTPUT_KIND_BY_COMMAND = {
583
+ doctor: 'environment_report',
584
+ schema: 'command_schema',
585
+ 'auth status': 'auth_status',
586
+ 'auth verify': 'auth_status',
587
+ 'auth login': 'auth_result',
588
+ 'account info': 'account_summary',
589
+ 'account teams': 'team_list',
590
+ 'project list': 'project_list',
591
+ 'project current': 'project_summary',
592
+ 'project users': 'project_user_list',
593
+ 'credits balance': 'credits_balance',
594
+ 'credits usage': 'credits_usage',
595
+ 'model image-models': 'model_list',
596
+ 'model video-models': 'model_list',
597
+ 'model options': 'model_options',
598
+ 'model create-spec': 'model_create_spec',
599
+ 'model input-guide': 'model_input_guide',
600
+ 'upload files': 'upload_result',
601
+ 'image fee': 'fee_estimate',
602
+ 'image create': 'task_submission',
603
+ 'image create-batch': 'batch_task_submission',
604
+ 'image status': 'task_status',
605
+ 'video fee': 'fee_estimate',
606
+ 'video create': 'task_submission',
607
+ 'video create-batch': 'batch_task_submission',
608
+ 'video status': 'task_status',
609
+ 'video subtitle-remove': 'subtitle_task_submission',
610
+ 'video subtitle-status': 'subtitle_task_status',
611
+ 'task list': 'task_list',
612
+ 'task wait': 'task_status',
613
+ 'task records': 'local_task_records',
614
+ 'task record-poll': 'batch_task_status',
615
+ 'asset match-actor': 'asset_match_list',
616
+ 'asset groups': 'asset_group_list',
617
+ 'asset group': 'asset_group_detail',
618
+ 'asset group-create': 'asset_group_write_result',
619
+ 'asset group-update': 'asset_group_write_result',
620
+ 'asset register': 'asset_register_result',
621
+ 'subject list': 'subject_list',
622
+ 'subject publish': 'subject_publish_result',
623
+ 'subject wait': 'subject_status',
624
+ 'subject publish-batch': 'batch_subject_publish_result',
625
+ 'artifact script get': 'artifact_full',
626
+ 'artifact script document': 'artifact_record',
627
+ 'artifact script rows': 'artifact_list',
628
+ 'artifact script row': 'artifact_record',
629
+ 'artifact script children': 'artifact_list',
630
+ 'artifact script upsert-row': 'artifact_write_result',
631
+ 'artifact script delete-row': 'artifact_delete_result',
632
+ 'artifact script import': 'artifact_import_result',
633
+ 'artifact asset get': 'artifact_full',
634
+ 'artifact asset actors': 'artifact_list',
635
+ 'artifact asset actor': 'artifact_record',
636
+ 'artifact asset props': 'artifact_list',
637
+ 'artifact asset prop': 'artifact_record',
638
+ 'artifact asset locations': 'artifact_list',
639
+ 'artifact asset location': 'artifact_record',
640
+ 'artifact asset upsert-actor': 'artifact_write_result',
641
+ 'artifact asset upsert-prop': 'artifact_write_result',
642
+ 'artifact asset upsert-location': 'artifact_write_result',
643
+ 'artifact asset upsert-actor-state': 'artifact_write_result',
644
+ 'artifact asset upsert-prop-state': 'artifact_write_result',
645
+ 'artifact asset upsert-location-state': 'artifact_write_result',
646
+ 'artifact asset delete-actor': 'artifact_delete_result',
647
+ 'artifact asset delete-prop': 'artifact_delete_result',
648
+ 'artifact asset delete-location': 'artifact_delete_result',
649
+ 'artifact asset delete-actor-state': 'artifact_delete_result',
650
+ 'artifact asset delete-prop-state': 'artifact_delete_result',
651
+ 'artifact asset delete-location-state': 'artifact_delete_result',
652
+ 'artifact asset import': 'artifact_import_result',
653
+ 'artifact video get': 'artifact_full',
654
+ 'artifact video episodes': 'artifact_list',
655
+ 'artifact video episode': 'artifact_record',
656
+ 'artifact video scenes': 'artifact_list',
657
+ 'artifact video scene': 'artifact_record',
658
+ 'artifact video clips': 'artifact_list',
659
+ 'artifact video clip': 'artifact_record',
660
+ 'artifact video upsert-episode': 'artifact_write_result',
661
+ 'artifact video upsert-scene': 'artifact_write_result',
662
+ 'artifact video upsert-clip': 'artifact_write_result',
663
+ 'artifact video update-clip-urls': 'artifact_write_result',
664
+ 'artifact video delete-episode': 'artifact_delete_result',
665
+ 'artifact video delete-scene': 'artifact_delete_result',
666
+ 'artifact video delete-clip': 'artifact_delete_result',
667
+ 'artifact video import-storyboard': 'artifact_import_result',
668
+ 'artifact clip get': 'artifact_full',
669
+ 'artifact clip episodes': 'artifact_list',
670
+ 'artifact clip episode': 'artifact_record',
671
+ 'artifact clip episode-by-id': 'artifact_record',
672
+ 'artifact clip upsert-episode': 'artifact_write_result',
673
+ 'artifact clip upsert-batch': 'artifact_import_result',
674
+ 'artifact clip update-status': 'artifact_status_update',
675
+ 'artifact clip delete-episode': 'artifact_delete_result',
676
+ };
677
+
678
+ const PREFLIGHTS_BY_COMMAND = {
679
+ 'image create': ['doctor --verify', 'model image-models', 'model options', 'model create-spec', 'confirm missing key params', 'image fee', 'image create --dry-run'],
680
+ 'image create-batch': ['doctor --verify', 'model image-models', 'model options', 'model create-spec', 'prepare JSONL input', 'image create-batch --dry-run'],
681
+ 'video create': ['doctor --verify', 'model video-models', 'model options', 'model create-spec', 'confirm missing key params', 'video fee', 'video create --dry-run'],
682
+ 'video create-batch': ['doctor --verify', 'model video-models', 'model options', 'model create-spec', 'prepare JSONL input', 'video create-batch --dry-run'],
683
+ 'subject publish': ['doctor --verify', 'subject publish --dry-run'],
684
+ 'subject publish-batch': ['doctor --verify', 'prepare JSONL input', 'subject publish-batch --dry-run'],
685
+ 'video subtitle-remove': ['doctor --verify', 'video subtitle-remove --dry-run'],
686
+ 'asset group-create': ['asset group-create --dry-run'],
687
+ 'asset group-update': ['asset group-update --dry-run'],
688
+ 'asset register': ['asset register --dry-run'],
689
+ 'upload files': ['upload files --dry-run'],
690
+ };
691
+
692
+ function summaryOf(command) {
693
+ return String(command.description || '').split('\n')[0];
694
+ }
695
+
696
+ function extractExamples(command) {
697
+ const lines = String(command.description || '').split('\n');
698
+ const examples = [];
699
+ let inExamples = false;
700
+ for (const line of lines) {
701
+ const trimmed = line.trim();
702
+ if (trimmed === 'Examples:') {
703
+ inExamples = true;
704
+ continue;
705
+ }
706
+ if (!inExamples) continue;
707
+ if (!trimmed) break;
708
+ if (/^[A-Za-z][A-Za-z ]+:$/.test(trimmed)) break;
709
+ examples.push(trimmed);
710
+ }
711
+ return examples;
712
+ }
713
+
714
+ function asJsonExample(example) {
715
+ const text = renderCliText(example);
716
+ if (/(^|\s)(-f\s+json|--format\s+json|--json)(\s|$)/.test(text)) return text;
717
+ return `${text} -f json`;
718
+ }
719
+
720
+ function commandNetworkMode(commandName) {
721
+ if (NETWORK_NONE_COMMANDS.has(commandName)) return 'none';
722
+ if (NETWORK_CONDITIONAL_COMMANDS.has(commandName)) return 'conditional';
723
+ return 'required';
724
+ }
725
+
726
+ function unknownCommandHint(commandName) {
727
+ const replacement = RENAMED_COMMAND_HINTS[commandName];
728
+ if (replacement) return `该命令已产品化命名为 ${commandPrefix()} ${replacement}。运行 ${commandPrefix()} ${replacement.split(' ')[0]} -h 查看同组命令。`;
729
+ const guess = COMMAND_GUESS_HINTS[commandName];
730
+ if (guess) return guess;
731
+ const group = String(commandName || '').split(' ').filter(Boolean)[0];
732
+ if (group === 'workspace') return `账号用 ${commandPrefix()} account ...,项目组用 ${commandPrefix()} project ...。运行 ${commandPrefix()} --help 查看可用命令。`;
733
+ if (group === 'billing') return `积分查询用 ${commandPrefix()} credits ...。运行 ${commandPrefix()} credits -h 查看可用命令。`;
734
+ return `运行 ${commandPrefix()} --help 查看可用命令。`;
735
+ }
736
+
737
+ function buildCommandSafety(command, optionKeys) {
738
+ const name = command.name;
739
+ const remoteWrite = REMOTE_WRITE_COMMANDS.has(name);
740
+ const localStateWrite = LOCAL_STATE_WRITE_COMMANDS.has(name);
741
+ const requiresConfirmation = CONFIRMATION_COMMANDS.has(name);
742
+ const costsPoints = COST_COMMANDS.has(name);
743
+ const destructive = DESTRUCTIVE_COMMANDS.has(name);
744
+ const supportsDryRun = optionKeys.has('dryRun');
745
+ return {
746
+ safeToAutoRun: !(remoteWrite || localStateWrite || requiresConfirmation || costsPoints || destructive),
747
+ network: commandNetworkMode(name),
748
+ remoteWrite,
749
+ localStateWrite,
750
+ costsPoints,
751
+ destructive,
752
+ requiresConfirmation,
753
+ confirmationFlag: requiresConfirmation ? '--yes' : null,
754
+ supportsDryRun,
755
+ dryRunFlag: supportsDryRun ? '--dry-run' : null,
756
+ longRunning: LONG_RUNNING_COMMANDS.has(name),
757
+ };
758
+ }
759
+
760
+ function buildCommandWorkflow(command) {
761
+ const outputKind = OUTPUT_KIND_BY_COMMAND[command.name] || 'generic';
762
+ const next = [];
763
+ if (['image create', 'video create'].includes(command.name)) {
764
+ next.push('读取 data.taskId 和 data.nextCommand', '运行 task wait 等待结果');
765
+ }
766
+ if (command.name === 'subject publish') {
767
+ next.push('读取 data.elementId', '运行 subject wait 获取 externalId / nextRefSubject');
768
+ }
769
+ if (['image create-batch', 'video create-batch', 'subject publish-batch'].includes(command.name)) {
770
+ next.push('读取每项 status / taskId / error', '使用 task record-poll 或对应 wait 命令恢复批量结果');
771
+ }
772
+ return {
773
+ outputKind,
774
+ recommendedPreflight: PREFLIGHTS_BY_COMMAND[command.name] || (ARTIFACT_WRITE_COMMANDS.includes(command.name) ? [`${command.name} --dry-run`] : []),
775
+ nextActions: next,
776
+ };
777
+ }
778
+
779
+ function buildAgentContract() {
780
+ return {
781
+ defaultOutputFormat: 'compact-text',
782
+ jsonFlag: '-f json',
783
+ outputPolicy: '默认输出精简 key=value 文本,只保留决策字段;需要稳定 JSON、嵌套结构或脚本严格解析时显式传 -f json 或 --json。JSON envelope 字段已做归一化,见 canonicalFields。',
784
+ canonicalFields: {
785
+ purpose: 'CLI 输出对上游平台多版本字段做了归一化。Agent 只识别 canonical 列,遇到 aliases 列出的旧字段名时说明是上游原始返回,CLI 已在 text / 归一化层做了 fallback。不要把旧名当成独立新字段对外报告。',
786
+ fields: [
787
+ { canonical: 'billingPointBalance', aliases: ['teamPointBalance'], meaning: '实际可扣积分余额(/api/anime/member/benefits/queryGroupPoint)' },
788
+ { canonical: 'billingPointRemainingAfter', aliases: ['teamPointRemainingAfter'], meaning: '本次任务后预计可扣积分余额' },
789
+ { canonical: 'projectBudgetBalance', aliases: ['projectPointBalance'], meaning: '项目组预算 / ROI 余额(不是积分)' },
790
+ { canonical: 'projectBudgetMax', aliases: ['projectPointMax'], meaning: '项目组预算上限' },
791
+ { canonical: 'projectBudgetRemainingAfter', aliases: ['projectPointRemainingAfter'], meaning: '本次任务后预计项目组预算余额' },
792
+ { canonical: 'pointCost', aliases: ['points', 'point', 'costPoint', 'estimatePoint'], meaning: '本次任务预计 / 实际消耗积分' },
793
+ ],
794
+ },
795
+ schemaPolicy: 'schema 是机器契约,只给摘要、参数 key、安全规则和示例;查看长帮助用 schema.commands[].helpCommand 或 lj-awb <domain> <command> -h。',
796
+ confirmationPolicy: 'schema.commands[].safety.requiresConfirmation=true 的命令,Agent 必须先向用户确认,再追加 --yes。',
797
+ dryRunPolicy: 'schema.commands[].safety.supportsDryRun=true 的写入 / 扣费命令,正式执行前优先跑 --dry-run。',
798
+ resourceShortcut: {
799
+ flag: '--resource',
800
+ syntax: 'type:usage[:reference_key]=path|url|asset:<id>; image create uses image:reference=... without key',
801
+ examples: [
802
+ 'image:reference=./ref.png',
803
+ 'image:first_frame=./hero.png',
804
+ 'image:first_frame=asset:<assetId>',
805
+ 'subject:reference:hero=asset:<externalId>',
806
+ ],
807
+ },
808
+ resourcesJson: {
809
+ flag: '--resources-json',
810
+ shape: [
811
+ {
812
+ type: 'image|video|audio|subject',
813
+ usage: 'reference|first_frame|last_frame|keyframe',
814
+ reference_key: 'video reference placeholder key; not used by image create image:reference',
815
+ source: { kind: 'url|asset_id', value: '<local-file-or-http-url-or-material-backendPath-or-asset-id>' },
816
+ },
817
+ ],
818
+ },
819
+ modelOptions: {
820
+ command: `${commandPrefix()} model options --model-group-code <code>`,
821
+ jsonCommand: `${commandPrefix()} model options --model-group-code <code> -f json`,
822
+ purpose: '查询指定模型支持的 CLI 参数、枚举值、默认值、素材约束和条件约束。',
823
+ useBefore: ['model create-spec', 'image fee', 'image create', 'video fee', 'video create'],
824
+ keyFields: ['params[].values', 'params[].defaultValue', 'resources[].mediaType', 'resources[].usage', 'resources[].fileTypes', 'resources[].maxFiles', 'resources[].supportLastFrameOnly', 'resources[].minDurationMs', 'resources[].maxDurationMs', 'constraints[].target', 'constraints[].conditions', 'constraints[].effect'],
825
+ },
826
+ modelCreateSpec: {
827
+ command: `${commandPrefix()} model create-spec --model-group-code <code>`,
828
+ jsonCommand: `${commandPrefix()} model create-spec --model-group-code <code> -f json`,
829
+ purpose: '查看指定模型如何组织 create 命令:输入模式、素材绑定规则、示例和创建前检查项。',
830
+ useBefore: ['image fee', 'image create', 'video fee', 'video create'],
831
+ keyFields: ['inputRequirement', 'supportedIntents', 'validationRules', 'agentGuidance', 'preflight', 'examples'],
832
+ },
833
+ modelInputGuide: {
834
+ command: `${commandPrefix()} model input-guide`,
835
+ jsonCommand: `${commandPrefix()} model input-guide -f json`,
836
+ purpose: '查看跨模型统一创建参数、resources 字段和素材绑定规则。',
837
+ useBefore: ['model options', 'model create-spec'],
838
+ },
839
+ taskTypes: {
840
+ image: 'IMAGE_CREATE',
841
+ video: 'VIDEO_GROUP',
842
+ },
843
+ exitCodes: [
844
+ { code: 0, type: 'success', meaning: '命令成功' },
845
+ { code: 1, type: 'runtime_error', meaning: '普通运行失败或平台业务失败' },
846
+ { code: 2, type: 'argument_error|unknown_command|unknown_option', meaning: '参数或命令错误' },
847
+ { code: 3, type: 'auth_required|auth_failed', meaning: '缺少认证或认证失败' },
848
+ { code: 10, type: 'confirmation_required', meaning: '需要用户确认后追加 --yes' },
849
+ { code: 20, type: 'task_still_running', meaning: '生图 / 生视频任务仍在运行,本轮等待窗口结束(error.type=task_still_running)。可继续 task wait 或 record-poll 续等。' },
850
+ { code: 20, type: 'subject_still_pending', meaning: '主体 externalId 尚未回填,本轮等待窗口结束(error.type=subject_still_pending)。可继续 subject wait 续等。' },
851
+ { code: 30, type: 'network_error', meaning: '网络、TLS、HTTP 或外部平台不可用' },
852
+ ],
853
+ };
854
+ }
855
+
856
+ function buildCommandSchema(commands, kwargs = {}, version = 'unknown') {
857
+ const domainFilter = String(kwargs.domain ?? '').trim();
858
+ const commandFilter = String(kwargs.command ?? '').trim();
859
+ const domains = new Map();
860
+ for (const command of commands) {
861
+ const group = commandGroup(command);
862
+ domains.set(group, {
863
+ name: group,
864
+ description: GROUP_DESCRIPTIONS[group] || '',
865
+ commandCount: (domains.get(group)?.commandCount || 0) + 1,
866
+ });
867
+ }
868
+ const filteredCommands = commands
869
+ .filter((command) => !domainFilter || commandGroup(command) === domainFilter)
870
+ .filter((command) => !commandFilter || command.name === commandFilter || commandSubcommand(command) === commandFilter);
871
+ const matchedDomains = new Set(filteredCommands.map((command) => commandGroup(command)));
872
+ const shouldFilterDomains = Boolean(domainFilter || commandFilter);
873
+ return {
874
+ schemaVersion: 1,
875
+ filters: {
876
+ domain: domainFilter || null,
877
+ command: commandFilter || null,
878
+ },
879
+ cli: {
880
+ name: 'lj-awb',
881
+ version,
882
+ commandPrefix: commandPrefix(),
883
+ },
884
+ usage: [
885
+ `${commandPrefix()} <domain> <command> [options]`,
886
+ `${commandPrefix()} <command> [options]`,
887
+ `${commandPrefix()} <domain> -h`,
888
+ `${commandPrefix()} <domain> <command> -h`,
889
+ ],
890
+ globalOptions: [
891
+ { flag: '-f, --format json', key: 'format', description: '输出 JSON;默认不传时输出精简文本' },
892
+ { flag: '--json', key: 'format', value: 'json', description: '等同于 -f json' },
893
+ { flag: '-h, --help', key: 'help', description: '查看帮助' },
894
+ { flag: '-v, --version', key: 'version', description: '查看版本' },
895
+ ],
896
+ agentContract: buildAgentContract(),
897
+ domains: Array.from(domains.values())
898
+ .filter((domain) => !shouldFilterDomains || matchedDomains.has(domain.name))
899
+ .sort((a, b) => a.name.localeCompare(b.name)),
900
+ commands: filteredCommands.map((command) => {
901
+ const requiredOptions = new Set();
902
+ if (commandGroup(command) === 'artifact') requiredOptions.add('projectId');
903
+ for (const key of COMMAND_REQUIRED_OPTIONS[command.name] || []) requiredOptions.add(key);
904
+ const requiredAnyOptions = COMMAND_REQUIRED_ANY_OPTIONS[command.name] || [];
905
+ const optionKeys = new Set((command.args || []).map((arg) => normalizeKey(arg.name)));
906
+ return {
907
+ name: command.name,
908
+ domain: commandGroup(command),
909
+ command: commandSubcommand(command),
910
+ invocation: `${commandPrefix()} ${command.name}`,
911
+ helpCommand: `${commandPrefix()} ${command.name} -h`,
912
+ summary: summaryOf(command),
913
+ description: renderCliText(summaryOf(command)),
914
+ examples: extractExamples(command).map(renderCliText),
915
+ jsonExamples: extractExamples(command).map(asJsonExample),
916
+ requiredOptions: [...requiredOptions],
917
+ requiredAnyOptions,
918
+ safety: buildCommandSafety(command, optionKeys),
919
+ workflow: buildCommandWorkflow(command),
920
+ options: (command.args || []).map((arg) => {
921
+ const key = normalizeKey(arg.name);
922
+ return {
923
+ name: arg.name,
924
+ key,
925
+ flag: formatOptionName(arg.name),
926
+ valueName: arg.valueName || null,
927
+ required: requiredOptions.has(key),
928
+ requiredAnyGroup: requiredAnyOptions.find((group) => group.includes(key)) || null,
929
+ description: arg.description || '',
930
+ };
931
+ }),
932
+ };
933
+ }),
934
+ };
935
+ }
936
+
937
+ function printRootHelp(commands, version) {
938
+ const groups = new Map();
939
+ for (const command of commands) {
940
+ const group = commandGroup(command);
941
+ if (!groups.has(group)) groups.set(group, []);
942
+ groups.get(group).push(command);
943
+ }
944
+
945
+ const lines = [
946
+ `lj-awb v${version}`,
947
+ '',
948
+ '灵境 AWB 命令行工具。默认输出精简文本;需要 JSON 时传 -f json。',
949
+ '',
950
+ 'Usage:',
951
+ ` ${commandPrefix()} <domain> <command> [options]`,
952
+ ` ${commandPrefix()} <command> [options]`,
953
+ ` ${commandPrefix()} <domain> -h`,
954
+ ` ${commandPrefix()} <domain> <command> -h`,
955
+ '',
956
+ 'Quick start:',
957
+ ` ${commandPrefix()} auth status`,
958
+ ` ${commandPrefix()} auth login --access-key <access-key>`,
959
+ ` ${commandPrefix()} model image-models --model Banana`,
960
+ ` ${commandPrefix()} model input-guide`,
961
+ ` ${commandPrefix()} model options --model-group-code <code>`,
962
+ ` ${commandPrefix()} model create-spec --model-group-code <code>`,
963
+ ` ${commandPrefix()} image create --model-group-code <code> --prompt "一只小狗" --dry-run`,
964
+ '',
965
+ 'Command groups:',
966
+ ];
967
+ for (const [group] of groups.entries()) {
968
+ lines.push(` ${group.padEnd(12)} ${GROUP_DESCRIPTIONS[group] || ''}`);
969
+ }
970
+ lines.push(
971
+ '',
972
+ 'More help:',
973
+ ` ${commandPrefix()} <domain> -h`,
974
+ ` ${commandPrefix()} <domain> <command> -h`,
975
+ ` ${commandPrefix()} schema -f json`,
976
+ ` ${commandPrefix()} doctor -h`,
977
+ '',
978
+ 'Global options:',
979
+ ' -f, --format json 输出 JSON;默认不传时输出精简文本',
980
+ ' --json 等同于 -f json',
981
+ ' -h, --help 查看帮助',
982
+ ' -v, --version 查看版本',
983
+ );
984
+ process.stdout.write(`${lines.join('\n')}\n`);
985
+ }
986
+
987
+ function printGroupHelp(group, commands, version) {
988
+ const items = commands.filter((command) => commandGroup(command) === group);
989
+ const subgroupMap = SUBGROUP_DESCRIPTIONS[group];
990
+ const hasSubgroups = Boolean(subgroupMap && Object.keys(subgroupMap).length);
991
+
992
+ const lines = [
993
+ `lj-awb v${version}`,
994
+ '',
995
+ GROUP_DESCRIPTIONS[group] || `${group} 命令组`,
996
+ '',
997
+ 'Usage:',
998
+ ];
999
+ if (group === 'system') {
1000
+ lines.push(` ${commandPrefix()} <command> [options]`);
1001
+ } else if (hasSubgroups) {
1002
+ lines.push(` ${commandPrefix()} ${group} <subdomain> <command> [options]`);
1003
+ } else {
1004
+ lines.push(` ${commandPrefix()} ${group} <command> [options]`);
1005
+ }
1006
+ lines.push('');
1007
+ if (group === 'system') {
1008
+ lines.push(`提示:system 是命令组;下面的命令可直接运行,例如 ${commandPrefix()} doctor。`);
1009
+ } else if (hasSubgroups) {
1010
+ lines.push(`提示:${group} 下按子领域分组;先选子领域再看具体命令。`);
1011
+ } else {
1012
+ lines.push(`提示:${group} 是命令组;请选择下面的子命令运行。`);
1013
+ }
1014
+ lines.push('');
1015
+
1016
+ if (hasSubgroups) {
1017
+ const presentSubgroups = new Set(
1018
+ items
1019
+ .map((command) => commandSubgroupTokens(command))
1020
+ .filter(Boolean)
1021
+ .map(([, subgroup]) => subgroup),
1022
+ );
1023
+ lines.push('Subdomains:');
1024
+ for (const [subgroup, description] of Object.entries(subgroupMap)) {
1025
+ if (!presentSubgroups.has(subgroup)) continue;
1026
+ lines.push(` ${subgroup.padEnd(10)} ${description}`);
1027
+ }
1028
+ } else {
1029
+ lines.push('Commands:');
1030
+ for (const command of items) {
1031
+ const subcommand = commandSubcommand(command);
1032
+ lines.push(` ${subcommand.padEnd(28)} ${summaryOf(command)}`);
1033
+ }
1034
+ }
1035
+
1036
+ if (GROUP_EXAMPLES[group]?.length) {
1037
+ lines.push('', 'Examples:');
1038
+ for (const example of GROUP_EXAMPLES[group]) {
1039
+ lines.push(` ${renderCliText(example)}`);
1040
+ }
1041
+ }
1042
+ lines.push('', 'More help:');
1043
+ if (group === 'system') {
1044
+ for (const command of items) lines.push(` ${commandPrefix()} ${command.name} -h`);
1045
+ } else if (hasSubgroups) {
1046
+ lines.push(` ${commandPrefix()} ${group} <subdomain> -h`);
1047
+ lines.push(` ${commandPrefix()} ${group} <subdomain> <command> -h`);
1048
+ } else {
1049
+ lines.push(` ${commandPrefix()} ${group} <command> -h`);
1050
+ }
1051
+ process.stdout.write(`${lines.join('\n')}\n`);
1052
+ }
1053
+
1054
+ function printSubgroupHelp(group, subgroup, commands, version) {
1055
+ const items = commands.filter((command) => {
1056
+ const tokens = commandSubgroupTokens(command);
1057
+ return tokens && tokens[0] === group && tokens[1] === subgroup;
1058
+ });
1059
+ const description = SUBGROUP_DESCRIPTIONS[group]?.[subgroup] || `${group} ${subgroup} 子领域`;
1060
+ const lines = [
1061
+ `lj-awb v${version}`,
1062
+ '',
1063
+ description,
1064
+ '',
1065
+ 'Usage:',
1066
+ ` ${commandPrefix()} ${group} ${subgroup} <command> [options]`,
1067
+ '',
1068
+ 'Commands:',
1069
+ ];
1070
+ for (const command of items) {
1071
+ const parts = String(command.name || '').split(' ').filter(Boolean);
1072
+ const tail = parts.slice(2).join(' ');
1073
+ lines.push(` ${tail.padEnd(28)} ${summaryOf(command)}`);
1074
+ }
1075
+ const examples = SUBGROUP_EXAMPLES[`${group} ${subgroup}`];
1076
+ if (examples?.length) {
1077
+ lines.push('', 'Examples:');
1078
+ for (const example of examples) {
1079
+ lines.push(` ${renderCliText(example)}`);
1080
+ }
1081
+ }
1082
+ lines.push('', 'More help:');
1083
+ lines.push(` ${commandPrefix()} ${group} ${subgroup} <command> -h`);
1084
+ process.stdout.write(`${lines.join('\n')}\n`);
1085
+ }
1086
+
1087
+ function printCommandHelp(command) {
1088
+ const lines = [`Usage: ${commandPrefix()} ${command.name} [options]`, '', renderCliText(command.description || '')];
1089
+ if (command.args?.length) {
1090
+ lines.push('', 'Options:');
1091
+ for (const arg of command.args) {
1092
+ const value = arg.valueName ? ` <${arg.valueName}>` : '';
1093
+ const description = arg.description ? ` ${arg.description}` : '';
1094
+ lines.push(` --${arg.name}${value}${description}`);
1095
+ }
1096
+ }
1097
+ lines.push('', 'Global options:', ' -f, --format json 输出 JSON;默认不传时输出精简文本', ' --json 等同于 -f json');
1098
+ process.stdout.write(`${lines.join('\n')}\n`);
1099
+ }
1100
+
1101
+ function validateCommandOptions(command, kwargs) {
1102
+ const allowedKeys = (command.args || []).map((arg) => normalizeKey(arg.name));
1103
+ const allowed = new Set(allowedKeys);
1104
+ const unknown = Object.keys(kwargs).filter((key) => !allowed.has(key));
1105
+ if (!unknown.length) return;
1106
+ const suggestions = {};
1107
+ for (const unk of unknown) {
1108
+ const matches = suggestSimilarOptions(unk, allowedKeys);
1109
+ if (matches.length) suggestions[formatOptionName(unk)] = matches;
1110
+ }
1111
+ const baseHint = `运行 ${commandPrefix()} ${command.name} -h 查看可用参数。`;
1112
+ const suggestionParts = Object.entries(suggestions).map(([opt, matches]) => `${opt} → ${matches.join(' / ')}`);
1113
+ const hint = suggestionParts.length
1114
+ ? `你是不是想用:${suggestionParts.join(';')}?否则 ${baseHint}`
1115
+ : baseHint;
1116
+ throw new LingjingAwbCliError(`未知参数:${unknown.map(formatOptionName).join(', ')}`, {
1117
+ type: 'unknown_option',
1118
+ exitCode: 2,
1119
+ hint,
1120
+ details: {
1121
+ command: command.name,
1122
+ unknownOptions: unknown.map(formatOptionName),
1123
+ ...(Object.keys(suggestions).length ? { suggestions } : {}),
1124
+ },
1125
+ });
1126
+ }
1127
+
1128
+ function printData(data, format, meta = {}, context = {}) {
1129
+ if (format === 'json') {
1130
+ process.stdout.write(`${JSON.stringify(createEnvelope({ status: 'success', data: normalizeJsonData(meta.command, data), meta }), null, 2)}\n`);
1131
+ return;
1132
+ }
1133
+ process.stdout.write(formatTextOutput(meta.command, data, context));
1134
+ }
1135
+
1136
+ function printError(error, format, meta = {}) {
1137
+ const payload = {
1138
+ type: error.type || 'error',
1139
+ message: error.message || String(error),
1140
+ ...(error.hint ? { hint: error.hint } : {}),
1141
+ ...(error.details ? { details: error.details } : {}),
1142
+ };
1143
+ if (format === 'json') {
1144
+ process.stderr.write(`${JSON.stringify(createEnvelope({ status: 'error', error: payload, meta }), null, 2)}\n`);
1145
+ } else {
1146
+ process.stderr.write(formatTextError(payload));
1147
+ }
1148
+ }
1149
+
1150
+ export async function runStandaloneCli(argv = process.argv.slice(2)) {
1151
+ process.env.LINGJING_AWB_COMMAND_PREFIX ??= 'lj-awb';
1152
+ const commands = buildRegistry();
1153
+ const version = await readVersion().catch(() => 'unknown');
1154
+
1155
+ if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
1156
+ const parsed = parseArgv(argv.filter((item) => item !== '-h' && item !== '--help'));
1157
+ const resolvedCommandName = resolveCommandName(parsed.commandName, commands);
1158
+ const command = commands.find((item) => item.name === resolvedCommandName);
1159
+ if (command) {
1160
+ printCommandHelp(command);
1161
+ } else if (isCommandSubgroupName(parsed.commandName, commands)) {
1162
+ const [group, subgroup] = parsed.commandName.split(' ');
1163
+ printSubgroupHelp(group, subgroup, commands, version);
1164
+ } else if (isCommandGroupName(parsed.commandName, commands)) {
1165
+ printGroupHelp(parsed.commandName, commands, version);
1166
+ } else {
1167
+ printRootHelp(commands, version);
1168
+ }
1169
+ return;
1170
+ }
1171
+
1172
+ if (argv.length === 1 && ['-v', '--version'].includes(argv[0])) {
1173
+ process.stdout.write(`lj-awb v${version}\n`);
1174
+ return;
1175
+ }
1176
+
1177
+ const { commandName: rawCommandName, kwargs, format } = parseArgv(argv);
1178
+ const commandName = resolveCommandName(rawCommandName, commands);
1179
+ const command = commands.find((item) => item.name === commandName);
1180
+ if (!command) {
1181
+ if (isCommandSubgroupName(rawCommandName, commands)) {
1182
+ const [group, subgroup] = rawCommandName.split(' ');
1183
+ printSubgroupHelp(group, subgroup, commands, version);
1184
+ return;
1185
+ }
1186
+ if (isCommandGroupName(rawCommandName, commands)) {
1187
+ printGroupHelp(rawCommandName, commands, version);
1188
+ return;
1189
+ }
1190
+ printError(new LingjingAwbCliError(`未知命令: ${rawCommandName || '(empty)'}`, {
1191
+ type: 'unknown_command',
1192
+ exitCode: 2,
1193
+ hint: unknownCommandHint(rawCommandName),
1194
+ }), format, { command: rawCommandName });
1195
+ process.exitCode = 2;
1196
+ return;
1197
+ }
1198
+
1199
+ const startedAt = Date.now();
1200
+ try {
1201
+ validateCommandOptions(command, kwargs);
1202
+ const data = command.virtual === 'schema'
1203
+ ? buildCommandSchema(commands, kwargs, version)
1204
+ : await command.func({ command }, kwargs);
1205
+ printData(data, format, { command: command.name, elapsedMs: Date.now() - startedAt }, buildOutputContext(command, kwargs));
1206
+ } catch (error) {
1207
+ const cliError = error instanceof LingjingAwbCliError
1208
+ ? error
1209
+ : new LingjingAwbCliError(error.message || String(error), { type: 'runtime_error', exitCode: 1 });
1210
+ printError(cliError, format, { command: command.name, elapsedMs: Date.now() - startedAt });
1211
+ process.exitCode = cliError.exitCode || 1;
1212
+ }
1213
+ }