from __future__ import annotations import json import os from typing import Any from runtime.orchestration.group_ingest import is_nonsend_channel_reply_text, should_send_channel_reply_text from runtime.scheduler.session_resolver import parse_delivery_json def _encode_weixin_outbound_source( *, context_token: str, attachments: list[dict[str, Any]] | None = None, media_path: str | None = None, ) -> str: payload: dict[str, Any] = { "kind": "scheduled_job", "context_token": str(context_token or "").strip(), } atts = [a for a in (attachments or []) if isinstance(a, dict)] if atts: payload["attachments"] = atts mp = str(media_path or "").strip() if mp: payload["media_path"] = mp return json.dumps(payload, ensure_ascii=False) def _decode_weixin_outbound_source(raw: str) -> dict[str, Any]: text = str(raw or "").strip() if not text: return {} try: data = json.loads(text) return data if isinstance(data, dict) else {} except Exception: return {} def enqueue_weixin_reply( *, channel: str, account_id: str, chat_id: str, text: str, context_token: str = "", store: Any = None, tenant_id: str = "", attachments: list[dict[str, Any]] | None = None, media_path: str | None = None, ) -> dict[str, Any]: from runtime.scheduler.weixin_delivery import normalize_weixin_channel ctx_tok = str(context_token or "").strip() if not ctx_tok: return { "ok": False, "channel": normalize_weixin_channel(channel), "error": "context_token_missing", "hint": "Send any message to the bot on WeChat first, then retry the scheduled job.", } durable_id = "" enqueuer = getattr(store, "enqueue_channel_outbound_message", None) if store is not None and callable(enqueuer): try: durable_id = str( enqueuer( channel=normalize_weixin_channel(channel), chat_id=str(chat_id or "").strip(), text=str(text or ""), tenant_id=str(tenant_id or ""), account_id=str(account_id or "").strip(), source=_encode_weixin_outbound_source( context_token=ctx_tok, attachments=attachments, media_path=media_path, ), ) or "" ).strip() except Exception: durable_id = "" try: from interfaces.http.weixin_ilink_api import enqueue_weixin_outbound_reply except Exception as exc: if durable_id: return { "ok": True, "channel": normalize_weixin_channel(channel), "message_id": durable_id, "queued": True, "durable": True, "context_token_present": True, "bridge_error": f"{type(exc).__name__}: {exc}", } return {"ok": False, "channel": channel, "error": f"{type(exc).__name__}: {exc}"} try: bridge_seq = enqueue_weixin_outbound_reply( channel=normalize_weixin_channel(channel), account_id=str(account_id or "").strip(), chat_id=str(chat_id or "").strip(), text=text, context_token=ctx_tok, attachments=attachments, media_path=media_path, ) return { "ok": True, "channel": normalize_weixin_channel(channel), "message_id": durable_id or bridge_seq, "bridge_seq": bridge_seq, "queued": True, "durable": bool(durable_id), "context_token_present": True, "account_id": str(account_id or "").strip(), "chat_id": str(chat_id or "").strip(), } except Exception as exc: if durable_id: return { "ok": True, "channel": normalize_weixin_channel(channel), "message_id": durable_id, "queued": True, "durable": True, "context_token_present": True, "bridge_error": f"{type(exc).__name__}: {exc}", } return {"ok": False, "channel": channel, "error": f"{type(exc).__name__}: {exc}"} def persist_channel_context_token( store: Any, *, tenant_id: str, channel: str, account_id: str, external_chat_id: str, context_token: str, ) -> None: from runtime.scheduler.weixin_delivery import normalize_weixin_channel setter = getattr(store, "set_channel_context_token", None) if not callable(setter): return tok = str(context_token or "").strip() chat_id = str(external_chat_id or "").strip() if not tok or not chat_id: return tid = str(tenant_id or "") acct = str(account_id or "").strip() channels = [] for ch in (channel, "wechat", "weixin"): c = normalize_weixin_channel(str(ch or "")) if str(ch or "").lower() in {"wechat", "weixin"} else str(ch or "").strip().lower() if c and c not in channels: channels.append(c) account_ids = [acct] if acct else [""] if acct: account_ids.append("") for ch in channels: for aid in account_ids: setter( tenant_id=tid, channel=ch, account_id=aid, external_chat_id=chat_id, context_token=tok, ) def extract_context_token_from_inbound_metadata(metadata: dict[str, Any] | None) -> str: meta = metadata if isinstance(metadata, dict) else {} raw = meta.get("raw") if isinstance(meta.get("raw"), dict) else {} msg = raw.get("msg") if isinstance(raw.get("msg"), dict) else {} for candidate in ( msg.get("context_token"), (raw.get("metadata") or {}).get("context_token") if isinstance(raw.get("metadata"), dict) else None, raw.get("context_token"), meta.get("context_token"), ): tok = str(candidate or "").strip() if tok: return tok return "" def deliver_scheduled_reply( store: Any, *, tenant_id: str, reply_text: str, delivery_json: str, resolved_channel: str = "", resolved_chat_id: str = "", resolved_account_id: str = "", session_id: str = "", ) -> dict[str, Any]: text = str(reply_text or "").strip() if not text or is_nonsend_channel_reply_text(text): return {"ok": False, "skipped": True, "reason": "empty_or_silent_reply"} ch_lower = str(resolved_channel or "").strip().lower() if ch_lower in {"wechat", "weixin"}: from runtime.application.gateway.inbound_service import _user_facing_wechat_reply text = _user_facing_wechat_reply(reply=text) delivery = parse_delivery_json(delivery_json) results: dict[str, Any] = {} wa = delivery.get("whatsapp") if isinstance(delivery.get("whatsapp"), dict) else {} wa_enabled = bool(wa.get("enabled")) and str(wa.get("target_type") or "none") != "none" chat_id = str(wa.get("chat_id") or resolved_chat_id or "").strip() account_id = str( wa.get("account_id") or resolved_account_id or os.getenv("AIA_WHATSAPP_ACCOUNT_ID") or "wa-default" ).strip() if wa_enabled and chat_id and should_send_channel_reply_text(text): msg_id = store.enqueue_channel_outbound_message( channel="whatsapp", chat_id=chat_id, text=text, tenant_id=tenant_id, account_id=account_id, source="scheduled_job", ) results["whatsapp"] = {"ok": True, "message_id": msg_id, "chat_id": chat_id} wx = delivery.get("weixin") if isinstance(delivery.get("weixin"), dict) else {} wx_enabled = bool(wx.get("enabled", True)) if wx_enabled and should_send_channel_reply_text(text): from runtime.scheduler.weixin_delivery import resolve_weixin_delivery_target target = resolve_weixin_delivery_target( store, tenant_id=tenant_id, session_id=session_id, delivery=delivery, resolved_channel=resolved_channel, resolved_chat_id=resolved_chat_id, resolved_account_id=resolved_account_id, ) wx_chat = str(target.get("chat_id") or "").strip() wx_account = str(target.get("account_id") or "").strip() wx_channel = str(target.get("channel") or "wechat") context_token = str(target.get("context_token") or "").strip() if wx_chat: results["weixin"] = enqueue_weixin_reply( channel=wx_channel, account_id=wx_account, chat_id=wx_chat, text=text, context_token=context_token, store=store, tenant_id=tenant_id, ) else: results["weixin"] = {"ok": False, "error": "weixin_chat_missing"} if not results: return {"ok": True, "skipped": True, "reason": "no_delivery_targets"} ok = all(bool((v or {}).get("ok")) for v in results.values() if isinstance(v, dict)) return {"ok": ok, "channels": results} __all__ = [ "deliver_scheduled_reply", "enqueue_weixin_reply", "extract_context_token_from_inbound_metadata", "persist_channel_context_token", ]