Merge pull request #71 from vinland100/feat/gitea-support

Feat/gitea support
This commit is contained in:
lintsinghua 2025-12-25 13:18:51 +08:00 committed by GitHub
commit 9bfde8ec24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 3830 additions and 3637 deletions

View File

@ -67,7 +67,7 @@
<td width="50%" align="center">
<strong>🗂️ 项目管理</strong><br/><br/>
<img src="frontend/public/images/README-show/项目管理.png" alt="项目管理" width="95%"><br/>
<em>GitHub/GitLab 导入,多项目协同管理</em>
<em>GitHub/GitLab/Gitea 导入,多项目协同管理</em>
</td>
</tr>
</table>
@ -262,10 +262,22 @@ docker compose up -d
- PostgreSQL 15+
- Docker (用于沙箱)
### 1. 后端启动
### 1. 手动启动数据库
```bash
docker compose up -d redis db
```
### 2. 后端启动
```bash
cd backend
# 配置环境
cp env.example .env
# 使用 uv 管理环境(推荐)
uv sync
source .venv/bin/activate
@ -274,10 +286,13 @@ source .venv/bin/activate
uvicorn app.main:app --reload
```
### 2. 前端启动
### 3. 前端启动
```bash
cd frontend
# 配置环境
cp .env.example .env
pnpm install
pnpm dev
```
@ -378,7 +393,7 @@ DeepSeek-Coder · Codestral<br/>
| 🤖 **Agent 深度审计** | Multi-Agent 协作,自主编排审计策略 | Agent |
| 🧠 **RAG 知识增强** | 代码语义理解CWE/CVE 知识库检索 | Agent |
| 🔒 **沙箱 PoC 验证** | Docker 隔离执行,验证漏洞有效性 | Agent |
| 🗂️ **项目管理** | GitHub/GitLab 导入ZIP 上传10+ 语言支持 | 通用 |
| 🗂️ **项目管理** | GitHub/GitLab/Gitea 导入ZIP 上传10+ 语言支持 | 通用 |
| ⚡ **即时分析** | 代码片段秒级分析,粘贴即用 | 通用 |
| 🔍 **五维检测** | Bug · 安全 · 性能 · 风格 · 可维护性 | 通用 |
| 💡 **What-Why-How** | 精准定位 + 原因解释 + 修复建议 | 通用 |

View File

@ -1 +1 @@
3.13
3.12

View File

@ -18,7 +18,7 @@ from app.models.user import User
from app.models.audit import AuditTask, AuditIssue
from app.models.user_config import UserConfig
import zipfile
from app.services.scanner import scan_repo_task, get_github_files, get_gitlab_files, get_github_branches, get_gitlab_branches, should_exclude, is_text_file
from app.services.scanner import scan_repo_task, get_github_files, get_gitlab_files, get_github_branches, get_gitlab_branches, get_gitea_branches, should_exclude, is_text_file
from app.services.zip_storage import (
save_project_zip, load_project_zip, get_project_zip_meta,
delete_project_zip, has_project_zip
@ -659,9 +659,10 @@ async def get_project_branches(
config = config.scalar_one_or_none()
github_token = settings.GITHUB_TOKEN
gitea_token = settings.GITEA_TOKEN
gitlab_token = settings.GITLAB_TOKEN
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken', 'giteaToken']
if config and config.other_config:
import json
@ -673,12 +674,13 @@ async def get_project_branches(
github_token = decrypted_val
elif field == 'gitlabToken':
gitlab_token = decrypted_val
elif field == 'giteaToken':
gitea_token = decrypted_val
repo_type = project.repository_type or "other"
# 详细日志
print(f"[Branch] 项目: {project.name}, 类型: {repo_type}, URL: {project.repository_url}")
print(f"[Branch] GitHub Token: {'已配置' if github_token else '未配置'}, GitLab Token: {'已配置' if gitlab_token else '未配置'}")
try:
if repo_type == "github":
@ -689,6 +691,10 @@ async def get_project_branches(
if not gitlab_token:
print("[Branch] 警告: GitLab Token 未配置,可能无法访问私有仓库")
branches = await get_gitlab_branches(project.repository_url, gitlab_token)
elif repo_type == "gitea":
if not gitea_token:
print("[Branch] 警告: Gitea Token 未配置,可能无法访问私有仓库")
branches = await get_gitea_branches(project.repository_url, gitea_token)
else:
# 对于其他类型,返回默认分支
print(f"[Branch] 仓库类型 '{repo_type}' 不支持获取分支,返回默认分支")

View File

@ -65,6 +65,9 @@ class Settings(BaseSettings):
# GitLab配置
GITLAB_TOKEN: Optional[str] = None
# Gitea配置
GITEA_TOKEN: Optional[str] = None
# 扫描配置
MAX_ANALYZE_FILES: int = 0 # 最大分析文件数0表示无限制
MAX_FILE_SIZE_BYTES: int = 200 * 1024 # 最大文件大小 200KB

View File

@ -16,7 +16,7 @@ class Project(Base):
# 仓库相关字段 (仅 source_type='repository' 时使用)
repository_url = Column(String, nullable=True)
repository_type = Column(String, default="other") # github, gitlab, other
repository_type = Column(String, default="other") # github, gitlab, gitea, other
default_branch = Column(String, default="main")
programming_languages = Column(Text, default="[]") # Stored as JSON string

View File

@ -1,5 +1,5 @@
"""
仓库扫描服务 - 支持GitHub和GitLab仓库扫描
仓库扫描服务 - 支持GitHub, GitLab Gitea 仓库扫描
"""
import asyncio
@ -9,6 +9,7 @@ from datetime import datetime, timezone
from urllib.parse import urlparse, quote
from sqlalchemy.ext.asyncio import AsyncSession
from app.utils.repo_utils import parse_repository_url
from app.models.audit import AuditTask, AuditIssue
from app.models.project import Project
from app.services.llm.service import LLMService
@ -117,6 +118,25 @@ async def github_api(url: str, token: str = None) -> Any:
return response.json()
async def gitea_api(url: str, token: str = None) -> Any:
"""调用Gitea API"""
headers = {"Content-Type": "application/json"}
t = token or settings.GITEA_TOKEN
if t:
headers["Authorization"] = f"token {t}"
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(url, headers=headers)
if response.status_code == 401:
raise Exception("Gitea API 401请配置 GITEA_TOKEN 或确认仓库权限")
if response.status_code == 403:
raise Exception("Gitea API 403请确认仓库权限/频率限制")
if response.status_code != 200:
raise Exception(f"Gitea API {response.status_code}: {url}")
return response.json()
async def gitlab_api(url: str, token: str = None) -> Any:
"""调用GitLab API"""
headers = {"Content-Type": "application/json"}
@ -149,15 +169,8 @@ async def fetch_file_content(url: str, headers: Dict[str, str] = None) -> Option
async def get_github_branches(repo_url: str, token: str = None) -> List[str]:
"""获取GitHub仓库分支列表"""
match = repo_url.rstrip('/').rstrip('.git')
if 'github.com/' in match:
parts = match.split('github.com/')[-1].split('/')
if len(parts) >= 2:
owner, repo = parts[0], parts[1]
else:
raise Exception("GitHub 仓库 URL 格式错误")
else:
raise Exception("GitHub 仓库 URL 格式错误")
repo_info = parse_repository_url(repo_url, "github")
owner, repo = repo_info['owner'], repo_info['repo']
branches_url = f"https://api.github.com/repos/{owner}/{repo}/branches?per_page=100"
branches_data = await github_api(branches_url, token)
@ -165,10 +178,24 @@ async def get_github_branches(repo_url: str, token: str = None) -> List[str]:
return [b["name"] for b in branches_data]
async def get_gitea_branches(repo_url: str, token: str = None) -> List[str]:
"""获取Gitea仓库分支列表"""
repo_info = parse_repository_url(repo_url, "gitea")
base_url = repo_info['base_url'] # This is {base}/api/v1
owner, repo = repo_info['owner'], repo_info['repo']
branches_url = f"{base_url}/repos/{owner}/{repo}/branches"
branches_data = await gitea_api(branches_url, token)
return [b["name"] for b in branches_data]
async def get_gitlab_branches(repo_url: str, token: str = None) -> List[str]:
"""获取GitLab仓库分支列表"""
parsed = urlparse(repo_url)
base = f"{parsed.scheme}://{parsed.netloc}"
extracted_token = token
if parsed.username:
@ -177,12 +204,11 @@ async def get_gitlab_branches(repo_url: str, token: str = None) -> List[str]:
elif parsed.username and not parsed.password:
extracted_token = parsed.username
path = parsed.path.strip('/').rstrip('.git')
if not path:
raise Exception("GitLab 仓库 URL 格式错误")
repo_info = parse_repository_url(repo_url, "gitlab")
base_url = repo_info['base_url']
project_path = quote(repo_info['project_path'], safe='')
project_path = quote(path, safe='')
branches_url = f"{base}/api/v4/projects/{project_path}/repository/branches?per_page=100"
branches_url = f"{base_url}/projects/{project_path}/repository/branches?per_page=100"
branches_data = await gitlab_api(branches_url, extracted_token)
return [b["name"] for b in branches_data]
@ -191,15 +217,8 @@ async def get_gitlab_branches(repo_url: str, token: str = None) -> List[str]:
async def get_github_files(repo_url: str, branch: str, token: str = None, exclude_patterns: List[str] = None) -> List[Dict[str, str]]:
"""获取GitHub仓库文件列表"""
# 解析仓库URL
match = repo_url.rstrip('/').rstrip('.git')
if 'github.com/' in match:
parts = match.split('github.com/')[-1].split('/')
if len(parts) >= 2:
owner, repo = parts[0], parts[1]
else:
raise Exception("GitHub 仓库 URL 格式错误")
else:
raise Exception("GitHub 仓库 URL 格式错误")
repo_info = parse_repository_url(repo_url, "github")
owner, repo = repo_info['owner'], repo_info['repo']
# 获取仓库文件树
tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{quote(branch)}?recursive=1"
@ -221,7 +240,6 @@ async def get_github_files(repo_url: str, branch: str, token: str = None, exclud
async def get_gitlab_files(repo_url: str, branch: str, token: str = None, exclude_patterns: List[str] = None) -> List[Dict[str, str]]:
"""获取GitLab仓库文件列表"""
parsed = urlparse(repo_url)
base = f"{parsed.scheme}://{parsed.netloc}"
# 从URL中提取token如果存在
extracted_token = token
@ -232,14 +250,12 @@ async def get_gitlab_files(repo_url: str, branch: str, token: str = None, exclud
extracted_token = parsed.username
# 解析项目路径
path = parsed.path.strip('/').rstrip('.git')
if not path:
raise Exception("GitLab 仓库 URL 格式错误")
project_path = quote(path, safe='')
repo_info = parse_repository_url(repo_url, "gitlab")
base_url = repo_info['base_url'] # {base}/api/v4
project_path = quote(repo_info['project_path'], safe='')
# 获取仓库文件树
tree_url = f"{base}/api/v4/projects/{project_path}/repository/tree?ref={quote(branch)}&recursive=true&per_page=100"
tree_url = f"{base_url}/projects/{project_path}/repository/tree?ref={quote(branch)}&recursive=true&per_page=100"
tree_data = await gitlab_api(tree_url, extracted_token)
files = []
@ -247,13 +263,37 @@ async def get_gitlab_files(repo_url: str, branch: str, token: str = None, exclud
if item.get("type") == "blob" and is_text_file(item["path"]) and not should_exclude(item["path"], exclude_patterns):
files.append({
"path": item["path"],
"url": f"{base}/api/v4/projects/{project_path}/repository/files/{quote(item['path'], safe='')}/raw?ref={quote(branch)}",
"url": f"{base_url}/projects/{project_path}/repository/files/{quote(item['path'], safe='')}/raw?ref={quote(branch)}",
"token": extracted_token
})
return files
async def get_gitea_files(repo_url: str, branch: str, token: str = None, exclude_patterns: List[str] = None) -> List[Dict[str, str]]:
"""获取Gitea仓库文件列表"""
repo_info = parse_repository_url(repo_url, "gitea")
base_url = repo_info['base_url']
owner, repo = repo_info['owner'], repo_info['repo']
# Gitea tree API: GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1
# 可以直接使用分支名作为sha
tree_url = f"{base_url}/repos/{owner}/{repo}/git/trees/{quote(branch)}?recursive=1"
tree_data = await gitea_api(tree_url, token)
files = []
for item in tree_data.get("tree", []):
# Gitea API returns 'type': 'blob' for files
if item.get("type") == "blob" and is_text_file(item["path"]) and not should_exclude(item["path"], exclude_patterns):
# 使用API raw endpoint: GET /repos/{owner}/{repo}/raw/{filepath}?ref={branch}
files.append({
"path": item["path"],
"url": f"{base_url}/repos/{owner}/{repo}/raw/{quote(item['path'])}?ref={quote(branch)}",
"token": token # 传递token以便fetch_file_content使用
})
return files
async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = None):
"""
后台仓库扫描任务
@ -312,24 +352,23 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
user_other_config = (user_config or {}).get('otherConfig', {})
github_token = user_other_config.get('githubToken') or settings.GITHUB_TOKEN
gitlab_token = user_other_config.get('gitlabToken') or settings.GITLAB_TOKEN
gitea_token = user_other_config.get('giteaToken') or settings.GITEA_TOKEN
files: List[Dict[str, str]] = []
extracted_gitlab_token = None
# 构建分支尝试顺序(分支降级机制)
branches_to_try = [branch]
if project.default_branch and project.default_branch != branch:
branches_to_try.append(project.default_branch)
for common_branch in ["main", "master"]:
if common_branch not in branches_to_try:
branches_to_try.append(common_branch)
actual_branch = branch # 实际使用的分支
last_error = None
actual_branch = branch
# 构造尝试的分支列表
branches_to_try = [branch]
if branch not in ["main", "master"]:
branches_to_try.extend(["main", "master"])
branches_to_try = list(dict.fromkeys(branches_to_try))
for try_branch in branches_to_try:
try:
print(f"🔄 尝试获取分支 {try_branch} 的文件列表...")
if repo_type == "github":
files = await get_github_files(repo_url, try_branch, github_token, task_exclude_patterns)
elif repo_type == "gitlab":
@ -337,8 +376,10 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
# GitLab文件可能带有token
if files and 'token' in files[0]:
extracted_gitlab_token = files[0].get('token')
elif repo_type == "gitea":
files = await get_gitea_files(repo_url, try_branch, gitea_token, task_exclude_patterns)
else:
raise Exception("不支持的仓库类型,仅支持 GitHub 和 GitLab 仓库")
raise Exception("不支持的仓库类型,仅支持 GitHub, GitLab 和 Gitea 仓库")
if files:
actual_branch = try_branch
@ -410,10 +451,21 @@ async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = N
try:
# 获取文件内容
headers = {}
# 使用提取的 GitLab token 或用户配置的 token
token_to_use = extracted_gitlab_token or gitlab_token
# 使用提取的 token 或用户配置的 token
if repo_type == "gitlab":
token_to_use = file_info.get('token') or gitlab_token
if token_to_use:
headers["PRIVATE-TOKEN"] = token_to_use
elif repo_type == "gitea":
token_to_use = file_info.get('token') or gitea_token
if token_to_use:
headers["Authorization"] = f"token {token_to_use}"
elif repo_type == "github":
# GitHub raw URL 也是直接下载通常public不需要tokenprivate需要
# GitHub raw user content url: raw.githubusercontent.com
if github_token:
headers["Authorization"] = f"Bearer {github_token}"
print(f"📥 正在获取文件: {file_info['path']}")
content = await fetch_file_content(file_info["url"], headers)

View File

View File

@ -0,0 +1,77 @@
from urllib.parse import urlparse, urlunparse
from typing import Dict, Optional
def parse_repository_url(repo_url: str, repo_type: str) -> Dict[str, str]:
"""
Parses a repository URL and returns its components.
Args:
repo_url: The repository URL.
repo_type: The type of repository ('github', 'gitlab', 'gitea').
Returns:
A dictionary containing parsed components:
- base_url: The API base URL (for self-hosted instances) or default API URL.
- owner: The owner/namespace of the repository.
- repo: The repository name.
- server_url: The base URL of the server (scheme + netloc).
Raises:
ValueError: If the URL is invalid or schema/domain check fails.
"""
if not repo_url:
raise ValueError(f"{repo_type} 仓库 URL 不能为空")
# Basic sanitization
repo_url = repo_url.strip()
# Check scheme to prevent SSRF (only allow http and https)
parsed = urlparse(repo_url)
if parsed.scheme not in ('http', 'https'):
raise ValueError(f"{repo_type} 仓库 URL 必须使用 http 或 https 协议")
# Remove .git suffix if present
path = parsed.path.strip('/')
if path.endswith('.git'):
path = path[:-4]
path_parts = path.split('/')
if len(path_parts) < 2:
raise ValueError(f"{repo_type} 仓库 URL 格式错误")
base = f"{parsed.scheme}://{parsed.netloc}"
if repo_type == "github":
# Handle github.com specifically if needed, or assume path_parts are owner/repo
# Case: https://github.com/owner/repo
if 'github.com' in parsed.netloc:
owner, repo = path_parts[-2], path_parts[-1]
api_base = "https://api.github.com"
else:
# Enterprise GitHub or similar?
owner, repo = path_parts[-2], path_parts[-1]
api_base = f"{base}/api/v3" # Assumption for GHE
elif repo_type == "gitlab":
# GitLab supports subgroups, so path could be group/subgroup/repo
# But commonly we just need project path (URL encoded)
# We'll treat the full path as the project path identifier
repo = path_parts[-1]
owner = "/".join(path_parts[:-1])
api_base = f"{base}/api/v4"
elif repo_type == "gitea":
# Gitea: /owner/repo
owner, repo = path_parts[0], path_parts[1]
api_base = f"{base}/api/v1"
else:
raise ValueError(f"不支持的仓库类型: {repo_type}")
return {
"base_url": api_base,
"owner": owner,
"repo": repo,
"project_path": path, # Useful for GitLab
"server_url": base
}

View File

@ -183,6 +183,11 @@ GITHUB_TOKEN=
# 权限要求: read_repository
GITLAB_TOKEN=
# Gitea Access Token
# 获取地址: https://[your-gitea-instance]/user/settings/applications
# 权限要求: read_repository
GITEA_TOKEN=
# =============================================
# 扫描配置
# =============================================

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@
# =============================================
# 使用南京大学镜像站加速拉取 GHCR 镜像
# 部署命令: curl -fsSL https://raw.githubusercontent.com/lintsinghua/DeepAudit/main/docker-compose.prod.cn.yml | docker compose -f - up -d
#
# 镜像加速说明:
# - 原始地址ghcr.io
# - 加速地址ghcr.nju.edu.cn南京大学开源镜像站

View File

@ -93,6 +93,7 @@ services:
- http_proxy=
- https_proxy=
- NO_PROXY=*
- VITE_API_BASE_URL=/api/v1
depends_on:
- backend
networks:

View File

@ -15,7 +15,7 @@
# 后端 API 地址
# - 本地开发: http://localhost:8000/api/v1
# - Docker Compose 部署: /api
VITE_API_BASE_URL=/api
VITE_API_BASE_URL=/api/v1
# =============================================
# Git 仓库集成配置(可选)

View File

@ -33,8 +33,8 @@ RUN pnpm config set network-timeout 300000 && \
# 复制源代码
COPY . .
# 🔥 构建时使用相对路径 /api - Nginx 会处理代理
ENV VITE_API_BASE_URL=/api/v1
# 🔥 构建时使用占位符 - 实现 Build Once Run Anywhere
ENV VITE_API_BASE_URL=__API_BASE_URL__
# 构建生产版本
RUN pnpm build
@ -50,6 +50,11 @@ COPY --from=builder /app/dist /usr/share/nginx/html
# 复制 Nginx 配置 (包含 SSE 反向代理配置)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制启动脚本
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,10 +1,16 @@
#!/bin/sh
set -e
# 替换 API 地址占位符
API_URL="${VITE_API_BASE_URL:-http://localhost:8000/api/v1}"
# 默认为 /api/v1这样即使用户不传参也能配合默认的 nginx 代理工作
API_URL="${VITE_API_BASE_URL:-/api/v1}"
echo "Injecting API URL: $API_URL"
# 在所有 JS 文件中替换占位符
find /app/dist -name '*.js' -exec sed -i "s|__API_BASE_URL__|${API_URL}|g" {} \;
# 注意:这里路径必须是 nginx 实际存放文件的路径
ESCAPED_API_URL=$(echo "${API_URL}" | sed 's/[&/|]/\\&/g')
find /usr/share/nginx/html -name '*.js' -exec sed -i "s|__API_BASE_URL__|${ESCAPED_API_URL}|g" {} \;
# 执行原始命令
exec "$@"

View File

@ -41,7 +41,7 @@ const DEFAULT_MODELS: Record<string, string> = {
interface SystemConfigData {
llmProvider: string; llmApiKey: string; llmModel: string; llmBaseUrl: string;
llmTimeout: number; llmTemperature: number; llmMaxTokens: number;
githubToken: string; gitlabToken: string;
githubToken: string; gitlabToken: string; giteaToken: string;
maxAnalyzeFiles: number; llmConcurrency: number; llmGapMs: number; outputLanguage: string;
}
@ -79,6 +79,7 @@ export function SystemConfig() {
llmMaxTokens: llmConfig.llmMaxTokens || 4096,
githubToken: otherConfig.githubToken || '',
gitlabToken: otherConfig.gitlabToken || '',
giteaToken: otherConfig.giteaToken || '',
maxAnalyzeFiles: otherConfig.maxAnalyzeFiles ?? 0,
llmConcurrency: otherConfig.llmConcurrency || 3,
llmGapMs: otherConfig.llmGapMs || 2000,
@ -98,7 +99,7 @@ export function SystemConfig() {
setConfig({
llmProvider: 'openai', llmApiKey: '', llmModel: '', llmBaseUrl: '',
llmTimeout: 150000, llmTemperature: 0.1, llmMaxTokens: 4096,
githubToken: '', gitlabToken: '',
githubToken: '', gitlabToken: '', giteaToken: '',
maxAnalyzeFiles: 0, llmConcurrency: 3, llmGapMs: 2000, outputLanguage: 'zh-CN',
});
}
@ -107,7 +108,7 @@ export function SystemConfig() {
setConfig({
llmProvider: 'openai', llmApiKey: '', llmModel: '', llmBaseUrl: '',
llmTimeout: 150000, llmTemperature: 0.1, llmMaxTokens: 4096,
githubToken: '', gitlabToken: '',
githubToken: '', gitlabToken: '', giteaToken: '',
maxAnalyzeFiles: 0, llmConcurrency: 3, llmGapMs: 2000, outputLanguage: 'zh-CN',
});
} finally {
@ -126,7 +127,7 @@ export function SystemConfig() {
llmMaxTokens: config.llmMaxTokens,
},
otherConfig: {
githubToken: config.githubToken, gitlabToken: config.gitlabToken,
githubToken: config.githubToken, gitlabToken: config.gitlabToken, giteaToken: config.giteaToken,
maxAnalyzeFiles: config.maxAnalyzeFiles, llmConcurrency: config.llmConcurrency,
llmGapMs: config.llmGapMs, outputLanguage: config.outputLanguage,
},
@ -145,6 +146,7 @@ export function SystemConfig() {
llmMaxTokens: llmConfig.llmMaxTokens || 4096,
githubToken: otherConfig.githubToken || '',
gitlabToken: otherConfig.gitlabToken || '',
giteaToken: otherConfig.giteaToken || '',
maxAnalyzeFiles: otherConfig.maxAnalyzeFiles ?? 0,
llmConcurrency: otherConfig.llmConcurrency || 3,
llmGapMs: otherConfig.llmGapMs || 2000,
@ -612,6 +614,22 @@ export function SystemConfig() {
</a>
</p>
</div>
<div className="space-y-2">
<Label className="text-xs font-bold text-muted-foreground uppercase">Gitea Token ()</Label>
<Input
type="password"
value={config.giteaToken}
onChange={(e) => updateConfig('giteaToken', e.target.value)}
placeholder="sha1_xxxxxxxxxxxx"
className="h-10 cyber-input"
/>
<p className="text-xs text-muted-foreground">
访 Gitea :{' '}
<span className="text-primary">
[your-gitea-instance]/user/settings/applications
</span>
</p>
</div>
<div className="bg-muted border border-border p-4 rounded-lg text-xs">
<p className="font-bold text-muted-foreground flex items-center gap-2 mb-2">
<Info className="w-4 h-4 text-sky-400" />

View File

@ -275,6 +275,7 @@ export default function Projects() {
switch (type) {
case 'github': return <Github className="w-5 h-5" />;
case 'gitlab': return <GitBranch className="w-5 h-5 text-orange-500" />;
case 'gitea': return <GitBranch className="w-5 h-5 text-green-600" />;
default: return <Folder className="w-5 h-5 text-muted-foreground" />;
}
};
@ -486,6 +487,7 @@ export default function Projects() {
<SelectContent className="cyber-dialog border-border">
<SelectItem value="github">GITHUB</SelectItem>
<SelectItem value="gitlab">GITLAB</SelectItem>
<SelectItem value="gitea">GITEA</SelectItem>
<SelectItem value="other">OTHER</SelectItem>
</SelectContent>
</Select>
@ -1016,6 +1018,7 @@ export default function Projects() {
<SelectContent className="cyber-dialog border-border">
<SelectItem value="github">GITHUB</SelectItem>
<SelectItem value="gitlab">GITLAB</SelectItem>
<SelectItem value="gitea">GITEA</SelectItem>
<SelectItem value="other">OTHER</SelectItem>
</SelectContent>
</Select>

View File

@ -30,6 +30,7 @@ export const REPOSITORY_PLATFORMS: Array<{
}> = [
{ value: 'github', label: 'GitHub' },
{ value: 'gitlab', label: 'GitLab' },
{ value: 'gitea', label: 'Gitea' },
{ value: 'other', label: '其他' }
];
@ -58,5 +59,6 @@ export const PLATFORM_COLORS: Record<RepositoryPlatform, {
}> = {
github: { bg: 'bg-foreground', text: 'text-background' },
gitlab: { bg: 'bg-orange-500', text: 'text-white' },
gitea: { bg: 'bg-green-600', text: 'text-white' },
other: { bg: 'bg-muted-foreground', text: 'text-background' }
};

View File

@ -24,7 +24,7 @@ export interface Profile {
export type ProjectSourceType = 'repository' | 'zip';
// 仓库平台类型
export type RepositoryPlatform = 'github' | 'gitlab' | 'other';
export type RepositoryPlatform = 'github' | 'gitlab' | 'gitea' | 'other';
// 项目相关类型
export interface Project {

View File

@ -48,6 +48,7 @@ export function getRepositoryPlatformLabel(platform?: string): string {
const labels: Record<string, string> = {
github: 'GitHub',
gitlab: 'GitLab',
gitea: 'Gitea',
other: '其他'
};
return labels[platform || 'other'] || '其他';