forked from boos/BoosAPI
fix: update desktop server configuration for consistent port handling and access
This commit is contained in:
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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({
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user