From c88623855ff9f77d45c724413cd9a6408cd20672 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 27 Jul 2026 10:28:12 +0800 Subject: [PATCH] fix(dev): stop stale frontend when portal exits --- docs/local-dev.md | 13 +++++++ package.json | 2 +- scripts/dev-core.mjs | 13 ++++--- scripts/dev-process-lifecycle.mjs | 16 +++++++++ scripts/dev-process-lifecycle.test.mjs | 47 ++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 scripts/dev-process-lifecycle.mjs create mode 100644 scripts/dev-process-lifecycle.test.mjs diff --git a/docs/local-dev.md b/docs/local-dev.md index b712856..4026b2d 100644 --- a/docs/local-dev.md +++ b/docs/local-dev.md @@ -22,6 +22,19 @@ pnpm dev pnpm open:local-test # 浏览器打开 H5 ``` +### H5 多个接口同时返回 500 + +如果 `5173` 页面里的 `/auth/status`、`/auth/wechat/config`、`/api/analytics/context` +同时返回 `text/plain` 500,先检查 Portal: + +```bash +curl -i http://127.0.0.1:8081/auth/status +``` + +`8081` 无法连接表示 Vite 还在运行、但它代理的 Portal 已退出,并非这些路由同时故障。 +前台 `pnpm dev` 会在任一受管子进程意外退出时结束整个开发栈;重新运行 `pnpm dev` +即可干净启动。LaunchAgent 模式则查看 `~/Library/Logs/memind-dev.log` 并按下节命令重启。 + ### LaunchAgent 常驻(推荐本机联调) 终端里跑 `pnpm dev` 会在 Cursor / 终端关闭时被 SIGTERM 停掉。若希望登录后自动拉起、掉线自恢复,可安装 LaunchAgent(与 imgproxy / goosed 同类): diff --git a/package.json b/package.json index 38e9329..7dcf554 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "smoke:orchestrator-shadow": "node scripts/smoke-orchestrator-shadow.mjs", "mock:memory-v2-services": "node scripts/mock-memory-v2-services.mjs", "test:memind": "node scripts/run-memind-tests.mjs", - "pretest": "node scripts/verify-portal-access-policy.mjs && node --test episodic-memory.test.mjs tkmind-proxy.test.mjs server/portal-access-policy.test.mjs server/portal-account-feedback-routes.test.mjs server/portal-agent-job-routes.test.mjs server/portal-agent-runtime-routes.test.mjs server/portal-agent-services-bootstrap.test.mjs server/portal-auth-services-bootstrap.test.mjs server/portal-auth-session-helpers.test.mjs server/portal-billing-routes.test.mjs server/portal-core-auth-routes.test.mjs server/portal-domain-services-bootstrap.test.mjs server/portal-gateway-services-bootstrap.test.mjs server/portal-integration-services-bootstrap.test.mjs server/portal-memory-session-services-bootstrap.test.mjs server/portal-notification-routes.test.mjs server/portal-mindspace-agent-operation-routes.test.mjs server/portal-mindspace-asset-delivery-routes.test.mjs server/portal-mindspace-asset-routes.test.mjs server/portal-mindspace-chat-save-routes.test.mjs server/portal-mindspace-chat-share-routes.test.mjs server/portal-mindspace-page-core-routes.test.mjs server/portal-mindspace-page-publish-routes.test.mjs server/portal-mindspace-space-routes.test.mjs server/portal-publication-routes.test.mjs server/portal-publication-shell.test.mjs server/portal-published-page-delivery.test.mjs server/portal-session-coordinator.test.mjs server/portal-session-routes.test.mjs server/portal-plaza-routes.test.mjs server/portal-static-delivery-routes.test.mjs server/portal-user-memory-routes.test.mjs server/portal-wechat-auth-routes.test.mjs server/portal-webhook-routes.test.mjs server/portal-wiki-routes.test.mjs server/portal-workspace-publication-delivery.test.mjs", + "pretest": "node scripts/verify-portal-access-policy.mjs && node --test scripts/dev-process-lifecycle.test.mjs episodic-memory.test.mjs tkmind-proxy.test.mjs server/portal-access-policy.test.mjs server/portal-account-feedback-routes.test.mjs server/portal-agent-job-routes.test.mjs server/portal-agent-runtime-routes.test.mjs server/portal-agent-services-bootstrap.test.mjs server/portal-auth-services-bootstrap.test.mjs server/portal-auth-session-helpers.test.mjs server/portal-billing-routes.test.mjs server/portal-core-auth-routes.test.mjs server/portal-domain-services-bootstrap.test.mjs server/portal-gateway-services-bootstrap.test.mjs server/portal-integration-services-bootstrap.test.mjs server/portal-memory-session-services-bootstrap.test.mjs server/portal-notification-routes.test.mjs server/portal-mindspace-agent-operation-routes.test.mjs server/portal-mindspace-asset-delivery-routes.test.mjs server/portal-mindspace-asset-routes.test.mjs server/portal-mindspace-chat-save-routes.test.mjs server/portal-mindspace-chat-share-routes.test.mjs server/portal-mindspace-page-core-routes.test.mjs server/portal-mindspace-page-publish-routes.test.mjs server/portal-mindspace-space-routes.test.mjs server/portal-publication-routes.test.mjs server/portal-publication-shell.test.mjs server/portal-published-page-delivery.test.mjs server/portal-session-coordinator.test.mjs server/portal-session-routes.test.mjs server/portal-plaza-routes.test.mjs server/portal-static-delivery-routes.test.mjs server/portal-user-memory-routes.test.mjs server/portal-wechat-auth-routes.test.mjs server/portal-webhook-routes.test.mjs server/portal-wiki-routes.test.mjs server/portal-workspace-publication-delivery.test.mjs", "test:scenario": "node scripts/run-scenario-test.mjs", "verify:children-hobby-diet-survey": "node scripts/verify-children-hobby-diet-survey.mjs", "dev:page-data-aider-loop": "node scripts/page-data-aider-dev-loop.mjs", diff --git a/scripts/dev-core.mjs b/scripts/dev-core.mjs index 391457b..69bfbb2 100644 --- a/scripts/dev-core.mjs +++ b/scripts/dev-core.mjs @@ -12,6 +12,7 @@ import { deepseekDisableThinkingEnabled, resolveDeepseekNoThinkListenPort, } from '../deepseek-no-think-proxy.mjs'; +import { handleUnexpectedChildExit } from './dev-process-lifecycle.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const opsDir = path.join(root, 'ops'); @@ -52,11 +53,13 @@ function spawnChild(command, args, label, cwd = root, extraEnv = {}) { stdio: 'inherit', }); child.on('exit', (code, signal) => { - if (signal) return; - if (code && code !== 0) { - console.error(`[${label}] exited with code ${code}`); - shutdown(code ?? 1); - } + handleUnexpectedChildExit({ + label, + code, + signal, + stopping, + shutdown, + }); }); return child; } diff --git a/scripts/dev-process-lifecycle.mjs b/scripts/dev-process-lifecycle.mjs new file mode 100644 index 0000000..8d68ea3 --- /dev/null +++ b/scripts/dev-process-lifecycle.mjs @@ -0,0 +1,16 @@ +export function handleUnexpectedChildExit({ + label, + code, + signal, + stopping, + shutdown, + log = console.error, +}) { + if (stopping) return false; + + const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`; + const shutdownCode = Number.isInteger(code) && code > 0 ? code : 1; + log(`[${label}] exited unexpectedly (${detail}); shutting down dev stack`); + shutdown(shutdownCode); + return true; +} diff --git a/scripts/dev-process-lifecycle.test.mjs b/scripts/dev-process-lifecycle.test.mjs new file mode 100644 index 0000000..25cfbd6 --- /dev/null +++ b/scripts/dev-process-lifecycle.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { handleUnexpectedChildExit } from './dev-process-lifecycle.mjs'; + +function invokeExit({ code, signal, stopping = false }) { + const shutdownCodes = []; + const messages = []; + const handled = handleUnexpectedChildExit({ + label: 'portal', + code, + signal, + stopping, + shutdown: (shutdownCode) => shutdownCodes.push(shutdownCode), + log: (message) => messages.push(message), + }); + return { handled, shutdownCodes, messages }; +} + +test('clean child exit shuts down the remaining dev stack as a failure', () => { + const result = invokeExit({ code: 0, signal: null }); + + assert.equal(result.handled, true); + assert.deepEqual(result.shutdownCodes, [1]); + assert.match(result.messages[0], /code 0/); +}); + +test('signal exit shuts down the remaining dev stack as a failure', () => { + const result = invokeExit({ code: null, signal: 'SIGKILL' }); + + assert.equal(result.handled, true); + assert.deepEqual(result.shutdownCodes, [1]); + assert.match(result.messages[0], /signal SIGKILL/); +}); + +test('non-zero child exit preserves its failure code', () => { + const result = invokeExit({ code: 7, signal: null }); + + assert.deepEqual(result.shutdownCodes, [7]); +}); + +test('child exits during an intentional shutdown are ignored', () => { + const result = invokeExit({ code: 0, signal: null, stopping: true }); + + assert.equal(result.handled, false); + assert.deepEqual(result.shutdownCodes, []); + assert.deepEqual(result.messages, []); +});