diff --git a/cmd/gui/main.js b/cmd/gui/main.js index 42b161f..f306794 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -263,7 +263,6 @@ function waitForCoreReady(timeoutMs = 10000) { let coreLastExit = 0; function stopCore() { - app.isQuitting = true; coreReady = false; if (coreProc && coreProc.exitCode === null) { try { diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html index 5846e04..272fe29 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -28,7 +28,6 @@ --sh-lg:0 18px 46px rgba(120,90,160,.22); --glass:18px; --ease-spring:cubic-bezier(.34,1.56,.64,1); - --accent:sakura; } html[data-accent="sakura"]{ --primary:#FF7FAC; --primary-h:#F33B7C; --primary-50:#FFF0F5; --primary-100:#FFE4E9; @@ -84,20 +83,6 @@ --blob1:rgba(168,85,247,.32); --blob2:rgba(34,211,238,.26); --blob3:rgba(139,92,246,.22); --line:rgba(168,85,247,.16); --line-strong:rgba(139,92,246,.18); } - html[data-theme="dark"]{ - --primary:#f54180; --primary-h:#f871a0; --primary-50:#310413; --primary-100:#610726; - --primary-200:#920b3a; --primary-300:#c20e4d; --primary-400:#f31260; --primary-500:#f54180; - --secondary:#5E9FBF; --secondary-h:#88C0D0; --secondary-50:#0E2A36; --secondary-200:#2A546A; - --danger:#DB3694; --danger-50:#3d1a24; --ok:#4f7cff; --warn:#e0b25c; --info:#6a8ffb; - --bg-s1:#14121c; --bg-s2:#1a1824; --bg-s3:#221a28; - --card:rgba(34,31,50,.55); --card-solid:#241f38; --card2:rgba(44,39,64,.42); - --line:rgba(255,127,172,.16); --line-strong:rgba(200,180,235,.18); - --fg:#ece6f5; --muted:#a09db8; - --blob1:rgba(255,79,160,.32); --blob2:rgba(100,180,215,.26); --blob3:rgba(219,54,148,.22); - --sh-sm:0 1px 2px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.05); - --sh-md:0 8px 26px rgba(0,0,0,.45); - --sh-lg:0 20px 52px rgba(0,0,0,.55); - } *{box-sizing:border-box} html,body{height:100%} body{margin:0;color:var(--fg);overflow:hidden; @@ -187,7 +172,6 @@ .tag-green{background:var(--primary-50);color:var(--ok)} .tag-red{background:var(--danger-50);color:var(--danger)} .tag-blue{background:var(--secondary-50);color:var(--secondary-h)} - .tag-warn{background:rgba(183,121,31,.14);color:var(--warn)} .net-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;margin-right:6px;vertical-align:1px} button{background:linear-gradient(135deg,var(--primary),var(--danger));color:#fff;border:0;border-radius:11px;padding:8px 16px;cursor:pointer; font:inherit;font-weight:600;transition:all .18s var(--ease-spring)} @@ -278,7 +262,6 @@ .filter-line select{width:auto;margin:0;padding:6px 10px} /* ---- keys tab ---- */ - .keys-grid{display:flex;flex-direction:column;gap:12px} .key-row{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--card);flex-wrap:wrap} .key-row .kr-name{font-weight:700;min-width:120px} .key-row .kr-key{font-family:'JetBrains Mono',ui-monospace,Consolas,monospace;font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;max-width:340px;white-space:nowrap} @@ -287,14 +270,11 @@ .tag-role{font-size:11px;padding:3px 10px;border-radius:99px;font-weight:700} .tag-role.admin{background:linear-gradient(135deg,var(--primary),var(--danger));color:#fff} .tag-role.user{background:var(--secondary-50);color:var(--secondary-h);border:1px solid var(--line)} - .scope-box{border-top:1px dashed var(--line);margin-top:8px;padding-top:10px} - .scope-chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center;min-height:40px} .scope-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:99px;background:linear-gradient(135deg,var(--primary),var(--secondary));color:#fff;font-size:12.5px;font-weight:600;cursor:grab;user-select:none} .scope-chip[draggable="true"]:active{cursor:grabbing} .scope-chip .rm{cursor:pointer;opacity:.75;font-size:13px;line-height:1;padding:0 2px} .scope-chip .rm:hover{opacity:1} .scope-chip.drag-over{outline:2px dashed #fff;outline-offset:2px} - .scope-add{padding:5px 10px;border-radius:99px;border:1px dashed var(--line);background:transparent;color:var(--muted);font-size:12.5px;cursor:pointer} .scope-unlim{color:var(--primary-h);font-weight:700;font-size:13px} .keys-create{display:grid;grid-template-columns:1fr 2fr 1fr 2fr;gap:10px;align-items:end} .keys-create label{font-size:11.5px;color:var(--muted);margin-bottom:4px} @@ -383,7 +363,6 @@ #modal-wrap{position:fixed;inset:0;background:rgba(30,20,60,.45);display:none;align-items:flex-start;justify-content:center;overflow:auto;padding:48px 20px;z-index:50;backdrop-filter:blur(3px)} #modal-wrap:not(:empty){display:flex} #modal-wrap .card{width:100%;max-width:480px;animation:cardIn .28s var(--ease-spring) both} - .twrap{overflow-x:auto;-webkit-overflow-scrolling:touch} /* ---- scratch-style priority canvas (sakura restyle, same classes) ---- */ .sort-canvas{position:relative;min-height:420px;padding:22px 14px 30px;margin:4px 0 10px;background-color:var(--card2); @@ -395,7 +374,6 @@ .scr-lane.scr-top .scr-tier span{opacity:.95} .scr-row{flex:1 1 auto;display:flex;align-items:center;padding:2px 18px;border-radius:8px;position:relative;transition:background .18s} .scr-row.hover{background:var(--primary-50)} - .scr-blocks{display:flex;flex-wrap:wrap;align-items:center;min-width:0} .scr-block{position:relative;display:flex;align-items:center;gap:10px;padding:12px 14px 12px 20px;color:#fff;font-weight:700;font-size:13px;user-select:none;margin-right:0; border-radius:11px 11px 7px 7px;box-shadow:0 6px 0 rgba(0,0,0,.14),inset 0 2px 0 rgba(255,255,255,.30),inset 0 -5px 0 rgba(0,0,0,.08);transition:opacity .15s} .scr-block::before{content:'';position:absolute;left:-7px;top:50%;transform:translateY(-50%);width:7px;height:30px;border-radius:7px 0 0 7px;background:inherit;z-index:2} @@ -451,7 +429,6 @@ .scr-block .scr-x{position:absolute;top:-6px;right:-6px;cursor:pointer;font-size:13px;line-height:1;padding:3px 5px;border-radius:50%; color:rgba(255,255,255,.9);background:rgba(0,0,0,.22);z-index:3} .scr-block .scr-x:hover{background:rgba(219,54,148,.85);color:#fff} - .m-model-row{display:flex;gap:8px;align-items:center} .m-id{margin:0 0 8px} .m-kind{flex:0 0 auto;margin:0 0 8px} .donut{position:relative;width:108px;height:108px} @@ -729,14 +706,12 @@ zh: { tagline: "统一 LLM 网关", logout: "退出登录", - langTo: "EN", navStatus: "状态", navChat: "对话", navSources: "源", navAdapters: "适配器", navSort: "优先级", navKeys: "密钥", - keysTitle: "密钥管理", keysHint: "管理员密钥可查看与管理全部密钥,并可为每个用户密钥配置可用模型范围;用户密钥只能看到自己。", kCreate: "新建密钥", @@ -747,30 +722,18 @@ kNote: "备注", kCreateBtn: "创建", kKey: "密钥", - kModels: "可用模型", - kCreated: "创建时间", - kActions: "操作", kCopy: "复制密钥", kDel: "删除", kNewKey: "新密钥", kNewOK: "密钥已创建", - kScope: "配置模型范围", kScopeTitle: "配置模型范围 —— 拖拽排列、下拉添加、×删除、复制/粘贴到其它用户", - kScopeHint: "拖拽排序 · 下拉添加 · × 删除 · 复制/粘贴列表", - kScopeSel: "选择模型…", - kCopyList: "复制列表", - kPasteList: "粘贴列表", kSaveScope: "保存", - kScopeSaved: "模型范围已保存", - kScopeEmpty: "全部模型", - kScopeAll: "空即不限", kMeTitle: "我的密钥", kMeRole: "角色", kMeModels: "我可用的模型", kMeHint: "密钥不可在此新建或删除;需要变更请联系管理员。", kEmpty: "暂无其他密钥", - kDelSelf: "不能删除当前登录所用密钥", kDelConfirm: "确定删除密钥 %s 吗?此后该密钥立即失效。", kAll: "不限", kBrickH: "点击编辑模型与配额 · 右键更多操作 · 拖动可跨密钥移动", @@ -805,7 +768,6 @@ copyCfg: "一键复制配置", copyEnv: "复制为环境变量", srcTitle: "源状态", - srcCount: "共 %d 个", tName: "名称", tAdapter: "适配器", tModels: "模型", @@ -816,13 +778,12 @@ offline: "退避 / 不可用", adTitle: "已加载适配器", tVersion: "版本", - chatTitle: "Chat 测试", cModel: "模型", cMsg: "Enter 发送,Shift+Enter 换行", send: "发送", + cClear: "清空", clear: "清空", cThinking: "推理", - cImg: "图片", cPick: "选择图片", cRemoveImg: "移除", cWelcome: "开始对话——选择模型,输入消息即可测试上游连接", @@ -884,20 +845,15 @@ sortTitle: "拖拽积木配置模型优先级", sortHint: "每行 = 一个优先级档位,行从上到下优先级递减;同一行的模型并排,视为同优先级。按住积木右侧 ⠿ 把手拖动:拖到行内 = 放入该档位或调整同档顺序,拖到行与行之间的缝隙 = 提升或降低到新档位。生图模型不参与排序。", - sortDragGrip: "拖拽前须按住把手", sortSave: "保存排序", sortReset: "重置", sortAdd: "添加档位", sortSaved: "排序已保存并热重载", - sortNoChange: "无变更", sortHintSave: "点击保存排序后生效", sortCooling: "冷却", sortFail: "失败", sortHealthTip: "冷却 / 失败次数 / 偏好分 实时状态", sortHealthReset: "链上冷却已复位", - sortSource: "源", - sortPrio: "优先级 %s", - sortEmpty: "该源暂无模型", kpiActive: "活跃请求", kpiReqs: "总请求", kpiOk: "成功率", @@ -919,7 +875,6 @@ expKeysCsv: "导出密钥用量", exitKeyView: "退出该密钥视图", dashStatus: "状态码分布", - thCode: "状态码", kOther: "其他", statusTag: "状态码分类统计(含 402 欠费 / 400 schema 错误;两者不计入上游退避但单独计数)", @@ -945,14 +900,12 @@ en: { tagline: "Unified LLM Gateway", logout: "Log out", - langTo: "中", navStatus: "Status", navChat: "Chat", navSources: "Sources", navAdapters: "Adapters", navSort: "Priority", navKeys: "Keys", - keysTitle: "Key management", keysHint: "Admin keys can view and manage every key and configure each user key\u0027s allowed models; user keys only see themselves.", kCreate: "Create key", @@ -963,32 +916,19 @@ kNote: "Note", kCreateBtn: "Create", kKey: "Key", - kModels: "Allowed models", - kCreated: "Created", - kActions: "Actions", kCopy: "Copy key", kDel: "Delete", kNewKey: "New key — copy & save it now", kNewOK: "Key created", - kScope: "Model scope", kScopeTitle: "Model scope — drag to order, add from dropdown, × to remove, copy/paste the list to other users", - kScopeHint: - "drag to order · add from dropdown · × to remove · copy/paste list", - kScopeSel: "Pick a model…", - kCopyList: "Copy list", - kPasteList: "Paste list", kSaveScope: "Save", - kScopeSaved: "Model scope saved", - kScopeEmpty: "all models", - kScopeAll: "empty = unlimited", kMeTitle: "My key", kMeRole: "Role", kMeModels: "Models I can use", kMeHint: "Keys cannot be created or deleted here; contact an admin to change.", kEmpty: "No other keys", - kDelSelf: "cannot delete the key you are logged in with", kDelConfirm: 'Delete key "%s"? It will stop working immediately.', kAll: "All", kBrickH: @@ -1025,7 +965,6 @@ copyCfg: "Copy config", copyEnv: "Copy as env vars", srcTitle: "Sources", - srcCount: "%d total", tName: "Name", tAdapter: "Adapter", tModels: "Models", @@ -1036,13 +975,12 @@ offline: "backoff / down", adTitle: "Loaded adapters", tVersion: "Version", - chatTitle: "Chat", cModel: "Model", cMsg: "Enter to send, Shift+Enter for newline", send: "Send", + cClear: "Clear", clear: "Clear", cThinking: "Thinking", - cImg: "Image", cPick: "Pick image", cRemoveImg: "Remove", cWelcome: @@ -1105,20 +1043,15 @@ 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", sortCooling: "cooling", sortFail: "fail", sortHealthTip: "live cooldown / failures / preference score", sortHealthReset: "chain cooldowns reset", - sortSource: "source", - sortPrio: "priority %s", - sortEmpty: "no models in this source", kpiActive: "Active requests", kpiReqs: "Requests", kpiOk: "Success rate", @@ -1140,7 +1073,6 @@ expKeysCsv: "Export key usage", exitKeyView: "Exit this key view", dashStatus: "Status codes", - thCode: "Code", kOther: "other", statusTag: "Per-status aggregates — 402 quota / 400 schema errors are counted here but never back off the provider", @@ -1191,10 +1123,8 @@ if (ll) ll.textContent = LANG === "zh" ? "EN" : "中"; document.documentElement.lang = LANG; } + let lastTab = "status"; - function activeTab() { - return lastTab; - } /* ---------- theme & header ---------- */ /* ---------- chrome: theme / lang / sidebar / breadcrumb ---------- */ @@ -1590,6 +1520,22 @@ const w = $("#modal-wrap"); if (w) w.remove(); } + // downloadStatsCsvFromForm reads the custom date range from the export + // modal's date inputs and forwards to downloadStatsCsv. (Previously this + // was referenced by the modal button but never defined — clicking + // "download" did nothing.) + function downloadStatsCsvFromForm() { + const f = $("#exp-from"); + const t = $("#exp-to"); + if (!f || !t) return; + // date inputs give YYYY-MM-DD; interpret as local days, end exclusive + const from = new Date(f.value + "T00:00:00").getTime(); + const to = t.value + ? new Date(t.value + "T00:00:00").getTime() + 864e5 + : Date.now(); + if (!isFinite(from) || from <= 0) return toast(t("expRange")); + downloadStatsCsv(from, to); + } function downloadKeysCsv() { location.href = "/api/stats?export=keys-csv" + @@ -2113,6 +2059,7 @@ + @@ -2302,7 +2249,6 @@ firstChunk = Date.now(); try { const resp = await fetch("/api/chat", { - method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, @@ -2551,7 +2497,6 @@ btn.disabled = true; try { await api("/api/sources", { - method: "POST", body: JSON.stringify(payload), }); toast(t("toastSaved")); @@ -2567,7 +2512,6 @@ async function delSource(name) { if (!confirm(tFmt("confirmDelSrc", name))) return; await api("/api/sources/" + encodeURIComponent(name), { - method: "DELETE", }); toast(t("toastDelOk")); renderSources(); @@ -2722,9 +2666,6 @@ `; } - function findSlots(li, ji) { - return sortState.lanes[li].models[ji]; - } function paintSort(affected) { const cv = $("#scr-canvas"); if (!cv) return; @@ -3154,7 +3095,6 @@ }), ); await api("/api/auto", { - method: "PUT", body: JSON.stringify({ rules }), }); } @@ -3413,7 +3353,6 @@ if (!name || !code) return toast(t("toastNeedAll")); try { await api("/api/adapters", { - method: "POST", body: JSON.stringify({ name, code }), }); toast(t("toastUploaded")); @@ -3427,7 +3366,6 @@ async function delAdapter(name) { if (!confirm(tFmt("confirmDelAdp", name))) return; await api("/api/adapters/" + encodeURIComponent(name), { - method: "DELETE", }); toast(t("toastDelOk")); renderAdapters(); @@ -3590,10 +3528,6 @@ const p = String(comb).split("|"); return { model: p[0], source: normSrc(p[1]) }; } - function scopeUncomb(comb) { - const p = String(comb).split("|"); - return { model: p[0], source: p[1] || "" }; - } function fmtQuota(q) { q = +q || 0; if (!q) return "∞"; @@ -3631,7 +3565,6 @@ } async function putScope(key, scopes) { await api("/api/keys/" + encodeURIComponent(key), { - method: "PUT", body: JSON.stringify({ models: scopes }), }); } @@ -3953,7 +3886,6 @@ let j; try { j = await api("/api/keys", { - method: "POST", body: JSON.stringify({ name, role: $("#kc-role").value, @@ -3983,7 +3915,6 @@ if (!confirm(tFmt("kDelConfirm", name || key))) return; try { await api("/api/keys/" + encodeURIComponent(key), { - method: "DELETE", }); toast(t("toastDelOk")); loadKeys();