mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: Normalize text-wrapped agent result JSON
Some providers return the final Agent answer as a text block containing structured JSON instead of Markdown. The desktop result fallback now parses that text-wrapped shape and formats plain and grouped result arrays into readable Markdown before display.
Constraint: Historical sessions can store Agent tool_result content as [{ type: 'text', text: '{...}' }] rather than a direct object
Rejected: Treat provider-specific JSON output as a model bug only | existing transcripts still need readable display
Confidence: high
Scope-risk: narrow
Directive: Agent result display must normalize text-wrapped structured payloads before falling back to raw text
Tested: cd desktop && bun run test -- run src/components/chat/MessageList.test.tsx -t "formats structured agent fallback"
Tested: cd desktop && bun run test -- run src/components/chat/MessageList.test.tsx -t agent
Not-tested: Full desktop gate per request to avoid broad gate/test runs
This commit is contained in:
parent
3433be18dd
commit
3ce7b02fc2
@ -1151,21 +1151,31 @@ describe('MessageList nested tool calls', () => {
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: {
|
||||
results: [
|
||||
{
|
||||
file: 'git:v0.2.6..v0.2.7',
|
||||
line: 0,
|
||||
snippet: 'v0.2.7 tag = a4c92ec7',
|
||||
context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。',
|
||||
},
|
||||
{
|
||||
file: '/tmp/example/src/lib.rs',
|
||||
line: 220,
|
||||
context: '中风险:服务默认监听 0.0.0.0。',
|
||||
},
|
||||
],
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
results: [
|
||||
{
|
||||
file: 'git:v0.2.6..v0.2.7',
|
||||
line: 0,
|
||||
snippet: 'v0.2.7 tag = a4c92ec7',
|
||||
context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。',
|
||||
},
|
||||
{
|
||||
risk: 'medium',
|
||||
items: [
|
||||
{
|
||||
file: '/tmp/example/src/lib.rs',
|
||||
line: 220,
|
||||
context: '中风险:服务默认监听 0.0.0.0。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
|
||||
@ -896,17 +896,32 @@ function formatAgentStructuredResult(content: unknown): string {
|
||||
|
||||
function parseStructuredAgentContent(content: unknown): Record<string, unknown> | unknown[] | null {
|
||||
if (typeof content === 'string') {
|
||||
const trimmed = content.trim()
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> | unknown[] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return parseStructuredAgentText(content)
|
||||
}
|
||||
|
||||
return typeof content === 'object' && content !== null ? content as Record<string, unknown> | unknown[] : null
|
||||
if (Array.isArray(content)) {
|
||||
return parseStructuredAgentText(extractTextContent(content))
|
||||
}
|
||||
|
||||
if (content && typeof content === 'object') {
|
||||
if ('results' in content) return content as Record<string, unknown>
|
||||
|
||||
const extracted = extractTextContent(content)
|
||||
return extracted ? parseStructuredAgentText(extracted) : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseStructuredAgentText(text: string): Record<string, unknown> | unknown[] | null {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> | unknown[] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatAgentStructuredResultItem(result: unknown, index: number): string {
|
||||
@ -920,6 +935,29 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string
|
||||
const context = getStringField(record, 'context')
|
||||
const snippet = getStringField(record, 'snippet')
|
||||
const message = getStringField(record, 'message') || getStringField(record, 'text') || getStringField(record, 'summary')
|
||||
const nestedItems = Array.isArray(record.items) ? record.items : []
|
||||
|
||||
if (nestedItems.length > 0) {
|
||||
const label = getStringField(record, 'risk') || getStringField(record, 'title') || message || 'Grouped results'
|
||||
const lines = [`${index + 1}. ${formatAgentGroupLabel(label)}`]
|
||||
if (context) lines.push(` - ${context}`)
|
||||
if (snippet) lines.push(` - ${snippet}`)
|
||||
|
||||
nestedItems
|
||||
.map(formatAgentStructuredNestedItem)
|
||||
.filter(Boolean)
|
||||
.forEach((item) => {
|
||||
lines.push(
|
||||
item
|
||||
.split('\n')
|
||||
.map((line, lineIndex) => `${lineIndex === 0 ? ' - ' : ' '}${line}`)
|
||||
.join('\n'),
|
||||
)
|
||||
})
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const lines = [`${index + 1}. ${location ? formatInlineCode(location) : 'Result'}`]
|
||||
|
||||
if (message) lines.push(` - ${message}`)
|
||||
@ -929,6 +967,31 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function formatAgentStructuredNestedItem(item: unknown): string {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
return extractTextContent(item).trim()
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>
|
||||
const location = formatAgentResultLocation(record)
|
||||
const context = getStringField(record, 'context')
|
||||
const snippet = getStringField(record, 'snippet')
|
||||
const message = getStringField(record, 'message') || getStringField(record, 'text') || getStringField(record, 'summary')
|
||||
const headingParts = [location ? formatInlineCode(location) : '', message].filter(Boolean)
|
||||
const lines = [headingParts.join(' - ') || 'Result']
|
||||
|
||||
if (context) lines.push(context)
|
||||
if (snippet) lines.push(snippet)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function formatAgentGroupLabel(label: string): string {
|
||||
const normalized = label.trim()
|
||||
if (!normalized) return 'Grouped results'
|
||||
return normalized.length > 1 ? `${normalized[0].toUpperCase()}${normalized.slice(1)}` : normalized.toUpperCase()
|
||||
}
|
||||
|
||||
function formatAgentResultLocation(record: Record<string, unknown>): string {
|
||||
const file = getStringField(record, 'file')
|
||||
if (!file) return ''
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user