const { Client } = require('pg'); const fs = require('fs'); const path = require('path'); require('dotenv').config({ path: '/root/miaojingAI/.env.local' }); const root = '/root/miaojingAI'; const client = new Client({ connectionString: process.env.LOCAL_DB_URL }); function storageKeyFromUrl(url) { if (!url || typeof url !== 'string') return null; const marker = '/api/local-storage/'; const index = url.indexOf(marker); if (index < 0) return null; try { return decodeURIComponent(url.slice(index + marker.length).split('?')[0]); } catch { return null; } } function countFiles(dir) { if (!fs.existsSync(dir)) return 0; let count = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); count += entry.isDirectory() ? countFiles(full) : 1; } return count; } async function q(name, sql) { const result = await client.query(sql); return { name, rows: result.rows }; } (async () => { await client.connect(); const checks = {}; checks.tableCounts = await q('table_counts', ` select 'auth.users' as table_name, count(*)::int as count from auth.users union all select 'profiles', count(*)::int from profiles union all select 'user_api_keys', count(*)::int from user_api_keys union all select 'works', count(*)::int from works union all select 'generation_jobs', count(*)::int from generation_jobs union all select 'credit_transactions', count(*)::int from credit_transactions union all select 'orders', count(*)::int from orders union all select 'api_providers', count(*)::int from api_providers union all select 'model_recommendations', count(*)::int from model_recommendations `); checks.profiles = await q('profiles', ` select id, email, nickname, role, membership_tier, credits_balance, is_active, avatar_url is not null as has_avatar, created_at, updated_at from profiles order by created_at desc `); checks.profileAuthIntegrity = await q('profile_auth_integrity', ` select 'profile_without_auth' as issue, count(*)::int as count from profiles p left join auth.users u on u.id = p.id where u.id is null union all select 'auth_without_profile', count(*)::int from auth.users u left join profiles p on p.id = u.id where p.id is null `); checks.admin = await q('admin', ` select p.id, p.email, p.nickname, p.role, p.membership_tier, p.is_active, u.password_hash is not null as has_password_hash from profiles p left join auth.users u on u.id = p.id where p.role = 'admin' or p.email = 'admin@example.com' order by p.created_at `); checks.apiKeys = await q('api_keys', ` select id, user_id, provider, supplier_name, type, model_name, note, api_key_preview, length(api_key_encrypted) as encrypted_len, api_key_encrypted like 'mjenc:v1:%' as has_enc_prefix, api_key_encrypted = api_key_preview as encrypted_equals_preview, is_active, created_at from user_api_keys order by created_at desc `); checks.apiKeyIntegrity = await q('api_key_integrity', ` select count(*)::int as orphan_api_keys from user_api_keys k left join profiles p on p.id = k.user_id where p.id is null `); checks.worksDistribution = await q('works_distribution', ` select type, status, is_public, count(*)::int as count, count(*) filter (where result_url like '/api/local-storage/%')::int as local_urls, count(*) filter (where result_url like 'data:%')::int as data_urls, count(*) filter (where result_url like '[%')::int as placeholder_urls from works group by type, status, is_public order by type, status, is_public `); checks.worksByUser = await q('works_by_user', ` select p.email, p.nickname, w.user_id, count(*)::int as works from works w left join profiles p on p.id = w.user_id group by p.email, p.nickname, w.user_id order by works desc nulls last `); checks.workIntegrity = await q('work_integrity', ` select 'works_missing_user' as issue, count(*)::int as count from works w left join profiles p on p.id = w.user_id where p.id is null union all select 'works_null_user', count(*)::int from works where user_id is null union all select 'works_bad_url', count(*)::int from works where result_url is null or result_url = '' or result_url like 'data:%' union all select 'public_not_gallery_url', count(*)::int from works where is_public = true and result_url not like '/api/local-storage/gallery/%' `); checks.reversePromptWorks = await q('reverse_prompt_works', ` select id, user_id, type, left(result_url, 100) as result_url, left(coalesce(params->>'referenceImage', ''), 100) as reference_image, prompt <> '' as has_prompt, negative_prompt is not null as has_negative_prompt, created_at from works where type = 'reverse-prompt' order by created_at desc limit 20 `); checks.galleryWorks = await q('gallery_works', ` select id, user_id, type, is_public, left(result_url, 100) as result_url, created_at from works where is_public = true order by created_at desc `); checks.generationJobs = await q('generation_jobs', ` select status, count(*)::int as count, count(*) filter (where user_id is null)::int as null_user_count from generation_jobs group by status order by status `); checks.generationJobsByUser = await q('generation_jobs_by_user', ` select p.email, p.nickname, j.user_id, count(*)::int as jobs from generation_jobs j left join profiles p on p.id = j.user_id group by p.email, p.nickname, j.user_id order by jobs desc nulls last `); const works = (await client.query(` select id, result_url, thumbnail_url, params from works order by created_at desc `)).rows; const missingFiles = []; let referencedLocalUrls = 0; for (const work of works) { const urls = [ ['result_url', work.result_url], ['thumbnail_url', work.thumbnail_url], ['referenceImage', work.params && work.params.referenceImage], ]; if (work.params && Array.isArray(work.params.referenceImages)) { work.params.referenceImages.forEach((url, index) => urls.push([`referenceImages[${index}]`, url])); } for (const [field, url] of urls) { const key = storageKeyFromUrl(url); if (!key) continue; referencedLocalUrls++; const full = path.join(root, 'local-storage', key); if (!fs.existsSync(full)) missingFiles.push({ workId: work.id, field, key }); } } checks.storage = { localStorageExists: fs.existsSync(path.join(root, 'local-storage')), backupsExists: fs.existsSync(path.join(root, 'backups')), localFiles: countFiles(path.join(root, 'local-storage')), generatedFiles: countFiles(path.join(root, 'local-storage', 'generated')), galleryFiles: countFiles(path.join(root, 'local-storage', 'gallery')), reversePromptFiles: countFiles(path.join(root, 'local-storage', 'reverse-prompt')), referencedLocalUrls, missingFiles, }; console.log(JSON.stringify(checks, null, 2)); await client.end(); })().catch((error) => { console.error(error); process.exit(1); });