feat: add pluginized member context profiling

This commit is contained in:
liuwei
2026-04-02 11:49:20 +08:00
parent 848e7b5ab9
commit ce1708677f
11 changed files with 916 additions and 4 deletions
+68 -1
View File
@@ -210,10 +210,27 @@ def api_group_members(roomid):
try:
server = current_app.dashboard_server
group_members = server.contact_db.get_chatroom_small_member_list(roomid)
context_enabled = bool(server.member_context_service) and server.member_context_service.is_group_enabled(roomid)
if context_enabled:
contexts = server.member_context_db.list_group_member_contexts(roomid)
context_map = {item.get("wxid"): item for item in contexts}
for member in group_members:
context = context_map.get(member.get("wxid"), {})
member["activity_level"] = context.get("activity_level", "")
member["response_style_hint"] = context.get("response_style_hint", "")
member["summary_text"] = context.get("summary_text", "")
member["last_profiled_at"] = context.get("last_profiled_at", "")
else:
for member in group_members:
member["activity_level"] = ""
member["response_style_hint"] = ""
member["summary_text"] = ""
member["last_profiled_at"] = ""
return jsonify({
"success": True,
"data": {
"members": group_members
"members": group_members,
"member_context_enabled": context_enabled
}
})
except Exception as e:
@@ -221,6 +238,56 @@ def api_group_members(roomid):
return jsonify({"success": False, "error": str(e)}), 500
@contacts_bp.route('/api/group_member_context/<roomid>/<wxid>', methods=['GET'])
@login_required
def api_group_member_context(roomid, wxid):
"""获取群成员交互摘要"""
try:
server = current_app.dashboard_server
if not server.member_context_service:
return jsonify({"success": False, "error": "成员交互摘要插件未加载"}), 503
if not server.member_context_service.is_group_enabled(roomid):
return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403
context = server.member_context_db.get_member_context(roomid, wxid)
if not context:
context = server.member_context_service.refresh_member_context(roomid, wxid)
return jsonify({"success": True, "data": {"context": context}})
except Exception as e:
logger.error(f"获取群成员交互摘要失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@contacts_bp.route('/api/group_member_context/refresh', methods=['POST'])
@login_required
def api_refresh_group_member_context():
"""刷新群成员交互摘要"""
try:
server = current_app.dashboard_server
if not server.member_context_service:
return jsonify({"success": False, "error": "成员交互摘要插件未加载"}), 503
data = request.json or {}
roomid = data.get("roomid")
wxid = data.get("wxid")
if roomid and wxid:
if not server.member_context_service.is_group_enabled(roomid):
return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403
context = server.member_context_service.refresh_member_context(roomid, wxid)
return jsonify({"success": True, "data": {"context": context}})
if roomid:
if not server.member_context_service.is_group_enabled(roomid):
return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403
result = server.member_context_service.refresh_group_contexts(roomid)
return jsonify({"success": True, "data": result})
result = server.member_context_service.refresh_all_chatrooms()
return jsonify({"success": True, "data": result})
except Exception as e:
logger.error(f"刷新群成员交互摘要失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@contacts_bp.route('/api/update', methods=['POST'])
@login_required
def api_contacts_update():
+4
View File
@@ -11,6 +11,7 @@ from flask import Flask, send_from_directory
from loguru import logger
from db.contacts_db import ContactsDBOperator
from db.member_context_db import MemberContextDBOperator
from db.message_storage import MessageStorageDB
from db.stats_db import StatsDBOperator
from db.task_db import TaskDBOperator
@@ -43,6 +44,7 @@ class DashboardServer:
self.stats_db = StatsDBOperator(self.db_manager)
self.message_storage = MessageStorageDB(self.db_manager)
self.contact_db: ContactsDBOperator = ContactsDBOperator(self.db_manager)
self.member_context_db = MemberContextDBOperator(self.db_manager)
self.task_db: TaskDBOperator = TaskDBOperator(self.db_manager)
# 获取联系人管理器实例
self.contact_manager = robot_instance.contact_manager
@@ -50,6 +52,8 @@ class DashboardServer:
self.plugin_registry = robot_instance.plugin_registry
self.client: WechatAPIClient = robot_instance.ipad_bot
self.robot = robot_instance
self.member_context_plugin = self.plugin_manager.plugins.get("成员交互摘要")
self.member_context_service = getattr(self.member_context_plugin, "service", None)
self.LOG.info("使用Robot实例的对象进行初始化")
else:
@@ -185,7 +185,10 @@
<div class="group-members-section">
<div class="section-title">
<h3>群成员列表</h3>
<el-input placeholder="搜索群成员..." v-model="groupMemberSearchQuery" class="group-search" clearable></el-input>
<div class="section-actions">
<el-input placeholder="搜索群成员..." v-model="groupMemberSearchQuery" class="group-search" clearable></el-input>
<el-button size="mini" type="success" @click="refreshCurrentGroupContexts">刷新本群摘要</el-button>
</div>
</div>
<el-table :data="filteredGroupMembers" style="width: 100%" v-loading="groupMembersLoading">
<el-table-column type="index" width="54"></el-table-column>
@@ -213,6 +216,15 @@
<el-table-column prop="latest_active_time" label="活跃时间">
<template slot-scope="scope">{% raw %}{{ scope.row.latest_active_time || '-' }}{% endraw %}</template>
</el-table-column>
<el-table-column prop="activity_level" label="互动强度" width="110"></el-table-column>
<el-table-column label="回复建议" min-width="220" show-overflow-tooltip>
<template slot-scope="scope">{% raw %}{{ scope.row.response_style_hint || '-' }}{% endraw %}</template>
</el-table-column>
<el-table-column label="后台摘要" width="130" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="openMemberContextDialog(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container" v-if="groupMembersList.length > 10">
@@ -233,6 +245,51 @@
</el-descriptions>
</el-dialog>
<el-dialog title="成员交互摘要" :visible.sync="memberContextDialogVisible" width="52%">
<div v-loading="memberContextLoading">
<el-alert
title="该摘要仅供后台调优参考,不会在群内对用户显式展示。"
type="info"
:closable="false"
show-icon>
</el-alert>
<div class="member-context-toolbar">
<div class="member-context-title">
<strong>{% raw %}{{ currentContextMember.name || currentContextMember.wxid || '成员' }}{% endraw %}</strong>
<span>{% raw %}{{ currentContextMember.wxid || '' }}{% endraw %}</span>
</div>
<el-button size="mini" type="success" @click="refreshMemberContext">刷新摘要</el-button>
</div>
<el-descriptions :column="1" border v-if="memberContext">
<el-descriptions-item label="互动强度">{% raw %}{{ memberContext.activity_level || '-' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="表达特征">{% raw %}{{ memberContext.message_pattern || '-' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="互动风格">{% raw %}{{ memberContext.interaction_style || '-' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="回复建议">{% raw %}{{ memberContext.response_style_hint || '-' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="长期关注">
<el-tag v-for="item in (memberContext.topics_of_interest || [])" :key="item" size="mini" class="context-tag">{% raw %}{{ item }}{% endraw %}</el-tag>
<span v-if="!(memberContext.topics_of_interest || []).length">-</span>
</el-descriptions-item>
<el-descriptions-item label="近期话题">
<el-tag v-for="item in (memberContext.recent_focus || [])" :key="item" size="mini" type="success" class="context-tag">{% raw %}{{ item }}{% endraw %}</el-tag>
<span v-if="!(memberContext.recent_focus || []).length">-</span>
</el-descriptions-item>
<el-descriptions-item label="显著特征">
<el-tag v-for="item in ((memberContext.meta || {}).engagement_traits || [])" :key="item" size="mini" type="warning" class="context-tag">{% raw %}{{ item }}{% endraw %}</el-tag>
<span v-if="!(((memberContext.meta || {}).engagement_traits || []).length)">-</span>
</el-descriptions-item>
<el-descriptions-item label="回复避坑">
<el-tag v-for="item in ((memberContext.meta || {}).reply_taboos || [])" :key="item" size="mini" type="danger" class="context-tag">{% raw %}{{ item }}{% endraw %}</el-tag>
<span v-if="!(((memberContext.meta || {}).reply_taboos || []).length)">-</span>
</el-descriptions-item>
<el-descriptions-item label="摘要说明">{% raw %}{{ memberContext.summary_text || '-' }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="样本消息">{% raw %}{{ memberContext.source_message_count || 0 }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="最后更新">{% raw %}{{ memberContext.last_profiled_at || '-' }}{% endraw %}</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
<el-dialog title="公众号详情" :visible.sync="officialDetailDialogVisible" width="50%">
<div class="detail-avatar-wrap">
<el-avatar size="large" :src="getHeadImage(currentOfficial.wxid)" @error="() => true" class="detail-avatar">
@@ -323,6 +380,8 @@
publicDetailDialogVisible: false,
currentGroup: {}, currentUser: {}, currentOfficial: {}, currentPublic: {},
groupMembersList: [], groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', groupMembersLoading: false,
memberContextDialogVisible: false, memberContextLoading: false, memberContext: null, currentContextMember: {},
memberContextEnabled: false,
chatDialogVisible: false, currentChatUser: null, messageInput: '', chatMessages: [],
linkDialogVisible: false,
linkForm: { url: '', title: '', description: '' }
@@ -386,12 +445,80 @@
axios.get(`/contacts/api/group_members/${roomid}`).then(response => {
if (response.data.success) {
const members = response.data.data.members;
this.groupMembersList = members.map(item => ({ wxid: item.wxid, name: item.nick_name, display_name: item.display_name, status: item.status, latest_active_time: item.latest_active_time, small_head_img_url: item.small_head_img_url }));
this.memberContextEnabled = !!response.data.data.member_context_enabled;
this.groupMembersList = members.map(item => ({ wxid: item.wxid, name: item.nick_name, display_name: item.display_name, status: item.status, latest_active_time: item.latest_active_time, small_head_img_url: item.small_head_img_url, activity_level: item.activity_level, response_style_hint: item.response_style_hint, summary_text: item.summary_text, last_profiled_at: item.last_profiled_at }));
} else { this.$message.error('获取群成员失败'); }
}).catch(error => { console.error('加载群成员数据失败:', error); this.$message.error('加载群成员数据失败'); }).finally(() => { this.groupMembersLoading = false; });
},
handleGroupMembersSizeChange(size) { this.groupMembersPageSize = size; },
handleGroupMembersCurrentChange(page) { this.groupMembersCurrentPage = page; },
refreshCurrentGroupContexts() {
if (!this.currentGroup.wxid) return;
if (!this.memberContextEnabled) {
this.$message.warning('该群未启用成员交互摘要功能');
return;
}
this.groupMembersLoading = true;
axios.post('/contacts/api/group_member_context/refresh', { roomid: this.currentGroup.wxid })
.then(response => {
if (response.data.success) {
this.$message.success('本群成员交互摘要已刷新');
this.loadGroupMembers(this.currentGroup.wxid);
} else {
this.$message.error('刷新本群成员交互摘要失败');
}
})
.catch(error => {
console.error('刷新本群成员交互摘要失败:', error);
this.$message.error('刷新本群成员交互摘要失败');
})
.finally(() => { this.groupMembersLoading = false; });
},
openMemberContextDialog(member) {
if (!this.memberContextEnabled) {
this.$message.warning('该群未启用成员交互摘要功能');
return;
}
this.currentContextMember = member;
this.memberContextDialogVisible = true;
this.loadMemberContext();
},
loadMemberContext() {
if (!this.currentGroup.wxid || !this.currentContextMember.wxid) return;
this.memberContextLoading = true;
this.memberContext = null;
axios.get(`/contacts/api/group_member_context/${this.currentGroup.wxid}/${this.currentContextMember.wxid}`)
.then(response => {
if (response.data.success) {
this.memberContext = response.data.data.context;
} else {
this.$message.error('加载成员交互摘要失败');
}
})
.catch(error => {
console.error('加载成员交互摘要失败:', error);
this.$message.error('加载成员交互摘要失败');
})
.finally(() => { this.memberContextLoading = false; });
},
refreshMemberContext() {
if (!this.currentGroup.wxid || !this.currentContextMember.wxid) return;
this.memberContextLoading = true;
axios.post('/contacts/api/group_member_context/refresh', {
roomid: this.currentGroup.wxid,
wxid: this.currentContextMember.wxid
}).then(response => {
if (response.data.success) {
this.memberContext = response.data.data.context;
this.$message.success('成员交互摘要已刷新');
} else {
this.$message.error('刷新成员交互摘要失败');
}
}).catch(error => {
console.error('刷新成员交互摘要失败:', error);
this.$message.error('刷新成员交互摘要失败');
}).finally(() => { this.memberContextLoading = false; });
},
openChatDialog(user) { this.currentChatUser = user; this.chatDialogVisible = true; this.chatMessages = []; },
async sendTextMessage() {
if (!this.messageInput.trim()) return;
@@ -462,10 +589,16 @@
margin: 20px 0 15px 0; border-bottom: 1px solid rgba(148,163,184,0.12); padding-bottom: 10px;
display: flex; justify-content: space-between; align-items: center;
}
.section-actions { display: flex; align-items: center; gap: 10px; }
.section-title h3 { margin: 0; font-size: 18px; color: #0f172a; }
.group-search { width: 220px; }
.detail-avatar-wrap { text-align: center; margin-bottom: 20px; }
.detail-avatar { width: 100px; height: 100px; }
.member-context-toolbar {
display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 16px 0;
}
.member-context-title { display: flex; flex-direction: column; gap: 4px; color: #475569; }
.context-tag { margin-right: 8px; margin-bottom: 8px; }
.chat-container { display: flex; flex-direction: column; height: 500px; }
.message-list {
flex: 1; overflow-y: auto; padding: 20px; background: rgba(248,250,252,0.82); border: 1px solid rgba(148,163,184,0.12);