ellm-proxy 0.0.6 → 0.0.8
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 +29 -11
- package/bin/ellm-proxy.js +2 -60
- package/dist/cli.js +35780 -0
- package/dist/server.js +16714 -6336
- package/dist/web-ui/assets/DashboardView-3c_i_3VF.js +1 -0
- package/dist/web-ui/assets/LayoutView-BhN1f8wo.js +1 -0
- package/dist/web-ui/assets/LayoutView-CNd_MJt2.css +1 -0
- package/dist/web-ui/assets/LoginView-J23vsMu2.js +1 -0
- package/dist/web-ui/assets/{LogsView-BlXITEJH.js → LogsView-2h2Y0XE8.js} +2 -2
- package/dist/web-ui/assets/ProcessView-Bn8Ph7rh.css +1 -0
- package/dist/web-ui/assets/ProcessView-Bs2k6aAf.js +1 -0
- package/dist/web-ui/assets/{ProvidersView-f8aZkMdw.js → ProvidersView-bE1Hv9D-.js} +2 -2
- package/dist/web-ui/assets/SettingsView-BmLmC-_v.js +1 -0
- package/dist/web-ui/assets/SettingsView-Ceyj-ZDZ.css +1 -0
- package/dist/web-ui/assets/SetupView-Cgj7ubNz.js +1 -0
- package/dist/web-ui/assets/SetupView-kPJXJ7dX.css +1 -0
- package/dist/web-ui/assets/config-nvSp8hHp.js +1 -0
- package/dist/web-ui/assets/{index-GohB4JDa.js → index-sPrBBnf5.js} +28 -28
- package/dist/web-ui/assets/{providers-Djtdy2tv.js → providers-di7L8jy_.js} +1 -1
- package/dist/web-ui/assets/useManagerApi-qPjxfB0X.js +1 -0
- package/dist/web-ui/assets/useToast-D76qyWZg.js +1 -0
- package/dist/web-ui/config.js +3 -6
- package/dist/web-ui/index.html +1 -1
- package/docs/DESIGN.md +43 -14
- package/ellm.config.example.json +2 -0
- package/package.json +2 -17
- package/bin/commands/autostart.js +0 -37
- package/bin/commands/config.js +0 -8
- package/bin/commands/pm2.js +0 -28
- package/bin/commands/service.js +0 -107
- package/dist/web-ui/assets/DashboardView-C3Hw-HpG.js +0 -1
- package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +0 -1
- package/dist/web-ui/assets/LayoutView-C9phf7in.js +0 -1
- package/dist/web-ui/assets/LoginView-es6pSPgt.js +0 -1
- package/dist/web-ui/assets/ProcessView-Bgf7-MLQ.js +0 -1
- package/dist/web-ui/assets/ProcessView-DK0TvDpR.css +0 -1
- package/dist/web-ui/assets/SettingsView-BkwyoOUb.css +0 -1
- package/dist/web-ui/assets/SettingsView-GvAA2vLp.js +0 -1
- package/dist/web-ui/assets/config-Cs3_z63C.js +0 -1
- package/dist/web-ui/assets/useApi-D_iJ2Mca.js +0 -1
- package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +0 -1
- package/dist/web-ui/assets/useToast-C0UMTbkx.js +0 -1
- package/manager/api/harness/backup.js +0 -20
- package/manager/api/harness/claude-code.js +0 -54
- package/manager/api/harness/codex.js +0 -123
- package/manager/api/harness/dsh.js +0 -175
- package/manager/api/harness/index.js +0 -118
- package/manager/api/service-settings.js +0 -98
- package/manager/api/service.js +0 -147
- package/manager/index.js +0 -182
- package/utils/autostart.js +0 -219
- package/utils/interpreter.js +0 -14
- package/utils/paths.js +0 -45
- package/utils/pm2.js +0 -130
- package/utils/port.js +0 -35
- package/utils/service.js +0 -205
- package/utils/ui.js +0 -59
- package/utils/updater.js +0 -210
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{X as w,r as u,l as i}from"./index-sPrBBnf5.js";import{u as C}from"./config-nvSp8hHp.js";const J=w("providers",()=>{const s=C(),c=u(!1),n=i(()=>s.providers),v=i(()=>s.loading),f=i(()=>n.value.length),m=i(()=>n.value.reduce((e,o)=>{var r;return e+(((r=o.models)==null?void 0:r.length)??0)},0)),t=u([]);function p(){try{const e=localStorage.getItem("provider_order");e&&(t.value=JSON.parse(e))}catch{}}function d(){try{localStorage.setItem("provider_order",JSON.stringify(t.value))}catch{}}const g=i(()=>{const e=t.value.map(r=>n.value.find(a=>a.name===r)).filter(r=>!!r);for(const r of n.value)e.find(a=>a.name===r.name)||e.push(r);const o=e.map(r=>r.name);return(o.length!==t.value.length||o.some((r,a)=>t.value[a]!==r))&&(t.value=o,d()),e});function S(e){t.value=e.map(o=>o.name),d()}function h(e){t.value=[e,...t.value.filter(o=>o!==e)],d()}async function y(){p(),await s.load()}async function l(e){return s.config.providers=e,await s.save()}async function P(e){const o=n.value.filter(r=>r.name!==e.name);return await l(o)}async function O(e){const o=n.value.map(r=>({...r,models:(r.models??[]).filter(a=>a.modelName!==e)}));return await l(o)}return{suppliers:n,loading:v,restarting:c,orderedProviders:g,providerCount:f,totalModelCount:m,loadProviders:y,saveProviders:l,deleteProvider:P,deleteModel:O,reorderProviders:S,moveProviderToTop:h}});export{J as u};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a9 as w}from"./index-sPrBBnf5.js";var T;const h=((T=window.__ELLM_CONFIG__)==null?void 0:T.API_BASE)??"",f=3e4;let u=null;function E(){return u===null&&(u=localStorage.getItem("webui_token")),u}function S(){u=null,localStorage.removeItem("webui_token"),w.push("/login")}async function e(t,c,n,p=f){const i={"Content-Type":"application/json"},a=E();a&&(i.Authorization=`Bearer ${a}`);const s=new AbortController,o=setTimeout(()=>s.abort(),p);try{const r=await fetch(`${h}${c}`,{method:t,headers:i,body:n!==void 0?JSON.stringify(n):void 0,signal:s.signal});if(r.status===401)throw S(),new Error("未登录");const d=await r.text();let l;try{l=d?JSON.parse(d):{}}catch{throw new Error(`响应格式错误 (HTTP ${r.status})`)}if(!r.ok||l.ok===!1)throw new Error(l.error||`HTTP ${r.status}`);return l}catch(r){throw r.name==="AbortError"?new Error("请求超时"):r instanceof TypeError?new Error("服务不可达(重启窗口期或未启动,请稍后重试)"):r}finally{clearTimeout(o)}}const P={status:()=>e("GET","/status"),processes:()=>e("GET","/processes").then(t=>t.processes),start:()=>e("POST","/start"),stop:()=>e("POST","/stop"),restart:()=>e("POST","/restart"),autostart:()=>e("GET","/autostart"),setAutostart:t=>e("POST","/autostart",{enabled:t}),update:()=>e("GET","/update"),setUpdateEnabled:t=>e("POST","/update/enabled",{enabled:t}),checkUpdate:()=>e("POST","/update/check"),applyUpdate:()=>e("POST","/update/apply"),harness:()=>e("GET","/harness"),setClaudeCode:t=>e("POST","/harness/claude-code",{enabled:t}),setCodex:t=>e("POST","/harness/codex",{enabled:t}),setDsh:t=>e("POST","/harness/dsh",{enabled:t})};async function A(t){const{targetVersion:c,sinceAppliedAt:n,timeoutMs:p=600*1e3,pollMs:i=3e3}=t,a=Date.now()+p;for(;;){if(Date.now()>=a)return null;await new Promise(r=>setTimeout(r,i));let s=null;try{s=await P.update()}catch{continue}const o=s.lastApply;if(o&&o.toVersion===c&&(o.appliedAt??0)>(n??0))return o;if(Date.now()>=a)return null}}export{P as m,A as w};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{aa as o}from"./index-sPrBBnf5.js";function u(){function s(t,a="success"){o({message:t,type:a,duration:3e3})}return{toast:s}}export{u};
|
package/dist/web-ui/config.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// web-ui 运行时配置(部署后可直接改这个文件,无需重新构建)
|
|
2
|
-
//
|
|
3
|
-
// API_BASE:管理台 API
|
|
4
|
-
//
|
|
5
|
-
// MANAGER_BASE:进程/代理开关接口(/status /restart /harness…)的基地址。
|
|
6
|
-
// 留空 = 同源(manager 托管形态);独立部署且需进程管理页时填 manager 地址,如 "http://127.0.0.1:29771"。
|
|
2
|
+
// 标准形态:服务进程托管本目录(http://127.0.0.1:<port>/),API_BASE 留空即全同源,无需修改。
|
|
3
|
+
// API_BASE:管理台 API 与管理域接口(/api/*、/status、/harness…)的基地址。
|
|
4
|
+
// 留空 = 同源;仅当把 dist 部署到其他静态服务器时填写,如 "http://192.168.1.10:29770"。
|
|
7
5
|
window.__ELLM_CONFIG__ = {
|
|
8
6
|
API_BASE: "",
|
|
9
|
-
MANAGER_BASE: "",
|
|
10
7
|
};
|
package/dist/web-ui/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>ELLM - 网关管理台</title>
|
|
8
8
|
<script src="/config.js"></script>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-sPrBBnf5.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-BecfPmag.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/docs/DESIGN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 1. 背景与目标
|
|
6
6
|
|
|
7
|
-
`ellm` monorepo(`gateway` 协议转换层 / `
|
|
7
|
+
`ellm` monorepo(`gateway` 协议转换层 / `server` 管理台服务,原名 backend / `web-ui` Vue 前端)原为纯本地工程,目标是把 backend 单进程应用(内嵌网关 + 管理台 API)改造为可发布的 npm 包 **`ellm-proxy`**(2026-09-03 起 web-ui 由 manager 管理服务托管,backend 不再托管):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm i -g ellm-proxy
|
|
@@ -28,6 +28,7 @@ ellm-proxy stop # 停止后台服务
|
|
|
28
28
|
| 10 | 默认端口 | 3000 太常用易冲突 → 随机定为 **29770**(避开 49152+ 系统动态端口区) |
|
|
29
29
|
| 11 | 不内嵌 node.exe | 服务要求的 Node ≥ 22.13.0 由 CLI 解析为 pm2 `interpreter`:CLI 自身 node 满足则用 `process.execPath`,否则经 **get-node**(随包依赖)解析/下载(缓存命中零下载);pm2 daemon 的 node 版本无关紧要 |
|
|
30
30
|
| 12 | web-ui 移交 manager 托管(2026-09-03) | backend 移除 `/web-ui` 静态托管(删 `webui-static.ts`);web-ui 改由 manager 管理服务托管:根路径静态资源 + 反代 `/api/*` 到主服务(端口每请求解析,主服务顺延漂移无感知),页面全接口同源零配置(`config.js` 的 `API_BASE`/`MANAGER_BASE` 留空);发布包恢复捆绑 `dist/web-ui/` 供 manager 定位(`ELLM_WEBUI_DIR` 可选覆盖,CLI 不再注入);manager 仍仅监听 127.0.0.1 |
|
|
31
|
+
| 13 | 去 pm2 + 单进程单端口(2026-09-06) | 详见 §6。删除 pm2/get-node 依赖与 manager/ 目录,全部运行时逻辑(管理域 API、CLI、进程管理原语)TS 化收进 backend 源码;单端口 29770 承载网关 + 管理台(管理 API 挂登录鉴权),29771 退役 |
|
|
31
32
|
|
|
32
33
|
## 3. 架构设计
|
|
33
34
|
|
|
@@ -35,18 +36,16 @@ ellm-proxy stop # 停止后台服务
|
|
|
35
36
|
|
|
36
37
|
```
|
|
37
38
|
ellm-proxy/
|
|
38
|
-
├── bin/ellm-proxy.js # CLI
|
|
39
|
-
├── dist/server.js #
|
|
40
|
-
├── dist/
|
|
41
|
-
├──
|
|
42
|
-
├── scripts/build-server.mjs
|
|
43
|
-
├── scripts/build-package.mjs
|
|
44
|
-
└──
|
|
39
|
+
├── bin/ellm-proxy.js # CLI shim(import dist/cli.js;npm bin 指向)
|
|
40
|
+
├── dist/server.js # 服务本体(esbuild 从 server 源码构建 + shebang/createRequire banner)
|
|
41
|
+
├── dist/cli.js # CLI(esbuild 从 server/src/cli 构建,与 server 共享 proc/paths 原语)
|
|
42
|
+
├── dist/web-ui/ # web-ui vite 构建产物(服务进程托管于 http://127.0.0.1:<port>/)
|
|
43
|
+
├── scripts/build-server.mjs | build-cli.mjs # 发布构建(esbuild 双入口)
|
|
44
|
+
├── scripts/build-package.mjs # 聚合 + 门禁(prepack:构建 server+cli → 拷 web-ui → 复制 example → shebang 校验)
|
|
45
|
+
└── manager/、utils/、bin/commands/ → 已删除(2026-09-06 单进程化,逻辑全部迁入 server 包源码(backend 包同日改名 server))
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
发布 manifest 要点:`engines >=22.13.0`(node:sqlite 无 flag 最低版);`bin=bin/ellm-proxy.js`;`files` 白名单(bin/、dist/、example、README、LICENSE、docs);不加 `private`;`dependencies` 为 `cac@^6`(CLI 框架)、`pm2@^7`(进程守护,局部随包分发)与 `get-node@^15`(解析满足 engines 的 node 作为 pm2 interpreter);bundle 自包含;发布时用 `npm publish`(不改写 manifest)。
|
|
48
|
+
发布 manifest 要点:`engines >=22.13.0`(node:sqlite 无 flag 最低版);`bin=bin/ellm-proxy.js`;`files` 白名单(bin/、dist/、example、README、LICENSE、docs);**`dependencies` 为空**(0.0.8 起全部逻辑 bundle 自包含,pm2/get-node 等依赖已删除);发布时用 `npm publish`(不改写 manifest)。
|
|
50
49
|
|
|
51
50
|
### 3.2 路径全 env 化(核心设计)
|
|
52
51
|
|
|
@@ -54,7 +53,7 @@ ellm-proxy/
|
|
|
54
53
|
|
|
55
54
|
| 配置 | 环境变量 | 缺省公式(仓库模式自动正确) | 包模式来源 |
|
|
56
55
|
| --- | --- | --- | --- |
|
|
57
|
-
| 应用目录(配置/数据基准) | `ELLM_APP_DIR` | `
|
|
56
|
+
| 应用目录(配置/数据基准) | `ELLM_APP_DIR` | `server/`(hereDir 推导) | start 脚本注入 = 工作目录(默认 `~/.ellm-proxy`) |
|
|
58
57
|
| 配置文件 | `ELLM_CONFIG` | `<APP_DIR>/ellm.config.json` | CLI `--config` 或用户 env |
|
|
59
58
|
| 数据目录 | `ELLM_DATA_DIR` | `<APP_DIR>/data` | CLI `--data-dir` 或用户 env |
|
|
60
59
|
| web-ui 静态资源 | `ELLM_WEBUI_DIR`(可选覆盖) | —(manager 按自身位置推导:包模式 `dist/web-ui` → 仓库模式 `web-ui/dist`) | manager 启动时自解析(CLI 不再注入) |
|
|
@@ -63,7 +62,7 @@ ellm-proxy/
|
|
|
63
62
|
|
|
64
63
|
设计约束与边界:
|
|
65
64
|
|
|
66
|
-
- **backend
|
|
65
|
+
- **server(原 backend)源码零模式分支**:`paths.ts` 只有「env ?? 公式」两种取值,不存在模式判断;仓库开发模式不设任何 env 即工作。
|
|
67
66
|
- **包模式必须经 CLI 启动**:绕过它裸跑 `dist/server.js` 时 env 未注入,会因找不到配置退出(报错信息指引)。官方入口始终是 `ellm-proxy start/stop`。
|
|
68
67
|
- **CLI 在转调前把路径参数统一转为绝对路径**:CLI 的 cwd 与 node 进程的工作目录不同,透传相对字符串会在另一基准下解析错位。
|
|
69
68
|
- **CLI 负责首次配置自愈**(server 端只读不建):`ensureConfig()` 查找链与 server 端一致,创建后提示编辑或 web-ui 修改。
|
|
@@ -79,7 +78,7 @@ ellm-proxy/
|
|
|
79
78
|
|
|
80
79
|
1. **`gateway/src/index.ts` 必须保持零顶层副作用**:backend 经 esbuild 全量内联 gateway 源码,任何 import 副作用都会执行。曾因 index.ts 顶层读 `ELLM_CONFIG` 装配 default app 与 backend 同名 env 撞车(实测踩坑)。独立网关装配已抽至 `src/standalone-app.ts`(`buildStandaloneApp()`)。
|
|
81
80
|
2. **包内布局约束**:`dist/web-ui/` 与 `dist/server.js` 同级(manager 缺省推导依赖此布局;`ELLM_WEBUI_DIR` 可覆盖)。
|
|
82
|
-
3. **`
|
|
81
|
+
3. **`server/ellm.config.json`(原 backend/)仍是配置唯一数据源**(原样读写不变式在发布包同样成立)。
|
|
83
82
|
|
|
84
83
|
## 4. 方案演进史(三轮,记录否决原因避免重走)
|
|
85
84
|
|
|
@@ -97,3 +96,33 @@ ellm-proxy/
|
|
|
97
96
|
- CLI:`-h`、`config`(首次创建)、`start`(参数透传链 + env 注入 + 401/200 鉴权)、`stop`(树杀 + 端口释放)、`restart`(运行中/未运行两态)
|
|
98
97
|
- 仓库产物模式(零 env)与包模式(CLI 注入)双模式启动均正常
|
|
99
98
|
- 包目录零读写(`ls` 包目录无新增文件)、`npm pack --dry-run` 核对 tarball 无 `src/`/`test/`/真实配置
|
|
99
|
+
|
|
100
|
+
## 6. 2026-09-06 重构:去 pm2 + 单进程单端口(决策 13)
|
|
101
|
+
|
|
102
|
+
### 6.1 动机与决策过程(注:backend 包于 2026-09-06 改名 server,本节与 §4 历史 backend 均指该包)
|
|
103
|
+
|
|
104
|
+
- pm2 是发布包最重的依赖(安装慢、daemon 常驻内存、env 白名单清洗等实测坑),且其 daemon 行为黑盒不可控;
|
|
105
|
+
- manager「独立常驻以远程拉起主服务」的设计前提是主服务可能被 pm2 单独停止——去 pm2 后前提消失;
|
|
106
|
+
- 管理域代码(manager/api + harness + updater + 静态托管 ≈ 700 行)与 CLI(bin + utils ≈ 600 行)随单进程化全部 TS 化收进 `server/src/`(admin/、proc/、cli/;backend 包同日改名 server),发布壳只剩构建产物,JS/TS 边界消失。
|
|
107
|
+
|
|
108
|
+
曾评估的替代方案(详见会话记录):更新者子进程方案(detached updater,被 D 方案吸收)、postinstall 自启(故障语义差,否决)、版本化安装 + current 切换(VS Code 式,终态最优但工程量大,留作后续演进)、OS 调度更新(粒度太粗)。更新链路定稿为 **D 方案(启动时生效)+ self-respawn**:看门狗/手动更新只写 pendingUpdate 标记并 respawn,新进程引导时装 `npm i -g` 后双跳换新代码——停机窗口与旧 pm2 流程相同(= npm 安装时长),实现量最小,回滚语义天然(装包失败磁盘仍是旧版)。
|
|
109
|
+
|
|
110
|
+
### 6.2 终态架构
|
|
111
|
+
|
|
112
|
+
- **单进程单端口**:`dist/server.js` 同时承载网关 `/v1/*`、管理台 `/api/*`(登录鉴权)、管理域 API(`/status` `/processes` `/restart` `/autostart` `/update/*` `/harness/*`,原 manager 免鉴权裸路径 → 挂 `authMiddleware`)、web-ui 静态托管(注册顺序最后,gateway 子应用无 catch-all 不遮蔽)。29771 退役。
|
|
113
|
+
- **进程管理原语(server/src/proc/)**:`run.json` 运行记录(pid/端口/startedAt/serverJs);`spawnDetached`(日志重定向到 logs/);`selfRespawn`(孵化后继 + 退出,`ELLM_RESPAWNED=1` 时后继进程对 EADDRINUSE 重试绑定 300ms×10s 而非顺延端口);`killTree`(win taskkill /T /F,posix 杀进程组);`waitHttpOnline`(端口真实可服务才算就绪);`installCrashRecovery`(uncaughtException → respawn,计数器 1 分钟 10 次封顶;SIGKILL/断电无守护,由自启项兜底)。
|
|
114
|
+
- **CLI(server/src/cli/)**:start = 配置自愈 → 旧 pm2 清理 → 已运行检测 → 端口预算(顺延)→ spawn detached → run.json+HTTP 探活;stop = 旧 pm2 清理 → 按 run.json pid 杀树。旧 pm2 清理数据源是 `~/.pm2/pids/<name>-<pmId>.pid`(**dump.pm2 已加密不可解析**,实测踩坑);守护仅在存活 pm2 应用全部是 ellm-proxy 系时才杀(这台机器曾有其他 pm2 应用)。
|
|
115
|
+
- **解释器解析保留**(proc/interpreter.ts + get-node):服务依赖 node:sqlite,CLI 自身 node < 22.13 时解析/下载 node 22 再 spawn;自启项内容含 nodePath + ELLM_* env 快照,保证登录启动与手动启动同参。
|
|
116
|
+
|
|
117
|
+
### 6.3 实测踩坑记录
|
|
118
|
+
|
|
119
|
+
- **pm2 pid 文件命名**:`pids/<name>-<pmId>.pid` 而非 `<pmId>.pid`,且 dump.pm2 主文件 require 出来是空对象(非文档所述纯数组)——旧版迁移清理必须以 pids 目录 + 文件名匹配为准。
|
|
120
|
+
- **esbuild ESM bundle 的 CJS 依赖**:get-node 链路的 tmp-promise、pidusage 内部 `require("util")` 会抛 "Dynamic require"——banner 加 `createRequire` 兜底(三个构建脚本统一)。
|
|
121
|
+
- **管理域 gatewayBase 必须每请求现算**:ctx.port 在 listen 后才回填,sub-app 创建时固化会得到 `:0`。
|
|
122
|
+
- **CLI 自身跑在旧 node 上**:dist/cli.js 的 bundle target node22 但语法兼容 18,真正跑服务前必须先解析解释器。
|
|
123
|
+
|
|
124
|
+
### 6.4 已知取舍
|
|
125
|
+
|
|
126
|
+
- 更新期间(npm 安装时长)服务不可用——与旧 pm2 流程一致;停机窗口能缩短到几秒的版本化安装方案(B)留作演进。
|
|
127
|
+
- SIGKILL/断电无自动拉起(pm2 autorestart 的唯一真损失)——由开机自启项兜底。
|
|
128
|
+
- 管理域 API 从「仅本机免鉴权」升级为「登录鉴权」,外部直调脚本需带 token;29771 书签失效。
|
package/ellm.config.example.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ellm-proxy",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Self-hosted LLM gateway with admin web UI (
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "Self-hosted LLM gateway with admin web UI (single process, built-in process management)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
|
-
"manager",
|
|
16
|
-
"utils",
|
|
17
15
|
"dist",
|
|
18
16
|
"docs",
|
|
19
17
|
"ellm.config.example.json",
|
|
@@ -25,19 +23,6 @@
|
|
|
25
23
|
"prepack": "node scripts/build-package.mjs",
|
|
26
24
|
"release": "node scripts/release.mjs"
|
|
27
25
|
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"@types/node": "^22.12.1"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@hono/node-server": "^1.19.17",
|
|
33
|
-
"cac": "^6.7.14",
|
|
34
|
-
"get-node": "^15.0.4",
|
|
35
|
-
"hono": "^4.13.5",
|
|
36
|
-
"mri": "^1.2.0",
|
|
37
|
-
"pm2": "^7.0.4",
|
|
38
|
-
"smol-toml": "^1.8.0",
|
|
39
|
-
"yaml": "^2.9.0"
|
|
40
|
-
},
|
|
41
26
|
"publishConfig": {
|
|
42
27
|
"registry": "https://registry.npmjs.org/"
|
|
43
28
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// autostart on|off 命令(CLI 薄壳):平台实现与状态探测在 utils/autostart.js(被管理服务复用),
|
|
2
|
-
// 本文件只做命令装配与人读输出。
|
|
3
|
-
import { autostartStatus, enableAutostart, disableAutostart } from "../../utils/autostart.js";
|
|
4
|
-
|
|
5
|
-
async function autostartOn() {
|
|
6
|
-
const { itemPath } = await enableAutostart();
|
|
7
|
-
console.log("开机自启已开启(登录时触发):");
|
|
8
|
-
console.log(` 自启项 : ${itemPath}(pm2 resurrect 恢复服务)`);
|
|
9
|
-
console.log(" 禁用方式 : ellm-proxy autostart off");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function autostartOff() {
|
|
13
|
-
const { removed } = await disableAutostart();
|
|
14
|
-
if (!removed) {
|
|
15
|
-
console.log("未找到自启项(可能未开启开机自启),无需操作。");
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.log("开机自启已关闭(自启项已移除)。");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function register(cli, run) {
|
|
22
|
-
cli
|
|
23
|
-
.command(
|
|
24
|
-
"autostart <action>",
|
|
25
|
-
"开机自启(登录时触发):on=注册自启项(Win 启动文件夹/mac LaunchAgent/Linux systemd user),off=移除",
|
|
26
|
-
)
|
|
27
|
-
.action((action) => {
|
|
28
|
-
if (action !== "on" && action !== "off") {
|
|
29
|
-
console.error(`未知参数: ${action}(用法: ellm-proxy autostart on|off)`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
return run(action === "on" ? autostartOn : autostartOff);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 状态探测保留导出(潜在 CLI status 输出用;当前 manager 走 utils 直连)
|
|
37
|
-
export { autostartStatus };
|
package/bin/commands/config.js
DELETED
package/bin/commands/pm2.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// pm2 透传命令:ellm-proxy pm2 <任意 pm2 参数>(如 list/ls/logs/monit/describe/flush)。
|
|
2
|
-
// 用包内局部 pm2 执行,用户无需自装全局 pm2;stdio 直接继承,logs/monit 等交互命令可实时输出、
|
|
3
|
-
// Ctrl-C 退出。透传参数不经 cac 解析(在 CLI 入口先于 cac 拦截),pm2 的 -- 选项原样可达。
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { pm2BinJs } from "../../utils/pm2.js";
|
|
6
|
-
|
|
7
|
-
/** 以包内 pm2 CLI 执行任意参数(node 版本无关紧要:pm2 bin 脚本兼容;daemon 连接与 interpreter 无关) */
|
|
8
|
-
export async function doPassthrough(args) {
|
|
9
|
-
if (args.length === 0) {
|
|
10
|
-
args = ["list"]; // 无参数 = 默认展示进程列表
|
|
11
|
-
}
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
const child = spawn(process.execPath, [pm2BinJs(), ...args], { stdio: "inherit", shell: false });
|
|
14
|
-
child.on("error", reject);
|
|
15
|
-
// pm2 退出码原样透传;exit 回调内 process.exit 结束进程(promise 永不 resolve)
|
|
16
|
-
child.on("exit", (code) => process.exit(code ?? 1));
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function register(cli, run) {
|
|
21
|
-
// cac 仅声明命令用于 -h 展示;实际执行走 CLI 入口的 pm2 拦截(保证 pm2 选项不被 cac 解析吞掉)
|
|
22
|
-
cli
|
|
23
|
-
.command(
|
|
24
|
-
"pm2 [args...]",
|
|
25
|
-
"透传执行包内 pm2 任意子命令(如 list/ls/logs/monit/describe/flush;pm2 的 -- 选项原样透传)",
|
|
26
|
-
)
|
|
27
|
-
.action(() => {});
|
|
28
|
-
}
|
package/bin/commands/service.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// 服务生命周期 CLI 命令:start / stop / restart(restart = stop + start,保证最新参数生效)。
|
|
2
|
-
// 进程编排核心在 utils/service.js,本文件只做命令装配与人读输出(摘要文案)。
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { APP_NAME, MANAGER_APP_NAME, pkgRoot, defaultAppDir, absolutize } from "../../utils/paths.js";
|
|
5
|
-
import { withPm2, findApp } from "../../utils/pm2.js";
|
|
6
|
-
import { doStart, doStop, resolvePort } from "../../utils/service.js";
|
|
7
|
-
import { ICONS, say, field } from "../../utils/ui.js";
|
|
8
|
-
|
|
9
|
-
/** 摘要块(start/restart 成功 与 默认命令已运行 共用的输出形态,图标 + 对齐标签) */
|
|
10
|
-
function printInfo({ appDir, port, managerPort, logsHint }) {
|
|
11
|
-
if (port) {
|
|
12
|
-
field(ICONS.gateway, "网关端点", `http://localhost:${port}`);
|
|
13
|
-
field(ICONS.api, "管理台 API", `http://localhost:${port}/api/*`);
|
|
14
|
-
if (managerPort) field(ICONS.webui, "管理界面", `http://127.0.0.1:${managerPort}/`);
|
|
15
|
-
} else {
|
|
16
|
-
field(
|
|
17
|
-
ICONS.question,
|
|
18
|
-
"访问入口",
|
|
19
|
-
"端口未知(请检查配置):网关 = http://localhost:<端口> · 管理界面 = http://127.0.0.1:<主端口+1>/",
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
field(
|
|
23
|
-
ICONS.api,
|
|
24
|
-
"管理服务",
|
|
25
|
-
`${managerPort ? `http://127.0.0.1:${managerPort}` : "端口未知(= 网关端口 + 1)"}(启停/代理开关接口,进程名 ${MANAGER_APP_NAME})`,
|
|
26
|
-
);
|
|
27
|
-
field(ICONS.folder, "应用目录", appDir);
|
|
28
|
-
field(ICONS.status, "运行状态", `pm2 ls(进程名 ${APP_NAME})· 日志 ${logsHint ?? "<应用目录>/logs/"}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 启动摘要(start/restart 成功后输出) */
|
|
32
|
-
function printSummary(appDir, config) {
|
|
33
|
-
printInfo({
|
|
34
|
-
appDir,
|
|
35
|
-
port: resolvePort(process.env.ELLM_PORT, config),
|
|
36
|
-
managerPort: process.env.ELLM_MANAGER_PORT,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 运行中服务的信息(默认命令:ellm-proxy 不带参数时已运行则展示此信息)。
|
|
41
|
-
* 端口/路径以 pm2 记录的进程 env 为准(与当前 CLI 会话 env 无关) */
|
|
42
|
-
function printRunningInfo(proc) {
|
|
43
|
-
const pmEnv = proc.pm2_env?.env ?? {};
|
|
44
|
-
const appDir = pmEnv.ELLM_APP_DIR || proc.pm2_env?.pm_cwd || defaultAppDir;
|
|
45
|
-
const uptime = proc.pm2_env?.pm_uptime ? new Date(proc.pm2_env.pm_uptime).toLocaleString() : "";
|
|
46
|
-
say(`${APP_NAME} 已在运行(pid ${proc.pid}${uptime ? ` · 自 ${uptime} 起` : ""})`, ICONS.running);
|
|
47
|
-
printInfo({
|
|
48
|
-
appDir,
|
|
49
|
-
port: resolvePort(pmEnv.ELLM_PORT, pmEnv.ELLM_CONFIG),
|
|
50
|
-
managerPort: pmEnv.ELLM_MANAGER_PORT,
|
|
51
|
-
logsHint: join(appDir, "logs"),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** start 成功后的统一输出 */
|
|
56
|
-
function printStarted({ proc, managerProc, appDir, config }) {
|
|
57
|
-
console.log();
|
|
58
|
-
say(`启动完成(主服务 pid ${proc.pid}${managerProc ? ` · 管理服务 pid ${managerProc.pid}` : ""})`, ICONS.success);
|
|
59
|
-
console.log();
|
|
60
|
-
printSummary(appDir, config);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 默认命令(ellm-proxy 不带参数):已运行则打印服务信息,未运行则启动(同 start) */
|
|
64
|
-
export async function doStartOrInfo(opts) {
|
|
65
|
-
const existing = await withPm2(() => findApp());
|
|
66
|
-
if (existing) return printRunningInfo(existing);
|
|
67
|
-
printStarted(await doStart(absolutize(opts)));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** start/restart/默认命令共用的选项集 */
|
|
71
|
-
export function withStartOptions(cmd) {
|
|
72
|
-
return cmd
|
|
73
|
-
.option("--port <n>", "监听端口(也可用 ELLM_PORT 环境变量)")
|
|
74
|
-
.option("--config <path>", "配置文件路径(也可用 ELLM_CONFIG 环境变量)")
|
|
75
|
-
.option("--data-dir <path>", "数据目录(也可用 ELLM_DATA_DIR 环境变量;默认 <appDir>/data)")
|
|
76
|
-
.option("--working-dir <path>", "应用目录(配置/数据/日志基目录 ELLM_APP_DIR,默认 ~/.ellm-proxy)")
|
|
77
|
-
.option("--server-js <path>", "显式指定 dist/server.js 路径(默认本包 dist/server.js)");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function register(cli, run) {
|
|
81
|
-
withStartOptions(cli.command("start", "pm2 后台守护启动(崩溃自动重启;首次自动创建配置)")).action((opts) =>
|
|
82
|
-
run(() => doStart(absolutize(opts)).then(printStarted)),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
withStartOptions(cli.command("restart", "重启服务(先 stop 后 start,保证最新参数生效,选项同 start)")).action(
|
|
86
|
-
(opts) =>
|
|
87
|
-
run(async () => {
|
|
88
|
-
const stopped = await doStop();
|
|
89
|
-
if (!stopped.main && !stopped.manager) say(`${APP_NAME} 未在运行(可能已停止)。`, ICONS.idle);
|
|
90
|
-
printStarted(await doStart(absolutize(opts)));
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
cli.command("stop", "停止服务(pm2 删除守护进程,不影响其他 node 进程)").action(() =>
|
|
95
|
-
run(async () => {
|
|
96
|
-
const stopped = await doStop();
|
|
97
|
-
if (!stopped.main && !stopped.manager) {
|
|
98
|
-
say(`${APP_NAME} 未在运行(可能已停止)。`, ICONS.idle);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
say(
|
|
102
|
-
stopped.manager ? `${APP_NAME} 已停止(管理服务 ${MANAGER_APP_NAME} 一并停止)。` : `${APP_NAME} 已停止。`,
|
|
103
|
-
ICONS.stopped,
|
|
104
|
-
);
|
|
105
|
-
}),
|
|
106
|
-
);
|
|
107
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{A as T}from"./useApi-D_iJ2Mca.js";import{u as H}from"./providers-Djtdy2tv.js";import{u as J}from"./useToast-C0UMTbkx.js";import{d as Q,p as W,e as h,g as o,w as s,c as V,i as B,r as y,a as _,j as X,o as c,b as Z,k as Y,t as u,f as l,h as p,F as ee,q as te,l as N,_ as ae}from"./index-GohB4JDa.js";import"./config-Cs3_z63C.js";const le={class:"flex-col-gap"},oe=["onClick"],se={key:1},ne={class:"flex-between"},de={class:"flex-gap-8"},ue={class:"usage-grid"},ie={class:"usage-cell"},re={class:"usage-num orange"},ce={class:"usage-cell"},ve={class:"usage-num blue"},_e={class:"usage-cell"},pe={class:"usage-num green"},ge={class:"usage-cell"},me={class:"usage-num purple"},fe={class:"usage-cell"},he={class:"usage-num purple"},ye={key:0,class:"usage-cell"},be={class:"usage-num gray"},we={key:0},ke=["onClick"],De=Q({__name:"DashboardView",setup(Ce){const k=H(),{toast:M}=J(),i=y(null),b=y(!0),D=y(""),v=y(null),w=y("today"),C=a=>{const e=a.getFullYear(),t=String(a.getMonth()+1).padStart(2,"0"),r=String(a.getDate()).padStart(2,"0");return`${e}-${t}-${r}`},F=a=>{const e=new Date;let t,r=e;if(a==="today")t=e;else if(a==="yesterday")t=new Date(e),t.setDate(e.getDate()-1),r=t;else if(a==="beforeYesterday")t=new Date(e),t.setDate(e.getDate()-2),r=t;else if(a==="last3days")t=new Date(e),t.setDate(e.getDate()-2);else if(a==="week"){t=new Date(e);const S=(e.getDay()+6)%7;t.setDate(e.getDate()-S)}else if(a==="month")t=new Date(e.getFullYear(),e.getMonth(),1);else return;w.value=a,v.value=[C(t),C(r)],f()},U=a=>{a?F(a):(w.value="",v.value=null,f())},A=()=>{w.value="",f()},I=N(()=>{var e;const a=new Set;for(const t of k.suppliers)for(const r of t.models)a.add(r.modelName);if((e=i.value)!=null&&e.models)for(const t of i.value.models)a.add(t.model);return[...a].sort()}),f=async()=>{var a;b.value=!0;try{const e=new URLSearchParams;D.value&&e.set("model",D.value),((a=v.value)==null?void 0:a.length)===2&&(e.set("startDate",v.value[0]),e.set("endDate",v.value[1]));const t=e.toString();i.value=await T.get(`/api/usage${t?`?${t}`:""}`)}catch(e){console.error(e)}finally{b.value=!1}},R=async()=>{var a;try{const[e]=await Promise.all([T.get("/api/usage"),k.loadProviders(),T.get("/api/status").then(t=>x.value=t.listenPort).catch(()=>x.value)]);if(i.value=e,!((a=v.value)!=null&&a.length)){const t=new Date;v.value=[C(t),C(t)],await f()}}catch(e){console.error(e)}finally{b.value=!1}},L=window.location.hostname,x=y(Number(window.location.port)||null),q=N(()=>`http://${L}${x.value?`:${x.value}`:""}`),g=a=>a.toLocaleString(),E=a=>a==null||a<=0?"-":a>=1e3?(a/1e3).toFixed(2)+"s":Math.round(a)+"ms",$=async a=>{try{await navigator.clipboard.writeText(a),M("已复制到剪贴板","success")}catch{const e=document.createElement("textarea");e.value=a,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select();try{document.execCommand("copy"),M("已复制到剪贴板","success")}catch{M("复制失败","error")}document.body.removeChild(e)}};return W(R),(a,e)=>{const t=_("el-table-column"),r=_("el-table"),S=_("el-card"),m=_("el-option"),P=_("el-select"),j=_("el-date-picker"),z=_("el-button"),K=_("el-tag"),O=_("el-empty"),G=X("loading");return c(),h("div",le,[o(S,{shadow:"never"},{header:s(()=>[...e[3]||(e[3]=[l("span",{class:"fw-600"},"服务信息",-1)])]),default:s(()=>[Z((c(),V(r,{border:"",data:[{key:"API 地址",value:q.value},{key:"供应商数量",value:String(Y(k).providerCount)},{key:"模型总数",value:String(Y(k).totalModelCount)}],"show-header":!1,class:"full-width"},{default:s(()=>[o(t,{prop:"key",width:"160"}),o(t,null,{default:s(({row:d})=>[d.key==="API 地址"?(c(),h("span",{key:0,class:"copy-text",onClick:n=>$(d.value)},u(d.value),9,oe)):(c(),h("span",se,u(d.value),1))]),_:1})]),_:1},8,["data"])),[[G,b.value]])]),_:1}),i.value?(c(),V(S,{key:0,shadow:"never"},{header:s(()=>[l("div",ne,[e[5]||(e[5]=l("span",{class:"fw-600"},"用量统计",-1)),l("div",de,[o(P,{modelValue:D.value,"onUpdate:modelValue":e[0]||(e[0]=d=>D.value=d),placeholder:"全部模型",clearable:"",filterable:"",style:{width:"220px"},onChange:f},{default:s(()=>[(c(!0),h(ee,null,te(I.value,d=>(c(),V(m,{key:d,label:d,value:d},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),o(P,{modelValue:w.value,"onUpdate:modelValue":e[1]||(e[1]=d=>w.value=d),placeholder:"时间范围",clearable:"",style:{width:"130px"},onChange:U},{default:s(()=>[o(m,{label:"今天",value:"today"}),o(m,{label:"昨天",value:"yesterday"}),o(m,{label:"前天",value:"beforeYesterday"}),o(m,{label:"近三天",value:"last3days"}),o(m,{label:"本周",value:"week"}),o(m,{label:"本月",value:"month"})]),_:1},8,["modelValue"]),o(j,{modelValue:v.value,"onUpdate:modelValue":e[2]||(e[2]=d=>v.value=d),type:"daterange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期","value-format":"YYYY-MM-DD",style:{width:"260px"},onChange:A},null,8,["modelValue"]),o(z,{loading:b.value,onClick:f},{default:s(()=>[...e[4]||(e[4]=[p("刷新",-1)])]),_:1},8,["loading"])]),o(K,{type:"info"},{default:s(()=>[p(u(i.value.total.calls)+" 次调用",1)]),_:1})])]),default:s(()=>{var d;return[l("div",ue,[l("div",ie,[l("div",re,u(g(i.value.total.total_tokens)),1),e[6]||(e[6]=l("div",{class:"usage-label"},"总 Token",-1))]),l("div",ce,[l("div",ve,u(g(i.value.total.input_tokens)),1),e[7]||(e[7]=l("div",{class:"usage-label"},"输入 Token",-1))]),l("div",_e,[l("div",pe,u(g(i.value.total.output_tokens)),1),e[8]||(e[8]=l("div",{class:"usage-label"},"输出 Token",-1))]),l("div",ge,[l("div",me,u(g(i.value.total.cache_read_tokens)),1),e[9]||(e[9]=l("div",{class:"usage-label"},"缓存读取",-1))]),l("div",fe,[l("div",he,u(i.value.total.cache_rate!=null?i.value.total.cache_rate+"%":"-"),1),e[10]||(e[10]=l("div",{class:"usage-label"},"缓存命中率",-1))]),i.value.logStorageMB!=null?(c(),h("div",ye,[l("div",be,u(i.value.logStorageMB),1),e[11]||(e[11]=l("div",{class:"usage-label"},"日志占用 (MB)",-1))])):B("",!0)]),(d=i.value.models)!=null&&d.length?(c(),h("div",we,[e[12]||(e[12]=l("div",{class:"section-title"},"模型用量",-1)),o(r,{data:i.value.models,border:"",stripe:"",class:"full-width"},{default:s(()=>[o(t,{prop:"model",label:"模型","min-width":"200"},{default:s(({row:n})=>[l("span",{class:"copy-text",onClick:xe=>$(n.model)},u(n.model),9,ke)]),_:1}),o(t,{label:"调用次数",width:"90",align:"center"},{default:s(({row:n})=>[p(u(n.calls),1)]),_:1}),o(t,{label:"平均耗时",width:"110",align:"center"},{default:s(({row:n})=>[p(u(E(n.avg_duration)),1)]),_:1}),o(t,{label:"总 Token",width:"140",align:"center"},{default:s(({row:n})=>[p(u(g(n.total_tokens)),1)]),_:1}),o(t,{label:"输入 Token",width:"140",align:"center"},{default:s(({row:n})=>[p(u(g(n.input_tokens)),1)]),_:1}),o(t,{label:"输出 Token",width:"140",align:"center"},{default:s(({row:n})=>[p(u(g(n.output_tokens)),1)]),_:1}),o(t,{label:"缓存读取",width:"130",align:"center"},{default:s(({row:n})=>[p(u(g(n.cache_read_tokens)),1)]),_:1}),o(t,{label:"缓存命中率",width:"110",align:"center"},{default:s(({row:n})=>[p(u(n.cache_rate!=null?n.cache_rate+"%":"-"),1)]),_:1})]),_:1},8,["data"])])):(c(),V(O,{key:1,description:"暂无调用记录","image-size":60}))]}),_:1})):B("",!0)])}}}),Pe=ae(De,[["__scopeId","data-v-de31ace5"]]);export{Pe as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.mt-12[data-v-e326b723]{margin-top:12px}.check-body[data-v-e326b723]{min-height:80px}.version-row[data-v-e326b723]{display:flex;align-items:center;gap:8px;margin-bottom:8px}.version-label[data-v-e326b723]{color:var(--el-text-color-secondary);font-size:13px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as E,u as N,c as b,w as e,r as f,a,o as _,b as W,e as D,F as z,f as i,g as o,h as u,t as x,i as $,E as I,j as L,_ as S,k as F,l as R,m as j,n as O}from"./index-GohB4JDa.js";import{m as T}from"./useManagerApi-C1_COxKg.js";import{u as q}from"./useToast-C0UMTbkx.js";const G={class:"check-body"},H={class:"version-row"},J={class:"version-row"},K=E({__name:"UpdateCheckDialog",props:{modelValue:{type:Boolean,default:!1},modelModifiers:{}},emits:["update:modelValue"],setup(B){const{toast:w}=q(),g=N(B,"modelValue"),d=f(!1),c=f(!1),n=f(null),v=f(null),p=f(null),r=async()=>{n.value=null,v.value=null,p.value=null,d.value=!0;try{n.value=await T.checkUpdate()}catch(l){v.value=l.message||"检查更新失败"}finally{d.value=!1}},t=async()=>{if(n.value){try{await I.confirm(`确定更新到 ${n.value.latestVersion}?更新期间主服务将短暂中断(停服安装后自动重启),管理服务随后自重启。`,"更新确认",{confirmButtonText:"立即更新",cancelButtonText:"取消",type:"warning"})}catch{return}c.value=!0;try{const l=await T.applyUpdate();p.value=l.note??`已更新 ${l.fromVersion} → ${l.toVersion}`,w("更新完成","success"),n.value={hasUpdate:!1,currentVersion:l.toVersion,latestVersion:l.toVersion,checkedAt:Date.now()}}catch(l){w(l.message||"更新失败","error")}finally{c.value=!1}}};return(l,s)=>{const k=a("el-tag"),V=a("el-alert"),y=a("el-button"),h=a("el-empty"),C=a("el-dialog"),U=L("loading");return _(),b(C,{modelValue:g.value,"onUpdate:modelValue":s[2]||(s[2]=m=>g.value=m),title:"检查更新",width:"420px","close-on-click-modal":!c.value,onOpen:r},{footer:e(()=>{var m;return[(m=n.value)!=null&&m.hasUpdate?(_(),b(y,{key:0,type:"primary",loading:c.value,onClick:t},{default:e(()=>[u(x(c.value?"更新中...":"立即更新"),1)]),_:1},8,["loading"])):$("",!0),o(y,{onClick:s[1]||(s[1]=A=>g.value=!1)},{default:e(()=>[...s[6]||(s[6]=[u("关闭",-1)])]),_:1})]}),default:e(()=>[W((_(),D("div",G,[n.value?(_(),D(z,{key:0},[i("div",H,[s[3]||(s[3]=i("span",{class:"version-label"},"当前版本",-1)),o(k,{size:"small"},{default:e(()=>[u(x(n.value.currentVersion),1)]),_:1})]),i("div",J,[s[4]||(s[4]=i("span",{class:"version-label"},"最新版本",-1)),o(k,{type:n.value.hasUpdate?"warning":"success",size:"small","disable-transitions":""},{default:e(()=>[u(x(n.value.latestVersion),1)]),_:1},8,["type"])]),o(V,{title:n.value.hasUpdate?`发现新版本 ${n.value.latestVersion}`:"已是最新版本",type:n.value.hasUpdate?"warning":"success","show-icon":"",closable:!1,class:"mt-12"},null,8,["title","type"])],64)):!d.value&&v.value?(_(),b(h,{key:1,description:v.value},{default:e(()=>[o(y,{type:"primary",onClick:r},{default:e(()=>[...s[5]||(s[5]=[u("重试",-1)])]),_:1})]),_:1},8,["description"])):$("",!0)])),[[U,d.value]]),p.value?(_(),b(V,{key:0,title:p.value,type:"info","show-icon":"",class:"mt-12",onClose:s[0]||(s[0]=m=>p.value=null)},null,8,["title"])):$("",!0)]),_:1},8,["modelValue","close-on-click-modal"])}}}),P=S(K,[["__scopeId","data-v-e326b723"]]),Q={class:"layout"},X={class:"header-nav"},Y={class:"header-actions"},Z={class:"main"},ee={class:"page"},se=E({__name:"LayoutView",setup(B){const w=O(),g=j(),d=f(!1),c=(()=>{const r=localStorage.getItem("webui_token")||"";try{return atob(r).split(":")[0]||"用户"}catch{return"用户"}})(),n=R(()=>{const r=w.path;return r==="/"?"/":r.startsWith("/providers")?"/providers":r.startsWith("/settings")?"/settings":r.startsWith("/logs")?"/logs":r.startsWith("/process")?"/process":"/"}),v=()=>{localStorage.removeItem("webui_token"),g.push("/login")},p=r=>{r==="logout"&&v(),r==="check-update"&&(d.value=!0)};return(r,t)=>{const l=a("el-menu-item"),s=a("el-menu"),k=a("ArrowDown"),V=a("el-icon"),y=a("el-button"),h=a("el-dropdown-item"),C=a("el-dropdown-menu"),U=a("el-dropdown"),m=a("el-header"),A=a("router-view");return _(),D("div",Q,[o(m,{class:"header"},{default:e(()=>[t[8]||(t[8]=i("h1",null,[i("span",null,"E"),u("LLM")],-1)),i("nav",X,[o(s,{mode:"horizontal","default-active":n.value,ellipsis:!1,router:"",class:"header-menu"},{default:e(()=>[o(l,{index:"/"},{default:e(()=>[...t[1]||(t[1]=[u("首页",-1)])]),_:1}),o(l,{index:"/providers"},{default:e(()=>[...t[2]||(t[2]=[u("供应商管理",-1)])]),_:1}),o(l,{index:"/logs"},{default:e(()=>[...t[3]||(t[3]=[u("日志",-1)])]),_:1}),o(l,{index:"/settings"},{default:e(()=>[...t[4]||(t[4]=[u("设置",-1)])]),_:1}),o(l,{index:"/process"},{default:e(()=>[...t[5]||(t[5]=[u("进程管理",-1)])]),_:1})]),_:1},8,["default-active"])]),i("div",Y,[o(U,{trigger:"click",onCommand:p,class:"ml-12"},{dropdown:e(()=>[o(C,null,{default:e(()=>[o(h,{command:"check-update"},{default:e(()=>[...t[6]||(t[6]=[u("检查更新",-1)])]),_:1}),o(h,{command:"logout",divided:""},{default:e(()=>[...t[7]||(t[7]=[u("退出",-1)])]),_:1})]),_:1})]),default:e(()=>[o(y,null,{default:e(()=>[u(x(F(c))+" ",1),o(V,{class:"el-icon--right"},{default:e(()=>[o(k)]),_:1})]),_:1})]),_:1})])]),_:1}),i("main",Z,[i("div",ee,[o(A)])]),o(P,{modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=M=>d.value=M)},null,8,["modelValue"])])}}});export{se as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as h,e as b,g as o,w as l,a,o as _,f as k,Y as x,a2 as f,c as C,i as L,h as B,r as i,m as N,_ as I}from"./index-GohB4JDa.js";import{A as K}from"./useApi-D_iJ2Mca.js";const A={class:"login-wrap"},E=h({__name:"LoginView",setup(M){const v=N(),n=i(""),s=i(""),t=i(""),d=i(!1);async function r(){if(!n.value){t.value="请输入用户名";return}if(!s.value){t.value="请输入密码";return}d.value=!0,t.value="";try{const u=await K.post("/api/auth/login",{username:n.value.trim(),password:s.value});localStorage.setItem("webui_token",u.token),v.push("/")}catch(u){t.value=u.message||"登录失败"}finally{d.value=!1}}return(u,e)=>{const c=a("el-input"),m=a("el-form-item"),w=a("el-alert"),g=a("el-button"),V=a("el-form"),y=a("el-card");return _(),b("div",A,[o(y,{shadow:"hover",class:"login-card"},{default:l(()=>[e[3]||(e[3]=k("h2",{class:"login-title"},"ELLM",-1)),o(V,{"label-position":"top",onSubmit:x(r,["prevent"])},{default:l(()=>[o(m,{label:"用户名"},{default:l(()=>[o(c,{modelValue:n.value,"onUpdate:modelValue":e[0]||(e[0]=p=>n.value=p),placeholder:"请输入用户名",onKeyup:f(r,["enter"])},null,8,["modelValue"])]),_:1}),o(m,{label:"密码"},{default:l(()=>[o(c,{modelValue:s.value,"onUpdate:modelValue":e[1]||(e[1]=p=>s.value=p),type:"password",placeholder:"请输入密码","show-password":"",onKeyup:f(r,["enter"])},null,8,["modelValue"])]),_:1}),t.value?(_(),C(w,{key:0,title:t.value,type:"error","show-icon":"",class:"mb-16"},null,8,["title"])):L("",!0),o(g,{type:"primary",class:"full-width",loading:d.value,onClick:r},{default:l(()=>[...e[2]||(e[2]=[B(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1})])}}}),P=I(E,[["__scopeId","data-v-c944978e"]]);export{P as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as X,p as Z,a7 as q,b as H,e as D,g as s,w as l,r as i,a as c,j as J,o as w,f as d,Z as Q,h as o,t as r,c as W,E as Y,_ as ee}from"./index-GohB4JDa.js";import{m as E}from"./useManagerApi-C1_COxKg.js";import{u as te}from"./useToast-C0UMTbkx.js";const ae={class:"flex-col-gap"},le={class:"flex-between"},se={class:"proc-name"},ne={class:"proc-sub"},oe=["onClick"],re={key:1,class:"muted-sm"},ie={class:"desc-full"},de=X({__name:"ProcessView",setup(ue){const{toast:b}=te(),v=i(!0),g=i(!1),y=i([]),x=i(null),p=i(null),h=i(Date.now()),I={daemon:"pm2 守护进程",manager:"管理服务",main:"代理服务"},C=e=>I[e.role]??e.role,k={daemon:"pm2 守护进程:托管代理服务与管理服务的后台守护,进程崩溃时自动拉起;自身不监听端口、不处理业务请求,不允许远程操控(停止它将失去自动恢复能力)。",manager:"管理服务(ellm-proxy-manager):独立常驻的轻量服务,提供进程启停接口与代理开关接口;代理服务停止时保持存活,保证还能在本页远程拉起代理服务。",main:"代理服务(ellm-proxy):网关本体,转发 /v1/* 模型请求,同时承载管理台页面与 API;本页的重启操作即作用于该进程,重启期间网关与管理台 API 短暂不可用(代理服务停止时重启等效于启动)。"},P=e=>k[e.role],_=i(!1),T=i(""),M=i(""),A=e=>{T.value=`${C(e)}(${$(e)})`,M.value=k[e.role],_.value=!0},$=e=>e.role==="daemon"?"pm2":e.role==="manager"?"ellm-proxy-manager":"ellm-proxy",O=e=>e.status==="online",N=e=>e.status?e.status==="online"?"running":e.status==="errored"?"abnormal":"stopped":"stopped",V=e=>{switch(e.status){case"online":return{text:"运行中",type:"success"};case"stopped":return{text:"已停止",type:"info"};case"errored":return{text:"异常",type:"danger"};case"waiting restart":case"launching":return{text:"重启中",type:"warning"};default:return{text:e.status||"未知",type:"info"}}},R=e=>{if(!O(e)||!e.startedAt)return"-";let t=Math.max(0,Math.floor((h.value-e.startedAt)/1e3));const m=Math.floor(t/86400);t-=m*86400;const u=Math.floor(t/3600);t-=u*3600;const n=Math.floor(t/60);return t-=n*60,[m?`${m}天`:"",u?`${u}时`:"",n?`${n}分`:"",`${t}秒`].filter(Boolean).join(" ")},S=e=>e?e>=1024**3?(e/1024**3).toFixed(1)+"G":e>=1024**2?(e/1024**2).toFixed(1)+"M":Math.round(e/1024)+"K":"-",f=async e=>{e||(y.value.length?g.value=!0:v.value=!0);try{y.value=await E.processes(),h.value=Date.now(),x.value=null}catch(t){e||(x.value=t.message)}finally{e||(v.value=!1,g.value=!1)}};let B;const j=()=>{p.value||document.hidden||f(!0)};Z(()=>{f(!1),B=window.setInterval(j,5e3)}),q(()=>window.clearInterval(B));const z=async()=>{try{await Y.confirm("确定重启 ellm-proxy 代理服务?重启期间网关与管理台 API 将短暂不可用(代理服务已停止时重启等效于启动)。","重启确认",{confirmButtonText:"重启",cancelButtonText:"取消",type:"warning"})}catch{return}p.value="restart";try{await E.restart(),b("重启完成","success")}catch(e){b(e.message||"操作失败","error")}finally{p.value=null,await f(!0)}};return(e,t)=>{const m=c("el-tag"),u=c("el-button"),n=c("el-table-column"),F=c("el-empty"),L=c("el-table"),U=c("el-card"),G=c("el-dialog"),K=J("loading");return H((w(),D("div",ae,[s(U,{shadow:"never"},{header:l(()=>[d("div",le,[t[6]||(t[6]=d("span",{class:"fw-600"},"进程管理",-1)),d("div",null,[s(m,{type:"info",class:"mr-8"},{default:l(()=>[...t[4]||(t[4]=[o("刷新间隔 5s",-1)])]),_:1}),s(u,{loading:g.value,onClick:t[0]||(t[0]=a=>f(!1))},{default:l(()=>[...t[5]||(t[5]=[o("刷新",-1)])]),_:1},8,["loading"])])])]),default:l(()=>[s(L,{data:y.value,border:"",stripe:"",class:"full-width"},{empty:l(()=>[s(F,{description:x.value||"暂无进程数据"},{default:l(()=>[s(u,{type:"primary",onClick:t[1]||(t[1]=a=>f(!1))},{default:l(()=>[...t[8]||(t[8]=[o("重试",-1)])]),_:1})]),_:1},8,["description"])]),default:l(()=>[s(n,{label:"进程","min-width":"180"},{default:l(({row:a})=>[d("div",se,[d("span",{class:Q(["status-dot",N(a)])},null,2),o(" "+r(C(a))+" ",1),d("span",ne,r($(a)),1)])]),_:1}),s(n,{label:"运行状态",width:"100",align:"center"},{default:l(({row:a})=>[s(m,{type:V(a).type,"disable-transitions":""},{default:l(()=>[o(r(V(a).text),1)]),_:2},1032,["type"])]),_:1}),s(n,{label:"PID",width:"90",align:"center"},{default:l(({row:a})=>[o(r(a.pid??"-"),1)]),_:1}),s(n,{label:"端口",width:"90",align:"center"},{default:l(({row:a})=>[o(r(a.port??"-"),1)]),_:1}),s(n,{label:"运行时长",width:"130",align:"center"},{default:l(({row:a})=>[o(r(R(a)),1)]),_:1}),s(n,{label:"地址","min-width":"170"},{default:l(({row:a})=>[o(r(a.address??"-"),1)]),_:1}),s(n,{label:"内存",width:"90",align:"center"},{default:l(({row:a})=>[o(r(S(a.memory)),1)]),_:1}),s(n,{label:"描述","min-width":"220"},{default:l(({row:a})=>[d("span",{class:"desc-cell",title:"点击查看详情",onClick:ce=>A(a)},r(P(a)),9,oe)]),_:1}),s(n,{label:"操作",width:"90",align:"center"},{default:l(({row:a})=>[a.role==="main"?(w(),W(u,{key:0,type:"primary",size:"small",loading:p.value==="restart",disabled:p.value!==null,onClick:z},{default:l(()=>[...t[7]||(t[7]=[o(" 重启 ",-1)])]),_:1},8,["loading","disabled"])):(w(),D("span",re,"不可操作"))]),_:1})]),_:1},8,["data"]),t[9]||(t[9]=d("div",{class:"muted-sm"}," 进程控制由管理服务(manager)执行,仅代理服务(ellm-proxy)可重启(已停止时重启等效于启动): 重启期间网关与管理台 API 短暂不可用。 pm2 守护进程与管理服务不允许远程操控(停止它们将失去远程恢复能力)。 ",-1))]),_:1}),s(G,{modelValue:_.value,"onUpdate:modelValue":t[3]||(t[3]=a=>_.value=a),title:T.value,width:"520px"},{footer:l(()=>[s(u,{type:"primary",onClick:t[2]||(t[2]=a=>_.value=!1)},{default:l(()=>[...t[10]||(t[10]=[o("知道了",-1)])]),_:1})]),default:l(()=>[d("p",ie,r(M.value),1)]),_:1},8,["modelValue","title"])])),[[K,v.value]])}}}),_e=ee(de,[["__scopeId","data-v-c87eb134"]]);export{_e as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@charset "UTF-8";.proc-name[data-v-c87eb134]{display:flex;align-items:center;gap:8px;font-weight:600}.proc-name .proc-sub[data-v-c87eb134]{font-weight:400;color:var(--el-text-color-secondary);font-size:12px}.status-dot[data-v-c87eb134]{display:inline-block;width:8px;height:8px;border-radius:50%}.status-dot.running[data-v-c87eb134]{background:var(--el-color-success);box-shadow:0 0 4px var(--el-color-success)}.status-dot.stopped[data-v-c87eb134]{background:var(--el-color-info)}.status-dot.abnormal[data-v-c87eb134]{background:var(--el-color-danger)}.desc-cell[data-v-c87eb134]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.desc-full[data-v-c87eb134]{margin:0;line-height:1.8}.mr-8[data-v-c87eb134]{margin-right:8px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.switch-row[data-v-65e65156]{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0}.switch-title[data-v-65e65156]{display:flex;align-items:center;gap:8px;font-weight:600;margin-bottom:4px}.switch-desc[data-v-65e65156]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-65e65156]{background:var(--el-fill-color-light);padding:1px 6px;border-radius:4px;font-size:12px}.switch-divider[data-v-65e65156]{margin:16px 0}.mb-16[data-v-03f482d3]{margin-bottom:16px}.mt-8[data-v-03f482d3]{margin-top:8px}.switch-row[data-v-03f482d3]{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0}.switch-title[data-v-03f482d3]{display:flex;align-items:center;gap:8px;font-weight:600;margin-bottom:4px}.switch-desc[data-v-03f482d3]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-03f482d3]{background:var(--el-fill-color-light);padding:1px 6px;border-radius:4px;font-size:12px}.version-row[data-v-03f482d3]{display:flex;align-items:center;gap:8px;margin-bottom:8px}.version-label[data-v-03f482d3]{color:var(--el-text-color-secondary);font-size:13px}.version-actions[data-v-03f482d3]{margin-top:12px;display:flex;gap:8px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as Y}from"./useToast-C0UMTbkx.js";import{A as ee}from"./useApi-D_iJ2Mca.js";import{u as le}from"./config-Cs3_z63C.js";import{d as F,p as X,b as q,o as m,c as B,w as n,e as $,f as s,t as h,i as D,h as u,g as a,F as H,r as d,E as W,a as c,j as G,_ as Q,l as j}from"./index-GohB4JDa.js";import{m as K}from"./useManagerApi-C1_COxKg.js";const te={class:"flex-between"},se={class:"muted-sm mb-8"},ae={class:"switch-row"},oe={class:"switch-info"},ne={class:"switch-title"},ue={class:"switch-row"},ie={class:"switch-info"},de={class:"switch-title"},re={class:"switch-row"},ce={class:"switch-info"},ve={class:"switch-title"},pe=F({__name:"HarnessSwitches",setup(J){const{toast:g}=Y(),b=d(!0),E=d(!1),v=d(null),V=d(null),i=d(null),x=d(null),_=d("success"),C=async f=>{f||(v.value?E.value=!0:b.value=!0);try{v.value=await K.harness(),V.value=null}catch(o){f||(V.value=o.message)}finally{f||(b.value=!1,E.value=!1)}};X(()=>C(!1));const S={claudeCode:{label:"Claude Code",file:"~/.claude/settings.json",toggle:K.setClaudeCode},codex:{label:"Codex CLI",file:"~/.codex/config.toml",toggle:K.setCodex},dsh:{label:"DeepSeek Harness",file:"~/.dsh/settings.yaml",toggle:K.setDsh}},k=async(f,o)=>{if(!v.value||i.value)return;const y=S[f],p=o?`确定开启 ${y.label} 代理?将改写 ${y.file}(开启前自动备份,关闭时从备份还原)。`:`确定关闭 ${y.label} 代理?将从备份还原 ${y.file}(无备份时移除代理相关配置)。`;try{await W.confirm(p,o?"开启确认":"关闭确认",{confirmButtonText:o?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=f;try{const l=await y.toggle(o);x.value=l.note??null,_.value=o?"success":"info",g(o?"已开启代理":"已关闭代理","success")}catch(l){g(l.message||"操作失败","error")}finally{i.value=null,await C(!0)}};return(f,o)=>{const y=c("el-button"),p=c("el-alert"),l=c("el-tag"),I=c("el-switch"),z=c("el-divider"),P=c("el-empty"),R=c("el-card"),N=G("loading");return q((m(),B(R,{shadow:"never"},{header:n(()=>[s("div",te,[o[7]||(o[7]=s("span",{class:"fw-600"},"代理开关",-1)),a(y,{loading:E.value,onClick:o[0]||(o[0]=T=>C(!1))},{default:n(()=>[...o[6]||(o[6]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:n(()=>[v.value?(m(),$(H,{key:0},[s("div",se," 将本机 CLI 工具的流量一键指向 ellm-proxy 网关("+h(v.value.gatewayBase)+")。 开启前自动备份原配置为 *.ellm-bak(保留最早的原始备份),关闭时从备份还原。 ",1),x.value?(m(),B(p,{key:0,title:x.value,type:_.value,"show-icon":"",class:"mb-8",onClose:o[1]||(o[1]=T=>x.value=null)},null,8,["title","type"])):D("",!0),s("div",ae,[s("div",oe,[s("div",ne,[o[8]||(o[8]=u(" Claude Code ",-1)),a(l,{type:v.value.claudeCode?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.claudeCode?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[9]||(o[9]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.claude/settings.json"),u(" 的 env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN, 鉴权取网关配置的 masterKey(未设置将开启失败);其余字段原样保留,重启 Claude Code 会话后生效。 ")],-1))]),a(I,{"model-value":v.value.claudeCode,loading:i.value==="claudeCode",onChange:o[2]||(o[2]=T=>k("claudeCode",!!T))},null,8,["model-value","loading"])]),a(z,{class:"switch-divider"}),s("div",ue,[s("div",ie,[s("div",de,[o[10]||(o[10]=u(" Codex CLI ",-1)),a(l,{type:v.value.codex?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.codex?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[11]||(o[11]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.codex/config.toml"),u(" 的 model_provider 与 [model_providers.ellm-proxy] 段 (保留用户注释与排版);开启后需按提示设置环境变量 ELLM_PROXY_API_KEY 供 Codex 鉴权。 ")],-1))]),a(I,{"model-value":v.value.codex,loading:i.value==="codex",onChange:o[3]||(o[3]=T=>k("codex",!!T))},null,8,["model-value","loading"])]),a(z,{class:"switch-divider"}),s("div",re,[s("div",ce,[s("div",ve,[o[12]||(o[12]=u(" DeepSeek Harness ",-1)),a(l,{type:v.value.dsh?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.dsh?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[13]||(o[13]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.dsh/settings.yaml"),u(" 的 llm-deepseek.baseURL 与 "),s("code",null,"~/.dsh/.credentials.yaml"),u(" 的 DEEPSEEK_API_KEY(取网关 masterKey,未设置则写占位值);模型请在 dsh 内选择与网关部署名一致的模型。 dsh 为 developer preview,配置格式可能随版本变化。 ")],-1))]),a(I,{"model-value":v.value.dsh,loading:i.value==="dsh",onChange:o[4]||(o[4]=T=>k("dsh",!!T))},null,8,["model-value","loading"])])],64)):(m(),B(P,{key:1,description:V.value||"暂无开关状态"},{default:n(()=>[a(y,{type:"primary",onClick:o[5]||(o[5]=T=>C(!1))},{default:n(()=>[...o[14]||(o[14]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1})),[[N,b.value]])}}}),me=Q(pe,[["__scopeId","data-v-65e65156"]]),fe={class:"service-settings"},_e={class:"flex-between"},ge={key:0,class:"switch-row"},ye={class:"switch-info"},we={class:"switch-title"},be={key:0,class:"switch-row"},Ce={class:"switch-info"},ke={class:"switch-title"},he={class:"switch-desc"},xe={key:0,class:"version-block"},$e={class:"version-row"},Ve={class:"muted-sm"},Te={key:0,class:"muted-sm"},Ae={class:"version-actions"},Ee=F({__name:"ServiceUpdateSettings",setup(J){const{toast:g}=Y(),b=d(!0),E=d(!1),v=d(null),V=d(null),i=d(null),x=d(null),_=d(null),C=d(!1),S=d(!1),k=d(null),f=async t=>{var e,r;if(!t){if(E.value)return;E.value=!0}try{const[A,U]=await Promise.allSettled([K.autostart(),K.update()]);v.value=A.status==="fulfilled"?A.value:null,V.value=A.status==="rejected"?(e=A.reason)==null?void 0:e.message:null,i.value=U.status==="fulfilled"?U.value:null,x.value=U.status==="rejected"?(r=U.reason)==null?void 0:r.message:null}finally{b.value=!1,E.value=!1}};X(()=>f(!1));const o=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.currentVersion)??null}),y=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.latestVersion)??null}),p=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.hasUpdate)===!0}),l=j(()=>{var e,r;const t=(r=(e=i.value)==null?void 0:e.lastCheck)==null?void 0:r.checkedAt;return t?new Date(t).toLocaleString():null}),I=j(()=>{var r;const t=(r=i.value)==null?void 0:r.lastApply;if(!t)return null;const e=t.appliedAt?new Date(t.appliedAt).toLocaleString():"";return`${t.fromVersion} → ${t.toVersion}${e?`(${e})`:""}`}),z=t=>t?new Date(t).toLocaleString():"",P=async t=>{if(!v.value||_.value)return;const e=t?"确定开启开机自启?将注册登录触发的自启项(登录时自动恢复服务)。":"确定关闭开机自启?将移除自启项,且此后首次启动不再自动注册。";try{await W.confirm(e,t?"开启确认":"关闭确认",{confirmButtonText:t?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}_.value="autostart";try{const r=await K.setAutostart(t);r.note?g(r.note,"success"):g(t?"开机自启已开启":"开机自启已关闭","success")}catch(r){g(r.message||"操作失败","error")}finally{_.value=null,await f(!0)}},R=async t=>{if(!i.value||_.value)return;const e=t?"确定开启自动更新?将每小时检查私有源,发现新版本自动安装并重启服务。":"确定关闭自动更新?不再自动检查(手动检查更新不受影响)。";try{await W.confirm(e,t?"开启确认":"关闭确认",{confirmButtonText:t?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}_.value="updateEnabled";try{const r=await K.setUpdateEnabled(t);r.note&&g(r.note,"success")}catch(r){g(r.message||"操作失败","error")}finally{_.value=null,await f(!0)}},N=async()=>{C.value=!0;try{const t=await K.checkUpdate();k.value=t.hasUpdate?`发现新版本 ${t.latestVersion}(当前 ${t.currentVersion}),可点击「立即更新」安装`:`已是最新版本 ${t.currentVersion}(检查于 ${z(t.checkedAt)})`,await f(!0)}catch(t){g(t.message||"检查更新失败","error")}finally{C.value=!1}},T=async()=>{try{await W.confirm(`确定更新到 ${y.value}?更新期间主服务将短暂中断(停服安装后自动重启),管理服务随后自重启。`,"更新确认",{confirmButtonText:"立即更新",cancelButtonText:"取消",type:"warning"})}catch{return}S.value=!0;try{const t=await K.applyUpdate();k.value=t.note??`已更新 ${t.fromVersion} → ${t.toVersion}`,g("更新完成","success"),await f(!0)}catch(t){g(t.message||"更新失败","error"),await f(!0)}finally{S.value=!1}};return(t,e)=>{const r=c("el-button"),A=c("el-tag"),U=c("el-switch"),M=c("el-empty"),O=c("el-card"),w=c("el-alert"),Z=G("loading");return q((m(),$("div",fe,[a(O,{shadow:"never",class:"mb-16"},{header:n(()=>[s("div",_e,[e[7]||(e[7]=s("span",{class:"fw-600"},"开机自启",-1)),a(r,{loading:E.value,onClick:e[0]||(e[0]=L=>f(!1))},{default:n(()=>[...e[6]||(e[6]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:n(()=>[v.value?(m(),$("div",ge,[s("div",ye,[s("div",we,[e[8]||(e[8]=u(" 登录时自动启动服务 ",-1)),a(A,{type:v.value.enabled?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.enabled?"已开启":"未开启"),1)]),_:1},8,["type"])]),e[9]||(e[9]=s("div",{class:"switch-desc"},[u(" 登录操作系统时自动执行 "),s("code",null,"pm2 resurrect"),u(" 恢复服务(无需管理员权限)。 关闭后首次启动 "),s("code",null,"ellm-proxy"),u(" 也不会再自动注册,重新开启即可解除。 ")],-1))]),a(U,{"model-value":v.value.enabled,loading:_.value==="autostart",onChange:e[1]||(e[1]=L=>P(!!L))},null,8,["model-value","loading"])])):(m(),B(M,{key:1,description:V.value||"暂无自启状态"},{default:n(()=>[a(r,{type:"primary",onClick:e[2]||(e[2]=L=>f(!1))},{default:n(()=>[...e[10]||(e[10]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1}),a(O,{shadow:"never",class:"mb-16"},{header:n(()=>[...e[11]||(e[11]=[s("div",{class:"flex-between"},[s("span",{class:"fw-600"},"自动更新")],-1)])]),default:n(()=>[i.value?(m(),$("div",be,[s("div",Ce,[s("div",ke,[e[12]||(e[12]=u(" 自动检查并安装新版本 ",-1)),a(A,{type:i.value.enabled?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(i.value.enabled?"已开启":"未开启"),1)]),_:1},8,["type"])]),s("div",he,[i.value.supported?(m(),$(H,{key:1},[u(" 每小时检查私有 npm 源,发现新版本自动安装并重启服务(更新期间服务短暂中断); 关闭后仍可在下方手动检查与更新。 ")],64)):(m(),$(H,{key:0},[u(" 当前为源码运行模式,更新功能不可用(仅 npm 全局安装的包支持自动更新)。 ")],64))])]),i.value.supported?(m(),B(U,{key:0,"model-value":i.value.enabled,loading:_.value==="updateEnabled",onChange:e[3]||(e[3]=L=>R(!!L))},null,8,["model-value","loading"])):D("",!0)])):(m(),B(M,{key:1,description:x.value||"暂无更新状态"},{default:n(()=>[a(r,{type:"primary",onClick:e[4]||(e[4]=L=>f(!1))},{default:n(()=>[...e[13]||(e[13]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1}),a(O,{shadow:"never"},{header:n(()=>[...e[14]||(e[14]=[s("div",{class:"flex-between"},[s("span",{class:"fw-600"},"版本信息")],-1)])]),default:n(()=>[i.value?(m(),$("div",xe,[s("div",$e,[e[17]||(e[17]=s("span",{class:"version-label"},"当前版本",-1)),a(A,{size:"small"},{default:n(()=>[u(h(o.value||"未知"),1)]),_:1}),y.value?(m(),$(H,{key:0},[e[16]||(e[16]=s("span",{class:"version-label"},"最新版本",-1)),a(A,{type:p.value?"warning":"success",size:"small","disable-transitions":""},{default:n(()=>[u(h(y.value),1)]),_:1},8,["type"]),p.value?(m(),B(A,{key:0,type:"warning",size:"small","disable-transitions":""},{default:n(()=>[...e[15]||(e[15]=[u("发现新版本",-1)])]),_:1})):D("",!0)],64)):D("",!0)]),s("div",Ve,[u(" 上次检查:"+h(l.value||"尚未检查")+" ",1),i.value.updating?(m(),$(H,{key:0},[u(" · 更新执行中...")],64)):D("",!0)]),I.value?(m(),$("div",Te,"上次更新:"+h(I.value),1)):D("",!0),s("div",Ae,[a(r,{loading:C.value,disabled:i.value.updating,onClick:N},{default:n(()=>[u(h(C.value?"检查中...":"检查更新"),1)]),_:1},8,["loading","disabled"]),p.value?(m(),B(r,{key:0,type:"primary",loading:S.value,disabled:i.value.updating,onClick:T},{default:n(()=>[u(h(S.value?"更新中...":`立即更新到 ${y.value}`),1)]),_:1},8,["loading","disabled"])):D("",!0)]),k.value?(m(),B(w,{key:1,title:k.value,type:"info","show-icon":"",class:"mt-8",onClose:e[5]||(e[5]=L=>k.value=null)},null,8,["title"])):D("",!0)])):(m(),B(M,{key:1,description:"更新状态不可用"}))]),_:1})])),[[Z,b.value]])}}}),Se=Q(Ee,[["__scopeId","data-v-03f482d3"]]),Ue={class:"flex-col-gap"},Be={class:"flex-between"},Ie={class:"muted-sm"},Pe={key:0},De={class:"flex-end"},Re=F({__name:"SettingsView",setup(J){const{toast:g}=Y(),b=le(),E=d("basic"),v=d(!0),V=d(!1),i=d(""),x=d(29770),_=d(null),C=d("passthrough"),S=d(!1),k=d(""),f=d(""),o=async()=>{var p;try{await b.load(),i.value=b.config.masterKey||"",x.value=b.config.port??29770,C.value=b.config.compression==="plain"?"plain":"passthrough",S.value=b.config.logContent===!0;const l=(p=b.config["web-ui"])==null?void 0:p["login-account"];k.value=(l==null?void 0:l.username)||"",f.value=(l==null?void 0:l.password)||"";try{_.value=await ee.get("/api/status").then(I=>I.listenPort)}catch{_.value=null}}catch(l){g(l.message,"error")}finally{v.value=!1}},y=async()=>{V.value=!0;try{await b.update(p=>{var l;p.masterKey=i.value.trim(),p.port=x.value,p.compression=C.value,p.logContent=S.value,p["web-ui"]??(p["web-ui"]={}),(l=p["web-ui"])["login-account"]??(l["login-account"]={}),p["web-ui"]["login-account"].username=k.value.trim(),p["web-ui"]["login-account"].password=f.value}),g("设置已保存并生效","success")}catch(p){g(p.message,"error")}finally{V.value=!1}};return X(o),(p,l)=>{const I=c("el-tag"),z=c("el-input"),P=c("el-form-item"),R=c("el-input-number"),N=c("el-option"),T=c("el-select"),t=c("el-switch"),e=c("el-form"),r=c("el-card"),A=c("el-button"),U=c("el-tab-pane"),M=c("el-tabs"),O=G("loading");return m(),B(M,{modelValue:E.value,"onUpdate:modelValue":l[6]||(l[6]=w=>E.value=w),class:"settings-tabs"},{default:n(()=>[a(U,{label:"基本设置",name:"basic"},{default:n(()=>[q((m(),$("div",Ue,[a(r,{shadow:"never"},{header:n(()=>[s("div",Be,[l[8]||(l[8]=s("span",{class:"fw-600"},"基本设置",-1)),a(I,{type:"success"},{default:n(()=>[...l[7]||(l[7]=[u("运行中",-1)])]),_:1})])]),default:n(()=>[a(e,{"label-position":"top"},{default:n(()=>[a(P,{label:"Master Key"},{default:n(()=>[a(z,{modelValue:i.value,"onUpdate:modelValue":l[0]||(l[0]=w=>i.value=w),type:"password",placeholder:"sk-1234","show-password":""},null,8,["modelValue"]),l[9]||(l[9]=s("div",{class:"muted-sm"}," 网关 API 认证密钥(/v1/* 请求需携带 Bearer <masterKey>;留空 = 本地放行) ",-1))]),_:1}),a(P,{label:"监听端口 (port)"},{default:n(()=>[a(R,{modelValue:x.value,"onUpdate:modelValue":l[1]||(l[1]=w=>x.value=w),min:1,max:65535,step:1,class:"full-width"},null,8,["modelValue"]),s("div",Ie,[l[11]||(l[11]=u(" 网关与管理台 API 的监听端口,保存后下次重启生效 ",-1)),_.value!==null?(m(),$(H,{key:0},[l[10]||(l[10]=u(" ;当前实际监听 ",-1)),s("b",null,h(_.value),1),_.value!==x.value?(m(),$("span",Pe,"(由 PORT 环境变量覆盖,此配置值不生效)")):D("",!0)],64)):D("",!0)])]),_:1}),a(P,{label:"压缩语义 (compression)"},{default:n(()=>[a(T,{modelValue:C.value,"onUpdate:modelValue":l[2]||(l[2]=w=>C.value=w),class:"full-width"},{default:n(()=>[a(N,{label:"透传 (passthrough)",value:"passthrough"}),a(N,{label:"明文 (plain)",value:"plain"})]),_:1},8,["modelValue"]),l[12]||(l[12]=s("div",{class:"muted-sm"},"上游响应压缩处理方式,保存后热更新生效",-1))]),_:1}),a(P,{label:"日志内容捕获 (logContent)"},{default:n(()=>[a(t,{modelValue:S.value,"onUpdate:modelValue":l[3]||(l[3]=w=>S.value=w)},null,8,["modelValue"]),l[13]||(l[13]=s("div",{class:"muted-sm"}," 记录完整请求/响应(含上游原始与转换后内容)进日志详情,用于排查定位问题;开启后有少量内存与存储开销,保存后热更新生效 ",-1))]),_:1}),a(P,{label:"管理台用户名 (web-ui.login-account.username)"},{default:n(()=>[a(z,{modelValue:k.value,"onUpdate:modelValue":l[4]||(l[4]=w=>k.value=w),placeholder:"admin"},null,8,["modelValue"]),l[14]||(l[14]=s("div",{class:"muted-sm"}," 管理台登录用户名,留空回退 WEBUI_USERNAME 环境变量或默认 admin ",-1))]),_:1}),a(P,{label:"管理台密码 (web-ui.login-account.password)"},{default:n(()=>[a(z,{modelValue:f.value,"onUpdate:modelValue":l[5]||(l[5]=w=>f.value=w),type:"password",placeholder:"admin","show-password":""},null,8,["modelValue"]),l[15]||(l[15]=s("div",{class:"muted-sm"}," 管理台登录密码,留空回退 WEBUI_PASSWORD 环境变量或默认 admin;修改保存后立即生效,需用新账号重新登录 ",-1))]),_:1})]),_:1})]),_:1}),s("div",De,[a(A,{type:"primary",loading:V.value,disabled:V.value,onClick:y},{default:n(()=>[u(h(V.value?"保存中...":"保存设置"),1)]),_:1},8,["loading","disabled"])])])),[[O,v.value]])]),_:1}),a(U,{label:"代理开关",name:"harness",lazy:""},{default:n(()=>[a(me)]),_:1}),a(U,{label:"服务与更新",name:"service",lazy:""},{default:n(()=>[a(Se)]),_:1})]),_:1},8,["modelValue"])}}});export{Re as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{W as p,r as u,l as r}from"./index-GohB4JDa.js";import{A as i}from"./useApi-D_iJ2Mca.js";const m=p("config",()=>{const e=u({}),o=u(!1),t=r(()=>Array.isArray(e.value.providers)?e.value.providers:[]),l=r(()=>t.value.length),c=r(()=>t.value.reduce((a,d)=>{var s;return a+(((s=d.models)==null?void 0:s.length)??0)},0));async function f(){o.value=!0;try{e.value=await i.get("/api/config")}finally{o.value=!1}}async function n(){return await i.put("/api/config",e.value)}async function v(a){return a(e.value),await n()}return{config:e,loading:o,providers:t,providerCount:l,totalModelCount:c,load:f,save:n,update:v}});export{m as u};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a9 as f}from"./index-GohB4JDa.js";var h;const g=((h=window.__ELLM_CONFIG__)==null?void 0:h.API_BASE)??"",p=3e4;let n=null;function _(){return n===null&&(n=localStorage.getItem("webui_token")),n}function m(){n=null,localStorage.removeItem("webui_token"),f.push("/login")}async function e(t,o,s){const l={"Content-Type":"application/json"},c=_();c&&(l.Authorization=`Bearer ${c}`);const u=new AbortController,E=setTimeout(()=>u.abort(),p);try{const r=await fetch(`${g}${o}`,{method:t,headers:l,body:s!==void 0?JSON.stringify(s):void 0,signal:u.signal});if(r.status===401){const T=await r.text();let i={};try{i=T?JSON.parse(T):{}}catch{}throw o==="/api/auth/login"?new Error(i.error||"用户名或密码错误"):(m(),new Error(i.error||"未登录"))}const w=await r.text();let a;try{a=w?JSON.parse(w):{}}catch{throw new Error(`响应格式错误 (HTTP ${r.status})`)}if(!r.ok)throw new Error(a.error||`HTTP ${r.status}`);return a}catch(r){throw r.name==="AbortError"?new Error("请求超时"):r}finally{clearTimeout(E)}}const S={get:t=>e("GET",t),post:(t,o)=>e("POST",t,o),put:(t,o)=>e("PUT",t,o),del:t=>e("DELETE",t)};export{S as A};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var i;const c=((i=window.__ELLM_CONFIG__)==null?void 0:i.API_BASE)??"";function h(){try{const t=c?new URL(c):new URL(location.href);return t.port?`${t.protocol}//${t.hostname}:${Number(t.port)+1}`:null}catch{return null}}var p;const l=((p=window.__ELLM_CONFIG__)==null?void 0:p.MANAGER_BASE)??h(),E=3e4;async function e(t,u,s,d=E){if(l===null)throw new Error("无法确定管理服务地址:config.js 未配置 MANAGER_BASE,且页面经 80/443 端口访问(无法按端口+1 推导)");const a=new AbortController,T=setTimeout(()=>a.abort(),d);try{const r=await fetch(`${l}${u}`,{method:t,headers:{"Content-Type":"application/json"},body:s!==void 0?JSON.stringify(s):void 0,signal:a.signal}),n=await r.text();let o;try{o=n?JSON.parse(n):{}}catch{throw new Error(`管理服务响应格式错误 (HTTP ${r.status})`)}if(!r.ok||o.ok===!1)throw new Error(o.error||`HTTP ${r.status}`);return o}catch(r){throw r.name==="AbortError"?new Error("请求超时"):r instanceof TypeError?new Error("管理服务不可达(请确认 ellm-proxy 已启动,且本页面通过 localhost/127.0.0.1 访问)"):r}finally{clearTimeout(T)}}const w={status:()=>e("GET","/status"),processes:()=>e("GET","/processes").then(t=>t.processes),start:()=>e("POST","/start"),stop:()=>e("POST","/stop"),restart:()=>e("POST","/restart"),autostart:()=>e("GET","/autostart"),setAutostart:t=>e("POST","/autostart",{enabled:t}),update:()=>e("GET","/update"),setUpdateEnabled:t=>e("POST","/update/enabled",{enabled:t}),checkUpdate:()=>e("POST","/update/check"),applyUpdate:()=>e("POST","/update/apply",void 0,6e5),harness:()=>e("GET","/harness"),setClaudeCode:t=>e("POST","/harness/claude-code",{enabled:t}),setCodex:t=>e("POST","/harness/codex",{enabled:t}),setDsh:t=>e("POST","/harness/dsh",{enabled:t})};export{w as m};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a8 as a}from"./index-GohB4JDa.js";function u(){function s(t,o="success"){a({message:t,type:o,duration:3e3})}return{toast:s}}export{u};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// harness 配置文件备份/还原(claude-code 与 codex 共用):
|
|
2
|
-
// 开启前备份原文件为 <file>.ellm-bak(已存在则保留最早原始配置不覆盖);关闭 = 从备份还原并删除备份。
|
|
3
|
-
import { existsSync, copyFileSync, rmSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
/** 备份原文件(<file>.ellm-bak;备份已存在则保留最早原始配置不覆盖)。返回是否新建了备份 */
|
|
6
|
-
export function backupOnce(file) {
|
|
7
|
-
const bak = `${file}.ellm-bak`;
|
|
8
|
-
if (existsSync(bak)) return false;
|
|
9
|
-
if (existsSync(file)) copyFileSync(file, bak);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** 从备份还原并删除备份;无备份返回 false */
|
|
14
|
-
export function restoreFromBackup(file) {
|
|
15
|
-
const bak = `${file}.ellm-bak`;
|
|
16
|
-
if (!existsSync(bak)) return false;
|
|
17
|
-
copyFileSync(bak, file);
|
|
18
|
-
rmSync(bak, { force: true });
|
|
19
|
-
return true;
|
|
20
|
-
}
|