473 lines
17 KiB
Python
473 lines
17 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
"""AgentCheckin engine: account orchestration, check-in runs, notifications."""
|
|||
|
|
import json
|
|||
|
|
import secrets
|
|||
|
|
import time
|
|||
|
|
import urllib.request
|
|||
|
|
|
|||
|
|
from common import (load_accounts, save_accounts, load_settings, load_state,
|
|||
|
|
save_state, log, now_ts, today_str, fmt_ts, fmt_now,
|
|||
|
|
public_account, USER_AGENT, _SSL_CTX)
|
|||
|
|
import workbuddy
|
|||
|
|
import trae
|
|||
|
|
import qoder
|
|||
|
|
|
|||
|
|
# result statuses considered "handled" (no retry needed)
|
|||
|
|
DONE_STATUSES = {"ok", "ok_unverified", "already", "inactive"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def new_account_id(platform):
|
|||
|
|
return "%s_%s" % (platform, secrets.token_hex(4))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def find_account(accounts, account_id):
|
|||
|
|
for acc in accounts:
|
|||
|
|
if acc.get("id") == account_id:
|
|||
|
|
return acc
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Account creation
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def add_workbuddy_manual(name, access_token, refresh_token="", domain="codebuddy.cn"):
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acc = {
|
|||
|
|
"id": new_account_id("wb"),
|
|||
|
|
"platform": "workbuddy",
|
|||
|
|
"name": name or "WorkBuddy 账号",
|
|||
|
|
"enabled": True,
|
|||
|
|
"created_at": fmt_now(),
|
|||
|
|
"auth": {
|
|||
|
|
"accessToken": access_token,
|
|||
|
|
"refreshToken": refresh_token,
|
|||
|
|
"expiresAt": 0,
|
|||
|
|
"domain": domain,
|
|||
|
|
},
|
|||
|
|
"account": {},
|
|||
|
|
"last_result": None,
|
|||
|
|
}
|
|||
|
|
accounts.append(acc)
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
log("added workbuddy account %s (%s)" % (acc["id"], acc["name"]))
|
|||
|
|
return acc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_workbuddy_oauth(payload):
|
|||
|
|
"""payload from workbuddy.poll_oauth(): {accessToken, refreshToken, ...}."""
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acct_info = payload.get("account") or {}
|
|||
|
|
domain = payload.get("domain") or "codebuddy.cn"
|
|||
|
|
acc = {
|
|||
|
|
"id": new_account_id("wb"),
|
|||
|
|
"platform": "workbuddy",
|
|||
|
|
"name": acct_info.get("nickname") or "WorkBuddy 账号",
|
|||
|
|
"enabled": True,
|
|||
|
|
"created_at": fmt_now(),
|
|||
|
|
"auth": {
|
|||
|
|
"accessToken": payload.get("accessToken", ""),
|
|||
|
|
"refreshToken": payload.get("refreshToken", ""),
|
|||
|
|
"expiresAt": payload.get("expiresAt") or 0,
|
|||
|
|
"domain": domain,
|
|||
|
|
},
|
|||
|
|
"account": {
|
|||
|
|
"uid": acct_info.get("uid", ""),
|
|||
|
|
"nickname": acct_info.get("nickname", ""),
|
|||
|
|
"enterpriseId": acct_info.get("enterpriseId", ""),
|
|||
|
|
},
|
|||
|
|
"last_result": None,
|
|||
|
|
}
|
|||
|
|
accounts.append(acc)
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
log("added workbuddy account via oauth: %s (%s)" % (acc["id"], acc["name"]))
|
|||
|
|
return acc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_trae(name, access_token, refresh_token="", device_id_str=""):
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acc = {
|
|||
|
|
"id": new_account_id("trae"),
|
|||
|
|
"platform": "trae",
|
|||
|
|
"name": name or "TRAE 账号",
|
|||
|
|
"enabled": True,
|
|||
|
|
"created_at": fmt_now(),
|
|||
|
|
"auth": {
|
|||
|
|
"accessToken": access_token,
|
|||
|
|
"refreshToken": refresh_token,
|
|||
|
|
},
|
|||
|
|
"device_id": device_id_str or trae.stable_device_id(new_account_id("trae")),
|
|||
|
|
"last_result": None,
|
|||
|
|
}
|
|||
|
|
accounts.append(acc)
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
log("added trae account %s (%s)" % (acc["id"], acc["name"]))
|
|||
|
|
return acc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_qoder(name, access_token, refresh_token="", device=None, domain="openapi.qoder.com.cn"):
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acc = {
|
|||
|
|
"id": new_account_id("qoder"),
|
|||
|
|
"platform": "qoder",
|
|||
|
|
"name": name or "Qoder 账号",
|
|||
|
|
"enabled": True,
|
|||
|
|
"created_at": fmt_now(),
|
|||
|
|
"auth": {
|
|||
|
|
"accessToken": access_token,
|
|||
|
|
"refreshToken": refresh_token,
|
|||
|
|
"expiresAt": 0,
|
|||
|
|
"domain": domain,
|
|||
|
|
},
|
|||
|
|
"device": {k: str(v) for k, v in (device or {}).items()
|
|||
|
|
if k in qoder.DEVICE_DEFAULTS},
|
|||
|
|
"last_result": None,
|
|||
|
|
}
|
|||
|
|
accounts.append(acc)
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
log("added qoder account %s (%s)" % (acc["id"], acc["name"]))
|
|||
|
|
return acc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def delete_account(account_id):
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
left = [a for a in accounts if a.get("id") != account_id]
|
|||
|
|
save_accounts(left)
|
|||
|
|
return len(left) != len(accounts)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def update_account(account_id, **fields):
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acc = find_account(accounts, account_id)
|
|||
|
|
if not acc:
|
|||
|
|
return None
|
|||
|
|
for k, v in fields.items():
|
|||
|
|
if k in ("name", "enabled"):
|
|||
|
|
acc[k] = v
|
|||
|
|
elif k == "accessToken" and v:
|
|||
|
|
acc.setdefault("auth", {})["accessToken"] = v
|
|||
|
|
elif k == "refreshToken" and v:
|
|||
|
|
acc.setdefault("auth", {})["refreshToken"] = v
|
|||
|
|
elif k == "domain" and v:
|
|||
|
|
acc.setdefault("auth", {})["domain"] = v
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
return acc
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Check-in runs
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def run_account(acc, force=False):
|
|||
|
|
"""Run check-in for one account. Returns result dict."""
|
|||
|
|
platform = acc.get("platform")
|
|||
|
|
started = fmt_now()
|
|||
|
|
if platform == "workbuddy":
|
|||
|
|
result = _run_workbuddy(acc)
|
|||
|
|
elif platform == "trae":
|
|||
|
|
result = trae.checkin_once(acc)
|
|||
|
|
elif platform == "qoder":
|
|||
|
|
result = qoder.checkin_once(acc)
|
|||
|
|
else:
|
|||
|
|
result = {"status": "fail", "message": "未知平台 %s" % platform, "points": 0}
|
|||
|
|
result["time"] = started
|
|||
|
|
result["finished_at"] = fmt_now()
|
|||
|
|
|
|||
|
|
acc["last_result"] = result
|
|||
|
|
acc["last_run"] = now_ts()
|
|||
|
|
if result.get("status") == "no_session":
|
|||
|
|
acc["need_reauth"] = True
|
|||
|
|
elif result.get("status") in DONE_STATUSES:
|
|||
|
|
acc["need_reauth"] = False
|
|||
|
|
_reload(acc)
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _reload(acc):
|
|||
|
|
"""Persist the mutated account back into the accounts file."""
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
for i, a in enumerate(accounts):
|
|||
|
|
if a.get("id") == acc.get("id"):
|
|||
|
|
accounts[i] = acc
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
accounts.append(acc)
|
|||
|
|
save_accounts(accounts)
|
|||
|
|
return accounts
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run_workbuddy(acc):
|
|||
|
|
auth = acc.get("auth", {})
|
|||
|
|
if not auth.get("accessToken"):
|
|||
|
|
return {"status": "no_auth", "message": "缺少 accessToken,请重新授权", "points": 0}
|
|||
|
|
if workbuddy.is_global(auth):
|
|||
|
|
return {"status": "inactive", "message": "Global(workbuddy.ai)账号不支持每日签到", "points": 0}
|
|||
|
|
# refresh token when close to expiry
|
|||
|
|
if workbuddy.token_expiring(auth) and auth.get("refreshToken"):
|
|||
|
|
ok, msg = workbuddy.refresh_token(acc)
|
|||
|
|
if not ok:
|
|||
|
|
log("workbuddy %s token refresh: %s" % (acc.get("id"), msg), "WARN")
|
|||
|
|
if "失效" in msg or "过期" in msg:
|
|||
|
|
return {"status": "no_session", "message": msg, "points": 0}
|
|||
|
|
else:
|
|||
|
|
_reload(acc)
|
|||
|
|
# status first (idempotent)
|
|||
|
|
checked, detail, err = workbuddy.query_status(acc)
|
|||
|
|
if err == "no_session":
|
|||
|
|
return {"status": "no_session", "message": "登录态已失效,请重新授权", "points": 0}
|
|||
|
|
if checked is True:
|
|||
|
|
credits = None
|
|||
|
|
if detail:
|
|||
|
|
credits = detail.get("total_credits")
|
|||
|
|
if credits is None:
|
|||
|
|
summary, _ = workbuddy.fetch_credits(acc)
|
|||
|
|
credits = (summary or {}).get("remain")
|
|||
|
|
return {"status": "already", "message": "今日已签到", "points": 0,
|
|||
|
|
"credits": credits, "detail": detail or {}}
|
|||
|
|
# claim
|
|||
|
|
status, message, points = workbuddy.perform_checkin(acc)
|
|||
|
|
if status == "no_session":
|
|||
|
|
return {"status": "no_session", "message": message, "points": 0}
|
|||
|
|
if status == "ok":
|
|||
|
|
# verify
|
|||
|
|
for _ in range(3):
|
|||
|
|
time.sleep(2)
|
|||
|
|
v_checked, v_detail, v_err = workbuddy.query_status(acc)
|
|||
|
|
if v_err == "no_session":
|
|||
|
|
break
|
|||
|
|
if v_checked is True:
|
|||
|
|
credits = None
|
|||
|
|
if v_detail:
|
|||
|
|
credits = v_detail.get("total_credits")
|
|||
|
|
if credits is None:
|
|||
|
|
summary, _ = workbuddy.fetch_credits(acc)
|
|||
|
|
credits = (summary or {}).get("remain")
|
|||
|
|
return {"status": "ok", "message": "签到成功(已回查确认)",
|
|||
|
|
"points": points, "credits": credits, "detail": v_detail or {}}
|
|||
|
|
if v_checked is False:
|
|||
|
|
break
|
|||
|
|
return {"status": "ok_unverified", "message": "领取成功(+%s)但回查未确认" % points,
|
|||
|
|
"points": points}
|
|||
|
|
return {"status": status, "message": message, "points": points}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_all(account_id=None, reason="manual"):
|
|||
|
|
"""Run check-in for all enabled accounts (or one). Returns summary dict."""
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
targets = [a for a in accounts if a.get("enabled")]
|
|||
|
|
if account_id:
|
|||
|
|
targets = [a for a in targets if a.get("id") == account_id]
|
|||
|
|
results = {}
|
|||
|
|
for acc in targets:
|
|||
|
|
try:
|
|||
|
|
res = run_account(acc)
|
|||
|
|
except Exception as e: # never let one account kill the run
|
|||
|
|
res = {"status": "fail", "message": "内部错误:%s" % e, "points": 0,
|
|||
|
|
"time": fmt_now(), "finished_at": fmt_now()}
|
|||
|
|
acc["last_result"] = res
|
|||
|
|
_reload(acc)
|
|||
|
|
results[acc["id"]] = {
|
|||
|
|
"platform": acc.get("platform"),
|
|||
|
|
"name": acc.get("name"),
|
|||
|
|
"status": res.get("status"),
|
|||
|
|
"message": res.get("message"),
|
|||
|
|
"points": res.get("points", 0),
|
|||
|
|
"credits": res.get("credits"),
|
|||
|
|
}
|
|||
|
|
log("checkin[%s] %s (%s): %s %s" % (reason, acc.get("id"), acc.get("platform"),
|
|||
|
|
res.get("status"), res.get("message")))
|
|||
|
|
# record today's summary in state (always, even with zero accounts, so the
|
|||
|
|
# daemon's startup catch-up does not repeat when no accounts exist)
|
|||
|
|
state = load_state()
|
|||
|
|
today = today_str()
|
|||
|
|
state.setdefault("today", {})[today] = results
|
|||
|
|
state["last_run"] = fmt_now()
|
|||
|
|
state["last_run_reason"] = reason
|
|||
|
|
# prune history: keep the last 30 days
|
|||
|
|
days = sorted(state.get("today", {}).keys())
|
|||
|
|
for old in days[:-30]:
|
|||
|
|
state["today"].pop(old, None)
|
|||
|
|
save_state(state)
|
|||
|
|
if results:
|
|||
|
|
notify(results, reason)
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_account(account_id):
|
|||
|
|
"""Query status + credits without claiming."""
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
acc = find_account(accounts, account_id)
|
|||
|
|
if not acc:
|
|||
|
|
return {"error": "账号不存在"}
|
|||
|
|
out = {"id": account_id, "platform": acc.get("platform")}
|
|||
|
|
try:
|
|||
|
|
if acc.get("platform") == "workbuddy":
|
|||
|
|
if workbuddy.token_expiring(acc.get("auth", {})) and acc["auth"].get("refreshToken"):
|
|||
|
|
ok, msg = workbuddy.refresh_token(acc)
|
|||
|
|
_reload(acc)
|
|||
|
|
out["refresh"] = msg
|
|||
|
|
checked, detail, err = workbuddy.query_status(acc)
|
|||
|
|
out["checked_in"] = checked
|
|||
|
|
out["error"] = err
|
|||
|
|
summary, cerr = workbuddy.fetch_credits(acc)
|
|||
|
|
if summary:
|
|||
|
|
out["credits"] = summary
|
|||
|
|
if cerr and cerr != "no_session":
|
|||
|
|
out["credits_error"] = cerr
|
|||
|
|
elif acc.get("platform") == "trae":
|
|||
|
|
checked, enable, credits, err = trae.query_status(acc)
|
|||
|
|
out["checked_in"] = checked
|
|||
|
|
out["enable"] = enable
|
|||
|
|
out["credits"] = credits
|
|||
|
|
out["error"] = err
|
|||
|
|
elif acc.get("platform") == "qoder":
|
|||
|
|
ok, msg = qoder.ensure_token(acc)
|
|||
|
|
_reload(acc)
|
|||
|
|
if ok:
|
|||
|
|
out["refresh"] = msg
|
|||
|
|
# 探测两个站点,判断 token 属于哪个域(两站 Token 不互通)
|
|||
|
|
probe = qoder.probe_domains(acc)
|
|||
|
|
out["domain_probe"] = probe
|
|||
|
|
current = (acc.get("auth") or {}).get("domain")
|
|||
|
|
ok_domain = None
|
|||
|
|
for dom, r in probe.items():
|
|||
|
|
if r.get("auth"):
|
|||
|
|
ok_domain = dom
|
|||
|
|
out["token_domain"] = ok_domain
|
|||
|
|
out["domain_match"] = (ok_domain == current) if ok_domain else None
|
|||
|
|
if ok_domain and ok_domain != current:
|
|||
|
|
out["hint"] = "Token 属于 %s,但当前域名是 %s,请切换" % (ok_domain, current)
|
|||
|
|
campaigns, err = qoder.query_campaigns(acc)
|
|||
|
|
if err:
|
|||
|
|
out["error"] = err
|
|||
|
|
else:
|
|||
|
|
benefits = qoder._benefits(campaigns)
|
|||
|
|
claimable = [c for c in benefits if c.get("claimStatus") == "CLAIMABLE"]
|
|||
|
|
out["claimable"] = len(claimable)
|
|||
|
|
out["campaigns"] = len(benefits)
|
|||
|
|
amounts = []
|
|||
|
|
for c in claimable:
|
|||
|
|
b = c.get("benefit") or {}
|
|||
|
|
if isinstance(b, dict) and b.get("amount"):
|
|||
|
|
amounts.append(b["amount"])
|
|||
|
|
if amounts:
|
|||
|
|
out["credits"] = sum(amounts)
|
|||
|
|
except Exception as e:
|
|||
|
|
out["error"] = str(e)
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Notifications (multi-platform webhook)
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
WEBHOOK_TYPES = ("auto", "wecom", "dingtalk", "feishu", "bark", "generic")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _detect_webhook_type(url):
|
|||
|
|
u = url.lower()
|
|||
|
|
if "qyapi.weixin.qq.com" in u:
|
|||
|
|
return "wecom"
|
|||
|
|
if "oapi.dingtalk.com" in u:
|
|||
|
|
return "dingtalk"
|
|||
|
|
if "open.feishu.cn" in u or "open.larkoffice.com" in u:
|
|||
|
|
return "feishu"
|
|||
|
|
if "api.day.app" in u or "bark" in u:
|
|||
|
|
return "bark"
|
|||
|
|
return "generic"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def notify_payload(wtype, url, title, content):
|
|||
|
|
"""Build the platform-correct request. Returns (final_url, body_or_None).
|
|||
|
|
|
|||
|
|
body=None means a plain GET (Bark URL-path API).
|
|||
|
|
"""
|
|||
|
|
if wtype == "auto" or wtype not in WEBHOOK_TYPES:
|
|||
|
|
wtype = _detect_webhook_type(url)
|
|||
|
|
text = title + "\n" + content
|
|||
|
|
if wtype in ("wecom", "dingtalk"):
|
|||
|
|
# 企业微信 / 钉钉自定义机器人: {"msgtype":"text","text":{"content":...}}
|
|||
|
|
return url, json.dumps({"msgtype": "text", "text": {"content": text}},
|
|||
|
|
ensure_ascii=False).encode("utf-8")
|
|||
|
|
if wtype == "feishu":
|
|||
|
|
# 飞书自定义机器人: {"msg_type":"text","content":{"text":...}}
|
|||
|
|
return url, json.dumps({"msg_type": "text", "content": {"text": text}},
|
|||
|
|
ensure_ascii=False).encode("utf-8")
|
|||
|
|
if wtype == "bark":
|
|||
|
|
# Bark: <base>/<title>/<body>(URL 路径传参,GET 即可)
|
|||
|
|
import urllib.parse
|
|||
|
|
base = url.rstrip("/")
|
|||
|
|
return "%s/%s/%s" % (base, urllib.parse.quote(title), urllib.parse.quote(content)), None
|
|||
|
|
# Server酱 / PushPlus / 通用: 尽量多带常见字段
|
|||
|
|
return url, json.dumps({"title": title, "content": content, "text": text,
|
|||
|
|
"msgtype": "text", "desp": content},
|
|||
|
|
ensure_ascii=False).encode("utf-8")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def notify(results, reason):
|
|||
|
|
settings = load_settings()
|
|||
|
|
url = (settings.get("webhook_url") or "").strip()
|
|||
|
|
if not url:
|
|||
|
|
return
|
|||
|
|
notify_on = settings.get("notify_on") or []
|
|||
|
|
has_fail = any(r["status"] in ("fail", "no_session", "no_auth", "retry_later")
|
|||
|
|
for r in results.values())
|
|||
|
|
has_success = any(r["status"] in ("ok", "ok_unverified") for r in results.values())
|
|||
|
|
if has_fail and "fail" not in notify_on:
|
|||
|
|
if not (has_success and "success" in notify_on):
|
|||
|
|
return
|
|||
|
|
if not has_fail and not has_success:
|
|||
|
|
return
|
|||
|
|
if not has_fail and has_success and "success" not in notify_on:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lines = []
|
|||
|
|
for r in results.values():
|
|||
|
|
mark = {"ok": "✅", "ok_unverified": "✅", "already": "☑️",
|
|||
|
|
"inactive": "➖", "fail": "❌", "no_session": "🔒",
|
|||
|
|
"no_auth": "🔒", "retry_later": "⏳"}.get(r["status"], "•")
|
|||
|
|
lines.append("%s %s %s:%s" % (mark, r.get("platform", ""), r.get("name", ""), r.get("message", "")))
|
|||
|
|
content = "\n".join(lines)
|
|||
|
|
title = "Agent 签到结果(%s)" % today_str()
|
|||
|
|
wtype = settings.get("webhook_type") or "auto"
|
|||
|
|
final_url, body = notify_payload(wtype, url, title, content)
|
|||
|
|
try:
|
|||
|
|
req = urllib.request.Request(final_url, data=body, method="POST" if body else "GET")
|
|||
|
|
if body:
|
|||
|
|
req.add_header("Content-Type", "application/json")
|
|||
|
|
req.add_header("User-Agent", USER_AGENT)
|
|||
|
|
with urllib.request.urlopen(req, timeout=15, context=_SSL_CTX) as resp:
|
|||
|
|
resp.read()
|
|||
|
|
log("webhook notification sent via %s (%d bytes)" % (wtype, len(body or 0)))
|
|||
|
|
except Exception as e:
|
|||
|
|
log("webhook notification failed (%s): %s" % (wtype, e), "WARN")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Dashboard state
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def dashboard():
|
|||
|
|
accounts = load_accounts()
|
|||
|
|
settings = load_settings()
|
|||
|
|
state = load_state()
|
|||
|
|
today = today_str()
|
|||
|
|
today_results = (state.get("today") or {}).get(today, {})
|
|||
|
|
items = []
|
|||
|
|
for acc in accounts:
|
|||
|
|
item = public_account(acc)
|
|||
|
|
item["today"] = today_results.get(acc.get("id"))
|
|||
|
|
item["need_reauth"] = bool(acc.get("need_reauth"))
|
|||
|
|
item["disabled"] = bool(acc.get("disabled"))
|
|||
|
|
items.append(item)
|
|||
|
|
return {
|
|||
|
|
"accounts": items,
|
|||
|
|
"settings": settings,
|
|||
|
|
"today": today,
|
|||
|
|
"today_results": today_results,
|
|||
|
|
"last_run": state.get("last_run"),
|
|||
|
|
"last_run_reason": state.get("last_run_reason"),
|
|||
|
|
"next_run_at": state.get("next_run_at"),
|
|||
|
|
"daemon": state.get("daemon"),
|
|||
|
|
}
|