function parseJsonReply(reply) { const text = String(reply ?? '').trim(); if (!text) return null; const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = fenced?.[1] ?? text; try { return JSON.parse(candidate); } catch { const start = candidate.indexOf('{'); const end = candidate.lastIndexOf('}'); if (start < 0 || end <= start) return null; try { return JSON.parse(candidate.slice(start, end + 1)); } catch { return null; } } } function boundedConfidence(value, fallback = 0.7) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.min(1, Math.max(0, parsed)); } function normalizeHourMinute(hour, minute, { defaultHour, defaultMinute }) { const safeHour = Number(hour); const safeMinute = Number(minute ?? 0); if (Number.isInteger(safeHour) && safeHour >= 0 && safeHour <= 23 && Number.isInteger(safeMinute) && safeMinute >= 0 && safeMinute <= 59) { return { hour: safeHour, minute: safeMinute }; } if (Number.isInteger(defaultHour) && Number.isInteger(defaultMinute)) { return { hour: defaultHour, minute: defaultMinute }; } return null; } export function normalizeSubscribeMorningLlmIntent(raw, { phase = 'active', defaultHour = 8, defaultMinute = 0, minConfidence = 0.65, } = {}) { const actionRaw = String(raw?.action ?? 'none').trim().toLowerCase(); const allowed = new Set(['confirm', 'modify', 'cancel', 'clarify', 'none']); const action = allowed.has(actionRaw) ? actionRaw : 'none'; const confidence = boundedConfidence(raw?.confidence); if (action === 'none' || confidence < minConfidence) { return { action: 'none', confidence, message: null, hour: null, minute: null }; } const time = normalizeHourMinute(raw?.hour, raw?.minute, { defaultHour, defaultMinute }); const message = String(raw?.message ?? '').trim() || null; if (action === 'clarify') { return { action, confidence, message: message || '请补充一下具体时间,例如「1 7点」或「早安改到 7 点」。', hour: null, minute: null, }; } if (action === 'cancel') { return { action, confidence, message: null, hour: null, minute: null }; } if (action === 'confirm') { if (phase !== 'pending') return { action: 'none', confidence, message: null, hour: null, minute: null }; const resolved = time ?? normalizeHourMinute(defaultHour, defaultMinute, { defaultHour, defaultMinute }); if (!resolved) return { action: 'none', confidence, message: null, hour: null, minute: null }; return { action, confidence, message: null, ...resolved }; } if (action === 'modify') { if (phase !== 'active' || !time) { return { action: 'clarify', confidence, message: message || '请告诉我想改到几点,例如「早安改到 7 点」。', hour: null, minute: null, }; } return { action, confidence, message: null, ...time }; } return { action: 'none', confidence, message: null, hour: null, minute: null }; } function buildSystemPrompt() { return [ '你是微信服务号「每日早安提醒」助手,只做意图识别,不生成页面。', '只输出 JSON,不要 markdown。', '{"action":"confirm|modify|cancel|clarify|none","hour":7,"minute":30,"confidence":0.0,"message":"可选中文追问"}', 'confirm:用户在关注欢迎语后想开通每日早安推送;可带具体时间。例:「1」「1 7点」「不要8点改7点半」。', 'modify:用户已开通,想改时间。例:「早安改到7点」「改成7点半」。必须给出 hour/minute。', 'cancel:用户明确不要早安提醒。例:「取消早安」「不要早安了」。', 'clarify:想改/想开但时间不清楚,message 用一句中文追问。', 'none:普通聊天、做页面、其它提醒,与早安订阅无关;或无法判断。', 'hour 取 0-23,minute 取 0-59;没提到分钟则 minute=0。', ].join('\n'); } async function withTimeout(promise, timeoutMs, label) { if (!timeoutMs || timeoutMs <= 0) return promise; let timer = null; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error(`${label} timed out after ${timeoutMs}ms`)); }, timeoutMs); timer.unref?.(); }), ]); } finally { if (timer) clearTimeout(timer); } } export async function parseSubscribeMorningIntentWithLlm({ text, phase = 'active', defaultHour = 8, defaultMinute = 0, currentHour = null, currentMinute = null, llmProviderService, modelProviderKeyId = null, model = null, minConfidence = 0.65, timeoutMs = 4000, logger = console, }) { if (!llmProviderService || typeof llmProviderService.createChatCompletion !== 'function') { return null; } try { const result = await withTimeout( llmProviderService.createChatCompletion({ ...(modelProviderKeyId ? { providerKeyId: modelProviderKeyId } : {}), ...(model ? { model } : {}), temperature: 0, messages: [ { role: 'system', content: buildSystemPrompt() }, { role: 'user', content: JSON.stringify({ phase, userMessage: String(text ?? '').trim(), defaultHour, defaultMinute, currentHour, currentMinute, }), }, ], }), timeoutMs, 'subscribe-morning-llm', ); if (!result?.ok) { logger?.warn?.('[wechat-subscribe-morning-llm] skipped:', result?.message ?? 'unknown'); return null; } const parsed = parseJsonReply(result.reply); if (!parsed) return null; return normalizeSubscribeMorningLlmIntent(parsed, { phase, defaultHour, defaultMinute, minConfidence, }); } catch (err) { logger?.warn?.( '[wechat-subscribe-morning-llm] skipped:', err instanceof Error ? err.message : err, ); return null; } } export async function resolveSubscribeMorningLlmIntent({ text, phase = 'active', defaultHour = 8, defaultMinute = 0, currentHour = null, currentMinute = null, wechatSubscribeMorningLlmConfigService = null, llmProviderService = null, logger = console, }) { if (!wechatSubscribeMorningLlmConfigService || !llmProviderService) return null; let enabled = false; let config = null; try { enabled = await wechatSubscribeMorningLlmConfigService.isSubscribeMorningLlmEnabled(); config = await wechatSubscribeMorningLlmConfigService.getConfig(); } catch (err) { logger?.warn?.( '[wechat-subscribe-morning-llm] config load failed:', err instanceof Error ? err.message : err, ); return null; } if (!enabled) return null; return parseSubscribeMorningIntentWithLlm({ text, phase, defaultHour, defaultMinute, currentHour, currentMinute, llmProviderService, modelProviderKeyId: config?.modelProviderKeyId ?? null, model: config?.model ?? null, minConfidence: config?.minConfidence ?? 0.65, timeoutMs: config?.timeoutMs ?? 4000, logger, }); }