Add WeCom markdown alerts and keep target exits while strategy is paused.

Notify open/close/start/pause/fault with SIM vs LIVE venue labels; configure webhook in Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 23:19:39 +08:00
parent 4d331da962
commit 56a4007130
12 changed files with 602 additions and 16 deletions
+114
View File
@@ -6,6 +6,7 @@ import {
apiFetch,
StrategySettings,
RuntimeSettings,
NotifySettings,
BackupStatus,
downloadBackup,
uploadRestoreBackup,
@@ -89,6 +90,10 @@ export default function SettingsPage() {
const [bnKey, setBnKey] = useState("");
const [bnSecret, setBnSecret] = useState("");
const [runtimeOk, setRuntimeOk] = useState("");
const [wecomEnabled, setWecomEnabled] = useState(false);
const [wecomWebhook, setWecomWebhook] = useState("");
const [wecomMeta, setWecomMeta] = useState<NotifySettings | null>(null);
const [wecomOk, setWecomOk] = useState("");
const [backup, setBackup] = useState<BackupStatus | null>(null);
const [bakAuto, setBakAuto] = useState(true);
@@ -106,6 +111,12 @@ export default function SettingsPage() {
setMode(r.mode === "LIVE" ? "LIVE" : "SIM");
})
.catch(() => undefined);
apiFetch<NotifySettings>("/api/settings/notify")
.then((n) => {
setWecomMeta(n);
setWecomEnabled(n.enabled === true);
})
.catch(() => undefined);
}
function loadBackup() {
@@ -351,6 +362,46 @@ export default function SettingsPage() {
}
}
async function onSaveWecom() {
setErr("");
setWecomOk("");
try {
const body: Record<string, unknown> = { enabled: wecomEnabled };
if (wecomWebhook.trim()) body.webhook_url = wecomWebhook.trim();
const n = await apiFetch<NotifySettings>("/api/settings/notify", {
method: "PUT",
body: JSON.stringify(body),
});
setWecomMeta(n);
setWecomEnabled(n.enabled === true);
setWecomWebhook("");
setWecomOk(
n.enabled
? n.webhook_configured
? "企业微信通知已保存并开启"
: "已开启,但尚未配置 Webhook"
: "企业微信通知已关闭",
);
} catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex));
}
}
async function onTestWecom() {
setErr("");
setWecomOk("");
try {
const n = await apiFetch<NotifySettings & { detail?: string }>(
"/api/settings/notify/test",
{ method: "POST", body: "{}" },
);
setWecomMeta(n);
setWecomOk(n.detail || "测试消息已发送,请查看企业微信群");
} catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex));
}
}
return (
<div className="settings-page">
<h2 style={{ marginTop: 0 }}></h2>
@@ -880,6 +931,65 @@ export default function SettingsPage() {
</div>
</section>
<section className="settings-section">
<h3></h3>
<p style={{ color: "var(--muted)", marginTop: 0 }}>
Webhook MarkdownOPEN/CLOSE/START/PAUSE/FAULT
<span className="mono">
{" "}
{wecomMeta?.venue_label ||
(runtime?.mode === "LIVE"
? `实盘·${(runtime.exchange || "okx").toUpperCase()}`
: "模拟盘")}
</span>
////·
</p>
<div className="settings-fields">
<div className="field">
<label htmlFor="wecomEn"></label>
<select
id="wecomEn"
className="mono"
value={wecomEnabled ? "1" : "0"}
onChange={(e) => setWecomEnabled(e.target.value === "1")}
>
<option value="0"></option>
<option value="1"></option>
</select>
</div>
<div className="field">
<label htmlFor="wecomUrl">Webhook URL</label>
<input
id="wecomUrl"
className="mono"
type="password"
autoComplete="off"
placeholder={
wecomMeta?.webhook_configured
? `已配置 ${wecomMeta.webhook_url_masked || "********"}`
: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
}
value={wecomWebhook}
onChange={(e) => setWecomWebhook(e.target.value)}
/>
</div>
</div>
{wecomOk ? <div className="settings-ok">{wecomOk}</div> : null}
<div className="settings-actions" style={{ gap: 8 }}>
<button className="btn ghost" type="button" onClick={() => void onSaveWecom()}>
</button>
<button
className="btn ghost"
type="button"
onClick={() => void onTestWecom()}
>
</button>
</div>
</section>
<RulesFold
open={runtimeRulesOpen}
onToggle={() => setRuntimeRulesOpen((v) => !v)}
@@ -905,6 +1015,10 @@ export default function SettingsPage() {
= LIVE
Passphrase
</li>
<li>
Webhook
URL 5
</li>
</ul>
</RulesFold>