Skip to content

feat: 完成 Chat2API 从 Electron 到纯 Web + Node.js 后端架构迁移#139

Open
zhaiiker wants to merge 54 commits into
xiaoY233:mainfrom
zhaiiker:main
Open

feat: 完成 Chat2API 从 Electron 到纯 Web + Node.js 后端架构迁移#139
zhaiiker wants to merge 54 commits into
xiaoY233:mainfrom
zhaiiker:main

Conversation

@zhaiiker
Copy link
Copy Markdown

概述
本 PR 将 Chat2API 从 Electron 桌面应用架构完整迁移至 纯 Web 前端(React SPA)+ Node.js 后端(Koa) 架构,实现单端口部署,适用于 VPS、Docker、云服务器等远程场景。

架构变化
迁移前(Electron) 迁移后(纯 Web)
运行方式 桌面客户端安装运行 node dist/backend/index.js 一行启动
前端交付 Electron BrowserWindow 后端自动托管 SPA 静态文件
访问方式 本机窗口 浏览器访问 http://host:8080/
部署要求 Windows/macOS 桌面环境 任意 Linux/Windows 服务器
端口 前后端分离 单端口统一(Web UI + API + 管理接口)
新增功能

  1. 单端口 SPA 托管
    后端 Koa 服务器自动检测 dist/frontend/ 目录并托管,所有客户端路由 fallback 到 index.html,无需额外的 Nginx 配置。

http://host:8080/ → Web 管理界面
http://host:8080/v1/ → OpenAI 兼容代理 API
http://host:8080/v0/management/ → 管理 API
2. 请求日志页面
新增完整的请求日志查看功能(/logs 页面):

表格展示所有 API 请求(时间、状态、模型、供应商、延迟)
支持状态筛选(成功/失败)和关键词搜索
5 秒自动刷新
点击查看请求详情(请求体、响应体、错误信息等)
一键清空日志
3. 控制台命令取 Token(替代 Electron 内置浏览器登录)
Electron 版本通过内置 BrowserWindow 打开供应商页面自动获取 Token。纯 Web 版本无法做到这一点,因此提供了全新的 控制台命令 方案:

每个供应商自动生成对应的一行 JS 命令
用户在供应商页面的浏览器 Console 中粘贴执行即可获取完整 Token
无需任何跨域请求、无需书签栏、无需额外插件
支持中英双语操作教程
各供应商命令示例:

供应商 控制台命令
DeepSeek localStorage.getItem('userToken')
GLM document.cookie.match(/chatglm_refresh_token=([^;]+)/)?.[1]
Kimi document.cookie.match(/kimi-auth=([^;]+)/)?.[1]
Qwen document.cookie.match(/tongyi_sso_ticket=([^;]+)/)?.[1]
Qwen-AI localStorage.getItem('token')
MiniMax JSON.stringify({token: localStorage.getItem('_token'), userId: localStorage.getItem('_userId')})
Perplexity document.cookie.match(/__Secure-next-auth.session-token=([^;]+)/)?.[1]
4. API Key 鉴权与 SPA 共存
API Key 认证仅保护 /v1/* 代理接口,Web UI 静态资源和管理接口各自独立鉴权,互不干扰。

部署方式

安装依赖 + 构建

npm ci
npm run build

启动(默认 8080 端口)

node dist/backend/index.js
支持环境变量或 .env 文件配置:

HOST=0.0.0.0
PORT=8080

CHAT2API_MANAGEMENT_SECRET=your-secret

CHAT2API_DATA_DIR=/var/lib/chat2api

Docker 部署同样支持,详见 Dockerfile。

向后兼容
✓ OpenAI 兼容接口(/v1/chat/completions 等)无变化
✓ Management API 无变化
✓ 数据文件格式(~/.chat2api/data.json)无变化
✓ 所有已有账户和配置自动保留

zhaiiker and others added 30 commits May 18, 2026 10:06
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Remove leftover Electron-only modules: backend/tray, backend/window,
  backend/logger, backend/tray.ts and backend/types/electron.d.ts.
- Drop now-unused electron and electron-updater deps from package.json.
- Fix broken '../../../../shared/types' imports in frontend (services,
  stores, components) to the @shared/types alias.
- Add a runtime electronShim that materialises window.electronAPI on
  top of ApiService so legacy components keep working over HTTP, and
  emulate push events (proxy status, config, logs) via polling.
- Backend now optionally serves the built frontend with SPA fallback so
  a single port can host the OpenAI proxy, the management API and the
  web UI simultaneously.
- Auto-generate a Management API secret on first run and print it to
  stdout once; allow pinning via CHAT2API_MANAGEMENT_SECRET.
- Bind 0.0.0.0 by default and surface OpenAI / Management / UI URLs in
  the boot log.
- Clean up backend/oauth/manager.ts (drop dead inAppLogin imports and
  unused storeManager) and re-export inAppLogin types correctly under
  isolatedModules.
- Add Dockerfile, docker-compose.yml, .dockerignore, .env.example and
  DEPLOYMENT.md for VPS / 24x7 deployments.

Co-authored-by: ZhaiKer <216113428+zhaiiker@users.noreply.github.com>
Web migration cleanup, runtime electronAPI shim, and VPS deployment
User-facing change: the management secret no longer surfaces in the UI.
On first launch the backend boots into 'first-run' mode and the web UI
shows a 'create administrator password' screen (>=8 chars). After that,
returning visitors see a normal password login. Both screens exchange
the password for a long-lived management secret which the SPA caches
in localStorage and sends as Authorization: Bearer.

Backend
- Add public /v0/management/auth/{status,setup,login} routes plus an
  authed /change_password endpoint. Setup and login are rate limited
  per remote address (10 attempts / 60s) and password hashing uses
  scrypt with a per-install random salt.
- managementAuthMiddleware now lets the public auth paths through
  unauthenticated; the global enable check in proxy/server.ts does the
  same so first-run still works when the API is otherwise disabled.
- Extend ManagementApiConfig with firstRunCompleted, passwordHash,
  passwordSalt and passwordSetAt; the password fields are redacted by
  the config GET endpoint alongside the secret itself.
- Default management API to enabled with firstRunCompleted=false so a
  fresh install lands on the setup screen automatically.
- backend/index.ts no longer auto-generates a secret on first boot; it
  prints a 'first run detected' banner instead and still honours the
  CHAT2API_MANAGEMENT_SECRET escape hatch for headless setups.

Frontend
- ApiService.auth.{status,setup,login,changePassword} added.
- The 401 handler in the response interceptor now ignores /auth/* so a
  failed login no longer blows away other state.
- Rewrite AuthProvider as a small state machine (loading / firstRun /
  login / authenticated / offline) that talks to ApiService.auth. The
  cached secret is validated against /config on boot.
- New PasswordSettings card surfaced under Settings - Security so the
  operator can change the password (and optionally rotate the secret)
  later without leaving the UI.

Docs: DEPLOYMENT.md updated to describe the new first-run flow.
Replace API secret login with first-run password flow
The conf package has been pure ESM since v11, but the backend is
compiled to CommonJS and loaded by ts-node, so 'require("conf")'
fails at runtime ('Cannot find module conf' / 'Cannot find name Conf').
The previous code tried a dynamic 'await import("conf")', but a second
'new Conf({...})' inside the recovery branch reused the symbol out of
scope, which is what TypeScript was actually complaining about.

Rather than push the whole backend to ESM, drop conf entirely. The only
methods we actually use are .get(), .set() and .clear(); a 100-line
JsonStore helper covers them with atomic writes (write-tmp-then-rename)
and top-level merging of defaults so newly-added schema keys appear
automatically.

- Add backend/store/jsonStore.ts.
- Switch backend/store/store.ts to JsonStore<StoreSchema>; keep the
  existing 'back up corrupted file and reinitialise' recovery branch.
- Drop conf from package.json.
Replace conf with an in-tree JSON store (fixes ESM/CJS boot crash)
In axios v1 response.headers is AxiosHeaders, so
response.headers['content-type'] is typed as
string | number | true | string[] | AxiosHeaders. Assigning that
union to a string-typed local fails strict type checking under
ts-node, which is what blocks 'npm run dev:backend'.

Coerce through a typeof check so mimeType stays a string.

Note: the same fix was previously pushed to PR #3 as a follow-up
commit, but PR #3 was merged before that commit landed, so main
still carried the broken line. Re-applying it directly here.
Fix axios header type narrowing in GLM file upload (re-apply)
Same axios v1 issue as glm.ts: response?.headers?.['content-encoding']
is typed as string | number | boolean | string[] | AxiosHeaders, so
calling .toLowerCase() on it fails strict type checking under ts-node.

Coerce both content-encoding reads (handleStream and handleNonStream)
through a typeof check so contentEncoding stays string | undefined for
the gzip/deflate/br/zstd switch downstream.
Two changes that work together:

1. Tailwind was scanning ./src/renderer/** which is the legacy Electron
   path and no longer exists. Vite's dev console was warning 'No utility
   classes were detected in your source files', and indeed every Tailwind
   class on the auth screen was being dropped, which is why it looked
   completely unstyled in the screenshots. Point content[] at frontend/.

2. Rewrite AuthProvider to match the rest of the app's visual language:
   - bokeh-bg backdrop with two animated blobs.
   - glass-card panel with a small gradient accent bar on top.
   - logo badge with a subtle accent ring + glow.
   - PasswordField helper that uses .glass-input plus a Show/Hide toggle.
   - Primary buttons use the .glass-btn-primary neumorphic style so dev
     and prod look the same regardless of theme.
   - Subtle scale-in animation on mount; tagline footer.
   The form logic, state machine and ApiService.auth.* calls are
   unchanged - this is purely a visual pass.
The frontend was hitting four endpoints that didn't exist on the
backend, surfacing as 404/405 in the access log right after login:

  GET  /v0/management/statistics/today          -> 404
  POST /v0/management/providers/check_all_status -> 405
  GET  /v0/management/providers/builtin          -> 404
  GET  /v0/management/sessions/config            -> 404

Add them. Two ordering subtleties matter:

- /providers/builtin and /providers/check_all_status MUST be registered
  before /providers/:id, otherwise the dynamic :id route gobbles the
  literal path. Same trick for /sessions/config vs /sessions/:id.
- /providers/:id/check_status was also missing despite the front-end
  shim referencing it, so add that too while we're here.

The new endpoints delegate to existing helpers (ProviderManager,
ProviderChecker, storeManager) and use dynamic imports so we don't
pull the providers module into the management routes' static graph.

Side cleanup: rename postcss.config.js -> .mjs and tailwind.config.js
-> .mjs. Both files already used 'export default' so they were ESM in
disguise. Without the .mjs extension, Vite was loading them via the
CJS bridge and printing 'The CJS build of Vite's Node API is
deprecated' on every dev start. The rename silences that warning
without touching the rest of the build.
The 'Open OAuth Login' button in AddAccountDialog / AddProviderDialog
was wired to oauth.startInAppLogin, which the new web shim throws
'Not supported in web version' from. The Electron version used to spawn
its own Chromium window and harvest the token from inside it; that does
not translate to a server-side service that the operator reaches over
HTTP from a different machine.

Replace it with a curated step-by-step extraction guide
(TokenExtractionGuide):

  1. Big 'Open login page' button -> window.open(loginUrl, '_blank')
  2. Inline DevTools navigation hint, e.g.
     'Application -> Local Storage -> chat.deepseek.com -> userToken'.
  3. Pre-keyed textarea for the token (and optional extras for the
     providers that need more than one field, like MiniMax / Mimo).
  4. Save calls /v0/management/oauth/login_with_token, which already
     validates the value against the provider's API. Same code path the
     Electron version used at the very end.

Provider catalogue covers deepseek / glm / kimi / minimax / qwen /
qwen-ai / zai / perplexity / mimo. Falls back to a 'switch to Manual
Input' hint for anything else.

Wire the new component into AddAccountDialog and AddProviderDialog,
deleting their now-dead handleOpenOAuthBrowser handlers and the
isOAuthLoading / oauthStatus state they used.

DEPLOYMENT.md: add an 'Adding provider accounts (OAuth tokens)' section
describing the in-page flow, plus an optional VNC-sidecar recipe
(referencing iBUHub/AIStudioToAPI) for operators who really want a
fully remote browser. The sidecar is opt-in - the main image stays
~150 MB instead of ballooning to ~2 GB just to satisfy a one-time
token harvest.
- Remove all VNC/noVNC references from DEPLOYMENT.md
- Add bookmarklet ticket store (backend/oauth/bookmarkletTickets.ts)
- Add bookmarklet script builder (backend/oauth/bookmarkletScript.ts)
- Add bookmarklet ingest route with public/auth endpoints
  (backend/proxy/routes/management/oauth/bookmarklet.ts)
- Register bookmarklet router in management index
- Add ApiService.oauth.bookmarklet.{issue,poll,cancel} on frontend
- Create BookmarkletPanel component with drag-to-bookmark UX + polling
- Integrate BookmarkletPanel into TokenExtractionGuide as the
  recommended flow; collapse old DevTools paste as a fallback
- Add CHAT2API_DISABLE_BOOKMARKLET env var documentation

Co-authored-by: ZhaiKer <216113428+zhaiiker@users.noreply.github.com>
- Add oauth.bookmarklet.* translation keys to zh-CN.json and en-US.json
- Refactor BookmarkletPanel.tsx to use useTranslation()/t()
- Refactor TokenExtractionGuide.tsx manual-fallback toggle to use i18n
- Fix data persistence: jsonStore.ts now recovers from .tmp file when
  the main data.json is corrupted or empty (prevents data loss on
  interrupted atomic writes or OOM kills)
Prints the resolved data directory, homedir(), env var, and
provider/account count to the console on every boot. This helps
diagnose why data may appear to vanish between dev restarts on
Windows PowerShell.
Log the full provider list from data.json and the BUILTIN_PROVIDERS
ids on every startup so we can see exactly why providers end up empty.
…s on restart

Root cause: POST /v0/management/providers was not forwarding the
client-supplied 'id' field to ProviderManager.create(). This caused
builtin providers (deepseek, glm, kimi, etc.) to get random generated
IDs instead of their canonical names. On restart, initializeDefaultProviders
filters providers by checking builtinIds.includes(p.id) — random IDs
fail that check and get silently dropped, leaving providers=[] while
accounts remain intact.

Fix: add 'id: request.id' to the ProviderManager.create() call and
make 'id' an optional field on CreateProviderRequest.
The SENSITIVE_KEYS list included 'apiKeys' which caused the entire
apiKeys array values to be replaced with '***' in the config response.
API keys need to be visible to operators (they configure AI clients
with them). Remove 'apiKeys' from the mask list and add 'passwordHash'
and 'passwordSalt' instead (those should never be returned).
maskSensitiveObject was masking any field named 'key' (catching
apiKeys[].key) and maskConfig was explicitly replacing apiKeys[].key
with '***'. This caused the frontend store to overwrite its local
state with starred-out values after any config update, making API
keys permanently invisible.

Now only managementApiSecret, passwordHash, passwordSalt, and
credentials fields are masked. apiKeys are returned in full.
1. Add POST /v0/management/accounts/validate_token endpoint that
   validates credentials BEFORE account creation. The frontend calls
   this when the user clicks 'Validate' in the add-provider dialog.
   Without it, validation always returns 404.

2. Remove overly aggressive masking in maskSensitiveObject that was
   replacing any field named 'key' with '***' (broke apiKeys display).
Frontend expects { valid, error?, userInfo? } but validateCredentials
returns { valid, error?, validatedAt, accountInfo? }. Map accountInfo
to userInfo in the response so the green success banner renders.
Root causes found and fixed:

1. Backend accounts route was masking all credentials with '***' before
   returning them. Since the management API is already bearer-auth
   protected, this was unnecessary and caused the frontend to store
   '***' as the actual credential value. REMOVED maskCredentials
   entirely — all endpoints now return decrypted cleartext.

2. electronShim.accounts.validate() checked res.success but the axios
   interceptor already unwraps {success,data}→data, so res is actually
   {valid:true,...}. Changed to check res.valid — this was the direct
   cause of 'credentials invalid' always showing after save.

3. AddProviderDialog only showed green success banner when BOTH
   valid=true AND userInfo existed. DeepSeek users with no profile
   name/email got valid=true but empty userInfo fields, causing no
   feedback. Changed condition to only require valid=true.
1. PasswordSettings.tsx: replace all hardcoded English strings with
   t() calls using new 'password.*' i18n keys (zh-CN + en-US).

2. GeneralSettings.tsx: remove autoStart, minimizeToTray, and
   closeBehavior cards — these are Electron desktop features that
   don't apply to a headless VPS web deployment.
Use Number.isFinite() guard before displaying avgLatency. When there
are no successful requests yet, the division produces NaN which was
passed directly to the UI as 'NaNms'.
- HIGH: Remove all console.log statements that leak plaintext credentials,
  tokens, and API response data (store.ts, deepseek, glm, minimax, zai,
  loadbalancer, checker, oauth adapters)

- MEDIUM: Restrict CORS for management API (/v0/management) to configured
  origins via CHAT2API_CORS_ORIGINS env var; defaults to same-origin only.
  Proxy API (/v1/) retains permissive CORS as intended.

- HIGH: Use timing-safe comparison (crypto.timingSafeEqual) for management
  API secret validation to prevent timing attacks.

- LOW: Reduce body parser size limit from 50MB to 10MB to mitigate
  potential DoS via oversized payloads.

- LOW: Add X-Management-Secret to Access-Control-Allow-Headers.

Co-authored-by: ZhaiKer <216113428+zhaiiker@users.noreply.github.com>
The GLM provider stores its refresh token in a cookie, not localStorage.
The bookmarklet was looking in the wrong place, causing token not found
errors on https://chatglm.cn/.
zhaiiker and others added 22 commits May 18, 2026 17:08
Update package-lock.json to resolve npm ci mismatch errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This option was removed in TypeScript 5.5+. Use noImplicitAny: false
(already set) to suppress implicit any errors instead. Fixes:

  error TS5102: Option 'suppressImplicitAnyIndexErrors' has been removed.

Co-authored-by: zhaiker01 <285887163+zhaiker01@users.noreply.github.com>
- Fix import paths: '../../shared/types' -> '../shared/types' in oauth/types
  and providers/custom (file is at backend/shared/types, not /shared/types).
- shared/types: import LegacyToolPromptConfig and ToolCallingConfig as types
  before re-exporting so they're available in local scope (fixes TS2304).
- shared/types: add credentialFields to Provider interface so providers/
  custom.ts can construct Provider with credentialFields.
- store/types: add credentialFields to Provider so accountUtils can read it.
- oauth/types: add 'token' to TokenType union (used by deepseek/minimax
  ManualTokenConfig entries).
- oauth/types: relax MANUAL_TOKEN_CONFIGS to Partial<Record<...>> so it
  doesn't need entries for every ProviderType (mimo, zai are not configured).
- proxy/adapters/index: PerplexityStreamHandler lives in perplexity-stream,
  not perplexity.
- proxy/adapters/prompt: convert getPromptVariant() PromptVariant|null
  results to PromptVariant|undefined where needed (TransformResult.variant
  and toolsToPrompt() expect undefined, not null).
- proxy/adapters/prompt: guard part.text type narrowing before push.
- proxy/adapters/prompt/DefaultPromptAdapter: import TOOL_PROMPT_SIGNATURES
  from utils/tools (where it's defined) instead of constants/signatures.
- proxy/index: routes/index.ts only has a default export; use
  'export { default as routes }'.
- proxy/utils/accountUtils: default account name to '' when accountInfo is
  undefined (Account.name is required).
- proxy/utils/clientDetector: look up promptSectionMarkers via
  CLIENT_SIGNATURES[clientType] instead of from DetectionResult (which
  doesn't carry that field).
- proxy/utils/index: avoid duplicate re-exports between toolParser/index
  and the deprecated streamToolHandler (createBaseChunk, flushToolCallBuffer,
  shouldBlockOutput).
- store/store: export StoreManager class.
- store/index: import storeManager so initializeStore() can call it.
The createAccount helper was returning userId, usageCount, and metadata,
none of which exist on the Account interface (which uses requestCount,
not usageCount, and does not track userId or metadata). Drop them to
match the declared return type Omit<Account, 'id' | 'createdAt' | 'updatedAt'>.

Fixes:
  error TS2353: Object literal may only specify known properties, and
  'userId' does not exist in type 'Omit<Account, ...>'.
The Logs page imports { RequestLogList } from '@/components/logs', but the
component never existed in this repo, causing vite build to fail with:

  [vite:load-fallback] Could not load .../frontend/src/components/logs

Add a real implementation that:
- Lists request logs in a table (time, status, model, provider, account, latency)
- Supports search and status filtering (all/success/error)
- Auto-refreshes every 5 seconds and on demand
- Lets the user clear all logs (with confirm dialog)
- Opens a detail dialog with tabs (Info, User Input, Request, Response, Error)

Also:
- Export the RequestLogEntry type from @shared/types so api.ts and the new
  component can import it from a single location.
- Update .gitignore: the existing 'logs/' rule was unintentionally hiding
  the new components/logs source folder. The un-ignore line still pointed
  at the old Electron path src/renderer/src/components/logs/, so add
  !frontend/src/components/logs/ for the post-migration layout.
The handler for GET / was setting ctx.status = 404 and returning, with a
comment saying 'let the static fallback render index.html'. But koa-router
will not invoke later middleware if the handler returns without calling
next(), so the static asset fallback (which knows how to serve index.html)
never ran. Browsers hitting http://host:8080/ saw a bare 404.

Accept next as a parameter and await it instead of returning, so the
fallback in the trailing app.use() runs and serves index.html. Sub-routes
already worked because they never matched this handler.
The API key middleware allow-listed only '/', '/health', '/stats' and
'/v0/management/*'. Everything else (including the SPA's own
/assets/*.js, /assets/*.css, *.png, and any client-side route like
/dashboard) was forced through API key validation. As soon as an
operator turned on 'Enable API Key' from the management UI, the next
page load fetched /assets/index-XXXX.js, hit the gate without an
Authorization header, and got a 401. The browser then rendered a blank
page because the SPA bundle never executed.

Restrict the gate to the OpenAI-compatible proxy endpoints (/v1/*),
which is the only surface the key was ever meant to protect. The
management API still has its own auth; the SPA bundle is now served
without authentication, the same way every other SPA-host setup does.
The /v0/management/oauth/bookmarklet/ingest endpoint is the only
management route that legitimately accepts cross-origin requests —
operators run the bookmarklet from the provider's own site
(chatglm.cn, chat.deepseek.com, ...) and POST the captured token
back here. The endpoint authenticates with a one-shot ticket baked
into the bookmarklet, not with cookies, so wildcard CORS is safe.

The route handler in routes/management/oauth/bookmarklet.ts already
sets Access-Control-Allow-Origin: * for itself, but the global CORS
middleware in setupMiddleware() runs first and:

  1. Refuses to emit CORS headers for /v0/management/* unless the
     Origin is in CHAT2API_CORS_ORIGINS (which obviously won't list
     every AI provider's domain).
  2. Short-circuits OPTIONS with status 204 BEFORE the request
     reaches the router, so the route-level CORS setup never runs
     for preflight.

Net result: GLM bookmarklet (and every other provider's) sees the
preflight come back without Access-Control-Allow-Origin and aborts
with 'Failed to fetch'.

Special-case the ingest path in the global middleware so it gets
Allow-Origin:* on both the preflight and the POST. Other management
routes still go through the strict whitelist.
After the bookmarklet flow finishes, the backend GLM adapter returns
credentials as { refreshToken, accessToken } (camelCase), but the form
field is named 'refresh_token' (snake_case) and the old mapper was
looking for 'chatglm_refresh_token' (the raw cookie name) - so the
mapping silently failed and the UI complained 'fill required field:
Refresh Token' even though we already had the token in hand.

Replace the per-provider single-key map with a list of candidate keys.
For each provider we now try every name the OAuth path may emit (raw
cookie names, the bookmarklet's relabelled 'token' field, snake_case,
and the camelCase keys the adapters actually return) and pick the
first non-empty value. The candidate gets written into the form field
the credentialFields config expects.
The OAuth success handler was calling setCredentials() and
setActiveTab('manual') in the same synchronous batch. When
React renders the tab content for 'manual', the credential fields
component mounts fresh — but because the state update for credentials
may not have been committed yet when the TabsContent mounts, the
fields render as empty.

Fix: switch tab first (so the fields mount), then set credentials
in a setTimeout(0) microtask so React can flush the tab switch
before filling the inputs. Also add console.log so operators can
see exactly what the OAuth flow returned and what got mapped.
The old BookmarkletPanel required users to drag a link into their
bookmark bar, navigate to the provider page, and click the bookmark.
This was confusing and often failed due to CORS/mixed-content issues
when the backend runs on a different server.

Replace with ConsoleScriptPanel — a much simpler flow:
  1. Click 'Generate Script' (issues a ticket on the backend)
  2. Click 'Copy Script' (copies one-line JS to clipboard)
  3. Open provider page (e.g. chatglm.cn), F12 → Console → Paste → Enter
  4. Script reads token from cookie/localStorage and POSTs to backend
  5. Panel auto-polls and picks up the result

Changes:
- New: frontend/src/components/oauth/ConsoleScriptPanel.tsx
- Modified: TokenExtractionGuide now renders ConsoleScriptPanel instead
  of BookmarkletPanel as the primary OAuth capture method
- Added oauth.console i18n keys (bilingual zh-CN / en-US) with
  step-by-step instructions
- BookmarkletPanel.tsx is kept in the repo (backend ingest endpoint
  still works) but no longer referenced from the UI

The backend bookmarklet ingest endpoint is unchanged — the console
script uses the exact same ticket/ingest/poll API, just triggered from
a pasted script instead of a dragged bookmark.
Replace the ticket/fetch/polling ConsoleScriptPanel with a dead-simple
version that just shows a per-provider one-liner JS command:

  GLM:       document.cookie.match(/chatglm_refresh_token=([^;]+)/)?.[1]
  DeepSeek:  localStorage.getItem('userToken')
  Kimi:      document.cookie.match(/kimi-auth=([^;]+)/)?.[1]
  Qwen:      document.cookie.match(/tongyi_sso_ticket=([^;]+)/)?.[1]
  etc.

User flow is now:
  1. Click 'Copy Command' in Chat2API
  2. Go to provider page → F12 → Console → Paste → Enter
  3. Right-click the returned value → 'Copy string'
  4. Paste into the token field in Chat2API → Add Account

No bookmarklets, no tickets, no auto-POST, no CORS issues.
The old BookmarkletPanel and backend ingest endpoint remain in the
codebase (they still work) but are no longer referenced from the UI.
1. Fix i18n language persistence:
   - Remove i18next-browser-languagedetector to eliminate race condition
     with zustand's persist middleware (both were writing to localStorage
     independently causing conflicts on reload)
   - Read initial language directly from zustand's persisted localStorage
     key (chat2api-settings) with browser navigator fallback
   - Zustand onRehydrateStorage callback still syncs i18n on hydration

2. Fix mobile responsiveness:
   - Add MobileSidebar component with slide-in overlay for mobile nav
   - Add hamburger menu button in Header (visible only on mobile)
   - Hide desktop Sidebar on screens < md breakpoint
   - Make Dialog component width responsive (calc(100%-2rem) on mobile)
   - Hide less-important table columns (provider, account, usage, date)
     on small screens using hidden sm:table-cell / md:table-cell
   - Responsive main content padding (p-3 → sm:p-4 → md:p-6)
   - Settings tabs: 3-col grid on mobile, 5-col on sm+
   - Add mobile CSS: reduce bokeh effects, smaller table cells,
     disable tooltips on touch devices
   - Responsive header spacing and proxy status display

3. Minor improvements:
   - Add nav.menu translation key for mobile sidebar title
   - Exclude mobileSidebarOpen from zustand persistence
   - ApiKeys card header stacks vertically on mobile

Co-authored-by: zhaiker01 <285887163+zhaiker01@users.noreply.github.com>
…responsive

fix: language persistence and mobile responsiveness
@clonesht
Copy link
Copy Markdown

clonesht commented May 22, 2026

UPDATE: Fixed by updating node 20. Looks like works fine, thanks! (I will keep it here, so it can be useful if others have same problem)

Thanks for the work, but I get this error:

$ node dist/backend/index.js
◇ injected env (0) from .env // tip: ⌘ suppress logs { quiet: true }
Initializing Chat2API Backend...
[Store] CHAT2API_DATA_DIR env: (not set)
[Store] os.homedir(): /home/clone
[Store] Resolved storage path: /home/clone/.chat2api
[Store] Data directory: /home/clone/.chat2api
[Store] Data file: /home/clone/.chat2api/data.json
[Store] initializeDefaultProviders: 0 providers in data.json, 9 builtin ids: [deepseek, glm, kimi, minimax, mimo, perplexity, qwen, qwen-ai, zai]
[Store] Loaded 0 providers, 0 accounts
Storage initialized successfully

================================================================
  First run detected.
  Open the web UI to create your administrator password.
  Until you do, the management API will reject every request
  except /v0/management/auth/{status,setup,login}.
================================================================

Serving frontend from: /home/clone/temp/Chat2API-web-main/dist/frontend
[SessionManager] Cleanup scheduler started, interval: 1 minute
[SessionManager] Initialized
Proxy server started on http://0.0.0.0:8080
OpenAI-compatible API: http://127.0.0.1:8080/v1/
Management API:        http://127.0.0.1:8080/v0/management/
Web UI:                http://127.0.0.1:8080/

$ curl http://127.0.0.1:8080
Internal Server Error⏎

$ curl http://127.0.0.1:8080/v0/management/
{"error":{"message":"Route not found: GET /v0/management/","type":"not_found_error"}}⏎

$ node -v
v18.19.1

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.4 LTS"

Log:

{"id":"1779467728017-de91xuy0i","timestamp":1779467728017,"level":"info","message":"Proxy server started successfully, listening on 0.0.0.0:8080"}
{"id":"1779467766104-l7jpis43q","timestamp":1779467766104,"level":"error","message":"Server error: matchedLayers.toReversed is not a function","data":{"status":500,"path":"/","method":"GET","stack":"TypeError: matchedLayers.toReversed is not a function\n    at RouterImplementation._setMatchedRouteInfo (/home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:968:38)\n    at RouterImplementation.<anonymous> (/home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:929:12)\n    at dispatch (/home/clone/temp/Chat2API-web-main/node_modules/koa-compose/index.js:42:32)\n    at /home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:1055:14\n    at dispatch (/home/clone/temp/Chat2API-web-main/node_modules/koa-compose/index.js:42:32)\n    at RouterImplementation.<anonymous> (/home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:926:16)\n    at dispatch (/home/clone/temp/Chat2API-web-main/node_modules/koa-compose/index.js:42:32)\n    at /home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:1055:14\n    at dispatch (/home/clone/temp/Chat2API-web-main/node_modules/koa-compose/index.js:42:32)\n    at RouterImplementation.<anonymous> (/home/clone/temp/Chat2API-web-main/node_modules/@koa/router/dist/index.js:926:16)"}}
{"id":"1779467794589-wcqyzff05","timestamp":1779467794589,"level":"warn","message":"GET /v0/management/ 404 2ms","data":{"method":"GET","path":"/v0/management/","status":404,"latency":2,"clientIP":"127.0.0.1","slowRequest":false}}

zhaiiker added 2 commits May 25, 2026 10:23
Updated Node.js version requirement from 18+ to 20+ in installation instructions.
@xiaoY233
Copy link
Copy Markdown
Owner

这是个大工程啊,一次性合并估计比较难,感谢提交,后续我消化下

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants