feat(ai): PR-7 前端 ai-service 按 agent_id 路由 endpoint
背景:
- 后端 aikefu 和 ai-agent 两个独立 addon (PR-6 v2.0)
- 前端 ai-chat 之前所有 API endpoint 都 hardcode 到 /api/kefu/* + /ws/aikefu
- 5 个 agent (智能客服默认 + 4 个 AI智能体) 实际上无法被前端独立路由
改动:
- 新增 resolveAddonName(agentId) -> 'aikefu' / 'aiagent'
- 新增 resolveApiUrl(apiName) -> '/api/kefu/X' / '/api/aiagent/X'
- 新增 resolveWsPath() -> '/ws/aikefu' / '/ws/aiagent'
- 全量替换 sendMessage / sendHttpStream / sendStreamMessage / generateConversationId /
getServiceStatus / getServiceConfig / clearConversation / getConversationHistory /
chatWithEventSource / WebSocket connect URL:
全部用 resolveApiUrl / resolveWsPath 动态计算
- setAgentId 跨 addon 切换时自动 disconnectWebSocket (路径变了)
- 新增 getAddonName() / getApiPrefix() 测试辅助方法
后端配套 PR-8+9:
- /api/aiagent/api/Chat/chat (HTTP, 含流式 SSE)
- /api/aiagent/api/Chat/{info,health,getHistory,clearConversation,createConversation}
- /ws/aiagent WebSocket 端点 (从 aikefu WebSocket 复制改名)
路由规则:
- agent_aikefu -> aikefu addon (老路径, 默认智能客服)
- 其他 agent (knowledge / quick_reply / sales / hr) -> aiagent addon (新路径)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,52 @@ let currentConversationId = null;
|
|||||||
// ====== AI Agent Generalization (customer service is the first agent) ======
|
// ====== AI Agent Generalization (customer service is the first agent) ======
|
||||||
// setAgentId updates currentAgentId; default is agent_aikefu.
|
// setAgentId updates currentAgentId; default is agent_aikefu.
|
||||||
// Any sendMessage / sendStreamMessage / getServiceConfig call passes agent_id automatically.
|
// 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';
|
export const DEFAULT_AGENT_ID = 'agent_aikefu';
|
||||||
let currentAgentId = DEFAULT_AGENT_ID;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取域名配置参数(避免重复代码)
|
* 获取域名配置参数(避免重复代码)
|
||||||
*/
|
*/
|
||||||
@@ -42,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 {
|
||||||
@@ -112,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: {} });
|
||||||
@@ -255,20 +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(),
|
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()
|
||||||
@@ -291,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(),
|
||||||
@@ -307,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) {
|
||||||
@@ -315,7 +371,6 @@ export default {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前消息回调
|
|
||||||
wsMessageContent = '';
|
wsMessageContent = '';
|
||||||
wsMessageActive = true;
|
wsMessageActive = true;
|
||||||
wsMessageOnChunk = onChunk || null;
|
wsMessageOnChunk = onChunk || null;
|
||||||
@@ -339,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: {
|
||||||
@@ -358,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()
|
||||||
@@ -380,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();
|
||||||
@@ -393,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;
|
||||||
@@ -416,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);
|
||||||
@@ -503,10 +558,11 @@ 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(),
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
@@ -528,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: {}
|
||||||
});
|
});
|
||||||
@@ -552,11 +609,12 @@ 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(),
|
agent_id: getAgentId(),
|
||||||
uniacid: this.getUniacid(),
|
uniacid: this.getUniacid(),
|
||||||
@@ -568,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,
|
||||||
@@ -583,11 +641,12 @@ 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(),
|
agent_id: getAgentId(),
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
@@ -604,6 +663,7 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话历史
|
* 获取会话历史
|
||||||
|
* PR-7: URL 按 agent_id 路由
|
||||||
*/
|
*/
|
||||||
async getConversationHistory(params = {}) {
|
async getConversationHistory(params = {}) {
|
||||||
try {
|
try {
|
||||||
@@ -623,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'
|
||||||
@@ -653,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
|
||||||
@@ -671,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;
|
||||||
@@ -716,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 });
|
||||||
});
|
});
|
||||||
@@ -743,10 +804,16 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set current AI Agent (default aikefu; link system passes agent_id).
|
* Set current AI Agent (default aikefu; link system passes agent_id).
|
||||||
|
* PR-7: 切换 agent 时, 如果跨 addon 则断开 WebSocket 重连 (路径变了)
|
||||||
* @param {string} agentId
|
* @param {string} agentId
|
||||||
*/
|
*/
|
||||||
setAgentId(agentId) {
|
setAgentId(agentId) {
|
||||||
|
var oldAddon = resolveAddonName(currentAgentId);
|
||||||
setAgentId(agentId);
|
setAgentId(agentId);
|
||||||
|
var newAddon = resolveAddonName(currentAgentId);
|
||||||
|
if (oldAddon !== newAddon) {
|
||||||
|
try { disconnectWebSocket(); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -756,4 +823,4 @@ export default {
|
|||||||
getAgentId() {
|
getAgentId() {
|
||||||
return getAgentId();
|
return getAgentId();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user