mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-26 10:41:11 +00:00
feat: detect stale local server and notify developer to restart (#1517)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Pre-commit / pre-commit (push) Has been cancelled
Test / Run Python Tests (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Pre-commit / pre-commit (push) Has been cancelled
Test / Run Python Tests (push) Has been cancelled
This commit is contained in:
parent
199b013044
commit
26cc5c4604
11 changed files with 358 additions and 33 deletions
|
|
@ -61,6 +61,11 @@ RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh
|
|||
RUN sed -i 's/\r$//' /app/celery/worker/start && chmod +x /app/celery/worker/start
|
||||
RUN sed -i 's/\r$//' /app/celery/beat/start && chmod +x /app/celery/beat/start
|
||||
|
||||
# Bake the latest server/ commit into the image for stale-server detection.
|
||||
# Uses --mount=type=bind to access .git without adding it to a layer.
|
||||
RUN --mount=type=bind,source=.git,target=/tmp/.git \
|
||||
echo "EIGENT_SERVER_GIT_COMMIT=$(git --git-dir=/tmp/.git log -1 --format=%H -- server/ 2>/dev/null || echo unknown)" > /app/.image_env
|
||||
|
||||
# Reset the entrypoint, don't invoke `uv`
|
||||
ENTRYPOINT []
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
- 配置中心 Config(保存各类工具/能力所需的密钥或参数)
|
||||
- `GET /configs`、`POST /configs`、`PUT /configs/{id}`、`DELETE /configs/{id}`、`GET /config/info`
|
||||
- 聊天与数据
|
||||
- 历史、快照、分享等接口位于 `app/controller/chat/`,数据全部落在本地数据库
|
||||
- 历史、快照、分享等接口位于 `app/domains/chat/api/`,数据全部落在本地数据库
|
||||
- MCP 服务管理(导入本地/远程 MCP 服务器)
|
||||
- `GET /mcps`、`POST /mcp/install`、`POST /mcp/import/{Local|Remote}` 等
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
- Config Center (store secrets/params required by tools/capabilities)
|
||||
- `GET /configs`, `POST /configs`, `PUT /configs/{id}`, `DELETE /configs/{id}`, `GET /config/info`
|
||||
- Chat & Data
|
||||
- History, snapshots, sharing, etc. in `app/controller/chat/`, all persisted to local DB
|
||||
- History, snapshots, sharing, etc. in `app/domains/chat/api/`, all persisted to local DB
|
||||
- MCP Management (import local/remote MCP servers)
|
||||
- `GET /mcps`, `POST /mcp/install`, `POST /mcp/import/{Local|Remote}`, etc.
|
||||
|
||||
|
|
|
|||
79
server/doc/server-refactor-v1.md
Normal file
79
server/doc/server-refactor-v1.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Server Refactor v1 - Upgrade Guide
|
||||
|
||||
> Applies to: v0.0.89+
|
||||
> PR: #1509
|
||||
|
||||
## What Changed
|
||||
|
||||
The server codebase has been restructured from a flat layout to a **domain-driven architecture**. No API endpoints or database schemas were changed — this is a code organization refactor only.
|
||||
|
||||
### Directory Mapping
|
||||
|
||||
| Before | After | Description |
|
||||
|---|---|---|
|
||||
| `app/component/` | `app/core/` | Infrastructure utilities (database, encryption, celery, etc.) |
|
||||
| `app/controller/` | `app/domains/*/api/` | API controllers, grouped by domain |
|
||||
| `app/service/` | `app/domains/*/service/` | Business logic, grouped by domain |
|
||||
| `app/exception/` | `app/shared/exception/` | Exception handling |
|
||||
| `app/type/` | `app/shared/types/` | Shared type definitions |
|
||||
| _(new)_ | `app/shared/auth/` | Authentication & authorization |
|
||||
| _(new)_ | `app/shared/middleware/` | CORS, rate limiting, trace ID |
|
||||
| _(new)_ | `app/shared/http/` | HTTP client utilities |
|
||||
| _(new)_ | `app/shared/logging/` | Logging & sensitive data filtering |
|
||||
|
||||
### Domain Structure
|
||||
|
||||
Each domain (`chat`, `config`, `mcp`, `model_provider`, `oauth`, `trigger`, `user`) follows the same layout:
|
||||
|
||||
```
|
||||
app/domains/<domain>/
|
||||
api/ # Controllers (route handlers)
|
||||
service/ # Business logic
|
||||
schema/ # Request/response schemas
|
||||
```
|
||||
|
||||
## Upgrade Action Required
|
||||
|
||||
**This is a breaking change for local deployments.** The old server code will fail to start due to changed import paths.
|
||||
|
||||
### Docker Users
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
You **must** include `--build` to rebuild the image. Running `docker-compose up -d` without `--build` will use the stale old image and fail.
|
||||
|
||||
### Non-Docker Users (Local Development)
|
||||
|
||||
If you are running the server directly (via `start_server.sh` or `uv run uvicorn`):
|
||||
|
||||
1. Stop the running server process
|
||||
2. Pull the latest code
|
||||
3. Restart the server
|
||||
|
||||
```bash
|
||||
# If using start_server.sh
|
||||
cd server
|
||||
./start_server.sh
|
||||
|
||||
# If running uvicorn directly
|
||||
cd server
|
||||
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
|
||||
```
|
||||
|
||||
### Electron App Users
|
||||
|
||||
If you are running Eigent as a desktop app, simply restart the application. The server will be restarted automatically.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Will I lose my data?**
|
||||
A: No. Database volumes and schemas are not affected. Only the Python source code layout changed.
|
||||
|
||||
**Q: Do I need to re-run database migrations?**
|
||||
A: No. There are no new migrations in this change.
|
||||
|
||||
**Q: I see import errors like `ModuleNotFoundError: No module named 'app.component'`**
|
||||
A: This means you are running an old server binary/image. Follow the upgrade steps above.
|
||||
79
server/doc/server-refactor-v1_CN.md
Normal file
79
server/doc/server-refactor-v1_CN.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Server 重构 v1 - 升级指南
|
||||
|
||||
> 适用版本: v0.0.89+
|
||||
> PR: #1509
|
||||
|
||||
## 改动概述
|
||||
|
||||
Server 代码从扁平结构重构为**领域驱动架构 (Domain-Driven)**。API 接口和数据库结构均未变更,这是一次纯代码组织层面的重构。
|
||||
|
||||
### 目录变更对照
|
||||
|
||||
| 重构前 | 重构后 | 说明 |
|
||||
|---|---|---|
|
||||
| `app/component/` | `app/core/` | 基础设施(数据库、加密、celery 等) |
|
||||
| `app/controller/` | `app/domains/*/api/` | 按领域分组的 API 控制器 |
|
||||
| `app/service/` | `app/domains/*/service/` | 按领域分组的业务逻辑 |
|
||||
| `app/exception/` | `app/shared/exception/` | 异常处理 |
|
||||
| `app/type/` | `app/shared/types/` | 共享类型定义 |
|
||||
| _(新增)_ | `app/shared/auth/` | 认证与授权 |
|
||||
| _(新增)_ | `app/shared/middleware/` | CORS、限流、Trace ID |
|
||||
| _(新增)_ | `app/shared/http/` | HTTP 客户端工具 |
|
||||
| _(新增)_ | `app/shared/logging/` | 日志与敏感信息过滤 |
|
||||
|
||||
### 领域结构
|
||||
|
||||
每个领域(`chat`、`config`、`mcp`、`model_provider`、`oauth`、`trigger`、`user`)遵循统一结构:
|
||||
|
||||
```
|
||||
app/domains/<领域>/
|
||||
api/ # 控制器(路由处理)
|
||||
service/ # 业务逻辑
|
||||
schema/ # 请求/响应模型
|
||||
```
|
||||
|
||||
## 升级操作(必须)
|
||||
|
||||
**此改动对本地部署是 breaking change。** 旧版 server 代码因 import 路径变更将无法启动。
|
||||
|
||||
### Docker 用户
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
**必须**加 `--build` 参数重新构建镜像。直接 `docker-compose up -d` 会使用旧镜像导致启动失败。
|
||||
|
||||
### 非 Docker 用户(本地开发)
|
||||
|
||||
如果你通过 `start_server.sh` 或 `uv run uvicorn` 直接运行 server:
|
||||
|
||||
1. 停止正在运行的 server 进程
|
||||
2. 拉取最新代码
|
||||
3. 重新启动 server
|
||||
|
||||
```bash
|
||||
# 使用 start_server.sh
|
||||
cd server
|
||||
./start_server.sh
|
||||
|
||||
# 直接运行 uvicorn
|
||||
cd server
|
||||
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
|
||||
```
|
||||
|
||||
### Electron 桌面应用用户
|
||||
|
||||
重启应用即可,server 会自动重启。
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 数据会丢失吗?**
|
||||
A: 不会。数据库卷和表结构未受影响,仅 Python 源码目录结构发生了变化。
|
||||
|
||||
**Q: 需要重新执行数据库迁移吗?**
|
||||
A: 不需要。此次改动没有新增数据库迁移。
|
||||
|
||||
**Q: 出现 `ModuleNotFoundError: No module named 'app.component'`**
|
||||
A: 说明正在运行旧版 server。请按上述升级步骤操作。
|
||||
|
|
@ -107,7 +107,7 @@ services:
|
|||
test:
|
||||
[
|
||||
'CMD-SHELL',
|
||||
'celery -A app.component.celery inspect ping -d celery@$$HOSTNAME',
|
||||
'celery -A app.core.celery inspect ping -d celery@$$HOSTNAME',
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ if str(_project_root) not in sys.path:
|
|||
sys.path.insert(0, str(_project_root))
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import subprocess
|
||||
from importlib.metadata import version as pkg_version
|
||||
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi_pagination import add_pagination
|
||||
|
|
@ -50,10 +51,51 @@ auto_include_routers(router, "", "app/domains")
|
|||
auto_include_routers(router, "", "app/api")
|
||||
api.include_router(router, prefix=f"{prefix}/v1")
|
||||
|
||||
# Server version — read once at import time so it reflects the running code
|
||||
try:
|
||||
SERVER_VERSION = pkg_version("Eigent")
|
||||
except Exception:
|
||||
SERVER_VERSION = "unknown"
|
||||
|
||||
# Git hash of the last commit that touched server/ — used for stale-server detection.
|
||||
# Captured once at startup; stays constant while the process lives.
|
||||
# 1) Try git directly (works in local dev)
|
||||
# 2) Fall back to .image_env baked by Dockerfile (works in Docker)
|
||||
def _read_server_code_hash() -> str:
|
||||
# Try git first (local dev)
|
||||
try:
|
||||
h = subprocess.check_output(
|
||||
["git", "log", "-1", "--format=%H", "--", "server/"],
|
||||
cwd=str(_project_root), text=True, stderr=subprocess.DEVNULL,
|
||||
).strip()
|
||||
if h:
|
||||
return h
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: read from Docker-baked .image_env
|
||||
try:
|
||||
env_file = pathlib.Path(__file__).parent / ".image_env"
|
||||
for line in env_file.read_text().splitlines():
|
||||
if line.startswith("EIGENT_SERVER_GIT_COMMIT="):
|
||||
v = line.split("=", 1)[1].strip()
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
SERVER_CODE_HASH = _read_server_code_hash()
|
||||
|
||||
|
||||
# Health check at root level for Docker healthcheck (GET /health)
|
||||
@api.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
return {"status": "ok", "service": "eigent-server"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "eigent-server",
|
||||
"version": SERVER_VERSION,
|
||||
"server_hash": SERVER_CODE_HASH,
|
||||
}
|
||||
|
||||
# Backward-compatible webhook route (/api/webhook/...)
|
||||
from app.domains.trigger.api.webhook_controller import router as webhook_router
|
||||
|
|
|
|||
27
server/uv.lock
generated
27
server/uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = "==3.12.*"
|
||||
resolution-markers = [
|
||||
"sys_platform == 'win32'",
|
||||
|
|
@ -479,6 +479,7 @@ dependencies = [
|
|||
{ name = "fastapi-pagination" },
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "loguru" },
|
||||
{ name = "openai" },
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pandas" },
|
||||
|
|
@ -523,6 +524,7 @@ requires-dist = [
|
|||
{ name = "fastapi-pagination", specifier = ">=0.12.34" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "itsdangerous", specifier = ">=2.2.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "openai", specifier = ">=1.99.3,<2" },
|
||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||
{ name = "pandas", specifier = ">=2.2.3" },
|
||||
|
|
@ -699,7 +701,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
|
||||
|
|
@ -847,6 +848,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
|
|
@ -1675,6 +1689,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.23.0"
|
||||
|
|
|
|||
|
|
@ -311,6 +311,80 @@ export async function checkBackendHealth(): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
// =============== Local Server Stale Detection ===============
|
||||
|
||||
/**
|
||||
* Git hash of the last commit that touched server/, injected by Vite at build
|
||||
* time. When the running server reports a different hash it means the server
|
||||
* process is stale and needs to be restarted / rebuilt.
|
||||
*/
|
||||
const EXPECTED_SERVER_HASH: string =
|
||||
import.meta.env.VITE_SERVER_CODE_HASH || '';
|
||||
|
||||
let serverStaleChecked = false;
|
||||
|
||||
/**
|
||||
* One-time check: when VITE_USE_LOCAL_PROXY is enabled, fetch the local
|
||||
* server's /health and compare its server_hash against the expected hash
|
||||
* baked into this build. Shows a persistent toast if they differ.
|
||||
*/
|
||||
export async function checkLocalServerStale(): Promise<void> {
|
||||
if (serverStaleChecked || !EXPECTED_SERVER_HASH) return;
|
||||
serverStaleChecked = true;
|
||||
|
||||
const useLocalProxy = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
|
||||
if (!useLocalProxy) return;
|
||||
|
||||
const serverUrl = import.meta.env.VITE_PROXY_URL || 'http://localhost:3001';
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const res = await fetch(`${serverUrl}/health`, {
|
||||
signal: controller.signal,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
let staleReason = '';
|
||||
|
||||
if (res.status === 404) {
|
||||
// /health endpoint doesn't exist — server predates v0.0.89
|
||||
staleReason = 'Server does not have /health endpoint (pre-v0.0.89)';
|
||||
} else if (res.ok) {
|
||||
const data = await res.json();
|
||||
const serverHash: string | undefined = data?.server_hash;
|
||||
|
||||
if (!serverHash) {
|
||||
staleReason = 'Server does not report version info (pre-v0.0.89)';
|
||||
} else if (
|
||||
serverHash !== 'unknown' &&
|
||||
serverHash !== EXPECTED_SERVER_HASH
|
||||
) {
|
||||
staleReason = `Server hash ${serverHash} != expected ${EXPECTED_SERVER_HASH}`;
|
||||
}
|
||||
} else {
|
||||
// Other HTTP errors — skip
|
||||
return;
|
||||
}
|
||||
|
||||
if (staleReason) {
|
||||
const { toast } = await import('sonner');
|
||||
toast.warning('Server code has been updated', {
|
||||
description:
|
||||
'Server is outdated. Please restart it or rebuild: docker-compose up --build -d',
|
||||
duration: Infinity,
|
||||
closeButton: true,
|
||||
});
|
||||
console.warn(`[Server Check] ${staleReason}. Please restart the server.`);
|
||||
}
|
||||
} catch {
|
||||
// server not reachable — skip silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple backend health check with retries
|
||||
* @param maxWaitMs - Maximum time to wait in milliseconds (default: 10000ms)
|
||||
|
|
@ -331,6 +405,10 @@ export async function waitForBackendReady(
|
|||
console.log(
|
||||
`[Backend Health Check] Backend is ready after ${Date.now() - startTime}ms`
|
||||
);
|
||||
|
||||
// Fire-and-forget: check local server version when using local proxy
|
||||
checkLocalServerStale();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { checkLocalServerStale } from '@/api/http';
|
||||
import ChatBox from '@/components/ChatBox';
|
||||
import Folder from '@/components/Folder';
|
||||
import UpdateElectron from '@/components/update';
|
||||
|
|
@ -180,6 +181,11 @@ export default function Home() {
|
|||
e.target.value = '';
|
||||
};
|
||||
|
||||
// One-time check: warn if local server is outdated after a git pull
|
||||
useEffect(() => {
|
||||
checkLocalServerStale();
|
||||
}, []);
|
||||
|
||||
// Detect files and triggers when project loads
|
||||
useEffect(() => {
|
||||
const detectAgentFiles = async () => {
|
||||
|
|
@ -393,7 +399,7 @@ export default function Home() {
|
|||
return (
|
||||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
<div className="inset-0 rounded-xl pointer-events-none absolute bg-transparent"></div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Workflow taskAssigning={[]} />
|
||||
</div>
|
||||
|
|
@ -407,7 +413,7 @@ export default function Home() {
|
|||
{activeTask.taskAssigning?.find(
|
||||
(agent) => agent.agent_id === activeWorkSpace
|
||||
)?.type === 'browser_agent' && (
|
||||
<div className="animate-in fade-in-0 slide-in-from-right-2 flex h-full w-full flex-1 duration-300">
|
||||
<div className="flex h-full w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<BrowserAgentWorkspace />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -415,7 +421,7 @@ export default function Home() {
|
|||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/*filter blur */}
|
||||
<div className="inset-0 rounded-xl pointer-events-none absolute bg-transparent"></div>
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Workflow taskAssigning={activeTask.taskAssigning || []} />
|
||||
</div>
|
||||
|
|
@ -434,7 +440,7 @@ export default function Home() {
|
|||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/*filter blur */}
|
||||
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
|
||||
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Folder />
|
||||
</div>
|
||||
|
|
@ -447,7 +453,7 @@ export default function Home() {
|
|||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/*filter blur */}
|
||||
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
|
||||
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Folder
|
||||
data={activeTask.taskAssigning?.find(
|
||||
|
|
@ -463,7 +469,7 @@ export default function Home() {
|
|||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/*filter blur */}
|
||||
<div className="blur-bg inset-0 rounded-xl bg-surface-secondary pointer-events-none absolute"></div>
|
||||
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-surface-secondary"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Folder />
|
||||
</div>
|
||||
|
|
@ -478,12 +484,12 @@ export default function Home() {
|
|||
// Render Tasks tab content (default)
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<div className="min-h-0 px-2 pb-2 pt-10 flex h-full flex-row overflow-hidden">
|
||||
<div className="min-h-0 min-w-0 gap-4 relative flex h-full flex-1 items-center justify-center overflow-hidden">
|
||||
<div className="flex h-full min-h-0 flex-row overflow-hidden px-2 pb-2 pt-10">
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 items-center justify-center gap-4 overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
key={`${isChatBoxVisible}-${chatPanelPosition}`}
|
||||
className="gap-0.5 w-full items-center justify-center"
|
||||
className="w-full items-center justify-center gap-0.5"
|
||||
>
|
||||
{/* ChatBox Panel - Left side */}
|
||||
{isChatBoxVisible && chatPanelPosition === 'left' && (
|
||||
|
|
@ -504,10 +510,10 @@ export default function Home() {
|
|||
<ResizablePanel className="h-full w-full min-w-[600px]">
|
||||
{chatStore.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId]?.activeWorkspace ? (
|
||||
<div className="rounded-2xl border-border-tertiary bg-surface-secondary flex h-full w-full flex-col border-solid">
|
||||
<div className="flex h-full w-full flex-col rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
|
||||
{/* Header with workspace tabs */}
|
||||
<div className="px-2 py-2 flex w-full items-center justify-between">
|
||||
<div className="gap-4 flex w-full flex-row items-center justify-start">
|
||||
<div className="flex w-full items-center justify-between px-2 py-2">
|
||||
<div className="flex w-full flex-row items-center justify-start gap-4">
|
||||
<MenuToggleGroup
|
||||
type="single"
|
||||
variant="info"
|
||||
|
|
@ -538,7 +544,7 @@ export default function Home() {
|
|||
icon={<Inbox />}
|
||||
showSubIcon={unviewedTabs.has('inbox')}
|
||||
subIcon={
|
||||
<span className="h-2 w-2 bg-red-500 rounded-full" />
|
||||
<span className="h-2 w-2 rounded-full bg-red-500" />
|
||||
}
|
||||
className="w-32"
|
||||
>
|
||||
|
|
@ -553,14 +559,14 @@ export default function Home() {
|
|||
}
|
||||
showSubIcon={unviewedTabs.has('triggers')}
|
||||
subIcon={
|
||||
<span className="h-2 w-2 bg-text-error rounded-full" />
|
||||
<span className="h-2 w-2 rounded-full bg-text-error" />
|
||||
}
|
||||
className="w-32"
|
||||
rightElement={
|
||||
wsConnectionStatus !== 'connected' && (
|
||||
<Popover>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<div className="h-6 w-6 rounded-md hover:bg-surface-tertiary flex cursor-pointer items-center justify-center transition-colors">
|
||||
<div className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-surface-tertiary">
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 ${wsConnectionStatus === 'connecting' ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
|
|
@ -571,7 +577,7 @@ export default function Home() {
|
|||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="gap-3 flex flex-col">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-body-sm text-text-body">
|
||||
Reconnect to trigger listener
|
||||
</p>
|
||||
|
|
@ -596,12 +602,12 @@ export default function Home() {
|
|||
</MenuToggleItem>
|
||||
</MenuToggleGroup>
|
||||
</div>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeWorkspaceTab !== 'inbox' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="w-24 rounded-lg items-center justify-center"
|
||||
className="w-24 items-center justify-center rounded-lg"
|
||||
onClick={() => {
|
||||
if (activeWorkspaceTab === 'workforce') {
|
||||
setAddWorkerDialogOpen(true);
|
||||
|
|
@ -664,10 +670,10 @@ export default function Home() {
|
|||
</div>
|
||||
) : (
|
||||
// Show default workspace when activeTaskId is null or task doesn't exist
|
||||
<div className="rounded-2xl border-border-tertiary bg-surface-secondary flex h-full w-full flex-col border-solid">
|
||||
<div className="flex h-full w-full flex-col rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
|
||||
{/* Header with workspace tabs */}
|
||||
<div className="px-2 py-2 flex w-full items-center justify-between">
|
||||
<div className="gap-4 flex w-full flex-row items-center justify-start">
|
||||
<div className="flex w-full items-center justify-between px-2 py-2">
|
||||
<div className="flex w-full flex-row items-center justify-start gap-4">
|
||||
<MenuToggleGroup
|
||||
type="single"
|
||||
variant="info"
|
||||
|
|
@ -698,7 +704,7 @@ export default function Home() {
|
|||
icon={<Inbox />}
|
||||
showSubIcon={unviewedTabs.has('inbox')}
|
||||
subIcon={
|
||||
<span className="h-2 w-2 bg-red-500 rounded-full" />
|
||||
<span className="h-2 w-2 rounded-full bg-red-500" />
|
||||
}
|
||||
className="w-32"
|
||||
>
|
||||
|
|
@ -713,14 +719,14 @@ export default function Home() {
|
|||
}
|
||||
showSubIcon={unviewedTabs.has('triggers')}
|
||||
subIcon={
|
||||
<span className="h-2 w-2 bg-red-500 rounded-full" />
|
||||
<span className="h-2 w-2 rounded-full bg-red-500" />
|
||||
}
|
||||
className="w-32"
|
||||
rightElement={
|
||||
wsConnectionStatus !== 'connected' && (
|
||||
<Popover>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<div className="h-6 w-6 rounded-md hover:bg-surface-tertiary flex cursor-pointer items-center justify-center transition-colors">
|
||||
<div className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-surface-tertiary">
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 ${wsConnectionStatus === 'connecting' ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
|
|
@ -731,7 +737,7 @@ export default function Home() {
|
|||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="gap-3 flex flex-col">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-text-body">
|
||||
Reconnect to trigger listener
|
||||
</p>
|
||||
|
|
@ -756,7 +762,7 @@ export default function Home() {
|
|||
</MenuToggleItem>
|
||||
</MenuToggleGroup>
|
||||
</div>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeWorkspaceTab !== 'inbox' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,25 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import electron from 'vite-plugin-electron/simple';
|
||||
import pkg from './package.json';
|
||||
|
||||
// Git hash of the last commit that touched server/ — used for stale-server detection.
|
||||
// Set as VITE_ env var so Vite exposes it to import.meta.env automatically.
|
||||
try {
|
||||
process.env.VITE_SERVER_CODE_HASH = execSync(
|
||||
'git log -1 --format=%H -- server/'
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
// git not available (CI, packaged build, etc.)
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
rmSync('dist-electron', { recursive: true, force: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue