chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -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))
|
||||
}
|
||||
Reference in new issue
Block a user