From 6ca08c1052868f0656af641358a724b321cc6cc0 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Sun, 15 Mar 2026 22:02:03 +0800 Subject: [PATCH] fix: update desktop server configuration for consistent port handling and access --- docs/deployment.md | 4 ++-- docs/getting-started.md | 26 ++++++++++++++------------ docs/operations.md | 2 +- src/desktop/main.ts | 10 ++-------- src/desktop/runtime.test.ts | 15 +++++++++++++-- src/desktop/runtime.ts | 11 ++++++++++- src/server/config.test.ts | 4 ++-- src/server/config.ts | 6 +----- 8 files changed, 45 insertions(+), 33 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index e2418ff..eded39e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -10,7 +10,7 @@ |------|----------|--------------|----------| | 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose / Zeabur | 固定服务地址,例如 `http://your-host:4000` 或反向代理域名 | 你挂载的 `DATA_DIR` / 持久化卷 | | 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | Render 分配的 `.onrender.com` 域名或自定义域名 | TiDB Serverless(外部 MySQL 数据库) | -| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;如需本机客户端直连,使用日志中打印的 `http://127.0.0.1:` | Electron `app.getPath('userData')/data` | +| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;默认本机客户端直连使用 `http://127.0.0.1:4000`,局域网客户端可使用当前机器 IP + `4000`;如有覆盖则按 `METAPI_DESKTOP_SERVER_PORT` 的实际端口访问 | Electron `app.getPath('userData')/data` | | 二次开发 / 调试 | 本地开发 | 前端 `http://localhost:5173`,后端默认 `http://localhost:4000` | 仓库内 `./data` 或自定义 `DATA_DIR` | > [!NOTE] @@ -211,7 +211,7 @@ docker run -d --name metapi \ ## 反向代理 -以下反向代理配置面向 Docker / 服务器模式。桌面版内置后端默认只绑定本机 `127.0.0.1`,通常不作为公网服务直接暴露。 +以下反向代理配置面向 Docker / 服务器模式。桌面版内置后端默认监听 `0.0.0.0:4000`,但通常仍作为单机桌面应用使用;如果要给局域网或公网客户端访问,请自行配置防火墙、反向代理和认证边界。若你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,请把示例里的 `4000` 改成对应端口。 ### Nginx diff --git a/docs/getting-started.md b/docs/getting-started.md index 0347b11..6ba63f7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -80,18 +80,18 @@ docker compose up -d | 项目 | 说明 | |------|------| | 管理界面 | 应用启动后会直接打开桌面窗口,不需要假设固定的 `http://localhost:4000` | -| 本地后端地址 | 桌面版把内置服务绑定到 `127.0.0.1`,默认会在 `4310..4399` 中挑选空闲端口;只有显式设置 `METAPI_DESKTOP_SERVER_PORT` 时才会固定 | +| 本地后端地址 | 桌面版内置服务默认监听 `0.0.0.0:4000`;桌面窗口和本机 curl 可继续使用 `http://127.0.0.1:4000`,局域网其他设备请使用当前机器的实际 IP + `4000`;如需改端口,可显式设置 `METAPI_DESKTOP_SERVER_PORT` | | 数据目录 | 保存在 `app.getPath('userData')/data`,不是仓库里的 `./data` | | 日志目录 | 保存在 `app.getPath('userData')/logs`;托盘菜单提供 `Open Logs Folder` | > [!TIP] > - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。 -> - 如果你要把本机其他客户端接到桌面版内置后端,先到日志里查当前端口,不要写死 `4000`。 +> - 如果没有额外覆盖端口,本机其他客户端可以直接连接 `http://127.0.0.1:4000`。 > [!WARNING] -> **端口冲突排障:** 如果桌面版启动后报端口被占用,可能是 `4310..4399` 范围内的端口全被其他应用占用了。 -> - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 固定到一个空闲端口 -> - 或关闭占用这些端口的应用后重启 Metapi Desktop +> **端口冲突排障:** 桌面版默认使用 `4000` 端口;如果该端口被其他应用占用: +> - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 改到一个空闲端口 +> - 或关闭占用 `4000` 的应用后重启 Metapi Desktop > [!NOTE] > 服务器部署统一推荐 Docker / Docker Compose,不再提供裸 Node.js 的 Release 压缩包。 @@ -196,7 +196,7 @@ npm run dev |----------|----------|----------------| | Docker / Docker Compose | `http://localhost:4000` | `http://localhost:4000` | | 本地开发 | `http://localhost:5173` | `http://localhost:4000` | -| 桌面版 | 直接使用桌面窗口 | 通常是4312,先从日志里的 `Proxy API:` 行确认当前 `http://127.0.0.1:` | +| 桌面版 | 直接使用桌面窗口 | 默认 `http://127.0.0.1:4000`;如果设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问,局域网其他设备改用当前机器 IP + 同一端口 | ### Docker / 本地开发:直接用 curl 验证 @@ -212,23 +212,25 @@ curl -sS http://localhost:4000/v1/chat/completions \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' ``` -### 桌面版:先确认当前端口再验证 +### 桌面版:默认直接用 4000 验证 打开托盘菜单的 `Open Logs Folder`,在最新日志里查找类似下面的启动信息: ```text -Dashboard: http://127.0.0.1:4312 -Proxy API: http://127.0.0.1:4312/v1/chat/completions +Dashboard: http://127.0.0.1:4000 +Proxy API: http://127.0.0.1:4000/v1/chat/completions ``` -然后把日志里的实际端口替换进 curl: +如果你没有覆盖端口,可直接执行: ```bash -curl -sS http://127.0.0.1:4312/v1/models \ +curl -sS http://127.0.0.1:4000/v1/models \ -H "Authorization: Bearer your-proxy-sk-token" ``` -返回正常响应,说明代理链路已经可用。 +如果你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,再把上面的 `4000` 替换成日志里的实际端口。返回正常响应,说明代理链路已经可用。 + +如果你要从同一局域网的其他设备访问桌面版,把上面的 `127.0.0.1` 替换成这台电脑的实际局域网 IP,并确认系统防火墙已放行对应端口。 ## 下一步 diff --git a/docs/operations.md b/docs/operations.md index 420f477..a3cc0f1 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -154,7 +154,7 @@ curl -sS http://localhost:4000/v1/chat/completions \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}' ``` -以上示例默认服务端部署监听 `localhost:4000`。Desktop 内置后端运行在 `127.0.0.1` 的动态端口,优先通过应用界面、托盘日志或健康检查日志排查,不要假定固定端口。 +以上示例默认服务端部署监听 `localhost:4000`。Desktop 内置后端默认监听 `0.0.0.0:4000`;本机排查通常可直接使用 `127.0.0.1:4000`,如果显式设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问;局域网排查改用当前机器的实际 IP。 ### 自动化监控建议 diff --git a/src/desktop/main.ts b/src/desktop/main.ts index f6d7ffe..df21f4a 100644 --- a/src/desktop/main.ts +++ b/src/desktop/main.ts @@ -9,7 +9,6 @@ import { } from 'electron'; import log from 'electron-log'; import electronUpdater from 'electron-updater'; -import getPort, { portNumbers } from 'get-port'; import { spawn, type ChildProcess } from 'node:child_process'; import { existsSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -18,6 +17,7 @@ import { createDesktopHealthUrl, createDesktopServerUrl, isFatalServerExit, + resolveDesktopServerPort, resolveDesktopServerWorkingDir, waitForServerReady, } from './runtime.js'; @@ -68,12 +68,6 @@ function resolveExternalServerUrl() { return (process.env.METAPI_DESKTOP_EXTERNAL_SERVER_URL || '').trim(); } -async function resolveServerPort() { - const forcedPort = Number.parseInt(process.env.METAPI_DESKTOP_SERVER_PORT || '', 10); - if (Number.isFinite(forcedPort) && forcedPort > 0) return forcedPort; - return getPort({ port: portNumbers(4310, 4399) }); -} - function showMainWindow() { if (!mainWindow) return; if (mainWindow.isMinimized()) mainWindow.restore(); @@ -185,7 +179,7 @@ async function waitForManagedServerReady(url: string) { async function startManagedBackend() { ensureDesktopDirs(); const serverEntryPath = getServerEntryPath(); - const port = await resolveServerPort(); + const port = resolveDesktopServerPort(process.env); serverUrl = createDesktopServerUrl(port); const env = buildDesktopServerEnv({ diff --git a/src/desktop/runtime.test.ts b/src/desktop/runtime.test.ts index f2bf957..1dbea80 100644 --- a/src/desktop/runtime.test.ts +++ b/src/desktop/runtime.test.ts @@ -3,12 +3,13 @@ import { buildDesktopServerEnv, createDesktopServerUrl, isFatalServerExit, + resolveDesktopServerPort, resolveDesktopServerWorkingDir, waitForServerReady, } from './runtime.js'; describe('desktop runtime helpers', () => { - it('builds desktop server env with loopback host and app directories', () => { + it('builds desktop server env with external listen host and app directories', () => { const env = buildDesktopServerEnv({ inheritedEnv: { AUTH_TOKEN: 'admin-token', @@ -19,7 +20,7 @@ describe('desktop runtime helpers', () => { port: 4312, }); - expect(env.HOST).toBe('127.0.0.1'); + expect(env.HOST).toBe('0.0.0.0'); expect(env.PORT).toBe('4312'); expect(env.DATA_DIR).toBe('/tmp/metapi-data'); expect(env.METAPI_LOG_DIR).toBe('/tmp/metapi-logs'); @@ -31,6 +32,16 @@ describe('desktop runtime helpers', () => { expect(createDesktopServerUrl(4312)).toBe('http://127.0.0.1:4312'); }); + it('defaults desktop backend port to 4000', () => { + expect(resolveDesktopServerPort({})).toBe(4000); + }); + + it('honors explicit desktop backend port override', () => { + expect(resolveDesktopServerPort({ + METAPI_DESKTOP_SERVER_PORT: '4312', + })).toBe(4312); + }); + it('uses resources path as backend cwd for packaged desktop builds', () => { expect(resolveDesktopServerWorkingDir({ appPath: 'C:/Users/test/AppData/Local/Programs/Metapi/resources/app.asar', diff --git a/src/desktop/runtime.ts b/src/desktop/runtime.ts index 71d0d2a..1ae4022 100644 --- a/src/desktop/runtime.ts +++ b/src/desktop/runtime.ts @@ -23,6 +23,7 @@ type DesktopServerWorkingDirInput = { isPackaged: boolean; }; +const DEFAULT_DESKTOP_SERVER_PORT = 4000; const DEFAULT_READY_TIMEOUT_MS = 30_000; const DEFAULT_READY_INTERVAL_MS = 250; @@ -31,9 +32,11 @@ function delay(ms: number) { } export function buildDesktopServerEnv(input: DesktopServerEnvInput): NodeJS.ProcessEnv { + const host = (input.inheritedEnv?.HOST || '0.0.0.0').trim() || '0.0.0.0'; + return { ...(input.inheritedEnv || {}), - HOST: '127.0.0.1', + HOST: host, PORT: String(input.port), DATA_DIR: input.userDataDir, METAPI_DESKTOP: '1', @@ -49,6 +52,12 @@ export function createDesktopHealthUrl(port: number): string { return `${createDesktopServerUrl(port)}/api/desktop/health`; } +export function resolveDesktopServerPort(env?: NodeJS.ProcessEnv): number { + const forcedPort = Number.parseInt(env?.METAPI_DESKTOP_SERVER_PORT || '', 10); + if (Number.isFinite(forcedPort) && forcedPort > 0) return forcedPort; + return DEFAULT_DESKTOP_SERVER_PORT; +} + export function resolveDesktopServerWorkingDir(input: DesktopServerWorkingDirInput): string { return input.isPackaged ? input.resourcesPath : input.appPath; } diff --git a/src/server/config.test.ts b/src/server/config.test.ts index 3bc80f6..20740a6 100644 --- a/src/server/config.test.ts +++ b/src/server/config.test.ts @@ -11,7 +11,7 @@ describe('buildConfig', () => { expect(config.dataDir).toBe('./data'); }); - it('keeps desktop deployments bound to loopback', () => { + it('aligns desktop deployments with server deployments for listen host', () => { const config = buildConfig({ HOST: '0.0.0.0', METAPI_DESKTOP: '1', @@ -19,7 +19,7 @@ describe('buildConfig', () => { DATA_DIR: '/tmp/metapi-data', }); - expect(config.listenHost).toBe('127.0.0.1'); + expect(config.listenHost).toBe('0.0.0.0'); expect(config.port).toBe(4312); expect(config.dataDir).toBe('/tmp/metapi-data'); }); diff --git a/src/server/config.ts b/src/server/config.ts index 3a45843..7322a42 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -32,11 +32,7 @@ function parseDbType(value: string | undefined): 'sqlite' | 'mysql' | 'postgres' } function parseListenHost(env: NodeJS.ProcessEnv): string { - const requestedHost = (env.HOST || '0.0.0.0').trim() || '0.0.0.0'; - if (parseBoolean(env.METAPI_DESKTOP, false)) { - return '127.0.0.1'; - } - return requestedHost; + return (env.HOST || '0.0.0.0').trim() || '0.0.0.0'; } export function buildConfig(env: NodeJS.ProcessEnv) {