From fe7ea03b8699b10a4daed7106f60e3e88b20099c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 6 Sep 2026 00:10:51 +0800 Subject: [PATCH] chore(sponsors): retire expired placements while preserving saved providers --- README.md | 30 ------ README.zh-CN.md | 30 ------ desktop/src/config/providerPresets.test.ts | 28 ++++-- .../pages/settings/ProviderSettings.test.tsx | 92 +++++++++++++++++++ src/server/__tests__/provider-presets.test.ts | 20 ++-- src/server/__tests__/providers.test.ts | 44 +++++++++ src/server/config/providerPresets.json | 12 +-- 7 files changed, 167 insertions(+), 89 deletions(-) create mode 100644 desktop/src/pages/settings/ProviderSettings.test.tsx diff --git a/README.md b/README.md index efb257c3..b615ede9 100644 --- a/README.md +++ b/README.md @@ -63,36 +63,6 @@ This project is maintained in the author's spare time. Corporate or individual s - - - - XuanShu API - - - - Thanks to XuanShu API for sponsoring this project. XuanShu API is an all-in-one, enterprise-grade AI gateway that gives you access to leading global models through a single unified API, and works out of the box with popular AI coding tools such as Claude Code, Codex, and Gemini CLI. Stability comes first: multi-account pool scheduling and automatic failover smooth out upstream account fluctuations. Model calls are priced as low as 25% of official rates, and balance top-ups get roughly a 20% bonus with no expiry. New users who register through this link receive a $5 credit. - - - - - - FennoAI - - - - Thanks to FennoAI for sponsoring this project. FennoAI is a high-stability, high-performance API relay service for enterprise engineering teams and individual developers. It speaks both the OpenAI and Anthropic protocols, so it drops straight into Codex, Claude Code, OpenCode, and other mainstream AI coding tools. The platform is built for enterprise-grade stability at hundreds of billions of tokens per day, and supports corporate settlement and invoicing for both domestic and overseas entities. As a cc-haha user perk, buying a subscription through this link gets you $50 of Coding Plan credit for $1.99. Referrals earn up to 20% commission — the more you invite, the more you get. - - - - - - Qiniu Cloud AI - - - - Thanks to Qiniu Cloud AI for sponsoring this project. Qiniu Cloud AI is the enterprise-grade LLM MaaS platform from Qiniu Cloud (02567.HK): one entry point to 150+ leading models worldwide, compatible with the major vendors' protocols, covering text, image, audio, video, and file processing across every modality, and serving more than 1.69 million enterprises and developers. As a cc-haha user perk, registering through this link gets enterprise users 12M free tokens and developers 3M free tokens. - - diff --git a/README.zh-CN.md b/README.zh-CN.md index 562e30e6..ce440d90 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -63,36 +63,6 @@ Claude Code Haha 是一个**桌面端 Claude Code 工作台**:多会话与全 - - - - 玄枢API - - - - 感谢 玄枢API 赞助本项目!玄枢API 是一站式企业级 AI 网关,通过一个统一 API 即可接入 Claude Code、Codex、Gemini CLI 等热门 AI 编程工具,调用全球顶尖模型。平台以稳定为前提,通过多账号池调度与自动故障切换屏蔽上游账号波动;模型调用低至官方定价 2.5 折,余额充值再享约 8 折优惠且不设有效期。新用户通过 专属链接 注册即赠 5 美元额度。 - - - - - - FennoAI - - - - 感谢 FennoAI 赞助本项目!FennoAI 是一家面向企业研发团队和开发者的高稳定、高性能 API 中转服务商,兼容 OpenAI 与 Anthropic 协议,可无缝接入 Codex、Claude Code、OpenCode 等主流 AI 编程工具。平台具备企业级稳定性,可支撑千亿 Token/日的调用规模,并支持境内外主体公对公结算及开票,满足企业级研发与采购需求。作为 cc-haha 用户专属福利,通过 专属链接 购买订阅,仅需 1.99 美元即可获得价值 50 美元的 Coding Plan 额度;同时支持邀请奖励,邀请好友购买最高可获得 20% 返佣,邀请越多,奖励越高。 - - - - - - 七牛云 AI - - - - 感谢 七牛云 AI 赞助本项目!七牛云 AI 是七牛云(02567.HK)旗下企业级大模型 MaaS 平台,一站式调用全球 150+ 主流模型,兼容全球主流模型厂商协议,覆盖文本、图像、音频、视频、文件处理等全模态处理能力,服务超过 169 万企业及开发者用户。作为 cc-haha 用户专属福利,通过 专属链接 注册,企业用户免费领 1200 万 Token,开发者免费领 300 万 Token。 - - diff --git a/desktop/src/config/providerPresets.test.ts b/desktop/src/config/providerPresets.test.ts index dc0bea9f..585cca45 100644 --- a/desktop/src/config/providerPresets.test.ts +++ b/desktop/src/config/providerPresets.test.ts @@ -96,15 +96,13 @@ describe('bundled provider presets', () => { expect(shengsuanyun?.featured).toBeUndefined() const selectableIds = selectableProviderPresets(BUNDLED_PROVIDER_PRESETS).map((p) => p.id) expect(selectableIds).not.toContain('shengsuanyun') - expect(selectableIds).toContain('xuanshuapi') - expect(selectableIds).toContain('fennoai') - expect(selectableIds).toContain('qiniuai') + expect(selectableIds).toContain('atlascloud') expect(selectableIds).toContain('custom') }) // Both gateways mount the Anthropic protocol on the bare host; a /v1 or // /anthropic suffix here would double up with the path Claude Code appends. - it('points the sponsored gateways at their Anthropic-compatible roots', () => { + it('keeps the retired gateways at their Anthropic-compatible roots', () => { const fennoai = BUNDLED_PROVIDER_PRESETS.find((preset) => preset.id === 'fennoai') const qiniuai = BUNDLED_PROVIDER_PRESETS.find((preset) => preset.id === 'qiniuai') @@ -114,13 +112,23 @@ describe('bundled provider presets', () => { expect(qiniuai && presetMatchesBaseUrl(qiniuai, ' HTTPS://API.QNAIGC.COM/ ')).toBe(true) }) - // The picker groups featured presets into their own row; losing the flag would - // silently demote the sponsors into the generic list. - it('keeps the sponsored gateways in the featured row', () => { - const featuredIds = BUNDLED_PROVIDER_PRESETS.filter((preset) => preset.featured).map((p) => p.id) + it.each(['xuanshuapi', 'fennoai', 'qiniuai'])('retires %s without removing saved-provider metadata', (id) => { + const preset = BUNDLED_PROVIDER_PRESETS.find((candidate) => candidate.id === id) - expect(featuredIds).toContain('fennoai') - expect(featuredIds).toContain('qiniuai') + expect(preset?.deprecated).toBe(true) + expect(preset?.apiKeyUrl).toBeUndefined() + expect(preset?.promoText).toBeUndefined() + expect(preset?.featured).toBeUndefined() + expect(preset?.authStrategy).toBe('auth_token') + expect(preset?.defaultEnv).toBeDefined() + expect(preset?.modelContextWindows).toBeDefined() + expect(selectableProviderPresets(BUNDLED_PROVIDER_PRESETS).map((candidate) => candidate.id)) + .not.toContain(id) + }) + + it('keeps Atlas Cloud selectable and featured', () => { + expect(selectableProviderPresets(BUNDLED_PROVIDER_PRESETS).filter((preset) => preset.featured).map((preset) => preset.id)) + .toEqual(['atlascloud']) }) it('keeps the retired 接口AI preset resolvable but not selectable', () => { diff --git a/desktop/src/pages/settings/ProviderSettings.test.tsx b/desktop/src/pages/settings/ProviderSettings.test.tsx new file mode 100644 index 00000000..7055a3fc --- /dev/null +++ b/desktop/src/pages/settings/ProviderSettings.test.tsx @@ -0,0 +1,92 @@ +import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import '@testing-library/jest-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { providersApi } from '../../api/providers' +import { useProviderStore } from '../../stores/providerStore' +import { useSettingsStore } from '../../stores/settingsStore' +import type { SavedProvider } from '../../types/provider' +import { ProviderSettings } from './ProviderSettings' + +vi.mock('../../components/settings/ClaudeOfficialLogin', () => ({ ClaudeOfficialLogin: () => null })) +vi.mock('../../components/settings/ChatGPTOfficialLogin', () => ({ ChatGPTOfficialLogin: () => null })) +vi.mock('../../components/settings/GrokOfficialLogin', () => ({ GrokOfficialLogin: () => null })) + +const savedProviders: SavedProvider[] = ([ + ['xuanshuapi', '玄枢API', 'https://www.xuanshuapi.com', 'claude-sonnet-5'], + ['fennoai', 'FennoAI', 'https://api.fenno.ai', 'claude-sonnet-5'], + ['qiniuai', '七牛云 AI', 'https://api.qnaigc.com', 'deepseek/deepseek-v4-pro'], +] as const).map(([presetId, name, baseUrl, model]) => ({ + id: `saved-${presetId}`, + presetId, + name, + baseUrl, + apiKey: 'fake-saved-api-key', + apiFormat: 'anthropic', + models: { main: model, haiku: model, sonnet: model, opus: model }, +})) + +describe('retired sponsor providers', () => { + beforeEach(() => { + useSettingsStore.setState({ locale: 'en' }) + vi.spyOn(useSettingsStore.getState(), 'fetchAll').mockResolvedValue() + vi.spyOn(providersApi, 'list').mockResolvedValue({ providers: savedProviders, activeId: null }) + vi.spyOn(providersApi, 'getSettings').mockResolvedValue({}) + vi.spyOn(providersApi, 'updateSettings').mockResolvedValue({ ok: true }) + }) + + afterEach(() => { + cleanup() + vi.restoreAllMocks() + }) + + it('loads saved providers while hiding their add-provider chips', async () => { + render() + for (const provider of savedProviders) { + expect(await screen.findByTestId(`provider-${provider.id}`)).toHaveTextContent(provider.name) + } + expect(useProviderStore.getState().providers).toEqual(savedProviders) + + fireEvent.click(screen.getByRole('button', { name: /Add Provider/ })) + const dialog = within(screen.getByRole('dialog')) + for (const provider of savedProviders) { + expect(dialog.queryByRole('button', { name: provider.name })).not.toBeInTheDocument() + } + expect(dialog.getByRole('button', { name: 'Atlas Cloud' })).toBeInTheDocument() + expect(dialog.getByRole('button', { name: 'Custom' })).toBeInTheDocument() + }) + + it.each(savedProviders)('edits and saves an existing $presetId provider without losing its connection', async (provider) => { + const update = vi.spyOn(providersApi, 'update').mockImplementation(async (id, input) => { + expect(id).toBe(provider.id) + const updated = { ...provider, ...input } as SavedProvider + vi.mocked(providersApi.list).mockResolvedValue({ + providers: savedProviders.map((saved) => saved.id === id ? updated : saved), + activeId: null, + }) + return { provider: updated } + }) + render() + const card = await screen.findByTestId(`provider-${provider.id}`) + fireEvent.click(within(card).getByRole('button', { name: 'Edit' })) + + const dialog = within(screen.getByRole('dialog')) + expect(dialog.getByDisplayValue(provider.baseUrl)).toBeInTheDocument() + expect(dialog.queryByRole('button', { name: /Get API Key/ })).not.toBeInTheDocument() + fireEvent.change(dialog.getByDisplayValue(provider.name), { target: { value: `${provider.name} edited` } }) + fireEvent.click(dialog.getByRole('button', { name: 'Save' })) + + await waitFor(() => expect(update).toHaveBeenCalledWith(provider.id, expect.objectContaining({ + name: `${provider.name} edited`, + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + apiFormat: provider.apiFormat, + authStrategy: 'auth_token', + models: provider.models, + modelContextWindows: { [provider.models.main]: 1000000 }, + }))) + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + expect(useProviderStore.getState().providers.find((saved) => saved.id === provider.id)) + .toMatchObject({ ...provider, name: `${provider.name} edited` }) + expect(screen.getByTestId(`provider-${provider.id}`)).toHaveTextContent(`${provider.name} edited`) + }) +}) diff --git a/src/server/__tests__/provider-presets.test.ts b/src/server/__tests__/provider-presets.test.ts index 472882e1..3e14b0e0 100644 --- a/src/server/__tests__/provider-presets.test.ts +++ b/src/server/__tests__/provider-presets.test.ts @@ -218,21 +218,21 @@ describe('provider presets API', () => { CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', }) expect(shengsuanyun?.modelContextWindows?.['anthropic/claude-opus-4.7']).toBe(1000000) - expect(xuanshuapi?.apiKeyUrl).toBe('https://www.xuanshuapi.com/register?aff=CC-HAHA&promo=CC-HAHA') - expect(xuanshuapi?.promoText).toContain('5 美元') - expect(xuanshuapi?.featured).toBe(true) + expect(xuanshuapi?.apiKeyUrl).toBeUndefined() + expect(xuanshuapi?.promoText).toBeUndefined() + expect(xuanshuapi?.featured).toBeUndefined() expect(xuanshuapi?.defaultEnv).toEqual({ CLAUDE_CODE_SUBAGENT_MODEL: 'claude-sonnet-5', }) expect(xuanshuapi?.modelContextWindows?.['claude-sonnet-5']).toBe(1000000) - expect(fennoai?.apiKeyUrl).toBe('https://api.fenno.ai/s/WD8c') - expect(fennoai?.promoText).toContain('1.99 美元') - expect(fennoai?.featured).toBe(true) + expect(fennoai?.apiKeyUrl).toBeUndefined() + expect(fennoai?.promoText).toBeUndefined() + expect(fennoai?.featured).toBeUndefined() expect(fennoai?.modelContextWindows?.['claude-sonnet-5']).toBe(1000000) expect(fennoai?.modelContextWindows?.['claude-haiku-4-5']).toBe(200000) - expect(qiniuai?.apiKeyUrl).toBe('https://s.qiniu.com/IZbyya') - expect(qiniuai?.promoText).toContain('Token') - expect(qiniuai?.featured).toBe(true) + expect(qiniuai?.apiKeyUrl).toBeUndefined() + expect(qiniuai?.promoText).toBeUndefined() + expect(qiniuai?.featured).toBeUndefined() expect(qiniuai?.modelContextWindows?.['deepseek/deepseek-v4-flash']).toBe(1000000) expect(qiniuai?.modelContextWindows?.['z-ai/glm-5.2']).toBe(1000000) expect(qiniuai?.modelContextWindows?.['moonshotai/kimi-k3']).toBe(262144) @@ -279,7 +279,7 @@ describe('provider presets API', () => { }) test('retired presets keep the runtime config saved providers resolve from them', () => { - for (const id of ['shengsuanyun', 'jiekouai']) { + for (const id of ['shengsuanyun', 'jiekouai', 'xuanshuapi', 'fennoai', 'qiniuai']) { const preset = PROVIDER_PRESETS.find((candidate) => candidate.id === id) expect(preset?.deprecated).toBe(true) diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index b446aff7..68fc871a 100644 --- a/src/server/__tests__/providers.test.ts +++ b/src/server/__tests__/providers.test.ts @@ -1287,6 +1287,50 @@ describe('ProviderService', () => { } }) + test.each([ + ['xuanshuapi', 'https://www.xuanshuapi.com', 'claude-sonnet-5'], + ['fennoai', 'https://api.fenno.ai', 'claude-sonnet-5'], + ['qiniuai', 'https://api.qnaigc.com', 'deepseek/deepseek-v4-pro'], + ])('keeps legacy %s providers editable and usable after retirement', async (presetId, baseUrl, model) => { + // This is an old on-disk record: auth/context fields were not always persisted. + const legacyProvider = { + id: `saved-${presetId}`, + ...sampleInput({ presetId, baseUrl, models: { main: model, haiku: model, sonnet: model, opus: model } }), + } + await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true }) + await fs.writeFile(path.join(tmpDir, 'cc-haha', 'providers.json'), JSON.stringify({ + providers: [legacyProvider], + activeId: legacyProvider.id, + })) + + const svc = new ProviderService() + expect((await svc.listProviders()).providers).toEqual([expect.objectContaining(legacyProvider)]) + + await svc.updateProvider(legacyProvider.id, { name: 'Renamed saved provider' }) + await svc.activateProvider(legacyProvider.id) + const restarted = new ProviderService() + expect(await restarted.getProvider(legacyProvider.id)).toMatchObject({ + ...legacyProvider, + name: 'Renamed saved provider', + }) + expect((await restarted.listProviders()).activeId).toBe(legacyProvider.id) + + const runtimeEnv = await restarted.getProviderRuntimeEnv(legacyProvider.id) + const settingsEnv = (await readSettings()).env as Record + for (const env of [runtimeEnv, settingsEnv]) { + expect(env).toMatchObject({ + ANTHROPIC_BASE_URL: baseUrl, + ANTHROPIC_AUTH_TOKEN: legacyProvider.apiKey, + ANTHROPIC_API_KEY: '', + ANTHROPIC_MODEL: model, + }) + expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)[model]).toBe(1000000) + if (presetId === 'xuanshuapi') { + expect(env.CLAUDE_CODE_SUBAGENT_MODEL).toBe('claude-sonnet-5') + } + } + }) + test('should include preset default env on activation and runtime env', async () => { const svc = new ProviderService() const provider = await svc.addProvider(sampleInput({ diff --git a/src/server/config/providerPresets.json b/src/server/config/providerPresets.json index b986aaef..2ced78c7 100644 --- a/src/server/config/providerPresets.json +++ b/src/server/config/providerPresets.json @@ -198,9 +198,7 @@ }, "needsApiKey": true, "websiteUrl": "https://www.xuanshuapi.com", - "apiKeyUrl": "https://www.xuanshuapi.com/register?aff=CC-HAHA&promo=CC-HAHA", - "promoText": "玄枢API 为 cc-haha 的用户提供了专属福利,通过此链接注册的新用户即赠 5 美元额度!", - "featured": true, + "deprecated": true, "authStrategy": "auth_token", "defaultEnv": { "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5" @@ -227,9 +225,7 @@ }, "needsApiKey": true, "websiteUrl": "https://api.fenno.ai", - "apiKeyUrl": "https://api.fenno.ai/s/WD8c", - "promoText": "FennoAI 为 cc-haha 的用户提供了专属福利,通过此链接购买订阅,仅需 1.99 美元即可获得价值 50 美元的 Coding Plan 额度!", - "featured": true, + "deprecated": true, "authStrategy": "auth_token", "defaultEnv": {}, "modelContextWindows": { @@ -254,9 +250,7 @@ }, "needsApiKey": true, "websiteUrl": "https://www.qiniu.com/ai/models", - "apiKeyUrl": "https://s.qiniu.com/IZbyya", - "promoText": "七牛云 AI 为 cc-haha 的用户提供了专属福利,通过此链接注册,企业用户免费领 1200 万 Token,开发者免费领 300 万 Token!", - "featured": true, + "deprecated": true, "authStrategy": "auth_token", "defaultEnv": {}, "modelContextWindows": {