From ab64fde70949ec4c2d13be40f5b7d35835d816e4 Mon Sep 17 00:00:00 2001 From: lintsinghua <1930438860@qq.com> Date: Mon, 27 Oct 2025 15:42:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20GitLab=20=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E6=94=AF=E6=8C=81=E5=92=8C=E8=AE=A4=E8=AF=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 GitLab Token 环境变量和运行时配置支持 - 实现 GitLab API v4 集成(仓库树和文件获取) - 在系统配置界面添加 GitLab Token 配置 - 支持容器环境下访问私有 GitLab 仓库 - 优化错误提示和调试日志 - 更新文档说明 GitLab 使用方法 --- .env.example | 19 ++++- README.md | 5 +- README_EN.md | 5 +- src/components/audit/CreateTaskDialog.tsx | 17 +++- .../audit/TerminalProgressDialog.tsx | 6 +- src/components/system/SystemConfig.tsx | 35 ++++++++ src/features/projects/services/repoScan.ts | 83 ++++++++++++++++--- src/shared/config/env.ts | 3 + 8 files changed, 151 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 19cc27a..a23b9ef 100644 --- a/.env.example +++ b/.env.example @@ -101,10 +101,23 @@ VITE_USE_LOCAL_DB=true # VITE_SUPABASE_URL=https://your-project.supabase.co # VITE_SUPABASE_ANON_KEY=your-anon-key-here -# ==================== GitHub 集成配置 (可选) ==================== -# 用于仓库分析功能 +# ==================== Git 仓库集成配置 (可选) ==================== +# 用于访问私有仓库进行代码审计 + +# GitHub Token # 获取Token: https://github.com/settings/tokens -# VITE_GITHUB_TOKEN=your_github_token_here +# 权限需求: 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 diff --git a/README.md b/README.md index d0a85b5..3eb7315 100644 --- a/README.md +++ b/README.md @@ -637,10 +637,11 @@ pnpm lint > 💡 **提示**:不配置Supabase时,系统以演示模式运行,数据不持久化 -#### GitHub集成配置(可选) +#### Git仓库集成配置(可选) | 变量名 | 必需 | 说明 | |--------|------|------| -| `VITE_GITHUB_TOKEN` | ❌ | GitHub Personal Access Token(用于仓库分析功能) | +| `VITE_GITHUB_TOKEN` | ❌ | GitHub Personal Access Token(用于访问私有GitHub仓库) | +| `VITE_GITLAB_TOKEN` | ❌ | GitLab Personal Access Token(用于访问私有GitLab仓库) | #### 分析行为配置 | 变量名 | 默认值 | 说明 | diff --git a/README_EN.md b/README_EN.md index 110c72d..b212825 100644 --- a/README_EN.md +++ b/README_EN.md @@ -638,10 +638,11 @@ pnpm lint > 💡 **Note**: Without Supabase config, system runs in demo mode without data persistence -#### GitHub Integration Configuration (Optional) +#### Git Repository Integration Configuration (Optional) | Variable | Required | Description | |----------|----------|-------------| -| `VITE_GITHUB_TOKEN` | ❌ | GitHub Personal Access Token (for repository analysis) | +| `VITE_GITHUB_TOKEN` | ❌ | GitHub Personal Access Token (for accessing private GitHub repositories) | +| `VITE_GITLAB_TOKEN` | ❌ | GitLab Personal Access Token (for accessing private GitLab repositories) | #### Analysis Behavior Configuration | Variable | Default | Description | diff --git a/src/components/audit/CreateTaskDialog.tsx b/src/components/audit/CreateTaskDialog.tsx index 36eacc9..bb5701d 100644 --- a/src/components/audit/CreateTaskDialog.tsx +++ b/src/components/audit/CreateTaskDialog.tsx @@ -134,12 +134,27 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr } else { // GitHub/GitLab等远程仓库 console.log('📡 调用 runRepositoryAudit...'); + + // 从运行时配置中获取 Token + const getRuntimeConfig = () => { + try { + const saved = localStorage.getItem('xcodereviewer_runtime_config'); + return saved ? JSON.parse(saved) : null; + } catch { + return null; + } + }; + const runtimeConfig = getRuntimeConfig(); + const githubToken = runtimeConfig?.githubToken || (import.meta.env.VITE_GITHUB_TOKEN as string | undefined); + const gitlabToken = runtimeConfig?.gitlabToken || (import.meta.env.VITE_GITLAB_TOKEN as string | undefined); + taskId = await runRepositoryAudit({ projectId: project.id, repoUrl: project.repository_url!, branch: taskForm.branch_name || project.default_branch || 'main', exclude: taskForm.exclude_patterns, - githubToken: undefined, + githubToken, + gitlabToken, createdBy: 'local-user' }); } diff --git a/src/components/audit/TerminalProgressDialog.tsx b/src/components/audit/TerminalProgressDialog.tsx index 0aad895..f3e9493 100644 --- a/src/components/audit/TerminalProgressDialog.tsx +++ b/src/components/audit/TerminalProgressDialog.tsx @@ -306,11 +306,11 @@ export default function TerminalProgressDialog({ addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); addLog("可能的原因:", "error"); addLog(" • 网络连接问题", "error"); - addLog(" • 仓库访问权限不足", "error"); - addLog(" • GitHub API 限流", "error"); + addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); + addLog(" • GitHub/GitLab API 限流", "error"); addLog(" • 代码文件格式错误", "error"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); - addLog("💡 建议: 检查网络连接和仓库配置后重试", "warning"); + addLog("💡 建议: 检查网络连接、仓库配置和 Token 设置后重试", "warning"); setIsFailed(true); if (pollIntervalRef.current) { diff --git a/src/components/system/SystemConfig.tsx b/src/components/system/SystemConfig.tsx index 392783f..6335ea3 100644 --- a/src/components/system/SystemConfig.tsx +++ b/src/components/system/SystemConfig.tsx @@ -80,6 +80,9 @@ interface SystemConfigData { // GitHub 配置 githubToken: string; + // GitLab 配置 + gitlabToken: string; + // 分析配置 maxAnalyzeFiles: number; llmConcurrency: number; @@ -111,6 +114,7 @@ export function SystemConfig() { doubaoApiKey: '', ollamaBaseUrl: 'http://localhost:11434/v1', githubToken: '', + gitlabToken: '', maxAnalyzeFiles: 40, llmConcurrency: 2, llmGapMs: 500, @@ -170,6 +174,7 @@ export function SystemConfig() { doubaoApiKey: import.meta.env.VITE_DOUBAO_API_KEY || '', ollamaBaseUrl: import.meta.env.VITE_OLLAMA_BASE_URL || 'http://localhost:11434/v1', githubToken: import.meta.env.VITE_GITHUB_TOKEN || '', + gitlabToken: import.meta.env.VITE_GITLAB_TOKEN || '', maxAnalyzeFiles: Number(import.meta.env.VITE_MAX_ANALYZE_FILES) || 40, llmConcurrency: Number(import.meta.env.VITE_LLM_CONCURRENCY) || 2, llmGapMs: Number(import.meta.env.VITE_LLM_GAP_MS) || 500, @@ -611,6 +616,36 @@ export function SystemConfig() { + + + GitLab 集成 + 配置 GitLab Personal Access Token 以访问私有仓库 + + + + GitLab Token(可选) + + updateConfig('gitlabToken', e.target.value)} + placeholder="glpat-xxxxxxxxxxxx" + /> + toggleShowApiKey('gitlab')} + > + {showApiKeys['gitlab'] ? : } + + + + 获取:https://gitlab.com/-/profile/personal_access_tokens + + + + + 配置说明 diff --git a/src/features/projects/services/repoScan.ts b/src/features/projects/services/repoScan.ts index b3d5949..afa88e6 100644 --- a/src/features/projects/services/repoScan.ts +++ b/src/features/projects/services/repoScan.ts @@ -28,12 +28,26 @@ async function githubApi(url: string, token?: string): Promise { return res.json() as Promise; } +async function gitlabApi(url: string, token?: string): Promise { + const headers: Record = { "Content-Type": "application/json" }; + const t = token || (import.meta.env.VITE_GITLAB_TOKEN as string | undefined); + if (t) headers["PRIVATE-TOKEN"] = t; + const res = await fetch(url, { headers }); + if (!res.ok) { + if (res.status === 401) throw new Error("GitLab API 401:请配置 VITE_GITLAB_TOKEN 或确认仓库权限"); + if (res.status === 403) throw new Error("GitLab API 403:请确认仓库权限/频率限制"); + throw new Error(`GitLab API ${res.status}: ${url}`); + } + return res.json() as Promise; +} + export async function runRepositoryAudit(params: { projectId: string; repoUrl: string; branch?: string; exclude?: string[]; githubToken?: string; + gitlabToken?: string; createdBy?: string; }) { const branch = params.branch || "main"; @@ -54,7 +68,12 @@ export async function runRepositoryAudit(params: { const taskId = (task as any).id as string; - console.log(`🚀 GitHub任务已创建: ${taskId},准备启动后台扫描...`); + // 检测仓库类型 + const isGitHub = /github\.com/i.test(params.repoUrl); + const isGitLab = /gitlab\.com|gitlab\./i.test(params.repoUrl); + const repoType = isGitHub ? "GitHub" : isGitLab ? "GitLab" : "Git"; + + console.log(`🚀 ${repoType}任务已创建: ${taskId},准备启动后台扫描...`); // 启动后台审计任务,不阻塞返回 (async () => { @@ -70,14 +89,47 @@ export async function runRepositoryAudit(params: { } as any); console.log(`✅ 任务 ${taskId}: 状态已更新为 running`); - const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i); - if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo"); - const owner = m[1]; - const repo = m[2]; + let files: { path: string; url?: string }[] = []; + + if (isGitHub) { + // GitHub 仓库处理 + const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i); + if (!m) throw new Error("GitHub 仓库 URL 格式错误,例如 https://github.com/owner/repo"); + const owner = m[1]; + const repo = m[2]; + + const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`; + const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken); + files = (tree.tree || []) + .filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes)) + .map(i => ({ path: i.path, url: `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${i.path}` })); + } else if (isGitLab) { + // GitLab 仓库处理 + const m = params.repoUrl.match(/gitlab\.com\/(.+?)\/(.+?)(?:\.git)?$/i); + if (!m) throw new Error("GitLab 仓库 URL 格式错误,例如 https://gitlab.com/owner/repo"); + const projectPath = encodeURIComponent(`${m[1]}/${m[2]}`); + + const treeUrl = `https://gitlab.com/api/v4/projects/${projectPath}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=100`; + console.log(`📡 GitLab API: 获取仓库文件树 - ${treeUrl}`); + const tree = await gitlabApi>(treeUrl, params.gitlabToken); + console.log(`✅ GitLab API: 获取到 ${tree.length} 个项目`); + + files = tree + .filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes)) + .map(i => ({ + path: i.path, + // GitLab 文件 API 路径需要完整的 URL 编码(包括斜杠) + url: `https://gitlab.com/api/v4/projects/${projectPath}/repository/files/${encodeURIComponent(i.path)}/raw?ref=${encodeURIComponent(branch)}` + })); + + console.log(`📝 GitLab: 过滤后可分析文件 ${files.length} 个`); + if (tree.length >= 100) { + console.warn(`⚠️ GitLab: 文件数量达到API限制(100),可能有文件未被扫描。建议使用排除模式减少文件数。`); + } + } else { + throw new Error("不支持的仓库类型,仅支持 GitHub 和 GitLab 仓库"); + } - const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`; - const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken); - let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes)); // 采样限制,优先分析较小文件与常见语言 files = files .sort((a, b) => (a.path.length - b.path.length)) @@ -107,8 +159,17 @@ export async function runRepositoryAudit(params: { const f = files[current]; totalFiles++; try { - const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${f.path}`; - const contentRes = await fetch(rawUrl); + // 使用预先构建的 URL(支持 GitHub 和 GitLab) + const rawUrl = f.url!; + const headers: Record = {}; + // 为 GitLab 添加认证 Token + if (isGitLab) { + const token = params.gitlabToken || (import.meta.env.VITE_GITLAB_TOKEN as string | undefined); + if (token) { + headers["PRIVATE-TOKEN"] = token; + } + } + const contentRes = await fetch(rawUrl, { headers }); if (!contentRes.ok) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } const content = await contentRes.text(); if (content.length > MAX_FILE_SIZE_BYTES) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; } @@ -144,7 +205,7 @@ export async function runRepositoryAudit(params: { } // 每分析一个文件都更新进度,确保实时性 - console.log(`📈 GitHub任务 ${taskId}: 进度 ${totalFiles}/${files.length} (${Math.round(totalFiles/files.length*100)}%)`); + console.log(`📈 ${repoType}任务 ${taskId}: 进度 ${totalFiles}/${files.length} (${Math.round(totalFiles/files.length*100)}%)`); await api.updateAuditTask(taskId, { status: "running", total_files: files.length, diff --git a/src/shared/config/env.ts b/src/shared/config/env.ts index c327d8b..fc80431 100644 --- a/src/shared/config/env.ts +++ b/src/shared/config/env.ts @@ -85,6 +85,9 @@ export const env = { // ==================== GitHub 配置 ==================== GITHUB_TOKEN: runtimeConfig?.githubToken || import.meta.env.VITE_GITHUB_TOKEN || '', + // ==================== GitLab 配置 ==================== + GITLAB_TOKEN: runtimeConfig?.gitlabToken || import.meta.env.VITE_GITLAB_TOKEN || '', + // ==================== 应用配置 ==================== APP_ID: import.meta.env.VITE_APP_ID || 'xcodereviewer',
+ 获取:https://gitlab.com/-/profile/personal_access_tokens +