feat: route Heicode desktop login through code.xinghanlab.com Manager

Three concrete pieces:

1. Provider preset taijiaicloud now points at https://code.xinghanlab.com
   instead of the old api.taijiaicloud.com. Together with the stock
   resolveOAuthConfig fallback (<baseUrl>/heicode/oauth/authorize), this
   flips oauthEnabled on for the login card and turns the existing
   browser-redirect bridge into the default flow. Card name + promo
   updated to reflect that this is "log in via Heicode Manager".

2. loginAndActivate softens its model probe. /v1/models is best-effort:
   only hard 401/403 auth failures abort login. 5xx / panics / empty
   lists fall back to preset.defaultModels so the user lands inside the
   app even if the gateway transiently misbehaves; they can re-pick
   models from Settings later.

3. heicode_oauth.go fallback page: /login → /sign-in (matches the
   actual SPA route), title/copy de-branded from "HeiCode/新 API 控制台"
   to plain "Heicode 控制台".

Also picks up the prior unstaged Windows polish: WindowControls (min/
max/close + drag region) on the login screen, ProviderLoginCard +
globals.css refinements that landed in earlier MSI builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:55:57 +08:00
co-authored by Claude Opus 4.7
parent 7ec7778c18
commit 4665f88921
9 changed files with 127 additions and 72 deletions
@@ -9,6 +9,8 @@
"core:window:allow-minimize",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"core:event:default",
"shell:allow-open",
{
"identifier": "shell:allow-execute",
+5 -3
View File
@@ -12,14 +12,16 @@
"app": {
"windows": [
{
"title": "HeiCode",
"title": "Heicode",
"width": 1440,
"height": 960,
"minWidth": 960,
"minHeight": 640,
"decorations": true,
"decorations": false,
"transparent": false,
"acceptFirstMouse": true
"acceptFirstMouse": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true
}
],
"security": {
@@ -1,5 +1,9 @@
import { useUIStore } from '../../stores/uiStore'
import { useTranslation } from '../../i18n'
import { WindowControls } from './WindowControls'
const isMacHost = typeof navigator !== 'undefined'
&& /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent || '')
export function TitleBar() {
const { activeView, setActiveView } = useUIStore()
@@ -10,12 +14,12 @@ export function TitleBar() {
className="h-[var(--titlebar-height)] flex items-center border-b border-[var(--color-border)] bg-[var(--color-surface)] select-none"
data-tauri-drag-region
>
{/* macOS traffic light spacer */}
<div className="w-[78px] flex-shrink-0" data-tauri-drag-region />
{/* macOS traffic-light spacer (native overlay buttons appear here) */}
{isMacHost ? <div className="w-[78px] flex-shrink-0" data-tauri-drag-region /> : <div className="w-3" data-tauri-drag-region />}
{/* Logo */}
<div className="flex items-center gap-2 mr-4" data-tauri-drag-region>
<span className="text-xs font-bold tracking-[0.18em] text-[var(--color-brand)] uppercase">Heicode</span>
<span className="text-xs font-bold tracking-[0.22em] text-[var(--color-brand)] uppercase">Heicode</span>
</div>
{/* Navigation arrows */}
@@ -57,11 +61,12 @@ export function TitleBar() {
</TabButton>
</div>
{/* Right: Settings */}
<div className="flex items-center gap-2 mr-4">
<button className="p-1.5 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
{/* Right: Settings + Window controls */}
<div className="flex items-center h-full">
<button className="p-1.5 mr-2 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
<span className="material-symbols-outlined text-[18px]">settings</span>
</button>
<WindowControls />
</div>
</div>
)
@@ -2,10 +2,14 @@
//
// 全屏登录页 — 当 Heicode 检测到尚未登录时由 AppShell 渲染。
// 仅 1 个登录入口:TaijiAICloud。
//
// 顶部自绘 36px 高的 chrome(drag region + WindowControls),因为
// tauri.conf.json 已设 decorations:false 以摆脱 Windows 系统灰边框。
import { useEffect } from 'react'
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
import { ProviderLoginCard } from './ProviderLoginCard'
import { WindowControls } from '../layout/WindowControls'
import { useTranslation } from '../../i18n'
export function HeicodeLoginPage() {
@@ -30,47 +34,69 @@ export function HeicodeLoginPage() {
return (
<div
className="flex h-screen w-screen flex-col items-center justify-center overflow-auto bg-[var(--color-surface)] px-6 py-10"
className="relative flex h-screen w-screen flex-col bg-[var(--color-background)]"
data-testid="heicode-login-page"
>
<div className="mb-10 text-center">
<h1 className="text-4xl font-bold tracking-[0.18em] uppercase text-[var(--color-brand)]">
Heicode
</h1>
<p className="mt-3 text-sm text-[var(--color-text-secondary)]">
{t('login.subtitle')}
</p>
{/* Chrome strip — drag region + window controls */}
<div
className="flex h-9 w-full items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-surface)]"
data-tauri-drag-region
>
<div className="flex items-center gap-2 px-4" data-tauri-drag-region>
<span className="text-[10px] font-bold tracking-[0.22em] uppercase text-[var(--color-brand)]">
Heicode
</span>
</div>
<WindowControls />
</div>
{!hasFetched && isLoading ? (
<div className="text-sm text-[var(--color-text-tertiary)]">
{t('common.loading')}
</div>
) : null}
{/* Subtle radial backdrop for premium feel */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
'radial-gradient(circle at 50% 30%, rgba(197,165,114,0.06), transparent 55%)',
}}
/>
{hasFetched && providers.length === 0 ? (
<div className="rounded-md border border-[var(--color-border-separator)] bg-[var(--color-surface-elevated,#fff)] p-4 text-sm text-[var(--color-error)]">
{t('login.errors.noProviders')}
{/* Content */}
<div className="relative flex flex-1 flex-col items-center justify-center overflow-auto px-6 py-10">
<div className="mb-12 text-center">
<h1 className="text-5xl font-bold tracking-[0.18em] uppercase text-[var(--color-brand)]">
Heicode
</h1>
<p className="mt-4 text-sm text-[var(--color-text-secondary)]">
{t('login.subtitle')}
</p>
</div>
) : null}
{hasFetched && providers.length > 0 ? (
<div className="grid w-full max-w-md grid-cols-1 gap-6">
{providers.map((provider) => (
<ProviderLoginCard
key={provider.id}
provider={provider}
/>
))}
</div>
) : null}
{!hasFetched && isLoading ? (
<div className="text-sm text-[var(--color-text-tertiary)]">
{t('common.loading')}
</div>
) : null}
{error ? (
<div className="mt-6 max-w-xl rounded-md bg-[var(--color-error-bg,rgba(220,38,38,.06))] px-4 py-3 text-sm text-[var(--color-error)]">
{error}
</div>
) : null}
{hasFetched && providers.length === 0 ? (
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-sm text-[var(--color-error)]">
{t('login.errors.noProviders')}
</div>
) : null}
{hasFetched && providers.length > 0 ? (
<div className="grid w-full max-w-md grid-cols-1 gap-6">
{providers.map((provider) => (
<ProviderLoginCard key={provider.id} provider={provider} />
))}
</div>
) : null}
{error ? (
<div className="mt-6 max-w-xl rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-4 py-3 text-sm text-[var(--color-error)]">
{error}
</div>
) : null}
</div>
</div>
)
}
@@ -68,36 +68,34 @@ export function ProviderLoginCard({ provider }: Props) {
const local = isLocalBaseUrl(provider.baseUrl)
return (
<div className="flex flex-col gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border-separator)] bg-[var(--color-surface)] p-6 shadow-[var(--shadow-card)]">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">
<div className="relative flex flex-col gap-5 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] p-7 shadow-[var(--shadow-dropdown)] backdrop-blur-sm transition-colors duration-200 hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))]">
<div className="flex items-center justify-between gap-3">
<h3 className="text-base font-semibold tracking-wide text-[var(--color-text-primary)]">
{provider.name}
</h3>
{provider.oauthEnabled ? (
<span className="rounded-full bg-[var(--color-success-bg,rgba(22,163,74,.1))] px-2 py-0.5 text-xs text-[var(--color-success)]">
<span className="rounded-full border border-[var(--color-success)]/25 bg-[var(--color-success)]/10 px-2.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--color-success)]">
{t('login.tags.recommended')}
</span>
) : (
<span className="rounded-full bg-[var(--color-surface-muted,#f0f0f0)] px-2 py-0.5 text-xs text-[var(--color-text-tertiary)]">
<span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-2.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--color-text-tertiary)]">
{t('login.tags.comingSoon')}
</span>
)}
</div>
{/* Always-visible baseUrl indicator. Helps the user immediately tell
whether this card is pointing at the public TaijiAICloud endpoint
or has been overridden to a local heicode dev gateway. */}
{/* baseUrl indicator */}
<div
className="flex flex-wrap items-center gap-1.5 text-xs text-[var(--color-text-tertiary)]"
className="flex flex-wrap items-center gap-1.5 text-[11px] text-[var(--color-text-tertiary)]"
data-testid={`heicode-login-card-baseurl-${provider.id}`}
>
<span>{t('login.baseUrl.label')}</span>
<code className="rounded bg-[var(--color-surface-muted,#f0f0f0)] px-1.5 py-0.5 font-mono text-[11px] text-[var(--color-text-secondary)] break-all">
<span className="opacity-70">{t('login.baseUrl.label')}</span>
<code className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[11px] text-[var(--color-text-secondary)] break-all">
{provider.baseUrl}
</code>
{local ? (
<span
className="rounded-full bg-[var(--color-warning-bg,rgba(217,119,6,.1))] px-2 py-0.5 text-[10px] uppercase tracking-wide text-[var(--color-warning)]"
className="rounded-full border border-[var(--color-warning)]/25 bg-[var(--color-warning)]/10 px-2 py-0.5 text-[10px] uppercase tracking-wider text-[var(--color-warning)]"
title={t('login.baseUrl.localHint')}
data-testid={`heicode-login-card-local-tag-${provider.id}`}
>
@@ -107,7 +105,7 @@ export function ProviderLoginCard({ provider }: Props) {
</div>
{provider.promoText ? (
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
{provider.promoText}
</p>
) : null}
@@ -117,7 +115,7 @@ export function ProviderLoginCard({ provider }: Props) {
type="button"
onClick={handleOAuth}
disabled={!provider.oauthEnabled || isLoggingIn || busy !== null}
className="rounded-md bg-[image:var(--gradient-btn-primary)] px-4 py-2.5 text-sm text-[var(--color-btn-primary-fg)] shadow-[var(--shadow-button-primary)] transition-opacity hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-50"
className="group/btn relative overflow-hidden rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-4 py-2.5 text-sm font-medium text-[var(--color-on-primary)] shadow-[var(--shadow-button-primary)] transition-all duration-200 hover:bg-[var(--color-primary-fixed-dim)] hover:shadow-[0_8px_24px_rgba(197,165,114,0.28)] disabled:cursor-not-allowed disabled:border-[var(--color-border)] disabled:bg-[var(--color-surface-container-high)] disabled:text-[var(--color-text-tertiary)] disabled:shadow-none"
>
{busy === 'oauth'
? t('login.oauth.opening')
@@ -133,7 +131,7 @@ export function ProviderLoginCard({ provider }: Props) {
</div>
{localError ? (
<div className="rounded-md bg-[var(--color-error-bg,rgba(220,38,38,.06))] px-3 py-2 text-xs text-[var(--color-error)]">
<div className="rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-3 py-2 text-xs text-[var(--color-error)]">
{localError}
</div>
) : null}
+16 -4
View File
@@ -191,6 +191,7 @@
/* Border aliases */
--color-border: rgba(255, 255, 255, 0.08);
--color-border-hi: rgba(255, 255, 255, 0.14);
--color-border-focus: var(--color-primary);
--color-border-separator: rgba(255, 255, 255, 0.06);
@@ -337,6 +338,7 @@
--sidebar-panel-bg-image: none;
--color-border: rgba(0, 0, 0, 0.08);
--color-border-hi: rgba(0, 0, 0, 0.14);
--color-border-focus: var(--color-primary);
--color-border-separator: rgba(0, 0, 0, 0.06);
@@ -637,11 +639,21 @@ button, input, textarea, select, a, [role="button"] {
color: inherit;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
/* Scrollbar — thin, subtle, premium */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-outline); border-radius: 9999px; opacity: 0.3; }
::-webkit-scrollbar-thumb:hover { opacity: 0.6; }
::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--color-outline) 30%, transparent);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
transition: background 160ms ease;
}
::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--color-outline) 60%, transparent);
background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }
.sidebar-scroll-area {
scrollbar-width: thin;
+12 -2
View File
@@ -288,7 +288,14 @@ async function loginAndActivate(
apiFormat: preset.apiFormat,
})
if (probe.models.length === 0) {
// Soft probe: if /v1/models is unreachable / panics / returns empty, fall back
// to the preset's defaultModels so the user can still complete login. They can
// re-pick models later from Settings once the gateway returns a proper list.
// Hard auth failures (401/403) still surface as model probe errors via fetchProviderModels;
// we treat any other failure (5xx, panic, network) as "skippable".
const isHardAuthError =
probe.error?.includes('HTTP 401') || probe.error?.includes('HTTP 403')
if (probe.models.length === 0 && isHardAuthError) {
throw ApiError.badRequest(
`校验失败: ${probe.error ?? '该 API Key 在 ' + preset.name + ' 上无可用模型'}`,
)
@@ -299,7 +306,10 @@ async function loginAndActivate(
const availableIds = new Set(probe.models.map(m => m.id))
const pick = (preferred: string): string => {
if (preferred && availableIds.has(preferred)) return preferred
return probe.models[0]?.id ?? preferred
if (probe.models.length > 0) return probe.models[0].id
// No live model list — use the preset default verbatim. The user can change
// it later once the gateway is healthy.
return preferred
}
const models = {
main: pick(preset.defaultModels.main),
@@ -15,8 +15,8 @@
},
{
"id": "taijiaicloud",
"name": "TaijiAICloud",
"baseUrl": "https://api.taijiaicloud.com",
"name": "Heicode",
"baseUrl": "https://code.xinghanlab.com",
"apiFormat": "anthropic",
"defaultModels": {
"main": "claude-sonnet-4-6",
@@ -25,9 +25,9 @@
"opus": "claude-opus-4-6"
},
"needsApiKey": true,
"websiteUrl": "https://api.taijiaicloud.com",
"apiKeyUrl": "https://api.taijiaicloud.com/dashboard/keys",
"promoText": "TaijiAICloud 是 HeiCode 推荐的国内 LLM 网关,原生支持 Anthropic /v1/messages 协议,覆盖 GPT / Claude / Gemini 等主流模型。",
"websiteUrl": "https://code.xinghanlab.com",
"apiKeyUrl": "https://code.xinghanlab.com/dashboard/keys",
"promoText": "通过 Heicode Manager 登录,自动配置模型与额度。",
"featured": true,
"defaultEnv": {
"API_TIMEOUT_MS": "3000000",
+5 -5
View File
@@ -152,14 +152,14 @@ func renderHeicodeLoginRequired(c *gin.Context, state, redirectURI, providerID s
url.QueryEscape(redirectURI),
url.QueryEscape(providerID),
)
loginURL := "/login"
loginURL := "/sign-in"
body := fmt.Sprintf(`<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>HeiCode 登录授权</title>
<title>Heicode 登录授权</title>
<style>
:root { color-scheme: light dark; }
html,body { margin:0; padding:0; }
@@ -175,11 +175,11 @@ func renderHeicodeLoginRequired(c *gin.Context, state, redirectURI, providerID s
</head>
<body>
<div class="card">
<h1>请先登录新 API 控制台</h1>
<p>HeiCode 客户端正在等待你完成授权。请在新打开的页面中登录到 <strong>%s</strong>,登录成功后本页会自动跳转回 HeiCode。</p>
<h1>请先登录 Heicode 控制台</h1>
<p>Heicode 桌面端正在等待你完成授权。请在新打开的页面中登录到 <strong>%s</strong>,登录成功后本页会自动跳转回客户端。</p>
<a id="goLogin" class="btn" href="%s" target="_blank" rel="noopener">打开登录页</a>
<p class="muted">如果浏览器没有自动跳转,请手动在登录后回到此页面,或刷新本页。</p>
<p class="muted">提供方:<code>%s</code></p>
<p class="muted">租户:<code>%s</code></p>
</div>
<script>
(function(){