feat: 仓库导入、分支模型重构与审查历史摘要

仓库接入
- 新增「从 Gitea 导入」:用全局 Token 列出可见仓库,一键建配置
- 新增仓库只填 Git 地址,自动解析 owner/name,支持 https/ssh/scp 写法
- 新增「测试连接」按钮,保存前即可校验地址并拉取分支

分支模型
- 由单一 base_branch + glob 改为「一个管理分支 + 多个检查分支」
- 管理分支是唯一合并目标;检查分支全部纳入监控
- 支持从任意分支克隆创建管理分支
- 审查基准改为管理分支的 merge-base;仅目标为管理分支的 PR 才自动合并
- 旧库自动迁移:base_branch 播种 managed_branch,branch_patterns 展开为检查分支

审查历史
- 新增 review_records 表与「审查历史」页
- 记录触发来源、PR 链接、审查范围、阻断阈值、发布方式、自动合并设置、
  排除路径、LLM 模型、token 消耗、耗时、需求背景与全部审查意见
- 详情弹窗一览,支持按仓库过滤

AI 摘要
- 新增独立摘要模块(app/lib/summary.js),与代码审查提示词分离
- 专用提示词输出固定四节、500 字内的中文记录:结论/范围/问题/要点
- 与代码审查共用全局 LLM 设置;摘要失败不影响审查与合并,可单条重跑

修复
- 摘要改用内置 fetch:运行镜像没有 curl,原先 spawn curl 必然 ENOENT
- 去掉 blob:none 部分克隆并把凭据写入 .git/config:
  惰性取 blob 不会带上 per-command extraHeader,私有库会报 could not read Username

UI
- 审查背景改为多行文本域(可滚动)
- 分支改为可点选列表,管理分支高亮
- 仓库表格展示管理分支与检查分支
This commit is contained in:
2026-09-20 12:38:23 +08:00
parent ed172bf369
commit 6dc77ce907
12 changed files with 1374 additions and 153 deletions
+51 -6
View File
@@ -23,8 +23,10 @@ header {
}
h1 { font-size: 18px; margin: 0; }
h2 { font-size: 15px; margin: 0 0 12px; }
h3 { font-size: 14px; margin: 0 0 8px; }
.status { color: var(--muted); font-size: 13px; }
nav { display: flex; gap: 4px; padding: 12px 24px 0; }
header .right { display: flex; align-items: center; gap: 12px; }
nav { display: flex; gap: 4px; padding: 12px 24px 0; flex-wrap: wrap; }
.tab {
background: transparent; border: 1px solid transparent; color: var(--muted);
padding: 8px 14px; border-radius: 8px 8px 0 0; cursor: pointer; font-size: 14px;
@@ -33,7 +35,7 @@ nav { display: flex; gap: 4px; padding: 12px 24px 0; }
main { padding: 0 24px 48px; }
.panel { display: none; background: var(--panel); border: 1px solid var(--border); border-radius: 0 8px 8px 8px; padding: 20px; }
.panel.active { display: block; }
.row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; gap: 12px; flex-wrap: wrap; }
button {
background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
padding: 7px 14px; border-radius: 6px; cursor: pointer; font-size: 13px;
@@ -41,6 +43,7 @@ button {
button:hover { border-color: var(--accent); }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button.danger:hover { border-color: var(--err); color: var(--err); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 500; }
@@ -54,17 +57,23 @@ code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; font-si
fieldset { border: 1px solid var(--border); border-radius: 8px; margin: 0 0 18px; padding: 16px; }
legend { color: var(--muted); padding: 0 6px; }
label { display: block; margin-bottom: 10px; color: var(--muted); font-size: 13px; }
input, select {
input, select, textarea {
display: block; width: 100%; margin-top: 4px; padding: 7px 9px;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); font-size: 13px;
color: var(--text); font-size: 13px; font-family: inherit;
}
textarea { resize: vertical; min-height: 120px; max-height: 360px; overflow-y: auto; line-height: 1.6; }
.inline { display: flex; gap: 8px; align-items: flex-start; }
.inline input { flex: 1; }
.inline button { margin-top: 4px; white-space: nowrap; }
.checks { display: flex; flex-wrap: wrap; gap: 16px; margin: 8px 0 14px; }
.checks label { display: flex; align-items: center; gap: 6px; margin: 0; }
.checks input { width: auto; margin: 0; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 0 16px; }
.actions { display: flex; gap: 8px; margin-top: 10px; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.hint { color: var(--muted); font-size: 12px; margin: 6px 0 0; }
.hint.ok { color: var(--ok); }
.hint.err { color: var(--err); }
.log {
background: #0a0c10; border: 1px solid var(--border); border-radius: 6px;
padding: 12px; font-size: 12px; overflow: auto; max-height: 420px; white-space: pre-wrap;
@@ -75,15 +84,51 @@ input, select {
/* Keep this last: it has to beat the display rules above. */
.hidden { display: none !important; }
.banner {
margin: 0 0 16px; padding: 10px 14px; border-radius: 8px; font-size: 13px;
border: 1px solid var(--err); color: var(--err); background: rgba(248,81,73,0.08);
}
.banner.ok { border-color: var(--ok); color: var(--ok); background: rgba(63,185,80,0.08); }
.empty { color: var(--muted); padding: 18px 4px; }
header .right { display: flex; align-items: center; gap: 12px; }
.linkish {
background: none; border: none; color: var(--muted); cursor: pointer;
font-size: 13px; text-decoration: underline; padding: 0;
}
.linkish:hover { color: var(--accent); }
.branch-list { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; min-height: 20px; }
.branch-list button {
padding: 3px 10px; border-radius: 12px; font-size: 12px;
}
.branch-list button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.import-list { max-height: 460px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; margin-top: 10px; }
.import-row {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.import-row:last-child { border-bottom: none; }
.import-row .meta { color: var(--muted); font-size: 12px; }
.import-row .name { font-family: ui-monospace, Consolas, monospace; }
.records { display: flex; flex-direction: column; gap: 12px; }
.record {
border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; background: var(--panel-2);
}
.record-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.record-head .who { font-family: ui-monospace, Consolas, monospace; font-size: 13px; }
.record-meta { color: var(--muted); font-size: 12px; margin-top: 4px; display: flex; gap: 10px; flex-wrap: wrap; }
.record-summary {
margin-top: 10px; padding: 10px 12px; border-left: 3px solid var(--accent);
background: rgba(76,141,255,0.06); border-radius: 0 6px 6px 0;
white-space: pre-wrap; font-size: 13px; line-height: 1.7;
}
.record-summary.pending { border-left-color: var(--warn); color: var(--muted); }
.record-summary.failed { border-left-color: var(--err); color: var(--muted); }
.record-actions { margin-top: 10px; display: flex; gap: 8px; }
.kv { display: grid; grid-template-columns: 140px 1fr; gap: 4px 12px; font-size: 13px; margin: 10px 0; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; word-break: break-all; }
.finding { border-left: 3px solid var(--border); padding: 6px 10px; margin: 6px 0; background: rgba(255,255,255,0.02); }
.finding.blocking { border-left-color: var(--err); }
.finding .where { font-family: ui-monospace, Consolas, monospace; font-size: 12px; color: var(--muted); }