feat: 添加 GitLab 仓库支持和认证配置
- 添加 GitLab Token 环境变量和运行时配置支持 - 实现 GitLab API v4 集成(仓库树和文件获取) - 在系统配置界面添加 GitLab Token 配置 - 支持容器环境下访问私有 GitLab 仓库 - 优化错误提示和调试日志 - 更新文档说明 GitLab 使用方法
This commit is contained in:
parent
7aaaa4a900
commit
ab64fde709
19
.env.example
19
.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
|
||||
|
|
|
|||
|
|
@ -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仓库) |
|
||||
|
||||
#### 分析行为配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>GitLab 集成</CardTitle>
|
||||
<CardDescription>配置 GitLab Personal Access Token 以访问私有仓库</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label>GitLab Token(可选)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type={showApiKeys['gitlab'] ? 'text' : 'password'}
|
||||
value={config.gitlabToken}
|
||||
onChange={(e) => updateConfig('gitlabToken', e.target.value)}
|
||||
placeholder="glpat-xxxxxxxxxxxx"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => toggleShowApiKey('gitlab')}
|
||||
>
|
||||
{showApiKeys['gitlab'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
获取:https://gitlab.com/-/profile/personal_access_tokens
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>配置说明</CardTitle>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,26 @@ async function githubApi<T>(url: string, token?: string): Promise<T> {
|
|||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function gitlabApi<T>(url: string, token?: string): Promise<T> {
|
||||
const headers: Record<string, string> = { "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<T>;
|
||||
}
|
||||
|
||||
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`);
|
||||
|
||||
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");
|
||||
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);
|
||||
let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes));
|
||||
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<Array<{ path: string; type: string }>>(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 仓库");
|
||||
}
|
||||
|
||||
// 采样限制,优先分析较小文件与常见语言
|
||||
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<string, string> = {};
|
||||
// 为 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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue