chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
/**
* 应用配置持久化。
*
* - 落盘位置:userData/config.json
* - API Key 优先用 Electron safeStorage 加密(Windows 走 DPAPI),
* 若系统不支持则降级为明文并在 UI 上如实标注。
* - 渲染进程只能拿到 GatewayConfigView(无明文 Key)。
*/
import { app, safeStorage } from 'electron'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import type { GatewayConfig, GatewayConfigView } from '@shared/types'
interface StoredConfig {
speechBaseUrl?: string
speechModel?: string
baseUrl: string
model: string
temperature: number
maxToolRounds: number
apiKeyEnc?: string
apiKeyPlain?: string
workspaceRoot?: string
}
const DEFAULTS: StoredConfig = {
baseUrl: 'https://api.deepseek.com/v1',
model: 'deepseek-chat',
temperature: 0.7,
maxToolRounds: 6
}
let cache: StoredConfig | null = null
function filePath(): string {
return join(app.getPath('userData'), 'config.json')
}
async function load(): Promise<StoredConfig> {
if (cache) return cache
try {
const raw = await fs.readFile(filePath(), 'utf8')
cache = { ...DEFAULTS, ...(JSON.parse(raw) as Partial<StoredConfig>) }
} catch {
cache = { ...DEFAULTS }
}
return cache
}
async function persist(next: StoredConfig): Promise<void> {
cache = next
const file = filePath()
await fs.mkdir(join(file, '..'), { recursive: true })
await fs.writeFile(file, JSON.stringify(next, null, 2), 'utf8')
}
function decryptKey(stored: StoredConfig): { key: string; storage: 'encrypted' | 'plaintext' | 'none' } {
if (stored.apiKeyEnc) {
try {
return { key: safeStorage.decryptString(Buffer.from(stored.apiKeyEnc, 'base64')), storage: 'encrypted' }
} catch {
return { key: '', storage: 'none' }
}
}
if (stored.apiKeyPlain) return { key: stored.apiKeyPlain, storage: 'plaintext' }
return { key: '', storage: 'none' }
}
/** 供主进程内部使用:拿到可直接请求上游的完整配置 */
export async function getGatewayConfig(): Promise<GatewayConfig> {
const stored = await load()
const { key } = decryptKey(stored)
return {
speechBaseUrl: stored.speechBaseUrl,
speechModel: stored.speechModel,
baseUrl: stored.baseUrl,
model: stored.model,
temperature: stored.temperature,
maxToolRounds: stored.maxToolRounds,
apiKey: key
}
}
export async function getWorkspaceRoot(): Promise<string> {
const stored = await load()
return stored.workspaceRoot || join(app.getPath('home'), 'YCodeWorkspace')
}
export async function setWorkspaceRoot(root: string): Promise<void> {
const stored = await load()
await persist({ ...stored, workspaceRoot: root })
}
/** 供渲染进程使用:脱敏视图 */
export async function getConfigView(): Promise<GatewayConfigView> {
const stored = await load()
const { key, storage } = decryptKey(stored)
return {
speechBaseUrl: stored.speechBaseUrl,
speechModel: stored.speechModel,
baseUrl: stored.baseUrl,
model: stored.model,
temperature: stored.temperature,
maxToolRounds: stored.maxToolRounds,
hasApiKey: key.length > 0,
keyStorage: key.length > 0 ? storage : 'none'
}
}
export interface ConfigPatch {
speechBaseUrl?: string
speechModel?: string
baseUrl?: string
model?: string
temperature?: number
maxToolRounds?: number
/** 传空字符串表示清除 Key;不传表示保持原值 */
apiKey?: string
}
export async function updateConfig(patch: ConfigPatch): Promise<GatewayConfigView> {
const stored = await load()
const next: StoredConfig = { ...stored }
if (patch.speechBaseUrl !== undefined) next.speechBaseUrl = patch.speechBaseUrl.trim()
if (patch.speechModel !== undefined) next.speechModel = patch.speechModel.trim()
if (patch.baseUrl !== undefined) next.baseUrl = patch.baseUrl.trim()
if (patch.model !== undefined) next.model = patch.model.trim()
if (patch.temperature !== undefined) next.temperature = clamp(patch.temperature, 0, 2)
if (patch.maxToolRounds !== undefined) next.maxToolRounds = clamp(Math.round(patch.maxToolRounds), 1, 24)
if (patch.apiKey !== undefined) {
const key = patch.apiKey.trim()
if (!key) {
delete next.apiKeyEnc
delete next.apiKeyPlain
} else if (safeStorage.isEncryptionAvailable()) {
next.apiKeyEnc = safeStorage.encryptString(key).toString('base64')
delete next.apiKeyPlain
} else {
next.apiKeyPlain = key
delete next.apiKeyEnc
}
}
await persist(next)
return getConfigView()
}
function clamp(value: number, min: number, max: number): number {
if (Number.isNaN(value)) return min
return Math.min(max, Math.max(min, value))
}