"use client"; import { useCallback, useEffect, useState } from "react"; import { AlertTriangle, Bot, Clock, Loader2, Play, Power, RefreshCw, Square, Zap, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { EmptyState } from "@/components/layout/empty-state"; import { InlineAlert } from "@/components/ui/inline-alert"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PageHeader } from "@/components/layout/page-header"; import { Switch } from "@/components/ui/switch"; import { useCapabilities } from "@/lib/capabilities/context"; import { useActionFeedback } from "@/lib/use-action-feedback"; import { AUTOMATION_TASK_TYPES, AUTOMATION_TASK_META, OUTBOUND_TASKS, type AutomationTaskType, } from "@/lib/automation/types"; import { cn } from "@/lib/utils"; interface AutomationRule { id: string; taskType: string; mode: string; dailyCap: number; schedule: string; enabled: boolean; lastRunAt?: string | null; } interface RulesData { accountId: string; accountName: string; automationEnabled: boolean; rules: AutomationRule[]; } interface ActionLog { id: string; taskType: string; action: string; mode: string; status: string; detail?: string | null; error?: string | null; createdAt: string; } const flowLabels: Record = { A: "流程 A", B: "流程 B", both: "流程 A + B", }; export default function AutomationPage() { const [rulesData, setRulesData] = useState(null); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [killOpen, setKillOpen] = useState(false); const { feedback, clearFeedback, showError, showSuccess, showWarning } = useActionFeedback(); const { isReady } = useCapabilities(); const load = useCallback(async (silent = false) => { if (!silent) setLoading(true); try { const [rulesRes, logsRes] = await Promise.all([ fetch("/api/automation/rules"), fetch("/api/automation/logs"), ]); const rulesData = await rulesRes.json(); const logsData = await logsRes.json(); if (rulesRes.ok) setRulesData(rulesData); if (logsRes.ok) setLogs(logsData.logs ?? []); } catch { // ignore } finally { if (!silent) setLoading(false); } }, []); useEffect(() => { load(); }, [load]); async function toggleMaster(enabled: boolean) { setBusy("master"); try { const res = await fetch("/api/automation/account", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ automationEnabled: enabled }), }); const data = await res.json(); if (!res.ok) { showError(data.error ?? "無法切換總開關", "操作失敗"); return; } setRulesData((prev) => (prev ? { ...prev, automationEnabled: enabled } : prev)); showSuccess(enabled ? "自動化已開啟" : "自動化已關閉"); } finally { setBusy(null); } } async function updateRule(taskType: AutomationTaskType, patch: Partial) { setBusy(`rule-${taskType}`); try { const res = await fetch("/api/automation/rules", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ taskType, ...patch }), }); const data = await res.json(); if (!res.ok) { showError(data.error ?? "無法更新規則", "儲存失敗"); return; } setRulesData((prev) => { if (!prev) return prev; const existing = prev.rules.find((r) => r.taskType === taskType); const updated = data.rule ?? { ...existing, ...patch }; const others = prev.rules.filter((r) => r.taskType !== taskType); return { ...prev, rules: [...others, updated] }; }); } finally { setBusy(null); } } async function runNow(taskType: AutomationTaskType) { setBusy(`run-${taskType}`); try { const res = await fetch("/api/automation/run", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ taskType }), }); const data = await res.json(); if (!res.ok) { showError(data.error ?? "執行失敗", "立即執行失敗"); return; } const result = data.result; if (result?.ok) { showSuccess(result.summary ?? "執行完成"); } else { showWarning(result?.summary ?? "執行未完成", "執行結果"); } load(true); } finally { setBusy(null); } } async function killAll() { setBusy("kill"); try { const res = await fetch("/api/automation/kill", { method: "POST" }); const data = await res.json(); if (!res.ok) { showError(data.error ?? "殺停失敗", "操作失敗"); return; } setRulesData((prev) => (prev ? { ...prev, automationEnabled: false } : prev)); showSuccess(`已緊急殺停 ${data.disabledAccounts ?? 0} 個帳號的自動化`); } finally { setBusy(null); } } if (loading) { return (
); } if (!rulesData) { return (
load()}>重新載入} />
); } return (
{feedback && ( )}
{/* 總開關 + 緊急殺停 */}

自動化總開關 {rulesData.accountName}

{rulesData.automationEnabled ? "已開啟 — 符合排程的規則會自動執行" : "已關閉 — 所有自動化任務暫停"}

toggleMaster(checked)} disabled={busy === "master"} />
{/* 規則清單 */}
{AUTOMATION_TASK_TYPES.map((taskType) => { const meta = AUTOMATION_TASK_META[taskType]; const rule = rulesData.rules.find((r) => r.taskType === taskType); const isOutbound = OUTBOUND_TASKS.includes(taskType); const isAutoMode = rule?.mode === "auto"; const capReady = taskType === "scan" ? isReady("scan") : taskType === "generate" ? isReady("generate") : taskType === "publish" ? isReady("publish") : taskType === "outreach" ? isReady("outreach") : isReady("ai"); return (
{meta.label} {flowLabels[meta.flow]} {isOutbound && isAutoMode && rule?.enabled && ( 自動對外 )} {meta.description}
updateRule(taskType, { enabled: checked }) } disabled={busy === `rule-${taskType}`} />
{isOutbound && isAutoMode && (

全自動模式會自動發文/留言,請確認內容品質。

)}
updateRule(taskType, { dailyCap: Math.max(0, Math.min(500, parseInt(e.target.value, 10) || 0)), }) } className="font-mono" />
updateRule(taskType, { schedule: e.target.value }) } placeholder="0 9 * * *" className="font-mono" />
{rule?.lastRunAt && ( 上次執行: {new Date(rule.lastRunAt).toLocaleDateString("zh-TW", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", })} )} {!capReady && rule?.enabled && ( 此任務所需功能尚未就緒 )}
); })}
{/* 執行日誌 */}
執行紀錄 最近 60 筆自動化動作
{logs.length === 0 ? (

尚無執行紀錄

) : (
{logs.map((log) => (
{log.status}

{AUTOMATION_TASK_META[log.taskType as AutomationTaskType]?.label ?? log.taskType} {log.action} · {log.mode}

{log.detail && (

{log.detail}

)} {log.error && (

{log.error}

)}
{new Date(log.createdAt).toLocaleDateString("zh-TW", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", })}
))}
)}
); }