#!/usr/bin/env node import { readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main'; const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, ''); async function walk(directory) { const files = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const target = path.join(directory, entry.name); if (entry.isDirectory()) files.push(...await walk(target)); else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')) files.push(target); } return files; } const requested = new Set(); for (const file of await walk('projects/xiaoshu-mobile/src/app')) { const source = await readFile(file, 'utf8'); for (const pattern of [ /\b(?:this\.)?api\.(?:get|post)(?:<[^>]*>)?\(\s*['"]([^'"]+)['"]/g, /\b(?:saveAction|deleteAction|listAction|loadAction):\s*['"]([^'"]+)['"]/g, /['"](ranklist_[1-5])['"]/g, /['"](reading\/(?:me|query|get|create|update|ai|user\/update))['"]/g, /\b(?:this\.)?auth\.login\(\s*['"]([^'"]+)['"]/g, ]) for (const match of source.matchAll(pattern)) requested.add(match[1]); } const paritySource = await readFile('projects/xiaoshu-mobile/src/app/core/source-parity.generated.ts', 'utf8'); const parityMatch = paritySource.match(/export const SOURCE_API_ACTIONS = (\[[\s\S]*?\]) as const;/); if (!parityMatch) throw new Error('无法读取 SOURCE_API_ACTIONS'); for (const action of JSON.parse(parityMatch[1])) requested.add(String(action)); const response = await fetch(`${FUNCTION_URL}/xiaoshu/app/gateway`, { method: 'POST', headers: { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'migration_status' } }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload.result) throw new Error(`migration_status 请求失败:${response.status}`); const implemented = new Set(Array.isArray(payload.result.implemented) ? payload.result.implemented.map(String) : []); const blocked = new Set(Object.keys(payload.result.blocked || {})); const covered = new Set([...implemented, ...blocked, ...(payload.result.disabledActions || [])]); const missing = [...requested].filter((action) => !covered.has(action) && !action.startsWith('ops/')).sort(); const report = { passed: missing.length === 0, requested: requested.size, implemented: implemented.size, blocked: blocked.size, missing, }; console.log(JSON.stringify(report, null, 2)); if (missing.length) process.exitCode = 1;