#!/usr/bin/env node /** * VOC Token Preflight v1 * * 检测 VOC-AI token 状态,供 OpenClaw Workshop 在进入 Session 1 之前 * 强制调用,确保 token 有效、余额充足,否则返回可直接展示给用户的 * Markdown 支付引导文案 + 专属充值链接。 * * 用法: * node voc-token-preflight.js [apigId] [--json] * * 参数: * apigId 要检测的 APIG objectId。缺省 7HwdQZk55B(voc-ecom,覆盖 Session 1 的 category-landscape 等付费 skill)。 * --json 只输出 JSON 到 stdout(静默 stderr 日志)。适合被其他脚本 pipe 解析。 * * 输出(stdout 最后一行): * PREFLIGHT_RESULT={"status":"valid|missing|expired|insufficient_balance|error", ...} * * 退出码: * 0 - valid * 1 - missing / expired / insufficient_balance(预期内的业务状态) * 2 - error(网络或未预期错误) */ const fs = require('fs'); const os = require('os'); const path = require('path'); const PARSE_BASE = 'https://server.fmode.cn/parse'; const APP_ID = 'ncloudmaster'; const APIG_PAY_BASE = 'https://app.fmode.cn/dev/apig-pay/'; const FUN_ID = 'HOkkX72PMF'; const args = process.argv.slice(2); const apigId = args.find(a => !a.startsWith('--')) || '7HwdQZk55B'; const jsonOnly = args.includes('--json'); function log(...msg) { if (!jsonOnly) console.error(...msg); } function buildPaymentUrl(userId) { const params = new URLSearchParams(); if (userId) params.set('user', userId); params.set('apigid', apigId); params.set('fun_id', FUN_ID); return `${APIG_PAY_BASE}?${params.toString()}`; } function buildDisplayMessage(status, ctx) { const { userId, balance, paymentUrl } = ctx; switch (status) { case 'valid': return [ '🔐 VOC-AI 服务已就绪', ` 👤 账号 ${userId}`, ` 💰 余额 ${balance} 次` ].join('\n'); case 'missing': case 'expired': return [ '🔐 需要开通 VOC-AI 数据服务', '━━━━━━━━━━━━━━━━━━━━', `👉 点击开通:${paymentUrl}`, '', '📋 操作步骤:', '1. 打开上方链接完成手机号登录', '2. 选择套餐扫码支付', '3. 支付成功后,点击页面顶部"📋 复制 Token"', '4. 将 token 粘贴到对话框发给我', '', '⏸️ 我会等你,粘过来就能继续。' ].join('\n'); case 'insufficient_balance': return [ `💰 VOC-AI 余额不足(当前 ${balance} 次)`, '━━━━━━━━━━━━━━━━━━━━', `👉 专属续费:${paymentUrl}`, '', '扫码支付后自动激活,无需重新登录。' ].join('\n'); case 'error': return `⚠️ Token 预飞遇到网络/系统错误,请稍后重试。`; default: return `⚠️ 未知状态: ${status}`; } } function emit(result) { if (jsonOnly) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`PREFLIGHT_RESULT=${JSON.stringify(result)}`); } if (result.status === 'valid') process.exit(0); if (result.status === 'error') process.exit(2); process.exit(1); } (async () => { log('══════════════════════════════════════════════'); log(' VOC Token Preflight v1'); log(` APIG: ${apigId}`); log('══════════════════════════════════════════════'); // Step 1: 读取本地 token const configPath = path.join(os.homedir(), '.openclaw', 'voc-credentials.json'); let token = null; if (fs.existsSync(configPath)) { try { const raw = fs.readFileSync(configPath, 'utf-8').trim(); const config = raw ? JSON.parse(raw) : {}; token = config.vocToken; } catch (e) { log(`⚠️ 读取 voc-credentials.json 失败: ${e.message}`); } } if (!token) { log('❌ 未找到 vocToken (voc-credentials.json 不存在或为空)'); const paymentUrl = buildPaymentUrl(); return emit({ status: 'missing', userId: null, balance: 0, paymentUrl, displayMessage: buildDisplayMessage('missing', { paymentUrl }), nextAction: 'showPaymentAndWait', apigId, timestamp: new Date().toISOString() }); } log(`✅ Token: ${token.slice(0, 6)}...${token.slice(-4)}`); // Step 2: /users/me 校验 const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Session-Token': token }; let user; try { const resp = await fetch(`${PARSE_BASE}/users/me?include=company`, { headers }); user = await resp.json(); if (!resp.ok || !user.objectId) { log(`❌ Token 失效: ${user.error || user.message || JSON.stringify(user)}`); const paymentUrl = buildPaymentUrl(); return emit({ status: 'expired', userId: null, balance: 0, paymentUrl, displayMessage: buildDisplayMessage('expired', { paymentUrl }), nextAction: 'showPaymentAndWait', apigId, parseError: user.error || user.message || null, timestamp: new Date().toISOString() }); } } catch (e) { log(`⚠️ 网络错误: ${e.message}`); return emit({ status: 'error', userId: null, balance: 0, paymentUrl: buildPaymentUrl(), displayMessage: buildDisplayMessage('error', {}), nextAction: 'retry', apigId, error: e.message, timestamp: new Date().toISOString() }); } log(`✅ Token 有效`); log(`👤 用户: ${user.username || user.objectId} (${user.objectId})`); const userCompanyId = user.company?.objectId || null; if (userCompanyId) log(`🏢 Company: ${userCompanyId}`); // Step 3: 查询 APIGAuth 余额 const paymentUrl = buildPaymentUrl(user.objectId); let balance = 0; let authId = null; let authCompanyId = null; try { const where = encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: user.objectId }, api: { __type: 'Pointer', className: 'APIG', objectId: apigId } })); const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${where}&limit=1`, { headers }); const data = await resp.json(); if (data.results && data.results.length > 0) { const record = data.results[0]; balance = record.count || 0; authId = record.objectId; authCompanyId = record.company?.objectId || null; log(`💰 APIGAuth: ${authId}, 余额: ${balance} 次, 已用: ${record.used || 0} 次`); if (userCompanyId && authCompanyId !== userCompanyId) { log(`⚠️ APIGAuth Company 不匹配: ${authCompanyId || '(未绑定)'} !== ${userCompanyId}`); } } else { log(`ℹ️ APIGAuth 记录不存在 (用户从未开通此 APIG)`); } } catch (e) { log(`⚠️ APIGAuth 查询失败: ${e.message}`); } if (balance <= 0) { log(`❌ 余额不足,需要充值`); return emit({ status: 'insufficient_balance', userId: user.objectId, username: user.username || null, authId, balance, paymentUrl, displayMessage: buildDisplayMessage('insufficient_balance', { userId: user.objectId, balance, paymentUrl }), nextAction: 'showPaymentAndWait', apigId, timestamp: new Date().toISOString() }); } if (userCompanyId && authCompanyId !== userCompanyId) { const currentAuthCompany = authCompanyId || null; return emit({ status: 'auth_company_mismatch', userId: user.objectId, username: user.username || null, authId, balance, userCompanyId, authCompanyId: currentAuthCompany, paymentUrl, displayMessage: [ '⚠️ VOC-AI 服务余额存在,但社媒网关可能仍会 403。', ` 用户 Company: ${userCompanyId}`, ` APIGAuth Company: ${currentAuthCompany || '(未绑定)'}`, ' 下一步:修复 APIGAuth.company 绑定,或部署后端 user/company OR 查询后再重试。' ].join('\n'), nextAction: 'repairApigAuthCompanyBinding', apigId, timestamp: new Date().toISOString() }); } log(`✅ 预飞通过,可进入 Session 1`); return emit({ status: 'valid', userId: user.objectId, username: user.username || null, authId, balance, paymentUrl, displayMessage: buildDisplayMessage('valid', { userId: user.objectId, balance, paymentUrl }), nextAction: 'proceed', apigId, timestamp: new Date().toISOString() }); })().catch(e => { log(`💥 未预期错误: ${e.stack || e.message}`); emit({ status: 'error', userId: null, balance: 0, paymentUrl: buildPaymentUrl(), displayMessage: buildDisplayMessage('error', {}), nextAction: 'retry', apigId, error: e.message, timestamp: new Date().toISOString() }); });