test-skill-auth.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/env node
  2. /**
  3. * 烟测:验证 skill-executor 的模板替换 + Authorization 注入是否正确
  4. * 读取某个 skill 的 api-config.json,按实际逻辑构造 headers/url,
  5. * 发一次真实请求,观察响应头部是否被后端识别。
  6. *
  7. * 用法: node scripts/tools/test-skill-auth.js <skill-dir>
  8. * 例: node scripts/tools/test-skill-auth.js social-media/instagram-search
  9. */
  10. const fs = require('fs');
  11. const os = require('os');
  12. const path = require('path');
  13. const skillDir = process.argv[2];
  14. if (!skillDir) {
  15. console.log('用法: node scripts/tools/test-skill-auth.js <skill-dir>');
  16. process.exit(1);
  17. }
  18. // 复用 executor 的模板函数
  19. function resolveTemplate(template, vars = {}) {
  20. if (typeof template !== 'string') return template;
  21. return template
  22. .replace(/\$\{(\w+)\}/g, (_, key) => vars[key] ?? '')
  23. .replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '')
  24. .replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? '');
  25. }
  26. function resolveTemplateDeep(obj, vars = {}) {
  27. if (obj == null) return obj;
  28. if (typeof obj === 'string') return resolveTemplate(obj, vars);
  29. if (Array.isArray(obj)) return obj.map(v => resolveTemplateDeep(v, vars));
  30. if (typeof obj === 'object') {
  31. const out = {};
  32. for (const [k, v] of Object.entries(obj)) out[k] = resolveTemplateDeep(v, vars);
  33. return out;
  34. }
  35. return obj;
  36. }
  37. const configPath = path.resolve(__dirname, '..', '..', skillDir, 'api-config.json');
  38. const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
  39. console.log(`Skill: ${cfg.displayName || cfg.name}`);
  40. // 读取 token
  41. const credPath = path.join(os.homedir(), '.openclaw', 'voc-credentials.json');
  42. const cred = fs.existsSync(credPath) ? JSON.parse(fs.readFileSync(credPath, 'utf-8')) : {};
  43. const vocToken = cred.vocToken || '';
  44. console.log(`Token: ${vocToken ? vocToken.slice(0, 6) + '...' + vocToken.slice(-4) : '(无)'}`);
  45. if (!cfg.endpoint) {
  46. console.error('此 skill 不是直接 endpoint 型(可能是 orchestration pipeline),跳过');
  47. process.exit(0);
  48. }
  49. const vars = {
  50. vocToken, TIKHUB_TOKEN: vocToken, token: vocToken, sessionToken: vocToken,
  51. query: 'nike', select: 'users', keyword: 'nike', count: 1, cursor: 0,
  52. username: 'nike', user_id: '1234567890'
  53. };
  54. const headers = resolveTemplateDeep(cfg.endpoint.headers || {}, vars);
  55. const query = resolveTemplateDeep(cfg.endpoint.queryParams || cfg.requestTransform?.queryParams || {}, vars);
  56. const url = resolveTemplate(cfg.endpoint.url, vars);
  57. // 兜底 Authorization
  58. if (vocToken && !headers.Authorization && !headers.authorization) {
  59. headers.Authorization = `Bearer ${vocToken}`;
  60. }
  61. // 拼 URL
  62. let finalUrl = url;
  63. const qsEntries = Object.entries(query).filter(([, v]) => v !== undefined && v !== '');
  64. if (qsEntries.length > 0) {
  65. const qs = new URLSearchParams();
  66. qsEntries.forEach(([k, v]) => qs.append(k, String(v)));
  67. finalUrl += (finalUrl.includes('?') ? '&' : '?') + qs.toString();
  68. }
  69. console.log('');
  70. console.log('─── 构造后的请求 ───');
  71. console.log(`${cfg.endpoint.method || 'GET'} ${finalUrl}`);
  72. console.log('Headers:');
  73. for (const [k, v] of Object.entries(headers)) {
  74. if (k.toLowerCase() === 'authorization' && vocToken) {
  75. console.log(` ${k}: Bearer ${vocToken.slice(0, 6)}...${vocToken.slice(-4)}`);
  76. } else {
  77. console.log(` ${k}: ${v}`);
  78. }
  79. }
  80. // 发一次真实请求
  81. console.log('');
  82. console.log('─── 实际调用 ───');
  83. (async () => {
  84. const t0 = Date.now();
  85. try {
  86. const resp = await fetch(finalUrl, {
  87. method: cfg.endpoint.method || 'GET',
  88. headers,
  89. body: ['POST','PUT','PATCH'].includes((cfg.endpoint.method||'GET').toUpperCase()) ? JSON.stringify(cfg.endpoint.body || {}) : undefined
  90. });
  91. const ms = Date.now() - t0;
  92. const text = await resp.text();
  93. let data; try { data = JSON.parse(text); } catch { data = text.slice(0, 200); }
  94. console.log(`HTTP ${resp.status} (${ms}ms)`);
  95. console.log('Response:', typeof data === 'string' ? data : JSON.stringify(data).slice(0, 300));
  96. console.log('');
  97. console.log('─── 诊断 ───');
  98. const msg = (data?.mess || data?.message || data?.error || '');
  99. if (resp.ok) {
  100. console.log('✅ 完整链路通!token 有效 + 余额充足');
  101. } else if (msg.includes('当前用户不存在') || msg.includes('无使用权限') || data?.code === 209) {
  102. console.log('✅ 模板注入正确!后端收到了 Bearer token 并尝试解析用户');
  103. console.log(' (报错原因是 token 过期,不是注入失败)');
  104. } else if (msg.includes('请输入') || msg.includes('sessionToken') || msg.includes('API_KEY')) {
  105. console.log('❌ 模板注入失败!后端没有收到 Bearer token');
  106. } else {
  107. console.log('⚠️ 其他错误:', msg);
  108. }
  109. } catch (e) {
  110. console.error('请求异常:', e.message);
  111. }
  112. })();