feat(scheduler): confirm before deleting scheduled jobs

Require preview confirmation for schedule_delete, extra confirm_foreign for others' jobs in groups, and mirror the same flow in chat slash commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-07-14 10:11:04 +08:00
parent d036190195
commit 30202dd291
6 changed files with 692 additions and 63 deletions

View file

@ -0,0 +1,197 @@
from __future__ import annotations
import json
from typing import Any
def _delivery_dict(job: Any) -> dict[str, Any]:
raw = getattr(job, "delivery_json", None)
if isinstance(job, dict):
if isinstance(job.get("delivery"), dict):
return dict(job.get("delivery") or {})
raw = job.get("delivery_json")
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 creator_from_delivery(delivery: dict[str, Any] | None) -> dict[str, str]:
raw = (delivery or {}).get("creator") if isinstance(delivery, dict) else None
if not isinstance(raw, dict):
return {}
return {
"user_id": str(raw.get("user_id") or "").strip(),
"external_user_id": str(raw.get("external_user_id") or "").strip(),
"push_name": str(raw.get("push_name") or "").strip(),
"session_id": str(raw.get("session_id") or "").strip(),
}
def merge_delivery_creator(
delivery: dict[str, Any] | None,
*,
user_id: str = "",
external_user_id: str = "",
push_name: str = "",
session_id: str = "",
) -> dict[str, Any]:
out = dict(delivery or {})
creator = {
"user_id": str(user_id or "").strip(),
"external_user_id": str(external_user_id or "").strip(),
"push_name": str(push_name or "").strip(),
"session_id": str(session_id or "").strip(),
}
if any(creator.values()):
out["creator"] = creator
return out
def job_is_foreign(
job: Any,
*,
actor_user_id: str = "",
actor_external_user_id: str = "",
) -> bool:
"""True when the actor is clearly not the creator (best-effort for group chats)."""
delivery = _delivery_dict(job)
creator = creator_from_delivery(delivery)
actor_uid = str(actor_user_id or "").strip()
actor_ext = str(actor_external_user_id or "").strip().lower()
creator_uid = str(creator.get("user_id") or getattr(job, "created_by_user_id", "") or "").strip()
if isinstance(job, dict) and not creator_uid:
creator_uid = str(job.get("created_by_user_id") or "").strip()
creator_ext = str(creator.get("external_user_id") or "").strip().lower()
if actor_ext and creator_ext and actor_ext != creator_ext:
return True
if actor_uid and creator_uid and actor_uid != creator_uid:
# Only treat as foreign when we also lack matching external ids
# (same account owner may create many channel jobs).
if creator_ext or actor_ext:
if creator_ext and actor_ext:
return actor_ext != creator_ext
# external known on one side only — prefer not to block
return False
return True
return False
def resolve_scheduled_jobs_by_id(
store: Any,
*,
tenant_id: str,
job_id: str,
limit: int = 200,
) -> list[Any]:
jid = str(job_id or "").strip()
if not jid:
return []
exact = store.scheduled_job_get(job_id=jid, tenant_id=tenant_id)
if exact is not None:
return [exact]
rows = store.scheduled_job_list(tenant_id=tenant_id, status=None, limit=max(1, min(int(limit), 500)))
return [r for r in rows if str(getattr(r, "id", "") or "").startswith(jid)]
def job_delete_preview(job: Any) -> dict[str, Any]:
delivery = _delivery_dict(job)
creator = creator_from_delivery(delivery)
recipe: dict[str, Any] = {}
try:
raw = getattr(job, "recipe_json", None)
if isinstance(job, dict):
if isinstance(job.get("recipe"), dict):
recipe = dict(job.get("recipe") or {})
else:
raw = job.get("recipe_json")
if not recipe and raw:
parsed = json.loads(str(raw or "{}"))
if isinstance(parsed, dict):
recipe = parsed
except Exception:
recipe = {}
goal = str((recipe or {}).get("goal") or getattr(job, "prompt_text", "") or "").strip()
if isinstance(job, dict) and not goal:
goal = str(job.get("prompt_text") or "").strip()
return {
"id": str(getattr(job, "id", None) or (job.get("id") if isinstance(job, dict) else "") or ""),
"name": str(getattr(job, "name", None) or (job.get("name") if isinstance(job, dict) else "") or ""),
"status": str(getattr(job, "status", None) or (job.get("status") if isinstance(job, dict) else "") or ""),
"schedule_kind": str(
getattr(job, "schedule_kind", None) or (job.get("schedule_kind") if isinstance(job, dict) else "") or ""
),
"schedule_expr": str(
getattr(job, "schedule_expr", None) or (job.get("schedule_expr") if isinstance(job, dict) else "") or ""
),
"prompt_text": str(
getattr(job, "prompt_text", None) or (job.get("prompt_text") if isinstance(job, dict) else "") or ""
),
"goal": goal,
"created_by_user_id": str(
getattr(job, "created_by_user_id", None)
or (job.get("created_by_user_id") if isinstance(job, dict) else "")
or ""
),
"creator": creator,
"next_run_at": str(
getattr(job, "next_run_at", None) or (job.get("next_run_at") if isinstance(job, dict) else "") or ""
)
or None,
}
def job_delete_preview_markdown(preview: dict[str, Any], *, foreign: bool = False, lang: str = "zh") -> str:
creator = preview.get("creator") if isinstance(preview.get("creator"), dict) else {}
creator_label = str(creator.get("push_name") or "").strip()
if not creator_label:
creator_label = str(creator.get("external_user_id") or preview.get("created_by_user_id") or "").strip() or "(未知)"
is_en = str(lang or "").lower().startswith("en")
if is_en:
lines = [
"## Scheduled job delete preview (confirm required)",
f"- **Id**: `{preview.get('id')}`",
f"- **Name**: {preview.get('name') or '(untitled)'}",
f"- **Schedule**: `{preview.get('schedule_kind')}` `{preview.get('schedule_expr')}`",
f"- **Status**: {preview.get('status')}",
f"- **Goal / prompt**: {preview.get('goal') or preview.get('prompt_text') or '(empty)'}",
f"- **Created by**: {creator_label}",
]
if foreign:
lines.append("- **Warning**: This job appears to be created by someone else.")
lines.append("")
lines.append("Reply **confirm delete** with this id, then call schedule_delete with confirmed=true.")
if foreign:
lines.append("Also set confirm_foreign=true after the owner explicitly agrees.")
return "\n".join(lines)
lines = [
"## 删除定时任务预览(需确认)",
f"- **Id**:`{preview.get('id')}`",
f"- **名称**:{preview.get('name') or '(未命名)'}",
f"- **时间**:`{preview.get('schedule_kind')}` `{preview.get('schedule_expr')}`",
f"- **状态**:{preview.get('status')}",
f"- **目标/内容**:{preview.get('goal') or preview.get('prompt_text') or '(空)'}",
f"- **创建者**:{creator_label}",
]
if foreign:
lines.append("- **注意**:该任务看起来是**其他人**创建的。")
lines.append("")
lines.append("请回复**确认删除**并带上该 id;确认后调用 `schedule_delete` 且 `confirmed=true`。")
if foreign:
lines.append("若删除他人任务,还需用户明确同意,并设置 `confirm_foreign=true`。")
return "\n".join(lines)
__all__ = [
"creator_from_delivery",
"job_delete_preview",
"job_delete_preview_markdown",
"job_is_foreign",
"merge_delivery_creator",
"resolve_scheduled_jobs_by_id",
]