Merge branch 'main' into add_lark

This commit is contained in:
Wendong-Fan 2026-01-13 19:47:48 +00:00 committed by GitHub
commit ceb556f111
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1096 additions and 729 deletions

1
.npmrc
View file

@ -3,4 +3,5 @@
shamefully-hoist=true
# For China 🇨🇳 developers
# registry=https://registry.npmmirror.com
# electron_mirror=https://npmmirror.com/mirrors/electron/

View file

@ -44,6 +44,8 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr
<br/>
[![][image-join-us]][join-us]
<details>
<summary><kbd>Table of contents</kbd></summary>

View file

@ -1 +1 @@
3.10.16
3.10.15

File diff suppressed because it is too large Load diff

View file

@ -112,6 +112,7 @@
"autoprefixer": "^10.4.20",
"electron": "^33.2.0",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^4.0.0",
"i18next": "^25.4.2",
"jsdom": "^26.1.0",
"postcss": "^8.4.49",
@ -133,4 +134,4 @@
"engines": {
"node": ">=18.0.0 <23.0.0"
}
}
}

View file

@ -1,8 +1,8 @@
debug=false
url_prefix=/api
secret_key=postgres
database_url=postgresql://postgres:postgres@localhost:5432/postgres
# Chat Share Secret Key
database_url=postgresql://postgres:123456@localhost:5432/postgres
# Chat Share Secret Key
CHAT_SHARE_SECRET_KEY=put-your-secret-key-here
CHAT_SHARE_SALT=put-your-encode-salt-here

View file

@ -21,10 +21,15 @@
---
### 快速开始Docker 推荐)
前置要求:已安装 Docker Desktop。
#### 前置要求
- **Docker Desktop**:已安装并运行
- **Python**3.10.*(推荐使用 3.10.15
- **Node.js**>=18.0.0 <23.0.0
#### 启动步骤
1) 启动服务
-
```bash
cd server
# 复制 .env.example 为 .env(或者按照.env.example的格式创建.env)
@ -83,8 +88,11 @@ docker logs -f eigent_postgres | cat
# 1) 停止容器中的 API 服务,仅保留数据库
docker stop eigent_api
# 2) 本地启动(需提供数据库连接串
# 2) 初始化数据库(首次或数据库结构变更时
cd server
uv run alembic upgrade head
# 3) 本地启动(需提供数据库连接串)
# 方式 A在当前 shell 导出环境变量
export database_url=postgresql://postgres:123456@localhost:5432/eigent
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
@ -107,4 +115,4 @@ uv run pybabel init -i messages.pot -d lang -l zh_CN
uv run pybabel compile -d lang -l zh_CN
```
如需完全离线环境,请仅使用本地模型与本地 MCP 服务器,并避免配置任何外部 Provider 与远程 MCP 地址。
如需完全离线环境,请仅使用本地模型与本地 MCP 服务器,并避免配置任何外部 Provider 与远程 MCP 地址。

View file

@ -21,7 +21,13 @@ Note: All the above data is stored in the local PostgreSQL volume in Docker (see
---
### Quick Start (Docker)
Prerequisite: Docker Desktop installed.
#### Prerequisites
- **Docker Desktop**: Installed and running
- **Python**: 3.10.* (3.10.15 recommended)
- **Node.js**: >=18.0.0 <23.0.0
#### Setup Steps
1) Start services
```bash
@ -81,8 +87,11 @@ You can run the API locally with hot-reload while keeping the database in Docker
# Stop API in container, keep DB
docker stop eigent_api
# Run locally (provide DB connection string)
# Initialize database (first-time or when DB schema changes)
cd server
uv run alembic upgrade head
# Run locally (provide DB connection string)
export database_url=postgresql://postgres:123456@localhost:5432/eigent
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
```

View file

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Form
from fastapi_babel import _
from sqlmodel import Session
from app.component import code
@ -7,7 +7,13 @@ from app.component.database import session
from app.component.encrypt import password_verify
from app.component.stack_auth import StackAuth
from app.exception.exception import UserException
from app.model.user.user import LoginByPasswordIn, LoginResponse, Status, User, RegisterIn
from app.model.user.user import (
LoginByPasswordIn,
LoginResponse,
Status,
User,
RegisterIn,
)
from app.component.environment import env
from utils import traceroot_wrapper as traceroot
@ -19,25 +25,62 @@ router = APIRouter(tags=["Login/Registration"])
@router.post("/login", name="login by email or password")
@traceroot.trace()
async def by_password(data: LoginByPasswordIn, session: Session = Depends(session)) -> LoginResponse:
async def by_password(
data: LoginByPasswordIn, session: Session = Depends(session)
) -> LoginResponse:
"""
User login with email and password
"""
email = data.email
user = User.by(User.email == email, s=session).one_or_none()
if not user:
logger.warning("Login failed: user not found", extra={"email": email})
raise UserException(code.password, _("Account or password error"))
if not password_verify(data.password, user.password):
logger.warning("Login failed: invalid password", extra={"user_id": user.id, "email": email})
logger.warning(
"Login failed: invalid password", extra={"user_id": user.id, "email": email}
)
raise UserException(code.password, _("Account or password error"))
logger.info("User login successful", extra={"user_id": user.id, "email": email})
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
@router.post("/dev_login", name="OAuth2 password flow login (for Swagger UI)")
@traceroot.trace()
async def dev_login(
username: str = Form(...), # OAuth2 uses 'username' but we accept email
password: str = Form(...),
session: Session = Depends(session),
) -> dict:
"""
OAuth2 password flow compatible login endpoint for Swagger UI.
This endpoint accepts form data (username/password) and returns an access token.
"""
# Use username as email (OAuth2 standard uses 'username' field)
email = username
user = User.by(User.email == email, s=session).one_or_none()
if not user:
logger.warning("OAuth2 login failed: user not found", extra={"email": email})
raise HTTPException(status_code=401, detail="Incorrect username or password")
if not password_verify(password, user.password):
logger.warning(
"OAuth2 login failed: invalid password",
extra={"user_id": user.id, "email": email},
)
raise HTTPException(status_code=401, detail="Incorrect username or password")
token = Auth.create_access_token(user.id)
logger.info("OAuth2 login successful", extra={"user_id": user.id, "email": email})
# Return OAuth2 compatible response
return {"access_token": token, "token_type": "bearer"}
@router.post("/login-by_stack", name="login by stack")
@traceroot.trace()
async def by_stack_auth(
@ -50,16 +93,21 @@ async def by_stack_auth(
stack_id = await StackAuth.user_id(token)
info = await StackAuth.user_info(token)
except Exception as e:
logger.error("Stack auth failed", extra={"type": type, "error": str(e)}, exc_info=True)
logger.error(
"Stack auth failed", extra={"type": type, "error": str(e)}, exc_info=True
)
raise HTTPException(500, detail=_("Authentication failed"))
user = User.by(User.stack_id == stack_id, s=session).one_or_none()
if not user:
if type != "signup":
logger.warning("Stack auth signup blocked: user not found", extra={"stack_id": stack_id, "type": type})
logger.warning(
"Stack auth signup blocked: user not found",
extra={"stack_id": stack_id, "type": type},
)
raise UserException(code.error, _("User not found"))
with session as s:
try:
user = User(
@ -72,18 +120,37 @@ async def by_stack_auth(
s.add(user)
s.commit()
s.refresh(user)
logger.info("New user registered via stack", extra={"user_id": user.id, "email": user.email, "stack_id": stack_id})
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
logger.info(
"New user registered via stack",
extra={
"user_id": user.id,
"email": user.email,
"stack_id": stack_id,
},
)
return LoginResponse(
token=Auth.create_access_token(user.id), email=user.email
)
except Exception as e:
s.rollback()
logger.error("Stack auth registration failed", extra={"stack_id": stack_id, "error": str(e)}, exc_info=True)
logger.error(
"Stack auth registration failed",
extra={"stack_id": stack_id, "error": str(e)},
exc_info=True,
)
raise UserException(code.error, _("Failed to register"))
else:
if user.status == Status.Block:
logger.warning("Blocked user login attempt", extra={"user_id": user.id, "stack_id": stack_id})
logger.warning(
"Blocked user login attempt",
extra={"user_id": user.id, "stack_id": stack_id},
)
raise UserException(code.error, _("Your account has been blocked."))
logger.info("User login via stack successful", extra={"user_id": user.id, "email": user.email, "stack_id": stack_id})
logger.info(
"User login via stack successful",
extra={"user_id": user.id, "email": user.email, "stack_id": stack_id},
)
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
@ -91,9 +158,11 @@ async def by_stack_auth(
@traceroot.trace()
async def register(data: RegisterIn, session: Session = Depends(session)):
email = data.email
if User.by(User.email == email, s=session).one_or_none():
logger.warning("Registration failed: email already exists", extra={"email": email})
logger.warning(
"Registration failed: email already exists", extra={"email": email}
)
raise UserException(code.error, _("Email already registered"))
with session as s:
@ -105,10 +174,17 @@ async def register(data: RegisterIn, session: Session = Depends(session)):
s.add(user)
s.commit()
s.refresh(user)
logger.info("User registered successfully", extra={"user_id": user.id, "email": email})
logger.info(
"User registered successfully",
extra={"user_id": user.id, "email": email},
)
except Exception as e:
s.rollback()
logger.error("User registration failed", extra={"email": email, "error": str(e)}, exc_info=True)
logger.error(
"User registration failed",
extra={"email": email, "error": str(e)},
exc_info=True,
)
raise UserException(code.error, _("Failed to register"))
return {"status": "success"}
return {"status": "success"}

View file

@ -0,0 +1,30 @@
services:
# PostgreSQL Database Only
postgres:
image: postgres:15
container_name: eigent_postgres
restart: unless-stopped
environment:
POSTGRES_DB: eigent
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- eigent_network
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres -d eigent" ]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
driver: local
networks:
eigent_network:
driver: bridge

View file

@ -12,22 +12,33 @@ from app import api
from app.component.environment import auto_include_routers, env
from fastapi.staticfiles import StaticFiles
# Import middleware to register BabelMiddleware
import app.middleware # noqa: F401
# Import exception handlers to register them
import app.exception.handler # noqa: F401
# Only initialize traceroot if enabled
if traceroot.is_enabled():
from traceroot.integrations.fastapi import connect_fastapi
connect_fastapi(api)
logger = traceroot.get_logger("server_main")
prefix = env("url_prefix", "")
auto_include_routers(api, prefix, "app/controller")
public_dir = os.environ.get("PUBLIC_DIR") or os.path.join(os.path.dirname(__file__), "app", "public")
public_dir = os.environ.get("PUBLIC_DIR") or os.path.join(
os.path.dirname(__file__), "app", "public"
)
if not os.path.isdir(public_dir):
try:
os.makedirs(public_dir, exist_ok=True)
logger.warning(f"Public directory did not exist. Created: {public_dir}")
except Exception as e:
logger.error(f"Public directory missing and could not be created: {public_dir}. Error: {e}")
logger.error(
f"Public directory missing and could not be created: {public_dir}. Error: {e}"
)
public_dir = None
if public_dir and os.path.isdir(public_dir):

View file

@ -56,7 +56,7 @@ export const INIT_PROVODERS: Provider[] = [
model_type: ""
},
{
id: 'bedrock',
id: 'aws-bedrock',
name: 'AWS Bedrock',
apiKey: '',
apiHost: '',

View file

@ -1,432 +1,466 @@
import { useAuthStore } from "@/store/authStore";
import { useNavigate, useLocation } from "react-router-dom";
import { useCallback, useEffect, useState, useRef } from "react";
import { useStackApp } from "@stackframe/react";
import loginGif from "@/assets/login.gif";
import { Button } from "@/components/ui/button";
import { useAuthStore } from '@/store/authStore';
import { useNavigate, useLocation } from 'react-router-dom';
import { useCallback, useEffect, useState, useRef } from 'react';
import { useStackApp } from '@stackframe/react';
import loginGif from '@/assets/login.gif';
import { Button } from '@/components/ui/button';
import { Input } from "@/components/ui/input";
import { Input } from '@/components/ui/input';
import github2 from "@/assets/github2.svg";
import google from "@/assets/google.svg";
import eye from "@/assets/eye.svg";
import eyeOff from "@/assets/eye-off.svg";
import { proxyFetchPost } from "@/api/http";
import { hasStackKeys } from "@/lib";
import { useTranslation } from "react-i18next";
import WindowControls from "@/components/WindowControls";
import github2 from '@/assets/github2.svg';
import google from '@/assets/google.svg';
import eye from '@/assets/eye.svg';
import eyeOff from '@/assets/eye-off.svg';
import { proxyFetchPost } from '@/api/http';
import { hasStackKeys } from '@/lib';
import { useTranslation } from 'react-i18next';
import WindowControls from '@/components/WindowControls';
const HAS_STACK_KEYS = hasStackKeys();
let lock = false;
export default function Login() {
const app = HAS_STACK_KEYS ? useStackApp() : null;
const { setAuth, setModelType, setLocalProxyValue } = useAuthStore();
const navigate = useNavigate();
const location = useLocation();
const [hidePassword, setHidePassword] = useState(true);
const { t } = useTranslation();
const [formData, setFormData] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState({
email: "",
password: "",
});
const [isLoading, setIsLoading] = useState(false);
const [generalError, setGeneralError] = useState("");
const titlebarRef = useRef<HTMLDivElement>(null);
const [platform, setPlatform] = useState<string>("");
const app = HAS_STACK_KEYS ? useStackApp() : null;
const { setAuth, setModelType, setLocalProxyValue } = useAuthStore();
const navigate = useNavigate();
const location = useLocation();
const [hidePassword, setHidePassword] = useState(true);
const { t } = useTranslation();
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [errors, setErrors] = useState({
email: '',
password: '',
});
const [isLoading, setIsLoading] = useState(false);
const [generalError, setGeneralError] = useState('');
const titlebarRef = useRef<HTMLDivElement>(null);
const [platform, setPlatform] = useState<string>('');
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validateForm = () => {
const newErrors = {
email: "",
password: "",
};
const validateForm = () => {
const newErrors = {
email: '',
password: '',
};
if (!formData.email) {
newErrors.email = t("layout.please-enter-email-address");
} else if (!validateEmail(formData.email)) {
newErrors.email = t("layout.please-enter-a-valid-email-address");
}
if (!formData.email) {
newErrors.email = t('layout.please-enter-email-address');
} else if (!validateEmail(formData.email)) {
newErrors.email = t('layout.please-enter-a-valid-email-address');
}
if (!formData.password) {
newErrors.password = t("layout.please-enter-password");
} else if (formData.password.length < 6) {
newErrors.password = t("layout.password-must-be-at-least-8-characters");
}
if (!formData.password) {
newErrors.password = t('layout.please-enter-password');
} else if (formData.password.length < 8) {
newErrors.password = t('layout.password-must-be-at-least-8-characters');
}
setErrors(newErrors);
return !newErrors.email && !newErrors.password;
};
setErrors(newErrors);
return !newErrors.email && !newErrors.password;
};
const getLoginErrorMessage = (data: any) => {
if (!data || typeof data !== "object" || typeof data.code !== "number") {
return "";
}
const getLoginErrorMessage = (data: any) => {
if (!data || typeof data !== 'object' || typeof data.code !== 'number') {
return '';
}
if (data.code === 0) {
return "";
}
if (data.code === 0) {
return '';
}
if (data.code === 10) {
return data.text || t("layout.login-failed-please-check-your-email-and-password");
}
if (data.code === 10) {
return (
data.text ||
t('layout.login-failed-please-check-your-email-and-password')
);
}
if (data.code === 1 && Array.isArray(data.error) && data.error.length > 0) {
const firstError = data.error[0];
if (typeof firstError === "string") {
return firstError;
}
if (typeof firstError?.msg === "string") {
return firstError.msg;
}
if (typeof firstError?.message === "string") {
return firstError.message;
}
}
if (data.code === 1 && Array.isArray(data.error) && data.error.length > 0) {
const firstError = data.error[0];
if (typeof firstError === 'string') {
return firstError;
}
if (typeof firstError?.msg === 'string') {
return firstError.msg;
}
if (typeof firstError?.message === 'string') {
return firstError.message;
}
}
return data.text || t("layout.login-failed-please-try-again");
};
return data.text || t('layout.login-failed-please-try-again');
};
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
if (errors[field as keyof typeof errors]) {
setErrors((prev) => ({
...prev,
[field]: "",
}));
}
if (errors[field as keyof typeof errors]) {
setErrors((prev) => ({
...prev,
[field]: '',
}));
}
if (generalError) {
setGeneralError("");
}
};
if (generalError) {
setGeneralError('');
}
};
//
const handleLogin = async () => {
if (!validateForm()) {
return;
}
//
const handleLogin = async () => {
if (!validateForm()) {
return;
}
setGeneralError("");
setIsLoading(true);
try {
const data = await proxyFetchPost("/api/login", {
email: formData.email,
password: formData.password,
});
setGeneralError('');
setIsLoading(true);
try {
const data = await proxyFetchPost('/api/login', {
email: formData.email,
password: formData.password,
});
const errorMessage = getLoginErrorMessage(data);
if (errorMessage) {
setGeneralError(errorMessage);
return;
}
const errorMessage = getLoginErrorMessage(data);
if (errorMessage) {
setGeneralError(errorMessage);
return;
}
setAuth({ email: formData.email, ...data });
setModelType('cloud');
// Record VITE_USE_LOCAL_PROXY value at login
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
setLocalProxyValue(localProxyValue);
navigate("/");
} catch (error: any) {
console.error("Login failed:", error);
setGeneralError(t("layout.login-failed-please-check-your-email-and-password"));
} finally {
setIsLoading(false);
}
};
setAuth({ email: formData.email, ...data });
setModelType('cloud');
// Record VITE_USE_LOCAL_PROXY value at login
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
setLocalProxyValue(localProxyValue);
navigate('/');
} catch (error: any) {
console.error('Login failed:', error);
setGeneralError(
t('layout.login-failed-please-check-your-email-and-password')
);
} finally {
setIsLoading(false);
}
};
const handleLoginByStack = async (token: string) => {
try {
const data = await proxyFetchPost("/api/login-by_stack?token=" + token, {
token: token,
});
const handleLoginByStack = async (token: string) => {
try {
const data = await proxyFetchPost('/api/login-by_stack?token=' + token, {
token: token,
});
const errorMessage = getLoginErrorMessage(data);
if (errorMessage) {
setGeneralError(errorMessage);
return;
}
console.log("data", data);
setModelType('cloud');
setAuth({ email: formData.email, ...data });
// Record VITE_USE_LOCAL_PROXY value at login
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
setLocalProxyValue(localProxyValue);
navigate("/");
} catch (error: any) {
console.error("Login failed:", error);
setGeneralError(t("layout.login-failed-please-check-your-email-and-password"));
} finally {
setIsLoading(false);
}
};
const errorMessage = getLoginErrorMessage(data);
if (errorMessage) {
setGeneralError(errorMessage);
return;
}
console.log('data', data);
setModelType('cloud');
setAuth({ email: formData.email, ...data });
// Record VITE_USE_LOCAL_PROXY value at login
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
setLocalProxyValue(localProxyValue);
navigate('/');
} catch (error: any) {
console.error('Login failed:', error);
setGeneralError(
t('layout.login-failed-please-check-your-email-and-password')
);
} finally {
setIsLoading(false);
}
};
const handleReloadBtn = async (type: string) => {
console.log("handleReloadBtn1", type);
const cookies = document.cookie.split("; ");
cookies.forEach((cookie) => {
const [name] = cookie.split("=");
if (name.startsWith("stack-oauth-outer-")) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}
});
console.log("handleReloadBtn2", type);
await app.signInWithOAuth(type);
};
const handleReloadBtn = async (type: string) => {
if (!app) {
console.error('Stack app not initialized');
return;
}
console.log('handleReloadBtn1', type);
const cookies = document.cookie.split('; ');
cookies.forEach((cookie) => {
const [name] = cookie.split('=');
if (name.startsWith('stack-oauth-outer-')) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}
});
console.log('handleReloadBtn2', type);
await app.signInWithOAuth(type);
};
const handleGetToken = async (code: string) => {
const code_verifier = localStorage.getItem("stack-oauth-outer-");
const formData = new URLSearchParams();
console.log(
"import.meta.env.PROD",
import.meta.env.PROD
? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback`
: `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback`
);
formData.append(
"redirect_uri",
import.meta.env.PROD
? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback`
: `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback`
);
formData.append("code_verifier", code_verifier || "");
formData.append("code", code);
formData.append("grant_type", "authorization_code");
formData.append("client_id", "aa49cdd0-318e-46bd-a540-0f1e5f2b391f");
formData.append(
"client_secret",
"pck_t13egrd9ve57tz52kfcd2s4h1zwya5502z43kr5xv5cx8"
);
const handleGetToken = async (code: string) => {
const code_verifier = localStorage.getItem('stack-oauth-outer-');
const formData = new URLSearchParams();
console.log(
'import.meta.env.PROD',
import.meta.env.PROD
? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback`
: `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback`
);
formData.append(
'redirect_uri',
import.meta.env.PROD
? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback`
: `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback`
);
formData.append('code_verifier', code_verifier || '');
formData.append('code', code);
formData.append('grant_type', 'authorization_code');
formData.append('client_id', 'aa49cdd0-318e-46bd-a540-0f1e5f2b391f');
formData.append(
'client_secret',
'pck_t13egrd9ve57tz52kfcd2s4h1zwya5502z43kr5xv5cx8'
);
try {
const res = await fetch(
"https://api.stack-auth.com/api/v1/auth/oauth/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
body: formData,
}
);
const data = await res.json(); // parse response data
return data.access_token;
} catch (error) {
console.error(error);
setIsLoading(false);
}
};
try {
const res = await fetch(
'https://api.stack-auth.com/api/v1/auth/oauth/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
body: formData,
}
);
const data = await res.json(); // parse response data
return data.access_token;
} catch (error) {
console.error(error);
setIsLoading(false);
}
};
const handleAuthCode = useCallback(
async (event: any, code: string) => {
if (lock || location.pathname !== "/login") return;
const handleAuthCode = useCallback(
async (event: any, code: string) => {
if (lock || location.pathname !== '/login') return;
lock = true;
setIsLoading(true);
let accessToken = await handleGetToken(code);
handleLoginByStack(accessToken);
setTimeout(() => {
lock = false;
}, 1500);
},
[location.pathname]
);
lock = true;
setIsLoading(true);
let accessToken = await handleGetToken(code);
handleLoginByStack(accessToken);
setTimeout(() => {
lock = false;
}, 1500);
},
[location.pathname]
);
useEffect(() => {
window.ipcRenderer?.on("auth-code-received", handleAuthCode);
useEffect(() => {
window.ipcRenderer?.on('auth-code-received', handleAuthCode);
return () => {
window.ipcRenderer?.off("auth-code-received", handleAuthCode);
};
}, []);
return () => {
window.ipcRenderer?.off('auth-code-received', handleAuthCode);
};
}, []);
useEffect(() => {
const p = window.electronAPI.getPlatform();
setPlatform(p);
useEffect(() => {
const p = window.electronAPI.getPlatform();
setPlatform(p);
if (platform === "darwin") {
titlebarRef.current?.classList.add("mac");
}
}, [platform]);
if (platform === 'darwin') {
titlebarRef.current?.classList.add('mac');
}
}, [platform]);
// Handle before-close event for login page
useEffect(() => {
const handleBeforeClose = () => {
// On login page, always close directly without confirmation
window.electronAPI.closeWindow(true);
};
// Handle before-close event for login page
useEffect(() => {
const handleBeforeClose = () => {
// On login page, always close directly without confirmation
window.electronAPI.closeWindow(true);
};
window.ipcRenderer?.on("before-close", handleBeforeClose);
window.ipcRenderer?.on('before-close', handleBeforeClose);
return () => {
window.ipcRenderer?.off("before-close", handleBeforeClose);
};
}, []);
return () => {
window.ipcRenderer?.off('before-close', handleBeforeClose);
};
}, []);
return (
<div className="h-full flex flex-col relative overflow-hidden">
{/* Titlebar with drag region and window controls */}
<div
className="absolute top-0 left-0 right-0 flex !h-9 items-center justify-between pl-2 py-1 z-50"
id="login-titlebar"
ref={titlebarRef}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
{/* Left spacer for macOS */}
<div
className={`${
platform === "darwin" ? "w-[70px]" : "w-0"
} flex items-center justify-center`}
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
>
{platform === "darwin" && <span className="text-label-md text-text-heading font-bold">Eigent</span>}
</div>
return (
<div className="h-full flex flex-col relative overflow-hidden">
{/* Titlebar with drag region and window controls */}
<div
className="absolute top-0 left-0 right-0 flex !h-9 items-center justify-between pl-2 py-1 z-50"
id="login-titlebar"
ref={titlebarRef}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
{/* Left spacer for macOS */}
<div
className={`${
platform === 'darwin' ? 'w-[70px]' : 'w-0'
} flex items-center justify-center`}
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
>
{platform === 'darwin' && (
<span className="text-label-md text-text-heading font-bold">
Eigent
</span>
)}
</div>
{/* Center drag region */}
<div
className="h-full flex-1 flex items-center"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="flex-1 h-10"></div>
</div>
{/* Center drag region */}
<div
className="h-full flex-1 flex items-center"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="flex-1 h-10"></div>
</div>
{/* Right window controls */}
<div
style={{ WebkitAppRegion: 'no-drag', pointerEvents: 'auto' } as React.CSSProperties}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<WindowControls />
</div>
</div>
{/* Right window controls */}
<div
style={
{
WebkitAppRegion: 'no-drag',
pointerEvents: 'auto',
} as React.CSSProperties
}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<WindowControls />
</div>
</div>
{/* Main content - image extends to top, form has padding */}
<div className={`p-2 flex items-center justify-center gap-2 h-full`}>
<div className="flex items-center justify-center h-full rounded-3xl bg-white-100%">
<img src={loginGif} className="rounded-3xl h-full object-cover" />
</div>
<div className="h-full flex-1 flex flex-col items-center justify-center pt-11">
<div className="flex-1 flex flex-col w-80 items-center justify-center">
<div className="flex self-stretch items-end justify-between mb-4">
<div className="text-text-heading text-heading-lg font-bold ">
{t("layout.login")}
</div>
<Button
variant="ghost"
size="sm"
onClick={() =>
window.open(
"https://www.eigent.ai/signup",
"_blank",
"noopener,noreferrer"
)
}
>
{t("layout.sign-up")}
</Button>
</div>
{HAS_STACK_KEYS && (
<div className="w-full pt-6">
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn("google")}
className="w-full rounded-[24px] mb-4 transition-all duration-300 ease-in-out text-[#F5F5F5] text-center font-inter text-[15px] font-bold leading-[22px] justify-center"
disabled={isLoading}
>
<img src={google} className="w-5 h-5" />
<span className="ml-2">{t("layout.continue-with-google-login")}</span>
</Button>
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn("github")}
className="w-full rounded-[24px] mb-4 transition-all duration-300 ease-in-out text-[#F5F5F5] text-center font-inter text-[15px] font-bold leading-[22px] justify-center"
disabled={isLoading}
>
<img src={github2} className="w-5 h-5" />
<span className="ml-2">{t("layout.continue-with-github-login")}</span>
</Button>
</div>
)}
{HAS_STACK_KEYS && (
<div className="mt-2 w-full text-[#222] text-center font-inter text-[15px] font-medium leading-[22px] mb-6">
{t("layout.or")}
</div>
)}
<div className="flex flex-col gap-4 w-full">
{generalError && (
<p className="text-text-cuation text-label-md mt-1 mb-4">
{generalError}
</p>
)}
<div className="flex flex-col gap-4 w-full mb-4 relative">
<Input
id="email"
type="email"
size="default"
title={t("layout.email")}
placeholder={t("layout.enter-your-email")}
required
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
state={errors.email ? "error" : undefined}
note={errors.email}
onEnter={handleLogin}
/>
{/* Main content - image extends to top, form has padding */}
<div className={`p-2 flex items-center justify-center gap-2 h-full`}>
<div className="flex items-center justify-center h-full rounded-3xl bg-white-100%">
<img src={loginGif} className="rounded-3xl h-full object-cover" />
</div>
<div className="h-full flex-1 flex flex-col items-center justify-center pt-11">
<div className="flex-1 flex flex-col w-80 items-center justify-center">
<div className="flex self-stretch items-end justify-between mb-4">
<div className="text-text-heading text-heading-lg font-bold ">
{t('layout.login')}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (import.meta.env.VITE_USE_LOCAL_PROXY === 'true') {
navigate('/signup');
} else {
window.open(
'https://www.eigent.ai/signup',
'_blank',
'noopener,noreferrer'
);
}
}}
>
{t('layout.sign-up')}
</Button>
</div>
{HAS_STACK_KEYS && (
<div className="w-full pt-6">
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('google')}
className="w-full rounded-[24px] mb-4 transition-all duration-300 ease-in-out text-[#F5F5F5] text-center font-inter text-[15px] font-bold leading-[22px] justify-center"
disabled={isLoading}
>
<img src={google} className="w-5 h-5" />
<span className="ml-2">
{t('layout.continue-with-google-login')}
</span>
</Button>
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('github')}
className="w-full rounded-[24px] mb-4 transition-all duration-300 ease-in-out text-[#F5F5F5] text-center font-inter text-[15px] font-bold leading-[22px] justify-center"
disabled={isLoading}
>
<img src={github2} className="w-5 h-5" />
<span className="ml-2">
{t('layout.continue-with-github-login')}
</span>
</Button>
</div>
)}
{HAS_STACK_KEYS && (
<div className="mt-2 w-full text-[#222] text-center font-inter text-[15px] font-medium leading-[22px] mb-6">
{t('layout.or')}
</div>
)}
<div className="flex flex-col gap-4 w-full">
{generalError && (
<p className="text-text-cuation text-label-md mt-1 mb-4">
{generalError}
</p>
)}
<div className="flex flex-col gap-4 w-full mb-4 relative">
<Input
id="email"
type="email"
size="default"
title={t('layout.email')}
placeholder={t('layout.enter-your-email')}
required
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
state={errors.email ? 'error' : undefined}
note={errors.email}
onEnter={handleLogin}
/>
<Input
id="password"
title={t("layout.password")}
size="default"
type={hidePassword ? "password" : "text"}
required
placeholder={t("layout.enter-your-password")}
value={formData.password}
onChange={(e) =>
handleInputChange("password", e.target.value)
}
state={errors.password ? "error" : undefined}
note={errors.password}
backIcon={<img src={hidePassword ? eye : eyeOff} />}
onBackIconClick={() => setHidePassword(!hidePassword)}
onEnter={handleLogin}
/>
</div>
</div>
<Button
onClick={handleLogin}
size="md"
variant="primary"
type="submit"
className="w-full rounded-full"
disabled={isLoading}
>
<span className="flex-1">
{isLoading ? t("layout.logging-in") : t("layout.log-in")}
</span>
</Button>
</div>
<Button
variant="ghost"
size="xs"
onClick={() => window.open("https://www.eigent.ai/privacy-policy", "_blank", "noopener,noreferrer")}
>
{t("layout.privacy-policy")}
</Button>
</div>
</div>
</div>
);
<Input
id="password"
title={t('layout.password')}
size="default"
type={hidePassword ? 'password' : 'text'}
required
placeholder={t('layout.enter-your-password')}
value={formData.password}
onChange={(e) =>
handleInputChange('password', e.target.value)
}
state={errors.password ? 'error' : undefined}
note={errors.password}
backIcon={<img src={hidePassword ? eye : eyeOff} />}
onBackIconClick={() => setHidePassword(!hidePassword)}
onEnter={handleLogin}
/>
</div>
</div>
<Button
onClick={handleLogin}
size="md"
variant="primary"
type="submit"
className="w-full rounded-full"
disabled={isLoading}
>
<span className="flex-1">
{isLoading ? t('layout.logging-in') : t('layout.log-in')}
</span>
</Button>
</div>
<Button
variant="ghost"
size="xs"
onClick={() =>
window.open(
'https://www.eigent.ai/privacy-policy',
'_blank',
'noopener,noreferrer'
)
}
>
{t('layout.privacy-policy')}
</Button>
</div>
</div>
</div>
);
}