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
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/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/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/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
diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py
index 5be2fd19a..e23a1d75e 100644
--- a/src/chat/utils/report_generator.py
+++ b/src/chat/utils/report_generator.py
@@ -1,10 +1,14 @@
"""
该模块用于生成HTML格式的统计报告。
"""
+
+import json
+import os
from datetime import datetime, timedelta
from typing import Any
import aiofiles
+from jinja2 import Environment, FileSystemLoader
from .statistic_keys import * # noqa: F403
@@ -48,6 +52,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,135 +159,37 @@ class HTMLReportGenerator:
"""
-
# 增加饼图和条形图
- # static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现
- static_charts = ""
- 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,
+ )
-
按模型分类统计
-
- 模型名称 调用次数 平均Token数 Token总量 TPS 每K Token成本 累计花费 平均耗时(秒)
- {model_rows}
-
-
-
按供应商分类统计
-
- 供应商名称 调用次数 Token总量 TPS 每K Token成本 累计花费
- {provider_rows}
-
-
-
按模块分类统计
-
-
- 模块名称 调用次数 输入Token 输出Token Token总量 累计花费 平均耗时(秒) 标准差(秒)
-
- {module_rows}
-
-
-
按请求类型分类统计
-
-
- 请求类型 调用次数 输入Token 输出Token Token总量 累计花费 平均耗时(秒) 标准差(秒)
-
- {type_rows}
-
-
-
聊天消息统计
-
-
- 联系人/群组名称 消息数量
-
- {chat_rows}
-
-
- """
def _generate_chart_tab(self, chart_data: dict) -> str:
"""生成图表选项卡的HTML内容。"""
- return f"""
-
-
数据图表
-
- 时间范围:
- 6小时
- 12小时
- 24小时
- 48小时
-
-
-
-
-
+ 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):
"""
@@ -291,77 +200,50 @@ class HTMLReportGenerator:
:param now: 当前时间。
:param output_path: 输出文件路径。
"""
- tab_list = [
+ tab_list_html = [
f'{period[2]} '
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)
+ await f.write(rendered_html)
diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py
index 08d2ed8be..5b1ce7826 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)
@@ -412,9 +412,9 @@ class StatisticOutputTask(AsyncTask):
(REQ_CNT_BY_MODEL, "model"),
(REQ_CNT_BY_MODULE, "module"),
]:
- time_cost_key = f"time_costs_by_{items}"
- avg_key = f"avg_time_costs_by_{items}"
- std_key = f"std_time_costs_by_{items}"
+ time_cost_key = f"TIME_COST_BY_{items.upper()}"
+ avg_key = f"AVG_TIME_COST_BY_{items.upper()}"
+ std_key = f"STD_TIME_COST_BY_{items.upper()}"
for item_name in period_stats[category_key]:
time_costs = period_stats[time_cost_key].get(item_name, [])
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 @@
+
+
数据图表
+
+ 时间范围:
+ 6小时
+ 12小时
+ 24小时
+ 48小时
+
+
+
\ 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..2229be785
--- /dev/null
+++ b/src/chat/utils/templates/report.css
@@ -0,0 +1,199 @@
+/* General Body Styles */
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ margin: 0;
+ padding: 20px;
+ background-color: #f0f4f8; /* Light blue-gray background */
+ color: #333; /* Darker text for better contrast */
+ line-height: 1.6;
+}
+
+/* Main Container */
+.container {
+ max-width: 95%; /* Make container almost full-width */
+ margin: 20px auto;
+ background-color: #FFFFFF; /* Pure white background */
+ padding: 30px;
+ border-radius: 12px; /* Slightly more rounded corners */
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.07); /* Softer, deeper shadow */
+}
+/* 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: #212529;
+ padding-bottom: 10px;
+ margin-top: 0;
+}
+
+h1 {
+ text-align: center;
+ font-size: 2.2em;
+ margin-bottom: 20px;
+ color: #2A6CB5; /* A deeper, more professional blue */
+}
+
+h2 {
+ font-size: 1.5em;
+ margin-top: 40px;
+ margin-bottom: 15px;
+ border-bottom: 2px solid #DDE6ED; /* Lighter border color */
+}
+
+/* Info Banners */
+.info-item {
+ background-color: #E9F2FA; /* Light blue background */
+ padding: 12px 18px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ font-size: 0.95em;
+ border: 1px solid #D1E3F4; /* Light blue border */
+}
+
+.info-item strong {
+ color: #2A6CB5; /* Deeper blue for emphasis */
+}
+
+/* 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: #2A6CB5;
+ background-color: #f0f4f8; /* Subtle hover background */
+}
+
+.tabs button.active {
+ color: #2A6CB5; /* Active tab color */
+ border-bottom-color: #2A6CB5; /* Active tab border color */
+}
+
+.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 #DDE6ED; /* Lighter border */
+ 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;
+ margin-top: 15px;
+ font-size: 0.9em;
+}
+
+th, td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #EAEAEA;
+}
+th {
+ background-color: #4A90E2; /* Main theme blue */
+ color: white;
+ font-weight: bold;
+ font-size: 0.95em;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+tr:nth-child(even) {
+ background-color: #F7FAFC; /* Very light blue for alternate rows */
+}
+
+tr:hover {
+ background-color: #E9F2FA; /* Light blue for hover */
+}
+
+/* Chart Container in Sidebar */
+.chart-container {
+ position: relative;
+ height: 300px; /* Adjust height as needed */
+ width: 100%;
+ margin-bottom: 20px;
+}
+
+/* Footer */
+.footer {
+ text-align: center;
+ margin-top: 40px;
+ font-size: 0.85em;
+ color: #6C757D;
+}
diff --git a/src/chat/utils/templates/report.html b/src/chat/utils/templates/report.html
new file mode 100644
index 000000000..b70146063
--- /dev/null
+++ b/src/chat/utils/templates/report.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ {{ 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..8cec90b56
--- /dev/null
+++ b/src/chat/utils/templates/report.js
@@ -0,0 +1,146 @@
+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 () {
+ // 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 },
+ 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
+ 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;
+ 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 && 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 && 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..b6c49eaf1
--- /dev/null
+++ b/src/chat/utils/templates/tab_content.html
@@ -0,0 +1,50 @@
+
+
+
+
+ 统计时段:
+ {{ start_time }} ~ {{ end_time }}
+
+ {{ summary_cards }}
+
+
按模型分类统计
+
+ 模型名称 调用次数 平均Token数 Token总量 TPS 每K Token成本 累计花费 平均耗时(秒)
+ {{ model_rows }}
+
+
+
按供应商分类统计
+
+ 供应商名称 调用次数 Token总量 TPS 每K Token成本 累计花费
+ {{ provider_rows }}
+
+
+
按模块分类统计
+
+
+ 模块名称 调用次数 输入Token 输出Token Token总量 累计花费 平均耗时(秒) 标准差(秒)
+
+ {{ module_rows }}
+
+
+
按请求类型分类统计
+
+
+ 请求类型 调用次数 输入Token 输出Token Token总量 累计花费 平均耗时(秒) 标准差(秒)
+
+ {{ type_rows }}
+
+
+
聊天消息统计
+
+
+ 联系人/群组名称 消息数量
+
+ {{ chat_rows }}
+
+
+
+
+
\ No newline at end of file
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)
# [图片(描述生成失败)] 等错误格式
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