mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: agent-only announce path, BB message IDs, sender identity, SSRF allowlist (#23970)
* fix(agents): defer announces until descendant cleanup settles * fix(bluebubbles): harden message metadata extraction * feat(contributors): rank by composite score (commits, PRs, LOC, tenure) * refactor(control-ui): move method guard after path checks to improve request handling * fix subagent completion announce when only current run is pending * fix(subagents): keep orchestrator runs active until descendants finish * fix: prepare PR feedback follow-ups (#23970) (thanks @tyler6204)
This commit is contained in:
@@ -45,8 +45,10 @@ for (const login of ensureLogins) {
|
||||
}
|
||||
}
|
||||
|
||||
const log = run("git log --format=%aN%x7c%aE --numstat");
|
||||
// %x1f = unit separator to avoid collisions with author names containing "|"
|
||||
const log = run("git log --reverse --format=%aN%x1f%aE%x1f%aI --numstat");
|
||||
const linesByLogin = new Map<string, number>();
|
||||
const firstCommitByLogin = new Map<string, string>();
|
||||
|
||||
let currentName: string | null = null;
|
||||
let currentEmail: string | null = null;
|
||||
@@ -56,10 +58,21 @@ for (const line of log.split("\n")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.includes("|") && !/^[0-9-]/.test(line)) {
|
||||
const [name, email] = line.split("|", 2);
|
||||
if (line.includes("\x1f") && !/^[0-9-]/.test(line)) {
|
||||
const [name, email, date] = line.split("\x1f", 3);
|
||||
currentName = name?.trim() ?? null;
|
||||
currentEmail = email?.trim().toLowerCase() ?? null;
|
||||
|
||||
// Track first commit date per login (log is --reverse so first seen = earliest)
|
||||
if (currentName && date) {
|
||||
const login = resolveLogin(currentName, currentEmail, apiByLogin, nameToLogin, emailToLogin);
|
||||
if (login) {
|
||||
const key = login.toLowerCase();
|
||||
if (!firstCommitByLogin.has(key)) {
|
||||
firstCommitByLogin.set(key, date.slice(0, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -68,7 +81,13 @@ for (const line of log.split("\n")) {
|
||||
}
|
||||
|
||||
const parts = line.split("\t");
|
||||
if (parts.length < 2) {
|
||||
if (parts.length < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip docs paths so bulk-generated i18n scaffolds don't inflate rankings
|
||||
const filePath = parts[2];
|
||||
if (filePath.startsWith("docs/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -94,6 +113,43 @@ for (const login of ensureLogins) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch merged PRs and count per author
|
||||
const prsByLogin = new Map<string, number>();
|
||||
const prRaw = run(
|
||||
`gh pr list -R ${REPO} --state merged --limit 5000 --json author --jq '.[].author.login'`,
|
||||
);
|
||||
for (const login of prRaw.split("\n")) {
|
||||
const trimmed = login.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
prsByLogin.set(trimmed, (prsByLogin.get(trimmed) ?? 0) + 1);
|
||||
}
|
||||
|
||||
// Repo epoch for tenure calculation (root commit date)
|
||||
const rootCommit = run("git rev-list --max-parents=0 HEAD").split("\n")[0];
|
||||
const repoEpochStr = run(`git log --format=%aI -1 ${rootCommit}`);
|
||||
const repoEpoch = new Date(repoEpochStr.slice(0, 10)).getTime();
|
||||
const nowDate = new Date().toISOString().slice(0, 10);
|
||||
const now = new Date(nowDate).getTime();
|
||||
const repoAgeDays = Math.max(1, (now - repoEpoch) / 86_400_000);
|
||||
|
||||
// Composite score:
|
||||
// base = commits*2 + merged_PRs*10 + sqrt(code_LOC)
|
||||
// tenure = 1.0 + (days_since_first_commit / repo_age)^2 * 0.5
|
||||
// score = base * tenure
|
||||
// Squared curve: only true early contributors get meaningful boost.
|
||||
// Day-1 = 1.5x, halfway through repo life = 1.125x, recent = ~1.0x.
|
||||
function computeScore(loc: number, commits: number, prs: number, firstDate: string): number {
|
||||
const base = commits * 2 + prs * 10 + Math.sqrt(loc);
|
||||
const daysIn = firstDate
|
||||
? Math.max(0, (now - new Date(firstDate.slice(0, 10)).getTime()) / 86_400_000)
|
||||
: 0;
|
||||
const tenureRatio = Math.min(1, daysIn / repoAgeDays);
|
||||
const tenure = 1.0 + tenureRatio * tenureRatio * 0.5;
|
||||
return base * tenure;
|
||||
}
|
||||
|
||||
const entriesByKey = new Map<string, Entry>();
|
||||
|
||||
for (const seed of seedEntries) {
|
||||
@@ -111,6 +167,7 @@ for (const seed of seedEntries) {
|
||||
apiByLogin.set(key, user);
|
||||
const existing = entriesByKey.get(key);
|
||||
if (!existing) {
|
||||
const fd = firstCommitByLogin.get(key) ?? "";
|
||||
entriesByKey.set(key, {
|
||||
key,
|
||||
login: user.login,
|
||||
@@ -118,6 +175,10 @@ for (const seed of seedEntries) {
|
||||
html_url: user.html_url,
|
||||
avatar_url: user.avatar_url,
|
||||
lines: 0,
|
||||
commits: 0,
|
||||
prs: 0,
|
||||
score: 0,
|
||||
firstCommitDate: fd,
|
||||
});
|
||||
} else {
|
||||
existing.display = existing.display || seed.display;
|
||||
@@ -150,28 +211,40 @@ for (const item of contributors) {
|
||||
|
||||
const existing = entriesByKey.get(key);
|
||||
if (!existing) {
|
||||
const lines = linesByLogin.get(key) ?? 0;
|
||||
const contributions = contributionsByLogin.get(key) ?? 0;
|
||||
const loc = linesByLogin.get(key) ?? 0;
|
||||
const commits = contributionsByLogin.get(key) ?? 0;
|
||||
const prs = prsByLogin.get(key) ?? 0;
|
||||
const fd = firstCommitByLogin.get(key) ?? "";
|
||||
entriesByKey.set(key, {
|
||||
key,
|
||||
login: user.login,
|
||||
display: pickDisplay(baseName, user.login),
|
||||
html_url: user.html_url,
|
||||
avatar_url: normalizeAvatar(user.avatar_url),
|
||||
lines: lines > 0 ? lines : contributions,
|
||||
lines: loc > 0 ? loc : commits,
|
||||
commits,
|
||||
prs,
|
||||
score: computeScore(loc, commits, prs, fd),
|
||||
firstCommitDate: fd,
|
||||
});
|
||||
} else {
|
||||
existing.login = user.login;
|
||||
existing.display = pickDisplay(baseName, user.login, existing.display);
|
||||
existing.html_url = user.html_url;
|
||||
existing.avatar_url = normalizeAvatar(user.avatar_url);
|
||||
const lines = linesByLogin.get(key) ?? 0;
|
||||
const contributions = contributionsByLogin.get(key) ?? 0;
|
||||
existing.lines = Math.max(existing.lines, lines > 0 ? lines : contributions);
|
||||
const loc = linesByLogin.get(key) ?? 0;
|
||||
const commits = contributionsByLogin.get(key) ?? 0;
|
||||
const prs = prsByLogin.get(key) ?? 0;
|
||||
const fd = firstCommitByLogin.get(key) ?? existing.firstCommitDate;
|
||||
existing.lines = Math.max(existing.lines, loc > 0 ? loc : commits);
|
||||
existing.commits = Math.max(existing.commits, commits);
|
||||
existing.prs = Math.max(existing.prs, prs);
|
||||
existing.firstCommitDate = fd || existing.firstCommitDate;
|
||||
existing.score = Math.max(existing.score, computeScore(loc, commits, prs, fd));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [login, lines] of linesByLogin.entries()) {
|
||||
for (const [login, loc] of linesByLogin.entries()) {
|
||||
if (entriesByKey.has(login)) {
|
||||
continue;
|
||||
}
|
||||
@@ -180,14 +253,20 @@ for (const [login, lines] of linesByLogin.entries()) {
|
||||
user = fetchUser(login) || undefined;
|
||||
}
|
||||
if (user) {
|
||||
const contributions = contributionsByLogin.get(login) ?? 0;
|
||||
const commits = contributionsByLogin.get(login) ?? 0;
|
||||
const prs = prsByLogin.get(login) ?? 0;
|
||||
const fd = firstCommitByLogin.get(login) ?? "";
|
||||
entriesByKey.set(login, {
|
||||
key: login,
|
||||
login: user.login,
|
||||
display: displayName[user.login.toLowerCase()] ?? user.login,
|
||||
html_url: user.html_url,
|
||||
avatar_url: normalizeAvatar(user.avatar_url),
|
||||
lines: lines > 0 ? lines : contributions,
|
||||
lines: loc > 0 ? loc : commits,
|
||||
commits,
|
||||
prs,
|
||||
score: computeScore(loc, commits, prs, fd),
|
||||
firstCommitDate: fd,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -195,22 +274,22 @@ for (const [login, lines] of linesByLogin.entries()) {
|
||||
const entries = Array.from(entriesByKey.values());
|
||||
|
||||
entries.sort((a, b) => {
|
||||
if (b.lines !== a.lines) {
|
||||
return b.lines - a.lines;
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return a.display.localeCompare(b.display);
|
||||
});
|
||||
|
||||
const lines: string[] = [];
|
||||
const htmlLines: string[] = [];
|
||||
for (let i = 0; i < entries.length; i += PER_LINE) {
|
||||
const chunk = entries.slice(i, i + PER_LINE);
|
||||
const parts = chunk.map((entry) => {
|
||||
return `<a href="${entry.html_url}"><img src="${entry.avatar_url}" width="48" height="48" alt="${entry.display}" title="${entry.display}"/></a>`;
|
||||
});
|
||||
lines.push(` ${parts.join(" ")}`);
|
||||
htmlLines.push(` ${parts.join(" ")}`);
|
||||
}
|
||||
|
||||
const block = `${lines.join("\n")}\n`;
|
||||
const block = `${htmlLines.join("\n")}\n`;
|
||||
const readme = readFileSync(readmePath, "utf8");
|
||||
const start = readme.indexOf('<p align="left">');
|
||||
const end = readme.indexOf("</p>", start);
|
||||
@@ -223,6 +302,24 @@ const next = `${readme.slice(0, start)}<p align="left">\n${block}${readme.slice(
|
||||
writeFileSync(readmePath, next);
|
||||
|
||||
console.log(`Updated README clawtributors: ${entries.length} entries`);
|
||||
console.log(`\nTop 25 by composite score: (commits*2 + PRs*10 + sqrt(LOC)) * tenure`);
|
||||
console.log(` tenure = 1.0 + (days_since_first_commit / repo_age)^2 * 0.5`);
|
||||
console.log(
|
||||
`${"#".padStart(3)} ${"login".padEnd(24)} ${"score".padStart(8)} ${"tenure".padStart(7)} ${"commits".padStart(8)} ${"PRs".padStart(6)} ${"LOC".padStart(10)} first commit`,
|
||||
);
|
||||
console.log("-".repeat(85));
|
||||
for (const entry of entries.slice(0, 25)) {
|
||||
const login = (entry.login ?? entry.key).slice(0, 24);
|
||||
const fd = entry.firstCommitDate || "?";
|
||||
const daysIn =
|
||||
fd !== "?" ? Math.max(0, (now - new Date(fd.slice(0, 10)).getTime()) / 86_400_000) : 0;
|
||||
const tr = Math.min(1, daysIn / repoAgeDays);
|
||||
const tenure = 1.0 + tr * tr * 0.5;
|
||||
console.log(
|
||||
`${entries.indexOf(entry) + 1}`.padStart(3) +
|
||||
` ${login.padEnd(24)} ${entry.score.toFixed(0).padStart(8)} ${tenure.toFixed(2).padStart(6)}x ${String(entry.commits).padStart(8)} ${String(entry.prs).padStart(6)} ${String(entry.lines).padStart(10)} ${fd}`,
|
||||
);
|
||||
}
|
||||
|
||||
function run(cmd: string): string {
|
||||
return execSync(cmd, {
|
||||
|
||||
Reference in New Issue
Block a user