Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import discord
|
| 2 |
from discord.ext import commands, tasks
|
| 3 |
from google import genai
|
|
@@ -33,9 +44,6 @@ logger = logging.getLogger("BK_GTA_ARCHITECT_TITAN")
|
|
| 33 |
# 1. 物理层:Gradio 哨兵与探针 (Gradio Sentinel)
|
| 34 |
# ==============================================================================
|
| 35 |
def get_status():
|
| 36 |
-
"""
|
| 37 |
-
提供给外部(如 UptimeRobot/Cron-Job)的心跳对账数据
|
| 38 |
-
"""
|
| 39 |
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 40 |
bot_status = "OFFLINE"
|
| 41 |
latency = "N/A"
|
|
@@ -45,7 +53,7 @@ def get_status():
|
|
| 45 |
latency = f"{round(bot.latency * 1000, 2)}ms"
|
| 46 |
except Exception as e:
|
| 47 |
logger.error(f"Status check failed: {e}")
|
| 48 |
-
|
| 49 |
return {
|
| 50 |
"status": "BK-GTA-TITAN-ACTIVE",
|
| 51 |
"logic_integrity": "100%",
|
|
@@ -63,7 +71,6 @@ def get_status():
|
|
| 63 |
DISCORD_TOKEN = os.environ.get('DISCORD_TOKEN', '').strip()
|
| 64 |
SESSION_ID = random.randint(10000, 99999)
|
| 65 |
|
| 66 |
-
# [CORE] 8 密钥轮询加载器
|
| 67 |
raw_keys = os.environ.get('GEMINI_KEYS_POOL', '')
|
| 68 |
KEY_POOL = [k.strip() for k in raw_keys.split(',') if k.strip()]
|
| 69 |
if not KEY_POOL:
|
|
@@ -73,30 +80,24 @@ if not KEY_POOL:
|
|
| 73 |
if not KEY_POOL:
|
| 74 |
logger.critical("❌ [CRITICAL] NO API KEYS FOUND! Please set GEMINI_KEYS_POOL.")
|
| 75 |
|
| 76 |
-
# 权限位设置
|
| 77 |
intents = discord.Intents.default()
|
| 78 |
intents.message_content = True
|
| 79 |
intents.members = True
|
| 80 |
bot = commands.Bot(command_prefix='!', intents=intents)
|
| 81 |
|
| 82 |
-
# 并发审计锁
|
| 83 |
audit_lock = asyncio.Lock()
|
| 84 |
-
# 优先使用持久化目录 /data
|
| 85 |
DB_PATH = '/data/bk_gta_v62_unified.json' if os.path.exists('/data') else 'bk_gta_v62_unified.json'
|
| 86 |
|
| 87 |
# ==============================================================================
|
| 88 |
# 3. 状态引擎 (BKEngine Core)
|
| 89 |
# ==============================================================================
|
| 90 |
class BKEngine:
|
| 91 |
-
"""
|
| 92 |
-
负责量化资产的持久化、ELO 计算及风险对账
|
| 93 |
-
"""
|
| 94 |
def __init__(self):
|
| 95 |
self.db_file = DB_PATH
|
| 96 |
self.data = self._load_db()
|
| 97 |
self.pending_challenges = {}
|
| 98 |
self.active_duels = {}
|
| 99 |
-
self.active_bets = {}
|
| 100 |
|
| 101 |
def _load_db(self):
|
| 102 |
if os.path.exists(self.db_file):
|
|
@@ -105,7 +106,7 @@ class BKEngine:
|
|
| 105 |
content = json.load(f)
|
| 106 |
if "users" not in content: content["users"] = {}
|
| 107 |
return content
|
| 108 |
-
except Exception as e:
|
| 109 |
logger.error(f"DB Load Failure: {e}")
|
| 110 |
return self._init_schema()
|
| 111 |
return self._init_schema()
|
|
@@ -115,7 +116,7 @@ class BKEngine:
|
|
| 115 |
"version": "62.9-Titan",
|
| 116 |
"users": {},
|
| 117 |
"factions": {
|
| 118 |
-
"BULLS": {"score": 0, "emoji": "🐂", "wins": 0},
|
| 119 |
"BEARS": {"score": 0, "emoji": "🐻", "wins": 0}
|
| 120 |
},
|
| 121 |
"prize_pool": 1000000.0,
|
|
@@ -152,7 +153,6 @@ class BKEngine:
|
|
| 152 |
if mdd is not None: u["mdd_record"] = mdd
|
| 153 |
if sharpe is not None: u["sharpe_record"] = sharpe
|
| 154 |
u["points"] += 50
|
| 155 |
-
# 记录最近 5 次审计历史
|
| 156 |
u["audit_history"].append({"s": score, "t": str(datetime.datetime.now())})
|
| 157 |
u["audit_history"] = u["audit_history"][-5:]
|
| 158 |
u["last_active"] = str(datetime.datetime.now())
|
|
@@ -163,17 +163,13 @@ class BKEngine:
|
|
| 163 |
K = 32
|
| 164 |
Rw, Rl = w["elo"], l["elo"]
|
| 165 |
Ew = 1 / (1 + 10 ** ((Rl - Rw) / 400))
|
| 166 |
-
|
| 167 |
w["elo"] = round(Rw + K * (1 - Ew))
|
| 168 |
l["elo"] = round(Rl + K * (0 - Ew))
|
| 169 |
-
|
| 170 |
w["win_streak"] += 1
|
| 171 |
l["win_streak"] = 0
|
| 172 |
-
|
| 173 |
if w["faction"] and w["faction"] in self.data["factions"]:
|
| 174 |
self.data["factions"][w["faction"]]["score"] += 15
|
| 175 |
self.data["factions"][w["faction"]]["wins"] += 1
|
| 176 |
-
|
| 177 |
self.save_db()
|
| 178 |
return w["elo"], l["elo"], w["win_streak"]
|
| 179 |
|
|
@@ -185,7 +181,7 @@ bk_engine = BKEngine()
|
|
| 185 |
def safe_extract_float(text, key):
|
| 186 |
try:
|
| 187 |
patterns = [
|
| 188 |
-
rf"{key}:\s*([-+]?\d*\.?\d+)",
|
| 189 |
rf"{key}\s*=\s*([-+]?\d*\.?\d+)",
|
| 190 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 191 |
]
|
|
@@ -205,11 +201,8 @@ def safe_extract_text(text, start, end=None):
|
|
| 205 |
except: return "N/A"
|
| 206 |
|
| 207 |
async def call_gemini(prompt, attachment=None):
|
| 208 |
-
"""
|
| 209 |
-
[Core Logic] 8 密钥池高频轮询与故障自动迁移
|
| 210 |
-
"""
|
| 211 |
if not KEY_POOL: return "ERROR: NO_KEYS"
|
| 212 |
-
|
| 213 |
img_data = None
|
| 214 |
if attachment:
|
| 215 |
try: img_data = await attachment.read()
|
|
@@ -227,7 +220,7 @@ async def call_gemini(prompt, attachment=None):
|
|
| 227 |
contents.append(types.Part.from_bytes(data=img_data, mime_type='image/png'))
|
| 228 |
|
| 229 |
response = await loop.run_in_executor(
|
| 230 |
-
None,
|
| 231 |
functools.partial(client.models.generate_content, model="gemini-1.5-flash", contents=contents)
|
| 232 |
)
|
| 233 |
if response and response.text:
|
|
@@ -249,8 +242,7 @@ async def profile(ctx, member: discord.Member = None):
|
|
| 249 |
t = member or ctx.author
|
| 250 |
u = bk_engine.get_user(t.id)
|
| 251 |
elo = u['elo']
|
| 252 |
-
|
| 253 |
-
# 动态段位
|
| 254 |
if elo < 1300: rank, color = "Iron 学徒 🗑️", 0x717d7e
|
| 255 |
elif elo < 1500: rank, color = "Bronze 资深 🥉", 0xcd7f32
|
| 256 |
elif elo < 1800: rank, color = "Silver 精英 🥈", 0xbdc3c7
|
|
@@ -261,7 +253,7 @@ async def profile(ctx, member: discord.Member = None):
|
|
| 261 |
emb = discord.Embed(title=f"📊 Quant Profile: {t.display_name}", color=color)
|
| 262 |
emb.add_field(name="Rating (ELO)", value=f"**{elo}** ({rank})", inline=True)
|
| 263 |
emb.add_field(name="Assets", value=f"💰 **{u['points']}**", inline=True)
|
| 264 |
-
|
| 265 |
f_info = u['faction'] or "Free Agent"
|
| 266 |
f_emoji = bk_engine.data["factions"].get(f_info, {}).get("emoji", "🌐")
|
| 267 |
emb.add_field(name="Faction", value=f"{f_emoji} {f_info}", inline=True)
|
|
@@ -272,7 +264,7 @@ async def profile(ctx, member: discord.Member = None):
|
|
| 272 |
f"Max MDD: {u.get('mdd_record',0):.2f}%"
|
| 273 |
)
|
| 274 |
emb.add_field(name="Institutional Metrics", value=f"```yaml\n{stats}\n```", inline=False)
|
| 275 |
-
|
| 276 |
if target_avatar := t.avatar:
|
| 277 |
emb.set_thumbnail(url=target_avatar.url)
|
| 278 |
emb.set_footer(text=f"Node: HuggingFace Titan | Session: {SESSION_ID}")
|
|
@@ -283,21 +275,21 @@ async def leaderboard(ctx):
|
|
| 283 |
all_users = []
|
| 284 |
for uid, d in bk_engine.data["users"].items():
|
| 285 |
all_users.append({"id": uid, "elo": d["elo"], "score": d["highest_score"]})
|
| 286 |
-
|
| 287 |
top = sorted(all_users, key=lambda x: x["elo"], reverse=True)[:10]
|
| 288 |
desc = ""
|
| 289 |
for idx, u in enumerate(top):
|
| 290 |
icon = ["🥇","🥈","🥉"][idx] if idx < 3 else f"{idx+1}."
|
| 291 |
desc += f"{icon} <@{u['id']}> : **{u['elo']}** (Peak: {u['score']:.1f})\n"
|
| 292 |
-
|
| 293 |
emb = discord.Embed(title="🏆 BK-GTA Institutional Leaderboard", description=desc or "No data.", color=0xf1c40f)
|
| 294 |
-
|
| 295 |
f_info = ""
|
| 296 |
for k, v in bk_engine.data["factions"].items():
|
| 297 |
f_info += f"{v['emoji']} {k}: {v['score']} pts "
|
| 298 |
if f_info:
|
| 299 |
emb.add_field(name="⚔️ Faction War", value=f_info, inline=False)
|
| 300 |
-
|
| 301 |
await ctx.send(embed=emb)
|
| 302 |
|
| 303 |
# ==============================================================================
|
|
@@ -307,7 +299,7 @@ async def leaderboard(ctx):
|
|
| 307 |
async def news(ctx):
|
| 308 |
if not ctx.message.attachments: return await ctx.send("❌ Error: 请上传情报截图。")
|
| 309 |
m = await ctx.send("📡 **[Sentinel] Analyzing Macro Pulse...**")
|
| 310 |
-
|
| 311 |
prompt = (
|
| 312 |
"你是一名顶级对冲基金策略师。请解析截图内容:\n"
|
| 313 |
"1. 评分 (SCORE): -10 (极大利空) 到 +10 (极大利多)。\n"
|
|
@@ -316,17 +308,17 @@ async def news(ctx):
|
|
| 316 |
"格式必须为:\nSCORE: [v]\nSUMMARY: [v]\nTAKE: [v]"
|
| 317 |
)
|
| 318 |
res = await call_gemini(prompt, ctx.message.attachments[0])
|
| 319 |
-
|
| 320 |
score = safe_extract_float(res, "SCORE")
|
| 321 |
summary = safe_extract_text(res, "SUMMARY", "TAKE")
|
| 322 |
take = safe_extract_text(res, "TAKE")
|
| 323 |
-
|
| 324 |
color = 0x2ecc71 if score > 0 else 0xe74c3c if score < 0 else 0xbdc3c7
|
| 325 |
emb = discord.Embed(title="📰 Macro Intelligence Analysis", color=color)
|
| 326 |
emb.add_field(name="Sentiment Score", value=f"**{score:+.1f} / 10**", inline=True)
|
| 327 |
emb.add_field(name="Core Catalyst", value=summary, inline=False)
|
| 328 |
emb.add_field(name="Institutional View", value=f"```\n{take}\n```", inline=False)
|
| 329 |
-
|
| 330 |
await m.delete()
|
| 331 |
await ctx.send(embed=emb)
|
| 332 |
|
|
@@ -336,7 +328,7 @@ async def news(ctx):
|
|
| 336 |
@bot.command()
|
| 337 |
async def evaluate(ctx):
|
| 338 |
if not ctx.message.attachments: return await ctx.send("❌ Error: 请上传回测或结算截图。")
|
| 339 |
-
|
| 340 |
if audit_lock.locked():
|
| 341 |
return await ctx.send("⚠️ **[System Busy]** 正在处理另一项审计,请稍后。")
|
| 342 |
|
|
@@ -349,9 +341,9 @@ async def evaluate(ctx):
|
|
| 349 |
"H_ZH: 中文核心优势或致命风险概括。\n"
|
| 350 |
"格式:\nRET: [v]\nMDD: [v]\nPF: [v]\nSHARPE: [v]\nTRADES: [v]\nS_BASE: [v]\nVIOLATION: [TRUE/FALSE]\nH_ZH: [v]"
|
| 351 |
)
|
| 352 |
-
|
| 353 |
res = await call_gemini(prompt, ctx.message.attachments[0])
|
| 354 |
-
|
| 355 |
if "ERROR" in res:
|
| 356 |
await m.delete()
|
| 357 |
return await ctx.send("🔴 [Audit Crash] 引擎请求失败。")
|
|
@@ -363,13 +355,11 @@ async def evaluate(ctx):
|
|
| 363 |
s_base = safe_extract_float(res, "S_BASE")
|
| 364 |
violation = "TRUE" in res.upper()
|
| 365 |
|
| 366 |
-
# 风控核心:MDD 否决制
|
| 367 |
if violation or mdd > 35.0:
|
| 368 |
s_final = 0.0
|
| 369 |
status = "❌ REJECTED (High Risk)"
|
| 370 |
color = 0xe74c3c
|
| 371 |
else:
|
| 372 |
-
# 复合评分逻辑:基础分 * 样本权重 * 风险权重
|
| 373 |
weight_sample = math.log10(max(trades, 1)) / 1.5
|
| 374 |
weight_risk = (sharpe / 2.0) if sharpe > 0 else 0.5
|
| 375 |
s_final = min(max(s_base * weight_sample * weight_risk, 0.0), 100.0)
|
|
@@ -378,14 +368,14 @@ async def evaluate(ctx):
|
|
| 378 |
|
| 379 |
bk_engine.update_audit(ctx.author.id, s_final, mdd, sharpe)
|
| 380 |
highlights = safe_extract_text(res, "H_ZH")
|
| 381 |
-
|
| 382 |
emb = discord.Embed(title="🛡️ Quantitative Audit Report", color=color)
|
| 383 |
emb.add_field(name="Verdict", value=f"**{status}**", inline=False)
|
| 384 |
metrics = f"Return: {ret}% | MDD: {mdd}%\nSharpe: {sharpe} | Trades: {trades}"
|
| 385 |
emb.add_field(name="Key Metrics", value=f"```\n{metrics}\n```", inline=False)
|
| 386 |
emb.add_field(name="Logic Score", value=f"**{s_final:.2f} / 100**", inline=True)
|
| 387 |
emb.add_field(name="Highlights", value=highlights, inline=False)
|
| 388 |
-
|
| 389 |
await m.delete()
|
| 390 |
await ctx.send(embed=emb)
|
| 391 |
|
|
@@ -397,7 +387,7 @@ async def evaluate(ctx):
|
|
| 397 |
async def open_bet(ctx, *, topic):
|
| 398 |
bid = str(random.randint(100, 999))
|
| 399 |
bk_engine.active_bets[bid] = {
|
| 400 |
-
"topic": topic, "up": 0, "down": 0,
|
| 401 |
"u_up": {}, "u_down": {}, "status": "OPEN",
|
| 402 |
"time": str(datetime.datetime.now())
|
| 403 |
}
|
|
@@ -407,20 +397,20 @@ async def open_bet(ctx, *, topic):
|
|
| 407 |
async def bet(ctx, bid: str, side: str, amt: int):
|
| 408 |
if bid not in bk_engine.active_bets or bk_engine.active_bets[bid]["status"] != "OPEN":
|
| 409 |
return await ctx.send("❌ 盘口不存在或已关闭。")
|
| 410 |
-
|
| 411 |
u = bk_engine.get_user(ctx.author.id)
|
| 412 |
if u["points"] < amt or amt <= 0:
|
| 413 |
return await ctx.send("❌ 资产点数不足。")
|
| 414 |
-
|
| 415 |
side = side.upper()
|
| 416 |
if side not in ["UP", "DOWN"]: return await ctx.send("❌ 必须选择 UP 或 DOWN。")
|
| 417 |
-
|
| 418 |
u["points"] -= amt
|
| 419 |
b = bk_engine.active_bets[bid]
|
| 420 |
pool = "u_up" if side == "UP" else "u_down"
|
| 421 |
b[side.lower()] += amt
|
| 422 |
b[pool][str(ctx.author.id)] = b[pool].get(str(ctx.author.id), 0) + amt
|
| 423 |
-
|
| 424 |
bk_engine.save_db()
|
| 425 |
await ctx.send(f"✅ 已确认:{ctx.author.name} 向 {side} 注入 {amt} 点资产。")
|
| 426 |
|
|
@@ -430,16 +420,16 @@ async def settle_bet(ctx, bid: str, winner: str):
|
|
| 430 |
if bid not in bk_engine.active_bets: return
|
| 431 |
b = bk_engine.active_bets[bid]
|
| 432 |
winner = winner.upper()
|
| 433 |
-
|
| 434 |
win_pool = b["u_up"] if winner == "UP" else b["u_down"]
|
| 435 |
total_w = b["up"] if winner == "UP" else b["down"]
|
| 436 |
total_l = b["down"] if winner == "UP" else b["up"]
|
| 437 |
-
|
| 438 |
if total_w > 0:
|
| 439 |
for uid, amt in win_pool.items():
|
| 440 |
profit = (amt / total_w) * total_l
|
| 441 |
bk_engine.get_user(int(uid))["points"] += int(amt + profit)
|
| 442 |
-
|
| 443 |
b["status"] = "SETTLED"
|
| 444 |
bk_engine.save_db()
|
| 445 |
await ctx.send(f"🏁 盘口 {bid} 已结算!胜利方:**{winner}**。资产已自动划转。")
|
|
@@ -458,11 +448,11 @@ async def challenge(ctx, member: discord.Member):
|
|
| 458 |
async def accept(ctx):
|
| 459 |
if ctx.author.id not in bk_engine.pending_challenges:
|
| 460 |
return await ctx.send("❌ 没有待处理的决斗请求。")
|
| 461 |
-
|
| 462 |
req = bk_engine.pending_challenges.pop(ctx.author.id)
|
| 463 |
did = req["id"]
|
| 464 |
bk_engine.active_duels[did] = {
|
| 465 |
-
"p1": req["challenger"], "p2": ctx.author.id,
|
| 466 |
"s1": None, "s2": None, "t": str(datetime.datetime.now())
|
| 467 |
}
|
| 468 |
await ctx.send(f"🔥 **竞技场锁定!** 决斗 ID: `{did}`\n请双方通过 `!duel_submit {did}` 提交审计截图。")
|
|
@@ -471,23 +461,23 @@ async def accept(ctx):
|
|
| 471 |
async def duel_submit(ctx, did: str):
|
| 472 |
if did not in bk_engine.active_duels: return
|
| 473 |
if not ctx.message.attachments: return
|
| 474 |
-
|
| 475 |
duel = bk_engine.active_duels[did]
|
| 476 |
if ctx.author.id not in [duel["p1"], duel["p2"]]: return
|
| 477 |
-
|
| 478 |
m = await ctx.send("🔍 正在审计决斗数据...")
|
| 479 |
res = await call_gemini("Extract SCORE:[0-100]. Format: SCORE:[v]", ctx.message.attachments[0])
|
| 480 |
score = safe_extract_float(res, "SCORE")
|
| 481 |
-
|
| 482 |
if ctx.author.id == duel["p1"]: duel["s1"] = score
|
| 483 |
else: duel["s2"] = score
|
| 484 |
-
|
| 485 |
if duel["s1"] is not None and duel["s2"] is not None:
|
| 486 |
p1, p2 = duel["p1"], duel["p2"]
|
| 487 |
s1, s2 = duel["s1"], duel["s2"]
|
| 488 |
wid, lid = (p1, p2) if s1 > s2 else (p2, p1)
|
| 489 |
w_elo, l_elo, streak = bk_engine.update_elo(wid, lid)
|
| 490 |
-
|
| 491 |
emb = discord.Embed(title="🏆 Duel Arena Verdict", color=0xf1c40f)
|
| 492 |
emb.add_field(name="Winner", value=f"<@{wid}>\nScore: {max(s1,s2)}\nELO: {w_elo}", inline=True)
|
| 493 |
emb.add_field(name="Loser", value=f"<@{lid}>\nScore: {min(s1,s2)}\nELO: {l_elo}", inline=True)
|
|
@@ -503,31 +493,24 @@ async def duel_submit(ctx, did: str):
|
|
| 503 |
# ==============================================================================
|
| 504 |
@bot.command()
|
| 505 |
async def viral_audit(ctx):
|
| 506 |
-
"""
|
| 507 |
-
[完全版] 提取审计数据并渲染具备像素级视觉逻辑的海报
|
| 508 |
-
"""
|
| 509 |
if not ctx.message.attachments: return
|
| 510 |
m = await ctx.send("🎨 **[GPU-Node] 正在渲染视觉海报...**")
|
| 511 |
-
|
| 512 |
p = "Extract RET: [v] and SHARPE: [v]. Format: RET: [v] SHARPE: [v]"
|
| 513 |
res = await call_gemini(p, ctx.message.attachments[0])
|
| 514 |
ret, sharpe = safe_extract_float(res, "RET"), safe_extract_float(res, "SHARPE")
|
| 515 |
|
| 516 |
try:
|
| 517 |
W, H = 600, 800
|
| 518 |
-
# 暗色底板
|
| 519 |
base = Image.new('RGB', (W, H), (18, 20, 30))
|
| 520 |
draw = ImageDraw.Draw(base)
|
| 521 |
|
| 522 |
-
# 绘制背景装饰网格 (逻辑恢复)
|
| 523 |
for i in range(0, W, 40): draw.line([(i, 0), (i, H)], fill=(30, 35, 45), width=1)
|
| 524 |
for j in range(0, H, 40): draw.line([(0, j), (W, j)], fill=(30, 35, 45), width=1)
|
| 525 |
-
|
| 526 |
-
# 边框
|
| 527 |
draw.rectangle([10, 10, 590, 790], outline=(46, 204, 113), width=2)
|
| 528 |
draw.rectangle([20, 20, 580, 780], outline=(46, 204, 113), width=5)
|
| 529 |
|
| 530 |
-
# 文字渲染
|
| 531 |
draw.text((40, 60), "BK-GTA QUANTITATIVE SYSTEM", fill=(255, 255, 255))
|
| 532 |
draw.text((40, 150), f"TRADER: {ctx.author.name.upper()}", fill=(46, 204, 113))
|
| 533 |
draw.text((40, 260), f"ALPHA RET: +{ret}%", fill=(255, 255, 255))
|
|
@@ -559,7 +542,7 @@ async def on_ready():
|
|
| 559 |
auto_backup.start()
|
| 560 |
await bot.change_presence(
|
| 561 |
activity=discord.Activity(
|
| 562 |
-
type=discord.ActivityType.watching,
|
| 563 |
name="HuggingFace Node"
|
| 564 |
)
|
| 565 |
)
|
|
@@ -573,40 +556,81 @@ async def on_disconnect():
|
|
| 573 |
async def on_resumed():
|
| 574 |
logger.info("✅ [Gateway] Session resumed successfully.")
|
| 575 |
|
| 576 |
-
|
|
|
|
|
|
|
| 577 |
def run_bot():
|
| 578 |
"""
|
| 579 |
-
[
|
| 580 |
-
|
| 581 |
-
|
|
|
|
| 582 |
"""
|
| 583 |
if not DISCORD_TOKEN:
|
| 584 |
-
logger.critical("❌ [FATAL] DISCORD_TOKEN
|
| 585 |
return
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
try:
|
| 588 |
-
bot.
|
|
|
|
| 589 |
except discord.LoginFailure:
|
| 590 |
-
logger.critical(
|
|
|
|
|
|
|
|
|
|
| 591 |
except discord.PrivilegedIntentsRequired:
|
| 592 |
logger.critical(
|
| 593 |
"❌ [FATAL] Privileged Intents not enabled!\n"
|
| 594 |
-
"Go to
|
| 595 |
-
"
|
| 596 |
)
|
| 597 |
except Exception as e:
|
| 598 |
-
logger.error(f"
|
| 599 |
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
|
| 601 |
|
| 602 |
if __name__ == "__main__":
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
time.sleep(3)
|
| 608 |
-
|
| 609 |
-
# 3. 启动 Gradio 前台哨兵
|
|
|
|
| 610 |
iface = gr.Interface(
|
| 611 |
fn=get_status,
|
| 612 |
inputs=None,
|
|
@@ -614,4 +638,23 @@ if __name__ == "__main__":
|
|
| 614 |
title="BK-GTA Sentinel AI",
|
| 615 |
description="Physical architecture monitor. Logic Integrity: Secure."
|
| 616 |
)
|
| 617 |
-
iface.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 核心问题
|
| 2 |
+
|
| 3 |
+
`bot.run()` 内部调用 `asyncio.run()`,它会注册**信号处理器**——这在子线程中会抛出 `ValueError: signal only works in main thread`,线程直接静默崩溃。
|
| 4 |
+
|
| 5 |
+
日志里你看到**零条** Bot 输出就是证据。
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
以下是完整修复后的 `app.py`,可以直接覆盖:
|
| 10 |
+
|
| 11 |
+
```python
|
| 12 |
import discord
|
| 13 |
from discord.ext import commands, tasks
|
| 14 |
from google import genai
|
|
|
|
| 44 |
# 1. 物理层:Gradio 哨兵与探针 (Gradio Sentinel)
|
| 45 |
# ==============================================================================
|
| 46 |
def get_status():
|
|
|
|
|
|
|
|
|
|
| 47 |
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 48 |
bot_status = "OFFLINE"
|
| 49 |
latency = "N/A"
|
|
|
|
| 53 |
latency = f"{round(bot.latency * 1000, 2)}ms"
|
| 54 |
except Exception as e:
|
| 55 |
logger.error(f"Status check failed: {e}")
|
| 56 |
+
|
| 57 |
return {
|
| 58 |
"status": "BK-GTA-TITAN-ACTIVE",
|
| 59 |
"logic_integrity": "100%",
|
|
|
|
| 71 |
DISCORD_TOKEN = os.environ.get('DISCORD_TOKEN', '').strip()
|
| 72 |
SESSION_ID = random.randint(10000, 99999)
|
| 73 |
|
|
|
|
| 74 |
raw_keys = os.environ.get('GEMINI_KEYS_POOL', '')
|
| 75 |
KEY_POOL = [k.strip() for k in raw_keys.split(',') if k.strip()]
|
| 76 |
if not KEY_POOL:
|
|
|
|
| 80 |
if not KEY_POOL:
|
| 81 |
logger.critical("❌ [CRITICAL] NO API KEYS FOUND! Please set GEMINI_KEYS_POOL.")
|
| 82 |
|
|
|
|
| 83 |
intents = discord.Intents.default()
|
| 84 |
intents.message_content = True
|
| 85 |
intents.members = True
|
| 86 |
bot = commands.Bot(command_prefix='!', intents=intents)
|
| 87 |
|
|
|
|
| 88 |
audit_lock = asyncio.Lock()
|
|
|
|
| 89 |
DB_PATH = '/data/bk_gta_v62_unified.json' if os.path.exists('/data') else 'bk_gta_v62_unified.json'
|
| 90 |
|
| 91 |
# ==============================================================================
|
| 92 |
# 3. 状态引擎 (BKEngine Core)
|
| 93 |
# ==============================================================================
|
| 94 |
class BKEngine:
|
|
|
|
|
|
|
|
|
|
| 95 |
def __init__(self):
|
| 96 |
self.db_file = DB_PATH
|
| 97 |
self.data = self._load_db()
|
| 98 |
self.pending_challenges = {}
|
| 99 |
self.active_duels = {}
|
| 100 |
+
self.active_bets = {}
|
| 101 |
|
| 102 |
def _load_db(self):
|
| 103 |
if os.path.exists(self.db_file):
|
|
|
|
| 106 |
content = json.load(f)
|
| 107 |
if "users" not in content: content["users"] = {}
|
| 108 |
return content
|
| 109 |
+
except Exception as e:
|
| 110 |
logger.error(f"DB Load Failure: {e}")
|
| 111 |
return self._init_schema()
|
| 112 |
return self._init_schema()
|
|
|
|
| 116 |
"version": "62.9-Titan",
|
| 117 |
"users": {},
|
| 118 |
"factions": {
|
| 119 |
+
"BULLS": {"score": 0, "emoji": "🐂", "wins": 0},
|
| 120 |
"BEARS": {"score": 0, "emoji": "🐻", "wins": 0}
|
| 121 |
},
|
| 122 |
"prize_pool": 1000000.0,
|
|
|
|
| 153 |
if mdd is not None: u["mdd_record"] = mdd
|
| 154 |
if sharpe is not None: u["sharpe_record"] = sharpe
|
| 155 |
u["points"] += 50
|
|
|
|
| 156 |
u["audit_history"].append({"s": score, "t": str(datetime.datetime.now())})
|
| 157 |
u["audit_history"] = u["audit_history"][-5:]
|
| 158 |
u["last_active"] = str(datetime.datetime.now())
|
|
|
|
| 163 |
K = 32
|
| 164 |
Rw, Rl = w["elo"], l["elo"]
|
| 165 |
Ew = 1 / (1 + 10 ** ((Rl - Rw) / 400))
|
|
|
|
| 166 |
w["elo"] = round(Rw + K * (1 - Ew))
|
| 167 |
l["elo"] = round(Rl + K * (0 - Ew))
|
|
|
|
| 168 |
w["win_streak"] += 1
|
| 169 |
l["win_streak"] = 0
|
|
|
|
| 170 |
if w["faction"] and w["faction"] in self.data["factions"]:
|
| 171 |
self.data["factions"][w["faction"]]["score"] += 15
|
| 172 |
self.data["factions"][w["faction"]]["wins"] += 1
|
|
|
|
| 173 |
self.save_db()
|
| 174 |
return w["elo"], l["elo"], w["win_streak"]
|
| 175 |
|
|
|
|
| 181 |
def safe_extract_float(text, key):
|
| 182 |
try:
|
| 183 |
patterns = [
|
| 184 |
+
rf"{key}:\s*([-+]?\d*\.?\d+)",
|
| 185 |
rf"{key}\s*=\s*([-+]?\d*\.?\d+)",
|
| 186 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 187 |
]
|
|
|
|
| 201 |
except: return "N/A"
|
| 202 |
|
| 203 |
async def call_gemini(prompt, attachment=None):
|
|
|
|
|
|
|
|
|
|
| 204 |
if not KEY_POOL: return "ERROR: NO_KEYS"
|
| 205 |
+
|
| 206 |
img_data = None
|
| 207 |
if attachment:
|
| 208 |
try: img_data = await attachment.read()
|
|
|
|
| 220 |
contents.append(types.Part.from_bytes(data=img_data, mime_type='image/png'))
|
| 221 |
|
| 222 |
response = await loop.run_in_executor(
|
| 223 |
+
None,
|
| 224 |
functools.partial(client.models.generate_content, model="gemini-1.5-flash", contents=contents)
|
| 225 |
)
|
| 226 |
if response and response.text:
|
|
|
|
| 242 |
t = member or ctx.author
|
| 243 |
u = bk_engine.get_user(t.id)
|
| 244 |
elo = u['elo']
|
| 245 |
+
|
|
|
|
| 246 |
if elo < 1300: rank, color = "Iron 学徒 🗑️", 0x717d7e
|
| 247 |
elif elo < 1500: rank, color = "Bronze 资深 🥉", 0xcd7f32
|
| 248 |
elif elo < 1800: rank, color = "Silver 精英 🥈", 0xbdc3c7
|
|
|
|
| 253 |
emb = discord.Embed(title=f"📊 Quant Profile: {t.display_name}", color=color)
|
| 254 |
emb.add_field(name="Rating (ELO)", value=f"**{elo}** ({rank})", inline=True)
|
| 255 |
emb.add_field(name="Assets", value=f"💰 **{u['points']}**", inline=True)
|
| 256 |
+
|
| 257 |
f_info = u['faction'] or "Free Agent"
|
| 258 |
f_emoji = bk_engine.data["factions"].get(f_info, {}).get("emoji", "🌐")
|
| 259 |
emb.add_field(name="Faction", value=f"{f_emoji} {f_info}", inline=True)
|
|
|
|
| 264 |
f"Max MDD: {u.get('mdd_record',0):.2f}%"
|
| 265 |
)
|
| 266 |
emb.add_field(name="Institutional Metrics", value=f"```yaml\n{stats}\n```", inline=False)
|
| 267 |
+
|
| 268 |
if target_avatar := t.avatar:
|
| 269 |
emb.set_thumbnail(url=target_avatar.url)
|
| 270 |
emb.set_footer(text=f"Node: HuggingFace Titan | Session: {SESSION_ID}")
|
|
|
|
| 275 |
all_users = []
|
| 276 |
for uid, d in bk_engine.data["users"].items():
|
| 277 |
all_users.append({"id": uid, "elo": d["elo"], "score": d["highest_score"]})
|
| 278 |
+
|
| 279 |
top = sorted(all_users, key=lambda x: x["elo"], reverse=True)[:10]
|
| 280 |
desc = ""
|
| 281 |
for idx, u in enumerate(top):
|
| 282 |
icon = ["🥇","🥈","🥉"][idx] if idx < 3 else f"{idx+1}."
|
| 283 |
desc += f"{icon} <@{u['id']}> : **{u['elo']}** (Peak: {u['score']:.1f})\n"
|
| 284 |
+
|
| 285 |
emb = discord.Embed(title="🏆 BK-GTA Institutional Leaderboard", description=desc or "No data.", color=0xf1c40f)
|
| 286 |
+
|
| 287 |
f_info = ""
|
| 288 |
for k, v in bk_engine.data["factions"].items():
|
| 289 |
f_info += f"{v['emoji']} {k}: {v['score']} pts "
|
| 290 |
if f_info:
|
| 291 |
emb.add_field(name="⚔️ Faction War", value=f_info, inline=False)
|
| 292 |
+
|
| 293 |
await ctx.send(embed=emb)
|
| 294 |
|
| 295 |
# ==============================================================================
|
|
|
|
| 299 |
async def news(ctx):
|
| 300 |
if not ctx.message.attachments: return await ctx.send("❌ Error: 请上传情报截图。")
|
| 301 |
m = await ctx.send("📡 **[Sentinel] Analyzing Macro Pulse...**")
|
| 302 |
+
|
| 303 |
prompt = (
|
| 304 |
"你是一名顶级对冲基金策略师。请解析截图内容:\n"
|
| 305 |
"1. 评分 (SCORE): -10 (极大利空) 到 +10 (极大利多)。\n"
|
|
|
|
| 308 |
"格式必须为:\nSCORE: [v]\nSUMMARY: [v]\nTAKE: [v]"
|
| 309 |
)
|
| 310 |
res = await call_gemini(prompt, ctx.message.attachments[0])
|
| 311 |
+
|
| 312 |
score = safe_extract_float(res, "SCORE")
|
| 313 |
summary = safe_extract_text(res, "SUMMARY", "TAKE")
|
| 314 |
take = safe_extract_text(res, "TAKE")
|
| 315 |
+
|
| 316 |
color = 0x2ecc71 if score > 0 else 0xe74c3c if score < 0 else 0xbdc3c7
|
| 317 |
emb = discord.Embed(title="📰 Macro Intelligence Analysis", color=color)
|
| 318 |
emb.add_field(name="Sentiment Score", value=f"**{score:+.1f} / 10**", inline=True)
|
| 319 |
emb.add_field(name="Core Catalyst", value=summary, inline=False)
|
| 320 |
emb.add_field(name="Institutional View", value=f"```\n{take}\n```", inline=False)
|
| 321 |
+
|
| 322 |
await m.delete()
|
| 323 |
await ctx.send(embed=emb)
|
| 324 |
|
|
|
|
| 328 |
@bot.command()
|
| 329 |
async def evaluate(ctx):
|
| 330 |
if not ctx.message.attachments: return await ctx.send("❌ Error: 请上传回测或结算截图。")
|
| 331 |
+
|
| 332 |
if audit_lock.locked():
|
| 333 |
return await ctx.send("⚠️ **[System Busy]** 正在处理另一项审计,请稍后。")
|
| 334 |
|
|
|
|
| 341 |
"H_ZH: 中文核心优势或致命风险概括。\n"
|
| 342 |
"格式:\nRET: [v]\nMDD: [v]\nPF: [v]\nSHARPE: [v]\nTRADES: [v]\nS_BASE: [v]\nVIOLATION: [TRUE/FALSE]\nH_ZH: [v]"
|
| 343 |
)
|
| 344 |
+
|
| 345 |
res = await call_gemini(prompt, ctx.message.attachments[0])
|
| 346 |
+
|
| 347 |
if "ERROR" in res:
|
| 348 |
await m.delete()
|
| 349 |
return await ctx.send("🔴 [Audit Crash] 引擎请求失败。")
|
|
|
|
| 355 |
s_base = safe_extract_float(res, "S_BASE")
|
| 356 |
violation = "TRUE" in res.upper()
|
| 357 |
|
|
|
|
| 358 |
if violation or mdd > 35.0:
|
| 359 |
s_final = 0.0
|
| 360 |
status = "❌ REJECTED (High Risk)"
|
| 361 |
color = 0xe74c3c
|
| 362 |
else:
|
|
|
|
| 363 |
weight_sample = math.log10(max(trades, 1)) / 1.5
|
| 364 |
weight_risk = (sharpe / 2.0) if sharpe > 0 else 0.5
|
| 365 |
s_final = min(max(s_base * weight_sample * weight_risk, 0.0), 100.0)
|
|
|
|
| 368 |
|
| 369 |
bk_engine.update_audit(ctx.author.id, s_final, mdd, sharpe)
|
| 370 |
highlights = safe_extract_text(res, "H_ZH")
|
| 371 |
+
|
| 372 |
emb = discord.Embed(title="🛡️ Quantitative Audit Report", color=color)
|
| 373 |
emb.add_field(name="Verdict", value=f"**{status}**", inline=False)
|
| 374 |
metrics = f"Return: {ret}% | MDD: {mdd}%\nSharpe: {sharpe} | Trades: {trades}"
|
| 375 |
emb.add_field(name="Key Metrics", value=f"```\n{metrics}\n```", inline=False)
|
| 376 |
emb.add_field(name="Logic Score", value=f"**{s_final:.2f} / 100**", inline=True)
|
| 377 |
emb.add_field(name="Highlights", value=highlights, inline=False)
|
| 378 |
+
|
| 379 |
await m.delete()
|
| 380 |
await ctx.send(embed=emb)
|
| 381 |
|
|
|
|
| 387 |
async def open_bet(ctx, *, topic):
|
| 388 |
bid = str(random.randint(100, 999))
|
| 389 |
bk_engine.active_bets[bid] = {
|
| 390 |
+
"topic": topic, "up": 0, "down": 0,
|
| 391 |
"u_up": {}, "u_down": {}, "status": "OPEN",
|
| 392 |
"time": str(datetime.datetime.now())
|
| 393 |
}
|
|
|
|
| 397 |
async def bet(ctx, bid: str, side: str, amt: int):
|
| 398 |
if bid not in bk_engine.active_bets or bk_engine.active_bets[bid]["status"] != "OPEN":
|
| 399 |
return await ctx.send("❌ 盘口不存在或已关闭。")
|
| 400 |
+
|
| 401 |
u = bk_engine.get_user(ctx.author.id)
|
| 402 |
if u["points"] < amt or amt <= 0:
|
| 403 |
return await ctx.send("❌ 资产点数不足。")
|
| 404 |
+
|
| 405 |
side = side.upper()
|
| 406 |
if side not in ["UP", "DOWN"]: return await ctx.send("❌ 必须选择 UP 或 DOWN。")
|
| 407 |
+
|
| 408 |
u["points"] -= amt
|
| 409 |
b = bk_engine.active_bets[bid]
|
| 410 |
pool = "u_up" if side == "UP" else "u_down"
|
| 411 |
b[side.lower()] += amt
|
| 412 |
b[pool][str(ctx.author.id)] = b[pool].get(str(ctx.author.id), 0) + amt
|
| 413 |
+
|
| 414 |
bk_engine.save_db()
|
| 415 |
await ctx.send(f"✅ 已确认:{ctx.author.name} 向 {side} 注入 {amt} 点资产。")
|
| 416 |
|
|
|
|
| 420 |
if bid not in bk_engine.active_bets: return
|
| 421 |
b = bk_engine.active_bets[bid]
|
| 422 |
winner = winner.upper()
|
| 423 |
+
|
| 424 |
win_pool = b["u_up"] if winner == "UP" else b["u_down"]
|
| 425 |
total_w = b["up"] if winner == "UP" else b["down"]
|
| 426 |
total_l = b["down"] if winner == "UP" else b["up"]
|
| 427 |
+
|
| 428 |
if total_w > 0:
|
| 429 |
for uid, amt in win_pool.items():
|
| 430 |
profit = (amt / total_w) * total_l
|
| 431 |
bk_engine.get_user(int(uid))["points"] += int(amt + profit)
|
| 432 |
+
|
| 433 |
b["status"] = "SETTLED"
|
| 434 |
bk_engine.save_db()
|
| 435 |
await ctx.send(f"🏁 盘口 {bid} 已结算!胜利方:**{winner}**。资产已自动划转。")
|
|
|
|
| 448 |
async def accept(ctx):
|
| 449 |
if ctx.author.id not in bk_engine.pending_challenges:
|
| 450 |
return await ctx.send("❌ 没有待处理的决斗请求。")
|
| 451 |
+
|
| 452 |
req = bk_engine.pending_challenges.pop(ctx.author.id)
|
| 453 |
did = req["id"]
|
| 454 |
bk_engine.active_duels[did] = {
|
| 455 |
+
"p1": req["challenger"], "p2": ctx.author.id,
|
| 456 |
"s1": None, "s2": None, "t": str(datetime.datetime.now())
|
| 457 |
}
|
| 458 |
await ctx.send(f"🔥 **竞技场锁定!** 决斗 ID: `{did}`\n请双方通过 `!duel_submit {did}` 提交审计截图。")
|
|
|
|
| 461 |
async def duel_submit(ctx, did: str):
|
| 462 |
if did not in bk_engine.active_duels: return
|
| 463 |
if not ctx.message.attachments: return
|
| 464 |
+
|
| 465 |
duel = bk_engine.active_duels[did]
|
| 466 |
if ctx.author.id not in [duel["p1"], duel["p2"]]: return
|
| 467 |
+
|
| 468 |
m = await ctx.send("🔍 正在审计决斗数据...")
|
| 469 |
res = await call_gemini("Extract SCORE:[0-100]. Format: SCORE:[v]", ctx.message.attachments[0])
|
| 470 |
score = safe_extract_float(res, "SCORE")
|
| 471 |
+
|
| 472 |
if ctx.author.id == duel["p1"]: duel["s1"] = score
|
| 473 |
else: duel["s2"] = score
|
| 474 |
+
|
| 475 |
if duel["s1"] is not None and duel["s2"] is not None:
|
| 476 |
p1, p2 = duel["p1"], duel["p2"]
|
| 477 |
s1, s2 = duel["s1"], duel["s2"]
|
| 478 |
wid, lid = (p1, p2) if s1 > s2 else (p2, p1)
|
| 479 |
w_elo, l_elo, streak = bk_engine.update_elo(wid, lid)
|
| 480 |
+
|
| 481 |
emb = discord.Embed(title="🏆 Duel Arena Verdict", color=0xf1c40f)
|
| 482 |
emb.add_field(name="Winner", value=f"<@{wid}>\nScore: {max(s1,s2)}\nELO: {w_elo}", inline=True)
|
| 483 |
emb.add_field(name="Loser", value=f"<@{lid}>\nScore: {min(s1,s2)}\nELO: {l_elo}", inline=True)
|
|
|
|
| 493 |
# ==============================================================================
|
| 494 |
@bot.command()
|
| 495 |
async def viral_audit(ctx):
|
|
|
|
|
|
|
|
|
|
| 496 |
if not ctx.message.attachments: return
|
| 497 |
m = await ctx.send("🎨 **[GPU-Node] 正在渲染视觉海报...**")
|
| 498 |
+
|
| 499 |
p = "Extract RET: [v] and SHARPE: [v]. Format: RET: [v] SHARPE: [v]"
|
| 500 |
res = await call_gemini(p, ctx.message.attachments[0])
|
| 501 |
ret, sharpe = safe_extract_float(res, "RET"), safe_extract_float(res, "SHARPE")
|
| 502 |
|
| 503 |
try:
|
| 504 |
W, H = 600, 800
|
|
|
|
| 505 |
base = Image.new('RGB', (W, H), (18, 20, 30))
|
| 506 |
draw = ImageDraw.Draw(base)
|
| 507 |
|
|
|
|
| 508 |
for i in range(0, W, 40): draw.line([(i, 0), (i, H)], fill=(30, 35, 45), width=1)
|
| 509 |
for j in range(0, H, 40): draw.line([(0, j), (W, j)], fill=(30, 35, 45), width=1)
|
| 510 |
+
|
|
|
|
| 511 |
draw.rectangle([10, 10, 590, 790], outline=(46, 204, 113), width=2)
|
| 512 |
draw.rectangle([20, 20, 580, 780], outline=(46, 204, 113), width=5)
|
| 513 |
|
|
|
|
| 514 |
draw.text((40, 60), "BK-GTA QUANTITATIVE SYSTEM", fill=(255, 255, 255))
|
| 515 |
draw.text((40, 150), f"TRADER: {ctx.author.name.upper()}", fill=(46, 204, 113))
|
| 516 |
draw.text((40, 260), f"ALPHA RET: +{ret}%", fill=(255, 255, 255))
|
|
|
|
| 542 |
auto_backup.start()
|
| 543 |
await bot.change_presence(
|
| 544 |
activity=discord.Activity(
|
| 545 |
+
type=discord.ActivityType.watching,
|
| 546 |
name="HuggingFace Node"
|
| 547 |
)
|
| 548 |
)
|
|
|
|
| 556 |
async def on_resumed():
|
| 557 |
logger.info("✅ [Gateway] Session resumed successfully.")
|
| 558 |
|
| 559 |
+
# ==============================================================================
|
| 560 |
+
# 12. 启动入口 — 关键修复区域
|
| 561 |
+
# ==============================================================================
|
| 562 |
def run_bot():
|
| 563 |
"""
|
| 564 |
+
[核心修复] bot.run() 内部调用 asyncio.run(),会注册信号处理器,
|
| 565 |
+
但信号处理器只能在主线程注册。在子线程中必须:
|
| 566 |
+
1. 手动创建事件循环 asyncio.new_event_loop()
|
| 567 |
+
2. 使用 bot.start()(协程版本)而非 bot.run()
|
| 568 |
"""
|
| 569 |
if not DISCORD_TOKEN:
|
| 570 |
+
logger.critical("❌ [FATAL] DISCORD_TOKEN is missing! Set it in HuggingFace Secrets.")
|
| 571 |
return
|
| 572 |
+
|
| 573 |
+
logger.info("🚀 [Bot Thread] Initializing Discord connection...")
|
| 574 |
+
|
| 575 |
+
# ====== 关键:为子线程创建专属事件循环 ======
|
| 576 |
+
loop = asyncio.new_event_loop()
|
| 577 |
+
asyncio.set_event_loop(loop)
|
| 578 |
+
|
| 579 |
try:
|
| 580 |
+
# bot.start() 是纯协程,不会触发信号注册
|
| 581 |
+
loop.run_until_complete(bot.start(DISCORD_TOKEN, reconnect=True))
|
| 582 |
except discord.LoginFailure:
|
| 583 |
+
logger.critical(
|
| 584 |
+
"❌ [FATAL] DISCORD_TOKEN is invalid!\n"
|
| 585 |
+
"Go to https://discord.com/developers/applications → Bot → Reset Token"
|
| 586 |
+
)
|
| 587 |
except discord.PrivilegedIntentsRequired:
|
| 588 |
logger.critical(
|
| 589 |
"❌ [FATAL] Privileged Intents not enabled!\n"
|
| 590 |
+
"Go to Discord Developer Portal → Bot tab →\n"
|
| 591 |
+
"Enable 'SERVER MEMBERS INTENT' and 'MESSAGE CONTENT INTENT'"
|
| 592 |
)
|
| 593 |
except Exception as e:
|
| 594 |
+
logger.error(f"❌ [Bot Fatal Error] {e}")
|
| 595 |
traceback.print_exc()
|
| 596 |
+
finally:
|
| 597 |
+
logger.warning("🔴 [Bot Thread] Event loop ending, cleaning up...")
|
| 598 |
+
try:
|
| 599 |
+
# 清理所有未完成的任务
|
| 600 |
+
pending = asyncio.all_tasks(loop)
|
| 601 |
+
for task in pending:
|
| 602 |
+
task.cancel()
|
| 603 |
+
# 等待取消完成
|
| 604 |
+
if pending:
|
| 605 |
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
| 606 |
+
if not bot.is_closed():
|
| 607 |
+
loop.run_until_complete(bot.close())
|
| 608 |
+
except Exception as cleanup_err:
|
| 609 |
+
logger.error(f"Cleanup error: {cleanup_err}")
|
| 610 |
+
finally:
|
| 611 |
+
loop.close()
|
| 612 |
+
logger.error("🔴 [Bot Thread] Discord bot has stopped.")
|
| 613 |
|
| 614 |
|
| 615 |
if __name__ == "__main__":
|
| 616 |
+
logger.info("=" * 60)
|
| 617 |
+
logger.info(" BK-GTA V62.9-Titan Starting...")
|
| 618 |
+
logger.info(f" Token present: {'YES' if DISCORD_TOKEN else 'NO (!!)'}")
|
| 619 |
+
logger.info(f" Token length: {len(DISCORD_TOKEN)} chars")
|
| 620 |
+
logger.info(f" Gemini keys: {len(KEY_POOL)}")
|
| 621 |
+
logger.info(f" DB path: {DB_PATH}")
|
| 622 |
+
logger.info("=" * 60)
|
| 623 |
+
|
| 624 |
+
# 1. 启动 Discord Bot 后台线程
|
| 625 |
+
bot_thread = threading.Thread(target=run_bot, daemon=True)
|
| 626 |
+
bot_thread.start()
|
| 627 |
+
logger.info("📡 [Main] Bot thread launched.")
|
| 628 |
+
|
| 629 |
+
# 2. 给 bot 线程一点启动时间
|
| 630 |
time.sleep(3)
|
| 631 |
+
|
| 632 |
+
# 3. 启动 Gradio 前台哨兵 (占据主线程)
|
| 633 |
+
logger.info("📡 [Main] Starting Gradio on port 7860...")
|
| 634 |
iface = gr.Interface(
|
| 635 |
fn=get_status,
|
| 636 |
inputs=None,
|
|
|
|
| 638 |
title="BK-GTA Sentinel AI",
|
| 639 |
description="Physical architecture monitor. Logic Integrity: Secure."
|
| 640 |
)
|
| 641 |
+
iface.launch(server_name="0.0.0.0", server_port=7860)
|
| 642 |
+
```
|
| 643 |
+
|
| 644 |
+
---
|
| 645 |
+
|
| 646 |
+
## 改了什么(只有第 12 节不同)
|
| 647 |
+
|
| 648 |
+
| 旧代码(崩溃) | 新代码(修复) |
|
| 649 |
+
|---|---|
|
| 650 |
+
| `bot.run(DISCORD_TOKEN)` | `loop.run_until_complete(bot.start(DISCORD_TOKEN))` |
|
| 651 |
+
| 无事件循环管理 | `asyncio.new_event_loop()` + `asyncio.set_event_loop(loop)` |
|
| 652 |
+
| 无启动诊断 | 启动时打印 Token 长度、密钥数、DB 路径 |
|
| 653 |
+
| 无 cleanup | `finally` 中取消任务 + 关闭 bot + 关闭 loop |
|
| 654 |
+
|
| 655 |
+
**核心一行修复:**
|
| 656 |
+
```python
|
| 657 |
+
# ❌ 旧:子线程中会因信号注册崩溃
|
| 658 |
+
bot.run(DISCORD_TOKEN, reconnect=True)
|
| 659 |
+
|
| 660 |
+
# ✅ 新:纯协程,不注册信号,子
|