ai_scheduler/web/assets/js/wx_chat.js

526 lines
24 KiB
JavaScript
Raw 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.

/* ============================================================
* 微信工作台 — 聊天 / 消息 / 会话模块
* 依赖:core.js(window.Core)、wx.js(window.WxN)
* ============================================================ */
(function (global) {
"use strict";
var Core = global.Core;
var WxN = global.WxN;
var $ = function (sel, root) { return (root || document).querySelector(sel); };
function state() { return WxN.state; }
/* ==================== brief 缓存辅助(不使用 localStorage) ==================== */
function loadBriefCache() { return {}; }
function saveBriefCache() {}
// 相对时间显示
function timeAgo(ts) {
var now = Date.now();
var diff = Math.floor((now - ts) / 1000);
if (diff < 60) return "刚刚";
if (diff < 3600) return Math.floor(diff / 60) + "分钟前";
if (diff < 86400) return Math.floor(diff / 3600) + "小时前";
if (diff < 604800) return Math.floor(diff / 86400) + "天前";
var d = new Date(ts);
return (d.getMonth() + 1) + "/" + d.getDate();
}
// 格式化消息时间戳
function fmtMsgTime(ts) {
if (!ts) return "";
var d = typeof ts === "number" && ts < 1e12 ? new Date(ts * 1000) : new Date(ts);
if (isNaN(d.getTime())) return "";
var h = d.getHours(), m = d.getMinutes();
var pad = function (n) { return n < 10 ? "0" + n : "" + n; };
var now = new Date();
if (d.toDateString() === now.toDateString()) return pad(h) + ":" + pad(m);
return (d.getMonth() + 1) + "/" + d.getDate() + " " + pad(h) + ":" + pad(m);
}
/* ==================== 会话列表(从 MongoDB 聚合) ==================== */
function loadConversations() {
var s = state();
if (!s.app || !s.app.appId) return;
var params = { page_size: 50 };
if (s.app.wxid) params.selfWxid = s.app.wxid;
Core.api("wx/conversation/list", params)
.then(function (list) {
s.conversations = list || [];
if (s.tab === "conversations") WxN.renderContacts();
fetchMissingChatroomBriefs();
// 初始化时自动加载第一条会话的聊天记录
if (!s.active && s.conversations.length > 0) {
console.log("[loadConversations] auto-opening first conversation: " + s.conversations[0].wxid);
openConversationChat(s.conversations[0].wxid);
}
})
.catch(function () {});
}
function fetchMissingChatroomBriefs() {
var s = state();
var missingChatrooms = [];
s.conversations.forEach(function (c) {
if (c.isGroup && !s.brief[c.wxid]) missingChatrooms.push(c.wxid);
});
if (!missingChatrooms.length) { fixMissingChatroomAvatars(); return; }
var batches = [];
for (var i = 0; i < missingChatrooms.length; i += 20) batches.push(missingChatrooms.slice(i, i + 20));
batches.forEach(function (batch) {
Core.wxApi("contacts/getBriefInfo", { appId: s.app.appId, wxids: batch })
.then(function (list) {
(list || []).forEach(function (info) { if (info && info.userName) s.brief[info.userName] = info; });
saveBriefCache();
if (s.tab === "conversations") WxN.renderContacts();
fixMissingChatroomAvatars();
})
.catch(function () {});
});
}
function fixMissingChatroomAvatars() {
var s = state();
var missingIds = [];
s.conversations.forEach(function (c) {
if (!c.isGroup) return;
var info = s.brief[c.wxid] || {};
if (info.bigHeadImgUrl || info.smallHeadImgUrl) return;
missingIds.push(c.wxid);
});
if (!missingIds.length) return;
// 一次批量调用后端,后端内部循环调 wx API 并返回所有群信息
Core.wxApi("batch_chatroom_info", { appId: s.app.appId, chatroomIds: missingIds })
.then(function (list) {
(list || []).forEach(function (data) {
if (!data || !data.chatroomId) return;
var existing = s.brief[data.chatroomId] || {};
existing.smallHeadImgUrl = existing.smallHeadImgUrl || data.smallHeadImgUrl || "";
existing.nickName = existing.nickName || data.nickName || "";
if (data.remark) existing.remark = existing.remark || data.remark;
s.brief[data.chatroomId] = existing;
});
saveBriefCache();
if (s.active && s.brief[s.active.wxid]) {
var ai = s.brief[s.active.wxid];
s.active.avatar = ai.bigHeadImgUrl || ai.smallHeadImgUrl || s.active.avatar;
s.active.name = ai.remark || ai.nickName || s.active.name;
WxN.renderChatHead();
}
if (s.tab === "conversations") WxN.renderContacts();
})
.catch(function () {});
}
function openConversationChat(wxid) {
var s = state();
var d = conversationDisplay(wxid);
var isRoom = wxid.indexOf("@chatroom") >= 0;
s.active = { wxid: wxid, name: d.name, avatar: d.avatar, type: isRoom ? "chatroom" : "friend", fromConv: true };
WxN.renderContacts();
var box = $("#wxMsgs", s.container);
box.innerHTML = '<div class="empty" style="margin:auto"><span class="spin"></span><div class="mt-1 fs-12">加载聊天记录…</div></div>';
var msgParams = { wxid: wxid, page_size: 200 };
if (s.app.wxid) msgParams.selfWxid = s.app.wxid;
Core.api("wx/conversation/msgs", msgParams)
.then(function (list) {
s.convMsgs[wxid] = list || [];
WxN.renderChat();
markConvAsRead(wxid);
if (isRoom) fetchGroupMemberList(wxid);
})
.catch(function () { s.convMsgs[wxid] = []; WxN.renderChat(); });
$("#wxText", s.container).focus();
}
function fetchGroupMemberList(chatroomId) {
var s = state();
if (!s.app || !s.app.appId) return;
Core.wxApi("group/getChatroomMemberList", { appId: s.app.appId, chatroomId: chatroomId })
.then(function (data) {
var members = (data && data.memberList) || [];
var map = {};
members.forEach(function (m) {
if (m && m.wxid) {
map[m.wxid] = {
nickName: m.nickName || m.displayName || m.wxid,
bigHeadImgUrl: m.bigHeadImgUrl || "",
smallHeadImgUrl: m.smallHeadImgUrl || ""
};
}
});
s.groupMembers[chatroomId] = map;
if (s.active && s.active.wxid === chatroomId && s.active.fromConv) WxN.renderChat();
})
.catch(function () {});
}
function markConvAsRead(wxid) {
var s = state();
if (!s.app || !s.app.appId) return;
var params = { wxid: wxid };
if (s.app.wxid) params.selfWxid = s.app.wxid;
Core.api("wx/conversation/read", params)
.then(function () {
s.conversations.forEach(function (c) { if (c.wxid === wxid) c.unreadCount = 0; });
if (s.tab === "conversations") WxN.renderContacts();
})
.catch(function () {});
}
function conversationDisplay(wxid) {
var s = state();
var info = s.brief[wxid] || {};
return { name: info.remark || info.nickName || wxid, avatar: info.bigHeadImgUrl || info.smallHeadImgUrl || "" };
}
/* ==================== 联系人详情悬浮气泡 ==================== */
var _detailCache = {};
var _detailTimer = null;
var _detailPopup = null;
function showContactDetail(wxid, el, e) {
clearTimeout(_detailTimer);
hideContactDetail();
_detailTimer = setTimeout(function () {
var rect = el.getBoundingClientRect();
var popup = document.createElement('div');
popup.className = 'wx-detail-popup';
popup.innerHTML = '<div class="wx-detail-loading"><span class="spin"></span> 加载中…</div>';
popup.style.position = 'fixed';
popup.style.zIndex = '9999';
// 视口边界检测:默认显示在右侧,空间不足则显示在左侧
var popupWidth = 280;
var leftPos = rect.right + 8;
if (leftPos + popupWidth > window.innerWidth - 10) {
leftPos = rect.left - popupWidth - 8;
}
if (leftPos < 10) leftPos = 10;
var topPos = rect.top;
if (topPos + 300 > window.innerHeight) {
topPos = Math.max(10, window.innerHeight - 310);
}
popup.style.left = leftPos + 'px';
popup.style.top = topPos + 'px';
document.body.appendChild(popup);
_detailPopup = popup;
if (_detailCache[wxid]) { renderDetailPopup(_detailCache[wxid]); return; }
var s = state();
Core.wxApi("contacts/getDetailInfo", { appId: s.app.appId, wxids: [wxid] })
.then(function (list) {
if (!list || !list.length) { renderDetailPopup(null); return; }
_detailCache[wxid] = list[0];
renderDetailPopup(list[0]);
})
.catch(function () { renderDetailPopup(null); });
}, 400);
}
function renderDetailPopup(info) {
if (!_detailPopup) return;
if (!info) { _detailPopup.innerHTML = '<div class="wx-detail-empty">暂无详情</div>'; return; }
var sex = info.sex === 1 ? '♂ 男' : info.sex === 2 ? '♀ 女' : '未知';
var sexClass = info.sex === 1 ? 'male' : info.sex === 2 ? 'female' : '';
var html = '<div class="wx-detail-card">';
html += '<div class="wx-detail-head">';
if (info.bigHeadImgUrl || info.smallHeadImgUrl) {
html += '<img class="wx-detail-avatar" src="' + Core.esc(info.bigHeadImgUrl || info.smallHeadImgUrl) + '" alt="" onerror="this.style.display=\'none\'">';
} else {
html += '<div class="wx-detail-avatar wx-detail-avatar-text">' + Core.esc(Core.initial(info.nickName || info.userName)) + '</div>';
}
html += '<div class="wx-detail-info">';
html += '<div class="wx-detail-name">' + Core.esc(info.nickName || info.userName) + '</div>';
html += '<div class="wx-detail-wxid">' + Core.esc(info.userName) + '</div>';
html += '</div>';
html += '<span class="wx-detail-sex ' + sexClass + '">' + sex + '</span>';
html += '</div>';
html += '<div class="wx-detail-body">';
if (info.remark) html += '<div class="wx-detail-row"><span class="wx-detail-label">备注</span><span class="wx-detail-value">' + Core.esc(info.remark) + '</span></div>';
if (info.alias) html += '<div class="wx-detail-row"><span class="wx-detail-label">微信号</span><span class="wx-detail-value">' + Core.esc(info.alias) + '</span></div>';
if (info.signature) html += '<div class="wx-detail-row"><span class="wx-detail-label">签名</span><span class="wx-detail-value wx-detail-signature">' + Core.esc(info.signature) + '</span></div>';
if (info.province || info.city) html += '<div class="wx-detail-row"><span class="wx-detail-label">地区</span><span class="wx-detail-value">' + Core.esc((info.province || '') + ' ' + (info.city || '')) + '</span></div>';
if (info.phoneNumList) html += '<div class="wx-detail-row"><span class="wx-detail-label">手机</span><span class="wx-detail-value">' + Core.esc(info.phoneNumList) + '</span></div>';
if (info.description) html += '<div class="wx-detail-row"><span class="wx-detail-label">描述</span><span class="wx-detail-value wx-detail-signature">' + Core.esc(info.description) + '</span></div>';
html += '</div></div>';
_detailPopup.innerHTML = html;
}
function hideContactDetail() {
clearTimeout(_detailTimer);
if (_detailPopup && _detailPopup.parentNode) _detailPopup.parentNode.removeChild(_detailPopup);
_detailPopup = null;
}
function clearDetailCache() { _detailCache = {}; }
/* ==================== 聊天 ==================== */
function openChat(wxid) {
var s = state();
var d = WxN.contactDisplay(wxid);
var isRoom = s.contacts.chatrooms.indexOf(wxid) >= 0;
s.active = { wxid: wxid, name: d.name, avatar: d.avatar, type: isRoom ? "chatroom" : "friend" };
if (!s.msgs[wxid]) s.msgs[wxid] = [];
WxN.renderContacts();
WxN.renderChat();
$("#wxText", s.container).focus();
}
function renderChatHead() {
var s = state();
var box = $("#wxChatHead", s.container);
var a = s.active;
if (!a) {
box.innerHTML = '<div class="wx-title c-dim">未选择会话</div><span class="spacer"></span>' +
'<span class="badge ' + (s.ai.sessionId ? "green" : "gray") + '">' + (s.ai.sessionId ? "AI 会话就绪" : "AI 未注册") + "</span>" +
'<button class="btn ghost sm" id="wxAiConfig">⚙ AI 助手设置</button>';
} else {
box.innerHTML =
(a.avatar ? '<img class="wx-avatar" src="' + Core.esc(a.avatar) + '" alt="">' : '<div class="wx-avatar">' + Core.esc(Core.initial(a.name)) + "</div>") +
"<div><div class=\"wx-title\">" + Core.esc(a.name) + "</div>" +
'<div class="wx-sub mono">' + Core.esc(a.wxid) + (a.type === "chatroom" ? " · 群聊" : "") + "</div></div>" +
'<span class="spacer"></span>' +
'<span class="badge ' + (s.ai.sessionId ? "green" : "gray") + '">' + (s.ai.sessionId ? "AI 会话就绪" : "AI 未注册") + "</span>" +
'<button class="btn ghost sm" id="wxAiConfig">⚙ AI 助手设置</button>';
}
$("#wxAiConfig", box).onclick = WxN.openAiConfig;
}
function renderChat() {
renderChatHead();
var s = state();
var box = $("#wxMsgs", s.container);
var a = s.active;
if (!a) {
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">◈</div>选择左侧联系人开始对话<br><span class="fs-12">发送内容将通过微信协议接口投递</span></div>';
return;
}
if (a.fromConv) {
var convMsgList = s.convMsgs[a.wxid] || [];
if (!convMsgList.length) {
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">✦</div>与「' + Core.esc(a.name) + '」暂无聊天记录<br><span class="fs-12">收到对方消息后将自动显示在这里</span></div>';
return;
}
box.innerHTML = renderConvMsgs(convMsgList);
box.scrollTop = box.scrollHeight;
return;
}
var msgs = s.msgs[a.wxid] || [];
if (!msgs.length) {
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">✦</div>与「' + Core.esc(a.name) + '」的会话已就绪<br><span class="fs-12">输入内容点击发送;也可粘贴客户消息后点击「AI 建议」获取话术</span></div>';
return;
}
box.innerHTML = msgs.map(function (m) {
var time = Core.esc(m.time || "");
if (m.role === "system") return '<div class="text-center fs-12 c-dim" style="width:100%">' + time + " · " + Core.esc(m.text) + "</div>";
if (m.role === "ai") return '<div class="msg-row"><div class="msg-avatar">✦</div><div><div class="msg-bubble" style="border-color:rgba(45,224,255,.4)">' + Core.esc(m.text) + '</div><div class="msg-time">' + time + " · AI 建议</div></div></div>";
return '<div class="msg-row self"><div class="msg-avatar">' + Core.esc(Core.initial((s.app && s.app.nickName) || "我")) + '</div><div><div class="msg-bubble' + (m.fail ? " fail" : "") + '">' + Core.esc(m.text) + '</div><div class="msg-time">' + time + (m.fail ? " · 发送失败" : "") + "</div></div></div>";
}).join("");
box.scrollTop = box.scrollHeight;
}
function renderConvMsgs(msgList) {
var s = state();
var selfName = (s.app && s.app.nickName) || "我";
var isGroupChat = s.active && s.active.type === "chatroom";
var members = (s.active && s.groupMembers && s.groupMembers[s.active.wxid]) || {};
var html = '';
var lastDate = '';
for (var i = 0; i < msgList.length; i++) {
var m = msgList[i];
var timeStr = fmtMsgTime(m.createAt);
var d = new Date(typeof m.createAt === "number" && m.createAt < 1e12 ? m.createAt * 1000 : m.createAt);
var dateKey = !isNaN(d.getTime()) ? d.toDateString() : "";
if (dateKey && dateKey !== lastDate) {
lastDate = dateKey;
html += '<div class="conv-date"><span>' + Core.esc(fmtMsgDate(d)) + '</span></div>';
}
if (m.msgType === "system" || m.msgType === "group_sys") {
html += '<div class="conv-sys">' + Core.esc(m.content || "系统消息") + '</div>';
continue;
}
var isSelf = m.direction === "self" || m.direction === "ai";
var senderName = '', senderAvatar = '';
if (isGroupChat && !isSelf) {
var senderWxid = m.senderWxid || '';
var memberInfo = members[senderWxid] || {};
senderName = memberInfo.nickName || senderWxid || '';
senderAvatar = memberInfo.bigHeadImgUrl || memberInfo.smallHeadImgUrl || '';
}
var avatarHtml;
if (m.direction === "ai") {
avatarHtml = '<div class="msg-avatar" style="background:rgba(45,224,255,0.15)">✦</div>';
} else if (isSelf) {
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial(selfName)) + '</div>';
} else if (isGroupChat && senderAvatar) {
avatarHtml = '<img class="msg-avatar" src="' + Core.esc(senderAvatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'msg-avatar\',textContent:\'' + Core.esc(Core.initial(senderName || '?')) + '\'}))">';
} else if (isGroupChat) {
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial(senderName || '?')) + '</div>';
} else {
var convAvatar = (s.active && s.active.avatar) || '';
if (convAvatar) {
avatarHtml = '<img class="msg-avatar" src="' + Core.esc(convAvatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'msg-avatar\',textContent:\'' + Core.esc(Core.initial((s.active && s.active.name) || '?')) + '\'}))">';
} else {
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial((s.active && s.active.name) || '?')) + '</div>';
}
}
var bubbleClass = isSelf ? 'msg-bubble conv-msg-self' : 'msg-bubble conv-msg-other';
var contentHtml = renderMsgContent(m.msgType, m.content, isSelf);
var senderNameHtml = (isGroupChat && !isSelf && senderName) ? '<div class="msg-sender-name">' + Core.esc(senderName) + '</div>' : '';
html += '<div class="msg-row' + (isSelf ? ' self' : '') + '">' + avatarHtml +
'<div>' + senderNameHtml +
'<div class="' + bubbleClass + '">' + contentHtml + '</div>' +
'<div class="msg-time">' + Core.esc(timeStr) + (m.direction === "ai" ? " · AI" : "") + "</div></div></div>";
}
return html;
}
function renderMsgContent(msgType, content, isSelf) {
if (!content) content = "";
switch (msgType) {
case "text": return Core.esc(content);
case "image":
var imgUrl = extractImgUrl(content);
if (imgUrl) return '<img class="conv-img" src="' + Core.esc(imgUrl) + '" alt="[图片]" onerror="this.outerHTML=\'<span>[图片]</span>\'">';
return '<span class="conv-media">[图片]</span>';
case "emoji": return '<span class="conv-media">😊 [表情]</span>';
case "voice": return '<span class="conv-media">🎤 [语音]</span>';
case "video": return '<span class="conv-media">🎬 [视频]</span>';
case "location": return '<span class="conv-media">📍 [位置]</span>';
case "namecard": return '<span class="conv-media">👤 [名片]</span>';
case "app_msg":
var title = extractAppMsgTitle(content);
return '<span class="conv-media">🔗 ' + Core.esc(title || "[链接/小程序]") + '</span>';
case "friend_req": return '<span class="conv-media">👥 [好友请求]</span>';
default: return '<span class="conv-media">[' + Core.esc(msgType || "消息") + ']</span>';
}
}
function extractImgUrl(xml) {
if (!xml) return "";
var m = xml.match(/cdnurl\s*=\s*"([^"]+)"/);
if (m && m[1]) return m[1];
m = xml.match(/aesurl\s*=\s*"([^"]+)"/);
if (m && m[1]) return m[1];
m = xml.match(/<url>\s*<!\[CDATA\[([^\]]+)\]/);
if (m && m[1]) return m[1];
if (xml.indexOf("http") === 0) return xml.trim();
return "";
}
function extractAppMsgTitle(xml) {
if (!xml) return "";
var m = xml.match(/<title>\s*<!\[CDATA\[([^\]]*)\]/);
if (m) return m[1];
m = xml.match(/<title>([^<]+)</);
if (m) return m[1];
return "";
}
function fmtMsgDate(d) {
var now = new Date();
if (d.toDateString() === now.toDateString()) return "今天";
var yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
if (d.toDateString() === yesterday.toDateString()) return "昨天";
return (d.getMonth() + 1) + "月" + d.getDate() + "日";
}
/* ==================== 发送消息 ==================== */
function doSend() {
var s = state();
if (!s.active) { Core.toast("请先选择联系人", "err"); return; }
var text = $("#wxText", s.container).value.trim();
if (!text) { Core.toast("请输入消息内容", "err"); return; }
$("#wxText", s.container).value = "";
sendBubbles(s.active.wxid, text);
}
function sendText(wxid, text, opts) {
opts = opts || {};
var s = state();
var hint = $("#wxSendHint", s.container);
if (hint) hint.textContent = "发送中…";
return Core.wxApi("message/postText", { appId: s.app.appId, toWxid: wxid, content: text, ats: "" })
.then(function () {
if (s.msgs[wxid] === undefined) s.msgs[wxid] = [];
s.msgs[wxid].push({ role: "self", text: text, time: Core.nowTime() });
if (s.convMsgs[wxid]) {
s.convMsgs[wxid].push({ direction: "self", msgType: "text", content: text, createAt: Math.floor(Date.now() / 1000) });
}
if (hint) hint.textContent = "已发送";
renderChat();
loadConversations();
if (!opts.quiet) Core.toast("已发送", "ok");
})
.catch(function (e) {
if (s.msgs[wxid] === undefined) s.msgs[wxid] = [];
s.msgs[wxid].push({ role: "self", text: text, time: Core.nowTime(), fail: true });
if (hint) hint.textContent = "发送失败";
renderChat();
Core.toast("发送失败:" + e.message, "err");
});
}
function sendBubbles(wxid, text) {
var bubbles = Core.splitBubbles(text);
if (!bubbles.length) return Promise.resolve();
var hint = $("#wxSendHint", state().container);
var i = 0;
function next() {
if (i >= bubbles.length) {
if (hint) hint.textContent = bubbles.length > 1 ? "已发送 " + bubbles.length + " 条" : "已发送";
return Promise.resolve();
}
var b = bubbles[i++];
if (hint) hint.textContent = "发送中…" + (bubbles.length > 1 ? "(" + i + "/" + bubbles.length + ")" : "");
return sendText(wxid, b, { quiet: true }).then(function () {
if (i >= bubbles.length) return;
return Core.sleep(900 + Math.random() * 1600).then(next);
});
}
return next().then(function () {
Core.toast(bubbles.length > 1 ? "已发送(" + bubbles.length + " 条消息)" : "已发送", "ok");
});
}
/* ==================== WebSocket 新消息处理 ==================== */
function handleNewMessage(data) {
if (!data || !data.wxid) return;
var s = state();
var wxid = data.wxid;
if (!s.convMsgs[wxid]) s.convMsgs[wxid] = [];
s.convMsgs[wxid].push({
direction: data.direction || "customer", msgType: data.msgType || "text",
content: data.content || "", senderWxid: data.senderWxid || "",
createAt: data.createAt || Math.floor(Date.now() / 1000)
});
if (s.active && s.active.wxid === wxid && s.active.fromConv) {
renderChat();
markConvAsRead(wxid);
} else if (data.direction === "customer") {
s.conversations.forEach(function (c) { if (c.wxid === wxid) c.unreadCount = (c.unreadCount || 0) + 1; });
}
loadConversations();
}
/* ==================== 导出 ==================== */
WxN.timeAgo = timeAgo;
WxN.fmtMsgTime = fmtMsgTime;
WxN.loadConversations = loadConversations;
WxN.openConversationChat = openConversationChat;
WxN.markConvAsRead = markConvAsRead;
WxN.conversationDisplay = conversationDisplay;
WxN.showContactDetail = showContactDetail;
WxN.hideContactDetail = hideContactDetail;
WxN.clearDetailCache = clearDetailCache;
WxN.openChat = openChat;
WxN.renderChatHead = renderChatHead;
WxN.renderChat = renderChat;
WxN.doSend = doSend;
WxN.sendBubbles = sendBubbles;
WxN.handleNewMessage = handleNewMessage;
})(window);