fix: enrich forge notifications with subject body and comment content
This commit is contained in:
parent
1b7d058d3c
commit
de2179540a
1 changed files with 133 additions and 10 deletions
|
|
@ -5,6 +5,9 @@
|
|||
//! unread notification as a broker `Wake { from: "forge" }` message so
|
||||
//! claude's normal turn loop picks it up.
|
||||
//!
|
||||
//! Each notification is enriched with the subject body and/or latest
|
||||
//! comment body so the agent sees actual content, not just a title.
|
||||
//!
|
||||
//! Graceful no-ops:
|
||||
//! - `HIVE_FORGE_URL` not set → disabled (no forge configured)
|
||||
//! - token file absent → disabled (agent has no forge account yet)
|
||||
|
|
@ -21,6 +24,8 @@ use tracing::{debug, info, warn};
|
|||
|
||||
const POLL_INTERVAL_SECS: u64 = 30;
|
||||
const HTTP_TIMEOUT_SECS: u64 = 10;
|
||||
/// Maximum characters of a body/comment to include in the wake message.
|
||||
const BODY_TRUNCATE: usize = 500;
|
||||
|
||||
/// Spawn point: called once from `hive-ag3nt serve`. Returns immediately if
|
||||
/// the forge is not configured for this agent. Otherwise loops forever,
|
||||
|
|
@ -76,6 +81,133 @@ pub async fn run(socket: PathBuf) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fetch a JSON value from a URL using the agent's forge token. Returns
|
||||
/// `None` on any HTTP or parse error (best-effort enrichment).
|
||||
async fn fetch_json(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
token: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
let resp = client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {token}"))
|
||||
.send()
|
||||
.await
|
||||
.ok()?;
|
||||
if !resp.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
resp.json().await.ok()
|
||||
}
|
||||
|
||||
/// Truncate a string to `max` bytes at a char boundary, appending `…` if cut.
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.len() <= max {
|
||||
return s.to_owned();
|
||||
}
|
||||
let end = s
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.take_while(|&i| i <= max - 3)
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
format!("{}…", &s[..end])
|
||||
}
|
||||
|
||||
/// Build a human-readable wake message for one Forgejo notification.
|
||||
/// Fetches the subject and (if present) the latest comment to include
|
||||
/// author and body. Falls back gracefully when API calls fail.
|
||||
async fn format_notification(
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
notif: &serde_json::Value,
|
||||
) -> String {
|
||||
let title = notif["subject"]["title"].as_str().unwrap_or("?");
|
||||
let notif_type = notif["subject"]["type"].as_str().unwrap_or("?");
|
||||
let html_url = notif["subject"]["html_url"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| notif["subject"]["url"].as_str().unwrap_or(""));
|
||||
let repo = notif["repository"]["full_name"].as_str().unwrap_or("?");
|
||||
|
||||
// API URLs for fetching content
|
||||
let subject_api_url = notif["subject"]["url"].as_str().unwrap_or("");
|
||||
let comment_api_url = notif["subject"]["latest_comment_url"].as_str().unwrap_or("");
|
||||
let comment_html_url = notif["subject"]["latest_comment_html_url"]
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
|
||||
// Determine whether this notification was triggered by a comment or by
|
||||
// creation/state-change of the subject itself.
|
||||
let has_comment = !comment_api_url.is_empty() && comment_api_url != subject_api_url;
|
||||
|
||||
if has_comment {
|
||||
// Notification triggered by a new comment.
|
||||
let comment = fetch_json(client, comment_api_url, token).await;
|
||||
let author = comment
|
||||
.as_ref()
|
||||
.and_then(|c| c["user"]["login"].as_str())
|
||||
.unwrap_or("?");
|
||||
let body = comment
|
||||
.as_ref()
|
||||
.and_then(|c| c["body"].as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
|
||||
let kind = match notif_type {
|
||||
"Pull Request" => "comment on PR",
|
||||
"Issue" => "comment on issue",
|
||||
_ => "comment",
|
||||
};
|
||||
let mut out = format!(
|
||||
"[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n",
|
||||
if comment_html_url.is_empty() { html_url } else { comment_html_url },
|
||||
truncate(body, BODY_TRUNCATE),
|
||||
);
|
||||
if out.ends_with('\n') {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
} else {
|
||||
// Notification triggered by creation or state change of the subject.
|
||||
let subject = fetch_json(client, subject_api_url, token).await;
|
||||
let author = subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["user"]["login"].as_str())
|
||||
.unwrap_or("?");
|
||||
let body = subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["body"].as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
let state = subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["state"].as_str())
|
||||
.unwrap_or("");
|
||||
let merged = subject
|
||||
.as_ref()
|
||||
.and_then(|s| s["merged"].as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let kind = match (notif_type, state, merged) {
|
||||
("Pull Request", "closed", true) => "PR merged".to_owned(),
|
||||
("Pull Request", "closed", false) => "PR closed".to_owned(),
|
||||
("Pull Request", _, _) => "new PR".to_owned(),
|
||||
("Issue", "closed", _) => "issue closed".to_owned(),
|
||||
("Issue", _, _) => "new issue".to_owned(),
|
||||
_ => format!("new {notif_type}"),
|
||||
};
|
||||
|
||||
let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}");
|
||||
if !body.is_empty() && !state.contains("closed") && !merged {
|
||||
out.push_str(&format!(
|
||||
"\n\n{author}: {}",
|
||||
truncate(body, BODY_TRUNCATE)
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
async fn poll_once(client: &reqwest::Client, forge_url: &str, token: &str, socket: &Path) {
|
||||
let url = format!("{forge_url}/api/v1/notifications?all=false&limit=50");
|
||||
let resp = match client
|
||||
|
|
@ -115,17 +247,8 @@ async fn poll_once(client: &reqwest::Client, forge_url: &str, token: &str, socke
|
|||
Some(n) => n,
|
||||
None => continue,
|
||||
};
|
||||
let title = notif["subject"]["title"].as_str().unwrap_or("?");
|
||||
let notif_type = notif["subject"]["type"].as_str().unwrap_or("?");
|
||||
let html_url = notif["subject"]["html_url"]
|
||||
.as_str()
|
||||
.or_else(|| notif["subject"]["url"].as_str())
|
||||
.unwrap_or("");
|
||||
let repo = notif["repository"]["full_name"].as_str().unwrap_or("?");
|
||||
|
||||
let body = format!(
|
||||
"forge notification: [{notif_type}] {title}\nrepo: {repo}\nurl: {html_url}"
|
||||
);
|
||||
let body = format_notification(client, token, notif).await;
|
||||
|
||||
let req = hive_sh4re::AgentRequest::Wake {
|
||||
from: "forge".to_owned(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue