v2: project cleanup, Desktop paths fixed, README updated, serve daemon added, mohe-xxm protocol documented

This commit is contained in:
hmo
2026-05-20 03:51:59 +08:00
parent 9e4c50a8a7
commit 3425ded733
10 changed files with 1090 additions and 163 deletions
+225 -45
View File
@@ -11,19 +11,19 @@ Windows 微信机器人 ↔ Linux Hermes AI,全自动双向聊天。
│ Windows 192.168.0.111 │
│ │
│ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ 微信 3.9.5.81 x64 │ │ 日常微信 WeChatAppEx 4.x │ │
│ │ 微信 3.9.10.19 x64 │ │ 日常微信 WeChatAppEx 4.x │ │
│ │ 机器人号 modachen │ │ 老爸日常使用,互不干扰 │ │
│ │ wxhook Bot类TCP │ └───────────────────────────┘ │
│ │ wxhelper DLL 注入 │ └───────────────────────────┘ │
│ └────────┬─────────┘ │
│ │ wxhook DLL 收消息
│ │ wxhelper TCP (:19099) 收消息
│ ┌────────▼────────────────┐ │
│ │ wechat_agent.py │ ← 常驻进程 │
│ │ wechat_agent.py v2 │ ← 常驻进程 │
│ │ │ │
│ │ Bot类接收微信事件(TCP) │ │
│ │ → POST Hermes API :8642 │ ← 带session: sisyphus
│ │ ← 收回复 → wxhook发回 │ │
│ │ :5801 ←Hermes找小小莫 │ ← 双向通道
│ │ 看门狗防 wxhook 挂掉 │ │
│ │ TCP 接收微信事件 │ │
│ │ → POST Hermes API :8642 │ ← sisyphus session
│ │ ← 收回复 → wxhelper 发 │ │
│ │ HTTP :19088 收发消息 │
│ │ 看门狗自愈 │ │
│ └────────┬────────────────┘ │
└───────────┼─────────────────────────────────────────────────┘
│ HTTP (局域网)
@@ -42,18 +42,53 @@ Windows 微信机器人 ↔ Linux Hermes AI,全自动双向聊天。
└──────────────────────────────────────────────────────────────┘
```
## 双向通道:小小莫 ↔ 莫荷
小小莫(Sisyphus)和莫荷(Hermes)通过 OpenCode session 实现双向沟通,不依赖微信。
### 通信方式
```
莫荷 --run --attach--> session (serve :4096)
小小莫 --session_search--> 读取莫荷消息
小小莫 --TUI 回复--> session(以 [xxm] 开头)
老莫 --询问小小莫--> 得知对话内容
```
### 协议
| 前缀 | 发送者 | 说明 |
|------|--------|------|
| `[mohe]` | 莫荷 | 通过 `run --attach` 写入 session |
| `[xxm]` | 小小莫 | 在 TUI 中回复莫荷时使用 |
### 数据流
```
莫荷想找小小莫
→ Linux 执行 opencode run --attach http://192.168.0.111:4096
→ 发消息带 [mohe] 前缀
→ 写入 Windows 的 opencode serve session
→ 小小莫通过 session_search 查到新消息
→ 在 TUI 中以 [xxm] 前缀回复
→ 莫荷可以通过 export/session 读到回复
```
---
## 数据流
### 文字消息
```
老爸发微信
→ WeChat 3.9.5.81 收到
→ wxhook DLL TCP → Bot 类 → on_msg 处理器
→ WeChat 3.9.10.19 收到
→ wxhelper DLL TCP (:19099) 通知
→ wechat_agent.py POST Hermes API (:8642)
→ X-Hermes-Session-Id: sisyphus (固定)
→ Hermes 处理 → 返回回复
→ wechat_agent.py 收回复 → wxhook API 发回
→ wechat_agent.py 收回复 → wxhelper API (:19088) 发回
→ 老爸手机收到
```
@@ -61,7 +96,7 @@ Windows 微信机器人 ↔ Linux Hermes AI,全自动双向聊天。
```
老爸发图片
→ WeChat 收到 → wxhook IMAGE_MESSAGE 事件
→ WeChat 收到 → wxhelper 图片事件
→ wechat_agent.py 保存图片 → 调豆包OCR (VolcEngine)
→ OCR 文字结果 + 通知 → POST Hermes API
→ Hermes 知道图片内容 → 回复老爸
@@ -86,19 +121,22 @@ Hermes → POST http://192.168.0.111:5801/hermes-msg
| 端口 | 用途 | 所在 |
|------|------|------|
| 19001 | wxhook HTTP API | Windows |
| 19088 | wxhelper HTTP API (收发消息) | Windows |
| 19099 | wxhelper TCP 事件推送 | Windows |
| 5801 | Hermes→小小莫 消息入口 | Windows |
| 8642 | Hermes API Server (OpenAI兼容) | Linux |
| 5800 | bridge.py (已废弃) | Linux |
| 19001 | History REST API (独立启动) | Windows |
## 组件
### Windows 端(wechat_agent.py
- **wxhook Bot 类** — DLL 注入 + TCP 收消息
### Windows 端(wechat_agent.py v2
- **wxhelper DLL 注入** — ttttupup/wxhelper 3.9.10.19 x64
- **TCP 接收消息** — :19099 收微信事件
- **HTTP 发送消息** — :19088 wxhelper API
- **Hermes API 调用** — 直接 POST :8642session 固定 `sisyphus`
- **回复服务** — 5801 端口收 Hermes 消息
- **看门狗** — 2 分钟无消息自动刷新 webhookAPI 挂了才重注入 DLL
- **昵称缓存** — 从 wxhook getContactList 获取联系人昵称
- **看门狗** — 120s 无消息刷新 webhookAPI 挂了才重注入 DLL
- **双向通道** — 莫荷通过 `opencode run --attach` 与小小莫沟通
### Linux 端(Hermes Gateway
- **API Server** — 0.0.0.0:8642Bearer auth
@@ -142,7 +180,7 @@ curl http://127.0.0.1:8642/v1/models
| 方向 | 方式 | 示例 |
|------|------|------|
| 小小莫 → Hermes | POST :8642/v1/chat/completions | 带 `X-Hermes-Session-Id: sisyphus` |
| Hermes → 小小莫 | POST :5801/hermes-msg | 写入 `C:\Users\hmo\Desktop\hermes_inbox.txt` |
| Hermes → 小小莫 | POST :5801/hermes-msg | 写入 `temp/hermes_inbox.txt` |
| 老爸 ↔ Hermes | 微信聊天 | 自动通过 wechat_agent.py 桥接 |
## 项目文件
@@ -150,12 +188,155 @@ curl http://127.0.0.1:8642/v1/models
```
wechat-hermes-gateway/
├── README.md # 本文档
├── api/
│ └── history_api.py # History REST API :19001
├── scripts/
│ ├── wechat_agent.py # 主力:微信机器人代理
── start_bridge.bat # 一键启动脚本
── start_bridge.bat # 微信桥接一键启动
│ ├── start_history_api.bat # History API 一键启动
│ ├── moho_view.py # 莫荷聊天记录查看器
│ └── moho_chat.py # 莫荷聊天查看器(备选)
└── temp/ # 废弃/临时脚本
```
## History REST API (:19001)
独立的 HTTP REST API 服务器,可以直接查询微信聊天记录。
### 启动方式
```batch
cd D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway
scripts\start_history_api.bat
```
或:
```powershell
$env:PYTHONHOME=''
python api\history_api.py --port 19001
```
**前提条件**: `wechat_agent.py` 已启动,wxhelper DLL 已注入。
### API 端点
| 方法 | 路径 | 说明 | 参数 |
|------|------|------|------|
| GET | `/` | API 信息 | - |
| GET | `/health` | 健康检查(含 wxhelper 状态) | - |
| GET | `/api/contacts` | 所有联系人列表 | - |
| GET | `/api/recent` | 最近聊天列表 | `?limit=20` |
| GET | `/api/history` | 查询聊天记录 | `?wxid=wxid_xxx&count=20` |
| POST | `/api/history` | 同上(JSON body | `{"wxid":"wxid_xxx","count":20}` |
### 响应格式
```json
{
"ok": true,
"wxid": "wxid_c0a6izmwd78y22",
"sender_name": "莫语不语",
"count": 5,
"messages": [
{
"time": "2026-05-19 10:30:00",
"timestamp": 1716153000,
"sender": "莫语不语",
"is_self": false,
"type": 1,
"type_name": "text",
"content": "消息内容..."
}
]
}
```
### 典型用法
```powershell
# 获取老爸的最近聊天记录
curl http://localhost:19001/api/history?wxid=wxid_c0a6izmwd78y22&count=20
# 获取联系人列表(人类可读)
curl http://localhost:19001/api/contacts
# 获取最近活跃的聊天
curl http://localhost:19001/api/recent?limit=10
# POST 方式
curl -X POST http://localhost:19001/api/history -H "Content-Type: application/json" -d '{"wxid":"wxid_c0a6izmwd78y22","count":50}'
```
## History REST API (:19001)
提供直接 HTTP REST 接口查询微信历史聊天记录,供程序化读取和记忆系统使用。
### 启动
```batch
cd D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway
scripts\start_history_api.bat
```
或直接:
```powershell
$env:PYTHONHOME=''
python api\history_api.py --port 19001
```
> 依赖:需要 `wechat_agent.py` 先启动(微信已登录 + wxhelper DLL 已注入),因为 API 通过 wxhelper (:19088) 查询数据库。
### API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/` | API 信息 |
| GET | `/health` | 健康检查(含 wxhelper 连接状态) |
| GET | `/api/contacts` | 获取所有微信联系人列表 |
| GET | `/api/history?wxid=X&count=20` | 查询与某联系人的聊天记录 |
| POST | `/api/history` | 同上,JSON body: `{"wxid":"X","count":20}` |
| GET | `/api/recent` | 最近有消息的联系人列表 |
### 响应格式
```json
{
"ok": true,
"wxid": "wxid_c0a6izmwd78y22",
"sender_name": "莫语不语",
"count": 20,
"messages": [
{
"time": "2026-05-19 10:30:00",
"timestamp": 1716153000,
"sender": "莫语不语",
"is_self": false,
"type": 1,
"type_name": "text",
"content": "今天吃了吗"
}
]
}
```
### curl 示例
```bash
# 健康检查
curl http://localhost:19001/health
# 获取联系人列表
curl http://localhost:19001/api/contacts
# 查询老爸聊天记录
curl "http://localhost:19001/api/history?wxid=wxid_c0a6izmwd78y22&count=20"
# POST 方式
curl -X POST http://localhost:19001/api/history -H "Content-Type: application/json" -d '{"wxid":"wxid_c0a6izmwd78y22","count":10}'
```
## 历史决策
1. **wxhook HTTP webhook 不可靠** → 改用 Bot 类 TCP 收消息
@@ -178,18 +359,19 @@ wechat-hermes-gateway/
| Hermes 身份认知 | ✅ 知道自己是莫荷/莫小荷,知道老爸 |
| 会话上下文持续 | ✅ session `sisyphus`,自动重置已关闭 |
| 小小莫 ↔ Hermes 双向通信 | ✅ API (:8642) + HTTP (:5801/hermes-msg) |
| wxhook 看门狗自愈 | ✅ 2分钟无消息刷新 webhook |
| 看门狗自愈 | ✅ 120s 无消息刷新 webhook |
| 昵称识别 | ✅ 从 getContactList 获取 |
| 联系人列表查询 | ✅ wxhook /api/getContactList |
| 联系人列表查询 | ✅ wxhelper /api/getContactList |
| 历史聊天记录查询 | ✅ [HISTORY:wxid:count] 标签 → MSG0.db SQL |
## 未实现 / 不可行
| 功能 | 原因 |
|------|------|
| 语音消息(STT) | wxhook 不支持语音提取 |
| 发送本地图片/文件 | bot.send_image API 已通,回复链路待完善 |
| 换头像/改资料 | wxhook 无相关 API |
| 群管理 | wxhook 群 API 有限 |
| 语音消息(STT) | wxhelper 不支持语音提取 |
| 发送本地图片/文件 | 功能已通,回复链路待完善 |
| 换头像/改资料 | wxhelper 无相关 API |
| 群管理 | wxhelper 群 API 有限 |
| iLink 官方 bot 接口 | 限制太多,弃用 |
| 多人独立会话 | 目前全部共享 `sisyphus` 单会话 |
@@ -197,24 +379,22 @@ wechat-hermes-gateway/
### 场景:Windows 重启
需要 3 步,**顺序不能错**
按顺序执行
```
第1步:双击 start_bridge.bat
→ 自动杀旧微信 → Bot 类启动新微信 + 注入 DLL
→ 等待微信窗口出现
1. 双击 start-opencode-serve.bat(项目根目录)
→ 启动 opencode serve:4096,莫荷连接用)
→ 启动后台守护(自动清理僵尸连接)
第2步:运行修复过低工具
→ 选择修复过低6.0\低版通用杀器.sp.exe
→ 自动扫描到已运行的微信 → 打补丁 → 弹出登录二维码
2. 打开经典微信 3.9.10.19
→ 扫码登录机器人号 modachenchen
第3步:手机扫码登录
→ 登录后 wechat_agent.py 自动检测到登录状态
→ 开始转发消息
→ 给 filehelper 发 "[Bridge] online" 确认
3. wechat_agent.py 在登录后自动注入 DLL
→ 自动开始转发消息
→ 日志在 projects/wechat-hermes-gateway/logs/
```
验证:手机发条消息给 modachenchen,看能否收到自动回复。
验证:微信上给 modachenchen 发条消息,看 Hermes 是否回复。
### 场景:Linux 重启
@@ -251,13 +431,13 @@ curl http://192.168.0.103:8642/v1/models -H "Authorization: Bearer hermes123"
|------|------|
| Gateway 偶尔 hang | 已修复 --replace 冲突,改用 systemd 管理 |
| 生图 API 有时较慢 | 商汤 SenseNova,首次调用需加载模型 |
| 语音转文字 | wxhook 不支持语音提取,暂不可行 |
| 语音转文字 | wxhelper 不支持语音提取,暂不可行 |
## 注意事项
- wxhook DLL 支持 x64 微信 3.9.5.81
- 每次 WeChat 重启需重新登录(修复过低工具)
- **start_bridge.bat 必须第 1 步执行**,修复工具第 2 步
- wxhelper DLL 支持 3.9.10.19 x64 微信
- 每次 WeChat 重启需重新登录
- 启动顺序:先开微信 → agent 自动注入 DLL
- Hermes API 首次调用可能较慢(大模型冷启动)
- 看门狗刷新 webhook不会误伤正常消息处理
- 如果微信登录后没反应,等 1-2 分钟看门狗会自动刷新
- 看门狗每 120s 刷新 webhookAPI 挂了自动重注入
- 如果微信登录后没反应,等 1-2 分钟看门狗会自动处理
+378
View File
@@ -0,0 +1,378 @@
"""
WeChat History REST API Server
Starts on port 19001. Queries WeChat chat history via wxhelper DLL (http://127.0.0.1:19088).
Usage:
python history_api.py # start on 0.0.0.0:19001
python history_api.py --port 19001 # explicit port
Endpoints:
GET / → API info
GET /health → health check
GET /api/contacts → list WeChat contacts (wxid + nickname)
GET /api/history?wxid=wxid_xxx&count=20 → query chat history
POST /api/history → same via JSON body {"wxid":"...","count":20}
Requires: wxhelper DLL injected and WeChat running (wechat_agent.py handles this).
"""
import os
import json
import time
import urllib.request
import urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from datetime import datetime
os.environ["no_proxy"] = "*"
os.environ["NO_PROXY"] = "*"
# ── Configuration ──
WX_API = "http://127.0.0.1:19088"
DEFAULT_PORT = 19001
BOT_WXID = "wxid_7onnerpx2s2l22"
HOST = "0.0.0.0"
# ── Cached state ──
nickname_cache = {}
db_handle_cache = None
# ── wxhelper API helpers ──
def wxpost(path, data=None, timeout=10):
"""Call wxhelper HTTP API."""
try:
body = json.dumps(data or {}).encode()
req = urllib.request.Request(
WX_API + path,
data=body,
headers={"Content-Type": "application/json"}
)
r = urllib.request.urlopen(req, timeout=timeout)
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
return json.loads(e.read().decode()) if e.code else {"code": -1}
except Exception as e:
return {"code": -1, "error": str(e)[:200]}
def get_db_handle():
"""Get handle for MSG*.db database containing MSG table. Cached after first call."""
global db_handle_cache
if db_handle_cache:
return db_handle_cache
r = wxpost("/api/getDBInfo", timeout=10)
dbs = r.get("data") or []
for db in dbs:
dbname = db.get("databaseName", "")
if "MSG" in dbname or "Msg" in dbname:
db_handle_cache = db.get("handle")
return db_handle_cache
return None
def get_nickname(wxid):
"""Get contact nickname from wxid, with caching."""
if wxid in nickname_cache:
return nickname_cache[wxid]
r = wxpost("/api/getContactList", timeout=10)
for c in (r.get("data") or []):
if c.get("wxid") == wxid:
nick = c.get("nickname") or c.get("customAccount") or wxid
nickname_cache[wxid] = nick
return nick
nickname_cache[wxid] = wxid
return wxid
def query_history(wxid, limit=10):
"""Query historical text messages from MSG table for a contact.
Returns list of dicts: [{CreateTime, IsSender, Type, content}, ...]"""
h = get_db_handle()
if not h:
return None
limit_val = min(int(limit), 200)
sql = (
f"SELECT CreateTime, IsSender, Type, SubType, StrContent, DisplayContent "
f"FROM MSG WHERE StrTalker='{wxid}' AND Type IN (1,49) "
f"ORDER BY CreateTime DESC LIMIT {limit_val}"
)
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
data = r.get("data") or []
if not data or len(data) < 2:
return None
# Skip header row, reverse to chronological order
rows = data[1:]
rows.reverse()
results = []
for row in rows:
content = (row[4] or "").strip() if len(row) > 4 else ""
if not content and len(row) > 5:
content = (row[5] or "").strip()
if not content:
continue
results.append({
"CreateTime": row[0],
"IsSender": row[1],
"Type": row[2],
"content": content
})
return results
def format_history_json(wxid, rows):
"""Format raw MSG rows into JSON-serializable dict for API response."""
sender_name = get_nickname(wxid)
bot_name = get_nickname(BOT_WXID)
if not rows:
return {
"ok": True, "wxid": wxid, "sender_name": sender_name,
"count": 0, "messages": []
}
messages = []
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else ""
is_sender = int(row.get("IsSender", 0))
msg_type = int(row.get("Type", 1))
content = row.get("content", "")
messages.append({
"time": time_str,
"timestamp": ts,
"sender": bot_name if is_sender else sender_name,
"is_self": bool(is_sender),
"type": msg_type,
"type_name": {1: "text", 49: "link"}.get(msg_type, f"type_{msg_type}"),
"content": content[:500]
})
return {
"ok": True,
"wxid": wxid,
"sender_name": sender_name,
"count": len(messages),
"messages": messages
}
def get_contacts():
"""Get all contacts from WeChat."""
r = wxpost("/api/getContactList", timeout=10)
contacts = r.get("data") or []
# Update cache
for c in contacts:
wxid = c.get("wxid", "")
nick = c.get("nickname") or c.get("customAccount") or wxid
nickname_cache[wxid] = nick
return [
{
"wxid": c.get("wxid", ""),
"nickname": c.get("nickname", ""),
"remark": c.get("remark", ""),
"customAccount": c.get("customAccount", ""),
}
for c in contacts
]
def get_recent_chats(limit=20):
"""Get list of contacts with recent messages."""
h = get_db_handle()
if not h:
return []
sql = (
f"SELECT StrTalker, MAX(CreateTime) as last_time, COUNT(*) as msg_count "
f"FROM MSG WHERE Type IN (1,49) "
f"GROUP BY StrTalker ORDER BY last_time DESC LIMIT {min(limit, 50)}"
)
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
data = r.get("data") or []
if not data or len(data) < 2:
return []
results = []
for row in data[1:]:
wxid = (row[0] or "").strip()
if not wxid or wxid in ("fmessage", "weixin", "wechat", "filehelper"):
continue
if wxid.startswith("gh_"):
continue
ts = int(row[1]) if row[1] else 0
count = int(row[2]) if len(row) > 2 and row[2] else 0
results.append({
"wxid": wxid,
"nickname": get_nickname(wxid),
"last_message_time": datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else None,
"last_message_ts": ts,
"message_count": count,
})
return results
# ── HTTP Request Handler ──
class HistoryAPIHandler(BaseHTTPRequestHandler):
def _send_json(self, data, status=200):
"""Send JSON response with proper headers."""
body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def _send_error_json(self, msg, status=400):
self._send_json({"ok": False, "error": msg}, status=status)
def _read_json_body(self):
"""Read and parse JSON request body."""
length = int(self.headers.get("Content-Length", 0))
if not length:
return {}
try:
body = self.rfile.read(length)
return json.loads(body)
except json.JSONDecodeError:
return {}
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
params = parse_qs(parsed.query)
# ── Root / Health ──
if path in ("/", ""):
self._send_json({
"service": "WeChat History API",
"version": "1.0",
"port": DEFAULT_PORT,
"wxhelper": WX_API,
"endpoints": {
"GET /api/contacts": "List all WeChat contacts",
"GET /api/history": "Query chat history (params: wxid, count)",
"POST /api/history": "Same via JSON body",
"GET /api/recent": "Recent chats list",
"GET /health": "Health check",
}
})
return
if path == "/health":
self._send_json({
"status": "ok",
"timestamp": datetime.now().isoformat(),
"wxhelper": bool(get_db_handle())
})
return
# ── Contacts ──
if path == "/api/contacts":
contacts = get_contacts()
self._send_json({"ok": True, "count": len(contacts), "contacts": contacts})
return
# ── Recent Chats ──
if path == "/api/recent":
limit = int(params.get("limit", ["20"])[0])
chats = get_recent_chats(limit)
self._send_json({"ok": True, "count": len(chats), "chats": chats})
return
# ── History ──
if path == "/api/history":
wxid = params.get("wxid", [""])[0]
count = params.get("count", ["10"])[0]
if not wxid:
self._send_error_json("Missing required parameter: wxid")
return
try:
rows = query_history(wxid, count)
result = format_history_json(wxid, rows)
self._send_json(result)
except Exception as e:
self._send_error_json(str(e)[:200], status=500)
return
# ── 404 ──
self._send_error_json(f"Not found: {path}", status=404)
def do_POST(self):
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
body = self._read_json_body()
# ── History (POST) ──
if path in ("/api/history", "/history"):
wxid = (body.get("wxid", "") or "").strip()
count = body.get("count", 10) or body.get("limit", 10)
if not wxid:
self._send_error_json("Missing required field: wxid")
return
try:
rows = query_history(wxid, count)
result = format_history_json(wxid, rows)
self._send_json(result)
except Exception as e:
self._send_error_json(str(e)[:200], status=500)
return
# ── 404 ──
self._send_error_json(f"Not found: {path}", status=404)
def do_OPTIONS(self):
"""Handle CORS preflight."""
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def log_message(self, *args):
# Suppress default access logs to stdout
pass
# ── Main ──
def main():
import sys
port = DEFAULT_PORT
if "--port" in sys.argv:
idx = sys.argv.index("--port")
if idx + 1 < len(sys.argv):
port = int(sys.argv[idx + 1])
# Check wxhelper connectivity
print(f"[History API] Checking wxhelper at {WX_API}...")
try:
r = wxpost("/api/checkLogin", timeout=5)
if r.get("code") == 1:
print("[History API] wxhelper ONLINE")
db_handle = get_db_handle()
print(f"[History API] DB handle: {db_handle or 'NOT FOUND'}")
else:
print(f"[History API] WARNING: wxhelper not logged in: {r}")
except Exception as e:
print(f"[History API] WARNING: Cannot reach wxhelper: {e}")
# Start server
server = HTTPServer((HOST, port), HistoryAPIHandler)
print(f"[History API] Listening on http://{HOST}:{port}")
print(f"[History API] Endpoints:")
print(f" GET http://localhost:{port}/")
print(f" GET http://localhost:{port}/health")
print(f" GET http://localhost:{port}/api/contacts")
print(f" GET http://localhost:{port}/api/recent")
print(f" GET http://localhost:{port}/api/history?wxid=wxid_xxx&count=20")
print(f" POST http://localhost:{port}/api/history")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[History API] Shutting down...")
server.shutdown()
if __name__ == "__main__":
main()
-75
View File
@@ -1,75 +0,0 @@
"""
[DEPRECATED] 早期架构组件,已由 wechat_agent.py 直调 Hermes API 替代。
不再使用,仅作为架构参考保留。
"""
import pymem, pymem.process, requests, time, json, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
DLL = r"C:\Users\hmo\AppData\Local\Programs\Python\Python310\Lib\site-packages\wxhook\tools\wxhook.dll"
WX = "http://127.0.0.1:19088"
HERMES = "http://192.168.0.103:5800/callback"
LOG = r"C:\Users\hmo\Desktop\bridge.log"
BOT = "wxid_7onnerpx2s2l22"
PORT = 5801
def log(m):
with open(LOG, "a", encoding="utf-8") as f:
f.write(f"{time.strftime('%H:%M:%S')} {m}\n")
# Inject DLL
try:
requests.post(f"{WX}/api/checkLogin", json={}, timeout=3)
log("DLL OK")
except:
pm = pymem.Pymem("WeChat.exe")
pymem.process.inject_dll(pm.process_handle, DLL.encode())
time.sleep(2)
log("DLL injected")
log(f"login: {requests.post(f'{WX}/api/checkLogin', json={}, timeout=5).json()}")
# Periodic webhook refresh (every 30 seconds)
def webhook_keepalive():
while True:
try:
requests.post(f"{WX}/api/hookSyncMsg", json={
"port": PORT, "ip": "0.0.0.0", "enableHttp": 1,
"url": f"http://127.0.0.1:{PORT}", "timeout": 300
}, timeout=5)
except:
pass
time.sleep(30)
threading.Thread(target=webhook_keepalive, daemon=True).start()
log("keepalive started (30s)")
# Test message
requests.post(f"{WX}/api/sendTextMsg",
json={"wxid": "filehelper", "msg": "[Bridge] online"}, timeout=5)
class H(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
try:
d = json.loads(body)
fu = d.get("fromUser", "")
ct = d.get("content", "")
if fu and ct and fu != BOT:
log(f"MSG {fu}: {ct[:60]}")
threading.Thread(target=lambda: requests.post(HERMES, json=[{"id": int(time.time()*1000), "type": 1, "content": ct, "sender": fu, "roomid": "", "ts": time.time()}], timeout=30), daemon=True).start()
self.send_response(200); self.end_headers(); return
to = d.get("to", "") or d.get("wxid", "")
msg = d.get("message", "") or d.get("content", "")
if to and msg:
r = requests.post(f"{WX}/api/sendTextMsg", json={"wxid": to, "msg": msg}, timeout=5)
log(f"SEND {to}: {r.get('msg','')}")
self.send_response(200); self.end_headers(); return
except Exception as e:
log(f"ERR: {str(e)[:80]}")
self.send_response(200); self.end_headers()
def do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(b'{"ok":true}')
def log_message(self, *a): pass
log(f"ready on :{PORT}")
HTTPServer(("0.0.0.0", PORT), H).serve_forever()
+118
View File
@@ -0,0 +1,118 @@
# Mohe & XiaoXiaoMo Chat Viewer
# Lists [mohe]/[xxm] messages from an OpenCode session.
#
# Usage:
# .\moho_chat.ps1 <session_id> [minutes]
#
# Examples:
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k 30
param(
[Parameter(Mandatory=$true)][string]$SessionId,
[int]$Minutes = 0
)
$ErrorActionPreference = 'Stop'
Write-Host "Exporting session..." -ForegroundColor DarkGray
$tmpFile = [System.IO.Path]::GetTempFileName() + ".json"
$env:CI = 'true'
opencode.cmd export $SessionId 2>$null | Set-Content -Path $tmpFile -NoNewline -Encoding UTF8
if (-not (Test-Path $tmpFile) -or (Get-Item $tmpFile).Length -eq 0) {
Write-Error "Export failed"
exit 1
}
$size = (Get-Item $tmpFile).Length
Write-Host "Read ($($size / 1MB -as [int]) MB)" -ForegroundColor DarkGray
$raw = Get-Content $tmpFile -Raw -Encoding UTF8
Remove-Item $tmpFile -Force
# Check if it's actually UTF-16LE in disguise (opencode export uses UTF-16LE)
if ($raw.Length -eq 0 -or $raw[0] -ne '{') {
# Re-read as UTF-16
$bytes = [System.IO.File]::ReadAllBytes($tmpFile)
Remove-Item $tmpFile -Force
$raw = [System.Text.Encoding]::Unicode.GetString($bytes)
}
$brace = $raw.IndexOf('{')
if ($brace -gt 0) { $raw = $raw.Substring($brace) }
# Use regex to find "text": "[mohe]..." patterns directly (no JSON parsing)
$pattern = '"text":\s*"\[(mohe|xxm)\][^"]*"'
$cutoff = if ($Minutes -gt 0) { (Get-Date).ToUniversalTime().AddMinutes(-$Minutes) } else { $null }
$results = @()
$matchPos = 0
while ($true) {
$m = [regex]::Match($raw, $pattern, $matchPos)
if (-not $m.Success) { break }
$matchPos = $m.Index + 1
$tag = $m.Groups[1].Value
$content = $m.Value
# Extract the full text content (everything between "text": " and the closing ")
$start = $m.Index + $m.Value.IndexOf('"', $m.Value.IndexOf(':')+1) + 1
$fullText = ''
$escape = $false
for ($i = $start; $i -lt $raw.Length; $i++) {
$c = $raw[$i]
if ($escape) { $fullText += $c; $escape = $false }
elseif ($c -eq '\') { $fullText += $c; $escape = $true }
elseif ($c -eq '"') { break }
else { $fullText += $c }
}
$fullText = $fullText.Trim()
if (-not ($fullText.StartsWith('[mohe]') -or $fullText.StartsWith('[xxm]'))) { continue }
$sender = if ($fullText.StartsWith('[mohe]')) { '[mohe] Mohe' } else { '[xxm] XiaoXiaoMo' }
$display = $fullText -replace '^\[\w+\]\s*', ''
# Get timestamp by looking backward
$before = $raw.Substring([Math]::Max(0, $m.Index - 3000), [Math]::Min(3000, $m.Index))
$tsMatch = [regex]::Match($before, '"timestamp":\s*"([^"]+)"')
$tsStr = if ($tsMatch.Success) { $tsMatch.Groups[1].Value } else { '' }
if (-not $tsStr) {
$crMatch = [regex]::Match($before, '"created":\s*(\d+)')
if ($crMatch.Success) {
$tsStr = ([DateTimeOffset]::FromUnixTimeMilliseconds([long]$crMatch.Groups[1].Value).UtcDateTime).ToString('o')
}
}
$ts = $null
if ($tsStr) {
try { $ts = [DateTime]::Parse($tsStr, $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) } catch {}
}
if ($cutoff -and $ts -and $ts -lt $cutoff) { continue }
$timeLocal = if ($ts) { $ts.ToLocalTime().ToString('HH:mm:ss') } else { '??' }
$results += [PSCustomObject]@{ Time=$timeLocal; Sort=if($ts){$ts.Ticks}else{0}; Sender=$sender; Message=$display }
}
$results = $results | Sort-Object Sort
if ($results.Count -eq 0) {
Write-Host "No [mohe]/[xxm] messages found" -ForegroundColor Yellow
if ($Minutes -gt 0) { Write-Host "(last $Minutes minutes)" }
exit
}
Write-Host "`n$($results.Count) message(s)" -ForegroundColor Cyan
if ($Minutes -gt 0) { Write-Host "(last $Minutes min)" }
Write-Host ("=" * 60)
foreach ($r in $results) {
Write-Host ("[{0}] {1}: {2}" -f $r.Time, $r.Sender, $r.Message)
}
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Mohe & XiaoXiaoMo Chat Viewer
Fixes one corrupted byte in session export, then parses cleanly.
Usage:
python moho_chat.py <session_id> [minutes]
Examples:
python moho_chat.py ses_1d95d15c4ffehQaZ6hrbIbak5k
python moho_chat.py ses_1d95d15c4ffehQaZ6hrbIbak5k 30
"""
import subprocess, sys, os, tempfile, re, json
from datetime import datetime, timezone, timedelta
def export(session_id):
"""Export session via cmd.exe (preserves UTF-16LE), return path."""
tmp = tempfile.NamedTemporaryFile(suffix='.json', delete=False)
tmp.close()
r = subprocess.run(
f'opencode.cmd export {session_id} > "{tmp.name}" 2>nul',
shell=True, timeout=180
)
if r.returncode != 0 or os.path.getsize(tmp.name) == 0:
raise ValueError('Export failed')
return tmp.name
def load(filepath):
"""Read UTF-16LE, fix corruption, parse JSON, return messages."""
with open(filepath, 'rb') as f:
raw = f.read()
# Decode UTF-16LE
if raw[:2] == b'\xff\xfe':
text = raw.decode('utf-16-le', errors='replace')
else:
text = raw.decode('utf-8', errors='replace')
# Strip to first {
brace = text.find('{')
if brace > 0:
text = text[brace:]
# FIX THE CORRUPTION:
# The first message's text field is missing its closing quote.
# Pattern: corrupt_char, comma, newline, then a field name
# Insert missing closing quote before the comma
# Specific fix: after corrupted chars that precede a structural comma
fixed = re.sub(
r'(\uFFFD)\s*,\s*\n\s*"(id|role|parts|info|timestamp|type|text)"',
r'?",\n "\2"',
text
)
# Also try without \uFFFD (in case it was decoded differently)
fixed = re.sub(
r'(\?)\s*,\s*\n\s*"(id|role|parts|info|timestamp|type|text)"',
r'?",\n "\2"',
fixed
)
return json.loads(fixed).get('messages', [])
def msg_text(msg):
parts = msg.get('parts', [])
text = ''
for p in parts:
if isinstance(p, dict) and p.get('type') == 'text':
text += p.get('text', '')
return text.strip()
def msg_ts(msg):
info = msg.get('info', {})
ts = info.get('timestamp', '') or ''
if not ts:
t = info.get('time', {})
if isinstance(t, dict) and t.get('created'):
ts = datetime.fromtimestamp(t['created']/1000, tz=timezone.utc).isoformat()
return ts
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
session_id = sys.argv[1]
minutes = int(sys.argv[2]) if len(sys.argv) > 2 else None
print('Exporting...', file=sys.stderr)
tmp = export(session_id)
try:
messages = load(tmp)
finally:
os.unlink(tmp)
print(f'{len(messages)} messages', file=sys.stderr)
# Search for 测试新协议 (Moho's test message keyword)
found_moho = 0
for m in messages:
t = msg_text(m)
if '测试新协议' in t or '写入Session' in t or '23 × 17' in t:
found_moho += 1
if found_moho <= 5:
print(f' [Moho #{found_moho}] {t[:200]!r}', file=sys.stderr)
print(f' Moho explicit messages: {found_moho}', file=sys.stderr)
cutoff = None
if minutes:
cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes)
results = []
for m in messages:
text = msg_text(m)
if not (text.startswith('[mohe]') or text.startswith('[xxm]')):
continue
sender = '[mohe] Mohe' if text.startswith('[mohe]') else '[xxm] XiaoXiaoMo'
display = re.sub(r'^\[\w+\]\s*', '', text, count=1).strip()
ts_str = msg_ts(m)
ts_dt = None
if ts_str:
try: ts_dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
except: pass
if cutoff and ts_dt and ts_dt < cutoff:
continue
results.append((ts_dt or datetime.min, ts_str, sender, display))
results.sort(key=lambda x: x[0])
if not results:
print('No [mohe]/[xxm] messages found' + (f' in last {minutes} min' if minutes else ''))
return
print(f'\n{len(results)} message(s)' + (f' (last {minutes} min)' if minutes else ''))
print('=' * 60)
for ts_dt, ts_str, sender, display in results:
t = '??'
if ts_str:
try: t = datetime.fromisoformat(ts_str.replace('Z', '+00:00')).strftime('%H:%M:%S')
except: t = str(ts_str)[:19]
print(f'[{t}] {sender}: {display}')
if __name__ == '__main__':
main()
-34
View File
@@ -1,34 +0,0 @@
@echo off
title WeChat Hermes Bridge
cd /d "%~dp0.."
set PYTHON=C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe
set AGENT=scripts\wechat_agent.py
echo ========================================
echo WeChat Hermes Bridge
echo ========================================
echo.
echo [1/3] 启动微信 + 注入 DLL...
echo.
set PYTHONHOME=
set WXHOOK_LOG_LEVEL=ERROR
%PYTHON% %AGENT%
echo.
echo ========================================
echo 微信已启动!
echo.
echo 第 2 步:运行修复过低工具 ^> 扫码登录
echo 路径:D:\Program Files (x86)\低版本修复工具\
echo 低版本修复工具\修复过低6.0\低版通用杀器.sp.exe
echo.
echo 第 3 步:手机扫码 → 自动开始转发
echo.
echo 按 Ctrl+C 停止桥接
echo ========================================
if %errorlevel% neq 0 (
pause
)
+28
View File
@@ -0,0 +1,28 @@
@echo off
title WeChat History API
cd /d "%~dp0.."
set PYTHON=python
echo ========================================
echo WeChat History REST API Server
echo Port: 19001
echo ========================================
echo.
echo Starting history API server...
echo Endpoints:
echo GET http://localhost:19001/
echo GET http://localhost:19001/health
echo GET http://localhost:19001/api/contacts
echo GET http://localhost:19001/api/recent
echo GET http://localhost:19001/api/history?wxid=wxid_xxx^&count=20
echo POST http://localhost:19001/api/history
echo ========================================
echo.
set PYTHONHOME=
%PYTHON% api\history_api.py --port 19001
if %errorlevel% neq 0 (
pause
)
+189 -8
View File
@@ -5,22 +5,29 @@ import os, json, time, threading, requests, re, socketserver, subprocess, urllib
os.environ["no_proxy"] = "*"
os.environ["NO_PROXY"] = "*"
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
BOT_WXID = "wxid_7onnerpx2s2l22"
BLOCK_WXIDS = {"fmessage", "weixin", "wechat"} # 系统账号/微信团队,不回复
WX_API = "http://127.0.0.1:19088"
LOG_FILE = r"C:\Users\hmo\Desktop\wechat_agent.log"
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
TEMP_DIR = os.path.join(PROJECT_ROOT, "temp")
LOG_FILE = os.path.join(LOG_DIR, "wechat_agent.log")
os.makedirs(LOG_DIR, exist_ok=True)
os.makedirs(TEMP_DIR, exist_ok=True)
TCP_PORT = 19099
last_msg_time = time.time()
nickname_cache = {}
db_handle_cache = None # MicroMsg.db handle for history queries
HERMES_API = "http://192.168.0.103:8642/v1/chat/completions"
HERMES_KEY = "hermes123"
SENSENOVA_KEY = "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO"
SENSENOVA_URL = "https://token.sensenova.cn/v1"
INJECTOR = r"C:\Users\hmo\Desktop\wxhelper_v11\wxhelper-3.9.5.81-v11\tool\injector\ConsoleApplication.exe"
WXHELPER_DLL = r"C:\Users\hmo\Desktop\wxhelper_391019.dll"
INJECTOR = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\ConsoleApplication.exe"
WXHELPER_DLL = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\wxhelper_391019.dll"
def log(m):
with open(LOG_FILE, "a", encoding="utf-8") as f:
@@ -37,6 +44,121 @@ def wxpost(path, data=None, timeout=10):
log(f"WX ERR: {e}")
return {"code": -1}
# ── History Query (via MSG table in MSG*.db databases) ──
def get_db_handle():
"""Get handle for database containing MSG table. Cached after first call."""
global db_handle_cache
if db_handle_cache:
return db_handle_cache
r = wxpost("/api/getDBInfo", timeout=10)
dbs = r.get("data") or []
# WeChat 3.9.5.81+: messages stored in MSG0.db, MSG1.db, etc.
# Older versions: messages in MicroMsg.db's MSG table
for db in dbs:
dbname = db.get("databaseName", "")
if "MSG" in dbname or "Msg" in dbname:
db_handle_cache = db.get("handle")
log(f"History DB: {dbname} handle={db_handle_cache}")
return db_handle_cache
log("History DB handle: NOT FOUND")
return None
# Message type labels
MSG_TYPES = {1: "文字", 3: "图片", 34: "语音", 43: "视频", 47: "表情", 49: "链接", 10000: "系统", 10002: "红包"}
def query_history(wxid, limit=10):
"""Query historical text messages with a contact from MSG table."""
h = get_db_handle()
if not h:
return None
# Text (type=1) and appmsg/link (type=49), use DisplayContent as fallback for StrContent
limit_val = min(int(limit), 50)
sql = f"SELECT CreateTime, IsSender, Type, SubType, StrContent, DisplayContent FROM MSG WHERE StrTalker='{wxid}' AND Type IN (1,49) ORDER BY CreateTime DESC LIMIT {limit_val}"
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
data = r.get("data") or []
if not data or len(data) < 2:
return None
# Skip header row, reverse to chronological order
rows = data[1:]
rows.reverse()
# Normalize content: prefer StrContent, fallback to DisplayContent
results = []
for row in rows:
content = (row[4] or "").strip() if len(row) > 4 else ""
if not content and len(row) > 5:
content = (row[5] or "").strip()
if not content:
continue
results.append({"CreateTime": row[0], "IsSender": row[1], "Type": row[2], "content": content})
return results
def format_history(wxid, rows):
"""Format MSG rows into readable chat history text."""
sender_name = get_nickname(wxid)
bot_name = get_nickname(BOT_WXID)
lines = [f"📜 最近与 {sender_name} 的聊天记录 ({len(rows)}条):"]
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = time.strftime("%m/%d %H:%M", time.localtime(ts)) if ts else "?"
is_sender = int(row.get("IsSender", 0))
msg_type = int(row.get("Type", 1))
content = row.get("content", "")
# Determine who sent it
who = bot_name if is_sender else sender_name
# Format content
if msg_type == 49:
content = f"[链接] {content[:60]}"
else:
content = content[:200]
lines.append(f"[{time_str}] {who}: {content}")
return "\n".join(lines)
def handle_history(wxid, count):
"""Query and format history for a given wxid. Returns text to send."""
try:
rows = query_history(wxid, count)
if rows:
return format_history(wxid, rows)
return f"暂无与 {get_nickname(wxid)} 的聊天记录"
except Exception as e:
log(f"History ERR: {e}")
return "查询历史记录失败"
def handle_history_json(wxid, count):
"""Query history and return JSON-serializable dict for HTTP API."""
try:
rows = query_history(wxid, count)
sender_name = get_nickname(wxid)
if not rows:
return {"ok": True, "wxid": wxid, "sender_name": sender_name, "count": 0, "messages": []}
bot_name = get_nickname(BOT_WXID)
messages = []
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) if ts else ""
is_sender = int(row.get("IsSender", 0))
msg_type = int(row.get("Type", 1))
content = row.get("content", "")
messages.append({
"time": time_str,
"timestamp": ts,
"sender": bot_name if is_sender else sender_name,
"is_self": bool(is_sender),
"type": msg_type,
"content": content[:500]
})
return {
"ok": True,
"wxid": wxid,
"sender_name": sender_name,
"count": len(messages),
"requested_count": min(int(count or 10), 50),
"messages": messages
}
except Exception as e:
log(f"History JSON ERR: {e}")
return {"ok": False, "error": str(e)[:200]}
def send_wx(wxid, msg):
# Strip weixin:// URLs that WeChat interprets as commands
import re as _re2
@@ -69,6 +191,25 @@ def call_hermes(wxid, content):
log(f"API ERR: {e}")
return None
def inject_to_hermes_session(text):
"""Inject chat history / context directly into Hermes's sisyphus session for memory repair."""
headers = {"Authorization": f"Bearer {HERMES_KEY}", "X-Hermes-Session-Id": "sisyphus", "Content-Type": "application/json"}
sys_prompt = "📥 MEMORY INJECTION: Below is past chat history. Absorb this into your context for memory repair. Do NOT reply to this — just acknowledge with 'Memory synced.'"
body = {"model": "hermes-agent", "messages": [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": text}
]}
try:
r = requests.post(HERMES_API, json=body, headers=headers, timeout=180, proxies={"http": None, "https": None})
if r.status_code == 200:
reply = r.json()["choices"][0]["message"]["content"]
log(f"Inject history OK: {reply[:50]}")
return True
log(f"Inject history HTTP {r.status_code}")
except Exception as e:
log(f"Inject history ERR: {e}")
return False
# ── Inject wxhelper DLL ──
def inject_wxhelper():
try:
@@ -179,6 +320,12 @@ def process_tags(reply, fu):
members = (r.get("data") or {}).get("members", "")
mlist = [m for m in members.split("\u0007") if m]
send_wx(fu, f"群成员 ({len(mlist)}): {','.join(mlist[:20])}")
# [HISTORY:wxid:count] - query chat history from MSG table
hm = re.search(r'\[HISTORY:(\S+?):(\d+)\]', clean)
if hm:
clean = re.sub(r'\s*\[HISTORY:\S+?:\d+\]\s*', '', clean).strip()
target_wxid, count = hm.group(1), int(hm.group(2))
threading.Thread(target=lambda: send_wx(fu, handle_history(target_wxid, count)), daemon=True).start()
# [PAT:roomid:wxid]
pm = re.search(r'\[PAT:(\S+):(\S+)\]', clean)
if pm:
@@ -191,7 +338,7 @@ def download_and_send_file(m, fu):
url = m.group(1).strip()
ir = requests.get(url, timeout=60, proxies={"http": None, "https": None})
if ir.status_code == 200:
tmp = os.path.join(r"C:\Users\hmo\Desktop", f"send_file_{int(time.time())}.dat")
tmp = os.path.join(TEMP_DIR, f"send_file_{int(time.time())}.dat")
with open(tmp, "wb") as f: f.write(ir.content)
wxpost("/api/sendFileMsg", {"wxid": fu, "filePath": tmp})
os.remove(tmp)
@@ -216,7 +363,7 @@ def handle_img(m, fu):
img_url = r.json()["data"][0]["url"]
ir = requests.get(img_url, timeout=60)
if ir.status_code == 200:
tmp = os.path.join(r"C:\Users\hmo\Desktop", f"gen_img_{int(time.time())}.png")
tmp = os.path.join(TEMP_DIR, f"gen_img_{int(time.time())}.png")
with open(tmp, "wb") as f: f.write(ir.content)
wxpost("/api/sendImagesMsg", {"wxid": fu, "imagePath": tmp})
os.remove(tmp)
@@ -225,7 +372,7 @@ def handle_img(m, fu):
if ir.status_code == 200:
ext = ".jpg"
if "png" in ir.headers.get("content-type", ""): ext = ".png"
tmp = os.path.join(r"C:\Users\hmo\Desktop", f"send_img_{int(time.time())}{ext}")
tmp = os.path.join(TEMP_DIR, f"send_img_{int(time.time())}{ext}")
with open(tmp, "wb") as f: f.write(ir.content)
wxpost("/api/sendImagesMsg", {"wxid": fu, "imagePath": tmp})
os.remove(tmp)
@@ -234,7 +381,7 @@ def download_emoji(m, fu):
url = m.group(1).strip()
ir = requests.get(url, timeout=30, proxies={"http": None, "https": None})
if ir.status_code == 200:
tmp = os.path.join(r"C:\Users\hmo\Desktop", f"emoji_{int(time.time())}.png")
tmp = os.path.join(TEMP_DIR, f"emoji_{int(time.time())}.png")
with open(tmp, "wb") as f: f.write(ir.content)
wxpost("/api/sendCustomEmotion", {"wxid": fu, "filePath": tmp})
os.remove(tmp)
@@ -293,11 +440,29 @@ class RH(BaseHTTPRequestHandler):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
try:
d = json.loads(body)
if self.path in ("/history", "/api/chatHistory"):
wxid = (d.get("wxid", "") or "").strip()
count = d.get("count", 10) or d.get("limit", 10)
if not wxid:
self._send_json({"ok": False, "error": "Missing wxid"})
return
self._send_json(handle_history_json(wxid, count))
return
if self.path == "/hermes-msg":
msg = d.get("message", "") or d.get("content", "") or str(d)[:200]
log("<<< HERMES: " + msg[:100])
with open(r"C:\Users\hmo\Desktop\hermes_inbox.txt", "a", encoding="utf-8") as f:
with open(os.path.join(TEMP_DIR, "hermes_inbox.txt"), "a", encoding="utf-8") as f:
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
# HISTORY_DATA tag: query history and inject back to Hermes session
hm = re.search(r'\[HISTORY_DATA:(\S+?):(\d+)\]', msg)
if hm:
target_wxid, count = hm.group(1), int(hm.group(2))
history_text = handle_history(target_wxid, count)
if history_text:
threading.Thread(target=lambda: inject_to_hermes_session(history_text), daemon=True).start()
log(f"HISTORY_DATA: injected for {target_wxid} ({count} msgs)")
else:
log(f"HISTORY_DATA: no messages for {target_wxid}")
self.send_response(200); self.end_headers(); return
to = d.get("to", "") or d.get("wxid", "")
msg = d.get("message", "") or d.get("content", "")
@@ -308,7 +473,23 @@ class RH(BaseHTTPRequestHandler):
log(f"RH ERR: {e}")
self.send_response(200); self.end_headers()
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path in ("/history", "/api/chatHistory"):
params = parse_qs(parsed.query)
wxid = params.get("wxid", [""])[0]
count = params.get("count", ["10"])[0]
result = handle_history_json(wxid, count)
log(f"HTTP GET {parsed.path} wxid={wxid} count={count}")
self._send_json(result)
return
self.send_response(200); self.end_headers(); self.wfile.write(b'{"ok":true}')
def _send_json(self, data):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
threading.Thread(target=lambda: HTTPServer(("0.0.0.0", 5801), RH).serve_forever(), daemon=True).start()
Binary file not shown.
Binary file not shown.