import { promises as fs } from 'node:fs' import { dirname, isAbsolute, relative, resolve, sep, join } from 'node:path' import { randomUUID } from 'node:crypto' import type { RevertChange } from '@shared/revert' import { getRevertChanges } from '@shared/revert' import type { ChatMessage } from '@shared/types' async function replaceFile(path: string, content: string | null): Promise { if (content === null) { await fs.unlink(path); return } const temporary = join(dirname(path), `.ycode-restore-${randomUUID()}.tmp`) try { await fs.writeFile(temporary, content, 'utf8'); await fs.rename(temporary, path) } finally { await fs.unlink(temporary).catch(() => undefined) } } /** Validate every current file before applying any change; roll back if a write fails. */ export async function restoreWorkspaceFiles(workspace: string, changes: RevertChange[]): Promise<() => Promise> { const root = await fs.realpath(workspace) const validated: RevertChange[] = [] for (const change of changes) { const path = await fs.realpath(resolve(change.path)) const rel = relative(root, path) if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error('恢复文件不在当前工作区内') if (await fs.readFile(path, 'utf8') !== change.after) throw new Error(`文件已被外部修改,未执行恢复:${rel}`) validated.push({ ...change, path }) } const applied: RevertChange[] = [] const undo = async (): Promise => { for (const change of [...applied].reverse()) await replaceFile(change.path, change.after) } try { for (const change of validated) { if (await fs.readFile(change.path, 'utf8') !== change.after) throw new Error('文件在恢复前已变化') await replaceFile(change.path, change.before) applied.push(change) } } catch (error) { await undo(); throw error } return undo } /** * 编辑提交前的产物回退(原版 userMessage.edit.hint:产物回退至此消息前版本)。 * 返回撤销函数;该消息之后没有可回退的写入时返回 undefined。 * 记录不完整时由 getRevertChanges 抛错,调用方应中止提交而不是静默保留文件修改。 */ export async function rollbackEditedArtifacts(workspace: string, messages: ChatMessage[], userMessageId: string): Promise<(() => Promise) | undefined> { const changes = getRevertChanges(messages, userMessageId) if (!changes.length) return undefined return restoreWorkspaceFiles(workspace, changes) }