From ebf06b376bb593e44e1589c28b63dacad4bf0942 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 3 Jul 2026 16:34:02 +0800 Subject: [PATCH 1/2] docs: clarify local and 103 runtime topology --- PRODUCTION_RELEASE_RULES.md | 4 +- README.md | 1 + docs/103-105-upgrade-runbook-2026-06-26.md | 2 + docs/103-runtime-topology.md | 113 ++++++++++++++++++ docs/agent-run-worker-rollout-runbook.md | 2 +- ...2-runtime-execution-assessment-20260702.md | 2 + .../memind-2-streaming-agent-runtime-plan.md | 2 + docs/g2-load-balancing.md | 4 +- docs/imgproxy-103-production-runbook.md | 4 +- docs/mindspace-seamless-migration-runbook.md | 51 ++++---- docs/mindspace-service-contract.md | 6 + docs/release-deploy.md | 3 +- docs/service-isolation-runbook.md | 1 + 13 files changed, 164 insertions(+), 31 deletions(-) create mode 100644 docs/103-runtime-topology.md diff --git a/PRODUCTION_RELEASE_RULES.md b/PRODUCTION_RELEASE_RULES.md index 4cf6e02..dc90218 100644 --- a/PRODUCTION_RELEASE_RULES.md +++ b/PRODUCTION_RELEASE_RULES.md @@ -5,10 +5,10 @@ 1. `103` 是正式生产主机,`105` 是云侧入口/历史链路;**本机一律不允许直接 `rsync` 到 `103` 或 `105`**,也不允许在线改源码后继续运行。 2. **禁止 SSH 登录 `105` 直接修改业务代码**(含服务号菜单脚本 `scripts/wechat-mp-menu.mjs`)。105 上文件是部署产物;变更必须:本地 `test-memind` 修改 → Git commit → 正式发布 → 必要时在目标环境执行 API 同步。详见 [docs/105-server-operations.md](docs/105-server-operations.md)。 2. **Portal 生产必须是无源码 runtime 模式**:构建只发生在本机 Mac,产物是 `.runtime/portal/`;`103` 只接收 runtime artifact、继承持久目录、启动服务,**禁止**在 `103` 上 `npm install`、`npm run build` 或保留可运行源码树。 -2. `MindSpace` 独立服务同样必须走单独 runtime artifact:本地 `node scripts/build-mindspace-service-runtime.mjs` -> `bash scripts/release-mindspace-service-prod.sh` -> 上传 `103` -> 备份 `/Users/john/MindSpace` 与共享 `Memind/.env` -> 原子切换到 `/Users/john/MindSpace` -> 健康检查 `127.0.0.1:8082/health` 与 `/mindspace/v1/contract`;禁止手工 SSH 改线上 `/Users/john/MindSpace` 源码。 +2. `MindSpace` 独立服务同样必须走单独 runtime artifact:本地 `node scripts/build-mindspace-service-runtime.mjs` -> `bash scripts/release-mindspace-service-prod.sh` -> 上传 `103` -> 备份 `/Users/john/MindSpace` 与共享 `Memind/.env` -> 原子切换到 `/Users/john/MindSpace` -> 健康检查 `127.0.0.1:8082/health` 与 `/mindspace/v1/contract`;禁止手工 SSH 改线上 `/Users/john/MindSpace` 源码。103 当前拓扑见 [docs/103-runtime-topology.md](docs/103-runtime-topology.md)。 3. Portal 生产发布唯一合法路径是:本地已提交代码 -> 本机 `node scripts/build-portal-runtime.mjs` -> `bash scripts/release-portal-runtime-prod.sh` -> 上传 `103` -> 全量备份 + 持久目录备份 -> 原子切换 live 目录 -> 重启 Portal -> 健康检查。 4. `scripts/release-prod.sh`(源码包发布)已停用,不得再用于 Portal;`rsync_to_server.sh` 与任何面向 `105` 的直接同步脚本也只保留为禁用提示。 -5. 发布包不得携带运行态资产;`.env`、`MindSpace/`、`data/`、`users/`、`.tailscale/`、`public/plaza-covers/`、`logs/` 只能从线上现有 live 目录继承。 +5. Portal 发布包不得携带运行态资产;`.env`、`data/`、`users/`、`.tailscale/`、`public/plaza-covers/`、`logs/` 只能从线上现有 live 目录继承。`/Users/john/MindSpace` 是独立 MindSpace Service 的生产根目录,不属于 Portal runtime 包;`/Users/john/Project/Memind/MindSpace` 只允许作为旧链路兼容/存量目录处理,不得再被写成 MindSpace Service 的当前根目录。 6. runtime artifact 必须包含 `server.mjs` 与 `mindspace-sandbox-mcp.mjs`(`sandbox-fs` 扩展依赖的独立子进程入口)。 7. 生产发布必须从已验证的完整 `main` 打整包:分支代码必须先合并进 `main`,禁止从功能分支、单个 commit、单个修复或局部差异单包发布。 8. 每次生产发布前必须确认 CI 已通过,且没有未合并的关键变更。 diff --git a/README.md b/README.md index 28a8230..6215d97 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ bash scripts/release-portal-runtime-prod.sh ## 文档 - [本地开发(test.*.tkmind.cn)](docs/local-dev.md) +- [本机与 103 运行拓扑](docs/103-runtime-topology.md) - [Memind 小白使用手册](docs/memind-beginner-guide.md) - [标准化开发测试发布约束](ENGINEERING_WORKFLOW_RULES.md) - [开发环境规则](DEVELOPMENT_RELEASE_RULES.md) diff --git a/docs/103-105-upgrade-runbook-2026-06-26.md b/docs/103-105-upgrade-runbook-2026-06-26.md index 080d7b0..d3bcb39 100644 --- a/docs/103-105-upgrade-runbook-2026-06-26.md +++ b/docs/103-105-upgrade-runbook-2026-06-26.md @@ -1,5 +1,7 @@ # 103 / 105 一次性升级实施记录(2026-06-26) +> 历史记录说明:本文记录 2026-06-26 当时的一次性升级事实。2026-07-03 后,103 的 MindSpace Service 已独立部署到 `/Users/john/MindSpace`;`/Users/john/Project/Memind/MindSpace` 只作为旧链路兼容/存量目录处理。当前生产拓扑以 [103 runtime topology](./103-runtime-topology.md) 为准。 + ## 目标 本次升级的目标不是继续修补旧工作树,而是一次性把: diff --git a/docs/103-runtime-topology.md b/docs/103-runtime-topology.md new file mode 100644 index 0000000..c609fc1 --- /dev/null +++ b/docs/103-runtime-topology.md @@ -0,0 +1,113 @@ +# Local and 103 runtime topology + +> Last confirmed: 2026-07-03 15:36 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. + +## Local Mac + +Local Memind connects to native `goosed`, not Colima/Docker `goosed`. + +| Item | Value | +|------|-------| +| Runtime type | Native launchd process | +| Service | `com.tkmind.local-goosed` | +| Binary | `/Users/john/Project/tkmind_go/target/release/goosed` | +| Command role | `goosed agent` | +| Port | `18006` only | +| Session store | PostgreSQL `goose_sessions_dev` | + +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. + +## Portal + +| Item | Value | +|------|-------| +| Host | `58.38.22.103` | +| Live directory | `/Users/john/Project/Memind` | +| Service | `cn.tkmind.memind-portal` | +| 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` | + +Portal must be released as a runtime artifact. Do not edit source directly on 103. + +## MindSpace Service + +| Item | Value | +|------|-------| +| Live directory | `/Users/john/MindSpace` | +| Service | `cn.tkmind.mindspace-service` | +| Start script | `/Users/john/MindSpace/scripts/run-mindspace-prod.sh` | +| Port | `8082` | +| Health check | `curl http://127.0.0.1:8082/health` | +| Contract check | `curl http://127.0.0.1:8082/mindspace/v1/contract` | +| Shared Memind root | `MEMIND_SHARED_ROOT=/Users/john/Project/Memind` | + +`/Users/john/MindSpace` is the production MindSpace service root. Do not treat `/Users/john/Project/Memind/MindSpace` as the canonical MindSpace service root after the split. + +## goosed + +103 goosed runs in Colima/Docker, not as native launchd processes. + +| Item | Value | +|------|-------| +| Docker context | `colima` | +| Compose directory | `/Users/john/Project/goosed-prod` | +| Compose env | `/Users/john/Project/goosed-prod/.env` | +| Image pattern | `tkmind/goosed:prod-${GOOSED_TAG}` | +| Session store | PostgreSQL `memind_sessions` through `GOOSE_SESSION_DB_URL` | + +Container layout: + +| Container | Host port | Container port | +|-----------|-----------|----------------| +| `goosed-prod-1` | `18006` | `18006` | +| `goosed-prod-2` | `18007` | `18006` | +| `goosed-prod-3` | `18008` | `18006` | +| `goosed-prod-4` | `18009` | `18006` | + +All four ports must return `ok` from `/status`. + +## PostgreSQL rule + +Both local native goosed and 103 Colima goosed persist sessions in PostgreSQL: + +- local native goosed: PostgreSQL database `goose_sessions_dev` +- 103 Colima goosed: PostgreSQL database `memind_sessions` + +Do not assume a local SQLite session store when debugging current Memind/goosed behavior. + +## Important path rule + +The canonical production MindSpace service root is: + +```text +/Users/john/MindSpace +``` + +The old monolith path: + +```text +/Users/john/Project/Memind/MindSpace +``` + +is legacy compatibility/storage context only. If Portal or goosed config still points business MindSpace service behavior at the old path, treat that as configuration drift to fix, not as the desired topology. + +In particular, check these before debugging missing folders/pages: + +```bash +ssh john@58.38.22.103 'grep -E "^(MINDSPACE|MINDSPACE_ROOT|MINDSPACE_SERVICE|MEMIND_SHARED_ROOT)=" /Users/john/Project/Memind/.env /Users/john/Project/goosed-prod/.env 2>/dev/null' +ssh john@58.38.22.103 'launchctl print gui/$(id -u)/cn.tkmind.mindspace-service | grep -E "program =|working directory|MINDSPACE|MEMIND_SHARED"' +ssh john@58.38.22.103 '/opt/homebrew/bin/docker inspect goosed-prod-1 --format "{{json .Mounts}}"' +``` + +## Current verification commands + +```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' +``` diff --git a/docs/agent-run-worker-rollout-runbook.md b/docs/agent-run-worker-rollout-runbook.md index ed5a201..9ccc657 100644 --- a/docs/agent-run-worker-rollout-runbook.md +++ b/docs/agent-run-worker-rollout-runbook.md @@ -7,7 +7,7 @@ This runbook controls the long-running Tool Gateway / external `agent-run-worker Production safety rules: - Do not delete PG data. -- Do not delete or rewrite `/Users/john/Project/Memind/MindSpace` user 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 default production state closed unless a rollout window is explicitly active. diff --git a/docs/architecture/memind-2-runtime-execution-assessment-20260702.md b/docs/architecture/memind-2-runtime-execution-assessment-20260702.md index 0278317..dff69db 100644 --- a/docs/architecture/memind-2-runtime-execution-assessment-20260702.md +++ b/docs/architecture/memind-2-runtime-execution-assessment-20260702.md @@ -1,5 +1,7 @@ # Memind 2.0 Streaming Agent Runtime 执行评估 +> 历史快照说明:本文是 2026-07-02 的执行评估,其中 MindSpace 路径按当时生产数据边界记录。103 当前生产 MindSpace Service 根目录以 [103 runtime topology](../103-runtime-topology.md) 为准,即 `/Users/john/MindSpace`;不要把本文里的旧 monolith 路径当作当前服务根目录。 + 日期: 2026-07-02 评估对象: diff --git a/docs/architecture/memind-2-streaming-agent-runtime-plan.md b/docs/architecture/memind-2-streaming-agent-runtime-plan.md index 404b179..3a1c941 100644 --- a/docs/architecture/memind-2-streaming-agent-runtime-plan.md +++ b/docs/architecture/memind-2-streaming-agent-runtime-plan.md @@ -1,5 +1,7 @@ # Memind 2.0 Streaming Agent Runtime 改造计划 +> 历史快照说明:本文记录 2026-07-02 streaming/runtime 改造过程,其中出现的 `/Users/john/Project/Memind/MindSpace` 是当时的存量数据保护/统计口径。103 当前生产 MindSpace Service 根目录以 [103 runtime topology](../103-runtime-topology.md) 为准,即 `/Users/john/MindSpace`;不要把本文历史路径当作当前服务根目录。 + 更新时间: 2026-07-02 执行状态: diff --git a/docs/g2-load-balancing.md b/docs/g2-load-balancing.md index c8ea505..1244de2 100644 --- a/docs/g2-load-balancing.md +++ b/docs/g2-load-balancing.md @@ -13,8 +13,8 @@ │ 105 / Studio 两个 portal ── 都连 ──→ Studio goosed 58.38.22.103:18006(主) └────→ Studio goosed 58.38.22.103:18007(备用/第二实例) - 发布页文件 ── 都读 ──→ Studio canonical /Users/john/Project/Memind/MindSpace - (105 经反向隧道 + rclone 挂载到本地同路径) + MindSpace 服务 ──→ Studio canonical /Users/john/MindSpace (:8082) + 旧发布页目录 ──兼容/存量──→ /Users/john/Project/Memind/MindSpace ``` - **为什么不用旧的 Cloudflare 直连**:当前入口已经迁到阿里云解析,外网流量先到 105,再由 105 转发到 103 / Studio。 diff --git a/docs/imgproxy-103-production-runbook.md b/docs/imgproxy-103-production-runbook.md index e3ccf5f..adfad00 100644 --- a/docs/imgproxy-103-production-runbook.md +++ b/docs/imgproxy-103-production-runbook.md @@ -1,6 +1,8 @@ # ImgProxy 103 生产部署说明 > 目标:把 `imgproxy` 部署到 `103 / Studio`,保持和当前本机开发一致的签名配置、端口约定、数据根语义与 URL 生成方式。 +> +> 2026-07-03 更新:103 当前 MindSpace Service 根目录是 `/Users/john/MindSpace`。本文下面的 `/Users/john/Project/Memind/MindSpace` 只表示旧发布页兼容/存量目录;不要把它理解为当前 MindSpace Service 根目录。当前生产拓扑见 [103 runtime topology](./103-runtime-topology.md)。 ## 先说结论 @@ -29,7 +31,7 @@ - `H5_USERS_ROOT=/Users/john/Project/Memind/users` - `MINDSPACE_STORAGE_ROOT=/Users/john/Project/Memind/data/mindspace` -- `MEMIND_SHARED_PUBLISH_ROOT=/Users/john/Project/Memind/MindSpace` +- `MEMIND_SHARED_PUBLISH_ROOT=/Users/john/Project/Memind/MindSpace`(旧发布页兼容/存量发布根,不是当前 MindSpace Service 根目录) 代码生成 `imgproxy` URL 时,传进去的是 `asset.storage_key`,例如: diff --git a/docs/mindspace-seamless-migration-runbook.md b/docs/mindspace-seamless-migration-runbook.md index 7d5229c..f39f688 100644 --- a/docs/mindspace-seamless-migration-runbook.md +++ b/docs/mindspace-seamless-migration-runbook.md @@ -4,12 +4,14 @@ This runbook defines the no-loss migration path for extracting MindSpace from the Memind monolith. -Current execution scope: +Current production status: -- Phase A only -- local standalone service on the same machine -- no 103 usage -- no 105 rollout yet +- 103 already runs standalone MindSpace Service from `/Users/john/MindSpace`. +- The service is `cn.tkmind.mindspace-service` on `127.0.0.1:8082`. +- Portal live root remains `/Users/john/Project/Memind`. +- `/Users/john/Project/Memind/MindSpace` is legacy compatibility/storage context only, not the current MindSpace Service root. + +Historical/local Phase A notes remain below for implementation context. For current 103 production truth, use [103 runtime topology](./103-runtime-topology.md). ## Invariants @@ -24,7 +26,7 @@ Run MindSpace as a standalone local service while Memind uses `MINDSPACE_SERVER_ Phase A is complete when: -- standalone MindSpace service uses `/Users/john/Project/MindSpace/runtime` by default +- standalone MindSpace service uses `/Users/john/MindSpace` as the production service root on 103 - Memind can use `MINDSPACE_SERVER_ADAPTER=remote` against the standalone service - `npm run verify:phase-a-local` passes for a real user with existing data - Memind remote-mode HTTP page list, page create, and page delete pass through the standalone service @@ -33,7 +35,8 @@ Phase A is complete when: ## Current data-root policy -- New default standalone data root: `/Users/john/Project/MindSpace/runtime` +- Production standalone service root on 103: `/Users/john/MindSpace` +- Old `/Users/john/Project/Memind/MindSpace` paths are migration source, rollback source, or legacy compatibility only - Old Memind directories are treated as migration source and rollback source - Data-root migration is executed by script, not by ad hoc manual copying @@ -49,7 +52,7 @@ Phase A is complete when: ### Standalone MindSpace service Path: -- `/Users/john/Project/MindSpace` +- `/Users/john/MindSpace` on 103 production Responsibilities: - bootstrap DB-backed local MindSpace services @@ -62,7 +65,7 @@ Responsibilities: ### Standalone data-root overrides -Use these when you intentionally want a runtime root other than the default `/Users/john/Project/MindSpace/runtime`: +Use these when you intentionally want a runtime root other than the production default `/Users/john/MindSpace`: - `MINDSPACE_SERVICE_H5_ROOT` - `MINDSPACE_STORAGE_ROOT` @@ -73,7 +76,7 @@ Use these when you intentionally want a runtime root other than the default `/Us ### 1. Start standalone MindSpace service ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace npm run migrate:data-root -- --yes MINDSPACE_MEMIND_ROOT=../Memind \ MINDSPACE_REMOTE_AUTH_TOKEN=local-dev-token \ @@ -85,11 +88,11 @@ node server.mjs ### 1b. Start standalone service against a new data root ```bash -cd /Users/john/Project/MindSpace -MINDSPACE_MEMIND_ROOT=../Memind \ -MINDSPACE_SERVICE_H5_ROOT=/Users/john/Project/MindSpace/runtime \ -MINDSPACE_STORAGE_ROOT=/Users/john/Project/MindSpace/runtime/data/mindspace \ -H5_USERS_ROOT=/Users/john/Project/MindSpace/runtime/users \ +cd /Users/john/MindSpace +MINDSPACE_MEMIND_ROOT=/Users/john/Project/Memind \ +MINDSPACE_SERVICE_H5_ROOT=/Users/john/MindSpace \ +MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace \ +H5_USERS_ROOT=/Users/john/MindSpace/users \ MINDSPACE_REMOTE_AUTH_TOKEN=local-dev-token \ MINDSPACE_SERVICE_PORT=19081 \ MINDSPACE_SERVICE_AGENT_WORKER_ENABLED=true \ @@ -113,7 +116,7 @@ Use the standard local startup flow already used in this repo. ### Phase A local gate ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ MINDSPACE_REMOTE_AUTH_TOKEN=local-dev-token \ @@ -141,7 +144,7 @@ Latest verified totals from the Phase A gate: ### Memind remote-mode HTTP gate ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ MEMIND_REMOTE_MODE_BASE_URL=http://127.0.0.1:18082 \ @@ -160,14 +163,14 @@ Expected result: ### Infra smoke ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace node scripts/loopback-smoke.mjs ``` ### User-scoped smoke ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_AUTH_TOKEN=local-dev-token \ node scripts/loopback-smoke.mjs @@ -181,7 +184,7 @@ Expected result: ### Local vs remote comparison ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ node scripts/compare-local-remote.mjs @@ -197,7 +200,7 @@ Expected result: ### Write-path parity ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ node scripts/compare-write-local-remote.mjs @@ -212,7 +215,7 @@ Expected result: ### Asset upload parity ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ node scripts/compare-asset-local-remote.mjs @@ -227,7 +230,7 @@ Expected result: ### Conversation package parity ```bash -cd /Users/john/Project/MindSpace +cd /Users/john/MindSpace MINDSPACE_LOOPBACK_USER_ID= \ MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:19081 \ node scripts/compare-conversation-package-local-remote.mjs @@ -315,7 +318,7 @@ Rollback: - stop standalone service - point Memind back to `local` or stop the standalone service -- if needed, start standalone with `MINDSPACE_SERVICE_H5_ROOT=/Users/john/Project/Memind` +- if needed for rollback/compatibility, start standalone with an explicit legacy root; do not assume `/Users/john/Project/Memind/MindSpace` is the current production service root - old Memind data root becomes active again immediately ## Deferred diff --git a/docs/mindspace-service-contract.md b/docs/mindspace-service-contract.md index 7620277..164ca97 100644 --- a/docs/mindspace-service-contract.md +++ b/docs/mindspace-service-contract.md @@ -4,6 +4,12 @@ 状态: Draft for P1/P2 +生产拓扑更新(2026-07-03): + +- 103 的 MindSpace Service 已独立部署在 `/Users/john/MindSpace`,服务为 `cn.tkmind.mindspace-service`,端口 `8082`。 +- Portal live 目录仍是 `/Users/john/Project/Memind`,但它不再是 MindSpace Service 的根目录。 +- `/Users/john/Project/Memind/MindSpace` 只能当旧链路兼容/存量目录处理;新排障和新配置必须优先看 [103 runtime topology](./103-runtime-topology.md)。 + 目标: - 定义 MindSpace 从 Memind Portal 单体拆出前必须稳定下来的 API、存储、URL、权限和 package 契约。 diff --git a/docs/release-deploy.md b/docs/release-deploy.md index 75a97be..b4e1c9a 100644 --- a/docs/release-deploy.md +++ b/docs/release-deploy.md @@ -42,7 +42,7 @@ bash scripts/release-portal-runtime-105.sh --yes 4. 本地生成 `memind-portal-runtime-.tar.gz` 和发布清单。 5. 通过 `scp` 上传到 103 的 `incoming/memind-portal-runtime/`。 6. 103 备份当前 `/Users/john/Project/Memind` 全目录 + 持久目录。 -7. 停止旧 Portal,解包 runtime artifact 到新的 live 目录,继承 `.env` / `MindSpace/` / `data/` / `users/` / `.tailscale/` / `public/plaza-covers/` / `logs/`。 +7. 停止旧 Portal,解包 runtime artifact 到新的 live 目录,继承 `.env` / `data/` / `users/` / `.tailscale/` / `public/plaza-covers/` / `logs/`。`/Users/john/MindSpace` 是独立 MindSpace Service 根目录,不随 Portal runtime 解包;`/Users/john/Project/Memind/MindSpace` 只作为旧链路兼容/存量目录看待。 8. 更新 LaunchAgent 指向 `${APP_DIR}/scripts/run-memind-portal-prod.sh`。 9. 启动 Portal,检查 `127.0.0.1:8081/api/status` 为 200。 10. 旧源码 live 目录移入 `archives/`,线上不再保留可运行源码树。 @@ -76,6 +76,7 @@ ExecStart=/opt/node/bin/node /root/tkmind_go/ui/h5/server.mjs ## 相关文档 +- [本机与 103 运行拓扑](103-runtime-topology.md) - [Portal 无源码迁移说明](no-source-portal-migration.md) - [105 服务器变更规范](105-server-operations.md) - [开发环境规则](../DEVELOPMENT_RELEASE_RULES.md) diff --git a/docs/service-isolation-runbook.md b/docs/service-isolation-runbook.md index 495830d..5614902 100644 --- a/docs/service-isolation-runbook.md +++ b/docs/service-isolation-runbook.md @@ -24,6 +24,7 @@ - 不在开发预览中使用生产 MindSpace service 的 `8082`。 - **禁止 SSH 到 `105` 直接改业务源码**;105 是入口/代理层,变更须本地 commit 后发布。见 [105 服务器变更规范](./105-server-operations.md)。 - 不在生产目录里跑会清理端口的开发脚本。 +- 103 当前生产拓扑以 [103-runtime-topology.md](./103-runtime-topology.md) 为准;不要用旧文档里的 `/Users/john/Project/Memind/MindSpace` 推断当前 MindSpace Service 根目录。 - 不执行 `scripts/install-prod-services.sh` 来做开发预览;它会释放生产端口。 - 不手动 kill `8081` 上的进程,除非目标就是恢复/重启生产,并且已经确认影响窗口。 From 9e4074474bc9bb7150d827bfa24a90aae7748992 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 3 Jul 2026 16:34:05 +0800 Subject: [PATCH 2/2] fix: harden session event proxy errors --- tkmind-proxy.mjs | 21 +++++++-- tkmind-proxy.test.mjs | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index 1fc6798..07caf80 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -390,6 +390,20 @@ function sendProxyResponse(res, upstream) { Readable.fromWeb(upstream.body).pipe(res); } +function writeSseProxyErrorAndEnd(res, message) { + if (res.writableEnded) return; + if (res.headersSent) { + try { + res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`); + } catch { + // The client may already be gone. Ending below is still harmless. + } + res.end(); + return; + } + res.status(502).json({ message }); +} + function extractSessionId(req, body) { const fromParams = req.params?.sessionId ?? req.params?.id; if (fromParams) return fromParams; @@ -1756,12 +1770,13 @@ export function createTkmindProxy({ if (!res.writableEnded) res.end(); return; } - res.status(502).json({ - message: sanitizeUserFacingProxyMessage( + writeSseProxyErrorAndEnd( + res, + sanitizeUserFacingProxyMessage( err instanceof Error ? err.message : 'SSE 代理失败', 'SSE 代理失败', ), - }); + ); } }; diff --git a/tkmind-proxy.test.mjs b/tkmind-proxy.test.mjs index c7ce91a..f0e6e16 100644 --- a/tkmind-proxy.test.mjs +++ b/tkmind-proxy.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; import fs from 'node:fs'; import { createServer } from 'node:http'; import os from 'node:os'; @@ -108,9 +109,39 @@ function createMemoryTestUserAuth(workingDir) { async getSessionTarget() { return { target: null, node: 0 }; }, + async billSessionUsage() { + return { ok: true, costCents: 0, balanceCents: 0 }; + }, }; } +function attachExpressResponseHelpers(res) { + res.status = (code) => { + res.statusCode = code; + return res; + }; + res.json = (payload) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.end(JSON.stringify(payload)); + return res; + }; + res.send = (payload = '') => { + res.end(payload); + return res; + }; + return res; +} + +async function listen(server) { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return server.address().port; +} + +async function closeServer(server) { + if (!server?.listening) return; + await new Promise((resolve) => server.close(resolve)); +} + test('sanitizePublicHtmlLinksInText downgrades missing own markdown public html links', () => { const owner = `test-user-${Date.now()}-markdown-missing`; const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public'); @@ -251,6 +282,78 @@ test('buildVisionPayload injects public standard image urls for page generation' assert.match(text, /不得改写图片里人物的年龄、性别、人数或主体关系/); }); +test('proxySessionEvents does not send JSON after SSE headers were sent', async () => { + let upstream; + + try { + upstream = createServer((req, res) => { + if (req.method === 'GET' && req.url === '/sessions/session-1/events') { + res.writeHead(200, { 'Content-Type': 'text/event-stream' }); + res.write('data: {"type":"message","content":"hello"}\n\n'); + res.end('data: {"type":"message","content":"second"}\n\n'); + return; + } + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ message: `unexpected ${req.method} ${req.url}` })); + }); + const upstreamPort = await listen(upstream); + const proxy = createTkmindProxy({ + apiTarget: `http://127.0.0.1:${upstreamPort}`, + apiSecret: 'test-secret', + userAuth: createMemoryTestUserAuth(process.cwd()), + }); + + const req = new EventEmitter(); + req.currentUser = { id: 'user-1', username: 'john' }; + req.get = () => ''; + req.once = req.once.bind(req); + req.off = req.off.bind(req); + + let writes = 0; + let jsonCalled = false; + const chunks = []; + const res = new EventEmitter(); + res.headersSent = false; + res.writableEnded = false; + res.statusCode = 200; + res.status = (code) => { + res.statusCode = code; + return res; + }; + res.setHeader = () => {}; + res.flushHeaders = () => { + res.headersSent = true; + }; + res.write = (chunk) => { + res.headersSent = true; + writes += 1; + if (writes > 1) throw new Error('simulated client write failure'); + chunks.push(String(chunk)); + return true; + }; + res.end = () => { + res.writableEnded = true; + }; + res.json = () => { + jsonCalled = true; + throw new Error('json must not be called after SSE started'); + }; + res.send = () => { + throw new Error('send must not be called after SSE started'); + }; + + await proxy.proxySessionEvents(req, res, 'session-1'); + + assert.equal(res.statusCode, 200); + assert.equal(res.headersSent, true); + assert.equal(res.writableEnded, true); + assert.equal(jsonCalled, false); + assert.match(chunks.join(''), /"content":"hello"/); + } finally { + await closeServer(upstream); + } +}); + test('startSessionForUser resolves memories through Memory V2 facade', async () => { let resolveInput = null; await withFakeGoosedSession(async ({ apiTarget, workingDir, harnessEntries }) => {