2026-02-07 21:17:17 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* AI 请求稳态管理器
|
|
|
|
|
|
* 提供请求锁、超时控制、自动重试、JSON 解析保护、失败兜底
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
// 请求锁
|
|
|
|
|
|
let isRequesting = false;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 安全的 JSON 解析
|
|
|
|
|
|
*/
|
|
|
|
|
|
function safeJSONParse(text) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(text);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('AI_JSON_FALLBACK: 首次解析失败,尝试提取 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 代码块解析也失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-20 15:10:23 +08:00
|
|
|
|
return { status: 'fallback_text', raw: text };
|
2026-02-07 21:17:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成兜底解读内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
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 请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 带重试的请求执行
|
|
|
|
|
|
*/
|
|
|
|
|
|
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} 次重试`);
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
|
|
return executeWithRetry(config, retryCount + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 安全的 AI 请求(主函数)
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
if (manualTimeout) {
|
|
|
|
|
|
throw new Error('Manual timeout');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 15:10:23 +08:00
|
|
|
|
// 5. 安全解析 JSON
|
2026-02-07 21:17:17 +08:00
|
|
|
|
const parsedResult = safeJSONParse(rawResponse);
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|