#!/usr/bin/env node /** * OpenClaw WeChat Skill - Deployment Checker v1.2.0 * * Cross-platform diagnostic script. Run this on the customer machine AFTER * running deploy-to-openclaw.*, and send the output back for troubleshooting. * * Usage: * node check-deployment.js * node check-deployment.js --openclaw-url=http://127.0.0.1:18789 */ 'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const http = require('http'); const https = require('https'); // ---- Args ---- const args = process.argv.slice(2); const getArg = (k, d) => { const hit = args.find(a => a.startsWith('--' + k + '=')); return hit ? hit.slice(('--' + k + '=').length) : d; }; const OPENCLAW_URL = getArg('openclaw-url', 'http://127.0.0.1:18789'); const SKILLS_ROOT = getArg('skills-root', path.join(os.homedir(), '.openclaw', 'skills')); // ---- Color helpers ---- const useColor = process.stdout.isTTY; const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s); const green = s => c('32', s); const red = s => c('31', s); const yellow = s => c('33', s); const cyan = s => c('36', s); const gray = s => c('90', s); const OK = green('[OK] '); const ERR = red('[FAIL] '); const WARN = yellow('[WARN] '); const INFO = cyan('[INFO] '); let passed = 0, failed = 0, warned = 0; function ok(msg) { console.log(OK + msg); passed++; } function fail(msg) { console.log(ERR + msg); failed++; } function warn(msg) { console.log(WARN + msg); warned++; } function info(msg) { console.log(INFO + msg); } function header(s) { console.log('\n' + cyan('=== ' + s + ' ===')); } function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } } // ---- HTTP helpers ---- function httpRequest(url, options = {}, body = null) { return new Promise((resolve) => { try { const u = new URL(url); const lib = u.protocol === 'https:' ? https : http; const req = lib.request({ hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80), path: u.pathname + u.search, method: options.method || 'GET', headers: options.headers || {}, timeout: options.timeout || 5000, }, (res) => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => { resolve({ ok: true, status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }); }); }); req.on('error', e => resolve({ ok: false, error: e.message })); req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); if (body) req.write(body); req.end(); } catch (e) { resolve({ ok: false, error: e.message }); } }); } // ================================================================= // Start checks // ================================================================= console.log(cyan('=== OpenClaw WeChat Skill Deployment Check v1.2.0 ===')); info('Platform: ' + process.platform + ' / Node ' + process.version); info('HOME: ' + os.homedir()); info('Skills root: ' + SKILLS_ROOT); info('OpenClaw URL: ' + OPENCLAW_URL); // ----------------------------------------------------------------- header('1. Skills directory layout'); // ----------------------------------------------------------------- const openclawDir = path.dirname(SKILLS_ROOT); if (exists(openclawDir)) ok('~/.openclaw exists: ' + openclawDir); else fail('~/.openclaw NOT FOUND — did you run the deploy script?'); if (exists(SKILLS_ROOT)) ok('skills dir exists: ' + SKILLS_ROOT); else fail('skills dir NOT FOUND — deploy script did not create ' + SKILLS_ROOT); const expectedSkills = [ 'wechat-check-online', 'wechat-send-text', 'wechat-get-messages', 'wechat-get-conversations', 'wechat-get-contact-list', 'wechat-get-contact-detail', 'wechat-auto-reply-start', 'wechat-auto-reply-stop', 'wechat-auto-reply-status', ]; // ----------------------------------------------------------------- header('2. Each WeChat skill'); // ----------------------------------------------------------------- let skillCount = 0; for (const name of expectedSkills) { const skillDir = path.join(SKILLS_ROOT, name); if (!exists(skillDir)) { fail(name + ': directory missing at ' + skillDir); continue; } const sm = path.join(skillDir, 'SKILL.md'); if (!exists(sm)) { fail(name + ': SKILL.md missing'); continue; } // Validate SKILL.md format: YAML frontmatter + executable instructions const content = fs.readFileSync(sm, 'utf8'); const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); if (!fmMatch) { fail(name + ': SKILL.md missing YAML frontmatter'); continue; } const fm = fmMatch[1]; if (!/^name:\s*\S+/m.test(fm)) { fail(name + ': SKILL.md frontmatter missing "name"'); continue; } if (!/^description:\s*\S+/m.test(fm)) { fail(name + ': SKILL.md frontmatter missing "description"'); continue; } // v1.1.0+: must contain runnable bash snippet so LLM knows how to execute const hasBash = /```bash/.test(content); if (!hasBash) { fail(name + ': SKILL.md has no bash code block — LLM will not know how to execute (v1.0.0 format)'); continue; } // HTTP skills (not the daemon control ones) should use curl const isControlSkill = /^wechat-auto-reply-(start|stop|status)$/.test(name); if (!isControlSkill && !/curl\s/.test(content)) { warn(name + ': SKILL.md has bash block but no curl command — please verify'); } // api-config.json is optional in v1.1.0 (reference only) const ac = path.join(skillDir, 'api-config.json'); if (exists(ac)) { try { JSON.parse(fs.readFileSync(ac, 'utf8')); } catch (e) { warn(name + ': api-config.json is not valid JSON — ' + e.message); } } ok(name + ' → SKILL.md OK'); skillCount++; } info('Valid skills: ' + skillCount + ' / ' + expectedSkills.length); // ----------------------------------------------------------------- header('3. Credentials file'); // ----------------------------------------------------------------- const credFile = path.join(openclawDir, 'wechat-credentials.json'); const tplFile = path.join(openclawDir, 'wechat-credentials.template.json'); let credUrl = null; if (exists(tplFile)) ok('template present: ' + tplFile); else warn('template missing: ' + tplFile); if (!exists(credFile)) { fail('wechat-credentials.json NOT FOUND at ' + credFile); } else { try { const cred = JSON.parse(fs.readFileSync(credFile, 'utf8')); if (!cred.wechatApiBase) fail('wechat-credentials.json: missing wechatApiBase field'); else { ok('wechatApiBase = ' + cred.wechatApiBase); credUrl = cred.wechatApiBase; } } catch (e) { fail('wechat-credentials.json is NOT valid JSON — ' + e.message); } } // ----------------------------------------------------------------- header('4. Workflows deployment'); // ----------------------------------------------------------------- const wfDir = path.join(openclawDir, 'workflows'); if (!exists(wfDir)) { warn('workflows dir NOT FOUND at ' + wfDir); } else { const expectedWf = ['wechat-auto-reply.workflow.json', 'pipeline.md']; for (const f of expectedWf) { if (exists(path.join(wfDir, f))) ok('workflow: ' + f); else warn('workflow missing: ' + f); } } // ----------------------------------------------------------------- header('4b. Auto-reply daemon'); // ----------------------------------------------------------------- const daemonFile = path.join(openclawDir, 'auto-reply-daemon.js'); const cfgFile = path.join(openclawDir, 'wechat-auto-reply-config.json'); const cfgTpl = path.join(openclawDir, 'wechat-auto-reply-config.template.json'); const pidFile = path.join(openclawDir, 'wechat-auto-reply.pid'); const logFile = path.join(openclawDir, 'logs', 'auto-reply.log'); if (!exists(daemonFile)) fail('auto-reply-daemon.js NOT FOUND at ' + daemonFile); else ok('daemon: ' + daemonFile); if (!exists(cfgTpl)) warn('config template missing: ' + cfgTpl); else ok('config template: ' + cfgTpl); if (!exists(cfgFile)) warn('user config missing: ' + cfgFile); else { try { JSON.parse(fs.readFileSync(cfgFile, 'utf8')); ok('user config valid JSON'); } catch (e) { fail('user config is NOT valid JSON — ' + e.message); } } // Daemon running status if (exists(pidFile)) { const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); if (!isFinite(pid)) warn('PID file has non-numeric content'); else { let alive = false; try { process.kill(pid, 0); alive = true; } catch {} if (alive) ok('daemon is running, PID=' + pid); else warn('PID file exists but process ' + pid + ' is dead — remove ' + pidFile + ' then restart via wechat-auto-reply-start'); } } else { info('daemon not running (no PID file) — start via wechat-auto-reply-start skill'); } if (exists(logFile)) { try { const data = fs.readFileSync(logFile, 'utf8'); const lines = data.trim().split(/\n/); const tail = lines.slice(-5); info('last 5 log lines:'); tail.forEach(l => console.log(' ' + gray(l))); } catch {} } // ----------------------------------------------------------------- header('5. WeChat backend connectivity'); // ----------------------------------------------------------------- (async () => { if (!credUrl) { warn('skip — no wechatApiBase in credentials'); } else { const probeUrl = credUrl.replace(/\/$/, '') + '/login/check-online'; info('POST ' + probeUrl); const r = await httpRequest(probeUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 8000, }, '{}'); if (!r.ok) fail('backend unreachable: ' + r.error); else if (r.status >= 200 && r.status < 500) { ok('backend responded HTTP ' + r.status); info('body: ' + (r.body || '').slice(0, 200)); } else { warn('backend responded HTTP ' + r.status); info('body: ' + (r.body || '').slice(0, 200)); } } // ----------------------------------------------------------------- header('6. OpenClaw service probe'); // ----------------------------------------------------------------- info('probing ' + OPENCLAW_URL); const rRoot = await httpRequest(OPENCLAW_URL + '/', { timeout: 4000 }); if (!rRoot.ok) { fail('OpenClaw service unreachable at ' + OPENCLAW_URL + ' — ' + rRoot.error); info('If OpenClaw runs on a different host/port, re-run:'); info(' node check-deployment.js --openclaw-url=http://:'); } else { ok('OpenClaw HTTP reachable (status=' + rRoot.status + ')'); // Try common skill-listing endpoints (best-effort — unknown API) const probeEndpoints = [ '/api/skills', '/api/skill/list', '/skills', '/api/v1/skills', ]; let hitAny = false; for (const ep of probeEndpoints) { const r = await httpRequest(OPENCLAW_URL + ep, { timeout: 3000 }); if (r.ok && r.status === 200 && r.body && r.body.length > 0) { const hasWechat = /wechat-/.test(r.body); if (hasWechat) { ok('skill API ' + ep + ' lists our wechat-* skills'); } else { warn('skill API ' + ep + ' reachable but NO wechat-* found in response'); info('body (first 300): ' + r.body.slice(0, 300)); } hitAny = true; break; } } if (!hitAny) { warn('could not auto-detect skill list API — please check OpenClaw UI -> 技能 panel manually'); } } // ----------------------------------------------------------------- header('Summary'); // ----------------------------------------------------------------- console.log(`${green('passed')}: ${passed} ${yellow('warnings')}: ${warned} ${red('failed')}: ${failed}`); if (failed === 0 && skillCount === expectedSkills.length) { console.log('\n' + green('✓ Deployment files look correct.')); console.log(gray('If the main agent still does not see wechat-* skills:')); console.log(gray(' 1. Restart the OpenClaw service to force skill re-scan')); console.log(gray(' 2. In OpenClaw UI -> 技能 panel, confirm wechat-* skills are listed')); console.log(gray(' 3. In the agent config, make sure these skills are enabled for the main agent')); } else { console.log('\n' + red('✗ Deployment has issues — fix the FAIL items above and re-run.')); } process.exit(failed > 0 ? 1 : 0); })();