xiaolongmr commited on
Commit
24f4569
·
1 Parent(s): d94512c

Use persistent SQLite storage for user data

Browse files
.env.example CHANGED
@@ -19,8 +19,9 @@ CHATGPT2API_AUTH_KEY=your_secret_key_here
19
  # ============================================
20
 
21
  # 存储后端类型(可选值: json, sqlite, postgres, git)
22
- # 默认: json
23
- STORAGE_BACKEND=json
 
24
 
25
  # ============================================
26
  # 数据库配置(当 STORAGE_BACKEND=sqlite/postgres 时使用)
@@ -32,8 +33,8 @@ STORAGE_BACKEND=json
32
  # Supabase 示例
33
  # DATABASE_URL=postgresql://postgres.xxx:password@aws-1-us-west-1.pooler.supabase.com:5432/postgres
34
 
35
- # SQLite 示例(不指定时自动使用 data/accounts.db)
36
- # DATABASE_URL=sqlite:///app/data/accounts.db
37
 
38
  # ============================================
39
  # Git 仓库配置(当 STORAGE_BACKEND=git 时使用)
 
19
  # ============================================
20
 
21
  # 存储后端类型(可选值: json, sqlite, postgres, git)
22
+ # 推荐 Hugging Face Storage Bucket 挂载到 /app/persist 后使用 sqlite。
23
+ # 如果没有设置 STORAGE_BACKEND 且 /app/persist 存在,程序会自动使用 /app/persist/accounts.db。
24
+ # STORAGE_BACKEND=sqlite
25
 
26
  # ============================================
27
  # 数据库配置(当 STORAGE_BACKEND=sqlite/postgres 时使用)
 
33
  # Supabase 示例
34
  # DATABASE_URL=postgresql://postgres.xxx:password@aws-1-us-west-1.pooler.supabase.com:5432/postgres
35
 
36
+ # SQLite 示例(不指定时自动使用持久化目录 accounts.db)
37
+ # DATABASE_URL=sqlite:////app/persist/accounts.db
38
 
39
  # ============================================
40
  # Git 仓库配置(当 STORAGE_BACKEND=git 时使用)
api/system.py CHANGED
@@ -90,11 +90,15 @@ def create_router(app_version: str) -> APIRouter:
90
  if not openid:
91
  raise HTTPException(status_code=502, detail={"error": "微信登录服务未返回 openid"})
92
 
93
- item, raw_key, created = auth_service.get_or_create_wechat_normal_user(
94
- openid=openid,
95
- nickname=str(user.get("nickname") or "").strip(),
96
- avatar=str(user.get("avatar") or "").strip(),
97
- )
 
 
 
 
98
  return {
99
  "ok": True,
100
  "version": app_version,
 
90
  if not openid:
91
  raise HTTPException(status_code=502, detail={"error": "微信登录服务未返回 openid"})
92
 
93
+ try:
94
+ item, raw_key, created = auth_service.get_or_create_wechat_normal_user(
95
+ openid=openid,
96
+ nickname=str(user.get("nickname") or "").strip(),
97
+ avatar=str(user.get("avatar") or "").strip(),
98
+ )
99
+ except Exception as exc:
100
+ print(f"[wechat-login] failed to save user: {exc}")
101
+ raise HTTPException(status_code=503, detail={"error": f"微信登录已验证,但保存用户失败:{exc}"}) from exc
102
  return {
103
  "ok": True,
104
  "version": app_version,
services/auth_service.py CHANGED
@@ -80,20 +80,21 @@ class AuthService:
80
  "wechat_bound_at": self._clean(raw.get("wechat_bound_at")) or None,
81
  }
82
 
83
- def _load(self) -> list[dict[str, object]]:
84
  try:
85
  items = self.storage.load_auth_keys()
86
- except Exception:
87
- return []
 
88
  if not isinstance(items, list):
89
- return []
90
  return [normalized for item in items if (normalized := self._normalize_item(item)) is not None]
91
 
92
  def _save(self) -> None:
93
  self.storage.save_auth_keys(self._items)
94
 
95
  def _reload_locked(self) -> None:
96
- self._items = self._load()
97
 
98
  @staticmethod
99
  def _public_item(item: dict[str, object]) -> dict[str, object]:
 
80
  "wechat_bound_at": self._clean(raw.get("wechat_bound_at")) or None,
81
  }
82
 
83
+ def _load(self, *, fallback: list[dict[str, object]] | None = None) -> list[dict[str, object]]:
84
  try:
85
  items = self.storage.load_auth_keys()
86
+ except Exception as exc:
87
+ print(f"[auth-service] load auth keys failed: {exc}")
88
+ return [dict(item) for item in (fallback or [])]
89
  if not isinstance(items, list):
90
+ return [dict(item) for item in (fallback or [])]
91
  return [normalized for item in items if (normalized := self._normalize_item(item)) is not None]
92
 
93
  def _save(self) -> None:
94
  self.storage.save_auth_keys(self._items)
95
 
96
  def _reload_locked(self) -> None:
97
+ self._items = self._load(fallback=self._items)
98
 
99
  @staticmethod
100
  def _public_item(item: dict[str, object]) -> dict[str, object]:
services/config.py CHANGED
@@ -10,7 +10,6 @@ import time
10
  from services.storage.base import StorageBackend
11
 
12
  BASE_DIR = Path(__file__).resolve().parents[1]
13
- DATA_DIR = BASE_DIR / "data"
14
  PERSIST_DIR = Path(os.getenv("CHATGPT2API_PERSIST_DIR", "").strip() or "/app/persist")
15
 
16
 
@@ -19,6 +18,7 @@ def _env_path(name: str, fallback: Path) -> Path:
19
  return Path(value) if value else fallback
20
 
21
 
 
22
  CONFIG_FILE = _env_path("CHATGPT2API_CONFIG_FILE", PERSIST_DIR / "config" / "config.json" if PERSIST_DIR.exists() else BASE_DIR / "config.json")
23
  VERSION_FILE = BASE_DIR / "VERSION"
24
  BACKUP_STATE_FILE = DATA_DIR / "backup_state.json"
 
10
  from services.storage.base import StorageBackend
11
 
12
  BASE_DIR = Path(__file__).resolve().parents[1]
 
13
  PERSIST_DIR = Path(os.getenv("CHATGPT2API_PERSIST_DIR", "").strip() or "/app/persist")
14
 
15
 
 
18
  return Path(value) if value else fallback
19
 
20
 
21
+ DATA_DIR = _env_path("CHATGPT2API_DATA_DIR", PERSIST_DIR if PERSIST_DIR.exists() else BASE_DIR / "data")
22
  CONFIG_FILE = _env_path("CHATGPT2API_CONFIG_FILE", PERSIST_DIR / "config" / "config.json" if PERSIST_DIR.exists() else BASE_DIR / "config.json")
23
  VERSION_FILE = BASE_DIR / "VERSION"
24
  BACKUP_STATE_FILE = DATA_DIR / "backup_state.json"
services/storage/database_storage.py CHANGED
@@ -1,6 +1,8 @@
1
  from __future__ import annotations
2
 
3
  import json
 
 
4
  from typing import Any
5
 
6
  from sqlalchemy import Column, String, Text, create_engine, Integer, text
@@ -42,13 +44,21 @@ class DatabaseStorageBackend(StorageBackend):
42
 
43
  def __init__(self, database_url: str):
44
  self.database_url = database_url
 
 
 
 
 
 
45
  self.engine = create_engine(
46
  database_url,
47
  pool_pre_ping=True, # 自动检测连接是否有效
48
  pool_recycle=3600, # 1小时回收连接
 
49
  )
50
  Base.metadata.create_all(self.engine)
51
  self.Session = sessionmaker(bind=self.engine)
 
52
 
53
  def load_accounts(self) -> list[dict[str, Any]]:
54
  """从数据库加载账号数据"""
@@ -68,7 +78,8 @@ class DatabaseStorageBackend(StorageBackend):
68
 
69
  def save_accounts(self, accounts: list[dict[str, Any]]) -> None:
70
  """保存账号数据到数据库"""
71
- self._save_rows(AccountModel, accounts, "access_token")
 
72
 
73
  def load_auth_keys(self) -> list[dict[str, Any]]:
74
  """从数据库加载鉴权密钥数据"""
@@ -76,7 +87,8 @@ class DatabaseStorageBackend(StorageBackend):
76
 
77
  def save_auth_keys(self, auth_keys: list[dict[str, Any]]) -> None:
78
  """保存鉴权密钥数据到数据库"""
79
- self._save_rows(AuthKeyModel, auth_keys, "id", "key_id")
 
80
 
81
  def load_shop_state(self) -> dict[str, Any]:
82
  session = self.Session()
@@ -92,20 +104,21 @@ class DatabaseStorageBackend(StorageBackend):
92
  session.close()
93
 
94
  def save_shop_state(self, state: dict[str, Any]) -> None:
95
- session = self.Session()
96
- try:
97
- row = session.query(ShopStateModel).filter_by(key="default").first()
98
- payload = json.dumps(state, ensure_ascii=False)
99
- if row is None:
100
- session.add(ShopStateModel(key="default", data=payload))
101
- else:
102
- row.data = payload
103
- session.commit()
104
- except Exception as e:
105
- session.rollback()
106
- raise e
107
- finally:
108
- session.close()
 
109
 
110
  def _load_rows(self, model: type[AccountModel] | type[AuthKeyModel]) -> list[dict[str, Any]]:
111
  session = self.Session()
 
1
  from __future__ import annotations
2
 
3
  import json
4
+ from pathlib import Path
5
+ from threading import Lock
6
  from typing import Any
7
 
8
  from sqlalchemy import Column, String, Text, create_engine, Integer, text
 
44
 
45
  def __init__(self, database_url: str):
46
  self.database_url = database_url
47
+ connect_args: dict[str, Any] = {}
48
+ if database_url.startswith("sqlite:///"):
49
+ db_path = database_url.removeprefix("sqlite:///")
50
+ if db_path and db_path != ":memory:":
51
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
52
+ connect_args["check_same_thread"] = False
53
  self.engine = create_engine(
54
  database_url,
55
  pool_pre_ping=True, # 自动检测连接是否有效
56
  pool_recycle=3600, # 1小时回收连接
57
+ connect_args=connect_args,
58
  )
59
  Base.metadata.create_all(self.engine)
60
  self.Session = sessionmaker(bind=self.engine)
61
+ self._lock = Lock()
62
 
63
  def load_accounts(self) -> list[dict[str, Any]]:
64
  """从数据库加载账号数据"""
 
78
 
79
  def save_accounts(self, accounts: list[dict[str, Any]]) -> None:
80
  """保存账号数据到数据库"""
81
+ with self._lock:
82
+ self._save_rows(AccountModel, accounts, "access_token")
83
 
84
  def load_auth_keys(self) -> list[dict[str, Any]]:
85
  """从数据库加载鉴权密钥数据"""
 
87
 
88
  def save_auth_keys(self, auth_keys: list[dict[str, Any]]) -> None:
89
  """保存鉴权密钥数据到数据库"""
90
+ with self._lock:
91
+ self._save_rows(AuthKeyModel, auth_keys, "id", "key_id")
92
 
93
  def load_shop_state(self) -> dict[str, Any]:
94
  session = self.Session()
 
104
  session.close()
105
 
106
  def save_shop_state(self, state: dict[str, Any]) -> None:
107
+ with self._lock:
108
+ session = self.Session()
109
+ try:
110
+ row = session.query(ShopStateModel).filter_by(key="default").first()
111
+ payload = json.dumps(state, ensure_ascii=False)
112
+ if row is None:
113
+ session.add(ShopStateModel(key="default", data=payload))
114
+ else:
115
+ row.data = payload
116
+ session.commit()
117
+ except Exception as e:
118
+ session.rollback()
119
+ raise e
120
+ finally:
121
+ session.close()
122
 
123
  def _load_rows(self, model: type[AccountModel] | type[AuthKeyModel]) -> list[dict[str, Any]]:
124
  session = self.Session()
services/storage/factory.py CHANGED
@@ -10,6 +10,81 @@ from services.storage.git_storage import GitStorageBackend
10
  from services.storage.json_storage import JSONStorageBackend
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def create_storage_backend(data_dir: Path) -> StorageBackend:
14
  """
15
  根据环境变量创建存储后端
@@ -23,7 +98,7 @@ def create_storage_backend(data_dir: Path) -> StorageBackend:
23
  - GIT_FILE_PATH: Git 仓库中的文件路径 (默认 accounts.json)
24
  - GIT_CACHE_DIR: Git 本地缓存目录 (默认系统临时目录)
25
  """
26
- backend_type = os.getenv("STORAGE_BACKEND", "json").lower().strip()
27
 
28
  print(f"[storage] Initializing storage backend: {backend_type}")
29
 
@@ -41,12 +116,15 @@ def create_storage_backend(data_dir: Path) -> StorageBackend:
41
 
42
  if not database_url:
43
  # 如果没有指定 DATABASE_URL,使用本地 SQLite
44
- database_url = f"sqlite:///{data_dir / 'accounts.db'}"
45
  print(f"[storage] No DATABASE_URL provided, using local SQLite: {database_url}")
46
  else:
47
  print(f"[storage] Using database storage: {_mask_password(database_url)}")
48
-
49
- return DatabaseStorageBackend(database_url)
 
 
 
50
 
51
  elif backend_type == "git":
52
  # Git 仓库存储
 
10
  from services.storage.json_storage import JSONStorageBackend
11
 
12
 
13
+ def _default_backend_type(data_dir: Path) -> str:
14
+ if os.getenv("STORAGE_BACKEND") is not None:
15
+ return os.getenv("STORAGE_BACKEND", "json").lower().strip()
16
+ persist_dir = Path(os.getenv("CHATGPT2API_PERSIST_DIR", "").strip() or "/app/persist")
17
+ if persist_dir.exists() or str(data_dir).startswith(str(persist_dir)):
18
+ return "sqlite"
19
+ return "json"
20
+
21
+
22
+ def _sqlite_database_url(data_dir: Path) -> str:
23
+ return f"sqlite:///{data_dir / 'accounts.db'}"
24
+
25
+
26
+ def _is_database_empty(backend: DatabaseStorageBackend) -> bool:
27
+ return not backend.load_accounts() and not backend.load_auth_keys() and not backend.load_shop_state()
28
+
29
+
30
+ def _build_git_backend_from_env() -> GitStorageBackend | None:
31
+ repo_url = os.getenv("GIT_REPO_URL", "").strip()
32
+ token = os.getenv("GIT_TOKEN", "").strip()
33
+ if not repo_url:
34
+ return None
35
+ branch = os.getenv("GIT_BRANCH", "main").strip()
36
+ file_path = os.getenv("GIT_FILE_PATH", "accounts.json").strip()
37
+ auth_keys_file_path = os.getenv("GIT_AUTH_KEYS_FILE_PATH", "auth_keys.json").strip()
38
+ shop_state_file_path = os.getenv("GIT_SHOP_STATE_FILE_PATH", "shop_state.json").strip()
39
+ cache_dir = Path(os.getenv("GIT_CACHE_DIR", "").strip() or (Path(tempfile.gettempdir()) / "chatgpt2api_git_cache"))
40
+ return GitStorageBackend(
41
+ repo_url=repo_url,
42
+ token=token,
43
+ branch=branch,
44
+ file_path=file_path,
45
+ auth_keys_file_path=auth_keys_file_path,
46
+ shop_state_file_path=shop_state_file_path,
47
+ local_cache_dir=cache_dir,
48
+ )
49
+
50
+
51
+ def _migrate_git_to_database_if_empty(backend: DatabaseStorageBackend) -> None:
52
+ if os.getenv("CHATGPT2API_SKIP_STORAGE_MIGRATION", "").strip().lower() in {"1", "true", "yes", "on"}:
53
+ return
54
+ try:
55
+ if not _is_database_empty(backend):
56
+ return
57
+ except Exception as exc:
58
+ print(f"[storage] skip migration, database check failed: {exc}")
59
+ return
60
+
61
+ git_backend = _build_git_backend_from_env()
62
+ if git_backend is None:
63
+ return
64
+
65
+ try:
66
+ accounts = git_backend.load_accounts()
67
+ auth_keys = git_backend.load_auth_keys()
68
+ shop_state = git_backend.load_shop_state()
69
+ except Exception as exc:
70
+ print(f"[storage] Git to SQLite migration skipped: {exc}")
71
+ return
72
+
73
+ if not accounts and not auth_keys and not shop_state:
74
+ return
75
+
76
+ print(
77
+ "[storage] Migrating Git data to SQLite: "
78
+ f"{len(accounts)} accounts, {len(auth_keys)} auth keys, shop_state={bool(shop_state)}"
79
+ )
80
+ if accounts:
81
+ backend.save_accounts(accounts)
82
+ if auth_keys:
83
+ backend.save_auth_keys(auth_keys)
84
+ if shop_state:
85
+ backend.save_shop_state(shop_state)
86
+
87
+
88
  def create_storage_backend(data_dir: Path) -> StorageBackend:
89
  """
90
  根据环境变量创建存储后端
 
98
  - GIT_FILE_PATH: Git 仓库中的文件路径 (默认 accounts.json)
99
  - GIT_CACHE_DIR: Git 本地缓存目录 (默认系统临时目录)
100
  """
101
+ backend_type = _default_backend_type(data_dir)
102
 
103
  print(f"[storage] Initializing storage backend: {backend_type}")
104
 
 
116
 
117
  if not database_url:
118
  # 如果没有指定 DATABASE_URL,使用本地 SQLite
119
+ database_url = _sqlite_database_url(data_dir)
120
  print(f"[storage] No DATABASE_URL provided, using local SQLite: {database_url}")
121
  else:
122
  print(f"[storage] Using database storage: {_mask_password(database_url)}")
123
+
124
+ backend = DatabaseStorageBackend(database_url)
125
+ if backend_type in {"sqlite", "database"}:
126
+ _migrate_git_to_database_if_empty(backend)
127
+ return backend
128
 
129
  elif backend_type == "git":
130
  # Git 仓库存储
services/storage/git_storage.py CHANGED
@@ -4,6 +4,7 @@ import json
4
  import shutil
5
  import tempfile
6
  from pathlib import Path
 
7
  from typing import Any
8
 
9
  from git import Repo
@@ -31,6 +32,7 @@ class GitStorageBackend(StorageBackend):
31
  self.file_path = file_path
32
  self.auth_keys_file_path = auth_keys_file_path
33
  self.shop_state_file_path = shop_state_file_path
 
34
 
35
  # 本地缓存目录
36
  if local_cache_dir is None:
@@ -61,6 +63,28 @@ class GitStorageBackend(StorageBackend):
61
 
62
  return repo_url
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def _clone_or_pull(self) -> Repo:
65
  """克隆或拉取仓库"""
66
  repo_path = self.local_cache_dir / "repo"
@@ -74,15 +98,14 @@ class GitStorageBackend(StorageBackend):
74
  return repo
75
  except GitCommandError:
76
  # 拉取失败,删除重新克隆
77
- shutil.rmtree(repo_path)
78
-
 
 
 
 
79
  # 克隆仓库
80
- repo = Repo.clone_from(
81
- self.auth_repo_url,
82
- repo_path,
83
- branch=self.branch,
84
- )
85
- return repo
86
 
87
  def load_accounts(self) -> list[dict[str, Any]]:
88
  """从 Git 仓库加载账号数据"""
@@ -139,31 +162,40 @@ class GitStorageBackend(StorageBackend):
139
  return data if isinstance(data, list) else []
140
 
141
  def _load_json_value(self, file_path: str) -> Any:
142
- repo = self._clone_or_pull()
143
- file_full_path = Path(repo.working_dir) / file_path
144
- if not file_full_path.exists():
145
- return None
146
- try:
147
- content = file_full_path.read_text(encoding="utf-8")
148
- return json.loads(content)
149
- except (json.JSONDecodeError, UnicodeDecodeError) as e:
150
- print(f"[git-storage] Failed to load {file_path}: {e}, returning empty data")
151
- return None
 
152
 
153
  def _save_json_file(self, file_path: str, items: Any, message: str) -> None:
154
- repo = self._clone_or_pull()
155
- file_full_path = Path(repo.working_dir) / file_path
156
- file_full_path.parent.mkdir(parents=True, exist_ok=True)
157
- file_full_path.write_text(
158
- json.dumps(items, ensure_ascii=False, indent=2) + "\n",
159
- encoding="utf-8",
160
- )
161
- repo.index.add([file_path])
162
- if repo.is_dirty():
163
- # The data repository only stores JSON state; deployment volumes may
164
- # preserve non-executable sample hooks, so skip hooks for reliability.
165
- repo.index.commit(message, skip_hooks=True)
166
- repo.remote("origin").push(self.branch)
 
 
 
 
 
 
 
 
167
 
168
  def health_check(self) -> dict[str, Any]:
169
  """健康检查"""
 
4
  import shutil
5
  import tempfile
6
  from pathlib import Path
7
+ from threading import Lock
8
  from typing import Any
9
 
10
  from git import Repo
 
32
  self.file_path = file_path
33
  self.auth_keys_file_path = auth_keys_file_path
34
  self.shop_state_file_path = shop_state_file_path
35
+ self._lock = Lock()
36
 
37
  # 本地缓存目录
38
  if local_cache_dir is None:
 
63
 
64
  return repo_url
65
 
66
+ def _safe_remove_repo(self, repo_path: Path) -> None:
67
+ try:
68
+ if repo_path.exists():
69
+ shutil.rmtree(repo_path)
70
+ except Exception as exc:
71
+ print(f"[git-storage] failed to remove broken cache {repo_path}: {exc}")
72
+
73
+ def _clone_repo(self, repo_path: Path) -> Repo:
74
+ for attempt in range(2):
75
+ try:
76
+ return Repo.clone_from(
77
+ self.auth_repo_url,
78
+ repo_path,
79
+ branch=self.branch,
80
+ )
81
+ except Exception:
82
+ self._safe_remove_repo(repo_path)
83
+ if attempt == 0:
84
+ continue
85
+ raise
86
+ raise RuntimeError("git clone failed")
87
+
88
  def _clone_or_pull(self) -> Repo:
89
  """克隆或拉取仓库"""
90
  repo_path = self.local_cache_dir / "repo"
 
98
  return repo
99
  except GitCommandError:
100
  # 拉取失败,删除重新克隆
101
+ self._safe_remove_repo(repo_path)
102
+ except Exception:
103
+ self._safe_remove_repo(repo_path)
104
+ elif repo_path.exists():
105
+ self._safe_remove_repo(repo_path)
106
+
107
  # 克隆仓库
108
+ return self._clone_repo(repo_path)
 
 
 
 
 
109
 
110
  def load_accounts(self) -> list[dict[str, Any]]:
111
  """从 Git 仓库加载账号数据"""
 
162
  return data if isinstance(data, list) else []
163
 
164
  def _load_json_value(self, file_path: str) -> Any:
165
+ with self._lock:
166
+ repo = self._clone_or_pull()
167
+ file_full_path = Path(repo.working_dir) / file_path
168
+ if not file_full_path.exists():
169
+ return None
170
+ try:
171
+ content = file_full_path.read_text(encoding="utf-8")
172
+ return json.loads(content)
173
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
174
+ print(f"[git-storage] Failed to load {file_path}: {e}, returning empty data")
175
+ return None
176
 
177
  def _save_json_file(self, file_path: str, items: Any, message: str) -> None:
178
+ with self._lock:
179
+ payload = json.dumps(items, ensure_ascii=False, indent=2) + "\n"
180
+ for attempt in range(2):
181
+ repo = self._clone_or_pull()
182
+ file_full_path = Path(repo.working_dir) / file_path
183
+ file_full_path.parent.mkdir(parents=True, exist_ok=True)
184
+ file_full_path.write_text(payload, encoding="utf-8")
185
+ repo.index.add([file_path])
186
+ if not repo.is_dirty():
187
+ return
188
+ try:
189
+ # The data repository only stores JSON state; deployment volumes may
190
+ # preserve non-executable sample hooks, so skip hooks for reliability.
191
+ repo.index.commit(message, skip_hooks=True)
192
+ repo.remote("origin").push(self.branch)
193
+ return
194
+ except GitCommandError:
195
+ if attempt == 0:
196
+ self._safe_remove_repo(self.local_cache_dir / "repo")
197
+ continue
198
+ raise
199
 
200
  def health_check(self) -> dict[str, Any]:
201
  """健康检查"""