You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature] 加一个 Web 可视化管理面板:无头 / 服务器 Docker 部署时也能看额度、切套餐、看日志 #58
src/tui/app.ts:249-259 的注释:"billing balance windows; manual refresh only — the billing gateway rate-limits frequent queries, so no polling timer",TUI 已经这么做了
Activity
TriDefender commentedon Sep 30, 2026
可以提个issue,但是需要注意非回环请求直接 403在VPS部署上可能会出问题,可以考虑ssh进去然后用tui?
cjjdaq commentedon Sep 30, 2026
Ma6302 commentedon Oct 1, 2026
感谢回复,也谢谢这个项目。
我这边的场景大概正好是你说的 VPS 情况,说明一下为什么 TUI 这条路走不通:
ghcr.io/tridefender/zcode-proxy:latest),容器 CMD 就是bun run src/index.ts serve,serve 模式没有 TUI 代码路径,容器也没分配 TTY,所以 SSH 进去只能看到日志,docker exec -it也起不了面板;auth.proxyApiKey给多个客户端(Claude Code / Codex / 本机 harness 等)共用。管理动作——切服务商/套餐、开关 claim 和 async、看日志和额度——现在只能docker exec或改config.yaml重启;docker logs -f能看日志但拿不到额度,quota子命令每次都要手动 exec 一次。所以我不主张放宽
isLoopback()的默认行为,那个默认是对的。我的想法是默认关闭、显式开启:serve模式下加一个开关(配置项或环境变量,比如ZCODE_PANEL_ENABLED、或者让ZCODE_CONTROL_BIND可配),不设就是现在的行为,保持 127.0.0.1 + 非回环 403;auth.proxyApiKey就行),并且别沿用/webui那条豁免。我实测过:4.7.2 时GET /webui直接返回 200——它在 src/server/server.ts#L80 里排在 proxyApiKey 校验(L87-90)之前,而/health、/quota、/v1/models、/mcp都是 401。聊天页本身不含凭据,问题不大;但管理接口要是也这样匿名可访问就危险了;功能上其实是把已有的东西接到一个页面上:
POST /control(status/setConfig/startProxy/stopProxy/getLogs/startOAuth/logout)已经齐了,GET /quota也有了,config/edit.ts能保留注释写回 YAML——缺的只是"在 serve 模式可选启动控制监听器"加上一个像webui.txt那样内嵌的单页。另外看到 v4.7.5 给终端面板加了套餐用量卡(进度条 + 重置倒计时),已经升级用上了,做得挺好看——但它仍然需要一个交互式终端才能看到;安卓端那块同理,服务器上拿不到。
如果方向定为 Web 面板,并且你愿意定个契约(路由前缀 + 鉴权方式),前端我可以直接提 PR,
GET /quota+POST /control已经够支撑了。上面 cjjdaq 贴的那张 UI,方向我觉得也可以借鉴。Ma6302 commentedon Oct 1, 2026
按前面的讨论,把面板整理成一份可实现的接口约定。同时我在自己那台 Ubuntu 服务器上用官方镜像把 serve 模式下的
POST /control全链路跑通了(原型只加了约 170 行 harness + 一个单页,没有改动上游任何文件)。下面第 4、8 节的返回结构都是实测输出,不是设计稿。ZCode Proxy 面板接口约定(草案 v1)
1. 开关与作用域
ZCODE_PANEL_ENABLEDfalseserve模式生效;false时行为与现在完全一致(不监听、不注册路由、不占端口)ZCODE_PANEL_PORT8090ZCODE_PANEL_CONTROL_PORT8091startControlListener()的控制端口ZCODE_PANEL_TOKEN127.0.0.1(沿用server.listen(port, "127.0.0.1")+isLoopback()双重校验),远端通过 SSH 隧道访问,不新增公网面。serve退出即释放端口。2. 鉴权
GET /)都要Authorization: Bearer <ZCODE_PANEL_TOKEN>或X-Panel-Token: <ZCODE_PANEL_TOKEN>;不通过 →401 {"ok":false,"error":"unauthorized: missing or bad panel token"}。/webui的豁免:src/server/server.ts里/webui的返回排在auth.proxyApiKey校验之前(v4.7.2 blob 的 L80 附近),面板若照抄就会变成"公网可匿名打开的管理面"。localStorage,不接受?token=查询参数(会进 nginx/access log)。3. 路由
GET/或/panelbun build --compile单文件二进制也能带上)POST/api/controlControlCommand,逐字转发给控制监听器(同进程可直接调handleControlRequest,不必绕 HTTP),响应 = 既有ControlResponseGET/api/panel/health{ok:true,version,provider,plan},给反代/探活用关键点:
/api/control不新增协议。 面板只是把浏览器请求转成 Android 壳已经在用的那些命令,因此不需要动ControlCommand/ControlOk/LogBuffer的任何一个类型。4. 命令与返回结构(v4.7.5 实测,来自
src/android/control.ts)成功返回(
ok:true):status{state:"running", provider, plan, proxyPort, loggedIn}—loggedIn由loadCredential()决定startProxy{event:"proxyStarted", port}stopProxy{event:"proxyStopped"}setConfig{event:"configUpdated", provider, plan};代理运行中先返回{ok:false,error:"stop_proxy_first"}getLogs{event:"logs", nextSince:number, lines:string[]}—nextSince是增量游标,LogBuffer已实现quota{event:"quota", quota:QuotaSnapshot}(结构与GET /quota完全一致)startOAuth{event:"oauthUrl", authorizeUrl, callbackPort};deliverOAuthCode完成登录logout{event:"loggedOut"}shutdown{event:"shuttingDown"}失败一律
{ok:false, error:string},HTTP 仍是 200(handleControlRequest的行为,面板需要照实透出,不要改成 4xx/5xx)。未接线时:
setConfig→config_update_unavailable、startProxy/stopProxy→proxy_lifecycle_unavailable、quota→quota_unavailable。传输层错误:非回环 → 403
forbidden: non-loopback remote address;路径/方法不对 → 404not_found: ...;body 非法 → 400invalid_json。QuotaSnapshot(src/server/routes-quota.ts):5. 前端刷新约定
getLogssince游标增量statusquotasrc/tui/app.ts:249-259的注释:"billing balance windows; manual refresh only — the billing gateway rate-limits frequent queries, so no polling timer",TUI 已经这么做了6. 边界与容错(实测发现)
loggedIn:false→ 面板显示"未登录 + 去登录",不要显示成错误;startProxy会回not_logged_in。codingPlan是null:实测这台是体验套餐,monitor 面返回errors: ["coding: 500 当前用户不存在coding plan"]。面板必须容忍codingPlan === null,并把errors[]展示成一句提示(否则体验套餐用户会看到一片空白或"面板坏了")。quota无凭据时{ok:false,error:"not logged in (run: zcode-proxy auth login)"}。7. 复用清单(几乎不需要新代码)
startControlListener()/handleControlRequest/LogBuffer——src/android/control.tscollectQuotaSnapshot()——src/server/routes-quota.ts(GET /quota用的同一个函数)updateConfigYaml()——src/config/edit.ts(保留注释写回,TUI 的p/t就用它)src/index.ts:322-333(Android 模式已经全部接好,serve 模式照抄即可)src/index.ts:316的const controlPort = Number(process.env.ZCODE_CONTROL_PORT ?? 0) || 0;目前只在 android 分支启动;serve 模式在
ZCODE_PANEL_ENABLED=true时开同一个监听器即可。8. 实测(v4.7.5 Docker,Ubuntu 云服务器,2026-10-01)
原型 = 官方镜像里的源码 + 一个约 170 行的 harness + 一个单页;跑在 8090/8091/8099(不碰生产的 8080/8081)。
安全路径:
结论:
POST /control的 10 个命令在 serve 模式下原样可用,quota/getLogs/startProxy/stopProxy都在真实凭据上跑通;缺的只有"serve 模式开监听器 + 内嵌单页 + 一个 token"。Ma6302 commentedon Oct 1, 2026
补一条订正 + 实现进展。
订正:上面第 2 节写的是"面板所有路由(含
GET /)都要 token",实际做下来这条要放宽 —— 浏览器的地址栏导航请求发不出自定义头,面板外壳如果强制 token,用户第一次打开页面只会拿到 401,体验是坏的。所以最终口径是:GET /(/panel)与GET /healthz免 token:只返回静态外壳与{"ok":true},不含任何账号、额度、日志数据,且只绑127.0.0.1(远端只能经 SSH 隧道访问)。POST /api/control必须带Authorization: Bearer <token>或X-Panel-Token: <token>,否则 401,且不会触达控制面。其余按上面草案不变:默认关闭、必须有
ZCODE_PANEL_TOKEN才启动(空 token 直接不启动并写明原因)、/v1/*与proxyApiKey逻辑不动、不沿用/webui那条鉴权豁免。实现已提 PR:#59 feat(serve): 可选内置 Web 面板 —— 无头/Docker 部署也能看额度、看日志、切套餐
src/server/panel.ts+src/server/panel-page.txt+src/server/panel.test.ts(17 个用例),src/index.ts的serve()接线;Android 路径、/v1/*、ControlCommand协议都未改。getLogs的since游标做增量。bun x tsc --noEmit通过、bun test928 pass / 1 fail(唯一失败是 Windows 本机既有的 captcha worker 用例);VPS 上用 Docker 做了端到端:状态、真实额度快照、增量日志、stop_proxy_first、启停代理、/v1/chat/completions200、docker stop0.15s 优雅退出,不设ZCODE_PANEL_ENABLED时行为与上游一致。feat(serve): optional web panel for headless deployments
Merge pull request #59 from Ma6302/panel-webui