refactor: 重构注释规范、修复类型错误、添加 lint hook 和 CLAUDE.md (#66)

- 后端按 Google Python Style Guide 重构注释,修复 ruff 和 pyright 错误(161→0)
- 前端按 Vue.js Style Guide 重构注释,修复 biome 错误
- 添加 PostToolUse lint hook(ruff + biome 自动检查)
- 更新 CLAUDE.md 覆盖 Commands/Testing/Structure/CodeStyle/GitWorkflow/Boundaries
- 修复 tsconfig.json 废弃的 baseUrl 配置
This commit is contained in:
MuWinds
2026-05-11 23:02:39 +08:00
committed by GitHub
parent c013bdae8a
commit 5305754f0b
247 changed files with 4808 additions and 3720 deletions
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# PostToolUse hook: 根据修改的文件路径运行对应的 lint 检查
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# 读取 stdin JSON,用 sed 提取 file_path(避免 jq 吞掉 Windows 反斜杠)
input=$(cat)
file=$(echo "$input" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
# 将 Windows 反斜杠转为正斜杠,并合并连续斜杠
file=$(echo "$file" | sed 's/\\/\//g' | tr -s '/')
[[ -z "$file" ]] && exit 0
file=$(echo "$file" | xargs)
# 去掉 REPO_ROOT 前缀得到相对路径
# REPO_ROOT 格式:/c/Users/...(Git Bash),文件路径格式:C:/Users/...(Windows)
norm_root="$REPO_ROOT"
if [[ "$norm_root" =~ ^/([a-zA-Z])/(.*) ]]; then
drive="${BASH_REMATCH[1]}"
rest="${BASH_REMATCH[2]}"
drive="${drive^^}"
norm_root="${drive}:/${rest}"
fi
if [[ "$file" == "$norm_root"/* ]]; then
file="${file#$norm_root/}"
elif [[ "$file" == "$norm_root"* ]]; then
file="${file#$norm_root}"
file="${file#/}"
elif [[ "$file" == "$REPO_ROOT"/* ]]; then
file="${file#$REPO_ROOT/}"
elif [[ "$file" == "$REPO_ROOT"* ]]; then
file="${file#$REPO_ROOT}"
file="${file#/}"
fi
run_check() {
local output
local exit_code=0
output=$("$@" 2>&1) || exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "$output" >&2
exit 2
fi
}
case "$file" in
backend/*.py)
cd "$REPO_ROOT/backend" || exit 0
export PYTHONIOENCODING=utf-8
run_check ./.venv/Scripts/python.exe -m ruff check app/
;;
frontend/src/*.vue|frontend/src/*.ts|frontend/src/*/*.vue|frontend/src/*/*.ts|frontend/src/*/*/*.vue|frontend/src/*/*/*.ts|frontend/src/*/*/*/*.vue|frontend/src/*/*/*/*.ts)
cd "$REPO_ROOT/frontend" || exit 0
run_check npx biome check "$file"
;;
esac
exit 0
+30
View File
@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(powershell -Command \"\\(Get-ChildItem -Path 'frontend\\\\src' -Recurse -Filter '*.vue'\\).Count\")",
"Bash(powershell -Command \"\\(Get-ChildItem -Path 'frontend\\\\src' -Recurse -Filter '*.ts'\\).Count\")",
"Bash(powershell *)",
"Bash(.\\\\backend\\\\.venv\\\\Scripts\\\\python.exe -m ruff check backend/app/)",
"Bash(npx biome *)",
"Bash(git checkout *)",
"Bash(git add *)",
"Bash(git commit -m ' *)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"shell": "bash",
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hook_lint.sh\"",
"timeout": 180,
"statusMessage": "Running lint checks..."
}
]
}
]
}
}
+2 -1
View File
@@ -6,4 +6,5 @@ backend/app/.DS_Store
._*
.Spotlight-V100
.Trashes
.idea/
.idea/
settings.local.json
+140 -84
View File
@@ -2,121 +2,177 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
## 项目概述
MathModelAgent is a mathematical modeling automation system designed specifically for math competitions. It automates the entire process of mathematical modeling, code generation, and paper writing to produce competition-ready submissions.
MathModelAgent 是数学建模竞赛自动化系统,通过多 Agent 协作完成建模、代码生成和论文撰写。核心工作流:CoordinatorAgent 分析问题 → ModelerAgent 建模 → CoderAgent 编码执行 → WriterAgent 撰写论文。
## Common Commands
## Commands
### 后端
### Backend Development
```bash
cd backend
# Install dependencies using uv
# 安装依赖
uv sync
# Activate virtual environment
source .venv/bin/activate # macOS/Linux
venv\Scripts\activate.bat # Windows
# Start development server
# 启动开发服务器(需要先启动 Redis)
ENV=DEV uvicorn app.main:app --host 0.0.0.0 --port 8000 --ws-ping-interval 60 --ws-ping-timeout 120 --reload
# Lint and format (using ruff)
ruff check .
ruff format .
# Lint(使用虚拟环境中的 ruff)
.\.venv\Scripts\python.exe -m ruff check app/
.\.venv\Scripts\python.exe -m ruff format app/
# 类型检查
npx pyright app/
```
### Frontend Development
### 前端
```bash
cd frontend
# Install dependencies
# 安装依赖
pnpm i
# Start development server
# 启动开发服务器
pnpm run dev
# Build for production
# 构建
pnpm run build
# Lint
npx biome check src/
npx biome check --write src/ # 自动修复
```
### Docker Deployment
### Docker
```bash
# Start all services
docker-compose up
# Run in background
docker-compose up -d
# Stop services
docker-compose down
docker-compose up -d # 后台启动
docker-compose down # 停止
```
## Architecture Overview
## 项目结构
### Project Structure
- **frontend/**: Vue 3 + TypeScript + Vite web application
- **backend/**: FastAPI-based Python backend with mathematical modeling logic
- **docker-compose.yml**: Container orchestration for development and deployment
```
backend/
app/
core/
agents/ # Agent 实现(继承 Agent 基类)
agent.py # Agent 基类:对话历史、轮次控制、记忆压缩
coordinator_agent.py # 任务分解
modeler_agent.py # 数学建模
coder_agent.py # 代码生成与执行
writer_agent.py # 论文撰写
llm/ # LLM 调用层(LiteLLM 封装)
prompts/ # 各 Agent 的 prompt 模板
flows.py # 编排逻辑(问题拆分、子任务管理)
workflow.py # 工作流主入口
routers/ # FastAPI 路由(REST + WebSocket)
schemas/ # Pydantic 模型(请求/响应/枚举)
services/ # Redis 管理、WebSocket 管理
tools/ # 代码解释器(本地 Jupyter / E2B 云端)
utils/ # 工具函数
config/ # 配置(Pydantic Settings)
### Core Components
frontend/
src/
apis/ # 后端 API 调用封装
components/ # 通用组件 + shadcn-vue UI 库(components/ui/ 不要修改)
pages/ # 页面组件(chat/、task/、login/)
stores/ # Pinia 状态管理
utils/ # 工具函数、类型定义、WebSocket 客户端
```
#### Backend Architecture
- **FastAPI Application**: Async web framework with WebSocket support
- **Multi-Agent System**:
- `ModelerAgent`: Analyzes problems and creates mathematical models
- `CoderAgent`: Generates and executes Python code using Jupyter notebooks
- `WriterAgent`: Composes academic papers from modeling results
- **Code Interpreters**:
- Local Jupyter-based interpreter (saves notebooks for editing)
- Cloud interpreters (E2B, Daytona) for remote execution
- **Task Management**: Redis-based queuing and status tracking
- **LLM Integration**: LiteLLM for multi-model support across different agents
## Code Style
#### Frontend Architecture
- **Vue 3 Composition API** with TypeScript
- **Pinia** for state management
- **Tailwind CSS** for styling
- **WebSocket Client**: Real-time task progress updates
- **Multi-page Application**: Chat interface, task details, and configuration
### 后端(Python)
### Key Workflow
1. User uploads data files and provides problem description
2. System creates modeling task with unique task_id
3. Agents work sequentially:
- Modeler analyzes problem and proposes mathematical approach
- Coder implements solution in Jupyter notebooks
- Writer generates formatted academic paper
4. Real-time progress updates via WebSocket
5. Results saved to `backend/project/work_dir/{task_id}/`
- 模块级、类级、公共方法均使用 Google 风格 docstring(Args/Returns/Raises)
- 类型注解:使用 `str | None` 而非 `Optional[str]`
- 异步:全程 async/await,FastAPI 路由均为 async def
- 注释:中文,解释 WHY 而非 WHAT
### Configuration Files
- **Backend**: `pyproject.toml` for Python dependencies, `.env.dev` for environment
- **Frontend**: `package.json` for Node.js dependencies, `.env.development` for config
- **Docker**: `docker-compose.yml` orchestrates Redis, backend, and frontend services
```python
"""模块级 docstring:描述模块用途。"""
### Development Environment Requirements
- Python 3.12+ (with uv for package management)
- Node.js with pnpm
- Redis server
- Docker (optional, for containerized deployment)
class ExampleAgent:
"""类级 docstring:简述职责。"""
## Important Development Notes
async def run(self, prompt: str, system_prompt: str) -> str:
"""执行任务并返回结果。
### Backend Development
- All new API routes should go in `backend/app/routers/` and be registered in `main.py`
- Core business logic belongs in `backend/app/core/`
- Utility functions should be placed in `backend/app/utils/`
- Use async/await patterns throughout for FastAPI compatibility
- Redis is used for task state management and WebSocket message broadcasting
Args:
prompt: 用户输入。
system_prompt: 系统提示词。
### Frontend Development
- Page components in `frontend/src/pages/` with `index.vue` as entry point
- Reusable components in `frontend/src/components/`
- API calls through `frontend/src/utils/request.ts` (axios wrapper)
- Use Vue 3 Composition API with `<script setup lang="ts">` syntax
- WebSocket connection for real-time task updates
Returns:
处理结果文本。
"""
```
### Testing and Quality
- Backend: Use ruff for linting and formatting (configured in pyproject.toml)
- Frontend: TypeScript compilation and Biome for linting
- Generated files are stored in `backend/project/work_dir/` for inspection
### 前端(Vue 3 + TypeScript)
- SFC 使用 `<script setup lang="ts">`
- 代码按逻辑分组,用注释分隔:`// ---- Props ----`、`// ---- State ----`、`// ---- Computed ----`、`// ---- Methods ----`
- TypeScript 接口和 API 函数使用 JSDoc `/** */` 注释
- UI 库组件(`components/ui/`)为 shadcn-vue 生成代码,不要修改
- 格式:tab 缩进,双引号,Biome 管理 lint 和格式化
```vue
<script setup lang="ts">
import { ref, computed } from "vue";
// ---- Props ----
/** 组件属性 */
interface Props {
/** 消息类型 */
type: "agent" | "user";
/** 消息内容 */
content: string;
}
const props = withDefaults(defineProps<Props>(), { type: "user" });
// ---- Computed ----
const rendered = computed(() => marked.parse(props.content));
</script>
```
## Git Workflow
提交信息格式:`<type>: <描述>`,type 包括:
- `feat`: 新功能
- `fix`: 修复
- `refactor`: 重构
- `chore`: 杂项变更
- `enhance`: 增强
- `docs`: 文档
示例:`feat: 添加 OpenAlex API Key 支持并更新相关配置`
## Boundaries
### 自动化 Lint Hook
每次 Edit/Write 文件后,PostToolUse hook 自动触发:
- `backend/**/*.py` → `ruff check app/`
- `frontend/src/**/*.{vue,ts}` → `biome check <file>`
hook 脚本位于 `.claude/hook_lint.sh`,配置位于 `.claude/settings.json`。
### 不要修改的内容
- `frontend/src/components/ui/` — shadcn-vue 第三方 UI 库组件
- 已有的 `# type: ignore` 注释 — 这些是经过验证的类型抑制,非遗留问题
- `.env` 相关文件中的实际配置值
### 运行环境
- Python 3.12+,包管理用 uv(非 pip)
- Node.js,包管理用 pnpm(版本见 packageManager 字段)
- Redis 必须运行(任务队列和 WebSocket 广播)
- 后端虚拟环境路径:`backend/.venv/`
+23 -8
View File
@@ -1,12 +1,19 @@
from pydantic import AnyUrl, BeforeValidator, computed_field, field_validator, Field
"""应用配置模块,基于 pydantic-settings 管理环境变量和全局配置。"""
from pydantic import BeforeValidator
from pydantic_settings import BaseSettings, SettingsConfigDict
import os
from typing import Annotated, Optional
def parse_cors(value: str) -> list[str]:
"""
Parses the CORS settings from a string to a list of URLs.
"""将 CORS 配置字符串解析为 URL 列表。
Args:
value: 逗号分隔的 URL 字符串,或 "*" 表示允许所有来源。
Returns:
解析后的 URL 列表。
"""
if value == "*":
return ["*"]
@@ -16,7 +23,8 @@ def parse_cors(value: str) -> list[str]:
class Settings(BaseSettings):
ENV: str
"""全局应用配置,从环境变量和 .env 文件加载。"""
ENV: str = "dev"
COORDINATOR_API_KEY: Optional[str] = None
COORDINATOR_MODEL: Optional[str] = None
@@ -38,8 +46,8 @@ class Settings(BaseSettings):
WRITER_BASE_URL: Optional[str] = None
WRITER_MAX_TOKENS: Optional[int] = None
MAX_CHAT_TURNS: int = 150
MAX_RETRIES: int = 5
MAX_CHAT_TURNS: Optional[int] = None
MAX_RETRIES: Optional[int] = None
E2B_API_KEY: Optional[str] = None
LOG_LEVEL: str = "DEBUG"
DEBUG: bool = True
@@ -47,6 +55,8 @@ class Settings(BaseSettings):
REDIS_MAX_CONNECTIONS: int = 10
CORS_ALLOW_ORIGINS: Annotated[list[str] | str, BeforeValidator(parse_cors)] = "*"
SERVER_HOST: str = "http://localhost:8000"
DEEPSEEK_MODEL: Optional[str] = None
DEEPSEEK_BASE_URL: Optional[str] = None
OPENALEX_EMAIL: Optional[str] = None
OPENALEX_API_KEY: Optional[str] = None
@@ -57,10 +67,15 @@ class Settings(BaseSettings):
)
@classmethod
def from_env(cls, env: str = None):
def from_env(cls, env: str | None = None):
"""根据环境名称加载对应配置。
Args:
env: 环境名称(如 dev、prod),默认从 ENV 环境变量获取。
"""
env = env or os.getenv("ENV", "dev")
env_file = f".env.{env.lower()}"
return cls(_env_file=env_file, _env_file_encoding="utf-8")
return cls(_env_file=env_file, _env_file_encoding="utf-8") # type: ignore[call-arg]
settings = Settings()
+18 -8
View File
@@ -1,17 +1,21 @@
"""Agent 基类模块,提供对话管理和记忆压缩功能。"""
from typing import Any
from app.core.llm.llm import LLM, simple_chat
from app.utils.log_util import logger
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
# TODO: Memory 的管理
# TODO: 评估任务完成情况,rethinking
class Agent:
"""Agent 基类,管理对话历史、轮次控制和记忆压缩。"""
def __init__(
self,
task_id: str,
model: LLM,
max_chat_turns: int = 30, # 单个agent最大对话轮次
max_chat_turns: int | None = None, # 单个agent最大对话轮次,None表示无限制
max_memory: int = 12, # 最大记忆轮次
) -> None:
self.task_id = task_id
@@ -21,15 +25,16 @@ class Agent:
self.current_chat_turns = 0 # 当前对话轮次计数器
self.max_memory = max_memory # 最大记忆轮次
async def run(self, prompt: str, system_prompt: str, sub_title: str) -> str:
"""
执行agent的对话并返回结果和总结
async def run(self, prompt: str, system_prompt: str, sub_title: str) -> Any:
"""执行 Agent 对话并返回模型响应。
Args:
prompt: 输入的提示
prompt: 用户输入的提示。
system_prompt: 系统提示词。
sub_title: 子任务标题。
Returns:
str: 模型的响应
模型的响应文本。
"""
try:
logger.info(f"{self.__class__.__name__}:开始:执行对话")
@@ -55,6 +60,11 @@ class Agent:
return error_msg
async def append_chat_history(self, msg: dict) -> None:
"""向对话历史追加消息,并在必要时触发记忆清理。
Args:
msg: 消息字典,需包含 role 和 content 字段。
"""
ic(f"添加消息: role={msg.get('role')}, 当前历史长度={len(self.chat_history)}")
self.chat_history.append(msg)
ic(f"添加后历史长度={len(self.chat_history)}")
@@ -67,7 +77,7 @@ class Agent:
ic("跳过内存清理(tool消息)")
async def clear_memory(self):
"""当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩"""
"""当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩。"""
ic(f"检查内存清理: 当前={len(self.chat_history)}, 最大={self.max_memory}")
if len(self.chat_history) <= self.max_memory:
+23 -11
View File
@@ -1,3 +1,5 @@
"""代码手 Agent 模块,负责生成和执行 Python 代码完成建模任务。"""
from app.core.agents.agent import Agent
from app.config.setting import settings
from app.utils.log_util import logger
@@ -17,16 +19,16 @@ from app.core.functions import coder_tools
# TODO: 引入创新方案:
# 代码强
class CoderAgent(Agent): # 同样继承自Agent类
class CoderAgent(Agent):
"""代码手 Agent,通过 LLM 生成代码并在解释器中执行,支持错误反思和重试。"""
def __init__(
self,
task_id: str,
model: LLM,
work_dir: str, # 工作目录
max_chat_turns: int = settings.MAX_CHAT_TURNS, # 最大聊天次数
max_retries: int = settings.MAX_RETRIES, # 最大反思次数
code_interpreter: BaseCodeInterpreter = None,
max_chat_turns: int | None = settings.MAX_CHAT_TURNS, # 最大聊天次数,None表示无限制
max_retries: int | None = settings.MAX_RETRIES, # 最大反思次数,None表示无限制
code_interpreter: BaseCodeInterpreter | None = None,
) -> None:
super().__init__(task_id, model, max_chat_turns)
self.work_dir = work_dir
@@ -35,8 +37,18 @@ class CoderAgent(Agent): # 同样继承自Agent类
self.system_prompt = CODER_PROMPT
self.code_interpreter = code_interpreter
async def run(self, prompt: str, subtask_title: str) -> CoderToWriter:
async def run(self, prompt: str, subtask_title: str) -> CoderToWriter: # type: ignore[reportIncompatibleMethodOverride]
"""执行代码手子任务,生成并运行代码。
Args:
prompt: 子任务描述。
subtask_title: 子任务标题,用于分段输出。
Returns:
CoderToWriter 对象,包含代码执行结果和生成的图片列表。
"""
logger.info(f"{self.__class__.__name__}:开始:执行子任务: {subtask_title}")
assert self.code_interpreter is not None, "code_interpreter 未初始化"
self.code_interpreter.add_section(subtask_title)
# 如果是第一次运行,则添加系统提示
@@ -62,7 +74,7 @@ class CoderAgent(Agent): # 同样继承自Agent类
last_error_message = ""
while True:
if retry_count >= self.max_retries:
if self.max_retries is not None and retry_count >= self.max_retries:
logger.error(f"超过最大尝试次数: {self.max_retries}")
await redis_manager.publish_message(
self.task_id,
@@ -70,11 +82,11 @@ class CoderAgent(Agent): # 同样继承自Agent类
)
logger.warning(f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}")
return CoderToWriter(
coder_response=f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}",
code_response=f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}",
created_images=[])
if self.current_chat_turns >= self.max_chat_turns:
if self.max_chat_turns is not None and self.current_chat_turns >= self.max_chat_turns:
logger.error(f"超过最大聊天次数: {self.max_chat_turns}")
await redis_manager.publish_message(
self.task_id,
@@ -179,7 +191,7 @@ class CoderAgent(Agent): # 同样继承自Agent类
# 没有工具调用,表示任务完成
logger.info("没有工具调用,任务完成")
return CoderToWriter(
coder_response=response.choices[0].message.content,
code_response=response.choices[0].message.content,
created_images=await self.code_interpreter.get_created_images(
subtask_title
),
+17 -15
View File
@@ -1,3 +1,5 @@
"""协调者 Agent 模块,负责识别用户意图并拆解数学建模问题。"""
from app.core.agents.agent import Agent
from app.core.llm.llm import LLM
from app.core.prompts import COORDINATOR_PROMPT
@@ -8,6 +10,7 @@ from app.schemas.A2A import CoordinatorToModeler
class CoordinatorAgent(Agent):
"""协调者 Agent,判断用户输入是否为数学建模问题并拆解为结构化问题列表。"""
def __init__(
self,
task_id: str,
@@ -17,15 +20,21 @@ class CoordinatorAgent(Agent):
super().__init__(task_id, model, max_chat_turns)
self.system_prompt = COORDINATOR_PROMPT
async def run(self, ques_all: str) -> CoordinatorToModeler:
"""用户输入问题 使用LLM 格式化 questions"""
async def run(self, ques_all: str) -> CoordinatorToModeler: # type: ignore[reportIncompatibleMethodOverride]
"""解析用户输入的问题并格式化为结构化 JSON。
Args:
ques_all: 用户输入的完整题目信息。
Returns:
CoordinatorToModeler 对象,包含结构化问题和问题数量。
"""
await self.append_chat_history(
{"role": "system", "content": self.system_prompt}
)
await self.append_chat_history({"role": "user", "content": ques_all})
max_retries = 3
attempt = 0
while attempt <= max_retries:
while True:
try:
response = await self.model.chat(
history=self.chat_history,
@@ -44,21 +53,14 @@ class CoordinatorAgent(Agent):
ques_count = questions["ques_count"]
logger.info(f"questions:{questions}")
return CoordinatorToModeler(questions=questions, ques_count=ques_count)
except (json.JSONDecodeError, ValueError, KeyError) as e:
attempt += 1
logger.warning(f"解析失败 (尝试 {attempt}/{max_retries}): {str(e)}")
if attempt > max_retries:
logger.error(f"超过最大重试次数,放弃解析")
raise RuntimeError(f"无法解析模型响应: {str(e)}")
logger.warning(f"解析失败 (尝试 {attempt}): {str(e)}")
# 添加错误反馈提示
error_prompt = f"⚠️ 上次响应格式错误: {str(e)}。请严格输出JSON格式"
await self.append_chat_history({
"role": "system",
"role": "system",
"content": self.system_prompt + "\n" + error_prompt
})
# 永远不会执行到这里
raise RuntimeError("意外的流程终止")
+25 -10
View File
@@ -1,3 +1,5 @@
"""建模手 Agent 模块,负责分析问题并制定数学建模方案。"""
from app.core.agents.agent import Agent
from app.core.llm.llm import LLM
from app.core.prompts import MODELER_PROMPT
@@ -5,11 +7,18 @@ from app.schemas.A2A import CoordinatorToModeler, ModelerToCoder
from app.utils.log_util import logger
import json
import re
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
def repair_json(json_str: str) -> dict | None:
"""Try to repair malformed JSON from LLM output."""
"""尝试修复 LLM 输出的格式错误的 JSON。
Args:
json_str: 可能包含格式错误的 JSON 字符串。
Returns:
修复后的字典,无法修复时返回 None。
"""
json_str = json_str.replace("```json", "").replace("```", "").strip()
# Try direct parse first
@@ -43,6 +52,7 @@ def repair_json(json_str: str) -> dict | None:
class ModelerAgent(Agent):
"""建模手 Agent,分析问题类型并制定建模方案、求解方法和可视化策略。"""
def __init__(
self,
task_id: str,
@@ -52,7 +62,15 @@ class ModelerAgent(Agent):
super().__init__(task_id, model, max_chat_turns)
self.system_prompt = MODELER_PROMPT
async def run(self, coordinator_to_modeler: CoordinatorToModeler) -> ModelerToCoder:
async def run(self, coordinator_to_modeler: CoordinatorToModeler) -> ModelerToCoder: # type: ignore[reportIncompatibleMethodOverride]
"""根据协调者拆解的问题生成建模方案。
Args:
coordinator_to_modeler: 协调者传递的结构化问题信息。
Returns:
ModelerToCoder 对象,包含各问题的建模解决方案。
"""
await self.append_chat_history(
{"role": "system", "content": self.system_prompt}
)
@@ -63,8 +81,8 @@ class ModelerAgent(Agent):
}
)
max_parse_retries = 3
for attempt in range(max_parse_retries):
attempt = 0
while True:
response = await self.model.chat(
history=self.chat_history,
agent_name=self.__class__.__name__,
@@ -79,8 +97,9 @@ class ModelerAgent(Agent):
ic(questions_solution)
return ModelerToCoder(questions_solution=questions_solution)
attempt += 1
logger.warning(
f"JSON 解析失败 (第{attempt + 1}次),请求模型重新生成"
f"JSON 解析失败 (第{attempt}次),请求模型重新生成"
)
await self.append_chat_history(
{"role": "assistant", "content": json_str}
@@ -91,7 +110,3 @@ class ModelerAgent(Agent):
"content": "你返回的JSON格式有误,请严格按照JSON格式重新输出,注意字符串值内的双引号必须转义为\\\",不要包含未转义的特殊字符。",
}
)
raise ValueError(
f"经过{max_parse_retries}次尝试仍无法解析JSON,请检查模型输出"
)
+18 -13
View File
@@ -1,3 +1,5 @@
"""写作手 Agent 模块,负责基于建模结果撰写学术论文。"""
from app.core.agents.agent import Agent
from app.core.llm.llm import LLM
from app.core.prompts import get_writer_prompt
@@ -8,23 +10,25 @@ from app.services.redis_manager import redis_manager
from app.schemas.response import SystemMessage, WriterMessage
import json
from app.core.functions import writer_tools
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
from app.schemas.A2A import WriterResponse
# 长文本
# TODO: 并行 parallel
# TODO: 获取当前文件下的文件
# TODO: 引用cites tool
class WriterAgent(Agent): # 同样继承自Agent类
class WriterAgent(Agent):
"""写作手 Agent,基于建模和代码执行结果撰写竞赛论文。"""
def __init__(
self,
task_id: str,
model: LLM,
max_chat_turns: int = 10, # 添加最大对话轮次限制
comp_template: CompTemplate = CompTemplate,
max_chat_turns: int | None = None, # 最大对话轮次,None表示无限制
comp_template: CompTemplate = CompTemplate.CHINA,
format_output: FormatOutPut = FormatOutPut.Markdown,
scholar: OpenAlexScholar = None,
scholar: OpenAlexScholar | None = None,
max_memory: int = 25, # 添加最大记忆轮次
) -> None:
super().__init__(task_id, model, max_chat_turns, max_memory)
@@ -35,11 +39,11 @@ class WriterAgent(Agent): # 同样继承自Agent类
self.system_prompt = get_writer_prompt(format_output)
self.available_images: list[str] = []
async def run(
async def run( # type: ignore[reportIncompatibleMethodOverride]
self,
prompt: str,
available_images: list[str] = None,
sub_title: str = None,
available_images: list[str] | None = None,
sub_title: str | None = None,
) -> WriterResponse:
"""
执行写作任务
@@ -85,6 +89,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
)
footnotes = []
response_content: str = ""
if (
hasattr(response.choices[0].message, "tool_calls")
@@ -105,7 +110,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
await redis_manager.publish_message(
self.task_id,
WriterMessage(
input={"query": query},
content=query,
),
)
@@ -114,6 +119,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
ic(response.choices[0].message.model_dump())
try:
assert self.scholar is not None, "scholar 未初始化"
papers = await self.scholar.search_papers(query)
except Exception as e:
error_msg = f"搜索文献失败: {str(e)}"
@@ -122,6 +128,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
response_content=error_msg, footnotes=footnotes
)
# TODO: pass to frontend
assert self.scholar is not None, "scholar 未初始化"
papers_str = self.scholar.papers_to_str(papers)
logger.info(f"搜索文献结果\n{papers_str}")
await self.append_chat_history(
@@ -147,9 +154,7 @@ class WriterAgent(Agent): # 同样继承自Agent类
return WriterResponse(response_content=response_content, footnotes=footnotes)
async def summarize(self) -> str:
"""
总结对话内容
"""
"""总结对话内容,生成任务执行摘要。"""
try:
await self.append_chat_history(
{"role": "user", "content": "请简单总结以上完成什么任务取得什么结果:"}
+36 -1
View File
@@ -1,14 +1,22 @@
"""工作流程定义模块,管理建模任务的求解和写作流程。"""
from app.models.user_output import UserOutput
from app.tools.base_interpreter import BaseCodeInterpreter
from app.core.agents.modeler_agent import ModelerToCoder
class Flows:
"""管理数学建模任务的求解流程和写作流程。"""
def __init__(self, questions: dict[str, str | int]):
self.flows: dict[str, dict] = {}
self.questions: dict[str, str | int] = questions
def set_flows(self, ques_count: int):
"""根据问题数量设置流程节点。
Args:
ques_count: 问题数量。
"""
ques_str = [f"ques{i}" for i in range(1, ques_count + 1)]
seq = [
"firstPage",
@@ -26,6 +34,15 @@ class Flows:
def get_solution_flows(
self, questions: dict[str, str | int], modeler_response: ModelerToCoder
):
"""生成求解阶段的流程配置。
Args:
questions: 包含各问题描述的字典。
modeler_response: 建模手的响应,包含各问题的解决方案。
Returns:
求解流程配置字典,键为任务名,值包含 coder_prompt 等信息。
"""
questions_quesx = {
key: value
for key, value in questions.items()
@@ -61,6 +78,16 @@ class Flows:
def get_write_flows(
self, user_output: UserOutput, config_template: dict, bg_ques_all: str
):
"""生成写作阶段的流程配置。
Args:
user_output: 用户输出对象,包含已求解的结果。
config_template: 论文模板配置。
bg_ques_all: 问题背景和题目信息。
Returns:
写作流程配置字典,键为章节名,值为写作提示。
"""
model_build_solve = user_output.get_model_build_solve()
flows = {
"firstPage": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["firstPage"]},撰写标题,摘要,关键词""",
@@ -118,7 +145,7 @@ class Flows:
"""获取问题1,2...的键"""
return list(self.get_questions_quesx().keys())
def get_questions_quesx(self) -> dict[str, str]:
def get_questions_quesx(self) -> dict[str, str | int]:
"""获取问题1,2,3...的键值对"""
# 获取所有以 "ques" 开头的键值对
questions_quesx = {
@@ -129,6 +156,14 @@ class Flows:
return questions_quesx
def get_seq(self, ques_count: int) -> dict[str, str]:
"""获取论文章节顺序。
Args:
ques_count: 问题数量。
Returns:
以章节名为键的有序字典。
"""
ques_str = [f"ques{i}" for i in range(1, ques_count + 1)]
seq = [
"firstPage",
+2
View File
@@ -1,3 +1,5 @@
"""工具函数定义模块,为各 Agent 提供可用的工具 schema。"""
coder_tools = [
{
"type": "function",
+59 -36
View File
@@ -1,3 +1,6 @@
"""LLM 交互模块,封装大语言模型的调用、重试和消息发送。"""
from typing import Any
from app.utils.common_utils import transform_link, split_footnotes
from app.utils.log_util import logger
import time
@@ -9,21 +12,22 @@ from app.schemas.response import (
CoordinatorMessage,
)
from app.services.redis_manager import redis_manager
from litellm import acompletion
import litellm
from litellm import acompletion # type: ignore[import-unresolved]
import litellm # type: ignore[import-unresolved]
from app.schemas.enums import AgentType
from app.utils.track import agent_metrics
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
litellm.callbacks = [agent_metrics]
class LLM:
"""大语言模型封装类,提供对话调用、重试和工具调用验证功能。"""
def __init__(
self,
api_key: str,
model: str,
base_url: str,
task_id: str,
api_key: str | None = None,
model: str | None = None,
base_url: str | None = None,
task_id: str = "",
max_tokens: int | None = None,
):
self.api_key = api_key
@@ -33,7 +37,15 @@ class LLM:
self.max_tokens = max_tokens
self.task_id = task_id
def _validate_config(self, agent_name: AgentType) -> None:
def _validate_config(self, agent_name: str) -> None:
"""验证 LLM 配置是否完整。
Args:
agent_name: Agent 类型名称,用于错误提示。
Raises:
ValueError: 模型 ID 或 API Key 未配置时抛出。
"""
if not self.model or not str(self.model).strip():
raise ValueError(f"{agent_name} 未配置模型 ID,请设置对应的 *_MODEL")
if not self.api_key or not str(self.api_key).strip():
@@ -41,15 +53,15 @@ class LLM:
async def chat(
self,
history: list = None,
tools: list = None,
tool_choice: str = None,
max_retries: int = 8, # 添加最大重试次数
history: list | None = None,
tools: list | None = None,
tool_choice: str | None = None,
max_retries: int | None = None, # 最大重试次数,None表示无限制
retry_delay: float = 1.0, # 添加重试延迟
top_p: float | None = None, # 添加top_p参数,
agent_name: AgentType = AgentType.SYSTEM, # CoderAgent or WriterAgent
agent_name: str = "SystemAgent", # CoderAgent or WriterAgent
sub_title: str | None = None,
) -> str:
) -> Any:
logger.info(f"subtitle是:{sub_title}")
self._validate_config(agent_name)
@@ -78,9 +90,9 @@ class LLM:
litellm.enable_json_schema_validation = True #加入json格式验证
# TODO: stream 输出
for attempt in range(max_retries):
attempt = 0
while True:
try:
# completion = self.client.chat.completions.create(**kwargs)
response = await acompletion(**kwargs)
logger.info(f"API返回: {response}")
if not response or not hasattr(response, "choices"):
@@ -89,12 +101,12 @@ class LLM:
await self.send_message(response, agent_name, sub_title)
return response
except Exception as e:
logger.error(f"第{attempt + 1}次重试: {str(e)}")
if attempt < max_retries - 1: # 如果不是最后一次尝试
time.sleep(retry_delay * (attempt + 1)) # 指数退避
continue
logger.debug(f"请求参数: {kwargs}")
raise # 如果所有重试都失败,则抛出异常
attempt += 1
logger.error(f"第{attempt}次重试: {str(e)}")
if max_retries is not None and attempt >= max_retries:
logger.debug(f"请求参数: {kwargs}")
raise
time.sleep(retry_delay * min(attempt, 10)) # 指数退避,上限10秒
def _validate_and_fix_tool_calls(self, history: list) -> list:
"""验证并修复工具调用完整性"""
@@ -154,9 +166,9 @@ class LLM:
cleaned_msg = {k: v for k, v in msg.items() if k != "tool_calls"}
if cleaned_msg.get("content"):
fixed_history.append(cleaned_msg)
ic(f" 🔧 移除所有tool_calls,保留消息内容")
ic(" 🔧 移除所有tool_calls,保留消息内容")
else:
ic(f" 🗑️ 完全移除空的tool_calls消息")
ic(" 🗑️ 完全移除空的tool_calls消息")
# 如果是tool响应消息,检查是否是孤立的
elif isinstance(msg, dict) and msg.get("role") == "tool":
@@ -175,7 +187,7 @@ class LLM:
if found_call:
fixed_history.append(msg)
ic(f" ✅ 保留有效的tool响应")
ic(" ✅ 保留有效的tool响应")
else:
ic(f" 🗑️ 移除孤立的tool响应: {tool_call_id}")
@@ -188,31 +200,43 @@ class LLM:
if len(fixed_history) != len(history):
ic(f"🔧 修复完成: {len(history)} -> {len(fixed_history)} 条消息")
else:
ic(f"✅ 验证通过,无需修复")
ic("✅ 验证通过,无需修复")
return fixed_history
async def send_message(self, response, agent_name, sub_title=None):
"""将 LLM 响应通过 Redis 发送给前端。
Args:
response: LLM 返回的响应对象。
agent_name: Agent 类型。
sub_title: 子任务标题。
"""
logger.info(f"subtitle是:{sub_title}")
content = response.choices[0].message.content
# tool_call 响应的 content 为 None,跳过消息发送
if content is None:
return
agent_msg: Any = None
match agent_name:
case AgentType.CODER:
agent_msg: CoderMessage = CoderMessage(content=content)
agent_msg = CoderMessage(content=content)
case AgentType.WRITER:
# 处理 Markdown 格式的图片语法
content, _ = split_footnotes(content)
content = transform_link(self.task_id, content)
agent_msg: WriterMessage = WriterMessage(
agent_msg = WriterMessage(
content=content,
sub_title=sub_title,
)
case AgentType.MODELER:
agent_msg: ModelerMessage = ModelerMessage(content=content)
agent_msg = ModelerMessage(content=content)
case AgentType.SYSTEM:
agent_msg: SystemMessage = SystemMessage(content=content)
agent_msg = SystemMessage(content=content)
case AgentType.COORDINATOR:
agent_msg: CoordinatorMessage = CoordinatorMessage(content=content)
agent_msg = CoordinatorMessage(content=content)
case _:
raise ValueError(f"不支持的agent类型: {agent_name}")
@@ -235,15 +259,14 @@ class LLM:
async def simple_chat(model: LLM, history: list) -> str:
"""
Description of the function.
"""使用 LLM 进行简单的单轮对话。
Args:
model (LLM): 模型
history (list): 构造好的历史记录(包含system_prompt,user_prompt)
model: LLM 实例。
history: 构造好的历史记录(包含 system_prompt 和 user_prompt)。
Returns:
return_type: Description of the return value.
模型的响应文本。
"""
kwargs = {
"api_key": model.api_key,
+8
View File
@@ -1,14 +1,22 @@
"""LLM 工厂模块,根据配置创建各 Agent 使用的 LLM 实例。"""
from app.config.setting import settings
from app.core.llm.llm import LLM
class LLMFactory:
"""LLM 工厂类,根据配置创建协调者、建模手、代码手和写作手的 LLM 实例。"""
task_id: str
def __init__(self, task_id: str) -> None:
self.task_id = task_id
def get_all_llms(self) -> tuple[LLM, LLM, LLM, LLM]:
"""创建所有 Agent 的 LLM 实例。
Returns:
包含 (coordinator_llm, modeler_llm, coder_llm, writer_llm) 的元组。
"""
coordinator_llm = LLM(
api_key=settings.COORDINATOR_API_KEY,
model=settings.COORDINATOR_MODEL,
+2
View File
@@ -1,3 +1,5 @@
"""代码手 Agent 的系统提示词。"""
import platform
CODER_PROMPT = f"""
+2
View File
@@ -1,3 +1,5 @@
"""协调者 Agent 的系统提示词。"""
FORMAT_QUESTIONS_PROMPT = """
用户将提供给你一段题目信息,**请你不要更改题目信息,完整将用户输入的内容**,以 JSON 的形式输出,输出的 JSON 需遵守以下的格式:
+2
View File
@@ -1,3 +1,5 @@
"""建模手 Agent 的系统提示词。"""
MODELER_PROMPT = """
# Role
你是一名数学建模竞赛经验丰富、善于思考的建模手,负责为每个问题制定建模方案和可视化策略。
+21
View File
@@ -1,4 +1,16 @@
"""共享的提示词工具函数。"""
def get_reflection_prompt(error_message, code) -> str:
"""生成代码错误反思提示词。
Args:
error_message: 错误信息。
code: 出错的代码。
Returns:
反思提示词字符串。
"""
return f"""The code execution encountered an error:
{error_message}
@@ -20,6 +32,15 @@ Please provide an explanation of what went wrong and Remenber call the function
def get_completion_check_prompt(prompt, text_to_gpt) -> str:
"""生成任务完成检查提示词。
Args:
prompt: 原始任务描述。
text_to_gpt: 最新执行结果。
Returns:
完成检查提示词字符串。
"""
return f"""
Please analyze the current state and determine if the task is fully completed:
+10
View File
@@ -1,9 +1,19 @@
"""写作手 Agent 的系统提示词。"""
from app.schemas.enums import FormatOutPut
def get_writer_prompt(
format_output: FormatOutPut = FormatOutPut.Markdown,
):
"""根据输出格式生成写作手的系统提示词。
Args:
format_output: 输出格式(Markdown 或 LaTeX)。
Returns:
写作手系统提示词字符串。
"""
return f"""
# Role
你是一名数学建模竞赛论文写作专家,擅长技术文档撰写和学术文献综合。
+16 -3
View File
@@ -1,3 +1,5 @@
"""工作流模块,编排多 Agent 协作完成数学建模任务。"""
from app.core.agents import WriterAgent, CoderAgent, CoordinatorAgent, ModelerAgent
from app.schemas.request import Problem
from app.schemas.response import SystemMessage
@@ -14,22 +16,31 @@ from app.core.llm.llm_factory import LLMFactory
class WorkFlow:
"""工作流基类。"""
def __init__(self):
pass
def execute(self) -> str:
def execute(self) -> None:
"""执行工作流。"""
# RichPrinter.workflow_start()
# RichPrinter.workflow_end()
pass
class MathModelWorkFlow(WorkFlow):
"""数学建模工作流,协调协调者、建模手、代码手和写作手完成完整建模任务。"""
task_id: str #
work_dir: str # worklow work dir
ques_count: int = 0 # 问题数量
questions: dict[str, str | int] = {} # 问题
async def execute(self, problem: Problem):
async def execute(self, problem: Problem): # type: ignore[reportIncompatibleMethodOverride]
"""执行数学建模工作流。
Args:
problem: 包含题目信息、模板配置等的 Problem 对象。
"""
self.task_id = problem.task_id
self.work_dir = create_work_dir(self.task_id)
@@ -82,6 +93,8 @@ class MathModelWorkFlow(WorkFlow):
timeout=3000,
)
assert settings.OPENALEX_EMAIL is not None, "OPENALEX_EMAIL 未配置"
assert settings.OPENALEX_API_KEY is not None, "OPENALEX_API_KEY 未配置"
scholar = OpenAlexScholar(
task_id=self.task_id,
email=settings.OPENALEX_EMAIL,
@@ -138,7 +151,7 @@ class MathModelWorkFlow(WorkFlow):
)
writer_prompt = flows.get_writer_prompt(
key, coder_response.code_response, code_interpreter, config_template
key, coder_response.code_response or "", code_interpreter, config_template
)
await redis_manager.publish_message(
+2 -1
View File
@@ -1,10 +1,11 @@
"""MathModelAgent 应用入口,配置 FastAPI 应用和中间件。"""
from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi.middleware.cors import CORSMiddleware
import os
from app.routers import modeling_router, ws_router, common_router, files_router
from app.utils.log_util import logger
from app.config.setting import settings
from fastapi.staticfiles import StaticFiles
from app.utils.cli import get_ascii_banner, center_cli_str
+38 -4
View File
@@ -1,3 +1,5 @@
"""用户输出管理模块,负责论文结果的拼接、引用处理和保存。"""
import os
import re
from app.utils.data_recorder import DataRecorder
@@ -7,6 +9,7 @@ import uuid
class UserOutput:
"""管理建模任务的输出结果,处理引用编号、脚注和最终论文拼接。"""
def __init__(
self, work_dir: str, ques_count: int, data_recorder: DataRecorder | None = None
):
@@ -46,16 +49,23 @@ class UserOutput:
]
def set_res(self, key: str, writer_response: WriterResponse):
"""设置指定章节的写作结果。
Args:
key: 章节标识(如 eda、ques1)。
writer_response: 写作手的响应对象。
"""
self.res[key] = {
"response_content": writer_response.response_content,
"footnotes": writer_response.footnotes,
}
def get_res(self):
"""获取所有章节的写作结果。"""
return self.res
def get_model_build_solve(self) -> str:
"""获取模型求解"""
"""获取模型求解结果的摘要字符串。"""
model_build_solve = ",".join(
f"{key}-{value}"
for key, value in self.res.items()
@@ -65,6 +75,14 @@ class UserOutput:
return model_build_solve
def replace_references_with_uuid(self, text: str) -> str:
"""将文本中的引用标记替换为 UUID,用于去重和排序。
Args:
text: 包含引用标记的文本。
Returns:
替换引用为 UUID 后的文本。
"""
# 匹配引用内容,格式为 {[^数字]: 引用内容}
# 修改正则表达式,匹配大括号包裹的引用格式
references = re.findall(r"\{\[\^(\d+)\]:\s*(.*?)\}", text, re.DOTALL)
@@ -104,6 +122,14 @@ class UserOutput:
return text
def sort_text_with_footnotes(self, replace_res: dict) -> dict:
"""按章节顺序排列文本并将 UUID 替换为连续编号。
Args:
replace_res: 已替换 UUID 的结果字典。
Returns:
按顺序编号后的结果字典。
"""
sort_res = {}
ref_index = 1
@@ -124,6 +150,14 @@ class UserOutput:
return sort_res
def append_footnotes_to_text(self, text: str) -> str:
"""在文本末尾追加参考文献列表。
Args:
text: 论文正文。
Returns:
附带参考文献的完整文本。
"""
text += "\n\n ## 参考文献"
# 将脚注转换为列表并按 number 排序
sorted_footnotes = sorted(self.footnotes.items(), key=lambda x: x[1]["number"])
@@ -132,6 +166,7 @@ class UserOutput:
return text
def get_result_to_save(self) -> str:
"""获取最终拼接的论文全文,包含引用处理和参考文献。"""
replace_res = {}
for key, value in self.res.items():
@@ -149,9 +184,8 @@ class UserOutput:
full_res = self.append_footnotes_to_text(full_res_1)
return full_res
def save_result(
self,
):
def save_result(self):
"""将结果保存为 res.json 和 res.md 文件。"""
with open(os.path.join(self.work_dir, "res.json"), "w", encoding="utf-8") as f:
json.dump(self.res, f, ensure_ascii=False, indent=4)
+23 -2
View File
@@ -1,3 +1,5 @@
"""通用路由模块,提供配置查询、消息获取和健康检查等接口。"""
import json
from pathlib import Path
@@ -12,6 +14,17 @@ router = APIRouter()
def _require_safe_task_id(task_id: str) -> str:
"""验证并返回安全的任务 ID。
Args:
task_id: 待验证的任务 ID。
Returns:
验证通过的任务 ID。
Raises:
HTTPException: 任务 ID 非法时返回 400。
"""
try:
return ensure_safe_task_id(task_id)
except ValueError as exc:
@@ -19,6 +32,14 @@ def _require_safe_task_id(task_id: str) -> str:
def _load_task_messages_from_file(task_id: str) -> list[dict]:
"""从文件加载指定任务的历史消息。
Args:
task_id: 任务 ID。
Returns:
消息列表,文件不存在时返回空列表。
"""
safe_task_id = _require_safe_task_id(task_id)
message_file = Path("logs/messages") / f"{safe_task_id}.json"
if not message_file.exists():
@@ -71,7 +92,7 @@ async def track(task_id: str):
@router.get("/status")
async def get_service_status():
"""获取各个服务的状态"""
"""获取后端和 Redis 的运行状态。"""
status = {
"backend": {"status": "running", "message": "Backend service is running"},
"redis": {"status": "unknown", "message": "Redis connection status unknown"}
@@ -80,7 +101,7 @@ async def get_service_status():
# 检查Redis连接状态
try:
redis_client = await redis_manager.get_client()
await redis_client.ping()
await redis_client.ping() # type: ignore[reportGeneralTypeIssues]
status["redis"] = {"status": "running", "message": "Redis connection is healthy"}
except Exception as e:
logger.error(f"Redis connection failed: {str(e)}")
+3 -1
View File
@@ -1,8 +1,10 @@
"""文件管理路由模块,提供文件下载、列表和目录打开等接口。"""
from fastapi import APIRouter
from app.utils.common_utils import get_current_files, get_work_dir
import os
import subprocess
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
from fastapi import HTTPException
router = APIRouter()
+13 -2
View File
@@ -1,3 +1,5 @@
"""建模任务路由模块,提供任务创建、API 验证和配置管理等接口。"""
from fastapi import APIRouter, BackgroundTasks, File, Form, UploadFile
from app.core.workflow import MathModelWorkFlow
from app.schemas.enums import CompTemplate, FormatOutPut
@@ -14,10 +16,10 @@ from app.utils.common_utils import (
import os
import asyncio
from fastapi import HTTPException
from icecream import ic
from icecream import ic # type: ignore[import-unresolved]
from app.schemas.request import ExampleRequest
from pydantic import BaseModel
import litellm
import litellm # type: ignore[import-unresolved]
from app.config.setting import settings
import requests
@@ -201,6 +203,7 @@ async def modeling(
logger.info(f"开始处理上传的文件,工作目录: {work_dir}")
for file in files:
try:
assert file.filename is not None
data_file_path = os.path.join(work_dir, file.filename)
logger.info(f"保存文件: {file.filename} -> {data_file_path}")
@@ -243,6 +246,14 @@ async def run_modeling_task_async(
comp_template: CompTemplate,
format_output: FormatOutPut,
):
"""异步执行建模任务。
Args:
task_id: 任务 ID。
ques_all: 完整题目信息。
comp_template: 竞赛模板类型。
format_output: 输出格式。
"""
logger.info(f"run modeling task for task_id: {task_id}")
problem = Problem(
+3 -1
View File
@@ -1,3 +1,5 @@
"""WebSocket 路由模块,提供实时任务消息推送。"""
import asyncio
import json
@@ -48,7 +50,7 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
# 建立 WebSocket 连接
await ws_manager.connect(websocket)
websocket.timeout = 500
# websocket.timeout 在 Starlette WebSocket 中不可用,已移除
logger.debug(f"WebSocket connection status: {websocket.client}")
# 订阅 Redis 频道
+6
View File
@@ -1,22 +1,28 @@
"""Agent 间通信数据模型定义。"""
from pydantic import BaseModel
from typing import Any
class CoordinatorToModeler(BaseModel):
"""协调者传递给建模手的数据结构。"""
questions: dict
ques_count: int
class ModelerToCoder(BaseModel):
"""建模手传递给代码手的数据结构。"""
questions_solution: dict[str, str]
class CoderToWriter(BaseModel):
"""代码手传递给写作手的数据结构。"""
code_response: str | None = None
code_output: str | None = None
created_images: list[str] | None = None
class WriterResponse(BaseModel):
"""写作手的响应数据结构。"""
response_content: Any
footnotes: list[tuple[str, str]] | None = None
+10 -4
View File
@@ -1,17 +1,22 @@
"""枚举类型定义模块。"""
from enum import Enum
class CompTemplate(str, Enum):
CHINA: str = "CHINA"
AMERICAN: str = "AMERICAN"
"""竞赛模板类型。"""
CHINA = "CHINA"
AMERICAN = "AMERICAN"
class FormatOutPut(str, Enum):
Markdown: str = "Markdown"
LaTeX: str = "LaTeX"
"""输出格式类型。"""
Markdown = "Markdown"
LaTeX = "LaTeX"
class AgentType(str, Enum):
"""Agent 类型标识。"""
COORDINATOR = "CoordinatorAgent"
MODELER = "ModelerAgent"
CODER = "CoderAgent"
@@ -20,6 +25,7 @@ class AgentType(str, Enum):
class AgentStatus(str, Enum):
"""Agent 执行状态。"""
START = "start"
WORKING = "working"
DONE = "done"
+4
View File
@@ -1,13 +1,17 @@
"""请求数据模型定义。"""
from pydantic import BaseModel
from app.schemas.enums import CompTemplate, FormatOutPut
class ExampleRequest(BaseModel):
"""示例建模请求。"""
example_id: str
source: str
class Problem(BaseModel):
"""建模问题描述。"""
task_id: str
ques_all: str = ""
comp_template: CompTemplate = CompTemplate.CHINA
+16 -12
View File
@@ -1,3 +1,5 @@
"""响应数据模型定义,包括消息类型和代码执行结果。"""
from typing import Literal, Union
from app.schemas.enums import AgentType
from pydantic import BaseModel, Field
@@ -5,6 +7,7 @@ from uuid import uuid4
class Message(BaseModel):
"""消息基类。"""
id: str = Field(default_factory=lambda: str(uuid4()))
msg_type: Literal[
"system", "agent", "user", "tool"
@@ -13,23 +16,23 @@ class Message(BaseModel):
class ToolMessage(Message):
msg_type: str = "tool"
msg_type: Literal["system", "agent", "user", "tool"] = "tool"
tool_name: Literal["execute_code", "search_scholar"]
input: dict
output: list
input: dict | None = None
output: list | None = None
class SystemMessage(Message):
msg_type: str = "system"
msg_type: Literal["system", "agent", "user", "tool"] = "system"
type: Literal["info", "warning", "success", "error"] = "info"
class UserMessage(Message):
msg_type: str = "user"
msg_type: Literal["system", "agent", "user", "tool"] = "user"
class AgentMessage(Message):
msg_type: str = "agent"
msg_type: Literal["system", "agent", "user", "tool"] = "agent"
agent_type: AgentType # CoordinatorAgent | ModelerAgent | CoderAgent | WriterAgent
@@ -42,20 +45,21 @@ class CoordinatorMessage(AgentMessage):
class CodeExecution(BaseModel):
"""代码执行结果基类。"""
res_type: Literal["stdout", "stderr", "result", "error"]
msg: str | None = None
class StdOutModel(CodeExecution):
res_type: str = "stdout"
res_type: Literal["stdout", "stderr", "result", "error"] = "stdout"
class StdErrModel(CodeExecution):
res_type: str = "stderr"
res_type: Literal["stdout", "stderr", "result", "error"] = "stderr"
class ResultModel(CodeExecution):
res_type: str = "result"
res_type: Literal["stdout", "stderr", "result", "error"] = "result"
format: Literal[
"text",
"html",
@@ -71,7 +75,7 @@ class ResultModel(CodeExecution):
class ErrorModel(CodeExecution):
res_type: str = "error"
res_type: Literal["stdout", "stderr", "result", "error"] = "error"
name: str
value: str
traceback: str
@@ -82,13 +86,13 @@ OutputItem = Union[StdOutModel, StdErrModel, ResultModel, ErrorModel]
class ScholarMessage(ToolMessage):
tool_name: str = "search_scholar"
tool_name: Literal["execute_code", "search_scholar"] = "search_scholar"
input: dict | None = None # query
output: list[str] | None = None # cites
class InterpreterMessage(ToolMessage):
tool_name: str = "execute_code"
tool_name: Literal["execute_code", "search_scholar"] = "execute_code"
input: dict | None = None # code
output: list[OutputItem] | None = None # code_results
+3
View File
@@ -1,8 +1,11 @@
"""工具调用结果数据模型。"""
from pydantic import BaseModel
from typing import Any, Optional
class ToolResult(BaseModel):
"""工具调用的通用返回结构。"""
success: bool
message: Optional[str] = None
data: Optional[Any] = None
+4 -1
View File
@@ -1,3 +1,5 @@
"""Redis 管理模块,提供消息发布/订阅和持久化存储。"""
import redis.asyncio as aioredis
from typing import Optional
import json
@@ -8,6 +10,7 @@ from app.utils.log_util import logger
class RedisManager:
"""Redis 连接管理器,负责消息发布/订阅和任务消息持久化。"""
def __init__(self):
self.redis_url = settings.REDIS_URL
self._client: Optional[aioredis.Redis] = None
@@ -23,7 +26,7 @@ class RedisManager:
max_connections=settings.REDIS_MAX_CONNECTIONS,
)
try:
await self._client.ping()
await self._client.ping() # type: ignore[reportGeneralTypeIssues]
logger.info(f"Redis 连接建立成功: {self.redis_url}")
return self._client
except Exception as e:
+3
View File
@@ -1,7 +1,10 @@
"""WebSocket 连接管理模块。"""
from fastapi import WebSocket
class WebSocketManager:
"""WebSocket 连接管理器,维护活跃连接并提供消息广播功能。"""
def __init__(self):
self.active_connections: list[WebSocket] = []
+3
View File
@@ -1,3 +1,5 @@
"""配置模板获取测试脚本。"""
import sys
import os
@@ -7,6 +9,7 @@ from app.schemas.enums import CompTemplate
def test_get_config_template():
"""测试获取中国竞赛模板配置。"""
from app.utils.common_utils import get_config_template
comp_template = CompTemplate.CHINA
+5
View File
@@ -1,10 +1,15 @@
"""通用工具函数单元测试。"""
import unittest
from app.utils.common_utils import split_footnotes
class TestCommonUtils(unittest.TestCase):
"""测试 common_utils 模块的核心函数。"""
def test_split_footnotes(self):
"""测试脚注分离功能。"""
text = "Example[^1]\n\n[^1]: Footnote content"
main, notes = split_footnotes(text)
self.assertEqual(main, "Example")
+11 -19
View File
@@ -1,41 +1,33 @@
"""E2B 代码解释器集成测试。"""
import os
import asyncio
import unittest
from dotenv import load_dotenv
from app.tools.e2b_interpreter import E2BCodeInterpreter
from app.tools.notebook_serializer import NotebookSerializer
from app.utils.common_utils import create_work_dir
try:
from dotenv import load_dotenv
except ModuleNotFoundError: # Fallback if python-dotenv is not installed
def load_dotenv(*args, **kwargs):
return None
try:
from app.tools.e2b_interpreter import E2BCodeInterpreter
except ModuleNotFoundError:
E2BCodeInterpreter = None
from app.utils.common_utils import create_task_id, create_work_dir
class TestE2BCodeInterpreter(unittest.TestCase):
def setUp(self):
load_dotenv()
"""E2B 代码解释器测试用例。"""
def setUp(self):
"""初始化测试环境,创建沙箱和代码解释器。"""
if E2BCodeInterpreter is None:
self.skipTest("e2b_code_interpreter not available")
_, dirs = create_work_dir("20250312-104132-d3625cab")
notebook = NotebookSerializer(dirs["jupyter"])
self.task_id = "20250312-104132-d3625cab"
self.work_dir = create_work_dir(self.task_id)
notebook_path = os.path.join(self.work_dir, "jupyter")
notebook = NotebookSerializer(notebook_path)
self.code_interpreter = E2BCodeInterpreter(
self.task_id, self.work_dir, notebook
)
def test_execute_code(self):
"""测试在 E2B 沙箱中执行 Python 代码。"""
if not os.getenv("E2B_API_KEY"):
self.skipTest("E2B_API_KEY not set")
+26 -31
View File
@@ -1,3 +1,5 @@
"""工具基类模块,提供工具注册和调用的基础设施。"""
from typing import Dict, Any, List, Callable
import inspect
from app.schemas.tool_result import ToolResult
@@ -9,16 +11,16 @@ def tool(
parameters: Dict[str, Dict[str, Any]],
required: List[str],
) -> Callable:
"""Tool registration decorator
"""工具注册装饰器,为函数生成工具 schema。
Args:
name: Tool name
description: Tool description
parameters: Tool parameter definitions
required: List of required parameters
name: 工具名称。
description: 工具描述。
parameters: 工具参数定义。
required: 必需参数列表。
Returns:
Decorator function
装饰器函数。
"""
def decorator(func):
@@ -47,66 +49,59 @@ def tool(
class BaseTool:
"""Base tool class, providing common tool calling methods"""
"""工具基类,提供工具注册、查询和调用的通用方法。"""
name: str = ""
def __init__(self):
"""Initialize base tool class"""
pass
self._tools_cache = None
def get_tools(self) -> List[Dict[str, Any]]:
"""Get all registered tools
Returns:
List of tools
"""
"""获取所有已注册的工具 schema 列表。"""
if self._tools_cache is not None:
return self._tools_cache
tools = []
for _, method in inspect.getmembers(self, inspect.ismethod):
if hasattr(method, "_tool_schema"):
tools.append(method._tool_schema)
schema = getattr(method, "_tool_schema", None)
if schema is not None:
tools.append(schema)
self._tools_cache = tools
return tools
def has_function(self, function_name: str) -> bool:
"""Check if specified function exists
"""检查指定名称的工具是否存在。
Args:
function_name: Function name
function_name: 工具函数名称。
Returns:
Whether the tool exists
工具是否存在。
"""
for _, method in inspect.getmembers(self, inspect.ismethod):
if (
hasattr(method, "_function_name")
and method._function_name == function_name
):
fn_name = getattr(method, "_function_name", None)
if fn_name == function_name:
return True
return False
async def invoke_function(self, function_name: str, **kwargs) -> ToolResult:
"""Invoke specified tool
"""调用指定的工具函数。
Args:
function_name: Function name
**kwargs: Parameters
function_name: 工具函数名称。
**kwargs: 传递给工具的参数。
Returns:
Invocation result
工具调用结果。
Raises:
ValueError: Raised when tool doesn't exist
ValueError: 工具不存在时抛出。
"""
for _, method in inspect.getmembers(self, inspect.ismethod):
if (
hasattr(method, "_function_name")
and method._function_name == function_name
):
fn_name = getattr(method, "_function_name", None)
if fn_name == function_name:
return await method(**kwargs)
raise ValueError(f"Tool '{function_name}' not found")
+3 -1
View File
@@ -1,4 +1,5 @@
# base_interpreter.py
"""代码解释器抽象基类模块。"""
import abc
import re
from app.tools.notebook_serializer import NotebookSerializer
@@ -11,6 +12,7 @@ from app.schemas.response import (
class BaseCodeInterpreter(abc.ABC):
"""代码解释器抽象基类,定义代码执行、输出管理和资源清理的接口。"""
def __init__(
self,
task_id: str,
+17 -12
View File
@@ -1,5 +1,7 @@
"""E2B 云端代码解释器模块,通过 E2B 沙箱执行 Python 代码。"""
import os
from e2b_code_interpreter import AsyncSandbox
from e2b_code_interpreter import AsyncSandbox # type: ignore[import-unresolved]
from app.schemas.response import (
ErrorModel,
OutputItem,
@@ -17,6 +19,7 @@ from app.tools.base_interpreter import BaseCodeInterpreter
class E2BCodeInterpreter(BaseCodeInterpreter):
"""基于 E2B 沙箱的云端代码解释器。"""
def __init__(
self,
task_id: str,
@@ -70,6 +73,7 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
with open(file_path, "rb") as f:
content = f.read()
# 使用官方推荐的 files.write 方法
assert self.sbx is not None
await self.sbx.files.write(f"/home/user/{file}", content)
logger.info(f"成功上传文件到沙箱: {file}")
except Exception as e:
@@ -159,20 +163,20 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
# 1. 文本格式
if str(result):
content_to_display.append(
ResultModel(type="result", format="text", msg=str(result))
ResultModel(res_type="result", format="text", msg=str(result))
)
# 2. HTML格式
if result._repr_html_():
content_to_display.append(
ResultModel(
type="result", format="html", msg=result._repr_html_()
res_type="result", format="html", msg=result._repr_html_()
)
)
# 3. Markdown格式
if result._repr_markdown_():
content_to_display.append(
ResultModel(
type="result",
res_type="result",
format="markdown",
msg=result._repr_markdown_(),
)
@@ -181,42 +185,42 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
if result._repr_png_():
content_to_display.append(
ResultModel(
type="result", format="png", msg=result._repr_png_()
res_type="result", format="png", msg=result._repr_png_()
)
)
# 5. JPEG图片
if result._repr_jpeg_():
content_to_display.append(
ResultModel(
type="result", format="jpeg", msg=result._repr_jpeg_()
res_type="result", format="jpeg", msg=result._repr_jpeg_()
)
)
# 6. SVG
if result._repr_svg_():
content_to_display.append(
ResultModel(
type="result", format="svg", msg=result._repr_svg_()
res_type="result", format="svg", msg=result._repr_svg_()
)
)
# 7. PDF
if result._repr_pdf_():
content_to_display.append(
ResultModel(
type="result", format="pdf", msg=result._repr_pdf_()
res_type="result", format="pdf", msg=result._repr_pdf_()
)
)
# 8. LaTeX
if result._repr_latex_():
content_to_display.append(
ResultModel(
type="result", format="latex", msg=result._repr_latex_()
res_type="result", format="latex", msg=result._repr_latex_()
)
)
# 9. JSON
if result._repr_json_():
content_to_display.append(
ResultModel(
type="result",
res_type="result",
format="json",
msg=json.dumps(result._repr_json_()),
)
@@ -225,7 +229,7 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
if result._repr_javascript_():
content_to_display.append(
ResultModel(
type="result",
res_type="result",
format="javascript",
msg=result._repr_javascript_(),
)
@@ -324,9 +328,10 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
async def download_all_files_from_sandbox(self) -> None:
"""从沙箱中下载所有文件并与本地同步"""
try:
assert self.sbx is not None
# 获取沙箱中的文件列表
sandbox_files = await self.sbx.files.list("/home/user")
sandbox_files_dict = {f.name: f for f in sandbox_files}
sandbox_files_dict = {f.name: f for f in sandbox_files} # noqa: F841
# 获取本地文件列表
local_files = set()
+22 -4
View File
@@ -1,5 +1,7 @@
# interpreter_factory.py
"""代码解释器工厂模块,根据配置创建本地或远程解释器。"""
from typing import Literal
from app.tools.base_interpreter import BaseCodeInterpreter
from app.tools.e2b_interpreter import E2BCodeInterpreter
from app.tools.local_interpreter import LocalCodeInterpreter
from app.tools.notebook_serializer import NotebookSerializer
@@ -15,6 +17,21 @@ async def create_interpreter(
notebook_serializer: NotebookSerializer,
timeout=3000,
):
"""创建代码解释器实例。
Args:
kind: 解释器类型,"remote" 使用 E2B 沙箱,"local" 使用本地 Jupyter。
task_id: 任务 ID。
work_dir: 工作目录。
notebook_serializer: Notebook 序列化器。
timeout: 超时时间(秒)。
Returns:
初始化完成的代码解释器实例。
Raises:
ValueError: 未知的解释器类型时抛出。
"""
if not settings.E2B_API_KEY:
logger.info("默认使用本地解释器")
kind = "local"
@@ -22,16 +39,17 @@ async def create_interpreter(
logger.info("使用远程解释器")
kind = "remote"
interp: BaseCodeInterpreter
if kind == "remote":
interp: E2BCodeInterpreter = await E2BCodeInterpreter.create(
interp = await E2BCodeInterpreter.create(
task_id=task_id,
work_dir=work_dir,
notebook_serializer=notebook_serializer,
)
await interp.initialize(timeout=timeout)
await interp.initialize(timeout=timeout) # type: ignore[reportCallIssue]
return interp
elif kind == "local":
interp: LocalCodeInterpreter = LocalCodeInterpreter(
interp = LocalCodeInterpreter(
task_id=task_id,
work_dir=work_dir,
notebook_serializer=notebook_serializer,
+14 -6
View File
@@ -1,3 +1,5 @@
"""本地代码解释器模块,通过本地 Jupyter 内核执行 Python 代码。"""
from app.tools.base_interpreter import BaseCodeInterpreter
from app.tools.notebook_serializer import NotebookSerializer
import jupyter_client
@@ -13,6 +15,7 @@ from app.schemas.response import (
class LocalCodeInterpreter(BaseCodeInterpreter):
"""基于本地 Jupyter 内核的代码解释器。"""
def __init__(
self,
task_id: str,
@@ -32,7 +35,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
)
self._pre_execute_code()
def _pre_execute_code(self):
def _pre_execute_code(self): # type: ignore[reportIncompatibleMethodOverride]
init_code = (
f"import os\n"
f"work_dir = r'{self.work_dir}'\n"
@@ -82,7 +85,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
text_to_gpt.append(self._truncate_text(f"[{mark}]\n{out_str}"))
# 添加text到notebook
content_to_display.append(
ResultModel(type="result", format="text", msg=out_str)
ResultModel(res_type="result", format="text", msg=out_str)
)
self.notebook_serializer.add_code_cell_output_to_notebook(out_str)
@@ -99,14 +102,14 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
if "png" in mark:
self.notebook_serializer.add_image_to_notebook(out_str, "image/png")
content_to_display.append(
ResultModel(type="result", format="png", msg=out_str)
ResultModel(res_type="result", format="png", msg=out_str)
)
else:
self.notebook_serializer.add_image_to_notebook(
out_str, "image/jpeg"
)
content_to_display.append(
ResultModel(type="result", format="jpeg", msg=out_str)
ResultModel(res_type="result", format="jpeg", msg=out_str)
)
elif mark == "error":
@@ -131,7 +134,9 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
)
def execute_code_(self, code) -> list[tuple[str, str]]:
msg_id = self.kc.execute(code)
assert self.kc is not None
assert self.km is not None
self.kc.execute(code)
logger.info(f"执行代码: {code}")
# Get the output of the code
msg_list = []
@@ -144,7 +149,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
and iopub_msg["content"].get("execution_state") == "idle"
):
break
except:
except Exception:
if self.interrupt_signal:
self.km.interrupt_kernel()
self.interrupt_signal = False
@@ -211,6 +216,8 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
async def cleanup(self):
# 关闭内核
assert self.kc is not None
assert self.km is not None
self.kc.shutdown()
logger.info("关闭内核")
self.km.shutdown_kernel()
@@ -220,6 +227,7 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
def restart_jupyter_kernel(self):
"""Restart the Jupyter kernel and recreate the work directory."""
assert self.kc is not None
self.kc.shutdown()
self.km, self.kc = jupyter_client.manager.start_new_kernel(
kernel_name="python3"
+4 -1
View File
@@ -1,10 +1,13 @@
"""Jupyter Notebook 序列化模块,管理 notebook 的创建和内容追加。"""
import nbformat
from nbformat import v4 as nbf
import ansi2html
import ansi2html # type: ignore[import-unresolved]
import os
class NotebookSerializer:
"""Jupyter Notebook 序列化器,负责创建和维护 .ipynb 文件。"""
def __init__(self, work_dir=None, notebook_name="notebook.ipynb"):
self.nb = nbf.new_notebook()
self.notebook_path = None
+23 -13
View File
@@ -1,3 +1,5 @@
"""OpenAlex 学术文献搜索模块。"""
import requests
from typing import List, Dict, Any
from app.services.redis_manager import redis_manager
@@ -5,12 +7,15 @@ from app.schemas.response import ScholarMessage
class OpenAlexScholar:
def __init__(self, task_id: str, email: str = None, api_key: str = None):
"""Initialize OpenAlex client.
"""OpenAlex 学术文献搜索客户端。"""
def __init__(self, task_id: str, email: str | None = None, api_key: str | None = None):
"""初始化 OpenAlex 客户端。
Args:
email: Optional email for better API service
api_key: Optional OpenAlex API key
task_id: 任务 ID。
email: 可选的邮箱地址,用于获取更好的 API 服务。
api_key: 可选的 OpenAlex API Key。
"""
self.base_url = "https://api.openalex.org"
self.email = email
@@ -18,7 +23,11 @@ class OpenAlexScholar:
self.task_id = task_id
def _get_request_url(self, endpoint: str) -> str:
"""Construct request URL with email parameter if provided."""
"""构建请求 URL。
Args:
endpoint: API 端点路径。
"""
if endpoint.startswith("/"):
endpoint = endpoint[1:]
return f"{self.base_url}/{endpoint}"
@@ -52,14 +61,14 @@ class OpenAlexScholar:
return " ".join(words).strip()
async def search_papers(self, query: str, limit: int = 8) -> List[Dict[str, Any]]:
"""Search for papers using OpenAlex API.
"""使用 OpenAlex API 搜索学术论文。
Args:
query: Search query string
limit: Maximum number of results to return
query: 搜索关键词。
limit: 最大返回结果数。
Returns:
List of papers with their details
包含论文详细信息的字典列表。
"""
# 构建基础 URL
base_url = self._get_request_url("works")
@@ -87,6 +96,7 @@ class OpenAlexScholar:
}
# 让 requests 处理参数编码和 URL 构建
response: requests.Response | None = None
try:
print(f"请求 URL: {base_url} 参数: {params}")
response = requests.get(base_url, params=params, headers=headers)
@@ -96,11 +106,11 @@ class OpenAlexScholar:
results = response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP 错误: {e}")
if response.status_code == 403:
if response is not None and response.status_code == 403:
print(
"提示: 403错误通常意味着您需要提供有效的邮箱地址或者遵循礼貌池(polite pool)规则"
)
if hasattr(response, "text"):
if response is not None and hasattr(response, "text"):
print(f"响应内容: {response.text}")
raise
except Exception as e:
@@ -165,7 +175,7 @@ class OpenAlexScholar:
return papers
def papers_to_str(self, papers: List[Dict[str, Any]]) -> str:
"""将文献列表转换为字符串"""
"""将文献列表转换为可读字符串。"""
result = ""
for paper in papers:
result += "\n" + "=" * 80
@@ -181,7 +191,7 @@ class OpenAlexScholar:
return result
def _format_citation(self, work: Dict[str, Any]) -> str:
"""Format citation in a readable format."""
"""将论文数据格式化为引用字符串。"""
# 获取所有作者
authors = [
authorship.get("author", {}).get("display_name")
+3
View File
@@ -1,3 +1,5 @@
"""Rich 终端美化输出模块。"""
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
@@ -8,6 +10,7 @@ from app.utils.log_util import logger
class RichPrinter:
"""Rich 终端美化打印工具,提供面板、表格和 Agent 消息的格式化输出。"""
# 类属性:全局样式配置
_styles = {
"success": {"emoji": "✅", "color": "green", "prefix": "成功"},
+17
View File
@@ -1,6 +1,15 @@
"""CLI 显示工具模块,提供 ASCII 横幅和终端居中显示功能。"""
from textwrap import dedent
def center_cli_str(text: str, width: int | None = None):
"""将多行文本在终端中居中显示。
Args:
text: 待居中的多行文本。
width: 终端宽度,默认自动检测。
"""
import shutil
width = width or shutil.get_terminal_size().columns
@@ -12,6 +21,14 @@ def center_cli_str(text: str, width: int | None = None):
def get_ascii_banner(center: bool = True) -> str:
"""获取项目 ASCII 横幅。
Args:
center: 是否居中显示。
Returns:
ASCII 横幅字符串。
"""
text = dedent(
r"""
===============================================================================
+80 -6
View File
@@ -1,3 +1,5 @@
"""通用工具函数模块,提供任务 ID 生成、文件操作和文档转换等功能。"""
import os
import datetime
import hashlib
@@ -5,15 +7,14 @@ import tomllib
from app.schemas.enums import CompTemplate
from app.utils.log_util import logger
import re
import pypandoc
import pypandoc # type: ignore[import-unresolved]
from app.config.setting import settings
from icecream import ic
TASK_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
def create_task_id() -> str:
"""生成任务ID"""
"""生成基于时间戳和随机哈希的唯一任务 ID。"""
# 生成时间戳和随机hash
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
random_hash = hashlib.md5(str(datetime.datetime.now()).encode()).hexdigest()[:8]
@@ -21,6 +22,17 @@ def create_task_id() -> str:
def ensure_safe_task_id(task_id: str) -> str:
"""验证任务 ID 的合法性,防止路径遍历攻击。
Args:
task_id: 待验证的任务 ID。
Returns:
验证通过的任务 ID。
Raises:
ValueError: 任务 ID 不合法时抛出。
"""
normalized = (task_id or "").strip()
if not normalized or not TASK_ID_PATTERN.fullmatch(normalized):
raise ValueError("非法 task_id")
@@ -28,6 +40,14 @@ def ensure_safe_task_id(task_id: str) -> str:
def create_work_dir(task_id: str) -> str:
"""为指定任务创建工作目录。
Args:
task_id: 任务 ID。
Returns:
工作目录路径。
"""
# 设置主工作目录和子目录
work_dir = os.path.join("project", "work_dir", task_id)
@@ -42,6 +62,17 @@ def create_work_dir(task_id: str) -> str:
def get_work_dir(task_id: str) -> str:
"""获取指定任务的工作目录路径。
Args:
task_id: 任务 ID。
Returns:
工作目录路径。
Raises:
FileNotFoundError: 工作目录不存在时抛出。
"""
work_dir = os.path.join("project", "work_dir", task_id)
if os.path.exists(work_dir):
return work_dir
@@ -50,23 +81,48 @@ def get_work_dir(task_id: str) -> str:
raise FileNotFoundError(f"工作目录不存在: {work_dir}")
# TODO: 是不是应该将 Prompt 写成一个 class
# TODO: 是不是应该将 Prompt 写成一个 class
def get_config_template(comp_template: CompTemplate = CompTemplate.CHINA) -> dict:
"""获取论文模板配置。
Args:
comp_template: 竞赛模板类型。
Returns:
模板配置字典。
"""
if comp_template == CompTemplate.CHINA:
return load_toml(os.path.join("app", "config", "md_template.toml"))
return {}
def load_toml(path: str) -> dict:
"""加载 TOML 配置文件。
Args:
path: TOML 文件路径。
"""
with open(path, "rb") as f:
return tomllib.load(f)
def load_markdown(path: str) -> str:
"""加载 Markdown 文件内容。
Args:
path: Markdown 文件路径。
"""
with open(path, "r", encoding="utf-8") as f:
return f.read()
def get_current_files(folder_path: str, type: str = "all") -> list[str]:
"""获取指定目录下的文件列表。
Args:
folder_path: 目录路径。
type: 文件类型过滤(all/md/ipynb/data/image)。
"""
files = os.listdir(folder_path)
if type == "all":
return files
@@ -82,10 +138,16 @@ def get_current_files(folder_path: str, type: str = "all") -> list[str]:
return [
file for file in files if file.endswith(".png") or file.endswith(".jpg")
]
return []
# 判断content是否包含图片 xx.png,对其处理为 ![filename](http://localhost:8000/static/20250428-200915-ebc154d4/filename.jpg)
def transform_link(task_id: str, content: str):
"""将 Markdown 中的图片链接转换为静态资源 URL。
Args:
task_id: 任务 ID,用于构建 URL 路径。
content: 包含图片链接的 Markdown 文本。
"""
content = re.sub(
r"!\[(.*?)\]\((.*?\.(?:png|jpg|jpeg|gif|bmp|webp))\)",
lambda match: f"![{match.group(1)}]({settings.SERVER_HOST}/static/{task_id}/{match.group(2)})",
@@ -94,8 +156,12 @@ def transform_link(task_id: str, content: str):
return content
# TODO: fix 公式显示
def md_2_docx(task_id: str):
"""将 Markdown 论文转换为 DOCX 格式。
Args:
task_id: 任务 ID。
"""
work_dir = get_work_dir(task_id)
md_path = os.path.join(work_dir, "res.md")
docx_path = os.path.join(work_dir, "res.docx")
@@ -119,6 +185,14 @@ def md_2_docx(task_id: str):
def split_footnotes(text: str) -> tuple[str, list[tuple[str, str]]]:
"""从文本中分离正文和脚注。
Args:
text: 包含脚注的完整文本。
Returns:
(正文, 脚注列表) 的元组,脚注格式为 (编号, 内容)。
"""
main_text = re.sub(
r"\n\[\^\d+\]:.*?(?=\n\[\^|\n\n|\Z)", "", text, flags=re.DOTALL
).strip()
+3 -2
View File
@@ -1,3 +1,5 @@
"""数据记录模块,记录 Agent 的对话历史和 Token 使用情况。"""
import json
import os
from app.utils.log_util import logger
@@ -5,9 +7,8 @@ from typing import Any, Dict
# TODO: 记录数据
# data analysis : save all data and result
# agent-histroy, token usgae, , cost , workflow cost , res
class DataRecorder:
"""记录 Agent 对话历史、Token 使用量和 API 调用费用。"""
def __init__(self, log_work_dir: str = ""):
self.total_cost = 0.0
self.agents_chat_history = {}
+4 -1
View File
@@ -1,10 +1,13 @@
"""日志初始化模块,配置 loguru 日志格式和输出。"""
import os
import sys
import time
from loguru import logger as _logger
from loguru import logger as _logger # type: ignore[import-unresolved]
class LoggerInitializer:
"""日志初始化器,配置控制台和文件日志输出。"""
def __init__(self):
self.log_path = os.path.join(os.getcwd(), "logs")
self.__ensure_log_directory_exists()
+6 -5
View File
@@ -1,20 +1,21 @@
from litellm.integrations.custom_logger import CustomLogger
import litellm
"""LLM 调用指标收集模块。"""
from litellm.integrations.custom_logger import CustomLogger # type: ignore[import-unresolved]
class AgentMetrics(CustomLogger):
#### ASYNC ####
"""LLM 调用指标收集器,记录成功的 API 调用信息。"""
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
try:
# response_cost = kwargs.get("response_cost", 0)
# print("streaming response_cost", response_cost)
print("agent_name", kwargs["litellm_params"]["metadata"]["agent_name"])
except:
except Exception:
pass
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
print(f"On Async Failure")
print("On Async Failure")
# 全局指标收集器实例
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import Toaster from '@/components/ui/toast/Toaster.vue'
import Toaster from "@/components/ui/toast/Toaster.vue";
</script>
<template>
+60 -42
View File
@@ -1,66 +1,84 @@
import request from "@/utils/request";
// 验证 API Key 请求参数
/** 验证 API Key 请求参数 */
export interface ValidateApiKeyRequest {
api_key: string;
base_url?: string;
model_id: string;
api_key: string;
base_url?: string;
model_id: string;
}
// 验证 API Key 响应
/** 验证 API Key 响应 */
export interface ValidateApiKeyResponse {
valid: boolean;
message: string;
valid: boolean;
message: string;
}
// 保存 API 配置请求参数
/** 保存 API 配置请求参数 */
export interface SaveApiConfigRequest {
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: 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;
}
/** 验证 OpenAlex Email 请求参数 */
export interface ValidateOpenalexEmailRequest {
email: string;
email: string;
}
/** 验证 OpenAlex Email 响应 */
export interface ValidateOpenalexEmailResponse {
valid: boolean;
message: string;
valid: boolean;
message: string;
}
// 验证 API Key
/**
* 验证 API Key 是否有效
* @param params 验证请求参数
*/
export function validateApiKey(params: ValidateApiKeyRequest) {
return request.post<ValidateApiKeyResponse>("/validate-api-key", params);
return request.post<ValidateApiKeyResponse>("/validate-api-key", params);
}
/**
* 验证 OpenAlex Email 是否有效
* @param params 验证请求参数
*/
export function validateOpenalexEmail(params: ValidateOpenalexEmailRequest) {
return request.post<ValidateOpenalexEmailResponse>("/validate-openalex-email", params);
return request.post<ValidateOpenalexEmailResponse>(
"/validate-openalex-email",
params,
);
}
// 保存 API 配置
/**
* 保存 API 配置到后端
* @param params API 配置参数
*/
export function saveApiConfig(params: SaveApiConfigRequest) {
return request.post<{ success: boolean; message: string }>("/save-api-config", params);
}
return request.post<{ success: boolean; message: string }>(
"/save-api-config",
params,
);
}
+16 -4
View File
@@ -1,15 +1,20 @@
import request from "@/utils/request";
import type { Message } from "@/utils/response";
/** 健康检查 */
export function getHelloWorld() {
return request.get<{ message: string }>("/");
}
// 获取论文顺序
/** 获取论文写作顺序 */
export function getWriterSeque() {
return request.get<{ writer_seque: string[] }>("/writer_seque");
}
/**
* 获取任务的历史消息
* @param task_id 任务ID
*/
export function getTaskMessages(task_id: string) {
return request.get<Message[]>("/messages", {
params: {
@@ -18,7 +23,10 @@ export function getTaskMessages(task_id: string) {
});
}
/**
* 打开工作目录
* @param task_id 任务ID
*/
export function openFolderAPI(task_id: string) {
return request.get<{ message: string }>("/open_folder", {
params: {
@@ -27,7 +35,11 @@ export function openFolderAPI(task_id: string) {
});
}
/**
* 提交样例任务
* @param example_id 样例ID
* @param source 来源
*/
export function exampleAPI(example_id: string, source: string) {
return request.post<{
task_id: string;
@@ -38,7 +50,7 @@ export function exampleAPI(example_id: string, source: string) {
});
}
// 获取服务状态
/** 获取后端和 Redis 服务状态 */
export function getServiceStatus() {
return request.get<{
backend: { status: string; message: string };
+24 -21
View File
@@ -1,29 +1,32 @@
import request from "@/utils/request";
/**
* 获取任务工作区文件列表
* @param task_id 任务ID
*/
export function getFiles(task_id: string) {
return request.get<{
files: {
filename: string;
file_type: string;
}[]
}>("/files", {
params: { task_id },
});
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,
}
})
return await request.get<{ download_url: string }>("/download_url", {
params: {
task_id,
filename,
},
});
}
/**
@@ -31,9 +34,9 @@ export async function getFileDownloadUrl(task_id: string, filename: string) {
* @param task_id 任务ID
*/
export async function getAllFilesDownloadUrl(task_id: string) {
return await request.get<{ download_url: string }>(`/download_all_url`, {
params: {
task_id,
}
})
}
return await request.get<{ download_url: string }>("/download_all_url", {
params: {
task_id,
},
});
}
+6 -2
View File
@@ -1,6 +1,10 @@
// ... existing code ...
import request from "@/utils/request";
/**
* 提交数学建模任务
* @param problem 问题描述
* @param files 上传的数据文件
*/
export function submitModelingTask(
problem: {
ques_all: string;
@@ -12,7 +16,7 @@ export function submitModelingTask(
const formData = new FormData();
// 添加问题数据
formData.append("ques_all", problem.ques_all);
formData.append("comp_template", 'CHINA');
formData.append("comp_template", "CHINA");
formData.append("format_output", problem.format_output || "Markdown");
if (files) {
+83 -85
View File
@@ -4,92 +4,90 @@
/* ... */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border:220 13% 91%;
--input:220 13% 91%;
--ring:262.1 83.3% 57.8%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background:224 71.4% 4.1%;
--foreground:210 20% 98%;
--card:224 71.4% 4.1%;
--card-foreground:210 20% 98%;
--popover:224 71.4% 4.1%;
--popover-foreground:210 20% 98%;
--primary:263.4 70% 50.4%;
--primary-foreground:210 20% 98%;
--secondary:215 27.9% 16.9%;
--secondary-foreground:210 20% 98%;
--muted:215 27.9% 16.9%;
--muted-foreground:217.9 10.6% 64.9%;
--accent:215 27.9% 16.9%;
--accent-foreground:210 20% 98%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:210 20% 98%;
--border:215 27.9% 16.9%;
--input:215 27.9% 16.9%;
--ring:263.4 70% 50.4%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 263.4 70% 50.4%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import NotebookArea from '@/components/NotebookArea.vue'
import NotebookArea from "@/components/NotebookArea.vue";
</script>
<template>
@@ -1,69 +1,79 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useTaskStore } from '@/stores/task'
import { Separator } from '@/components/ui/separator'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { useTaskStore } from "@/stores/task";
import { computed } from "vue";
const taskStore = useTaskStore()
// ---- Reactive State ----
// 获取最新的CoordinatorMessage
const taskStore = useTaskStore();
// ---- Computed ----
/** 获取最新的协调者消息 */
const latestCoordinatorMessage = computed(() => {
const messages = taskStore.coordinatorMessages
return messages.length > 0 ? messages[messages.length - 1] : null
})
const messages = taskStore.coordinatorMessages;
return messages.length > 0 ? messages[messages.length - 1] : null;
});
// 解析CoordinatorMessage的JSON内容
/** 解析协调者消息的 JSON 内容 */
const coordinatorData = computed(() => {
if (!latestCoordinatorMessage.value?.content) return null
if (!latestCoordinatorMessage.value?.content) return null;
try {
const content = latestCoordinatorMessage.value.content
// 移除可能的```json标记
const cleanContent = content.replace(/```json\n?/, '').replace(/```$/, '').trim()
return JSON.parse(cleanContent)
} catch (error) {
console.error('解析CoordinatorMessage失败:', error)
return null
}
})
try {
const content = latestCoordinatorMessage.value.content;
// 移除可能的```json标记
const cleanContent = content
.replace(/```json\n?/, "")
.replace(/```$/, "")
.trim();
return JSON.parse(cleanContent);
} catch (error) {
console.error("解析CoordinatorMessage失败:", error);
return null;
}
});
// 获取最新的ModelerMessage
/** 获取最新的建模者消息 */
const latestModelerMessage = computed(() => {
const messages = taskStore.modelerMessages
return messages.length > 0 ? messages[messages.length - 1] : null
})
const messages = taskStore.modelerMessages;
return messages.length > 0 ? messages[messages.length - 1] : null;
});
// 解析ModelerMessage的JSON内容
/** 解析建模者消息的 JSON 内容 */
const modelerData = computed(() => {
if (!latestModelerMessage.value?.content) return null
if (!latestModelerMessage.value?.content) return null;
try {
const content = latestModelerMessage.value.content
// 移除可能的```json标记
const cleanContent = content.replace(/```json\n?/, '').replace(/```$/, '').trim()
return JSON.parse(cleanContent)
} catch (error) {
console.error('解析ModelerMessage失败:', error)
return null
}
})
try {
const content = latestModelerMessage.value.content;
// 移除可能的```json标记
const cleanContent = content
.replace(/```json\n?/, "")
.replace(/```$/, "")
.trim();
return JSON.parse(cleanContent);
} catch (error) {
console.error("解析ModelerMessage失败:", error);
return null;
}
});
// 生成问题列表
/** 从协调者数据中提取问题列表 */
const questionsList = computed(() => {
if (!coordinatorData.value) return []
if (!coordinatorData.value) return [];
const questions = []
for (let i = 1; i <= coordinatorData.value.ques_count; i++) {
const quesKey = `ques${i}`
if (coordinatorData.value[quesKey]) {
questions.push({
number: i,
content: coordinatorData.value[quesKey]
})
}
}
return questions
})
const questions = [];
for (let i = 1; i <= coordinatorData.value.ques_count; i++) {
const quesKey = `ques${i}`;
if (coordinatorData.value[quesKey]) {
questions.push({
number: i,
content: coordinatorData.value[quesKey],
});
}
}
return questions;
});
</script>
<template>
@@ -1,64 +1,89 @@
<script setup lang="ts">
import { onMounted, ref, watch, computed } from 'vue';
import { renderMarkdown } from '@/utils/markdown';
import type { WriterMessage } from '@/utils/response'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollArea } from "@/components/ui/scroll-area";
import { renderMarkdown } from "@/utils/markdown";
import type { WriterMessage } from "@/utils/response";
import { computed, onMounted, ref, watch } from "vue";
// ---- Types ----
/** 内容段落数据结构 */
interface ContentSection {
id: number;
content: string;
renderedContent: string;
sub_title?: string;
id: number;
content: string;
renderedContent: string;
sub_title?: string;
}
// ---- Props ----
const props = defineProps<{
messages: WriterMessage[]
writerSequence: string[]
}>()
messages: WriterMessage[];
writerSequence: string[];
}>();
// ---- Reactive State ----
const sections = ref<ContentSection[]>([]);
let nextId = 0;
// 添加新的内容段落
// ---- Methods ----
/** 添加新的内容段落 */
const appendContent = async (content: string, sub_title?: string) => {
const renderedContent = await renderMarkdown(content);
sections.value.push({
id: nextId++,
content,
renderedContent,
sub_title
});
const renderedContent = await renderMarkdown(content);
sections.value.push({
id: nextId++,
content,
renderedContent,
sub_title,
});
};
// 根据 writerSequence 排序内容
// ---- Computed ----
/** 根据 writerSequence 排序内容 */
const sortedSections = computed(() => {
if (!props.writerSequence.length) return sections.value;
if (!props.writerSequence.length) return sections.value;
return [...sections.value].sort((a, b) => {
const aIndex = a.sub_title ? props.writerSequence.indexOf(a.sub_title) : Infinity;
const bIndex = b.sub_title ? props.writerSequence.indexOf(b.sub_title) : Infinity;
return [...sections.value].sort((a, b) => {
const aIndex = a.sub_title
? props.writerSequence.indexOf(a.sub_title)
: Number.POSITIVE_INFINITY;
const bIndex = b.sub_title
? props.writerSequence.indexOf(b.sub_title)
: Number.POSITIVE_INFINITY;
if (aIndex === Infinity && bIndex === Infinity) return 0;
if (aIndex === Infinity) return 1;
if (bIndex === Infinity) return -1;
if (
aIndex === Number.POSITIVE_INFINITY &&
bIndex === Number.POSITIVE_INFINITY
)
return 0;
if (aIndex === Number.POSITIVE_INFINITY) return 1;
if (bIndex === Number.POSITIVE_INFINITY) return -1;
return aIndex - bIndex;
});
return aIndex - bIndex;
});
});
// 监听消息变化
watch(() => props.messages, async (messages) => {
// 清空现有内容
sections.value = [];
nextId = 0;
// ---- Watch ----
// 按顺序添加每个消息的内容
for (const msg of messages) {
if (msg.content) {
await appendContent(msg.content, msg.sub_title);
}
}
}, { immediate: true });
/** 监听消息变化,重新渲染内容 */
watch(
() => props.messages,
async (messages) => {
// 清空现有内容
sections.value = [];
nextId = 0;
// 按顺序添加每个消息的内容
for (const msg of messages) {
if (msg.content) {
await appendContent(msg.content, msg.sub_title);
}
}
},
{ immediate: true },
);
</script>
<template>
+78 -72
View File
@@ -1,83 +1,89 @@
<script setup lang="ts">
import { QQ_GROUP, TWITTER, GITHUB_LINK, BILLBILL, XHS, DISCORD } from '@/utils/const'
import NavUser from './NavUser.vue'
import {
BILLBILL,
DISCORD,
GITHUB_LINK,
QQ_GROUP,
TWITTER,
XHS,
} from "@/utils/const";
import NavUser from "./NavUser.vue";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
type SidebarProps,
SidebarRail,
} from '@/components/ui/sidebar'
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
type SidebarProps,
SidebarRail,
} from "@/components/ui/sidebar";
const props = defineProps<SidebarProps>()
// ---- Props ----
// This is sample data.
const props = defineProps<SidebarProps>();
// ---- Reactive State ----
/** 导航菜单数据 */
const data = {
navMain: [
{
title: '开始',
url: '#',
items: [
{
title: '开始新任务',
url: '#',
isActive: false,
},
],
},
{
title: '历史任务',
url: '#',
items: [
],
},
],
}
navMain: [
{
title: "开始",
url: "#",
items: [
{
title: "开始新任务",
url: "#",
isActive: false,
},
],
},
{
title: "历史任务",
url: "#",
items: [],
},
],
};
const socialMedia = [
{
name: 'QQ',
url: QQ_GROUP,
icon: '/qq.svg',
},
{
name: 'Twitter',
url: TWITTER,
icon: '/twitter.svg',
},
{
name: 'GitHub',
url: GITHUB_LINK,
icon: '/github.svg',
},
{
name: '哔哩哔哩',
url: BILLBILL,
icon: '/bilibili.svg',
},
{
name: '小红书',
url: XHS,
icon: '/xiaohongshu.svg',
},
{
name: 'Discord',
url: DISCORD,
icon: '/discord.svg',
},
]
{
name: "QQ",
url: QQ_GROUP,
icon: "/qq.svg",
},
{
name: "Twitter",
url: TWITTER,
icon: "/twitter.svg",
},
{
name: "GitHub",
url: GITHUB_LINK,
icon: "/github.svg",
},
{
name: "哔哩哔哩",
url: BILLBILL,
icon: "/bilibili.svg",
},
{
name: "小红书",
url: XHS,
icon: "/xiaohongshu.svg",
},
{
name: "Discord",
url: DISCORD,
icon: "/discord.svg",
},
];
</script>
<template>
+17 -13
View File
@@ -1,25 +1,29 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue'
import { marked } from 'marked'
import { computed } from 'vue'
import { AgentType } from '@/utils/enum'
import { cn } from "@/lib/utils";
import type { AgentType } from "@/utils/enum";
import { marked } from "marked";
import type { HTMLAttributes } from "vue";
import { computed } from "vue";
// ---- Props ----
interface BubbleProps {
type: 'agent' | 'user'
agentType?: AgentType
class?: HTMLAttributes['class']
content: string
type: "agent" | "user";
agentType?: AgentType;
class?: HTMLAttributes["class"];
content: string;
}
const props = withDefaults(defineProps<BubbleProps>(), {
type: 'user'
})
type: "user",
});
// ---- Computed ----
/** 渲染 Markdown 内容 */
const renderedContent = computed(() => {
return marked.parse(props.content)
})
return marked.parse(props.content);
});
</script>
<template>
+22 -17
View File
@@ -1,25 +1,30 @@
<script setup lang="ts">
import Bubble from './Bubble.vue'
import SystemMessage from './SystemMessage.vue'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Send } from 'lucide-vue-next'
import type { Message } from '@/utils/response'
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { Message } from "@/utils/response";
import { Send } from "lucide-vue-next";
import { ref } from "vue";
import Bubble from "./Bubble.vue";
import SystemMessage from "./SystemMessage.vue";
const props = defineProps<{ messages: Message[] }>()
// ---- Props ----
const inputValue = ref('')
const inputRef = ref<HTMLInputElement | null>(null)
const scrollRef = ref<HTMLDivElement | null>(null)
const props = defineProps<{ messages: Message[] }>();
// ---- Reactive State ----
const inputValue = ref("");
const inputRef = ref<HTMLInputElement | null>(null);
const scrollRef = ref<HTMLDivElement | null>(null);
// ---- Methods ----
/** 发送消息(本地处理) */
const sendMessage = () => {
// 这里只处理本地 user 消息输入,如需和后端交互请在父组件处理
if (!inputValue.value.trim()) return
// 可以通过 emit 事件让父组件处理 user 消息
inputValue.value = ''
inputRef.value?.focus()
}
if (!inputValue.value.trim()) return;
inputValue.value = "";
inputRef.value?.focus();
};
</script>
<template>
+31 -27
View File
@@ -1,40 +1,44 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ref } from "vue";
// 控制弹窗显示
const showConfirmDialog = ref(false)
let resolvePromise: (value: boolean) => void
// ---- Reactive State ----
// 打开确认弹窗(返回Promise)
/** 控制弹窗显示 */
const showConfirmDialog = ref(false);
let resolvePromise: (value: boolean) => void;
// ---- Methods ----
/** 打开确认弹窗(返回 Promise) */
const openConfirmDialog = () => {
showConfirmDialog.value = true
return new Promise((resolve) => {
resolvePromise = resolve
})
}
showConfirmDialog.value = true;
return new Promise((resolve) => {
resolvePromise = resolve;
});
};
// 处理确认操作
/** 处理确认操作 */
const handleConfirm = () => {
showConfirmDialog.value = false
resolvePromise(true) // 继续执行
}
showConfirmDialog.value = false;
resolvePromise(true); // 继续执行
};
// 处理取消操作
/** 处理取消操作 */
const handleCancel = () => {
showConfirmDialog.value = false
resolvePromise(false) // 返回上传文件
}
showConfirmDialog.value = false;
resolvePromise(false); // 返回上传文件
};
// 暴露方法给父组件
defineExpose({ openConfirmDialog })
/** 暴露方法给父组件 */
defineExpose({ openConfirmDialog });
</script>
<template>
<!-- 确认弹窗 -->
+36 -27
View File
@@ -1,40 +1,49 @@
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import Tree from '@/components/Tree.vue'
import { File } from 'lucide-vue-next'
import { useTaskStore } from '@/stores/task'
import { Sidebar, SidebarContent, SidebarGroup, SidebarFooter, SidebarProvider } from '@/components/ui/sidebar'
const taskStore = useTaskStore()
const isLoading = ref(true)
// 从消息中提取最新的文件列表
const files = taskStore.files as string[]
import Tree from "@/components/Tree.vue";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarProvider,
} from "@/components/ui/sidebar";
import { useTaskStore } from "@/stores/task";
import { File } from "lucide-vue-next";
import { computed, onMounted, ref } from "vue";
// 将文件列表转换为树形结构
// ---- Reactive State ----
const taskStore = useTaskStore();
const isLoading = ref(true);
/** 从消息中提取最新的文件列表 */
const files = taskStore.files as string[];
// ---- Computed ----
/** 将文件列表转换为树形结构 */
const fileTree = computed(() => {
// 无论files是否为空,只要计算属性被触发,就认为数据已加载完成
isLoading.value = false
isLoading.value = false;
return files;
});
// 直接返回文件列表,不做转换,因为Tree组件期望接收string或数组
return files
})
// ---- Lifecycle Hooks ----
// 添加超时机制,确保即使数据没有加载也会在一定时间后显示内容
onMounted(() => {
// 3秒后无论如何都取消加载状态
setTimeout(() => {
isLoading.value = false
}, 3000)
})
setTimeout(() => {
isLoading.value = false;
}, 3000);
});
// ---- Methods ----
const handleFileClick = (file: string) => {
// 处理文件点击
console.log('File clicked:', file)
}
console.log("File clicked:", file);
};
const handleFileDownload = (file: string) => {
// 处理文件下载
console.log('Download file:', file)
}
console.log("Download file:", file);
};
</script>
<template>
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
</script>
<template>
+56 -51
View File
@@ -1,63 +1,68 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { useRouter } from 'vue-router'
import { Button } from "@/components/ui/button";
import { ref } from "vue";
import { useRouter } from "vue-router";
// 导入图片资源
import huashuCupC from '@/assets/example/华数杯2023年C题.png'
import wuyiCupC from '@/assets/example/2025五一杯C题.png'
import mcmCupC from '@/assets/example/2024高教杯C题.png'
import { exampleAPI } from '@/apis/commonApi'
import { exampleAPI } from "@/apis/commonApi";
import mcmCupC from "@/assets/example/2024高教杯C题.png";
import wuyiCupC from "@/assets/example/2025五一杯C题.png";
import huashuCupC from "@/assets/example/华数杯2023年C题.png";
// 定义样例类型
// ---- Types ----
/** 建模样例数据结构 */
interface ModelingExample {
id: number
title: string
source: string
description: string
tags: string[]
problemText: string
image: string
id: number;
title: string;
source: string;
description: string;
tags: string[];
problemText: string;
image: string;
}
const router = useRouter()
// ---- Reactive State ----
const router = useRouter();
/** 样例列表 */
const examples = ref<ModelingExample[]>([
{
id: 1,
title: "母亲身心健康对婴儿成长的影响",
source: "2023华数杯C题",
description: "研究母亲身心健康对婴儿成长的影响",
tags: ["分类问题", "成长", "健康"],
problemText: "给定母亲身心健康数据,建立一个预测模型,预测婴儿成长情况。",
image: huashuCupC
},
{
id: 2,
title: "社交媒体平台用户分析问题",
source: "2025五一杯C题",
description: "分析社交媒体平台用户行为特征",
tags: ["社交媒体", "用户行为"],
problemText: "分析社交媒体平台用户行为特征,构建用户画像模型。",
image: wuyiCupC
},
{
id: 3,
title: "农作物的种植策略",
source: "2024高教杯C题",
description: "研究农作物的种植策略",
tags: ["种植策略", "农作物", "生长"],
problemText: "研究农作物的种植策略,建立一个优化模型,使得农作物产量最大化。",
image: mcmCupC
}
])
{
id: 1,
title: "母亲身心健康对婴儿成长的影响",
source: "2023华数杯C题",
description: "研究母亲身心健康对婴儿成长的影响",
tags: ["分类问题", "成长", "健康"],
problemText: "给定母亲身心健康数据,建立一个预测模型,预测婴儿成长情况。",
image: huashuCupC,
},
{
id: 2,
title: "社交媒体平台用户分析问题",
source: "2025五一杯C题",
description: "分析社交媒体平台用户行为特征",
tags: ["社交媒体", "用户行为"],
problemText: "分析社交媒体平台用户行为特征,构建用户画像模型。",
image: wuyiCupC,
},
{
id: 3,
title: "农作物的种植策略",
source: "2024高教杯C题",
description: "研究农作物的种植策略",
tags: ["种植策略", "农作物", "生长"],
problemText:
"研究农作物的种植策略,建立一个优化模型,使得农作物产量最大化。",
image: mcmCupC,
},
]);
// 选择样例并跳转到任务创建步骤
/** 选择样例并跳转到任务创建步骤 */
const selectExample = async (example: ModelingExample) => {
const res = await exampleAPI(example.id.toString(), example.source)
const task_id = res?.data?.task_id
router.push(`/task/${task_id}`)
}
const res = await exampleAPI(example.id.toString(), example.source);
const task_id = res?.data?.task_id;
router.push(`/task/${task_id}`);
};
</script>
<template>
+41 -41
View File
@@ -1,56 +1,56 @@
<script setup lang="ts">
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/components/ui/avatar'
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import ApiKeyDialog from "@/pages/chat/components/ApiDialog.vue";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
LogOut,
KeyRound,
} from 'lucide-vue-next'
import { ref } from 'vue'
import ApiKeyDialog from '@/pages/chat/components/ApiDialog.vue'
BadgeCheck,
Bell,
ChevronsUpDown,
KeyRound,
LogOut,
} from "lucide-vue-next";
import { ref } from "vue";
// ---- Props ----
const props = defineProps({
user: {
type: Object,
default: () => ({
name: 'San Jin',
email: 'mathmodel@mathmodel.com',
avatar: 'https://github.com/jihe520.png'
})
}
})
user: {
type: Object,
default: () => ({
name: "San Jin",
email: "mathmodel@mathmodel.com",
avatar: "https://github.com/jihe520.png",
}),
},
});
const { isMobile } = useSidebar()
// ---- Reactive State ----
const { isMobile } = useSidebar();
// API Key 对话框控制
const isApiKeyDialogOpen = ref(false)
/** API Key 对话框开关状态 */
const isApiKeyDialogOpen = ref(false);
// ---- Methods ----
const openApiKeyDialog = () => {
isApiKeyDialogOpen.value = true
}
isApiKeyDialogOpen.value = true;
};
</script>
<template>
+34 -31
View File
@@ -1,41 +1,44 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useTaskStore } from '@/stores/task'
import NotebookCell from '@/components/NotebookCell.vue'
import type { NoteCell, CodeCell, ResultCell } from '@/utils/interface'
import NotebookCell from "@/components/NotebookCell.vue";
import { useTaskStore } from "@/stores/task";
import type { CodeCell, NoteCell, ResultCell } from "@/utils/interface";
import { computed } from "vue";
// 使用任务存储
const taskStore = useTaskStore()
console.log('interpreterMessage:', taskStore.interpreterMessage)
// 将代码消息转换为Notebook单元格
// ---- Reactive State ----
const taskStore = useTaskStore();
// ---- Computed ----
/** 将代码执行消息转换为 Notebook 单元格列表 */
const cells = computed<NoteCell[]>(() => {
const notebookCells: NoteCell[] = []
const notebookCells: NoteCell[] = [];
// 获取代码执行工具消息,按顺序处理
for (const toolMsg of taskStore.interpreterMessage) {
console.log('Code execute message:', toolMsg)
// 获取代码执行工具消息,按顺序处理
for (const toolMsg of taskStore.interpreterMessage) {
console.log("Code execute message:", toolMsg);
// 处理代码输入消息
if (toolMsg.input && toolMsg.input.code) {
const codeCell: CodeCell = {
type: 'code',
content: toolMsg.input.code
}
notebookCells.push(codeCell)
}
// 处理代码输入消息
if (toolMsg.input?.code) {
const codeCell: CodeCell = {
type: "code",
content: toolMsg.input.code,
};
notebookCells.push(codeCell);
}
// 处理执行结果消息
if (toolMsg.output && toolMsg.output.length > 0) {
const resultCell: ResultCell = {
type: 'result',
code_results: toolMsg.output
}
notebookCells.push(resultCell)
}
}
// 处理执行结果消息
if (toolMsg.output && toolMsg.output.length > 0) {
const resultCell: ResultCell = {
type: "result",
code_results: toolMsg.output,
};
notebookCells.push(resultCell);
}
}
return notebookCells
})
return notebookCells;
});
</script>
<template>
+50 -43
View File
@@ -1,65 +1,72 @@
<script setup lang="ts">
import type { CodeExecutionResult } from '@/utils/response'
import { renderMarkdown } from '@/utils/markdown'
import type { NoteCell, CodeCell, ResultCell } from '@/utils/interface'
import type { CodeCell, NoteCell, ResultCell } from "@/utils/interface";
import { renderMarkdown } from "@/utils/markdown";
import type { CodeExecutionResult } from "@/utils/response";
// ---- Props ----
defineProps<{
cell: NoteCell
}>()
cell: NoteCell;
}>();
// 获取结果格式的CSS类
// ---- Methods ----
/** 获取结果格式对应的 CSS 类 */
const getResultClass = (result: CodeExecutionResult) => {
switch (result.res_type) {
case 'stdout':
return 'text-gray-600'
case 'stderr':
return 'text-orange-600'
case 'error':
return 'text-red-600'
default:
return 'text-gray-800'
}
}
switch (result.res_type) {
case "stdout":
return "text-gray-600";
case "stderr":
return "text-orange-600";
case "error":
return "text-red-600";
default:
return "text-gray-800";
}
};
// 判断结果是否为图片
/** 判断结果是否为图片格式 */
const isImageResult = (result: CodeExecutionResult) => {
return result.res_type === 'result' &&
['png', 'jpeg', 'svg'].includes(result.format as string)
}
return (
result.res_type === "result" &&
["png", "jpeg", "svg"].includes(result.format as string)
);
};
// 判断结果是否为LaTeX
/** 判断结果是否为 LaTeX 格式 */
const isLatexResult = (result: CodeExecutionResult) => {
return result.res_type === 'result' && result.format === 'latex'
}
return result.res_type === "result" && result.format === "latex";
};
// 判断结果是否为JSON
/** 判断结果是否为 JSON 格式 */
const isJsonResult = (result: CodeExecutionResult) => {
return result.res_type === 'result' && result.format === 'json'
}
return result.res_type === "result" && result.format === "json";
};
// 格式化JSON显示
/** 格式化 JSON 显示 */
const formatJson = (jsonString: string) => {
try {
const parsed = JSON.parse(jsonString)
return JSON.stringify(parsed, null, 2)
} catch (e) {
return jsonString
}
}
try {
const parsed = JSON.parse(jsonString);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return jsonString;
}
};
// 渲染Markdown内容
/** 渲染 Markdown 内容 */
const renderMarkdownContent = (content: string) => {
return renderMarkdown(content)
}
return renderMarkdown(content);
};
// 类型守卫函数,用于区分单元格类型
/** 类型守卫:判断是否为代码单元格 */
const isCodeCell = (cell: NoteCell): cell is CodeCell => {
return cell.type === 'code'
}
return cell.type === "code";
};
/** 类型守卫:判断是否为结果单元格 */
const isResultCell = (cell: NoteCell): cell is ResultCell => {
return cell.type === 'result'
}
return cell.type === "result";
};
</script>
<template>
+6 -6
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { Label } from '@/components/ui/label'
import { Label } from "@/components/ui/label";
import {
SidebarGroup,
SidebarGroupContent,
SidebarInput,
} from '@/components/ui/sidebar'
import { Search } from 'lucide-vue-next'
SidebarGroup,
SidebarGroupContent,
SidebarInput,
} from "@/components/ui/sidebar";
import { Search } from "lucide-vue-next";
</script>
<template>
+78 -66
View File
@@ -16,91 +16,103 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { getServiceStatus } from '@/apis/commonApi'
import { useToast } from '@/components/ui/toast/use-toast'
import { getServiceStatus } from "@/apis/commonApi";
import { useToast } from "@/components/ui/toast/use-toast";
import { onMounted, onUnmounted, ref } from "vue";
// ---- Types ----
/** 单个服务状态 */
interface ServiceStatus {
status: 'running' | 'error' | 'unknown'
message: string
status: "running" | "error" | "unknown";
message: string;
}
/** 所有服务状态 */
interface Services {
backend: ServiceStatus
redis: ServiceStatus
backend: ServiceStatus;
redis: ServiceStatus;
}
const { toast } = useToast()
// ---- Reactive State ----
const { toast } = useToast();
/** 服务状态数据 */
const services = ref<Services>({
backend: { status: 'unknown', message: 'Checking...' },
redis: { status: 'unknown', message: 'Checking...' }
})
backend: { status: "unknown", message: "Checking..." },
redis: { status: "unknown", message: "Checking..." },
});
let statusInterval: number | null = null
let statusInterval: number | null = null;
// ---- Methods ----
/** 获取状态对应的背景和文字样式 */
const getStatusClass = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800'
case 'error':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
switch (status) {
case "running":
return "bg-green-100 text-green-800";
case "error":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
};
/** 获取状态指示点的颜色样式 */
const getStatusDotClass = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-500'
case 'error':
return 'bg-red-500'
default:
return 'bg-gray-400'
}
}
switch (status) {
case "running":
return "bg-green-500";
case "error":
return "bg-red-500";
default:
return "bg-gray-400";
}
};
/** 检查服务状态并处理状态变化 */
const checkStatus = async () => {
try {
const response = await getServiceStatus()
const oldStatus = { ...services.value }
services.value = response.data as Services
try {
const response = await getServiceStatus();
const oldStatus = { ...services.value };
services.value = response.data as Services;
// 检查是否有服务状态变化为错误
Object.keys(response.data).forEach((key) => {
const serviceKey = key as keyof Services
const newStatus = response.data[serviceKey].status
const oldStatusValue = oldStatus[serviceKey].status
// 检查是否有服务状态变化为错误
for (const key of Object.keys(response.data)) {
const serviceKey = key as keyof Services;
const newStatus = response.data[serviceKey].status;
const oldStatusValue = oldStatus[serviceKey].status;
if (newStatus === 'error' && oldStatusValue !== 'error') {
toast({
title: '服务警告',
description: `${serviceKey.toUpperCase()} 服务连接失败: ${response.data[serviceKey].message}`,
variant: 'destructive',
})
}
})
} catch (error) {
console.error('Failed to check service status:', error)
toast({
title: '状态检查失败',
description: '无法获取服务状态,请检查网络连接',
variant: 'destructive',
})
}
}
if (newStatus === "error" && oldStatusValue !== "error") {
toast({
title: "服务警告",
description: `${serviceKey.toUpperCase()} 服务连接失败: ${response.data[serviceKey].message}`,
variant: "destructive",
});
}
}
} catch (error) {
console.error("Failed to check service status:", error);
toast({
title: "状态检查失败",
description: "无法获取服务状态,请检查网络连接",
variant: "destructive",
});
}
};
// ---- Lifecycle Hooks ----
onMounted(() => {
// 立即检查一次
checkStatus()
// 每30秒检查一次状态
statusInterval = setInterval(checkStatus, 30000)
})
checkStatus();
statusInterval = setInterval(checkStatus, 30000);
});
onUnmounted(() => {
if (statusInterval) {
clearInterval(statusInterval)
}
})
if (statusInterval) {
clearInterval(statusInterval);
}
});
</script>
+17 -12
View File
@@ -1,23 +1,28 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
// ---- Props ----
interface SystemMessageProps {
class?: HTMLAttributes['class']
content: string
type?: 'info' | 'warning' | 'success' | 'error'
class?: HTMLAttributes["class"];
content: string;
type?: "info" | "warning" | "success" | "error";
}
const props = withDefaults(defineProps<SystemMessageProps>(), {
type: 'info'
})
type: "info",
});
/** 不同消息类型的样式映射 */
const typeStyles = {
info: 'text-blue-500 dark:text-blue-400 bg-blue-500/5 border-blue-500/10',
warning: 'text-yellow-500 dark:text-yellow-400 bg-yellow-500/5 border-yellow-500/10',
success: 'text-green-500 dark:text-green-400 bg-green-500/5 border-green-500/10',
error: 'text-red-500 dark:text-red-400 bg-red-500/5 border-red-500/10'
}
info: "text-blue-500 dark:text-blue-400 bg-blue-500/5 border-blue-500/10",
warning:
"text-yellow-500 dark:text-yellow-400 bg-yellow-500/5 border-yellow-500/10",
success:
"text-green-500 dark:text-green-400 bg-green-500/5 border-green-500/10",
error: "text-red-500 dark:text-red-400 bg-red-500/5 border-red-500/10",
};
</script>
<template>
+12 -12
View File
@@ -1,19 +1,19 @@
<script setup lang="ts">
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
} from '@/components/ui/sidebar'
import { ChevronRight, File, Folder } from 'lucide-vue-next'
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
} from "@/components/ui/sidebar";
import { ChevronRight, File, Folder } from "lucide-vue-next";
const props = defineProps<{
item: string | any[]
}>()
const [name, ...items] = Array.isArray(props.item) ? props.item : [props.item]
item: string | unknown[];
}>();
const [name, ...items] = Array.isArray(props.item) ? props.item : [props.item];
</script>
<template>
+147 -138
View File
@@ -1,166 +1,175 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { FileUp } from 'lucide-vue-next'
import { Textarea } from '@/components/ui/textarea'
import { saveApiConfig } from "@/apis/apiKeyApi";
import { submitModelingTask } from "@/apis/submitModelingApi";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import FileConfirmDialog from './FileConfirmDialog.vue'
import { submitModelingTask } from '@/apis/submitModelingApi'
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'
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/toast";
import { useApiKeyStore } from "@/stores/apiKeys";
import { useTaskStore } from "@/stores/task";
import { FileUp } from "lucide-vue-next";
import { Rocket } from "lucide-vue-next";
import { ref } from "vue";
import { useRouter } from "vue-router";
import type FileConfirmDialog from "./FileConfirmDialog.vue";
const taskStore = useTaskStore()
const { toast } = useToast()
const apiKeyStore = useApiKeyStore()
const currentStep = ref(1)
const fileConfirmDialog = ref<InstanceType<typeof FileConfirmDialog> | null>(null)
const fileUploaded = ref(true)
// ---- Reactive State ----
// 表单数据
const uploadedFiles = ref<File[]>([])
const question = ref('')
const taskStore = useTaskStore();
const { toast } = useToast();
const apiKeyStore = useApiKeyStore();
const currentStep = ref(1);
const fileConfirmDialog = ref<InstanceType<typeof FileConfirmDialog> | null>(
null,
);
const fileUploaded = ref(true);
/** 已上传的文件列表 */
const uploadedFiles = ref<File[]>([]);
/** 题目内容 */
const question = ref("");
/** 选项配置 */
const selectedOptions = ref({
template: '国赛',
language: '中文',
format: 'Markdown',
})
template: "国赛",
language: "中文",
format: "Markdown",
});
/** 选择器配置列表 */
const selectConfig = [
{
key: '模板',
label: '选择模板',
options: ['国赛', '美赛'],
},
{
key: '语言',
label: '选择语言',
options: ['中文', '英文'],
},
{
key: '格式',
label: '选择格式',
options: ['Markdown', 'LaTeX'],
},
]
{
key: "模板",
label: "选择模板",
options: ["国赛", "美赛"],
},
{
key: "语言",
label: "选择语言",
options: ["中文", "英文"],
},
{
key: "格式",
label: "选择格式",
options: ["Markdown", "LaTeX"],
},
];
// 添加状态控制
const showUploadSuccess = ref(false)
/** 上传成功提示显示状态 */
const showUploadSuccess = ref(false);
// 提交任务
const showSubmitSuccess = ref(false)
/** 提交成功提示显示状态 */
const showSubmitSuccess = ref(false);
const taskId = ref<string | null>(null)
/** 任务ID */
const taskId = ref<string | null>(null);
// 添加 fileInput 的类型声明
const fileInput = ref<HTMLInputElement | null>(null)
/** 文件输入元素引用 */
const fileInput = ref<HTMLInputElement | null>(null);
// ---- Methods ----
const nextStep = () => {
if (currentStep.value < 2)
currentStep.value++
}
if (currentStep.value < 2) currentStep.value++;
};
const prevStep = () => {
if (currentStep.value > 1)
currentStep.value--
}
if (currentStep.value > 1) currentStep.value--;
};
// 修改文件上传处理
/** 处理文件上传事件 */
const handleFileUpload = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files.length > 0) {
uploadedFiles.value = Array.from(input.files)
fileUploaded.value = true
showUploadSuccess.value = true // 显示提示
setTimeout(() => {
showUploadSuccess.value = false // 3秒后自动隐藏
}, 1000)
}
}
const router = useRouter()
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
uploadedFiles.value = Array.from(input.files);
fileUploaded.value = true;
showUploadSuccess.value = true; // 显示提示
setTimeout(() => {
showUploadSuccess.value = false; // 3秒后自动隐藏
}, 1000);
}
};
const router = useRouter();
/** 提交建模任务 */
const handleSubmit = async () => {
try {
try {
if (apiKeyStore.isEmpty) {
toast({
title: "请先配置 API Key",
description: "在侧边栏 -> 头像 -> API Key 中配置 API Key",
variant: "destructive",
});
return;
}
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,
openalex_email: apiKeyStore.openalexEmail,
});
// 保存 API Key
await saveApiConfig({
coordinator: apiKeyStore.coordinatorConfig,
modeler: apiKeyStore.modelerConfig,
coder: apiKeyStore.coderConfig,
writer: apiKeyStore.writerConfig,
openalex_email: apiKeyStore.openalexEmail
})
if (uploadedFiles.value.length === 0) {
if (!fileConfirmDialog.value) return;
if (uploadedFiles.value.length === 0) {
if (!fileConfirmDialog.value) return
const shouldContinue = await fileConfirmDialog.value.openConfirmDialog()
if (!shouldContinue) {
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,
comp_template: selectedOptions.value.template,
format_output: selectedOptions.value.format
},
uploadedFiles.value
)
const shouldContinue = await fileConfirmDialog.value.openConfirmDialog();
taskId.value = response?.data?.task_id ?? null
taskStore.addUserMessage(question.value)
if (!shouldContinue) {
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,
comp_template: selectedOptions.value.template,
format_output: selectedOptions.value.format,
},
uploadedFiles.value,
);
showSubmitSuccess.value = true
setTimeout(() => {
showSubmitSuccess.value = false // 3秒后自动隐藏
}, 3000)
router.push(`/task/${taskId.value}`)
toast({
title: '任务提交成功',
description: '任务提交成功,编号为:' + taskId.value,
})
} catch (error) {
console.error('任务提交失败:', error)
toast({
title: '任务提交失败',
description: '请检查 API Key 是否正确',
variant: 'destructive',
})
}
}
taskId.value = response?.data?.task_id ?? null;
taskStore.addUserMessage(question.value);
showSubmitSuccess.value = true;
setTimeout(() => {
showSubmitSuccess.value = false; // 3秒后自动隐藏
}, 3000);
router.push(`/task/${taskId.value}`);
toast({
title: "任务提交成功",
description: `任务提交成功,编号为:${taskId.value}`,
});
} catch (error) {
console.error("任务提交失败:", error);
toast({
title: "任务提交失败",
description: "请检查 API Key 是否正确",
variant: "destructive",
});
}
};
</script>
<template>
+19 -15
View File
@@ -1,26 +1,30 @@
<script setup lang="ts">
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar'
import { Check, ChevronsUpDown, GalleryVerticalEnd } from 'lucide-vue-next'
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { Check, ChevronsUpDown, GalleryVerticalEnd } from "lucide-vue-next";
import { ref } from 'vue'
import { ref } from "vue";
// ---- Props ----
const props = defineProps<{
versions: string[]
defaultVersion: string
}>()
versions: string[];
defaultVersion: string;
}>();
const selectedVersion = ref(props.defaultVersion)
// ---- Reactive State ----
const selectedVersion = ref(props.defaultVersion);
</script>
<template>
+6 -6
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { type AlertVariants, alertVariants } from '.'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
import { type AlertVariants, alertVariants } from ".";
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: AlertVariants['variant']
}>()
class?: HTMLAttributes["class"];
variant?: AlertVariants["variant"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
+19 -19
View File
@@ -1,23 +1,23 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { type VariantProps, cva } from "class-variance-authority";
export { default as Alert } from './Alert.vue'
export { default as AlertDescription } from './AlertDescription.vue'
export { default as AlertTitle } from './AlertTitle.vue'
export { default as Alert } from "./Alert.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
},
)
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
export type AlertVariants = VariantProps<typeof alertVariants>
export type AlertVariants = VariantProps<typeof alertVariants>;
+15 -12
View File
@@ -1,17 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { AvatarRoot } from 'reka-ui'
import { avatarVariant, type AvatarVariants } from '.'
import { cn } from "@/lib/utils";
import { AvatarRoot } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { type AvatarVariants, avatarVariant } from ".";
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
size?: AvatarVariants['size']
shape?: AvatarVariants['shape']
}>(), {
size: 'sm',
shape: 'circle',
})
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"];
size?: AvatarVariants["size"];
shape?: AvatarVariants["shape"];
}>(),
{
size: "sm",
shape: "circle",
},
);
</script>
<template>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from 'reka-ui'
import { AvatarFallback, type AvatarFallbackProps } from "reka-ui";
const props = defineProps<AvatarFallbackProps>()
const props = defineProps<AvatarFallbackProps>();
</script>
<template>
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { AvatarImageProps } from 'reka-ui'
import { AvatarImage } from 'reka-ui'
import type { AvatarImageProps } from "reka-ui";
import { AvatarImage } from "reka-ui";
const props = defineProps<AvatarImageProps>()
const props = defineProps<AvatarImageProps>();
</script>
<template>
+20 -20
View File
@@ -1,24 +1,24 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { type VariantProps, cva } from "class-variance-authority";
export { default as Avatar } from './Avatar.vue'
export { default as AvatarFallback } from './AvatarFallback.vue'
export { default as AvatarImage } from './AvatarImage.vue'
export { default as Avatar } from "./Avatar.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
size: {
sm: 'h-10 w-10 text-xs',
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
},
shape: {
circle: 'rounded-full',
square: 'rounded-md',
},
},
},
)
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
},
shape: {
circle: "rounded-full",
square: "rounded-md",
},
},
},
);
export type AvatarVariants = VariantProps<typeof avatarVariant>
export type AvatarVariants = VariantProps<typeof avatarVariant>;
@@ -1,9 +1,9 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { MoreHorizontal } from 'lucide-vue-next'
import { cn } from "@/lib/utils";
import { MoreHorizontal } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,11 +1,14 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
as: 'a',
})
const props = withDefaults(
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
{
as: "a",
},
);
</script>
<template>
@@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,7 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue'
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
export { default as BreadcrumbList } from './BreadcrumbList.vue'
export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'
export { default as Breadcrumb } from "./Breadcrumb.vue";
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue";
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue";
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue";
+9 -9
View File
@@ -1,18 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { type ButtonVariants, buttonVariants } from '.'
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { type ButtonVariants, buttonVariants } from ".";
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
as: "button",
});
</script>
<template>
+32 -31
View File
@@ -1,35 +1,36 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { type VariantProps, cva } from "class-variance-authority";
export { default as Button } from './Button.vue'
export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
xs: "h-7 rounded px-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>
export type ButtonVariants = VariantProps<typeof buttonVariants>;
+4 -4
View File
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
+6 -6
View File
@@ -1,6 +1,6 @@
export { default as Card } from './Card.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'
export { default as Card } from "./Card.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
@@ -1,11 +1,11 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui";
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()
const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>();
const forwarded = useForwardPropsEmits(props, emits)
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>

Some files were not shown because too many files have changed in this diff Show More