feat(pool): 号池额度主动探测、封禁自动清除、调度硬优先级与前端重构

- 新增 PoolQuotaProbeScheduler,按 probing_interval_minutes 主动探测静默 Key 额度
- pool_advanced 增加 probing_enabled / auto_remove_banned_keys 配置项
- error_handler 和 quota_service 支持封禁 Key 自动删除及缓存清理
- multi_score 策略从加权混合重构为硬优先级排序,引入 mutex_group 互斥组
- 指纹注入从 handler 层下移至 ClaudeCode envelope 层
- OAuth 批量导入支持 concurrency 并发参数
- 前端号池管理拆分高级设置/账号批量/代理设置为独立组件
- 号池总览接口精简,仅返回已启用调度的 Provider
This commit is contained in:
fawney19
2026-03-05 15:15:26 +08:00
parent fdb50a065b
commit b1be413dc0
29 changed files with 3098 additions and 852 deletions

View File

@@ -0,0 +1,110 @@
<template>
<div class="relative">
<button
type="button"
:class="cn(
'h-9 px-3 border rounded-lg bg-background text-left flex items-center justify-between hover:bg-muted/50 transition-colors gap-1',
triggerClass,
)"
:disabled="disabled"
@click="isOpen = !isOpen"
>
<span
:class="modelValue.length ? 'text-foreground' : 'text-muted-foreground'"
class="text-xs truncate"
>
{{ displayText }}
</span>
<ChevronDown
class="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform"
:class="isOpen ? 'rotate-180' : ''"
/>
</button>
<div
v-if="isOpen"
class="fixed inset-0 z-[80]"
@click.stop="isOpen = false"
/>
<div
v-if="isOpen"
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
:style="dropdownMinWidth ? { minWidth: dropdownMinWidth } : undefined"
>
<div
v-for="item in options"
:key="item.value"
class="flex items-center gap-2 px-3 py-1.5 hover:bg-muted/50 cursor-pointer text-xs"
@click="toggle(item.value)"
>
<input
type="checkbox"
:checked="modelValue.includes(item.value)"
class="h-4 w-4 rounded border-border/60 bg-card/80 text-primary shadow-sm accent-primary cursor-pointer"
@click.stop
@change="toggle(item.value)"
>
<span class="text-sm">{{ item.label }}</span>
</div>
<div
v-if="options.length === 0"
class="px-3 py-2 text-sm text-muted-foreground"
>
{{ emptyText }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ChevronDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
export interface MultiSelectOption {
value: string
label: string
}
const props = withDefaults(defineProps<{
modelValue: string[]
options: MultiSelectOption[]
placeholder?: string
emptyText?: string
triggerClass?: string
dropdownMinWidth?: string
disabled?: boolean
}>(), {
placeholder: '请选择',
emptyText: '暂无选项',
triggerClass: '',
dropdownMinWidth: undefined,
disabled: false,
})
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
const isOpen = ref(false)
const displayText = computed(() => {
if (props.modelValue.length === 0) return props.placeholder
if (props.modelValue.length <= 2) {
return props.modelValue
.map(v => props.options.find(o => o.value === v)?.label ?? v)
.join(', ')
}
return `已选择 ${props.modelValue.length}`
})
function toggle(value: string) {
const newValue = [...props.modelValue]
const index = newValue.indexOf(value)
if (index === -1) {
newValue.push(value)
} else {
newValue.splice(index, 1)
}
emit('update:modelValue', newValue)
}
</script>

View File

@@ -10,4 +10,5 @@ export { default as LoadingState } from './LoadingState.vue'
// 表单组件
export { default as ModelMultiSelect } from './ModelMultiSelect.vue'
export { default as MultiSelect } from './MultiSelect.vue'
export { default as TimeRangePicker } from './TimeRangePicker.vue'