diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..1445d2c --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,138 @@ +# name: Build and Push Docker Image + +# on: +# push: +# branches: +# - main +# paths: +# - 'backend/**' +# - 'frontend/**' +# - 'docker-compose.yml' +# - '.github/workflows/docker-build-push.yml' +# pull_request: +# branches: +# - main +# paths: +# - 'backend/**' +# - 'frontend/**' +# - 'docker-compose.yml' +# - '.github/workflows/docker-build-push.yml' + +# env: +# REGISTRY: docker.io +# BACKEND_IMAGE_NAME: mathmodelagent-backend +# FRONTEND_IMAGE_NAME: mathmodelagent-frontend + +# jobs: +# build-backend: +# runs-on: ubuntu-latest +# outputs: +# image-tag: ${{ steps.meta.outputs.tags }} +# image-digest: ${{ steps.build.outputs.digest }} + +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 + +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 + +# - name: Log in to Docker Hub +# if: github.event_name != 'pull_request' +# uses: docker/login-action@v3 +# with: +# registry: ${{ env.REGISTRY }} +# username: ${{ secrets.DOCKER_USERNAME }} +# password: ${{ secrets.DOCKER_PASSWORD }} + +# - name: Extract metadata for backend +# id: meta +# uses: docker/metadata-action@v5 +# with: +# images: ${{ env.REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/${{ env.BACKEND_IMAGE_NAME }} +# tags: | +# type=ref,event=branch +# type=ref,event=pr +# type=sha,prefix={{branch}}- +# type=raw,value=latest,enable={{is_default_branch}} + +# - name: Build and push backend Docker image +# id: build +# uses: docker/build-push-action@v5 +# with: +# context: ./backend +# file: ./backend/Dockerfile +# push: ${{ github.event_name != 'pull_request' }} +# tags: ${{ steps.meta.outputs.tags }} +# labels: ${{ steps.meta.outputs.labels }} +# cache-from: type=gha +# cache-to: type=gha,mode=max +# platforms: linux/amd64,linux/arm64 + +# build-frontend: +# runs-on: ubuntu-latest +# outputs: +# image-tag: ${{ steps.meta.outputs.tags }} +# image-digest: ${{ steps.build.outputs.digest }} + +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 + +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 + +# - name: Log in to Docker Hub +# if: github.event_name != 'pull_request' +# uses: docker/login-action@v3 +# with: +# registry: ${{ env.REGISTRY }} +# username: ${{ secrets.DOCKER_USERNAME }} +# password: ${{ secrets.DOCKER_PASSWORD }} + +# - name: Extract metadata for frontend +# id: meta +# uses: docker/metadata-action@v5 +# with: +# images: ${{ env.REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/${{ env.FRONTEND_IMAGE_NAME }} +# tags: | +# type=ref,event=branch +# type=ref,event=pr +# type=sha,prefix={{branch}}- +# type=raw,value=latest,enable={{is_default_branch}} + +# - name: Build and push frontend Docker image +# id: build +# uses: docker/build-push-action@v5 +# with: +# context: ./frontend +# file: ./frontend/Dockerfile +# push: ${{ github.event_name != 'pull_request' }} +# tags: ${{ steps.meta.outputs.tags }} +# labels: ${{ steps.meta.outputs.labels }} +# cache-from: type=gha +# cache-to: type=gha,mode=max +# platforms: linux/amd64,linux/arm64 + +# security-scan: +# runs-on: ubuntu-latest +# needs: [build-backend, build-frontend] +# if: github.event_name != 'pull_request' + +# strategy: +# matrix: +# component: [backend, frontend] + +# steps: +# - name: Run Trivy vulnerability scanner +# uses: aquasecurity/trivy-action@master +# with: +# image-ref: ${{ needs[format('build-{0}', matrix.component)].outputs.image-tag }} +# format: 'sarif' +# output: 'trivy-results-${{ matrix.component }}.sarif' + +# - name: Upload Trivy scan results to GitHub Security tab +# uses: github/codeql-action/upload-sarif@v3 +# if: always() +# with: +# sarif_file: 'trivy-results-${{ matrix.component }}.sarif' diff --git a/README.md b/README.md index 5422547..77e96ad 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,8 @@ docker-compose up -d 启动后端 -*启动 redis* +> [!CAUTION] +> 启动 Redis, 下载和运行问 AI ```bash cd backend # 切换到 backend 目录下 diff --git a/backend/.env.dev.example b/backend/.env.dev.example index cc698de..a1fd6f1 100644 --- a/backend/.env.dev.example +++ b/backend/.env.dev.example @@ -20,10 +20,6 @@ WRITER_API_KEY= WRITER_MODEL= # WRITER_BASE_URL= -DEFAULT_API_KEY= -DEFAULT_MODEL= -# DEFAULT_BASE_URL= - # 模型最大问答次数 MAX_CHAT_TURNS=60 # 思考反思次数 @@ -39,6 +35,7 @@ LOG_LEVEL=DEBUG DEBUG=true # 确保安装 Redis # 如果是docker: REDIS_URL=redis://redis:6379/0 +# 本地部署 : redis://localhost:6379/0 REDIS_URL=redis://localhost:6379/0 REDIS_MAX_CONNECTIONS=20 CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost:3000 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 479d7af..6a70efe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -22,4 +22,5 @@ RUN --mount=type=cache,target=/root/.cache/uv \ EXPOSE 8000 +# 直接使用 uvicorn,因为依赖已安装到系统 Python CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--ws-ping-interval", "60", "--ws-ping-timeout", "120"] \ No newline at end of file diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index ce8602d..fcb2dd7 100644 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -18,35 +18,31 @@ def parse_cors(value: str) -> list[str]: class Settings(BaseSettings): ENV: str - COORDINATOR_API_KEY: str - COORDINATOR_MODEL: str + COORDINATOR_API_KEY: Optional[str] = None + COORDINATOR_MODEL: Optional[str] = None COORDINATOR_BASE_URL: Optional[str] = None - MODELER_API_KEY: str - MODELER_MODEL: str + MODELER_API_KEY: Optional[str] = None + MODELER_MODEL: Optional[str] = None MODELER_BASE_URL: Optional[str] = None - CODER_API_KEY: str - CODER_MODEL: str + CODER_API_KEY: Optional[str] = None + CODER_MODEL: Optional[str] = None CODER_BASE_URL: Optional[str] = None - WRITER_API_KEY: str - WRITER_MODEL: str + WRITER_API_KEY: Optional[str] = None + WRITER_MODEL: Optional[str] = None WRITER_BASE_URL: Optional[str] = None - DEFAULT_API_KEY: str - DEFAULT_MODEL: str - DEFAULT_BASE_URL: Optional[str] = None - - MAX_CHAT_TURNS: int - MAX_RETRIES: int + MAX_CHAT_TURNS: int = 60 + MAX_RETRIES: int = 5 E2B_API_KEY: Optional[str] = None - LOG_LEVEL: str - DEBUG: bool - REDIS_URL: str - REDIS_MAX_CONNECTIONS: int - CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)] - SERVER_HOST: str = "http://localhost:8000" # 默认值 + LOG_LEVEL: str = "DEBUG" + DEBUG: bool = True + REDIS_URL: str = "redis://redis:6379/0" + REDIS_MAX_CONNECTIONS: int = 10 + CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)] = "*" + SERVER_HOST: str = "http://localhost:8000" OPENALEX_EMAIL: Optional[str] = None model_config = SettingsConfigDict( @@ -55,13 +51,6 @@ class Settings(BaseSettings): extra="allow", ) - def get_deepseek_config(self) -> dict: - return { - "api_key": self.DEEPSEEK_API_KEY, - "model": self.DEEPSEEK_MODEL, - "base_url": self.DEEPSEEK_BASE_URL, - } - @classmethod def from_env(cls, env: str = None): env = env or os.getenv("ENV", "dev") diff --git a/backend/app/core/agents/coordinator_agent.py b/backend/app/core/agents/coordinator_agent.py index 583e63d..ecf6fa9 100644 --- a/backend/app/core/agents/coordinator_agent.py +++ b/backend/app/core/agents/coordinator_agent.py @@ -30,9 +30,9 @@ async def run(self, ques_all: str) -> CoordinatorToModeler: ) json_str = response.choices[0].message.content - if not json_str.startswith("```json"): - logger.info(f"拒绝回答用户非数学建模请求:{json_str}") - raise ValueError(f"拒绝回答用户非数学建模请求:{json_str}") + # if not json_str.startswith("```json"): + # logger.info(f"拒绝回答用户非数学建模请求:{json_str}") + # raise ValueError(f"拒绝回答用户非数学建模请求:{json_str}") # 清理 JSON 字符串 json_str = json_str.replace("```json", "").replace("```", "").strip() diff --git a/backend/app/core/prompts.py b/backend/app/core/prompts.py index d4332ec..4c3c5c7 100644 --- a/backend/app/core/prompts.py +++ b/backend/app/core/prompts.py @@ -60,6 +60,8 @@ CODER_PROMPT = f""" You are an AI code interpreter specializing in data analysis with Python. Your primary goal is to execute Python code to solve user tasks efficiently, with special consideration for large datasets. +中文回复 + **Environment**: {platform.system()} **Key Skills**: pandas, numpy, seaborn, matplotlib, scikit-learn, xgboost, scipy **Data Visualization Style**: Nature/Science publication quality @@ -139,6 +141,8 @@ def get_writer_prompt( # Role Definition Professional writer for mathematical modeling competitions with expertise in technical documentation and literature synthesis + 中文回复 + # Core Tasks 1. Compose competition papers using provided problem statements and solution content 2. Strictly adhere to {format_output} formatting templates diff --git a/backend/app/routers/modeling_router.py b/backend/app/routers/modeling_router.py index ba00180..b36210d 100644 --- a/backend/app/routers/modeling_router.py +++ b/backend/app/routers/modeling_router.py @@ -16,10 +16,114 @@ from fastapi import HTTPException from icecream import ic from app.schemas.request import ExampleRequest +from pydantic import BaseModel +import litellm +from app.config.setting import settings router = APIRouter() +class ValidateApiKeyRequest(BaseModel): + api_key: str + base_url: str = "https://api.openai.com/v1" + model_id: str + + +class ValidateApiKeyResponse(BaseModel): + valid: bool + message: str + + +class SaveApiConfigRequest(BaseModel): + coordinator: dict + modeler: dict + coder: dict + writer: dict + + +@router.post("/save-api-config") +async def save_api_config(request: SaveApiConfigRequest): + """ + 保存验证成功的 API 配置到 settings + """ + try: + # 更新各个模块的设置 + if request.coordinator: + settings.COORDINATOR_API_KEY = request.coordinator.get('apiKey', '') + settings.COORDINATOR_MODEL = request.coordinator.get('modelId', '') + settings.COORDINATOR_BASE_URL = request.coordinator.get('baseUrl', '') + + if request.modeler: + settings.MODELER_API_KEY = request.modeler.get('apiKey', '') + settings.MODELER_MODEL = request.modeler.get('modelId', '') + settings.MODELER_BASE_URL = request.modeler.get('baseUrl', '') + + if request.coder: + settings.CODER_API_KEY = request.coder.get('apiKey', '') + settings.CODER_MODEL = request.coder.get('modelId', '') + settings.CODER_BASE_URL = request.coder.get('baseUrl', '') + + if request.writer: + settings.WRITER_API_KEY = request.writer.get('apiKey', '') + settings.WRITER_MODEL = request.writer.get('modelId', '') + settings.WRITER_BASE_URL = request.writer.get('baseUrl', '') + + return {"success": True, "message": "配置保存成功"} + except Exception as e: + logger.error(f"保存配置失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"保存配置失败: {str(e)}") + + +@router.post("/validate-api-key", response_model=ValidateApiKeyResponse) +async def validate_api_key(request: ValidateApiKeyRequest): + """ + 验证 API Key 的有效性 + """ + try: + # 使用 litellm 发送测试请求 + await litellm.acompletion( + model=request.model_id, + messages=[{"role": "user", "content": "Hi"}], + max_tokens=1, + api_key=request.api_key, + base_url=request.base_url if request.base_url != "https://api.openai.com/v1" else None, + ) + + return ValidateApiKeyResponse( + valid=True, + message="✓ 模型 API 验证成功" + ) + except Exception as e: + error_msg = str(e) + + # 解析不同类型的错误 + if "401" in error_msg or "Unauthorized" in error_msg: + return ValidateApiKeyResponse( + valid=False, + message="✗ API Key 无效或已过期" + ) + elif "404" in error_msg or "Not Found" in error_msg: + return ValidateApiKeyResponse( + valid=False, + message="✗ 模型 ID 不存在或 Base URL 错误" + ) + elif "429" in error_msg or "rate limit" in error_msg.lower(): + return ValidateApiKeyResponse( + valid=False, + message="✗ 请求过于频繁,请稍后再试" + ) + elif "403" in error_msg or "Forbidden" in error_msg: + return ValidateApiKeyResponse( + valid=False, + message="✗ API 权限不足或账户余额不足" + ) + else: + return ValidateApiKeyResponse( + valid=False, + message=f"✗ 验证失败: {error_msg[:50]}..." + ) + + @router.post("/example") async def exampleModeling( example_request: ExampleRequest, diff --git a/backend/app/utils/log_util.py b/backend/app/utils/log_util.py index 48bfb66..99e919a 100644 --- a/backend/app/utils/log_util.py +++ b/backend/app/utils/log_util.py @@ -39,14 +39,14 @@ def init_log(self): ) _logger.remove() # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 - _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=True) + _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=False) _logger.add( self.log_path_error, filter=self.__filter, format=format_str, rotation="50MB", encoding="utf-8", - enqueue=True, + enqueue=False, compression="zip", ) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 331c2d5..8593ec7 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,15 +1,20 @@ -# 构建阶段 -FROM node:20 AS build +FROM node:20-alpine WORKDIR /app + +# 复制依赖文件 COPY package.json pnpm-lock.yaml ./ + +# 安装 pnpm RUN npm install -g pnpm -RUN pnpm install + +# 清理 npm 缓存并安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源代码 COPY . . -# 应用将运行的端口 (Vite 开发环境通常是 5173) EXPOSE 5173 -# 运行开发服务器的命令 -# --host 确保可以从容器外部访问 +# 运行开发服务器 CMD ["pnpm", "run", "dev", "--host"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index c602318..927da22 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "md-editor-v3": "^5.4.1", "motion-v": "1.0.0-alpha.0", "pinia": "^3.0.1", + "pinia-plugin-persistedstate": "^4.5.0", "reka-ui": "^2.0.0", "render-jupyter-notebook-vue": "^2.2.4", "theme-colors": "^0.1.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e456fbc..3bed9de 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: pinia: specifier: ^3.0.1 version: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + pinia-plugin-persistedstate: + specifier: ^4.5.0 + version: 4.5.0(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))) reka-ui: specifier: ^2.0.0 version: 2.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) @@ -1186,6 +1189,9 @@ packages: resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} engines: {node: '>= 0.4'} + deep-pick-omit@1.2.1: + resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1210,6 +1216,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1821,6 +1830,20 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pinia-plugin-persistedstate@4.5.0: + resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + '@pinia/nuxt': '>=0.10.0' + pinia: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@pinia/nuxt': + optional: true + pinia: + optional: true + pinia@3.0.1: resolution: {integrity: sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg==} peerDependencies: @@ -3922,6 +3945,8 @@ snapshots: object-keys: 1.1.1 regexp.prototype.flags: 1.5.4 + deep-pick-omit@1.2.1: {} + deepmerge@4.3.1: {} deferred-leveldown@5.3.0: @@ -3946,6 +3971,8 @@ snapshots: delayed-stream@1.0.0: {} + destr@2.0.5: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -4541,6 +4568,14 @@ snapshots: pify@2.3.0: {} + pinia-plugin-persistedstate@4.5.0(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))): + dependencies: + deep-pick-omit: 1.2.1 + defu: 6.1.4 + destr: 2.0.5 + optionalDependencies: + pinia: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) + pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)): dependencies: '@vue/devtools-api': 7.7.2 diff --git a/frontend/src/apis/apiKeyApi.ts b/frontend/src/apis/apiKeyApi.ts new file mode 100644 index 0000000..a4a3fd2 --- /dev/null +++ b/frontend/src/apis/apiKeyApi.ts @@ -0,0 +1,48 @@ +import request from "@/utils/request"; + +// 验证 API Key 请求参数 +export interface ValidateApiKeyRequest { + api_key: string; + base_url?: string; + model_id: string; +} + +// 验证 API Key 响应 +export interface ValidateApiKeyResponse { + valid: boolean; + message: string; +} + +// 保存 API 配置请求参数 +export interface SaveApiConfigRequest { + coordinator: { + apiKey: string; + baseUrl: string; + modelId: string; + }; + modeler: { + apiKey: string; + baseUrl: string; + modelId: string; + }; + coder: { + apiKey: string; + baseUrl: string; + modelId: string; + }; + writer: { + apiKey: string; + baseUrl: string; + modelId: string; + }; +} + +// 验证 API Key +export function validateApiKey(params: ValidateApiKeyRequest) { + return request.post("/validate-api-key", params); +} + +// 保存 API 配置 +export function saveApiConfig(params: SaveApiConfigRequest) { + return request.post<{ success: boolean; message: string }>("/save-api-config", params); +} \ No newline at end of file diff --git a/frontend/src/apis/submitModelingApi.ts b/frontend/src/apis/submitModelingApi.ts index 6a142cd..88ec3d0 100644 --- a/frontend/src/apis/submitModelingApi.ts +++ b/frontend/src/apis/submitModelingApi.ts @@ -28,7 +28,7 @@ export function submitModelingTask( return request.post<{ task_id: string; status: string; - }>("/modeling/", formData, { + }>("/modeling", formData, { headers: { "Content-Type": "multipart/form-data", }, diff --git a/frontend/src/components/NavUser.vue b/frontend/src/components/NavUser.vue index c41516f..55b0381 100644 --- a/frontend/src/components/NavUser.vue +++ b/frontend/src/components/NavUser.vue @@ -26,8 +26,10 @@ import { ChevronsUpDown, CreditCard, LogOut, - Sparkles, + KeyRound, } from 'lucide-vue-next' +import { ref } from 'vue' +import ApiKeyDialog from '@/pages/chat/components/ApiDialog.vue' const props = defineProps({ user: { @@ -41,6 +43,15 @@ const props = defineProps({ }) const { isMobile } = useSidebar() + + +// API Key 对话框控制 +const isApiKeyDialogOpen = ref(false) + +const openApiKeyDialog = () => { + isApiKeyDialogOpen.value = true +} + diff --git a/frontend/src/components/UserStepper.vue b/frontend/src/components/UserStepper.vue index 5eff821..3387cd9 100644 --- a/frontend/src/components/UserStepper.vue +++ b/frontend/src/components/UserStepper.vue @@ -17,9 +17,13 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Rocket } from 'lucide-vue-next' import { useRouter } from 'vue-router' import { useTaskStore } from '@/stores/task' +import { useToast } from '@/components/ui/toast' +import { useApiKeyStore } from '@/stores/apiKeys' +import { saveApiConfig } from '@/apis/apiKeyApi' const taskStore = useTaskStore() - +const { toast } = useToast() +const apiKeyStore = useApiKeyStore() const currentStep = ref(1) const fileUploaded = ref(true) @@ -86,15 +90,41 @@ const handleFileUpload = (event: Event) => { const router = useRouter() + const handleSubmit = async () => { try { + + if (apiKeyStore.isEmpty) { + toast({ + title: '请先配置 API Key', + description: '在侧边栏 -> 头像 -> API Key 中配置 API Key', + variant: 'destructive', + }) + return + } + + // 保存 API Key + await saveApiConfig({ + coordinator: apiKeyStore.coordinatorConfig, + modeler: apiKeyStore.modelerConfig, + coder: apiKeyStore.coderConfig, + writer: apiKeyStore.writerConfig + }) + + + + if (uploadedFiles.value.length === 0) { - throw new Error('请先上传文件') + toast({ + title: '请先上传文件', + description: '请先上传文件', + variant: 'destructive', + }) + return } console.log(selectedOptions.value) console.log(question.value) console.log(uploadedFiles.value) - const response = await submitModelingTask( { ques_all: question.value, @@ -112,11 +142,17 @@ const handleSubmit = async () => { showSubmitSuccess.value = false // 3秒后自动隐藏 }, 3000) router.push(`/task/${taskId.value}`) - console.log('任务提交成功:', response?.data) - // 这里可以添加跳转或状态更新逻辑 + toast({ + title: '任务提交成功', + description: '任务提交成功,编号为:' + taskId.value, + }) } catch (error) { console.error('任务提交失败:', error) - // 这里可以添加错误处理逻辑 + toast({ + title: '任务提交失败', + description: '请检查 API Key 是否正确', + variant: 'destructive', + }) } } diff --git a/frontend/src/components/ui/dialog/Dialog.vue b/frontend/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..730a99e --- /dev/null +++ b/frontend/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogClose.vue b/frontend/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..dc50e79 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogContent.vue b/frontend/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..c77a39e --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,47 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogDescription.vue b/frontend/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..dbb576a --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogFooter.vue b/frontend/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..b47ae0e --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogHeader.vue b/frontend/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..ab9f29e --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogScrollContent.vue b/frontend/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..6efddd6 --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,56 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogTitle.vue b/frontend/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..5101beb --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/components/ui/dialog/DialogTrigger.vue b/frontend/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..c7bbe1e --- /dev/null +++ b/frontend/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/components/ui/dialog/index.ts b/frontend/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..c9c577f --- /dev/null +++ b/frontend/src/components/ui/dialog/index.ts @@ -0,0 +1,9 @@ +export { default as Dialog } from "./Dialog.vue" +export { default as DialogClose } from "./DialogClose.vue" +export { default as DialogContent } from "./DialogContent.vue" +export { default as DialogDescription } from "./DialogDescription.vue" +export { default as DialogFooter } from "./DialogFooter.vue" +export { default as DialogHeader } from "./DialogHeader.vue" +export { default as DialogScrollContent } from "./DialogScrollContent.vue" +export { default as DialogTitle } from "./DialogTitle.vue" +export { default as DialogTrigger } from "./DialogTrigger.vue" diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 3c0c642..9642085 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,8 +3,11 @@ import { createPinia } from 'pinia' import '@/assets/style.css' import App from '@/App.vue' import router from '@/router' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) const app = createApp(App) app.use(router) diff --git a/frontend/src/pages/chat/components/ApiDialog.vue b/frontend/src/pages/chat/components/ApiDialog.vue new file mode 100644 index 0000000..cc65754 --- /dev/null +++ b/frontend/src/pages/chat/components/ApiDialog.vue @@ -0,0 +1,317 @@ + + + diff --git a/frontend/src/pages/chat/index.vue b/frontend/src/pages/chat/index.vue index 6880ae4..27a746a 100644 --- a/frontend/src/pages/chat/index.vue +++ b/frontend/src/pages/chat/index.vue @@ -11,6 +11,8 @@ import { SidebarTrigger, } from '@/components/ui/sidebar' import { getHelloWorld } from '@/apis/commonApi' +import Button from '@/components/ui/button/Button.vue' +import { AppWindow } from 'lucide-vue-next' onMounted(() => { getHelloWorld().then((res) => { console.log(res.data) @@ -25,6 +27,14 @@ onMounted(() => {
+
@@ -38,7 +48,9 @@ onMounted(() => { - +
+ 项目处于内测阶段,欢迎进群反馈 +
diff --git a/frontend/src/pages/task/index.vue b/frontend/src/pages/task/index.vue index 1f51ce5..e313c14 100644 --- a/frontend/src/pages/task/index.vue +++ b/frontend/src/pages/task/index.vue @@ -120,7 +120,7 @@ onBeforeUnmount(() => { -
+
diff --git a/frontend/src/stores/apiKeys.ts b/frontend/src/stores/apiKeys.ts new file mode 100644 index 0000000..b07a45e --- /dev/null +++ b/frontend/src/stores/apiKeys.ts @@ -0,0 +1,93 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import { AgentType } from "@/utils/enum"; +import type { ModelConfig } from "@/utils/interface"; + + +export const useApiKeyStore = defineStore('apiKeys', () => { + // 各个模型的配置 + const coordinatorConfig = ref({ + apiKey: '', + baseUrl: '', + modelId: '' + }); + + const modelerConfig = ref({ + apiKey: '', + baseUrl: '', + modelId: '' + }); + + const coderConfig = ref({ + apiKey: '', + baseUrl: '', + modelId: '' + }); + + const writerConfig = ref({ + apiKey: '', + baseUrl: '', + modelId: '' + }); + + const isEmpty = computed(() => { + return Object.values(getAllAgentConfigs()).every(config => config.apiKey === '') + }) + + // 设置协调者模型配置 + function setCoordinatorConfig(config: ModelConfig) { + coordinatorConfig.value = { ...config }; + } + + // 设置建模者模型配置 + function setModelerConfig(config: ModelConfig) { + modelerConfig.value = { ...config }; + } + + // 设置编码者模型配置 + function setCoderConfig(config: ModelConfig) { + coderConfig.value = { ...config }; + } + + // 设置写作者模型配置 + function setWriterConfig(config: ModelConfig) { + writerConfig.value = { ...config }; + } + + // 获取所有 agent 配置 + function getAllAgentConfigs() { + return { + [AgentType.COORDINATOR]: coordinatorConfig.value, + [AgentType.MODELER]: modelerConfig.value, + [AgentType.CODER]: coderConfig.value, + [AgentType.WRITER]: writerConfig.value, + }; + } + + // 重置所有配置 + function resetAll() { + coordinatorConfig.value = { apiKey: '', baseUrl: '', modelId: '' }; + modelerConfig.value = { apiKey: '', baseUrl: '', modelId: '' }; + coderConfig.value = { apiKey: '', baseUrl: '', modelId: '' }; + writerConfig.value = { apiKey: '', baseUrl: '', modelId: '' }; + } + + return { + // 状态 + coordinatorConfig, + modelerConfig, + coderConfig, + writerConfig, + isEmpty, + + // 方法 + setCoordinatorConfig, + setModelerConfig, + setCoderConfig, + setWriterConfig, + getAllAgentConfigs, + resetAll + } +}, { + persist: true // 启用持久化存储 +}); diff --git a/frontend/src/utils/interface.ts b/frontend/src/utils/interface.ts index f89e9e1..d6604f1 100644 --- a/frontend/src/utils/interface.ts +++ b/frontend/src/utils/interface.ts @@ -13,4 +13,11 @@ export interface ResultCell { } // 笔记本单元格类型(代码或结果) -export type NoteCell = CodeCell | ResultCell \ No newline at end of file +export type NoteCell = CodeCell | ResultCell + + +export interface ModelConfig { + apiKey: string; + baseUrl: string; + modelId: string; +} \ No newline at end of file diff --git a/push-to-dockerhub.sh b/push-to-dockerhub.sh new file mode 100755 index 0000000..dde4cf1 --- /dev/null +++ b/push-to-dockerhub.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# push-to-dockerhub.sh + +set -e + +USERNAME="sanjin66" +BACKEND_TAG="${USERNAME}/mathmodelagent-backend-github:latest" +FRONTEND_TAG="${USERNAME}/mathmodelagent-frontend-github:latest" + +echo "开始构建和推送 MathModelAgent..." + +# 构建并推送后端 +echo "构建后端镜像..." +docker build -t ${BACKEND_TAG} ./backend +echo "推送后端镜像..." +docker push ${BACKEND_TAG} + +# 构建并推送前端 +echo "构建前端镜像..." +docker build -t ${FRONTEND_TAG} ./frontend +echo "推送前端镜像..." +docker push ${FRONTEND_TAG} + +echo "推送完成!" \ No newline at end of file