chore:更新 AI 服务会话管理逻辑,优化用户状态切换时的 key 迁移与会话隔离,确保 H5 端 elicitResponse 请求正常处理
This commit is contained in:
@@ -19,12 +19,38 @@ let currentConversationId = null;
|
||||
export const DEFAULT_AGENT_ID = 'agent_aikefu';
|
||||
let currentAgentId = DEFAULT_AGENT_ID;
|
||||
|
||||
// 多端/多 agent 会话隔离: 不同 agent 的 conversation_id 单独存
|
||||
export const CONVERSATION_KEY_PREFIX = 'ai_conversation_id_';
|
||||
// PR-MULTI-TURN-KEY-BINDING (2026-09-18): key 格式变更 + 三维隔离
|
||||
// 老 key: ai_conversation_id_<agentId> (只按 agent 隔离, 跨用户/跨 uniacid 串会话)
|
||||
// 新 key: ai_conv_<uniacid>_<userId>_<agentId> (按 uniacid × userId × agent 三维隔离)
|
||||
// 一次性迁移: 读老 key → 新 key 为空 → 写入新 key 后保留老 key (1 版本后清理)
|
||||
export const CONVERSATION_KEY_PREFIX = 'ai_conv_';
|
||||
const LEGACY_KEY_PREFIX = 'ai_conversation_id_';
|
||||
|
||||
export function getConversationKey(agentId) {
|
||||
return CONVERSATION_KEY_PREFIX + (agentId || DEFAULT_AGENT_ID);
|
||||
let siteId = 0;
|
||||
let userId = 'anon';
|
||||
try {
|
||||
siteId = uni.getStorageSync('uniacid') || Config.uniacid || 0;
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
userId = getUserId() || 'anon';
|
||||
} catch (e) { /* ignore */ }
|
||||
return CONVERSATION_KEY_PREFIX + siteId + '_' + userId + '_' + (agentId || DEFAULT_AGENT_ID);
|
||||
}
|
||||
// 兼容旧代码: 单 key 别名(指向默认 agent)
|
||||
|
||||
// 一次性老 key 迁移: 新 key 为空且老 key 有值 → 写入新 key, 保留老 key 1 版本兜底
|
||||
function migrateLegacyConversationKey(agentId) {
|
||||
try {
|
||||
const legacyKey = LEGACY_KEY_PREFIX + (agentId || DEFAULT_AGENT_ID);
|
||||
const legacy = uni.getStorageSync(legacyKey);
|
||||
const newKey = getConversationKey(agentId);
|
||||
if (legacy && !uni.getStorageSync(newKey)) {
|
||||
uni.setStorageSync(newKey, legacy);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// 兼容旧代码: 单 key 别名(指向默认 agent) — 给老代码引用,值是动态计算的
|
||||
export const CONVERSATION_KEY = getConversationKey(DEFAULT_AGENT_ID);
|
||||
|
||||
// 统一 SSE/WS 事件分发器 (透出所有事件类型)
|
||||
@@ -84,13 +110,24 @@ function getDomainParams() {
|
||||
}
|
||||
|
||||
// 初始化时从本地读取会话ID(按当前 agent 隔离)
|
||||
// PR-MULTI-TURN-KEY-BINDING (2026-09-18): 先迁移老 key 再读新 key
|
||||
// H5 时序坑: 模块顶层同步执行时 uni.getStorageSync 仍返回空字符串
|
||||
// (uniapp H5 runtime shim 在 webpack bundle 之后才注入),延迟 1 个 tick
|
||||
// 让 uni runtime 就绪后再读 — 实测 100ms 后能读到值
|
||||
try {
|
||||
const saved = uni.getStorageSync(getConversationKey(currentAgentId));
|
||||
if (saved) {
|
||||
currentConversationId = saved;
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
migrateLegacyConversationKey(currentAgentId);
|
||||
const saved = uni.getStorageSync(getConversationKey(currentAgentId));
|
||||
if (saved) {
|
||||
currentConversationId = saved;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('读取会话ID失败:', e);
|
||||
}
|
||||
}, 0);
|
||||
} catch (e) {
|
||||
console.warn('读取会话ID失败:', e);
|
||||
console.warn('初始化会话ID任务派发失败:', e);
|
||||
}
|
||||
|
||||
const GUEST_ID_KEY = 'ai_guest_user_id';
|
||||
@@ -550,7 +587,11 @@ export default {
|
||||
// #ifdef H5
|
||||
try {
|
||||
var chatUrl = resolveApiUrl('chat');
|
||||
var url = Config.baseUrl + chatUrl;
|
||||
// PR-AI-CHAT-URL-NORMALIZE (2026-09-19): Config.baseUrl 文档约定以 '/' 结尾 (config.js:25),
|
||||
// resolveApiUrl 返 '/api/kefu/chat' 也以 '/' 开头 → 直接拼接会出 '//api/kefu/chat' (双斜杠)。
|
||||
// nginx 实际会 normalize, 但 console 会脏、第三方抓包看不懂, 单点 normalize 消除连续 //。
|
||||
// 只动 URL 拼接本身, 不改 baseUrl 约定 (整个项目所有 API 都依赖这个约定)。
|
||||
var url = (Config.baseUrl + chatUrl).replace(/([^:])\/\//g, '$1/');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -598,6 +639,20 @@ export default {
|
||||
if (!jsonStr || jsonStr === '[DONE]') continue;
|
||||
try {
|
||||
const data = JSON.parse(jsonStr);
|
||||
// PR-SSE-ERROR-PASSTHROUGH (2026-09-19): blocking 错误响应 (aikefu Kefu.php:558)
|
||||
// 或 aiagent Chat.php catch 块会在 stream 模式下也返 {"code":-1,"message":"..."}
|
||||
// 这种响应没 event 字段, 不应进 switch 的 default (会污染 console)
|
||||
if (data && typeof data === 'object' && !Array.isArray(data) &&
|
||||
data.code !== undefined && data.code !== 0 && !data.event) {
|
||||
console.warn('[AI SSE] 后端错误响应:', data.code, data.message);
|
||||
fire('error', { code: data.code, message: data.message || '请求失败' });
|
||||
break;
|
||||
}
|
||||
// 空数组是 PHP json_encode([]) 的产物 (如 attachments 或 history 字段),
|
||||
// 不是事件, 静默跳过
|
||||
if (Array.isArray(data)) {
|
||||
continue;
|
||||
}
|
||||
switch (data.event) {
|
||||
case 'message': {
|
||||
var text = data.answer || data.text || data.content || '';
|
||||
@@ -705,11 +760,13 @@ export default {
|
||||
|
||||
/**
|
||||
* 设置并持久化会话ID
|
||||
* PR-MULTI-TURN-KEY-BINDING (2026-09-18): 写入前先迁移老 key
|
||||
*/
|
||||
setConversationId(id) {
|
||||
if (id && id !== currentConversationId) {
|
||||
currentConversationId = id;
|
||||
try {
|
||||
migrateLegacyConversationKey(currentAgentId);
|
||||
uni.setStorageSync(getConversationKey(currentAgentId), id);
|
||||
} catch (e) {
|
||||
console.error('保存会话ID失败:', e);
|
||||
@@ -717,6 +774,36 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PR-MULTI-TURN-KEY-BINDING (2026-09-18): 用户切换钩子
|
||||
* App.vue 在 setMemberInfo 之后调用,触发清旧 user key + 重置 currentConversationId
|
||||
* 避免"用户 A 退出 → 用户 B 登录 → 直接复用 A 的会话 ID" 串会话 bug
|
||||
*
|
||||
* 调用时机:
|
||||
* - App.vue onLaunch 时如果检测到 memberInfo 恢复 → 调一次
|
||||
* - 任何 setMemberInfo 提交后调一次
|
||||
* 行为:
|
||||
* - 比对新老 user_id,不同时移除"旧 user"维度的 storage key
|
||||
* - 重置 currentConversationId 为 null (强制下一轮 chat 用新 user 的会话或新建)
|
||||
* - 记录 last_user_id 用于下次对比
|
||||
*/
|
||||
onUserChange() {
|
||||
try {
|
||||
const newUid = String(getUserId() || 'anon');
|
||||
const oldUid = uni.getStorageSync('ai_last_user_id');
|
||||
if (oldUid && oldUid !== newUid) {
|
||||
// 清旧用户 key 维度 (按当前 agent 即可,下次 setAgentId 会重新计算)
|
||||
const oldKey = CONVERSATION_KEY_PREFIX + (uni.getStorageSync('uniacid') || Config.uniacid || 0)
|
||||
+ '_' + oldUid + '_' + (currentAgentId || DEFAULT_AGENT_ID);
|
||||
try { uni.removeStorageSync(oldKey); } catch (e) { /* ignore */ }
|
||||
}
|
||||
uni.setStorageSync('ai_last_user_id', newUid);
|
||||
currentConversationId = null;
|
||||
} catch (e) {
|
||||
console.warn('[ai-service] onUserChange failed:', e && e.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除会话ID
|
||||
*/
|
||||
@@ -871,10 +958,33 @@ export default {
|
||||
return true;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
// H5 无原生 WS 长连接, HTTP SSE 也无 elicit_response 端点
|
||||
// 兜底: 仅展示用户选项, 30s 超时由后端 pending_stale 兜底
|
||||
console.warn('[ai-service] H5 elicitResponse 无可用通道, 仅展示用户选项 + 超时由 pending_stale 兜底');
|
||||
return false;
|
||||
// PR-ELICIT-RESPONSE-HTTP (2026-09-18): H5 端改 fetch POST 新端点
|
||||
// 后端 InitRoute 已注册 /aiagent/api/chat/elicit-response 和 /aikefu/api/chat/elicit-response
|
||||
// 复用 resolveApiUrl 走 agent_id 路由,与 chat/info 等接口一致
|
||||
try {
|
||||
const resp = await fetch(Config.baseUrl + resolveApiUrl('chat/elicit-response'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Token': store.state.token || 'test_token'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversation_id,
|
||||
message_id,
|
||||
tool_call_id,
|
||||
rpc_id,
|
||||
answers,
|
||||
uniacid: this.getUniacid(),
|
||||
user_id: getUserId(),
|
||||
token: store.state.token || 'test_token'
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
return !!(data && (data.code === 0 || data.success));
|
||||
} catch (e) {
|
||||
console.warn('[ai-service] H5 elicitResponse HTTP failed:', e && e.message);
|
||||
return false;
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user