mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +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',
|
id: 'result-agent',
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
toolUseId: 'agent-1',
|
toolUseId: 'agent-1',
|
||||||
content: {
|
content: [
|
||||||
results: [
|
{
|
||||||
{
|
type: 'text',
|
||||||
file: 'git:v0.2.6..v0.2.7',
|
text: JSON.stringify({
|
||||||
line: 0,
|
results: [
|
||||||
snippet: 'v0.2.7 tag = a4c92ec7',
|
{
|
||||||
context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。',
|
file: 'git:v0.2.6..v0.2.7',
|
||||||
},
|
line: 0,
|
||||||
{
|
snippet: 'v0.2.7 tag = a4c92ec7',
|
||||||
file: '/tmp/example/src/lib.rs',
|
context: '版本范围判断:release-notes/v0.2.7.md 明确相比 v0.2.6。',
|
||||||
line: 220,
|
},
|
||||||
context: '中风险:服务默认监听 0.0.0.0。',
|
{
|
||||||
},
|
risk: 'medium',
|
||||||
],
|
items: [
|
||||||
},
|
{
|
||||||
|
file: '/tmp/example/src/lib.rs',
|
||||||
|
line: 220,
|
||||||
|
context: '中风险:服务默认监听 0.0.0.0。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
isError: false,
|
isError: false,
|
||||||
timestamp: 2,
|
timestamp: 2,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -896,17 +896,32 @@ function formatAgentStructuredResult(content: unknown): string {
|
|||||||
|
|
||||||
function parseStructuredAgentContent(content: unknown): Record<string, unknown> | unknown[] | null {
|
function parseStructuredAgentContent(content: unknown): Record<string, unknown> | unknown[] | null {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
const trimmed = content.trim()
|
return parseStructuredAgentText(content)
|
||||||
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 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 {
|
function formatAgentStructuredResultItem(result: unknown, index: number): string {
|
||||||
@ -920,6 +935,29 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string
|
|||||||
const context = getStringField(record, 'context')
|
const context = getStringField(record, 'context')
|
||||||
const snippet = getStringField(record, 'snippet')
|
const snippet = getStringField(record, 'snippet')
|
||||||
const message = getStringField(record, 'message') || getStringField(record, 'text') || getStringField(record, 'summary')
|
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'}`]
|
const lines = [`${index + 1}. ${location ? formatInlineCode(location) : 'Result'}`]
|
||||||
|
|
||||||
if (message) lines.push(` - ${message}`)
|
if (message) lines.push(` - ${message}`)
|
||||||
@ -929,6 +967,31 @@ function formatAgentStructuredResultItem(result: unknown, index: number): string
|
|||||||
return lines.join('\n')
|
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 {
|
function formatAgentResultLocation(record: Record<string, unknown>): string {
|
||||||
const file = getStringField(record, 'file')
|
const file = getStringField(record, 'file')
|
||||||
if (!file) return ''
|
if (!file) return ''
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user