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
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
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:
--- /dev/null
+{
+ "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