Initial lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# ai-agent-admin
|
||||||
|
|
||||||
|
轻量化 AI Agent 管理后台,保留基础 Admin 能力,并提供 Provider、Model、Agent、Workflow、Knowledge Base 和 Agent Team 协作模块。
|
||||||
|
|
||||||
|
默认管理员:
|
||||||
|
|
||||||
|
- 用户名:admin
|
||||||
|
- 密码:admin123456
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
APP_NAME=AI Agent Admin
|
||||||
|
ENV=prod
|
||||||
|
DEBUG=false
|
||||||
|
APP_HOST=127.0.0.1
|
||||||
|
APP_PORT=18083
|
||||||
|
DATABASE_URL=postgresql+asyncpg://ai_agent_admin:change-me@127.0.0.1:5432/ai_agent_admin
|
||||||
|
REDIS_URL=redis://127.0.0.1:6379/3
|
||||||
|
JWT_SECRET_KEY=change-me-in-production
|
||||||
|
SEED_ADMIN_EMAIL=admin@ai-agent.local
|
||||||
|
SEED_ADMIN_PASSWORD=admin123456
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app ./app
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "18083"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.crud import crud_router
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models import (
|
||||||
|
Agent,
|
||||||
|
AgentConversation,
|
||||||
|
AgentMessage,
|
||||||
|
AgentTeam,
|
||||||
|
CollaborationRun,
|
||||||
|
KnowledgeBase,
|
||||||
|
LLMModel,
|
||||||
|
LLMProvider,
|
||||||
|
Workflow,
|
||||||
|
WorkflowRun,
|
||||||
|
)
|
||||||
|
from app.schemas.ai import (
|
||||||
|
AgentBase,
|
||||||
|
AgentOut,
|
||||||
|
ChatIn,
|
||||||
|
CollaborationRunIn,
|
||||||
|
CollaborationRunOut,
|
||||||
|
ConversationOut,
|
||||||
|
KnowledgeBaseIn,
|
||||||
|
KnowledgeBaseOut,
|
||||||
|
ModelBase,
|
||||||
|
ModelOut,
|
||||||
|
ProviderBase,
|
||||||
|
ProviderOut,
|
||||||
|
TeamBase,
|
||||||
|
TeamOut,
|
||||||
|
WorkflowBase,
|
||||||
|
WorkflowOut,
|
||||||
|
WorkflowRunIn,
|
||||||
|
WorkflowRunOut,
|
||||||
|
)
|
||||||
|
from app.schemas.common import Page, ResponseModel
|
||||||
|
from app.services.collaboration import CollaborationService
|
||||||
|
from app.services.llm import LLMService, sse
|
||||||
|
from app.services.workflow import WorkflowService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/ai", tags=["AI 平台"], dependencies=[Depends(get_current_user)])
|
||||||
|
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/providers", tags=["Provider"], model=LLMProvider, create_schema=ProviderBase, update_schema=ProviderBase, out_schema=ProviderOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/models", tags=["Model"], model=LLMModel, create_schema=ModelBase, update_schema=ModelBase, out_schema=ModelOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/agents", tags=["Agent"], model=Agent, create_schema=AgentBase, update_schema=AgentBase, out_schema=AgentOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/workflows", tags=["Workflow"], model=Workflow, create_schema=WorkflowBase, update_schema=WorkflowBase, out_schema=WorkflowOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/knowledge-bases", tags=["Knowledge"], model=KnowledgeBase, create_schema=KnowledgeBaseIn, update_schema=KnowledgeBaseIn, out_schema=KnowledgeBaseOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/teams", tags=["Agent Team"], model=AgentTeam, create_schema=TeamBase, update_schema=TeamBase, out_schema=TeamOut)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/agents/{agent_id}/chat")
|
||||||
|
async def chat(agent_id: str, payload: ChatIn, db: AsyncSession = Depends(get_db)):
|
||||||
|
agent = await db.get(Agent, agent_id)
|
||||||
|
if not agent or agent.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||||
|
|
||||||
|
conversation = None
|
||||||
|
if payload.conversation_id:
|
||||||
|
conversation = await db.get(AgentConversation, payload.conversation_id)
|
||||||
|
if not conversation:
|
||||||
|
conversation = AgentConversation(agent_id=agent.id, title=payload.message[:60] or "新建对话")
|
||||||
|
db.add(conversation)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
user_msg = AgentMessage(conversation_id=conversation.id, role="user", content=payload.message)
|
||||||
|
assistant_msg = AgentMessage(conversation_id=conversation.id, role="assistant", content="", status="pending")
|
||||||
|
db.add_all([user_msg, assistant_msg])
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
async def generate():
|
||||||
|
started = time.time()
|
||||||
|
yield sse({"type": "start", "conversation_id": conversation.id, "message_id": assistant_msg.id})
|
||||||
|
chunks = []
|
||||||
|
service = LLMService(db)
|
||||||
|
async for chunk in service.stream(agent, payload.message):
|
||||||
|
chunks.append(chunk)
|
||||||
|
yield sse({"type": "chunk", "content": chunk})
|
||||||
|
assistant_msg.content = "".join(chunks)
|
||||||
|
assistant_msg.status = "completed"
|
||||||
|
assistant_msg.elapsed_time = int((time.time() - started) * 1000)
|
||||||
|
conversation.total_tokens = conversation.total_tokens + len(assistant_msg.content)
|
||||||
|
await db.commit()
|
||||||
|
yield sse({"type": "complete", "conversation_id": conversation.id, "message_id": assistant_msg.id})
|
||||||
|
yield sse("[DONE]")
|
||||||
|
|
||||||
|
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agents/{agent_id}/conversations", response_model=Page[ConversationOut])
|
||||||
|
async def list_conversations(agent_id: str, page: int = 1, page_size: int = 20, db: AsyncSession = Depends(get_db)):
|
||||||
|
query = select(AgentConversation).where(AgentConversation.agent_id == agent_id, AgentConversation.is_deleted == False)
|
||||||
|
result = await db.execute(query.order_by(AgentConversation.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
|
||||||
|
total = len(result.scalars().all())
|
||||||
|
result = await db.execute(query.order_by(AgentConversation.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
|
||||||
|
return Page(items=result.scalars().all(), total=total, page=page, page_size=page_size)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/conversations/{conversation_id}/messages")
|
||||||
|
async def list_messages(conversation_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(
|
||||||
|
select(AgentMessage)
|
||||||
|
.where(AgentMessage.conversation_id == conversation_id, AgentMessage.is_deleted == False)
|
||||||
|
.order_by(AgentMessage.created_at.asc())
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/workflows/{workflow_id}/publish", response_model=WorkflowOut)
|
||||||
|
async def publish_workflow(workflow_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
workflow = await db.get(Workflow, workflow_id)
|
||||||
|
if not workflow or workflow.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||||
|
workflow.status = "published"
|
||||||
|
workflow.published_version = workflow.version
|
||||||
|
workflow.published_definition = workflow.definition
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(workflow)
|
||||||
|
return workflow
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/workflows/{workflow_id}/run", response_model=WorkflowRunOut)
|
||||||
|
async def run_workflow(workflow_id: str, payload: WorkflowRunIn, db: AsyncSession = Depends(get_db)):
|
||||||
|
workflow = await db.get(Workflow, workflow_id)
|
||||||
|
if not workflow or workflow.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="工作流不存在")
|
||||||
|
return await WorkflowService(db).run(workflow, payload.inputs)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/workflow-runs", response_model=Page[WorkflowRunOut])
|
||||||
|
async def list_workflow_runs(page: int = 1, page_size: int = 20, db: AsyncSession = Depends(get_db)):
|
||||||
|
query = select(WorkflowRun).where(WorkflowRun.is_deleted == False)
|
||||||
|
result = await db.execute(query.order_by(WorkflowRun.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
|
||||||
|
items = result.scalars().all()
|
||||||
|
return Page(items=items, total=len(items), page=page, page_size=page_size)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/teams/{team_id}/run", response_model=CollaborationRunOut)
|
||||||
|
async def run_team(team_id: str, payload: CollaborationRunIn, db: AsyncSession = Depends(get_db)):
|
||||||
|
team = await db.get(AgentTeam, team_id)
|
||||||
|
if not team or team.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="团队不存在")
|
||||||
|
return await CollaborationService(db).run(team, payload.task)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/collaboration-runs", response_model=Page[CollaborationRunOut])
|
||||||
|
async def list_collaboration_runs(page: int = 1, page_size: int = 20, db: AsyncSession = Depends(get_db)):
|
||||||
|
query = select(CollaborationRun).where(CollaborationRun.is_deleted == False)
|
||||||
|
result = await db.execute(query.order_by(CollaborationRun.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
|
||||||
|
items = result.scalars().all()
|
||||||
|
return Page(items=items, total=len(items), page=page, page_size=page_size)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy import or_, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import create_access_token, hash_password, verify_password
|
||||||
|
from app.models import User
|
||||||
|
from app.schemas.core import LoginIn, TokenOut, UserCreate, UserOut, UserUpdate
|
||||||
|
from app.schemas.common import Page
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=TokenOut)
|
||||||
|
async def login(payload: LoginIn, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(
|
||||||
|
or_(User.username == payload.username, User.email == payload.username),
|
||||||
|
User.is_deleted == False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user or not verify_password(payload.password, user.password_hash):
|
||||||
|
raise HTTPException(status_code=400, detail="用户名或密码错误")
|
||||||
|
if user.status != "enabled":
|
||||||
|
raise HTTPException(status_code=403, detail="用户已禁用")
|
||||||
|
return TokenOut(access_token=create_access_token(user.id, {"username": user.username}))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserOut)
|
||||||
|
async def me(user: User = Depends(get_current_user)):
|
||||||
|
return user
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.crud import crud_router
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import hash_password
|
||||||
|
from app.models import Announcement, Menu, Permission, Role, User
|
||||||
|
from app.schemas.common import Page, ResponseModel
|
||||||
|
from app.schemas.core import (
|
||||||
|
AnnouncementBase,
|
||||||
|
AnnouncementOut,
|
||||||
|
MenuBase,
|
||||||
|
MenuOut,
|
||||||
|
PermissionBase,
|
||||||
|
PermissionOut,
|
||||||
|
RoleBase,
|
||||||
|
RoleOut,
|
||||||
|
UserCreate,
|
||||||
|
UserOut,
|
||||||
|
UserUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/core", tags=["基础管理"], dependencies=[Depends(get_current_user)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users", response_model=Page[UserOut])
|
||||||
|
async def list_users(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
keyword: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
conditions = [User.is_deleted == False]
|
||||||
|
if keyword:
|
||||||
|
conditions.append((User.username.ilike(f"%{keyword}%")) | (User.email.ilike(f"%{keyword}%")))
|
||||||
|
query = select(User).where(*conditions)
|
||||||
|
total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
||||||
|
result = await db.execute(query.order_by(User.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
|
||||||
|
return Page(items=result.scalars().all(), total=total or 0, page=page, page_size=page_size)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users", response_model=UserOut)
|
||||||
|
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||||
|
exists = await db.scalar(select(User.id).where((User.username == payload.username) | (User.email == payload.email)))
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=400, detail="用户已存在")
|
||||||
|
user = User(**payload.model_dump(exclude={"password"}), password_hash=hash_password(payload.password))
|
||||||
|
db.add(user)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}", response_model=UserOut)
|
||||||
|
async def update_user(user_id: str, payload: UserUpdate, db: AsyncSession = Depends(get_db)):
|
||||||
|
user = await db.get(User, user_id)
|
||||||
|
if not user or user.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
data = payload.model_dump(exclude_unset=True)
|
||||||
|
password = data.pop("password", None)
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(user, key, value)
|
||||||
|
if password:
|
||||||
|
user.password_hash = hash_password(password)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/users/{user_id}", response_model=ResponseModel)
|
||||||
|
async def delete_user(user_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
user = await db.get(User, user_id)
|
||||||
|
if not user or user.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
user.is_deleted = True
|
||||||
|
await db.commit()
|
||||||
|
return ResponseModel(message="deleted")
|
||||||
|
|
||||||
|
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/roles", tags=["角色"], model=Role, create_schema=RoleBase, update_schema=RoleBase, out_schema=RoleOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(
|
||||||
|
prefix="/permissions",
|
||||||
|
tags=["权限"],
|
||||||
|
model=Permission,
|
||||||
|
create_schema=PermissionBase,
|
||||||
|
update_schema=PermissionBase,
|
||||||
|
out_schema=PermissionOut,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(prefix="/menus", tags=["菜单"], model=Menu, create_schema=MenuBase, update_schema=MenuBase, out_schema=MenuOut)
|
||||||
|
)
|
||||||
|
router.include_router(
|
||||||
|
crud_router(
|
||||||
|
prefix="/announcements",
|
||||||
|
tags=["公告"],
|
||||||
|
model=Announcement,
|
||||||
|
create_schema=AnnouncementBase,
|
||||||
|
update_schema=AnnouncementBase,
|
||||||
|
out_schema=AnnouncementOut,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from typing import Any, Type
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models import User
|
||||||
|
from app.schemas.common import Page, ResponseModel
|
||||||
|
|
||||||
|
|
||||||
|
def crud_router(
|
||||||
|
*,
|
||||||
|
prefix: str,
|
||||||
|
tags: list[str],
|
||||||
|
model: Type,
|
||||||
|
create_schema: Type[BaseModel],
|
||||||
|
update_schema: Type[BaseModel],
|
||||||
|
out_schema: Type[BaseModel],
|
||||||
|
) -> APIRouter:
|
||||||
|
router = APIRouter(prefix=prefix, tags=tags, dependencies=[Depends(get_current_user)])
|
||||||
|
|
||||||
|
@router.get("", response_model=Page[out_schema]) # type: ignore[valid-type]
|
||||||
|
async def list_items(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
keyword: str | None = Query(None),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
conditions = [model.is_deleted == False]
|
||||||
|
if keyword and hasattr(model, "name"):
|
||||||
|
conditions.append(model.name.ilike(f"%{keyword}%"))
|
||||||
|
query = select(model).where(*conditions)
|
||||||
|
total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
||||||
|
result = await db.execute(
|
||||||
|
query.order_by(model.sort.desc(), model.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||||
|
)
|
||||||
|
return Page(items=result.scalars().all(), total=total or 0, page=page, page_size=page_size)
|
||||||
|
|
||||||
|
@router.get("/{item_id}", response_model=out_schema) # type: ignore[valid-type]
|
||||||
|
async def get_item(item_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
item = await db.get(model, item_id)
|
||||||
|
if not item or item.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.post("", response_model=out_schema) # type: ignore[valid-type]
|
||||||
|
async def create_item(payload: create_schema, db: AsyncSession = Depends(get_db)): # type: ignore[valid-type]
|
||||||
|
item = model(**payload.model_dump())
|
||||||
|
db.add(item)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.put("/{item_id}", response_model=out_schema) # type: ignore[valid-type]
|
||||||
|
async def update_item(item_id: str, payload: update_schema, db: AsyncSession = Depends(get_db)): # type: ignore[valid-type]
|
||||||
|
item = await db.get(model, item_id)
|
||||||
|
if not item or item.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
data: dict[str, Any] = payload.model_dump(exclude_unset=True)
|
||||||
|
for key, value in data.items():
|
||||||
|
if hasattr(item, key):
|
||||||
|
setattr(item, key, value)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.delete("/{item_id}", response_model=ResponseModel)
|
||||||
|
async def delete_item(item_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
item = await db.get(model, item_id)
|
||||||
|
if not item or item.is_deleted:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
item.is_deleted = True
|
||||||
|
await db.commit()
|
||||||
|
return ResponseModel(message="deleted")
|
||||||
|
|
||||||
|
return router
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import decode_access_token
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
token: Annotated[str, Depends(oauth2_scheme)],
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db)],
|
||||||
|
) -> User:
|
||||||
|
payload = decode_access_token(token)
|
||||||
|
if not payload or not payload.get("sub"):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||||
|
result = await db.execute(select(User).where(User.id == payload["sub"], User.is_deleted == False))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user or user.status != "enabled":
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user")
|
||||||
|
return user
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
app_name: str = "AI Agent Admin"
|
||||||
|
env: str = "dev"
|
||||||
|
debug: bool = False
|
||||||
|
app_host: str = "127.0.0.1"
|
||||||
|
app_port: int = 18083
|
||||||
|
database_url: str = "postgresql+asyncpg://ai_agent_admin:ai_agent_admin@127.0.0.1:5432/ai_agent_admin"
|
||||||
|
redis_url: Optional[str] = "redis://127.0.0.1:6379/3"
|
||||||
|
jwt_secret_key: str = "change-me-in-production"
|
||||||
|
jwt_algorithm: str = "HS256"
|
||||||
|
access_token_expire_minutes: int = 1440
|
||||||
|
cors_origins: str = "*"
|
||||||
|
seed_admin_email: str = "admin@ai-agent.local"
|
||||||
|
seed_admin_password: str = "admin123456"
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_async_engine(
|
||||||
|
settings.database_url,
|
||||||
|
echo=settings.debug,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
yield session
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str, password_hash: str) -> bool:
|
||||||
|
return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(subject: str, extra: dict[str, Any] | None = None) -> str:
|
||||||
|
expires = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||||
|
payload: dict[str, Any] = {"sub": subject, "exp": expires}
|
||||||
|
if extra:
|
||||||
|
payload.update(extra)
|
||||||
|
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_access_token(token: str) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from app.api.ai import router as ai_router
|
||||||
|
from app.api.auth import router as auth_router
|
||||||
|
from app.api.core import router as core_router
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.database import AsyncSessionLocal, Base, engine
|
||||||
|
from app.models import * # noqa: F401,F403
|
||||||
|
from app.services.seed import seed_database
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
await seed_database(db)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title=settings.app_name, version="0.1.0", lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"] if settings.cors_origins == "*" else settings.cors_origins.split(","),
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(auth_router, prefix="/api")
|
||||||
|
app.include_router(core_router, prefix="/api")
|
||||||
|
app.include_router(ai_router, prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok", "app": settings.app_name}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run("app.main:app", host=settings.app_host, port=settings.app_port, reload=settings.debug)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from app.models.ai import (
|
||||||
|
Agent,
|
||||||
|
AgentConversation,
|
||||||
|
AgentMessage,
|
||||||
|
AgentTeam,
|
||||||
|
CollaborationRun,
|
||||||
|
KnowledgeBase,
|
||||||
|
LLMModel,
|
||||||
|
LLMProvider,
|
||||||
|
Workflow,
|
||||||
|
WorkflowRun,
|
||||||
|
)
|
||||||
|
from app.models.core import Announcement, Menu, Permission, Role, User
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Agent",
|
||||||
|
"AgentConversation",
|
||||||
|
"AgentMessage",
|
||||||
|
"AgentTeam",
|
||||||
|
"Announcement",
|
||||||
|
"CollaborationRun",
|
||||||
|
"KnowledgeBase",
|
||||||
|
"LLMModel",
|
||||||
|
"LLMProvider",
|
||||||
|
"Menu",
|
||||||
|
"Permission",
|
||||||
|
"Role",
|
||||||
|
"User",
|
||||||
|
"Workflow",
|
||||||
|
"WorkflowRun",
|
||||||
|
]
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, JSON, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
from app.models.base import TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class LLMProvider(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_llm_provider"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
provider_type: Mapped[str] = mapped_column(String(40), default="openai_compatible")
|
||||||
|
base_url: Mapped[str] = mapped_column(String(300), default="")
|
||||||
|
api_key: Mapped[str] = mapped_column(String(500), default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
models: Mapped[list["LLMModel"]] = relationship("LLMModel", back_populates="provider")
|
||||||
|
|
||||||
|
|
||||||
|
class LLMModel(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_llm_model"
|
||||||
|
|
||||||
|
provider_id: Mapped[str] = mapped_column(String(32), ForeignKey("ai_llm_provider.id"), index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(120))
|
||||||
|
display_name: Mapped[str] = mapped_column(String(120), default="")
|
||||||
|
context_length: Mapped[int] = mapped_column(Integer, default=8192)
|
||||||
|
supports_streaming: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
supports_function_call: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
default_temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||||
|
default_max_tokens: Mapped[int] = mapped_column(Integer, default=2048)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
provider: Mapped[LLMProvider] = relationship("LLMProvider", back_populates="models")
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_agent"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
avatar: Mapped[str] = mapped_column(String(300), default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="draft")
|
||||||
|
persona: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
system_prompt: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
model_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("ai_llm_model.id"), nullable=True)
|
||||||
|
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||||
|
max_tokens: Mapped[int] = mapped_column(Integer, default=2048)
|
||||||
|
tools: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
knowledge_base_ids: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
enable_memory: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
memory_window: Mapped[int] = mapped_column(Integer, default=10)
|
||||||
|
model: Mapped[LLMModel | None] = relationship("LLMModel")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentConversation(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_agent_conversation"
|
||||||
|
|
||||||
|
agent_id: Mapped[str] = mapped_column(String(32), ForeignKey("ai_agent.id"), index=True)
|
||||||
|
user_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||||
|
title: Mapped[str] = mapped_column(String(160), default="新建对话")
|
||||||
|
total_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
agent: Mapped[Agent] = relationship("Agent")
|
||||||
|
messages: Mapped[list["AgentMessage"]] = relationship("AgentMessage", back_populates="conversation")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentMessage(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_agent_message"
|
||||||
|
|
||||||
|
conversation_id: Mapped[str] = mapped_column(String(32), ForeignKey("ai_agent_conversation.id"), index=True)
|
||||||
|
role: Mapped[str] = mapped_column(String(20))
|
||||||
|
content: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="completed")
|
||||||
|
reasoning_steps: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
total_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
elapsed_time: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
conversation: Mapped[AgentConversation] = relationship("AgentConversation", back_populates="messages")
|
||||||
|
|
||||||
|
|
||||||
|
class Workflow(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_workflow"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="draft")
|
||||||
|
version: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
published_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
|
definition: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
published_definition: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
input_variables: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
output_variables: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
run_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowRun(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_workflow_run"
|
||||||
|
|
||||||
|
workflow_id: Mapped[str] = mapped_column(String(32), ForeignKey("ai_workflow.id"), index=True)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="running")
|
||||||
|
trigger_type: Mapped[str] = mapped_column(String(40), default="api")
|
||||||
|
inputs: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
outputs: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
execution_log: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
error_message: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
total_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
elapsed_time: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
started_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
|
||||||
|
completed_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
|
||||||
|
workflow: Mapped[Workflow] = relationship("Workflow")
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeBase(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_knowledge_base"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(120))
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentTeam(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_agent_team"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(120))
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
mode: Mapped[str] = mapped_column(String(20), default="sequential")
|
||||||
|
members: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborationRun(TimestampMixin, Base):
|
||||||
|
__tablename__ = "ai_collaboration_run"
|
||||||
|
|
||||||
|
team_id: Mapped[str] = mapped_column(String(32), ForeignKey("ai_agent_team.id"), index=True)
|
||||||
|
task: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="running")
|
||||||
|
messages: Mapped[list] = mapped_column(JSON, default=list)
|
||||||
|
final_answer: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
elapsed_time: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
team: Mapped[AgentTeam] = relationship("AgentTeam")
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, Integer, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
def new_id() -> str:
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampMixin:
|
||||||
|
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
|
||||||
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from sqlalchemy import Boolean, Column, ForeignKey, String, Table, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
from app.models.base import TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
role_permission = Table(
|
||||||
|
"core_role_permission",
|
||||||
|
Base.metadata,
|
||||||
|
Column("role_id", String(32), ForeignKey("core_role.id"), primary_key=True),
|
||||||
|
Column("permission_id", String(32), ForeignKey("core_permission.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
user_role = Table(
|
||||||
|
"core_user_role",
|
||||||
|
Base.metadata,
|
||||||
|
Column("user_id", String(32), ForeignKey("core_user.id"), primary_key=True),
|
||||||
|
Column("role_id", String(32), ForeignKey("core_role.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(TimestampMixin, Base):
|
||||||
|
__tablename__ = "core_user"
|
||||||
|
|
||||||
|
email: Mapped[str] = mapped_column(String(120), unique=True, index=True)
|
||||||
|
username: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
password_hash: Mapped[str] = mapped_column(String(200))
|
||||||
|
nickname: Mapped[str] = mapped_column(String(80), default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
roles: Mapped[list["Role"]] = relationship("Role", secondary=user_role, back_populates="users")
|
||||||
|
|
||||||
|
|
||||||
|
class Role(TimestampMixin, Base):
|
||||||
|
__tablename__ = "core_role"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(80), unique=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="enabled")
|
||||||
|
users: Mapped[list[User]] = relationship("User", secondary=user_role, back_populates="roles")
|
||||||
|
permissions: Mapped[list["Permission"]] = relationship("Permission", secondary=role_permission)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(TimestampMixin, Base):
|
||||||
|
__tablename__ = "core_permission"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
|
code: Mapped[str] = mapped_column(String(120), unique=True, index=True)
|
||||||
|
resource: Mapped[str] = mapped_column(String(80), default="")
|
||||||
|
action: Mapped[str] = mapped_column(String(40), default="")
|
||||||
|
|
||||||
|
|
||||||
|
class Menu(TimestampMixin, Base):
|
||||||
|
__tablename__ = "core_menu"
|
||||||
|
|
||||||
|
parent_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||||
|
title: Mapped[str] = mapped_column(String(100))
|
||||||
|
path: Mapped[str] = mapped_column(String(200), default="")
|
||||||
|
icon: Mapped[str] = mapped_column(String(100), default="")
|
||||||
|
permission_code: Mapped[str] = mapped_column(String(120), default="")
|
||||||
|
visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Announcement(TimestampMixin, Base):
|
||||||
|
__tablename__ = "core_announcement"
|
||||||
|
|
||||||
|
title: Mapped[str] = mapped_column(String(160))
|
||||||
|
content: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="draft")
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
provider_type: str = "openai_compatible"
|
||||||
|
base_url: str = ""
|
||||||
|
api_key: str = ""
|
||||||
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderOut(ProviderBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelBase(BaseModel):
|
||||||
|
provider_id: str
|
||||||
|
name: str
|
||||||
|
display_name: str = ""
|
||||||
|
context_length: int = 8192
|
||||||
|
supports_streaming: bool = True
|
||||||
|
supports_function_call: bool = False
|
||||||
|
default_temperature: float = 0.7
|
||||||
|
default_max_tokens: int = 2048
|
||||||
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class ModelOut(ModelBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: str = ""
|
||||||
|
avatar: str = ""
|
||||||
|
status: str = "draft"
|
||||||
|
persona: dict[str, Any] = {}
|
||||||
|
system_prompt: str = ""
|
||||||
|
model_id: str | None = None
|
||||||
|
temperature: float = 0.7
|
||||||
|
max_tokens: int = 2048
|
||||||
|
tools: list[Any] = []
|
||||||
|
knowledge_base_ids: list[str] = []
|
||||||
|
enable_memory: bool = True
|
||||||
|
memory_window: int = 10
|
||||||
|
|
||||||
|
|
||||||
|
class AgentOut(AgentBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatIn(BaseModel):
|
||||||
|
message: str
|
||||||
|
conversation_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationOut(BaseModel):
|
||||||
|
id: str
|
||||||
|
agent_id: str
|
||||||
|
title: str
|
||||||
|
total_tokens: int = 0
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageOut(BaseModel):
|
||||||
|
id: str
|
||||||
|
conversation_id: str
|
||||||
|
role: str
|
||||||
|
content: str
|
||||||
|
status: str
|
||||||
|
total_tokens: int = 0
|
||||||
|
elapsed_time: int = 0
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: str = ""
|
||||||
|
status: str = "draft"
|
||||||
|
definition: dict[str, Any] = {}
|
||||||
|
input_variables: list[Any] = []
|
||||||
|
output_variables: list[Any] = []
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowOut(WorkflowBase):
|
||||||
|
id: str
|
||||||
|
version: int
|
||||||
|
published_version: int | None = None
|
||||||
|
published_definition: dict[str, Any] = {}
|
||||||
|
run_count: int = 0
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowRunIn(BaseModel):
|
||||||
|
inputs: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowRunOut(BaseModel):
|
||||||
|
id: str
|
||||||
|
workflow_id: str
|
||||||
|
status: str
|
||||||
|
inputs: dict[str, Any]
|
||||||
|
outputs: dict[str, Any]
|
||||||
|
execution_log: list[Any]
|
||||||
|
error_message: str = ""
|
||||||
|
total_tokens: int = 0
|
||||||
|
elapsed_time: int = 0
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeBaseIn(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: str = ""
|
||||||
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeBaseOut(KnowledgeBaseIn):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: str = ""
|
||||||
|
mode: str = "sequential"
|
||||||
|
members: list[dict[str, Any]] = []
|
||||||
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class TeamOut(TeamBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborationRunIn(BaseModel):
|
||||||
|
task: str
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborationRunOut(BaseModel):
|
||||||
|
id: str
|
||||||
|
team_id: str
|
||||||
|
task: str
|
||||||
|
status: str
|
||||||
|
messages: list[Any]
|
||||||
|
final_answer: str
|
||||||
|
elapsed_time: int
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseModel(BaseModel):
|
||||||
|
success: bool = True
|
||||||
|
message: str = "ok"
|
||||||
|
data: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Page(BaseModel, Generic[T]):
|
||||||
|
items: list[T]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class IdsIn(BaseModel):
|
||||||
|
ids: list[str]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
class LoginIn(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenOut(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
username: str
|
||||||
|
nickname: str = ""
|
||||||
|
status: str = "enabled"
|
||||||
|
is_superuser: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str = "123456"
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
email: EmailStr | None = None
|
||||||
|
username: str | None = None
|
||||||
|
nickname: str | None = None
|
||||||
|
status: str | None = None
|
||||||
|
is_superuser: bool | None = None
|
||||||
|
password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserOut(UserBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: str = ""
|
||||||
|
status: str = "enabled"
|
||||||
|
|
||||||
|
|
||||||
|
class RoleOut(RoleBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
resource: str = ""
|
||||||
|
action: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionOut(PermissionBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MenuBase(BaseModel):
|
||||||
|
parent_id: str | None = None
|
||||||
|
title: str
|
||||||
|
path: str = ""
|
||||||
|
icon: str = ""
|
||||||
|
permission_code: str = ""
|
||||||
|
visible: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class MenuOut(MenuBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementBase(BaseModel):
|
||||||
|
title: str
|
||||||
|
content: str = ""
|
||||||
|
status: str = "draft"
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementOut(AnnouncementBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models import Agent, AgentTeam, CollaborationRun
|
||||||
|
from app.services.llm import LLMService
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborationService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.llm = LLMService(db)
|
||||||
|
|
||||||
|
async def run(self, team: AgentTeam, task: str) -> CollaborationRun:
|
||||||
|
started = time.time()
|
||||||
|
run = CollaborationRun(team_id=team.id, task=task, status="running", messages=[], final_answer="")
|
||||||
|
self.db.add(run)
|
||||||
|
await self.db.flush()
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
context = task
|
||||||
|
for member in team.members or []:
|
||||||
|
agent_id = member.get("agent_id")
|
||||||
|
role = member.get("role", "member")
|
||||||
|
agent = await self.db.get(Agent, agent_id) if agent_id else None
|
||||||
|
if not agent:
|
||||||
|
content = f"{role}: 未配置有效智能体,跳过。"
|
||||||
|
else:
|
||||||
|
content = await self.llm.complete(agent, f"你的协作角色是 {role}。请基于上下文完成任务:{context}")
|
||||||
|
messages.append({"role": role, "agent_id": agent_id, "agent_name": agent.name if agent else "", "content": content})
|
||||||
|
context = f"{context}\n\n[{role}] {content}"
|
||||||
|
|
||||||
|
run.messages = messages
|
||||||
|
run.final_answer = messages[-1]["content"] if messages else "团队暂无成员,无法执行协作任务。"
|
||||||
|
run.status = "completed"
|
||||||
|
run.elapsed_time = int((time.time() - started) * 1000)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(run)
|
||||||
|
return run
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models import Agent, LLMModel, LLMProvider
|
||||||
|
|
||||||
|
|
||||||
|
class LLMService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def complete(self, agent: Agent, user_message: str, history: list[dict] | None = None) -> str:
|
||||||
|
if not agent.model_id:
|
||||||
|
return self._fallback_answer(agent, user_message)
|
||||||
|
model = await self.db.get(LLMModel, agent.model_id)
|
||||||
|
if not model:
|
||||||
|
return self._fallback_answer(agent, user_message)
|
||||||
|
provider = await self.db.get(LLMProvider, model.provider_id)
|
||||||
|
if not provider or provider.status != "enabled" or not provider.api_key or not provider.base_url:
|
||||||
|
return self._fallback_answer(agent, user_message)
|
||||||
|
|
||||||
|
messages = [{"role": "system", "content": agent.system_prompt or f"你是 {agent.name}。"}]
|
||||||
|
messages.extend(history or [])
|
||||||
|
messages.append({"role": "user", "content": user_message})
|
||||||
|
url = provider.base_url.rstrip("/") + "/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": model.name,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": agent.temperature,
|
||||||
|
"max_tokens": agent.max_tokens,
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient(timeout=60) as client:
|
||||||
|
response = await client.post(
|
||||||
|
url,
|
||||||
|
headers={"Authorization": f"Bearer {provider.api_key}", "Content-Type": "application/json"},
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data.get("choices", [{}])[0].get("message", {}).get("content") or ""
|
||||||
|
|
||||||
|
async def stream(self, agent: Agent, user_message: str, history: list[dict] | None = None) -> AsyncGenerator[str, None]:
|
||||||
|
answer = await self.complete(agent, user_message, history)
|
||||||
|
for chunk in self._chunk_text(answer):
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
def _fallback_answer(self, agent: Agent, user_message: str) -> str:
|
||||||
|
prompt = agent.system_prompt.strip() or "轻量 AI Agent"
|
||||||
|
return (
|
||||||
|
f"{agent.name} 已收到任务:{user_message}\n\n"
|
||||||
|
f"当前使用本地占位响应。配置可用的 LLM Provider 和 Model 后,将自动调用真实模型。\n\n"
|
||||||
|
f"系统提示词摘要:{prompt[:160]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _chunk_text(self, text: str) -> list[str]:
|
||||||
|
if not text:
|
||||||
|
return [""]
|
||||||
|
return [text[i : i + 24] for i in range(0, len(text), 24)]
|
||||||
|
|
||||||
|
|
||||||
|
def sse(data: dict | str) -> str:
|
||||||
|
if isinstance(data, str):
|
||||||
|
payload = data
|
||||||
|
else:
|
||||||
|
payload = json.dumps(data, ensure_ascii=False, default=str)
|
||||||
|
return f"data: {payload}\n\n"
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import hash_password
|
||||||
|
from app.models import Agent, AgentTeam, Announcement, LLMModel, LLMProvider, Menu, Permission, Role, User, Workflow
|
||||||
|
|
||||||
|
|
||||||
|
async def seed_database(db: AsyncSession) -> None:
|
||||||
|
exists = await db.scalar(select(User.id).limit(1))
|
||||||
|
if exists:
|
||||||
|
return
|
||||||
|
|
||||||
|
admin = User(
|
||||||
|
email=settings.seed_admin_email,
|
||||||
|
username="admin",
|
||||||
|
nickname="系统管理员",
|
||||||
|
password_hash=hash_password(settings.seed_admin_password),
|
||||||
|
is_superuser=True,
|
||||||
|
)
|
||||||
|
role = Role(name="超级管理员", code="admin", description="系统内置管理员")
|
||||||
|
permissions = [
|
||||||
|
Permission(name="用户管理", code="core:user", resource="user", action="manage"),
|
||||||
|
Permission(name="角色管理", code="core:role", resource="role", action="manage"),
|
||||||
|
Permission(name="AI 管理", code="ai:manage", resource="ai", action="manage"),
|
||||||
|
]
|
||||||
|
role.permissions = permissions
|
||||||
|
admin.roles = [role]
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
Menu(title="工作台", path="/", icon="Monitor"),
|
||||||
|
Menu(title="用户管理", path="/system/users", icon="User"),
|
||||||
|
Menu(title="角色管理", path="/system/roles", icon="Lock"),
|
||||||
|
Menu(title="菜单管理", path="/system/menus", icon="Menu"),
|
||||||
|
Menu(title="权限管理", path="/system/permissions", icon="Key"),
|
||||||
|
Menu(title="公告管理", path="/system/announcements", icon="Bell"),
|
||||||
|
Menu(title="Provider", path="/ai/providers", icon="Connection"),
|
||||||
|
Menu(title="Model", path="/ai/models", icon="Cpu"),
|
||||||
|
Menu(title="Agent", path="/ai/agents", icon="Avatar"),
|
||||||
|
Menu(title="Agent Chat", path="/ai/chat", icon="ChatLineRound"),
|
||||||
|
Menu(title="Workflow", path="/ai/workflows", icon="Share"),
|
||||||
|
Menu(title="Workflow Runs", path="/ai/workflow-runs", icon="Tickets"),
|
||||||
|
Menu(title="Knowledge Base", path="/ai/knowledge", icon="Collection"),
|
||||||
|
Menu(title="Agent Team", path="/ai/teams", icon="Operation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
provider = LLMProvider(
|
||||||
|
name="OpenAI Compatible",
|
||||||
|
code="openai_compatible",
|
||||||
|
provider_type="openai_compatible",
|
||||||
|
base_url="",
|
||||||
|
api_key="",
|
||||||
|
)
|
||||||
|
model = LLMModel(provider=provider, name="gpt-compatible", display_name="默认兼容模型")
|
||||||
|
agent = Agent(
|
||||||
|
name="Planner",
|
||||||
|
code="planner",
|
||||||
|
status="published",
|
||||||
|
system_prompt="你是任务规划智能体,负责拆解目标并给出执行计划。",
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
workflow = Workflow(
|
||||||
|
name="最小 LLM 工作流",
|
||||||
|
code="minimal_llm",
|
||||||
|
status="published",
|
||||||
|
definition={
|
||||||
|
"nodes": [
|
||||||
|
{"id": "start", "type": "start", "data": {}},
|
||||||
|
{"id": "plan", "type": "agent", "data": {"agent_id": ""}},
|
||||||
|
{"id": "end", "type": "end", "data": {}},
|
||||||
|
],
|
||||||
|
"edges": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
team = AgentTeam(
|
||||||
|
name="默认协作团队",
|
||||||
|
code="default_team",
|
||||||
|
description="Planner -> Executor -> Reviewer 的最小协作演示",
|
||||||
|
members=[{"agent_id": "", "role": "planner"}, {"agent_id": "", "role": "reviewer"}],
|
||||||
|
)
|
||||||
|
announcement = Announcement(title="AI Agent Admin 已初始化", content="轻量管理后台已可用。", status="published")
|
||||||
|
|
||||||
|
db.add_all([admin, role, *permissions, *menus, provider, model, agent, workflow, team, announcement])
|
||||||
|
await db.flush()
|
||||||
|
workflow.definition["nodes"][1]["data"]["agent_id"] = agent.id
|
||||||
|
team.members = [{"agent_id": agent.id, "role": "planner"}, {"agent_id": agent.id, "role": "reviewer"}]
|
||||||
|
await db.commit()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models import Agent, Workflow, WorkflowRun
|
||||||
|
from app.services.llm import LLMService
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.llm = LLMService(db)
|
||||||
|
|
||||||
|
async def run(self, workflow: Workflow, inputs: dict[str, Any]) -> WorkflowRun:
|
||||||
|
started = time.time()
|
||||||
|
definition = workflow.published_definition or workflow.definition or {}
|
||||||
|
run = WorkflowRun(
|
||||||
|
workflow_id=workflow.id,
|
||||||
|
status="running",
|
||||||
|
inputs=inputs,
|
||||||
|
outputs={},
|
||||||
|
execution_log=[],
|
||||||
|
started_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
self.db.add(run)
|
||||||
|
await self.db.flush()
|
||||||
|
|
||||||
|
variables: dict[str, Any] = dict(inputs)
|
||||||
|
log: list[dict[str, Any]] = []
|
||||||
|
try:
|
||||||
|
nodes = definition.get("nodes") or []
|
||||||
|
for node in nodes:
|
||||||
|
node_type = node.get("type", "unknown")
|
||||||
|
node_id = node.get("id", node_type)
|
||||||
|
data = node.get("data") or {}
|
||||||
|
entry = {"node_id": node_id, "node_type": node_type, "status": "completed"}
|
||||||
|
if node_type == "start":
|
||||||
|
entry["output"] = variables
|
||||||
|
elif node_type == "llm":
|
||||||
|
agent_id = data.get("agent_id")
|
||||||
|
prompt = data.get("prompt") or inputs.get("task") or inputs.get("message") or ""
|
||||||
|
agent = await self.db.get(Agent, agent_id) if agent_id else None
|
||||||
|
if agent:
|
||||||
|
result = await self.llm.complete(agent, prompt)
|
||||||
|
else:
|
||||||
|
result = f"LLM 节点占位输出:{prompt}"
|
||||||
|
variables[node_id] = result
|
||||||
|
entry["output"] = result
|
||||||
|
elif node_type == "agent":
|
||||||
|
agent_id = data.get("agent_id")
|
||||||
|
agent = await self.db.get(Agent, agent_id) if agent_id else None
|
||||||
|
task = data.get("task") or inputs.get("task") or inputs.get("message") or ""
|
||||||
|
result = await self.llm.complete(agent, task) if agent else f"Agent 节点占位输出:{task}"
|
||||||
|
variables[node_id] = result
|
||||||
|
entry["output"] = result
|
||||||
|
elif node_type == "condition":
|
||||||
|
entry["output"] = {"matched": True}
|
||||||
|
elif node_type in {"parallel", "merge", "tool", "http"}:
|
||||||
|
entry["output"] = f"{node_type} 节点已执行最小占位逻辑"
|
||||||
|
elif node_type == "end":
|
||||||
|
entry["output"] = variables
|
||||||
|
else:
|
||||||
|
entry["status"] = "skipped"
|
||||||
|
entry["output"] = "未知节点类型,已跳过"
|
||||||
|
log.append(entry)
|
||||||
|
|
||||||
|
run.status = "completed"
|
||||||
|
run.outputs = {"result": variables}
|
||||||
|
run.execution_log = log
|
||||||
|
workflow.run_count = (workflow.run_count or 0) + 1
|
||||||
|
except Exception as exc:
|
||||||
|
run.status = "failed"
|
||||||
|
run.error_message = str(exc)
|
||||||
|
run.execution_log = log
|
||||||
|
finally:
|
||||||
|
run.elapsed_time = int((time.time() - started) * 1000)
|
||||||
|
run.completed_at = datetime.utcnow()
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(run)
|
||||||
|
return run
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
fastapi==0.121.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
asyncpg==0.29.0
|
||||||
|
pydantic==2.5.2
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
bcrypt==4.2.1
|
||||||
|
httpx==0.27.0
|
||||||
|
redis==5.0.1
|
||||||
|
python-multipart==0.0.19
|
||||||
|
alembic==1.13.0
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
exec .venv/bin/uvicorn app.main:app --host "${APP_HOST:-127.0.0.1}" --port "${APP_PORT:-18083}"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AI Agent Admin FastAPI backend
|
||||||
|
After=network.target postgresql@17-main.service redis-server.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/opt/apps/ai-agent-admin/backend
|
||||||
|
EnvironmentFile=/opt/apps/ai-agent-admin/backend/.env
|
||||||
|
ExecStart=/opt/apps/ai-agent-admin/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 18083
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR=/opt/apps/ai-agent-admin
|
||||||
|
WEB_DIR=/var/www/ai-agent-admin
|
||||||
|
DB_NAME=ai_agent_admin
|
||||||
|
DB_USER=ai_agent_admin
|
||||||
|
DB_PASSWORD=${DB_PASSWORD:-ai_agent_admin}
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
sudo -n -u postgres psql -tc "select 1 from pg_roles where rolname='${DB_USER}'" | grep -q 1 || \
|
||||||
|
sudo -n -u postgres psql -c "create user ${DB_USER} with password '${DB_PASSWORD}'"
|
||||||
|
sudo -n -u postgres psql -tc "select 1 from pg_database where datname='${DB_NAME}'" | grep -q 1 || \
|
||||||
|
sudo -n -u postgres createdb -O "${DB_USER}" "${DB_NAME}"
|
||||||
|
|
||||||
|
cd "$APP_DIR/backend"
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install --upgrade pip
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
p = Path('.env')
|
||||||
|
text = p.read_text()
|
||||||
|
text = text.replace('change-me@127.0.0.1:5432/ai_agent_admin', 'ai_agent_admin@127.0.0.1:5432/ai_agent_admin')
|
||||||
|
text = text.replace('JWT_SECRET_KEY=change-me-in-production', 'JWT_SECRET_KEY=ai-agent-admin-prod-secret-change-later')
|
||||||
|
p.write_text(text)
|
||||||
|
PY
|
||||||
|
|
||||||
|
sudo -n cp "$APP_DIR/deploy/ai-agent-admin-backend.service" /etc/systemd/system/ai-agent-admin-backend.service
|
||||||
|
sudo -n systemctl daemon-reload
|
||||||
|
sudo -n systemctl enable ai-agent-admin-backend.service
|
||||||
|
sudo -n systemctl restart ai-agent-admin-backend.service
|
||||||
|
|
||||||
|
mkdir -p "$WEB_DIR"
|
||||||
|
rm -rf "$WEB_DIR"/*
|
||||||
|
cp -a "$APP_DIR/frontend/dist/." "$WEB_DIR/"
|
||||||
|
|
||||||
|
NGINX_SITE=/etc/nginx/sites-enabled/default
|
||||||
|
if ! sudo -n grep -q "AI Agent Admin begin" "$NGINX_SITE"; then
|
||||||
|
tmp=$(mktemp)
|
||||||
|
sudo -n awk 'BEGIN{done=0} /location \\// && done==0 {while ((getline line < "/opt/apps/ai-agent-admin/deploy/nginx-ai-agent-admin.conf") > 0) print line; done=1} {print}' "$NGINX_SITE" > "$tmp"
|
||||||
|
sudo -n cp "$tmp" "$NGINX_SITE"
|
||||||
|
rm -f "$tmp"
|
||||||
|
fi
|
||||||
|
sudo -n nginx -t
|
||||||
|
sudo -n systemctl reload nginx
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# AI Agent Admin begin
|
||||||
|
location = /ai-agent-admin {
|
||||||
|
return 301 /ai-agent-admin/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /ai-agent-admin/basic-api/ {
|
||||||
|
proxy_pass http://127.0.0.1:18083/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
client_max_body_size 100m;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /ai-agent-admin/ {
|
||||||
|
try_files $uri $uri/ /ai-agent-admin/index.html;
|
||||||
|
}
|
||||||
|
# AI Agent Admin end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18083:18083"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
Generated
+2104
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-agent-admin-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0 --port 5178",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview --host 0.0.0.0 --port 4178"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"element-plus": "^2.9.3",
|
||||||
|
"pinia": "^2.3.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<router-view v-if="isLoginPage" />
|
||||||
|
<el-container v-else class="app-shell">
|
||||||
|
<el-aside width="248px" class="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark">AI</span>
|
||||||
|
<div>
|
||||||
|
<strong>Agent Admin</strong>
|
||||||
|
<small>轻量智能体后台</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-menu router :default-active="$route.path" class="menu">
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>工作台</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-sub-menu index="system">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>系统管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/system/users">用户管理</el-menu-item>
|
||||||
|
<el-menu-item index="/system/roles">角色管理</el-menu-item>
|
||||||
|
<el-menu-item index="/system/menus">菜单管理</el-menu-item>
|
||||||
|
<el-menu-item index="/system/permissions">权限管理</el-menu-item>
|
||||||
|
<el-menu-item index="/system/announcements">公告管理</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
<el-sub-menu index="ai">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Connection /></el-icon>
|
||||||
|
<span>AI 管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/ai/providers">Provider</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/models">Model</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/agents">Agent</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/chat">Agent Chat</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/workflows">Workflow</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/workflow-runs">Workflow Runs</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/knowledge">Knowledge Base</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/teams">Agent Team</el-menu-item>
|
||||||
|
<el-menu-item index="/ai/collaboration-runs">Collaboration Runs</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="topbar">
|
||||||
|
<div>
|
||||||
|
<strong>{{ pageTitle }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="user">
|
||||||
|
<span>{{ auth.user?.nickname || auth.user?.username }}</span>
|
||||||
|
<el-button text @click="logout">退出</el-button>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main">
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Connection, Monitor, Setting } from '@element-plus/icons-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAuthStore } from './stores/auth';
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isLoginPage = computed(() => route.path === '/login');
|
||||||
|
const pageTitle = computed(() => route.meta.title || 'AI Agent Admin');
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
auth.logout();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '/ai-agent-admin/basic-api/api',
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('ai-agent-admin-token');
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response.data,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('ai-agent-admin-token');
|
||||||
|
location.href = '/ai-agent-admin/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { api };
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { api } from './http';
|
||||||
|
|
||||||
|
export interface ResourceConfig {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
fields: FieldConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldConfig {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type?: 'text' | 'textarea' | 'number' | 'boolean' | 'json';
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resources: Record<string, ResourceConfig> = {
|
||||||
|
users: {
|
||||||
|
title: '用户管理',
|
||||||
|
path: '/core/users',
|
||||||
|
fields: [
|
||||||
|
{ key: 'email', label: '邮箱', required: true },
|
||||||
|
{ key: 'username', label: '用户名', required: true },
|
||||||
|
{ key: 'nickname', label: '昵称' },
|
||||||
|
{ key: 'password', label: '密码' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
{ key: 'is_superuser', label: '超级管理员', type: 'boolean' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
title: '角色管理',
|
||||||
|
path: '/core/roles',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'description', label: '描述', type: 'textarea' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
title: '权限管理',
|
||||||
|
path: '/core/permissions',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'resource', label: '资源' },
|
||||||
|
{ key: 'action', label: '动作' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
menus: {
|
||||||
|
title: '菜单管理',
|
||||||
|
path: '/core/menus',
|
||||||
|
fields: [
|
||||||
|
{ key: 'parent_id', label: '父级 ID' },
|
||||||
|
{ key: 'title', label: '标题', required: true },
|
||||||
|
{ key: 'path', label: '路径' },
|
||||||
|
{ key: 'icon', label: '图标' },
|
||||||
|
{ key: 'permission_code', label: '权限编码' },
|
||||||
|
{ key: 'visible', label: '显示', type: 'boolean' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
announcements: {
|
||||||
|
title: '公告管理',
|
||||||
|
path: '/core/announcements',
|
||||||
|
fields: [
|
||||||
|
{ key: 'title', label: '标题', required: true },
|
||||||
|
{ key: 'content', label: '内容', type: 'textarea' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
providers: {
|
||||||
|
title: 'LLM Provider',
|
||||||
|
path: '/ai/providers',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'provider_type', label: '类型' },
|
||||||
|
{ key: 'base_url', label: 'Base URL' },
|
||||||
|
{ key: 'api_key', label: 'API Key' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
title: 'LLM Model',
|
||||||
|
path: '/ai/models',
|
||||||
|
fields: [
|
||||||
|
{ key: 'provider_id', label: 'Provider ID', required: true },
|
||||||
|
{ key: 'name', label: '模型名', required: true },
|
||||||
|
{ key: 'display_name', label: '显示名' },
|
||||||
|
{ key: 'context_length', label: '上下文长度', type: 'number' },
|
||||||
|
{ key: 'supports_streaming', label: '流式', type: 'boolean' },
|
||||||
|
{ key: 'supports_function_call', label: 'Function Call', type: 'boolean' },
|
||||||
|
{ key: 'default_temperature', label: '默认温度', type: 'number' },
|
||||||
|
{ key: 'default_max_tokens', label: '最大 Token', type: 'number' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
title: 'Agent 管理',
|
||||||
|
path: '/ai/agents',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'description', label: '描述', type: 'textarea' },
|
||||||
|
{ key: 'avatar', label: '头像' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
{ key: 'system_prompt', label: '系统提示词', type: 'textarea' },
|
||||||
|
{ key: 'model_id', label: 'Model ID' },
|
||||||
|
{ key: 'temperature', label: '温度', type: 'number' },
|
||||||
|
{ key: 'max_tokens', label: '最大 Token', type: 'number' },
|
||||||
|
{ key: 'tools', label: '工具 JSON', type: 'json' },
|
||||||
|
{ key: 'knowledge_base_ids', label: '知识库 ID JSON', type: 'json' },
|
||||||
|
{ key: 'enable_memory', label: '记忆', type: 'boolean' },
|
||||||
|
{ key: 'memory_window', label: '记忆窗口', type: 'number' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workflows: {
|
||||||
|
title: 'Workflow',
|
||||||
|
path: '/ai/workflows',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'description', label: '描述', type: 'textarea' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
{ key: 'definition', label: '定义 JSON', type: 'json' },
|
||||||
|
{ key: 'input_variables', label: '输入变量 JSON', type: 'json' },
|
||||||
|
{ key: 'output_variables', label: '输出变量 JSON', type: 'json' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
knowledge: {
|
||||||
|
title: 'Knowledge Base',
|
||||||
|
path: '/ai/knowledge-bases',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'description', label: '描述', type: 'textarea' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
title: 'Agent Team',
|
||||||
|
path: '/ai/teams',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称', required: true },
|
||||||
|
{ key: 'code', label: '编码', required: true },
|
||||||
|
{ key: 'description', label: '描述', type: 'textarea' },
|
||||||
|
{ key: 'mode', label: '模式' },
|
||||||
|
{ key: 'members', label: '成员 JSON', type: 'json' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function listResource(key: string, params?: Record<string, unknown>) {
|
||||||
|
return api.get(resources[key].path, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveResource(key: string, payload: Record<string, unknown>, id?: string) {
|
||||||
|
return id ? api.put(`${resources[key].path}/${id}`, payload) : api.post(resources[key].path, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteResource(key: string, id: string) {
|
||||||
|
return api.delete(`${resources[key].path}/${id}`);
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue';
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
|
import App from './App.vue';
|
||||||
|
import { router } from './router';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
createApp(App).use(createPinia()).use(router).use(ElementPlus).mount('#app');
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAuthStore } from './stores/auth';
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory('/ai-agent-admin/'),
|
||||||
|
routes: [
|
||||||
|
{ path: '/login', component: () => import('./views/LoginView.vue') },
|
||||||
|
{ path: '/', component: () => import('./views/DashboardView.vue') },
|
||||||
|
{ path: '/system/:resource', component: () => import('./views/ResourceView.vue') },
|
||||||
|
{ path: '/ai/chat', component: () => import('./views/AgentChatView.vue') },
|
||||||
|
{ path: '/ai/workflow-runs', component: () => import('./views/RunsView.vue'), props: { type: 'workflow' } },
|
||||||
|
{ path: '/ai/collaboration-runs', component: () => import('./views/RunsView.vue'), props: { type: 'collaboration' } },
|
||||||
|
{ path: '/ai/:resource', component: () => import('./views/ResourceView.vue') },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
if (to.path === '/login') return true;
|
||||||
|
if (!auth.token) return '/login';
|
||||||
|
if (!auth.user) await auth.fetchMe();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
import { api } from '@/api/http';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
token: localStorage.getItem('ai-agent-admin-token') || '',
|
||||||
|
user: null as any,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
const data: any = await api.post('/auth/login', { username, password });
|
||||||
|
this.token = data.access_token;
|
||||||
|
localStorage.setItem('ai-agent-admin-token', this.token);
|
||||||
|
await this.fetchMe();
|
||||||
|
},
|
||||||
|
async fetchMe() {
|
||||||
|
if (!this.token) return;
|
||||||
|
this.user = await api.get('/auth/me');
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.token = '';
|
||||||
|
this.user = null;
|
||||||
|
localStorage.removeItem('ai-agent-admin-token');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
:root {
|
||||||
|
color: #1f2937;
|
||||||
|
font-family:
|
||||||
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: #f5f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: #101827;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
align-items: center;
|
||||||
|
background: #2dd4bf;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #083344;
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: 800;
|
||||||
|
height: 34px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand small {
|
||||||
|
color: #9ca3af;
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .el-menu {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .el-menu-item,
|
||||||
|
.sidebar .el-sub-menu__title {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .el-menu-item.is-active {
|
||||||
|
background: #1f3a4a;
|
||||||
|
color: #67e8f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #0f172a, #164e63);
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 28px;
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: 320px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-log {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 460px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
background: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant {
|
||||||
|
background: #ecfdf5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>智能体</h3>
|
||||||
|
<el-button @click="loadAgents">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="agents" highlight-current-row @current-change="selectAgent">
|
||||||
|
<el-table-column prop="name" label="名称" />
|
||||||
|
<el-table-column prop="status" label="状态" width="100" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>{{ currentAgent?.name || 'Agent Chat' }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="chat-log">
|
||||||
|
<div v-for="(message, index) in messages" :key="index" class="message" :class="message.role">
|
||||||
|
<strong>{{ message.role === 'user' ? '我' : 'Agent' }}</strong>
|
||||||
|
<div>{{ message.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-input v-model="input" :rows="3" type="textarea" placeholder="输入任务或问题" style="margin-top: 12px" />
|
||||||
|
<el-button type="primary" :loading="sending" style="margin-top: 12px" @click="send">发送</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { api } from '@/api/http';
|
||||||
|
|
||||||
|
const agents = ref<any[]>([]);
|
||||||
|
const currentAgent = ref<any>(null);
|
||||||
|
const input = ref('请给出这个系统下一步优化建议');
|
||||||
|
const messages = ref<{ role: string; content: string }[]>([]);
|
||||||
|
const sending = ref(false);
|
||||||
|
|
||||||
|
onMounted(loadAgents);
|
||||||
|
|
||||||
|
async function loadAgents() {
|
||||||
|
const data: any = await api.get('/ai/agents');
|
||||||
|
agents.value = data.items || [];
|
||||||
|
currentAgent.value ||= agents.value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAgent(row: any) {
|
||||||
|
currentAgent.value = row;
|
||||||
|
messages.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
if (!currentAgent.value) return ElMessage.warning('请先选择智能体');
|
||||||
|
if (!input.value.trim()) return;
|
||||||
|
const userText = input.value;
|
||||||
|
input.value = '';
|
||||||
|
messages.value.push({ role: 'user', content: userText });
|
||||||
|
const assistant = { role: 'assistant', content: '' };
|
||||||
|
messages.value.push(assistant);
|
||||||
|
sending.value = true;
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('ai-agent-admin-token');
|
||||||
|
const response = await fetch(`/ai-agent-admin/basic-api/api/ai/agents/${currentAgent.value.id}/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message: userText }),
|
||||||
|
});
|
||||||
|
if (!response.body) throw new Error('浏览器不支持流式读取');
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
const text = decoder.decode(value);
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if (!line.startsWith('data: ')) continue;
|
||||||
|
const raw = line.slice(6);
|
||||||
|
if (raw === '[DONE]') continue;
|
||||||
|
const event = JSON.parse(raw);
|
||||||
|
if (event.type === 'chunk') assistant.content += event.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '发送失败');
|
||||||
|
} finally {
|
||||||
|
sending.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="grid">
|
||||||
|
<div v-for="item in metrics" :key="item.label" class="metric">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<strong>{{ item.value }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel" style="margin-top: 16px">
|
||||||
|
<h3>当前交付边界</h3>
|
||||||
|
<el-table :data="modules" border>
|
||||||
|
<el-table-column prop="name" label="模块" />
|
||||||
|
<el-table-column prop="scope" label="范围" />
|
||||||
|
<el-table-column prop="status" label="状态" width="120" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const metrics = [
|
||||||
|
{ label: '基础 Admin', value: '5' },
|
||||||
|
{ label: 'AI 模块', value: '8' },
|
||||||
|
{ label: '前端架构', value: 'Vite' },
|
||||||
|
{ label: '构建模式', value: '单应用' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
{ name: '认证/RBAC', scope: '登录、用户、角色、菜单、权限、公告', status: '可用' },
|
||||||
|
{ name: 'AI Agent', scope: 'Provider、Model、Agent、Chat SSE', status: '可用' },
|
||||||
|
{ name: 'Workflow', scope: 'JSON 定义、发布、运行记录', status: '可用' },
|
||||||
|
{ name: 'Agent Team', scope: '顺序多 Agent 协作和运行记录', status: '可用' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<el-form class="login-box" @submit.prevent="submit">
|
||||||
|
<h2>AI Agent Admin</h2>
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="username" placeholder="用户名或邮箱" size="large" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="password" placeholder="密码" show-password size="large" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" native-type="submit" size="large" :loading="loading" style="width: 100%">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
|
const username = ref('admin');
|
||||||
|
const password = ref('admin123456');
|
||||||
|
const loading = ref(false);
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await auth.login(username.value, password.value);
|
||||||
|
router.push('/');
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.response?.data?.detail || '登录失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>{{ config.title }}</h3>
|
||||||
|
<div>
|
||||||
|
<el-button v-if="resource === 'workflows'" @click="runSelected">运行</el-button>
|
||||||
|
<el-button v-if="resource === 'teams'" @click="runTeam">协作运行</el-button>
|
||||||
|
<el-button type="primary" @click="openCreate">新增</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table :data="items" border highlight-current-row @current-change="current = $event">
|
||||||
|
<el-table-column prop="id" label="ID" width="220" />
|
||||||
|
<el-table-column v-for="field in visibleFields" :key="field.key" :prop="field.key" :label="field.label" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="status" label="状态" width="120" />
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="editing?.id ? '编辑' : '新增'" width="680px">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item v-for="field in config.fields" :key="field.key" :label="field.label">
|
||||||
|
<el-switch v-if="field.type === 'boolean'" v-model="form[field.key]" />
|
||||||
|
<el-input-number v-else-if="field.type === 'number'" v-model="form[field.key]" style="width: 100%" />
|
||||||
|
<el-input v-else-if="field.type === 'textarea' || field.type === 'json'" v-model="form[field.key]" :rows="field.type === 'json' ? 8 : 3" type="textarea" />
|
||||||
|
<el-input v-else v-model="form[field.key]" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import { api } from '@/api/http';
|
||||||
|
import { deleteResource, listResource, resources, saveResource } from '@/api/resources';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const resource = computed(() => String(route.params.resource || 'users'));
|
||||||
|
const config = computed(() => resources[resource.value]);
|
||||||
|
const items = ref<any[]>([]);
|
||||||
|
const current = ref<any>(null);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const editing = ref<any>(null);
|
||||||
|
const form = reactive<Record<string, any>>({});
|
||||||
|
|
||||||
|
const visibleFields = computed(() => config.value.fields.filter((field) => !['api_key', 'password', 'tools', 'knowledge_base_ids', 'definition', 'input_variables', 'output_variables', 'members'].includes(field.key)).slice(0, 5));
|
||||||
|
|
||||||
|
watch(resource, load);
|
||||||
|
onMounted(load);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!config.value) return;
|
||||||
|
const data: any = await listResource(resource.value);
|
||||||
|
items.value = data.items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(row?: any) {
|
||||||
|
Object.keys(form).forEach((key) => delete form[key]);
|
||||||
|
for (const field of config.value.fields) {
|
||||||
|
const value = row?.[field.key];
|
||||||
|
if (field.type === 'json') {
|
||||||
|
form[field.key] = value === undefined ? '[]' : JSON.stringify(value, null, 2);
|
||||||
|
} else if (field.type === 'boolean') {
|
||||||
|
form[field.key] = value ?? false;
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
form[field.key] = value ?? 0;
|
||||||
|
} else {
|
||||||
|
form[field.key] = value ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editing.value = null;
|
||||||
|
resetForm();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: any) {
|
||||||
|
editing.value = row;
|
||||||
|
resetForm(row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const payload: Record<string, any> = {};
|
||||||
|
for (const field of config.value.fields) {
|
||||||
|
if (field.key === 'password' && !form[field.key]) continue;
|
||||||
|
if (field.type === 'json') {
|
||||||
|
payload[field.key] = form[field.key] ? JSON.parse(form[field.key]) : [];
|
||||||
|
} else {
|
||||||
|
payload[field.key] = form[field.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveResource(resource.value, payload, editing.value?.id);
|
||||||
|
dialogVisible.value = false;
|
||||||
|
ElMessage.success('已保存');
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row: any) {
|
||||||
|
await ElMessageBox.confirm(`确认删除 ${row.name || row.title || row.username || row.id}?`);
|
||||||
|
await deleteResource(resource.value, row.id);
|
||||||
|
ElMessage.success('已删除');
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSelected() {
|
||||||
|
const row = current.value || items.value[0];
|
||||||
|
if (!row) return ElMessage.warning('请先选择工作流');
|
||||||
|
await api.post(`/ai/workflows/${row.id}/run`, { inputs: { task: '验证最小工作流运行' } });
|
||||||
|
ElMessage.success('工作流已运行');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTeam() {
|
||||||
|
const row = current.value || items.value[0];
|
||||||
|
if (!row) return ElMessage.warning('请先选择团队');
|
||||||
|
await api.post(`/ai/teams/${row.id}/run`, { task: '请协作完成一个轻量后台建设验收建议' });
|
||||||
|
ElMessage.success('协作任务已运行');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>{{ type === 'workflow' ? 'Workflow Runs' : 'Collaboration Runs' }}</h3>
|
||||||
|
<el-button @click="load">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="items" border>
|
||||||
|
<el-table-column prop="id" label="ID" width="220" />
|
||||||
|
<el-table-column v-if="type === 'workflow'" prop="workflow_id" label="Workflow ID" width="220" />
|
||||||
|
<el-table-column v-else prop="team_id" label="Team ID" width="220" />
|
||||||
|
<el-table-column prop="status" label="状态" width="120" />
|
||||||
|
<el-table-column prop="elapsed_time" label="耗时 ms" width="120" />
|
||||||
|
<el-table-column label="详情">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<pre>{{ JSON.stringify(row.outputs || row.messages || row.final_answer, null, 2) }}</pre>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { api } from '@/api/http';
|
||||||
|
|
||||||
|
const props = defineProps<{ type: 'workflow' | 'collaboration' }>();
|
||||||
|
const items = ref<any[]>([]);
|
||||||
|
|
||||||
|
watch(() => props.type, load);
|
||||||
|
onMounted(load);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const path = props.type === 'workflow' ? '/ai/workflow-runs' : '/ai/collaboration-runs';
|
||||||
|
const data: any = await api.get(path);
|
||||||
|
items.value = data.items || [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: '/ai-agent-admin/',
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/ai-agent-admin/basic-api': {
|
||||||
|
target: 'http://127.0.0.1:18083',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/ai-agent-admin\/basic-api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
|
chunkSizeWarningLimit: 900,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user