ezllm 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/server.js +63 -5
- package/dist/web-ui/assets/{DashboardView-DTF4RzgZ.js → DashboardView-ChJpgKis.js} +1 -1
- package/dist/web-ui/assets/{LayoutView-CkUPhcPQ.js → LayoutView-C8YPw0x2.js} +1 -1
- package/dist/web-ui/assets/{LoginView-CzfNjlvz.js → LoginView-C4hkT-3T.js} +1 -1
- package/dist/web-ui/assets/{LogsView-BYA6dgGO.js → LogsView-DdS8qKEs.js} +1 -1
- package/dist/web-ui/assets/ProvidersView-BD33caY8.js +14 -0
- package/dist/web-ui/assets/ProvidersView-DNqX_AWw.css +1 -0
- package/dist/web-ui/assets/{ServiceStatusView-c0jJPnb2.js → ServiceStatusView-BtF40E0V.js} +1 -1
- package/dist/web-ui/assets/{SettingsView-BZsuqac5.js → SettingsView-SdL8ednl.js} +1 -1
- package/dist/web-ui/assets/{SetupView-DpD5okpe.js → SetupView-BZFBBRSd.js} +1 -1
- package/dist/web-ui/assets/{config-C6eC0CWg.js → config-Dbms19Et.js} +1 -1
- package/dist/web-ui/assets/{index-DRdS47Vm.js → index-C6wv4UB9.js} +1 -1
- package/dist/web-ui/assets/{message-D5w4qfHY.js → message-CEBSyhlN.js} +1 -1
- package/dist/web-ui/assets/{providers-BM4fqFJi.js → providers-DkujdEZ8.js} +1 -1
- package/dist/web-ui/assets/{router-DjNMXIJT.js → router-BqkHX02I.js} +2 -2
- package/dist/web-ui/assets/{useLoadError-qYTOtUW0.js → useLoadError-Yu7YH4ZH.js} +1 -1
- package/dist/web-ui/assets/{useManagerApi-COiXXii9.js → useManagerApi-fVrRDZkc.js} +1 -1
- package/dist/web-ui/assets/useToast-DJwG-33L.js +1 -0
- package/dist/web-ui/index.html +3 -3
- package/docs/DESIGN.md +2 -2
- package/hook.example.js +176 -0
- package/package.json +2 -1
- package/dist/web-ui/assets/ProvidersView-DYUaYhO-.js +0 -14
- package/dist/web-ui/assets/ProvidersView-ufXnqFGi.css +0 -1
- package/dist/web-ui/assets/useToast-DKg4wBxA.js +0 -1
package/README.md
CHANGED
|
@@ -54,4 +54,16 @@ ezllm restart # 重启
|
|
|
54
54
|
ezllm install # 装配/修复自带 Node 运行时的环境(幂等;自动迁移旧数据目录)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## 进阶:改写请求报文(可选)
|
|
58
|
+
|
|
59
|
+
如果某个客户端每轮都在请求里塞大量固定内容(工具描述里内嵌的 API 手册、注入的记忆索引等),可以在网关侧裁掉,从根上省 token。
|
|
60
|
+
|
|
61
|
+
把发布包里的 `hook.example.js` 复制到应用目录(默认 `~/.ezllm`)并改名为 **`hook.cjs`**,按注释改里面的规则,然后 `ezllm restart`。
|
|
62
|
+
|
|
63
|
+
- **候选文件名**:`hook.cjs` → `hook.mjs` → `hook.js`,取第一个存在的。**推荐 `hook.cjs`**——`.js` 的解析模式(CJS 还是 ESM)取决于它祖先目录里最近的 `package.json`,不受你控制(比如家目录若有一份 `"type": "module"` 的 package.json,`hook.js` 里的 CJS 写法就会失效);`.cjs` 强制按 CJS 解析,任何环境都稳。
|
|
64
|
+
- **只有请求侧改写**一个钩子(`onModelRequest`):在发往上游前改请求体/请求头,**改不了响应**。
|
|
65
|
+
- **改完必须重启服务**:钩子在启动时加载,不支持运行期热加载。
|
|
66
|
+
- **文件不存在 = 什么都不做**;文件写错也只打一条告警、服务照常启动(不会因为钩子报错而起不来)。
|
|
67
|
+
- **改动的部分要保持内容稳定**(别插时间戳 / 随机 id),否则会打断上游的缓存前缀,命中率下降得不偿失。
|
|
68
|
+
|
|
57
69
|
> 说明:本工具为个人本地使用设计,非高并发架构,不建议作为商业代理部署。
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import crypto from "crypto";
|
|
|
8
8
|
import { closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
10
|
import { cwd } from "node:process";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
13
13
|
import { DatabaseSync } from "node:sqlite";
|
|
14
14
|
import { brotliCompressSync, brotliDecompressSync, constants as constants$1, gunzipSync } from "node:zlib";
|
|
@@ -13242,6 +13242,61 @@ function validateConfig(config) {
|
|
|
13242
13242
|
const { masterKey: _masterKey, "web-ui": _webUi, logs: _logs, setup: _setup, autoUpdate: _autoUpdate, allowLan: _allowLan, latencyTest: _latencyTest, ...gatewayConfig } = config;
|
|
13243
13243
|
return expandConfig(gatewayConfig);
|
|
13244
13244
|
}
|
|
13245
|
+
/**
|
|
13246
|
+
* 用户钩子文件搜索路径(固定约定,非配置项):`<appDir>/hook.cjs` / `hook.mjs` / `hook.js`
|
|
13247
|
+
* 三选一。数组按优先级排列;实际加载的是其中**第一个存在的**(见 resolveHookPath)。
|
|
13248
|
+
*/
|
|
13249
|
+
var USER_HOOK_PATHS = [
|
|
13250
|
+
"hook.cjs",
|
|
13251
|
+
"hook.mjs",
|
|
13252
|
+
"hook.js"
|
|
13253
|
+
].map((n) => join(APP_DIR, n));
|
|
13254
|
+
/** 取第一个存在的候选路径;都不存在返回 null(= 未配置钩子,静默跳过) */
|
|
13255
|
+
function resolveHookPath() {
|
|
13256
|
+
for (const p of USER_HOOK_PATHS) if (existsSync(p)) return p;
|
|
13257
|
+
return null;
|
|
13258
|
+
}
|
|
13259
|
+
/**
|
|
13260
|
+
* 把 import 结果归一为钩子对象:兼容 esm default 导出与 cjs module.exports。
|
|
13261
|
+
* 优先级 default → module.exports → 裸对象,任一命中即止。
|
|
13262
|
+
*/
|
|
13263
|
+
function pickHooks(mod, path) {
|
|
13264
|
+
if (mod === null || typeof mod !== "object") return null;
|
|
13265
|
+
const m = mod;
|
|
13266
|
+
const candidate = m["default"] ?? m["module.exports"] ?? m;
|
|
13267
|
+
if (candidate === null || typeof candidate !== "object") return null;
|
|
13268
|
+
const hooks = candidate;
|
|
13269
|
+
if (typeof hooks.onModelRequest !== "function") {
|
|
13270
|
+
console.warn(`[ezllm] ${path} 未导出 onModelRequest 函数,已忽略(不挂任何钩子)`);
|
|
13271
|
+
return null;
|
|
13272
|
+
}
|
|
13273
|
+
return { onModelRequest: hooks.onModelRequest };
|
|
13274
|
+
}
|
|
13275
|
+
/**
|
|
13276
|
+
* 加载并注册用户钩子(文件不存在 = 静默跳过,按现状运行)。
|
|
13277
|
+
*
|
|
13278
|
+
* **失败只告警、绝不致命**:hook.js 是用户手写文件,一个语法错误不该打死整个网关——
|
|
13279
|
+
* 那会连管理台一起失去,用户无法自救。失败时打印完整原因并保持无钩子运行。
|
|
13280
|
+
*
|
|
13281
|
+
* @param ellm 已构造的网关实例(钩子挂其 HookRunner)
|
|
13282
|
+
*/
|
|
13283
|
+
async function registerUserHooks(ellm) {
|
|
13284
|
+
const hookPath = resolveHookPath();
|
|
13285
|
+
if (hookPath === null) return;
|
|
13286
|
+
let mod;
|
|
13287
|
+
try {
|
|
13288
|
+
mod = await import(pathToFileURL(hookPath).href);
|
|
13289
|
+
} catch (error) {
|
|
13290
|
+
console.error(`[ezllm] 加载用户钩子失败,本次按无钩子运行:${hookPath}`);
|
|
13291
|
+
console.error(`[ezllm] 原因:${error instanceof Error ? error.message : String(error)}`);
|
|
13292
|
+
console.error("[ezllm] 提示:模块写法与文件扩展名不匹配。CJS 内容(module.exports)请用 hook.cjs,ESM 内容(export default)请用 hook.mjs——这两个扩展名强制解析模式,不受任何 package.json 的 type 影响");
|
|
13293
|
+
return;
|
|
13294
|
+
}
|
|
13295
|
+
const hooks = pickHooks(mod, hookPath);
|
|
13296
|
+
if (hooks?.onModelRequest === void 0) return;
|
|
13297
|
+
ellm.addHook("onModelRequest", hooks.onModelRequest);
|
|
13298
|
+
console.log(`[ezllm] 已加载用户钩子:${hookPath}(onModelRequest)`);
|
|
13299
|
+
}
|
|
13245
13300
|
//#endregion
|
|
13246
13301
|
//#region ../../packages/server/src/services/gateway-runtime.ts
|
|
13247
13302
|
/**
|
|
@@ -13263,12 +13318,13 @@ function validateConfig(config) {
|
|
|
13263
13318
|
* @param options - traceSink 可选(观测面落库 sink);缺省时观测数据不落库
|
|
13264
13319
|
* @returns 可热更新的运行时句柄:rebuild() 热重载配置,proxy 挂载到外层 Hono
|
|
13265
13320
|
*/
|
|
13266
|
-
function createRuntime(config, hooks, options = {}) {
|
|
13321
|
+
async function createRuntime(config, hooks, options = {}) {
|
|
13267
13322
|
const ellm = new Ellm({
|
|
13268
13323
|
config,
|
|
13269
13324
|
traceSink: options.traceSink
|
|
13270
13325
|
});
|
|
13271
13326
|
hooks.register(ellm);
|
|
13327
|
+
await registerUserHooks(ellm);
|
|
13272
13328
|
const state = {
|
|
13273
13329
|
current: ellm,
|
|
13274
13330
|
port: ellm.expanded.port,
|
|
@@ -17957,7 +18013,8 @@ function createQuotaApp() {
|
|
|
17957
18013
|
vendors: listQuotaVendors().map((v) => ({
|
|
17958
18014
|
vendorId: v.vendorId,
|
|
17959
18015
|
vendorLabel: v.vendorLabel,
|
|
17960
|
-
hosts: v.hosts
|
|
18016
|
+
hosts: v.hosts,
|
|
18017
|
+
apiHosts: v.apiHosts ?? []
|
|
17961
18018
|
})),
|
|
17962
18019
|
providers: getAllQuotaEntries()
|
|
17963
18020
|
});
|
|
@@ -17968,7 +18025,8 @@ function createQuotaApp() {
|
|
|
17968
18025
|
vendors: listQuotaVendors().map((v) => ({
|
|
17969
18026
|
vendorId: v.vendorId,
|
|
17970
18027
|
vendorLabel: v.vendorLabel,
|
|
17971
|
-
hosts: v.hosts
|
|
18028
|
+
hosts: v.hosts,
|
|
18029
|
+
apiHosts: v.apiHosts ?? []
|
|
17972
18030
|
}))
|
|
17973
18031
|
});
|
|
17974
18032
|
});
|
|
@@ -41761,7 +41819,7 @@ if (orphanTmpRemoved > 0) console.log(`[ezllm] spool 启动清理:回收中断
|
|
|
41761
41819
|
await recoverSpool();
|
|
41762
41820
|
var traceSink = createSqliteTraceSink();
|
|
41763
41821
|
var knownModelNames = () => config.providers.flatMap((p) => p.models.map((m) => m.modelName));
|
|
41764
|
-
var runtime = createRuntime(config, {
|
|
41822
|
+
var runtime = await createRuntime(config, {
|
|
41765
41823
|
authorize(authHeader) {
|
|
41766
41824
|
const masterKey = resolveMasterKey();
|
|
41767
41825
|
if (masterKey === "") return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,A as t,Et as n,F as r,Ft as i,Gt as a,H as o,I as s,L as c,M as l,N as u,Q as d,T as ee,a as f,at as p,d as m,gt as te,ht as h,j as g,k as _,ot as ne,pn as v,rt as re,tt as y,yt as b}from"./router-
|
|
1
|
+
import{$ as e,A as t,Et as n,F as r,Ft as i,Gt as a,H as o,I as s,L as c,M as l,N as u,Q as d,T as ee,a as f,at as p,d as m,gt as te,ht as h,j as g,k as _,ot as ne,pn as v,rt as re,tt as y,yt as b}from"./router-BqkHX02I.js";import{t as x}from"./useManagerApi-fVrRDZkc.js";import{t as S}from"./providers-DkujdEZ8.js";import{t as C}from"./useToast-DJwG-33L.js";import{t as w}from"./useLoadError-Yu7YH4ZH.js";import{t as T}from"./latestRequest-Dk21IcrH.js";import{t as ie}from"./clipboard-CorU9X8e.js";import{a as ae,o as E,r as oe,t as se}from"./format-DrbP4_cY.js";b();function ce(e){let{loadError:t,run:r}=w(e),i=n(null);function a(e){let t=async()=>{let n=await r(e);return n?i.value===t&&(i.value=null):i.value=t,n};return t}function o(e){(i.value??e)()}return{loadError:t,track:a,retry:o}}o(),b(),a();var le={class:`flex-col-gap`},ue=[`onClick`],de={key:1},fe={class:`flex-between`},pe={class:`flex-gap-8`},me={class:`usage-grid`},he={class:`usage-cell`},ge={class:`usage-num orange`},_e={class:`usage-cell`},D={class:`usage-num blue`},O={class:`usage-cell`},k={class:`usage-num green`},A={class:`usage-cell`},j={class:`usage-num purple`},M={class:`usage-cell`},N={class:`usage-num purple`},P={key:0,class:`usage-cell`},F={class:`usage-num gray`},I={key:0},L=[`onClick`],R=m(c({__name:`DashboardView`,setup(a){let o=S(),{toast:c}=C(),{loadError:m,track:b,retry:w}=ce(),R=n(null),z=n(!0),B=n(``),V=n(`today`),H=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,`0`)}-${String(e.getDate()).padStart(2,`0`)}`,U=H(new Date),W=n([U,U]),ve=e=>{let t=new Date,n,r=t;if(e===`today`)n=t;else if(e===`yesterday`)n=new Date(t),n.setDate(t.getDate()-1),r=n;else if(e===`beforeYesterday`)n=new Date(t),n.setDate(t.getDate()-2),r=n;else if(e===`last3days`)n=new Date(t),n.setDate(t.getDate()-2);else if(e===`week`){n=new Date(t);let e=(t.getDay()+6)%7;n.setDate(t.getDate()-e)}else if(e===`month`)n=new Date(t.getFullYear(),t.getMonth(),1);else return;V.value=e,W.value=[H(n),H(r)],G()},ye=e=>{e?ve(e):(V.value=``,W.value=null,G())},be=()=>{V.value=``,G()},xe=_(()=>{let e=new Set;for(let t of o.suppliers)for(let n of t.models)e.add(n.modelName);if(R.value?.models)for(let t of R.value.models)e.add(t.model);return[...e].sort()}),Se=T(),G=b(async()=>{let e=Se();z.value=!0;try{let t=new URLSearchParams;B.value&&t.set(`model`,B.value),W.value?.length===2&&(t.set(`startDate`,W.value[0]),t.set(`endDate`,W.value[1]));let n=t.toString(),r=await f.get(`/api/usage${n?`?${n}`:``}`);if(!e())return;R.value=r}catch(t){if(!e())return;throw t}finally{e()&&(z.value=!1)}}),Ce=T(),K=b(async()=>{let e=Ce();try{await Promise.all([G(),o.loadProviders()])}catch(t){if(!e())return;throw t}finally{e()&&(z.value=!1)}}),we=()=>w(K),Te=window.location.hostname,Ee=Number(window.location.port)||null,De=_(()=>se({protocol:window.location.protocol,hostname:Te,port:Ee})),q=n(null),J=n(null),Y=n(!1),X=n(Date.now()),Z=async()=>{try{let[e,t]=await Promise.all([x.processes(),f.get(`/api/config`).catch(()=>null)]);q.value=e[0]??null,Y.value=t===null,J.value=t?.masterKey??``,X.value=Date.now()}catch{}},Oe=e=>{if(!e.startedAt)return`-`;let t=Math.max(0,Math.floor((X.value-e.startedAt)/1e3)),n=Math.floor(t/86400);t-=n*86400;let r=Math.floor(t/3600);t-=r*3600;let i=Math.floor(t/60);return t-=i*60,[n?`${n}天`:``,r?`${r}时`:``,i?`${i}分`:``,`${t}秒`].filter(Boolean).join(` `)},ke=_(()=>{let e=[{key:`API 地址`,value:De.value},{key:`API 密钥`,value:Y.value?`获取失败,请刷新重试`:J.value||`未设置(/v1 免鉴权)`},{key:`供应商数量`,value:String(o.providerCount)},{key:`模型总数`,value:String(o.totalModelCount)}],t=q.value;return t&&e.push({key:`运行状态`,value:`运行中`},{key:`PID`,value:String(t.pid??`-`)},{key:`运行时长`,value:Oe(t)},{key:`内存占用`,value:ae(t.memory)}),e}),Q,$=async e=>{await ie(e)?c(`已复制到剪贴板`,`success`):c(`复制失败`,`error`)};return d(()=>{K(),Z(),Q=window.setInterval(Z,1e4)}),e(()=>window.clearInterval(Q)),(e,n)=>{let a=p(`el-button`),o=p(`el-alert`),c=p(`el-table-column`),d=p(`el-table`),f=p(`el-card`),_=p(`el-option`),b=p(`el-select`),x=p(`el-date-picker`),S=p(`el-tag`),C=p(`el-empty`),w=ne(`loading`);return y(),u(`div`,le,[i(m)?(y(),g(o,{key:0,title:i(m),type:`error`,"show-icon":``,closable:!1},{default:h(()=>[s(a,{size:`small`,onClick:we},{default:h(()=>[...n[3]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):l(``,!0),s(f,{shadow:`never`},{header:h(()=>[...n[4]||=[t(`span`,{class:`fw-600`},`服务信息`,-1)]]),default:h(()=>[te((y(),g(d,{border:``,data:ke.value,"show-header":!1,class:`full-width`},{default:h(()=>[s(c,{prop:`key`,width:`160`}),s(c,null,{default:h(({row:e})=>[e.key===`API 地址`||e.key===`API 密钥`?(y(),u(`span`,{key:0,class:`copy-text`,onClick:t=>$(e.value)},v(e.value),9,ue)):(y(),u(`span`,de,v(e.value),1))]),_:1})]),_:1},8,[`data`])),[[w,z.value]])]),_:1}),R.value?(y(),g(f,{key:1,shadow:`never`},{header:h(()=>[t(`div`,fe,[n[6]||=t(`span`,{class:`fw-600`},`用量统计`,-1),t(`div`,pe,[s(b,{modelValue:B.value,"onUpdate:modelValue":n[0]||=e=>B.value=e,placeholder:`全部模型`,clearable:``,filterable:``,style:{width:`220px`},onChange:i(G)},{default:h(()=>[(y(!0),u(ee,null,re(xe.value,e=>(y(),g(_,{key:e,label:e,value:e},null,8,[`label`,`value`]))),128))]),_:1},8,[`modelValue`,`onChange`]),s(b,{modelValue:V.value,"onUpdate:modelValue":n[1]||=e=>V.value=e,placeholder:`时间范围`,clearable:``,style:{width:`130px`},onChange:ye},{default:h(()=>[s(_,{label:`今天`,value:`today`}),s(_,{label:`昨天`,value:`yesterday`}),s(_,{label:`前天`,value:`beforeYesterday`}),s(_,{label:`近三天`,value:`last3days`}),s(_,{label:`本周`,value:`week`}),s(_,{label:`本月`,value:`month`})]),_:1},8,[`modelValue`]),s(x,{modelValue:W.value,"onUpdate:modelValue":n[2]||=e=>W.value=e,type:`daterange`,"range-separator":`至`,"start-placeholder":`开始日期`,"end-placeholder":`结束日期`,"value-format":`YYYY-MM-DD`,style:{width:`260px`},onChange:be},null,8,[`modelValue`]),s(a,{loading:z.value,onClick:i(G)},{default:h(()=>[...n[5]||=[r(`刷新`,-1)]]),_:1},8,[`loading`,`onClick`])]),s(S,{type:`info`},{default:h(()=>[r(v(R.value.total.calls)+` 次调用`,1)]),_:1})])]),default:h(()=>[t(`div`,me,[t(`div`,he,[t(`div`,ge,v(i(E)(R.value.total.total_tokens)),1),n[7]||=t(`div`,{class:`usage-label`},`总 Token`,-1)]),t(`div`,_e,[t(`div`,D,v(i(E)(R.value.total.input_tokens)),1),n[8]||=t(`div`,{class:`usage-label`},`输入 Token`,-1)]),t(`div`,O,[t(`div`,k,v(i(E)(R.value.total.output_tokens)),1),n[9]||=t(`div`,{class:`usage-label`},`输出 Token`,-1)]),t(`div`,A,[t(`div`,j,v(i(E)(R.value.total.cache_read_tokens)),1),n[10]||=t(`div`,{class:`usage-label`},`缓存读取`,-1)]),t(`div`,M,[t(`div`,N,v(R.value.total.cache_rate==null?`-`:R.value.total.cache_rate+`%`),1),n[11]||=t(`div`,{class:`usage-label`},`缓存命中率`,-1)]),R.value.logStorageMB==null?l(``,!0):(y(),u(`div`,P,[t(`div`,F,v(R.value.logStorageMB),1),n[12]||=t(`div`,{class:`usage-label`},`数据目录占用 (MB)`,-1)]))]),R.value.models?.length?(y(),u(`div`,I,[n[13]||=t(`div`,{class:`section-title`},`模型用量`,-1),s(d,{data:R.value.models,border:``,stripe:``,class:`full-width`},{default:h(()=>[s(c,{prop:`model`,label:`模型`,"min-width":`200`},{default:h(({row:e})=>[t(`span`,{class:`copy-text`,onClick:t=>$(e.model)},v(e.model),9,L)]),_:1}),s(c,{label:`调用次数`,width:`90`,align:`center`},{default:h(({row:e})=>[r(v(e.calls),1)]),_:1}),s(c,{label:`平均耗时`,width:`110`,align:`center`},{default:h(({row:e})=>[r(v(i(oe)(e.avg_duration)),1)]),_:1}),s(c,{label:`总 Token`,width:`140`,align:`center`},{default:h(({row:e})=>[r(v(i(E)(e.total_tokens)),1)]),_:1}),s(c,{label:`输入 Token`,width:`140`,align:`center`},{default:h(({row:e})=>[r(v(i(E)(e.input_tokens)),1)]),_:1}),s(c,{label:`输出 Token`,width:`140`,align:`center`},{default:h(({row:e})=>[r(v(i(E)(e.output_tokens)),1)]),_:1}),s(c,{label:`缓存读取`,width:`130`,align:`center`},{default:h(({row:e})=>[r(v(i(E)(e.cache_read_tokens)),1)]),_:1}),s(c,{label:`缓存命中率`,width:`110`,align:`center`},{default:h(({row:e})=>[r(v(e.cache_rate==null?`-`:e.cache_rate+`%`),1)]),_:1})]),_:1},8,[`data`])])):(y(),g(C,{key:1,description:`暂无调用记录`,"image-size":60}))]),_:1})):l(``,!0)])}}}),[[`__scopeId`,`data-v-e5f81c6f`]]);export{R as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{A as e,Et as t,F as n,Ft as r,Gt as i,H as a,I as o,L as s,M as c,N as l,Q as u,T as d,at as f,d as p,gt as m,ht as h,i as g,j as _,k as v,l as y,n as b,o as x,ot as S,pn as C,tt as w,u as T,ut as E,yt as D}from"./router-
|
|
1
|
+
import{A as e,Et as t,F as n,Ft as r,Gt as i,H as a,I as o,L as s,M as c,N as l,Q as u,T as d,at as f,d as p,gt as m,ht as h,i as g,j as _,k as v,l as y,n as b,o as x,ot as S,pn as C,tt as w,u as T,ut as E,yt as D}from"./router-BqkHX02I.js";import{m as O}from"./message-CEBSyhlN.js";import{t as k}from"./index-C6wv4UB9.js";import{n as A,t as j}from"./useManagerApi-fVrRDZkc.js";import{t as M}from"./useToast-DJwG-33L.js";import{n as N,t as P}from"./version-quqH5oMc.js";a(),i(),D();var F={class:`check-body`},I={class:`version-row`},L={class:`version-row`},R={class:`muted-sm`},z=p(s({__name:`UpdateCheckDialog`,props:{modelValue:{type:Boolean,default:!1},modelModifiers:{}},emits:[`update:modelValue`],setup(i){let{toast:a}=M(),s=E(i,`modelValue`),u=t(!1),p=t(!1),g=t(null),v=t(null),y=t(null),b=t(null),x=null,T=()=>x?.abort(),D=e=>new Date(e).toLocaleString(),O=e=>`https://www.npmjs.com/package/ezllm/v/${e}`,z=async()=>{v.value=null,y.value=null,b.value=null,u.value=!0;try{let e=await j.update();if(g.value=e,!e.supported)return;v.value=await j.checkUpdate()}catch(e){y.value=e.message||`检查更新失败`}finally{u.value=!1}},B=async()=>{if(!v.value)return;let e=v.value.latestVersion;try{await k.confirm(`确定更新到 ${e}?服务会自动重启并在启动时安装新版本:重启期间管理台短暂不可访问(预计 1~3 分钟),更新完成后自动恢复。`,`更新确认`,{confirmButtonText:`立即更新`,cancelButtonText:`取消`,type:`warning`})}catch{return}p.value=!0;let t=new AbortController;x=t;try{let e=await j.update(),n=await j.applyUpdate();if(!n.updated){b.value=`已是最新版本 ${n.toVersion}`,v.value={hasUpdate:!1,currentVersion:n.toVersion,latestVersion:n.toVersion,checkedAt:Date.now()};return}b.value=`正在重启服务并安装 ${n.toVersion}(预计 1~3 分钟):重启期间管理台短暂不可访问,完成后这里会自动显示结果。`;let r=await A({targetVersion:n.toVersion,sinceAppliedAt:e.lastApply?.appliedAt??null,signal:t.signal});if(t.signal.aborted)return;r&&!r.error?(a(`已更新到 ${r.toVersion}`,`success`),b.value=`已更新 ${r.fromVersion} → ${r.toVersion},服务已在新版本上运行。`,v.value={hasUpdate:!1,currentVersion:r.toVersion,latestVersion:r.toVersion,checkedAt:Date.now()}):r?.error?(a(`更新失败:${r.error}`,`error`),b.value=N(r.error)):b.value=`更新仍在进行(安装耗时较长):稍后刷新页面查看最新状态。`}catch(e){a(e.message||`更新失败`,`error`)}finally{p.value=!1,x===t&&(x=null)}};return(t,i)=>{let a=f(`el-alert`),x=f(`el-tag`),E=f(`el-link`),k=f(`el-button`),A=f(`el-dialog`),j=S(`loading`);return w(),_(A,{modelValue:s.value,"onUpdate:modelValue":i[2]||=e=>s.value=e,title:`检查更新`,width:`420px`,"close-on-click-modal":!p.value,onOpen:z,onClose:T},{footer:h(()=>[v.value?.hasUpdate?(w(),_(k,{key:0,type:`primary`,loading:p.value,onClick:B},{default:h(()=>[n(C(p.value?`更新中...`:`立即更新`),1)]),_:1},8,[`loading`])):c(``,!0),o(k,{onClick:i[1]||=e=>s.value=!1},{default:h(()=>[...i[11]||=[n(`关闭`,-1)]]),_:1})]),default:h(()=>[m((w(),l(`div`,F,[g.value&&!g.value.supported?(w(),_(a,{key:0,title:`当前为源码运行模式,更新功能不可用`,type:`info`,"show-icon":``,closable:!1},{default:h(()=>[...i[3]||=[e(`div`,{class:`alert-hint`},[n(` 仅 npm 全局安装的包支持自动更新。本地开发用仓库里的 `),e(`code`,null,`pnpm upgrade:local`),n(` 更新运行中的服务。 `)],-1)]]),_:1})):v.value?(w(),l(d,{key:1},[e(`div`,I,[i[5]||=e(`span`,{class:`version-label`},`当前版本`,-1),o(x,{size:`small`},{default:h(()=>[n(C(v.value.currentVersion),1)]),_:1}),r(P)(v.value.currentVersion)?(w(),_(x,{key:0,type:`warning`,size:`small`,"disable-transitions":``},{default:h(()=>[...i[4]||=[n(`本地构建`,-1)]]),_:1})):c(``,!0)]),e(`div`,L,[i[6]||=e(`span`,{class:`version-label`},`最新版本`,-1),o(x,{type:v.value.hasUpdate?`warning`:`success`,size:`small`,"disable-transitions":``},{default:h(()=>[n(C(v.value.latestVersion),1)]),_:1},8,[`type`])]),o(a,{title:v.value.hasUpdate?`发现新版本 ${v.value.latestVersion}(当前 ${v.value.currentVersion})`:`已是最新版本 ${v.value.currentVersion}`,type:v.value.hasUpdate?`warning`:`success`,"show-icon":``,closable:!1,class:`mt-12`},null,8,[`title`,`type`]),e(`div`,R,[n(` 检查时间:`+C(D(v.value.checkedAt))+` `,1),v.value.hasUpdate?(w(),l(d,{key:0},[i[8]||=n(` · `,-1),o(E,{type:`primary`,href:O(v.value.latestVersion),target:`_blank`},{default:h(()=>[...i[7]||=[n(`在 npm 查看该版本`,-1)]]),_:1},8,[`href`])],64)):c(``,!0)])],64)):!u.value&&y.value?(w(),_(a,{key:2,title:y.value,type:`error`,"show-icon":``,closable:!1},{default:h(()=>[i[10]||=e(`div`,{class:`alert-hint`},` 检查需访问 npm 源,可能是网络不可达或版本号解析失败;稍后重试即可。 `,-1),o(k,{size:`small`,class:`mt-8`,onClick:z},{default:h(()=>[...i[9]||=[n(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):c(``,!0)])),[[j,u.value]]),b.value?(w(),_(a,{key:0,title:b.value,type:`info`,"show-icon":``,class:`mt-12`,onClose:i[0]||=e=>b.value=null},null,8,[`title`])):c(``,!0)]),_:1},8,[`modelValue`,`close-on-click-modal`])}}}),[[`__scopeId`,`data-v-ea1efda8`]]);a(),i(),D();var B={class:`layout`},V={class:`header-nav`},H={class:`header-actions`},U={class:`main`},W={class:`page`},G=s({__name:`LayoutView`,setup(i){let a=y(),s=T(),d=t(!1);u(()=>{b()});let p=v(()=>g.value?.authRequired!==!1),m=v(()=>{if(!p.value)return`免登录`;let e=localStorage.getItem(`webui_token`)||``;try{return atob(e).split(`:`)[0]||`用户`}catch{return`用户`}}),S=v(()=>{let e=a.path;return e===`/`?`/`:e.startsWith(`/providers`)?`/providers`:e.startsWith(`/service-status`)?`/service-status`:e.startsWith(`/settings`)?`/settings`:e.startsWith(`/logs`)?`/logs`:`/`}),E=()=>{x(),s.push(`/login`)},D=e=>{e===`logout`&&E(),e===`check-update`&&(d.value=!0)};return(t,i)=>{let a=f(`el-menu-item`),s=f(`el-menu`),u=f(`el-icon`),g=f(`el-button`),v=f(`el-dropdown-item`),y=f(`el-dropdown-menu`),b=f(`el-dropdown`),x=f(`el-header`),T=f(`router-view`);return w(),l(`div`,B,[o(x,{class:`header`},{default:h(()=>[i[8]||=e(`h1`,null,[e(`span`,null,`E`),n(`ZLLM`)],-1),e(`nav`,V,[o(s,{mode:`horizontal`,"default-active":S.value,ellipsis:!1,router:``,class:`header-menu`},{default:h(()=>[o(a,{index:`/`},{default:h(()=>[...i[1]||=[n(`首页`,-1)]]),_:1}),o(a,{index:`/providers`},{default:h(()=>[...i[2]||=[n(`供应商管理`,-1)]]),_:1}),o(a,{index:`/service-status`},{default:h(()=>[...i[3]||=[n(`服务状态`,-1)]]),_:1}),o(a,{index:`/logs`},{default:h(()=>[...i[4]||=[n(`日志`,-1)]]),_:1}),o(a,{index:`/settings`},{default:h(()=>[...i[5]||=[n(`设置`,-1)]]),_:1})]),_:1},8,[`default-active`])]),e(`div`,H,[o(b,{trigger:`click`,onCommand:D,class:`ml-12`},{dropdown:h(()=>[o(y,null,{default:h(()=>[o(v,{command:`check-update`},{default:h(()=>[...i[6]||=[n(`检查更新`,-1)]]),_:1}),p.value?(w(),_(v,{key:0,command:`logout`,divided:``},{default:h(()=>[...i[7]||=[n(`退出`,-1)]]),_:1})):c(``,!0)]),_:1})]),default:h(()=>[o(g,null,{default:h(()=>[n(C(m.value)+` `,1),o(u,{class:`el-icon--right`},{default:h(()=>[o(r(O))]),_:1})]),_:1})]),_:1})])]),_:1}),e(`main`,U,[e(`div`,W,[o(T)])]),o(z,{modelValue:d.value,"onUpdate:modelValue":i[0]||=e=>d.value=e},null,8,[`modelValue`])])}}});export{G as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{A as e,C as t,Et as n,F as r,H as i,I as a,L as o,M as s,N as c,S as l,a as u,at as d,c as f,d as p,h as m,ht as h,j as g,tt as _,u as v,yt as y}from"./router-
|
|
1
|
+
import{A as e,C as t,Et as n,F as r,H as i,I as a,L as o,M as s,N as c,S as l,a as u,at as d,c as f,d as p,h as m,ht as h,j as g,tt as _,u as v,yt as y}from"./router-BqkHX02I.js";i(),m(),y();var b={class:`login-wrap`},x=p(o({__name:`LoginView`,setup(i){let o=v(),p=n(``),m=n(``),y=n(``),x=n(!1);async function S(){if(!p.value){y.value=`请输入用户名`;return}if(!m.value){y.value=`请输入密码`;return}x.value=!0,y.value=``;try{let e=await u.post(`/api/auth/login`,{username:p.value.trim(),password:m.value});f(e.token),o.push(`/`)}catch(e){y.value=e.message||`登录失败`}finally{x.value=!1}}return(n,i)=>{let o=d(`el-input`),u=d(`el-form-item`),f=d(`el-alert`),v=d(`el-button`),C=d(`el-form`),w=d(`el-card`);return _(),c(`div`,b,[a(w,{shadow:`hover`,class:`login-card`},{default:h(()=>[i[3]||=e(`h2`,{class:`login-title`},`EZLLM`,-1),a(C,{"label-position":`top`,onSubmit:t(S,[`prevent`])},{default:h(()=>[a(u,{label:`用户名`},{default:h(()=>[a(o,{modelValue:p.value,"onUpdate:modelValue":i[0]||=e=>p.value=e,placeholder:`请输入用户名`,onKeyup:l(S,[`enter`])},null,8,[`modelValue`])]),_:1}),a(u,{label:`密码`},{default:h(()=>[a(o,{modelValue:m.value,"onUpdate:modelValue":i[1]||=e=>m.value=e,type:`password`,placeholder:`请输入密码`,"show-password":``,onKeyup:l(S,[`enter`])},null,8,[`modelValue`])]),_:1}),y.value?(_(),g(f,{key:0,title:y.value,type:`error`,"show-icon":``,class:`mb-16`},null,8,[`title`])):s(``,!0),a(v,{type:`primary`,class:`full-width`,loading:x.value,onClick:S},{default:h(()=>[...i[2]||=[r(` 登录 `,-1)]]),_:1},8,[`loading`])]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-c8c59fbd`]]);export{x as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{A as e,Et as t,F as n,Ft as r,Gt as i,H as a,I as o,L as s,M as c,N as l,Q as u,S as d,T as f,a as p,at as m,d as h,gt as ee,h as g,ht as _,j as v,k as y,ot as b,pn as x,rt as S,tt as C,yt as w}from"./router-
|
|
1
|
+
import{A as e,Et as t,F as n,Ft as r,Gt as i,H as a,I as o,L as s,M as c,N as l,Q as u,S as d,T as f,a as p,at as m,d as h,gt as ee,h as g,ht as _,j as v,k as y,ot as b,pn as x,rt as S,tt as C,yt as w}from"./router-BqkHX02I.js";import{t as T}from"./index-C6wv4UB9.js";import{t as E}from"./providers-DkujdEZ8.js";import{t as D}from"./useToast-DJwG-33L.js";import{t as O}from"./useLoadError-Yu7YH4ZH.js";import{t as k}from"./latestRequest-Dk21IcrH.js";import{t as A}from"./clipboard-CorU9X8e.js";import{c as j,l as M,n as N,o as P,r as te,s as F}from"./format-DrbP4_cY.js";a(),w(),i();var I={key:1,class:`detail-content`},L={key:0,class:`ip-link`},R={class:`ip-pop`},z={class:`ip-pop-row`},B={key:0,class:`ip-pop-row`},V={key:1,class:`ip-pop-error`},H={key:2},U=[`href`],W={key:1},G={key:1},ne={class:`error-text`},re={class:`pane-bar`},ie={class:`json-block`},K={class:`pane-bar`},q={class:`json-block`},J={class:`pane-bar`},ae={class:`json-block`},Y={class:`pane-bar`},oe={class:`json-block`},X={class:`pane-bar`},Z={class:`json-block`},se={class:`pane-bar`},Q={class:`json-block`},ce={class:`pane-bar`},le={class:`json-block`},ue={class:`pane-bar`},de={class:`json-block`},fe=h(s({__name:`LogDetailDialog`,setup(i,{expose:a}){let{toast:s}=D(),{loadError:u,run:d}=O(),h=t(!1),g=t(null),y=t(`info`),S=t(!1),w=t(null),T=k(),E=e=>{w.value=e,h.value=!0,g.value=null,y.value=`info`;let t=T();d(async()=>{try{let n=e.id?`?id=${e.id}`:`?_file=${encodeURIComponent(e._file)}`,r=await p.get(`/api/logs/detail`+n);if(!t())return;r&&r.detail?g.value={...r.detail,...r.meta}:g.value=r}catch(e){if(!t())return;throw e}})},fe=()=>{w.value&&E(w.value)};a({open:E});let $=async e=>{await A(e)?s(`已复制到剪贴板`,`success`):s(`复制失败`,`error`)},pe=async e=>{if(!(e._ipInfo||e._ipInfoError)){S.value=!0;try{let t=await p.get(`/api/logs/ipinfo?ip=${encodeURIComponent(e.ip)}`);t.error?e._ipInfoError=t.error:e._ipInfo=t}catch{e._ipInfoError=`查询失败`}finally{S.value=!1}}},me=e=>[e.country,e.region,e.city].filter(Boolean).join(` · `)||`-`;return(t,i)=>{let a=m(`el-button`),s=m(`el-alert`),d=m(`el-descriptions-item`),p=m(`el-tag`),w=m(`el-divider`),T=m(`el-popover`),E=m(`el-descriptions`),D=m(`el-tab-pane`),O=m(`el-tabs`),k=m(`el-dialog`),A=b(`loading`);return C(),v(k,{modelValue:h.value,"onUpdate:modelValue":i[11]||=e=>h.value=e,title:`日志详情`,width:`80%`,top:`5vh`,"destroy-on-close":``},{default:_(()=>[r(u)?(C(),v(s,{key:0,title:r(u),type:`error`,"show-icon":``,closable:!1,class:`mb-16`},{default:_(()=>[o(a,{size:`small`,onClick:fe},{default:_(()=>[...i[12]||=[n(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):c(``,!0),g.value?(C(),l(`div`,I,[o(O,{modelValue:y.value,"onUpdate:modelValue":i[10]||=e=>y.value=e},{default:_(()=>[o(D,{label:`基本信息`,name:`info`},{default:_(()=>[o(E,{column:2,border:``},{default:_(()=>[o(d,{label:`时间`},{default:_(()=>[n(x(r(j)(g.value.time)),1)]),_:1}),o(d,{label:`模型`},{default:_(()=>[n(x(g.value.model),1)]),_:1}),o(d,{label:`方法`},{default:_(()=>[n(x(g.value.method),1)]),_:1}),o(d,{label:`路径`},{default:_(()=>[n(x(g.value.path),1)]),_:1}),o(d,{label:`状态码`},{default:_(()=>[n(x(g.value.status),1)]),_:1}),o(d,{label:`流式`},{default:_(()=>[o(p,{type:g.value.stream?`warning`:`info`},{default:_(()=>[n(x(g.value.stream?`是`:`否`),1)]),_:1},8,[`type`])]),_:1}),o(d,{label:`IP`},{default:_(()=>[g.value.ip?(C(),l(`span`,L,[o(T,{placement:`right`,width:`240`,trigger:`click`,onShow:i[0]||=e=>pe(g.value)},{reference:_(()=>[e(`span`,null,x(g.value.ip),1)]),default:_(()=>[ee((C(),l(`div`,R,[g.value._ipInfo?(C(),l(f,{key:0},[e(`div`,z,[i[13]||=e(`span`,{class:`ip-pop-label`},`归属地`,-1),e(`span`,null,x(me(g.value._ipInfo)),1)]),g.value._ipInfo.isp?(C(),l(`div`,B,[i[14]||=e(`span`,{class:`ip-pop-label`},`运营商`,-1),e(`span`,null,x(g.value._ipInfo.isp),1)])):c(``,!0)],64)):g.value._ipInfoError?(C(),l(`div`,V,x(g.value._ipInfoError),1)):(C(),l(`span`,H,`查询中...`)),o(w,{style:{margin:`8px 0`}}),e(`a`,{class:`ip-pop-link`,href:`https://m.ip138.com/iplookup.php?ip=${encodeURIComponent(g.value.ip)}`,target:`_blank`},`在 ip138 查看`,8,U)])),[[A,S.value]])]),_:1})])):(C(),l(`span`,W,`-`))]),_:1}),o(d,{label:`Session`},{default:_(()=>[n(x(g.value.session_id||`-`),1)]),_:1}),o(d,{label:`Request ID`},{default:_(()=>[g.value.request_id?(C(),l(`span`,{key:0,class:`request-id`,title:`点击复制(可用于关联控制台日志与检索)`,onClick:i[1]||=e=>$(g.value.request_id)},x(g.value.request_id),1)):(C(),l(`span`,G,`-`))]),_:1}),o(d,{label:`输入 Token(含缓存读取)`},{default:_(()=>[n(x(r(P)(g.value.input_tokens??0)),1)]),_:1}),o(d,{label:`输出 Token`},{default:_(()=>[n(x(r(P)(g.value.output_tokens??0)),1)]),_:1}),o(d,{label:`缓存读取`},{default:_(()=>[n(x(r(P)(g.value.cache_read_tokens??0)),1)]),_:1}),o(d,{label:`请求耗时`},{default:_(()=>[n(x(r(te)(g.value.duration)),1)]),_:1}),o(d,{label:`缓存命中率`},{default:_(()=>[n(x(r(N)(g.value)),1)]),_:1}),g.value.error?(C(),v(d,{key:0,label:`错误`,span:2},{default:_(()=>[e(`span`,ne,x(g.value.error),1)]),_:1})):c(``,!0)]),_:1}),g.value.response?.truncated?(C(),v(s,{key:0,class:`mt-16`,type:`warning`,closable:!1,title:`内容超过单请求捕获上限,已截断`})):c(``,!0)]),_:1}),g.value.response?._capture?(C(),l(f,{key:0},[o(D,{label:`客户端请求`,name:`inbound`},{default:_(()=>[e(`div`,re,[o(a,{size:`small`,class:`copy-btn`,onClick:i[2]||=e=>$(r(F)(g.value.response.inbound_request))},{default:_(()=>[...i[15]||=[n(` 复制 `,-1)]]),_:1})]),e(`pre`,ie,x(r(F)(g.value.response.inbound_request)),1)]),_:1}),o(D,{label:`转发上游的请求`,name:`upstreamReq`},{default:_(()=>[e(`div`,K,[o(a,{size:`small`,class:`copy-btn`,onClick:i[3]||=e=>$(r(F)(g.value.response.upstream_request))},{default:_(()=>[...i[16]||=[n(` 复制 `,-1)]]),_:1})]),e(`pre`,q,x(r(F)(g.value.response.upstream_request)),1)]),_:1}),g.value.response.upstream_response_text?(C(),v(D,{key:0,label:`上游响应(原始)`,name:`upstreamRes`},{default:_(()=>[e(`div`,J,[o(a,{size:`small`,class:`copy-btn`,onClick:i[4]||=e=>$(g.value.response.upstream_response_text)},{default:_(()=>[...i[17]||=[n(` 复制 `,-1)]]),_:1})]),e(`pre`,ae,x(r(M)(g.value.response.upstream_response_text)),1)]),_:1})):g.value.response.upstream_stream_frames?.length?(C(),v(D,{key:1,label:`上游响应(原始流,${g.value.response.upstream_stream_frames.length} 帧)`,name:`upstreamStream`},{default:_(()=>[e(`div`,Y,[o(a,{size:`small`,class:`copy-btn`,onClick:i[5]||=e=>$(g.value.response.upstream_stream_frames.join(`
|
|
2
2
|
|
|
3
3
|
`))},{default:_(()=>[...i[18]||=[n(` 复制 `,-1)]]),_:1})]),e(`pre`,oe,x(g.value.response.upstream_stream_frames.join(`
|
|
4
4
|
|