From 6d10c71d15ede2cfb3dc194a01b9e48c02cba16f Mon Sep 17 00:00:00 2001 From: lanrtop Date: Fri, 3 Jul 2026 18:59:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(gitea+mcp):=20PR=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=20+=20poll=5Fnow=20agent=20=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=88=E5=85=AC=E7=BD=91=20Gitea=20webhook=20?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=BE=BE=E9=99=8D=E7=BA=A7=E6=96=B9=E6=A1=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/gitea.rs | 76 +++++++++++++++++++++++++++++++++ src-tauri/src/mcp_server.rs | 13 ++++++ 2 files changed, 89 insertions(+) diff --git a/src-tauri/src/commands/gitea.rs b/src-tauri/src/commands/gitea.rs index 79a7b17..f691417 100644 --- a/src-tauri/src/commands/gitea.rs +++ b/src-tauri/src/commands/gitea.rs @@ -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 { diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index bf19c34..441919d 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -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();