release: opensource snapshot 2026-02-27 19:25:00

This commit is contained in:
saturn
2026-02-27 19:25:00 +08:00
commit 5de9622c8b
1055 changed files with 164772 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core'
import { prisma } from '@/lib/prisma'
import { encodeImageUrls } from '@/lib/contracts/image-urls-contract'
type AppearanceRow = {
id: string
imageUrls: string | null
previousImageUrls: string | null
}
type DynamicModel = {
findMany: (args: unknown) => Promise<AppearanceRow[]>
update: (args: unknown) => Promise<unknown>
}
type FieldName = 'imageUrls' | 'previousImageUrls'
type NormalizeResult = {
next: string
changed: boolean
reason: 'ok' | 'null' | 'invalid_json' | 'not_array' | 'filtered_non_string' | 'normalized_json'
}
type ModelStats = {
scanned: number
updatedRows: number
changedFields: number
reasons: Record<string, number>
}
const BATCH_SIZE = 200
const APPLY = process.argv.includes('--apply')
const MODELS: Array<{ name: string; model: string }> = [
{ name: 'CharacterAppearance', model: 'characterAppearance' },
{ name: 'GlobalCharacterAppearance', model: 'globalCharacterAppearance' },
]
const prismaDynamic = prisma as unknown as Record<string, DynamicModel>
function print(message: string) {
process.stdout.write(`${message}\n`)
}
function normalizeField(raw: string | null): NormalizeResult {
if (raw === null) {
return {
next: encodeImageUrls([]),
changed: true,
reason: 'null',
}
}
try {
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) {
return {
next: encodeImageUrls([]),
changed: true,
reason: 'not_array',
}
}
const stringOnly = parsed.filter((item): item is string => typeof item === 'string')
const next = encodeImageUrls(stringOnly)
if (parsed.length !== stringOnly.length) {
return {
next,
changed: true,
reason: 'filtered_non_string',
}
}
if (raw !== next) {
return {
next,
changed: true,
reason: 'normalized_json',
}
}
return {
next,
changed: false,
reason: 'ok',
}
} catch {
return {
next: encodeImageUrls([]),
changed: true,
reason: 'invalid_json',
}
}
}
async function migrateModel(modelName: string, modelKey: string) {
const model = prismaDynamic[modelKey]
if (!model) {
throw new Error(`Prisma model not found: ${modelKey}`)
}
const stats: ModelStats = {
scanned: 0,
updatedRows: 0,
changedFields: 0,
reasons: {
ok: 0,
null: 0,
invalid_json: 0,
not_array: 0,
filtered_non_string: 0,
normalized_json: 0,
},
}
const samples: Array<{ id: string; field: FieldName; reason: NormalizeResult['reason']; before: string | null; after: string }> = []
let cursor: string | null = null
while (true) {
const rows = await model.findMany({
select: {
id: true,
imageUrls: true,
previousImageUrls: true,
},
...(cursor
? {
cursor: { id: cursor },
skip: 1,
}
: {}),
orderBy: { id: 'asc' },
take: BATCH_SIZE,
})
if (rows.length === 0) break
for (const row of rows) {
stats.scanned += 1
const imageUrlsResult = normalizeField(row.imageUrls)
const previousImageUrlsResult = normalizeField(row.previousImageUrls)
stats.reasons[imageUrlsResult.reason] += 1
stats.reasons[previousImageUrlsResult.reason] += 1
const data: Partial<Record<FieldName, string>> = {}
if (imageUrlsResult.changed) {
data.imageUrls = imageUrlsResult.next
stats.changedFields += 1
if (samples.length < 20) {
samples.push({
id: row.id,
field: 'imageUrls',
reason: imageUrlsResult.reason,
before: row.imageUrls,
after: imageUrlsResult.next,
})
}
}
if (previousImageUrlsResult.changed) {
data.previousImageUrls = previousImageUrlsResult.next
stats.changedFields += 1
if (samples.length < 20) {
samples.push({
id: row.id,
field: 'previousImageUrls',
reason: previousImageUrlsResult.reason,
before: row.previousImageUrls,
after: previousImageUrlsResult.next,
})
}
}
if (Object.keys(data).length > 0) {
stats.updatedRows += 1
if (APPLY) {
await model.update({
where: { id: row.id },
data,
})
}
}
}
cursor = rows[rows.length - 1]?.id || null
}
const summary = `[migrate-image-urls-contract] ${modelName}: scanned=${stats.scanned} updatedRows=${stats.updatedRows} changedFields=${stats.changedFields}`
_ulogInfo(summary)
print(summary)
print(`[migrate-image-urls-contract] ${modelName}: reasons=${JSON.stringify(stats.reasons)}`)
if (samples.length > 0) {
print(`[migrate-image-urls-contract] ${modelName}: sampleChanges=${JSON.stringify(samples, null, 2)}`)
}
return stats
}
async function main() {
print(`[migrate-image-urls-contract] mode=${APPLY ? 'apply' : 'dry-run'}`)
const totals = {
scanned: 0,
updatedRows: 0,
changedFields: 0,
}
for (const target of MODELS) {
const stats = await migrateModel(target.name, target.model)
totals.scanned += stats.scanned
totals.updatedRows += stats.updatedRows
totals.changedFields += stats.changedFields
}
print(`[migrate-image-urls-contract] done scanned=${totals.scanned} updatedRows=${totals.updatedRows} changedFields=${totals.changedFields} mode=${APPLY ? 'apply' : 'dry-run'}`)
}
main()
.catch((error) => {
_ulogError('[migrate-image-urls-contract] failed:', error)
process.exitCode = 1
})
.finally(async () => {
await prisma.$disconnect()
})