import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, '..', '..'); const cloudDir = path.join(rootDir, 'cloud-functions'); const deployableDir = path.join(cloudDir, 'deployable'); const cloudFunctionsTs = path.join(rootDir, 'src', 'app', 'services', 'cloud-functions.ts'); const deployDoc = path.join(cloudDir, 'DEPLOY.md'); const expectedFiles = [ '_session.js', '_parseClassStore.js', '09-uploadManager.js', '10-authCreditManager.js', '11-jimengManager.js', '14-systemStorageManager.js', '15-fileAssetManager.js', ]; const sessionUsers = [ '09-uploadManager.js', '14-systemStorageManager.js', '15-fileAssetManager.js', ]; const deployableSessionUsers = [ '01-manifestManager.js', '02-taskManager.js', '03-historyManager.js', '04-resultManager.js', '05-remixManager.js', '06-voiceManager.js', '09-uploadManager.js', '13-douyinInsightManager.js', '14-systemStorageManager.js', '15-fileAssetManager.js', ]; const deployKeys = [ 'authCredit', 'systemStorage', 'fileAsset', 'upload', ]; const errors = []; const warnings = []; for (const file of expectedFiles) { assertFile(path.join(cloudDir, file), `Missing cloud function source: ${file}`); } for (const file of expectedFiles) { const fullPath = path.join(cloudDir, file); if (fs.existsSync(fullPath)) checkNodeSyntax(fullPath); } for (const file of deployableSessionUsers) { const fullPath = path.join(deployableDir, file); assertFile(fullPath, `Missing deployable single-file cloud function: ${file}. Run npm run build:cloud-functions`); if (fs.existsSync(fullPath)) { checkNodeSyntax(fullPath); const text = read(fullPath); if (text.includes("require('./_session')") || text.includes('require("./_session")')) { errors.push(`${file} deployable build must inline _session and must not require it`); } if (text.includes("require('./_parseClassStore')") || text.includes('require("./_parseClassStore")')) { errors.push(`${file} deployable build must inline _parseClassStore and must not require it`); } if (!text.includes('本文件由 scripts/build-cloud-functions.mjs 生成')) { warnings.push(`${file} deployable build is missing generated-file banner`); } } } for (const file of sessionUsers) { const text = read(path.join(cloudDir, file)); if (!text.includes("require('./_session')") && !text.includes('require("./_session")')) { errors.push(`${file} must use shared _session.js for session ownership checks`); } } const cloudConfig = read(cloudFunctionsTs); for (const key of deployKeys) { if (!new RegExp(`\\b${key}\\s*:`).test(cloudConfig)) { errors.push(`CLOUD_FN.${key} is missing in src/app/services/cloud-functions.ts`); } } const allCloudSource = expectedFiles.map((file) => read(path.join(cloudDir, file))).join('\n'); if (allCloudSource.includes('6pF6EAdKT')) { errors.push('Wrong video workflow APIG id 6pF6EAdKT is still present; expected 6pFf6EAdKT'); } if (!allCloudSource.includes('6pFf6EAdKT')) { errors.push('Expected video workflow APIG id 6pFf6EAdKT was not found in cloud function sources'); } const deployText = read(deployDoc); for (const file of expectedFiles) { if (!deployText.includes(file)) { warnings.push(`DEPLOY.md does not mention ${file}`); } } if (!deployText.includes('fmode') || !deployText.includes('authCredit') || !deployText.includes('systemStorage') || !deployText.includes('fileAsset')) { warnings.push('DEPLOY.md should explicitly state new functions use the existing fmode cloud function mechanism'); } if (!deployText.includes('SMOKE_SESSION_TOKEN')) { warnings.push('DEPLOY.md should document SMOKE_SESSION_TOKEN deployment validation'); } if (!deployText.includes('cloud-functions/deployable')) { warnings.push('DEPLOY.md should tell users to deploy cloud-functions/deployable single-file builds on fmode'); } for (const file of ['14-systemStorageManager.js', '15-fileAssetManager.js']) { const text = read(path.join(cloudDir, file)); if (!text.includes('adminInspect')) { errors.push(`${file} must expose adminInspect for storage governance inspection`); } if (/\bPsql\.query\b|\bSystemEntity\b|\bSystemAudit\b|\bDataMigration\b|\bFileAsset\b/.test(text)) { errors.push(`${file} must use VideoWorkflow* Parse Class storage and must not reference old PSQL table names`); } const deployableText = read(path.join(deployableDir, file)); if (/\bPsql\.query\b|\bSystemEntity\b|\bSystemAudit\b|\bDataMigration\b|\bFileAsset\b/.test(deployableText)) { errors.push(`${file} deployable must not reference old PSQL table names`); } } if (warnings.length) { for (const warning of warnings) console.warn(`WARN ${warning}`); } if (errors.length) { for (const error of errors) console.error(`FAIL ${error}`); process.exit(1); } console.log('Cloud function readiness passed'); function assertFile(filePath, message) { if (!fs.existsSync(filePath)) errors.push(message); } function read(filePath) { try { return fs.readFileSync(filePath, 'utf8'); } catch { return ''; } } function checkNodeSyntax(filePath) { try { execFileSync(process.execPath, ['--check', filePath], { stdio: 'pipe' }); } catch (error) { const output = `${error.stdout || ''}${error.stderr || ''}`.trim(); errors.push(`${path.relative(rootDir, filePath)} syntax check failed${output ? `: ${output}` : ''}`); } }