#!/usr/bin/env node /** * 微信插件完整测试脚本 * * 功能: * 1. 二维码登录 * 2. 启动消息监听 * 3. 自动回复测试 * 4. 消息收发验证 * * 使用方法: * node scripts/test-weixin-plugin.js * 或 * npm run test:full */ import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; // 获取项目根目录 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); // 导入微信插件模块 const weixinPluginPath = path.join(projectRoot, 'weixin-plugin'); console.log('='.repeat(60)); console.log('微信插件完整测试脚本'); console.log('='.repeat(60)); console.log(`项目根目录: ${projectRoot}`); console.log(`插件路径: ${weixinPluginPath}`); console.log(''); // ============================================================================= // 颜色输出工具 // ============================================================================= const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', }; function log(color, ...args) { console.log(color + args.join(' ') + colors.reset); } function logInfo(...args) { log(colors.cyan, '[INFO]', ...args); } function logSuccess(...args) { log(colors.green, '[OK]', ...args); } function logError(...args) { log(colors.red, '[ERROR]', ...args); } function logWarn(...args) { log(colors.yellow, '[WARN]', ...args); } // ============================================================================= // 测试配置 // ============================================================================= const TEST_CONFIG = { // 测试账号ID(留空使用默认) accountId: process.env.WEIXIN_ACCOUNT_ID || '', // 登录超时时间(毫秒) loginTimeout: 5 * 60 * 1000, // 消息监听超时(毫秒,0 表示不限制) listenTimeout: 0, // 是否启用自动回复 autoReply: true, // 自动回复前缀 autoReplyPrefix: '[自动回复] ', // 调试模式 debug: true, }; // ============================================================================= // 导入微信插件模块 // ============================================================================= function resolveModulePath() { const distPath = path.join(weixinPluginPath, 'dist', 'weixinGateway.js'); if (fs.existsSync(distPath)) { return distPath; } const srcPath = path.join(weixinPluginPath, 'weixinGateway.ts'); if (fs.existsSync(srcPath)) { return srcPath; } throw new Error('找不到 weixinGateway 模块文件'); } let WeixinGateway, startWeixinLogin, waitWeixinLogin, resolveWeixinAccount, loadWeixinAccount; try { const modulePath = resolveModulePath(); const weixinGatewayModule = await import(modulePath); WeixinGateway = weixinGatewayModule.WeixinGateway; startWeixinLogin = weixinGatewayModule.startWeixinLogin; waitWeixinLogin = weixinGatewayModule.waitWeixinLogin; // 尝试从 channel 导入账号管理函数 try { const channelModule = await import(path.join(weixinPluginPath, 'dist', 'weixin', 'channel.js')); resolveWeixinAccount = channelModule.resolveWeixinAccount; loadWeixinAccount = channelModule.loadWeixinAccount; } catch (err) { logWarn('无法导入账号管理函数,将跳过已有登录检查'); } logInfo('成功加载微信插件模块'); } catch (err) { logError('加载微信插件模块失败:', err.message); logInfo('请确保已编译插件: npm run build'); process.exit(1); } // ============================================================================= // 工具函数 // ============================================================================= /** * 保存二维码到文件 */ async function saveQRCode(qrcodeDataUrl, filename = 'weixin-qrcode.png') { const matches = qrcodeDataUrl.match(/^data:image\/png;base64,(.+)$/); if (!matches) { throw new Error('无效的二维码数据'); } const buffer = Buffer.from(matches[1], 'base64'); const filepath = path.join(projectRoot, filename); fs.writeFileSync(filepath, buffer); logSuccess(`二维码已保存到: ${filepath}`); return filepath; } /** * 格式化时间戳 */ function formatTime(timestamp) { return new Date(timestamp).toLocaleString('zh-CN'); } /** * 格式化持续时间 */ function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}小时${minutes % 60}分钟`; } else if (minutes > 0) { return `${minutes}分钟${seconds % 60}秒`; } else { return `${seconds}秒`; } } // ============================================================================= // 测试流程 // ============================================================================= /** * 步骤 1: 检查登录状态或二维码登录 */ async function testLogin() { logInfo('检查登录状态...'); console.log(''); let targetAccountId = TEST_CONFIG.accountId || 'test-account'; // 1. 首先检查是否有已保存的登录信息 if (resolveWeixinAccount && loadWeixinAccount) { try { // 如果没有指定账号ID,尝试查找已保存的账号 if (!TEST_CONFIG.accountId) { const fs = await import('fs'); const path = await import('path'); const os = await import('os'); const stateDir = process.env.OPENCLAW_STATE_DIR || process.env.CLAWDBOT_STATE_DIR || path.join(os.homedir(), '.openclaw'); const accountsDir = path.join(stateDir, 'openclaw-weixin', 'accounts'); const registryPath = path.join(accountsDir, 'registered-accounts.json'); if (fs.existsSync(registryPath)) { const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); if (registry.accounts && registry.accounts.length > 0) { targetAccountId = registry.accounts[0]; logInfo(`找到已保存的账号: ${targetAccountId}`); } } } // 检查指定账号的登录信息 const resolved = resolveWeixinAccount({}, targetAccountId); const accountData = loadWeixinAccount(targetAccountId); // 检查是否有有效的登录信息(更宽松的检查) const hasValidLogin = accountData?.token || (resolved.configured && resolved.token); if (hasValidLogin) { const token = accountData?.token || resolved.token; const userId = accountData?.userId || resolved.userId; const baseUrl = accountData?.baseUrl || resolved.baseUrl; logSuccess('发现已保存的登录信息!'); console.log(''); logInfo('账号信息:'); logInfo(` - Account ID: ${targetAccountId}`); logInfo(` - User ID: ${userId || 'N/A'}`); logInfo(` - Base URL: ${baseUrl || 'N/A'}`); logInfo(` - Token: ${token.substring(0, 30)}...`); console.log(''); logInfo('跳过二维码登录,直接使用已有登录信息'); console.log(''); return { connected: true, accountId: targetAccountId, userId: userId, baseUrl: baseUrl || 'https://ilinkai.weixin.qq.com', botToken: token, }; } } catch (err) { logWarn('检查已有登录信息失败:', err.message); logInfo('将继续进行二维码登录'); console.log(''); } } else { logWarn('账号管理函数不可用,跳过已有登录检查'); } // 2. 没有找到有效的登录信息,进行二维码登录 logInfo('未找到有效的登录信息,开始二维码登录流程...'); console.log(''); try { // 1. 获取二维码 logInfo('正在获取登录二维码...'); const { sessionKey, qrcodeUrl } = await startWeixinLogin(targetAccountId); if (!qrcodeUrl) { throw new Error('获取二维码失败'); } // 2. 保存二维码 await saveQRCode(qrcodeUrl); logSuccess('二维码获取成功!'); console.log(''); // 3. 显示二维码 logInfo('请使用微信扫描以下二维码登录:'); console.log(''); console.log(qrcodeUrl); console.log(''); logInfo('等待扫码登录...'); // 4. 等待登录 const loginResult = await waitWeixinLogin(sessionKey, TEST_CONFIG.loginTimeout); if (!loginResult.connected) { throw new Error(loginResult.error || '登录失败'); } logSuccess('登录成功!'); console.log(''); logInfo('账号信息:'); logInfo(` - Account ID: ${loginResult.accountId}`); logInfo(` - User ID: ${loginResult.userId}`); logInfo(` - Base URL: ${loginResult.baseUrl}`); console.log(''); return loginResult; } catch (err) { logError('登录失败:', err.message); throw err; } } /** * 步骤 2: 启动网关 */ async function testGateway() { logInfo('初始化微信网关...'); console.log(''); const gateway = new WeixinGateway(); // 设置消息回调 let messageCount = 0; let firstMessageTime = null; gateway.setMessageCallback(async (message, replyFn) => { messageCount++; if (!firstMessageTime) { firstMessageTime = Date.now(); } logSuccess('收到消息!'); console.log(''); logInfo('消息详情:'); logInfo(` - 平台: ${message.platform}`); logInfo(` - 发送者: ${message.senderId}`); logInfo(` - 会话: ${message.conversationId}`); logInfo(` - 类型: ${message.chatType}`); logInfo(` - 内容: ${message.content}`); logInfo(` - 时间: ${formatTime(message.timestamp)}`); console.log(''); // 自动回复 if (TEST_CONFIG.autoReply) { const replyText = `${TEST_CONFIG.autoReplyPrefix}已收到你的消息:"${message.content}"(第${messageCount}条)`; logInfo(`发送自动回复: ${replyText}`); try { await replyFn(replyText); logSuccess('自动回复发送成功'); } catch (err) { logError('自动回复发送失败:', err.message); } console.log(''); } }); // 监听连接事件 gateway.once('connected', () => { logSuccess('网关已连接!'); const status = gateway.getStatus(); logInfo(`账号: ${status.accountId}`); logInfo(`启动时间: ${formatTime(status.startedAt)}`); console.log(''); }); gateway.on('error', (error) => { logError('网关错误:', error.message); }); gateway.on('message', (message) => { // 消息已在回调中处理 }); // 启动网关 try { // 先设置连接监听器,再启动网关 await new Promise((resolve, reject) => { const timeout = setTimeout(() => { try { gateway.off('connected'); gateway.off('error'); } catch (e) { // 忽略移除监听器的错误 } reject(new Error('连接超时')); }, 10000); gateway.once('connected', () => { clearTimeout(timeout); resolve(); }); gateway.once('error', (err) => { clearTimeout(timeout); try { gateway.off('connected'); } catch (e) { // 忽略移除监听器的错误 } reject(err); }); // 启动网关 gateway.start({ enabled: true, debug: TEST_CONFIG.debug, }).catch(reject); }); return gateway; } catch (err) { logError('启动网关失败:', err.message); throw err; } } /** * 步骤 3: 消息监听测试 */ async function testMessageListening(gateway) { logInfo('开始监听消息...'); logInfo('你可以向微信机器人发送消息进行测试'); logInfo('按 Ctrl+C 退出测试'); console.log(''); logInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); const startTime = Date.now(); let lastMessageCount = 0; // 定时显示统计信息 const statsInterval = setInterval(() => { const status = gateway.getStatus(); const elapsed = Date.now() - startTime; console.log(''); logInfo('━━━━ 运行状态 ━━━━'); logInfo(`运行时长: ${formatDuration(elapsed)}`); logInfo(`连接状态: ${status.connected ? '已连接' : '未连接'}`); logInfo(`入站消息: ${status.lastInboundAt ? `最后于 ${formatTime(status.lastInboundAt)}` : '暂无'}`); logInfo(`出站消息: ${status.lastOutboundAt ? `最后于 ${formatTime(status.lastOutboundAt)}` : '暂无'}`); logInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); }, 30000); // 等待用户中断 return new Promise((resolve) => { process.on('SIGINT', () => { console.log(''); logInfo('收到退出信号,正在停止...'); clearInterval(statsInterval); const elapsed = Date.now() - startTime; logInfo('━━━━ 测试统计 ━━━━'); logInfo(`运行时长: ${formatDuration(elapsed)}`); logInfo(`接收消息数: ${lastMessageCount} 条`); logInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); resolve(); }); // 监听消息事件来更新计数 gateway.on('message', () => { lastMessageCount++; }); }); } /** * 主测试流程 */ async function main() { const startTime = Date.now(); try { // 步骤 1: 登录 const loginResult = await testLogin(); // 步骤 2: 启动网关 const gateway = await testGateway(); // 步骤 3: 监听消息 await testMessageListening(gateway); // 停止网关 logInfo('正在停止网关...'); await gateway.stop(); logSuccess('网关已停止'); // 完成 const elapsed = Date.now() - startTime; console.log(''); logSuccess('测试完成!总耗时:', formatDuration(elapsed)); } catch (err) { console.log(''); logError('测试失败:', err.message); if (TEST_CONFIG.debug) { console.log(''); console.error(err); } process.exit(1); } } // ============================================================================= // 运行测试 // ============================================================================= console.log('测试配置:'); console.log(` - 账号ID: ${TEST_CONFIG.accountId || 'test-account (默认)'}`); console.log(` - 登录超时: ${formatDuration(TEST_CONFIG.loginTimeout)}`); console.log(` - 自动回复: ${TEST_CONFIG.autoReply ? '启用' : '禁用'}`); console.log(` - 调试模式: ${TEST_CONFIG.debug ? '启用' : '禁用'}`); console.log(''); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); main().catch((err) => { logError('未处理的错误:', err); process.exit(1); });