refactor(models): address PR #232 review comments

- i18n: Extract hardcoded Chinese strings to translation keys
- fix(url): Restore full-width punctuation in URL regex
- chore: Remove linux-schema.json (build artifact)
- chore: Add linux-schema.json to .gitignore
- refactor: Unify save strategy to use autoSave consistently
- style: Add trailing newline to en.json

Closes review items 1-6 from PR #232
This commit is contained in:
friendfish
2026-04-21 01:00:19 +08:00
parent 74793d81a4
commit f61cb65b4a
5 changed files with 34 additions and 2623 deletions

2
src/locales/en.json Normal file → Executable file
View File

@@ -2201,4 +2201,4 @@
"confirmOk": "OK",
"confirmCancel": "Cancel"
}
}
}

14
src/locales/modules/models.js Normal file → Executable file
View File

@@ -153,4 +153,18 @@ export default {
restartFailed: _('重启失败', 'Restart failed', '重啟失敗'),
configSavedGwFailed: _('配置已保存,但 Gateway 重启失败', 'Config saved, but Gateway restart failed', '設定已儲存,但 Gateway 重啟失敗'),
visitSite: _('访问 {name}官网', 'Visit {name} website', '訪問 {name}官網'),
// 瀑布流编辑器新增 keys
systemModelTitle: _('系统主/备模型', 'System Primary/Backup Models', '系統主/備模型'),
activeChainTitle: _('当前生效链 (支持拖拽排序)', 'Active Chain (drag to reorder)', '目前生效鏈 (支援拖曳排序)'),
candidatePoolTitle: _('可用候选池 (按服务商分组)', 'Candidate Pool (grouped by provider)', '可用候選池 (按服務商分組)'),
noCandidateModel: _('无可用候选模型', 'No candidate models available', '無可用候選模型'),
bestPracticeHint: _('💡 最佳实践:建议备选模型保持在 2-3 款并分布在不同服务商,以平衡可用性与延迟。', '💡 Best practice: Keep 2-3 fallback models distributed across different providers to balance availability and latency.', '💡 最佳實踐:建議備選模型保持在 2-3 款並分布在不同服務商,以平衡可用性與延遲。'),
setAsPrimarySuccess: _('已将 {model} 设为主模型', 'Set {model} as primary model', '已將 {model} 設為主模型'),
noFallbackSelected: _('尚未选择备选模型', 'No fallback models selected', '尚未選擇備選模型'),
cancel: _('取消', 'Cancel', '取消'),
saveAndApply: _('保存并应用', 'Save and Apply', '儲存並套用'),
nFallbacks: _('{count} 个备选', '{count} fallback(s)', '{count} 個備選'),
setAsPrimary: _('设为主用', 'Set as Primary', '設為主用'),
remove: _('移除', 'Remove', '移除'),
add: _('加入', 'Add', '加入'),
}

32
src/pages/models.js Normal file → Executable file
View File

@@ -125,10 +125,10 @@ function renderDefaultBar(page, state) {
<div class="config-section-title" id="system-model-title" style="display:flex; justify-content:space-between; align-items:center; cursor:pointer; user-select:none;">
<div style="display:flex; align-items:center; gap:8px">
<span style="display:inline-block;width:16px;font-size:12px;color:var(--text-tertiary)">${chevron}</span>
<span>系统主/备模型</span>
<span>${t('models.systemModelTitle')}</span>
<div style="display:flex; gap:8px; margin-left: 12px; align-items: baseline;">
<span style="color:var(--success); font-family:var(--font-mono); font-size: 0.9em; font-weight: 500;">${primary || '未配置'}</span>
<span style="font-size: 11px; color: var(--text-tertiary); font-weight: normal;">${fallbacks.length} 个备选</span>
<span style="color:var(--success); font-family:var(--font-mono); font-size: 0.9em; font-weight: 500;">${primary || t('models.notConfigured')}</span>
<span style="font-size: 11px; color: var(--text-tertiary); font-weight: normal;">${t('models.nFallbacks', { count: fallbacks.length })}</span>
</div>
</div>
</div>
@@ -177,12 +177,12 @@ function renderFallbackWaterfall(state) {
return `
<div class="fallback-editor-panel" style="background: var(--bg-secondary); padding: 12px; border-radius: var(--radius-md);">
<div style="margin-bottom: 12px; font-size: 11px; color: var(--text-secondary); background: var(--bg-info-subtle); padding: 6px 10px; border-radius: 4px; border-left: 3px solid var(--primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<strong>💡 最佳实践:</strong> 建议备选模型保持在 <strong>2-3 款</strong> 并分布在不同服务商,以平衡可用性与延迟。
${t('models.bestPracticeHint')}
</div>
<div style="display: grid; grid-template-columns: 1fr 1.2fr; gap: 24px;">
<div>
<div style="font-size: var(--font-size-xs); font-weight: bold; margin-bottom: 8px; color: var(--text-tertiary);">当前生效链 (支持拖拽排序)</div>
<div style="font-size: var(--font-size-xs); font-weight: bold; margin-bottom: 8px; color: var(--text-tertiary);">${t('models.activeChainTitle')}</div>
<div id="active-fallback-list" style="display: flex; flex-direction: column; gap: 4px; min-height: 50px;">
${currentFallbacks.map((f, i) => `
<div class="fallback-chain-item" data-id="${f}" style="display: flex; align-items: center; justify-content: space-between; background: var(--bg-primary); padding: 6px 10px; border-radius: 4px; border: 1px solid var(--border-color);">
@@ -191,19 +191,19 @@ function renderFallbackWaterfall(state) {
<span style="font-family: var(--font-mono); font-size: var(--font-size-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${i + 1}. ${f}</span>
</div>
<div style="display: flex; gap: 4px; flex-shrink: 0;">
<button class="btn btn-xs btn-secondary btn-set-primary-from-fb" data-id="${f}" style="padding: 1px 4px; font-size: 10px;">设为主用</button>
<button class="btn-icon btn-remove-fb" data-id="${f}" title="移除">${icon('x', 12)}</button>
<button class="btn btn-xs btn-secondary btn-set-primary-from-fb" data-id="${f}" style="padding: 1px 4px; font-size: 10px;">${t('models.setAsPrimary')}</button>
<button class="btn-icon btn-remove-fb" data-id="${f}" title="${t('models.remove')}">${icon('x', 12)}</button>
</div>
</div>
`).join('')}
${currentFallbacks.length === 0 ? `<div style="font-size: 12px; color: var(--text-tertiary); text-align: center; padding: 20px; border: 1px dashed var(--border-color); border-radius: 4px;">尚未选择备选模型</div>` : ''}
${currentFallbacks.length === 0 ? `<div style="font-size: 12px; color: var(--text-tertiary); text-align: center; padding: 20px; border: 1px dashed var(--border-color); border-radius: 4px;">${t('models.noFallbackSelected')}</div>` : ''}
</div>
</div>
<div>
<div style="font-size: var(--font-size-xs); font-weight: bold; margin-bottom: 8px; color: var(--text-tertiary);">可用候选池 (按服务商分组)</div>
<div style="font-size: var(--font-size-xs); font-weight: bold; margin-bottom: 8px; color: var(--text-tertiary);">${t('models.candidatePoolTitle')}</div>
<div id="candidate-model-pool" style="display: flex; flex-direction: column; gap: 6px; max-height: 300px; overflow-y: auto; padding-right: 4px;">
${Object.keys(candidatesByProvider).length === 0 ? `<div style="font-size: 12px; color: var(--text-tertiary); text-align: center; padding: 20px;">无可用候选模型</div>` :
${Object.keys(candidatesByProvider).length === 0 ? `<div style="font-size: 12px; color: var(--text-tertiary); text-align: center; padding: 20px;">${t('models.noCandidateModel')}</div>` :
Object.keys(candidatesByProvider).map(pKey => {
const collapsed = !!state._fallback_candidates_collapsed[pKey]
const mIds = candidatesByProvider[pKey]
@@ -218,7 +218,7 @@ function renderFallbackWaterfall(state) {
${mIds.map(mId => `
<div class="candidate-item" style="display: flex; align-items: center; justify-content: space-between; background: var(--bg-primary); padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border-color); opacity: 0.9;">
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${mId}</span>
<button class="btn btn-xs btn-primary btn-add-fb" data-full="${pKey}/${mId}" style="padding: 1px 6px; font-size: 10px;">加入</button>
<button class="btn btn-xs btn-primary btn-add-fb" data-full="${pKey}/${mId}" style="padding: 1px 6px; font-size: 10px;">${t('models.add')}</button>
</div>
`).join('')}
</div>
@@ -231,8 +231,8 @@ function renderFallbackWaterfall(state) {
</div>
<div style="margin-top: 16px; display: flex; justify-content: flex-end; gap: 8px;">
<button class="btn btn-sm btn-secondary" id="btn-cancel-fallback">取消</button>
<button class="btn btn-sm btn-primary" id="btn-save-fallback">保存并应用</button>
<button class="btn btn-sm btn-secondary" id="btn-cancel-fallback">${t('models.cancel')}</button>
<button class="btn btn-sm btn-primary" id="btn-save-fallback">${t('models.saveAndApply')}</button>
</div>
</div>
`
@@ -267,7 +267,7 @@ function bindWaterfallActions(page, state) {
state.config.agents.defaults.model.fallbacks = newFallbacks
renderDefaultBar(page, state)
toast(`已将 ${full} 设为主模型`, 'success')
toast(t('models.setAsPrimarySuccess', { model: full }), 'success')
}
})
@@ -380,7 +380,7 @@ function bindWaterfallActions(page, state) {
// 保存
container.querySelector('#btn-save-fallback').onclick = async () => {
state.showFallbackEditor = false
await doAutoSave(state)
autoSave(state)
renderDefaultBar(page, state)
}
}
@@ -1657,7 +1657,7 @@ async function testModel(btn, state, providerKey, idx) {
const summary = lines[0]
const detail = lines.slice(1).join('\n').trim()
if (detail) {
const detailHtml = detail.replace(/</g, '&lt;').replace(/(https?:\/\/[^\s,,。;))'"&]+)/g, '<a href="$1" target="_blank" style="color:var(--primary);text-decoration:underline">$1</a>')
const detailHtml = detail.replace(/</g, '&lt;').replace(/(https?:\/\/[^\s,。;'"&]+)/g, '<a href="$1" target="_blank" style="color:var(--primary);text-decoration:underline">$1</a>')
toast(`<strong>${modelId}</strong> ${summary.replace(/</g, '&lt;')}<br><span style="font-size:11px;line-height:1.5;word-break:break-all">${detailHtml}</span>`, 'warning', { duration: 10000, html: true })
} else {
toast(`${modelId} ${summary}`, 'warning', { duration: 6000 })