feat(wechat): migrate schedule and sync handlers into wechat package

Extract schedule, greeting/status/connectivity replies, chat-general prompt, and display-name helpers from wechat-mp.mjs. Route sync intents via classifyWechatIntent and add channel isolation CI check.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-04 13:48:47 +08:00
parent fafc1fe7fd
commit 031c6e086a
9 changed files with 435 additions and 300 deletions
+37
View File
@@ -0,0 +1,37 @@
export function looksLikeScheduleConfirmation(text) {
const compact = String(text ?? '').replace(/\s+/g, '');
if (!compact) return false;
if (/(没有|没能|未能|无法|不能|失败|需要你|请补充|请告诉)/.test(compact)) return false;
return /(已经|已|现在).{0,12}(设置|安排|记录|加上|添加|创建).{0,12}(待办|提醒|日程|安排|闹钟)|设置好了|已经设置好了|已经加上/.test(
compact,
);
}
export async function guardScheduleConfirmationReply({
replyText,
scheduleService,
userId,
sourceMessageId,
logger,
}) {
if (!scheduleService || !looksLikeScheduleConfirmation(replyText)) return replyText;
const messageId = String(sourceMessageId ?? '').trim();
if (!messageId || typeof scheduleService.listItemsBySourceMessage !== 'function') {
return replyText;
}
const items = await scheduleService
.listItemsBySourceMessage({
userId,
sourceMessageId: messageId,
limit: 5,
})
.catch((err) => {
logger?.warn?.('Schedule confirmation guard failed:', err);
return [];
});
if (items.length > 0) return replyText;
return [
'我刚才没有确认到待办/提醒已经写入系统,所以这次不能算设置成功。',
'请再发一次完整安排,比如“明天早上 6 点跑步,5 点半提醒我”。我会在工具写入成功后再确认。',
].join('\n');
}
+70
View File
@@ -0,0 +1,70 @@
import { isScheduleIntent, parseScheduleIntent } from '../../schedule-intent.mjs';
export async function handleWechatScheduleIntent({ intent, user, scheduleService }) {
if (!scheduleService) return null;
const scheduleIntent = parseScheduleIntent(intent.agentText);
if (!isScheduleIntent(scheduleIntent)) return null;
const timezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
if (scheduleIntent.action === 'create_todo') {
if (scheduleIntent.needsClarification?.includes('todo_title')) {
return '可以。你想让我记哪一条待办?例如“帮我记一下 跟段吃饭”。';
}
const item = await scheduleService.createItem({
userId: user.userId,
kind: 'task',
title: scheduleIntent.title,
timezone,
sourceChannel: 'wechat',
sourceMessageId: intent.msgId || null,
sourceText: intent.agentText,
metadata: {
source: 'wechat_mp',
},
});
return `已记录到待办列表:${item.title},未设置提醒。`;
}
if (scheduleIntent.action === 'create_daily_todo_digest') {
if (scheduleIntent.needsClarification?.includes('digest_time')) {
return '可以。你想每天几点收到当天待办记录?比如“每天早上 7 点发给我”。';
}
const subscription = await scheduleService.createDailyTodoDigest({
userId: user.userId,
hour: scheduleIntent.hour,
minute: scheduleIntent.minute,
timezone,
channel: 'wechat',
sourceChannel: 'wechat',
sourceMessageId: intent.msgId || null,
sourceText: intent.agentText,
});
const minuteText = subscription.minute === 0 ? '' : `${String(subscription.minute).padStart(2, '0')}`;
return `已设置:我会每天早上 ${subscription.hour}${minuteText} 通过服务号把当天待办记录发给你。`;
}
if (scheduleIntent.action === 'create_balance_alert') {
if (scheduleIntent.needsClarification?.includes('threshold')) {
return '可以。你想在余额低于多少时提醒我?例如“余额低于 20 元提醒我”。';
}
const subscription = await scheduleService.createBalanceLowAlert({
userId: user.userId,
thresholdCents: scheduleIntent.thresholdCents,
channel: 'wechat',
sourceChannel: 'wechat',
sourceMessageId: intent.msgId || null,
sourceText: intent.agentText,
});
return `已设置:当余额低于 ${(subscription.thresholdCents / 100).toFixed(2)} 元时,我会通过服务号提醒你。`;
}
if (scheduleIntent.action === 'query_schedule') {
return scheduleService.buildTodoDigestText({
userId: user.userId,
timezone,
});
}
return null;
}
+50
View File
@@ -0,0 +1,50 @@
import { resolveWechatAddressName } from '../user/display-name.mjs';
export function buildGreetingText(user) {
const name = resolveWechatAddressName(user);
return name ? `你好,${name}!我在呢,有什么需要?` : '你好!我在呢,有什么需要?';
}
export function buildStatusText(user, fallbackText) {
const name = resolveWechatAddressName(user);
if (!name) return fallbackText;
return `我在这边,${name}。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。`;
}
export function buildConnectivityTestReply(user) {
const name = resolveWechatAddressName(user);
return name
? `${name},公众号消息通道正常,我收到你的测试了。`
: '公众号消息通道正常,我收到你的测试了。';
}
/**
* Resolve an immediate sync XML reply for lightweight intents.
* @returns {string|null}
*/
export function resolveSyncReply(wechatIntent, user, { statusFallbackText = '' } = {}) {
switch (wechatIntent?.kind) {
case 'status.probe':
return buildStatusText(user, statusFallbackText);
case 'greeting':
return buildGreetingText(user);
case 'connectivity.test':
return buildConnectivityTestReply(user);
default:
return null;
}
}
/**
* Whether this intent should be answered synchronously before agent dispatch.
*/
export function shouldHandleSyncReply(intent, wechatIntent) {
const msgType = String(intent?.msgType ?? '').toLowerCase();
if (wechatIntent?.kind === 'connectivity.test') {
return msgType === 'text' || msgType === 'voice';
}
if (wechatIntent?.kind === 'status.probe' || wechatIntent?.kind === 'greeting') {
return msgType === 'text';
}
return false;
}