feat: Add bot analysis and command services, bot settings, and a frontend account center with subscription and EVM wallet integration.
This commit is contained in:
@@ -633,6 +633,17 @@ export function AccountCenter() {
|
|||||||
const address = String(accounts?.[0] || "").toLowerCase();
|
const address = String(accounts?.[0] || "").toLowerCase();
|
||||||
if (!address) throw new Error("钱包账户为空");
|
if (!address) throw new Error("钱包账户为空");
|
||||||
|
|
||||||
|
const existingWallet = boundWallets.find(
|
||||||
|
(w) => String(w.address || "").toLowerCase() === address,
|
||||||
|
);
|
||||||
|
if (existingWallet) {
|
||||||
|
setWalletAddress(address);
|
||||||
|
setSelectedWallet(address);
|
||||||
|
setPaymentInfo(`${walletLabel} 已绑定: ${shortAddress(address)}`);
|
||||||
|
setPaymentBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setWalletAddress(address);
|
setWalletAddress(address);
|
||||||
const challengeRes = await fetch("/api/payments/wallets/challenge", {
|
const challengeRes = await fetch("/api/payments/wallets/challenge", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -82,8 +82,16 @@ export function UnlockProOverlay({
|
|||||||
const finalPayLabel =
|
const finalPayLabel =
|
||||||
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
|
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
|
||||||
|
|
||||||
|
const maxDiscountUsdInt = Math.max(1, Math.floor(billing.maxDiscountUsd || 0));
|
||||||
|
const maxPointsForFullDiscount = Math.max(
|
||||||
|
billing.pointsPerUsd,
|
||||||
|
billing.pointsPerUsd * maxDiscountUsdInt,
|
||||||
|
);
|
||||||
|
const redeemableUsdNow = billing.pointsEnabled
|
||||||
|
? Math.min(maxDiscountUsdInt, Math.floor(points / Math.max(1, billing.pointsPerUsd)))
|
||||||
|
: 0;
|
||||||
const progressPct = billing.pointsEnabled
|
const progressPct = billing.pointsEnabled
|
||||||
? Math.min(100, Math.round((points / billing.pointsPerUsd) * 100))
|
? Math.min(100, Math.round((points / maxPointsForFullDiscount) * 100))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -202,6 +210,11 @@ export function UnlockProOverlay({
|
|||||||
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}`
|
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}`
|
||||||
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
|
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
|
||||||
</p>
|
</p>
|
||||||
|
<p className={s.pointsNote}>
|
||||||
|
{isEn
|
||||||
|
? `Now redeemable: $${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}`
|
||||||
|
: `当前可抵:${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className={s.pointsBalance}>
|
<div className={s.pointsBalance}>
|
||||||
<Sparkles size={11} />
|
<Sparkles size={11} />
|
||||||
@@ -242,17 +255,21 @@ export function UnlockProOverlay({
|
|||||||
? "Points redemption is unavailable for this plan."
|
? "Points redemption is unavailable for this plan."
|
||||||
: "当前套餐暂不支持积分抵扣。"
|
: "当前套餐暂不支持积分抵扣。"
|
||||||
: isEn
|
: isEn
|
||||||
? `Need ${billing.pointsPerUsd} pts minimum. You have: ${points}`
|
? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per $1, up to $${maxDiscountUsdInt}. You have: ${points}`
|
||||||
: `至少需要 ${billing.pointsPerUsd} 积分,当前仅有 ${points}`}
|
: `满 ${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1U,最多抵 ${maxDiscountUsdInt}U。当前 ${points} 分`}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{billing.pointsEnabled && (
|
{billing.pointsEnabled && (
|
||||||
<div className={s.progressWrap}>
|
<div className={s.progressWrap}>
|
||||||
<div className={s.progressHeader}>
|
<div className={s.progressHeader}>
|
||||||
<span>
|
<span>
|
||||||
{points} / {billing.pointsPerUsd}
|
{points} / {maxPointsForFullDiscount}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{isEn
|
||||||
|
? `$${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}`
|
||||||
|
: `${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`}
|
||||||
</span>
|
</span>
|
||||||
<span>{progressPct}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={s.progressTrack}>
|
<div className={s.progressTrack}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ class DebAnalysisService:
|
|||||||
city_input_norm = city_input.strip().lower()
|
city_input_norm = city_input.strip().lower()
|
||||||
return aliases.get(city_input_norm, city_input_norm)
|
return aliases.get(city_input_norm, city_input_norm)
|
||||||
|
|
||||||
|
# Backward-compatible alias used by older service wrappers.
|
||||||
|
def resolve_deb_city(self, city_input: str) -> str:
|
||||||
|
return self.resolve_city(city_input)
|
||||||
|
|
||||||
def has_history(self, city_name: str) -> bool:
|
def has_history(self, city_name: str) -> bool:
|
||||||
_is_excluded_model_name, load_history, reconcile_recent_actual_highs = (
|
_is_excluded_model_name, load_history, reconcile_recent_actual_highs = (
|
||||||
self._load_deb_module_api()
|
self._load_deb_module_api()
|
||||||
@@ -42,6 +46,10 @@ class DebAnalysisService:
|
|||||||
city_data = data.get(city_name)
|
city_data = data.get(city_name)
|
||||||
return isinstance(city_data, dict) and bool(city_data)
|
return isinstance(city_data, dict) and bool(city_data)
|
||||||
|
|
||||||
|
# Backward-compatible alias used by older service wrappers.
|
||||||
|
def has_deb_history(self, city_name: str) -> bool:
|
||||||
|
return self.has_history(city_name)
|
||||||
|
|
||||||
def build_deb_accuracy_report(self, city_name: str, deb_query_cost: int) -> str:
|
def build_deb_accuracy_report(self, city_name: str, deb_query_cost: int) -> str:
|
||||||
_is_excluded_model_name, load_history, reconcile_recent_actual_highs = (
|
_is_excluded_model_name, load_history, reconcile_recent_actual_highs = (
|
||||||
self._load_deb_module_api()
|
self._load_deb_module_api()
|
||||||
@@ -217,4 +225,3 @@ class DebAnalysisService:
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"💸 本次消耗 <code>{deb_query_cost}</code> 积分。")
|
lines.append(f"💸 本次消耗 <code>{deb_query_cost}</code> 积分。")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ class DebCommandService:
|
|||||||
self.analysis = analysis
|
self.analysis = analysis
|
||||||
|
|
||||||
def resolve_city(self, city_input: str) -> str:
|
def resolve_city(self, city_input: str) -> str:
|
||||||
return self.analysis.resolve_deb_city(city_input)
|
return self.analysis.resolve_city(city_input)
|
||||||
|
|
||||||
def has_history(self, city_name: str) -> bool:
|
def has_history(self, city_name: str) -> bool:
|
||||||
return self.analysis.has_deb_history(city_name)
|
return self.analysis.has_history(city_name)
|
||||||
|
|
||||||
def build_report(self, city_name: str, deb_query_cost: int) -> DebReportResult:
|
def build_report(self, city_name: str, deb_query_cost: int) -> DebReportResult:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+2
-2
@@ -18,5 +18,5 @@ MESSAGE_POINTS = _env_int("POLYWEATHER_BOT_MESSAGE_POINTS", 4, min_value=1)
|
|||||||
MESSAGE_DAILY_CAP = _env_int("POLYWEATHER_BOT_MESSAGE_DAILY_CAP", 40, min_value=1)
|
MESSAGE_DAILY_CAP = _env_int("POLYWEATHER_BOT_MESSAGE_DAILY_CAP", 40, min_value=1)
|
||||||
MESSAGE_MIN_LENGTH = _env_int("POLYWEATHER_BOT_MESSAGE_MIN_LENGTH", 3, min_value=1)
|
MESSAGE_MIN_LENGTH = _env_int("POLYWEATHER_BOT_MESSAGE_MIN_LENGTH", 3, min_value=1)
|
||||||
MESSAGE_COOLDOWN_SEC = _env_int("POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC", 30, min_value=0)
|
MESSAGE_COOLDOWN_SEC = _env_int("POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC", 30, min_value=0)
|
||||||
CITY_QUERY_COST = _env_int("POLYWEATHER_BOT_CITY_QUERY_COST", 1, min_value=0)
|
CITY_QUERY_COST = _env_int("POLYWEATHER_BOT_CITY_QUERY_COST", 2, min_value=0)
|
||||||
DEB_QUERY_COST = _env_int("POLYWEATHER_BOT_DEB_QUERY_COST", 1, min_value=0)
|
DEB_QUERY_COST = _env_int("POLYWEATHER_BOT_DEB_QUERY_COST", 2, min_value=0)
|
||||||
|
|||||||
Reference in New Issue
Block a user