mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 前端全面替换 any 为 unknown 并统一错误处理,后端用量记录补写请求头/体
- 前端 API 层、stores、conversation 解析器、组件全面替换 any 为 unknown/具体类型 - 错误处理统一使用 parseApiError/getErrorStatus 替代 err.response?.data?.detail 模式 - 后端 handler/TaskService/UsageLifecycle/StreamTracker 链路传递 request_headers/request_body - streaming/pending 状态更新时可补写客户端和提供商的请求头及请求体 - 新增 TaskService 和 UsageService 相关测试
This commit is contained in:
@@ -371,6 +371,7 @@ import Skeleton from '@/components/ui/skeleton.vue'
|
||||
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-vue-next'
|
||||
import { requestTraceApi, type RequestTrace, type CandidateRecord } from '@/api/requestTrace'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
|
||||
// 节点组类型
|
||||
@@ -459,8 +460,10 @@ const getFinalStatusLabel = (status: string) => {
|
||||
}
|
||||
|
||||
// 获取最终状态徽章样式
|
||||
const getFinalStatusBadgeVariant = (status: string): any => {
|
||||
const variants: Record<string, string> = {
|
||||
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark'
|
||||
|
||||
const getFinalStatusBadgeVariant = (status: string): BadgeVariant => {
|
||||
const variants: Record<string, BadgeVariant> = {
|
||||
success: 'success',
|
||||
failed: 'destructive',
|
||||
streaming: 'secondary',
|
||||
@@ -493,19 +496,19 @@ const formatSize = (bytes: number): string => {
|
||||
}
|
||||
|
||||
// 代理 timing 分阶段展示
|
||||
const proxyTimingBreakdown = (proxy: Record<string, any>): string => {
|
||||
const t = proxy.timing
|
||||
const proxyTimingBreakdown = (proxy: Record<string, unknown>): string => {
|
||||
const t = proxy.timing as Record<string, number | null | undefined> | undefined
|
||||
if (!t) return ''
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// 兼容旧版 timing(含 body_read_ms/decompress_ms)
|
||||
const readDecompress = (t.body_read_ms || 0) + (t.decompress_ms || 0)
|
||||
const readDecompress = ((t.body_read_ms as number) || 0) + ((t.decompress_ms as number) || 0)
|
||||
if (readDecompress > 0) {
|
||||
let label = `读取 ${formatLatency(readDecompress)}`
|
||||
if (t.decompress_ms != null && t.decompress_ms > 0 && t.wire_size != null && t.body_size != null && t.body_size > 0) {
|
||||
const ratio = Math.round((1 - t.wire_size / t.body_size) * 100)
|
||||
label += ` ${formatSize(t.wire_size)}→${formatSize(t.body_size)}`
|
||||
if (t.decompress_ms != null && t.decompress_ms > 0 && t.wire_size != null && t.body_size != null && (t.body_size as number) > 0) {
|
||||
const ratio = Math.round((1 - (t.wire_size as number) / (t.body_size as number)) * 100)
|
||||
label += ` ${formatSize(t.wire_size as number)}→${formatSize(t.body_size as number)}`
|
||||
if (ratio > 0) label += ` -${ratio}%`
|
||||
}
|
||||
parts.push(label)
|
||||
@@ -514,29 +517,29 @@ const proxyTimingBreakdown = (proxy: Record<string, any>): string => {
|
||||
const ttfbMs = t.ttfb_ms ?? t.upstream_ms
|
||||
const processingMs = t.upstream_processing_ms ?? (
|
||||
ttfbMs != null && t.connect_ms != null && t.tls_ms != null
|
||||
? Math.max(0, ttfbMs - t.connect_ms - t.tls_ms)
|
||||
? Math.max(0, (ttfbMs as number) - (t.connect_ms as number) - (t.tls_ms as number))
|
||||
: null
|
||||
)
|
||||
|
||||
if (t.dns_ms != null && t.dns_ms > 0) {
|
||||
parts.push(`DNS ${formatLatency(t.dns_ms)}`)
|
||||
if (t.dns_ms != null && (t.dns_ms as number) > 0) {
|
||||
parts.push(`DNS ${formatLatency(t.dns_ms as number)}`)
|
||||
}
|
||||
if (t.connect_ms != null && t.connect_ms > 0) {
|
||||
parts.push(`连接 ${formatLatency(t.connect_ms)}`)
|
||||
if (t.connect_ms != null && (t.connect_ms as number) > 0) {
|
||||
parts.push(`连接 ${formatLatency(t.connect_ms as number)}`)
|
||||
}
|
||||
if (t.tls_ms != null && t.tls_ms > 0) {
|
||||
parts.push(`TLS ${formatLatency(t.tls_ms)}`)
|
||||
if (t.tls_ms != null && (t.tls_ms as number) > 0) {
|
||||
parts.push(`TLS ${formatLatency(t.tls_ms as number)}`)
|
||||
}
|
||||
if (ttfbMs != null && ttfbMs > 0) {
|
||||
parts.push(`TTFB ${formatLatency(ttfbMs)}`)
|
||||
if (ttfbMs != null && (ttfbMs as number) > 0) {
|
||||
parts.push(`TTFB ${formatLatency(ttfbMs as number)}`)
|
||||
}
|
||||
if (processingMs != null && processingMs > 0) {
|
||||
parts.push(`上游处理 ${formatLatency(Math.round(processingMs))}`)
|
||||
if (processingMs != null && (processingMs as number) > 0) {
|
||||
parts.push(`上游处理 ${formatLatency(Math.round(processingMs as number))}`)
|
||||
}
|
||||
|
||||
// 计算 Aether→代理 之间无法解释的耗时差
|
||||
if (proxy.ttfb_ms != null && t.total_ms != null) {
|
||||
const gap = proxy.ttfb_ms - t.total_ms
|
||||
const gap = (proxy.ttfb_ms as number) - (t.total_ms as number)
|
||||
if (gap > 500) {
|
||||
parts.push(`传输 ${formatLatency(Math.round(gap))}`)
|
||||
}
|
||||
@@ -817,9 +820,9 @@ const loadTrace = async (silent = false) => {
|
||||
|
||||
try {
|
||||
trace.value = await requestTraceApi.getRequestTrace(props.requestId)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!silent) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
error.value = parseApiError(err, '加载失败')
|
||||
}
|
||||
log.error('加载请求追踪失败:', err)
|
||||
} finally {
|
||||
|
||||
@@ -699,6 +699,7 @@ const autoRefreshing = ref(false)
|
||||
const curlCopying = ref(false)
|
||||
const curlCopied = ref(false)
|
||||
const replayDialogOpen = ref(false)
|
||||
const AUTO_REFRESH_INTERVAL_MS = 1000
|
||||
|
||||
// 监听标签页切换
|
||||
watch(activeTab, (newTab) => {
|
||||
@@ -1010,15 +1011,15 @@ const visibleTabs = computed(() => {
|
||||
return tabs.filter(tab => {
|
||||
switch (tab.name) {
|
||||
case 'request-headers':
|
||||
return hasContent(detail.value!.request_headers)
|
||||
return hasContent(detail.value?.request_headers) || hasContent(detail.value?.provider_request_headers)
|
||||
case 'request-body':
|
||||
return hasContent(detail.value!.request_body) || hasContent(detail.value!.provider_request_body)
|
||||
return hasContent(detail.value?.request_body) || hasContent(detail.value?.provider_request_body)
|
||||
case 'response-headers':
|
||||
return hasContent(detail.value!.response_headers) || hasContent(detail.value!.client_response_headers)
|
||||
return hasContent(detail.value?.response_headers) || hasContent(detail.value?.client_response_headers)
|
||||
case 'response-body':
|
||||
return hasContent(detail.value!.response_body) || hasContent(detail.value!.client_response_body)
|
||||
return hasContent(detail.value?.response_body) || hasContent(detail.value?.client_response_body)
|
||||
case 'metadata':
|
||||
return hasContent(detail.value!.metadata)
|
||||
return hasContent(detail.value?.metadata)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1078,6 +1079,15 @@ async function loadDetail(id: string, silent = false) {
|
||||
if (silent) {
|
||||
timelineRef.value?.refresh()
|
||||
}
|
||||
|
||||
// 抽屉打开时,对进行中请求自动保持刷新,保证详情实时更新
|
||||
if (props.isOpen) {
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
} else {
|
||||
startAutoRefresh()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to load request detail:', err)
|
||||
if (!silent) {
|
||||
@@ -1108,6 +1118,23 @@ function stopAutoRefresh() {
|
||||
autoRefreshing.value = false
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshTimer.value || !props.requestId || !props.isOpen) {
|
||||
return
|
||||
}
|
||||
autoRefreshing.value = true
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, AUTO_REFRESH_INTERVAL_MS)
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (!props.requestId) return
|
||||
|
||||
@@ -1132,16 +1159,7 @@ async function refreshDetail() {
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, 1000)
|
||||
startAutoRefresh()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -1300,7 +1318,7 @@ function copyContent(tabName: string) {
|
||||
}
|
||||
} else {
|
||||
// JSON 视图模式:复制原始 JSON
|
||||
let data: any = null
|
||||
let data: unknown = null
|
||||
switch (tabName) {
|
||||
case 'request-headers':
|
||||
data = dataSource.value === 'provider'
|
||||
@@ -1380,8 +1398,8 @@ function openReplayDialog() {
|
||||
interface HeaderEntry {
|
||||
key: string
|
||||
status: 'added' | 'modified' | 'removed' | 'unchanged'
|
||||
originalValue?: any
|
||||
newValue?: any
|
||||
originalValue?: unknown
|
||||
newValue?: unknown
|
||||
}
|
||||
|
||||
const mergedHeaderEntries = computed(() => {
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
</Card>
|
||||
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
||||
<Card
|
||||
v-else-if="data.raw_response && data.metadata?.parse_error"
|
||||
v-else-if="hasParseError"
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
||||
<span class="text-xs text-amber-700 dark:text-amber-300">{{ data.metadata.parse_error }}</span>
|
||||
<span class="text-xs text-amber-700 dark:text-amber-300">{{ parseErrorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ rawResponseContent }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -74,12 +74,14 @@
|
||||
:style="{ width: `${line.indent * 16}px` }"
|
||||
/>
|
||||
<!-- 内容 -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="line-content"
|
||||
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
||||
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
||||
v-html="getDisplayHtml(line)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,14 +114,47 @@ interface DisplayLine extends JsonLine {
|
||||
displayLineNumber: number
|
||||
}
|
||||
|
||||
/** JSON data can be any serializable value: object, array, string, number, boolean, null */
|
||||
type JsonValue = Record<string, unknown> | unknown[] | string | number | boolean | null | undefined
|
||||
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
data: JsonValue
|
||||
viewMode: 'formatted' | 'raw' | 'compare'
|
||||
expandDepth: number
|
||||
isDark: boolean
|
||||
emptyMessage: string
|
||||
}>()
|
||||
|
||||
/** Safely cast data to an object for property access in templates */
|
||||
const dataAsObject = computed(() => {
|
||||
if (props.data && typeof props.data === 'object' && !Array.isArray(props.data)) {
|
||||
return props.data as Record<string, unknown>
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
/** Whether the data contains a raw_response with a parse error (non-JSON response) */
|
||||
const hasParseError = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
if (!obj) return false
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return Boolean(obj.raw_response && metadata?.parse_error)
|
||||
})
|
||||
|
||||
/** Parse error message */
|
||||
const parseErrorMessage = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
if (!obj) return ''
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return String(metadata?.parse_error || '')
|
||||
})
|
||||
|
||||
/** Raw response content */
|
||||
const rawResponseContent = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
return obj ? String(obj.raw_response || '') : ''
|
||||
})
|
||||
|
||||
const collapsedBlocks = ref<Set<string>>(new Set())
|
||||
const lines = ref<JsonLine[]>([])
|
||||
|
||||
@@ -145,14 +180,14 @@ const escapeHtml = (str: string): string => {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
const parseJsonToLines = (data: unknown): JsonLine[] => {
|
||||
const result: JsonLine[] = []
|
||||
let lineNumber = 1
|
||||
let blockIdCounter = 0
|
||||
|
||||
const getBlockId = () => `block-${blockIdCounter++}`
|
||||
|
||||
const processValue = (value: any, indent: number, isLast: boolean, keyPrefix: string = ''): void => {
|
||||
const processValue = (value: unknown, indent: number, isLast: boolean, keyPrefix: string = ''): void => {
|
||||
const comma = isLast ? '' : ','
|
||||
|
||||
if (value === null) {
|
||||
@@ -232,7 +267,8 @@ const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
result[startLine].blockEnd = result.length - 1
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
const keys = Object.keys(value)
|
||||
const obj = value as Record<string, unknown>
|
||||
const keys = Object.keys(obj)
|
||||
if (keys.length === 0) {
|
||||
result.push({
|
||||
id: result.length,
|
||||
@@ -259,7 +295,7 @@ const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
const keyHtml = getTokenHtml(`"${escapeHtml(key)}"`, 'key') + getTokenHtml(': ', 'punctuation')
|
||||
processValue(value[key], indent + 1, i === keys.length - 1, keyHtml)
|
||||
processValue(obj[key], indent + 1, i === keys.length - 1, keyHtml)
|
||||
})
|
||||
|
||||
result.push({
|
||||
|
||||
@@ -165,11 +165,11 @@ const props = defineProps<{
|
||||
detail: RequestDetail
|
||||
viewMode: 'compare' | 'formatted' | 'raw'
|
||||
dataSource: 'client' | 'provider'
|
||||
currentHeaderData: any
|
||||
currentHeaderData: Record<string, unknown> | null
|
||||
currentExpandDepth: number
|
||||
hasProviderHeaders: boolean
|
||||
clientHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
providerHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
clientHeadersWithDiff: Array<{ key: string; value: unknown; status: string }>
|
||||
providerHeadersWithDiff: Array<{ key: string; value: unknown; status: string }>
|
||||
headerStats: { added: number; modified: number; removed: number; unchanged: number }
|
||||
isDark: boolean
|
||||
}>()
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from '../types'
|
||||
import { createDefaultStats } from '../types'
|
||||
import { log } from '@/utils/logger'
|
||||
import { getErrorStatus } from '@/types/api-error'
|
||||
|
||||
export interface UseUsageDataOptions {
|
||||
isAdminPage: Ref<boolean>
|
||||
@@ -81,27 +82,31 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
usageApi.getUsageByApiFormat(dateRange)
|
||||
])
|
||||
|
||||
// statsData may contain additional fields not declared in UsageStats
|
||||
const statsRaw = statsData as Record<string, unknown>
|
||||
stats.value = {
|
||||
total_requests: statsData.total_requests || 0,
|
||||
total_tokens: statsData.total_tokens || 0,
|
||||
total_cost: statsData.total_cost || 0,
|
||||
total_actual_cost: (statsData as any).total_actual_cost,
|
||||
total_actual_cost: statsData.total_actual_cost,
|
||||
avg_response_time: statsData.avg_response_time || 0,
|
||||
error_count: (statsData as any).error_count,
|
||||
error_rate: (statsData as any).error_rate,
|
||||
cache_stats: (statsData as any).cache_stats,
|
||||
error_count: typeof statsRaw.error_count === 'number' ? statsRaw.error_count : undefined,
|
||||
error_rate: typeof statsRaw.error_rate === 'number' ? statsRaw.error_rate : undefined,
|
||||
cache_stats: statsRaw.cache_stats as UsageStatsState['cache_stats'],
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
activity_heatmap: null
|
||||
}
|
||||
|
||||
modelStats.value = modelData.map(item => ({
|
||||
model: item.model,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: (item as any).actual_cost
|
||||
}))
|
||||
modelStats.value = modelData.map(item => {
|
||||
const raw = item as Record<string, unknown>
|
||||
return {
|
||||
model: item.model,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: typeof raw.actual_cost === 'number' ? raw.actual_cost : undefined
|
||||
}
|
||||
})
|
||||
|
||||
providerStats.value = providerData.map(item => ({
|
||||
provider: item.provider,
|
||||
@@ -142,10 +147,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
avg_response_time: userData.avg_response_time || 0,
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
activity_heatmap: null
|
||||
}
|
||||
|
||||
modelStats.value = (userData.summary_by_model || []).map((item: any) => ({
|
||||
modelStats.value = (userData.summary_by_model || []).map((item) => ({
|
||||
model: item.model,
|
||||
request_count: item.requests || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
@@ -153,13 +157,14 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
actual_cost: item.actual_total_cost_usd
|
||||
}))
|
||||
|
||||
providerStats.value = (userData.summary_by_provider || []).map((item: any) => ({
|
||||
providerStats.value = (userData.summary_by_provider || []).map((item) => ({
|
||||
provider: item.provider,
|
||||
requests: item.requests || 0,
|
||||
totalTokens: 0,
|
||||
totalCost: item.total_cost_usd || 0,
|
||||
successRate: item.success_rate || 0,
|
||||
avgResponseTime: item.avg_response_time_ms > 0
|
||||
? `${(item.avg_response_time_ms / 1000).toFixed(2)}s`
|
||||
avgResponseTime: (item.avg_response_time_ms ?? 0) > 0
|
||||
? `${((item.avg_response_time_ms ?? 0) / 1000).toFixed(2)}s`
|
||||
: '-'
|
||||
}))
|
||||
|
||||
@@ -221,8 +226,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
})
|
||||
.sort((a, b) => b.request_count - a.request_count)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response?.status !== 403) {
|
||||
} catch (error: unknown) {
|
||||
if (getErrorStatus(error) !== 403) {
|
||||
log.error('加载统计数据失败:', error)
|
||||
}
|
||||
stats.value = createDefaultStats()
|
||||
@@ -244,7 +249,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
const offset = (pagination.page - 1) * pagination.pageSize
|
||||
|
||||
// 构建请求参数
|
||||
const params: any = {
|
||||
const params: Record<string, unknown> = {
|
||||
limit: pagination.pageSize,
|
||||
offset,
|
||||
...currentDateRange.value
|
||||
|
||||
@@ -33,6 +33,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Claude API 格式解析器
|
||||
*/
|
||||
@@ -43,7 +46,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 Claude 格式
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -52,37 +55,41 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('openai') || lowerHint.includes('gemini')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('claude')) return 95
|
||||
|
||||
// 3. 检查请求体结构
|
||||
if (!requestBody?.messages || !Array.isArray(requestBody.messages)) {
|
||||
if (!req?.messages || !Array.isArray(req.messages)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody) {
|
||||
// Claude 响应特征
|
||||
const respType = typeof respBody.type === 'string' ? respBody.type : ''
|
||||
if (
|
||||
respBody.type === 'message' ||
|
||||
respBody.type?.startsWith('content_block') ||
|
||||
respBody.type?.startsWith('message_')
|
||||
respType === 'message' ||
|
||||
respType.startsWith('content_block') ||
|
||||
respType.startsWith('message_')
|
||||
) {
|
||||
return 90
|
||||
}
|
||||
// 明确是 OpenAI 格式
|
||||
if (respBody.choices || respBody.object?.includes('chat.completion')) {
|
||||
const respObject = typeof respBody.object === 'string' ? respBody.object : ''
|
||||
if (respBody.choices || respObject.includes('chat.completion')) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查 Claude 特有的请求字段
|
||||
if (requestBody.system !== undefined) {
|
||||
if (req.system !== undefined) {
|
||||
// system 可以是字符串或数组,这是 Claude 的特征
|
||||
return 70
|
||||
}
|
||||
@@ -94,26 +101,27 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('claude', '无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
isStream: body.stream === true,
|
||||
apiFormat: 'claude',
|
||||
model: requestBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// 提取 system prompt
|
||||
result.system = this.extractSystemPrompt(requestBody.system)
|
||||
result.system = this.extractSystemPrompt(body.system)
|
||||
|
||||
// 提取 messages
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
const parsedMsg = this.parseMessage(msg)
|
||||
if (Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
const parsedMsg = this.parseMessage(msg as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
@@ -129,22 +137,23 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('claude', '无响应体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'claude',
|
||||
model: responseBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// Claude 响应格式: { type: "message", content: [...] }
|
||||
if (Array.isArray(responseBody.content)) {
|
||||
const contentBlocks = this.parseContentBlocks(responseBody.content, 'assistant')
|
||||
if (Array.isArray(body.content)) {
|
||||
const contentBlocks = this.parseContentBlocks(body.content as RawObject[], 'assistant')
|
||||
if (contentBlocks.length > 0) {
|
||||
result.messages.push(createMessage('assistant', contentBlocks))
|
||||
}
|
||||
@@ -159,7 +168,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('claude', '无响应数据')
|
||||
}
|
||||
@@ -175,47 +184,49 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
const blocks = new Map<number, {
|
||||
type: ContentBlock['type']
|
||||
parts: string[]
|
||||
metadata?: any
|
||||
metadata?: Record<string, string>
|
||||
}>()
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
// 提取模型名
|
||||
if (chunk.message?.model && !result.model) {
|
||||
result.model = chunk.message.model
|
||||
const chunkMessage = chunk.message as RawObject | undefined
|
||||
if (typeof chunkMessage?.model === 'string' && !result.model) {
|
||||
result.model = chunkMessage.model
|
||||
}
|
||||
|
||||
if (chunk.type === 'content_block_start') {
|
||||
const index = chunk.index ?? 0
|
||||
const block = chunk.content_block
|
||||
const index = (typeof chunk.index === 'number' ? chunk.index : 0)
|
||||
const block = chunk.content_block as RawObject | undefined
|
||||
if (block?.type === 'text') {
|
||||
blocks.set(index, { type: 'text', parts: [block.text || ''] })
|
||||
blocks.set(index, { type: 'text', parts: [String(block.text || '')] })
|
||||
} else if (block?.type === 'thinking') {
|
||||
blocks.set(index, {
|
||||
type: 'thinking',
|
||||
parts: [block.thinking || ''],
|
||||
metadata: { signature: block.signature },
|
||||
parts: [String(block.thinking || '')],
|
||||
metadata: { signature: String(block.signature || '') },
|
||||
})
|
||||
} else if (block?.type === 'tool_use') {
|
||||
blocks.set(index, {
|
||||
type: 'tool_use',
|
||||
parts: [],
|
||||
metadata: { toolName: block.name, toolId: block.id },
|
||||
metadata: { toolName: String(block.name || ''), toolId: String(block.id || '') },
|
||||
})
|
||||
}
|
||||
} else if (chunk.type === 'content_block_delta') {
|
||||
const index = chunk.index ?? 0
|
||||
const delta = chunk.delta
|
||||
const index = (typeof chunk.index === 'number' ? chunk.index : 0)
|
||||
const delta = chunk.delta as RawObject | undefined
|
||||
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') {
|
||||
if (block && delta) {
|
||||
if (delta.type === 'text_delta') {
|
||||
block.parts.push(String(delta.text || ''))
|
||||
} else if (delta.type === 'thinking_delta') {
|
||||
block.parts.push(String(delta.thinking || ''))
|
||||
} else if (delta.type === 'input_json_delta') {
|
||||
block.parts.push(String(delta.partial_json || ''))
|
||||
} else if (delta.type === 'signature_delta') {
|
||||
block.metadata = block.metadata || {}
|
||||
block.metadata.signature = (block.metadata.signature || '') + (delta.signature || '')
|
||||
block.metadata.signature = (block.metadata.signature || '') + String(delta.signature || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +269,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 提取 system prompt
|
||||
*/
|
||||
private extractSystemPrompt(system: any): string | undefined {
|
||||
private extractSystemPrompt(system: unknown): string | undefined {
|
||||
if (!system) return undefined
|
||||
|
||||
if (typeof system === 'string') {
|
||||
@@ -267,8 +278,8 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
|
||||
if (Array.isArray(system)) {
|
||||
return system
|
||||
.filter((b: any) => b.type === 'text')
|
||||
.map((b: any) => b.text)
|
||||
.filter((b: RawObject) => b.type === 'text')
|
||||
.map((b: RawObject) => String(b.text || ''))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
@@ -278,7 +289,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单条消息
|
||||
*/
|
||||
private parseMessage(msg: any): ParsedMessage | null {
|
||||
private parseMessage(msg: RawObject): ParsedMessage | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = msg.role as MessageRole
|
||||
@@ -292,13 +303,13 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析消息内容
|
||||
*/
|
||||
private parseMessageContent(content: any, role: MessageRole): ContentBlock[] {
|
||||
private parseMessageContent(content: unknown, role: MessageRole): ContentBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return [createTextBlock(content)]
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return this.parseContentBlocks(content, role)
|
||||
return this.parseContentBlocks(content as RawObject[], role)
|
||||
}
|
||||
|
||||
return []
|
||||
@@ -307,7 +318,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析内容块数组
|
||||
*/
|
||||
private parseContentBlocks(blocks: any[], role: MessageRole): ContentBlock[] {
|
||||
private parseContentBlocks(blocks: RawObject[], role: MessageRole): ContentBlock[] {
|
||||
const result: ContentBlock[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
@@ -323,28 +334,31 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单个内容块
|
||||
*/
|
||||
private parseContentBlock(block: any, _role: MessageRole): ContentBlock | null {
|
||||
private parseContentBlock(block: RawObject, _role: MessageRole): ContentBlock | null {
|
||||
if (!block || !block.type) return null
|
||||
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return createTextBlock(block.text || '')
|
||||
return createTextBlock(String(block.text || ''))
|
||||
|
||||
case 'thinking':
|
||||
return createThinkingBlock(block.thinking || '', block.signature)
|
||||
return createThinkingBlock(
|
||||
String(block.thinking || ''),
|
||||
typeof block.signature === 'string' ? block.signature : undefined
|
||||
)
|
||||
|
||||
case 'tool_use':
|
||||
return createToolUseBlock(
|
||||
block.id || '',
|
||||
block.name || '',
|
||||
block.input || {}
|
||||
String(block.id || ''),
|
||||
String(block.name || ''),
|
||||
(block.input as Record<string, unknown>) || {}
|
||||
)
|
||||
|
||||
case 'tool_result':
|
||||
return createToolResultBlock(
|
||||
block.tool_use_id || '',
|
||||
String(block.tool_use_id || ''),
|
||||
this.parseToolResultContent(block.content),
|
||||
block.is_error
|
||||
block.is_error as boolean | undefined
|
||||
)
|
||||
|
||||
case 'image':
|
||||
@@ -358,23 +372,23 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析图片块
|
||||
*/
|
||||
private parseImageBlock(block: any): ContentBlock | null {
|
||||
const source = block.source
|
||||
private parseImageBlock(block: RawObject): ContentBlock | null {
|
||||
const source = block.source as RawObject | undefined
|
||||
if (!source) {
|
||||
return createImageBlock('base64', { alt: '[图片]' })
|
||||
}
|
||||
|
||||
if (source.type === 'base64') {
|
||||
return createImageBlock('base64', {
|
||||
data: source.data,
|
||||
mimeType: source.media_type,
|
||||
data: typeof source.data === 'string' ? source.data : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (source.type === 'url') {
|
||||
return createImageBlock('url', {
|
||||
url: source.url,
|
||||
mimeType: source.media_type,
|
||||
url: typeof source.url === 'string' ? source.url : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,16 +398,17 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析工具结果内容
|
||||
*/
|
||||
private parseToolResultContent(content: any): string | ContentBlock[] {
|
||||
private parseToolResultContent(content: unknown): string | ContentBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const blocks: ContentBlock[] = []
|
||||
for (const item of content) {
|
||||
for (const rawItem of content) {
|
||||
const item = rawItem as RawObject
|
||||
if (item.type === 'text') {
|
||||
blocks.push(createTextBlock(item.text || ''))
|
||||
blocks.push(createTextBlock(String(item.text || '')))
|
||||
} else if (item.type === 'image') {
|
||||
const imgBlock = this.parseImageBlock(item)
|
||||
if (imgBlock) blocks.push(imgBlock)
|
||||
@@ -412,17 +427,18 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
const isStream = body.stream === true
|
||||
|
||||
// 渲染 system prompt
|
||||
const system = this.extractSystemPrompt(requestBody.system)
|
||||
const system = this.extractSystemPrompt(body.system)
|
||||
if (system) {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
createTextRenderBlock(system),
|
||||
@@ -430,9 +446,9 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 渲染 messages
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
const msgBlock = this.renderMessage(msg)
|
||||
if (Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
const msgBlock = this.renderMessage(msg as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
@@ -448,7 +464,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -459,13 +475,15 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// Claude 响应格式: { type: "message", content: [...] }
|
||||
if (Array.isArray(responseBody.content)) {
|
||||
const contentBlocks = this.renderContentBlocks(responseBody.content)
|
||||
if (Array.isArray(body.content)) {
|
||||
const rawContent = body.content as RawObject[]
|
||||
const contentBlocks = this.renderContentBlocks(rawContent)
|
||||
if (contentBlocks.length > 0) {
|
||||
const badges = this.getBadgesForContent(responseBody.content)
|
||||
const badges = this.getBadgesForContent(rawContent)
|
||||
blocks.push(createMessageBlock('assistant', contentBlocks, {
|
||||
roleLabel: 'Assistant',
|
||||
badges,
|
||||
@@ -482,7 +500,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -517,7 +535,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单条消息
|
||||
*/
|
||||
private renderMessage(msg: any): RenderBlock | null {
|
||||
private renderMessage(msg: RawObject): RenderBlock | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = msg.role as MessageRole
|
||||
@@ -536,13 +554,13 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染消息内容
|
||||
*/
|
||||
private renderMessageContent(content: any): RenderBlock[] {
|
||||
private renderMessageContent(content: unknown): RenderBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return [createTextRenderBlock(content)]
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return this.renderContentBlocks(content)
|
||||
return this.renderContentBlocks(content as RawObject[])
|
||||
}
|
||||
|
||||
return []
|
||||
@@ -551,7 +569,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染原始内容块数组
|
||||
*/
|
||||
private renderContentBlocks(blocks: any[]): RenderBlock[] {
|
||||
private renderContentBlocks(blocks: RawObject[]): RenderBlock[] {
|
||||
const result: RenderBlock[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
@@ -567,30 +585,30 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单个原始内容块
|
||||
*/
|
||||
private renderContentBlock(block: any): RenderBlock | null {
|
||||
private renderContentBlock(block: RawObject): RenderBlock | null {
|
||||
if (!block || !block.type) return null
|
||||
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return createTextRenderBlock(block.text || '')
|
||||
return createTextRenderBlock(String(block.text || ''))
|
||||
|
||||
case 'thinking':
|
||||
return createCollapsibleBlock(
|
||||
`思考过程 (${(block.thinking || '').length} 字符)`,
|
||||
[createCodeBlock(block.thinking || '')],
|
||||
`思考过程 (${String(block.thinking || '').length} 字符)`,
|
||||
[createCodeBlock(String(block.thinking || ''))],
|
||||
{ defaultOpen: false, className: 'thinking-block' }
|
||||
)
|
||||
|
||||
case 'tool_use':
|
||||
return createToolUseRenderBlock(
|
||||
block.name || '工具调用',
|
||||
String(block.name || '工具调用'),
|
||||
this.formatJson(block.input),
|
||||
block.id
|
||||
typeof block.id === 'string' ? block.id : undefined
|
||||
)
|
||||
|
||||
case 'tool_result': {
|
||||
const content = this.formatToolResultContent(block.content)
|
||||
return createToolResultRenderBlock(content, block.is_error)
|
||||
return createToolResultRenderBlock(content, block.is_error as boolean | undefined)
|
||||
}
|
||||
|
||||
case 'image':
|
||||
@@ -666,8 +684,8 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染图片块
|
||||
*/
|
||||
private renderImageBlock(block: any): RenderBlock | null {
|
||||
const source = block.source
|
||||
private renderImageBlock(block: RawObject): RenderBlock | null {
|
||||
const source = block.source as RawObject | undefined
|
||||
if (!source) {
|
||||
return createImageRenderBlock({ alt: '[图片]' })
|
||||
}
|
||||
@@ -675,14 +693,14 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
if (source.type === 'base64') {
|
||||
return createImageRenderBlock({
|
||||
src: `data:${source.media_type || 'image/png'};base64,${source.data}`,
|
||||
mimeType: source.media_type,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (source.type === 'url') {
|
||||
return createImageRenderBlock({
|
||||
src: source.url,
|
||||
mimeType: source.media_type,
|
||||
src: typeof source.url === 'string' ? source.url : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -705,11 +723,11 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取原始内容的徽章
|
||||
*/
|
||||
private getBadgesForRawContent(content: any): BadgeRenderBlock[] {
|
||||
private getBadgesForRawContent(content: unknown): BadgeRenderBlock[] {
|
||||
if (!Array.isArray(content)) return []
|
||||
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
const types = new Set(content.map((b: any) => b.type))
|
||||
const types = new Set(content.map((b: RawObject) => b.type))
|
||||
|
||||
if (types.has('thinking')) {
|
||||
badges.push(createBadgeBlock('思考', 'secondary'))
|
||||
@@ -730,7 +748,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取内容的徽章
|
||||
*/
|
||||
private getBadgesForContent(content: any[]): BadgeRenderBlock[] {
|
||||
private getBadgesForContent(content: RawObject[]): BadgeRenderBlock[] {
|
||||
return this.getBadgesForRawContent(content)
|
||||
}
|
||||
|
||||
@@ -760,10 +778,10 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
@@ -775,15 +793,15 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化工具结果内容
|
||||
*/
|
||||
private formatToolResultContent(content: any): string {
|
||||
private formatToolResultContent(content: unknown): string {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((item: any) => {
|
||||
if (item.type === 'text') return item.text
|
||||
.map((item: RawObject) => {
|
||||
if (item.type === 'text') return String(item.text || '')
|
||||
if (item.type === 'image') return '[图片]'
|
||||
return ''
|
||||
})
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Gemini API 格式解析器
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 Gemini 格式
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -47,19 +50,21 @@ export class GeminiParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('claude') || lowerHint.includes('openai')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('gemini')) return 95
|
||||
|
||||
// 3. Gemini 特有结构: 使用 contents 而非 messages
|
||||
if (requestBody?.contents && Array.isArray(requestBody.contents)) {
|
||||
if (req?.contents && Array.isArray(req.contents)) {
|
||||
return 90
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody?.candidates) {
|
||||
return 85
|
||||
@@ -71,32 +76,33 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('gemini', '无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'gemini',
|
||||
model: requestBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// 提取 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)
|
||||
const sysInst = (body.system_instruction || body.systemInstruction) as RawObject | undefined
|
||||
if (sysInst?.parts && Array.isArray(sysInst.parts)) {
|
||||
result.system = (sysInst.parts as RawObject[])
|
||||
.filter((p: RawObject) => typeof p.text === 'string')
|
||||
.map((p: RawObject) => String(p.text))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
// 提取 contents
|
||||
if (Array.isArray(requestBody.contents)) {
|
||||
for (const content of requestBody.contents) {
|
||||
const parsedMsg = this.parseContent(content)
|
||||
if (Array.isArray(body.contents)) {
|
||||
for (const content of body.contents) {
|
||||
const parsedMsg = this.parseContent(content as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
@@ -112,12 +118,13 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('gemini', '无响应体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
@@ -125,9 +132,11 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidate = responseBody.candidates?.[0]
|
||||
if (candidate?.content?.parts) {
|
||||
const contentBlocks = this.parseParts(candidate.content.parts)
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const contentBlocks = this.parseParts(candidateContent.parts as RawObject[])
|
||||
if (contentBlocks.length > 0) {
|
||||
result.messages.push(createMessage('assistant', contentBlocks))
|
||||
}
|
||||
@@ -142,7 +151,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('gemini', '无响应数据')
|
||||
}
|
||||
@@ -155,16 +164,24 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
const textParts: string[] = []
|
||||
const toolCalls: { name: string; args: any }[] = []
|
||||
const toolCalls: { name: string; args: Record<string, unknown> }[] = []
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const parts = chunk.candidates?.[0]?.content?.parts
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
const candidates = chunk.candidates as RawObject[] | undefined
|
||||
const firstCandidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = firstCandidate?.content as RawObject | undefined
|
||||
const parts = candidateContent?.parts as RawObject[] | undefined
|
||||
if (parts) {
|
||||
for (const part of parts) {
|
||||
if (part.text) {
|
||||
if (typeof part.text === 'string') {
|
||||
textParts.push(part.text)
|
||||
} else if (part.functionCall) {
|
||||
toolCalls.push(part.functionCall)
|
||||
const fc = part.functionCall as RawObject
|
||||
toolCalls.push({
|
||||
name: String(fc.name || ''),
|
||||
args: (fc.args as Record<string, unknown>) || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,11 +216,12 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 content 对象
|
||||
*/
|
||||
private parseContent(content: any): ParsedMessage | null {
|
||||
private parseContent(content: RawObject): ParsedMessage | null {
|
||||
if (!content) return null
|
||||
|
||||
const role = this.mapRole(content.role)
|
||||
const contentBlocks = this.parseParts(content.parts || [])
|
||||
const role = this.mapRole(typeof content.role === 'string' ? content.role : undefined)
|
||||
const parts = Array.isArray(content.parts) ? content.parts as RawObject[] : []
|
||||
const contentBlocks = this.parseParts(parts)
|
||||
|
||||
if (contentBlocks.length === 0) return null
|
||||
|
||||
@@ -213,7 +231,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 parts 数组
|
||||
*/
|
||||
private parseParts(parts: any[]): ContentBlock[] {
|
||||
private parseParts(parts: RawObject[]): ContentBlock[] {
|
||||
const result: ContentBlock[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
@@ -229,36 +247,39 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单个 part
|
||||
*/
|
||||
private parsePart(part: any): ContentBlock | null {
|
||||
private parsePart(part: RawObject): ContentBlock | null {
|
||||
if (!part) return null
|
||||
|
||||
// 文本
|
||||
if (part.text !== undefined) {
|
||||
return createTextBlock(part.text)
|
||||
return createTextBlock(String(part.text))
|
||||
}
|
||||
|
||||
// 内联数据(图片等)
|
||||
if (part.inlineData) {
|
||||
const inlineData = part.inlineData as RawObject
|
||||
return createImageBlock('base64', {
|
||||
data: part.inlineData.data,
|
||||
mimeType: part.inlineData.mimeType,
|
||||
data: typeof inlineData.data === 'string' ? inlineData.data : undefined,
|
||||
mimeType: typeof inlineData.mimeType === 'string' ? inlineData.mimeType : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 函数调用
|
||||
if (part.functionCall) {
|
||||
const fc = part.functionCall as RawObject
|
||||
return createToolUseBlock(
|
||||
'',
|
||||
part.functionCall.name || '',
|
||||
part.functionCall.args || {}
|
||||
String(fc.name || ''),
|
||||
(fc.args as Record<string, unknown>) || {}
|
||||
)
|
||||
}
|
||||
|
||||
// 函数响应
|
||||
if (part.functionResponse) {
|
||||
const fr = part.functionResponse as RawObject
|
||||
return createToolResultBlock(
|
||||
'', // Gemini 用 name 关联
|
||||
JSON.stringify(part.functionResponse.response, null, 2)
|
||||
JSON.stringify(fr.response, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,20 +307,21 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
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)
|
||||
const sysInst = (body.system_instruction || body.systemInstruction) as RawObject | undefined
|
||||
if (sysInst?.parts && Array.isArray(sysInst.parts)) {
|
||||
const systemText = (sysInst.parts as RawObject[])
|
||||
.filter((p: RawObject) => typeof p.text === 'string')
|
||||
.map((p: RawObject) => String(p.text))
|
||||
.join('\n')
|
||||
if (systemText) {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
@@ -309,9 +331,9 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 渲染 contents
|
||||
if (Array.isArray(requestBody.contents)) {
|
||||
for (const content of requestBody.contents) {
|
||||
const msgBlock = this.renderContent(content)
|
||||
if (Array.isArray(body.contents)) {
|
||||
for (const content of body.contents) {
|
||||
const msgBlock = this.renderContent(content as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
@@ -327,7 +349,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -338,14 +360,18 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidate = responseBody.candidates?.[0]
|
||||
if (candidate?.content?.parts) {
|
||||
const contentBlocks = this.renderParts(candidate.content.parts)
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const parts = candidateContent.parts as RawObject[]
|
||||
const contentBlocks = this.renderParts(parts)
|
||||
if (contentBlocks.length > 0) {
|
||||
const badges = this.getBadgesForParts(candidate.content.parts)
|
||||
const badges = this.getBadgesForParts(parts)
|
||||
blocks.push(createMessageBlock('assistant', contentBlocks, {
|
||||
roleLabel: 'Assistant',
|
||||
badges: badges.length > 0 ? badges : undefined,
|
||||
@@ -362,7 +388,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -397,15 +423,16 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 content 对象
|
||||
*/
|
||||
private renderContent(content: any): RenderBlock | null {
|
||||
private renderContent(content: RawObject): RenderBlock | null {
|
||||
if (!content) return null
|
||||
|
||||
const role = this.mapRole(content.role)
|
||||
const contentBlocks = this.renderParts(content.parts || [])
|
||||
const role = this.mapRole(typeof content.role === 'string' ? content.role : undefined)
|
||||
const parts = Array.isArray(content.parts) ? content.parts as RawObject[] : []
|
||||
const contentBlocks = this.renderParts(parts)
|
||||
|
||||
if (contentBlocks.length === 0) return null
|
||||
|
||||
const badges = this.getBadgesForParts(content.parts || [])
|
||||
const badges = this.getBadgesForParts(parts)
|
||||
|
||||
return createMessageBlock(role, contentBlocks, {
|
||||
roleLabel: this.getRoleLabel(role),
|
||||
@@ -416,7 +443,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 parts 数组
|
||||
*/
|
||||
private renderParts(parts: any[]): RenderBlock[] {
|
||||
private renderParts(parts: RawObject[]): RenderBlock[] {
|
||||
const result: RenderBlock[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
@@ -432,34 +459,37 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单个 part
|
||||
*/
|
||||
private renderPart(part: any): RenderBlock | null {
|
||||
private renderPart(part: RawObject): RenderBlock | null {
|
||||
if (!part) return null
|
||||
|
||||
// 文本
|
||||
if (part.text !== undefined) {
|
||||
return createTextRenderBlock(part.text)
|
||||
return createTextRenderBlock(String(part.text))
|
||||
}
|
||||
|
||||
// 内联数据(图片等)
|
||||
if (part.inlineData) {
|
||||
const inlineData = part.inlineData as RawObject
|
||||
return createImageRenderBlock({
|
||||
src: `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`,
|
||||
mimeType: part.inlineData.mimeType,
|
||||
src: `data:${inlineData.mimeType || 'image/png'};base64,${inlineData.data}`,
|
||||
mimeType: typeof inlineData.mimeType === 'string' ? inlineData.mimeType : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 函数调用
|
||||
if (part.functionCall) {
|
||||
const fc = part.functionCall as RawObject
|
||||
return createToolUseRenderBlock(
|
||||
part.functionCall.name || '函数调用',
|
||||
this.formatJson(part.functionCall.args)
|
||||
String(fc.name || '函数调用'),
|
||||
this.formatJson(fc.args)
|
||||
)
|
||||
}
|
||||
|
||||
// 函数响应
|
||||
if (part.functionResponse) {
|
||||
const fr = part.functionResponse as RawObject
|
||||
return createToolResultRenderBlock(
|
||||
this.formatJson(part.functionResponse.response)
|
||||
this.formatJson(fr.response)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -533,11 +563,11 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取 parts 的徽章
|
||||
*/
|
||||
private getBadgesForParts(parts: any[]): BadgeRenderBlock[] {
|
||||
private getBadgesForParts(parts: RawObject[]): 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)
|
||||
const hasImage = parts.some((p: RawObject) => p.inlineData)
|
||||
const hasToolCall = parts.some((p: RawObject) => p.functionCall)
|
||||
const hasToolResult = parts.some((p: RawObject) => p.functionResponse)
|
||||
|
||||
if (hasToolCall) {
|
||||
badges.push(createBadgeBlock('函数调用', 'outline'))
|
||||
@@ -575,10 +605,10 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* OpenAI API 格式解析器
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 OpenAI 格式(包括 Chat Completions 和 CLI/Responses API)
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -47,24 +50,26 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('claude') || lowerHint.includes('gemini')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('gpt') || model.includes('o1') || model.includes('o3')) return 95
|
||||
|
||||
// 3. 检查请求体结构
|
||||
// OpenAI CLI (Responses API) 使用 input 字段
|
||||
const isCliFormat = requestBody?.input !== undefined || requestBody?.instructions !== undefined
|
||||
const isCliFormat = req?.input !== undefined || req?.instructions !== undefined
|
||||
// OpenAI Chat Completions 使用 messages 数组
|
||||
const isChatFormat = requestBody?.messages && Array.isArray(requestBody.messages)
|
||||
const isChatFormat = req?.messages && Array.isArray(req.messages)
|
||||
|
||||
if (!isCliFormat && !isChatFormat) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody) {
|
||||
// OpenAI CLI 响应特征: type 字段为 response.* 格式
|
||||
@@ -72,11 +77,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return 95
|
||||
}
|
||||
// OpenAI Chat Completions 响应特征: choices 数组
|
||||
if (respBody.choices || respBody.object?.includes('chat.completion')) {
|
||||
const respObject = typeof respBody.object === 'string' ? respBody.object : ''
|
||||
if (respBody.choices || respObject.includes('chat.completion')) {
|
||||
return 90
|
||||
}
|
||||
// 明确是 Claude 格式
|
||||
if (respBody.type === 'message' || respBody.type?.startsWith('content_block')) {
|
||||
const respType = typeof respBody.type === 'string' ? respBody.type : ''
|
||||
if (respType === 'message' || respType.startsWith('content_block')) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -87,8 +94,9 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// OpenAI 的 system 是在 messages 数组中作为 role: system
|
||||
const hasSystemInMessages = requestBody.messages?.some(
|
||||
(m: any) => m.role === 'system'
|
||||
const messages = req?.messages as RawObject[] | undefined
|
||||
const hasSystemInMessages = messages?.some(
|
||||
(m: RawObject) => m.role === 'system'
|
||||
)
|
||||
if (hasSystemInMessages) {
|
||||
return 60
|
||||
@@ -100,7 +108,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检查是否为 OpenAI CLI (Responses API) 的响应事件
|
||||
*/
|
||||
private isCliResponseEvent(chunk: any): boolean {
|
||||
private isCliResponseEvent(chunk: RawObject | null | undefined): boolean {
|
||||
const type = chunk?.type
|
||||
if (typeof type !== 'string') return false
|
||||
return type.startsWith('response.') || chunk?.object === 'response'
|
||||
@@ -109,35 +117,38 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('openai', '无请求体')
|
||||
}
|
||||
|
||||
const body = requestBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = requestBody.input !== undefined || requestBody.instructions !== undefined
|
||||
const isCliFormat = body.input !== undefined || body.instructions !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliRequest(requestBody)
|
||||
return this.parseCliRequest(body)
|
||||
}
|
||||
|
||||
return this.parseChatRequest(requestBody)
|
||||
return this.parseChatRequest(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 请求
|
||||
*/
|
||||
private parseChatRequest(requestBody: any): ParsedConversation {
|
||||
private parseChatRequest(requestBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
apiFormat: 'openai',
|
||||
model: requestBody.model,
|
||||
model: typeof requestBody.model === 'string' ? requestBody.model : undefined,
|
||||
}
|
||||
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
for (const rawMsg of requestBody.messages) {
|
||||
const msg = rawMsg as RawObject
|
||||
// OpenAI 的 system 消息在 messages 数组中
|
||||
if (msg.role === 'system') {
|
||||
const systemText = typeof msg.content === 'string'
|
||||
@@ -169,17 +180,17 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
* - 使用 input 字段(可以是字符串、消息数组或对象)
|
||||
* - 使用 instructions 字段作为系统指令
|
||||
*/
|
||||
private parseCliRequest(requestBody: any): ParsedConversation {
|
||||
private parseCliRequest(requestBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
apiFormat: 'openai',
|
||||
model: requestBody.model,
|
||||
model: typeof requestBody.model === 'string' ? requestBody.model : undefined,
|
||||
}
|
||||
|
||||
// 处理 instructions(系统指令)
|
||||
if (requestBody.instructions) {
|
||||
if (typeof requestBody.instructions === 'string') {
|
||||
result.system = requestBody.instructions
|
||||
}
|
||||
|
||||
@@ -192,17 +203,20 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (Array.isArray(input)) {
|
||||
// 消息数组
|
||||
for (const item of input) {
|
||||
const parsedMsg = this.parseCliInputItem(item)
|
||||
const parsedMsg = this.parseCliInputItem(item as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
}
|
||||
} else if (input?.messages && Array.isArray(input.messages)) {
|
||||
} else if (input && typeof input === 'object') {
|
||||
const inputObj = input as RawObject
|
||||
// 包装在对象中的消息数组
|
||||
for (const item of input.messages) {
|
||||
const parsedMsg = this.parseCliInputItem(item)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
if (Array.isArray(inputObj.messages)) {
|
||||
for (const item of inputObj.messages) {
|
||||
const parsedMsg = this.parseCliInputItem(item as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,23 +230,24 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 CLI 格式的单个输入项
|
||||
*/
|
||||
private parseCliInputItem(item: any): ParsedMessage | null {
|
||||
private parseCliInputItem(item: RawObject): ParsedMessage | null {
|
||||
if (!item) return null
|
||||
|
||||
const itemType = item.type
|
||||
|
||||
// 标准消息(有 role 字段)
|
||||
if (itemType === 'message' || item.role) {
|
||||
const role = this.mapRole(item.role)
|
||||
const role = this.mapRole(String(item.role || ''))
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
const content = item.content
|
||||
if (typeof content === 'string') {
|
||||
contentBlocks.push(createTextBlock(content))
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
for (const rawPart of content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
||||
contentBlocks.push(createTextBlock(part.text || ''))
|
||||
contentBlocks.push(createTextBlock(String(part.text || '')))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,15 +258,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
|
||||
// function_call -> 工具调用
|
||||
if (itemType === 'function_call') {
|
||||
const toolId = item.call_id || item.id || ''
|
||||
const toolName = item.name || ''
|
||||
const args = item.arguments || '{}'
|
||||
const toolId = String(item.call_id || item.id || '')
|
||||
const toolName = String(item.name || '')
|
||||
const args = String(item.arguments || '{}')
|
||||
return createMessage('assistant', [createToolUseBlock(toolId, toolName, args)])
|
||||
}
|
||||
|
||||
// function_call_output -> 工具结果
|
||||
if (itemType === 'function_call_output') {
|
||||
const toolUseId = item.call_id || item.id || ''
|
||||
const toolUseId = String(item.call_id || item.id || '')
|
||||
const output = typeof item.output === 'string'
|
||||
? item.output
|
||||
: JSON.stringify(item.output, null, 2)
|
||||
@@ -264,52 +279,58 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('openai', '无响应体')
|
||||
}
|
||||
|
||||
const body = responseBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = this.isCliResponseEvent(responseBody) ||
|
||||
responseBody.object === 'response' ||
|
||||
responseBody.output !== undefined
|
||||
const isCliFormat = this.isCliResponseEvent(body) ||
|
||||
body.object === 'response' ||
|
||||
body.output !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliResponse(responseBody)
|
||||
return this.parseCliResponse(body)
|
||||
}
|
||||
|
||||
return this.parseChatResponse(responseBody)
|
||||
return this.parseChatResponse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 响应
|
||||
*/
|
||||
private parseChatResponse(responseBody: any): ParsedConversation {
|
||||
private parseChatResponse(responseBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'openai',
|
||||
model: responseBody.model,
|
||||
model: typeof responseBody.model === 'string' ? responseBody.model : undefined,
|
||||
}
|
||||
|
||||
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
|
||||
const message = responseBody.choices?.[0]?.message
|
||||
const choices = responseBody.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const message = firstChoice?.message as RawObject | undefined
|
||||
if (message) {
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
if (message.content) {
|
||||
if (typeof message.content === 'string') {
|
||||
contentBlocks.push(createTextBlock(message.content))
|
||||
}
|
||||
|
||||
// 工具调用
|
||||
if (message.tool_calls) {
|
||||
for (const call of message.tool_calls) {
|
||||
if (Array.isArray(message.tool_calls)) {
|
||||
for (const rawCall of message.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseBlock(
|
||||
call.id || '',
|
||||
call.function?.name || '',
|
||||
call.function?.arguments || '{}'
|
||||
String(call.id || ''),
|
||||
String(fn?.name || ''),
|
||||
String(fn?.arguments || '{}')
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -330,13 +351,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
*
|
||||
* CLI 响应格式: { output: [{ type: "message", content: [...] }] }
|
||||
*/
|
||||
private parseCliResponse(responseBody: any): ParsedConversation {
|
||||
private parseCliResponse(responseBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'openai',
|
||||
model: responseBody.model,
|
||||
model: typeof responseBody.model === 'string' ? responseBody.model : undefined,
|
||||
}
|
||||
|
||||
const output = responseBody.output
|
||||
@@ -344,13 +365,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return result
|
||||
}
|
||||
|
||||
for (const item of output) {
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message') {
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
for (const rawContent of item.content) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
contentBlocks.push(createTextBlock(content.text))
|
||||
}
|
||||
}
|
||||
@@ -371,13 +394,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('openai', '无响应数据')
|
||||
}
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = chunks.some(chunk => this.isCliResponseEvent(chunk))
|
||||
const isCliFormat = chunks.some(chunk => this.isCliResponseEvent(chunk as RawObject))
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliStreamResponse(chunks)
|
||||
@@ -389,7 +412,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 流式响应
|
||||
*/
|
||||
private parseChatStreamResponse(chunks: any[]): ParsedConversation {
|
||||
private parseChatStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
@@ -400,35 +423,42 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
const textParts: string[] = []
|
||||
const toolCalls = new Map<number, { name: string; id: string; args: string[] }>()
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
// 提取模型名
|
||||
if (chunk.model && !result.model) {
|
||||
if (typeof chunk.model === 'string' && !result.model) {
|
||||
result.model = chunk.model
|
||||
}
|
||||
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
const choices = chunk.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const delta = firstChoice?.delta as RawObject | undefined
|
||||
if (typeof delta?.content === 'string') {
|
||||
textParts.push(delta.content)
|
||||
}
|
||||
if (delta?.tool_calls) {
|
||||
for (const call of delta.tool_calls) {
|
||||
const index = call.index ?? 0
|
||||
if (Array.isArray(delta?.tool_calls)) {
|
||||
for (const rawCall of delta.tool_calls as unknown[]) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
const index = (typeof call.index === 'number' ? call.index : 0)
|
||||
if (!toolCalls.has(index)) {
|
||||
toolCalls.set(index, {
|
||||
name: call.function?.name || '',
|
||||
id: call.id || '',
|
||||
name: String(fn?.name || ''),
|
||||
id: String(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 existing = toolCalls.get(index)
|
||||
if (existing) {
|
||||
if (typeof fn?.name === 'string') {
|
||||
existing.name = fn.name
|
||||
}
|
||||
if (typeof call.id === 'string') {
|
||||
existing.id = call.id
|
||||
}
|
||||
if (typeof fn?.arguments === 'string') {
|
||||
existing.args.push(fn.arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,7 +499,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
* - response.completed: 响应完成(包含完整响应和 usage)
|
||||
* - response.function_call_arguments.delta: 函数调用参数增量
|
||||
*/
|
||||
private parseCliStreamResponse(chunks: any[]): ParsedConversation {
|
||||
private parseCliStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
@@ -482,13 +512,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
let currentToolId = ''
|
||||
let currentToolName = ''
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
const eventType = chunk.type
|
||||
|
||||
// 从 response.created 或 response.completed 提取模型名
|
||||
if (!result.model) {
|
||||
const response = chunk.response
|
||||
if (response?.model) {
|
||||
const response = chunk.response as RawObject | undefined
|
||||
if (typeof response?.model === 'string') {
|
||||
result.model = response.model
|
||||
}
|
||||
}
|
||||
@@ -498,18 +529,21 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
const delta = chunk.delta
|
||||
if (typeof delta === 'string') {
|
||||
textParts.push(delta)
|
||||
} else if (delta?.text) {
|
||||
textParts.push(delta.text)
|
||||
} else if (delta && typeof delta === 'object') {
|
||||
const deltaObj = delta as RawObject
|
||||
if (typeof deltaObj.text === 'string') {
|
||||
textParts.push(deltaObj.text)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理函数调用输出项添加: response.output_item.added
|
||||
if (eventType === 'response.output_item.added') {
|
||||
const item = chunk.item
|
||||
const item = chunk.item as RawObject | undefined
|
||||
if (item?.type === 'function_call') {
|
||||
currentToolId = item.call_id || item.id || ''
|
||||
currentToolName = item.name || ''
|
||||
currentToolId = String(item.call_id || item.id || '')
|
||||
currentToolName = String(item.name || '')
|
||||
if (currentToolId && !toolCalls.has(currentToolId)) {
|
||||
toolCalls.set(currentToolId, {
|
||||
name: currentToolName,
|
||||
@@ -524,8 +558,8 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
// 处理函数调用参数增量: response.function_call_arguments.delta
|
||||
if (eventType === 'response.function_call_arguments.delta') {
|
||||
const delta = chunk.delta
|
||||
if (delta && currentToolId && toolCalls.has(currentToolId)) {
|
||||
toolCalls.get(currentToolId)!.args.push(delta)
|
||||
if (typeof delta === 'string' && currentToolId && toolCalls.has(currentToolId)) {
|
||||
toolCalls.get(currentToolId)?.args.push(delta)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -533,17 +567,19 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
// 处理完成事件: response.completed
|
||||
// 如果之前没有收集到文本,从完成事件中提取
|
||||
if (eventType === 'response.completed') {
|
||||
const response = chunk.response
|
||||
if (response?.model && !result.model) {
|
||||
const response = chunk.response as RawObject | undefined
|
||||
if (typeof response?.model === 'string' && !result.model) {
|
||||
result.model = response.model
|
||||
}
|
||||
|
||||
// 从 output 中提取文本(备用方案)
|
||||
if (textParts.length === 0 && response?.output) {
|
||||
for (const item of response.output) {
|
||||
if (item?.type === 'message' && item?.content) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
if (textParts.length === 0 && Array.isArray(response?.output)) {
|
||||
for (const rawItem of response.output as unknown[]) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message' && Array.isArray(item?.content)) {
|
||||
for (const rawContent of item.content as unknown[]) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
textParts.push(content.text)
|
||||
}
|
||||
}
|
||||
@@ -583,10 +619,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单条消息
|
||||
*/
|
||||
private parseMessage(msg: any): ParsedMessage | null {
|
||||
private parseMessage(msg: RawObject): ParsedMessage | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = this.mapRole(msg.role)
|
||||
const role = this.mapRole(String(msg.role))
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
@@ -594,12 +630,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
contentBlocks.push(createTextBlock(msg.content))
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Vision API 格式
|
||||
for (const part of msg.content) {
|
||||
for (const rawPart of msg.content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'text') {
|
||||
contentBlocks.push(createTextBlock(part.text || ''))
|
||||
contentBlocks.push(createTextBlock(String(part.text || '')))
|
||||
} else if (part.type === 'image_url') {
|
||||
const imageUrl = part.image_url as RawObject | undefined
|
||||
contentBlocks.push(createImageBlock('url', {
|
||||
url: part.image_url?.url,
|
||||
url: typeof imageUrl?.url === 'string' ? imageUrl.url : undefined,
|
||||
alt: '[图片]',
|
||||
}))
|
||||
}
|
||||
@@ -607,12 +645,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 工具调用(assistant 消息)
|
||||
if (msg.tool_calls) {
|
||||
for (const call of msg.tool_calls) {
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
for (const rawCall of msg.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseBlock(
|
||||
call.id || '',
|
||||
call.function?.name || '',
|
||||
call.function?.arguments || '{}'
|
||||
String(call.id || ''),
|
||||
String(fn?.name || ''),
|
||||
String(fn?.arguments || '{}')
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -623,7 +663,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content, null, 2)
|
||||
contentBlocks.push(createToolResultBlock(
|
||||
msg.tool_call_id,
|
||||
String(msg.tool_call_id),
|
||||
content
|
||||
))
|
||||
}
|
||||
@@ -658,31 +698,34 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
const body = requestBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = requestBody.input !== undefined || requestBody.instructions !== undefined
|
||||
const isCliFormat = body.input !== undefined || body.instructions !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.renderCliRequest(requestBody)
|
||||
return this.renderCliRequest(body)
|
||||
}
|
||||
|
||||
return this.renderChatRequest(requestBody)
|
||||
return this.renderChatRequest(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 OpenAI Chat Completions 请求
|
||||
*/
|
||||
private renderChatRequest(requestBody: any): RenderResult {
|
||||
private renderChatRequest(requestBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
for (const rawMsg of requestBody.messages) {
|
||||
const msg = rawMsg as RawObject
|
||||
// system 消息单独处理
|
||||
if (msg.role === 'system') {
|
||||
const systemText = typeof msg.content === 'string' ? msg.content : ''
|
||||
@@ -710,13 +753,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 OpenAI CLI (Responses API) 请求
|
||||
*/
|
||||
private renderCliRequest(requestBody: any): RenderResult {
|
||||
private renderCliRequest(requestBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
|
||||
// 渲染 instructions(系统指令)
|
||||
if (requestBody.instructions) {
|
||||
if (typeof requestBody.instructions === 'string') {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
createTextRenderBlock(requestBody.instructions),
|
||||
], { roleLabel: 'Instructions' }))
|
||||
@@ -733,17 +776,20 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (Array.isArray(input)) {
|
||||
// 消息数组
|
||||
for (const item of input) {
|
||||
const msgBlock = this.renderCliInputItem(item)
|
||||
const msgBlock = this.renderCliInputItem(item as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
}
|
||||
} else if (input?.messages && Array.isArray(input.messages)) {
|
||||
} else if (input && typeof input === 'object') {
|
||||
const inputObj = input as RawObject
|
||||
// 包装在对象中的消息数组
|
||||
for (const item of input.messages) {
|
||||
const msgBlock = this.renderCliInputItem(item)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
if (Array.isArray(inputObj.messages)) {
|
||||
for (const item of inputObj.messages) {
|
||||
const msgBlock = this.renderCliInputItem(item as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,23 +803,24 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 CLI 格式的单个输入项
|
||||
*/
|
||||
private renderCliInputItem(item: any): RenderBlock | null {
|
||||
private renderCliInputItem(item: RawObject): RenderBlock | null {
|
||||
if (!item) return null
|
||||
|
||||
const itemType = item.type
|
||||
|
||||
// 标准消息
|
||||
if (itemType === 'message' || item.role) {
|
||||
const role = this.mapRole(item.role)
|
||||
const role = this.mapRole(String(item.role || ''))
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
|
||||
const content = item.content
|
||||
if (typeof content === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(content))
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
for (const rawPart of content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
||||
contentBlocks.push(createTextRenderBlock(part.text || ''))
|
||||
contentBlocks.push(createTextRenderBlock(String(part.text || '')))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,10 +831,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
|
||||
// function_call -> 工具调用
|
||||
if (itemType === 'function_call') {
|
||||
const toolName = item.name || '工具调用'
|
||||
const toolName = String(item.name || '工具调用')
|
||||
const args = this.formatJson(item.arguments)
|
||||
return createMessageBlock('assistant', [
|
||||
createToolUseRenderBlock(toolName, args, item.call_id || item.id),
|
||||
createToolUseRenderBlock(toolName, args, String(item.call_id || item.id || '')),
|
||||
], { roleLabel: 'Assistant', badges: [createBadgeBlock('工具调用', 'outline')] })
|
||||
}
|
||||
|
||||
@@ -807,7 +854,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -817,44 +864,50 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return this.renderStreamResponse(responseBody.chunks || [])
|
||||
}
|
||||
|
||||
const body = responseBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = this.isCliResponseEvent(responseBody) ||
|
||||
responseBody.object === 'response' ||
|
||||
responseBody.output !== undefined
|
||||
const isCliFormat = this.isCliResponseEvent(body) ||
|
||||
body.object === 'response' ||
|
||||
body.output !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.renderCliResponse(responseBody)
|
||||
return this.renderCliResponse(body)
|
||||
}
|
||||
|
||||
return this.renderChatResponse(responseBody)
|
||||
return this.renderChatResponse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 OpenAI Chat Completions 响应
|
||||
*/
|
||||
private renderChatResponse(responseBody: any): RenderResult {
|
||||
private renderChatResponse(responseBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
|
||||
const message = responseBody.choices?.[0]?.message
|
||||
const choices = responseBody.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const message = firstChoice?.message as RawObject | undefined
|
||||
if (message) {
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
if (message.content) {
|
||||
if (typeof message.content === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(message.content))
|
||||
}
|
||||
|
||||
// 工具调用
|
||||
if (message.tool_calls) {
|
||||
if (Array.isArray(message.tool_calls)) {
|
||||
badges.push(createBadgeBlock('工具调用', 'outline'))
|
||||
for (const call of message.tool_calls) {
|
||||
for (const rawCall of message.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseRenderBlock(
|
||||
call.function?.name || '工具调用',
|
||||
this.formatJson(call.function?.arguments),
|
||||
call.id
|
||||
String(fn?.name || '工具调用'),
|
||||
this.formatJson(fn?.arguments),
|
||||
typeof call.id === 'string' ? call.id : undefined
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -876,7 +929,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 OpenAI CLI (Responses API) 响应
|
||||
*/
|
||||
private renderCliResponse(responseBody: any): RenderResult {
|
||||
private renderCliResponse(responseBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
@@ -885,13 +938,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return { blocks, isStream: false }
|
||||
}
|
||||
|
||||
for (const item of output) {
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message') {
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
for (const rawContent of item.content) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(content.text))
|
||||
}
|
||||
}
|
||||
@@ -914,7 +969,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -949,10 +1004,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单条消息
|
||||
*/
|
||||
private renderMessage(msg: any): RenderBlock | null {
|
||||
private renderMessage(msg: RawObject): RenderBlock | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = this.mapRole(msg.role)
|
||||
const role = this.mapRole(String(msg.role))
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
|
||||
@@ -961,13 +1016,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
contentBlocks.push(createTextRenderBlock(msg.content))
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Vision API 格式
|
||||
for (const part of msg.content) {
|
||||
for (const rawPart of msg.content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'text') {
|
||||
contentBlocks.push(createTextRenderBlock(part.text || ''))
|
||||
contentBlocks.push(createTextRenderBlock(String(part.text || '')))
|
||||
} else if (part.type === 'image_url') {
|
||||
badges.push(createBadgeBlock('图片', 'secondary'))
|
||||
const imageUrl = part.image_url as RawObject | undefined
|
||||
contentBlocks.push(createImageRenderBlock({
|
||||
src: part.image_url?.url,
|
||||
src: typeof imageUrl?.url === 'string' ? imageUrl.url : undefined,
|
||||
alt: '[图片]',
|
||||
}))
|
||||
}
|
||||
@@ -975,13 +1032,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 工具调用(assistant 消息)
|
||||
if (msg.tool_calls) {
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
badges.push(createBadgeBlock('工具调用', 'outline'))
|
||||
for (const call of msg.tool_calls) {
|
||||
for (const rawCall of msg.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseRenderBlock(
|
||||
call.function?.name || '工具调用',
|
||||
this.formatJson(call.function?.arguments),
|
||||
call.id
|
||||
String(fn?.name || '工具调用'),
|
||||
this.formatJson(fn?.arguments),
|
||||
typeof call.id === 'string' ? call.id : undefined
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1091,10 +1150,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
|
||||
@@ -40,7 +40,7 @@ class ParserRegistry {
|
||||
/**
|
||||
* 检测 API 格式并返回最佳匹配的解析器
|
||||
*/
|
||||
detectParser(requestBody: any, responseBody: any, hint?: string): ApiFormatParser | undefined {
|
||||
detectParser(requestBody: unknown, responseBody: unknown, hint?: string): ApiFormatParser | undefined {
|
||||
let bestParser: ApiFormatParser | undefined
|
||||
let bestScore = 0
|
||||
|
||||
@@ -58,7 +58,7 @@ class ParserRegistry {
|
||||
/**
|
||||
* 检测 API 格式
|
||||
*/
|
||||
detectFormat(requestBody: any, responseBody: any, hint?: string): ApiFormat {
|
||||
detectFormat(requestBody: unknown, responseBody: unknown, hint?: string): ApiFormat {
|
||||
const parser = this.detectParser(requestBody, responseBody, hint)
|
||||
return parser?.format ?? 'unknown'
|
||||
}
|
||||
@@ -76,8 +76,8 @@ parserRegistry.register(geminiParser)
|
||||
* 解析请求体
|
||||
*/
|
||||
export function parseRequest(
|
||||
requestBody: any,
|
||||
responseBody?: any,
|
||||
requestBody: unknown,
|
||||
responseBody?: unknown,
|
||||
formatHint?: string
|
||||
): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
@@ -96,8 +96,8 @@ export function parseRequest(
|
||||
* 解析响应体
|
||||
*/
|
||||
export function parseResponse(
|
||||
responseBody: any,
|
||||
requestBody?: any,
|
||||
responseBody: unknown,
|
||||
requestBody?: unknown,
|
||||
formatHint?: string
|
||||
): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
@@ -121,8 +121,8 @@ export function parseResponse(
|
||||
* 检测 API 格式
|
||||
*/
|
||||
export function detectApiFormat(
|
||||
requestBody: any,
|
||||
responseBody: any,
|
||||
requestBody: unknown,
|
||||
responseBody: unknown,
|
||||
hint?: string
|
||||
): ApiFormat {
|
||||
return parserRegistry.detectFormat(requestBody, responseBody, hint)
|
||||
@@ -132,8 +132,8 @@ export function detectApiFormat(
|
||||
* 渲染请求体
|
||||
*/
|
||||
export function renderRequest(
|
||||
requestBody: any,
|
||||
responseBody?: any,
|
||||
requestBody: unknown,
|
||||
responseBody?: unknown,
|
||||
formatHint?: string
|
||||
): RenderResult {
|
||||
if (!requestBody) {
|
||||
@@ -152,8 +152,8 @@ export function renderRequest(
|
||||
* 渲染响应体
|
||||
*/
|
||||
export function renderResponse(
|
||||
responseBody: any,
|
||||
requestBody?: any,
|
||||
responseBody: unknown,
|
||||
requestBody?: unknown,
|
||||
formatHint?: string
|
||||
): RenderResult {
|
||||
if (!responseBody) {
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface ToolUseContentBlock extends ContentBlockBase {
|
||||
type: 'tool_use'
|
||||
toolId: string
|
||||
toolName: string
|
||||
input: Record<string, any> | string
|
||||
input: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
/** 工具结果内容块 */
|
||||
@@ -151,7 +151,7 @@ export interface FormatDetector {
|
||||
* @param hint 后端提供的格式提示
|
||||
* @returns 匹配置信度 (0-100),0 表示不匹配
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number
|
||||
}
|
||||
|
||||
/** 请求体解析器 */
|
||||
@@ -161,7 +161,7 @@ export interface RequestParser {
|
||||
* @param requestBody 请求体
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation
|
||||
parseRequest(requestBody: unknown): ParsedConversation
|
||||
}
|
||||
|
||||
/** 响应体解析器 */
|
||||
@@ -171,14 +171,14 @@ export interface ResponseParser {
|
||||
* @param responseBody 响应体
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation
|
||||
parseResponse(responseBody: unknown): ParsedConversation
|
||||
|
||||
/**
|
||||
* 解析流式响应
|
||||
* @param chunks 响应块列表
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation
|
||||
}
|
||||
|
||||
/** 完整的 API 格式解析器 */
|
||||
@@ -193,14 +193,14 @@ export interface ApiFormatParser extends FormatDetector, RequestParser, Response
|
||||
* @param requestBody 请求体
|
||||
* @returns 渲染结果
|
||||
*/
|
||||
renderRequest(requestBody: any): import('./render').RenderResult
|
||||
renderRequest(requestBody: unknown): import('./render').RenderResult
|
||||
|
||||
/**
|
||||
* 渲染响应体为渲染块
|
||||
* @param responseBody 响应体
|
||||
* @returns 渲染结果
|
||||
*/
|
||||
renderResponse(responseBody: any): import('./render').RenderResult
|
||||
renderResponse(responseBody: unknown): import('./render').RenderResult
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -215,12 +215,15 @@ export interface StreamMetadata {
|
||||
/** 流式响应体结构 */
|
||||
export interface StreamResponseBody {
|
||||
metadata?: StreamMetadata
|
||||
chunks?: any[]
|
||||
chunks?: unknown[]
|
||||
}
|
||||
|
||||
/** 检查是否为流式响应 */
|
||||
export function isStreamResponse(body: any): body is StreamResponseBody {
|
||||
return body?.metadata?.stream === true && Array.isArray(body?.chunks)
|
||||
export function isStreamResponse(body: unknown): body is StreamResponseBody {
|
||||
if (!body || typeof body !== 'object') return false
|
||||
const obj = body as Record<string, unknown>
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return metadata?.stream === true && Array.isArray(obj.chunks)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -241,7 +244,7 @@ export function createThinkingBlock(thinking: string, signature?: string): Think
|
||||
export function createToolUseBlock(
|
||||
toolId: string,
|
||||
toolName: string,
|
||||
input: Record<string, any> | string
|
||||
input: Record<string, unknown> | string
|
||||
): ToolUseContentBlock {
|
||||
return { type: 'tool_use', toolId, toolName, input }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user