From 0e485492cf9b6aff0268f87f2297e6a0f51de111 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 8 Jun 2026 15:05:57 +0800 Subject: [PATCH] Initial lightweight AI agent admin --- .gitignore | 7 + README.md | 8 + backend/.env.example | 10 + backend/Dockerfile | 7 + backend/app/__init__.py | 1 + backend/app/api/ai.py | 170 ++ backend/app/api/auth.py | 33 + backend/app/api/core.py | 107 ++ backend/app/api/crud.py | 79 + backend/app/api/deps.py | 26 + backend/app/core/config.py | 30 + backend/app/core/database.py | 26 + backend/app/core/security.py | 30 + backend/app/main.py | 47 + backend/app/models/__init__.py | 31 + backend/app/models/ai.py | 141 ++ backend/app/models/base.py | 19 + backend/app/models/core.py | 71 + backend/app/schemas/ai.py | 168 ++ backend/app/schemas/common.py | 22 + backend/app/schemas/core.py | 93 ++ backend/app/services/collaboration.py | 39 + backend/app/services/llm.py | 69 + backend/app/services/seed.py | 87 + backend/app/services/workflow.py | 82 + backend/requirements.txt | 12 + backend/scripts/run.sh | 4 + deploy/ai-agent-admin-backend.service | 15 + deploy/deploy_remote.sh | 50 + deploy/nginx-ai-agent-admin.conf | 20 + docker-compose.yml | 8 + frontend/index.html | 2 + frontend/package-lock.json | 2104 +++++++++++++++++++++++++ frontend/package.json | 25 + frontend/src/App.vue | 80 + frontend/src/api/http.ts | 25 + frontend/src/api/resources.ts | 163 ++ frontend/src/env.d.ts | 5 + frontend/src/main.ts | 11 + frontend/src/router.ts | 24 + frontend/src/stores/auth.ts | 27 + frontend/src/styles.css | 155 ++ frontend/src/views/AgentChatView.vue | 94 ++ frontend/src/views/DashboardView.vue | 34 + frontend/src/views/LoginView.vue | 42 + frontend/src/views/ResourceView.vue | 132 ++ frontend/src/views/RunsView.vue | 38 + frontend/tsconfig.json | 21 + frontend/vite.config.ts | 26 + 49 files changed, 4520 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/ai.py create mode 100644 backend/app/api/auth.py create mode 100644 backend/app/api/core.py create mode 100644 backend/app/api/crud.py create mode 100644 backend/app/api/deps.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/database.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/ai.py create mode 100644 backend/app/models/base.py create mode 100644 backend/app/models/core.py create mode 100644 backend/app/schemas/ai.py create mode 100644 backend/app/schemas/common.py create mode 100644 backend/app/schemas/core.py create mode 100644 backend/app/services/collaboration.py create mode 100644 backend/app/services/llm.py create mode 100644 backend/app/services/seed.py create mode 100644 backend/app/services/workflow.py create mode 100644 backend/requirements.txt create mode 100644 backend/scripts/run.sh create mode 100644 deploy/ai-agent-admin-backend.service create mode 100644 deploy/deploy_remote.sh create mode 100644 deploy/nginx-ai-agent-admin.conf create mode 100644 docker-compose.yml create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/http.ts create mode 100644 frontend/src/api/resources.ts create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/views/AgentChatView.vue create mode 100644 frontend/src/views/DashboardView.vue create mode 100644 frontend/src/views/LoginView.vue create mode 100644 frontend/src/views/ResourceView.vue create mode 100644 frontend/src/views/RunsView.vue create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dead8ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.venv +__pycache__ +*.pyc +.env +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b28d36 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# ai-agent-admin + +轻量化 AI Agent 管理后台,保留基础 Admin 能力,并提供 Provider、Model、Agent、Workflow、Knowledge Base 和 Agent Team 协作模块。 + +默认管理员: + +- 用户名:admin +- 密码:admin123456 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..f05c820 --- /dev/null +++ b/backend/.env.example @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b748754 --- /dev/null +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/ai.py b/backend/app/api/ai.py new file mode 100644 index 0000000..763ea2a --- /dev/null +++ b/backend/app/api/ai.py @@ -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) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..3bc8455 --- /dev/null +++ b/backend/app/api/auth.py @@ -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 diff --git a/backend/app/api/core.py b/backend/app/api/core.py new file mode 100644 index 0000000..381dccb --- /dev/null +++ b/backend/app/api/core.py @@ -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, + ) +) diff --git a/backend/app/api/crud.py b/backend/app/api/crud.py new file mode 100644 index 0000000..d77c88b --- /dev/null +++ b/backend/app/api/crud.py @@ -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 diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000..573455a --- /dev/null +++ b/backend/app/api/deps.py @@ -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 diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..e0ddd5d --- /dev/null +++ b/backend/app/core/config.py @@ -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() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..9d6b159 --- /dev/null +++ b/backend/app/core/database.py @@ -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 diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..34b2f82 --- /dev/null +++ b/backend/app/core/security.py @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..fb7856b --- /dev/null +++ b/backend/app/main.py @@ -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) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..a2a6b24 --- /dev/null +++ b/backend/app/models/__init__.py @@ -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", +] diff --git a/backend/app/models/ai.py b/backend/app/models/ai.py new file mode 100644 index 0000000..02f2c07 --- /dev/null +++ b/backend/app/models/ai.py @@ -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") diff --git a/backend/app/models/base.py b/backend/app/models/base.py new file mode 100644 index 0000000..8cda013 --- /dev/null +++ b/backend/app/models/base.py @@ -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) diff --git a/backend/app/models/core.py b/backend/app/models/core.py new file mode 100644 index 0000000..d5b1c83 --- /dev/null +++ b/backend/app/models/core.py @@ -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") diff --git a/backend/app/schemas/ai.py b/backend/app/schemas/ai.py new file mode 100644 index 0000000..f529ed4 --- /dev/null +++ b/backend/app/schemas/ai.py @@ -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) diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py new file mode 100644 index 0000000..4d5cbce --- /dev/null +++ b/backend/app/schemas/common.py @@ -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] diff --git a/backend/app/schemas/core.py b/backend/app/schemas/core.py new file mode 100644 index 0000000..bddfd03 --- /dev/null +++ b/backend/app/schemas/core.py @@ -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) diff --git a/backend/app/services/collaboration.py b/backend/app/services/collaboration.py new file mode 100644 index 0000000..55f92d4 --- /dev/null +++ b/backend/app/services/collaboration.py @@ -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 diff --git a/backend/app/services/llm.py b/backend/app/services/llm.py new file mode 100644 index 0000000..cc7de61 --- /dev/null +++ b/backend/app/services/llm.py @@ -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" diff --git a/backend/app/services/seed.py b/backend/app/services/seed.py new file mode 100644 index 0000000..a0bdd07 --- /dev/null +++ b/backend/app/services/seed.py @@ -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() diff --git a/backend/app/services/workflow.py b/backend/app/services/workflow.py new file mode 100644 index 0000000..e2817df --- /dev/null +++ b/backend/app/services/workflow.py @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..8864aae --- /dev/null +++ b/backend/requirements.txt @@ -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 diff --git a/backend/scripts/run.sh b/backend/scripts/run.sh new file mode 100644 index 0000000..3cd2c9f --- /dev/null +++ b/backend/scripts/run.sh @@ -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}" diff --git a/deploy/ai-agent-admin-backend.service b/deploy/ai-agent-admin-backend.service new file mode 100644 index 0000000..82ae9e0 --- /dev/null +++ b/deploy/ai-agent-admin-backend.service @@ -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 diff --git a/deploy/deploy_remote.sh b/deploy/deploy_remote.sh new file mode 100644 index 0000000..f3c6e45 --- /dev/null +++ b/deploy/deploy_remote.sh @@ -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 diff --git a/deploy/nginx-ai-agent-admin.conf b/deploy/nginx-ai-agent-admin.conf new file mode 100644 index 0000000..907f962 --- /dev/null +++ b/deploy/nginx-ai-agent-admin.conf @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c21a3a5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + backend: + build: ./backend + restart: unless-stopped + env_file: + - ./backend/.env + ports: + - "127.0.0.1:18083:18083" diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..fa207c8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..495fc7d --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2104 @@ +{ + "name": "ai-agent-admin-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-agent-admin-frontend", + "version": "0.1.0", + "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" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "vue": "3.5.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.1", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.1.tgz", + "integrity": "sha512-UFnm1+BckNi+azkKJ7L32q1uXs9ekr99Z9pWTQPeDR05jqEWUwQq51ro4kZMVrANbjknX3Z7ukCZwTi2T6Tr9A==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.1" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.35", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.4.tgz", + "integrity": "sha512-joip1uZTaQR0nD23N400gIdJ7xY+WiiiMA/BCKz842gvGBknqDQAzklUvDEhqFvvrhQY8S2ZANBMu4X70VMFGw==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..861bd6b --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..5027c99 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,80 @@ + + + diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts new file mode 100644 index 0000000..10e3600 --- /dev/null +++ b/frontend/src/api/http.ts @@ -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 }; diff --git a/frontend/src/api/resources.ts b/frontend/src/api/resources.ts new file mode 100644 index 0000000..200c5b6 --- /dev/null +++ b/frontend/src/api/resources.ts @@ -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 = { + 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) { + return api.get(resources[key].path, { params }); +} + +export function saveResource(key: string, payload: Record, 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}`); +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..64c3fd9 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..a5d6c35 --- /dev/null +++ b/frontend/src/main.ts @@ -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'); diff --git a/frontend/src/router.ts b/frontend/src/router.ts new file mode 100644 index 0000000..e0c4007 --- /dev/null +++ b/frontend/src/router.ts @@ -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; +}); diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..1cf5e14 --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -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'); + }, + }, +}); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..dbda4a2 --- /dev/null +++ b/frontend/src/styles.css @@ -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; +} diff --git a/frontend/src/views/AgentChatView.vue b/frontend/src/views/AgentChatView.vue new file mode 100644 index 0000000..49fe2a6 --- /dev/null +++ b/frontend/src/views/AgentChatView.vue @@ -0,0 +1,94 @@ + + + diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue new file mode 100644 index 0000000..b13bad0 --- /dev/null +++ b/frontend/src/views/DashboardView.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..3f598bb --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,42 @@ + + + diff --git a/frontend/src/views/ResourceView.vue b/frontend/src/views/ResourceView.vue new file mode 100644 index 0000000..5276e83 --- /dev/null +++ b/frontend/src/views/ResourceView.vue @@ -0,0 +1,132 @@ + + + diff --git a/frontend/src/views/RunsView.vue b/frontend/src/views/RunsView.vue new file mode 100644 index 0000000..b3ca27b --- /dev/null +++ b/frontend/src/views/RunsView.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..712e5fe --- /dev/null +++ b/frontend/tsconfig.json @@ -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"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..03f5d86 --- /dev/null +++ b/frontend/vite.config.ts @@ -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, + }, +});