| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- #!/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()
- });
- });
|