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 CHANGED
@@ -1,13 +1,24 @@
1
1
  # ellm-proxy
2
2
 
3
- 自研 LLM 网关(行为基准 litellm 1.100.0),单进程后台常驻一键启停(跨平台,内置进程管理,无 pm2)。单端口提供全部能力:网关、管理台 API 与管理界面同端口(默认 29770)。
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 -h # 查看帮助
23
- ellm-proxy start # 后台常驻启动(首次自动创建配置)
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
- **进程管理(内置,跨平台)**:CLI 启动时以 detached 方式拉起服务进程并写运行记录 `~/.ellm-proxy/run.json`,探活以「端口 HTTP 可达」为准。服务进程自持崩溃自愈(未捕获异常自动孵化后继进程接管端口,1 分钟窗口封顶 10 次,防端口占用无限循环),日志写 `<appDir>/logs/`。服务要求的 Node ≥22.13 由 CLI 解析(当前 node 满足则直接用,否则经 get-node 自动下载一次并缓存)。Windows、Linux、macOS 均可用;运行状态直接运行 `ellm-proxy`(无参数)查看。
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
- **热重启**:管理台「进程管理」页的重启按钮(或配置变更)触发 self-respawn——服务先孵化新进程接管端口再退出,停机 1~2 秒。
129
+ **热重启**:配置变更或更新时触发 self-respawn——服务先孵化新进程接管端口再退出,停机 1~2 秒。
37
130
 
38
- **开机自启(跨平台)**:`ellm-proxy autostart on` 注册登录触发的自启项(内容为直接启动服务,含运行参数 env 快照),三平台均无需管理员权限——Windows 写启动文件夹 `.cmd`、macOS 写 LaunchAgent(`launchctl load`)、Linux 写 systemd user unit(`systemctl --user enable`,WSL/无 systemd 环境会报错提示);`autostart off` 移除。旧版(≤0.0.7,pm2 resurrect 内容)自启项会在下次 start 时自动迁移重写,旧版 pm2 托管的进程与守护也会被自动清理。
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
- - **手动更新**:管理台「设置 → 服务与更新」检查并一键更新——服务先回响应再自重启,新进程启动时执行 `npm i -g` 安装并自动换新代码生效(双跳),安装失败自动回滚旧版本继续服务。
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
- 首次执行 `ellm-proxy start` 会自动创建示例配置,之后两种方式修改:
152
+ 其他环境变量:`ELLM_PORT`(服务监听端口,或 `start --port`)、`HOST`(监听地址)。日志保留策略:配置 `logs.detail-retention-days` / `logs.row-retention-days`(详情/摘要分层保留,0 = 永久),默认启动后 30 秒首跑清理、之后每 6 小时一次。
60
153
 
61
- 1. 直接编辑配置文件;
62
- 2. 启动服务后访问管理界面 `http://127.0.0.1:29770/` 在线修改(实时热更新)。
154
+ ## 从旧版本升级
63
155
 
64
- 其他环境变量:`ELLM_PORT`(服务监听端口,或 `start --port`)、`HOST`(监听地址)、`ELLM_MASTER_KEY`(网关主密钥,低于配置文件 `masterKey` 字段)。启动时端口若被占用会自动顺延到下一个空闲端口(仅本次运行生效,不改配置文件),顺延结果会打印在启动摘要中。
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 的发布载体:发布版 `dist/server.js` 与 `dist/cli.js` 由 `scripts/build-server.mjs` / `build-cli.mjs` 从 `server`(原 backend)源码直接构建(esbuild 全量内联,含管理域与 CLI),`web-ui` 产物由 `pnpm build:web` 生成后拷入 `dist/web-ui/`(服务进程托管):
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 { existsSync as existsSync3, readFileSync as readFileSync6, readdirSync } from "node:fs";
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
- "<!-- ellm-proxy-v2: launch service directly (no pm2) -->",
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
- "[Unit]",
35267
- "Description=ellm-proxy autostart (ellm-proxy-v2, direct service start)",
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 startupDir = windowsStartupDir();
35322
- const vbsPath = join6(startupDir, AUTOSTART_VBS);
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
- if (readTextDetectingBom(itemPath).includes(MARKER)) enabled = true;
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(status.outdated ? `\u5DF2\u66F4\u65B0\u5F00\u673A\u81EA\u542F\u9879: ${itemPath}` : `\u5DF2\u81EA\u52A8\u6CE8\u518C\u5F00\u673A\u81EA\u542F: ${itemPath}`);
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
- "<!-- ellm-proxy-v2: launch service directly (no pm2) -->",
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
- "[Unit]",
20686
- "Description=ellm-proxy autostart (ellm-proxy-v2, direct service start)",
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 startupDir = windowsStartupDir();
20741
- const vbsPath = join5(startupDir, AUTOSTART_VBS);
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
- if (readTextDetectingBom(itemPath).includes(MARKER)) enabled = true;
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-BmtcBnoW.js";import{m as de}from"./useManagerApi-PldAaNYo.js";import{u as ie}from"./providers-Aw7T7j0r.js";import{u as ce}from"./useToast-C9BTyaBD.js";import"./config-CHaFDQC7.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
+ 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-BmtcBnoW.js";import{m as D,w as J}from"./useManagerApi-PldAaNYo.js";import{u as K}from"./useToast-C9BTyaBD.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
+ 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-BmtcBnoW.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
+ 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-BmtcBnoW.js";import{u as me}from"./providers-Aw7T7j0r.js";import{u as Z}from"./useToast-C9BTyaBD.js";import"./config-CHaFDQC7.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(`
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