@playcraft/cli 0.0.31 → 0.0.32
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/dist/commands/prefab.js +51 -184
- package/dist/commands/recommend.js +227 -0
- package/dist/index.js +3 -0
- package/dist/utils/agent-api-client.js +0 -12
- package/package.json +2 -2
package/dist/commands/prefab.js
CHANGED
|
@@ -2,7 +2,7 @@ import { inspect } from 'node:util';
|
|
|
2
2
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import JSON5 from 'json5';
|
|
5
|
-
import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest,
|
|
5
|
+
import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest, resolveGameConfigPath, validateThemeValue, validateConfigValue, describeJsonSchemaFields, describeConfigSchemaFields, getPrefabFieldDescriptors, getPrefabFieldDiffs, prefabHasSchemaDiff, } from '@playcraft/common/prefab';
|
|
6
6
|
const DEFAULT_PAGE_LIMIT = 50;
|
|
7
7
|
/** 无法识别 External / PlayCanvas 项目类型(供测试在 mock process.exit 后通过 instanceof 识别)。 */
|
|
8
8
|
export class ProjectDetectError extends Error {
|
|
@@ -266,99 +266,22 @@ function scanExtensionMetaData(projectDir) {
|
|
|
266
266
|
}
|
|
267
267
|
return map;
|
|
268
268
|
}
|
|
269
|
-
/** Write a single field's value into MetaData.json configSchema.{field}.default */
|
|
270
|
-
function writeMetaDataDefault(projectDir, extensionName, field, value) {
|
|
271
|
-
const metaPath = join(projectDir, LITE_CREATOR_EXTENSIONS_DIR, extensionName, 'MetaData.json');
|
|
272
|
-
const raw = readFileSync(metaPath, 'utf-8');
|
|
273
|
-
const metaData = JSON.parse(raw);
|
|
274
|
-
const configSchema = metaData.configSchema;
|
|
275
|
-
if (!configSchema?.[field]) {
|
|
276
|
-
throw new Error(`MetaData.json for "${extensionName}" has no configSchema.${field}`);
|
|
277
|
-
}
|
|
278
|
-
configSchema[field].default = value;
|
|
279
|
-
writeFileSync(metaPath, JSON.stringify(metaData, null, 2), 'utf-8');
|
|
280
|
-
}
|
|
281
|
-
/** Write multiple fields' values into MetaData.json configSchema.*.default */
|
|
282
|
-
function writeMetaDataDefaults(projectDir, extensionName, values) {
|
|
283
|
-
const metaPath = join(projectDir, LITE_CREATOR_EXTENSIONS_DIR, extensionName, 'MetaData.json');
|
|
284
|
-
const raw = readFileSync(metaPath, 'utf-8');
|
|
285
|
-
const metaData = JSON.parse(raw);
|
|
286
|
-
const configSchema = metaData.configSchema;
|
|
287
|
-
if (!configSchema) {
|
|
288
|
-
throw new Error(`MetaData.json for "${extensionName}" has no configSchema`);
|
|
289
|
-
}
|
|
290
|
-
for (const [field, value] of Object.entries(values)) {
|
|
291
|
-
if (configSchema[field]) {
|
|
292
|
-
configSchema[field].default = value;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
writeFileSync(metaPath, JSON.stringify(metaData, null, 2), 'utf-8');
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Get the main scene ID from manifest.json.
|
|
299
|
-
*
|
|
300
|
-
* Priority:
|
|
301
|
-
* 1. Scene with isMain: true
|
|
302
|
-
* 2. If only one scene exists, use that scene
|
|
303
|
-
* 3. If multiple scenes without isMain, return null (caller should fallback)
|
|
304
|
-
*/
|
|
305
|
-
function getMainSceneId(projectDir) {
|
|
306
|
-
if (!fileExists(projectDir, MANIFEST_PATH))
|
|
307
|
-
return null;
|
|
308
|
-
try {
|
|
309
|
-
const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
|
|
310
|
-
const scenes = manifestJson?.scenes;
|
|
311
|
-
if (!Array.isArray(scenes) || scenes.length === 0)
|
|
312
|
-
return null;
|
|
313
|
-
// Priority 1: Find scene with isMain: true
|
|
314
|
-
const mainScene = scenes.find((s) => s.isMain === true);
|
|
315
|
-
if (mainScene?.id)
|
|
316
|
-
return String(mainScene.id);
|
|
317
|
-
// Priority 2: If only one scene, use it
|
|
318
|
-
if (scenes.length === 1 && scenes[0]?.id) {
|
|
319
|
-
return String(scenes[0].id);
|
|
320
|
-
}
|
|
321
|
-
// Multiple scenes without isMain → cannot determine
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Resolve the GameConfig file path for a given variant (scene).
|
|
330
|
-
*
|
|
331
|
-
* If variantId is not provided, automatically detect the current scene:
|
|
332
|
-
* 1. Scene with isMain: true in manifest.json
|
|
333
|
-
* 2. If only one scene exists, use that scene
|
|
334
|
-
* 3. If multiple scenes without isMain, return null (fallback to MetaData.json)
|
|
335
|
-
*/
|
|
336
269
|
function resolveGameConfigPathForVariant(projectDir, variantId) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (!
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
return null;
|
|
343
|
-
const sceneJson = JSON.parse(readLocalFile(projectDir, scenePath(sceneId)));
|
|
270
|
+
if (!variantId)
|
|
271
|
+
return DEFAULT_GAME_PATH;
|
|
272
|
+
if (!fileExists(projectDir, scenePath(variantId)))
|
|
273
|
+
return DEFAULT_GAME_PATH;
|
|
274
|
+
const sceneJson = JSON.parse(readLocalFile(projectDir, scenePath(variantId)));
|
|
344
275
|
if (!fileExists(projectDir, ASSETS_JSON_PATH))
|
|
345
|
-
return
|
|
276
|
+
return DEFAULT_GAME_PATH;
|
|
346
277
|
const assetsJson = JSON.parse(readLocalFile(projectDir, ASSETS_JSON_PATH));
|
|
347
|
-
|
|
278
|
+
const resolved = resolveGameConfigPath(sceneJson, assetsJson);
|
|
279
|
+
return resolved ?? DEFAULT_GAME_PATH;
|
|
348
280
|
}
|
|
349
281
|
function listLocalScenes(projectDir) {
|
|
350
282
|
if (!fileExists(projectDir, MANIFEST_PATH))
|
|
351
283
|
return [];
|
|
352
284
|
const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
|
|
353
|
-
if (fileExists(projectDir, ASSETS_JSON_PATH)) {
|
|
354
|
-
const assetsJson = JSON.parse(readLocalFile(projectDir, ASSETS_JSON_PATH));
|
|
355
|
-
return listScenesWithGameConfig(manifestJson, (sceneId) => {
|
|
356
|
-
const sp = scenePath(sceneId);
|
|
357
|
-
if (!fileExists(projectDir, sp))
|
|
358
|
-
return null;
|
|
359
|
-
return JSON.parse(readLocalFile(projectDir, sp));
|
|
360
|
-
}, assetsJson);
|
|
361
|
-
}
|
|
362
285
|
return listScenesFromManifest(manifestJson);
|
|
363
286
|
}
|
|
364
287
|
function loadPrefabs(projectDir, systemType, opts) {
|
|
@@ -368,22 +291,11 @@ function loadPrefabs(projectDir, systemType, opts) {
|
|
|
368
291
|
const data = loadThemeData(projectDir, themeId);
|
|
369
292
|
return parseThemePrefabs(schema, data, themeId);
|
|
370
293
|
}
|
|
371
|
-
const metaMap = scanExtensionMetaData(projectDir);
|
|
372
294
|
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return buildExtensionPrefabs(metaMap, { extensions, config }, configPath);
|
|
378
|
-
}
|
|
379
|
-
// 无场景级 GameConfig → 从 DefaultGame.json 只读 Extensions 列表(不读 Config 值)
|
|
380
|
-
if (fileExists(projectDir, DEFAULT_GAME_PATH)) {
|
|
381
|
-
const defaultGameJson = loadGameConfig(projectDir, DEFAULT_GAME_PATH);
|
|
382
|
-
const { extensions } = parseGameConfig(defaultGameJson);
|
|
383
|
-
return buildExtensionPrefabs(metaMap, { extensions, config: {} }, DEFAULT_GAME_PATH);
|
|
384
|
-
}
|
|
385
|
-
// 完全无 GameConfig → 仅根据 MetaData 构建(全部标记为未启用)
|
|
386
|
-
return buildExtensionPrefabs(metaMap, { extensions: [], config: {} }, '');
|
|
295
|
+
const gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
296
|
+
const { extensions, config } = parseGameConfig(gameConfigJson);
|
|
297
|
+
const metaMap = scanExtensionMetaData(projectDir);
|
|
298
|
+
return buildExtensionPrefabs(metaMap, { extensions, config }, configPath);
|
|
387
299
|
}
|
|
388
300
|
// ─── dotpath helper ──────────────────────────────────────────────────────────
|
|
389
301
|
function getByDotPath(obj, path) {
|
|
@@ -520,17 +432,8 @@ export function registerPrefabCommands(program) {
|
|
|
520
432
|
return;
|
|
521
433
|
}
|
|
522
434
|
for (const s of scenes) {
|
|
523
|
-
const mark = s.isActive ? ' *' : '';
|
|
524
435
|
const gc = s.gameConfigPath ? ` [${s.gameConfigPath}]` : '';
|
|
525
|
-
console.log(` ${s.name} (${s.id})${
|
|
526
|
-
}
|
|
527
|
-
if (scenes.some((s) => s.isActive)) {
|
|
528
|
-
console.log('');
|
|
529
|
-
console.log('标 * 为 manifest 中的主场景(isMain)。其他场景请用 --variant <sceneId> 指定上下文。');
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
console.log('');
|
|
533
|
-
console.log('(manifest 未标记主场景;操作其他场景请用 --variant <sceneId>)');
|
|
436
|
+
console.log(` ${s.name} (${s.id})${gc}`);
|
|
534
437
|
}
|
|
535
438
|
});
|
|
536
439
|
}
|
|
@@ -915,12 +818,18 @@ export function registerPrefabCommands(program) {
|
|
|
915
818
|
});
|
|
916
819
|
}
|
|
917
820
|
else {
|
|
821
|
+
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
822
|
+
const gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
823
|
+
const { config } = parseGameConfig(gameConfigJson);
|
|
918
824
|
const metaMap = scanExtensionMetaData(projectDir);
|
|
919
825
|
const meta = metaMap.get(key);
|
|
920
826
|
if (!meta) {
|
|
921
827
|
console.error(`Error: extension "${key}" 不存在。`);
|
|
922
828
|
process.exit(1);
|
|
923
829
|
}
|
|
830
|
+
const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
|
|
831
|
+
const existingKey = findConfigKeyCaseInsensitive(config, configKey);
|
|
832
|
+
const currentValues = (existingKey ? config[existingKey] : {}) ?? {};
|
|
924
833
|
const fieldDef = meta.configSchema?.[field];
|
|
925
834
|
const fieldType = fieldDef?.type ?? 'string';
|
|
926
835
|
const coerced = coerceValue(rawValue, fieldType);
|
|
@@ -931,22 +840,9 @@ export function registerPrefabCommands(program) {
|
|
|
931
840
|
process.exit(1);
|
|
932
841
|
}
|
|
933
842
|
}
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
938
|
-
const { config } = parseGameConfig(gameConfigJson);
|
|
939
|
-
const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
|
|
940
|
-
const existingKey = findConfigKeyCaseInsensitive(config, configKey);
|
|
941
|
-
const currentValues = (existingKey ? config[existingKey] : {}) ?? {};
|
|
942
|
-
const newValues = { ...currentValues, [field]: coerced };
|
|
943
|
-
const updated = updateGameConfigValue(gameConfigJson, configKey, newValues);
|
|
944
|
-
writeGameConfig(projectDir, configPath, updated);
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
// 无场景级 GameConfig → 写 MetaData.json 的 configSchema.{field}.default
|
|
948
|
-
writeMetaDataDefault(projectDir, key, field, coerced);
|
|
949
|
-
}
|
|
843
|
+
const newValues = { ...currentValues, [field]: coerced };
|
|
844
|
+
const updated = updateGameConfigValue(gameConfigJson, configKey, newValues);
|
|
845
|
+
writeGameConfig(projectDir, configPath, updated);
|
|
950
846
|
const payload = { success: true, key, field, value: coerced };
|
|
951
847
|
outputResult(asJson, payload, () => {
|
|
952
848
|
console.log(`已更新 ${key}.${field} = ${formatHumanValue(coerced)}`);
|
|
@@ -1044,67 +940,38 @@ export function registerPrefabCommands(program) {
|
|
|
1044
940
|
}
|
|
1045
941
|
else {
|
|
1046
942
|
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
943
|
+
let gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
1047
944
|
const metaMap = scanExtensionMetaData(projectDir);
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
for (const [extKey, fieldMap] of Object.entries(batch)) {
|
|
1052
|
-
if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
|
|
1053
|
-
throw new Error(`extension "${extKey}" 的值必须是对象(字段 map)`);
|
|
1054
|
-
}
|
|
1055
|
-
const meta = metaMap.get(extKey);
|
|
1056
|
-
if (!meta) {
|
|
1057
|
-
throw new Error(`extension "${extKey}" 不存在`);
|
|
1058
|
-
}
|
|
1059
|
-
const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
|
|
1060
|
-
const { config } = parseGameConfig(gameConfigJson);
|
|
1061
|
-
const existingKey = findConfigKeyCaseInsensitive(config, configKey);
|
|
1062
|
-
let currentValues = (existingKey ? config[existingKey] : {}) ?? {};
|
|
1063
|
-
currentValues = typeof currentValues === 'object' && !Array.isArray(currentValues)
|
|
1064
|
-
? { ...currentValues }
|
|
1065
|
-
: {};
|
|
1066
|
-
for (const [field, rawVal] of Object.entries(fieldMap)) {
|
|
1067
|
-
const fieldDef = meta.configSchema?.[field];
|
|
1068
|
-
const fieldType = fieldDef?.type ?? 'string';
|
|
1069
|
-
const coerced = coerceBatchLeaf(rawVal, fieldType);
|
|
1070
|
-
if (fieldDef) {
|
|
1071
|
-
const result = validateConfigValue(fieldDef, coerced);
|
|
1072
|
-
if (!result.valid) {
|
|
1073
|
-
throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
currentValues = setByDotPath(currentValues, field, coerced);
|
|
1077
|
-
}
|
|
1078
|
-
gameConfigJson = updateGameConfigValue(gameConfigJson, configKey, currentValues);
|
|
945
|
+
for (const [extKey, fieldMap] of Object.entries(batch)) {
|
|
946
|
+
if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
|
|
947
|
+
throw new Error(`extension "${extKey}" 的值必须是对象(字段 map)`);
|
|
1079
948
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
if (
|
|
1098
|
-
|
|
1099
|
-
if (!result.valid) {
|
|
1100
|
-
throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
|
|
1101
|
-
}
|
|
949
|
+
const meta = metaMap.get(extKey);
|
|
950
|
+
if (!meta) {
|
|
951
|
+
throw new Error(`extension "${extKey}" 不存在`);
|
|
952
|
+
}
|
|
953
|
+
const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
|
|
954
|
+
const { config } = parseGameConfig(gameConfigJson);
|
|
955
|
+
const existingKey = findConfigKeyCaseInsensitive(config, configKey);
|
|
956
|
+
let currentValues = (existingKey ? config[existingKey] : {}) ?? {};
|
|
957
|
+
currentValues = typeof currentValues === 'object' && !Array.isArray(currentValues)
|
|
958
|
+
? { ...currentValues }
|
|
959
|
+
: {};
|
|
960
|
+
for (const [field, rawVal] of Object.entries(fieldMap)) {
|
|
961
|
+
const fieldDef = meta.configSchema?.[field];
|
|
962
|
+
const fieldType = fieldDef?.type ?? 'string';
|
|
963
|
+
const coerced = coerceBatchLeaf(rawVal, fieldType);
|
|
964
|
+
if (fieldDef) {
|
|
965
|
+
const result = validateConfigValue(fieldDef, coerced);
|
|
966
|
+
if (!result.valid) {
|
|
967
|
+
throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
|
|
1102
968
|
}
|
|
1103
|
-
coercedValues[field] = coerced;
|
|
1104
969
|
}
|
|
1105
|
-
|
|
970
|
+
currentValues = setByDotPath(currentValues, field, coerced);
|
|
1106
971
|
}
|
|
972
|
+
gameConfigJson = updateGameConfigValue(gameConfigJson, configKey, currentValues);
|
|
1107
973
|
}
|
|
974
|
+
writeGameConfig(projectDir, configPath, gameConfigJson);
|
|
1108
975
|
}
|
|
1109
976
|
const payload = { success: true, updatedKeys: Object.keys(batch) };
|
|
1110
977
|
outputResult(asJson, payload, () => {
|
|
@@ -1144,7 +1011,7 @@ export function registerPrefabCommands(program) {
|
|
|
1144
1011
|
});
|
|
1145
1012
|
}
|
|
1146
1013
|
else {
|
|
1147
|
-
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant)
|
|
1014
|
+
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
1148
1015
|
const gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
1149
1016
|
const { extensions } = parseGameConfig(gameConfigJson);
|
|
1150
1017
|
if (extensions.includes(key)) {
|
|
@@ -1188,7 +1055,7 @@ export function registerPrefabCommands(program) {
|
|
|
1188
1055
|
});
|
|
1189
1056
|
}
|
|
1190
1057
|
else {
|
|
1191
|
-
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant)
|
|
1058
|
+
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
1192
1059
|
const gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
1193
1060
|
const { extensions } = parseGameConfig(gameConfigJson);
|
|
1194
1061
|
const filtered = extensions.filter((n) => n !== key);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
3
|
+
function handleError(err) {
|
|
4
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5
|
+
console.error(`Error: ${msg}`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
function output(data, opts) {
|
|
9
|
+
const text = JSON.stringify(data, null, opts.pretty ? 2 : 0);
|
|
10
|
+
if (opts.output) {
|
|
11
|
+
writeFileSync(opts.output, text);
|
|
12
|
+
console.error(`Wrote ${opts.output}`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
process.stdout.write(text + '\n');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parseTagsArg(raw) {
|
|
19
|
+
if (!raw)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
24
|
+
throw new Error('tags must be a JSON object');
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
throw new Error(`Invalid --tags JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseIntOpt(label, raw) {
|
|
33
|
+
if (raw === undefined || raw === '')
|
|
34
|
+
return undefined;
|
|
35
|
+
const n = Number.parseInt(raw, 10);
|
|
36
|
+
if (!Number.isFinite(n))
|
|
37
|
+
throw new Error(`Invalid ${label}: "${raw}" is not a valid integer`);
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 注册 `playcraft recommend <subcommand>` 命令组。
|
|
42
|
+
*
|
|
43
|
+
* Subcommands(全部调 /api/agent/tools/recommend-*):
|
|
44
|
+
* meta 打印当前数据快照的渠道 / 指标 / 维度元数据
|
|
45
|
+
* data 下载完整 RecommendData 快照
|
|
46
|
+
* similar 策略一: Top-N 相似 Creative + KNN 预估
|
|
47
|
+
* improvements 策略二: R1(可选 R2) 的 Tag 改进建议 + diff
|
|
48
|
+
* random 策略三: 批量随机生成 Tag 组合
|
|
49
|
+
* optimal 辅助: 理论最优 Tag 组合(perDim ω² 排序)
|
|
50
|
+
* tag-to-atom 辅助: Tag → Atom slot 映射
|
|
51
|
+
*/
|
|
52
|
+
export function registerRecommendCommands(program) {
|
|
53
|
+
const rec = program
|
|
54
|
+
.command('recommend')
|
|
55
|
+
.description('Tag → Atom 推荐(后端 /api/agent/tools/recommend-*);需 .playcraft.json 或环境变量');
|
|
56
|
+
// ─── meta / data ───────────────────────────────────────────
|
|
57
|
+
rec
|
|
58
|
+
.command('meta')
|
|
59
|
+
.description('打印当前数据快照的元数据(渠道 / 指标 / 维度)')
|
|
60
|
+
.option('--pretty', '美化 JSON 输出')
|
|
61
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
try {
|
|
64
|
+
const client = new AgentApiClient();
|
|
65
|
+
const result = await client.get('/recommend-meta');
|
|
66
|
+
output(result, opts);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
handleError(e);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
rec
|
|
73
|
+
.command('data')
|
|
74
|
+
.description('下载完整 RecommendData 快照(含 creatives/channelStats/dimValueStats)')
|
|
75
|
+
.option('--pretty', '美化 JSON 输出')
|
|
76
|
+
.option('--output <file>', '写入文件而非 stdout', '')
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
try {
|
|
79
|
+
const client = new AgentApiClient();
|
|
80
|
+
const result = await client.get('/recommend-data');
|
|
81
|
+
output(result, opts);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
handleError(e);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// ─── 策略一: similar ───────────────────────────────────────
|
|
88
|
+
rec
|
|
89
|
+
.command('similar')
|
|
90
|
+
.description('策略一: 相似 Creative + KNN 预估')
|
|
91
|
+
.requiredOption('--channel <name>', '投放渠道(Applovin|Facebook|Google|Moloco|Unity)')
|
|
92
|
+
.option('--metric <name>', '指标(ctr|cvr|cpi|ipm|cost|installs)', 'ctr')
|
|
93
|
+
.option('--creative <id>', '种子素材 ID(与 --tags 二选一)')
|
|
94
|
+
.option('--tags <json>', '种子 tags JSON(与 --creative 二选一)')
|
|
95
|
+
.option('--top <n>', 'Top-N 相似数量', '5')
|
|
96
|
+
.option('--k <n>', 'KNN 邻居数', '5')
|
|
97
|
+
.option('--no-knn', '不返回 KNN 预估')
|
|
98
|
+
.option('--include-atoms', '为每个相似项返回 atoms 配置')
|
|
99
|
+
.option('--pretty', '美化 JSON 输出')
|
|
100
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
101
|
+
.action(async (opts) => {
|
|
102
|
+
try {
|
|
103
|
+
if (!opts.creative && !opts.tags) {
|
|
104
|
+
throw new Error('Either --creative <id> or --tags <json> must be provided');
|
|
105
|
+
}
|
|
106
|
+
const body = {
|
|
107
|
+
channel: opts.channel,
|
|
108
|
+
metric: opts.metric,
|
|
109
|
+
creativeId: opts.creative,
|
|
110
|
+
tags: parseTagsArg(opts.tags),
|
|
111
|
+
topN: parseIntOpt('--top', opts.top),
|
|
112
|
+
k: parseIntOpt('--k', opts.k),
|
|
113
|
+
includeKnn: opts.knn,
|
|
114
|
+
includeAtoms: !!opts.includeAtoms,
|
|
115
|
+
};
|
|
116
|
+
const client = new AgentApiClient();
|
|
117
|
+
const result = await client.post('/recommend-similar', body);
|
|
118
|
+
output(result, opts);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
handleError(e);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// ─── 策略二: improvements ──────────────────────────────────
|
|
125
|
+
rec
|
|
126
|
+
.command('improvements')
|
|
127
|
+
.description('策略二: R1 的 Tag 改进建议 + 可选 R2 diff')
|
|
128
|
+
.requiredOption('--channel <name>', '投放渠道')
|
|
129
|
+
.option('--metric <name>', '指标(ctr|cvr|cpi|ipm|cost|installs)', 'ctr')
|
|
130
|
+
.requiredOption('--r1 <id>', '当前素材 ID')
|
|
131
|
+
.option('--r2 <id>', '对照标杆素材 ID(可选)')
|
|
132
|
+
.option('--pretty', '美化 JSON 输出')
|
|
133
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
134
|
+
.action(async (opts) => {
|
|
135
|
+
try {
|
|
136
|
+
const body = {
|
|
137
|
+
channel: opts.channel,
|
|
138
|
+
metric: opts.metric,
|
|
139
|
+
r1Id: opts.r1,
|
|
140
|
+
r2Id: opts.r2,
|
|
141
|
+
};
|
|
142
|
+
const client = new AgentApiClient();
|
|
143
|
+
const result = await client.post('/recommend-improvements', body);
|
|
144
|
+
output(result, opts);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
handleError(e);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// ─── 策略三: random ────────────────────────────────────────
|
|
151
|
+
rec
|
|
152
|
+
.command('random')
|
|
153
|
+
.description('策略三: 批量随机生成 Tag 组合,按预估指标排序')
|
|
154
|
+
.requiredOption('--channel <name>', '投放渠道')
|
|
155
|
+
.option('--metric <name>', '指标(ctr|cvr|cpi|ipm|cost|installs)', 'ctr')
|
|
156
|
+
.option('--batch <n>', '生成数量', '10')
|
|
157
|
+
.option('--similar-top <n>', '每条结果附带的相似 Creative 数量', '5')
|
|
158
|
+
.option('--no-atoms', '不为每条结果返回 atoms')
|
|
159
|
+
.option('--seed <n>', '随机种子(可选;提供时结果可复现)')
|
|
160
|
+
.option('--pretty', '美化 JSON 输出')
|
|
161
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
try {
|
|
164
|
+
const body = {
|
|
165
|
+
channel: opts.channel,
|
|
166
|
+
metric: opts.metric,
|
|
167
|
+
batchSize: parseIntOpt('--batch', opts.batch),
|
|
168
|
+
similarTopN: parseIntOpt('--similar-top', opts.similarTop),
|
|
169
|
+
includeAtoms: opts.atoms,
|
|
170
|
+
seed: opts.seed === undefined ? undefined : parseIntOpt('--seed', opts.seed),
|
|
171
|
+
};
|
|
172
|
+
const client = new AgentApiClient();
|
|
173
|
+
const result = await client.post('/recommend-random', body);
|
|
174
|
+
output(result, opts);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
handleError(e);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// ─── 辅助: optimal ────────────────────────────────────────
|
|
181
|
+
rec
|
|
182
|
+
.command('optimal')
|
|
183
|
+
.description('辅助: 理论最优 Tag 组合(perDim ω² 排序)')
|
|
184
|
+
.requiredOption('--channel <name>', '投放渠道')
|
|
185
|
+
.option('--metric <name>', '指标(ctr|cvr|cpi|ipm|cost|installs)', 'ctr')
|
|
186
|
+
.option('--top <n>', 'Top-K 数量', '5')
|
|
187
|
+
.option('--include-atoms', '为每条结果附带 atoms 配置')
|
|
188
|
+
.option('--pretty', '美化 JSON 输出')
|
|
189
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
190
|
+
.action(async (opts) => {
|
|
191
|
+
try {
|
|
192
|
+
const body = {
|
|
193
|
+
channel: opts.channel,
|
|
194
|
+
metric: opts.metric,
|
|
195
|
+
topK: parseIntOpt('--top', opts.top),
|
|
196
|
+
includeAtoms: !!opts.includeAtoms,
|
|
197
|
+
};
|
|
198
|
+
const client = new AgentApiClient();
|
|
199
|
+
const result = await client.post('/recommend-optimal', body);
|
|
200
|
+
output(result, opts);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
handleError(e);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// ─── 辅助: tag-to-atom ────────────────────────────────────
|
|
207
|
+
rec
|
|
208
|
+
.command('tag-to-atom')
|
|
209
|
+
.description('辅助: Tag → Atom slot 映射')
|
|
210
|
+
.requiredOption('--tags <json>', 'Tag JSON,如 \'{"玩法类型":"三消复刻","背景场景":"公园"}\'')
|
|
211
|
+
.option('--pretty', '美化 JSON 输出')
|
|
212
|
+
.option('--output <file>', '写入文件而非 stdout')
|
|
213
|
+
.action(async (opts) => {
|
|
214
|
+
try {
|
|
215
|
+
const tags = parseTagsArg(opts.tags);
|
|
216
|
+
if (!tags)
|
|
217
|
+
throw new Error('--tags is required');
|
|
218
|
+
const body = { tags };
|
|
219
|
+
const client = new AgentApiClient();
|
|
220
|
+
const result = await client.post('/recommend-tag-to-atom', body);
|
|
221
|
+
output(result, opts);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
handleError(e);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { registerToolsCommands } from './commands/tools.js';
|
|
|
19
19
|
import { registerImageCommands } from './commands/image.js';
|
|
20
20
|
import { registerAudioCommands } from './commands/audio.js';
|
|
21
21
|
import { registerPrefabCommands } from './commands/prefab.js';
|
|
22
|
+
import { registerRecommendCommands } from './commands/recommend.js';
|
|
22
23
|
import { CLI_ROOT_DESCRIPTION, getCliTopicsHelpText, registerRootProgramHelp, } from './cli-root-help.js';
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
25
|
const __dirname = dirname(__filename);
|
|
@@ -329,4 +330,6 @@ registerImageCommands(program);
|
|
|
329
330
|
registerAudioCommands(program);
|
|
330
331
|
// Remix prefab management (playcraft prefab <command>)
|
|
331
332
|
registerPrefabCommands(program);
|
|
333
|
+
// Tag → Atom recommend (playcraft recommend <command>)
|
|
334
|
+
registerRecommendCommands(program);
|
|
332
335
|
program.parse(process.argv);
|
|
@@ -62,24 +62,12 @@ export class AgentApiClient {
|
|
|
62
62
|
return res.json();
|
|
63
63
|
}
|
|
64
64
|
static loadConfig() {
|
|
65
|
-
// 兜底覆盖三类常见沙箱运行用户:
|
|
66
|
-
// - AGS 镜像(ccr.ccs.tencentyun.com/ags-image/sandbox-code)默认运行用户为 `user`,HOME=/home/user
|
|
67
|
-
// - 自建 docker/Dockerfile 沙箱 runtime stage 用户为 `opencode`,HOME=/home/opencode
|
|
68
|
-
// - 以 root 启动 CLI 的旁路场景,HOME=/root
|
|
69
|
-
// 第一条 cwd 与第二条 /project 是与运行用户无关的硬编码命中点。
|
|
70
65
|
const searchPaths = [
|
|
71
66
|
join(process.cwd(), '.playcraft.json'),
|
|
72
67
|
'/project/.playcraft.json',
|
|
73
|
-
'/home/user/.playcraft.json',
|
|
74
|
-
'/home/opencode/.playcraft.json',
|
|
75
|
-
'/root/.playcraft.json',
|
|
76
68
|
join(homedir(), '.playcraft.json'),
|
|
77
69
|
];
|
|
78
|
-
const seen = new Set();
|
|
79
70
|
for (const p of searchPaths) {
|
|
80
|
-
if (seen.has(p))
|
|
81
|
-
continue;
|
|
82
|
-
seen.add(p);
|
|
83
71
|
if (existsSync(p)) {
|
|
84
72
|
try {
|
|
85
73
|
return JSON.parse(readFileSync(p, 'utf-8'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"release": "node scripts/release.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@playcraft/build": "^0.0.
|
|
26
|
+
"@playcraft/build": "^0.0.33",
|
|
27
27
|
"@playcraft/common": "^0.0.20",
|
|
28
28
|
"chokidar": "^4.0.3",
|
|
29
29
|
"commander": "^13.1.0",
|