update-token-config-v2.js 7.8 KB

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