This commit is contained in:
pengGgxp 2025-05-03 16:52:20 +00:00 committed by GitHub
commit f74d2bbbe6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 5626 additions and 21 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ dist-ssr
*.local
/pywxdump/ui/web/*
/pywxdump/ui/web/assets/*
/pywxdump/wxdump_work
test2.py

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: utils.py
# Name: api_utils.py
# Description:
# Author: xaoyaoo
# Date: 2023/12/03

View File

View File

@ -0,0 +1,14 @@
# 接入dify工作流,用来制作微信聊天记录可视化
class Dify(object):
def __init__(self):
api_key = ""
api_base_url = ""
#TODO: 文件上传
def file_upload(self):
pass
#TODO: 运行工作流
def run_workflows(self):
pass

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,476 @@
# LLM api相关
import enum
import json
import os
import re
import httpx
import openai
from openai import OpenAI
from pywxdump.api.remote_server import gc
class BaseLLMApi(object):
def __init__(self,api_key,base_url=None):
# 设置名字,以供其他函数使用 !!!不使用,
# self.api_key_string = "API_KEY"
# self.base_url_string = "BASE_URL"
# self.env_api_key_string = self.__class__.__name__ + "_" + self.api_key_string
# self.env_base_url_string = self.__class__.__name__ + "_" + self.base_url_string
# self.setting_string = self.__class__.__name__ + "_setting"
self.API_KEY = api_key
self.BASE_URL = base_url
self.module = (
)#模型列表
self.HTTP_CLIENT = None
self.isReady = False
self.message = []
# 执行初始化方法
self.set_default_fn()
def set_default_fn(self):
if not self.module:
self.set_default_module()
if not self.BASE_URL:
self.set_default_base_url()
if not self.message:
self.set_default_message()
def set_default_module(self):
self.module = ()
def set_default_base_url(self):
self.BASE_URL = ""
def set_default_message(self):
"""
要确保message中至少有两个元素第一个元素为系统消息第二个元素为用户消息且第二个元素中有{{content}}
"""
self.message = [
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello {{content}}"},
]
def set_module(self, module):
self.module = module
def set_api_key(self, api_key):
self.API_KEY = api_key
def set_base_url(self, base_url):
self.BASE_URL = base_url
def set_message(self, message):
self.message = message
# def ready(self):
# if not self.API_KEY:
# # 从配置中获取这个功能必须配合网页API开启后才能使用
# self.API_KEY = gc.get_conf(gc.at,self.setting_string)[self.api_key_string]
# if not self.API_KEY:
# raise RuntimeError("API_KEY must be set")
# # 设置环境变量
# os.environ[self.env_api_key_string] = self.API_KEY
# os.environ[self.env_base_url_string] = self.BASE_URL
#
# self.isReady = True
# return
def ready(self):
if not self.BASE_URL and not self.API_KEY:
raise RuntimeError("API_KEY or BASE_URL must be set")
self.isReady = True
@property
def http_client(self):
if not self.isReady:
self.ready()
try:
self.HTTP_CLIENT = OpenAI(api_key=self.API_KEY, base_url=self.BASE_URL)
return self.HTTP_CLIENT
except:
raise RuntimeError("HTTP_CLIENT set not successfully,please check!")
def send_msg(self, message=None, module=None, stream=False):
"""
向大模型发送信息
如果非流式返回则直接输出内容
否则使用openai文档规定格式输出
"""
if message is None:
self.message[1]["content"] = self.message[1].get("content").replace("{{content}}", " ")
message = self.message
else:
self.message[1]["content"] = self.message[1].get("content").replace("{{content}}", message)
message = self.message
response = self.http_client.chat.completions.create(
model=self.module[module],
messages=message,
stream=stream
)
if not stream:
return self.process_msg(response.choices[0].message.content)
else:
return self.process_msg(response.response.read().decode("utf-8"))
def process_msg(self,x):
return x
class DeepSeekApi(BaseLLMApi):
def set_default_module(self):
self.module = (
"deepseek-chat",
"deepseek-reasoner"
)
def set_default_base_url(self):
self.BASE_URL = "https://api.deepseek.com"
def set_default_message(self):
self.message = [
{"role": "system", "content": """从内容中提取出以下信息,可以根据内容多少进行列表扩展或增加,请仔细思索怎么填充内容,如果没有给到合理的名称或其他内容,就以合理的方式思考并添加。
- 最后的输出值使用严格的json格式
- 不要私自添加json块或减少json块
- 内容中不要使用换行符如果内容原本有多个换行符删掉原本多余的的换行符只保留一个换行符再加入进去
- 内容中如果有很奇怪的字符比如''\''''\\''影响代码编译的字符删除原本的字符再加入进去
{
"header": {
"title": "[群名称]报告",
"date": "[日期]",
"metaInfo": {
"totalMessages": "[数量]",
"activeUsers": "[数量]",
"timeRange": "[时间范围]"
}
},
"sections": {
"hotTopics": {
"items": [
{
"name": "[热点话题名称]",
"category": "[话题分类]",
"summary": "[简要总结(50-100字)]",
"keywords": ["[关键词1]", "[关键词2]"],
"mentions": "[次数]"
}
]
},
"tutorials": {
"items": [
{
"type": "[TUTORIAL | NEWS | RESOURCE]",
"title": "[分享的教程或资源标题]",
"sharedBy": "[昵称]",
"time": "[时间]",
"summary": "[内容简介]",
"keyPoints": ["[要点1]", "[要点2]"],
"url": "[URL]",
"domain": "[域名]",
"category": "[分类]"
}
]
},
"importantMessages": {
"items": [
{
"time": "[消息时间]",
"sender": "[发送者昵称]",
"type": "[NOTICE | EVENT | ANNOUNCEMENT | OTHER]",
"priority": "[高|中|低]",
"content": "[消息内容]",
"fullContent": "[完整通知内容]"
}
]
},
"dialogues": {
"items": [
{
"type": "[DIALOGUE | QUOTE]",
"messages": [
{
"speaker": "[说话者昵称]",
"time": "[发言时间]",
"content": "[消息内容]"
}
],
"highlight": "[对话中的金句或亮点]",
"relatedTopic": "[某某话题]"
}
]
},
"qa": {
"items": [
{
"question": {
"asker": "[提问者昵称]",
"time": "[提问时间]",
"content": "[问题内容]",
"tags": ["[相关标签1]", "[相关标签2]"]
},
"answers": [
{
"responder": "[回答者昵称]",
"time": "[回答时间]",
"content": "[回答内容]",
"isAccepted": true
}
]
}
]
},
"analytics": {
"heatmap": [
{
"topic": "[话题名称]",
"percentage": "[百分比]",
"color": "#3da9fc",
"count": "[数量]"
}
],
"chattyRanking": [
{
"rank": 1,
"name": "[群友昵称]",
"count": "[数量]",
"characteristics": ["[特点1]", "[特点2]"],
"commonWords": ["[常用词1]", "[常用词2]"]
}
],
"nightOwl": {
"name": "[熬夜冠军昵称]",
"title": "[熬夜冠军称号]",
"latestTime": "[时间]",
"messageCount": "[数量]",
"lastMessage": "[最后一条深夜消息内容]"
}
},
"wordCloud": {
"words": [
{
"text": "[关键词1]",
"size": 38,
"color": "#00b4d8",
"rotation": -15
}
],
"legend": [
{"color": "#00b4d8", "label": "[分类1] 相关词汇"},
{"color": "#4361ee", "label": "[分类2] 相关词汇"}
]
}
},
"footer": {
"dataSource": "[群名称]聊天记录",
"generationTime": "[当前时间]",
"statisticalPeriod": "[日期] [时间范围]",
"disclaimer": "本报告内容基于群聊公开讨论,如有不当内容或侵权问题请联系管理员处理。"
}
}"""},
{"role": "user", "content": """你好,以下是我要提取的内容: {{content}}"""},
]
def process_msg(self,x):
"""
识别json格式并返回字典
"""
pattern = re.compile('{.*}', flags=re.IGNORECASE | re.MULTILINE | re.S)
# print(pattern.search(json_data).group())
json_data = json.loads(pattern.search(x).group())
return json_data
# if __name__ == "__main__":
# deepseek_api = DeepSeekApi("sk-2ed4377a895d4ce18e086258c254fc8e")
#
# response = deepseek_api.send_msg(module=0,message="""""")
# print(response)

View File

@ -5,6 +5,7 @@
# Author: xaoyaoo
# Date: 2024/04/20
# -------------------------------------------------------------------------------
import datetime
import json
import os
from pywxdump.db import DBHandler
@ -39,5 +40,135 @@ def export_json(wxid, outpath, db_config, my_wxid="我", indent=4):
return True, f"导出成功: {outpath}"
def export_json_mini(wxid, outpath, db_config, my_wxid="", indent=4):
# 确保输出目录存在
if not os.path.exists(outpath):
outpath = os.path.join(os.getcwd(), "export_mini" + os.sep + wxid)
os.makedirs(outpath, exist_ok=True)
db = DBHandler(db_config, my_wxid)
# 获取消息总数
count = db.get_msgs_count(wxid)
chatCount = count.get(wxid, 0)
if chatCount == 0:
return False, "没有聊天记录"
users = {}
page_size = chatCount + 1 # 保持与原函数一致的分页逻辑
for i in range(0, chatCount, page_size):
start_index = i
data, users_t = db.get_msgs(wxid, start_index, page_size)
users.update(users_t) # 合并用户信息
if not data:
continue
# 构建简化数据
mini_data = []
for msg in data:
# 获取昵称优先用备注没有则用昵称最后用wxid
user_info = users.get(msg.get("talker"), {})
nickname = user_info.get("remark") or user_info.get("nickname") or msg.get("talker")
mini_msg = {
"nickname": nickname,
"message": msg.get("msg", ""),
"time": msg.get("CreateTime", "")
}
mini_data.append(mini_msg)
# 保存简化后的文件
save_path = os.path.join(outpath, f"{wxid}_mini_{i}_{i + page_size}.json")
with open(save_path, "w", encoding="utf-8") as f:
json.dump(mini_data, f, ensure_ascii=False, indent=indent)
return True, f"简化版导出成功: {outpath}"
def export_json_mini_time_limit(wxid, outpath, db_config, my_wxid="",
start_createtime=None, end_createtime=None, indent=4):
"""
带时间过滤的简化版聊天记录导出
:param start_createtime: 开始时间格式2025-4-30 16:55:01
:param end_createtime: 结束时间格式2025-4-30 16:55:01
"""
# 创建输出目录
if not os.path.exists(outpath):
outpath = os.path.join(os.getcwd(), "export_mini" + os.sep + wxid)
os.makedirs(outpath, exist_ok=True)
# 初始化数据库连接
db = DBHandler(db_config, my_wxid)
# 时间格式转换
def str_to_timestamp(time_str):
if not time_str:
return None
try:
dt = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
return int(dt.timestamp())
except ValueError:
raise ValueError(f"无效时间格式: {time_str},示例: 2025-04-30 16:55:01")
start_ts = str_to_timestamp(start_createtime)
end_ts = str_to_timestamp(end_createtime)
# 获取消息数据(带时间过滤)
all_data = []
users = {}
page_size = 5000 # 每次获取5000条
start_index = 0
while True:
# 获取分页数据(自动包含时间过滤条件)
data, users_t = db.get_msgs(
wxid,
start_index=start_index,
page_size=page_size,
start_createtime=start_ts,
end_createtime=end_ts
)
if not data:
break
all_data.extend(data)
users.update(users_t)
start_index += page_size
if not all_data:
return False, "指定时间段内没有聊天记录"
# 构建简化数据结构
mini_data = []
for msg in all_data:
talker = msg.get("talker")
user_info = users.get(talker, {})
mini_msg = {
"sender": user_info.get("remark") or user_info.get("nickname") or talker,
"content": msg.get("msg", ""),
"timestamp": msg.get("CreateTime")
}
mini_data.append(mini_msg)
# 生成带时间范围的文件名
time_suffix = ""
if start_createtime or end_createtime:
start_part = start_createtime.replace(" ", "_").replace(":", "-") if start_createtime else "all"
end_part = end_createtime.replace(" ", "_").replace(":", "-") if end_createtime else "now"
time_suffix = f"_{start_part}_to_{end_part}"
filename = f"{wxid}_mini{time_suffix}_ai.json"
save_path = os.path.join(outpath, filename)
with open(save_path, "w", encoding="utf-8") as f:
json.dump(mini_data, f, ensure_ascii=False, indent=indent)
return True, f"导出成功: {save_path}", filename
if __name__ == '__main__':
pass

View File

@ -5,6 +5,8 @@
# Author: xaoyaoo
# Date: 2024/01/02
# -------------------------------------------------------------------------------
import datetime
import json
import os
import time
import shutil
@ -12,6 +14,7 @@ from collections import Counter
from urllib.parse import quote, unquote
from typing import List, Optional
import fastapi.requests
from pydantic import BaseModel
from fastapi import APIRouter, Response, Body, Query, Request
from starlette.responses import StreamingResponse, FileResponse
@ -20,8 +23,10 @@ import pywxdump
from pywxdump import decrypt_merge, get_core_db
from pywxdump.db import DBHandler
from pywxdump.db.utils import download_file, dat2img
from .api_utils.html import HtmlController
from .export import export_csv, export_json, export_html
from .export.exportJSON import export_json_mini, export_json_mini_time_limit
from .rjson import ReJson, RqJson
from .utils import error9999, gc, asyncError9999, rs_loger
@ -134,11 +139,17 @@ def get_msgs(wxid: str = Body(...), start: int = Body(...), limit: int = Body(..
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit)
start_createtime = datetime.datetime.strptime("2025-04-28 00:54:33",
"%Y-%m-%d %H:%M:%S").timestamp()
end_createtime = datetime.datetime.now().timestamp()
msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit, ) #
return ReJson(0, {"msg_list": msgs, "user_list": users})
@ -464,6 +475,51 @@ def get_export_json(wxid: str = Body(..., embed=True)):
return ReJson(2001, body=ret)
class ExportJsonMiniRequest(BaseModel):
start_createtime: int
end_createtime: int
@rs_api.api_route('/export_json_mini_select_time', methods=["GET", 'POST'])
def get_export_json(wxid: str = Body(..., embed=True), time: ExportJsonMiniRequest = Body(..., embed=True)):
"""
导出json,选择时间迷你版本
:return:
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_conf(my_wxid, "db_config")
if not wxid:
return ReJson(1002, body=f"username is required: {wxid}")
outpath = os.path.join(gc.work_path, "export", my_wxid, "json", wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
start_createtime = time.start_createtime / 1000.0 # 格式为 时间戳
end_createtime = time.end_createtime / 1000.0
start_createtime = datetime.datetime.fromtimestamp(float(start_createtime)).strftime("%Y-%m-%d %H:%M:%S") #转换成日期格式
end_createtime = datetime.datetime.fromtimestamp(float(end_createtime)).strftime("%Y-%m-%d %H:%M:%S")
code, ret, filename = export_json_mini_time_limit(wxid, outpath, db_config, my_wxid=my_wxid,
start_createtime=start_createtime, end_createtime=end_createtime)
if code:
# 成功创建,执行生成可视化页面的逻辑
# with open(os.path.join(gc.work_path, "export", my_wxid, "html", wxid, filename), "w", encoding="utf-8") as f:
# f.write(
# #现在是fake
# HtmlController().create_html(json_data=None)
# )
return ReJson(0, ret)
else:
return ReJson(2001, body=ret)
class ExportHtmlRequest(BaseModel):
wxid: str
@ -502,6 +558,251 @@ def get_export_html(wxid: str = Body(..., embed=True)):
# end 导出聊天记录 *******************************************************************************************************
# AI可视化生成 **********************************************
#TODO:查询当前登录用户文件夹下是否有导出数据是否已经存在ui界面
def recursive_listdir(path,list:List):
"""
遍历文件夹获取所有文件 包括子目录
"""
files = os.listdir(path)
for file in files:
file_path = os.path.join(path, file)
if os.path.isdir(file_path):
recursive_listdir(file_path,list)
elif os.path.isfile(file_path):
list.append(file_path)
def de_weight(l1:List,l2:List):
"""
列表去重针对特定对象
"""
len1 = min(len(l1), len(l2))
len1 = len1-1 if len1 > 1 else len1
for i in range(len1):
if l1[i]["wxid"] == l2[i]["wxid"] and l1[i]["start_time"] == l2[i]["start_time"] and l1[i]["end_time"] == l2[i][
"end_time"]:
l1[i]["flag"] = True
l2.pop(i)
return l1+l2
@rs_api.api_route('/ai_ui_json_list', methods=["GET", 'POST'])
def get_ai_ui_json_list():
"""
获取可视化json文件列表
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
# 遍历json文件夹查找最后带_ai的文件
work_path = os.path.join(gc.work_path, "export", my_wxid, "json")
if not os.path.exists(work_path):
os.makedirs(work_path)
file_list:List[str]=[]
recursive_listdir(work_path,list=file_list)
# 解析文件名
ui_list = []
for file in file_list:
if file.split('.')[0].split('_')[-1] == 'ai':
# 可进行ai可视化的文件
ui_list.append(file)
# print(ui_list)
# 构造字典对象
ui_dict_list = []
for s in ui_list:
wxid = s.split('\\')[-1].split('.')[0].split('_')[0] if "@" in s.split('\\')[-1] else \
s.split('\\')[-1].split('.')[0].split('_')[1] # wxid
time_start = " ".join(s.split('\\')[-1].split('.')[0].split('_')[2:4]) if "@" in s.split('\\')[
-1] else " ".join(s.split('\\')[-1].split('.')[0].split('_')[3:5]) # time start
time_end = " ".join(s.split('\\')[-1].split('.')[0].split('_')[5:7]) if "@" in s.split('\\')[-1] else " ".join(
s.split('\\')[-1].split('.')[0].split('_')[6:8]) # time end
ui_dict_list.append({"wxid": wxid, "start_time": time_start, "end_time": time_end, "flag": False})
# 遍历ai_json文件夹,获取所有文件名
work_path = os.path.join(gc.work_path, "export", my_wxid, "ai_json")
if not os.path.exists(work_path):
os.makedirs(work_path)
file_list:List[str]=[]
recursive_listdir(work_path,list=file_list)
# 解析文件名
ai_list = []
for file in file_list:
ai_list.append(file)
ai_dict_list = []
# 构造字典对象
for s in ai_list:
wxid = s.split('\\')[-1].split('.')[0].split('_')[0] if "@" in s.split('\\')[-1] else \
s.split('\\')[-1].split('.')[0].split('_')[1] # wxid
time_start = " ".join(s.split('\\')[-1].split('.')[0].split('_')[2:4]) if "@" in s.split('\\')[
-1] else " ".join(s.split('\\')[-1].split('.')[0].split('_')[3:5]) # time start
time_end = " ".join(s.split('\\')[-1].split('.')[0].split('_')[5:7]) if "@" in s.split('\\')[-1] else " ".join(
s.split('\\')[-1].split('.')[0].split('_')[6:8]) # time end
ai_dict_list.append({"wxid": wxid, "start_time": time_start, "end_time": time_end, "flag": True})
# # 合并两个字典列表
# dict_list = ui_dict_list + ai_dict_list
# print(ui_dict_list)
# print(ai_dict_list)
# 去重
dict_list = de_weight(ui_dict_list,ai_dict_list)
return ReJson(0,body={"items":dict_list})
def get_file_path(work_path: str, file_name: str) -> str | None:
"""
获取ai_json文件路径
"""
# 遍历文件夹内的所有文件,找到对应文件名的文件路径
path_list = os.listdir(work_path)
for path in path_list:
full_path = os.path.join(work_path, path)
if os.path.isfile(full_path) and path == file_name:
return full_path
elif os.path.isdir(full_path):
result = get_file_path(full_path, file_name)
if result is not None:
return result
return None
class FileNameRequest(BaseModel):
wxid: str
start_time: str
end_time: str
@rs_api.api_route('/db_to_ai_json', methods=["GET", 'POST'])
def db_to_ai_json(file_name: FileNameRequest = Body(..., embed=True)):
"""
导出聊天记录到ai_json
"""
start_time = file_name.start_time
end_time = file_name.end_time
wxid = file_name.wxid
file_name = wxid + '_mini_' + start_time.replace(' ', '_').replace(':', '-') + '_to_' + end_time.replace(' ', '_').replace(':', '-') + '_ai'
# file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_' + end_time.replace(' ', '_').replace(':', '-')
file_name = file_name + '.json'
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
result = get_file_path(os.path.join(gc.work_path, "export", my_wxid, "json"), file_name)
if result is None:
return ReJson(1002, body=f"file not found: {file_name}")
# 获取文件内容
with open(result, "r", encoding="utf-8") as f:
json_data = json.load(f)
if not json_data:
return ReJson(1002, body=f"json_data is empty: {file_name}")
#通过llm处理生成ai_json
from .api_utils.llm import DeepSeekApi
# 获取apikey
apikey = gc.get_conf(my_wxid, "deepseek_setting").get("API_KEY")
if not apikey:
return ReJson(1002, body="deepseek_setting.API_KEY is required")
llm_api = DeepSeekApi(api_key=apikey)
json_data = llm_api.send_msg(module=0,message=json.dumps(json_data))
# 保存到ai_json
ai_json_path = os.path.join(gc.work_path, "export", my_wxid, "ai_json")
if not os.path.exists(ai_json_path):
os.makedirs(ai_json_path)
assert isinstance(ai_json_path, str)
file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_to_' + end_time.replace(' ',
'_').replace(
':', '-')
file_name = file_name + '.json'
ai_json_file_path = os.path.join(ai_json_path, file_name)
with open(ai_json_file_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, ensure_ascii=False)
return ReJson(0, body=f"save to {ai_json_file_path}")
class FileNameGetUiRequest(BaseModel):
wxid: str
start_time: str
end_time: str
# 获取可视化界面json文件
@rs_api.api_route('/get_ui_json', methods=["GET", 'POST'])
def get_ui_json(file_name: FileNameGetUiRequest = Body(..., embed=True)):
"""
获取可视化界面json文件
"""
# print(file_name.wxid)
start_time = file_name.start_time
end_time = file_name.end_time
wxid = file_name.wxid if "@" in file_name.wxid else "wxid_" + file_name.wxid
# start_time = datetime.datetime.fromtimestamp(float(start_time)).strftime("%Y-%m-%d %H:%M:%S") #转换成日期格式
# end_time = datetime.datetime.fromtimestamp(float(end_time)).strftime("%Y-%m-%d %H:%M:%S")
file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_to_' + end_time.replace(' ', '_').replace(':', '-')
file_name = file_name + '.json'
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
result = get_file_path(os.path.join(gc.work_path, "export", my_wxid, "ai_json"), file_name)
if result is None:
return ReJson(1002, body=f"file not found: {file_name}")
# 获取文件内容
with open(result, "r", encoding="utf-8") as f:
json_data = json.load(f)
if not json_data:
return ReJson(1002, body=f"json_data is empty: {file_name}")
return ReJson(0, body=json_data)
# AI可视化生成 *******************************************************************************************************
# start 聊天记录分析api **************************************************************************************************
class DateCountRequest(BaseModel):
wxid: str = ""
@ -659,4 +960,62 @@ def get_readme():
else:
return ReJson(2001, body="status_code is not 200")
class DifyApiModel(BaseModel):
api_key: str
base_url: str
@rs_api.api_route('/dify_setting', methods=["GET", 'POST'])
@error9999
def dify_setting(request: Request = None, dify: DifyApiModel = Body(None, embed=True)):
"""
dify设置
"""
if request.method == "GET":
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
gc.get_conf(my_wxid, "dify_setting")
return ReJson(0, body=gc.get_conf(my_wxid, "dify_setting"))
elif request.method == "POST":
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
if not dify.api_key and not dify.base_url:
return ReJson(1002, body="dify_setting is required")
gc.set_conf(my_wxid, "dify_setting", {"API_KEY": dify.api_key, "BASE_URL": dify.base_url})
return ReJson(0, body=gc.get_conf(my_wxid, "dify_setting"))
return ReJson(2001, body="status_code is not 200")
class DeepSeekApiModel(BaseModel):
api_key: str
@rs_api.api_route('/deepseek_setting', methods=["GET", 'POST'])
@error9999
def deepseek_setting(request: Request = None, deepseek: DeepSeekApiModel = Body(None, embed=True)):
"""
deepseek设置
"""
if request.method == "GET":
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
gc.get_conf(my_wxid, "deepseek_setting")
return ReJson(0, body=gc.get_conf(my_wxid, "deepseek_setting"))
elif request.method == "POST":
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
if not deepseek or not deepseek.api_key:
return ReJson(1002, body="deepseek_setting is required")
gc.set_conf(my_wxid, "deepseek_setting", {"API_KEY": deepseek.api_key})
return ReJson(0, body=gc.get_conf(my_wxid, "deepseek_setting"))
return ReJson(2001, body="status_code is not 200")
# END 关于、帮助、设置 ***************************************************************************************************

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: utils.py
# Name: api_utils.py
# Description:
# Author: xaoyaoo
# Date: 2024/01/16

View File

@ -59,6 +59,8 @@ class DBHandler(MicroHandler, MediaHandler, OpenIMContactHandler, PublicMsgHandl
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
"CreateTime": CreateTime, }
"""
msgs0, wxid_list0 = self.get_msg_list(wxids=wxids, start_index=start_index, page_size=page_size,
msg_type=msg_type,
msg_sub_type=msg_sub_type, start_createtime=start_createtime,

View File

@ -103,9 +103,16 @@ class MsgHandler(DatabaseBase):
f"{sql_sub_type}"
f"{sql_start_createtime}"
f"{sql_end_createtime}"
f"ORDER BY CreateTime ASC LIMIT ?,?"
f"ORDER BY CreateTime ASC LIMIT ? OFFSET ?"
)
param = param + (start_index, page_size)
param = param + ( page_size,start_index)
# # 测试
# print(sql + "\n" + " ".join([str(i) for i in param]))
# print(sql + "\n" + " ".join([str(i) for i in param]))
# print(sql + "\n" + " ".join([str(i) for i in param]))
result = self.execute(sql, param)
if not result:
return [], []

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: __init__.py.py
# Description: db.utils
# Description: db.api_utils
# Author: xaoyaoo
# Date: 2024/07/23
# -------------------------------------------------------------------------------

View File

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: __init__.py.py
# Description:
# Author: xaoyaoo
# Date: 2023/12/03
# -------------------------------------------------------------------------------
# from .view_chat import app_show_chat, get_user_list, export
if __name__ == '__main__':
pass

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: utils.py
# Name: api_utils.py
# Description:
# Author: xaoyaoo
# Date: 2023/12/25

View File

@ -1,5 +1,7 @@
openai==1.77.0
setuptools
wheel
pycryptodome
pycryptodomex
pywin32
silk-python
@ -15,4 +17,4 @@ pymem
pydantic==2.7.0
fastapi
uvicorn
python-dotenv
python-dotenv

5
temp.md Normal file
View File

@ -0,0 +1,5 @@
- 导出json可选时间
- 单独导航栏 Ai可视化
- 可以进行可视化的文件列表
- 已经可视化的列表
- 查看

864
test.html Normal file
View File

@ -0,0 +1,864 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>恋爱报告报告 - 2025-04-29 至 2025-04-30</title>
<style>
/* 严格定义的CSS样式确保风格一致性 */
:root {
--bg-primary: #0f0e17;
--bg-secondary: #1a1925;
--bg-tertiary: #252336;
--text-primary: #fffffe;
--text-secondary: #a7a9be;
--accent-primary: #ff8906;
--accent-secondary: #f25f4c;
--accent-tertiary: #e53170;
--accent-blue: #3da9fc;
--accent-purple: #7209b7;
--accent-cyan: #00b4d8;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Display', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 16px;
width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
background-color: var(--bg-secondary);
margin-bottom: 30px;
}
h1 {
font-size: 36px;
font-weight: 700;
color: var(--accent-primary);
margin-bottom: 10px;
}
.date {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 20px;
}
.meta-info {
display: flex;
justify-content: center;
gap: 20px;
}
.meta-info span {
background-color: var(--bg-tertiary);
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
}
section {
background-color: var(--bg-secondary);
margin-bottom: 30px;
padding: 25px;
}
h2 {
font-size: 28px;
font-weight: 600;
color: var(--accent-blue);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--accent-blue);
}
h3 {
font-size: 22px;
font-weight: 600;
color: var(--accent-primary);
margin: 15px 0 10px 0;
}
h4 {
font-size: 18px;
font-weight: 600;
color: var(--accent-secondary);
margin: 12px 0 8px 0;
}
p {
margin-bottom: 15px;
}
ul, ol {
margin-left: 20px;
margin-bottom: 15px;
}
li {
margin-bottom: 5px;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* 卡片容器样式 */
.topics-container, .tutorials-container, .messages-container,
.dialogues-container, .qa-container, .participants-container {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
/* 卡片样式 */
.topic-card, .tutorial-card, .message-card,
.dialogue-card, .qa-card, .participant-item, .night-owl-item {
background-color: var(--bg-tertiary);
padding: 20px;
}
/* 话题卡片 */
.topic-category {
display: inline-block;
background-color: var(--accent-blue);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.topic-keywords {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0;
}
.keyword {
background-color: rgba(61, 169, 252, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.topic-mentions {
color: var(--accent-cyan);
font-weight: 600;
}
/* 教程卡片 */
.tutorial-type {
display: inline-block;
background-color: var(--accent-secondary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.tutorial-meta {
color: var(--text-secondary);
margin-bottom: 10px;
font-size: 14px;
}
.tutorial-category {
margin-top: 10px;
font-style: italic;
color: var(--text-secondary);
}
/* 消息卡片 */
.message-meta {
margin-bottom: 10px;
}
.message-meta span {
margin-right: 15px;
font-size: 14px;
}
.message-type {
background-color: var(--accent-tertiary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
}
.priority {
padding: 3px 10px;
border-radius: 15px;
}
.priority-high {
background-color: var(--accent-secondary);
}
.priority-medium {
background-color: var(--accent-primary);
}
.priority-low {
background-color: var(--accent-blue);
}
/* 对话卡片 */
.dialogue-type {
display: inline-block;
background-color: var(--accent-purple);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.dialogue-content {
background-color: rgba(255, 255, 255, 0.05);
padding: 15px;
margin-bottom: 15px;
}
.dialogue-highlight {
font-style: italic;
color: var(--accent-primary);
margin: 10px 0;
font-weight: 600;
}
/* 问答卡片 */
.question {
margin-bottom: 15px;
}
.question-meta, .answer-meta {
color: var(--text-secondary);
margin-bottom: 5px;
font-size: 14px;
}
.question-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.tag {
background-color: rgba(114, 9, 183, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.answer {
background-color: rgba(255, 255, 255, 0.05);
padding: 15px;
margin-top: 10px;
}
.accepted-badge {
background-color: var(--accent-primary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
}
/* 热度图 */
.heatmap-container {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.heat-topic {
font-weight: 600;
margin-bottom: 5px;
}
.heat-bar {
height: 20px;
background-color: rgba(255, 255, 255, 0.1);
margin: 5px 0;
border-radius: 10px;
overflow: hidden;
}
.heat-fill {
height: 100%;
border-radius: 10px;
}
/* 话唠榜 */
.participant-rank {
font-size: 28px;
font-weight: 700;
color: var(--accent-primary);
margin-right: 15px;
float: left;
}
.participant-name {
font-weight: 600;
font-size: 18px;
margin-bottom: 5px;
}
.participant-count {
color: var(--accent-cyan);
margin-bottom: 10px;
}
.participant-characteristics, .participant-words {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.characteristic {
background-color: rgba(242, 95, 76, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.word {
background-color: rgba(229, 49, 112, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
/* 熬夜冠军 */
.night-owl-item {
background: linear-gradient(135deg, #0f0e17 0%, #192064 100%);
padding: 20px;
display: flex;
align-items: center;
}
.owl-crown {
font-size: 40px;
margin-right: 20px;
}
.owl-name {
font-weight: 600;
font-size: 18px;
margin-bottom: 5px;
}
.owl-title {
color: var(--accent-primary);
font-style: italic;
margin-bottom: 10px;
}
.owl-time, .owl-messages {
color: var(--text-secondary);
margin-bottom: 5px;
}
.owl-note {
font-size: 14px;
color: var(--text-secondary);
margin-top: 10px;
font-style: italic;
}
/* 词云 - 云朵样式 */
.cloud-container {
position: relative;
margin: 0 auto;
padding: 20px 0;
}
.cloud-wordcloud {
position: relative;
width: 600px;
height: 400px;
margin: 0 auto;
background-color: var(--bg-tertiary);
border-radius: 50%;
box-shadow:
40px 40px 0 -5px var(--bg-tertiary),
80px 10px 0 -10px var(--bg-tertiary),
110px 35px 0 -5px var(--bg-tertiary),
-40px 50px 0 -8px var(--bg-tertiary),
-70px 20px 0 -10px var(--bg-tertiary);
overflow: visible;
}
.cloud-word {
position: absolute;
transform-origin: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.cloud-word:hover {
transform: scale(1.1);
z-index: 10;
}
.cloud-legend {
margin-top: 60px;
display: flex;
justify-content: center;
gap: 30px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
}
/* 底部 */
footer {
text-align: center;
padding: 20px 0;
margin-top: 50px;
background-color: var(--bg-secondary);
color: var(--text-secondary);
font-size: 14px;
}
footer p {
margin: 5px 0;
}
.disclaimer {
margin-top: 15px;
font-style: italic;
}
/* 新增头像相关样式 */
.user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
transition: transform 0.3s ease;
position: relative;
cursor: pointer;
border: 2px solid var(--accent-primary);
}
/* 头像悬停效果 */
.user-avatar:hover {
transform: scale(1.1) rotate(5deg);
z-index: 100;
}
/* 头像tooltip */
.avatar-tooltip {
visibility: hidden;
background-color: var(--bg-tertiary);
color: var(--text-primary);
text-align: center;
padding: 5px 10px;
border-radius: 6px;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
opacity: 0;
transition: opacity 0.3s;
font-size: 14px;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
}
.user-avatar:hover .avatar-tooltip {
visibility: visible;
opacity: 1;
}
/* 热度用户专区 */
.hot-users {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 20px;
margin-top: 20px;
}
.hot-user-item {
position: relative;
text-align: center;
}
/* 皇冠标识 */
.hot-crown {
position: absolute;
top: -10px;
right: -5px;
font-size: 24px;
color: #ffd700;
filter: drop-shadow(0 2px 2px rgba(0,0,0,0.3));
}
</style>
</head>
<body>
<header>
<h1>恋爱报告报告</h1>
<p class="date">2025-04-29 至 2025-04-30</p>
<div class="meta-info">
<span>总消息数500+</span>
<span>活跃用户2</span>
<span>时间范围00:05:04 至 22:20:26</span>
</div>
</header>
<!-- 1. 今日讨论热点 -->
<section class="hot-topics">
<h2>今日讨论热点</h2>
<div class="topics-container">
<!-- 在这里填充讨论热点内容严格按照以下格式保留3-5个话题 -->
<div class="topic-card">
<h3>恋爱沟通</h3>
<div class="topic-category">情感交流</div>
<p class="topic-summary">双方围绕情感需求、沟通方式和相互理解展开多次对话,涉及'控制欲''陪伴需求''表达方式'等核心矛盾点。典型对话如'你就不能说话吗'-'不困了,跟我聊聊天',体现双方对沟通频率的认知差异。</p>
<div class="topic-keywords">
<span class="keyword">控制</span><span class="keyword">陪伴</span><span class="keyword">理解</span>
</div>
<div class="topic-mentions">提及次数30+</div>
</div>
<div class="topic-card">
<h3>项目合作</h3>
<div class="topic-category">学业协作</div>
<p class="topic-summary">4月29日上午集中讨论服务设计项目开发事宜涉及原型设计、比赛规划、老师对接等具体内容。昏沉沉的提出开发支持意愿要哄宝宝开心则负责团队协调双方就'高保真原型''互联网+大赛'等专业概念进行多次确认。</p>
<div class="topic-keywords">
<span class="keyword">原型</span><span class="keyword">开发</span><span class="keyword">比赛</span>
</div>
<div class="topic-mentions">提及次数50+</div>
</div>
<!-- 复制上述卡片结构添加更多话题 -->
</div>
</section>
<!-- 2. 实用教程与资源分享 -->
<section class="tutorials">
<h2>实用教程与资源分享</h2>
<div class="tutorials-container">
<!-- 在这里填充教程和资源内容,严格按照以下格式 -->
<div class="tutorial-card">
<div class="tutorial-type">RESOURCE</div>
<h3>产品设计开发流程</h3>
<div class="tutorial-meta">
<span class="shared-by">分享者:昏沉沉的</span>
<span class="share-time">时间2025-04-29 09:42:26</span>
</div>
<p class="tutorial-summary">讨论软件产品开发各阶段要点,强调从调研到高保真原型的设计闭环</p>
<div class="key-points">
<h4>要点:</h4>
<ul><li>功能闭环</li><li>UI完善</li><li>比赛适配</li></ul>
</div>
<div class="tutorial-link">
<a href="" class="link valid">查看原文: 微信聊天</a>
</div>
<div class="tutorial-category">分类:产品设计</div>
</div>
<!-- 复制上述卡片结构添加更多资源 -->
</div>
</section>
<!-- 3. 重要消息汇总 -->
<section class="important-messages">
<h2>重要消息汇总</h2>
<div class="messages-container">
<!-- 在这里填充重要消息内容,严格按照以下格式 -->
<div class="message-card">
<div class="message-meta">
<span class="time">2025-04-29 00:29:04</span>
<span class="sender">要哄宝宝开心,宝宝开心我就开心</span>
<span class="message-type">OTHER</span>
<span class="priority priority-高">优先级:高</span>
</div>
<p class="message-content">只要你做的不论是我,还是外人看来是爱我的</p>
<div class="message-full-content">
<p>完整情感需求表达</p>
</div>
</div>
<div class="message-card">
<div class="message-meta">
<span class="time">2025-04-29 10:15:17</span>
<span class="sender">昏沉沉的</span>
<span class="message-type">EVENT</span>
<span class="priority priority-中">优先级:中</span>
</div>
<p class="message-content">你说你男朋友做开发的 做出来原型的话可以让你男朋友看一下 可以开发</p>
<div class="message-full-content">
<p>项目合作具体提案</p>
</div>
</div>
<!-- 复制上述卡片结构添加更多消息 -->
</div>
</section>
<!-- 4. 有趣对话或金句 -->
<section class="interesting-dialogues">
<h2>有趣对话或金句</h2>
<div class="dialogues-container">
<!-- 在这里填充对话内容,严格按照以下格式 -->
<div class="dialogue-card">
<div class="dialogue-type">DIALOGUE</div>
<div class="dialogue-content">
<div class="message">
<div class="message-meta">
<span class="speaker">要哄宝宝开心,宝宝开心我就开心</span>
<span class="time">2025-04-29 00:07:44</span>
</div>
<p class="message-content">不困了,跟我聊聊天</p>
</div>
<div class="message">
<div class="message-meta">
<span class="speaker">昏沉沉的</span>
<span class="time">2025-04-29 00:07:42</span>
</div>
<p class="message-content">不能跟你说话吗</p>
</div>
</div>
<div class="dialogue-highlight">沟通需求 vs 陪伴需求</div>
<div class="dialogue-topic">相关话题:情感表达差异</div>
</div>
<!-- 复制上述卡片结构添加更多对话 -->
</div>
</section>
<!-- 5. 问题与解答 -->
<section class="questions-answers">
<h2>问题与解答</h2>
<div class="qa-container">
<!-- 在这里填充问答内容,严格按照以下格式 -->
<div class="qa-card">
<div class="question">
<div class="question-meta">
<span class="asker">要哄宝宝开心,宝宝开心我就开心</span>
<span class="time">2025-04-29 10:33:25</span>
</div>
<p class="question-content">你开发要钱吗</p>
<div class="question-tags">
<span class="tag">项目</span><span class="tag">费用</span>
</div>
</div>
<div class="answers">
<div class="answer">
<div class="answer-meta">
<span class="responder">昏沉沉的</span>
<span class="time">2025-04-29 10:45:18</span>
<span class='accepted-badge'>最佳回答</span>
</div>
<p class="answer-content">那不就完了</p>
</div>
</div>
</div>
<!-- 复制上述卡片结构添加更多问答 -->
</div>
</section>
<!-- 6. 群内数据可视化 -->
<section class="analytics">
<h2>群内数据可视化</h2>
<!-- 话题热度 -->
<h3>话题热度</h3>
<div class="heatmap-container">
<!-- 在这里填充话题热度数据,严格按照以下格式 -->
<!-- 复制上述结构添加更多热度项,每项使用不同颜色 -->
<div class="heat-item">
<div class="heat-topic">情感交流</div>
<div class="heat-percentage">45%%</div>
<div class="heat-bar">
<div class="heat-fill" style="width: 45%%; background-color: #3da9fc;"></div>
</div>
<div class="heat-count">200+条消息</div>
</div>
<div class="heat-item">
<div class="heat-topic">项目讨论</div>
<div class="heat-percentage">30%%</div>
<div class="heat-bar">
<div class="heat-fill" style="width: 30%%; background-color: #f25f4c;"></div>
</div>
<div class="heat-count">150+条消息</div>
</div>
<!-- 可用的颜色: #3da9fc, #f25f4c, #7209b7, #e53170, #00b4d8, #4cc9f0 -->
</div>
<!-- 话唠榜 -->
<!-- 在话唠榜添加头像 -->
<section class="analytics">
<h3>话唠榜</h3>
<div class="participants-container">
<div class="participant-item">
<div class="participant-rank">1</div>
<div class="participant-info">
<div class="participant-name">要哄宝宝开心,宝宝开心我就开心</div>
<div class="participant-count">发言数300+</div>
<div class="participant-characteristics">
<span class="characteristic">情感表达</span><span class="characteristic">细节追问</span>
</div>
<div class="participant-words">
<span class="word"></span><span class="word">OK</span><span class="word"></span>
</div>
</div>
</div>
</div>
</section>
<!-- 熬夜冠军 -->
<h3>熬夜冠军</h3>
<div class="night-owls-container">
<!-- 在这里填充熬夜冠军数据,严格按照以下格式 -->
<div class="night-owl-item">
<div class="owl-crown" title="熬夜冠军">👑</div>
<div class="owl-info">
<div class="owl-name">昏沉沉的</div>
<div class="owl-title">深夜程序员</div>
<div class="owl-time">最晚活跃时间01:42:38</div>
<div class="owl-messages">深夜消息数50+</div>
<div class="owl-last-message">对不起</div>
<div class="owl-note">熬夜时段定义为23:00-06:00已考虑不同时区</div>
</div>
</div>
</section>
<!-- 7. 词云 -->
<section class="word-cloud">
<h2>热门词云</h2>
<div class="cloud-container">
<!-- 词云容器 - 现在是云朵样式 -->
<div class="cloud-wordcloud" id="word-cloud">
<!-- 为每个词创建一个span元素使用绝对定位放置 -->
<!-- 以下是一些示例请根据实际内容生成40-60个词 -->
<span class="cloud-word" style="left: 300px; top: 120px;
font-size: 38px; color: #00b4d8;
transform: rotate(-15deg);">开发</span>
<span class="cloud-word" style="left: 300px; top: 120px;
font-size: 32px; color: #00b4d8;
transform: rotate(15deg);">睡觉</span>
<!-- 继续添加更多词 -->
</div>
<div class="cloud-legend">
<div class="legend-item">
<span class="legend-color" style="background-color: #00b4d8;"></span>
<span class="legend-label">项目 相关词汇</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: #4361ee;"></span>
<span class="legend-label">情感 相关词汇</span>
</div>
</div>
</div>
</section>
<!-- 8. 页面底部 -->
<footer>
<p>数据来源:恋爱私聊记录聊天记录</p>
<p>生成时间:<span class="generation-time">2025-05-01 10:00:00</span></p>
<p>统计周期2025-04-29 至 2025-04-30 [时间范围]</p>
<p class="disclaimer">免责声明:本报告内容基于群聊公开讨论,如有不当内容或侵权问题请联系管理员处理。</p>
</footer>
</body>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取所有词云元素
const cloudWords = document.querySelectorAll('.cloud-word');
const container = document.querySelector('.cloud-wordcloud');
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
// 为每个词云元素设置随机位置
cloudWords.forEach(word => {
const wordWidth = word.offsetWidth;
const wordHeight = word.offsetHeight;
// 计算随机位置,确保词云元素不会超出容器边界
const randomLeft = Math.random() * (containerWidth - wordWidth);
const randomTop = Math.random() * (containerHeight - wordHeight);
// 设置位置
word.style.left = `${randomLeft}px`;
word.style.top = `${randomTop}px`;
// 添加悬停效果
word.addEventListener('mouseover', function() {
this.style.transform = 'scale(1.1)';
this.style.zIndex = '10';
});
word.addEventListener('mouseout', function() {
this.style.transform = 'scale(1)';
this.style.zIndex = '1';
});
});
});
</script>
</html>

955
test.py Normal file

File diff suppressed because one or more lines are too long

19
test2.py Normal file
View File

@ -0,0 +1,19 @@
import os
import site
# s = r"E:\project\wx_db_ui\PyWxDump-master\pywxdump\wxdump_work\export\wxid_7l787uu0sm8e22\ai_json\48805389894@chatroom_aiyes_2025-04-30_00-00-00_2025-05-01_to_23-59-59.json"
# wxid = s.split('\\')[-1].split('.')[0].split('_')[0] if "@" in s.split('\\')[-1] else \
# s.split('\\')[-1].split('.')[0].split('_')[1] # wxid
# time_start = " ".join(s.split('\\')[-1].split('.')[0].split('_')[2:4]) if "@" in s.split('\\')[
# -1] else " ".join(s.split('\\')[-1].split('.')[0].split('_')[3:5]) # time start
# time_end = " ".join(s.split('\\')[-1].split('.')[0].split('_')[5:7]) if "@" in s.split('\\')[-1] else " ".join(
# s.split('\\')[-1].split('.')[0].split('_')[6:8]) # time end
#
# print(wxid)
# print(time_start)
# print(time_end)
print(site.getsitepackages())

View File

@ -145,7 +145,8 @@ with open("dist/wxdump_version_info.txt", "w", encoding="utf-8") as f:
# 获取安装包的路径
package_path = site.getsitepackages()
if package_path:
package_path = package_path[1] # 假设取第一个安装包的路径
# package_path = site.getsitepackages()[0] # 假设取第一个安装包的路径
package_path = r"E:\project\wx_db_ui\PyWxDump-master"
current_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在路径
require_path = os.path.join(os.path.dirname(current_path), "requirements.txt") # requirements.txt 路径
@ -153,7 +154,7 @@ if package_path:
hidden_imports = f.read().splitlines()
hidden_imports = [i.replace('-', '_').split("=")[0].split("~")[0] for i in hidden_imports if
i and i not in ["setuptools", "wheel"]] # 去掉setuptools、wheel
hidden_imports += ["pywxdump", "pywxdump.db", "pywxdump.db.__init__.utils"]
hidden_imports += ["win32com",'Crypto' ,"pywxdump", "pywxdump.db", "pywxdump.db.__init__.utils"]
# 获取 ui 文件夹下的所有文件 用于打包
root_path = os.path.join(package_path, 'pywxdump')

1078
text.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,762 @@
任务:根据 提供的微信群聊天记录json格式生成今日群/好友日报输出为风格固定、一致的HTML页面适合截图分享
## 日报模式选择
- 日报模式:[完整版/简化版] (默认为完整版)
- 如果需要简化版,请在提交时注明"生成简化版"
## 简化版说明
如选择"简化版",将只生成以下核心部分:
- 今日讨论热点最多3个
- 重要消息汇总
- 话唠榜仅前3名
- 简化版词云
日报内容更精简,适合快速浏览和分享。
## 聊天记录格式
``` json
[
{
"nickname": "昏沉沉的", # 发消息人昵称
"message": "XXX", # 消息内容
"time": "2025-04-27 11:33:20" #发消息时间
},
]
```
如未能识别消息格式或未找到有效记录,将显示提示信息并尝试按最佳猜测处理。
## 输出要求
必须使用以下固定的HTML模板和CSS样式仅更新内容部分确保每次生成的页面风格完全一致。使用严格定义的深色科技风格。
## HTML结构模板
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>[群名称]报告 - [日期]</title>
<style>
/* 严格定义的CSS样式确保风格一致性 */
:root {
--bg-primary: #0f0e17;
--bg-secondary: #1a1925;
--bg-tertiary: #252336;
--text-primary: #fffffe;
--text-secondary: #a7a9be;
--accent-primary: #ff8906;
--accent-secondary: #f25f4c;
--accent-tertiary: #e53170;
--accent-blue: #3da9fc;
--accent-purple: #7209b7;
--accent-cyan: #00b4d8;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Display', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 16px;
width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
background-color: var(--bg-secondary);
margin-bottom: 30px;
}
h1 {
font-size: 36px;
font-weight: 700;
color: var(--accent-primary);
margin-bottom: 10px;
}
.date {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 20px;
}
.meta-info {
display: flex;
justify-content: center;
gap: 20px;
}
.meta-info span {
background-color: var(--bg-tertiary);
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
}
section {
background-color: var(--bg-secondary);
margin-bottom: 30px;
padding: 25px;
}
h2 {
font-size: 28px;
font-weight: 600;
color: var(--accent-blue);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--accent-blue);
}
h3 {
font-size: 22px;
font-weight: 600;
color: var(--accent-primary);
margin: 15px 0 10px 0;
}
h4 {
font-size: 18px;
font-weight: 600;
color: var(--accent-secondary);
margin: 12px 0 8px 0;
}
p {
margin-bottom: 15px;
}
ul, ol {
margin-left: 20px;
margin-bottom: 15px;
}
li {
margin-bottom: 5px;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* 卡片容器样式 */
.topics-container, .tutorials-container, .messages-container,
.dialogues-container, .qa-container, .participants-container {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
/* 卡片样式 */
.topic-card, .tutorial-card, .message-card,
.dialogue-card, .qa-card, .participant-item, .night-owl-item {
background-color: var(--bg-tertiary);
padding: 20px;
}
/* 话题卡片 */
.topic-category {
display: inline-block;
background-color: var(--accent-blue);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.topic-keywords {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0;
}
.keyword {
background-color: rgba(61, 169, 252, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.topic-mentions {
color: var(--accent-cyan);
font-weight: 600;
}
/* 教程卡片 */
.tutorial-type {
display: inline-block;
background-color: var(--accent-secondary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.tutorial-meta {
color: var(--text-secondary);
margin-bottom: 10px;
font-size: 14px;
}
.tutorial-category {
margin-top: 10px;
font-style: italic;
color: var(--text-secondary);
}
/* 消息卡片 */
.message-meta {
margin-bottom: 10px;
}
.message-meta span {
margin-right: 15px;
font-size: 14px;
}
.message-type {
background-color: var(--accent-tertiary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
}
.priority {
padding: 3px 10px;
border-radius: 15px;
}
.priority-high {
background-color: var(--accent-secondary);
}
.priority-medium {
background-color: var(--accent-primary);
}
.priority-low {
background-color: var(--accent-blue);
}
/* 对话卡片 */
.dialogue-type {
display: inline-block;
background-color: var(--accent-purple);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
margin-bottom: 10px;
}
.dialogue-content {
background-color: rgba(255, 255, 255, 0.05);
padding: 15px;
margin-bottom: 15px;
}
.dialogue-highlight {
font-style: italic;
color: var(--accent-primary);
margin: 10px 0;
font-weight: 600;
}
/* 问答卡片 */
.question {
margin-bottom: 15px;
}
.question-meta, .answer-meta {
color: var(--text-secondary);
margin-bottom: 5px;
font-size: 14px;
}
.question-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.tag {
background-color: rgba(114, 9, 183, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.answer {
background-color: rgba(255, 255, 255, 0.05);
padding: 15px;
margin-top: 10px;
}
.accepted-badge {
background-color: var(--accent-primary);
color: var(--text-primary);
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
}
/* 热度图 */
.heatmap-container {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.heat-topic {
font-weight: 600;
margin-bottom: 5px;
}
.heat-bar {
height: 20px;
background-color: rgba(255, 255, 255, 0.1);
margin: 5px 0;
border-radius: 10px;
overflow: hidden;
}
.heat-fill {
height: 100%;
border-radius: 10px;
}
/* 话唠榜 */
.participant-rank {
font-size: 28px;
font-weight: 700;
color: var(--accent-primary);
margin-right: 15px;
float: left;
}
.participant-name {
font-weight: 600;
font-size: 18px;
margin-bottom: 5px;
}
.participant-count {
color: var(--accent-cyan);
margin-bottom: 10px;
}
.participant-characteristics, .participant-words {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.characteristic {
background-color: rgba(242, 95, 76, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
.word {
background-color: rgba(229, 49, 112, 0.2);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
}
/* 熬夜冠军 */
.night-owl-item {
background: linear-gradient(135deg, #0f0e17 0%, #192064 100%);
padding: 20px;
display: flex;
align-items: center;
}
.owl-crown {
font-size: 40px;
margin-right: 20px;
}
.owl-name {
font-weight: 600;
font-size: 18px;
margin-bottom: 5px;
}
.owl-title {
color: var(--accent-primary);
font-style: italic;
margin-bottom: 10px;
}
.owl-time, .owl-messages {
color: var(--text-secondary);
margin-bottom: 5px;
}
.owl-note {
font-size: 14px;
color: var(--text-secondary);
margin-top: 10px;
font-style: italic;
}
/* 词云 - 云朵样式 */
.cloud-container {
position: relative;
margin: 0 auto;
padding: 20px 0;
}
.cloud-wordcloud {
position: relative;
width: 600px;
height: 400px;
margin: 0 auto;
background-color: var(--bg-tertiary);
border-radius: 50%;
box-shadow:
40px 40px 0 -5px var(--bg-tertiary),
80px 10px 0 -10px var(--bg-tertiary),
110px 35px 0 -5px var(--bg-tertiary),
-40px 50px 0 -8px var(--bg-tertiary),
-70px 20px 0 -10px var(--bg-tertiary);
overflow: visible;
}
.cloud-word {
position: absolute;
transform-origin: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.cloud-word:hover {
transform: scale(1.1);
z-index: 10;
}
.cloud-legend {
margin-top: 60px;
display: flex;
justify-content: center;
gap: 30px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
}
/* 底部 */
footer {
text-align: center;
padding: 20px 0;
margin-top: 50px;
background-color: var(--bg-secondary);
color: var(--text-secondary);
font-size: 14px;
}
footer p {
margin: 5px 0;
}
.disclaimer {
margin-top: 15px;
font-style: italic;
}
</style>
</head>
<body>
<header>
<h1>[群名称]报告</h1>
<p class="date">[日期]</p>
<div class="meta-info">
<span>总消息数:[数量]</span>
<span>活跃用户:[数量]</span>
<span>时间范围:[时间范围]</span>
</div>
</header>
<!-- 1. 今日讨论热点 -->
<section class="hot-topics">
<h2>今日讨论热点</h2>
<div class="topics-container">
<!-- 在这里填充讨论热点内容严格按照以下格式保留3-5个话题 -->
<div class="topic-card">
<h3>[热点话题名称]</h3>
<div class="topic-category">[话题分类]</div>
<p class="topic-summary">[简要总结(50-100字)]</p>
<div class="topic-keywords">
<span class="keyword">[关键词1]</span>
<span class="keyword">[关键词2]</span>
<!-- 添加更多关键词 -->
</div>
<div class="topic-mentions">提及次数:[次数]</div>
</div>
<!-- 复制上述卡片结构添加更多话题 -->
</div>
</section>
<!-- 2. 实用教程与资源分享 -->
<section class="tutorials">
<h2>实用教程与资源分享</h2>
<div class="tutorials-container">
<!-- 在这里填充教程和资源内容,严格按照以下格式 -->
<div class="tutorial-card">
<div class="tutorial-type">[TUTORIAL | NEWS | RESOURCE]</div>
<h3>[分享的教程或资源标题]</h3>
<div class="tutorial-meta">
<span class="shared-by">分享者:[昵称]</span>
<span class="share-time">时间:[时间]</span>
</div>
<p class="tutorial-summary">[内容简介]</p>
<div class="key-points">
<h4>要点:</h4>
<ul>
<li>[要点1]</li>
<li>[要点2]</li>
<!-- 添加更多要点 -->
</ul>
</div>
<div class="tutorial-link">
<a href="[URL]" class="link valid">查看原文: [域名]</a>
</div>
<div class="tutorial-category">分类:[分类]</div>
</div>
<!-- 复制上述卡片结构添加更多资源 -->
</div>
</section>
<!-- 3. 重要消息汇总 -->
<section class="important-messages">
<h2>重要消息汇总</h2>
<div class="messages-container">
<!-- 在这里填充重要消息内容,严格按照以下格式 -->
<div class="message-card">
<div class="message-meta">
<span class="time">[消息时间]</span>
<span class="sender">[发送者昵称]</span>
<span class="message-type">[NOTICE | EVENT | ANNOUNCEMENT | OTHER]</span>
<span class="priority priority-high">优先级:[高|中|低]</span>
</div>
<p class="message-content">[消息内容]</p>
<div class="message-full-content">
<p>[完整通知内容]</p>
</div>
</div>
<!-- 复制上述卡片结构添加更多消息 -->
</div>
</section>
<!-- 4. 有趣对话或金句 -->
<section class="interesting-dialogues">
<h2>有趣对话或金句</h2>
<div class="dialogues-container">
<!-- 在这里填充对话内容,严格按照以下格式 -->
<div class="dialogue-card">
<div class="dialogue-type">[DIALOGUE | QUOTE]</div>
<div class="dialogue-content">
<div class="message">
<div class="message-meta">
<span class="speaker">[说话者昵称]</span>
<span class="time">[发言时间]</span>
</div>
<p class="message-content">[消息内容]</p>
</div>
<div class="message">
<div class="message-meta">
<span class="speaker">[说话者昵称]</span>
<span class="time">[发言时间]</span>
</div>
<p class="message-content">[消息内容]</p>
</div>
<!-- 添加更多对话消息 -->
</div>
<div class="dialogue-highlight">[对话中的金句或亮点]</div>
<div class="dialogue-topic">相关话题:[某某话题]</div>
</div>
<!-- 复制上述卡片结构添加更多对话 -->
</div>
</section>
<!-- 5. 问题与解答 -->
<section class="questions-answers">
<h2>问题与解答</h2>
<div class="qa-container">
<!-- 在这里填充问答内容,严格按照以下格式 -->
<div class="qa-card">
<div class="question">
<div class="question-meta">
<span class="asker">[提问者昵称]</span>
<span class="time">[提问时间]</span>
</div>
<p class="question-content">[问题内容]</p>
<div class="question-tags">
<span class="tag">[相关标签1]</span>
<span class="tag">[相关标签2]</span>
<!-- 添加更多标签 -->
</div>
</div>
<div class="answers">
<div class="answer">
<div class="answer-meta">
<span class="responder">[回答者昵称]</span>
<span class="time">[回答时间]</span>
<span class="accepted-badge">最佳回答</span>
</div>
<p class="answer-content">[回答内容]</p>
</div>
<!-- 添加更多回答 -->
</div>
</div>
<!-- 复制上述卡片结构添加更多问答 -->
</div>
</section>
<!-- 6. 群内数据可视化 -->
<section class="analytics">
<h2>群内数据可视化</h2>
<!-- 话题热度 -->
<h3>话题热度</h3>
<div class="heatmap-container">
<!-- 在这里填充话题热度数据,严格按照以下格式 -->
<div class="heat-item">
<div class="heat-topic">[话题名称]</div>
<div class="heat-percentage">[百分比]%</div>
<div class="heat-bar">
<div class="heat-fill" style="width: [百分比]%; background-color: #3da9fc;"></div>
</div>
<div class="heat-count">[数量]条消息</div>
</div>
<!-- 复制上述结构添加更多热度项,每项使用不同颜色 -->
<div class="heat-item">
<div class="heat-topic">[话题名称]</div>
<div class="heat-percentage">[百分比]%</div>
<div class="heat-bar">
<div class="heat-fill" style="width: [百分比]%; background-color: #f25f4c;"></div>
</div>
<div class="heat-count">[数量]条消息</div>
</div>
<!-- 可用的颜色: #3da9fc, #f25f4c, #7209b7, #e53170, #00b4d8, #4cc9f0 -->
</div>
<!-- 话唠榜 -->
<h3>话唠榜</h3>
<div class="participants-container">
<!-- 在这里填充话唠榜数据,严格按照以下格式 -->
<div class="participant-item">
<div class="participant-rank">1</div>
<div class="participant-info">
<div class="participant-name">[群友昵称]</div>
<div class="participant-count">[数量]条消息</div>
<div class="participant-characteristics">
<span class="characteristic">[特点1]</span>
<span class="characteristic">[特点2]</span>
<!-- 添加更多特点 -->
</div>
<div class="participant-words">
<span class="word">[常用词1]</span>
<span class="word">[常用词2]</span>
<!-- 添加更多常用词 -->
</div>
</div>
</div>
<!-- 复制上述结构添加更多参与者 -->
</div>
<!-- 熬夜冠军 -->
<h3>熬夜冠军</h3>
<div class="night-owls-container">
<!-- 在这里填充熬夜冠军数据,严格按照以下格式 -->
<div class="night-owl-item">
<div class="owl-crown" title="熬夜冠军">👑</div>
<div class="owl-info">
<div class="owl-name">[熬夜冠军昵称]</div>
<div class="owl-title">[熬夜冠军称号]</div>
<div class="owl-time">最晚活跃时间:[时间]</div>
<div class="owl-messages">深夜消息数:[数量]</div>
<div class="owl-last-message">[最后一条深夜消息内容]</div>
<div class="owl-note">熬夜时段定义为23:00-06:00已考虑不同时区</div>
</div>
</div>
</div>
</section>
<!-- 7. 词云 -->
<section class="word-cloud">
<h2>热门词云</h2>
<div class="cloud-container">
<!-- 词云容器 - 现在是云朵样式 -->
<div class="cloud-wordcloud" id="word-cloud">
<!-- 为每个词创建一个span元素使用绝对定位放置 -->
<!-- 以下是一些示例请根据实际内容生成40-60个词 -->
<span class="cloud-word" style="left: 300px; top: 120px; font-size: 38px; color: #00b4d8; transform: rotate(-15deg); font-weight: bold;">[关键词1]</span>
<span class="cloud-word" style="left: 180px; top: 150px; font-size: 32px; color: #4cc9f0; transform: rotate(5deg); font-weight: bold;">[关键词2]</span>
<span class="cloud-word" style="left: 400px; top: 180px; font-size: 28px; color: #f25f4c; transform: rotate(-5deg);">[关键词3]</span>
<span class="cloud-word" style="left: 250px; top: 220px; font-size: 24px; color: #ff8906; transform: rotate(10deg);">[关键词4]</span>
<span class="cloud-word" style="left: 350px; top: 90px; font-size: 22px; color: #e53170; transform: rotate(-10deg);">[关键词5]</span>
<!-- 继续添加更多词 -->
</div>
<div class="cloud-legend">
<div class="legend-item">
<span class="legend-color" style="background-color: #00b4d8;"></span>
<span class="legend-label">[分类1] 相关词汇</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: #4361ee;"></span>
<span class="legend-label">[分类2] 相关词汇</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: #7209b7;"></span>
<span class="legend-label">[分类3] 相关词汇</span>
</div>
</div>
</div>
</section>
<!-- 8. 页面底部 -->
<footer>
<p>数据来源:[群名称]聊天记录</p>
<p>生成时间:<span class="generation-time">[当前时间]</span></p>
<p>统计周期:[日期] [时间范围]</p>
<p class="disclaimer">免责声明:本报告内容基于群聊公开讨论,如有不当内容或侵权问题请联系管理员处理。</p>
</footer>
</body>
</html>