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:
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user