Skip to content

Commit e1a3d64

Browse files
committed
0.3.0
1 parent 5d508a1 commit e1a3d64

9 files changed

Lines changed: 449 additions & 241 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414

1515
## 📝 更新记录
1616

17-
### 2025-08-01 v0.2.6 没有更新,庆祝一下无严重bug持续一整周。后台收发消息量达到 100w/6.5w
17+
### 2025-08-04 v0.3.0 修复了前端数据统计和导出和所有已知bug。
1818

1919
<details>
2020
<summary>点此展开</summary>
2121

22+
2025-08-01 v0.2.6 没有更新,庆祝一下无严重bug持续一整周。后台收发消息量达到 100w/6.5w
23+
2224
2025-07-29 v0.2.5 版本支持到python3.10,取消依赖py3.12的高级字符串解析
2325

2426
2025-07-29 v0.2.4 优化了自动初始化环境,已在windows和root环境的vps下测试

‎README_API.md‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,6 @@ BotShepherd Web API 提供了完整的系统管理功能,包括连接管理、
456456
{"time": "2023-01-01 00:00", "count": 10},
457457
{"time": "2023-01-01 03:00", "count": 15}
458458
],
459-
"message_types": {
460-
"text": 800,
461-
"image": 150,
462-
"voice": 50
463-
},
464459
"top_groups": [
465460
{
466461
"group_id": "789012",

‎app/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
BotShepherd应用核心模块
33
"""
44

5-
__version__ = "0.2.6" # 准备快进到 1.0.0
5+
__version__ = "0.3.0" # 准备快进到 1.0.0
66
__author__ = "Loping151"
77
__github__ = "https://github.com/Loping151/BotShepherd"
88
__description__ = "星星花与牧羊人 - 多框架统一管理平台"

‎app/commands/builtin_commands/help_command.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ async def _show_command_help(self, command_name: str, event: Event, permission_m
5858
# 检查用户是否有权限查看此指令
5959
user_level = permission_manager.get_user_permission_level(event)
6060
if user_level.value < command.required_permission.value:
61-
return self.format_error(f"您没有权限查看指令 {command_name} 的帮助")
61+
# return self.format_error(f"您没有权限查看指令 {command_name} 的帮助")
62+
return # 不提示
6263

6364
help_text = command.get_help()
6465

‎app/config/config_manager.py‎

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -709,53 +709,6 @@ def validate_connection_config(self, config: Dict[str, Any]) -> bool:
709709

710710
return True
711711

712-
# def validate_global_config(self, config: Dict[str, Any]) -> bool:
713-
# """验证全局配置"""
714-
# required_fields = ["superusers", "command_prefix"]
715-
716-
# for field in required_fields:
717-
# if field not in config:
718-
# return False
719-
720-
# # 验证超级用户列表
721-
# if not isinstance(config["superusers"], list) or len(config["superusers"]) == 0:
722-
# return False
723-
724-
# # 验证指令前缀
725-
# if not isinstance(config["command_prefix"], str) or len(config["command_prefix"]) == 0:
726-
# return False
727-
728-
# return True
729-
730-
# 配置备份和恢复
731-
# async def create_config_backup(self, backup_name: Optional[str] = None) -> str:
732-
# """创建配置备份"""
733-
# if not self._config_backup:
734-
# raise RuntimeError("配置备份管理器未初始化")
735-
736-
# backup_path = await self._config_backup.create_backup(backup_name)
737-
# return str(backup_path)
738-
739-
# async def restore_config_backup(self, backup_name: str):
740-
# """恢复配置备份"""
741-
# if not self._config_backup:
742-
# raise RuntimeError("配置备份管理器未初始化")
743-
744-
# await self._config_backup.restore_backup(backup_name)
745-
# # 重新加载所有配置
746-
# await self._load_all_configs()
747-
748-
# def list_config_backups(self) -> List[Dict[str, Any]]:
749-
# """列出所有配置备份"""
750-
# if not self._config_backup:
751-
# return []
752-
753-
# return self._config_backup.list_backups()
754-
755-
# async def cleanup_old_backups(self, keep_count: int = 10):
756-
# """清理旧备份"""
757-
# if self._config_backup:
758-
# await self._config_backup.cleanup_old_backups(keep_count)
759712

760713
async def setup_initial_config(self):
761714
"""设置初始配置"""

‎app/database/database_manager.py‎

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pathlib import Path
1010
from typing import Dict, List, Any, Optional
1111
from dataclasses import dataclass
12-
from sqlalchemy import select, and_, or_, func, desc
12+
from sqlalchemy import case, select, and_, or_, func, desc
1313
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
1414
from sqlalchemy.orm import sessionmaker
1515
from sqlalchemy import text
@@ -431,60 +431,6 @@ async def count_messages_by_time_intervals(self,
431431
print(f"按时间间隔统计消息数量失败: {e}")
432432
return []
433433

434-
async def count_messages_by_type(self,
435-
self_id: str = None,
436-
start_time: int = None,
437-
end_time: int = None,
438-
direction: str = "SEND") -> Dict[str, int]:
439-
"""按消息类型统计消息数量"""
440-
async with self.session_factory() as session:
441-
try:
442-
conditions = []
443-
if self_id:
444-
conditions.append(Message.self_id == self_id)
445-
if direction:
446-
conditions.append(Message.direction == direction)
447-
if start_time:
448-
conditions.append(Message.timestamp >= start_time)
449-
if end_time:
450-
conditions.append(Message.timestamp <= end_time)
451-
452-
# 按消息类型分组统计
453-
stmt = select(
454-
Message.message_type,
455-
func.count(Message.id).label('count')
456-
).group_by(Message.message_type)
457-
458-
if conditions:
459-
stmt = stmt.where(and_(*conditions))
460-
461-
result = await session.execute(stmt)
462-
rows = result.fetchall()
463-
464-
# 格式化结果,将消息类型转换为中文
465-
type_mapping = {
466-
'group': '群聊',
467-
'private': '私聊',
468-
'notice': '通知',
469-
'request': '请求',
470-
'meta_event': '元事件'
471-
}
472-
473-
type_stats = {}
474-
for row in rows:
475-
message_type = row.message_type
476-
count = row.count
477-
478-
# 转换为中文名称
479-
chinese_type = type_mapping.get(message_type, message_type)
480-
type_stats[chinese_type] = count
481-
482-
return type_stats
483-
484-
except Exception as e:
485-
print(f"按消息类型统计消息数量失败: {e}")
486-
return {}
487-
488434

489435
async def _start_cleanup_task(self):
490436
"""启动数据清理任务"""

‎app/web_api/web_server.py‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -664,16 +664,6 @@ def api_statistics():
664664
)
665665
)
666666

667-
# 获取真实的消息类型分布
668-
message_types = asyncio.run(
669-
self.database_manager.count_messages_by_type(
670-
self_id=self_id,
671-
start_time=start_time,
672-
end_time=end_time,
673-
direction=direction
674-
)
675-
)
676-
677667
# 获取收到的消息总量(RECV方向)
678668
received_messages = asyncio.run(
679669
self.database_manager.count_messages(
@@ -701,14 +691,13 @@ def api_statistics():
701691
'active_groups': active_groups,
702692
'received_messages': received_messages, # 收到的消息总量
703693
'hourly_trend': hourly_trend, # 每3小时的趋势数据
704-
'message_types': message_types, # 真实的消息类型分布
705694
'top_groups': [
706695
{
707696
'group_id': group_id,
708697
'message_count': count,
709698
'active_users': 1 # 简化处理
710699
}
711-
for group_id, count in sorted(group_stats.items(), key=lambda x: x[1], reverse=True)[:10]
700+
for group_id, count in sorted(group_stats.items(), key=lambda x: x[1], reverse=True)
712701
],
713702
# 添加变化指示器
714703
'messages_change': total_messages - yesterday_messages,

‎templates/query.html‎

Lines changed: 115 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -399,34 +399,127 @@ <h5 class="mb-0">
399399
}
400400

401401
// 导出结果
402-
function exportResults() {
403-
if (currentResults.length === 0) {
402+
// 导出结果 - 修复版本
403+
async function exportResults() {
404+
if (!currentResults || currentResults.length === 0) {
404405
showToast('暂无数据可导出', 'warning');
405406
return;
406407
}
407408

408-
// 创建CSV内容
409-
let csvContent = "data:text/csv;charset=utf-8,";
410-
csvContent += "时间,方向,账号,用户ID,群组ID,消息内容\n";
409+
const exportButton = document.querySelector('button[onclick="exportResults()"]');
410+
const originalText = exportButton.innerHTML;
411411

412-
currentResults.forEach(message => {
413-
const time = new Date(message.timestamp * 1000).toLocaleString('zh-CN');
414-
const direction = message.direction === 'SEND' ? '发送' : '接收';
415-
const content = (message.message_content || message.raw_message).replace(/"/g, '""');
412+
try {
413+
// 显示导出中状态
414+
exportButton.disabled = true;
415+
exportButton.innerHTML = '<i class="bi bi-hourglass-split"></i> 导出中...';
416416

417-
csvContent += `"${time}","${direction}","${message.self_id}","${message.user_id || ''}","${message.group_id || ''}","${content}"\n`;
418-
});
419-
420-
// 下载文件
421-
const encodedUri = encodeURI(csvContent);
422-
const link = document.createElement("a");
423-
link.setAttribute("href", encodedUri);
424-
link.setAttribute("download", `message_query_${new Date().toISOString().split('T')[0]}.csv`);
425-
document.body.appendChild(link);
426-
link.click();
427-
document.body.removeChild(link);
428-
429-
showToast('查询结果已导出', 'success');
417+
// 获取完整的查询结果(如果总数大于当前结果数)
418+
let allResults = currentResults;
419+
420+
if (totalCount > currentResults.length) {
421+
// 需要获取完整数据
422+
const form = document.getElementById('queryForm');
423+
const formData = new FormData(form);
424+
const params = new URLSearchParams();
425+
426+
// 构建查询参数(与performQuery函数中的逻辑一致)
427+
for (let [key, value] of formData.entries()) {
428+
const trimmedValue = value.trim();
429+
if (trimmedValue) {
430+
if (key === 'keywords' && trimmedValue.includes(',')) {
431+
const keywords = trimmedValue.split(/[,,]/).map(k => k.trim()).filter(k => k);
432+
keywords.forEach(keyword => params.append('keywords', keyword));
433+
} else if (key === 'start_time' || key === 'end_time') {
434+
const timestamp = Math.floor(new Date(trimmedValue).getTime() / 1000);
435+
params.append(key, timestamp);
436+
} else {
437+
params.append(key, trimmedValue);
438+
}
439+
}
440+
}
441+
442+
// 设置大的limit以获取所有结果
443+
params.set('limit', '10000');
444+
params.set('offset', '0');
445+
446+
// 重新查询获取完整数据
447+
const response = await apiRequest(`/api/query_messages?${params.toString()}`);
448+
allResults = response.messages || [];
449+
}
450+
451+
if (allResults.length === 0) {
452+
showToast('没有数据可导出', 'warning');
453+
return;
454+
}
455+
456+
// 创建CSV内容
457+
const csvRows = [];
458+
csvRows.push(['时间', '方向', '机器人账号', '用户ID', '用户名', '群组ID', '消息内容']);
459+
460+
allResults.forEach(message => {
461+
const time = new Date(message.timestamp * 1000).toLocaleString('zh-CN');
462+
const direction = message.direction === 'SEND' ? '发送' : '接收';
463+
464+
// 解析发送者信息
465+
let senderName = '';
466+
try {
467+
const sender = message.sender_info || {};
468+
senderName = sender.nickname || sender.card || '';
469+
} catch {
470+
senderName = '';
471+
}
472+
473+
const content = (message.message_content || message.raw_message || '').replace(/"/g, '""');
474+
475+
csvRows.push([
476+
time,
477+
direction,
478+
message.self_id || '',
479+
message.user_id || '',
480+
senderName,
481+
message.group_id || '',
482+
content
483+
]);
484+
});
485+
486+
// 转换为CSV格式
487+
const csvContent = csvRows.map(row =>
488+
row.map(field => `"${field}"`).join(',')
489+
).join('\n');
490+
491+
// 创建Blob并下载
492+
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8' });
493+
const url = window.URL.createObjectURL(blob);
494+
const link = document.createElement('a');
495+
496+
// 生成文件名
497+
const now = new Date();
498+
const dateStr = now.toISOString().split('T')[0];
499+
const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '');
500+
const filename = `消息查询结果_${dateStr}_${timeStr}.csv`;
501+
502+
link.href = url;
503+
link.download = filename;
504+
link.style.display = 'none';
505+
506+
document.body.appendChild(link);
507+
link.click();
508+
document.body.removeChild(link);
509+
510+
// 释放URL对象
511+
window.URL.revokeObjectURL(url);
512+
513+
showToast(`成功导出 ${allResults.length} 条消息记录`, 'success');
514+
515+
} catch (error) {
516+
console.error('导出失败:', error);
517+
showToast('导出失败: ' + error.message, 'danger');
518+
} finally {
519+
// 恢复按钮状态
520+
exportButton.disabled = false;
521+
exportButton.innerHTML = originalText;
522+
}
430523
}
431524

432525
// HTML转义函数

0 commit comments

Comments
 (0)