

























@@ -7,6 +7,7 @@ import crypto from "node:crypto";
77import fs from "node:fs";
88import os from "node:os";
99import path from "node:path";
10+import { pathToFileURL } from "node:url";
10111112const REPO = "openclaw/openclaw";
1213const REPO_URL = `https://github.com/${REPO}`;
@@ -50,6 +51,34 @@ function ghGraphQL(query, options = {}) {
5051return gh(["api", "graphql", "-f", `query=${query}`], options);
5152}
525354+function isBodyLocationType(locationType) {
55+return locationType === "issue_body" || locationType === "pull_request_body";
56+}
57+58+export function decideBodyRedaction(currentBody, redactedBody) {
59+const bodyChanged = String(currentBody) !== String(redactedBody);
60+return {
61+body_changed: bodyChanged,
62+notify_required: bodyChanged,
63+};
64+}
65+66+export function loadBodyRedactionResult(locationType, resultFile) {
67+if (!isBodyLocationType(locationType)) {
68+return { notify_required: true };
69+}
70+if (!resultFile) {
71+fail("Body notifications require a redaction result file from redact-body-if-needed");
72+}
73+if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
74+75+const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
76+if (typeof result.notify_required !== "boolean") {
77+fail(`Invalid redaction result file: missing boolean notify_required in ${resultFile}`);
78+}
79+return result;
80+}
81+5382function failOnGraphQLFailure(result, message) {
5483if (result?.gh_failed) {
5584const details = (
@@ -470,6 +499,43 @@ function cmdRedactBody(kind, number, bodyFile) {
470499console.log(JSON.stringify({ ok: true, kind, number: Number(number) }));
471500}
472501502+/**
503+ * redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>
504+ * PATCH only when the agent-produced redacted body differs from the current body.
505+ */
506+function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile, resultFile) {
507+if (!kind || !number || !currentBodyFile || !redactedBodyFile || !resultFile) {
508+fail(
509+"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
510+);
511+}
512+if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
513+if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
514+515+const currentBody = fs.readFileSync(currentBodyFile, "utf8");
516+const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
517+const decision = decideBodyRedaction(currentBody, redactedBody);
518+const result = {
519+ok: true,
520+ kind,
521+number: Number(number),
522+ ...decision,
523+};
524+525+if (decision.body_changed) {
526+const endpoint =
527+kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
528+gh(["api", endpoint, "-X", "PATCH", "-F", `body=@${redactedBodyFile}`]);
529+result.redacted = true;
530+} else {
531+result.redacted = false;
532+result.reason = "current_body_already_redacted";
533+}
534+535+fs.writeFileSync(resultFile, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
536+console.log(JSON.stringify(result));
537+}
538+473539/**
474540 * delete-comment <comment-id>
475541 * Delete a comment (and all its edit history).
@@ -555,6 +621,17 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
555621556622const types = secretTypes.split(",").map((s) => s.trim());
557623const typeList = types.map((t, i) => `${i + 1}. **${t}**`).join("\n");
624+const redactionResult = loadBodyRedactionResult(locationType, replyToNodeId);
625+if (isBodyLocationType(locationType) && !redactionResult.notify_required) {
626+console.log(
627+JSON.stringify({
628+ok: true,
629+skipped: true,
630+reason: "current_body_already_redacted",
631+}),
632+);
633+return;
634+}
558635559636let locationDesc;
560637let actionDesc;
@@ -758,12 +835,13 @@ function cmdSummary(jsonFile) {
758835759836// ─── Dispatch ───────────────────────────────────────────────────────────────
760837761-const [command, ...args] = process.argv.slice(2);
838+const args = [];
762839763-const commands = {
840+export const commands = {
764841"fetch-alert": () => cmdFetchAlert(args[0]),
765842"fetch-content": () => cmdFetchContent(args[0]),
766843"redact-body": () => cmdRedactBody(args[0], args[1], args[2]),
844+"redact-body-if-needed": () => cmdRedactBodyIfNeeded(args[0], args[1], args[2], args[3], args[4]),
767845"delete-comment": () => cmdDeleteComment(args[0]),
768846"delete-discussion-comment": () => cmdDeleteDiscussionComment(args[0]),
769847"recreate-comment": () => cmdRecreateComment(args[0], args[1]),
@@ -774,26 +852,37 @@ const commands = {
774852summary: () => cmdSummary(args[0]),
775853};
776854777-if (!command || !commands[command]) {
778-console.error(
779-[
780-"Usage: node secret-scanning.mjs <command> [args]",
781-"",
782-"Commands:",
783-" fetch-alert <number> Fetch alert metadata + locations",
784-" fetch-content '<location-json>' Fetch content for a location",
785-" redact-body <issue|pr> <n> <file> PATCH body with redacted file",
786-" delete-comment <comment-id> Delete a comment",
787-" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
788-" recreate-comment <issue-n> <file> Create replacement comment",
789-" recreate-discussion-comment <disc-node-id> <file> [reply-to-node-id] Create discussion comment (GraphQL)",
790-" notify <target> <author> <type> <types> [reply-to-node-id] Post notification",
791-" resolve <n> [resolution] [comment] Close alert",
792-" list-open List open alerts",
793-" summary <json-file> Print formatted summary",
794-].join("\n"),
795-);
796-process.exit(1);
855+function main(argv = process.argv.slice(2)) {
856+const [command, ...commandArgs] = argv;
857+args.length = 0;
858+args.push(...commandArgs);
859+860+if (!command || !commands[command]) {
861+console.error(
862+[
863+"Usage: node secret-scanning.mjs <command> [args]",
864+"",
865+"Commands:",
866+" fetch-alert <number> Fetch alert metadata + locations",
867+" fetch-content '<location-json>' Fetch content for a location",
868+" redact-body <issue|pr> <n> <file> PATCH body with redacted file",
869+" redact-body-if-needed <issue|pr> <n> <current-file> <redacted-file> <result-file> PATCH body only if redaction changed it",
870+" delete-comment <comment-id> Delete a comment",
871+" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
872+" recreate-comment <issue-n> <file> Create replacement comment",
873+" recreate-discussion-comment <disc-node-id> <file> [reply-to-node-id] Create discussion comment (GraphQL)",
874+" notify <target> <author> <type> <types> [reply-to-node-id|body-result-file] Post notification",
875+" resolve <n> [resolution] [comment] Close alert",
876+" list-open List open alerts",
877+" summary <json-file> Print formatted summary",
878+].join("\n"),
879+);
880+process.exit(1);
881+}
882+883+commands[command]();
797884}
798885799-commands[command]();
886+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
887+main();
888+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。