mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 06:33:15 +08:00
feat(weixin): native official runner and OSS install docs
Default Weixin to official native runner, add /weixin/native/reply host endpoint, and harden gateway stop to clean stale port listeners. Document open-source installation flow for gateway + Personal WeChat. Made-with: Cursor
This commit is contained in:
parent
31e3962c74
commit
88b1fdc9d3
12 changed files with 823 additions and 42 deletions
|
|
@ -9,11 +9,19 @@ from typing import Any
|
|||
|
||||
from fastapi import APIRouter, Header, HTTPException, Request
|
||||
|
||||
from oclaw.runtime.application.gateway import process_inbound_payload_usecase
|
||||
def _process_inbound_payload_usecase(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
# Lazy import to avoid circular imports during FastAPI app bootstrap.
|
||||
from oclaw.runtime.application.gateway import process_inbound_payload_usecase
|
||||
|
||||
return process_inbound_payload_usecase(payload)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Historical iLink-compatible fallback endpoints. The primary path is now
|
||||
# `/weixin/native/reply` + `official_runner.ts`, but these routes remain for
|
||||
# short-term rollback compatibility.
|
||||
|
||||
|
||||
def _require_ilink_auth(
|
||||
*,
|
||||
|
|
@ -123,6 +131,48 @@ def _extract_inbound_identity(body: dict[str, Any]) -> tuple[str, str]:
|
|||
return user_id, chat_id
|
||||
|
||||
|
||||
def _build_native_reply_payload(body: dict[str, Any]) -> dict[str, Any]:
|
||||
channel = _normalize_channel(body.get("channel"))
|
||||
account_id = _resolve_account_id(body)
|
||||
ctx = body.get("ctx") if isinstance(body.get("ctx"), dict) else {}
|
||||
metadata = body.get("metadata") if isinstance(body.get("metadata"), dict) else {}
|
||||
attachments = body.get("attachments") if isinstance(body.get("attachments"), list) else []
|
||||
|
||||
user_id = str(
|
||||
body.get("user_id")
|
||||
or ctx.get("From")
|
||||
or ctx.get("To")
|
||||
or body.get("external_user_id")
|
||||
or ""
|
||||
).strip()
|
||||
chat_id = str(
|
||||
body.get("chat_id")
|
||||
or ctx.get("To")
|
||||
or ctx.get("From")
|
||||
or body.get("external_chat_id")
|
||||
or user_id
|
||||
or ""
|
||||
).strip()
|
||||
text = str(body.get("text") or ctx.get("Body") or ctx.get("CommandBody") or "").strip()
|
||||
if not metadata:
|
||||
metadata = {"source": "weixin_official_native"}
|
||||
else:
|
||||
metadata = dict(metadata)
|
||||
metadata.setdefault("source", "weixin_official_native")
|
||||
if ctx:
|
||||
metadata["weixin_ctx"] = ctx
|
||||
|
||||
return {
|
||||
"channel": channel,
|
||||
"account_id": account_id,
|
||||
"user_id": user_id,
|
||||
"chat_id": chat_id or user_id,
|
||||
"text": text,
|
||||
"attachments": [a for a in attachments if isinstance(a, dict)],
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
|
||||
class _IlinkBridge:
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
|
|
@ -236,7 +286,7 @@ async def _process_inbound_and_enqueue(
|
|||
return ""
|
||||
|
||||
try:
|
||||
out = await asyncio.wait_for(asyncio.to_thread(process_inbound_payload_usecase, payload), timeout=60.0)
|
||||
out = await asyncio.wait_for(asyncio.to_thread(_process_inbound_payload_usecase, payload), timeout=60.0)
|
||||
except asyncio.TimeoutError:
|
||||
ctx_token = _extract_context_token(payload)
|
||||
_BRIDGE.enqueue_reply(
|
||||
|
|
@ -406,5 +456,27 @@ async def ilink_sendtyping(
|
|||
return {"ret": 0}
|
||||
|
||||
|
||||
@router.post("/weixin/native/reply")
|
||||
async def weixin_native_reply(
|
||||
body: dict[str, Any],
|
||||
authorizationtype: str | None = Header(default=None, alias="AuthorizationType"),
|
||||
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||
) -> dict[str, Any]:
|
||||
_require_ilink_auth(authorization_type=authorizationtype, authorization=authorization)
|
||||
payload = _build_native_reply_payload(body)
|
||||
if not str(payload.get("user_id") or "").strip():
|
||||
return {"ok": False, "error": "missing user_id", "replies": []}
|
||||
if not str(payload.get("account_id") or "").strip():
|
||||
return {"ok": False, "error": "missing account_id", "replies": []}
|
||||
out = await asyncio.to_thread(_process_inbound_payload_usecase, payload)
|
||||
replies = out.get("replies") if isinstance(out, dict) else []
|
||||
if not isinstance(replies, list):
|
||||
replies = []
|
||||
return {
|
||||
"ok": bool((out or {}).get("ok", True)) if isinstance(out, dict) else True,
|
||||
"replies": [r for r in replies if isinstance(r, dict)],
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue