sync-reading-prompts.mjs 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. #!/usr/bin/env node
  2. const SOURCE_URL = (process.env.XIAOSHU_READING_PROMPT_SOURCE_URL || 'https://server.fmode.cn/parse').replace(/\/$/, '');
  3. const SOURCE_APP_ID = process.env.XIAOSHU_READING_PROMPT_SOURCE_APP_ID || 'ncloudmaster';
  4. const TARGET_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
  5. const TARGET_APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  6. const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
  7. const commit = process.argv.includes('--commit');
  8. const CODES = [
  9. 'english-reading-article-tpl',
  10. 'english-reading-article-supply-tpl',
  11. 'english-reading-question-json-tpl',
  12. ];
  13. if (commit && !MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY,不会写入生产数据');
  14. async function parseRequest(url, headers, init = {}) {
  15. const response = await fetch(url, { ...init, headers: { ...headers, ...(init.headers || {}) } });
  16. const text = await response.text();
  17. let payload;
  18. try { payload = text ? JSON.parse(text) : {}; }
  19. catch { throw new Error(`Parse 返回了无效 JSON(${response.status})`); }
  20. if (!response.ok || payload.error) throw new Error(payload.error || `Parse 请求失败(${response.status})`);
  21. return payload;
  22. }
  23. function classQuery(baseUrl, where) {
  24. return `${baseUrl}/classes/PromptTemplate?where=${encodeURIComponent(JSON.stringify(where))}&limit=100`;
  25. }
  26. const sourceHeaders = { 'X-Parse-Application-Id': SOURCE_APP_ID };
  27. const targetHeaders = {
  28. 'X-Parse-Application-Id': TARGET_APP_ID,
  29. ...(MASTER_KEY ? { 'X-Parse-Master-Key': MASTER_KEY } : {}),
  30. 'Content-Type': 'application/json',
  31. };
  32. const sourcePayload = await parseRequest(classQuery(SOURCE_URL, { code: { $in: CODES } }), sourceHeaders);
  33. const sourceRows = Array.isArray(sourcePayload.results) ? sourcePayload.results : [];
  34. const sourceByCode = new Map(sourceRows.map((row) => [String(row.code || ''), row]));
  35. const missingSource = CODES.filter((code) => !sourceByCode.has(code));
  36. if (missingSource.length) throw new Error(`原系统缺少阅读模板:${missingSource.join(', ')}`);
  37. let targetRows = [];
  38. try {
  39. const targetPayload = await parseRequest(classQuery(TARGET_URL, { code: { $in: CODES } }), targetHeaders);
  40. targetRows = Array.isArray(targetPayload.results) ? targetPayload.results : [];
  41. } catch (error) {
  42. if (!commit) throw error;
  43. targetRows = [];
  44. }
  45. const targetByCode = new Map(targetRows.map((row) => [String(row.code || ''), row]));
  46. const summary = { source: sourceRows.length, targetBefore: targetRows.length, create: 0, update: 0, unchanged: 0, committed: commit };
  47. for (const code of CODES) {
  48. const source = sourceByCode.get(code);
  49. const target = targetByCode.get(code);
  50. const value = {
  51. code,
  52. name: String(source.name || code),
  53. template: String(source.template || ''),
  54. ...(source.output === undefined ? {} : { output: source.output }),
  55. };
  56. const unchanged = target
  57. && String(target.name || '') === value.name
  58. && String(target.template || '') === value.template
  59. && JSON.stringify(target.output ?? null) === JSON.stringify(value.output ?? null);
  60. if (unchanged) { summary.unchanged += 1; continue; }
  61. if (target) summary.update += 1;
  62. else summary.create += 1;
  63. if (!commit) continue;
  64. const url = target
  65. ? `${TARGET_URL}/classes/PromptTemplate/${encodeURIComponent(target.objectId)}`
  66. : `${TARGET_URL}/classes/PromptTemplate`;
  67. await parseRequest(url, targetHeaders, { method: target ? 'PUT' : 'POST', body: JSON.stringify(value) });
  68. }
  69. console.log(JSON.stringify(summary, null, 2));