feat: add v2-refactor-temp directory for V2 refactoring tools

- Add data-classify tools for data inventory extraction and code generation
  - Include consolidated Chinese documentation (README.md)
  - Update generated file path references

  This temporary directory will be removed after V2 refactor is complete.
This commit is contained in:
fullex 2025-11-29 11:55:45 +08:00
parent 143b4c46c8
commit 806a294508
17 changed files with 8312 additions and 93 deletions

View File

@ -1,10 +1,10 @@
/**
* Auto-generated preferences configuration
* Generated at: 2025-11-28T16:19:17.585Z
* Generated at: 2025-11-29T03:45:07.207Z
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
* node local/tools/data-classify/scripts/generate-preferences.js
* node v2-refactor-temp/tools/data-classify/scripts/generate-preferences.js
*
* ## Key Naming Convention
*

View File

@ -1,6 +1,6 @@
/**
* Auto-generated preference mappings from classification.json
* Generated at: 2025-11-28T13:37:20.033Z
* Generated at: 2025-11-29T03:45:07.227Z
*
* This file contains pure mapping relationships without default values.
* Default values are managed in packages/shared/data/preferences.ts
@ -30,50 +30,18 @@ export const ELECTRON_STORE_MAPPINGS = [
*/
export const REDUX_STORE_MAPPINGS = {
settings: [
{
originalKey: 'autoCheckUpdate',
targetKey: 'app.dist.auto_update.enabled'
},
{
originalKey: 'clickTrayToShowQuickAssistant',
targetKey: 'feature.quick_assistant.click_tray_to_show'
},
{
originalKey: 'disableHardwareAcceleration',
targetKey: 'app.disable_hardware_acceleration'
},
{
originalKey: 'enableDataCollection',
targetKey: 'app.privacy.data_collection.enabled'
},
{
originalKey: 'enableDeveloperMode',
targetKey: 'app.developer_mode.enabled'
},
{
originalKey: 'enableQuickAssistant',
targetKey: 'feature.quick_assistant.enabled'
},
{
originalKey: 'language',
targetKey: 'app.language'
},
{
originalKey: 'launchToTray',
targetKey: 'app.tray.on_launch'
},
{
originalKey: 'testChannel',
targetKey: 'app.dist.test_plan.channel'
},
{
originalKey: 'testPlan',
targetKey: 'app.dist.test_plan.enabled'
},
{
originalKey: 'theme',
targetKey: 'ui.theme_mode'
},
{
originalKey: 'launchToTray',
targetKey: 'app.tray.on_launch'
},
{
originalKey: 'tray',
targetKey: 'app.tray.enabled'
@ -82,6 +50,38 @@ export const REDUX_STORE_MAPPINGS = {
originalKey: 'trayOnClose',
targetKey: 'app.tray.on_close'
},
{
originalKey: 'clickTrayToShowQuickAssistant',
targetKey: 'feature.quick_assistant.click_tray_to_show'
},
{
originalKey: 'enableQuickAssistant',
targetKey: 'feature.quick_assistant.enabled'
},
{
originalKey: 'autoCheckUpdate',
targetKey: 'app.dist.auto_update.enabled'
},
{
originalKey: 'testPlan',
targetKey: 'app.dist.test_plan.enabled'
},
{
originalKey: 'testChannel',
targetKey: 'app.dist.test_plan.channel'
},
{
originalKey: 'enableDataCollection',
targetKey: 'app.privacy.data_collection.enabled'
},
{
originalKey: 'disableHardwareAcceleration',
targetKey: 'app.disable_hardware_acceleration'
},
{
originalKey: 'enableDeveloperMode',
targetKey: 'app.developer_mode.enabled'
},
{
originalKey: 'showAssistants',
targetKey: 'assistant.tab.show'
@ -649,12 +649,8 @@ export const REDUX_STORE_MAPPINGS = {
targetKey: 'feature.selection.enabled'
},
{
originalKey: 'filterList',
targetKey: 'feature.selection.filter_list'
},
{
originalKey: 'filterMode',
targetKey: 'feature.selection.filter_mode'
originalKey: 'triggerMode',
targetKey: 'feature.selection.trigger_mode'
},
{
originalKey: 'isFollowToolbar',
@ -665,8 +661,12 @@ export const REDUX_STORE_MAPPINGS = {
targetKey: 'feature.selection.remember_win_size'
},
{
originalKey: 'triggerMode',
targetKey: 'feature.selection.trigger_mode'
originalKey: 'filterMode',
targetKey: 'feature.selection.filter_mode'
},
{
originalKey: 'filterList',
targetKey: 'feature.selection.filter_list'
},
{
originalKey: 'isCompact',
@ -696,55 +696,25 @@ export const REDUX_STORE_MAPPINGS = {
},
{
originalKey: 'memoryConfig.isAutoDimensions',
targetKey: 'feature.memory.is_auto_dimensions'
targetKey: 'feature.memory.auto_dimensions'
},
{
originalKey: 'memoryConfig.customFactExtractionPrompt',
targetKey: 'feature.memory.fact_extraction_prompt'
},
{
originalKey: 'memoryConfig.customUpdateMemoryPrompt',
targetKey: 'feature.memory.update_memory_prompt'
},
{
originalKey: 'currentUserId',
targetKey: 'feature.memory.current_user_id'
},
{
originalKey: 'globalMemoryEnabled',
targetKey: 'feature.memory.enabled'
}
],
note: [
{
originalKey: 'settings.isFullWidth',
targetKey: 'feature.notes.full_width'
},
{
originalKey: 'settings.fontFamily',
targetKey: 'feature.notes.font_family'
},
{
originalKey: 'settings.fontSize',
targetKey: 'feature.notes.font_size'
},
{
originalKey: 'settings.showTableOfContents',
targetKey: 'feature.notes.show_table_of_contents'
},
{
originalKey: 'settings.defaultViewMode',
targetKey: 'feature.notes.default_view_mode'
},
{
originalKey: 'settings.defaultEditMode',
targetKey: 'feature.notes.default_edit_mode'
},
{
originalKey: 'settings.showTabStatus',
targetKey: 'feature.notes.show_tab_status'
},
{
originalKey: 'settings.showWorkspace',
targetKey: 'feature.notes.show_workspace'
},
{
originalKey: 'notesPath',
targetKey: 'feature.notes.path'
},
{
originalKey: 'sortType',
targetKey: 'feature.notes.sort_type'
}
],
nutstore: [
{
originalKey: 'nutstoreToken',
@ -836,6 +806,48 @@ export const REDUX_STORE_MAPPINGS = {
originalKey: 'shortcuts.exit_fullscreen',
targetKey: 'shortcut.app.exit_fullscreen'
}
],
note: [
{
originalKey: 'settings.isFullWidth',
targetKey: 'feature.notes.full_width'
},
{
originalKey: 'settings.fontFamily',
targetKey: 'feature.notes.font_family'
},
{
originalKey: 'settings.fontSize',
targetKey: 'feature.notes.font_size'
},
{
originalKey: 'settings.showTableOfContents',
targetKey: 'feature.notes.show_table_of_contents'
},
{
originalKey: 'settings.defaultViewMode',
targetKey: 'feature.notes.default_view_mode'
},
{
originalKey: 'settings.defaultEditMode',
targetKey: 'feature.notes.default_edit_mode'
},
{
originalKey: 'settings.showTabStatus',
targetKey: 'feature.notes.show_tab_status'
},
{
originalKey: 'settings.showWorkspace',
targetKey: 'feature.notes.show_workspace'
},
{
originalKey: 'notesPath',
targetKey: 'feature.notes.path'
},
{
originalKey: 'sortType',
targetKey: 'feature.notes.sort_type'
}
]
} as const
@ -844,9 +856,9 @@ export const REDUX_STORE_MAPPINGS = {
/**
* :
* - ElectronStore项: 1
* - Redux Store项: 199
* - Redux分类: settings, selectionStore, memory, note, nutstore, shortcuts
* - 总配置项: 200
* - Redux Store项: 202
* - Redux分类: settings, selectionStore, memory, nutstore, shortcuts, note
* - 总配置项: 203
*
* 使:
* 1. ElectronStore读取: configManager.get(mapping.originalKey)

View File

@ -0,0 +1,40 @@
# V2 重构临时目录
本目录是 Cherry Studio V2 数据和 UI 重构项目的临时工作目录,用于存放重构过程中使用的共享工具、文档和临时文件。
**重要**: 本目录将在 V2 重构完成后删除。
## 目录结构
```
v2-refactor-temp/
├── tools/ # 重构工具
│ └── data-classify/ # 数据分类与代码生成工具
├── docs/ # 临时文档(如有需要)
└── README.md # 本文件
```
## 包含内容
### 工具 (tools/)
- **data-classify/** - 数据分类与代码生成工具
- 从源码提取数据清单
- 管理数据分类映射
- 生成 TypeScript 类型定义和迁移映射
- 详见 [tools/data-classify/README.md](./tools/data-classify/README.md)
## 使用说明
1. 本目录不包含任何生产代码,仅用于重构辅助
2. 生成的代码会输出到正式的项目目录中
3. 不要在本目录中存放需要长期保留的内容
## 清理计划
V2 重构完成后,本目录将被完全删除。届时需要:
1. 确认所有工具不再需要
2. 将有价值的文档迁移到正式位置(如有)
3. 删除整个 `v2-refactor-temp/` 目录
4. 更新 `.gitignore` 和相关引用

View File

@ -0,0 +1 @@
*.backup.*

View File

@ -0,0 +1,510 @@
# 数据分类与代码生成工具
Cherry Studio 数据重构项目的自动化工具集,用于管理数据分类和生成 TypeScript 代码。
**版本**: 2.0.0
**更新日期**: 2025-11-28
## 概述
本工具集提供以下功能:
- **数据提取**: 扫描源代码,构建数据清单
- **分类管理**: 维护分类映射,支持增量更新
- **代码生成**: 生成 TypeScript 接口和迁移映射
- **验证检查**: 确保清单与分类之间的一致性
## 目录结构
```
v2-refactor-temp/tools/data-classify/
├── scripts/
│ ├── lib/
│ │ └── classificationUtils.js # 共享工具函数
│ ├── extract-inventory.js # 从源码提取数据清单
│ ├── generate-all.js # 运行所有生成器
│ ├── generate-preferences.js # 生成 preferenceSchemas.ts
│ ├── generate-migration.js # 生成 PreferencesMappings.ts
│ ├── validate-consistency.js # 验证数据一致性
│ ├── validate-generation.js # 验证生成代码质量
│ └── check-duplicates.js # 检查重复的目标键
├── data/
│ ├── classification.json # 分类映射(人工维护)
│ └── inventory.json # 数据清单(脚本生成)
├── package.json
└── README.md # 本文档
```
## 快速开始
```bash
# 进入工具目录
cd v2-refactor-temp/tools/data-classify
# 安装依赖
npm install
# 运行完整工作流
npm run all
# 或者分步执行
npm run extract # 提取数据清单
npm run generate # 生成所有代码
npm run validate # 验证一致性
npm run validate:gen # 验证生成代码
```
## 可用脚本
| 脚本 | 说明 |
|------|------|
| `npm run extract` | 从源文件提取数据清单 |
| `npm run generate` | 运行所有代码生成器 |
| `npm run generate:preferences` | 仅生成 preferenceSchemas.ts |
| `npm run generate:migration` | 仅生成 PreferencesMappings.ts |
| `npm run validate` | 验证数据一致性 |
| `npm run validate:gen` | 验证生成代码质量 |
| `npm run check:duplicates` | 检查重复的目标键 |
| `npm run all` | 运行完整工作流 |
## 脚本架构
### 依赖关系图
```
┌─────────────────────────────────────────────────────────────┐
│ 共享模块 │
│ scripts/lib/classificationUtils.js │
│ - loadClassification() - traverseClassifications() │
│ - saveClassification() - calculateStats() │
│ - loadInventory() - normalizeType() │
│ - extractPreferencesData() - inferTypeFromValue() │
└─────────────────────────────────────────────────────────────┘
▲ ▲
│ │
┌───────────┘ └───────────┐
│ │
┌───────┴───────┐ ┌────────┴────────┐
│ extract- │ │ validate- │
│ inventory.js │ │ consistency.js │
│ │ │ │
│ 扫描源码 │ │ 检查数据 │
│ 构建清单 │ │ 一致性 │
└───────────────┘ └─────────────────┘
┌─────────────────────┐
│ generate-all.js │──────────────────────────────────┐
│ │ │
│ 编排所有生成器 │ │
└─────────────────────┘ │
│ │
│ require() │ require()
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ generate- │ │ generate- │
│ preferences.js │ │ migration.js │
│ │ │ │
│ 生成 │ │ 生成 │
│ preferenceSchemas.ts│ │ PreferencesMappings │
└─────────────────────┘ └─────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ validate- │ │ check- │
│ generation.js │ │ duplicates.js │
│ │ │ │
│ 验证生成代码质量 │ │ 检查重复目标键 │
│ (独立运行) │ │ (独立运行) │
└─────────────────────┘ └─────────────────────┘
```
### 脚本详情
| 脚本 | 输入 | 输出 | 依赖 |
|------|------|------|------|
| `extract-inventory.js` | 源代码文件 | `data/inventory.json` | `classificationUtils.js` |
| `generate-preferences.js` | `classification.json` | `preferenceSchemas.ts` | 无 |
| `generate-migration.js` | `classification.json` | `PreferencesMappings.ts` | 无 |
| `generate-all.js` | - | 运行两个生成器 | `generate-preferences.js`, `generate-migration.js` |
| `validate-consistency.js` | `inventory.json`, `classification.json` | `validation-report.md` | `classificationUtils.js` |
| `validate-generation.js` | 生成的 `.ts` 文件 | 控制台输出 | 无 |
| `check-duplicates.js` | `classification.json` | 控制台输出 | 无 |
## 数据分类工作流
### 1. 提取数据清单
```bash
npm run extract
```
扫描源文件并提取以下数据源的信息:
- **Redux Store**: `src/renderer/src/store/*.ts`
- **Electron Store**: `src/main/services/ConfigManager.ts`
- **LocalStorage**: 所有使用 localStorage 的文件
- **Dexie 数据库**: `src/renderer/src/databases/index.ts`
### 2. 分类数据
编辑 `data/classification.json` 对每个数据项进行分类:
```json
{
"originalKey": "theme",
"type": "string",
"status": "classified",
"category": "preferences",
"targetKey": "ui.theme_mode"
}
```
### 3. 生成代码
```bash
npm run generate
```
生成以下 TypeScript 文件:
- `packages/shared/data/preference/preferenceSchemas.ts` - 类型定义
- `src/main/data/migration/v2/migrators/mappings/PreferencesMappings.ts` - 迁移映射
### 4. 验证
```bash
npm run validate
npm run validate:gen
```
验证内容:
- 所有清单项都已分类
- 没有孤立的分类条目
- 命名规范一致
- 没有重复的目标键
- 生成代码结构正确
---
## 数据分类标准
根据 Cherry Studio 数据重构架构,所有数据需要分类到以下 5 个类别之一:
### 1. 偏好配置 (preferences)
**判断标准**:
- ✅ 影响应用全局行为的配置
- ✅ 用户可以修改的设置项
- ✅ 简单的数据类型boolean/string/number/简单 array/object
- ✅ 结构相对稳定,不经常变化
- ✅ 数据量小,可以重建
- ✅ 需要在窗口间同步
**典型例子**:
- `showAssistants`: 是否显示助手面板
- `theme`: 主题设置light/dark/system
- `fontSize`: 字体大小
- `language`: 界面语言
**命名规范**:
- 使用点分隔的层级结构:`ui.fontSize`、`system.language`
- 分组前缀:`ui.*`(界面)、`system.*`(系统)、`app.*`(应用行为)等
### 2. 用户数据 (user_data)
**判断标准**:
- ✅ 用户创建或输入的内容
- ✅ 不可丢失的重要数据
- ✅ 数据量可能很大
- ✅ 需要完整备份和迁移机制
- ✅ 可能包含敏感信息
**典型例子**:
- `topics`: 对话历史
- `messages`: 消息内容
- `files`: 用户上传的文件
- `knowledge_notes`: 知识库笔记
**特殊处理**:
- 敏感数据需要加密存储
- 大数据表需要考虑分页和流式处理
### 3. 缓存数据 (cache)
**判断标准**:
- ✅ 可以重新生成的数据
- ✅ 主要用于性能优化
- ✅ 丢失后不影响核心功能
- ✅ 有过期时间或清理机制
**典型例子**:
- `failed_favicon_*`: 失败的 favicon 缓存
- 搜索结果缓存
- 图片预览缓存
- 模型响应缓存
### 4. 运行时数据 (runtime)
**判断标准**:
- ✅ 内存型数据,不需要持久化
- ✅ 生命周期 ≤ 应用进程
- ✅ 应用重启后可以丢失
- ✅ 临时状态信息
**典型例子**:
- 当前选中的对话
- 临时的输入状态
- UI 组件的展开/折叠状态
- 网络请求状态
### 5. 应用资源 (resources)
**判断标准**:
- ✅ 静态资源文件
- ✅ 随应用分发的内容
- ✅ 不需要用户修改
- ✅ 暂不考虑重构
**典型例子**:
- 图标文件
- 本地化翻译文件
- 默认配置文件
- 帮助文档
---
## 分类决策流程图
```
数据项
是否用户创建/输入的内容?
↓ 是 ↓ 否
用户数据 是否需要持久化?
↓ 否 ↓ 是
运行时数据 是否可重新生成?
↓ 是 ↓ 否
缓存数据 是否用户可修改?
↓ 是 ↓ 否
偏好配置 应用资源
```
---
## 分类示例
### 示例 1: Redux settings.showAssistants
```json
{
"classifications": {
"redux": {
"settings": [
{
"originalKey": "showAssistants",
"type": "boolean",
"defaultValue": true,
"status": "classified",
"category": "preferences",
"targetKey": "ui.show_assistants"
}
]
}
}
}
```
**分析过程**:
1. 数据用途:控制是否显示助手面板
2. 用户可修改:✅
3. 影响全局:✅
4. 数据简单:✅ boolean 类型
5. 结论:偏好配置
### 示例 2: 嵌套结构 (Redux settings with children)
```json
{
"originalKey": "codeEditor",
"type": "object",
"children": [
{
"originalKey": "enabled",
"type": "boolean",
"defaultValue": true,
"status": "classified",
"category": "preferences",
"targetKey": "code_editor.enabled"
},
{
"originalKey": "fontSize",
"type": "number",
"defaultValue": 14,
"status": "classified",
"category": "preferences",
"targetKey": "code_editor.font_size"
}
]
}
```
**注意**: 父级项不需要 `status`/`category`/`targetKey`,这些只在叶子节点设置。
### 示例 3: Dexie topics 表
```json
{
"originalKey": "topics",
"type": "table",
"status": "classified",
"category": "user_data",
"targetTable": "topic",
"notes": "用户对话历史,核心业务数据"
}
```
---
## 命名规范
偏好配置键必须遵循:`namespace.sub.key_name`
**规则**:
- 至少 2 个由点分隔的段
- 仅使用小写字母、数字、下划线
- 模式:`/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/`
**示例**:
- `app.theme` (有效)
- `chat.input.send_shortcut` (有效)
- `Theme` (无效 - 没有点分隔符)
- `App.User` (无效 - 大写字母)
---
## 增量更新策略
### 核心特性
- **保留已分类数据**: 重新运行提取不会丢失已有分类
- **标记删除项**: 删除的数据项被标记但不移除
- **自动发现新项**: 新数据项自动添加到待处理列表
- **自动备份**: 每次运行前自动备份原分类文件
### 更新流程
1. 代码变更后运行 `npm run extract`
2. 脚本自动备份 `classification.json``classification.backup.json`
3. 脚本识别新增和删除的数据项
4. 新项添加到 `pending` 数组
5. 删除项标记为 `status: 'classified-deleted'`
6. 手动处理新的待处理项
---
## 文件格式说明
### inventory.json 结构
```json
{
"metadata": {
"generatedAt": "ISO 日期",
"version": "版本号"
},
"redux": {
"moduleName": {
"fieldName": {
"type": "数据类型",
"defaultValue": "默认值"
}
}
},
"electronStore": { ... },
"localStorage": { ... },
"dexie": { ... }
}
```
### classification.json 结构
```json
{
"metadata": {
"version": "版本号",
"lastUpdated": "ISO 日期"
},
"classifications": {
"redux": {
"moduleName": [
{
"originalKey": "字段名",
"type": "数据类型",
"status": "classified|pending|classified-deleted",
"category": "preferences|user_data|cache|runtime|resources",
"targetKey": "target.key.name"
}
]
},
"electronStore": { ... },
"localStorage": { ... },
"dexie": { ... }
}
}
```
### 状态值说明
| Status | 说明 | 操作建议 |
|--------|------|----------|
| `pending` | 待分类 | 需要人工分析并设置 category 和 targetKey |
| `classified` | 已分类 | 分类完成,可用于代码生成 |
| `classified-deleted` | 已分类但源已删除 | 源代码中已不存在,保留历史记录 |
---
## 故障排除
### "Module not found" 错误
```bash
cd v2-refactor-temp/tools/data-classify
npm install
```
### 验证错误
1. 检查 `validation-report.md` 了解详情
2. 修复 `classification.json` 条目
3. 重新运行验证
### 生成代码问题
1. 运行 `npm run validate:gen` 识别问题
2. 检查源分类数据
3. 使用 `npm run generate` 重新生成
### 数据项被错误标记为删除
检查提取脚本的模式是否正确匹配代码结构。
### 如何恢复意外删除的分类
从以下位置恢复 `classification.json`
- 自动备份文件:`classification.backup.json`
- Git 历史记录
---
## 当前进度 (2025-11-28)
### 已完成
1. **自动生成映射关系** - `generate-migration.js` 生成纯映射代码
2. **158 个真实配置项迁移** - 替换了原来的 3 个硬编码测试项
3. **嵌套路径支持** - 处理 Redux Store children 结构39 个嵌套路径)
4. **类型安全迁移** - 基于 `preferenceSchemas.ts` 类型定义
5. **脚本重构** - 共享工具、一致路径、移除废弃脚本
### 生成的核心文件
- **preferenceSchemas.ts** - 类型安全配置定义200 个偏好项)
- **PreferencesMappings.ts** - 纯映射常量ElectronStore + Redux 项)
### 技术特性
- **数据源分离**: ElectronStore简单数组、Redux Store按类别分组
- **嵌套路径解析**: 支持 `codeEditor.enabled`、`exportMenuOptions.docx` 等
- **统一默认值管理**: 单一数据源,无重复定义
- **自动去重**: 重复 targetKey 自动处理redux 优先级最高)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,490 @@
{
"name": "data-classify",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "data-classify",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"glob": "^11.0.3"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}

View File

@ -0,0 +1,30 @@
{
"name": "data-classify",
"version": "2.0.0",
"description": "Data classification and code generation tools for Cherry Studio data refactoring",
"main": "index.js",
"directories": {
"doc": "docs"
},
"scripts": {
"extract": "node scripts/extract-inventory.js",
"generate": "node scripts/generate-all.js",
"generate:preferences": "node scripts/generate-preferences.js",
"generate:migration": "node scripts/generate-migration.js",
"validate": "node scripts/validate-consistency.js",
"validate:gen": "node scripts/validate-generation.js",
"check:duplicates": "node scripts/check-duplicates.js",
"all": "npm run extract && npm run generate && npm run validate && npm run validate:gen"
},
"keywords": [
"data-classification",
"code-generation",
"migration"
],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"glob": "^11.0.3"
}
}

View File

@ -0,0 +1,78 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
function checkDuplicatesAndChildren() {
const classificationFile = path.join(__dirname, '../data/classification.json')
const classification = JSON.parse(fs.readFileSync(classificationFile, 'utf8'))
// 提取所有preferences项包括children
const allPrefs = []
function extractItems(items, source, category, parentKey = '') {
if (!Array.isArray(items)) return
items.forEach((item) => {
// 处理有children的项目
if (item.children) {
console.log(`发现children项: ${source}/${category}/${item.originalKey}`)
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
// 处理普通项目
if (item.category === 'preferences' && item.status === 'classified' && item.targetKey) {
allPrefs.push({
source,
category,
originalKey: parentKey + item.originalKey,
targetKey: item.targetKey,
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`
})
}
})
}
// 遍历所有数据源
;['electronStore', 'redux', 'localStorage'].forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
console.log(`\n=== 总共找到 ${allPrefs.length} 个preferences项 ===\n`)
// 检查重复的targetKey
const targetKeyGroups = {}
allPrefs.forEach((pref) => {
if (!targetKeyGroups[pref.targetKey]) {
targetKeyGroups[pref.targetKey] = []
}
targetKeyGroups[pref.targetKey].push(pref)
})
// 显示重复项
const duplicates = Object.keys(targetKeyGroups).filter((key) => targetKeyGroups[key].length > 1)
if (duplicates.length > 0) {
console.log('=== 重复的targetKey ===')
duplicates.forEach((targetKey) => {
console.log(`\n${targetKey}:`)
targetKeyGroups[targetKey].forEach((pref) => {
console.log(` - ${pref.fullPath}`)
})
})
} else {
console.log('✅ 没有发现重复的targetKey')
}
return { allPrefs, duplicates: duplicates.map((key) => targetKeyGroups[key]) }
}
if (require.main === module) {
checkDuplicatesAndChildren()
}
module.exports = checkDuplicatesAndChildren

View File

@ -0,0 +1,545 @@
#!/usr/bin/env node
/**
* Data Inventory Extractor
*
* Extracts data inventory from Cherry Studio source code and manages
* incremental updates to classification.json with backup protection.
*
* Features:
* - Extracts Redux, ElectronStore, LocalStorage, and Dexie data
* - Preserves existing classifications during updates
* - Creates automatic backups before modifications
* - Supports nested data structures with children
*
* Usage:
* node v2-refactor-temp/tools/data-classify/scripts/extract-inventory.js
*/
const fs = require('fs')
const path = require('path')
const {
loadClassification,
saveClassification,
normalizeType,
inferTypeFromValue,
calculateStats,
DATA_DIR
} = require('./lib/classificationUtils')
// Redux store modules configuration
const REDUX_STORE_MODULES = {
assistants: { file: 'assistants.ts', interface: 'AssistantsState' },
backup: { file: 'backup.ts', interface: 'BackupState' },
copilot: { file: 'copilot.ts', interface: 'CopilotState' },
inputTools: { file: 'inputTools.ts', interface: 'InputToolsState' },
knowledge: { file: 'knowledge.ts', interface: 'KnowledgeState' },
llm: { file: 'llm.ts', interface: 'LlmState' },
mcp: { file: 'mcp.ts', interface: 'McpState' },
memory: { file: 'memory.ts', interface: 'MemoryState' },
messageBlock: { file: 'messageBlock.ts', interface: 'MessageBlockState' },
migrate: { file: 'migrate.ts', interface: 'MigrateState' },
minapps: { file: 'minapps.ts', interface: 'MinappsState' },
newMessage: { file: 'newMessage.ts', interface: 'NewMessageState' },
nutstore: { file: 'nutstore.ts', interface: 'NutstoreState' },
paintings: { file: 'paintings.ts', interface: 'PaintingsState' },
preprocess: { file: 'preprocess.ts', interface: 'PreprocessState' },
runtime: { file: 'runtime.ts', interface: 'RuntimeState' },
selectionStore: { file: 'selectionStore.ts', interface: 'SelectionState' },
settings: { file: 'settings.ts', interface: 'SettingsState' },
shortcuts: { file: 'shortcuts.ts', interface: 'ShortcutsState' },
tabs: { file: 'tabs.ts', interface: 'TabsState' },
toolPermissions: { file: 'toolPermissions.ts', interface: 'ToolPermissionsState' },
translate: { file: 'translate.ts', interface: 'TranslateState' },
websearch: { file: 'websearch.ts', interface: 'WebSearchState' },
codeTools: { file: 'codeTools.ts', interface: 'CodeToolsState' },
ocr: { file: 'ocr.ts', interface: 'OcrState' },
note: { file: 'note.ts', interface: 'NoteState' }
}
class DataExtractor {
constructor(rootDir = '../../../../') {
this.rootDir = path.resolve(__dirname, rootDir)
this.dataDir = DATA_DIR
console.log('Root directory:', this.rootDir)
console.log('Data directory:', this.dataDir)
}
/**
* Main extraction entry point
*/
async extract() {
console.log('Starting data inventory extraction...\n')
const inventory = {
metadata: {
generatedAt: new Date().toISOString(),
version: '2.0.0',
description: 'Cherry Studio data inventory'
},
redux: await this.extractReduxData(),
electronStore: await this.extractElectronStoreData(),
localStorage: await this.extractLocalStorageData(),
dexie: await this.extractDexieData()
}
// Load existing classification and merge
let existingClassification
try {
existingClassification = loadClassification(this.dataDir)
} catch {
existingClassification = { classifications: {} }
}
const updatedData = this.mergeWithExisting(inventory, existingClassification)
// Save results
this.saveInventory(updatedData.inventory)
saveClassification(updatedData.classification, this.dataDir)
console.log('\nData extraction complete!')
this.printSummary(updatedData)
}
/**
* Extract Redux store data from source files
*/
async extractReduxData() {
console.log('Extracting Redux Store data...')
const reduxData = {}
for (const [moduleName, moduleInfo] of Object.entries(REDUX_STORE_MODULES)) {
const filePath = path.join(this.rootDir, `src/renderer/src/store/${moduleInfo.file}`)
if (!fs.existsSync(filePath)) {
console.warn(` Warning: ${moduleInfo.file} not found`)
continue
}
const content = fs.readFileSync(filePath, 'utf8')
const stateInterface = this.extractStateInterface(content, moduleInfo.interface)
const initialState = this.extractInitialState(content)
reduxData[moduleName] = {
_meta: {
file: `src/renderer/src/store/${moduleInfo.file}`,
interface: moduleInfo.interface
}
}
// Add fields from interface and initial state
const fields = Object.keys(stateInterface).length > 0 ? stateInterface : initialState
for (const [fieldName, fieldInfo] of Object.entries(fields)) {
if (fieldName === '_meta') continue
reduxData[moduleName][fieldName] = {
file: `src/renderer/src/store/${moduleInfo.file}`,
type: fieldInfo.type || inferTypeFromValue(initialState[fieldName]),
defaultValue: initialState[fieldName] ?? fieldInfo.defaultValue ?? null
}
}
}
console.log(` Found ${Object.keys(reduxData).length} Redux modules`)
return reduxData
}
/**
* Extract Electron Store configuration keys
*/
async extractElectronStoreData() {
console.log('Extracting Electron Store data...')
const electronStoreData = {}
const configManagerPath = path.join(this.rootDir, 'src/main/services/ConfigManager.ts')
if (!fs.existsSync(configManagerPath)) {
console.warn(' Warning: ConfigManager.ts not found')
return electronStoreData
}
const content = fs.readFileSync(configManagerPath, 'utf8')
const configKeys = this.extractConfigKeys(content)
for (const key of configKeys) {
electronStoreData[key] = {
file: 'src/main/services/ConfigManager.ts',
enum: `ConfigKeys.${key}`,
type: 'unknown',
defaultValue: null
}
}
console.log(` Found ${configKeys.length} ConfigKeys`)
return electronStoreData
}
/**
* Extract localStorage usage from source files
*/
async extractLocalStorageData() {
console.log('Extracting LocalStorage data...')
const localStorageData = {}
const { glob } = require('glob')
const files = await glob('src/**/*.ts', { cwd: this.rootDir })
for (const file of files) {
const filePath = path.join(this.rootDir, file)
const content = fs.readFileSync(filePath, 'utf8')
// Find localStorage.getItem and localStorage.setItem calls
const getItemRegex = /localStorage\.getItem\(['"]([^'"]+)['"]\)/g
const setItemRegex = /localStorage\.setItem\(['"]([^'"]+)['"],/g
let match
while ((match = getItemRegex.exec(content)) !== null) {
if (!localStorageData[match[1]]) {
localStorageData[match[1]] = {
file: file,
type: 'string',
defaultValue: null
}
}
}
while ((match = setItemRegex.exec(content)) !== null) {
if (!localStorageData[match[1]]) {
localStorageData[match[1]] = {
file: file,
type: 'string',
defaultValue: null
}
}
}
}
console.log(` Found ${Object.keys(localStorageData).length} localStorage keys`)
return localStorageData
}
/**
* Extract Dexie database tables
*/
async extractDexieData() {
console.log('Extracting Dexie data...')
const dexieData = {}
const databasePath = path.join(this.rootDir, 'src/renderer/src/databases/index.ts')
if (!fs.existsSync(databasePath)) {
console.warn(' Warning: databases/index.ts not found')
return dexieData
}
const content = fs.readFileSync(databasePath, 'utf8')
// Match table definitions like: tableName: EntityTable<TypeName>
const tableRegex = /(\w+):\s*EntityTable<([^>]+)>/g
let match
while ((match = tableRegex.exec(content)) !== null) {
dexieData[match[1]] = {
file: 'src/renderer/src/databases/index.ts',
type: `EntityTable<${match[2]}>`,
schema: null
}
}
console.log(` Found ${Object.keys(dexieData).length} Dexie tables`)
return dexieData
}
/**
* Extract TypeScript interface fields
*/
extractStateInterface(content, interfaceName) {
const fields = {}
// Match interface definition
const interfaceMatch = content.match(new RegExp(`export interface ${interfaceName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm'))
if (!interfaceMatch) return fields
const interfaceBody = interfaceMatch[1]
const lines = interfaceBody.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue
// Match field: type pattern
const fieldMatch = trimmed.match(/^(\w+)(\?)?:\s*([^;]+)/)
if (fieldMatch) {
const fieldName = fieldMatch[1]
let fieldType = fieldMatch[3].trim().replace(/[,;]$/, '')
fields[fieldName] = {
type: normalizeType(fieldType),
defaultValue: null
}
}
}
return fields
}
/**
* Extract initial state values from TypeScript file
*/
extractInitialState(content) {
const state = {}
// Match initialState definition
const stateMatch = content.match(/(?:export )?const initialState[^=]*=\s*\{([\s\S]*?)\n\}(?=\s*\n|$)/m)
if (!stateMatch) return state
// Simple field extraction
const stateBody = stateMatch[1]
const lines = stateBody.split('\n')
let currentField = null
let currentValue = ''
let braceCount = 0
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('//')) continue
if (currentField) {
currentValue += ' ' + trimmed
braceCount += (trimmed.match(/\{/g) || []).length
braceCount -= (trimmed.match(/\}/g) || []).length
if (braceCount === 0 && (trimmed.endsWith(',') || trimmed === '}')) {
state[currentField] = this.parseValue(currentValue.replace(/,$/, '').trim())
currentField = null
currentValue = ''
}
continue
}
const fieldMatch = trimmed.match(/^(\w+):\s*(.+)/)
if (fieldMatch) {
const fieldName = fieldMatch[1]
const fieldValue = fieldMatch[2]
braceCount = (fieldValue.match(/\{/g) || []).length - (fieldValue.match(/\}/g) || []).length
if (braceCount === 0 && (fieldValue.endsWith(',') || fieldValue.endsWith('}'))) {
state[fieldName] = this.parseValue(fieldValue.replace(/,$/, '').trim())
} else {
currentField = fieldName
currentValue = fieldValue
}
}
}
return state
}
/**
* Extract ConfigKeys enum values
*/
extractConfigKeys(content) {
const keys = []
const enumMatch = content.match(/export enum ConfigKeys \{([^}]+)\}/g)
if (enumMatch) {
const enumContent = enumMatch[0]
const keyRegex = /(\w+)\s*=/g
let match
while ((match = keyRegex.exec(enumContent)) !== null) {
keys.push(match[1])
}
}
return keys
}
/**
* Parse a value string to appropriate type
*/
parseValue(valueStr) {
const trimmed = valueStr.trim()
if (trimmed === 'true') return true
if (trimmed === 'false') return false
if (trimmed === 'null') return null
if (trimmed === 'undefined') return undefined
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10)
if (/^-?\d*\.\d+$/.test(trimmed)) return parseFloat(trimmed)
// Handle strings
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1)
}
// Handle arrays and objects - return as string for complex values
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
try {
return JSON.parse(trimmed.replace(/'/g, '"'))
} catch {
return trimmed
}
}
return trimmed
}
/**
* Merge new inventory with existing classification
*/
mergeWithExisting(newInventory, existingClassification) {
const updatedClassifications = this.convertToNestedStructure(newInventory, existingClassification)
// Calculate statistics
const stats = calculateStats(updatedClassifications)
const updatedClassification = {
metadata: {
version: '2.0.0',
lastUpdated: new Date().toISOString(),
totalItems: stats.total,
classified: stats.byStatus.classified || 0,
pending: stats.byStatus.pending || 0,
deleted: stats.byStatus['classified-deleted'] || 0
},
classifications: updatedClassifications
}
return {
inventory: newInventory,
classification: updatedClassification
}
}
/**
* Convert flat inventory to nested classification structure
*/
convertToNestedStructure(newInventory, existingClassification) {
const nestedClassifications = {}
const existing = existingClassification?.classifications || {}
for (const [source, data] of Object.entries(newInventory)) {
if (source === 'metadata') continue
if (!nestedClassifications[source]) {
nestedClassifications[source] = {}
}
if (source === 'redux') {
// Redux: group by module
for (const [moduleName, moduleData] of Object.entries(data)) {
if (!nestedClassifications[source][moduleName]) {
nestedClassifications[source][moduleName] = []
}
for (const fieldName of Object.keys(moduleData)) {
if (fieldName === '_meta') continue
const fieldData = moduleData[fieldName]
const existingItem = this.findExisting(existing, source, moduleName, fieldName)
nestedClassifications[source][moduleName].push({
originalKey: fieldName,
type: existingItem?.type || normalizeType(fieldData?.type),
defaultValue: existingItem?.defaultValue ?? fieldData?.defaultValue ?? null,
status: existingItem?.status || 'pending',
category: existingItem?.category || null,
targetKey: existingItem?.targetKey || null,
...(existingItem?.children ? { children: existingItem.children } : {})
})
}
}
} else {
// Other sources: direct mapping
for (const [tableName, tableData] of Object.entries(data)) {
if (!nestedClassifications[source][tableName]) {
nestedClassifications[source][tableName] = []
}
const existingItem = this.findExisting(existing, source, tableName, null)
nestedClassifications[source][tableName].push({
originalKey: tableName,
type: existingItem?.type || (source === 'dexie' ? 'table' : normalizeType(tableData?.type)),
defaultValue: existingItem?.defaultValue ?? tableData?.defaultValue ?? null,
status: existingItem?.status || 'pending',
category: existingItem?.category || null,
targetKey: existingItem?.targetKey || null
})
}
}
}
return nestedClassifications
}
/**
* Find existing classification item
*/
findExisting(existing, source, moduleOrTable, field) {
if (!existing[source]) return null
const sourceData = existing[source]
const items = sourceData[moduleOrTable]
if (!Array.isArray(items)) return null
// For redux: find by field name in module items
if (field) {
for (const item of items) {
if (item.originalKey === field) return item
if (item.children) {
const child = item.children.find((c) => c.originalKey === field)
if (child) return child
}
}
} else {
// For other sources: find by table name
return items.find((item) => item.originalKey === moduleOrTable)
}
return null
}
/**
* Save inventory to file
*/
saveInventory(inventory) {
const inventoryPath = path.join(this.dataDir, 'inventory.json')
fs.writeFileSync(inventoryPath, JSON.stringify(inventory, null, 2), 'utf8')
console.log(`\nInventory saved: ${inventoryPath}`)
}
/**
* Print extraction summary
*/
printSummary(updatedData) {
const { inventory, classification } = updatedData
console.log('\n========== Extraction Summary ==========')
console.log(`Redux modules: ${Object.keys(inventory.redux || {}).length}`)
console.log(`Electron Store keys: ${Object.keys(inventory.electronStore || {}).length}`)
console.log(`LocalStorage keys: ${Object.keys(inventory.localStorage || {}).length}`)
console.log(`Dexie tables: ${Object.keys(inventory.dexie || {}).length}`)
console.log('----------------------------------------')
console.log(`Total items: ${classification.metadata.totalItems}`)
console.log(`Classified: ${classification.metadata.classified}`)
console.log(`Pending: ${classification.metadata.pending}`)
if (classification.metadata.deleted > 0) {
console.log(`Deleted: ${classification.metadata.deleted}`)
}
console.log('========================================\n')
}
}
// Run script
if (require.main === module) {
const extractor = new DataExtractor()
extractor.extract().catch(console.error)
}
module.exports = DataExtractor

View File

@ -0,0 +1,43 @@
#!/usr/bin/env node
const PreferencesGenerator = require('./generate-preferences')
const MigrationGenerator = require('./generate-migration')
async function generateAll() {
console.log('🚀 开始生成preferences.ts和迁移代码...\n')
try {
// 步骤1: 生成preferences.ts
console.log('📋 步骤 1/2: 生成preferences.ts')
const preferencesGenerator = new PreferencesGenerator()
preferencesGenerator.generate()
console.log('✅ preferences.ts 生成完成\n')
// 步骤2: 生成迁移代码
console.log('🔄 步骤 2/2: 生成迁移代码')
const migrationGenerator = new MigrationGenerator()
migrationGenerator.generate()
console.log('✅ 迁移代码生成完成\n')
// 成功总结
console.log('🎉 所有代码生成成功!')
console.log('\n📝 生成的文件:')
console.log(' - packages/shared/data/preference/preferenceSchemas.ts')
console.log(' - src/main/data/migration/v2/migrators/mappings/PreferencesMappings.ts')
console.log('\n🔧 下一步操作:')
console.log(' 1. 运行 yarn typecheck 检查类型')
console.log(' 2. 运行 yarn lint --fix 格式化代码')
console.log(' 3. 测试迁移代码的功能')
} catch (error) {
console.error('❌ 生成过程中发生错误:', error.message)
process.exit(1)
}
}
// 如果直接运行此脚本
if (require.main === module) {
generateAll()
}
module.exports = generateAll

View File

@ -0,0 +1,239 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
class SimpleMappingGenerator {
constructor() {
this.dataDir = path.resolve(__dirname, '../data')
this.targetDir = path.resolve(__dirname, '../../../../src/main/data/migration/v2/migrators/mappings')
this.classificationFile = path.join(this.dataDir, 'classification.json')
}
generate() {
console.log('开始生成简化的映射关系代码...')
// 读取分类数据
const classification = this.loadClassification()
// 提取preferences相关数据
const preferencesData = this.extractPreferencesData(classification)
// 创建目标目录
this.ensureTargetDirectory()
// 生成映射关系文件
this.generateMappings(preferencesData)
console.log('映射关系生成完成!')
this.printSummary(preferencesData)
}
loadClassification() {
if (!fs.existsSync(this.classificationFile)) {
throw new Error(`分类文件不存在: ${this.classificationFile}`)
}
const content = fs.readFileSync(this.classificationFile, 'utf8')
return JSON.parse(content)
}
extractPreferencesData(classification) {
const allPreferencesData = []
const sources = ['electronStore', 'redux', 'localStorage']
// 递归提取项目包括children (保持现有逻辑)
const extractItems = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
items.forEach((item) => {
// 处理有children的项目
if (item.children && Array.isArray(item.children)) {
console.log(`处理children项: ${source}/${category}/${item.originalKey}`)
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
// 处理普通项目
if (item.category === 'preferences' && item.status === 'classified' && item.targetKey) {
allPreferencesData.push({
...item,
source,
sourceCategory: category,
originalKey: parentKey + item.originalKey, // 包含父级路径
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`
})
}
})
}
sources.forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
console.log(`提取到 ${allPreferencesData.length} 个preferences项包含children`)
// 处理重复的targetKey优先使用redux数据
const targetKeyGroups = {}
allPreferencesData.forEach((item) => {
if (!targetKeyGroups[item.targetKey]) {
targetKeyGroups[item.targetKey] = []
}
targetKeyGroups[item.targetKey].push(item)
})
// 去重按redux > localStorage > electronStore优先级选择
const sourcePriority = { redux: 3, localStorage: 2, electronStore: 1 }
const deduplicatedData = []
Object.keys(targetKeyGroups).forEach((targetKey) => {
const items = targetKeyGroups[targetKey]
if (items.length > 1) {
console.log(`发现重复targetKey: ${targetKey},共${items.length}`)
items.forEach((item) => console.log(` - ${item.fullPath}`))
// 按优先级排序,选择最高优先级的项
items.sort((a, b) => sourcePriority[b.source] - sourcePriority[a.source])
const selected = items[0]
console.log(` 选择: ${selected.fullPath}`)
deduplicatedData.push(selected)
} else {
deduplicatedData.push(items[0])
}
})
console.log(`去重后剩余 ${deduplicatedData.length} 个preferences项`)
// 按数据源分组
const preferencesData = {
electronStore: [],
redux: [],
localStorage: [],
all: deduplicatedData
}
deduplicatedData.forEach((item) => {
if (preferencesData[item.source]) {
preferencesData[item.source].push(item)
}
})
return preferencesData
}
ensureTargetDirectory() {
if (!fs.existsSync(this.targetDir)) {
fs.mkdirSync(this.targetDir, { recursive: true })
}
}
generateMappings(preferencesData) {
// 生成ElectronStore映射 - 简单结构不需要sourceCategory
const electronStoreMappings = preferencesData.electronStore.map((item) => ({
originalKey: item.originalKey,
targetKey: item.targetKey
}))
// 生成Redux映射 - 按category分组
const reduxMappings = {}
preferencesData.redux.forEach((item) => {
if (!reduxMappings[item.sourceCategory]) {
reduxMappings[item.sourceCategory] = []
}
reduxMappings[item.sourceCategory].push({
originalKey: item.originalKey, // 可能包含嵌套路径,如"codeEditor.enabled"
targetKey: item.targetKey
})
})
// 生成映射关系文件内容
const content = `/**
* Auto-generated preference mappings from classification.json
* Generated at: ${new Date().toISOString()}
*
* This file contains pure mapping relationships without default values.
* Default values are managed in packages/shared/data/preferences.ts
*
* === AUTO-GENERATED CONTENT START ===
*/
/**
* ElectronStore映射关系 - 简单一层结构
*
* ElectronStore没有嵌套originalKey直接对应configManager.get(key)
*/
export const ELECTRON_STORE_MAPPINGS = ${JSON.stringify(electronStoreMappings, null, 2)} as const
/**
* Redux Store映射关系 - 按category分组支持嵌套路径
*
* Redux Store可能有children结构originalKey可能包含嵌套路径:
* - 直接字段: "theme" -> reduxData.settings.theme
* - 嵌套字段: "codeEditor.enabled" -> reduxData.settings.codeEditor.enabled
* - 多层嵌套: "exportMenuOptions.docx" -> reduxData.settings.exportMenuOptions.docx
*/
export const REDUX_STORE_MAPPINGS = ${JSON.stringify(reduxMappings, null, 2)} as const
// === AUTO-GENERATED CONTENT END ===
/**
* 映射统计:
* - ElectronStore项: ${electronStoreMappings.length}
* - Redux Store项: ${preferencesData.redux.length}
* - Redux分类: ${Object.keys(reduxMappings).join(', ')}
* - 总配置项: ${preferencesData.all.length}
*
* 使用说明:
* 1. ElectronStore读取: configManager.get(mapping.originalKey)
* 2. Redux读取: 需要解析嵌套路径 reduxData[category][originalKey路径]
* 3. 默认值: 从defaultPreferences.default[mapping.targetKey]获取
*/`
// 写入文件
const targetFile = path.join(this.targetDir, 'PreferencesMappings.ts')
fs.writeFileSync(targetFile, content, 'utf8')
console.log(`映射关系文件已生成: ${targetFile}`)
}
printSummary(preferencesData) {
console.log(`\n生成摘要:`)
console.log(`- 输出文件: PreferencesMappings.ts`)
console.log(`- ElectronStore映射: ${preferencesData.electronStore.length}`)
console.log(`- Redux Store映射: ${preferencesData.redux.length}`)
console.log(`- 总配置项: ${preferencesData.all.length}`)
// 显示Redux分类
const reduxCategories = [...new Set(preferencesData.redux.map((item) => item.sourceCategory))]
console.log(`- Redux分类: ${reduxCategories.join(', ')}`)
// 显示一些嵌套路径的例子
const nestedKeys = preferencesData.redux
.filter((item) => item.originalKey.includes('.'))
.slice(0, 5)
.map((item) => item.originalKey)
if (nestedKeys.length > 0) {
console.log(`\n嵌套路径示例:`)
nestedKeys.forEach((key) => console.log(` - ${key}`))
}
}
}
// 主执行逻辑
if (require.main === module) {
try {
const generator = new SimpleMappingGenerator()
generator.generate()
} catch (error) {
console.error('生成失败:', error.message)
process.exit(1)
}
}
module.exports = SimpleMappingGenerator

View File

@ -0,0 +1,401 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
class PreferencesGenerator {
constructor() {
this.dataDir = path.resolve(__dirname, '../data')
this.targetFile = path.resolve(__dirname, '../../../../packages/shared/data/preference/preferenceSchemas.ts')
this.classificationFile = path.join(this.dataDir, 'classification.json')
}
generate() {
console.log('开始生成 preferences.ts...')
// 读取分类数据
const classification = this.loadClassification()
// 提取preferences相关数据
const preferencesData = this.extractPreferencesData(classification)
// 构建类型结构
const typeStructure = this.buildTypeStructure(preferencesData)
// 生成TypeScript代码
const content = this.generateTypeScriptCode(typeStructure, preferencesData)
// 写入文件
this.writePreferencesFile(content)
console.log('preferences.ts 生成完成!')
this.printSummary(preferencesData)
}
loadClassification() {
if (!fs.existsSync(this.classificationFile)) {
throw new Error(`分类文件不存在: ${this.classificationFile}`)
}
const content = fs.readFileSync(this.classificationFile, 'utf8')
return JSON.parse(content)
}
extractPreferencesData(classification) {
const allPreferencesData = []
const sources = ['electronStore', 'redux', 'localStorage']
// 递归提取项目包括children
const extractItems = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
items.forEach((item) => {
// 处理有children的项目
if (item.children && Array.isArray(item.children)) {
console.log(`处理children项: ${source}/${category}/${item.originalKey}`)
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
// 处理普通项目
if (item.category === 'preferences' && item.status === 'classified' && item.targetKey) {
allPreferencesData.push({
...item,
source,
sourceCategory: category,
originalKey: parentKey + item.originalKey, // 包含父级路径
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`
})
}
})
}
sources.forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
console.log(`提取到 ${allPreferencesData.length} 个preferences项包含children`)
// 处理重复的targetKey优先使用redux数据
const targetKeyGroups = {}
allPreferencesData.forEach((item) => {
if (!targetKeyGroups[item.targetKey]) {
targetKeyGroups[item.targetKey] = []
}
targetKeyGroups[item.targetKey].push(item)
})
// 去重按redux > localStorage > electronStore优先级选择
const sourcePriority = { redux: 3, localStorage: 2, electronStore: 1 }
const deduplicatedData = []
Object.keys(targetKeyGroups).forEach((targetKey) => {
const items = targetKeyGroups[targetKey]
if (items.length > 1) {
console.log(`发现重复targetKey: ${targetKey},共${items.length}`)
items.forEach((item) => console.log(` - ${item.fullPath}`))
// 按优先级排序,选择最高优先级的项
items.sort((a, b) => sourcePriority[b.source] - sourcePriority[a.source])
const selected = items[0]
console.log(` 选择: ${selected.fullPath}`)
deduplicatedData.push(selected)
} else {
deduplicatedData.push(items[0])
}
})
console.log(`去重后剩余 ${deduplicatedData.length} 个preferences项`)
return deduplicatedData
}
buildTypeStructure(preferencesData) {
const structure = { default: {} }
preferencesData.forEach((item) => {
if (!item.targetKey) return
// 直接使用targetKey作为键不进行拆分
structure.default[item.targetKey] = {
type: this.mapType(item.type, item.defaultValue),
defaultValue: item.defaultValue,
description: `${item.source}/${item.sourceCategory}/${item.originalKey}`,
originalItem: item
}
})
return structure
}
mapType(itemType, defaultValue) {
// 优先使用明确定义的类型只有当type为unknown时才进行类型推断
// 'VALUE: null' is a special marker to indicate the value should be null and not overwritten
const isNullable = defaultValue === null || defaultValue === undefined || defaultValue === 'VALUE: null'
// 如果type不是unknown直接使用定义好的类型
if (itemType && itemType !== 'unknown') {
// 处理简单的基础类型
if (itemType === 'boolean') {
return isNullable ? 'boolean | null' : 'boolean'
}
if (itemType === 'string') {
return isNullable ? 'string | null' : 'string'
}
if (itemType === 'number') {
return isNullable ? 'number | null' : 'number'
}
// 处理数组类型支持string[]、number[]等格式)
if (itemType.endsWith('[]')) {
return isNullable ? `${itemType} | null` : itemType
}
// 处理array泛型类型
if (itemType === 'array') {
// 尝试从默认值推断数组元素类型
if (Array.isArray(defaultValue) && defaultValue.length > 0) {
const elementType = typeof defaultValue[0]
return `${elementType}[]`
}
return isNullable ? 'unknown[] | null' : 'unknown[]'
}
// 处理object类型
if (itemType === 'object') {
return isNullable ? 'Record<string, unknown> | null' : 'Record<string, unknown>'
}
// 对于其他明确定义的类型,直接使用
return isNullable ? `${itemType} | null` : itemType
}
// 只有当type为unknown或未定义时才基于默认值进行类型推断
if (defaultValue !== null && defaultValue !== undefined) {
const valueType = typeof defaultValue
if (valueType === 'boolean' || valueType === 'string' || valueType === 'number') {
return valueType
}
if (Array.isArray(defaultValue)) {
return 'unknown[]'
}
if (valueType === 'object') {
return 'Record<string, unknown>'
}
}
return 'unknown | null'
}
generateTypeScriptCode(structure, preferencesData) {
const header = `/**
* Auto-generated preferences configuration
* Generated at: ${new Date().toISOString()}
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
* node v2-refactor-temp/tools/data-classify/scripts/generate-preferences.js
*
* ## Key Naming Convention
*
* All preference keys MUST follow the format: \`namespace.sub.key_name\`
*
* Rules:
* - At least 2 segments separated by dots (.)
* - Each segment uses lowercase letters, numbers, and underscores only
* - Pattern: /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/
*
* Examples:
* - 'app.user.avatar' (valid)
* - 'chat.multi_select_mode' (valid)
* - 'userAvatar' (invalid - missing dot separator)
* - 'App.user' (invalid - uppercase not allowed)
*
* This convention is enforced by ESLint rule: data-schema-key/valid-key
*
* === AUTO-GENERATED CONTENT START ===
*/
import { MEMORY_FACT_EXTRACTION_PROMPT, MEMORY_UPDATE_SYSTEM_PROMPT,TRANSLATE_PROMPT } from '@shared/config/prompts'
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
"typeLiterals": { "order": "alphabetically" }
}] */`
// 生成接口定义
const interfaceCode = this.generateInterface(structure)
// 生成默认值对象
const defaultsCode = this.generateDefaults(structure)
const footer = `
// === AUTO-GENERATED CONTENT END ===
/**
* 生成统计:
* - 总配置项: ${preferencesData.length}
* - electronStore项: ${preferencesData.filter((p) => p.source === 'electronStore').length}
* - redux项: ${preferencesData.filter((p) => p.source === 'redux').length}
* - localStorage项: ${preferencesData.filter((p) => p.source === 'localStorage').length}
*/`
return [header, interfaceCode, defaultsCode, footer].join('\n\n')
}
generateInterface(structure, depth = 0) {
const indent = ' '.repeat(depth)
if (depth === 0) {
// 顶层接口
let code = `export interface PreferenceSchemas {\n`
Object.keys(structure)
.sort()
.forEach((scope) => {
code += `${indent} ${scope}: {\n`
code += this.generateInterfaceProperties(structure[scope], depth + 2)
code += `${indent} }\n`
})
code += `}`
return code
}
}
generateInterfaceProperties(obj, depth) {
const indent = ' '.repeat(depth)
let code = ''
// 获取所有键并排序
const keys = Object.keys(obj).sort()
keys.forEach((key) => {
const value = obj[key]
if (value.type) {
// 叶子节点 - 实际的配置项直接使用targetKey
const comment = value.description ? `${indent}// ${value.description}\n` : ''
code += `${comment}${indent}'${key}': ${value.type}\n`
} else {
// 中间节点 - 嵌套对象
code += `${indent}'${key}': {\n`
code += this.generateInterfaceProperties(value, depth + 1)
code += `${indent}}\n`
}
})
return code
}
generateDefaults(structure) {
const header = `/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
export const DefaultPreferences: PreferenceSchemas = {`
let code = header + '\n'
Object.keys(structure)
.sort()
.forEach((scope) => {
code += ` ${scope}: {\n`
code += this.generateDefaultsProperties(structure[scope], 2)
code += ' }\n'
})
code += '}'
return code
}
generateDefaultsProperties(obj, depth) {
const indent = ' '.repeat(depth)
let code = ''
// 获取所有键并排序
const keys = Object.keys(obj).sort()
keys.forEach((key, index) => {
const value = obj[key]
const isLast = index === keys.length - 1
if (value.type) {
// 叶子节点 - 实际的配置项直接使用targetKey
const defaultVal = this.formatDefaultValue(value.defaultValue)
code += `${indent}'${key}': ${defaultVal}${isLast ? '' : ','}\n`
} else {
// 中间节点 - 嵌套对象
code += `${indent}'${key}': {\n`
code += this.generateDefaultsProperties(value, depth + 1)
code += `${indent}}${isLast ? '' : ','}\n`
}
})
return code
}
formatDefaultValue(value) {
if (value === null || value === undefined) {
return 'null'
}
if (typeof value === 'string') {
// Handle special "VALUE: xxxx" format - use xxxx directly without quotes
if (value.startsWith('VALUE: ')) {
return value.substring(7) // Remove "VALUE: " prefix and don't add quotes
}
return `'${value.replace(/'/g, "\\'")}'`
}
if (typeof value === 'boolean' || typeof value === 'number') {
return String(value)
}
if (Array.isArray(value)) {
return `[${value.map((item) => this.formatDefaultValue(item)).join(', ')}]`
}
if (typeof value === 'object') {
const entries = Object.entries(value).map(([k, v]) => `${k}: ${this.formatDefaultValue(v)}`)
return `{ ${entries.join(', ')} }`
}
return JSON.stringify(value)
}
writePreferencesFile(content) {
const targetDir = path.dirname(this.targetFile)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
fs.writeFileSync(this.targetFile, content, 'utf8')
}
printSummary(preferencesData) {
console.log(`\n生成摘要:`)
console.log(`- 总配置项: ${preferencesData.length}`)
console.log(`- electronStore项: ${preferencesData.filter((p) => p.source === 'electronStore').length}`)
console.log(`- redux项: ${preferencesData.filter((p) => p.source === 'redux').length}`)
console.log(`- localStorage项: ${preferencesData.filter((p) => p.source === 'localStorage').length}`)
console.log(`- 输出文件: ${this.targetFile}`)
// 显示一些示例targetKey
const sampleKeys = preferencesData
.slice(0, 5)
.map((p) => p.targetKey)
.filter(Boolean)
if (sampleKeys.length > 0) {
console.log(`\n示例配置键:`)
sampleKeys.forEach((key) => console.log(` - ${key}`))
}
}
}
// 主执行逻辑
if (require.main === module) {
try {
const generator = new PreferencesGenerator()
generator.generate()
} catch (error) {
console.error('生成失败:', error.message)
process.exit(1)
}
}
module.exports = PreferencesGenerator

View File

@ -0,0 +1,323 @@
/**
* Shared utilities for data classification tools
*
* This module provides common functions used across multiple scripts:
* - Loading and saving classification data
* - Extracting preferences data with deduplication
* - Traversing nested classification structures
*/
const fs = require('fs')
const path = require('path')
/**
* Default data directory path
*/
const DATA_DIR = path.resolve(__dirname, '../../data')
/**
* Load classification.json file
* @param {string} [dataDir] - Optional custom data directory
* @returns {Object} Parsed classification data
*/
function loadClassification(dataDir = DATA_DIR) {
const classificationFile = path.join(dataDir, 'classification.json')
if (!fs.existsSync(classificationFile)) {
throw new Error(`Classification file not found: ${classificationFile}`)
}
const content = fs.readFileSync(classificationFile, 'utf8')
return JSON.parse(content)
}
/**
* Save classification data to file with backup
* @param {Object} classification - Classification data to save
* @param {string} [dataDir] - Optional custom data directory
*/
function saveClassification(classification, dataDir = DATA_DIR) {
const classificationFile = path.join(dataDir, 'classification.json')
// Create backup if file exists
if (fs.existsSync(classificationFile)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const backupPath = path.join(dataDir, `classification.backup.${timestamp}.json`)
fs.copyFileSync(classificationFile, backupPath)
console.log(`Backup created: ${backupPath}`)
}
fs.writeFileSync(classificationFile, JSON.stringify(classification, null, 2), 'utf8')
console.log(`Classification saved: ${classificationFile}`)
}
/**
* Load inventory.json file
* @param {string} [dataDir] - Optional custom data directory
* @returns {Object} Parsed inventory data
*/
function loadInventory(dataDir = DATA_DIR) {
const inventoryFile = path.join(dataDir, 'inventory.json')
if (!fs.existsSync(inventoryFile)) {
throw new Error(`Inventory file not found: ${inventoryFile}`)
}
const content = fs.readFileSync(inventoryFile, 'utf8')
return JSON.parse(content)
}
/**
* Source priority for deduplication
* Higher number = higher priority
*/
const SOURCE_PRIORITY = {
redux: 3,
localStorage: 2,
electronStore: 1
}
/**
* Extract preferences data from classification with deduplication
* Supports nested structures with children
*
* @param {Object} classification - Classification data object
* @returns {Object} Object with categorized preferences data:
* - all: all deduplicated items
* - electronStore: items from electronStore
* - redux: items from redux
* - localStorage: items from localStorage
*/
function extractPreferencesData(classification) {
const allPreferencesData = []
const sources = ['electronStore', 'redux', 'localStorage']
// Recursive function to extract items including children
const extractItems = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
items.forEach((item) => {
// Handle items with children
if (item.children && Array.isArray(item.children)) {
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
// Handle regular items
if (item.category === 'preferences' && item.status === 'classified' && item.targetKey) {
allPreferencesData.push({
...item,
source,
sourceCategory: category,
originalKey: parentKey + item.originalKey,
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`
})
}
})
}
// Extract from all sources
sources.forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
// Handle duplicate targetKeys with priority-based selection
const targetKeyGroups = {}
allPreferencesData.forEach((item) => {
if (!targetKeyGroups[item.targetKey]) {
targetKeyGroups[item.targetKey] = []
}
targetKeyGroups[item.targetKey].push(item)
})
// Deduplicate using source priority
const deduplicatedData = []
Object.keys(targetKeyGroups).forEach((targetKey) => {
const items = targetKeyGroups[targetKey]
if (items.length > 1) {
// Sort by priority and select highest
items.sort((a, b) => SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source])
}
deduplicatedData.push(items[0])
})
// Group by source
const result = {
electronStore: [],
redux: [],
localStorage: [],
all: deduplicatedData
}
deduplicatedData.forEach((item) => {
if (result[item.source]) {
result[item.source].push(item)
}
})
return result
}
/**
* Traverse nested classification structure
* Calls callback for each item (including nested children)
*
* @param {Object} classifications - The classifications object
* @param {Function} callback - Function to call for each item: (item, source, category, fullKey) => void
*/
function traverseClassifications(classifications, callback) {
const traverse = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
for (const item of items) {
const fullKey = parentKey ? `${parentKey}.${item.originalKey}` : item.originalKey
// Call callback for this item
callback(item, source, category, fullKey)
// Recurse into children
if (item.children && Array.isArray(item.children)) {
traverse(item.children, source, category, fullKey)
}
}
}
// Iterate through all sources and categories
for (const [source, sourceData] of Object.entries(classifications)) {
if (typeof sourceData === 'object' && sourceData !== null) {
for (const [category, items] of Object.entries(sourceData)) {
if (Array.isArray(items)) {
traverse(items, source, category, '')
}
}
}
}
}
/**
* Get all flat keys from nested classification structure
* Used for comparison and validation
*
* @param {Object} classifications - The classifications object
* @returns {string[]} Array of full keys in format "source.category.field" or "source.category.parent.child"
*/
function getAllClassificationKeys(classifications) {
const keys = []
traverseClassifications(classifications, (item, source, category, fullKey) => {
// Build the complete key path
if (source === 'redux') {
keys.push(`${source}.${category}.${fullKey}`)
} else {
keys.push(`${source}.${fullKey}`)
}
})
return keys
}
/**
* Calculate statistics from nested classification structure
*
* @param {Object} classifications - The classifications object
* @returns {Object} Statistics object with counts by status and category
*/
function calculateStats(classifications) {
const stats = {
byStatus: {
pending: 0,
classified: 0,
'classified-deleted': 0
},
byCategory: {},
total: 0
}
traverseClassifications(classifications, (item) => {
stats.total++
// Count by status
if (item.status) {
stats.byStatus[item.status] = (stats.byStatus[item.status] || 0) + 1
}
// Count by category (only for classified items)
if (item.status === 'classified' && item.category) {
stats.byCategory[item.category] = (stats.byCategory[item.category] || 0) + 1
}
})
return stats
}
/**
* Normalize type string to standard format
*
* @param {string} type - Type string to normalize
* @returns {string} Normalized type string
*/
function normalizeType(type) {
if (!type || type === 'unknown') {
return 'unknown'
}
const cleanType = String(type).toLowerCase().trim()
// Handle union types - extract primary type
if (cleanType.includes('|')) {
const types = cleanType.split('|').map((t) => t.trim())
const nonNullTypes = types.filter((t) => t !== 'null' && t !== 'undefined')
if (nonNullTypes.length > 0) {
return normalizeType(nonNullTypes[0])
}
return normalizeType(types[0])
}
// Map to standard types
if (cleanType === 'string' || cleanType.includes('string')) return 'string'
if (cleanType === 'number' || cleanType.includes('number')) return 'number'
if (cleanType === 'boolean' || cleanType.includes('boolean')) return 'boolean'
if (cleanType === 'array' || cleanType.includes('[]')) return 'array'
if (cleanType === 'object' || cleanType.includes('object') || cleanType.includes('record')) return 'object'
if (cleanType === 'null') return 'null'
if (cleanType === 'undefined') return 'undefined'
if (cleanType.includes('entitytable') || cleanType === 'table') return 'table'
return 'unknown'
}
/**
* Infer type from a value
*
* @param {*} value - Value to infer type from
* @returns {string} Inferred type string
*/
function inferTypeFromValue(value) {
if (value === null) return 'null'
if (value === undefined) return 'undefined'
if (typeof value === 'boolean') return 'boolean'
if (typeof value === 'number') return 'number'
if (typeof value === 'string') return 'string'
if (Array.isArray(value)) return 'array'
if (typeof value === 'object') return 'object'
return 'unknown'
}
module.exports = {
DATA_DIR,
loadClassification,
saveClassification,
loadInventory,
extractPreferencesData,
traverseClassifications,
getAllClassificationKeys,
calculateStats,
normalizeType,
inferTypeFromValue,
SOURCE_PRIORITY
}

View File

@ -0,0 +1,293 @@
#!/usr/bin/env node
/**
* Data Consistency Validator
*
* Validates consistency between inventory.json and classification.json files.
* Supports nested classification structures with children arrays.
*
* Checks:
* - Missing classifications (data in inventory but not classified)
* - Orphaned classifications (classified items not in inventory)
* - Naming consistency (preferences should use dot-separated keys)
* - Duplicate target keys
*
* Usage:
* node v2-refactor-temp/tools/data-classify/scripts/validate-consistency.js
*/
const fs = require('fs')
const path = require('path')
const {
loadClassification,
loadInventory,
traverseClassifications,
calculateStats,
DATA_DIR
} = require('./lib/classificationUtils')
class DataValidator {
constructor() {
this.dataDir = DATA_DIR
}
/**
* Main validation entry point
*/
validate() {
console.log('Starting data consistency validation...\n')
let inventory, classification
try {
inventory = loadInventory(this.dataDir)
classification = loadClassification(this.dataDir)
} catch (error) {
console.error(`Failed to load data files: ${error.message}`)
process.exit(1)
}
const issues = []
// Run all consistency checks
issues.push(...this.checkMissingClassifications(inventory, classification))
issues.push(...this.checkOrphanedClassifications(inventory, classification))
issues.push(...this.checkNamingConsistency(classification))
issues.push(...this.checkDuplicateTargets(classification))
// Generate validation report
this.generateReport(issues, classification)
if (issues.length === 0) {
console.log('Validation passed, no issues found')
} else {
const errors = issues.filter((i) => i.severity === 'error')
const warnings = issues.filter((i) => i.severity === 'warning')
console.log(`Found ${errors.length} errors, ${warnings.length} warnings`)
if (errors.length > 0) {
process.exit(1)
}
}
}
/**
* Get all data keys from inventory
*/
getAllInventoryKeys(inventory) {
const keys = []
for (const [source, data] of Object.entries(inventory)) {
if (source === 'metadata') continue
for (const [moduleOrTable, moduleData] of Object.entries(data)) {
if (source === 'redux') {
// Redux: keys are source.module.field
for (const fieldName of Object.keys(moduleData)) {
if (fieldName === '_meta') continue
keys.push(`${source}.${moduleOrTable}.${fieldName}`)
}
} else {
// Other sources: keys are source.tableName
keys.push(`${source}.${moduleOrTable}`)
}
}
}
return keys
}
/**
* Get all data keys from classification (supports nested structure)
*/
getAllClassificationKeys(classification) {
const keys = []
traverseClassifications(classification.classifications, (item, source, category, fullKey) => {
// Skip deleted items
if (item.status === 'classified-deleted') return
// Build full key path
if (source === 'redux') {
keys.push(`${source}.${category}.${fullKey}`)
} else {
keys.push(`${source}.${fullKey}`)
}
})
return keys
}
/**
* Check for data items not yet classified
*/
checkMissingClassifications(inventory, classification) {
const issues = []
const inventoryKeys = this.getAllInventoryKeys(inventory)
const classificationKeys = new Set(this.getAllClassificationKeys(classification))
for (const key of inventoryKeys) {
if (!classificationKeys.has(key)) {
issues.push({
type: 'missing_classification',
severity: 'warning',
key: key,
message: `Data item "${key}" is not classified`
})
}
}
return issues
}
/**
* Check for classified items not in current inventory
*/
checkOrphanedClassifications(inventory, classification) {
const issues = []
const inventoryKeys = new Set(this.getAllInventoryKeys(inventory))
traverseClassifications(classification.classifications, (item, source, category, fullKey) => {
// Skip deleted items
if (item.status === 'classified-deleted') return
// Build full key path
let fullKeyPath
if (source === 'redux') {
fullKeyPath = `${source}.${category}.${fullKey}`
} else {
fullKeyPath = `${source}.${fullKey}`
}
if (!inventoryKeys.has(fullKeyPath)) {
issues.push({
type: 'orphaned_classification',
severity: 'warning',
key: fullKeyPath,
message: `Classified item "${fullKeyPath}" not found in current inventory`
})
}
})
return issues
}
/**
* Check naming consistency for preferences
*/
checkNamingConsistency(classification) {
const issues = []
traverseClassifications(classification.classifications, (item) => {
if (item.status !== 'classified') return
if (item.category !== 'preferences') return
// Preferences should have dot-separated targetKey
if (item.targetKey && !item.targetKey.includes('.')) {
issues.push({
type: 'naming_inconsistency',
severity: 'warning',
key: item.originalKey,
message: `Preference targetKey "${item.targetKey}" should use dot-separated hierarchy (e.g., "ui.theme")`
})
}
})
return issues
}
/**
* Check for duplicate target keys
*/
checkDuplicateTargets(classification) {
const issues = []
const targetKeyMap = {}
traverseClassifications(classification.classifications, (item, source, category, fullKey) => {
if (item.status === 'classified-deleted') return
const targetKey = item.targetKey || item.targetTable
if (!targetKey) return
const sourceKey = source === 'redux' ? `${source}.${category}.${fullKey}` : `${source}.${fullKey}`
if (targetKeyMap[targetKey]) {
issues.push({
type: 'duplicate_target',
severity: 'error',
key: sourceKey,
message: `Target key "${targetKey}" is used by both "${sourceKey}" and "${targetKeyMap[targetKey]}"`
})
} else {
targetKeyMap[targetKey] = sourceKey
}
})
return issues
}
/**
* Generate validation report markdown file
*/
generateReport(issues, classification) {
const reportPath = path.join(this.dataDir, '../validation-report.md')
const stats = calculateStats(classification.classifications)
let report = `# Data Validation Report\n\n`
report += `Generated: ${new Date().toISOString()}\n`
report += `Issues found: ${issues.length}\n\n`
if (issues.length === 0) {
report += `## Validation Passed\n\nNo consistency issues found.\n\n`
} else {
const errors = issues.filter((i) => i.severity === 'error')
const warnings = issues.filter((i) => i.severity === 'warning')
if (errors.length > 0) {
report += `## Errors (${errors.length})\n\n`
for (const issue of errors) {
report += `### ${issue.type}\n`
report += `- **Item**: \`${issue.key}\`\n`
report += `- **Issue**: ${issue.message}\n\n`
}
}
if (warnings.length > 0) {
report += `## Warnings (${warnings.length})\n\n`
for (const issue of warnings) {
report += `### ${issue.type}\n`
report += `- **Item**: \`${issue.key}\`\n`
report += `- **Issue**: ${issue.message}\n\n`
}
}
}
// Statistics section
report += `## Classification Statistics\n\n`
report += `### By Status\n\n`
report += `- **Pending**: ${stats.byStatus.pending || 0}\n`
report += `- **Classified**: ${stats.byStatus.classified || 0}\n`
report += `- **Deleted**: ${stats.byStatus['classified-deleted'] || 0}\n\n`
if (Object.keys(stats.byCategory).length > 0) {
report += `### By Category\n\n`
for (const [category, count] of Object.entries(stats.byCategory)) {
report += `- **${category}**: ${count}\n`
}
report += `\n`
}
report += `### Total Items: ${stats.total}\n`
fs.writeFileSync(reportPath, report, 'utf8')
console.log(`Validation report saved: ${reportPath}`)
}
}
// Run script
if (require.main === module) {
const validator = new DataValidator()
validator.validate()
}
module.exports = DataValidator

View File

@ -0,0 +1,239 @@
#!/usr/bin/env node
/**
* Generated Code Quality Validator
*
* Validates the quality and correctness of auto-generated code files.
* Checks generated preferences schema and migration mapping files.
*
* Validates:
* - preferenceSchemas.ts - Interface definitions and default values
* - PreferencesMappings.ts - Source to target key mappings
*
* Usage:
* node v2-refactor-temp/tools/data-classify/scripts/validate-generation.js
*/
const fs = require('fs')
const path = require('path')
class GenerationValidator {
constructor() {
this.projectRoot = path.resolve(__dirname, '../../../../')
// Updated paths to match actual project structure
this.preferencesFile = path.join(this.projectRoot, 'packages/shared/data/preference/preferenceSchemas.ts')
this.mappingsFile = path.join(
this.projectRoot,
'src/main/data/migration/v2/migrators/mappings/PreferencesMappings.ts'
)
this.results = {
preferences: { valid: false, errors: [], warnings: [] },
mappings: { valid: false, errors: [], warnings: [] }
}
}
/**
* Main validation entry point
*/
validate() {
console.log('Validating generated code quality...\n')
this.validatePreferencesFile()
this.validateMappingsFile()
this.printSummary()
return this.results
}
/**
* Validate preferenceSchemas.ts file
*/
validatePreferencesFile() {
console.log('Validating preferenceSchemas.ts...')
if (!fs.existsSync(this.preferencesFile)) {
this.results.preferences.errors.push(`File not found: ${this.preferencesFile}`)
return
}
try {
const content = fs.readFileSync(this.preferencesFile, 'utf8')
// Check for auto-generation marker
if (!content.includes('AUTO-GENERATED CONTENT START')) {
this.results.preferences.errors.push('Missing auto-generated content marker')
}
// Check for interface definition (updated name)
if (!content.includes('export interface PreferenceSchemas')) {
this.results.preferences.errors.push('Missing PreferenceSchemas interface definition')
}
// Check for ESLint configuration
if (!content.includes('/* eslint')) {
this.results.preferences.warnings.push('Missing ESLint configuration comment')
}
// Check for imports
if (!content.includes("from '@shared/data/preference/preferenceTypes'")) {
this.results.preferences.warnings.push('Missing preferenceTypes import')
}
// Count preference keys
const keyMatches = content.match(/'[\w.]+'/g)
if (keyMatches) {
const keyCount = keyMatches.length
console.log(` Found ${keyCount} preference keys`)
if (keyCount < 50) {
this.results.preferences.warnings.push(`Low preference key count: ${keyCount} (expected 100+)`)
}
} else {
this.results.preferences.warnings.push('Could not parse preference key count')
}
// Check key naming convention
const invalidKeys = []
const keyPattern = /^\s*'([^']+)':/gm
let match
while ((match = keyPattern.exec(content)) !== null) {
const key = match[1]
// Keys should be dot-separated lowercase
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(key)) {
invalidKeys.push(key)
}
}
if (invalidKeys.length > 0 && invalidKeys.length <= 5) {
this.results.preferences.warnings.push(
`Found ${invalidKeys.length} keys not matching naming convention: ${invalidKeys.slice(0, 3).join(', ')}...`
)
} else if (invalidKeys.length > 5) {
this.results.preferences.warnings.push(`Found ${invalidKeys.length} keys not matching naming convention`)
}
if (this.results.preferences.errors.length === 0) {
this.results.preferences.valid = true
console.log(' preferenceSchemas.ts validation passed')
}
} catch (error) {
this.results.preferences.errors.push(`Failed to read file: ${error.message}`)
}
}
/**
* Validate PreferencesMappings.ts file
*/
validateMappingsFile() {
console.log('\nValidating PreferencesMappings.ts...')
if (!fs.existsSync(this.mappingsFile)) {
this.results.mappings.errors.push(`File not found: ${this.mappingsFile}`)
return
}
try {
const content = fs.readFileSync(this.mappingsFile, 'utf8')
// Check for auto-generation marker
if (!content.includes('AUTO-GENERATED CONTENT')) {
this.results.mappings.warnings.push('Missing auto-generated content marker')
}
// Check for ELECTRON_STORE_MAPPINGS export
if (!content.includes('export const ELECTRON_STORE_MAPPINGS')) {
this.results.mappings.errors.push('Missing ELECTRON_STORE_MAPPINGS export')
}
// Check for REDUX_STORE_MAPPINGS export
if (!content.includes('export const REDUX_STORE_MAPPINGS')) {
this.results.mappings.errors.push('Missing REDUX_STORE_MAPPINGS export')
}
// Check for as const assertion for type safety
if (!content.includes('as const')) {
this.results.mappings.warnings.push('Missing "as const" assertion for type safety')
}
// Count mapping entries
const originalKeyMatches = content.match(/originalKey:/g)
if (originalKeyMatches) {
const mappingCount = originalKeyMatches.length
console.log(` Found ${mappingCount} mapping entries`)
if (mappingCount < 10) {
this.results.mappings.warnings.push(`Low mapping count: ${mappingCount} (expected 50+)`)
}
}
// Check for valid mapping structure
const mappingPattern = /\{\s*originalKey:\s*'[^']+',\s*targetKey:\s*'[^']+'\s*\}/g
const validMappings = content.match(mappingPattern)
if (!validMappings || validMappings.length === 0) {
this.results.mappings.warnings.push('Could not find valid mapping structures')
}
if (this.results.mappings.errors.length === 0) {
this.results.mappings.valid = true
console.log(' PreferencesMappings.ts validation passed')
}
} catch (error) {
this.results.mappings.errors.push(`Failed to read file: ${error.message}`)
}
}
/**
* Print validation summary
*/
printSummary() {
console.log('\n' + '='.repeat(50))
console.log('Validation Summary')
console.log('='.repeat(50))
// Preferences results
console.log(`\npreferenceSchemas.ts: ${this.results.preferences.valid ? 'PASSED' : 'FAILED'}`)
for (const error of this.results.preferences.errors) {
console.log(` ERROR: ${error}`)
}
for (const warning of this.results.preferences.warnings) {
console.log(` WARNING: ${warning}`)
}
// Mappings results
console.log(`\nPreferencesMappings.ts: ${this.results.mappings.valid ? 'PASSED' : 'FAILED'}`)
for (const error of this.results.mappings.errors) {
console.log(` ERROR: ${error}`)
}
for (const warning of this.results.mappings.warnings) {
console.log(` WARNING: ${warning}`)
}
// Overall result
const overallValid = this.results.preferences.valid && this.results.mappings.valid
const totalErrors = this.results.preferences.errors.length + this.results.mappings.errors.length
const totalWarnings = this.results.preferences.warnings.length + this.results.mappings.warnings.length
console.log('\n' + '='.repeat(50))
console.log(`Overall: ${overallValid ? 'PASSED' : 'FAILED'}`)
console.log(`Errors: ${totalErrors}, Warnings: ${totalWarnings}`)
if (overallValid) {
console.log('\nGenerated code quality is acceptable!')
} else {
console.log('\nPlease fix the errors and regenerate the code.')
}
}
}
// Run script
if (require.main === module) {
const validator = new GenerationValidator()
const results = validator.validate()
const hasErrors = results.preferences.errors.length > 0 || results.mappings.errors.length > 0
process.exit(hasErrors ? 1 : 0)
}
module.exports = GenerationValidator