小橙子 535d777ec4
fix(providers): route 'Fetch Models' through server to bypass mixed-content / CORS (#28)
* fix(providers): route 'Fetch Models' through server to bypass mixed-content / CORS

The "获取模型 / Fetch Models" button in the provider settings dialog used
to call upstream `/v1/models` directly from the renderer with `fetch()`.
That works for HTTPS providers whose endpoint sets permissive CORS
headers (e.g. api.openai.com, api.anthropic.com), but fails for any
plain-`http://` provider — most commonly self-hosted relay endpoints
like `http://47.116.22.0:3000`.

Two failure modes both surface as the same opaque "Failed to fetch":

1. Mixed-content block. The desktop renderer runs in a secure context
   (Tauri at `tauri://localhost`, Electron with web security on), and
   modern webviews refuse to fetch plain-HTTP URLs from a secure-context
   page. The request never leaves the browser.

2. CORS-missing. Even when reachable, many self-hosted relays do not
   return `Access-Control-Allow-Origin` for `/v1/models`, so the browser
   blocks the response.

The "Test Connection" button never had this problem because it already
proxies through `POST /api/providers/{id}/test`, which runs on the local
server (Node, no webview restrictions). The fix routes "Fetch Models"
through the same server so it inherits the same isolation.

What this PR adds

Server side
- ApiError.badGateway(message) — new 502 helper for upstream failures.
- POST /api/providers/fetch-models with the new FetchModelsSchema (zod):
  { baseUrl: url, apiKey: non-empty, apiFormat: 'anthropic' | 'openai_chat'
    | 'openai_responses' default 'anthropic' }.
- ProviderService.fetchUpstreamModels(input) — server-side GET against
  `${baseUrl}/v1/models` (or `${baseUrl}/models` if baseUrl already ends
  in `/v1`), reusing the existing network-proxy / timeout plumbing. Sets
  Bearer for OpenAI-compatible formats, x-api-key + anthropic-version
  for native Anthropic. Returns `{ status, data }` where `data` is the
  upstream JSON verbatim. Non-2xx upstreams surface as ApiError.badGateway
  with the upstream's `error.message` if the body has one. Network
  failures and timeouts also map to badGateway with a clear message.

Desktop side
- providersApi.fetchModels(input) — new client wrapping the endpoint.
- handleFetchModels in Settings.tsx now calls providersApi.fetchModels
  instead of fetching directly. The existing `extractModelEntries`
  parser already accepts arbitrary upstream JSON (data / models / items
  / results arrays, plus { id | name | model } per entry), so no
  parsing change was needed.

Tests
- 5 new ProviderService unit tests covering the wire shape:
  · OpenAI format → Bearer header, GET /v1/models, no x-api-key
  · Anthropic format → x-api-key + anthropic-version, no Authorization
  · URL hygiene — strips trailing slashes, doesn't duplicate /v1
  · Non-2xx upstream → ApiError 502 with `HTTP {status}: {message}`
  · Network error → ApiError 502 with "Upstream fetch failed: ..."

Verification

- `bun test src/server/__tests__/providers.test.ts -t "fetchUpstreamModels"`
  → 5/5 pass.
- `bun run lint` (desktop): tsc --noEmit clean (after `bun install` to
  pick up the @codemirror packages added by an earlier PR — orthogonal
  to this change).
- All existing provider tests unchanged (no regression in test file).

Tested: server unit (5 cases covering wire shape, URL hygiene, error
paths); existing testProviderConfig tests still pass; desktop tsc clean.
Not-tested: live smoke against an actual self-hosted relay (no provider
fixture configured locally). The bug repro is well-understood: the
secure-context webview's mixed-content block is documented browser
behaviour and the fix removes the renderer-side fetch entirely.

Confidence: high
Scope-risk: narrow

* test(providers): add desktop-side fetchModels client test (clears change-policy block)

Locks the renderer→server contract: providersApi.fetchModels must POST to the local server with baseUrl/apiKey/apiFormat in the body, NOT fetch the upstream URL directly. The same-origin assertion guards against any future regression that reintroduces the mixed-content / CORS bug class.

Two cases:

- success path: upstream JSON forwarded verbatim, body shape locked

- error path: 502 from server surfaces upstream message in thrown error

---------

Co-authored-by: 你的姓名 <you@example.com>
2026-06-12 14:08:55 +08:00
..

Claude Code Haha Desktop

基于 Tauri 2 + React 的桌面客户端。

开发

bun install
bun run tauri dev

构建

# macOS (Apple Silicon)
./scripts/build-macos-arm64.sh

# Windows (x64, MSI only)
.\scripts\build-windows-x64.ps1

构建产物位于 build-artifacts/ 目录,文件名会显式包含平台、架构和包类型。

常见问题

macOS 提示"已损坏,无法打开"

xattr -cr /Applications/Claude\ Code\ Haha.app