From dd21c03e06a11bc78e9c2d0bc9ef39ced76a8e70 Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Fri, 13 Feb 2026 11:55:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20AI=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E5=8F=8B=E5=A5=BD?= =?UTF-8?q?=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取 System Prompt 为常量避免模板解析问题 - 优化 Mock 解析器支持农历和节假日识别 - 返回友好的 response 文本而非 JSON 结构 - 支持每年重复类型 Co-Authored-By: Claude Opus 4.5 --- src/routes/ai.ts | 209 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 173 insertions(+), 36 deletions(-) diff --git a/src/routes/ai.ts b/src/routes/ai.ts index eb77571..2988343 100644 --- a/src/routes/ai.ts +++ b/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 }; }