V1.2: 添加问题过滤系统、AI Prompt优化和请求稳态层

新增功能:
- 问题过滤与质量评分系统
- 改写建议弹窗交互
- AI Prompt 三层结构优化
- AI 请求稳态管理器(请求锁、超时控制、自动重试、JSON保护)
- 动态加载状态提示

优化改进:
- 修复正则表达式匹配问题
- 修复弹窗按钮文字长度限制
- 优化改写建议的关联性和情感共鸣
- 增强 AI 解读的问题相关性和稳定性
This commit is contained in:
huanglimeng 2026-02-07 21:17:17 +08:00
parent 71a73908fa
commit 27b5c0a3ad
5 changed files with 708 additions and 47 deletions

View File

@ -6,6 +6,8 @@ const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/chat/completions';
const { minorArcana } = require('../../data/tarot/index'); const { minorArcana } = require('../../data/tarot/index');
const { getQuestionType } = require('../../utils/questionType'); const { getQuestionType } = require('../../utils/questionType');
const { generateSpreadStory } = require('../../utils/spreadStory'); const { generateSpreadStory } = require('../../utils/spreadStory');
const { validateQuestion, scoreQuestion } = require('../../utils/questionFilter');
const { safeAIRequest } = require('../../utils/aiRequestManager');
// --- Prompt 模板定义 --- // --- Prompt 模板定义 ---
@ -52,25 +54,100 @@ const TAROT_PROMPT_NIGHT = `你是一位安静、克制、不制造依赖的塔
输出要求 输出要求
- 只输出解读文本`; - 只输出解读文本`;
// 3. 增强版模板:结构化深度解读
const TAROT_PROMPT_ENHANCED = `你是一位专业、温和且具有洞察力的塔罗解读者。
你的任务是根据选定的牌阵为用户提供有深度有层次且具有陪伴感的解读
解读边界 // 3. 增强版模板:解读稳定器 + 高关联性结构化解读
- 不预测具体事件不给绝对结论不使用恐吓性语言 const TAROT_PROMPT_ENHANCED = `📏 解读稳定规则(高优先级执行)
- 保持温和的引导口气像是在进行一场心灵对话
解读结构 你必须保持解读风格稳定具体且一致不允许出现质量波动
请严格按照以下 JSON 格式输出解读内容不要包含任何其他说明性文字
解读长度控制
整体长度400700 不少于 300 不超过 900
各模块建议
- 问题回应23
- 牌面分析每张牌2句以内
- 当前状态23
- 发展趋势23
- 行动建议35条短句
禁止超长段落大段心理学科普冗长比喻
结构稳定规则必须严格执行
输出必须包含且仅包含以下5个部分不允许新增其他模块
1. 问题回应
2. 牌面分析
3. 当前状态
4. 发展趋势
5. 行动建议
解读深度控制
每段必须包含至少一个情境描述心理状态行为倾向关系互动
禁止单纯解释牌意抽象空话能量类泛词堆叠
情绪语气稳定器
必须温和理解用户提供支持
避免过度肯定一定会绝对否定绝不会命令式建议恐吓式预测
推荐句式"你当前可能正在经历…" "这可能让你感到…" "可以尝试…"
百科化防护规则非常重要
禁止出现"通常这张牌代表…" "根据牌义…" "在传统塔罗中…"
如果出现类似内容必须立即转化为与用户问题相关的情境说明
参考牌义权重限制
信息优先级用户问题 > 提问领域 > 抽到的牌 > 参考牌义
参考牌义只能用于理解不允许复述或改写复述
质量自检生成前内部执行
在输出前检查
开头是否回应问题
是否逐张牌关联问题
是否包含可执行建议
是否出现百科语句
是否结构完整
---
🎯 角色设定
你是一位专业温和具有洞察力的塔罗解读师
你的解读必须围绕用户提出的问题展开而不是输出通用牌义
本次解读优先级
用户问题 > 提问领域 > 抽到的牌 > 参考牌义
🧭 解读核心要求非常重要
1 解读必须紧密围绕用户问题展开
2 开头第一句话必须直接回应用户的问题情境
3 每一张牌都要说明它与该问题的具体关系
4 禁止输出通用百科式牌义解释
5 必须结合提问领域进行情境化分析
6 解读重点放在状态趋势与建议而非绝对预测
🎨 语言风格要求
温和具体有针对性
使用"你当前的情况可能是…"
避免绝对判断和命令式语气
不要出现"根据牌义来说…" "通常这张牌代表…"
🚫 禁止行为
不要复述参考牌义原文
不要脱离用户问题进行泛泛而谈
不要写百科解释
不要生成医疗法律金融建议
📤 输出格式
请严格按照以下 JSON 格式输出不要包含任何其他说明性文字
{ {
"theme": "用一句话概括本次解读的核心主题", "theme": "直接回应用户问题的核心主题(必须提及问题关键词)",
"status": "描述用户当下的情绪或处境状态", "status": "用户在这个问题上的当前状态",
"influence": "分析当前局面背后的潜在影响因素(内在或外在)", "influence": "影响这个问题的潜在因素",
"advice": "给出具体、温和且具有操作性的行动建议", "advice": "温和、可执行的个人成长建议",
"positions": [ "positions": [
{ {
"posName": "位置名称", "posName": "位置名称",
"posMeaning": "由你结合牌面对该位置的详细解读(约 60-100 字)" "posMeaning": "解释这张牌如何影响该问题情境60-100字必须联系问题"
} }
] ]
}`; }`;
@ -183,6 +260,7 @@ Page({
selectedSpread: null, selectedSpread: null,
question: '', // 用户输入的问题 question: '', // 用户输入的问题
questionType: '综合问题', // 问题类型 questionType: '综合问题', // 问题类型
questionQuality: null, // 问题质量评分
drawnCardIndices: [], // 用户选中的牌索引 drawnCardIndices: [], // 用户选中的牌索引
drawnCards: [], // 实际抽到的牌对象 drawnCards: [], // 实际抽到的牌对象
revealedCount: 0, // 已翻开的数量 revealedCount: 0, // 已翻开的数量
@ -196,10 +274,11 @@ Page({
infoAnimation: {}, infoAnimation: {},
guideAnimation: {}, guideAnimation: {},
// AI 相关 // AI 解读相关
aiResult: null,
isAiLoading: false, isAiLoading: false,
aiExplanation: '', aiLoadingText: '正在抽取牌面…', // 动态加载提示
spreadStory: '', // 综合故事线 spreadStory: '',
// 3. 前缀文案列表(随机抽取) // 3. 前缀文案列表(随机抽取)
prefixList: [ prefixList: [
@ -422,8 +501,65 @@ Page({
// --- 1.5 确认问题并开始洗牌 --- // --- 1.5 确认问题并开始洗牌 ---
confirmQuestion: function () { confirmQuestion: function () {
const question = this.data.question.trim(); const question = this.data.question.trim();
const questionType = getQuestionType(question); console.log('confirmQuestion 被调用,问题:', question);
// 新增:过滤验证
const filterResult = validateQuestion(question);
console.log('过滤结果:', filterResult);
if (filterResult.status === 'reject') {
console.log('触发拒绝弹窗');
// 显示拒绝弹窗
wx.showModal({
title: '温馨提示',
content: filterResult.reason,
showCancel: false,
confirmText: '我知道了'
});
return;
}
if (filterResult.status === 'rewrite') {
console.log('触发改写建议弹窗');
// 显示改写建议弹窗 - 简化版本
const firstSuggestion = filterResult.suggestions[0];
const modalContent = `${filterResult.reason}\n\n${firstSuggestion}`;
console.log('弹窗内容:', modalContent);
wx.showModal({
title: '提问建议',
content: modalContent,
confirmText: '使用建议',
cancelText: '继续提问',
success: (res) => {
console.log('弹窗回调:', res);
if (res.confirm) {
// 使用建议问题
this.setData({
question: firstSuggestion,
questionQuality: scoreQuestion(firstSuggestion)
});
}
// 无论选择哪个,都继续流程
this.proceedToShuffle();
},
fail: (err) => {
console.error('弹窗显示失败:', err);
// 即使弹窗失败,也继续流程
this.proceedToShuffle();
}
});
return;
}
console.log('通过验证,继续流程');
// pass 状态:直接继续
this.proceedToShuffle();
},
// 抽离洗牌逻辑
proceedToShuffle: function () {
const questionType = getQuestionType(this.data.question);
this.setData({ this.setData({
questionType: questionType, questionType: questionType,
state: 'shuffling' state: 'shuffling'
@ -437,8 +573,11 @@ Page({
// 输入问题 // 输入问题
onQuestionInput: function (e) { onQuestionInput: function (e) {
const question = e.detail.value;
const quality = scoreQuestion(question);
this.setData({ this.setData({
question: e.detail.value question: question,
questionQuality: quality
}); });
}, },
@ -508,13 +647,14 @@ Page({
}, },
// --- 获取 AI 解读主逻辑 --- // --- 获取 AI 解读主逻辑 ---
fetchAiInterpretation: function () { fetchAiInterpretation: async function () {
const { drawnCards, selectedSpread } = this.data; const { drawnCards, selectedSpread, question, questionType } = this.data;
if (!drawnCards.length) return; if (!drawnCards.length) return;
this.setData({ this.setData({
isAiLoading: true, isAiLoading: true,
aiResult: null aiResult: null,
aiLoadingText: '正在抽取牌面…'
}); });
// 1. 构建卡牌信息 // 1. 构建卡牌信息
@ -522,13 +662,39 @@ Page({
return `位置[${selectedSpread.positions[index]}]${card.name} (${card.isReversed ? '逆位' : '正位'}),关键词:${card.keyword || (card.keywords ? card.keywords.join(',') : '')}`; return `位置[${selectedSpread.positions[index]}]${card.name} (${card.isReversed ? '逆位' : '正位'}),关键词:${card.keyword || (card.keywords ? card.keywords.join(',') : '')}`;
}).join('\n'); }).join('\n');
const userPrompt = `牌阵:${selectedSpread.name}\n${cardDetails}`; // 2. 构建参考牌义(供 AI 理解用)
const cardMeanings = drawnCards.map((card, index) => {
const meaning = card.isReversed ? (card.reversedMeaning || card.description) : (card.meaning || card.description);
return `${card.name} (${card.isReversed ? '逆位' : '正位'})${meaning}`;
}).join('\n');
// 3. 构建完整的用户提示
const userPrompt = `📥 输入信息
用户问题
${question || '未指定具体问题'}
提问领域
${questionType}
牌阵
${selectedSpread.name}
抽到的牌
${cardDetails}
参考牌义内部理解用
${cardMeanings}
参考牌义仅用于理解不允许逐字复述请严格围绕用户的问题进行解读`;
console.log("正在获取深度解读..."); console.log("正在获取深度解读...");
console.log("用户问题:", question);
console.log("提问领域:", questionType);
wx.request({ // 4. 使用稳态请求管理器
const result = await safeAIRequest({
url: DEEPSEEK_BASE_URL, url: DEEPSEEK_BASE_URL,
method: 'POST',
header: { header: {
'Authorization': `Bearer ${DEEPSEEK_API_KEY}`, 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -542,30 +708,42 @@ Page({
stream: false, stream: false,
response_format: { type: "json_object" } response_format: { type: "json_object" }
}, },
timeout: 20000, drawnCards,
success: (res) => { spread: selectedSpread,
if (res.data && res.data.choices && res.data.choices[0]) { onProgress: (text) => {
try { this.setData({ aiLoadingText: text });
const aiResult = JSON.parse(res.data.choices[0].message.content);
const spreadStory = generateSpreadStory(drawnCards);
this.setData({
aiResult,
spreadStory: spreadStory,
isAiLoading: false
});
} catch (e) {
console.error("JSON 解析失败", e);
this.handleAiFallback();
}
} else {
this.handleAiFallback();
}
},
fail: (err) => {
console.error("请求失败", err);
this.handleAiFallback();
} }
}); });
// 5. 处理结果
if (result.status === 'blocked') {
wx.showToast({
title: result.message,
icon: 'none',
duration: 2000
});
this.setData({ isAiLoading: false });
return;
}
// 6. 生成整体趋势
const spreadStory = generateSpreadStory(drawnCards);
// 7. 更新数据
this.setData({
aiResult: result.data,
spreadStory: spreadStory,
isAiLoading: false
});
// 8. 如果是 fallback显示提示
if (result.status === 'fallback' && result.error) {
wx.showToast({
title: 'AI 解读失败,已使用备用解读',
icon: 'none',
duration: 3000
});
}
}, },
handleAiFallback: function () { handleAiFallback: function () {

View File

@ -34,6 +34,12 @@
maxlength="200" maxlength="200"
></textarea> ></textarea>
<text class="char-count">{{question.length}}/200</text> <text class="char-count">{{question.length}}/200</text>
<!-- 新增:质量评分显示 -->
<view class="quality-indicator" wx:if="{{questionQuality && question.length > 0}}">
<text class="quality-icon">{{questionQuality.icon}}</text>
<text class="quality-text">提问质量:{{questionQuality.level}}</text>
</view>
</view> </view>
<button class="start-btn" bindtap="confirmQuestion" hover-class="btn-hover">开始抽牌</button> <button class="start-btn" bindtap="confirmQuestion" hover-class="btn-hover">开始抽牌</button>
</view> </view>
@ -143,7 +149,7 @@
<view class="interpretation-area" wx:if="{{revealedCount === selectedSpread.cardCount}}" animation="{{infoAnimation}}"> <view class="interpretation-area" wx:if="{{revealedCount === selectedSpread.cardCount}}" animation="{{infoAnimation}}">
<view class="loading-ai" wx:if="{{isAiLoading}}"> <view class="loading-ai" wx:if="{{isAiLoading}}">
<text class="loading-dot">···</text> <text class="loading-dot">···</text>
<text>正在深度解读能量流向...</text> <text>{{aiLoadingText}}</text>
</view> </view>
<view class="structured-result" wx:else> <view class="structured-result" wx:else>

View File

@ -521,3 +521,27 @@ page {
transform: scale(0.98); transform: scale(0.98);
opacity: 0.9; opacity: 0.9;
} }
/* 质量评分指示器 */
.quality-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, rgba(139, 233, 253, 0.1), rgba(80, 250, 123, 0.1));
border-radius: 16rpx;
border: 1px solid rgba(139, 233, 253, 0.3);
}
.quality-icon {
font-size: 40rpx;
margin-right: 12rpx;
}
.quality-text {
font-size: 28rpx;
color: #8be9fd;
font-weight: 500;
}

220
utils/aiRequestManager.js Normal file
View File

@ -0,0 +1,220 @@
/**
* 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
};

233
utils/questionFilter.js Normal file
View File

@ -0,0 +1,233 @@
/**
* 塔罗小程序 - 提问过滤与质量评分模块
* 纯本地规则不依赖外部 API
*/
/**
* 验证问题并返回过滤结果
* @param {string} question - 用户输入的问题
* @returns {Object} { status: "pass"|"rewrite"|"reject", reason: "", suggestions: [] }
*/
function validateQuestion(question) {
if (!question || question.trim().length === 0) {
return {
status: 'reject',
reason: '请输入您的问题',
suggestions: []
};
}
const q = question.trim();
// ========== 1. 直接拒绝规则 (reject) ==========
// 1.1 联系方式检测
const contactPatterns = [
/微信|wx|weixin|vx/i,
/qq|扣扣|q q/i,
/电话|手机|tel|phone/i,
/tg|telegram|飞机/i,
/\d{5,}/ // 5位以上数字可能是QQ号、电话等
];
if (contactPatterns.some(pattern => pattern.test(q))) {
return {
status: 'reject',
reason: '为了保证内容健康与占卜体验,该问题暂不支持占卜。',
suggestions: []
};
}
// 1.2 广告引流检测
const adPatterns = [
/加我|联系我|找我|私信|咨询我/i,
/推广|代理|兼职|赚钱|刷单/i,
/链接|网址|http|www\./i
];
if (adPatterns.some(pattern => pattern.test(q))) {
return {
status: 'reject',
reason: '为了保证内容健康与占卜体验,该问题暂不支持占卜。',
suggestions: []
};
}
// 1.3 成人/违法内容检测
const illegalPatterns = [
/黄色|色情|裸|性爱/i,
/毒品|大麻|冰毒/i,
/枪支|炸药|爆炸/i
];
if (illegalPatterns.some(pattern => pattern.test(q))) {
return {
status: 'reject',
reason: '为了保证内容健康与占卜体验,该问题暂不支持占卜。',
suggestions: []
};
}
// 1.4 专业决策类检测(医疗/法律/金融/赌博)
const professionalPatterns = [
/病|癌|肿瘤|手术|治疗|吃药|诊断/i,
/官司|诉讼|判决|坐牢|犯法/i,
/股票|期货|基金|投资.*收益|炒股/i,
/彩票|赌博|赌|博彩|中奖号码/i
];
if (professionalPatterns.some(pattern => pattern.test(q))) {
return {
status: 'reject',
reason: '为了让占卜更聚焦你自身的成长,建议调整提问方式。塔罗牌无法替代专业咨询。',
suggestions: []
};
}
// 1.5 危险内容检测
const dangerPatterns = [
/自杀|轻生|结束生命|不想活/i,
/杀人|伤害.*人|报复/i,
/什么时候死|何时去世|死亡时间/i
];
if (dangerPatterns.some(pattern => pattern.test(q))) {
return {
status: 'reject',
reason: '为了保证内容健康与占卜体验,该问题暂不支持占卜。如您正面临困境,建议寻求专业心理咨询帮助。',
suggestions: []
};
}
// ========== 2. 建议改写规则 (rewrite) ==========
const rewritePatterns = [
{
// 他/她爱不爱我
pattern: /(他|她|ta|TA|对方|那个人)(爱不爱|喜不喜欢|有没有感觉|在乎)/i,
suggestions: [
'我在这段感情中感受到爱了吗',
'我对这段关系的真实期待是什么',
'我需要怎样的爱才能感到满足'
]
},
{
// 是否出轨
pattern: /(他|她|ta|TA|对方).*(出轨|劈腿|有别人|背叛)/i,
suggestions: [
'我内心的不安来自哪里',
'我在这段关系中缺失了什么',
'我该如何重建内心的安全感'
]
},
{
// TA在干嘛
pattern: /(他|她|ta|TA|对方).*(在干嘛|在做什么|在想什么|想法)/i,
suggestions: [
'我为什么会如此在意对方的动态',
'我现在最需要关注的是什么',
'我该如何平复内心的焦虑'
]
},
{
// 控制类问题
pattern: /(怎么|如何).*(让|使|控制|改变).*(他|她|ta|TA|对方)/i,
suggestions: [
'我可以如何让自己变得更有吸引力',
'我在关系中真正想要的是什么',
'我该如何更好地表达自己的需求'
]
},
{
// 怎么让他喜欢我
pattern: /(怎么|如何).*(让|使).*(他|她|ta|TA|对方).*(喜欢|爱上|回来)/i,
suggestions: [
'我身上最值得被爱的特质是什么',
'我该如何在关系中保持真实的自己',
'我准备好迎接一段健康的关系了吗'
]
}
];
for (const item of rewritePatterns) {
if (item.pattern.test(q)) {
console.log('匹配到改写规则:', item.pattern);
return {
status: 'rewrite',
reason: '为了让占卜更聚焦你的成长,我们建议这样提问:',
suggestions: item.suggestions
};
}
}
// ========== 3. 通过验证 ==========
console.log('问题通过所有过滤规则');
return {
status: 'pass',
reason: '',
suggestions: []
};
}
/**
* 评估问题质量
* @param {string} question - 用户输入的问题
* @returns {Object} { score: 0-5, level: "普通"|"良好"|"优秀", icon: "🌱"|"🌿"|"🌳" }
*/
function scoreQuestion(question) {
if (!question || question.trim().length === 0) {
return {
score: 0,
level: '普通',
icon: '🌱'
};
}
const q = question.trim();
let score = 0;
// 规则1: 以"我"为主体 (+2分)
if (/^我|[,。!?\s]我/.test(q)) {
score += 2;
}
// 规则2: 包含情绪或状态词 (+1分)
const emotionWords = /感受|状态|心情|情绪|焦虑|迷茫|困惑|压力|快乐|幸福|痛苦|难过/;
if (emotionWords.test(q)) {
score += 1;
}
// 规则3: 开放式问题 (+1分)
const openQuestions = /如何|怎样|什么|哪些|为什么/;
if (openQuestions.test(q)) {
score += 1;
}
// 规则4: 包含绝对预测词 (-2分)
const absoluteWords = /会不会|能不能|一定会|肯定|必然|到底|结果是/;
if (absoluteWords.test(q)) {
score -= 2;
}
// 确保分数在 0-5 范围内
score = Math.max(0, Math.min(5, score));
// 映射等级
let level, icon;
if (score <= 2) {
level = '普通';
icon = '🌱';
} else if (score <= 4) {
level = '良好';
icon = '🌿';
} else {
level = '优秀';
icon = '🌳';
}
return {
score,
level,
icon
};
}
module.exports = {
validateQuestion,
scoreQuestion
};