@lingjingai/lj-awb-cli-pre 0.3.18 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -8
- package/build/_shared.mjs +54 -5
- package/build/prod.mjs +12 -3
- package/package.json +6 -2
- package/packages/awb-cli/package.json +2 -2
- package/packages/awb-core/package.json +6 -2
- package/packages/awb-core/src/api.js +22 -0
- package/packages/awb-core/src/commands.js +112 -39
- package/packages/awb-core/src/common.js +8 -0
- package/packages/awb-core/src/output.js +2030 -8
- package/packages/awb-core/src/services.js +1835 -205
- package/packages/awb-core/src/standalone.js +472 -136
- package/packages/awb-core/src/update.js +327 -0
- package/skills/lj-awb/SKILL.md +35 -12
- package/skills/lj-awb/VERSION +1 -1
- package/skills/lj-awb/compat.json +3 -3
- package/skills/lj-awb/modules/artifact/asset.md +1 -1
- package/skills/lj-awb/modules/artifact/clip.md +1 -1
- package/skills/lj-awb/modules/artifact/script.md +1 -1
- package/skills/lj-awb/modules/artifact/video.md +1 -1
- package/skills/lj-awb/modules/asset.md +10 -1
- package/skills/lj-awb/modules/auth.md +9 -1
- package/skills/lj-awb/modules/create-contract.md +5 -2
- package/skills/lj-awb/modules/create.md +4 -2
- package/skills/lj-awb/modules/driver.md +12 -6
- package/skills/lj-awb/modules/image.md +3 -1
- package/skills/lj-awb/modules/model.md +12 -9
- package/skills/lj-awb/modules/task.md +4 -1
- package/skills/lj-awb/modules/upload.md +1 -1
- package/skills/lj-awb/modules/video.md +11 -2
- package/skills/lj-awb/modules/workflows.md +3 -1
- package/skills/lj-awb/references/error-codes.md +24 -0
- package/skills/lj-awb/references/model-options-read.md +16 -10
- package/skills/lj-awb/references/output-fields.md +10 -7
- package/skills/lj-awb/scripts/resolve-lj-awb-cmd.sh +106 -4
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import {
|
|
8
|
+
LingjingAwbCliError,
|
|
9
|
+
loadState,
|
|
10
|
+
nowIso,
|
|
11
|
+
saveState,
|
|
12
|
+
toBool,
|
|
13
|
+
toInt,
|
|
14
|
+
trimToNull,
|
|
15
|
+
} from './common.js';
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const DEFAULT_CLI_PACKAGE_NAME = '@lingjingai/lj-awb-cli';
|
|
19
|
+
const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/';
|
|
20
|
+
const DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
const DEFAULT_NPM_TIMEOUT_MS = 15_000;
|
|
22
|
+
const AUTO_CHECK_TIMEOUT_MS = 4_000;
|
|
23
|
+
|
|
24
|
+
function commandPrefix() {
|
|
25
|
+
return process.env.LINGJING_AWB_COMMAND_PREFIX || 'lj-awb';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function boolEnv(value) {
|
|
29
|
+
return ['1', 'true', 'yes', 'y', 'on'].includes(String(value ?? '').trim().toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readPackageJsonSync(packagePath) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fsSync.readFileSync(packagePath, 'utf8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function corePackagePath() {
|
|
41
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rootPackagePath() {
|
|
45
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readCurrentCliVersion() {
|
|
49
|
+
const pkg = JSON.parse(await fs.readFile(corePackagePath(), 'utf8'));
|
|
50
|
+
return pkg.version || 'unknown';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function currentRootDir() {
|
|
54
|
+
return path.dirname(rootPackagePath());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isLocalCheckout() {
|
|
58
|
+
return fsSync.existsSync(path.join(currentRootDir(), '.git'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveCliPackageName(kwargs = {}) {
|
|
62
|
+
const explicit = trimToNull(kwargs.packageName || kwargs.package || process.env.LINGJING_AWB_CLI_PACKAGE);
|
|
63
|
+
if (explicit) return explicit;
|
|
64
|
+
const rootPkg = readPackageJsonSync(rootPackagePath());
|
|
65
|
+
const name = trimToNull(rootPkg?.name);
|
|
66
|
+
return name?.startsWith('@lingjingai/lj-awb-cli') ? name : DEFAULT_CLI_PACKAGE_NAME;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveRegistry(kwargs = {}) {
|
|
70
|
+
return trimToNull(
|
|
71
|
+
kwargs.registry
|
|
72
|
+
|| process.env.LINGJING_AWB_UPDATE_REGISTRY
|
|
73
|
+
|| process.env.NPM_CONFIG_REGISTRY
|
|
74
|
+
|| process.env.npm_config_registry,
|
|
75
|
+
) || DEFAULT_NPM_REGISTRY;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateCheckIntervalMs() {
|
|
79
|
+
const raw = process.env.LINGJING_AWB_UPDATE_CHECK_INTERVAL_MS;
|
|
80
|
+
if (raw === undefined || raw === null || raw === '') return DEFAULT_CHECK_INTERVAL_MS;
|
|
81
|
+
return Math.max(0, toInt(raw, DEFAULT_CHECK_INTERVAL_MS));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateCheckDisabled() {
|
|
85
|
+
return boolEnv(process.env.LINGJING_AWB_DISABLE_UPDATE_CHECK)
|
|
86
|
+
|| boolEnv(process.env.AWB_DISABLE_UPDATE_CHECK)
|
|
87
|
+
|| ['0', 'false', 'off', 'no'].includes(String(process.env.LINGJING_AWB_UPDATE_CHECK ?? '').trim().toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function forceAutoUpdateCheck() {
|
|
91
|
+
return boolEnv(process.env.LINGJING_AWB_UPDATE_CHECK)
|
|
92
|
+
|| String(process.env.LINGJING_AWB_UPDATE_CHECK || '').trim().toLowerCase() === 'force';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseVersion(value) {
|
|
96
|
+
const text = String(value || '').trim().replace(/^v/i, '');
|
|
97
|
+
const [main, pre = ''] = text.split('-', 2);
|
|
98
|
+
const parts = main.split('.').map((item) => Number.parseInt(item, 10));
|
|
99
|
+
if (parts.some((item) => !Number.isFinite(item))) return null;
|
|
100
|
+
return { parts, pre };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function semverLessThan(a, b) {
|
|
104
|
+
const left = parseVersion(a);
|
|
105
|
+
const right = parseVersion(b);
|
|
106
|
+
if (!left || !right) return false;
|
|
107
|
+
for (let index = 0; index < 3; index += 1) {
|
|
108
|
+
const lv = left.parts[index] || 0;
|
|
109
|
+
const rv = right.parts[index] || 0;
|
|
110
|
+
if (lv < rv) return true;
|
|
111
|
+
if (lv > rv) return false;
|
|
112
|
+
}
|
|
113
|
+
if (left.pre && !right.pre) return true;
|
|
114
|
+
if (!left.pre && right.pre) return false;
|
|
115
|
+
return left.pre < right.pre;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function npmArgsWithRegistry(args, registry) {
|
|
119
|
+
return registry ? [...args, `--registry=${registry}`] : args;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function npmInstallSpec(packageName, version) {
|
|
123
|
+
return version && version !== 'latest' ? `${packageName}@${version}` : packageName;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runNpm(args, options = {}) {
|
|
127
|
+
try {
|
|
128
|
+
const result = await execFileAsync('npm', args, {
|
|
129
|
+
cwd: currentRootDir(),
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
timeout: options.timeoutMs || DEFAULT_NPM_TIMEOUT_MS,
|
|
132
|
+
maxBuffer: 1024 * 1024,
|
|
133
|
+
env: process.env,
|
|
134
|
+
});
|
|
135
|
+
return result;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const stderr = String(error.stderr || '').trim();
|
|
138
|
+
const stdout = String(error.stdout || '').trim();
|
|
139
|
+
throw new LingjingAwbCliError('npm 命令执行失败', {
|
|
140
|
+
type: 'network_error',
|
|
141
|
+
exitCode: 30,
|
|
142
|
+
hint: stderr || stdout || error.message || '请确认 npm 可用,并且当前网络可以访问 npm registry。',
|
|
143
|
+
details: {
|
|
144
|
+
command: `npm ${args.join(' ')}`,
|
|
145
|
+
code: error.code,
|
|
146
|
+
signal: error.signal,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function fetchLatestCliVersion(options = {}) {
|
|
153
|
+
const mocked = trimToNull(process.env.LINGJING_AWB_UPDATE_LATEST_VERSION);
|
|
154
|
+
if (mocked) return mocked;
|
|
155
|
+
const packageName = options.packageName || resolveCliPackageName(options);
|
|
156
|
+
const registry = options.registry || resolveRegistry(options);
|
|
157
|
+
const result = await runNpm(
|
|
158
|
+
npmArgsWithRegistry(['view', packageName, 'version'], registry),
|
|
159
|
+
{ timeoutMs: options.timeoutMs || DEFAULT_NPM_TIMEOUT_MS },
|
|
160
|
+
);
|
|
161
|
+
const version = String(result.stdout || '').trim().replace(/^"|"$/g, '');
|
|
162
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
|
163
|
+
throw new LingjingAwbCliError('无法识别 npm latest 版本', {
|
|
164
|
+
type: 'runtime_error',
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
hint: `npm view ${packageName} version 返回了非 semver 内容。`,
|
|
167
|
+
details: { version },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return version;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildUpdateNotice({ currentVersion, latestVersion, packageName, checkedAt }) {
|
|
174
|
+
const command = `${commandPrefix()} update`;
|
|
175
|
+
return {
|
|
176
|
+
message: `检测到 ${commandPrefix()} 新版本 ${latestVersion}(当前 ${currentVersion})。运行 ${command} 更新 CLI 和 skill。`,
|
|
177
|
+
command,
|
|
178
|
+
checkCommand: `${commandPrefix()} update --check`,
|
|
179
|
+
packageName,
|
|
180
|
+
currentVersion,
|
|
181
|
+
latestVersion,
|
|
182
|
+
checkedAt,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cachedNotice(state, currentVersion, packageName) {
|
|
187
|
+
const cached = state?.updateCheck;
|
|
188
|
+
if (!cached || cached.packageName !== packageName || cached.currentVersion !== currentVersion) return null;
|
|
189
|
+
if (!cached.latestVersion || !semverLessThan(currentVersion, cached.latestVersion)) return null;
|
|
190
|
+
return buildUpdateNotice({
|
|
191
|
+
currentVersion,
|
|
192
|
+
latestVersion: cached.latestVersion,
|
|
193
|
+
packageName,
|
|
194
|
+
checkedAt: cached.checkedAtText || null,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function shouldSkipAutoCheck() {
|
|
199
|
+
if (updateCheckDisabled()) return true;
|
|
200
|
+
if (forceAutoUpdateCheck()) return false;
|
|
201
|
+
if (process.env.CI) return true;
|
|
202
|
+
if (isLocalCheckout()) return true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function checkCliUpdate(options = {}) {
|
|
207
|
+
const currentVersion = options.currentVersion || await readCurrentCliVersion().catch(() => 'unknown');
|
|
208
|
+
const packageName = resolveCliPackageName(options);
|
|
209
|
+
if (!currentVersion || currentVersion === 'unknown' || shouldSkipAutoCheck()) return null;
|
|
210
|
+
|
|
211
|
+
const state = await loadState().catch(() => ({}));
|
|
212
|
+
const cached = state?.updateCheck || {};
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const intervalMs = options.force ? 0 : updateCheckIntervalMs();
|
|
215
|
+
const cacheHit = cached.packageName === packageName
|
|
216
|
+
&& cached.currentVersion === currentVersion
|
|
217
|
+
&& cached.checkedAt
|
|
218
|
+
&& (now - Number(cached.checkedAt) < intervalMs);
|
|
219
|
+
if (cacheHit) return cachedNotice(state, currentVersion, packageName);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const registry = resolveRegistry(options);
|
|
223
|
+
const latestVersion = await fetchLatestCliVersion({
|
|
224
|
+
packageName,
|
|
225
|
+
registry,
|
|
226
|
+
timeoutMs: options.timeoutMs || AUTO_CHECK_TIMEOUT_MS,
|
|
227
|
+
});
|
|
228
|
+
const checkedAtText = nowIso();
|
|
229
|
+
await saveState({
|
|
230
|
+
updateCheck: {
|
|
231
|
+
packageName,
|
|
232
|
+
registry,
|
|
233
|
+
currentVersion,
|
|
234
|
+
latestVersion,
|
|
235
|
+
checkedAt: now,
|
|
236
|
+
checkedAtText,
|
|
237
|
+
},
|
|
238
|
+
}).catch(() => {});
|
|
239
|
+
if (!semverLessThan(currentVersion, latestVersion)) return null;
|
|
240
|
+
return buildUpdateNotice({ currentVersion, latestVersion, packageName, checkedAt: checkedAtText });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
await saveState({
|
|
243
|
+
updateCheck: {
|
|
244
|
+
packageName,
|
|
245
|
+
currentVersion,
|
|
246
|
+
latestVersion: cached.latestVersion || null,
|
|
247
|
+
checkedAt: now,
|
|
248
|
+
checkedAtText: nowIso(),
|
|
249
|
+
error: error.message || String(error),
|
|
250
|
+
},
|
|
251
|
+
}).catch(() => {});
|
|
252
|
+
return cachedNotice({ updateCheck: cached }, currentVersion, packageName);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function tailLines(value, limit = 8) {
|
|
257
|
+
const lines = String(value || '').trim().split(/\r?\n/).filter(Boolean);
|
|
258
|
+
return lines.slice(Math.max(0, lines.length - limit));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function runCliUpdate(kwargs = {}) {
|
|
262
|
+
const currentVersion = await readCurrentCliVersion().catch(() => 'unknown');
|
|
263
|
+
const packageName = resolveCliPackageName(kwargs);
|
|
264
|
+
const registry = resolveRegistry(kwargs);
|
|
265
|
+
const latestVersion = await fetchLatestCliVersion({ packageName, registry });
|
|
266
|
+
const updateAvailable = currentVersion !== 'unknown' && semverLessThan(currentVersion, latestVersion);
|
|
267
|
+
const checkOnly = toBool(kwargs.check);
|
|
268
|
+
const force = toBool(kwargs.force);
|
|
269
|
+
const installVersion = latestVersion || 'latest';
|
|
270
|
+
const installSpec = npmInstallSpec(packageName, force ? 'latest' : installVersion);
|
|
271
|
+
const installArgs = npmArgsWithRegistry(['install', '-g', installSpec], registry);
|
|
272
|
+
|
|
273
|
+
if (checkOnly) {
|
|
274
|
+
return {
|
|
275
|
+
checked: true,
|
|
276
|
+
updated: false,
|
|
277
|
+
updateAvailable,
|
|
278
|
+
packageName,
|
|
279
|
+
currentVersion,
|
|
280
|
+
latestVersion,
|
|
281
|
+
command: updateAvailable ? `${commandPrefix()} update` : null,
|
|
282
|
+
message: updateAvailable
|
|
283
|
+
? `发现新版本 ${latestVersion},运行 ${commandPrefix()} update 更新。`
|
|
284
|
+
: '当前已是最新版本。',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!updateAvailable && !force) {
|
|
289
|
+
return {
|
|
290
|
+
checked: true,
|
|
291
|
+
updated: false,
|
|
292
|
+
updateAvailable: false,
|
|
293
|
+
packageName,
|
|
294
|
+
currentVersion,
|
|
295
|
+
latestVersion,
|
|
296
|
+
message: '当前已是最新版本。',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const result = await runNpm(installArgs, { timeoutMs: 120_000 });
|
|
301
|
+
await saveState({
|
|
302
|
+
updateCheck: {
|
|
303
|
+
packageName,
|
|
304
|
+
registry,
|
|
305
|
+
currentVersion: latestVersion,
|
|
306
|
+
latestVersion,
|
|
307
|
+
checkedAt: Date.now(),
|
|
308
|
+
checkedAtText: nowIso(),
|
|
309
|
+
},
|
|
310
|
+
}).catch(() => {});
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
checked: true,
|
|
314
|
+
updated: true,
|
|
315
|
+
updateAvailable,
|
|
316
|
+
packageName,
|
|
317
|
+
previousVersion: currentVersion,
|
|
318
|
+
currentVersion: latestVersion,
|
|
319
|
+
latestVersion,
|
|
320
|
+
command: `npm ${installArgs.join(' ')}`,
|
|
321
|
+
skillUpdated: true,
|
|
322
|
+
restartRecommended: true,
|
|
323
|
+
message: '更新完成。请退出并重新打开 AI Agent,以加载最新 CLI 和 skill。',
|
|
324
|
+
stdoutTail: tailLines(result.stdout),
|
|
325
|
+
stderrTail: tailLines(result.stderr),
|
|
326
|
+
};
|
|
327
|
+
}
|
package/skills/lj-awb/SKILL.md
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lj-awb
|
|
3
|
-
version: 0.
|
|
4
|
-
description: "灵境 AWB CLI skill。使用 `lj-awb` 命令调用动漫平台 / AWB 云端能力,覆盖认证、项目组、积分、模型发现、上传、统一 create
|
|
3
|
+
version: 0.4.5
|
|
4
|
+
description: "灵境 AWB CLI skill。使用 `lj-awb` 命令调用动漫平台 / AWB 云端能力,覆盖认证、项目组、积分、模型发现、上传、统一 create 创建域、任务查询、视频超分、最终产物 artifact CRUD 与本地 JSON 导入。用户说生图、生视频、视频超分、主体、音色、素材加白、去字幕、artifact 写入或查询时使用。正式生成、切换项目组、清空认证、artifact 写入等写入或扣费动作前必须确认。"
|
|
5
5
|
metadata:
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
bootstrap:
|
|
7
|
+
package: "@lingjingai/lj-awb-cli"
|
|
8
|
+
version: "0.4.5"
|
|
9
|
+
bin: "lj-awb"
|
|
8
10
|
cliHelp: "lj-awb --help"
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
# lj-awb CLI Skill
|
|
12
14
|
|
|
13
15
|
`lj-awb` 是确定性 CLI,skill 的职责是调度:少查、少问、少重跑,按 schema 和业务链条把命令串对。不要把本 skill 当成一堆命令片段来拼。
|
|
16
|
+
普通命令如果 JSON 输出里出现 `meta._notice.update`,先完成当前用户请求,再告诉用户当前版本和最新版本,并建议执行 `lj-awb update`;更新后提醒重新打开 AI Agent。
|
|
14
17
|
|
|
15
18
|
## 先读驱动器
|
|
16
19
|
|
|
@@ -21,31 +24,47 @@ metadata:
|
|
|
21
24
|
- 创作、批量、主体、音色、素材加白、artifact 的最短链条。
|
|
22
25
|
- 禁止清单:旧命令域、默认参数代决策、单条循环批量任务、直连后端等。
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
只在驱动器把你引到某个细节模块时才继续读该模块。不要为了“掌握全部功能”一次性加载所有模块。
|
|
25
28
|
|
|
26
29
|
## 入口命令
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
本 skill 是完整入口:如果用户只安装了 skill,也要先自举补齐 CLI。每个新环境第一次使用 `lj-awb` 前,先解析命令:
|
|
29
32
|
|
|
30
33
|
```bash
|
|
31
|
-
lj-awb
|
|
34
|
+
for dir in "$PWD/skills/lj-awb" "$HOME/.codex/skills/lj-awb" "$HOME/.claude/skills/lj-awb" "$HOME/.cc-switch/skills/lj-awb"; do
|
|
35
|
+
if [ -x "$dir/scripts/resolve-lj-awb-cmd.sh" ]; then
|
|
36
|
+
LINGJING_AWB_CMD="$(bash "$dir/scripts/resolve-lj-awb-cmd.sh")"
|
|
37
|
+
break
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
: "${LINGJING_AWB_CMD:?lj-awb bootstrap script not found}"
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
`resolve-lj-awb-cmd.sh` 会按顺序处理:
|
|
44
|
+
|
|
45
|
+
- 已存在兼容版本 `lj-awb`:直接返回命令路径。
|
|
46
|
+
- 未安装或版本低于 [`compat.json`](compat.json) 的 `minCliVersion`:自动执行 `npm install -g @lingjingai/lj-awb-cli@<minCliVersion>`。
|
|
47
|
+
- 用户要预发环境时,安装前设置 `LINGJING_AWB_CLI_PACKAGE=@lingjingai/lj-awb-cli-pre`。
|
|
48
|
+
- 缺少 npm / Node 时,提示用户先安装 Node.js `>=20`,再重试。
|
|
49
|
+
|
|
50
|
+
脚本 stdout 只输出可执行命令;安装日志和错误走 stderr。后续命令优先使用解析结果:
|
|
35
51
|
|
|
36
52
|
```bash
|
|
37
|
-
|
|
53
|
+
"$LINGJING_AWB_CMD" --help
|
|
38
54
|
```
|
|
39
55
|
|
|
40
56
|
非单条查询任务、命令不确定或涉及写入 / 扣费时,本轮只读一次机器契约:
|
|
41
57
|
|
|
42
58
|
```bash
|
|
43
|
-
|
|
59
|
+
"$LINGJING_AWB_CMD" schema --brief -f json
|
|
44
60
|
```
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
`schema --brief` 只用于掌握能力域、安全分组、业务路由和 agentContract。执行具体命令前,如果命令、参数、requiredOptions、safety 或 nextActions 不确定,再读精确契约,例如 `lj-awb schema --domain model --command video-models -f json`。不要把完整 `lj-awb schema -f json` 当默认入口;只有校验覆盖、脚本生成或 brief 缺失字段时才读完整 schema。
|
|
63
|
+
|
|
64
|
+
schema 查询必须先返回,再组织业务命令;不要把 schema 查询和猜测命令放进同一批并行调用。
|
|
47
65
|
|
|
48
66
|
如果用户已经给出完整只读命令,且该命令在本 skill 已知范围内,可以直接执行,不必为了形式补跑 schema。命令名、参数名、requiredOptions、safety、workflow.nextActions 都以 schema 为准。旧根域 `image` / `video` / `asset` / `subject` 不存在,不要尝试旧入口。
|
|
67
|
+
`update` 只做显式安装;`update --check` 只检查不安装。
|
|
49
68
|
|
|
50
69
|
## 能力地图
|
|
51
70
|
|
|
@@ -68,6 +87,7 @@ lj-awb schema -f json
|
|
|
68
87
|
| “有哪些模型 / 推荐模型 / 用某某模型” | [`modules/model.md`](modules/model.md),模型口语名默认是平台模型,不是本地工具 |
|
|
69
88
|
| “生图 / 生成图片 / 参考图生图” | [`modules/driver.md`](modules/driver.md) 创作链条 + [`modules/create-contract.md`](modules/create-contract.md) |
|
|
70
89
|
| “生视频 / 首帧 / 首尾帧 / 音频参考 / 让图动起来” | [`modules/driver.md`](modules/driver.md) 创作链条 + [`modules/task-manual.md`](modules/task-manual.md) |
|
|
90
|
+
| “超分 / 放大到 2K / 1080P” | [`modules/video.md`](modules/video.md),传 `objectName` 走 material 视频超分任务体系 |
|
|
71
91
|
| “固定角色 / 主体 / 同一个人 / 可灵或 Vidu 角色一致” | [`modules/subject.md`](modules/subject.md) |
|
|
72
92
|
| “主体音色 / 音色克隆 / 给主体配音色” | [`modules/subject.md`](modules/subject.md) |
|
|
73
93
|
| “素材加白 / 白名单 / 过审 / 素材组” | [`modules/asset.md`](modules/asset.md) |
|
|
@@ -86,8 +106,9 @@ lj-awb schema -f json
|
|
|
86
106
|
- `fee` 是最终估价,不是参数探索工具。不要为多个 quality / duration / ratio / 渠道组合反复跑 fee。
|
|
87
107
|
- 正式图片 / 视频任务要带 `--project-group-no`,除非命令 schema 没有该参数。
|
|
88
108
|
- 命令返回 `nextCommand`、`nextRefSubject`、`nextVoiceArg` 时优先复用返回值,不手拼。
|
|
89
|
-
-
|
|
109
|
+
- 默认输出是面向人类的 pretty 表格;Agent 不要解析 pretty,默认显式传 `-f text` 读取稳定的分区 key=value。只有脚本严格解析或必须读完整嵌套结构时才用 `-f json`。
|
|
90
110
|
- 模型口语名命中后必须过“候选展示门”:先把候选模型、真实参数取值和资源能力转成用户可见清单,再推荐或追问;不能只在内部读完 `model options` 就直接代选默认模型 / 参数。
|
|
111
|
+
- 候选展示门是 STOP gate:输出清单后必须停下等待用户选择模型和关键参数,或等待用户明确说“按默认 / 你来选”。在此之前不得继续 `model create-spec`、`fee`、`dry-run`、写 JSONL 或 `create`。
|
|
91
112
|
- 积分口径只把 `billingPointBalance` 当可扣积分余额;`projectBudgetBalance` 是项目组预算,不要混说。
|
|
92
113
|
- 不直连 material / asset / 外部服务;所有业务能力统一通过 `lj-awb`。
|
|
93
114
|
|
|
@@ -102,6 +123,8 @@ lj-awb schema -f json
|
|
|
102
123
|
|
|
103
124
|
## 安装与版本
|
|
104
125
|
|
|
126
|
+
如果用户只安装了 skill,第一次使用时会由 bootstrap 脚本自动安装 CLI。手动安装命令如下:
|
|
127
|
+
|
|
105
128
|
```bash
|
|
106
129
|
npm install -g @lingjingai/lj-awb-cli
|
|
107
130
|
```
|
package/skills/lj-awb/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.5
|
|
@@ -54,7 +54,7 @@ lj-awb artifact asset import --project-id <id> --input-dir 2_asset/output --dry-
|
|
|
54
54
|
|
|
55
55
|
- 单条 body 使用 AWB 接口的 camelCase 字段;不要在 body 里传 `id`、`projectId`、`createdAt`、`updatedAt`、`isDeleted` 等审计字段。
|
|
56
56
|
- 写入 / 删除都是逻辑写入,但仍需 `--dry-run` 预览 + `--yes` 确认两步走。
|
|
57
|
-
-
|
|
57
|
+
- 不确定参数时先读精确 schema,例如 `lj-awb schema --domain artifact --command "asset upsert-actor" -f json`。
|
|
58
58
|
- 输出字段速查见 [`../../references/output-fields.md`](../../references/output-fields.md) 的"Artifact"小节。
|
|
59
59
|
|
|
60
60
|
## 下一步
|
|
@@ -55,7 +55,7 @@ lj-awb artifact clip delete-episode --project-id <id> --video-episode-id 101 --d
|
|
|
55
55
|
|
|
56
56
|
- 单条 body 使用 AWB 接口的 camelCase 字段;不要在 body 里传 `id`、`projectId`、`createdAt`、`updatedAt`、`isDeleted` 等审计字段。
|
|
57
57
|
- 写入 / 删除都是逻辑写入,但仍需 `--dry-run` 预览 + `--yes` 确认两步走。
|
|
58
|
-
-
|
|
58
|
+
- 不确定参数时先读精确 schema,例如 `lj-awb schema --domain artifact --command "clip upsert-episode" -f json`。
|
|
59
59
|
- 输出字段速查见 [`../../references/output-fields.md`](../../references/output-fields.md) 的"Artifact"小节。
|
|
60
60
|
|
|
61
61
|
## 下一步
|
|
@@ -28,7 +28,7 @@ lj-awb artifact script import --project-id <id> --input-file script.json --dry-r
|
|
|
28
28
|
|
|
29
29
|
- 单条 body 使用 AWB 接口的 camelCase 字段;不要在 body 里传内部审计字段(`id`、`projectId`、`createdAt`、`updatedAt`、`isDeleted`),CLI 会自动注入或忽略。
|
|
30
30
|
- 写入 / 删除都是逻辑写入(不真删数据),但仍需 `--dry-run` 预览 + `--yes` 确认两步走。
|
|
31
|
-
-
|
|
31
|
+
- 不确定参数时先读精确 schema,例如 `lj-awb schema --domain artifact --command "script import" -f json`。
|
|
32
32
|
- 输出字段速查见 [`../../references/output-fields.md`](../../references/output-fields.md) 的"Artifact"小节。
|
|
33
33
|
|
|
34
34
|
## 下一步
|
|
@@ -55,7 +55,7 @@ lj-awb artifact video import-storyboard --project-id <id> --input-file ep001_sto
|
|
|
55
55
|
- 单条 body 使用 AWB 接口的 camelCase 字段;不要在 body 里传 `id`、`projectId`、`createdAt`、`updatedAt`、`isDeleted` 等审计字段。
|
|
56
56
|
- `upsert-episode` 返回的 `id` 是数字主键,下游 [`clip 子域`](clip.md) 的 `--video-episode-id` 必须用这个值,不是 `ep_001` 业务键。
|
|
57
57
|
- 写入 / 删除都是逻辑写入,但仍需 `--dry-run` 预览 + `--yes` 确认两步走。
|
|
58
|
-
-
|
|
58
|
+
- 不确定参数时先读精确 schema,例如 `lj-awb schema --domain artifact --command "video upsert-episode" -f json`。
|
|
59
59
|
- 输出字段速查见 [`../../references/output-fields.md`](../../references/output-fields.md) 的"Artifact"小节。
|
|
60
60
|
|
|
61
61
|
## 下一步
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
| `lj-awb create asset-group --platform JIMENG --name "<name>" --dry-run` | 预览创建素材组 |
|
|
13
13
|
| `lj-awb create asset-group --platform JIMENG --name "<name>" --yes` | 创建素材组(云端写入) |
|
|
14
14
|
| `lj-awb create asset-group-update --group-id <id> --platform JIMENG --name "<name>" --yes` | 改指定平台素材组名 |
|
|
15
|
-
| `lj-awb create asset --group-id <id> --platform JIMENG --url "asset-review/a.png" --name "女主正面" --yes` |
|
|
15
|
+
| `lj-awb create asset --group-id <id> --platform JIMENG --url "asset-review/a.png" --name "女主正面" --yes` | 注册已上传素材;CLI 自动按扩展名传 `assetType` |
|
|
16
16
|
| `lj-awb create asset --group-id <id> --platform JIMENG --file ./a.png --name "女主正面" --yes` | 先上传到 asset-review 场景再注册(一步完成) |
|
|
17
|
+
| `lj-awb create asset --group-id <id> --platform JIMENG --file ./clip.mp4 --name "参考视频" --dry-run` | 视频素材加白,自动传 `assetType=Video` |
|
|
18
|
+
| `lj-awb create asset --group-id <id> --platform JIMENG --file ./voice.wav --name "参考音频" --dry-run` | 音频素材加白,自动传 `assetType=Audio` |
|
|
19
|
+
| `lj-awb create asset --group-id <id> --platform JIMENG --file ./clip.mov --name "参考视频" --auto-convert --yes` | 本地素材不合法时自动转码为加白规格后上传 |
|
|
17
20
|
| `lj-awb create asset-match-actor --description "..." --tags-json '[{"tagId":"o_102"}]'` | 候选匹配(角色画风 / 标签) |
|
|
18
21
|
|
|
19
22
|
## 什么时候用 match-actor
|
|
@@ -47,6 +50,12 @@ lj-awb create video --model-group-code <videoModelGroupCode> \
|
|
|
47
50
|
|
|
48
51
|
- `create asset-group` / `create asset-group-update` / `create asset` 都是云端写入,**必须确认**后追加 `--yes`。
|
|
49
52
|
- `create asset-*` 平台必须显式传 `--platform JIMENG|BYTEPLUS`;不要依赖默认平台,不要传中文名、小写名或供应商别名。
|
|
53
|
+
- `create asset` 会自动判断并传入 `assetType=Image|Video|Audio`;不要让用户手填资产类型。本地 `--file` 会在提交前校验格式、大小、尺寸、时长、帧率等规则。
|
|
54
|
+
- 本地图片限制:格式 jpeg/jpg、png、webp、bmp、tiff/tif、gif、heic/heif;宽高比(宽/高)在 [0.4,2.5];宽和高都在 [300,6000] px;单张图片小于 30MB。
|
|
55
|
+
- 本地视频限制:格式 mp4、mov;时长 [2,15] 秒;宽高比(宽/高)在 [0.4,2.5];宽和高都在 [300,6000] px;总像素数在 [409600,927408];单个视频不超过 50MB;FPS 在 [24,60]。平台文档里的 480p / 720p 体现在自动转码目标会压到合法像素区间,不再用“短边必须等于 480/720”作为源文件硬过滤。
|
|
56
|
+
- 本地音频限制:格式 wav、mp3;时长 [2,15] 秒。
|
|
57
|
+
- 本地素材不合法时,`--dry-run` 不上传,会返回 `conversionRequired=true` 和 `conversionPlan`;正式执行会询问是否自动转码为合法规格后继续。非交互脚本可追加 `--auto-convert --yes`。
|
|
58
|
+
- 如果本机缺少 `ffprobe` / `ffmpeg`,CLI 在 macOS + Homebrew 环境下会自动运行 `brew install ffmpeg`;其他环境会提示安装命令。需要关闭自动安装时设置 `LINGJING_AWB_AUTO_INSTALL_FFPROBE=0`。
|
|
50
59
|
- `model asset-review-models` 只做发现,不自动创建资产组或素材;不要把 `modelGroupCode` 塞进资产创建命令。
|
|
51
60
|
- 不使用 `/assets/submissions` 这类隐式自动补组流程;资产组是否复用、是否新建由当前任务显式决定。
|
|
52
61
|
- 主体 element(视频里的"同一个人"概念)优先走 `create subject`,不要把素材组 / 素材 ID 当 subjectId 用——两者对应平台不同业务实体。
|
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
| `lj-awb auth verify` | 联网校验 access key 是否远端有效 |
|
|
11
11
|
| `lj-awb account info` | 联网确认 access key 有效,并查看当前用户 / 团队 / 项目组 |
|
|
12
12
|
| `lj-awb doctor --verify` | 联网体检认证、API、项目组和运行环境 |
|
|
13
|
-
| `lj-awb auth login
|
|
13
|
+
| `lj-awb auth login` | 浏览器授权登录:创建登录任务、展示链接、轮询直到授权成功并自动保存 access key |
|
|
14
|
+
| `lj-awb auth login --no-wait --json` | 只创建登录任务,返回 `flowId` / `verifyUrl`,不阻塞(AI agent / 脚本用) |
|
|
15
|
+
| `lj-awb auth login --flow-id <flowId>` | 复用已有登录任务继续轮询(配合 `--no-wait`,不要重复发起新 login) |
|
|
16
|
+
| `lj-awb auth login --access-key <key>` | 跳过浏览器授权,直接校验并保存 access key |
|
|
14
17
|
| `LINGJING_AWB_ACCESS_KEY=<key> lj-awb auth login` | 从环境变量保存 access key(CLI 自动读环境变量) |
|
|
15
18
|
| `lj-awb auth login --access-key <key> --skip-verify` | 仅保存,不联网校验 |
|
|
19
|
+
| `lj-awb auth logout` | 退出登录:清除本地保存的 access key(无需 `--yes`) |
|
|
16
20
|
| `lj-awb auth clear --dry-run` | 预览清空本地认证 |
|
|
17
21
|
| `lj-awb auth clear --yes` | 确认清空本地认证 |
|
|
18
22
|
|
|
@@ -21,6 +25,10 @@
|
|
|
21
25
|
- 自动化环境只使用 `LINGJING_AWB_ACCESS_KEY`。
|
|
22
26
|
- 不要把 access key 明文写入对话、日志、文档或任务台账。
|
|
23
27
|
- `auth status` 只读本地配置;正式创作前用 `auth verify`、`account info` 或 `doctor --verify` 确认远端可用。
|
|
28
|
+
- 浏览器授权登录(`auth login` 不带 `--access-key`)默认阻塞轮询,**最长约 10 分钟**等用户在浏览器完成授权;runner 的 timeout 需 ≥ 600s。
|
|
29
|
+
- AI agent / 无法实时看输出的场景:先 `auth login --no-wait --json` 拿 `flowId` + `verifyUrl` 给用户,待用户授权后再 `auth login --flow-id <flowId>` 续轮询;**不要重复发起新的 login**,否则会生成新 `flowId` 使旧链接失效。
|
|
30
|
+
- `auth logout` 退出登录:只清本地 access key,无需 `--yes`。
|
|
31
|
+
- 登录流程相关错误(`auth_flow_expired` / `auth_flow_canceled` / `auth_flow_pending`)见 [`../references/error-codes.md`](../references/error-codes.md)。
|
|
24
32
|
- `auth clear` 是本地破坏性动作,必须先确认或 dry-run。
|
|
25
33
|
- 错误恢复(exit 3 / auth_failed / auth_required)见 [`../references/error-codes.md`](../references/error-codes.md) 场景 4。
|
|
26
34
|
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
| `--generate-num` | 生图张数 | 仅图片模型支持时使用 |
|
|
14
14
|
| `--duration` | 生视频时长,单位秒 | 对应平台旧参数 `generated_time`,只能从允许值选择 |
|
|
15
15
|
| `--need-audio` | 是否需要输出音效 | 仅用户明确要求输出音效,且 `model options.params[key=needAudio]` 存在时使用;不是上传音频入口。模型未暴露该 param 时**不要为了"显式关闭"传 `--need-audio false`**,CLI 会以 `argument_error: 模型不支持参数:needAudio` 拒绝;不传等价于不开音效 |
|
|
16
|
+
| `--model-param key=value` | 新增 / 未硬编码的模型配置参数 | key 必须来自 `model options.params[]`;可重复传。适合 `generation_effort=high` 这类通用 Enum / Boolean / Number / String 配置 |
|
|
17
|
+
| `--model-params-json` | 一次传多个通用模型配置参数 | JSON 对象或 JSON 文件路径;命令行专用参数如 `--ratio`、`--quality`、`--duration` 优先级更高 |
|
|
16
18
|
| `--resource` | 单个素材输入,可重复传 | `type:usage[:key]=path|url|asset:<id>` |
|
|
17
19
|
| `--resources-json` | 多素材输入,适合复杂 keyframe / 批量构造 | JSON 数组或 JSON 文件路径 |
|
|
18
20
|
| `--project-group-no` | 任务归属项目组 | 用户明确指定,或来自当前项目组 |
|
|
@@ -94,6 +96,7 @@ JSON 语法:
|
|
|
94
96
|
| `quality` | `--quality` | `promptParams.quality` | `quality` |
|
|
95
97
|
| `generate_num` | `--generate-num` | `promptParams.generate_num` | `generate_num` |
|
|
96
98
|
| `generated_time` | `--duration` | `promptParams.duration` | `generated_time` |
|
|
99
|
+
| 其他带 `cliArg` 的模型配置参数 | `--model-param <key>=...` / `--model-params-json` | `promptParams.<key>` | 同名透传 |
|
|
97
100
|
| `iref` | `--resource image:reference=...` | `promptParams.resources[]` | `iref` |
|
|
98
101
|
| `frames` | `--resource image:first_frame=...` | `promptParams.resources[]` | `frames` |
|
|
99
102
|
| `multi_param` | `--resource image/video/audio/subject:reference[:key]=...` | `promptParams.resources[]` | `multi_param` |
|
|
@@ -117,11 +120,11 @@ Agent 必须按顺序做:
|
|
|
117
120
|
5. 若本轮已读取 `model create-spec` 则直接复用;否则运行 `model create-spec --model-group-code <code> -f json`。
|
|
118
121
|
6. 先读 `inputRequirement`,确认是否必须提供视觉输入。
|
|
119
122
|
7. 用 `supportedIntents[]` 匹配用户意图;不要只看模型列表摘要就生成最终能力结论。
|
|
120
|
-
8. 参数枚举值只从 `model options.params[].values` 选择,素材限制只从 `model options.resources[]` 校验,参数 / 资源联动限制按 `model options.constraints[]`
|
|
123
|
+
8. 参数枚举值只从 `model options.params[].values` 选择,素材限制只从 `model options.resources[]` 校验,参数 / 资源联动限制按 `model options.constraints[]` 收窄。新增通用参数只要在 `params[]` 中出现且带 `cliArg`,就通过 `--model-param` / `--model-params-json` 显式传入。
|
|
121
124
|
9. 不主动列举所有缺失控制项;只追问会影响价格 / 效果的关键参数。视频包括 `quality`、`duration`、约束后仍可选的 `ratio`,以及用户明确要求输出音效时的 `needAudio`;图片包括 `quality`、`ratio`、`generateNum`。
|
|
122
125
|
10. 组装最终 prompt;如果可见文本发生变化,先展示给用户。
|
|
123
126
|
11. 若用户上传或指定音频文件,必须先确认 `model options.resources[]` 中存在 `mediaType=AUDIO usage=reference` 后再组织 `audio:reference` 资源;只有用户明确要求“输出是否带模型生成音效/音频”时,才检查 `model options.params[]` 是否存在 `needAudio`。
|
|
124
|
-
12. 用户未提供关键参数时,把 `model options.params[].values` 和 `defaultValue` 转成候选问题;用户选择或确认“按默认”前,不跑 `create image-fee` / `create video-fee`,也不跑 `create --dry-run
|
|
127
|
+
12. 用户未提供关键参数时,把 `model options.params[].values` 和 `defaultValue` 转成候选问题;用户选择或确认“按默认”前,不跑 `create image-fee` / `create video-fee`,也不跑 `create --dry-run`。即使模型给了 `defaultValue`,也不要静默写入请求。
|
|
125
128
|
13. 跑 `create image-fee` / `create video-fee` 获取预估积分——**只跑一次,参数取用户已确认的那组**。`fee` 是"用户敲定参数后的最终估价",不是参数探索 / 候选比较工具:不要为了对比价格档跑 `fee` × 多个 `quality` / `duration` / `ratio` 组合,也不要为了横向比同款不同渠道跑 `fee` × 多个 `modelGroupCode`;价格档差异从 `model options.feeCalcType` + 参数取值表里就能口算/展示给用户,比较应该发生在 `model options` 阶段。
|
|
126
129
|
14. 跑 `create --dry-run` 检查请求体和本地素材。
|
|
127
130
|
15. 用户确认模型、项目组、最终 prompt 文本、素材、关键参数和积分后,才追加 `--yes` 正式提交。
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
| `lj-awb create video ...` | 提交单条生视频任务 |
|
|
13
13
|
| `lj-awb create video-batch ...` | 批量提交生视频任务 |
|
|
14
14
|
| `lj-awb create video-fee ...` | 生视频估价;关键参数确认后只跑一次 |
|
|
15
|
+
| `lj-awb create video-super-resolution ...` | 基于 objectName 提交视频超分任务 |
|
|
16
|
+
| `lj-awb create video-super-resolution-fee ...` | 视频超分估价;先确认 objectName |
|
|
15
17
|
| `lj-awb create video-subtitle-removal --source-task-id <videoTaskId> ...` | 基于 material 来源视频任务提交去字幕 |
|
|
16
18
|
| `lj-awb create subject-list ...` | 查询可复用主体,避免重复发布 |
|
|
17
19
|
| `lj-awb create subject --model-code tx|vidu ...` | 创建可复用主体 element |
|
|
@@ -30,8 +32,8 @@
|
|
|
30
32
|
## 编排
|
|
31
33
|
|
|
32
34
|
- 图片 / 视频创建参数和资源绑定仍按 [`create-contract.md`](create-contract.md) 校验。
|
|
33
|
-
- 估价也在 `create` 域:`create image-fee` / `create video-fee`;只在关键参数确认后执行。
|
|
34
|
-
- 状态和等待仍在查询域:`task image-status` / `task video-status` / `task wait` / `create subject-wait` / `create subject-voice-wait` / `task video-subtitle-status`。
|
|
35
|
+
- 估价也在 `create` 域:`create image-fee` / `create video-fee` / `create video-super-resolution-fee`;只在关键参数确认后执行。
|
|
36
|
+
- 状态和等待仍在查询域:`task image-status` / `task video-status` / `task wait` / `create subject-wait` / `create subject-voice-wait` / `task video-subtitle-status` / `task video-super-resolution-status`。
|
|
35
37
|
- 所有 `create` 写入或扣费命令正式执行前必须先 `--dry-run`,用户确认后再 `--yes`。
|
|
36
38
|
- 查询型 create 子命令(`subject-list`、`subject-voice-list`、`asset-groups`、`asset-group-get`、`asset-match-actor`)是为了避免重复创建;创建前优先查重。
|
|
37
39
|
- `asset-groups` / `asset-group-get` / `asset-group` / `asset-group-update` / `asset` 都必须显式传 `--platform`;平台先由 `model asset-review-models` 或用户选择确定。
|