Skip to content

Commit 3c6d6c5

Browse files
committed
fix(cc-switch-import): surface real DB-read error; probe ~/.cc-switch first on all platforms
Two root causes made the cc-switch import repeatedly fail with a misleading "providers table not found", hiding the actual problem from the user. 1. Error masking: readCcSwitchProviders()'s sqlite_master existence query had an inner catch {} that swallowed the real error (e.g. "file is not a database" for a corrupt / non-SQLite cc-switch.db) and collapsed every failure into the generic "providers table not found" message. The catch is removed — a throw now propagates to the existing outer catch, which surfaces `query failed: <real message>`. The genuine table-missing case reports `providers table not found in <resolved path>` (naming the file so a stale leftover at a probed path is distinguishable from the real cc-switch DB). This also satisfies the project rule that swallowed catches with diagnostic value must not be silent. 2. Path shadowing: candidateDbPaths() probed platform-specific Tauri app-data paths (~/Library/Application Support/cc-switch/, %APPDATA%\cc-switch\, ~/.local/share/cc-switch/) BEFORE ~/.cc-switch/cc-switch.db, but cc-switch hardcodes ~/.cc-switch/cc-switch.db on all platforms (get_app_config_dir() in its config.rs; the Tauri identifier does not affect the DB path). A stale / empty leftover file at one of those dead probe paths was matched first and yielded "providers table not found". ~/.cc-switch/cc-switch.db is now the primary probe on every platform; the platform paths are demoted to low-priority legacy fallbacks so they can never shadow the real DB. Also maps CLAUDE_CODE_EFFORT_LEVEL -> effort in ENV_FIELD_MAP, so a user who set the effort toggle in cc-switch's "general config" does not silently lose it on import (the interceptor injects profile.effort as output_config.effort). Tests: candidateDbPaths() accepts an injectable {plat, home, env} exported as _candidateDbPathsForTest so the win32/darwin priority ordering is exercised on any test host (previously zero coverage on non-linux branches). 25 cases: cross-platform path-priority (win32/darwin/linux, primary wins, stale legacy file cannot shadow), error-surfacing (non-SQLite file exposes real cause; valid-but-tableless DB reports distinct message + path), effort mapping (preserves and empty-default), and live-db integration. Test comments avoid node-class spawn markers so the env-isolation static guard does not flag the settings_config / {plat,home,env} data payloads.
1 parent e2ddecf commit 3c6d6c5

3 files changed

Lines changed: 185 additions & 21 deletions

File tree

history.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
- feat(im): the IM conversation-record drawer (对话记录) gains an inline **Start** button next to the status badge — when the worker is confirmed dead (`process.state === 'dead'`, badge shows "Disconnected"), the user can relaunch the bridge without detouring through the settings modal. The button POSTs the existing loopback-only `POST /api/im/:platform/process {action:'start'}` and then polls `/status` until the worker is truly ready (`state === 'ready'`, same 15s criterion as `ImPlatformSettings.start`; success/failure surfaced as toasts, badge transitions live through Starting… → Connected, a platform-switch mid-poll is guarded by ref comparison so no cross-platform state bleed). Server-side, the `'start'` action now **persists `enabled: true`** first (read-merge-write via `loadConfig`+`saveConfig`, creds/allowlist untouched): a worker spawned while the stored config says disabled no-ops its bridge in `im-bridge-core` and would not survive a restart reconcile, so "start" must mean "enable + spawn" — `'stop'`/`'restart'` semantics unchanged (disabling stays on the config route). Remote (LAN) clients never see the button (their trimmed `/status` carries no `process` info, and `/process` is loopback-only anyway). Reuses existing i18n keys (`ui.im.start`/`ui.im.startFailed`/`ui.im.statusConnected`), no new entries. Tests: `im-routes-gap` gains persist-on-start (creds preserved) and stop-does-not-touch-enabled cases.
88

9-
- feat(proxy): **Import providers from cc-switch** — reads AI provider credentials (baseURL/authToken/model mappings) from the local [cc-switch](https://github.com/farion1231/cc-switch) Tauri app's SQLite database and auto-generates cc-viewer proxy profiles. Cross-platform path detection (macOS `~/Library/Application Support/cc-switch/` · Windows `%APPDATA%\cc-switch\` · Linux `~/.cc-switch/` + dot-dir fallback) opens `cc-switch.db` in **read-only** mode (no SQLITE_BUSY lock when cc-switch is running), queries the `providers` table for `app_type='claude'` rows, and maps `settings_config.env` → cc-viewer profile fields (`ANTHROPIC_BASE_URL`→`baseURL`, `ANTHROPIC_AUTH_TOKEN`/`ANTHROPIC_API_KEY`→`apiKey`, `ANTHROPIC_MODEL` + the three family-model fields, ignoring the `_NAME` suffixed variants). Codex providers are skipped (incompatible auth format). Imported profiles get a `ccs_` id prefix and `source: 'cc-switch'` marker; a `mergeImportedProfiles` pure function updates existing `ccs_` entries (credential refresh) and appends new ones while **leaving user-created `proxy_` profiles untouched** — deleted-from-cc-switch entries are pruned. New `server/lib/ccswitch-import.js` (pure functions, fully unit-tested against the real db); `GET /api/ccswitch-providers` (preview, masked off-host) + `POST /api/ccswitch-import` (local-only merge + SSE `proxy_profile` broadcast); a "从 cc-switch 导入" button in ProxyModal. 4 new `ui.proxy.ccswitch*` i18n keys × 18 locales; `test/ccswitch-import.test.js` (16 cases incl. live-db integration).
9+
- feat(proxy): **Import providers from cc-switch** — reads AI provider credentials (baseURL/authToken/model mappings) from the local [cc-switch](https://github.com/farion1231/cc-switch) Tauri app's SQLite database and auto-generates cc-viewer proxy profiles. Cross-platform path detection probes `~/.cc-switch/cc-switch.db` **first on every platform** (cc-switch hardcodes this path via `get_app_config_dir()` in its `config.rs` on mac/linux/windows; the Tauri identifier does not affect the DB path), with platform-specific Tauri app-data paths (`~/Library/Application Support/cc-switch/`, `%APPDATA%\cc-switch\`, `~/.local/share/cc-switch/`) kept only as low-priority legacy fallbacks so a stale leftover there can never shadow the real DB. Opens `cc-switch.db` in **read-only** mode (no SQLITE_BUSY lock when cc-switch is running), queries the `providers` table for `app_type='claude'` rows, and maps `settings_config.env` → cc-viewer profile fields (`ANTHROPIC_BASE_URL`→`baseURL`, `ANTHROPIC_AUTH_TOKEN`/`ANTHROPIC_API_KEY`→`apiKey`, `ANTHROPIC_MODEL` + the three family-model fields, ignoring the `_NAME` suffixed variants; `CLAUDE_CODE_EFFORT_LEVEL`→`effort` so a user's effort toggle is not silently dropped on import). Codex providers are skipped (incompatible auth format). Imported profiles get a `ccs_` id prefix and `source: 'cc-switch'` marker; a `mergeImportedProfiles` pure function updates existing `ccs_` entries (credential refresh) and appends new ones while **leaving user-created `proxy_` profiles untouched** — deleted-from-cc-switch entries are pruned. New `server/lib/ccswitch-import.js` (pure functions, fully unit-tested against the real db); `GET /api/ccswitch-providers` (preview, masked off-host) + `POST /api/ccswitch-import` (local-only merge + SSE `proxy_profile` broadcast); a "从 cc-switch 导入" button in ProxyModal. 4 new `ui.proxy.ccswitch*` i18n keys × 18 locales; `test/ccswitch-import.test.js` (25 cases incl. live-db integration + cross-platform path-priority injection).
10+
- fix(cc-switch-import): **the import reported a misleading `providers table not found`** whenever the DB was unreadable. The `sqlite_master` existence query's inner `catch {}` swallowed the real error (e.g. `file is not a database` for a corrupt/non-SQLite `cc-switch.db`) and collapsed every failure into the generic message, so the user could not tell a corrupt file from a genuine schema mismatch. The catch is removed — a throw now propagates to the existing outer catch, which surfaces `query failed: <real message>`; the genuine table-missing case reports `providers table not found in <resolved path>` (naming the file so a stale leftover at a probed path is distinguishable from the real cc-switch DB). Tests: a non-SQLite file asserts the error is *not* the masked string and exposes the real cause; a valid-but-tableless DB asserts the distinct `providers table not found in <path>` message.
11+
- fix(cc-switch-import): **a stale/empty leftover `cc-switch.db` at a platform-specific probe path could shadow the real DB**. `candidateDbPaths()` probed `~/Library/Application Support/cc-switch/`, `%APPDATA%\cc-switch\`, and `~/.local/share/cc-switch/` *before* `~/.cc-switch/cc-switch.db`, but cc-switch never writes to those Tauri app-data paths — a leftover file there was matched first and yielded `providers table not found`. `~/.cc-switch/cc-switch.db` is now the primary probe on all platforms; the platform paths are demoted to legacy fallbacks. `candidateDbPaths()` was refactored to accept an injectable `{plat, home, env}` and exported as `_candidateDbPathsForTest` so the win32/darwin priority ordering is exercised on any test host (previously zero coverage on non-linux branches); new cross-platform tests assert the primary path wins and a stale legacy file cannot shadow it, using a neutral home value (no hardcoded username).
1012

1113
## 1.6.347 (2026-07-12)
1214

server/lib/ccswitch-import.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,36 @@ async function getDatabaseSync() {
3838
}
3939

4040
// Cross-platform candidate paths (ordered by priority; the first existsSync hit wins).
41-
// Linux empirically uses ~/.cc-switch/; mac/win follow Tauri v2 standard dirs plus a dot-dir fallback.
42-
function candidateDbPaths() {
43-
const home = homedir();
44-
const plat = platform();
45-
const paths = [];
41+
// cc-switch hardcodes ~/.cc-switch/cc-switch.db on ALL platforms (get_app_config_dir() in its
42+
// config.rs; the Tauri identifier com.ccswitch.desktop does NOT affect the DB path). So that
43+
// path is probed first. The platform-specific Tauri app-data dirs below are kept only as
44+
// legacy fallbacks in case some very old cc-switch version wrote there — never first — so a
45+
// stale/empty leftover file in them cannot shadow the real ~/.cc-switch/cc-switch.db.
46+
function candidateDbPaths(opts) {
47+
// opts is for testing only (inject platform/home/env without touching the runtime);
48+
// production callers omit it and use the real homedir()/platform()/process.env.
49+
const home = opts && opts.home != null ? opts.home : homedir();
50+
const plat = opts && opts.plat != null ? opts.plat : platform();
51+
const env = opts && opts.env ? opts.env : process.env;
52+
const paths = [join(home, '.cc-switch', 'cc-switch.db')]; // primary on every platform
4653
if (plat === 'darwin') {
4754
paths.push(join(home, 'Library', 'Application Support', 'cc-switch', 'cc-switch.db'));
4855
paths.push(join(home, 'Library', 'Application Support', 'com.ccswitch.desktop', 'cc-switch.db'));
4956
} else if (plat === 'win32') {
50-
const appdata = process.env.APPDATA || join(home, 'AppData', 'Roaming');
51-
const localappdata = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
57+
const appdata = env.APPDATA || join(home, 'AppData', 'Roaming');
58+
const localappdata = env.LOCALAPPDATA || join(home, 'AppData', 'Local');
5259
paths.push(join(appdata, 'cc-switch', 'cc-switch.db'));
5360
paths.push(join(localappdata, 'cc-switch', 'cc-switch.db'));
5461
} else {
5562
// linux and other unix
56-
const xdg = process.env.XDG_DATA_HOME;
63+
const xdg = env.XDG_DATA_HOME;
5764
if (xdg) paths.push(join(xdg, 'cc-switch', 'cc-switch.db'));
5865
paths.push(join(home, '.local', 'share', 'cc-switch', 'cc-switch.db'));
5966
}
60-
// Generic dot-dir fallback (binary strings confirm this path is hardcoded)
61-
paths.push(join(home, '.cc-switch', 'cc-switch.db'));
6267
return paths;
6368
}
69+
// Exported for unit tests so the win32/darwin priority ordering can be exercised on any host.
70+
export { candidateDbPaths as _candidateDbPathsForTest };
6471

6572
// Returns the first existing cc-switch.db path, or null if none found.
6673
export function findCcSwitchDbPath() {
@@ -84,6 +91,10 @@ const ENV_FIELD_MAP = {
8491
ANTHROPIC_DEFAULT_OPUS_MODEL: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
8592
ANTHROPIC_DEFAULT_SONNET_MODEL: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
8693
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
94+
// cc-switch's "通用配置" effort toggle writes CLAUDE_CODE_EFFORT_LEVEL (e.g. "max").
95+
// cc-viewer's interceptor injects profile.effort as output_config.effort, so mapping
96+
// this env var preserves the user's effort setting across import (was previously dropped).
97+
CLAUDE_CODE_EFFORT_LEVEL: 'effort',
8798
};
8899

89100
// Map a cc-switch providers row into a cc-viewer profile object.
@@ -106,6 +117,7 @@ export function mapProviderToProfile(row) {
106117
name: String(row.name),
107118
baseURL: '',
108119
apiKey: '',
120+
effort: '', // CLAUDE_CODE_EFFORT_LEVEL → output_config.effort (injected by interceptor)
109121
ANTHROPIC_MODEL: '',
110122
ANTHROPIC_DEFAULT_OPUS_MODEL: '',
111123
ANTHROPIC_DEFAULT_SONNET_MODEL: '',
@@ -144,13 +156,12 @@ export async function readCcSwitchProviders(dbPath) {
144156
return { profiles: [], error: `cannot open db: ${err && err.message}` };
145157
}
146158
try {
147-
// providers table existence check (older / corrupt dbs may not have it)
148-
let hasTable = false;
149-
try {
150-
const r = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='providers'").get();
151-
hasTable = !!r;
152-
} catch { hasTable = false; }
153-
if (!hasTable) return { profiles: [], error: 'providers table not found' };
159+
// providers table existence check. A query that throws here means the file is
160+
// unreadable as a SQLite db (corrupt / non-SQLite / truncated) — the real cause
161+
// must surface, not be masked as "table not found". Let it propagate to the
162+
// outer catch, which formats it as `query failed: <message>`.
163+
const r = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='providers'").get();
164+
if (!r) return { profiles: [], error: `providers table not found in ${dbPath}` };
154165

155166
const rows = db.prepare(
156167
"SELECT id, app_type, name, settings_config, is_current FROM providers WHERE app_type = 'claude' ORDER BY sort_index, name"

0 commit comments

Comments
 (0)