mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
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:
parent
143b4c46c8
commit
806a294508
@ -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
|
||||
*
|
||||
|
||||
@ -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)
|
||||
|
||||
40
v2-refactor-temp/README.md
Normal file
40
v2-refactor-temp/README.md
Normal 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` 和相关引用
|
||||
1
v2-refactor-temp/tools/data-classify/.gitignore
vendored
Normal file
1
v2-refactor-temp/tools/data-classify/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.backup.*
|
||||
510
v2-refactor-temp/tools/data-classify/README.md
Normal file
510
v2-refactor-temp/tools/data-classify/README.md
Normal 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 优先级最高)
|
||||
3409
v2-refactor-temp/tools/data-classify/data/classification.json
Normal file
3409
v2-refactor-temp/tools/data-classify/data/classification.json
Normal file
File diff suppressed because it is too large
Load Diff
1566
v2-refactor-temp/tools/data-classify/data/inventory.json
Normal file
1566
v2-refactor-temp/tools/data-classify/data/inventory.json
Normal file
File diff suppressed because it is too large
Load Diff
490
v2-refactor-temp/tools/data-classify/package-lock.json
generated
Normal file
490
v2-refactor-temp/tools/data-classify/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
v2-refactor-temp/tools/data-classify/package.json
Normal file
30
v2-refactor-temp/tools/data-classify/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
43
v2-refactor-temp/tools/data-classify/scripts/generate-all.js
Normal file
43
v2-refactor-temp/tools/data-classify/scripts/generate-all.js
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user