Files
weixin-plugin-test/scripts/test-weixin-plugin.js
zyh 49ae0bd1e7 feat: 优化测试脚本,支持使用已有登录信息
- 添加检查已有登录信息的逻辑
- 如果发现已保存的账号,直接使用,跳过二维码登录
- 自动查找已保存的账号,无需手动指定账号ID
- 修复网关连接超时问题
- 改进事件监听器设置顺序

测试体验优化:
- 无需每次都重新扫码登录
- 自动复用已保存的 token
- 提高测试效率

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-30 08:33:47 +00:00

517 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});