feat: optimize model name display in English

This commit is contained in:
zhayujie
2026-05-25 15:09:53 +08:00
parent 40c48a9a61
commit e05f85f3ce
6 changed files with 32 additions and 23 deletions

View File

@@ -367,6 +367,15 @@ function t(key) {
return (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en[key]) || key; return (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en[key]) || key;
} }
// Resolve a localized label that may be either a plain string or
// a {zh, en} object returned by the backend.
function localizedLabel(label) {
if (label && typeof label === 'object') {
return label[currentLang] || label.en || label.zh || '';
}
return label || '';
}
function applyI18n() { function applyI18n() {
document.querySelectorAll('[data-i18n]').forEach(el => { document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.dataset.i18n); el.textContent = t(el.dataset.i18n);
@@ -3164,7 +3173,7 @@ function initConfigView(data) {
configCurrentModel = data.model || ''; configCurrentModel = data.model || '';
const providerEl = document.getElementById('cfg-provider'); const providerEl = document.getElementById('cfg-provider');
const providerOpts = Object.entries(configProviders).map(([pid, p]) => ({ value: pid, label: p.label })); const providerOpts = Object.entries(configProviders).map(([pid, p]) => ({ value: pid, label: localizedLabel(p.label) }));
// if use_linkai is enabled, always select linkai as the provider // if use_linkai is enabled, always select linkai as the provider
// Otherwise prefer bot_type from config, fall back to model-based detection // Otherwise prefer bot_type from config, fall back to model-based detection
@@ -3914,7 +3923,7 @@ function renderVendorChip(p) {
bg-slate-50 dark:bg-white/5 hover:border-primary-300 dark:hover:border-primary-500/50 bg-slate-50 dark:bg-white/5 hover:border-primary-300 dark:hover:border-primary-500/50
cursor-pointer transition-colors duration-150 text-left"> cursor-pointer transition-colors duration-150 text-left">
${renderProviderLogo(p, 28)} ${renderProviderLogo(p, 28)}
<span class="flex-1 min-w-0 text-sm font-medium text-slate-800 dark:text-slate-100 truncate">${escapeHtml(p.label)}</span> <span class="flex-1 min-w-0 text-sm font-medium text-slate-800 dark:text-slate-100 truncate">${escapeHtml(localizedLabel(p.label))}</span>
<i class="fas fa-pen-to-square text-[11px] text-slate-400 dark:text-slate-500 group-hover:text-primary-500 transition-colors"></i> <i class="fas fa-pen-to-square text-[11px] text-slate-400 dark:text-slate-500 group-hover:text-primary-500 transition-colors"></i>
</button>`; </button>`;
} }
@@ -3922,7 +3931,7 @@ function renderVendorChip(p) {
// Render a uniformly-styled logo for a provider. Tries an SVG asset first; if // Render a uniformly-styled logo for a provider. Tries an SVG asset first; if
// it 404s the <img> swaps itself for a monogram fallback via onerror. // it 404s the <img> swaps itself for a monogram fallback via onerror.
function renderProviderLogo(p, sizePx) { function renderProviderLogo(p, sizePx) {
const initial = (p.label || p.id || '?').slice(0, 1).toUpperCase(); const initial = (localizedLabel(p.label) || p.id || '?').slice(0, 1).toUpperCase();
const sz = sizePx || 32; const sz = sizePx || 32;
const url = `${MODELS_PROVIDER_LOGO_PATH}/${encodeURIComponent(p.id)}.svg`; const url = `${MODELS_PROVIDER_LOGO_PATH}/${encodeURIComponent(p.id)}.svg`;
const fallbackId = `pl-${p.id}-${Math.random().toString(36).slice(2, 8)}`; const fallbackId = `pl-${p.id}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3977,7 +3986,7 @@ function renderCapabilityHeaderTag(def, cap) {
function _searchProviderLabel(cap, providerId) { function _searchProviderLabel(cap, providerId) {
const list = (cap && cap.providers) || []; const list = (cap && cap.providers) || [];
const hit = list.find(p => p.id === providerId); const hit = list.find(p => p.id === providerId);
return hit ? hit.label : providerId; return hit ? localizedLabel(hit.label) : providerId;
} }
// Search card body: strategy picker + (when fixed) provider picker + a // Search card body: strategy picker + (when fixed) provider picker + a
@@ -4103,7 +4112,7 @@ function _renderSearchSummary(body, cap) {
class="inline-flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-md cursor-pointer class="inline-flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-md cursor-pointer
bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400
hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors"> hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<i class="fas fa-check text-[10px]"></i>${escapeHtml(p.label)} <i class="fas fa-check text-[10px]"></i>${escapeHtml(localizedLabel(p.label))}
</button> </button>
`).join(''); `).join('');
host.innerHTML = ` host.innerHTML = `
@@ -4150,7 +4159,7 @@ function openSearchAddProviderPicker(missingProviders) {
class="w-full flex items-center justify-between px-3 py-2.5 rounded-lg cursor-pointer class="w-full flex items-center justify-between px-3 py-2.5 rounded-lg cursor-pointer
bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10
text-sm text-slate-700 dark:text-slate-200 transition-colors"> text-sm text-slate-700 dark:text-slate-200 transition-colors">
<span>${escapeHtml(p.label)}</span> <span>${escapeHtml(localizedLabel(p.label))}</span>
<i class="fas fa-chevron-right text-[10px] text-slate-400"></i> <i class="fas fa-chevron-right text-[10px] text-slate-400"></i>
</button> </button>
`).join(''); `).join('');
@@ -4607,7 +4616,7 @@ function renderCapabilityHints(def, cap, body, currentProvider) {
// id ("linkai") when we know it. Falls back to the id when the // id ("linkai") when we know it. Falls back to the id when the
// provider isn't in our vendor table (rare). // provider isn't in our vendor table (rare).
const provMeta = modelsState.providers.find(p => p.id === fbProv); const provMeta = modelsState.providers.find(p => p.id === fbProv);
const fbProvLabel = (provMeta && provMeta.label) || fbProv; const fbProvLabel = (provMeta && localizedLabel(provMeta.label)) || fbProv;
const fbText = fbModel ? `${fbProvLabel} / ${fbModel}` : fbProvLabel; const fbText = fbModel ? `${fbProvLabel} / ${fbModel}` : fbProvLabel;
slot.innerHTML = ` slot.innerHTML = `
<p class="flex items-center gap-1.5 text-xs text-slate-400 dark:text-slate-500 min-w-0"> <p class="flex items-center gap-1.5 text-xs text-slate-400 dark:text-slate-500 min-w-0">
@@ -4639,7 +4648,7 @@ function buildCapabilityProviderOptions(def, cap) {
const configured = !tracked || !!meta.configured; const configured = !tracked || !!meta.configured;
return { return {
value: pid, value: pid,
label: (meta && meta.label) || pid, label: (meta && localizedLabel(meta.label)) || pid,
_tracked: tracked, _tracked: tracked,
_configured: configured, _configured: configured,
}; };
@@ -5069,7 +5078,7 @@ function openVendorModal(providerId, onSaved) {
const pickerEl = document.getElementById('vendor-modal-picker'); const pickerEl = document.getElementById('vendor-modal-picker');
const pickerOpts = modelsState.providers.map(p => ({ const pickerOpts = modelsState.providers.map(p => ({
value: p.id, value: p.id,
label: p.label, label: localizedLabel(p.label),
_configured: !!p.configured, _configured: !!p.configured,
})); }));
initDropdown(pickerEl, pickerOpts, defaultId, (val) => fillVendorModalForProvider(val)); initDropdown(pickerEl, pickerOpts, defaultId, (val) => fillVendorModalForProvider(val));
@@ -5108,7 +5117,7 @@ function openVendorModal(providerId, onSaved) {
function fillVendorModalForProvider(providerId) { function fillVendorModalForProvider(providerId) {
const meta = modelsState.providers.find(p => p.id === providerId); const meta = modelsState.providers.find(p => p.id === providerId);
if (!meta) return; if (!meta) return;
document.getElementById('vendor-modal-title').textContent = meta.label; document.getElementById('vendor-modal-title').textContent = localizedLabel(meta.label);
document.getElementById('vendor-modal-subtitle').textContent = meta.id; document.getElementById('vendor-modal-subtitle').textContent = meta.id;
// ----- API Base ----- // ----- API Base -----

View File

@@ -1337,7 +1337,7 @@ class ConfigHandler:
"models": [const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o], "models": [const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o],
}), }),
("zhipu", { ("zhipu", {
"label": "智谱AI", "label": {"zh": "智谱AI", "en": "GLM"},
"api_key_field": "zhipu_ai_api_key", "api_key_field": "zhipu_ai_api_key",
"api_base_key": "zhipu_ai_api_base", "api_base_key": "zhipu_ai_api_base",
"api_base_default": "https://open.bigmodel.cn/api/paas/v4", "api_base_default": "https://open.bigmodel.cn/api/paas/v4",
@@ -1345,7 +1345,7 @@ class ConfigHandler:
"models": [const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7], "models": [const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7],
}), }),
("dashscope", { ("dashscope", {
"label": "通义千问", "label": {"zh": "通义千问", "en": "Qwen"},
"api_key_field": "dashscope_api_key", "api_key_field": "dashscope_api_key",
"api_base_key": None, "api_base_key": None,
"api_base_default": None, "api_base_default": None,
@@ -1353,7 +1353,7 @@ class ConfigHandler:
"models": [const.QWEN36_PLUS, const.QWEN37_MAX, const.QWEN35_PLUS, const.QWEN3_MAX], "models": [const.QWEN36_PLUS, const.QWEN37_MAX, const.QWEN35_PLUS, const.QWEN3_MAX],
}), }),
("doubao", { ("doubao", {
"label": "豆包", "label": {"zh": "豆包", "en": "Doubao"},
"api_key_field": "ark_api_key", "api_key_field": "ark_api_key",
"api_base_key": "ark_base_url", "api_base_key": "ark_base_url",
"api_base_default": "https://ark.cn-beijing.volces.com/api/v3", "api_base_default": "https://ark.cn-beijing.volces.com/api/v3",
@@ -1369,7 +1369,7 @@ class ConfigHandler:
"models": [const.KIMI_K2_6, const.KIMI_K2_5, const.KIMI_K2], "models": [const.KIMI_K2_6, const.KIMI_K2_5, const.KIMI_K2],
}), }),
("qianfan", { ("qianfan", {
"label": "百度千帆", "label": {"zh": "百度千帆", "en": "ERNIE"},
"api_key_field": "qianfan_api_key", "api_key_field": "qianfan_api_key",
"api_base_key": "qianfan_api_base", "api_base_key": "qianfan_api_base",
"api_base_default": "https://qianfan.baidubce.com/v2", "api_base_default": "https://qianfan.baidubce.com/v2",
@@ -1385,7 +1385,7 @@ class ConfigHandler:
"models": _RECOMMENDED_MODELS, "models": _RECOMMENDED_MODELS,
}), }),
("custom", { ("custom", {
"label": "自定义", "label": {"zh": "自定义", "en": "Custom"},
"api_key_field": "custom_api_key", "api_key_field": "custom_api_key",
"api_base_key": "custom_api_base", "api_base_key": "custom_api_base",
"api_base_default": "", "api_base_default": "",
@@ -2241,10 +2241,10 @@ class ModelsHandler:
_SEARCH_PROVIDERS = ("bocha", "qianfan", "zhipu", "linkai") _SEARCH_PROVIDERS = ("bocha", "qianfan", "zhipu", "linkai")
_SEARCH_PROVIDER_LABELS = { _SEARCH_PROVIDER_LABELS = {
"bocha": "博查", "bocha": {"zh": "博查", "en": "Bocha"},
"zhipu": "智谱", "zhipu": {"zh": "智谱", "en": "GLM"},
"qianfan": "百度千帆", "qianfan": {"zh": "百度千帆", "en": "ERNIE"},
"linkai": "LinkAI", "linkai": {"zh": "LinkAI", "en": "LinkAI"},
} }
@classmethod @classmethod

View File

@@ -32,6 +32,6 @@ A snapshot of each vendor's capabilities. "Text" refers to the main chat model;
**Option 1 (recommended):** Manage models and capabilities online via the [Web console](/en/channels/web), with no need to edit the configuration file: **Option 1 (recommended):** Manage models and capabilities online via the [Web console](/en/channels/web), with no need to edit the configuration file:
<img width="900" src="https://cdn.link-ai.tech/doc/20260521212527.png" /> <img width="900" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-models-config.png" />
**Option 2:** Edit `config.json` manually and fill in the model name and API key for the selected vendor. Every model also supports OpenAI-compatible access — just set `bot_type` to `openai` and configure `open_ai_api_base` and `open_ai_api_key`. **Option 2:** Edit `config.json` manually and fill in the model name and API key for the selected vendor. Every model also supports OpenAI-compatible access — just set `bot_type` to `openai` and configure `open_ai_api_base` and `open_ai_api_key`.

View File

@@ -15,7 +15,7 @@ The Web Console adds a new **Models** page that organizes everything by **provid
Documentation: [Models Overview](https://docs.cowagent.ai/en/models) Documentation: [Models Overview](https://docs.cowagent.ai/en/models)
<img width="720" alt="20260522113305" src="https://cdn.link-ai.tech/doc/20260522113305.png" /> <img width="720" alt="20260522113305" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-models-config.png" />
## 🧩 MCP Protocol Support ## 🧩 MCP Protocol Support

View File

@@ -15,7 +15,7 @@ Web コンソールに「モデル」ページを新設。**モデルプロバ
ドキュメント:[モデル概要](https://docs.cowagent.ai/ja/models) ドキュメント:[モデル概要](https://docs.cowagent.ai/ja/models)
<img width="720" alt="20260522113305" src="https://cdn.link-ai.tech/doc/20260522113305.png" /> <img width="720" alt="20260522113305" src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/en/web-console-models-config.png" />
## 🧩 MCP プロトコル対応 ## 🧩 MCP プロトコル対応

View File

@@ -227,7 +227,7 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
## 🏢 企业服务 ## 🏢 企业服务
<a href="https://link-ai.tech" target="_blank"><img width="550" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a> <a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a>
> [LinkAI](https://link-ai.tech/) 是面向企业和个人的一站式 AI 智能体平台,为 CowAgent 提供云端托管和企业级支持: > [LinkAI](https://link-ai.tech/) 是面向企业和个人的一站式 AI 智能体平台,为 CowAgent 提供云端托管和企业级支持:
> >