feat: 优化 AI 解析接口返回友好消息
- 提取 System Prompt 为常量避免模板解析问题 - 优化 Mock 解析器支持农历和节假日识别 - 返回友好的 response 文本而非 JSON 结构 - 支持每年重复类型 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
875f902ac3
commit
dd21c03e06
209
src/routes/ai.ts
209
src/routes/ai.ts
@ -6,17 +6,68 @@ import { asyncHandler } from '../middleware/errorHandler';
|
||||
|
||||
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({
|
||||
message: z.string().min(1, 'Message is required').max(1000),
|
||||
});
|
||||
|
||||
// AI parsed event validation schema
|
||||
// AI parsed event validation schema - 扩展支持所有字段
|
||||
const parsedEventSchema = z.object({
|
||||
type: z.enum(['anniversary', 'reminder']),
|
||||
title: z.string().min(1).max(200),
|
||||
date: z.string().datetime(),
|
||||
timezone: z.string(), // 时区信息
|
||||
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
|
||||
@ -61,6 +112,12 @@ async function callDeepSeek(message: string): Promise<{
|
||||
}
|
||||
|
||||
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, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -72,18 +129,7 @@ async function callDeepSeek(message: string): Promise<{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are a helpful assistant that helps users create events (anniversaries or reminders) from natural language.
|
||||
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"
|
||||
}`
|
||||
content: SYSTEM_PROMPT
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
@ -109,9 +155,19 @@ async function callDeepSeek(message: string): Promise<{
|
||||
const parsed = rawParsed.parsed || rawParsed;
|
||||
// Validate parsed data with Zod schema
|
||||
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 {
|
||||
parsed: validated,
|
||||
response: parsed.response || content,
|
||||
response: `已为你创建${typeText}「${validated.title}」,日期:${dateText}${repeatText}`,
|
||||
};
|
||||
} catch (e) {
|
||||
// Schema validation failed, use mock response
|
||||
@ -135,35 +191,116 @@ function mockParseResponse(message: string): { parsed: any; response: string } {
|
||||
const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我');
|
||||
|
||||
// Detect lunar date
|
||||
const isLunar = lowerMessage.includes('农历');
|
||||
const isLunar = lowerMessage.includes('农历') || lowerMessage.includes('阴历');
|
||||
|
||||
// Detect repeat type
|
||||
let repeatType = 'none';
|
||||
if (lowerMessage.includes('每年') || lowerMessage.includes(' yearly') || lowerMessage.includes('周年')) {
|
||||
repeatType = 'yearly';
|
||||
} else if (lowerMessage.includes('每月') || lowerMessage.includes(' monthly')) {
|
||||
repeatType = 'monthly';
|
||||
// Detect holiday
|
||||
const isHoliday = lowerMessage.includes('春节') || lowerMessage.includes('清明') ||
|
||||
lowerMessage.includes('劳动节') || lowerMessage.includes('国庆') ||
|
||||
lowerMessage.includes('元旦') || lowerMessage.includes('中秋') ||
|
||||
lowerMessage.includes('端午');
|
||||
|
||||
// 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)
|
||||
const title = message.split(/[\s\d\u4e00-\u9fa5]+/)[0] || message.substring(0, 50);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
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 {
|
||||
parsed: {
|
||||
type: isReminder ? 'reminder' : 'anniversary',
|
||||
title: title.trim(),
|
||||
date: tomorrow.toISOString(),
|
||||
is_lunar: isLunar,
|
||||
repeat_type: repeatType,
|
||||
},
|
||||
response: `我帮你记的是:
|
||||
**${title.trim()}**
|
||||
📅 ${tomorrow.toLocaleDateString('zh-CN')}
|
||||
🔄 ${repeatType === 'yearly' ? '每年重复' : repeatType === 'monthly' ? '每月重复' : '不重复'}`
|
||||
parsed,
|
||||
response: responseMsg
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user