refactor: 将对话解析器拆分为独立模块

将 lib/conversationParser 重构为 conversation/ 目录:
- 按 API 格式拆分解析器 (claude.ts, openai.ts, gemini.ts)
- 提取公共渲染逻辑到 render.ts
- 添加解析器注册表支持自动格式检测
- 更新组件导入路径
This commit is contained in:
fawney19
2026-01-16 03:11:41 +08:00
parent 02770d56df
commit adc82532eb
10 changed files with 2844 additions and 3 deletions

View File

@@ -539,7 +539,7 @@ import {
renderResponse,
type RenderResult,
type RenderBlock,
} from '../lib/conversationParser'
} from '../conversation'
const props = defineProps<{
isOpen: boolean

View File

@@ -191,7 +191,7 @@
<script setup lang="ts">
import { User, Bot, Settings, Wrench, AlertCircle, ChevronRight, FileText, Image as ImageIcon } from 'lucide-vue-next'
import Badge from '@/components/ui/badge.vue'
import type { RenderBlock } from '../../lib/conversationParser'
import type { RenderBlock } from '../../conversation'
defineProps<{
blocks: RenderBlock[]

View File

@@ -38,7 +38,7 @@
<script setup lang="ts">
import { AlertCircle, Zap } from 'lucide-vue-next'
import BlockRenderer from './BlockRenderer.vue'
import type { RenderResult } from '../../lib/conversationParser'
import type { RenderResult } from '../../conversation'
defineProps<{
renderResult: RenderResult

View File

@@ -0,0 +1,814 @@
/**
* Claude API 格式解析器
*/
import type {
ApiFormatParser,
ParsedConversation,
ParsedMessage,
ContentBlock,
MessageRole,
} from './types'
import {
createEmptyConversation,
createMessage,
createTextBlock,
createThinkingBlock,
createToolUseBlock,
createToolResultBlock,
createImageBlock,
isStreamResponse,
} from './types'
import type { RenderResult, RenderBlock, BadgeRenderBlock } from './render'
import {
createTextBlock as createTextRenderBlock,
createCollapsibleBlock,
createCodeBlock,
createBadgeBlock,
createImageBlock as createImageRenderBlock,
createErrorBlock as createErrorRenderBlock,
createMessageBlock,
createToolUseBlock as createToolUseRenderBlock,
createToolResultBlock as createToolResultRenderBlock,
createEmptyRenderResult,
} from './render'
/**
* Claude API 格式解析器
*/
export class ClaudeParser implements ApiFormatParser {
readonly format = 'claude' as const
readonly displayName = 'Claude'
/**
* 检测是否为 Claude 格式
*/
detect(requestBody: any, responseBody: any, hint?: string): number {
// 1. 后端提示优先
if (hint) {
const lowerHint = hint.toLowerCase()
if (lowerHint.includes('claude')) return 100
// 如果明确是其他格式,返回 0
if (lowerHint.includes('openai') || lowerHint.includes('gemini')) return 0
}
// 2. 检查模型名
const model = requestBody?.model?.toLowerCase() || ''
if (model.includes('claude')) return 95
// 3. 检查请求体结构
if (!requestBody?.messages || !Array.isArray(requestBody.messages)) {
return 0
}
// 4. 检查响应体特征
const respBody = isStreamResponse(responseBody)
? responseBody.chunks?.[0]
: responseBody
if (respBody) {
// Claude 响应特征
if (
respBody.type === 'message' ||
respBody.type?.startsWith('content_block') ||
respBody.type?.startsWith('message_')
) {
return 90
}
// 明确是 OpenAI 格式
if (respBody.choices || respBody.object?.includes('chat.completion')) {
return 0
}
}
// 5. 检查 Claude 特有的请求字段
if (requestBody.system !== undefined) {
// system 可以是字符串或数组,这是 Claude 的特征
return 70
}
// 默认返回中等置信度Aether 主要用于 Claude
return 50
}
/**
* 解析请求体
*/
parseRequest(requestBody: any): ParsedConversation {
if (!requestBody) {
return createEmptyConversation('claude', '无请求体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: requestBody.stream === true,
apiFormat: 'claude',
model: requestBody.model,
}
// 提取 system prompt
result.system = this.extractSystemPrompt(requestBody.system)
// 提取 messages
if (Array.isArray(requestBody.messages)) {
for (const msg of requestBody.messages) {
const parsedMsg = this.parseMessage(msg)
if (parsedMsg) {
result.messages.push(parsedMsg)
}
}
}
return result
} catch (e) {
return createEmptyConversation('claude', `解析失败: ${e}`)
}
}
/**
* 解析响应体
*/
parseResponse(responseBody: any): ParsedConversation {
if (!responseBody) {
return createEmptyConversation('claude', '无响应体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: false,
apiFormat: 'claude',
model: responseBody.model,
}
// Claude 响应格式: { type: "message", content: [...] }
if (Array.isArray(responseBody.content)) {
const contentBlocks = this.parseContentBlocks(responseBody.content, 'assistant')
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
}
return result
} catch (e) {
return createEmptyConversation('claude', `解析失败: ${e}`)
}
}
/**
* 解析流式响应
*/
parseStreamResponse(chunks: any[]): ParsedConversation {
if (!chunks || chunks.length === 0) {
return createEmptyConversation('claude', '无响应数据')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: true,
apiFormat: 'claude',
}
// 按 content block index 分组累积
const blocks = new Map<number, {
type: ContentBlock['type']
parts: string[]
metadata?: any
}>()
for (const chunk of chunks) {
// 提取模型名
if (chunk.message?.model && !result.model) {
result.model = chunk.message.model
}
if (chunk.type === 'content_block_start') {
const index = chunk.index ?? 0
const block = chunk.content_block
if (block?.type === 'text') {
blocks.set(index, { type: 'text', parts: [block.text || ''] })
} else if (block?.type === 'thinking') {
blocks.set(index, {
type: 'thinking',
parts: [block.thinking || ''],
metadata: { signature: block.signature },
})
} else if (block?.type === 'tool_use') {
blocks.set(index, {
type: 'tool_use',
parts: [],
metadata: { toolName: block.name, toolId: block.id },
})
}
} else if (chunk.type === 'content_block_delta') {
const index = chunk.index ?? 0
const delta = chunk.delta
const block = blocks.get(index)
if (block) {
if (delta?.type === 'text_delta') {
block.parts.push(delta.text || '')
} else if (delta?.type === 'thinking_delta') {
block.parts.push(delta.thinking || '')
} else if (delta?.type === 'input_json_delta') {
block.parts.push(delta.partial_json || '')
} else if (delta?.type === 'signature_delta') {
block.metadata = block.metadata || {}
block.metadata.signature = (block.metadata.signature || '') + (delta.signature || '')
}
}
}
}
// 转换为消息内容块
const contentBlocks: ContentBlock[] = []
const sortedEntries = Array.from(blocks.entries()).sort((a, b) => a[0] - b[0])
for (const [, block] of sortedEntries) {
const content = block.parts.join('')
switch (block.type) {
case 'text':
contentBlocks.push(createTextBlock(content))
break
case 'thinking':
contentBlocks.push(createThinkingBlock(content, block.metadata?.signature))
break
case 'tool_use':
contentBlocks.push(createToolUseBlock(
block.metadata?.toolId || '',
block.metadata?.toolName || '',
content || '{}'
))
break
}
}
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
return result
} catch (e) {
return createEmptyConversation('claude', `解析失败: ${e}`)
}
}
/**
* 提取 system prompt
*/
private extractSystemPrompt(system: any): string | undefined {
if (!system) return undefined
if (typeof system === 'string') {
return system
}
if (Array.isArray(system)) {
return system
.filter((b: any) => b.type === 'text')
.map((b: any) => b.text)
.join('\n')
}
return undefined
}
/**
* 解析单条消息
*/
private parseMessage(msg: any): ParsedMessage | null {
if (!msg || !msg.role) return null
const role = msg.role as MessageRole
const contentBlocks = this.parseMessageContent(msg.content, role)
if (contentBlocks.length === 0) return null
return createMessage(role, contentBlocks)
}
/**
* 解析消息内容
*/
private parseMessageContent(content: any, role: MessageRole): ContentBlock[] {
if (typeof content === 'string') {
return [createTextBlock(content)]
}
if (Array.isArray(content)) {
return this.parseContentBlocks(content, role)
}
return []
}
/**
* 解析内容块数组
*/
private parseContentBlocks(blocks: any[], role: MessageRole): ContentBlock[] {
const result: ContentBlock[] = []
for (const block of blocks) {
const parsed = this.parseContentBlock(block, role)
if (parsed) {
result.push(parsed)
}
}
return result
}
/**
* 解析单个内容块
*/
private parseContentBlock(block: any, _role: MessageRole): ContentBlock | null {
if (!block || !block.type) return null
switch (block.type) {
case 'text':
return createTextBlock(block.text || '')
case 'thinking':
return createThinkingBlock(block.thinking || '', block.signature)
case 'tool_use':
return createToolUseBlock(
block.id || '',
block.name || '',
block.input || {}
)
case 'tool_result':
return createToolResultBlock(
block.tool_use_id || '',
this.parseToolResultContent(block.content),
block.is_error
)
case 'image':
return this.parseImageBlock(block)
default:
return null
}
}
/**
* 解析图片块
*/
private parseImageBlock(block: any): ContentBlock | null {
const source = block.source
if (!source) {
return createImageBlock('base64', { alt: '[图片]' })
}
if (source.type === 'base64') {
return createImageBlock('base64', {
data: source.data,
mimeType: source.media_type,
})
}
if (source.type === 'url') {
return createImageBlock('url', {
url: source.url,
mimeType: source.media_type,
})
}
return createImageBlock('base64', { alt: '[图片]' })
}
/**
* 解析工具结果内容
*/
private parseToolResultContent(content: any): string | ContentBlock[] {
if (typeof content === 'string') {
return content
}
if (Array.isArray(content)) {
const blocks: ContentBlock[] = []
for (const item of content) {
if (item.type === 'text') {
blocks.push(createTextBlock(item.text || ''))
} else if (item.type === 'image') {
const imgBlock = this.parseImageBlock(item)
if (imgBlock) blocks.push(imgBlock)
}
}
return blocks.length > 0 ? blocks : JSON.stringify(content, null, 2)
}
return JSON.stringify(content, null, 2)
}
// ============================================================
// 渲染方法
// ============================================================
/**
* 渲染请求体
*/
renderRequest(requestBody: any): RenderResult {
if (!requestBody) {
return createEmptyRenderResult('无请求体')
}
try {
const blocks: RenderBlock[] = []
const isStream = requestBody.stream === true
// 渲染 system prompt
const system = this.extractSystemPrompt(requestBody.system)
if (system) {
blocks.push(createMessageBlock('system', [
createTextRenderBlock(system),
], { roleLabel: 'System' }))
}
// 渲染 messages
if (Array.isArray(requestBody.messages)) {
for (const msg of requestBody.messages) {
const msgBlock = this.renderMessage(msg)
if (msgBlock) {
blocks.push(msgBlock)
}
}
}
return { blocks, isStream }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染响应体
*/
renderResponse(responseBody: any): RenderResult {
if (!responseBody) {
return createEmptyRenderResult('无响应体')
}
// 检查是否为流式响应
if (isStreamResponse(responseBody)) {
return this.renderStreamResponse(responseBody.chunks || [])
}
try {
const blocks: RenderBlock[] = []
// Claude 响应格式: { type: "message", content: [...] }
if (Array.isArray(responseBody.content)) {
const contentBlocks = this.renderContentBlocks(responseBody.content)
if (contentBlocks.length > 0) {
const badges = this.getBadgesForContent(responseBody.content)
blocks.push(createMessageBlock('assistant', contentBlocks, {
roleLabel: 'Assistant',
badges,
}))
}
}
return { blocks, isStream: false }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染流式响应
*/
private renderStreamResponse(chunks: any[]): RenderResult {
if (!chunks || chunks.length === 0) {
return createEmptyRenderResult('无响应数据')
}
try {
// 先解析流式响应
const parsed = this.parseStreamResponse(chunks)
if (parsed.parseError) {
return createEmptyRenderResult(parsed.parseError)
}
const blocks: RenderBlock[] = []
// 渲染解析后的消息
for (const msg of parsed.messages) {
const contentBlocks = this.renderParsedContentBlocks(msg.content)
if (contentBlocks.length > 0) {
const badges = this.getBadgesForParsedContent(msg.content)
blocks.push(createMessageBlock(msg.role, contentBlocks, {
roleLabel: this.getRoleLabel(msg.role),
badges,
}))
}
}
return { blocks, isStream: true }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染单条消息
*/
private renderMessage(msg: any): RenderBlock | null {
if (!msg || !msg.role) return null
const role = msg.role as MessageRole
const contentBlocks = this.renderMessageContent(msg.content)
if (contentBlocks.length === 0) return null
const badges = this.getBadgesForRawContent(msg.content)
return createMessageBlock(role, contentBlocks, {
roleLabel: this.getRoleLabel(role),
badges,
})
}
/**
* 渲染消息内容
*/
private renderMessageContent(content: any): RenderBlock[] {
if (typeof content === 'string') {
return [createTextRenderBlock(content)]
}
if (Array.isArray(content)) {
return this.renderContentBlocks(content)
}
return []
}
/**
* 渲染原始内容块数组
*/
private renderContentBlocks(blocks: any[]): RenderBlock[] {
const result: RenderBlock[] = []
for (const block of blocks) {
const rendered = this.renderContentBlock(block)
if (rendered) {
result.push(rendered)
}
}
return result
}
/**
* 渲染单个原始内容块
*/
private renderContentBlock(block: any): RenderBlock | null {
if (!block || !block.type) return null
switch (block.type) {
case 'text':
return createTextRenderBlock(block.text || '')
case 'thinking':
return createCollapsibleBlock(
`思考过程 (${(block.thinking || '').length} 字符)`,
[createCodeBlock(block.thinking || '')],
{ defaultOpen: false, className: 'thinking-block' }
)
case 'tool_use':
return createToolUseRenderBlock(
block.name || '工具调用',
this.formatJson(block.input),
block.id
)
case 'tool_result': {
const content = this.formatToolResultContent(block.content)
return createToolResultRenderBlock(content, block.is_error)
}
case 'image':
return this.renderImageBlock(block)
default:
return null
}
}
/**
* 渲染已解析的内容块数组
*/
private renderParsedContentBlocks(blocks: ContentBlock[]): RenderBlock[] {
const result: RenderBlock[] = []
for (const block of blocks) {
const rendered = this.renderParsedContentBlock(block)
if (rendered) {
result.push(rendered)
}
}
return result
}
/**
* 渲染单个已解析的内容块
*/
private renderParsedContentBlock(block: ContentBlock): RenderBlock | null {
switch (block.type) {
case 'text':
return createTextRenderBlock(block.text)
case 'thinking':
return createCollapsibleBlock(
`思考过程 (${block.thinking.length} 字符)`,
[createCodeBlock(block.thinking)],
{ defaultOpen: false, className: 'thinking-block' }
)
case 'tool_use':
return createToolUseRenderBlock(
block.toolName || '工具调用',
this.formatJson(block.input),
block.toolId
)
case 'tool_result': {
const content = typeof block.content === 'string'
? block.content
: this.formatParsedToolResultContent(block.content)
return createToolResultRenderBlock(content, block.isError)
}
case 'image':
return createImageRenderBlock({
src: block.sourceType === 'base64'
? `data:${block.mimeType || 'image/png'};base64,${block.data}`
: block.url,
mimeType: block.mimeType,
alt: block.alt || '图片',
})
case 'error':
return createErrorRenderBlock(block.message, block.code)
default:
return null
}
}
/**
* 渲染图片块
*/
private renderImageBlock(block: any): RenderBlock | null {
const source = block.source
if (!source) {
return createImageRenderBlock({ alt: '[图片]' })
}
if (source.type === 'base64') {
return createImageRenderBlock({
src: `data:${source.media_type || 'image/png'};base64,${source.data}`,
mimeType: source.media_type,
})
}
if (source.type === 'url') {
return createImageRenderBlock({
src: source.url,
mimeType: source.media_type,
})
}
return createImageRenderBlock({ alt: '[图片]' })
}
/**
* 获取角色显示标签
*/
private getRoleLabel(role: MessageRole): string {
switch (role) {
case 'user': return 'User'
case 'assistant': return 'Assistant'
case 'system': return 'System'
case 'tool': return 'Tool'
default: return role
}
}
/**
* 获取原始内容的徽章
*/
private getBadgesForRawContent(content: any): BadgeRenderBlock[] {
if (!Array.isArray(content)) return []
const badges: BadgeRenderBlock[] = []
const types = new Set(content.map((b: any) => b.type))
if (types.has('thinking')) {
badges.push(createBadgeBlock('思考', 'secondary'))
}
if (types.has('tool_use')) {
badges.push(createBadgeBlock('工具调用', 'outline'))
}
if (types.has('tool_result')) {
badges.push(createBadgeBlock('工具结果', 'outline'))
}
if (types.has('image')) {
badges.push(createBadgeBlock('图片', 'secondary'))
}
return badges
}
/**
* 获取内容的徽章
*/
private getBadgesForContent(content: any[]): BadgeRenderBlock[] {
return this.getBadgesForRawContent(content)
}
/**
* 获取已解析内容的徽章
*/
private getBadgesForParsedContent(content: ContentBlock[]): BadgeRenderBlock[] {
const badges: BadgeRenderBlock[] = []
const types = new Set(content.map(b => b.type))
if (types.has('thinking')) {
badges.push(createBadgeBlock('思考', 'secondary'))
}
if (types.has('tool_use')) {
badges.push(createBadgeBlock('工具调用', 'outline'))
}
if (types.has('tool_result')) {
badges.push(createBadgeBlock('工具结果', 'outline'))
}
if (types.has('image')) {
badges.push(createBadgeBlock('图片', 'secondary'))
}
return badges
}
/**
* 格式化 JSON
*/
private formatJson(input: any): string {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input)
return JSON.stringify(parsed, null, 2)
} catch {
return input
}
}
return JSON.stringify(input, null, 2)
}
/**
* 格式化工具结果内容
*/
private formatToolResultContent(content: any): string {
if (typeof content === 'string') {
return content
}
if (Array.isArray(content)) {
return content
.map((item: any) => {
if (item.type === 'text') return item.text
if (item.type === 'image') return '[图片]'
return ''
})
.filter(Boolean)
.join('\n')
}
return JSON.stringify(content, null, 2)
}
/**
* 格式化已解析的工具结果内容
*/
private formatParsedToolResultContent(content: ContentBlock[]): string {
return content
.map(block => {
if (block.type === 'text') return block.text
if (block.type === 'image') return '[图片]'
if (block.type === 'error') return `[错误: ${block.message}]`
return ''
})
.filter(Boolean)
.join('\n')
}
}
/** 单例实例 */
export const claudeParser = new ClaudeParser()

View File

@@ -0,0 +1,607 @@
/**
* Gemini API 格式解析器
*/
import type {
ApiFormatParser,
ParsedConversation,
ParsedMessage,
ContentBlock,
MessageRole,
} from './types'
import {
createEmptyConversation,
createMessage,
createTextBlock,
createToolUseBlock,
createToolResultBlock,
createImageBlock,
isStreamResponse,
} from './types'
import type { RenderResult, RenderBlock, BadgeRenderBlock } from './render'
import {
createTextBlock as createTextRenderBlock,
createBadgeBlock,
createImageBlock as createImageRenderBlock,
createMessageBlock,
createToolUseBlock as createToolUseRenderBlock,
createToolResultBlock as createToolResultRenderBlock,
createEmptyRenderResult,
} from './render'
/**
* Gemini API 格式解析器
*/
export class GeminiParser implements ApiFormatParser {
readonly format = 'gemini' as const
readonly displayName = 'Gemini'
/**
* 检测是否为 Gemini 格式
*/
detect(requestBody: any, responseBody: any, hint?: string): number {
// 1. 后端提示优先
if (hint) {
const lowerHint = hint.toLowerCase()
if (lowerHint.includes('gemini')) return 100
if (lowerHint.includes('claude') || lowerHint.includes('openai')) return 0
}
// 2. 检查模型名
const model = requestBody?.model?.toLowerCase() || ''
if (model.includes('gemini')) return 95
// 3. Gemini 特有结构: 使用 contents 而非 messages
if (requestBody?.contents && Array.isArray(requestBody.contents)) {
return 90
}
// 4. 检查响应体特征
const respBody = isStreamResponse(responseBody)
? responseBody.chunks?.[0]
: responseBody
if (respBody?.candidates) {
return 85
}
return 0
}
/**
* 解析请求体
*/
parseRequest(requestBody: any): ParsedConversation {
if (!requestBody) {
return createEmptyConversation('gemini', '无请求体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: false,
apiFormat: 'gemini',
model: requestBody.model,
}
// 提取 system instruction
const sysInst = requestBody.system_instruction || requestBody.systemInstruction
if (sysInst?.parts) {
result.system = sysInst.parts
.filter((p: any) => p.text)
.map((p: any) => p.text)
.join('\n')
}
// 提取 contents
if (Array.isArray(requestBody.contents)) {
for (const content of requestBody.contents) {
const parsedMsg = this.parseContent(content)
if (parsedMsg) {
result.messages.push(parsedMsg)
}
}
}
return result
} catch (e) {
return createEmptyConversation('gemini', `解析失败: ${e}`)
}
}
/**
* 解析响应体
*/
parseResponse(responseBody: any): ParsedConversation {
if (!responseBody) {
return createEmptyConversation('gemini', '无响应体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: false,
apiFormat: 'gemini',
}
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
const candidate = responseBody.candidates?.[0]
if (candidate?.content?.parts) {
const contentBlocks = this.parseParts(candidate.content.parts)
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
}
return result
} catch (e) {
return createEmptyConversation('gemini', `解析失败: ${e}`)
}
}
/**
* 解析流式响应
*/
parseStreamResponse(chunks: any[]): ParsedConversation {
if (!chunks || chunks.length === 0) {
return createEmptyConversation('gemini', '无响应数据')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: true,
apiFormat: 'gemini',
}
const textParts: string[] = []
const toolCalls: { name: string; args: any }[] = []
for (const chunk of chunks) {
const parts = chunk.candidates?.[0]?.content?.parts
if (parts) {
for (const part of parts) {
if (part.text) {
textParts.push(part.text)
} else if (part.functionCall) {
toolCalls.push(part.functionCall)
}
}
}
}
const contentBlocks: ContentBlock[] = []
// 文本内容
if (textParts.length > 0) {
contentBlocks.push(createTextBlock(textParts.join('')))
}
// 工具调用
for (const call of toolCalls) {
contentBlocks.push(createToolUseBlock(
'', // Gemini 没有 tool_use_id
call.name || '',
call.args || {}
))
}
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
return result
} catch (e) {
return createEmptyConversation('gemini', `解析失败: ${e}`)
}
}
/**
* 解析 content 对象
*/
private parseContent(content: any): ParsedMessage | null {
if (!content) return null
const role = this.mapRole(content.role)
const contentBlocks = this.parseParts(content.parts || [])
if (contentBlocks.length === 0) return null
return createMessage(role, contentBlocks)
}
/**
* 解析 parts 数组
*/
private parseParts(parts: any[]): ContentBlock[] {
const result: ContentBlock[] = []
for (const part of parts) {
const block = this.parsePart(part)
if (block) {
result.push(block)
}
}
return result
}
/**
* 解析单个 part
*/
private parsePart(part: any): ContentBlock | null {
if (!part) return null
// 文本
if (part.text !== undefined) {
return createTextBlock(part.text)
}
// 内联数据(图片等)
if (part.inlineData) {
return createImageBlock('base64', {
data: part.inlineData.data,
mimeType: part.inlineData.mimeType,
})
}
// 函数调用
if (part.functionCall) {
return createToolUseBlock(
'',
part.functionCall.name || '',
part.functionCall.args || {}
)
}
// 函数响应
if (part.functionResponse) {
return createToolResultBlock(
'', // Gemini 用 name 关联
JSON.stringify(part.functionResponse.response, null, 2)
)
}
return null
}
/**
* 映射角色
*/
private mapRole(role: string | undefined): MessageRole {
switch (role) {
case 'user':
return 'user'
case 'model':
return 'assistant'
default:
return 'user'
}
}
// ============================================================
// 渲染方法
// ============================================================
/**
* 渲染请求体
*/
renderRequest(requestBody: any): RenderResult {
if (!requestBody) {
return createEmptyRenderResult('无请求体')
}
try {
const blocks: RenderBlock[] = []
// 渲染 system instruction
const sysInst = requestBody.system_instruction || requestBody.systemInstruction
if (sysInst?.parts) {
const systemText = sysInst.parts
.filter((p: any) => p.text)
.map((p: any) => p.text)
.join('\n')
if (systemText) {
blocks.push(createMessageBlock('system', [
createTextRenderBlock(systemText),
], { roleLabel: 'System' }))
}
}
// 渲染 contents
if (Array.isArray(requestBody.contents)) {
for (const content of requestBody.contents) {
const msgBlock = this.renderContent(content)
if (msgBlock) {
blocks.push(msgBlock)
}
}
}
return { blocks, isStream: false }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染响应体
*/
renderResponse(responseBody: any): RenderResult {
if (!responseBody) {
return createEmptyRenderResult('无响应体')
}
// 检查是否为流式响应
if (isStreamResponse(responseBody)) {
return this.renderStreamResponse(responseBody.chunks || [])
}
try {
const blocks: RenderBlock[] = []
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
const candidate = responseBody.candidates?.[0]
if (candidate?.content?.parts) {
const contentBlocks = this.renderParts(candidate.content.parts)
if (contentBlocks.length > 0) {
const badges = this.getBadgesForParts(candidate.content.parts)
blocks.push(createMessageBlock('assistant', contentBlocks, {
roleLabel: 'Assistant',
badges: badges.length > 0 ? badges : undefined,
}))
}
}
return { blocks, isStream: false }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染流式响应
*/
private renderStreamResponse(chunks: any[]): RenderResult {
if (!chunks || chunks.length === 0) {
return createEmptyRenderResult('无响应数据')
}
try {
// 先解析流式响应
const parsed = this.parseStreamResponse(chunks)
if (parsed.parseError) {
return createEmptyRenderResult(parsed.parseError)
}
const blocks: RenderBlock[] = []
// 渲染解析后的消息
for (const msg of parsed.messages) {
const contentBlocks = this.renderParsedContentBlocks(msg.content)
if (contentBlocks.length > 0) {
const badges = this.getBadgesForParsedContent(msg.content)
blocks.push(createMessageBlock(msg.role, contentBlocks, {
roleLabel: this.getRoleLabel(msg.role),
badges: badges.length > 0 ? badges : undefined,
}))
}
}
return { blocks, isStream: true }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染 content 对象
*/
private renderContent(content: any): RenderBlock | null {
if (!content) return null
const role = this.mapRole(content.role)
const contentBlocks = this.renderParts(content.parts || [])
if (contentBlocks.length === 0) return null
const badges = this.getBadgesForParts(content.parts || [])
return createMessageBlock(role, contentBlocks, {
roleLabel: this.getRoleLabel(role),
badges: badges.length > 0 ? badges : undefined,
})
}
/**
* 渲染 parts 数组
*/
private renderParts(parts: any[]): RenderBlock[] {
const result: RenderBlock[] = []
for (const part of parts) {
const rendered = this.renderPart(part)
if (rendered) {
result.push(rendered)
}
}
return result
}
/**
* 渲染单个 part
*/
private renderPart(part: any): RenderBlock | null {
if (!part) return null
// 文本
if (part.text !== undefined) {
return createTextRenderBlock(part.text)
}
// 内联数据(图片等)
if (part.inlineData) {
return createImageRenderBlock({
src: `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`,
mimeType: part.inlineData.mimeType,
})
}
// 函数调用
if (part.functionCall) {
return createToolUseRenderBlock(
part.functionCall.name || '函数调用',
this.formatJson(part.functionCall.args)
)
}
// 函数响应
if (part.functionResponse) {
return createToolResultRenderBlock(
this.formatJson(part.functionResponse.response)
)
}
return null
}
/**
* 渲染已解析的内容块数组
*/
private renderParsedContentBlocks(blocks: ContentBlock[]): RenderBlock[] {
const result: RenderBlock[] = []
for (const block of blocks) {
const rendered = this.renderParsedContentBlock(block)
if (rendered) {
result.push(rendered)
}
}
return result
}
/**
* 渲染单个已解析的内容块
*/
private renderParsedContentBlock(block: ContentBlock): RenderBlock | null {
switch (block.type) {
case 'text':
return createTextRenderBlock(block.text)
case 'tool_use':
return createToolUseRenderBlock(
block.toolName || '函数调用',
this.formatJson(block.input)
)
case 'tool_result': {
const content = typeof block.content === 'string'
? block.content
: this.formatParsedToolResultContent(block.content)
return createToolResultRenderBlock(content, block.isError)
}
case 'image':
return createImageRenderBlock({
src: block.sourceType === 'base64'
? `data:${block.mimeType || 'image/png'};base64,${block.data}`
: block.url,
mimeType: block.mimeType,
alt: block.alt || '图片',
})
default:
return null
}
}
/**
* 获取角色显示标签
*/
private getRoleLabel(role: MessageRole): string {
switch (role) {
case 'user': return 'User'
case 'assistant': return 'Model'
case 'system': return 'System'
case 'tool': return 'Tool'
default: return role
}
}
/**
* 获取 parts 的徽章
*/
private getBadgesForParts(parts: any[]): BadgeRenderBlock[] {
const badges: BadgeRenderBlock[] = []
const hasImage = parts.some((p: any) => p.inlineData)
const hasToolCall = parts.some((p: any) => p.functionCall)
const hasToolResult = parts.some((p: any) => p.functionResponse)
if (hasToolCall) {
badges.push(createBadgeBlock('函数调用', 'outline'))
}
if (hasToolResult) {
badges.push(createBadgeBlock('函数结果', 'outline'))
}
if (hasImage) {
badges.push(createBadgeBlock('图片', 'secondary'))
}
return badges
}
/**
* 获取已解析内容的徽章
*/
private getBadgesForParsedContent(content: ContentBlock[]): BadgeRenderBlock[] {
const badges: BadgeRenderBlock[] = []
const types = new Set(content.map(b => b.type))
if (types.has('tool_use')) {
badges.push(createBadgeBlock('函数调用', 'outline'))
}
if (types.has('tool_result')) {
badges.push(createBadgeBlock('函数结果', 'outline'))
}
if (types.has('image')) {
badges.push(createBadgeBlock('图片', 'secondary'))
}
return badges
}
/**
* 格式化 JSON
*/
private formatJson(input: any): string {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input)
return JSON.stringify(parsed, null, 2)
} catch {
return input
}
}
return JSON.stringify(input, null, 2)
}
/**
* 格式化已解析的工具结果内容
*/
private formatParsedToolResultContent(content: ContentBlock[]): string {
return content
.map(block => {
if (block.type === 'text') return block.text
if (block.type === 'image') return '[图片]'
if (block.type === 'error') return `[错误: ${block.message}]`
return ''
})
.filter(Boolean)
.join('\n')
}
}
/** 单例实例 */
export const geminiParser = new GeminiParser()

View File

@@ -0,0 +1,104 @@
/**
* 对话解析器
*
* 统一解析 Claude/OpenAI/Gemini 等 API 格式的请求和响应
*
* @example
* ```ts
* import { renderRequest, renderResponse, detectApiFormat } from '@/features/usage/conversation'
*
* // 检测 API 格式
* const format = detectApiFormat(requestBody, responseBody, apiFormatHint)
*
* // 渲染请求体为渲染块
* const requestResult = renderRequest(requestBody, responseBody, apiFormatHint)
*
* // 渲染响应体为渲染块
* const responseResult = renderResponse(responseBody, requestBody, apiFormatHint)
* ```
*/
// 导出解析类型
export type {
ApiFormat,
MessageRole,
ContentBlockType,
ContentBlock,
TextContentBlock,
ThinkingContentBlock,
ToolUseContentBlock,
ToolResultContentBlock,
ImageContentBlock,
FileContentBlock,
CodeContentBlock,
ErrorContentBlock,
ParsedMessage,
ParsedConversation,
ApiFormatParser,
FormatDetector,
RequestParser,
ResponseParser,
} from './types'
// 导出渲染类型
export type {
RenderBlock,
RenderResult,
TextRenderBlock,
CollapsibleRenderBlock,
CodeRenderBlock,
BadgeRenderBlock,
ImageRenderBlock,
ErrorRenderBlock,
ContainerRenderBlock,
MessageRenderBlock,
ToolUseRenderBlock,
ToolResultRenderBlock,
DividerRenderBlock,
LabelRenderBlock,
} from './render'
// 导出解析工具函数
export {
isStreamResponse,
createTextBlock,
createThinkingBlock,
createToolUseBlock,
createToolResultBlock,
createImageBlock,
createErrorBlock,
createEmptyConversation,
createMessage,
} from './types'
// 导出渲染工具函数
export {
createTextBlock as createTextRenderBlock,
createCollapsibleBlock,
createCodeBlock,
createBadgeBlock,
createImageBlock as createImageRenderBlock,
createErrorBlock as createErrorRenderBlock,
createContainerBlock,
createMessageBlock,
createToolUseBlock as createToolUseRenderBlock,
createToolResultBlock as createToolResultRenderBlock,
createDividerBlock,
createLabelBlock,
createEmptyRenderResult,
} from './render'
// 导出解析器
export { claudeParser, ClaudeParser } from './claude'
export { openaiParser, OpenAIParser } from './openai'
export { geminiParser, GeminiParser } from './gemini'
// 导出注册表和统一入口
export {
parserRegistry,
parseRequest,
parseResponse,
detectApiFormat,
renderRequest,
renderResponse,
} from './registry'

View File

@@ -0,0 +1,630 @@
/**
* OpenAI API 格式解析器
*/
import type {
ApiFormatParser,
ParsedConversation,
ParsedMessage,
ContentBlock,
MessageRole,
} from './types'
import {
createEmptyConversation,
createMessage,
createTextBlock,
createToolUseBlock,
createToolResultBlock,
createImageBlock,
isStreamResponse,
} from './types'
import type { RenderResult, RenderBlock, BadgeRenderBlock } from './render'
import {
createTextBlock as createTextRenderBlock,
createBadgeBlock,
createImageBlock as createImageRenderBlock,
createMessageBlock,
createToolUseBlock as createToolUseRenderBlock,
createToolResultBlock as createToolResultRenderBlock,
createEmptyRenderResult,
} from './render'
/**
* OpenAI API 格式解析器
*/
export class OpenAIParser implements ApiFormatParser {
readonly format = 'openai' as const
readonly displayName = 'OpenAI'
/**
* 检测是否为 OpenAI 格式
*/
detect(requestBody: any, responseBody: any, hint?: string): number {
// 1. 后端提示优先
if (hint) {
const lowerHint = hint.toLowerCase()
if (lowerHint.includes('openai')) return 100
if (lowerHint.includes('claude') || lowerHint.includes('gemini')) return 0
}
// 2. 检查模型名
const model = requestBody?.model?.toLowerCase() || ''
if (model.includes('gpt') || model.includes('o1') || model.includes('o3')) return 95
// 3. 检查请求体结构
if (!requestBody?.messages || !Array.isArray(requestBody.messages)) {
return 0
}
// 4. 检查响应体特征
const respBody = isStreamResponse(responseBody)
? responseBody.chunks?.[0]
: responseBody
if (respBody) {
// OpenAI 响应特征: choices 数组
if (respBody.choices || respBody.object?.includes('chat.completion')) {
return 90
}
// 明确是 Claude 格式
if (respBody.type === 'message' || respBody.type?.startsWith('content_block')) {
return 0
}
}
// 5. 检查 OpenAI 特有的请求结构
// OpenAI 的 system 是在 messages 数组中作为 role: system
const hasSystemInMessages = requestBody.messages?.some(
(m: any) => m.role === 'system'
)
if (hasSystemInMessages) {
return 60
}
return 0
}
/**
* 解析请求体
*/
parseRequest(requestBody: any): ParsedConversation {
if (!requestBody) {
return createEmptyConversation('openai', '无请求体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: requestBody.stream === true,
apiFormat: 'openai',
model: requestBody.model,
}
if (Array.isArray(requestBody.messages)) {
for (const msg of requestBody.messages) {
// OpenAI 的 system 消息在 messages 数组中
if (msg.role === 'system') {
const systemText = typeof msg.content === 'string'
? msg.content
: ''
result.system = result.system
? `${result.system }\n${ systemText}`
: systemText
continue
}
const parsedMsg = this.parseMessage(msg)
if (parsedMsg) {
result.messages.push(parsedMsg)
}
}
}
return result
} catch (e) {
return createEmptyConversation('openai', `解析失败: ${e}`)
}
}
/**
* 解析响应体
*/
parseResponse(responseBody: any): ParsedConversation {
if (!responseBody) {
return createEmptyConversation('openai', '无响应体')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: false,
apiFormat: 'openai',
model: responseBody.model,
}
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
const message = responseBody.choices?.[0]?.message
if (message) {
const contentBlocks: ContentBlock[] = []
// 文本内容
if (message.content) {
contentBlocks.push(createTextBlock(message.content))
}
// 工具调用
if (message.tool_calls) {
for (const call of message.tool_calls) {
contentBlocks.push(createToolUseBlock(
call.id || '',
call.function?.name || '',
call.function?.arguments || '{}'
))
}
}
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
}
return result
} catch (e) {
return createEmptyConversation('openai', `解析失败: ${e}`)
}
}
/**
* 解析流式响应
*/
parseStreamResponse(chunks: any[]): ParsedConversation {
if (!chunks || chunks.length === 0) {
return createEmptyConversation('openai', '无响应数据')
}
try {
const result: ParsedConversation = {
messages: [],
isStream: true,
apiFormat: 'openai',
}
const textParts: string[] = []
const toolCalls = new Map<number, { name: string; id: string; args: string[] }>()
for (const chunk of chunks) {
// 提取模型名
if (chunk.model && !result.model) {
result.model = chunk.model
}
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
textParts.push(delta.content)
}
if (delta?.tool_calls) {
for (const call of delta.tool_calls) {
const index = call.index ?? 0
if (!toolCalls.has(index)) {
toolCalls.set(index, {
name: call.function?.name || '',
id: call.id || '',
args: [],
})
}
const existing = toolCalls.get(index)!
if (call.function?.name) {
existing.name = call.function.name
}
if (call.id) {
existing.id = call.id
}
if (call.function?.arguments) {
existing.args.push(call.function.arguments)
}
}
}
}
const contentBlocks: ContentBlock[] = []
// 文本内容
if (textParts.length > 0) {
contentBlocks.push(createTextBlock(textParts.join('')))
}
// 工具调用
for (const [, call] of toolCalls) {
contentBlocks.push(createToolUseBlock(
call.id,
call.name,
call.args.join('')
))
}
if (contentBlocks.length > 0) {
result.messages.push(createMessage('assistant', contentBlocks))
}
return result
} catch (e) {
return createEmptyConversation('openai', `解析失败: ${e}`)
}
}
/**
* 解析单条消息
*/
private parseMessage(msg: any): ParsedMessage | null {
if (!msg || !msg.role) return null
const role = this.mapRole(msg.role)
const contentBlocks: ContentBlock[] = []
// 文本内容
if (typeof msg.content === 'string') {
contentBlocks.push(createTextBlock(msg.content))
} else if (Array.isArray(msg.content)) {
// Vision API 格式
for (const part of msg.content) {
if (part.type === 'text') {
contentBlocks.push(createTextBlock(part.text || ''))
} else if (part.type === 'image_url') {
contentBlocks.push(createImageBlock('url', {
url: part.image_url?.url,
alt: '[图片]',
}))
}
}
}
// 工具调用assistant 消息)
if (msg.tool_calls) {
for (const call of msg.tool_calls) {
contentBlocks.push(createToolUseBlock(
call.id || '',
call.function?.name || '',
call.function?.arguments || '{}'
))
}
}
// 工具结果tool 消息)
if (msg.tool_call_id) {
const content = typeof msg.content === 'string'
? msg.content
: JSON.stringify(msg.content, null, 2)
contentBlocks.push(createToolResultBlock(
msg.tool_call_id,
content
))
}
if (contentBlocks.length === 0) return null
return createMessage(role, contentBlocks)
}
/**
* 映射角色
*/
private mapRole(role: string): MessageRole {
switch (role) {
case 'user':
return 'user'
case 'assistant':
return 'assistant'
case 'system':
return 'system'
case 'tool':
return 'tool'
default:
return 'user'
}
}
// ============================================================
// 渲染方法
// ============================================================
/**
* 渲染请求体
*/
renderRequest(requestBody: any): RenderResult {
if (!requestBody) {
return createEmptyRenderResult('无请求体')
}
try {
const blocks: RenderBlock[] = []
const isStream = requestBody.stream === true
if (Array.isArray(requestBody.messages)) {
for (const msg of requestBody.messages) {
// system 消息单独处理
if (msg.role === 'system') {
const systemText = typeof msg.content === 'string' ? msg.content : ''
if (systemText) {
blocks.push(createMessageBlock('system', [
createTextRenderBlock(systemText),
], { roleLabel: 'System' }))
}
continue
}
const msgBlock = this.renderMessage(msg)
if (msgBlock) {
blocks.push(msgBlock)
}
}
}
return { blocks, isStream }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染响应体
*/
renderResponse(responseBody: any): RenderResult {
if (!responseBody) {
return createEmptyRenderResult('无响应体')
}
// 检查是否为流式响应
if (isStreamResponse(responseBody)) {
return this.renderStreamResponse(responseBody.chunks || [])
}
try {
const blocks: RenderBlock[] = []
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
const message = responseBody.choices?.[0]?.message
if (message) {
const contentBlocks: RenderBlock[] = []
const badges: BadgeRenderBlock[] = []
// 文本内容
if (message.content) {
contentBlocks.push(createTextRenderBlock(message.content))
}
// 工具调用
if (message.tool_calls) {
badges.push(createBadgeBlock('工具调用', 'outline'))
for (const call of message.tool_calls) {
contentBlocks.push(createToolUseRenderBlock(
call.function?.name || '工具调用',
this.formatJson(call.function?.arguments),
call.id
))
}
}
if (contentBlocks.length > 0) {
blocks.push(createMessageBlock('assistant', contentBlocks, {
roleLabel: 'Assistant',
badges: badges.length > 0 ? badges : undefined,
}))
}
}
return { blocks, isStream: false }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染流式响应
*/
private renderStreamResponse(chunks: any[]): RenderResult {
if (!chunks || chunks.length === 0) {
return createEmptyRenderResult('无响应数据')
}
try {
// 先解析流式响应
const parsed = this.parseStreamResponse(chunks)
if (parsed.parseError) {
return createEmptyRenderResult(parsed.parseError)
}
const blocks: RenderBlock[] = []
// 渲染解析后的消息
for (const msg of parsed.messages) {
const contentBlocks = this.renderParsedContentBlocks(msg.content)
if (contentBlocks.length > 0) {
const badges = this.getBadgesForParsedContent(msg.content)
blocks.push(createMessageBlock(msg.role, contentBlocks, {
roleLabel: this.getRoleLabel(msg.role),
badges: badges.length > 0 ? badges : undefined,
}))
}
}
return { blocks, isStream: true }
} catch (e) {
return createEmptyRenderResult(`渲染失败: ${e}`)
}
}
/**
* 渲染单条消息
*/
private renderMessage(msg: any): RenderBlock | null {
if (!msg || !msg.role) return null
const role = this.mapRole(msg.role)
const contentBlocks: RenderBlock[] = []
const badges: BadgeRenderBlock[] = []
// 文本内容
if (typeof msg.content === 'string') {
contentBlocks.push(createTextRenderBlock(msg.content))
} else if (Array.isArray(msg.content)) {
// Vision API 格式
for (const part of msg.content) {
if (part.type === 'text') {
contentBlocks.push(createTextRenderBlock(part.text || ''))
} else if (part.type === 'image_url') {
badges.push(createBadgeBlock('图片', 'secondary'))
contentBlocks.push(createImageRenderBlock({
src: part.image_url?.url,
alt: '[图片]',
}))
}
}
}
// 工具调用assistant 消息)
if (msg.tool_calls) {
badges.push(createBadgeBlock('工具调用', 'outline'))
for (const call of msg.tool_calls) {
contentBlocks.push(createToolUseRenderBlock(
call.function?.name || '工具调用',
this.formatJson(call.function?.arguments),
call.id
))
}
}
// 工具结果tool 消息)
if (msg.tool_call_id) {
badges.push(createBadgeBlock('工具结果', 'outline'))
const content = typeof msg.content === 'string'
? msg.content
: JSON.stringify(msg.content, null, 2)
contentBlocks.push(createToolResultRenderBlock(content))
}
if (contentBlocks.length === 0) return null
return createMessageBlock(role, contentBlocks, {
roleLabel: this.getRoleLabel(role),
badges: badges.length > 0 ? badges : undefined,
})
}
/**
* 渲染已解析的内容块数组
*/
private renderParsedContentBlocks(blocks: ContentBlock[]): RenderBlock[] {
const result: RenderBlock[] = []
for (const block of blocks) {
const rendered = this.renderParsedContentBlock(block)
if (rendered) {
result.push(rendered)
}
}
return result
}
/**
* 渲染单个已解析的内容块
*/
private renderParsedContentBlock(block: ContentBlock): RenderBlock | null {
switch (block.type) {
case 'text':
return createTextRenderBlock(block.text)
case 'tool_use':
return createToolUseRenderBlock(
block.toolName || '工具调用',
this.formatJson(block.input),
block.toolId
)
case 'tool_result': {
const content = typeof block.content === 'string'
? block.content
: this.formatParsedToolResultContent(block.content)
return createToolResultRenderBlock(content, block.isError)
}
case 'image':
return createImageRenderBlock({
src: block.sourceType === 'base64'
? `data:${block.mimeType || 'image/png'};base64,${block.data}`
: block.url,
mimeType: block.mimeType,
alt: block.alt || '图片',
})
default:
return null
}
}
/**
* 获取角色显示标签
*/
private getRoleLabel(role: MessageRole): string {
switch (role) {
case 'user': return 'User'
case 'assistant': return 'Assistant'
case 'system': return 'System'
case 'tool': return 'Tool'
default: return role
}
}
/**
* 获取已解析内容的徽章
*/
private getBadgesForParsedContent(content: ContentBlock[]): BadgeRenderBlock[] {
const badges: BadgeRenderBlock[] = []
const types = new Set(content.map(b => b.type))
if (types.has('tool_use')) {
badges.push(createBadgeBlock('工具调用', 'outline'))
}
if (types.has('tool_result')) {
badges.push(createBadgeBlock('工具结果', 'outline'))
}
if (types.has('image')) {
badges.push(createBadgeBlock('图片', 'secondary'))
}
return badges
}
/**
* 格式化 JSON
*/
private formatJson(input: any): string {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input)
return JSON.stringify(parsed, null, 2)
} catch {
return input
}
}
return JSON.stringify(input, null, 2)
}
/**
* 格式化已解析的工具结果内容
*/
private formatParsedToolResultContent(content: ContentBlock[]): string {
return content
.map(block => {
if (block.type === 'text') return block.text
if (block.type === 'image') return '[图片]'
if (block.type === 'error') return `[错误: ${block.message}]`
return ''
})
.filter(Boolean)
.join('\n')
}
}
/** 单例实例 */
export const openaiParser = new OpenAIParser()

View File

@@ -0,0 +1,169 @@
/**
* 解析器注册表和统一入口
*/
import type { ApiFormat, ApiFormatParser, ParsedConversation } from './types'
import { createEmptyConversation, isStreamResponse } from './types'
import type { RenderResult } from './render'
import { createEmptyRenderResult } from './render'
import { claudeParser } from './claude'
import { openaiParser } from './openai'
import { geminiParser } from './gemini'
/**
* 解析器注册表
*/
class ParserRegistry {
private parsers: ApiFormatParser[] = []
/**
* 注册解析器
*/
register(parser: ApiFormatParser): void {
this.parsers.push(parser)
}
/**
* 获取所有解析器
*/
getAll(): ApiFormatParser[] {
return [...this.parsers]
}
/**
* 根据格式获取解析器
*/
getByFormat(format: ApiFormat): ApiFormatParser | undefined {
return this.parsers.find(p => p.format === format)
}
/**
* 检测 API 格式并返回最佳匹配的解析器
*/
detectParser(requestBody: any, responseBody: any, hint?: string): ApiFormatParser | undefined {
let bestParser: ApiFormatParser | undefined
let bestScore = 0
for (const parser of this.parsers) {
const score = parser.detect(requestBody, responseBody, hint)
if (score > bestScore) {
bestScore = score
bestParser = parser
}
}
return bestScore > 0 ? bestParser : undefined
}
/**
* 检测 API 格式
*/
detectFormat(requestBody: any, responseBody: any, hint?: string): ApiFormat {
const parser = this.detectParser(requestBody, responseBody, hint)
return parser?.format ?? 'unknown'
}
}
/** 全局解析器注册表 */
export const parserRegistry = new ParserRegistry()
// 注册默认解析器
parserRegistry.register(claudeParser)
parserRegistry.register(openaiParser)
parserRegistry.register(geminiParser)
/**
* 解析请求体
*/
export function parseRequest(
requestBody: any,
responseBody?: any,
formatHint?: string
): ParsedConversation {
if (!requestBody) {
return createEmptyConversation('unknown', '无请求体')
}
const parser = parserRegistry.detectParser(requestBody, responseBody, formatHint)
if (!parser) {
return createEmptyConversation('unknown', '无法识别的 API 格式')
}
return parser.parseRequest(requestBody)
}
/**
* 解析响应体
*/
export function parseResponse(
responseBody: any,
requestBody?: any,
formatHint?: string
): ParsedConversation {
if (!responseBody) {
return createEmptyConversation('unknown', '无响应体')
}
const parser = parserRegistry.detectParser(requestBody, responseBody, formatHint)
if (!parser) {
return createEmptyConversation('unknown', '无法识别的 API 格式')
}
// 判断是否为流式响应
if (isStreamResponse(responseBody)) {
return parser.parseStreamResponse(responseBody.chunks || [])
}
return parser.parseResponse(responseBody)
}
/**
* 检测 API 格式
*/
export function detectApiFormat(
requestBody: any,
responseBody: any,
hint?: string
): ApiFormat {
return parserRegistry.detectFormat(requestBody, responseBody, hint)
}
/**
* 渲染请求体
*/
export function renderRequest(
requestBody: any,
responseBody?: any,
formatHint?: string
): RenderResult {
if (!requestBody) {
return createEmptyRenderResult('无请求体')
}
const parser = parserRegistry.detectParser(requestBody, responseBody, formatHint)
if (!parser) {
return createEmptyRenderResult('无法识别的 API 格式')
}
return parser.renderRequest(requestBody)
}
/**
* 渲染响应体
*/
export function renderResponse(
responseBody: any,
requestBody?: any,
formatHint?: string
): RenderResult {
if (!responseBody) {
return createEmptyRenderResult('无响应体')
}
const parser = parserRegistry.detectParser(requestBody, responseBody, formatHint)
if (!parser) {
return createEmptyRenderResult('无法识别的 API 格式')
}
return parser.renderResponse(responseBody)
}

View File

@@ -0,0 +1,230 @@
/**
* 渲染块类型定义
* 用于描述对话内容的渲染结构,由通用渲染组件解释执行
*/
// ============================================================
// 渲染块类型
// ============================================================
/** 文本块 */
export interface TextRenderBlock {
type: 'text'
content: string
/** 是否保留空白pre-wrap */
preWrap?: boolean
/** 额外样式类名 */
className?: string
}
/** 可折叠块 */
export interface CollapsibleRenderBlock {
type: 'collapsible'
/** 折叠标题 */
title: string
/** 折叠内容 */
content: RenderBlock[]
/** 默认是否展开 */
defaultOpen?: boolean
/** 额外样式类名 */
className?: string
}
/** 代码块 */
export interface CodeRenderBlock {
type: 'code'
code: string
language?: string
/** 最大高度限制 */
maxHeight?: number
}
/** 徽章块 */
export interface BadgeRenderBlock {
type: 'badge'
label: string
variant?: 'default' | 'secondary' | 'outline' | 'destructive'
}
/** 图片块 */
export interface ImageRenderBlock {
type: 'image'
/** Base64 数据或 URL */
src?: string
alt?: string
mimeType?: string
}
/** 错误块 */
export interface ErrorRenderBlock {
type: 'error'
message: string
code?: string
}
/** 容器块 - 用于包装一组子块 */
export interface ContainerRenderBlock {
type: 'container'
children: RenderBlock[]
/** 额外样式类名 */
className?: string
/** 容器头部(可选) */
header?: RenderBlock[]
}
/** 消息块 - 表示一条完整的对话消息 */
export interface MessageRenderBlock {
type: 'message'
/** 角色标识 */
role: 'user' | 'assistant' | 'system' | 'tool'
/** 角色显示名称 */
roleLabel?: string
/** 消息头部的徽章 */
badges?: BadgeRenderBlock[]
/** 消息内容 */
content: RenderBlock[]
}
/** 工具调用块 */
export interface ToolUseRenderBlock {
type: 'tool_use'
toolName: string
toolId?: string
input: string
}
/** 工具结果块 */
export interface ToolResultRenderBlock {
type: 'tool_result'
content: string
isError?: boolean
}
/** 分隔符块 */
export interface DividerRenderBlock {
type: 'divider'
}
/** 标签行块 - 用于显示 key-value 形式的信息 */
export interface LabelRenderBlock {
type: 'label'
label: string
value: string
/** 值是否使用等宽字体 */
mono?: boolean
}
// ============================================================
// 联合类型
// ============================================================
/** 所有渲染块类型的联合 */
export type RenderBlock =
| TextRenderBlock
| CollapsibleRenderBlock
| CodeRenderBlock
| BadgeRenderBlock
| ImageRenderBlock
| ErrorRenderBlock
| ContainerRenderBlock
| MessageRenderBlock
| ToolUseRenderBlock
| ToolResultRenderBlock
| DividerRenderBlock
| LabelRenderBlock
// ============================================================
// 渲染结果
// ============================================================
/** 渲染结果 */
export interface RenderResult {
/** 渲染块列表 */
blocks: RenderBlock[]
/** 是否为流式响应 */
isStream?: boolean
/** 渲染错误 */
error?: string
}
// ============================================================
// 工具函数 - 创建渲染块
// ============================================================
export function createTextBlock(content: string, options?: Partial<TextRenderBlock>): TextRenderBlock {
return { type: 'text', content, preWrap: true, ...options }
}
export function createCollapsibleBlock(
title: string,
content: RenderBlock[],
options?: Partial<CollapsibleRenderBlock>
): CollapsibleRenderBlock {
return { type: 'collapsible', title, content, defaultOpen: false, ...options }
}
export function createCodeBlock(code: string, language?: string): CodeRenderBlock {
return { type: 'code', code, language }
}
export function createBadgeBlock(
label: string,
variant?: BadgeRenderBlock['variant']
): BadgeRenderBlock {
return { type: 'badge', label, variant }
}
export function createImageBlock(options: Omit<ImageRenderBlock, 'type'>): ImageRenderBlock {
return { type: 'image', ...options }
}
export function createErrorBlock(message: string, code?: string): ErrorRenderBlock {
return { type: 'error', message, code }
}
export function createContainerBlock(
children: RenderBlock[],
options?: Partial<ContainerRenderBlock>
): ContainerRenderBlock {
return { type: 'container', children, ...options }
}
export function createMessageBlock(
role: MessageRenderBlock['role'],
content: RenderBlock[],
options?: Partial<MessageRenderBlock>
): MessageRenderBlock {
return { type: 'message', role, content, ...options }
}
export function createToolUseBlock(
toolName: string,
input: string,
toolId?: string
): ToolUseRenderBlock {
return { type: 'tool_use', toolName, input, toolId }
}
export function createToolResultBlock(
content: string,
isError?: boolean
): ToolResultRenderBlock {
return { type: 'tool_result', content, isError }
}
export function createDividerBlock(): DividerRenderBlock {
return { type: 'divider' }
}
export function createLabelBlock(
label: string,
value: string,
mono?: boolean
): LabelRenderBlock {
return { type: 'label', label, value, mono }
}
/** 创建空的渲染结果 */
export function createEmptyRenderResult(error?: string): RenderResult {
return { blocks: [], error }
}

View File

@@ -0,0 +1,287 @@
/**
* 对话解析器类型定义
* 统一各种 API 格式Claude/OpenAI/Gemini的消息解析接口
*/
// ============================================================
// API 格式枚举
// ============================================================
export type ApiFormat = 'claude' | 'openai' | 'gemini' | 'unknown'
// ============================================================
// 消息类型定义
// ============================================================
/** 消息角色 */
export type MessageRole = 'system' | 'user' | 'assistant' | 'tool'
/** 内容块类型 */
export type ContentBlockType =
| 'text' // 普通文本
| 'thinking' // 思考过程Claude extended thinking
| 'tool_use' // 工具调用
| 'tool_result' // 工具结果
| 'image' // 图片
| 'file' // 文件
| 'code' // 代码块
| 'error' // 错误信息
/** 内容块基础接口 */
export interface ContentBlockBase {
type: ContentBlockType
}
/** 文本内容块 */
export interface TextContentBlock extends ContentBlockBase {
type: 'text'
text: string
}
/** 思考内容块 */
export interface ThinkingContentBlock extends ContentBlockBase {
type: 'thinking'
thinking: string
/** 签名(用于验证,可选) */
signature?: string
}
/** 工具调用内容块 */
export interface ToolUseContentBlock extends ContentBlockBase {
type: 'tool_use'
toolId: string
toolName: string
input: Record<string, any> | string
}
/** 工具结果内容块 */
export interface ToolResultContentBlock extends ContentBlockBase {
type: 'tool_result'
toolUseId: string
content: string | ContentBlock[]
isError?: boolean
}
/** 图片内容块 */
export interface ImageContentBlock extends ContentBlockBase {
type: 'image'
/** 图片来源类型 */
sourceType: 'base64' | 'url'
/** Base64 数据或 URL */
data?: string
url?: string
/** MIME 类型 */
mimeType?: string
/** 图片描述alt text */
alt?: string
}
/** 文件内容块 */
export interface FileContentBlock extends ContentBlockBase {
type: 'file'
fileName: string
fileType?: string
fileSize?: number
content?: string
}
/** 代码内容块 */
export interface CodeContentBlock extends ContentBlockBase {
type: 'code'
language?: string
code: string
}
/** 错误内容块 */
export interface ErrorContentBlock extends ContentBlockBase {
type: 'error'
message: string
code?: string
}
/** 所有内容块类型的联合 */
export type ContentBlock =
| TextContentBlock
| ThinkingContentBlock
| ToolUseContentBlock
| ToolResultContentBlock
| ImageContentBlock
| FileContentBlock
| CodeContentBlock
| ErrorContentBlock
// ============================================================
// 消息定义
// ============================================================
/** 解析后的消息 */
export interface ParsedMessage {
/** 消息角色 */
role: MessageRole
/** 内容块列表 */
content: ContentBlock[]
}
/** 解析后的对话 */
export interface ParsedConversation {
/** 系统提示词 */
system?: string
/** 消息列表 */
messages: ParsedMessage[]
/** 是否为流式响应 */
isStream: boolean
/** 解析错误(如果有) */
parseError?: string
/** 原始 API 格式 */
apiFormat: ApiFormat
/** 模型名称 */
model?: string
}
// ============================================================
// 解析器接口
// ============================================================
/** API 格式检测器 */
export interface FormatDetector {
/**
* 检测是否匹配该格式
* @param requestBody 请求体
* @param responseBody 响应体
* @param hint 后端提供的格式提示
* @returns 匹配置信度 (0-100)0 表示不匹配
*/
detect(requestBody: any, responseBody: any, hint?: string): number
}
/** 请求体解析器 */
export interface RequestParser {
/**
* 解析请求体
* @param requestBody 请求体
* @returns 解析后的对话
*/
parseRequest(requestBody: any): ParsedConversation
}
/** 响应体解析器 */
export interface ResponseParser {
/**
* 解析响应体
* @param responseBody 响应体
* @returns 解析后的对话
*/
parseResponse(responseBody: any): ParsedConversation
/**
* 解析流式响应
* @param chunks 响应块列表
* @returns 解析后的对话
*/
parseStreamResponse(chunks: any[]): ParsedConversation
}
/** 完整的 API 格式解析器 */
export interface ApiFormatParser extends FormatDetector, RequestParser, ResponseParser {
/** 格式名称 */
readonly format: ApiFormat
/** 格式显示名称 */
readonly displayName: string
/**
* 渲染请求体为渲染块
* @param requestBody 请求体
* @returns 渲染结果
*/
renderRequest(requestBody: any): import('./render').RenderResult
/**
* 渲染响应体为渲染块
* @param responseBody 响应体
* @returns 渲染结果
*/
renderResponse(responseBody: any): import('./render').RenderResult
}
// ============================================================
// 流式响应相关
// ============================================================
/** 流式响应元数据 */
export interface StreamMetadata {
stream: boolean
}
/** 流式响应体结构 */
export interface StreamResponseBody {
metadata?: StreamMetadata
chunks?: any[]
}
/** 检查是否为流式响应 */
export function isStreamResponse(body: any): body is StreamResponseBody {
return body?.metadata?.stream === true && Array.isArray(body?.chunks)
}
// ============================================================
// 工具函数
// ============================================================
/** 创建文本内容块 */
export function createTextBlock(text: string): TextContentBlock {
return { type: 'text', text }
}
/** 创建思考内容块 */
export function createThinkingBlock(thinking: string, signature?: string): ThinkingContentBlock {
return { type: 'thinking', thinking, signature }
}
/** 创建工具调用内容块 */
export function createToolUseBlock(
toolId: string,
toolName: string,
input: Record<string, any> | string
): ToolUseContentBlock {
return { type: 'tool_use', toolId, toolName, input }
}
/** 创建工具结果内容块 */
export function createToolResultBlock(
toolUseId: string,
content: string | ContentBlock[],
isError?: boolean
): ToolResultContentBlock {
return { type: 'tool_result', toolUseId, content, isError }
}
/** 创建图片内容块 */
export function createImageBlock(
sourceType: 'base64' | 'url',
options: { data?: string; url?: string; mimeType?: string; alt?: string }
): ImageContentBlock {
return { type: 'image', sourceType, ...options }
}
/** 创建错误内容块 */
export function createErrorBlock(message: string, code?: string): ErrorContentBlock {
return { type: 'error', message, code }
}
/** 创建空的解析结果 */
export function createEmptyConversation(
apiFormat: ApiFormat = 'unknown',
parseError?: string
): ParsedConversation {
return {
messages: [],
isStream: false,
apiFormat,
parseError,
}
}
/** 创建解析后的消息 */
export function createMessage(role: MessageRole, content: ContentBlock[]): ParsedMessage {
return { role, content }
}