@lingjingai/lj-awb-cli-pre 0.3.16 → 0.3.18

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.
@@ -58,6 +58,9 @@ const REMOTE_VOICE_MIME_TYPES = new Set([
58
58
  'video/mp4',
59
59
  ]);
60
60
  const REMOTE_VOICE_EXTENSIONS = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.mp4']);
61
+ const ASSET_PLATFORM_CODES = ['JIMENG', 'BYTEPLUS'];
62
+ const ASSET_PLATFORM_CODE_SET = new Set(ASSET_PLATFORM_CODES);
63
+ const ASSET_PLATFORM_HINT = ASSET_PLATFORM_CODES.join('|');
61
64
 
62
65
  export function normalizeUserInfo(payload) {
63
66
  const data = payload && typeof payload === 'object' ? payload : {};
@@ -732,6 +735,44 @@ export async function listModels(kind, kwargs = {}) {
732
735
  return { usage, models };
733
736
  }
734
737
 
738
+ function requireAssetPlatform(value, options = {}) {
739
+ const flag = options.flag || '--platform';
740
+ const platform = trimToNull(value);
741
+ if (!platform) {
742
+ throw argumentError(`缺少参数:${flag}`, `素材加白平台必须显式传入 ${ASSET_PLATFORM_HINT},不要依赖默认平台。`);
743
+ }
744
+ if (!ASSET_PLATFORM_CODE_SET.has(platform)) {
745
+ throw argumentError(`不支持的平台:${platform}`, `只接受 ${ASSET_PLATFORM_HINT};请使用 model asset-review-models 查看模型支持的平台。`);
746
+ }
747
+ return platform;
748
+ }
749
+
750
+ function optionalAssetPlatform(value) {
751
+ if (value == null || value === '') return null;
752
+ return requireAssetPlatform(value);
753
+ }
754
+
755
+ function normalizeAssetReviewModelRecord(item = {}) {
756
+ return compactRecord({
757
+ modelGroupCode: item?.modelGroupCode ?? item?.groupCode ?? null,
758
+ platform: item?.platform ?? null,
759
+ });
760
+ }
761
+
762
+ export async function assetReviewModels(kwargs = {}) {
763
+ const modelGroupCode = trimToNull(kwargs.modelGroupCode);
764
+ const platform = optionalAssetPlatform(kwargs.platform);
765
+ const payload = await awbApi.fetchAssetReviewModels();
766
+ const models = normalizeRows(payload)
767
+ .map((item) => normalizeAssetReviewModelRecord(item))
768
+ .filter((item) => !modelGroupCode || item.modelGroupCode === modelGroupCode)
769
+ .filter((item) => !platform || item.platform === platform);
770
+ return {
771
+ models,
772
+ ...(toBool(kwargs.includeRaw) ? { raw: payload } : {}),
773
+ };
774
+ }
775
+
735
776
  async function findModelGroup(modelGroupCode, options = {}) {
736
777
  const includeRaw = Boolean(options.includeRaw);
737
778
  const [imageResult, videoResult] = await Promise.allSettled([
@@ -2168,7 +2209,35 @@ async function readJsonMaybeFile(value, fallback) {
2168
2209
  return JSON.parse(fileText);
2169
2210
  }
2170
2211
 
2171
- async function collectFileSpecs(kwargs = {}, defaultSceneType = TASK_UPLOAD_SCENE.DEFAULT) {
2212
+ const AUDIO_VIDEO_EXTENSIONS = new Set([
2213
+ '.aac',
2214
+ '.aiff',
2215
+ '.avi',
2216
+ '.flac',
2217
+ '.m4a',
2218
+ '.m4v',
2219
+ '.mkv',
2220
+ '.mov',
2221
+ '.mp3',
2222
+ '.mp4',
2223
+ '.mpeg',
2224
+ '.mpg',
2225
+ '.oga',
2226
+ '.ogg',
2227
+ '.ogv',
2228
+ '.wav',
2229
+ '.webm',
2230
+ ]);
2231
+
2232
+ function defaultUploadSceneForFile(filePath, mimeType = '') {
2233
+ const normalizedMime = String(mimeType || '').toLowerCase();
2234
+ if (normalizedMime.startsWith('audio/') || normalizedMime.startsWith('video/')) return TASK_UPLOAD_SCENE.VIDEO_CREATE;
2235
+ const ext = path.extname(String(filePath || '')).toLowerCase();
2236
+ if (AUDIO_VIDEO_EXTENSIONS.has(ext)) return TASK_UPLOAD_SCENE.VIDEO_CREATE;
2237
+ return TASK_UPLOAD_SCENE.DEFAULT;
2238
+ }
2239
+
2240
+ async function collectFileSpecs(kwargs = {}, defaultSceneType = null) {
2172
2241
  const specs = [];
2173
2242
  for (const file of parseListArg(kwargs.file)) specs.push({ file });
2174
2243
  for (const file of parseListArg(kwargs.files)) specs.push({ file });
@@ -2181,11 +2250,14 @@ async function collectFileSpecs(kwargs = {}, defaultSceneType = TASK_UPLOAD_SCEN
2181
2250
  }
2182
2251
  }
2183
2252
  return specs
2184
- .map((item) => ({
2185
- file: trimToNull(item.file ?? item.path ?? item.filePath),
2186
- sceneType: trimToNull(item.sceneType ?? kwargs.sceneType) ?? defaultSceneType,
2187
- projectNo: trimToNull(item.projectNo ?? kwargs.projectNo) ?? '',
2188
- }))
2253
+ .map((item) => {
2254
+ const file = trimToNull(item.file ?? item.path ?? item.filePath);
2255
+ return {
2256
+ file,
2257
+ sceneType: trimToNull(item.sceneType ?? kwargs.sceneType) ?? defaultSceneType ?? defaultUploadSceneForFile(file),
2258
+ projectNo: trimToNull(item.projectNo ?? kwargs.projectNo) ?? '',
2259
+ };
2260
+ })
2189
2261
  .filter((item) => item.file);
2190
2262
  }
2191
2263
 
@@ -2194,7 +2266,7 @@ function dryRunBackendPath(filePath, sceneType) {
2194
2266
  }
2195
2267
 
2196
2268
  export async function uploadFilesCommand(kwargs = {}) {
2197
- const specs = await collectFileSpecs(kwargs, trimToNull(kwargs.sceneType) ?? TASK_UPLOAD_SCENE.DEFAULT);
2269
+ const specs = await collectFileSpecs(kwargs, trimToNull(kwargs.sceneType));
2198
2270
  if (!specs.length) throw argumentError('缺少上传文件', '传 --file <path> 或 --files a.png,b.mp4。');
2199
2271
  if (toBool(kwargs.dryRun)) {
2200
2272
  const files = [];
@@ -2224,7 +2296,7 @@ export async function uploadLocalFile(filePath, options = {}) {
2224
2296
  throw argumentError(`文件不存在:${filePath}`);
2225
2297
  }
2226
2298
  const buffer = await fs.readFile(inspected.filePath);
2227
- const sceneType = options.sceneType ?? TASK_UPLOAD_SCENE.DEFAULT;
2299
+ const sceneType = options.sceneType ?? defaultUploadSceneForFile(inspected.filePath, inspected.mimeType);
2228
2300
  const groupId = crypto.randomUUID().replaceAll('-', '');
2229
2301
  const secret = await awbApi.fetchUploadSecret({
2230
2302
  sceneType,
@@ -3433,6 +3505,7 @@ function normalizeAssetGroup(item = {}) {
3433
3505
  groupId: item?.id ?? item?.groupId ?? item?.assetGroupsId ?? null,
3434
3506
  name: item?.name ?? item?.groupName ?? null,
3435
3507
  description: item?.description ?? null,
3508
+ platform: item?.platform ?? null,
3436
3509
  projectName: item?.projectName ?? null,
3437
3510
  };
3438
3511
  }
@@ -3640,43 +3713,53 @@ export async function assetMatchActor(kwargs = {}) {
3640
3713
  }
3641
3714
 
3642
3715
  export async function assetGroupList(kwargs = {}) {
3716
+ const platform = requireAssetPlatform(kwargs.platform);
3643
3717
  const payload = await awbApi.listAssetGroups({
3644
3718
  name: kwargs.name ?? '',
3719
+ platform,
3645
3720
  pageNumber: toInt(kwargs.pageNumber, 1),
3646
3721
  pageSize: toInt(kwargs.pageSize, 20),
3647
3722
  ...(parseListArg(kwargs.groupIds).length ? { groupIds: parseListArg(kwargs.groupIds) } : {}),
3648
3723
  });
3649
- return { groups: extractAssetGroupRows(payload), raw: toBool(kwargs.includeRaw) ? payload : undefined };
3724
+ return { platform, groups: extractAssetGroupRows(payload), raw: toBool(kwargs.includeRaw) ? payload : undefined };
3650
3725
  }
3651
3726
 
3652
3727
  export async function assetGroupGet(kwargs = {}) {
3653
3728
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3654
- const payload = await awbApi.getAssetGroup(groupId);
3655
- return payload && typeof payload === 'object' && !Array.isArray(payload)
3656
- ? normalizeAssetGroup(payload)
3657
- : extractAssetGroupRows(payload)[0] ?? { groupId };
3729
+ const platform = requireAssetPlatform(kwargs.platform);
3730
+ const payload = await awbApi.getAssetGroup(groupId, { platform });
3731
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
3732
+ const group = normalizeAssetGroup(payload);
3733
+ return { ...group, platform: group.platform ?? platform };
3734
+ }
3735
+ return extractAssetGroupRows(payload)[0] ?? { groupId, platform };
3658
3736
  }
3659
3737
 
3660
3738
  export async function assetGroupCreate(kwargs = {}) {
3661
3739
  const name = requireValue(kwargs, 'name');
3740
+ const platform = requireAssetPlatform(kwargs.platform);
3662
3741
  const body = {
3663
3742
  name,
3664
3743
  description: kwargs.description ?? '',
3744
+ platform,
3665
3745
  projectName: kwargs.projectName ?? 'default',
3666
3746
  };
3667
3747
  if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group', request: body };
3668
3748
  ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'create asset-group', body });
3669
3749
  const payload = await awbApi.createAssetGroup(body);
3670
- return { created: true, groupId: payload?.id ?? payload?.groupId ?? payload ?? null, name, projectName: body.projectName };
3750
+ return { created: true, groupId: payload?.id ?? payload?.groupId ?? payload ?? null, name, platform, projectName: body.projectName };
3671
3751
  }
3672
3752
 
3673
3753
  export async function assetGroupUpdate(kwargs = {}) {
3674
3754
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3675
- const body = {};
3755
+ const platform = requireAssetPlatform(kwargs.platform);
3756
+ const body = { platform };
3676
3757
  if (kwargs.name != null) body.name = kwargs.name;
3677
3758
  if (kwargs.description != null) body.description = kwargs.description;
3678
3759
  if (kwargs.projectName != null) body.projectName = kwargs.projectName;
3679
- if (!Object.keys(body).length) throw argumentError('缺少素材组更新字段', '至少传 --name、--description --project-name。');
3760
+ if (!['name', 'description', 'projectName'].some((key) => Object.prototype.hasOwnProperty.call(body, key))) {
3761
+ throw argumentError('缺少素材组更新字段', '至少传 --name、--description 或 --project-name;--platform 只用于定位平台资产组。');
3762
+ }
3680
3763
  if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group-update', groupId, request: body };
3681
3764
  ensureConfirmed(kwargs, '更新素材组是云端写入动作,需要确认', { action: 'create asset-group-update', groupId, body });
3682
3765
  await awbApi.updateAssetGroup(groupId, body);
@@ -3692,6 +3775,7 @@ function extractAssetId(payload) {
3692
3775
  export async function assetRegister(kwargs = {}) {
3693
3776
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3694
3777
  const name = requireValue(kwargs, 'name');
3778
+ const platform = requireAssetPlatform(kwargs.platform);
3695
3779
  const localFile = trimToNull(kwargs.file);
3696
3780
  const assetPath = trimToNull(kwargs.backendPath) ?? normalizeCosAssetPath(kwargs.url);
3697
3781
  if (!localFile && !assetPath) throw argumentError('缺少素材路径', '传 --file、--backend-path 或 --url。');
@@ -3701,20 +3785,20 @@ export async function assetRegister(kwargs = {}) {
3701
3785
  action: 'create asset',
3702
3786
  request: {
3703
3787
  assetGroupsId: groupId,
3704
- url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.SUBJECT)) : assetPath,
3788
+ url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.ASSET_REVIEW)) : assetPath,
3705
3789
  name,
3706
- ...(kwargs.platform ? { platform: kwargs.platform } : {}),
3790
+ platform,
3707
3791
  },
3708
3792
  localFile: localFile ? await inspectLocalFile(localFile) : null,
3709
3793
  };
3710
3794
  }
3711
3795
  ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'create asset', groupId, name });
3712
- const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.SUBJECT }) : null;
3796
+ const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.ASSET_REVIEW }) : null;
3713
3797
  const body = {
3714
3798
  assetGroupsId: groupId,
3715
3799
  url: normalizeCosAssetPath(uploaded?.backendPath ?? assetPath),
3716
3800
  name,
3717
- ...(kwargs.platform ? { platform: kwargs.platform } : {}),
3801
+ platform,
3718
3802
  };
3719
3803
  const payload = await awbApi.registerAsset(body);
3720
3804
  return {
@@ -3722,6 +3806,7 @@ export async function assetRegister(kwargs = {}) {
3722
3806
  assetId: extractAssetId(payload),
3723
3807
  groupId,
3724
3808
  name,
3809
+ platform,
3725
3810
  assetPath: body.url,
3726
3811
  ...(uploaded ? { upload: uploaded } : {}),
3727
3812
  };
@@ -4008,37 +4093,32 @@ function tagListFromArg(value) {
4008
4093
  .filter((item) => item.tagId);
4009
4094
  }
4010
4095
 
4011
- function normalizeSubjectCreateModelCode(value) {
4012
- const text = trimToNull(value);
4013
- if (!text) return null;
4014
- const normalized = text.toLowerCase();
4015
- if (normalized === 'tx' || normalized === 'vidu') return normalized;
4016
- return null;
4017
- }
4096
+ const SUBJECT_MODEL_CODE_HINT = '传 --model-code tx|vidu;KeLing / 可灵主体明确传 tx,Vidu 主体明确传 vidu。';
4018
4097
 
4019
4098
  function resolveSubjectCreateModelCode(kwargs = {}) {
4020
4099
  const explicitModelCode = trimToNull(kwargs.modelCode ?? kwargs.model_code);
4021
- const modelCode = normalizeSubjectCreateModelCode(explicitModelCode);
4022
- if (modelCode) return { modelCode };
4100
+ if (explicitModelCode === 'tx' || explicitModelCode === 'vidu') {
4101
+ return { modelCode: explicitModelCode };
4102
+ }
4023
4103
  if (explicitModelCode) {
4024
4104
  throw argumentError(
4025
4105
  `主体 modelCode 不支持:${explicitModelCode}`,
4026
- '只支持 --model-code tx|vidu;KeLing / 可灵用 tx,Vidu 用 vidu。',
4106
+ SUBJECT_MODEL_CODE_HINT,
4027
4107
  );
4028
4108
  }
4029
4109
 
4030
4110
  throw argumentError(
4031
4111
  '缺少主体 modelCode',
4032
- '传 --model-code tx|vidu;KeLing / 可灵用 tx,Vidu 用 vidu。',
4112
+ SUBJECT_MODEL_CODE_HINT,
4033
4113
  );
4034
4114
  }
4035
4115
 
4036
- function buildSubjectCreateBody(kwargs, specs, assets) {
4116
+ function buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution = resolveSubjectCreateModelCode(kwargs)) {
4037
4117
  const name = requireValue(kwargs, 'name');
4038
4118
  const description = trimToNull(kwargs.description ?? kwargs.elementDescription) ?? name;
4039
4119
  const primary = assets.find((item) => item.isPrimary);
4040
4120
  const referAssets = assets.filter((item) => item.assetPath);
4041
- const { modelCode } = resolveSubjectCreateModelCode(kwargs);
4121
+ const { modelCode } = modelCodeResolution;
4042
4122
  return compactRecord({
4043
4123
  reqTaskId: trimToNull(kwargs.reqTaskId),
4044
4124
  modelCode,
@@ -4117,10 +4197,7 @@ export async function subjectPublish(kwargs = {}) {
4117
4197
  dryRun: true,
4118
4198
  action: 'create subject',
4119
4199
  name,
4120
- resolvedModelCode: modelCodeResolution.modelCode,
4121
- resolvedFrom: modelCodeResolution.resolvedFrom,
4122
- resolvedFromValue: modelCodeResolution.resolvedFromValue,
4123
- request: buildSubjectCreateBody(kwargs, specs, assets),
4200
+ request: buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution),
4124
4201
  assets,
4125
4202
  localFiles: await inspectLocalFiles(specs.map((item) => item.file).filter(Boolean)),
4126
4203
  nextRefSubject: `${name}=<externalId>`,
@@ -4140,7 +4217,7 @@ export async function subjectPublish(kwargs = {}) {
4140
4217
  ...(uploaded ? { upload: uploaded } : {}),
4141
4218
  });
4142
4219
  }
4143
- const body = buildSubjectCreateBody(kwargs, specs, assets);
4220
+ const body = buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution);
4144
4221
  const payload = await awbApi.createElement(body);
4145
4222
  const elementId = extractCreatedElementId(payload);
4146
4223
  if (!elementId) throw new LingjingAwbCliError('主体创建完成但未拿到 elementId', { type: 'api_error', exitCode: 1, details: payload });
@@ -4229,14 +4306,14 @@ export async function subtitleRemove(kwargs = {}) {
4229
4306
  });
4230
4307
  await appendTaskRecord(kwargs, {
4231
4308
  taskId: result.taskId,
4232
- taskType: 'VIDEO_GROUP',
4309
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4233
4310
  projectGroupNo: built.projectGroupNo,
4234
4311
  promptSummary: `去字幕 sourceTaskId=${built.sourceTaskId}`,
4235
4312
  });
4236
4313
  return {
4237
4314
  ...result,
4238
4315
  nextCommand: result.taskId
4239
- ? `lj-awb task wait --task-id ${result.taskId} --task-type VIDEO_GROUP${built.projectGroupNo ? ` --project-group-no ${built.projectGroupNo}` : ''} -f json`
4316
+ ? `lj-awb task video-subtitle-status --task-id ${result.taskId}${built.projectGroupNo ? ` --project-group-no ${built.projectGroupNo}` : ''} -f json`
4240
4317
  : null,
4241
4318
  };
4242
4319
  }
@@ -4270,6 +4347,6 @@ export async function subtitleStatus(kwargs = {}) {
4270
4347
  return taskStatus({
4271
4348
  ...kwargs,
4272
4349
  taskId,
4273
- taskType: 'VIDEO_GROUP',
4350
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4274
4351
  });
4275
4352
  }
@@ -24,6 +24,7 @@ const OPTION_SYNONYMS = {
24
24
  threads: ['concurrency'],
25
25
  output: ['format'],
26
26
  json: ['format'],
27
+ keyword: ['model'],
27
28
  input: ['inputFile'],
28
29
  in: ['inputFile'],
29
30
  dry: ['dryRun'],
@@ -60,6 +61,16 @@ function suggestSimilarOptions(unknownKey, allowedKeys) {
60
61
  for (const syn of synonyms) {
61
62
  if (allowedKeys.includes(syn)) record(syn, -1);
62
63
  }
64
+ if (unknownKey.length === 1) {
65
+ const ch = unknownKey.toLowerCase();
66
+ for (const allowed of allowedKeys) {
67
+ if (allowed.toLowerCase().startsWith(ch)) record(allowed, 1);
68
+ }
69
+ return [...scored.entries()]
70
+ .sort((x, y) => x[1] - y[1])
71
+ .slice(0, 3)
72
+ .map(([key]) => formatOptionName(key));
73
+ }
63
74
  for (const allowed of allowedKeys) {
64
75
  const a = unknownKey.toLowerCase();
65
76
  const b = allowed.toLowerCase();
@@ -153,6 +164,7 @@ function assignKwarg(kwargs, key, value) {
153
164
  function parseArgv(argv) {
154
165
  const commandParts = [];
155
166
  const kwargs = {};
167
+ const kwargRawFlags = {};
156
168
  let format = 'text';
157
169
 
158
170
  for (let index = 0; index < argv.length; index += 1) {
@@ -182,10 +194,27 @@ function parseArgv(argv) {
182
194
  }
183
195
  continue;
184
196
  }
197
+ if (token.length > 1 && token.startsWith('-') && /^-[A-Za-z]/.test(token)) {
198
+ const raw = token.slice(1);
199
+ const eqIndex = raw.indexOf('=');
200
+ const keyPart = eqIndex >= 0 ? raw.slice(0, eqIndex) : raw;
201
+ const inlineValue = eqIndex >= 0 ? raw.slice(eqIndex + 1) : undefined;
202
+ const key = normalizeKey(keyPart);
203
+ if (!(key in kwargRawFlags)) kwargRawFlags[key] = token;
204
+ if (inlineValue !== undefined) {
205
+ assignKwarg(kwargs, key, inlineValue);
206
+ } else if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
207
+ assignKwarg(kwargs, key, argv[index + 1]);
208
+ index += 1;
209
+ } else {
210
+ assignKwarg(kwargs, key, true);
211
+ }
212
+ continue;
213
+ }
185
214
  commandParts.push(token);
186
215
  }
187
216
 
188
- return { commandName: commandParts.join(' '), kwargs, format };
217
+ return { commandName: commandParts.join(' '), kwargs, kwargRawFlags, format };
189
218
  }
190
219
 
191
220
  async function readVersion() {
@@ -217,6 +246,33 @@ const VIRTUAL_COMMANDS = [
217
246
  ];
218
247
 
219
248
  const RENAMED_COMMAND_HINTS = {
249
+ image: 'create image',
250
+ 'image fee': 'create image-fee',
251
+ 'image create': 'create image',
252
+ 'image create-batch': 'create image-batch',
253
+ 'image status': 'task image-status',
254
+ video: 'create video',
255
+ 'video fee': 'create video-fee',
256
+ 'video create': 'create video',
257
+ 'video create-batch': 'create video-batch',
258
+ 'video status': 'task video-status',
259
+ 'video subtitle-remove': 'create video-subtitle-removal',
260
+ 'video subtitle-status': 'task video-subtitle-status',
261
+ asset: 'create asset',
262
+ 'asset match-actor': 'create asset-match-actor',
263
+ 'asset groups': 'create asset-groups',
264
+ 'asset group': 'create asset-group-get',
265
+ 'asset group-create': 'create asset-group',
266
+ 'asset group-update': 'create asset-group-update',
267
+ 'asset register': 'create asset',
268
+ subject: 'create subject',
269
+ 'subject list': 'create subject-list',
270
+ 'subject publish': 'create subject',
271
+ 'subject wait': 'create subject-wait',
272
+ 'subject publish-batch': 'create subject-batch',
273
+ 'subject voice list': 'create subject-voice-list',
274
+ 'subject voice create': 'create subject-voice',
275
+ 'subject voice wait': 'create subject-voice-wait',
220
276
  'workspace me': 'account info',
221
277
  'workspace teams': 'account teams',
222
278
  'workspace team-select': 'account switch-team',
@@ -367,6 +423,7 @@ const GROUP_EXAMPLES = {
367
423
  model: [
368
424
  'lj-awb model image-models --model Banana',
369
425
  'lj-awb model video-models --model Seedance',
426
+ 'lj-awb model asset-review-models --platform JIMENG',
370
427
  'lj-awb model input-guide',
371
428
  'lj-awb model options --model-group-code <code>',
372
429
  'lj-awb model create-spec --model-group-code <code>',
@@ -381,8 +438,8 @@ const GROUP_EXAMPLES = {
381
438
  'lj-awb create video-fee --model-group-code <code> --prompt "雨夜奔跑" --duration 5',
382
439
  'lj-awb create subject --model-code tx --name 女主 --resource primary:./three-view.png --dry-run',
383
440
  'lj-awb create subject-wait --element-id <elementId> --wait-seconds 300',
384
- 'lj-awb create asset --group-id <id> --file ./actor.png --name "女主正面" --dry-run',
385
- 'lj-awb create asset-groups --name "女主"',
441
+ 'lj-awb create asset --group-id <id> --platform JIMENG --file ./actor.png --name "女主正面" --dry-run',
442
+ 'lj-awb create asset-groups --platform JIMENG --name "女主"',
386
443
  ],
387
444
  task: [
388
445
  'lj-awb task list --task-type IMAGE_CREATE --project-group-no <no>',
@@ -417,10 +474,11 @@ const COMMAND_REQUIRED_OPTIONS = {
417
474
  'task video-subtitle-status': ['taskId'],
418
475
  'task wait': ['taskId', 'taskType'],
419
476
  'create asset-match-actor': ['description'],
420
- 'create asset-group-get': ['groupId'],
421
- 'create asset-group': ['name'],
422
- 'create asset-group-update': ['groupId'],
423
- 'create asset': ['groupId', 'name'],
477
+ 'create asset-groups': ['platform'],
478
+ 'create asset-group-get': ['groupId', 'platform'],
479
+ 'create asset-group': ['name', 'platform'],
480
+ 'create asset-group-update': ['groupId', 'platform'],
481
+ 'create asset': ['groupId', 'name', 'platform'],
424
482
  'artifact script row': ['rowKind', 'entityKey'],
425
483
  'artifact script children': ['parentKey'],
426
484
  'artifact script delete-row': ['rowKind', 'entityKey'],
@@ -595,6 +653,7 @@ const OUTPUT_KIND_BY_COMMAND = {
595
653
  'credits usage': 'credits_usage',
596
654
  'model image-models': 'model_list',
597
655
  'model video-models': 'model_list',
656
+ 'model asset-review-models': 'asset_review_model_list',
598
657
  'model options': 'model_options',
599
658
  'model create-spec': 'model_create_spec',
600
659
  'model input-guide': 'model_input_guide',
@@ -687,9 +746,9 @@ const PREFLIGHTS_BY_COMMAND = {
687
746
  'create subject': ['doctor --verify', 'create subject --model-code tx|vidu --dry-run'],
688
747
  'create subject-batch': ['doctor --verify', 'prepare JSONL input with modelCode', 'create subject-batch --dry-run'],
689
748
  'create video-subtitle-removal': ['doctor --verify', 'create video-subtitle-removal --source-task-id <videoTaskId> --dry-run'],
690
- 'create asset-group': ['create asset-group --dry-run'],
691
- 'create asset-group-update': ['create asset-group-update --dry-run'],
692
- 'create asset': ['create asset --dry-run'],
749
+ 'create asset-group': ['model asset-review-models', 'create asset-group --platform <platform> --dry-run'],
750
+ 'create asset-group-update': ['create asset-group-update --platform <platform> --dry-run'],
751
+ 'create asset': ['model asset-review-models', 'create asset --platform <platform> --dry-run'],
693
752
  'upload files': ['upload files --dry-run'],
694
753
  };
695
754
 
@@ -782,6 +841,23 @@ function buildCommandWorkflow(command) {
782
841
  if (command.name === 'create asset') {
783
842
  next.push('读取 data.assetPath', '若后端返回审核 taskId,再运行 task wait --task-type ASSET_REGISTER');
784
843
  }
844
+ if (command.name === 'model image-models' || command.name === 'model video-models') {
845
+ next.push('读取 data.models[] 得到全部候选;用户给了模型口语名时保留同族全部候选,不只取第一个');
846
+ next.push('对每个候选运行 model options --model-group-code <modelGroupCode> 查看真实参数和素材约束');
847
+ next.push('先向用户展示候选模型 + 参数取值 + 资源能力,再推荐模型或进入 fee/dry-run');
848
+ }
849
+ if (command.name === 'model asset-review-models') {
850
+ next.push('读取 data.models[].platform', '用 create asset-groups --platform <platform> 查重或创建素材组');
851
+ }
852
+ if (command.name === 'model options') {
853
+ next.push('读取 params/resources/constraints 确认参数与素材约束', '运行 model create-spec --model-group-code <code> 查看创建写法和示例');
854
+ }
855
+ if (command.name === 'model create-spec') {
856
+ next.push('按 examples / supportedIntents 组装命令', '用户确认关键参数后先运行 create image-fee 或 create video-fee,再 dry-run');
857
+ }
858
+ if (command.name === 'model input-guide') {
859
+ next.push('运行 model image-models 或 model video-models 选择模型', '选定模型后运行 model options --model-group-code <code>');
860
+ }
785
861
  if (['create image-batch', 'create video-batch', 'create subject-batch'].includes(command.name)) {
786
862
  next.push('读取每项 status / taskId / error', '使用 task record-poll 或对应 wait 命令恢复批量结果');
787
863
  }
@@ -803,6 +879,7 @@ function buildAgentContract() {
803
879
  'schema version / domains / commands',
804
880
  'auth / account / team / current projectGroup',
805
881
  'modelCandidates by taskKind + keyword',
882
+ 'assetReviewModels by modelGroupCode / platform',
806
883
  'modelOptions / modelCreateSpec by modelGroupCode',
807
884
  'uploaded backendPath by local file path or remote URL',
808
885
  'taskId -> taskType / projectGroupNo / resultUrls / errorMessage',
@@ -813,10 +890,12 @@ function buildAgentContract() {
813
890
  invalidation: '用户切换账号、团队、项目组、模型、素材、prompt 或关键参数后,只刷新受影响的缓存;不要重复跑未变化的 model options / create-spec / fee。',
814
891
  },
815
892
  workflowPolicy: [
893
+ '模型口语名命中后必须先展示候选模型、真实参数取值和资源能力;没有完成用户可见候选清单前,不得代选默认模型 / 参数,也不得进入 fee 或 dry-run。',
816
894
  '模型探索阶段只读 model list / options / create-spec;fee 只在用户确认关键参数后跑一次。',
817
895
  'supportsDryRun=true 的写入 / 扣费命令先 dry-run,确认后 yes;不要把 dry-run 当参数探索工具反复跑。',
818
896
  '用户给出多条同模型同参数任务时优先 batch + task-record-file,不要单条循环 create。',
819
897
  '命令返回 nextCommand / nextRefSubject / nextVoiceArg 时优先复用返回值,不手拼等价命令。',
898
+ '素材加白平台先通过 model asset-review-models 或用户明确输入确定;create asset-* 必须显式传 --platform,不要依赖默认平台。',
820
899
  '旧根域 image / video / asset / subject 已移除;只使用 create / task / artifact 等 schema 暴露的 domain。',
821
900
  ],
822
901
  canonicalFields: {
@@ -854,6 +933,12 @@ function buildAgentContract() {
854
933
  },
855
934
  ],
856
935
  },
936
+ modelCandidatePresentation: {
937
+ purpose: '把 model image-models / video-models 与每个候选的 model options 转成用户可见清单,避免 Agent 内部看完 options 后直接代选。',
938
+ trigger: '用户问有哪些模型、给出口语名、或模型列表返回候选时都触发;即使只有一个候选也要展示真实可选项。',
939
+ requiredVisibleFields: ['displayName', 'modelDesc', 'taskQueueNum', 'quality values/default', 'ratio values/default', 'duration or generateNum values/default', 'resource modes/media/usages', 'channel or fast/pro differences'],
940
+ blockedBeforePresentation: ['create image-fee', 'create video-fee', 'create image --dry-run', 'create video --dry-run', 'default model recommendation', 'default quality/ratio/duration choice'],
941
+ },
857
942
  modelOptions: {
858
943
  command: `${commandPrefix()} model options --model-group-code <code>`,
859
944
  jsonCommand: `${commandPrefix()} model options --model-group-code <code> -f json`,
@@ -1150,28 +1235,37 @@ function printCommandHelp(command) {
1150
1235
  process.stdout.write(`${lines.join('\n')}\n`);
1151
1236
  }
1152
1237
 
1153
- function validateCommandOptions(command, kwargs) {
1238
+ function validateCommandOptions(command, kwargs, kwargRawFlags = {}) {
1154
1239
  const allowedKeys = (command.args || []).map((arg) => normalizeKey(arg.name));
1155
1240
  const allowed = new Set(allowedKeys);
1156
1241
  const unknown = Object.keys(kwargs).filter((key) => !allowed.has(key));
1157
1242
  if (!unknown.length) return;
1243
+ const displayFlag = (key) => kwargRawFlags[key] || formatOptionName(key);
1158
1244
  const suggestions = {};
1159
1245
  for (const unk of unknown) {
1160
1246
  const matches = suggestSimilarOptions(unk, allowedKeys);
1161
- if (matches.length) suggestions[formatOptionName(unk)] = matches;
1247
+ if (matches.length) suggestions[displayFlag(unk)] = matches;
1162
1248
  }
1249
+ const usedShortForm = unknown.some((key) => {
1250
+ const flag = kwargRawFlags[key];
1251
+ return flag && flag.startsWith('-') && !flag.startsWith('--');
1252
+ });
1163
1253
  const baseHint = `运行 ${commandPrefix()} ${command.name} -h 查看可用参数。`;
1254
+ const shortHint = usedShortForm
1255
+ ? `本 CLI 仅支持 -f / -h / -v 等少数短选项,业务参数请使用 --xxx 长形式。`
1256
+ : '';
1164
1257
  const suggestionParts = Object.entries(suggestions).map(([opt, matches]) => `${opt} → ${matches.join(' / ')}`);
1165
- const hint = suggestionParts.length
1166
- ? `你是不是想用:${suggestionParts.join(';')}?否则 ${baseHint}`
1167
- : baseHint;
1168
- throw new LingjingAwbCliError(`未知参数:${unknown.map(formatOptionName).join(', ')}`, {
1258
+ const hintParts = [];
1259
+ if (suggestionParts.length) hintParts.push(`你是不是想用:${suggestionParts.join(';')}?`);
1260
+ if (shortHint) hintParts.push(shortHint);
1261
+ hintParts.push(baseHint);
1262
+ throw new LingjingAwbCliError(`未知参数:${unknown.map(displayFlag).join(', ')}`, {
1169
1263
  type: 'unknown_option',
1170
1264
  exitCode: 2,
1171
- hint,
1265
+ hint: hintParts.join(' '),
1172
1266
  details: {
1173
1267
  command: command.name,
1174
- unknownOptions: unknown.map(formatOptionName),
1268
+ unknownOptions: unknown.map(displayFlag),
1175
1269
  ...(Object.keys(suggestions).length ? { suggestions } : {}),
1176
1270
  },
1177
1271
  });
@@ -1233,7 +1327,7 @@ export async function runStandaloneCli(argv = process.argv.slice(2)) {
1233
1327
  return;
1234
1328
  }
1235
1329
 
1236
- const { commandName: rawCommandName, kwargs, format } = parseArgv(argv);
1330
+ const { commandName: rawCommandName, kwargs, kwargRawFlags, format } = parseArgv(argv);
1237
1331
  const commandName = resolveCommandName(rawCommandName, commands);
1238
1332
  const command = commands.find((item) => item.name === commandName);
1239
1333
  if (!command) {
@@ -1257,7 +1351,7 @@ export async function runStandaloneCli(argv = process.argv.slice(2)) {
1257
1351
 
1258
1352
  const startedAt = Date.now();
1259
1353
  try {
1260
- validateCommandOptions(command, kwargs);
1354
+ validateCommandOptions(command, kwargs, kwargRawFlags);
1261
1355
  const data = command.virtual === 'schema'
1262
1356
  ? buildCommandSchema(commands, kwargs, version)
1263
1357
  : await command.func({ command }, kwargs);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lj-awb
3
- version: 0.3.16
3
+ version: 0.3.18
4
4
  description: "灵境 AWB CLI skill。使用 `lj-awb` 命令调用动漫平台 / AWB 云端能力,覆盖认证、项目组、积分、模型发现、上传、统一 create 创建域、任务查询、最终产物 artifact CRUD 与本地 JSON 导入。用户说生图、生视频、主体、音色、素材加白、去字幕、artifact 写入或查询时使用。正式生成、切换项目组、清空认证、artifact 写入等写入或扣费动作前必须确认。"
5
5
  metadata:
6
6
  requires:
@@ -43,6 +43,8 @@ LINGJING_AWB_CMD="$(bash "$(dirname "$0")/scripts/resolve-lj-awb-cmd.sh")"
43
43
  lj-awb schema -f json
44
44
  ```
45
45
 
46
+ 如果只是不确定某个命令参数,优先读精确契约,例如 `lj-awb schema --domain model --command video-models -f json`。schema 查询必须先返回,再组织业务命令;不要把 schema 查询和猜测命令放进同一批并行调用。
47
+
46
48
  如果用户已经给出完整只读命令,且该命令在本 skill 已知范围内,可以直接执行,不必为了形式补跑 schema。命令名、参数名、requiredOptions、safety、workflow.nextActions 都以 schema 为准。旧根域 `image` / `video` / `asset` / `subject` 不存在,不要尝试旧入口。
47
49
 
48
50
  ## 能力地图
@@ -84,7 +86,8 @@ lj-awb schema -f json
84
86
  - `fee` 是最终估价,不是参数探索工具。不要为多个 quality / duration / ratio / 渠道组合反复跑 fee。
85
87
  - 正式图片 / 视频任务要带 `--project-group-no`,除非命令 schema 没有该参数。
86
88
  - 命令返回 `nextCommand`、`nextRefSubject`、`nextVoiceArg` 时优先复用返回值,不手拼。
87
- - compact text 是默认输出;只有 schema、options、create-spec、完整嵌套结构或脚本解析时用 `-f json`。
89
+ - compact text 是默认输出;复杂模型命令按 `section:` + 缩进 `key=value` 输出,先按分区读 `summary` / `params` / `resources` / `constraints` / `intents` / `examples` / `next`。只有 schema、options、create-spec、完整嵌套结构或脚本解析时用 `-f json`。
90
+ - 模型口语名命中后必须过“候选展示门”:先把候选模型、真实参数取值和资源能力转成用户可见清单,再推荐或追问;不能只在内部读完 `model options` 就直接代选默认模型 / 参数。
88
91
  - 积分口径只把 `billingPointBalance` 当可扣积分余额;`projectBudgetBalance` 是项目组预算,不要混说。
89
92
  - 不直连 material / asset / 外部服务;所有业务能力统一通过 `lj-awb`。
90
93
 
@@ -1 +1 @@
1
- 0.3.16
1
+ 0.3.18
@@ -1,6 +1,6 @@
1
1
  {
2
- "skillVersion": "0.3.16",
3
- "minCliVersion": "0.3.16",
2
+ "skillVersion": "0.3.18",
3
+ "minCliVersion": "0.3.18",
4
4
  "preferredCommand": "lj-awb",
5
- "updatedAt": "2026-05-18"
5
+ "updatedAt": "2026-05-19"
6
6
  }