mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 将对话解析器拆分为独立模块
将 lib/conversationParser 重构为 conversation/ 目录: - 按 API 格式拆分解析器 (claude.ts, openai.ts, gemini.ts) - 提取公共渲染逻辑到 render.ts - 添加解析器注册表支持自动格式检测 - 更新组件导入路径
This commit is contained in:
@@ -539,7 +539,7 @@ import {
|
||||
renderResponse,
|
||||
type RenderResult,
|
||||
type RenderBlock,
|
||||
} from '../lib/conversationParser'
|
||||
} from '../conversation'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
814
frontend/src/features/usage/conversation/claude.ts
Normal file
814
frontend/src/features/usage/conversation/claude.ts
Normal 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()
|
||||
607
frontend/src/features/usage/conversation/gemini.ts
Normal file
607
frontend/src/features/usage/conversation/gemini.ts
Normal 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()
|
||||
104
frontend/src/features/usage/conversation/index.ts
Normal file
104
frontend/src/features/usage/conversation/index.ts
Normal 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'
|
||||
630
frontend/src/features/usage/conversation/openai.ts
Normal file
630
frontend/src/features/usage/conversation/openai.ts
Normal 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()
|
||||
169
frontend/src/features/usage/conversation/registry.ts
Normal file
169
frontend/src/features/usage/conversation/registry.ts
Normal 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)
|
||||
}
|
||||
230
frontend/src/features/usage/conversation/render.ts
Normal file
230
frontend/src/features/usage/conversation/render.ts
Normal 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 }
|
||||
}
|
||||
287
frontend/src/features/usage/conversation/types.ts
Normal file
287
frontend/src/features/usage/conversation/types.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user