feat(monitor): 推送渠道拆分为飞书/企业微信独立勾选 (#74)

把监控推送从单一总开关 webhook_enabled(bool)升级为渠道数组
webhook_channels(list),飞书与企业微信各自独立勾选,做到「勾哪个
推哪个」。沿用项目已有的 review_push_channels 渠道数组模式。

后端:
- RuleModel 新增 webhook_channels 字段;normalize() 兼容老规则,
  webhook_enabled=True 自动迁移为 ['feishu','wecom']
- _maybe_send_webhook 按 channels 判断:'feishu' in channels 才推
  飞书,'wecom' in channels 才推企业微信
- 新增偏好 webhook_default_channels + PUT 接口;老 webhook_enabled_default
  保留兼容

前端:
- RuleEditor 飞书/企业微信勾选各自绑数组 includes/toggle
- 设置页推送通知卡片飞书/企业微信默认勾选独立
- 连板梯队封单弹窗「飞书」单胶囊改为飞书+企业微信双胶囊
- api.ts 类型 + updateWebhookDefaultChannels
This commit is contained in:
wshy
2026-07-08 13:48:22 +08:00
committed by GitHub
parent 5b2c88c93e
commit 8e56f3efb8
9 changed files with 197 additions and 66 deletions
+2 -1
View File
@@ -49,7 +49,8 @@ class RuleModel(BaseModel):
cooldown_seconds: int = 3600
severity: str = "info" # info | warn | critical
webhook_url: str = "" # Webhook 推送地址 (推送到 QMT 等外部软件, 待定)
webhook_enabled: bool = False
webhook_enabled: bool = False # 兼容老规则 (已由 webhook_channels 取代, 仅做向后兼容读)
webhook_channels: list[str] = [] # 命中时推送的外部渠道 (合法值 'feishu' | 'wecom')
message: str = ""
# ladder 专属 (连板梯队封单监控)
metric: str = "sealed_vol" # sealed_vol=封单量(手) | sealed_amount=封单额(元)
+21 -3
View File
@@ -400,6 +400,7 @@ def get_preferences() -> dict:
"feishu_webhook_secret": preferences.get_feishu_webhook_secret(),
"wecom_webhook_url": preferences.get_wecom_webhook_url(),
"webhook_enabled_default": preferences.get_webhook_enabled_default(),
"webhook_default_channels": preferences.get_webhook_default_channels(),
"sidebar_index_symbols": preferences.get_sidebar_index_symbols(),
"nav_order": preferences.get_nav_order(),
"nav_hidden": preferences.get_nav_hidden(),
@@ -853,10 +854,10 @@ class WebhookEnabledDefaultIn(BaseModel):
@router.put("/preferences/webhook-enabled-default")
def update_webhook_enabled_default(req: WebhookEnabledDefaultIn) -> dict:
"""新建监控规则时是否默认勾选「飞书推送」
"""新建监控规则时是否默认勾选推送 (老布尔接口, 兼容旧前端)
数据模型当前只有飞书一个可用渠道 (QMT/ptrade 待定),故此处仅一个布尔。
单条规则仍可在规则编辑页独立修改此项
数据模型为渠道数组 (webhook_default_channels); 此处转译为
True→['feishu','wecom'], False→[]。新前端请改用 webhook-default-channels 接口
"""
from app.services import preferences
@@ -864,6 +865,23 @@ def update_webhook_enabled_default(req: WebhookEnabledDefaultIn) -> dict:
return {"webhook_enabled_default": saved}
class WebhookDefaultChannelsIn(BaseModel):
channels: list[str] # 多选: ['feishu','wecom'] 等; 空数组=默认不推送
@router.put("/preferences/webhook-default-channels")
def update_webhook_default_channels(req: WebhookDefaultChannelsIn) -> dict:
"""新建监控规则时默认勾选的推送渠道 (多选)。
作为新建规则的默认推送渠道预填, 单条规则仍可在规则编辑页独立修改。
空数组=默认不推送。白名单外的渠道会被过滤掉。
"""
from app.services import preferences
saved = preferences.set_webhook_default_channels(req.channels)
return {"webhook_default_channels": saved}
@router.put("/preferences/quote-interval")
def update_quote_interval(req: QuoteIntervalIn, request: Request) -> dict:
"""更新行情轮询间隔。按档位自动 clamp。"""
+39 -6
View File
@@ -496,20 +496,53 @@ def set_wecom_webhook_url(url: str) -> str:
def get_webhook_enabled_default() -> bool:
"""新建监控规则时是否默认勾选「飞书推送」
"""新建监控规则时是否默认勾选推送 (老布尔, 已由 webhook_default_channels 取代)
数据模型当前只有一个 webhook_enabled 布尔 (即飞书), QMT/ptrade 待定
此默认值供规则编辑器新建规则时预填, 单条规则仍可独立修改。
保留向后兼容: 读取 webhook_default_channels 非空时返回 True
"""
return load().get("webhook_enabled_default", False)
return bool(get_webhook_default_channels())
def set_webhook_enabled_default(enabled: bool) -> bool:
"""保存飞书推送默认勾选态"""
save({"webhook_enabled_default": bool(enabled)})
"""保存推送默认勾选态 (老布尔兼容入口)。
新数据模型为渠道数组; 此处把老布尔转译: True→['feishu','wecom'], False→[]。
"""
set_webhook_default_channels(["feishu", "wecom"] if enabled else [])
return get_webhook_enabled_default()
def get_webhook_default_channels() -> list[str]:
"""新建监控规则时默认勾选的推送渠道 (多选)。
空列表 = 新建规则默认不推送; ['feishu'] = 默认推飞书。
此默认值供规则编辑器新建规则时预填, 单条规则仍可独立修改。
向后兼容: 老版本只有布尔 webhook_enabled_default (勾选即飞书+企业微信双推),
这里把 True 迁移为 ['feishu','wecom'], 还原当时的实际行为。
"""
d = load()
raw = d.get("webhook_default_channels")
if isinstance(raw, list):
return [c for c in raw if c in REVIEW_PUSH_CHANNELS]
# 兼容老布尔开关 (勾选即双推)
if d.get("webhook_enabled_default") is True:
return ["feishu", "wecom"]
return []
def set_webhook_default_channels(channels: list[str]) -> list[str]:
"""保存新建规则默认推送渠道 (多选)。过滤白名单外、去重、保序。空列表 = 不推送。"""
seen: set[str] = set()
cleaned: list[str] = []
for c in channels or []:
if c in REVIEW_PUSH_CHANNELS and c not in seen:
seen.add(c)
cleaned.append(c)
save({"webhook_default_channels": cleaned})
return cleaned
def get_screener_auto_run() -> bool:
"""选股页进入时是否自动运行所有策略 (获取命中数)。默认开。"""
return load().get("screener_auto_run", True)
+13 -9
View File
@@ -895,7 +895,7 @@ class QuoteService:
# cooldown 去重已在 MonitorRuleEngine 做过, 这里只负责转发。
self._maybe_send_system_notifications(all_alerts)
# Webhook 推送 (飞书等外部 IM, 由规则 webhook_enabled 开关控制)。
# Webhook 推送 (飞书等外部 IM, 由规则 webhook_channels 指定渠道)。
# 紧随系统通知, 同样静默降级不阻断主流程。
if rule_events:
self._maybe_send_webhook(rule_events, engine)
@@ -940,10 +940,10 @@ class QuoteService:
return enriched_today
def _maybe_send_webhook(self, rule_events: list[dict], engine) -> None:
"""把告警通过 Webhook 推送到外部 IM (由规则 webhook_enabled 开关控制)。
"""把告警通过 Webhook 推送到外部 IM (由规则 webhook_channels 指定渠道)。
- 飞书 / 企业微信任一已配置即生效 (两个都没配才跳过)
- 仅推送 webhook_enabled=True 的规则触发的告警
- 仅推送 webhook_channels 非空的规则触发的告警, 且只投递被勾选的渠道
- 失败静默, 不阻断主流程
- 去重: 复用 MonitorRuleEngine 的 cooldown, 此处不重复去重
@@ -970,7 +970,10 @@ class QuoteService:
enqueued = 0
for ev in rule_events:
rule = rules.get(ev.get("rule_id"))
if not rule or not rule.get("webhook_enabled"):
# webhook_channels 指定命中的渠道 (['feishu'] / ['wecom'] / ['feishu','wecom'] / []).
# 空列表 = 该规则不推送。仅推送「渠道已选 + 对应地址已配置」的组合。
channels = rule.get("webhook_channels") if rule else None
if not channels:
continue
source = ev.get("source", "")
source_label = source_labels.get(source, source or "通知")
@@ -980,16 +983,17 @@ class QuoteService:
title = f"TickFlow · {source_label}"
body = f"{symbol} {name} {message}".strip() if symbol else (message or name)
# 提交到独立线程池, 不阻塞行情轮询线程 (webhook 慢/重试不拖累实时行情+告警)。
# 飞书 + 企业微信双通道; 应用内 alerts.jsonl 记录与 SSE 已在前面完成, 不依赖
# webhook 成败, 失败由 webhook_adapter 记 WARNING(可见)。
if feishu_url:
# 按渠道独立投递: 飞书 / 企业微信谁被勾选且已配置就推谁。
# 应用内 alerts.jsonl 记录与 SSE 已在前面完成, 不依赖 webhook 成败,
# 失败由 webhook_adapter 记 WARNING(可见)。
if feishu_url and "feishu" in channels:
_WEBHOOK_EXECUTOR.submit(webhook_adapter.send_feishu, feishu_url, title, body, feishu_secret)
enqueued += 1
if wecom_url:
if wecom_url and "wecom" in channels:
_WEBHOOK_EXECUTOR.submit(webhook_adapter.send_wecom, wecom_url, title, body)
enqueued += 1
if enqueued:
logger.info("Webhook 已提交 %d 条 (异步投递, 飞书+企业微信, 失败记 WARNING)", enqueued)
logger.info("Webhook 已提交 %d 条 (异步投递, 按渠道独立投递, 失败记 WARNING)", enqueued)
except Exception as e: # noqa: BLE001
logger.warning("Webhook 提交异常 (不影响告警主流程): %s", e)
+9
View File
@@ -189,6 +189,15 @@ def normalize(rule: dict) -> dict:
r.setdefault("message", "")
r.setdefault("webhook_url", "")
r.setdefault("webhook_enabled", False)
# webhook_channels: 命中时推送的外部渠道 (合法值 'feishu' | 'wecom')。
# 向后兼容: 老规则只有 webhook_enabled 布尔 (当时勾选即飞书+企业微信双推),
# 这里把 webhook_enabled=True 但未带 webhook_channels 的老规则迁移为 ['feishu','wecom'],
# 还原其当时的实际行为, 用户无感知。
if r.get("webhook_channels") is None:
r["webhook_channels"] = ["feishu", "wecom"] if r.get("webhook_enabled") else []
else:
# 防御性过滤, 只保留合法渠道
r["webhook_channels"] = [c for c in r["webhook_channels"] if c in ("feishu", "wecom")]
r.setdefault("created_at", datetime.now(timezone.utc).isoformat())
return r
+60 -18
View File
@@ -46,13 +46,17 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
const options = useQuery({ queryKey: QK.monitorRuleOptions, queryFn: api.monitorRuleOptions })
const { data: prefs } = usePreferences()
const feishuConfigured = !!(prefs?.feishu_webhook_url)
const wecomConfigured = !!(prefs?.wecom_webhook_url)
const [editing] = useState(!!rule)
// 新建规则: 预填全局「默认推送渠道」(飞书), preset 显式指定时以 preset 为准。
// 新建规则: 预填全局「默认推送渠道」(多选数组), preset 显式指定时以 preset 为准。
// 编辑规则: 完全沿用规则自身配置, 不受默认值影响。
const [draft, setDraft] = useState<MonitorRule>(
rule
? { ...rule, conditions: rule.conditions.map(c => ({ ...c })) }
: { ...emptyRule(preset), webhook_enabled: preset?.webhook_enabled ?? !!(prefs?.webhook_enabled_default) },
: {
...emptyRule(preset),
webhook_channels: preset?.webhook_channels ?? (prefs?.webhook_default_channels ?? []),
},
)
const assetType = draft.asset_type ?? 'stock'
// 策略列表跟随资产类型: ETF 只列技术类策略。
@@ -121,6 +125,13 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
setSymbolQuery('')
}
// 勾选/取消勾选某个推送渠道 (飞书 / 企业微信 各自独立)
const toggleChannel = (ch: string) =>
setDraft(d => {
const cur = d.webhook_channels ?? []
return { ...d, webhook_channels: cur.includes(ch) ? cur.filter(c => c !== ch) : [...cur, ch] }
})
const thresholdFields = options.data?.threshold_fields ?? []
const operators = options.data?.operators ?? ['>', '>=', '<', '<=', '==', '!=']
const selectedSignals = draft.conditions.filter(c => c.op === 'truth').map(c => c.field)
@@ -404,7 +415,7 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
</label>
</div>
{/* Webhook 推送 — 飞书可用, QMT/ptrade 待定 */}
{/* Webhook 推送 — 飞书 / 企业微信 可用, QMT/ptrade 待定 */}
<div className="rounded-btn border border-border/40 bg-base/40 p-3 space-y-2">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-medium text-foreground">Webhook </span>
@@ -417,19 +428,36 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={!!draft.webhook_enabled}
onChange={e => setDraft(d => ({ ...d, webhook_enabled: e.target.checked }))}
checked={(draft.webhook_channels ?? []).includes('feishu')}
onChange={() => toggleChannel('feishu')}
className="h-3 w-3 accent-accent cursor-pointer"
/>
<span className="text-[11px] text-foreground"></span>
<span className="text-[9px] text-muted"></span>
{draft.webhook_enabled && (
{(draft.webhook_channels ?? []).includes('feishu') && (
<span className={`ml-auto text-[9px] ${feishuConfigured ? 'text-emerald-500' : 'text-warning'}`}>
{feishuConfigured ? '已配置' : '未配置'}
</span>
)}
</label>
{/* 企业微信 (可用) */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={(draft.webhook_channels ?? []).includes('wecom')}
onChange={() => toggleChannel('wecom')}
className="h-3 w-3 accent-accent cursor-pointer"
/>
<span className="text-[11px] text-foreground"></span>
<span className="text-[9px] text-muted"></span>
{(draft.webhook_channels ?? []).includes('wecom') && (
<span className={`ml-auto text-[9px] ${wecomConfigured ? 'text-emerald-500' : 'text-warning'}`}>
{wecomConfigured ? '已配置' : '未配置'}
</span>
)}
</label>
{/* QMT (待定) */}
<label className="flex items-center gap-2 cursor-not-allowed opacity-50">
<input type="checkbox" disabled className="h-3 w-3 accent-accent" />
@@ -445,18 +473,32 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
</label>
</div>
{/* 飞书勾选但全局未配置 → 提示前往设置 */}
{draft.webhook_enabled && !feishuConfigured && (
<p className="text-[10px] leading-relaxed text-warning/80">
Webhook ,
<Link to="/settings?tab=monitoring" className="text-accent hover:text-accent/80"> </Link>
</p>
)}
{draft.webhook_enabled && feishuConfigured && (
<p className="text-[10px] leading-relaxed text-muted">
,
</p>
)}
{/* 勾选了某渠道但该渠道地址未配置 → 提示前往设置 */}
{(draft.webhook_channels ?? []).length > 0 && (() => {
const selected = draft.webhook_channels ?? []
const unconfigured: string[] = []
if (selected.includes('feishu') && !feishuConfigured) unconfigured.push('飞书')
if (selected.includes('wecom') && !wecomConfigured) unconfigured.push('企业微信')
if (unconfigured.length === 0) return null
return (
<p className="text-[10px] leading-relaxed text-warning/80">
{unconfigured.join('、')},
<Link to="/settings?tab=monitoring" className="text-accent hover:text-accent/80"> </Link>
</p>
)
})()}
{(draft.webhook_channels ?? []).length > 0 && (() => {
const selected = draft.webhook_channels ?? []
const ready: string[] = []
if (selected.includes('feishu') && feishuConfigured) ready.push('飞书')
if (selected.includes('wecom') && wecomConfigured) ready.push('企业微信')
if (ready.length === 0) return null
return (
<p className="text-[10px] leading-relaxed text-muted">
,{ready.join(' + ')}
</p>
)
})()}
</div>
{error && <div className="rounded-btn border border-danger/30 bg-danger/5 px-3 py-2 text-xs text-danger">{error}</div>}
+8 -1
View File
@@ -440,7 +440,8 @@ export interface MonitorRule {
severity: 'info' | 'warn' | 'critical'
message: string
webhook_url?: string
webhook_enabled?: boolean
webhook_enabled?: boolean // 兼容老规则, 已由 webhook_channels 取代
webhook_channels?: string[] // 命中时推送的外部渠道 (合法值 'feishu' | 'wecom')
created_at?: string
// ladder 专属: 封单监控
metric?: 'sealed_vol' | 'sealed_amount' // 量(手) / 额(元)
@@ -792,6 +793,7 @@ export interface Preferences {
feishu_webhook_secret?: string
wecom_webhook_url?: string
webhook_enabled_default?: boolean
webhook_default_channels?: string[]
sidebar_index_symbols: string[]
nav_order: string[]
nav_hidden: string[]
@@ -998,6 +1000,11 @@ export const api = {
method: 'PUT',
body: JSON.stringify({ enabled }),
}),
updateWebhookDefaultChannels: (channels: string[]) =>
request<{ webhook_default_channels: string[] }>('/api/settings/preferences/webhook-default-channels', {
method: 'PUT',
body: JSON.stringify({ channels }),
}),
updatePipelineSchedule: (hour: number, minute: number) =>
request<{ hour: number; minute: number }>('/api/settings/preferences/pipeline-schedule', {
method: 'PUT',
+31 -17
View File
@@ -374,9 +374,9 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
const ruleId = `mr_ladder_${stock.symbol.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`
const existing = monitorRule
// 推送外部开关默认值: 取偏好设置中的全局默认 (已有规则沿用其值)
// 推送渠道默认值: 取偏好设置中的全局默认 (已有规则沿用其值)
const { data: prefs } = usePreferences()
const webhookDefault = prefs?.webhook_enabled_default ?? false
const webhookDefaultChannels = prefs?.webhook_default_channels ?? []
// 单位倍率: 输入值 × 倍率 = 原始单位 (量=手, 额=元)
const VOL_UNITS = [
@@ -404,7 +404,12 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
const mult = units.find(u => u.key === initUnit)?.mult ?? 1
return String(existing.threshold / mult)
})
const [pushExternal, setPushExternal] = useState(existing?.webhook_enabled ?? webhookDefault)
// 推送渠道 (多选): 新建取全局默认, 已有规则沿用其 webhook_channels
const [pushChannels, setPushChannels] = useState<string[]>(
existing?.webhook_channels ?? webhookDefaultChannels,
)
const togglePushChannel = (ch: string) =>
setPushChannels(cur => cur.includes(ch) ? cur.filter(c => c !== ch) : [...cur, ch])
const [saving, setSaving] = useState(false)
const warnLabel = direction === 'down' ? '翘板预警' : '炸板预警'
@@ -440,7 +445,7 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
cooldown_seconds: existing?.cooldown_seconds ?? 600,
severity: 'warn',
message: '',
webhook_enabled: pushExternal,
webhook_channels: pushChannels,
} as MonitorRule)
onChanged()
onClose()
@@ -531,21 +536,30 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
</select>
</div>
{/* 推送渠道: 胶囊标签 (后续可扩展钉钉/企微等), 选中带强调色 */}
{/* 推送渠道: 胶囊标签 (飞书 / 企业微信 各自独立勾选), 选中带强调色 */}
<div className="flex items-center gap-2">
<span className="text-[10px] text-muted shrink-0 w-8"></span>
<button
type="button"
onClick={() => setPushExternal(v => !v)}
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-[10px] font-medium transition-colors border cursor-pointer ${
pushExternal
? 'bg-accent/15 text-accent border-accent/40'
: 'bg-elevated/40 text-muted border-border hover:text-secondary'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${pushExternal ? 'bg-accent' : 'bg-muted/50'}`} />
</button>
{([
{ key: 'feishu', label: '飞书' },
{ key: 'wecom', label: '企业微信' },
] as const).map(ch => {
const on = pushChannels.includes(ch.key)
return (
<button
key={ch.key}
type="button"
onClick={() => togglePushChannel(ch.key)}
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-[10px] font-medium transition-colors border cursor-pointer ${
on
? 'bg-accent/15 text-accent border-accent/40'
: 'bg-elevated/40 text-muted border-border hover:text-secondary'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${on ? 'bg-accent' : 'bg-muted/50'}`} />
{ch.label}
</button>
)
})}
</div>
{/* 权限提示 (免费用户) */}
+14 -11
View File
@@ -54,8 +54,8 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
const refreshPages = prefs?.sse_refresh_pages ?? {}
const limitLadderMonitor = prefs?.limit_ladder_monitor_enabled ?? false
const hasDepth = !!caps?.capabilities?.['depth5.batch']
// 新建监控规则时是否默认勾选飞书推送 (全局默认值, 单条规则可独立修改)
const webhookDefault = prefs?.webhook_enabled_default ?? false
// 新建监控规则时默认勾选的推送渠道 (全局默认值数组, 单条规则可独立修改)
const webhookDefaultChannels = prefs?.webhook_default_channels ?? []
const sidebarIndexSymbols = prefs?.sidebar_index_symbols ?? SIDEBAR_INDEX_OPTIONS.map(i => i.symbol)
const indicesPinned = prefs?.indices_nav_pinned ?? true
const isRunning = quoteStatus?.running ?? false
@@ -128,10 +128,13 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
qc.invalidateQueries({ queryKey: QK.preferences })
}, [qc])
const toggleWebhookDefault = useCallback(async (enabled: boolean) => {
await api.updateWebhookDefault(enabled)
// 勾选/取消勾选某个默认推送渠道 (飞书 / 企业微信 各自独立)
const toggleDefaultChannel = useCallback(async (ch: string, enabled: boolean) => {
const cur = prefs?.webhook_default_channels ?? []
const next = enabled ? [...cur, ch] : cur.filter(c => c !== ch)
await api.updateWebhookDefaultChannels(next)
qc.invalidateQueries({ queryKey: QK.preferences })
}, [qc])
}, [qc, prefs])
const saveFeishuWebhook = useMutation({
mutationFn: ({ url, secret }: { url: string; secret: string }) => api.updateFeishuWebhook(url, secret),
@@ -437,15 +440,15 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
>
<input
type="checkbox"
checked={webhookDefault}
onChange={e => { e.stopPropagation(); toggleWebhookDefault(e.target.checked) }}
checked={webhookDefaultChannels.includes('feishu')}
onChange={e => { e.stopPropagation(); toggleDefaultChannel('feishu', e.target.checked) }}
onClick={e => e.stopPropagation()}
title="作为新建规则的默认推送渠道"
className="h-3 w-3 accent-accent cursor-pointer"
/>
<span className="text-[11px] font-medium text-foreground"></span>
<span className="text-[9px] text-muted"></span>
{webhookDefault && (
{webhookDefaultChannels.includes('feishu') && (
<span className="rounded bg-accent/15 px-1 py-px text-[9px] text-accent"></span>
)}
<span className={`ml-auto text-[9px] ${feishuWebhookUrl ? 'text-emerald-500' : 'text-warning'}`}>
@@ -523,15 +526,15 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
>
<input
type="checkbox"
checked={webhookDefault}
onChange={e => { e.stopPropagation(); toggleWebhookDefault(e.target.checked) }}
checked={webhookDefaultChannels.includes('wecom')}
onChange={e => { e.stopPropagation(); toggleDefaultChannel('wecom', e.target.checked) }}
onClick={e => e.stopPropagation()}
title="作为新建规则的默认推送渠道"
className="h-3 w-3 accent-accent cursor-pointer"
/>
<span className="text-[11px] font-medium text-foreground"></span>
<span className="text-[9px] text-muted"></span>
{webhookDefault && (
{webhookDefaultChannels.includes('wecom') && (
<span className="rounded bg-accent/15 px-1 py-px text-[9px] text-accent"></span>
)}
<span className={`ml-auto text-[9px] ${wecomWebhookUrl ? 'text-emerald-500' : 'text-warning'}`}>