Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -344,3 +344,4 @@ package-lock.json
|
||||
package.json
|
||||
src/chat/planner_actions/新建 文本文档.txt
|
||||
/backup
|
||||
mofox_bot_statistics.html
|
||||
|
||||
@@ -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 # 日志目录
|
||||
|
||||
@@ -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 "",
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 增加饼图和条形图
|
||||
# static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现
|
||||
static_charts = ""
|
||||
return f"""
|
||||
<div id="{div_id}" class="tab-content">
|
||||
<p class="info-item">
|
||||
<strong>统计时段: </strong>
|
||||
{start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
|
||||
</p>
|
||||
{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,
|
||||
)
|
||||
|
||||
<h2>按模型分类统计</h2>
|
||||
<table>
|
||||
<tr><th>模型名称</th><th>调用次数</th><th>平均Token数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th><th>平均耗时(秒)</th></tr>
|
||||
<tbody>{model_rows}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按供应商分类统计</h2>
|
||||
<table>
|
||||
<tr><th>供应商名称</th><th>调用次数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th></tr>
|
||||
<tbody>{provider_rows}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按模块分类统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>模块名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th><th>平均耗时(秒)</th><th>标准差(秒)</th></tr>
|
||||
</thead>
|
||||
<tbody>{module_rows}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按请求类型分类统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>请求类型</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th><th>平均耗时(秒)</th><th>标准差(秒)</th></tr>
|
||||
</thead>
|
||||
<tbody>{type_rows}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>聊天消息统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>联系人/群组名称</th><th>消息数量</th></tr>
|
||||
</thead>
|
||||
<tbody>{chat_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
def _generate_chart_tab(self, chart_data: dict) -> str:
|
||||
"""生成图表选项卡的HTML内容。"""
|
||||
return f"""
|
||||
<div id="charts" class="tab-content">
|
||||
<h2>数据图表</h2>
|
||||
<div style="margin: 20px 0; text-align: center;">
|
||||
<label style="margin-right: 10px; font-weight: bold;">时间范围:</label>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('6h')">6小时</button>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('12h')">12小时</button>
|
||||
<button class="time-range-btn active" onclick="switchTimeRange('24h')">24小时</button>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('48h')">48小时</button>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div style="margin-bottom: 40px;"><canvas id="totalCostChart" width="800" height="400"></canvas></div>
|
||||
<div style="margin-bottom: 40px;"><canvas id="costByModuleChart" width="800" height="400"></canvas></div>
|
||||
<div style="margin-bottom: 40px;"><canvas id="costByModelChart" width="800" height="400"></canvas></div>
|
||||
<div><canvas id="messageByChatChart" width="800" height="400"></canvas></div>
|
||||
</div>
|
||||
<style>
|
||||
.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; }}
|
||||
</style>
|
||||
<script>
|
||||
const allChartData = {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 }}
|
||||
}};
|
||||
function switchTimeRange(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' }}
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
document.addEventListener('DOMContentLoaded', function() {{
|
||||
if (allChartData['24h']) {{
|
||||
updateAllCharts(allChartData['24h'], '24h');
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</div>
|
||||
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'<button class="tab-link" onclick="showTab(event, \'{period[0]}\')">{period[2]}</button>'
|
||||
for period in self.stat_period
|
||||
]
|
||||
tab_list.append('<button class="tab-link" onclick="showTab(event, \'charts\')">数据图表</button>')
|
||||
tab_list_html.append('<button class="tab-link" onclick="showTab(event, \'charts\')">数据图表</button>')
|
||||
|
||||
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"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MoFox-Bot运行统计报告</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
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; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>MoFox-Bot运行统计报告</h1>
|
||||
<p class="info-item"><strong>统计截止时间:</strong> {now.strftime("%Y-%m-%d %H:%M:%S")}</p>
|
||||
<div class="tabs">{joined_tab_list}</div>
|
||||
{joined_tab_content}
|
||||
</div>
|
||||
<script>
|
||||
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");
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
async with aiofiles.open(output_path, "w", encoding="utf-8") as f:
|
||||
await f.write(html_template)
|
||||
await f.write(rendered_html)
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
16
src/chat/utils/templates/charts_tab.html
Normal file
16
src/chat/utils/templates/charts_tab.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div id="charts" class="tab-content">
|
||||
<h2>数据图表</h2>
|
||||
<div style="margin: 20px 0; text-align: center;">
|
||||
<label style="margin-right: 10px; font-weight: bold;">时间范围:</label>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('6h')">6小时</button>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('12h')">12小时</button>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('24h')">24小时</button>
|
||||
<button class="time-range-btn" onclick="switchTimeRange('48h')">48小时</button>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div style="margin-bottom: 40px;"><canvas id="totalCostChart" width="800" height="400"></canvas></div>
|
||||
<div style="margin-bottom: 40px;"><canvas id="costByModuleChart" width="800" height="400"></canvas></div>
|
||||
<div style="margin-bottom: 40px;"><canvas id="costByModelChart" width="800" height="400"></canvas></div>
|
||||
<div><canvas id="messageByChatChart" width="800" height="400"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
199
src/chat/utils/templates/report.css
Normal file
199
src/chat/utils/templates/report.css
Normal file
@@ -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;
|
||||
}
|
||||
23
src/chat/utils/templates/report.html
Normal file
23
src/chat/utils/templates/report.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ report_title }}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>{{ report_css }}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{{ report_title }}</h1>
|
||||
<p class="info-item"><strong>统计截止时间:</strong> {{ generation_time }}</p>
|
||||
<div class="tabs">{{ tab_list }}</div>
|
||||
{{ tab_content }}
|
||||
</div>
|
||||
<script>
|
||||
const all_chart_data_json_string = `{{ all_chart_data|safe }}`;
|
||||
const static_chart_data_json_string = `{{ static_chart_data|safe }}`;
|
||||
</script>
|
||||
<script>{{ report_js }}</script>
|
||||
</body>
|
||||
</html>
|
||||
146
src/chat/utils/templates/report.js
Normal file
146
src/chat/utils/templates/report.js
Normal file
@@ -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: '花费 (¥)' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
9
src/chat/utils/templates/static_charts.html
Normal file
9
src/chat/utils/templates/static_charts.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<h2>数据总览</h2>
|
||||
<div class="chart-grid">
|
||||
<div class="chart-container">
|
||||
<canvas id="providerCostPieChart_{{ period_id }}"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="modelCostBarChart_{{ period_id }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
50
src/chat/utils/templates/tab_content.html
Normal file
50
src/chat/utils/templates/tab_content.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<div id="{{ div_id }}" class="tab-content">
|
||||
<div class="dashboard-layout">
|
||||
<div class="main-content">
|
||||
<p class="info-item">
|
||||
<strong>统计时段: </strong>
|
||||
{{ start_time }} ~ {{ end_time }}
|
||||
</p>
|
||||
{{ summary_cards }}
|
||||
|
||||
<h2>按模型分类统计</h2>
|
||||
<table>
|
||||
<tr><th>模型名称</th><th>调用次数</th><th>平均Token数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th><th>平均耗时(秒)</th></tr>
|
||||
<tbody>{{ model_rows }}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按供应商分类统计</h2>
|
||||
<table>
|
||||
<tr><th>供应商名称</th><th>调用次数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th></tr>
|
||||
<tbody>{{ provider_rows }}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按模块分类统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>模块名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th><th>平均耗时(秒)</th><th>标准差(秒)</th></tr>
|
||||
</thead>
|
||||
<tbody>{{ module_rows }}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>按请求类型分类统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>请求类型</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th><th>平均耗时(秒)</th><th>标准差(秒)</th></tr>
|
||||
</thead>
|
||||
<tbody>{{ type_rows }}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>聊天消息统计</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>联系人/群组名称</th><th>消息数量</th></tr>
|
||||
</thead>
|
||||
<tbody>{{ chat_rows }}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
{{ static_charts }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -930,12 +930,11 @@ def filter_system_format_content(content: str | None) -> str:
|
||||
过滤系统格式化内容,移除回复、@、图片、表情包等系统生成的格式文本
|
||||
|
||||
此方法过滤以下类型的系统格式化内容:
|
||||
1. 回复格式:[回复xxx],说:xxx
|
||||
1. 回复格式:[回复xxx],说:xxx (包括深度嵌套)
|
||||
2. 表情包格式:[表情包:xxx]
|
||||
3. 图片格式:[图片:xxx]
|
||||
4. @格式:@<xxx>
|
||||
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:数字>],说: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. 移除@格式:@<xxx>
|
||||
# 移除@格式:@<xxx>
|
||||
cleaned_content = re.sub(r"@<[^>]*>", "", cleaned_content)
|
||||
|
||||
# 6. 移除其他可能的系统格式
|
||||
# [表情包(描述生成失败)] 等错误格式
|
||||
cleaned_content = re.sub(r"\[表情包\([^)]*\)\]", "", cleaned_content)
|
||||
# [图片(描述生成失败)] 等错误格式
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""数据库配置层
|
||||
|
||||
职责:
|
||||
- 数据库配置现已集成到全局配置中
|
||||
- 通过 src.config.config.global_config.database 访问
|
||||
- 优化参数配置
|
||||
|
||||
注意:此模块已废弃,配置已迁移到 global_config
|
||||
"""
|
||||
|
||||
__all__ = []
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user