huaanglimeng/utils/aiRequestManager.js

221 lines
6.0 KiB
JavaScript
Raw Permalink Normal View History

/**
* AI 请求稳态管理器
* 提供请求锁超时控制自动重试JSON 解析保护失败兜底
*/
// 请求锁
let isRequesting = false;
/**
* 安全的 JSON 解析
* @param {string} text - 待解析的文本
* @returns {Object} 解析结果
*/
function safeJSONParse(text) {
try {
return JSON.parse(text);
} catch (e) {
console.log('AI_JSON_FALLBACK: 首次解析失败,尝试提取 JSON 代码块');
// 尝试提取 JSON 代码块(可能被包裹在 ```json ``` 中)
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/) || text.match(/```\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
try {
return JSON.parse(jsonMatch[1]);
} catch (e2) {
console.error('AI_JSON_FALLBACK: JSON 代码块解析也失败');
}
}
// 完全失败,返回 fallback 状态
return {
status: 'fallback_text',
raw: text
};
}
}
/**
* 生成兜底解读内容
* @param {Array} drawnCards - 抽到的牌
* @param {Object} spread - 牌阵信息
* @returns {Object} 兜底解读
*/
function generateFallbackInterpretation(drawnCards, spread) {
const positions = drawnCards.map((card, index) => ({
posName: spread.positions[index],
posMeaning: `${card.name}${card.isReversed ? '(逆位)' : '(正位)'}提示你关注当前的能量变化,保持觉察。`
}));
return {
theme: '本次牌面提示你正处于一个需要观察和思考的阶段',
status: '你可能正在经历一些内在的调整',
influence: '当前能量正在变化之中',
advice: '给自己一点时间观察,避免过度解读当前状况,关注真实感受',
positions: positions
};
}
/**
* 执行单次 AI 请求
* @param {Object} config - 请求配置
* @returns {Promise} 请求结果
*/
function executeRequest(config) {
return new Promise((resolve, reject) => {
const { url, data, timeout = 90000 } = config;
wx.request({
url,
method: 'POST',
header: config.header,
data,
timeout,
success: (res) => {
if (res.data && res.data.choices && res.data.choices[0]) {
resolve(res.data.choices[0].message.content);
} else {
reject(new Error('AI 返回数据格式异常'));
}
},
fail: (err) => {
reject(err);
}
});
});
}
/**
* 带重试的请求执行
* @param {Object} config - 请求配置
* @param {number} retryCount - 当前重试次数
* @returns {Promise} 请求结果
*/
async function executeWithRetry(config, retryCount = 0) {
try {
const result = await executeRequest(config);
return result;
} catch (err) {
const shouldRetry = retryCount < 1 && (
err.errMsg?.includes('timeout') ||
err.errMsg?.includes('fail') ||
err.statusCode >= 500
);
if (shouldRetry) {
console.log(`AI_REQUEST_RETRY: 第 ${retryCount + 1} 次重试`);
// 等待 1500ms 后重试
await new Promise(resolve => setTimeout(resolve, 1500));
return executeWithRetry(config, retryCount + 1);
}
throw err;
}
}
/**
* 安全的 AI 请求主函数
* @param {Object} payload - 请求参数
* @returns {Promise<Object>} 请求结果
*/
async function safeAIRequest(payload) {
const {
url,
header,
data,
drawnCards,
spread,
onProgress
} = payload;
// 1. 请求锁检查
if (isRequesting) {
console.log('AI_REQUEST_BLOCKED: 请求进行中');
return {
status: 'blocked',
message: '请求进行中,请稍候'
};
}
isRequesting = true;
console.log('AI_REQUEST_START');
// 2. 进度提示
let progressTimer1, progressTimer2;
if (onProgress) {
onProgress('正在抽取牌面…');
progressTimer1 = setTimeout(() => onProgress('正在深入解读…'), 5000);
progressTimer2 = setTimeout(() => onProgress('解读较复杂,请稍候…'), 15000);
}
// 3. 手动超时监控95秒
let manualTimeout = false;
const timeoutTimer = setTimeout(() => {
manualTimeout = true;
console.log('AI_REQUEST_TIMEOUT: 手动超时触发');
}, 95000);
try {
// 4. 执行请求(带重试)
const rawResponse = await executeWithRetry({
url,
header,
data,
timeout: 90000
});
// 清除超时定时器
clearTimeout(timeoutTimer);
clearTimeout(progressTimer1);
clearTimeout(progressTimer2);
// 5. 检查手动超时
if (manualTimeout) {
throw new Error('Manual timeout');
}
// 6. 安全解析 JSON
const parsedResult = safeJSONParse(rawResponse);
// 7. 检查是否是 fallback 状态
if (parsedResult.status === 'fallback_text') {
console.log('AI_JSON_FALLBACK: 使用兜底解读');
isRequesting = false;
return {
status: 'fallback',
data: generateFallbackInterpretation(drawnCards, spread)
};
}
console.log('AI_REQUEST_SUCCESS');
isRequesting = false;
return {
status: 'success',
data: parsedResult
};
} catch (err) {
console.error('AI_REQUEST_FAILED:', err);
// 清除定时器
clearTimeout(timeoutTimer);
clearTimeout(progressTimer1);
clearTimeout(progressTimer2);
isRequesting = false;
// 返回兜底解读
return {
status: 'fallback',
data: generateFallbackInterpretation(drawnCards, spread),
error: err.errMsg || err.message
};
}
}
module.exports = {
safeAIRequest
};