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:
oliver 2026-04-28 19:26:26 +08:00
parent 31e3962c74
commit 88b1fdc9d3
12 changed files with 823 additions and 42 deletions

View file

@ -30,7 +30,7 @@ class WeixinIlinkApiTests(unittest.TestCase):
self.assertEqual((r.json() or {}).get("ret"), 400)
def test_sendmessage_enqueues_reply_for_getupdates(self) -> None:
old_usecase = weixin_ilink_api.process_inbound_payload_usecase
old_usecase = weixin_ilink_api._process_inbound_payload_usecase
def _fake_usecase(payload: dict[str, object]) -> dict[str, object]:
text = str(payload.get("text") or "")
@ -45,7 +45,7 @@ class WeixinIlinkApiTests(unittest.TestCase):
}
try:
weixin_ilink_api.process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
weixin_ilink_api._process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
s = self.client.post(
"/ilink/bot/sendmessage",
headers=self.headers,
@ -77,7 +77,49 @@ class WeixinIlinkApiTests(unittest.TestCase):
self.assertTrue(msgs, data)
self.assertEqual(str((msgs[0] or {}).get("text") or ""), "echo:ping")
finally:
weixin_ilink_api.process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
def test_native_reply_returns_sync_replies(self) -> None:
old_usecase = weixin_ilink_api._process_inbound_payload_usecase
def _fake_usecase(payload: dict[str, object]) -> dict[str, object]:
text = str(payload.get("text") or "")
self.assertEqual(str(payload.get("channel") or ""), "wechat")
self.assertEqual(str(payload.get("user_id") or ""), "wxid_u3")
return {
"ok": True,
"replies": [
{
"chat_id": str(payload.get("chat_id") or ""),
"text": f"native:{text}",
}
],
}
try:
weixin_ilink_api._process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
r = self.client.post(
"/weixin/native/reply",
headers=self.headers,
json={
"channel": "wechat",
"account_id": "bot-1",
"ctx": {
"From": "wxid_u3",
"To": "wxid_u3",
"Body": "hello native",
"context_token": "ctx-1",
},
},
)
self.assertEqual(r.status_code, 200, r.text)
data = r.json() or {}
self.assertTrue(data.get("ok"), data)
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
self.assertEqual(len(replies), 1, data)
self.assertEqual(str((replies[0] or {}).get("text") or ""), "native:hello native")
finally:
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
if __name__ == "__main__":

View file

@ -0,0 +1,50 @@
from __future__ import annotations
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def _read(rel: str) -> str:
return (REPO_ROOT / rel).read_text(encoding="utf-8")
def test_weixin_start_defaults_to_official_runner() -> None:
text = _read("runtime/operations/scripts/weixin_start.ps1")
assert '$runnerMode = "official"' in text
assert '$runnerFile = "official_runner.ts"' in text
assert 'AIA_WEIXIN_RUNNER_MODE' in text
assert 'official_runner.ts' in text
def test_weixin_status_and_stop_track_official_runner() -> None:
status_text = _read("runtime/operations/scripts/weixin_status.ps1")
stop_text = _read("runtime/operations/scripts/weixin_stop.ps1")
assert "official_runner.ts" in status_text
assert "official_runner.ts" in stop_text
def test_weixin_install_copies_official_runner() -> None:
text = _read("runtime/operations/scripts/weixin_install.ps1")
assert 'Copy-Item -Path (Join-Path $bridgeSrc "official_runner.ts")' in text
assert "Ensure-OfficialPluginRuntimeDeps" in text
def test_weixin_start_ensures_plugin_runtime_deps() -> None:
text = _read("runtime/operations/scripts/weixin_start.ps1")
assert "Ensure-OfficialPluginRuntimeDeps" in text
assert "npm.cmd install openclaw@latest --no-save" in text
def test_official_runner_prefers_direct_inbound_before_other_fallbacks() -> None:
text = _read("runtime/operations/weixin_bridge/official_runner.ts")
assert "const native = await postNativeReply" in text
assert "falling back to direct /inbound bridge" not in text
assert "legacy local ilink bridge" not in text
def test_official_runner_logs_active_bridge_path() -> None:
text = _read("runtime/operations/weixin_bridge/official_runner.ts")
assert "official runner started account=" in text
assert "native reply failed; no fallback enabled" in text