From b232a527942e0e25307f155ee77dc00e69eda7ab Mon Sep 17 00:00:00 2001 From: chengyongru Date: Fri, 12 Jun 2026 14:51:02 +0800 Subject: [PATCH] fix: tighten cron session deletion UX --- docs/chat-apps.md | 4 +- nanobot/agent/tools/cron.py | 2 +- webui/src/components/DeleteConfirm.tsx | 84 ++++++++++++++++++++++-- webui/src/i18n/locales/en/common.json | 17 ++++- webui/src/i18n/locales/es/common.json | 17 ++++- webui/src/i18n/locales/fr/common.json | 17 ++++- webui/src/i18n/locales/id/common.json | 17 ++++- webui/src/i18n/locales/ja/common.json | 17 ++++- webui/src/i18n/locales/ko/common.json | 17 ++++- webui/src/i18n/locales/vi/common.json | 17 ++++- webui/src/i18n/locales/zh-CN/common.json | 17 ++++- webui/src/i18n/locales/zh-TW/common.json | 17 ++++- 12 files changed, 225 insertions(+), 18 deletions(-) diff --git a/docs/chat-apps.md b/docs/chat-apps.md index 068e7edfc..f23ed7b91 100644 --- a/docs/chat-apps.md +++ b/docs/chat-apps.md @@ -572,7 +572,9 @@ nanobot gateway DM the bot directly or @mention it in a channel — it should respond! > [!TIP] -> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels). +> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels via `groupAllowFrom`). +> - `groupAllowFrom`: channel IDs the bot may respond in when `groupPolicy` is `"allowlist"`. +> - `groupRequireMention`: when `true` and `groupPolicy` is `"allowlist"`, the bot only replies to channels in `groupAllowFrom` **and** only when @mentioned (instead of every message). No effect for `"mention"`/`"open"`. Use this to scope the bot to approved channels while keeping mention-only behavior. > - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs. diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index 6f554d7bd..9587756c5 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -72,7 +72,7 @@ class CronTool(Tool, ContextAware): """Set the current session context for scheduled cron job ownership.""" raw_key = f"{ctx.channel}:{ctx.chat_id}" if ctx.channel and ctx.chat_id else "" self._session_key.set( - raw_key if ctx.session_key in {None, "", UNIFIED_SESSION_KEY} else ctx.session_key + raw_key if ctx.session_key == UNIFIED_SESSION_KEY else (ctx.session_key or "") ) def set_cron_context(self, active: bool): diff --git a/webui/src/components/DeleteConfirm.tsx b/webui/src/components/DeleteConfirm.tsx index 2a58a7dc5..0f5e7f04c 100644 --- a/webui/src/components/DeleteConfirm.tsx +++ b/webui/src/components/DeleteConfirm.tsx @@ -8,8 +8,11 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import type { TFunction } from "i18next"; import { Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { currentLocale } from "@/i18n"; +import { fmtDateTime } from "@/lib/format"; import type { SessionAutomationJob } from "@/lib/types"; interface DeleteConfirmProps { @@ -28,6 +31,7 @@ export function DeleteConfirm({ onConfirm, }: DeleteConfirmProps) { const { t } = useTranslation(); + const locale = currentLocale(); const hasAutomations = automations.length > 0; const visibleAutomations = automations.slice(0, 4); const hiddenCount = Math.max(0, automations.length - visibleAutomations.length); @@ -49,23 +53,29 @@ export function DeleteConfirm({ {hasAutomations ? t("deleteConfirm.automationsDescription", { count: automations.length, - defaultValue: - "This chat has scheduled automations. Deleting it will also delete them.", }) : t("deleteConfirm.description")} {hasAutomations ? ( -
+
{visibleAutomations.map((job) => ( -
- {job.name || job.id} +
+
+ {job.name || job.id} +
+
+ + {formatAutomationSchedule(job, t, locale)} + + · + {formatAutomationNextRun(job, t, locale)} +
))} {hiddenCount > 0 ? (
{t("deleteConfirm.moreAutomations", { count: hiddenCount, - defaultValue: "+ {{count}} more", })}
) : null} @@ -85,7 +95,7 @@ export function DeleteConfirm({ > {hasAutomations ? t("deleteConfirm.confirmWithAutomations", { - defaultValue: "Delete all", + count: automations.length, }) : t("deleteConfirm.confirm")} @@ -94,3 +104,63 @@ export function DeleteConfirm({ ); } + +function formatAutomationSchedule( + job: SessionAutomationJob, + t: TFunction, + locale: string, +): string { + if (job.schedule.kind === "at" && job.schedule.at_ms) { + return t("deleteConfirm.schedule.at", { time: fmtDateTime(job.schedule.at_ms, locale) }); + } + if (job.schedule.kind === "every" && job.schedule.every_ms) { + return t("deleteConfirm.schedule.every", { + duration: formatDuration(job.schedule.every_ms, locale), + }); + } + if (job.schedule.kind === "cron" && job.schedule.expr) { + return job.schedule.tz + ? t("deleteConfirm.schedule.cronWithTz", { + expr: job.schedule.expr, + tz: job.schedule.tz, + }) + : t("deleteConfirm.schedule.cron", { expr: job.schedule.expr }); + } + return t("deleteConfirm.schedule.unknown"); +} + +function formatAutomationNextRun( + job: SessionAutomationJob, + t: TFunction, + locale: string, +): string { + if (!job.enabled) return t("deleteConfirm.next.disabled"); + const next = job.state.next_run_at_ms; + if (!next) return t("deleteConfirm.next.none"); + return t("deleteConfirm.next.label", { time: fmtDateTime(next, locale) }); +} + +function formatDuration(ms: number, locale: string): string { + const units: Array<[Intl.NumberFormatOptions["unit"], number]> = [ + ["day", 86_400_000], + ["hour", 3_600_000], + ["minute", 60_000], + ["second", 1000], + ]; + for (const [unit, size] of units) { + if (ms >= size && ms % size === 0) { + return new Intl.NumberFormat(locale, { + style: "unit", + unit, + unitDisplay: "long", + maximumFractionDigits: 0, + }).format(ms / size); + } + } + return new Intl.NumberFormat(locale, { + style: "unit", + unit: "minute", + unitDisplay: "long", + maximumFractionDigits: 1, + }).format(ms / 60_000); +} diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index 605e66f01..3729d9e13 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -551,7 +551,22 @@ "title": "Delete this chat?", "description": "This action cannot be undone.", "cancel": "Cancel", - "confirm": "Delete" + "confirm": "Delete", + "automationsDescription": "This chat has scheduled automations. Deleting it will also delete them.", + "moreAutomations": "+ {{count}} more", + "confirmWithAutomations": "Delete chat and automations", + "schedule": { + "at": "{{time}}", + "every": "Every {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "Custom schedule" + }, + "next": { + "label": "Next: {{time}}", + "disabled": "Paused", + "none": "No next run" + } }, "connection": { "idle": "Idle", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 165940215..1cf97f7bd 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -551,7 +551,22 @@ "title": "¿Eliminar este chat?", "description": "Esta acción no se puede deshacer.", "cancel": "Cancelar", - "confirm": "Eliminar" + "confirm": "Eliminar", + "automationsDescription": "Este chat tiene automatizaciones programadas. Al eliminarlo también se eliminarán.", + "moreAutomations": "+ {{count}} más", + "confirmWithAutomations": "Eliminar chat y automatizaciones", + "schedule": { + "at": "{{time}}", + "every": "Cada {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "Programación personalizada" + }, + "next": { + "label": "Siguiente: {{time}}", + "disabled": "Pausada", + "none": "Sin próxima ejecución" + } }, "connection": { "idle": "Inactivo", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 2f87ec4b9..ed37dcb30 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -551,7 +551,22 @@ "title": "Supprimer cette discussion ?", "description": "Cette action est irréversible.", "cancel": "Annuler", - "confirm": "Supprimer" + "confirm": "Supprimer", + "automationsDescription": "Cette discussion contient des automatisations planifiées. La supprimer les supprimera aussi.", + "moreAutomations": "+ {{count}} autres", + "confirmWithAutomations": "Supprimer la discussion et les automatisations", + "schedule": { + "at": "{{time}}", + "every": "Tous les {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "Planification personnalisée" + }, + "next": { + "label": "Prochaine exécution : {{time}}", + "disabled": "En pause", + "none": "Aucune prochaine exécution" + } }, "connection": { "idle": "Inactif", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 72770eaea..2aae43477 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -551,7 +551,22 @@ "title": "Hapus obrolan ini?", "description": "Tindakan ini tidak dapat dibatalkan.", "cancel": "Batal", - "confirm": "Hapus" + "confirm": "Hapus", + "automationsDescription": "Obrolan ini memiliki automasi terjadwal. Menghapusnya juga akan menghapus automasi tersebut.", + "moreAutomations": "+ {{count}} lagi", + "confirmWithAutomations": "Hapus obrolan dan automasi", + "schedule": { + "at": "{{time}}", + "every": "Setiap {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "Jadwal khusus" + }, + "next": { + "label": "Berikutnya: {{time}}", + "disabled": "Dijeda", + "none": "Tidak ada jadwal berikutnya" + } }, "connection": { "idle": "Idle", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index fef2bf225..3100e98ba 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -551,7 +551,22 @@ "title": "このチャットを削除しますか?", "description": "この操作は元に戻せません。", "cancel": "キャンセル", - "confirm": "削除" + "confirm": "削除", + "automationsDescription": "このチャットにはスケジュール済みの自動タスクがあります。削除するとそれらも削除されます。", + "moreAutomations": "他 {{count}} 件", + "confirmWithAutomations": "チャットと自動タスクを削除", + "schedule": { + "at": "{{time}}", + "every": "{{duration}} ごと", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "カスタムスケジュール" + }, + "next": { + "label": "次回: {{time}}", + "disabled": "一時停止中", + "none": "次回実行なし" + } }, "connection": { "idle": "待機中", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index dcc0b2172..952e48f68 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -551,7 +551,22 @@ "title": "이 채팅을 삭제할까요?", "description": "이 작업은 되돌릴 수 없습니다.", "cancel": "취소", - "confirm": "삭제" + "confirm": "삭제", + "automationsDescription": "이 채팅에는 예약된 자동화가 있습니다. 채팅을 삭제하면 자동화도 함께 삭제됩니다.", + "moreAutomations": "+ {{count}}개 더", + "confirmWithAutomations": "채팅과 자동화 삭제", + "schedule": { + "at": "{{time}}", + "every": "{{duration}}마다", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "사용자 지정 일정" + }, + "next": { + "label": "다음: {{time}}", + "disabled": "일시 중지됨", + "none": "다음 실행 없음" + } }, "connection": { "idle": "대기 중", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index ee2e8463c..21faca3d7 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -551,7 +551,22 @@ "title": "Xóa cuộc trò chuyện này?", "description": "Không thể hoàn tác thao tác này.", "cancel": "Hủy", - "confirm": "Xóa" + "confirm": "Xóa", + "automationsDescription": "Cuộc trò chuyện này có các tự động hóa đã lên lịch. Xóa cuộc trò chuyện cũng sẽ xóa chúng.", + "moreAutomations": "+ {{count}} mục nữa", + "confirmWithAutomations": "Xóa trò chuyện và tự động hóa", + "schedule": { + "at": "{{time}}", + "every": "Mỗi {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "Lịch tùy chỉnh" + }, + "next": { + "label": "Tiếp theo: {{time}}", + "disabled": "Đã tạm dừng", + "none": "Không có lần chạy tiếp theo" + } }, "connection": { "idle": "Rảnh", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 29aa44baa..29de0f71e 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -551,7 +551,22 @@ "title": "删除这个对话?", "description": "此操作无法撤销。", "cancel": "取消", - "confirm": "删除" + "confirm": "删除", + "automationsDescription": "这个对话有关联的自动任务。删除对话也会删除这些自动任务。", + "moreAutomations": "另有 {{count}} 个", + "confirmWithAutomations": "删除对话和自动任务", + "schedule": { + "at": "{{time}}", + "every": "每 {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "自定义计划" + }, + "next": { + "label": "下次:{{time}}", + "disabled": "已暂停", + "none": "没有下次运行" + } }, "connection": { "idle": "空闲", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index b5b382fac..9143f8047 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -551,7 +551,22 @@ "title": "刪除這個對話?", "description": "此操作無法復原。", "cancel": "取消", - "confirm": "刪除" + "confirm": "刪除", + "automationsDescription": "這個對話有關聯的自動任務。刪除對話也會刪除這些自動任務。", + "moreAutomations": "另有 {{count}} 個", + "confirmWithAutomations": "刪除對話和自動任務", + "schedule": { + "at": "{{time}}", + "every": "每 {{duration}}", + "cron": "Cron {{expr}}", + "cronWithTz": "Cron {{expr}} · {{tz}}", + "unknown": "自訂計畫" + }, + "next": { + "label": "下次:{{time}}", + "disabled": "已暫停", + "none": "沒有下次執行" + } }, "connection": { "idle": "閒置",