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
|
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):
|
def format_msg_type(t):
|
||||||
return {
|
return {
|
||||||
1: '文本', 3: '图片', 34: '语音', 42: '名片',
|
1: '文本', 3: '图片', 34: '语音', 42: '名片',
|
||||||
|
|
@ -1849,6 +1939,20 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(data)
|
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':
|
elif self.path == '/stream':
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'text/event-stream')
|
self.send_header('Content-Type', 'text/event-stream')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue