audit-app-cloud-actions.mjs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. #!/usr/bin/env node
  2. import { readdir, readFile } from 'node:fs/promises';
  3. import path from 'node:path';
  4. import vm from 'node:vm';
  5. const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  6. const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, '');
  7. async function walk(directory) {
  8. const files = [];
  9. for (const entry of await readdir(directory, { withFileTypes: true })) {
  10. const target = path.join(directory, entry.name);
  11. if (entry.isDirectory()) files.push(...await walk(target));
  12. else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')) files.push(target);
  13. }
  14. return files;
  15. }
  16. const requested = new Set();
  17. for (const file of await walk('projects/xiaoshu-mobile/src/app')) {
  18. const source = await readFile(file, 'utf8');
  19. for (const pattern of [
  20. /\b(?:this\.)?api\.(?:get|post)(?:<[^>]*>)?\(\s*['"]([^'"]+)['"]/g,
  21. /\b(?:saveAction|deleteAction|listAction|loadAction):\s*['"]([^'"]+)['"]/g,
  22. /['"](ranklist_[1-5])['"]/g,
  23. /['"](reading\/(?:me|query|get|create|update|ai|user\/update))['"]/g,
  24. /\b(?:this\.)?auth\.login\(\s*['"]([^'"]+)['"]/g,
  25. ]) for (const match of source.matchAll(pattern)) requested.add(match[1]);
  26. }
  27. const paritySource = await readFile('projects/xiaoshu-mobile/src/app/core/source-parity.generated.ts', 'utf8');
  28. const parityMatch = paritySource.match(/export const SOURCE_API_ACTIONS = (\[[\s\S]*?\]) as const;/);
  29. if (!parityMatch) throw new Error('无法读取 SOURCE_API_ACTIONS');
  30. for (const action of JSON.parse(parityMatch[1])) requested.add(String(action));
  31. let payload;
  32. const local=process.argv.includes('--local');
  33. if(local){
  34. const source=await readFile(new URL('./deploy-admin-functions.mjs',import.meta.url),'utf8');
  35. const code=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)?.[1];if(!code)throw Error('Cannot extract local gateway');
  36. const context=vm.createContext({console,process:{env:{}},Date});vm.runInContext(code+'\n'+await readFile(new URL('./cloud/mobile-teaching.js',import.meta.url),'utf8'),context);
  37. const response={json:body=>{payload=body;return response;},status:()=>response};
  38. await context.handler({body:{params:{action:'migration_status'}}},response);
  39. if(!payload?.result)throw Error('Local migration_status failed');
  40. }else{
  41. 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'}}),signal:AbortSignal.timeout(20000)});
  42. payload=await response.json().catch(()=>({}));if(!response.ok||!payload.result)throw Error(`migration_status 请求失败:${response.status}`);
  43. }
  44. const implemented = new Set(Array.isArray(payload.result.implemented) ? payload.result.implemented.map(String) : []);
  45. const blocked = new Set(Object.keys(payload.result.blocked || {}));
  46. const covered = new Set([...implemented, ...blocked, ...(payload.result.disabledActions || [])]);
  47. const missing = [...requested].filter((action) => !covered.has(action) && !action.startsWith('ops/')).sort();
  48. const report = {
  49. source:local?'local-code':'online-gateway',
  50. passed: missing.length === 0,
  51. requested: requested.size,
  52. implemented: implemented.size,
  53. blocked: blocked.size,
  54. missing,
  55. };
  56. console.log(JSON.stringify(report, null, 2));
  57. if (missing.length) process.exitCode = 1;