Compare commits

..

5 Commits

Author SHA1 Message Date
ddshi
0c670de0dd fix: 优化 DeepSeek prompt 修复时间解析问题
- 下午3点 = 15:00 (不是03:00)
- "X点后" 理解为"几点之后"而非"X小时后"
- 添加关键规则强调

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:26:22 +08:00
ddshi
dc98407edc fix: 移除 mock fallback,完全使用 DeepSeek AI 解析
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:51:54 +08:00
ddshi
d862203140 fix: 优化AI解析和添加调试日志
- 简化SYSTEM_PROMPT,使AI更容易理解并返回正确的JSON格式
- 添加详细日志帮助调试AI解析问题
- 改进错误处理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:17:56 +08:00
ddshi
1ff0693901 fix: 优化AI解析,智能提取标题和内容
- 改进标题提取:去除"提醒"、"帮我"等前缀,保留事件名称
- 改进内容提取:提取日期时间后的描述性文字
- 添加优先级识别:红色、绿色、黄色标记
- 添加提前提醒识别:提前X天/小时/分钟提醒
- 更新SYSTEM_PROMPT:指导AI正确提取各字段

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:05:30 +08:00
ddshi
beedafc7d1 feat: 优化AI文本识别的prompt
- 增强意图识别:明确区分纪念日和提醒的规则
- 扩展农历节日识别:添加更多传统节日
- 扩展公历节日识别:添加情人节、妇女节、儿童节等
- 优化重复规则识别逻辑
- 优化mock函数:更智能的日期解析和标题提取

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:42:42 +08:00
2 changed files with 256 additions and 56 deletions

2
.env
View File

@ -14,7 +14,7 @@ DATABASE_URL=file:./prisma/dev.db
# DATABASE_URL=postgresql://qia_admin:your-password@postgres.ap-shanghai.myqcloud.com:5432/qia # DATABASE_URL=postgresql://qia_admin:your-password@postgres.ap-shanghai.myqcloud.com:5432/qia
# DeepSeek AI # DeepSeek AI
DEEPSEEK_API_KEY=sk-7e34702637f74020b62cdd62d3f48559 DEEPSEEK_API_KEY=sk-3e2019e98c6f406b86ba31c7b820fb51
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
# CORS (支持多个开发端口) # CORS (支持多个开发端口)

View File

@ -11,25 +11,110 @@ const SYSTEM_PROMPT = `你是一个帮助用户创建事件(纪念日或提醒
${new Date().toISOString()} ${new Date().toISOString()}
JSON格式返回 - JSON中遵守
1. 3 = 15:00不是03:00
2. 1 = 13:002 = 14:003 = 15:004 = 16:00
3. 8 = 20:009 = 21:00
4. "X点后""X点之后""X小时后"
- "下午3点后" = 15:00
- "晚上8点后" = 20:00
5. "X点"
{ JSON格式
"parsed": {
"type": "anniversary 或 reminder",
"title": "事件标题",
"date": "2026-02-13T15:00:00Z",
"timezone": "Asia/Shanghai",
"is_lunar": true false,
"repeat_type": "daily, weekly, monthly, yearly 或 none"
},
"response": "友好的确认消息"
}
{"type": "anniversary", "title": "事件标题", "content": "详细内容", "date": "2026-02-13T09:00:00Z", "timezone": "Asia/Shanghai", "is_lunar": false, "repeat_type": "yearly", "priority": "none", "reminder_times": []}
- type: "anniversary"() "reminder"()
- title: 简洁的事件标题"提醒""帮我"
- content: 仅提醒类型需要
- date: ISO 8601
- timezone: "Asia/Shanghai"
- is_lunar: 是否农历日期
- repeat_type: "daily"(), "weekly"(), "monthly"(), "yearly"(), "none"()
- priority: "none", "red", "green", "yellow"
- reminder_times: 提前提醒时间数组
- "提醒我明天上午9点开会" title = "开会"
- "提醒我春节回家" title = "春节回家"
- "帮我记一下妈妈的生日" title = "妈妈的生日"
- "上午9点" date = 09
- "下午3点" date = 153=153
- "晚上8点" date = 20
- "下午3点后" date = 15:00<15:00或明天15:00如果当前时间>=15:00
- "晚上8点后" date = 20:00 20:00
##
### vs
- "生日""纪念日""结婚纪念日""节日""春节""中秋""端午""元宵" type = "anniversary"
- "提醒""任务""todo""待办""会议""约会""上课" type = "reminder"
- "reminder"
###
-
- "提醒我""帮我""请帮我"
-
- "提醒我明天上午9点开会" title = "开会"
- "提醒我春节回家" title = "春节回家"
###
- content
-
- "提醒我明天上午9点开会讨论项目进度" content = "讨论项目进度"
###
1. "农历正月初一""农历三月三""阴历八月十五" is_lunar = true
2.
3. "明天""后天""下周""下个月""明年"
4. "周一""周二"... "周日""星期天"
5. "3月8日""5月1日""10月1日"
###
- "上午9点" 09:00:00Z
- "下午3点" 15:00:00Z 03:00
- "晚上8点" 20:00:00Z
- "14:30" 14:30:00Z
- "上午X点" = X:00X=1-12
- "下午X点" = X+12:003=151=13
- "晚上X点" = X+12:008=2010=22
### "X点后"
- "8点后""9点后""10点后" "几点以后""几小时后"
- "下午3点后" "下午3点"=15:00"后""之后"
- "晚上8点后" "晚上8点"=20:00"后""之后"
-
1. // + X点
2. 24
3. "后"
- "X点后""X小时后"
###
- "每天" repeat_type = "daily"
- "每周" repeat_type = "weekly"
- "每月" repeat_type = "monthly"
- "每年" repeat_type = "yearly"
- "不重复""仅一次" repeat_type = "none"
###
- "重要""紧急""红色" priority = "red"
- "绿色" priority = "green"
- "黄色" priority = "yellow"
- priority = "none"
###
- "提前1天提醒" reminder_times = [eventDate - 1]
- "提前2小时提醒" reminder_times = [eventDate - 2]
- "提前30分钟提醒" reminder_times = [eventDate - 30]
##
1. date ISO 8601: YYYY-MM-DDTHH:mm:ssZ 1. date ISO 8601: YYYY-MM-DDTHH:mm:ssZ
2. "明天"date = ( + 1) 2. "明天"date = ( + 1)
3. "后天"date = ( + 2) 3. "后天"date = ( + 2)
4. "下周三"date = 09:00:00Z 4. "下周三"date = 09:00:00Z
5. "下午3点"date = 今天下午15:00:00Z 或用户指定的日期的15:00:00Z 5. "下午3点"date = 今天下午15:00:00Z 或用户指定的日期的15:00:00Z
6. 使 09:00:00Z 6. 使 09:00:00Z
7. NEVER 使 2024-01-02T15:00:00Z使 7. NEVER 使 2024-01-02T15:00:00Z使
@ -39,7 +124,8 @@ const SYSTEM_PROMPT = `你是一个帮助用户创建事件(纪念日或提醒
-> ->
is_holiday=true is_holiday=true
-
-
- 使 "Asia/Shanghai" - 使 "Asia/Shanghai"
@ -49,7 +135,9 @@ const SYSTEM_PROMPT = `你是一个帮助用户创建事件(纪念日或提醒
- -
- "下周三" - "下周三"
- 使`; - 使
- "提醒""帮我"
- "X点后""X小时后""8点后""8点""8小时以后"`;
const parseMessageSchema = z.object({ const parseMessageSchema = z.object({
message: z.string().min(1, 'Message is required').max(1000), message: z.string().min(1, 'Message is required').max(1000),
@ -107,8 +195,7 @@ async function callDeepSeek(message: string): Promise<{
const apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions'; const apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions';
if (!apiKey || apiKey === 'sk-xxx') { if (!apiKey || apiKey === 'sk-xxx') {
// Mock response for development throw new Error('DeepSeek API Key 未配置');
return mockParseResponse(message);
} }
try { try {
@ -147,14 +234,19 @@ async function callDeepSeek(message: string): Promise<{
const data = await response.json(); const data = await response.json();
const content = data.choices[0]?.message?.content || ''; const content = data.choices[0]?.message?.content || '';
// Log the raw response for debugging
console.log('[AI] Raw response:', content);
// Extract JSON from response // Extract JSON from response
const jsonMatch = content.match(/\{[\s\S]*\}/); const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) { if (jsonMatch) {
try { try {
const rawParsed = JSON.parse(jsonMatch[0]); const rawParsed = JSON.parse(jsonMatch[0]);
console.log('[AI] Parsed JSON:', rawParsed);
const parsed = rawParsed.parsed || rawParsed; const parsed = rawParsed.parsed || rawParsed;
// Validate parsed data with Zod schema // Validate parsed data with Zod schema
const validated = parsedEventSchema.parse(parsed); const validated = parsedEventSchema.parse(parsed);
console.log('[AI] Validated:', validated);
// 构建友好的用户消息 // 构建友好的用户消息
const typeText = validated.type === 'anniversary' ? '纪念日' : '提醒'; const typeText = validated.type === 'anniversary' ? '纪念日' : '提醒';
const dateObj = new Date(validated.date); const dateObj = new Date(validated.date);
@ -170,15 +262,16 @@ async function callDeepSeek(message: string): Promise<{
response: `已为你创建${typeText}${validated.title}」,日期:${dateText}${repeatText}`, response: `已为你创建${typeText}${validated.title}」,日期:${dateText}${repeatText}`,
}; };
} catch (e) { } catch (e) {
// Schema validation failed, use mock response console.error('[AI] Parse error:', e);
return mockParseResponse(message); throw new Error(`AI 解析失败: ${e instanceof Error ? e.message : '未知错误'}`);
} }
} }
return mockParseResponse(message); console.warn('[AI] No JSON found in response');
throw new Error('AI 响应格式错误,未能解析出 JSON');
} catch (error) { } catch (error) {
console.error('DeepSeek API error:', error); console.error('DeepSeek API error:', error);
return mockParseResponse(message); throw new Error(`DeepSeek API 调用失败: ${error instanceof Error ? error.message : '未知错误'}`);
} }
} }
@ -187,17 +280,36 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
// Simple mock parser for testing // Simple mock parser for testing
const lowerMessage = message.toLowerCase(); const lowerMessage = message.toLowerCase();
// Detect if it's a reminder (contains "提醒", "提醒我", etc.) // 意图识别:纪念日 vs 提醒
const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我'); const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我') ||
lowerMessage.includes('任务') || lowerMessage.includes('todo') ||
lowerMessage.includes('待办') || lowerMessage.includes('会议') ||
lowerMessage.includes('约会') || lowerMessage.includes('上课');
// 意图识别:纪念日
const isAnniversary = lowerMessage.includes('生日') || lowerMessage.includes('纪念日') ||
lowerMessage.includes('节日') || lowerMessage.includes('春节') ||
lowerMessage.includes('中秋') || lowerMessage.includes('端午') ||
lowerMessage.includes('元宵');
// 强制使用正确类型
const eventType = isAnniversary ? 'anniversary' : (isReminder ? 'reminder' : 'reminder');
// Detect lunar date // Detect lunar date
const isLunar = lowerMessage.includes('农历') || lowerMessage.includes('阴历'); const isLunar = lowerMessage.includes('农历') || lowerMessage.includes('阴历') ||
lowerMessage.includes('正月') || lowerMessage.includes('腊月');
// Detect holiday // Detect holiday
const isHoliday = lowerMessage.includes('春节') || lowerMessage.includes('清明') || const isHoliday = lowerMessage.includes('春节') || lowerMessage.includes('元宵') ||
lowerMessage.includes('劳动节') || lowerMessage.includes('国庆') || lowerMessage.includes('清明') || lowerMessage.includes('端午') ||
lowerMessage.includes('元旦') || lowerMessage.includes('中秋') || lowerMessage.includes('七夕') || lowerMessage.includes('中元') ||
lowerMessage.includes('端午'); lowerMessage.includes('中秋') || lowerMessage.includes('重阳') ||
lowerMessage.includes('腊八') || lowerMessage.includes('小年') ||
lowerMessage.includes('除夕') || lowerMessage.includes('劳动节') ||
lowerMessage.includes('国庆') || lowerMessage.includes('元旦') ||
lowerMessage.includes('妇女节') || lowerMessage.includes('儿童节') ||
lowerMessage.includes('建军节') || lowerMessage.includes('教师节') ||
lowerMessage.includes('圣诞节') || lowerMessage.includes('情人节');
// Detect priority (color) // Detect priority (color)
let priority: 'none' | 'red' | 'green' | 'yellow' = 'none'; let priority: 'none' | 'red' | 'green' | 'yellow' = 'none';
@ -209,15 +321,6 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
priority = 'yellow'; priority = 'yellow';
} }
// Detect content (after colon or special markers)
let content = '';
if (message.includes('') || message.includes(':')) {
const parts = message.split(/[:]/);
if (parts.length > 1) {
content = parts[1].trim();
}
}
// Detect repeat type // Detect repeat type
let repeatType: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none' = 'none'; let repeatType: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none' = 'none';
if (lowerMessage.includes('每天') || lowerMessage.includes('每日')) { if (lowerMessage.includes('每天') || lowerMessage.includes('每日')) {
@ -230,17 +333,105 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
repeatType = 'yearly'; repeatType = 'yearly';
} }
// Extract title (simple heuristic - first phrase before date or colon) // Detect reminder times (提前提醒)
let title = message.split(/[:]/)[0] || message; let reminderTimes: string[] = [];
title = title.split(/[\s\d\u4e00-\u9fa5]+/)[0] || title.substring(0, 50); // 提前X天提醒
const daysMatch = lowerMessage.match(/提前(\d+)天/);
if (daysMatch) {
const days = parseInt(daysMatch[1]);
const reminderDate = new Date(targetDate);
reminderDate.setDate(reminderDate.getDate() - days);
reminderTimes.push(reminderDate.toISOString());
}
// 提前X小时提醒
const hoursMatch = lowerMessage.match(/提前(\d+)小时?/);
if (hoursMatch) {
const hours = parseInt(hoursMatch[1]);
const reminderDate = new Date(targetDate);
reminderDate.setHours(reminderDate.getHours() - hours);
reminderTimes.push(reminderDate.toISOString());
}
// 提前X分钟提醒
const minutesMatch = lowerMessage.match(/提前(\d+)分钟/);
if (minutesMatch) {
const minutes = parseInt(minutesMatch[1]);
const reminderDate = new Date(targetDate);
reminderDate.setMinutes(reminderDate.getMinutes() - minutes);
reminderTimes.push(reminderDate.toISOString());
}
// 默认提醒(不提前)
if (reminderTimes.length === 0) {
reminderTimes = [];
}
// Mock date (tomorrow at 3 PM local time by default for China UTC+8) // Extract title - 智能提取标题
const tomorrow = new Date(); let title = message;
tomorrow.setDate(tomorrow.getDate() + 1); let extractedContent = '';
tomorrow.setHours(15, 0, 0, 0); // 默认下午3点中国时区
// 去除常见的开头
const prefixes = ['帮我', '提醒我', '提醒', '请帮我', '我要', '我想', '帮我提醒'];
for (const prefix of prefixes) {
if (title.toLowerCase().startsWith(prefix)) {
title = title.slice(prefix.length);
break;
}
}
// 处理"提醒XXX提醒时间YYY"这种格式 - 提取标题和内容
// 查找最后一个"提醒"或"在"后面的内容作为内容
const reminderMatch = message.match(/(?:提醒|在|于)\s*(.+)$/);
if (reminderMatch && reminderMatch[1]) {
extractedContent = reminderMatch[1].trim();
}
// 去除冒号后面的内容
if (title.includes('') || title.includes(':')) {
const colonParts = title.split(/[:]/);
title = colonParts[0].trim();
if (!extractedContent && colonParts.length > 1) {
extractedContent = colonParts.slice(1).join(':').trim();
}
}
// 去除日期和时间相关文字
// 去除"明天"、"后天"、"下周"等相对日期
title = title.replace(/(?:明天|后天|今天|下周|下个月|明年|今年|去年)/g, '');
// 去除具体日期如"3月8日"、"2024年5月1日"
title = title.replace(/\d{1,4}[年/-]\d{1,2}[月/-]\d{1,2}[日号]?/g, '');
title = title.replace(/[\d一二三四五六七八九十]+[月日号]/g, '');
// 去除星期几
title = title.replace(/星期[一二三四五六日天]/g, '');
title = title.replace(/周一|周二|周三|周四|周五|周六|周日/g, '');
// 去除时间如"上午9点"、"下午3点"、"14:30"
title = title.replace(/(?:上午|中午|下午|晚上)?\s*\d{1,2}(?::\d{2})?\s*(?:点|时)/g, '');
// 去除"在"、"于"等介词
title = title.replace(/^[在于是于]\s*/g, '');
// 去除多余空格
title = title.trim();
// 如果标题为空或太短,使用原始消息
if (title.length < 2) {
title = message.substring(0, 30);
}
// 限制标题长度
title = title.substring(0, 50);
// Mock date (tomorrow at 9 AM local time by default for China UTC+8)
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() + 1);
targetDate.setHours(9, 0, 0, 0); // 默认上午9点中国时区
// 检测相对日期
if (lowerMessage.includes('今天')) {
targetDate.setDate(new Date().getDate());
} else if (lowerMessage.includes('后天')) {
targetDate.setDate(new Date().getDate() + 2);
} else if (lowerMessage.includes('下周')) {
targetDate.setDate(new Date().getDate() + 7);
}
// 检测消息中的时间(如"下午3点"、"下午四点"、"14:30"等) // 检测消息中的时间(如"下午3点"、"下午四点"、"14:30"等)
const timePattern = message.match(/(?:下午|晚上)?(\d{1,2})(?::(\d{2}))?\s*(?:点|时)/); const timePattern = message.match(/(?:上午|中午|下午|晚上)?(\d{1,2})(?::(\d{2}))?\s*(?:点|时)/);
if (timePattern) { if (timePattern) {
let hours = parseInt(timePattern[1]); let hours = parseInt(timePattern[1]);
const minutes = timePattern[2] ? parseInt(timePattern[2]) : 0; const minutes = timePattern[2] ? parseInt(timePattern[2]) : 0;
@ -248,14 +439,18 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
if (message.includes('下午') || message.includes('晚上')) { if (message.includes('下午') || message.includes('晚上')) {
if (hours < 12) hours += 12; if (hours < 12) hours += 12;
} }
tomorrow.setHours(hours, minutes, 0, 0); // 中午时间
if (message.includes('中午') && hours < 12) {
hours = 12;
}
targetDate.setHours(hours, minutes, 0, 0);
} }
// Build parsed result // Build parsed result
const parsed: any = { const parsed: any = {
type: isReminder ? 'reminder' : 'anniversary', type: eventType,
title: title.trim(), title: title.trim(),
date: tomorrow.toISOString(), date: targetDate.toISOString(),
timezone: 'Asia/Shanghai', // 默认使用中国时区 timezone: 'Asia/Shanghai', // 默认使用中国时区
is_lunar: isLunar, is_lunar: isLunar,
repeat_type: repeatType, repeat_type: repeatType,
@ -265,11 +460,14 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
if (isHoliday) { if (isHoliday) {
parsed.is_holiday = true; parsed.is_holiday = true;
} }
if (isReminder && priority !== 'none') { if (eventType === 'reminder' && priority !== 'none') {
parsed.priority = priority; parsed.priority = priority;
} }
if (isReminder && content) { if (eventType === 'reminder' && extractedContent) {
parsed.content = content; parsed.content = extractedContent;
}
if (eventType === 'reminder' && reminderTimes.length > 0) {
parsed.reminder_times = reminderTimes;
} }
// Generate response // Generate response
@ -281,9 +479,11 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
none: '不重复' none: '不重复'
}[repeatType]; }[repeatType];
let responseMsg = `我帮你记的是: const typeText = eventType === 'anniversary' ? '纪念日' : '提醒';
let responseMsg = `我帮你创建的是:
**${title.trim()}** **${title.trim()}**
📅 ${tomorrow.toLocaleDateString('zh-CN')}`; 📅 ${targetDate.toLocaleDateString('zh-CN')} ${targetDate.getHours()}:${String(targetDate.getMinutes()).padStart(2, '0')}`;
if (repeatType !== 'none') { if (repeatType !== 'none') {
responseMsg += `\n🔄 ${repeatText}`; responseMsg += `\n🔄 ${repeatText}`;
} }
@ -293,7 +493,7 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
if (isHoliday) { if (isHoliday) {
responseMsg += `\n🎉 节假日`; responseMsg += `\n🎉 节假日`;
} }
if (isReminder && priority !== 'none') { if (eventType === 'reminder' && priority !== 'none') {
const colorText = { red: '红色', green: '绿色', yellow: '黄色', none: '无' }; const colorText = { red: '红色', green: '绿色', yellow: '黄色', none: '无' };
responseMsg += `\n🏷 ${colorText[priority]}`; responseMsg += `\n🏷 ${colorText[priority]}`;
} }