Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions main/plugins/Group Monitoring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 群成员监控插件 v2.0

实时监控群聊成员变动,有成员加入或退出时自动通知。

## 功能特性

- 🔍 **实时监控**:基于系统消息,即时检测成员变动
- 📥 **进群监控**:成员加入时发送欢迎消息
- 📤 **退群监控**:成员退出时发送通知
- 📝 **自定义消息**:可自定义欢迎语和退群通知语
- 🎛️ **可视化控制面板**:选择要监控的群聊

## 使用方法

### 打开控制面板

在微信聊天界面长按消息,选择 **群监控** 菜单。

### 面板功能

| 功能 | 说明 |
|------|------|
| 启用监控 | 开启/关闭总开关 |
| 退群监控 | 开启后,有成员退群时发送通知 |
| 进群监控 | 开启后,有成员进群时发送欢迎消息 |
| 消息设置 | 自定义欢迎语和退群通知语 |
| 群列表 | 显示所有群聊,可单独开启/关闭监控 |

### 消息模板

支持变量替换:

| 变量 | 说明 | 示例 |
|------|------|------|
| `@name` | 成员的群昵称 | `欢迎 @name 加入群聊!` |

## 配置说明

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `gm_enabled` | false | 总开关 |
| `gm_watch_{群ID}` | false | 单个群的监控开关 |
| `gm_notify` | true | 退群监控开关 |
| `gm_at_join` | false | 进群监控开关 |
| `gm_welcome` | `欢迎 @name 加入群聊!` | 欢迎语模板 |
| `gm_leave` | `@name 退出了群聊` | 退群通知语模板 |

## 文件结构

```
群成员监控/
├── info.prop # 插件元数据
├── main.java # 入口 + 消息监听
├── README.md # 使用文档
├── UI开发文档.md # UI 开发参考
└── views/
├── helper.java # 核心逻辑
└── ui.java # UI 面板
```

## 技术实现

### 消息格式

成员变动时,微信会发送系统消息,包含XML格式数据:

```xml
<sysmsg type="sysmsgtemplate">
<sysmsgtemplate>
<content_template type="tmpl_type_profile">
<template><![CDATA[你将"$kickoutname$"移出了群聊]]></template>
<link_list>
<link name="kickoutname" type="link_profile">
<memberlist>
<member>
<username><![CDATA[wxid_xxx]]></username>
<nickname><![CDATA[昵称]]></nickname>
</member>
</memberlist>
</link>
</link_list>
</content_template>
</sysmsgtemplate>
</sysmsg>
```

### 类型判断

| content_template type | 场景 |
|----------------------|------|
| `tmpl_type_profile` | 移出群聊 |
| `tmpl_type_profilewithrevoke` | 邀请加入 |

### 昵称获取

使用 `getUserName(groupId, wxid)` 获取群内昵称:
- 优先显示群昵称
- 如果没有设置群昵称,显示备注或微信昵称

## 注意事项

- 已退出的群聊不会显示在列表中
- 需要重新加载插件才能生效
- 静默运行,无日志输出
4 changes: 4 additions & 0 deletions main/plugins/Group Monitoring/info.prop
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name=群成员监控
author=Operit
version=2.0.0
desc=监控指定群聊的成员变化,UI面板控制开关
49 changes: 49 additions & 0 deletions main/plugins/Group Monitoring/main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// ==================== 群成员监控 v2.0.0 ====================
// 实时监控:基于系统消息检测进退群

String KEY_ENABLED = "gm_enabled"
String PRE = "gm_watch_"
String KEY_NOTIFY = "gm_notify"
String KEY_AT_JOIN = "gm_at_join"

onLoad() {
if (!configContains(KEY_ENABLED)) {
setBoolean(KEY_ENABLED, false)
setBoolean(KEY_NOTIFY, true)
setBoolean(KEY_AT_JOIN, false)
}
loadJava("views/helper")
loadJava("views/ui")
}

onUnload() {
}

void onMsg(Object msg) {
String content = msg.content
if (content == null || content.isEmpty()) return

// 检查是否包含群成员变动的 XML 特征
if (!content.contains("sysmsgtemplate")) return

if (!msg.isGroupChat()) return

// 检查总开关
boolean enabled = getBoolean(KEY_ENABLED, false)
boolean groupWatched = getBoolean(PRE + msg.talker, false)

if (!enabled) {
return
}

if (!groupWatched) {
return
}

// 处理成员变动
handleMemberChange(msg.talker, content)
}

void onMsgMenu(Object msg) {
addMenuItem("群监控", "", () -> { showMainPanel(); })
}
128 changes: 128 additions & 0 deletions main/plugins/Group Monitoring/views/helper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// ==================== helper.java - 实时监控核心 ====================

// 处理成员变动
void handleMemberChange(String gid, String content) {
if (!getBoolean(PRE + gid, false)) return

// 提取 wxid
String wxid = extractWxid(content)
if (wxid == null || wxid.isEmpty()) return

// 提取昵称
String nickname = extractNickname(content)

// 判断加入/退出
boolean isJoin = content.contains("tmpl_type_profilewithrevoke")
boolean isLeave = content.contains("tmpl_type_profile") && !isJoin

if (isJoin) {
handleJoin(gid, wxid, nickname)
} else if (isLeave) {
handleLeave(gid, wxid, nickname)
}
}

// 处理加入
void handleJoin(String gid, String wxid, String nickname) {
// 获取群昵称
String name = getUserName(gid, wxid)
if (name == null || name.isEmpty()) {
name = nickname // 备用
}
// 进群监控开启时,发送欢迎消息
if (getBoolean(KEY_AT_JOIN, false)) {
String welcome = getString("gm_welcome", "欢迎 @name 加入群聊!")
String msg = welcome.replace("@name", name)
sendText(gid, msg)
}

setLong("gm_last", System.currentTimeMillis() / 1000)
}

// 处理退出
void handleLeave(String gid, String wxid, String nickname) {
// 获取群昵称
String name = getUserName(gid, wxid)
if (name == null || name.isEmpty()) {
name = nickname // 备用
}
// 退群监控开启时,发送通知
if (getBoolean(KEY_NOTIFY, false)) {
String leaveMsg = getString("gm_leave", "@name 退出了群聊")
String msg = leaveMsg.replace("@name", name)
sendText(gid, msg)
}

setLong("gm_last", System.currentTimeMillis() / 1000)
}

// 从 XML 提取 wxid
String extractWxid(String xml) {
try {
// 匹配 revokemsg 中的 wxid
int idx = xml.indexOf("<fromusername>")
if (idx > 0) {
int start = idx + 14
int end = xml.indexOf("</fromusername>", start)
if (end > start) {
return xml.substring(start, end)
}
}

// 匹配 profilewithrevoke 中的 wxid
idx = xml.indexOf("wxid_")
if (idx >= 0) {
int end = idx
while (end < xml.length()) {
char c = xml.charAt(end)
if (c == '<' || c == '&' || c == ' ' || c == '"') break
end++
}
return xml.substring(idx, end)
}
} catch (Exception e) {
}
return null
}

// 从 XML 提取昵称
String extractNickname(String xml) {
try {
// 查找 nickname 标签内的 CDATA
int nickIdx = xml.indexOf("<nickname>")
if (nickIdx >= 0) {
int cdataStart = xml.indexOf("<![CDATA[", nickIdx)
if (cdataStart >= 0 && cdataStart - nickIdx < 20) {
int start = cdataStart + 9 // "<![CDATA[".length()
int end = xml.indexOf("]]>", start)
if (end > start) {
String name = xml.substring(start, end).trim()
if (!name.isEmpty()) {
return name
}
}
}

// 如果没有 CDATA,尝试普通内容
int start = nickIdx + 10
int end = xml.indexOf("</nickname>", start)
if (end > start) {
String name = xml.substring(start, end).trim()
if (!name.isEmpty()) {
return name
}
}
}
} catch (Exception e) {
}
return "未知用户"
}

// 获取成员名称(优先用getUserName)
String getMemberName(String gid, String wxid, String fallback) {
String name = getUserName(gid, wxid)
if (name == null || name.isEmpty()) {
return fallback
}
return name
}
Loading