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'
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
const memberId = store.state.memberInfo?.id;
|
||||
if (memberId) return memberId;
|
||||
|
||||
|
||||
let guestId;
|
||||
try {
|
||||
guestId = uni.getStorageSync(GUEST_ID_KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
|
||||
if (!guestId) {
|
||||
guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
|
||||
try {
|
||||
@@ -50,6 +98,20 @@ function getUserId() {
|
||||
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
|
||||
// ================================
|
||||
// WebSocket 长连接管理(微信小程序)
|
||||
@@ -91,7 +153,9 @@ function ensureWebSocketConnected() {
|
||||
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;
|
||||
wsAuthenticated = false;
|
||||
wsSocketTask = wx.connectSocket({ url: wssUrl, header: {} });
|
||||
@@ -106,6 +170,7 @@ function ensureWebSocketConnected() {
|
||||
wsSocketTask.send({
|
||||
data: JSON.stringify({
|
||||
action: 'auth',
|
||||
agent_id: getAgentId(),
|
||||
uniacid: _uniacid,
|
||||
token: _token,
|
||||
user_id: _userId
|
||||
@@ -233,19 +298,35 @@ export default {
|
||||
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 模式)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async sendMessage(message, options = {}) {
|
||||
try {
|
||||
const params = {
|
||||
url: '/api/kefu/chat',
|
||||
url: resolveApiUrl('chat'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
user_id: getUserId(),
|
||||
stream: false,
|
||||
inputs: {},
|
||||
query: message,
|
||||
response_mode: 'blocking', // 强制 blocking
|
||||
response_mode: 'blocking',
|
||||
user: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
...getDomainParams()
|
||||
@@ -268,7 +349,6 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('[AI Service] AiChat API请求失败,详细错误:', error);
|
||||
console.error('[AI Service] 错误堆栈:', error.stack);
|
||||
// 打印更多信息帮助调试
|
||||
console.error('[AI Service] store 状态:', {
|
||||
memberInfo: store.state.memberInfo,
|
||||
uniacid: this.getUniacid(),
|
||||
@@ -284,7 +364,6 @@ export default {
|
||||
*/
|
||||
async sendStreamMessage(message, onChunk, onComplete) {
|
||||
// #ifdef MP
|
||||
// 先确保连接已就绪
|
||||
try {
|
||||
await ensureWebSocketConnected();
|
||||
} catch (e) {
|
||||
@@ -292,7 +371,6 @@ export default {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 设置当前消息回调
|
||||
wsMessageContent = '';
|
||||
wsMessageActive = true;
|
||||
wsMessageOnChunk = onChunk || null;
|
||||
@@ -305,6 +383,7 @@ export default {
|
||||
wsSocketTask.send({
|
||||
data: JSON.stringify({
|
||||
action: 'chat',
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
query: message,
|
||||
user_id: getUserId(),
|
||||
@@ -315,13 +394,16 @@ export default {
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP 流式请求(仅 H5 使用)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async sendHttpStream(message, onChunk, onComplete) {
|
||||
// #ifdef H5
|
||||
try {
|
||||
const url = Config.baseUrl + `/api/kefu/chat`;
|
||||
var chatUrl = resolveApiUrl('chat');
|
||||
var url = Config.baseUrl + chatUrl;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -334,6 +416,7 @@ export default {
|
||||
inputs: {},
|
||||
query: message,
|
||||
response_mode: 'streaming',
|
||||
agent_id: getAgentId(),
|
||||
user_id: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
...getDomainParams()
|
||||
@@ -356,9 +439,8 @@ export default {
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// 按行分割,保留不完整的最后一行
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 未完成的行留到下次
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
@@ -369,14 +451,12 @@ export default {
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.event === 'message') {
|
||||
var text = data.answer || data.text || '';
|
||||
// 兼容嵌套 data.data 格式
|
||||
if (!text && data.data) {
|
||||
text = data.data.content || data.data.answer || '';
|
||||
}
|
||||
content += text;
|
||||
if (onChunk) onChunk(text);
|
||||
}
|
||||
// 兼容两种层级:conversation_id 可能在顶层或 data.data 中
|
||||
var cid = data.conversation_id || (data.data && data.data.conversation_id) || '';
|
||||
if (cid) {
|
||||
conversationId = cid;
|
||||
@@ -392,7 +472,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后残留的 buffer(如果有)
|
||||
if (buffer.trim().startsWith('data:')) {
|
||||
try {
|
||||
const jsonStr = buffer.trim().slice(5);
|
||||
@@ -479,11 +558,13 @@ export default {
|
||||
|
||||
/**
|
||||
* 生成新会话(如需)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async generateConversationId() {
|
||||
const params = {
|
||||
url: '/api/kefu/createConversation',
|
||||
url: resolveApiUrl('createConversation'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
token: store.state.token || 'test_token',
|
||||
user_id: getUserId(),
|
||||
@@ -503,11 +584,12 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取服务状态
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getServiceStatus() {
|
||||
try {
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/health',
|
||||
url: resolveApiUrl('health'),
|
||||
async: false,
|
||||
data: {}
|
||||
});
|
||||
@@ -527,12 +609,14 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取服务配置信息
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getServiceConfig() {
|
||||
try {
|
||||
const params = {
|
||||
url: '/api/kefu/info',
|
||||
url: resolveApiUrl('info'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
token: store.state.token || 'test_token',
|
||||
user_id: getUserId(),
|
||||
@@ -542,7 +626,7 @@ export default {
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
};
|
||||
const response = await http.sendRequest({
|
||||
...params,
|
||||
async: false,
|
||||
@@ -557,12 +641,14 @@ export default {
|
||||
|
||||
/**
|
||||
* 清除会话历史(调用后端接口)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async clearConversation(conversationId) {
|
||||
try {
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/clear-conversation',
|
||||
url: resolveApiUrl('clear-conversation'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
conversation_id: conversationId,
|
||||
...getDomainParams()
|
||||
},
|
||||
@@ -577,6 +663,7 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getConversationHistory(params = {}) {
|
||||
try {
|
||||
@@ -585,6 +672,7 @@ export default {
|
||||
}
|
||||
console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit);
|
||||
const requestData = {
|
||||
agent_id: params.agent_id || getAgentId(),
|
||||
uniacid: params.uniacid || this.getUniacid(),
|
||||
conversation_id: params.conversation_id,
|
||||
user_id: params.user_id || getUserId(),
|
||||
@@ -595,7 +683,7 @@ export default {
|
||||
...getDomainParams()
|
||||
};
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/getHistory',
|
||||
url: resolveApiUrl('getHistory'),
|
||||
data: requestData,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -625,6 +713,7 @@ export default {
|
||||
|
||||
/**
|
||||
* [H5 Only] EventSource 流式聊天(备用)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
// #ifdef H5
|
||||
@@ -632,6 +721,7 @@ export default {
|
||||
window.currentEventSource.close();
|
||||
}
|
||||
const paramsObj = {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
user_id: getUserId(),
|
||||
member_id: getUserId(),
|
||||
@@ -642,7 +732,8 @@ export default {
|
||||
...getDomainParams()
|
||||
};
|
||||
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 {
|
||||
const eventSource = new EventSource(url);
|
||||
window.currentEventSource = eventSource;
|
||||
@@ -687,7 +778,6 @@ export default {
|
||||
*/
|
||||
async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
// #ifdef H5
|
||||
// 实际逻辑已在 sendHttpStream 中实现,此处可复用或留空
|
||||
return this.sendHttpStream(message, onMessage, (res) => {
|
||||
onComplete?.({ message: res.content, conversation_id: res.conversation_id });
|
||||
});
|
||||
@@ -711,4 +801,26 @@ export default {
|
||||
// H5 无 WebSocket 长连接,无需操作
|
||||
},
|
||||
// #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 Platform from './platform.js';
|
||||
import PAGE_KEY_MAP from './util.js';
|
||||
|
||||
export class CustomerService {
|
||||
constructor(vueInstance, externalConfig = null) {
|
||||
@@ -118,6 +119,16 @@ export class CustomerService {
|
||||
|
||||
/**
|
||||
* 跳转到 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() {
|
||||
try {
|
||||
@@ -127,7 +138,7 @@ export class CustomerService {
|
||||
}
|
||||
|
||||
// ✅ 修正路径:必须与 pages.json 中注册的路径一致
|
||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
||||
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||
|
||||
// ✅ 使用 navigateTo 保留返回栈(体验更好)
|
||||
uni.navigateTo({
|
||||
@@ -551,6 +562,9 @@ export class CustomerService {
|
||||
|
||||
/**
|
||||
* 打开 AI 智能助手
|
||||
*
|
||||
* 注意:不在此处注入 __dyu=FloatBtn,理由同 openAiChatService()。
|
||||
* 由调用方(hover-nav.vue 等)按需注入。
|
||||
*/
|
||||
openAIChat() {
|
||||
try {
|
||||
@@ -558,7 +572,7 @@ export class CustomerService {
|
||||
this.vm.setAiUnreadCount(0);
|
||||
}
|
||||
|
||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
||||
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||
|
||||
uni.navigateTo({
|
||||
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 LOGIN_PAGE_URL = '/pages_tool/login/login';
|
||||
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 到实际路径的映射表(统一管理所有页面的语义常量 → 实际路径)
|
||||
// 前端组织页面(重命名路径、分包调整)时只需要修改此映射表
|
||||
@@ -1203,6 +1204,23 @@ export default {
|
||||
* @param {Object} 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));
|
||||
//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
Reference in New Issue
Block a user