mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
Dissolve the by-kind @shared/config junk drawer per shared-layer governance: route each member by shape and actual consumer process — cross-process slices into types//utils//ai/, single-process code back into main/renderer (Invariant 1.1). Confirm each item's real consumer process rather than trusting the directional plan (API_SERVER_DEFAULTS is renderer-only, MIN_WINDOW_* is cross-process, providers.ts is renderer-only), and drop dead consts (ZOOM_LEVELS/ZOOM_OPTIONS, bookExts, thirdPartyApplicationExts). Purge runtime logic from types/ so the bucket holds only declarations: move serializeError + AI-SDK error guards to utils/error.ts, the FileHandle factories/guards to utils/file/handle.ts, isSerializable + SerializableSchema to utils/serializable.ts, and the tab-instance guard/normalizer to utils/tabInstanceMetadata.ts. Tests follow the logic to utils/__tests__; the type-level ipc contract test is retired (its invariants kept as a breadcrumb for the future IpcApi Zod schema). types/ is now logic-free and test-free. Also: remove the dead @shared mock from the packages/ui code-editor test so packages/ui no longer references production code; fix the data-classify preference generator prompts import and the update-languages output path to the relocated modules. Update shared-layer-architecture (3.1 type/util test rule, 5/6 config dissolution) and renderer-architecture cross-references.
489 lines
16 KiB
JavaScript
489 lines
16 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, '../../../../src/shared/data/preference/preferenceSchemas.ts')
|
||
this.classificationFile = path.join(this.dataDir, 'classification.json')
|
||
this.targetKeyDefinitionsFile = path.join(this.dataDir, 'target-key-definitions.json')
|
||
}
|
||
|
||
generate() {
|
||
console.log('开始生成 preferences.ts...')
|
||
|
||
// 读取分类数据
|
||
const classification = this.loadClassification()
|
||
|
||
// 提取preferences相关数据
|
||
const classificationData = this.extractPreferencesData(classification)
|
||
|
||
// 读取target-key-definitions.json(复杂映射的target key定义)
|
||
const targetKeyDefinitions = this.loadTargetKeyDefinitions()
|
||
|
||
// 合并数据:target-key-definitions 覆盖 classification
|
||
const preferencesData = this.mergeDataSources(classificationData, targetKeyDefinitions)
|
||
|
||
// 构建类型结构
|
||
const typeStructure = this.buildTypeStructure(preferencesData)
|
||
|
||
// 生成TypeScript代码
|
||
const content = this.generateTypeScriptCode(typeStructure, preferencesData)
|
||
|
||
// 写入文件
|
||
this.writePreferencesFile(content)
|
||
|
||
console.log('preferences.ts 生成完成!')
|
||
this.printSummary(preferencesData)
|
||
}
|
||
|
||
/**
|
||
* Load target-key-definitions.json for complex mapping target keys
|
||
* These definitions override or extend classification.json
|
||
*/
|
||
loadTargetKeyDefinitions() {
|
||
if (!fs.existsSync(this.targetKeyDefinitionsFile)) {
|
||
console.log('target-key-definitions.json 不存在,跳过')
|
||
return { definitions: [] }
|
||
}
|
||
|
||
const content = fs.readFileSync(this.targetKeyDefinitionsFile, 'utf8')
|
||
const data = JSON.parse(content)
|
||
console.log(`读取 target-key-definitions.json: ${data.definitions?.length || 0} 项定义`)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Merge classification data with target-key-definitions
|
||
* Target-key-definitions take priority (can override or disable keys)
|
||
*/
|
||
mergeDataSources(classificationData, targetKeyDefinitions) {
|
||
// Use Map to deduplicate by targetKey, definitions take priority
|
||
const targetKeyMap = new Map()
|
||
|
||
// First add classification data
|
||
for (const item of classificationData) {
|
||
if (item.targetKey) {
|
||
targetKeyMap.set(item.targetKey, {
|
||
...item,
|
||
_source: 'classification'
|
||
})
|
||
}
|
||
}
|
||
|
||
const definitionsCount = {
|
||
added: 0,
|
||
overridden: 0,
|
||
disabled: 0
|
||
}
|
||
|
||
// Then process target-key-definitions (override or disable)
|
||
for (const def of targetKeyDefinitions.definitions || []) {
|
||
if (def.status === 'classified') {
|
||
const existed = targetKeyMap.has(def.targetKey)
|
||
targetKeyMap.set(def.targetKey, {
|
||
targetKey: def.targetKey,
|
||
type: def.type,
|
||
defaultValue: def.defaultValue,
|
||
source: 'target-key-definitions',
|
||
sourceCategory: def.source || 'complex',
|
||
originalKey: def.source || 'complex',
|
||
fullPath: `target-key-definitions/${def.targetKey}`,
|
||
_source: 'target-key-definitions'
|
||
})
|
||
if (existed) {
|
||
definitionsCount.overridden++
|
||
console.log(` 覆盖: ${def.targetKey}`)
|
||
} else {
|
||
definitionsCount.added++
|
||
console.log(` 新增: ${def.targetKey}`)
|
||
}
|
||
} else if (def.status === 'pending' && targetKeyMap.has(def.targetKey)) {
|
||
// status: pending can disable keys from classification
|
||
targetKeyMap.delete(def.targetKey)
|
||
definitionsCount.disabled++
|
||
console.log(` 禁用: ${def.targetKey}`)
|
||
}
|
||
}
|
||
|
||
if (definitionsCount.added + definitionsCount.overridden + definitionsCount.disabled > 0) {
|
||
console.log(
|
||
`target-key-definitions 处理完成: 新增 ${definitionsCount.added}, 覆盖 ${definitionsCount.overridden}, 禁用 ${definitionsCount.disabled}`
|
||
)
|
||
}
|
||
|
||
return Array.from(targetKeyMap.values())
|
||
}
|
||
|
||
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', 'dexieSettings']
|
||
|
||
// 递归提取项目,包括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 > dexieSettings > localStorage > electronStore优先级选择
|
||
const sourcePriority = { redux: 4, dexieSettings: 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 { TRANSLATE_PROMPT } from '@shared/ai/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}
|
||
* - dexieSettings项: ${preferencesData.filter((p) => p.source === 'dexieSettings').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(`- dexieSettings项: ${preferencesData.filter((p) => p.source === 'dexieSettings').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
|