ezllm 0.0.1

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/bin/ellm-proxy.js +3 -0
  4. package/dist/cli.js +27667 -0
  5. package/dist/package.json +1 -0
  6. package/dist/server.js +42403 -0
  7. package/dist/web-ui/assets/DashboardView-DwlQExQv.css +1 -0
  8. package/dist/web-ui/assets/DashboardView-fRDkGq0g.js +1 -0
  9. package/dist/web-ui/assets/LayoutView-CK_MaWN7.js +1 -0
  10. package/dist/web-ui/assets/LayoutView-kZ2yGen8.css +1 -0
  11. package/dist/web-ui/assets/LoginView-B1naaT8E.js +1 -0
  12. package/dist/web-ui/assets/LoginView-CSfb59pE.css +1 -0
  13. package/dist/web-ui/assets/LogsView-CCjJTjvZ.css +1 -0
  14. package/dist/web-ui/assets/LogsView-YtH8gJPU.js +9 -0
  15. package/dist/web-ui/assets/ProvidersView-Caqpg_0F.css +1 -0
  16. package/dist/web-ui/assets/ProvidersView-DWtS_e7t.js +31 -0
  17. package/dist/web-ui/assets/ServiceStatusView-D3ybS2T-.js +39 -0
  18. package/dist/web-ui/assets/ServiceStatusView-DrPoYMN-.css +1 -0
  19. package/dist/web-ui/assets/SettingsView-D1kbH5Ev.css +1 -0
  20. package/dist/web-ui/assets/SettingsView-DepOaZYg.js +1 -0
  21. package/dist/web-ui/assets/SetupView-BwDwokAd.js +1 -0
  22. package/dist/web-ui/assets/SetupView-pS9nSnLg.css +1 -0
  23. package/dist/web-ui/assets/clipboard-aCBIMfw7.js +1 -0
  24. package/dist/web-ui/assets/config-CdOq1p3C.js +1 -0
  25. package/dist/web-ui/assets/format-kTdM4RVv.js +1 -0
  26. package/dist/web-ui/assets/index-BecfPmag.css +1 -0
  27. package/dist/web-ui/assets/index-CgzCvxTY.js +96 -0
  28. package/dist/web-ui/assets/providers-i7t_wEzk.js +1 -0
  29. package/dist/web-ui/assets/useLoadError-UA3r7sR9.js +1 -0
  30. package/dist/web-ui/assets/useManagerApi-CBQNomtR.js +1 -0
  31. package/dist/web-ui/assets/useToast-DNMhOOeI.js +1 -0
  32. package/dist/web-ui/assets/version-BKHjE_LF.js +1 -0
  33. package/dist/web-ui/config.js +7 -0
  34. package/dist/web-ui/index.html +15 -0
  35. package/docs/DESIGN.md +90 -0
  36. package/ezllm.config.example.json +33 -0
  37. package/package.json +55 -0
@@ -0,0 +1 @@
1
+ import{a1 as w,r as u,p as i}from"./index-CgzCvxTY.js";import{u as C}from"./config-CdOq1p3C.js";const J=w("providers",()=>{const s=C(),c=u(!1),a=i(()=>s.providers),v=i(()=>s.loading),f=i(()=>a.value.length),m=i(()=>a.value.reduce((e,o)=>{var r;return e+(((r=o.models)==null?void 0:r.length)??0)},0)),t=u([]);function p(){try{const e=localStorage.getItem("provider_order");e&&(t.value=JSON.parse(e))}catch{}}function d(){try{localStorage.setItem("provider_order",JSON.stringify(t.value))}catch{}}const g=i(()=>{const e=t.value.map(r=>a.value.find(n=>n.name===r)).filter(r=>!!r);for(const r of a.value)e.find(n=>n.name===r.name)||e.push(r);const o=e.map(r=>r.name);return(o.length!==t.value.length||o.some((r,n)=>t.value[n]!==r))&&(t.value=o,d()),e});function S(e){t.value=e.map(o=>o.name),d()}function h(e){t.value=[e,...t.value.filter(o=>o!==e)],d()}async function y(){p(),await s.load()}async function l(e){return s.config.providers=e,await s.save()}async function P(e){const o=a.value.filter(r=>r.name!==e.name);return await l(o)}async function O(e){const o=a.value.map(r=>({...r,models:(r.models??[]).filter(n=>n.modelName!==e)}));return await l(o)}return{suppliers:a,loading:v,restarting:c,orderedProviders:g,providerCount:f,totalModelCount:m,loadProviders:y,saveProviders:l,deleteProvider:P,deleteModel:O,reorderProviders:S,moveProviderToTop:h}});export{J as u};
@@ -0,0 +1 @@
1
+ import{r as e}from"./index-CgzCvxTY.js";function l(a="加载失败,请稍后重试"){const r=e(null);async function n(t){r.value=null;try{return await t(),!0}catch(u){return r.value=(u==null?void 0:u.message)||a,!1}}return{loadError:r,run:n}}export{l as u};
@@ -0,0 +1 @@
1
+ import{ag as e}from"./index-CgzCvxTY.js";const c={status:()=>e.get("/status"),processes:()=>e.get("/processes").then(t=>t.processes),autostart:()=>e.get("/autostart"),setAutostart:t=>e.post("/autostart",{enabled:t}),update:()=>e.get("/update"),setUpdateEnabled:t=>e.post("/update/enabled",{enabled:t}),checkUpdate:()=>e.post("/update/check"),applyUpdate:()=>e.post("/update/apply"),harness:()=>e.get("/harness"),setClaudeCode:t=>e.post("/harness/claude-code",{enabled:t}),setCodex:t=>e.post("/harness/codex",{enabled:t}),setDsh:t=>e.post("/harness/dsh",{enabled:t})};async function h(t){const{targetVersion:r,sinceAppliedAt:n,timeoutMs:u=600*1e3,pollMs:d=3e3,signal:s}=t,o=Date.now()+u;for(;;){if(s!=null&&s.aborted||Date.now()>=o||(await new Promise(l=>setTimeout(l,d)),s!=null&&s.aborted))return null;let p=null;try{p=await c.update()}catch{continue}const a=p.lastApply;if(a&&a.toVersion===r&&(a.appliedAt??0)>(n??0))return a;if(Date.now()>=o)return null}}export{c as m,h as w};
@@ -0,0 +1 @@
1
+ import{ah as o}from"./index-CgzCvxTY.js";function e(){function a(n,s="success"){o({message:n,type:s,duration:s==="warning"?8e3:3e3})}return{toast:a}}export{e as u};
@@ -0,0 +1 @@
1
+ function r(t){return`更新失败:${t}(更新未生效,服务仍在旧版本运行)`}function e(t){return typeof t=="string"&&/-dev\.\d+$/.test(t)}export{e as i,r as u};
@@ -0,0 +1,7 @@
1
+ // web-ui 运行时配置(部署后可直接改这个文件,无需重新构建)
2
+ // 标准形态:服务进程托管本目录(http://127.0.0.1:<port>/),API_BASE 留空即全同源,无需修改。
3
+ // API_BASE:管理台 API 与管理域接口(/api/*、/status、/harness…)的基地址。
4
+ // 留空 = 同源;仅当把 dist 部署到其他静态服务器时填写,如 "http://192.168.1.10:29770"。
5
+ window.__ELLM_CONFIG__ = {
6
+ API_BASE: "",
7
+ };
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>EZLLM - 网关管理台</title>
8
+ <script src="/config.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-CgzCvxTY.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-BecfPmag.css">
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ </body>
15
+ </html>
package/docs/DESIGN.md ADDED
@@ -0,0 +1,90 @@
1
+ # ellm-proxy 发布包设计文档
2
+
3
+ > 本文档写发布包的**当前**结构与行为约束(包布局、路径 env 化、启停形态、更新链路、维护须知)。
4
+ > 使用说明见 [README.md](../README.md)。
5
+
6
+ ## 1. 目标与形态
7
+
8
+ 把 monorepo(`app/cli` 发布包 / `app/web` 前端 / `packages/server` 服务 / `packages/gateway` 网关 / `packages/protocol` 协议层)里的 server 单进程应用,构建为可发布的 npm 包 **`ellm-proxy`**:
9
+
10
+ ```bash
11
+ npm i -g ellm-proxy
12
+ ellm-proxy start # 后台启动(隐藏窗口,首次自动创建配置)
13
+ ellm-proxy stop # 停止后台服务
14
+ ```
15
+
16
+ 发布形态是**完整单进程应用**(网关 `/v1/*` + 管理台 `/api/*` + 管理域 API + web-ui 静态托管),不是 SDK。包名与内部子包 `gateway` 的相似性已知并接受。
17
+
18
+ ## 2. 包结构与发布流程
19
+
20
+ ```
21
+ app/cli/
22
+ ├── bin/ellm-proxy.js # CLI shim(import dist/cli.js;npm bin 指向)
23
+ ├── dist/server.js # 服务本体(esbuild 从 server 源码构建 + shebang/createRequire banner)
24
+ ├── dist/cli.js # CLI(esbuild 从 packages/server/src/cli/bin.ts 构建,与 server 共享 proc/paths 原语)
25
+ ├── dist/web-ui/ # web-ui vite 构建产物(服务进程托管于 http://127.0.0.1:<port>/)
26
+ ├── scripts/build-server.ts | build-cli.ts # 发布构建(esbuild 双入口,tsx 执行)
27
+ ├── scripts/build-package.ts # 聚合 + 门禁(prepack:构建 server+cli → 拷 web-ui → 复制 example → shebang 校验)
28
+ ├── scripts/lib/release-utils.ts # 升级/发布脚本的纯函数层(版本号改写、前置校验、阶段参数、失败处置)
29
+ ├── scripts/lib/exec.ts # 命令执行底座(只有 Windows 的 npm/npx/pnpm 走 shell,git 直连以防参数被拆)
30
+ └── scripts/dev-upgrade.ts | release.ts # 本地一键升级 / 发布(质检与发布两个阶段共用 release.ts)
31
+ ```
32
+
33
+ 发布 manifest 要点:`bin=bin/ellm-proxy.js`;`files` 白名单(bin/、dist/、example、README、LICENSE、docs);**`dependencies` 为空**(全部逻辑 bundle 自包含),devDependencies 只放构建与测试工具;发布时用 `npm publish`(不改写 manifest,`prepack` 自动构建)。
34
+
35
+ 构建链:`pnpm build:all` = web-ui(vite)→ server bundle(esbuild,含 gateway/protocol 源码)→ 发布包聚合(双 bundle + web-ui + 示例配置 + shebang 门禁)。产物校验由 `build-package.ts` 承担,缺 web-ui/dist 或示例配置直接失败。
36
+
37
+ 发布链:本地分两步 —— `pnpm release:checks`(前置校验:工作区干净 + 有 remote + 不落后远端 → 质量闸 typecheck/test/format:check;只读不动 git)与 `pnpm release`(构建校验 → patch+1 → 提交 → 打 `v<版本>` tag → 推送)→ GitHub Actions 在 tag 上校验 tag 与 `app/cli/package.json` 版本一致后 `npm publish`(凭 `NPM_TOKEN`),真正的发版动作在 CI 侧。
38
+
39
+ ## 3. 路径全 env 化(核心设计)
40
+
41
+ 所有路径「env 优先、缺省公式兜底」,相对路径按当前工作目录展开,绝对路径原样:
42
+
43
+ | 配置 | 环境变量 | 缺省公式 | 包模式来源 |
44
+ | --- | --- | --- | --- |
45
+ | 应用目录(配置/数据基准) | `ELLM_APP_DIR` | `packages/server/`(hereDir 推导) | CLI 注入(默认 `~/.ezllm`) |
46
+ | 配置文件 | `ELLM_CONFIG` | `<APP_DIR>/ezllm.config.json` | CLI `--config` 或用户 env |
47
+ | 数据目录 | `ELLM_DATA_DIR` | `<APP_DIR>/data` | CLI `--data-dir` 或用户 env |
48
+ | web-ui 静态资源 | `ELLM_WEBUI_DIR`(可选覆盖) | 按模块位置推导:`dist/web-ui` 与 bundle 同级(包模式);`app/web/dist`(仓库模式) | 服务进程启动时自解析 |
49
+
50
+ 服务端不加载任何 `.env` / env 文件;环境变量一律由用户会话导出或 CLI 注入。约束与边界:
51
+
52
+ - **server 源码零模式分支**:`paths.ts` 只有「env ?? 公式」两种取值,不存在模式判断;仓库开发模式不设任何 env 即工作。
53
+ - **包模式必须经 CLI 启动**:绕过它裸跑 `dist/server.js` 时 env 未注入,会因找不到配置退出(报错信息指引)。官方入口始终是 `ellm-proxy start/stop`。
54
+ - **CLI 在转调前把路径参数统一转为绝对路径**:CLI 的 cwd 与 node 进程的工作目录不同,透传相对字符串会在另一基准下解析错位。
55
+ - **CLI 负责首次配置自愈**(server 端只读不建):`ensureConfig()` 查找链与 server 端一致,创建后提示编辑或经管理台修改。
56
+
57
+ ## 4. 启停与进程管理
58
+
59
+ - **start**:配置自愈 → 已运行检测(run.json + pid 存活)→ 端口预算(`--port` > `ELLM_PORT` > 配置 `port`;被占则顺延到空闲端口并提示,顺延仅本次运行生效)→ `spawnDetached` → 轮询 run.json + HTTP 探活(端口真实可服务才算就绪)→ 首启自启自愈。
60
+ - **stop**:按 run.json 的 pid 杀进程树并清记录。`restart` = stop + start(保证最新参数生效)。
61
+ - **进程管理原语**(`packages/server/src/proc/`):`run.json` 运行记录(pid/端口/startedAt/serverJs);`spawnDetached`(日志重定向到 `<appDir>/logs/`);`selfRespawn`(孵化后继进程后自身退出,`ELLM_RESPAWNED=1` 的后继对 EADDRINUSE 重试绑定而非顺延端口);`killTree`(Windows `taskkill /T /F`,POSIX 杀进程组);`waitHttpOnline`;`installCrashRecovery`(uncaughtException → respawn,1 分钟 10 次封顶)。
62
+ - **开机自启**(`ellm-proxy autostart on|off`,跨平台、登录触发、免管理员):Windows 写启动文件夹(`.vbs` 隐藏窗口入口 + appDir 内 `.cmd`,`chcp 65001` 统一日志编码)、macOS 写 LaunchAgent、Linux 写 systemd user unit;自启项内容含解释器路径与 `ELLM_*` env 快照,保证登录启动与手动启动同参。
63
+ - **解释器解析**(`proc/interpreter.ts` + get-node):服务依赖 `node:sqlite`(≥ 22.13),CLI 自身 node 过旧时解析/下载 node 22 再 spawn。
64
+ - **管理域不设生命周期入口**:启停统一走 CLI(管理台由服务本体托管,页面无法自举拉起已停止的服务);管理域只留状态查询与设置类接口,每条路由带登录鉴权。
65
+
66
+ ## 5. 更新链路
67
+
68
+ 自动更新采用「启动时生效 + self-respawn 双跳」:
69
+
70
+ 1. 检查(管理台请求惰性触发 + 定时检查每小时轮询,两通道共享互斥与墙钟节流;失败 10 分钟后可重试)发现新版本 → 写 `pendingUpdate` 标记 → respawn;
71
+ 2. 新进程启动引导读到标记 → `npm i -g ellm-proxy@<版本>`(此时服务未起,无文件句柄冲突)→ 清标记 → 再 respawn 一次换新代码;
72
+ 3. 安装失败:清标记、写 `lastApply` 错误,以旧版本继续启动。
73
+
74
+ 开关是配置文件顶层 `autoUpdate`(运行状态 `lastCheck`/`lastApply`/`pendingUpdate` 存 `<appDir>/updater.json`);源码运行(包根不在 `node_modules` 下)时更新域整体关闭,防 dev 会话误更新全局包;更新源可用 `ELLM_UPDATE_REGISTRY` 覆盖(默认 npm 官方)。停机窗口 = npm 安装时长。
75
+
76
+ ## 6. 跨包不变式(维护须知)
77
+
78
+ 1. **`packages/gateway/src/index.ts` 必须保持零顶层副作用**:server 经 esbuild 全量内联 gateway 源码,任何 import 副作用都会执行。独立网关装配在 `src/standalone-app.ts`。
79
+ 2. **`dist/web-ui/` 与 `dist/server.js` 同级**:`admin/webui-static.ts` 的缺省推导依赖此布局(`ELLM_WEBUI_DIR` 可覆盖)。
80
+ 3. **`ezllm.config.json` 是配置唯一数据源**(原样读写,勿加转换层)。
81
+ 4. **管理域 `gatewayBase` 每请求现算**:`ctx.port` 在 listen 后才回填,sub-app 创建时固化会得到 `:0`。
82
+ 5. **esbuild ESM bundle 的 CJS 依赖需 `createRequire` banner**(三个构建脚本统一):get-node 链路的 tmp-promise、pidusage 内部 `require("util")` 会抛 "Dynamic require"。
83
+
84
+ ## 7. 验证基线(发布前回归清单)
85
+
86
+ - `pnpm -r test:coverage` / `pnpm -r typecheck` / `pnpm format:check` / `pnpm check:knip` 全绿(覆盖率为各包 vitest.config.ts 的门槛)。
87
+ - CLI 冒烟:`-h`、`config`(首次创建)、`start`(参数透传 + env 注入 + 鉴权 200/401)、`stop`(树杀 + 端口释放)、`restart`(运行中/未运行两态)。
88
+ - 仓库产物模式(零 env)与包模式(CLI 注入)双模式启动均正常。
89
+ - 包目录零读写(`ls` 包目录无新增文件);`npm pack` 核对 tarball 无 `src/`/`test/`/真实配置。
90
+ - 实机升级:`pnpm upgrade:local` 走完 build + 安装 + 重启(`ELLM_NO_INSTALL=1` 可只验构建链路)。
@@ -0,0 +1,33 @@
1
+ {
2
+ "port": 29770,
3
+ "masterKey": "",
4
+ "setup": { "completed": false },
5
+ "autoUpdate": true,
6
+ "providers": [
7
+ {
8
+ "name": "my-openai",
9
+ "alias": "my",
10
+ "customLlmProvider": "openai",
11
+ "apiKey": "os.environ/MY_OPENAI_API_KEY",
12
+ "apiBase": "https://api.openai.com/v1",
13
+ "models": [
14
+ { "modelName": "my/gpt-4o-mini", "model": "gpt-4o-mini" }
15
+ ]
16
+ },
17
+ {
18
+ "name": "my-anthropic",
19
+ "customLlmProvider": "anthropic",
20
+ "apiKey": "os.environ/MY_ANTHROPIC_API_KEY",
21
+ "models": [
22
+ { "modelName": "my/claude-sonnet-4-5", "model": "claude-sonnet-4-5" },
23
+ {
24
+ "modelName": "my/mixed-model",
25
+ "model": "some-model",
26
+ "customLlmProvider": "openai",
27
+ "apiBase": "https://example.com/v1",
28
+ "apiKey": "os.environ/OTHER_KEY"
29
+ }
30
+ ]
31
+ }
32
+ ]
33
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "ezllm",
3
+ "version": "0.0.1",
4
+ "description": "Self-hosted LLM gateway with admin web UI (single process, built-in process management)",
5
+ "keywords": [
6
+ "llm",
7
+ "llm-proxy",
8
+ "llm-gateway",
9
+ "ai-gateway",
10
+ "api-proxy",
11
+ "anthropic",
12
+ "claude",
13
+ "claude-code",
14
+ "openai",
15
+ "deepseek",
16
+ "relay"
17
+ ],
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+ssh://git@github.com/doyzhengdev/ellm-proxy.git"
22
+ },
23
+ "type": "module",
24
+ "bin": {
25
+ "ezllm": "bin/ellm-proxy.js"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "dist",
30
+ "docs",
31
+ "ezllm.config.example.json",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsx scripts/build-package.ts",
37
+ "prepack": "tsx scripts/build-package.ts",
38
+ "release": "tsx scripts/release.ts",
39
+ "release:checks": "tsx scripts/release.ts --checks",
40
+ "upgrade:local": "tsx scripts/dev-upgrade.ts",
41
+ "typecheck": "npx nve 22 tsc --noEmit",
42
+ "test": "npx nve 22 vitest run",
43
+ "test:coverage": "npx nve 22 vitest run --coverage"
44
+ },
45
+ "publishConfig": {
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.20.1",
50
+ "@vitest/coverage-v8": "2.1.9",
51
+ "tsx": "^4.23.12",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^2.1.9"
54
+ }
55
+ }