ai_scheduler/web/assets/js/wx.js

769 lines
33 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.

/* ============================================================
* 微信工作台模块(核心)
* - 状态管理 / 生命周期(mount / unmount)
* - 通讯录渲染 / WebSocket / 微信个人信息
* - AI 助手设置
* 子模块:wx_login.js(登录/在线检测)、wx_chat.js(聊天/会话/消息)
* 依赖:core.js(window.Core)
* 用法:Wx.mount(document.getElementById('view-wx'))
* ============================================================ */
(function (global) {
"use strict";
var $ = function (sel, root) { return (root || document).querySelector(sel); };
var $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); };
var state = null;
// WebSocket 连接状态
var wsConn = null; // WebSocket 实例
var wsReconnectTimer = null; // 重连定时器
var wsReconnectDelay = 1000; // 当前重连延迟(ms)
var WS_MAX_RECONNECT = 30000; // 最大重连延迟
/* ==================== 共享命名空间 ==================== */
// 子模块(wx_login.js / wx_chat.js)通过 WxN 访问共享状态和函数
var WxN = global.WxN || (global.WxN = {});
Object.defineProperty(WxN, 'state', {
get: function () { return state; },
set: function (v) { state = v; },
configurable: true
});
WxN.Core = Core;
WxN.$ = $;
WxN.$$ = $$;
/* ==================== brief 本地缓存 ==================== */
// 缓存结构:{ [appId]: { wxid: briefInfo, ... } }
function loadBriefCache() {
return Core.store(Core.KEYS.wxBrief) || {};
}
function saveBriefCache() {
var all = loadBriefCache();
var appId = state.app && state.app.appId;
if (!appId) return;
all[appId] = state.brief;
Core.store(Core.KEYS.wxBrief, all);
}
function clearBriefCache() {
var appId = state.app && state.app.appId;
if (!appId) { Core.store(Core.KEYS.wxBrief, null); return; }
var all = loadBriefCache();
delete all[appId];
Core.store(Core.KEYS.wxBrief, all);
}
function freshState(container) {
var app = Core.store(Core.KEYS.wxApp) || null;
// 从本地缓存恢复 brief 数据
var cachedBrief = {};
if (app && app.appId) {
var all = loadBriefCache();
cachedBrief = all[app.appId] || {};
}
return {
container: container,
app: app,
online: false,
tab: "conversations", // conversations | friends | chatrooms
contacts: { friends: [], chatrooms: [], ghs: [] },
conversations: [], // 会话列表(从 MongoDB 聚合)
convMsgs: {}, // {wxid: [{direction, msgType, content, createAt}]}
groupMembers: {}, // {chatroomWxid: {memberWxid: {nickName, bigHeadImgUrl, smallHeadImgUrl}}}
brief: cachedBrief, // wxid -> info(从缓存恢复)
briefLoading: false,
briefLoadedCount: 0, // 已加载 brief 的数量
briefTotalCount: 0, // 需要加载的总数
search: "",
active: null, // {wxid, name, avatar, type}
msgs: {}, // {wxid: [{role:'self', text, time}]}
qr: null, // {uuid, img, status, expired}
loginTimer: null,
ai: { sessionId: null, mission: "", versionDesc: "" },
aiTip: null,
wxAdvicer: null // {advicerId, name}
};
}
/* ==================== 挂载 ==================== */
function mount(container) {
// 防御性清理:防止外部未先调用 unmount 导致旧连接/定时器泄漏
unmount();
state = freshState(container);
container.innerHTML = shellHtml();
bindEvents();
renderContacts();
WxN.renderChat();
// 从 URL hash 获取 wx-advicer 值,调用 advicer/info 获取 wx_device_id(即 app_id),再检测在线状态
var advicerId = parseHashAdvicerId();
if (advicerId) {
console.log("[Wx.mount] hash wx-advicer=" + advicerId + ", fetching fresh device info");
WxN.renderLoginCard();
fetchDeviceAndCheck(advicerId);
} else {
console.log("[Wx.mount] no wx-advicer in hash, showing QR login");
WxN.renderLoginCard();
}
return { unmount: unmount };
}
/** 从 URL hash 解析 advicerId(格式:#wx-advicer=123) */
function parseHashAdvicerId() {
var hash = location.hash.replace(/^#/, '');
var match = hash.match(/wx-advicer=(\d+)/);
return match ? Number(match[1]) : 0;
}
/** 通过 API 获取销售的微信设备 ID,然后检测在线状态 */
function fetchDeviceAndCheck(advicerId) {
Core.api("advicer/info", { advicerId: advicerId }).then(function (info) {
if (!info || !info.wx_device_id) {
console.log("[Wx.mount] advicer not found or no wx_device_id");
return;
}
// 用设备 ID 作为 appId,检测在线状态
state.app = { appId: info.wx_device_id };
// 更新销售个人信息(姓名等,供提示栏显示)
if (state.wxAdvicer) {
state.wxAdvicer.name = info.name || state.wxAdvicer.name;
// 同步更新提示栏中的销售姓名显示
var nameEl = document.querySelector('#wx-mount-point .ci-name');
if (nameEl) nameEl.textContent = info.name || '';
}
console.log("[Wx.mount] got wxDeviceId=" + info.wx_device_id + ", checking online");
WxN.checkOnline().then(function (online) {
console.log("[Wx.mount] checkOnline result: " + online);
if (online) {
WxN.checkOnlineLoop();
loadProfile().then(function () { WxN.loadConversations(); });
WxN.loadContacts();
} else {
WxN.getQrCode();
}
});
}).catch(function (e) {
console.error("[Wx.mount] fetchDeviceAndCheck error:", e);
});
}
function unmount() {
if (!state) return;
if (state.loginTimer) clearInterval(state.loginTimer);
disconnectWs();
// 清理联系人详情缓存
WxN.clearDetailCache();
WxN.hideContactDetail();
state = null;
}
function shellHtml() {
return (
'<div class="wx-shell">' +
'<div class="wx-left">' +
'<div id="wxLoginCard"></div>' +
'<div class="wx-search">' +
'<input id="wxSearch" class="input sm" placeholder="搜索联系人 / 群聊">' +
'<button class="btn ghost sm" id="wxRefresh" title="刷新通讯录">⟳</button>' +
"</div>" +
'<div class="wx-contacts" id="wxContacts"></div>' +
"</div>" +
'<div class="wx-right">' +
'<div class="wx-chat-head" id="wxChatHead"></div>' +
'<div id="wxAiTipWrap"></div>' +
'<div class="wx-msgs" id="wxMsgs"></div>' +
'<div class="wx-input-area">' +
'<textarea class="wx-textarea" id="wxText" placeholder="选择左侧联系人后输入消息内容…(Ctrl + Enter 发送)"></textarea>' +
'<div class="wx-send-row">' +
'<span class="wx-hint">发送身份:<span id="wxSelfName" class="c-cyan">-</span></span>' +
'<span class="spacer"></span>' +
'<span class="wx-hint" id="wxSendHint"></span>' +
'<button class="btn success" id="wxAiAsk" title="基于 AI 会话给出话术建议">✦ AI 建议</button>' +
'<button class="btn primary" id="wxSendBtn">发送 (Ctrl+Enter)</button>' +
"</div>" +
"</div>" +
"</div>" +
"</div>"
);
}
function bindEvents() {
$("#wxRefresh").onclick = function () {
// 刷新:清除 brief 缓存,重新拉取通讯录 + brief + 会话
clearBriefCache();
state.brief = {};
WxN.loadContacts(true);
WxN.loadConversations();
};
$("#wxSearch").oninput = function (e) { state.search = e.target.value.trim(); renderContacts(); };
$("#wxSendBtn").onclick = WxN.doSend;
$("#wxAiAsk").onclick = doAskAI;
$("#wxText").addEventListener("keydown", function (e) {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); WxN.doSend(); }
});
}
/* ==================== 微信个人信息 ==================== */
/** 获取微信个人信息(昵称、头像、wxid 等) */
function loadProfile() {
if (!state || !state.app || !state.app.appId) return Promise.resolve();
console.log("[loadProfile] calling with appId=" + state.app.appId);
return Core.wxApi("personal/getProfile", { appId: state.app.appId })
.then(function (profile) {
console.log("[loadProfile] response:", profile);
if (!profile) return;
// 合并微信个人信息到 state.app
state.app.nickName = profile.nickName || state.app.nickName || "";
state.app.wxid = profile.wxid || state.app.wxid || "";
state.app.headImgUrl = profile.headImgUrl || profile.headImageUrl || state.app.headImgUrl || "";
// 持久化到 localStorage
Core.store(Core.KEYS.wxApp, state.app);
WxN.renderLoginCard();
// 更新发送身份显示
var selfName = $("#wxSelfName", state.container);
if (selfName) selfName.textContent = state.app.nickName || state.app.wxid || "-";
})
.catch(function (e) { console.error("[loadProfile] error:", e); });
}
/* ==================== 通讯录 ==================== */
function loadContacts(forceRefresh) {
if (!state.app || !state.app.appId) { Core.toast("请先登录微信", "err"); return; }
Core.loading(true, "拉取通讯录…");
Core.wxApi("contacts/fetchContactsList", { appId: state.app.appId })
.then(function (data) {
Core.loading(false);
state.contacts = {
friends: (data && data.friends) || [],
chatrooms: (data && data.chatrooms) || [],
ghs: (data && data.ghs) || []
};
renderContacts();
// 群聊仍需 getBriefInfo 拉取
loadBriefInfoForChatrooms();
// 好友信息统一由 diff 接口提供:库中已有的从 list 填充 brief,差集部分调 getBriefInfo 补充后入库
syncCustomerDiff();
})
.catch(function (e) { Core.loading(false); Core.toast("拉取通讯录失败:" + e.message, "err"); });
}
/** 仅拉取群聊的 brief info */
function loadBriefInfoForChatrooms() {
var chatrooms = state.contacts.chatrooms;
if (!chatrooms.length) return;
var batches = [];
for (var i = 0; i < chatrooms.length; i += 20) batches.push(chatrooms.slice(i, i + 20));
var chain = Promise.resolve();
batches.forEach(function (batch) {
chain = chain.then(function () {
return getBriefInfoWithRetry(batch)
.then(function (list) {
(list || []).forEach(function (info) { if (info && info.userName) state.brief[info.userName] = info; });
renderContacts();
})
.catch(function () {});
});
});
}
/** getBriefInfo 带重试(最多 3 次,间隔 2 秒) */
function getBriefInfoWithRetry(wxids, retries) {
retries = retries || 3;
return Core.wxApi("contacts/getBriefInfo", { appId: state.app.appId, wxids: wxids })
.catch(function (e) {
if (retries <= 1) throw e;
console.warn("[getBriefInfo] retrying, " + (retries - 1) + " attempts left");
return new Promise(function (resolve) { setTimeout(resolve, 2000); })
.then(function () { return getBriefInfoWithRetry(wxids, retries - 1); });
});
}
/** 同步好友差集到 ai_advice_customer 表 */
function syncCustomerDiff() {
var selfWxid = state.app && state.app.wxid;
if (!selfWxid || !state.contacts.friends.length) return;
Core.api("advicer/customer/diff", { selfWxid: selfWxid, userNames: state.contacts.friends })
.then(function (res) {
if (!res) return;
// 将库中已有的好友记录写入 brief
(res.list || []).forEach(function (item) {
if (item.user_name && !state.brief[item.user_name]) {
state.brief[item.user_name] = {
userName: item.user_name,
nickName: item.nick_name,
remark: item.remark,
sex: item.sex,
bigHeadImgUrl: item.big_head_img_url,
smallHeadImgUrl: item.small_head_img_url,
alias: item.alias,
signature: item.signature,
province: item.province,
city: item.city
};
}
});
renderContacts();
var diff = res.diff;
if (!diff || !diff.length) return;
console.log("[syncCustomerDiff] missing " + diff.length + " friends, fetching brief info");
// 每批 20 个拉取 getBriefInfo(带重试)
var batches = [];
for (var i = 0; i < diff.length; i += 20) batches.push(diff.slice(i, i + 20));
var allInfo = [];
var chain = Promise.resolve();
batches.forEach(function (batch) {
chain = chain.then(function () {
return getBriefInfoWithRetry(batch)
.then(function (list) {
(list || []).forEach(function (info) {
if (info && info.userName) {
allInfo.push(info);
state.brief[info.userName] = info;
}
});
});
});
});
chain.then(function () {
if (!allInfo.length) return;
// 组装入库数据
var records = allInfo.map(function (info) {
return {
selfWxid: selfWxid,
userName: info.userName || "",
nickName: info.nickName || "",
pyInitial: info.pyInitial || "",
quanPin: info.quanPin || "",
sex: info.sex || 0,
remark: info.remark || "",
remarkPyInitial: info.remarkPyInitial || "",
remarkQuanPin: info.remarkQuanPin || "",
signature: info.signature || "",
alias: info.alias || "",
snsBgImg: info.snsBgImg || "",
country: info.country || "",
bigHeadImgUrl: info.bigHeadImgUrl || "",
smallHeadImgUrl: info.smallHeadImgUrl || "",
description: info.description || "",
cardImgUrl: info.cardImgUrl || "",
labelList: info.labelList || "",
province: info.province || "",
city: info.city || "",
phoneNumList: info.phoneNumList || ""
};
});
Core.api("advicer/customer/batch_add", { list: records })
.then(function () { console.log("[syncCustomerDiff] synced " + records.length + " friends"); })
.catch(function (e) { console.warn("[syncCustomerDiff] batch_add failed:", e); });
renderContacts();
});
})
.catch(function (e) { console.warn("[syncCustomerDiff] diff failed:", e); });
}
function contactDisplay(wxid) {
var info = state.brief[wxid] || {};
var name = info.remark || info.nickName || wxid;
return { name: name, avatar: info.bigHeadImgUrl || info.smallHeadImgUrl || "" };
}
function renderContacts() {
var box = $("#wxContacts", state.container);
if (!state.app || !state.app.appId) {
box.innerHTML = '<div class="empty" style="padding:30px 14px"><div class="big">◇</div>登录微信后自动拉取通讯录</div>';
return;
}
var kw = state.search.toLowerCase();
var tabs =
'<div class="seg" style="margin:10px 12px 4px">' +
'<div class="seg-item ' + (state.tab === "conversations" ? "active" : "") + '" data-tab="conversations">会话 ' + state.conversations.length + "</div>" +
'<div class="seg-item ' + (state.tab === "friends" ? "active" : "") + '" data-tab="friends">好友</div>' +
'<div class="seg-item ' + (state.tab === "chatrooms" ? "active" : "") + '" data-tab="chatrooms">群聊 ' + state.contacts.chatrooms.length + "</div>" +
"</div>";
var items;
if (state.tab === "conversations") {
// 会话列表
var convList = state.conversations;
if (kw) {
convList = convList.filter(function (c) {
var d = WxN.conversationDisplay(c.wxid);
return d.name.toLowerCase().indexOf(kw) >= 0 || c.wxid.toLowerCase().indexOf(kw) >= 0;
});
}
if (!convList.length) {
items = '<div class="empty" style="padding:30px 14px"><div class="big">✉</div>' + (kw ? "无匹配会话" : "暂无会话,收到消息后自动显示") + "</div>";
} else {
items = convList.map(function (c) {
var d = WxN.conversationDisplay(c.wxid);
var active = state.active && state.active.wxid === c.wxid ? " active" : "";
var timeStr = c.lastMsgTime ? WxN.timeAgo(c.lastMsgTime * 1000) : "";
var dirIcon = c.lastDirection === "customer" ? "←" : c.lastDirection === "self" ? "→" : "";
var unreadBadge = (c.unreadCount > 0)
? '<span class="conv-unread">' + (c.unreadCount > 99 ? "99+" : c.unreadCount) + '</span>'
: '';
return '<div class="contact-item conv-item' + active + '" data-wxid="' + Core.esc(c.wxid) + '">' +
'<div class="conv-avatar-wrap">' +
(d.avatar
? '<img class="wx-avatar" style="width:36px;height:36px" src="' + Core.esc(d.avatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'wx-avatar\',style:\'width:36px;height:36px\',textContent:\'?\'}))">'
: '<div class="wx-avatar" style="width:36px;height:36px">' + Core.esc(Core.initial(d.name)) + "</div>") +
unreadBadge + '</div>' +
'<div class="ci-main"><div class="ci-name-row"><span class="ci-name">' + Core.esc(d.name) + '</span>' +
(c.isGroup ? '<span class="ci-tag">群</span>' : '') +
'<span class="ci-time">' + Core.esc(timeStr) + "</span></div>" +
'<div class="ci-sub">' + Core.esc(dirIcon + " " + (c.lastMsg || "")) + "</div></div></div>";
}).join("");
}
} else {
// 好友/群聊列表(原有逻辑)
var list = state.tab === "friends" ? state.contacts.friends : state.contacts.chatrooms;
if (state.tab === "friends") {
list = list.filter(function (w) {
var info = state.brief[w] || {};
return info.sex !== 0;
});
}
if (kw) {
list = list.filter(function (w) {
var d = contactDisplay(w);
return d.name.toLowerCase().indexOf(kw) >= 0 || w.toLowerCase().indexOf(kw) >= 0;
});
}
if (state.briefLoading && !Object.keys(state.brief).length) {
items = '<div class="empty" style="padding:26px"><span class="spin"></span><div class="mt-1 fs-12">正在加载联系人信息…</div></div>';
} else if (!list.length) {
items = '<div class="empty" style="padding:30px 14px"><div class="big">☰</div>' + (kw ? "无匹配联系人" : "暂无联系人,点击右上角 ⟳ 刷新") + "</div>";
} else {
items = list.map(function (w) {
var d = contactDisplay(w);
var active = state.active && state.active.wxid === w ? " active" : "";
return '<div class="contact-item' + active + '" data-wxid="' + Core.esc(w) + '">' +
(d.avatar
? '<img class="wx-avatar" style="width:36px;height:36px" src="' + Core.esc(d.avatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'wx-avatar\',style:\'width:36px;height:36px\',textContent:\'?\'}))">'
: '<div class="wx-avatar" style="width:36px;height:36px">' + Core.esc(Core.initial(d.name)) + "</div>") +
'<div class="ci-main"><div class="ci-name">' + Core.esc(d.name) + "</div>" +
'<div class="ci-sub mono">' + Core.esc(w) + "</div></div></div>";
}).join("");
}
}
// 加载中进度提示
var progressHint = '';
if (state.briefLoading && state.briefTotalCount > 0) {
progressHint = '<div class="fs-11 c-dim" style="text-align:center;padding:4px 0">加载联系人信息 ' +
Math.min(state.briefLoadedCount, state.briefTotalCount) + ' / ' + state.briefTotalCount + '</div>';
}
box.innerHTML = tabs + items + progressHint;
$$(".seg-item", box).forEach(function (s) {
s.onclick = function () {
state.tab = s.dataset.tab;
if (state.tab === "conversations") WxN.loadConversations();
renderContacts();
};
});
$$(".contact-item", box).forEach(function (c) {
c.onclick = function () {
var wxid = c.dataset.wxid;
if (state.tab === "conversations") {
WxN.openConversationChat(wxid);
} else {
WxN.openChat(wxid);
}
};
// 悬浮显示详情气泡(仅好友/群聊 tab)
if (state.tab !== "conversations") {
c.onmouseenter = function (e) { WxN.showContactDetail(c.dataset.wxid, c, e); };
c.onmouseleave = function () { WxN.hideContactDetail(); };
}
});
}
/* ==================== WebSocket 实时通信 ==================== */
function connectWs() {
if (!state || !state.app || !state.app.appId) {
console.warn("[WS] connectWs skipped: appId missing");
return;
}
console.log("[WS] connecting, appId=" + state.app.appId);
disconnectWs(); // 先断开已有连接
var token = Core.store(Core.KEYS.token);
if (!token) { console.warn("[WS] connectWs skipped: token missing"); return; }
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
var url = protocol + "//" + location.host + "/api/v1/advicer/ws?token=" + encodeURIComponent(token);
try {
wsConn = new WebSocket(url);
} catch (e) {
scheduleWsReconnect();
return;
}
wsConn.onopen = function () {
console.log("[WS] connected, subscribing appId=" + state.app.appId);
wsReconnectDelay = 1000; // 重置重连延迟
// 订阅当前 appId 的事件
wsConn.send(JSON.stringify({ action: "subscribe", appId: state.app.appId }));
// 启动心跳(每 20 秒发一次 ping)
startWsHeartbeat();
};
wsConn.onmessage = function (e) {
try {
var msg = JSON.parse(e.data);
console.log("[WS] received event:", msg.event, msg.data);
handleWsEvent(msg);
} catch (err) { console.warn("[WS] parse error:", err); }
};
wsConn.onclose = function () {
console.log("[WS] connection closed");
stopWsHeartbeat();
wsConn = null;
scheduleWsReconnect();
};
wsConn.onerror = function () {
// onclose 会紧随触发,无需额外处理
};
}
function disconnectWs() {
stopWsHeartbeat();
if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; }
if (wsConn) {
wsConn.onclose = null; // 避免触发重连
wsConn.close();
wsConn = null;
}
wsReconnectDelay = 1000;
}
function scheduleWsReconnect() {
if (!state || !state.app || !state.app.appId) return;
if (wsReconnectTimer) return;
wsReconnectTimer = setTimeout(function () {
wsReconnectTimer = null;
connectWs();
}, wsReconnectDelay);
wsReconnectDelay = Math.min(wsReconnectDelay * 2, WS_MAX_RECONNECT);
}
var wsHeartbeatTimer = null;
function startWsHeartbeat() {
stopWsHeartbeat();
wsHeartbeatTimer = setInterval(function () {
if (wsConn && wsConn.readyState === WebSocket.OPEN) {
wsConn.send(JSON.stringify({ action: "ping" }));
}
}, 20000);
}
function stopWsHeartbeat() {
if (wsHeartbeatTimer) { clearInterval(wsHeartbeatTimer); wsHeartbeatTimer = null; }
}
function handleWsEvent(msg) {
if (!msg || !msg.event || !state) return;
switch (msg.event) {
case "new_message":
WxN.handleNewMessage(msg.data);
break;
case "online_status":
WxN.handleOnlineStatus(msg.data);
break;
}
}
/* ==================== AI 助手 ==================== */
function openAiConfig() {
var proj = Core.store(Core.KEYS.project);
if (!proj || !proj.projectId) {
Core.toast("请先在「项目管理」进入一个项目,AI 会话基于项目的销售版本/技巧注册", "err");
return;
}
var projectId = proj.projectId;
var m = Core.modal({
title: "⚙ AI 助手设置(销售话术演练会话)",
width: "wide",
body: '<div class="empty" style="padding:20px"><span class="spin"></span></div>'
});
Core.api("advicer/list", { projectId: projectId }).then(function (advicers) {
advicers = advicers || [];
var advOptions = advicers.map(function (a) {
return '<option value="' + a.advicer_id + '">#' + a.advicer_id + " " + Core.esc(a.name || "") + "</option>";
}).join("");
m.body.innerHTML =
'<div class="card-sub" style="margin-top:0">选择销售版本 + 聊天技巧 + 客户,注册一个 AI 演练会话;在聊天窗口点击「AI 建议」即可生成回复话术。</div>' +
'<div class="field"><label class="label">销售人员<span class="req">*</span></label><select id="ai-advicer" class="select">' + advOptions + "</select></div>" +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">销售版本<span class="req">*</span></label><select id="ai-version" class="select"><option>加载中…</option></select></div>' +
'<div class="field"><label class="label">聊天技巧<span class="req">*</span></label><select id="ai-skill" class="select"><option>加载中…</option></select></div>' +
"</div>" +
'<div class="field"><label class="label">客户(可选)</label><select id="ai-client" class="select"><option value="">不指定客户</option></select></div>' +
'<div class="field"><label class="label">任务 Mission<span class="req">*</span></label><textarea id="ai-mission" class="textarea" placeholder="本轮对话任务,如:了解客户预算与购房目的,邀约到访">' + Core.esc(state.ai.mission || "") + "</textarea></div>" +
'<div class="row"><span class="badge ' + (state.ai.sessionId ? "green" : "gray") + '" id="ai-session-badge">' + (state.ai.sessionId ? "会话已就绪(重注册将覆盖)" : "尚未注册会话") + "</span></div>";
$(".modal-foot", m.root).innerHTML = '<button class="btn ghost mf-cancel">关闭</button><button class="btn primary mf-regis">注册 AI 会话</button>';
$(".mf-cancel", m.root).onclick = m.close;
if (!advicers.length) {
m.body.innerHTML += '<div class="mt-1 fs-12 c-warn">当前项目暂无销售人员,请先到「数据中心 → 销售人员」新增。</div>';
} else {
loadVersions(advicers[0].advicer_id);
loadSkills(advicers[0].advicer_id);
}
loadClients();
$("#ai-advicer", m.root).onchange = function () {
loadVersions(this.value);
loadSkills(this.value);
};
function loadVersions(advicerId) {
Core.api("advicer/version/list", { advicerId: Number(advicerId) }).then(function (list) {
list = list || [];
$("#ai-version", m.root).innerHTML = list.length
? list.map(function (v) { return '<option value="' + v.id + '">' + Core.esc(v.versionDesc || v.id) + "</option>"; }).join("")
: '<option value="">无版本(请到数据中心创建)</option>';
}).catch(function (e) { Core.toast(e.message, "err"); });
}
function loadSkills(advicerId) {
Core.api("skill/list", { projectId: projectId, advicerId: Number(advicerId) }).then(function (list) {
list = list || [];
$("#ai-skill", m.root).innerHTML = list.length
? list.map(function (s) { return '<option value="' + s.id + '">' + Core.esc((s.desc || "技巧") + " · " + s.id.slice(-6)) + "</option>"; }).join("")
: '<option value="">无技巧(请到数据中心创建)</option>';
}).catch(function (e) { Core.toast(e.message, "err"); });
}
function loadClients() {
Core.api("client/list", { projectId: projectId }).then(function (list) {
list = list || [];
$("#ai-client", m.root).innerHTML = '<option value="">不指定客户</option>' +
list.map(function (c) {
var name = (c.personalInfo && c.personalInfo.name) || "客户";
return '<option value="' + c.id + '">' + Core.esc(name) + " · " + c.id.slice(-6) + "</option>";
}).join("");
}).catch(function () {});
}
$(".mf-regis", m.root).onclick = function () {
var versionId = $("#ai-version", m.root).value;
var skillId = $("#ai-skill", m.root).value;
var mission = $("#ai-mission", m.root).value.trim();
if (!versionId) { Core.toast("请选择销售版本", "err"); return; }
if (!skillId) { Core.toast("请选择聊天技巧", "err"); return; }
if (!mission) { Core.toast("请填写任务 Mission", "err"); return; }
Core.loading(true, "注册 AI 会话…");
Core.api("chat/regis", {
advicerVersionId: versionId,
clientId: $("#ai-client", m.root).value || "",
talkSkillId: skillId,
mission: mission
}).then(function (sessionId) {
Core.loading(false);
state.ai.sessionId = sessionId;
state.ai.mission = mission;
Core.toast("AI 会话注册成功", "ok");
m.close();
WxN.renderChatHead();
}).catch(function (e) { Core.loading(false); Core.toast("注册失败:" + e.message, "err"); });
};
}).catch(function (e) {
m.body.innerHTML = '<div class="empty">' + Core.esc(e.message) + "</div>";
});
}
function clearAiTip() {
state.aiTip = null;
var wrap = $("#wxAiTipWrap", state.container);
if (wrap) wrap.innerHTML = "";
}
function renderAiTip(tip) {
state.aiTip = tip;
var wrap = $("#wxAiTipWrap", state.container);
var badge = tip.missionStatus
? '<span class="badge ' + (tip.missionStatus === "completed" ? "green" : tip.missionStatus === "fail" ? "red" : "yellow") + '">' + Core.esc(tip.missionStatus) + "</span>"
: "";
wrap.innerHTML =
'<div class="ai-tip">' +
'<div class="ai-head">✦ AI 话术建议 ' + badge + '<span class="spacer"></span><span class="fs-12 c-dim">' + Core.esc(tip.missionDesc || "") + "</span></div>" +
'<div class="ai-body">' + Core.splitBubbles(tip.text).map(function (b) {
return '<div class="msg-bubble" style="border-color:rgba(45,224,255,.35);margin:0 0 6px">' + Core.esc(b) + "</div>";
}).join("") + "</div>" +
'<div class="ai-foot">' +
'<button class="btn success sm" id="aiFill">填入输入框</button>' +
(state.active ? '<button class="btn ghost sm" id="aiSendNow">直接发送</button>' : "") +
'<button class="btn ghost sm" id="aiClose">关闭</button>' +
"</div></div>";
$("#aiFill", wrap).onclick = function () {
$("#wxText", state.container).value = tip.text;
$("#wxText", state.container).focus();
clearAiTip();
};
var sendNow = $("#aiSendNow", wrap);
if (sendNow) sendNow.onclick = function () {
if (!state.active) return;
if (sendNow.disabled) return;
sendNow.disabled = true;
sendNow.textContent = "发送中…";
WxN.sendBubbles(state.active.wxid, tip.text).then(function () {
sendNow.disabled = false;
sendNow.textContent = "直接发送";
clearAiTip();
});
};
$("#aiClose", wrap).onclick = function () {
if (state.active) {
var bs = Core.splitBubbles(tip.text);
if (!bs.length) bs = ["(空消息)"];
bs.forEach(function (b) {
state.msgs[state.active.wxid].push({ role: "ai", text: b, time: Core.nowTime() });
});
}
clearAiTip();
WxN.renderChat();
};
}
function doAskAI() {
if (!state.ai.sessionId) {
Core.toast("尚未注册 AI 会话,请点击右上角「AI 助手设置」", "err");
openAiConfig();
return;
}
var draft = $("#wxText", state.container).value.trim();
if (!draft) { Core.toast("请先在输入框粘贴客户消息或沟通上下文", "err"); return; }
Core.loading(true, "AI 生成话术中…");
Core.api("chat/chat", { sessionId: state.ai.sessionId, content: draft })
.then(function (assistant) {
Core.loading(false);
assistant = assistant || {};
var text = assistant.result || "";
if (typeof assistant === "string") text = assistant;
if (!text) { Core.toast("AI 未返回内容", "err"); return; }
renderAiTip({
text: text,
missionStatus: assistant.mission_status || "",
missionDesc: assistant.mission_complete_desc || ""
});
})
.catch(function (e) { Core.loading(false); Core.toast("生成失败:" + e.message, "err"); });
}
/* ==================== 导出 ==================== */
WxN.clearBriefCache = clearBriefCache;
WxN.loadProfile = loadProfile;
WxN.loadContacts = loadContacts;
WxN.renderContacts = renderContacts;
WxN.renderChatHead = WxN.renderChatHead || function () {}; // 由 wx_chat.js 覆盖
WxN.renderChat = WxN.renderChat || function () {}; // 由 wx_chat.js 覆盖
WxN.contactDisplay = contactDisplay;
WxN.connectWs = connectWs;
WxN.disconnectWs = disconnectWs;
WxN.openAiConfig = openAiConfig;
global.Wx = { mount: mount, unmount: unmount };
})(window);