This commit is contained in:
2026-04-21 00:48:17 +08:00
parent 75944b1f1f
commit effe0355bd
19 changed files with 2897 additions and 180 deletions

View 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);
});