duqing2026's picture
优化
2cf991a
Raw
History Blame Contribute Delete
8.23 kB
import os
import json
import threading
import time
import shutil
from pathlib import Path
from flask import Flask, jsonify, request, send_from_directory
try:
from dotenv import load_dotenv
load_dotenv(override=True)
except ImportError:
pass
try:
from huggingface_hub import HfApi, snapshot_download, create_repo
except ImportError:
HfApi = None
app = Flask(__name__, static_folder='frontend/dist')
# Configuration
DATA_DIR = Path("data")
DATA_DIR.mkdir(parents=True, exist_ok=True)
SKILLS_FILE = DATA_DIR / "skills.json"
REPO_ID = os.getenv("HF_DB_REPO", "duqing2026/skill-tree-data") # Default repo, user should configure
HF_TOKEN = os.getenv("HF_TOKEN")
# Default initial data
DEFAULT_SKILLS = {
"nodes": [
{
"id": "1",
"type": "input",
"data": {
"label": "开始",
"status": "completed",
"description": "这是你的技能树起点。",
"links": []
},
"position": {"x": 250, "y": 0}
},
{
"id": "2",
"data": {
"label": "学习 Python",
"status": "in_progress",
"description": "掌握 Python 基础语法和常用库。",
"links": [{"title": "Python 官网", "url": "https://www.python.org"}]
},
"position": {"x": 100, "y": 100}
},
{
"id": "3",
"data": {
"label": "学习 React",
"status": "pending",
"description": "学习组件化开发和 Hooks。",
"links": [{"title": "React 文档", "url": "https://react.dev"}]
},
"position": {"x": 400, "y": 100}
},
],
"edges": [
{"id": "e1-2", "source": "1", "target": "2"},
{"id": "e1-3", "source": "1", "target": "3"},
]
}
class SyncManager:
def __init__(self, repo_id, token, data_dir):
self.repo_id = repo_id
self.token = token
self.data_dir = data_dir
self.api = HfApi(token=token) if HfApi and token else None
self.is_pushing = False
self.status = "online" # online, offline, auth_error
self.last_error = None
# Ensure repo exists
if self.api:
try:
# Check connection first with a lightweight call
self.api.whoami()
self.status = "online"
print(f"Sync: Ensuring repository {self.repo_id} exists...")
create_repo(
repo_id=self.repo_id,
token=self.token,
repo_type="dataset",
exist_ok=True,
private=True
)
except Exception as e:
# Suppress full stack trace for connection errors
error_str = str(e)
self.last_error = error_str
if "MaxRetryError" in error_str or "SSLError" in error_str or "ConnectionError" in error_str:
print(f"Sync: Warning - Could not connect to Hugging Face (Offline Mode). Sync disabled.")
self.status = "offline"
self.api = None # Disable sync for this session
elif "401" in error_str or "Invalid user token" in error_str:
print(f"Sync: Error - Invalid HF_TOKEN. Sync disabled. Please check your .env file.")
self.status = "auth_error"
self.api = None
else:
print(f"Sync: Warning - Could not ensure repo exists: {e}")
self.status = "error"
def pull(self):
if not self.api:
print("Sync: Skipping pull (no API/Token)")
return
print("Sync: Pulling data...")
try:
temp_dir = self.data_dir.parent / "temp_data_sync"
if temp_dir.exists(): shutil.rmtree(temp_dir)
temp_dir.mkdir()
snapshot_download(
repo_id=self.repo_id,
repo_type="dataset",
local_dir=temp_dir,
token=self.token,
allow_patterns=["skills.json"]
)
# Simple overwrite strategy for now, as this is a personal tool
# In a real multi-user scenario, we'd need smart merging
src = temp_dir / "skills.json"
dst = self.data_dir / "skills.json"
if src.exists():
shutil.copy2(src, dst)
print("Sync: Data pulled successfully.")
else:
print("Sync: Remote has no skills.json.")
# If remote is empty but we have local data, push it to initialize remote
if (self.data_dir / "skills.json").exists():
print("Sync: Initializing remote repository with local data...")
self.push()
shutil.rmtree(temp_dir)
except Exception as e:
error_str = str(e)
if "404" in error_str or "Repository Not Found" in error_str:
print(f"Sync: Remote data not found (new repo?). Starting with local data.")
# If remote is empty but we have local data, push it to initialize remote
if (self.data_dir / "skills.json").exists():
print("Sync: Initializing remote repository with local data...")
self.push()
else:
print(f"Sync: Pull failed: {e}")
def push(self):
if not self.api: return
if self.is_pushing: return
print("Sync: Pushing data...")
self.is_pushing = True
try:
self.api.upload_file(
path_or_fileobj=self.data_dir / "skills.json",
path_in_repo="skills.json",
repo_id=self.repo_id,
repo_type="dataset",
commit_message=f"Update skills.json {int(time.time())}"
)
print("Sync: Push successful.")
except Exception as e:
print(f"Sync: Push failed: {e}")
finally:
self.is_pushing = False
sync_manager = SyncManager(REPO_ID, HF_TOKEN, DATA_DIR)
# Initialize data
if not SKILLS_FILE.exists():
# Try to pull first
sync_manager.pull()
# If still not exists, use default
if not SKILLS_FILE.exists():
with open(SKILLS_FILE, "w") as f:
json.dump(DEFAULT_SKILLS, f, indent=2)
# New default data created, push it to remote
print("Sync: Created default data. Pushing to remote...")
sync_manager.push()
else:
# If local exists, try to pull (and auto-push if remote is empty via the catch block above)
sync_manager.pull()
@app.route("/")
def serve_index():
if os.path.exists(os.path.join(app.static_folder, "index.html")):
return send_from_directory(app.static_folder, "index.html")
return "Frontend not built. Please run `npm run build` in frontend directory."
@app.route("/<path:path>")
def serve_static(path):
if os.path.exists(os.path.join(app.static_folder, path)):
return send_from_directory(app.static_folder, path)
return serve_index()
@app.route("/api/skills", methods=["GET"])
def get_skills():
if SKILLS_FILE.exists():
with open(SKILLS_FILE, "r") as f:
return jsonify(json.load(f))
return jsonify(DEFAULT_SKILLS)
@app.route("/api/skills", methods=["POST"])
def save_skills():
data = request.json
with open(SKILLS_FILE, "w") as f:
json.dump(data, f, indent=2)
# Trigger sync in background
if sync_manager.api:
threading.Thread(target=sync_manager.push).start()
return jsonify({"status": "success"})
@app.route("/api/sync/status", methods=["GET"])
def get_sync_status():
return jsonify({
"status": sync_manager.status,
"repo_id": sync_manager.repo_id,
"last_error": sync_manager.last_error
})
if __name__ == "__main__":
port = int(os.getenv("PORT", 7860))
app.run(host="0.0.0.0", port=port)