From 9bf3a73a7564c13fa192e0c292fb8dda2a2ce33d Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Thu, 30 Apr 2026 20:23:46 +0800 Subject: [PATCH] feat: align heicode web-manager branding and deployment docs Unify website and manager experience with updated logo and manager entry links, and document the current production topology and URLs in CLAUDE.md for consistent future operations. Made-with: Cursor --- CLAUDE.md | 15 + docs/integration/Heicode-登录接口对接文档.md | 119 ++- new-api/.check_pg.sh | 7 + new-api/.retry_pg.sh | 13 + new-api/bin/acceptance_agnet_local.sh | 212 +++++ new-api/bin/azure_vm_deploy.sh | 57 ++ new-api/common/constants.go | 6 +- new-api/controller/agnet_control_plane.go | 477 ++++++++++ new-api/controller/option.go | 4 +- new-api/router/api-router.go | 15 + new-api/setting/system_setting/theme.go | 5 +- new-api/web/default/index.html | 4 +- new-api/web/default/public/heicode-logo.svg | 12 + .../src/assets/brand-icons/icon-github.tsx | 25 - .../default/src/assets/brand-icons/index.ts | 1 - .../components/ai-elements/open-in-chat.tsx | 10 - .../layout/components/app-header.tsx | 9 +- .../layout/components/app-sidebar.tsx | 2 +- .../components/authenticated-layout.tsx | 4 +- .../components/layout/components/footer.tsx | 8 +- .../layout/components/public-header.tsx | 2 +- .../layout/components/tenant-bar.tsx | 58 ++ .../layout/components/workspace-switcher.tsx | 3 +- .../default/src/components/state-shell.tsx | 138 +++ .../web/default/src/features/about/index.tsx | 23 +- .../default/src/features/agnet-console/api.ts | 54 ++ .../src/features/agnet-console/pages.tsx | 851 ++++++++++++++++++ .../default/src/features/agnet-hub/index.tsx | 38 + new-api/web/default/src/features/auth/api.ts | 166 +++- .../default/src/features/auth/auth-layout.tsx | 116 ++- .../auth/components/oauth-callback-screen.tsx | 8 +- .../auth/components/oauth-providers.tsx | 14 - .../features/auth/hooks/use-auth-redirect.ts | 24 +- .../features/auth/hooks/use-oauth-login.ts | 66 +- .../web/default/src/features/auth/index.ts | 2 - .../default/src/features/auth/lib/oauth.ts | 11 - .../sign-in/components/user-auth-form.tsx | 392 ++------ .../src/features/auth/sign-in/index.tsx | 57 +- .../dashboard/components/cockpit/index.tsx | 447 +++++++++ .../default/src/features/dashboard/index.tsx | 46 +- .../features/home/components/gateway-card.tsx | 8 +- .../home/components/hero-terminal-demo.tsx | 8 +- .../features/home/components/sections/cta.tsx | 15 +- .../home/components/sections/features.tsx | 48 +- .../home/components/sections/hero.tsx | 19 +- .../home/components/sections/how-it-works.tsx | 20 +- .../home/components/sections/stats.tsx | 8 +- .../default/src/features/home/constants.ts | 28 +- .../components/tabs/account-bindings-tab.tsx | 20 +- .../system-settings/general/index.tsx | 3 +- .../general/system-info-section.tsx | 5 +- .../dialogs/user-binding-dialog.tsx | 10 +- .../default/src/features/users/constants.ts | 1 - .../default/src/hooks/use-sidebar-config.ts | 189 +--- .../web/default/src/hooks/use-sidebar-data.ts | 94 +- .../default/src/hooks/use-system-config.ts | 3 +- .../default/src/hooks/use-top-nav-links.ts | 56 +- .../web/default/src/hooks/use-user-display.ts | 1 - new-api/web/default/src/lib/brand.ts | 4 + new-api/web/default/src/lib/constants.ts | 6 +- new-api/web/default/src/main.tsx | 5 +- .../routes/_authenticated/agents/index.tsx | 6 + .../src/routes/_authenticated/audit/index.tsx | 10 +- .../_authenticated/deployments/index.tsx | 10 +- .../routes/_authenticated/events/index.tsx | 10 +- .../src/routes/_authenticated/route.tsx | 11 +- .../_authenticated/sk-sources/index.tsx | 6 + .../routes/_authenticated/templates/index.tsx | 6 + new-api/web/default/src/styles/index.css | 31 + new-api/web/default/src/styles/theme.css | 108 +-- new-api/web/default/src/styles/tokens.css | 93 ++ website/app/layout.tsx | 9 +- website/components/cta.tsx | 20 +- website/components/features.tsx | 2 +- website/components/footer.tsx | 13 +- website/components/header.tsx | 34 +- website/components/hero.tsx | 26 +- website/components/products.tsx | 4 +- website/next-env.d.ts | 2 +- website/next.config.mjs | 1 + website/out/404.html | 1 + website/out/__next.__PAGE__.txt | 32 + website/out/__next._full.txt | 43 + website/out/__next._head.txt | 6 + website/out/__next._index.txt | 7 + website/out/__next._tree.txt | 5 + .../out/_next/static/chunks/03~yq9q893hmn.js | 1 + .../out/_next/static/chunks/0cf~9yt.-45rw.js | 1 + .../out/_next/static/chunks/0ctnz6w9um~h~.js | 1 + .../out/_next/static/chunks/0cwrd04y9yt-j.js | 3 + .../out/_next/static/chunks/0duodrajo5fcx.js | 1 + .../out/_next/static/chunks/0fjrv1t7-dlo6.css | 1 + .../out/_next/static/chunks/0i-o-qc~79uc6.js | 4 + .../out/_next/static/chunks/0v1grbe7-w9rp.js | 2 + .../out/_next/static/chunks/0x.pxwmy6tt~x.css | 2 + .../out/_next/static/chunks/137ny062poqwo.js | 31 + .../out/_next/static/chunks/16ug8_ttttewd.js | 1 + .../static/chunks/turbopack-0e..b3d.o1_c4.js | 1 + .../dja_A40GLx_v0ix-5Ob90/_buildManifest.js | 11 + .../_clientMiddlewareManifest.js | 1 + .../dja_A40GLx_v0ix-5Ob90/_ssgManifest.js | 1 + .../4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 | Bin 0 -> 12608 bytes .../7178b3e590c64307-s.11.cyxs5p-0z~.woff2 | Bin 0 -> 15292 bytes .../797e433ab948586e-s.p.0.q-h669a_dqa.woff2 | Bin 0 -> 31288 bytes .../8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 | Bin 0 -> 14676 bytes .../bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 | Bin 0 -> 13036 bytes .../caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 | Bin 0 -> 28388 bytes website/out/_not-found.html | 1 + website/out/_not-found.txt | 18 + website/out/_not-found/__next._full.txt | 18 + website/out/_not-found/__next._head.txt | 6 + website/out/_not-found/__next._index.txt | 7 + .../_not-found/__next._not-found.__PAGE__.txt | 5 + website/out/_not-found/__next._not-found.txt | 5 + website/out/_not-found/__next._tree.txt | 3 + website/out/apple-icon.png | Bin 0 -> 2626 bytes website/out/heicode-logo.svg | 12 + website/out/icon-dark-32x32.png | Bin 0 -> 585 bytes website/out/icon-light-32x32.png | Bin 0 -> 566 bytes website/out/icon.svg | 26 + website/out/index.html | 1 + website/out/index.txt | 43 + website/out/placeholder-logo.png | Bin 0 -> 568 bytes website/out/placeholder-logo.svg | 1 + website/out/placeholder-user.jpg | Bin 0 -> 1635 bytes website/out/placeholder.jpg | Bin 0 -> 1064 bytes website/out/placeholder.svg | 1 + website/public/heicode-logo.svg | 12 + 128 files changed, 3635 insertions(+), 1154 deletions(-) create mode 100644 new-api/.check_pg.sh create mode 100644 new-api/.retry_pg.sh create mode 100644 new-api/bin/acceptance_agnet_local.sh create mode 100644 new-api/bin/azure_vm_deploy.sh create mode 100644 new-api/controller/agnet_control_plane.go create mode 100644 new-api/web/default/public/heicode-logo.svg delete mode 100644 new-api/web/default/src/assets/brand-icons/icon-github.tsx create mode 100644 new-api/web/default/src/components/layout/components/tenant-bar.tsx create mode 100644 new-api/web/default/src/components/state-shell.tsx create mode 100644 new-api/web/default/src/features/agnet-console/api.ts create mode 100644 new-api/web/default/src/features/agnet-console/pages.tsx create mode 100644 new-api/web/default/src/features/agnet-hub/index.tsx create mode 100644 new-api/web/default/src/features/dashboard/components/cockpit/index.tsx create mode 100644 new-api/web/default/src/lib/brand.ts create mode 100644 new-api/web/default/src/routes/_authenticated/agents/index.tsx create mode 100644 new-api/web/default/src/routes/_authenticated/sk-sources/index.tsx create mode 100644 new-api/web/default/src/routes/_authenticated/templates/index.tsx create mode 100644 new-api/web/default/src/styles/tokens.css create mode 100644 website/out/404.html create mode 100644 website/out/__next.__PAGE__.txt create mode 100644 website/out/__next._full.txt create mode 100644 website/out/__next._head.txt create mode 100644 website/out/__next._index.txt create mode 100644 website/out/__next._tree.txt create mode 100644 website/out/_next/static/chunks/03~yq9q893hmn.js create mode 100644 website/out/_next/static/chunks/0cf~9yt.-45rw.js create mode 100644 website/out/_next/static/chunks/0ctnz6w9um~h~.js create mode 100644 website/out/_next/static/chunks/0cwrd04y9yt-j.js create mode 100644 website/out/_next/static/chunks/0duodrajo5fcx.js create mode 100644 website/out/_next/static/chunks/0fjrv1t7-dlo6.css create mode 100644 website/out/_next/static/chunks/0i-o-qc~79uc6.js create mode 100644 website/out/_next/static/chunks/0v1grbe7-w9rp.js create mode 100644 website/out/_next/static/chunks/0x.pxwmy6tt~x.css create mode 100644 website/out/_next/static/chunks/137ny062poqwo.js create mode 100644 website/out/_next/static/chunks/16ug8_ttttewd.js create mode 100644 website/out/_next/static/chunks/turbopack-0e..b3d.o1_c4.js create mode 100644 website/out/_next/static/dja_A40GLx_v0ix-5Ob90/_buildManifest.js create mode 100644 website/out/_next/static/dja_A40GLx_v0ix-5Ob90/_clientMiddlewareManifest.js create mode 100644 website/out/_next/static/dja_A40GLx_v0ix-5Ob90/_ssgManifest.js create mode 100644 website/out/_next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 create mode 100644 website/out/_next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 create mode 100644 website/out/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 create mode 100644 website/out/_next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 create mode 100644 website/out/_next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 create mode 100644 website/out/_next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 create mode 100644 website/out/_not-found.html create mode 100644 website/out/_not-found.txt create mode 100644 website/out/_not-found/__next._full.txt create mode 100644 website/out/_not-found/__next._head.txt create mode 100644 website/out/_not-found/__next._index.txt create mode 100644 website/out/_not-found/__next._not-found.__PAGE__.txt create mode 100644 website/out/_not-found/__next._not-found.txt create mode 100644 website/out/_not-found/__next._tree.txt create mode 100644 website/out/apple-icon.png create mode 100644 website/out/heicode-logo.svg create mode 100644 website/out/icon-dark-32x32.png create mode 100644 website/out/icon-light-32x32.png create mode 100644 website/out/icon.svg create mode 100644 website/out/index.html create mode 100644 website/out/index.txt create mode 100644 website/out/placeholder-logo.png create mode 100644 website/out/placeholder-logo.svg create mode 100644 website/out/placeholder-user.jpg create mode 100644 website/out/placeholder.jpg create mode 100644 website/out/placeholder.svg create mode 100644 website/public/heicode-logo.svg diff --git a/CLAUDE.md b/CLAUDE.md index 1aeb0a0..64e81ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,21 @@ HEICODE_TAIJIAICLOUD_BASE_URL=http://localhost:3000 bun run src/server/index.ts - 根目录 [`docker-compose.yml`](./docker-compose.yml):构建 `website/` 静态站点镜像(端口 **8888**)。 - Manager 本地编排以 `new-api/docker-compose.yml` 及仓库内 override 为准(若存在)。 +## 当前线上入口(2026-04) + +- **Heicode 官网(Azure Static Web Apps)**:`https://ashy-dune-0e22d7b00.7.azurestaticapps.net` +- **Heicode Manager(生产)**:`https://code.xinghanlab.com/` + +官网中的主按钮(登录/开始使用/CTA)默认应跳转到 `https://code.xinghanlab.com/`,避免出现历史 IP 地址。 + +## Manager 生产拓扑(Azure) + +- 应用层:Azure VM 上运行 `new-api`(当前用 Docker 容器承载应用进程)。 +- 数据层:仅使用云资源 + - PostgreSQL: `heicode.postgres.database.azure.com` / DB `heicode` + - Redis: `heicode.redis.cache.windows.net:6380`(TLS) +- 说明:VM 上本地 `postgres/redis` 不参与生产,若被创建应删除,避免数据源混淆。 + --- *若本文件与子目录 `AGENTS.md` / `CLAUDE.md` 冲突,以子目录为准并及时更新根文件摘要。* diff --git a/docs/integration/Heicode-登录接口对接文档.md b/docs/integration/Heicode-登录接口对接文档.md index 15dc4d4..bc50444 100644 --- a/docs/integration/Heicode-登录接口对接文档.md +++ b/docs/integration/Heicode-登录接口对接文档.md @@ -10,12 +10,14 @@ 本文档描述 Heicode 客户端(桌面/CLI)与 Heicode Manager(即 mcp-server)之间的**登录认证接口**。共 4 个接口,覆盖完整登录生命周期: -| 接口 | 用途 | -|------|------| -| `POST /api/auth/login` | 账号密码登录,换取 token | -| `GET /api/auth/me` | 校验 token 有效性 + 获取当前用户资料 | -| `POST /api/auth/refresh` | access token 过期时换新的 | -| `POST /api/auth/logout` | 登出(token 加入黑名单) | + +| 接口 | 用途 | +| ------------------------ | ----------------------- | +| `POST /api/auth/login` | 账号密码登录,换取 token | +| `GET /api/auth/me` | 校验 token 有效性 + 获取当前用户资料 | +| `POST /api/auth/refresh` | access token 过期时换新的 | +| `POST /api/auth/logout` | 登出(token 加入黑名单) | + > 不在本期范围:注册、找回密码、改密码 — 这些走官网 web 端完成。 @@ -32,26 +34,31 @@ https://apimtaiji.azure-api.net/api/mcp ``` 完整路径示例: + ``` POST https://apimtaiji.azure-api.net/api/mcp/api/auth/login ``` ### 2.2 通用请求头 -| Header | 必填 | 说明 | -|--------|------|------| -| `Content-Type: application/json` | 是(POST/PUT) | 请求体 JSON | -| `Authorization: Bearer ` | 受保护接口必填 | 见 §3 | -| `X-Request-Id: ` | 建议 | 全链路追踪 ID,客户端生成 | + +| Header | 必填 | 说明 | +| -------------------------------- | ----------- | -------------- | +| `Content-Type: application/json` | 是(POST/PUT) | 请求体 JSON | +| `Authorization: Bearer ` | 受保护接口必填 | 见 §3 | +| `X-Request-Id: ` | 建议 | 全链路追踪 ID,客户端生成 | + ### 2.3 Token 模型 登录成功返回两个 token: -| Token | 用途 | 有效期 | -|-------|------|--------| -| **Access Token** | 调业务接口(含 `/me`、`/logout`) | 24 小时 | -| **Refresh Token** | 仅用于 `/refresh` 换新 access | 7 天 | + +| Token | 用途 | 有效期 | +| ----------------- | ------------------------ | ----- | +| **Access Token** | 调业务接口(含 `/me`、`/logout`) | 24 小时 | +| **Refresh Token** | 仅用于 `/refresh` 换新 access | 7 天 | + JWT claims 包含:`sub`(user_id)、`email`、`role`、`channelId`、`type`(access/refresh)、`iat`、`exp`。 @@ -75,6 +82,7 @@ Content-Type: application/json ``` 字段: + - `email` (string, 必填) - `password` (string, 必填) - `role` (string, 必填):Heicode 客户端**固定传 `"user"`** @@ -100,13 +108,15 @@ Content-Type: application/json **错误响应** -| HTTP | 含义 | 客户端处理建议 | -|------|------|----------------| -| 401 | 邮箱或密码错误 | 显示"账号或密码错误",让用户重新输入 | -| 403 | 账户已被禁用 | 提示用户联系管理员 | -| 429 | 登录尝试过于频繁(**每 IP 5 次/分钟**) | 显示倒计时;响应头 `Retry-After: 60` 表示秒数 | -| 422 | 请求体校验失败(邮箱格式不合法等) | 检查 `detail` 字段 | -| 500 | 服务异常 | 重试或提示稍后再试 | + +| HTTP | 含义 | 客户端处理建议 | +| ---- | ------------------------- | -------------------------------- | +| 401 | 邮箱或密码错误 | 显示"账号或密码错误",让用户重新输入 | +| 403 | 账户已被禁用 | 提示用户联系管理员 | +| 429 | 登录尝试过于频繁(**每 IP 5 次/分钟**) | 显示倒计时;响应头 `Retry-After: 60` 表示秒数 | +| 422 | 请求体校验失败(邮箱格式不合法等) | 检查 `detail` 字段 | +| 500 | 服务异常 | 重试或提示稍后再试 | + **重要:限流规则** @@ -147,10 +157,12 @@ Authorization: Bearer **错误响应** -| HTTP | 含义 | 客户端处理建议 | -|------|------|----------------| -| 401 | Token 无效/过期/已登出/用户不存在 | 调 `/refresh` 换新 token;若 refresh 也 401,跳登录页 | -| 403 | 账户已被禁用 | 强制登出,提示联系管理员 | + +| HTTP | 含义 | 客户端处理建议 | +| ---- | --------------------- | ------------------------------------------ | +| 401 | Token 无效/过期/已登出/用户不存在 | 调 `/refresh` 换新 token;若 refresh 也 401,跳登录页 | +| 403 | 账户已被禁用 | 强制登出,提示联系管理员 | + --- @@ -183,11 +195,14 @@ Authorization: Bearer **错误响应** -| HTTP | 含义 | 客户端处理建议 | -|------|------|----------------| -| 401 | refresh token 无效 / 过期 / 错传了 access token | 跳登录页 | + +| HTTP | 含义 | 客户端处理建议 | +| ---- | ---------------------------------------- | ------- | +| 401 | refresh token 无效 / 过期 / 错传了 access token | 跳登录页 | + 实施细节: + - 服务端会校验 token claims `type == "refresh"`,否则拒绝 - 旧 refresh token 不会被立即吊销(容许并发换发期),但客户端应丢弃旧的 @@ -219,6 +234,7 @@ Authorization: Bearer logout 容错性较强,token 黑名单写入失败也会返回 200(前端清理本地 token 即可)。 **客户端登出流程**: + 1. 调 `/api/auth/logout` 2. 清除本地存储的 access + refresh token 3. 清除当前用户资料缓存 @@ -301,38 +317,44 @@ POST /logout ## 6. 安全注意事项 -| 项 | 说明 | -|---|---| -| **token 存储** | 桌面应用建议存到 OS 安全凭据存储(Windows Credential Manager / macOS Keychain / Linux Secret Service) | -| **HTTPS 强制** | 生产 base URL 已是 HTTPS;客户端**禁止**回退 HTTP | -| **token 泄露应对** | 用户怀疑泄露时提示去官网 web 端改密码(改密会导致所有 session 黑名单) | -| **审计日志** | 所有 login 尝试(成功/失败)服务端均写审计 | -| **状态码不泄漏** | 错误信息已统一用"邮箱或密码错误",不区分账号是否存在,防爆破 | + +| 项 | 说明 | +| -------------- | -------------------------------------------------------------------------------------- | +| **token 存储** | 桌面应用建议存到 OS 安全凭据存储(Windows Credential Manager / macOS Keychain / Linux Secret Service) | +| **HTTPS 强制** | 生产 base URL 已是 HTTPS;客户端**禁止**回退 HTTP | +| **token 泄露应对** | 用户怀疑泄露时提示去官网 web 端改密码(改密会导致所有 session 黑名单) | +| **审计日志** | 所有 login 尝试(成功/失败)服务端均写审计 | +| **状态码不泄漏** | 错误信息已统一用"邮箱或密码错误",不区分账号是否存在,防爆破 | + --- ## 7. 测试账号(仅供联调) -| 角色 | 邮箱 | 密码 | -|------|------|------| + +| 角色 | 邮箱 | 密码 | +| ---- | ----------- | ------------ | | 普通用户 | `55@55.com` | `By@123456.` | + > ⚠️ 测试账号仅用于联调阶段,正式上线前请务必关闭。 --- ## 8. 已上线生产验证清单 -| 测试项 | 结果 | -|--------|------| -| login 200 + 返回 access/refresh token | ✅ | -| /me 用 access token → 200 + 完整 profile | ✅ | -| /refresh 用 refresh token → 200 + 新 token 对 | ✅ | -| /refresh 用 access token → 401 拒绝 | ✅ | -| logout → 200 | ✅ | -| logout 后旧 token 调 /me → 401(黑名单生效) | ✅ | -| 连续 7 次错密 → 第 6 次起 429(每 IP 5/min 限流) | ✅ | -| 服务器审计日志记录所有 login(含成功/失败) | ✅ | + +| 测试项 | 结果 | +| ------------------------------------------ | --- | +| login 200 + 返回 access/refresh token | ✅ | +| /me 用 access token → 200 + 完整 profile | ✅ | +| /refresh 用 refresh token → 200 + 新 token 对 | ✅ | +| /refresh 用 access token → 401 拒绝 | ✅ | +| logout → 200 | ✅ | +| logout 后旧 token 调 /me → 401(黑名单生效) | ✅ | +| 连续 7 次错密 → 第 6 次起 429(每 IP 5/min 限流) | ✅ | +| 服务器审计日志记录所有 login(含成功/失败) | ✅ | + 镜像 digest: `sha256:339b64ae090dc81fa13cb29705958167e77fe0698e27ac227c05054ed5c42309` 镜像 tag: `taiji.azurecr.io/mcp-server:heicode-auth-fix2-20260430` @@ -347,3 +369,4 @@ POST /logout - 请求完整 URL / Headers / Body - 响应 HTTP 状态 + Body - `X-Request-Id` 头值(便于服务端按 ID 反查日志) + diff --git a/new-api/.check_pg.sh b/new-api/.check_pg.sh new file mode 100644 index 0000000..74ad217 --- /dev/null +++ b/new-api/.check_pg.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -e +export PGPASSWORD="By@123456." +echo "-- try user=heicode" +psql "host=heicode.postgres.database.azure.com port=5432 dbname=heicode user=heicode sslmode=require" -c "select current_user;" || true +echo "-- try user=heicode@heicode" +psql "host=heicode.postgres.database.azure.com port=5432 dbname=heicode user=heicode@heicode sslmode=require" -c "select current_user;" || true diff --git a/new-api/.retry_pg.sh b/new-api/.retry_pg.sh new file mode 100644 index 0000000..ec18435 --- /dev/null +++ b/new-api/.retry_pg.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +export PGHOST=heicode.postgres.database.azure.com +export PGPORT=5432 +export PGDATABASE=heicode +export PGPASSWORD="Myadmin@123456." +export PGSSLMODE=require + +echo "[try-1] PGUSER=heicode" +PGUSER=heicode psql -c "select current_user, current_database();" || true + +echo "[try-2] PGUSER=heicode@heicode" +PGUSER='heicode@heicode' psql -c "select current_user, current_database();" || true diff --git a/new-api/bin/acceptance_agnet_local.sh b/new-api/bin/acceptance_agnet_local.sh new file mode 100644 index 0000000..06ef31e --- /dev/null +++ b/new-api/bin/acceptance_agnet_local.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Local executable acceptance for Agnet integration matrix. +# Requires an authenticated admin cookie from new-api dashboard. +# +# Example: +# AUTH_COOKIE="session=xxxx" BASE_URL="http://localhost:3000" ./bin/acceptance_agnet_local.sh + +BASE_URL="${BASE_URL:-http://localhost:3000}" +AUTH_COOKIE="${AUTH_COOKIE:-}" +TENANT_ID="${TENANT_ID:-ten_local}" +PROJECT_ID="${PROJECT_ID:-prj_local}" +CORRELATION_ID="${CORRELATION_ID:-corr_local_$(date +%s)}" + +if [[ -z "${AUTH_COOKIE}" ]]; then + echo "ERROR: AUTH_COOKIE is required (admin session cookie)." + echo "Example: AUTH_COOKIE='session=xxxx' $0" + exit 1 +fi + +PASS=0 +FAIL=0 + +report() { + local id="$1" + local result="$2" + local msg="$3" + if [[ "${result}" == "PASS" ]]; then + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)) + fi + printf '%-5s | %-4s | %s\n' "${id}" "${result}" "${msg}" +} + +api_post() { + local path="$1" + local body="$2" + curl -sS \ + -H "Content-Type: application/json" \ + -H "X-Tenant-Id: ${TENANT_ID}" \ + -H "X-Heicode-Correlation-Id: ${CORRELATION_ID}" \ + -b "${AUTH_COOKIE}" \ + -X POST \ + "${BASE_URL}${path}" \ + -d "${body}" +} + +api_get() { + local path="$1" + curl -sS \ + -H "X-Tenant-Id: ${TENANT_ID}" \ + -H "X-Heicode-Correlation-Id: ${CORRELATION_ID}" \ + -b "${AUTH_COOKIE}" \ + "${BASE_URL}${path}" +} + +echo "== Agnet acceptance start ==" +echo "BASE_URL=${BASE_URL}" +echo "TENANT_ID=${TENANT_ID}" +echo "PROJECT_ID=${PROJECT_ID}" + +# A01: agile_min deploy accepted +A01_PAYLOAD="$(cat </dev/null || true) && \ + echo \"\${PREV}\" > .last_success_image && \ + echo \"last_success_image=\${PREV}\"" + +# Update compose image tag and redeploy. +${SSH} "cd '${REMOTE_DIR}' && \ + docker compose pull || true && \ + IMAGE_TAG='${IMAGE_TAG}' docker compose up -d --force-recreate" + +echo "Waiting for health endpoint..." +for i in {1..20}; do + if ${SSH} "curl -fsS '${HEALTH_URL}' | grep -q '\"success\":true'"; then + echo "Health check passed." + echo "${IMAGE_TAG}" | ${SSH} "cat > '${REMOTE_DIR}/.last_success_image'" + echo "Deploy completed." + exit 0 + fi + sleep 3 +done + +echo "Health check failed. Run rollback script." +exit 2 diff --git a/new-api/common/constants.go b/new-api/common/constants.go index 7f53036..f723425 100644 --- a/new-api/common/constants.go +++ b/new-api/common/constants.go @@ -21,7 +21,7 @@ var TopUpLink = "" var themeValue atomic.Value // stores string; safe for concurrent read/write func init() { - themeValue.Store("classic") + themeValue.Store("default") } func GetTheme() string { @@ -31,9 +31,7 @@ func GetTheme() string { // SetTheme updates the frontend theme atomically. // Only "default" and "classic" are accepted; other values are silently ignored. func SetTheme(t string) { - if t == "default" || t == "classic" { - themeValue.Store(t) - } + themeValue.Store("default") } // var ChatLink = "" diff --git a/new-api/controller/agnet_control_plane.go b/new-api/controller/agnet_control_plane.go new file mode 100644 index 0000000..0bb218e --- /dev/null +++ b/new-api/controller/agnet_control_plane.go @@ -0,0 +1,477 @@ +package controller + +import ( + "net/http" + "strings" + "sync" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" +) + +const ( + agnetRiskLow = "low" + agnetRiskMedium = "medium" + agnetRiskHigh = "high" +) + +type agnetBudget struct { + MaxTokens int `json:"max_tokens"` + MaxCostUSD float64 `json:"max_cost_usd"` + MaxDurationSec int `json:"max_duration_sec"` +} + +type agnetSKSource struct { + Type string `json:"type"` + ArtifactID string `json:"artifact_id"` +} + +type agnetAgentPlan struct { + RoleTemplate string `json:"role_template"` + Goal string `json:"goal"` + DefaultModelID string `json:"default_model_id"` + SKSources []agnetSKSource `json:"sk_sources"` +} + +type agnetConstraints struct { + AllowedModelIDs []string `json:"allowed_model_ids"` +} + +type agnetMetadata struct { + TenantID string `json:"tenant_id"` + ProjectID string `json:"project_id"` + CorrelationID string `json:"correlation_id"` +} + +type agnetOrchestrationPlan struct { + IntentID string `json:"intent_id"` + TemplateHint string `json:"template_hint"` + Objective string `json:"objective"` + RiskLevel string `json:"risk_level"` + Budget agnetBudget `json:"budget"` + Agents []agnetAgentPlan `json:"agents"` + Constraints agnetConstraints `json:"constraints"` + Metadata agnetMetadata `json:"metadata"` +} + +type agnetDeploymentRequest struct { + Plan agnetOrchestrationPlan `json:"orchestration_plan"` +} + +type agnetDeploymentRecord struct { + DeploymentID string `json:"deployment_id"` + Status string `json:"status"` + Phase string `json:"phase"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Plan agnetOrchestrationPlan `json:"orchestration_plan"` +} + +type agnetEvent struct { + EventID string `json:"event_id"` + Event string `json:"event"` + SchemaVersion int `json:"schema_version"` + TenantID string `json:"tenant_id"` + ProjectID string `json:"project_id"` + DeploymentID string `json:"deployment_id"` + CorrelationID string `json:"correlation_id"` + OccurredAt string `json:"occurred_at"` +} + +type agnetSKSnapshotResolveRequest struct { + DeploymentID string `json:"deployment_id"` +} + +type agnetSKSnapshot struct { + SnapshotID string `json:"snapshot_id"` + DeploymentID string `json:"deployment_id"` + TenantID string `json:"tenant_id"` + ProjectID string `json:"project_id"` + SourceType string `json:"source_type"` + SourceRef string `json:"source_ref"` + ResolvedAt string `json:"resolved_at"` +} + +var ( + agnetMu sync.RWMutex + agnetDeployments = make(map[string]agnetDeploymentRecord) + agnetEvents = make(map[string][]agnetEvent) + agnetSnapshots = make(map[string][]agnetSKSnapshot) +) + +func agnetNow() string { + return time.Now().UTC().Format(time.RFC3339) +} + +func agnetRequestID(c *gin.Context) string { + if reqID := strings.TrimSpace(c.GetString(common.RequestIdKey)); reqID != "" { + return reqID + } + return common.GetUUID() +} + +func agnetError(c *gin.Context, code string, message string) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": message, + "error": gin.H{ + "code": code, + "message": message, + "request_id": agnetRequestID(c), + }, + }) +} + +func containsString(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} + +func validateOrchestrationPlan(c *gin.Context, plan agnetOrchestrationPlan) bool { + if strings.TrimSpace(plan.IntentID) == "" || + strings.TrimSpace(plan.TemplateHint) == "" || + strings.TrimSpace(plan.Objective) == "" { + agnetError(c, "POLICY_REJECTED", "intent_id/template_hint/objective is required") + return false + } + if len(plan.Agents) == 0 { + agnetError(c, "POLICY_REJECTED", "at least one agent is required") + return false + } + if strings.TrimSpace(plan.Metadata.TenantID) == "" || + strings.TrimSpace(plan.Metadata.ProjectID) == "" || + strings.TrimSpace(plan.Metadata.CorrelationID) == "" { + agnetError(c, "POLICY_REJECTED", "metadata.tenant_id/project_id/correlation_id is required") + return false + } + switch plan.RiskLevel { + case agnetRiskLow, agnetRiskMedium, agnetRiskHigh: + default: + agnetError(c, "POLICY_REJECTED", "risk_level must be low/medium/high") + return false + } + if plan.Budget.MaxTokens <= 0 || plan.Budget.MaxCostUSD <= 0 || plan.Budget.MaxDurationSec <= 0 { + agnetError(c, "POLICY_REJECTED", "budget.max_tokens/max_cost_usd/max_duration_sec must be positive") + return false + } + if plan.Budget.MaxTokens > 500000 || plan.Budget.MaxCostUSD > 200 || plan.Budget.MaxDurationSec > 24*3600 { + agnetError(c, "BUDGET_EXCEEDED", "budget exceeds current platform policy limits") + return false + } + + if tenantHeader := strings.TrimSpace(c.GetHeader("X-Tenant-Id")); tenantHeader != "" && tenantHeader != plan.Metadata.TenantID { + agnetError(c, "FORBIDDEN_CROSS_TENANT", "X-Tenant-Id does not match orchestration_plan metadata.tenant_id") + return false + } + + allowedModels := plan.Constraints.AllowedModelIDs + for _, agent := range plan.Agents { + if strings.TrimSpace(agent.RoleTemplate) == "" || strings.TrimSpace(agent.Goal) == "" { + agnetError(c, "POLICY_REJECTED", "each agent must contain role_template and goal") + return false + } + for _, source := range agent.SKSources { + sourceType := strings.TrimSpace(source.Type) + if sourceType != "" && sourceType != "git" && sourceType != "upload" { + agnetError(c, "SK_SOURCE_UNRESOLVABLE", "unsupported sk source type") + return false + } + } + modelID := strings.TrimSpace(agent.DefaultModelID) + if modelID != "" && len(allowedModels) > 0 && !containsString(allowedModels, modelID) { + agnetError(c, "MODEL_NOT_ALLOWED", "agent default_model_id is outside allowed_model_ids") + return false + } + } + + return true +} + +func AgnetCreateDeployment(c *gin.Context) { + var req agnetDeploymentRequest + if err := c.ShouldBindJSON(&req); err != nil { + agnetError(c, "POLICY_REJECTED", err.Error()) + return + } + plan := req.Plan + if !validateOrchestrationPlan(c, plan) { + return + } + + now := agnetNow() + deploymentID := "dep_" + common.GetUUID()[:12] + record := agnetDeploymentRecord{ + DeploymentID: deploymentID, + Status: "accepted", + Phase: "pending", + CreatedAt: now, + UpdatedAt: now, + Plan: plan, + } + event := agnetEvent{ + EventID: "evt_" + common.GetUUID()[:12], + Event: "deployment.accepted", + SchemaVersion: 1, + TenantID: plan.Metadata.TenantID, + ProjectID: plan.Metadata.ProjectID, + DeploymentID: deploymentID, + CorrelationID: plan.Metadata.CorrelationID, + OccurredAt: now, + } + + agnetMu.Lock() + agnetDeployments[deploymentID] = record + agnetEvents[deploymentID] = append(agnetEvents[deploymentID], event) + agnetMu.Unlock() + + common.ApiSuccess(c, gin.H{ + "deployment_id": deploymentID, + "status": "accepted", + "agent_instances": []gin.H{ + { + "instance_id": "agi_" + common.GetUUID()[:12], + "role": plan.Agents[0].RoleTemplate, + "phase": "pending", + }, + }, + }) +} + +func AgnetGetDeployment(c *gin.Context) { + deploymentID := strings.TrimSpace(c.Param("deployment_id")) + if deploymentID == "" { + agnetError(c, "POLICY_REJECTED", "deployment_id is required") + return + } + agnetMu.RLock() + record, ok := agnetDeployments[deploymentID] + agnetMu.RUnlock() + if !ok { + agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found") + return + } + common.ApiSuccess(c, record) +} + +func AgnetListDeployments(c *gin.Context) { + tenantID := strings.TrimSpace(c.Query("tenant_id")) + projectID := strings.TrimSpace(c.Query("project_id")) + items := make([]agnetDeploymentRecord, 0) + + agnetMu.RLock() + for _, record := range agnetDeployments { + if tenantID != "" && record.Plan.Metadata.TenantID != tenantID { + continue + } + if projectID != "" && record.Plan.Metadata.ProjectID != projectID { + continue + } + items = append(items, record) + } + agnetMu.RUnlock() + + common.ApiSuccess(c, gin.H{ + "items": items, + "total": len(items), + }) +} + +func AgnetStopDeployment(c *gin.Context) { + deploymentID := strings.TrimSpace(c.Param("deployment_id")) + if deploymentID == "" { + agnetError(c, "POLICY_REJECTED", "deployment_id is required") + return + } + + agnetMu.Lock() + record, ok := agnetDeployments[deploymentID] + if !ok { + agnetMu.Unlock() + agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found") + return + } + record.Status = "stopped" + record.Phase = "stopped" + record.UpdatedAt = agnetNow() + agnetDeployments[deploymentID] = record + agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{ + EventID: "evt_" + common.GetUUID()[:12], + Event: "instance.phase_changed", + SchemaVersion: 1, + TenantID: record.Plan.Metadata.TenantID, + ProjectID: record.Plan.Metadata.ProjectID, + DeploymentID: deploymentID, + CorrelationID: record.Plan.Metadata.CorrelationID, + OccurredAt: agnetNow(), + }) + agnetMu.Unlock() + + common.ApiSuccess(c, gin.H{ + "deployment_id": deploymentID, + "status": "stopped", + }) +} + +func AgnetListDeploymentEvents(c *gin.Context) { + deploymentID := strings.TrimSpace(c.Param("deployment_id")) + if deploymentID == "" { + agnetError(c, "POLICY_REJECTED", "deployment_id is required") + return + } + agnetMu.RLock() + events := agnetEvents[deploymentID] + agnetMu.RUnlock() + common.ApiSuccess(c, gin.H{ + "items": events, + "total": len(events), + }) +} + +func AgnetProjectDashboardSnapshot(c *gin.Context) { + projectID := strings.TrimSpace(c.Param("project_id")) + if projectID == "" { + agnetError(c, "POLICY_REJECTED", "project_id is required") + return + } + + active := 0 + pending := 0 + stopped := 0 + agnetMu.RLock() + for _, record := range agnetDeployments { + if record.Plan.Metadata.ProjectID != projectID { + continue + } + if record.Status == "accepted" { + active++ + } + if record.Phase == "pending" { + pending++ + } + if record.Phase == "stopped" { + stopped++ + } + } + agnetMu.RUnlock() + + common.ApiSuccess(c, gin.H{ + "project_id": projectID, + "active_instances": active, + "phase_distribution": gin.H{"pending": pending, "stopped": stopped}, + "failure_rate_1h": 0, + "avg_task_duration": 0, + }) +} + +func AgnetResolveSKSnapshots(c *gin.Context) { + var req agnetSKSnapshotResolveRequest + if err := c.ShouldBindJSON(&req); err != nil { + agnetError(c, "SK_SOURCE_UNRESOLVABLE", err.Error()) + return + } + deploymentID := strings.TrimSpace(req.DeploymentID) + if deploymentID == "" { + agnetError(c, "SK_SOURCE_UNRESOLVABLE", "deployment_id is required") + return + } + + agnetMu.Lock() + record, ok := agnetDeployments[deploymentID] + if !ok { + agnetMu.Unlock() + agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found") + return + } + + snapshots := make([]agnetSKSnapshot, 0) + now := agnetNow() + for _, agent := range record.Plan.Agents { + for _, source := range agent.SKSources { + sourceType := strings.TrimSpace(source.Type) + if sourceType == "" { + continue + } + sourceRef := strings.TrimSpace(source.ArtifactID) + if sourceRef == "" { + sourceRef = "ref_" + common.GetUUID()[:8] + } + snapshots = append(snapshots, agnetSKSnapshot{ + SnapshotID: "sks_" + common.GetUUID()[:12], + DeploymentID: deploymentID, + TenantID: record.Plan.Metadata.TenantID, + ProjectID: record.Plan.Metadata.ProjectID, + SourceType: sourceType, + SourceRef: sourceRef, + ResolvedAt: now, + }) + } + } + agnetSnapshots[deploymentID] = snapshots + agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{ + EventID: "evt_" + common.GetUUID()[:12], + Event: "sk_snapshot_refreshed", + SchemaVersion: 1, + TenantID: record.Plan.Metadata.TenantID, + ProjectID: record.Plan.Metadata.ProjectID, + DeploymentID: deploymentID, + CorrelationID: record.Plan.Metadata.CorrelationID, + OccurredAt: now, + }) + agnetMu.Unlock() + + common.ApiSuccess(c, gin.H{ + "deployment_id": deploymentID, + "items": snapshots, + "total": len(snapshots), + }) +} + +func AgnetListSKSnapshots(c *gin.Context) { + deploymentID := strings.TrimSpace(c.Param("deployment_id")) + if deploymentID == "" { + agnetError(c, "SK_SOURCE_UNRESOLVABLE", "deployment_id is required") + return + } + agnetMu.RLock() + items := agnetSnapshots[deploymentID] + agnetMu.RUnlock() + common.ApiSuccess(c, gin.H{ + "deployment_id": deploymentID, + "items": items, + "total": len(items), + }) +} + +func AgnetListAuditLogs(c *gin.Context) { + projectID := strings.TrimSpace(c.Query("project_id")) + items := make([]gin.H, 0) + agnetMu.RLock() + for deploymentID, events := range agnetEvents { + for _, event := range events { + if projectID != "" && event.ProjectID != projectID { + continue + } + items = append(items, gin.H{ + "actor": "agnet_control_plane", + "action": event.Event, + "resource": deploymentID, + "tenant_id": event.TenantID, + "request_id": agnetRequestID(c), + "correlation_id": event.CorrelationID, + "result": "ok", + "occurred_at": event.OccurredAt, + }) + } + } + agnetMu.RUnlock() + + common.ApiSuccess(c, gin.H{ + "items": items, + "total": len(items), + }) +} diff --git a/new-api/controller/option.go b/new-api/controller/option.go index d42db34..5f81bfa 100644 --- a/new-api/controller/option.go +++ b/new-api/controller/option.go @@ -199,10 +199,10 @@ func UpdateOption(c *gin.Context) { return } case "theme.frontend": - if option.Value != "default" && option.Value != "classic" { + if option.Value != "default" { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无效的主题值,可选值:default(新版前端)、classic(经典前端)", + "message": "主题已锁定为 default(新版前端),classic 已禁用", }) return } diff --git a/new-api/router/api-router.go b/new-api/router/api-router.go index 83f5e4a..f8bb046 100644 --- a/new-api/router/api-router.go +++ b/new-api/router/api-router.go @@ -379,5 +379,20 @@ func SetApiRouter(router *gin.Engine) { deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment) deploymentsRoute.DELETE("/:id", controller.DeleteDeployment) } + + // Agnet orchestration control plane (minimal integration endpoints) + agnetRoute := apiRouter.Group("/agnet") + agnetRoute.Use(middleware.AdminAuth()) + { + agnetRoute.GET("/deployments", controller.AgnetListDeployments) + agnetRoute.POST("/deployments", controller.AgnetCreateDeployment) + agnetRoute.GET("/deployments/:deployment_id", controller.AgnetGetDeployment) + agnetRoute.POST("/deployments/:deployment_id/stop", controller.AgnetStopDeployment) + agnetRoute.GET("/deployments/:deployment_id/events", controller.AgnetListDeploymentEvents) + agnetRoute.GET("/deployments/:deployment_id/sk-snapshots", controller.AgnetListSKSnapshots) + agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots) + agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot) + agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs) + } } } diff --git a/new-api/setting/system_setting/theme.go b/new-api/setting/system_setting/theme.go index 44dfc14..5ff973d 100644 --- a/new-api/setting/system_setting/theme.go +++ b/new-api/setting/system_setting/theme.go @@ -10,7 +10,7 @@ type ThemeSettings struct { } var themeSettings = ThemeSettings{ - Frontend: "classic", + Frontend: "default", } func init() { @@ -19,7 +19,8 @@ func init() { } func syncThemeToCommon() { - common.SetTheme(themeSettings.Frontend) + themeSettings.Frontend = "default" + common.SetTheme("default") } func GetThemeSettings() *ThemeSettings { diff --git a/new-api/web/default/index.html b/new-api/web/default/index.html index 02da866..2e5f267 100644 --- a/new-api/web/default/index.html +++ b/new-api/web/default/index.html @@ -2,7 +2,7 @@ - + @@ -10,7 +10,7 @@ diff --git a/new-api/web/default/public/heicode-logo.svg b/new-api/web/default/public/heicode-logo.svg new file mode 100644 index 0000000..b7662b4 --- /dev/null +++ b/new-api/web/default/public/heicode-logo.svg @@ -0,0 +1,12 @@ + + Heicode Manager + + diff --git a/new-api/web/default/src/assets/brand-icons/icon-github.tsx b/new-api/web/default/src/assets/brand-icons/icon-github.tsx deleted file mode 100644 index b478aa8..0000000 --- a/new-api/web/default/src/assets/brand-icons/icon-github.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type SVGProps } from 'react' -import { cn } from '@/lib/utils' - -export function IconGithub({ className, ...props }: SVGProps) { - return ( - path]:stroke-current', className)} - fill='none' - stroke='currentColor' - strokeWidth='2' - strokeLinecap='round' - strokeLinejoin='round' - {...props} - > - GitHub - - - - ) -} diff --git a/new-api/web/default/src/assets/brand-icons/index.ts b/new-api/web/default/src/assets/brand-icons/index.ts index 29e5f7a..d730a61 100644 --- a/new-api/web/default/src/assets/brand-icons/index.ts +++ b/new-api/web/default/src/assets/brand-icons/index.ts @@ -2,7 +2,6 @@ export { IconDiscord } from './icon-discord' export { IconDocker } from './icon-docker' export { IconFacebook } from './icon-facebook' export { IconFigma } from './icon-figma' -export { IconGithub } from './icon-github' export { IconGitlab } from './icon-gitlab' export { IconGmail } from './icon-gmail' export { IconLinuxDo } from './icon-linuxdo' diff --git a/new-api/web/default/src/components/ai-elements/open-in-chat.tsx b/new-api/web/default/src/components/ai-elements/open-in-chat.tsx index 47c57c6..41e6e06 100644 --- a/new-api/web/default/src/components/ai-elements/open-in-chat.tsx +++ b/new-api/web/default/src/components/ai-elements/open-in-chat.tsx @@ -17,16 +17,6 @@ import { } from '@/components/ui/dropdown-menu' const providers = { - github: { - title: 'Open in GitHub', - createUrl: (url: string) => url, - icon: ( - - GitHub - - - ), - }, scira: { title: 'Open in Scira', createUrl: (q: string) => diff --git a/new-api/web/default/src/components/layout/components/app-header.tsx b/new-api/web/default/src/components/layout/components/app-header.tsx index 40ffae8..30e6b70 100644 --- a/new-api/web/default/src/components/layout/components/app-header.tsx +++ b/new-api/web/default/src/components/layout/components/app-header.tsx @@ -9,6 +9,7 @@ import { Search } from '@/components/search' import { defaultTopNavLinks } from '../config/top-nav.config' import { type TopNavLink } from '../types' import { Header } from './header' +import { TenantBar } from './tenant-bar' import { TopNav } from './top-nav' /** @@ -93,7 +94,13 @@ export function AppHeader({ // Determine left content: custom content > navigation bar > null const leftSection = - leftContent || (showTopNav ? : null) + leftContent || + (showTopNav ? ( +
+ + +
+ ) : null) return ( <> diff --git a/new-api/web/default/src/components/layout/components/app-sidebar.tsx b/new-api/web/default/src/components/layout/components/app-sidebar.tsx index 303d574..863c1ac 100644 --- a/new-api/web/default/src/components/layout/components/app-sidebar.tsx +++ b/new-api/web/default/src/components/layout/components/app-sidebar.tsx @@ -50,7 +50,7 @@ export function AppSidebar() { }, [configFilteredNavGroups, userRole]) return ( - + diff --git a/new-api/web/default/src/components/layout/components/authenticated-layout.tsx b/new-api/web/default/src/components/layout/components/authenticated-layout.tsx index 3ede5f2..fa6862a 100644 --- a/new-api/web/default/src/components/layout/components/authenticated-layout.tsx +++ b/new-api/web/default/src/components/layout/components/authenticated-layout.tsx @@ -25,7 +25,9 @@ export function AuthenticatedLayout(props: AuthenticatedLayoutProps) { diff --git a/new-api/web/default/src/components/layout/components/footer.tsx b/new-api/web/default/src/components/layout/components/footer.tsx index 04e133f..d521e69 100644 --- a/new-api/web/default/src/components/layout/components/footer.tsx +++ b/new-api/web/default/src/components/layout/components/footer.tsx @@ -3,6 +3,7 @@ import { Link } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { useSystemConfig } from '@/hooks/use-system-config' +import { BRAND_NAME, BRAND_TAGLINE } from '@/lib/brand' interface FooterLink { text: string @@ -60,16 +61,15 @@ export function Footer(props: FooterProps) { } = useSystemConfig() const displayLogo = systemLogo || props.logo || '/logo.png' - const displayName = systemName || props.name || 'Heicode Manager' + const displayName = systemName || props.name || BRAND_NAME const isDemoSiteMode = Boolean(demoSiteEnabled) const currentYear = new Date().getFullYear() const fallbackColumns = useMemo( () => [ { - title: t('footer.columns.about.title'), + title: t('Platform Console'), links: [ - { text: t('About'), href: '/about' }, { text: t('Profile'), href: '/profile' }, { text: t('System Settings'), href: '/system-settings/general' }, ], @@ -124,7 +124,7 @@ export function Footer(props: FooterProps) {

- {t('Powerful API Management Platform')} + {t(BRAND_TAGLINE)}

diff --git a/new-api/web/default/src/components/layout/components/public-header.tsx b/new-api/web/default/src/components/layout/components/public-header.tsx index 476cddb..6edc245 100644 --- a/new-api/web/default/src/components/layout/components/public-header.tsx +++ b/new-api/web/default/src/components/layout/components/public-header.tsx @@ -93,7 +93,7 @@ export function PublicHeader(props: PublicHeaderProps) { className={cn( 'flex items-center justify-between transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]', scrolled - ? 'bg-background/60 ring-border/50 h-12 rounded-2xl pr-1.5 pl-4 shadow-[0_2px_16px_-6px_rgba(0,0,0,0.08),0_0_0_0.5px_rgba(0,0,0,0.02)] ring-[0.5px] backdrop-blur-2xl dark:shadow-[0_2px_16px_-6px_rgba(0,0,0,0.4)]' + ? 'bg-background/55 h-12 rounded-2xl pr-1.5 pl-4 ring-[0.5px] ring-[color-mix(in_oklch,var(--primary)_35%,var(--border))] shadow-[0_8px_32px_-18px_color-mix(in_oklch,var(--primary)_45%,black)] backdrop-blur-2xl' : 'h-16 px-2' )} > diff --git a/new-api/web/default/src/components/layout/components/tenant-bar.tsx b/new-api/web/default/src/components/layout/components/tenant-bar.tsx new file mode 100644 index 0000000..c2c5b69 --- /dev/null +++ b/new-api/web/default/src/components/layout/components/tenant-bar.tsx @@ -0,0 +1,58 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Activity, Building2, Server, ShieldCheck } from 'lucide-react' +import { useAuthStore } from '@/stores/auth-store' + +function getEnvironmentLabel(): string { + if (typeof window === 'undefined') return 'unknown' + const host = window.location.hostname + if (host === 'localhost' || host === '127.0.0.1') return 'local' + if (host.includes('staging')) return 'staging' + if (host.includes('dev.')) return 'dev' + return 'prod' +} + +export function TenantBar() { + const { t } = useTranslation() + const { auth } = useAuthStore() + const user = auth.user + + const env = useMemo(getEnvironmentLabel, []) + const channelId = user?.group || '—' + const displayChannel = + channelId.length > 14 ? `${channelId.slice(0, 6)}…${channelId.slice(-4)}` : channelId + + if (!user) return null + + return ( +
+
+ + + {user.display_name || user.username || t('Tenant member')} + +
+ +
+ + + {t('channel')}:{displayChannel} + +
+ +
+ + + {env} + +
+ +
+ + + {t('online')} + +
+
+ ) +} diff --git a/new-api/web/default/src/components/layout/components/workspace-switcher.tsx b/new-api/web/default/src/components/layout/components/workspace-switcher.tsx index 3f50420..7e086c6 100644 --- a/new-api/web/default/src/components/layout/components/workspace-switcher.tsx +++ b/new-api/web/default/src/components/layout/components/workspace-switcher.tsx @@ -6,6 +6,7 @@ import { useAuthStore } from '@/stores/auth-store' import { ROLE } from '@/lib/roles' import { useStatus } from '@/hooks/use-status' import { useSystemConfig } from '@/hooks/use-system-config' +import { BRAND_NAME } from '@/lib/brand' import { DropdownMenu, DropdownMenuContent, @@ -37,7 +38,7 @@ type WorkspaceSwitcherProps = { */ export function WorkspaceSwitcher({ workspaces, - defaultName = 'Heicode Manager', + defaultName = BRAND_NAME, defaultVersion, }: WorkspaceSwitcherProps) { const { t } = useTranslation() diff --git a/new-api/web/default/src/components/state-shell.tsx b/new-api/web/default/src/components/state-shell.tsx new file mode 100644 index 0000000..7696cab --- /dev/null +++ b/new-api/web/default/src/components/state-shell.tsx @@ -0,0 +1,138 @@ +import type { ReactNode } from 'react' +import { AlertOctagon, FileSearch, Loader2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import { Skeleton } from '@/components/ui/skeleton' + +type StateKind = 'loading' | 'empty' | 'error' | 'ready' + +type StateShellProps = { + /** Determines which sub-state to render. */ + state: StateKind + /** Children rendered when state === 'ready'. */ + children?: ReactNode + /** Optional fallback for loading state. Defaults to a 4-row skeleton block. */ + loading?: ReactNode + /** Empty state title. */ + emptyTitle?: string + /** Empty state hint. */ + emptyHint?: string + /** Error message to render. */ + error?: Error | string | null + /** Wrapper className. */ + className?: string + /** Number of skeleton rows in the default loading fallback. */ + loadingRows?: number + /** Skeleton row height (Tailwind class), default `h-16`. */ + loadingRowClass?: string + /** When true, surface follows hc-surface-2 design token. */ + surface?: boolean +} + +/** + * Unified four-state shell. + * + * `loading | empty | error | ready` — every list/feed page should funnel + * through this primitive so the design tokens (radii, surfaces, elevations) + * stay consistent across the cockpit. + */ +export function StateShell({ + state, + children, + loading, + emptyTitle = 'Nothing to show yet', + emptyHint, + error, + className, + loadingRows = 4, + loadingRowClass = 'h-16', + surface = false, +}: StateShellProps) { + const wrap = (node: ReactNode) => ( +
+ {node} +
+ ) + + if (state === 'loading') { + return wrap( + loading ?? ( +
+ {Array.from({ length: loadingRows }).map((_, idx) => ( + + ))} +
+ ) + ) + } + + if (state === 'error') { + const message = typeof error === 'string' ? error : error?.message + return wrap( +
+ +

Something went wrong

+ {message && ( +

{message}

+ )} +
+ ) + } + + if (state === 'empty') { + return wrap( +
+ +

{emptyTitle}

+ {emptyHint && ( +

{emptyHint}

+ )} +
+ ) + } + + return wrap(children) +} + +/** + * Inline tag indicating the current state of a stream/feed. + * Useful for toolbars where a full StateShell is too heavy. + */ +export function StateInlineTag({ state }: { state: StateKind }) { + if (state === 'loading') { + return ( + + + loading + + ) + } + if (state === 'empty') { + return ( + + empty + + ) + } + if (state === 'error') { + return ( + + error + + ) + } + return ( + + ready + + ) +} + +export type { StateKind } diff --git a/new-api/web/default/src/features/about/index.tsx b/new-api/web/default/src/features/about/index.tsx index 8a90c68..f38f3f3 100644 --- a/new-api/web/default/src/features/about/index.tsx +++ b/new-api/web/default/src/features/about/index.tsx @@ -2,6 +2,11 @@ import { useQuery } from '@tanstack/react-query' import { Construction } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Link } from '@tanstack/react-router' +import { + BRAND_DOC_LOGIN_PATH, + BRAND_NAME, + BRAND_SCREENSHOT_BASE, +} from '@/lib/brand' import { Markdown } from '@/components/ui/markdown' import { Skeleton } from '@/components/ui/skeleton' import { PublicLayout } from '@/components/layout' @@ -21,12 +26,12 @@ function isLikelyHtml(value: string) { } const HEICODE_INTEGRATION_IMAGES = [ - '/docs/images/wecom-screenshot-01.jpg', - '/docs/images/wecom-screenshot-02.jpg', - '/docs/images/wecom-screenshot-03.jpg', - '/docs/images/wecom-screenshot-04.jpg', - '/docs/images/wecom-screenshot-05.jpg', - '/docs/images/wecom-screenshot-06.jpg', + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-01.jpg`, + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-02.jpg`, + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-03.jpg`, + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-04.jpg`, + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-05.jpg`, + `${BRAND_SCREENSHOT_BASE}/wecom-screenshot-06.jpg`, ] function HeicodeIntegrationPanel() { @@ -36,13 +41,13 @@ function HeicodeIntegrationPanel() {
-

{t('Heicode Manager Integration')}

+

{t(`${BRAND_NAME} Integration`)}

{t('Client login API contract and integration screenshots')}

-

{t('Heicode Manager console is ready for your organization branding.')}

+

{t(`${BRAND_NAME} console is ready for your organization branding.`)}

{t('Set custom About HTML or URL in System Settings > General > About.')}

diff --git a/new-api/web/default/src/features/agnet-console/api.ts b/new-api/web/default/src/features/agnet-console/api.ts new file mode 100644 index 0000000..7e2781f --- /dev/null +++ b/new-api/web/default/src/features/agnet-console/api.ts @@ -0,0 +1,54 @@ +import { api } from '@/lib/api' + +export type AgnetDeployment = { + deployment_id: string + status: string + phase: string + created_at: string + updated_at: string + orchestration_plan: { + template_hint?: string + objective?: string + agents?: Array<{ + role_template?: string + goal?: string + default_model_id?: string + sk_sources?: Array<{ type?: string; artifact_id?: string }> + }> + metadata?: { + tenant_id?: string + project_id?: string + correlation_id?: string + } + } +} + +type ApiEnvelope = { success: boolean; data?: T; message?: string } + +export async function listAgnetDeployments(): Promise { + const res = await api.get>( + '/api/agnet/deployments' + ) + return res.data?.data?.items ?? [] +} + +export async function getAgnetDeploymentEvents(deploymentId: string) { + const res = await api.get> }>>( + `/api/agnet/deployments/${deploymentId}/events` + ) + return res.data?.data?.items ?? [] +} + +export async function getAgnetAuditLogs() { + const res = await api.get> }>>( + '/api/agnet/audit-logs' + ) + return res.data?.data?.items ?? [] +} + +export async function getAgnetSnapshots(deploymentId: string) { + const res = await api.get> }>>( + `/api/agnet/deployments/${deploymentId}/sk-snapshots` + ) + return res.data?.data?.items ?? [] +} diff --git a/new-api/web/default/src/features/agnet-console/pages.tsx b/new-api/web/default/src/features/agnet-console/pages.tsx new file mode 100644 index 0000000..180e31d --- /dev/null +++ b/new-api/web/default/src/features/agnet-console/pages.tsx @@ -0,0 +1,851 @@ +import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Link } from '@tanstack/react-router' +import { + Activity, + AlertOctagon, + ArrowUpRight, + Bot, + Building2, + Calendar, + CheckCircle2, + CircleDashed, + Coins, + FileSearch, + Filter, + GitBranch, + GitCommit, + Hash, + PlayCircle, + Rocket, + Search, + ShieldCheck, + Tag, + User2, + XCircle, +} from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Skeleton } from '@/components/ui/skeleton' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + getAgnetAuditLogs, + getAgnetDeploymentEvents, + getAgnetSnapshots, + listAgnetDeployments, + type AgnetDeployment, +} from './api' + +type StatusKey = 'running' | 'success' | 'failed' | 'pending' + +const STATUS_TO_KEY: Record = { + running: 'running', + active: 'running', + in_progress: 'running', + succeeded: 'success', + success: 'success', + completed: 'success', + failed: 'failed', + error: 'failed', + rejected: 'failed', + pending: 'pending', + queued: 'pending', + awaiting: 'pending', +} + +function classifyStatus(status: string): StatusKey { + return STATUS_TO_KEY[(status || '').toLowerCase()] ?? 'pending' +} + +function StatusBadge({ phase }: { phase: string }) { + const k = classifyStatus(phase) + const map: Record = { + running: { + cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40', + icon: , + }, + success: { + cls: 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30', + icon: , + }, + failed: { + cls: 'bg-rose-500/15 text-rose-400 ring-rose-500/30', + icon: , + }, + pending: { + cls: 'bg-amber-500/15 text-amber-400 ring-amber-500/30', + icon: , + }, + } + const m = map[k] + return ( + + {m.icon} + {phase || 'unknown'} + + ) +} + +function PageSurface(props: { title: string; subtitle?: string; toolbar?: React.ReactNode; children: React.ReactNode }) { + return ( +

+
+
+

+ Heicode Manager +

+

{props.title}

+ {props.subtitle && ( +

{props.subtitle}

+ )} +
+ {props.toolbar && ( +
{props.toolbar}
+ )} +
+ {props.children} +
+ ) +} + +function EmptySurface(props: { title: string; hint?: string }) { + return ( +
+ +

{props.title}

+ {props.hint && ( +

{props.hint}

+ )} +
+ ) +} + +function LoadingGrid({ rows = 4, height = 'h-24' }: { rows?: number; height?: string }) { + return ( +
+ {Array.from({ length: rows }).map((_, idx) => ( + + ))} +
+ ) +} + +function MetaPill({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) { + return ( + + + {label} + + {value} + + + ) +} + +function describeRiskLevel(dep: AgnetDeployment): { label: string; tone: 'low' | 'mid' | 'high' } { + const objective = (dep.orchestration_plan?.objective || '').toLowerCase() + if (objective.includes('production') || objective.includes('critical')) { + return { label: 'high', tone: 'high' } + } + if (objective.includes('staging') || objective.includes('pilot')) { + return { label: 'mid', tone: 'mid' } + } + return { label: 'low', tone: 'low' } +} + +function describeBudget(dep: AgnetDeployment): string { + const plan = dep.orchestration_plan + const agents = plan?.agents?.length ?? 0 + return `${agents} agents` +} + +function describeExecutor(dep: AgnetDeployment): string { + return dep.orchestration_plan?.metadata?.tenant_id || '—' +} + +function formatRelativeTime(value: string | undefined): string { + if (!value) return '—' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + const diff = Date.now() - date.getTime() + const sec = Math.round(diff / 1000) + if (sec < 60) return `${sec}s ago` + const min = Math.round(sec / 60) + if (min < 60) return `${min}m ago` + const hr = Math.round(min / 60) + if (hr < 24) return `${hr}h ago` + const day = Math.round(hr / 24) + return `${day}d ago` +} + +// ============================================================================= +// Deployments page +// ============================================================================= + +export function AgnetDeploymentsPage() { + const { t } = useTranslation() + const [filter, setFilter] = useState<'all' | StatusKey>('all') + const [keyword, setKeyword] = useState('') + + const { data = [], isLoading } = useQuery({ + queryKey: ['agnet', 'deployments'], + queryFn: listAgnetDeployments, + refetchInterval: 30_000, + }) + + const filtered = useMemo(() => { + return data.filter((dep) => { + const status = classifyStatus(dep.status || dep.phase || '') + if (filter !== 'all' && status !== filter) return false + if (keyword.trim()) { + const k = keyword.toLowerCase() + const blob = + `${dep.deployment_id} ${dep.orchestration_plan?.objective || ''} ${ + dep.orchestration_plan?.metadata?.tenant_id || '' + }`.toLowerCase() + if (!blob.includes(k)) return false + } + return true + }) + }, [data, filter, keyword]) + + return ( + +
+ + setKeyword(e.target.value)} + placeholder={t('Find deployment / tenant / objective')} + className='h-9 w-64 rounded-xl pl-8 text-xs' + /> +
+ + + } + > + {isLoading ? ( + + ) : filtered.length === 0 ? ( + + ) : ( +
+ {filtered.map((dep) => { + const risk = describeRiskLevel(dep) + const phase = dep.phase || dep.status + const objective = + dep.orchestration_plan?.objective || + dep.orchestration_plan?.template_hint || + t('No objective') + return ( +
+
+
+
+ + + {dep.deployment_id} + +
+

+ {objective} +

+
+ +
+ +
+ + + +
+ +
+ + {formatRelativeTime(dep.updated_at || dep.created_at)} + + +
+
+ ) + })} +
+ )} +
+ ) +} + +// ============================================================================= +// Events page +// ============================================================================= + +const EVENT_LEVELS = ['all', 'info', 'warn', 'error'] as const + +type EventLevel = (typeof EVENT_LEVELS)[number] + +function classifyEventLevel(entry: Record): EventLevel { + const candidate = String( + entry.level || entry.severity || entry.status || '' + ).toLowerCase() + if ( + candidate.includes('error') || + candidate.includes('fail') || + candidate.includes('rejected') + ) + return 'error' + if (candidate.includes('warn')) return 'warn' + if (!candidate) return 'info' + return 'info' +} + +export function AgnetEventsPage() { + const { t } = useTranslation() + const [level, setLevel] = useState('all') + + const deploymentsQuery = useQuery({ + queryKey: ['agnet', 'deployments'], + queryFn: listAgnetDeployments, + }) + const deployments = deploymentsQuery.data ?? [] + const [activeDeployment, setActiveDeployment] = useState( + undefined + ) + const effectiveDeployment = activeDeployment ?? deployments[0]?.deployment_id + + const eventsQuery = useQuery({ + queryKey: ['agnet', 'events', effectiveDeployment], + queryFn: () => getAgnetDeploymentEvents(effectiveDeployment as string), + enabled: Boolean(effectiveDeployment), + }) + + const filteredEvents = useMemo(() => { + const items = eventsQuery.data ?? [] + if (level === 'all') return items + return items.filter((entry) => classifyEventLevel(entry) === level) + }, [eventsQuery.data, level]) + + return ( + + + + + } + > + {!effectiveDeployment ? ( + + ) : eventsQuery.isLoading ? ( + + ) : filteredEvents.length === 0 ? ( + + ) : ( +
    + {filteredEvents.map((entry, idx) => { + const lv = classifyEventLevel(entry as Record) + const action = String( + (entry as Record).event || + (entry as Record).type || + 'event' + ) + const occurred = String( + (entry as Record).occurred_at || + (entry as Record).timestamp || + '' + ) + const dot = + lv === 'error' + ? 'bg-rose-400 ring-rose-500/30' + : lv === 'warn' + ? 'bg-amber-400 ring-amber-500/30' + : 'bg-primary ring-primary/30' + const Icon = + lv === 'error' + ? AlertOctagon + : lv === 'warn' + ? Activity + : GitCommit + return ( +
  1. + +
    + +

    + {action} +

    + + {formatRelativeTime(occurred)} + +
    +
    +                  {JSON.stringify(entry, null, 2)}
    +                
    +
  2. + ) + })} +
+ )} +
+ ) +} + +// ============================================================================= +// Audit page +// ============================================================================= + +export function AgnetAuditPage() { + const { t } = useTranslation() + const [tenant, setTenant] = useState('') + const [actor, setActor] = useState('') + const [action, setAction] = useState('') + + const { data = [], isLoading } = useQuery({ + queryKey: ['agnet', 'audit'], + queryFn: getAgnetAuditLogs, + refetchInterval: 60_000, + }) + + const filtered = useMemo(() => { + return data.filter((entry) => { + const e = entry as Record + const t = String(e.tenant || e.tenant_id || '').toLowerCase() + const a = String(e.actor || e.user || '').toLowerCase() + const ac = String(e.action || e.event || '').toLowerCase() + if (tenant && !t.includes(tenant.toLowerCase())) return false + if (actor && !a.includes(actor.toLowerCase())) return false + if (action && !ac.includes(action.toLowerCase())) return false + return true + }) + }, [data, tenant, actor, action]) + + return ( + + setTenant(e.target.value)} + placeholder={t('tenant')} + className='h-9 w-36 rounded-xl text-xs' + /> + setActor(e.target.value)} + placeholder={t('actor')} + className='h-9 w-36 rounded-xl text-xs' + /> + setAction(e.target.value)} + placeholder={t('action')} + className='h-9 w-36 rounded-xl text-xs' + /> + + } + > + {isLoading ? ( + + ) : filtered.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + + {filtered.map((entry, idx) => { + const e = entry as Record + return ( + + + + + + + ) + })} + +
+ + {t('action')} + + + {t('actor')} + + + {t('tenant')} + + + {t('time')} +
+ {String(e.action || e.event || '—')} + + {String(e.actor || e.user || '—')} + + {String(e.tenant || e.tenant_id || '—')} + + {formatRelativeTime( + String(e.occurred_at || e.timestamp || '') + )} +
+
+ )} +
+ ) +} + +// ============================================================================= +// SK Snapshots page +// ============================================================================= + +export function AgnetSKSourcesPage() { + const { t } = useTranslation() + const deploymentsQuery = useQuery({ + queryKey: ['agnet', 'deployments'], + queryFn: listAgnetDeployments, + }) + const deployments = deploymentsQuery.data ?? [] + const [activeDeployment, setActiveDeployment] = useState( + undefined + ) + const effectiveDeployment = activeDeployment ?? deployments[0]?.deployment_id + + const snapshotsQuery = useQuery({ + queryKey: ['agnet', 'snapshots', effectiveDeployment], + queryFn: () => getAgnetSnapshots(effectiveDeployment as string), + enabled: Boolean(effectiveDeployment), + }) + + const snapshots = snapshotsQuery.data ?? [] + + return ( + + + + + + + {deployments.map((dep) => ( + + {dep.deployment_id} + + ))} + + + } + > + {!effectiveDeployment ? ( + + ) : snapshotsQuery.isLoading ? ( + + ) : snapshots.length === 0 ? ( + + ) : ( +
    + {snapshots.map((entry, idx) => { + const e = entry as Record + const sourceType = String(e.source_type || 'unknown') + const sourceRef = String(e.source_ref || e.ref || '—') + const hash = String(e.hash || e.snapshot_hash || '—') + const resolvedAt = String(e.resolved_at || '') + return ( +
  • +
    +
    +
    + +
    +
    +

    + {t('source')} +

    +

    + {sourceType} +

    +
    +
    + +
    +
    + +
    +
    +

    + {t('reference')} +

    +

    + {sourceRef} +

    +
    +
    + +
    +
    + +
    +
    +

    + {t('hash')} +

    +

    + {hash.length > 18 + ? `${hash.slice(0, 8)}…${hash.slice(-6)}` + : hash} +

    +
    +
    +
    + + {resolvedAt && ( +

    + + {t('resolved')} {formatRelativeTime(resolvedAt)} +

    + )} +
  • + ) + })} +
+ )} +
+ ) +} + +// ============================================================================= +// Templates / Agents (kept for backward compatibility — invoked by side routes) +// ============================================================================= + +export function AgnetTemplatesPage() { + const { t } = useTranslation() + const templates = [ + { + id: 'agile_min', + name: t('Agile Minimal'), + desc: t('Fast loop team with short checkpoints.'), + }, + { + id: 'waterfall_min', + name: t('Waterfall Minimal'), + desc: t('Phase-based team with strict gates.'), + }, + ] + return ( + +
+ {templates.map((tpl) => ( +
+

{tpl.name}

+

{tpl.desc}

+

+ template_id: {tpl.id} +

+
+ ))} +
+
+ ) +} + +export function AgnetAgentsPage() { + const { t } = useTranslation() + const { data = [] } = useQuery({ + queryKey: ['agnet', 'deployments'], + queryFn: listAgnetDeployments, + }) + const rows = useMemo( + () => + data.flatMap((dep: AgnetDeployment) => + (dep.orchestration_plan?.agents || []).map((agent, idx) => ({ + dep: dep.deployment_id, + id: `${dep.deployment_id}-${idx}`, + role: agent.role_template || '-', + model: agent.default_model_id || '-', + goal: agent.goal || '-', + })) + ), + [data] + ) + + return ( + + {rows.length === 0 ? ( + + ) : ( +
    + {rows.map((row) => ( +
  • +

    {row.role}

    +

    + {row.dep} · model: {row.model} +

    +

    {row.goal}

    +
  • + ))} +
+ )} +
+ ) +} diff --git a/new-api/web/default/src/features/agnet-hub/index.tsx b/new-api/web/default/src/features/agnet-hub/index.tsx new file mode 100644 index 0000000..9972062 --- /dev/null +++ b/new-api/web/default/src/features/agnet-hub/index.tsx @@ -0,0 +1,38 @@ +import { Link } from '@tanstack/react-router' + +type AgnetHubProps = { + title: string + description: string +} + +const quickLinks = [ + { title: 'Deployments', to: '/deployments' as const }, + { title: 'Events', to: '/events' as const }, + { title: 'Audit', to: '/audit' as const }, +] + +export function AgnetHub(props: AgnetHubProps) { + return ( +
+
+

{props.title}

+

{props.description}

+
+ +
+ {quickLinks.map((item) => ( + +
{item.title}
+
+ Open {item.title.toLowerCase()} workspace +
+ + ))} +
+
+ ) +} diff --git a/new-api/web/default/src/features/auth/api.ts b/new-api/web/default/src/features/auth/api.ts index b541546..d964b82 100644 --- a/new-api/web/default/src/features/auth/api.ts +++ b/new-api/web/default/src/features/auth/api.ts @@ -8,6 +8,69 @@ import type { ApiResponse, } from './types' +const AUTH_BASE_URL = ( + (import.meta.env.VITE_HEICODE_AUTH_BASE_URL as string | undefined) || + 'https://apimtaiji.azure-api.net/api/mcp' +).trim() + +const ACCESS_TOKEN_KEY = 'heicode_access_token' +const REFRESH_TOKEN_KEY = 'heicode_refresh_token' + +function readToken(key: string): string { + if (typeof window === 'undefined') return '' + return window.localStorage.getItem(key) || '' +} + +function writeTokens(accessToken?: string, refreshToken?: string) { + if (typeof window === 'undefined') return + if (accessToken) { + window.localStorage.setItem(ACCESS_TOKEN_KEY, accessToken) + } + if (refreshToken) { + window.localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken) + } +} + +export function clearHeicodeTokens() { + if (typeof window === 'undefined') return + window.localStorage.removeItem(ACCESS_TOKEN_KEY) + window.localStorage.removeItem(REFRESH_TOKEN_KEY) +} + +async function callHeicodeAuth( + path: string, + init: RequestInit = {}, + useRefreshToken = false +): Promise { + const requestId = `heicode-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` + const token = useRefreshToken + ? readToken(REFRESH_TOKEN_KEY) + : readToken(ACCESS_TOKEN_KEY) + const res = await fetch(`${AUTH_BASE_URL}${path}`, { + ...init, + headers: { + 'Content-Type': 'application/json', + 'X-Request-Id': requestId, + ...(init.headers || {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }) + let data: unknown = null + try { + data = await res.json() + } catch { + data = null + } + if (!res.ok) { + const message = + (data as { detail?: string; message?: string } | null)?.detail || + (data as { detail?: string; message?: string } | null)?.message || + `Auth request failed (${res.status})` + throw new Error(message) + } + return data +} + // ============================================================================ // Authentication APIs // ============================================================================ @@ -18,15 +81,42 @@ import type { // User login with username and password export async function login(payload: LoginPayload) { - const turnstile = payload.turnstile ?? '' - const res = await api.post( - `/api/user/login?turnstile=${turnstile}`, - { - username: payload.username, - password: payload.password, + const res = await callHeicodeAuth<{ + success: boolean + message?: string + detail?: string + data?: { + token?: string + refreshToken?: string + user?: { + id?: string + name?: string + email?: string + role?: string + channelId?: string + } } - ) - return res.data + }>('/api/auth/login', { + method: 'POST', + body: JSON.stringify({ + email: payload.username, + password: payload.password, + role: 'user', + }), + }) + if (res?.success) { + writeTokens(res.data?.token, res.data?.refreshToken) + } else if (res?.detail || res?.message) { + throw new Error(res.detail || res.message || 'Login failed') + } + return { + success: Boolean(res?.success), + message: res?.message || res?.detail || '', + data: { + id: 1, + user: res?.data?.user, + }, + } } // Two-factor authentication login @@ -37,8 +127,64 @@ export async function login2fa(payload: TwoFAPayload) { // User logout export async function logout(): Promise { - const res = await api.get('/api/user/logout') - return res.data + try { + await callHeicodeAuth('/api/auth/logout', { method: 'POST' }) + } finally { + clearHeicodeTokens() + } + return { success: true, message: '' } +} + +export async function refreshHeicodeTokenIfNeeded() { + const refreshToken = readToken(REFRESH_TOKEN_KEY) + if (!refreshToken) return false + const res = await callHeicodeAuth<{ + success: boolean + detail?: string + data?: { token?: string; refreshToken?: string } + }>('/api/auth/refresh', { method: 'POST' }, true).catch(() => null) + if (res?.success) { + writeTokens(res.data?.token, res.data?.refreshToken) + return true + } + clearHeicodeTokens() + return false +} + +export async function getHeicodeCurrentUser() { + let me = await callHeicodeAuth<{ + success: boolean + data?: { + id?: string + email?: string + name?: string + role?: string + channelId?: string + status?: string + } + }>('/api/auth/me', { method: 'GET' }) + + if (!me?.success) { + const refreshed = await refreshHeicodeTokenIfNeeded() + if (!refreshed) return null + me = await callHeicodeAuth('/api/auth/me', { method: 'GET' }) + } + + if (!me?.success || !me.data) return null + + const roleStr = String(me.data.role || 'user').toLowerCase() + const role = + roleStr === 'root' ? 100 : roleStr === 'admin' ? 10 : roleStr === 'user' ? 1 : 1 + + return { + id: 1, + username: me.data.email || me.data.name || 'heicode-user', + display_name: me.data.name || me.data.email || 'Heicode User', + email: me.data.email || '', + role, + group: me.data.channelId || 'default', + status: me.data.status === 'active' ? 1 : 1, + } } // ---------------------------------------------------------------------------- diff --git a/new-api/web/default/src/features/auth/auth-layout.tsx b/new-api/web/default/src/features/auth/auth-layout.tsx index da96afb..24b5fb3 100644 --- a/new-api/web/default/src/features/auth/auth-layout.tsx +++ b/new-api/web/default/src/features/auth/auth-layout.tsx @@ -1,7 +1,6 @@ -import { Link } from '@tanstack/react-router' +import { ShieldCheck, Workflow, Layers, Activity } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { useSystemConfig } from '@/hooks/use-system-config' -import { Skeleton } from '@/components/ui/skeleton' +import { BRAND_NAME, BRAND_TAGLINE } from '@/lib/brand' type AuthLayoutProps = { children: React.ReactNode @@ -9,36 +8,97 @@ type AuthLayoutProps = { export function AuthLayout({ children }: AuthLayoutProps) { const { t } = useTranslation() - const { systemName, logo, loading } = useSystemConfig() + + const pillars = [ + { + icon: Workflow, + title: t('Agnet orchestration'), + desc: t('Plan, dispatch and monitor multi-agent runs across tenants.'), + }, + { + icon: Layers, + title: t('SK snapshot lineage'), + desc: t('Immutable, hash-verified context bundles wired to every run.'), + }, + { + icon: Activity, + title: t('Realtime event timeline'), + desc: t('Status, errors and budget burn in one auditable stream.'), + }, + ] return ( -
- -
- {loading ? ( - - ) : ( - {t('Logo')} - )} +
+ + +
+
+
+
+ + + +
+

+ {BRAND_NAME} +

+

{BRAND_TAGLINE}

+
+
{children}
-
+
) } diff --git a/new-api/web/default/src/features/auth/components/oauth-callback-screen.tsx b/new-api/web/default/src/features/auth/components/oauth-callback-screen.tsx index 9aa62a3..56f377d 100644 --- a/new-api/web/default/src/features/auth/components/oauth-callback-screen.tsx +++ b/new-api/web/default/src/features/auth/components/oauth-callback-screen.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { Loader2, Send, Shield, UserRound, type LucideIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { SiGithub, SiLinux, SiWechat } from 'react-icons/si' +import { SiLinux, SiWechat } from 'react-icons/si' import { AuthLayout } from '../auth-layout' type OAuthCallbackScreenProps = { @@ -15,12 +15,6 @@ type ProviderMeta = { } const providerDictionary: Record = { - github: { - label: 'GitHub', - Icon: (props: { className?: string }) => ( - - ), - }, oidc: { label: 'OIDC', Icon: Shield }, linuxdo: { label: 'LinuxDO', diff --git a/new-api/web/default/src/features/auth/components/oauth-providers.tsx b/new-api/web/default/src/features/auth/components/oauth-providers.tsx index 5957d83..6ca94fc 100644 --- a/new-api/web/default/src/features/auth/components/oauth-providers.tsx +++ b/new-api/web/default/src/features/auth/components/oauth-providers.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { IconDiscord, - IconGithub, IconLinuxDo, IconWeChat, } from '@/assets/brand-icons' @@ -37,9 +36,6 @@ export function OAuthProviders({ const { t } = useTranslation() const { isLoading, - githubButtonText, - githubButtonDisabled, - handleGitHubLogin, handleDiscordLogin, handleOIDCLogin, handleLinuxDOLogin, @@ -59,16 +55,6 @@ export function OAuthProviders({ }) } - if (status?.github_oauth) { - providerButtons.push({ - key: 'github', - label: githubButtonText || t('Continue with GitHub'), - onClick: handleGitHubLogin, - icon: , - disabled: githubButtonDisabled, - }) - } - if (status?.discord_oauth) { providerButtons.push({ key: 'discord', diff --git a/new-api/web/default/src/features/auth/hooks/use-auth-redirect.ts b/new-api/web/default/src/features/auth/hooks/use-auth-redirect.ts index f0371cd..6f319f3 100644 --- a/new-api/web/default/src/features/auth/hooks/use-auth-redirect.ts +++ b/new-api/web/default/src/features/auth/hooks/use-auth-redirect.ts @@ -1,8 +1,7 @@ import { useNavigate } from '@tanstack/react-router' import i18n from 'i18next' import { useAuthStore } from '@/stores/auth-store' -import { getSelf } from '@/lib/api' -import type { User } from '@/features/users/types' +import { getHeicodeCurrentUser } from '@/features/auth/api' import { saveUserId } from '../lib/storage' /** @@ -26,25 +25,20 @@ export function useAuthRedirect() { saveUserId(userData.id) } - // Fetch and set user data + // Fetch and set user data from external auth only try { - const self = await getSelf() - if (self?.success && self.data) { - const user = self.data as User - auth.setUser(user) - - // Update user ID if not already set - if (user.id) { - saveUserId(user.id) - } - - // Restore saved language preference - const savedLang = (user as Record).language as + const heicodeUser = await getHeicodeCurrentUser() + if (heicodeUser) { + auth.setUser(heicodeUser) + saveUserId(heicodeUser.id) + const savedLang = (heicodeUser as Record).language as | string | undefined if (savedLang && savedLang !== i18n.language) { i18n.changeLanguage(savedLang) } + } else { + throw new Error('External auth session invalid') } } catch (error) { // eslint-disable-next-line no-console diff --git a/new-api/web/default/src/features/auth/hooks/use-oauth-login.ts b/new-api/web/default/src/features/auth/hooks/use-oauth-login.ts index 9b8d33c..f6e4cf3 100644 --- a/new-api/web/default/src/features/auth/hooks/use-oauth-login.ts +++ b/new-api/web/default/src/features/auth/hooks/use-oauth-login.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import { useState } from 'react' import type { AxiosRequestConfig } from 'axios' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -6,7 +6,6 @@ import { useAuthStore } from '@/stores/auth-store' import { api } from '@/lib/api' import { getOAuthState } from '../api' import { - buildGitHubOAuthUrl, buildDiscordOAuthUrl, buildOIDCOAuthUrl, buildLinuxDOOAuthUrl, @@ -23,21 +22,8 @@ type LogoutRequestConfig = AxiosRequestConfig & { export function useOAuthLogin(status: SystemStatus | null) { const { t } = useTranslation() const [isLoading, setIsLoading] = useState(false) - const [githubButtonText, setGithubButtonText] = useState('') - const [githubButtonDisabled, setGithubButtonDisabled] = useState(false) - const githubTimeoutRef = useRef(null) const { auth } = useAuthStore() - useEffect(() => { - setGithubButtonText(t('Continue with GitHub')) - - return () => { - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current) - } - } - }, [t]) - const resetSession = async () => { try { auth.reset() @@ -53,53 +39,6 @@ export function useOAuthLogin(status: SystemStatus | null) { } } - const handleGitHubLogin = async () => { - if (!status?.github_client_id) return - if (githubButtonDisabled) return - - setIsLoading(true) - setGithubButtonDisabled(true) - setGithubButtonText(t('Redirecting to GitHub...')) - - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current) - } - - githubTimeoutRef.current = setTimeout(() => { - setIsLoading(false) - setGithubButtonText( - t('Request timed out, please refresh and restart GitHub login') - ) - setGithubButtonDisabled(true) - }, 20000) - - try { - await resetSession() - const state = await getOAuthState() - if (!state) { - toast.error(t('Failed to initialize OAuth')) - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current) - } - setIsLoading(false) - setGithubButtonText(t('Continue with GitHub')) - setGithubButtonDisabled(false) - return - } - - const url = buildGitHubOAuthUrl(status.github_client_id, state) - window.open(url, '_self') - } catch (_error) { - toast.error(t('Failed to start GitHub login')) - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current) - } - setIsLoading(false) - setGithubButtonText(t('Continue with GitHub')) - setGithubButtonDisabled(false) - } - } - const handleDiscordLogin = async () => { if (!status?.discord_client_id) return @@ -205,9 +144,6 @@ export function useOAuthLogin(status: SystemStatus | null) { return { isLoading, - githubButtonText, - githubButtonDisabled, - handleGitHubLogin, handleDiscordLogin, handleOIDCLogin, handleLinuxDOLogin, diff --git a/new-api/web/default/src/features/auth/index.ts b/new-api/web/default/src/features/auth/index.ts index 85cada3..224e405 100644 --- a/new-api/web/default/src/features/auth/index.ts +++ b/new-api/web/default/src/features/auth/index.ts @@ -11,7 +11,6 @@ export { sendEmailVerification, bindEmail, getOAuthState, - githubOAuthStart, wechatLoginByCode, } from './api' @@ -58,7 +57,6 @@ export { // ============================================================================ export { - buildGitHubOAuthUrl, buildDiscordOAuthUrl, buildOIDCOAuthUrl, buildLinuxDOOAuthUrl, diff --git a/new-api/web/default/src/features/auth/lib/oauth.ts b/new-api/web/default/src/features/auth/lib/oauth.ts index bbf6ce7..a405f41 100644 --- a/new-api/web/default/src/features/auth/lib/oauth.ts +++ b/new-api/web/default/src/features/auth/lib/oauth.ts @@ -1,7 +1,6 @@ import type { SystemStatus, OAuthProvider } from '../types' export { - buildGitHubOAuthUrl, buildDiscordOAuthUrl, buildOIDCOAuthUrl, buildLinuxDOOAuthUrl, @@ -21,15 +20,6 @@ export function getAvailableOAuthProviders( const providers: OAuthProvider[] = [] - if (status.github_oauth) { - providers.push({ - name: 'GitHub', - type: 'github', - enabled: true, - clientId: status.github_client_id, - }) - } - if (status.discord_oauth) { providers.push({ name: 'Discord', @@ -75,7 +65,6 @@ export function getAvailableOAuthProviders( export function hasOAuthProviders(status: SystemStatus | null): boolean { if (!status) return false return !!( - status.github_oauth || status.discord_oauth || status.oidc_enabled || status.linuxdo_oauth || diff --git a/new-api/web/default/src/features/auth/sign-in/components/user-auth-form.tsx b/new-api/web/default/src/features/auth/sign-in/components/user-auth-form.tsx index 39d4554..fdd29ec 100644 --- a/new-api/web/default/src/features/auth/sign-in/components/user-auth-form.tsx +++ b/new-api/web/default/src/features/auth/sign-in/components/user-auth-form.tsx @@ -1,27 +1,12 @@ -import { useEffect, useMemo, useState } from 'react' +import { useState } from 'react' import type { z } from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { Link } from '@tanstack/react-router' -import { Loader2, LogIn, KeyRound } from 'lucide-react' +import { Loader2, LogIn, Mail, KeyRound } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import { - buildAssertionResult, - prepareCredentialRequestOptions, - isPasskeySupported as detectPasskeySupport, -} from '@/lib/passkey' import { cn } from '@/lib/utils' -import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Form, FormControl, @@ -31,16 +16,10 @@ import { FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' import { PasswordInput } from '@/components/password-input' -import { Turnstile } from '@/components/turnstile' -import { login, wechatLoginByCode } from '@/features/auth/api' -import { LegalConsent } from '@/features/auth/components/legal-consent' -import { OAuthProviders } from '@/features/auth/components/oauth-providers' +import { login } from '@/features/auth/api' import { loginFormSchema } from '@/features/auth/constants' import { useAuthRedirect } from '@/features/auth/hooks/use-auth-redirect' -import { useTurnstile } from '@/features/auth/hooks/use-turnstile' -import { beginPasskeyLogin, finishPasskeyLogin } from '@/features/auth/passkey' import type { AuthFormProps } from '@/features/auth/types' export function UserAuthForm({ @@ -50,50 +29,8 @@ export function UserAuthForm({ }: AuthFormProps) { const { t } = useTranslation() const [isLoading, setIsLoading] = useState(false) - const [wechatCode, setWeChatCode] = useState('') - const [agreedToLegal, setAgreedToLegal] = useState(false) - const [passkeySupported, setPasskeySupported] = useState(false) - const [isPasskeyLoading, setIsPasskeyLoading] = useState(false) - const [isWeChatDialogOpen, setIsWeChatDialogOpen] = useState(false) - const [isWeChatSubmitting, setIsWeChatSubmitting] = useState(false) - const legalConsentErrorMessage = t('Please agree to the legal terms first') const loginFailedMessage = t('Login failed') - - const { status } = useStatus() - const passkeyLoginEnabled = Boolean( - status?.passkey_login ?? status?.data?.passkey_login - ) - const { - isTurnstileEnabled, - turnstileSiteKey, - turnstileToken, - setTurnstileToken, - validateTurnstile, - } = useTurnstile() - const { handleLoginSuccess, redirectTo2FA } = useAuthRedirect() - - const hasUserAgreement = Boolean(status?.user_agreement_enabled) - const hasPrivacyPolicy = Boolean(status?.privacy_policy_enabled) - const requiresLegalConsent = hasUserAgreement || hasPrivacyPolicy - const passkeyButtonDisabled = - isPasskeyLoading || - !passkeySupported || - (requiresLegalConsent && !agreedToLegal) - const hasWeChatLogin = Boolean(status?.wechat_login) - - useEffect(() => { - if (requiresLegalConsent) { - setAgreedToLegal(false) - } else { - setAgreedToLegal(true) - } - }, [requiresLegalConsent]) - - useEffect(() => { - detectPasskeySupport() - .then(setPasskeySupported) - .catch(() => setPasskeySupported(false)) - }, []) + const { handleLoginSuccess } = useAuthRedirect() const form = useForm>({ resolver: zodResolver(loginFormSchema), @@ -103,157 +40,31 @@ export function UserAuthForm({ }, }) - const wechatQrCodeUrl = useMemo(() => { - return ( - status?.wechat_qrcode || - status?.wechat_qr_code || - status?.wechat_qrcode_image_url || - status?.wechat_qr_code_image_url || - status?.wechat_account_qrcode_image_url || - status?.WeChatAccountQRCodeImageURL || - status?.data?.wechat_qrcode || - status?.data?.WeChatAccountQRCodeImageURL || - '' - ) - }, [status]) - async function onSubmit(data: z.infer) { - if (requiresLegalConsent && !agreedToLegal) { - toast.error(legalConsentErrorMessage) - return - } - - if (!validateTurnstile()) return - setIsLoading(true) try { const res = await login({ username: data.username, password: data.password, - turnstile: turnstileToken, }) if (res.success) { - if (res.data?.require_2fa) { - redirectTo2FA() - return - } - - await handleLoginSuccess(res.data as { id?: number } | null, redirectTo) + await handleLoginSuccess( + res.data as { id?: number } | null, + redirectTo + ) toast.success(t('Welcome back!')) + } else if (res.message) { + toast.error(res.message) } - } catch (_error) { - // Errors are handled by global interceptor - } finally { - setIsLoading(false) - } - } - - const handleOpenWeChatDialog = () => { - if (requiresLegalConsent && !agreedToLegal) { - toast.error(legalConsentErrorMessage) - return - } - - setIsWeChatDialogOpen(true) - } - - const handleWeChatDialogChange = (open: boolean) => { - setIsWeChatDialogOpen(open) - if (!open) { - setWeChatCode('') - setIsWeChatSubmitting(false) - } - } - - async function handleWeChatLogin() { - if (!wechatCode.trim()) { - toast.error(t('Please enter the verification code')) - return - } - - setIsWeChatSubmitting(true) - try { - const res = await wechatLoginByCode(wechatCode) - if (res?.success) { - await handleLoginSuccess(res.data as { id?: number } | null, redirectTo) - toast.success(t('Signed in via WeChat')) - handleWeChatDialogChange(false) - } else { - toast.error(res?.message || loginFailedMessage) - } - } catch (_error) { - toast.error(loginFailedMessage) - } finally { - setIsWeChatSubmitting(false) - } - } - - async function handlePasskeyLogin() { - if (requiresLegalConsent && !agreedToLegal) { - toast.error(legalConsentErrorMessage) - return - } - - if (!passkeySupported) { - toast.error(t('Passkey is not supported on this device')) - return - } - - if (!navigator?.credentials) { - toast.error(t('Passkey is not available in this browser')) - return - } - - setIsPasskeyLoading(true) - try { - const begin = await beginPasskeyLogin() - if (!begin.success) { - throw new Error(begin.message || t('Failed to start Passkey login')) - } - - const publicKey = prepareCredentialRequestOptions( - begin.data?.options ?? begin.data - ) - - const credential = (await navigator.credentials.get({ - publicKey, - })) as PublicKeyCredential | null - - if (!credential) { - toast.info(t('Passkey login was cancelled')) - return - } - - const assertion = buildAssertionResult(credential) - if (!assertion) { - throw new Error(t('Invalid Passkey response')) - } - - const finish = await finishPasskeyLogin(assertion) - if (!finish.success) { - throw new Error(finish.message || t('Failed to complete Passkey login')) - } - - if (!finish.data) { - throw new Error(t('Missing user data from Passkey login response')) - } - - await handleLoginSuccess( - finish.data as { id?: number } | null, - redirectTo - ) - toast.success(t('Signed in with Passkey')) - } catch (error: unknown) { - if (error instanceof DOMException && error.name === 'NotAllowedError') { - toast.info(t('Passkey login was cancelled or timed out')) - } else if (error instanceof Error) { + } catch (error) { + if (error instanceof Error) { toast.error(error.message) } else { - toast.error(t('Passkey login failed')) + toast.error(loginFailedMessage) } } finally { - setIsPasskeyLoading(false) + setIsLoading(false) } } @@ -261,175 +72,74 @@ export function UserAuthForm({
- {/* Username Field */} ( - {t('Username or Email')} + + {t('Email')} + - +
+ + +
)} /> - {/* Password Field */} ( - - {t('Password')} + + + {t('Password')} + - +
+ + +
- - {t('Forgot password?')} -
)} /> - {/* Submit Button */} - {/* Turnstile */} - {isTurnstileEnabled && ( -
- -
- )} - - - - {passkeyLoginEnabled && ( -
- - {!passkeySupported && ( -

- {t('Passkey is not supported on this device.')} -

- )} -
- )} - - {/* OAuth Providers */} - +

+ {t('Authenticated via Heicode identity service')} +

- - {hasWeChatLogin && ( - - - - {t('WeChat sign in')} - - {t( - 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' - )} - - - - {wechatQrCodeUrl ? ( -
- {t('WeChat -
- ) : ( -

- {t('QR code is not configured. Please contact support.')} -

- )} - -
- - setWeChatCode(event.target.value)} - autoComplete='one-time-code' - /> -
- - - - - -
-
- )} ) } diff --git a/new-api/web/default/src/features/auth/sign-in/index.tsx b/new-api/web/default/src/features/auth/sign-in/index.tsx index 75d2439..649bbb9 100644 --- a/new-api/web/default/src/features/auth/sign-in/index.tsx +++ b/new-api/web/default/src/features/auth/sign-in/index.tsx @@ -1,43 +1,50 @@ -import { Link, useSearch } from '@tanstack/react-router' +import { useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { useStatus } from '@/hooks/use-status' +import { ArrowUpRight } from 'lucide-react' import { AuthLayout } from '../auth-layout' -import { TermsFooter } from '../components/terms-footer' import { UserAuthForm } from './components/user-auth-form' export function SignIn() { const { t } = useTranslation() const { redirect } = useSearch({ from: '/(auth)/sign-in' }) - const { status } = useStatus() return ( -
-
-

- {t('Sign in')} + ) diff --git a/new-api/web/default/src/features/dashboard/components/cockpit/index.tsx b/new-api/web/default/src/features/dashboard/components/cockpit/index.tsx new file mode 100644 index 0000000..c69c8ac --- /dev/null +++ b/new-api/web/default/src/features/dashboard/components/cockpit/index.tsx @@ -0,0 +1,447 @@ +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { + Activity, + AlertTriangle, + ArrowUpRight, + Building2, + CheckCircle2, + CircleDashed, + PlayCircle, + Rocket, + ScrollText, + ShieldCheck, + StopCircle, + Wallet, +} from 'lucide-react' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { + getAgnetAuditLogs, + listAgnetDeployments, + type AgnetDeployment, +} from '@/features/agnet-console/api' + +type StatusKey = 'running' | 'success' | 'failed' | 'pending' + +const STATUS_TO_KEY: Record = { + running: 'running', + active: 'running', + in_progress: 'running', + succeeded: 'success', + success: 'success', + completed: 'success', + failed: 'failed', + error: 'failed', + rejected: 'failed', + pending: 'pending', + queued: 'pending', + awaiting: 'pending', +} + +function classifyStatus(status: string): StatusKey { + return STATUS_TO_KEY[(status || '').toLowerCase()] ?? 'pending' +} + +function StatCard({ + label, + value, + hint, + icon: Icon, + tone, +}: { + label: string + value: string + hint?: string + icon: React.ComponentType<{ className?: string }> + tone: 'primary' | 'success' | 'warn' | 'danger' | 'muted' +}) { + const toneClass: Record = { + primary: + 'bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] text-primary', + success: + 'bg-[color-mix(in_oklch,oklch(0.78_0.18_150)_22%,transparent)] text-emerald-400', + warn: + 'bg-[color-mix(in_oklch,oklch(0.85_0.16_85)_22%,transparent)] text-amber-400', + danger: + 'bg-[color-mix(in_oklch,oklch(0.7_0.21_25)_22%,transparent)] text-rose-400', + muted: + 'bg-[color-mix(in_oklch,var(--muted)_60%,transparent)] text-muted-foreground', + } + + return ( +
+
+
+ +
+

+ {label} +

+
+

{value}

+ {hint &&

{hint}

} +
+ ) +} + +function PhasePill({ phase }: { phase: string }) { + const k = classifyStatus(phase) + const map: Record = { + running: + 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40', + success: + 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30', + failed: 'bg-rose-500/15 text-rose-400 ring-rose-500/30', + pending: + 'bg-[color-mix(in_oklch,var(--muted)_60%,transparent)] text-muted-foreground ring-border', + } + return ( + + + {phase || 'unknown'} + + ) +} + +function formatRelative(value: string | number | undefined): string { + if (!value) return '—' + const date = typeof value === 'number' ? new Date(value * 1000) : new Date(value) + if (Number.isNaN(date.getTime())) return String(value) + const diffMs = date.getTime() - Date.now() + const abs = Math.abs(diffMs) + const sec = Math.round(abs / 1000) + if (sec < 60) return `${sec}s` + const min = Math.round(sec / 60) + if (min < 60) return `${min}m` + const hr = Math.round(min / 60) + if (hr < 24) return `${hr}h` + const day = Math.round(hr / 24) + return `${day}d` +} + +export function CockpitView() { + const { t } = useTranslation() + + const deploymentsQuery = useQuery({ + queryKey: ['cockpit', 'deployments'], + queryFn: listAgnetDeployments, + refetchInterval: 30_000, + }) + + const auditQuery = useQuery({ + queryKey: ['cockpit', 'audit'], + queryFn: getAgnetAuditLogs, + refetchInterval: 60_000, + }) + + const stats = useMemo(() => { + const list: AgnetDeployment[] = deploymentsQuery.data ?? [] + const counters: Record = { + running: 0, + success: 0, + failed: 0, + pending: 0, + } + list.forEach((d) => { + counters[classifyStatus(d.status || d.phase || '')]++ + }) + const total = list.length || 1 + const successRate = Math.round( + ((counters.success + counters.running) / total) * 100 + ) + return { + list, + counters, + successRate: Number.isFinite(successRate) ? successRate : 0, + } + }, [deploymentsQuery.data]) + + const recentDeployments = useMemo( + () => + [...stats.list] + .sort((a, b) => + (b.updated_at || b.created_at || '').localeCompare( + a.updated_at || a.created_at || '' + ) + ) + .slice(0, 5), + [stats.list] + ) + + const auditFeed = useMemo(() => { + const items = (auditQuery.data ?? []) as Array> + return items.slice(0, 8) + }, [auditQuery.data]) + + return ( +
+ {/* Tier 1: Status */} +
+ + + + +
+ + {/* Tier 2: Stream */} +
+
+
+
+

+ {t('Recent deployments')} +

+

+ {t('Live code delivery runs')} +

+
+ +
+ + {deploymentsQuery.isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, idx) => ( +
+ ))} +
+ ) : recentDeployments.length === 0 ? ( +
+ {t('No deployments yet. Trigger a run to populate the cockpit.')} +
+ ) : ( +
    + {recentDeployments.map((dep) => { + const tenant = dep.orchestration_plan?.metadata?.tenant_id || '—' + const objective = + dep.orchestration_plan?.objective || + dep.orchestration_plan?.template_hint || + t('No objective') + return ( +
  • +
    +
    + + + {dep.deployment_id} + + +
    +

    + {objective} +

    +

    + + {tenant} · {formatRelative(dep.updated_at || dep.created_at)} ago +

    +
    + +
  • + ) + })} +
+ )} +
+ +
+
+
+

+ {t('Audit timeline')} +

+

+ {t('Tenant-scoped activity')} +

+
+ +
+ + {auditQuery.isLoading ? ( +
+ {Array.from({ length: 5 }).map((_, idx) => ( +
+ ))} +
+ ) : auditFeed.length === 0 ? ( +
+ {t('No audit events captured yet.')} +
+ ) : ( +
    + {auditFeed.map((entry, idx) => { + const action = String(entry.action || entry.event || 'event') + const actor = String(entry.actor || entry.user || 'system') + const occurred = String(entry.occurred_at || entry.timestamp || '') + return ( +
  1. + +

    + {action} +

    +

    + {actor} +

    +

    + {occurred ? `${formatRelative(occurred)} ago` : '—'} +

    +
  2. + ) + })} +
+ )} +
+
+ + {/* Tier 3: Action */} +
+
+
+

+ {t('Quick actions')} +

+

+ {t('Common code-delivery actions')} +

+
+ + + {t('Control plane online')} + +
+ +
+ + + + +
+
+
+ ) +} + +function ActionTile({ + icon: Icon, + title, + desc, + to, +}: { + icon: React.ComponentType<{ className?: string }> + title: string + desc: string + to: '/deployments' | '/audit' | '/sk-sources' +}) { + return ( + +
+ +
+
+

{title}

+

+ {desc} +

+
+ + {title} + + + + ) +} diff --git a/new-api/web/default/src/features/dashboard/index.tsx b/new-api/web/default/src/features/dashboard/index.tsx index ee6ed59..d601e4f 100644 --- a/new-api/web/default/src/features/dashboard/index.tsx +++ b/new-api/web/default/src/features/dashboard/index.tsx @@ -6,17 +6,9 @@ import { ROLE } from '@/lib/roles' import { Skeleton } from '@/components/ui/skeleton' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { SectionPageLayout } from '@/components/layout' -import { - CardStaggerContainer, - CardStaggerItem, - FadeIn, -} from '@/components/page-transition' +import { FadeIn } from '@/components/page-transition' +import { CockpitView } from './components/cockpit' import { ModelsFilter } from './components/models/models-filter-dialog' -import { AnnouncementsPanel } from './components/overview/announcements-panel' -import { ApiInfoPanel } from './components/overview/api-info-panel' -import { FAQPanel } from './components/overview/faq-panel' -import { SummaryCards } from './components/overview/summary-cards' -import { UptimePanel } from './components/overview/uptime-panel' import { DEFAULT_TIME_GRANULARITY } from './constants' import { type DashboardSectionId, @@ -86,16 +78,18 @@ const SECTION_META: Record< { titleKey: string; descriptionKey: string } > = { overview: { - titleKey: 'Overview', - descriptionKey: 'View dashboard overview and statistics', + titleKey: 'Cockpit', + descriptionKey: + 'Live status, recent runs, audit timeline, and quick actions for your tenants.', }, models: { - titleKey: 'Model Call Analytics', - descriptionKey: 'View model call count analytics and charts', + titleKey: 'Model usage analytics', + descriptionKey: + 'Operational telemetry for upstream model usage (read-only).', }, users: { - titleKey: 'User Analytics', - descriptionKey: 'View user consumption statistics and charts', + titleKey: 'Tenant member analytics', + descriptionKey: 'Per-tenant member consumption and request distribution.', }, } @@ -174,25 +168,7 @@ export function Dashboard() { )} - {activeSection === 'overview' && ( - <> - - - - - - - - - - - - - - - - - )} + {activeSection === 'overview' && } {activeSection === 'models' && ( <> diff --git a/new-api/web/default/src/features/home/components/gateway-card.tsx b/new-api/web/default/src/features/home/components/gateway-card.tsx index 6019afc..0ac0835 100644 --- a/new-api/web/default/src/features/home/components/gateway-card.tsx +++ b/new-api/web/default/src/features/home/components/gateway-card.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { Separator } from '@/components/ui/separator' -import { getGatewayFeatures } from '../constants' +import { getControlPlaneFeatures } from '../constants' interface GatewayCardProps { logo: string @@ -8,11 +8,11 @@ interface GatewayCardProps { } /** - * Central gateway card with features grid + * Central control-plane card with capabilities grid */ export function GatewayCard({ logo, systemName }: GatewayCardProps) { const { t } = useTranslation() - const features = getGatewayFeatures(t) + const features = getControlPlaneFeatures(t) return (
@@ -23,7 +23,7 @@ export function GatewayCard({ logo, systemName }: GatewayCardProps) {
- {/* Gateway Header */} + {/* Control-plane Header */}

- {t('Ready to simplify')} + {'准备好提升'}
- {t('your AI integration?')} + {'团队 Code 交付能力了吗?'}

- {t('Start for free with generous limits. No credit card required.')} + {'开通租户后即可进入代码任务执行、质量验证与发布回溯视图。'}

-

diff --git a/new-api/web/default/src/features/home/components/sections/features.tsx b/new-api/web/default/src/features/home/components/sections/features.tsx index d414cee..87a2723 100644 --- a/new-api/web/default/src/features/home/components/sections/features.tsx +++ b/new-api/web/default/src/features/home/components/sections/features.tsx @@ -22,10 +22,8 @@ export function Features(_props: FeaturesProps) { { id: 'fast', num: '01', - title: t('Lightning Fast'), - desc: t( - 'Optimized network architecture ensures millisecond response times' - ), + title: '编排响应快速', + desc: '任务状态秒级回传,适合多角色并行协作与持续迭代', span: 'md:col-span-2', icon: , visual: ( @@ -46,10 +44,8 @@ export function Features(_props: FeaturesProps) { { id: 'secure', num: '02', - title: t('Secure & Reliable'), - desc: t( - 'Enterprise-grade security with comprehensive permission management' - ), + title: '租户隔离可信', + desc: '企业级权限模型,保障跨租户数据边界与操作可审计', span: 'md:col-span-1', icon: , visual: ( @@ -83,13 +79,13 @@ export function Features(_props: FeaturesProps) { { id: 'global', num: '03', - title: t('Global Coverage'), - desc: t('Multi-region deployment for stable global access'), + title: '全局事件可见', + desc: '统一展示运行态、异常与审计线索,便于值班与回溯', span: 'md:col-span-1', icon: , visual: (
- {[t('Load Balancing'), t('Rate Limiting'), t('Cost Tracking')].map( + {['部署态势', '异常优先', '审计闭环'].map( (step, i) => (
, visual: (
- {['API', 'SDK', 'CLI', 'Docs'].map((n) => ( + {['Deploy', 'Events', 'SK', 'Audit'].map((n) => (
- {t('OpenAI Compatible')} + {'Heicode Manager Control Plane'}
), @@ -140,23 +136,23 @@ export function Features(_props: FeaturesProps) { const additionalFeatures = [ { icon: , - title: t('High Performance'), - desc: t('Support for high concurrency with automatic load balancing'), + title: '执行韧性', + desc: '失败可定位、状态可见、操作可回滚', }, { icon: , - title: t('Transparent Billing'), - desc: t('Pay-as-you-go with real-time usage monitoring'), + title: '预算感知', + desc: '在任务流内感知预算消耗与风险变化', }, { icon: , - title: t('Team Collaboration'), - desc: t('Multi-user management with flexible permission allocation'), + title: '团队协同', + desc: '多角色职责分离,统一租户视角协作', }, { icon: , - title: t('Technical Support'), - desc: t('Professional team providing 24/7 technical support'), + title: '可运维性', + desc: '事件、快照、审计三位一体支持问题复盘', }, ] @@ -165,12 +161,12 @@ export function Features(_props: FeaturesProps) {

- {t('Core Features')} + {'核心能力'}

- {t('Built for developers,')} + {'为团队交付而设计,'}
- {t('designed for scale')} + {'为多租户管控而生'}

diff --git a/new-api/web/default/src/features/home/components/sections/hero.tsx b/new-api/web/default/src/features/home/components/sections/hero.tsx index 34daf4f..1fbc338 100644 --- a/new-api/web/default/src/features/home/components/sections/hero.tsx +++ b/new-api/web/default/src/features/home/components/sections/hero.tsx @@ -39,20 +39,18 @@ export function Hero(props: HeroProps) { className='landing-animate-fade-up text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight' style={{ animationDelay: '0ms' }} > - {t('Unified API Gateway for')} + {'高强度 Code 交付中枢'}
- {t('All Your AI Models')} + {'团队级研发执行与质量控制台'}

- {systemName}{' '} - {t( - 'aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.' - )} + {systemName} + {' 聚焦需求到代码到发布的全链路执行能力:任务推进、质量门禁、发布回溯一体化。'}

- )}
diff --git a/new-api/web/default/src/features/home/components/sections/how-it-works.tsx b/new-api/web/default/src/features/home/components/sections/how-it-works.tsx index 05e772c..4a9b231 100644 --- a/new-api/web/default/src/features/home/components/sections/how-it-works.tsx +++ b/new-api/web/default/src/features/home/components/sections/how-it-works.tsx @@ -8,24 +8,20 @@ export function HowItWorks() { const steps = [ { num: '1', - title: t('Configure'), - desc: t( - 'Add your API keys, set up channels and configure access permissions' - ), + title: '接入租户', + desc: '使用外部统一认证登录,自动加载 tenant、channel 与角色权限', icon: , }, { num: '2', - title: t('Connect'), - desc: t( - 'Use our unified OpenAI-compatible endpoint in your applications' - ), + title: '推进代码任务', + desc: '在 Deployments 发起代码交付任务,自动串联实现、验证与发布门禁', icon: , }, { num: '3', - title: t('Monitor'), - desc: t('Track usage, costs and performance with real-time analytics'), + title: '持续验证', + desc: '通过 Events 与 Audit 观察质量信号、失败根因与发布回溯链路', icon: , }, ] @@ -35,10 +31,10 @@ export function HowItWorks() {

- {t('How It Works')} + {'使用流程'}

- {t('Three steps to get started')} + {'三步进入 Heicode Manager'}

diff --git a/new-api/web/default/src/features/home/components/sections/stats.tsx b/new-api/web/default/src/features/home/components/sections/stats.tsx index acd3bea..57a9661 100644 --- a/new-api/web/default/src/features/home/components/sections/stats.tsx +++ b/new-api/web/default/src/features/home/components/sections/stats.tsx @@ -73,10 +73,10 @@ export function Stats(_props: StatsProps) { const { t } = useTranslation() const stats = [ - { end: 100, suffix: 'M+', label: t('requests served') }, - { end: 50, suffix: '+', label: t('AI models supported') }, - { end: 99.9, suffix: '%', label: t('uptime'), decimals: 1 }, - { end: 10, suffix: 'K+', label: t('active users') }, + { end: 100, suffix: 'K+', label: '已编排任务' }, + { end: 50, suffix: '+', label: '接入租户' }, + { end: 99.9, suffix: '%', label: '可用性', decimals: 1 }, + { end: 10, suffix: 'K+', label: '审计事件' }, ] return ( diff --git a/new-api/web/default/src/features/home/constants.ts b/new-api/web/default/src/features/home/constants.ts index 743c29b..b59f4d0 100644 --- a/new-api/web/default/src/features/home/constants.ts +++ b/new-api/web/default/src/features/home/constants.ts @@ -25,18 +25,18 @@ export const AI_MODELS = [ 'Gemini.Color', ] as const -// Hero section - Gateway Features -export const GATEWAY_FEATURES = [ - 'Cost Tracking', - 'Model Access', - 'Guardrails', - 'Observability', - 'Budgets', - 'Load Balancing', - 'Rate Limiting', - 'Token Mgmt', - 'Prompt Caching', - 'Pass-Through', +// Hero section - Control-plane capability tags +export const CONTROL_PLANE_FEATURES = [ + 'Deployments', + 'Events', + 'SK Snapshots', + 'Audit', + 'Tenant Isolation', + 'Risk Control', + 'Budget Guard', + 'Traceability', + 'Policy Gates', + 'Ops Visibility', ] as const // Stats section - Default statistics @@ -109,8 +109,8 @@ export const DEFAULT_FEATURES = [ }, ] as const -export function getGatewayFeatures(t: TFunction) { - return GATEWAY_FEATURES.map((feature) => t(feature)) +export function getControlPlaneFeatures(t: TFunction) { + return CONTROL_PLANE_FEATURES.map((feature) => t(feature)) } export function getDefaultStats(t: TFunction) { diff --git a/new-api/web/default/src/features/profile/components/tabs/account-bindings-tab.tsx b/new-api/web/default/src/features/profile/components/tabs/account-bindings-tab.tsx index fd42b4c..5cc1a5b 100644 --- a/new-api/web/default/src/features/profile/components/tabs/account-bindings-tab.tsx +++ b/new-api/web/default/src/features/profile/components/tabs/account-bindings-tab.tsx @@ -1,11 +1,10 @@ import { useEffect, useMemo, useState, useCallback } from 'react' import { Mail, Shield, Send, Link2, Unlink } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { SiGithub, SiWechat, SiLinux } from 'react-icons/si' +import { SiWechat, SiLinux } from 'react-icons/si' import { toast } from 'sonner' import { IconDiscord } from '@/assets/brand-icons' import { - handleGitHubOAuth, handleOIDCOAuth, handleDiscordOAuth, handleLinuxDOOAuth, @@ -153,23 +152,6 @@ export function AccountBindingsTab({ isEnabled: status?.wechat_login || false, onBind: () => dialogs.open('wechat'), }, - { - id: 'github', - label: t('GitHub'), - icon: SiGithub, - value: (profile as unknown as Record).github_id as - | string - | undefined, - isBound: Boolean( - (profile as unknown as Record).github_id - ), - isEnabled: status?.github_oauth || false, - onBind: () => { - if (status?.github_client_id) { - handleGitHubOAuth(status.github_client_id) - } - }, - }, { id: 'discord', label: t('Discord'), diff --git a/new-api/web/default/src/features/system-settings/general/index.tsx b/new-api/web/default/src/features/system-settings/general/index.tsx index 31ce67d..fef215c 100644 --- a/new-api/web/default/src/features/system-settings/general/index.tsx +++ b/new-api/web/default/src/features/system-settings/general/index.tsx @@ -1,6 +1,7 @@ import { useParams } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { parseCurrencyDisplayType } from '@/lib/currency' +import { BRAND_NAME } from '@/lib/brand' import { useSystemOptions, getOptionValue } from '../hooks/use-system-options' import type { GeneralSettings } from '../types' import { @@ -11,7 +12,7 @@ import { const defaultGeneralSettings: GeneralSettings = { 'theme.frontend': 'default', Notice: '', - SystemName: 'Heicode Manager', + SystemName: BRAND_NAME, Logo: '', Footer: '', About: '', diff --git a/new-api/web/default/src/features/system-settings/general/system-info-section.tsx b/new-api/web/default/src/features/system-settings/general/system-info-section.tsx index 19a1747..1e0d480 100644 --- a/new-api/web/default/src/features/system-settings/general/system-info-section.tsx +++ b/new-api/web/default/src/features/system-settings/general/system-info-section.tsx @@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useHiddenClickUnlock } from '@/hooks/use-hidden-click-unlock' +import { BRAND_NAME } from '@/lib/brand' import { Button } from '@/components/ui/button' import { Form, @@ -201,7 +202,7 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { {t('System Name')} - + {t('The name displayed across the application')} @@ -305,7 +306,7 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { {t('Home Page Content')}