#!/usr/bin/env node /** * VOC Token 设置工具 v3 * * 功能(默认): * 1. 验证并写入 session token 到 ~/.openclaw/voc-credentials.json * 2. 调 /users/me 验证 token 有效性(有效才算写入成功) * 3. 只报告 Company 绑定状态,不强制创建 * (后端扣费只要 user+api 指针,不要求 Company) * * 可选(加 flag 时): * --ensure-company 显式触发 Company 自动创建并绑定到用户 + APIGAuth * (只在某些需要 Company 的旧接口场景才用) * --skip-verify 跳过 /users/me 校验,纯写入(离线场景) * * 用法: * node set-voc-token.js * node set-voc-token.js r:1234567890abcdef1234567890abcdef * node set-voc-token.js r:... --ensure-company */ 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_ID = 'Vo3ROWEvDy'; const args = process.argv.slice(2); const tokenArg = args.find(arg => !arg.startsWith('--')); const USE_CURRENT = args.includes('--current') || args.includes('--from-config'); let token = tokenArg || (args.find(arg => arg.startsWith('--token=')) || '').slice(8); const ENSURE_COMPANY = args.includes('--ensure-company'); const SKIP_VERIFY = args.includes('--skip-verify'); const configDir = path.join(os.homedir(), '.openclaw'); const configPath = path.join(configDir, 'voc-credentials.json'); if (!token && USE_CURRENT && fs.existsSync(configPath)) { try { const raw = fs.readFileSync(configPath, 'utf-8').trim(); const currentConfig = raw ? JSON.parse(raw) : {}; token = currentConfig.vocToken || currentConfig.token || ''; } catch (_) {} } if (!token) { console.log('用法: node set-voc-token.js [--ensure-company] [--skip-verify]'); console.log(' node set-voc-token.js --current --ensure-company'); console.log('示例: node set-voc-token.js r:1234567890abcdef1234567890abcdef'); console.log('说明: 默认只写 token + 验证有效性;Company 非必需'); console.log(' --ensure-company 为用户自动创建并绑定 Company(仅特殊场景需要)'); console.log(' --skip-verify 跳过 token 有效性校验(离线场景)'); process.exit(1); } if (!/^r:[A-Za-z0-9_]{20,}$/.test(token)) { console.error('无效 token:应为 Parse session token,格式 r: 开头 + 至少20位字母数字'); console.error('示例: r:o6cq805yB8NDuJ2O_ycT3EgkICREqh4E'); process.exit(1); } // ─── Parse REST helpers ─── function parseHeaders(sessionToken) { return { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Session-Token': sessionToken }; } function parseMasterHeaders() { return { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Master-Key': process.env.PARSE_MASTER_KEY || '' }; } // ─── Step 1: 写入 token ─── let config = {}; if (fs.existsSync(configPath)) { try { const raw = fs.readFileSync(configPath, 'utf-8').trim(); config = raw ? JSON.parse(raw) : {}; } catch (error) { console.error(`读取现有配置失败: ${error.message}`); process.exit(1); } } config.vocToken = token; try { fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); } catch (error) { console.error(`写入配置失败: ${error.message}`); process.exit(1); } const masked = `${token.slice(0, 6)}...${token.slice(-4)}`; console.log(''); console.log('══════════════════════════════════════════════'); console.log(' VOC Token 设置工具 v3'); console.log('══════════════════════════════════════════════'); console.log(` ✅ Token 已写入: ${masked}`); console.log(` 📄 配置文件: ${configPath}`); // ─── Step 2: 校验 token 有效性 + 报告状态 ─── (async () => { if (SKIP_VERIFY) { console.log(' ⏭️ 跳过 token 校验 (--skip-verify)'); console.log(''); return; } console.log(''); console.log('── 校验 Token ──'); let user; try { const resp = await fetch(`${PARSE_BASE}/users/me?include=company`, { headers: parseHeaders(token) }); user = await resp.json(); if (!resp.ok || !user.objectId) { console.error(` ❌ Token 无效或已过期: ${user.error || user.message || JSON.stringify(user)}`); console.log(' 请去支付页重新登录获取新 token: https://app.fmode.cn/dev/apig-pay/'); console.log(''); process.exitCode = 1; return; } console.log(` ✅ Token 有效`); console.log(` 👤 用户: ${user.username || user.objectId} (${user.objectId})`); } catch (e) { console.log(` ⚠️ 无法连接服务器: ${e.message}`); console.log(' Token 已写入但未校验;确认网络后可再次运行验证'); console.log(''); return; } // 报告 Company 绑定状态(仅信息,不强制) const hasCompany = user.company && user.company.objectId; console.log(` 🏢 Company: ${hasCompany ? user.company.objectId : '(未绑定,后端扣费不要求)'}`); // 报告 APIGAuth 状态(仅信息) let apigAuthRecord = null; try { const query = encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: user.objectId }, api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID } })); const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, { headers: parseHeaders(token) }); const data = await resp.json(); if (data.results && data.results.length > 0) { const r = data.results[0]; apigAuthRecord = r; console.log(` 💰 APIGAuth: ${r.objectId}, 余额: ${r.count ?? 0} 次, 已用: ${r.used ?? 0} 次`); const authCompanyId = r.company?.objectId || null; if (hasCompany && authCompanyId !== user.company.objectId) { console.log(` ⚠️ APIGAuth Company 不匹配: ${authCompanyId || '(未绑定)'} !== ${user.company.objectId}`); } } else { console.log(` ℹ️ APIGAuth 记录不存在(首次在支付页充值后会自动创建)`); } } catch (e) { console.log(` ⚠️ APIGAuth 状态查询失败: ${e.message}`); } // 显式 --ensure-company 才触发 Company 创建 if (ENSURE_COMPANY && !hasCompany) { console.log(''); console.log('── 显式创建 Company (--ensure-company) ──'); await ensureCompanyBinding(user, token); } else if (ENSURE_COMPANY && hasCompany) { console.log(''); console.log('── 修复 APIGAuth Company 绑定 (--ensure-company) ──'); await ensureApigAuthCompany(user, token, user.company.objectId); } else if (!hasCompany) { console.log(''); console.log(' ℹ️ 如某些旧接口确实要求 Company,可重跑: node set-voc-token.js --ensure-company'); } else if (apigAuthRecord && (apigAuthRecord.company?.objectId || null) !== user.company.objectId) { console.log(''); console.log(' ℹ️ 检测到 APIGAuth Company 可能导致 voc-social 403,可重跑: node set-voc-token.js --current --ensure-company'); } console.log(''); console.log(' 🎉 Token 设置完成!OpenClaw gateway 可直接使用该 token 调 skill'); console.log(''); })(); async function ensureApigAuthCompany(user, token, companyId) { if (!companyId) { console.log(' ⚠️ 缺少 Company,无法绑定 APIGAuth'); return; } try { const query = encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: user.objectId }, api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID } })); const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, { headers: parseHeaders(token) }); const data = await resp.json(); if (!data.results || data.results.length === 0) { console.log(' ℹ️ APIGAuth 记录不存在,先在支付页开通/充值后再绑定'); return; } const authRecord = data.results[0]; const currentCompanyId = authRecord.company?.objectId || null; if (currentCompanyId === companyId) { console.log(` ✅ APIGAuth 已绑定当前 Company: ${companyId}`); return; } const headers = parseMasterHeaders(); if (!headers['X-Parse-Master-Key']) { headers['X-Parse-Session-Token'] = token; delete headers['X-Parse-Master-Key']; } const updateResp = await fetch(`${PARSE_BASE}/classes/APIGAuth/${authRecord.objectId}`, { method: 'PUT', headers, body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } }) }); if (updateResp.ok) { console.log(` ✅ APIGAuth Company 已修复: ${currentCompanyId || '(未绑定)'} -> ${companyId}`); } else { const err = await updateResp.json(); console.log(` ⚠️ APIGAuth Company 修复失败: ${err.error || JSON.stringify(err)}`); } } catch (e) { console.log(` ⚠️ APIGAuth Company 修复跳过: ${e.message}`); } } // ─── 辅助函数:显式创建并绑定 Company ─── async function ensureCompanyBinding(user, token) { // 创建 Company let companyId; try { const companyName = `${user.username || 'user'}_company`; const resp = await fetch(`${PARSE_BASE}/classes/Company`, { method: 'POST', headers: parseHeaders(token), body: JSON.stringify({ name: companyName, createdBy: { __type: 'Pointer', className: '_User', objectId: user.objectId } }) }); const data = await resp.json(); if (!resp.ok || !data.objectId) { throw new Error(data.error || data.message || JSON.stringify(data)); } companyId = data.objectId; console.log(` ✅ Company 创建成功: ${companyId} (${companyName})`); } catch (e) { console.error(` ❌ Company 创建失败: ${e.message}`); return; } // 绑定到用户 try { const headers = parseMasterHeaders(); if (!headers['X-Parse-Master-Key']) { headers['X-Parse-Session-Token'] = token; delete headers['X-Parse-Master-Key']; } const resp = await fetch(`${PARSE_BASE}/users/${user.objectId}`, { method: 'PUT', headers, body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } }) }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || data.message || JSON.stringify(data)); console.log(` ✅ 用户已绑定 Company`); } catch (e) { console.error(` ⚠️ 用户绑定 Company 失败: ${e.message}`); } // 绑定到 APIGAuth(如果有) try { const query = encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: user.objectId }, api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID } })); const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, { headers: parseHeaders(token) }); const data = await resp.json(); if (data.results && data.results.length > 0) { const authRecord = data.results[0]; const currentCompanyId = authRecord.company?.objectId || null; if (currentCompanyId === companyId) { console.log(` ✅ APIGAuth 已绑定 Company`); } else { const headers2 = parseMasterHeaders(); if (!headers2['X-Parse-Master-Key']) { headers2['X-Parse-Session-Token'] = token; delete headers2['X-Parse-Master-Key']; } const resp2 = await fetch(`${PARSE_BASE}/classes/APIGAuth/${authRecord.objectId}`, { method: 'PUT', headers: headers2, body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } }) }); if (resp2.ok) { console.log(` ✅ APIGAuth 已绑定 Company`); } else { const err = await resp2.json(); console.log(` ⚠️ APIGAuth 绑定失败: ${err.error || JSON.stringify(err)}`); } } } } catch (e) { console.log(` ⚠️ APIGAuth 绑定跳过: ${e.message}`); } }