Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/issues/cc-switch-config-path-discovery/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CC Switch Config Path Discovery Plan

## Implementation

- Extend CC Switch source path resolution in `ProviderImportService`.
- Look for CC Switch Desktop `app_paths.json` at platform-specific locations:
- macOS: `~/Library/Application Support/com.ccswitch.desktop/app_paths.json`
- Windows: `%APPDATA%/com.ccswitch.desktop/app_paths.json`
- Linux: `$XDG_CONFIG_HOME/com.ccswitch.desktop/app_paths.json`, falling back to `~/.config/com.ccswitch.desktop/app_paths.json`
- Read `app_config_dir_override` and use `<override>/cc-switch.db` when it exists.
- Fall back to the existing default path and Windows HOME fallback when the override cannot be used.
- Keep read errors on the selected database visible as a source read error.

## Compatibility

- Existing CC Switch imports keep the same default path when no Desktop override is configured.
- Other provider import sources keep their existing path logic.
- The provider parser and app-type allowlist remain unchanged.

## Test Strategy

- Add a scan test for a Desktop override database containing Claude-compatible providers.
- Add fallback tests for invalid or missing override databases.
- Add a Codex-only override test to confirm Codex rows are still ignored.
- Keep the Windows HOME fallback test passing.
25 changes: 25 additions & 0 deletions docs/issues/cc-switch-config-path-discovery/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CC Switch Config Path Discovery

## Goal

DeepChat should import CC Switch providers from the active CC Switch Desktop data directory when CC Switch is configured to use a custom config path.

## User Stories

- As a user who moved CC Switch data to a sync directory, I can import the providers visible in CC Switch Desktop.
- As a user with a stale default `~/.cc-switch/cc-switch.db`, I do not get an empty import result when the active database is elsewhere.
- As a user with Codex-only CC Switch rows, DeepChat continues to hide Codex provider rows from provider import.

## Acceptance Criteria

- CC Switch import reads `app_config_dir_override` from CC Switch Desktop `app_paths.json` when available.
- If `<app_config_dir_override>/cc-switch.db` exists, that database is used before the default `~/.cc-switch/cc-switch.db`.
- If the override file is missing, invalid, empty, or points to a missing database, import falls back to the existing default path behavior.
- The scan result `configPath` reflects the database path actually used.
- Existing CC Switch app-type support stays limited to `claude`, `claude-desktop`, `gemini`, `opencode`, `openclaw`, and `hermes`.

## Non-Goals

- Importing CC Switch `codex` providers.
- Scanning the filesystem for arbitrary `cc-switch.db` files.
- Changing provider mapping or credential filtering rules.
7 changes: 7 additions & 0 deletions docs/issues/cc-switch-config-path-discovery/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CC Switch Config Path Discovery Tasks

- [x] Add SDD issue documents.
- [x] Implement CC Switch Desktop override path discovery.
- [x] Add provider import tests for override and fallback behavior.
- [x] Run focused provider import tests.
- [x] Run format, i18n, lint, and typecheck.
22 changes: 22 additions & 0 deletions docs/issues/cherry-studio-config-path-discovery/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Cherry Studio Config Path Discovery Plan

## Implementation

- Extend provider import source path resolution for `cherry-studio`.
- Resolve the current default Cherry Studio LevelDB path using the existing source definition.
- Read `~/.cherrystudio/config/config.json`, matching Cherry Studio's `appDataPath` config shape.
- Support both legacy string `appDataPath` and array entries with `dataPath`.
- Treat each configured `dataPath` as a candidate Cherry Studio user data directory and use `<candidate>/Local Storage/leveldb` when it exists.
- Preserve the existing default path fallback when no valid candidate is found.

## Compatibility

- Default Cherry Studio imports keep the same path behavior.
- Symlink-based Cherry Studio data moves continue to work through the existing default path.
- The discovered path only changes where the LevelDB is read from; provider parsing remains unchanged.

## Test Strategy

- Add a scan test for a configured custom Cherry Studio data directory.
- Add fallback coverage for a discovered path without a LevelDB directory.
- Keep the existing Cherry Studio LevelDB snapshot import test passing.
25 changes: 25 additions & 0 deletions docs/issues/cherry-studio-config-path-discovery/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Cherry Studio Config Path Discovery

## Goal

DeepChat should import Cherry Studio providers from Cherry Studio's active custom data directory when users move the app data outside the default platform path.

## User Stories

- As a user who launches Cherry Studio with a custom user data directory, I can import the providers visible in Cherry Studio.
- As a user with a stale default Cherry Studio data directory, I do not get outdated or empty import results when the active LevelDB is elsewhere.
- As a user without a custom Cherry Studio directory, existing default-path import behavior remains unchanged.

## Acceptance Criteria

- Cherry Studio import can discover a custom data directory recorded in Cherry Studio's home config.
- If the configured directory contains `Local Storage/leveldb`, DeepChat scans that LevelDB before the default one.
- If the discovered directory is missing, invalid, or does not contain a LevelDB directory, DeepChat falls back to the existing default path behavior.
- The scan result `configPath` reflects the LevelDB path actually used.
- Provider parsing, credential filtering, and provider mapping behavior remain unchanged.

## Non-Goals

- Scanning arbitrary user directories such as Downloads for Cherry Studio data.
- Importing Cherry Studio data beyond model providers.
- Adding a user-facing manual path picker.
7 changes: 7 additions & 0 deletions docs/issues/cherry-studio-config-path-discovery/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Cherry Studio Config Path Discovery Tasks

- [x] Add SDD issue documents.
- [x] Implement Cherry Studio custom data-dir discovery.
- [x] Add provider import tests for discovered path and fallback behavior.
- [x] Run focused provider import tests.
- [x] Run format, i18n, lint, and typecheck.
200 changes: 200 additions & 0 deletions src/main/routes/providers/providerImportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ const CC_SWITCH_APP_TYPES = [
'openclaw',
'hermes'
] as const
const CC_SWITCH_DB_FILE_NAME = 'cc-switch.db'
const CC_SWITCH_DESKTOP_CONFIG_DIR_NAME = 'com.ccswitch.desktop'
const CC_SWITCH_DESKTOP_APP_PATHS_FILE_NAME = 'app_paths.json'
const CHERRY_STUDIO_LEVELDB_RELATIVE_PATH = 'Local Storage/leveldb'
const CHERRY_STUDIO_CONFIG_RELATIVE_PATH = '.cherrystudio/config/config.json'

const SOURCE_PREFIX: Record<ProviderImportSourceId, string> = {
alma: 'alma',
Expand Down Expand Up @@ -315,6 +320,17 @@ const buildWindowsDisplayPath = (
relativePath: string
) => `${base}\\${relativePath.replaceAll('/', '\\')}`

const getContainedRelativePath = (basePath: string, targetPath: string): string => {
const relativePath = path.relative(basePath, targetPath)
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return ''
}
return relativePath
}

const toUnixRelativeDisplayPath = (relativePath: string): string =>
relativePath.split(path.sep).join('/')

const isSupportedScanPlatform = (
platform: NodeJS.Platform
): platform is 'darwin' | 'linux' | 'win32' =>
Expand Down Expand Up @@ -1456,6 +1472,24 @@ export class ProviderImportService {
}

private resolveSourcePath(definition: SourceDefinition): ResolvedSourcePath {
if (definition.id === 'cc-switch') {
const overridePath = this.resolveCcSwitchDesktopOverridePath()
if (overridePath) {
return overridePath
}
}

if (definition.id === 'cherry-studio') {
const customPath = this.resolveCherryStudioCustomDataPath()
if (customPath) {
return customPath
}
}

return this.resolveDefaultSourcePath(definition)
}

private resolveDefaultSourcePath(definition: SourceDefinition): ResolvedSourcePath {
if (this.platform === 'win32') {
const basePath = definition.windowsBase === 'appData' ? this.appDataDir : this.homeDir
const displayBase = definition.windowsBase === 'appData' ? '%APPDATA%' : '%USERPROFILE%'
Expand Down Expand Up @@ -1483,4 +1517,170 @@ export class ProviderImportService {
displayPath: buildUnixDisplayPath(definition.unixRelativePath)
}
}

private resolveCherryStudioCustomDataPath(): ResolvedSourcePath | null {
const candidateDirs = this.readCherryStudioConfiguredDataDirs()

for (const candidateDir of candidateDirs) {
const sourcePath = path.join(candidateDir, CHERRY_STUDIO_LEVELDB_RELATIVE_PATH)
if (!fs.existsSync(sourcePath)) {
continue
}

return {
sourcePath,
displayPath: this.buildDetectedSourceDisplayPath(sourcePath)
}
}

return null
}

private readCherryStudioConfiguredDataDirs(): string[] {
const configPath = path.join(this.homeDir, CHERRY_STUDIO_CONFIG_RELATIVE_PATH)
if (!fs.existsSync(configPath)) {
return []
}

let config: Record<string, unknown>
try {
config = toObjectValue(safeJsonParse(fs.readFileSync(configPath, 'utf8')))
} catch {
return []
}

const appDataPath = config.appDataPath
if (typeof appDataPath === 'string') {
return this.normalizeDetectedPaths([appDataPath])
}

if (!Array.isArray(appDataPath)) {
return []
}

const entries = appDataPath
.map((entry) => toObjectValue(entry))
.filter((entry) => toStringValue(entry.dataPath))
.sort((left, right) => {
const leftExecutableExists = fs.existsSync(toStringValue(left.executablePath))
const rightExecutableExists = fs.existsSync(toStringValue(right.executablePath))
return Number(rightExecutableExists) - Number(leftExecutableExists)
})
return this.normalizeDetectedPaths(entries.map((entry) => toStringValue(entry.dataPath)))
}

private resolveCcSwitchDesktopOverridePath(): ResolvedSourcePath | null {
const appPathsPath = this.resolveCcSwitchDesktopAppPathsPath()
if (!fs.existsSync(appPathsPath)) {
return null
}

let appPathsFile = ''
try {
appPathsFile = fs.readFileSync(appPathsPath, 'utf8')
} catch {
return null
}

const appPaths = toObjectValue(safeJsonParse(appPathsFile))
const overrideDir = toStringValue(appPaths.app_config_dir_override)
if (!overrideDir) {
return null
}

const dbPath = path.join(this.resolveCcSwitchOverrideDir(overrideDir), CC_SWITCH_DB_FILE_NAME)
if (!fs.existsSync(dbPath)) {
return null
}

return {
sourcePath: dbPath,
displayPath: this.buildDetectedSourceDisplayPath(dbPath)
}
}

private resolveCcSwitchDesktopAppPathsPath(): string {
if (this.platform === 'win32') {
return path.join(
this.appDataDir,
CC_SWITCH_DESKTOP_CONFIG_DIR_NAME,
CC_SWITCH_DESKTOP_APP_PATHS_FILE_NAME
)
}

if (this.platform === 'linux') {
const xdgConfigDir = (process.env.XDG_CONFIG_HOME ?? '').trim()
const configBaseDir = xdgConfigDir || path.join(this.homeDir, '.config')
return path.join(
configBaseDir,
CC_SWITCH_DESKTOP_CONFIG_DIR_NAME,
CC_SWITCH_DESKTOP_APP_PATHS_FILE_NAME
)
}

return path.join(
this.homeDir,
'Library/Application Support',
CC_SWITCH_DESKTOP_CONFIG_DIR_NAME,
CC_SWITCH_DESKTOP_APP_PATHS_FILE_NAME
)
}

private resolveCcSwitchOverrideDir(overrideDir: string): string {
return this.resolveDetectedPath(overrideDir)
}

private resolveDetectedPath(value: string): string {
const trimmed = value.trim()
if (!trimmed) {
return ''
}

if (trimmed === '~') {
return this.homeDir
}

if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
return path.join(this.homeDir, trimmed.slice(2))
}

return path.isAbsolute(trimmed) ? trimmed : path.resolve(this.homeDir, trimmed)
}

private normalizeDetectedPaths(values: string[]): string[] {
const result: string[] = []
const seen = new Set<string>()
for (const value of values) {
const resolvedPath = this.resolveDetectedPath(value)
if (!resolvedPath || seen.has(resolvedPath)) {
continue
}
seen.add(resolvedPath)
result.push(resolvedPath)
}
return result
}

private buildDetectedSourceDisplayPath(sourcePath: string): string {
if (this.platform === 'win32') {
const appDataRelativePath = getContainedRelativePath(this.appDataDir, sourcePath)
if (appDataRelativePath) {
return buildWindowsDisplayPath('%APPDATA%', toUnixRelativeDisplayPath(appDataRelativePath))
}

const homeRelativePath = getContainedRelativePath(this.homeDir, sourcePath)
if (homeRelativePath) {
return buildWindowsDisplayPath('%USERPROFILE%', toUnixRelativeDisplayPath(homeRelativePath))
}

return sourcePath
}

const homeRelativePath = getContainedRelativePath(this.homeDir, sourcePath)
if (homeRelativePath) {
return buildUnixDisplayPath(toUnixRelativeDisplayPath(homeRelativePath))
}

return sourcePath
}
}
Loading