From a8e8a5d170b03b79e6b793c9fc008624d8efe8cf Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 10:56:33 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat(report):=20=E4=B8=BA=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8A=A5=E5=91=8A=E5=BC=95=E5=85=A5=E5=9B=BE=E5=BD=A2?= =?UTF-8?q?=E5=8C=96=E8=B4=B9=E7=94=A8=E5=88=86=E6=9E=90=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为增强 HTML 报告的可读性和洞察力,此变更集成了 Chart.js 来将关键的成本数据进行可视化。 现在,报告的每个标签页都会包含一个“数据总览”部分,其中有两个核心图表: 1. **供应商成本构成图 (饼图)**: 直观展示不同供应商的费用占比,帮助快速定位主要开销来源。 2. **模型成本排行榜 (条形图)**: 按费用降序排列各个模型,清晰地识别出成本最高的模型。 这些图表是动态生成的,如果统计周期内数据不足,将显示提示信息,避免用户面对空白图表。 --- src/chat/utils/report_generator.py | 83 +++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py index 5be2fd19a..5526781fa 100644 --- a/src/chat/utils/report_generator.py +++ b/src/chat/utils/report_generator.py @@ -154,8 +154,7 @@ class HTMLReportGenerator: """ # 增加饼图和条形图 - # static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 - static_charts = "" + static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 return f"""

@@ -365,3 +364,83 @@ class HTMLReportGenerator: """ async with aiofiles.open(output_path, "w", encoding="utf-8") as f: await f.write(html_template) + def _generate_static_charts_div(self, stat_data: dict[str, Any], period_id: str) -> str: + """生成静态图表(饼图、条形图)的HTML和JS。""" + provider_cost_data = stat_data.get(PIE_CHART_COST_BY_PROVIDER, {}) + model_cost_data = stat_data.get(BAR_CHART_COST_BY_MODEL, {}) + + if not provider_cost_data and not model_cost_data: + return "

数据总览

当前时段暂无足够数据生成图表。

" + + provider_labels = provider_cost_data.get('labels', []) + provider_data = provider_cost_data.get('data', []) + model_labels = model_cost_data.get('labels', []) + model_data = model_cost_data.get('data', []) + + return f""" +

数据总览

+
+
+ +
+
+ +
+
+ + """ From 5326c69163844e6a5b1672713c1cdedb0a04add4 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:10:16 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat(prompt):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=BE=A4=E8=81=8A=E6=8F=90=E9=86=92=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=86=85=E5=AD=98=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在系统提示中引入动态群聊提醒,使其具备上下文意识。现在仅在对话发生在群聊环境中时注入该提醒,从而防止在私聊中可能引起模型混淆。 这是通过向`PromptParameters`添加`group_chat_reminder_block`实现的,该模块会根据聊天类型有条件地填充。 此外,本次提交还包括修复在内存搜索过程中可能出现的`AttributeError`,确保在尝试使用内存模块之前先进行配置。 --- src/chat/replyer/default_generator.py | 38 ++++++++++++++++++--------- src/chat/utils/prompt.py | 4 +++ src/chat/utils/prompt_params.py | 3 ++- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index be7fc3ea3..d145c6db0 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -9,7 +9,7 @@ import re import time import traceback from datetime import datetime, timedelta -from typing import Any +from typing import Any, Literal from src.chat.express.expression_selector import expression_selector from src.chat.message_receive.chat_stream import ChatStream @@ -129,7 +129,7 @@ def init_prompt(): ## 规则 {safety_guidelines_block} -注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。 +{group_chat_reminder_block} 你的回复应该是一条简短、完整且口语化的回复。 -------------------------------- @@ -211,7 +211,7 @@ If you need to use the search tool, please directly call the function "lpmm_sear ## 规则 {safety_guidelines_block} -注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。 +{group_chat_reminder_block} 你的回复应该是一条简短、完整且口语化的回复。 -------------------------------- @@ -355,7 +355,7 @@ class DefaultReplyer: try: # 从available_actions中提取prompt_mode(由action_manager传递) # 如果没有指定,默认使用s4u模式 - prompt_mode_value: str = "s4u" + prompt_mode_value: Any = "s4u" if available_actions and "_prompt_mode" in available_actions: mode = available_actions.get("_prompt_mode", "s4u") # 确保类型安全 @@ -602,14 +602,20 @@ class DefaultReplyer: } # 使用记忆管理器的智能检索(多查询策略) - memories = await manager.search_memories( - query=target, - top_k=global_config.memory.search_top_k, - min_importance=global_config.memory.search_min_importance, - include_forgotten=False, - use_multi_query=True, - context=query_context, - ) + memories = [] + if global_config.memory: + memories = [] + if global_config.memory: + top_k = global_config.memory.search_top_k + min_importance = global_config.memory.search_min_importance + memories = await manager.search_memories( + query=target, + top_k=top_k, + min_importance=min_importance, + include_forgotten=False, + use_multi_query=True, + context=query_context, + ) if memories: logger.info(f"[记忆图] 检索到 {len(memories)} 条相关记忆") @@ -1095,7 +1101,7 @@ class DefaultReplyer: available_actions: dict[str, ActionInfo] | None = None, enable_tool: bool = True, reply_message: DatabaseMessages | None = None, - prompt_mode: str = "s4u", # 新增参数:s4u 或 normal + prompt_mode: Literal["s4u", "normal", "minimal"] = "s4u", # 新增参数:s4u 或 normal ) -> str: """ 构建回复器上下文 @@ -1506,6 +1512,11 @@ class DefaultReplyer: auth_role_prompt_block = await self._build_auth_role_prompt() + # 动态构建群聊提醒 + group_chat_reminder_block = "" + if is_group_chat: + group_chat_reminder_block = "注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。" + # 使用新的统一Prompt系统 - 创建PromptParameters prompt_parameters = PromptParameters( chat_scene=chat_scene_prompt, @@ -1542,6 +1553,7 @@ class DefaultReplyer: mood_prompt=mood_prompt, auth_role_prompt_block=auth_role_prompt_block, action_descriptions=action_descriptions, + group_chat_reminder_block=group_chat_reminder_block, bot_name=global_config.bot.nickname, bot_nickname=",".join(global_config.bot.alias_names) if global_config.bot.alias_names else "", ) diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index ae1a7a311..bb0686e9d 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -910,6 +910,8 @@ class Prompt: or context_data.get("auth_role_prompt_block", ""), "chat_scene": self.parameters.chat_scene or "你正在一个QQ群里聊天,你需要理解整个群的聊天动态和话题走向,并做出自然的回应。", + "group_chat_reminder_block": self.parameters.group_chat_reminder_block + or context_data.get("group_chat_reminder_block", ""), } def _prepare_normal_params(self, context_data: dict[str, Any]) -> dict[str, Any]: @@ -952,6 +954,8 @@ class Prompt: or "你正在一个QQ群里聊天,你需要理解整个群的聊天动态和话题走向,并做出自然的回应。", "bot_name": self.parameters.bot_name, "bot_nickname": self.parameters.bot_nickname, + "group_chat_reminder_block": self.parameters.group_chat_reminder_block + or context_data.get("group_chat_reminder_block", ""), } def _prepare_default_params(self, context_data: dict[str, Any]) -> dict[str, Any]: diff --git a/src/chat/utils/prompt_params.py b/src/chat/utils/prompt_params.py index 47c6d46e4..9f6c60d3a 100644 --- a/src/chat/utils/prompt_params.py +++ b/src/chat/utils/prompt_params.py @@ -63,7 +63,8 @@ class PromptParameters: mood_prompt: str = "" action_descriptions: str = "" notice_block: str = "" - + group_chat_reminder_block: str = "" + # 可用动作信息 available_actions: dict[str, Any] | None = None From bc533880dded08f4e7e093f636fa1dbe396a3d6f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 11:26:28 +0800 Subject: [PATCH 03/11] =?UTF-8?q?refactor(report):=20=E7=A7=BB=E9=99=A4HTM?= =?UTF-8?q?L=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了HTML报告生成功能,包括 `HTMLReportGenerator` 类及其在统计输出任务中的调用逻辑。 此举旨在简化代码库,剥离一个维护成本较高的功能。删除该组件可以简化统计模块并减少依赖,同时也为未来可能采用更现代、更灵活的报告方案做准备。 BREAKING CHANGE: 系统不再生成HTML格式的统计报告。 --- src/chat/utils/report_generator.py | 446 ----------------------------- src/chat/utils/statistic.py | 23 -- 2 files changed, 469 deletions(-) delete mode 100644 src/chat/utils/report_generator.py diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py deleted file mode 100644 index 5526781fa..000000000 --- a/src/chat/utils/report_generator.py +++ /dev/null @@ -1,446 +0,0 @@ -""" -该模块用于生成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[AVG_TOK_BY_MODEL].get(model_name, 0)}" - f"{stat_data[TOTAL_TOK_BY_MODEL].get(model_name, 0)}" - f"{stat_data[TPS_BY_MODEL].get(model_name, 0):.2f}" - f"{stat_data[COST_PER_KTOK_BY_MODEL].get(model_name, 0):.4f} ¥" - 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"" - for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items()) - ] - ) - # 按供应商分类统计 - provider_rows = "\n".join( - [ - f"" - f"{provider_name}" - f"{count}" - f"{stat_data[TOTAL_TOK_BY_PROVIDER].get(provider_name, 0)}" - f"{stat_data[TPS_BY_PROVIDER].get(provider_name, 0):.2f}" - f"{stat_data[COST_PER_KTOK_BY_PROVIDER].get(provider_name, 0):.4f} ¥" - f"{stat_data[COST_BY_PROVIDER].get(provider_name, 0):.4f} ¥" - f"" - for provider_name, count in sorted(stat_data[REQ_CNT_BY_PROVIDER].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()) - ] - ) - summary_cards = f""" -
-
-

总花费

-

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

-
-
-

总请求数

-

{stat_data.get(TOTAL_REQ_CNT, 0)}

-
-
-

总Token数

-

{sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values())}

-
-
-

总消息数

-

{stat_data.get(TOTAL_MSG_CNT, 0)}

-
-
-

总在线时间

-

{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}

-
-
- """ - - # 增加饼图和条形图 - static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 - return f""" -
-

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

- {summary_cards} - {static_charts} - -

按模型分类统计

- - - {model_rows} -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - {provider_rows} -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

- - - - - {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) - def _generate_static_charts_div(self, stat_data: dict[str, Any], period_id: str) -> str: - """生成静态图表(饼图、条形图)的HTML和JS。""" - provider_cost_data = stat_data.get(PIE_CHART_COST_BY_PROVIDER, {}) - model_cost_data = stat_data.get(BAR_CHART_COST_BY_MODEL, {}) - - if not provider_cost_data and not model_cost_data: - return "

数据总览

当前时段暂无足够数据生成图表。

" - - provider_labels = provider_cost_data.get('labels', []) - provider_data = provider_cost_data.get('data', []) - model_labels = model_cost_data.get('labels', []) - model_data = model_cost_data.get('data', []) - - return f""" -

数据总览

-
-
- -
-
- -
-
- - """ diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index 08d2ed8be..8007f8f97 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -16,7 +16,6 @@ logger = get_logger("maibot_statistic") # 彻底异步化:删除原同步包装器 _sync_db_get,所有数据库访问统一使用 await db_get。 -from .report_generator import HTMLReportGenerator, format_online_time from .statistic_keys import * @@ -181,16 +180,6 @@ class StatisticOutputTask(AsyncTask): logger.info("统计数据收集完成") self._statistic_console_output(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("统计数据HTML报告输出完成") except Exception as e: logger.exception(f"输出统计数据过程中发生异常,错误信息:{e}") @@ -207,18 +196,6 @@ class StatisticOutputTask(AsyncTask): logger.info("(后台) 正在收集统计数据(异步)...") stats = await self._collect_all_statistics(now) self._statistic_console_output(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}") From 08c1d73f7774f76e3c11b509e58d187ce90efa40 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:28:25 +0800 Subject: [PATCH 04/11] =?UTF-8?q?refactor(chat):=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E5=92=8C=E6=94=B9=E8=BF=9B=E5=9B=9E=E5=A4=8D=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前用于过滤 `[回复...]` 格式的实现依赖于多个正则表达式。这种方法比较复杂,并且无法正确处理深度嵌套的回复字符串。 此提交重构了逻辑,采用更简单、更稳健的基于字符串的方法。通过在以 `[回复` 开头的字符串中找到最后一个闭合括号 `]`,新逻辑可以可靠地去除整个回复前缀,无论嵌套深度如何。这提高了解析的准确性,并简化了代码。 --- src/chat/utils/utils.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 97874b714..38205477e 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -930,12 +930,11 @@ def filter_system_format_content(content: str | None) -> str: 过滤系统格式化内容,移除回复、@、图片、表情包等系统生成的格式文本 此方法过滤以下类型的系统格式化内容: - 1. 回复格式:[回复xxx],说:xxx + 1. 回复格式:[回复xxx],说:xxx (包括深度嵌套) 2. 表情包格式:[表情包:xxx] 3. 图片格式:[图片:xxx] 4. @格式:@ 5. 错误格式:[表情包(...)]、[图片(...)] - 6. [回复开头的格式 Args: content: 原始内容 @@ -949,29 +948,20 @@ def filter_system_format_content(content: str | None) -> str: original_content = content cleaned_content = content.strip() - # 1. 移除回复格式:[回复xxx],说:xxx(各种变体) - # 匹配所有包含"],说:"格式的回复 - cleaned_content = re.sub(r"\[回复[^\]]*\],说:\s*", "", cleaned_content) - # 匹配 [回复],说:xxx 格式 - cleaned_content = re.sub(r"\[回复<[^>]*>\],说:\s*", "", cleaned_content) - - # 2. 处理原有的[回复开头格式(保持向后兼容) - # 注意:这步要在上面处理完成后再执行,避免冲突 + # 核心逻辑:优先处理最复杂的[回复...]格式,特别是嵌套格式。 + # 这种方法最稳健:如果以[回复开头,就找到最后一个],然后切掉之前的所有内容。 if cleaned_content.startswith("[回复"): last_bracket_index = cleaned_content.rfind("]") if last_bracket_index != -1: cleaned_content = cleaned_content[last_bracket_index + 1 :].strip() - # 3. 移除表情包格式:[表情包:xxx] + # 在处理完回复格式后,再清理其他简单的格式 + # 移除表情包格式:[表情包:xxx] cleaned_content = re.sub(r"\[表情包:[^\]]*\]", "", cleaned_content) - - # 4. 移除图片格式:[图片:xxx] + # 移除图片格式:[图片:xxx] cleaned_content = re.sub(r"\[图片:[^\]]*\]", "", cleaned_content) - - # 5. 移除@格式:@ + # 移除@格式:@ cleaned_content = re.sub(r"@<[^>]*>", "", cleaned_content) - - # 6. 移除其他可能的系统格式 # [表情包(描述生成失败)] 等错误格式 cleaned_content = re.sub(r"\[表情包\([^)]*\)\]", "", cleaned_content) # [图片(描述生成失败)] 等错误格式 From 81b83c88dc553b22964fff68c05c245619ca6a19 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 11:35:41 +0800 Subject: [PATCH 05/11] =?UTF-8?q?Revert=20"refactor(report):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4HTML=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bc533880dded08f4e7e093f636fa1dbe396a3d6f. --- src/chat/utils/report_generator.py | 446 +++++++++++++++++++++++++++++ src/chat/utils/statistic.py | 23 ++ 2 files changed, 469 insertions(+) create mode 100644 src/chat/utils/report_generator.py diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py new file mode 100644 index 000000000..5526781fa --- /dev/null +++ b/src/chat/utils/report_generator.py @@ -0,0 +1,446 @@ +""" +该模块用于生成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[AVG_TOK_BY_MODEL].get(model_name, 0)}" + f"{stat_data[TOTAL_TOK_BY_MODEL].get(model_name, 0)}" + f"{stat_data[TPS_BY_MODEL].get(model_name, 0):.2f}" + f"{stat_data[COST_PER_KTOK_BY_MODEL].get(model_name, 0):.4f} ¥" + 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"" + for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items()) + ] + ) + # 按供应商分类统计 + provider_rows = "\n".join( + [ + f"" + f"{provider_name}" + f"{count}" + f"{stat_data[TOTAL_TOK_BY_PROVIDER].get(provider_name, 0)}" + f"{stat_data[TPS_BY_PROVIDER].get(provider_name, 0):.2f}" + f"{stat_data[COST_PER_KTOK_BY_PROVIDER].get(provider_name, 0):.4f} ¥" + f"{stat_data[COST_BY_PROVIDER].get(provider_name, 0):.4f} ¥" + f"" + for provider_name, count in sorted(stat_data[REQ_CNT_BY_PROVIDER].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()) + ] + ) + summary_cards = f""" +
+
+

总花费

+

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

+
+
+

总请求数

+

{stat_data.get(TOTAL_REQ_CNT, 0)}

+
+
+

总Token数

+

{sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values())}

+
+
+

总消息数

+

{stat_data.get(TOTAL_MSG_CNT, 0)}

+
+
+

总在线时间

+

{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}

+
+
+ """ + + # 增加饼图和条形图 + static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 + return f""" +
+

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

+ {summary_cards} + {static_charts} + +

按模型分类统计

+ + + {model_rows} +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + {provider_rows} +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

+ + + + + {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) + def _generate_static_charts_div(self, stat_data: dict[str, Any], period_id: str) -> str: + """生成静态图表(饼图、条形图)的HTML和JS。""" + provider_cost_data = stat_data.get(PIE_CHART_COST_BY_PROVIDER, {}) + model_cost_data = stat_data.get(BAR_CHART_COST_BY_MODEL, {}) + + if not provider_cost_data and not model_cost_data: + return "

数据总览

当前时段暂无足够数据生成图表。

" + + provider_labels = provider_cost_data.get('labels', []) + provider_data = provider_cost_data.get('data', []) + model_labels = model_cost_data.get('labels', []) + model_data = model_cost_data.get('data', []) + + return f""" +

数据总览

+
+
+ +
+
+ +
+
+ + """ diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index 8007f8f97..08d2ed8be 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -16,6 +16,7 @@ logger = get_logger("maibot_statistic") # 彻底异步化:删除原同步包装器 _sync_db_get,所有数据库访问统一使用 await db_get。 +from .report_generator import HTMLReportGenerator, format_online_time from .statistic_keys import * @@ -180,6 +181,16 @@ class StatisticOutputTask(AsyncTask): logger.info("统计数据收集完成") self._statistic_console_output(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("统计数据HTML报告输出完成") except Exception as e: logger.exception(f"输出统计数据过程中发生异常,错误信息:{e}") @@ -196,6 +207,18 @@ class StatisticOutputTask(AsyncTask): logger.info("(后台) 正在收集统计数据(异步)...") stats = await self._collect_all_statistics(now) self._statistic_console_output(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}") From b1a022cc3c007ef53cec7bead44d8c1823dc326e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 12:09:37 +0800 Subject: [PATCH 06/11] =?UTF-8?q?refactor(report):=20=E4=BD=BF=E7=94=A8Jin?= =?UTF-8?q?ja2=E6=A8=A1=E6=9D=BF=E9=87=8D=E6=9E=84=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将报告生成逻辑与表示层分离,以提高代码的可维护性和可读性。 - HTML、CSS 和 JavaScript 从 Python f-string 中提取到独立的模板文件中。 - 引入 Jinja2 模板引擎动态渲染报告内容,使未来修改报告样式和结构更加方便,实现了逻辑和视图的分离。 --- src/chat/utils/report_generator.py | 336 ++++---------------- src/chat/utils/templates/charts_tab.html | 16 + src/chat/utils/templates/report.css | 177 +++++++++++ src/chat/utils/templates/report.html | 19 ++ src/chat/utils/templates/report.js | 134 ++++++++ src/chat/utils/templates/static_charts.html | 9 + src/chat/utils/templates/tab_content.html | 44 +++ 7 files changed, 468 insertions(+), 267 deletions(-) create mode 100644 src/chat/utils/templates/charts_tab.html create mode 100644 src/chat/utils/templates/report.css create mode 100644 src/chat/utils/templates/report.html create mode 100644 src/chat/utils/templates/report.js create mode 100644 src/chat/utils/templates/static_charts.html create mode 100644 src/chat/utils/templates/tab_content.html diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py index 5526781fa..3bc974757 100644 --- a/src/chat/utils/report_generator.py +++ b/src/chat/utils/report_generator.py @@ -1,9 +1,12 @@ """ 该模块用于生成HTML格式的统计报告。 """ + from datetime import datetime, timedelta from typing import Any - +import json +import os +from jinja2 import Environment, FileSystemLoader import aiofiles from .statistic_keys import * # noqa: F403 @@ -48,6 +51,9 @@ class HTMLReportGenerator: self.name_mapping = name_mapping self.stat_period = stat_period self.deploy_time = deploy_time + # 初始化Jinja2环境 + template_dir = os.path.join(os.path.dirname(__file__), "templates") + self.jinja_env = Environment(loader=FileSystemLoader(template_dir)) def _format_stat_data_div(self, stat_data: dict[str, Any], div_id: str, start_time: datetime, now: datetime) -> str: """ @@ -152,134 +158,37 @@ class HTMLReportGenerator:
""" - # 增加饼图和条形图 - static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 - return f""" -
-

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

- {summary_cards} - {static_charts} + static_charts = self._generate_static_charts_div(stat_data, div_id) + template = self.jinja_env.get_template("tab_content.html") + return template.render( + div_id=div_id, + start_time=start_time.strftime("%Y-%m-%d %H:%M:%S"), + end_time=now.strftime("%Y-%m-%d %H:%M:%S"), + summary_cards=summary_cards, + static_charts=static_charts, + model_rows=model_rows, + provider_rows=provider_rows, + module_rows=module_rows, + type_rows=type_rows, + chat_rows=chat_rows, + ) -

按模型分类统计

- - - {model_rows} -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - {provider_rows} -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

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

数据图表

-
- - - - - -
-
-
-
-
-
-
- - -
+ template = self.jinja_env.get_template("charts_tab.html") + return template.render() + + def _generate_static_charts_div(self, stat_data: dict[str, Any], div_id: str)-> str: """ + 生成静态图表的HTML div。 + + :param stat_data: 统计数据。 + :param div_id: The ID for the period, used to uniquely identify chart canvases. + :return: 渲染后的HTML字符串。 + """ + template = self.jinja_env.get_template("static_charts.html") + return template.render(period_id=div_id) async def generate_report(self, stat: dict[str, Any], chart_data: dict, now: datetime, output_path: str): """ @@ -290,157 +199,50 @@ class HTMLReportGenerator: :param now: 当前时间。 :param output_path: 输出文件路径。 """ - tab_list = [ + tab_list_html = [ f'' for period in self.stat_period ] - tab_list.append('') + tab_list_html.append('') - tab_content_list = [ + tab_content_html_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_html_list.append(self._format_stat_data_div(stat["all_time"], "all_time", self.deploy_time, now)) + tab_content_html_list.append(self._generate_chart_tab(chart_data)) + + static_chart_data = {} + for period in self.stat_period: + period_id = period[0] + static_chart_data[period_id] = { + "provider_cost_data": stat[period_id].get(PIE_CHART_COST_BY_PROVIDER, {}), + "model_cost_data": stat[period_id].get(BAR_CHART_COST_BY_MODEL, {}), + } + static_chart_data["all_time"] = { + "provider_cost_data": stat["all_time"].get(PIE_CHART_COST_BY_PROVIDER, {}), + "model_cost_data": stat["all_time"].get(BAR_CHART_COST_BY_MODEL, {}), + } + + # 渲染模板 + # 读取CSS和JS文件内容 + async with aiofiles.open(os.path.join(self.jinja_env.loader.searchpath[0], "report.css"), "r", encoding="utf-8") as f: + report_css = await f.read() + async with aiofiles.open(os.path.join(self.jinja_env.loader.searchpath[0], "report.js"), "r", encoding="utf-8") as f: + report_js = await f.read() + # 渲染模板 + template = self.jinja_env.get_template("report.html") + rendered_html = template.render( + report_title="MoFox-Bot运行统计报告", + generation_time=now.strftime("%Y-%m-%d %H:%M:%S"), + tab_list="\n".join(tab_list_html), + tab_content="\n".join(tab_content_html_list), + all_chart_data=json.dumps(chart_data), + static_chart_data=json.dumps(static_chart_data), + report_css=report_css, + report_js=report_js, ) - 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) - def _generate_static_charts_div(self, stat_data: dict[str, Any], period_id: str) -> str: - """生成静态图表(饼图、条形图)的HTML和JS。""" - provider_cost_data = stat_data.get(PIE_CHART_COST_BY_PROVIDER, {}) - model_cost_data = stat_data.get(BAR_CHART_COST_BY_MODEL, {}) - - if not provider_cost_data and not model_cost_data: - return "

数据总览

当前时段暂无足够数据生成图表。

" - - provider_labels = provider_cost_data.get('labels', []) - provider_data = provider_cost_data.get('data', []) - model_labels = model_cost_data.get('labels', []) - model_data = model_cost_data.get('data', []) - - return f""" -

数据总览

-
-
- -
-
- -
-
- - """ + await f.write(rendered_html) diff --git a/src/chat/utils/templates/charts_tab.html b/src/chat/utils/templates/charts_tab.html new file mode 100644 index 000000000..52e6c3024 --- /dev/null +++ b/src/chat/utils/templates/charts_tab.html @@ -0,0 +1,16 @@ +
+

数据图表

+
+ + + + + +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/chat/utils/templates/report.css b/src/chat/utils/templates/report.css new file mode 100644 index 000000000..1c6603e5d --- /dev/null +++ b/src/chat/utils/templates/report.css @@ -0,0 +1,177 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f4f7f6; + color: #333; + line-height: 1.6; +} + +.container { + max-width: 900px; + margin: 20px auto; + background-color: #fff; + padding: 25px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +h1, h2 { + color: #2c3e50; + border-bottom: 2px solid #3498db; + padding-bottom: 10px; + margin-top: 0; +} + +h1 { + text-align: center; + font-size: 2em; +} + +h2 { + font-size: 1.5em; + margin-top: 30px; +} + +p { + margin-bottom: 10px; +} + +.info-item { + background-color: #ecf0f1; + padding: 8px 12px; + border-radius: 4px; + margin-bottom: 8px; + font-size: 0.95em; +} + +.info-item strong { + color: #2980b9; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; + font-size: 0.9em; +} + +th, td { + border: 1px solid #ddd; + padding: 10px; + text-align: left; +} + +th { + background-color: #3498db; + color: white; + font-weight: bold; +} + +tr:nth-child(even) { + background-color: #f9f9f9; +} + +.footer { + text-align: center; + margin-top: 30px; + font-size: 0.8em; + color: #7f8c8d; +} + +.tabs { + overflow: hidden; + background: #ecf0f1; + display: flex; +} + +.tabs button { + background: inherit; + border: none; + outline: none; + padding: 14px 16px; + cursor: pointer; + transition: 0.3s; + font-size: 16px; +} + +.tabs button:hover { + background-color: #d4dbdc; +} + +.tabs button.active { + background-color: #b3bbbd; +} + +.tab-content { + display: none; + padding: 20px; + background-color: #fff; + border: 1px solid #ccc; +} + +.tab-content.active { + display: block; +} + +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin: 20px 0; +} + +.card { + background-color: #ecf0f1; + padding: 15px; + border-radius: 5px; + text-align: center; +} + +.card h3 { + margin: 0 0 10px; + font-size: 1em; + color: #2c3e50; +} + +.card p { + margin: 0; + font-size: 1.2em; + font-weight: bold; + color: #34495e; +} + +.chart-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 20px; +} + +.chart-container { + position: relative; + height: 40vh; + width: 100%; +} + +.time-range-btn { + background-color: #ecf0f1; + border: 1px solid #bdc3c7; + color: #2c3e50; + padding: 8px 16px; + margin: 0 5px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; +} + +.time-range-btn:hover { + background-color: #d5dbdb; +} + +.time-range-btn.active { + background-color: #3498db; + color: white; + border-color: #2980b9; +} \ No newline at end of file diff --git a/src/chat/utils/templates/report.html b/src/chat/utils/templates/report.html new file mode 100644 index 000000000..9ac7d2e3e --- /dev/null +++ b/src/chat/utils/templates/report.html @@ -0,0 +1,19 @@ + + + + + + {{ report_title }} + + + + +
+

{{ report_title }}

+

统计截止时间: {{ generation_time }}

+
{{ tab_list }}
+ {{ tab_content }} +
+ + + \ No newline at end of file diff --git a/src/chat/utils/templates/report.js b/src/chat/utils/templates/report.js new file mode 100644 index 000000000..7f944cb3a --- /dev/null +++ b/src/chat/utils/templates/report.js @@ -0,0 +1,134 @@ +let i, tab_content, tab_links; +tab_content = document.getElementsByClassName("tab-content"); +tab_links = document.getElementsByClassName("tab-link"); +if (tab_content.length > 0) tab_content[0].classList.add("active"); +if (tab_links.length > 0) tab_links[0].classList.add("active"); +function showTab(evt, tabName) { + for (i = 0; i < tab_content.length; i++) tab_content[i].classList.remove("active"); + for (i = 0; i < tab_links.length; i++) tab_links[i].classList.remove("active"); + document.getElementById(tabName).classList.add("active"); + evt.currentTarget.classList.add("active"); +} + +document.addEventListener('DOMContentLoaded', function () { + // This is a placeholder for chart data which will be injected by python. + const allChartData = JSON.parse('{{ all_chart_data }}') +; + let currentCharts = {}; + const chartConfigs = { + totalCost: { id: 'totalCostChart', title: '总花费', yAxisLabel: '花费 (¥)', dataKey: 'total_cost_data', fill: true }, + costByModule: { id: 'costByModuleChart', title: '各模块花费', yAxisLabel: '花费 (¥)', dataKey: 'cost_by_module', fill: false }, + costByModel: { id: 'costByModelChart', title: '各模型花费', yAxisLabel: '花费 (¥)', dataKey: 'cost_by_model', fill: false }, + messageByChat: { id: 'messageByChatChart', title: '各聊天流消息数', yAxisLabel: '消息数', dataKey: 'message_by_chat', fill: false } + }; + + window.switchTimeRange = function(timeRange) { + document.querySelectorAll('.time-range-btn').forEach(btn => btn.classList.remove('active')); + event.target.classList.add('active'); + updateAllCharts(allChartData[timeRange], timeRange); + } + + function updateAllCharts(data, timeRange) { + Object.values(currentCharts).forEach(chart => chart && chart.destroy()); + currentCharts = {}; + Object.keys(chartConfigs).forEach(type => createChart(type, data, timeRange)); + } + + function createChart(chartType, data, timeRange) { + const config = chartConfigs[chartType]; + if (!data || !data[config.dataKey]) return; + const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e', '#e67e22', '#95a5a6', '#f1c40f']; + let datasets = []; + if (chartType === 'totalCost') { + datasets = [{ label: config.title, data: data[config.dataKey], borderColor: colors[0], backgroundColor: 'rgba(52, 152, 219, 0.1)', tension: 0.4, fill: config.fill }]; + } else { + let i = 0; + Object.entries(data[config.dataKey]).forEach(([name, chartData]) => { + datasets.push({ label: name, data: chartData, borderColor: colors[i % colors.length], backgroundColor: colors[i % colors.length] + '20', tension: 0.4, fill: config.fill }); + i++; + }); + } + currentCharts[chartType] = new Chart(document.getElementById(config.id), { + type: 'line', + data: { labels: data.time_labels, datasets: datasets }, + options: { + responsive: true, + plugins: { title: { display: true, text: `${timeRange}内${config.title}趋势`, font: { size: 16 } }, legend: { display: chartType !== 'totalCost', position: 'top' } }, + scales: { x: { title: { display: true, text: '时间' }, ticks: { maxTicksLimit: 12 } }, y: { title: { display: true, text: config.yAxisLabel }, beginAtZero: true } }, + interaction: { intersect: false, mode: 'index' } + } + }); + } + + if (allChartData['24h']) { + updateAllCharts(allChartData['24h'], '24h'); + // Activate the 24h button by default + document.querySelectorAll('.time-range-btn').forEach(btn => { + if (btn.textContent.includes('24小时')) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + } + + // Static charts + const staticChartData = JSON.parse('{{ static_chart_data }}') +; + Object.keys(staticChartData).forEach(period_id => { + const providerCostData = staticChartData[period_id].provider_cost_data; + const modelCostData = staticChartData[period_id].model_cost_data; + const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e74c3c', '#9b59b6', '#1abc9c', '#34495e', '#e67e22']; + + // Provider Cost Pie Chart + const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`); + if (providerCtx && providerCostData && providerCostData.data.length > 0) { + new Chart(providerCtx, { + type: 'pie', + data: { + labels: providerCostData.labels, + datasets: [{ + label: '按供应商花费', + data: providerCostData.data, + backgroundColor: colors, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { display: true, text: '按供应商花费分布', font: { size: 16 } }, + legend: { position: 'top' } + } + } + }); + } + + // Model Cost Bar Chart + const modelCtx = document.getElementById(`modelCostBarChart_${period_id}`); + if (modelCtx && modelCostData && modelCostData.data.length > 0) { + new Chart(modelCtx, { + type: 'bar', + data: { + labels: modelCostData.labels, + datasets: [{ + label: '按模型花费', + data: modelCostData.data, + backgroundColor: colors, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { display: true, text: '按模型花费排行', font: { size: 16 } }, + legend: { display: false } + }, + scales: { + y: { beginAtZero: true, title: { display: true, text: '花费 (¥)' } } + } + } + }); + } + }); +}); \ No newline at end of file diff --git a/src/chat/utils/templates/static_charts.html b/src/chat/utils/templates/static_charts.html new file mode 100644 index 000000000..3fd37ad4e --- /dev/null +++ b/src/chat/utils/templates/static_charts.html @@ -0,0 +1,9 @@ +

数据总览

+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/chat/utils/templates/tab_content.html b/src/chat/utils/templates/tab_content.html new file mode 100644 index 000000000..130c20cff --- /dev/null +++ b/src/chat/utils/templates/tab_content.html @@ -0,0 +1,44 @@ +
+

+ 统计时段: + {{ start_time }} ~ {{ end_time }} +

+ {{ summary_cards }} + {{ static_charts }} + +

按模型分类统计

+ + + {{ model_rows }} +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + {{ provider_rows }} +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

+ + + + + {{ chat_rows }} +
联系人/群组名称消息数量
+
\ No newline at end of file From 7dc4be3555661e83c97295df0fc60c0fb92261ff Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 12:47:36 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat(report):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=8A=A5=E5=91=8A=E9=A1=B5=E9=9D=A2=E7=9A=84?= =?UTF-8?q?UI=E5=92=8C=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对生成的HTML统计报告进行了全面的视觉和结构重构,以提供更现代化、更具可读性的用户界面。 - 采用仪表盘式的双栏布局(主内容区和侧边栏),将图表移至侧边栏,使数据表格更聚焦。 - 全面更新了CSS样式,包括配色、字体、卡片、表格和标签页,提升了整体的美观度和用户体验。 - 引入了响应式设计,以更好地适配移动设备。 - 将默认输出文件名从 `maibot_statistics.html` 更改为 `mofox_bot_statistics.html` 以保持项目命名一致性。 --- docker-compose.yml | 2 +- src/chat/utils/statistic.py | 2 +- src/chat/utils/templates/report.css | 260 ++++++++++++---------- src/chat/utils/templates/tab_content.html | 80 ++++--- 4 files changed, 187 insertions(+), 157 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e7fe3cdb2..63ba59661 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: volumes: - ./docker-config/core/.env:/app/.env # 持久化env配置文件 - ./docker-config/core:/app/config # 持久化bot配置文件 - - ./data/core/maibot_statistics.html:/app/maibot_statistics.html #统计数据输出 + - ./data/core/mofox_bot_statistics.html:/app/mofox_bot_statistics.html #统计数据输出 - ./data/app:/app/data # 共享目录 - ./data/core/plugins:/app/plugins # 插件目录 - ./data/core/logs:/app/logs # 日志目录 diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index 08d2ed8be..ea89700c6 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -115,7 +115,7 @@ class StatisticOutputTask(AsyncTask): SEP_LINE = "-" * 84 - def __init__(self, record_file_path: str = "maibot_statistics.html"): + def __init__(self, record_file_path: str = "mofox_bot_statistics.html"): # 延迟300秒启动,运行间隔300秒 super().__init__(task_name="Statistics Data Output Task", wait_before_start=0, run_interval=300) diff --git a/src/chat/utils/templates/report.css b/src/chat/utils/templates/report.css index 1c6603e5d..05a9c3ceb 100644 --- a/src/chat/utils/templates/report.css +++ b/src/chat/utils/templates/report.css @@ -1,54 +1,159 @@ +/* General Body Styles */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px; - background-color: #f4f7f6; - color: #333; + background-color: #F8F9FA; /* Light grey background */ + color: #495057; /* Softer text color */ line-height: 1.6; } +/* Main Container */ .container { - max-width: 900px; + max-width: 1200px; margin: 20px auto; - background-color: #fff; - padding: 25px; + background-color: #FFFFFF; /* Pure white background */ + padding: 30px; border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + border: 1px solid #EAEAEA; } +/* Dashboard Layout */ +.dashboard-layout { + display: flex; + gap: 30px; +} + +.main-content { + flex: 65%; + min-width: 0; +} + +.sidebar-content { + flex: 35%; + min-width: 0; +} + +/* Responsive Design for Mobile */ +@media (max-width: 992px) { + .dashboard-layout { + flex-direction: column; + } + .main-content, .sidebar-content { + flex: 1; + } +} + +/* Typography */ h1, h2 { - color: #2c3e50; - border-bottom: 2px solid #3498db; + color: #212529; padding-bottom: 10px; margin-top: 0; } h1 { text-align: center; - font-size: 2em; + font-size: 2.2em; + margin-bottom: 20px; + color: #4A90E2; /* Main blue for title */ } h2 { font-size: 1.5em; - margin-top: 30px; -} - -p { - margin-bottom: 10px; + margin-top: 40px; + margin-bottom: 15px; + border-bottom: 2px solid #EAEAEA; } +/* Info Banners */ .info-item { - background-color: #ecf0f1; - padding: 8px 12px; - border-radius: 4px; - margin-bottom: 8px; + background-color: #E9ECEF; + padding: 10px 15px; + border-radius: 6px; + margin-bottom: 20px; font-size: 0.95em; + border: 1px solid #DEE2E6; } .info-item strong { - color: #2980b9; + color: #4A90E2; } +/* Tabs */ +.tabs { + border-bottom: 2px solid #DEE2E6; + display: flex; + margin-bottom: 20px; +} + +.tabs button { + background: none; + border: none; + outline: none; + padding: 14px 20px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 16px; + color: #6C757D; + border-bottom: 3px solid transparent; + margin-bottom: -2px; /* Align with container border */ +} + +.tabs button:hover { + color: #212529; +} + +.tabs button.active { + color: #4A90E2; + border-bottom-color: #4A90E2; +} + +.tab-content { + display: none; + padding-top: 10px; +} + +.tab-content.active { + display: block; +} + +/* Summary Cards */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 20px; + margin: 20px 0; +} + +.card { + background-color: #FFFFFF; + padding: 20px; + border-radius: 8px; + text-align: center; + border: 1px solid #EAEAEA; + transition: all 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 15px rgba(0,0,0,0.08); +} + +.card h3 { + margin: 0 0 10px; + font-size: 1em; + color: #6C757D; +} + +.card p { + margin: 0; + font-size: 1.8em; + font-weight: bold; + color: #212529; +} + +/* Tables */ table { width: 100%; border-collapse: collapse; @@ -57,121 +162,40 @@ table { } th, td { - border: 1px solid #ddd; - padding: 10px; + padding: 12px 15px; text-align: left; + border-bottom: 1px solid #EAEAEA; } th { - background-color: #3498db; + background-color: #4A90E2; color: white; font-weight: bold; + font-size: 0.95em; + text-transform: uppercase; + letter-spacing: 0.5px; } tr:nth-child(even) { - background-color: #f9f9f9; + background-color: #F8F9FA; } -.footer { - text-align: center; - margin-top: 30px; - font-size: 0.8em; - color: #7f8c8d; -} - -.tabs { - overflow: hidden; - background: #ecf0f1; - display: flex; -} - -.tabs button { - background: inherit; - border: none; - outline: none; - padding: 14px 16px; - cursor: pointer; - transition: 0.3s; - font-size: 16px; -} - -.tabs button:hover { - background-color: #d4dbdc; -} - -.tabs button.active { - background-color: #b3bbbd; -} - -.tab-content { - display: none; - padding: 20px; - background-color: #fff; - border: 1px solid #ccc; -} - -.tab-content.active { - display: block; -} - -.summary-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 15px; - margin: 20px 0; -} - -.card { - background-color: #ecf0f1; - padding: 15px; - border-radius: 5px; - text-align: center; -} - -.card h3 { - margin: 0 0 10px; - font-size: 1em; - color: #2c3e50; -} - -.card p { - margin: 0; - font-size: 1.2em; - font-weight: bold; - color: #34495e; -} - -.chart-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-top: 20px; +tr:hover { + background-color: #E9ECEF; } +/* Chart Container in Sidebar */ .chart-container { position: relative; - height: 40vh; + height: 300px; /* Adjust height as needed */ width: 100%; + margin-bottom: 20px; } -.time-range-btn { - background-color: #ecf0f1; - border: 1px solid #bdc3c7; - color: #2c3e50; - padding: 8px 16px; - margin: 0 5px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: all 0.3s ease; +/* Footer */ +.footer { + text-align: center; + margin-top: 40px; + font-size: 0.85em; + color: #6C757D; } - -.time-range-btn:hover { - background-color: #d5dbdb; -} - -.time-range-btn.active { - background-color: #3498db; - color: white; - border-color: #2980b9; -} \ No newline at end of file diff --git a/src/chat/utils/templates/tab_content.html b/src/chat/utils/templates/tab_content.html index 130c20cff..b6c49eaf1 100644 --- a/src/chat/utils/templates/tab_content.html +++ b/src/chat/utils/templates/tab_content.html @@ -1,44 +1,50 @@
-

- 统计时段: - {{ start_time }} ~ {{ end_time }} -

- {{ summary_cards }} - {{ static_charts }} +
+
+

+ 统计时段: + {{ start_time }} ~ {{ end_time }} +

+ {{ summary_cards }} -

按模型分类统计

- - - {{ model_rows }} -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+

按模型分类统计

+ + + {{ model_rows }} +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
-

按供应商分类统计

- - - {{ provider_rows }} -
供应商名称调用次数Token总量TPS每K Token成本累计花费
+

按供应商分类统计

+ + + {{ provider_rows }} +
供应商名称调用次数Token总量TPS每K Token成本累计花费
-

按模块分类统计

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

按模块分类统计

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

按请求类型分类统计

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

按请求类型分类统计

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

聊天消息统计

- - - - - {{ chat_rows }} -
联系人/群组名称消息数量
+

聊天消息统计

+ + + + + {{ chat_rows }} +
联系人/群组名称消息数量
+
+ +
\ No newline at end of file From e1622ca6be66effea1fa62c3645e7af8cd35993f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 12:51:42 +0800 Subject: [PATCH 08/11] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=BA=9F=E5=BC=83=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧的数据库配置模块 `src/common/database/config` 已被完全移除。该模块已被标记为废弃,其功能已统一整合到 `global_config` 中。 本次重构旨在: - 简化代码库,消除冗余和过时的文件。 - 统一配置管理,提高可维护性。 此外,对 `AttentionOptimizer` 中的类变量添加了 `ClassVar` 类型注解,以增强代码的清晰度和类型安全性。 --- mofox_bot_statistics.html | 833 ++++++++++++++++++ src/chat/utils/attention_optimizer.py | 8 +- src/common/database/config/__init__.py | 11 - .../database/config/old/database_config.py | 149 ---- 4 files changed, 837 insertions(+), 164 deletions(-) create mode 100644 mofox_bot_statistics.html delete mode 100644 src/common/database/config/__init__.py delete mode 100644 src/common/database/config/old/database_config.py diff --git a/mofox_bot_statistics.html b/mofox_bot_statistics.html new file mode 100644 index 000000000..0e32cb688 --- /dev/null +++ b/mofox_bot_statistics.html @@ -0,0 +1,833 @@ + + + + + + MoFox-Bot运行统计报告 + + + + +
+

MoFox-Bot运行统计报告

+

统计截止时间: 2025-11-13 12:50:57

+
+ + + + +
+
+
+
+

+ 统计时段: + 2025-11-06 12:50:57 ~ 2025-11-13 12:50:57 +

+ +
+
+

总花费

+

0.0000 ¥

+
+
+

总请求数

+

0

+
+
+

总Token数

+

0

+
+
+

总消息数

+

0

+
+
+

总在线时间

+

4小时31分钟40秒

+
+
+ + +

按模型分类统计

+ + + +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

+ + + + + +
联系人/群组名称消息数量
+
+ +
+
+
+
+
+

+ 统计时段: + 2025-11-12 12:50:57 ~ 2025-11-13 12:50:57 +

+ +
+
+

总花费

+

0.0000 ¥

+
+
+

总请求数

+

0

+
+
+

总Token数

+

0

+
+
+

总消息数

+

0

+
+
+

总在线时间

+

30分钟8秒

+
+
+ + +

按模型分类统计

+ + + +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

+ + + + + +
联系人/群组名称消息数量
+
+ +
+
+
+
+
+

+ 统计时段: + 2025-11-13 09:50:57 ~ 2025-11-13 12:50:57 +

+ +
+
+

总花费

+

0.0000 ¥

+
+
+

总请求数

+

0

+
+
+

总Token数

+

0

+
+
+

总消息数

+

0

+
+
+

总在线时间

+

17分钟39秒

+
+
+ + +

按模型分类统计

+ + + +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

+ + + + + +
联系人/群组名称消息数量
+
+ +
+
+
+
+
+

+ 统计时段: + 2025-11-13 11:50:57 ~ 2025-11-13 12:50:57 +

+ +
+
+

总花费

+

0.0000 ¥

+
+
+

总请求数

+

0

+
+
+

总Token数

+

0

+
+
+

总消息数

+

0

+
+
+

总在线时间

+

13分钟56秒

+
+
+ + +

按模型分类统计

+ + + +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
+ +

按供应商分类统计

+ + + +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

+ + + + + +
联系人/群组名称消息数量
+
+ +
+
+
+
+
+

+ 统计时段: + 2025-10-02 21:26:50 ~ 2025-11-13 12:50:57 +

+ +
+
+

总花费

+

2234.0000 ¥

+
+
+

总请求数

+

2234

+
+
+

总Token数

+

1218356

+
+
+

总消息数

+

994

+
+
+

总在线时间

+

4小时57分钟30秒

+
+
+ + +

按模型分类统计

+ + + + + + + + + + + + +
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
BAAI/bge-m311210482560.000.0000 ¥1121.0000 ¥78.608 秒
Qwen/Qwen2.5-VL-72B-Instruct500667000.000.0000 ¥50.0000 ¥366.725 秒
Qwen/Qwen3-14B700901910.000.0000 ¥70.0000 ¥31.441 秒
Qwen/Qwen3-30B-A3B45302819440.000.0000 ¥453.0000 ¥75.589 秒
Qwen/Qwen3-8B9066820.000.0000 ¥9.0000 ¥8.275 秒
deepseek-ai/DeepSeek-R1-Distill-Qwen-32B210118110.000.0000 ¥21.0000 ¥11.524 秒
deepseek-ai/DeepSeek-V3.1-Terminus42806232240.000.0000 ¥428.0000 ¥2324.821 秒
deepseek-ai/DeepSeek-V3.2-Exp3042670.000.0000 ¥3.0000 ¥80.927 秒
gemini-2.5-pro690681950.000.0000 ¥69.0000 ¥898.429 秒
moonshotai/Kimi-K2-Instruct100170860.000.0000 ¥10.0000 ¥35.644 秒
+ +

按供应商分类统计

+ + + +
供应商名称调用次数Token总量TPS每K Token成本累计花费
+ +

按模块分类统计

+ + + + + + + + + + + + + + + + + + + + + + + + +
模块名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
action372210218372210590372.0000 ¥61.562 秒28.224 秒
chat16323381493248716.0000 ¥347.109 秒1.013 秒
chat_stream_impression_update4250492634304.0000 ¥38.162 秒0.000 秒
embedding35631947031947356.0000 ¥71.454 秒12.687 秒
emoji238057744880123.0000 ¥248.367 秒3.224 秒
expressor57461351684630357.0000 ¥453.845 秒282.887 秒
image415260761695877641.0000 ¥268.468 秒31.588 秒
individuality1140231631.0000 ¥1.808 秒0.000 秒
interest_embedding76516309016309765.0000 ¥30.717 秒6.880 秒
interest_generation296531612812.0000 ¥50.171 秒0.000 秒
memory13917718231665208847139.0000 ¥1306.658 秒45.801 秒
monthly_plan274035310932.0000 ¥10.196 秒0.000 秒
mood12640413162142034126.0000 ¥585.337 秒298.293 秒
planner611576721008516775761.0000 ¥310.203 秒18.367 秒
plugin15624858312858261441156.0000 ¥576.318 秒112.317 秒
relationship_tracker199523212271.0000 ¥17.689 秒0.000 秒
schedule15836094611782115.0000 ¥388.476 秒0.000 秒
tool_executor708885813339019170.0000 ¥31.441 秒4.774 秒
unknown21443573761181121.0000 ¥11.524 秒3.068 秒
video_analysis63306274160476.0000 ¥18.737 秒3.772 秒
+ +

按请求类型分类统计

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求类型调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
action.judge372210218372210590372.0000 ¥61.562 秒28.224 秒
chat.replyer16323381493248716.0000 ¥347.109 秒1.013 秒
chat_stream_impression_update4250492634304.0000 ¥38.162 秒0.000 秒
embedding35631947031947356.0000 ¥71.454 秒12.687 秒
emoji238057744880123.0000 ¥248.367 秒3.224 秒
expressor.learner57461351684630357.0000 ¥453.845 秒282.887 秒
image415260761695877641.0000 ¥268.468 秒31.588 秒
individuality.compress1140231631.0000 ¥1.808 秒0.000 秒
interest_embedding76516309016309765.0000 ¥30.717 秒6.880 秒
interest_generation296531612812.0000 ¥50.171 秒0.000 秒
memory.extraction551150322268413771655.0000 ¥1287.156 秒0.000 秒
memory.query_planner724764572625490772.0000 ¥75.871 秒13.881 秒
memory.value_assessment121450517191622412.0000 ¥43.272 秒0.136 秒
monthly_plan274035310932.0000 ¥10.196 秒0.000 秒
mood12640413162142034126.0000 ¥585.337 秒298.293 秒
planner611576721008516775761.0000 ¥310.203 秒18.367 秒
plugin.generate15024168912847254536150.0000 ¥572.632 秒112.215 秒
plugin.set_emoji_like.select_emoji668941169056.0000 ¥3.686 秒0.102 秒
relationship_tracker199523212271.0000 ¥17.689 秒0.000 秒
schedule15836094611782115.0000 ¥388.476 秒0.000 秒
tool_executor708885813339019170.0000 ¥31.441 秒4.774 秒
unknown21443573761181121.0000 ¥11.524 秒3.068 秒
video_analysis63306274160476.0000 ¥18.737 秒3.772 秒
+ +

聊天消息统计

+ + + + + + + + + + +
联系人/群组名称消息数量
MaiCore答疑群(尊王攘夷)50
因为没有群了所以只能拿这个测试了331
墨狐狐🌟起源之地590
亚马逊雨林,一处任何人也找不到的角落(4
墨狐5
一闪14
+
+ +
+
+
+

数据图表

+
+ + + + + +
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/chat/utils/attention_optimizer.py b/src/chat/utils/attention_optimizer.py index 770f17da9..e8210a685 100644 --- a/src/chat/utils/attention_optimizer.py +++ b/src/chat/utils/attention_optimizer.py @@ -13,7 +13,7 @@ import hashlib import random import re -from typing import Any, Literal +from typing import Any, ClassVar, Literal from src.common.logger import get_logger from src.config.config import global_config @@ -26,7 +26,7 @@ class AttentionOptimizer: # 可交换的block组定义(组内block可以随机排序) # 每个组是一个列表,包含可以互换位置的block名称 - SWAPPABLE_BLOCK_GROUPS = [ + SWAPPABLE_BLOCK_GROUPS:ClassVar = [ # 用户相关信息组(记忆、关系、表达习惯) ["memory_block", "relation_info_block", "expression_habits_block"], # 上下文增强组(工具、知识、跨群) @@ -37,7 +37,7 @@ class AttentionOptimizer: # 语义等价的文本替换模板 # 格式: {原始文本: [替换选项1, 替换选项2, ...]} - SEMANTIC_VARIANTS = { + SEMANTIC_VARIANTS:ClassVar = { "当前时间": ["当前时间", "现在是", "此时此刻", "时间"], "最近的系统通知": ["最近的系统通知", "系统通知", "通知消息", "最新通知"], "聊天历史": ["聊天历史", "对话记录", "历史消息", "之前的对话"], @@ -125,7 +125,7 @@ class AttentionOptimizer: for group in self.SWAPPABLE_BLOCK_GROUPS: # 过滤出实际存在且非空的block existing_blocks = [ - block for block in group if block in context_data and context_data[block] + block for block in group if context_data.get(block) ] if len(existing_blocks) > 1: diff --git a/src/common/database/config/__init__.py b/src/common/database/config/__init__.py deleted file mode 100644 index 903651d74..000000000 --- a/src/common/database/config/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""数据库配置层 - -职责: -- 数据库配置现已集成到全局配置中 -- 通过 src.config.config.global_config.database 访问 -- 优化参数配置 - -注意:此模块已废弃,配置已迁移到 global_config -""" - -__all__ = [] diff --git a/src/common/database/config/old/database_config.py b/src/common/database/config/old/database_config.py deleted file mode 100644 index 71cc9824b..000000000 --- a/src/common/database/config/old/database_config.py +++ /dev/null @@ -1,149 +0,0 @@ -"""数据库配置管理 - -统一管理数据库连接配置 -""" - -import os -from dataclasses import dataclass -from typing import Any -from urllib.parse import quote_plus - -from src.common.logger import get_logger - -logger = get_logger("database_config") - - -@dataclass -class DatabaseConfig: - """数据库配置""" - - # 基础配置 - db_type: str # "sqlite" 或 "mysql" - url: str # 数据库连接URL - - # 引擎配置 - engine_kwargs: dict[str, Any] - - # SQLite特定配置 - sqlite_path: str | None = None - - # MySQL特定配置 - mysql_host: str | None = None - mysql_port: int | None = None - mysql_user: str | None = None - mysql_password: str | None = None - mysql_database: str | None = None - mysql_charset: str = "utf8mb4" - mysql_unix_socket: str | None = None - - -_database_config: DatabaseConfig | None = None - - -def get_database_config() -> DatabaseConfig: - """获取数据库配置 - - 从全局配置中读取数据库设置并构建配置对象 - """ - global _database_config - - if _database_config is not None: - return _database_config - - from src.config.config import global_config - - config = global_config.database - - # 构建数据库URL - if config.database_type == "mysql": - # MySQL配置 - encoded_user = quote_plus(config.mysql_user) - encoded_password = quote_plus(config.mysql_password) - - if config.mysql_unix_socket: - # Unix socket连接 - encoded_socket = quote_plus(config.mysql_unix_socket) - url = ( - f"mysql+aiomysql://{encoded_user}:{encoded_password}" - f"@/{config.mysql_database}" - f"?unix_socket={encoded_socket}&charset={config.mysql_charset}" - ) - else: - # TCP连接 - url = ( - f"mysql+aiomysql://{encoded_user}:{encoded_password}" - f"@{config.mysql_host}:{config.mysql_port}/{config.mysql_database}" - f"?charset={config.mysql_charset}" - ) - - engine_kwargs = { - "echo": False, - "future": True, - "pool_size": config.connection_pool_size, - "max_overflow": config.connection_pool_size * 2, - "pool_timeout": config.connection_timeout, - "pool_recycle": 3600, - "pool_pre_ping": True, - "connect_args": { - "autocommit": config.mysql_autocommit, - "charset": config.mysql_charset, - "connect_timeout": config.connection_timeout, - }, - } - - _database_config = DatabaseConfig( - db_type="mysql", - url=url, - engine_kwargs=engine_kwargs, - mysql_host=config.mysql_host, - mysql_port=config.mysql_port, - mysql_user=config.mysql_user, - mysql_password=config.mysql_password, - mysql_database=config.mysql_database, - mysql_charset=config.mysql_charset, - mysql_unix_socket=config.mysql_unix_socket, - ) - - logger.info( - f"MySQL配置已加载: " - f"{config.mysql_user}@{config.mysql_host}:{config.mysql_port}/{config.mysql_database}" - ) - - else: - # SQLite配置 - if not os.path.isabs(config.sqlite_path): - ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) - db_path = os.path.join(ROOT_PATH, config.sqlite_path) - else: - db_path = config.sqlite_path - - # 确保数据库目录存在 - os.makedirs(os.path.dirname(db_path), exist_ok=True) - - url = f"sqlite+aiosqlite:///{db_path}" - - engine_kwargs = { - "echo": False, - "future": True, - "connect_args": { - "check_same_thread": False, - "timeout": 60, - }, - } - - _database_config = DatabaseConfig( - db_type="sqlite", - url=url, - engine_kwargs=engine_kwargs, - sqlite_path=db_path, - ) - - logger.info(f"SQLite配置已加载: {db_path}") - - return _database_config - - -def reset_database_config(): - """重置数据库配置(用于测试)""" - global _database_config - _database_config = None From df2e6eabdc5975e7e34c9eddac16eb27e9e57dcf Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 13:13:43 +0800 Subject: [PATCH 09/11] add igorne --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b3bea392c..5b55012bd 100644 --- a/.gitignore +++ b/.gitignore @@ -344,3 +344,4 @@ package-lock.json package.json src/chat/planner_actions/新建 文本文档.txt /backup +mofox_bot_statistics.html From a9f4c422b1846a5dde487e3ae63a31e70243b350 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 13:13:53 +0800 Subject: [PATCH 10/11] 1 --- mofox_bot_statistics.html | 833 -------------------------------------- 1 file changed, 833 deletions(-) delete mode 100644 mofox_bot_statistics.html diff --git a/mofox_bot_statistics.html b/mofox_bot_statistics.html deleted file mode 100644 index 0e32cb688..000000000 --- a/mofox_bot_statistics.html +++ /dev/null @@ -1,833 +0,0 @@ - - - - - - MoFox-Bot运行统计报告 - - - - -
-

MoFox-Bot运行统计报告

-

统计截止时间: 2025-11-13 12:50:57

-
- - - - -
-
-
-
-

- 统计时段: - 2025-11-06 12:50:57 ~ 2025-11-13 12:50:57 -

- -
-
-

总花费

-

0.0000 ¥

-
-
-

总请求数

-

0

-
-
-

总Token数

-

0

-
-
-

总消息数

-

0

-
-
-

总在线时间

-

4小时31分钟40秒

-
-
- - -

按模型分类统计

- - - -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

- - - - - -
联系人/群组名称消息数量
-
- -
-
-
-
-
-

- 统计时段: - 2025-11-12 12:50:57 ~ 2025-11-13 12:50:57 -

- -
-
-

总花费

-

0.0000 ¥

-
-
-

总请求数

-

0

-
-
-

总Token数

-

0

-
-
-

总消息数

-

0

-
-
-

总在线时间

-

30分钟8秒

-
-
- - -

按模型分类统计

- - - -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

- - - - - -
联系人/群组名称消息数量
-
- -
-
-
-
-
-

- 统计时段: - 2025-11-13 09:50:57 ~ 2025-11-13 12:50:57 -

- -
-
-

总花费

-

0.0000 ¥

-
-
-

总请求数

-

0

-
-
-

总Token数

-

0

-
-
-

总消息数

-

0

-
-
-

总在线时间

-

17分钟39秒

-
-
- - -

按模型分类统计

- - - -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

- - - - - -
联系人/群组名称消息数量
-
- -
-
-
-
-
-

- 统计时段: - 2025-11-13 11:50:57 ~ 2025-11-13 12:50:57 -

- -
-
-

总花费

-

0.0000 ¥

-
-
-

总请求数

-

0

-
-
-

总Token数

-

0

-
-
-

总消息数

-

0

-
-
-

总在线时间

-

13分钟56秒

-
-
- - -

按模型分类统计

- - - -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
- -

按供应商分类统计

- - - -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

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

按请求类型分类统计

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

聊天消息统计

- - - - - -
联系人/群组名称消息数量
-
- -
-
-
-
-
-

- 统计时段: - 2025-10-02 21:26:50 ~ 2025-11-13 12:50:57 -

- -
-
-

总花费

-

2234.0000 ¥

-
-
-

总请求数

-

2234

-
-
-

总Token数

-

1218356

-
-
-

总消息数

-

994

-
-
-

总在线时间

-

4小时57分钟30秒

-
-
- - -

按模型分类统计

- - - - - - - - - - - - -
模型名称调用次数平均Token数Token总量TPS每K Token成本累计花费平均耗时(秒)
BAAI/bge-m311210482560.000.0000 ¥1121.0000 ¥78.608 秒
Qwen/Qwen2.5-VL-72B-Instruct500667000.000.0000 ¥50.0000 ¥366.725 秒
Qwen/Qwen3-14B700901910.000.0000 ¥70.0000 ¥31.441 秒
Qwen/Qwen3-30B-A3B45302819440.000.0000 ¥453.0000 ¥75.589 秒
Qwen/Qwen3-8B9066820.000.0000 ¥9.0000 ¥8.275 秒
deepseek-ai/DeepSeek-R1-Distill-Qwen-32B210118110.000.0000 ¥21.0000 ¥11.524 秒
deepseek-ai/DeepSeek-V3.1-Terminus42806232240.000.0000 ¥428.0000 ¥2324.821 秒
deepseek-ai/DeepSeek-V3.2-Exp3042670.000.0000 ¥3.0000 ¥80.927 秒
gemini-2.5-pro690681950.000.0000 ¥69.0000 ¥898.429 秒
moonshotai/Kimi-K2-Instruct100170860.000.0000 ¥10.0000 ¥35.644 秒
- -

按供应商分类统计

- - - -
供应商名称调用次数Token总量TPS每K Token成本累计花费
- -

按模块分类统计

- - - - - - - - - - - - - - - - - - - - - - - - -
模块名称调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
action372210218372210590372.0000 ¥61.562 秒28.224 秒
chat16323381493248716.0000 ¥347.109 秒1.013 秒
chat_stream_impression_update4250492634304.0000 ¥38.162 秒0.000 秒
embedding35631947031947356.0000 ¥71.454 秒12.687 秒
emoji238057744880123.0000 ¥248.367 秒3.224 秒
expressor57461351684630357.0000 ¥453.845 秒282.887 秒
image415260761695877641.0000 ¥268.468 秒31.588 秒
individuality1140231631.0000 ¥1.808 秒0.000 秒
interest_embedding76516309016309765.0000 ¥30.717 秒6.880 秒
interest_generation296531612812.0000 ¥50.171 秒0.000 秒
memory13917718231665208847139.0000 ¥1306.658 秒45.801 秒
monthly_plan274035310932.0000 ¥10.196 秒0.000 秒
mood12640413162142034126.0000 ¥585.337 秒298.293 秒
planner611576721008516775761.0000 ¥310.203 秒18.367 秒
plugin15624858312858261441156.0000 ¥576.318 秒112.317 秒
relationship_tracker199523212271.0000 ¥17.689 秒0.000 秒
schedule15836094611782115.0000 ¥388.476 秒0.000 秒
tool_executor708885813339019170.0000 ¥31.441 秒4.774 秒
unknown21443573761181121.0000 ¥11.524 秒3.068 秒
video_analysis63306274160476.0000 ¥18.737 秒3.772 秒
- -

按请求类型分类统计

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
请求类型调用次数输入Token输出TokenToken总量累计花费平均耗时(秒)标准差(秒)
action.judge372210218372210590372.0000 ¥61.562 秒28.224 秒
chat.replyer16323381493248716.0000 ¥347.109 秒1.013 秒
chat_stream_impression_update4250492634304.0000 ¥38.162 秒0.000 秒
embedding35631947031947356.0000 ¥71.454 秒12.687 秒
emoji238057744880123.0000 ¥248.367 秒3.224 秒
expressor.learner57461351684630357.0000 ¥453.845 秒282.887 秒
image415260761695877641.0000 ¥268.468 秒31.588 秒
individuality.compress1140231631.0000 ¥1.808 秒0.000 秒
interest_embedding76516309016309765.0000 ¥30.717 秒6.880 秒
interest_generation296531612812.0000 ¥50.171 秒0.000 秒
memory.extraction551150322268413771655.0000 ¥1287.156 秒0.000 秒
memory.query_planner724764572625490772.0000 ¥75.871 秒13.881 秒
memory.value_assessment121450517191622412.0000 ¥43.272 秒0.136 秒
monthly_plan274035310932.0000 ¥10.196 秒0.000 秒
mood12640413162142034126.0000 ¥585.337 秒298.293 秒
planner611576721008516775761.0000 ¥310.203 秒18.367 秒
plugin.generate15024168912847254536150.0000 ¥572.632 秒112.215 秒
plugin.set_emoji_like.select_emoji668941169056.0000 ¥3.686 秒0.102 秒
relationship_tracker199523212271.0000 ¥17.689 秒0.000 秒
schedule15836094611782115.0000 ¥388.476 秒0.000 秒
tool_executor708885813339019170.0000 ¥31.441 秒4.774 秒
unknown21443573761181121.0000 ¥11.524 秒3.068 秒
video_analysis63306274160476.0000 ¥18.737 秒3.772 秒
- -

聊天消息统计

- - - - - - - - - - -
联系人/群组名称消息数量
MaiCore答疑群(尊王攘夷)50
因为没有群了所以只能拿这个测试了331
墨狐狐🌟起源之地590
亚马逊雨林,一处任何人也找不到的角落(4
墨狐5
一闪14
-
- -
-
-
-

数据图表

-
- - - - - -
-
-
-
-
-
-
-
-
- - - \ No newline at end of file From 992a1129ce83d1c14d0e9a2527f1dcf2a2c683dc Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 13 Nov 2025 14:31:54 +0800 Subject: [PATCH 11/11] =?UTF-8?q?refactor(report):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E6=A0=B7=E5=BC=8F=E5=92=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整了报告页面的CSS样式,包括颜色、阴影和布局,以提供更专业、现代的视觉效果。 - 改进了从后端向前端JavaScript传递图表数据的方式。现在通过一个独立的` \ No newline at end of file diff --git a/src/chat/utils/templates/report.js b/src/chat/utils/templates/report.js index 7f944cb3a..8cec90b56 100644 --- a/src/chat/utils/templates/report.js +++ b/src/chat/utils/templates/report.js @@ -11,9 +11,15 @@ function showTab(evt, tabName) { } document.addEventListener('DOMContentLoaded', function () { - // This is a placeholder for chart data which will be injected by python. - const allChartData = JSON.parse('{{ all_chart_data }}') -; + // Chart data is injected by python via the HTML template. + let allChartData = {}; + try { + allChartData = JSON.parse(all_chart_data_json_string); + } catch (e) { + console.error("Failed to parse all_chart_data:", e); + console.error("Problematic all_chart_data string:", all_chart_data_json_string); + } + let currentCharts = {}; const chartConfigs = { totalCost: { id: 'totalCostChart', title: '总花费', yAxisLabel: '花费 (¥)', dataKey: 'total_cost_data', fill: true }, @@ -73,8 +79,14 @@ document.addEventListener('DOMContentLoaded', function () { } // Static charts - const staticChartData = JSON.parse('{{ static_chart_data }}') -; + let staticChartData = {}; + try { + staticChartData = JSON.parse(static_chart_data_json_string); + } catch (e) { + console.error("Failed to parse static_chart_data:", e); + console.error("Problematic static_chart_data string:", static_chart_data_json_string); + } + Object.keys(staticChartData).forEach(period_id => { const providerCostData = staticChartData[period_id].provider_cost_data; const modelCostData = staticChartData[period_id].model_cost_data; @@ -82,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function () { // Provider Cost Pie Chart const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`); - if (providerCtx && providerCostData && providerCostData.data.length > 0) { + if (providerCtx && providerCostData && providerCostData.data && providerCostData.data.length > 0) { new Chart(providerCtx, { type: 'pie', data: { @@ -106,7 +118,7 @@ document.addEventListener('DOMContentLoaded', function () { // Model Cost Bar Chart const modelCtx = document.getElementById(`modelCostBarChart_${period_id}`); - if (modelCtx && modelCostData && modelCostData.data.length > 0) { + if (modelCtx && modelCostData && modelCostData.data && modelCostData.data.length > 0) { new Chart(modelCtx, { type: 'bar', data: {