Files
memind/wechat-subscribe-morning-llm.mjs
T
john e42417bd6e feat(wechat): add subscribe morning reminder, plaza welcome, and LLM fallback
Enable daily morning greeting on reply 1 (with custom time, modify, and cancel),
random greeting delivery, M发现 in subscribe welcome, and optional LLM parsing when
rules miss. Also fix WeChat MP draft/config error passthrough and add Tang E2E scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:32:10 +08:00

229 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-23minute 取 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,
});
}