ellm-proxy 0.0.12 → 0.0.14
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 +115 -25
- package/dist/cli.js +8 -59
- package/dist/server.js +6 -24
- package/dist/web-ui/assets/{DashboardView-BWPFis7P.js → DashboardView-C_UXsKRa.js} +1 -1
- package/dist/web-ui/assets/{LayoutView-BkUJgLW2.js → LayoutView-VF0751s6.js} +1 -1
- package/dist/web-ui/assets/{LoginView-C8E6nJw2.js → LoginView-9pRN4_jz.js} +1 -1
- package/dist/web-ui/assets/{LogsView-CtRfyOCZ.js → LogsView-Ddiqpn27.js} +1 -1
- package/dist/web-ui/assets/{ProvidersView-B6fm2FPQ.js → ProvidersView-BisIWeCb.js} +2 -2
- package/dist/web-ui/assets/{SettingsView-BHuXlGS1.js → SettingsView-2J7lJg6I.js} +1 -1
- package/dist/web-ui/assets/{SetupView-Oau3EJgh.js → SetupView-ByC3ZfGq.js} +1 -1
- package/dist/web-ui/assets/{config-CHaFDQC7.js → config-CFGI6BQU.js} +1 -1
- package/dist/web-ui/assets/{index-BmtcBnoW.js → index-DzBTME5Z.js} +2 -2
- package/dist/web-ui/assets/{providers-Aw7T7j0r.js → providers-DN-6AbdJ.js} +1 -1
- package/dist/web-ui/assets/{useManagerApi-PldAaNYo.js → useManagerApi-C3X3JX66.js} +1 -1
- package/dist/web-ui/assets/{useToast-C9BTyaBD.js → useToast-BIVFyGJl.js} +1 -1
- package/dist/web-ui/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# ellm-proxy
|
|
2
2
|
|
|
3
|
-
自研 LLM 网关(行为基准 litellm 1.100.0
|
|
4
|
-
|
|
5
|
-
> 需求决策与架构设计详见 [docs/DESIGN.md](docs/DESIGN.md)。
|
|
3
|
+
自研 LLM 网关(行为基准 litellm 1.100.0),单进程后台常驻、一键启停(跨平台,内置进程管理,无 pm2)。单端口提供全部能力——网关、管理台 API 与管理界面同端口(默认 29770):
|
|
6
4
|
|
|
7
5
|
- `/v1/*` — LLM 网关端点(OpenAI / Anthropic 协议互转,多供应商路由、压缩、鉴权)
|
|
8
6
|
- `/api/*` — 管理台 API(登录鉴权、配置热更新、用量/日志)
|
|
9
7
|
- `http://127.0.0.1:<port>/` — 管理界面(服务进程托管 web-ui,同端口;远程访问靠登录保护)
|
|
10
8
|
|
|
9
|
+
> 需求决策与架构设计详见 [docs/DESIGN.md](docs/DESIGN.md)。
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- **协议互转**:客户端与上游可各用各的协议,网关自动转换——`anthropic ↔ chat-completions` 两条跨协议桥 + 同协议直通 + `responses` 路径,共 4 条请求链
|
|
14
|
+
- **多供应商路由**:供应商/模型两级配置,模型可单独覆盖协议与上游地址;内置 10 个供应商模板(deepseek、kimi、zhipu、zhipu-coding、ark、ark-coding、dashscope、openai、st(商汤)、custom),管理台拖拽排序、一键测试
|
|
15
|
+
- **autoV1**:上游地址缺 `/v1` 时自动补全(provider 级可关、model 级可覆盖)
|
|
16
|
+
- **鉴权**:网关主密钥(`/v1/*`)与管理台登录(`/api/*`、web-ui)两套独立体系
|
|
17
|
+
- **日志**:SQLite 存储,brotli 压缩 + 内容寻址去重(重复长文本只存一份),分层保留清理
|
|
18
|
+
- **代理开关**:一键把 Claude Code、Codex、DeepSeek Harness(dsh)的流量指向本机网关,含配置备份还原
|
|
19
|
+
- **自动更新**(可选开启):手动一键更新或每小时看门狗自动检查,安装失败自动回滚
|
|
20
|
+
- **开机自启**:三平台登录触发直启,免管理员权限
|
|
21
|
+
|
|
11
22
|
## 安装
|
|
12
23
|
|
|
13
24
|
要求 Node.js **>= 22.13.0**(内置 `node:sqlite`,无需 flag)。
|
|
@@ -16,30 +27,112 @@
|
|
|
16
27
|
npm i -g ellm-proxy
|
|
17
28
|
```
|
|
18
29
|
|
|
19
|
-
##
|
|
30
|
+
## 快速开始
|
|
20
31
|
|
|
21
32
|
```bash
|
|
22
|
-
ellm-proxy
|
|
23
|
-
|
|
33
|
+
ellm-proxy start # 后台常驻启动(首次自动创建示例配置)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
1. 打开管理界面 `http://127.0.0.1:29770/`,首次访问进入**首启向导**:选择部署形态(本机使用 / 局域网公开),设置网关主密钥与登录账号(局域网公开形态强制设置主密钥和账号,且禁用 admin/admin;本机使用可全部跳过)。
|
|
37
|
+
2. 在「供应商」页添加上游供应商(可选内置模板),配置 API Key 与模型映射。
|
|
38
|
+
3. 把客户端 base_url 指向网关即可,例如 Claude Code 指向 `http://127.0.0.1:29770`,OpenAI SDK 指向 `http://127.0.0.1:29770/v1`(也可直接用管理台「设置 → 代理开关」一键接入)。
|
|
39
|
+
|
|
40
|
+
## 网关端点
|
|
41
|
+
|
|
42
|
+
| 端点 | 说明 |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `POST /v1/chat/completions` | OpenAI Chat Completions |
|
|
45
|
+
| `POST /v1/messages` | Anthropic Messages |
|
|
46
|
+
| `POST /v1/messages/count_tokens` | token 计数(本地估算,不触上游) |
|
|
47
|
+
| `POST /v1/responses` | OpenAI Responses |
|
|
48
|
+
| `GET /v1/models` | 模型列表 |
|
|
49
|
+
| 裸路径别名 `/chat/completions`、`/messages`、`/responses`… | 兼容不带 `/v1` 的 base_url(如 Codex) |
|
|
50
|
+
|
|
51
|
+
网关鉴权:配置 `masterKey`(或环境变量 `ELLM_MASTER_KEY`,低于配置文件)后,`/v1/*` 需 `Bearer <key>`;未设置主密钥则放行(本机个人使用模式)。可用 `ellm-proxy master-key` 查看/设置。
|
|
52
|
+
|
|
53
|
+
## 供应商配置
|
|
54
|
+
|
|
55
|
+
配置文件由首启或 `ellm-proxy config` 自动创建,示例:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"port": 29770,
|
|
60
|
+
"masterKey": "",
|
|
61
|
+
"providers": [
|
|
62
|
+
{
|
|
63
|
+
"name": "my-openai",
|
|
64
|
+
"alias": "my",
|
|
65
|
+
"customLlmProvider": "openai",
|
|
66
|
+
"apiKey": "os.environ/MY_OPENAI_API_KEY",
|
|
67
|
+
"apiBase": "https://api.openai.com/v1",
|
|
68
|
+
"models": [
|
|
69
|
+
{ "modelName": "my/gpt-4o-mini", "model": "gpt-4o-mini" }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "my-anthropic",
|
|
74
|
+
"customLlmProvider": "anthropic",
|
|
75
|
+
"models": [
|
|
76
|
+
{ "modelName": "my/claude-sonnet-4-5", "model": "claude-sonnet-4-5" },
|
|
77
|
+
{
|
|
78
|
+
"modelName": "my/mixed-model",
|
|
79
|
+
"model": "some-model",
|
|
80
|
+
"customLlmProvider": "openai",
|
|
81
|
+
"apiBase": "https://example.com/v1",
|
|
82
|
+
"apiKey": "os.environ/OTHER_KEY"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
要点:
|
|
91
|
+
|
|
92
|
+
- `customLlmProvider` 为上游协议(`openai` | `anthropic` | `openai-responses`),provider 级设置后可被单个 model 覆盖(如上例 `my/mixed-model`),客户端协议与上游协议任意组合自动互转;
|
|
93
|
+
- `apiKey` 支持 `os.environ/NAME` 前缀引用环境变量;
|
|
94
|
+
- `apiBase` 不以 `/vN` 结尾时自动补 `/v1`(`autoV1` 可在 provider 级关闭、model 级覆盖);
|
|
95
|
+
- 内置模板在管理台「供应商 → 添加」中选择,免去手写地址。
|
|
96
|
+
|
|
97
|
+
修改配置两种方式:直接编辑配置文件,或在管理界面在线修改(实时热更新)。
|
|
98
|
+
|
|
99
|
+
## 管理界面
|
|
100
|
+
|
|
101
|
+
- **首页**:服务信息卡(运行状态、PID、内存、API 密钥)+ 用量概览
|
|
102
|
+
- **供应商**:拖拽排序,模板化添加,模型级覆盖协议/地址,逐个或批量测试
|
|
103
|
+
- **日志**:按日期/会话筛选,查看请求详情(含压缩去重存储的原始内容,可开关捕获)
|
|
104
|
+
- **设置**:三个标签页——基本设置(主密钥/端口/压缩/内容捕获/登录账号)、代理开关(Claude Code / Codex / dsh 一键指向网关,自动备份原配置、可还原)、服务与更新(自动更新、开机自启)
|
|
105
|
+
|
|
106
|
+
登录鉴权:未设置账号时免登录直接进入(本机使用默认形态);`ellm-proxy account <user> <pass>` 设置账号(密码 ≥6 位),`--reset` 清除恢复免登录。
|
|
107
|
+
|
|
108
|
+
## CLI 参考
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ellm-proxy(无参数) # 已运行则显示服务信息,否则启动
|
|
112
|
+
ellm-proxy start # 后台常驻启动(选项:--port / --config / --data-dir / --working-dir)
|
|
24
113
|
ellm-proxy stop # 停止服务
|
|
25
114
|
ellm-proxy restart # 重启(先 stop 后 start,保证最新参数生效,选项同 start)
|
|
26
|
-
ellm-proxy(无参数) # 已运行则显示服务信息,否则启动
|
|
27
115
|
ellm-proxy autostart on # 开机自启(登录时触发,免管理员权限)
|
|
28
116
|
ellm-proxy autostart off # 关闭开机自启
|
|
29
|
-
ellm-proxy config #
|
|
30
|
-
ellm-proxy master-key # 查看/设置网关主密钥(--random / --clear
|
|
31
|
-
ellm-proxy account # 查看/设置管理台登录账号(--reset
|
|
117
|
+
ellm-proxy config # 显示配置文件路径(不存在自动创建示例配置)
|
|
118
|
+
ellm-proxy master-key # 查看/设置网关主密钥(--random 随机生成 / --clear 清除)
|
|
119
|
+
ellm-proxy account # 查看/设置管理台登录账号(--reset 恢复免登录)
|
|
120
|
+
ellm-proxy -h # 查看帮助
|
|
32
121
|
```
|
|
33
122
|
|
|
34
|
-
|
|
123
|
+
启动时端口若被占用会自动顺延到下一个空闲端口(仅本次运行生效,不改配置文件),顺延结果打印在启动摘要中。
|
|
124
|
+
|
|
125
|
+
## 进程管理与开机自启
|
|
126
|
+
|
|
127
|
+
**进程管理(内置,跨平台)**:CLI 启动时以 detached 方式拉起服务进程并写运行记录 `~/.ellm-proxy/run.json`,探活以「端口 HTTP 可达」为准。服务进程自持崩溃自愈(未捕获异常自动孵化后继进程接管端口,1 分钟窗口封顶 10 次,防端口占用无限循环),日志写 `<appDir>/logs/`。Node ≥22.13 由 CLI 解析(当前 node 满足则直接用,否则经 get-node 自动下载一次并缓存)。
|
|
35
128
|
|
|
36
|
-
|
|
129
|
+
**热重启**:配置变更或更新时触发 self-respawn——服务先孵化新进程接管端口再退出,停机 1~2 秒。
|
|
37
130
|
|
|
38
|
-
**开机自启(跨平台)**:`ellm-proxy autostart on` 注册登录触发的自启项(内容为直接启动服务,含运行参数 env 快照),三平台均无需管理员权限——Windows
|
|
131
|
+
**开机自启(跨平台)**:`ellm-proxy autostart on` 注册登录触发的自启项(内容为直接启动服务,含运行参数 env 快照),三平台均无需管理员权限——Windows 写启动文件夹(vbs 隐藏窗口 + cmd)、macOS 写 LaunchAgent(`launchctl load`)、Linux 写 systemd user unit(`systemctl --user enable`,WSL/无 systemd 环境会报错提示);`autostart off` 移除。
|
|
39
132
|
|
|
40
133
|
## 自动更新(可选开启)
|
|
41
134
|
|
|
42
|
-
- **手动更新**:管理台「设置 →
|
|
135
|
+
- **手动更新**:管理台「设置 → 服务与更新」检查并一键更新——服务先回响应再自重启,新进程执行 `npm i -g` 安装并自动换新代码生效(双跳),安装失败自动回滚旧版本继续服务。
|
|
43
136
|
- **自动更新**:看门狗每小时检查 npm 源(默认 npmjs,`ELLM_UPDATE_REGISTRY` 可覆盖),发现新版本自动走同一链路;开关存配置文件顶层 `autoUpdate`。
|
|
44
137
|
- 源码/本地构建运行时更新域自动关闭(仅 npm 全局安装的包支持)。
|
|
45
138
|
|
|
@@ -56,23 +149,20 @@ ellm-proxy account # 查看/设置管理台登录账号(--reset)
|
|
|
56
149
|
|
|
57
150
|
所有路径均支持相对路径(按当前工作目录展开)与绝对路径。服务端自身不加载任何 `.env` 文件;`ELLM_MASTER_KEY` 等环境变量直接在会话中导出即可。
|
|
58
151
|
|
|
59
|
-
|
|
152
|
+
其他环境变量:`ELLM_PORT`(服务监听端口,或 `start --port`)、`HOST`(监听地址)。日志保留策略:配置 `logs.detail-retention-days` / `logs.row-retention-days`(详情/摘要分层保留,0 = 永久),默认启动后 30 秒首跑清理、之后每 6 小时一次。
|
|
60
153
|
|
|
61
|
-
|
|
62
|
-
2. 启动服务后访问管理界面 `http://127.0.0.1:29770/` 在线修改(实时热更新)。
|
|
154
|
+
## 从旧版本升级
|
|
63
155
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## 从 0.0.7 及更早版本升级
|
|
67
|
-
|
|
68
|
-
1. `npm i -g ellm-proxy@latest`(或等自动更新);
|
|
69
|
-
2. 直接 `ellm-proxy start`——旧版 pm2 托管的进程与守护会被自动清理,pm2 自启项自动迁移为直启模式;
|
|
70
|
-
3. 管理台地址从 `http://127.0.0.1:29771/` 变更为 `http://127.0.0.1:29770/`(单端口化,29771 退役)。
|
|
156
|
+
`npm i -g ellm-proxy@latest` 后直接 `ellm-proxy start` 即可。0.0.7 及更早版本的 pm2 托管进程/守护会被自动清理,pm2 自启项自动迁移为直启模式;管理台地址已单端口化为 `http://127.0.0.1:29770/`(29771 退役)。
|
|
71
157
|
|
|
72
158
|
## 本地开发(仓库内)
|
|
73
159
|
|
|
74
|
-
本包是 `ellm` monorepo
|
|
160
|
+
本包是 `ellm` monorepo(pnpm workspace)的发布载体。源码分四个包:`server`(Hono 单进程后端 + CLI)、`gateway`(网关主体:配置展开/路由/观测)、`protocol`(协议适配层:4 条请求链)、`web-ui`(Vue3 + Element Plus 管理界面)。发布版 `dist/server.js` 与 `dist/cli.js` 由 esbuild 从 `server` 源码全量内联构建,`web-ui` 产物拷入 `dist/web-ui/` 由服务进程托管:
|
|
75
161
|
|
|
76
162
|
```bash
|
|
77
163
|
pnpm pack:proxy # 构建 web-ui → 构建发布版 + 聚合 → npm pack 生成 tgz
|
|
164
|
+
pnpm test # 全部包测试(vitest)
|
|
165
|
+
pnpm build:all:restart # 全量构建后重启本机服务
|
|
78
166
|
```
|
|
167
|
+
|
|
168
|
+
发布走 GitHub Actions:推送 `v*.*.*` tag 自动校验版本并 `npm publish`(包名 [ellm-proxy](https://www.npmjs.com/package/ellm-proxy))。
|
package/dist/cli.js
CHANGED
|
@@ -20915,8 +20915,7 @@ function absolutize(opts) {
|
|
|
20915
20915
|
}
|
|
20916
20916
|
|
|
20917
20917
|
// ../server/src/cli/service.ts
|
|
20918
|
-
import {
|
|
20919
|
-
import { homedir as homedir3 } from "node:os";
|
|
20918
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
20920
20919
|
import { join as join7 } from "node:path";
|
|
20921
20920
|
|
|
20922
20921
|
// ../server/src/proc/index.ts
|
|
@@ -35173,7 +35172,6 @@ var PLIST_LABEL = "com.ellm-proxy.autostart";
|
|
|
35173
35172
|
var LINUX_UNIT = "ellm-proxy.service";
|
|
35174
35173
|
var OPT_OUT_FILE = "autostart.opt-out";
|
|
35175
35174
|
var MARKER = "ellm-proxy-v3";
|
|
35176
|
-
var V2_MARKER = "ellm-proxy-v2";
|
|
35177
35175
|
var ENV_WHITELIST_PREFIX = "ELLM_";
|
|
35178
35176
|
var ENV_WHITELIST_KEYS = /* @__PURE__ */ new Set(["WEBUI_USERNAME", "WEBUI_PASSWORD"]);
|
|
35179
35177
|
function collectAutostartEnv(env10 = process.env) {
|
|
@@ -35239,7 +35237,7 @@ function darwinBody(nodePath, target) {
|
|
|
35239
35237
|
return [
|
|
35240
35238
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
35241
35239
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
35242
|
-
|
|
35240
|
+
`<!-- ${MARKER}: launch service directly -->`,
|
|
35243
35241
|
'<plist version="1.0">',
|
|
35244
35242
|
"<dict>",
|
|
35245
35243
|
` <key>Label</key><string>${PLIST_LABEL}</string>`,
|
|
@@ -35263,8 +35261,8 @@ function darwinBody(nodePath, target) {
|
|
|
35263
35261
|
function linuxBody(nodePath, target) {
|
|
35264
35262
|
const envLines = Object.entries(target.env).map(([k, v]) => `Environment="${k}=${v.replace(/"/g, '\\"')}"`).join("\n");
|
|
35265
35263
|
return [
|
|
35266
|
-
|
|
35267
|
-
|
|
35264
|
+
`[Unit]`,
|
|
35265
|
+
`Description=ellm-proxy autostart (${MARKER}, direct service start)`,
|
|
35268
35266
|
"After=default.target",
|
|
35269
35267
|
"",
|
|
35270
35268
|
"[Service]",
|
|
@@ -35285,7 +35283,6 @@ async function windowsOn(nodePath, target) {
|
|
|
35285
35283
|
const cmdPath = join6(target.appDir, AUTOSTART_CMD);
|
|
35286
35284
|
writeFileSync3(cmdPath, windowsBody(nodePath, target), "utf-8");
|
|
35287
35285
|
writeFileSync3(join6(startupDir, AUTOSTART_VBS), `\uFEFF${windowsVbsBody(cmdPath)}`, "utf16le");
|
|
35288
|
-
rmSync2(join6(startupDir, AUTOSTART_CMD), { force: true });
|
|
35289
35286
|
return join6(startupDir, AUTOSTART_VBS);
|
|
35290
35287
|
}
|
|
35291
35288
|
async function darwinOn(nodePath, target) {
|
|
@@ -35318,12 +35315,9 @@ async function linuxOn(nodePath, target) {
|
|
|
35318
35315
|
return itemPath;
|
|
35319
35316
|
}
|
|
35320
35317
|
async function windowsOff() {
|
|
35321
|
-
const
|
|
35322
|
-
const
|
|
35323
|
-
const legacyCmd = join6(startupDir, AUTOSTART_CMD);
|
|
35324
|
-
const existed = existsSync2(vbsPath) || existsSync2(legacyCmd);
|
|
35318
|
+
const vbsPath = join6(windowsStartupDir(), AUTOSTART_VBS);
|
|
35319
|
+
const existed = existsSync2(vbsPath);
|
|
35325
35320
|
rmSync2(vbsPath, { force: true });
|
|
35326
|
-
rmSync2(legacyCmd, { force: true });
|
|
35327
35321
|
return existed;
|
|
35328
35322
|
}
|
|
35329
35323
|
async function darwinOff() {
|
|
@@ -35349,28 +35343,15 @@ function readTextDetectingBom(path7) {
|
|
|
35349
35343
|
function autostartStatus(appDir) {
|
|
35350
35344
|
const itemPath = platformItemPath();
|
|
35351
35345
|
let enabled = false;
|
|
35352
|
-
let outdated = false;
|
|
35353
35346
|
if (itemPath && existsSync2(itemPath)) {
|
|
35354
35347
|
try {
|
|
35355
|
-
|
|
35356
|
-
else outdated = true;
|
|
35348
|
+
enabled = readTextDetectingBom(itemPath).includes(MARKER);
|
|
35357
35349
|
} catch {
|
|
35358
35350
|
}
|
|
35359
35351
|
}
|
|
35360
|
-
if (platform8() === "win32" && !enabled) {
|
|
35361
|
-
const legacyCmd = join6(windowsStartupDir(), AUTOSTART_CMD);
|
|
35362
|
-
if (existsSync2(legacyCmd)) {
|
|
35363
|
-
try {
|
|
35364
|
-
if (readTextDetectingBom(legacyCmd).includes(MARKER) || readTextDetectingBom(legacyCmd).includes(V2_MARKER))
|
|
35365
|
-
outdated = true;
|
|
35366
|
-
} catch {
|
|
35367
|
-
}
|
|
35368
|
-
}
|
|
35369
|
-
}
|
|
35370
35352
|
return {
|
|
35371
35353
|
platform: platform8(),
|
|
35372
35354
|
enabled,
|
|
35373
|
-
outdated,
|
|
35374
35355
|
itemPath,
|
|
35375
35356
|
optOut: existsSync2(optOutPath(appDir))
|
|
35376
35357
|
};
|
|
@@ -35404,7 +35385,7 @@ async function ensureAutostartOnStart(target) {
|
|
|
35404
35385
|
if (status.enabled) return;
|
|
35405
35386
|
if (status.optOut) return;
|
|
35406
35387
|
const { itemPath } = await enableAutostart(target);
|
|
35407
|
-
console.log(
|
|
35388
|
+
console.log(`\u5DF2\u81EA\u52A8\u6CE8\u518C\u5F00\u673A\u81EA\u542F: ${itemPath}`);
|
|
35408
35389
|
console.log("\u7981\u7528\u65B9\u5F0F: ellm-proxy autostart off \u6216\u7BA1\u7406\u53F0\u8BBE\u7F6E\u9875\u3002");
|
|
35409
35390
|
} catch (err) {
|
|
35410
35391
|
console.warn(`\u5F00\u673A\u81EA\u542F\u6CE8\u518C\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u6B21\u542F\u52A8\uFF09: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -35506,36 +35487,6 @@ function resolvePort(portEnv, configFile) {
|
|
|
35506
35487
|
}
|
|
35507
35488
|
return port;
|
|
35508
35489
|
}
|
|
35509
|
-
var LEGACY_PM2_APPS = /* @__PURE__ */ new Set(["ellm-proxy", "ellm-proxy-manager"]);
|
|
35510
|
-
async function cleanupLegacyPm2() {
|
|
35511
|
-
const pm2Home = process.env["PM2_HOME"] || join7(homedir3(), ".pm2");
|
|
35512
|
-
const pidsDir = join7(pm2Home, "pids");
|
|
35513
|
-
const daemonPidFile = join7(pm2Home, "pm2.pid");
|
|
35514
|
-
if (!existsSync3(daemonPidFile)) return false;
|
|
35515
|
-
try {
|
|
35516
|
-
const candidates = [];
|
|
35517
|
-
for (const file of existsSync3(pidsDir) ? readdirSync(pidsDir) : []) {
|
|
35518
|
-
const match = /^(.+)-(\d+)\.pid$/.exec(file);
|
|
35519
|
-
if (!match) continue;
|
|
35520
|
-
const pid = Number(readFileSync6(join7(pidsDir, file), "utf-8").trim()) || 0;
|
|
35521
|
-
if (pid && isPidAlive(pid)) candidates.push({ name: match[1] ?? "", pid });
|
|
35522
|
-
}
|
|
35523
|
-
const ours = candidates.filter((c3) => LEGACY_PM2_APPS.has(c3.name));
|
|
35524
|
-
if (ours.length === 0) return false;
|
|
35525
|
-
for (const item of ours) await killTree(item.pid);
|
|
35526
|
-
const othersAlive = candidates.filter((c3) => !LEGACY_PM2_APPS.has(c3.name));
|
|
35527
|
-
if (othersAlive.length === 0) {
|
|
35528
|
-
const daemonPid = Number(readFileSync6(daemonPidFile, "utf-8").trim()) || 0;
|
|
35529
|
-
if (daemonPid && isPidAlive(daemonPid)) await killTree(daemonPid);
|
|
35530
|
-
say("\u5DF2\u6E05\u7406\u65E7\u7248 pm2 \u6258\u7BA1\u8FDB\u7A0B\u4E0E\u5B88\u62A4\uFF08\u672C\u6B21\u5347\u7EA7\u8D77\u670D\u52A1\u7531\u5185\u7F6E\u8FDB\u7A0B\u7BA1\u7406\u6258\u7BA1\uFF09\u3002", ICONS2.skip);
|
|
35531
|
-
} else {
|
|
35532
|
-
warn("\u68C0\u6D4B\u5230 pm2 \u5B88\u62A4\u8FDB\u7A0B\u4ECD\u5728\u8FD0\u884C\uFF08\u8FD8\u6709\u5176\u4ED6\u5E94\u7528\uFF09\u3002\u65E7\u7248 ellm-proxy \u5DF2\u505C\u6B62\uFF0C\u82E5\u5B83\u88AB pm2 \u81EA\u52A8\u62C9\u56DE\uFF0C\u8BF7\u6267\u884C: npx pm2 delete ellm-proxy ellm-proxy-manager");
|
|
35533
|
-
}
|
|
35534
|
-
return true;
|
|
35535
|
-
} catch {
|
|
35536
|
-
return false;
|
|
35537
|
-
}
|
|
35538
|
-
}
|
|
35539
35490
|
async function waitServiceOnline(appDir, timeoutMs = 15e3) {
|
|
35540
35491
|
const deadline = Date.now() + timeoutMs;
|
|
35541
35492
|
let lastError = "";
|
|
@@ -35557,7 +35508,6 @@ async function doStart(opts) {
|
|
|
35557
35508
|
say("\u6B63\u5728\u542F\u52A8 ellm-proxy\uFF08\u5185\u7F6E\u8FDB\u7A0B\u7BA1\u7406\uFF0C\u540E\u53F0\u5E38\u9A7B\uFF09...", ICONS2.rocket);
|
|
35558
35509
|
const appDir = resolveAppDir(opts.workingDir);
|
|
35559
35510
|
const config = ensureConfig(opts.config, appDir);
|
|
35560
|
-
await cleanupLegacyPm2();
|
|
35561
35511
|
const existing = readRunInfo(appDir);
|
|
35562
35512
|
if (existing && isPidAlive(existing.pid)) {
|
|
35563
35513
|
throw new Error(`\u5DF2\u5728\u8FD0\u884C\uFF08pid ${String(existing.pid)}\uFF09\u3002\u8BF7\u5148\u505C\u6B62: ellm-proxy stop`);
|
|
@@ -35600,7 +35550,6 @@ async function doStart(opts) {
|
|
|
35600
35550
|
}
|
|
35601
35551
|
async function doStop() {
|
|
35602
35552
|
const appDir = resolveAppDir();
|
|
35603
|
-
await cleanupLegacyPm2();
|
|
35604
35553
|
const info = readRunInfo(appDir);
|
|
35605
35554
|
if (info && isPidAlive(info.pid)) {
|
|
35606
35555
|
begin(`\u505C\u6B62\u670D\u52A1 (pid ${String(info.pid)}) `);
|
package/dist/server.js
CHANGED
|
@@ -20592,7 +20592,6 @@ var PLIST_LABEL = "com.ellm-proxy.autostart";
|
|
|
20592
20592
|
var LINUX_UNIT = "ellm-proxy.service";
|
|
20593
20593
|
var OPT_OUT_FILE = "autostart.opt-out";
|
|
20594
20594
|
var MARKER = "ellm-proxy-v3";
|
|
20595
|
-
var V2_MARKER = "ellm-proxy-v2";
|
|
20596
20595
|
var ENV_WHITELIST_PREFIX = "ELLM_";
|
|
20597
20596
|
var ENV_WHITELIST_KEYS = /* @__PURE__ */ new Set(["WEBUI_USERNAME", "WEBUI_PASSWORD"]);
|
|
20598
20597
|
function collectAutostartEnv(env = process.env) {
|
|
@@ -20658,7 +20657,7 @@ function darwinBody(nodePath, target) {
|
|
|
20658
20657
|
return [
|
|
20659
20658
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
20660
20659
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
20661
|
-
|
|
20660
|
+
`<!-- ${MARKER}: launch service directly -->`,
|
|
20662
20661
|
'<plist version="1.0">',
|
|
20663
20662
|
"<dict>",
|
|
20664
20663
|
` <key>Label</key><string>${PLIST_LABEL}</string>`,
|
|
@@ -20682,8 +20681,8 @@ function darwinBody(nodePath, target) {
|
|
|
20682
20681
|
function linuxBody(nodePath, target) {
|
|
20683
20682
|
const envLines = Object.entries(target.env).map(([k, v]) => `Environment="${k}=${v.replace(/"/g, '\\"')}"`).join("\n");
|
|
20684
20683
|
return [
|
|
20685
|
-
|
|
20686
|
-
|
|
20684
|
+
`[Unit]`,
|
|
20685
|
+
`Description=ellm-proxy autostart (${MARKER}, direct service start)`,
|
|
20687
20686
|
"After=default.target",
|
|
20688
20687
|
"",
|
|
20689
20688
|
"[Service]",
|
|
@@ -20704,7 +20703,6 @@ async function windowsOn(nodePath, target) {
|
|
|
20704
20703
|
const cmdPath = join5(target.appDir, AUTOSTART_CMD);
|
|
20705
20704
|
writeFileSync5(cmdPath, windowsBody(nodePath, target), "utf-8");
|
|
20706
20705
|
writeFileSync5(join5(startupDir, AUTOSTART_VBS), `\uFEFF${windowsVbsBody(cmdPath)}`, "utf16le");
|
|
20707
|
-
rmSync2(join5(startupDir, AUTOSTART_CMD), { force: true });
|
|
20708
20706
|
return join5(startupDir, AUTOSTART_VBS);
|
|
20709
20707
|
}
|
|
20710
20708
|
async function darwinOn(nodePath, target) {
|
|
@@ -20737,12 +20735,9 @@ async function linuxOn(nodePath, target) {
|
|
|
20737
20735
|
return itemPath;
|
|
20738
20736
|
}
|
|
20739
20737
|
async function windowsOff() {
|
|
20740
|
-
const
|
|
20741
|
-
const
|
|
20742
|
-
const legacyCmd = join5(startupDir, AUTOSTART_CMD);
|
|
20743
|
-
const existed = existsSync7(vbsPath) || existsSync7(legacyCmd);
|
|
20738
|
+
const vbsPath = join5(windowsStartupDir(), AUTOSTART_VBS);
|
|
20739
|
+
const existed = existsSync7(vbsPath);
|
|
20744
20740
|
rmSync2(vbsPath, { force: true });
|
|
20745
|
-
rmSync2(legacyCmd, { force: true });
|
|
20746
20741
|
return existed;
|
|
20747
20742
|
}
|
|
20748
20743
|
async function darwinOff() {
|
|
@@ -20768,28 +20763,15 @@ function readTextDetectingBom(path) {
|
|
|
20768
20763
|
function autostartStatus(appDir) {
|
|
20769
20764
|
const itemPath = platformItemPath();
|
|
20770
20765
|
let enabled = false;
|
|
20771
|
-
let outdated = false;
|
|
20772
20766
|
if (itemPath && existsSync7(itemPath)) {
|
|
20773
20767
|
try {
|
|
20774
|
-
|
|
20775
|
-
else outdated = true;
|
|
20768
|
+
enabled = readTextDetectingBom(itemPath).includes(MARKER);
|
|
20776
20769
|
} catch {
|
|
20777
20770
|
}
|
|
20778
20771
|
}
|
|
20779
|
-
if (platform() === "win32" && !enabled) {
|
|
20780
|
-
const legacyCmd = join5(windowsStartupDir(), AUTOSTART_CMD);
|
|
20781
|
-
if (existsSync7(legacyCmd)) {
|
|
20782
|
-
try {
|
|
20783
|
-
if (readTextDetectingBom(legacyCmd).includes(MARKER) || readTextDetectingBom(legacyCmd).includes(V2_MARKER))
|
|
20784
|
-
outdated = true;
|
|
20785
|
-
} catch {
|
|
20786
|
-
}
|
|
20787
|
-
}
|
|
20788
|
-
}
|
|
20789
20772
|
return {
|
|
20790
20773
|
platform: platform(),
|
|
20791
20774
|
enabled,
|
|
20792
|
-
outdated,
|
|
20793
20775
|
itemPath,
|
|
20794
20776
|
optOut: existsSync7(optOutPath(appDir))
|
|
20795
20777
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{d as te,k as ae,q as le,e as w,g as o,w as s,c as S,i as U,r as p,A as V,a as m,j as oe,o as c,b as se,t as r,f as l,h as _,F as ne,v as ue,m as $,_ as re}from"./index-
|
|
1
|
+
import{d as te,k as ae,q as le,e as w,g as o,w as s,c as S,i as U,r as p,A as V,a as m,j as oe,o as c,b as se,t as r,f as l,h as _,F as ne,v as ue,m as $,_ as re}from"./index-DzBTME5Z.js";import{m as de}from"./useManagerApi-C3X3JX66.js";import{u as ie}from"./providers-DN-6AbdJ.js";import{u as ce}from"./useToast-BIVFyGJl.js";import"./config-CFGI6BQU.js";const ve={class:"flex-col-gap"},pe=["onClick"],me={key:1},_e={class:"flex-between"},ge={class:"flex-gap-8"},fe={class:"usage-grid"},he={class:"usage-cell"},ye={class:"usage-num orange"},we={class:"usage-cell"},be={class:"usage-num blue"},ke={class:"usage-cell"},De={class:"usage-num green"},xe={class:"usage-cell"},Ce={class:"usage-num purple"},Me={class:"usage-cell"},Se={class:"usage-num purple"},Ve={key:0,class:"usage-cell"},Pe={class:"usage-num gray"},$e={key:0},Te=["onClick"],Ae=te({__name:"DashboardView",setup(Be){const D=ie(),{toast:P}=ce(),d=p(null),b=p(!0),x=p(""),v=p(null),k=p("today"),C=t=>{const e=t.getFullYear(),a=String(t.getMonth()+1).padStart(2,"0"),i=String(t.getDate()).padStart(2,"0");return`${e}-${a}-${i}`},R=t=>{const e=new Date;let a,i=e;if(t==="today")a=e;else if(t==="yesterday")a=new Date(e),a.setDate(e.getDate()-1),i=a;else if(t==="beforeYesterday")a=new Date(e),a.setDate(e.getDate()-2),i=a;else if(t==="last3days")a=new Date(e),a.setDate(e.getDate()-2);else if(t==="week"){a=new Date(e);const f=(e.getDay()+6)%7;a.setDate(e.getDate()-f)}else if(t==="month")a=new Date(e.getFullYear(),e.getMonth(),1);else return;k.value=t,v.value=[C(a),C(i)],y()},K=t=>{t?R(t):(k.value="",v.value=null,y())},L=()=>{k.value="",y()},j=$(()=>{var e;const t=new Set;for(const a of D.suppliers)for(const i of a.models)t.add(i.modelName);if((e=d.value)!=null&&e.models)for(const a of d.value.models)t.add(a.model);return[...t].sort()}),y=async()=>{var t;b.value=!0;try{const e=new URLSearchParams;x.value&&e.set("model",x.value),((t=v.value)==null?void 0:t.length)===2&&(e.set("startDate",v.value[0]),e.set("endDate",v.value[1]));const a=e.toString();d.value=await V.get(`/api/usage${a?`?${a}`:""}`)}catch(e){console.error(e)}finally{b.value=!1}},q=async()=>{var t;try{const[e]=await Promise.all([V.get("/api/usage"),D.loadProviders(),V.get("/api/status").then(a=>M.value=a.listenPort).catch(()=>M.value)]);if(d.value=e,!((t=v.value)!=null&&t.length)){const a=new Date;v.value=[C(a),C(a)],await y()}}catch(e){console.error(e)}finally{b.value=!1}},E=window.location.hostname,M=p(Number(window.location.port)||null),O=$(()=>`http://${E}${M.value?`:${M.value}`:""}`),T=p(null),A=p(null),B=p(Date.now()),I=async()=>{try{const[t,e]=await Promise.all([de.processes(),V.get("/api/config").catch(()=>null)]);T.value=t[0]??null,A.value=(e==null?void 0:e.masterKey)??"",B.value=Date.now()}catch{}},z=t=>{if(!t.startedAt)return"-";let e=Math.max(0,Math.floor((B.value-t.startedAt)/1e3));const a=Math.floor(e/86400);e-=a*86400;const i=Math.floor(e/3600);e-=i*3600;const f=Math.floor(e/60);return e-=f*60,[a?`${a}天`:"",i?`${i}时`:"",f?`${f}分`:"",`${e}秒`].filter(Boolean).join(" ")},G=t=>t?t>=1024**3?(t/1024**3).toFixed(1)+"G":t>=1024**2?(t/1024**2).toFixed(1)+"M":Math.round(t/1024)+"K":"-",H=$(()=>{const t=[{key:"API 地址",value:O.value},{key:"API 密钥",value:A.value||"未设置(/v1 免鉴权)"},{key:"供应商数量",value:String(D.providerCount)},{key:"模型总数",value:String(D.totalModelCount)}],e=T.value;return e&&t.push({key:"运行状态",value:e.status==="online"?"运行中":e.status==="errored"?"异常":"已停止"},{key:"PID",value:String(e.pid??"-")},{key:"运行时长",value:z(e)},{key:"内存占用",value:G(e.memory)}),t});let Y;const g=t=>t.toLocaleString(),J=t=>t==null||t<=0?"-":t>=1e3?(t/1e3).toFixed(2)+"s":Math.round(t)+"ms",F=async t=>{try{await navigator.clipboard.writeText(t),P("已复制到剪贴板","success")}catch{const e=document.createElement("textarea");e.value=t,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select();try{document.execCommand("copy"),P("已复制到剪贴板","success")}catch{P("复制失败","error")}document.body.removeChild(e)}};return ae(()=>{q(),I(),Y=window.setInterval(I,1e4)}),le(()=>window.clearInterval(Y)),(t,e)=>{const a=m("el-table-column"),i=m("el-table"),f=m("el-card"),h=m("el-option"),N=m("el-select"),Q=m("el-date-picker"),W=m("el-button"),X=m("el-tag"),Z=m("el-empty"),ee=oe("loading");return c(),w("div",ve,[o(f,{shadow:"never"},{header:s(()=>[...e[3]||(e[3]=[l("span",{class:"fw-600"},"服务信息",-1)])]),default:s(()=>[se((c(),S(i,{border:"",data:H.value,"show-header":!1,class:"full-width"},{default:s(()=>[o(a,{prop:"key",width:"160"}),o(a,null,{default:s(({row:n})=>[n.key==="API 地址"||n.key==="API 密钥"?(c(),w("span",{key:0,class:"copy-text",onClick:u=>F(n.value)},r(n.value),9,pe)):(c(),w("span",me,r(n.value),1))]),_:1})]),_:1},8,["data"])),[[ee,b.value]])]),_:1}),d.value?(c(),S(f,{key:0,shadow:"never"},{header:s(()=>[l("div",_e,[e[5]||(e[5]=l("span",{class:"fw-600"},"用量统计",-1)),l("div",ge,[o(N,{modelValue:x.value,"onUpdate:modelValue":e[0]||(e[0]=n=>x.value=n),placeholder:"全部模型",clearable:"",filterable:"",style:{width:"220px"},onChange:y},{default:s(()=>[(c(!0),w(ne,null,ue(j.value,n=>(c(),S(h,{key:n,label:n,value:n},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),o(N,{modelValue:k.value,"onUpdate:modelValue":e[1]||(e[1]=n=>k.value=n),placeholder:"时间范围",clearable:"",style:{width:"130px"},onChange:K},{default:s(()=>[o(h,{label:"今天",value:"today"}),o(h,{label:"昨天",value:"yesterday"}),o(h,{label:"前天",value:"beforeYesterday"}),o(h,{label:"近三天",value:"last3days"}),o(h,{label:"本周",value:"week"}),o(h,{label:"本月",value:"month"})]),_:1},8,["modelValue"]),o(Q,{modelValue:v.value,"onUpdate:modelValue":e[2]||(e[2]=n=>v.value=n),type:"daterange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期","value-format":"YYYY-MM-DD",style:{width:"260px"},onChange:L},null,8,["modelValue"]),o(W,{loading:b.value,onClick:y},{default:s(()=>[...e[4]||(e[4]=[_("刷新",-1)])]),_:1},8,["loading"])]),o(X,{type:"info"},{default:s(()=>[_(r(d.value.total.calls)+" 次调用",1)]),_:1})])]),default:s(()=>{var n;return[l("div",fe,[l("div",he,[l("div",ye,r(g(d.value.total.total_tokens)),1),e[6]||(e[6]=l("div",{class:"usage-label"},"总 Token",-1))]),l("div",we,[l("div",be,r(g(d.value.total.input_tokens)),1),e[7]||(e[7]=l("div",{class:"usage-label"},"输入 Token",-1))]),l("div",ke,[l("div",De,r(g(d.value.total.output_tokens)),1),e[8]||(e[8]=l("div",{class:"usage-label"},"输出 Token",-1))]),l("div",xe,[l("div",Ce,r(g(d.value.total.cache_read_tokens)),1),e[9]||(e[9]=l("div",{class:"usage-label"},"缓存读取",-1))]),l("div",Me,[l("div",Se,r(d.value.total.cache_rate!=null?d.value.total.cache_rate+"%":"-"),1),e[10]||(e[10]=l("div",{class:"usage-label"},"缓存命中率",-1))]),d.value.logStorageMB!=null?(c(),w("div",Ve,[l("div",Pe,r(d.value.logStorageMB),1),e[11]||(e[11]=l("div",{class:"usage-label"},"日志占用 (MB)",-1))])):U("",!0)]),(n=d.value.models)!=null&&n.length?(c(),w("div",$e,[e[12]||(e[12]=l("div",{class:"section-title"},"模型用量",-1)),o(i,{data:d.value.models,border:"",stripe:"",class:"full-width"},{default:s(()=>[o(a,{prop:"model",label:"模型","min-width":"200"},{default:s(({row:u})=>[l("span",{class:"copy-text",onClick:Ie=>F(u.model)},r(u.model),9,Te)]),_:1}),o(a,{label:"调用次数",width:"90",align:"center"},{default:s(({row:u})=>[_(r(u.calls),1)]),_:1}),o(a,{label:"平均耗时",width:"110",align:"center"},{default:s(({row:u})=>[_(r(J(u.avg_duration)),1)]),_:1}),o(a,{label:"总 Token",width:"140",align:"center"},{default:s(({row:u})=>[_(r(g(u.total_tokens)),1)]),_:1}),o(a,{label:"输入 Token",width:"140",align:"center"},{default:s(({row:u})=>[_(r(g(u.input_tokens)),1)]),_:1}),o(a,{label:"输出 Token",width:"140",align:"center"},{default:s(({row:u})=>[_(r(g(u.output_tokens)),1)]),_:1}),o(a,{label:"缓存读取",width:"130",align:"center"},{default:s(({row:u})=>[_(r(g(u.cache_read_tokens)),1)]),_:1}),o(a,{label:"缓存命中率",width:"110",align:"center"},{default:s(({row:u})=>[_(r(u.cache_rate!=null?u.cache_rate+"%":"-"),1)]),_:1})]),_:1},8,["data"])])):(c(),S(Z,{key:1,description:"暂无调用记录","image-size":60}))]}),_:1})):U("",!0)])}}}),Ke=re(Ae,[["__scopeId","data-v-ff795a0a"]]);export{Ke as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{d as T,u as R,c as k,w as o,r as g,a as r,o as v,b as z,e as B,F,f as c,g as l,h as u,t as $,i as x,E as I,j as L,_ as W,k as q,l as j,m as A,n as O,p as G,s as H}from"./index-
|
|
1
|
+
import{d as T,u as R,c as k,w as o,r as g,a as r,o as v,b as z,e as B,F,f as c,g as l,h as u,t as $,i as x,E as I,j as L,_ as W,k as q,l as j,m as A,n as O,p as G,s as H}from"./index-DzBTME5Z.js";import{m as D,w as J}from"./useManagerApi-C3X3JX66.js";import{u as K}from"./useToast-BIVFyGJl.js";const P={class:"check-body"},Q={class:"version-row"},X={class:"version-row"},Y=T({__name:"UpdateCheckDialog",props:{modelValue:{type:Boolean,default:!1},modelModifiers:{}},emits:["update:modelValue"],setup(S){const{toast:w}=K(),y=R(S,"modelValue"),p=g(!1),m=g(!1),a=g(null),f=g(null),i=g(null),h=async()=>{a.value=null,f.value=null,i.value=null,p.value=!0;try{a.value=await D.checkUpdate()}catch(e){f.value=e.message||"检查更新失败"}finally{p.value=!1}},n=async()=>{var e;if(a.value){try{await I.confirm(`确定更新到 ${a.value.latestVersion}?服务将自动重启并在启动时安装新版本(期间界面请求会短暂失败,属正常现象)。`,"更新确认",{confirmButtonText:"立即更新",cancelButtonText:"取消",type:"warning"})}catch{return}m.value=!0;try{const t=await D.update(),d=await D.applyUpdate();if(!d.updated){i.value=`已是最新版本 ${d.toVersion}`,a.value={hasUpdate:!1,currentVersion:d.toVersion,latestVersion:d.toVersion,checkedAt:Date.now()};return}i.value=`正在重启服务并安装 ${d.toVersion},请稍候(预计 1~3 分钟)...`;const s=await J({targetVersion:d.toVersion,sinceAppliedAt:((e=t.lastApply)==null?void 0:e.appliedAt)??null});s&&!s.error?(w(`已更新到 ${s.toVersion}`,"success"),i.value=`已更新 ${s.fromVersion} → ${s.toVersion}`,a.value={hasUpdate:!1,currentVersion:s.toVersion,latestVersion:s.toVersion,checkedAt:Date.now()}):s!=null&&s.error?(w(`更新失败:${s.error}`,"error"),i.value=`更新失败:${s.error}(服务已恢复旧版本)`):i.value="更新耗时较长,请稍后刷新页面查看结果"}catch(t){w(t.message||"更新失败","error")}finally{m.value=!1}}};return(e,t)=>{const d=r("el-tag"),s=r("el-alert"),V=r("el-button"),U=r("el-empty"),b=r("el-dialog"),C=L("loading");return v(),k(b,{modelValue:y.value,"onUpdate:modelValue":t[2]||(t[2]=_=>y.value=_),title:"检查更新",width:"420px","close-on-click-modal":!m.value,onOpen:h},{footer:o(()=>{var _;return[(_=a.value)!=null&&_.hasUpdate?(v(),k(V,{key:0,type:"primary",loading:m.value,onClick:n},{default:o(()=>[u($(m.value?"更新中...":"立即更新"),1)]),_:1},8,["loading"])):x("",!0),l(V,{onClick:t[1]||(t[1]=M=>y.value=!1)},{default:o(()=>[...t[6]||(t[6]=[u("关闭",-1)])]),_:1})]}),default:o(()=>[z((v(),B("div",P,[a.value?(v(),B(F,{key:0},[c("div",Q,[t[3]||(t[3]=c("span",{class:"version-label"},"当前版本",-1)),l(d,{size:"small"},{default:o(()=>[u($(a.value.currentVersion),1)]),_:1})]),c("div",X,[t[4]||(t[4]=c("span",{class:"version-label"},"最新版本",-1)),l(d,{type:a.value.hasUpdate?"warning":"success",size:"small","disable-transitions":""},{default:o(()=>[u($(a.value.latestVersion),1)]),_:1},8,["type"])]),l(s,{title:a.value.hasUpdate?`发现新版本 ${a.value.latestVersion}`:"已是最新版本",type:a.value.hasUpdate?"warning":"success","show-icon":"",closable:!1,class:"mt-12"},null,8,["title","type"])],64)):!p.value&&f.value?(v(),k(U,{key:1,description:f.value},{default:o(()=>[l(V,{type:"primary",onClick:h},{default:o(()=>[...t[5]||(t[5]=[u("重试",-1)])]),_:1})]),_:1},8,["description"])):x("",!0)])),[[C,p.value]]),i.value?(v(),k(s,{key:0,title:i.value,type:"info","show-icon":"",class:"mt-12",onClose:t[0]||(t[0]=_=>i.value=null)},null,8,["title"])):x("",!0)]),_:1},8,["modelValue","close-on-click-modal"])}}}),Z=W(Y,[["__scopeId","data-v-eaed444e"]]),ee={class:"layout"},te={class:"header-nav"},oe={class:"header-actions"},le={class:"main"},se={class:"page"},ue=T({__name:"LayoutView",setup(S){const w=G(),y=O(),p=g(!1);q(()=>{j()});const m=A(()=>{var n;return((n=H.value)==null?void 0:n.authRequired)!==!1}),a=A(()=>{if(!m.value)return"免登录";const n=localStorage.getItem("webui_token")||"";try{return atob(n).split(":")[0]||"用户"}catch{return"用户"}}),f=A(()=>{const n=w.path;return n==="/"?"/":n.startsWith("/providers")?"/providers":n.startsWith("/settings")?"/settings":n.startsWith("/logs")?"/logs":"/"}),i=()=>{localStorage.removeItem("webui_token"),y.push("/login")},h=n=>{n==="logout"&&i(),n==="check-update"&&(p.value=!0)};return(n,e)=>{const t=r("el-menu-item"),d=r("el-menu"),s=r("ArrowDown"),V=r("el-icon"),U=r("el-button"),b=r("el-dropdown-item"),C=r("el-dropdown-menu"),_=r("el-dropdown"),M=r("el-header"),E=r("router-view");return v(),B("div",ee,[l(M,{class:"header"},{default:o(()=>[e[7]||(e[7]=c("h1",null,[c("span",null,"E"),u("LLM")],-1)),c("nav",te,[l(d,{mode:"horizontal","default-active":f.value,ellipsis:!1,router:"",class:"header-menu"},{default:o(()=>[l(t,{index:"/"},{default:o(()=>[...e[1]||(e[1]=[u("首页",-1)])]),_:1}),l(t,{index:"/providers"},{default:o(()=>[...e[2]||(e[2]=[u("供应商管理",-1)])]),_:1}),l(t,{index:"/logs"},{default:o(()=>[...e[3]||(e[3]=[u("日志",-1)])]),_:1}),l(t,{index:"/settings"},{default:o(()=>[...e[4]||(e[4]=[u("设置",-1)])]),_:1})]),_:1},8,["default-active"])]),c("div",oe,[l(_,{trigger:"click",onCommand:h,class:"ml-12"},{dropdown:o(()=>[l(C,null,{default:o(()=>[l(b,{command:"check-update"},{default:o(()=>[...e[5]||(e[5]=[u("检查更新",-1)])]),_:1}),m.value?(v(),k(b,{key:0,command:"logout",divided:""},{default:o(()=>[...e[6]||(e[6]=[u("退出",-1)])]),_:1})):x("",!0)]),_:1})]),default:o(()=>[l(U,null,{default:o(()=>[u($(a.value)+" ",1),l(V,{class:"el-icon--right"},{default:o(()=>[l(s)]),_:1})]),_:1})]),_:1})])]),_:1}),c("main",le,[c("div",se,[l(E)])]),l(Z,{modelValue:p.value,"onUpdate:modelValue":e[0]||(e[0]=N=>p.value=N)},null,8,["modelValue"])])}}});export{ue as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{d as h,e as b,g as o,w as l,a,o as _,f as k,a0 as x,a6 as f,c as C,i as L,h as B,r as i,A as N,n as I,_ as K}from"./index-
|
|
1
|
+
import{d as h,e as b,g as o,w as l,a,o as _,f as k,a0 as x,a6 as f,c as C,i as L,h as B,r as i,A as N,n as I,_ as K}from"./index-DzBTME5Z.js";const A={class:"login-wrap"},E=h({__name:"LoginView",setup(M){const v=I(),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 N.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 p=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(p,{modelValue:n.value,"onUpdate:modelValue":e[0]||(e[0]=c=>n.value=c),placeholder:"请输入用户名",onKeyup:f(r,["enter"])},null,8,["modelValue"])]),_:1}),o(m,{label:"密码"},{default:l(()=>[o(p,{modelValue:s.value,"onUpdate:modelValue":e[1]||(e[1]=c=>s.value=c),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})])}}}),U=K(E,[["__scopeId","data-v-c944978e"]]);export{U as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{d as G,a as b,j as H,o as _,c as q,w as a,e as g,g as e,h as i,t as u,a2 as m,b as Q,F as P,f as d,i as j,r as y,A,_ as X,k as pe,v as ce,a6 as _e,m as ve}from"./index-
|
|
1
|
+
import{d as G,a as b,j as H,o as _,c as q,w as a,e as g,g as e,h as i,t as u,a2 as m,b as Q,F as P,f as d,i as j,r as y,A,_ as X,k as pe,v as ce,a6 as _e,m as ve}from"./index-DzBTME5Z.js";import{u as me}from"./providers-DN-6AbdJ.js";import{u as Z}from"./useToast-BIVFyGJl.js";import"./config-CFGI6BQU.js";async function ee(p){try{if(navigator.clipboard&&window.isSecureContext)return await navigator.clipboard.writeText(p),!0}catch{}try{const f=document.createElement("textarea");f.value=p,f.style.position="fixed",f.style.opacity="0",document.body.appendChild(f),f.select();const k=document.execCommand("copy");return document.body.removeChild(f),k}catch{return!1}}const te=p=>{if(!p)return"-";const f=new Date(p),k=w=>String(w).padStart(2,"0");return`${f.getFullYear()}-${k(f.getMonth()+1)}-${k(f.getDate())} ${k(f.getHours())}:${k(f.getMinutes())}:${k(f.getSeconds())}`},V=p=>p?typeof p=="string"?p:JSON.stringify(p,null,2):"-",fe=p=>{if(!p)return"-";try{return JSON.stringify(JSON.parse(p),null,2)}catch{return p}},T=p=>p.toLocaleString(),le=p=>p==null||p<=0?"-":p>=1e3?(p/1e3).toFixed(2)+"s":Math.round(p)+"ms",ae=p=>{const f=p.input_tokens||0,k=p.cache_read_tokens||0;return f<=0?"-":(k/f*100).toFixed(1)+"%"},be={key:0,class:"detail-content"},ge={key:0,class:"ip-link"},ye={class:"ip-pop"},ke={class:"ip-pop-row"},he={key:0,class:"ip-pop-row"},xe={key:1,class:"ip-pop-error"},we={key:2},De=["href"],Ce={key:1},$e={key:1},Ve={class:"error-text"},Se={class:"pane-bar"},Ie={class:"json-block"},qe={class:"pane-bar"},ze={class:"json-block"},Re={class:"pane-bar"},Ue={class:"json-block"},je={class:"pane-bar"},Te={class:"json-block"},Ne={class:"pane-bar"},Le={class:"json-block"},Me={class:"pane-bar"},Ye={class:"json-block"},Ee={class:"pane-bar"},Pe={class:"json-block"},Fe={class:"pane-bar"},Be={class:"json-block"},Je=G({__name:"LogDetailDialog",setup(p,{expose:f}){const{toast:k}=Z(),w=y(!1),t=y(null),S=y("info"),I=y(!1);f({open:async v=>{w.value=!0,t.value=null,S.value="info";try{const l=v.id?`?id=${v.id}`:`?_file=${encodeURIComponent(v._file)}`,c=await A.get("/api/logs/detail"+l);c&&c.detail?t.value={...c.detail,...c.meta}:t.value=c}catch(l){console.error(l)}}});const h=async v=>{await ee(v)?k("已复制到剪贴板","success"):k("复制失败","error")},N=async v=>{if(!(v._ipInfo||v._ipInfoError)){I.value=!0;try{const l=await A.get(`/api/logs/ipinfo?ip=${encodeURIComponent(v.ip)}`);l.error?v._ipInfoError=l.error:v._ipInfo=l}catch{v._ipInfoError="查询失败"}finally{I.value=!1}}},M=v=>[v.country,v.region,v.city].filter(Boolean).join(" · ")||"-";return(v,l)=>{const c=b("el-descriptions-item"),Y=b("el-tag"),F=b("el-divider"),B=b("el-popover"),J=b("el-descriptions"),E=b("el-alert"),x=b("el-tab-pane"),D=b("el-button"),K=b("el-tabs"),L=b("el-dialog"),C=H("loading");return _(),q(L,{modelValue:w.value,"onUpdate:modelValue":l[11]||(l[11]=z=>w.value=z),title:"日志详情",width:"80%",top:"5vh","destroy-on-close":""},{default:a(()=>[t.value?(_(),g("div",be,[e(K,{modelValue:S.value,"onUpdate:modelValue":l[10]||(l[10]=z=>S.value=z)},{default:a(()=>{var z,r,s;return[e(x,{label:"基本信息",name:"info"},{default:a(()=>{var n;return[e(J,{column:2,border:""},{default:a(()=>[e(c,{label:"时间"},{default:a(()=>[i(u(m(te)(t.value.time)),1)]),_:1}),e(c,{label:"模型"},{default:a(()=>[i(u(t.value.model),1)]),_:1}),e(c,{label:"方法"},{default:a(()=>[i(u(t.value.method),1)]),_:1}),e(c,{label:"路径"},{default:a(()=>[i(u(t.value.path),1)]),_:1}),e(c,{label:"状态码"},{default:a(()=>[i(u(t.value.status),1)]),_:1}),e(c,{label:"流式"},{default:a(()=>[e(Y,{type:t.value.stream?"warning":"info"},{default:a(()=>[i(u(t.value.stream?"是":"否"),1)]),_:1},8,["type"])]),_:1}),e(c,{label:"IP"},{default:a(()=>[t.value.ip?(_(),g("span",ge,[e(B,{placement:"right",width:"240",trigger:"click",onShow:l[0]||(l[0]=R=>N(t.value))},{reference:a(()=>[d("span",null,u(t.value.ip),1)]),default:a(()=>[Q((_(),g("div",ye,[t.value._ipInfo?(_(),g(P,{key:0},[d("div",ke,[l[12]||(l[12]=d("span",{class:"ip-pop-label"},"归属地",-1)),d("span",null,u(M(t.value._ipInfo)),1)]),t.value._ipInfo.isp?(_(),g("div",he,[l[13]||(l[13]=d("span",{class:"ip-pop-label"},"运营商",-1)),d("span",null,u(t.value._ipInfo.isp),1)])):j("",!0)],64)):t.value._ipInfoError?(_(),g("div",xe,u(t.value._ipInfoError),1)):(_(),g("span",we,"查询中...")),e(F,{style:{margin:"8px 0"}}),d("a",{class:"ip-pop-link",href:`https://m.ip138.com/iplookup.php?ip=${encodeURIComponent(t.value.ip)}`,target:"_blank"},"在 ip138 查看",8,De)])),[[C,I.value]])]),_:1})])):(_(),g("span",Ce,"-"))]),_:1}),e(c,{label:"Session"},{default:a(()=>[i(u(t.value.session_id||"-"),1)]),_:1}),e(c,{label:"Request ID"},{default:a(()=>[t.value.request_id?(_(),g("span",{key:0,class:"request-id",title:"点击复制(可用于关联控制台日志与检索)",onClick:l[1]||(l[1]=R=>h(t.value.request_id))},u(t.value.request_id),1)):(_(),g("span",$e,"-"))]),_:1}),e(c,{label:"输入 Token(含缓存读取)"},{default:a(()=>[i(u(m(T)(t.value.input_tokens??0)),1)]),_:1}),e(c,{label:"输出 Token"},{default:a(()=>[i(u(m(T)(t.value.output_tokens??0)),1)]),_:1}),e(c,{label:"缓存读取"},{default:a(()=>[i(u(m(T)(t.value.cache_read_tokens??0)),1)]),_:1}),e(c,{label:"请求耗时"},{default:a(()=>[i(u(m(le)(t.value.duration)),1)]),_:1}),e(c,{label:"缓存命中率"},{default:a(()=>[i(u(m(ae)(t.value)),1)]),_:1}),t.value.error?(_(),q(c,{key:0,label:"错误",span:2},{default:a(()=>[d("span",Ve,u(t.value.error),1)]),_:1})):j("",!0)]),_:1}),(n=t.value.response)!=null&&n.truncated?(_(),q(E,{key:0,class:"mt-16",type:"warning",closable:!1,title:"内容超过单请求捕获上限,已截断"})):j("",!0)]}),_:1}),(z=t.value.response)!=null&&z._capture?(_(),g(P,{key:0},[e(x,{label:"客户端请求",name:"inbound"},{default:a(()=>[d("div",Se,[e(D,{size:"small",class:"copy-btn",onClick:l[2]||(l[2]=n=>h(m(V)(t.value.response.inbound_request)))},{default:a(()=>[...l[14]||(l[14]=[i(" 复制 ",-1)])]),_:1})]),d("pre",Ie,u(m(V)(t.value.response.inbound_request)),1)]),_:1}),e(x,{label:"转发上游的请求",name:"upstreamReq"},{default:a(()=>[d("div",qe,[e(D,{size:"small",class:"copy-btn",onClick:l[3]||(l[3]=n=>h(m(V)(t.value.response.upstream_request)))},{default:a(()=>[...l[15]||(l[15]=[i(" 复制 ",-1)])]),_:1})]),d("pre",ze,u(m(V)(t.value.response.upstream_request)),1)]),_:1}),t.value.response.upstream_response_text?(_(),q(x,{key:0,label:"上游响应(原始)",name:"upstreamRes"},{default:a(()=>[d("div",Re,[e(D,{size:"small",class:"copy-btn",onClick:l[4]||(l[4]=n=>h(t.value.response.upstream_response_text))},{default:a(()=>[...l[16]||(l[16]=[i(" 复制 ",-1)])]),_:1})]),d("pre",Ue,u(m(fe)(t.value.response.upstream_response_text)),1)]),_:1})):(r=t.value.response.upstream_stream_frames)!=null&&r.length?(_(),q(x,{key:1,label:`上游响应(原始流,${t.value.response.upstream_stream_frames.length} 帧)`,name:"upstreamStream"},{default:a(()=>[d("div",je,[e(D,{size:"small",class:"copy-btn",onClick:l[5]||(l[5]=n=>h(t.value.response.upstream_stream_frames.join(`
|
|
2
2
|
|
|
3
3
|
`)))},{default:a(()=>[...l[17]||(l[17]=[i(" 复制 ",-1)])]),_:1})]),d("pre",Te,u(t.value.response.upstream_stream_frames.join(`
|
|
4
4
|
|