feat(seed-warn): dynamic seed key detection in UI modal; feat(https): TLS config (tls_cert_file/tls_key_file) with dynamic base_url; docs: rotate admin key reminder

This commit is contained in:
root
2026-08-10 12:11:32 +08:00
parent a5c370018c
commit fe06e0861a
7 changed files with 59 additions and 7 deletions

View File

@ -468,6 +468,7 @@ const STR = {
toastDelOk:'已删除', confirmDelSrc:'确定删除源 %s 吗?', confirmDelAdp:'确定删除适配器 %s 吗?',
u:'用户', a:'助手', at:'助手[思考]', aEmpty:'(空回复)', aErr:'请求失败: %s', cErr:'错误: %s',
chatMeta:'模型=%s · 耗时 %s ms · %d 字符', connPinned:'# 固定到模型: %s · %s', connAuto:'# 模型名 AUTO',
seedWarnTitle:'请更换初始管理员密钥', seedWarnText:'当前登录的是配置文件中的初始密钥,明文写入 config.yaml、存在泄露风险。请在下方创建新的管理员密钥用新密钥登录后删除此初始密钥。', seedWarnGo:'去更换密钥', seedWarnLater:'稍后', seedWarnDismiss:'本次不再提示',
sortTitle:'拖拽积木配置模型优先级', sortHint:'每行 = 一个优先级档位,行从上到下优先级递减;同一行的模型并排,视为同优先级。按住积木右侧 ⠿ 把手拖动:拖到行内 = 放入该档位或调整同档顺序,拖到行与行之间的缝隙 = 提升或降低到新档位。生图模型不参与排序。', sortDragGrip:'拖拽前须按住把手',
sortSave:'保存排序', sortReset:'重置', sortAdd:'添加档位', sortSaved:'排序已保存并热重载', sortNoChange:'无变更', sortHintSave:'点击保存排序后生效',
sortSource:'源', sortPrio:'优先级 %s', sortEmpty:'该源暂无模型',
@ -521,6 +522,7 @@ kMeTitle:'My key', kMeRole:'Role', kMeModels:'Models I can use', kMeHint:'Keys c
toastDelOk:'Deleted', confirmDelSrc:'Delete source %s?', confirmDelAdp:'Delete adapter %s?',
u:'You:', a:'Assistant:', at:'Assistant [thinking]', aEmpty:'empty', aErr:'Request failed: %s',
cErr:'Error: %s', chatMeta:'Model=%s · %s ms · %d chars', connPinned:'# Pinned to model: %s · %s', connAuto:'# Model "AUTO" follows the Priority page AUTO chain',
seedWarnTitle:'Replace the initial admin key', seedWarnText:'You are logged in with the seed key from config.yaml. It is plaintext in the config file and a security risk. Create a new admin key below, log in with it, then delete this seed key.', seedWarnGo:'Change my key', seedWarnLater:'Later', seedWarnDismiss:'Don\'t ask again',
sortTitle:'Drag blocks to set model priority', sortHint:'Each row = one priority tier, rows go high→low; models on the same row sit side by side and share that priority. Grab the ⠿ handle on the right of a block to drag: drop into a row = join that tier or reorder within it, drop into the gap between rows = move up/down a tier. Image models stay out.', sortDragGrip:'grab the handle to drag',
sortSave:'Save order', sortReset:'Reset', sortAdd:'Add slot', sortSaved:'Order saved & hot-reloaded', sortNoChange:'No changes', sortHintSave:'Click Save for it to take effect',
sortSource:'source', sortPrio:'priority %s', sortEmpty:'no models in this source',
@ -563,12 +565,13 @@ document.getElementById('btn-lang').onclick = () => {
};
document.getElementById('btn-logout').onclick = () => { location.href = '/api/logout'; };
document.querySelectorAll('nav button').forEach(b => {
b.onclick = () => {
document.querySelectorAll('nav button').forEach(x => x.classList.toggle('active', x === b));
['status','chat','keys','sort','sources','adapters'].forEach(tn => $('#tab-' + tn).classList.toggle('hidden', tn !== b.dataset.tab));
lastTab = b.dataset.tab; refresh(lastTab);
};
b.onclick = () => goTab(b.dataset.tab);
});
function goTab(name) {
document.querySelectorAll('nav button').forEach(x => x.classList.toggle('active', x.dataset.tab === name));
['status','chat','keys','sort','sources','adapters'].forEach(tn => $('#tab-' + tn).classList.toggle('hidden', tn !== name));
lastTab = name; refresh(lastTab);
}
applyI18n();
/* ---------- helpers ---------- */
@ -1736,6 +1739,20 @@ async function renderKeys() {
window._me = me.key;
return me.key.role === 'admin' ? renderKeysAdmin(me.key) : renderKeysUser(me.key);
}
function maybeWarnSeed(k) {
if (!k || !k.seed) return;
if (localStorage.getItem('llms-proxy.seedWarn') === '1') return;
const wrap = document.createElement('div'); wrap.id = 'modal-wrap';
wrap.style.cssText = 'position:fixed;inset:0;background:rgba(15,22,44,.45);display:flex;align-items:flex-start;justify-content:center;overflow:auto;padding:48px 20px;z-index:50';
wrap.innerHTML = `<div class="card" style="width:460px;max-width:100%">
<h2>⚠ ${esc(t('seedWarnTitle'))}</h2>
<p class="muted" style="line-height:1.6">${esc(t('seedWarnText'))}</p>
<p><button onclick="this.closest('#modal-wrap').remove();goTab('keys')">${esc(t('seedWarnGo'))}</button>
<button class="ghost" onclick="this.closest('#modal-wrap').remove()">${esc(t('seedWarnLater'))}</button>
<button class="ghost" onclick="localStorage.setItem('llms-proxy.seedWarn','1');this.closest('#modal-wrap').remove()">${esc(t('seedWarnDismiss'))}</button></p>
</div>`;
document.body.appendChild(wrap);
}
async function renderKeysUser(me) {
$('#tab-keys').innerHTML = `
<div class="card"><h2>${t('kMeTitle')}</h2>
@ -2067,6 +2084,7 @@ function refresh(tab) {
try {
const me = await api('/api/keys/me');
window._me = me.key;
maybeWarnSeed(me.key);
if (me.key.role !== 'admin') {
['sort', 'sources', 'adapters'].forEach(tn => {
const b = document.querySelector(`nav button[data-tab="${tn}"]`);