feat(provider): 增加 Claude Code 适配器、高级配置能力与 OAuth 账号类型统一解析

- 新增 Claude Code provider adapter (context, envelope, plugin, constants)
- 扩展 provider admin 路由,支持 Claude Code 高级配置 (CRUD)
- 统一 OAuth 账号类型解析逻辑,前后端对齐
- 重构 BatchAssignModelsDialog / ModelMappingDialog,简化组件逻辑
- handler 基类增强: request_builder 支持 Claude Code 信封格式
- CLI stream/sync mixin 适配 Claude Code 流式与同步模式
- 扩展 candidate builder / failover / scheduler 对 Claude Code 的支持
- 前端增加请求时间线可视化 (HorizontalRequestTimeline)
- 补充 Claude Code envelope / runtime controls / distributed sessions 等测试

Closes #183
Closes #185

Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-02-27 13:54:46 +08:00
parent f2f2a2dbc4
commit 579b5e4623
55 changed files with 3106 additions and 652 deletions

View File

@@ -252,6 +252,62 @@
</span>
</span>
</div>
<div
v-if="currentAttempt.extra_data?.pool_selection"
class="info-item"
>
<span class="info-label">号池调度</span>
<span class="info-value info-value-stacked">
<span class="pool-reason">
<span
class="pool-reason-tag"
:class="'pool-' + currentAttempt.extra_data.pool_selection.reason"
>
{{ poolSelectionLabel(currentAttempt.extra_data.pool_selection.reason) }}
</span>
<span
v-if="currentAttempt.extra_data.pool_selection.cost_soft_threshold"
class="pool-cost-warn"
>接近限额</span>
</span>
<span
v-if="currentAttempt.extra_data.pool_selection.cost_window_usage"
class="text-xs text-muted-foreground"
>
{{ formatNumber(currentAttempt.extra_data.pool_selection.cost_window_usage) }}
<template v-if="currentAttempt.extra_data.pool_selection.cost_limit">
/ {{ formatNumber(currentAttempt.extra_data.pool_selection.cost_limit) }}
</template>
tokens
</span>
</span>
</div>
<div
v-if="currentAttempt.extra_data?.pool_skip"
class="info-item"
>
<span class="info-label">号池跳过</span>
<span class="info-value info-value-stacked">
<span class="pool-skip-type">
{{ poolSkipLabel(currentAttempt.extra_data.pool_skip.type) }}
</span>
<span
v-if="currentAttempt.extra_data.pool_skip.cooldown_reason"
class="text-xs text-muted-foreground"
>
{{ currentAttempt.extra_data.pool_skip.cooldown_reason }}
<template v-if="currentAttempt.extra_data.pool_skip.cooldown_ttl != null">
({{ currentAttempt.extra_data.pool_skip.cooldown_ttl }}s)
</template>
</span>
<span
v-if="currentAttempt.extra_data.pool_skip.cost_window_usage"
class="text-xs text-muted-foreground"
>
{{ formatNumber(currentAttempt.extra_data.pool_skip.cost_window_usage) }} tokens
</span>
</span>
</div>
<div
v-if="mergedCapabilities.length > 0"
class="info-item"
@@ -764,6 +820,25 @@ const formatCapabilityLabel = (cap: string): string => {
return labels[cap] || cap
}
const poolSelectionLabel = (reason: string): string => {
const labels: Record<string, string> = {
sticky: '粘性会话',
lru: 'LRU',
random: '随机',
tiebreak: '随机 (平分)',
}
return labels[reason] || reason
}
const poolSkipLabel = (type: string): string => {
const labels: Record<string, string> = {
cooldown: '冷却中',
cost_exhausted: '额度耗尽',
upstream: '上游跳过',
}
return labels[type] || type
}
// 检查组是否被悬浮
const isGroupHovered = (groupIndex: number) => {
return hoveredGroupIndex.value === groupIndex
@@ -1485,6 +1560,52 @@ const getStatusColorClass = (status: string) => {
gap: 0.375rem;
}
/* 号池调度 */
.pool-reason {
display: flex;
align-items: center;
gap: 0.375rem;
}
.pool-reason-tag {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.5rem;
font-size: 0.7rem;
font-weight: 500;
border-radius: 4px;
white-space: nowrap;
border: 1px solid hsl(var(--border));
}
.pool-reason-tag.pool-sticky {
color: hsl(var(--chart-4));
border-color: hsl(var(--chart-4) / 0.3);
background: hsl(var(--chart-4) / 0.08);
}
.pool-reason-tag.pool-lru {
color: hsl(var(--chart-2));
border-color: hsl(var(--chart-2) / 0.3);
background: hsl(var(--chart-2) / 0.08);
}
.pool-reason-tag.pool-random,
.pool-reason-tag.pool-tiebreak {
color: hsl(var(--muted-foreground));
}
.pool-cost-warn {
font-size: 0.65rem;
color: hsl(var(--chart-5));
font-weight: 500;
}
.pool-skip-type {
font-weight: 500;
color: hsl(var(--muted-foreground));
}
/* 能力标签 */
.capability-tags {
display: flex;

View File

@@ -370,6 +370,51 @@
</div>
</Card>
<!-- 号池调度摘要 -->
<Card v-if="poolSummary">
<div class="p-3 sm:p-4">
<div class="text-xs text-muted-foreground mb-2 font-medium">
号池调度
</div>
<div class="flex items-center gap-3 flex-wrap text-sm">
<span class="font-mono">
{{ poolSummary.total_keys }} 候选
</span>
<span class="text-muted-foreground">|</span>
<span class="font-mono">
{{ poolSummary.attempted }} 尝试
</span>
<template v-if="poolSummary.skipped_cooldown > 0">
<span class="text-muted-foreground">|</span>
<span class="font-mono text-amber-600 dark:text-amber-400">
{{ poolSummary.skipped_cooldown }} 冷却跳过
</span>
</template>
<template v-if="poolSummary.skipped_cost > 0">
<span class="text-muted-foreground">|</span>
<span class="font-mono text-orange-600 dark:text-orange-400">
{{ poolSummary.skipped_cost }} 成本跳过
</span>
</template>
<template v-if="poolSummary.sticky_session">
<span class="text-muted-foreground">|</span>
<Badge
variant="outline"
class="text-[10px] px-1.5 py-0 h-4"
>
粘性会话
</Badge>
</template>
<template v-if="poolSummary.success_reason">
<span class="text-muted-foreground">|</span>
<span class="text-xs text-muted-foreground">
{{ poolSummary.success_reason }}
</span>
</template>
</div>
</div>
</Card>
<!-- 请求链路追踪卡片 -->
<div v-if="detail.request_id || detail.id">
<HorizontalRequestTimeline
@@ -733,6 +778,13 @@ const isDark = computed(() => {
return document.documentElement.classList.contains('dark')
})
// 号池调度摘要
const poolSummary = computed(() => {
const ps = detail.value?.metadata?.pool_summary as Record<string, unknown> | undefined
if (!ps || !ps.enabled) return null
return ps
})
// 检测是否有提供商请求头
const hasProviderHeaders = computed(() => {
return !!(detail.value?.provider_request_headers &&