mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
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 <[email protected]>
This commit is contained in:
@@ -19,35 +19,9 @@
|
||||
class="pl-8 h-9"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="upstreamModelsLoaded"
|
||||
type="button"
|
||||
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
||||
title="刷新上游模型"
|
||||
:disabled="fetchingUpstreamModels"
|
||||
@click="fetchUpstreamModels(true)"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': fetchingUpstreamModels }"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!fetchingUpstreamModels"
|
||||
type="button"
|
||||
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
||||
title="从提供商获取模型"
|
||||
@click="fetchUpstreamModels()"
|
||||
>
|
||||
<Zap class="w-4 h-4" />
|
||||
</button>
|
||||
<Loader2
|
||||
v-else
|
||||
class="w-4 h-4 animate-spin text-muted-foreground shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 单列模型列表 -->
|
||||
<!-- 模型列表 -->
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<div
|
||||
@@ -58,17 +32,12 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 全局模型组 -->
|
||||
<div v-if="filteredGlobalModels.length > 0 || !upstreamModelsLoaded">
|
||||
<!-- 全局模型列表 -->
|
||||
<div v-if="filteredGlobalModels.length > 0">
|
||||
<div
|
||||
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
@click="toggleGroupCollapse('global')"
|
||||
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
class="w-4 h-4 transition-transform shrink-0"
|
||||
:class="collapsedGroups.has('global') ? '-rotate-90' : ''"
|
||||
/>
|
||||
<span class="text-xs font-medium">全局模型</span>
|
||||
<span class="text-xs text-muted-foreground">({{ filteredGlobalModels.length }})</span>
|
||||
</div>
|
||||
@@ -81,16 +50,7 @@
|
||||
{{ isAllGlobalModelsSelected ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-show="!collapsedGroups.has('global')"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-if="filteredGlobalModels.length === 0"
|
||||
class="py-4 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
暂无可用全局模型
|
||||
</div>
|
||||
<div class="space-y-1 p-2">
|
||||
<div
|
||||
v-for="model in filteredGlobalModels"
|
||||
:key="model.id"
|
||||
@@ -118,85 +78,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上游模型组 -->
|
||||
<div v-if="filteredUpstreamModels.length > 0">
|
||||
<div
|
||||
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
@click="toggleGroupCollapse('upstream')"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
class="w-4 h-4 transition-transform shrink-0"
|
||||
:class="collapsedGroups.has('upstream') ? '-rotate-90' : ''"
|
||||
/>
|
||||
<span class="text-xs font-medium">上游模型</span>
|
||||
<span class="text-xs text-muted-foreground">({{ filteredUpstreamModels.length }})</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-primary hover:underline shrink-0"
|
||||
@click.stop="toggleAllUpstreamModels"
|
||||
>
|
||||
{{ isAllUpstreamModelsSelected ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-show="!collapsedGroups.has('upstream')"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="model in filteredUpstreamModels"
|
||||
:key="model.id"
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
||||
@click="toggleUpstreamModelSelection(model.id)"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
||||
:class="isUpstreamModelSelected(model.id) ? 'bg-primary border-primary' : ''"
|
||||
>
|
||||
<Check
|
||||
v-if="isUpstreamModelSelected(model.id)"
|
||||
class="w-3 h-3 text-primary-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-sm font-medium truncate">
|
||||
{{ model.id }}
|
||||
</p>
|
||||
<span
|
||||
v-for="fmt in model.api_formats"
|
||||
:key="fmt"
|
||||
class="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0"
|
||||
>
|
||||
{{ formatApiFormat(fmt) }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="model.owned_by"
|
||||
class="text-xs text-muted-foreground truncate"
|
||||
>
|
||||
{{ model.owned_by }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="filteredGlobalModels.length === 0 && filteredUpstreamModels.length === 0"
|
||||
v-if="filteredGlobalModels.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
||||
>
|
||||
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
||||
<p class="text-sm">
|
||||
{{ searchQuery ? '无匹配结果' : '暂无可用模型' }}
|
||||
{{ searchQuery ? '无匹配结果' : '暂无可用全局模型' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="!upstreamModelsLoaded"
|
||||
class="text-xs mt-1"
|
||||
>
|
||||
点击上方按钮从上游获取模型
|
||||
<p class="text-xs mt-1">
|
||||
请先前往"模型目录"页面创建全局模型
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -234,7 +126,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Layers, Loader2, ChevronDown, Zap, RefreshCw, Search, Check } from 'lucide-vue-next'
|
||||
import { Layers, Loader2, Search, Check } from 'lucide-vue-next'
|
||||
import Dialog from '@/components/ui/dialog/Dialog.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
@@ -249,11 +141,8 @@ import {
|
||||
getProviderModels,
|
||||
batchAssignModelsToProvider,
|
||||
deleteModel,
|
||||
importModelsFromUpstream,
|
||||
type Model
|
||||
} from '@/api/endpoints'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { useUpstreamModelsCache, type UpstreamModel } from '../composables/useUpstreamModelsCache'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -266,32 +155,22 @@ const emit = defineEmits<{
|
||||
'changed': []
|
||||
}>()
|
||||
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
const { error: showError, success } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
|
||||
// 状态
|
||||
const loadingGlobalModels = ref(false)
|
||||
const fetchingUpstreamModels = ref(false)
|
||||
const upstreamModelsLoaded = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
// 数据
|
||||
const allGlobalModels = ref<GlobalModelResponse[]>([])
|
||||
const existingModels = ref<Model[]>([])
|
||||
const upstreamModels = ref<UpstreamModel[]>([])
|
||||
|
||||
// 选择状态(本地状态,保存时才提交)
|
||||
const selectedGlobalModelIds = ref<Set<string>>(new Set())
|
||||
const selectedUpstreamModelIds = ref<Set<string>>(new Set())
|
||||
|
||||
// 初始状态(用于计算变更)
|
||||
const initialGlobalModelIds = ref<Set<string>>(new Set())
|
||||
const initialUpstreamModelNames = ref<Set<string>>(new Set())
|
||||
|
||||
// 折叠状态
|
||||
const collapsedGroups = ref<Set<string>>(new Set())
|
||||
|
||||
// 搜索状态
|
||||
const searchQuery = ref('')
|
||||
@@ -305,18 +184,6 @@ const existingGlobalModelIds = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// 已关联的上游模型名称集合
|
||||
const existingUpstreamModelNames = computed(() => {
|
||||
const names = new Set<string>()
|
||||
for (const m of existingModels.value) {
|
||||
names.add(m.provider_model_name)
|
||||
for (const mapping of m.provider_model_mappings ?? []) {
|
||||
if (mapping.name) names.add(mapping.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
})
|
||||
|
||||
// 过滤后的全局模型
|
||||
const filteredGlobalModels = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
@@ -328,59 +195,17 @@ const filteredGlobalModels = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 过滤后的上游模型(后端已按 id 聚合)
|
||||
const filteredUpstreamModels = computed(() => {
|
||||
if (!upstreamModelsLoaded.value) return []
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
let models = upstreamModels.value
|
||||
|
||||
if (query) {
|
||||
models = models.filter(m => m.id.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
// 按 id 排序
|
||||
return [...models].sort((a, b) => a.id.localeCompare(b.id))
|
||||
})
|
||||
|
||||
// 上游模型是否全选
|
||||
const isAllUpstreamModelsSelected = computed(() => {
|
||||
if (filteredUpstreamModels.value.length === 0) return false
|
||||
return filteredUpstreamModels.value.every(m => selectedUpstreamModelIds.value.has(m.id))
|
||||
})
|
||||
|
||||
// 全选/取消全选上游模型
|
||||
function toggleAllUpstreamModels() {
|
||||
const allIds = filteredUpstreamModels.value.map(m => m.id)
|
||||
if (isAllUpstreamModelsSelected.value) {
|
||||
// 取消全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.delete(id)
|
||||
}
|
||||
} else {
|
||||
// 全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查全局模型是否已选中
|
||||
function isGlobalModelSelected(globalModelId: string): boolean {
|
||||
return selectedGlobalModelIds.value.has(globalModelId)
|
||||
}
|
||||
|
||||
// 检查上游模型是否已选中
|
||||
function isUpstreamModelSelected(modelId: string): boolean {
|
||||
return selectedUpstreamModelIds.value.has(modelId)
|
||||
}
|
||||
|
||||
// 全局模型是否全选
|
||||
const isAllGlobalModelsSelected = computed(() => {
|
||||
if (filteredGlobalModels.value.length === 0) return false
|
||||
return filteredGlobalModels.value.every(m => isGlobalModelSelected(m.id))
|
||||
})
|
||||
|
||||
// 检查全局模型是否已选中
|
||||
function isGlobalModelSelected(globalModelId: string): boolean {
|
||||
return selectedGlobalModelIds.value.has(globalModelId)
|
||||
}
|
||||
|
||||
// 计算待添加的全局模型
|
||||
const globalModelsToAdd = computed(() => {
|
||||
const toAdd: string[] = []
|
||||
@@ -403,42 +228,16 @@ const globalModelsToRemove = computed(() => {
|
||||
return toRemove
|
||||
})
|
||||
|
||||
// 计算待添加的上游模型
|
||||
const upstreamModelsToAdd = computed(() => {
|
||||
const toAdd: string[] = []
|
||||
for (const id of selectedUpstreamModelIds.value) {
|
||||
if (!initialUpstreamModelNames.value.has(id)) {
|
||||
toAdd.push(id)
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
})
|
||||
|
||||
// 计算待移除的上游模型
|
||||
const upstreamModelsToRemove = computed(() => {
|
||||
const toRemove: string[] = []
|
||||
for (const id of initialUpstreamModelNames.value) {
|
||||
if (!selectedUpstreamModelIds.value.has(id)) {
|
||||
toRemove.push(id)
|
||||
}
|
||||
}
|
||||
return toRemove
|
||||
})
|
||||
|
||||
// 是否有变更
|
||||
const hasChanges = computed(() => {
|
||||
return globalModelsToAdd.value.length > 0 ||
|
||||
globalModelsToRemove.value.length > 0 ||
|
||||
upstreamModelsToAdd.value.length > 0 ||
|
||||
upstreamModelsToRemove.value.length > 0
|
||||
globalModelsToRemove.value.length > 0
|
||||
})
|
||||
|
||||
// 待变更数量
|
||||
const pendingChangesCount = computed(() => {
|
||||
return globalModelsToAdd.value.length +
|
||||
globalModelsToRemove.value.length +
|
||||
upstreamModelsToAdd.value.length +
|
||||
upstreamModelsToRemove.value.length
|
||||
globalModelsToRemove.value.length
|
||||
})
|
||||
|
||||
// 切换全局模型选择
|
||||
@@ -451,26 +250,14 @@ function toggleGlobalModelSelection(id: string) {
|
||||
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
||||
}
|
||||
|
||||
// 切换上游模型选择
|
||||
function toggleUpstreamModelSelection(id: string) {
|
||||
if (selectedUpstreamModelIds.value.has(id)) {
|
||||
selectedUpstreamModelIds.value.delete(id)
|
||||
} else {
|
||||
selectedUpstreamModelIds.value.add(id)
|
||||
}
|
||||
selectedUpstreamModelIds.value = new Set(selectedUpstreamModelIds.value)
|
||||
}
|
||||
|
||||
// 全选/取消全选全局模型
|
||||
function toggleAllGlobalModels() {
|
||||
const allIds = filteredGlobalModels.value.map(m => m.id)
|
||||
if (isAllGlobalModelsSelected.value) {
|
||||
// 取消全选
|
||||
for (const id of allIds) {
|
||||
selectedGlobalModelIds.value.delete(id)
|
||||
}
|
||||
} else {
|
||||
// 全选
|
||||
for (const id of allIds) {
|
||||
selectedGlobalModelIds.value.add(id)
|
||||
}
|
||||
@@ -478,16 +265,6 @@ function toggleAllGlobalModels() {
|
||||
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
function toggleGroupCollapse(group: string) {
|
||||
if (collapsedGroups.value.has(group)) {
|
||||
collapsedGroups.value.delete(group)
|
||||
} else {
|
||||
collapsedGroups.value.add(group)
|
||||
}
|
||||
collapsedGroups.value = new Set(collapsedGroups.value)
|
||||
}
|
||||
|
||||
// 处理关闭
|
||||
async function handleClose() {
|
||||
if (hasChanges.value) {
|
||||
@@ -530,23 +307,6 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除上游模型
|
||||
for (const modelId of upstreamModelsToRemove.value) {
|
||||
const existingModel = existingModels.value.find(m =>
|
||||
m.provider_model_name === modelId ||
|
||||
m.provider_model_mappings?.some(mapping => mapping.name === modelId)
|
||||
)
|
||||
if (existingModel) {
|
||||
hasAnyOperation = true
|
||||
try {
|
||||
await deleteModel(props.providerId, existingModel.id)
|
||||
totalSuccess++
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '移除失败'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加全局模型
|
||||
if (globalModelsToAdd.value.length > 0) {
|
||||
hasAnyOperation = true
|
||||
@@ -561,20 +321,6 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加上游模型
|
||||
if (upstreamModelsToAdd.value.length > 0) {
|
||||
hasAnyOperation = true
|
||||
try {
|
||||
const result = await importModelsFromUpstream(props.providerId, upstreamModelsToAdd.value)
|
||||
totalSuccess += result.success.length
|
||||
if (result.errors.length > 0) {
|
||||
allErrors.push(...result.errors.map(e => e.error))
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '导入上游模型失败'))
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSuccess > 0) {
|
||||
success(`成功处理 ${totalSuccess} 个模型`)
|
||||
}
|
||||
@@ -587,7 +333,6 @@ async function handleSave() {
|
||||
emit('update:open', false)
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
// 即使出错,如果已执行过操作,也通知父组件刷新数据
|
||||
if (hasAnyOperation) {
|
||||
emit('changed')
|
||||
}
|
||||
@@ -596,52 +341,28 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
// 从已有数据同步选择状态(全局模型)
|
||||
// 从已有数据同步选择状态
|
||||
function syncGlobalModelSelection() {
|
||||
const globalIds = [...existingGlobalModelIds.value].filter((id): id is string => id !== undefined)
|
||||
selectedGlobalModelIds.value = new Set(globalIds)
|
||||
initialGlobalModelIds.value = new Set(globalIds)
|
||||
}
|
||||
|
||||
// 从已有数据同步选择状态(上游模型)
|
||||
function syncUpstreamModelSelection() {
|
||||
// 只同步当前已加载的上游模型中,与已关联模型匹配的部分
|
||||
const selected = new Set<string>()
|
||||
for (const model of upstreamModels.value) {
|
||||
if (existingUpstreamModelNames.value.has(model.id)) {
|
||||
selected.add(model.id)
|
||||
}
|
||||
}
|
||||
selectedUpstreamModelIds.value = selected
|
||||
initialUpstreamModelNames.value = new Set(selected)
|
||||
}
|
||||
|
||||
// 监听打开状态
|
||||
watch(() => props.open, async (isOpen) => {
|
||||
if (isOpen && props.providerId) {
|
||||
await loadData()
|
||||
} else {
|
||||
// 重置状态
|
||||
upstreamModels.value = []
|
||||
upstreamModelsLoaded.value = false
|
||||
collapsedGroups.value = new Set()
|
||||
searchQuery.value = ''
|
||||
selectedGlobalModelIds.value = new Set()
|
||||
selectedUpstreamModelIds.value = new Set()
|
||||
initialGlobalModelIds.value = new Set()
|
||||
initialUpstreamModelNames.value = new Set()
|
||||
}
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
await Promise.all([loadGlobalModels(), loadExistingModels()])
|
||||
|
||||
// 同步全局模型选择状态
|
||||
syncGlobalModelSelection()
|
||||
|
||||
// 初始折叠状态
|
||||
collapsedGroups.value = new Set()
|
||||
}
|
||||
|
||||
// 加载全局模型列表
|
||||
@@ -665,26 +386,4 @@ async function loadExistingModels() {
|
||||
showError(parseApiError(err, '加载已关联模型失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
// 从提供商获取模型
|
||||
async function fetchUpstreamModels(forceRefresh = false) {
|
||||
try {
|
||||
fetchingUpstreamModels.value = true
|
||||
const result = await fetchCachedModels(props.providerId, undefined, forceRefresh)
|
||||
if (result) {
|
||||
if (result.error) {
|
||||
showError(result.error, '错误')
|
||||
} else {
|
||||
upstreamModels.value = result.models
|
||||
upstreamModelsLoaded.value = true
|
||||
// 同步上游模型选择状态
|
||||
syncUpstreamModelSelection()
|
||||
// 全部折叠
|
||||
collapsedGroups.value = new Set(['global', 'upstream'])
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fetchingUpstreamModels.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user