export interface AgentStepItem { title: string; content: string; } export interface AgentParsed { steps: AgentStepItem[]; thinkingMarkdown: string; answerMarkdown: string; thinkingComplete: boolean; } const STEP_RE = /([\s\S]*?)<\/step>/g; const ANSWER_RE = /([\s\S]*?)<\/answer>/; const THINKING_COMMENT_RE = //i; const WAITING_PREFIX = '仍在等待模型首个 token...'; const stripCodeFence = (raw: string): string => { const trimmed = raw.trim(); const match = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed); if (match) return match[1].trim(); return trimmed.replace(/^```(?:json)?\s*/i, '').trim(); }; const stripThinkTags = ( text: string, ): { remainder: string; thinkBlock: string; closed: boolean } => { const start = text.indexOf(''); if (start === -1) return { remainder: text, thinkBlock: '', closed: false }; const after = text.slice(start + ''.length); const endIdx = after.indexOf(''); if (endIdx === -1) { return { remainder: text.slice(0, start), thinkBlock: after, closed: false, }; } return { remainder: text.slice(0, start) + after.slice(endIdx + ''.length), thinkBlock: after.slice(0, endIdx), closed: true, }; }; const mergeStep = (steps: AgentStepItem[], nextStep: AgentStepItem) => { const existingIndex = steps.findIndex((step) => step.title === nextStep.title); if (existingIndex === -1) { steps.push(nextStep); return; } const existing = steps[existingIndex]; const nextIsWaiting = nextStep.content.startsWith(WAITING_PREFIX); const existingLines = existing.content .split('\n') .map((line) => line.trim()) .filter(Boolean); const stableLines = existingLines.filter((line) => !line.startsWith(WAITING_PREFIX)); if (nextIsWaiting) { const mergedLines = stableLines.length > 0 ? [...stableLines, nextStep.content] : [nextStep.content]; steps[existingIndex] = { ...existing, content: mergedLines.join('\n'), }; return; } const normalizedLines = stableLines.includes(nextStep.content) ? stableLines : [...stableLines, nextStep.content]; steps[existingIndex] = { ...existing, content: normalizedLines.join('\n'), }; }; const extractSteps = (text: string): { steps: AgentStepItem[]; remainder: string } => { const steps: AgentStepItem[] = []; const remainder = text.replace(STEP_RE, (_, title: string, content: string) => { mergeStep(steps, { title: title.trim(), content: content.trim() }); return ''; }); return { steps, remainder }; }; const extractTaggedSection = ( text: string, re: RegExp, ): { value: string; remainder: string } => { const match = re.exec(text); if (!match) return { value: '', remainder: text }; return { value: match[1].trim(), remainder: text.replace(match[0], '').trim(), }; }; const extractJsonStringField = ( text: string, field: string, ): { value: string; complete: boolean; matched: boolean } => { const re = new RegExp(`"${field}"\\s*:\\s*"`); const match = re.exec(text); if (!match) return { value: '', complete: false, matched: false }; let i = match.index + match[0].length; let value = ''; while (i < text.length) { const ch = text[i]; if (ch === '\\') { if (i + 1 >= text.length) return { value, complete: false, matched: true }; const next = text[i + 1]; switch (next) { case 'n': value += '\n'; break; case 't': value += '\t'; break; case 'r': value += '\r'; break; case '"': value += '"'; break; case '\\': value += '\\'; break; case '/': value += '/'; break; case 'b': value += '\b'; break; case 'f': value += '\f'; break; case 'u': if (i + 5 < text.length) { const hex = text.slice(i + 2, i + 6); value += String.fromCharCode(parseInt(hex, 16)); i += 6; continue; } return { value, complete: false, matched: true }; default: value += next; } i += 2; } else if (ch === '"') { return { value, complete: true, matched: true }; } else { value += ch; i++; } } return { value, complete: false, matched: true }; }; const extractItems = (text: string): unknown[] => { const re = /"items"\s*:\s*\[/; const match = re.exec(text); if (!match) return []; let i = match.index + match[0].length; const out: unknown[] = []; while (i < text.length) { while (i < text.length && /[\s,]/.test(text[i])) i++; if (i >= text.length) break; if (text[i] === ']') break; if (text[i] !== '{') { i++; continue; } let depth = 0; let inStr = false; let escape = false; let j = i; for (; j < text.length; j++) { const c = text[j]; if (escape) { escape = false; continue; } if (inStr) { if (c === '\\') escape = true; else if (c === '"') inStr = false; } else if (c === '"') inStr = true; else if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) { j++; break; } } } if (depth !== 0) break; try { out.push(JSON.parse(text.slice(i, j))); } catch { // ignore partial item } i = j; } return out; }; const itemToMarkdown = (item: unknown, idx: number): string => { if (item == null) return `${idx + 1}.`; if (typeof item !== 'object') return `${idx + 1}. ${String(item)}`; const o = item as Record; const title = (typeof o.title === 'string' && o.title) || (typeof o.expected_behavior === 'string' && o.expected_behavior) || (typeof o.name === 'string' && o.name) || `条目 ${idx + 1}`; const body = (typeof o.content === 'string' && o.content) || (typeof o.reason === 'string' && o.reason) || ''; const lines: string[] = [`**${idx + 1}. ${title}**`]; if (body) lines.push(body); if (typeof o.expected_behavior === 'string' && typeof o.driving_factor === 'string') { if (!o.title) { lines.length = 0; lines.push(`**${idx + 1}. ${o.expected_behavior}**`); } lines.push(`_驱动因素_:${o.driving_factor}`); } if (Array.isArray(o.references) && o.references.length > 0) { const refs = (o.references as unknown[]) .filter((x) => typeof x === 'string' || typeof x === 'number') .map((x) => `\`${String(x)}\``) .join(' '); if (refs) lines.push(`引用:${refs}`); } return lines.join('\n\n'); }; const itemsToMarkdown = (items: unknown[]): string => items.map((item, idx) => itemToMarkdown(item, idx)).join('\n\n---\n\n'); export const parseAgentResponse = (raw: string): AgentParsed => { if (!raw) { return { steps: [], thinkingMarkdown: '', answerMarkdown: '', thinkingComplete: false, }; } const stripped = stripThinkTags(raw); const stepsResult = extractSteps(stripped.remainder); let body = stripCodeFence(stepsResult.remainder); const extraThinking = stripped.thinkBlock; let thinkingComplete = stripped.closed; const answerSection = extractTaggedSection(body, ANSWER_RE); body = answerSection.remainder; const thinkingCommentSection = extractTaggedSection(body, THINKING_COMMENT_RE); body = thinkingCommentSection.remainder; if (body) { const start = body.indexOf('{'); if (start !== -1) body = body.slice(start); } if (body) { try { const obj = JSON.parse(body) as Record; if (obj && typeof obj === 'object') { const thinking = typeof obj.thinking === 'string' ? obj.thinking : ''; const items = Array.isArray(obj.items) ? (obj.items as unknown[]) : []; const answerMarkdown = items.length > 0 ? itemsToMarkdown(items) : answerSection.value; return { steps: stepsResult.steps, thinkingMarkdown: [extraThinking, thinking, thinkingCommentSection.value] .filter(Boolean) .join('\n\n'), answerMarkdown, thinkingComplete: true, }; } } catch { // fall through } } const thinkingField = extractJsonStringField(body, 'thinking'); const items = extractItems(body); if (thinkingField.complete) thinkingComplete = true; let fallbackAnswer = answerSection.value || body.trim(); if (items.length > 0) { fallbackAnswer = itemsToMarkdown(items); } if (!thinkingField.matched && items.length === 0 && !extraThinking) { return { steps: stepsResult.steps, thinkingMarkdown: '', answerMarkdown: fallbackAnswer, thinkingComplete: false, }; } return { steps: stepsResult.steps, thinkingMarkdown: [extraThinking, thinkingField.value, thinkingCommentSection.value] .filter(Boolean) .join('\n\n'), answerMarkdown: fallbackAnswer, thinkingComplete, }; };