mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 07:59:39 +00:00
Merge branch 'main' into enhance/add-login-close-btn
This commit is contained in:
commit
0ddf559ec2
5 changed files with 121 additions and 10 deletions
16
backend/app/controller/health_controller.py
Normal file
16
backend/app/controller/health_controller.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(tags=["Health"])
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
service: str
|
||||
|
||||
|
||||
@router.get("/health", name="health check", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint for verifying backend is ready to accept requests."""
|
||||
return HealthResponse(status="ok", service="eigent")
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ Centralized router registration for the Eigent API.
|
|||
All routers are explicitly registered here for better visibility and maintainability.
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from app.controller import chat_controller, model_controller, task_controller, tool_controller
|
||||
from app.controller import chat_controller, model_controller, task_controller, tool_controller, health_controller
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("router")
|
||||
|
|
@ -23,6 +23,11 @@ def register_routers(app: FastAPI, prefix: str = "") -> None:
|
|||
prefix: Optional global prefix for all routes (e.g., "/api")
|
||||
"""
|
||||
routers_config = [
|
||||
{
|
||||
"router": health_controller.router,
|
||||
"tags": ["Health"],
|
||||
"description": "Health check endpoint for service readiness"
|
||||
},
|
||||
{
|
||||
"router": chat_controller.router,
|
||||
"tags": ["chat"],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import log from 'electron-log'
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as net from "net";
|
||||
import * as http from "http";
|
||||
import { ipcMain, BrowserWindow, app } from 'electron'
|
||||
import { promisify } from 'util'
|
||||
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
|
||||
|
|
@ -195,21 +196,77 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
|
||||
|
||||
let started = false;
|
||||
let healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
const startTimeout = setTimeout(() => {
|
||||
if (!started) {
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend failed to start within timeout'));
|
||||
}
|
||||
}, 30000); // 30 second timeout
|
||||
|
||||
// Helper function to poll health endpoint
|
||||
const pollHealthEndpoint = (): void => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20; // 5 seconds total (20 * 250ms)
|
||||
const intervalMs = 250;
|
||||
|
||||
healthCheckInterval = setInterval(() => {
|
||||
attempts++;
|
||||
const healthUrl = `http://127.0.0.1:${port}/health`;
|
||||
|
||||
const req = http.get(healthUrl, { timeout: 1000 }, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
log.info(`Backend health check passed after ${attempts} attempts`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
resolve(node_process);
|
||||
} else {
|
||||
// Non-200 status (e.g., 404), continue polling unless max attempts reached
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check failed after ${attempts} attempts with status ${res.statusCode}`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error(`Backend health check failed: HTTP ${res.statusCode}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
// Connection error - backend might not be ready yet, continue polling
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check failed after ${attempts} attempts: unable to connect`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend health check failed: unable to connect'));
|
||||
}
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check timed out after ${attempts} attempts`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend health check timed out'));
|
||||
}
|
||||
});
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
node_process.stdout.on('data', (data) => {
|
||||
displayFilteredLogs(data);
|
||||
// check output content, judge if start success
|
||||
if (!started && data.toString().includes("Uvicorn running on")) {
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
resolve(node_process);
|
||||
log.info('Uvicorn startup detected, starting health check polling...');
|
||||
pollHealthEndpoint();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -217,9 +274,8 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
displayFilteredLogs(data);
|
||||
|
||||
if (!started && data.toString().includes("Uvicorn running on")) {
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
resolve(node_process);
|
||||
log.info('Uvicorn startup detected (stderr), starting health check polling...');
|
||||
pollHealthEndpoint();
|
||||
}
|
||||
|
||||
// Check for port binding errors
|
||||
|
|
@ -227,6 +283,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
data.toString().includes("bind() failed")) {
|
||||
started = true; // Prevent multiple rejections
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error(`Port ${port} is already in use`));
|
||||
}
|
||||
|
|
@ -234,6 +291,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
|
||||
node_process.on('close', (code) => {
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
if (!started) {
|
||||
reject(new Error(`fastapi exited with code ${code}`));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from fastapi_pagination import Page
|
|||
from fastapi_pagination.ext.sqlmodel import paginate
|
||||
from app.model.chat.chat_history import ChatHistoryOut, ChatHistoryIn, ChatHistory, ChatHistoryUpdate
|
||||
from fastapi_babel import _
|
||||
from sqlmodel import Session, select, desc
|
||||
from sqlmodel import Session, select, desc, case
|
||||
from app.component.auth import Auth, auth_must
|
||||
from app.component.database import session
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
|
@ -38,7 +38,19 @@ def create_chat_history(data: ChatHistoryIn, session: Session = Depends(session)
|
|||
def list_chat_history(session: Session = Depends(session), auth: Auth = Depends(auth_must)) -> Page[ChatHistoryOut]:
|
||||
"""List chat histories for current user."""
|
||||
user_id = auth.user.id
|
||||
stmt = select(ChatHistory).where(ChatHistory.user_id == user_id).order_by(desc(ChatHistory.created_at))
|
||||
|
||||
# Order by created_at descending, but fallback to id descending for old records without timestamps
|
||||
# This ensures newer records with timestamps come first, followed by old records ordered by id
|
||||
stmt = (
|
||||
select(ChatHistory)
|
||||
.where(ChatHistory.user_id == user_id)
|
||||
.order_by(
|
||||
desc(case((ChatHistory.created_at.is_(None), 0), else_=1)), # Non-null created_at first
|
||||
desc(ChatHistory.created_at), # Then by created_at descending
|
||||
desc(ChatHistory.id) # Finally by id descending for records with same/null created_at
|
||||
)
|
||||
)
|
||||
|
||||
result = paginate(session, stmt)
|
||||
total = result.total if hasattr(result, 'total') else 0
|
||||
logger.debug("Chat histories listed", extra={"user_id": user_id, "total": total})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from sqlalchemy import Float, Integer
|
|||
from sqlmodel import Field, SmallInteger, Column, JSON, String
|
||||
from typing import Optional
|
||||
from enum import IntEnum
|
||||
from datetime import datetime
|
||||
from sqlalchemy_utils import ChoiceType
|
||||
from app.model.abstract.model import AbstractModel, DefaultTimes
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
|
@ -13,6 +14,16 @@ class ChatStatus(IntEnum):
|
|||
|
||||
|
||||
class ChatHistory(AbstractModel, DefaultTimes, table=True):
|
||||
"""
|
||||
Chat history model with timestamp tracking.
|
||||
|
||||
Inherits from DefaultTimes which provides:
|
||||
- created_at: timestamp when record is created (auto-populated)
|
||||
- updated_at: timestamp when record is last modified (auto-updated)
|
||||
- deleted_at: timestamp for soft deletion (nullable)
|
||||
|
||||
For legacy records without timestamps, sorting falls back to id ordering.
|
||||
"""
|
||||
id: int = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(index=True)
|
||||
task_id: str = Field(index=True, unique=True)
|
||||
|
|
@ -70,13 +81,22 @@ class ChatHistoryOut(BaseModel):
|
|||
summary: str | None = None
|
||||
tokens: int
|
||||
status: int
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def fill_project_id_from_task_id(self):
|
||||
"""fill by task_id when project_id is None"""
|
||||
"""Fill project_id from task_id when project_id is None"""
|
||||
if self.project_id is None:
|
||||
self.project_id = self.task_id
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def handle_legacy_timestamps(self):
|
||||
"""Handle legacy records that might not have timestamp fields"""
|
||||
# For old records without timestamps, we rely on database-level defaults
|
||||
# The sorting in the controller will handle ordering appropriately
|
||||
return self
|
||||
|
||||
|
||||
class ChatHistoryUpdate(BaseModel):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue