feat: Claude Console账号增强,升级模型支持列表为模型映射表!

This commit is contained in:
KevinLiao
2025-07-30 23:13:59 +08:00
parent acf8dbb970
commit 3c797a85e0
16 changed files with 289 additions and 102 deletions

View File

@@ -25,7 +25,7 @@ class ClaudeConsoleAccountService {
apiUrl = '',
apiKey = '',
priority = 50, // 默认优先级501-100
supportedModels = [], // 支持的模型列表,空数组表示支持所有
supportedModels = [], // 支持的模型列表或映射表,空数组/对象表示支持所有
userAgent = 'claude-cli/1.0.61 (console, cli)',
rateLimitDuration = 60, // 限流时间(分钟)
proxy = null,
@@ -41,6 +41,9 @@ class ClaudeConsoleAccountService {
const accountId = uuidv4();
// 处理 supportedModels确保向后兼容
const processedModels = this._processModelMapping(supportedModels);
const accountData = {
id: accountId,
platform: 'claude-console',
@@ -49,7 +52,7 @@ class ClaudeConsoleAccountService {
apiUrl: apiUrl,
apiKey: this._encryptSensitiveData(apiKey),
priority: priority.toString(),
supportedModels: JSON.stringify(supportedModels),
supportedModels: JSON.stringify(processedModels),
userAgent,
rateLimitDuration: rateLimitDuration.toString(),
proxy: proxy ? JSON.stringify(proxy) : '',
@@ -209,7 +212,9 @@ class ClaudeConsoleAccountService {
if (updates.priority !== undefined) updatedData.priority = updates.priority.toString();
if (updates.supportedModels !== undefined) {
logger.debug(`[DEBUG] Updating supportedModels: ${JSON.stringify(updates.supportedModels)}`);
updatedData.supportedModels = JSON.stringify(updates.supportedModels);
// 处理 supportedModels,确保向后兼容
const processedModels = this._processModelMapping(updates.supportedModels);
updatedData.supportedModels = JSON.stringify(processedModels);
}
if (updates.userAgent !== undefined) updatedData.userAgent = updates.userAgent;
if (updates.rateLimitDuration !== undefined) updatedData.rateLimitDuration = updates.rateLimitDuration.toString();
@@ -488,6 +493,55 @@ class ClaudeConsoleAccountService {
minutesRemaining: 0
};
}
// 🔄 处理模型映射,确保向后兼容
_processModelMapping(supportedModels) {
// 如果是空值,返回空对象(支持所有模型)
if (!supportedModels || (Array.isArray(supportedModels) && supportedModels.length === 0)) {
return {};
}
// 如果已经是对象格式(新的映射表格式),直接返回
if (typeof supportedModels === 'object' && !Array.isArray(supportedModels)) {
return supportedModels;
}
// 如果是数组格式(旧格式),转换为映射表
if (Array.isArray(supportedModels)) {
const mapping = {};
supportedModels.forEach(model => {
if (model && typeof model === 'string') {
mapping[model] = model; // 映射到自身
}
});
return mapping;
}
// 其他情况返回空对象
return {};
}
// 🔍 检查模型是否支持(用于调度)
isModelSupported(modelMapping, requestedModel) {
// 如果映射表为空,支持所有模型
if (!modelMapping || Object.keys(modelMapping).length === 0) {
return true;
}
// 检查请求的模型是否在映射表的键中
return Object.prototype.hasOwnProperty.call(modelMapping, requestedModel);
}
// 🔄 获取映射后的模型名称
getMappedModel(modelMapping, requestedModel) {
// 如果映射表为空,返回原模型
if (!modelMapping || Object.keys(modelMapping).length === 0) {
return requestedModel;
}
// 返回映射后的模型,如果不存在则返回原模型
return modelMapping[requestedModel] || requestedModel;
}
}
module.exports = new ClaudeConsoleAccountService();

View File

@@ -25,6 +25,22 @@ class ClaudeConsoleRelayService {
logger.debug(`🔑 Account has apiKey: ${!!account.apiKey}`);
logger.debug(`📝 Request model: ${requestBody.model}`);
// 处理模型映射
let mappedModel = requestBody.model;
if (account.supportedModels && typeof account.supportedModels === 'object' && !Array.isArray(account.supportedModels)) {
const newModel = claudeConsoleAccountService.getMappedModel(account.supportedModels, requestBody.model);
if (newModel !== requestBody.model) {
logger.info(`🔄 Mapping model from ${requestBody.model} to ${newModel}`);
mappedModel = newModel;
}
}
// 创建修改后的请求体
const modifiedRequestBody = {
...requestBody,
model: mappedModel
};
// 模型兼容性检查已经在调度器中完成,这里不需要再检查
// 创建代理agent
@@ -67,7 +83,7 @@ class ClaudeConsoleRelayService {
const requestConfig = {
method: 'POST',
url: apiEndpoint,
data: requestBody,
data: modifiedRequestBody,
headers: {
'Content-Type': 'application/json',
'x-api-key': account.apiKey,

View File

@@ -174,10 +174,20 @@ class UnifiedClaudeScheduler {
account.schedulable !== false) { // 检查是否可调度
// 检查模型支持(如果有请求的模型)
if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
if (!account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
if (requestedModel && account.supportedModels) {
// 兼容旧格式(数组)和新格式(对象)
if (Array.isArray(account.supportedModels)) {
// 旧格式:数组
if (account.supportedModels.length > 0 && !account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
}
} else if (typeof account.supportedModels === 'object') {
// 新格式:映射表
if (Object.keys(account.supportedModels).length > 0 && !claudeConsoleAccountService.isModelSupported(account.supportedModels, requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{c as b,r as x,q as f,x as a,z as s,L as i,Q as y,u as o,P as m,Y as _,K as u,aq as c,O as g,y as n}from"./vue-vendor-CKToUHZx.js";import{_ as v,u as w}from"./index-COOF1SF1.js";/* empty css */import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const h={class:"flex items-center justify-center min-h-screen p-6"},k={class:"glass-strong rounded-3xl p-10 w-full max-w-md shadow-2xl"},L={class:"text-center mb-8"},S={class:"w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden"},V=["src"],I={key:1,class:"fas fa-cloud text-3xl text-gray-700"},N={key:1,class:"w-12 h-12 bg-gray-300/50 rounded animate-pulse"},q={key:0,class:"text-3xl font-bold text-white mb-2 header-title"},D={key:1,class:"h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"},E=["disabled"],j={key:0,class:"fas fa-sign-in-alt mr-2"},B={key:1,class:"loading-spinner mr-2"},M={key:0,class:"mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm"},F={__name:"LoginView",setup(O){const e=w(),d=b(()=>e.oemLoading),l=x({username:"",password:""});f(()=>{e.loadOemSettings()});const p=async()=>{await e.login(l.value)};return(T,t)=>(n(),a("div",h,[s("div",k,[s("div",L,[s("div",S,[d.value?(n(),a("div",N)):(n(),a(y,{key:0},[o(e).oemSettings.siteIconData||o(e).oemSettings.siteIcon?(n(),a("img",{key:0,src:o(e).oemSettings.siteIconData||o(e).oemSettings.siteIcon,alt:"Logo",class:"w-12 h-12 object-contain",onError:t[0]||(t[0]=r=>r.target.style.display="none")},null,40,V)):(n(),a("i",I))],64))]),!d.value&&o(e).oemSettings.siteName?(n(),a("h1",q,m(o(e).oemSettings.siteName),1)):d.value?(n(),a("div",D)):i("",!0),t[3]||(t[3]=s("p",{class:"text-gray-600 text-lg"}," 管理后台 ",-1))]),s("form",{class:"space-y-6",onSubmit:_(p,["prevent"])},[s("div",null,[t[4]||(t[4]=s("label",{class:"block text-sm font-semibold text-gray-900 mb-3"},"用户名",-1)),u(s("input",{"onUpdate:modelValue":t[1]||(t[1]=r=>l.value.username=r),type:"text",required:"",class:"form-input w-full",placeholder:"请输入用户名"},null,512),[[c,l.value.username]])]),s("div",null,[t[5]||(t[5]=s("label",{class:"block text-sm font-semibold text-gray-900 mb-3"},"密码",-1)),u(s("input",{"onUpdate:modelValue":t[2]||(t[2]=r=>l.value.password=r),type:"password",required:"",class:"form-input w-full",placeholder:"请输入密码"},null,512),[[c,l.value.password]])]),s("button",{type:"submit",disabled:o(e).loginLoading,class:"btn btn-primary w-full py-4 px-6 text-lg font-semibold"},[o(e).loginLoading?i("",!0):(n(),a("i",j)),o(e).loginLoading?(n(),a("div",B)):i("",!0),g(" "+m(o(e).loginLoading?"登录中...":"登录"),1)],8,E)],32),o(e).loginError?(n(),a("div",M,[t[6]||(t[6]=s("i",{class:"fas fa-exclamation-triangle mr-2"},null,-1)),g(m(o(e).loginError),1)])):i("",!0)])]))}},P=v(F,[["__scopeId","data-v-82195a01"]]);export{P as default};

View File

@@ -1 +0,0 @@
/* empty css */import{_ as r}from"./index-COOF1SF1.js";import{x as t,y as s,z as o,Q as d,L as a,A as c,C as g,P as i}from"./vue-vendor-CKToUHZx.js";const u={class:"flex items-center gap-4"},f={class:"w-12 h-12 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl flex items-center justify-center backdrop-blur-sm flex-shrink-0 overflow-hidden"},y=["src"],m={key:1,class:"fas fa-cloud text-xl text-gray-700"},h={key:1,class:"w-8 h-8 bg-gray-300/50 rounded animate-pulse"},x={class:"flex flex-col justify-center min-h-[48px]"},b={class:"flex items-center gap-3"},k={key:1,class:"h-8 w-64 bg-gray-300/50 rounded animate-pulse"},_={key:0,class:"text-gray-600 text-sm leading-tight mt-0.5"},S={__name:"LogoTitle",props:{loading:{type:Boolean,default:!1},title:{type:String,default:""},subtitle:{type:String,default:""},logoSrc:{type:String,default:""},titleClass:{type:String,default:"text-gray-900"}},setup(e){const n=l=>{l.target.style.display="none"};return(l,p)=>(s(),t("div",u,[o("div",f,[e.loading?(s(),t("div",h)):(s(),t(d,{key:0},[e.logoSrc?(s(),t("img",{key:0,src:e.logoSrc,alt:"Logo",class:"w-8 h-8 object-contain",onError:n},null,40,y)):(s(),t("i",m))],64))]),o("div",x,[o("div",b,[!e.loading&&e.title?(s(),t("h1",{key:0,class:g(["text-2xl font-bold header-title leading-tight",e.titleClass])},i(e.title),3)):e.loading?(s(),t("div",k)):a("",!0),c(l.$slots,"after-title",{},void 0,!0)]),e.subtitle?(s(),t("p",_,i(e.subtitle),1)):a("",!0)])]))}},C=r(S,[["__scopeId","data-v-718feedc"]]);export{C as L};

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
import{aR as k,r as x,aW as C,q as O,x as m,z as e,u as i,K as T,aq as N,L as _,O as v,C as j,P as S,y as g}from"./vue-vendor-CKToUHZx.js";import{s as c}from"./toast-BvwA7Mwb.js";import{a as D,_ as F}from"./index-COOF1SF1.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const E=k("settings",()=>{const l=x({siteName:"Claude Relay Service",siteIcon:"",siteIconData:"",updatedAt:null}),r=x(!1),p=x(!1),d=async()=>{r.value=!0;try{const s=await D.get("/admin/oem-settings");return s&&s.success&&(l.value={...l.value,...s.data},f()),s}catch(s){throw console.error("Failed to load OEM settings:",s),s}finally{r.value=!1}},a=async s=>{p.value=!0;try{const o=await D.put("/admin/oem-settings",s);return o&&o.success&&(l.value={...l.value,...o.data},f()),o}catch(o){throw console.error("Failed to save OEM settings:",o),o}finally{p.value=!1}},w=async()=>{const s={siteName:"Claude Relay Service",siteIcon:"",siteIconData:"",updatedAt:null};return l.value={...s},await a(s)},f=()=>{if(l.value.siteName&&(document.title=`${l.value.siteName} - 管理后台`),l.value.siteIconData||l.value.siteIcon){const s=document.querySelector('link[rel="icon"]')||document.createElement("link");s.rel="icon",s.href=l.value.siteIconData||l.value.siteIcon,document.querySelector('link[rel="icon"]')||document.head.appendChild(s)}};return{oemSettings:l,loading:r,saving:p,loadOemSettings:d,saveOemSettings:a,resetOemSettings:w,applyOemSettings:f,formatDateTime:s=>s?new Date(s).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"",validateIconFile:s=>{const o=[];return s.size>350*1024&&o.push("图标文件大小不能超过 350KB"),["image/x-icon","image/png","image/jpeg","image/jpg","image/svg+xml"].includes(s.type)||o.push("不支持的文件类型,请选择 .ico, .png, .jpg 或 .svg 文件"),{isValid:o.length===0,errors:o}},fileToBase64:s=>new Promise((o,n)=>{const t=new FileReader;t.onload=u=>o(u.target.result),t.onerror=n,t.readAsDataURL(s)})}}),B={class:"settings-container"},V={class:"card p-6"},R={key:0,class:"text-center py-12"},M={key:1,class:"table-container"},A={class:"min-w-full"},q={class:"divide-y divide-gray-200/50"},z={class:"table-row"},K={class:"px-6 py-4"},L={class:"table-row"},U={class:"px-6 py-4"},$={class:"space-y-3"},P={key:0,class:"inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"},W=["src"],G={class:"px-6 py-6",colspan:"2"},H={class:"flex items-center justify-between"},J={class:"flex gap-3"},Q=["disabled"],X={key:0,class:"loading-spinner mr-2"},Y={key:1,class:"fas fa-save mr-2"},Z=["disabled"],ee={key:0,class:"text-sm text-gray-500"},te={__name:"SettingsView",setup(l){const r=E(),{loading:p,saving:d,oemSettings:a}=C(r),w=x();O(async()=>{try{await r.loadOemSettings()}catch{c("加载设置失败","error")}});const f=async()=>{try{const n={siteName:a.value.siteName,siteIcon:a.value.siteIcon,siteIconData:a.value.siteIconData},t=await r.saveOemSettings(n);t&&t.success?c("OEM设置保存成功","success"):c((t==null?void 0:t.message)||"保存失败","error")}catch{c("保存OEM设置失败","error")}},b=async()=>{if(confirm(`确定要重置为默认设置吗?
这将清除所有自定义的网站名称和图标设置。`))try{const n=await r.resetOemSettings();n&&n.success?c("已重置为默认设置","success"):c("重置失败","error")}catch{c("重置失败","error")}},h=async n=>{const t=n.target.files[0];if(!t)return;const u=r.validateIconFile(t);if(!u.isValid){u.errors.forEach(y=>c(y,"error"));return}try{const y=await r.fileToBase64(t);a.value.siteIconData=y}catch{c("文件读取失败","error")}n.target.value=""},I=()=>{a.value.siteIcon="",a.value.siteIconData=""},s=()=>{console.warn("Icon failed to load")},o=r.formatDateTime;return(n,t)=>(g(),m("div",B,[e("div",V,[t[12]||(t[12]=e("div",{class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},[e("div",null,[e("h3",{class:"text-xl font-bold text-gray-900 mb-2"}," 其他设置 "),e("p",{class:"text-gray-600"}," 自定义网站名称和图标 ")])],-1)),i(p)?(g(),m("div",R,t[2]||(t[2]=[e("div",{class:"loading-spinner mx-auto mb-4"},null,-1),e("p",{class:"text-gray-500"}," 正在加载设置... ",-1)]))):(g(),m("div",M,[e("table",A,[e("tbody",q,[e("tr",z,[t[4]||(t[4]=e("td",{class:"px-6 py-4 whitespace-nowrap w-48"},[e("div",{class:"flex items-center"},[e("div",{class:"w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-font text-white text-xs"})]),e("div",null,[e("div",{class:"text-sm font-semibold text-gray-900"}," 网站名称 "),e("div",{class:"text-xs text-gray-500"}," 品牌标识 ")])])],-1)),e("td",K,[T(e("input",{"onUpdate:modelValue":t[0]||(t[0]=u=>i(a).siteName=u),type:"text",class:"form-input w-full max-w-md",placeholder:"Claude Relay Service",maxlength:"100"},null,512),[[N,i(a).siteName]]),t[3]||(t[3]=e("p",{class:"text-xs text-gray-500 mt-1"}," 将显示在浏览器标题和页面头部 ",-1))])]),e("tr",L,[t[9]||(t[9]=e("td",{class:"px-6 py-4 whitespace-nowrap w-48"},[e("div",{class:"flex items-center"},[e("div",{class:"w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-image text-white text-xs"})]),e("div",null,[e("div",{class:"text-sm font-semibold text-gray-900"}," 网站图标 "),e("div",{class:"text-xs text-gray-500"}," Favicon ")])])],-1)),e("td",U,[e("div",$,[i(a).siteIconData||i(a).siteIcon?(g(),m("div",P,[e("img",{src:i(a).siteIconData||i(a).siteIcon,alt:"图标预览",class:"w-8 h-8",onError:s},null,40,W),t[6]||(t[6]=e("span",{class:"text-sm text-gray-600"},"当前图标",-1)),e("button",{class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors",onClick:I},t[5]||(t[5]=[e("i",{class:"fas fa-trash mr-1"},null,-1),v("删除 ",-1)]))])):_("",!0),e("div",null,[e("input",{ref_key:"iconFileInput",ref:w,type:"file",accept:".ico,.png,.jpg,.jpeg,.svg",class:"hidden",onChange:h},null,544),e("button",{class:"btn btn-success px-4 py-2",onClick:t[1]||(t[1]=u=>n.$refs.iconFileInput.click())},t[7]||(t[7]=[e("i",{class:"fas fa-upload mr-2"},null,-1),v(" 上传图标 ",-1)])),t[8]||(t[8]=e("span",{class:"text-xs text-gray-500 ml-3"},"支持 .ico, .png, .jpg, .svg 格式,最大 350KB",-1))])])])]),e("tr",null,[e("td",G,[e("div",H,[e("div",J,[e("button",{disabled:i(d),class:j(["btn btn-primary px-6 py-3",{"opacity-50 cursor-not-allowed":i(d)}]),onClick:f},[i(d)?(g(),m("div",X)):(g(),m("i",Y)),v(" "+S(i(d)?"保存中...":"保存设置"),1)],10,Q),e("button",{class:"btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3",disabled:i(d),onClick:b},t[10]||(t[10]=[e("i",{class:"fas fa-undo mr-2"},null,-1),v(" 重置为默认 ",-1)]),8,Z)]),i(a).updatedAt?(g(),m("div",ee,[t[11]||(t[11]=e("i",{class:"fas fa-clock mr-1"},null,-1)),v(" 最后更新:"+S(i(o)(i(a).updatedAt)),1)])):_("",!0)])])])])])]))])]))}},le=F(te,[["__scopeId","data-v-d29d5f49"]]);export{le as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -18,12 +18,12 @@
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<script type="module" crossorigin src="/admin-next/assets/index-COOF1SF1.js"></script>
<script type="module" crossorigin src="/admin-next/assets/index-Ch5822Og.js"></script>
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
<link rel="stylesheet" crossorigin href="/admin-next/assets/element-plus-CPnoEkWW.css">
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-Ba0i43MQ.css">
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-CANUYAyV.css">
</head>
<body>
<div id="app"></div>

View File

@@ -250,36 +250,92 @@
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">支持的模型 (可选)--注意,ClaudeCode必须加上hiku模型</label>
<div class="mb-2 flex gap-2">
<label class="block text-sm font-semibold text-gray-700 mb-3">模型映射表 (可选)</label>
<div class="bg-blue-50 p-3 rounded-lg mb-3">
<p class="text-xs text-blue-700">
<i class="fas fa-info-circle mr-1" />
留空表示支持所有模型且不修改请求配置映射后左侧模型会被识别为支持的模型右侧是实际发送的模型
</p>
</div>
<!-- 模型映射表 -->
<div class="space-y-2 mb-3">
<div
v-for="(mapping, index) in modelMappings"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="mapping.from"
type="text"
class="form-input flex-1"
placeholder="原始模型名称"
>
<i class="fas fa-arrow-right text-gray-400" />
<input
v-model="mapping.to"
type="text"
class="form-input flex-1"
placeholder="映射后的模型名称"
>
<button
type="button"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
@click="removeModelMapping(index)"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
<!-- 添加映射按钮 -->
<button
type="button"
class="w-full px-4 py-2 border-2 border-dashed border-gray-300 text-gray-600 rounded-lg hover:border-gray-400 hover:text-gray-700 transition-colors"
@click="addModelMapping"
>
<i class="fas fa-plus mr-2" />
添加模型映射
</button>
<!-- 快捷添加按钮 -->
<div class="mt-3 flex flex-wrap gap-2">
<button
type="button"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
@click="addPresetModel('claude-sonnet-4-20250514')"
@click="addPresetMapping('claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20241022')"
>
+ claude-sonnet-4-20250514
+ Sonnet 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-opus-4-20250514')"
@click="addPresetMapping('claude-3-opus-20240229', 'claude-3-opus-20240229')"
>
+ claude-opus-4-20250514
+ Opus 3
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-3-5-haiku-20241022')"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors"
@click="addPresetMapping('claude-3-5-haiku-20241022', 'claude-3-5-haiku-20241022')"
>
+ claude-3-5-haiku-20241022
+ Haiku 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-orange-100 text-orange-700 rounded-lg hover:bg-orange-200 transition-colors"
@click="addPresetMapping('claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022')"
>
+ Sonnet 4 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
@click="addPresetMapping('claude-opus-4-20250514', 'claude-3-opus-20240229')"
>
+ Opus 4 3
</button>
</div>
<textarea
v-model="form.supportedModels"
rows="3"
class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型"
/>
<p class="text-xs text-gray-500 mt-1">
留空表示支持所有模型如果指定模型请求中的模型不在列表内将不会调度到此账号
</p>
@@ -565,36 +621,92 @@
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">支持的模型 (可选)</label>
<div class="mb-2 flex gap-2">
<label class="block text-sm font-semibold text-gray-700 mb-3">模型映射表 (可选)</label>
<div class="bg-blue-50 p-3 rounded-lg mb-3">
<p class="text-xs text-blue-700">
<i class="fas fa-info-circle mr-1" />
留空表示支持所有模型且不修改请求配置映射后左侧模型会被识别为支持的模型右侧是实际发送的模型
</p>
</div>
<!-- 模型映射表 -->
<div class="space-y-2 mb-3">
<div
v-for="(mapping, index) in modelMappings"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="mapping.from"
type="text"
class="form-input flex-1"
placeholder="原始模型名称"
>
<i class="fas fa-arrow-right text-gray-400" />
<input
v-model="mapping.to"
type="text"
class="form-input flex-1"
placeholder="映射后的模型名称"
>
<button
type="button"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
@click="removeModelMapping(index)"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
<!-- 添加映射按钮 -->
<button
type="button"
class="w-full px-4 py-2 border-2 border-dashed border-gray-300 text-gray-600 rounded-lg hover:border-gray-400 hover:text-gray-700 transition-colors"
@click="addModelMapping"
>
<i class="fas fa-plus mr-2" />
添加模型映射
</button>
<!-- 快捷添加按钮 -->
<div class="mt-3 flex flex-wrap gap-2">
<button
type="button"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
@click="addPresetModel('claude-sonnet-4-20250514')"
@click="addPresetMapping('claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20241022')"
>
+ claude-sonnet-4-20250514
+ Sonnet 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-opus-4-20250514')"
@click="addPresetMapping('claude-3-opus-20240229', 'claude-3-opus-20240229')"
>
+ claude-opus-4-20250514
+ Opus 3
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-3-5-haiku-20241022')"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors"
@click="addPresetMapping('claude-3-5-haiku-20241022', 'claude-3-5-haiku-20241022')"
>
+ claude-3-5-haiku-20241022
+ Haiku 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-orange-100 text-orange-700 rounded-lg hover:bg-orange-200 transition-colors"
@click="addPresetMapping('claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022')"
>
+ Sonnet 4 3.5
</button>
<button
type="button"
class="px-3 py-1 text-xs bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
@click="addPresetMapping('claude-opus-4-20250514', 'claude-3-opus-20240229')"
>
+ Opus 4 3
</button>
</div>
<textarea
v-model="form.supportedModels"
rows="3"
class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型"
/>
</div>
<div>
@@ -770,11 +882,32 @@ const form = ref({
apiUrl: props.account?.apiUrl || '',
apiKey: props.account?.apiKey || '',
priority: props.account?.priority || 50,
supportedModels: props.account?.supportedModels?.join('\n') || '',
userAgent: props.account?.userAgent || '',
rateLimitDuration: props.account?.rateLimitDuration || 60
})
// 模型映射表数据
const modelMappings = ref([])
// 初始化模型映射表
const initModelMappings = () => {
if (props.account?.supportedModels) {
// 如果是对象格式(新的映射表)
if (typeof props.account.supportedModels === 'object' && !Array.isArray(props.account.supportedModels)) {
modelMappings.value = Object.entries(props.account.supportedModels).map(([from, to]) => ({
from,
to
}))
} else if (Array.isArray(props.account.supportedModels)) {
// 如果是数组格式(旧格式),转换为映射表
modelMappings.value = props.account.supportedModels.map(model => ({
from: model,
to: model
}))
}
}
}
// 表单验证错误
const errors = ref({
name: '',
@@ -955,9 +1088,7 @@ const createAccount = async () => {
data.apiUrl = form.value.apiUrl
data.apiKey = form.value.apiKey
data.priority = form.value.priority || 50
data.supportedModels = form.value.supportedModels
? form.value.supportedModels.split('\n').filter(m => m.trim())
: []
data.supportedModels = convertMappingsToObject() || {}
data.userAgent = form.value.userAgent || null
data.rateLimitDuration = form.value.rateLimitDuration || 60
}
@@ -1067,9 +1198,7 @@ const updateAccount = async () => {
data.apiKey = form.value.apiKey
}
data.priority = form.value.priority || 50
data.supportedModels = form.value.supportedModels
? form.value.supportedModels.split('\n').filter(m => m.trim())
: []
data.supportedModels = convertMappingsToObject() || {}
data.userAgent = form.value.userAgent || null
data.rateLimitDuration = form.value.rateLimitDuration || 60
}
@@ -1125,28 +1254,44 @@ watch(() => form.value.platform, (newPlatform) => {
}
})
// 添加预设模型
const addPresetModel = (modelName) => {
// 获取当前模型列表
const currentModels = form.value.supportedModels
? form.value.supportedModels.split('\n').filter(m => m.trim())
: []
// 检查是否已存在
if (currentModels.includes(modelName)) {
showToast(`模型 ${modelName} 已存在`, 'info')
// 添加模型映射
const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' })
}
// 移除模型映射
const removeModelMapping = (index) => {
modelMappings.value.splice(index, 1)
}
// 添加预设映射
const addPresetMapping = (from, to) => {
// 检查是否已存在相同的映射
const exists = modelMappings.value.some(mapping => mapping.from === from)
if (exists) {
showToast(`模型 ${from} 的映射已存在`, 'info')
return
}
// 添加到列表
currentModels.push(modelName)
form.value.supportedModels = currentModels.join('\n')
showToast(`已添加模型 ${modelName}`, 'success')
modelMappings.value.push({ from, to })
showToast(`已添加映射: ${from}${to}`, 'success')
}
// 将模型映射表转换为对象格式
const convertMappingsToObject = () => {
const mapping = {}
modelMappings.value.forEach(item => {
if (item.from && item.to) {
mapping[item.from] = item.to
}
})
return Object.keys(mapping).length > 0 ? mapping : null
}
// 监听账户变化,更新表单
watch(() => props.account, (newAccount) => {
if (newAccount) {
initModelMappings()
// 重新初始化代理配置
const proxyConfig = newAccount.proxy && newAccount.proxy.host && newAccount.proxy.port
? {
@@ -1180,10 +1325,12 @@ watch(() => props.account, (newAccount) => {
apiUrl: newAccount.apiUrl || '',
apiKey: '', // 编辑模式不显示现有的 API Key
priority: newAccount.priority || 50,
supportedModels: newAccount.supportedModels?.join('\n') || '',
userAgent: newAccount.userAgent || '',
rateLimitDuration: newAccount.rateLimitDuration || 60
}
}
}, { immediate: true })
// 初始化时调用
initModelMappings()
</script>