-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoverlay_tool.py
More file actions
677 lines (567 loc) · 26.7 KB
/
overlay_tool.py
File metadata and controls
677 lines (567 loc) · 26.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
#!/usr/bin/env python3
"""
MaaOWM V3 — Overlay Workspace Manager (基于 MaaFW Oracle)
V3 核心改动:
- 委托 MaaFW PipelineDumper 做语义合并, 不再自己实现字段级 diff
- canonical 比对天然支持 V1/V2 混用 (输出统一为 V2)
- mount 时存 base 快照, unmount 时用快照减数 (与 git pull 解耦)
- 不再处理 image/model (直接 passthrough)
"""
from __future__ import annotations
import sys
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
try:
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich.prompt import Prompt, Confirm
from rich import box
except ImportError:
print("错误: 缺少 rich 库。请运行: pip install rich")
sys.exit(1)
from core import config as config_mod
from core import diff
from core import env_check
from core import inplace
from core import preflight
VERSION = "0.7.7"
STATE_UNMOUNTED = "未挂载"
STATE_MOUNTED = "已挂载"
HELP_TEXT = """\
[bold cyan]MaaOWM — MaaFramework Overlay Workspace Manager[/bold cyan]
为多适配包项目 (base + PC/Mobile/...) 提供"挂载-编辑-卸载"工作流。
挂载: 把 base+mod 合并成全字段工作区, 让编辑器看见完整世界。
卸载: 把工作区和 base 做字段级 diff, 写回最小 mod 增量。
[bold yellow]━━━ 第一次用 ━━━[/bold yellow]
1. 准备 overlay_config.json (放在 MaaOWM 目录):
{
"target": "../你的项目/assets/resource/PC",
"base_layers": ["../你的项目/assets/resource/base"]
}
路径相对配置文件目录。target 是你要 overlay 编辑的适配包。base多包用,隔开。
2. 用项目虚拟环境的 Python 运行:
& "你的项目/.venv/Scripts/python.exe" overlay_tool.py (Windows)
用错 Python 会触发 numpy 不兼容; 出错时会有友好诊断。
3. 按 [M] 挂载 → 用 MaaPipelineEditor / VSCode 打开 target 目录编辑
4. 编辑完按 [U] 卸载, target 目录变回干净的 minimal mod
[bold yellow]━━━ 日常工作流 ━━━[/bold yellow]
[M] 挂载 → 用编辑器在 target 目录开发 → [U] 卸载
挂载后 target 目录 = 全字段工作区 (易读, 编辑器看得见所有字段)
卸载后 target 目录 = minimal mod (干净, 仅你实际改动的)
挂载期间可以随时:
[C] 检查工作区状态 (变动统计 / 语法预检)
[B] 查看备份目录 [L] 查看上次操作日志
[bold yellow]━━━ 主菜单速查 ━━━[/bold yellow]
[M]ount 挂载 备份 mod, 写入 base+mod 合并工作区
[U]nmount 卸载 diff 出 minimal mod, 写回适配包
[C]heck 检查 dry-run 验证工作区可加载 + 文件变动统计
[V]ersion 切 V1/V2 输出格式 (仅未挂载时可切)
[N]ode-refs 切 next/on_error 紧凑写法开关
[L]og 看上次操作日志
[B]ackup 列出 .maaowm/ 下的备份目录
[H]elp 本帮助
[Q]uit 退出
[bold yellow]━━━ 工作区编辑准则 ━━━[/bold yellow]
[green]✓ 改字段值[/green] post_delay: 3000 → 200
[green]✓ 给 task 加新字段[/green]
[green]✓ 新建 task[/green]
[green]✓ 改 / 加 / 删 doc/desc 注释[/green]
[red]✗ 不要删字段想"还原 base 值"[/red]
工作区独立加载, 缺失字段用框架默认值 (不是 base 的值)。
要还原 base 的某字段, 直接把值改成期望的形态。
[red]✗ 不要随意删被引用的 task[/red]
next/on_error 引用不存在的 task 会拒绝加载。
要让 task 失效, 改 enabled: false 而非删除。
[dim]删整字段 doc → mod 不写, 重挂载时 base 的 doc 恢复 (撤回修改)
要真正清空 → 写 doc: "" (显式空字符串)[/dim]
[bold yellow]━━━ V1 / V2 输出格式选择 ━━━[/bold yellow]
[cyan]V2 (默认)[/cyan] recognition/action 嵌套形态, 字段归属清晰
适合: 习惯 MaaFW Pipeline V2 文档标准写法
[cyan]V1[/cyan] recognition 字段名拍平到 task 顶层
适合: 习惯 手写Pipeline 风格
[V] 切换 (仅未挂载时)。挂载状态切换会破坏 git diff, 已禁。
[bold yellow]━━━ 出错了怎么办 ━━━[/bold yellow]
[red]启动报 numpy / maa 加载错误[/red]
大概率是 Python 解释器和 maa 所在环境不匹配。
按提示用该环境的 Python 重跑。
[red][U] 卸载提示"加载工作区失败"[/red]
工作区 JSON 有语法/字段错误。
用 VSCode + MaaSupport 插件查具体位置, 或按 [C] 看错误。
[red]改了 doc/desc 卸载后 mod 没变化[/red]
只改注释 + 该 task 没其他变动 → 自动检测到 extras 变化会写入。
如果你只是改了又改回去, oracle 看是 IDENTICAL, 确实不写。
[red]误操作了想恢复[/red]
[B] 查备份。.maaowm/<时间戳>/mod/ 是挂载前的样子。
work/ 是上次卸载前的工作区样子。
[bold yellow]━━━ 进阶[/bold yellow]
• 挂载后工作区根目录有 __OWM_README__.md, 含编辑细节
• README.md 面向使用者的完整指引
• ARCHITECTURE.md V3 设计文档, 给想理解原理或接手维护的人
"""
class OverlayToolApp:
def __init__(self, config_path: str | Path):
self.console = Console()
self.config_path = Path(config_path).resolve()
self.config: config_mod.OverlayConfig | None = None
self.state: str = STATE_UNMOUNTED
def _detect_state(self) -> str:
if self.config and inplace.is_mounted(self.config):
return STATE_MOUNTED
return STATE_UNMOUNTED
def _render_header(self):
mounted = self.state == STATE_MOUNTED
state_style = "bold green" if mounted else "bold yellow"
state_label = STATE_MOUNTED if mounted else STATE_UNMOUNTED
title = Text()
title.append("MaaOWM V3 ", style="bold white")
title.append(f"v{VERSION}", style="dim")
title.append(" ", style="")
title.append("Oracle-based", style="bold magenta")
status = Text()
status.append("状态: ", style="bold")
status.append(state_label, style=state_style)
if mounted and self.config:
info = inplace.get_mount_info(self.config)
if info:
status.append(f" ({info['mount_ts_readable']} 挂载, ", style="dim")
status.append(f"{info['task_count']} task", style="cyan")
status.append(")", style="dim")
if self.config:
cfg = self.config
status.append("\n")
status.append("Target : ", style="dim")
status.append(str(cfg.target_path), style="white")
status.append("\nBase : ", style="dim")
status.append(" ".join(str(p) for p in cfg.base_layer_paths()), style="white")
if cfg.maa_pkg_dir:
status.append("\nmaa : ", style="dim")
status.append(str(cfg.maa_pkg_dir) + " (显式)", style="cyan")
status.append("\n输出 : ", style="dim")
if cfg.output_format == "v1":
status.append("V1", style="bold magenta")
status.append(" (拍平 / 省略默认)", style="dim magenta")
else:
status.append("V2", style="bold cyan")
status.append(" (嵌套 / 默认值省略)", style="dim cyan")
# 紧凑节点引用 (默认开, 关闭时显示警示)
if not cfg.compact_node_refs:
status.append(" | next: ", style="dim")
status.append("object 形态", style="yellow")
content = Text()
content.append_text(title)
content.append("\n")
content.append_text(status)
self.console.print(Panel(content, box=box.ROUNDED, border_style="blue"))
def _render_menu(self):
mounted = self.state == STATE_MOUNTED
table = Table(show_header=False, box=None, padding=(0, 2), expand=False)
table.add_column(style="bold cyan", width=4)
table.add_column()
items = []
if not mounted:
items.append(("M", "挂载", "备份 mod, base+mod 合并写入工作区"))
else:
items.append(("U", "卸载", "diff 提取 minimal mod, 写回 mod 包"))
items.append(("C", "检查工作区", "预检语法 + 文件级变动统计 (dry-run)"))
items.append(("V", "切换输出格式", "V2 ↔ V1 (下次写文件时生效)"))
items.append(("N", "切换紧凑节点引用", "next/on_error: 字符串 ↔ object"))
items += [
("L", "查看日志", "operations.log 最近记录"),
("B", "查看备份", ".maaowm/ 中的备份列表"),
("H", "使用说明", ""),
("0", "退出", ""),
]
for key, label, desc in items:
row = label + (f" [dim]— {desc}[/dim]" if desc else "")
table.add_row(f"[{key}]", row)
self.console.print(table)
self.console.print()
def action_mount(self):
assert self.config is not None
self.console.print("\n[bold]━━━ 挂载 ━━━[/bold]\n")
fmt = self.config.output_format
fmt_label = (
"[magenta]V1[/magenta] (拍平 / 省略默认)"
if fmt == "v1"
else "[cyan]V2[/cyan] (嵌套 / 默认值省略)"
)
compact_label = (
"[green]紧凑写法[/green]"
if self.config.compact_node_refs
else "[yellow]object 形态[/yellow]"
)
self.console.print(
"[yellow]注意[/yellow] 挂载将备份当前 mod 包, 然后用 base+mod 合并的 canonical "
"(默认值省略后) 内容覆盖。\n"
" 若 mod 在 Git 仓库中, git status 会出现大量变更, 属正常现象。\n"
" 建议挂载前先 commit 当前状态。\n"
f" 当前输出格式: {fmt_label}\n"
f" next/on_error: {compact_label}\n"
)
if not Confirm.ask("确认继续挂载?", default=True):
self.console.print("[yellow]操作取消[/yellow]")
return
# 预检 maa 环境
if not self._precheck_maa_env():
return
try:
result = inplace.mount(
self.config,
progress_callback=lambda m: self.console.print(f" [dim]→ {m}[/dim]"),
)
except inplace.MountError as e:
self.console.print(f"\n[red]✗ 挂载失败:[/red] {e}")
return
if result.warnings:
self.console.print("[yellow]警告:[/yellow]")
for w in result.warnings:
self.console.print(f" [yellow]![/yellow] {w}")
self.console.print(f"\n[green]✓ 挂载完成[/green] {result.summary()}")
self.console.print(
f" 备份: [dim].maaowm/{result.og_backup}[/dim]\n"
" [cyan]现可用 MaaPipelineEditor 打开 mod 控制器编辑。[/cyan]\n"
" [cyan]编辑完毕选 [U] 卸载。[/cyan]"
)
self.state = STATE_MOUNTED
def action_unmount(self):
assert self.config is not None
self.console.print("\n[bold]━━━ 卸载 ━━━[/bold]\n")
fmt = self.config.output_format
fmt_label = (
"[magenta]V1[/magenta] (拍平 / 省略默认)"
if fmt == "v1"
else "[cyan]V2[/cyan] (嵌套 / 默认值省略)"
)
compact_label = (
"[green]紧凑写法[/green]"
if self.config.compact_node_refs
else "[yellow]object 形态[/yellow]"
)
self.console.print(f" 当前输出格式: {fmt_label}")
self.console.print(f" next/on_error: {compact_label}\n")
if not Confirm.ask("确认继续卸载?", default=True):
self.console.print("[yellow]操作取消[/yellow]")
return
# 预检 maa 环境
if not self._precheck_maa_env():
return
try:
result = inplace.unmount(
self.config,
progress_callback=lambda m: self.console.print(f" [dim]→ {m}[/dim]"),
)
except inplace.UnmountError as e:
self.console.print(f"\n[red]✗ 卸载失败:[/red] {e}")
return
if result.warnings:
self.console.print("[yellow]警告:[/yellow]")
for w in result.warnings:
self.console.print(f" [yellow]![/yellow] {w}")
self.console.print(f"\n[green]✓ 卸载完成[/green] {result.summary()}")
if result.hints:
self.console.print("\n[bold]Hints:[/bold]")
for h in result.hints:
style = "yellow" if h.severity == "warn" else "blue"
sym = "⚠" if h.severity == "warn" else "ℹ"
self.console.print(f" [{style}]{sym}[/{style}] [bold]{h.task}[/bold]")
for line in h.text.splitlines():
self.console.print(f" [{style}]{line}[/{style}]")
self.console.print(
f"\n 工作区备份: [dim].maaowm/{result.work_backup}[/dim]\n"
" [cyan]mod 已恢复为 minimal 增量, 请用 Git 检查并提交。[/cyan]"
)
self.state = STATE_UNMOUNTED
def action_view_log(self):
assert self.config is not None
self.console.print("\n[bold]━━━ 操作日志 (最近 50 条) ━━━[/bold]\n")
lines = inplace.get_log_lines(self.config)
if not lines:
self.console.print("[dim]暂无日志记录。[/dim]")
return
for line in lines:
if "[MOUNT-OK]" in line:
self.console.print(f" [green]{line}[/green]")
elif "[UNMOUNT-OK]" in line:
self.console.print(f" [cyan]{line}[/cyan]")
elif "-FAIL" in line:
self.console.print(f" [red]{line}[/red]")
else:
self.console.print(f" [dim]{line}[/dim]")
def action_view_backups(self):
assert self.config is not None
self.console.print("\n[bold]━━━ 备份列表 ━━━[/bold]\n")
backups = inplace.get_backup_list(self.config)
if not backups:
self.console.print("[dim]暂无备份。[/dim]")
return
tbl = Table(box=box.SIMPLE_HEAD, show_edge=False, pad_edge=False, padding=(0, 1))
tbl.add_column("时间", style="dim", no_wrap=True)
tbl.add_column("类型", no_wrap=True)
tbl.add_column("目录名", style="white")
for b in backups:
kind_style = "yellow" if "og" in b["kind"] else "cyan"
tbl.add_row(b["mtime_str"], f"[{kind_style}]{b['kind']}[/]", b["name"])
self.console.print(tbl)
self.console.print(f"\n [dim]备份位置: {self.config.owm_dir}[/dim]")
def action_validate(self):
"""检查工作区: 预检语法 + 文件级变动统计 (dry-run, 不动任何文件)。"""
assert self.config is not None
self.console.print("\n[bold]━━━ 检查工作区 ━━━[/bold]\n")
# 预检 maa 环境
if not self._precheck_maa_env():
return
try:
inplace.oracle.init(self.config.maa_pkg_dir)
except inplace.oracle.OracleError as e:
self.console.print(f"[red]oracle 初始化失败: {e}[/red]")
return
result = preflight.validate_workspace(
self.config,
progress_callback=lambda m: self.console.print(f" [dim]→ {m}[/dim]"),
)
# 失败路径
if not result.ok:
self.console.print(f"\n[red]✗ {result.summary}[/red]\n")
for line in (result.error_detail or "").splitlines():
self.console.print(f" [red]{line}[/red]")
self.console.print(
"\n [yellow]此状态下 [U] 卸载将拒绝执行, 直至错误修复。[/yellow]"
)
return
# 成功路径 — 总览 → 文件列表 → 总结
self.console.print()
self._render_validate_report(result)
def _render_validate_report(self, result):
"""成功验证后渲染报告: 总览 → 文件列表 → 总结"""
# 总览
overview = Table(
title="变动总览", title_style="bold cyan",
box=box.SIMPLE_HEAD, show_edge=False, padding=(0, 2),
)
overview.add_column("状态", style="bold", no_wrap=True)
overview.add_column("数量", justify="right", style="cyan")
overview.add_row("修改", str(result.total_modified))
overview.add_row("新增", str(result.total_added))
overview.add_row("删除", str(result.total_deleted))
overview.add_row("无变化", f"[dim]{result.total_identical}[/dim]")
self.console.print(overview)
self.console.print()
# 文件列表
if result.file_stats:
files_tbl = Table(
title="按文件分布", title_style="bold cyan",
box=box.SIMPLE_HEAD, show_edge=False, padding=(0, 2),
)
files_tbl.add_column("文件", style="white", no_wrap=False)
files_tbl.add_column("修改", justify="right", no_wrap=True)
files_tbl.add_column("新增", justify="right", no_wrap=True)
files_tbl.add_column("删除", justify="right", no_wrap=True)
files_tbl.add_column("无变化", justify="right", style="dim", no_wrap=True)
def _cell(n: int, color: str) -> str:
if n == 0:
return "[dim]·[/dim]"
return f"[{color}]{n}[/{color}]"
for fs in result.file_stats:
if fs.has_changes:
name_style = "bright_white"
else:
name_style = "dim"
files_tbl.add_row(
f"[{name_style}]{fs.relative}[/{name_style}]",
_cell(fs.modified, "yellow"),
_cell(fs.added, "green"),
_cell(fs.deleted, "red"),
str(fs.identical) if fs.identical else "[dim]·[/dim]",
)
self.console.print(files_tbl)
self.console.print()
# 总结
if result.has_changes():
self.console.print(f"[green]✓ {result.summary}[/green]")
self.console.print(
" [dim]→ 改动量已达预期可考虑执行 [U] 卸载, "
"并 git commit 留个版本.[/dim]"
)
else:
self.console.print(f"[yellow]· {result.summary}[/yellow]")
self.console.print(
" [dim]→ 无可卸载内容, 卸载也不会产生 mod 文件.[/dim]"
)
def _precheck_maa_env(self) -> bool:
"""检查 maa 环境是否可用. 失败时打印诊断并返回 False."""
assert self.config is not None
err = env_check.precheck(self.config)
if err is None:
return True
self.console.print()
self.console.print(err.formatted_message)
return False
def action_help(self):
self.console.print()
self.console.print(Panel(HELP_TEXT, title="使用说明", border_style="cyan", expand=False))
def action_toggle_format(self):
"""切换 V2 ↔ V1 输出格式 (仅未挂载时可切)。"""
assert self.config is not None
self.console.print("\n[bold]━━━ 切换输出格式 ━━━[/bold]\n")
cur = self.config.output_format
new = "v1" if cur == "v2" else "v2"
cur_label = "[cyan]V2[/cyan] (嵌套 / 默认值省略)" if cur == "v2" else "[magenta]V1[/magenta] (拍平 / 省略默认)"
new_label = "[magenta]V1[/magenta] (拍平 / 省略默认)" if new == "v1" else "[cyan]V2[/cyan] (嵌套 / 默认值省略)"
self.console.print(f" 当前: {cur_label}")
self.console.print(f" 目标: {new_label}\n")
mounted = self.state == STATE_MOUNTED
if new == "v1":
base_hint = (
"[yellow]提示[/yellow] V1 模式说明:\n"
" • 工作区文件: recognition/action 字段拍平到 task 顶层\n"
" • mod 产物: 同样拍平形态\n"
" • 默认 type (DirectHit/DoNothing) 整段省略\n"
)
else:
base_hint = (
"[yellow]提示[/yellow] V2 模式说明:\n"
" • 工作区文件: recognition/action 嵌套对象形态\n"
" • mod 产物: 同样嵌套形态\n"
)
if mounted:
timing_hint = (
" [bold]当前已挂载[/bold] — 工作区文件保持不变 (避免覆盖你的编辑).\n"
" 下次卸载时, mod 产物将以新格式写入.\n"
" 如想立即看到新格式工作区: 卸载 → 切换 → 重新挂载.\n"
)
else:
timing_hint = (
" [bold]当前未挂载[/bold] — 不影响任何文件.\n"
" 下次挂载时, 工作区将以新格式写入.\n"
)
self.console.print(base_hint + timing_hint)
if not Confirm.ask(f"确认切换为 {new.upper()}?", default=True):
self.console.print("[yellow]操作取消[/yellow]")
return
try:
config_mod.set_output_format_in_config(self.config_path, new)
except Exception as e:
self.console.print(f"[red]✗ 写入配置失败: {e}[/red]")
return
# 重新加载 config 让本次会话生效
self.config = config_mod.load_config(self.config_path)
self.console.print(
f"[green]✓ 已切换为 {new.upper()}[/green] "
f"[dim](写入 {self.config_path.name})[/dim]"
)
def action_toggle_compact(self):
"""切换 next/on_error 紧凑写法。"""
assert self.config is not None
self.console.print("\n[bold]━━━ 切换紧凑节点引用 ━━━[/bold]\n")
cur = self.config.compact_node_refs
new = not cur
cur_label = "[green]启用[/green] (字符串 + [JumpBack] 前缀)" if cur else "[yellow]关闭[/yellow] (object 形态)"
new_label = "[green]启用[/green] (字符串 + [JumpBack] 前缀)" if new else "[yellow]关闭[/yellow] (object 形态)"
self.console.print(f" 当前: {cur_label}")
self.console.print(f" 目标: {new_label}\n")
if new:
self.console.print(
'[yellow]提示[/yellow] 紧凑写法示例:\n'
' next: ["TaskA", "[JumpBack]TaskB", "[Anchor][JumpBack]TaskC"]\n'
" 适合手写, 与 base 习惯一致, 大多数项目首选.\n"
)
else:
self.console.print(
'[yellow]提示[/yellow] object 写法示例:\n'
' next: [{"name":"TaskA", "anchor":false, "jump_back":false}, ...]\n'
" 字段显式, 适合脚本程序处理, 极少用户偏好.\n"
)
mounted = self.state == STATE_MOUNTED
if mounted:
self.console.print(
" [bold]当前已挂载[/bold] — 工作区文件保持不变.\n"
" 下次卸载时 mod 产物将以新写法写入.\n"
" 如想立即看到新写法工作区: 卸载 → 切换 → 重新挂载.\n"
)
else:
self.console.print(
" [bold]当前未挂载[/bold] — 下次挂载/卸载时生效.\n"
)
if not Confirm.ask(
f"确认{'启用' if new else '关闭'}紧凑节点引用?", default=True
):
self.console.print("[yellow]操作取消[/yellow]")
return
try:
config_mod.set_compact_node_refs_in_config(self.config_path, new)
except Exception as e:
self.console.print(f"[red]✗ 写入配置失败: {e}[/red]")
return
self.config = config_mod.load_config(self.config_path)
self.console.print(
f"[green]✓ 紧凑节点引用已{'启用' if new else '关闭'}[/green] "
f"[dim](写入 {self.config_path.name})[/dim]"
)
def run(self):
self.console.clear()
self.console.print(f"[dim]配置文件: {self.config_path}[/dim]\n")
if not self.config_path.exists():
self.console.print(f"[yellow]配置文件不存在: {self.config_path}[/yellow]\n")
if Confirm.ask("是否生成示例配置?", default=True):
config_mod.write_sample_config(self.config_path)
self.console.print(f"[green]✓[/green] 示例已写入 {self.config_path}")
self.console.print("[dim]请按项目实际路径修改后重新运行。[/dim]")
return
try:
self.config = config_mod.load_config(self.config_path)
except config_mod.ConfigError as e:
self.console.print(f"[red]配置加载失败: {e}[/red]")
return
errs = self.config.validate()
if errs:
self.console.print("[red]配置校验失败:[/red]")
for err in errs:
self.console.print(f" [red]✗[/red] {err}")
return
self.state = self._detect_state()
while True:
self.console.print()
self._render_header()
self.console.print()
self._render_menu()
mounted = self.state == STATE_MOUNTED
if mounted:
choices = ["U", "C", "V", "N", "L", "B", "H", "0"]
else:
choices = ["M", "V", "N", "L", "B", "H", "0"]
choice = Prompt.ask("选择操作", choices=choices, default="0").upper()
if choice == "M":
self.action_mount()
elif choice == "U":
self.action_unmount()
elif choice == "C":
self.action_validate()
elif choice == "V":
self.action_toggle_format()
elif choice == "N":
self.action_toggle_compact()
elif choice == "L":
self.action_view_log()
elif choice == "B":
self.action_view_backups()
elif choice == "H":
self.action_help()
elif choice == "0":
self.console.print("\n[dim]再见![/dim]")
break
def main():
if len(sys.argv) > 1:
config_path = Path(sys.argv[1])
else:
cwd_config = Path.cwd() / "overlay_config.json"
script_config = _SCRIPT_DIR / "overlay_config.json"
config_path = cwd_config if cwd_config.exists() else (
script_config if script_config.exists() else cwd_config
)
OverlayToolApp(config_path).run()
if __name__ == "__main__":
main()