diff --git a/agent-studio-mini/apps/web/src/Monitoring.tsx b/agent-studio-mini/apps/web/src/Monitoring.tsx new file mode 100644 index 0000000..33eb7c7 --- /dev/null +++ b/agent-studio-mini/apps/web/src/Monitoring.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +type Settings={enabled:boolean;minSpend:number;minOrders:number;minImpressions:number;staleHours:number;fatigueCtrDrop:number;fatigueViewDrop:number;fatigueMinClicks:number}; +type Account={id:string;tenant:string;name:string;currency:string;timezone:string;period?:string;count:number;excluded:number;error?:string;fatigueError?:string;fatigueCount?:number;fatigueExcluded?:number}; +type FatigueRow={key:string;id:string;campaignId:string;variant:string;state:string;reason:string;suggestions:string[];handled:boolean;period:string;account:Account;ctrDrop:number|null;viewDrop:number|null;recent:{ctr:number|null;impressions:number;clicks:number;days:number};baseline:{ctr:number|null;impressions:number;clicks:number;days:number}}; +type Row={key:string;id:string;name:string;target:number|null;roi:number|null;cpa:number|null;ctr:number|null;state:string;reason:string;suggestions:string[];handled:boolean;account?:Account;period?:string;metrics?:{spend:number;orders:number;revenue:number}}; +type State={settings:Settings;accounts?:Account[];lastCheck?:string;period?:string;lastSuccess?:string;error?:string;rows:Row[];fatigueRows?:FatigueRow[];newAlerts:number;busy?:boolean}; +const labels:Record={potential:"高潜候选",under_target:"不达标",no_orders:"消耗无订单",creative_review:"效率预警",observe:"待观察",data_issue:"数据待核实",target_missing:"目标缺失",normal:"正常",inactive:"未启用"}; +export function Monitoring({onClose}:{onClose:()=>void}) { + const [accountFilter,setAccountFilter]=useState(""); + const [view,setView]=useState("campaign"); + const [fatigueState,setFatigueState]=useState(""); + const [visibleCount,setVisibleCount]=useState(50); + const [state,setState]=useState();const [error,setError]=useState("");const [busy,setBusy]=useState(false);const [settings,setSettings]=useState(); + async function request(url:string,method="GET",body?:unknown){const r=await fetch(url,{method,headers:body===undefined?undefined:{"content-type":"application/json"},body:body===undefined?undefined:JSON.stringify(body)});if(!r.ok)throw new Error("操作失败,请稍后重试");return await r.json() as State;} + useEffect(()=>{let live=true;const load=()=>request("/api/monitoring").then(s=>{if(live){setState(s);setSettings(old=>old??s.settings);}}).catch(()=>{if(live)setError("预警服务连接失败");});void load();const timer=setInterval(()=>void load(),30000);return()=>{live=false;clearInterval(timer);};},[]); + useEffect(()=>{const close=(event:KeyboardEvent)=>{if(event.key==="Escape")onClose();};window.addEventListener("keydown",close);return()=>window.removeEventListener("keydown",close);},[onClose]); + async function action(url:string,method="POST",body?:unknown){setBusy(true);setError("");try{setState(await request(url,method,body));}catch(e){setError(String(e));}finally{setBusy(false);}} + const num=(n:number|null|undefined)=>n==null?"—":n.toFixed(2); + const fatigueRows=state?.fatigueRows?.filter(r=>(!accountFilter||`${r.account.tenant}:${r.account.id}`===accountFilter)&&(!fatigueState||r.state===fatigueState))??[]; + return
{if(event.target===event.currentTarget)onClose();}}> +
+

SW Ads · 投放与素材疲劳预警

+

只读监控 · 所有建议需人工确认 · ROI = 平台归因成交金额 ÷ 广告消耗,不代表净利润

+

范围:所有可访问的 TikTok 账户({state?.accounts?.length??0} 个)。每次检查刷新账户列表;各账户按自己的时区、币种和 Campaign 目标判断,不合并不同币种。

+

最近检查:{state?.lastCheck?new Date(state.lastCheck).toLocaleString():"尚未检查"} · 上次全部成功:{state?.lastSuccess?new Date(state.lastSuccess).toLocaleString():"—"}(各账户最近完整业务日,对比前一日)

+ {(error||state?.error)&&

{error||state?.error}。下方可能为上次成功数据,不能视为当前预警。

} + + {state?.accounts?.map(a=>
{a.name}

{a.id} · {a.currency} · {a.timezone} · {a.period??"未完成"} · 电商 Campaign {a.count} 个

{a.error?

{a.error}(检查失败,不代表正常)

:

{a.count?"已完成读取,详见下方预警":"没有可评估的电商 Campaign,不代表账户投放正常"}{a.excluded>0?`;另有 ${a.excluded} 条非电商记录未使用电商 ROI 规则评估`:""}

}
)} + {settings&&
规则设置(不是 Campaign 目标 ROI) + + {([ ["minSpend","最低消耗(账户币种)"],["minOrders","最低订单数"],["minImpressions","CTR最低曝光"],["staleHours","同步有效期(小时)"],["fatigueCtrDrop","素材 CTR 相对降幅(%)"],["fatigueViewDrop","6秒观看率相对降幅(%)"],["fatigueMinClicks","素材基线最低点击"]] as const).map(([key,label])=>)} + +
} +

本次新增提示:{state?.newAlerts??0}。相同日期、Campaign、目标和异常类型不重复计数;“已处理”仅记录本地状态,不执行广告操作。

+ +

+ {view==="fatigue"&&
+

最近3个完整业务日,对比此前7日;按账户、Campaign、素材ID及投放口径分别判断。CTR按总点击÷总曝光计算。阈值是可调整的初始规则,不是平台官方标准。

+

点击率下降且6秒观看率下降才提示“疑似疲劳”。频次未参与判断;观看数据缺失时只提示衰退。ADS_AND_ORGANIC 包含自然流量,不能解释为纯广告效果。商品卡不参加视频疲劳判断。

+ {state?.accounts?.filter(a=>!accountFilter||`${a.tenant}:${a.id}`===accountFilter).map(a=>

{a.name}:{a.error||a.fatigueError||`${a.fatigueCount??0} 条素材结果;排除 ${a.fatigueExcluded??0} 条非视频日记录或未匹配素材`}

)} + {!state?.fatigueRows?.length&&

暂无素材结果。请检查数据覆盖或点击“立即检查”,不能视为素材正常。

} + +

筛选结果 {fatigueRows.length} 条,当前展示 {Math.min(visibleCount,fatigueRows.length)} 条。

+ {fatigueRows.slice(0,visibleCount).map(r=>
+

素材 {r.id} · {r.state==="suspected_fatigue"?"疑似疲劳":r.state==="creative_decline"?"素材衰退 / 疲劳证据不足":labels[r.state]??r.state}{r.handled?" · 已处理":""}

+

{r.account.name} · Campaign {r.campaignId} · {r.variant||"视频"} · {r.period}

+

基线 CTR {num(r.baseline.ctr===null?null:r.baseline.ctr*100)}% → 近期 {num(r.recent.ctr===null?null:r.recent.ctr*100)}% · 相对降幅 {num(r.ctrDrop)}% · 6秒观看率相对降幅 {r.viewDrop===null?"无有效数据":`${num(r.viewDrop)}%`}

+

基线:{r.baseline.impressions} 曝光 / {r.baseline.clicks} 点击 / {r.baseline.days} 个有曝光日;近期:{r.recent.impressions} 曝光 / {r.recent.clicks} 点击 / {r.recent.days} 个有曝光日

+

{r.reason}

    {r.suggestions.map(s=>
  • {s}
  • )}
+
)} + {visibleCountsetVisibleCount(visibleCount+50)}>显示更多素材(50条)} +
} + {view==="campaign"&&<> + {!state?.rows?.length&&

暂无结果。请先检查;空结果不代表所有广告正常。

} + {state?.rows?.filter(r=>!accountFilter||`${r.account?.tenant}:${r.account?.id}`===accountFilter).map(r=>
+

{r.name} 【{labels[r.state]??r.state}】{r.handled?" · 已处理":""}

{r.id} +

账户:{r.account?.name??"旧版结果,请重新检查"} · 币种:{r.account?.currency??"—"} · 日期:{r.period??"—"}

+

目标 ROI {num(r.target)} · 实际 ROI {num(r.roi)} · 消耗 {num(r.metrics?.spend)} · 成交额 {num(r.metrics?.revenue)} · 订单 {r.metrics?.orders??"—"} · CPA {num(r.cpa)} · 商品 CTR {r.ctr==null?"—":`${num(r.ctr*100)}%`}

+

{r.reason}

    {r.suggestions.map(s=>
  • {s}
  • )}
+ +
)} + } +
; +}