feat(desktop): 后端改为多服务可配置(8090-8099 各服务地址可指向本机或其它服务器)
This commit is contained in:
+4
-1
@@ -8,7 +8,10 @@
|
||||
- 静态托管 `renderer-dist`(即 `ui/dist` 的副本);
|
||||
- 把 `/api` 反代到**可配置的后端地址**。
|
||||
- 前端源码 `ui/` **零改动**、后端 **CORS 零改动**:相对路径 `/api` 与 SSE 流靠同源天然可用。
|
||||
- 后端地址配置(`config.js`)优先级:`PLANTAI_BFF_URL` 环境变量 > `userData/config.json` > 默认 `http://127.0.0.1:8090`。可在应用内菜单「设置 → 后端地址…」修改(保存后重载)。
|
||||
- 后端各服务地址配置(`config.js`,存 `userData/config.json` 的 `services` 对象;应用内菜单「设置 → 后端服务地址…」逐项修改,保存后重载):
|
||||
- `bff`(8090,总入口)、`qa`(8091)、`rag`(8092)、`legacy`(8093)、`vstore`(8094)、`paper`(8097)、`bioflow`(8098)——每项都可指向本机或其它服务器。
|
||||
- `PLANTAI_BFF_URL` 环境变量可覆盖 `bff`(脚本/CI 用)。
|
||||
- **关于多服务路由**:前端只调用 `/api/*`,`bff`(BFF)是唯一入口;反代把 `/api` 转到 `bff`,由 BFF 内部再路由到 qa/rag/vstore/paper/bioflow。因此要把某个服务挪到别的服务器,需改 **BFF 自己的配置**(`sdk/app/config.py` 的 `qa_base_url` / `rag_base_url` / `rag_legacy_base_url` / `vstore_base_url` / `paper_writer_url` / `bioflow_service_url`,或用 `.env` 覆盖);桌面端设置里其余服务的地址当前仅作记录/预留。
|
||||
|
||||
## 环境要求
|
||||
|
||||
|
||||
+41
-20
@@ -1,39 +1,60 @@
|
||||
// config.js —— 后端地址配置的读写(主进程)。
|
||||
// 优先级:环境变量 PLANTAI_BFF_URL > userData/config.json > 默认 http://127.0.0.1:8090。
|
||||
// config.js —— 后端各服务地址配置的读写(主进程)。
|
||||
// 设置文件 userData/config.json 形如 { services: { bff, qa, rag, legacy, vstore, paper, bioflow } }。
|
||||
// 每个地址都可指向本机或其它服务器。功能上桌面端反代只走 bff(BFF 是前端 /api/* 的唯一入口,
|
||||
// 由 BFF 内部再路由到 qa/rag/vstore/paper/bioflow——见 README)。
|
||||
// 环境变量 PLANTAI_BFF_URL 覆盖 bff(供脚本/CI 用)。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_BACKEND_URL = 'http://127.0.0.1:8090';
|
||||
const DEFAULT_SERVICES = {
|
||||
bff: 'http://127.0.0.1:8090', // BFF 总入口(前端 /api/* 的唯一入口)
|
||||
qa: 'http://127.0.0.1:8091', // QA 问答(qa_base_url)
|
||||
rag: 'http://127.0.0.1:8092', // 方法学 RAG(rag_base_url)
|
||||
legacy: 'http://127.0.0.1:8093', // 旧版 RAG(rag_legacy_base_url)
|
||||
vstore: 'http://127.0.0.1:8094', // 文档向量库(vstore_base_url)
|
||||
paper: 'http://127.0.0.1:8097', // 论文撰写(paper_writer_url)
|
||||
bioflow: 'http://127.0.0.1:8098', // 生信 bioflow(bioflow_service_url)
|
||||
};
|
||||
|
||||
const SERVICE_KEYS = Object.keys(DEFAULT_SERVICES);
|
||||
|
||||
function normalizeUrl(url) {
|
||||
return String(url || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function configPath(userDataDir) {
|
||||
return path.join(userDataDir, 'config.json');
|
||||
}
|
||||
|
||||
function normalizeBackendUrl(url) {
|
||||
return String(url || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function loadConfig(userDataDir) {
|
||||
const envUrl = normalizeBackendUrl(process.env.PLANTAI_BFF_URL);
|
||||
if (envUrl) return { backendUrl: envUrl, source: 'env' };
|
||||
function loadServices(userDataDir) {
|
||||
const services = { ...DEFAULT_SERVICES };
|
||||
// 设置文件覆盖默认
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath(userDataDir), 'utf8');
|
||||
const cfg = JSON.parse(raw);
|
||||
const fileUrl = normalizeBackendUrl(cfg && cfg.backendUrl);
|
||||
if (fileUrl) return { backendUrl: fileUrl, source: 'file' };
|
||||
const saved = (cfg && cfg.services) || {};
|
||||
for (const k of SERVICE_KEYS) {
|
||||
const v = normalizeUrl(saved[k]);
|
||||
if (v) services[k] = v;
|
||||
}
|
||||
} catch (_e) {
|
||||
// 无配置文件或解析失败 → 落到默认
|
||||
}
|
||||
return { backendUrl: DEFAULT_BACKEND_URL, source: 'default' };
|
||||
// 环境变量最高优先级,最后覆盖 bff
|
||||
const envBff = normalizeUrl(process.env.PLANTAI_BFF_URL);
|
||||
if (envBff) services.bff = envBff;
|
||||
return services;
|
||||
}
|
||||
|
||||
function saveConfig(userDataDir, backendUrl) {
|
||||
function saveServices(userDataDir, services) {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
configPath(userDataDir),
|
||||
JSON.stringify({ backendUrl: normalizeBackendUrl(backendUrl) }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
const out = {};
|
||||
for (const k of SERVICE_KEYS) {
|
||||
const v = normalizeUrl(services && services[k]);
|
||||
out[k] = v || DEFAULT_SERVICES[k];
|
||||
}
|
||||
fs.writeFileSync(configPath(userDataDir), JSON.stringify({ services: out }, null, 2), 'utf8');
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, saveConfig, DEFAULT_BACKEND_URL };
|
||||
module.exports = { loadServices, saveServices, DEFAULT_SERVICES, SERVICE_KEYS };
|
||||
|
||||
+19
-17
@@ -1,11 +1,13 @@
|
||||
// main.js —— Electron 主进程(瘦客户端壳)。
|
||||
// 启动流程:读后端地址配置 → 起本地静态 + /api 反代服务器(仅监听 127.0.0.1)→ 建窗口加载。
|
||||
// 启动流程:读后端各服务地址配置 → 起本地静态 + /api 反代服务器(仅监听 127.0.0.1)→ 建窗口加载。
|
||||
// 前端 ui/ 源码零改动:相对路径 /api 与 SSE 流靠「同源」天然可用,后端 CORS 也无需改。
|
||||
// 反代功能上只走 bff(BFF 是前端唯一入口);qa/rag/vstore/paper/bioflow 地址在设置里可改、持久化,
|
||||
// 实际的各服务再路由由 BFF 自己的 config/.env 完成(见 README)。
|
||||
const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { createServer } = require('./server');
|
||||
const { loadConfig, saveConfig } = require('./config');
|
||||
const { loadServices, saveServices } = require('./config');
|
||||
|
||||
let mainWindow = null;
|
||||
let settingsWindow = null;
|
||||
@@ -19,9 +21,9 @@ function staticDir() {
|
||||
: path.join(__dirname, 'renderer-dist');
|
||||
}
|
||||
|
||||
function startServer(backendUrl) {
|
||||
function startServer(bffUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srv = createServer({ staticDir: staticDir(), backendUrl });
|
||||
const srv = createServer({ staticDir: staticDir(), backendUrl: bffUrl });
|
||||
srv.once('error', reject);
|
||||
srv.listen(0, '127.0.0.1', () => {
|
||||
server = srv;
|
||||
@@ -37,9 +39,9 @@ function stopServer() {
|
||||
}
|
||||
}
|
||||
|
||||
async function restartServer(backendUrl) {
|
||||
async function restartServer(bffUrl) {
|
||||
stopServer();
|
||||
currentOrigin = await startServer(backendUrl);
|
||||
currentOrigin = await startServer(bffUrl);
|
||||
return currentOrigin;
|
||||
}
|
||||
|
||||
@@ -71,12 +73,12 @@ function openSettingsWindow() {
|
||||
return;
|
||||
}
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 460,
|
||||
height: 300,
|
||||
width: 500,
|
||||
height: 560,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
title: '后端地址设置',
|
||||
title: '后端服务设置',
|
||||
parent: mainWindow || undefined,
|
||||
modal: !!mainWindow,
|
||||
autoHideMenuBar: true,
|
||||
@@ -98,7 +100,7 @@ function buildMenu() {
|
||||
{
|
||||
label: '设置',
|
||||
submenu: [
|
||||
{ label: '后端地址…', click: openSettingsWindow },
|
||||
{ label: '后端服务地址…', click: openSettingsWindow },
|
||||
{ type: 'separator' },
|
||||
{ role: 'reload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
@@ -129,18 +131,18 @@ app.whenReady().then(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcMain.handle('desktop:get-backend-url', () => loadConfig(app.getPath('userData')).backendUrl);
|
||||
ipcMain.handle('desktop:set-backend-url', async (_event, url) => {
|
||||
ipcMain.handle('desktop:get-config', () => loadServices(app.getPath('userData')));
|
||||
ipcMain.handle('desktop:set-config', async (_event, services) => {
|
||||
// 用户显式保存后,文件配置优先于环境变量(否则 env 会一直盖住用户设置)
|
||||
delete process.env.PLANTAI_BFF_URL;
|
||||
saveConfig(app.getPath('userData'), url);
|
||||
await restartServer(loadConfig(app.getPath('userData')).backendUrl);
|
||||
const saved = saveServices(app.getPath('userData'), services);
|
||||
await restartServer(saved.bff);
|
||||
if (mainWindow) await mainWindow.loadURL(currentOrigin);
|
||||
return loadConfig(app.getPath('userData')).backendUrl;
|
||||
return saved;
|
||||
});
|
||||
|
||||
const { backendUrl } = loadConfig(app.getPath('userData'));
|
||||
currentOrigin = await startServer(backendUrl);
|
||||
const services = loadServices(app.getPath('userData'));
|
||||
currentOrigin = await startServer(services.bff);
|
||||
createMainWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
+2
-2
@@ -2,6 +2,6 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('desktopApi', {
|
||||
getBackendUrl: () => ipcRenderer.invoke('desktop:get-backend-url'),
|
||||
setBackendUrl: (url) => ipcRenderer.invoke('desktop:set-backend-url', url),
|
||||
getConfig: () => ipcRenderer.invoke('desktop:get-config'),
|
||||
setConfig: (services) => ipcRenderer.invoke('desktop:set-config', services),
|
||||
});
|
||||
|
||||
+37
-16
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>后端地址设置</title>
|
||||
<title>后端服务设置</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body {
|
||||
@@ -12,24 +12,27 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e6e8eb;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
h1 { font-size: 15px; margin: 0 0 8px; font-weight: 600; }
|
||||
p.desc { color: #9aa0a6; margin: 0 0 14px; font-size: 12px; line-height: 1.5; }
|
||||
h1 { font-size: 15px; margin: 0 0 4px; font-weight: 600; }
|
||||
p.desc { color: #9aa0a6; margin: 0 0 16px; font-size: 12px; line-height: 1.5; }
|
||||
.field { display: flex; align-items: center; margin-bottom: 10px; gap: 10px; }
|
||||
label { width: 120px; flex: none; color: #c6cad1; font-size: 12px; text-align: right; }
|
||||
input {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 10px;
|
||||
font-size: 13px;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
color: #e6e8eb;
|
||||
background: #1a1d23;
|
||||
border: 1px solid #2a2f38;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
input:focus { border-color: #3b82f6; }
|
||||
.actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
||||
.actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
||||
button {
|
||||
padding: 7px 16px;
|
||||
font-size: 13px;
|
||||
@@ -45,25 +48,43 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>后端地址</h1>
|
||||
<p class="desc">PlantAI 后端(BFF)地址,例如 http://127.0.0.1:8090。保存后立即重载页面。</p>
|
||||
<input id="url" type="text" placeholder="http://127.0.0.1:8090" spellcheck="false">
|
||||
<h1>后端服务地址</h1>
|
||||
<p class="desc">各服务地址可指向本机或其它服务器。前端统一经 BFF 总入口访问,由 BFF 内部再路由到其余服务。保存后立即重载。</p>
|
||||
|
||||
<div class="field"><label for="bff">BFF 总入口</label><input id="bff" spellcheck="false"></div>
|
||||
<div class="field"><label for="qa">QA 问答</label><input id="qa" spellcheck="false"></div>
|
||||
<div class="field"><label for="rag">方法学 RAG</label><input id="rag" spellcheck="false"></div>
|
||||
<div class="field"><label for="legacy">旧版 RAG</label><input id="legacy" spellcheck="false"></div>
|
||||
<div class="field"><label for="vstore">文档向量库</label><input id="vstore" spellcheck="false"></div>
|
||||
<div class="field"><label for="paper">论文撰写</label><input id="paper" spellcheck="false"></div>
|
||||
<div class="field"><label for="bioflow">生信 bioflow</label><input id="bioflow" spellcheck="false"></div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="cancel">取消</button>
|
||||
<button id="save" class="primary">保存</button>
|
||||
</div>
|
||||
<p id="hint" class="hint"></p>
|
||||
|
||||
<script>
|
||||
const input = document.getElementById('url');
|
||||
const KEYS = ['bff', 'qa', 'rag', 'legacy', 'vstore', 'paper', 'bioflow'];
|
||||
const hint = document.getElementById('hint');
|
||||
|
||||
window.desktopApi.getBackendUrl().then((url) => { input.value = url || ''; });
|
||||
window.desktopApi.getConfig().then((services) => {
|
||||
for (const k of KEYS) {
|
||||
const el = document.getElementById(k);
|
||||
if (el) el.value = (services && services[k]) || '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('save').addEventListener('click', async () => {
|
||||
const url = input.value.trim();
|
||||
if (!url) { hint.textContent = '地址不能为空'; return; }
|
||||
const services = {};
|
||||
for (const k of KEYS) {
|
||||
const el = document.getElementById(k);
|
||||
services[k] = (el && el.value || '').trim();
|
||||
}
|
||||
if (!services.bff) { hint.textContent = 'BFF 总入口不能为空'; return; }
|
||||
try {
|
||||
await window.desktopApi.setBackendUrl(url);
|
||||
await window.desktopApi.setConfig(services);
|
||||
window.close();
|
||||
} catch (e) {
|
||||
hint.textContent = '保存失败:' + (e && e.message ? e.message : e);
|
||||
|
||||
Reference in New Issue
Block a user