openclaw / src /cron /command-output-summary.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
3144483 verified
Raw History Blame Contribute Delete
3.89 kB
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { redactToolPayloadText } from "../logging/redact.js";
const MAX_PRESERVED_ACTION_LINES = 12;
const ACTION_LINE_PATTERNS = [
/\b(device|user|verification|authorization|auth|login)\s+code\b/i,
/\benter\s+(?:the\s+)?(?:code|verification code|device code)\b/i,
/\bcopy\s+(?:this\s+)?code\b/i,
/\bvisit\s+(?:https?:\/\/|www\.)/i,
/\bopen\s+(?:https?:\/\/|www\.)/i,
/\bbrowser\s+(?:to|at)\s+(?:https?:\/\/|www\.)/i,
/\blog(?:\s|-)?in\s+(?:at|to|with)\b/i,
/\bauth(?:enticate|orize)\s+(?:at|with|using)\b/i,
/\bhttps?:\/\/[^\s]+\/(?:device|activate|login|oauth|authorize|auth)\b/i,
];
const URL_PATTERN = /\b(?:https?:\/\/|www\.)\S+/gi;
const CODE_PATTERN = /\b[A-Z0-9]{4}(?:[- ][A-Z0-9]{3,8}){1,4}\b/g;
const UNSEPARATED_CODE_PATTERN = /\b[A-Z0-9]{6,12}\b/g;
const SECRET_ASSIGNMENT_PATTERN =
/\b((?:access|refresh)[_-]?token|api[_-]?key|token|password|secret)\s*([:=])\s*([^\s;&]+)/gi;
export function isCronCommandActionCriticalLine(line: string): boolean {
const normalized = normalizeOptionalString(line);
return Boolean(normalized && ACTION_LINE_PATTERNS.some((pattern) => pattern.test(normalized)));
}
function normalizeLines(lines: string[] | undefined): string[] {
const result: string[] = [];
for (const line of lines ?? []) {
const normalized = normalizeOptionalString(line);
if (normalized && !result.includes(normalized)) {
result.push(normalized);
}
if (result.length >= MAX_PRESERVED_ACTION_LINES) {
break;
}
}
return result;
}
export function buildCronCommandSummary(params: {
stdout: string;
stderr: string;
preservedStdoutLines?: string[];
preservedStderrLines?: string[];
}): string | undefined {
const stdout = normalizeOptionalString(params.stdout);
const stderr = normalizeOptionalString(params.stderr);
const tail = stdout && stderr ? `stdout:\n${stdout}\n\nstderr:\n${stderr}` : (stdout ?? stderr);
const preserved = [
...normalizeLines(params.preservedStdoutLines),
...normalizeLines(params.preservedStderrLines),
];
if (preserved.length === 0) {
return tail;
}
const missing = new Set(preserved);
for (const line of tail?.split(/\r?\n/) ?? []) {
const normalized = line.trim();
// Check the short action list first so unrelated long lines never need hashing.
if (preserved.includes(normalized)) {
missing.delete(normalized);
if (missing.size === 0) {
return tail;
}
}
}
// Retain stream order and repeated actions from different output streams.
const actionLines = preserved.filter((line) => missing.has(line));
const actionBlock = `action-required output preserved:\n${actionLines.join("\n")}`;
return tail ? `${actionBlock}\n\n${tail}` : actionBlock;
}
function cronCommandSummaryNeedsExternalRedaction(summary: string | undefined): boolean {
if (!summary) {
return false;
}
return summary
.split(/\r?\n/)
.some(
(line) =>
line.startsWith("action-required output preserved:") ||
isCronCommandActionCriticalLine(line),
);
}
export function redactCronCommandSummaryForExternalDelivery(
summary: string | undefined,
): string | undefined {
if (!summary || !cronCommandSummaryNeedsExternalRedaction(summary)) {
return summary;
}
return summary
.split(/(\r?\n)/)
.map((part) => {
if (/^\r?\n$/.test(part) || !isCronCommandActionCriticalLine(part)) {
return part;
}
return redactToolPayloadText(part)
.replace(SECRET_ASSIGNMENT_PATTERN, (_match, key: string, separator: string) => {
return `${key}${separator}***`;
})
.replace(URL_PATTERN, "[redacted-url]")
.replace(CODE_PATTERN, "[redacted-code]")
.replace(UNSEPARATED_CODE_PATTERN, "[redacted-code]");
})
.join("");
}