mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
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:
@@ -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=封单额(元)
|
||||
|
||||
@@ -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。"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 权限提示 (免费用户) */}
|
||||
|
||||
@@ -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'}`}>
|
||||
|
||||
Reference in New Issue
Block a user