mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Add complete server-side implementation for the Claude Code Desktop App UI: - REST API: sessions, conversations, settings, models, scheduled tasks, search, agents, and status endpoints (9 modules, 30+ endpoints) - WebSocket: real-time chat streaming with state transitions, ping/pong, permission request forwarding, and stop generation support - Services: sessionService (JSONL read/write, CLI-compatible), settingsService (atomic writes), cronService, searchService (ripgrep), agentService (YAML management) - Middleware: CORS (localhost-only), auth, unified error handling - Tests: 180 tests (unit + E2E + business flow), all passing - Docs: PRD, UI design spec, server architecture design Non-invasive: all new code under src/server/, no changes to existing CLI code. CLI/UI data interop: reads/writes the same JSONL/JSON files as the CLI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
/**
|
||
* 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<Record<string, unknown>> {
|
||
try {
|
||
const raw = await fs.readFile(filePath, 'utf-8')
|
||
return JSON.parse(raw) as Record<string, unknown>
|
||
} 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<Record<string, unknown>> {
|
||
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<Record<string, unknown>> {
|
||
return this.readJsonFile(this.getUserSettingsPath())
|
||
}
|
||
|
||
/** 获取项目级设置 */
|
||
async getProjectSettings(projectRoot?: string): Promise<Record<string, unknown>> {
|
||
return this.readJsonFile(this.getProjectSettingsPath(projectRoot))
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 写入(原子写入:先写临时文件,再 rename)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 原子写入 JSON 文件 */
|
||
private async writeJsonFile(
|
||
filePath: string,
|
||
data: Record<string, unknown>,
|
||
): Promise<void> {
|
||
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<string, unknown>): Promise<void> {
|
||
const current = await this.getUserSettings()
|
||
const merged = Object.assign({}, current, settings)
|
||
await this.writeJsonFile(this.getUserSettingsPath(), merged)
|
||
}
|
||
|
||
/** 更新项目级设置(浅合并) */
|
||
async updateProjectSettings(
|
||
settings: Record<string, unknown>,
|
||
projectRoot?: string,
|
||
): Promise<void> {
|
||
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<string> {
|
||
const settings = await this.getUserSettings()
|
||
return (settings.defaultMode as string) || 'default'
|
||
}
|
||
|
||
/** 设置权限模式 */
|
||
async setPermissionMode(mode: string): Promise<void> {
|
||
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 })
|
||
}
|
||
}
|