web-checkin 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ """Web Check-in Hub:WorkBuddy / Trae / Qoder 多账号签到与用量查询。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.1.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """支持 ``python -m web_checkin`` 启动服务。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -0,0 +1,282 @@
1
+ """统一账号存储:WorkBuddy 与 Trae 共用同一个 SQLite 数据库。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import time
9
+ from typing import Any, Iterable
10
+
11
+ from .paths import default_db_path
12
+
13
+
14
+ DEFAULT_HUB_DB = str(default_db_path())
15
+
16
+
17
+ class AccountStore:
18
+ def __init__(self, db_path: str = DEFAULT_HUB_DB):
19
+ self.db_path = db_path
20
+ parent = os.path.dirname(os.path.abspath(db_path))
21
+ if parent:
22
+ os.makedirs(parent, exist_ok=True)
23
+ self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
24
+ self._conn.row_factory = sqlite3.Row
25
+ self._conn.execute("PRAGMA foreign_keys = ON")
26
+ self._conn.execute("PRAGMA busy_timeout = 5000")
27
+ self._init_schema()
28
+ try:
29
+ os.chmod(self.db_path, 0o600)
30
+ except OSError:
31
+ pass
32
+
33
+ def _init_schema(self) -> None:
34
+ self._conn.executescript(
35
+ """
36
+ CREATE TABLE IF NOT EXISTS accounts (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ provider TEXT NOT NULL,
39
+ external_id TEXT NOT NULL,
40
+ display_name TEXT NOT NULL DEFAULT '',
41
+ label TEXT NOT NULL DEFAULT '',
42
+ endpoint TEXT NOT NULL,
43
+ credential TEXT NOT NULL,
44
+ enabled INTEGER NOT NULL DEFAULT 1,
45
+ last_status TEXT,
46
+ last_status_at INTEGER,
47
+ last_checkin_at INTEGER,
48
+ last_checkin_ok INTEGER,
49
+ last_checkin_message TEXT,
50
+ created_at INTEGER NOT NULL,
51
+ updated_at INTEGER NOT NULL,
52
+ UNIQUE(provider, external_id)
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS checkin_logs (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ account_id INTEGER NOT NULL,
58
+ provider TEXT NOT NULL,
59
+ action TEXT NOT NULL,
60
+ ok INTEGER NOT NULL,
61
+ code INTEGER,
62
+ message TEXT,
63
+ payload_json TEXT,
64
+ created_at INTEGER NOT NULL,
65
+ FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS settings (
69
+ key TEXT PRIMARY KEY,
70
+ value TEXT NOT NULL
71
+ );
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_checkin_logs_account_time
74
+ ON checkin_logs(account_id, created_at);
75
+ """
76
+ )
77
+ columns = {row[1] for row in self._conn.execute("PRAGMA table_info(accounts)").fetchall()}
78
+ if "schedule_time" not in columns:
79
+ self._conn.execute("ALTER TABLE accounts ADD COLUMN schedule_time TEXT NOT NULL DEFAULT '09:00'")
80
+ if "schedule_enabled" not in columns:
81
+ self._conn.execute("ALTER TABLE accounts ADD COLUMN schedule_enabled INTEGER NOT NULL DEFAULT 1")
82
+ if "schedule_last_date" not in columns:
83
+ self._conn.execute("ALTER TABLE accounts ADD COLUMN schedule_last_date TEXT")
84
+ self._conn.commit()
85
+
86
+ def close(self) -> None:
87
+ self._conn.close()
88
+
89
+ @staticmethod
90
+ def _public_account(row: sqlite3.Row) -> dict[str, Any]:
91
+ data = dict(row)
92
+ data.pop("credential", None)
93
+ data["enabled"] = bool(data.get("enabled"))
94
+ data["schedule_enabled"] = bool(data.get("schedule_enabled"))
95
+ try:
96
+ data["last_status"] = json.loads(data["last_status"]) if data.get("last_status") else None
97
+ except Exception:
98
+ data["last_status"] = None
99
+ return data
100
+
101
+ def upsert_account(
102
+ self,
103
+ *,
104
+ provider: str,
105
+ external_id: str,
106
+ display_name: str,
107
+ endpoint: str,
108
+ credential: str,
109
+ label: str = "",
110
+ ) -> int:
111
+ timestamp = int(time.time())
112
+ self._conn.execute(
113
+ """
114
+ INSERT INTO accounts (
115
+ provider, external_id, display_name, label, endpoint, credential,
116
+ enabled, created_at, updated_at
117
+ ) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)
118
+ ON CONFLICT(provider, external_id) DO UPDATE SET
119
+ display_name = CASE WHEN excluded.display_name = '' THEN accounts.display_name ELSE excluded.display_name END,
120
+ label = CASE WHEN excluded.label = '' THEN accounts.label ELSE excluded.label END,
121
+ endpoint = excluded.endpoint,
122
+ credential = excluded.credential,
123
+ enabled = 1,
124
+ updated_at = excluded.updated_at
125
+ """,
126
+ (provider, external_id, display_name, label, endpoint, credential, timestamp, timestamp),
127
+ )
128
+ self._conn.commit()
129
+ row = self._conn.execute(
130
+ "SELECT id FROM accounts WHERE provider = ? AND external_id = ?",
131
+ (provider, external_id),
132
+ ).fetchone()
133
+ return int(row["id"])
134
+
135
+ def list_accounts(self, provider: str | None = None, enabled_only: bool = False) -> list[dict[str, Any]]:
136
+ sql = "SELECT * FROM accounts"
137
+ conditions: list[str] = []
138
+ params: list[Any] = []
139
+ if provider:
140
+ conditions.append("provider = ?")
141
+ params.append(provider)
142
+ if enabled_only:
143
+ conditions.append("enabled = 1")
144
+ if conditions:
145
+ sql += " WHERE " + " AND ".join(conditions)
146
+ sql += " ORDER BY CASE provider WHEN 'workbuddy' THEN 0 WHEN 'trae' THEN 1 ELSE 2 END, created_at, id"
147
+ return [self._public_account(row) for row in self._conn.execute(sql, params).fetchall()]
148
+
149
+ def list_account_rows(self, enabled_only: bool = False) -> list[sqlite3.Row]:
150
+ """仅供后端平台调用使用,返回包含凭据的内部账号记录。"""
151
+ sql = "SELECT * FROM accounts"
152
+ if enabled_only:
153
+ sql += " WHERE enabled = 1"
154
+ sql += " ORDER BY CASE provider WHEN 'workbuddy' THEN 0 WHEN 'trae' THEN 1 ELSE 2 END, created_at, id"
155
+ return self._conn.execute(sql).fetchall()
156
+
157
+ def get_account(self, account_id: int) -> sqlite3.Row | None:
158
+ return self._conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone()
159
+
160
+ def set_enabled(self, account_id: int, enabled: bool) -> bool:
161
+ cursor = self._conn.execute(
162
+ "UPDATE accounts SET enabled = ?, updated_at = ? WHERE id = ?",
163
+ (int(enabled), int(time.time()), account_id),
164
+ )
165
+ self._conn.commit()
166
+ return cursor.rowcount == 1
167
+
168
+ def update_schedule(self, account_id: int, *, enabled: bool, schedule_time: str) -> bool:
169
+ cursor = self._conn.execute(
170
+ """
171
+ UPDATE accounts
172
+ SET schedule_enabled = ?, schedule_time = ?, updated_at = ?
173
+ WHERE id = ?
174
+ """,
175
+ (int(enabled), schedule_time, int(time.time()), account_id),
176
+ )
177
+ self._conn.commit()
178
+ return cursor.rowcount == 1
179
+
180
+ def mark_schedule_run(self, account_id: int, run_date: str) -> None:
181
+ self._conn.execute(
182
+ "UPDATE accounts SET schedule_last_date = ?, updated_at = ? WHERE id = ?",
183
+ (run_date, int(time.time()), account_id),
184
+ )
185
+ self._conn.commit()
186
+
187
+ def delete_account(self, account_id: int) -> bool:
188
+ cursor = self._conn.execute("DELETE FROM accounts WHERE id = ?", (account_id,))
189
+ self._conn.commit()
190
+ return cursor.rowcount == 1
191
+
192
+ def update_account(self, account_id: int, *, credential: str, display_name: str | None = None) -> None:
193
+ self._conn.execute(
194
+ "UPDATE accounts SET credential = ?, display_name = COALESCE(?, display_name), updated_at = ? WHERE id = ?",
195
+ (credential, display_name, int(time.time()), account_id),
196
+ )
197
+ self._conn.commit()
198
+
199
+ def update_status(self, account_id: int, status: dict[str, Any] | None) -> None:
200
+ self._conn.execute(
201
+ "UPDATE accounts SET last_status = ?, last_status_at = ?, updated_at = ? WHERE id = ?",
202
+ (
203
+ json.dumps(status, ensure_ascii=False, sort_keys=True) if status is not None else None,
204
+ int(time.time()),
205
+ int(time.time()),
206
+ account_id,
207
+ ),
208
+ )
209
+ self._conn.commit()
210
+
211
+ def update_checkin(self, account_id: int, *, ok: bool, message: str) -> None:
212
+ self._conn.execute(
213
+ """
214
+ UPDATE accounts
215
+ SET last_checkin_at = ?, last_checkin_ok = ?, last_checkin_message = ?, updated_at = ?
216
+ WHERE id = ?
217
+ """,
218
+ (int(time.time()), int(ok), message[:500], int(time.time()), account_id),
219
+ )
220
+ self._conn.commit()
221
+
222
+ def add_log(
223
+ self,
224
+ account_id: int,
225
+ provider: str,
226
+ *,
227
+ action: str,
228
+ ok: bool,
229
+ code: int | None = None,
230
+ message: str = "",
231
+ payload: Any = None,
232
+ ) -> None:
233
+ self._conn.execute(
234
+ """
235
+ INSERT INTO checkin_logs
236
+ (account_id, provider, action, ok, code, message, payload_json, created_at)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
238
+ """,
239
+ (
240
+ account_id,
241
+ provider,
242
+ action,
243
+ int(ok),
244
+ code,
245
+ message[:500],
246
+ json.dumps(payload, ensure_ascii=False, sort_keys=True) if payload is not None else None,
247
+ int(time.time()),
248
+ ),
249
+ )
250
+ self._conn.commit()
251
+
252
+ def recent_logs(self, limit: int = 80) -> list[dict[str, Any]]:
253
+ rows = self._conn.execute(
254
+ """
255
+ SELECT l.id, l.account_id, a.display_name, a.label, l.provider, l.action,
256
+ l.ok, l.code, l.message, l.created_at
257
+ FROM checkin_logs l
258
+ LEFT JOIN accounts a ON a.id = l.account_id
259
+ ORDER BY l.id DESC LIMIT ?
260
+ """,
261
+ (max(1, min(limit, 500)),),
262
+ ).fetchall()
263
+ result = []
264
+ for row in rows:
265
+ data = dict(row)
266
+ data["ok"] = bool(data["ok"])
267
+ result.append(data)
268
+ return result
269
+
270
+ def get_setting(self, key: str, default: str | None = None) -> str | None:
271
+ row = self._conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
272
+ return row["value"] if row else default
273
+
274
+ def set_setting(self, key: str, value: str) -> None:
275
+ self._conn.execute(
276
+ "INSERT INTO settings(key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
277
+ (key, value),
278
+ )
279
+ self._conn.commit()
280
+
281
+ def all_settings(self) -> dict[str, str]:
282
+ return {row["key"]: row["value"] for row in self._conn.execute("SELECT key, value FROM settings")}