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.
- web_checkin/__init__.py +7 -0
- web_checkin/__main__.py +8 -0
- web_checkin/account_store.py +282 -0
- web_checkin/app.py +474 -0
- web_checkin/cli.py +43 -0
- web_checkin/paths.py +72 -0
- web_checkin/qoder_checkin.py +147 -0
- web_checkin/qoder_login.py +273 -0
- web_checkin/qoder_tool.py +273 -0
- web_checkin/static/assets/index-CekGxNfP.js +10 -0
- web_checkin/static/assets/index-DBqTs3Tn.css +2 -0
- web_checkin/static/index.html +14 -0
- web_checkin/trae_tool.py +705 -0
- web_checkin/trae_usage.py +182 -0
- web_checkin/workbuddy_checkin.py +155 -0
- web_checkin/workbuddy_common.py +529 -0
- web_checkin/workbuddy_login.py +138 -0
- web_checkin/workbuddy_tool.py +537 -0
- web_checkin/workbuddy_usage.py +177 -0
- web_checkin-1.1.0.dist-info/METADATA +325 -0
- web_checkin-1.1.0.dist-info/RECORD +25 -0
- web_checkin-1.1.0.dist-info/WHEEL +5 -0
- web_checkin-1.1.0.dist-info/entry_points.txt +2 -0
- web_checkin-1.1.0.dist-info/licenses/LICENSE +21 -0
- web_checkin-1.1.0.dist-info/top_level.txt +1 -0
web_checkin/__init__.py
ADDED
web_checkin/__main__.py
ADDED
|
@@ -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")}
|