set-voc-token.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/env node
  2. /**
  3. * VOC Token 设置工具 v3
  4. *
  5. * 功能(默认):
  6. * 1. 验证并写入 session token 到 ~/.openclaw/voc-credentials.json
  7. * 2. 调 /users/me 验证 token 有效性(有效才算写入成功)
  8. * 3. 只报告 Company 绑定状态,不强制创建
  9. * (后端扣费只要 user+api 指针,不要求 Company)
  10. *
  11. * 可选(加 flag 时):
  12. * --ensure-company 显式触发 Company 自动创建并绑定到用户 + APIGAuth
  13. * (只在某些需要 Company 的旧接口场景才用)
  14. * --skip-verify 跳过 /users/me 校验,纯写入(离线场景)
  15. *
  16. * 用法:
  17. * node set-voc-token.js <session-token>
  18. * node set-voc-token.js r:1234567890abcdef1234567890abcdef
  19. * node set-voc-token.js r:... --ensure-company
  20. */
  21. const fs = require('fs');
  22. const os = require('os');
  23. const path = require('path');
  24. const PARSE_BASE = 'https://server.fmode.cn/parse';
  25. const APP_ID = 'ncloudmaster';
  26. const APIG_ID = 'Vo3ROWEvDy';
  27. const args = process.argv.slice(2);
  28. const tokenArg = args.find(arg => !arg.startsWith('--'));
  29. const token = tokenArg || (args.find(arg => arg.startsWith('--token=')) || '').slice(8);
  30. const ENSURE_COMPANY = args.includes('--ensure-company');
  31. const SKIP_VERIFY = args.includes('--skip-verify');
  32. if (!token) {
  33. console.log('用法: node set-voc-token.js <session-token> [--ensure-company] [--skip-verify]');
  34. console.log('示例: node set-voc-token.js r:1234567890abcdef1234567890abcdef');
  35. console.log('说明: 默认只写 token + 验证有效性;Company 非必需');
  36. console.log(' --ensure-company 为用户自动创建并绑定 Company(仅特殊场景需要)');
  37. console.log(' --skip-verify 跳过 token 有效性校验(离线场景)');
  38. process.exit(1);
  39. }
  40. if (!/^r:[A-Za-z0-9_]{20,}$/.test(token)) {
  41. console.error('无效 token:应为 Parse session token,格式 r: 开头 + 至少20位字母数字');
  42. console.error('示例: r:o6cq805yB8NDuJ2O_ycT3EgkICREqh4E');
  43. process.exit(1);
  44. }
  45. // ─── Parse REST helpers ───
  46. function parseHeaders(sessionToken) {
  47. return {
  48. 'Content-Type': 'application/json',
  49. 'X-Parse-Application-Id': APP_ID,
  50. 'X-Parse-Session-Token': sessionToken
  51. };
  52. }
  53. function parseMasterHeaders() {
  54. return {
  55. 'Content-Type': 'application/json',
  56. 'X-Parse-Application-Id': APP_ID,
  57. 'X-Parse-Master-Key': process.env.PARSE_MASTER_KEY || ''
  58. };
  59. }
  60. // ─── Step 1: 写入 token ───
  61. const configDir = path.join(os.homedir(), '.openclaw');
  62. const configPath = path.join(configDir, 'voc-credentials.json');
  63. let config = {};
  64. if (fs.existsSync(configPath)) {
  65. try {
  66. const raw = fs.readFileSync(configPath, 'utf-8').trim();
  67. config = raw ? JSON.parse(raw) : {};
  68. } catch (error) {
  69. console.error(`读取现有配置失败: ${error.message}`);
  70. process.exit(1);
  71. }
  72. }
  73. config.vocToken = token;
  74. try {
  75. fs.mkdirSync(configDir, { recursive: true });
  76. fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
  77. } catch (error) {
  78. console.error(`写入配置失败: ${error.message}`);
  79. process.exit(1);
  80. }
  81. const masked = `${token.slice(0, 6)}...${token.slice(-4)}`;
  82. console.log('');
  83. console.log('══════════════════════════════════════════════');
  84. console.log(' VOC Token 设置工具 v3');
  85. console.log('══════════════════════════════════════════════');
  86. console.log(` ✅ Token 已写入: ${masked}`);
  87. console.log(` 📄 配置文件: ${configPath}`);
  88. // ─── Step 2: 校验 token 有效性 + 报告状态 ───
  89. (async () => {
  90. if (SKIP_VERIFY) {
  91. console.log(' ⏭️ 跳过 token 校验 (--skip-verify)');
  92. console.log('');
  93. return;
  94. }
  95. console.log('');
  96. console.log('── 校验 Token ──');
  97. let user;
  98. try {
  99. const resp = await fetch(`${PARSE_BASE}/users/me?include=company`, { headers: parseHeaders(token) });
  100. user = await resp.json();
  101. if (!resp.ok || !user.objectId) {
  102. console.error(` ❌ Token 无效或已过期: ${user.error || user.message || JSON.stringify(user)}`);
  103. console.log(' 请去支付页重新登录获取新 token: https://app.fmode.cn/dev/apig-pay/');
  104. console.log('');
  105. process.exitCode = 1;
  106. return;
  107. }
  108. console.log(` ✅ Token 有效`);
  109. console.log(` 👤 用户: ${user.username || user.objectId} (${user.objectId})`);
  110. } catch (e) {
  111. console.log(` ⚠️ 无法连接服务器: ${e.message}`);
  112. console.log(' Token 已写入但未校验;确认网络后可再次运行验证');
  113. console.log('');
  114. return;
  115. }
  116. // 报告 Company 绑定状态(仅信息,不强制)
  117. const hasCompany = user.company && user.company.objectId;
  118. console.log(` 🏢 Company: ${hasCompany ? user.company.objectId : '(未绑定,后端扣费不要求)'}`);
  119. // 报告 APIGAuth 状态(仅信息)
  120. try {
  121. const query = encodeURIComponent(JSON.stringify({
  122. user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
  123. api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID }
  124. }));
  125. const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, {
  126. headers: parseHeaders(token)
  127. });
  128. const data = await resp.json();
  129. if (data.results && data.results.length > 0) {
  130. const r = data.results[0];
  131. console.log(` 💰 APIGAuth: ${r.objectId}, 余额: ${r.count ?? 0} 次, 已用: ${r.used ?? 0} 次`);
  132. } else {
  133. console.log(` ℹ️ APIGAuth 记录不存在(首次在支付页充值后会自动创建)`);
  134. }
  135. } catch (e) {
  136. console.log(` ⚠️ APIGAuth 状态查询失败: ${e.message}`);
  137. }
  138. // 显式 --ensure-company 才触发 Company 创建
  139. if (ENSURE_COMPANY && !hasCompany) {
  140. console.log('');
  141. console.log('── 显式创建 Company (--ensure-company) ──');
  142. await ensureCompanyBinding(user, token);
  143. } else if (!hasCompany) {
  144. console.log('');
  145. console.log(' ℹ️ 如某些旧接口确实要求 Company,可重跑: node set-voc-token.js <token> --ensure-company');
  146. }
  147. console.log('');
  148. console.log(' 🎉 Token 设置完成!OpenClaw gateway 可直接使用该 token 调 skill');
  149. console.log('');
  150. })();
  151. // ─── 辅助函数:显式创建并绑定 Company ───
  152. async function ensureCompanyBinding(user, token) {
  153. // 创建 Company
  154. let companyId;
  155. try {
  156. const companyName = `${user.username || 'user'}_company`;
  157. const resp = await fetch(`${PARSE_BASE}/classes/Company`, {
  158. method: 'POST',
  159. headers: parseHeaders(token),
  160. body: JSON.stringify({ name: companyName, createdBy: { __type: 'Pointer', className: '_User', objectId: user.objectId } })
  161. });
  162. const data = await resp.json();
  163. if (!resp.ok || !data.objectId) {
  164. throw new Error(data.error || data.message || JSON.stringify(data));
  165. }
  166. companyId = data.objectId;
  167. console.log(` ✅ Company 创建成功: ${companyId} (${companyName})`);
  168. } catch (e) {
  169. console.error(` ❌ Company 创建失败: ${e.message}`);
  170. return;
  171. }
  172. // 绑定到用户
  173. try {
  174. const headers = parseMasterHeaders();
  175. if (!headers['X-Parse-Master-Key']) {
  176. headers['X-Parse-Session-Token'] = token;
  177. delete headers['X-Parse-Master-Key'];
  178. }
  179. const resp = await fetch(`${PARSE_BASE}/users/${user.objectId}`, {
  180. method: 'PUT',
  181. headers,
  182. body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } })
  183. });
  184. const data = await resp.json();
  185. if (!resp.ok) throw new Error(data.error || data.message || JSON.stringify(data));
  186. console.log(` ✅ 用户已绑定 Company`);
  187. } catch (e) {
  188. console.error(` ⚠️ 用户绑定 Company 失败: ${e.message}`);
  189. }
  190. // 绑定到 APIGAuth(如果有)
  191. try {
  192. const query = encodeURIComponent(JSON.stringify({
  193. user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
  194. api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID }
  195. }));
  196. const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, {
  197. headers: parseHeaders(token)
  198. });
  199. const data = await resp.json();
  200. if (data.results && data.results.length > 0) {
  201. const authRecord = data.results[0];
  202. if (authRecord.company && authRecord.company.objectId) {
  203. console.log(` ✅ APIGAuth 已绑定 Company`);
  204. } else {
  205. const headers2 = parseMasterHeaders();
  206. if (!headers2['X-Parse-Master-Key']) {
  207. headers2['X-Parse-Session-Token'] = token;
  208. delete headers2['X-Parse-Master-Key'];
  209. }
  210. const resp2 = await fetch(`${PARSE_BASE}/classes/APIGAuth/${authRecord.objectId}`, {
  211. method: 'PUT',
  212. headers: headers2,
  213. body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } })
  214. });
  215. if (resp2.ok) {
  216. console.log(` ✅ APIGAuth 已绑定 Company`);
  217. } else {
  218. const err = await resp2.json();
  219. console.log(` ⚠️ APIGAuth 绑定失败: ${err.error || JSON.stringify(err)}`);
  220. }
  221. }
  222. }
  223. } catch (e) {
  224. console.log(` ⚠️ APIGAuth 绑定跳过: ${e.message}`);
  225. }
  226. }