- 添加项目配置文档 (AGENTS.md) - 创建 package.json,包含环信 SDK 及测试依赖 - 添加环信 polyfill 工具模块 - 添加双用户消息互发测试脚本 - 添加 polyfill 测试脚本 Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
220 lines
8.1 KiB
JavaScript
220 lines
8.1 KiB
JavaScript
/**
|
||
* 环信 SDK Node.js Polyfill 测试脚本
|
||
*
|
||
* 用法:
|
||
* node scripts/test-easemob-polyfill.js [方案] [appKey] [user] [pwd] [restServer] [socketServer]
|
||
*
|
||
* 方案:
|
||
* manual - 手动 polyfill (self + ws + localStorage)
|
||
* happydom - 使用 happy-dom 注入完整浏览器环境
|
||
* xhr2 - 手动 polyfill + xhr2 包 (推荐)
|
||
*
|
||
* 私有云配置:
|
||
* restServer - REST API 服务器地址 (如 http://a1.easemob.com)
|
||
* socketServer - WebSocket 服务器地址 (如 ws://im-api-v2.easemob.com/ws)
|
||
*
|
||
* 示例:
|
||
* node scripts/test-easemob-polyfill.js xhr2 "your#appkey" "username" "password"
|
||
* node scripts/test-easemob-polyfill.js xhr2 "your#appkey" "username" "password" "http://a1.easemob.com" "ws://im-api-v2.easemob.com/ws"
|
||
*/
|
||
|
||
const { applyManualPolyfill, applyHappyDomPolyfill, printGlobalState } = require('./easemob-polyfill');
|
||
|
||
const [,, method = 'xhr2', appKey, user, pwd, restServer, socketServer] = process.argv;
|
||
|
||
if (!appKey || !user || !pwd) {
|
||
console.log('用法: node scripts/test-easemob-polyfill.js <manual|xhr2|happydom> <appKey> <user> <pwd> [restServer] [socketServer]');
|
||
console.log('');
|
||
console.log('方案说明:');
|
||
console.log(' manual - 手动 polyfill (无 XMLHttpRequest)');
|
||
console.log(' xhr2 - 手动 polyfill + xhr2 包 (推荐)');
|
||
console.log(' happydom - 使用 happy-dom 注入完整浏览器环境');
|
||
console.log('');
|
||
console.log('私有云参数:');
|
||
console.log(' restServer - REST API 地址 (如 http://a1.easemob.com)');
|
||
console.log(' socketServer - WebSocket 地址 (如 ws://im-api-v2.easemob.com/ws)');
|
||
console.log('');
|
||
console.log('示例:');
|
||
console.log(' # 公有云:');
|
||
console.log(' node scripts/test-easemob-polyfill.js xhr2 "org#app" "user1" "pass1"');
|
||
console.log(' # 私有云:');
|
||
console.log(' node scripts/test-easemob-polyfill.js xhr2 "org#app" "user1" "pass1" "http://a1.easemob.com" "ws://im-api-v2.easemob.com/ws"');
|
||
process.exit(1);
|
||
}
|
||
|
||
const resolvedRest = restServer || 'https://a1.easemob.com';
|
||
const resolvedSocket = socketServer || 'wss://im-api-v2.easemob.com/ws';
|
||
|
||
console.log('=== 环信 SDK Node.js Polyfill 测试 ===');
|
||
console.log(`方案: ${method}`);
|
||
console.log(`AppKey: ${appKey}`);
|
||
console.log(`User: ${user}`);
|
||
console.log(`REST Server: ${resolvedRest}`);
|
||
console.log(`Socket Server: ${resolvedSocket}`);
|
||
console.log('');
|
||
|
||
async function main() {
|
||
// ─── Step 1: 应用 polyfill ───
|
||
console.log('[Step 1] 应用 polyfill...');
|
||
|
||
if (method === 'happydom') {
|
||
await applyHappyDomPolyfill();
|
||
} else {
|
||
applyManualPolyfill();
|
||
// 按 method 选择 XHR polyfill
|
||
if (method === 'xhr2') {
|
||
try {
|
||
const { XMLHttpRequest } = require('xhr2');
|
||
globalThis.XMLHttpRequest = XMLHttpRequest;
|
||
console.log(' + XMLHttpRequest = xhr2');
|
||
} catch {
|
||
console.warn(' ! xhr2 包未安装,跳过');
|
||
}
|
||
} else if (method === 'xhr1') {
|
||
try {
|
||
const { XMLHttpRequest } = require('xmlhttprequest');
|
||
globalThis.XMLHttpRequest = XMLHttpRequest;
|
||
console.log(' + XMLHttpRequest = xmlhttprequest (旧版)');
|
||
} catch {
|
||
console.warn(' ! xmlhttprequest 包未安装,跳过');
|
||
}
|
||
}
|
||
// manual 方案不注入 XMLHttpRequest
|
||
|
||
// Patch XHR.open: SDK 用相对路径,Node.js XHR 需要绝对 URL
|
||
if (globalThis.XMLHttpRequest) {
|
||
const BASE_URL = resolvedRest;
|
||
const origOpen = globalThis.XMLHttpRequest.prototype.open;
|
||
globalThis.XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
|
||
let finalUrl = url;
|
||
if (typeof url === 'string' && url.startsWith('/')) {
|
||
finalUrl = BASE_URL + url;
|
||
console.log(` >>> XHR patch: ${method} ${url} -> ${finalUrl}`);
|
||
}
|
||
return origOpen.call(this, method, finalUrl, ...rest);
|
||
};
|
||
console.log(' + XHR.open patched (相对路径 -> ' + BASE_URL + ')');
|
||
}
|
||
}
|
||
|
||
console.log('[Step 1] Polyfill 完成 ✓');
|
||
printGlobalState();
|
||
|
||
// ─── Step 2: 加载 SDK ───
|
||
console.log('\n[Step 2] 加载 easemob-websdk...');
|
||
let websdk;
|
||
try {
|
||
const rawSdk = require('easemob-websdk');
|
||
websdk = rawSdk.default || rawSdk;
|
||
console.log('[Step 2] SDK 加载成功 ✓');
|
||
console.log(` - SDK keys: ${Object.keys(websdk).slice(0, 10).join(', ')}`);
|
||
console.log(` - Has connection: ${typeof websdk.connection}`);
|
||
console.log(` - Has message: ${typeof websdk.message}`);
|
||
} catch (err) {
|
||
console.error('[Step 2] SDK 加载失败 ✗');
|
||
console.error(` Error: ${err.message}`);
|
||
console.error(` Stack: ${err.stack}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── Step 3: 创建连接实例 ───
|
||
console.log('\n[Step 3] 创建连接实例...');
|
||
let conn;
|
||
try {
|
||
conn = new websdk.connection({
|
||
appKey,
|
||
isHttpDNS: false,
|
||
restUrl: resolvedRest,
|
||
msyncUrl: resolvedSocket,
|
||
});
|
||
console.log('[Step 3] 连接实例创建成功 ✓');
|
||
console.log(` - conn type: ${typeof conn}`);
|
||
console.log(` - Has open: ${typeof conn.open}`);
|
||
console.log(` - Has listen: ${typeof conn.listen}`);
|
||
console.log(` - Has close: ${typeof conn.close}`);
|
||
} catch (err) {
|
||
console.error('[Step 3] 创建连接实例失败 ✗');
|
||
console.error(` Error: ${err.message}`);
|
||
console.error(` Stack: ${err.stack}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── Step 4: 注册事件监听 ───
|
||
console.log('\n[Step 4] 注册事件监听...');
|
||
try {
|
||
conn.listen({
|
||
onOpened: () => {
|
||
console.log('\n★ [Event] 连接已打开 (onOpened) ★');
|
||
printGlobalState();
|
||
console.log('');
|
||
console.log('=== 测试成功!环信 SDK 在 Node.js 下正常工作 ===');
|
||
console.log('5 秒后自动断开...');
|
||
setTimeout(() => {
|
||
try { conn.close(); } catch (e) { /* ignore */ }
|
||
setTimeout(() => process.exit(0), 1000);
|
||
}, 5000);
|
||
},
|
||
onClosed: () => {
|
||
console.log('[Event] 连接已关闭 (onClosed)');
|
||
},
|
||
onError: (error) => {
|
||
const errMsg = error?.message || error?.data?.error_description || JSON.stringify(error);
|
||
console.error(`\n✗ [Event] 错误 (onError): ${errMsg}`);
|
||
// 不退出,有些错误之后还能重连
|
||
},
|
||
onTextMessage: (message) => {
|
||
console.log(`\n[Event] 收到文本消息: ${JSON.stringify(message).substring(0, 200)}`);
|
||
},
|
||
onPictureMessage: (message) => {
|
||
console.log(`\n[Event] 收到图片消息: from=${message.from}`);
|
||
},
|
||
onAudioMessage: (message) => {
|
||
console.log(`\n[Event] 收到语音消息: from=${message.from}`);
|
||
},
|
||
onVideoMessage: (message) => {
|
||
console.log(`\n[Event] 收到视频消息: from=${message.from}`);
|
||
},
|
||
onFileMessage: (message) => {
|
||
console.log(`\n[Event] 收到文件消息: from=${message.from}`);
|
||
},
|
||
onPresence: (message) => {
|
||
console.log(`[Event] Presence: ${message?.type}`);
|
||
},
|
||
});
|
||
console.log('[Step 4] 事件监听注册成功 ✓');
|
||
} catch (err) {
|
||
console.error('[Step 4] 注册事件监听失败 ✗');
|
||
console.error(` Error: ${err.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── Step 5: 登录 ───
|
||
console.log('\n[Step 5] 开始登录...');
|
||
console.log(` user: ${user}`);
|
||
try {
|
||
await conn.open({ user, pwd });
|
||
console.log('[Step 5] open() 已调用,等待 onOpened 回调...');
|
||
} catch (err) {
|
||
console.error('[Step 5] 登录失败 ✗');
|
||
console.error(` Error: ${err.message}`);
|
||
console.error(` Stack: ${err.stack}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 超时保护:30 秒无 onOpened 则退出
|
||
setTimeout(() => {
|
||
console.error('\n✗ 超时:30 秒内未收到 onOpened 回调');
|
||
console.error(' 可能原因:');
|
||
console.error(' 1. WebSocket 连接失败(检查网络/防火墙)');
|
||
console.error(' 2. Polyfill 不完整(缺少必要的浏览器 API)');
|
||
console.error(' 3. 账号密码错误');
|
||
process.exit(1);
|
||
}, 30000);
|
||
}
|
||
|
||
// ─── 启动 ───
|
||
main().catch((err) => {
|
||
console.error('Fatal error:', err);
|
||
process.exit(1);
|
||
});
|