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:
@@ -57,6 +57,7 @@
|
|||||||
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
||||||
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
||||||
"verify:mindspace-publish-guards:full": "node scripts/verify-mindspace-publish-guards.mjs --with-runtime",
|
"verify:mindspace-publish-guards:full": "node scripts/verify-mindspace-publish-guards.mjs --with-runtime",
|
||||||
|
"verify:wechat-channel-isolation": "node scripts/check-wechat-channel-isolation.mjs",
|
||||||
"test:mindspace-e2e": "node scripts/mindspace-e2e.mjs",
|
"test:mindspace-e2e": "node scripts/mindspace-e2e.mjs",
|
||||||
"test:mindspace-pages-e2e": "node scripts/mindspace-pages-e2e.mjs",
|
"test:mindspace-pages-e2e": "node scripts/mindspace-pages-e2e.mjs",
|
||||||
"test:mindspace-publications-e2e": "node scripts/mindspace-publications-e2e.mjs",
|
"test:mindspace-publications-e2e": "node scripts/mindspace-publications-e2e.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const wechatRoot = path.join(root, 'wechat');
|
||||||
|
const forbiddenWechatImports = [
|
||||||
|
'mindspace-public-finish-sync.mjs',
|
||||||
|
'chat-finish-sync.mjs',
|
||||||
|
'useTKMindChat',
|
||||||
|
'src/hooks/useTKMindChat',
|
||||||
|
];
|
||||||
|
const forbiddenH5ImportPatterns = [
|
||||||
|
/from\s+['"][^'"]*\/wechat\//,
|
||||||
|
/import\s*\(\s*['"][^'"]*\/wechat\//,
|
||||||
|
/require\s*\(\s*['"][^'"]*\/wechat\//,
|
||||||
|
];
|
||||||
|
|
||||||
|
function walk(dir, acc = []) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full, acc);
|
||||||
|
else if (entry.name.endsWith('.mjs') || entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) acc.push(full);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkWechatImports() {
|
||||||
|
const violations = [];
|
||||||
|
for (const file of walk(wechatRoot)) {
|
||||||
|
const text = fs.readFileSync(file, 'utf8');
|
||||||
|
for (const token of forbiddenWechatImports) {
|
||||||
|
if (text.includes(token)) violations.push({ file, token });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkH5ImportsWechat() {
|
||||||
|
const violations = [];
|
||||||
|
const h5Paths = [
|
||||||
|
path.join(root, 'src'),
|
||||||
|
path.join(root, 'chat-finish-sync.mjs'),
|
||||||
|
path.join(root, 'mindspace-public-finish-sync.mjs'),
|
||||||
|
].filter((p) => fs.existsSync(p));
|
||||||
|
|
||||||
|
for (const base of h5Paths) {
|
||||||
|
const files = fs.statSync(base).isDirectory() ? walk(base) : [base];
|
||||||
|
for (const file of files) {
|
||||||
|
const text = fs.readFileSync(file, 'utf8');
|
||||||
|
for (const pattern of forbiddenH5ImportPatterns) {
|
||||||
|
if (pattern.test(text)) violations.push({ file, token: pattern.source });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wechatViolations = checkWechatImports();
|
||||||
|
const h5Violations = checkH5ImportsWechat();
|
||||||
|
|
||||||
|
if (wechatViolations.length === 0 && h5Violations.length === 0) {
|
||||||
|
console.log('wechat channel isolation check passed');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of wechatViolations) {
|
||||||
|
console.error(`wechat forbidden import: ${item.token} in ${path.relative(root, item.file)}`);
|
||||||
|
}
|
||||||
|
for (const item of h5Violations) {
|
||||||
|
console.error(`h5 forbidden import: ${item.token} in ${path.relative(root, item.file)}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
+40
-300
@@ -5,23 +5,31 @@ import { fetch as undiciFetch } from 'undici';
|
|||||||
import { developerToolsFromPolicy } from './capabilities.mjs';
|
import { developerToolsFromPolicy } from './capabilities.mjs';
|
||||||
import { mergeMessageContent } from './message-stream.mjs';
|
import { mergeMessageContent } from './message-stream.mjs';
|
||||||
import { reconcileAgentSession } from './session-reconcile.mjs';
|
import { reconcileAgentSession } from './session-reconcile.mjs';
|
||||||
import { isScheduleIntent, parseScheduleIntent, shouldUseScheduleAssistant } from './schedule-intent.mjs';
|
|
||||||
import { isStubPublicHtmlContent, materializeMissingPublicHtmlWrites } from './mindspace-public-finish-sync.mjs';
|
import { isStubPublicHtmlContent, materializeMissingPublicHtmlWrites } from './mindspace-public-finish-sync.mjs';
|
||||||
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
|
||||||
import { buildCurrentTimeAgentPrefix } from './user-memory-profile.mjs';
|
|
||||||
import { buildPublicUrl, PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
import { buildPublicUrl, PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||||
import { buildAutoChatSkillPrefix } from './chat-skills.mjs';
|
|
||||||
import { downloadTemporaryMedia, persistWechatImage } from './wechat-media.mjs';
|
import { downloadTemporaryMedia, persistWechatImage } from './wechat-media.mjs';
|
||||||
|
import { normalizeWechatName, resolveWechatAddressName } from './wechat/user/display-name.mjs';
|
||||||
import { buildAckText } from './wechat/ack/ack-provider.mjs';
|
import { buildAckText } from './wechat/ack/ack-provider.mjs';
|
||||||
|
import { guardScheduleConfirmationReply } from './wechat/handlers/schedule-guard.mjs';
|
||||||
|
import { handleWechatScheduleIntent } from './wechat/handlers/schedule.mjs';
|
||||||
|
import {
|
||||||
|
buildStatusText,
|
||||||
|
resolveSyncReply,
|
||||||
|
shouldHandleSyncReply,
|
||||||
|
} from './wechat/handlers/sync-replies.mjs';
|
||||||
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
||||||
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
|
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
|
||||||
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
||||||
|
import { buildWechatAgentPrompt } from './wechat/prompts/chat-general.mjs';
|
||||||
import {
|
import {
|
||||||
buildPageGenerateAgentPrompt,
|
buildPageGenerateAgentPrompt,
|
||||||
buildPagePublishFailureText,
|
buildPagePublishFailureText,
|
||||||
} from './wechat/prompts/page-generate.mjs';
|
} from './wechat/prompts/page-generate.mjs';
|
||||||
import { selectSendableHtmlArtifacts } from './wechat/verify/page-artifact.mjs';
|
import { selectSendableHtmlArtifacts } from './wechat/verify/page-artifact.mjs';
|
||||||
|
|
||||||
|
export { buildWechatAgentPrompt };
|
||||||
|
|
||||||
const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
|
const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
|
||||||
const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
|
const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
|
||||||
'https://api.weixin.qq.com/cgi-bin/message/custom/send';
|
'https://api.weixin.qq.com/cgi-bin/message/custom/send';
|
||||||
@@ -951,25 +959,6 @@ export function shouldRetryHtmlGenerationReply({
|
|||||||
return !hasAnyUrl(reply?.text);
|
return !hasAnyUrl(reply?.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isQuestionStatusProbe(text) {
|
|
||||||
return /^[??]+$/.test(String(text ?? '').trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSimpleGreeting(text) {
|
|
||||||
const normalized = String(text ?? '')
|
|
||||||
.trim()
|
|
||||||
.replace(/[!!。.\s]+$/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
return /^(你好|您好|在吗|在不在|嗨|hi|hello|hey)$/.test(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isConnectivityTest(text) {
|
|
||||||
const normalized = String(text ?? '')
|
|
||||||
.trim()
|
|
||||||
.replace(/[!!。.\s]+$/g, '');
|
|
||||||
return /^(测试\s*\d*|test\s*\d*)$/i.test(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTopicResetIntent(text) {
|
function isTopicResetIntent(text) {
|
||||||
return isTopicResetText(text);
|
return isTopicResetText(text);
|
||||||
}
|
}
|
||||||
@@ -1177,153 +1166,6 @@ function normalizeWechatInboundIntent(inbound) {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWechatAgentPrompt(intent, { grantedSkills = [] } = {}) {
|
|
||||||
const msgType = String(intent?.msgType ?? 'text');
|
|
||||||
const autoSkillPrefix =
|
|
||||||
msgType === 'text' || msgType === 'voice'
|
|
||||||
? buildAutoChatSkillPrefix(intent?.agentText ?? intent?.content, grantedSkills)
|
|
||||||
: '';
|
|
||||||
const docxDownloadHint =
|
|
||||||
looksLikeHtmlGenerationIntent(intent?.agentText ?? intent?.content) &&
|
|
||||||
looksLikeDocxDownloadIntent(intent?.agentText ?? intent?.content)
|
|
||||||
? [
|
|
||||||
'【Word 下载要求】用户明确要求页面里可下载 Word / docx。',
|
|
||||||
'开始前必须先调用 `load_skill` → `docx-generate`,并按技能说明生成目标 `public/*.docx`。',
|
|
||||||
'生成后必须确认目标 `.docx` 已存在,再调用 `load_skill` → `static-page-publish` 写 `public/*.html`。',
|
|
||||||
'HTML 中只能用同目录相对路径链接该文档;禁止只写下载按钮却没有先把 `.docx` 落盘。',
|
|
||||||
'',
|
|
||||||
].join('\n')
|
|
||||||
: '';
|
|
||||||
const pagePublishHint = looksLikeHtmlGenerationIntent(intent?.agentText ?? intent?.content)
|
|
||||||
? [
|
|
||||||
'【页面发布技能要求】这条消息是在生成可访问 HTML 页面。',
|
|
||||||
'开始前必须先调用 `load_skill` → `static-page-publish`,不能省略,也不能写完页面后再补调。',
|
|
||||||
'随后必须用 sandbox-fs 的 `write_file` / `edit_file` 写入 `public/*.html`。',
|
|
||||||
'禁止用 shell / cat / heredoc / echo / cp 写入 HTML:shell 在容器内执行,文件不会出现在公网 MindSpace 路径,用户会收到「文件不存在」。',
|
|
||||||
'在回复用户前,确认 `public/` 下目标 HTML 已通过 write_file 落盘;没有落盘就不要发送链接或说「已发布」。',
|
|
||||||
'最终只给用户一个正式域名的唯一正确链接;不要输出错误域名、备用链接或让用户手动保存文件。',
|
|
||||||
'',
|
|
||||||
].join('\n')
|
|
||||||
: '';
|
|
||||||
const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
|
|
||||||
const currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone });
|
|
||||||
const scheduleAssistantHint = shouldUseScheduleAssistant(intent?.agentText ?? intent?.content)
|
|
||||||
? [
|
|
||||||
'【日程技能要求】这条消息涉及待办、提醒或日程。',
|
|
||||||
'开始前先加载 `schedule-assistant` skill,并严格按 skill 里的边界执行。',
|
|
||||||
'写入工具时优先使用 startLocal / endLocal / remindLocal(YYYY-MM-DD HH:mm),不要自行估算 Unix 毫秒。',
|
|
||||||
'只有在 `schedule_create_item` / `schedule_create_reminder` 等工具成功返回后,才能告诉用户“已经设置好了”。',
|
|
||||||
intent?.msgId ? `调用 schedule_create_item 时必须传入 sourceMessageId: ${intent.msgId}` : '',
|
|
||||||
'',
|
|
||||||
].filter(Boolean).join('\n')
|
|
||||||
: '';
|
|
||||||
if (msgType === 'voice') {
|
|
||||||
return [
|
|
||||||
docxDownloadHint,
|
|
||||||
currentTimeHint,
|
|
||||||
scheduleAssistantHint,
|
|
||||||
'【微信服务号语音消息】用户通过语音输入,以下是微信识别结果。',
|
|
||||||
'',
|
|
||||||
`用户语音识别文本:${autoSkillPrefix}${String(intent?.agentText ?? '').trim()}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (msgType === 'image') {
|
|
||||||
return [
|
|
||||||
docxDownloadHint,
|
|
||||||
currentTimeHint,
|
|
||||||
scheduleAssistantHint,
|
|
||||||
'【微信服务号图片消息】用户发送了图片。',
|
|
||||||
'如用户没有明确要求,请先根据图片内容给出简短理解,并询问下一步。',
|
|
||||||
'',
|
|
||||||
String(intent?.agentText ?? '').trim(),
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (msgType === 'location') {
|
|
||||||
const location = intent?.location ?? {};
|
|
||||||
return [
|
|
||||||
currentTimeHint,
|
|
||||||
scheduleAssistantHint,
|
|
||||||
'【微信服务号位置消息】用户发送了当前位置。',
|
|
||||||
location.label ? `地址:${location.label}` : '',
|
|
||||||
location.latitude !== null && location.latitude !== undefined
|
|
||||||
? `纬度:${location.latitude}`
|
|
||||||
: '',
|
|
||||||
location.longitude !== null && location.longitude !== undefined
|
|
||||||
? `经度:${location.longitude}`
|
|
||||||
: '',
|
|
||||||
'请结合位置回答用户可能的路线、附近、行程或提醒需求;如果意图不明确,先简短询问。',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (msgType === 'link') {
|
|
||||||
const link = intent?.link ?? {};
|
|
||||||
return [
|
|
||||||
currentTimeHint,
|
|
||||||
scheduleAssistantHint,
|
|
||||||
'【微信服务号链接消息】用户分享了链接。',
|
|
||||||
link.title ? `标题:${link.title}` : '',
|
|
||||||
link.description ? `描述:${link.description}` : '',
|
|
||||||
link.url ? `URL:${link.url}` : '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
const content = String(intent?.agentText ?? intent?.content ?? '').trim();
|
|
||||||
const lines = [currentTimeHint];
|
|
||||||
if (docxDownloadHint) lines.push(docxDownloadHint);
|
|
||||||
if (pagePublishHint) lines.push(pagePublishHint);
|
|
||||||
if (scheduleAssistantHint) lines.push(scheduleAssistantHint);
|
|
||||||
lines.push(
|
|
||||||
'【微信服务号新消息】请只回答下面这条用户消息,不要主动延续无关的历史话题。',
|
|
||||||
'若用户只是在测试连通性,请一句话确认收到即可,不要展开旧任务。',
|
|
||||||
'',
|
|
||||||
`用户消息:${autoSkillPrefix}${content}`,
|
|
||||||
);
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildConnectivityTestReply(user) {
|
|
||||||
const name = resolveWechatAddressName(user);
|
|
||||||
return name
|
|
||||||
? `${name},公众号消息通道正常,我收到你的测试了。`
|
|
||||||
: '公众号消息通道正常,我收到你的测试了。';
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWechatName(value) {
|
|
||||||
const name = String(value ?? '').trim();
|
|
||||||
if (!name || /^wx_[a-z0-9_]{4,64}$/i.test(name)) return '';
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWechatAddressName(user) {
|
|
||||||
return normalizeWechatName(user?.nickname) || normalizeWechatName(user?.displayName) || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGreetingText(user) {
|
|
||||||
const name = resolveWechatAddressName(user);
|
|
||||||
return name ? `你好,${name}!我在呢,有什么需要?` : '你好!我在呢,有什么需要?';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStatusText(user, fallbackText) {
|
|
||||||
const name = resolveWechatAddressName(user);
|
|
||||||
if (!name) return fallbackText;
|
|
||||||
return `我在这边,${name}。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function successResponse(task = null) {
|
function successResponse(task = null) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -1967,28 +1809,14 @@ export function createWechatMpService({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestId = crypto.randomUUID();
|
const requestId = crypto.randomUUID();
|
||||||
const guardScheduleReply = async (replyText) => {
|
const guardScheduleReply = (replyText) =>
|
||||||
if (!scheduleService || !looksLikeScheduleConfirmation(replyText)) return replyText;
|
guardScheduleConfirmationReply({
|
||||||
const sourceMessageId = String(intent.msgId ?? '').trim();
|
replyText,
|
||||||
if (!sourceMessageId || typeof scheduleService.listItemsBySourceMessage !== 'function') {
|
scheduleService,
|
||||||
return replyText;
|
userId: user.userId,
|
||||||
}
|
sourceMessageId: intent.msgId,
|
||||||
const items = await scheduleService
|
logger,
|
||||||
.listItemsBySourceMessage({
|
});
|
||||||
userId: user.userId,
|
|
||||||
sourceMessageId,
|
|
||||||
limit: 5,
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.warn?.('Schedule confirmation guard failed:', err);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
if (items.length > 0) return replyText;
|
|
||||||
return [
|
|
||||||
'我刚才没有确认到待办/提醒已经写入系统,所以这次不能算设置成功。',
|
|
||||||
'请再发一次完整安排,比如“明天早上 6 点跑步,5 点半提醒我”。我会在工具写入成功后再确认。',
|
|
||||||
].join('\n');
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const requestStartedAt = Date.now();
|
const requestStartedAt = Date.now();
|
||||||
const agentPrompt =
|
const agentPrompt =
|
||||||
@@ -2211,74 +2039,6 @@ export function createWechatMpService({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScheduleIntent = async ({ intent, user }) => {
|
|
||||||
if (!scheduleService) return null;
|
|
||||||
const scheduleIntent = parseScheduleIntent(intent.agentText);
|
|
||||||
if (!isScheduleIntent(scheduleIntent)) return null;
|
|
||||||
|
|
||||||
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: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
|
||||||
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: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
|
||||||
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') {
|
|
||||||
const text = await scheduleService.buildTodoDigestText({
|
|
||||||
userId: user.userId,
|
|
||||||
timezone: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInboundMessage = async (bodyText, query = {}) => {
|
const handleInboundMessage = async (bodyText, query = {}) => {
|
||||||
if (!verifyRequest(query)) {
|
if (!verifyRequest(query)) {
|
||||||
return { ok: false, status: 403, body: 'invalid signature' };
|
return { ok: false, status: 403, body: 'invalid signature' };
|
||||||
@@ -2506,46 +2266,26 @@ export function createWechatMpService({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.msgType === 'text' && isQuestionStatusProbe(intent.agentText)) {
|
if (intent.msgType === 'text' || intent.msgType === 'voice') {
|
||||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
const wechatIntent = classifyWechatIntent(intent);
|
||||||
return {
|
if (shouldHandleSyncReply(intent, wechatIntent)) {
|
||||||
ok: true,
|
const syncReply = resolveSyncReply(wechatIntent, boundUser, {
|
||||||
status: 200,
|
statusFallbackText: config.statusText,
|
||||||
contentType: 'application/xml; charset=utf-8',
|
});
|
||||||
body: buildWechatTextReply({
|
if (syncReply) {
|
||||||
toUserName: inbound.fromUserName,
|
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
||||||
fromUserName: inbound.toUserName,
|
return {
|
||||||
content: buildStatusText(boundUser, config.statusText),
|
ok: true,
|
||||||
}),
|
status: 200,
|
||||||
};
|
contentType: 'application/xml; charset=utf-8',
|
||||||
}
|
body: buildWechatTextReply({
|
||||||
|
toUserName: inbound.fromUserName,
|
||||||
if (intent.msgType === 'text' && isSimpleGreeting(intent.agentText)) {
|
fromUserName: inbound.toUserName,
|
||||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
content: syncReply,
|
||||||
return {
|
}),
|
||||||
ok: true,
|
};
|
||||||
status: 200,
|
}
|
||||||
contentType: 'application/xml; charset=utf-8',
|
}
|
||||||
body: buildWechatTextReply({
|
|
||||||
toUserName: inbound.fromUserName,
|
|
||||||
fromUserName: inbound.toUserName,
|
|
||||||
content: buildGreetingText(boundUser),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((intent.msgType === 'text' || intent.msgType === 'voice') && isConnectivityTest(intent.agentText)) {
|
|
||||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/xml; charset=utf-8',
|
|
||||||
body: buildWechatTextReply({
|
|
||||||
toUserName: inbound.fromUserName,
|
|
||||||
fromUserName: inbound.toUserName,
|
|
||||||
content: buildConnectivityTestReply(boundUser),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inbound.msgId && typeof userAuth.recordWechatMpMessage === 'function') {
|
if (inbound.msgId && typeof userAuth.recordWechatMpMessage === 'function') {
|
||||||
@@ -2572,7 +2312,7 @@ export function createWechatMpService({
|
|||||||
|
|
||||||
const scheduleReply =
|
const scheduleReply =
|
||||||
intent.msgType === 'text' || intent.msgType === 'voice'
|
intent.msgType === 'text' || intent.msgType === 'voice'
|
||||||
? await handleScheduleIntent({ intent, user: boundUser })
|
? await handleWechatScheduleIntent({ intent, user: boundUser, scheduleService })
|
||||||
: null;
|
: null;
|
||||||
if (scheduleReply) {
|
if (scheduleReply) {
|
||||||
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
|
if (inbound.msgId && typeof userAuth.finishWechatMpMessage === 'function') {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { buildAutoChatSkillPrefix } from '../../chat-skills.mjs';
|
||||||
|
import { shouldUseScheduleAssistant } from '../../schedule-intent.mjs';
|
||||||
|
import { buildCurrentTimeAgentPrefix } from '../../user-memory-profile.mjs';
|
||||||
|
import { isPageGenerateText, wantsDocxDownload } from '../intent/patterns.mjs';
|
||||||
|
|
||||||
|
export function buildWechatAgentPrompt(intent, { grantedSkills = [] } = {}) {
|
||||||
|
const msgType = String(intent?.msgType ?? 'text');
|
||||||
|
const agentText = intent?.agentText ?? intent?.content ?? '';
|
||||||
|
const autoSkillPrefix =
|
||||||
|
msgType === 'text' || msgType === 'voice'
|
||||||
|
? buildAutoChatSkillPrefix(agentText, grantedSkills)
|
||||||
|
: '';
|
||||||
|
const docxDownloadHint =
|
||||||
|
isPageGenerateText(agentText) && wantsDocxDownload(agentText)
|
||||||
|
? [
|
||||||
|
'【Word 下载要求】用户明确要求页面里可下载 Word / docx。',
|
||||||
|
'开始前必须先调用 `load_skill` → `docx-generate`,并按技能说明生成目标 `public/*.docx`。',
|
||||||
|
'生成后必须确认目标 `.docx` 已存在,再调用 `load_skill` → `static-page-publish` 写 `public/*.html`。',
|
||||||
|
'HTML 中只能用同目录相对路径链接该文档;禁止只写下载按钮却没有先把 `.docx` 落盘。',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
: '';
|
||||||
|
const pagePublishHint = isPageGenerateText(agentText)
|
||||||
|
? [
|
||||||
|
'【页面发布技能要求】这条消息是在生成可访问 HTML 页面。',
|
||||||
|
'开始前必须先调用 `load_skill` → `static-page-publish`,不能省略,也不能写完页面后再补调。',
|
||||||
|
'随后必须用 sandbox-fs 的 `write_file` / `edit_file` 写入 `public/*.html`。',
|
||||||
|
'禁止用 shell / cat / heredoc / echo / cp 写入 HTML:shell 在容器内执行,文件不会出现在公网 MindSpace 路径,用户会收到「文件不存在」。',
|
||||||
|
'在回复用户前,确认 `public/` 下目标 HTML 已通过 write_file 落盘;没有落盘就不要发送链接或说「已发布」。',
|
||||||
|
'最终只给用户一个正式域名的唯一正确链接;不要输出错误域名、备用链接或让用户手动保存文件。',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
: '';
|
||||||
|
const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
|
||||||
|
const currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone });
|
||||||
|
const scheduleAssistantHint = shouldUseScheduleAssistant(agentText)
|
||||||
|
? [
|
||||||
|
'【日程技能要求】这条消息涉及待办、提醒或日程。',
|
||||||
|
'开始前先加载 `schedule-assistant` skill,并严格按 skill 里的边界执行。',
|
||||||
|
'写入工具时优先使用 startLocal / endLocal / remindLocal(YYYY-MM-DD HH:mm),不要自行估算 Unix 毫秒。',
|
||||||
|
'只有在 `schedule_create_item` / `schedule_create_reminder` 等工具成功返回后,才能告诉用户“已经设置好了”。',
|
||||||
|
intent?.msgId ? `调用 schedule_create_item 时必须传入 sourceMessageId: ${intent.msgId}` : '',
|
||||||
|
'',
|
||||||
|
].filter(Boolean).join('\n')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (msgType === 'voice') {
|
||||||
|
return [
|
||||||
|
docxDownloadHint,
|
||||||
|
currentTimeHint,
|
||||||
|
scheduleAssistantHint,
|
||||||
|
'【微信服务号语音消息】用户通过语音输入,以下是微信识别结果。',
|
||||||
|
'',
|
||||||
|
`用户语音识别文本:${autoSkillPrefix}${String(agentText).trim()}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
if (msgType === 'image') {
|
||||||
|
return [
|
||||||
|
docxDownloadHint,
|
||||||
|
currentTimeHint,
|
||||||
|
scheduleAssistantHint,
|
||||||
|
'【微信服务号图片消息】用户发送了图片。',
|
||||||
|
'如用户没有明确要求,请先根据图片内容给出简短理解,并询问下一步。',
|
||||||
|
'',
|
||||||
|
String(agentText).trim(),
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
if (msgType === 'location') {
|
||||||
|
const location = intent?.location ?? {};
|
||||||
|
return [
|
||||||
|
currentTimeHint,
|
||||||
|
scheduleAssistantHint,
|
||||||
|
'【微信服务号位置消息】用户发送了当前位置。',
|
||||||
|
location.label ? `地址:${location.label}` : '',
|
||||||
|
location.latitude !== null && location.latitude !== undefined
|
||||||
|
? `纬度:${location.latitude}`
|
||||||
|
: '',
|
||||||
|
location.longitude !== null && location.longitude !== undefined
|
||||||
|
? `经度:${location.longitude}`
|
||||||
|
: '',
|
||||||
|
'请结合位置回答用户可能的路线、附近、行程或提醒需求;如果意图不明确,先简短询问。',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
if (msgType === 'link') {
|
||||||
|
const link = intent?.link ?? {};
|
||||||
|
return [
|
||||||
|
currentTimeHint,
|
||||||
|
scheduleAssistantHint,
|
||||||
|
'【微信服务号链接消息】用户分享了链接。',
|
||||||
|
link.title ? `标题:${link.title}` : '',
|
||||||
|
link.description ? `描述:${link.description}` : '',
|
||||||
|
link.url ? `URL:${link.url}` : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
const content = String(agentText).trim();
|
||||||
|
const lines = [currentTimeHint];
|
||||||
|
if (docxDownloadHint) lines.push(docxDownloadHint);
|
||||||
|
if (pagePublishHint) lines.push(pagePublishHint);
|
||||||
|
if (scheduleAssistantHint) lines.push(scheduleAssistantHint);
|
||||||
|
lines.push(
|
||||||
|
'【微信服务号新消息】请只回答下面这条用户消息,不要主动延续无关的历史话题。',
|
||||||
|
'若用户只是在测试连通性,请一句话确认收到即可,不要展开旧任务。',
|
||||||
|
'',
|
||||||
|
`用户消息:${autoSkillPrefix}${content}`,
|
||||||
|
);
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function normalizeWechatName(value) {
|
||||||
|
const name = String(value ?? '').trim();
|
||||||
|
if (!name || /^wx_[a-z0-9_]{4,64}$/i.test(name)) return '';
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWechatAddressName(user) {
|
||||||
|
return normalizeWechatName(user?.nickname) || normalizeWechatName(user?.displayName) || '';
|
||||||
|
}
|
||||||
@@ -10,7 +10,10 @@ import {
|
|||||||
selectSendableHtmlArtifacts,
|
selectSendableHtmlArtifacts,
|
||||||
verifyPageArtifactContent,
|
verifyPageArtifactContent,
|
||||||
} from './verify/page-artifact.mjs';
|
} from './verify/page-artifact.mjs';
|
||||||
|
import { guardScheduleConfirmationReply, looksLikeScheduleConfirmation } from './handlers/schedule-guard.mjs';
|
||||||
import { resolvePageGenerateOutcome } from './handlers/page-generate.mjs';
|
import { resolvePageGenerateOutcome } from './handlers/page-generate.mjs';
|
||||||
|
import { buildGreetingText, resolveSyncReply } from './handlers/sync-replies.mjs';
|
||||||
|
import { buildWechatAgentPrompt } from './prompts/chat-general.mjs';
|
||||||
|
|
||||||
test('classifyWechatIntent detects page.generate', () => {
|
test('classifyWechatIntent detects page.generate', () => {
|
||||||
const intent = classifyWechatIntent({
|
const intent = classifyWechatIntent({
|
||||||
@@ -50,6 +53,42 @@ test('selectSendableHtmlArtifacts never returns stub html', () => {
|
|||||||
assert.equal(isStubPublicHtmlContent('<html>服务号自动补出简版页面</html>'), true);
|
assert.equal(isStubPublicHtmlContent('<html>服务号自动补出简版页面</html>'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveSyncReply handles greeting and status probes', () => {
|
||||||
|
const user = { nickname: '唐' };
|
||||||
|
assert.match(
|
||||||
|
resolveSyncReply(classifyWechatIntent({ msgType: 'text', agentText: '你好' }), user, {
|
||||||
|
statusFallbackText: '处理中',
|
||||||
|
}),
|
||||||
|
/唐/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
resolveSyncReply(classifyWechatIntent({ msgType: 'text', agentText: '??' }), user, {
|
||||||
|
statusFallbackText: '处理中',
|
||||||
|
}),
|
||||||
|
/我在这边/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildWechatAgentPrompt lives in wechat prompts package', () => {
|
||||||
|
const prompt = buildWechatAgentPrompt({
|
||||||
|
msgType: 'text',
|
||||||
|
agentText: '明天早上 6 点跑步,5 点半提醒我',
|
||||||
|
});
|
||||||
|
assert.match(prompt, /schedule-assistant/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('schedule confirmation guard blocks false positives', async () => {
|
||||||
|
assert.equal(looksLikeScheduleConfirmation('已经帮你设置好了待办提醒'), true);
|
||||||
|
const guarded = await guardScheduleConfirmationReply({
|
||||||
|
replyText: '已经帮你设置好了待办提醒',
|
||||||
|
scheduleService: { listItemsBySourceMessage: async () => [] },
|
||||||
|
userId: 'user-1',
|
||||||
|
sourceMessageId: 'msg-1',
|
||||||
|
logger: null,
|
||||||
|
});
|
||||||
|
assert.match(guarded, /不能算设置成功/);
|
||||||
|
});
|
||||||
|
|
||||||
test('resolvePageGenerateOutcome fails when only stub artifacts exist', () => {
|
test('resolvePageGenerateOutcome fails when only stub artifacts exist', () => {
|
||||||
const outcome = resolvePageGenerateOutcome({
|
const outcome = resolvePageGenerateOutcome({
|
||||||
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/tang-poem.html' },
|
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/tang-poem.html' },
|
||||||
|
|||||||
Reference in New Issue
Block a user