update-token-config-v2.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. #!/usr/bin/env node
  2. /**
  3. * 批量更新所有 api-config.json 的 tokenConfig 为 v2(多用户动态解析版)
  4. *
  5. * 变更:
  6. * - 移除硬编码的 qrCodeUrl(含固定 authid)
  7. * - 新增 paymentUrlResolution:AI Agent 动态解析用户 → 拼充值 URL
  8. * - onMissing/onBalanceInsufficient 的 action 改为 resolveUserThenShowPayment
  9. *
  10. * 用法:
  11. * node scripts/tools/update-token-config-v2.js # 更新项目源码
  12. * node scripts/tools/update-token-config-v2.js --deployed # 更新已部署的 skills
  13. * node scripts/tools/update-token-config-v2.js --both # 两者都更新
  14. * node scripts/tools/update-token-config-v2.js --dry-run # 预览
  15. */
  16. const fs = require('fs');
  17. const path = require('path');
  18. const os = require('os');
  19. // ============================================
  20. // 新版 tokenConfig(多用户动态解析)
  21. // ============================================
  22. const NEW_TOKEN_CONFIG = {
  23. type: "bearer",
  24. configFile: "~/.openclaw/voc-credentials.json",
  25. tokenField: "vocToken",
  26. currentToken: "r:858b3ee92314d5447d1fc3cdc10462d7",
  27. resolutionOrder: ["configFile", "currentToken"],
  28. apigId: "Vo3ROWEvDy",
  29. paymentUrlResolution: {
  30. description: "动态构建充值URL:通过当前token查出用户objectId,拼接到充值页面",
  31. steps: [
  32. "1. 从 voc-credentials.json 或 currentToken 获取当前 session token",
  33. "2. 调用 GET https://server.fmode.cn/parse/users/me (Header: X-Parse-Application-Id: ncloudmaster, X-Parse-Session-Token: {token})获取用户 objectId",
  34. "3. 拼接充值URL: https://app.fmode.cn/dev/apig-pay/?user={objectId}&apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF",
  35. "4. 将该URL展示给用户,引导扫码支付"
  36. ],
  37. userResolveEndpoint: "https://server.fmode.cn/parse/users/me",
  38. userResolveHeaders: {
  39. "X-Parse-Application-Id": "ncloudmaster",
  40. "X-Parse-Session-Token": "{vocToken}"
  41. },
  42. paymentBaseUrl: "https://app.fmode.cn/dev/apig-pay/",
  43. paymentParams: {
  44. user: "{resolvedUserId}",
  45. apigid: "Vo3ROWEvDy",
  46. fun_id: "HOkkX72PMF"
  47. },
  48. balanceCheckEndpoint: "https://server.fmode.cn/api/apig/getApig"
  49. },
  50. onMissing: {
  51. action: "resolveUserThenShowPayment",
  52. title: "扫码开通 VOC-AI 数据服务",
  53. message: "此 Skill 需要有效的 API Token。请按以下步骤操作:\n1. 如果你已有 Token,请告诉我,我会保存到 ~/.openclaw/voc-credentials.json\n2. 如果没有 Token,我会引导你扫码充值开通"
  54. },
  55. onBalanceInsufficient: {
  56. action: "resolveUserThenShowPayment",
  57. title: "VOC-AI Token 余额不足,请扫码充值",
  58. message: "当前 Token 余额不足,我将为你生成专属充值链接,扫码支付后即可继续使用。"
  59. }
  60. };
  61. const NEW_ERROR_HANDLING = {
  62. balanceInsufficient: {
  63. conditions: [
  64. { responseField: "code", operator: "in", value: [-2, -3, -10, 402, 429] },
  65. { responseField: "msg", operator: "contains", value: ["余额不足", "insufficient", "balance", "quota"] },
  66. { responseField: "message", operator: "contains", value: ["余额不足", "insufficient", "balance"] }
  67. ],
  68. matchMode: "any",
  69. trigger: "tokenConfig.onBalanceInsufficient"
  70. },
  71. unauthorized: {
  72. conditions: [
  73. { responseField: "code", operator: "in", value: [401, 403] },
  74. { responseField: "msg", operator: "contains", value: ["unauthorized", "token", "invalid", "auth"] }
  75. ],
  76. matchMode: "any",
  77. trigger: "tokenConfig.onMissing"
  78. }
  79. };
  80. // ============================================
  81. // 参数解析
  82. // ============================================
  83. const args = process.argv.slice(2);
  84. const dryRun = args.includes('--dry-run');
  85. const updateDeployed = args.includes('--deployed') || args.includes('--both');
  86. const updateSource = !args.includes('--deployed') || args.includes('--both');
  87. const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
  88. const DEPLOYED_DIR = path.join(os.homedir(), '.openclaw', 'skills');
  89. const CATEGORIES = [
  90. 'voc', 'social-media', 'competitor-analysis', 'review-analysis',
  91. 'synthesis', 'social-voc', 'douyin', 'video-creation', 'jimeng',
  92. 'payment', 'test', 'workshop'
  93. ];
  94. // ============================================
  95. // 核心逻辑
  96. // ============================================
  97. function findAllApiConfigs(rootDir, isFlat) {
  98. const results = [];
  99. if (isFlat) {
  100. // Deployed: flat structure ~/.openclaw/skills/{skill}/api-config.json
  101. if (!fs.existsSync(rootDir)) return results;
  102. fs.readdirSync(rootDir).forEach(skill => {
  103. const cfgPath = path.join(rootDir, skill, 'api-config.json');
  104. if (fs.existsSync(cfgPath)) {
  105. results.push({ path: cfgPath, skill });
  106. }
  107. });
  108. } else {
  109. // Source: categorized structure {category}/{skill}/api-config.json
  110. CATEGORIES.forEach(cat => {
  111. const catDir = path.join(rootDir, cat);
  112. if (!fs.existsSync(catDir)) return;
  113. fs.readdirSync(catDir).forEach(skill => {
  114. const cfgPath = path.join(catDir, skill, 'api-config.json');
  115. if (fs.existsSync(cfgPath)) {
  116. results.push({ path: cfgPath, skill, category: cat });
  117. }
  118. });
  119. });
  120. }
  121. return results;
  122. }
  123. function updateConfigFile(filePath, skillName) {
  124. let content;
  125. try {
  126. const raw = fs.readFileSync(filePath, 'utf-8');
  127. // Remove BOM if present
  128. content = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw;
  129. } catch (e) {
  130. console.log(` [ERROR] Cannot read: ${skillName} - ${e.message}`);
  131. return false;
  132. }
  133. let config;
  134. try {
  135. config = JSON.parse(content);
  136. } catch (e) {
  137. console.log(` [ERROR] Invalid JSON: ${skillName}`);
  138. return false;
  139. }
  140. if (!config.tokenConfig) {
  141. return false; // No tokenConfig, skip
  142. }
  143. // Replace tokenConfig and errorHandling
  144. config.tokenConfig = { ...NEW_TOKEN_CONFIG };
  145. config.errorHandling = { ...NEW_ERROR_HANDLING };
  146. if (!dryRun) {
  147. fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
  148. }
  149. return true;
  150. }
  151. // ============================================
  152. // 主流程
  153. // ============================================
  154. console.log('');
  155. console.log(' ====================================================');
  156. console.log(' tokenConfig v2 Batch Updater (多用户动态解析)');
  157. console.log(' ====================================================');
  158. if (dryRun) console.log(' [DRY RUN] Preview only');
  159. console.log('');
  160. let totalUpdated = 0;
  161. let totalSkipped = 0;
  162. if (updateSource) {
  163. console.log(' [Source] Updating project files...');
  164. const sourceConfigs = findAllApiConfigs(PROJECT_ROOT, false);
  165. sourceConfigs.forEach(({ path: p, skill, category }) => {
  166. if (updateConfigFile(p, skill)) {
  167. console.log(` [OK] ${category}/${skill}`);
  168. totalUpdated++;
  169. } else {
  170. totalSkipped++;
  171. }
  172. });
  173. console.log('');
  174. }
  175. if (updateDeployed) {
  176. console.log(' [Deployed] Updating ~/.openclaw/skills/...');
  177. const deployedConfigs = findAllApiConfigs(DEPLOYED_DIR, true);
  178. deployedConfigs.forEach(({ path: p, skill }) => {
  179. if (updateConfigFile(p, skill)) {
  180. console.log(` [OK] ${skill}`);
  181. totalUpdated++;
  182. } else {
  183. totalSkipped++;
  184. }
  185. });
  186. console.log('');
  187. }
  188. console.log(' ====================================================');
  189. console.log(` Updated: ${totalUpdated}, Skipped: ${totalSkipped}`);
  190. console.log(' ====================================================');
  191. if (dryRun) console.log(' Remove --dry-run to apply.');
  192. console.log('');