205 lines
7.2 KiB
JavaScript
205 lines
7.2 KiB
JavaScript
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);
|
|
});
|