From: Codebuddy Date: Wed, 30 Sep 2026 02:33:29 +0000 (+0800) Subject: tasks: 任务清单纳入 git 版本管理(固定文件+自动提交+非破坏式恢复) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=e361c50afd231f2c9afac32e09babc454dd571ae;p=codebuddy-web.git tasks: 任务清单纳入 git 版本管理(固定文件+自动提交+非破坏式恢复) - .gitignore: 取消忽略 data/tasks.json,忽略其 .tmp/损坏备份 - app.py: save_tasks 保存后自动 git commit;load_tasks 缺失/损坏时优先从 git HEAD 恢复,不再静默用单任务种子覆盖 --- diff --git a/.gitignore b/.gitignore index 2cc51c4..25c1581 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,12 @@ __pycache__/ logs/*.log # Runtime state: task registry (session IDs, last-used timestamps) +# 任务清单纳入版本管理(可 git 提交 / 损坏或丢失时从 git 恢复),故取消忽略 data/tasks.json +!data/tasks.json +# 写入临时文件与损坏备份不纳入版本管理 +data/tasks.json.tmp +data/tasks.json.corrupted* # Runtime state: user-configured quick links (name + URL entries) data/urls.json diff --git a/backend/app.py b/backend/app.py index c4325f2..eeb6cdd 100644 --- a/backend/app.py +++ b/backend/app.py @@ -226,8 +226,61 @@ def now_iso() -> str: return datetime.datetime.now().isoformat(timespec="seconds") +# ==================== 任务清单的 git 持久化 ==================== +# 任务清单(data/tasks.json) 纳入 git 版本管理: +# - 每次保存后自动 git commit,便于追溯与跨环境同步; +# - 文件缺失/损坏时优先从 git HEAD 恢复,避免被“只含 words”的种子静默覆盖(根治数据丢失)。 +_GIT_LOCK = threading.Lock() +_REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _tasks_git_rel() -> str: + return os.path.relpath(TASKS_FILE, _REPO_ROOT) + + +def _git_commit_tasks() -> None: + """保存任务清单后自动提交到 git。失败仅告警,不影响接口正常返回。""" + try: + with _GIT_LOCK: + subprocess.run(["git", "add", "--", _tasks_git_rel()], cwd=_REPO_ROOT, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + r = subprocess.run(["git", "commit", "-m", f"tasks: auto-save {now_iso()}"], + cwd=_REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # 无实际变更时 git commit 返回非 0,忽略 + if r.returncode == 0: + logger.info("任务清单已自动 git commit") + except Exception as e: + logger.warning(f"任务清单 git commit 失败(文件已保存): {e}") + + +def _restore_tasks_from_git() -> dict | None: + """尝试从 git HEAD 恢复任务清单;不可用时返回 None。""" + try: + r = subprocess.run(["git", "show", f"HEAD:{_tasks_git_rel()}"], cwd=_REPO_ROOT, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=True) + return json.loads(r.stdout) + except Exception: + return None + + +def _backup_corrupt_tasks() -> None: + """把损坏的任务清单备份为带时间戳的文件,便于事后排查。""" + try: + ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + dst = f"{TASKS_FILE}.corrupted-{ts}" + os.replace(TASKS_FILE, dst) + logger.warning(f"任务清单已损坏,已备份为 {dst}") + except Exception as e: + logger.warning(f"备份损坏任务清单失败: {e}") + + def load_tasks() -> dict: if not os.path.isfile(TASKS_FILE): + restored = _restore_tasks_from_git() + if restored is not None: + logger.info("任务清单缺失,已从 git HEAD 恢复") + save_tasks(restored) + return restored data = seed_tasks() save_tasks(data) return data @@ -235,6 +288,13 @@ def load_tasks() -> dict: with open(TASKS_FILE, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, OSError): + # 非破坏式:先备份坏文件,再优先从 git 恢复,绝不静默用“只含 words”覆盖 + _backup_corrupt_tasks() + restored = _restore_tasks_from_git() + if restored is not None: + logger.info("任务清单损坏,已从 git HEAD 恢复") + save_tasks(restored) + return restored data = seed_tasks() save_tasks(data) return data @@ -245,6 +305,7 @@ def save_tasks(data: dict) -> None: with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp, TASKS_FILE) + _git_commit_tasks() def load_urls() -> list: diff --git a/data/tasks.json b/data/tasks.json new file mode 100644 index 0000000..07ab200 --- /dev/null +++ b/data/tasks.json @@ -0,0 +1,88 @@ +{ + "tasks": [ + { + "id": "df2f2032-51e7-4b7d-ae67-fbfa7900e101", + "name": "words", + "cwd": "/home/words", + "session_id": "86ac790a-6b7b-4a62-9df0-075537847d1b", + "created_at": "2026-09-30T00:42:44", + "last_used": "2026-09-30T09:06:23" + }, + { + "id": "263361c1-2e75-43ba-bf86-884e28da1422", + "name": "temp", + "cwd": "/home/temp", + "session_id": "417d5e55-943c-4d30-a294-3f3db3f0690b", + "created_at": "2026-09-30T08:59:34", + "last_used": "2026-09-30T09:00:10" + }, + { + "id": "78e2052b-c340-476e-bc80-5845e07778e4", + "name": "AIDC", + "cwd": "/home/Station AI Demo", + "session_id": "c338824c-9b37-425a-ba6b-4a3876c952d2", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46", + "description": "", + "group": "261a5121-eb02-4ce3-b969-ea4d954c3240" + }, + { + "id": "fb663800-a21e-4a10-8ed7-eb818127c46a", + "name": "Chatbot", + "cwd": "/home/Chatbot", + "session_id": "a1d63dee-297f-4b2a-9b95-3f92c8557852", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46" + }, + { + "id": "ba726294-a571-4af3-bab1-fe37ad445a33", + "name": "Codebuddy-web", + "cwd": "/home/Codebuddy-web", + "session_id": "b7619308-b1da-4cf8-8c67-79f44b8a2931", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46" + }, + { + "id": "5bf9803c-860d-4613-8106-8b6f46a0f493", + "name": "Station AI Demo", + "cwd": "/home/Station AI Demo", + "session_id": "9a72567a-2218-4b02-92c9-6b6872f0dd89", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46", + "description": "", + "group": "261a5121-eb02-4ce3-b969-ea4d954c3240" + }, + { + "id": "2d276dae-2274-4f59-a90f-4222f0a2fc0a", + "name": "StationBuilder", + "cwd": "/home/StationBuilder", + "session_id": "ec66b280-e19a-4aaa-8504-0d4fe36d59d3", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46", + "description": "", + "group": "261a5121-eb02-4ce3-b969-ea4d954c3240" + }, + { + "id": "baf07eb2-2c9a-483e-a2cc-9f15871bbc00", + "name": "StudyBase", + "cwd": "/home/StudyBase", + "session_id": "8c6ac226-52b7-4761-a06a-a5c9c67cad80", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:15:21" + }, + { + "id": "512fbc22-c49d-44a6-be80-355191d237a5", + "name": "study-mountion", + "cwd": "/home/study-mountion", + "session_id": "6d4b2cd9-04ce-4d96-88b1-194f0f605746", + "created_at": "2026-09-30T10:11:46", + "last_used": "2026-09-30T10:11:46" + } + ], + "groups": [ + { + "id": "261a5121-eb02-4ce3-b969-ea4d954c3240", + "name": "iStation" + } + ] +} \ No newline at end of file