#!/usr/bin/env node /** * 批量更新所有 api-config.json 的 tokenConfig 为 v2(多用户动态解析版) * * 变更: * - 移除硬编码的 qrCodeUrl(含固定 authid) * - 新增 paymentUrlResolution:AI Agent 动态解析用户 → 拼充值 URL * - onMissing/onBalanceInsufficient 的 action 改为 resolveUserThenShowPayment * * 用法: * node scripts/tools/update-token-config-v2.js # 更新项目源码 * node scripts/tools/update-token-config-v2.js --deployed # 更新已部署的 skills * node scripts/tools/update-token-config-v2.js --both # 两者都更新 * node scripts/tools/update-token-config-v2.js --dry-run # 预览 */ const fs = require('fs'); const path = require('path'); const os = require('os'); // ============================================ // 新版 tokenConfig(多用户动态解析) // ============================================ const NEW_TOKEN_CONFIG = { type: "bearer", configFile: "~/.openclaw/voc-credentials.json", tokenField: "vocToken", resolutionOrder: ["configFile"], apigId: "Vo3ROWEvDy", paymentUrlResolution: { description: "动态构建充值URL:在已存在 session token 时查出用户 objectId,拼接到专属充值页面", steps: [ "1. 从 voc-credentials.json 获取当前 session token", "2. 调用 GET https://server.fmode.cn/parse/users/me (Header: X-Parse-Application-Id: ncloudmaster, X-Parse-Session-Token: {token})获取用户 objectId", "3. 拼接充值URL: https://app.fmode.cn/dev/apig-pay/?user={objectId}&apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF", "4. 将该URL展示给用户,引导扫码支付" ], userResolveEndpoint: "https://server.fmode.cn/parse/users/me", userResolveHeaders: { "X-Parse-Application-Id": "ncloudmaster", "X-Parse-Session-Token": "{vocToken}" }, paymentBaseUrl: "https://app.fmode.cn/dev/apig-pay/", paymentParams: { user: "{resolvedUserId}", apigid: "Vo3ROWEvDy", fun_id: "HOkkX72PMF" }, balanceCheckEndpoint: "https://server.fmode.cn/api/apig/getApig" }, onMissing: { action: "showPaymentQR", qrCodeUrl: "https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF", title: "扫码开通 VOC-AI 数据服务", message: "此 Skill 需要有效的 API Token。请先打开充值页面完成登录和充值;当用户提供 session token 后,请运行 node ~/.openclaw/tools/set-voc-token.js 写入 ~/.openclaw/voc-credentials.json 的 vocToken 字段,再重试当前 Skill。", tokenSetupCommandTemplate: "node ~/.openclaw/tools/set-voc-token.js {sessionToken}", tokenSetupInstructions: "当用户完成登录和充值后,如果用户把 session token 发给你,请直接运行 node ~/.openclaw/tools/set-voc-token.js 写入 ~/.openclaw/voc-credentials.json,然后重试当前 Skill。" }, onBalanceInsufficient: { action: "resolveUserThenShowPayment", title: "VOC-AI Token 余额不足,请扫码充值", message: "当前 Token 余额不足,我将为你生成专属充值链接,扫码支付后即可继续使用。" } }; const NEW_ERROR_HANDLING = { balanceInsufficient: { conditions: [ { responseField: "code", operator: "in", value: [-2, -3, -10, 402, 429] }, { responseField: "msg", operator: "contains", value: ["余额不足", "insufficient", "balance", "quota"] }, { responseField: "message", operator: "contains", value: ["余额不足", "insufficient", "balance"] } ], matchMode: "any", trigger: "tokenConfig.onBalanceInsufficient" }, unauthorized: { conditions: [ { responseField: "code", operator: "in", value: [401, 403] }, { responseField: "msg", operator: "contains", value: ["unauthorized", "token", "invalid", "auth"] } ], matchMode: "any", trigger: "tokenConfig.onMissing" } }; // ============================================ // 参数解析 // ============================================ const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const updateDeployed = args.includes('--deployed') || args.includes('--both'); const updateSource = !args.includes('--deployed') || args.includes('--both'); const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); const DEPLOYED_DIR = path.join(os.homedir(), '.openclaw', 'skills'); const CATEGORIES = [ 'voc', 'social-media', 'competitor-analysis', 'review-analysis', 'synthesis', 'social-voc', 'douyin', 'video-creation', 'jimeng', 'payment', 'test', 'workshop' ]; // ============================================ // 核心逻辑 // ============================================ function findAllApiConfigs(rootDir, isFlat) { const results = []; if (isFlat) { // Deployed: flat structure ~/.openclaw/skills/{skill}/api-config.json if (!fs.existsSync(rootDir)) return results; fs.readdirSync(rootDir).forEach(skill => { const cfgPath = path.join(rootDir, skill, 'api-config.json'); if (fs.existsSync(cfgPath)) { results.push({ path: cfgPath, skill }); } }); } else { // Source: categorized structure {category}/{skill}/api-config.json CATEGORIES.forEach(cat => { const catDir = path.join(rootDir, cat); if (!fs.existsSync(catDir)) return; fs.readdirSync(catDir).forEach(skill => { const cfgPath = path.join(catDir, skill, 'api-config.json'); if (fs.existsSync(cfgPath)) { results.push({ path: cfgPath, skill, category: cat }); } }); }); } return results; } function updateConfigFile(filePath, skillName) { let content; try { const raw = fs.readFileSync(filePath, 'utf-8'); // Remove BOM if present content = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw; } catch (e) { console.log(` [ERROR] Cannot read: ${skillName} - ${e.message}`); return false; } let config; try { config = JSON.parse(content); } catch (e) { console.log(` [ERROR] Invalid JSON: ${skillName}`); return false; } if (!config.tokenConfig) { return false; // No tokenConfig, skip } // Replace tokenConfig and errorHandling config.tokenConfig = { ...NEW_TOKEN_CONFIG }; config.errorHandling = { ...NEW_ERROR_HANDLING }; if (!dryRun) { fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8'); } return true; } // ============================================ // 主流程 // ============================================ console.log(''); console.log(' ===================================================='); console.log(' tokenConfig v2 Batch Updater (多用户动态解析)'); console.log(' ===================================================='); if (dryRun) console.log(' [DRY RUN] Preview only'); console.log(''); let totalUpdated = 0; let totalSkipped = 0; if (updateSource) { console.log(' [Source] Updating project files...'); const sourceConfigs = findAllApiConfigs(PROJECT_ROOT, false); sourceConfigs.forEach(({ path: p, skill, category }) => { if (updateConfigFile(p, skill)) { console.log(` [OK] ${category}/${skill}`); totalUpdated++; } else { totalSkipped++; } }); console.log(''); } if (updateDeployed) { console.log(' [Deployed] Updating ~/.openclaw/skills/...'); const deployedConfigs = findAllApiConfigs(DEPLOYED_DIR, true); deployedConfigs.forEach(({ path: p, skill }) => { if (updateConfigFile(p, skill)) { console.log(` [OK] ${skill}`); totalUpdated++; } else { totalSkipped++; } }); console.log(''); } console.log(' ===================================================='); console.log(` Updated: ${totalUpdated}, Skipped: ${totalSkipped}`); console.log(' ===================================================='); if (dryRun) console.log(' Remove --dry-run to apply.'); console.log('');