From 7e3c018a5503ba691b0f7cc26d0df2e4276e9a9c Mon Sep 17 00:00:00 2001 From: john Date: Wed, 1 Jul 2026 08:32:02 +0800 Subject: [PATCH] fix(agent): inject Asia/Shanghai time anchor on 0630007 line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent replies were reporting UTC time because 0630007-memind never injected TKMind 当前时间基准 into session memory. Port the time anchor from the 0630004 work only—no miniapp branch changes—and add a 103 rollback script. Co-authored-by: Cursor --- .../103-rollback-agent-time-fix-2026-07-01.md | 51 ++++ scripts/rollback-portal-runtime-prod.sh | 224 ++++++++++++++++++ session-reconcile.test.mjs | 27 ++- user-memory-profile.mjs | 86 +++++++ user-memory-profile.test.mjs | 59 ++++- wechat-mp.mjs | 4 + 6 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 docs/103-rollback-agent-time-fix-2026-07-01.md create mode 100755 scripts/rollback-portal-runtime-prod.sh diff --git a/docs/103-rollback-agent-time-fix-2026-07-01.md b/docs/103-rollback-agent-time-fix-2026-07-01.md new file mode 100644 index 0000000..6f3bd73 --- /dev/null +++ b/docs/103-rollback-agent-time-fix-2026-07-01.md @@ -0,0 +1,51 @@ +# 103 回退说明 — Agent 时间基准修复(2026-07-01) + +## 背景 + +- 分支:`0630007-agent-time-fix`(从 `0630007-memind` 切出,**不含** miniapp / 0630006 改动) +- 修复:向 Agent 注入 `TKMind 当前时间基准`(Asia/Shanghai),避免回复里报 UTC 时间 + +## 发布前 103 状态(可回退目标) + +| 项 | 值 | +|---|---| +| release_id | `20260701-075736-16828a5` | +| git_head | `16828a58f7fc79d7b9a174ffa87f5579894798fc` | +| git_branch | `0630007-memind` | + +回退到上述 release 发布前版本: + +```bash +bash scripts/rollback-portal-runtime-prod.sh 20260701-075736-16828a5 --yes +``` + +## 查看 103 上所有可回退点 + +```bash +bash scripts/rollback-portal-runtime-prod.sh --list +``` + +## 回退源优先级 + +1. `/Users/john/Project/archives/Memind-source-before-/` +2. `/Users/john/Project/backups/memind/memind-full--before.tar.gz` + +发布脚本会自动创建上述两份备份;回退脚本优先用 archive(最快)。 + +## 发布后验收 + +1. `curl -s http://127.0.0.1:8081/api/status` → 200 +2. 新建 H5 会话,问「现在几点」→ 应报 **上海时间**(非 UTC 前一天 23:xx) +3. 日志无 `User auth bootstrap failed` + +## 若新 release 有问题 + +假设新 release_id 为 `20260701-HHMMSS-`,立即回退: + +```bash +bash scripts/rollback-portal-runtime-prod.sh 20260701-HHMMSS- --yes +``` + +失败的 live 目录会保留在: + +`/Users/john/Project/archives/Memind-failed-after--/` diff --git a/scripts/rollback-portal-runtime-prod.sh b/scripts/rollback-portal-runtime-prod.sh new file mode 100755 index 0000000..f68c86d --- /dev/null +++ b/scripts/rollback-portal-runtime-prod.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOST="${STUDIO_HOST:-58.38.22.103}" +REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}" +APP_DIR="${REMOTE_ROOT}/Memind" +BACKUP_DIR="${REMOTE_ROOT}/backups/memind" +ARCHIVE_DIR="${REMOTE_ROOT}/archives" +HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}" +PORTAL_LABEL="cn.tkmind.memind-portal" +LAUNCHD_GUI="gui/$(id -u)" +AUTO_YES=0 +RELEASE_ID="" + +usage() { + cat <<'EOF' +用法: + bash scripts/rollback-portal-runtime-prod.sh [--yes] + bash scripts/rollback-portal-runtime-prod.sh --list [--limit N] + +说明: + 将 103 Portal 回退到指定 release 发布前的 live 目录。 + + RELEASE_ID 形如 20260701-075736-16828a5(与 release-portal-runtime-prod.sh 输出一致)。 + + 回退优先级: + 1. /Users/john/Project/archives/Memind-source-before- + 2. /Users/john/Project/backups/memind/memind-full--before.tar.gz + + 当前失败的 live 目录会移入 archive,便于事后排查。 + +示例: + bash scripts/rollback-portal-runtime-prod.sh --list + bash scripts/rollback-portal-runtime-prod.sh 20260701-075736-16828a5 --yes +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --yes|-y) AUTO_YES=1 ;; + --list) + ssh -o BatchMode=yes "${HOST}" "bash -s" <<'REMOTE' +set -euo pipefail +echo "=== 当前 live manifest ===" +if [[ -f /Users/john/Project/Memind/.release-manifest.txt ]]; then + cat /Users/john/Project/Memind/.release-manifest.txt +else + echo "(无 manifest)" +fi +echo +echo "=== 可回退 archive(最近 10 条)===" +ls -1dt /Users/john/Project/archives/Memind-source-before-* 2>/dev/null | head -10 || true +echo +echo "=== 可回退 full backup(最近 10 条)===" +ls -1dt /Users/john/Project/backups/memind/memind-full-*-before.tar.gz 2>/dev/null | head -10 || true +REMOTE + exit 0 + ;; + -h|--help) + usage + exit 0 + ;; + *) + if [[ -z "${RELEASE_ID}" ]]; then + RELEASE_ID="$1" + else + echo "未知参数: $1" >&2 + usage >&2 + exit 1 + fi + ;; + esac + shift +done + +if [[ -z "${RELEASE_ID}" ]]; then + usage >&2 + exit 1 +fi + +say() { + printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*" +} + +say "103 回退预检查" +ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo rollback-ssh-ok" >/dev/null + +ARCHIVED_SOURCE="${ARCHIVE_DIR}/Memind-source-before-${RELEASE_ID}" +FULL_BACKUP="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before.tar.gz" + +ssh -o BatchMode=yes "${HOST}" "test -d '${ARCHIVED_SOURCE}' || test -f '${FULL_BACKUP}'" || { + echo "找不到 release ${RELEASE_ID} 的回退源:" >&2 + echo " ${ARCHIVED_SOURCE}" >&2 + echo " ${FULL_BACKUP}" >&2 + echo "可先运行: bash scripts/rollback-portal-runtime-prod.sh --list" >&2 + exit 1 +} + +if [[ "${AUTO_YES}" -ne 1 ]]; then + say "回退确认" + echo "目标主机: ${HOST}" + echo "回退 release: ${RELEASE_ID}" + echo "当前 live: ${APP_DIR}" + ssh -o BatchMode=yes "${HOST}" " + if [[ -d '${ARCHIVED_SOURCE}' ]]; then echo 'source=archived_source'; fi + if [[ -f '${FULL_BACKUP}' ]]; then echo 'source=full_backup'; fi + if [[ -f '${APP_DIR}/.release-manifest.txt' ]]; then echo '--- current manifest ---'; cat '${APP_DIR}/.release-manifest.txt'; fi + " + read -r -p "确认回退到 ${RELEASE_ID} 发布前版本? [y/N] " confirm /dev/null 2>&1 || true +lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true +sleep 2 +lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true + +say "准备回退目录" +rm -rf "${RESTORE_DIR}" +mkdir -p "${RESTORE_DIR}" + +if [[ -d "${ARCHIVED_SOURCE}" ]]; then + say "从 archived_source 恢复: ${ARCHIVED_SOURCE}" + cp -a "${ARCHIVED_SOURCE}/." "${RESTORE_DIR}/" +elif [[ -f "${FULL_BACKUP}" ]]; then + say "从 full backup 恢复: ${FULL_BACKUP}" + tar -xzf "${FULL_BACKUP}" -C "${RESTORE_DIR}" +else + echo "rollback source missing" >&2 + exit 1 +fi + +say "归档当前失败 live 目录" +if [[ -d "${APP_DIR}" ]]; then + rm -rf "${FAILED_LIVE}" + mv "${APP_DIR}" "${FAILED_LIVE}" +fi +mv "${RESTORE_DIR}" "${APP_DIR}" + +cat > "${APP_DIR}/.rollback-manifest.txt" < "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" < + + + + Label + ${PORTAL_LABEL} + ProgramArguments + + ${APP_DIR}/scripts/run-memind-portal-prod.sh + + WorkingDirectory + ${APP_DIR} + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + ${HOME}/Library/Logs/memind-portal.log + StandardErrorPath + ${HOME}/Library/Logs/memind-portal.log + + +EOF + launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" >/dev/null 2>&1 || true + launchctl enable "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true + launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || \ + nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 & +else + nohup /opt/homebrew/opt/node@24/bin/node "${APP_DIR}/server.mjs" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 & +fi + +say "健康检查" +for _ in $(seq 1 60); do + portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)" + if [[ "${portal_code}" == "200" ]]; then + break + fi + sleep 2 +done +portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)" +if [[ "${portal_code}" != "200" ]]; then + echo "rollback health check failed: portal=${portal_code}" >&2 + exit 1 +fi + +say "回退完成" +printf 'live_dir=%s\n' "${APP_DIR}" +printf 'failed_live=%s\n' "${FAILED_LIVE}" +printf 'rolled_back_release=%s\n' "${RELEASE_ID}" +REMOTE_SCRIPT + +say "103 回退后状态" +ssh -o BatchMode=yes "${HOST}" "curl -s '${HEALTH_URL}' && echo && cat '${APP_DIR}/.rollback-manifest.txt' 2>/dev/null || true" diff --git a/session-reconcile.test.mjs b/session-reconcile.test.mjs index 0d8506a..844df75 100644 --- a/session-reconcile.test.mjs +++ b/session-reconcile.test.mjs @@ -6,6 +6,16 @@ import { reconcileAgentSession, } from './session-reconcile.mjs'; +function harnessMemoryResponse(pathname) { + if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') { + return { + ok: true, + text: async () => JSON.stringify({ remembered: true, ok: true }), + }; + } + return null; +} + test('extensionConfigsMatch compares available_tools', () => { const sessionExt = { name: 'developer', available_tools: ['write', 'edit'] }; const desired = { name: 'developer', available_tools: ['write', 'edit', 'shell', 'tree'] }; @@ -77,6 +87,8 @@ test('reconcileAgentSession tolerates invalid working directory during resume sy text: async () => JSON.stringify({ ok: true }), }; } + const harness = harnessMemoryResponse(pathname); + if (harness) return harness; throw new Error(`unexpected path: ${pathname}`); }; @@ -90,6 +102,8 @@ test('reconcileAgentSession tolerates invalid working directory during resume sy '/sessions/session-1', '/agent/update_working_dir', '/sessions/session-1/extensions', + '/agent/harness_remember', + '/agent/harness_bootstrap', ]); }); @@ -118,6 +132,8 @@ test('reconcileAgentSession skips restart when extensions already match', async text: async () => JSON.stringify({ ok: true }), }; } + const harness = harnessMemoryResponse(pathname); + if (harness) return harness; throw new Error(`unexpected path: ${pathname}`); }; @@ -126,7 +142,12 @@ test('reconcileAgentSession skips restart when extensions already match', async sessionPolicy: { extensionOverrides: [{ name: 'skills', available_tools: [] }] }, }); - assert.deepEqual(calls, ['/sessions/session-1', '/sessions/session-1/extensions']); + assert.deepEqual(calls, [ + '/sessions/session-1', + '/sessions/session-1/extensions', + '/agent/harness_remember', + '/agent/harness_bootstrap', + ]); }); test('reconcileAgentSession restarts after adding missing extensions', async () => { @@ -151,6 +172,8 @@ test('reconcileAgentSession restarts after adding missing extensions', async () text: async () => JSON.stringify({ ok: true }), }; } + const harness = harnessMemoryResponse(pathname); + if (harness) return harness; throw new Error(`unexpected path: ${pathname}`); }; @@ -164,5 +187,7 @@ test('reconcileAgentSession restarts after adding missing extensions', async () '/sessions/session-1/extensions', '/agent/add_extension', '/agent/restart', + '/agent/harness_remember', + '/agent/harness_bootstrap', ]); }); diff --git a/user-memory-profile.mjs b/user-memory-profile.mjs index 411fc82..4ff9817 100644 --- a/user-memory-profile.mjs +++ b/user-memory-profile.mjs @@ -4,6 +4,7 @@ import { resolveUserAddressName } from './user-publish.mjs'; export const USER_MEMORY_PROFILE_FILENAME = '.tkmind-profile.json'; export const USER_MEMORY_PROFILE_VERSION = 1; +const DEFAULT_TIMEZONE = 'Asia/Shanghai'; export function buildInitialUserMemoryProfile({ userId, @@ -148,14 +149,81 @@ export function renderStoredUserMemoriesForHarness(memories) { ].join('\n'); } +function formatDateParts(now, timezone) { + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + const parts = Object.fromEntries( + formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value]), + ); + return { + year: parts.year ?? '0000', + month: parts.month ?? '01', + day: parts.day ?? '01', + }; +} + +function formatLocalClockParts(now, timezone) { + const formatter = new Intl.DateTimeFormat('en-GB', { + timeZone: timezone, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + const parts = Object.fromEntries( + formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value]), + ); + return { + hour: Number(parts.hour ?? 0), + minute: Number(parts.minute ?? 0), + }; +} + +export function resolveTimeOfDayPeriod(hour) { + const h = Number(hour); + if (h < 5) return { period: '凌晨', greeting: '你好' }; + if (h < 9) return { period: '早上', greeting: '早上好' }; + if (h < 12) return { period: '上午', greeting: '上午好' }; + if (h < 14) return { period: '中午', greeting: '中午好' }; + if (h < 18) return { period: '下午', greeting: '下午好' }; + return { period: '晚上', greeting: '晚上好' }; +} + +export function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) { + const { year, month, day } = formatDateParts(now, timezone); + const { hour, minute } = formatLocalClockParts(now, timezone); + const { period, greeting } = resolveTimeOfDayPeriod(hour); + const clock = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; + const weekday = new Intl.DateTimeFormat('zh-CN', { + timeZone: timezone, + weekday: 'long', + }).format(new Date(now)); + return [ + '## TKMind 当前时间基准', + '', + `- 当前时区:${timezone}`, + `- 当前日期:${year}-${month}-${day}(${weekday})`, + `- 当前时刻:${clock}(${period})`, + `- 若需时段问候可参考:${greeting}(仅新会话开场或用户主动打招呼时使用,普通任务回复不要每条都加)`, + '- 回答中涉及“今天 / 明天 / 后天 / 周几 / 早上 / 下午 / 晚上”等时间表述时,必须以上述日期与时刻为准,禁止自行假设当前时间或使用 UTC 等其它时区。', + ].join('\n'); +} + export function buildSessionMemoryEntries({ workingDir, sessionPolicy, sandboxConstraints = null, userContext = null, userMemories = null, + now = Date.now(), }) { const entries = []; + const timezone = String( + userContext?.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? DEFAULT_TIMEZONE, + ).trim() || DEFAULT_TIMEZONE; if (sandboxConstraints?.trim()) { entries.push({ @@ -172,6 +240,24 @@ export function buildSessionMemoryEntries({ }); } + entries.push({ + title: 'TKMind 当前时间基准', + content: renderCurrentTimeAnchor({ now, timezone }), + }); + + const scheduleTools = (sessionPolicy?.extensionOverrides ?? []).find((ext) => ext.name === 'sandbox-fs'); + if ((scheduleTools?.available_tools ?? []).includes('schedule_create_item')) { + entries.push({ + title: 'TKMind 日程写入规则', + content: [ + '创建或提醒待办/日程时:', + '- 必须使用 startLocal / endLocal / remindLocal,格式 YYYY-MM-DD HH:mm(用户时区墙上时钟)。', + '- 禁止自行估算 Unix 毫秒时间戳;传错会被工具拒绝。', + '- 写入后调用 schedule_list_items 核对日期是否与用户表述一致。', + ].join('\n'), + }); + } + if (!hasMemoryStore(sessionPolicy)) { return entries; } diff --git a/user-memory-profile.test.mjs b/user-memory-profile.test.mjs index a9e86e5..f665285 100644 --- a/user-memory-profile.test.mjs +++ b/user-memory-profile.test.mjs @@ -8,6 +8,8 @@ import { buildTaskRoutingAgentText, resolveCodeExecutorRouting, renderCodeExecutorGuidance, + renderCurrentTimeAnchor, + resolveTimeOfDayPeriod, suggestCodeExecutorForTask, buildSessionMemoryEntries, ensureUserMemoryProfile, @@ -77,13 +79,18 @@ test('buildSessionMemoryEntries injects sandbox, guidance, and profile', () => { sessionPolicy, sandboxConstraints: '## sandbox', userContext: { userId: 'user-2', displayName: 'Bob', username: 'bob' }, + now: Date.UTC(2026, 5, 29, 5, 0, 0), }); - assert.equal(entries.length, 4); + assert.equal(entries.length, 5); assert.equal(entries[0].title, 'TKMind 用户空间沙箱'); assert.equal(entries[1].title, 'TKMind 代码委托策略'); assert.match(entries[1].content, /openhands/); - assert.match(entries[2].content, /长期记忆/); - assert.match(entries[3].content, /Bob/); + assert.equal(entries[2].title, 'TKMind 当前时间基准'); + assert.match(entries[2].content, /2026-06-29(星期一)/); + assert.match(entries[2].content, /13:00(中午)/); + assert.match(entries[2].content, /若需时段问候可参考:中午好/); + assert.match(entries[3].content, /长期记忆/); + assert.match(entries[4].content, /Bob/); }); test('buildSessionMemoryEntries injects stored conversation memories', () => { @@ -94,11 +101,57 @@ test('buildSessionMemoryEntries injects stored conversation memories', () => { sessionPolicy, userContext: { userId: 'user-3', displayName: 'Carol', username: 'carol' }, userMemories: [{ label: 'interest', text: '用户关注 AI 产品设计' }], + now: Date.UTC(2026, 5, 29, 5, 0, 0), }); assert.equal(entries.at(-1).title, 'TKMind 已沉淀用户记忆'); assert.match(entries.at(-1).content, /AI 产品设计/); }); +test('buildSessionMemoryEntries injects time anchor even without memory store', () => { + const sessionPolicy = buildAgentExtensionPolicy({ + ...DEFAULT_USER_CAPABILITIES, + memory_store: false, + }); + const entries = buildSessionMemoryEntries({ + workingDir: '/tmp/unused', + sessionPolicy, + now: Date.UTC(2026, 5, 29, 5, 0, 0), + }); + assert.equal(entries.length, 1); + assert.equal(entries[0].title, 'TKMind 当前时间基准'); + assert.match(entries[0].content, /Asia\/Shanghai/); + assert.match(entries[0].content, /13:00(中午)/); +}); + +test('renderCurrentTimeAnchor formats Shanghai weekday from exact date', () => { + const text = renderCurrentTimeAnchor({ + now: Date.UTC(2026, 5, 29, 5, 0, 0), + timezone: 'Asia/Shanghai', + }); + assert.match(text, /2026-06-29(星期一)/); + assert.match(text, /Asia\/Shanghai/); + assert.match(text, /13:00(中午)/); + assert.match(text, /若需时段问候可参考:中午好/); + assert.match(text, /普通任务回复不要每条都加/); +}); + +test('renderCurrentTimeAnchor uses local morning greeting at 07:03 Shanghai', () => { + const text = renderCurrentTimeAnchor({ + now: Date.UTC(2026, 5, 29, 23, 3, 0), + timezone: 'Asia/Shanghai', + }); + assert.match(text, /2026-06-30(星期二)/); + assert.match(text, /07:03(早上)/); + assert.match(text, /若需时段问候可参考:早上好/); +}); + +test('resolveTimeOfDayPeriod maps hours to greeting labels', () => { + assert.deepEqual(resolveTimeOfDayPeriod(2), { period: '凌晨', greeting: '你好' }); + assert.deepEqual(resolveTimeOfDayPeriod(7), { period: '早上', greeting: '早上好' }); + assert.deepEqual(resolveTimeOfDayPeriod(10), { period: '上午', greeting: '上午好' }); + assert.deepEqual(resolveTimeOfDayPeriod(20), { period: '晚上', greeting: '晚上好' }); +}); + test('renderMemoryStoreGuidance scopes memory to one user', () => { const text = renderMemoryStoreGuidance({ addressName: '小陈' }); assert.match(text, /小陈/); diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 5c8ee4e..d0560a1 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -6,6 +6,7 @@ import { developerToolsFromPolicy } from './capabilities.mjs'; import { mergeMessageContent } from './message-stream.mjs'; import { reconcileAgentSession } from './session-reconcile.mjs'; import { isScheduleIntent, parseScheduleIntent, shouldUseScheduleAssistant } from './schedule-intent.mjs'; +import { renderCurrentTimeAnchor } from './user-memory-profile.mjs'; import { PUBLISH_ROOT_DIR } from './user-publish.mjs'; import { downloadTemporaryMedia, persistWechatImage } from './wechat-media.mjs'; import { buildAckText } from './wechat/ack/ack-provider.mjs'; @@ -1071,10 +1072,13 @@ function buildWechatAgentPrompt(intent) { '', ].join('\n') : ''; + const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai'; const scheduleAssistantHint = shouldUseScheduleAssistant(intent?.agentText ?? intent?.content) ? [ '【日程技能要求】这条消息涉及待办、提醒或日程。', '开始前先加载 `schedule-assistant` skill,并严格按 skill 里的边界执行。', + '写入工具时优先使用 startLocal / endLocal / remindLocal(YYYY-MM-DD HH:mm),不要自行估算 Unix 毫秒。', + renderCurrentTimeAnchor({ timezone: scheduleTimezone }), '只有在 `schedule_create_item` / `schedule_create_reminder` 等工具成功返回后,才能告诉用户“已经设置好了”。', intent?.msgId ? `调用 schedule_create_item 时必须传入 sourceMessageId: ${intent.msgId}` : '', '',