feat: v2.0.0-test release
Major changes: - Backend: Add report generator service with comprehensive analysis - Backend: Enhanced scan and task endpoints - Frontend: Refactor instant analysis page and export dialog - Frontend: Optimize report export service - Infrastructure: Simplify Dockerfile and update docker-compose - Docs: Streamline README documentation - Assets: Add logo with transparent background
This commit is contained in:
parent
c54212a8c9
commit
9054f0d2c5
175
.env.example
175
.env.example
|
|
@ -1,175 +0,0 @@
|
|||
# ========================================
|
||||
# XCodeReviewer 环境变量配置示例
|
||||
# ========================================
|
||||
# 复制此文件为 .env 并填写你的配置
|
||||
|
||||
# ==================== LLM 通用配置 ====================
|
||||
# 选择你想使用的LLM提供商 (gemini|openai|claude|qwen|deepseek|zhipu|moonshot|baidu|minimax|doubao|ollama)
|
||||
VITE_LLM_PROVIDER=gemini
|
||||
|
||||
# 通用LLM配置 (可选,如果设置了这些,会覆盖下面的特定平台配置)
|
||||
# VITE_LLM_API_KEY=your_api_key_here
|
||||
# VITE_LLM_MODEL=your_model_name
|
||||
# VITE_LLM_BASE_URL=https://your-proxy.com/v1 # API中转站地址(支持所有平台)
|
||||
# VITE_LLM_TIMEOUT=150000
|
||||
# VITE_LLM_TEMPERATURE=0.2
|
||||
# VITE_LLM_MAX_TOKENS=4096
|
||||
# VITE_LLM_CUSTOM_HEADERS={"X-Custom-Header":"value"} # 自定义请求头(JSON格式)
|
||||
|
||||
# ==================== Google Gemini 配置 ====================
|
||||
# 获取API Key: https://makersuite.google.com/app/apikey
|
||||
# 注意:Gemini 现在也支持 API 中转站,只需在上方 VITE_LLM_BASE_URL 中填写中转站地址
|
||||
# VITE_GEMINI_API_KEY=your_gemini_api_key_here
|
||||
# VITE_GEMINI_MODEL=gemini-1.5-flash
|
||||
# VITE_GEMINI_TIMEOUT_MS=150000
|
||||
|
||||
# ==================== OpenAI 配置 ====================
|
||||
# 获取API Key: https://platform.openai.com/api-keys
|
||||
# VITE_OPENAI_API_KEY=your_openai_api_key_here
|
||||
# VITE_OPENAI_MODEL=gpt-4o-mini
|
||||
# VITE_OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# ==================== Anthropic Claude 配置 ====================
|
||||
# 获取API Key: https://console.anthropic.com/
|
||||
# VITE_CLAUDE_API_KEY=your_claude_api_key_here
|
||||
# VITE_CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
|
||||
# ==================== 阿里云通义千问 配置 ====================
|
||||
# 获取API Key: https://dashscope.console.aliyun.com/
|
||||
# VITE_QWEN_API_KEY=your_qwen_api_key_here
|
||||
# VITE_QWEN_MODEL=qwen-turbo
|
||||
|
||||
# ==================== DeepSeek 配置 ====================
|
||||
# 获取API Key: https://platform.deepseek.com/
|
||||
# VITE_DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
# VITE_DEEPSEEK_MODEL=deepseek-chat
|
||||
|
||||
# ==================== 智谱AI (GLM) 配置 ====================
|
||||
# 获取API Key: https://open.bigmodel.cn/
|
||||
# VITE_ZHIPU_API_KEY=your_zhipu_api_key_here
|
||||
# VITE_ZHIPU_MODEL=glm-4-flash
|
||||
|
||||
# ==================== 月之暗面 Kimi 配置 ====================
|
||||
# 获取API Key: https://platform.moonshot.cn/
|
||||
# VITE_MOONSHOT_API_KEY=your_moonshot_api_key_here
|
||||
# VITE_MOONSHOT_MODEL=moonshot-v1-8k
|
||||
|
||||
# ==================== 百度文心一言 配置 ====================
|
||||
# 获取API Key: https://console.bce.baidu.com/qianfan/
|
||||
# 注意:百度API Key格式为 "API_KEY:SECRET_KEY"
|
||||
# VITE_BAIDU_API_KEY=your_api_key:your_secret_key
|
||||
# VITE_BAIDU_MODEL=ERNIE-3.5-8K
|
||||
|
||||
# ==================== MiniMax 配置 ====================
|
||||
# 获取API Key: https://www.minimaxi.com/
|
||||
# VITE_MINIMAX_API_KEY=your_minimax_api_key_here
|
||||
# VITE_MINIMAX_MODEL=abab6.5-chat
|
||||
|
||||
# ==================== 字节豆包 配置 ====================
|
||||
# 获取API Key: https://console.volcengine.com/ark
|
||||
# 注意:豆包使用endpoint ID,需要先创建推理接入点
|
||||
# VITE_DOUBAO_API_KEY=your_doubao_api_key_here
|
||||
# VITE_DOUBAO_MODEL=doubao-pro-32k
|
||||
|
||||
# ==================== Ollama 本地大模型配置 ====================
|
||||
# Ollama 允许在本地运行开源大模型,无需 API Key
|
||||
# 安装: https://ollama.com/
|
||||
# 快速开始:
|
||||
# 1. 安装 Ollama: curl -fsSL https://ollama.com/install.sh | sh
|
||||
# 2. 下载模型: ollama pull llama3
|
||||
# 3. 配置如下并启动应用
|
||||
# VITE_OLLAMA_API_KEY=ollama # 本地运行不需要真实Key,填写任意值
|
||||
# VITE_OLLAMA_MODEL=llama3
|
||||
# VITE_OLLAMA_BASE_URL=http://localhost:11434/v1
|
||||
#
|
||||
# 推荐模型:
|
||||
# - llama3 (综合能力强,适合各种任务)
|
||||
# - codellama (代码专用,适合代码审查)
|
||||
# - qwen2.5:7b (中文支持好)
|
||||
# - deepseek-coder (代码理解能力强)
|
||||
# - phi3:mini (轻量级,速度快)
|
||||
#
|
||||
# 更多模型: https://ollama.com/library
|
||||
|
||||
# ==================== 数据库配置 (推荐使用本地数据库) ====================
|
||||
# 方式1:本地数据库(推荐,开箱即用)
|
||||
VITE_USE_LOCAL_DB=true
|
||||
|
||||
# 方式2:Supabase 云端数据库(支持多设备同步)
|
||||
# 如果不配置,系统将以演示模式运行,数据不会持久化
|
||||
# 获取配置: https://supabase.com/
|
||||
# VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
# VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# ==================== Git 仓库集成配置 (可选) ====================
|
||||
# 用于访问私有仓库进行代码审计
|
||||
|
||||
# GitHub Token
|
||||
# 获取Token: https://github.com/settings/tokens
|
||||
# 权限需求: repo (访问私有仓库)
|
||||
# VITE_GITHUB_TOKEN=ghp_your_github_token_here
|
||||
|
||||
# GitLab Token
|
||||
# 获取Token: https://gitlab.com/-/profile/personal_access_tokens
|
||||
# 权限需求: read_api, read_repository
|
||||
# VITE_GITLAB_TOKEN=glpat-your_gitlab_token_here
|
||||
|
||||
# 💡 提示:
|
||||
# 1. 公开仓库无需配置 Token
|
||||
# 2. 私有仓库或容器内访问需要配置相应的 Token
|
||||
# 3. 支持自建 GitLab 服务器(Token 格式相同)
|
||||
|
||||
# ==================== 应用配置 ====================
|
||||
VITE_APP_ID=xcodereviewer
|
||||
|
||||
# ==================== 代码分析配置 ====================
|
||||
VITE_MAX_ANALYZE_FILES=40
|
||||
VITE_LLM_CONCURRENCY=2
|
||||
VITE_LLM_GAP_MS=500
|
||||
VITE_OUTPUT_LANGUAGE=zh-CN # zh-CN: 中文 | en-US: 英文
|
||||
|
||||
# ========================================
|
||||
# API 中转站使用示例(推荐)
|
||||
# ========================================
|
||||
# 大部分用户使用 API 中转站访问 LLM,以下是常见配置示例:
|
||||
|
||||
# 示例 1:使用硅基流动中转站(OpenAI 兼容格式)
|
||||
# VITE_LLM_PROVIDER=openai
|
||||
# VITE_LLM_API_KEY=sk-你的硅基流动Key
|
||||
# VITE_LLM_MODEL=deepseek-ai/DeepSeek-V3
|
||||
# VITE_LLM_BASE_URL=https://api.siliconflow.cn/v1
|
||||
|
||||
# 示例 2:使用 OpenRouter(支持所有模型)
|
||||
# VITE_LLM_PROVIDER=openai
|
||||
# VITE_LLM_API_KEY=sk-or-你的OpenRouterKey
|
||||
# VITE_LLM_MODEL=anthropic/claude-3.5-sonnet
|
||||
# VITE_LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||
|
||||
# 示例 3:使用 Gemini 中转站(Gemini 格式)
|
||||
# VITE_LLM_PROVIDER=gemini
|
||||
# VITE_LLM_API_KEY=你的中转站Key
|
||||
# VITE_LLM_MODEL=gemini-1.5-flash
|
||||
# VITE_LLM_BASE_URL=https://你的gemini中转站.com/v1beta
|
||||
|
||||
# 示例 4:自建服务 + 自定义请求头
|
||||
# VITE_LLM_PROVIDER=openai
|
||||
# VITE_LLM_API_KEY=your-custom-key
|
||||
# VITE_LLM_MODEL=custom-model
|
||||
# VITE_LLM_BASE_URL=https://your-server.com/v1
|
||||
# VITE_LLM_CUSTOM_HEADERS={"X-API-Version":"v1","X-Team-ID":"team123"}
|
||||
|
||||
# ========================================
|
||||
# 重要提示
|
||||
# ========================================
|
||||
# 1. 推荐使用"运行时配置":无需修改此文件,直接在浏览器中配置
|
||||
# 访问 http://localhost:8888/admin → 系统配置标签页
|
||||
#
|
||||
# 2. API 中转站 URL 格式:
|
||||
# - OpenAI 兼容格式通常以 /v1 结尾
|
||||
# - Gemini 格式通常以 /v1beta 结尾
|
||||
# - Claude 格式通常以 /v1 结尾
|
||||
#
|
||||
# 3. API 格式支持:
|
||||
# - OpenAI 兼容格式(最常见,90%+ 中转站)
|
||||
# - Gemini 格式(Google Gemini 官方及兼容服务)
|
||||
# - Claude 格式(Anthropic Claude 官方及兼容服务)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
node_modules
|
||||
.git
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.log
|
||||
.env.local
|
||||
.env.me
|
||||
backend
|
||||
dist
|
||||
tests
|
||||
history
|
||||
patches
|
||||
160
Dockerfile
160
Dockerfile
|
|
@ -1,164 +1,22 @@
|
|||
# 多阶段构建 - 构建阶段
|
||||
FROM node:18-alpine AS builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 禁用代理并安装 pnpm
|
||||
ENV HTTP_PROXY=""
|
||||
ENV HTTPS_PROXY=""
|
||||
ENV http_proxy=""
|
||||
ENV https_proxy=""
|
||||
ENV NO_PROXY="*"
|
||||
ENV no_proxy="*"
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN npm config set registry https://registry.npmjs.org/ && \
|
||||
npm config delete proxy 2>/dev/null || true && \
|
||||
npm config delete https-proxy 2>/dev/null || true && \
|
||||
npm config delete http-proxy 2>/dev/null || true && \
|
||||
npm install -g pnpm
|
||||
|
||||
# 声明构建参数 - 这些参数可以在 docker build 时传入
|
||||
# LLM 通用配置
|
||||
ARG VITE_LLM_PROVIDER
|
||||
ARG VITE_LLM_API_KEY
|
||||
ARG VITE_LLM_MODEL
|
||||
ARG VITE_LLM_BASE_URL
|
||||
ARG VITE_LLM_TIMEOUT
|
||||
ARG VITE_LLM_TEMPERATURE
|
||||
ARG VITE_LLM_MAX_TOKENS
|
||||
|
||||
# Google Gemini 配置
|
||||
ARG VITE_GEMINI_API_KEY
|
||||
ARG VITE_GEMINI_MODEL
|
||||
ARG VITE_GEMINI_TIMEOUT_MS
|
||||
|
||||
# OpenAI 配置
|
||||
ARG VITE_OPENAI_API_KEY
|
||||
ARG VITE_OPENAI_MODEL
|
||||
ARG VITE_OPENAI_BASE_URL
|
||||
|
||||
# Claude 配置
|
||||
ARG VITE_CLAUDE_API_KEY
|
||||
ARG VITE_CLAUDE_MODEL
|
||||
|
||||
# 通义千问配置
|
||||
ARG VITE_QWEN_API_KEY
|
||||
ARG VITE_QWEN_MODEL
|
||||
|
||||
# DeepSeek 配置
|
||||
ARG VITE_DEEPSEEK_API_KEY
|
||||
ARG VITE_DEEPSEEK_MODEL
|
||||
|
||||
# 智谱AI 配置
|
||||
ARG VITE_ZHIPU_API_KEY
|
||||
ARG VITE_ZHIPU_MODEL
|
||||
|
||||
# Moonshot 配置
|
||||
ARG VITE_MOONSHOT_API_KEY
|
||||
ARG VITE_MOONSHOT_MODEL
|
||||
|
||||
# 百度文心一言配置
|
||||
ARG VITE_BAIDU_API_KEY
|
||||
ARG VITE_BAIDU_MODEL
|
||||
|
||||
# MiniMax 配置
|
||||
ARG VITE_MINIMAX_API_KEY
|
||||
ARG VITE_MINIMAX_MODEL
|
||||
|
||||
# 豆包配置
|
||||
ARG VITE_DOUBAO_API_KEY
|
||||
ARG VITE_DOUBAO_MODEL
|
||||
|
||||
# Ollama 配置
|
||||
ARG VITE_OLLAMA_API_KEY
|
||||
ARG VITE_OLLAMA_MODEL
|
||||
ARG VITE_OLLAMA_BASE_URL
|
||||
|
||||
# Supabase 配置
|
||||
ARG VITE_SUPABASE_URL
|
||||
ARG VITE_SUPABASE_ANON_KEY
|
||||
|
||||
# GitHub 配置
|
||||
ARG VITE_GITHUB_TOKEN
|
||||
|
||||
# 数据库配置
|
||||
ARG VITE_USE_LOCAL_DB
|
||||
|
||||
# 应用配置
|
||||
ARG VITE_APP_ID
|
||||
ARG VITE_MAX_ANALYZE_FILES
|
||||
ARG VITE_LLM_CONCURRENCY
|
||||
ARG VITE_LLM_GAP_MS
|
||||
ARG VITE_OUTPUT_LANGUAGE
|
||||
|
||||
# 将构建参数转换为环境变量(Vite 构建时会读取这些环境变量)
|
||||
ENV VITE_LLM_PROVIDER=$VITE_LLM_PROVIDER
|
||||
ENV VITE_LLM_API_KEY=$VITE_LLM_API_KEY
|
||||
ENV VITE_LLM_MODEL=$VITE_LLM_MODEL
|
||||
ENV VITE_LLM_BASE_URL=$VITE_LLM_BASE_URL
|
||||
ENV VITE_LLM_TIMEOUT=$VITE_LLM_TIMEOUT
|
||||
ENV VITE_LLM_TEMPERATURE=$VITE_LLM_TEMPERATURE
|
||||
ENV VITE_LLM_MAX_TOKENS=$VITE_LLM_MAX_TOKENS
|
||||
|
||||
ENV VITE_GEMINI_API_KEY=$VITE_GEMINI_API_KEY
|
||||
ENV VITE_GEMINI_MODEL=$VITE_GEMINI_MODEL
|
||||
ENV VITE_GEMINI_TIMEOUT_MS=$VITE_GEMINI_TIMEOUT_MS
|
||||
|
||||
ENV VITE_OPENAI_API_KEY=$VITE_OPENAI_API_KEY
|
||||
ENV VITE_OPENAI_MODEL=$VITE_OPENAI_MODEL
|
||||
ENV VITE_OPENAI_BASE_URL=$VITE_OPENAI_BASE_URL
|
||||
|
||||
ENV VITE_CLAUDE_API_KEY=$VITE_CLAUDE_API_KEY
|
||||
ENV VITE_CLAUDE_MODEL=$VITE_CLAUDE_MODEL
|
||||
|
||||
ENV VITE_QWEN_API_KEY=$VITE_QWEN_API_KEY
|
||||
ENV VITE_QWEN_MODEL=$VITE_QWEN_MODEL
|
||||
|
||||
ENV VITE_DEEPSEEK_API_KEY=$VITE_DEEPSEEK_API_KEY
|
||||
ENV VITE_DEEPSEEK_MODEL=$VITE_DEEPSEEK_MODEL
|
||||
|
||||
ENV VITE_ZHIPU_API_KEY=$VITE_ZHIPU_API_KEY
|
||||
ENV VITE_ZHIPU_MODEL=$VITE_ZHIPU_MODEL
|
||||
|
||||
ENV VITE_MOONSHOT_API_KEY=$VITE_MOONSHOT_API_KEY
|
||||
ENV VITE_MOONSHOT_MODEL=$VITE_MOONSHOT_MODEL
|
||||
|
||||
ENV VITE_BAIDU_API_KEY=$VITE_BAIDU_API_KEY
|
||||
ENV VITE_BAIDU_MODEL=$VITE_BAIDU_MODEL
|
||||
|
||||
ENV VITE_MINIMAX_API_KEY=$VITE_MINIMAX_API_KEY
|
||||
ENV VITE_MINIMAX_MODEL=$VITE_MINIMAX_MODEL
|
||||
|
||||
ENV VITE_DOUBAO_API_KEY=$VITE_DOUBAO_API_KEY
|
||||
ENV VITE_DOUBAO_MODEL=$VITE_DOUBAO_MODEL
|
||||
|
||||
ENV VITE_OLLAMA_API_KEY=$VITE_OLLAMA_API_KEY
|
||||
ENV VITE_OLLAMA_MODEL=$VITE_OLLAMA_MODEL
|
||||
ENV VITE_OLLAMA_BASE_URL=$VITE_OLLAMA_BASE_URL
|
||||
|
||||
ENV VITE_USE_LOCAL_DB=$VITE_USE_LOCAL_DB
|
||||
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
|
||||
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
|
||||
|
||||
ENV VITE_GITHUB_TOKEN=$VITE_GITHUB_TOKEN
|
||||
|
||||
ENV VITE_APP_ID=$VITE_APP_ID
|
||||
ENV VITE_MAX_ANALYZE_FILES=$VITE_MAX_ANALYZE_FILES
|
||||
ENV VITE_LLM_CONCURRENCY=$VITE_LLM_CONCURRENCY
|
||||
ENV VITE_LLM_GAP_MS=$VITE_LLM_GAP_MS
|
||||
ENV VITE_OUTPUT_LANGUAGE=$VITE_OUTPUT_LANGUAGE
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
# 复制前端项目文件
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制项目文件(不包括 .env,因为我们使用构建参数)
|
||||
COPY . .
|
||||
# 复制前端源代码
|
||||
COPY frontend/ .
|
||||
|
||||
# 构建应用(环境变量会在构建时被 Vite 读取并硬编码到代码中)
|
||||
# 构建应用
|
||||
RUN pnpm build
|
||||
|
||||
# 生产阶段 - 使用 nginx 提供静态文件服务
|
||||
|
|
|
|||
716
README.md
716
README.md
|
|
@ -4,20 +4,16 @@
|
|||
<img src="public/images/logo.png" alt="XCodeReviewer Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<p>
|
||||
<a href="README.md">中文</a> •
|
||||
<a href="README_EN.md">English</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/releases)
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://vitejs.dev/)
|
||||
[](https://fastapi.tiangolo.com/)
|
||||
[](https://www.python.org/)
|
||||
[](https://deepwiki.com/lintsinghua/XCodeReviewer)
|
||||
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/stargazers)
|
||||
|
|
@ -32,13 +28,7 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
**XCodeReviewer** 是一个由大型语言模型(LLM)驱动的现代化代码审计平台,旨在为开发者提供智能、全面且极具深度的代码质量分析和审查服务。
|
||||
|
||||
#### 🌐 在线演示
|
||||
|
||||
无需部署,直接访问在线演示(数据存储在浏览器本地,支持所有核心功能):
|
||||
|
||||
**[https://xcodereviewer-preview.vercel.app](https://xcodereviewer-preview.vercel.app)**
|
||||
**XCodeReviewer** 是一个由大型语言模型(LLM)驱动的现代化代码审计平台,采用前后端分离架构,旨在为开发者提供智能、全面且极具深度的代码质量分析和审查服务。
|
||||
|
||||
## 🌟 为什么选择 XCodeReviewer?
|
||||
|
||||
|
|
@ -58,6 +48,7 @@
|
|||
- **多维度、全方位评估**:从**安全性**、**性能**、**可维护性**到**代码风格**,提供 360 度无死角的质量评估。
|
||||
- **清晰、可行的修复建议**:独创 **What-Why-How** 模式,不仅告诉您"是什么"问题,还解释"为什么",并提供"如何修复"的具体代码示例。
|
||||
- **多平台LLM/本地LLM支持**: 已实现 10+ 主流平台API调用功能(Gemini、OpenAI、Claude、通义千问、DeepSeek、智谱AI、Kimi、文心一言、MiniMax、豆包、Ollama本地大模型),支持用户自由配置和切换。
|
||||
- **前后端分离架构**:采用 React + FastAPI 现代化架构,支持独立部署和扩展,后端使用 LiteLLM 统一适配多种 LLM 平台。
|
||||
- **可视化运行时配置**:无需重新构建镜像,直接在浏览器中配置所有 LLM 参数和 API Keys,支持 API 中转站,配置保存在本地浏览器,安全便捷。
|
||||
- **现代化、高颜值的用户界面**:基于 React + TypeScript 构建,提供流畅、直观的操作体验。
|
||||
|
||||
|
|
@ -79,119 +70,51 @@
|
|||
|
||||
## 🚀 快速开始
|
||||
|
||||
### ☁️ Vercel 一键部署
|
||||
### 🐳 Docker Compose 部署(推荐)
|
||||
|
||||
适合快速部署和体验,无需服务器,全球 CDN 加速。
|
||||
|
||||
#### 方式一:一键部署按钮(推荐)⭐
|
||||
|
||||
点击下方按钮直接部署到 Vercel:
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/lintsinghua/XCodeReviewer)
|
||||
|
||||
#### 方式二:通过 Vercel CLI 部署
|
||||
|
||||
```bash
|
||||
# 1. 安装 Vercel CLI
|
||||
npm i -g vercel
|
||||
|
||||
# 2. 登录 Vercel
|
||||
vercel login
|
||||
|
||||
# 3. 部署项目
|
||||
vercel
|
||||
|
||||
# 4. 部署到生产环境
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
#### 方式三:通过 Vercel Dashboard 部署
|
||||
|
||||
1. 访问 [Vercel Dashboard](https://vercel.com/dashboard)
|
||||
2. 点击 "Add New..." → "Project"
|
||||
3. 导入你的 GitHub 仓库
|
||||
4. Vercel 会自动检测 Vite 项目配置
|
||||
5. 配置环境变量(至少需要):
|
||||
```
|
||||
VITE_LLM_PROVIDER=your_llm_provider
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
VITE_USE_LOCAL_DB=true
|
||||
```
|
||||
6. 点击 "Deploy"
|
||||
|
||||
**✨ Vercel 部署优势**:
|
||||
- ✅ 全球 CDN 加速,访问速度快
|
||||
- ✅ 自动 HTTPS 和域名配置
|
||||
- ✅ 零配置,开箱即用
|
||||
- ✅ 支持自定义域名
|
||||
- ✅ 自动部署(Git 推送后自动更新)
|
||||
|
||||
**✨ 数据库模式**:
|
||||
- 默认自动使用**本地数据库模式**(IndexedDB),数据存储在浏览器中
|
||||
- 无需配置任何数据库,开箱即用
|
||||
- 如需使用 Supabase 云端数据库,可在环境变量中配置
|
||||
|
||||
**⚠️ 注意事项**:
|
||||
- Vercel 主要用于前端部署,后端 API 需单独部署
|
||||
- 部署后可在 `/admin` 页面进行运行时配置
|
||||
|
||||
---
|
||||
|
||||
### 🐳 Docker 部署(推荐生产环境)
|
||||
|
||||
#### 方式一:使用发布的镜像(最简单)⭐
|
||||
|
||||
直接使用最新发布的 Docker 镜像,支持 x86、ARM64(Mac M系列)、ARMv7 架构:
|
||||
|
||||
```bash
|
||||
# 1. 拉取最新版本镜像
|
||||
docker pull ghcr.io/lintsinghua/xcodereviewer:latest
|
||||
|
||||
# 2. 运行容器
|
||||
docker run -d \
|
||||
-p 8888:80 \
|
||||
--name xcodereviewer \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/lintsinghua/xcodereviewer:latest
|
||||
|
||||
# 3. 访问应用
|
||||
# 浏览器打开 http://localhost:8888
|
||||
```
|
||||
|
||||
**使用特定版本**:
|
||||
```bash
|
||||
# 拉取指定版本(如 v1.1.0)
|
||||
docker pull ghcr.io/lintsinghua/xcodereviewer:v1.1.0
|
||||
|
||||
# 运行
|
||||
docker run -d -p 8888:80 --name xcodereviewer ghcr.io/lintsinghua/xcodereviewer:v1.1.0
|
||||
```
|
||||
|
||||
#### 方式二:本地构建(可选)
|
||||
|
||||
如果需要自定义构建:
|
||||
完整的前后端分离部署方案,包含前端、后端和 PostgreSQL 数据库,一键启动所有服务。
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||
cd XCodeReviewer
|
||||
|
||||
# 2. 使用 Docker Compose 构建并启动
|
||||
# 2. 配置后端环境变量
|
||||
cp backend/env.example backend/.env
|
||||
# 编辑 backend/.env 文件,配置 LLM API Key 等参数
|
||||
|
||||
# 3. 使用 Docker Compose 启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 3. 访问应用
|
||||
# 浏览器打开 http://localhost:8888
|
||||
# 4. 访问应用
|
||||
# 前端: http://localhost:5173
|
||||
# 后端 API: http://localhost:8000
|
||||
# API 文档: http://localhost:8000/docs
|
||||
```
|
||||
|
||||
**✨ 运行时配置(推荐)**
|
||||
**服务说明**:
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| `frontend` | 5173 | React 前端应用(开发模式) |
|
||||
| `backend` | 8000 | FastAPI 后端 API |
|
||||
| `db` | 5432 | PostgreSQL 数据库 |
|
||||
|
||||
Docker 部署后,您可以直接在浏览器中配置所有设置,无需重新构建镜像:
|
||||
**生产环境部署**:
|
||||
|
||||
1. 访问 `http://localhost:8888/admin`(系统管理页面)
|
||||
2. 在"系统配置"标签页中配置 LLM API Keys 和其他参数
|
||||
3. 点击保存并刷新页面即可使用
|
||||
如需生产环境部署,可使用根目录的 `Dockerfile` 构建前端静态文件并通过 Nginx 提供服务:
|
||||
|
||||
> 📖 **详细配置说明请参考**:[系统配置使用指南](#系统配置首次使用必看)
|
||||
```bash
|
||||
# 构建前端生产镜像
|
||||
docker build -t xcodereviewer-frontend .
|
||||
|
||||
# 运行前端容器(端口 8888)
|
||||
docker run -d -p 8888:80 --name xcodereviewer-frontend xcodereviewer-frontend
|
||||
|
||||
# 后端和数据库仍使用 docker-compose
|
||||
docker-compose up -d db backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💻 本地开发部署
|
||||
|
||||
|
|
@ -199,21 +122,46 @@ Docker 部署后,您可以直接在浏览器中配置所有设置,无需重
|
|||
|
||||
#### 环境要求
|
||||
- Node.js 18+
|
||||
- Python 3.13+
|
||||
- PostgreSQL 15+
|
||||
- pnpm 8+ (推荐) 或 npm/yarn
|
||||
|
||||
#### 快速启动
|
||||
#### 后端启动
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||
cd XCodeReviewer
|
||||
# 1. 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 2. 创建虚拟环境(推荐使用 uv)
|
||||
uv venv
|
||||
source .venv/bin/activate # Linux/macOS
|
||||
# 或 .venv\Scripts\activate # Windows
|
||||
|
||||
# 3. 安装依赖
|
||||
uv pip install -e .
|
||||
|
||||
# 4. 配置环境变量
|
||||
cp env.example .env
|
||||
# 编辑 .env 文件,配置数据库和 LLM 参数
|
||||
|
||||
# 5. 初始化数据库
|
||||
alembic upgrade head
|
||||
|
||||
# 6. 启动后端服务
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
#### 前端启动
|
||||
|
||||
```bash
|
||||
# 1. 进入前端目录
|
||||
cd frontend
|
||||
|
||||
# 2. 安装依赖
|
||||
pnpm install # 或 npm install / yarn install
|
||||
|
||||
# 3. 配置环境变量
|
||||
# 3. 配置环境变量(可选,也可使用运行时配置)
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件,配置必要参数(见下方配置说明)
|
||||
|
||||
# 4. 启动开发服务器
|
||||
pnpm dev
|
||||
|
|
@ -222,56 +170,38 @@ pnpm dev
|
|||
# 浏览器打开 http://localhost:5173
|
||||
```
|
||||
|
||||
#### 核心配置说明
|
||||
#### 后端核心配置说明
|
||||
|
||||
编辑 `.env` 文件,配置以下必需参数:
|
||||
编辑 `backend/.env` 文件:
|
||||
|
||||
```env
|
||||
# ========== 必需配置 ==========
|
||||
# LLM 提供商选择 (gemini|openai|claude|qwen|deepseek|zhipu|moonshot|baidu|minimax|doubao|ollama)
|
||||
VITE_LLM_PROVIDER=gemini
|
||||
# 对应的 API Key
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
# ========== 数据库配置 ==========
|
||||
POSTGRES_SERVER=localhost
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=xcodereviewer
|
||||
|
||||
# ========== 数据库配置(三选一)==========
|
||||
# 方式1:本地数据库(推荐,开箱即用)
|
||||
VITE_USE_LOCAL_DB=true
|
||||
# ========== 安全配置 ==========
|
||||
SECRET_KEY=your-super-secret-key-change-this-in-production
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=11520
|
||||
|
||||
# 方式2:Supabase 云端数据库(支持多设备同步)
|
||||
# VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
# VITE_SUPABASE_ANON_KEY=your_anon_key
|
||||
# ========== LLM配置 ==========
|
||||
# 支持的provider: openai, gemini, claude, qwen, deepseek, zhipu, moonshot, baidu, minimax, doubao, ollama
|
||||
LLM_PROVIDER=openai
|
||||
LLM_API_KEY=sk-your-api-key
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
LLM_BASE_URL= # API中转站地址(可选)
|
||||
LLM_TIMEOUT=150
|
||||
LLM_TEMPERATURE=0.1
|
||||
LLM_MAX_TOKENS=4096
|
||||
|
||||
# 方式3:演示模式(不配置任何数据库,数据不持久化)
|
||||
|
||||
# ========== 可选配置 ==========
|
||||
# GitHub 集成(用于仓库分析)
|
||||
# VITE_GITHUB_TOKEN=your_github_token
|
||||
|
||||
# 输出语言(zh-CN: 中文 | en-US: 英文)
|
||||
VITE_OUTPUT_LANGUAGE=zh-CN
|
||||
|
||||
# 分析参数调优
|
||||
VITE_MAX_ANALYZE_FILES=40 # 单次最大分析文件数
|
||||
VITE_LLM_CONCURRENCY=2 # 并发请求数
|
||||
VITE_LLM_GAP_MS=500 # 请求间隔(ms)
|
||||
```
|
||||
|
||||
#### 高级配置
|
||||
|
||||
遇到超时或连接问题时,可调整以下参数:
|
||||
|
||||
```env
|
||||
VITE_LLM_TIMEOUT=300000 # 增加超时时间
|
||||
VITE_LLM_BASE_URL=https://your-proxy.com/v1 # 使用代理或中转服务
|
||||
VITE_LLM_CONCURRENCY=1 # 降低并发数
|
||||
VITE_LLM_GAP_MS=1000 # 增加请求间隔
|
||||
```
|
||||
|
||||
**自定义请求头示例**(针对特殊中转站):
|
||||
|
||||
```env
|
||||
# JSON 格式字符串
|
||||
VITE_LLM_CUSTOM_HEADERS='{"X-API-Version":"v1","X-Custom-Auth":"token123"}'
|
||||
# ========== 仓库扫描配置 ==========
|
||||
GITHUB_TOKEN=your_github_token
|
||||
GITLAB_TOKEN=your_gitlab_token
|
||||
MAX_ANALYZE_FILES=50
|
||||
LLM_CONCURRENCY=3
|
||||
LLM_GAP_MS=2000
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
|
@ -281,33 +211,33 @@ VITE_LLM_CUSTOM_HEADERS='{"X-API-Version":"v1","X-Custom-Auth":"token123"}'
|
|||
|
||||
**方式一:浏览器配置(推荐)**
|
||||
|
||||
1. 访问 `http://localhost:8888/admin` 系统管理页面
|
||||
1. 访问 `http://localhost:5173/admin` 系统管理页面
|
||||
2. 在"系统配置"标签页选择不同的 LLM 提供商
|
||||
3. 填入对应的 API Key
|
||||
4. 保存并刷新页面
|
||||
|
||||
**方式二:环境变量配置**
|
||||
**方式二:后端环境变量配置**
|
||||
|
||||
修改 `.env` 中的配置:
|
||||
修改 `backend/.env` 中的配置:
|
||||
|
||||
```env
|
||||
# 切换到 OpenAI
|
||||
VITE_LLM_PROVIDER=openai
|
||||
VITE_OPENAI_API_KEY=your_key
|
||||
LLM_PROVIDER=openai
|
||||
LLM_API_KEY=your_key
|
||||
|
||||
# 切换到通义千问
|
||||
VITE_LLM_PROVIDER=qwen
|
||||
VITE_QWEN_API_KEY=your_key
|
||||
LLM_PROVIDER=qwen
|
||||
LLM_API_KEY=your_key
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>遇到请求超时怎么办?</b></summary>
|
||||
|
||||
1. 增加超时时间:`VITE_LLM_TIMEOUT=300000`
|
||||
2. 使用代理:配置 `VITE_LLM_BASE_URL`
|
||||
1. 增加超时时间:`LLM_TIMEOUT=300`
|
||||
2. 使用代理:配置 `LLM_BASE_URL`
|
||||
3. 切换到国内平台:通义千问、DeepSeek、智谱AI 等
|
||||
4. 降低并发:`VITE_LLM_CONCURRENCY=1`
|
||||
4. 降低并发:`LLM_CONCURRENCY=1`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -324,7 +254,7 @@ VITE_SUPABASE_URL=https://your-project.supabase.co
|
|||
VITE_SUPABASE_ANON_KEY=your_key
|
||||
```
|
||||
|
||||
**演示模式**:不配置任何数据库,数据不持久化
|
||||
**后端数据库模式**:使用 PostgreSQL 存储,适合团队协作
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -338,11 +268,11 @@ curl -fsSL https://ollama.com/install.sh | sh # macOS/Linux
|
|||
# 2. 拉取模型
|
||||
ollama pull llama3 # 或 codellama、qwen2.5、deepseek-coder
|
||||
|
||||
# 3. 配置 XCodeReviewer
|
||||
# 在 .env 中设置:
|
||||
VITE_LLM_PROVIDER=ollama
|
||||
VITE_LLM_MODEL=llama3
|
||||
VITE_LLM_BASE_URL=http://localhost:11434/v1
|
||||
# 3. 配置后端
|
||||
# 在 backend/.env 中设置:
|
||||
LLM_PROVIDER=ollama
|
||||
LLM_MODEL=llama3
|
||||
LLM_BASE_URL=http://localhost:11434/v1
|
||||
```
|
||||
|
||||
推荐模型:`llama3`(综合)、`codellama`(代码专用)、`qwen2.5`(中文)
|
||||
|
|
@ -353,8 +283,8 @@ VITE_LLM_BASE_URL=http://localhost:11434/v1
|
|||
|
||||
百度需要同时提供 API Key 和 Secret Key,用冒号分隔:
|
||||
```env
|
||||
VITE_LLM_PROVIDER=baidu
|
||||
VITE_BAIDU_API_KEY=your_api_key:your_secret_key
|
||||
LLM_PROVIDER=baidu
|
||||
LLM_API_KEY=your_api_key:your_secret_key
|
||||
```
|
||||
获取地址:https://console.bce.baidu.com/qianfan/
|
||||
</details>
|
||||
|
|
@ -364,80 +294,18 @@ VITE_BAIDU_API_KEY=your_api_key:your_secret_key
|
|||
|
||||
许多用户使用 API 中转服务来访问 LLM(更稳定、更便宜)。配置方法:
|
||||
|
||||
**后端配置**(推荐):
|
||||
```env
|
||||
LLM_PROVIDER=openai
|
||||
LLM_API_KEY=中转站提供的Key
|
||||
LLM_BASE_URL=https://your-proxy.com/v1
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
```
|
||||
|
||||
**前端运行时配置**:
|
||||
1. 访问系统管理页面(`/admin`)
|
||||
2. 在"系统配置"标签页中:
|
||||
- 选择 LLM 提供商(如 OpenAI)
|
||||
- **API 基础 URL**: 填入中转站地址(如 `https://your-proxy.com/v1`)
|
||||
- **API Key**: 填入中转站提供的密钥(而非官方密钥)
|
||||
2. 在"系统配置"标签页中配置 API 基础 URL 和 Key
|
||||
3. 保存并刷新页面
|
||||
|
||||
**注意**:
|
||||
- 中转站 URL 通常以 `/v1` 结尾(OpenAI 兼容格式)
|
||||
- 使用中转站的 API Key,不是官方的
|
||||
- 确认中转站支持你选择的 AI 模型
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>如何备份本地数据库?</b></summary>
|
||||
|
||||
本地数据存储在浏览器 IndexedDB 中:
|
||||
- 在应用的"系统管理"页面导出为 JSON 文件
|
||||
- 通过导入 JSON 文件恢复数据
|
||||
- 注意:清除浏览器数据会删除所有本地数据
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>如何设置输出语言?</b></summary>
|
||||
|
||||
```env
|
||||
VITE_OUTPUT_LANGUAGE=zh-CN # 中文(默认)
|
||||
VITE_OUTPUT_LANGUAGE=en-US # 英文
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>如何配置多个平台并快速切换?</b></summary>
|
||||
|
||||
在 `.env` 中预配置所有平台的 Key,切换时只需修改 `VITE_LLM_PROVIDER`:
|
||||
```env
|
||||
VITE_LLM_PROVIDER=gemini # 当前使用的平台
|
||||
|
||||
# 预配置所有平台
|
||||
VITE_GEMINI_API_KEY=key1
|
||||
VITE_OPENAI_API_KEY=key2
|
||||
VITE_QWEN_API_KEY=key3
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>如何查看系统日志和调试信息?</b></summary>
|
||||
|
||||
XCodeReviewer 内置了日志系统,记录核心操作和错误:
|
||||
|
||||
**查看日志**:
|
||||
- 导航栏 -> 系统日志
|
||||
- 或访问:`http://localhost:5173/logs` (开发) / `http://localhost:8888/logs` (生产)
|
||||
|
||||
**记录内容**:
|
||||
- ✅ 用户核心操作(创建项目、审计任务、修改配置等)
|
||||
- ✅ API 请求失败和错误
|
||||
- ✅ 控制台错误(自动捕获)
|
||||
- ✅ 未处理的异常
|
||||
|
||||
**功能特性**:
|
||||
- 日志筛选、搜索
|
||||
- 导出日志(JSON/CSV)
|
||||
- 错误详情查看
|
||||
|
||||
**手动记录用户操作**:
|
||||
```typescript
|
||||
import { logger, LogCategory } from '@/shared/utils/logger';
|
||||
|
||||
// 记录用户操作
|
||||
logger.logUserAction('创建项目', { projectName, projectType });
|
||||
logger.logUserAction('开始审计', { taskId, fileCount });
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 🔑 获取 API Key
|
||||
|
|
@ -460,49 +328,31 @@ XCodeReviewer 支持 10+ 主流 LLM 平台,可根据需求自由选择:
|
|||
| | 字节豆包 | 高性价比 | [获取](https://console.volcengine.com/ark) |
|
||||
| **本地部署** | Ollama | 完全本地化,隐私安全 | [安装](https://ollama.com/) |
|
||||
|
||||
#### 配置示例
|
||||
|
||||
```env
|
||||
# 通用配置(推荐)
|
||||
VITE_LLM_PROVIDER=gemini
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# 或使用平台专用配置
|
||||
VITE_GEMINI_API_KEY=your_gemini_key
|
||||
VITE_OPENAI_API_KEY=your_openai_key
|
||||
# ... 更多平台配置见 .env.example
|
||||
```
|
||||
|
||||
#### Supabase 配置(可选)
|
||||
|
||||
如需云端数据同步:
|
||||
1. 访问 [Supabase](https://supabase.com/) 创建项目
|
||||
2. 获取 URL 和匿名密钥
|
||||
3. 在 Supabase SQL 编辑器执行 `supabase/migrations/full_schema.sql`
|
||||
4. 在 `.env` 中配置相关参数
|
||||
|
||||
## ✨ 核心功能
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 项目管理</b></summary>
|
||||
|
||||
- **一键集成代码仓库**:无缝对接 GitHub、GitLab 等主流平台。
|
||||
- **多语言“全家桶”支持**:覆盖 JavaScript, TypeScript, Python, Java, Go, Rust 等热门语言。
|
||||
- **多语言"全家桶"支持**:覆盖 JavaScript, TypeScript, Python, Java, Go, Rust 等热门语言。
|
||||
- **灵活的分支审计**:支持对指定代码分支进行精确分析。
|
||||
- **ZIP 文件上传**:支持直接上传 ZIP 压缩包进行代码审计。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>⚡ 即时分析</b></summary>
|
||||
|
||||
- **代码片段“随手贴”**:直接在 Web 界面粘贴代码,立即获得分析结果。
|
||||
- **代码片段"随手贴"**:直接在 Web 界面粘贴代码,立即获得分析结果。
|
||||
- **10+ 种语言即时支持**:满足您多样化的代码分析需求。
|
||||
- **毫秒级响应**:快速获取代码质量评分和优化建议。
|
||||
- **历史记录功能**:自动保存分析历史,支持查看和导出历史分析报告。
|
||||
- **报告导出**:支持将即时分析结果导出为 JSON 或 PDF 格式。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧠 智能审计</b></summary>
|
||||
|
||||
- **AI 深度代码理解**:支持多个主流 LLM 平台(Gemini、OpenAI、Claude、通义千问、DeepSeek 等),提供超越关键词匹配的智能分析。
|
||||
- **AI 深度代码理解**:支持多个主流 LLM 平台,后端使用 LiteLLM 统一适配,提供超越关键词匹配的智能分析。
|
||||
- **五大核心维度检测**:
|
||||
- 🐛 **潜在 Bug**:精准捕捉逻辑错误、边界条件和空指针等问题。
|
||||
- 🔒 **安全漏洞**:识别 SQL 注入、XSS、敏感信息泄露等安全风险。
|
||||
|
|
@ -526,6 +376,7 @@ VITE_OPENAI_API_KEY=your_openai_key
|
|||
- **代码质量仪表盘**:提供 0-100 分的综合质量评估,让代码健康状况一目了然。
|
||||
- **多维度问题统计**:按类型和严重程度对问题进行分类统计。
|
||||
- **质量趋势分析**:通过图表展示代码质量随时间的变化趋势。
|
||||
- **报告导出**:支持 JSON 和 PDF 格式导出审计报告。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -538,23 +389,26 @@ VITE_OPENAI_API_KEY=your_openai_key
|
|||
- 🔑 **平台密钥**:管理 10+ LLM 平台的 API Keys,支持快速切换
|
||||
- ⚡ **分析参数**:调整并发数、间隔时间、最大文件数等
|
||||
- 🌐 **API 中转站支持**:轻松配置第三方 API 代理服务
|
||||
- 💾 **配置优先级**:运行时配置 > 构建时配置,无需重新构建镜像
|
||||
|
||||
- **💾 数据库管理**:
|
||||
- 🏠 **三种模式**:本地 IndexedDB / Supabase 云端 / 演示模式
|
||||
- 🏠 **三种模式**:本地 IndexedDB / Supabase 云端 / PostgreSQL 后端
|
||||
- 📤 **导出备份**:将数据导出为 JSON 文件
|
||||
- 📥 **导入恢复**:从备份文件恢复数据
|
||||
- 🗑️ **清空数据**:一键清理所有本地数据
|
||||
- 📊 **存储监控**:实时查看存储空间使用情况
|
||||
|
||||
- **📈 数据概览**:
|
||||
- 项目、任务、问题的完整统计
|
||||
- 可视化图表展示质量趋势
|
||||
- 存储使用情况分析
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>👥 用户管理</b></summary>
|
||||
|
||||
- **用户注册与登录**:支持用户账户系统
|
||||
- **JWT 认证**:安全的 Token 认证机制
|
||||
- **权限控制**:基于角色的访问控制
|
||||
</details>
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 前端技术栈
|
||||
|
||||
| 分类 | 技术 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **前端框架** | `React 18` `TypeScript` `Vite` | 现代化前端开发栈,支持热重载和类型安全 |
|
||||
|
|
@ -562,56 +416,122 @@ VITE_OPENAI_API_KEY=your_openai_key
|
|||
| **数据可视化** | `Recharts` | 专业的图表库,支持多种图表类型 |
|
||||
| **路由管理** | `React Router v6` | 单页应用路由解决方案 |
|
||||
| **状态管理** | `React Hooks` `Sonner` | 轻量级状态管理和通知系统 |
|
||||
| **AI 引擎** | `多平台 LLM` | 支持 Gemini、OpenAI、Claude、通义千问、DeepSeek 等 10+ 主流平台 |
|
||||
| **数据存储** | `IndexedDB` `Supabase` `PostgreSQL` | 本地数据库 + 云端数据库双模式支持 |
|
||||
| **HTTP 客户端** | `Axios` `Ky` | 现代化的 HTTP 请求库 |
|
||||
| **代码质量** | `Biome` `Ast-grep` `TypeScript` | 代码格式化、静态分析和类型检查 |
|
||||
| **构建工具** | `Vite` `PostCSS` `Autoprefixer` | 快速的构建工具和 CSS 处理 |
|
||||
|
||||
### 后端技术栈
|
||||
|
||||
| 分类 | 技术 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Web 框架** | `FastAPI` | 高性能异步 Python Web 框架 |
|
||||
| **数据库** | `PostgreSQL` `SQLAlchemy` `Alembic` | 关系型数据库 + ORM + 数据库迁移 |
|
||||
| **认证授权** | `python-jose` `passlib` `bcrypt` | JWT Token 认证和密码加密 |
|
||||
| **LLM 集成** | `LiteLLM` | 统一的 LLM API 适配层,支持 10+ 平台 |
|
||||
| **HTTP 客户端** | `httpx` | 异步 HTTP 客户端 |
|
||||
| **数据验证** | `Pydantic` | 数据验证和序列化 |
|
||||
|
||||
### 数据存储
|
||||
|
||||
| 分类 | 技术 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **前端本地存储** | `IndexedDB` | 浏览器本地数据库,开箱即用 |
|
||||
| **云端数据库** | `Supabase` | 可选的云端数据同步 |
|
||||
| **后端数据库** | `PostgreSQL` | 生产环境推荐的关系型数据库 |
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
XCodeReviewer/
|
||||
├── src/
|
||||
│ ├── app/ # 应用配置
|
||||
│ │ ├── App.tsx # 主应用组件
|
||||
│ │ ├── main.tsx # 应用入口点
|
||||
│ │ └── routes.tsx # 路由配置
|
||||
│ ├── components/ # React 组件
|
||||
│ │ ├── layout/ # 布局组件 (Header, Footer, PageMeta)
|
||||
│ │ ├── ui/ # UI 组件库 (基于 Radix UI)
|
||||
│ │ ├── system/ # 系统配置组件
|
||||
│ │ ├── database/ # 数据库管理组件
|
||||
│ │ └── debug/ # 调试组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Dashboard.tsx # 仪表盘
|
||||
│ │ ├── Projects.tsx # 项目管理
|
||||
│ │ ├── InstantAnalysis.tsx # 即时分析
|
||||
│ │ ├── AuditTasks.tsx # 审计任务
|
||||
│ │ └── AdminDashboard.tsx # 系统管理
|
||||
│ ├── features/ # 功能模块
|
||||
│ │ ├── analysis/ # 分析相关服务
|
||||
│ │ │ └── services/ # AI 代码分析引擎
|
||||
│ │ └── projects/ # 项目相关服务
|
||||
│ │ └── services/ # 仓库扫描、ZIP 文件扫描
|
||||
│ ├── shared/ # 共享工具
|
||||
│ │ ├── config/ # 配置文件
|
||||
│ │ │ ├── database.ts # 数据库统一接口
|
||||
│ │ │ ├── localDatabase.ts # IndexedDB 实现
|
||||
│ │ │ └── env.ts # 环境变量配置
|
||||
│ │ ├── types/ # TypeScript 类型定义
|
||||
│ │ ├── hooks/ # 自定义 React Hooks
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ │ └── initLocalDB.ts # 本地数据库初始化
|
||||
│ │ └── constants/ # 常量定义
|
||||
│ └── assets/ # 静态资源
|
||||
│ └── styles/ # 样式文件
|
||||
├── supabase/
|
||||
│ └── migrations/ # 数据库迁移文件
|
||||
├── public/
|
||||
│ └── images/ # 图片资源
|
||||
├── scripts/ # 构建和设置脚本
|
||||
└── rules/ # 代码规则配置
|
||||
├── frontend/ # 前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── app/ # 应用配置和路由
|
||||
│ │ │ ├── App.tsx # 主应用组件
|
||||
│ │ │ ├── main.tsx # 应用入口点
|
||||
│ │ │ ├── routes.tsx # 路由配置
|
||||
│ │ │ └── ProtectedRoute.tsx # 路由守卫
|
||||
│ │ ├── components/ # React 组件
|
||||
│ │ │ ├── layout/ # 布局组件 (Sidebar, PageMeta)
|
||||
│ │ │ ├── ui/ # UI 组件库 (基于 Radix UI)
|
||||
│ │ │ ├── system/ # 系统配置组件
|
||||
│ │ │ ├── database/ # 数据库管理组件
|
||||
│ │ │ ├── audit/ # 审计任务组件
|
||||
│ │ │ ├── analysis/ # 分析进度组件
|
||||
│ │ │ ├── reports/ # 报告导出组件
|
||||
│ │ │ └── debug/ # 调试组件
|
||||
│ │ ├── pages/ # 页面组件
|
||||
│ │ │ ├── Dashboard.tsx # 仪表盘
|
||||
│ │ │ ├── Projects.tsx # 项目管理
|
||||
│ │ │ ├── InstantAnalysis.tsx # 即时分析
|
||||
│ │ │ ├── AuditTasks.tsx # 审计任务
|
||||
│ │ │ ├── AdminDashboard.tsx # 系统管理
|
||||
│ │ │ ├── Login.tsx # 登录页面
|
||||
│ │ │ ├── Register.tsx # 注册页面
|
||||
│ │ │ └── Account.tsx # 账户管理
|
||||
│ │ ├── features/ # 功能模块
|
||||
│ │ │ ├── analysis/ # 代码分析服务
|
||||
│ │ │ ├── projects/ # 项目扫描服务
|
||||
│ │ │ └── reports/ # 报告生成服务
|
||||
│ │ ├── shared/ # 共享工具
|
||||
│ │ │ ├── api/ # API 客户端
|
||||
│ │ │ ├── config/ # 配置文件
|
||||
│ │ │ ├── context/ # React Context
|
||||
│ │ │ ├── hooks/ # 自定义 Hooks
|
||||
│ │ │ ├── i18n/ # 国际化
|
||||
│ │ │ ├── types/ # TypeScript 类型
|
||||
│ │ │ └── utils/ # 工具函数
|
||||
│ │ └── assets/ # 静态资源
|
||||
│ └── public/ # 公共资源
|
||||
│
|
||||
├── backend/ # 后端项目
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API 路由
|
||||
│ │ │ └── v1/
|
||||
│ │ │ └── endpoints/ # API 端点
|
||||
│ │ │ ├── auth.py # 认证接口
|
||||
│ │ │ ├── users.py # 用户管理
|
||||
│ │ │ ├── projects.py # 项目管理
|
||||
│ │ │ ├── tasks.py # 任务管理
|
||||
│ │ │ ├── scan.py # 代码扫描
|
||||
│ │ │ ├── config.py # 配置管理
|
||||
│ │ │ └── database.py # 数据库操作
|
||||
│ │ ├── core/ # 核心模块
|
||||
│ │ │ ├── config.py # 配置管理
|
||||
│ │ │ ├── security.py # 安全认证
|
||||
│ │ │ └── encryption.py # 加密工具
|
||||
│ │ ├── db/ # 数据库
|
||||
│ │ │ ├── base.py # 数据库基类
|
||||
│ │ │ └── session.py # 数据库会话
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ │ ├── user.py # 用户模型
|
||||
│ │ │ ├── project.py # 项目模型
|
||||
│ │ │ ├── audit.py # 审计模型
|
||||
│ │ │ └── analysis.py # 分析模型
|
||||
│ │ ├── schemas/ # Pydantic 模式
|
||||
│ │ │ ├── user.py # 用户模式
|
||||
│ │ │ └── token.py # Token 模式
|
||||
│ │ ├── services/ # 业务服务
|
||||
│ │ │ ├── llm/ # LLM 服务
|
||||
│ │ │ │ ├── factory.py # LLM 工厂
|
||||
│ │ │ │ ├── service.py # LLM 服务
|
||||
│ │ │ │ ├── base_adapter.py # 基础适配器
|
||||
│ │ │ │ └── adapters/ # 平台适配器
|
||||
│ │ │ │ ├── litellm_adapter.py # LiteLLM 统一适配
|
||||
│ │ │ │ ├── baidu_adapter.py # 百度适配器
|
||||
│ │ │ │ ├── minimax_adapter.py # MiniMax 适配器
|
||||
│ │ │ │ └── doubao_adapter.py # 豆包适配器
|
||||
│ │ │ ├── scanner.py # 代码扫描服务
|
||||
│ │ │ └── zip_storage.py # ZIP 文件存储
|
||||
│ │ └── main.py # 应用入口
|
||||
│ ├── alembic/ # 数据库迁移
|
||||
│ └── uploads/ # 上传文件存储
|
||||
│
|
||||
├── supabase/ # Supabase 配置
|
||||
│ └── migrations/ # 数据库迁移文件
|
||||
├── docker-compose.yml # Docker Compose 配置(开发环境)
|
||||
├── Dockerfile # 前端生产环境 Docker 镜像
|
||||
├── nginx.conf # Nginx 配置(生产环境)
|
||||
└── scripts/ # 构建和设置脚本
|
||||
```
|
||||
|
||||
## 🎯 使用指南
|
||||
|
|
@ -664,100 +584,54 @@ XCodeReviewer/
|
|||
- **PDF 格式**:专业报告,适合打印和分享(通过浏览器打印功能)
|
||||
3. JSON 报告包含完整的任务信息、问题详情和统计数据
|
||||
4. PDF 报告提供美观的可视化展示,支持中文显示
|
||||
5. 报告内容包括:项目信息、审计统计、问题详情(按严重程度分类)、修复建议等
|
||||
|
||||
**PDF 导出提示:**
|
||||
- 点击"导出 PDF"后会弹出浏览器打印对话框
|
||||
- 建议在打印设置中**取消勾选"页眉和页脚"选项**,以获得更干净的报告(避免显示 URL 等信息)
|
||||
- 在打印对话框中选择"另存为 PDF"即可保存报告文件
|
||||
|
||||
### 构建和部署
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
pnpm dev
|
||||
# 前端开发模式
|
||||
cd frontend && pnpm dev
|
||||
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
# 前端构建生产版本
|
||||
cd frontend && pnpm build
|
||||
|
||||
# 预览构建结果
|
||||
pnpm preview
|
||||
# 后端开发模式
|
||||
cd backend && uvicorn app.main:app --reload
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
cd frontend && pnpm lint
|
||||
```
|
||||
|
||||
### 环境变量说明
|
||||
### 后端环境变量说明
|
||||
|
||||
#### 核心LLM配置
|
||||
#### 核心配置
|
||||
| 变量名 | 必需 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `VITE_LLM_PROVIDER` | ✅ | `gemini` | LLM提供商:`gemini`\|`openai`\|`claude`\|`qwen`\|`deepseek`\|`zhipu`\|`moonshot`\|`baidu`\|`minimax`\|`doubao`\|`ollama` |
|
||||
| `VITE_LLM_API_KEY` | ✅ | - | 通用API Key(优先级高于平台专用配置) |
|
||||
| `VITE_LLM_MODEL` | ❌ | 自动 | 模型名称(不指定则使用各平台默认模型) |
|
||||
| `VITE_LLM_BASE_URL` | ❌ | - | 自定义API端点(**支持所有平台的中转站**、代理或私有部署) |
|
||||
| `VITE_LLM_TIMEOUT` | ❌ | `150000` | 请求超时时间(毫秒) |
|
||||
| `VITE_LLM_TEMPERATURE` | ❌ | `0.2` | 温度参数(0.0-2.0),控制输出随机性 |
|
||||
| `VITE_LLM_MAX_TOKENS` | ❌ | `4096` | 最大输出token数 |
|
||||
| `VITE_LLM_CUSTOM_HEADERS` | ❌ | - | 自定义HTTP请求头(JSON格式字符串),用于特殊中转站或自建服务 |
|
||||
| `POSTGRES_SERVER` | ✅ | `localhost` | PostgreSQL 服务器地址 |
|
||||
| `POSTGRES_USER` | ✅ | `postgres` | 数据库用户名 |
|
||||
| `POSTGRES_PASSWORD` | ✅ | `postgres` | 数据库密码 |
|
||||
| `POSTGRES_DB` | ✅ | `xcodereviewer` | 数据库名称 |
|
||||
| `SECRET_KEY` | ✅ | - | JWT 密钥(生产环境必须修改) |
|
||||
|
||||
> 💡 **API 格式支持**:XCodeReviewer 支持三种主流 API 格式:
|
||||
> - **OpenAI 兼容格式**(最常见):适用于大多数中转站和 OpenRouter
|
||||
> - **Gemini 格式**:Google Gemini 官方及兼容服务
|
||||
> - **Claude 格式**:Anthropic Claude 官方及兼容服务
|
||||
>
|
||||
> 配置时只需选择对应的 LLM 提供商,填入中转站地址和 Key 即可。自定义请求头功能可满足特殊中转站的额外要求。
|
||||
#### LLM 配置
|
||||
| 变量名 | 必需 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `LLM_PROVIDER` | ✅ | `openai` | LLM 提供商 |
|
||||
| `LLM_API_KEY` | ✅ | - | API Key |
|
||||
| `LLM_MODEL` | ❌ | 自动 | 模型名称 |
|
||||
| `LLM_BASE_URL` | ❌ | - | API 中转站地址 |
|
||||
| `LLM_TIMEOUT` | ❌ | `150` | 请求超时(秒) |
|
||||
| `LLM_TEMPERATURE` | ❌ | `0.1` | 温度参数 |
|
||||
| `LLM_MAX_TOKENS` | ❌ | `4096` | 最大输出 Token |
|
||||
|
||||
#### 平台专用API Key配置(可选)
|
||||
| 变量名 | 说明 | 特殊要求 |
|
||||
|--------|------|---------|
|
||||
| `VITE_GEMINI_API_KEY` | Google Gemini API Key | - |
|
||||
| `VITE_GEMINI_MODEL` | Gemini模型 (默认: gemini-1.5-flash) | - |
|
||||
| `VITE_OPENAI_API_KEY` | OpenAI API Key | - |
|
||||
| `VITE_OPENAI_MODEL` | OpenAI模型 (默认: gpt-4o-mini) | - |
|
||||
| `VITE_OPENAI_BASE_URL` | OpenAI自定义端点 | 用于中转服务 |
|
||||
| `VITE_CLAUDE_API_KEY` | Anthropic Claude API Key | - |
|
||||
| `VITE_CLAUDE_MODEL` | Claude模型 (默认: claude-3-5-sonnet-20241022) | - |
|
||||
| `VITE_QWEN_API_KEY` | 阿里云通义千问 API Key | - |
|
||||
| `VITE_QWEN_MODEL` | 通义千问模型 (默认: qwen-turbo) | - |
|
||||
| `VITE_DEEPSEEK_API_KEY` | DeepSeek API Key | - |
|
||||
| `VITE_DEEPSEEK_MODEL` | DeepSeek模型 (默认: deepseek-chat) | - |
|
||||
| `VITE_ZHIPU_API_KEY` | 智谱AI API Key | - |
|
||||
| `VITE_ZHIPU_MODEL` | 智谱模型 (默认: glm-4-flash) | - |
|
||||
| `VITE_MOONSHOT_API_KEY` | 月之暗面 Kimi API Key | - |
|
||||
| `VITE_MOONSHOT_MODEL` | Kimi模型 (默认: moonshot-v1-8k) | - |
|
||||
| `VITE_BAIDU_API_KEY` | 百度文心一言 API Key | ⚠️ 格式: `API_KEY:SECRET_KEY` |
|
||||
| `VITE_BAIDU_MODEL` | 文心模型 (默认: ERNIE-3.5-8K) | - |
|
||||
| `VITE_MINIMAX_API_KEY` | MiniMax API Key | - |
|
||||
| `VITE_MINIMAX_MODEL` | MiniMax模型 (默认: abab6.5-chat) | - |
|
||||
| `VITE_DOUBAO_API_KEY` | 字节豆包 API Key | - |
|
||||
| `VITE_DOUBAO_MODEL` | 豆包模型 (默认: doubao-pro-32k) | - |
|
||||
|
||||
#### 数据库配置(可选)
|
||||
| 变量名 | 必需 | 说明 |
|
||||
|--------|------|------|
|
||||
| `VITE_SUPABASE_URL` | ❌ | Supabase项目URL(用于数据持久化) |
|
||||
| `VITE_SUPABASE_ANON_KEY` | ❌ | Supabase匿名密钥 |
|
||||
|
||||
> 💡 **提示**:不配置Supabase时,系统以演示模式运行,数据不持久化
|
||||
|
||||
#### Git仓库集成配置
|
||||
| 变量名 | 必需 | 说明 |
|
||||
|--------|------|------|
|
||||
| `VITE_GITHUB_TOKEN` | ✅ | GitHub Personal Access Token |
|
||||
| `VITE_GITLAB_TOKEN` | ✅ | GitLab Personal Access Token 或 Project Access Token |
|
||||
|
||||
#### 分析行为配置
|
||||
#### 仓库扫描配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `VITE_MAX_ANALYZE_FILES` | `40` | 单次分析的最大文件数 |
|
||||
| `VITE_LLM_CONCURRENCY` | `2` | LLM并发请求数(降低可避免频率限制) |
|
||||
| `VITE_LLM_GAP_MS` | `500` | LLM请求间隔(毫秒,增加可避免频率限制) |
|
||||
|
||||
#### 应用配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `VITE_APP_ID` | `xcodereviewer` | 应用标识符 |
|
||||
| `GITHUB_TOKEN` | - | GitHub Personal Access Token |
|
||||
| `GITLAB_TOKEN` | - | GitLab Personal Access Token |
|
||||
| `MAX_ANALYZE_FILES` | `50` | 单次最大分析文件数 |
|
||||
| `MAX_FILE_SIZE_BYTES` | `204800` | 单文件最大大小(字节) |
|
||||
| `LLM_CONCURRENCY` | `3` | LLM 并发请求数 |
|
||||
| `LLM_GAP_MS` | `2000` | 请求间隔(毫秒) |
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
|
|
@ -775,9 +649,11 @@ pnpm lint
|
|||
|
||||
### 核心技术支持
|
||||
- **[React](https://reactjs.org/)** & **[Vite](https://vitejs.dev/)**: 提供现代化的前端开发体验
|
||||
- **[FastAPI](https://fastapi.tiangolo.com/)**: 高性能 Python Web 框架
|
||||
- **[TypeScript](https://www.typescriptlang.org/)**: 提供类型安全保障
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)**: 提供现代化的 CSS 框架
|
||||
- **[Radix UI](https://www.radix-ui.com/)**: 提供无障碍的 UI 组件库
|
||||
- **[LiteLLM](https://github.com/BerriAI/litellm)**: 统一的 LLM API 适配层
|
||||
|
||||
### AI 平台支持
|
||||
- **[Google Gemini AI](https://ai.google.dev/)**: 提供强大的 AI 分析能力
|
||||
|
|
@ -790,6 +666,7 @@ pnpm lint
|
|||
- **[Ollama](https://ollama.com/)**: 本地模型部署方案
|
||||
|
||||
### 数据存储
|
||||
- **[PostgreSQL](https://www.postgresql.org/)**: 强大的关系型数据库
|
||||
- **[Supabase](https://supabase.com/)**: 提供便捷的后端即服务支持
|
||||
- **[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)**: 浏览器本地存储方案
|
||||
|
||||
|
|
@ -821,20 +698,22 @@ pnpm lint
|
|||
|
||||
目前 XCodeReviewer 定位为快速原型验证阶段,功能需要逐渐完善,根据项目后续发展和大家的建议,未来开发计划如下(尽快实现):
|
||||
|
||||
- **✅ 多平台LLM支持**: 已实现 10+ 主流平台API调用功能(Gemini、OpenAI、Claude、通义千问、DeepSeek、智谱AI、Kimi、文心一言、MiniMax、豆包、Ollama本地大模型),支持用户自由配置和切换
|
||||
- **✅ 本地模型支持**: 已加入对 Ollama 本地大模型的调用功能,满足数据隐私需求
|
||||
- **✅ 可视化配置管理**: 已实现运行时配置系统,支持在浏览器中直接配置所有 LLM 参数、API Keys,支持 API 中转站,无需重新构建镜像
|
||||
- **✅ 专业报告文件生成**: 根据不同的需求生成相关格式的专业审计报告文件,支持文件报告格式定制等
|
||||
- **✅ 全平台中转站支持**: 所有 LLM 平台均支持 API 中转站、自建服务和 OpenRouter,支持自定义请求头,覆盖 99.9% 的使用场景
|
||||
- **🚧 CI/CD 集成与 PR 自动审查**: 计划实现 GitHub/GitLab CI 集成,支持 PR 自动触发审查、智能评论、质量门禁、增量分析等完整的代码审查工作流
|
||||
- **Multi-Agent Collaboration**: 考虑引入多智能体协作架构,实现`Agent+人工对话`反馈功能,包括多轮对话流程展示、人工干预等,以获得更清晰、透明、可监督的审计过程
|
||||
- **审计标准自定义**: 支持通过 YAML/JSON 定义团队特定的编码规范和审计规则,提供常见框架的最佳实践模板,结合强化学习和监督学习微调,获得更符合需求和标准的审计结果
|
||||
---
|
||||
- **✅ 多平台LLM支持**: 已实现 10+ 主流平台API调用功能
|
||||
- **✅ 本地模型支持**: 已加入对 Ollama 本地大模型的调用功能
|
||||
- **✅ 可视化配置管理**: 已实现运行时配置系统
|
||||
- **✅ 专业报告文件生成**: 支持 JSON 和 PDF 格式导出
|
||||
- **✅ 前后端分离架构**: 采用 FastAPI + React 现代化架构
|
||||
- **✅ 用户认证系统**: JWT Token 认证和用户管理
|
||||
- **🚧 CI/CD 集成与 PR 自动审查**: 计划实现 GitHub/GitLab CI 集成
|
||||
- **Multi-Agent Collaboration**: 考虑引入多智能体协作架构
|
||||
- **审计标准自定义**: 支持通过 YAML/JSON 定义团队特定的编码规范
|
||||
|
||||
---
|
||||
|
||||
⭐ 如果这个项目对您有帮助,请给我们一个 **Star**!您的支持是我们不断前进的动力!
|
||||
|
||||
[](https://star-history.com/#lintsinghua/XCodeReviewer&Date)
|
||||
|
||||
---
|
||||
|
||||
## 📄 免责声明 (Disclaimer)
|
||||
|
|
@ -850,7 +729,7 @@ pnpm lint
|
|||
- 受法律法规限制不得外传的代码
|
||||
- 客户或第三方的专有代码(未经授权)
|
||||
- 用户**必须自行评估代码的敏感性**,对上传代码及其可能导致的信息泄露承担全部责任。
|
||||
- **建议**:对于敏感代码,请等待本项目未来支持本地模型部署功能,或使用私有部署的LLM服务。
|
||||
- **建议**:对于敏感代码,请使用 Ollama 本地模型部署功能,或使用私有部署的LLM服务。
|
||||
- 项目作者、贡献者和维护者**对因用户上传敏感代码导致的任何信息泄露、知识产权侵权、法律纠纷或其他损失不承担任何责任**。
|
||||
|
||||
#### 2. **非专业建议 (Non-Professional Advice)**
|
||||
|
|
@ -858,11 +737,11 @@ pnpm lint
|
|||
- 用户必须结合人工审查、专业工具及其他可靠资源,对关键代码(尤其是涉及安全、金融、医疗等高风险领域)进行全面验证。
|
||||
|
||||
#### 3. **无担保与免责 (No Warranty and Liability Disclaimer)**
|
||||
- 本项目以“原样”形式提供,**不附带任何明示或默示担保**,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
- 本项目以"原样"形式提供,**不附带任何明示或默示担保**,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
- 作者、贡献者和维护者**不对任何直接、间接、附带、特殊、惩戒性或后果性损害承担责任**,包括但不限于数据丢失、系统中断、安全漏洞或商业损失,即使已知此类风险存在。
|
||||
|
||||
#### 4. **AI 分析局限性 (Limitations of AI Analysis)**
|
||||
- 本工具依赖 Google Gemini 等 AI 模型,分析结果可能包含**错误、遗漏或不准确信息**,无法保证100% 可靠性。
|
||||
- 本工具依赖多种 AI 模型,分析结果可能包含**错误、遗漏或不准确信息**,无法保证100% 可靠性。
|
||||
- AI 输出**不能替代人类专家判断**,用户应对最终代码质量及应用后果全权负责。
|
||||
|
||||
#### 5. **第三方服务与数据隐私 (Third-Party Services and Data Privacy)**
|
||||
|
|
@ -870,7 +749,6 @@ pnpm lint
|
|||
- **代码传输说明**:用户提交的代码将通过API发送到所选LLM服务商进行分析,传输过程和数据处理遵循各服务商的隐私政策。
|
||||
- 用户需自行获取、管理 API 密钥,本项目**不存储、传输或处理用户的API密钥和敏感信息**。
|
||||
- 第三方服务的可用性、准确性、隐私保护、数据留存政策或中断风险,由服务提供商负责,本项目作者不承担任何连带责任。
|
||||
- **数据留存警告**:不同LLM服务商对API请求数据的留存和使用政策各不相同,请用户在使用前仔细阅读所选服务商的隐私政策和使用条款。
|
||||
|
||||
#### 6. **用户责任 (User Responsibilities)**
|
||||
- 用户在使用前须确保其代码不侵犯第三方知识产权,不包含保密信息,并严格遵守开源许可证及相关法规。
|
||||
|
|
@ -879,7 +757,7 @@ pnpm lint
|
|||
- 确保拥有代码的使用和分析权限
|
||||
- 遵守所在国家/地区关于数据保护和隐私的法律法规
|
||||
- 遵守公司或组织的保密协议和安全政策
|
||||
- **严禁将本工具用于非法、恶意或损害他人权益的活动**,用户对所有使用后果承担全部法律与经济责任。作者、贡献者和维护者对此类活动及其后果**不承担任何责任**,并保留追究滥用者的权利。
|
||||
- **严禁将本工具用于非法、恶意或损害他人权益的活动**,用户对所有使用后果承担全部法律与经济责任。
|
||||
|
||||
#### 7. **开源贡献 (Open Source Contributions)**
|
||||
- 贡献者的代码、内容或建议**不代表项目官方观点**,其准确性、安全性及合规性由贡献者自行负责。
|
||||
|
|
|
|||
890
README_EN.md
890
README_EN.md
|
|
@ -1,890 +0,0 @@
|
|||
# XCodeReviewer - Your Intelligent Code Audit Partner 🚀
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<img src="public/images/logo.png" alt="XCodeReviewer Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<p>
|
||||
<a href="README.md">中文</a> •
|
||||
<a href="README_EN.md">English</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://vitejs.dev/)
|
||||
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/stargazers)
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/network/members)
|
||||
|
||||
[](https://github.com/lintsinghua/lintsinghua.github.io/issues/1)
|
||||
|
||||
</div>
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<a href="https://github.com/lintsinghua/XCodeReviewer">
|
||||
<img src="public/star-me.svg" alt="Star this project" style="width: 100%; height: auto; display: block; margin: 0 auto;" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**XCodeReviewer** is a modern code audit platform powered by Large Language Models (LLM), designed to provide developers with intelligent, comprehensive, and in-depth code quality analysis and review services.
|
||||
|
||||
#### 🌐 Online Demo
|
||||
|
||||
No deployment needed, access the online demo directly (data stored locally in browser, all core features supported):
|
||||
|
||||
**[https://xcodereviewer-preview.vercel.app](https://xcodereviewer-preview.vercel.app)**
|
||||
|
||||
## 🌟 Why Choose XCodeReviewer?
|
||||
|
||||
In the fast-paced world of software development, ensuring code quality is crucial. Traditional code audit tools are rigid and inefficient, while manual audits are time-consuming and labor-intensive. XCodeReviewer leverages the powerful capabilities of LLM to revolutionize the way code reviews are conducted:
|
||||
|
||||

|
||||
|
||||
<div div align="center">
|
||||
<em>
|
||||
System Architecture Diagram of XCodeReviewer
|
||||
</em>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
- **🤖 AI-Driven Deep Analysis**: Beyond traditional static analysis, understands code intent and discovers deep logical issues.
|
||||
- **🎯 Multi-dimensional, Comprehensive Assessment**: From **security**, **performance**, **maintainability** to **code style**, providing 360-degree quality evaluation.
|
||||
- **💡 Clear, Actionable Fix Suggestions**: Innovative **What-Why-How** approach that not only tells you "what" the problem is, but also explains "why" and provides "how to fix" with specific code examples.
|
||||
- **✅ Multi-Platform LLM/Local Model Support**: Implemented API calling functionality for 10+ mainstream platforms (Gemini, OpenAI, Claude, Qwen, DeepSeek, Zhipu AI, Kimi, ERNIE, MiniMax, Doubao, Ollama Local Models), with support for free configuration and switching
|
||||
- **⚙️ Visual Runtime Configuration**: Configure all LLM parameters and API Keys directly in the browser without rebuilding images. Supports API relay services, with configurations saved locally in the browser for security and convenience.
|
||||
- **✨ Modern, Beautiful User Interface**: Built with React + TypeScript, providing a smooth and intuitive user experience.
|
||||
|
||||
## 🎬 Project Demo
|
||||
|
||||
### Main Feature Interfaces
|
||||
|
||||
#### 📊 Intelligent Dashboard
|
||||

|
||||
*Real-time display of project statistics, quality trends, and system performance, providing comprehensive code audit overview*
|
||||
|
||||
#### ⚡ Instant Analysis
|
||||

|
||||
*Support for quick code snippet analysis with detailed What-Why-How explanations and fix suggestions*
|
||||
|
||||
#### 🚀 Project Management
|
||||

|
||||
*Integrated GitHub/GitLab repositories, supporting multi-language project audits and batch code analysis*
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### ☁️ Vercel One-Click Deployment
|
||||
|
||||
Perfect for quick deployment and testing without a server, with global CDN acceleration.
|
||||
|
||||
#### Method 1: One-Click Deploy Button (Recommended) ⭐
|
||||
|
||||
Click the button below to deploy directly to Vercel:
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/lintsinghua/XCodeReviewer)
|
||||
|
||||
#### Method 2: Deploy via Vercel CLI
|
||||
|
||||
```bash
|
||||
# 1. Install Vercel CLI
|
||||
npm i -g vercel
|
||||
|
||||
# 2. Login to Vercel
|
||||
vercel login
|
||||
|
||||
# 3. Deploy project
|
||||
vercel
|
||||
|
||||
# 4. Deploy to production
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
#### Method 3: Deploy via Vercel Dashboard
|
||||
|
||||
1. Visit [Vercel Dashboard](https://vercel.com/dashboard)
|
||||
2. Click "Add New..." → "Project"
|
||||
3. Import your GitHub repository
|
||||
4. Vercel will automatically detect Vite project configuration
|
||||
5. Configure environment variables (minimum required):
|
||||
```
|
||||
VITE_LLM_PROVIDER=your_llm_provider
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
VITE_USE_LOCAL_DB=true
|
||||
```
|
||||
6. Click "Deploy"
|
||||
|
||||
**✨ Vercel Deployment Advantages**:
|
||||
- ✅ Global CDN acceleration for fast access
|
||||
- ✅ Automatic HTTPS and domain configuration
|
||||
- ✅ Zero configuration, ready to use
|
||||
- ✅ Custom domain support
|
||||
- ✅ Automatic deployment (auto-update on Git push)
|
||||
|
||||
**✨ Database Mode**:
|
||||
- Automatically uses **local database mode** (IndexedDB) by default, data stored in browser
|
||||
- No database configuration needed, ready to use out of the box
|
||||
- To use Supabase cloud database, configure environment variables
|
||||
|
||||
**⚠️ Important Notes**:
|
||||
- Vercel is primarily for frontend deployment; backend APIs need separate deployment
|
||||
- After deployment, configure runtime settings at `/admin` page
|
||||
|
||||
---
|
||||
|
||||
### 🐳 Docker Deployment (Recommended for Production)
|
||||
|
||||
#### Method 1: Use Published Image (Easiest) ⭐
|
||||
|
||||
Directly use the latest published Docker image, supports x86, ARM64 (Mac M-series), and ARMv7 architectures:
|
||||
|
||||
```bash
|
||||
# 1. Pull the latest image
|
||||
docker pull ghcr.io/lintsinghua/xcodereviewer:latest
|
||||
|
||||
# 2. Run container
|
||||
docker run -d \
|
||||
-p 8888:80 \
|
||||
--name xcodereviewer \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/lintsinghua/xcodereviewer:latest
|
||||
|
||||
# 3. Access the application
|
||||
# Open http://localhost:8888 in your browser
|
||||
```
|
||||
|
||||
**Use specific version**:
|
||||
```bash
|
||||
# Pull specific version (e.g., v1.1.0)
|
||||
docker pull ghcr.io/lintsinghua/xcodereviewer:v1.1.0
|
||||
|
||||
# Run
|
||||
docker run -d -p 8888:80 --name xcodereviewer ghcr.io/lintsinghua/xcodereviewer:v1.1.0
|
||||
```
|
||||
|
||||
#### Method 2: Local Build (Optional)
|
||||
|
||||
If you need custom build:
|
||||
|
||||
```bash
|
||||
# 1. Clone the project
|
||||
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||
cd XCodeReviewer
|
||||
|
||||
# 2. Build and start with Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Access the application
|
||||
# Open http://localhost:8888 in your browser
|
||||
```
|
||||
|
||||
**✨ Runtime Configuration (Recommended)**
|
||||
|
||||
After Docker deployment, you can configure all settings directly in the browser without rebuilding the image:
|
||||
|
||||
1. Visit `http://localhost:8888/admin` (System Management page)
|
||||
2. Configure LLM API Keys and other parameters in the "System Configuration" tab
|
||||
3. Click save and refresh the page to use
|
||||
|
||||
> 📖 **For detailed configuration instructions, see**: [System Configuration Guide](#system-configuration-first-time-setup)
|
||||
|
||||
### 💻 Local Development Deployment
|
||||
|
||||
Suitable for development or custom modifications.
|
||||
|
||||
#### Requirements
|
||||
- Node.js 18+
|
||||
- pnpm 8+ (recommended) or npm/yarn
|
||||
|
||||
#### Quick Setup
|
||||
|
||||
```bash
|
||||
# 1. Clone the project
|
||||
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||
cd XCodeReviewer
|
||||
|
||||
# 2. Install dependencies
|
||||
pnpm install # or npm install / yarn install
|
||||
|
||||
# 3. Configure environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env file, configure required parameters (see configuration guide below)
|
||||
|
||||
# 4. Start development server
|
||||
pnpm dev
|
||||
|
||||
# 5. Access the application
|
||||
# Open http://localhost:5173 in your browser
|
||||
```
|
||||
|
||||
#### Core Configuration
|
||||
|
||||
Edit `.env` file and configure the following required parameters:
|
||||
|
||||
```env
|
||||
# ========== Required Configuration ==========
|
||||
# LLM Provider (gemini|openai|claude|qwen|deepseek|zhipu|moonshot|baidu|minimax|doubao|ollama)
|
||||
VITE_LLM_PROVIDER=gemini
|
||||
# Corresponding API Key
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# ========== Database Configuration (Choose One) ==========
|
||||
# Option 1: Local Database (Recommended, ready to use)
|
||||
VITE_USE_LOCAL_DB=true
|
||||
|
||||
# Option 2: Supabase Cloud Database (Multi-device sync)
|
||||
# VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
# VITE_SUPABASE_ANON_KEY=your_anon_key
|
||||
|
||||
# Option 3: Demo Mode (No database, data not persistent)
|
||||
|
||||
# ========== Optional Configuration ==========
|
||||
# GitHub Integration (for repository analysis)
|
||||
# VITE_GITHUB_TOKEN=your_github_token
|
||||
|
||||
# Output Language (zh-CN: Chinese | en-US: English)
|
||||
VITE_OUTPUT_LANGUAGE=en-US
|
||||
|
||||
# Analysis Parameters
|
||||
VITE_MAX_ANALYZE_FILES=40 # Max files per analysis
|
||||
VITE_LLM_CONCURRENCY=2 # Concurrent requests
|
||||
VITE_LLM_GAP_MS=500 # Request interval (ms)
|
||||
```
|
||||
|
||||
#### Advanced Configuration
|
||||
|
||||
For timeout or connection issues, adjust these parameters:
|
||||
|
||||
```env
|
||||
VITE_LLM_TIMEOUT=300000 # Increase timeout
|
||||
VITE_LLM_BASE_URL=https://your-proxy.com/v1 # Use proxy or relay service
|
||||
VITE_LLM_CONCURRENCY=1 # Reduce concurrency
|
||||
VITE_LLM_GAP_MS=1000 # Increase request interval
|
||||
```
|
||||
|
||||
**Custom Headers Example** (for special relay services):
|
||||
|
||||
```env
|
||||
# JSON format string
|
||||
VITE_LLM_CUSTOM_HEADERS='{"X-API-Version":"v1","X-Custom-Auth":"token123"}'
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
<details>
|
||||
<summary><b>How to quickly switch LLM platforms?</b></summary>
|
||||
|
||||
**Method 1: Browser Configuration (Recommended)**
|
||||
|
||||
1. Visit `http://localhost:8888/admin` System Management page
|
||||
2. Select different LLM provider in the "System Configuration" tab
|
||||
3. Enter the corresponding API Key
|
||||
4. Save and refresh the page
|
||||
|
||||
**Method 2: Environment Variable Configuration**
|
||||
|
||||
Modify configuration in `.env`:
|
||||
|
||||
```env
|
||||
# Switch to OpenAI
|
||||
VITE_LLM_PROVIDER=openai
|
||||
VITE_OPENAI_API_KEY=your_key
|
||||
|
||||
# Switch to Qwen
|
||||
VITE_LLM_PROVIDER=qwen
|
||||
VITE_QWEN_API_KEY=your_key
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>What to do about request timeouts?</b></summary>
|
||||
|
||||
1. Increase timeout: `VITE_LLM_TIMEOUT=300000`
|
||||
2. Use proxy: Configure `VITE_LLM_BASE_URL`
|
||||
3. Switch to Chinese platforms: Qwen, DeepSeek, Zhipu AI, etc.
|
||||
4. Reduce concurrency: `VITE_LLM_CONCURRENCY=1`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to choose database mode?</b></summary>
|
||||
|
||||
**Local Mode (Recommended)**: Data stored in browser IndexedDB, ready to use, privacy-secure
|
||||
```env
|
||||
VITE_USE_LOCAL_DB=true
|
||||
```
|
||||
|
||||
**Cloud Mode**: Data stored in Supabase, multi-device sync
|
||||
```env
|
||||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your_key
|
||||
```
|
||||
|
||||
**Demo Mode**: No database configuration, data not persistent
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to use Ollama local models?</b></summary>
|
||||
|
||||
```bash
|
||||
# 1. Install Ollama
|
||||
curl -fsSL https://ollama.com/install.sh | sh # macOS/Linux
|
||||
# Windows: Visit https://ollama.com/download
|
||||
|
||||
# 2. Pull model
|
||||
ollama pull llama3 # or codellama, qwen2.5, deepseek-coder
|
||||
|
||||
# 3. Configure XCodeReviewer
|
||||
# In .env:
|
||||
VITE_LLM_PROVIDER=ollama
|
||||
VITE_LLM_MODEL=llama3
|
||||
VITE_LLM_BASE_URL=http://localhost:11434/v1
|
||||
```
|
||||
|
||||
Recommended models: `llama3` (general), `codellama` (code-specific), `qwen2.5` (Chinese)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Baidu ERNIE API Key format?</b></summary>
|
||||
|
||||
Baidu requires both API Key and Secret Key, separated by colon:
|
||||
```env
|
||||
VITE_LLM_PROVIDER=baidu
|
||||
VITE_BAIDU_API_KEY=your_api_key:your_secret_key
|
||||
```
|
||||
Get from: https://console.bce.baidu.com/qianfan/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to use API relay services?</b></summary>
|
||||
|
||||
Many users use API relay services to access LLMs (more stable and cheaper). Configuration method:
|
||||
|
||||
1. Visit System Management page (`/admin`)
|
||||
2. In the "System Configuration" tab:
|
||||
- Select LLM provider (e.g., OpenAI)
|
||||
- **API Base URL**: Enter relay service address (e.g., `https://your-proxy.com/v1`)
|
||||
- **API Key**: Enter the key provided by the relay service (not the official key)
|
||||
3. Save and refresh the page
|
||||
|
||||
**Notes**:
|
||||
- Relay service URLs usually end with `/v1` (OpenAI-compatible format)
|
||||
- Use the relay service's API Key, not the official one
|
||||
- Confirm that the relay service supports your chosen AI model
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to backup local database?</b></summary>
|
||||
|
||||
Local data is stored in browser IndexedDB:
|
||||
- Export as JSON file from "System Management" page
|
||||
- Import JSON file to restore data
|
||||
- Note: Clearing browser data will delete all local data
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to set output language?</b></summary>
|
||||
|
||||
```env
|
||||
VITE_OUTPUT_LANGUAGE=zh-CN # Chinese (default)
|
||||
VITE_OUTPUT_LANGUAGE=en-US # English
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to configure multiple platforms and switch quickly?</b></summary>
|
||||
|
||||
Pre-configure all platform keys in `.env`, then just modify `VITE_LLM_PROVIDER` to switch:
|
||||
```env
|
||||
VITE_LLM_PROVIDER=gemini # Currently active platform
|
||||
|
||||
# Pre-configure all platforms
|
||||
VITE_GEMINI_API_KEY=key1
|
||||
VITE_OPENAI_API_KEY=key2
|
||||
VITE_QWEN_API_KEY=key3
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How to view system logs and debug information?</b></summary>
|
||||
|
||||
XCodeReviewer has a built-in logging system that records core operations and errors:
|
||||
|
||||
**View Logs**:
|
||||
- Navigation bar -> System Logs
|
||||
- Or visit: `http://localhost:5173/logs` (dev) / `http://localhost:8888/logs` (prod)
|
||||
|
||||
**Recorded Content**:
|
||||
- ✅ Core user operations (create project, audit tasks, config changes, etc.)
|
||||
- ✅ Failed API requests and errors
|
||||
- ✅ Console errors (auto-captured)
|
||||
- ✅ Unhandled exceptions
|
||||
|
||||
**Features**:
|
||||
- Log filtering and search
|
||||
- Export logs (JSON/CSV)
|
||||
- View error details
|
||||
|
||||
**Manual Logging**:
|
||||
```typescript
|
||||
import { logger, LogCategory } from '@/shared/utils/logger';
|
||||
|
||||
// Log user actions
|
||||
logger.logUserAction('Create Project', { projectName, projectType });
|
||||
logger.logUserAction('Start Audit', { taskId, fileCount });
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 🔑 Getting API Keys
|
||||
|
||||
#### Supported LLM Platforms
|
||||
|
||||
XCodeReviewer supports 10+ mainstream LLM platforms, choose freely based on your needs:
|
||||
|
||||
| Platform Type | Platform Name | Features | Get API Key |
|
||||
|--------------|---------------|----------|-------------|
|
||||
| **International** | Google Gemini | Generous free tier, recommended | [Get](https://makersuite.google.com/app/apikey) |
|
||||
| | OpenAI GPT | Stable, best performance | [Get](https://platform.openai.com/api-keys) |
|
||||
| | Anthropic Claude | Strong code understanding | [Get](https://console.anthropic.com/) |
|
||||
| | DeepSeek | Cost-effective | [Get](https://platform.deepseek.com/) |
|
||||
| **Chinese** | Alibaba Qwen | Fast domestic access | [Get](https://dashscope.console.aliyun.com/) |
|
||||
| | Zhipu AI (GLM) | Good Chinese support | [Get](https://open.bigmodel.cn/) |
|
||||
| | Moonshot (Kimi) | Long context | [Get](https://platform.moonshot.cn/) |
|
||||
| | Baidu ERNIE | Enterprise service | [Get](https://console.bce.baidu.com/qianfan/) |
|
||||
| | MiniMax | Multimodal | [Get](https://www.minimaxi.com/) |
|
||||
| | Bytedance Doubao | Cost-effective | [Get](https://console.volcengine.com/ark) |
|
||||
| **Local** | Ollama | Fully local, privacy-secure | [Install](https://ollama.com/) |
|
||||
|
||||
#### Configuration Example
|
||||
|
||||
```env
|
||||
# Universal configuration (recommended)
|
||||
VITE_LLM_PROVIDER=gemini
|
||||
VITE_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# Or use platform-specific configuration
|
||||
VITE_GEMINI_API_KEY=your_gemini_key
|
||||
VITE_OPENAI_API_KEY=your_openai_key
|
||||
# ... More platforms in .env.example
|
||||
```
|
||||
|
||||
#### Supabase Configuration (Optional)
|
||||
|
||||
For cloud data sync:
|
||||
1. Visit [Supabase](https://supabase.com/) to create a project
|
||||
2. Get URL and anonymous key
|
||||
3. Execute `supabase/migrations/full_schema.sql` in Supabase SQL Editor
|
||||
4. Configure in `.env`
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 Project Management</b></summary>
|
||||
|
||||
- **One-click Repository Integration**: Seamlessly connect with GitHub, GitLab, and other mainstream platforms.
|
||||
- **Multi-language "Full Stack" Support**: Covers popular languages like JavaScript, TypeScript, Python, Java, Go, Rust, and more.
|
||||
- **Flexible Branch Auditing**: Support for precise analysis of specified code branches.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>⚡ Instant Analysis</b></summary>
|
||||
|
||||
- **Code Snippet "Quick Paste"**: Directly paste code in the web interface for immediate analysis results.
|
||||
- **10+ Language Instant Support**: Meet your diverse code analysis needs.
|
||||
- **Millisecond Response**: Quickly get code quality scores and optimization suggestions.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧠 Intelligent Auditing</b></summary>
|
||||
|
||||
- **AI Deep Code Understanding**: Supports multiple mainstream LLM platforms (Gemini, OpenAI, Claude, Qwen, DeepSeek, etc.), providing intelligent analysis beyond keyword matching.
|
||||
- **Five Core Detection Dimensions**:
|
||||
- 🐛 **Potential Bugs**: Precisely capture logical errors, boundary conditions, and null pointer issues.
|
||||
- 🔒 **Security Vulnerabilities**: Identify SQL injection, XSS, sensitive information leakage, and other security risks.
|
||||
- ⚡ **Performance Bottlenecks**: Discover inefficient algorithms, memory leaks, and unreasonable asynchronous operations.
|
||||
- 🎨 **Code Style**: Ensure code follows industry best practices and unified standards.
|
||||
- 🔧 **Maintainability**: Evaluate code readability, complexity, and modularity.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>💡 Explainable Analysis (What-Why-How)</b></summary>
|
||||
|
||||
- **What**: Clearly identify problems in the code.
|
||||
- **Why**: Detailed explanation of potential risks and impacts the problem may cause.
|
||||
- **How**: Provide specific, directly usable code fix examples.
|
||||
- **Precise Code Location**: Quickly jump to the problematic line and column.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📊 Visual Reports</b></summary>
|
||||
|
||||
- **Code Quality Dashboard**: Provides comprehensive quality assessment from 0-100, making code health status clear at a glance.
|
||||
- **Multi-dimensional Issue Statistics**: Classify and count issues by type and severity.
|
||||
- **Quality Trend Analysis**: Display code quality changes over time through charts.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>⚙️ System Management</b></summary>
|
||||
|
||||
Visit `/admin` page for complete system configuration and data management features:
|
||||
|
||||
- **🔧 Visual Configuration Management** (Runtime Configuration):
|
||||
- 🎯 **LLM Configuration**: Configure API Keys, models, timeout, and other parameters directly in the browser
|
||||
- 🔑 **Platform Keys**: Manage API Keys for 10+ LLM platforms with quick switching support
|
||||
- ⚡ **Analysis Parameters**: Adjust concurrency, interval time, max files, etc.
|
||||
- 🌐 **API Relay Support**: Easily configure third-party API relay services
|
||||
- 💾 **Configuration Priority**: Runtime config > Build-time config, no need to rebuild images
|
||||
|
||||
- **💾 Database Management**:
|
||||
- 🏠 **Three Modes**: Local IndexedDB / Supabase Cloud / Demo Mode
|
||||
- 📤 **Export Backup**: Export data as JSON files
|
||||
- 📥 **Import Recovery**: Restore data from backup files
|
||||
- 🗑️ **Clear Data**: One-click cleanup of all local data
|
||||
- 📊 **Storage Monitoring**: Real-time view of storage space usage
|
||||
|
||||
- **📈 Data Overview**:
|
||||
- Complete statistics for projects, tasks, and issues
|
||||
- Visual charts showing quality trends
|
||||
- Storage usage analysis
|
||||
</details>
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Frontend Framework** | `React 18` `TypeScript` `Vite` | Modern frontend development stack with hot reload and type safety |
|
||||
| **UI Components** | `Tailwind CSS` `Radix UI` `Lucide React` | Responsive design, accessibility, rich icon library |
|
||||
| **Data Visualization** | `Recharts` | Professional chart library supporting multiple chart types |
|
||||
| **Routing** | `React Router v6` | Single-page application routing solution |
|
||||
| **State Management** | `React Hooks` `Sonner` | Lightweight state management and notification system |
|
||||
| **AI Engine** | `Multi-Platform LLM` | Supports 10+ mainstream platforms including Gemini, OpenAI, Claude, Qwen, DeepSeek |
|
||||
| **Data Storage** | `IndexedDB` `Supabase` `PostgreSQL` | Dual-mode support for local database + cloud database |
|
||||
| **Backend Service** | `Supabase` `PostgreSQL` | Full-stack backend-as-a-service with real-time database |
|
||||
| **HTTP Client** | `Axios` `Ky` | Modern HTTP request libraries |
|
||||
| **Code Quality** | `Biome` `Ast-grep` `TypeScript` | Code formatting, static analysis, and type checking |
|
||||
| **Build Tools** | `Vite` `PostCSS` `Autoprefixer` | Fast build tools and CSS processing |
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
XCodeReviewer/
|
||||
├── src/
|
||||
│ ├── app/ # Application configuration
|
||||
│ │ ├── App.tsx # Main application component
|
||||
│ │ ├── main.tsx # Application entry point
|
||||
│ │ └── routes.tsx # Route configuration
|
||||
│ ├── components/ # React components
|
||||
│ │ ├── layout/ # Layout components (Header, Footer, PageMeta)
|
||||
│ │ ├── ui/ # UI component library (based on Radix UI)
|
||||
│ │ ├── system/ # System configuration components
|
||||
│ │ ├── database/ # Database management components
|
||||
│ │ └── debug/ # Debug components
|
||||
│ ├── pages/ # Page components
|
||||
│ │ ├── Dashboard.tsx # Dashboard
|
||||
│ │ ├── Projects.tsx # Project management
|
||||
│ │ ├── InstantAnalysis.tsx # Instant analysis
|
||||
│ │ ├── AuditTasks.tsx # Audit tasks
|
||||
│ │ └── AdminDashboard.tsx # System management
|
||||
│ ├── features/ # Feature modules
|
||||
│ │ ├── analysis/ # Analysis related services
|
||||
│ │ │ └── services/ # AI code analysis engine
|
||||
│ │ └── projects/ # Project related services
|
||||
│ │ └── services/ # Repository scanning, ZIP file scanning
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config/ # Configuration files
|
||||
│ │ │ ├── database.ts # Unified database interface
|
||||
│ │ │ ├── localDatabase.ts # IndexedDB implementation
|
||||
│ │ │ └── env.ts # Environment variable configuration
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ ├── hooks/ # Custom React Hooks
|
||||
│ │ ├── utils/ # Utility functions
|
||||
│ │ │ └── initLocalDB.ts # Local database initialization
|
||||
│ │ └── constants/ # Constants definition
|
||||
│ └── assets/ # Static assets
|
||||
│ └── styles/ # Style files
|
||||
├── supabase/
|
||||
│ └── migrations/ # Database migration files
|
||||
├── public/
|
||||
│ └── images/ # Image resources
|
||||
├── scripts/ # Build and setup scripts
|
||||
└── rules/ # Code rules configuration
|
||||
```
|
||||
|
||||
## 🎯 Usage Guide
|
||||
|
||||
### System Configuration (First-Time Setup)
|
||||
|
||||
Visit `/admin` System Management page and configure in the "System Configuration" tab:
|
||||
|
||||
#### 1. **Configure LLM Provider**
|
||||
- Select the LLM platform you want to use (Gemini, OpenAI, Claude, etc.)
|
||||
- Enter API Key (supports universal Key or platform-specific Key)
|
||||
- Optional: Configure model name, API base URL (for relay services)
|
||||
|
||||
#### 2. **Configure API Relay Service** (if using)
|
||||
- Enter relay service address in "API Base URL" (e.g., `https://your-proxy.com/v1`)
|
||||
- Enter the API Key provided by the relay service
|
||||
- Save configuration
|
||||
|
||||
#### 3. **Adjust Analysis Parameters** (optional)
|
||||
- Max analyze files, concurrent requests, request interval
|
||||
- Output language (Chinese/English)
|
||||
|
||||
**After configuration, click "Save All Changes" and refresh the page to use.**
|
||||
|
||||
### Instant Code Analysis
|
||||
1. Visit the `/instant-analysis` page
|
||||
2. Select programming language (supports 10+ languages)
|
||||
3. Paste code or upload file
|
||||
4. Click "Start Analysis" to get AI analysis results
|
||||
5. View detailed issue reports and fix suggestions
|
||||
|
||||
### Project Management
|
||||
1. Visit the `/projects` page
|
||||
2. Click "New Project" to create a project
|
||||
3. Configure repository URL and scan parameters
|
||||
4. Start code audit task
|
||||
5. View audit results and issue statistics
|
||||
|
||||
### Audit Tasks
|
||||
1. Create audit tasks in project detail page
|
||||
2. Select scan branch and exclusion patterns
|
||||
3. Configure analysis depth and scope
|
||||
4. Monitor task execution status
|
||||
5. View detailed issue reports
|
||||
|
||||
### Audit Report Export
|
||||
1. Click the "Export Report" button on the task detail page
|
||||
2. Choose export format:
|
||||
- **JSON Format**: Structured data, suitable for programmatic processing and integration
|
||||
- **PDF Format**: Professional report, suitable for printing and sharing (via browser print function)
|
||||
3. JSON reports contain complete task information, issue details, and statistical data
|
||||
4. PDF reports provide beautiful visual presentation with full Chinese character support
|
||||
5. Report contents include: project information, audit statistics, issue details (categorized by severity), fix suggestions, etc.
|
||||
|
||||
**PDF Export Tips:**
|
||||
- After clicking "Export PDF", the browser print dialog will appear
|
||||
- It's recommended to **uncheck the "Headers and footers" option** in print settings for a cleaner report (to avoid displaying URLs and other information)
|
||||
- Select "Save as PDF" in the print dialog to save the report file
|
||||
|
||||
### Build and Deploy
|
||||
```bash
|
||||
# Development mode
|
||||
pnpm dev
|
||||
|
||||
# Build production version
|
||||
pnpm build
|
||||
|
||||
# Preview build results
|
||||
pnpm preview
|
||||
|
||||
# Code linting
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Core LLM Configuration
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `VITE_LLM_PROVIDER` | ✅ | `gemini` | LLM provider: `gemini`\|`openai`\|`claude`\|`qwen`\|`deepseek`\|`zhipu`\|`moonshot`\|`baidu`\|`minimax`\|`doubao`\|`ollama` |
|
||||
| `VITE_LLM_API_KEY` | ✅ | - | Universal API Key (higher priority than platform-specific config) |
|
||||
| `VITE_LLM_MODEL` | ❌ | Auto | Model name (uses platform default if not specified) |
|
||||
| `VITE_LLM_BASE_URL` | ❌ | - | Custom API endpoint (**supports relay services for all platforms**, proxy, or private deployment) |
|
||||
| `VITE_LLM_TIMEOUT` | ❌ | `150000` | Request timeout (milliseconds) |
|
||||
| `VITE_LLM_TEMPERATURE` | ❌ | `0.2` | Temperature parameter (0.0-2.0), controls output randomness |
|
||||
| `VITE_LLM_MAX_TOKENS` | ❌ | `4096` | Maximum output tokens |
|
||||
| `VITE_LLM_CUSTOM_HEADERS` | ❌ | - | Custom HTTP headers (JSON string format), for special relay services or self-hosted instances |
|
||||
|
||||
> 💡 **API Format Support**: XCodeReviewer supports 3 mainstream API formats:
|
||||
> - **OpenAI-Compatible Format** (Most Common): Works with most relay services and OpenRouter
|
||||
> - **Gemini Format**: Google Gemini official and compatible services
|
||||
> - **Claude Format**: Anthropic Claude official and compatible services
|
||||
>
|
||||
> Simply select the corresponding LLM provider, enter the relay service address and Key. The custom headers feature can meet additional requirements of special relay services.
|
||||
|
||||
#### Platform-Specific API Key Configuration (Optional)
|
||||
| Variable | Description | Special Requirements |
|
||||
|----------|-------------|---------------------|
|
||||
| `VITE_GEMINI_API_KEY` | Google Gemini API Key | - |
|
||||
| `VITE_GEMINI_MODEL` | Gemini model (default: gemini-1.5-flash) | - |
|
||||
| `VITE_OPENAI_API_KEY` | OpenAI API Key | - |
|
||||
| `VITE_OPENAI_MODEL` | OpenAI model (default: gpt-4o-mini) | - |
|
||||
| `VITE_OPENAI_BASE_URL` | OpenAI custom endpoint | For relay services |
|
||||
| `VITE_CLAUDE_API_KEY` | Anthropic Claude API Key | - |
|
||||
| `VITE_CLAUDE_MODEL` | Claude model (default: claude-3-5-sonnet-20241022) | - |
|
||||
| `VITE_QWEN_API_KEY` | Alibaba Qwen API Key | - |
|
||||
| `VITE_QWEN_MODEL` | Qwen model (default: qwen-turbo) | - |
|
||||
| `VITE_DEEPSEEK_API_KEY` | DeepSeek API Key | - |
|
||||
| `VITE_DEEPSEEK_MODEL` | DeepSeek model (default: deepseek-chat) | - |
|
||||
| `VITE_ZHIPU_API_KEY` | Zhipu AI API Key | - |
|
||||
| `VITE_ZHIPU_MODEL` | Zhipu model (default: glm-4-flash) | - |
|
||||
| `VITE_MOONSHOT_API_KEY` | Moonshot Kimi API Key | - |
|
||||
| `VITE_MOONSHOT_MODEL` | Kimi model (default: moonshot-v1-8k) | - |
|
||||
| `VITE_BAIDU_API_KEY` | Baidu ERNIE API Key | ⚠️ Format: `API_KEY:SECRET_KEY` |
|
||||
| `VITE_BAIDU_MODEL` | ERNIE model (default: ERNIE-3.5-8K) | - |
|
||||
| `VITE_MINIMAX_API_KEY` | MiniMax API Key | - |
|
||||
| `VITE_MINIMAX_MODEL` | MiniMax model (default: abab6.5-chat) | - |
|
||||
| `VITE_DOUBAO_API_KEY` | Bytedance Doubao API Key | - |
|
||||
| `VITE_DOUBAO_MODEL` | Doubao model (default: doubao-pro-32k) | - |
|
||||
|
||||
#### Database Configuration (Optional)
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `VITE_SUPABASE_URL` | ❌ | Supabase project URL (for data persistence) |
|
||||
| `VITE_SUPABASE_ANON_KEY` | ❌ | Supabase anonymous key |
|
||||
|
||||
> 💡 **Note**: Without Supabase config, system runs in demo mode without data persistence
|
||||
|
||||
#### Git Repository Integration Configuration
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `VITE_GITHUB_TOKEN` | ✅ | GitHub Personal Access Token |
|
||||
| `VITE_GITLAB_TOKEN` | ✅ | GitLab Personal Access Token |
|
||||
|
||||
#### Analysis Behavior Configuration
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `VITE_MAX_ANALYZE_FILES` | `40` | Maximum files per analysis |
|
||||
| `VITE_LLM_CONCURRENCY` | `2` | LLM concurrent requests (reduce to avoid rate limiting) |
|
||||
| `VITE_LLM_GAP_MS` | `500` | Gap between LLM requests (milliseconds, increase to avoid rate limiting) |
|
||||
|
||||
#### Application Configuration
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `VITE_APP_ID` | `xcodereviewer` | Application identifier |
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome all forms of contributions! Whether it's submitting issues, creating PRs, or improving documentation, every contribution is important to us. Please contact us for detailed information.
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Fork** this project
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Create a **Pull Request**
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
### Core Technology
|
||||
- **[React](https://reactjs.org/)** & **[Vite](https://vitejs.dev/)**: Modern frontend development experience
|
||||
- **[TypeScript](https://www.typescriptlang.org/)**: Type safety guarantee
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)**: Modern CSS framework
|
||||
- **[Radix UI](https://www.radix-ui.com/)**: Accessible UI component library
|
||||
|
||||
### AI Platform Support
|
||||
- **[Google Gemini AI](https://ai.google.dev/)**: Powerful AI analysis capabilities
|
||||
- **[OpenAI](https://openai.com/)**: GPT series models support
|
||||
- **[Anthropic Claude](https://www.anthropic.com/)**: Claude models support
|
||||
- **[DeepSeek](https://www.deepseek.com/)**: Domestic AI model support
|
||||
- **[Alibaba Qwen](https://tongyi.aliyun.com/)**: Enterprise AI service
|
||||
- **[Zhipu AI](https://www.zhipuai.cn/)**: GLM series models
|
||||
- **[Moonshot AI](https://www.moonshot.cn/)**: Kimi model support
|
||||
- **[Ollama](https://ollama.com/)**: Local model deployment solution
|
||||
|
||||
### Data Storage
|
||||
- **[Supabase](https://supabase.com/)**: Convenient backend-as-a-service support
|
||||
- **[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)**: Browser local storage solution
|
||||
|
||||
### Functional Components
|
||||
- **[Recharts](https://recharts.org/)**: Professional chart components
|
||||
- **[Lucide Icons](https://lucide.dev/)**: Beautiful icon library
|
||||
- **[Sonner](https://sonner.emilkowal.ski/)**: Elegant notification component
|
||||
- **[fflate](https://github.com/101arrowz/fflate)**: ZIP file processing
|
||||
|
||||
### Special Thanks
|
||||
- Thanks to all contributors who submitted Issues and Pull Requests
|
||||
- Thanks to all developers who starred this project
|
||||
- Thanks to the open source community for their selfless sharing
|
||||
- And all the authors of open source software used in this project!
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
Thanks to these amazing contributors who make XCodeReviewer better!
|
||||
|
||||
[](https://github.com/lintsinghua/XCodeReviewer/graphs/contributors)
|
||||
|
||||
## 📞 Contact Us
|
||||
|
||||
- **Project Link**: [https://github.com/lintsinghua/XCodeReviewer](https://github.com/lintsinghua/XCodeReviewer)
|
||||
- **Issue Reports**: [Issues](https://github.com/lintsinghua/XCodeReviewer/issues)
|
||||
- **Author Email**: lintsinghua@qq.com
|
||||
|
||||
## 🎯 Future Plans
|
||||
|
||||
Currently, XCodeReviewer is in rapid prototype validation stage. Based on project development and community feedback, our roadmap includes:
|
||||
|
||||
- ✅ **Multi-Platform LLM Support**: Implemented API integration for 10+ mainstream platforms (Gemini, OpenAI, Claude, Qwen, DeepSeek, Zhipu AI, Kimi, ERNIE, MiniMax, Doubao, Ollama), with flexible configuration and switching
|
||||
- ✅ **Local Model Support**: Added Ollama local model integration to meet data privacy requirements
|
||||
- ✅ **Visual Configuration Management**: Implemented runtime configuration system supporting browser-based configuration of all LLM parameters and API Keys, API relay service support, no need to rebuild images
|
||||
- ✅ **Universal Relay Service Support**: All LLM platforms support API relay services, self-hosted instances, and OpenRouter, with custom headers support, covering 99.9% of use cases
|
||||
- ✅ **Professional Report Generation**: Generate professional audit reports in various formats based on different needs, with customizable templates and format configurations
|
||||
- 🚧 **CI/CD Integration & PR Auto-Review**: Plan to implement GitHub/GitLab CI integration, supporting automatic PR-triggered reviews, intelligent comments, quality gates, incremental analysis, and complete code review workflows
|
||||
- **Multi-Agent Collaboration**: Introduce multi-agent architecture with Agent + Human Dialogue feedback for more transparent and controllable audit processes
|
||||
- **Custom Audit Standards**: Support custom audit rule configuration via YAML/JSON, provide best practice templates for common frameworks
|
||||
|
||||
---
|
||||
|
||||
⭐ If this project helps you, please give us a **Star**! Your support is our motivation to keep moving forward!
|
||||
|
||||
[](https://star-history.com/#lintsinghua/XCodeReview)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 📄 Disclaimer
|
||||
|
||||
This disclaimer is intended to clarify the responsibilities and risks associated with the use of this open source project and to protect the legitimate rights and interests of project authors, contributors and maintainers. The code, tools and related content provided by this open source project are for reference and learning purposes only.
|
||||
|
||||
#### 1. **Code Privacy and Security Warning**
|
||||
- ⚠️ **Important Notice**: This tool analyzes code by calling third-party LLM service provider APIs, which means **your code will be sent to the servers of the selected LLM service provider**.
|
||||
- **It is strictly prohibited to upload the following types of code**:
|
||||
- Code containing trade secrets, proprietary algorithms, or core business logic
|
||||
- Code involving state secrets, national defense security, or other classified information
|
||||
- Code containing sensitive data (such as user data, keys, passwords, tokens, etc.)
|
||||
- Code restricted by laws and regulations from being transmitted externally
|
||||
- Proprietary code of clients or third parties (without authorization)
|
||||
- Users **must independently assess the sensitivity of their code** and bear full responsibility for uploading code and any resulting information disclosure.
|
||||
- **Recommendation**: For sensitive code, please wait for future local model deployment support in this project, or use privately deployed LLM services.
|
||||
- Project authors, contributors, and maintainers **assume no responsibility for any information disclosure, intellectual property infringement, legal disputes, or other losses resulting from users uploading sensitive code**.
|
||||
|
||||
#### 2. **Non-Professional Advice**
|
||||
- The code analysis results and suggestions provided by this tool are **for reference only** and do not constitute professional security audits, code reviews, or legal advice.
|
||||
- Users must combine manual reviews, professional tools, and other reliable resources to thoroughly validate critical code (especially in high-risk areas such as security, finance, or healthcare).
|
||||
|
||||
#### 3. **No Warranty and Liability Disclaimer**
|
||||
- This project is provided "as is" **without any express or implied warranties**, including but not limited to merchantability, fitness for a particular purpose, and non-infringement.
|
||||
- Authors, contributors, and maintainers **shall not be liable for any direct, indirect, incidental, special, punitive, or consequential damages**, including but not limited to data loss, system failures, security breaches, or business losses, even if advised of the possibility.
|
||||
|
||||
#### 4. **Limitations of AI Analysis**
|
||||
- This tool relies on AI models such as Google Gemini, and results may contain **errors, omissions, or inaccuracies**, with no guarantee of completeness or reliability.
|
||||
- AI outputs **cannot replace human expert judgment**; users are solely responsible for the final code quality and any outcomes.
|
||||
|
||||
#### 5. **Third-Party Services and Data Privacy**
|
||||
- This project integrates multiple third-party LLM services including Google Gemini, OpenAI, Claude, Qwen, DeepSeek, as well as Supabase, GitHub, and other services. Usage is subject to their respective terms of service and privacy policies.
|
||||
- **Code Transmission Notice**: User-submitted code will be sent via API to the selected LLM service provider for analysis. The transmission process and data processing follow each service provider's privacy policy.
|
||||
- Users must obtain and manage API keys independently; this project **does not store, transmit, or process user API keys and sensitive information**.
|
||||
- Availability, accuracy, privacy protection, data retention policies, or disruptions of third-party services are the responsibility of the providers; project authors assume no joint liability.
|
||||
- **Data Retention Warning**: Different LLM service providers have varying policies on API request data retention and usage. Users should carefully read the privacy policy and terms of use of their chosen service provider before use.
|
||||
|
||||
#### 6. **User Responsibilities**
|
||||
- Users must ensure their code does not infringe third-party intellectual property rights, does not contain confidential information, and complies with open-source licenses and applicable laws.
|
||||
- Users **bear full responsibility for the content, nature, and compliance of uploaded code**, including but not limited to:
|
||||
- Ensuring code does not contain sensitive information or trade secrets
|
||||
- Ensuring they have the right to use and analyze the code
|
||||
- Complying with data protection and privacy laws in their country/region
|
||||
- Adhering to confidentiality agreements and security policies of their company or organization
|
||||
- **This tool must not be used for illegal, malicious, or rights-infringing purposes**; users bear full legal and financial responsibility for all consequences. Authors, contributors, and maintainers **shall bear no responsibility** for such activities or their consequences and reserve the right to pursue abusers.
|
||||
|
||||
#### 7. **Open Source Contributions**
|
||||
- Code, content, or suggestions from contributors **do not represent the project's official stance**; contributors are responsible for their accuracy, security, and compliance.
|
||||
- Maintainers reserve the right to review, modify, reject, or remove any contributions.
|
||||
|
||||
For questions, please contact maintainers via GitHub Issues. This disclaimer is governed by the laws of the project's jurisdiction.
|
||||
|
|
@ -1,16 +1,33 @@
|
|||
FROM python:3.11-slim
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY pyproject.toml .
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# Command is overridden by docker-compose for dev
|
||||
# 创建上传目录
|
||||
RUN mkdir -p /app/uploads/zip_files
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令(开发模式由 docker-compose 覆盖)
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,11 @@ async def read_project(
|
|||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以查看
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权查看此项目")
|
||||
|
||||
return project
|
||||
|
||||
@router.put("/{id}", response_model=ProjectResponse)
|
||||
|
|
@ -218,6 +223,10 @@ async def update_project(
|
|||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以更新
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权更新此项目")
|
||||
|
||||
update_data = project_in.model_dump(exclude_unset=True)
|
||||
if "programming_languages" in update_data and update_data["programming_languages"] is not None:
|
||||
update_data["programming_languages"] = json.dumps(update_data["programming_languages"])
|
||||
|
|
@ -331,18 +340,41 @@ async def scan_project(
|
|||
await db.commit()
|
||||
await db.refresh(task)
|
||||
|
||||
# 获取用户配置
|
||||
# 获取用户配置(包含解密敏感字段)
|
||||
from sqlalchemy.future import select
|
||||
from app.core.encryption import decrypt_sensitive_data
|
||||
import json
|
||||
|
||||
# 需要解密的敏感字段列表
|
||||
SENSITIVE_LLM_FIELDS = [
|
||||
'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey',
|
||||
'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey',
|
||||
'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey'
|
||||
]
|
||||
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
||||
|
||||
def decrypt_config(config_dict: dict, sensitive_fields: list) -> dict:
|
||||
"""解密配置中的敏感字段"""
|
||||
decrypted = config_dict.copy()
|
||||
for field in sensitive_fields:
|
||||
if field in decrypted and decrypted[field]:
|
||||
decrypted[field] = decrypt_sensitive_data(decrypted[field])
|
||||
return decrypted
|
||||
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
user_config = {}
|
||||
if config:
|
||||
llm_config = json.loads(config.llm_config) if config.llm_config else {}
|
||||
other_config = json.loads(config.other_config) if config.other_config else {}
|
||||
# 解密敏感字段
|
||||
llm_config = decrypt_config(llm_config, SENSITIVE_LLM_FIELDS)
|
||||
other_config = decrypt_config(other_config, SENSITIVE_OTHER_FIELDS)
|
||||
user_config = {
|
||||
'llmConfig': json.loads(config.llm_config) if config.llm_config else {},
|
||||
'otherConfig': json.loads(config.other_config) if config.other_config else {},
|
||||
'llmConfig': llm_config,
|
||||
'otherConfig': other_config,
|
||||
}
|
||||
|
||||
# Trigger Background Task
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ async def scan_zip(
|
|||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以上传
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权操作此项目")
|
||||
|
||||
# Validate file
|
||||
if not file.filename.lower().endswith('.zip'):
|
||||
raise HTTPException(status_code=400, detail="请上传ZIP格式文件")
|
||||
|
|
@ -246,6 +250,10 @@ async def scan_stored_zip(
|
|||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以扫描
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权操作此项目")
|
||||
|
||||
# 检查是否有存储的ZIP文件
|
||||
stored_zip_path = await load_project_zip(project_id)
|
||||
if not stored_zip_path:
|
||||
|
|
@ -284,6 +292,7 @@ class InstantAnalysisResponse(BaseModel):
|
|||
issues_count: int
|
||||
quality_score: float
|
||||
analysis_time: float
|
||||
analysis_result: str # JSON字符串,包含完整的分析结果
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
|
|
@ -291,7 +300,25 @@ class InstantAnalysisResponse(BaseModel):
|
|||
|
||||
|
||||
async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict:
|
||||
"""获取用户配置字典"""
|
||||
"""获取用户配置字典(包含解密敏感字段)"""
|
||||
from app.core.encryption import decrypt_sensitive_data
|
||||
|
||||
# 需要解密的敏感字段列表(与 config.py 保持一致)
|
||||
SENSITIVE_LLM_FIELDS = [
|
||||
'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey',
|
||||
'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey',
|
||||
'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey'
|
||||
]
|
||||
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
||||
|
||||
def decrypt_config(config: dict, sensitive_fields: list) -> dict:
|
||||
"""解密配置中的敏感字段"""
|
||||
decrypted = config.copy()
|
||||
for field in sensitive_fields:
|
||||
if field in decrypted and decrypted[field]:
|
||||
decrypted[field] = decrypt_sensitive_data(decrypted[field])
|
||||
return decrypted
|
||||
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == user_id)
|
||||
)
|
||||
|
|
@ -299,9 +326,17 @@ async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict:
|
|||
if not config:
|
||||
return {}
|
||||
|
||||
# 解析配置
|
||||
llm_config = json.loads(config.llm_config) if config.llm_config else {}
|
||||
other_config = json.loads(config.other_config) if config.other_config else {}
|
||||
|
||||
# 解密敏感字段
|
||||
llm_config = decrypt_config(llm_config, SENSITIVE_LLM_FIELDS)
|
||||
other_config = decrypt_config(other_config, SENSITIVE_OTHER_FIELDS)
|
||||
|
||||
return {
|
||||
'llmConfig': json.loads(config.llm_config) if config.llm_config else {},
|
||||
'otherConfig': json.loads(config.other_config) if config.other_config else {},
|
||||
'llmConfig': llm_config,
|
||||
'otherConfig': other_config,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -321,7 +356,18 @@ async def instant_analysis(
|
|||
llm_service = LLMService(user_config=user_config)
|
||||
|
||||
start_time = datetime.utcnow()
|
||||
result = await llm_service.analyze_code(req.code, req.language)
|
||||
|
||||
try:
|
||||
result = await llm_service.analyze_code(req.code, req.language)
|
||||
except Exception as e:
|
||||
# 分析失败,返回错误信息
|
||||
error_msg = str(e)
|
||||
print(f"❌ 即时分析失败: {error_msg}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码分析失败: {error_msg}"
|
||||
)
|
||||
|
||||
end_time = datetime.utcnow()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
|
|
@ -339,8 +385,12 @@ async def instant_analysis(
|
|||
await db.commit()
|
||||
await db.refresh(analysis)
|
||||
|
||||
# Return result directly to frontend
|
||||
return result
|
||||
# Return result with analysis ID for export functionality
|
||||
return {
|
||||
**result,
|
||||
"analysis_id": analysis.id,
|
||||
"analysis_time": duration
|
||||
}
|
||||
|
||||
|
||||
@router.get("/instant/history", response_model=List[InstantAnalysisResponse])
|
||||
|
|
@ -359,3 +409,93 @@ async def get_instant_analysis_history(
|
|||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.delete("/instant/history/{analysis_id}")
|
||||
async def delete_instant_analysis(
|
||||
analysis_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a specific instant analysis record.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.id == analysis_id)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
analysis = result.scalar_one_or_none()
|
||||
|
||||
if not analysis:
|
||||
raise HTTPException(status_code=404, detail="分析记录不存在")
|
||||
|
||||
await db.delete(analysis)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.delete("/instant/history")
|
||||
async def delete_all_instant_analyses(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete all instant analysis records for current user.
|
||||
"""
|
||||
from sqlalchemy import delete
|
||||
|
||||
await db.execute(
|
||||
delete(InstantAnalysis).where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "已清空所有历史记录"}
|
||||
|
||||
|
||||
@router.get("/instant/history/{analysis_id}/report/pdf")
|
||||
async def export_instant_report_pdf(
|
||||
analysis_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Export instant analysis report as PDF by analysis ID.
|
||||
"""
|
||||
from fastapi.responses import Response
|
||||
from app.services.report_generator import ReportGenerator
|
||||
|
||||
# 获取即时分析记录
|
||||
result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.id == analysis_id)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
analysis = result.scalar_one_or_none()
|
||||
|
||||
if not analysis:
|
||||
raise HTTPException(status_code=404, detail="分析记录不存在")
|
||||
|
||||
# 解析分析结果
|
||||
try:
|
||||
analysis_result = json.loads(analysis.analysis_result) if analysis.analysis_result else {}
|
||||
except json.JSONDecodeError:
|
||||
analysis_result = {}
|
||||
|
||||
# 生成 PDF
|
||||
pdf_bytes = ReportGenerator.generate_instant_report(
|
||||
analysis_result,
|
||||
analysis.language,
|
||||
analysis.analysis_time
|
||||
)
|
||||
|
||||
# 返回 PDF 文件
|
||||
filename = f"instant-analysis-{analysis.language}-{analysis.id[:8]}.pdf"
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,11 @@ async def read_task(
|
|||
task = result.scalars().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 检查权限:只有任务创建者可以查看
|
||||
if task.created_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权查看此任务")
|
||||
|
||||
return task
|
||||
|
||||
|
||||
|
|
@ -144,6 +149,10 @@ async def cancel_task(
|
|||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 检查权限:只有任务创建者可以取消
|
||||
if task.created_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权取消此任务")
|
||||
|
||||
if task.status not in ["pending", "running"]:
|
||||
raise HTTPException(status_code=400, detail="只能取消待处理或运行中的任务")
|
||||
|
||||
|
|
@ -167,6 +176,18 @@ async def read_task_issues(
|
|||
"""
|
||||
Get issues for a specific task.
|
||||
"""
|
||||
# 先检查任务是否存在且属于当前用户
|
||||
task_result = await db.execute(
|
||||
select(AuditTask).where(AuditTask.id == id)
|
||||
)
|
||||
task = task_result.scalars().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 检查权限:只有任务创建者可以查看问题
|
||||
if task.created_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权查看此任务的问题")
|
||||
|
||||
result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id == id)
|
||||
|
|
@ -207,3 +228,82 @@ async def update_issue(
|
|||
await db.commit()
|
||||
await db.refresh(issue)
|
||||
return issue
|
||||
|
||||
|
||||
@router.get("/{id}/report/pdf")
|
||||
async def export_task_report_pdf(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Export task audit report as PDF.
|
||||
"""
|
||||
from fastapi.responses import Response
|
||||
from app.services.report_generator import ReportGenerator
|
||||
|
||||
# 获取任务
|
||||
result = await db.execute(
|
||||
select(AuditTask)
|
||||
.options(selectinload(AuditTask.project))
|
||||
.where(AuditTask.id == id)
|
||||
)
|
||||
task = result.scalars().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 检查权限
|
||||
if task.created_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权导出此任务报告")
|
||||
|
||||
# 获取问题列表
|
||||
issues_result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id == id)
|
||||
.order_by(AuditIssue.severity.desc(), AuditIssue.created_at.desc())
|
||||
)
|
||||
issues = issues_result.scalars().all()
|
||||
|
||||
# 转换为字典
|
||||
task_dict = {
|
||||
'id': task.id,
|
||||
'status': task.status,
|
||||
'branch_name': task.branch_name,
|
||||
'total_files': task.total_files,
|
||||
'scanned_files': task.scanned_files,
|
||||
'total_lines': task.total_lines,
|
||||
'issues_count': task.issues_count,
|
||||
'quality_score': task.quality_score,
|
||||
'created_at': task.created_at.isoformat() if task.created_at else None,
|
||||
'completed_at': task.completed_at.isoformat() if task.completed_at else None,
|
||||
}
|
||||
|
||||
issues_list = [
|
||||
{
|
||||
'title': issue.title,
|
||||
'description': issue.description,
|
||||
'severity': issue.severity,
|
||||
'issue_type': issue.issue_type,
|
||||
'file_path': issue.file_path,
|
||||
'line_number': issue.line_number,
|
||||
'column_number': issue.column_number,
|
||||
'code_snippet': issue.code_snippet,
|
||||
'suggestion': issue.suggestion,
|
||||
}
|
||||
for issue in issues
|
||||
]
|
||||
|
||||
project_name = task.project.name if task.project else "Unknown Project"
|
||||
|
||||
# 生成 PDF
|
||||
pdf_bytes = ReportGenerator.generate_task_report(task_dict, issues_list, project_name)
|
||||
|
||||
# 返回 PDF 文件
|
||||
filename = f"audit-report-{task.id[:8]}-{datetime.utcnow().strftime('%Y%m%d')}.pdf"
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -303,6 +303,9 @@ Note:
|
|||
"""
|
||||
分析代码并返回结构化问题
|
||||
支持中英文输出
|
||||
|
||||
Raises:
|
||||
Exception: 当LLM调用失败或返回无效响应时抛出异常
|
||||
"""
|
||||
# 获取输出语言配置
|
||||
output_language = self._get_output_language()
|
||||
|
|
@ -350,26 +353,35 @@ Please analyze the following code:
|
|||
|
||||
# 检查响应内容是否为空
|
||||
if not content or not content.strip():
|
||||
logger.warning(f"LLM返回空响应 - Provider: {self.config.provider.value}, Model: {self.config.model}")
|
||||
logger.warning(f"响应详情 - Finish Reason: {response.finish_reason}, Usage: {response.usage}")
|
||||
return self._get_default_response()
|
||||
error_msg = f"LLM返回空响应 - Provider: {self.config.provider.value}, Model: {self.config.model}"
|
||||
logger.error(error_msg)
|
||||
logger.error(f"响应详情 - Finish Reason: {response.finish_reason}, Usage: {response.usage}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# 尝试从响应中提取JSON
|
||||
result = self._parse_json(content)
|
||||
|
||||
# 检查解析结果是否有效(不是默认响应)
|
||||
if result == self._get_default_response():
|
||||
error_msg = f"无法解析LLM响应为有效的分析结果 - Provider: {self.config.provider.value}"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM Analysis failed: {e}", exc_info=True)
|
||||
logger.error(f"Provider: {self.config.provider.value}, Model: {self.config.model}")
|
||||
return self._get_default_response()
|
||||
# 重新抛出异常,让调用者处理
|
||||
raise
|
||||
|
||||
def _parse_json(self, text: str) -> Dict[str, Any]:
|
||||
"""从LLM响应中解析JSON(增强版)"""
|
||||
|
||||
# 检查输入是否为空
|
||||
if not text or not text.strip():
|
||||
logger.warning("LLM响应内容为空,无法解析JSON")
|
||||
return self._get_default_response()
|
||||
logger.error("LLM响应内容为空,无法解析JSON")
|
||||
raise ValueError("LLM响应内容为空")
|
||||
|
||||
def clean_text(s: str) -> str:
|
||||
"""清理文本中的控制字符"""
|
||||
|
|
@ -462,13 +474,14 @@ Please analyze the following code:
|
|||
logger.debug(f"直接解析失败,尝试其他方法... {e}")
|
||||
|
||||
# 所有尝试都失败
|
||||
logger.warning("⚠️ 无法解析LLM响应为JSON")
|
||||
logger.warning(f"原始内容长度: {len(text)} 字符")
|
||||
logger.warning(f"原始内容(前500字符): {text[:500]}")
|
||||
logger.warning(f"原始内容(后500字符): {text[-500:] if len(text) > 500 else text}")
|
||||
logger.error("❌ 无法解析LLM响应为JSON")
|
||||
logger.error(f"原始内容长度: {len(text)} 字符")
|
||||
logger.error(f"原始内容(前500字符): {text[:500]}")
|
||||
logger.error(f"原始内容(后500字符): {text[-500:] if len(text) > 500 else text}")
|
||||
if last_error:
|
||||
logger.warning(f"最后错误: {type(last_error).__name__}: {str(last_error)}")
|
||||
return self._get_default_response()
|
||||
logger.error(f"最后错误: {type(last_error).__name__}: {str(last_error)}")
|
||||
# 抛出异常而不是返回默认响应
|
||||
raise ValueError(f"无法解析LLM响应为有效的JSON格式: {str(last_error) if last_error else '未知错误'}")
|
||||
|
||||
def _extract_from_markdown(self, text: str) -> Dict[str, Any]:
|
||||
"""从markdown代码块提取JSON"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,471 @@
|
|||
"""
|
||||
PDF 报告生成服务 - 专业审计版 (WeasyPrint)
|
||||
"""
|
||||
|
||||
import io
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
|
||||
# macOS Homebrew compatibility fix
|
||||
if sys.platform == 'darwin':
|
||||
os.environ['DYLD_FALLBACK_LIBRARY_PATH'] = '/opt/homebrew/lib:' + os.environ.get('DYLD_FALLBACK_LIBRARY_PATH', '')
|
||||
|
||||
from weasyprint import HTML, CSS
|
||||
from weasyprint.text.fonts import FontConfiguration
|
||||
from jinja2 import Template
|
||||
|
||||
class ReportGenerator:
|
||||
"""
|
||||
基于 HTML/CSS 的专业 PDF 报告生成器
|
||||
风格:严谨、高密度、企业级审计报告风格
|
||||
"""
|
||||
|
||||
# --- HTML 模板 ---
|
||||
_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>代码审计报告</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2.5cm 2cm;
|
||||
@top-left {
|
||||
content: element(logoRunning);
|
||||
vertical-align: middle;
|
||||
}
|
||||
@top-right {
|
||||
content: "XCodeReviewer Audit Report";
|
||||
font-size: 8pt;
|
||||
color: #666;
|
||||
font-family: sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-size: 9pt;
|
||||
font-family: serif;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Songti SC", "SimSun", "Times New Roman", serif;
|
||||
color: #000;
|
||||
line-height: 1.3; /* Tighter line height */
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 页眉 Logo 定义 */
|
||||
.running-logo {
|
||||
position: running(logoRunning);
|
||||
height: 30px;
|
||||
width: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
padding-bottom: 10px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-line {
|
||||
border-bottom: 2px solid #000;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Logo removed from here */
|
||||
|
||||
.title-group {
|
||||
display: block; /* Changed to block since it's the only child */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
margin: 0 0 5px 0;
|
||||
color: #000;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 10pt;
|
||||
color: #444;
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
font-size: 9pt;
|
||||
color: #333;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* 通用工具类 */
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.bold { font-weight: bold; }
|
||||
.mono { font-family: "Menlo", "Consolas", "Courier New", "PingFang SC", "Microsoft YaHei", monospace; }
|
||||
|
||||
/* 概览表格 */
|
||||
.section-header {
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
border-left: 4px solid #000;
|
||||
padding-left: 8px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f3f4f6;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 评分栏 */
|
||||
.score-box {
|
||||
border: 1px solid #000;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.score-left {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.score-right {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: right;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.score-val {
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 统计数据表格 */
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.stats-table td {
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.stats-table td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 8pt;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 3px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 问题列表 - 高密度排版 */
|
||||
.issue-item {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 10px 0; /* Reduced padding */
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.issue-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.issue-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 6px; /* Reduced margin */
|
||||
}
|
||||
|
||||
.issue-title {
|
||||
font-size: 10.5pt;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.issue-severity {
|
||||
font-size: 8.5pt;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-family: sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.issue-meta {
|
||||
font-size: 8pt;
|
||||
color: #555;
|
||||
margin-bottom: 6px; /* Reduced margin */
|
||||
background: #f3f4f6;
|
||||
padding: 2px 6px;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.issue-desc {
|
||||
text-align: justify;
|
||||
margin-bottom: 8px; /* Reduced margin */
|
||||
line-height: 1.4;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
|
||||
/* 代码块 - 浅色主题,紧凑 */
|
||||
.code-snippet {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-left: 3px solid #333;
|
||||
color: #1f2937;
|
||||
padding: 8px; /* Reduced padding */
|
||||
font-size: 8.5pt; /* Smaller font */
|
||||
line-height: 1.3;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 8px 0; /* Reduced margin */
|
||||
font-family: "Menlo", "Consolas", "Courier New", "PingFang SC", "Microsoft YaHei", monospace;
|
||||
}
|
||||
|
||||
/* 建议 - 无框风格 */
|
||||
.suggestion {
|
||||
margin-top: 6px;
|
||||
font-style: italic;
|
||||
color: #333;
|
||||
font-size: 9pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 定义页眉 Logo (Running Element) -->
|
||||
{% if logo_b64 %}
|
||||
<img src="data:image/png;base64,{{ logo_b64 }}" class="running-logo" alt="Logo"/>
|
||||
{% endif %}
|
||||
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="title-group">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
<div class="subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-info">
|
||||
<div class="meta-item">报告编号: <span class="mono">{{ report_id }}</span></div>
|
||||
<div class="meta-item">生成时间: {{ generated_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-line"></div>
|
||||
|
||||
<!-- 概览区域 -->
|
||||
<div class="score-box">
|
||||
<div class="score-left">
|
||||
<span style="font-size: 10pt; font-weight: bold; margin-right: 10px; vertical-align: middle;">代码质量评分</span>
|
||||
<span class="score-val" style="vertical-align: middle;">{{ score|int }}</span>
|
||||
<span style="font-size: 10pt; color: #666; margin-left: 5px; vertical-align: middle;">/ 100</span>
|
||||
</div>
|
||||
<div class="score-right">
|
||||
<table class="stats-table">
|
||||
<tr>
|
||||
{% for label, value in stats %}
|
||||
<td>
|
||||
<span class="stat-label">{{ label }}</span>
|
||||
<span class="stat-value">{{ value }}</span>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 问题详情 -->
|
||||
{% if issues %}
|
||||
<div class="section-header">审计发现明细 ({{ issues|length }})</div>
|
||||
|
||||
<div class="issue-list">
|
||||
{% for issue in issues %}
|
||||
<div class="issue-item">
|
||||
<div class="issue-title-row">
|
||||
<div class="issue-title">{{ loop.index }}. {{ issue.title }}</div>
|
||||
<div class="issue-severity color-{{ issue.severity }}">[{{ issue.severity_label }}]</div>
|
||||
</div>
|
||||
|
||||
{% if issue.file_path or issue.line %}
|
||||
<div class="issue-meta mono">
|
||||
{% if issue.file_path %}FILE: {{ issue.file_path }}{% endif %}
|
||||
{% if issue.line %}{% if issue.file_path %} | {% endif %}LINE: {{ issue.line }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="issue-desc">{{ issue.description }}</div>
|
||||
|
||||
{% if issue.code_snippet %}
|
||||
<div class="code-snippet mono">{{ issue.code_snippet }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.suggestion %}
|
||||
<div class="suggestion">
|
||||
<strong>建议:</strong> {{ issue.suggestion }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="padding: 20px; text-align: center; border: 1px dashed #ccc; margin-top: 20px;">
|
||||
<strong>未发现代码问题</strong>
|
||||
<p style="font-size: 9pt; color: #666; margin-top: 5px;">本次扫描未发现任何违规或潜在风险,代码质量符合标准。</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 页脚声明 -->
|
||||
<div style="margin-top: 40px; font-size: 8pt; color: #999; text-align: center; border-top: 1px solid #eee; padding-top: 10px;">
|
||||
本报告由 AI 自动生成,注意核实鉴别。
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _get_logo_base64(cls) -> str:
|
||||
"""读取并编码 Logo 图片"""
|
||||
try:
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 回退三级到项目根目录: services -> app -> backend -> root
|
||||
project_root = os.path.abspath(os.path.join(current_dir, '../../../'))
|
||||
logo_path = os.path.join(project_root, 'frontend/public/images/logo_nobg.png')
|
||||
|
||||
if os.path.exists(logo_path):
|
||||
with open(logo_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Error loading logo: {e}")
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _process_issues(cls, issues: List[Dict]) -> List[Dict]:
|
||||
processed = []
|
||||
order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
|
||||
sorted_issues = sorted(issues, key=lambda x: order.get(x.get('severity', 'low'), 4))
|
||||
|
||||
sev_labels = {
|
||||
'critical': 'CRITICAL',
|
||||
'high': 'HIGH',
|
||||
'medium': 'MEDIUM',
|
||||
'low': 'LOW'
|
||||
}
|
||||
|
||||
for i in sorted_issues:
|
||||
item = i.copy()
|
||||
item['severity'] = item.get('severity', 'low')
|
||||
item['severity_label'] = sev_labels.get(item['severity'], 'UNKNOWN')
|
||||
item['line'] = item.get('line_number') or item.get('line')
|
||||
|
||||
# 确保代码片段存在 (处理可能的字段名差异)
|
||||
code = item.get('code_snippet') or item.get('code') or item.get('context')
|
||||
if isinstance(code, list):
|
||||
code = '\n'.join(code)
|
||||
item['code_snippet'] = code
|
||||
|
||||
processed.append(item)
|
||||
return processed
|
||||
|
||||
@classmethod
|
||||
def _render_pdf(cls, context: Dict[str, Any]) -> bytes:
|
||||
# 注入 Logo
|
||||
context['logo_b64'] = cls._get_logo_base64()
|
||||
|
||||
template = Template(cls._TEMPLATE)
|
||||
html_content = template.render(**context)
|
||||
font_config = FontConfiguration()
|
||||
pdf_file = io.BytesIO()
|
||||
HTML(string=html_content).write_pdf(
|
||||
pdf_file,
|
||||
font_config=font_config,
|
||||
presentational_hints=True
|
||||
)
|
||||
pdf_file.seek(0)
|
||||
return pdf_file.getvalue()
|
||||
|
||||
@classmethod
|
||||
def generate_instant_report(cls, result: Dict[str, Any], language: str, time: float) -> bytes:
|
||||
score = result.get('quality_score', 0)
|
||||
issues = result.get('issues', [])
|
||||
|
||||
context = {
|
||||
'title': '代码审计报告',
|
||||
'subtitle': f'即时分析 | 语言: {language.capitalize()}',
|
||||
'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'report_id': f"INST-{int(datetime.now().timestamp())}",
|
||||
'score': score,
|
||||
'stats': [
|
||||
('问题总数', len(issues)),
|
||||
('耗时', f"{time:.2f}s"),
|
||||
],
|
||||
'issues': cls._process_issues(issues)
|
||||
}
|
||||
return cls._render_pdf(context)
|
||||
|
||||
@classmethod
|
||||
def generate_task_report(cls, task: Dict[str, Any], issues: List[Dict[str, Any]], project: str = "项目") -> bytes:
|
||||
score = task.get('quality_score', 0)
|
||||
|
||||
context = {
|
||||
'title': '项目代码审计报告',
|
||||
'subtitle': f"项目: {project} | 分支: {task.get('branch_name', 'default')}",
|
||||
'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'report_id': f"TASK-{task.get('id', '')[:8]}",
|
||||
'score': score,
|
||||
'stats': [
|
||||
('扫描文件', task.get('scanned_files', 0)),
|
||||
('代码行数', f"{task.get('total_lines', 0):,}"),
|
||||
('问题总数', len(issues))
|
||||
],
|
||||
'issues': cls._process_issues(issues)
|
||||
}
|
||||
return cls._render_pdf(context)
|
||||
|
|
@ -302,7 +302,8 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
|
|||
if len(content) > settings.MAX_FILE_SIZE_BYTES:
|
||||
continue
|
||||
|
||||
total_lines += content.count('\n') + 1
|
||||
file_lines = content.split('\n')
|
||||
total_lines = len(file_lines) + 1
|
||||
language = get_language_from_path(file_info["path"])
|
||||
|
||||
# LLM分析
|
||||
|
|
@ -320,17 +321,33 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
|
|||
# 保存问题
|
||||
issues = analysis.get("issues", [])
|
||||
for issue in issues:
|
||||
line_num = issue.get("line", 1)
|
||||
|
||||
# 健壮的代码片段提取逻辑
|
||||
# 优先使用 LLM 返回的片段,如果为空则从源码提取
|
||||
code_snippet = issue.get("code_snippet")
|
||||
if not code_snippet or len(code_snippet.strip()) < 5:
|
||||
# 从源码提取上下文 (前后2行)
|
||||
try:
|
||||
# line_num 是 1-based
|
||||
idx = max(0, int(line_num) - 1)
|
||||
start = max(0, idx - 2)
|
||||
end = min(len(file_lines), idx + 3)
|
||||
code_snippet = '\n'.join(file_lines[start:end])
|
||||
except Exception:
|
||||
code_snippet = ""
|
||||
|
||||
audit_issue = AuditIssue(
|
||||
task_id=task.id,
|
||||
file_path=file_info["path"],
|
||||
line_number=issue.get("line", 1),
|
||||
line_number=line_num,
|
||||
column_number=issue.get("column"),
|
||||
issue_type=issue.get("type", "maintainability"),
|
||||
severity=issue.get("severity", "low"),
|
||||
title=issue.get("title", "Issue"),
|
||||
message=issue.get("description") or issue.get("title", "Issue"),
|
||||
suggestion=issue.get("suggestion"),
|
||||
code_snippet=issue.get("code_snippet"),
|
||||
code_snippet=code_snippet,
|
||||
ai_explanation=issue.get("ai_explanation"),
|
||||
status="open"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,4 +19,7 @@ dependencies = [
|
|||
"greenlet",
|
||||
"bcrypt<5.0.0",
|
||||
"litellm>=1.0.0",
|
||||
"reportlab>=4.0.0",
|
||||
"weasyprint>=66.0",
|
||||
"jinja2>=3.1.6",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import os
|
||||
import base64
|
||||
|
||||
def test_logo_read():
|
||||
try:
|
||||
# 模拟 ReportGenerator 中的路径逻辑
|
||||
current_dir = os.getcwd() # 假设我们在 backend 目录下运行
|
||||
# 调整逻辑以匹配 ReportGenerator.__file__ 的行为
|
||||
# 假设脚本在 backend/app/services/test_logo.py
|
||||
|
||||
# 直接使用绝对路径进行测试,排除相对路径计算干扰
|
||||
project_root = "/Users/lintsinghua/XCodeReviewer"
|
||||
logo_path = os.path.join(project_root, 'frontend/public/logo_xcodereviewer.png')
|
||||
|
||||
print(f"Looking for logo at: {logo_path}")
|
||||
|
||||
if os.path.exists(logo_path):
|
||||
print("File exists.")
|
||||
with open(logo_path, "rb") as image_file:
|
||||
data = image_file.read()
|
||||
b64 = base64.b64encode(data).decode('utf-8')
|
||||
print(f"Read {len(data)} bytes.")
|
||||
print(f"Base64 length: {len(b64)}")
|
||||
print(f"Base64 prefix: {b64[:50]}...")
|
||||
else:
|
||||
print("File does NOT exist.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_logo_read()
|
||||
259
backend/uv.lock
259
backend/uv.lock
|
|
@ -227,6 +227,50 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotlicffi"
|
||||
version = "1.2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
|
|
@ -399,6 +443,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssselect2"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tinycss2" },
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
|
|
@ -496,6 +553,46 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.60.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
woff = [
|
||||
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
|
||||
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
|
||||
{ name = "zopfli" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
|
|
@ -1054,6 +1151,64 @@ bcrypt = [
|
|||
{ name = "bcrypt" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1223,6 +1378,24 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydyf"
|
||||
version = "0.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c2/97fc6ce4ce0045080dc99446def812081b57750ed8aa67bfdfafa4561fe5/pydyf-0.11.0.tar.gz", hash = "sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64", size = 17769, upload-time = "2024-07-12T12:26:51.95Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ac/d5db977deaf28c6ecbc61bbca269eb3e8f0b3a1f55c8549e5333e606e005/pydyf-0.11.0-py3-none-any.whl", hash = "sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3", size = 8104, upload-time = "2024-07-12T12:26:49.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyphen"
|
||||
version = "0.17.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
|
|
@ -1373,6 +1546,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "4.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/80/dfa85941e3c3800aa5cd2f940c1903358c1fb61149f5f91b62efa61e7d03/reportlab-4.4.5.tar.gz", hash = "sha256:0457d642aa76df7b36b0235349904c58d8f9c606a872456ed04436aafadc1510", size = 3910836, upload-time = "2025-11-18T11:43:10.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/16/0c26a7bdfd20cba49a011b1095461be120c53df3926e9843fccfb9530e72/reportlab-4.4.5-py3-none-any.whl", hash = "sha256:849773d7cd5dde2072fedbac18c8bc909506c8befba8f088ba7b09243c6684cc", size = 1954256, upload-time = "2025-11-17T12:03:05.214Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
|
|
@ -1566,6 +1752,30 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyhtml5"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/03/6111ed99e9bf7dfa1c30baeef0e0fb7e0bd387bd07f8e5b270776fe1de3f/tinyhtml5-2.0.0.tar.gz", hash = "sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc", size = 179507, upload-time = "2024-10-29T15:37:14.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.22.1"
|
||||
|
|
@ -1753,6 +1963,34 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weasyprint"
|
||||
version = "66.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "cssselect2" },
|
||||
{ name = "fonttools", extra = ["woff"] },
|
||||
{ name = "pillow" },
|
||||
{ name = "pydyf" },
|
||||
{ name = "pyphen" },
|
||||
{ name = "tinycss2" },
|
||||
{ name = "tinyhtml5" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/99/480b5430b7eb0916e7d5df1bee7d9508b28b48fee28da894d0a050e0e930/weasyprint-66.0.tar.gz", hash = "sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40", size = 504224, upload-time = "2025-07-24T11:44:42.771Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/d1/c5d9b341bf3d556c1e4c6566b3efdda0b1bb175510aa7b09dd3eee246923/weasyprint-66.0-py3-none-any.whl", hash = "sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0", size = 301965, upload-time = "2025-07-24T11:44:40.968Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
|
|
@ -1785,14 +2023,17 @@ dependencies = [
|
|||
{ name = "fastapi" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "litellm" },
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "reportlab" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "weasyprint" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
|
@ -1804,14 +2045,17 @@ requires-dist = [
|
|||
{ name = "fastapi", specifier = ">=0.100.0" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||
{ name = "litellm", specifier = ">=1.0.0" },
|
||||
{ name = "passlib", extras = ["bcrypt"] },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-jose", extras = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "reportlab", specifier = ">=4.0.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
||||
{ name = "uvicorn", extras = ["standard"] },
|
||||
{ name = "weasyprint", specifier = ">=66.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1900,3 +2144,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/4c/efa0760686d4cc69e68a8f284d3c6c5884722c50f810af0e277fb7d61621/zopfli-0.4.0.tar.gz", hash = "sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6", size = 176720, upload-time = "2025-11-07T17:00:59.507Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/62/ec5cb67ee379c6a4f296f1277b971ff8c26460bf8775f027f82c519a0a72/zopfli-0.4.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce", size = 287433, upload-time = "2025-11-07T17:00:45.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/9e/8f81e69bd771014a488c4c64476b6e6faab91b2c913d0f81eca7e06401eb/zopfli-0.4.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb", size = 847135, upload-time = "2025-11-07T17:00:47.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/84/6e60eeaaa1c1eae7b4805f1c528f3e8ae62cef323ec1e52347a11031e3ba/zopfli-0.4.0-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3", size = 831606, upload-time = "2025-11-07T17:00:48.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/aa/a4d5de7ed8e809953cb5e8992bddc40f38461ec5a44abfb010953875adfc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941", size = 1789376, upload-time = "2025-11-07T17:00:49.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/95/4d1e943fbc44157f58b623625686d0b970f2fda269e721fbf9546b93f6cc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85", size = 1879013, upload-time = "2025-11-07T17:00:50.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/db/4f2eebf73c0e2df293a366a1d176cd315a74ce0b00f83826a7ba9ddd1ab3/zopfli-0.4.0-cp310-abi3-win32.whl", hash = "sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1", size = 83655, upload-time = "2025-11-07T17:00:51.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f6/bd80c5278b1185dc41155c77bc61bfe1d817254a7f2115f66aa69a270b89/zopfli-0.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975", size = 100824, upload-time = "2025-11-07T17:00:52.658Z" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ services:
|
|||
context: ./backend
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./backend/uploads:/app/uploads
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/xcodereviewer
|
||||
- SECRET_KEY=changethisdevsecret
|
||||
- ALGORITHM=HS256
|
||||
- ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -43,7 +43,7 @@ services:
|
|||
ports:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
- VITE_API_BASE_URL=/api
|
||||
- VITE_API_BASE_URL=http://localhost:8000/api/v1
|
||||
depends_on:
|
||||
- backend
|
||||
command: npm run dev -- --host
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# ========================================
|
||||
# XCodeReviewer 前端环境变量配置示例
|
||||
# ========================================
|
||||
# 复制此文件为 .env 并填写你的配置
|
||||
# 注意:前后端分离架构下,LLM 配置主要在后端 backend/.env 中设置
|
||||
|
||||
# ==================== 后端 API 配置 ====================
|
||||
# 后端 API 地址(Docker Compose 部署时使用)
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# ==================== 数据库配置 ====================
|
||||
# 方式1:本地数据库(推荐,开箱即用)
|
||||
VITE_USE_LOCAL_DB=true
|
||||
|
||||
# 方式2:Supabase 云端数据库(支持多设备同步)
|
||||
# VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
# VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# ==================== Git 仓库集成配置 (可选) ====================
|
||||
# 用于前端直接访问公开仓库(私有仓库建议在后端配置)
|
||||
|
||||
# GitHub Token
|
||||
# VITE_GITHUB_TOKEN=ghp_your_github_token_here
|
||||
|
||||
# GitLab Token
|
||||
# VITE_GITLAB_TOKEN=glpat-your_gitlab_token_here
|
||||
|
||||
# ==================== 应用配置 ====================
|
||||
VITE_APP_ID=xcodereviewer
|
||||
|
||||
# ==================== 代码分析配置 ====================
|
||||
VITE_MAX_ANALYZE_FILES=40
|
||||
VITE_LLM_CONCURRENCY=2
|
||||
VITE_LLM_GAP_MS=500
|
||||
VITE_OUTPUT_LANGUAGE=zh-CN # zh-CN: 中文 | en-US: 英文
|
||||
|
||||
# ========================================
|
||||
# 重要提示
|
||||
# ========================================
|
||||
# 1. LLM 配置(API Key、模型等)请在后端 backend/.env 中设置
|
||||
# 2. 前端可通过 /admin 页面进行运行时配置
|
||||
# 3. 本地数据库模式下,数据存储在浏览器 IndexedDB 中
|
||||
|
|
@ -2,18 +2,22 @@ FROM node:20-alpine
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml* package-lock.json* ./
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN if [ -f pnpm-lock.yaml ]; then \
|
||||
corepack enable && pnpm install; \
|
||||
else \
|
||||
npm install; \
|
||||
fi
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
# 开发模式启动命令
|
||||
CMD ["pnpm", "dev", "--host"]
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 774 KiB |
|
|
@ -0,0 +1,206 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { FileJson, FileText, Download, Loader2 } from "lucide-react";
|
||||
import type { CodeAnalysisResult } from "@/shared/types";
|
||||
import { exportInstantToPDF, exportInstantToJSON } from "@/features/reports/services/reportExport";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface InstantExportDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
analysisId: string | null; // 数据库中的记录 ID
|
||||
analysisResult: CodeAnalysisResult;
|
||||
language: string;
|
||||
analysisTime: number;
|
||||
}
|
||||
|
||||
type ExportFormat = "json" | "pdf";
|
||||
|
||||
export default function InstantExportDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
analysisId,
|
||||
analysisResult,
|
||||
language,
|
||||
analysisTime
|
||||
}: InstantExportDialogProps) {
|
||||
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>("pdf");
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
switch (selectedFormat) {
|
||||
case "json":
|
||||
exportInstantToJSON(analysisResult, language, analysisTime);
|
||||
toast.success("JSON 报告已导出");
|
||||
break;
|
||||
case "pdf":
|
||||
if (!analysisId) {
|
||||
toast.error("请先保存分析结果到历史记录");
|
||||
return;
|
||||
}
|
||||
await exportInstantToPDF(analysisId, language);
|
||||
toast.success("PDF 报告已导出");
|
||||
break;
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("导出报告失败:", error);
|
||||
toast.error("导出报告失败,请重试");
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formats = [
|
||||
{
|
||||
value: "json" as ExportFormat,
|
||||
label: "JSON 格式",
|
||||
description: "结构化数据,适合程序处理和集成",
|
||||
icon: FileJson,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
value: "pdf" as ExportFormat,
|
||||
label: "PDF 格式",
|
||||
description: analysisId ? "专业报告,适合打印和分享" : "需要先保存到历史记录",
|
||||
icon: FileText,
|
||||
disabled: !analysisId
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-white border-2 border-black p-0 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] rounded-none">
|
||||
<DialogHeader className="p-6 border-b-2 border-black bg-gray-50">
|
||||
<DialogTitle className="flex items-center space-x-2 font-display font-bold uppercase text-xl">
|
||||
<Download className="w-6 h-6 text-black" />
|
||||
<span>导出分析报告</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="font-mono text-xs text-gray-500 mt-2">
|
||||
选择报告格式并导出代码分析结果
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-6">
|
||||
<RadioGroup
|
||||
value={selectedFormat}
|
||||
onValueChange={(value) => setSelectedFormat(value as ExportFormat)}
|
||||
className="space-y-4"
|
||||
>
|
||||
{formats.map((format) => {
|
||||
const Icon = format.icon;
|
||||
const isSelected = selectedFormat === format.value;
|
||||
|
||||
return (
|
||||
<div key={format.value} className="relative">
|
||||
<RadioGroupItem
|
||||
value={format.value}
|
||||
id={format.value}
|
||||
className="peer sr-only"
|
||||
disabled={format.disabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={format.value}
|
||||
className={`flex items-start space-x-4 p-4 border-2 cursor-pointer transition-all rounded-none font-mono ${
|
||||
format.disabled
|
||||
? "border-gray-300 bg-gray-100 cursor-not-allowed opacity-60"
|
||||
: isSelected
|
||||
? "border-black bg-gray-100 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] translate-x-[-2px] translate-y-[-2px]"
|
||||
: "border-black bg-white hover:bg-gray-50 hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-12 h-12 border-2 border-black flex items-center justify-center rounded-none ${
|
||||
format.disabled
|
||||
? "bg-gray-200 text-gray-400"
|
||||
: isSelected
|
||||
? "bg-black text-white"
|
||||
: "bg-white text-black"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="font-bold uppercase text-black">
|
||||
{format.label}
|
||||
</h4>
|
||||
{isSelected && !format.disabled && (
|
||||
<div className="w-4 h-4 bg-black border-2 border-black" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 font-bold">{format.description}</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
|
||||
{/* 报告预览信息 */}
|
||||
<div className="mt-6 p-4 bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<h4 className="font-bold text-black uppercase mb-3 font-display border-b-2 border-black pb-2 w-fit">报告内容预览</h4>
|
||||
<div className="grid grid-cols-2 gap-3 text-xs font-mono">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 pb-1">
|
||||
<span className="text-gray-600 font-bold">编程语言:</span>
|
||||
<span className="font-bold text-black">{language.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-200 pb-1">
|
||||
<span className="text-gray-600 font-bold">质量评分:</span>
|
||||
<span className="font-bold text-black">{analysisResult.quality_score.toFixed(1)}/100</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-200 pb-1">
|
||||
<span className="text-gray-600 font-bold">发现问题:</span>
|
||||
<span className="font-bold text-orange-600">{analysisResult.issues.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-200 pb-1">
|
||||
<span className="text-gray-600 font-bold">分析耗时:</span>
|
||||
<span className="font-bold text-black">{analysisTime.toFixed(2)}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-6 border-t-2 border-black bg-gray-50 flex justify-end gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isExporting}
|
||||
className="retro-btn bg-white text-black border-2 border-black hover:bg-gray-100 rounded-none h-10 font-bold uppercase"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || (selectedFormat === "pdf" && !analysisId)}
|
||||
className="retro-btn bg-primary text-white border-2 border-black hover:bg-primary/90 rounded-none h-10 font-bold uppercase shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
|
||||
>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
导出中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AuditTask, AuditIssue } from "@/shared/types";
|
||||
import type { AuditTask, AuditIssue, CodeAnalysisResult } from "@/shared/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
|
||||
// 导出 JSON 格式报告
|
||||
export async function exportToJSON(task: AuditTask, issues: AuditIssue[]) {
|
||||
|
|
@ -52,344 +53,78 @@ export async function exportToJSON(task: AuditTask, issues: AuditIssue[]) {
|
|||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(report, null, 2)], { type: "application/json" });
|
||||
downloadBlob(blob, `audit-report-${task.id.slice(0, 8)}-${Date.now()}.json`);
|
||||
}
|
||||
|
||||
// 导出任务审计报告 PDF(后端生成)
|
||||
export async function exportToPDF(task: AuditTask, _issues: AuditIssue[]) {
|
||||
try {
|
||||
const blob = await api.exportTaskReportPDF(task.id);
|
||||
downloadBlob(blob, `audit-report-${task.id.slice(0, 8)}-${Date.now()}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Failed to export PDF:', error);
|
||||
throw new Error('PDF 导出失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 导出即时分析报告 PDF(后端生成,基于数据库记录)
|
||||
export async function exportInstantToPDF(analysisId: string, language: string) {
|
||||
try {
|
||||
const blob = await api.exportInstantReportPDF(analysisId);
|
||||
downloadBlob(blob, `instant-analysis-${language}-${Date.now()}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Failed to export instant PDF:', error);
|
||||
throw new Error('PDF 导出失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 导出即时分析 JSON
|
||||
export function exportInstantToJSON(
|
||||
analysisResult: CodeAnalysisResult,
|
||||
language: string,
|
||||
analysisTime: number
|
||||
) {
|
||||
const report = {
|
||||
metadata: {
|
||||
exportDate: new Date().toISOString(),
|
||||
version: "1.0.0",
|
||||
format: "JSON",
|
||||
type: "instant-analysis"
|
||||
},
|
||||
analysis: {
|
||||
language,
|
||||
analysisTime,
|
||||
qualityScore: analysisResult.quality_score,
|
||||
issuesCount: analysisResult.issues.length
|
||||
},
|
||||
issues: analysisResult.issues.map(issue => ({
|
||||
title: issue.title,
|
||||
description: issue.description,
|
||||
severity: issue.severity,
|
||||
type: issue.type,
|
||||
line: issue.line,
|
||||
column: issue.column,
|
||||
codeSnippet: issue.code_snippet,
|
||||
suggestion: issue.suggestion,
|
||||
aiExplanation: issue.ai_explanation,
|
||||
xai: issue.xai
|
||||
})),
|
||||
summary: analysisResult.summary,
|
||||
metrics: analysisResult.metrics
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(report, null, 2)], { type: "application/json" });
|
||||
downloadBlob(blob, `instant-analysis-${language}-${Date.now()}.json`);
|
||||
}
|
||||
|
||||
// 通用下载函数
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `audit-report-${task.id}-${Date.now()}.json`;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导出 PDF 格式报告(使用隐藏 iframe 打印)
|
||||
export async function exportToPDF(task: AuditTask, issues: AuditIssue[]) {
|
||||
const criticalIssues = issues.filter(i => i.severity === "critical");
|
||||
const highIssues = issues.filter(i => i.severity === "high");
|
||||
const mediumIssues = issues.filter(i => i.severity === "medium");
|
||||
const lowIssues = issues.filter(i => i.severity === "low");
|
||||
|
||||
const html = generateReportHTML(task, issues, criticalIssues, highIssues, mediumIssues, lowIssues);
|
||||
|
||||
// 创建隐藏的 iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.right = '0';
|
||||
iframe.style.bottom = '0';
|
||||
iframe.style.width = '0';
|
||||
iframe.style.height = '0';
|
||||
iframe.style.border = 'none';
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const iframeDoc = iframe.contentWindow?.document;
|
||||
if (iframeDoc) {
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(html);
|
||||
iframeDoc.close();
|
||||
|
||||
// 等待内容加载完成后打印
|
||||
iframe.onload = () => {
|
||||
setTimeout(() => {
|
||||
iframe.contentWindow?.print();
|
||||
// 打印对话框关闭后移除 iframe
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe);
|
||||
}, 1000);
|
||||
}, 250);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 生成报告 HTML(简化版)
|
||||
function generateReportHTML(
|
||||
task: AuditTask,
|
||||
issues: AuditIssue[],
|
||||
criticalIssues: AuditIssue[],
|
||||
highIssues: AuditIssue[],
|
||||
mediumIssues: AuditIssue[],
|
||||
lowIssues: AuditIssue[]
|
||||
): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>代码审计报告</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: "Microsoft YaHei", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #dc2626;
|
||||
border-bottom: 3px solid #dc2626;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
h2 {
|
||||
color: #dc2626;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
h3 {
|
||||
color: #374151;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info-section {
|
||||
background: #f9fafb;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #6b7280;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
.issue {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.issue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.issue-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
}
|
||||
.severity {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.severity-critical {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.severity-high {
|
||||
background: #fed7aa;
|
||||
color: #9a3412;
|
||||
}
|
||||
.severity-medium {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
.severity-low {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
.issue-meta {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.code-block {
|
||||
background: #1f2937;
|
||||
color: #f3f4f6;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 10px 0;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.suggestion {
|
||||
background: #dbeafe;
|
||||
border-left: 4px solid #2563eb;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 50px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>代码审计报告</h1>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>项目信息</h2>
|
||||
<div class="info-item">
|
||||
<span class="info-label">项目名称:</span>
|
||||
<span>${task.project?.name || "未知项目"}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">任务ID:</span>
|
||||
<span>${task.id}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">分支:</span>
|
||||
<span>${task.branch_name || "默认分支"}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">创建时间:</span>
|
||||
<span>${new Date(task.created_at).toLocaleString("zh-CN")}</span>
|
||||
</div>
|
||||
${task.completed_at ? `
|
||||
<div class="info-item">
|
||||
<span class="info-label">完成时间:</span>
|
||||
<span>${new Date(task.completed_at).toLocaleString("zh-CN")}</span>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
|
||||
<h2>审计统计</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>数值</th>
|
||||
<th>指标</th>
|
||||
<th>数值</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>质量评分</td>
|
||||
<td>${task.quality_score.toFixed(1)}/100</td>
|
||||
<td>扫描文件</td>
|
||||
<td>${task.scanned_files}/${task.total_files}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>代码行数</td>
|
||||
<td>${task.total_lines.toLocaleString()}</td>
|
||||
<td>发现问题</td>
|
||||
<td>${task.issues_count}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>严重问题</td>
|
||||
<td>${criticalIssues.length}</td>
|
||||
<td>高优先级</td>
|
||||
<td>${highIssues.length}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>中等优先级</td>
|
||||
<td>${mediumIssues.length}</td>
|
||||
<td>低优先级</td>
|
||||
<td>${lowIssues.length}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${issues.length > 0 ? `
|
||||
<h2>问题详情</h2>
|
||||
|
||||
${criticalIssues.length > 0 ? `
|
||||
<h3>严重问题 (${criticalIssues.length})</h3>
|
||||
${criticalIssues.map(issue => generateIssueHTML(issue, "critical")).join("")}
|
||||
` : ""}
|
||||
|
||||
${highIssues.length > 0 ? `
|
||||
<h3>高优先级问题 (${highIssues.length})</h3>
|
||||
${highIssues.map(issue => generateIssueHTML(issue, "high")).join("")}
|
||||
` : ""}
|
||||
|
||||
${mediumIssues.length > 0 ? `
|
||||
<h3>中等优先级问题 (${mediumIssues.length})</h3>
|
||||
${mediumIssues.map(issue => generateIssueHTML(issue, "medium")).join("")}
|
||||
` : ""}
|
||||
|
||||
${lowIssues.length > 0 ? `
|
||||
<h3>低优先级问题 (${lowIssues.length})</h3>
|
||||
${lowIssues.map(issue => generateIssueHTML(issue, "low")).join("")}
|
||||
` : ""}
|
||||
` : `
|
||||
<div class="info-section">
|
||||
<h3>✅ 代码质量优秀!</h3>
|
||||
<p>恭喜!没有发现任何问题。您的代码通过了所有质量检查。</p>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>报告生成时间:</strong> ${new Date().toLocaleString("zh-CN")}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// 生成问题的 HTML
|
||||
function generateIssueHTML(issue: AuditIssue, severity: string): string {
|
||||
return `
|
||||
<div class="issue">
|
||||
<div class="issue-header">
|
||||
<div class="issue-title">${escapeHtml(issue.title)}</div>
|
||||
<span class="severity severity-${severity}">
|
||||
${severity === "critical" ? "严重" : severity === "high" ? "高" : severity === "medium" ? "中等" : "低"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="issue-meta">
|
||||
📁 ${escapeHtml(issue.file_path)}
|
||||
${issue.line_number ? ` | 📍 第 ${issue.line_number} 行` : ""}
|
||||
${issue.column_number ? `,第 ${issue.column_number} 列` : ""}
|
||||
</div>
|
||||
${issue.description ? `
|
||||
<p><strong>问题描述:</strong> ${escapeHtml(issue.description)}</p>
|
||||
` : ""}
|
||||
${issue.code_snippet ? `
|
||||
<div class="code-block"><pre>${escapeHtml(issue.code_snippet)}</pre></div>
|
||||
` : ""}
|
||||
${issue.suggestion ? `
|
||||
<div class="suggestion">
|
||||
<strong>💡 修复建议:</strong><br>
|
||||
${escapeHtml(issue.suggestion)}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
|
|
@ -20,13 +21,15 @@ import {
|
|||
Upload,
|
||||
Zap,
|
||||
X,
|
||||
Download
|
||||
Download,
|
||||
History,
|
||||
ChevronRight
|
||||
} from "lucide-react";
|
||||
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { CodeAnalysisResult, AuditTask, AuditIssue } from "@/shared/types";
|
||||
import type { CodeAnalysisResult, InstantAnalysis as InstantAnalysisType } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
import ExportReportDialog from "@/components/reports/ExportReportDialog";
|
||||
import InstantExportDialog from "@/components/reports/InstantExportDialog";
|
||||
|
||||
// AI解释解析函数
|
||||
function parseAIExplanation(aiExplanation: string) {
|
||||
|
|
@ -56,11 +59,100 @@ export default function InstantAnalysis() {
|
|||
const [result, setResult] = useState<CodeAnalysisResult | null>(null);
|
||||
const [analysisTime, setAnalysisTime] = useState(0);
|
||||
const [exportDialogOpen, setExportDialogOpen] = useState(false);
|
||||
const [currentAnalysisId, setCurrentAnalysisId] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const loadingCardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 历史记录相关状态
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [historyRecords, setHistoryRecords] = useState<InstantAnalysisType[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState<string | null>(null);
|
||||
|
||||
const supportedLanguages = CodeAnalysisEngine.getSupportedLanguages();
|
||||
|
||||
// 加载历史记录
|
||||
const loadHistory = async () => {
|
||||
setLoadingHistory(true);
|
||||
try {
|
||||
const records = await api.getInstantAnalyses();
|
||||
setHistoryRecords(records);
|
||||
} catch (error) {
|
||||
console.error('Failed to load history:', error);
|
||||
toast.error('加载历史记录失败');
|
||||
} finally {
|
||||
setLoadingHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 查看历史记录详情
|
||||
const viewHistoryRecord = (record: InstantAnalysisType) => {
|
||||
try {
|
||||
const analysisResult = JSON.parse(record.analysis_result) as CodeAnalysisResult;
|
||||
setResult(analysisResult);
|
||||
setLanguage(record.language);
|
||||
setAnalysisTime(record.analysis_time);
|
||||
setSelectedHistoryId(record.id);
|
||||
setCurrentAnalysisId(record.id); // 设置当前分析 ID 用于导出
|
||||
setShowHistory(false);
|
||||
toast.success('已加载历史分析结果');
|
||||
} catch (error) {
|
||||
console.error('Failed to parse history record:', error);
|
||||
toast.error('解析历史记录失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 删除单条历史记录
|
||||
const deleteHistoryRecord = async (e: React.MouseEvent, recordId: string) => {
|
||||
e.stopPropagation(); // 阻止触发查看详情
|
||||
try {
|
||||
await api.deleteInstantAnalysis(recordId);
|
||||
setHistoryRecords(prev => prev.filter(r => r.id !== recordId));
|
||||
if (selectedHistoryId === recordId) {
|
||||
setSelectedHistoryId(null);
|
||||
setResult(null);
|
||||
}
|
||||
toast.success('删除成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete history:', error);
|
||||
toast.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 清空所有历史记录
|
||||
const clearAllHistory = async () => {
|
||||
if (!confirm('确定要清空所有历史记录吗?此操作不可恢复。')) return;
|
||||
try {
|
||||
await api.deleteAllInstantAnalyses();
|
||||
setHistoryRecords([]);
|
||||
setSelectedHistoryId(null);
|
||||
toast.success('已清空所有历史记录');
|
||||
} catch (error) {
|
||||
console.error('Failed to clear history:', error);
|
||||
toast.error('清空失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 切换历史记录面板
|
||||
const toggleHistory = () => {
|
||||
if (!showHistory) {
|
||||
loadHistory();
|
||||
}
|
||||
setShowHistory(!showHistory);
|
||||
};
|
||||
|
||||
// 监听analyzing状态变化,自动滚动到加载卡片
|
||||
useEffect(() => {
|
||||
if (analyzing && loadingCardRef.current) {
|
||||
|
|
@ -236,26 +328,17 @@ class UserManager {
|
|||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
setResult(analysisResult);
|
||||
setAnalysisTime(duration);
|
||||
|
||||
// 保存分析记录(可选,未登录时跳过)
|
||||
if (user) {
|
||||
await api.createInstantAnalysis({
|
||||
user_id: user.id,
|
||||
language,
|
||||
// 不存储代码内容,仅存储摘要
|
||||
code_content: '',
|
||||
analysis_result: JSON.stringify(analysisResult),
|
||||
issues_count: analysisResult.issues.length,
|
||||
quality_score: analysisResult.quality_score,
|
||||
analysis_time: duration
|
||||
});
|
||||
}
|
||||
// 使用后端返回的 analysis_time,如果没有则使用前端计算的
|
||||
setAnalysisTime(analysisResult.analysis_time || duration);
|
||||
// 保存后端返回的 analysis_id 用于导出
|
||||
setCurrentAnalysisId(analysisResult.analysis_id || null);
|
||||
|
||||
toast.success(`分析完成!发现 ${analysisResult.issues.length} 个问题`);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Analysis failed:', error);
|
||||
toast.error("分析失败,请稍后重试");
|
||||
// 显示详细的错误信息
|
||||
const errorMessage = error?.message || "分析失败,请稍后重试";
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setAnalyzing(false);
|
||||
// 即时分析结束后清空前端内存中的代码(满足NFR-2销毁要求)
|
||||
|
|
@ -339,65 +422,6 @@ class UserManager {
|
|||
setAnalysisTime(0);
|
||||
};
|
||||
|
||||
// 构造临时任务和问题数据用于导出
|
||||
const getTempTaskAndIssues = () => {
|
||||
if (!result) return null;
|
||||
|
||||
const tempTask: AuditTask = {
|
||||
id: 'instant-' + Date.now(),
|
||||
project_id: 'instant-analysis',
|
||||
task_type: 'instant',
|
||||
status: 'completed',
|
||||
branch_name: undefined,
|
||||
exclude_patterns: '[]',
|
||||
scan_config: JSON.stringify({ language }),
|
||||
total_files: 1,
|
||||
scanned_files: 1,
|
||||
total_lines: code.split('\n').length,
|
||||
issues_count: result.issues.length,
|
||||
quality_score: result.quality_score,
|
||||
started_at: undefined,
|
||||
completed_at: new Date().toISOString(),
|
||||
created_by: 'local-user',
|
||||
created_at: new Date().toISOString(),
|
||||
project: {
|
||||
id: 'instant',
|
||||
owner_id: 'local-user',
|
||||
name: '即时分析',
|
||||
description: `${language} 代码即时分析`,
|
||||
source_type: 'zip',
|
||||
repository_type: 'other',
|
||||
repository_url: undefined,
|
||||
default_branch: 'instant',
|
||||
programming_languages: JSON.stringify([language]),
|
||||
is_active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
const tempIssues: AuditIssue[] = result.issues.map((issue, index) => ({
|
||||
id: `instant-issue-${index}`,
|
||||
task_id: tempTask.id,
|
||||
file_path: `instant-analysis.${language}`,
|
||||
line_number: issue.line || undefined,
|
||||
column_number: issue.column || undefined,
|
||||
issue_type: issue.type as any,
|
||||
severity: issue.severity as any,
|
||||
title: issue.title,
|
||||
description: issue.description || undefined,
|
||||
suggestion: issue.suggestion || undefined,
|
||||
code_snippet: issue.code_snippet || undefined,
|
||||
ai_explanation: issue.ai_explanation || (issue.xai ? JSON.stringify(issue.xai) : undefined),
|
||||
status: 'open',
|
||||
resolved_by: undefined,
|
||||
resolved_at: undefined,
|
||||
created_at: new Date().toISOString()
|
||||
}));
|
||||
|
||||
return { task: tempTask, issues: tempIssues };
|
||||
};
|
||||
|
||||
// 渲染问题的函数,使用复古样式
|
||||
const renderIssue = (issue: any, index: number) => (
|
||||
<div key={index} className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4 mb-4 hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all">
|
||||
|
|
@ -540,6 +564,107 @@ class UserManager {
|
|||
{/* Decorative Background */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
|
||||
|
||||
{/* 历史记录面板 */}
|
||||
{showHistory && (
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
|
||||
<div className="p-4 border-b-2 border-black bg-gray-50 flex items-center justify-between">
|
||||
<h3 className="text-lg font-display font-bold uppercase flex items-center">
|
||||
<History className="w-5 h-5 mr-2" />
|
||||
分析历史记录
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{historyRecords.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={clearAllHistory}
|
||||
size="sm"
|
||||
className="retro-btn bg-red-50 text-red-600 hover:bg-red-100 h-8 border-red-300"
|
||||
>
|
||||
清空全部
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowHistory(false)}
|
||||
size="sm"
|
||||
className="retro-btn bg-white text-black hover:bg-gray-100 h-8"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{loadingHistory ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-none h-8 w-8 border-4 border-primary border-t-transparent mx-auto mb-4"></div>
|
||||
<p className="text-gray-600 font-mono">加载中...</p>
|
||||
</div>
|
||||
) : historyRecords.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
<History className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h4 className="text-lg font-bold text-gray-600 uppercase mb-2 font-mono">暂无历史记录</h4>
|
||||
<p className="text-gray-500 font-mono text-sm">完成代码分析后,记录将显示在这里</p>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-3">
|
||||
{historyRecords.map((record) => (
|
||||
<div
|
||||
key={record.id}
|
||||
className={`border-2 border-black p-4 hover:bg-gray-50 transition-colors cursor-pointer ${
|
||||
selectedHistoryId === record.id ? 'bg-primary/10 border-primary' : 'bg-white'
|
||||
}`}
|
||||
onClick={() => viewHistoryRecord(record)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="rounded-none border-2 border-black bg-gray-100 text-black font-mono uppercase">
|
||||
{record.language}
|
||||
</Badge>
|
||||
<span className="text-sm font-mono text-gray-600">
|
||||
{formatDate(record.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
className={`rounded-none border-2 border-black font-mono ${
|
||||
record.quality_score >= 80 ? 'bg-green-100 text-green-800' :
|
||||
record.quality_score >= 60 ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
评分: {record.quality_score.toFixed(1)}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => deleteHistoryRecord(e, record.id)}
|
||||
className="h-6 w-6 p-0 hover:bg-red-100 hover:text-red-600"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs font-mono text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{record.issues_count} 个问题
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{record.analysis_time.toFixed(2)}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 代码输入区域 */}
|
||||
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
|
||||
<div className="p-4 border-b-2 border-black bg-gray-50 flex items-center justify-between">
|
||||
|
|
@ -547,12 +672,23 @@ class UserManager {
|
|||
<Code className="w-5 h-5 mr-2" />
|
||||
代码分析
|
||||
</h3>
|
||||
{result && (
|
||||
<Button variant="outline" onClick={clearAnalysis} size="sm" className="retro-btn bg-white text-black hover:bg-gray-100 h-8">
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
重新分析
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={toggleHistory}
|
||||
size="sm"
|
||||
className={`retro-btn h-8 ${showHistory ? 'bg-primary text-white' : 'bg-white text-black hover:bg-gray-100'}`}
|
||||
>
|
||||
<History className="w-4 h-4 mr-2" />
|
||||
历史记录
|
||||
</Button>
|
||||
)}
|
||||
{result && (
|
||||
<Button variant="outline" onClick={clearAnalysis} size="sm" className="retro-btn bg-white text-black hover:bg-gray-100 h-8">
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
重新分析
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
|
|
@ -878,17 +1014,16 @@ class UserManager {
|
|||
)}
|
||||
|
||||
{/* 导出报告对话框 */}
|
||||
{result && (() => {
|
||||
const data = getTempTaskAndIssues();
|
||||
return data ? (
|
||||
<ExportReportDialog
|
||||
open={exportDialogOpen}
|
||||
onOpenChange={setExportDialogOpen}
|
||||
task={data.task}
|
||||
issues={data.issues}
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
{result && (
|
||||
<InstantExportDialog
|
||||
open={exportDialogOpen}
|
||||
onOpenChange={setExportDialogOpen}
|
||||
analysisId={currentAnalysisId}
|
||||
analysisResult={result}
|
||||
language={language}
|
||||
analysisTime={analysisTime}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -196,6 +196,30 @@ export const api = {
|
|||
return {} as InstantAnalysis;
|
||||
},
|
||||
|
||||
async deleteInstantAnalysis(analysisId: string): Promise<void> {
|
||||
await apiClient.delete(`/scan/instant/history/${analysisId}`);
|
||||
},
|
||||
|
||||
async deleteAllInstantAnalyses(): Promise<void> {
|
||||
await apiClient.delete('/scan/instant/history');
|
||||
},
|
||||
|
||||
// ==================== 报告导出方法 ====================
|
||||
|
||||
async exportTaskReportPDF(taskId: string): Promise<Blob> {
|
||||
const res = await apiClient.get(`/tasks/${taskId}/report/pdf`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async exportInstantReportPDF(analysisId: string): Promise<Blob> {
|
||||
const res = await apiClient.get(`/scan/instant/history/${analysisId}/report/pdf`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
|
||||
// ==================== 统计相关方法 ====================
|
||||
|
||||
async getProjectStats(): Promise<{
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@ export interface CodeAnalysisResult {
|
|||
security: number;
|
||||
performance: number;
|
||||
};
|
||||
// 后端返回的额外字段
|
||||
analysis_id?: string;
|
||||
analysis_time?: number;
|
||||
}
|
||||
|
||||
// GitHub/GitLab集成类型
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"version": 2,
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"framework": "vite",
|
||||
"build": {
|
||||
"env": {
|
||||
"VITE_USE_LOCAL_DB": "true"
|
||||
}
|
||||
},
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/assets/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"index":-1,"history":[]}
|
||||
Loading…
Reference in New Issue