feat(gitea+mcp): PR 状态轮询 + poll_now agent 按钮(公网 Gitea webhook 不可达降级方案)

This commit is contained in:
lanrtop 2026-07-03 18:59:19 +09:00
parent 97b116857f
commit 6d10c71d15
2 changed files with 89 additions and 0 deletions

View File

@ -535,9 +535,85 @@ pub fn poll_all_repos() -> Result<(usize, usize), String> {
crate::db::log_event("gitea_poll", "info",
&format!("轮询 {polled} 个仓库,新采集 {ingested} 条 commit"), None);
}
// PR 状态轮询:弥补公网 Gitea + 内网炼境时 webhook 不可达的场景
let mut pr_ingested = 0usize;
for (project_id, owner, repo, base_url, token) in &repos {
pr_ingested += poll_pr_events(&conn, project_id, owner, repo, base_url, token);
}
if pr_ingested > 0 {
crate::db::log_event("gitea_poll", "info",
&format!("PR 轮询新采集 {pr_ingested} 条事件"), None);
}
Ok((polled, ingested))
}
/// 轮询单个仓库的 PR 状态,将未记录的事件写入 pr_events返回新增条数
fn poll_pr_events(
conn: &rusqlite::Connection,
project_id: &str,
owner: &str,
repo: &str,
base_url: &str,
token: &str,
) -> usize {
let mut count = 0;
for state in ["open", "closed"] {
let url = format!(
"{base_url}/api/v1/repos/{owner}/{repo}/pulls?state={state}&limit=50&type=pullrequest"
);
let resp = match ureq::get(&url)
.set("Authorization", &format!("token {token}"))
.call()
{
Ok(r) => r,
Err(_) => continue,
};
let prs: serde_json::Value = match resp.into_json() {
Ok(j) => j,
Err(_) => continue,
};
let prs = match prs.as_array() {
Some(a) => a,
None => continue,
};
for pr in prs {
let pr_number = match pr["number"].as_i64() {
Some(n) => n,
None => continue,
};
let title = pr["title"].as_str().unwrap_or("").to_string();
let head_branch = pr["head"]["ref"].as_str().unwrap_or("").to_string();
let base_branch = pr["base"]["ref"].as_str().unwrap_or("").to_string();
let author = pr["user"]["login"].as_str().unwrap_or("").to_string();
let merged = pr["merged"].as_bool().unwrap_or(false);
let pr_state = pr["state"].as_str().unwrap_or("open");
// 确定动作:已合并 > 关闭 > 开启
let action = if merged { "merged" } else if pr_state == "closed" { "closed" } else { "opened" };
let existing: i64 = conn.query_row(
"SELECT COUNT(*) FROM pr_events WHERE project_id = ?1 AND pr_number = ?2 AND action = ?3",
params![project_id, pr_number, action],
|r| r.get(0),
).unwrap_or(0);
if existing == 0 {
let id = uuid::Uuid::new_v4().to_string();
if conn.execute(
"INSERT INTO pr_events (id, project_id, pr_number, title, action, head_branch, base_branch, author)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![id, project_id, pr_number, title, action, head_branch, base_branch, author],
).is_ok() {
count += 1;
}
}
}
}
count
}
/// 手动触发一次轮询UI「立即拉取」按钮
#[tauri::command]
pub fn gitea_poll_now() -> Result<serde_json::Value, String> {

View File

@ -540,6 +540,15 @@ fn tools_list_result() -> Value {
"required": ["project_id"]
}
},
{
"name": "poll_now",
"description": "立即触发一次 Gitea 轮询:拉取所有绑定仓库的最新 commit 和 PR 状态写入飞轮。等价于炼境 UI「立即拉取」按钮。适用于公网 Gitea + 内网炼境webhook 不可达)场景。",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "setup_webhook",
"description": "为项目重新注册 Gitea webhook订阅 push + pull_request 事件)。等价于炼境 UI「重设」按钮。导入新项目后、或需要更新事件订阅时调用。",
@ -651,6 +660,10 @@ async fn tools_call(group_id: &str, params: Option<&Value>) -> Value {
let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("");
crate::commands::gitea::gitea_api_create_pull_request(pid, head, base, title, body)
}
"poll_now" => {
crate::commands::gitea::poll_all_repos()
.map(|(polled, ingested)| format!("轮询完成:{polled} 个仓库,新 commit {ingested}PR 事件已同步"))
}
"setup_webhook" => {
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
let port = read_port();