config.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. """配置加载模块"""
  2. import os
  3. import secrets
  4. from pathlib import Path
  5. import yaml
  6. class Config:
  7. """全局配置,从 config.yaml 加载"""
  8. def __init__(self, config_path: str = "config.yaml"):
  9. self.config_path = Path(config_path)
  10. self._data = {}
  11. if self.config_path.exists():
  12. with open(self.config_path, "r", encoding="utf-8") as f:
  13. self._data = yaml.safe_load(f) or {}
  14. def save(self):
  15. with open(self.config_path, "w", encoding="utf-8") as f:
  16. yaml.dump(self._data, f, allow_unicode=True, default_flow_style=False)
  17. def _resolve(self, p: str) -> Path:
  18. """将相对路径解析为配置文件所在目录的绝对路径"""
  19. path = Path(p)
  20. if not path.is_absolute():
  21. path = self.config_path.parent / path
  22. return path.resolve()
  23. # ── 路径 ──────────────────────────────────────────────
  24. @property
  25. def data_dir(self) -> Path:
  26. return self._resolve(self._get("paths.data_dir", "data"))
  27. @property
  28. def problems_dir(self) -> Path:
  29. return self._resolve(self._get("paths.problems_dir", "data/problems"))
  30. @property
  31. def papers_dir(self) -> Path:
  32. return self._resolve(self._get("paths.papers_dir", "data/papers"))
  33. @property
  34. def db_path(self) -> Path:
  35. return self._resolve(self._get("paths.db_path", "data/bank.db"))
  36. @property
  37. def backups_dir(self) -> Path:
  38. return self._resolve(self._get("paths.backups_dir", "data/backups"))
  39. @property
  40. def knowledge_tree_path(self) -> Path:
  41. return self._resolve(self._get("paths.knowledge_tree", "src/知识点树.md"))
  42. @property
  43. def knowledge_map_path(self) -> Path:
  44. return self._resolve(self._get("paths.knowledge_map", "src/knowledge_map.json"))
  45. # ── 服务端 ────────────────────────────────────────────
  46. @property
  47. def host(self) -> str:
  48. return self._get("server.host", "0.0.0.0")
  49. @property
  50. def port(self) -> int:
  51. return int(self._get("server.port", 8765))
  52. @property
  53. def token(self) -> str:
  54. t = self._get("server.token", "")
  55. if not t:
  56. t = secrets.token_hex(16)
  57. self._set("server.token", t)
  58. self.save()
  59. return t
  60. def regenerate_token(self) -> str:
  61. t = secrets.token_hex(16)
  62. self._set("server.token", t)
  63. self.save()
  64. return t
  65. # ── 默认值 ────────────────────────────────────────────
  66. @property
  67. def default_import_type(self) -> str:
  68. return self._get("defaults.import_type", "精选题")
  69. @property
  70. def max_backups(self) -> int:
  71. return int(self._get("defaults.max_backups", 30))
  72. # ── WebDAV ────────────────────────────────────────────
  73. @property
  74. def webdav_enabled(self) -> bool:
  75. return bool(self._get("webdav.enabled", False))
  76. @property
  77. def webdav_url(self) -> str:
  78. return self._get("webdav.url", "")
  79. @property
  80. def webdav_username(self) -> str:
  81. return self._get("webdav.username", "")
  82. @property
  83. def webdav_password(self) -> str:
  84. return self._get("webdav.password", "")
  85. @property
  86. def webdav_remote_path(self) -> str:
  87. return self._get("webdav.remote_path", "/tiku-backup")
  88. def set_webdav(self, url: str, username: str, password: str, remote_path: str = "/tiku-backup"):
  89. self._set("webdav.enabled", True)
  90. self._set("webdav.url", url)
  91. self._set("webdav.username", username)
  92. self._set("webdav.password", password)
  93. self._set("webdav.remote_path", remote_path)
  94. self.save()
  95. # ── helpers ───────────────────────────────────────────
  96. def _get(self, dotted_key: str, default=None):
  97. keys = dotted_key.split(".")
  98. val = self._data
  99. for k in keys:
  100. if isinstance(val, dict):
  101. val = val.get(k)
  102. else:
  103. return default
  104. return val if val is not None else default
  105. def _set(self, dotted_key: str, value):
  106. keys = dotted_key.split(".")
  107. d = self._data
  108. for k in keys[:-1]:
  109. if k not in d:
  110. d[k] = {}
  111. d = d[k]
  112. d[keys[-1]] = value
  113. # 全局单例,通过 app 的 state 注入
  114. _config_instance: Config | None = None
  115. def get_config(config_path: str = "config.yaml") -> Config:
  116. global _config_instance
  117. if _config_instance is None:
  118. _config_instance = Config(config_path)
  119. return _config_instance
  120. def set_config(cfg: Config):
  121. global _config_instance
  122. _config_instance = cfg