Spaces:
Running
Running
Rename main.py to app.py
Browse files- main.py → app.py +176 -371
main.py → app.py
RENAMED
|
@@ -15,71 +15,45 @@ import sys
|
|
| 15 |
import traceback
|
| 16 |
import io
|
| 17 |
import time
|
|
|
|
| 18 |
from PIL import Image, ImageDraw, ImageFont
|
| 19 |
-
|
| 20 |
-
from threading import Thread
|
| 21 |
|
| 22 |
# ==============================================================================
|
| 23 |
-
# 0. 机构级审计日志 (
|
| 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 |
-
|
| 36 |
-
|
| 37 |
-
@app.route('/')
|
| 38 |
-
def home():
|
| 39 |
"""
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
注入 bot.latency 以确保监控访问能强制唤醒 Discord 线程。
|
| 43 |
"""
|
| 44 |
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 45 |
latency = "N/A"
|
| 46 |
try:
|
| 47 |
-
|
| 48 |
-
if bot.is_ready():
|
| 49 |
latency = f"{round(bot.latency * 1000, 2)}ms"
|
| 50 |
-
except
|
| 51 |
-
|
| 52 |
|
| 53 |
return {
|
| 54 |
-
"status": "BK-GTA-
|
| 55 |
-
"
|
| 56 |
-
"engine": "V62.9-Titan",
|
| 57 |
"bot_latency": latency,
|
| 58 |
"server_time": now,
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
}
|
| 62 |
|
| 63 |
-
def run_web_server():
|
| 64 |
-
"""
|
| 65 |
-
[CRITICAL] Hugging Face 强制要求监听 7860 端口。
|
| 66 |
-
这是外部 UptimeRobot 能够触达容器的唯一物理入口。
|
| 67 |
-
"""
|
| 68 |
-
try:
|
| 69 |
-
logger.info("📡 [Sentinel] Starting Web Server on Port 7860...")
|
| 70 |
-
import logging
|
| 71 |
-
log = logging.getLogger('werkzeug')
|
| 72 |
-
log.setLevel(logging.ERROR)
|
| 73 |
-
app.run(host='0.0.0.0', port=7860, threaded=True)
|
| 74 |
-
except Exception as e:
|
| 75 |
-
logger.error(f"⚠️ [Sentinel] Web Server Failure: {e}")
|
| 76 |
-
|
| 77 |
-
def keep_alive():
|
| 78 |
-
"""开启独立守护线程运行 Web 服务"""
|
| 79 |
-
t = Thread(target=run_web_server)
|
| 80 |
-
t.daemon = True
|
| 81 |
-
t.start()
|
| 82 |
-
|
| 83 |
# ==============================================================================
|
| 84 |
# 2. 密钥池与环境配置 (Key Rotation Engine)
|
| 85 |
# ==============================================================================
|
|
@@ -87,7 +61,6 @@ DISCORD_TOKEN = os.environ.get('DISCORD_TOKEN')
|
|
| 87 |
SESSION_ID = random.randint(10000, 99999)
|
| 88 |
|
| 89 |
# [CORE] 8 密钥轮询加载器
|
| 90 |
-
# 在 HF Secrets 中设置 GEMINI_KEYS_POOL = key1,key2,key3...
|
| 91 |
raw_keys = os.environ.get('GEMINI_KEYS_POOL', '')
|
| 92 |
KEY_POOL = []
|
| 93 |
|
|
@@ -114,31 +87,27 @@ intents.message_content = True
|
|
| 114 |
intents.members = True
|
| 115 |
bot = commands.Bot(command_prefix='!', intents=intents)
|
| 116 |
|
| 117 |
-
# 并发审计锁 (
|
| 118 |
audit_lock = asyncio.Lock()
|
| 119 |
|
| 120 |
-
# 数据库路径适配 (
|
| 121 |
if os.path.exists('/data'):
|
| 122 |
DB_PATH_CONFIG = '/data/bk_gta_v62_unified.json'
|
| 123 |
logger.info(f"📂 [Storage] Using Persistent Storage: {DB_PATH_CONFIG}")
|
| 124 |
else:
|
| 125 |
DB_PATH_CONFIG = 'bk_gta_v62_unified.json'
|
| 126 |
-
logger.warning(f"⚠️ [Storage] Using Ephemeral Storage (Data lost on restart)
|
| 127 |
|
| 128 |
# ==============================================================================
|
| 129 |
-
# 3.
|
| 130 |
# ==============================================================================
|
| 131 |
class BKEngine:
|
| 132 |
-
"""
|
| 133 |
-
核心数据引擎:负责量化资产持久化、状态树管理及风险指标对账
|
| 134 |
-
"""
|
| 135 |
def __init__(self):
|
| 136 |
self.db_file = DB_PATH_CONFIG
|
| 137 |
self.data = self._load_db()
|
| 138 |
self.pending_challenges = {}
|
| 139 |
self.active_duels = {}
|
| 140 |
self.active_bets = {}
|
| 141 |
-
self.system_uptime = datetime.datetime.now()
|
| 142 |
|
| 143 |
def _load_db(self):
|
| 144 |
if os.path.exists(self.db_file):
|
|
@@ -154,7 +123,7 @@ class BKEngine:
|
|
| 154 |
|
| 155 |
def _init_schema(self):
|
| 156 |
return {
|
| 157 |
-
"version": "62.9-
|
| 158 |
"users": {},
|
| 159 |
"factions": {
|
| 160 |
"BULLS": {"score": 0, "emoji": "🐂", "total_wins": 0},
|
|
@@ -166,7 +135,6 @@ class BKEngine:
|
|
| 166 |
}
|
| 167 |
|
| 168 |
def save_db(self):
|
| 169 |
-
"""执行物理持久化,确保数据资产安全"""
|
| 170 |
try:
|
| 171 |
# 原子写入防止文件损坏
|
| 172 |
temp = self.db_file + '.tmp'
|
|
@@ -189,7 +157,6 @@ class BKEngine:
|
|
| 189 |
"win_streak": 0,
|
| 190 |
"mdd_record": 0.0,
|
| 191 |
"sharpe_record": 0.0,
|
| 192 |
-
"performance_log": [],
|
| 193 |
"last_active": str(datetime.datetime.now())
|
| 194 |
}
|
| 195 |
return self.data["users"][uid]
|
|
@@ -198,144 +165,104 @@ class BKEngine:
|
|
| 198 |
u = self.get_user(user_id)
|
| 199 |
u["audits_count"] += 1
|
| 200 |
u["total_score"] += score
|
| 201 |
-
if score > u.get("highest_score", 0):
|
| 202 |
-
u["highest_score"] = score
|
| 203 |
if mdd is not None: u["mdd_record"] = mdd
|
| 204 |
if sharpe is not None: u["sharpe_record"] = sharpe
|
| 205 |
-
|
| 206 |
-
# 奖励计算:审计积分奖励
|
| 207 |
u["points"] += 50
|
| 208 |
u["last_active"] = str(datetime.datetime.now())
|
| 209 |
-
u["performance_log"].append({"score": score, "time": str(datetime.datetime.now())})
|
| 210 |
self.save_db()
|
| 211 |
|
| 212 |
def update_elo(self, winner_id, loser_id):
|
| 213 |
-
|
| 214 |
-
基于 ELO 等级分系统的量化能力更新算法
|
| 215 |
-
K = 32 (常数项)
|
| 216 |
-
"""
|
| 217 |
-
w = self.get_user(winner_id)
|
| 218 |
-
l = self.get_user(loser_id)
|
| 219 |
-
|
| 220 |
K = 32
|
| 221 |
Rw, Rl = w["elo"], l["elo"]
|
| 222 |
Ew = 1 / (1 + 10 ** ((Rl - Rw) / 400))
|
| 223 |
-
|
| 224 |
w["elo"] = round(Rw + K * (1 - Ew))
|
| 225 |
l["elo"] = round(Rl + K * (0 - Ew))
|
| 226 |
-
|
| 227 |
w["win_streak"] += 1
|
| 228 |
l["win_streak"] = 0
|
| 229 |
-
|
| 230 |
-
# 阵营荣誉点累加
|
| 231 |
-
if w["faction"] and w["faction"] in self.data["factions"]:
|
| 232 |
self.data["factions"][w["faction"]]["score"] += 15
|
| 233 |
self.data["factions"][w["faction"]]["total_wins"] += 1
|
| 234 |
-
|
| 235 |
self.save_db()
|
| 236 |
return w["elo"], l["elo"], w["win_streak"]
|
| 237 |
|
| 238 |
bk_engine = BKEngine()
|
| 239 |
|
| 240 |
# ==============================================================================
|
| 241 |
-
# 4.
|
| 242 |
# ==============================================================================
|
| 243 |
def safe_extract_float(text, key):
|
| 244 |
-
"""鲁棒性数值提取引擎,支持多重前缀匹配"""
|
| 245 |
try:
|
| 246 |
patterns = [
|
| 247 |
-
rf"{key}:\s*([-+]?\d*\.?\d+)",
|
| 248 |
rf"{key}\s*=\s*([-+]?\d*\.?\d+)",
|
| 249 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 250 |
]
|
| 251 |
-
for
|
| 252 |
-
match = re.search(
|
| 253 |
if match: return float(match.group(1))
|
| 254 |
return 0.0
|
| 255 |
-
except
|
| 256 |
|
| 257 |
def safe_extract_text(text, key_start, key_end=None):
|
| 258 |
-
"""逻辑块提取引擎,支持长文本切片"""
|
| 259 |
try:
|
| 260 |
-
if key_end:
|
| 261 |
-
|
| 262 |
-
else:
|
| 263 |
-
pattern = rf"{key_start}:(.*)"
|
| 264 |
-
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
| 265 |
if match:
|
| 266 |
-
|
| 267 |
-
# 过滤 Markdown 字符防止渲染崩溃
|
| 268 |
-
return content.replace('*', '').replace('`', '').strip()
|
| 269 |
return "N/A"
|
| 270 |
-
except
|
| 271 |
|
| 272 |
async def call_gemini_multiplex(prompt, attachment=None):
|
| 273 |
"""
|
| 274 |
[Core Logic] 8 密钥自动轮询与故障转移
|
| 275 |
-
异步多密钥池轮询请求核心 | 具备 429 限流自动熔断机制
|
| 276 |
"""
|
| 277 |
-
if not KEY_POOL:
|
| 278 |
-
logger.error("Request Blocked: No API Keys available.")
|
| 279 |
-
return "ERROR: NO_KEYS_CONFIGURED"
|
| 280 |
|
| 281 |
image_data = None
|
| 282 |
if attachment:
|
| 283 |
-
try:
|
| 284 |
-
|
| 285 |
-
except Exception as e:
|
| 286 |
-
logger.error(f"Attachment Read Error: {e}")
|
| 287 |
-
return "ERROR: IMG_READ_FAIL"
|
| 288 |
|
| 289 |
loop = asyncio.get_running_loop()
|
| 290 |
-
|
| 291 |
-
# 随机打乱密钥顺序以实现负载均衡
|
| 292 |
shuffled_keys = list(KEY_POOL)
|
| 293 |
random.shuffle(shuffled_keys)
|
| 294 |
|
| 295 |
for i, api_key in enumerate(shuffled_keys):
|
| 296 |
try:
|
| 297 |
-
# 实例化客户端
|
| 298 |
client = genai.Client(api_key=api_key)
|
| 299 |
-
|
| 300 |
if image_data:
|
| 301 |
-
|
| 302 |
|
| 303 |
-
# 使用线程池执行同步 SDK 调用以防阻塞事件循环
|
| 304 |
response = await loop.run_in_executor(
|
| 305 |
None,
|
| 306 |
-
functools.partial(client.models.generate_content, model="gemini-1.5-flash", contents=
|
| 307 |
)
|
| 308 |
|
| 309 |
-
if response and response.text:
|
| 310 |
-
return response.text
|
| 311 |
|
| 312 |
except Exception as e:
|
| 313 |
err_msg = str(e)
|
| 314 |
if "429" in err_msg:
|
| 315 |
-
logger.warning(f"⚠️ Key {i+1}
|
| 316 |
-
continue
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
return "ERROR: BAD_REQUEST"
|
| 320 |
-
else:
|
| 321 |
-
logger.error(f"🤖 Gemini Error on Key {i+1}: {err_msg}")
|
| 322 |
-
continue # 尝试下一个
|
| 323 |
|
| 324 |
return "ERROR: ALL_KEYS_EXHAUSTED"
|
| 325 |
|
| 326 |
# ==============================================================================
|
| 327 |
-
# 5.
|
| 328 |
# ==============================================================================
|
| 329 |
@bot.command()
|
| 330 |
async def profile(ctx, member: discord.Member = None):
|
| 331 |
-
"""
|
| 332 |
-
展现量化交易员的详细画像与 ELO 评级
|
| 333 |
-
"""
|
| 334 |
target = member or ctx.author
|
| 335 |
u = bk_engine.get_user(target.id)
|
| 336 |
-
|
| 337 |
-
# 动态段位逻辑展开
|
| 338 |
elo = u['elo']
|
|
|
|
|
|
|
| 339 |
if elo < 1300: rank, color = "Iron (学徒) 🗑️", 0x717d7e
|
| 340 |
elif elo < 1500: rank, color = "Bronze (资深) 🥉", 0xcd7f32
|
| 341 |
elif elo < 1800: rank, color = "Silver (精英) 🥈", 0xbdc3c7
|
|
@@ -346,98 +273,65 @@ async def profile(ctx, member: discord.Member = None):
|
|
| 346 |
emb = discord.Embed(title=f"📊 Quantitative Profile: {target.display_name}", color=color)
|
| 347 |
emb.add_field(name="Rating (ELO)", value=f"**{elo}**\nRank: `{rank}`", inline=True)
|
| 348 |
emb.add_field(name="Assets (Points)", value=f"💰 **{u['points']}**", inline=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
-
|
| 351 |
-
f_emoji = bk_engine.data["factions"].get(f_name, {}).get("emoji", "🌐") if f_name else "Ronin"
|
| 352 |
-
emb.add_field(name="Faction", value=f"{f_emoji} {f_name or 'Unassigned'}", inline=True)
|
| 353 |
-
|
| 354 |
-
# 展开性能统计指标
|
| 355 |
-
perf_stats = (
|
| 356 |
f"Total Audits: {u['audits_count']}\n"
|
| 357 |
-
f"Peak
|
| 358 |
-
f"
|
| 359 |
-
f"Max Win Streak: {u['win_streak']}"
|
| 360 |
)
|
| 361 |
-
emb.add_field(name="
|
| 362 |
-
|
| 363 |
-
if target.avatar:
|
| 364 |
-
emb.set_thumbnail(url=target.avatar.url)
|
| 365 |
|
| 366 |
-
emb.
|
|
|
|
| 367 |
await ctx.send(embed=emb)
|
| 368 |
|
| 369 |
@bot.command()
|
| 370 |
async def leaderboard(ctx):
|
| 371 |
-
"""
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
all_users = []
|
| 375 |
-
for uid, data in bk_engine.data["users"].items():
|
| 376 |
-
all_users.append({"id": uid, "elo": data["elo"], "score": data["highest_score"]})
|
| 377 |
-
|
| 378 |
-
# 按 ELO 排序
|
| 379 |
-
top_elo = sorted(all_users, key=lambda x: x["elo"], reverse=True)[:10]
|
| 380 |
-
|
| 381 |
desc = ""
|
| 382 |
-
for
|
| 383 |
-
|
| 384 |
-
desc += f"{
|
| 385 |
-
|
| 386 |
-
emb = discord.Embed(
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
)
|
|
|
|
|
|
|
| 391 |
|
| 392 |
-
# 加入阵营战统计
|
| 393 |
-
f_info = ""
|
| 394 |
-
for name, info in bk_engine.data["factions"].items():
|
| 395 |
-
f_info += f"{info['emoji']} {name}: {info['score']} pts "
|
| 396 |
-
if f_info:
|
| 397 |
-
emb.add_field(name="⚔️ Faction War Status", value=f_info, inline=False)
|
| 398 |
-
|
| 399 |
await ctx.send(embed=emb)
|
| 400 |
|
| 401 |
# ==============================================================================
|
| 402 |
-
# 6.
|
| 403 |
# ==============================================================================
|
| 404 |
@bot.command()
|
| 405 |
async def news(ctx):
|
| 406 |
-
""
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
if not ctx.message.attachments:
|
| 410 |
-
return await ctx.send("❌ Audit Refused: 请上传新闻图表。")
|
| 411 |
-
|
| 412 |
-
m = await ctx.send("📡 **[HF-Node] Parsing Macro Pulse...**")
|
| 413 |
-
|
| 414 |
prompt = (
|
| 415 |
-
"
|
| 416 |
-
"
|
| 417 |
-
"2. 核心变量 (SUMMARY): 30字内概括。\n"
|
| 418 |
-
"3. 隐含逻辑 (TAKE): 机构视角的解读 (100字内)。\n"
|
| 419 |
-
"输出格式必须严格如下:\n"
|
| 420 |
-
"SCORE: [分值]\n"
|
| 421 |
-
"SUMMARY: [内容]\n"
|
| 422 |
-
"TAKE: [解读]"
|
| 423 |
)
|
| 424 |
-
|
| 425 |
res = await call_gemini_multiplex(prompt, ctx.message.attachments[0])
|
| 426 |
|
| 427 |
-
if "ERROR" in res:
|
| 428 |
-
await m.delete()
|
| 429 |
-
return await ctx.send("🔴 [Audit Failure] Intelligence node offline.")
|
| 430 |
-
|
| 431 |
score = safe_extract_float(res, "SCORE")
|
| 432 |
summary = safe_extract_text(res, "SUMMARY", "TAKE")
|
| 433 |
take = safe_extract_text(res, "TAKE")
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
emb
|
| 437 |
-
emb.add_field(name="
|
| 438 |
-
emb.add_field(name="
|
| 439 |
-
|
| 440 |
-
|
| 441 |
await m.delete()
|
| 442 |
await ctx.send(embed=emb)
|
| 443 |
|
|
@@ -447,39 +341,28 @@ async def news(ctx):
|
|
| 447 |
@bot.command()
|
| 448 |
async def evaluate(ctx):
|
| 449 |
"""
|
| 450 |
-
|
| 451 |
-
带有并发锁 (Semaphore) 防止 HF 容器 OOM。
|
| 452 |
"""
|
| 453 |
-
if not ctx.message.attachments:
|
| 454 |
-
|
| 455 |
-
|
| 456 |
# [Lock Check] 检查是否忙碌
|
| 457 |
if audit_lock.locked():
|
| 458 |
-
return await ctx.send("⚠️ **[System Busy]** Another audit is processing. Please wait 10s
|
| 459 |
|
| 460 |
async with audit_lock:
|
| 461 |
-
|
| 462 |
-
m = await ctx.send(f"🛡️ **[SID:{sid}] Audit Engine Initializing... (Locked Mode)**")
|
| 463 |
-
|
| 464 |
-
# 深度审计 Prompt
|
| 465 |
prompt = (
|
| 466 |
-
"
|
| 467 |
-
"
|
| 468 |
-
"
|
| 469 |
-
"2. 亮点 (H_ZH): 中文优势概括。\n"
|
| 470 |
-
"3. 评分 (S_BASE): 基础评分 (0-100)。\n"
|
| 471 |
-
"4. 否决项 (VIOLATION): MDD > 35% 或 交易笔数 < 10 时为 TRUE。\n"
|
| 472 |
-
"输出格式:\n"
|
| 473 |
-
"RET: [值]\nMDD: [值]\nPF: [值]\nSHARPE: [值]\nTRADES: [笔数]\nS_BASE: [分]\nVIOLATION: [TRUE/FALSE]\nH_ZH: [优势]"
|
| 474 |
)
|
| 475 |
-
|
| 476 |
res = await call_gemini_multiplex(prompt, ctx.message.attachments[0])
|
| 477 |
-
|
| 478 |
if "ERROR" in res:
|
| 479 |
await m.delete()
|
| 480 |
-
return await ctx.send("🔴
|
| 481 |
|
| 482 |
-
# 提取数值
|
| 483 |
ret = safe_extract_float(res, "RET")
|
| 484 |
mdd = safe_extract_float(res, "MDD")
|
| 485 |
pf = safe_extract_float(res, "PF")
|
|
@@ -488,199 +371,134 @@ async def evaluate(ctx):
|
|
| 488 |
s_base = safe_extract_float(res, "S_BASE")
|
| 489 |
violation = "TRUE" in res.upper()
|
| 490 |
|
| 491 |
-
# 逻辑加权算法展开:S = S_base * (log10(Trades)/1.5) * Sharpe_Factor
|
| 492 |
-
# 如果 MDD > 35%,执行风控一票否决
|
| 493 |
if violation or mdd > 35.0:
|
| 494 |
s_final = 0.0
|
| 495 |
-
status = "
|
| 496 |
color = 0xe74c3c
|
| 497 |
else:
|
|
|
|
| 498 |
trades_adj = math.log10(max(trades, 1)) / 1.5
|
| 499 |
-
s_final = s_base * min(max(trades_adj, 0.5), 1.2) * (sharpe
|
| 500 |
s_final = min(max(s_final, 0.0), 100.0)
|
| 501 |
-
status = "
|
| 502 |
color = 0x2ecc71
|
| 503 |
|
| 504 |
-
# 持久化
|
| 505 |
bk_engine.update_audit_stats(ctx.author.id, s_final, mdd, sharpe)
|
| 506 |
-
h_zh = safe_extract_text(res, "H_ZH")
|
| 507 |
-
|
| 508 |
-
emb = discord.Embed(title="🛡️ BK-GTA Gene Audit Report", color=color)
|
| 509 |
-
emb.add_field(name="Verdict", value=f"**{status}**", inline=False)
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
)
|
| 516 |
-
emb.add_field(name="Data Extraction", value=f"```\n{metrics}\n```", inline=False)
|
| 517 |
-
emb.add_field(name="Analysis Score", value=f"**{s_final:.2f} / 100**", inline=True)
|
| 518 |
-
emb.add_field(name="Highlights", value=h_zh, inline=False)
|
| 519 |
|
| 520 |
-
emb.set_footer(text=f"SID: {sid} | Assets +50")
|
| 521 |
await m.delete()
|
| 522 |
await ctx.send(embed=emb)
|
| 523 |
|
| 524 |
# ==============================================================================
|
| 525 |
-
# 8. 模块四:博弈盘口 (
|
| 526 |
# ==============================================================================
|
| 527 |
@bot.command()
|
| 528 |
@commands.has_permissions(administrator=True)
|
| 529 |
-
async def open_bet(ctx, *, topic
|
| 530 |
-
"""
|
| 531 |
-
由合伙人开启的宏观/行情博弈盘口
|
| 532 |
-
"""
|
| 533 |
bid = str(random.randint(100, 999))
|
| 534 |
bk_engine.active_bets[bid] = {
|
| 535 |
-
"topic": topic,
|
| 536 |
-
"up": 0, "down": 0,
|
| 537 |
-
"u_up": {}, "u_down": {},
|
| 538 |
"status": "OPEN",
|
| 539 |
"start_time": str(datetime.datetime.now())
|
| 540 |
}
|
| 541 |
-
await ctx.send(f"🎰 **[
|
| 542 |
|
| 543 |
@bot.command()
|
| 544 |
async def bet(ctx, bid: str, side: str, amt: int):
|
| 545 |
-
"""
|
| 546 |
-
交易员消耗积分参与盘口对冲
|
| 547 |
-
"""
|
| 548 |
-
if bid not in bk_engine.active_bets:
|
| 549 |
-
return await ctx.send("❌ Error: Invalid Bet ID.")
|
| 550 |
-
|
| 551 |
-
b = bk_engine.active_bets[bid]
|
| 552 |
-
if b["status"] != "OPEN":
|
| 553 |
-
return await ctx.send("❌ Error: This market is closed.")
|
| 554 |
-
|
| 555 |
u = bk_engine.get_user(ctx.author.id)
|
| 556 |
-
if u["points"] < amt
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
side
|
| 560 |
-
if side not in ["UP", "DOWN"]:
|
| 561 |
-
return await ctx.send("❌ Error: Choose UP or DOWN.")
|
| 562 |
-
|
| 563 |
-
# 锁定积分
|
| 564 |
-
u["points"] -= amt
|
| 565 |
-
uid = str(ctx.author.id)
|
| 566 |
|
| 567 |
-
|
|
|
|
| 568 |
b[side.lower()] += amt
|
| 569 |
-
b[
|
| 570 |
-
|
| 571 |
bk_engine.save_db()
|
| 572 |
-
await ctx.send(f"✅
|
| 573 |
|
| 574 |
@bot.command()
|
| 575 |
@commands.has_permissions(administrator=True)
|
| 576 |
async def settle_bet(ctx, bid: str, winner: str):
|
| 577 |
-
"""
|
| 578 |
-
结算盘口并自动执行积分再分配
|
| 579 |
-
"""
|
| 580 |
if bid not in bk_engine.active_bets: return
|
| 581 |
-
b = bk_engine.active_bets[bid]
|
| 582 |
-
|
| 583 |
-
|
| 584 |
win_pool = b["u_up"] if winner == "UP" else b["u_down"]
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
if
|
| 589 |
for uid, amt in win_pool.items():
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
user = bk_engine.get_user(int(uid))
|
| 593 |
-
user["points"] += round(amt + profit)
|
| 594 |
|
| 595 |
b["status"] = "SETTLED"
|
| 596 |
bk_engine.save_db()
|
| 597 |
-
await ctx.send(f"🏁 Bet
|
| 598 |
|
| 599 |
# ==============================================================================
|
| 600 |
-
# 9. 模块五:PVP 竞技场 (Arena
|
| 601 |
# ==============================================================================
|
| 602 |
@bot.command()
|
| 603 |
async def challenge(ctx, member: discord.Member):
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
"""
|
| 607 |
-
|
| 608 |
-
return await ctx.send("❌ Logical Error: 不能向自己发起决斗。")
|
| 609 |
-
|
| 610 |
-
did = f"DUEL-{random.randint(100, 999)}"
|
| 611 |
-
bk_engine.pending_challenges[member.id] = {
|
| 612 |
-
"challenger": ctx.author.id,
|
| 613 |
-
"did": did,
|
| 614 |
-
"expiry": datetime.datetime.now() + datetime.timedelta(hours=1)
|
| 615 |
-
}
|
| 616 |
-
|
| 617 |
-
await ctx.send(f"⚔️ **{ctx.author.name}** 发起决斗请求!目标:{member.mention}\nID: `{did}` | `!accept` 接受。")
|
| 618 |
|
| 619 |
@bot.command()
|
| 620 |
async def accept(ctx):
|
| 621 |
-
|
| 622 |
-
接受竞技场挑战
|
| 623 |
-
"""
|
| 624 |
-
if ctx.author.id not in bk_engine.pending_challenges:
|
| 625 |
-
return await ctx.send("❌ Error: No pending challenges.")
|
| 626 |
-
|
| 627 |
req = bk_engine.pending_challenges.pop(ctx.author.id)
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
bk_engine.active_duels[did] = {
|
| 631 |
-
"p1": req["challenger"], "p2": ctx.author.id,
|
| 632 |
-
"s1": None, "s2": None,
|
| 633 |
-
"start": str(datetime.datetime.now())
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
await ctx.send(f"🔥 **ARENA LOCKED!** Duel ID: `{did}`\n双方请通过 `!duel_submit {did}` 上传审计截图。")
|
| 637 |
|
| 638 |
@bot.command()
|
| 639 |
async def duel_submit(ctx, did: str):
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
if did not in bk_engine.active_duels:
|
| 644 |
-
return await ctx.send("❌ Error: Duel not active.")
|
| 645 |
-
|
| 646 |
-
if not ctx.message.attachments:
|
| 647 |
-
return await ctx.send("❌ Refused: 请上传结算截图。")
|
| 648 |
-
|
| 649 |
duel = bk_engine.active_duels[did]
|
| 650 |
if ctx.author.id not in [duel["p1"], duel["p2"]]: return
|
| 651 |
-
|
| 652 |
-
m = await ctx.send(
|
| 653 |
-
|
| 654 |
-
res = await call_gemini_multiplex(prompt, ctx.message.attachments[0])
|
| 655 |
score = safe_extract_float(res, "SCORE")
|
| 656 |
-
|
| 657 |
if ctx.author.id == duel["p1"]: duel["s1"] = score
|
| 658 |
else: duel["s2"] = score
|
| 659 |
-
|
| 660 |
-
# 逻辑���歧点:如果双方都交了,执行结算
|
| 661 |
if duel["s1"] is not None and duel["s2"] is not None:
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
emb.add_field(name="🥇 Winner", value=f"<@{wid}>\nScore: {max(duel['s1'], duel['s2']):.1f}\nNew ELO: {new_w}", inline=True)
|
| 667 |
-
emb.add_field(name="🥈 Loser", value=f"<@{lid}>\nScore: {min(duel['s1'], duel['s2']):.1f}\nNew ELO: {new_l}", inline=True)
|
| 668 |
-
emb.set_footer(text=f"Streak: {strk} | Duel ID: {did}")
|
| 669 |
|
|
|
|
|
|
|
|
|
|
| 670 |
await ctx.send(embed=emb)
|
| 671 |
del bk_engine.active_duels[did]
|
| 672 |
else:
|
| 673 |
-
await ctx.send(f"✅
|
| 674 |
-
|
| 675 |
await m.delete()
|
| 676 |
|
| 677 |
# ==============================================================================
|
| 678 |
-
# 10.
|
| 679 |
# ==============================================================================
|
| 680 |
@bot.command()
|
| 681 |
async def viral_audit(ctx):
|
| 682 |
"""
|
| 683 |
-
提取审计数据并渲染具备社交传播属性的海报图片
|
| 684 |
包含完整的 PIL 绘图逻辑,无省略。
|
| 685 |
"""
|
| 686 |
if not ctx.message.attachments:
|
|
@@ -729,52 +547,39 @@ async def viral_audit(ctx):
|
|
| 729 |
await m.delete()
|
| 730 |
|
| 731 |
# ==============================================================================
|
| 732 |
-
# 11.
|
| 733 |
# ==============================================================================
|
| 734 |
@tasks.loop(hours=1)
|
| 735 |
async def auto_backup():
|
| 736 |
-
"""本地资产数据库每小时原子化备份"""
|
| 737 |
bk_engine.save_db()
|
| 738 |
-
logger.info("💾 [Checkpoint] Data persistence executed.")
|
| 739 |
|
| 740 |
@bot.event
|
| 741 |
async def on_ready():
|
| 742 |
-
# 激活心跳对账
|
| 743 |
if not auto_backup.is_running(): auto_backup.start()
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="HF-Node Ultimate"))
|
| 747 |
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
logger.info(f"🚀 Session: {SESSION_ID} | Logic Integrity: Full")
|
| 751 |
-
logger.info(f"🛡️ Sentinel Port: 7860 (Active)")
|
| 752 |
-
logger.info(f"--------------------------------------------------")
|
| 753 |
-
|
| 754 |
-
@bot.event
|
| 755 |
-
async def on_command_error(ctx, error):
|
| 756 |
-
"""全局错误拦截器,严禁静默失效"""
|
| 757 |
-
if isinstance(error, commands.CommandNotFound): return
|
| 758 |
-
if isinstance(error, commands.MissingPermissions):
|
| 759 |
-
return await ctx.send("❌ 权限审计未通过:仅限合伙人执行。")
|
| 760 |
-
|
| 761 |
-
# 堆栈追踪打印至 Console
|
| 762 |
-
exc = "".join(traceback.format_exception(type(error), error, error.__traceback__))
|
| 763 |
-
logger.error(f"🔥 [System Crash] {exc}")
|
| 764 |
-
await ctx.send(f"⚠️ [Logic Breach] 内部逻辑断裂: `{error}`")
|
| 765 |
-
|
| 766 |
-
# ==============================================================================
|
| 767 |
-
# 12. 物理点火启动链 (Physical Start Engine)
|
| 768 |
-
# ==============================================================================
|
| 769 |
-
if __name__ == "__main__":
|
| 770 |
-
# 1. 物理启动 Web 哨兵接口 (Port 7860)
|
| 771 |
-
keep_alive()
|
| 772 |
-
|
| 773 |
-
# 2. 逻辑启动 Discord Bot 核心
|
| 774 |
-
if not DISCORD_TOKEN:
|
| 775 |
-
logger.critical("❌ CRITICAL: DISCORD_TOKEN is missing in environment.")
|
| 776 |
-
else:
|
| 777 |
try:
|
| 778 |
bot.run(DISCORD_TOKEN)
|
| 779 |
except Exception as e:
|
| 780 |
-
logger.critical(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
import traceback
|
| 16 |
import io
|
| 17 |
import time
|
| 18 |
+
import threading
|
| 19 |
from PIL import Image, ImageDraw, ImageFont
|
| 20 |
+
import gradio as gr # 替换 Flask 为 Gradio
|
|
|
|
| 21 |
|
| 22 |
# ==============================================================================
|
| 23 |
+
# 0. 机构级审计日志 (Institutional Logging)
|
| 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_GRADIO")
|
| 31 |
|
| 32 |
# ==============================================================================
|
| 33 |
+
# 1. 物理层适配:Gradio 哨兵 (Sentinel Probe)
|
| 34 |
# ==============================================================================
|
| 35 |
+
def get_status():
|
|
|
|
|
|
|
|
|
|
| 36 |
"""
|
| 37 |
+
提供给 UptimeRobot 的监控探针内容
|
| 38 |
+
Gradio 会自动将其渲染为一个 API 端点,确保 HF 容器不休眠
|
|
|
|
| 39 |
"""
|
| 40 |
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 41 |
latency = "N/A"
|
| 42 |
try:
|
| 43 |
+
if bot and bot.is_ready():
|
|
|
|
| 44 |
latency = f"{round(bot.latency * 1000, 2)}ms"
|
| 45 |
+
except:
|
| 46 |
+
pass
|
| 47 |
|
| 48 |
return {
|
| 49 |
+
"status": "BK-GTA-GRADIO-TITAN-ACTIVE",
|
| 50 |
+
"version": "V62.9-Full-Stack",
|
|
|
|
| 51 |
"bot_latency": latency,
|
| 52 |
"server_time": now,
|
| 53 |
+
"keys_loaded": len(KEY_POOL),
|
| 54 |
+
"memory_usage": f"{sys.getsizeof(bk_engine.data) / 1024:.2f} KB"
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# ==============================================================================
|
| 58 |
# 2. 密钥池与环境配置 (Key Rotation Engine)
|
| 59 |
# ==============================================================================
|
|
|
|
| 61 |
SESSION_ID = random.randint(10000, 99999)
|
| 62 |
|
| 63 |
# [CORE] 8 密钥轮询加载器
|
|
|
|
| 64 |
raw_keys = os.environ.get('GEMINI_KEYS_POOL', '')
|
| 65 |
KEY_POOL = []
|
| 66 |
|
|
|
|
| 87 |
intents.members = True
|
| 88 |
bot = commands.Bot(command_prefix='!', intents=intents)
|
| 89 |
|
| 90 |
+
# 并发审计锁 (防止内存溢出)
|
| 91 |
audit_lock = asyncio.Lock()
|
| 92 |
|
| 93 |
+
# 数据库路径适配 (Gradio 模式也支持 /data 持久化)
|
| 94 |
if os.path.exists('/data'):
|
| 95 |
DB_PATH_CONFIG = '/data/bk_gta_v62_unified.json'
|
| 96 |
logger.info(f"📂 [Storage] Using Persistent Storage: {DB_PATH_CONFIG}")
|
| 97 |
else:
|
| 98 |
DB_PATH_CONFIG = 'bk_gta_v62_unified.json'
|
| 99 |
+
logger.warning(f"⚠️ [Storage] Using Ephemeral Storage (Data lost on restart).")
|
| 100 |
|
| 101 |
# ==============================================================================
|
| 102 |
+
# 3. 状态引擎 (BKEngine Core)
|
| 103 |
# ==============================================================================
|
| 104 |
class BKEngine:
|
|
|
|
|
|
|
|
|
|
| 105 |
def __init__(self):
|
| 106 |
self.db_file = DB_PATH_CONFIG
|
| 107 |
self.data = self._load_db()
|
| 108 |
self.pending_challenges = {}
|
| 109 |
self.active_duels = {}
|
| 110 |
self.active_bets = {}
|
|
|
|
| 111 |
|
| 112 |
def _load_db(self):
|
| 113 |
if os.path.exists(self.db_file):
|
|
|
|
| 123 |
|
| 124 |
def _init_schema(self):
|
| 125 |
return {
|
| 126 |
+
"version": "62.9-Gradio",
|
| 127 |
"users": {},
|
| 128 |
"factions": {
|
| 129 |
"BULLS": {"score": 0, "emoji": "🐂", "total_wins": 0},
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
def save_db(self):
|
|
|
|
| 138 |
try:
|
| 139 |
# 原子写入防止文件损坏
|
| 140 |
temp = self.db_file + '.tmp'
|
|
|
|
| 157 |
"win_streak": 0,
|
| 158 |
"mdd_record": 0.0,
|
| 159 |
"sharpe_record": 0.0,
|
|
|
|
| 160 |
"last_active": str(datetime.datetime.now())
|
| 161 |
}
|
| 162 |
return self.data["users"][uid]
|
|
|
|
| 165 |
u = self.get_user(user_id)
|
| 166 |
u["audits_count"] += 1
|
| 167 |
u["total_score"] += score
|
| 168 |
+
if score > u.get("highest_score", 0): u["highest_score"] = score
|
|
|
|
| 169 |
if mdd is not None: u["mdd_record"] = mdd
|
| 170 |
if sharpe is not None: u["sharpe_record"] = sharpe
|
|
|
|
|
|
|
| 171 |
u["points"] += 50
|
| 172 |
u["last_active"] = str(datetime.datetime.now())
|
|
|
|
| 173 |
self.save_db()
|
| 174 |
|
| 175 |
def update_elo(self, winner_id, loser_id):
|
| 176 |
+
w, l = self.get_user(winner_id), self.get_user(loser_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
K = 32
|
| 178 |
Rw, Rl = w["elo"], l["elo"]
|
| 179 |
Ew = 1 / (1 + 10 ** ((Rl - Rw) / 400))
|
|
|
|
| 180 |
w["elo"] = round(Rw + K * (1 - Ew))
|
| 181 |
l["elo"] = round(Rl + K * (0 - Ew))
|
|
|
|
| 182 |
w["win_streak"] += 1
|
| 183 |
l["win_streak"] = 0
|
| 184 |
+
if w["faction"]:
|
|
|
|
|
|
|
| 185 |
self.data["factions"][w["faction"]]["score"] += 15
|
| 186 |
self.data["factions"][w["faction"]]["total_wins"] += 1
|
|
|
|
| 187 |
self.save_db()
|
| 188 |
return w["elo"], l["elo"], w["win_streak"]
|
| 189 |
|
| 190 |
bk_engine = BKEngine()
|
| 191 |
|
| 192 |
# ==============================================================================
|
| 193 |
+
# 4. 辅助内核 (Helper Kernels)
|
| 194 |
# ==============================================================================
|
| 195 |
def safe_extract_float(text, key):
|
|
|
|
| 196 |
try:
|
| 197 |
patterns = [
|
| 198 |
+
rf"{key}:\s*([-+]?\d*\.?\d+)",
|
| 199 |
rf"{key}\s*=\s*([-+]?\d*\.?\d+)",
|
| 200 |
rf"【{key}】\s*([-+]?\d*\.?\d+)"
|
| 201 |
]
|
| 202 |
+
for p in patterns:
|
| 203 |
+
match = re.search(p, text, re.IGNORECASE)
|
| 204 |
if match: return float(match.group(1))
|
| 205 |
return 0.0
|
| 206 |
+
except: return 0.0
|
| 207 |
|
| 208 |
def safe_extract_text(text, key_start, key_end=None):
|
|
|
|
| 209 |
try:
|
| 210 |
+
p = rf"{key_start}:(.*?){key_end}:" if key_end else rf"{key_start}:(.*)"
|
| 211 |
+
match = re.search(p, text, re.DOTALL | re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
| 212 |
if match:
|
| 213 |
+
return match.group(1).strip().replace('*', '').replace('`', '').strip()
|
|
|
|
|
|
|
| 214 |
return "N/A"
|
| 215 |
+
except: return "N/A"
|
| 216 |
|
| 217 |
async def call_gemini_multiplex(prompt, attachment=None):
|
| 218 |
"""
|
| 219 |
[Core Logic] 8 密钥自动轮询与故障转移
|
|
|
|
| 220 |
"""
|
| 221 |
+
if not KEY_POOL: return "ERROR: NO_KEYS_CONFIGURED"
|
|
|
|
|
|
|
| 222 |
|
| 223 |
image_data = None
|
| 224 |
if attachment:
|
| 225 |
+
try: image_data = await attachment.read()
|
| 226 |
+
except: return "ERROR: IMG_READ_FAIL"
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
loop = asyncio.get_running_loop()
|
|
|
|
|
|
|
| 229 |
shuffled_keys = list(KEY_POOL)
|
| 230 |
random.shuffle(shuffled_keys)
|
| 231 |
|
| 232 |
for i, api_key in enumerate(shuffled_keys):
|
| 233 |
try:
|
|
|
|
| 234 |
client = genai.Client(api_key=api_key)
|
| 235 |
+
contents = [prompt]
|
| 236 |
if image_data:
|
| 237 |
+
contents.append(types.Part.from_bytes(data=image_data, mime_type='image/png'))
|
| 238 |
|
|
|
|
| 239 |
response = await loop.run_in_executor(
|
| 240 |
None,
|
| 241 |
+
functools.partial(client.models.generate_content, model="gemini-1.5-flash", contents=contents)
|
| 242 |
)
|
| 243 |
|
| 244 |
+
if response and response.text: return response.text
|
|
|
|
| 245 |
|
| 246 |
except Exception as e:
|
| 247 |
err_msg = str(e)
|
| 248 |
if "429" in err_msg:
|
| 249 |
+
logger.warning(f"⚠️ Key {i+1} Rate Limited. Switching...")
|
| 250 |
+
continue
|
| 251 |
+
logger.error(f"🤖 Key {i+1} Error: {err_msg}")
|
| 252 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
return "ERROR: ALL_KEYS_EXHAUSTED"
|
| 255 |
|
| 256 |
# ==============================================================================
|
| 257 |
+
# 5. 模块一:全网天梯 (Leaderboard)
|
| 258 |
# ==============================================================================
|
| 259 |
@bot.command()
|
| 260 |
async def profile(ctx, member: discord.Member = None):
|
|
|
|
|
|
|
|
|
|
| 261 |
target = member or ctx.author
|
| 262 |
u = bk_engine.get_user(target.id)
|
|
|
|
|
|
|
| 263 |
elo = u['elo']
|
| 264 |
+
|
| 265 |
+
# 完整段位逻辑
|
| 266 |
if elo < 1300: rank, color = "Iron (学徒) 🗑️", 0x717d7e
|
| 267 |
elif elo < 1500: rank, color = "Bronze (资深) 🥉", 0xcd7f32
|
| 268 |
elif elo < 1800: rank, color = "Silver (精英) 🥈", 0xbdc3c7
|
|
|
|
| 273 |
emb = discord.Embed(title=f"📊 Quantitative Profile: {target.display_name}", color=color)
|
| 274 |
emb.add_field(name="Rating (ELO)", value=f"**{elo}**\nRank: `{rank}`", inline=True)
|
| 275 |
emb.add_field(name="Assets (Points)", value=f"💰 **{u['points']}**", inline=True)
|
| 276 |
+
|
| 277 |
+
f_info = u['faction'] or "Free Agent"
|
| 278 |
+
f_emoji = bk_engine.data["factions"].get(f_info, {}).get("emoji", "")
|
| 279 |
+
emb.add_field(name="Faction", value=f"{f_emoji} {f_info}", inline=True)
|
| 280 |
|
| 281 |
+
stats = (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
f"Total Audits: {u['audits_count']}\n"
|
| 283 |
+
f"Peak Score: {u['highest_score']:.1f}\n"
|
| 284 |
+
f"Max MDD: {u.get('mdd_record',0):.2f}%"
|
|
|
|
| 285 |
)
|
| 286 |
+
emb.add_field(name="Performance Metrics", value=f"```yaml\n{stats}\n```", inline=False)
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
+
if target.avatar: emb.set_thumbnail(url=target.avatar.url)
|
| 289 |
+
emb.set_footer(text=f"Session: {SESSION_ID} | Node: HuggingFace")
|
| 290 |
await ctx.send(embed=emb)
|
| 291 |
|
| 292 |
@bot.command()
|
| 293 |
async def leaderboard(ctx):
|
| 294 |
+
users = [{"id": k, "elo": v["elo"], "score": v["highest_score"]} for k,v in bk_engine.data["users"].items()]
|
| 295 |
+
top = sorted(users, key=lambda x: x["elo"], reverse=True)[:10]
|
| 296 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
desc = ""
|
| 298 |
+
for i, u in enumerate(top):
|
| 299 |
+
icon = ["🥇","🥈","🥉"][i] if i < 3 else f"{i+1}."
|
| 300 |
+
desc += f"{icon} <@{u['id']}> : **{u['elo']}** (Peak: {u['score']:.1f})\n"
|
| 301 |
+
|
| 302 |
+
emb = discord.Embed(title="🏆 Global Ranking (Institutional)", description=desc or "No data available.", color=0xf1c40f)
|
| 303 |
+
|
| 304 |
+
# 阵营战况
|
| 305 |
+
f_txt = ""
|
| 306 |
+
for k, v in bk_engine.data["factions"].items():
|
| 307 |
+
f_txt += f"{v['emoji']} {k}: {v['score']} pts | "
|
| 308 |
+
if f_txt: emb.add_field(name="Faction War Status", value=f_txt, inline=False)
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
await ctx.send(embed=emb)
|
| 311 |
|
| 312 |
# ==============================================================================
|
| 313 |
+
# 6. 模块二:宏观情报 (Macro Intelligence)
|
| 314 |
# ==============================================================================
|
| 315 |
@bot.command()
|
| 316 |
async def news(ctx):
|
| 317 |
+
if not ctx.message.attachments: return await ctx.send("❌ Audit Refused: Please upload an image.")
|
| 318 |
+
m = await ctx.send("📡 **[HF-Node] Analyzing Macro Pulse...**")
|
| 319 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
prompt = (
|
| 321 |
+
"Analyze chart/news. Output: SCORE: (-10 to 10), SUMMARY: (text), TAKE: (institutional view). "
|
| 322 |
+
"Format: SCORE: [v] SUMMARY: [v] TAKE: [v]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
)
|
|
|
|
| 324 |
res = await call_gemini_multiplex(prompt, ctx.message.attachments[0])
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
score = safe_extract_float(res, "SCORE")
|
| 327 |
summary = safe_extract_text(res, "SUMMARY", "TAKE")
|
| 328 |
take = safe_extract_text(res, "TAKE")
|
| 329 |
+
|
| 330 |
+
emb = discord.Embed(title="📰 Macro Intelligence", color=0x2ecc71 if score > 0 else 0xe74c3c)
|
| 331 |
+
emb.add_field(name="Sentiment Score", value=f"**{score:+.1f}**")
|
| 332 |
+
emb.add_field(name="Executive Summary", value=summary, inline=False)
|
| 333 |
+
emb.add_field(name="Institutional Take", value=f"```{take}```", inline=False)
|
| 334 |
+
|
|
|
|
| 335 |
await m.delete()
|
| 336 |
await ctx.send(embed=emb)
|
| 337 |
|
|
|
|
| 341 |
@bot.command()
|
| 342 |
async def evaluate(ctx):
|
| 343 |
"""
|
| 344 |
+
带有并发锁的审计功能,防止 HF 容器 OOM
|
|
|
|
| 345 |
"""
|
| 346 |
+
if not ctx.message.attachments: return await ctx.send("❌ No image detected.")
|
| 347 |
+
|
|
|
|
| 348 |
# [Lock Check] 检查是否忙碌
|
| 349 |
if audit_lock.locked():
|
| 350 |
+
return await ctx.send("⚠️ **[System Busy]** Another audit is processing. Please wait 10s.")
|
| 351 |
|
| 352 |
async with audit_lock:
|
| 353 |
+
m = await ctx.send("🛡️ **[Audit Engine] Processing... (Locked)**")
|
|
|
|
|
|
|
|
|
|
| 354 |
prompt = (
|
| 355 |
+
"Quant Audit. Extract RET, MDD, PF, SHARPE, TRADES. S_BASE(0-100). "
|
| 356 |
+
"VIOLATION(TRUE if MDD>35). Highlights(H_ZH). "
|
| 357 |
+
"Format: RET:[v] MDD:[v] PF:[v] SHARPE:[v] TRADES:[v] S_BASE:[v] VIOLATION:[v] H_ZH:[v]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
)
|
| 359 |
+
|
| 360 |
res = await call_gemini_multiplex(prompt, ctx.message.attachments[0])
|
| 361 |
+
|
| 362 |
if "ERROR" in res:
|
| 363 |
await m.delete()
|
| 364 |
+
return await ctx.send(f"🔴 Audit Failed: {res}")
|
| 365 |
|
|
|
|
| 366 |
ret = safe_extract_float(res, "RET")
|
| 367 |
mdd = safe_extract_float(res, "MDD")
|
| 368 |
pf = safe_extract_float(res, "PF")
|
|
|
|
| 371 |
s_base = safe_extract_float(res, "S_BASE")
|
| 372 |
violation = "TRUE" in res.upper()
|
| 373 |
|
|
|
|
|
|
|
| 374 |
if violation or mdd > 35.0:
|
| 375 |
s_final = 0.0
|
| 376 |
+
status = "REJECTED (High Risk)"
|
| 377 |
color = 0xe74c3c
|
| 378 |
else:
|
| 379 |
+
# 完整评分公式
|
| 380 |
trades_adj = math.log10(max(trades, 1)) / 1.5
|
| 381 |
+
s_final = s_base * min(max(trades_adj, 0.5), 1.2) * (sharpe/2.0 if sharpe>0 else 0.5)
|
| 382 |
s_final = min(max(s_final, 0.0), 100.0)
|
| 383 |
+
status = "CLEARED"
|
| 384 |
color = 0x2ecc71
|
| 385 |
|
|
|
|
| 386 |
bk_engine.update_audit_stats(ctx.author.id, s_final, mdd, sharpe)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
+
emb = discord.Embed(title="🛡️ Audit Report", color=color)
|
| 389 |
+
emb.add_field(name="Verdict", value=status, inline=False)
|
| 390 |
+
emb.add_field(name="Metrics", value=f"```Return: {ret}%\nMDD: {mdd}%\nSharpe: {sharpe}\nPF: {pf}```", inline=False)
|
| 391 |
+
emb.add_field(name="Score", value=f"**{s_final:.1f}**")
|
| 392 |
+
emb.add_field(name="Highlights", value=safe_extract_text(res, "H_ZH"), inline=False)
|
|
|
|
|
|
|
|
|
|
| 393 |
|
|
|
|
| 394 |
await m.delete()
|
| 395 |
await ctx.send(embed=emb)
|
| 396 |
|
| 397 |
# ==============================================================================
|
| 398 |
+
# 8. 模块四:博弈盘口 (Betting System)
|
| 399 |
# ==============================================================================
|
| 400 |
@bot.command()
|
| 401 |
@commands.has_permissions(administrator=True)
|
| 402 |
+
async def open_bet(ctx, *, topic):
|
|
|
|
|
|
|
|
|
|
| 403 |
bid = str(random.randint(100, 999))
|
| 404 |
bk_engine.active_bets[bid] = {
|
| 405 |
+
"topic": topic,
|
| 406 |
+
"up": 0, "down": 0,
|
| 407 |
+
"u_up": {}, "u_down": {},
|
| 408 |
"status": "OPEN",
|
| 409 |
"start_time": str(datetime.datetime.now())
|
| 410 |
}
|
| 411 |
+
await ctx.send(f"🎰 **Bet Market Open [{bid}]**\nTopic: {topic}\nType `!bet {bid} UP 100` to join.")
|
| 412 |
|
| 413 |
@bot.command()
|
| 414 |
async def bet(ctx, bid: str, side: str, amt: int):
|
| 415 |
+
if bid not in bk_engine.active_bets or bk_engine.active_bets[bid]["status"] != "OPEN": return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
u = bk_engine.get_user(ctx.author.id)
|
| 417 |
+
if u["points"] < amt: return await ctx.send("❌ Insufficient funds.")
|
| 418 |
+
|
| 419 |
+
u["points"] -= amt; side = side.upper()
|
| 420 |
+
if side not in ["UP", "DOWN"]: return await ctx.send("❌ Choose UP or DOWN.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
+
pool = "u_up" if side == "UP" else "u_down"
|
| 423 |
+
b = bk_engine.active_bets[bid]
|
| 424 |
b[side.lower()] += amt
|
| 425 |
+
b[pool][str(ctx.author.id)] = b[pool].get(str(ctx.author.id), 0) + amt
|
|
|
|
| 426 |
bk_engine.save_db()
|
| 427 |
+
await ctx.send(f"✅ Bet {amt} on {side} confirmed.")
|
| 428 |
|
| 429 |
@bot.command()
|
| 430 |
@commands.has_permissions(administrator=True)
|
| 431 |
async def settle_bet(ctx, bid: str, winner: str):
|
|
|
|
|
|
|
|
|
|
| 432 |
if bid not in bk_engine.active_bets: return
|
| 433 |
+
b = bk_engine.active_bets[bid]; winner = winner.upper()
|
| 434 |
+
|
|
|
|
| 435 |
win_pool = b["u_up"] if winner == "UP" else b["u_down"]
|
| 436 |
+
total_w = b["up"] if winner == "UP" else b["down"]
|
| 437 |
+
total_l = b["down"] if winner == "UP" else b["up"]
|
| 438 |
+
|
| 439 |
+
if total_w > 0:
|
| 440 |
for uid, amt in win_pool.items():
|
| 441 |
+
profit = (amt/total_w) * total_l
|
| 442 |
+
bk_engine.get_user(int(uid))["points"] += int(amt + profit)
|
|
|
|
|
|
|
| 443 |
|
| 444 |
b["status"] = "SETTLED"
|
| 445 |
bk_engine.save_db()
|
| 446 |
+
await ctx.send(f"🏁 Bet {bid} Settled. Winner: {winner}. Payouts Distributed.")
|
| 447 |
|
| 448 |
# ==============================================================================
|
| 449 |
+
# 9. 模块五:PVP 竞技场 (Arena)
|
| 450 |
# ==============================================================================
|
| 451 |
@bot.command()
|
| 452 |
async def challenge(ctx, member: discord.Member):
|
| 453 |
+
if member.id == ctx.author.id: return
|
| 454 |
+
did = f"D-{random.randint(1000,9999)}"
|
| 455 |
+
bk_engine.pending_challenges[member.id] = {"c": ctx.author.id, "id": did}
|
| 456 |
+
await ctx.send(f"⚔️ {ctx.author.name} challenges {member.name}!\nID: `{did}`. Type `!accept` to start.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
|
| 458 |
@bot.command()
|
| 459 |
async def accept(ctx):
|
| 460 |
+
if ctx.author.id not in bk_engine.pending_challenges: return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
req = bk_engine.pending_challenges.pop(ctx.author.id)
|
| 462 |
+
bk_engine.active_duels[req["id"]] = {"p1": req["c"], "p2": ctx.author.id, "s1": None, "s2": None}
|
| 463 |
+
await ctx.send(f"🔥 Duel `{req['id']}` Active! Submit screenshots via `!duel_submit {req['id']}`")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
@bot.command()
|
| 466 |
async def duel_submit(ctx, did: str):
|
| 467 |
+
if did not in bk_engine.active_duels: return
|
| 468 |
+
if not ctx.message.attachments: return
|
| 469 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
duel = bk_engine.active_duels[did]
|
| 471 |
if ctx.author.id not in [duel["p1"], duel["p2"]]: return
|
| 472 |
+
|
| 473 |
+
m = await ctx.send("🔍 Auditing duel submission...")
|
| 474 |
+
res = await call_gemini_multiplex("Extract SCORE:[0-100]. Format: SCORE:[v]", ctx.message.attachments[0])
|
|
|
|
| 475 |
score = safe_extract_float(res, "SCORE")
|
| 476 |
+
|
| 477 |
if ctx.author.id == duel["p1"]: duel["s1"] = score
|
| 478 |
else: duel["s2"] = score
|
| 479 |
+
|
|
|
|
| 480 |
if duel["s1"] is not None and duel["s2"] is not None:
|
| 481 |
+
p1, p2 = duel["p1"], duel["p2"]
|
| 482 |
+
s1, s2 = duel["s1"], duel["s2"]
|
| 483 |
+
wid, lid = (p1, p2) if s1 > s2 else (p2, p1)
|
| 484 |
+
w_elo, l_elo, _ = bk_engine.update_elo(wid, lid)
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
+
emb = discord.Embed(title="🏆 Duel Result", color=0xf1c40f)
|
| 487 |
+
emb.add_field(name="Winner", value=f"<@{wid}>\nScore: {max(s1,s2)}\nELO: {w_elo}")
|
| 488 |
+
emb.add_field(name="Loser", value=f"<@{lid}>\nScore: {min(s1,s2)}\nELO: {l_elo}")
|
| 489 |
await ctx.send(embed=emb)
|
| 490 |
del bk_engine.active_duels[did]
|
| 491 |
else:
|
| 492 |
+
await ctx.send(f"✅ Score {score} recorded. Waiting for opponent.")
|
|
|
|
| 493 |
await m.delete()
|
| 494 |
|
| 495 |
# ==============================================================================
|
| 496 |
+
# 10. 模块六:海报渲染 (Full Viral Poster)
|
| 497 |
# ==============================================================================
|
| 498 |
@bot.command()
|
| 499 |
async def viral_audit(ctx):
|
| 500 |
"""
|
| 501 |
+
[完整版] 提取审计数据并渲染具备社交传播属性的海报图片
|
| 502 |
包含完整的 PIL 绘图逻辑,无省略。
|
| 503 |
"""
|
| 504 |
if not ctx.message.attachments:
|
|
|
|
| 547 |
await m.delete()
|
| 548 |
|
| 549 |
# ==============================================================================
|
| 550 |
+
# 11. 启动引擎 (Gradio + Discord Thread)
|
| 551 |
# ==============================================================================
|
| 552 |
@tasks.loop(hours=1)
|
| 553 |
async def auto_backup():
|
|
|
|
| 554 |
bk_engine.save_db()
|
|
|
|
| 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="HF-Native Node"))
|
| 560 |
+
logger.info(f"✅ [V62.9-Gradio] Online. Session: {SESSION_ID}")
|
|
|
|
| 561 |
|
| 562 |
+
def run_discord_bot():
|
| 563 |
+
if DISCORD_TOKEN:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
try:
|
| 565 |
bot.run(DISCORD_TOKEN)
|
| 566 |
except Exception as e:
|
| 567 |
+
logger.critical(f"Bot Crash: {e}")
|
| 568 |
+
else:
|
| 569 |
+
logger.critical("No Token")
|
| 570 |
+
|
| 571 |
+
if __name__ == "__main__":
|
| 572 |
+
# 1. 在后台线程启动 Discord 机器人 (非阻塞)
|
| 573 |
+
threading.Thread(target=run_discord_bot).start()
|
| 574 |
+
|
| 575 |
+
# 2. 在主线程启动 Gradio UI (这是 HF 要求的保活入口)
|
| 576 |
+
# 这会生成一个网页,显示 JSON 状态,UptimeRobot 访问这个网页就能保活
|
| 577 |
+
iface = gr.Interface(
|
| 578 |
+
fn=get_status,
|
| 579 |
+
inputs=None,
|
| 580 |
+
outputs="json",
|
| 581 |
+
title="BK-GTA Sentinel",
|
| 582 |
+
description="Core logic is running in background thread.",
|
| 583 |
+
every=5 # 自动刷新
|
| 584 |
+
)
|
| 585 |
+
iface.launch(server_name="0.0.0.0", server_port=7860)
|