'init'
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled

This commit is contained in:
eric
2026-03-24 14:46:41 -05:00
parent c5a1a9a404
commit b77a8cadb2
164 changed files with 57060 additions and 58 deletions

292
web2_wechat_chatbot.py Normal file
View File

@@ -0,0 +1,292 @@
"""
微信 Chatbot 桥接:与 openclaw-wechat 使用的代理 HTTP API 对齐(见 integrations/openclaw-wechat
需自备 apiKey 与 proxyUrl第三方代理服务本模块提供扫码登录、状态查询与发消息等 REST供 Web / CLI / Agent 调用。
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Optional
import httpx
from fastapi import Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
CONFIG_NAME = "wechat_chatbot.json"
STATE_NAME = "wechat_chatbot.state.json"
def _config_path(base: Path) -> Path:
return base / "config" / CONFIG_NAME
def _state_path(base: Path) -> Path:
return base / "config" / STATE_NAME
def load_wechat_chatbot_config(base_dir: Path) -> dict[str, Any]:
p = _config_path(base_dir)
if not p.exists():
return {}
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return {}
def load_wechat_state(base_dir: Path) -> dict[str, Any]:
p = _state_path(base_dir)
if not p.exists():
return {}
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return {}
def save_wechat_state(base_dir: Path, state: dict[str, Any]) -> None:
p = _state_path(base_dir)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def mask_api_key(key: str) -> str:
k = (key or "").strip()
if len(k) <= 8:
return "****" if k else ""
return f"{k[:4]}{k[-4:]}"
class WechatProxyClient:
"""与 freestylefly/openclaw-wechat 中 ProxyClient 请求格式一致。"""
def __init__(
self,
api_key: str,
base_url: str,
account_id: str = "default",
timeout: float = 60.0,
):
if not api_key or not api_key.strip():
raise ValueError("缺少 apiKey")
if not base_url or not base_url.strip():
raise ValueError("缺少 proxyUrl")
self.api_key = api_key.strip()
self.base_url = base_url.strip().rstrip("/")
self.account_id = account_id.strip() or "default"
self.timeout = timeout
def _request(self, endpoint: str, data: Optional[dict[str, Any]] = None) -> Any:
url = f"{self.base_url}{endpoint}"
headers = {
"Content-Type": "application/json",
"X-API-Key": self.api_key,
"X-Account-ID": self.account_id,
}
with httpx.Client(timeout=self.timeout) as client:
r = client.post(url, headers=headers, json=data)
try:
result = r.json()
except Exception:
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:500]}") from None
if not r.is_success:
raise RuntimeError(result.get("error") or result.get("message") or f"HTTP {r.status_code}")
code = result.get("code")
if code in ("1000", "1001", "1002"):
d = result.get("data")
return d if isinstance(d, (dict, list)) else result
if code and str(code) != "1000":
raise RuntimeError(result.get("message") or str(result))
d = result.get("data")
return d if d is not None else result
def get_status(self) -> dict[str, Any]:
out = self._request("/v1/account/status")
if not isinstance(out, dict):
return {"raw": out}
return {
"valid": out.get("valid", True),
"wcId": out.get("wcId"),
"isLoggedIn": out.get("isLoggedIn", False),
"nickName": out.get("nickName"),
"tier": out.get("tier"),
"quota": out.get("quota"),
"error": out.get("error"),
}
def get_qr_code(self, device_type: str = "ipad", proxy: str = "10") -> dict[str, str]:
out = self._request(
"/v1/iPadLogin",
{"deviceType": device_type or "ipad", "proxy": proxy or "10"},
)
if not isinstance(out, dict):
raise RuntimeError("代理返回格式异常")
w_id = out.get("wId") or out.get("w_id")
qr_url = out.get("qrCodeUrl") or out.get("qr_code_url")
if not w_id or not qr_url:
raise RuntimeError("代理未返回二维码信息")
return {"wId": str(w_id), "qrCodeUrl": str(qr_url)}
def check_login(self, w_id: str) -> dict[str, Any]:
out = self._request("/v1/getIPadLoginInfo", {"wId": w_id})
if not isinstance(out, dict):
return {"status": "waiting"}
st = out.get("status")
if st == "logged_in":
return {
"status": "logged_in",
"wcId": out.get("wcId"),
"nickName": out.get("nickName"),
"headUrl": out.get("headUrl"),
}
if st == "need_verify":
return {"status": "need_verify", "verifyUrl": out.get("verifyUrl")}
return {"status": "waiting"}
def send_text(self, target_id: str, content: str) -> dict[str, Any]:
out = self._request("/v1/sendText", {"wcId": target_id, "content": content})
return out if isinstance(out, dict) else {"result": out}
def register_webhook(self, webhook_url: str) -> None:
self._request("/v1/webhook/register", {"webhookUrl": webhook_url})
def build_client(cfg: dict[str, Any]) -> WechatProxyClient:
return WechatProxyClient(
api_key=str(cfg.get("apiKey") or ""),
base_url=str(cfg.get("proxyUrl") or ""),
account_id=str(cfg.get("accountId") or "default"),
)
class WechatConfigBody(BaseModel):
apiKey: Optional[str] = None
proxyUrl: Optional[str] = None
deviceType: Optional[str] = None
proxy: Optional[str] = None
webhookHost: Optional[str] = None
webhookPort: Optional[int] = None
webhookPath: Optional[str] = None
accountId: Optional[str] = None
class WechatSendBody(BaseModel):
targetId: str = Field(..., description="接收方 wxid 或群 id")
text: str = Field(..., description="文本内容")
class WechatWebhookBody(BaseModel):
webhookUrl: str
def register_wechat_chatbot_routes(app, base_dir: Path) -> None:
"""在 FastAPI app 上注册 /chatbot/wechat/* 路由。"""
@app.get("/chatbot/wechat/config")
async def wechat_config_get():
cfg = load_wechat_chatbot_config(base_dir)
st = load_wechat_state(base_dir)
key = str(cfg.get("apiKey") or "")
return {
"configured": bool(cfg.get("proxyUrl") and key),
"proxyUrl": cfg.get("proxyUrl") or "",
"apiKeyMasked": mask_api_key(key),
"deviceType": cfg.get("deviceType") or "ipad",
"proxy": cfg.get("proxy") or "10",
"webhookHost": cfg.get("webhookHost") or "",
"webhookPort": int(cfg.get("webhookPort") or 18790),
"webhookPath": cfg.get("webhookPath") or "/webhook/wechat",
"accountId": cfg.get("accountId") or "default",
"session": {
"wcId": st.get("wcId"),
"nickName": st.get("nickName"),
},
}
@app.post("/chatbot/wechat/config")
async def wechat_config_post(body: WechatConfigBody):
cfg = load_wechat_chatbot_config(base_dir)
d = body.model_dump(exclude_none=True)
for k, v in d.items():
cfg[k] = v
out_path = _config_path(base_dir)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
return {"ok": True}
@app.get("/chatbot/wechat/proxy/status")
async def wechat_proxy_status():
cfg = load_wechat_chatbot_config(base_dir)
if not cfg.get("apiKey") or not cfg.get("proxyUrl"):
return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400)
try:
c = build_client(cfg)
return c.get_status()
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=502)
@app.post("/chatbot/wechat/qr/start")
async def wechat_qr_start():
cfg = load_wechat_chatbot_config(base_dir)
if not cfg.get("apiKey") or not cfg.get("proxyUrl"):
return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400)
try:
c = build_client(cfg)
dt = str(cfg.get("deviceType") or "ipad")
px = str(cfg.get("proxy") or "10")
return c.get_qr_code(device_type=dt, proxy=px)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=502)
@app.post("/chatbot/wechat/login/check")
async def wechat_login_check(request: Request):
try:
body = await request.json()
except Exception:
return JSONResponse({"error": "需要 JSON body: { \"wId\": \"...\" }"}, status_code=400)
w_id = (body or {}).get("wId") or (body or {}).get("w_id")
if not w_id or not isinstance(w_id, str):
return JSONResponse({"error": "缺少 wId"}, status_code=400)
cfg = load_wechat_chatbot_config(base_dir)
if not cfg.get("apiKey") or not cfg.get("proxyUrl"):
return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400)
try:
c = build_client(cfg)
r = c.check_login(w_id.strip())
if r.get("status") == "logged_in" and r.get("wcId"):
save_wechat_state(
base_dir,
{
"wcId": r.get("wcId"),
"nickName": r.get("nickName"),
},
)
return r
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=502)
@app.post("/chatbot/wechat/send")
async def wechat_send(body: WechatSendBody):
cfg = load_wechat_chatbot_config(base_dir)
if not cfg.get("apiKey") or not cfg.get("proxyUrl"):
return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400)
try:
c = build_client(cfg)
return c.send_text(body.targetId.strip(), body.text)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=502)
@app.post("/chatbot/wechat/webhook/register")
async def wechat_webhook_reg(body: WechatWebhookBody):
cfg = load_wechat_chatbot_config(base_dir)
if not cfg.get("apiKey") or not cfg.get("proxyUrl"):
return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400)
try:
c = build_client(cfg)
c.register_webhook(body.webhookUrl.strip())
return {"ok": True}
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=502)