fix: update desktop server configuration for consistent port handling and access

This commit is contained in:
ciat-777
2026-03-15 22:02:03 +08:00
parent 4b42751953
commit 6ca08c1052
8 changed files with 45 additions and 33 deletions
+2 -2
View File
@@ -10,7 +10,7 @@
|------|----------|--------------|----------| |------|----------|--------------|----------|
| 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose / Zeabur | 固定服务地址,例如 `http://your-host:4000` 或反向代理域名 | 你挂载的 `DATA_DIR` / 持久化卷 | | 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose / Zeabur | 固定服务地址,例如 `http://your-host:4000` 或反向代理域名 | 你挂载的 `DATA_DIR` / 持久化卷 |
| 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | Render 分配的 `.onrender.com` 域名或自定义域名 | TiDB Serverless(外部 MySQL 数据库) | | 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | Render 分配的 `.onrender.com` 域名或自定义域名 | TiDB Serverless(外部 MySQL 数据库) |
| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;如需本机客户端直连,使用日志中打印的 `http://127.0.0.1:<port>` | 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` | | 二次开发 / 调试 | 本地开发 | 前端 `http://localhost:5173`,后端默认 `http://localhost:4000` | 仓库内 `./data` 或自定义 `DATA_DIR` |
> [!NOTE] > [!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 ### Nginx
+14 -12
View File
@@ -80,18 +80,18 @@ docker compose up -d
| 项目 | 说明 | | 项目 | 说明 |
|------|------| |------|------|
| 管理界面 | 应用启动后会直接打开桌面窗口,不需要假设固定的 `http://localhost:4000` | | 管理界面 | 应用启动后会直接打开桌面窗口,不需要假设固定的 `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')/data`,不是仓库里的 `./data` |
| 日志目录 | 保存在 `app.getPath('userData')/logs`;托盘菜单提供 `Open Logs Folder` | | 日志目录 | 保存在 `app.getPath('userData')/logs`;托盘菜单提供 `Open Logs Folder` |
> [!TIP] > [!TIP]
> - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。 > - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。
> - 如果你要把本机其他客户端接到桌面版内置后端,先到日志里查当前端口,不要写死 `4000`。 > - 如果没有额外覆盖端口,本机其他客户端可以直接连接 `http://127.0.0.1:4000`。
> [!WARNING] > [!WARNING]
> **端口冲突排障:** 如果桌面版启动后报端口被占用,可能是 `4310..4399` 范围内的端口全被其他应用占用了。 > **端口冲突排障:** 桌面版默认使用 `4000` 端口;如果该端口被其他应用占用:
> - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 固定到一个空闲端口 > - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 改到一个空闲端口
> - 或关闭占用这些端口的应用后重启 Metapi Desktop > - 或关闭占用 `4000` 的应用后重启 Metapi Desktop
> [!NOTE] > [!NOTE]
> 服务器部署统一推荐 Docker / Docker Compose,不再提供裸 Node.js 的 Release 压缩包。 > 服务器部署统一推荐 Docker / Docker Compose,不再提供裸 Node.js 的 Release 压缩包。
@@ -196,7 +196,7 @@ npm run dev
|----------|----------|----------------| |----------|----------|----------------|
| Docker / Docker Compose | `http://localhost:4000` | `http://localhost:4000` | | Docker / Docker Compose | `http://localhost:4000` | `http://localhost:4000` |
| 本地开发 | `http://localhost:5173` | `http://localhost:4000` | | 本地开发 | `http://localhost:5173` | `http://localhost:4000` |
| 桌面版 | 直接使用桌面窗口 | 通常是4312,先从日志里的 `Proxy API:` 行确认当前 `http://127.0.0.1:<port>` | | 桌面版 | 直接使用桌面窗口 | 默认 `http://127.0.0.1:4000`;如果设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问,局域网其他设备改用当前机器 IP + 同一端口 |
### Docker / 本地开发:直接用 curl 验证 ### Docker / 本地开发:直接用 curl 验证
@@ -212,23 +212,25 @@ curl -sS http://localhost:4000/v1/chat/completions \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
``` ```
### 桌面版:先确认当前端口再验证 ### 桌面版:默认直接用 4000 验证
打开托盘菜单的 `Open Logs Folder`,在最新日志里查找类似下面的启动信息: 打开托盘菜单的 `Open Logs Folder`,在最新日志里查找类似下面的启动信息:
```text ```text
Dashboard: http://127.0.0.1:4312 Dashboard: http://127.0.0.1:4000
Proxy API: http://127.0.0.1:4312/v1/chat/completions Proxy API: http://127.0.0.1:4000/v1/chat/completions
``` ```
然后把日志里的实际端口替换进 curl: 如果你没有覆盖端口,可直接执行:
```bash ```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" -H "Authorization: Bearer your-proxy-sk-token"
``` ```
返回正常响应,说明代理链路已经可用。 如果你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,再把上面的 `4000` 替换成日志里的实际端口。返回正常响应,说明代理链路已经可用。
如果你要从同一局域网的其他设备访问桌面版,把上面的 `127.0.0.1` 替换成这台电脑的实际局域网 IP,并确认系统防火墙已放行对应端口。
## 下一步 ## 下一步
+1 -1
View File
@@ -154,7 +154,7 @@ curl -sS http://localhost:4000/v1/chat/completions \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}' -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。
### 自动化监控建议 ### 自动化监控建议
+2 -8
View File
@@ -9,7 +9,6 @@ import {
} from 'electron'; } from 'electron';
import log from 'electron-log'; import log from 'electron-log';
import electronUpdater from 'electron-updater'; import electronUpdater from 'electron-updater';
import getPort, { portNumbers } from 'get-port';
import { spawn, type ChildProcess } from 'node:child_process'; import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs'; import { existsSync, mkdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -18,6 +17,7 @@ import {
createDesktopHealthUrl, createDesktopHealthUrl,
createDesktopServerUrl, createDesktopServerUrl,
isFatalServerExit, isFatalServerExit,
resolveDesktopServerPort,
resolveDesktopServerWorkingDir, resolveDesktopServerWorkingDir,
waitForServerReady, waitForServerReady,
} from './runtime.js'; } from './runtime.js';
@@ -68,12 +68,6 @@ function resolveExternalServerUrl() {
return (process.env.METAPI_DESKTOP_EXTERNAL_SERVER_URL || '').trim(); 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() { function showMainWindow() {
if (!mainWindow) return; if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore(); if (mainWindow.isMinimized()) mainWindow.restore();
@@ -185,7 +179,7 @@ async function waitForManagedServerReady(url: string) {
async function startManagedBackend() { async function startManagedBackend() {
ensureDesktopDirs(); ensureDesktopDirs();
const serverEntryPath = getServerEntryPath(); const serverEntryPath = getServerEntryPath();
const port = await resolveServerPort(); const port = resolveDesktopServerPort(process.env);
serverUrl = createDesktopServerUrl(port); serverUrl = createDesktopServerUrl(port);
const env = buildDesktopServerEnv({ const env = buildDesktopServerEnv({
+13 -2
View File
@@ -3,12 +3,13 @@ import {
buildDesktopServerEnv, buildDesktopServerEnv,
createDesktopServerUrl, createDesktopServerUrl,
isFatalServerExit, isFatalServerExit,
resolveDesktopServerPort,
resolveDesktopServerWorkingDir, resolveDesktopServerWorkingDir,
waitForServerReady, waitForServerReady,
} from './runtime.js'; } from './runtime.js';
describe('desktop runtime helpers', () => { 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({ const env = buildDesktopServerEnv({
inheritedEnv: { inheritedEnv: {
AUTH_TOKEN: 'admin-token', AUTH_TOKEN: 'admin-token',
@@ -19,7 +20,7 @@ describe('desktop runtime helpers', () => {
port: 4312, 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.PORT).toBe('4312');
expect(env.DATA_DIR).toBe('/tmp/metapi-data'); expect(env.DATA_DIR).toBe('/tmp/metapi-data');
expect(env.METAPI_LOG_DIR).toBe('/tmp/metapi-logs'); 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'); 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', () => { it('uses resources path as backend cwd for packaged desktop builds', () => {
expect(resolveDesktopServerWorkingDir({ expect(resolveDesktopServerWorkingDir({
appPath: 'C:/Users/test/AppData/Local/Programs/Metapi/resources/app.asar', appPath: 'C:/Users/test/AppData/Local/Programs/Metapi/resources/app.asar',
+10 -1
View File
@@ -23,6 +23,7 @@ type DesktopServerWorkingDirInput = {
isPackaged: boolean; isPackaged: boolean;
}; };
const DEFAULT_DESKTOP_SERVER_PORT = 4000;
const DEFAULT_READY_TIMEOUT_MS = 30_000; const DEFAULT_READY_TIMEOUT_MS = 30_000;
const DEFAULT_READY_INTERVAL_MS = 250; const DEFAULT_READY_INTERVAL_MS = 250;
@@ -31,9 +32,11 @@ function delay(ms: number) {
} }
export function buildDesktopServerEnv(input: DesktopServerEnvInput): NodeJS.ProcessEnv { export function buildDesktopServerEnv(input: DesktopServerEnvInput): NodeJS.ProcessEnv {
const host = (input.inheritedEnv?.HOST || '0.0.0.0').trim() || '0.0.0.0';
return { return {
...(input.inheritedEnv || {}), ...(input.inheritedEnv || {}),
HOST: '127.0.0.1', HOST: host,
PORT: String(input.port), PORT: String(input.port),
DATA_DIR: input.userDataDir, DATA_DIR: input.userDataDir,
METAPI_DESKTOP: '1', METAPI_DESKTOP: '1',
@@ -49,6 +52,12 @@ export function createDesktopHealthUrl(port: number): string {
return `${createDesktopServerUrl(port)}/api/desktop/health`; 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 { export function resolveDesktopServerWorkingDir(input: DesktopServerWorkingDirInput): string {
return input.isPackaged ? input.resourcesPath : input.appPath; return input.isPackaged ? input.resourcesPath : input.appPath;
} }
+2 -2
View File
@@ -11,7 +11,7 @@ describe('buildConfig', () => {
expect(config.dataDir).toBe('./data'); 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({ const config = buildConfig({
HOST: '0.0.0.0', HOST: '0.0.0.0',
METAPI_DESKTOP: '1', METAPI_DESKTOP: '1',
@@ -19,7 +19,7 @@ describe('buildConfig', () => {
DATA_DIR: '/tmp/metapi-data', 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.port).toBe(4312);
expect(config.dataDir).toBe('/tmp/metapi-data'); expect(config.dataDir).toBe('/tmp/metapi-data');
}); });
+1 -5
View File
@@ -32,11 +32,7 @@ function parseDbType(value: string | undefined): 'sqlite' | 'mysql' | 'postgres'
} }
function parseListenHost(env: NodeJS.ProcessEnv): string { function parseListenHost(env: NodeJS.ProcessEnv): string {
const requestedHost = (env.HOST || '0.0.0.0').trim() || '0.0.0.0'; return (env.HOST || '0.0.0.0').trim() || '0.0.0.0';
if (parseBoolean(env.METAPI_DESKTOP, false)) {
return '127.0.0.1';
}
return requestedHost;
} }
export function buildConfig(env: NodeJS.ProcessEnv) { export function buildConfig(env: NodeJS.ProcessEnv) {