check-deployment.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #!/usr/bin/env node
  2. /**
  3. * OpenClaw WeChat Skill - Deployment Checker v1.2.0
  4. *
  5. * Cross-platform diagnostic script. Run this on the customer machine AFTER
  6. * running deploy-to-openclaw.*, and send the output back for troubleshooting.
  7. *
  8. * Usage:
  9. * node check-deployment.js
  10. * node check-deployment.js --openclaw-url=http://127.0.0.1:18789
  11. */
  12. 'use strict';
  13. const fs = require('fs');
  14. const path = require('path');
  15. const os = require('os');
  16. const http = require('http');
  17. const https = require('https');
  18. // ---- Args ----
  19. const args = process.argv.slice(2);
  20. const getArg = (k, d) => {
  21. const hit = args.find(a => a.startsWith('--' + k + '='));
  22. return hit ? hit.slice(('--' + k + '=').length) : d;
  23. };
  24. const OPENCLAW_URL = getArg('openclaw-url', 'http://127.0.0.1:18789');
  25. const SKILLS_ROOT = getArg('skills-root', path.join(os.homedir(), '.openclaw', 'skills'));
  26. // ---- Color helpers ----
  27. const useColor = process.stdout.isTTY;
  28. const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
  29. const green = s => c('32', s);
  30. const red = s => c('31', s);
  31. const yellow = s => c('33', s);
  32. const cyan = s => c('36', s);
  33. const gray = s => c('90', s);
  34. const OK = green('[OK] ');
  35. const ERR = red('[FAIL] ');
  36. const WARN = yellow('[WARN] ');
  37. const INFO = cyan('[INFO] ');
  38. let passed = 0, failed = 0, warned = 0;
  39. function ok(msg) { console.log(OK + msg); passed++; }
  40. function fail(msg) { console.log(ERR + msg); failed++; }
  41. function warn(msg) { console.log(WARN + msg); warned++; }
  42. function info(msg) { console.log(INFO + msg); }
  43. function header(s) {
  44. console.log('\n' + cyan('=== ' + s + ' ==='));
  45. }
  46. function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
  47. // ---- HTTP helpers ----
  48. function httpRequest(url, options = {}, body = null) {
  49. return new Promise((resolve) => {
  50. try {
  51. const u = new URL(url);
  52. const lib = u.protocol === 'https:' ? https : http;
  53. const req = lib.request({
  54. hostname: u.hostname,
  55. port: u.port || (u.protocol === 'https:' ? 443 : 80),
  56. path: u.pathname + u.search,
  57. method: options.method || 'GET',
  58. headers: options.headers || {},
  59. timeout: options.timeout || 5000,
  60. }, (res) => {
  61. const chunks = [];
  62. res.on('data', c => chunks.push(c));
  63. res.on('end', () => {
  64. resolve({ ok: true, status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') });
  65. });
  66. });
  67. req.on('error', e => resolve({ ok: false, error: e.message }));
  68. req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
  69. if (body) req.write(body);
  70. req.end();
  71. } catch (e) {
  72. resolve({ ok: false, error: e.message });
  73. }
  74. });
  75. }
  76. // =================================================================
  77. // Start checks
  78. // =================================================================
  79. console.log(cyan('=== OpenClaw WeChat Skill Deployment Check v1.2.0 ==='));
  80. info('Platform: ' + process.platform + ' / Node ' + process.version);
  81. info('HOME: ' + os.homedir());
  82. info('Skills root: ' + SKILLS_ROOT);
  83. info('OpenClaw URL: ' + OPENCLAW_URL);
  84. // -----------------------------------------------------------------
  85. header('1. Skills directory layout');
  86. // -----------------------------------------------------------------
  87. const openclawDir = path.dirname(SKILLS_ROOT);
  88. if (exists(openclawDir)) ok('~/.openclaw exists: ' + openclawDir);
  89. else fail('~/.openclaw NOT FOUND — did you run the deploy script?');
  90. if (exists(SKILLS_ROOT)) ok('skills dir exists: ' + SKILLS_ROOT);
  91. else fail('skills dir NOT FOUND — deploy script did not create ' + SKILLS_ROOT);
  92. const expectedSkills = [
  93. 'wechat-check-online',
  94. 'wechat-send-text',
  95. 'wechat-get-messages',
  96. 'wechat-get-conversations',
  97. 'wechat-get-contact-list',
  98. 'wechat-get-contact-detail',
  99. 'wechat-auto-reply-start',
  100. 'wechat-auto-reply-stop',
  101. 'wechat-auto-reply-status',
  102. ];
  103. // -----------------------------------------------------------------
  104. header('2. Each WeChat skill');
  105. // -----------------------------------------------------------------
  106. let skillCount = 0;
  107. for (const name of expectedSkills) {
  108. const skillDir = path.join(SKILLS_ROOT, name);
  109. if (!exists(skillDir)) { fail(name + ': directory missing at ' + skillDir); continue; }
  110. const sm = path.join(skillDir, 'SKILL.md');
  111. if (!exists(sm)) { fail(name + ': SKILL.md missing'); continue; }
  112. // Validate SKILL.md format: YAML frontmatter + executable instructions
  113. const content = fs.readFileSync(sm, 'utf8');
  114. const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
  115. if (!fmMatch) { fail(name + ': SKILL.md missing YAML frontmatter'); continue; }
  116. const fm = fmMatch[1];
  117. if (!/^name:\s*\S+/m.test(fm)) { fail(name + ': SKILL.md frontmatter missing "name"'); continue; }
  118. if (!/^description:\s*\S+/m.test(fm)) { fail(name + ': SKILL.md frontmatter missing "description"'); continue; }
  119. // v1.1.0+: must contain runnable bash snippet so LLM knows how to execute
  120. const hasBash = /```bash/.test(content);
  121. if (!hasBash) {
  122. fail(name + ': SKILL.md has no bash code block — LLM will not know how to execute (v1.0.0 format)');
  123. continue;
  124. }
  125. // HTTP skills (not the daemon control ones) should use curl
  126. const isControlSkill = /^wechat-auto-reply-(start|stop|status)$/.test(name);
  127. if (!isControlSkill && !/curl\s/.test(content)) {
  128. warn(name + ': SKILL.md has bash block but no curl command — please verify');
  129. }
  130. // api-config.json is optional in v1.1.0 (reference only)
  131. const ac = path.join(skillDir, 'api-config.json');
  132. if (exists(ac)) {
  133. try { JSON.parse(fs.readFileSync(ac, 'utf8')); }
  134. catch (e) { warn(name + ': api-config.json is not valid JSON — ' + e.message); }
  135. }
  136. ok(name + ' → SKILL.md OK');
  137. skillCount++;
  138. }
  139. info('Valid skills: ' + skillCount + ' / ' + expectedSkills.length);
  140. // -----------------------------------------------------------------
  141. header('3. Credentials file');
  142. // -----------------------------------------------------------------
  143. const credFile = path.join(openclawDir, 'wechat-credentials.json');
  144. const tplFile = path.join(openclawDir, 'wechat-credentials.template.json');
  145. let credUrl = null;
  146. if (exists(tplFile)) ok('template present: ' + tplFile);
  147. else warn('template missing: ' + tplFile);
  148. if (!exists(credFile)) {
  149. fail('wechat-credentials.json NOT FOUND at ' + credFile);
  150. } else {
  151. try {
  152. const cred = JSON.parse(fs.readFileSync(credFile, 'utf8'));
  153. if (!cred.wechatApiBase) fail('wechat-credentials.json: missing wechatApiBase field');
  154. else { ok('wechatApiBase = ' + cred.wechatApiBase); credUrl = cred.wechatApiBase; }
  155. } catch (e) {
  156. fail('wechat-credentials.json is NOT valid JSON — ' + e.message);
  157. }
  158. }
  159. // -----------------------------------------------------------------
  160. header('4. Workflows deployment');
  161. // -----------------------------------------------------------------
  162. const wfDir = path.join(openclawDir, 'workflows');
  163. if (!exists(wfDir)) {
  164. warn('workflows dir NOT FOUND at ' + wfDir);
  165. } else {
  166. const expectedWf = ['wechat-auto-reply.workflow.json', 'pipeline.md'];
  167. for (const f of expectedWf) {
  168. if (exists(path.join(wfDir, f))) ok('workflow: ' + f);
  169. else warn('workflow missing: ' + f);
  170. }
  171. }
  172. // -----------------------------------------------------------------
  173. header('4b. Auto-reply daemon');
  174. // -----------------------------------------------------------------
  175. const daemonFile = path.join(openclawDir, 'auto-reply-daemon.js');
  176. const cfgFile = path.join(openclawDir, 'wechat-auto-reply-config.json');
  177. const cfgTpl = path.join(openclawDir, 'wechat-auto-reply-config.template.json');
  178. const pidFile = path.join(openclawDir, 'wechat-auto-reply.pid');
  179. const logFile = path.join(openclawDir, 'logs', 'auto-reply.log');
  180. if (!exists(daemonFile)) fail('auto-reply-daemon.js NOT FOUND at ' + daemonFile);
  181. else ok('daemon: ' + daemonFile);
  182. if (!exists(cfgTpl)) warn('config template missing: ' + cfgTpl);
  183. else ok('config template: ' + cfgTpl);
  184. if (!exists(cfgFile)) warn('user config missing: ' + cfgFile);
  185. else {
  186. try { JSON.parse(fs.readFileSync(cfgFile, 'utf8')); ok('user config valid JSON'); }
  187. catch (e) { fail('user config is NOT valid JSON — ' + e.message); }
  188. }
  189. // Daemon running status
  190. if (exists(pidFile)) {
  191. const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
  192. if (!isFinite(pid)) warn('PID file has non-numeric content');
  193. else {
  194. let alive = false;
  195. try { process.kill(pid, 0); alive = true; } catch {}
  196. if (alive) ok('daemon is running, PID=' + pid);
  197. else warn('PID file exists but process ' + pid + ' is dead — remove ' + pidFile + ' then restart via wechat-auto-reply-start');
  198. }
  199. } else {
  200. info('daemon not running (no PID file) — start via wechat-auto-reply-start skill');
  201. }
  202. if (exists(logFile)) {
  203. try {
  204. const data = fs.readFileSync(logFile, 'utf8');
  205. const lines = data.trim().split(/\n/);
  206. const tail = lines.slice(-5);
  207. info('last 5 log lines:');
  208. tail.forEach(l => console.log(' ' + gray(l)));
  209. } catch {}
  210. }
  211. // -----------------------------------------------------------------
  212. header('5. WeChat backend connectivity');
  213. // -----------------------------------------------------------------
  214. (async () => {
  215. if (!credUrl) {
  216. warn('skip — no wechatApiBase in credentials');
  217. } else {
  218. const probeUrl = credUrl.replace(/\/$/, '') + '/login/check-online';
  219. info('POST ' + probeUrl);
  220. const r = await httpRequest(probeUrl, {
  221. method: 'POST',
  222. headers: { 'Content-Type': 'application/json' },
  223. timeout: 8000,
  224. }, '{}');
  225. if (!r.ok) fail('backend unreachable: ' + r.error);
  226. else if (r.status >= 200 && r.status < 500) {
  227. ok('backend responded HTTP ' + r.status);
  228. info('body: ' + (r.body || '').slice(0, 200));
  229. } else {
  230. warn('backend responded HTTP ' + r.status);
  231. info('body: ' + (r.body || '').slice(0, 200));
  232. }
  233. }
  234. // -----------------------------------------------------------------
  235. header('6. OpenClaw service probe');
  236. // -----------------------------------------------------------------
  237. info('probing ' + OPENCLAW_URL);
  238. const rRoot = await httpRequest(OPENCLAW_URL + '/', { timeout: 4000 });
  239. if (!rRoot.ok) {
  240. fail('OpenClaw service unreachable at ' + OPENCLAW_URL + ' — ' + rRoot.error);
  241. info('If OpenClaw runs on a different host/port, re-run:');
  242. info(' node check-deployment.js --openclaw-url=http://<host>:<port>');
  243. } else {
  244. ok('OpenClaw HTTP reachable (status=' + rRoot.status + ')');
  245. // Try common skill-listing endpoints (best-effort — unknown API)
  246. const probeEndpoints = [
  247. '/api/skills',
  248. '/api/skill/list',
  249. '/skills',
  250. '/api/v1/skills',
  251. ];
  252. let hitAny = false;
  253. for (const ep of probeEndpoints) {
  254. const r = await httpRequest(OPENCLAW_URL + ep, { timeout: 3000 });
  255. if (r.ok && r.status === 200 && r.body && r.body.length > 0) {
  256. const hasWechat = /wechat-/.test(r.body);
  257. if (hasWechat) {
  258. ok('skill API ' + ep + ' lists our wechat-* skills');
  259. } else {
  260. warn('skill API ' + ep + ' reachable but NO wechat-* found in response');
  261. info('body (first 300): ' + r.body.slice(0, 300));
  262. }
  263. hitAny = true;
  264. break;
  265. }
  266. }
  267. if (!hitAny) {
  268. warn('could not auto-detect skill list API — please check OpenClaw UI -> 技能 panel manually');
  269. }
  270. }
  271. // -----------------------------------------------------------------
  272. header('Summary');
  273. // -----------------------------------------------------------------
  274. console.log(`${green('passed')}: ${passed} ${yellow('warnings')}: ${warned} ${red('failed')}: ${failed}`);
  275. if (failed === 0 && skillCount === expectedSkills.length) {
  276. console.log('\n' + green('✓ Deployment files look correct.'));
  277. console.log(gray('If the main agent still does not see wechat-* skills:'));
  278. console.log(gray(' 1. Restart the OpenClaw service to force skill re-scan'));
  279. console.log(gray(' 2. In OpenClaw UI -> 技能 panel, confirm wechat-* skills are listed'));
  280. console.log(gray(' 3. In the agent config, make sure these skills are enabled for the main agent'));
  281. } else {
  282. console.log('\n' + red('✗ Deployment has issues — fix the FAIL items above and re-run.'));
  283. }
  284. process.exit(failed > 0 ? 1 : 0);
  285. })();