Spaces:
No application file
No application file
Update app.py
Browse files
app.py
CHANGED
|
@@ -20,67 +20,60 @@ from PIL import Image, ImageDraw, ImageFont
|
|
| 20 |
import gradio as gr
|
| 21 |
|
| 22 |
# ==============================================================================
|
| 23 |
-
# 0. 机构级审计日志 (Institutional
|
| 24 |
# ==============================================================================
|
| 25 |
logging.basicConfig(
|
| 26 |
level=logging.INFO,
|
| 27 |
format='%(asctime)s [%(levelname)s] %(message)s',
|
| 28 |
handlers=[logging.StreamHandler(sys.stdout)]
|
| 29 |
)
|
| 30 |
-
logger = logging.getLogger("
|
| 31 |
|
| 32 |
# ==============================================================================
|
| 33 |
-
# 1. 物理层
|
| 34 |
# ==============================================================================
|
| 35 |
def get_status():
|
| 36 |
"""
|
| 37 |
-
提供给 UptimeRobot
|
| 38 |
"""
|
| 39 |
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
| 40 |
latency = "N/A"
|
| 41 |
try:
|
| 42 |
if bot and bot.is_ready():
|
|
|
|
| 43 |
latency = f"{round(bot.latency * 1000, 2)}ms"
|
| 44 |
-
except:
|
| 45 |
-
|
| 46 |
|
| 47 |
return {
|
| 48 |
-
"status": "BK-GTA-
|
| 49 |
-
"
|
| 50 |
-
"
|
|
|
|
|
|
|
| 51 |
"server_time": now,
|
| 52 |
-
"
|
| 53 |
-
"
|
| 54 |
}
|
| 55 |
|
| 56 |
# ==============================================================================
|
| 57 |
-
# 2. 密钥池与环境配置 (Key Rotation
|
| 58 |
# ==============================================================================
|
| 59 |
-
DISCORD_TOKEN = os.environ.get('DISCORD_TOKEN')
|
| 60 |
SESSION_ID = random.randint(10000, 99999)
|
| 61 |
|
| 62 |
# [CORE] 8 密钥轮询加载器
|
| 63 |
raw_keys = os.environ.get('GEMINI_KEYS_POOL', '')
|
| 64 |
-
KEY_POOL = []
|
| 65 |
-
|
| 66 |
-
if raw_keys:
|
| 67 |
-
parts = raw_keys.split(',')
|
| 68 |
-
for k in parts:
|
| 69 |
-
clean_key = k.strip()
|
| 70 |
-
if clean_key:
|
| 71 |
-
KEY_POOL.append(clean_key)
|
| 72 |
-
logger.info(f"✅ [System] Loaded GEMINI_KEYS_POOL with {len(KEY_POOL)} keys.")
|
| 73 |
-
else:
|
| 74 |
-
# 兼容单密钥模式
|
| 75 |
single = os.environ.get('GEMINI_KEY')
|
| 76 |
-
if single:
|
| 77 |
-
KEY_POOL.append(single)
|
| 78 |
-
logger.warning("⚠️ [System] Running in SINGLE KEY mode.")
|
| 79 |
|
| 80 |
if not KEY_POOL:
|
| 81 |
-
logger.critical("❌ [CRITICAL] NO API KEYS FOUND! Please set GEMINI_KEYS_POOL
|
| 82 |
|
| 83 |
-
#
|
| 84 |
intents = discord.Intents.default()
|
| 85 |
intents.message_content = True
|
| 86 |
intents.members = True
|
|
@@ -88,21 +81,18 @@ bot = commands.Bot(command_prefix='!', intents=intents)
|
|
| 88 |
|
| 89 |
# 并发审计锁
|
| 90 |
audit_lock = asyncio.Lock()
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
if os.path.exists('/data'):
|
| 94 |
-
DB_PATH_CONFIG = '/data/bk_gta_v62_unified.json'
|
| 95 |
-
logger.info(f"📂 [Storage] Using Persistent Storage: {DB_PATH_CONFIG}")
|
| 96 |
-
else:
|
| 97 |
-
DB_PATH_CONFIG = 'bk_gta_v62_unified.json'
|
| 98 |
-
logger.warning(f"⚠️ [Storage] Using Ephemeral Storage.")
|
| 99 |
|
| 100 |
# ==============================================================================
|
| 101 |
# 3. 状态引擎 (BKEngine Core)
|
| 102 |
# ==============================================================================
|
| 103 |
class BKEngine:
|
|
|
|
|
|
|
|
|
|
| 104 |
def __init__(self):
|
| 105 |
-
self.db_file =
|
| 106 |
self.data = self._load_db()
|
| 107 |
self.pending_challenges = {}
|
| 108 |
self.active_duels = {}
|
|
@@ -122,15 +112,15 @@ class BKEngine:
|
|
| 122 |
|
| 123 |
def _init_schema(self):
|
| 124 |
return {
|
| 125 |
-
"version": "62.9-
|
| 126 |
"users": {},
|
| 127 |
"factions": {
|
| 128 |
-
"BULLS": {"score": 0, "emoji": "🐂", "
|
| 129 |
-
"BEARS": {"score": 0, "emoji": "🐻", "
|
| 130 |
},
|
| 131 |
"prize_pool": 1000000.0,
|
| 132 |
"logs": [],
|
| 133 |
-
"
|
| 134 |
}
|
| 135 |
|
| 136 |
def save_db(self):
|
|
@@ -140,33 +130,31 @@ class BKEngine:
|
|
| 140 |
json.dump(self.data, f, ensure_ascii=False, indent=4)
|
| 141 |
os.replace(temp, self.db_file)
|
| 142 |
except Exception as e:
|
| 143 |
-
logger.error(f"💾 [DB Error]
|
| 144 |
|
| 145 |
def get_user(self, user_id):
|
| 146 |
uid = str(user_id)
|
| 147 |
if uid not in self.data["users"]:
|
| 148 |
self.data["users"][uid] = {
|
| 149 |
-
"elo": 1200,
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
-
"total_score": 0.0,
|
| 154 |
-
"highest_score": 0.0,
|
| 155 |
-
"win_streak": 0,
|
| 156 |
-
"mdd_record": 0.0,
|
| 157 |
-
"sharpe_record": 0.0,
|
| 158 |
-
"last_active": str(datetime.datetime.now())
|
| 159 |
}
|
| 160 |
return self.data["users"][uid]
|
| 161 |
|
| 162 |
-
def
|
| 163 |
u = self.get_user(user_id)
|
| 164 |
u["audits_count"] += 1
|
| 165 |
u["total_score"] += score
|
| 166 |
-
if score > u.get("highest_score", 0):
|
|
|
|
| 167 |
if mdd is not None: u["mdd_record"] = mdd
|
| 168 |
if sharpe is not None: u["sharpe_record"] = sharpe
|
| 169 |
u["points"] += 50
|
|
|
|
|
|
|
|
|
|
| 170 |
u["last_active"] = str(datetime.datetime.now())
|
| 171 |
self.save_db()
|
| 172 |
|
|
@@ -175,20 +163,24 @@ class BKEngine:
|
|
| 175 |
K = 32
|
| 176 |
Rw, Rl = w["elo"], l["elo"]
|
| 177 |
Ew = 1 / (1 + 10 ** ((Rl - Rw) / 400))
|
|
|
|
| 178 |
w["elo"] = round(Rw + K * (1 - Ew))
|
| 179 |
l["elo"] = round(Rl + K * (0 - Ew))
|
|
|
|
| 180 |
w["win_streak"] += 1
|
| 181 |
l["win_streak"] = 0
|
| 182 |
-
|
|
|
|
| 183 |
self.data["factions"][w["faction"]]["score"] += 15
|
| 184 |
-
self.data["factions"][w["faction"]]["
|
|
|
|
| 185 |
self.save_db()
|
| 186 |
return w["elo"], l["elo"], w["win_streak"]
|
| 187 |
|
| 188 |
bk_engine = BKEngine()
|
| 189 |
|
| 190 |
# ==============================================================================
|
| 191 |
-
# 4. 辅助内核 (
|
| 192 |
# ==============================================================================
|
| 193 |
def safe_extract_float(text, key):
|
| 194 |
try:
|
|
@@ -198,78 +190,80 @@ def safe_extract_float(text, key):
|
|
| 198 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 199 |
]
|
| 200 |
for p in patterns:
|
| 201 |
-
match = re.search(p, text, re.
|
| 202 |
if match: return float(match.group(1))
|
| 203 |
return 0.0
|
| 204 |
except: return 0.0
|
| 205 |
|
| 206 |
-
def safe_extract_text(text,
|
| 207 |
try:
|
| 208 |
-
p = rf"{
|
| 209 |
-
match = re.search(p, text, re.
|
| 210 |
if match:
|
| 211 |
return match.group(1).strip().replace('*', '').replace('`', '').strip()
|
| 212 |
return "N/A"
|
| 213 |
except: return "N/A"
|
| 214 |
|
| 215 |
-
async def
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
|
| 219 |
if attachment:
|
| 220 |
-
try:
|
| 221 |
-
except: return "ERROR:
|
| 222 |
|
| 223 |
loop = asyncio.get_running_loop()
|
| 224 |
-
|
| 225 |
-
random.shuffle(
|
| 226 |
|
| 227 |
-
for
|
| 228 |
try:
|
| 229 |
client = genai.Client(api_key=api_key)
|
| 230 |
contents = [prompt]
|
| 231 |
-
if
|
| 232 |
-
contents.append(types.Part.from_bytes(data=
|
| 233 |
|
| 234 |
response = await loop.run_in_executor(
|
| 235 |
None,
|
| 236 |
functools.partial(client.models.generate_content, model="gemini-1.5-flash", contents=contents)
|
| 237 |
)
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
except Exception as e:
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
logger.warning(f"⚠️ Key {i+1} Rate Limited. Switching...")
|
| 245 |
continue
|
| 246 |
-
logger.error(f"
|
| 247 |
continue
|
| 248 |
|
| 249 |
return "ERROR: ALL_KEYS_EXHAUSTED"
|
| 250 |
|
| 251 |
# ==============================================================================
|
| 252 |
-
# 5. 模块一:全网天梯
|
| 253 |
# ==============================================================================
|
| 254 |
@bot.command()
|
| 255 |
async def profile(ctx, member: discord.Member = None):
|
| 256 |
-
|
| 257 |
-
u = bk_engine.get_user(
|
| 258 |
elo = u['elo']
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
elif elo <
|
| 263 |
-
elif elo <
|
| 264 |
-
elif elo <
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
emb
|
| 269 |
-
emb.add_field(name="
|
|
|
|
| 270 |
|
| 271 |
f_info = u['faction'] or "Free Agent"
|
| 272 |
-
f_emoji = bk_engine.data["factions"].get(f_info, {}).get("emoji", "")
|
| 273 |
emb.add_field(name="Faction", value=f"{f_emoji} {f_info}", inline=True)
|
| 274 |
|
| 275 |
stats = (
|
|
@@ -277,147 +271,165 @@ async def profile(ctx, member: discord.Member = None):
|
|
| 277 |
f"Peak Score: {u['highest_score']:.1f}\n"
|
| 278 |
f"Max MDD: {u.get('mdd_record',0):.2f}%"
|
| 279 |
)
|
| 280 |
-
emb.add_field(name="
|
| 281 |
|
| 282 |
-
if
|
| 283 |
-
|
|
|
|
| 284 |
await ctx.send(embed=emb)
|
| 285 |
|
| 286 |
@bot.command()
|
| 287 |
async def leaderboard(ctx):
|
| 288 |
-
|
| 289 |
-
|
|
|
|
| 290 |
|
|
|
|
| 291 |
desc = ""
|
| 292 |
-
for
|
| 293 |
-
icon = ["🥇","🥈","🥉"][
|
| 294 |
desc += f"{icon} <@{u['id']}> : **{u['elo']}** (Peak: {u['score']:.1f})\n"
|
| 295 |
|
| 296 |
-
emb = discord.Embed(title="🏆
|
| 297 |
|
| 298 |
-
|
| 299 |
for k, v in bk_engine.data["factions"].items():
|
| 300 |
-
|
| 301 |
-
if
|
| 302 |
-
|
|
|
|
| 303 |
await ctx.send(embed=emb)
|
| 304 |
|
| 305 |
# ==============================================================================
|
| 306 |
-
# 6. 模块二:宏观情报
|
| 307 |
# ==============================================================================
|
| 308 |
@bot.command()
|
| 309 |
async def news(ctx):
|
| 310 |
-
if not ctx.message.attachments: return await ctx.send("❌
|
| 311 |
-
m = await ctx.send("📡 **[
|
| 312 |
|
| 313 |
prompt = (
|
| 314 |
-
"
|
| 315 |
-
"
|
|
|
|
|
|
|
|
|
|
| 316 |
)
|
| 317 |
-
res = await
|
| 318 |
|
| 319 |
score = safe_extract_float(res, "SCORE")
|
| 320 |
summary = safe_extract_text(res, "SUMMARY", "TAKE")
|
| 321 |
take = safe_extract_text(res, "TAKE")
|
| 322 |
|
| 323 |
-
|
| 324 |
-
emb.
|
| 325 |
-
emb.add_field(name="
|
| 326 |
-
emb.add_field(name="
|
|
|
|
| 327 |
|
| 328 |
await m.delete()
|
| 329 |
await ctx.send(embed=emb)
|
| 330 |
|
| 331 |
# ==============================================================================
|
| 332 |
-
# 7. 模块三:量化审计
|
| 333 |
# ==============================================================================
|
| 334 |
@bot.command()
|
| 335 |
async def evaluate(ctx):
|
| 336 |
-
if not ctx.message.attachments: return await ctx.send("❌
|
|
|
|
| 337 |
if audit_lock.locked():
|
| 338 |
-
return await ctx.send("⚠️ **[System Busy]**
|
| 339 |
|
| 340 |
async with audit_lock:
|
| 341 |
-
m = await ctx.send("🛡️ **[Audit Engine]
|
| 342 |
prompt = (
|
| 343 |
-
"
|
| 344 |
-
"
|
| 345 |
-
"
|
|
|
|
|
|
|
| 346 |
)
|
| 347 |
|
| 348 |
-
res = await
|
| 349 |
|
| 350 |
if "ERROR" in res:
|
| 351 |
await m.delete()
|
| 352 |
-
return await ctx.send(
|
| 353 |
|
| 354 |
ret = safe_extract_float(res, "RET")
|
| 355 |
mdd = safe_extract_float(res, "MDD")
|
| 356 |
-
pf = safe_extract_float(res, "PF")
|
| 357 |
sharpe = safe_extract_float(res, "SHARPE")
|
| 358 |
trades = int(safe_extract_float(res, "TRADES"))
|
| 359 |
s_base = safe_extract_float(res, "S_BASE")
|
| 360 |
violation = "TRUE" in res.upper()
|
| 361 |
|
|
|
|
| 362 |
if violation or mdd > 35.0:
|
| 363 |
s_final = 0.0
|
| 364 |
-
status = "REJECTED (High Risk)"
|
| 365 |
color = 0xe74c3c
|
| 366 |
else:
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
|
|
|
| 371 |
color = 0x2ecc71
|
| 372 |
|
| 373 |
-
bk_engine.
|
|
|
|
| 374 |
|
| 375 |
-
emb = discord.Embed(title="🛡️ Audit Report", color=color)
|
| 376 |
-
emb.add_field(name="Verdict", value=status, inline=False)
|
| 377 |
-
|
| 378 |
-
emb.add_field(name="
|
| 379 |
-
emb.add_field(name="
|
|
|
|
| 380 |
|
| 381 |
await m.delete()
|
| 382 |
await ctx.send(embed=emb)
|
| 383 |
|
| 384 |
# ==============================================================================
|
| 385 |
-
# 8. 模块四:博弈盘口
|
| 386 |
# ==============================================================================
|
| 387 |
@bot.command()
|
| 388 |
@commands.has_permissions(administrator=True)
|
| 389 |
async def open_bet(ctx, *, topic):
|
| 390 |
bid = str(random.randint(100, 999))
|
| 391 |
bk_engine.active_bets[bid] = {
|
| 392 |
-
"topic": topic,
|
| 393 |
-
"
|
| 394 |
-
"
|
| 395 |
-
"status": "OPEN",
|
| 396 |
-
"start_time": str(datetime.datetime.now())
|
| 397 |
}
|
| 398 |
-
await ctx.send(f"🎰 **
|
| 399 |
|
| 400 |
@bot.command()
|
| 401 |
async def bet(ctx, bid: str, side: str, amt: int):
|
| 402 |
-
if bid not in bk_engine.active_bets or bk_engine.active_bets[bid]["status"] != "OPEN":
|
|
|
|
|
|
|
| 403 |
u = bk_engine.get_user(ctx.author.id)
|
| 404 |
-
if u["points"] < amt
|
|
|
|
| 405 |
|
| 406 |
-
|
| 407 |
-
if side not in ["UP", "DOWN"]: return await ctx.send("❌
|
| 408 |
|
| 409 |
-
|
| 410 |
b = bk_engine.active_bets[bid]
|
|
|
|
| 411 |
b[side.lower()] += amt
|
| 412 |
b[pool][str(ctx.author.id)] = b[pool].get(str(ctx.author.id), 0) + amt
|
|
|
|
| 413 |
bk_engine.save_db()
|
| 414 |
-
await ctx.send(f"✅
|
| 415 |
|
| 416 |
@bot.command()
|
| 417 |
@commands.has_permissions(administrator=True)
|
| 418 |
async def settle_bet(ctx, bid: str, winner: str):
|
| 419 |
if bid not in bk_engine.active_bets: return
|
| 420 |
-
b = bk_engine.active_bets[bid]
|
|
|
|
| 421 |
|
| 422 |
win_pool = b["u_up"] if winner == "UP" else b["u_down"]
|
| 423 |
total_w = b["up"] if winner == "UP" else b["down"]
|
|
@@ -425,29 +437,35 @@ async def settle_bet(ctx, bid: str, winner: str):
|
|
| 425 |
|
| 426 |
if total_w > 0:
|
| 427 |
for uid, amt in win_pool.items():
|
| 428 |
-
profit = (amt/total_w) * total_l
|
| 429 |
bk_engine.get_user(int(uid))["points"] += int(amt + profit)
|
| 430 |
|
| 431 |
b["status"] = "SETTLED"
|
| 432 |
bk_engine.save_db()
|
| 433 |
-
await ctx.send(f"🏁
|
| 434 |
|
| 435 |
# ==============================================================================
|
| 436 |
-
# 9. 模块五:PVP 竞技场
|
| 437 |
# ==============================================================================
|
| 438 |
@bot.command()
|
| 439 |
async def challenge(ctx, member: discord.Member):
|
| 440 |
if member.id == ctx.author.id: return
|
| 441 |
-
did = f"D-{random.randint(1000,9999)}"
|
| 442 |
-
bk_engine.pending_challenges[member.id] = {"
|
| 443 |
-
await ctx.send(f"⚔️ {ctx.author.name}
|
| 444 |
|
| 445 |
@bot.command()
|
| 446 |
async def accept(ctx):
|
| 447 |
-
if ctx.author.id not in bk_engine.pending_challenges:
|
|
|
|
|
|
|
| 448 |
req = bk_engine.pending_challenges.pop(ctx.author.id)
|
| 449 |
-
|
| 450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
|
| 452 |
@bot.command()
|
| 453 |
async def duel_submit(ctx, did: str):
|
|
@@ -457,8 +475,8 @@ async def duel_submit(ctx, did: str):
|
|
| 457 |
duel = bk_engine.active_duels[did]
|
| 458 |
if ctx.author.id not in [duel["p1"], duel["p2"]]: return
|
| 459 |
|
| 460 |
-
m = await ctx.send("🔍
|
| 461 |
-
res = await
|
| 462 |
score = safe_extract_float(res, "SCORE")
|
| 463 |
|
| 464 |
if ctx.author.id == duel["p1"]: duel["s1"] = score
|
|
@@ -468,97 +486,102 @@ async def duel_submit(ctx, did: str):
|
|
| 468 |
p1, p2 = duel["p1"], duel["p2"]
|
| 469 |
s1, s2 = duel["s1"], duel["s2"]
|
| 470 |
wid, lid = (p1, p2) if s1 > s2 else (p2, p1)
|
| 471 |
-
w_elo, l_elo,
|
| 472 |
|
| 473 |
-
emb = discord.Embed(title="🏆 Duel
|
| 474 |
-
emb.add_field(name="Winner", value=f"<@{wid}>\nScore: {max(s1,s2)}\nELO: {w_elo}")
|
| 475 |
-
emb.add_field(name="Loser", value=f"<@{lid}>\nScore: {min(s1,s2)}\nELO: {l_elo}")
|
|
|
|
| 476 |
await ctx.send(embed=emb)
|
| 477 |
del bk_engine.active_duels[did]
|
| 478 |
else:
|
| 479 |
-
await ctx.send(f"✅
|
| 480 |
await m.delete()
|
| 481 |
|
| 482 |
# ==============================================================================
|
| 483 |
-
# 10. 模块六:海报渲染
|
| 484 |
# ==============================================================================
|
| 485 |
@bot.command()
|
| 486 |
async def viral_audit(ctx):
|
| 487 |
"""
|
| 488 |
-
[完
|
| 489 |
"""
|
| 490 |
-
if not ctx.message.attachments:
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
m = await ctx.send("🎨 **[GPU-Node] Rendering Institutional Poster...**")
|
| 494 |
|
| 495 |
-
|
| 496 |
-
res = await
|
| 497 |
ret, sharpe = safe_extract_float(res, "RET"), safe_extract_float(res, "SHARPE")
|
| 498 |
|
| 499 |
try:
|
| 500 |
W, H = 600, 800
|
| 501 |
-
|
|
|
|
| 502 |
draw = ImageDraw.Draw(base)
|
| 503 |
|
|
|
|
| 504 |
for i in range(0, W, 40): draw.line([(i, 0), (i, H)], fill=(30, 35, 45), width=1)
|
| 505 |
for j in range(0, H, 40): draw.line([(0, j), (W, j)], fill=(30, 35, 45), width=1)
|
| 506 |
|
|
|
|
| 507 |
draw.rectangle([10, 10, 590, 790], outline=(46, 204, 113), width=2)
|
| 508 |
draw.rectangle([20, 20, 580, 780], outline=(46, 204, 113), width=5)
|
| 509 |
|
|
|
|
| 510 |
draw.text((40, 60), "BK-GTA QUANTITATIVE SYSTEM", fill=(255, 255, 255))
|
| 511 |
-
draw.text((40, 150), f"TRADER
|
| 512 |
-
draw.text((40, 260), f"ALPHA
|
| 513 |
-
draw.text((40, 360), f"SHARPE
|
| 514 |
-
draw.text((40, 460), f"SESSION: {SESSION_ID}", fill=(
|
| 515 |
-
|
| 516 |
-
draw.text((40, 720), "VERIFIED BY BK-GTA SENTINEL AI", fill=(46, 204, 113))
|
| 517 |
|
| 518 |
fp = io.BytesIO()
|
| 519 |
base.save(fp, 'PNG')
|
| 520 |
fp.seek(0)
|
| 521 |
await ctx.send(file=discord.File(fp, filename=f"Viral_{ctx.author.id}.png"))
|
| 522 |
-
|
| 523 |
except Exception as e:
|
| 524 |
logger.error(f"Render Error: {e}")
|
| 525 |
-
await ctx.send(f"❌ 渲染引擎
|
| 526 |
finally:
|
| 527 |
await m.delete()
|
| 528 |
|
| 529 |
# ==============================================================================
|
| 530 |
-
# 11.
|
| 531 |
# ==============================================================================
|
| 532 |
@tasks.loop(hours=1)
|
| 533 |
async def auto_backup():
|
| 534 |
bk_engine.save_db()
|
|
|
|
| 535 |
|
| 536 |
@bot.event
|
| 537 |
async def on_ready():
|
| 538 |
if not auto_backup.is_running(): auto_backup.start()
|
| 539 |
-
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="
|
| 540 |
-
logger.info(f"✅ [V62.9-
|
| 541 |
|
| 542 |
-
def
|
| 543 |
-
if DISCORD_TOKEN:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
try:
|
| 545 |
bot.run(DISCORD_TOKEN)
|
| 546 |
except Exception as e:
|
| 547 |
-
logger.
|
| 548 |
-
|
| 549 |
-
logger.critical("No Token")
|
| 550 |
|
| 551 |
if __name__ == "__main__":
|
| 552 |
-
# 1.
|
| 553 |
-
threading.Thread(target=
|
| 554 |
|
| 555 |
-
# 2.
|
| 556 |
-
# [FIX] 移除了 every=5 参数以修复 TypeError
|
| 557 |
iface = gr.Interface(
|
| 558 |
fn=get_status,
|
| 559 |
inputs=None,
|
| 560 |
outputs="json",
|
| 561 |
-
title="BK-GTA Sentinel",
|
| 562 |
-
description="
|
| 563 |
)
|
| 564 |
iface.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 20 |
import gradio as gr
|
| 21 |
|
| 22 |
# ==============================================================================
|
| 23 |
+
# 0. 机构级审计日志 (Institutional Log Engine)
|
| 24 |
# ==============================================================================
|
| 25 |
logging.basicConfig(
|
| 26 |
level=logging.INFO,
|
| 27 |
format='%(asctime)s [%(levelname)s] %(message)s',
|
| 28 |
handlers=[logging.StreamHandler(sys.stdout)]
|
| 29 |
)
|
| 30 |
+
logger = logging.getLogger("BK_GTA_ARCHITECT_TITAN")
|
| 31 |
|
| 32 |
# ==============================================================================
|
| 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"
|
| 42 |
try:
|
| 43 |
if bot and bot.is_ready():
|
| 44 |
+
bot_status = "ONLINE"
|
| 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%",
|
| 52 |
+
"engine_version": "V62.9-Gradio-Final",
|
| 53 |
+
"discord_bot": bot_status,
|
| 54 |
+
"latency": latency,
|
| 55 |
"server_time": now,
|
| 56 |
+
"keys_pool": len(KEY_POOL),
|
| 57 |
+
"db_size": f"{sys.getsizeof(bk_engine.data) / 1024:.2f} KB"
|
| 58 |
}
|
| 59 |
|
| 60 |
# ==============================================================================
|
| 61 |
+
# 2. 密钥池与环境配置 (Key Rotation & Env)
|
| 62 |
# ==============================================================================
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
single = os.environ.get('GEMINI_KEY')
|
| 71 |
+
if single: KEY_POOL.append(single)
|
|
|
|
|
|
|
| 72 |
|
| 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
|
|
|
|
| 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 = {}
|
|
|
|
| 112 |
|
| 113 |
def _init_schema(self):
|
| 114 |
return {
|
| 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,
|
| 122 |
"logs": [],
|
| 123 |
+
"last_sync": str(datetime.datetime.now())
|
| 124 |
}
|
| 125 |
|
| 126 |
def save_db(self):
|
|
|
|
| 130 |
json.dump(self.data, f, ensure_ascii=False, indent=4)
|
| 131 |
os.replace(temp, self.db_file)
|
| 132 |
except Exception as e:
|
| 133 |
+
logger.error(f"💾 [DB Error] {e}")
|
| 134 |
|
| 135 |
def get_user(self, user_id):
|
| 136 |
uid = str(user_id)
|
| 137 |
if uid not in self.data["users"]:
|
| 138 |
self.data["users"][uid] = {
|
| 139 |
+
"elo": 1200, "points": 1000, "faction": None,
|
| 140 |
+
"audits_count": 0, "total_score": 0.0, "highest_score": 0.0,
|
| 141 |
+
"win_streak": 0, "mdd_record": 0.0, "sharpe_record": 0.0,
|
| 142 |
+
"audit_history": [], "last_active": str(datetime.datetime.now())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
return self.data["users"][uid]
|
| 145 |
|
| 146 |
+
def update_audit(self, user_id, score, mdd=None, sharpe=None):
|
| 147 |
u = self.get_user(user_id)
|
| 148 |
u["audits_count"] += 1
|
| 149 |
u["total_score"] += score
|
| 150 |
+
if score > u.get("highest_score", 0):
|
| 151 |
+
u["highest_score"] = score
|
| 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())
|
| 159 |
self.save_db()
|
| 160 |
|
|
|
|
| 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 |
|
| 180 |
bk_engine = BKEngine()
|
| 181 |
|
| 182 |
# ==============================================================================
|
| 183 |
+
# 4. 辅助内核与密钥多路复用 (Multiplexing Kernel)
|
| 184 |
# ==============================================================================
|
| 185 |
def safe_extract_float(text, key):
|
| 186 |
try:
|
|
|
|
| 190 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 191 |
]
|
| 192 |
for p in patterns:
|
| 193 |
+
match = re.search(p, text, re.I)
|
| 194 |
if match: return float(match.group(1))
|
| 195 |
return 0.0
|
| 196 |
except: return 0.0
|
| 197 |
|
| 198 |
+
def safe_extract_text(text, start, end=None):
|
| 199 |
try:
|
| 200 |
+
p = rf"{start}:(.*?){end}:" if end else rf"{start}:(.*)"
|
| 201 |
+
match = re.search(p, text, re.S | re.I)
|
| 202 |
if match:
|
| 203 |
return match.group(1).strip().replace('*', '').replace('`', '').strip()
|
| 204 |
return "N/A"
|
| 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()
|
| 216 |
+
except: return "ERROR: READ_FAIL"
|
| 217 |
|
| 218 |
loop = asyncio.get_running_loop()
|
| 219 |
+
shuffled = list(KEY_POOL)
|
| 220 |
+
random.shuffle(shuffled)
|
| 221 |
|
| 222 |
+
for api_key in shuffled:
|
| 223 |
try:
|
| 224 |
client = genai.Client(api_key=api_key)
|
| 225 |
contents = [prompt]
|
| 226 |
+
if img_data:
|
| 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:
|
| 234 |
+
return response.text
|
|
|
|
| 235 |
except Exception as e:
|
| 236 |
+
if "429" in str(e):
|
| 237 |
+
logger.warning("Key Rate Limited. Retrying with next...")
|
|
|
|
| 238 |
continue
|
| 239 |
+
logger.error(f"Gemini API Error: {e}")
|
| 240 |
continue
|
| 241 |
|
| 242 |
return "ERROR: ALL_KEYS_EXHAUSTED"
|
| 243 |
|
| 244 |
# ==============================================================================
|
| 245 |
+
# 5. 模块一:全网天梯 (Rank & Stats)
|
| 246 |
# ==============================================================================
|
| 247 |
@bot.command()
|
| 248 |
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
|
| 257 |
+
elif elo < 2100: rank, color = "Gold 专家 🥇", 0xf1c40f
|
| 258 |
+
elif elo < 2500: rank, color = "Platinum 大师 💎", 0x3498db
|
| 259 |
+
else: rank, color = "Grandmaster 传奇 👑", 0x9b59b6
|
| 260 |
+
|
| 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)
|
| 268 |
|
| 269 |
stats = (
|
|
|
|
| 271 |
f"Peak Score: {u['highest_score']:.1f}\n"
|
| 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}")
|
| 279 |
await ctx.send(embed=emb)
|
| 280 |
|
| 281 |
@bot.command()
|
| 282 |
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 |
# ==============================================================================
|
| 304 |
+
# 6. 模块二:宏观情报 (Macro Analysis)
|
| 305 |
# ==============================================================================
|
| 306 |
@bot.command()
|
| 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"
|
| 314 |
+
"2. 总结 (SUMMARY): 30字内概括核心变量。\n"
|
| 315 |
+
"3. 解读 (TAKE): 机构视角的隐含逻辑。\n"
|
| 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 |
|
| 333 |
# ==============================================================================
|
| 334 |
+
# 7. 模块三:量化审计 (Institutional Audit)
|
| 335 |
# ==============================================================================
|
| 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 |
|
| 343 |
async with audit_lock:
|
| 344 |
+
m = await ctx.send("🛡️ **[Audit Engine] 启动深度扫描... (锁闭模式)**")
|
| 345 |
prompt = (
|
| 346 |
+
"你是一名顶级量化风险官。请提取并审计以下指标:\n"
|
| 347 |
+
"RET: 收益率, MDD: 最大回撤, PF: 盈利因子, SHARPE: 夏普比率, TRADES: 总笔数, S_BASE: 基础评分(0-100)。\n"
|
| 348 |
+
"若 MDD > 35% 或交易笔数 < 10,则 VIOLATION 为 TRUE。\n"
|
| 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] 引擎请求失败。")
|
| 358 |
|
| 359 |
ret = safe_extract_float(res, "RET")
|
| 360 |
mdd = safe_extract_float(res, "MDD")
|
|
|
|
| 361 |
sharpe = safe_extract_float(res, "SHARPE")
|
| 362 |
trades = int(safe_extract_float(res, "TRADES"))
|
| 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)
|
| 376 |
+
status = "✅ AUDIT CLEARED"
|
| 377 |
color = 0x2ecc71
|
| 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 |
|
| 392 |
# ==============================================================================
|
| 393 |
+
# 8. 模块四:博弈盘口 (Institutional Betting)
|
| 394 |
# ==============================================================================
|
| 395 |
@bot.command()
|
| 396 |
@commands.has_permissions(administrator=True)
|
| 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 |
}
|
| 404 |
+
await ctx.send(f"🎰 **[Bet-ID:{bid}]** 新博弈盘口已开启!\n主题:**{topic}**\n输入 `!bet {bid} UP/DOWN [金额]` 参与。")
|
| 405 |
|
| 406 |
@bot.command()
|
| 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 |
|
| 427 |
@bot.command()
|
| 428 |
@commands.has_permissions(administrator=True)
|
| 429 |
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"]
|
|
|
|
| 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}**。资产已自动划转。")
|
| 446 |
|
| 447 |
# ==============================================================================
|
| 448 |
+
# 9. 模块五:PVP 竞技场 (Duel Arena)
|
| 449 |
# ==============================================================================
|
| 450 |
@bot.command()
|
| 451 |
async def challenge(ctx, member: discord.Member):
|
| 452 |
if member.id == ctx.author.id: return
|
| 453 |
+
did = f"D-{random.randint(1000, 9999)}"
|
| 454 |
+
bk_engine.pending_challenges[member.id] = {"challenger": ctx.author.id, "id": did}
|
| 455 |
+
await ctx.send(f"⚔️ {ctx.author.name} 向 {member.mention} 发起挑战!\nID: `{did}`. 输入 `!accept` 接受。")
|
| 456 |
|
| 457 |
@bot.command()
|
| 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}` 提交审计截图。")
|
| 469 |
|
| 470 |
@bot.command()
|
| 471 |
async def duel_submit(ctx, did: str):
|
|
|
|
| 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
|
|
|
|
| 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)
|
| 494 |
+
emb.set_footer(text=f"Win Streak: {streak} | Duel ID: {did}")
|
| 495 |
await ctx.send(embed=emb)
|
| 496 |
del bk_engine.active_duels[did]
|
| 497 |
else:
|
| 498 |
+
await ctx.send(f"✅ 交易员 <@{ctx.author.id}> 分数已锁定 ({score})。等待对手提交。")
|
| 499 |
await m.delete()
|
| 500 |
|
| 501 |
# ==============================================================================
|
| 502 |
+
# 10. 模块六:海报渲染引擎 (Poster Rendering)
|
| 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))
|
| 534 |
+
draw.text((40, 360), f"SHARPE: {sharpe}", fill=(255, 255, 255))
|
| 535 |
+
draw.text((40, 460), f"SESSION: {SESSION_ID}", fill=(100, 100, 110))
|
| 536 |
+
draw.text((40, 720), "VERIFIED BY SENTINEL AI", fill=(46, 204, 113))
|
|
|
|
| 537 |
|
| 538 |
fp = io.BytesIO()
|
| 539 |
base.save(fp, 'PNG')
|
| 540 |
fp.seek(0)
|
| 541 |
await ctx.send(file=discord.File(fp, filename=f"Viral_{ctx.author.id}.png"))
|
|
|
|
| 542 |
except Exception as e:
|
| 543 |
logger.error(f"Render Error: {e}")
|
| 544 |
+
await ctx.send(f"❌ 渲染引擎崩溃: `{e}`")
|
| 545 |
finally:
|
| 546 |
await m.delete()
|
| 547 |
|
| 548 |
# ==============================================================================
|
| 549 |
+
# 11. 系统守护与自动备份链 (Maintenance)
|
| 550 |
# ==============================================================================
|
| 551 |
@tasks.loop(hours=1)
|
| 552 |
async def auto_backup():
|
| 553 |
bk_engine.save_db()
|
| 554 |
+
logger.info("💾 [System] Hourly Database Backup Executed.")
|
| 555 |
|
| 556 |
@bot.event
|
| 557 |
async def on_ready():
|
| 558 |
if not auto_backup.is_running(): auto_backup.start()
|
| 559 |
+
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="HuggingFace Node"))
|
| 560 |
+
logger.info(f"✅ [Online] BK-GTA V62.9-Titan Live. Session: {SESSION_ID}")
|
| 561 |
|
| 562 |
+
def run_bot():
|
| 563 |
+
if not DISCORD_TOKEN:
|
| 564 |
+
logger.critical("❌ [FATAL] DISCORD_TOKEN Missing.")
|
| 565 |
+
return
|
| 566 |
+
|
| 567 |
+
# 自动重连容器
|
| 568 |
+
while True:
|
| 569 |
try:
|
| 570 |
bot.run(DISCORD_TOKEN)
|
| 571 |
except Exception as e:
|
| 572 |
+
logger.error(f"⚠️ [Socket Error] {e}. Restarting in 10s...")
|
| 573 |
+
time.sleep(10)
|
|
|
|
| 574 |
|
| 575 |
if __name__ == "__main__":
|
| 576 |
+
# 1. 启动 Discord 后台线程
|
| 577 |
+
threading.Thread(target=run_bot, daemon=True).start()
|
| 578 |
|
| 579 |
+
# 2. 启动 Gradio 前台哨兵 (强制监听 7860 端口)
|
|
|
|
| 580 |
iface = gr.Interface(
|
| 581 |
fn=get_status,
|
| 582 |
inputs=None,
|
| 583 |
outputs="json",
|
| 584 |
+
title="BK-GTA Sentinel AI",
|
| 585 |
+
description="Physical architecture monitor. Logic Integrity: Secure."
|
| 586 |
)
|
| 587 |
iface.launch(server_name="0.0.0.0", server_port=7860)
|