This commit is contained in:
Hua
2025-02-18 16:53:34 +08:00
parent 8b4b4b4181
commit 5cfdc92556
21 changed files with 3139 additions and 0 deletions

680
static/index.html Normal file
View File

@ -0,0 +1,680 @@
<!DOCTYPE html>
<html>
<head>
<title>Code Review 配置</title>
<meta charset="UTF-8">
<meta name="color-scheme" content="light dark">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.container { max-width: 800px; margin-top: 2rem; }
.form-group { margin-bottom: 1rem; }
.password-field {
position: relative;
}
.toggle-password {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
border: none;
background: none;
cursor: pointer;
}
/* 主题切换按钮样式 */
.theme-switcher {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
}
.theme-switcher .dropdown-menu {
min-width: 8rem;
}
.theme-switcher .dropdown-item i {
width: 1rem;
margin-right: 0.5rem;
}
.theme-switcher .dropdown-item.active {
background-color: var(--bs-primary);
color: white;
}
/* 暗色主题下的一些自定义样式 */
[data-bs-theme="dark"] {
color-scheme: dark;
background-color: #1a1d20;
color: #e9ecef;
}
[data-bs-theme="dark"] .card {
background-color: #2b3035;
border-color: #495057;
}
[data-bs-theme="dark"] .form-control {
background-color: #1a1d20;
border-color: #495057;
color: #e9ecef;
}
[data-bs-theme="dark"] .form-control:focus {
background-color: #1a1d20;
border-color: #86b7fe;
color: #e9ecef;
}
/* 暗色主题下的其他元素样式 */
[data-bs-theme="dark"] .card-header {
background-color: #343a40;
border-bottom-color: #495057;
}
[data-bs-theme="dark"] .btn-outline-secondary {
color: #e9ecef;
border-color: #495057;
}
[data-bs-theme="dark"] .btn-outline-secondary:hover {
background-color: #495057;
color: #fff;
}
[data-bs-theme="dark"] .dropdown-menu {
background-color: #2b3035;
border-color: #495057;
}
[data-bs-theme="dark"] .dropdown-item {
color: #e9ecef;
}
[data-bs-theme="dark"] .dropdown-item:hover {
background-color: #343a40;
}
/* 亮色主题样式 */
[data-bs-theme="light"] {
background-color: #ffffff;
color: #212529;
}
</style>
</head>
<body data-bs-theme="light">
<div class="theme-switcher dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-sun-fill"></i>
<span class="d-none d-md-inline ms-1">主题</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><button class="dropdown-item" data-theme="light"><i class="bi bi-sun-fill"></i>亮色</button></li>
<li><button class="dropdown-item" data-theme="dark"><i class="bi bi-moon-fill"></i>暗色</button></li>
<li><button class="dropdown-item" data-theme="auto"><i class="bi bi-circle-half"></i>自动</button></li>
</ul>
</div>
<div class="container">
<div id="loginForm" class="card mb-3">
<div class="card-body">
<div class="form-group">
<label>管理令牌</label>
<input type="password" class="form-control" id="adminToken">
</div>
<button type="button" class="btn btn-primary" onclick="verifyToken()">验证</button>
</div>
</div>
<div id="configSection" style="display: none;">
<h2>Code Review 配置</h2>
<form id="configForm">
<div class="card mb-3">
<div class="card-header">
AI 配置
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addAIService()">添加 AI 服务</button>
</div>
<div class="card-body" id="aiServices">
<!-- AI 服务配置将动态添加到这里 -->
</div>
</div>
<div class="card mb-3">
<div class="card-header">自动禁用配置</div>
<div class="card-body">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="auto_disable.enabled" id="autoDisableEnabled">
<label class="form-check-label" for="autoDisableEnabled">启用自动禁用</label>
</div>
<div class="form-group">
<label>最大失败次数</label>
<input type="number" class="form-control" name="auto_disable.max_failures" min="1" value="3">
</div>
<div class="form-group">
<label>重置时间(分钟)</label>
<input type="number" class="form-control" name="auto_disable.reset_after" min="1" value="30">
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
Git 平台配置
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addGitPlatform()">添加平台</button>
</div>
<div class="card-body">
<div id="gitPlatforms">
<!-- Git 平台配置将动态添加到这里 -->
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">保存配置</button>
</form>
</div>
</div>
<script>
// 添加 Bootstrap JS
document.write('<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"><\/script>');
let adminToken = '';
// 验证令牌
async function verifyToken() {
const token = document.getElementById('adminToken').value;
try {
const response = await fetch('/api/config', {
headers: {
'X-Admin-Token': token
}
});
if (!response.ok) {
throw new Error('令牌验证失败');
}
// 验证成功,保存令牌并显示配置界面
adminToken = token;
document.getElementById('loginForm').style.display = 'none';
document.getElementById('configSection').style.display = 'block';
// 加载配置
loadConfig();
} catch (error) {
alert(error.message);
}
}
// 加载配置
async function loadConfig() {
try {
const response = await fetch('/api/config', {
headers: {
'X-Admin-Token': adminToken
}
});
const config = await response.json();
// 填充自动禁用配置
document.querySelector('[name="auto_disable.enabled"]').checked = config.auto_disable.enabled;
document.querySelector('[name="auto_disable.max_failures"]').value = config.auto_disable.max_failures;
document.querySelector('[name="auto_disable.reset_after"]').value = config.auto_disable.reset_after;
// 填充 AI 服务配置
if (config.ais && config.ais.length > 0) {
const aiServices = document.getElementById('aiServices');
aiServices.innerHTML = ''; // 清空现有内容
config.ais.forEach((ai, index) => {
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, ai));
});
} else {
addAIService(); // 添加一个默认的 AI 服务配置
}
// 填充 Git 配置
const gitPlatforms = document.getElementById('gitPlatforms');
gitPlatforms.innerHTML = ''; // 清空现有内容
console.log('Git config:', config.git); // 调试日志
// 检查并处理 Git 平台配置
if (config.git && config.git.length > 0) {
config.git.forEach((platform, index) => {
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index, platform));
});
} else {
addGitPlatform(); // 添加一个默认的平台配置
}
// 添加调试日志
console.log('Loaded config:', config);
} catch (error) {
console.error('加载配置失败:', error);
alert('加载配置失败: ' + error.message);
}
}
// 默认系统提示词
const defaultSystemMsg = `你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。
你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。
在审查代码时:
- 审查代码变更(差异)并提供反馈
- 仔细检查是否真的存在错误或需要优化的空间,突出显示它们
- 不要突出显示小问题和细节
- 如果有多个评论,请使用要点符号
- 你不需要解释代码的功能
- 请使用中文给出反馈
- 如果你认为不需要优化或修改,请只回复 666`;
function createAIServiceHTML(index, ai = {}) {
return `
<div class="ai-service mb-4" data-index="${index}">
<h5 class="d-flex justify-content-between">
AI 服务 #${index + 1}
<button type="button" class="btn btn-sm btn-danger" onclick="removeAIService(${index})">删除</button>
</h5>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="ais[${index}].enabled" ${ai.enabled ? 'checked' : ''}>
<label class="form-check-label">启用服务</label>
</div>
<div class="form-group">
<label>名称</label>
<input type="text" class="form-control" name="ais[${index}].name"
value="${ai.name || ''}" required
title="请输入 AI 服务名称">
</div>
<div class="form-group">
<label>类型</label>
<select class="form-control" name="ais[${index}].type"
onchange="toggleAPIKeyField(${index})" required
title="请选择 AI 服务类型">
<option value="openai" ${ai.type === 'openai' ? 'selected' : ''}>OpenAI</option>
<option value="ollama" ${ai.type === 'ollama' ? 'selected' : ''}>Ollama</option>
</select>
</div>
<div class="form-group">
<label>API URL</label>
<input type="url" class="form-control" name="ais[${index}].url"
value="${ai.url || ''}" required
title="请输入有效的 API URL"
pattern="https?://.+">
</div>
<div class="api-key-field" style="display: ${ai.type === 'ollama' ? 'none' : 'block'}">
${createPasswordField(`ais[${index}].api_key`, ai.api_key, 'API Key')}
</div>
<div class="form-group">
<label>模型</label>
<input type="text" class="form-control" name="ais[${index}].model"
value="${ai.model || ''}" required
title="请输入模型名称">
</div>
<div class="form-group">
<label>Temperature</label>
<input type="number" class="form-control" name="ais[${index}].temperature"
value="${ai.temperature ?? 0}" min="0" max="2" step="0.1" required>
<small class="form-text text-muted">控制输出的随机性0-20 表示最确定的输出</small>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="ais[${index}].stream" ${ai.stream ? 'checked' : ''}>
<label class="form-check-label">启用流式响应</label>
<small class="form-text text-muted d-block">启用后可以逐步获取 AI 的响应</small>
</div>
<div class="form-group">
<label>系统提示词</label>
<textarea class="form-control" name="ais[${index}].system_msg" rows="3"
placeholder="设置与 AI 对话的系统提示词,用于定义 AI 的角色和行为"
>${ai.system_msg || defaultSystemMsg}</textarea>
</div>
<div class="form-group">
<label>权重</label>
<input type="number" class="form-control" name="ais[${index}].weight" value="${ai.weight || 1}" min="1" required>
<small class="form-text text-muted">在相同优先级的 AI 中,按权重比例分配请求</small>
</div>
<div class="form-group">
<label>优先级</label>
<input type="number" class="form-control" name="ais[${index}].priority" value="${ai.priority || 0}" min="0" required>
<small class="form-text text-muted">数字越大优先级越高,优先级相同的 AI 将按权重负载均衡</small>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="ais[${index}].auto_disable" ${ai.auto_disable ? 'checked' : ''}>
<label class="form-check-label">使用自定义自动禁用配置</label>
</div>
<div class="form-group">
<label>最大失败次数(可选)</label>
<input type="number" class="form-control" name="ais[${index}].max_failures" min="1" value="${ai.max_failures || ''}">
</div>
<div class="form-group">
<label>重置时间(分钟,可选)</label>
<input type="number" class="form-control" name="ais[${index}].reset_after" min="1" value="${ai.reset_after || ''}">
</div>
</div>
`;
}
function addAIService() {
const aiServices = document.getElementById('aiServices');
const index = aiServices.children.length;
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, {
system_msg: defaultSystemMsg,
temperature: 0,
stream: false
}));
}
function removeAIService(index) {
const service = document.querySelector(`.ai-service[data-index="${index}"]`);
if (service) {
service.remove();
}
}
// 添加检查平台名称唯一性的函数
function isGitPlatformNameUnique(name, currentIndex) {
const platforms = document.querySelectorAll('.git-platform');
for (let i = 0; i < platforms.length; i++) {
if (i === currentIndex) continue;
const nameInput = platforms[i].querySelector('input[name$="].name"]');
if (nameInput && nameInput.value.trim() === name.trim()) {
return false;
}
}
return true;
}
// 修改 createGitPlatformHTML 函数,添加 oninput 事件处理
function createGitPlatformHTML(index, platform = {}) {
// 添加调试日志
console.log('Creating Git platform HTML with:', platform);
return `
<div class="git-platform mb-4" data-index="${index}">
<h5 class="d-flex justify-content-between">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<input type="text" class="form-control" name="git[${index}].name"
value="${platform.name || ''}" placeholder="平台名称" required
title="请输入 Git 平台名称"
oninput="validateGitPlatformName(this, ${index})"
data-original-value="${platform.name || ''}">
<div class="invalid-feedback">平台名称已存在,请使用其他名称</div>
</div>
<button type="button" class="btn btn-sm btn-danger" onclick="removeGitPlatform(${index})">删除</button>
</h5>
<div class="form-group">
<label>平台类型</label>
<select class="form-control" name="git[${index}].type" required
title="请选择 Git 平台类型">
<option value="gitlab" ${platform.type === 'gitlab' ? 'selected' : ''}>GitLab</option>
<option value="gitee" ${platform.type === 'gitee' ? 'selected' : ''}>Gitee</option>
</select>
</div>
${createPasswordField(`git[${index}].token`, platform.token, 'Token', '请输入平台访问令牌')}
${createPasswordField(`git[${index}].webhook_secret`, platform.webhook_secret, 'Webhook Secret', '请输入 Webhook 密钥')}
<div class="form-group">
<label>API Base URL</label>
<input type="url" class="form-control" name="git[${index}].api_base"
value="${platform.api_base || ''}" required
title="请输入有效的 API Base URL"
pattern="https?://.+">
</div>
</div>
`;
}
// 添加验证平台名称的函数
function validateGitPlatformName(input, index) {
const name = input.value.trim();
const originalValue = input.dataset.originalValue;
// 如果是编辑现有平台且名称未改变,则不需要验证
if (name === originalValue) {
input.setCustomValidity('');
input.classList.remove('is-invalid');
return true;
}
if (!isGitPlatformNameUnique(name, index)) {
input.setCustomValidity('平台名称必须唯一');
input.classList.add('is-invalid');
return false;
} else {
input.setCustomValidity('');
input.classList.remove('is-invalid');
return true;
}
}
function addGitPlatform() {
const gitPlatforms = document.getElementById('gitPlatforms');
const index = gitPlatforms.children.length;
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index));
}
function removeGitPlatform(index) {
const platform = document.querySelector(`.git-platform[data-index="${index}"]`);
if (platform) {
platform.remove();
}
}
// 修改表单提交处理,添加额外的验证
document.getElementById('configForm').addEventListener('submit', async (e) => {
e.preventDefault();
// 验证所有 Git 平台名称的唯一性
const gitPlatformNames = new Set();
const platforms = document.querySelectorAll('.git-platform input[name$="].name"]');
let hasNameConflict = false;
platforms.forEach((input, index) => {
const name = input.value.trim();
if (gitPlatformNames.has(name)) {
hasNameConflict = true;
input.setCustomValidity('平台名称必须唯一');
input.classList.add('is-invalid');
} else {
gitPlatformNames.add(name);
}
});
if (hasNameConflict) {
e.target.reportValidity();
return;
}
// 使用 HTML5 原生表单验证
const form = e.target;
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const formEntries = Array.from(formData.entries()).reduce((acc, [key, value]) => {
// 处理复选框和数字类型的值
if (key.endsWith('.enabled') || key.endsWith('.auto_disable') || key.endsWith('.stream')) {
acc[key] = value === 'on';
} else if (key.includes('.temperature')) {
acc[key] = parseFloat(value);
} else if (key.includes('.weight') || key.includes('.max_failures') ||
key.includes('.reset_after') || key === 'port') {
acc[key] = value ? parseInt(value) : null;
} else {
acc[key] = value;
}
return acc;
}, {});
const config = unflattenObject(formEntries);
// 转换 Git 配置格式
if (config.git && Array.isArray(config.git)) {
config.git = {
platforms: config.git.map(platform => ({
name: platform.name,
type: platform.type,
token: platform.token,
webhook_secret: platform.webhook_secret,
api_base: platform.api_base,
signature_header: platform.type === 'gitlab' ? 'X-Gitlab-Token' : 'X-Gitee-Token',
event_header: platform.type === 'gitlab' ? 'X-Gitlab-Event' : 'X-Gitee-Event'
}))
};
}
// 处理 AI 配置中的空值
if (config.ais) {
config.ais = config.ais.map(ai => {
// 如果 max_failures 和 reset_after 为空,则设置为 null
if (!ai.max_failures) ai.max_failures = null;
if (!ai.reset_after) ai.reset_after = null;
return ai;
});
}
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Admin-Token': adminToken
},
body: JSON.stringify(config)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '更新配置失败');
}
alert('配置已更新');
// 重新加载配置以显示最新状态
loadConfig();
} catch (error) {
alert(error.message);
console.error('保存配置失败:', error);
}
});
// 辅助函数:展平对象
function flattenObject(obj, prefix = '') {
return Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
if (typeof obj[k] === 'object' && obj[k] !== null) {
Object.assign(acc, flattenObject(obj[k], pre + k));
} else {
acc[pre + k] = obj[k];
}
return acc;
}, {});
}
// 辅助函数:还原对象
function unflattenObject(obj) {
const result = {};
Object.entries(obj).forEach(([key, value]) => {
const parts = key.split('.');
let current = result;
while (parts.length > 1) {
const part = parts.shift();
current = current[part] = current[part] || {};
}
current[parts[0]] = value;
});
return result;
}
// 添加切换密码显示的函数
function togglePassword(button) {
const input = button.previousElementSibling;
const type = input.getAttribute('type');
input.setAttribute('type', type === 'password' ? 'text' : 'password');
button.innerHTML = type === 'password' ?
'<i class="bi bi-eye-slash"></i>' :
'<i class="bi bi-eye"></i>';
}
// 创建带切换按钮的密码输入框
function createPasswordField(name, value, label, title = '') {
return `
<div class="form-group">
<label>${label}</label>
<div class="password-field">
<input type="password" class="form-control"
name="${name}"
value="${value || ''}"
${name.includes('api_key') ? '' : 'required'}
title="${title || `请输入${label}`}">
<button type="button" class="toggle-password" onclick="togglePassword(this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
`;
}
// 添加切换 API Key 字段显示的函数
function toggleAPIKeyField(index) {
const aiService = document.querySelector(`.ai-service[data-index="${index}"]`);
const typeSelect = aiService.querySelector('select[name^="ais"][name$=".type"]');
const apiKeyField = aiService.querySelector('.api-key-field');
const apiKeyInput = apiKeyField.querySelector('input');
if (typeSelect.value === 'ollama') {
apiKeyField.style.display = 'none';
if (apiKeyInput) {
apiKeyInput.value = '';
apiKeyInput.removeAttribute('required');
}
} else {
apiKeyField.style.display = 'block';
if (apiKeyInput) {
apiKeyInput.setAttribute('required', '');
}
}
}
// 主题切换功能
function setTheme(theme) {
const body = document.body;
const button = document.querySelector('.theme-switcher .dropdown-toggle i');
// 更新下拉菜单选中状态
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
item.classList.toggle('active', item.dataset.theme === theme);
});
if (theme === 'auto') {
// 检测系统主题
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
body.setAttribute('data-bs-theme', prefersDark ? 'dark' : 'light');
button.className = 'bi bi-circle-half';
} else {
body.setAttribute('data-bs-theme', theme);
button.className = theme === 'light' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
}
// 保存主题设置
localStorage.setItem('theme', theme);
}
// 初始化主题
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (localStorage.getItem('theme') === 'auto') {
document.body.setAttribute('data-bs-theme', e.matches ? 'dark' : 'light');
}
});
// 添加主题切换事件监听
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
item.addEventListener('click', () => setTheme(item.dataset.theme));
});
}
// 页面加载时初始化主题
document.addEventListener('DOMContentLoaded', initTheme);
</script>
<!-- 在 body 末尾添加 Bootstrap Icons CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
</body>
</html>