set-voc-token.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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 USE_CURRENT = args.includes('--current') || args.includes('--from-config');
  30. let token = tokenArg || (args.find(arg => arg.startsWith('--token=')) || '').slice(8);
  31. const ENSURE_COMPANY = args.includes('--ensure-company');
  32. const SKIP_VERIFY = args.includes('--skip-verify');
  33. const configDir = path.join(os.homedir(), '.openclaw');
  34. const configPath = path.join(configDir, 'voc-credentials.json');
  35. if (!token && USE_CURRENT && fs.existsSync(configPath)) {
  36. try {
  37. const raw = fs.readFileSync(configPath, 'utf-8').trim();
  38. const currentConfig = raw ? JSON.parse(raw) : {};
  39. token = currentConfig.vocToken || currentConfig.token || '';
  40. } catch (_) {}
  41. }
  42. if (!token) {
  43. console.log('用法: node set-voc-token.js <session-token> [--ensure-company] [--skip-verify]');
  44. console.log(' node set-voc-token.js --current --ensure-company');
  45. console.log('示例: node set-voc-token.js r:1234567890abcdef1234567890abcdef');
  46. console.log('说明: 默认只写 token + 验证有效性;Company 非必需');
  47. console.log(' --ensure-company 为用户自动创建并绑定 Company(仅特殊场景需要)');
  48. console.log(' --skip-verify 跳过 token 有效性校验(离线场景)');
  49. process.exit(1);
  50. }
  51. if (!/^r:[A-Za-z0-9_]{20,}$/.test(token)) {
  52. console.error('无效 token:应为 Parse session token,格式 r: 开头 + 至少20位字母数字');
  53. console.error('示例: r:o6cq805yB8NDuJ2O_ycT3EgkICREqh4E');
  54. process.exit(1);
  55. }
  56. // ─── Parse REST helpers ───
  57. function parseHeaders(sessionToken) {
  58. return {
  59. 'Content-Type': 'application/json',
  60. 'X-Parse-Application-Id': APP_ID,
  61. 'X-Parse-Session-Token': sessionToken
  62. };
  63. }
  64. function parseMasterHeaders() {
  65. return {
  66. 'Content-Type': 'application/json',
  67. 'X-Parse-Application-Id': APP_ID,
  68. 'X-Parse-Master-Key': process.env.PARSE_MASTER_KEY || ''
  69. };
  70. }
  71. // ─── Step 1: 写入 token ───
  72. let config = {};
  73. if (fs.existsSync(configPath)) {
  74. try {
  75. const raw = fs.readFileSync(configPath, 'utf-8').trim();
  76. config = raw ? JSON.parse(raw) : {};
  77. } catch (error) {
  78. console.error(`读取现有配置失败: ${error.message}`);
  79. process.exit(1);
  80. }
  81. }
  82. config.vocToken = token;
  83. try {
  84. fs.mkdirSync(configDir, { recursive: true });
  85. fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
  86. } catch (error) {
  87. console.error(`写入配置失败: ${error.message}`);
  88. process.exit(1);
  89. }
  90. const masked = `${token.slice(0, 6)}...${token.slice(-4)}`;
  91. console.log('');
  92. console.log('══════════════════════════════════════════════');
  93. console.log(' VOC Token 设置工具 v3');
  94. console.log('══════════════════════════════════════════════');
  95. console.log(` ✅ Token 已写入: ${masked}`);
  96. console.log(` 📄 配置文件: ${configPath}`);
  97. // ─── Step 2: 校验 token 有效性 + 报告状态 ───
  98. (async () => {
  99. if (SKIP_VERIFY) {
  100. console.log(' ⏭️ 跳过 token 校验 (--skip-verify)');
  101. console.log('');
  102. return;
  103. }
  104. console.log('');
  105. console.log('── 校验 Token ──');
  106. let user;
  107. try {
  108. const resp = await fetch(`${PARSE_BASE}/users/me?include=company`, { headers: parseHeaders(token) });
  109. user = await resp.json();
  110. if (!resp.ok || !user.objectId) {
  111. console.error(` ❌ Token 无效或已过期: ${user.error || user.message || JSON.stringify(user)}`);
  112. console.log(' 请去支付页重新登录获取新 token: https://app.fmode.cn/dev/apig-pay/');
  113. console.log('');
  114. process.exitCode = 1;
  115. return;
  116. }
  117. console.log(` ✅ Token 有效`);
  118. console.log(` 👤 用户: ${user.username || user.objectId} (${user.objectId})`);
  119. } catch (e) {
  120. console.log(` ⚠️ 无法连接服务器: ${e.message}`);
  121. console.log(' Token 已写入但未校验;确认网络后可再次运行验证');
  122. console.log('');
  123. return;
  124. }
  125. // 报告 Company 绑定状态(仅信息,不强制)
  126. const hasCompany = user.company && user.company.objectId;
  127. console.log(` 🏢 Company: ${hasCompany ? user.company.objectId : '(未绑定,后端扣费不要求)'}`);
  128. // 报告 APIGAuth 状态(仅信息)
  129. let apigAuthRecord = null;
  130. try {
  131. const query = encodeURIComponent(JSON.stringify({
  132. user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
  133. api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID }
  134. }));
  135. const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, {
  136. headers: parseHeaders(token)
  137. });
  138. const data = await resp.json();
  139. if (data.results && data.results.length > 0) {
  140. const r = data.results[0];
  141. apigAuthRecord = r;
  142. console.log(` 💰 APIGAuth: ${r.objectId}, 余额: ${r.count ?? 0} 次, 已用: ${r.used ?? 0} 次`);
  143. const authCompanyId = r.company?.objectId || null;
  144. if (hasCompany && authCompanyId !== user.company.objectId) {
  145. console.log(` ⚠️ APIGAuth Company 不匹配: ${authCompanyId || '(未绑定)'} !== ${user.company.objectId}`);
  146. }
  147. } else {
  148. console.log(` ℹ️ APIGAuth 记录不存在(首次在支付页充值后会自动创建)`);
  149. }
  150. } catch (e) {
  151. console.log(` ⚠️ APIGAuth 状态查询失败: ${e.message}`);
  152. }
  153. // 显式 --ensure-company 才触发 Company 创建
  154. if (ENSURE_COMPANY && !hasCompany) {
  155. console.log('');
  156. console.log('── 显式创建 Company (--ensure-company) ──');
  157. await ensureCompanyBinding(user, token);
  158. } else if (ENSURE_COMPANY && hasCompany) {
  159. console.log('');
  160. console.log('── 修复 APIGAuth Company 绑定 (--ensure-company) ──');
  161. await ensureApigAuthCompany(user, token, user.company.objectId);
  162. } else if (!hasCompany) {
  163. console.log('');
  164. console.log(' ℹ️ 如某些旧接口确实要求 Company,可重跑: node set-voc-token.js <token> --ensure-company');
  165. } else if (apigAuthRecord && (apigAuthRecord.company?.objectId || null) !== user.company.objectId) {
  166. console.log('');
  167. console.log(' ℹ️ 检测到 APIGAuth Company 可能导致 voc-social 403,可重跑: node set-voc-token.js --current --ensure-company');
  168. }
  169. console.log('');
  170. console.log(' 🎉 Token 设置完成!OpenClaw gateway 可直接使用该 token 调 skill');
  171. console.log('');
  172. })();
  173. async function ensureApigAuthCompany(user, token, companyId) {
  174. if (!companyId) {
  175. console.log(' ⚠️ 缺少 Company,无法绑定 APIGAuth');
  176. return;
  177. }
  178. try {
  179. const query = encodeURIComponent(JSON.stringify({
  180. user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
  181. api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID }
  182. }));
  183. const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, {
  184. headers: parseHeaders(token)
  185. });
  186. const data = await resp.json();
  187. if (!data.results || data.results.length === 0) {
  188. console.log(' ℹ️ APIGAuth 记录不存在,先在支付页开通/充值后再绑定');
  189. return;
  190. }
  191. const authRecord = data.results[0];
  192. const currentCompanyId = authRecord.company?.objectId || null;
  193. if (currentCompanyId === companyId) {
  194. console.log(` ✅ APIGAuth 已绑定当前 Company: ${companyId}`);
  195. return;
  196. }
  197. const headers = parseMasterHeaders();
  198. if (!headers['X-Parse-Master-Key']) {
  199. headers['X-Parse-Session-Token'] = token;
  200. delete headers['X-Parse-Master-Key'];
  201. }
  202. const updateResp = await fetch(`${PARSE_BASE}/classes/APIGAuth/${authRecord.objectId}`, {
  203. method: 'PUT',
  204. headers,
  205. body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } })
  206. });
  207. if (updateResp.ok) {
  208. console.log(` ✅ APIGAuth Company 已修复: ${currentCompanyId || '(未绑定)'} -> ${companyId}`);
  209. } else {
  210. const err = await updateResp.json();
  211. console.log(` ⚠️ APIGAuth Company 修复失败: ${err.error || JSON.stringify(err)}`);
  212. }
  213. } catch (e) {
  214. console.log(` ⚠️ APIGAuth Company 修复跳过: ${e.message}`);
  215. }
  216. }
  217. // ─── 辅助函数:显式创建并绑定 Company ───
  218. async function ensureCompanyBinding(user, token) {
  219. // 创建 Company
  220. let companyId;
  221. try {
  222. const companyName = `${user.username || 'user'}_company`;
  223. const resp = await fetch(`${PARSE_BASE}/classes/Company`, {
  224. method: 'POST',
  225. headers: parseHeaders(token),
  226. body: JSON.stringify({ name: companyName, createdBy: { __type: 'Pointer', className: '_User', objectId: user.objectId } })
  227. });
  228. const data = await resp.json();
  229. if (!resp.ok || !data.objectId) {
  230. throw new Error(data.error || data.message || JSON.stringify(data));
  231. }
  232. companyId = data.objectId;
  233. console.log(` ✅ Company 创建成功: ${companyId} (${companyName})`);
  234. } catch (e) {
  235. console.error(` ❌ Company 创建失败: ${e.message}`);
  236. return;
  237. }
  238. // 绑定到用户
  239. try {
  240. const headers = parseMasterHeaders();
  241. if (!headers['X-Parse-Master-Key']) {
  242. headers['X-Parse-Session-Token'] = token;
  243. delete headers['X-Parse-Master-Key'];
  244. }
  245. const resp = await fetch(`${PARSE_BASE}/users/${user.objectId}`, {
  246. method: 'PUT',
  247. headers,
  248. body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } })
  249. });
  250. const data = await resp.json();
  251. if (!resp.ok) throw new Error(data.error || data.message || JSON.stringify(data));
  252. console.log(` ✅ 用户已绑定 Company`);
  253. } catch (e) {
  254. console.error(` ⚠️ 用户绑定 Company 失败: ${e.message}`);
  255. }
  256. // 绑定到 APIGAuth(如果有)
  257. try {
  258. const query = encodeURIComponent(JSON.stringify({
  259. user: { __type: 'Pointer', className: '_User', objectId: user.objectId },
  260. api: { __type: 'Pointer', className: 'APIG', objectId: APIG_ID }
  261. }));
  262. const resp = await fetch(`${PARSE_BASE}/classes/APIGAuth?where=${query}&limit=1`, {
  263. headers: parseHeaders(token)
  264. });
  265. const data = await resp.json();
  266. if (data.results && data.results.length > 0) {
  267. const authRecord = data.results[0];
  268. const currentCompanyId = authRecord.company?.objectId || null;
  269. if (currentCompanyId === companyId) {
  270. console.log(` ✅ APIGAuth 已绑定 Company`);
  271. } else {
  272. const headers2 = parseMasterHeaders();
  273. if (!headers2['X-Parse-Master-Key']) {
  274. headers2['X-Parse-Session-Token'] = token;
  275. delete headers2['X-Parse-Master-Key'];
  276. }
  277. const resp2 = await fetch(`${PARSE_BASE}/classes/APIGAuth/${authRecord.objectId}`, {
  278. method: 'PUT',
  279. headers: headers2,
  280. body: JSON.stringify({ company: { __type: 'Pointer', className: 'Company', objectId: companyId } })
  281. });
  282. if (resp2.ok) {
  283. console.log(` ✅ APIGAuth 已绑定 Company`);
  284. } else {
  285. const err = await resp2.json();
  286. console.log(` ⚠️ APIGAuth 绑定失败: ${err.error || JSON.stringify(err)}`);
  287. }
  288. }
  289. }
  290. } catch (e) {
  291. console.log(` ⚠️ APIGAuth 绑定跳过: ${e.message}`);
  292. }
  293. }