diff --git a/docs/README.md b/docs/README.md index 037275b..4e55237 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,7 +28,7 @@ npm run docs:build |------|--------|------------| | 对外第一印象、产品定位、核心入口 | [文档首页](/) | 需要调整公开落地页信息架构、首页 CTA 或首屏导航时 | | 新用户部署与首条请求 | [快速上手](./getting-started.md) | 新安装流程、默认端口、首次调用步骤变化时 | -| 上游平台选择与接法 | [上游接入](./upstream-integration.md) | 平台支持范围、官方预设、AI 请求地址池、自动识别规则变化时 | +| 上游平台选择与接法 | [上游接入](./upstream-integration.md) | 平台支持范围、官方预设、API 请求地址池、自动识别规则变化时 | | Provider OAuth 授权 | [OAuth 管理](./oauth.md) | 支持的 OAuth provider、授权流程、回调方式或自动重绑能力变化时 | | 生产部署与回滚 | [部署指南](./deployment.md) | Docker Compose、反向代理、升级回滚策略变更时 | | K3s / Helm 高级升级面板 | [K3s 更新中心(高级)](./k3s-update-center.md) | 需要说明谁适合使用更新中心、helper 怎么配、K3s/Helm 发布链路怎么接入时 | diff --git a/docs/getting-started.md b/docs/getting-started.md index 441be5b..4818e1a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -144,7 +144,7 @@ npm run dev - 平台通常可自动检测;如果因为防护页、反向代理或特殊路径导致检测失败,再手动选择。 - 可选是否开启系统代理,方便国内机器访问国外中转站。 - 可选站点权重,站点权重越大,路由将更加频繁使用这个站点的模型。 -- 如果这个站点的控制台 URL 和真实 AI 请求地址不同,不要直接把主站点 URL 改掉,而是在表单下方补「AI 请求地址池」。 +- 如果这个站点的控制台 URL 和真实 API 请求地址不同,不要直接把主站点 URL 改掉,而是在表单下方补「API 请求地址池」。 > [!IMPORTANT] > 通用平台常见写法是填控制台地址或 provider base URL;但如果你选的是**官方预设**,请保留预设自动带出的完整路径,哪怕它本来就包含 `/v1`、`/anthropic` 或 `/api/coding/...`。 diff --git a/docs/index.md b/docs/index.md index c261b07..a505815 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ features: details: 从部署到第一条请求,按步骤完成最小可用环境搭建。 link: /getting-started - title: 上游接入 - details: 按平台类型、官方预设和 AI 请求地址池的现状,快速判断站点该怎么接。 + details: 按平台类型、官方预设和 API 请求地址池的现状,快速判断站点该怎么接。 link: /upstream-integration - title: OAuth 管理 details: 直接接入 Codex、Claude、Gemini CLI、Antigravity 等 provider 授权账号。 diff --git a/docs/management-api.md b/docs/management-api.md index 198ef82..0d20993 100644 --- a/docs/management-api.md +++ b/docs/management-api.md @@ -228,7 +228,7 @@ curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites/detect" \ | `isPinned` | `boolean` | 否 | 是否置顶 | | `sortOrder` | `number` | 否 | 排序值,非负整数 | | `globalWeight` | `number` | 否 | 站点全局权重,必须为正数 | -| `apiEndpoints` | `array` | 否 | 仅用于 AI 请求的数据面地址池;不替代主站点 URL | +| `apiEndpoints` | `array` | 否 | 仅用于 API 请求的数据面地址池;不替代主站点 URL | 示例: diff --git a/docs/upstream-integration.md b/docs/upstream-integration.md index 2cce3a4..67cf7e8 100644 --- a/docs/upstream-integration.md +++ b/docs/upstream-integration.md @@ -443,18 +443,18 @@ CPA 这类站点推荐直接使用 **API Key**: 2. Metapi 会向该 URL 发送 POST 请求执行签到 3. 请求头自动携带账号凭证 -### AI 请求地址池 +### API 请求地址池 -现在不少站点已经不是“一个 URL 同时负责后台面板和 `/v1/*` AI 请求”。 +现在不少站点已经不是“一个 URL 同时负责后台面板和 `/v1/*` API 请求”。 Metapi 当前支持把这两层拆开: - **主站点 URL**:用于登录、签到、后台接口、系统访问令牌管理 -- **AI 请求地址池**:用于真正转发 `/v1/*`、`/chat/completions`、`/responses` 等模型请求 +- **API 请求地址池**:用于真正转发 `/v1/*`、`/chat/completions`、`/responses` 等模型请求 -适合填写 AI 请求地址池的场景: +适合填写 API 请求地址池的场景: -1. 控制台地址和 AI 网关地址不同 +1. 控制台地址和 API 网关地址不同 2. 同一个站点有多个 API host,需要健康切换 3. 你想保留面板地址不变,只把模型流量导向专用网关 @@ -462,7 +462,7 @@ Metapi 当前支持把这两层拆开: ```text 主站点 URL = 控制面 -AI 请求地址池 = 数据面 +API 请求地址池 = 数据面 ``` --- @@ -572,7 +572,7 @@ Metapi 会定期抓取已接入站点的公告,并在首次发现时写入站 - 官方预设则相反,**不要**把预设自动带出的 `/v1` / `/anthropic` / `/api/coding/...` 手动删掉 2. 网络不通(检查代理配置或防火墙) 3. 凭证无效(重新验证账号密码、Session 或 API Key) -4. 主站点 URL 和真实 AI 请求地址不同,但还没配置 AI 请求地址池 +4. 主站点 URL 和真实 API 请求地址不同,但还没配置 API 请求地址池 **解决方法:** @@ -676,7 +676,7 @@ Metapi 会定期抓取已接入站点的公告,并在首次发现时写入站 - **每周检查:** 账号健康状态、余额预警 - **每月清理:** 禁用长期不可用的站点和账号 - **凭证轮换:** 定期更新 Access Token 和 API Key -- **官方预设站点:** 新增模型时记得补充推荐模型和 AI 请求地址池 +- **官方预设站点:** 新增模型时记得补充推荐模型和 API 请求地址池 --- diff --git a/src/server/routes/api/accounts.apiSite.test.ts b/src/server/routes/api/accounts.apiSite.test.ts index 65b8791..de5775f 100644 --- a/src/server/routes/api/accounts.apiSite.test.ts +++ b/src/server/routes/api/accounts.apiSite.test.ts @@ -62,14 +62,14 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { const site = await db.insert(schema.sites).values({ name: 'Nihao Panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); await db.insert(schema.siteApiEndpoints).values({ siteId: site.id, - url: 'https://api.nih.cc', + url: 'https://api.example.com', enabled: true, sortOrder: 0, }).run(); @@ -90,7 +90,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { tokenType: 'apikey', modelCount: 1, }); - expect(getModelsMock).toHaveBeenCalledWith('https://api.nih.cc', 'sk-nihao', undefined); + expect(getModelsMock).toHaveBeenCalledWith('https://api.example.com', 'sk-nihao', undefined); expect(verifyTokenMock).not.toHaveBeenCalled(); }); @@ -101,7 +101,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { const site = await db.insert(schema.sites).values({ name: 'Nihao Pool', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); @@ -109,13 +109,13 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { await db.insert(schema.siteApiEndpoints).values([ { siteId: site.id, - url: 'https://api-a.nih.cc', + url: 'https://api-a.example.com', enabled: true, sortOrder: 0, }, { siteId: site.id, - url: 'https://api-b.nih.cc', + url: 'https://api-b.example.com', enabled: true, sortOrder: 1, }, @@ -137,12 +137,12 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { tokenType: 'apikey', modelCount: 1, }); - expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api-a.nih.cc', 'sk-rotate', undefined); - expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api-b.nih.cc', 'sk-rotate', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api-a.example.com', 'sk-rotate', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api-b.example.com', 'sk-rotate', undefined); const endpoints = await db.select().from(schema.siteApiEndpoints).all(); - const firstEndpoint = endpoints.find((item) => item.url === 'https://api-a.nih.cc'); - const secondEndpoint = endpoints.find((item) => item.url === 'https://api-b.nih.cc'); + const firstEndpoint = endpoints.find((item) => item.url === 'https://api-a.example.com'); + const secondEndpoint = endpoints.find((item) => item.url === 'https://api-b.example.com'); expect(firstEndpoint?.cooldownUntil).toBeTruthy(); expect(firstEndpoint?.lastFailureReason).toContain('HTTP 502'); expect(secondEndpoint?.lastSelectedAt).toBeTruthy(); @@ -158,14 +158,14 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { const site = await db.insert(schema.sites).values({ name: 'Nihao Panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); await db.insert(schema.siteApiEndpoints).values({ siteId: site.id, - url: 'https://api.nih.cc', + url: 'https://api.example.com', enabled: true, sortOrder: 0, }).run(); @@ -186,7 +186,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { tokenType: 'session', apiToken: 'sk-derived', }); - expect(verifyTokenMock).toHaveBeenCalledWith('https://nih.cc', 'session-token', undefined); + expect(verifyTokenMock).toHaveBeenCalledWith('https://console.example.com', 'session-token', undefined); expect(getModelsMock).not.toHaveBeenCalled(); }); @@ -195,14 +195,14 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { const site = await db.insert(schema.sites).values({ name: 'Nihao Panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); await db.insert(schema.siteApiEndpoints).values({ siteId: site.id, - url: 'https://api.nih.cc', + url: 'https://api.example.com', enabled: true, sortOrder: 0, }).run(); @@ -221,7 +221,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { expect(response.json()).toMatchObject({ tokenType: 'apikey', }); - expect(getModelsMock).toHaveBeenCalledWith('https://api.nih.cc', 'sk-nihao-create', undefined); + expect(getModelsMock).toHaveBeenCalledWith('https://api.example.com', 'sk-nihao-create', undefined); }); it('rotates API key account creation across configured ai endpoints after a retryable failure', async () => { @@ -231,7 +231,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { const site = await db.insert(schema.sites).values({ name: 'Nihao Create Pool', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); @@ -239,13 +239,13 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { await db.insert(schema.siteApiEndpoints).values([ { siteId: site.id, - url: 'https://api-create-a.nih.cc', + url: 'https://api-create-a.example.com', enabled: true, sortOrder: 0, }, { siteId: site.id, - url: 'https://api-create-b.nih.cc', + url: 'https://api-create-b.example.com', enabled: true, sortOrder: 1, }, @@ -265,7 +265,7 @@ describe('accounts api endpoint host selection', { timeout: 15_000 }, () => { expect(response.json()).toMatchObject({ tokenType: 'apikey', }); - expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api-create-a.nih.cc', 'sk-nihao-create-rotate', undefined); - expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api-create-b.nih.cc', 'sk-nihao-create-rotate', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(1, 'https://api-create-a.example.com', 'sk-nihao-create-rotate', undefined); + expect(getModelsMock).toHaveBeenNthCalledWith(2, 'https://api-create-b.example.com', 'sk-nihao-create-rotate', undefined); }); }); diff --git a/src/server/routes/proxy/chat.siteApiEndpoint.test.ts b/src/server/routes/proxy/chat.siteApiEndpoint.test.ts index c2eafa8..700b885 100644 --- a/src/server/routes/proxy/chat.siteApiEndpoint.test.ts +++ b/src/server/routes/proxy/chat.siteApiEndpoint.test.ts @@ -150,7 +150,7 @@ describe('chat proxy site api endpoint rotation', () => { it('rotates to the next configured ai endpoint for retryable /v1/chat/completions failures', async () => { const site = await db.insert(schema.sites).values({ name: 'nihao-panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'openai', status: 'active', }).returning().get(); @@ -168,13 +168,13 @@ describe('chat proxy site api endpoint rotation', () => { await db.insert(schema.siteApiEndpoints).values([ { siteId: site.id, - url: 'https://api-a.nih.cc', + url: 'https://api-a.example.com', enabled: true, sortOrder: 0, }, { siteId: site.id, - url: 'https://api-b.nih.cc', + url: 'https://api-b.example.com', enabled: true, sortOrder: 1, }, @@ -222,10 +222,10 @@ describe('chat proxy site api endpoint rotation', () => { expect(response.statusCode).toBe(200); expect(response.json()?.choices?.[0]?.message?.content).toBe('ok via api-b'); expect(fetchMock).toHaveBeenCalledTimes(4); - expect(String(fetchMock.mock.calls[0]?.[0] || '')).toBe('https://api-a.nih.cc/v1/chat/completions'); - expect(String(fetchMock.mock.calls[1]?.[0] || '')).toBe('https://api-a.nih.cc/v1/responses'); - expect(String(fetchMock.mock.calls[2]?.[0] || '')).toBe('https://api-a.nih.cc/v1/messages'); - expect(String(fetchMock.mock.calls[3]?.[0] || '')).toBe('https://api-b.nih.cc/v1/chat/completions'); + expect(String(fetchMock.mock.calls[0]?.[0] || '')).toBe('https://api-a.example.com/v1/chat/completions'); + expect(String(fetchMock.mock.calls[1]?.[0] || '')).toBe('https://api-a.example.com/v1/responses'); + expect(String(fetchMock.mock.calls[2]?.[0] || '')).toBe('https://api-a.example.com/v1/messages'); + expect(String(fetchMock.mock.calls[3]?.[0] || '')).toBe('https://api-b.example.com/v1/chat/completions'); expect(selectNextChannelMock).not.toHaveBeenCalled(); expect(recordFailureMock).not.toHaveBeenCalled(); expect(recordSuccessMock).toHaveBeenCalledTimes(1); @@ -235,7 +235,7 @@ describe('chat proxy site api endpoint rotation', () => { .orderBy(asc(schema.siteApiEndpoints.sortOrder), asc(schema.siteApiEndpoints.id)) .all(); expect(storedEndpoints[0]).toMatchObject({ - url: 'https://api-a.nih.cc', + url: 'https://api-a.example.com', lastFailureReason: 'HTTP 502: [upstream:/v1/messages] Upstream returned HTTP 502: bad gateway via messages', }); expect(storedEndpoints[0]?.cooldownUntil).toBeTruthy(); diff --git a/src/server/routes/proxy/completions.siteApiEndpoint.test.ts b/src/server/routes/proxy/completions.siteApiEndpoint.test.ts index d08961b..c5e2aee 100644 --- a/src/server/routes/proxy/completions.siteApiEndpoint.test.ts +++ b/src/server/routes/proxy/completions.siteApiEndpoint.test.ts @@ -120,7 +120,7 @@ describe('/v1/completions site api endpoint rotation', () => { it('cools down a retryable failed endpoint and retries the next endpoint within the same site', async () => { const site = await db.insert(schema.sites).values({ name: 'nihao-panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); @@ -138,13 +138,13 @@ describe('/v1/completions site api endpoint rotation', () => { await db.insert(schema.siteApiEndpoints).values([ { siteId: site.id, - url: 'https://api-a.nih.cc', + url: 'https://api-a.example.com', enabled: true, sortOrder: 0, }, { siteId: site.id, - url: 'https://api-b.nih.cc', + url: 'https://api-b.example.com', enabled: true, sortOrder: 1, }, @@ -194,8 +194,8 @@ describe('/v1/completions site api endpoint rotation', () => { object: 'text_completion', }); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(String(fetchMock.mock.calls[0]?.[0] || '')).toBe('https://api-a.nih.cc/v1/completions'); - expect(String(fetchMock.mock.calls[1]?.[0] || '')).toBe('https://api-b.nih.cc/v1/completions'); + expect(String(fetchMock.mock.calls[0]?.[0] || '')).toBe('https://api-a.example.com/v1/completions'); + expect(String(fetchMock.mock.calls[1]?.[0] || '')).toBe('https://api-b.example.com/v1/completions'); expect(selectNextChannelMock).not.toHaveBeenCalled(); expect(recordFailureMock).not.toHaveBeenCalled(); expect(recordSuccessMock).toHaveBeenCalledTimes(1); @@ -205,12 +205,12 @@ describe('/v1/completions site api endpoint rotation', () => { .orderBy(asc(schema.siteApiEndpoints.sortOrder), asc(schema.siteApiEndpoints.id)) .all(); expect(storedEndpoints[0]).toMatchObject({ - url: 'https://api-a.nih.cc', + url: 'https://api-a.example.com', lastFailureReason: 'HTTP 502: bad gateway', }); expect(storedEndpoints[0]?.cooldownUntil).toBeTruthy(); expect(storedEndpoints[1]).toMatchObject({ - url: 'https://api-b.nih.cc', + url: 'https://api-b.example.com', }); expect(storedEndpoints[1]?.lastSelectedAt).toBeTruthy(); }); diff --git a/src/server/routes/proxy/responses.websocket.test.ts b/src/server/routes/proxy/responses.websocket.test.ts index 92fb080..45a8ee5 100644 --- a/src/server/routes/proxy/responses.websocket.test.ts +++ b/src/server/routes/proxy/responses.websocket.test.ts @@ -617,7 +617,7 @@ describe('responses websocket transport', () => { type: 'error', status: 408, error: { - message: '当前站点的 AI 请求地址均不可用', + message: '当前站点的 API 请求地址均不可用', }, }); expect(fetchMock).toHaveBeenCalledTimes(0); diff --git a/src/server/services/modelService.discovery.test.ts b/src/server/services/modelService.discovery.test.ts index a1045e9..7a89e41 100644 --- a/src/server/services/modelService.discovery.test.ts +++ b/src/server/services/modelService.discovery.test.ts @@ -138,21 +138,21 @@ describe('refreshModelsForAccount credential discovery', () => { it('uses the configured ai endpoint for direct model discovery credentials', async () => { getApiTokenMock.mockResolvedValue(null); getModelsMock.mockImplementation(async (baseUrl: string, token: string) => ( - baseUrl === 'https://api.nih.cc' && token === 'session-token' + baseUrl === 'https://api.example.com' && token === 'session-token' ? ['gpt-4.1'] : [] )); const site = await db.insert(schema.sites).values({ name: 'nihao-panel', - url: 'https://nih.cc', + url: 'https://console.example.com', platform: 'new-api', status: 'active', }).returning().get(); await db.insert(schema.siteApiEndpoints).values({ siteId: site.id, - url: 'https://api.nih.cc', + url: 'https://api.example.com', enabled: true, sortOrder: 0, }).run(); @@ -175,7 +175,7 @@ describe('refreshModelsForAccount credential discovery', () => { modelCount: 1, modelsPreview: ['gpt-4.1'], }); - expect(getModelsMock).toHaveBeenCalledWith('https://api.nih.cc', 'session-token', undefined); + expect(getModelsMock).toHaveBeenCalledWith('https://api.example.com', 'session-token', undefined); }); it('deduplicates discovered model names before writing availability rows', async () => { diff --git a/src/server/services/siteApiEndpointService.ts b/src/server/services/siteApiEndpointService.ts index d5913ef..9f2ae6d 100644 --- a/src/server/services/siteApiEndpointService.ts +++ b/src/server/services/siteApiEndpointService.ts @@ -207,7 +207,7 @@ export async function requireSiteApiBaseUrl( ): Promise { const baseUrl = await resolveSiteApiBaseUrl(site, now); if (baseUrl) return baseUrl; - throw new Error('当前站点的 AI 请求地址均不可用'); + throw new Error('当前站点的 API 请求地址均不可用'); } export async function recordSiteApiEndpointFailure( @@ -258,11 +258,11 @@ export async function runWithSiteApiEndpointPool( const target = await selectSiteApiEndpointTarget(site); if (!target) { if (lastError) throw lastError; - throw new Error('当前站点的 AI 请求地址均不可用'); + throw new Error('当前站点的 API 请求地址均不可用'); } if (target.endpointId && attemptedEndpointIds.has(target.endpointId)) { if (lastError) throw lastError; - throw new Error('当前站点的 AI 请求地址均不可用'); + throw new Error('当前站点的 API 请求地址均不可用'); } try { diff --git a/src/web/pages/Sites.tsx b/src/web/pages/Sites.tsx index 43588d4..46533e9 100644 --- a/src/web/pages/Sites.tsx +++ b/src/web/pages/Sites.tsx @@ -517,7 +517,7 @@ export default function Sites() { } const serializedApiEndpoints = serializeSiteApiEndpoints(form.apiEndpoints); if (!serializedApiEndpoints.valid) { - toast.error(serializedApiEndpoints.error || 'AI 请求地址格式不正确'); + toast.error(serializedApiEndpoints.error || 'API 请求地址格式不正确'); return; } @@ -1028,7 +1028,7 @@ export default function Sites() {
setForm((prev) => ({ ...prev, url: e.target.value }))} onBlur={() => { @@ -1102,7 +1102,7 @@ export default function Sites() {
)}
- 主站点 URL 用于登录、签到、面板接口和系统访问令牌管理;如果 AI 请求地址和主站点不同,请在下面的 AI 请求地址池里填写。 + 主站点 URL 用于登录、签到、面板接口和系统访问令牌管理;如果 API 请求地址和主站点不同,请在下面的 API 请求地址池里填写。
- AI 请求地址池 + API 请求地址池
@@ -1146,7 +1146,7 @@ export default function Sites() { >
updateApiEndpointRow(index, { url: e.target.value })} style={{ ...formInputStyle, flex: 1, fontFamily: 'var(--font-mono)' }} @@ -1405,7 +1405,7 @@ export default function Sites() { style={formInputStyle} />
- 这里只是 HTTP/SOCKS 代理地址,不是上游 AI API 地址。填写后优先使用站点代理;留空则使用系统代理或直连(取决于设置开关状态)。 + 这里只是 HTTP/SOCKS 代理地址,不是上游 API 请求地址。填写后优先使用站点代理;留空则使用系统代理或直连(取决于设置开关状态)。
diff --git a/src/web/pages/helpers/sitesEditor.test.ts b/src/web/pages/helpers/sitesEditor.test.ts index a970095..e7eb9f1 100644 --- a/src/web/pages/helpers/sitesEditor.test.ts +++ b/src/web/pages/helpers/sitesEditor.test.ts @@ -191,7 +191,7 @@ describe('buildSiteSaveAction', () => { ])).toEqual({ valid: false, apiEndpoints: [], - error: 'AI 请求地址 "https://api.example.com" 重复了', + error: 'API 请求地址 "https://api.example.com" 重复了', }); }); }); diff --git a/src/web/pages/helpers/sitesEditor.ts b/src/web/pages/helpers/sitesEditor.ts index 33f2346..49907a2 100644 --- a/src/web/pages/helpers/sitesEditor.ts +++ b/src/web/pages/helpers/sitesEditor.ts @@ -225,7 +225,7 @@ export function serializeSiteApiEndpoints(fields: SiteApiEndpointField[]): { return { valid: false, apiEndpoints: [], - error: `AI 请求地址 "${normalizedUrl}" 重复了`, + error: `API 请求地址 "${normalizedUrl}" 重复了`, }; } seen.add(normalizedUrl); diff --git a/src/web/pages/sites.centered-modal.test.ts b/src/web/pages/sites.centered-modal.test.ts index e42c242..cb9a3ba 100644 --- a/src/web/pages/sites.centered-modal.test.ts +++ b/src/web/pages/sites.centered-modal.test.ts @@ -10,4 +10,21 @@ describe('Sites centered modal adoption', () => { expect(source).toContain(' { + const source = readFileSync(resolve(process.cwd(), 'src/web/pages/Sites.tsx'), 'utf8'); + + expect(source).toContain('API 请求地址池'); + expect(source).toContain('+ 添加 API 地址'); + expect(source).toContain('站点 URL(面板/登录/签到地址,如 https://console.example.com)'); + expect(source).toContain('API 请求地址(如 https://api.example.com)'); + expect(source).toContain('label="API 请求地址"'); + expect(source).toContain('API 地址: {buildSiteApiEndpointSummary(site)}'); + expect(source).not.toContain('nih.cc'); + expect(source).not.toContain('api.nih.cc'); + expect(source).not.toContain('AI 请求地址池'); + expect(source).not.toContain('+ 添加 AI 地址'); + expect(source).not.toContain('label="AI 请求地址"'); + expect(source).not.toContain('AI 地址: {buildSiteApiEndpointSummary(site)}'); + }); });