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:
lintsinghua 2025-11-28 20:34:15 +08:00
parent c54212a8c9
commit 9054f0d2c5
26 changed files with 2005 additions and 2142 deletions

View File

@ -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
# 方式2Supabase 云端数据库(支持多设备同步)
# 如果不配置,系统将以演示模式运行,数据不会持久化
# 获取配置: 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 官方及兼容服务)

View File

@ -1,12 +0,0 @@
node_modules
.git
.vscode
.DS_Store
*.log
.env.local
.env.me
backend
dist
tests
history
patches

View File

@ -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
View File

@ -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">
[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/lintsinghua/XCodeReviewer/releases)
[![Version](https://img.shields.io/badge/version-1.3.4-blue.svg)](https://github.com/lintsinghua/XCodeReviewer/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![React](https://img.shields.io/badge/React-18-61dafb.svg)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-5.1-646cff.svg)](https://vitejs.dev/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688.svg)](https://fastapi.tiangolo.com/)
[![Python](https://img.shields.io/badge/Python-3.13+-3776ab.svg)](https://www.python.org/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lintsinghua/XCodeReviewer)
[![Stars](https://img.shields.io/github/stars/lintsinghua/XCodeReviewer?style=social)](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
[![Deploy with Vercel](https://vercel.com/button)](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、ARM64Mac 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
# 方式2Supabase 云端数据库(支持多设备同步)
# 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**!您的支持是我们不断前进的动力!
[![Star History](https://api.star-history.com/svg?repos=lintsinghua/XCodeReviewer&type=Date)](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)**
- 贡献者的代码、内容或建议**不代表项目官方观点**,其准确性、安全性及合规性由贡献者自行负责。

View File

@ -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">
[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](https://github.com/lintsinghua/XCodeReviewer/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![React](https://img.shields.io/badge/React-18-61dafb.svg)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-5.1-646cff.svg)](https://vitejs.dev/)
[![Stars](https://img.shields.io/github/stars/lintsinghua/XCodeReviewer?style=social)](https://github.com/lintsinghua/XCodeReviewer/stargazers)
[![Forks](https://img.shields.io/github/forks/lintsinghua/XCodeReviewer?style=social)](https://github.com/lintsinghua/XCodeReviewer/network/members)
[![Sponsor](https://img.shields.io/badge/Sponsor-赞助-blueviolet)](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:
![System Architecture Diagram](public/diagram.svg)
<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
![Intelligent Dashboard](public/images/example1.png)
*Real-time display of project statistics, quality trends, and system performance, providing comprehensive code audit overview*
#### ⚡ Instant Analysis
![Instant Analysis](public/images/example2.png)
*Support for quick code snippet analysis with detailed What-Why-How explanations and fix suggestions*
#### 🚀 Project Management
![Project Management](public/images/example3.png)
*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:
[![Deploy with Vercel](https://vercel.com/button)](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!
[![Contributors](https://contrib.rocks/image?repo=lintsinghua/XCodeReviewer)](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!
[![Star History](https://api.star-history.com/svg?repos=lintsinghua/XCodeReviewer&type=Date)](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.

View File

@ -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"]

View File

@ -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

View File

@ -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}"'
}
)

View File

@ -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}"'
}
)

View File

@ -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"""

View File

@ -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)

View File

@ -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"
)

View File

@ -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",
]

32
backend/test_logo.py Normal file
View File

@ -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()

View File

@ -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" },
]

View File

@ -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

42
frontend/.env.example Normal file
View File

@ -0,0 +1,42 @@
# ========================================
# XCodeReviewer 前端环境变量配置示例
# ========================================
# 复制此文件为 .env 并填写你的配置
# 注意前后端分离架构下LLM 配置主要在后端 backend/.env 中设置
# ==================== 后端 API 配置 ====================
# 后端 API 地址Docker Compose 部署时使用)
VITE_API_BASE_URL=/api
# ==================== 数据库配置 ====================
# 方式1本地数据库推荐开箱即用
VITE_USE_LOCAL_DB=true
# 方式2Supabase 云端数据库(支持多设备同步)
# 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 中

View File

@ -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

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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<{

View File

@ -209,6 +209,9 @@ export interface CodeAnalysisResult {
security: number;
performance: number;
};
// 后端返回的额外字段
analysis_id?: string;
analysis_time?: number;
}
// GitHub/GitLab集成类型

View File

@ -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"
}
]
}
]
}

View File

@ -1 +0,0 @@
{"index":-1,"history":[]}