mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
- 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.
402 lines
13 KiB
JavaScript
402 lines
13 KiB
JavaScript
#!/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
|