微信聊天记录生成可视化界面更新完毕

This commit is contained in:
pengGgxp 2025-05-03 06:26:31 +08:00
parent 36657cb754
commit 6bb8b8b6ec
8 changed files with 1710 additions and 1157 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ dist-ssr
/pywxdump/ui/web/*
/pywxdump/ui/web/assets/*
/pywxdump/wxdump_work
test2.py

View File

@ -577,6 +577,22 @@ def recursive_listdir(path,list:List):
def de_weight(l1:List,l2:List):
"""
列表去重针对特定对象
"""
len1 = min(len(l1), len(l2))
len1 = len1-1 if len1 > 0 else 0
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
@ -592,7 +608,7 @@ def get_ai_ui_json_list():
# 遍历json文件夹查找最后带_ai的文件
work_path = os.path.join(gc.work_path, "export", my_wxid, "json")
if not os.path.exists(work_path):
return ReJson(0,body={"ui_dict_list":[],"ai_dict_list":[]})
os.makedirs(work_path)
file_list:List[str]=[]
recursive_listdir(work_path,list=file_list)
@ -602,22 +618,25 @@ def get_ai_ui_json_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('.')[0].split('_')[0] # wxid
time_start = " ".join(s.split('.')[0].split('_')[2:4]) # time start
time_end = " ".join(s.split('.')[0].split('_')[5:7]) # time end
flag = s.split('.')[0].split('_')[-1] # flag
ui_dict_list.append({"wxid": wxid, "time_start": time_start, "time_end": time_end, "flag": flag})
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):
return ReJson(0,body={"ui_dict_list":ui_dict_list,"ai_dict_list":[]})
os.makedirs(work_path)
file_list:List[str]=[]
recursive_listdir(work_path,list=file_list)
@ -630,14 +649,23 @@ def get_ai_ui_json_list():
# 构造字典对象
for s in ai_list:
wxid = s.split('.')[0].split('_')[0] # wxid
time_start = " ".join(s.split('.')[0].split('_')[2:4]) # time start
time_end = " ".join(s.split('.')[0].split('_')[5:7]) # time end
ai_dict_list.append({"wxid": wxid, "time_start": time_start, "time_end": time_end})
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={"ui_dict_list":ui_dict_list,"ai_dict_list":ai_dict_list})
return ReJson(0,body={"items":dict_list})
@ -646,6 +674,9 @@ 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)
@ -659,21 +690,20 @@ def get_file_path(work_path: str, file_name: str) -> str | None:
class FileNameRequest(BaseModel):
wxid: str
start_time: int
end_time: int
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 /1000.0
end_time = file_name.end_time /1000.0
start_time = file_name.start_time
end_time = file_name.end_time
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 + '_mini_' + start_time.replace(' ', '_').replace(':', '-') + '_' + end_time.replace(' ', '_').replace(':', '-') + '_ai'
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'
@ -682,8 +712,11 @@ def db_to_ai_json(file_name: FileNameRequest = Body(..., embed=True)):
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}")
@ -700,7 +733,7 @@ def db_to_ai_json(file_name: FileNameRequest = Body(..., embed=True)):
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_data)
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")
@ -708,7 +741,7 @@ def db_to_ai_json(file_name: FileNameRequest = Body(..., embed=True)):
os.makedirs(ai_json_path)
assert isinstance(ai_json_path, str)
file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_' + end_time.replace(' ',
file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_to_' + end_time.replace(' ',
'_').replace(
':', '-')
file_name = file_name + '.json'
@ -720,28 +753,35 @@ def db_to_ai_json(file_name: FileNameRequest = Body(..., embed=True)):
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: FileNameRequest = Body(..., embed=True)):
def get_ui_json(file_name: FileNameGetUiRequest = Body(..., embed=True)):
"""
获取可视化界面json文件
"""
start_time = file_name.start_time /1000.0
end_time = file_name.end_time /1000.0
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")
# print(file_name.wxid)
file_name = wxid + '_aiyes_' + start_time.replace(' ', '_').replace(':', '-') + '_' + end_time.replace(' ', '_').replace(':', '-')
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, "json"), file_name)
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}")

View File

@ -1,62 +1,100 @@
import http from "@/utils/axios.js";
// const is_local_data = false;
const is_local_data = localStorage.getItem('isUseLocalData') === 't';
const is_local_data = localStorage.getItem("isUseLocalData") === "t";
export const apiVersion = () => {
return http.get('/api/rs/version').then((res: any) => {
return res;
}).catch((err: any) => {
console.log(err);
return '';
return http
.get("/api/rs/version")
.then((res: any) => {
return res;
})
}
.catch((err: any) => {
console.log(err);
return "";
});
};
export const api_db_init = async () => {
const t = await http.get('/api/rs/is_init')
console.log("is_db_init", !!t);
return !!t;
}
const t = await http.get("/api/rs/is_init");
console.log("is_db_init", !!t);
return !!t;
};
export const api_img = (url: string) => {
if (is_local_data) {
return `./imgsrc?src=${url}`;
}
return `/api/rs/imgsrc?src=${url}`;
}
if (is_local_data) {
return `./imgsrc?src=${url}`;
}
return `/api/rs/imgsrc?src=${url}`;
};
export const api_audio = (url: string) => {
if (is_local_data) {
return `./audio?src=${url}`;
}
return `/api/rs/audio?src=${url}`;
}
if (is_local_data) {
return `./audio?src=${url}`;
}
return `/api/rs/audio?src=${url}`;
};
export const api_video = (url: string) => {
if (is_local_data) {
return `./video?src=${url}`;
}
return `/api/rs/video?src=${url}`;
}
if (is_local_data) {
return `./video?src=${url}`;
}
return `/api/rs/video?src=${url}`;
};
export const api_file = (url: string) => {
if (is_local_data) {
return `./file?src=${url}`;
}
return `/api/rs/file?src=${url}`;
}
if (is_local_data) {
return `./file?src=${url}`;
}
return `/api/rs/file?src=${url}`;
};
// file_info
export const api_file_info = (url: string) => {
if (is_local_data) {
return `./file_info?src=${url}`;
}
return http.post('/api/rs/file_info', {
'file_path': url,
}).then((res: any) => {
return res;
}).catch((err: any) => {
console.log(err);
return '';
if (is_local_data) {
return `./file_info?src=${url}`;
}
return http
.post("/api/rs/file_info", {
file_path: url,
})
}
.then((res: any) => {
return res;
})
.catch((err: any) => {
console.log(err);
return "";
});
};
// DeepSeek设置部分
export const apiDeepSeekSet = (key: string) => {
return http
.post("/api/rs/deepseek_setting", {
deepseek: {
api_key: key,
},
})
.then((res: any) => {
return res;
})
.catch((err: any) => {
console.log(err);
return "";
});
};
/**
* DeepSeek设置
*/
export const apiDeepSeekGet = () => {
return http
.get("/api/rs/deepseek_setting")
.then((res: any) => {
return res;
})
.catch((err: any) => {
console.log(err);
return "";
});
};

View File

@ -112,3 +112,50 @@ export const apiMsgs = (wxid: string, start: number, limit: number) => {
return '';
})
}
/**
* ai可视化文件列表
*/
export const apiAiList = () =>{
return http.get('/api/rs/ai_ui_json_list' ).then((res: any) => {
return res;
}).catch((err: any) => {
console.log(err);
return '';
})
}
/**
* ai可视化文件内容
*/
export interface AiUiJson {
wxid: string,
start_time:string,
end_time:string,
}
export const apiAiUiJson = (file_name: AiUiJson) =>{
return http.post('/api/rs/get_ui_json', {file_name}).then((res: any) => {
return res;
}).catch((err: any) => {
console.log(err);
return '';
})
}
export const apiAiUiCreateJson = (file_name: AiUiJson) =>{
return http.post('/api/rs/db_to_ai_json', {file_name}).then((res: any) => {
return res;
}).catch((err: any) => {
console.log(err);
return '';
})
}

View File

@ -0,0 +1,72 @@
<template>
<div class="deepseek-set">
<el-form :model="form" label-width="120px">
<el-form-item label="DeepSeek API Key">
<el-input
v-model="form.apiKey"
placeholder="请输入DeepSeek API Key"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
提交
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { apiDeepSeekSet, apiDeepSeekGet } from '@/api/base'
const form = ref({
apiKey: ''
})
const submitting = ref(false)
const fetchSettings = async () => {
try {
const res = await apiDeepSeekGet()
if (res?.API_KEY) {
form.value.apiKey = res.API_KEY
}
} catch (error) {
ElMessage.error('获取设置失败')
}
}
onMounted(() => {
fetchSettings()
})
const handleSubmit = async () => {
if (!form.value.apiKey) {
ElMessage.warning('请输入API Key')
return
}
submitting.value = true
try {
await apiDeepSeekSet(form.value.apiKey)
ElMessage.success('设置成功')
} catch (error) {
ElMessage.error('设置失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.deepseek-set {
padding: 20px;
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<div
style="
background-color: #d2d2fa;
height: 100vh;
display: grid;
place-items: center;
"
>
<div
style="
background-color: #fff;
width: 90%;
height: 80%;
border-radius: 10px;
padding: 20px;
overflow: auto;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
>
<div style="font-size: 20px; font-weight: bold">微信数据可视化</div>
</div>
<div style="margin-top: 20px">
<el-table
:data="tableData"
style="width: 100%"
:default-sort="{ prop: 'startTime', order: 'descending' }"
>
<el-table-column
prop="wxid"
label="微信ID"
sortable
:filter-method="filterWxid"
></el-table-column>
<el-table-column
prop="start_time"
label="开始时间"
sortable
:filter-method="filterStartTime"
></el-table-column>
<el-table-column
prop="end_time"
label="结束时间"
sortable
:filter-method="filterEndTime"
></el-table-column>
<el-table-column
prop="flag"
label="状态"
:filters="[
{ text: '已生成', value: true },
{ text: '未生成', value: false },
]"
:filter-method="filterFlag"
>
<template #default="scope">
<el-tag :type="scope.row.flag ? 'success' : 'warning'">
{{ scope.row.flag ? "已生成" : "未生成" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
v-if="scope.row.flag"
type="primary"
@click="handleJump(scope.row)"
>跳转</el-button
>
<el-button
v-else
type="success"
@click="handleGenerate(scope.row)"
>生成</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup tang="ts">
import { onMounted, ref } from "vue";
import { apiAiList } from "@/api/chat";
import { useRoute, useRouter } from "vue-router";
import {ElLoading,ElMessage} from "element-plus";
import { apiAiUiCreateJson } from "@/api/chat";
const route = useRoute();
const router = useRouter();
const tableData = ref([
]);
/**
* 获取数据
*/
const getTableData = async () => {
try {
const res = await apiAiList();
// console.log("" + res.items[0]);
tableData.value = res.items;
} catch (error) {
console.error("获取数据失败:", error);
}
};
const filterWxid = (value, row) => {
return row.wxid === value;
};
const filterStartTime = (value, row) => {
return row.startTime.includes(value);
};
const filterEndTime = (value, row) => {
return row.endTime.includes(value);
};
const filterFlag = (value, row) => {
return row.flag === value;
};
const handleJump = (row) => {
//
/***
* 构建查询参数
* 格式
*/
console.log("跳转到可视化页面:", row);
router.push({
path: "/chat2ui",
query: {
wxid: row.wxid,
start_time: row.start_time,
end_time: row.end_time,
},
});
};
const handleGenerate = async (row) => {
const loading = ElLoading.service({
lock: true,
text: '正在生成可视化数据可能需要3分钟左右...',
background: 'rgba(0, 0, 0, 0.7)'
});
try {
// 3
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), 180000)
);
const response = await Promise.race([
apiAiUiCreateJson({
wxid: row.wxid,
start_time: row.start_time,
end_time: row.end_time
}),
timeoutPromise
]);
if (response) {
ElMessage.success('可视化数据生成成功');
await getTableData(); //
}
} catch (error) {
console.error('生成失败:', error);
ElMessage.error(error.message || '生成可视化数据失败');
} finally {
loading.close();
}
};
onMounted(async () => {
console.log("页面加载完成");
await getTableData();
});
</script>
<style scoped></style>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
import DbInitView from "@/views/DbInitView.vue";
import {ref} from "vue";
import DbInitComponent from "@/components/utils/DbInitComponent.vue";
import deepseekSet from "@/components/utils/DeepSeekSet.vue";
const setting_selected = ref("")
const MeneSelect = (val: any) => {
@ -34,14 +34,15 @@ const MeneSelect = (val: any) => {
<el-menu-item index="db_init">
<span>初始化设置</span>
</el-menu-item>
<el-menu-item index="3">
<span></span>
<el-menu-item index="deepseek_set">
<span>DeepSeek设置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main style="height: 100%;max-height: 100%;width: 100%;margin: 0;padding: 0;">
<db-init-component v-if="setting_selected=='db_init'"></db-init-component>
<deepseek-set v-if="setting_selected=='deepseek_set'"></deepseek-set>
</el-main>
</el-container>
</el-container>