1
This commit is contained in:
229
server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts
Normal file
229
server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { createDatabase } from '../db.js';
|
||||
import { RuntimeRepository } from '../repositories/runtimeRepository.js';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { CustomWorldAgentSessionStore } from '../services/customWorldAgentSessionStore.js';
|
||||
|
||||
type RecordValue = Record<string, unknown>;
|
||||
|
||||
function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is RecordValue {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function toRecordArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is RecordValue => isRecord(entry))
|
||||
: [];
|
||||
}
|
||||
|
||||
function resolvePublicAssetPath(publicDir: string, imageSrc: unknown) {
|
||||
const normalizedImageSrc = toText(imageSrc);
|
||||
if (!normalizedImageSrc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return path.join(publicDir, normalizedImageSrc.replace(/^\/+/u, ''));
|
||||
}
|
||||
|
||||
async function ensureSquareRoleImage(publicDir: string, imageSrc: unknown) {
|
||||
const assetPath = resolvePublicAssetPath(publicDir, imageSrc);
|
||||
if (!assetPath || !fs.existsSync(assetPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = await sharp(assetPath).metadata();
|
||||
if (
|
||||
typeof metadata.width === 'number' &&
|
||||
typeof metadata.height === 'number' &&
|
||||
metadata.width === metadata.height
|
||||
) {
|
||||
return {
|
||||
imageSrc: toText(imageSrc),
|
||||
updated: false,
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
};
|
||||
}
|
||||
|
||||
const squaredBuffer = await sharp(assetPath)
|
||||
.resize(1024, 1024, {
|
||||
fit: 'cover',
|
||||
position: 'attention',
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
fs.writeFileSync(assetPath, squaredBuffer);
|
||||
|
||||
return {
|
||||
imageSrc: toText(imageSrc),
|
||||
updated: true,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const userId = 'user_02b1dea4e951b13fe53db236560bdf28';
|
||||
const sessionId = 'custom-world-agent-session-019e192a4060d18b92df127f1dafe8ae';
|
||||
const profileId = 'custom-world-mo744tca-深海奇境';
|
||||
const config = loadConfig({
|
||||
projectRoot: path.resolve(process.cwd(), '..'),
|
||||
});
|
||||
const db = await createDatabase(config);
|
||||
|
||||
try {
|
||||
const runtimeRepository = new RuntimeRepository(db);
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const session = await sessionStore.getSnapshot(userId, sessionId);
|
||||
if (!session || !isRecord(session.draftProfile)) {
|
||||
throw new Error('未找到目标世界草稿 session,无法同步历史保存档案。');
|
||||
}
|
||||
|
||||
const savedProfileEntry = (
|
||||
await runtimeRepository.listCustomWorldProfiles(userId)
|
||||
).find((entry) => entry.profileId === profileId);
|
||||
if (!savedProfileEntry) {
|
||||
throw new Error('未找到目标 saved profile,无法同步历史保存档案。');
|
||||
}
|
||||
|
||||
const draftProfile = session.draftProfile;
|
||||
const nextProfile = JSON.parse(
|
||||
JSON.stringify(savedProfileEntry.profile),
|
||||
) as RecordValue;
|
||||
|
||||
const draftPlayableById = new Map(
|
||||
toRecordArray(draftProfile.playableNpcs).map((entry) => [toText(entry.id), entry] as const),
|
||||
);
|
||||
const draftStoryById = new Map(
|
||||
toRecordArray(draftProfile.storyNpcs).map((entry) => [toText(entry.id), entry] as const),
|
||||
);
|
||||
const draftLandmarkById = new Map(
|
||||
toRecordArray(draftProfile.landmarks).map((entry) => [toText(entry.id), entry] as const),
|
||||
);
|
||||
const draftSceneChapterBySceneId = new Map(
|
||||
toRecordArray(draftProfile.sceneChapters).map((entry) => [toText(entry.sceneId), entry] as const),
|
||||
);
|
||||
|
||||
const playableNpcs = toRecordArray(nextProfile.playableNpcs).map((role) => {
|
||||
const draftRole = draftPlayableById.get(toText(role.id));
|
||||
if (!draftRole) {
|
||||
return role;
|
||||
}
|
||||
|
||||
return {
|
||||
...role,
|
||||
imageSrc: toText(draftRole.imageSrc) || role.imageSrc,
|
||||
generatedVisualAssetId:
|
||||
toText(draftRole.generatedVisualAssetId) || role.generatedVisualAssetId,
|
||||
};
|
||||
});
|
||||
|
||||
const storyNpcs = toRecordArray(nextProfile.storyNpcs).map((role) => {
|
||||
const draftRole = draftStoryById.get(toText(role.id));
|
||||
if (!draftRole) {
|
||||
return role;
|
||||
}
|
||||
|
||||
return {
|
||||
...role,
|
||||
imageSrc: toText(draftRole.imageSrc) || role.imageSrc,
|
||||
generatedVisualAssetId:
|
||||
toText(draftRole.generatedVisualAssetId) || role.generatedVisualAssetId,
|
||||
};
|
||||
});
|
||||
|
||||
const landmarks = toRecordArray(nextProfile.landmarks).map((landmark) => {
|
||||
const draftLandmark = draftLandmarkById.get(toText(landmark.id));
|
||||
const draftSceneChapter = draftSceneChapterBySceneId.get(toText(landmark.id));
|
||||
const firstActImageSrc =
|
||||
toRecordArray(draftSceneChapter?.acts)
|
||||
.map((act) => toText(act.backgroundImageSrc))
|
||||
.find(Boolean) || '';
|
||||
|
||||
return {
|
||||
...landmark,
|
||||
imageSrc:
|
||||
toText(draftLandmark?.imageSrc) ||
|
||||
firstActImageSrc ||
|
||||
toText(landmark.imageSrc) ||
|
||||
undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const sceneChapterBlueprints = toRecordArray(
|
||||
nextProfile.sceneChapterBlueprints,
|
||||
).map((chapter) => {
|
||||
const draftChapter = draftSceneChapterBySceneId.get(toText(chapter.sceneId));
|
||||
if (!draftChapter) {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
const draftActById = new Map(
|
||||
toRecordArray(draftChapter.acts).map((act) => [toText(act.id), act] as const),
|
||||
);
|
||||
|
||||
return {
|
||||
...chapter,
|
||||
acts: toRecordArray(chapter.acts).map((act) => {
|
||||
const draftAct = draftActById.get(toText(act.id));
|
||||
if (!draftAct) {
|
||||
return act;
|
||||
}
|
||||
|
||||
return {
|
||||
...act,
|
||||
backgroundImageSrc:
|
||||
toText(draftAct.backgroundImageSrc) || act.backgroundImageSrc,
|
||||
backgroundAssetId:
|
||||
toText(draftAct.backgroundAssetId) || act.backgroundAssetId,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const roleImageUpdates = await Promise.all(
|
||||
[...playableNpcs, ...storyNpcs].map((role) =>
|
||||
ensureSquareRoleImage(config.publicDir, role.imageSrc),
|
||||
),
|
||||
);
|
||||
|
||||
nextProfile.playableNpcs = playableNpcs;
|
||||
nextProfile.storyNpcs = storyNpcs;
|
||||
nextProfile.landmarks = landmarks;
|
||||
nextProfile.sceneChapterBlueprints = sceneChapterBlueprints;
|
||||
|
||||
const updatedEntry = await runtimeRepository.upsertCustomWorldProfile(
|
||||
userId,
|
||||
profileId,
|
||||
nextProfile,
|
||||
savedProfileEntry.authorDisplayName || '玩家',
|
||||
);
|
||||
|
||||
const summary = {
|
||||
profileId,
|
||||
syncedPlayableCount: playableNpcs.length,
|
||||
syncedStoryCount: storyNpcs.length,
|
||||
syncedLandmarkCount: landmarks.length,
|
||||
syncedSceneChapterCount: sceneChapterBlueprints.length,
|
||||
squareRoleImagesUpdated: roleImageUpdates.filter((entry) => entry?.updated).length,
|
||||
coverImageSrc: updatedEntry.entry.coverImageSrc,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user