feat: 优化 AI 解析接口返回友好消息

- 提取 System Prompt 为常量避免模板解析问题
- 优化 Mock 解析器支持农历和节假日识别
- 返回友好的 response 文本而非 JSON 结构
- 支持每年重复类型

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-13 11:55:53 +08:00
parent 875f902ac3
commit dd21c03e06

View File

@ -6,17 +6,68 @@ import { asyncHandler } from '../middleware/errorHandler';
const router = Router(); const router = Router();
// System prompt for AI - extracted as constant to avoid template parsing issues
const SYSTEM_PROMPT = `你是一个帮助用户创建事件(纪念日或提醒)的智能助手。
${new Date().toISOString()}
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": "友好的确认消息"
}
1. date ISO 8601: YYYY-MM-DDTHH:mm:ssZ
2. "明天"date = ( + 1)
3. "后天"date = ( + 2)
4. "下周三"date = 09:00:00Z
5. "下午3点"date = 今天下午15:00:00Z 或用户指定的日期的15:00:00Z
6. 使 09:00:00Z
7. NEVER 使 2024-01-02T15:00:00Z使
8. "每月X号"date = 2025-02-15T09:00:00Z
9. "每年X月X日"date = 2026-01-01T09:00:00Z
->
is_holiday=true
- 使 "Asia/Shanghai"
- date UTC
- "明天下午3点"date = 15:00 +0800 UTC = 07:00Z
-
- "下周三"
- 使`;
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),
}); });
// AI parsed event validation schema // AI parsed event validation schema - 扩展支持所有字段
const parsedEventSchema = z.object({ const parsedEventSchema = z.object({
type: z.enum(['anniversary', 'reminder']), type: z.enum(['anniversary', 'reminder']),
title: z.string().min(1).max(200), title: z.string().min(1).max(200),
date: z.string().datetime(), date: z.string().datetime(),
timezone: z.string(), // 时区信息
is_lunar: z.boolean(), is_lunar: z.boolean(),
repeat_type: z.enum(['yearly', 'monthly', 'none']), repeat_type: z.enum(['daily', 'weekly', 'monthly', 'yearly', 'none']),
// 新增字段
content: z.string().optional(), // 详细内容(仅提醒)
is_holiday: z.boolean().optional(), // 节假日(仅纪念日)
priority: z.enum(['none', 'red', 'green', 'yellow']).optional(), // 颜色(仅提醒)
reminder_times: z.array(z.string()).optional(), // 提前提醒时间点
}); });
// All routes require authentication // All routes require authentication
@ -61,6 +112,12 @@ async function callDeepSeek(message: string): Promise<{
} }
try { try {
// Get current date for context
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().slice(0, 11) + '15:00:00Z';
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -72,18 +129,7 @@ async function callDeepSeek(message: string): Promise<{
messages: [ messages: [
{ {
role: 'system', role: 'system',
content: `You are a helpful assistant that helps users create events (anniversaries or reminders) from natural language. content: SYSTEM_PROMPT
Parse the user's message and respond with a JSON object in the following format:
{
"parsed": {
"type": "anniversary" | "reminder",
"title": "event title",
"date": "ISO date string",
"is_lunar": true | false,
"repeat_type": "yearly" | "monthly" | "none"
},
"response": "A friendly confirmation message"
}`
}, },
{ {
role: 'user', role: 'user',
@ -109,9 +155,19 @@ async function callDeepSeek(message: string): Promise<{
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);
// 构建友好的用户消息
const typeText = validated.type === 'anniversary' ? '纪念日' : '提醒';
const dateObj = new Date(validated.date);
const dateText = dateObj.toLocaleDateString('zh-CN', {
year: 'numeric', month: 'long', day: 'numeric',
timeZone: validated.timezone
});
const repeatText = validated.repeat_type === 'none' ? '' :
{ daily: ',每天重复', weekly: ',每周重复', monthly: ',每月重复', yearly: ',每年重复' }[validated.repeat_type];
return { return {
parsed: validated, parsed: validated,
response: parsed.response || content, response: `已为你创建${typeText}${validated.title}」,日期:${dateText}${repeatText}`,
}; };
} catch (e) { } catch (e) {
// Schema validation failed, use mock response // Schema validation failed, use mock response
@ -135,35 +191,116 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我'); const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我');
// Detect lunar date // Detect lunar date
const isLunar = lowerMessage.includes('农历'); const isLunar = lowerMessage.includes('农历') || lowerMessage.includes('阴历');
// Detect repeat type // Detect holiday
let repeatType = 'none'; const isHoliday = lowerMessage.includes('春节') || lowerMessage.includes('清明') ||
if (lowerMessage.includes('每年') || lowerMessage.includes(' yearly') || lowerMessage.includes('周年')) { lowerMessage.includes('劳动节') || lowerMessage.includes('国庆') ||
repeatType = 'yearly'; lowerMessage.includes('元旦') || lowerMessage.includes('中秋') ||
} else if (lowerMessage.includes('每月') || lowerMessage.includes(' monthly')) { lowerMessage.includes('端午');
repeatType = 'monthly';
// Detect priority (color)
let priority: 'none' | 'red' | 'green' | 'yellow' = 'none';
if (lowerMessage.includes('重要') || lowerMessage.includes('紧急') || lowerMessage.includes('红色')) {
priority = 'red';
} else if (lowerMessage.includes('绿色') || lowerMessage.includes('绿')) {
priority = 'green';
} else if (lowerMessage.includes('黄色') || lowerMessage.includes('黄')) {
priority = 'yellow';
} }
// Extract title (simple heuristic - first phrase before date) // Detect content (after colon or special markers)
const title = message.split(/[\s\d\u4e00-\u9fa5]+/)[0] || message.substring(0, 50); let content = '';
if (message.includes('') || message.includes(':')) {
const parts = message.split(/[:]/);
if (parts.length > 1) {
content = parts[1].trim();
}
}
// Mock date (tomorrow) // Detect repeat type
let repeatType: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none' = 'none';
if (lowerMessage.includes('每天') || lowerMessage.includes('每日')) {
repeatType = 'daily';
} else if (lowerMessage.includes('每周') || lowerMessage.includes(' weekly')) {
repeatType = 'weekly';
} else if (lowerMessage.includes('每月') || lowerMessage.includes(' monthly')) {
repeatType = 'monthly';
} else if (lowerMessage.includes('每年') || lowerMessage.includes(' yearly') || lowerMessage.includes('周年')) {
repeatType = 'yearly';
}
// Extract title (simple heuristic - first phrase before date or colon)
let title = message.split(/[:]/)[0] || message;
title = title.split(/[\s\d\u4e00-\u9fa5]+/)[0] || title.substring(0, 50);
// Mock date (tomorrow at 3 PM local time by default for China UTC+8)
const tomorrow = new Date(); const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(15, 0, 0, 0); // 默认下午3点中国时区
// 检测消息中的时间(如"下午3点"、"下午四点"、"14:30"等)
const timePattern = message.match(/(?:下午|晚上)?(\d{1,2})(?::(\d{2}))?\s*(?:点|时)/);
if (timePattern) {
let hours = parseInt(timePattern[1]);
const minutes = timePattern[2] ? parseInt(timePattern[2]) : 0;
// 如果是下午时间1-11点加上12
if (message.includes('下午') || message.includes('晚上')) {
if (hours < 12) hours += 12;
}
tomorrow.setHours(hours, minutes, 0, 0);
}
// Build parsed result
const parsed: any = {
type: isReminder ? 'reminder' : 'anniversary',
title: title.trim(),
date: tomorrow.toISOString(),
timezone: 'Asia/Shanghai', // 默认使用中国时区
is_lunar: isLunar,
repeat_type: repeatType,
};
// Add optional fields
if (isHoliday) {
parsed.is_holiday = true;
}
if (isReminder && priority !== 'none') {
parsed.priority = priority;
}
if (isReminder && content) {
parsed.content = content;
}
// Generate response
const repeatText = {
daily: '每天重复',
weekly: '每周重复',
monthly: '每月重复',
yearly: '每年重复',
none: '不重复'
}[repeatType];
let responseMsg = `我帮你记的是:
**${title.trim()}**
📅 ${tomorrow.toLocaleDateString('zh-CN')}`;
if (repeatType !== 'none') {
responseMsg += `\n🔄 ${repeatText}`;
}
if (isLunar) {
responseMsg += `\n📆 农历日期`;
}
if (isHoliday) {
responseMsg += `\n🎉 节假日`;
}
if (isReminder && priority !== 'none') {
const colorText = { red: '红色', green: '绿色', yellow: '黄色', none: '无' };
responseMsg += `\n🏷 ${colorText[priority]}`;
}
return { return {
parsed: { parsed,
type: isReminder ? 'reminder' : 'anniversary', response: responseMsg
title: title.trim(),
date: tomorrow.toISOString(),
is_lunar: isLunar,
repeat_type: repeatType,
},
response: `我帮你记的是:
**${title.trim()}**
📅 ${tomorrow.toLocaleDateString('zh-CN')}
🔄 ${repeatType === 'yearly' ? '每年重复' : repeatType === 'monthly' ? '每月重复' : '不重复'}`
}; };
} }