| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- #!/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 <session-token>
- * 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 <session-token> [--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 <token> --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}`);
- }
- }
|