admin.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. """管理路由"""
  2. from fastapi import APIRouter, Request, HTTPException, Depends
  3. from app.database import get_connection
  4. from app.routers.auth import verify_token
  5. from app.services.backup import manual_backup, auto_backup, get_backup_history
  6. from app.services.indexer import incremental_update
  7. router = APIRouter()
  8. @router.get("/admin/backup-config")
  9. def get_backup_config(request: Request, _=Depends(verify_token)):
  10. """查看 WebDAV 配置"""
  11. config = request.app.state.config
  12. return {
  13. "enabled": config.webdav_enabled,
  14. "url": config.webdav_url,
  15. "username": config.webdav_username,
  16. "configured": bool(config.webdav_enabled and config.webdav_url),
  17. }
  18. @router.put("/admin/backup-config")
  19. def set_backup_config(request: Request, body: dict, _=Depends(verify_token)):
  20. """设置 WebDAV 配置"""
  21. config = request.app.state.config
  22. config.set_webdav(
  23. url=body.get("url", ""),
  24. username=body.get("username", ""),
  25. password=body.get("password", ""),
  26. remote_path=body.get("remote_path", "/tiku-backup"),
  27. )
  28. return {"ok": True}
  29. @router.post("/admin/backup/trigger")
  30. def trigger_backup(request: Request, _=Depends(verify_token)):
  31. """手动触发增量备份"""
  32. config = request.app.state.config
  33. try:
  34. auto_backup(config)
  35. return {"ok": True}
  36. except Exception as e:
  37. raise HTTPException(status_code=500, detail={
  38. "error": {"code": "backup_failed", "message": str(e)}
  39. })
  40. @router.post("/admin/full-backup")
  41. def trigger_full_backup(request: Request, _=Depends(verify_token)):
  42. """手动触发全量备份(bank.db + 全部 docx)"""
  43. config = request.app.state.config
  44. try:
  45. from app.services.backup import full_backup
  46. backup_dir = full_backup(config)
  47. return {"ok": True, "path": str(backup_dir.name)}
  48. except Exception as e:
  49. raise HTTPException(status_code=500, detail={
  50. "error": {"code": "full_backup_failed", "message": str(e)}
  51. })
  52. @router.get("/admin/backup/history")
  53. def backup_history(request: Request, _=Depends(verify_token)):
  54. """备份历史"""
  55. config = request.app.state.config
  56. return {"backups": get_backup_history(config)}
  57. @router.post("/admin/clear-all")
  58. def clear_all(request: Request, _=Depends(verify_token)):
  59. """清空题库(删除所有题目和数据库索引,保留知识点)"""
  60. config = request.app.state.config
  61. try:
  62. # Delete all docx files
  63. for docx in config.problems_dir.glob("*.docx"):
  64. docx.unlink()
  65. # Delete DB and rebuild schema + knowledge
  66. conn = get_connection(config.db_path)
  67. conn.execute("DELETE FROM problems")
  68. conn.execute("DELETE FROM problems_fts")
  69. conn.execute("DELETE FROM cart_items")
  70. conn.execute("DELETE FROM import_tasks")
  71. conn.commit()
  72. conn.close()
  73. # Rebuild index
  74. from app.services.indexer import incremental_update
  75. incremental_update(config)
  76. return {"ok": True}
  77. except Exception as e:
  78. raise HTTPException(status_code=500, detail={
  79. "error": {"code": "clear_failed", "message": str(e)}
  80. })
  81. @router.post("/admin/rebuild-index")
  82. def rebuild_index(request: Request, _=Depends(verify_token)):
  83. """手动增量索引"""
  84. config = request.app.state.config
  85. try:
  86. changes = incremental_update(config)
  87. return {"ok": True, "changes": changes}
  88. except Exception as e:
  89. raise HTTPException(status_code=500, detail={
  90. "error": {"code": "rebuild_failed", "message": str(e)}
  91. })
  92. @router.post("/admin/knowledge-build")
  93. def knowledge_build(request: Request, _=Depends(verify_token)):
  94. """构建知识点编码"""
  95. config = request.app.state.config
  96. try:
  97. from app.services.knowledge import build_knowledge
  98. build_knowledge(config)
  99. return {"ok": True}
  100. except Exception as e:
  101. raise HTTPException(status_code=500, detail={
  102. "error": {"code": "knowledge_build_failed", "message": str(e)}
  103. })
  104. @router.post("/admin/git-update")
  105. def git_update(request: Request, _=Depends(verify_token)):
  106. """从远端 git 拉取更新并重启服务"""
  107. import subprocess, os, signal
  108. config = request.app.state.config
  109. project_dir = config.config_path.parent
  110. try:
  111. result = subprocess.run(
  112. ["git", "pull", "origin", "master"],
  113. cwd=str(project_dir),
  114. capture_output=True, text=True, timeout=30
  115. )
  116. if "up to date" in result.stdout.lower() or "已经是最新" in result.stdout:
  117. return {"updated": False, "message": "已是最新版本", "git_output": result.stdout}
  118. if result.returncode != 0:
  119. raise RuntimeError(result.stderr)
  120. # 有更新,1秒后重启
  121. import threading, sys
  122. def restart():
  123. import time
  124. time.sleep(1)
  125. sys.exit(0)
  126. threading.Thread(target=restart, daemon=True).start()
  127. return {"updated": True, "message": "已更新,正在重启...", "git_output": result.stdout}
  128. except subprocess.TimeoutExpired as e:
  129. raise HTTPException(status_code=500, detail={
  130. "error": {"code": "git_timeout", "message": "git pull 超时"}
  131. })
  132. except Exception as e:
  133. raise HTTPException(status_code=500, detail={
  134. "error": {"code": "git_failed", "message": str(e)}
  135. })