Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions app/api/admin/channel/type-name/route.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
import { NextRequest, NextResponse } from 'next/server'

import { ChannelTypeMapName } from '@/types/admin/channels/channelInfo'
import { ChannelType, ChannelTypeMapName } from '@/types/admin/channels/channelInfo'
import { ApiProxyBackendResp, ApiResp } from '@/types/api'
import { parseJwtToken } from '@/utils/backend/auth'
import { isAdmin } from '@/utils/backend/isAdmin'

export const dynamic = 'force-dynamic'

type ApiProxyBackendChannelTypeMapNameResponse = ApiProxyBackendResp<ChannelTypeMapName>
type ApiProxyBackendChannelTypeMetasResponse = ApiProxyBackendResp<
Record<string, { name?: string }>
>

export type GetChannelTypeNamesResponse = ApiResp<ChannelTypeMapName>

async function fetchChannelTypeNames(): Promise<ChannelTypeMapName | undefined> {
try {
const typeMetasUrl = new URL(
`/api/channels/type_metas`,
global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy
)
const token = global.AppConfig?.auth.aiProxyBackendKey
const typeMetasResponse = await fetch(typeMetasUrl.toString(), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `${token}`,
},
cache: 'no-store',
})

if (typeMetasResponse.ok) {
const result: ApiProxyBackendChannelTypeMetasResponse = await typeMetasResponse.json()
if (!result.success) {
throw new Error(result.message || 'admin channels api:ai proxy backend error')
}
return Object.entries(result.data || {}).reduce<ChannelTypeMapName>((acc, [type, meta]) => {
if (meta.name) {
acc[type as ChannelType] = meta.name
}
return acc
}, {})
}

const url = new URL(
`/api/channels/type_names`,
global.AppConfig?.backend.aiproxyInternal || global.AppConfig?.backend.aiproxy
)
const token = global.AppConfig?.auth.aiProxyBackendKey
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Expand Down
2 changes: 1 addition & 1 deletion app/api/admin/option/batch/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async function batchOption(batchOptionData: BatchOptionData): Promise<string> {

const token = global.AppConfig?.auth.aiProxyBackendKey
const response = await fetch(url.toString(), {
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `${token}`,
Expand Down
198 changes: 198 additions & 0 deletions artifacts/deploy-adjust/aiproxy-cluster-20260530.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# AIProxy Sealos Deploy Adjust Report

- Overall: PASS
- Date: 2026-05-30 CST
- Target: `id=<cluster-id>`, `ip=<cluster-ip>`
- Cluster domain: `<cluster-domain>`
- Branch: `codex/aiproxy-deploy-adjust`
- Baseline command: `sealos run ghcr.io/sealos-apps/aiproxy/aiproxy-cluster:sha-5db063c`
- Final validation image: `<validation-image>`

## Inputs

| Item | Value |
|---|---|
| Base package | `sealos-pro-v5.1.2-rc5-amd64.tar` |
| Addon package | `admin-cluster-v0.1.0-amd64.tar` |
| App package | `ghcr.io/sealos-apps/aiproxy/aiproxy-cluster:sha-5db063c` |
| Namespace | `aiproxy-system` |
| UI validation | AIProxy dashboard, global configs, price/model page |
| API validation | `/v1/models`, `/v1/chat/completions` |
| Test channel | `<test-channel-name>` |
| Test model | `claude-3-5-haiku-20241022` |

Signed package URLs and AI provider credentials are intentionally not recorded in this report.

## Baseline Result

The baseline install command completed, but `aiproxy-web` failed at runtime:

```text
Error: Cannot find module '/app/server.js'
```

Image inspection showed the standalone server was generated below `/app/app/server.js`, while the Dockerfile starts `node server.js` from `/app`.

After the web pod was repaired, the channel form still failed because the frontend requested the removed backend endpoint `/api/channels/type_names`. The running AIProxy backend exposes `/api/channels/type_metas`.

The global default-model save path also failed through the frontend because the backend supports `POST /api/option/batch`, while the frontend proxy was calling it with `PUT`.

## Code Fixes

| File | Change | Result |
|---|---|---|
| `next.config.js` | Set `experimental.outputFileTracingRoot` to `__dirname` so Next standalone output contains root `server.js`. | PASS |
| `app/api/admin/channel/type-name/route.ts` | Prefer `/api/channels/type_metas` and map metadata to the existing frontend type-name shape. Keep `/type_names` fallback. | PASS |
| `app/api/admin/option/batch/route.ts` | Keep frontend `PUT` route, but proxy to backend with `POST /api/option/batch`. | PASS |

## Rebuild And Redeploy Evidence

```bash
rsync -a --delete --exclude '.git' --exclude 'node_modules' --exclude '.next' <repo-root>/ root@<cluster-ip>:<remote-build-dir>/
ssh root@<cluster-ip> 'cd <remote-build-dir> && sealos build --platform linux/amd64 -t <validation-image> -f Dockerfile .'
ssh root@<cluster-ip> 'ctr -n k8s.io images import <validation-image-archive>'
ssh root@<cluster-ip> 'kubectl -n aiproxy-system set image deploy/aiproxy-web aiproxy=<validation-image>'
ssh root@<cluster-ip> 'kubectl -n aiproxy-system rollout status deploy/aiproxy-web --timeout=180s'
```

Expected result: rollout completes and `aiproxy-web` logs show `Ready`.

Observed result:

```text
deployment "aiproxy-web" successfully rolled out
Next.js 14.2.35
Ready in 591ms
```

## UI Validation

Browser validation used real page navigation, clicks, fills, and submit actions against:

```text
https://aiproxy-web.<cluster-domain>/zh/dashboard
```

Because the app is normally launched inside Sealos Desktop, the browser test injected a short-lived `ns-admin` app JWT into page API requests. Channel configuration itself was performed through the AIProxy pages, not by calling backend channel APIs directly. Screenshots intentionally avoid the moment where the provider key is visible in the form.

| Step | Expected | Result | Evidence |
|---|---|---|---|
| Open AIProxy dashboard | Dashboard loads and shows the test channel. | PASS | `artifacts/deploy-adjust/images/01-dashboard-channel-list.png` |
| Configure/save channel from the page | Existing Anthropic channel remains enabled and saved with the current provider config. | PASS | `artifacts/deploy-adjust/images/02-dashboard-channel-updated.png` |
| Open global configs | Default-model configuration page loads. | PASS | `artifacts/deploy-adjust/images/03-global-configs-default-model.png` |
| Open price/model page | `claude-3-5-haiku-20241022` is visible as an enabled model. | PASS | `artifacts/deploy-adjust/images/04-price-enabled-model.png` |

## API Validation

The user key created in the `ns-admin` workspace was used for model and chat-completion verification.

```bash
curl -k -H "Authorization: Bearer <user-api-key>" \
https://aiproxy.<cluster-domain>/v1/models
```

Expected result: `claude-3-5-haiku-20241022` is listed.

Observed result: PASS.

```bash
curl -k -H "Authorization: Bearer <user-api-key>" \
-H 'Content-Type: application/json' \
-d '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":"reply exactly: ok"}],"max_tokens":16}' \
https://aiproxy.<cluster-domain>/v1/chat/completions
```

Expected result: HTTP 200 and assistant content `ok`.

Observed result:

```json
{"model":"claude-3-5-haiku-20241022","content":"ok","total_tokens":11}
```

## Cluster And Legacy Data Checks

| Check | Expected | Result |
|---|---|---|
| Node health | `pve-<cluster-id>` Ready | PASS |
| Non-running pods | None | PASS |
| Failed Helm releases | None | PASS |
| AIProxy Helm releases | `aiproxy`, `aiproxy-database`, `aiproxy-web` deployed | PASS |
| Current channel list | Only the Helm-backed validation channel remains; no `probe-*` leftovers | PASS |
| aiproxy-web image | `<validation-image>` | PASS |
| `/api/status` | HTTP 200 | PASS |
| `/api/init-app-config` | HTTP 200 | PASS |

Sanitized channel state:

```text
id=<channel-id> name=<test-channel-name> type=14 status=1 models=[]
```

## Offline Image Check

Allowed prefixes:

- `sealos.hub:5000/`
- `hub.<cluster-domain>/`

Command:

```bash
CLOUD_DOMAIN=<cluster-domain>
crictl images | awk -v domain="$CLOUD_DOMAIN" '
NR==1 { next }
{
image=$1
hub_host="hub." domain
allowed_hub_domain=(domain != "" && (image == hub_host || index(image, hub_host "/") == 1))
if (image !~ /^sealos\.hub:5000(\/|$)/ && !allowed_hub_domain) {
print image
}
}
' | sort -u
```

Expected result: no output.

Observed result: PASS.

## Local Verification

```bash
pnpm -s build
pnpm -s exec tsc --noEmit --pretty false
git diff --check
pnpm -s exec prettier --check app/api/admin/channel/type-name/route.ts app/api/admin/option/batch/route.ts next.config.js
helm lint deploy/charts/aiproxy-web
helm template aiproxy-web deploy/charts/aiproxy-web >/tmp/aiproxy-web-helm-template.yaml
```

All commands passed. Existing build warnings remain: React Hook dependency warnings, webpack dynamic dependency warning from `web-worker`, localStorage experimental warnings, and the existing Next dynamic server usage warning during prerender.

## Manual Retest

```bash
ssh root@<cluster-ip> 'kubectl -n aiproxy-system get pods'
ssh root@<cluster-ip> 'kubectl -n aiproxy-system logs deploy/aiproxy-web --tail=80'
curl -k https://aiproxy.<cluster-domain>/api/status
curl -k https://aiproxy-web.<cluster-domain>/api/init-app-config
```

Expected result: all AIProxy pods are Running, web logs include `Ready`, and both HTTP checks return 200.

For AI verification, create or reuse an `ns-admin` API key and call:

```bash
curl -k -H "Authorization: Bearer <user-api-key>" \
-H 'Content-Type: application/json' \
-d '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":"reply exactly: ok"}],"max_tokens":16}' \
https://aiproxy.<cluster-domain>/v1/chat/completions
```

Expected result: HTTP 200 with assistant content `ok`.

## Risk Notes

- Local macOS trust setup via `cert.sh` fetched the cluster CA but `security add-trusted-cert` did not make curl/browser trust effective. Browser/API validation used explicit TLS ignore flags.
- The target cluster was patched with a validation image built from this branch. Production use requires the CI-built runtime image and Sealos image for this branch.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions artifacts/deploy-adjust/test-cases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# AIProxy Sealos 修复验证用例集

- 测试日期:2026-05-30
- 测试集群:`ID=<cluster-id>`,`<cluster-ip>`
- 集群域名:`<cluster-domain>`
- 测试分支:`codex/aiproxy-deploy-adjust`
- 基线部署命令:`sealos run ghcr.io/sealos-apps/aiproxy/aiproxy-cluster:sha-5db063c`
- 修复验证镜像:`<validation-image>`

## 用例列表

| 用例 | 目标 | 结果 | 截图/证据 |
|---|---|---|---|
| [TC-01 Web 启动修复](./TC-01-web-standalone-startup.md) | 验证 `aiproxy-web` 修复后能启动并完成 rollout。 | 通过 | 命令输出 |
| [TC-02 页面配置渠道](./TC-02-channel-page-config.md) | 验证通过 AIProxy 管理中心页面配置并保存渠道。 | 通过 | `images/01-dashboard-channel-list.png`、`images/02-dashboard-channel-updated.png` |
| [TC-03 页面访问渠道模型](./TC-03-model-and-price-page.md) | 验证页面能看到刚才渠道可用的模型。 | 通过 | `images/03-global-configs-default-model.png`、`images/04-price-enabled-model.png` |
| [TC-04 用户 Key 调用 AI 能力](./TC-04-user-key-ai-api.md) | 验证用户申请的 key 能通过配置渠道完成 AI 调用。 | 通过 | API 输出 |
| [TC-05 Helm 与遗留数据检查](./TC-05-helm-and-legacy-data.md) | 验证当前部署由 Helm 管理,无失败 release 和残留测试渠道。 | 通过 | 命令输出 |
| [TC-06 离线镜像检查](./TC-06-offline-image-check.md) | 验证节点镜像只来自允许的本地仓库前缀。 | 通过 | 命令输出 |
| [TC-07 本地构建与图表检查](./TC-07-local-build-and-chart-check.md) | 验证正确目录中的代码、类型、格式和 Helm 图表。 | 通过 | 命令输出 |
| [TC-08 流水线验证](./TC-08-github-actions.md) | 验证正确仓库分支的官方构建流水线通过。 | 通过 | GitHub Actions |

## 截图说明

截图只保留页面状态和验证结果,已打码测试渠道名,不保留 AI provider key、用户 API key、JWT、后端 admin key 或签名下载地址。

## 总结论

基线包部署后暴露出三个问题:Web standalone 输出路径错误、渠道类型接口与当前后端不兼容、全局配置批量保存代理方法错误。修复后,页面配置、模型访问、用户 key 调用 AI、Helm 状态、离线镜像、本地验证和官方流水线均通过。
47 changes: 47 additions & 0 deletions artifacts/deploy-adjust/test-cases/TC-01-web-standalone-startup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# TC-01 Web 启动修复

## 测试目标

验证 `sealos run ghcr.io/sealos-apps/aiproxy/aiproxy-cluster:sha-5db063c` 部署后的 `aiproxy-web` 启动问题已经修复,Web Pod 能正常启动并完成 rollout。

## 前置条件

- Sealos 集群已安装完成:`<cluster-ip>`
- AIProxy 基线包已经执行过:`sealos run ghcr.io/sealos-apps/aiproxy/aiproxy-cluster:sha-5db063c`
- 在正确仓库目录 `<repo-root>` 的 `codex/aiproxy-deploy-adjust` 分支构建验证镜像

## 关联代码修改

- `next.config.js:12`:将 `experimental.outputFileTracingRoot` 设置为 `__dirname`,确保 Next standalone 输出中存在容器启动命令需要的 `/app/server.js`。

## 测试流程

1. 观察基线部署后的 Web Pod 日志。
2. 使用当前修复分支构建验证镜像。
3. 将 `aiproxy-web` Deployment 镜像切换到修复后的验证镜像。
4. 等待 Deployment rollout 完成。
5. 查看 Web 日志是否进入 Ready 状态。

## 命令证据

```bash
ssh root@<cluster-ip> 'kubectl -n aiproxy-system logs deploy/aiproxy-web --tail=80'
ssh root@<cluster-ip> 'kubectl -n aiproxy-system set image deploy/aiproxy-web aiproxy=<validation-image>'
ssh root@<cluster-ip> 'kubectl -n aiproxy-system rollout status deploy/aiproxy-web --timeout=180s'
```

## 预期结果

- 基线错误 `Cannot find module '/app/server.js'` 不再出现。
- Deployment rollout 成功。
- `aiproxy-web` 日志显示 Next.js 已 Ready。

## 实际结果

```text
deployment "aiproxy-web" successfully rolled out
Next.js 14.2.35
Ready in 591ms
```

结果:通过。
45 changes: 45 additions & 0 deletions artifacts/deploy-adjust/test-cases/TC-02-channel-page-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# TC-02 页面配置渠道

## 测试目标

验证 AIProxy 管理中心页面可以打开渠道列表,并通过页面操作配置和保存当前 AI provider 渠道。

## 前置条件

- `aiproxy-web` 已完成 TC-01 的启动修复。
- 浏览器访问地址:`https://aiproxy-web.<cluster-domain>/zh/dashboard`
- 使用 `ns-admin` 工作空间访问页面。

## 关联代码修改

- `app/api/admin/channel/type-name/route.ts:17`:优先请求当前后端支持的 `/api/channels/type_metas`。
- `app/api/admin/channel/type-name/route.ts:38`:把后端 metadata 映射成前端既有的 `ChannelTypeMapName` 结构。
- 保留 `/api/channels/type_names` fallback,避免老后端兼容性回退被破坏。

## 测试流程

1. 打开 AIProxy 管理中心渠道页面。
2. 检查渠道列表正常加载。
3. 通过页面创建或编辑 Anthropic 渠道。
4. 填入当前 provider 配置并保存。
5. 返回渠道列表确认渠道处于启用状态。

## 截图证据

![渠道列表](./images/01-dashboard-channel-list.png)

![渠道保存后状态](./images/02-dashboard-channel-updated.png)

## 预期结果

- 渠道页面不再因为 `/api/channels/type_names` 不存在而失败。
- 页面能正常读取渠道类型。
- 渠道保存后显示在列表中并保持启用。

## 实际结果

- 渠道页面加载成功。
- 测试渠道 `<test-channel-name>` 保存成功。
- 截图中渠道状态可见且页面无错误提示。

结果:通过。
Loading
Loading