feat: 客服自动回复、历史消息修复、扫码兼容页与商家头像
- 新增 scriptService 对接话术接口,cs-chat 支持关键词自动回复 - chat-history 增加 waitChatImReady,修复私聊/群聊/客服历史加载 - 恢复 dashouduan/guanshiduan 兼容页跳转 fighter 保留旧二维码 - 商家我的页头像与标签挤压样式优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
8
app.json
8
app.json
@@ -17,6 +17,14 @@
|
|||||||
"components/chenghao-tag/chenghao-tag"
|
"components/chenghao-tag/chenghao-tag"
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "pages/dashouduan",
|
||||||
|
"pages": ["dashouduan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/guanshiduan",
|
||||||
|
"pages": ["guanshiduan"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/pick-user",
|
"root": "pages/pick-user",
|
||||||
"pages": ["pick-user"]
|
"pages": ["pick-user"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
fetchHistoryMessages,
|
fetchHistoryMessages,
|
||||||
getOldestHistoryTimestamp,
|
getOldestHistoryTimestamp,
|
||||||
mergeHistoryMessages,
|
mergeHistoryMessages,
|
||||||
waitImConnected,
|
waitChatImReady,
|
||||||
} from '../../utils/chat-history.js';
|
} from '../../utils/chat-history.js';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
@@ -98,27 +98,27 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureChatReady() {
|
async ensureChatReady() {
|
||||||
const targetUserId = getLocalImUserId(app);
|
const targetUserId = getLocalImUserId(app);
|
||||||
if (!targetUserId) return;
|
if (!targetUserId) return;
|
||||||
|
|
||||||
const ready = () => {
|
const ready = async () => {
|
||||||
this.setupAllListeners();
|
this.setupAllListeners();
|
||||||
this.loadHistory(true);
|
await this.loadHistory(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (app.ensureConnection) {
|
try {
|
||||||
const ret = app.ensureConnection();
|
const ok = await waitChatImReady(app, 12000);
|
||||||
if (ret && typeof ret.then === 'function') {
|
if (ok) {
|
||||||
ret.then(ready).catch(ready);
|
await ready();
|
||||||
} else {
|
return;
|
||||||
setTimeout(ready, 300);
|
|
||||||
}
|
}
|
||||||
return;
|
} catch (e) {
|
||||||
|
console.warn('[私聊] IM 连接失败', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isConnected() && wx.goEasy.im?.userId === targetUserId) {
|
if (this.isConnected() && wx.goEasy.im?.userId === targetUserId) {
|
||||||
ready();
|
await ready();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.connectGoEasy(targetUserId);
|
this.connectGoEasy(targetUserId);
|
||||||
@@ -295,7 +295,7 @@ Page({
|
|||||||
|
|
||||||
this.setData({ loadingHistory: true });
|
this.setData({ loadingHistory: true });
|
||||||
try {
|
try {
|
||||||
const ready = await waitImConnected(app);
|
const ready = await waitChatImReady(app, 12000);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
fetchHistoryMessages,
|
fetchHistoryMessages,
|
||||||
getOldestHistoryTimestamp,
|
getOldestHistoryTimestamp,
|
||||||
mergeHistoryMessages,
|
mergeHistoryMessages,
|
||||||
waitImConnected,
|
waitChatImReady,
|
||||||
} from '../../utils/chat-history.js';
|
} from '../../utils/chat-history.js';
|
||||||
|
import { matchAutoReply, getWelcomeText } from '../../utils/scriptService.js';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -37,6 +38,7 @@ Page({
|
|||||||
keyboardHeight: 0,
|
keyboardHeight: 0,
|
||||||
bottomSafeHeight: 140,
|
bottomSafeHeight: 140,
|
||||||
defaultAvatarUrl: '',
|
defaultAvatarUrl: '',
|
||||||
|
welcomeText: '你好,请问有什么可以帮到您的?',
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
@@ -58,6 +60,7 @@ Page({
|
|||||||
this.setData({ teamId, teamName, teamAvatar: teamAvatarResolved, defaultAvatarUrl: defAvatar });
|
this.setData({ teamId, teamName, teamAvatar: teamAvatarResolved, defaultAvatarUrl: defAvatar });
|
||||||
wx.setNavigationBarTitle({ title: teamName });
|
wx.setNavigationBarTitle({ title: teamName });
|
||||||
this.initCurrentUser();
|
this.initCurrentUser();
|
||||||
|
this.loadWelcomeScript();
|
||||||
this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => {
|
this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => {
|
||||||
const def = getDefaultAvatarUrl(app);
|
const def = getDefaultAvatarUrl(app);
|
||||||
this.setData({
|
this.setData({
|
||||||
@@ -100,31 +103,52 @@ Page({
|
|||||||
this.clearAllListeners();
|
this.clearAllListeners();
|
||||||
},
|
},
|
||||||
|
|
||||||
autoConnect() {
|
async loadWelcomeScript() {
|
||||||
|
try {
|
||||||
|
const text = await getWelcomeText();
|
||||||
|
if (!text) return;
|
||||||
|
this.setData({ welcomeText: text });
|
||||||
|
const msgs = this.data.messages;
|
||||||
|
const idx = msgs.findIndex((m) => m.messageId === 'welcome-cs');
|
||||||
|
if (idx >= 0) {
|
||||||
|
const updated = [...msgs];
|
||||||
|
updated[idx] = { ...updated[idx], payload: { ...updated[idx].payload, text } };
|
||||||
|
this.setData({ messages: updated });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('loadWelcomeScript failed', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async autoConnect() {
|
||||||
const targetUserId = getLocalImUserId(app);
|
const targetUserId = getLocalImUserId(app);
|
||||||
if (!targetUserId) return;
|
if (!targetUserId) return;
|
||||||
|
|
||||||
const ready = () => {
|
const ready = async () => {
|
||||||
this.setupAllListeners();
|
this.setupAllListeners();
|
||||||
this.loadHistory(true);
|
await this.loadHistory(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (app.ensureConnection) {
|
try {
|
||||||
app.ensureConnection();
|
const ok = await waitChatImReady(app, 12000);
|
||||||
setTimeout(ready, 400);
|
if (ok) {
|
||||||
return;
|
await ready();
|
||||||
}
|
|
||||||
|
|
||||||
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
|
|
||||||
if (status === 'connected' || status === 'reconnected') {
|
|
||||||
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
|
|
||||||
if (currentUserId === targetUserId) {
|
|
||||||
ready();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wx.goEasy.disconnect();
|
} catch (e) {
|
||||||
|
console.warn('[客服] IM 连接失败', e);
|
||||||
}
|
}
|
||||||
this.connectGoEasy(targetUserId);
|
|
||||||
|
if (this.isConnected()) {
|
||||||
|
await ready();
|
||||||
|
} else {
|
||||||
|
this.connectGoEasy(targetUserId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isConnected() {
|
||||||
|
const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
|
||||||
|
return s === 'connected' || s === 'reconnected';
|
||||||
},
|
},
|
||||||
|
|
||||||
connectGoEasy(userId) {
|
connectGoEasy(userId) {
|
||||||
@@ -256,7 +280,7 @@ Page({
|
|||||||
|
|
||||||
this.setData({ loadingHistory: true });
|
this.setData({ loadingHistory: true });
|
||||||
try {
|
try {
|
||||||
const ready = await waitImConnected(app);
|
const ready = await waitChatImReady(app, 12000);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
@@ -317,7 +341,7 @@ Page({
|
|||||||
name: this.data.teamName || '客服',
|
name: this.data.teamName || '客服',
|
||||||
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
|
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
|
||||||
},
|
},
|
||||||
payload: { text: '你好,请问有什么可以帮到您的?' },
|
payload: { text: this.data.welcomeText || '你好,请问有什么可以帮到您的?' },
|
||||||
status: 'success',
|
status: 'success',
|
||||||
read: false,
|
read: false,
|
||||||
formattedTime: formatDate(Date.now()),
|
formattedTime: formatDate(Date.now()),
|
||||||
@@ -451,11 +475,53 @@ Page({
|
|||||||
});
|
});
|
||||||
wx.goEasy.im.sendMessage({
|
wx.goEasy.im.sendMessage({
|
||||||
message:msg,
|
message:msg,
|
||||||
onSuccess:() => this.updateMsg(id, 'success'),
|
onSuccess:() => {
|
||||||
|
this.updateMsg(id, 'success');
|
||||||
|
this.tryAutoReply(text);
|
||||||
|
},
|
||||||
onFailed:() => this.updateMsg(id, 'failed')
|
onFailed:() => this.updateMsg(id, 'failed')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async tryAutoReply(userText) {
|
||||||
|
try {
|
||||||
|
const result = await matchAutoReply(userText);
|
||||||
|
if (result && result.matched && result.content) {
|
||||||
|
this.insertCsAutoReply(result);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('tryAutoReply failed', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insertCsAutoReply(result) {
|
||||||
|
const replyType = (result && result.reply_type) || 'text';
|
||||||
|
const content = (result && result.content) || '';
|
||||||
|
if (!content) return;
|
||||||
|
const isImage = replyType === 'image';
|
||||||
|
const msg = {
|
||||||
|
messageId: 'auto-reply-' + Date.now(),
|
||||||
|
type: isImage ? 'image' : 'text',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
senderId: this.data.teamId,
|
||||||
|
receiverId: this.data.currentUser?.id,
|
||||||
|
senderData: {
|
||||||
|
name: this.data.teamName,
|
||||||
|
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
|
||||||
|
},
|
||||||
|
payload: isImage ? { url: content } : { text: content },
|
||||||
|
status: 'success',
|
||||||
|
read: false,
|
||||||
|
formattedTime: formatDate(Date.now()),
|
||||||
|
showTime: this.needShow(Date.now()),
|
||||||
|
};
|
||||||
|
if (isImage) msg.imageUrl = content;
|
||||||
|
this.setData({
|
||||||
|
messages: [...this.data.messages, msg],
|
||||||
|
scrollToView: 'msg-bottom',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
sendImageMsg(filePath, fileObj) {
|
sendImageMsg(filePath, fileObj) {
|
||||||
const { currentUser, teamId, teamName, teamAvatar } = this.data;
|
const { currentUser, teamId, teamName, teamAvatar } = this.data;
|
||||||
if (!teamId || !currentUser || !filePath) return;
|
if (!teamId || !currentUser || !filePath) return;
|
||||||
|
|||||||
14
pages/dashouduan/dashouduan.js
Normal file
14
pages/dashouduan/dashouduan.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/** 旧二维码兼容:dashouduan → 打手中心,inviteCode 自动注册逻辑不变 */
|
||||||
|
import { parseSceneOptions } from '../../utils/base-page.js';
|
||||||
|
|
||||||
|
Page({
|
||||||
|
onLoad(options) {
|
||||||
|
const parsed = parseSceneOptions(options || {});
|
||||||
|
const inviteCode = parsed.inviteCode || options.inviteCode || '';
|
||||||
|
let url = '/pages/fighter/fighter';
|
||||||
|
if (inviteCode) {
|
||||||
|
url += `?inviteCode=${encodeURIComponent(inviteCode)}`;
|
||||||
|
}
|
||||||
|
wx.reLaunch({ url });
|
||||||
|
},
|
||||||
|
});
|
||||||
1
pages/dashouduan/dashouduan.json
Normal file
1
pages/dashouduan/dashouduan.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "navigationBarTitleText": "加载中", "usingComponents": {} }
|
||||||
1
pages/dashouduan/dashouduan.wxml
Normal file
1
pages/dashouduan/dashouduan.wxml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<view class="tip">正在进入打手端…</view>
|
||||||
1
pages/dashouduan/dashouduan.wxss
Normal file
1
pages/dashouduan/dashouduan.wxss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.tip { display:flex; align-items:center; justify-content:center; min-height:100vh; color:#999; font-size:28rpx; }
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
fetchHistoryMessages,
|
fetchHistoryMessages,
|
||||||
getOldestHistoryTimestamp,
|
getOldestHistoryTimestamp,
|
||||||
mergeHistoryMessages,
|
mergeHistoryMessages,
|
||||||
waitImConnected,
|
waitChatImReady,
|
||||||
} from '../../utils/chat-history.js';
|
} from '../../utils/chat-history.js';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
@@ -280,29 +280,32 @@ Page({
|
|||||||
this.stopOrderStatusPoll();
|
this.stopOrderStatusPoll();
|
||||||
},
|
},
|
||||||
|
|
||||||
autoConnect() {
|
async autoConnect() {
|
||||||
const targetUserId = this.data.currentUser?.id || getLocalImUserId(app);
|
const targetUserId = this.data.currentUser?.id || getLocalImUserId(app);
|
||||||
if (!targetUserId) return;
|
if (!targetUserId) return;
|
||||||
|
|
||||||
const afterReady = () => {
|
const afterReady = async () => {
|
||||||
this.subscribeGroupIfNeeded().then(() => {
|
await this.subscribeGroupIfNeeded();
|
||||||
this.setupAllListeners();
|
this.setupAllListeners();
|
||||||
this.loadHistory(true);
|
await this.loadHistory(true);
|
||||||
this.markGroupMessageAsRead();
|
this.markGroupMessageAsRead();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (app.ensureConnection) {
|
try {
|
||||||
app.ensureConnection();
|
const ok = await waitChatImReady(app, 12000);
|
||||||
setTimeout(afterReady, 400);
|
if (ok) {
|
||||||
return;
|
await afterReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[群聊] IM 连接失败', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
|
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
|
||||||
if (status === 'connected' || status === 'reconnected') {
|
if (status === 'connected' || status === 'reconnected') {
|
||||||
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
|
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
|
||||||
if (currentUserId === targetUserId) {
|
if (currentUserId === targetUserId) {
|
||||||
afterReady();
|
await afterReady();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wx.goEasy.disconnect();
|
wx.goEasy.disconnect();
|
||||||
@@ -413,7 +416,7 @@ Page({
|
|||||||
|
|
||||||
this.setData({ loadingHistory: true });
|
this.setData({ loadingHistory: true });
|
||||||
try {
|
try {
|
||||||
const ready = await waitImConnected(app);
|
const ready = await waitChatImReady(app, 12000);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
14
pages/guanshiduan/guanshiduan.js
Normal file
14
pages/guanshiduan/guanshiduan.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/** 旧二维码兼容:guanshiduan → 打手中心管事注册,逻辑不变 */
|
||||||
|
import { parseSceneOptions } from '../../utils/base-page.js';
|
||||||
|
|
||||||
|
Page({
|
||||||
|
onLoad(options) {
|
||||||
|
const parsed = parseSceneOptions(options || {});
|
||||||
|
const inviteCode = parsed.inviteCode || options.inviteCode || '';
|
||||||
|
let url = '/pages/fighter/fighter?registerType=guanshi';
|
||||||
|
if (inviteCode) {
|
||||||
|
url += `&inviteCode=${encodeURIComponent(inviteCode)}`;
|
||||||
|
}
|
||||||
|
wx.reLaunch({ url });
|
||||||
|
},
|
||||||
|
});
|
||||||
1
pages/guanshiduan/guanshiduan.json
Normal file
1
pages/guanshiduan/guanshiduan.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "navigationBarTitleText": "加载中", "usingComponents": {} }
|
||||||
1
pages/guanshiduan/guanshiduan.wxml
Normal file
1
pages/guanshiduan/guanshiduan.wxml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<view class="tip">正在进入管事注册…</view>
|
||||||
1
pages/guanshiduan/guanshiduan.wxss
Normal file
1
pages/guanshiduan/guanshiduan.wxss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.tip { display:flex; align-items:center; justify-content:center; min-height:100vh; color:#999; font-size:28rpx; }
|
||||||
@@ -72,6 +72,21 @@ page {
|
|||||||
padding: 10rpx 30rpx;
|
padding: 10rpx 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-info > .flex {
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info > .flex > .avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info > .flex > view:not(.avatar) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 110rpx;
|
width: 110rpx;
|
||||||
height: 110rpx;
|
height: 110rpx;
|
||||||
@@ -90,7 +105,12 @@ page {
|
|||||||
.nickname-row {
|
.nickname-row {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
gap: 6rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-identity-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nickname-edit-btn {
|
.nickname-edit-btn {
|
||||||
@@ -124,7 +144,7 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-zone {
|
.badge-zone {
|
||||||
padding: 0 24rpx 8rpx;
|
padding: 0 24rpx 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-scroll {
|
.badge-scroll {
|
||||||
@@ -134,19 +154,21 @@ page {
|
|||||||
|
|
||||||
.badge-tag-wrap {
|
.badge-tag-wrap {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transform: scale(0.72);
|
transform: scale(0.65);
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
margin-right: -8rpx;
|
margin-right: -12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity-tags-row {
|
.identity-tags-row {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4rpx;
|
gap: 2rpx;
|
||||||
margin-top: 8rpx;
|
margin-top: 6rpx;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity-tags-row .badge-tag-wrap {
|
.identity-tags-row .badge-tag-wrap {
|
||||||
transform: scale(0.68);
|
transform: scale(0.62);
|
||||||
|
margin-right: -14rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-btns .sj-btn-outline {
|
.wallet-btns .sj-btn-outline {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* - 统一 IM 连接等待与 history API 封装
|
* - 统一 IM 连接等待与 history API 封装
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-)/;
|
const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-|auto-reply-)/;
|
||||||
|
|
||||||
export function isHistorySyntheticMessage(msg) {
|
export function isHistorySyntheticMessage(msg) {
|
||||||
return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId)));
|
return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId)));
|
||||||
@@ -22,6 +22,21 @@ export function getOldestHistoryTimestamp(messages, fallback = null) {
|
|||||||
return oldest != null ? oldest : fallback;
|
return oldest != null ? oldest : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 等待 IM 连接就绪(优先 ensureImForRole,避免未连上就拉历史) */
|
||||||
|
export async function waitChatImReady(app, timeoutMs = 12000) {
|
||||||
|
const role = app?.globalData?.currentRole || 'normal';
|
||||||
|
if (app?.ensureImForRole) {
|
||||||
|
try {
|
||||||
|
await app.ensureImForRole(role);
|
||||||
|
if (isImConnected()) return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[IM] ensureImForRole failed', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (app?.ensureConnection) app.ensureConnection();
|
||||||
|
return waitImConnected(app, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
export function isImConnected() {
|
export function isImConnected() {
|
||||||
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
|
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
|
||||||
return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im;
|
return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im;
|
||||||
|
|||||||
127
utils/scriptService.js
Normal file
127
utils/scriptService.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// utils/scriptService.js — 客服话术拉取与自动回复匹配
|
||||||
|
import request from './request.js';
|
||||||
|
|
||||||
|
const DEFAULT_SCRIPTS = {
|
||||||
|
chat_image_confirm: {
|
||||||
|
title: '温馨提示',
|
||||||
|
content: '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
|
||||||
|
confirm_text: '确认',
|
||||||
|
cancel_text: '取消',
|
||||||
|
},
|
||||||
|
cs_welcome: {
|
||||||
|
content: '你好,请问有什么可以帮到您的?',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const textCache = {};
|
||||||
|
const TEXT_CACHE_TTL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function fitModalBtn(text, fallback) {
|
||||||
|
const t = (text || fallback || '确定').trim();
|
||||||
|
if (t && t.length <= 4) return t;
|
||||||
|
return fallback || '确定';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConfirmModal(script, onConfirm, onCancel) {
|
||||||
|
wx.showModal({
|
||||||
|
title: ((script.title || '提示').trim()).slice(0, 32),
|
||||||
|
content: script.content || '',
|
||||||
|
confirmText: fitModalBtn(script.confirm_text, '确认'),
|
||||||
|
cancelText: fitModalBtn(script.cancel_text, '取消'),
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) onConfirm && onConfirm();
|
||||||
|
else onCancel && onCancel();
|
||||||
|
},
|
||||||
|
fail: () => onCancel && onCancel(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeScript(sceneKey, remote) {
|
||||||
|
const base = DEFAULT_SCRIPTS[sceneKey] || {};
|
||||||
|
if (!remote) return { ...base };
|
||||||
|
return { ...base, ...remote };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchScripts(sceneKeys) {
|
||||||
|
if (!sceneKeys || !sceneKeys.length) return {};
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = {};
|
||||||
|
const needFetch = [];
|
||||||
|
|
||||||
|
sceneKeys.forEach((key) => {
|
||||||
|
const entry = textCache[key];
|
||||||
|
if (entry && now - entry.time < TEXT_CACHE_TTL) {
|
||||||
|
cached[key] = entry.data;
|
||||||
|
} else {
|
||||||
|
needFetch.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needFetch.length) {
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/peizhi/huashuhq',
|
||||||
|
method: 'POST',
|
||||||
|
data: { scene_keys: needFetch },
|
||||||
|
});
|
||||||
|
const payload = (res && res.data && res.data.code === 200) ? res.data.data || {} : {};
|
||||||
|
needFetch.forEach((key) => {
|
||||||
|
textCache[key] = { time: now, data: payload[key] };
|
||||||
|
cached[key] = payload[key];
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('fetchScripts failed', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
sceneKeys.forEach((key) => {
|
||||||
|
result[key] = mergeScript(key, cached[key]);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showConfirmByScene(sceneKey, onConfirm, onCancel) {
|
||||||
|
try {
|
||||||
|
const data = await fetchScripts([sceneKey]);
|
||||||
|
openConfirmModal(data[sceneKey] || mergeScript(sceneKey, null), onConfirm, onCancel);
|
||||||
|
} catch (e) {
|
||||||
|
openConfirmModal(mergeScript(sceneKey, null), onConfirm, onCancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function matchAutoReply(userText) {
|
||||||
|
if (!userText || !userText.trim()) {
|
||||||
|
return { matched: false, content: '', reply_type: 'text' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/peizhi/huashu_match',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
scene_key: 'cs_auto_reply',
|
||||||
|
user_text: userText.trim(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (res && res.data && res.data.code === 200 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('matchAutoReply failed', e);
|
||||||
|
}
|
||||||
|
return { matched: false, content: '', reply_type: 'text' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWelcomeText() {
|
||||||
|
const data = await fetchScripts(['cs_welcome']);
|
||||||
|
const script = data.cs_welcome || DEFAULT_SCRIPTS.cs_welcome;
|
||||||
|
return script.content || DEFAULT_SCRIPTS.cs_welcome.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchScripts,
|
||||||
|
showConfirmByScene,
|
||||||
|
matchAutoReply,
|
||||||
|
getWelcomeText,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user