From 6ef18290720a1ad603501d521d369f51c63481bc Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Wed, 12 Nov 2025 20:34:36 +0800 Subject: [PATCH] refactor(statistic): decouple statistics collection from report generation Introduces a dedicated `HTMLReportGenerator` class in `report_generator.py` to handle all aspects of HTML and chart rendering. This decouples the report presentation logic from the data collection process within `StatisticOutputTask`. Key changes include: - Migrated all HTML and JavaScript generation into the new `HTMLReportGenerator`. - Extracted all statistic key constants into a separate `statistic_keys.py` file for improved organization. - Renamed `_generate_chart_data` to `_collect_chart_data` to better reflect its purpose. - Improved data handling robustness by using `.get()` for dictionary access and safely handling database query results. --- src/chat/utils/report_generator.py | 322 +++++++++++++ src/chat/utils/statistic.py | 732 +++-------------------------- src/chat/utils/statistic_keys.py | 43 ++ src/main.py | 2 +- src/manager/local_store_manager.py | 10 +- 5 files changed, 429 insertions(+), 680 deletions(-) create mode 100644 src/chat/utils/report_generator.py create mode 100644 src/chat/utils/statistic_keys.py diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py new file mode 100644 index 000000000..59467efaa --- /dev/null +++ b/src/chat/utils/report_generator.py @@ -0,0 +1,322 @@ +""" +该模块用于生成HTML格式的统计报告。 +""" +from datetime import datetime, timedelta +from typing import Any + +import aiofiles + +from .statistic_keys import * # noqa: F403 + + +def format_online_time(online_seconds: int) -> str: + """ + 格式化在线时间。 + + :param online_seconds: 在线时间(秒)。 + :return: 格式化后的在线时间字符串。 + """ + total_online_time = timedelta(seconds=online_seconds) + days = total_online_time.days + hours = total_online_time.seconds // 3600 + minutes = (total_online_time.seconds // 60) % 60 + seconds = total_online_time.seconds % 60 + if days > 0: + return f"{days}天{hours}小时{minutes}分钟{seconds}秒" + elif hours > 0: + return f"{hours}小时{minutes}分钟{seconds}秒" + else: + return f"{minutes}分钟{seconds}秒" + + +class HTMLReportGenerator: + """生成HTML统计报告""" + + def __init__( + self, + name_mapping: dict, + stat_period: list, + deploy_time: datetime, + ): + """ + 初始化报告生成器。 + + :param name_mapping: 聊天ID到名称的映射。 + :param stat_period: 统计时间段配置。 + :param deploy_time: 系统部署时间。 + """ + self.name_mapping = name_mapping + self.stat_period = stat_period + self.deploy_time = deploy_time + + def _format_stat_data_div(self, stat_data: dict[str, Any], div_id: str, start_time: datetime, now: datetime) -> str: + """ + 将单个时间段的统计数据格式化为HTML div块。 + + :param stat_data: 统计数据。 + :param div_id: div的ID。 + :param start_time: 统计时间段的开始时间。 + :param now: 当前时间。 + :return: HTML字符串。 + """ + # 按模型分类统计 + model_rows = "\n".join( + [ + f"" + f"{model_name}" + f"{count}" + f"{stat_data[IN_TOK_BY_MODEL].get(model_name, 0)}" + f"{stat_data[OUT_TOK_BY_MODEL].get(model_name, 0)}" + f"{stat_data[TOTAL_TOK_BY_MODEL].get(model_name, 0)}" + f"{stat_data[COST_BY_MODEL].get(model_name, 0):.4f} ¥" + f"{stat_data[AVG_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒" + f"{stat_data[STD_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒" + f"" + for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items()) + ] + ) + # 按请求类型分类统计 + type_rows = "\n".join( + [ + f"" + f"{req_type}" + f"{count}" + f"{stat_data[IN_TOK_BY_TYPE].get(req_type, 0)}" + f"{stat_data[OUT_TOK_BY_TYPE].get(req_type, 0)}" + f"{stat_data[TOTAL_TOK_BY_TYPE].get(req_type, 0)}" + f"{stat_data[COST_BY_TYPE].get(req_type, 0):.4f} ¥" + f"{stat_data[AVG_TIME_COST_BY_TYPE].get(req_type, 0):.3f} 秒" + f"{stat_data[STD_TIME_COST_BY_TYPE].get(req_type, 0):.3f} 秒" + f"" + for req_type, count in sorted(stat_data[REQ_CNT_BY_TYPE].items()) + ] + ) + # 按模块分类统计 + module_rows = "\n".join( + [ + f"" + f"{module_name}" + f"{count}" + f"{stat_data[IN_TOK_BY_MODULE].get(module_name, 0)}" + f"{stat_data[OUT_TOK_BY_MODULE].get(module_name, 0)}" + f"{stat_data[TOTAL_TOK_BY_MODULE].get(module_name, 0)}" + f"{stat_data[COST_BY_MODULE].get(module_name, 0):.4f} ¥" + f"{stat_data[AVG_TIME_COST_BY_MODULE].get(module_name, 0):.3f} 秒" + f"{stat_data[STD_TIME_COST_BY_MODULE].get(module_name, 0):.3f} 秒" + f"" + for module_name, count in sorted(stat_data[REQ_CNT_BY_MODULE].items()) + ] + ) + # 聊天消息统计 + chat_rows = "\n".join( + [ + f"{self.name_mapping.get(chat_id, ('未知', 0))[0]}{count}" + for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items()) + ] + ) + return f""" +
+

+ 统计时段: + {start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")} +

+

总在线时间: {format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}

+

总消息数: {stat_data.get(TOTAL_MSG_CNT, 0)}

+

总请求数: {stat_data.get(TOTAL_REQ_CNT, 0)}

+

总花费: {stat_data.get(TOTAL_COST, 0):.4f} ¥

+ +

按模型分类统计

+ + + {model_rows} +
模型名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
+ +

按模块分类统计

+ + + + + {module_rows} +
模块名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
+ +

按请求类型分类统计

+ + + + + {type_rows} +
请求类型调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
+ +

聊天消息统计

+ + + + + {chat_rows} +
联系人/群组名称消息数量
+
+ """ + + def _generate_chart_tab(self, chart_data: dict) -> str: + """生成图表选项卡的HTML内容。""" + return f""" +
+

数据图表

+
+ + + + + +
+
+
+
+
+
+
+ + +
+ """ + + async def generate_report(self, stat: dict[str, Any], chart_data: dict, now: datetime, output_path: str): + """ + 生成并写入完整的HTML报告文件。 + + :param stat: 所有时间段的统计数据。 + :param chart_data: 用于图表的数据。 + :param now: 当前时间。 + :param output_path: 输出文件路径。 + """ + tab_list = [ + f'' + for period in self.stat_period + ] + tab_list.append('') + + tab_content_list = [ + self._format_stat_data_div(stat[period[0]], period[0], now - period[1], now) + for period in self.stat_period + if period[0] != "all_time" + ] + tab_content_list.append( + self._format_stat_data_div(stat["all_time"], "all_time", self.deploy_time, now) + ) + tab_content_list.append(self._generate_chart_tab(chart_data)) + + joined_tab_list = "\n".join(tab_list) + joined_tab_content = "\n".join(tab_content_list) + + html_template = f""" + + + + + + MoFox-Bot运行统计报告 + + + + +
+

MoFox-Bot运行统计报告

+

统计截止时间: {now.strftime("%Y-%m-%d %H:%M:%S")}

+
{joined_tab_list}
+ {joined_tab_content} +
+ + + + """ + async with aiofiles.open(output_path, "w", encoding="utf-8") as f: + await f.write(html_template) diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index aeef1f8b7..8043450d9 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -16,44 +16,8 @@ logger = get_logger("maibot_statistic") # 彻底异步化:删除原同步包装器 _sync_db_get,所有数据库访问统一使用 await db_get。 -# 统计数据的键 -TOTAL_REQ_CNT = "total_requests" -TOTAL_COST = "total_cost" -REQ_CNT_BY_TYPE = "requests_by_type" -REQ_CNT_BY_USER = "requests_by_user" -REQ_CNT_BY_MODEL = "requests_by_model" -REQ_CNT_BY_MODULE = "requests_by_module" -IN_TOK_BY_TYPE = "in_tokens_by_type" -IN_TOK_BY_USER = "in_tokens_by_user" -IN_TOK_BY_MODEL = "in_tokens_by_model" -IN_TOK_BY_MODULE = "in_tokens_by_module" -OUT_TOK_BY_TYPE = "out_tokens_by_type" -OUT_TOK_BY_USER = "out_tokens_by_user" -OUT_TOK_BY_MODEL = "out_tokens_by_model" -OUT_TOK_BY_MODULE = "out_tokens_by_module" -TOTAL_TOK_BY_TYPE = "tokens_by_type" -TOTAL_TOK_BY_USER = "tokens_by_user" -TOTAL_TOK_BY_MODEL = "tokens_by_model" -TOTAL_TOK_BY_MODULE = "tokens_by_module" -COST_BY_TYPE = "costs_by_type" -COST_BY_USER = "costs_by_user" -COST_BY_MODEL = "costs_by_model" -COST_BY_MODULE = "costs_by_module" -ONLINE_TIME = "online_time" -TOTAL_MSG_CNT = "total_messages" -MSG_CNT_BY_CHAT = "messages_by_chat" -TIME_COST_BY_TYPE = "time_costs_by_type" -TIME_COST_BY_USER = "time_costs_by_user" -TIME_COST_BY_MODEL = "time_costs_by_model" -TIME_COST_BY_MODULE = "time_costs_by_module" -AVG_TIME_COST_BY_TYPE = "avg_time_costs_by_type" -AVG_TIME_COST_BY_USER = "avg_time_costs_by_user" -AVG_TIME_COST_BY_MODEL = "avg_time_costs_by_model" -AVG_TIME_COST_BY_MODULE = "avg_time_costs_by_module" -STD_TIME_COST_BY_TYPE = "std_time_costs_by_type" -STD_TIME_COST_BY_USER = "std_time_costs_by_user" -STD_TIME_COST_BY_MODEL = "std_time_costs_by_model" -STD_TIME_COST_BY_MODULE = "std_time_costs_by_module" +from .report_generator import HTMLReportGenerator, format_online_time +from .statistic_keys import * class OnlineTimeRecordTask(AsyncTask): @@ -95,13 +59,15 @@ class OnlineTimeRecordTask(AsyncTask): if recent_records: # 找到近期记录,更新它 - self.record_id = recent_records["id"] - await db_query( - model_class=OnlineTime, - query_type="update", - filters={"id": self.record_id}, - data={"end_timestamp": extended_end_time}, - ) + record_to_use = recent_records[0] if isinstance(recent_records, list) else recent_records + self.record_id = record_to_use.get("id") + if self.record_id: + await db_query( + model_class=OnlineTime, + query_type="update", + filters={"id": self.record_id}, + data={"end_timestamp": extended_end_time}, + ) else: # 创建新记录 new_record = await db_query( @@ -115,8 +81,8 @@ class OnlineTimeRecordTask(AsyncTask): }, ) if new_record: - self.record_id = new_record["id"] - + record_to_use = new_record[0] if isinstance(new_record, list) else new_record + self.record_id = record_to_use.get("id") except Exception as e: logger.error(f"在线时间记录失败,错误信息:{e}") @@ -165,14 +131,14 @@ class StatisticOutputTask(AsyncTask): """ now = datetime.now() - if "deploy_time" in local_storage: + deploy_time_ts = local_storage.get("deploy_time") + if deploy_time_ts: # 如果存在部署时间,则使用该时间作为全量统计的起始时间 - deploy_time = datetime.fromtimestamp(local_storage["deploy_time"]) # type: ignore + deploy_time = datetime.fromtimestamp(deploy_time_ts) # type: ignore else: # 否则,使用最大时间范围,并记录部署时间为当前时间 deploy_time = datetime(2000, 1, 1) local_storage["deploy_time"] = now.timestamp() - self.stat_period: list[tuple[str, timedelta, str]] = [ ("all_time", now - deploy_time, "自部署以来"), # 必须保留"all_time" ("last_7_days", timedelta(days=7), "最近7天"), @@ -211,11 +177,21 @@ class StatisticOutputTask(AsyncTask): try: now = datetime.now() logger.info("正在收集统计数据(异步)...") - stats = await asyncio.create_task(self._collect_all_statistics(now)) + stats = await self._collect_all_statistics(now) logger.info("统计数据收集完成") + self._statistic_console_output(stats, now) - await asyncio.create_task(self._generate_html_report(stats, now)) - logger.info("统计数据输出完成") + # 使用新的 HTMLReportGenerator 生成报告 + chart_data = await self._collect_chart_data(stats) + deploy_time = datetime.fromtimestamp(local_storage.get("deploy_time", now.timestamp())) + report_generator = HTMLReportGenerator( + name_mapping=self.name_mapping, + stat_period=self.stat_period, + deploy_time=deploy_time, + ) + await report_generator.generate_report(stats, chart_data, now, self.record_file_path) + logger.info("统计数据HTML报告输出完成") + except Exception as e: logger.exception(f"输出统计数据过程中发生异常,错误信息:{e}") @@ -231,14 +207,23 @@ class StatisticOutputTask(AsyncTask): logger.info("(后台) 正在收集统计数据(异步)...") stats = await self._collect_all_statistics(now) self._statistic_console_output(stats, now) - await self._generate_html_report(stats, now) + + # 使用新的 HTMLReportGenerator 生成报告 + chart_data = await self._collect_chart_data(stats) + deploy_time = datetime.fromtimestamp(local_storage.get("deploy_time", now.timestamp())) + report_generator = HTMLReportGenerator( + name_mapping=self.name_mapping, + stat_period=self.stat_period, + deploy_time=deploy_time, + ) + await report_generator.generate_report(stats, chart_data, now, self.record_file_path) + logger.info("统计数据后台输出完成") except Exception as e: logger.exception(f"后台统计数据输出过程中发生异常:{e}") # 创建后台任务,立即返回 asyncio.create_task(_async_collect_and_output()) # noqa: RUF006 - # -- 以下为统计数据收集方法 -- @staticmethod @@ -512,12 +497,12 @@ class StatisticOutputTask(AsyncTask): continue # Update name_mapping - if chat_id in self.name_mapping: - if chat_name != self.name_mapping[chat_id][0] and message_time_ts > self.name_mapping[chat_id][1]: + if chat_name: + if chat_id in self.name_mapping: + if chat_name != self.name_mapping[chat_id][0] and message_time_ts > self.name_mapping[chat_id][1]: + self.name_mapping[chat_id] = (chat_name, message_time_ts) + else: self.name_mapping[chat_id] = (chat_name, message_time_ts) - else: - self.name_mapping[chat_id] = (chat_name, message_time_ts) - for idx, (_, period_start_dt) in enumerate(collect_period): if message_time_ts >= period_start_dt.timestamp(): for period_key, _ in collect_period[idx:]: @@ -673,341 +658,13 @@ class StatisticOutputTask(AsyncTask): return "" output = ["聊天消息统计:", " 联系人/群组名称 消息数量"] output.extend( - f"{self.name_mapping[chat_id][0][:32]:<32} {count:>10}" + f"{self.name_mapping.get(chat_id, (chat_id, 0))[0][:32]:<32} {count:>10}" for chat_id, count in sorted(stats[MSG_CNT_BY_CHAT].items()) ) output.append("") return "\n".join(output) - @staticmethod - def _get_chat_display_name_from_id(chat_id: str) -> str: - """从chat_id获取显示名称""" - try: - # 首先尝试从chat_stream获取真实群组名称 - from src.chat.message_receive.chat_stream import get_chat_manager - - chat_manager = get_chat_manager() - - if chat_id in chat_manager.streams: - stream = chat_manager.streams[chat_id] - if stream.group_info and hasattr(stream.group_info, "group_name"): - group_name = stream.group_info.group_name - if group_name and group_name.strip(): - return group_name.strip() - elif stream.user_info and hasattr(stream.user_info, "user_nickname"): - user_name = stream.user_info.user_nickname - if user_name and user_name.strip(): - return user_name.strip() - - # 如果从chat_stream获取失败,尝试解析chat_id格式 - if chat_id.startswith("g"): - return f"群聊{chat_id[1:]}" - elif chat_id.startswith("u"): - return f"用户{chat_id[1:]}" - else: - return chat_id - except Exception as e: - logger.warning(f"获取聊天显示名称失败: {e}") - return chat_id - - # 移除_generate_versions_tab方法 - - async def _generate_html_report(self, stat: dict[str, Any], now: datetime): - """ - 生成HTML格式的统计报告 - :param stat: 统计数据 - :param now: 基准当前时间 - :return: HTML格式的统计报告 - """ - - # 移除版本对比内容相关tab和内容 - tab_list = [ - f'' - for period in self.stat_period - ] - tab_list.append('') - - def _format_stat_data(stat_data: dict[str, Any], div_id: str, start_time: datetime) -> str: - """ - 格式化一个时间段的统计数据到html div块 - :param stat_data: 统计数据 - :param div_id: div的ID - :param start_time: 统计时间段开始时间 - """ - # format总在线时间 - - # 按模型分类统计 - model_rows = "\n".join( - [ - f"" - f"{model_name}" - f"{count}" - f"{stat_data[IN_TOK_BY_MODEL][model_name]}" - f"{stat_data[OUT_TOK_BY_MODEL][model_name]}" - f"{stat_data[TOTAL_TOK_BY_MODEL][model_name]}" - f"{stat_data[COST_BY_MODEL][model_name]:.4f} ¥" - f"{stat_data[AVG_TIME_COST_BY_MODEL][model_name]:.3f} 秒" - f"{stat_data[STD_TIME_COST_BY_MODEL][model_name]:.3f} 秒" - f"" - for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items()) - ] - ) - # 按请求类型分类统计 - type_rows = "\n".join( - [ - f"" - f"{req_type}" - f"{count}" - f"{stat_data[IN_TOK_BY_TYPE][req_type]}" - f"{stat_data[OUT_TOK_BY_TYPE][req_type]}" - f"{stat_data[TOTAL_TOK_BY_TYPE][req_type]}" - f"{stat_data[COST_BY_TYPE][req_type]:.4f} ¥" - f"{stat_data[AVG_TIME_COST_BY_TYPE][req_type]:.3f} 秒" - f"{stat_data[STD_TIME_COST_BY_TYPE][req_type]:.3f} 秒" - f"" - for req_type, count in sorted(stat_data[REQ_CNT_BY_TYPE].items()) - ] - ) - # 按模块分类统计 - module_rows = "\n".join( - [ - f"" - f"{module_name}" - f"{count}" - f"{stat_data[IN_TOK_BY_MODULE][module_name]}" - f"{stat_data[OUT_TOK_BY_MODULE][module_name]}" - f"{stat_data[TOTAL_TOK_BY_MODULE][module_name]}" - f"{stat_data[COST_BY_MODULE][module_name]:.4f} ¥" - f"{stat_data[AVG_TIME_COST_BY_MODULE][module_name]:.3f} 秒" - f"{stat_data[STD_TIME_COST_BY_MODULE][module_name]:.3f} 秒" - f"" - for module_name, count in sorted(stat_data[REQ_CNT_BY_MODULE].items()) - ] - ) - - # 聊天消息统计 - chat_rows = "\n".join( - [ - f"{self.name_mapping[chat_id][0]}{count}" - for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items()) - ] - ) - # 生成HTML - return f""" -
-

- 统计时段: - {start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")} -

-

总在线时间: {_format_online_time(stat_data[ONLINE_TIME])}

-

总消息数: {stat_data[TOTAL_MSG_CNT]}

-

总请求数: {stat_data[TOTAL_REQ_CNT]}

-

总花费: {stat_data[TOTAL_COST]:.4f} ¥

- -

按模型分类统计

- - - - {model_rows} - -
模块名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
- -

按模块分类统计

- - - - - - {module_rows} - -
模块名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
- -

按请求类型分类统计

- - - - - - {type_rows} - -
请求类型调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
- -

聊天消息统计

- - - - - - {chat_rows} - -
联系人/群组名称消息数量
- - -
- """ - - tab_content_list = [ - _format_stat_data(stat[period[0]], period[0], now - period[1]) - for period in self.stat_period - if period[0] != "all_time" - ] - - tab_content_list.append( - _format_stat_data(stat["all_time"], "all_time", datetime.fromtimestamp(local_storage["deploy_time"])) # type: ignore - ) - - # 不再添加版本对比内容 - # 添加图表内容 (修正缩进) - chart_data = await self._generate_chart_data(stat) - tab_content_list.append(self._generate_chart_tab(chart_data)) - - joined_tab_list = "\n".join(tab_list) - joined_tab_content = "\n".join(tab_content_list) - - html_template = ( - """ - - - - - - MoFox-Bot运行统计报告 - - - - -""" - + f""" -
-

MoFox-Bot运行统计报告

-

统计截止时间: {now.strftime("%Y-%m-%d %H:%M:%S")}

- -
- {joined_tab_list} -
- - {joined_tab_content} -
-""" - + """ - - - - """ - ) - - async with aiofiles.open(self.record_file_path, "w", encoding="utf-8") as f: - await f.write(html_template) - - async def _generate_chart_data(self, stat: dict[str, Any]) -> dict: + async def _collect_chart_data(self, stat: dict[str, Any]) -> dict: """生成图表数据 (异步)""" now = datetime.now() chart_data: dict[str, Any] = {} @@ -1088,15 +745,17 @@ class StatisticOutputTask(AsyncTask): time_diff = msg_ts - start_time.timestamp() idx = int(time_diff // interval_seconds) if 0 <= idx < len(time_points): + chat_id = None if msg.get("chat_info_group_id"): - chat_name = msg.get("chat_info_group_name") or f"群{msg['chat_info_group_id']}" + chat_id = f"g{msg['chat_info_group_id']}" elif msg.get("user_id"): - chat_name = msg.get("user_nickname") or f"用户{msg['user_id']}" - else: - continue - if chat_name not in message_by_chat: - message_by_chat[chat_name] = [0] * len(time_points) - message_by_chat[chat_name][idx] += 1 + chat_id = f"u{msg['user_id']}" + + if chat_id: + chat_name = self.name_mapping.get(chat_id, (chat_id, 0))[0] + if chat_name not in message_by_chat: + message_by_chat[chat_name] = [0] * len(time_points) + message_by_chat[chat_name][idx] += 1 return { "time_labels": time_labels, @@ -1105,282 +764,3 @@ class StatisticOutputTask(AsyncTask): "cost_by_module": cost_by_module, "message_by_chat": message_by_chat, } - - @staticmethod - def _generate_chart_tab(chart_data: dict) -> str: - # sourcery skip: extract-duplicate-method, move-assign-in-block - """生成图表选项卡HTML内容""" - - # 生成不同颜色的调色板 - colors = [ - "#3498db", - "#e74c3c", - "#2ecc71", - "#f39c12", - "#9b59b6", - "#1abc9c", - "#34495e", - "#e67e22", - "#95a5a6", - "#f1c40f", - ] - - # 默认使用24小时数据生成数据集 - default_data = chart_data["24h"] - - # 为每个模型生成数据集 - model_datasets = [] - for i, (model_name, cost_data) in enumerate(default_data["cost_by_model"].items()): - color = colors[i % len(colors)] - model_datasets.append(f"""{{ - label: '{model_name}', - data: {cost_data}, - borderColor: '{color}', - backgroundColor: '{color}20', - tension: 0.4, - fill: false - }}""") - - ",\n ".join(model_datasets) - - # 为每个模块生成数据集 - module_datasets = [] - for i, (module_name, cost_data) in enumerate(default_data["cost_by_module"].items()): - color = colors[i % len(colors)] - module_datasets.append(f"""{{ - label: '{module_name}', - data: {cost_data}, - borderColor: '{color}', - backgroundColor: '{color}20', - tension: 0.4, - fill: false - }}""") - - ",\n ".join(module_datasets) - - # 为每个聊天流生成消息数据集 - message_datasets = [] - for i, (chat_name, message_data) in enumerate(default_data["message_by_chat"].items()): - color = colors[i % len(colors)] - message_datasets.append(f"""{{ - label: '{chat_name}', - data: {message_data}, - borderColor: '{color}', - backgroundColor: '{color}20', - tension: 0.4, - fill: false - }}""") - - ",\n ".join(message_datasets) - - return f""" -
-

数据图表

- - -
- - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
- - - - -
- """ diff --git a/src/chat/utils/statistic_keys.py b/src/chat/utils/statistic_keys.py new file mode 100644 index 000000000..41e96b1be --- /dev/null +++ b/src/chat/utils/statistic_keys.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +该模块用于存放统计数据相关的常量键名。 +""" + +# 统计数据的键 +TOTAL_REQ_CNT = "total_requests" +TOTAL_COST = "total_cost" +REQ_CNT_BY_TYPE = "requests_by_type" +REQ_CNT_BY_USER = "requests_by_user" +REQ_CNT_BY_MODEL = "requests_by_model" +REQ_CNT_BY_MODULE = "requests_by_module" +IN_TOK_BY_TYPE = "in_tokens_by_type" +IN_TOK_BY_USER = "in_tokens_by_user" +IN_TOK_BY_MODEL = "in_tokens_by_model" +IN_TOK_BY_MODULE = "in_tokens_by_module" +OUT_TOK_BY_TYPE = "out_tokens_by_type" +OUT_TOK_BY_USER = "out_tokens_by_user" +OUT_TOK_BY_MODEL = "out_tokens_by_model" +OUT_TOK_BY_MODULE = "out_tokens_by_module" +TOTAL_TOK_BY_TYPE = "tokens_by_type" +TOTAL_TOK_BY_USER = "tokens_by_user" +TOTAL_TOK_BY_MODEL = "tokens_by_model" +TOTAL_TOK_BY_MODULE = "tokens_by_module" +COST_BY_TYPE = "costs_by_type" +COST_BY_USER = "costs_by_user" +COST_BY_MODEL = "costs_by_model" +COST_BY_MODULE = "costs_by_module" +ONLINE_TIME = "online_time" +TOTAL_MSG_CNT = "total_messages" +MSG_CNT_BY_CHAT = "messages_by_chat" +TIME_COST_BY_TYPE = "time_costs_by_type" +TIME_COST_BY_USER = "time_costs_by_user" +TIME_COST_BY_MODEL = "time_costs_by_model" +TIME_COST_BY_MODULE = "time_costs_by_module" +AVG_TIME_COST_BY_TYPE = "avg_time_costs_by_type" +AVG_TIME_COST_BY_USER = "avg_time_costs_by_user" +AVG_TIME_COST_BY_MODEL = "avg_time_costs_by_model" +AVG_TIME_COST_BY_MODULE = "avg_time_costs_by_module" +STD_TIME_COST_BY_TYPE = "std_time_costs_by_type" +STD_TIME_COST_BY_USER = "std_time_costs_by_user" +STD_TIME_COST_BY_MODEL = "std_time_costs_by_model" +STD_TIME_COST_BY_MODULE = "std_time_costs_by_module" \ No newline at end of file diff --git a/src/main.py b/src/main.py index a0e8572f6..f39d3f956 100644 --- a/src/main.py +++ b/src/main.py @@ -400,7 +400,7 @@ MoFox_Bot(第三方修改版) base_init_tasks = [ async_task_manager.add_task(OnlineTimeRecordTask()), async_task_manager.add_task(StatisticOutputTask()), - async_task_manager.add_task(TelemetryHeartBeatTask()), + #async_task_manager.add_task(TelemetryHeartBeatTask()), ] await asyncio.gather(*base_init_tasks, return_exceptions=True) diff --git a/src/manager/local_store_manager.py b/src/manager/local_store_manager.py index f5b5a28ca..bd94e7b56 100644 --- a/src/manager/local_store_manager.py +++ b/src/manager/local_store_manager.py @@ -21,6 +21,10 @@ class LocalStoreManager: self.store = {} self.load_local_store() + def __contains__(self, key: str) -> bool: + """检查键是否存在""" + return key in self.store + def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None: """获取本地存储数据""" return self.store.get(item) @@ -38,9 +42,9 @@ class LocalStoreManager: else: logger.warning(f"尝试删除不存在的键: {key}") - def __contains__(self, item: str) -> bool: - """检查本地存储数据是否存在""" - return item in self.store + def get(self, key: str, default=None): + """获取本地存储数据,支持默认值""" + return self.store.get(key, default) def load_local_store(self): """加载本地存储数据"""