fix(agent): inject Asia/Shanghai time anchor on 0630007 line

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 <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-01 08:32:02 +08:00
parent 235ca0971e
commit 62c1d72470
5 changed files with 317 additions and 2 deletions
@@ -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-<RELEASE_ID>/`
2. `/Users/john/Project/backups/memind/memind-full-<RELEASE_ID>-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-<sha>`,立即回退:
```bash
bash scripts/rollback-portal-runtime-prod.sh 20260701-HHMMSS-<sha> --yes
```
失败的 live 目录会保留在:
`/Users/john/Project/archives/Memind-failed-after-<RELEASE_ID>-<timestamp>/`
+224
View File
@@ -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 <RELEASE_ID> [--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-<RELEASE_ID>
2. /Users/john/Project/backups/memind/memind-full-<RELEASE_ID>-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/tty
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
fi
say "在 103 执行回退"
ssh -o BatchMode=yes "${HOST}" \
"RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' ARCHIVED_SOURCE='${ARCHIVED_SOURCE}' FULL_BACKUP='${FULL_BACKUP}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT'
set -euo pipefail
ROLLBACK_TS="$(date +%Y%m%d-%H%M%S)"
FAILED_LIVE="${ARCHIVE_DIR}/Memind-failed-after-${RELEASE_ID}-${ROLLBACK_TS}"
RESTORE_DIR="${APP_DIR}.rollback-${ROLLBACK_TS}"
say() {
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
}
say "停止 Portal"
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/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" <<EOF
rollback_at=${ROLLBACK_TS}
rolled_back_release=${RELEASE_ID}
failed_live=${FAILED_LIVE}
restored_from=$([[ -d "${ARCHIVED_SOURCE}" ]] && echo archived_source || echo full_backup)
EOF
say "重启 Portal"
if [[ -x "${APP_DIR}/scripts/run-memind-portal-prod.sh" ]]; then
cat > "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PORTAL_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>${APP_DIR}/scripts/run-memind-portal-prod.sh</string>
</array>
<key>WorkingDirectory</key>
<string>${APP_DIR}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>${HOME}/Library/Logs/memind-portal.log</string>
<key>StandardErrorPath</key>
<string>${HOME}/Library/Logs/memind-portal.log</string>
</dict>
</plist>
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"
+26 -1
View File
@@ -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',
]);
});
+16
View File
@@ -107,6 +107,22 @@ test('buildSessionMemoryEntries injects stored conversation memories', () => {
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),
-1
View File
@@ -1658,7 +1658,6 @@ export function createWechatMpService({
tolerateInvalidWorkingDir: true,
},
);
<<<<<<< HEAD
const routeHasTools = await sessionHasRequiredTools(
fetchForSession,
existingRoute.agentSessionId,