mirror of https://github.com/jackwener/wx-cli.git
feat: 新增联系人标签查询功能
解析 contact.db 的 contact_label 表和 extra_buffer protobuf Field #30, 支持查询标签列表及指定标签下的成员。 - mcp_server.py: 新增 get_contact_tags / get_tag_members MCP 工具 - monitor_web.py: 新增 /api/tags JSON 端点,支持 ?name= 过滤 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>feat/daemon-cli
parent
b80e7d1c14
commit
7eb29b03e8
1399
mcp_server.py
1399
mcp_server.py
File diff suppressed because it is too large
Load Diff
104
monitor_web.py
104
monitor_web.py
|
|
@ -447,6 +447,96 @@ def load_contact_names():
|
|||
return names
|
||||
|
||||
|
||||
def _extract_pb_field_30(data):
|
||||
"""从 extra_buffer (protobuf) 中提取 Field #30 的字符串值(联系人标签ID)"""
|
||||
if not data:
|
||||
return None
|
||||
pos = 0
|
||||
n = len(data)
|
||||
while pos < n:
|
||||
tag = 0
|
||||
shift = 0
|
||||
while pos < n:
|
||||
b = data[pos]; pos += 1
|
||||
tag |= (b & 0x7f) << shift
|
||||
if not (b & 0x80):
|
||||
break
|
||||
shift += 7
|
||||
field_num = tag >> 3
|
||||
wire_type = tag & 0x07
|
||||
if wire_type == 0:
|
||||
while pos < n and data[pos] & 0x80:
|
||||
pos += 1
|
||||
pos += 1
|
||||
elif wire_type == 2:
|
||||
length = 0; shift = 0
|
||||
while pos < n:
|
||||
b = data[pos]; pos += 1
|
||||
length |= (b & 0x7f) << shift
|
||||
if not (b & 0x80):
|
||||
break
|
||||
shift += 7
|
||||
if field_num == 30:
|
||||
try:
|
||||
return data[pos:pos + length].decode('utf-8')
|
||||
except Exception:
|
||||
return None
|
||||
pos += length
|
||||
elif wire_type == 1:
|
||||
pos += 8
|
||||
elif wire_type == 5:
|
||||
pos += 4
|
||||
else:
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def load_contact_tags():
|
||||
"""加载联系人标签及其成员"""
|
||||
try:
|
||||
conn = sqlite3.connect(CONTACT_CACHE)
|
||||
try:
|
||||
label_rows = conn.execute(
|
||||
"SELECT label_id_, label_name_, sort_order_ FROM contact_label ORDER BY sort_order_"
|
||||
).fetchall()
|
||||
except Exception:
|
||||
conn.close()
|
||||
return []
|
||||
if not label_rows:
|
||||
conn.close()
|
||||
return []
|
||||
|
||||
labels = {}
|
||||
for lid, lname, sort_order in label_rows:
|
||||
labels[lid] = {'id': lid, 'name': lname, 'sort_order': sort_order, 'members': []}
|
||||
|
||||
names = load_contact_names()
|
||||
rows = conn.execute(
|
||||
"SELECT username, extra_buffer FROM contact WHERE extra_buffer IS NOT NULL"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
|
||||
for username, buf in rows:
|
||||
label_str = _extract_pb_field_30(buf)
|
||||
if not label_str:
|
||||
continue
|
||||
display = names.get(username, username)
|
||||
for lid_s in label_str.split(','):
|
||||
try:
|
||||
lid = int(lid_s.strip())
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
if lid in labels:
|
||||
labels[lid]['members'].append({'username': username, 'display_name': display})
|
||||
|
||||
result = sorted(labels.values(), key=lambda t: t['sort_order'])
|
||||
for t in result:
|
||||
t['member_count'] = len(t['members'])
|
||||
return result
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def format_msg_type(t):
|
||||
return {
|
||||
1: '文本', 3: '图片', 34: '语音', 42: '名片',
|
||||
|
|
@ -1849,6 +1939,20 @@ class Handler(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
elif self.path.startswith('/api/tags'):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
name_filter = params.get('name', [''])[0].strip().lower()
|
||||
|
||||
tags = load_contact_tags()
|
||||
if name_filter:
|
||||
tags = [t for t in tags if name_filter in t['name'].lower()]
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(tags, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
elif self.path == '/stream':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/event-stream')
|
||||
|
|
|
|||
Loading…
Reference in New Issue