/** * Settings Service — 读写用户级和项目级设置文件 * * 设置文件为 JSON 格式: * - 用户级: ~/.claude/settings.json * - 项目级: {projectRoot}/.claude/settings.json * * 合并策略:Object.assign({}, userSettings, projectSettings) */ import * as fs from 'fs/promises' import * as path from 'path' import * as os from 'os' import { ApiError } from '../middleware/errorHandler.js' const VALID_PERMISSION_MODES = [ 'default', 'acceptEdits', 'plan', 'bypassPermissions', 'dontAsk', ] as const export type PermissionMode = (typeof VALID_PERMISSION_MODES)[number] export class SettingsService { private projectRoot?: string constructor(projectRoot?: string) { this.projectRoot = projectRoot } /** 配置目录,支持通过环境变量覆盖(便于测试) */ private getConfigDir(): string { return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') } /** 用户级设置文件路径 */ private getUserSettingsPath(): string { return path.join(this.getConfigDir(), 'settings.json') } /** 项目级设置文件路径 */ private getProjectSettingsPath(projectRoot?: string): string { const root = projectRoot || this.projectRoot if (!root) { throw ApiError.badRequest('Project root is required for project settings') } return path.join(root, '.claude', 'settings.json') } // --------------------------------------------------------------------------- // 读取 // --------------------------------------------------------------------------- /** 安全读取 JSON 文件,文件不存在时返回空对象 */ private async readJsonFile(filePath: string): Promise> { try { const raw = await fs.readFile(filePath, 'utf-8') return JSON.parse(raw) as Record } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return {} } throw ApiError.internal(`Failed to read settings from ${filePath}: ${err}`) } } /** 获取合并后的设置(user + project) */ async getSettings(projectRoot?: string): Promise> { const user = await this.getUserSettings() try { const project = await this.getProjectSettings(projectRoot) return Object.assign({}, user, project) } catch { // project root 未指定时,仅返回 user settings return user } } /** 获取用户级设置 */ async getUserSettings(): Promise> { return this.readJsonFile(this.getUserSettingsPath()) } /** 获取项目级设置 */ async getProjectSettings(projectRoot?: string): Promise> { return this.readJsonFile(this.getProjectSettingsPath(projectRoot)) } // --------------------------------------------------------------------------- // 写入(原子写入:先写临时文件,再 rename) // --------------------------------------------------------------------------- /** 原子写入 JSON 文件 */ private async writeJsonFile( filePath: string, data: Record, ): Promise { const dir = path.dirname(filePath) await fs.mkdir(dir, { recursive: true }) const tmpFile = `${filePath}.tmp.${Date.now()}` try { await fs.writeFile(tmpFile, JSON.stringify(data, null, 2) + '\n', 'utf-8') await fs.rename(tmpFile, filePath) } catch (err) { // 清理临时文件(best-effort) await fs.unlink(tmpFile).catch(() => {}) throw ApiError.internal(`Failed to write settings to ${filePath}: ${err}`) } } /** 更新用户级设置(浅合并) */ async updateUserSettings(settings: Record): Promise { const current = await this.getUserSettings() const merged = Object.assign({}, current, settings) await this.writeJsonFile(this.getUserSettingsPath(), merged) } /** 更新项目级设置(浅合并) */ async updateProjectSettings( settings: Record, projectRoot?: string, ): Promise { const filePath = this.getProjectSettingsPath(projectRoot) const current = await this.readJsonFile(filePath) const merged = Object.assign({}, current, settings) await this.writeJsonFile(filePath, merged) } // --------------------------------------------------------------------------- // 权限模式 // --------------------------------------------------------------------------- /** 获取当前权限模式 */ async getPermissionMode(): Promise { const settings = await this.getUserSettings() return (settings.defaultMode as string) || 'default' } /** 设置权限模式 */ async setPermissionMode(mode: string): Promise { if (!VALID_PERMISSION_MODES.includes(mode as PermissionMode)) { throw ApiError.badRequest( `Invalid permission mode: "${mode}". Valid modes: ${VALID_PERMISSION_MODES.join(', ')}`, ) } await this.updateUserSettings({ defaultMode: mode }) } }