Compare commits

..

6 Commits

Author SHA1 Message Date
john 0d6258d00a chore: remove mm.tkmind.cn references 2026-07-07 10:01:32 +08:00
john cf04e73f51 docs(ops): 同步 103 生产 goosed 扩容为 9 实例
确认 goosed-prod-1..9 映射 18006-18014,更新 topology、发包必看、
发布 skill 与 release 脚本的动态 pool 校验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 09:22:40 +08:00
john 5485cc563b merge origin/main: 合并 runtime bundle 修复与本地图片物化/build 修复 2026-07-07 09:09:32 +08:00
tkmind 0edd531ed9 Merge pull request 'fix(ops): 将 goose session postgres ensure 脚本打入 runtime 包' (#8) from fix/goose-session-postgres-runtime-bundle into main 2026-07-06 09:11:59 +00:00
john 3befb0e257 fix(build): runtime 清理失败时回退 shell rm -rf
避免 macOS 上 pnpm node_modules 树导致 ENOTEMPTY 阻断发布构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:06:06 +08:00
john 0ae7a6a677 fix(mindspace): 公开页上传图片物化到 static 路径供转发访问
Finish 同步与首次访问时将私有资产 API 图片复制到 public/.tmp-images,
避免其他用户转发链接后因无登录态无法加载页面内图片。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:03:47 +08:00
18 changed files with 360 additions and 179 deletions
+4 -4
View File
@@ -74,12 +74,12 @@ curl -k -i https://m.tkmind.cn/auth/login \
必须是 **401** + `用户名或密码错误`。如果是 `503` + `未配置用户数据库或访问密码`,说明用户系统 bootstrap 失败——不是简单健康检查能发现的问题,必须当场处理,不能算发布成功。
### 3.2 Portal + 个 goosed 实例(SSH 103
### 3.2 Portal + 个 goosed 实例(SSH 103
```bash
ssh john@58.38.22.103 '
printf "portal="; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081/api/status
for p in 18006 18007 18008 18009; do
for p in $(seq 18006 18014); do
printf "goosed_${p}="
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1:${p}/status
done
@@ -89,7 +89,7 @@ ssh john@58.38.22.103 '
'
```
必须全部是 `200`,五个 env var 全部 `SET`
必须全部是 `200`Portal + 9 个 goosed target,五个 env var 全部 `SET`
### 3.3 日志关键词检查(SSH 103
@@ -164,7 +164,7 @@ ssh john@58.38.22.103 '
- persisted backup 路径
- Portal 健康结果(200/非200
- `/auth/login` 结果(401 + 正确消息,还是别的)
- 18006-18009 健康结果
- `18006``18014` 健康结果(共 9 个 target
- 日志关键词检查结果
- 第 3.4 节真实业务路径验证结果(含遗留的测试 page id,交给用户决定是否清理)
- 是否发现并处理了问题;如果发布后发现回滚,说明触发原因
-52
View File
@@ -1,52 +0,0 @@
server {
listen 80;
server_name mm.tkmind.cn;
location /.well-known/acme-challenge/ {
root /opt/homebrew/var/www;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name mm.tkmind.cn;
ssl_certificate /opt/homebrew/etc/nginx/ssl/mm.tkmind.cn.crt;
ssl_certificate_key /opt/homebrew/etc/nginx/ssl/mm.tkmind.cn.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
location ~ ^/api/(sessions/[^/]+/events|agent/runs/[^/]+/events)$ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
}
location / {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
+12 -5
View File
@@ -1,6 +1,6 @@
# Local and 103 runtime topology
> Last confirmed: 2026-07-03 15:36 CST.
> Last confirmed: 2026-07-07 09:19 CST.
>
> This is the current topology source of truth for local Memind and 103 production. Prefer this document over older migration notes. Older architecture documents may contain historical paths from before the MindSpace split.
@@ -19,7 +19,7 @@ Local Memind connects to native `goosed`, not Colima/Docker `goosed`.
Local ports `18007`-`18009` are not the intended local goosed pool. If they are listening locally, treat that as drift unless a specific local test explicitly opened them.
Local Colima may exist for other workloads, but local Memind/goosed is not supposed to route through the 103-style `goosed-prod-1..4` Colima pool.
Local Colima may exist for other workloads, but local Memind/goosed is not supposed to route through the 103-style `goosed-prod-1..9` Colima pool.
## Portal
@@ -31,6 +31,7 @@ Local Colima may exist for other workloads, but local Memind/goosed is not suppo
| Start script | `/Users/john/Project/Memind/scripts/run-memind-portal-prod.sh` |
| Port | `8081` |
| Health check | `curl http://127.0.0.1:8081/api/status` |
| Upstream goosed pool | `TKMIND_API_TARGETS``https://127.0.0.1:18006``18014`9 targets2026-07-07 确认) |
Portal must be released as a runtime artifact. Do not edit source directly on 103.
@@ -70,7 +71,7 @@ ssh john@58.38.22.103 '/opt/homebrew/opt/postgresql@17/bin/pg_isready -h 127.0.0
`cn.tkmind.goosed-monitor`103)运行 `goosed-prod/scripts/monitor-goosed-containers.mjs`,本地 native goosed 监控用 `scripts/monitor-goosed-fds.mjs`;两者都会在巡检时调用 `ensure-goose-session-postgres.sh`
Container layout:
Container layout (host port → container `18006`):
| Container | Host port | Container port |
|-----------|-----------|----------------|
@@ -78,8 +79,13 @@ Container layout:
| `goosed-prod-2` | `18007` | `18006` |
| `goosed-prod-3` | `18008` | `18006` |
| `goosed-prod-4` | `18009` | `18006` |
| `goosed-prod-5` | `18010` | `18006` |
| `goosed-prod-6` | `18011` | `18006` |
| `goosed-prod-7` | `18012` | `18006` |
| `goosed-prod-8` | `18013` | `18006` |
| `goosed-prod-9` | `18014` | `18006` |
All four ports must return `ok` from `/status`.
All nine host ports must return `ok` from `/status`, and must appear in Portal `.env` `TKMIND_API_TARGETS`.
## PostgreSQL rule
@@ -119,5 +125,6 @@ ssh john@58.38.22.103 '/opt/homebrew/bin/docker inspect goosed-prod-1 --format "
```bash
ssh john@58.38.22.103 'curl -fsS http://127.0.0.1:8081/api/status'
ssh john@58.38.22.103 'curl -fsS http://127.0.0.1:8082/health'
ssh john@58.38.22.103 'for p in 18006 18007 18008 18009; do curl -kfsS https://127.0.0.1:$p/status; echo; done'
ssh john@58.38.22.103 'for p in $(seq 18006 18014); do curl -kfsS https://127.0.0.1:$p/status; echo; done'
ssh john@58.38.22.103 'grep ^TKMIND_API_TARGETS= /Users/john/Project/Memind/.env'
```
+6 -6
View File
@@ -8,8 +8,8 @@ Production safety rules:
- Do not delete PG data.
- Do not delete or rewrite MindSpace user data. On current 103 production, MindSpace Service root is `/Users/john/MindSpace`; `/Users/john/Project/Memind/MindSpace` is legacy compatibility/storage context. Confirm against [103 runtime topology](./103-runtime-topology.md) before any file operation.
- Keep H5 public traffic on `https://mm.tkmind.cn`.
- Treat `m.tkmind.cn` / 105 tunnel as legacy rollback transport, not the primary H5 path.
- Keep H5 public traffic on `https://m.tkmind.cn`.
- Treat the 105 tunnel path as legacy rollback transport, not the primary H5 path.
- Keep default production state closed unless a rollout window is explicitly active.
## Default Closed State
@@ -18,7 +18,7 @@ Expected production state:
```bash
cd /Users/john/Project/Memind
curl -fsS https://mm.tkmind.cn/api/runtime/status
curl -fsS https://m.tkmind.cn/api/runtime/status
node scripts/check-agent-run-worker.mjs
node scripts/runtime-slo-report.mjs
```
@@ -140,7 +140,7 @@ launchctl kickstart -k "gui/$(id -u)/cn.tkmind.memind-portal"
4. Verify Portal policy.
```bash
curl -fsS https://mm.tkmind.cn/api/runtime/status | node -e '
curl -fsS https://m.tkmind.cn/api/runtime/status | node -e '
let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{
const j=JSON.parse(s);
console.log(JSON.stringify({
@@ -297,7 +297,7 @@ MEMIND_AGENT_RUN_WORKER_START=0 bash /Users/john/Project/Memind/scripts/install-
4. Verify closed state.
```bash
curl -fsS https://mm.tkmind.cn/api/runtime/status
curl -fsS https://m.tkmind.cn/api/runtime/status
node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs
node /Users/john/Project/Memind/scripts/runtime-slo-report.mjs
```
@@ -309,7 +309,7 @@ If code tasks appear stuck:
```bash
node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs
tail -200 /Users/john/Library/Logs/memind-agent-run-worker.log
curl -fsS https://mm.tkmind.cn/api/runtime/status
curl -fsS https://m.tkmind.cn/api/runtime/status
```
If `oldestPendingAgeMs` grows while worker is running:
@@ -11,13 +11,13 @@
- 分支: `memind-streaming-runtime-20260702`
- 最新提交: 本文档所在分支 HEAD,提交信息 `fix: harden auth middleware against transient db errors`
- 最新 release: `20260702-104734-212e163` 后续改造以生产备份和分支提交方式推进。
- 当前公网入口: `https://mm.tkmind.cn`
- 当前公网入口: `https://m.tkmind.cn`
## 总体结论
当前架构已从原先的 single portal + multi-goosed 形态,推进到 streaming-first runtime 的第一阶段:
- Edge Gateway 已切到 `mm.tkmind.cn -> local nginx -> Portal :8081`
- Edge Gateway 已切到 `m.tkmind.cn -> local nginx -> Portal :8081`
- Stream Controller 已具备 SSE headers、abort propagation、backpressure pipeline。
- Redis Router 已启用,承担 worker runtime state。
- Goose Worker Pool 已从固定单点走向四 worker 可观测调度。
@@ -48,12 +48,12 @@
- 验证:
- `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs`: `15 passed`
- `npm run build:portal-runtime`: passed
- `https://mm.tkmind.cn/api/runtime/status`: `ok=true`
- `https://m.tkmind.cn/api/runtime/status`: `ok=true`
- `scripts/check-tool-runtime.mjs`: `ok=true`
- `scripts/runtime-slo-report.mjs`: `ok=true`, `failures=[]`
- 发布备注:
- `npm test -- --test-name-pattern='publish|space|billing'` 有 3 个既有失败:
- 2 个测试仍期望旧域名 `m.tkmind.cn`,但当前生产基线是 `mm.tkmind.cn`
- 2 个测试仍期望旧域名 `m.tkmind.cn`,但当前生产基线是 `m.tkmind.cn`
- 1 个测试期望 executor command 缺失,但本机当前已有对应命令
- 因失败项与 P6.3 无关,发布使用 `--skip-tests`,仍执行构建和 publish guard。
- 数据边界:
@@ -102,7 +102,7 @@
- related tests: `43 passed`
- 发布运行态:
- 发布脚本在 legacy `m.tkmind.cn` tunnel 检查处返回 1。
- `mm.tkmind.cn` 主路径和 Portal runtime 已正常。
- `m.tkmind.cn` 主路径和 Portal runtime 已正常。
- 已清理占用 8081 的孤儿 Portal node,让 LaunchAgent 接管。
- final Portal listener: pid `86462`, port `8081`
- 第二次真实 canary:
@@ -473,9 +473,9 @@ Data boundary:
- `npm run build:portal-runtime`: passed
- 发布备注:
- release 脚本在 legacy `m.tkmind.cn` tunnel step 短暂返回失败。
- Portal runtime 已切换,`mm.tkmind.cn` 主路径健康。
- Portal runtime 已切换,`m.tkmind.cn` 主路径健康。
- 后续检查显示 `cn.tkmind.memind-portal-tunnel` 已恢复 running。
- 当前 H5 主路径仍为 `https://mm.tkmind.cn`,不依赖 105 转发。
- 当前 H5 主路径仍为 `https://m.tkmind.cn`,不依赖 105 转发。
- Final runtime:
- `toolRuntime.codeRunPolicy.enabled=false`
- `toolRuntime.codeRunPolicy.userAllowlist=[]`
@@ -580,10 +580,10 @@ Data boundary:
结果: 通过。
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `H5_PUBLIC_BASE_URL=https://mm.tkmind.cn`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `H5_PUBLIC_BASE_URL=https://m.tkmind.cn`
- `m.tkmind.cn` 已作为 legacy/rollback 记录,不作为后续 H5 主路径。
- nginx `mm.tkmind.cn` 已对 SSE 路由禁用 buffering/cache/gzip,并设置长 read/send timeout。
- nginx `m.tkmind.cn` 已对 SSE 路由禁用 buffering/cache/gzip,并设置长 read/send timeout。
- SSE route:
- `/api/sessions/<id>/events`
- `/api/agent/runs/<id>/events`
@@ -688,7 +688,7 @@ Data boundary:
- LaunchAgent 参数包含 `--prune --retention-days 30`
- 只清理 `reports/runtime-slo` 顶层过期 `.json/.md`
- 最新报告 `reportPrune.errors=[]`
- P5.14 已给四个 goosed worker 增加常驻 sidecar heartbeat:
- P5.14 已给全部 goosed worker(当前 9 个,`18006..18014`增加常驻 sidecar heartbeat:
- `goosed-1`: `statusCode=200`, `latencyMs=62`
- `goosed-2`: `statusCode=200`, `latencyMs=46`
- `goosed-3`: `statusCode=200`, `latencyMs=46`
@@ -7,7 +7,7 @@
执行状态:
- P0 StreamController v1: 已完成代码落地、语法检查、Portal kickstart 和基础健康验证。
- P1 Gateway SSE 配置: 已完成,`mm.tkmind.cn` 接入 PortalSSE route 已禁缓冲。
- P1 Gateway SSE 配置: 已完成,`m.tkmind.cn` 接入 PortalSSE route 已禁缓冲。
- P2 Redis Runtime State + Router v1: 已完成可回退版本并启用 Redis。
- P3 Observability: 已完成第一版,只读 runtime 状态接口已上线。
- P4 Tool Gateway v1: 已完成第一步,普通用户默认不再暴露 Aider/OpenHands,显式用户白名单保留。
@@ -53,9 +53,9 @@
## 当前基线
- Memind Portal: `/Users/john/Project/Memind/server.mjs`,本机 `:8081`
- H5 public base: `https://mm.tkmind.cn``m.tkmind.cn` 暂时不再作为 H5 public base
- goosed worker pool: Docker/Colima 内 `goosed-prod-1..4`,宿主机 `18006..18009`,当前健康
- H5 上游配置: `TKMIND_API_TARGETS=https://127.0.0.1:18006,...,18009`
- H5 public base: `https://m.tkmind.cn`
- goosed worker pool: Docker/Colima 内 `goosed-prod-1..9`,宿主机 `18006..18014`2026-07-07 确认 9 个 healthy
- H5 上游配置: `TKMIND_API_TARGETS=https://127.0.0.1:18006,...,18014`(须与 compose 容器数一致)
- 当前已有 session affinity: start 时分配 worker,后续 reply/events 回到同一 worker。
- imgproxy: 原生 `127.0.0.1:20082`,兼容代理 `10.10.0.2:20081`
- 风险点: SSE 代理尚未完整处理 `flushHeaders``X-Accel-Buffering`、客户端断开 abort、写入 backpressure 和统一 pipeline 收尾。
@@ -113,11 +113,11 @@ flowchart LR
- `node --check /Users/john/Project/Memind/server.mjs` 通过。
- 本机 `http://127.0.0.1:8081/api/status` 仍返回 `ok`
- 四个 `https://127.0.0.1:18006..18009/status` 仍返回 `ok`
- 全部 `https://127.0.0.1:18006..18014/status` 仍返回 `ok`
### P1: Gateway SSE 配置
目标: `mm.tkmind.cn` 新入口和本机入口不缓冲 SSE;后续 H5 不再走 105 nginx 反向隧道转发。
目标: `m.tkmind.cn` 新入口和本机入口不缓冲 SSE;后续 H5 不再走 105 nginx 反向隧道转发。
任务:
@@ -128,12 +128,12 @@ flowchart LR
- `proxy_read_timeout 3600`
- `proxy_send_timeout 3600`
- `add_header X-Accel-Buffering no always`
- 确认 `mm.tkmind.cn/api/status` 返回真实 H5 API,而不是维护页 HTML。
- 确认 `m.tkmind.cn/api/status` 返回真实 H5 API,而不是维护页 HTML。
-`m.tkmind.cn -> 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081` 标记为 legacy/rollback-only。
验收:
- `mm.tkmind.cn/api/status` 返回后端 `ok` 或明确 JSON health,而不是维护页 HTML。
- `m.tkmind.cn/api/status` 返回后端 `ok` 或明确 JSON health,而不是维护页 HTML。
- SSE 响应头包含 `X-Accel-Buffering: no`
### P2: Redis Runtime State + Router v1
@@ -220,7 +220,7 @@ Drain 操作:
```bash
docker exec memind-runtime-redis redis-cli SET memind:runtime:worker:goosed-3:drain 1
curl -sk https://mm.tkmind.cn/api/runtime/status
curl -sk https://m.tkmind.cn/api/runtime/status
docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:drain
```
@@ -233,7 +233,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
## 执行顺序
1. P0 立即执行,低风险高收益。
2. P1 基于 `mm.tkmind.cn` 新入口执行;除非明确回滚,不再改造 105 H5 转发链路。
2. P1 基于 `m.tkmind.cn` 新入口执行;除非明确回滚,不再改造 105 H5 转发链路。
3. P2/P3 一起推进,先观测再调度。
4. P4/P5 在流式链路稳定后推进。
@@ -272,7 +272,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
决策:
- H5 public base 从 `https://m.tkmind.cn` 临时切换为 `https://mm.tkmind.cn`
- H5 public base 从 `https://m.tkmind.cn` 临时切换为 `https://m.tkmind.cn`
- 后续 H5 公网访问不再走 105 转发链路。
- 旧链路 `m.tkmind.cn -> 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081` 仅保留为 legacy/rollback-only 说明,不作为 Memind 2.0 改造目标。
@@ -285,25 +285,25 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
已执行:
-`H5_PUBLIC_BASE_URL` 改为 `https://mm.tkmind.cn`
-`VITE_MINDSPACE_BASE` 改为 `https://mm.tkmind.cn`
- 将 Portal 启动脚本默认 `H5_PUBLIC_BASE_URL` 改为 `https://mm.tkmind.cn`
-`H5_PUBLIC_BASE_URL` 改为 `https://m.tkmind.cn`
-`VITE_MINDSPACE_BASE` 改为 `https://m.tkmind.cn`
- 将 Portal 启动脚本默认 `H5_PUBLIC_BASE_URL` 改为 `https://m.tkmind.cn`
- 更新 `RUNBOOK.txt`,明确 105 转发链路不再作为后续 H5 公网路径。
- 通过 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal` 重启 Portal。
验证:
- Portal 新 PID: `94324`,监听 `*:8081`
- 运行环境确认: `H5_PUBLIC_BASE_URL=https://mm.tkmind.cn``VITE_MINDSPACE_BASE=https://mm.tkmind.cn`
- 运行环境确认: `H5_PUBLIC_BASE_URL=https://m.tkmind.cn``VITE_MINDSPACE_BASE=https://m.tkmind.cn`
- 本机 `http://127.0.0.1:8081/api/status` 返回 `ok`
- 个 goosed worker `18006..18009` 均返回 `ok`
- 公网 `https://mm.tkmind.cn/api/status` 当前返回 nginx `502 Bad Gateway`,说明域名侧入口尚未接到当前 Portal;该问题纳入 P1 新入口修复,不再回到 105 转发链路处理。
- 个 goosed worker `18006..18014` 均返回 `ok`
- 公网 `https://m.tkmind.cn/api/status` 当前返回 nginx `502 Bad Gateway`,说明域名侧入口尚未接到当前 Portal;该问题纳入 P1 新入口修复,不再回到 105 转发链路处理。
### 2026-07-02 P1 Gateway SSE 配置
变更文件:
- `/opt/homebrew/etc/nginx/servers/mm.tkmind.cn.conf`
- `/opt/homebrew/etc/nginx/servers/` 下对应 `m.tkmind.cn` 的生产入口配置
- `/Users/john/Project/Memind/scripts/wechat-mp-menu.mjs`
- `/Users/john/Project/Memind/server.mjs`
- `/Users/john/Project/Memind/dist/dev/wechat-share-demo.html`
@@ -311,11 +311,11 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
备份:
- `/opt/homebrew/etc/nginx/backups/mm.tkmind.cn.conf.bak-streaming-20260702-0635`
- `/opt/homebrew/etc/nginx/backups/` 下对应 `m.tkmind.cn` 的 streaming 备份配置
已完成:
- `mm.tkmind.cn` 通过本机 nginx 反向代理到 Portal `127.0.0.1:8081`
- `m.tkmind.cn` 通过本机 nginx 反向代理到 Portal `127.0.0.1:8081`
-`/api/sessions/<id>/events``/api/agent/runs/<id>/events` 增加 SSE 专用 nginx location:
- `proxy_buffering off`
- `proxy_cache off`
@@ -324,16 +324,16 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `proxy_send_timeout 3600s`
- `add_header X-Accel-Buffering no always`
- 通用 `location /` 增加 `proxy_read_timeout 300s``proxy_send_timeout 300s`
- 微信菜单脚本默认入口改为 `https://mm.tkmind.cn``https://mm.tkmind.cn/space`
- `server.mjs``H5_PUBLIC_BASE_URL` 缺省值改为 `https://mm.tkmind.cn`
- 微信菜单脚本默认入口改为 `https://m.tkmind.cn``https://m.tkmind.cn/space`
- `server.mjs``H5_PUBLIC_BASE_URL` 缺省值改为 `https://m.tkmind.cn`
验证:
- `nginx -t` 通过。
- `nginx -s reload` 已执行。
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://mm.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
- 未登录访问 `https://mm.tkmind.cn/api/agent/runs/test-run/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://m.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
- 未登录访问 `https://m.tkmind.cn/api/agent/runs/test-run/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
### 2026-07-02 P2 Redis Runtime State + Router v1
@@ -375,8 +375,8 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `memind:runtime:worker:goosed-3:active_streams=1`
- `memind:runtime:worker:goosed-3:heartbeat=<timestamp>`
- `memind:runtime:stream:20260701_24:status=active`
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- 个 goosed worker `18006..18009` 均返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- 个 goosed worker `18006..18014` 均返回 `ok`
### 2026-07-02 P3 Observability v1
@@ -398,11 +398,11 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
验证:
- `/opt/homebrew/opt/node@24/bin/node --check /Users/john/Project/Memind/server.mjs` 通过。
- `https://mm.tkmind.cn/api/runtime/status` 返回 `ok: true`
- `https://m.tkmind.cn/api/runtime/status` 返回 `ok: true`
- 返回中 `router.enabled=true`namespace 为 `memind:runtime`
- 返回中个目标 `18006..18009``healthy: true`
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://mm.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
- 返回中个目标 `18006..18014``healthy: true`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://m.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
### 2026-07-02 P4 Tool Gateway v1 第一阶段
@@ -435,10 +435,10 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
验证:
- `/opt/homebrew/opt/node@24/bin/node --check /Users/john/Project/Memind/server.mjs` 通过。
- `https://mm.tkmind.cn/api/runtime/status` 返回四个 worker `healthy=true`
- `https://mm.tkmind.cn/api/runtime/status` 返回四个 worker `drain=false`
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://mm.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
- `https://m.tkmind.cn/api/runtime/status` 返回四个 worker `healthy=true`
- `https://m.tkmind.cn/api/runtime/status` 返回四个 worker `drain=false`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- 未登录访问 `https://m.tkmind.cn/api/sessions/test-session/events` 返回 `401`,且响应头包含 `x-accel-buffering: no`
### 2026-07-02 生产同步分支
@@ -453,7 +453,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- 新建干净源码目录 `/Users/john/Project/memind-clean-main-20260702`
-`origin/main` 创建分支 `memind-streaming-runtime-20260702`
- 迁入本次已在生产验证的 StreamController、Redis Router、runtime status、`mm.tkmind.cn` 和 nginx SSE 配置样例。
- 迁入本次已在生产验证的 StreamController、Redis Router、runtime status、`m.tkmind.cn` 和 nginx SSE 配置样例。
- 新增远程开发机同步说明 `docs/architecture/remote-dev-sync.md`
限制:
@@ -485,7 +485,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `score`
- 新增只读检查脚本 `scripts/check-stream-runtime.mjs`
- 新增 drain 运维脚本 `scripts/runtime-worker-drain.mjs`
- runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 `mm.tkmind.cn -> local nginx -> Portal :8081`
- runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 `m.tkmind.cn -> local nginx -> Portal :8081`
- 本地和远端分支提交 `4420cca chore: add runtime observability ops`
- 修正检查脚本为 `HEAD` 探测 SSE 入口,避免健康检查打开真实上游 SSE;提交 `c9b7252 fix: avoid opening sse in runtime check`
@@ -576,8 +576,8 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
生产验证:
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://mm.tkmind.cn/api/runtime/status` 返回 router enabled四个 goosed target healthy。
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/runtime/status` 返回 router enabled全部 goosed target healthy。
- `scripts/check-stream-runtime.mjs` 返回 `ok=true`
- `scripts/check-tool-runtime.mjs` 返回 `ok=true`,普通 chat 不注入 Aider/OpenHandscode mode 对白名单用户可注入。
- `scripts/runtime-worker-drain.mjs status` 返回 `ok=true`,四个 worker drain=false。
@@ -676,9 +676,9 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
生产验证:
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `/api/runtime/status.toolRuntime.codeRunsEnabled=false`
- 四个 goosed target healthy。
- 全部 goosed target healthy。
- `scripts/check-agent-code-run-entry.mjs` 返回 `ok=true`
- `scripts/check-stream-runtime.mjs` 返回 `ok=true`
- `scripts/runtime-worker-drain.mjs status` 返回 `ok=true`,四个 worker drain=false。
@@ -735,7 +735,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `dryRun=true`
- 四个 worker 均列出。
- 当前无 stale worker,未执行 apply。
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
### 2026-07-02 P5.7 Worker Metrics 采样接入
@@ -752,7 +752,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `status`:采样但不写 Redis。
- `sample`:采样并写入 Redis。
- `--dry-run`:强制只读。
- 对 Docker worker `goosed-prod-1..4` 采集:
- 对 Docker worker `goosed-prod-1..9` 采集:
- `cpuLoad`
- `memoryPressure`
- `fdPressure`
@@ -789,7 +789,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
生产验证:
- `node scripts/runtime-worker-metrics.mjs sample` 返回 `ok=true`四个 `goosed-prod-*``healthy`
- `node scripts/runtime-worker-metrics.mjs sample` 返回 `ok=true`全部 `goosed-prod-*``healthy`
- `/api/runtime/status.router.workers[]` 已显示:
- `cpuLoad`
- `memoryPressure`
@@ -799,7 +799,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `containerHealth`
- `metricsSampledAt`
-`score`
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `scripts/check-stream-runtime.mjs` 返回 `ok=true`
- `scripts/check-agent-code-run-entry.mjs` 返回 `ok=true`
- `scripts/runtime-worker-drain.mjs reconcile` 返回 `ok=true``dryRun=true`
@@ -903,7 +903,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `/Users/john/Project/Memind/server.mjs`
- `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs`
- 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`
- 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`
- 重启窗口内 `m.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://m.tkmind.cn/api/status` 返回 `ok`
生产验证:
@@ -963,7 +963,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `/Users/john/Project/Memind/server.mjs`
- `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs`
- 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`
- 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`
- 重启窗口内 `m.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://m.tkmind.cn/api/status` 返回 `ok`
生产验证:
@@ -1029,7 +1029,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `/Users/john/Project/Memind/scripts/agent-run-worker.mjs`
- `/Users/john/Project/Memind/RUNBOOK.txt`
- 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`
- 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 runtime status 返回 `ok=true`
- 重启窗口内 `m.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 runtime status 返回 `ok=true`
生产验证:
@@ -1084,7 +1084,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs`
- `/Users/john/Project/Memind/scripts/agent-run-worker.mjs`
- 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`
- 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`
- 重启窗口内 `m.tkmind.cn` 曾短暂返回 nginx `502`Portal 随后正常监听 `:8081`,公网 `https://m.tkmind.cn/api/status` 返回 `ok`
生产验证:
@@ -1333,7 +1333,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
- `toolRuntime.queue.inFlight=0`
- `toolRuntime.queue.pendingDispatches=0`
- `toolRuntime.queue.statusCounts={}`
- `mm.tkmind.cn` runtime health 仍为 `ok=true`
- `m.tkmind.cn` runtime health 仍为 `ok=true`
### 2026-07-02 P5.13 Runtime SLO Report Retention
@@ -1444,7 +1444,7 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
发布后状态:
- `https://mm.tkmind.cn/api/status`: `ok`
- `https://m.tkmind.cn/api/status`: `ok`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.chatInjectsCodeTools=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -1536,7 +1536,7 @@ canary 验证:
- `/Users/john/Project/backups/memind/memind-persisted-20260702-085706-a0af049-before.tar.gz`
- `/Users/john/Project/archives/Memind-source-before-20260702-085706-a0af049`
- 恢复后验证:
- `https://mm.tkmind.cn/api/status`: `ok`
- `https://m.tkmind.cn/api/status`: `ok`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.chatInjectsCodeTools=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -1668,8 +1668,8 @@ canary 验证:
- 未删除 PG 数据
- 未删除或改写 MindSpace 用户数据
- 发布验证:
- `https://mm.tkmind.cn/api/status`: `ok`
- `https://mm.tkmind.cn/api/runtime/status`: `ok=true`
- `https://m.tkmind.cn/api/status`: `ok`
- `https://m.tkmind.cn/api/runtime/status`: `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.chatInjectsCodeTools=false`
- `toolRuntime.queue.toolGateway.enabled=false`
@@ -1809,7 +1809,7 @@ Dry-run canary:
- 生产 `.env``MEMIND_TOOL_GATEWAY_*`
- 生产 `.env``MEMIND_AGENT_CODE_RUNS_*`
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.toolGateway.enabled=false`
@@ -1891,7 +1891,7 @@ file proof:
- 生产 `.env``MEMIND_TOOL_GATEWAY_*`
- 生产 `.env``MEMIND_AGENT_CODE_RUNS_*`
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.toolGateway.enabled=false`
@@ -1986,7 +1986,7 @@ file proof:
- 已恢复 `.env`
- 临时登录 session 已 revoke。
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -2056,11 +2056,11 @@ file proof:
- release 脚本第一次在已知历史 docx 缺失链接处停止。
- 按用户要求不修复/不删除/不改写历史用户数据,第二次发布使用 `ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1` 仅跳过该守卫。
- release 过程中脚本完成当前 live 全目录备份和持久目录备份。
- 105 legacy tunnel step 仍由脚本执行并健康,但 H5 主路径继续是 `https://mm.tkmind.cn`
- 105 legacy tunnel step 仍由脚本执行并健康,但 H5 主路径继续是 `https://m.tkmind.cn`
发布后状态:
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -2150,7 +2150,7 @@ installer 默认行为:
发布后状态:
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -2266,7 +2266,7 @@ canary repo:
- `installed-disabled`
- `state=not running`
- no `agent-run-worker.mjs` process
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -2338,7 +2338,7 @@ canary repo:
发布后验证:
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunsEnabled=false`
- `toolRuntime.queue.autoDispatch=true`
@@ -2426,14 +2426,14 @@ runtime/status:
- release 脚本在 legacy `m.tkmind.cn` tunnel step 短暂返回失败;Portal runtime 已切换且健康。
- 后续检查显示:
- `mm.tkmind.cn` 主路径正常。
- `m.tkmind.cn` 主路径正常。
- `cn.tkmind.memind-portal-tunnel` 已恢复 running。
- Portal node pid `35983` 正在监听 `:8081`
- 该 legacy tunnel 不是当前 H5 主路径,当前 H5 主路径仍是 `https://mm.tkmind.cn`
- 该 legacy tunnel 不是当前 H5 主路径,当前 H5 主路径仍是 `https://m.tkmind.cn`
发布后状态:
- `https://mm.tkmind.cn/api/runtime/status`:
- `https://m.tkmind.cn/api/runtime/status`:
- `ok=true`
- `toolRuntime.codeRunPolicy.enabled=false`
- `toolRuntime.codeRunPolicy.userAllowlist=[]`
@@ -2722,7 +2722,7 @@ runtime/status:
验证:
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `/api/runtime/status`:
- `codeRunPolicy.requireValidation=true`
- `toolRuntime.queue.maxConcurrentRuns=2`
@@ -2849,8 +2849,8 @@ runtime/status:
生产验证:
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://mm.tkmind.cn/auth/status` 未登录返回 `200 {"authenticated":false,"mode":"user"}`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/auth/status` 未登录返回 `200 {"authenticated":false,"mode":"user"}`
- `/api/runtime/status.toolRuntime`:
- `codeRunsEnabled=true`
- `requireValidation=true`
@@ -2909,7 +2909,7 @@ runtime/status:
生产验证:
- `https://mm.tkmind.cn/` 已加载新 bundle:
- `https://m.tkmind.cn/` 已加载新 bundle:
- `/assets/index-jYf9W5vk.js`
- `/api/runtime/status`:
- `ok=true`
@@ -2990,7 +2990,7 @@ runtime/status:
生产验证:
- `https://mm.tkmind.cn/api/status` 返回 `ok`
- `https://m.tkmind.cn/api/status` 返回 `ok`
- `/api/runtime/status.toolRuntime.queue`:
- `maxConcurrentRuns=2`
- `heartbeatMs=30000`
+7 -7
View File
@@ -19,7 +19,7 @@
- 不在生产运行目录执行 `git reset --hard``git clean -fdx``rm -rf`、数据库清理或数据目录同步删除。
- 不提交生产 `.env`、数据库、`MindSpace/``data/``users/``.tailscale/``logs/`、证书、密钥和运行缓存。
- 本次 H5 public base 临时切到 `https://mm.tkmind.cn`
- 本次 H5 public base 临时切到 `https://m.tkmind.cn`
- 后续公网 H5 不再依赖 105 转发链路;105 只保留 legacy/rollback 说明。
## 远程开发机拉取方式
@@ -51,8 +51,8 @@ cp .env.example .env
生产关键项:
```text
H5_PUBLIC_BASE_URL=https://mm.tkmind.cn
VITE_MINDSPACE_BASE=https://mm.tkmind.cn
H5_PUBLIC_BASE_URL=https://m.tkmind.cn
VITE_MINDSPACE_BASE=https://m.tkmind.cn
MEMIND_RUNTIME_REDIS_URL=redis://127.0.0.1:6379/0
MEMIND_RUNTIME_REDIS_NAMESPACE=memind:runtime
```
@@ -62,8 +62,8 @@ MEMIND_RUNTIME_REDIS_NAMESPACE=memind:runtime
- StreamController v1: SSE headers、flush、abort propagation、backpressure-aware pipeline。
- Redis Runtime Router v1: session pointer、worker score、active stream、drain。
- Runtime status: `/api/runtime/status`
- Gateway SSE 示例: `deploy/nginx/mm.tkmind.cn.conf`
- H5 public base: `mm.tkmind.cn`
- Gateway SSE 示例: `m.tkmind.cn` 对应的 nginx SSE 配置
- H5 public base: `m.tkmind.cn`
- 普通用户默认不暴露 Aider/OpenHands,显式白名单保留。
## 验证命令
@@ -71,8 +71,8 @@ MEMIND_RUNTIME_REDIS_NAMESPACE=memind:runtime
```bash
node --check server.mjs
node --check scripts/wechat-mp-menu.mjs
curl -sk https://mm.tkmind.cn/api/status
curl -sk https://mm.tkmind.cn/api/runtime/status
curl -sk https://m.tkmind.cn/api/status
curl -sk https://m.tkmind.cn/api/runtime/status
```
## 回滚思路
+2 -2
View File
@@ -109,8 +109,8 @@ Plaza 本地开发说明见 [plaza-local.md](./plaza-local.md)。生产发布、
| 项 | 本地(本机 `.env` / `../../.env.local` | 103 生产(服务器 `/Users/john/Project/Memind/.env` |
|---|---|---|
| 配置真相 | 复制 `.env.example`,只填本地联调值 | 仅在 103 维护,**不提交 Git** |
| `TKMIND_API_TARGET` | 通常单实例 `https://127.0.0.1:18006` | goosed 实例,见 [103 runtime topology](./103-runtime-topology.md) |
| `H5_PUBLIC_BASE_URL` | `http://127.0.0.1:5173` | `https://mm.tkmind.cn` |
| `TKMIND_API_TARGET` | 通常单实例 `https://127.0.0.1:18006` | 9 个 goosed target`18006``18014`,见 [103 runtime topology](./103-runtime-topology.md) |
| `H5_PUBLIC_BASE_URL` | `http://127.0.0.1:5173` | `https://m.tkmind.cn` |
| `MEMIND_SESSION_BROKER_ENABLED` | Patch 2 起可本地设 `1` 验证 | 灰度窗口由运维在 103 `.env` 单独开启 |
| RDS / Redis | 可连本地 MySQL 或留空 | 生产 RDS + Redis,见 103 `.env` |
@@ -45,7 +45,7 @@
| 检查项 | 结果 |
|--------|------|
| Portal `8081/api/status` | 200 |
| goosed 1800618009 `/status` | 200 |
| goosed `18006``18014` `/status` | 2002026-07-07 起为 9 个 target0706007 时代为 4 个) |
| `https://m.tkmind.cn/auth/login` 错误密码 | 401 + `用户名或密码错误` |
| Portal 日志关键词 | 无异常关键词 |
| memind_adm API `8085/health` | 200 |
+11 -14
View File
@@ -86,7 +86,7 @@ curl -k -i https://m.tkmind.cn/auth/login \
```bash
ssh john@58.38.22.103 '
printf "portal="; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081/api/status
for p in 18006 18007 18008 18009; do
for p in $(seq 18006 18014); do
printf "goosed_${p}="
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1:${p}/status
done
@@ -100,10 +100,7 @@ ssh john@58.38.22.103 '
必须看到:
- `portal=200`
- `goosed_18006=200`
- `goosed_18007=200`
- `goosed_18008=200`
- `goosed_18009=200`
- `goosed_18006=200``goosed_18014=200`(共 9 个 target
- `DATABASE_URL=SET`
- `TKMIND_API_TARGETS=SET`
- `GOOSED_MCP_NODE_PATH=SET`
@@ -193,10 +190,10 @@ TKMIND_ENABLE_CODE_EXECUTION_EXTENSION=1
Portal 现在使用:
```bash
TKMIND_API_TARGETS=https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009
TKMIND_API_TARGETS=https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009,https://127.0.0.1:18010,https://127.0.0.1:18011,https://127.0.0.1:18012,https://127.0.0.1:18013,https://127.0.0.1:18014
```
发布或重建 goosed 容器后,要确认 18006-18009 都有 provider 配置。否则 Agent 会出现:
发布或重建 goosed 容器后,要确认 `18006``18014` 全部 target 都有 provider 配置。否则 Agent 会出现:
```text
Provider 'custom_deepseek' is not configured
@@ -233,7 +230,7 @@ ssh john@58.38.22.103 '
export PORTAL_RUNTIME_DIR="$portal_runtime_dir" MINDSPACE_ROOT="$mindspace_root"
set +a
/opt/homebrew/bin/docker compose -f docker-compose.prod.yml ps
for c in goosed-prod-1 goosed-prod-2 goosed-prod-3 goosed-prod-4; do
for c in goosed-prod-{1..9}; do
echo "--- $c"
/opt/homebrew/bin/docker exec "$c" sh -lc "
ls -l /usr/local/bin/node \
@@ -248,9 +245,9 @@ ssh john@58.38.22.103 '
重建容器后必须重新确认:
- 个容器都是 `healthy`
- 18006-18009 `/status` 都是 200。
- provider 已同步到四个 target。
- 个容器都是 `healthy`
- `18006``18014` `/status` 都是 200。
- provider 已同步到全部 target。
- sandbox-fs smoke 能成功 `add extension` 并完成一次 `/reply`
## 6. 出问题时先走这条判断链
@@ -259,9 +256,9 @@ ssh john@58.38.22.103 '
2. `/auth/login` 是否进入 400/401,而不是 503
3. 103 Portal 进程是否拿到了 `DATABASE_URL`
4. `~/Library/Logs/memind-portal.log` 是否有 `User auth bootstrap failed`
5. 18006-18009 是否全 200
5. `18006``18014` 是否全 200
6. goosed 容器内 `/usr/local/bin/node``/opt/portal/mindspace-sandbox-mcp.mjs` 是否存在?
7. provider 是否同步到四个 target
7. provider 是否同步到全部 target
8. 如果是微信服务号或专属 Agent 报错,必须测真实业务路径,不要只看健康接口。
## 7. 发布完成后给用户的最小回报格式
@@ -274,7 +271,7 @@ ssh john@58.38.22.103 '
- persisted backup
- Portal 健康结果
- `/auth/login` 结果
- 18006-18009 健康结果
- `18006``18014` 健康结果(共 9 个 target
- 是否发现并处理了日志里的错误关键词
不要只说“已发布成功”。
+50
View File
@@ -50,6 +50,7 @@ export async function materializePrivateAssetsInWorkspaceHtml({
htmlRelativePath,
writeBack = false,
}) {
PRIVATE_ASSET_URL_PATTERN.lastIndex = 0;
const matches = [...String(html).matchAll(PRIVATE_ASSET_URL_PATTERN)];
if (matches.length === 0) {
return { html, count: 0, changed: false };
@@ -106,6 +107,55 @@ export async function materializePrivateAssetsInWorkspaceHtml({
return { html: result, count, changed: true };
}
export function htmlReferencesPrivateAssets(html) {
PRIVATE_ASSET_URL_PATTERN.lastIndex = 0;
const found = PRIVATE_ASSET_URL_PATTERN.test(String(html ?? ''));
PRIVATE_ASSET_URL_PATTERN.lastIndex = 0;
return found;
}
export async function materializePrivateAssetsInPublicHtmlFiles({
pool,
storageRoot,
h5Root,
userId,
publishDir,
relativePaths = [],
}) {
const materialized = [];
const skipped = [];
const uniquePaths = [...new Set(relativePaths)].filter(Boolean);
for (const htmlRelativePath of uniquePaths) {
const htmlPath = path.join(publishDir, htmlRelativePath);
let html;
try {
html = await fs.readFile(htmlPath, 'utf8');
} catch {
skipped.push(htmlRelativePath);
continue;
}
if (!htmlReferencesPrivateAssets(html)) {
skipped.push(htmlRelativePath);
continue;
}
const result = await materializePrivateAssetsInWorkspaceHtml({
pool,
storageRoot,
h5Root,
userId,
html,
htmlRelativePath,
writeBack: true,
});
if (result.changed) {
materialized.push({ relativePath: htmlRelativePath, count: result.count });
} else {
skipped.push(htmlRelativePath);
}
}
return { materialized, skipped };
}
export async function repairMissingHtmlAssetReferences({ pool, userId, sourceContent, html }) {
const collectIds = (text) => [
...new Set([...String(text).matchAll(PRIVATE_ASSET_URL_PATTERN)].map((match) => match[1])),
+39
View File
@@ -14,6 +14,7 @@ import {
findPublishHtml,
injectHtmlBaseHref,
materializePrivateAssetsInWorkspaceHtml,
materializePrivateAssetsInPublicHtmlFiles,
repairMissingHtmlAssetReferences,
resolveClosestHtmlRelativePath,
resolveChatSaveAnalysis,
@@ -226,6 +227,44 @@ test('materializePrivateAssetsInWorkspaceHtml copies images to public/.tmp-image
await fs.stat(path.join(publishDir, '.tmp-images', `${assetId}.jpg`));
});
test('materializePrivateAssetsInPublicHtmlFiles rewrites referenced public html files', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-chat-save-materialize-batch-'));
const storageRoot = path.join(root, 'storage');
const assetId = '44444444-4444-4444-8444-444444444444';
const userId = USER_ID;
const storageKey = `users/${userId}/assets/${assetId}/versions/v1`;
const sourcePath = path.join(storageRoot, storageKey);
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(sourcePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
const publishDir = path.join(root, 'MindSpace', userId);
const htmlRelativePath = 'public/share.html';
const htmlPath = path.join(publishDir, htmlRelativePath);
await fs.mkdir(path.dirname(htmlPath), { recursive: true });
const html = `<!doctype html><img src="/api/mindspace/v1/assets/${assetId}/download?inline=1">`;
await fs.writeFile(htmlPath, html, 'utf8');
const pool = {
query: async () => [
[{ id: assetId, mime_type: 'image/jpeg', original_filename: 'hero.jpg', storage_key: storageKey }],
],
};
const result = await materializePrivateAssetsInPublicHtmlFiles({
pool,
storageRoot,
h5Root: root,
userId,
publishDir,
relativePaths: [htmlRelativePath],
});
assert.equal(result.materialized.length, 1);
assert.equal(result.materialized[0].relativePath, htmlRelativePath);
const saved = await fs.readFile(htmlPath, 'utf8');
assert.match(saved, /\.tmp-images\/44444444-4444-4444-8444-444444444444\.jpg/);
});
test('repairMissingHtmlAssetReferences swaps invalid html asset ids from chat message', async () => {
const validId = '22222222-2222-4222-8222-222222222222';
const invalidId = '33333333-3333-4333-8333-333333333333';
+24 -2
View File
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { extractStaticPageLinks } from './mindspace-chat-save.mjs';
import { extractStaticPageLinks, materializePrivateAssetsInPublicHtmlFiles } from './mindspace-chat-save.mjs';
import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.mjs';
import { scheduleWorkspaceHtmlThumbnailSidecars } from './mindspace-workspace-thumbnails.mjs';
@@ -480,6 +480,9 @@ export async function syncPublicHtmlAfterFinish({
syncWorkspaceAssets,
registerPublicHtmlArtifacts,
sessionId = null,
pool = null,
storageRoot = null,
h5Root = null,
} = {}) {
if (!hasRecentOwnPublicHtmlReference(messages, currentUser, { publishDir })) {
return { materialized: [], skipped: [], synced: false };
@@ -496,6 +499,17 @@ export async function syncPublicHtmlAfterFinish({
});
const publicHtmlRelativePaths = publicHtmlArtifactRefs.map((ref) => ref.relativePath);
const sourceMessageId = publicHtmlArtifactRefs.find((ref) => ref.messageId)?.messageId ?? null;
let assetMaterialization = { materialized: [], skipped: [] };
if (pool && storageRoot && h5Root && currentUser?.id && publicHtmlRelativePaths.length > 0) {
assetMaterialization = await materializePrivateAssetsInPublicHtmlFiles({
pool,
storageRoot,
h5Root,
userId: currentUser.id,
publishDir,
relativePaths: publicHtmlRelativePaths,
}).catch(() => ({ materialized: [], skipped: publicHtmlRelativePaths }));
}
let synced = false;
if (typeof syncWorkspaceAssets === 'function' && currentUser?.id) {
await syncWorkspaceAssets(currentUser.id, {
@@ -512,5 +526,13 @@ export async function syncPublicHtmlAfterFinish({
artifactRefs: publicHtmlArtifactRefs,
});
}
return { materialized, skipped, synced, docxSync, publicHtmlRelativePaths, publicHtmlArtifactRefs };
return {
materialized,
skipped,
synced,
docxSync,
publicHtmlRelativePaths,
publicHtmlArtifactRefs,
assetMaterialization,
};
}
+52
View File
@@ -249,6 +249,58 @@ test('syncPublicHtmlAfterFinish registers existing public html artifacts for the
}
});
test('syncPublicHtmlAfterFinish materializes private image assets into public html files', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-assets-'));
const publishDir = path.join(root, 'MindSpace', CURRENT_USER.id);
const storageRoot = path.join(root, 'storage');
const assetId = '55555555-5555-4555-8555-555555555555';
const storageKey = `users/${CURRENT_USER.id}/assets/${assetId}/versions/v1`;
fs.mkdirSync(path.dirname(path.join(storageRoot, storageKey)), { recursive: true });
fs.writeFileSync(path.join(storageRoot, storageKey), Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(
path.join(publishDir, 'public/tea.html'),
`<!doctype html><img src="/api/mindspace/v1/assets/${assetId}/download?inline=1">`,
);
const messages = [
{
id: 'msg-tea-1',
role: 'assistant',
content: [
{
type: 'text',
text: `[茶馆](https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/tea.html)`,
},
],
},
];
const pool = {
query: async () => [
[{ id: assetId, mime_type: 'image/jpeg', original_filename: 'tea.jpg', storage_key: storageKey }],
],
};
const result = await syncPublicHtmlAfterFinish({
messages,
currentUser: CURRENT_USER,
publishDir,
sessionId: 'session-tea',
pool,
storageRoot,
h5Root: root,
syncWorkspaceAssets: async () => {},
});
assert.equal(result.assetMaterialization.materialized.length, 1);
const saved = fs.readFileSync(path.join(publishDir, 'public/tea.html'), 'utf8');
assert.match(saved, /\.tmp-images\/55555555-5555-4555-8555-555555555555\.jpg/);
assert.ok(fs.existsSync(path.join(publishDir, 'public/.tmp-images', `${assetId}.jpg`)));
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('collectOwnPublicHtmlArtifactRefs includes source message ids when available', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
+8 -2
View File
@@ -65,7 +65,13 @@ async function exists(targetPath) {
}
async function remove(targetPath) {
await fs.rm(targetPath, { recursive: true, force: true });
if (!(await exists(targetPath))) return;
try {
await fs.rm(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
} catch (error) {
if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EBUSY') throw error;
await run('rm', ['-rf', targetPath]);
}
}
async function copyDir(source, target) {
@@ -397,7 +403,7 @@ async function writeMetadata() {
'Deployment and operations transport:',
' H5 public domain: m.tkmind.cn',
' Current public path: m.tkmind.cn -> 105 nginx -> 103 Portal :8081',
' Test-only path: mm.tkmind.cn -> 105 nginx -> local/103 test routing when explicitly enabled.',
' No separate public test domain remains; use explicit local/test overrides only when needed.',
' Production H5 and WeChat links should use m.tkmind.cn unless a test rollout explicitly overrides H5_PUBLIC_BASE_URL.',
'',
'Streaming runtime operations:',
+33 -2
View File
@@ -214,7 +214,17 @@ fi
docker_bin="/opt/homebrew/bin/docker"
if [[ -x "${docker_bin}" ]] && "${docker_bin}" ps --format '{{.Names}}' 2>/dev/null | grep -q '^goosed-prod-1$'; then
for index in 1 2 3 4; do
goosed_indexes=()
while IFS= read -r name; do
if [[ "${name}" =~ ^goosed-prod-([0-9]+)$ ]]; then
goosed_indexes+=("${BASH_REMATCH[1]}")
fi
done < <("${docker_bin}" ps --format '{{.Names}}' | sort -V)
if ((${#goosed_indexes[@]} == 0)); then
echo "goosed dependency check failed: no goosed-prod-* containers running" >&2
missing=1
fi
for index in "${goosed_indexes[@]}"; do
container="goosed-prod-${index}"
host_port=$((18005 + index))
if ! "${docker_bin}" ps --format '{{.Names}} {{.Ports}} {{.Status}}' | grep -q "^${container} .*0.0.0.0:${host_port}->18006/tcp.*healthy"; then
@@ -233,7 +243,7 @@ if [[ -x "${docker_bin}" ]] && "${docker_bin}" ps --format '{{.Names}}' 2>/dev/n
echo "goosed dependency check failed: Portal .env must set GOOSED_MCP_NODE_PATH and GOOSED_MCP_SERVER_PATH for Docker goosed" >&2
missing=1
else
for index in 1 2 3 4; do
for index in "${goosed_indexes[@]}"; do
container="goosed-prod-${index}"
if ! "${docker_bin}" exec "${container}" sh -lc "test -x '${portal_mcp_node}' && test -f '${portal_mcp_server}'" >/dev/null 2>&1; then
echo "goosed dependency check failed: ${container} cannot resolve Portal .env MCP paths '${portal_mcp_node}' and '${portal_mcp_server}'" >&2
@@ -241,6 +251,27 @@ if [[ -x "${docker_bin}" ]] && "${docker_bin}" ps --format '{{.Names}}' 2>/dev/n
fi
done
fi
targets_line="$(grep -E '^TKMIND_API_TARGETS=' /Users/john/Project/Memind/.env | tail -1 | cut -d= -f2- || true)"
if [[ -z "${targets_line}" ]]; then
echo "goosed dependency check failed: Portal .env must set TKMIND_API_TARGETS for Docker goosed pool" >&2
missing=1
else
IFS=',' read -ra target_urls <<< "${targets_line}"
for target_url in "${target_urls[@]}"; do
trimmed="$(echo "${target_url}" | tr -d ' \"')"
if [[ -z "${trimmed}" ]]; then
continue
fi
status="$(curl -skS -m 5 "${trimmed}/status" 2>/dev/null || true)"
if [[ "${status}" != "ok" ]]; then
echo "goosed dependency check failed: ${trimmed}/status != ok (${status:-empty})" >&2
missing=1
fi
done
if ((${#target_urls[@]} != ${#goosed_indexes[@]})); then
echo "goosed dependency check warning: TKMIND_API_TARGETS count (${#target_urls[@]}) != running containers (${#goosed_indexes[@]})" >&2
fi
fi
fi
else
api_secret="$(grep -E '^TKMIND_SERVER__SECRET_KEY=' /Users/john/Project/Memind/.env 2>/dev/null | tail -1 | cut -d= -f2- || true)"
+1 -1
View File
@@ -14,7 +14,7 @@ fi
export NODE_ENV=production
export H5_PORT="${H5_PORT:-8081}"
export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"
export TKMIND_API_TARGETS="${TKMIND_API_TARGETS:-https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009}"
export TKMIND_API_TARGETS="${TKMIND_API_TARGETS:-https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009,https://127.0.0.1:18010,https://127.0.0.1:18011,https://127.0.0.1:18012,https://127.0.0.1:18013,https://127.0.0.1:18014}"
export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"
export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"
+29
View File
@@ -116,6 +116,7 @@ import {
injectHtmlBaseHref,
resolveClosestHtmlRelativePath,
createAssetDataUriResolver,
htmlReferencesPrivateAssets,
materializePrivateAssetsInWorkspaceHtml,
repairMissingHtmlAssetReferences,
resolveChatSaveAnalysis,
@@ -4846,6 +4847,9 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
currentUser: req.currentUser,
publishDir,
sessionId: sid,
pool: authPool,
storageRoot: resolveMindSpaceRuntimeConfig(__dirname, process.env).storageRoot,
h5Root: __dirname,
syncWorkspaceAssets:
WORKSPACE_MAINTENANCE_ENABLED && mindSpaceAssets
? (userId, options) => mindSpaceAssets.syncWorkspaceAssets(userId, options)
@@ -5728,6 +5732,30 @@ async function sendLongImageDownloadIfRequested(req, res, filePath) {
});
}
async function ensurePublicHtmlPrivateAssetsMaterialized(filePath, html) {
if (!authPool || !htmlReferencesPrivateAssets(html)) return html;
const normalized = path.resolve(String(filePath ?? ''));
const ownerMatch = normalized.match(
new RegExp(`[\\\\/]${PUBLISH_ROOT_DIR}[\\\\/]([0-9a-f-]{36})[\\\\/]`, 'i'),
);
const userId = ownerMatch?.[1] ?? null;
if (!userId) return html;
const publishDir = resolveMindSpaceUserPublishDir(__dirname, { id: userId });
const htmlRelativePath = path.relative(publishDir, normalized).replace(/\\/g, '/');
if (!htmlRelativePath.startsWith(`${PUBLIC_ZONE_DIR}/`)) return html;
const { storageRoot } = resolveMindSpaceRuntimeConfig(__dirname, process.env);
const result = await materializePrivateAssetsInWorkspaceHtml({
pool: authPool,
storageRoot,
h5Root: __dirname,
userId,
html,
htmlRelativePath,
writeBack: true,
}).catch(() => ({ html, changed: false }));
return result.changed ? result.html : html;
}
async function sendPublishFile(req, res, filePath) {
if (!filePath.toLowerCase().endsWith('.html')) {
res.sendFile(filePath, (err) => {
@@ -5743,6 +5771,7 @@ async function sendPublishFile(req, res, filePath) {
res.status(404).json({ message: '文件不存在' });
return;
}
html = await ensurePublicHtmlPrivateAssetsMaterialized(filePath, html);
html = preparePublicHtmlAssetDelivery(html, INTERNAL_AGENT_SECRET);
const embed = isPlazaEmbedRequest(req.query);
if (embed) {