mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 15:24:06 +00:00
fix: tighten cron session deletion UX
This commit is contained in:
parent
c4b64a4caf
commit
b232a52794
@ -572,7 +572,9 @@ nanobot gateway
|
|||||||
DM the bot directly or @mention it in a channel — it should respond!
|
DM the bot directly or @mention it in a channel — it should respond!
|
||||||
|
|
||||||
> [!TIP]
|
> [!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.
|
> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@ -72,7 +72,7 @@ class CronTool(Tool, ContextAware):
|
|||||||
"""Set the current session context for scheduled cron job ownership."""
|
"""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 ""
|
raw_key = f"{ctx.channel}:{ctx.chat_id}" if ctx.channel and ctx.chat_id else ""
|
||||||
self._session_key.set(
|
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):
|
def set_cron_context(self, active: bool):
|
||||||
|
|||||||
@ -8,8 +8,11 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import type { TFunction } from "i18next";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { currentLocale } from "@/i18n";
|
||||||
|
import { fmtDateTime } from "@/lib/format";
|
||||||
import type { SessionAutomationJob } from "@/lib/types";
|
import type { SessionAutomationJob } from "@/lib/types";
|
||||||
|
|
||||||
interface DeleteConfirmProps {
|
interface DeleteConfirmProps {
|
||||||
@ -28,6 +31,7 @@ export function DeleteConfirm({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
}: DeleteConfirmProps) {
|
}: DeleteConfirmProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const locale = currentLocale();
|
||||||
const hasAutomations = automations.length > 0;
|
const hasAutomations = automations.length > 0;
|
||||||
const visibleAutomations = automations.slice(0, 4);
|
const visibleAutomations = automations.slice(0, 4);
|
||||||
const hiddenCount = Math.max(0, automations.length - visibleAutomations.length);
|
const hiddenCount = Math.max(0, automations.length - visibleAutomations.length);
|
||||||
@ -49,23 +53,29 @@ export function DeleteConfirm({
|
|||||||
{hasAutomations
|
{hasAutomations
|
||||||
? t("deleteConfirm.automationsDescription", {
|
? t("deleteConfirm.automationsDescription", {
|
||||||
count: automations.length,
|
count: automations.length,
|
||||||
defaultValue:
|
|
||||||
"This chat has scheduled automations. Deleting it will also delete them.",
|
|
||||||
})
|
})
|
||||||
: t("deleteConfirm.description")}
|
: t("deleteConfirm.description")}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
{hasAutomations ? (
|
{hasAutomations ? (
|
||||||
<div className="mt-4 max-h-32 w-full overflow-y-auto rounded-2xl bg-muted/55 px-3 py-2 text-left">
|
<div className="mt-4 max-h-40 w-full overflow-y-auto rounded-2xl bg-muted/55 px-3 py-2 text-left">
|
||||||
{visibleAutomations.map((job) => (
|
{visibleAutomations.map((job) => (
|
||||||
<div key={job.id} className="truncate text-[13px] leading-6 text-foreground">
|
<div key={job.id} className="min-w-0 py-1.5">
|
||||||
|
<div className="truncate text-[13px] font-medium leading-5 text-foreground">
|
||||||
{job.name || job.id}
|
{job.name || job.id}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-0.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-[11.5px] leading-5 text-muted-foreground">
|
||||||
|
<span className="truncate">
|
||||||
|
{formatAutomationSchedule(job, t, locale)}
|
||||||
|
</span>
|
||||||
|
<span aria-hidden>·</span>
|
||||||
|
<span className="truncate">{formatAutomationNextRun(job, t, locale)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{hiddenCount > 0 ? (
|
{hiddenCount > 0 ? (
|
||||||
<div className="text-[13px] leading-6 text-muted-foreground">
|
<div className="text-[13px] leading-6 text-muted-foreground">
|
||||||
{t("deleteConfirm.moreAutomations", {
|
{t("deleteConfirm.moreAutomations", {
|
||||||
count: hiddenCount,
|
count: hiddenCount,
|
||||||
defaultValue: "+ {{count}} more",
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -85,7 +95,7 @@ export function DeleteConfirm({
|
|||||||
>
|
>
|
||||||
{hasAutomations
|
{hasAutomations
|
||||||
? t("deleteConfirm.confirmWithAutomations", {
|
? t("deleteConfirm.confirmWithAutomations", {
|
||||||
defaultValue: "Delete all",
|
count: automations.length,
|
||||||
})
|
})
|
||||||
: t("deleteConfirm.confirm")}
|
: t("deleteConfirm.confirm")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
@ -94,3 +104,63 @@ export function DeleteConfirm({
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "Delete this chat?",
|
"title": "Delete this chat?",
|
||||||
"description": "This action cannot be undone.",
|
"description": "This action cannot be undone.",
|
||||||
"cancel": "Cancel",
|
"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": {
|
"connection": {
|
||||||
"idle": "Idle",
|
"idle": "Idle",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "¿Eliminar este chat?",
|
"title": "¿Eliminar este chat?",
|
||||||
"description": "Esta acción no se puede deshacer.",
|
"description": "Esta acción no se puede deshacer.",
|
||||||
"cancel": "Cancelar",
|
"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": {
|
"connection": {
|
||||||
"idle": "Inactivo",
|
"idle": "Inactivo",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "Supprimer cette discussion ?",
|
"title": "Supprimer cette discussion ?",
|
||||||
"description": "Cette action est irréversible.",
|
"description": "Cette action est irréversible.",
|
||||||
"cancel": "Annuler",
|
"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": {
|
"connection": {
|
||||||
"idle": "Inactif",
|
"idle": "Inactif",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "Hapus obrolan ini?",
|
"title": "Hapus obrolan ini?",
|
||||||
"description": "Tindakan ini tidak dapat dibatalkan.",
|
"description": "Tindakan ini tidak dapat dibatalkan.",
|
||||||
"cancel": "Batal",
|
"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": {
|
"connection": {
|
||||||
"idle": "Idle",
|
"idle": "Idle",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "このチャットを削除しますか?",
|
"title": "このチャットを削除しますか?",
|
||||||
"description": "この操作は元に戻せません。",
|
"description": "この操作は元に戻せません。",
|
||||||
"cancel": "キャンセル",
|
"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": {
|
"connection": {
|
||||||
"idle": "待機中",
|
"idle": "待機中",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "이 채팅을 삭제할까요?",
|
"title": "이 채팅을 삭제할까요?",
|
||||||
"description": "이 작업은 되돌릴 수 없습니다.",
|
"description": "이 작업은 되돌릴 수 없습니다.",
|
||||||
"cancel": "취소",
|
"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": {
|
"connection": {
|
||||||
"idle": "대기 중",
|
"idle": "대기 중",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "Xóa cuộc trò chuyện này?",
|
"title": "Xóa cuộc trò chuyện này?",
|
||||||
"description": "Không thể hoàn tác thao tác này.",
|
"description": "Không thể hoàn tác thao tác này.",
|
||||||
"cancel": "Hủ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": {
|
"connection": {
|
||||||
"idle": "Rảnh",
|
"idle": "Rảnh",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "删除这个对话?",
|
"title": "删除这个对话?",
|
||||||
"description": "此操作无法撤销。",
|
"description": "此操作无法撤销。",
|
||||||
"cancel": "取消",
|
"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": {
|
"connection": {
|
||||||
"idle": "空闲",
|
"idle": "空闲",
|
||||||
|
|||||||
@ -551,7 +551,22 @@
|
|||||||
"title": "刪除這個對話?",
|
"title": "刪除這個對話?",
|
||||||
"description": "此操作無法復原。",
|
"description": "此操作無法復原。",
|
||||||
"cancel": "取消",
|
"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": {
|
"connection": {
|
||||||
"idle": "閒置",
|
"idle": "閒置",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user