Merge branch 'dev/1.0' of http://git.aigc-quickapp.com/Uniapp/lucky_shop into temp2797
This commit is contained in:
@@ -4,8 +4,56 @@ import store from '@/store/index.js'
|
|||||||
import lang from './lang.js'
|
import lang from './lang.js'
|
||||||
|
|
||||||
let currentConversationId = null;
|
let currentConversationId = null;
|
||||||
|
|
||||||
|
// ====== AI Agent Generalization (customer service is the first agent) ======
|
||||||
|
// setAgentId updates currentAgentId; default is agent_aikefu.
|
||||||
|
// Any sendMessage / sendStreamMessage / getServiceConfig call passes agent_id automatically.
|
||||||
|
//
|
||||||
|
// PR-7: 按 agent_id 路由 endpoint
|
||||||
|
// - agent_aikefu (默认智能客服) -> 走 aikefu addon (/api/kefu/* + /ws/aikefu)
|
||||||
|
// - 其他 agent (knowledge / quick_reply / sales / hr) -> 走 ai-agent addon (/api/aiagent/* + /ws/aiagent)
|
||||||
|
// - 前端不关心后端两个 addon 之间的差异,只关心 agent_id 路由
|
||||||
|
|
||||||
|
export const DEFAULT_AGENT_ID = 'agent_aikefu';
|
||||||
|
let currentAgentId = DEFAULT_AGENT_ID;
|
||||||
export const CONVERSATION_KEY = 'ai_conversation_id';
|
export const CONVERSATION_KEY = 'ai_conversation_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR-7: agent_id -> 后端 addon 路由映射
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - agent_aikefu (默认智能客服) -> aikefu addon (老路径 /api/kefu/*)
|
||||||
|
* - 其他 agent -> aiagent addon (新路径 /api/aiagent/*)
|
||||||
|
*
|
||||||
|
* 后端两个 addon 完全独立:
|
||||||
|
* - aikefu: lucky_aikefu_config / conversation / message
|
||||||
|
* - aiagent: lucky_ai_agent_config / conversation / message
|
||||||
|
*/
|
||||||
|
function resolveAddonName(agentId) {
|
||||||
|
if (!agentId || agentId === 'agent_aikefu') return 'aikefu';
|
||||||
|
return 'aiagent';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR-7: agent_id -> API endpoint URL
|
||||||
|
* agent_aikefu -> '/api/kefu/...' (兼容老路径)
|
||||||
|
* 其他 -> '/api/aiagent/...' (ai-agent 新路径)
|
||||||
|
*/
|
||||||
|
function resolveApiUrl(apiName) {
|
||||||
|
var addon = resolveAddonName(currentAgentId);
|
||||||
|
if (addon === 'aikefu') {
|
||||||
|
return '/api/kefu/' + apiName;
|
||||||
|
}
|
||||||
|
return '/api/aiagent/' + apiName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR-7: WebSocket URL 路径(/ws/{addonName})
|
||||||
|
*/
|
||||||
|
function resolveWsPath() {
|
||||||
|
return '/ws/' + resolveAddonName(currentAgentId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取域名配置参数(避免重复代码)
|
* 获取域名配置参数(避免重复代码)
|
||||||
*/
|
*/
|
||||||
@@ -35,12 +83,12 @@ const GUEST_ID_KEY = 'ai_guest_user_id';
|
|||||||
function getUserId() {
|
function getUserId() {
|
||||||
const memberId = store.state.memberInfo?.id;
|
const memberId = store.state.memberInfo?.id;
|
||||||
if (memberId) return memberId;
|
if (memberId) return memberId;
|
||||||
|
|
||||||
let guestId;
|
let guestId;
|
||||||
try {
|
try {
|
||||||
guestId = uni.getStorageSync(GUEST_ID_KEY);
|
guestId = uni.getStorageSync(GUEST_ID_KEY);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
|
|
||||||
if (!guestId) {
|
if (!guestId) {
|
||||||
guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
|
guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +98,20 @@ function getUserId() {
|
|||||||
return guestId;
|
return guestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 AI Agent。
|
||||||
|
*/
|
||||||
|
function getAgentId() {
|
||||||
|
return currentAgentId || DEFAULT_AGENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前 AI Agent。
|
||||||
|
*/
|
||||||
|
function setAgentId(agentId) {
|
||||||
|
if (agentId && typeof agentId === 'string') currentAgentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
// #ifdef MP
|
// #ifdef MP
|
||||||
// ================================
|
// ================================
|
||||||
// WebSocket 长连接管理(微信小程序)
|
// WebSocket 长连接管理(微信小程序)
|
||||||
@@ -91,7 +153,9 @@ function ensureWebSocketConnected() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var wssUrl = 'wss://' + domain + '/ws/aikefu';
|
// PR-7: WebSocket 路径按当前 agent_id 路由 — agent_aikefu 走 /ws/aikefu,其他走 /ws/aiagent
|
||||||
|
var wsPath = resolveWsPath();
|
||||||
|
var wssUrl = 'wss://' + domain + wsPath;
|
||||||
wsConnected = false;
|
wsConnected = false;
|
||||||
wsAuthenticated = false;
|
wsAuthenticated = false;
|
||||||
wsSocketTask = wx.connectSocket({ url: wssUrl, header: {} });
|
wsSocketTask = wx.connectSocket({ url: wssUrl, header: {} });
|
||||||
@@ -106,6 +170,7 @@ function ensureWebSocketConnected() {
|
|||||||
wsSocketTask.send({
|
wsSocketTask.send({
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
action: 'auth',
|
action: 'auth',
|
||||||
|
agent_id: getAgentId(),
|
||||||
uniacid: _uniacid,
|
uniacid: _uniacid,
|
||||||
token: _token,
|
token: _token,
|
||||||
user_id: _userId
|
user_id: _userId
|
||||||
@@ -233,19 +298,35 @@ export default {
|
|||||||
return uni.getStorageSync('uniacid') || Config.uniacid; // 从缓存中获取uniacid,或使用默认uniacid
|
return uni.getStorageSync('uniacid') || Config.uniacid; // 从缓存中获取uniacid,或使用默认uniacid
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR-7: 获取当前 agent 对应的 addon 名称 (aikefu / aiagent)
|
||||||
|
*/
|
||||||
|
getAddonName() {
|
||||||
|
return resolveAddonName(currentAgentId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR-7: 获取当前 agent 对应的 API endpoint 前缀
|
||||||
|
*/
|
||||||
|
getApiPrefix() {
|
||||||
|
return resolveAddonName(currentAgentId) === 'aikefu' ? '/api/kefu' : '/api/aiagent';
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送普通消息(blocking 模式)
|
* 发送普通消息(blocking 模式)
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async sendMessage(message, options = {}) {
|
async sendMessage(message, options = {}) {
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
url: '/api/kefu/chat',
|
url: resolveApiUrl('chat'),
|
||||||
data: {
|
data: {
|
||||||
|
agent_id: getAgentId(),
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
stream: false,
|
stream: false,
|
||||||
inputs: {},
|
inputs: {},
|
||||||
query: message,
|
query: message,
|
||||||
response_mode: 'blocking', // 强制 blocking
|
response_mode: 'blocking',
|
||||||
user: getUserId(),
|
user: getUserId(),
|
||||||
conversation_id: this.getConversationId() || '',
|
conversation_id: this.getConversationId() || '',
|
||||||
...getDomainParams()
|
...getDomainParams()
|
||||||
@@ -268,7 +349,6 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AI Service] AiChat API请求失败,详细错误:', error);
|
console.error('[AI Service] AiChat API请求失败,详细错误:', error);
|
||||||
console.error('[AI Service] 错误堆栈:', error.stack);
|
console.error('[AI Service] 错误堆栈:', error.stack);
|
||||||
// 打印更多信息帮助调试
|
|
||||||
console.error('[AI Service] store 状态:', {
|
console.error('[AI Service] store 状态:', {
|
||||||
memberInfo: store.state.memberInfo,
|
memberInfo: store.state.memberInfo,
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
@@ -284,7 +364,6 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async sendStreamMessage(message, onChunk, onComplete) {
|
async sendStreamMessage(message, onChunk, onComplete) {
|
||||||
// #ifdef MP
|
// #ifdef MP
|
||||||
// 先确保连接已就绪
|
|
||||||
try {
|
try {
|
||||||
await ensureWebSocketConnected();
|
await ensureWebSocketConnected();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -292,7 +371,6 @@ export default {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前消息回调
|
|
||||||
wsMessageContent = '';
|
wsMessageContent = '';
|
||||||
wsMessageActive = true;
|
wsMessageActive = true;
|
||||||
wsMessageOnChunk = onChunk || null;
|
wsMessageOnChunk = onChunk || null;
|
||||||
@@ -305,6 +383,7 @@ export default {
|
|||||||
wsSocketTask.send({
|
wsSocketTask.send({
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
action: 'chat',
|
action: 'chat',
|
||||||
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
query: message,
|
query: message,
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
@@ -315,13 +394,16 @@ export default {
|
|||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 流式请求(仅 H5 使用)
|
* HTTP 流式请求(仅 H5 使用)
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async sendHttpStream(message, onChunk, onComplete) {
|
async sendHttpStream(message, onChunk, onComplete) {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
try {
|
try {
|
||||||
const url = Config.baseUrl + `/api/kefu/chat`;
|
var chatUrl = resolveApiUrl('chat');
|
||||||
|
var url = Config.baseUrl + chatUrl;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -334,6 +416,7 @@ export default {
|
|||||||
inputs: {},
|
inputs: {},
|
||||||
query: message,
|
query: message,
|
||||||
response_mode: 'streaming',
|
response_mode: 'streaming',
|
||||||
|
agent_id: getAgentId(),
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
conversation_id: this.getConversationId() || '',
|
conversation_id: this.getConversationId() || '',
|
||||||
...getDomainParams()
|
...getDomainParams()
|
||||||
@@ -356,9 +439,8 @@ export default {
|
|||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
// 按行分割,保留不完整的最后一行
|
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split('\n');
|
||||||
buffer = lines.pop() || ''; // 未完成的行留到下次
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -369,14 +451,12 @@ export default {
|
|||||||
const data = JSON.parse(jsonStr);
|
const data = JSON.parse(jsonStr);
|
||||||
if (data.event === 'message') {
|
if (data.event === 'message') {
|
||||||
var text = data.answer || data.text || '';
|
var text = data.answer || data.text || '';
|
||||||
// 兼容嵌套 data.data 格式
|
|
||||||
if (!text && data.data) {
|
if (!text && data.data) {
|
||||||
text = data.data.content || data.data.answer || '';
|
text = data.data.content || data.data.answer || '';
|
||||||
}
|
}
|
||||||
content += text;
|
content += text;
|
||||||
if (onChunk) onChunk(text);
|
if (onChunk) onChunk(text);
|
||||||
}
|
}
|
||||||
// 兼容两种层级:conversation_id 可能在顶层或 data.data 中
|
|
||||||
var cid = data.conversation_id || (data.data && data.data.conversation_id) || '';
|
var cid = data.conversation_id || (data.data && data.data.conversation_id) || '';
|
||||||
if (cid) {
|
if (cid) {
|
||||||
conversationId = cid;
|
conversationId = cid;
|
||||||
@@ -392,7 +472,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理最后残留的 buffer(如果有)
|
|
||||||
if (buffer.trim().startsWith('data:')) {
|
if (buffer.trim().startsWith('data:')) {
|
||||||
try {
|
try {
|
||||||
const jsonStr = buffer.trim().slice(5);
|
const jsonStr = buffer.trim().slice(5);
|
||||||
@@ -479,11 +558,13 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成新会话(如需)
|
* 生成新会话(如需)
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async generateConversationId() {
|
async generateConversationId() {
|
||||||
const params = {
|
const params = {
|
||||||
url: '/api/kefu/createConversation',
|
url: resolveApiUrl('createConversation'),
|
||||||
data: {
|
data: {
|
||||||
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
token: store.state.token || 'test_token',
|
token: store.state.token || 'test_token',
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
@@ -503,11 +584,12 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取服务状态
|
* 获取服务状态
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async getServiceStatus() {
|
async getServiceStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await http.sendRequest({
|
const response = await http.sendRequest({
|
||||||
url: '/api/kefu/health',
|
url: resolveApiUrl('health'),
|
||||||
async: false,
|
async: false,
|
||||||
data: {}
|
data: {}
|
||||||
});
|
});
|
||||||
@@ -527,12 +609,14 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取服务配置信息
|
* 获取服务配置信息
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async getServiceConfig() {
|
async getServiceConfig() {
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
url: '/api/kefu/info',
|
url: resolveApiUrl('info'),
|
||||||
data: {
|
data: {
|
||||||
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
token: store.state.token || 'test_token',
|
token: store.state.token || 'test_token',
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
@@ -542,7 +626,7 @@ export default {
|
|||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const response = await http.sendRequest({
|
const response = await http.sendRequest({
|
||||||
...params,
|
...params,
|
||||||
async: false,
|
async: false,
|
||||||
@@ -557,12 +641,14 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除会话历史(调用后端接口)
|
* 清除会话历史(调用后端接口)
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async clearConversation(conversationId) {
|
async clearConversation(conversationId) {
|
||||||
try {
|
try {
|
||||||
const response = await http.sendRequest({
|
const response = await http.sendRequest({
|
||||||
url: '/api/kefu/clear-conversation',
|
url: resolveApiUrl('clear-conversation'),
|
||||||
data: {
|
data: {
|
||||||
|
agent_id: getAgentId(),
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
...getDomainParams()
|
...getDomainParams()
|
||||||
},
|
},
|
||||||
@@ -577,6 +663,7 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话历史
|
* 获取会话历史
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async getConversationHistory(params = {}) {
|
async getConversationHistory(params = {}) {
|
||||||
try {
|
try {
|
||||||
@@ -585,6 +672,7 @@ export default {
|
|||||||
}
|
}
|
||||||
console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit);
|
console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit);
|
||||||
const requestData = {
|
const requestData = {
|
||||||
|
agent_id: params.agent_id || getAgentId(),
|
||||||
uniacid: params.uniacid || this.getUniacid(),
|
uniacid: params.uniacid || this.getUniacid(),
|
||||||
conversation_id: params.conversation_id,
|
conversation_id: params.conversation_id,
|
||||||
user_id: params.user_id || getUserId(),
|
user_id: params.user_id || getUserId(),
|
||||||
@@ -595,7 +683,7 @@ export default {
|
|||||||
...getDomainParams()
|
...getDomainParams()
|
||||||
};
|
};
|
||||||
const response = await http.sendRequest({
|
const response = await http.sendRequest({
|
||||||
url: '/api/kefu/getHistory',
|
url: resolveApiUrl('getHistory'),
|
||||||
data: requestData,
|
data: requestData,
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -625,6 +713,7 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* [H5 Only] EventSource 流式聊天(备用)
|
* [H5 Only] EventSource 流式聊天(备用)
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
@@ -632,6 +721,7 @@ export default {
|
|||||||
window.currentEventSource.close();
|
window.currentEventSource.close();
|
||||||
}
|
}
|
||||||
const paramsObj = {
|
const paramsObj = {
|
||||||
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
user_id: getUserId(),
|
user_id: getUserId(),
|
||||||
member_id: getUserId(),
|
member_id: getUserId(),
|
||||||
@@ -642,7 +732,8 @@ export default {
|
|||||||
...getDomainParams()
|
...getDomainParams()
|
||||||
};
|
};
|
||||||
const params = new URLSearchParams(paramsObj);
|
const params = new URLSearchParams(paramsObj);
|
||||||
const url = `${Config.baseUrl}/api/kefu/chat?${params.toString()}`;
|
var chatUrl = resolveApiUrl('chat');
|
||||||
|
const url = `${Config.baseUrl}${chatUrl}?${params.toString()}`;
|
||||||
try {
|
try {
|
||||||
const eventSource = new EventSource(url);
|
const eventSource = new EventSource(url);
|
||||||
window.currentEventSource = eventSource;
|
window.currentEventSource = eventSource;
|
||||||
@@ -687,7 +778,6 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
// 实际逻辑已在 sendHttpStream 中实现,此处可复用或留空
|
|
||||||
return this.sendHttpStream(message, onMessage, (res) => {
|
return this.sendHttpStream(message, onMessage, (res) => {
|
||||||
onComplete?.({ message: res.content, conversation_id: res.conversation_id });
|
onComplete?.({ message: res.content, conversation_id: res.conversation_id });
|
||||||
});
|
});
|
||||||
@@ -711,4 +801,26 @@ export default {
|
|||||||
// H5 无 WebSocket 长连接,无需操作
|
// H5 无 WebSocket 长连接,无需操作
|
||||||
},
|
},
|
||||||
// #endif
|
// #endif
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* Set current AI Agent (default aikefu; link system passes agent_id).
|
||||||
|
* PR-7: 切换 agent 时, 如果跨 addon 则断开 WebSocket 重连 (路径变了)
|
||||||
|
* @param {string} agentId
|
||||||
|
*/
|
||||||
|
setAgentId(agentId) {
|
||||||
|
var oldAddon = resolveAddonName(currentAgentId);
|
||||||
|
setAgentId(agentId);
|
||||||
|
var newAddon = resolveAddonName(currentAgentId);
|
||||||
|
if (oldAddon !== newAddon) {
|
||||||
|
try { disconnectWebSocket(); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current AI Agent.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getAgentId() {
|
||||||
|
return getAgentId();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import IMAGE_CONFIG from './image-config.js';
|
import IMAGE_CONFIG from './image-config.js';
|
||||||
import Platform from './platform.js';
|
import Platform from './platform.js';
|
||||||
|
import PAGE_KEY_MAP from './util.js';
|
||||||
|
|
||||||
export class CustomerService {
|
export class CustomerService {
|
||||||
constructor(vueInstance, externalConfig = null) {
|
constructor(vueInstance, externalConfig = null) {
|
||||||
@@ -118,6 +119,16 @@ export class CustomerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转到 AI智能客服页面(AiChat)
|
* 跳转到 AI智能客服页面(AiChat)
|
||||||
|
*
|
||||||
|
* 注意:不在此处注入 __dyu=FloatBtn。
|
||||||
|
* 该标记原本是 diy-float-btn.vue 用来告诉 ai-chat "我是从浮动按钮来的,请隐藏底部 tabBar"。
|
||||||
|
* 此方法是 customer-service.js 的**通用 AI 客服入口**,被多处复用:
|
||||||
|
* - hover-nav(悬浮客服按钮)
|
||||||
|
* - 商品详情页 / 会员中心 / 个人中心 等的"联系客服"按钮
|
||||||
|
* - DIY 浮动按钮(diy-float-btn 内部已自行注入 __dyu=FloatBtn,这里再注入会重复)
|
||||||
|
* 若在此处统一注入,会让所有 AI 客服入口都隐藏底部 tabBar,语义错误且侵入性过强。
|
||||||
|
* hover-nav 需要隐藏 tabBar 的场景,由 hover-nav.vue#handleUnifiedKefuClick
|
||||||
|
* 自行注入 __dyu=FloatBtn(与 diy-float-btn.vue#onFloatBtnClick 一致)。
|
||||||
*/
|
*/
|
||||||
openAiChatService() {
|
openAiChatService() {
|
||||||
try {
|
try {
|
||||||
@@ -127,7 +138,7 @@ export class CustomerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 修正路径:必须与 pages.json 中注册的路径一致
|
// ✅ 修正路径:必须与 pages.json 中注册的路径一致
|
||||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||||
|
|
||||||
// ✅ 使用 navigateTo 保留返回栈(体验更好)
|
// ✅ 使用 navigateTo 保留返回栈(体验更好)
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@@ -551,6 +562,9 @@ export class CustomerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开 AI 智能助手
|
* 打开 AI 智能助手
|
||||||
|
*
|
||||||
|
* 注意:不在此处注入 __dyu=FloatBtn,理由同 openAiChatService()。
|
||||||
|
* 由调用方(hover-nav.vue 等)按需注入。
|
||||||
*/
|
*/
|
||||||
openAIChat() {
|
openAIChat() {
|
||||||
try {
|
try {
|
||||||
@@ -558,7 +572,7 @@ export class CustomerService {
|
|||||||
this.vm.setAiUnreadCount(0);
|
this.vm.setAiUnreadCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: aiChatUrl,
|
url: aiChatUrl,
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export const CONTACT_PAGE_URL = '/pages_tool/contact/contact';
|
|||||||
export const MEMBER_PAGE_URL = '/pages_tool/member/index';
|
export const MEMBER_PAGE_URL = '/pages_tool/member/index';
|
||||||
export const LOGIN_PAGE_URL = '/pages_tool/login/login';
|
export const LOGIN_PAGE_URL = '/pages_tool/login/login';
|
||||||
export const AI_CHAT_PAGE_URL = '/pages_tool/ai-chat/index';
|
export const AI_CHAT_PAGE_URL = '/pages_tool/ai-chat/index';
|
||||||
|
export const AI_AGENT_PAGE_URL = '/pages_tool/ai-agent/index'; // AI 智能体入口页(智能客服是首个 agent)
|
||||||
|
|
||||||
// page_key 到实际路径的映射表(统一管理所有页面的语义常量 → 实际路径)
|
// page_key 到实际路径的映射表(统一管理所有页面的语义常量 → 实际路径)
|
||||||
// 前端组织页面(重命名路径、分包调整)时只需要修改此映射表
|
// 前端组织页面(重命名路径、分包调整)时只需要修改此映射表
|
||||||
@@ -1203,6 +1204,23 @@ export default {
|
|||||||
* @param {Object} link
|
* @param {Object} link
|
||||||
*/
|
*/
|
||||||
diyRedirectTo(link) {
|
diyRedirectTo(link) {
|
||||||
|
|
||||||
|
// ---------- AI Agent 通用链接(智能客服是首个 agent;新增 AI 销售/HR 等不用改这段) ----------
|
||||||
|
// 放在 stale-uniacid 与 resolveLinkUrl 之前:AI Agent 是纯内部路径,走后续 webview 分支会被错判。
|
||||||
|
if (link && (link.name === 'AI_AGENT' || link.kind === 'AI_AGENT')) {
|
||||||
|
const agentId = (link.page_params && link.page_params.agent_id) || 'agent_aikefu';
|
||||||
|
const title = link.title || 'AI 智能体';
|
||||||
|
// 透传来源标记:浮动按钮点击时 __dyu=FloatBtn,目标页据此隐藏底部 tabBar
|
||||||
|
// (diy-float-btn.vue#onFloatBtnClick 已写入 page_params.__dyu)
|
||||||
|
const dyuEntry = (link.page_params && link.page_params.__dyu) || '';
|
||||||
|
return this.redirectTo(AI_AGENT_PAGE_URL, {
|
||||||
|
agent_id: agentId,
|
||||||
|
title: encodeURIComponent(title),
|
||||||
|
enable_set_title: link.enable_set_title ? 1 : 0,
|
||||||
|
...(dyuEntry ? { __dyu: dyuEntry } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`-----> diyRedirectTo`, JSON.stringify(link));
|
console.log(`-----> diyRedirectTo`, JSON.stringify(link));
|
||||||
//if (link == null || Object.keys(link).length == 1) return;
|
//if (link == null || Object.keys(link).length == 1) return;
|
||||||
|
|
||||||
|
|||||||
2090
common/js/util.js.bak
Normal file
2090
common/js/util.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
:class="{ left_top: value.bottomPosition == 1, right_top: value.bottomPosition == 2, left_bottom: value.bottomPosition == 3, right_bottom: value.bottomPosition == 4 }"
|
:class="{ left_top: value.bottomPosition == 1, right_top: value.bottomPosition == 2, left_bottom: value.bottomPosition == 3, right_bottom: value.bottomPosition == 4 }"
|
||||||
:style="style">
|
:style="style">
|
||||||
<block v-for="(item, index) in value.list" :key="index">
|
<block v-for="(item, index) in value.list" :key="index">
|
||||||
<view class="button-box" @tap.stop="$util.diyRedirectTo(item.link)"
|
<view class="button-box" @tap.stop="onFloatBtnClick(item)"
|
||||||
:style="{ width: value.imageSize + 'px', height: value.imageSize + 'px', fontSize: value.imageSize + 'px' }">
|
:style="{ width: value.imageSize + 'px', height: value.imageSize + 'px', fontSize: value.imageSize + 'px' }">
|
||||||
<image v-if="!item.iconType || item.iconType == 'img'" :src="$util.img(item.imageUrl)" mode="aspectFit"
|
<image v-if="!item.iconType || item.iconType == 'img'" :src="$util.img(item.imageUrl)" mode="aspectFit"
|
||||||
show-menu-by-longpress />
|
show-menu-by-longpress />
|
||||||
@@ -44,7 +44,44 @@ export default {
|
|||||||
componentRefresh: function (nval) { }
|
componentRefresh: function (nval) { }
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
methods: {},
|
methods: {
|
||||||
|
/**
|
||||||
|
* 浮动按钮点击处理:
|
||||||
|
* 在跳转前给 link 注入 __dyu=FloatBtn 来源标记,
|
||||||
|
* 让目标页面(尤其是 ai-chat)能识别"来自浮动按钮"从而隐藏底部 tabBar,
|
||||||
|
* 与"从底部导航栏点击进入时显示并高亮 tabBar"的场景并存。
|
||||||
|
*
|
||||||
|
* 为什么用 __dyu 这种 3 字符命名:
|
||||||
|
* - 双下划线 __ 前缀 + 3 位字母(dy-ui 框架) 是前端约定俗成的"系统内部保留字段"标志
|
||||||
|
* (如 __proto__, __esModule),业务方配置 link 时几乎不可能使用这种短命名 → 与业务参数零冲突
|
||||||
|
* - URL 中占用空间极小,不污染业务 query 字符串
|
||||||
|
* - 未来如需扩展其他组件的入口来源,可继续使用 __dyu 前缀(如 __dyu=Banner / __dyu=Nav)
|
||||||
|
*
|
||||||
|
* 实现要点:
|
||||||
|
* - 浅拷贝 item.link 避免污染原数据(可能被 v-for 复用)
|
||||||
|
* - 把标记写入 page_params,diyRedirectTo → redirectTo 会自动拼到 URL query,
|
||||||
|
* 目标页 onLoad(options) 可直接读取
|
||||||
|
* - 同时在 wap_url 末尾追加/合并 __dyu=FloatBtn 兜底:某些路径不走 page_params 拼接
|
||||||
|
*/
|
||||||
|
onFloatBtnClick(item) {
|
||||||
|
if (!item || !item.link) return;
|
||||||
|
const link = { ...item.link };
|
||||||
|
link.page_params = Object.assign({}, link.page_params || {}, {
|
||||||
|
__dyu: 'FloatBtn'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 兜底:直接把标记拼到 wap_url query,防止 page_params 在某些分支未被透传
|
||||||
|
if (link.wap_url && typeof link.wap_url === 'string' && link.wap_url.indexOf('http') === -1) {
|
||||||
|
const sep = link.wap_url.indexOf('?') === -1 ? '?' : '&';
|
||||||
|
// 避免重复追加(精确匹配 __dyu= 而不是 __dyu,避免误伤可能的 __dyu_other)
|
||||||
|
if (link.wap_url.indexOf('__dyu=') === -1) {
|
||||||
|
link.wap_url = link.wap_url + sep + '__dyu=FloatBtn';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$util.diyRedirectTo(link);
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style() {
|
style() {
|
||||||
let style = {},
|
let style = {},
|
||||||
|
|||||||
@@ -743,7 +743,53 @@ export default {
|
|||||||
this.$langConfig.change(targetLang, url);
|
this.$langConfig.change(targetLang, url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服按钮点击的统一入口。
|
||||||
|
*
|
||||||
|
* 关键修复:智能客服(AI 客服)类型需要走 ai-chat 页面,
|
||||||
|
* 且必须在 URL 上注入 __dyu=FloatBtn 标记,
|
||||||
|
* 让 ai-chat/index.vue 的 onLoad(options) 识别"来自悬浮按钮/浮动按钮",
|
||||||
|
* commit setBottomNavHidden(true) 隐藏底部 tabBar。
|
||||||
|
*
|
||||||
|
* 为什么不在 customer-service.js#openAiChatService 统一注入:
|
||||||
|
* 该方法是通用 AI 客服入口,被多处复用(hover-nav / 商品详情页 / 会员中心 / DIY 浮动按钮等),
|
||||||
|
* 其中 DIY 浮动按钮(diy-float-btn.vue)已自行注入 __dyu=FloatBtn;其他入口(商品详情页等)
|
||||||
|
* 是否要隐藏 tabBar 是另一个独立的产品决策,不能在此一刀切。
|
||||||
|
* 因此由调用方按需注入:本方法(hover-nav)显式注入,diy-float-btn 显式注入,
|
||||||
|
* 其他入口保持原样。
|
||||||
|
*
|
||||||
|
* 与 diy-float-btn.vue#onFloatBtnClick 的行为完全一致,只是触发位置不同。
|
||||||
|
*/
|
||||||
handleUnifiedKefuClick() {
|
handleUnifiedKefuClick() {
|
||||||
|
// 仅智能客服(aikefu)类型需要走 ai-chat 页面并隐藏 tabBar;
|
||||||
|
// 其他类型(企业微信 / 第三方 / 小程序原生客服等)由 customerService 内部
|
||||||
|
// 路由到对应渠道(navigateToMiniProgram / openCustomerServiceChat 等),
|
||||||
|
// 与底部 tabBar 无关,继续走原统一入口。
|
||||||
|
if (this.customerService && this.customerService.isAIKefu && this.customerService.isAIKefu()) {
|
||||||
|
// 清除未读数(与 openAiChatService 行为保持一致)
|
||||||
|
if (typeof this.setAiUnreadCount === 'function') {
|
||||||
|
this.setAiUnreadCount(0);
|
||||||
|
} else if (this.$store && typeof this.$store.commit === 'function') {
|
||||||
|
try { this.$store.commit('setAiUnreadCount', 0); } catch (e) { /* 兼容旧 store */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入 __dyu=FloatBtn 标记,与 diy-float-btn.vue#onFloatBtnClick 语义一致
|
||||||
|
const aiChatUrl = `${this.$util.PAGE_KEY_MAP.PAGE_AI_CHAT}?__dyu=FloatBtn`;
|
||||||
|
uni.navigateTo({
|
||||||
|
url: aiChatUrl,
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[hover-nav] 跳转 AI智能客服失败:', err);
|
||||||
|
// H5 兜底
|
||||||
|
// #ifdef H5
|
||||||
|
window.location.href = aiChatUrl;
|
||||||
|
// #endif
|
||||||
|
uni.showToast({ title: this.$lang('customerService.openFailed'), icon: 'none' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非智能客服类型:走原统一入口(企业微信 / 第三方 / 微信原生客服 等)
|
||||||
this.customerService.handleUnifiedKefuClick({
|
this.customerService.handleUnifiedKefuClick({
|
||||||
sendMessageTitle: '来自悬浮按钮的咨询',
|
sendMessageTitle: '来自悬浮按钮的咨询',
|
||||||
sendMessagePath: '/pages/index/index'
|
sendMessagePath: '/pages/index/index'
|
||||||
|
|||||||
@@ -136,4 +136,8 @@ export const lang = {
|
|||||||
logInitNavbarFailed: 'Failed to initialize navigation bar',
|
logInitNavbarFailed: 'Failed to initialize navigation bar',
|
||||||
logChatComponentNotInit: 'Chat component not initialized',
|
logChatComponentNotInit: 'Chat component not initialized',
|
||||||
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...'
|
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...'
|
||||||
}
|
|
||||||
|
// ====== AI Agent general copy (customer service is the first agent) ======
|
||||||
|
aiAgentDefaultTitle: 'AI Agent',
|
||||||
|
aiAgentOpenFailed: 'Failed to open AI Agent, please try again later',
|
||||||
|
aiAgentNotConfigured: 'This AI Agent is not enabled'}
|
||||||
|
|||||||
@@ -136,4 +136,8 @@ export const lang = {
|
|||||||
logInitNavbarFailed: '初始化导航栏失败',
|
logInitNavbarFailed: '初始化导航栏失败',
|
||||||
logChatComponentNotInit: '聊天组件未初始化',
|
logChatComponentNotInit: '聊天组件未初始化',
|
||||||
logServicerConfigNotLoaded: '【AI客服】servicerConfig 未加载,200ms后重试...'
|
logServicerConfigNotLoaded: '【AI客服】servicerConfig 未加载,200ms后重试...'
|
||||||
}
|
|
||||||
|
// ====== AI Agent 通用文案(智能客服是首个 agent) ======
|
||||||
|
aiAgentDefaultTitle: 'AI 智能体',
|
||||||
|
aiAgentOpenFailed: '打开 AI 智能体失败,请稍后再试',
|
||||||
|
aiAgentNotConfigured: '该 AI 智能体暂未启用'}
|
||||||
|
|||||||
59
pages.json
59
pages.json
@@ -1086,7 +1086,64 @@
|
|||||||
{
|
{
|
||||||
"path": "ai-chat/index",
|
"path": "ai-chat/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "AI智能客服"
|
"navigationBarTitleText": "AI智能客服",
|
||||||
|
"componentPlaceholder": {
|
||||||
|
"diy-article": "view",
|
||||||
|
"diy-audio": "view",
|
||||||
|
"diy-bargain": "view",
|
||||||
|
"diy-bottom-nav": "view",
|
||||||
|
"diy-category": "view",
|
||||||
|
"diy-category-item": "view",
|
||||||
|
"diy-channel-list": "view",
|
||||||
|
"diy-channel-video": "view",
|
||||||
|
"diy-comp-extend": "view",
|
||||||
|
"diy-coupon": "view",
|
||||||
|
"diy-digit": "view",
|
||||||
|
"diy-fenxiao-goods-list": "view",
|
||||||
|
"diy-float-btn": "view",
|
||||||
|
"diy-follow-official-account": "view",
|
||||||
|
"diy-form": "view",
|
||||||
|
"diy-goods-brand": "view",
|
||||||
|
"diy-goods-list": "view",
|
||||||
|
"diy-goods-recommend": "view",
|
||||||
|
"diy-graphic-nav": "view",
|
||||||
|
"diy-group": "view",
|
||||||
|
"diy-groupbuy": "view",
|
||||||
|
"diy-horz-blank": "view",
|
||||||
|
"diy-horz-line": "view",
|
||||||
|
"diy-hot-area": "view",
|
||||||
|
"diy-icon": "view",
|
||||||
|
"diy-image-nav": "view",
|
||||||
|
"diy-img-ads": "view",
|
||||||
|
"diy-index-page": "view",
|
||||||
|
"diy-kefu": "view",
|
||||||
|
"diy-listmenu": "view",
|
||||||
|
"diy-live": "view",
|
||||||
|
"diy-many-goods-list": "view",
|
||||||
|
"diy-map": "view",
|
||||||
|
"diy-member-info": "view",
|
||||||
|
"diy-member-my-order": "view",
|
||||||
|
"diy-merch-list": "view",
|
||||||
|
"diy-notes": "view",
|
||||||
|
"diy-notice": "view",
|
||||||
|
"diy-payment-qrcode": "view",
|
||||||
|
"diy-picture": "view",
|
||||||
|
"diy-pinfan": "view",
|
||||||
|
"diy-pintuan": "view",
|
||||||
|
"diy-presale": "view",
|
||||||
|
"diy-quick-nav": "view",
|
||||||
|
"diy-recommend-code": "view",
|
||||||
|
"diy-rich-text": "view",
|
||||||
|
"diy-rubik-cube": "view",
|
||||||
|
"diy-search": "view",
|
||||||
|
"diy-seckill": "view",
|
||||||
|
"diy-store": "view",
|
||||||
|
"diy-store-label": "view",
|
||||||
|
"diy-tab": "view",
|
||||||
|
"diy-text": "view",
|
||||||
|
"diy-video": "view",
|
||||||
|
"diy-video-list": "view"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ export default {
|
|||||||
uni.setStorageSync('selectedProduct', productInfo);
|
uni.setStorageSync('selectedProduct', productInfo);
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages_tool/ai-chat/index'
|
url: this.$util.PAGE_KEY_MAP.PAGE_AI_CHAT
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|
||||||
<!-- 聊天内容区域:从导航栏下方到容器底部 -->
|
<!-- 聊天内容区域:从导航栏下方到容器底部 -->
|
||||||
<view class="chat-content">
|
<view class="chat-content" :style="{ bottom: tabBarHeight }">
|
||||||
<ai-chat-message
|
<ai-chat-message
|
||||||
ref="chat"
|
ref="chat"
|
||||||
:initial-messages="initialMessages"
|
:initial-messages="initialMessages"
|
||||||
@@ -68,6 +68,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部导航栏(仅在 tabBar 场景下显示):
|
||||||
|
通过 golbalConfig mixin 的 shouldShowTabBar 控制 v-show,
|
||||||
|
组件内部已绑定 data-component-name="diy-bottom-nav"。
|
||||||
|
- 后端把 ai-chat 配为底部导航栏项 → 用户点击该项进入 → tabBar 显示并高亮对应项
|
||||||
|
- 浮动按钮(diy-float-btn)进入时 onLoad commit setBottomNavHidden(true)
|
||||||
|
→ shouldShowTabBar=false → tabBar 隐藏
|
||||||
|
- 离开页面(onUnload)还原 bottomNavHidden → 不污染其他页面 -->
|
||||||
|
<diy-bottom-nav></diy-bottom-nav>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -87,6 +96,9 @@ export default {
|
|||||||
return {
|
return {
|
||||||
initialMessages: [],
|
initialMessages: [],
|
||||||
fallbackWelcome: '',
|
fallbackWelcome: '',
|
||||||
|
// AI Agent 通用入参(链接系统透传;老客服入口不传也能跑,默认 agent_aikefu)
|
||||||
|
agentId: 'agent_aikefu',
|
||||||
|
pageTitle: '',
|
||||||
|
|
||||||
// --- 系统导航栏/状态栏相关 ---
|
// --- 系统导航栏/状态栏相关 ---
|
||||||
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||||
@@ -105,7 +117,15 @@ export default {
|
|||||||
keyboardHeight: 0,
|
keyboardHeight: 0,
|
||||||
|
|
||||||
// 事件处理器引用(用于清理)
|
// 事件处理器引用(用于清理)
|
||||||
safeEventHandlers: new Map()
|
safeEventHandlers: new Map(),
|
||||||
|
|
||||||
|
// 是否从浮动按钮进入
|
||||||
|
// 用来在运行时区分两种场景:
|
||||||
|
// 1) 后台把 ai-chat 配为底部导航栏 tabBar 项 → 用户点击该项进入 → 底部 tabBar 显示并高亮
|
||||||
|
// 2) 浮动按钮(diy-float-btn)的链接指向 ai-chat → 用户点击浮动按钮进入 → 底部 tabBar 隐藏
|
||||||
|
// 通过 onLoad(options).from 判定;同时会通过 $store.state.bottomNavHidden 临时隐藏 tabBar,
|
||||||
|
// 并在 onUnload 还原,避免影响其他页面。
|
||||||
|
isFromFloatBtn: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -127,6 +147,21 @@ export default {
|
|||||||
async onLoad(options) {
|
async onLoad(options) {
|
||||||
this.fallbackWelcome = this.$lang('ai-chat.index.fallbackWelcome');
|
this.fallbackWelcome = this.$lang('ai-chat.index.fallbackWelcome');
|
||||||
|
|
||||||
|
// ====== AI Agent 通用入参(链接系统 -> 通用 agent_id/title) ======
|
||||||
|
// 老客服入口不传 options 也能跑(智能客服是首个 agent,默认 agent_aikefu)。
|
||||||
|
// 新增 AI 销售/HR/...智能体只需在 link.page_params.agent_id 传不同值,
|
||||||
|
// 当前 onShow/initChat 不区分 agent 行为,后续按业务扩展按 agentId 路由即可。
|
||||||
|
try {
|
||||||
|
this.agentId = (options && options.agent_id) || 'agent_aikefu';
|
||||||
|
const rawTitle = (options && options.title) || this.$lang('ai-chat.index.title');
|
||||||
|
this.pageTitle = decodeURIComponent(rawTitle);
|
||||||
|
// 同步 agentId 给 ai-service,后续 sendMessage / WS auth 自动带新 agent_id
|
||||||
|
try { aiService.setAgentId(this.agentId); } catch (e) { /* 兼容旧 ai-service 版本 */ }
|
||||||
|
} catch (e) {
|
||||||
|
this.agentId = 'agent_aikefu';
|
||||||
|
this.pageTitle = this.$lang('ai-chat.index.title');
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ 修改:取消权限校验,允许所有客服类型访问此页面
|
// ✅ 修改:取消权限校验,允许所有客服类型访问此页面
|
||||||
// const hasAccess = this.checkAccessPermission();
|
// const hasAccess = this.checkAccessPermission();
|
||||||
// if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
@@ -134,7 +169,27 @@ export default {
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
this.$langConfig.title(this.$lang('ai-chat.index.title'));
|
this.$langConfig.title(this.pageTitle || this.$lang('ai-chat.index.title'));
|
||||||
|
|
||||||
|
// ====== 区分底部导航栏/浮动按钮两种进入来源 ======
|
||||||
|
// diy-float-btn 跳转时会在 link 上注入 __dyu=FloatBtn
|
||||||
|
// (见 diy-float-btn.vue#onFloatBtnClick)
|
||||||
|
// 这种情况应当隐藏底部 tabBar(AI 客服页是全屏聊天体验,不应被 tabBar 占用屏幕)
|
||||||
|
// 而点击底部导航栏 tabBar 项进入时,通常不会携带 __dyu query,
|
||||||
|
// 此时 tabBar 正常显示并由 checkTabBarActive 步骤2精确命中高亮当前项。
|
||||||
|
// 用 __dyu 这种 3 字符 + 双下划线前缀的命名,几乎不可能与业务 page_params 冲突
|
||||||
|
// (业务方不会使用 __ 开头的短变量名)。
|
||||||
|
const dyuEntry = (options && options.__dyu) || '';
|
||||||
|
if (dyuEntry === 'FloatBtn') {
|
||||||
|
this.isFromFloatBtn = true;
|
||||||
|
// 临时隐藏底部导航栏;onUnload 会还原。
|
||||||
|
try {
|
||||||
|
this.$store.commit('setBottomNavHidden', true);
|
||||||
|
} catch (e) { /* 兼容没有该 mutation 的旧 store */ }
|
||||||
|
} else {
|
||||||
|
this.isFromFloatBtn = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.initChat();
|
this.initChat();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -180,6 +235,15 @@ export default {
|
|||||||
console.log(this.$lang('ai-chat.index.logOnUnloadTriggered'));
|
console.log(this.$lang('ai-chat.index.logOnUnloadTriggered'));
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
aiService.disconnectWebSocket();
|
aiService.disconnectWebSocket();
|
||||||
|
|
||||||
|
// 还原底部导航栏显示状态:如果 onLoad 时因为来自浮动按钮而临时隐藏,
|
||||||
|
// 必须在页面卸载时还原,否则返回到 tabBar 页时 tabBar 会保持隐藏状态。
|
||||||
|
if (this.isFromFloatBtn) {
|
||||||
|
try {
|
||||||
|
this.$store.commit('setBottomNavHidden', false);
|
||||||
|
} catch (e) { /* 兼容没有该 mutation 的旧 store */ }
|
||||||
|
this.isFromFloatBtn = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -647,7 +711,9 @@ export default {
|
|||||||
/* #endif */
|
/* #endif */
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
/* bottom 由模板动态绑定到 tabBarHeight:
|
||||||
|
- 浮动按钮入口 / 其他隐藏 tabBar 场景:0rpx,占满到底部
|
||||||
|
- tabBar 项入口:tabBarHeight,避免被底部 tabBar 遮挡聊天内容 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部tabBar占位样式 */
|
/* 底部tabBar占位样式 */
|
||||||
|
|||||||
Reference in New Issue
Block a user