audit-app-cloud-actions.mjs 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. #!/usr/bin/env node
  2. import { readdir, readFile } from 'node:fs/promises';
  3. import path from 'node:path';
  4. const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  5. const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, '');
  6. async function walk(directory) {
  7. const files = [];
  8. for (const entry of await readdir(directory, { withFileTypes: true })) {
  9. const target = path.join(directory, entry.name);
  10. if (entry.isDirectory()) files.push(...await walk(target));
  11. else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')) files.push(target);
  12. }
  13. return files;
  14. }
  15. const requested = new Set();
  16. for (const file of await walk('projects/xiaoshu-mobile/src/app')) {
  17. const source = await readFile(file, 'utf8');
  18. for (const pattern of [
  19. /\b(?:this\.)?api\.(?:get|post)(?:<[^>]*>)?\(\s*['"]([^'"]+)['"]/g,
  20. /\b(?:this\.)?auth\.login\(\s*['"]([^'"]+)['"]/g,
  21. ]) for (const match of source.matchAll(pattern)) requested.add(match[1]);
  22. }
  23. const paritySource = await readFile('projects/xiaoshu-mobile/src/app/core/source-parity.generated.ts', 'utf8');
  24. const parityMatch = paritySource.match(/export const SOURCE_API_ACTIONS = (\[[\s\S]*?\]) as const;/);
  25. if (!parityMatch) throw new Error('无法读取 SOURCE_API_ACTIONS');
  26. for (const action of JSON.parse(parityMatch[1])) requested.add(String(action));
  27. const response = await fetch(`${FUNCTION_URL}/xiaoshu/app/gateway`, {
  28. method: 'POST',
  29. headers: { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' },
  30. body: JSON.stringify({ params: { action: 'migration_status' } }),
  31. });
  32. const payload = await response.json().catch(() => ({}));
  33. if (!response.ok || !payload.result) throw new Error(`migration_status 请求失败:${response.status}`);
  34. const implemented = new Set(Array.isArray(payload.result.implemented) ? payload.result.implemented.map(String) : []);
  35. const blocked = new Set(Object.keys(payload.result.blocked || {}));
  36. const covered = new Set([...implemented, ...blocked]);
  37. const missing = [...requested].filter((action) => !covered.has(action) && !action.startsWith('ops/')).sort();
  38. const report = {
  39. passed: missing.length === 0,
  40. requested: requested.size,
  41. implemented: implemented.size,
  42. blocked: blocked.size,
  43. missing,
  44. };
  45. console.log(JSON.stringify(report, null, 2));
  46. if (missing.length) process.exitCode = 1;