]> acesimba.cloud Git - codebuddy-web.git/commitdiff
tasks: 任务清单纳入 git 版本管理(固定文件+自动提交+非破坏式恢复)
authorCodebuddy <codebuddy@localhost>
Wed, 30 Sep 2026 02:33:29 +0000 (10:33 +0800)
committerCodebuddy <codebuddy@localhost>
Wed, 30 Sep 2026 02:33:29 +0000 (10:33 +0800)
- .gitignore: 取消忽略 data/tasks.json,忽略其 .tmp/损坏备份
- app.py: save_tasks 保存后自动 git commit;load_tasks 缺失/损坏时优先从 git HEAD 恢复,不再静默用单任务种子覆盖

.gitignore
backend/app.py
data/tasks.json [new file with mode: 0644]

index 2cc51c4b53c3b8f8cec22821e1cdd254a23ec1fa..25c1581d1385c3276a1b2784fbba3e0ec9ffc603 100644 (file)
@@ -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
index c4325f2509f010469102325d97b9963cc1690df3..eeb6cdd426630b7e97ed8a7e0454677a728cc675 100644 (file)
@@ -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 (file)
index 0000000..07ab200
--- /dev/null
@@ -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