反馈改右上角轻提示;侧栏加撤回重做等配置动作
设置页的横幅换成 toast(右滑入、几秒后上移淡出,web/toast.ts 单例): 动作结果看一眼就行,不值得在页面里占一块常驻位置。表单校验类的错误 仍留在各自表单里。 撤回栈就是备份链:撤回把最新备份应用回去并消费掉它,撤回前的状态进 重做栈(.patchbay-redo,同样留 10 份);重做反向;新的正常保存清空 重做栈,不留分叉历史。这两条路复用 applyConfig 的保护链路,用 restoreFrom 参数跳过自建备份——否则备份链会被撤回本身污染,撤两次 等于原地打转。侧栏左下角一排图标:撤回、重做、历史记录(备份列表 弹窗)、更新配置(POST /api/kernel/reload,给 SSH 手改后用)、重启 内核;收拢时容器变窄自动折成一列。 设置页顺手修三处:GEO 行的编辑按钮在输入框开着时再点会闪(blur 关了 click 又开),mousedown.prevent 保焦点、click 显式开关;上传按钮改 「选择或拖放文件」,投放区拖入高亮;恢复策略挪到恢复行下面。
This commit is contained in:
+102
-2
@@ -106,11 +106,16 @@ export function rewriteYaml(currentYaml: string, nextData: ConfigObject): string
|
||||
*
|
||||
* `baseRevision` 是客户端读到这份配置时拿到的指纹。传 undefined 表示
|
||||
* 「不管别人改没改,以我为准」,仅用于还原备份这类明确要覆盖的场景。
|
||||
*
|
||||
* `opts.restoreFrom`:撤回/重做专用。给了它就不再新建备份(否则备份链会被
|
||||
* 撤回本身污染,撤两次等于原地打转),重载失败时用它当还原源;同时不清空
|
||||
* 重做栈——只有「正常的新改动」才让已撤回的未来失效。
|
||||
*/
|
||||
export async function applyConfig(
|
||||
env: Env,
|
||||
nextData: ConfigObject,
|
||||
baseRevision?: string,
|
||||
opts?: { restoreFrom?: string },
|
||||
): Promise<ApplyOutcome> {
|
||||
const current = await readDocument(env)
|
||||
|
||||
@@ -185,8 +190,8 @@ export async function applyConfig(
|
||||
}
|
||||
|
||||
// 校验过了才动真文件。备份放在写入之前。
|
||||
const backupPath = await createBackup(env)
|
||||
const backupTiming = lap('备份')
|
||||
const backupPath = opts?.restoreFrom ?? (await createBackup(env))
|
||||
const backupTiming = opts?.restoreFrom ? '' : lap('备份')
|
||||
try {
|
||||
await writeFile(env.configPath, nextYaml, 'utf8')
|
||||
} catch (error) {
|
||||
@@ -204,6 +209,7 @@ export async function applyConfig(
|
||||
if (!kernel.online) {
|
||||
// mihomo -t 已经把过关,文件留给内核下次启动时读。
|
||||
await pruneBackups(env)
|
||||
if (!opts?.restoreFrom) await clearRedo(env)
|
||||
console.log(
|
||||
`[apply] 共 ${String(Date.now() - clock.start)}ms(${checkTiming},${backupTiming},${writeTiming},未重载)`,
|
||||
)
|
||||
@@ -240,6 +246,7 @@ export async function applyConfig(
|
||||
}
|
||||
|
||||
await pruneBackups(env)
|
||||
if (!opts?.restoreFrom) await clearRedo(env)
|
||||
const timings = [checkTiming, backupTiming, writeTiming, reloadTiming].filter(Boolean).join(',')
|
||||
console.log(`[apply] 共 ${String(Date.now() - clock.start)}ms(${timings})`)
|
||||
return {
|
||||
@@ -343,6 +350,99 @@ export async function importConfig(
|
||||
return applyConfig(env, merged)
|
||||
}
|
||||
|
||||
/* ── 撤回 / 重做 ──────────────────────────────────
|
||||
*
|
||||
* 撤回栈就是备份链:每次成功落盘都留了「改动前」的备份。撤回把最新备份应用
|
||||
* 回去并**消费掉它**(从链里删除),同时把撤回前的状态推进重做栈;重做反向。
|
||||
* 任何正常的新改动清空重做栈——和编辑器撤回的通行语义一致,不留分叉历史。
|
||||
*/
|
||||
|
||||
async function redoDir(env: Env): Promise<string> {
|
||||
const dir = join(dirname(env.configPath), '.patchbay-redo')
|
||||
await mkdir(dir, { recursive: true })
|
||||
return dir
|
||||
}
|
||||
|
||||
/** 重做条目按名字排序就是按时间排序。带毫秒:连续快速撤回不能互相覆盖。 */
|
||||
function redoName(configPath: string, at: Date): string {
|
||||
const p = (n: number, w = 2) => String(n).padStart(w, '0')
|
||||
const stamp =
|
||||
`${String(at.getFullYear())}${p(at.getMonth() + 1)}${p(at.getDate())}` +
|
||||
`-${p(at.getHours())}${p(at.getMinutes())}${p(at.getSeconds())}${p(at.getMilliseconds(), 3)}`
|
||||
return `${basename(configPath)}.redo-${stamp}`
|
||||
}
|
||||
|
||||
/** 新的在前,同 listBackups。 */
|
||||
export async function listRedo(env: Env): Promise<string[]> {
|
||||
const dir = await redoDir(env)
|
||||
const prefix = `${basename(env.configPath)}.redo-`
|
||||
const entries = await readdir(dir)
|
||||
return entries.filter((name) => name.startsWith(prefix)).sort().reverse()
|
||||
}
|
||||
|
||||
async function clearRedo(env: Env): Promise<void> {
|
||||
const dir = await redoDir(env)
|
||||
const entries = await listRedo(env)
|
||||
await Promise.all(entries.map((name) => unlink(join(dir, name)).catch(() => {})))
|
||||
}
|
||||
|
||||
/** 当前文件进重做栈,返回落点路径。 */
|
||||
async function pushRedo(env: Env): Promise<string> {
|
||||
const dir = await redoDir(env)
|
||||
const path = join(dir, redoName(env.configPath, new Date()))
|
||||
await copyFile(env.configPath, path)
|
||||
// 和备份同一个上限。裁掉的是最「未来」的状态,同备份链裁掉最远的过去
|
||||
const stale = (await listRedo(env)).slice(KEEP_BACKUPS)
|
||||
await Promise.all(stale.map((name) => unlink(join(dir, name)).catch(() => {})))
|
||||
return path
|
||||
}
|
||||
|
||||
/** 撤回最近一次改动:应用最新备份并消费掉它,撤回前的状态进重做栈。 */
|
||||
export async function undoConfig(env: Env): Promise<ApplyOutcome> {
|
||||
const [target] = await listBackups(env)
|
||||
if (!target) {
|
||||
return {
|
||||
ok: false,
|
||||
stage: 'self-check',
|
||||
message: '没有可以撤回的改动',
|
||||
hint: '每次保存都会把改动前的状态留进撤回栈,现在栈是空的。',
|
||||
restored: false,
|
||||
}
|
||||
}
|
||||
const dir = await backupDir(env)
|
||||
const targetYaml = await readFile(join(dir, target), 'utf8')
|
||||
const redoPath = await pushRedo(env)
|
||||
const result = await applyConfig(env, openDocument(targetYaml).data, undefined, {
|
||||
restoreFrom: redoPath,
|
||||
})
|
||||
if (result.ok) await unlink(join(dir, target)).catch(() => {})
|
||||
else await unlink(redoPath).catch(() => {})
|
||||
return result
|
||||
}
|
||||
|
||||
/** 重做最近一次被撤回的改动。当前状态回到撤回栈,重做条目被消费。 */
|
||||
export async function redoConfig(env: Env): Promise<ApplyOutcome> {
|
||||
const [target] = await listRedo(env)
|
||||
if (!target) {
|
||||
return {
|
||||
ok: false,
|
||||
stage: 'self-check',
|
||||
message: '没有可以重做的改动',
|
||||
hint: '只有撤回过的改动才能重做,新的保存会清空重做栈。',
|
||||
restored: false,
|
||||
}
|
||||
}
|
||||
const dir = await redoDir(env)
|
||||
const targetYaml = await readFile(join(dir, target), 'utf8')
|
||||
const backupPath = await createBackup(env)
|
||||
const result = await applyConfig(env, openDocument(targetYaml).data, undefined, {
|
||||
restoreFrom: backupPath,
|
||||
})
|
||||
if (result.ok) await unlink(join(dir, target)).catch(() => {})
|
||||
else await unlink(backupPath).catch(() => {})
|
||||
return result
|
||||
}
|
||||
|
||||
/** 把某个备份恢复成当前配置。走的是和普通改动同一条保护链路。 */
|
||||
export async function restoreBackup(env: Env, name: string): Promise<ApplyOutcome> {
|
||||
const dir = await backupDir(env)
|
||||
|
||||
Reference in New Issue
Block a user