From f67a297991b65f8351caf801b757702b9a98906f Mon Sep 17 00:00:00 2001 From: gsd Date: Mon, 8 Jun 2026 22:35:53 +0300 Subject: [PATCH] kamaz ai 2 --- other_ext/kamaz_ai_2.py | 364 ++++++++++++++++++++++++++++++++++++ other_ext/webhook_helper.py | 8 + 2 files changed, 372 insertions(+) create mode 100644 other_ext/kamaz_ai_2.py diff --git a/other_ext/kamaz_ai_2.py b/other_ext/kamaz_ai_2.py new file mode 100644 index 0000000..4797db7 --- /dev/null +++ b/other_ext/kamaz_ai_2.py @@ -0,0 +1,364 @@ +from collections import defaultdict, Counter +import math, traceback + +from discord.ext import tasks +import discord +import asyncio +import aiohttp +import os + +# ============================================================ +# ХРАНИЛИЩЕ ДАННЫХ (in-memory, наполняется при инициализации) +# ============================================================ +class DataStore: + def __init__(self): + self.reports = {} # id -> report dict + self.actions = defaultdict(list) # report_id -> [action, ...] + # индексы для быстрого поиска по обвиняемому + self.by_accused = defaultdict(list) # r_steam']['steam2'] -> [report_id] + self.by_author = defaultdict(list) # a_steam']['steam2'] -> [report_id] + + def load(self, reports): + """Полная загрузка/перезагрузка данных от главного приложения.""" + self.reports.clear() + self.actions.clear() + self.by_accused.clear() + self.by_author.clear() + + permitions = [] + for r in reports: + try: + if r['r_permition'] not in permitions: + permitions.append(r['r_permition']) + + if r['a_permition'] not in permitions: + permitions.append(r['a_permition']) + + self.reports[r['id']] = r + if r['r_steam']: + self.by_accused[r['r_steam']['steam2']].append(r['id']) + if r['a_steam']: + self.by_author[r['a_steam']['steam2']].append(r['id']) + except: + print(r) + traceback.print_exc() + raise Exception + + for a in reports: + self.actions[a['id']] = a['actions'] + print(permitions) + + def add_report(self, report): + """Добавить новую жалобу (после того как модератор принял решение).""" + self.reports[report['id']] = report + self.by_accused[report['r_steam']['steam2']].append(report['id']) + self.by_author[report['a_steam']['steam2']].append(report['id']) + + def add_action(self, report_id, action): + self.actions[report_id].append(action) + +class KamazAi2: + action_translate = { + 'ban': "забанить игрока", + 'inspect': "проверить профиль стима и решить что делать дальше", + 'kick': "кикнуть игрока", + 'ban30': "легкий бан на 30 минут", + 'ban120': "бан на пару часов", + 'noreason': "не вводить причину", + 'author_kick': "кикнуть автора репорта", + 'mute': "замьютить игрока", + 'unban': "разбанить игрока если тот в бане, ебанутое решение", + 'author_inspect': 'глянуть профиль автора репорта', + 'none': 'ничего не делать, лучше не лезть' + } + # ============================================================ + # КЛАССИФИКАЦИЯ ACTION + # ============================================================ + # Действия делятся на: применённые к обвиняемому, к автору, нейтральные. + ACTION_GROUPS = { + # против обвиняемого (жалоба обоснована) + 'ban': {'target': 'accused', 'severity': 3, 'guilty': True}, + 'kick': {'target': 'accused', 'severity': 2, 'guilty': True}, + 'inspect': {'target': 'accused', 'severity': 1, 'guilty': None}, + # против автора (жалоба ложная / абуз репорта) + 'author_kick': {'target': 'author', 'severity': 2, 'guilty': False}, + 'author_inspect': {'target': 'author', 'severity': 1, 'guilty': None}, + # нейтральные / отклонение + 'noreason': {'target': 'none', 'severity': 0, 'guilty': False}, + 'none': {"target": 'none', 'severity': 0, 'guilty': False} + } + + def permition_translate(self, permition): + if not permition or permition == 'DONT_HAVE': + return 0 + if permition in ['VIP', 'FREE']: + return 1 + if permition in ['MODERATOR']: + return 4 + if permition in ['ADMIN', 'ROOT']: + return 5 + + return 0 + + def __init__(self, reports): + self.store = DataStore() + self.store.load(reports) + + # ============================================================ + # ЭВРИСТИКИ ПО ИГРОВЫМ МЕТРИКАМ + # ============================================================ + async def kd_ratio(self, kills, deads): + try: + return kills / max(deads, 1) + except Exception as e: + return 1 + + + async def analyze_metrics(self, report): + """ + Возвращает список факторов-сигналов (флагов) с весом и пояснением. + Положительный вес score -> жалоба скорее обоснована (вина обвиняемого). + Отрицательный -> жалоба скорее ложная. + """ + factors = [] + + a_kd = await self.kd_ratio(report['a_kills'], report['a_deads']) + r_kd = await self.kd_ratio(report['r_kills'], report['r_deads']) + reason = (report.get('reasons') or '').lower() + + # --- Подозрение на читы: аномально высокий KD у обвиняемого --- + if any(w in reason for w in ['чит', 'cheat', 'аим', 'aim', 'wh', 'хак']): + if r_kd >= 5 and report['r_kills'] >= 30: + factors.append((+0.4, f"Обвиняемый имеет очень высокий KD ({r_kd:.1f}) " + f"при {report['r_kills']} убийствах — признак читов")) + elif r_kd < 2: + factors.append((-0.3, f"Жалоба на читы, но KD обвиняемого низкий ({r_kd:.1f}) " + f"— читы маловероятны")) + else: + factors.append((+0.1, f"Жалоба на читы, KD обвиняемого {r_kd:.1f} — требуется проверка")) + + # --- Абуз прав (VIP/FreeVIP абуз) --- + if 'абуз' in reason or 'vip' in reason: + if self.permition_translate(report['r_permition']) > 0: + factors.append((+0.2, f"Обвиняемый имеет права (permition={report['r_permition']}), " + f"жалоба на абуз прав правдоподобна")) + else: + factors.append((-0.4, "Жалоба на абуз прав, но у обвиняемого нет прав — необоснованно")) + + # --- Обвиняемый — администратор/модератор (защита от ложных жалоб) --- + if self.permition_translate(report['r_permition']) >= 4: + factors.append((-0.3, f"Обвиняемый имеет высокие права (permition={report['r_permition']}) " + f"— возможна ложная жалоба от обиженного игрока")) + + # --- Автор играет плохо и жалуется на сильного игрока (тильт-репорт) --- + if a_kd < 0.5 and r_kd > 2 and 'чит' not in reason: + factors.append((-0.2, f"Автор играет слабо (KD {a_kd:.1f}) против сильного оппонента " + f"(KD {r_kd:.1f}) — возможен репорт «на эмоциях»")) + + # --- Слишком мало времени на сервере у обвиняемого (нечего оценивать) --- + if report['r_seconds'] < 120: + factors.append((-0.15, f"Обвиняемый провёл на сервере мало времени " + f"({report['r_seconds']} сек) — мало данных")) + + return factors + + + # ============================================================ + # ПОИСК ПОХОЖИХ ИСТОРИЧЕСКИХ ЖАЛОБ + # ============================================================ + async def reason_similarity(self, r1, r2): + """Простое сравнение причин по словам (Жаккар).""" + s1 = set((r1 or '').lower().split()) + s2 = set((r2 or '').lower().split()) + if not s1 or not s2: + return 0.0 + return len(s1 & s2) / len(s1 | s2) + + + async def similarity(self, new_report, old_report): + """Оценка схожести двух жалоб (0..1).""" + score = 0.0 + + # тот же обвиняемый — очень сильный сигнал + if new_report['r_steam'] and old_report['r_steam'] and new_report['r_steam']['steam2'] == old_report['r_steam']['steam2']: + score += 0.5 + # тот же автор + if new_report['a_steam'] and old_report['a_steam'] and new_report['a_steam']['steam2'] == old_report['a_steam']['steam2']: + score += 0.15 + # похожая причина + score += 0.25 * await self.reason_similarity(new_report.get('reasons'), old_report.get('reasons')) + + # схожесть KD обвиняемого + new_kd = await self.kd_ratio(new_report['r_kills'], new_report['r_deads']) + old_kd = await self.kd_ratio(old_report['r_kills'], old_report['r_deads']) + kd_diff = abs(new_kd - old_kd) + score += 0.10 * math.exp(-kd_diff / 3.0) # затухание + + return min(score, 1.0) + + + async def find_similar(self, new_report, top_k=5, min_sim=0.15): + results = [] + for rid, old in self.store.reports.items(): + acts = self.store.actions.get(rid) + if not acts: + continue # без решения история бесполезна + sim = await self.similarity(new_report, old) + if sim >= min_sim: + results.append((sim, rid, old, acts)) + results.sort(key=lambda x: x[0], reverse=True) + return results[:top_k] + + + # ============================================================ + # ИТОГОВОЕ РЕШЕНИЕ + # ============================================================ + async def decide(self, new_report): + # 1. Метрические факторы + factors = await self.analyze_metrics(new_report) + metric_score = sum(w for w, _ in factors) + + # 2. Похожие прошлые жалобы + similar = await self.find_similar(new_report) + + # Взвешенное голосование по action из похожих кейсов + action_votes = Counter() + guilty_score = 0.0 + sim_total = 0.0 + + similar_payload = [] + for sim, rid, old, acts in similar: + sim_total += sim + for act in acts: + action_votes[act] += sim + meta = self.ACTION_GROUPS.get(act, {}) + if meta.get('guilty') is True: + guilty_score += sim + elif meta.get('guilty') is False: + guilty_score -= sim + similar_payload.append({ + 'report_id': rid, + 'similarity': round(sim, 3), + 'reasons': old.get('reasons'), + 'r_steam2': old['r_steam']['steam2'], + 'actions': acts, + }) + + # Нормализуем вклад истории + history_score = (guilty_score / sim_total) if sim_total > 0 else 0.0 + + # 3. Финальный счёт: история весит больше, чем эвристики + final_score = 0.6 * history_score + 0.4 * metric_score + + # 4. Рекомендованное действие + if action_votes: + recommended_action = action_votes.most_common(1)[0][0] + else: + recommended_action = None + + # Маппинг финального счёта в вердикт + if final_score >= 0.35: + verdict = 'GUILTY' # жалоба обоснована — наказать обвиняемого + if not recommended_action or self.ACTION_GROUPS.get(recommended_action, {}).get('target') != 'accused': + recommended_action = 'inspect' + elif final_score <= -0.25: + verdict = 'FALSE_REPORT' # ложная жалоба + if not recommended_action or self.ACTION_GROUPS.get(recommended_action, {}).get('target') == 'accused': + recommended_action = 'none' + else: + verdict = 'UNCERTAIN' + recommended_action = recommended_action or 'inspect' + + confidence = min(abs(final_score) + 0.1 * len(similar), 1.0) + + return { + 'verdict': verdict, + 'recommended_action': recommended_action, + 'action_translate': self.action_translate.get(recommended_action, recommended_action), + 'confidence': round(confidence, 3), + 'final_score': round(final_score, 3), + 'metric_score': round(metric_score, 3), + 'history_score': round(history_score, 3), + 'action_distribution': dict(action_votes), + 'explanations': [text for _, text in factors], + 'similar_reports': similar_payload, + } + +class Extension: + core = None + + def __init__(self, core): + self.core = core + self.kamazai2 = None + self.reports_list = [] + + async def task(self, timeout = 15): + if os.getenv('BACKEND_URL') and os.getenv("BACKEND_SECRETKEY"): + pass + else: + print("Cannot init kamazAI2, BACKEND_URL or BACKEND_SECRETKEY is missing in env") + return + + await self.core.wait_until_ready() + while True: + await self.updater() + await asyncio.sleep(timeout) + + async def updater(self): + try: + if self.kamazai2 == None: + print("Sync report list") + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: + async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/s", ssl = False) as response: + self.reports_list = await response.json() + self.kamazai2 = KamazAi2(self.reports_list) + print("KamazAI2 Enabled") + except: + traceback.print_exc() + + async def __call__(self, message: discord.Message = None): + if self.kamazai2 == None: + print("call kamazAi2 but he is not init") + return await message.reply(content=f'KamazAI2 не иницилизирован') + + try: + report_id = message.embeds[0].color.value + #print("Found report id", report_id) + except: + return await message.reply(content=f'KamazAI2 не может получить индификатор репорта') + + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: + async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/{report_id}", ssl = False) as response: + report = await response.json() + + result = await self.kamazai2.decide(report) + + content = f'KamazAI2 решил что с этим репортом надо сделать: \n' + result['action_translate'] + '\n' + 'Обоснование: ' + "\n".join(result['explanations']) + "\nОцените решение камаза где :thumbsup: - норм или :thumbsdown: - не очень" + response = await message.reply(content=content) + try: + await response.add_reaction('👍') + await response.add_reaction('👎') + except: + pass + + return response + +if __name__ == "__main__": + async def run(): + print("run") + from json import load + with open("/Users/gsd/Downloads/reports.json", "r", encoding="utf8") as report_list: + kamazAi = KamazAi2(load(report_list)) + + test_report1 = {"id":23059,"a_nickname":"серийный чувак","a_permition":"DONT_HAVE","a_kills":29,"a_deads":38,"a_seconds":3237,"r_nickname":"Корабль Бомж 1","r_permition":"DONT_HAVE","r_kills":44,"r_deads":31,"r_seconds":3598,"reasons":"Читы","utime":1780842262,"srv":"srv9","online":18,"type":"IN_GAME","actions":[],"serverName":"Норильск 2019","a_steam":{"steam3":"[U:1:1675616295]","steam2":"STEAM_0:1:837808147","steam64":"76561199635882023","community_url":"https://steamcommunity.com/profiles/76561199635882023","account_id":1675616295},"r_steam":{"steam3":"[U:1:1546493291]","steam2":"STEAM_0:1:773246645","steam64":"76561199506759019","community_url":"https://steamcommunity.com/profiles/76561199506759019","account_id":1546493291}} + test_report2 = {"id":23048,"a_nickname":"Евгений Задротов","a_permition":"FREE","a_kills":63,"a_deads":68,"a_seconds":5031,"r_nickname":"Kapusta_KvSH","r_permition":"VIP","r_kills":11,"r_deads":2,"r_seconds":436,"reasons":"VIP абуз","utime":1780837315,"srv":"srv5","online":21,"type":"IN_GAME","actions":[],"serverName":"Завод Ultimate","a_steam":{"steam3":"[U:1:1623272121]","steam2":"STEAM_0:1:811636060","steam64":"76561199583537849","community_url":"https://steamcommunity.com/profiles/76561199583537849","account_id":1623272121},"r_steam":{"steam3":"[U:1:196806785]","steam2":"STEAM_0:1:98403392","steam64":"76561198157072513","community_url":"https://steamcommunity.com/profiles/76561198157072513","account_id":196806785}} + + print(test_report1) + print(await kamazAi.decide(test_report1)) + print() + print(test_report2) + print(await kamazAi.decide(test_report2)) + import asyncio + asyncio.run(run()) \ No newline at end of file diff --git a/other_ext/webhook_helper.py b/other_ext/webhook_helper.py index 628d2f4..d5f83c4 100644 --- a/other_ext/webhook_helper.py +++ b/other_ext/webhook_helper.py @@ -68,6 +68,14 @@ class Extension: print("Cannot call kamazAI") traceback.print_exc() + try: + #kamazai2 + kwargs = {'message': message} + await self.core.loaded_extensions['kamaz_ai_2'](**kwargs) + except: + print("Cannot call kamazAI2") + traceback.print_exc() + return @core.listen()