mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 22:15:57 +08:00
feat(scheduler): add scheduled jobs with channel-aware delivery
Introduce persisted cron/interval jobs, gateway scheduler loop, worker turns, and admin CRUD/edit UI. Route proactive reminders via the originating chat channel (WhatsApp vs WeChat), harden Weixin outbound with durable queue and PG-compatible polling, and fix chat UI to show each scheduled reminder separately. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5336f8835f
commit
47813f6a94
45 changed files with 4956 additions and 43 deletions
|
|
@ -217,6 +217,15 @@ def _run_startup_hooks(app: FastAPI) -> None:
|
|||
_log_info(f"[startup-prebuild] scheduler_started interval_s={_prewarm_interval_seconds()}")
|
||||
except Exception as exc:
|
||||
_log_warn(f"[startup-prebuild] scheduler_start_failed: {exc}")
|
||||
try:
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
from runtime.scheduler.service import ensure_scheduler_started
|
||||
|
||||
store = get_assistant_store()
|
||||
sid = ensure_scheduler_started(store=store)
|
||||
_log_info(f"[scheduled-jobs] scheduler_started thread={sid}")
|
||||
except Exception as exc:
|
||||
_log_warn(f"[scheduled-jobs] scheduler_start_failed: {exc}")
|
||||
_relocate_root_scan_artifacts()
|
||||
startup_targets = _resolve_startup_workspace_dirs(cfg)
|
||||
initialize_hooks_runtime(cfg=cfg, workspace_dir=startup_targets[0][1])
|
||||
|
|
@ -302,6 +311,31 @@ def create_app() -> FastAPI:
|
|||
changed = store.ack_channel_outbound_message(message_id=msg_id, ok=ok, error=err)
|
||||
return {"ok": changed}
|
||||
|
||||
@app.get("/weixin/outbound/pending")
|
||||
def weixin_outbound_pending(account_id: str = "", limit: int = 20) -> dict[str, Any]:
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
|
||||
store = get_assistant_store()
|
||||
lister = getattr(store, "list_pending_weixin_outbound_messages", None)
|
||||
if not callable(lister):
|
||||
return {"ok": True, "items": []}
|
||||
items = lister(account_id=str(account_id or "").strip(), limit=limit)
|
||||
return {"ok": True, "items": items}
|
||||
|
||||
@app.post("/weixin/outbound/ack")
|
||||
def weixin_outbound_ack(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
|
||||
body = payload if isinstance(payload, dict) else {}
|
||||
msg_id = str(body.get("id") or "").strip()
|
||||
if not msg_id:
|
||||
return {"ok": False, "error": "missing id"}
|
||||
ok = bool(body.get("ok", True))
|
||||
err = str(body.get("error") or "").strip()
|
||||
store = get_assistant_store()
|
||||
changed = store.ack_channel_outbound_message(message_id=msg_id, ok=ok, error=err)
|
||||
return {"ok": changed}
|
||||
|
||||
@app.post("/wecom/inbound")
|
||||
async def wecom_inbound(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = payload if isinstance(payload, dict) else {}
|
||||
|
|
|
|||
|
|
@ -262,12 +262,23 @@ class _IlinkBridge:
|
|||
if eid <= cursor:
|
||||
continue
|
||||
if str(event.get("token") or "") != token:
|
||||
continue
|
||||
ev_tok = str(event.get("token") or "")
|
||||
if ev_tok and ev_tok not in ("default", token):
|
||||
continue
|
||||
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
|
||||
if channel and str(payload.get("channel") or "") != channel:
|
||||
continue
|
||||
if account_id and str(payload.get("account_id") or "") != account_id:
|
||||
continue
|
||||
alt = {"wechat", "weixin"}
|
||||
if not (
|
||||
channel in alt
|
||||
and str(payload.get("channel") or "") in alt
|
||||
):
|
||||
continue
|
||||
req_acct = str(account_id or "").strip()
|
||||
pay_acct = str(payload.get("account_id") or "").strip()
|
||||
if req_acct and pay_acct and pay_acct != req_acct:
|
||||
legacy = {"", "weixin-default"}
|
||||
if pay_acct not in legacy and req_acct not in legacy:
|
||||
continue
|
||||
next_cursor = max(next_cursor, eid)
|
||||
item = dict(payload)
|
||||
item["id"] = str(eid)
|
||||
|
|
@ -319,6 +330,23 @@ async def _process_inbound_and_enqueue(
|
|||
|
||||
return ""
|
||||
|
||||
ctx_token = _extract_context_token(payload)
|
||||
try:
|
||||
from runtime.scheduler.channel_delivery import persist_channel_context_token
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
|
||||
store = get_assistant_store()
|
||||
persist_channel_context_token(
|
||||
store,
|
||||
tenant_id=str(payload.get("tenant_id") or ""),
|
||||
channel=channel,
|
||||
account_id=account_id,
|
||||
external_chat_id=str(chat_id or user_id or ""),
|
||||
context_token=ctx_token,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
out = await asyncio.wait_for(asyncio.to_thread(_process_inbound_payload_usecase, payload), timeout=60.0)
|
||||
except asyncio.TimeoutError:
|
||||
|
|
@ -534,5 +562,32 @@ async def whatsapp_native_reply(
|
|||
}
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
def enqueue_weixin_outbound_reply(
|
||||
*,
|
||||
channel: str,
|
||||
account_id: str,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
context_token: str = "",
|
||||
token: str = "",
|
||||
) -> str:
|
||||
"""Enqueue a proactive Weixin/WeChat outbound message for the sidecar poll loop."""
|
||||
tok = str(
|
||||
token or os.getenv("AIA_WEIXIN_ILINK_TOKEN") or os.getenv("AIA_ILINK_TOKEN") or "default"
|
||||
).strip()
|
||||
ch = str(channel or "wechat").strip().lower()
|
||||
if ch == "weixin":
|
||||
ch = "wechat"
|
||||
_BRIDGE.enqueue_reply(
|
||||
token=tok,
|
||||
channel=ch,
|
||||
account_id=str(account_id or "").strip(),
|
||||
chat_id=str(chat_id or "").strip(),
|
||||
text=str(text or ""),
|
||||
context_token=str(context_token or "").strip(),
|
||||
)
|
||||
return str(_BRIDGE._seq)
|
||||
|
||||
|
||||
__all__ = ["router", "enqueue_weixin_outbound_reply"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue