简化配置

添加文件夹显示
This commit is contained in:
Sanjin
2025-09-02 00:02:36 +08:00
parent 108551b87f
commit 22b46a36e4
19 changed files with 581 additions and 348 deletions
-138
View File
@@ -1,138 +0,0 @@
# 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'
@@ -29,7 +29,6 @@ MAX_RETRIES=5
# E2B_API_KEY=
SERVER_HOST=http://localhost:8000
# 使用 email 注册账号从 https://openalex.org/ 文献
# OPENALEX_EMAIL=example@example.com
OPENALEX_EMAIL=
LOG_LEVEL=DEBUG
-2
View File
@@ -2,8 +2,6 @@ venv/
_pycache_/
*.pyc
.venv/
.env.dev
.env.prod
.idea/
.vscode/
+1 -1
View File
@@ -38,7 +38,7 @@ app.include_router(files_router.router)
# 跨域 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ALLOW_ORIGINS,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
+16 -1
View File
@@ -8,12 +8,27 @@ from fastapi import HTTPException
router = APIRouter()
@router.get("/download_url")
async def get_download_url(task_id: str, filename: str):
return {"download_url": f"http://localhost:8000/static/{task_id}/{filename}"}
@router.get("/download_all_url")
async def get_download_all_url(task_id: str):
return {"download_url": f"http://localhost:8000/static/{task_id}/all.zip"}
@router.get("/files")
async def get_files(task_id: str):
work_dir = get_work_dir(task_id)
files = get_current_files(work_dir, "all")
file_all = []
return {"files": files}
for i in files:
file_type = i.split(".")[-1]
file_all.append({"filename": i, "file_type": file_type})
return file_all
@router.get("/open_folder")
+63 -37
View File
@@ -19,6 +19,7 @@ from app.schemas.request import ExampleRequest
from pydantic import BaseModel
import litellm
from app.config.setting import settings
import requests
router = APIRouter()
@@ -29,6 +30,15 @@ class ValidateApiKeyRequest(BaseModel):
model_id: str
class ValidateOpenalexEmailRequest(BaseModel):
email: str
class ValidateOpenalexEmailResponse(BaseModel):
valid: bool
message: str
class ValidateApiKeyResponse(BaseModel):
valid: bool
message: str
@@ -39,6 +49,7 @@ class SaveApiConfigRequest(BaseModel):
modeler: dict
coder: dict
writer: dict
openalex_email: str
@router.post("/save-api-config")
@@ -49,25 +60,28 @@ async def save_api_config(request: SaveApiConfigRequest):
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', '')
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', '')
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', '')
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', '')
settings.WRITER_API_KEY = request.writer.get("apiKey", "")
settings.WRITER_MODEL = request.writer.get("modelId", "")
settings.WRITER_BASE_URL = request.writer.get("baseUrl", "")
if request.openalex_email:
settings.OPENALEX_EMAIL = request.openalex_email
return {"success": True, "message": "配置保存成功"}
except Exception as e:
logger.error(f"保存配置失败: {str(e)}")
@@ -86,44 +100,56 @@ async def validate_api_key(request: ValidateApiKeyRequest):
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 验证成功"
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 无效或已过期"
)
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 错误"
valid=False, message="✗ 模型 ID 不存在或 Base URL 错误"
)
elif "429" in error_msg or "rate limit" in error_msg.lower():
return ValidateApiKeyResponse(
valid=False,
message="✗ 请求过于频繁,请稍后再试"
valid=False, message="✗ 请求过于频繁,请稍后再试"
)
elif "403" in error_msg or "Forbidden" in error_msg:
return ValidateApiKeyResponse(
valid=False,
message="✗ API 权限不足或账户余额不足"
valid=False, message="✗ API 权限不足或账户余额不足"
)
else:
return ValidateApiKeyResponse(
valid=False,
message=f"✗ 验证失败: {error_msg[:50]}..."
valid=False, message=f"✗ 验证失败: {error_msg[:50]}..."
)
@router.post("/validate-openalex-email", response_model=ValidateOpenalexEmailResponse)
async def validate_openalex_email(request: ValidateOpenalexEmailRequest):
"""
验证 OpenAlex Email 的有效性
"""
try:
response = requests.get(
f"https://api.openalex.org/works?mailto={request.email}"
)
logger.debug(f"OpenAlex Email 验证响应: {response}")
response.raise_for_status()
return ValidateOpenalexEmailResponse(
valid=True, message="✓ OpenAlex Email 验证成功"
)
except Exception as e:
return ValidateOpenalexEmailResponse(
valid=False, message=f"✗ OpenAlex Email 验证失败: {str(e)}"
)
@router.post("/example")
async def exampleModeling(
example_request: ExampleRequest,
@@ -235,8 +261,8 @@ async def run_modeling_task_async(
# 创建任务并等待它完成
task = asyncio.create_task(MathModelWorkFlow().execute(problem))
# 设置超时时间(比如 60 分钟)
await asyncio.wait_for(task, timeout=3600)
# 设置超时时间(比如 300 分钟)
await asyncio.wait_for(task, timeout=3600 * 5)
# 发送任务完成状态
await redis_manager.publish_message(
-1
View File
@@ -25,7 +25,6 @@ dist-ssr
.env
.env.local
.env.development
.env.production
.env.test
+18
View File
@@ -19,22 +19,36 @@ export interface SaveApiConfigRequest {
apiKey: string;
baseUrl: string;
modelId: string;
provider: string;
};
modeler: {
apiKey: string;
baseUrl: string;
modelId: string;
provider: string;
};
coder: {
apiKey: string;
baseUrl: string;
modelId: string;
provider: string;
};
writer: {
apiKey: string;
baseUrl: string;
modelId: string;
provider: string;
};
openalex_email: string;
}
export interface ValidateOpenalexEmailRequest {
email: string;
}
export interface ValidateOpenalexEmailResponse {
valid: boolean;
message: string;
}
// 验证 API Key
@@ -42,6 +56,10 @@ export function validateApiKey(params: ValidateApiKeyRequest) {
return request.post<ValidateApiKeyResponse>("/validate-api-key", params);
}
export function validateOpenalexEmail(params: ValidateOpenalexEmailRequest) {
return request.post<ValidateOpenalexEmailResponse>("/validate-openalex-email", params);
}
// 保存 API 配置
export function saveApiConfig(params: SaveApiConfigRequest) {
return request.post<{ success: boolean; message: string }>("/save-api-config", params);
+33 -1
View File
@@ -1,7 +1,39 @@
import request from "@/utils/request";
export function getFiles(task_id: string) {
return request.get<{ message: string }>("/files", {
return request.get<{
files: {
filename: string;
file_type: string;
}[]
}>("/files", {
params: { task_id },
});
}
/**
* 获取单个文件下载链接
* @param task_id 任务ID
* @param filename 文件名
*/
export async function getFileDownloadUrl(task_id: string, filename: string) {
return await request.get<{ download_url: string }>(`/download_url`, {
params: {
task_id,
filename,
}
})
}
/**
* 获取所有文件压缩包下载链接
* @param task_id 任务ID
*/
export async function getAllFilesDownloadUrl(task_id: string) {
return await request.get<{ download_url: string }>(`/download_all_url`, {
params: {
task_id,
}
})
}
+1 -37
View File
@@ -36,43 +36,7 @@ const data = {
title: '历史任务',
url: '#',
items: [
{
title: 'Routing',
url: '#',
},
{
title: 'Data Fetching',
url: '#',
isActive: true,
},
{
title: 'Rendering',
url: '#',
},
{
title: 'Caching',
url: '#',
},
{
title: 'Styling',
url: '#',
},
{
title: 'Optimizing',
url: '#',
},
{
title: 'Configuring',
url: '#',
},
{
title: 'Testing',
url: '#',
},
{
title: 'Authentication',
url: '#',
},
],
},
-5
View File
@@ -24,7 +24,6 @@ import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
KeyRound,
} from 'lucide-vue-next'
@@ -103,10 +102,6 @@ const openApiKeyDialog = () => {
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
+170 -73
View File
@@ -7,42 +7,57 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useApiKeyStore } from '@/stores/apiKeys'
import { CheckCircle, XCircle } from 'lucide-vue-next'
import { validateApiKey, saveApiConfig } from '@/apis/apiKeyApi'
import { validateApiKey, saveApiConfig, validateOpenalexEmail } from '@/apis/apiKeyApi'
const apiKeyStore = useApiKeyStore()
// 本地表单数据
const form = ref<{
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 };
coordinator: { apiKey: string; baseUrl: string; modelId: string; provider: string };
modeler: { apiKey: string; baseUrl: string; modelId: string; provider: string };
coder: { apiKey: string; baseUrl: string; modelId: string; provider: string };
writer: { apiKey: string; baseUrl: string; modelId: string; provider: string };
openalex_email: string;
}>({
coordinator: {
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
},
modeler: {
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
},
coder: {
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
},
writer: {
apiKey: '',
baseUrl: '',
modelId: ''
}
modelId: '',
provider: ''
},
openalex_email: ''
})
// 验证状态
@@ -51,7 +66,8 @@ const validationResults = ref({
coordinator: { valid: false, message: '' },
modeler: { valid: false, message: '' },
coder: { valid: false, message: '' },
writer: { valid: false, message: '' }
writer: { valid: false, message: '' },
openalex_email: { valid: false, message: '' }
})
// 计算所有验证是否都通过
@@ -73,6 +89,7 @@ const loadFromStore = () => {
form.value.modeler = { ...apiKeyStore.modelerConfig }
form.value.coder = { ...apiKeyStore.coderConfig }
form.value.writer = { ...apiKeyStore.writerConfig }
form.value.openalex_email = apiKeyStore.openalexEmail
}
// 保存表单数据到 store
@@ -82,7 +99,7 @@ const saveToStore = async () => {
apiKeyStore.setModelerConfig(form.value.modeler)
apiKeyStore.setCoderConfig(form.value.coder)
apiKeyStore.setWriterConfig(form.value.writer)
apiKeyStore.setOpenalexEmail(form.value.openalex_email)
// 如果验证成功,也保存到后端设置
if (allValid.value) {
try {
@@ -90,7 +107,8 @@ const saveToStore = async () => {
coordinator: form.value.coordinator,
modeler: form.value.modeler,
coder: form.value.coder,
writer: form.value.writer
writer: form.value.writer,
openalex_email: form.value.openalex_email
})
} catch (error) {
console.error('保存配置到后端失败:', error)
@@ -159,15 +177,11 @@ const validateAllApiKeys = async () => {
coordinator: { valid: false, message: '' },
modeler: { valid: false, message: '' },
coder: { valid: false, message: '' },
writer: { valid: false, message: '' }
writer: { valid: false, message: '' },
openalex_email: { valid: false, message: '' }
}
try {
// 在 E2B 验证后等待 500ms
await new Promise(resolve => setTimeout(resolve, 500))
// 逐个验证各模型 API Keys避免并发请求
for (const config of modelConfigs.value) {
const key = config.key as keyof typeof validationResults.value
@@ -182,6 +196,10 @@ const validateAllApiKeys = async () => {
// 每次验证后等待 1 秒,避免触发速率限制
await new Promise(resolve => setTimeout(resolve, 1000))
}
// 验证 OpenAlex Email
validationResults.value.openalex_email = await validateOpenalexEmail({ email: form.value.openalex_email }).then(res => res.data)
} catch (error) {
console.error('验证过程中发生错误:', error)
// 显示全局错误
@@ -198,30 +216,70 @@ const validateAllApiKeys = async () => {
}
}
const links = {
const resetAll = () => {
form.value = {
coordinator: { apiKey: '', baseUrl: '', modelId: '', provider: '' },
modeler: { apiKey: '', baseUrl: '', modelId: '', provider: '' },
coder: { apiKey: '', baseUrl: '', modelId: '', provider: '' },
writer: { apiKey: '', baseUrl: '', modelId: '', provider: '' },
openalex_email: ''
}
}
const providers = {
"DeepSeek": {
"url": "https://platform.deepseek.com/api_keys",
"key": "DeepSeek",
"BaseURL": "https://api.deepseek.com",
"ModelID": "deepseek/deepseek-chat"
"baseUrl": "https://api.deepseek.com",
"modelId": "deepseek/deepseek-chat"
},
"硅基流动": {
"url": "https://cloud.siliconflow.cn/i/UIb4Enf4",
"key": "硅基流动",
"BaseURL": "https://api.siliconflow.cn",
"ModelID": "openai/deepseek-ai/DeepSeek-V3"
"baseUrl": "https://api.siliconflow.cn",
"modelId": "openai/deepseek-ai/DeepSeek-V3"
},
"Sophnet": {
"url": "https://www.sophnet.com/#?code=AZBSFG",
"key": "Sophnet",
"BaseURL": "https://www.sophnet.com/api/open-apis",
"ModelID": "openai/DeepSeek-V3-Fast"
"baseUrl": "https://www.sophnet.com/api/open-apis",
"modelId": "openai/DeepSeek-V3-Fast"
},
"OpenAI": {
"url": "https://platform.openai.com/api-keys",
"key": "OpenAI",
"BaseURL": "https://api.openai.com",
"ModelID": "openai/gpt-4o"
"baseUrl": "https://api.openai.com",
"modelId": "openai/gpt-5"
},
"302.AI": {
"url": "https://302.ai/",
"key": "302.AI",
"baseUrl": "https://api.302.ai",
"modelId": "openai/deepseek-chat"
},
"OpenAI 兼容": {
"url": "/",
"key": "OpenAI 兼容",
"baseUrl": "basurl",
"modelId": "provider/model_id"
}
}
// 当供应商选择改变时,自动填写配置
const onProviderChange = (configKey: string, providerKey: string) => {
const provider = providers[providerKey as keyof typeof providers]
if (provider) {
const formConfig = (form.value as any)[configKey]
formConfig.provider = providerKey
formConfig.baseUrl = provider.baseUrl
formConfig.modelId = provider.modelId
// 清除之前的验证结果
validationResults.value[configKey as keyof typeof validationResults.value] = {
valid: false,
message: ''
}
}
}
@@ -231,25 +289,10 @@ const links = {
<Dialog :open="props.open" @update:open="updateOpen">
<DialogContent class="max-w-xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>API Key 设置</DialogTitle>
<DialogTitle>设置</DialogTitle>
<DialogDescription>
为每个 Agent 配置合适模型
<br>
<div class="">
<div v-for="link in links" :key="link.key">
<div class="flex flex-col gap-1">
<a :href="link.url" target="_blank" class="text-blue-600 hover:text-blue-800 underline text-xs">
{{ link.key }}
</a>
<div class="text-xs text-muted-foreground">
Base URL: {{ link.BaseURL }}
</div>
<div class="text-xs text-muted-foreground">
Model ID: {{ link.ModelID }}
</div>
</div>
</div>
</div>
<div><a href="https://docs.litellm.ai/docs/providers" target="_blank"
class="text-blue-600 hover:text-blue-800 underline text-xs">
more details
@@ -263,51 +306,105 @@ const links = {
<!-- Models Configurations -->
<div v-for="config in modelConfigs" :key="config.key" class="space-y-2">
<h3 class="text-sm font-medium">{{ config.label }}</h3>
<div class="grid grid-cols-1 gap-2">
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label :for="`${config.key}-api-key`" class="text-xs text-muted-foreground">API Key</Label>
<div class="flex items-center gap-2">
<Input :id="`${config.key}-api-key`" v-model.trim="(form as any)[config.key].apiKey" type="password"
placeholder="请输入 API Key" class="h-7 text-xs flex-1" />
<div v-if="validationResults[config.key as keyof typeof validationResults].message"
class="flex items-center">
<CheckCircle v-if="validationResults[config.key as keyof typeof validationResults].valid"
class="h-4 w-4 text-green-500" />
<XCircle v-else class="h-4 w-4 text-red-500" />
<Label :for="`${config.key}-provider`" class="text-xs text-muted-foreground">提供商</Label>
<div class="flex gap-2 items-center">
<Select :model-value="(form as any)[config.key].provider"
@update:model-value="(value: any) => value && onProviderChange(config.key, String(value))">
<SelectTrigger class="w-[120px] h-7 text-xs">
<SelectValue placeholder="选择提供商" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>提供商</SelectLabel>
<SelectItem v-for="(provider, key) in providers" :key="key" :value="key">
{{ provider.key }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div v-if="(form as any)[config.key].provider">
<a :href="providers[(form as any)[config.key].provider as keyof typeof providers]?.url"
target="_blank" class="text-blue-600 hover:text-blue-800 underline text-xs">
{{ providers[(form as any)[config.key].provider as keyof typeof providers]?.key }}
</a>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label :for="`${config.key}-base-url`" class="text-xs text-muted-foreground">Base URL</Label>
<Input :id="`${config.key}-base-url`" v-model.trim="(form as any)[config.key].baseUrl"
placeholder="https://api.deepseek.com" class="h-7 text-xs" />
</div>
<div class="space-y-1">
<Label :for="`${config.key}-model-id`" class="text-xs text-muted-foreground">Model ID</Label>
<Input :id="`${config.key}-model-id`" v-model.trim="(form as any)[config.key].modelId"
placeholder="provider/model_id" class="h-7 text-xs" />
<div class="space-y-1">
<Label :for="`${config.key}-api-key`" class="text-xs text-muted-foreground">API Key</Label>
<Input :id="`${config.key}-api-key`" v-model.trim="(form as any)[config.key].apiKey" type="password"
placeholder="请输入 API Key" class="h-7 text-xs flex-1" />
<div v-if="validationResults[config.key as keyof typeof validationResults].message"
class="flex items-center">
<CheckCircle v-if="validationResults[config.key as keyof typeof validationResults].valid"
class="h-4 w-4 text-green-500" />
<XCircle v-else class="h-4 w-4 text-red-500" />
</div>
</div>
<div v-if="validationResults[config.key as keyof typeof validationResults].message" :class="[
'text-xs px-2 py-1 rounded text-left border',
validationResults[config.key as keyof typeof validationResults].valid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
]">
{{ validationResults[config.key as keyof typeof validationResults].message }}
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<Label :for="`${config.key}-base-url`" class="text-xs text-muted-foreground">Base URL</Label>
<Input :id="`${config.key}-base-url`" v-model.trim="(form as any)[config.key].baseUrl"
placeholder="baseUrl" class="h-7 text-xs" />
</div>
<div class="space-y-1">
<Label :for="`${config.key}-model-id`" class="text-xs text-muted-foreground">Model ID</Label>
<Input :id="`${config.key}-model-id`" v-model.trim="(form as any)[config.key].modelId"
placeholder="provider/model_id" class="h-7 text-xs" />
</div>
</div>
<div v-if="validationResults[config.key as keyof typeof validationResults].message" :class="[
'text-xs px-2 py-1 rounded text-left border',
validationResults[config.key as keyof typeof validationResults].valid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
]">
{{ validationResults[config.key as keyof typeof validationResults].message }}
</div>
</div>
</div>
<div class="space-y-2">
<h3 class="text-sm font-medium">其他</h3>
<Label :for="`openalex-email`" class="text-xs text-muted-foreground">OpenAlex Email</Label>
<div class="text-xs text-muted-foreground">
使用 email 注册账号从 <a href="https://openalex.org/" target="_blank"
class="text-blue-600 hover:text-blue-800 underline text-xs">OpenAlex</a> 获取访问文献权利
</div>
<Input :id="`openalex-email`" v-model.trim="form.openalex_email" placeholder="请输入 OpenAlex Email"
class="h-7 text-xs flex-1" />
<div v-if="validationResults.openalex_email.message" :class="[
'text-xs px-2 py-1 rounded text-left border',
validationResults.openalex_email.valid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
]">
{{ validationResults.openalex_email.message }}
</div>
</div>
<div class="flex justify-between items-center pt-3 border-t">
<Button @click="validateAllApiKeys" :disabled="validating" class="h-7 text-xs px-3" variant="secondary">
{{ validating ? '验证中...' : '一键验证' }}
</Button>
<div class="flex justify-between items-center gap-2">
<Button @click="validateAllApiKeys" :disabled="validating" class="h-7 text-xs px-3" variant="secondary">
{{ validating ? '验证中...' : '一键验证' }}
</Button>
<Button @click="resetAll" class="h-7 text-xs px-3" variant="secondary">
重置
</Button>
</div>
<div class="flex space-x-2">
<Button variant="outline" @click="updateOpen(false)" class="h-7 text-xs px-3">
取消
</Button>
<Button v-if="allValid" @click="saveAndClose" class="h-7 text-xs px-3">
<Button @click="saveAndClose" class="h-7 text-xs px-3">
保存
</Button>
</div>
@@ -0,0 +1,242 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Folder, RefreshCw, File, FileText, Download, Archive, Files } from 'lucide-vue-next'
import { Button } from '@/components/ui/button';
import { getFileDownloadUrl, getAllFilesDownloadUrl, getFiles } from '@/apis/filesApi';
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useToast } from '@/components/ui/toast/use-toast'
import { useRoute } from 'vue-router';
const route = useRoute()
const taskId = route.params.task_id;
const { toast } = useToast()
// 文件列表相关
const fileListVisible = ref(false)
const fileList = ref<any[]>([])
const loadingFiles = ref(false)
const downloadingFile = ref<string | null>(null)
const downloadingAll = ref(false)
const openFolder = async () => {
console.log('openFolder', taskId)
try {
loadingFiles.value = true
const res = await getFiles(taskId as string);
console.log(res);
if (res.data) {
fileList.value = Array.isArray(res.data) ? res.data : [res.data]
fileListVisible.value = true
} else {
toast({
title: "获取文件列表失败",
description: "无法获取工作区文件列表",
variant: "destructive"
})
}
} catch (error) {
console.error('获取文件列表失败:', error)
toast({
title: "错误",
description: "获取文件列表时出现错误",
variant: "destructive"
})
} finally {
loadingFiles.value = false
}
}
// 获取文件图标
const getFileIcon = (fileName: string) => {
const ext = fileName.split('.').pop()?.toLowerCase()
const textExts = ['txt', 'md', 'json', 'csv', 'xml', 'yml', 'yaml']
if (textExts.includes(ext || '')) {
return FileText
}
return File
}
// 获取文件大小格式化
const formatFileSize = (size: number | undefined) => {
if (!size) return ''
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
// 下载单个文件
const downloadSingleFile = async (filename: string) => {
try {
downloadingFile.value = filename
const res = await getFileDownloadUrl(taskId as string, filename)
if (res.data?.download_url) {
// 创建隐藏的链接元素并触发下载
const link = document.createElement('a')
link.href = res.data.download_url
link.download = filename
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
toast({
title: "下载成功",
description: `文件 ${filename} 开始下载`
})
} else {
throw new Error('获取下载链接失败')
}
} catch (error) {
console.error('下载文件失败:', error)
toast({
title: "下载失败",
description: `下载文件 ${filename} 时出现错误`,
variant: "destructive"
})
} finally {
downloadingFile.value = null
}
}
// 下载所有文件
const downloadAll = async () => {
try {
downloadingAll.value = true
const res = await getAllFilesDownloadUrl(taskId as string)
if (res.data?.download_url) {
// 创建隐藏的链接元素并触发下载
const link = document.createElement('a')
link.href = res.data.download_url
link.download = `task_${taskId}_files.zip`
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
toast({
title: "下载成功",
description: "所有文件压缩包开始下载"
})
} else {
throw new Error('获取下载链接失败')
}
} catch (error) {
console.error('下载所有文件失败:', error)
toast({
title: "下载失败",
description: "下载所有文件时出现错误",
variant: "destructive"
})
} finally {
downloadingAll.value = false
}
}
</script>
<template>
<Sheet v-model:open="fileListVisible">
<SheetTrigger asChild>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button @click="openFolder()" :disabled="loadingFiles" class="flex gap-2" size="icon">
<RefreshCw v-if="loadingFiles" class="w-4 h-4 animate-spin" />
<Files v-else class="w-4 h-4" />
<Folder v-else class="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>工作区文件</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SheetTrigger>
<SheetContent side="right" class="w-[400px] sm:w-[540px]">
<SheetHeader>
<SheetTitle class="flex items-center justify-between mr-5">
工作区文件
</SheetTitle>
<SheetDescription>
当前会话的工作区文件列表
<br>
运行的结果和产生在<span class="font-mono">backend/project/work_dir/{{ taskId }}/*</span> 目录下
</SheetDescription>
</SheetHeader>
<div class="mt-6">
<ScrollArea class="h-[calc(100vh-120px)]">
<div v-if="fileList.length === 0" class="text-center py-8 text-gray-500">
暂无文件
</div>
<div v-else class="space-y-2">
<div v-for="(file, index) in fileList" :key="index"
class="flex items-center gap-3 p-3 rounded-lg border hover:bg-gray-50 transition-colors">
<component :is="getFileIcon(file.name || file.filename || '')"
class="w-5 h-5 text-gray-600 flex-shrink-0" />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">
{{ file.name || file.filename || 'Unknown' }}
</div>
<div class="text-xs text-gray-500 flex gap-2">
<span v-if="file.size">{{ formatFileSize(file.size) }}</span>
<span v-if="file.modified_time">{{ new Date(file.modified_time).toLocaleDateString()
}}</span>
<span v-if="file.type">{{ file.type }}</span>
</div>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button @click="downloadSingleFile(file.name || file.filename || '')"
:disabled="downloadingFile === (file.name || file.filename || '')" size="sm" variant="ghost"
class="flex-shrink-0">
<RefreshCw v-if="downloadingFile === (file.name || file.filename || '')"
class="w-4 h-4 animate-spin" />
<Download v-else class="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>下载文件</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</ScrollArea>
</div>
</SheetContent>
</Sheet>
</template>
+3 -14
View File
@@ -18,9 +18,8 @@ import { onMounted, onBeforeUnmount, ref } from 'vue'
import { useTaskStore } from '@/stores/task'
import { getWriterSeque } from '@/apis/commonApi';
import { Button } from '@/components/ui/button';
import { openFolderAPI } from '@/apis/commonApi';
import { useToast } from '@/components/ui/toast/use-toast'
import { Folder } from 'lucide-vue-next'
import FilesSheet from '@/pages/task/components/FileSheet.vue'
const { toast } = useToast()
@@ -69,15 +68,6 @@ onMounted(async () => {
updateDuration() // 立即更新一次
})
const openFolder = async () => {
const res = await openFolderAPI(props.task_id);
console.log(res);
toast({
title: '打开工作目录成功',
description: res.data.message,
})
}
onBeforeUnmount(() => {
taskStore.closeWebSocket()
@@ -125,9 +115,8 @@ onBeforeUnmount(() => {
下载消息
</Button>
<Button @click="openFolder" class="flex">
<Folder class="w-5 h-5" /> workspace
</Button>
<FilesSheet />
</div>
</div>
+27 -13
View File
@@ -9,31 +9,37 @@ export const useApiKeyStore = defineStore('apiKeys', () => {
const coordinatorConfig = ref<ModelConfig>({
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
});
const modelerConfig = ref<ModelConfig>({
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
});
const coderConfig = ref<ModelConfig>({
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
});
const writerConfig = ref<ModelConfig>({
apiKey: '',
baseUrl: '',
modelId: ''
modelId: '',
provider: ''
});
const openalexEmail = ref<string>('');
const isEmpty = computed(() => {
return Object.values(getAllAgentConfigs()).every(config => config.apiKey === '')
})
// 设置协调者模型配置
function setCoordinatorConfig(config: ModelConfig) {
coordinatorConfig.value = { ...config };
@@ -54,6 +60,11 @@ export const useApiKeyStore = defineStore('apiKeys', () => {
writerConfig.value = { ...config };
}
function setOpenalexEmail(email: string) {
console.log('setOpenalexEmail', email)
openalexEmail.value = email;
}
// 获取所有 agent 配置
function getAllAgentConfigs() {
return {
@@ -66,10 +77,11 @@ export const useApiKeyStore = defineStore('apiKeys', () => {
// 重置所有配置
function resetAll() {
coordinatorConfig.value = { apiKey: '', baseUrl: '', modelId: '' };
modelerConfig.value = { apiKey: '', baseUrl: '', modelId: '' };
coderConfig.value = { apiKey: '', baseUrl: '', modelId: '' };
writerConfig.value = { apiKey: '', baseUrl: '', modelId: '' };
coordinatorConfig.value = { apiKey: '', baseUrl: '', modelId: '', provider: '' };
modelerConfig.value = { apiKey: '', baseUrl: '', modelId: '', provider: '' };
coderConfig.value = { apiKey: '', baseUrl: '', modelId: '', provider: '' };
writerConfig.value = { apiKey: '', baseUrl: '', modelId: '', provider: '' };
openalexEmail.value = '';
}
return {
@@ -78,13 +90,15 @@ export const useApiKeyStore = defineStore('apiKeys', () => {
modelerConfig,
coderConfig,
writerConfig,
openalexEmail,
isEmpty,
// 方法
setCoordinatorConfig,
setModelerConfig,
setCoderConfig,
setWriterConfig,
setOpenalexEmail,
getAllAgentConfigs,
resetAll
}
+1
View File
@@ -20,4 +20,5 @@ export interface ModelConfig {
apiKey: string;
baseUrl: string;
modelId: string;
provider: string;
}
+6
View File
@@ -1 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
-24
View File
@@ -1,24 +0,0 @@
#!/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 "推送完成!"