This commit is contained in:
2026-05-13 10:18:54 +03:00
parent cd8d2683c0
commit c5b61ee3d8
38 changed files with 3929 additions and 3718 deletions
+283
View File
@@ -0,0 +1,283 @@
import {Message} from "typescript-telegram-bot-api";
export type AiRunnerLogLevel = "trace" | "debug" | "info" | "success" | "warn" | "error";
export type AiRunnerLogDetails = Record<string, unknown>;
export type AiLogToolCallLike = {
id: string;
name: string;
argumentsText: string;
};
const AI_RUNNER_LOG_PREFIX = "unified-ai-runner";
const AI_RUNNER_LOG_MAX_STRING = 600;
const AI_RUNNER_LOG_MAX_ARRAY = 8;
const LOG_LEVEL_WEIGHT: Record<AiRunnerLogLevel, number> = {
trace: 10,
debug: 20,
info: 30,
success: 30,
warn: 40,
error: 50,
};
const AI_RUNNER_LOG_COLORS: Record<AiRunnerLogLevel | "reset" | "bold" | "dim" | "label" | "key" | "value", string> = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
trace: "\x1b[90m",
debug: "\x1b[90m",
info: "\x1b[36m",
success: "\x1b[32m",
warn: "\x1b[33m",
error: "\x1b[31m",
label: "\x1b[35m",
key: "\x1b[94m",
value: "\x1b[97m",
};
function envBool(name: string, defaultValue: boolean): boolean {
const value = process.env[name];
if (value === undefined) return defaultValue;
return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
}
function aiRunnerLogsEnabled(): boolean {
return envBool("AI_RUNNER_LOGS", true) && envBool("AI_LOG_ENABLED", true);
}
function aiRunnerColorsEnabled(): boolean {
return envBool("AI_RUNNER_LOG_COLORS", true) && !process.env.NO_COLOR;
}
function configuredMinLevel(): AiRunnerLogLevel {
const raw = process.env.AI_LOG_LEVEL?.trim().toLowerCase();
if (raw && raw in LOG_LEVEL_WEIGHT) return raw as AiRunnerLogLevel;
return "debug";
}
function shouldWriteLevel(level: AiRunnerLogLevel): boolean {
return LOG_LEVEL_WEIGHT[level] >= LOG_LEVEL_WEIGHT[configuredMinLevel()];
}
function paintAiLog(value: string, color: keyof typeof AI_RUNNER_LOG_COLORS): string {
if (!aiRunnerColorsEnabled()) return value;
return `${AI_RUNNER_LOG_COLORS[color]}${value}${AI_RUNNER_LOG_COLORS.reset}`;
}
function truncateAiLogString(value: string, max = AI_RUNNER_LOG_MAX_STRING): string {
if (value.length <= max) return value;
return `${value.slice(0, max)}… (+${value.length - max} chars)`;
}
function safeJsonParseObject(value?: string): Record<string, unknown> {
if (!value?.trim()) return {};
try {
const parsed: unknown = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: {};
} catch {
return {};
}
}
function isSecretKey(keyPath: string): boolean {
const normalized = keyPath.toLowerCase();
return normalized.includes("token")
|| normalized.includes("secret")
|| normalized.includes("apikey")
|| normalized.includes("api_key")
|| normalized.includes("authorization")
|| normalized.endsWith(".key")
|| normalized === "key";
}
function isPromptKey(keyPath: string): boolean {
const normalized = keyPath.toLowerCase();
return normalized.includes("prompt") || normalized.includes("systemprompt");
}
function isTextPreviewKey(keyPath: string): boolean {
const normalized = keyPath.toLowerCase();
return normalized.includes("content")
|| normalized.includes("message")
|| normalized.includes("text")
|| normalized.includes("preview")
|| normalized.includes("input")
|| normalized.includes("output");
}
function isToolArgsKey(keyPath: string): boolean {
const normalized = keyPath.toLowerCase();
return normalized.endsWith("args")
|| normalized.endsWith("arguments")
|| normalized.includes("toolargs")
|| normalized.includes("tool_args");
}
function isDaoKey(keyPath: string): boolean {
const normalized = keyPath.toLowerCase();
return normalized.includes("dao")
|| normalized.includes("database")
|| normalized.includes("db.")
|| normalized.includes("sql")
|| normalized.includes("chunk");
}
function shouldRedactKey(keyPath: string): boolean {
if (isSecretKey(keyPath)) return true;
if (isPromptKey(keyPath) && !envBool("AI_LOG_PROMPTS", false)) return true;
if (isToolArgsKey(keyPath) && !envBool("AI_LOG_TOOL_ARGS", false)) return true;
if (isDaoKey(keyPath) && !envBool("AI_LOG_DAO", false)) return true;
if (isTextPreviewKey(keyPath) && !envBool("AI_LOG_TEXT_PREVIEW", false)) return true;
return false;
}
function primitiveToLogValue(value: unknown): unknown {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack?.split("\n").slice(0, 6).join("\n"),
};
}
if (typeof value === "string") return truncateAiLogString(value);
if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) return value;
if (typeof value === "bigint") return value.toString();
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
if (Buffer.isBuffer(value)) return `<Buffer ${(value as {length: number}).length} bytes>`;
return undefined;
}
export function flattenAiLogDetails(
value: unknown,
keyPath = "",
depth = 0,
seen = new WeakSet<object>(),
): Record<string, unknown> {
if (keyPath && shouldRedactKey(keyPath)) {
return {[keyPath]: "<redacted>"};
}
const primitive = primitiveToLogValue(value);
if (primitive !== undefined || value === undefined) {
return keyPath ? {[keyPath]: primitive} : {value: primitive};
}
if (typeof value !== "object" || value === null) {
return keyPath ? {[keyPath]: String(value)} : {value: String(value)};
}
if (seen.has(value)) {
return keyPath ? {[keyPath]: "[Circular]"} : {value: "[Circular]"};
}
seen.add(value);
if (Array.isArray(value)) {
if (depth >= 2) {
return keyPath ? {[keyPath]: `[Array ${value.length}]`} : {value: `[Array ${value.length}]`};
}
const entries: Record<string, unknown> = {};
value.slice(0, AI_RUNNER_LOG_MAX_ARRAY).forEach((item, index) => {
Object.assign(entries, flattenAiLogDetails(item, keyPath ? `${keyPath}.${index}` : String(index), depth + 1, seen));
});
if (value.length > AI_RUNNER_LOG_MAX_ARRAY) {
entries[keyPath ? `${keyPath}.__more` : "__more"] = value.length - AI_RUNNER_LOG_MAX_ARRAY;
}
return entries;
}
if (depth >= 3) {
return keyPath ? {[keyPath]: "[Object]"} : {value: "[Object]"};
}
const entries: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
const childPath = keyPath ? `${keyPath}.${key}` : key;
if ((key.toLowerCase() === "data" || key.toLowerCase() === "image_url" || key.toLowerCase().endsWith("b64")) && typeof raw === "string") {
entries[childPath] = `<${raw.length} chars>`;
continue;
}
Object.assign(entries, flattenAiLogDetails(raw, childPath, depth + 1, seen));
}
return entries;
}
export function redactLogValue(value: unknown): Record<string, unknown> {
return flattenAiLogDetails(value);
}
function formatAiLogDetails(details?: AiRunnerLogDetails): string {
if (!details || !Object.keys(details).length) return "";
const flattened = flattenAiLogDetails(details);
const chunks = Object.entries(flattened).map(([key, value]) => {
const safeValue = typeof value === "string" ? value : JSON.stringify(value);
return `${paintAiLog(key, "key")}=${paintAiLog(safeValue ?? "undefined", "value")}`;
});
return ` ${chunks.join(" ")}`;
}
export function aiLog(level: AiRunnerLogLevel, event: string, details?: AiRunnerLogDetails): void {
if (!aiRunnerLogsEnabled() || !shouldWriteLevel(level)) return;
const timestamp = paintAiLog(new Date().toISOString(), "dim");
const prefix = paintAiLog(AI_RUNNER_LOG_PREFIX, "bold");
const levelText = paintAiLog(level.toUpperCase().padEnd(7), level);
const eventText = paintAiLog(event, "label");
const line = `${timestamp} ${prefix} ${levelText} ${eventText}${formatAiLogDetails(details)}`;
if (level === "error") {
console.error(line);
} else if (level === "warn") {
console.warn(line);
} else {
console.log(line);
}
}
export function aiLogDuration(startedAt: number): string {
const ms = Date.now() - startedAt;
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
export function aiLogToolCall(toolCall: AiLogToolCallLike): Record<string, unknown> {
return {
id: toolCall.id,
name: toolCall.name,
arguments: safeJsonParseObject(toolCall.argumentsText),
};
}
export function aiLogMessageIdentity(msg: Message | undefined): Record<string, unknown> | undefined {
if (!msg) return undefined;
return {
chatId: msg.chat?.id,
chatType: msg.chat?.type,
messageId: msg.message_id,
fromId: msg.from?.id,
username: msg.from?.username,
};
}
export function aiLogProviderTarget(target: {provider: string; purpose?: string; model?: string; baseUrl?: string; apiKey?: string} | undefined): Record<string, unknown> | undefined {
if (!target) return undefined;
return {
provider: target.provider,
purpose: target.purpose,
model: target.model,
baseUrl: target.baseUrl,
apiKey: target.apiKey,
};
}