94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
/**
|
|
* message-filter.ts — outgoing message sanitization
|
|
*
|
|
* 1. Brand replace: OpenClaw → Titan, openclaw emoji → 🐶
|
|
* 2. Path scrub: strip /home/openclaw/ prefix from tool content
|
|
* 3. Smart truncation: single truncator
|
|
* - SQL results: row count summary + first N lines
|
|
* - Other: first 400 chars + …[N more chars]
|
|
*/
|
|
|
|
// ── 1. Brand replace ────────────────────────────────────────────────────────
|
|
|
|
const OPENCLAW_EMOJIS = /🐾|🦞|🤖|🦅/gu;
|
|
const OPENCLAW_NAME = /\bOpenClaw\b/g;
|
|
|
|
function brandReplace(str: string): string {
|
|
return str.replace(OPENCLAW_EMOJIS, '🐶').replace(OPENCLAW_NAME, 'Titan');
|
|
}
|
|
|
|
// ── 2. Path scrub ────────────────────────────────────────────────────────────
|
|
|
|
const PATH_ABS = /\/home\/openclaw\//g;
|
|
const PATH_TILDE = /~\/\.openclaw\//g;
|
|
const PATH_DOT_CLAW = /\.openclaw\//g;
|
|
|
|
function scrubPaths(str: string): string {
|
|
return str.replace(PATH_ABS, '').replace(PATH_TILDE, '').replace(PATH_DOT_CLAW, '');
|
|
}
|
|
|
|
// ── 3. Smart truncation ──────────────────────────────────────────────────────
|
|
|
|
const SOFT_LIMIT = 80;
|
|
const SQL_ROWS = 8;
|
|
|
|
function looksLikeSqlResult(str: string): boolean {
|
|
const lines = str.split('\n').filter(l => l.trim());
|
|
if (lines.length < 2) return false;
|
|
const tabLines = lines.filter(l => l.includes('\t') || (l.match(/\|/g) || []).length >= 2);
|
|
return tabLines.length > lines.length * 0.5;
|
|
}
|
|
|
|
function truncateSql(str: string): string {
|
|
const lines = str.split('\n').filter(l => l.trim());
|
|
const total = lines.length;
|
|
if (total <= SQL_ROWS + 1) return str;
|
|
return lines.slice(0, SQL_ROWS).join('\n') + `\n… [${total - SQL_ROWS} more rows]`;
|
|
}
|
|
|
|
function smartTruncate(str: unknown): string {
|
|
if (typeof str !== 'string') {
|
|
try { str = JSON.stringify(str); } catch (_) { return String(str); }
|
|
}
|
|
const s = str as string;
|
|
if (s.length <= SOFT_LIMIT) return s;
|
|
if (looksLikeSqlResult(s)) return truncateSql(s);
|
|
return s.slice(0, 40) + '…' + s.slice(-40);
|
|
}
|
|
|
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
|
|
export function filterText(str: string | null | undefined): string | null | undefined {
|
|
if (str == null) return str;
|
|
return scrubPaths(brandReplace(String(str)));
|
|
}
|
|
|
|
function extractSql(cmd: string): string | null {
|
|
const m = cmd.match(/(?:mysql|mariadb)\b[^"']*-e\s+["']([^"']+)["']/s);
|
|
if (m) return m[1].trim().replace(/\\`/g, '').replace(/`/g, '');
|
|
return null;
|
|
}
|
|
|
|
function prettifyArgs(val: unknown): string {
|
|
if (typeof val === 'string') {
|
|
try { val = JSON.parse(val); } catch (_) { return val; }
|
|
}
|
|
if (val && typeof val === 'object') {
|
|
const obj = val as Record<string, unknown>;
|
|
if (typeof obj.command === 'string') {
|
|
return extractSql(obj.command) || obj.command;
|
|
}
|
|
const keys = Object.keys(obj);
|
|
if (keys.length === 1 && typeof obj[keys[0]] === 'string') return obj[keys[0]] as string;
|
|
try { return JSON.stringify(val); } catch (_) { return '[non-serializable]'; }
|
|
}
|
|
return String(val);
|
|
}
|
|
|
|
export function filterValue(val: unknown): string | null | undefined {
|
|
if (val == null) return val as null | undefined;
|
|
return smartTruncate(scrubPaths(brandReplace(prettifyArgs(val))));
|
|
}
|
|
|
|
export { brandReplace, scrubPaths, smartTruncate };
|