114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import type { BotEvent, ChatMessage } from '../../types/bot';
|
||
|
||
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
||
const OSC_RE = /\x1b\][^\u0007]*(\u0007|\x1b\\)/g;
|
||
const NON_TEXT_RE = /[^\u0009\u0020-\u007E\u4E00-\u9FFF。,!?:;、“”‘’()《》【】—…·\-_./:\\,%+*='"`|<>]/g;
|
||
const CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
||
const ATTACHMENT_BLOCK_RE = /\[Attached Files\][\s\S]*?\[\/Attached Files\]/gi;
|
||
|
||
function cleanLine(line: string) {
|
||
return line
|
||
.replace(OSC_RE, '')
|
||
.replace(ANSI_RE, '')
|
||
.replace(/\[(\?|\d|;)+[A-Za-z]/g, '')
|
||
.replace(/\[(\d+)?K/g, '')
|
||
.replace(NON_TEXT_RE, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
export function normalizeUserMessageText(input: string) {
|
||
let text = (input || '').replace(/\r\n/g, '\n').trim();
|
||
if (!text) return '';
|
||
|
||
// Keep attachment list out of editable/visible command text.
|
||
text = text.replace(ATTACHMENT_BLOCK_RE, '').trim();
|
||
|
||
// Strip role prefixes injected by some gateways, e.g. "You: ...".
|
||
text = text.replace(/(^|\n)\s*(you|user|你)\s*[::]\s*/gi, '$1').trim();
|
||
text = text.replace(/\n{3,}/g, '\n\n');
|
||
|
||
// Collapse duplicate echoes like "xxx You: xxx" / "xxx xxx".
|
||
const flat = text.replace(/\s+/g, ' ').trim();
|
||
const prefixedRepeat = flat.match(/^(.{4,}?)\s+(you|user|你)\s*[::]\s*\1$/iu);
|
||
if (prefixedRepeat) return prefixedRepeat[1].trim();
|
||
const exactRepeat = flat.match(/^(.{4,}?)\s+\1$/u);
|
||
if (exactRepeat) return exactRepeat[1].trim();
|
||
|
||
return text;
|
||
}
|
||
|
||
export function normalizeAssistantMessageText(input: string) {
|
||
let text = String(input || '')
|
||
.replace(OSC_RE, '')
|
||
.replace(ANSI_RE, '')
|
||
.replace(CONTROL_RE, '')
|
||
.replace(/\r\n/g, '\n')
|
||
.replace(/\r/g, '\n')
|
||
.trim();
|
||
if (!text) return '';
|
||
|
||
// Remove dashboard wrapper if channel accidentally outputs raw marker line.
|
||
text = text.replace(/__DASHBOARD_DATA_START__/g, '').replace(/__DASHBOARD_DATA_END__/g, '').trim();
|
||
|
||
// Reduce excessive blank lines while keeping markdown readability.
|
||
text = text.replace(/\n{4,}/g, '\n\n\n');
|
||
return text;
|
||
}
|
||
|
||
export function summarizeProgressText(input: string, isZh: boolean) {
|
||
const raw = normalizeAssistantMessageText(input);
|
||
if (!raw) return isZh ? '处理中...' : 'Processing...';
|
||
const firstLine = raw
|
||
.split('\n')
|
||
.map((v) => v.trim())
|
||
.find((v) => v.length > 0);
|
||
const line = (firstLine || raw)
|
||
.replace(/[`*_>#|\[\]\(\)]/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
if (!line) return isZh ? '处理中...' : 'Processing...';
|
||
return line.length > 96 ? `${line.slice(0, 96)}...` : line;
|
||
}
|
||
|
||
/**
|
||
* 核心逻辑:日志解析器仅用于提取“状态事件”(用于显示思考气泡)。
|
||
* 所有的正式对话气泡(用户指令、AI回复)必须由结构化总线消息驱动。
|
||
*/
|
||
export function parseLogToArtifacts(
|
||
raw: string,
|
||
ts: number = Date.now(),
|
||
): { message?: ChatMessage; event?: BotEvent } | null {
|
||
const line = cleanLine(raw);
|
||
if (!line || line.length < 3) return null;
|
||
const lower = line.toLowerCase();
|
||
|
||
// 1. 忽略结构化标签、系统日志和心跳干扰
|
||
if (
|
||
lower.includes('__dashboard_data') ||
|
||
lower.includes('litellm') ||
|
||
lower.includes('heartbeat') ||
|
||
lower.includes('starting nanobot gateway')
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
// 2. 仅提取思考/工具执行状态
|
||
if (lower.includes('nanobot is thinking')) {
|
||
return { event: { state: 'THINKING', text: 'Thinking', ts } };
|
||
}
|
||
|
||
const toolMatch = line.match(/execut(?:e|ing) tool[:\s]+([\w\-./]+)/i);
|
||
if (toolMatch) {
|
||
return { event: { state: 'TOOL_CALL', text: `Executing Tool: ${toolMatch[1]}`, ts } };
|
||
}
|
||
|
||
// 3. 错误状态提取
|
||
if (lower.includes('traceback') || (lower.includes('error') && !lower.includes('no error'))) {
|
||
return { event: { state: 'ERROR', text: 'Execution Error', ts } };
|
||
}
|
||
|
||
// 绝对不返回 message 对象
|
||
return null;
|
||
}
|