mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
fix(scheduler): isolate scheduled runs and @mention job creator on WhatsApp
Use a fresh execution session per run while keeping source_session_id for delivery only; filter bot JIDs and default scheduled @mentions to the job creator. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f5cb64a3a4
commit
62ac04c40d
8 changed files with 524 additions and 166 deletions
|
|
@ -334,6 +334,7 @@ def _handle_productivity_commands(
|
|||
session_id=str(session_id or "").strip(),
|
||||
)
|
||||
from runtime.scheduler.job_delete import merge_delivery_creator
|
||||
from runtime.scheduler.whatsapp_mentions import finalize_whatsapp_scheduled_delivery
|
||||
|
||||
delivery = merge_delivery_creator(
|
||||
delivery,
|
||||
|
|
@ -342,6 +343,11 @@ def _handle_productivity_commands(
|
|||
push_name=str(creator_push_name or ""),
|
||||
session_id=str(session_id or "").strip(),
|
||||
)
|
||||
delivery = finalize_whatsapp_scheduled_delivery(
|
||||
delivery,
|
||||
creator_external_user_id=str(creator_external_user_id or ""),
|
||||
creator_push_name=str(creator_push_name or ""),
|
||||
)
|
||||
row = store.scheduled_job_create(
|
||||
tenant_id=tenant_id,
|
||||
name=prompt_text[:40] or "定时提醒",
|
||||
|
|
|
|||
|
|
@ -552,6 +552,12 @@ class ToolExecutor:
|
|||
tool_args["creator_external_user_id"] = external_uid
|
||||
if push_name and not str(tool_args.get("creator_push_name") or "").strip():
|
||||
tool_args["creator_push_name"] = push_name
|
||||
bot_jid_raw = str(md.get("bot_jid") or raw_block.get("botJid") or "").strip()
|
||||
bot_lid_raw = str(md.get("bot_lid") or md.get("botLid") or raw_block.get("botLid") or "").strip()
|
||||
if bot_jid_raw and not str(tool_args.get("whatsapp_bot_jid") or "").strip():
|
||||
tool_args["whatsapp_bot_jid"] = bot_jid_raw
|
||||
if bot_lid_raw and not str(tool_args.get("whatsapp_bot_lid") or "").strip():
|
||||
tool_args["whatsapp_bot_lid"] = bot_lid_raw
|
||||
raw_mentions = md.get("mentioned_jids") or md.get("mentionedJids") or md.get("mentions") or []
|
||||
mention_list = raw_mentions if isinstance(raw_mentions, list) else []
|
||||
bot_jid = str(md.get("bot_jid") or "").strip().lower()
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ from runtime.scheduler.session_resolver import parse_delivery_json
|
|||
from runtime.scheduler.whatsapp_mentions import (
|
||||
encode_whatsapp_outbound_source,
|
||||
format_whatsapp_mention_text,
|
||||
infer_whatsapp_mention_jids_from_text,
|
||||
normalize_whatsapp_mention_jids,
|
||||
resolve_scheduled_whatsapp_mention_targets,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -282,30 +281,13 @@ def deliver_scheduled_reply(
|
|||
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 (text or has_attachments):
|
||||
mention_jids = normalize_whatsapp_mention_jids(wa.get("mention_jids"))
|
||||
if not mention_jids:
|
||||
mention_jids = infer_whatsapp_mention_jids_from_text(
|
||||
text,
|
||||
store=store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
)
|
||||
mention_names = wa.get("mention_names") if isinstance(wa.get("mention_names"), list) else None
|
||||
if mention_names is None and mention_jids:
|
||||
from runtime.scheduler.whatsapp_mentions import _lookup_push_name
|
||||
|
||||
derived_names: list[str] = []
|
||||
for jid in mention_jids:
|
||||
derived_names.append(
|
||||
_lookup_push_name(
|
||||
store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
jid=str(jid or ""),
|
||||
)
|
||||
)
|
||||
if any(derived_names):
|
||||
mention_names = derived_names
|
||||
mention_jids, mention_names = resolve_scheduled_whatsapp_mention_targets(
|
||||
delivery=delivery,
|
||||
reply_text=text,
|
||||
store=store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
)
|
||||
out_text = format_whatsapp_mention_text(
|
||||
text,
|
||||
mention_jids,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ def enqueue_scheduled_job_run(
|
|||
store,
|
||||
job=job,
|
||||
created_by_user_id=str(getattr(job, "created_by_user_id", "") or ""),
|
||||
run_id=str(run.id),
|
||||
)
|
||||
except Exception as exc:
|
||||
store.scheduled_job_run_update(
|
||||
|
|
@ -95,6 +96,7 @@ def enqueue_scheduled_job_run(
|
|||
"trace_id": trace_id,
|
||||
"run_id": agent_run_id,
|
||||
"session_id": resolved.session_id,
|
||||
"source_session_id": str(resolved.source_session_id or getattr(job, "source_session_id", "") or ""),
|
||||
"tenant_id": tenant_id,
|
||||
"user_id": resolved.user_id,
|
||||
"viewer_username": viewer_username,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class ResolvedSession:
|
|||
external_chat_id: str
|
||||
external_user_id: str
|
||||
is_group: bool
|
||||
source_session_id: str = ""
|
||||
|
||||
|
||||
def _ensure_administrator_owner(store: Any, *, tenant_id: str) -> dict[str, Any] | None:
|
||||
|
|
@ -50,6 +51,137 @@ def _ensure_administrator_owner(store: Any, *, tenant_id: str) -> dict[str, Any]
|
|||
}
|
||||
|
||||
|
||||
def _create_scheduled_execution_session(
|
||||
store: Any,
|
||||
*,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
job_name: str,
|
||||
run_id: str = "",
|
||||
) -> str:
|
||||
rid = str(run_id or "").strip()
|
||||
title = f"Scheduled · {job_name}"
|
||||
if rid:
|
||||
title = f"{title} · {rid[:8]}"
|
||||
tid = str(tenant_id or "").strip()
|
||||
uid = str(user_id or "").strip()
|
||||
if uid and tid:
|
||||
sess = store.create_session_for_user(title=title, tenant_id=tid, user_id=uid)
|
||||
else:
|
||||
sess = store.create_session(title)
|
||||
return str(sess.id)
|
||||
|
||||
|
||||
def _resolve_scheduled_channel_context(
|
||||
store: Any,
|
||||
*,
|
||||
job: Any,
|
||||
created_by_user_id: str = "",
|
||||
) -> tuple[str, str, str, str, str, str, bool, str]:
|
||||
"""Return tenant_id, user_id, channel, account_id, external_chat_id, external_user_id, is_group, source_session_id."""
|
||||
tenant_id = str(getattr(job, "tenant_id", "") or "")
|
||||
delivery = parse_delivery_json(str(getattr(job, "delivery_json", "") or "{}"))
|
||||
source_session_id = str(getattr(job, "source_session_id", "") or "").strip()
|
||||
|
||||
if source_session_id:
|
||||
sess = store.get_session_in_tenant(session_id=source_session_id, tenant_id=tenant_id)
|
||||
if sess:
|
||||
owner = store.get_ui_session_owner(session_id=source_session_id) or {}
|
||||
user_id = str(owner.get("user_id") or created_by_user_id or "").strip()
|
||||
if not user_id:
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str((admin or {}).get("user_id") or "")
|
||||
channel_ctx = None
|
||||
lookup = getattr(store, "lookup_channel_session_by_session_id", None)
|
||||
if callable(lookup):
|
||||
channel_ctx = lookup(tenant_id=tenant_id, session_id=source_session_id)
|
||||
if isinstance(channel_ctx, dict) and str(channel_ctx.get("channel") or "").strip():
|
||||
ch = str(channel_ctx.get("channel") or "").strip().lower()
|
||||
account_id = str(channel_ctx.get("account_id") or "").strip()
|
||||
external_chat_id = str(channel_ctx.get("external_chat_id") or "").strip()
|
||||
external_user_id = str(channel_ctx.get("external_user_id") or "").strip()
|
||||
is_group = ch == "whatsapp" and external_chat_id.endswith("@g.us")
|
||||
return (
|
||||
tenant_id,
|
||||
user_id,
|
||||
ch,
|
||||
account_id or ("weixin-default" if ch in {"weixin", "wechat"} else ""),
|
||||
external_chat_id,
|
||||
external_user_id,
|
||||
is_group,
|
||||
source_session_id,
|
||||
)
|
||||
return (
|
||||
tenant_id,
|
||||
user_id,
|
||||
"admin_chat",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
False,
|
||||
source_session_id,
|
||||
)
|
||||
|
||||
wa = delivery.get("whatsapp") if isinstance(delivery.get("whatsapp"), dict) else {}
|
||||
wx = delivery.get("weixin") if isinstance(delivery.get("weixin"), dict) else {}
|
||||
wa_enabled = bool(wa.get("enabled")) and str(wa.get("target_type") or "none") != "none"
|
||||
wx_enabled = bool(wx.get("enabled", True))
|
||||
|
||||
if wa_enabled and str(wa.get("chat_id") or "").strip():
|
||||
chat_id = str(wa.get("chat_id") or "").strip()
|
||||
account_id = str(wa.get("account_id") or os.getenv("AIA_WHATSAPP_ACCOUNT_ID") or "wa-default").strip()
|
||||
target_type = str(wa.get("target_type") or "direct").strip().lower()
|
||||
is_group = target_type == "group" or chat_id.endswith("@g.us")
|
||||
external_user_id = session_user_key(is_group=is_group, external_user_id=chat_id.split("@", 1)[0])
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str(created_by_user_id or (admin or {}).get("user_id") or "")
|
||||
return (
|
||||
tenant_id,
|
||||
user_id,
|
||||
"whatsapp",
|
||||
account_id,
|
||||
chat_id,
|
||||
external_user_id,
|
||||
is_group,
|
||||
"",
|
||||
)
|
||||
|
||||
if wx_enabled:
|
||||
binding = resolve_weixin_binding(store, tenant_id=tenant_id)
|
||||
if not binding:
|
||||
raise RuntimeError("weixin_binding_missing")
|
||||
channel = str(binding.get("channel") or "weixin")
|
||||
account_id = str(binding.get("account_id") or "weixin-default")
|
||||
external_user_id = str(binding.get("external_user_id") or "")
|
||||
external_chat_id = str(binding.get("external_chat_id") or external_user_id)
|
||||
user_id = str(binding.get("user_id") or created_by_user_id or "")
|
||||
return (
|
||||
tenant_id,
|
||||
user_id,
|
||||
channel,
|
||||
account_id,
|
||||
external_chat_id,
|
||||
external_user_id,
|
||||
False,
|
||||
"",
|
||||
)
|
||||
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str(created_by_user_id or (admin or {}).get("user_id") or "")
|
||||
if not user_id:
|
||||
raise RuntimeError("scheduled_session_owner_missing")
|
||||
return (
|
||||
tenant_id,
|
||||
user_id,
|
||||
"admin_chat",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
False,
|
||||
source_session_id,
|
||||
)
|
||||
|
||||
|
||||
def resolve_weixin_binding(store: Any, *, tenant_id: str) -> dict[str, Any] | None:
|
||||
owner = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
if not owner:
|
||||
|
|
@ -123,153 +255,40 @@ def resolve_scheduled_session(
|
|||
*,
|
||||
job: Any,
|
||||
created_by_user_id: str = "",
|
||||
run_id: str = "",
|
||||
) -> ResolvedSession:
|
||||
tenant_id = str(getattr(job, "tenant_id", "") or "")
|
||||
delivery = parse_delivery_json(str(getattr(job, "delivery_json", "") or "{}"))
|
||||
source_session_id = str(getattr(job, "source_session_id", "") or "").strip()
|
||||
job_name = str(getattr(job, "name", "") or "Scheduled task")
|
||||
|
||||
if source_session_id:
|
||||
sess = store.get_session_in_tenant(session_id=source_session_id, tenant_id=tenant_id)
|
||||
if sess:
|
||||
owner = store.get_ui_session_owner(session_id=source_session_id) or {}
|
||||
user_id = str(owner.get("user_id") or created_by_user_id or "").strip()
|
||||
if not user_id:
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str((admin or {}).get("user_id") or "")
|
||||
channel_ctx = None
|
||||
lookup = getattr(store, "lookup_channel_session_by_session_id", None)
|
||||
if callable(lookup):
|
||||
channel_ctx = lookup(tenant_id=tenant_id, session_id=source_session_id)
|
||||
if isinstance(channel_ctx, dict) and str(channel_ctx.get("channel") or "").strip():
|
||||
ch = str(channel_ctx.get("channel") or "").strip().lower()
|
||||
account_id = str(channel_ctx.get("account_id") or "").strip()
|
||||
external_chat_id = str(channel_ctx.get("external_chat_id") or "").strip()
|
||||
external_user_id = str(channel_ctx.get("external_user_id") or "").strip()
|
||||
is_group = ch == "whatsapp" and external_chat_id.endswith("@g.us")
|
||||
return ResolvedSession(
|
||||
session_id=source_session_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
channel=ch,
|
||||
account_id=account_id or ("weixin-default" if ch in {"weixin", "wechat"} else ""),
|
||||
external_chat_id=external_chat_id,
|
||||
external_user_id=external_user_id,
|
||||
is_group=is_group,
|
||||
)
|
||||
return ResolvedSession(
|
||||
session_id=source_session_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
channel="admin_chat",
|
||||
account_id="",
|
||||
external_chat_id="",
|
||||
external_user_id="",
|
||||
is_group=False,
|
||||
)
|
||||
|
||||
wa = delivery.get("whatsapp") if isinstance(delivery.get("whatsapp"), dict) else {}
|
||||
wx = delivery.get("weixin") if isinstance(delivery.get("weixin"), dict) else {}
|
||||
wa_enabled = bool(wa.get("enabled")) and str(wa.get("target_type") or "none") != "none"
|
||||
wx_enabled = bool(wx.get("enabled", True))
|
||||
|
||||
if wa_enabled and str(wa.get("chat_id") or "").strip():
|
||||
chat_id = str(wa.get("chat_id") or "").strip()
|
||||
account_id = str(wa.get("account_id") or os.getenv("AIA_WHATSAPP_ACCOUNT_ID") or "wa-default").strip()
|
||||
target_type = str(wa.get("target_type") or "direct").strip().lower()
|
||||
is_group = target_type == "group" or chat_id.endswith("@g.us")
|
||||
external_user_id = session_user_key(is_group=is_group, external_user_id=chat_id.split("@", 1)[0])
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str(created_by_user_id or (admin or {}).get("user_id") or "")
|
||||
session_id = store.get_or_create_channel_session_v2(
|
||||
tenant_id=tenant_id,
|
||||
channel="whatsapp",
|
||||
account_id=account_id,
|
||||
external_chat_id=chat_id,
|
||||
external_user_id=external_user_id,
|
||||
session_title=f"Scheduled · {job_name}",
|
||||
)
|
||||
from runtime.application.gateway.channel_session_owner import (
|
||||
assign_channel_session_to_account_owner,
|
||||
)
|
||||
|
||||
assign_channel_session_to_account_owner(
|
||||
store,
|
||||
session_id=session_id,
|
||||
channel="whatsapp",
|
||||
account_id=account_id,
|
||||
tenant_id=tenant_id,
|
||||
fallback_user_id=user_id,
|
||||
)
|
||||
return ResolvedSession(
|
||||
session_id=session_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
channel="whatsapp",
|
||||
account_id=account_id,
|
||||
external_chat_id=chat_id,
|
||||
external_user_id=external_user_id,
|
||||
is_group=is_group,
|
||||
)
|
||||
|
||||
if wx_enabled:
|
||||
binding = resolve_weixin_binding(store, tenant_id=tenant_id)
|
||||
if not binding:
|
||||
raise RuntimeError("weixin_binding_missing")
|
||||
channel = str(binding.get("channel") or "weixin")
|
||||
account_id = str(binding.get("account_id") or "weixin-default")
|
||||
external_user_id = str(binding.get("external_user_id") or "")
|
||||
external_chat_id = str(binding.get("external_chat_id") or external_user_id)
|
||||
user_id = str(binding.get("user_id") or created_by_user_id or "")
|
||||
session_id = store.get_or_create_channel_session_v2(
|
||||
tenant_id=tenant_id,
|
||||
channel=channel,
|
||||
account_id=account_id,
|
||||
external_chat_id=external_chat_id,
|
||||
external_user_id=external_user_id,
|
||||
session_title=f"Scheduled · {job_name}",
|
||||
)
|
||||
from runtime.application.gateway.channel_session_owner import (
|
||||
assign_channel_session_to_account_owner,
|
||||
)
|
||||
|
||||
assign_channel_session_to_account_owner(
|
||||
store,
|
||||
session_id=session_id,
|
||||
channel=channel,
|
||||
account_id=account_id,
|
||||
tenant_id=tenant_id,
|
||||
fallback_user_id=user_id,
|
||||
)
|
||||
return ResolvedSession(
|
||||
session_id=session_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
channel=channel,
|
||||
account_id=account_id,
|
||||
external_chat_id=external_chat_id,
|
||||
external_user_id=external_user_id,
|
||||
is_group=False,
|
||||
)
|
||||
|
||||
admin = _ensure_administrator_owner(store, tenant_id=tenant_id)
|
||||
user_id = str(created_by_user_id or (admin or {}).get("user_id") or "")
|
||||
if not user_id:
|
||||
raise RuntimeError("scheduled_session_owner_missing")
|
||||
sess = store.create_session_for_user(
|
||||
title=f"Scheduled · {job_name}",
|
||||
(
|
||||
tenant_id,
|
||||
user_id,
|
||||
channel,
|
||||
account_id,
|
||||
external_chat_id,
|
||||
external_user_id,
|
||||
is_group,
|
||||
source_session_id,
|
||||
) = _resolve_scheduled_channel_context(
|
||||
store,
|
||||
job=job,
|
||||
created_by_user_id=created_by_user_id,
|
||||
)
|
||||
execution_session_id = _create_scheduled_execution_session(
|
||||
store,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
job_name=job_name,
|
||||
run_id=run_id,
|
||||
)
|
||||
return ResolvedSession(
|
||||
session_id=str(sess.id),
|
||||
session_id=execution_session_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
channel="admin_chat",
|
||||
account_id="",
|
||||
external_chat_id="",
|
||||
external_user_id="",
|
||||
is_group=False,
|
||||
channel=channel,
|
||||
account_id=account_id,
|
||||
external_chat_id=external_chat_id,
|
||||
external_user_id=external_user_id,
|
||||
is_group=is_group,
|
||||
source_session_id=source_session_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -273,6 +273,116 @@ def mention_tag_for_jid(jid: str, *, push_name: str = "") -> str:
|
|||
return f"@{local}" if local else ""
|
||||
|
||||
|
||||
def resolve_scheduled_whatsapp_mention_targets(
|
||||
*,
|
||||
delivery: dict[str, Any],
|
||||
reply_text: str,
|
||||
store: Any,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
) -> tuple[list[str], list[str] | None]:
|
||||
"""Resolve @mention targets for scheduled WhatsApp delivery (creator by default)."""
|
||||
from runtime.orchestration.group_ingest import _filter_non_bot_mention_jids, jids_same_user
|
||||
from runtime.scheduler.job_delete import creator_from_delivery
|
||||
|
||||
wa = delivery.get("whatsapp") if isinstance(delivery.get("whatsapp"), dict) else {}
|
||||
creator = creator_from_delivery(delivery)
|
||||
bot_meta: dict[str, Any] = {}
|
||||
bot_jid = str(wa.get("bot_jid") or "").strip()
|
||||
bot_lid = str(wa.get("bot_lid") or "").strip()
|
||||
if bot_jid:
|
||||
bot_meta["bot_jid"] = bot_jid
|
||||
if bot_lid:
|
||||
bot_meta["bot_lid"] = bot_lid
|
||||
|
||||
explicit = _filter_non_bot_mention_jids(
|
||||
normalize_whatsapp_mention_jids(wa.get("mention_jids")),
|
||||
bot_jid=bot_jid or None,
|
||||
metadata=bot_meta or None,
|
||||
)
|
||||
creator_jids = normalize_whatsapp_mention_jids([creator.get("external_user_id") or ""])
|
||||
creator_jid = creator_jids[0] if creator_jids else ""
|
||||
|
||||
if explicit:
|
||||
mention_jids = explicit
|
||||
elif creator_jids:
|
||||
mention_jids = creator_jids
|
||||
else:
|
||||
mention_jids = infer_whatsapp_mention_jids_from_text(
|
||||
reply_text,
|
||||
store=store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
)
|
||||
|
||||
mention_names = wa.get("mention_names") if isinstance(wa.get("mention_names"), list) else None
|
||||
if mention_names is None and creator.get("push_name") and creator_jids:
|
||||
if not mention_jids or (
|
||||
creator_jid and len(mention_jids) == 1 and jids_same_user(mention_jids[0], creator_jid)
|
||||
):
|
||||
mention_names = [str(creator.get("push_name") or "").strip()]
|
||||
if mention_names is None and mention_jids:
|
||||
from runtime.scheduler.whatsapp_mentions import _lookup_push_name
|
||||
|
||||
derived_names: list[str] = []
|
||||
for jid in mention_jids:
|
||||
derived_names.append(
|
||||
_lookup_push_name(
|
||||
store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
jid=str(jid or ""),
|
||||
)
|
||||
)
|
||||
if any(derived_names):
|
||||
mention_names = derived_names
|
||||
return mention_jids, mention_names
|
||||
|
||||
|
||||
def finalize_whatsapp_scheduled_delivery(
|
||||
delivery: dict[str, Any] | None,
|
||||
*,
|
||||
creator_external_user_id: str = "",
|
||||
creator_push_name: str = "",
|
||||
bot_jid: str = "",
|
||||
bot_lid: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Store bot identity for delivery filtering and default @mention to the job creator."""
|
||||
from runtime.orchestration.group_ingest import _filter_non_bot_mention_jids
|
||||
|
||||
out = dict(delivery or {})
|
||||
wa = out.get("whatsapp")
|
||||
if not isinstance(wa, dict) or not wa.get("enabled"):
|
||||
return out
|
||||
wa = dict(wa)
|
||||
bot = str(bot_jid or "").strip()
|
||||
lid = str(bot_lid or "").strip()
|
||||
if bot:
|
||||
wa["bot_jid"] = bot
|
||||
if lid:
|
||||
wa["bot_lid"] = lid
|
||||
meta: dict[str, Any] = {}
|
||||
if bot:
|
||||
meta["bot_jid"] = bot
|
||||
if lid:
|
||||
meta["bot_lid"] = lid
|
||||
jids = _filter_non_bot_mention_jids(
|
||||
normalize_whatsapp_mention_jids(wa.get("mention_jids")),
|
||||
bot_jid=bot or None,
|
||||
metadata=meta or None,
|
||||
)
|
||||
creator_ext = str(creator_external_user_id or "").strip()
|
||||
if not jids and creator_ext:
|
||||
jids = normalize_whatsapp_mention_jids([creator_ext])
|
||||
pname = str(creator_push_name or "").strip()
|
||||
if pname:
|
||||
wa["mention_names"] = [pname]
|
||||
if jids:
|
||||
wa["mention_jids"] = jids
|
||||
out["whatsapp"] = wa
|
||||
return out
|
||||
|
||||
|
||||
def format_whatsapp_mention_text(
|
||||
text: str,
|
||||
mention_jids: list[str],
|
||||
|
|
@ -311,6 +421,8 @@ __all__ = [
|
|||
"decode_whatsapp_outbound_source",
|
||||
"encode_whatsapp_outbound_source",
|
||||
"extract_whatsapp_mention_names",
|
||||
"finalize_whatsapp_scheduled_delivery",
|
||||
"resolve_scheduled_whatsapp_mention_targets",
|
||||
"format_whatsapp_mention_text",
|
||||
"infer_whatsapp_mention_jids_from_names",
|
||||
"infer_whatsapp_mention_jids_from_text",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ from runtime.scheduler.recipe import (
|
|||
)
|
||||
from runtime.scheduler.service import run_scheduled_job_now
|
||||
from runtime.scheduler.system_timezone import default_system_timezone
|
||||
from runtime.scheduler.whatsapp_mentions import merge_whatsapp_mention_jids, merge_whatsapp_mention_names
|
||||
from runtime.scheduler.whatsapp_mentions import (
|
||||
finalize_whatsapp_scheduled_delivery,
|
||||
merge_whatsapp_mention_jids,
|
||||
merge_whatsapp_mention_names,
|
||||
)
|
||||
from runtime.tools.base import ToolSpec
|
||||
from runtime.tools.context_inject import enrich_tool_arguments
|
||||
from runtime.types import normalize_interaction_mode, normalize_requested_specialist
|
||||
|
|
@ -243,6 +247,13 @@ def schedule_create_tool() -> ToolSpec:
|
|||
push_name=str(args.get("creator_push_name") or "").strip(),
|
||||
session_id=str(session_id or ""),
|
||||
)
|
||||
delivery = finalize_whatsapp_scheduled_delivery(
|
||||
delivery,
|
||||
creator_external_user_id=str(args.get("creator_external_user_id") or "").strip(),
|
||||
creator_push_name=str(args.get("creator_push_name") or "").strip(),
|
||||
bot_jid=str(args.get("whatsapp_bot_jid") or "").strip(),
|
||||
bot_lid=str(args.get("whatsapp_bot_lid") or "").strip(),
|
||||
)
|
||||
interaction_mode = normalize_interaction_mode(
|
||||
str(args.get("interaction_mode") or "expert")
|
||||
)
|
||||
|
|
@ -325,6 +336,14 @@ def schedule_create_tool() -> ToolSpec:
|
|||
"type": "string",
|
||||
"description": "Auto-filled channel display name of the creator.",
|
||||
},
|
||||
"whatsapp_bot_jid": {
|
||||
"type": "string",
|
||||
"description": "Auto-filled bot JID for filtering self-mentions on delivery.",
|
||||
},
|
||||
"whatsapp_bot_lid": {
|
||||
"type": "string",
|
||||
"description": "Auto-filled bot LID for filtering self-mentions on delivery.",
|
||||
},
|
||||
"delivery": {"type": "object"},
|
||||
"description": {"type": "string"},
|
||||
},
|
||||
|
|
|
|||
212
tests/test_scheduled_session_isolation.py
Normal file
212
tests/test_scheduled_session_isolation.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from runtime.scheduler.channel_delivery import deliver_scheduled_reply
|
||||
from runtime.scheduler.session_resolver import resolve_scheduled_session
|
||||
from runtime.scheduler.whatsapp_mentions import (
|
||||
finalize_whatsapp_scheduled_delivery,
|
||||
resolve_scheduled_whatsapp_mention_targets,
|
||||
)
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
|
||||
|
||||
class ScheduledSessionIsolationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
self.db = Path(self._tmp.name) / "sched_iso.sqlite"
|
||||
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
|
||||
self.store = SqliteStore(str(self.db))
|
||||
self.tenant_id = str(self.store.create_tenant("Team")["id"])
|
||||
admin = self.store.create_user_account(
|
||||
tenant_id=self.tenant_id,
|
||||
username="administrator",
|
||||
display_name="Admin",
|
||||
role="owner",
|
||||
password_hash="x",
|
||||
is_active=True,
|
||||
)
|
||||
self.admin_id = str(admin["id"])
|
||||
interactive = self.store.create_session("WA interactive")
|
||||
self.interactive_session_id = str(interactive.id)
|
||||
self.store.ensure_ui_session_owner(
|
||||
session_id=self.interactive_session_id,
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.admin_id,
|
||||
)
|
||||
with self.store._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO channel_session_v2
|
||||
(tenant_id, channel, account_id, external_chat_id, external_user_id, session_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
||||
""",
|
||||
(
|
||||
self.tenant_id,
|
||||
"whatsapp",
|
||||
"wa-default",
|
||||
"120363012345678@g.us",
|
||||
"333465375410398@lid",
|
||||
self.interactive_session_id,
|
||||
),
|
||||
)
|
||||
self.store.add_message(
|
||||
session_id=self.interactive_session_id,
|
||||
role="user",
|
||||
content="帮我设个喝水提醒",
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_resolve_scheduled_session_uses_fresh_execution_session(self) -> None:
|
||||
job = mock.MagicMock()
|
||||
job.tenant_id = self.tenant_id
|
||||
job.source_session_id = self.interactive_session_id
|
||||
job.delivery_json = json.dumps(
|
||||
{
|
||||
"whatsapp": {
|
||||
"enabled": True,
|
||||
"target_type": "group",
|
||||
"chat_id": "120363012345678@g.us",
|
||||
"account_id": "wa-default",
|
||||
},
|
||||
"weixin": {"enabled": False},
|
||||
}
|
||||
)
|
||||
job.name = "喝水提醒"
|
||||
|
||||
resolved = resolve_scheduled_session(
|
||||
self.store,
|
||||
job=job,
|
||||
created_by_user_id=self.admin_id,
|
||||
run_id="run-abc12345",
|
||||
)
|
||||
self.assertNotEqual(resolved.session_id, self.interactive_session_id)
|
||||
self.assertEqual(resolved.source_session_id, self.interactive_session_id)
|
||||
self.assertEqual(resolved.external_chat_id, "120363012345678@g.us")
|
||||
rows = self.store.get_messages(session_id=resolved.session_id, limit=10)
|
||||
self.assertEqual(len(rows), 0)
|
||||
|
||||
|
||||
class ScheduledMentionCreatorTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
self.db = Path(self._tmp.name) / "sched_mention.sqlite"
|
||||
self.store = SqliteStore(str(self.db))
|
||||
tenant = self.store.create_tenant("Team")
|
||||
self.tenant_id = str(tenant["id"])
|
||||
self.store.upsert_whatsapp_contact(
|
||||
tenant_id=self.tenant_id,
|
||||
account_id="wa-default",
|
||||
external_user_id="333465375410398@lid",
|
||||
push_name="Egista Hadi Putranto",
|
||||
phone="33346537541839",
|
||||
list_type="whitelist",
|
||||
)
|
||||
self.store.upsert_whatsapp_contact(
|
||||
tenant_id=self.tenant_id,
|
||||
account_id="wa-default",
|
||||
external_user_id="999999999999@lid",
|
||||
push_name="Oliver",
|
||||
phone="",
|
||||
list_type="whitelist",
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_finalize_defaults_mention_to_creator_and_filters_bot(self) -> None:
|
||||
delivery = finalize_whatsapp_scheduled_delivery(
|
||||
{
|
||||
"whatsapp": {
|
||||
"enabled": True,
|
||||
"target_type": "group",
|
||||
"chat_id": "120363012345678@g.us",
|
||||
"mention_jids": ["999999999999@lid"],
|
||||
},
|
||||
"creator": {
|
||||
"external_user_id": "333465375410398@lid",
|
||||
"push_name": "Egista Hadi Putranto",
|
||||
},
|
||||
},
|
||||
creator_external_user_id="333465375410398@lid",
|
||||
creator_push_name="Egista Hadi Putranto",
|
||||
bot_jid="",
|
||||
bot_lid="999999999999@lid",
|
||||
)
|
||||
wa = delivery["whatsapp"]
|
||||
self.assertEqual(wa["mention_jids"], ["333465375410398@lid"])
|
||||
self.assertEqual(wa["mention_names"], ["Egista Hadi Putranto"])
|
||||
|
||||
def test_resolve_scheduled_mention_targets_prefers_creator_when_only_bot_configured(self) -> None:
|
||||
delivery = {
|
||||
"whatsapp": {
|
||||
"enabled": True,
|
||||
"mention_jids": ["999999999999@lid"],
|
||||
"bot_lid": "999999999999@lid",
|
||||
},
|
||||
"creator": {
|
||||
"external_user_id": "333465375410398@lid",
|
||||
"push_name": "Egista Hadi Putranto",
|
||||
},
|
||||
}
|
||||
jids, names = resolve_scheduled_whatsapp_mention_targets(
|
||||
delivery=delivery,
|
||||
reply_text="该喝水啦",
|
||||
store=self.store,
|
||||
tenant_id=self.tenant_id,
|
||||
account_id="wa-default",
|
||||
)
|
||||
self.assertEqual(jids, ["333465375410398@lid"])
|
||||
self.assertEqual(names, ["Egista Hadi Putranto"])
|
||||
|
||||
def test_deliver_scheduled_reply_mentions_creator_not_bot(self) -> None:
|
||||
delivery = finalize_whatsapp_scheduled_delivery(
|
||||
{
|
||||
"whatsapp": {
|
||||
"enabled": True,
|
||||
"target_type": "group",
|
||||
"chat_id": "120363012345678@g.us",
|
||||
"account_id": "wa-default",
|
||||
"mention_jids": ["999999999999@lid"],
|
||||
},
|
||||
},
|
||||
creator_external_user_id="333465375410398@lid",
|
||||
creator_push_name="Egista Hadi Putranto",
|
||||
bot_lid="999999999999@lid",
|
||||
)
|
||||
delivery = {
|
||||
**delivery,
|
||||
"weixin": {"enabled": False},
|
||||
"creator": {
|
||||
"external_user_id": "333465375410398@lid",
|
||||
"push_name": "Egista Hadi Putranto",
|
||||
},
|
||||
}
|
||||
result = deliver_scheduled_reply(
|
||||
self.store,
|
||||
tenant_id=self.tenant_id,
|
||||
reply_text="💧 该喝水啦!",
|
||||
delivery_json=json.dumps(delivery, ensure_ascii=False),
|
||||
)
|
||||
self.assertTrue(result.get("ok"), result)
|
||||
pending = self.store.list_pending_channel_outbound_messages(
|
||||
channel="whatsapp",
|
||||
account_id="wa-default",
|
||||
limit=5,
|
||||
)
|
||||
self.assertEqual(len(pending), 1)
|
||||
out_text = str(pending[0].get("text") or "")
|
||||
self.assertTrue(out_text.startswith("@Egista Hadi Putranto"))
|
||||
source = json.loads(str(pending[0].get("source") or "{}"))
|
||||
self.assertEqual(source.get("mention_jids"), ["333465375410398@lid"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue