mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 14:01:30 +08:00
Clear all `barrel/index-no-impl` violations in src/main (14 barrels) by turning each index.ts into a pure re-export or dissolving it: - Aggregation buckets → drop the barrel, move the assembled object/array/fn to a named file, deep-import at the sole consumer: data/api/handlers → apiHandlers.ts, ipc/handlers → ipcHandlers.ts, migrators → migratorRegistry.ts, params/features → internalFeatures.ts, browser/tools → registry.ts, builtin → registerBuiltinTools.ts, db/seeding → seederRegistry.ts. - Single-impl dirs flattened: provider/extensions.ts, provider/cherryai.ts. - Barrel kept, impl extracted to a named file: i18n/resolver.ts, ai/types/providerConfig.ts. - Route subdir flattened to match its flat siblings: routes/knowledge.ts + routes/knowledgeSchemas.ts. - runtime: replace the import-time `claudeCode/register` side effect with an explicit registerRuntimeDrivers() invoked from AgentSessionRuntimeService.onInit, so runtime/index.ts stays a pure re-export. - loop: dissolve the non-enforced barrel into loop/types.ts + loop/hookRunner.ts. Also route DataApiService's apiHandlers import through the data/api barrel, and expose ClaudeCodeRuntimeDriver via the claudeCode barrel, clearing the two deep-import warnings this batch touched. Docs and comments updated to the new paths.
165 lines
13 KiB
Markdown
165 lines
13 KiB
Markdown
# Migration V2 (Main Process)
|
|
|
|
Architecture for the new one-shot migration from the legacy Dexie + Redux Persist stores into the SQLite schema. This module owns orchestration, data access helpers, migrator plugins, and IPC entry points used by the renderer migration window.
|
|
|
|
## Version Upgrade Requirements
|
|
|
|
The v2 migration system enforces a **linear upgrade path** to ensure
|
|
data integrity:
|
|
|
|
```
|
|
v1.old → v1.last (≥1.9.0) → v2.0.0 → v2.x
|
|
```
|
|
|
|
### Why a linear path?
|
|
|
|
v2.0.0 contains the one-shot data migration from Redux/Dexie to SQLite.
|
|
Supporting migration from every v1 version would create an O(n²) test
|
|
matrix. By requiring all users to be on the final v1 release first, the
|
|
migration code only needs to handle a single source data format.
|
|
|
|
### How it works
|
|
|
|
1. **VersionService** has been embedded since v1.7. It writes a
|
|
`version.log` file to `{userData}/` on every launch where the
|
|
version changes.
|
|
2. On v2 first launch, `v2MigrationGate.ts` reads `version.log` via
|
|
`MigrationPaths.versionLogFile` (using the resolved userData path
|
|
that accounts for v1 custom directories).
|
|
3. If the previous version is too old, missing, or if the user skipped
|
|
v2.0.0, the gate shows an error dialog and quits.
|
|
|
|
### Blocking rules
|
|
|
|
| Scenario | Block reason | User action |
|
|
|----------|-------------|-------------|
|
|
| No `version.log` (v1 < 1.7 user) | `no_version_log` | Install v1.last, run once, then install v2.0.0 |
|
|
| Previous version < 1.9.0 | `v1_too_old` | Upgrade to v1.last first |
|
|
| Previous version is v1.x but current > v2.0.0 | `v2_gateway_skipped` | Install v2.0.0 first |
|
|
|
|
### Pre-release versions
|
|
|
|
v2.0.0 pre-releases (alpha/beta/rc) are treated as **before v2.0.0**
|
|
per semver ordering. They are allowed as migration targets from v1.last
|
|
(the gateway check coerces `currentVersion`, so `2.0.0-alpha` → `2.0.0`
|
|
passes). Pre-release to pre-release upgrades work because migration
|
|
status is `completed` after the first successful run.
|
|
|
|
The gateway is **strictly v2.0.0** — v2.0.x patches are blocked from
|
|
being a first migration target. This may be relaxed in a future release.
|
|
|
|
### Relationship with the auto-updater
|
|
|
|
The auto-updater (`AppUpdaterService`) controls which versions are
|
|
offered via OTA using `minCompatibleVersion` in the remote config. The
|
|
migration gate is a **separate safety net** for users who manually
|
|
download and install a version. Both systems enforce compatible upgrade
|
|
paths but operate independently.
|
|
|
|
## Directory Layout
|
|
|
|
```
|
|
src/main/data/migration/v2/
|
|
├── core/ # Engine + shared context
|
|
├── migrators/ # Domain-specific migrators and mappings
|
|
├── utils/ # Data source readers (Redux, Dexie, streaming JSON)
|
|
├── window/ # IPC handlers + migration window manager
|
|
└── index.ts # Public exports for main process
|
|
```
|
|
|
|
## Core Contracts
|
|
|
|
- `core/MigrationEngine.ts` coordinates all migrators in order, surfaces progress to the UI, and marks status in `app_state.key = 'migration_v2_status'`. It will clear new-schema tables before running and abort on any validation failure.
|
|
- `core/MigrationPaths.ts` defines `MigrationPaths` (a frozen object of pre-computed paths) and `resolveMigrationPaths()` which detects v1 legacy userData directories from `~/.cherrystudio/config/config.json`. Called once at the migration gate entry, before engine initialization. All migration code uses these paths instead of `app.getPath()` — see the **Path safety** convention below.
|
|
- `core/MigrationContext.ts` builds the shared context passed to every migrator:
|
|
- `sources`: `ConfigManager` (ElectronStore), `ReduxStateReader` (parsed Redux Persist data), `DexieFileReader` (JSON exports), `LegacyHomeConfigReader` (v1 `~/.cherrystudio/config/config.json` for the config-file migration path used by `BootConfigMigrator`)
|
|
- `db`: current SQLite connection
|
|
- `paths`: `MigrationPaths` — pre-computed filesystem paths; migrators that need file paths use `ctx.paths` instead of `app.getPath()`
|
|
- `sharedData`: `Map` for passing cross-cutting info between migrators
|
|
- `logger`: `loggerService` scoped to migration
|
|
- `@shared/data/migration/v2/types` defines stages, results, and validation stats used across main and renderer.
|
|
|
|
## Migrators
|
|
|
|
- Base contract: extend `migrators/BaseMigrator.ts` and implement:
|
|
- `id`, `name`, `description`, `order` (lower runs first)
|
|
- `prepare(ctx)`: dry-run checks, counts, and staging data; return `PrepareResult`
|
|
- `execute(ctx)`: perform inserts/updates; manage your own transactions; report progress via `reportProgress`; self-check FK integrity of owned tables via `assertOwnedForeignKeys` (see Conventions → Foreign keys)
|
|
- `validate(ctx)`: verify counts and integrity; return `ValidateResult` with stats (`sourceCount`, `targetCount`, `skippedCount`) and any `errors`
|
|
- Registration: list migrators (in order) in `migrators/migratorRegistry.ts` so the engine can sort and run them.
|
|
- Current migrators (see `migrators/README-<name>.md` for detailed documentation):
|
|
- `PreferencesMigrator` (implemented): maps ElectronStore + Redux settings to the `preference` table using `mappings/PreferencesMappings.ts`.
|
|
- `ChatMigrator` (implemented): migrates topics and messages from Dexie to SQLite. See [`README-ChatMigrator.md`](../../../src/main/data/migration/v2/migrators/README-ChatMigrator.md).
|
|
- `BootConfigMigrator` (implemented, file-target): migrates early-boot settings into the file-based `bootConfigService` (`~/.cherrystudio/boot-config.json`) rather than a SQLite table. Reads from Redux (`disableHardwareAcceleration`) and from the v1 home config file (`~/.cherrystudio/config/config.json`'s `appDataPath` → `app.user_data_path`) via a `'configfile'` source kind. See [`README-BootConfigMigrator.md`](../../../src/main/data/migration/v2/migrators/README-BootConfigMigrator.md).
|
|
- `AssistantMigrator`, `KnowledgeMigrator` (placeholders): scaffolding and TODO notes for future tables.
|
|
- Conventions:
|
|
- All logging goes through `loggerService` with a migrator-specific context.
|
|
- Use `MigrationContext.sources` instead of accessing raw files/stores directly.
|
|
- Use `sharedData` to pass IDs or lookup tables between migrators (e.g., assistant -> chat references) instead of re-reading sources.
|
|
- Stream large Dexie exports (`JsonStreamReader`) and batch inserts to avoid memory spikes.
|
|
- **Foreign keys are OFF for the whole migration — do NOT toggle them per-migrator**: better-sqlite3 keeps a single persistent connection open for the whole process, so the engine sets `PRAGMA foreign_keys = OFF` **once** on that connection (in `MigrationDbService`) and it stays in effect for the entire migration — there is no per-transaction reconnection that could reset it. This lets bulk inserts carry not-yet-resolved references (self-referencing `message.parentId`, or cross-domain refs a later migrator resolves). Integrity is verified in two layers: (1) each migrator calls `this.assertOwnedForeignKeys(ctx.db, [...])` at the end of `execute()` for the tables it owns, giving early, well-attributed failures; (2) the engine runs a whole-database `PRAGMA foreign_key_check` after all migrators complete (`MigrationEngine.verifyForeignKeys`) as the final backstop.
|
|
- **Self-check scope**: pass only tables whose FKs are fully resolved when *your* migrator finishes. **Exclude** refs a later migrator resolves — e.g. `assistant_knowledge_base.knowledgeBaseId` is written by `AssistantMigrator` but only becomes valid after `KnowledgeMigrator` remaps/prunes it, so `KnowledgeMigrator` self-checks that table, not `AssistantMigrator`. Dedicated file association tables (for example `chat_message_file_ref`) may be self-checked by the migrator that owns both the source rows and ref rows.
|
|
- Count validation is mandatory; engine will fail the run if `targetCount < sourceCount - skippedCount` or if `ValidateResult.errors` is non-empty.
|
|
- Keep migrations idempotent per run—engine clears target tables before it starts, but each migrator should tolerate retries within the same run.
|
|
- **Path safety**: All filesystem paths MUST come from `ctx.paths` (the `MigrationPaths` object). NEVER call `app.getPath('userData')` or construct paths with `path.join` from scratch. Doing so bypasses the v1 legacy userData detection and may cause data loss for users with custom `appDataPath` configurations. If you need a path not yet in `MigrationPaths`, add it to the interface — do not inline it.
|
|
|
|
## Utilities
|
|
|
|
- `utils/ReduxStateReader.ts`: safe accessor for categorized Redux Persist data with dot-path lookup.
|
|
- `utils/DexieFileReader.ts`: reads exported Dexie JSON tables; can stream large tables.
|
|
- `utils/JsonStreamReader.ts`: streaming reader with batching, counting, and sampling helpers for very large arrays.
|
|
- `utils/LegacyHomeConfigReader.ts`: synchronously reads the v1 `~/.cherrystudio/config/config.json` file and normalizes its `appDataPath` field (both the legacy string shape and the current `{ executablePath, dataPath }[]` shape) into a `Record<executablePath, dataPath> | null`. Used exclusively by `BootConfigMigrator`'s `'configfile'` source.
|
|
|
|
## Window & IPC Integration
|
|
|
|
- `window/MigrationIpcHandler.ts` exposes IPC channels for the migration UI:
|
|
- Receives Redux data and Dexie export path, starts the engine, and streams progress back to renderer.
|
|
- Manages retry/cancel/restart/skip actions.
|
|
- `window/MigrationWindowManager.ts` creates the frameless migration window, handles lifecycle, and relaunch instructions after completion in production.
|
|
|
|
## Implementation Checklist for New Migrators
|
|
|
|
- [ ] Add mapping definitions (if needed) under `migrators/mappings/`.
|
|
- [ ] Implement `prepare/execute/validate` with explicit counts, batch inserts, and integrity checks.
|
|
- [ ] Wire progress updates through `reportProgress` so UI shows per-migrator progress.
|
|
- [ ] Register the migrator in `migrators/migratorRegistry.ts` with the correct `order`.
|
|
- [ ] Add any new target tables to `MigrationEngine.verifyAndClearNewTables` once those tables exist.
|
|
- [ ] Self-check FK integrity at the end of `execute()` via `this.assertOwnedForeignKeys(ctx.db, [...ownedTables])`, excluding cross-domain-deferred refs and shared polymorphic tables (see Conventions → Foreign keys). Do NOT toggle `PRAGMA foreign_keys` yourself — the engine keeps it OFF for the whole migration.
|
|
- [ ] Include detailed comments for maintainability (file-level, function-level, logic blocks).
|
|
- [ ] **Create/update `migrators/README-<MigratorName>.md`** with detailed documentation including:
|
|
- Data sources and target tables
|
|
- Key transformations
|
|
- Field mappings (source → target)
|
|
- Dropped fields and rationale
|
|
- Code quality notes
|
|
|
|
## Order-Key Stamping in Migrators
|
|
|
|
Legacy Redux/Dexie → SQLite migrators for sortable resources must produce `order_key` values for every row they insert. The v2 migrator layer owns a pair of **pure functions** under `src/main/data/migration/v2/utils/orderKey.ts` that handle this without touching the DB — they take a pre-flattened array and return the same rows with `orderKey` attached.
|
|
|
|
| Helper | Shape | Use for |
|
|
|---|---|---|
|
|
| `assignOrderKeysInSequence(rows)` | Returns `rows` with one monotonically increasing `orderKey` per row. | Whole-table ordering (e.g. `mcp_server`, `user_provider`, `miniapp`). |
|
|
| `assignOrderKeysByScope(rows, getScope)` | Groups rows by the scope key, stamps each bucket independently (independent key spaces per bucket). | Partitioned tables (e.g. `topic.groupId`, `user_model.providerId`, `group.entityType`). |
|
|
|
|
**Pattern — flatten first, stamp last:** keep `transform*` functions pure (no `index` parameter, no `sortOrder` argument); flatten the legacy source into an array, then stamp keys onto the whole array:
|
|
|
|
```typescript
|
|
import { assignOrderKeysByScope, assignOrderKeysInSequence } from '@data/migration/v2/utils/orderKey'
|
|
|
|
// Before — each transform took an index and emitted a sortOrder
|
|
const rows = legacyServers.map((src, i) => transformMcpServer(src, i).row)
|
|
|
|
// After — transforms are pure; keys are assigned after the flatten
|
|
const rows = legacyServers.map((src) => transformMcpServerV2(src).row)
|
|
const stamped = assignOrderKeysInSequence(rows)
|
|
await tx.insert(mcpServerTable).values(stamped)
|
|
|
|
// Partitioned example — each providerId becomes its own independent key space
|
|
const stamped = assignOrderKeysByScope(userModels, (m) => m.providerId)
|
|
```
|
|
|
|
**Import rule — never reach for `fractional-indexing` directly:** the migrator helpers delegate to `generateOrderKeySequence` exported from `src/main/data/services/utils/orderKey.ts`, which is the **single** sanctioned integration point for the library. Migrator code, migration scripts, and drizzle custom-migration callbacks all re-import from that service-layer wrapper. This keeps the library boundary auditable and leaves a single place to change the character set or swap implementations.
|
|
|
|
For the runtime counterparts (`insertWithOrderKey` / `insertManyWithOrderKey` / `applyMoves` / `resetOrder`) used outside the migration window, see [Reorder Guide — Server-Side Service Helpers](./data-ordering-guide.md#3-server-side-service-helpers).
|