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:
@@ -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。推送 Markdown,带标识(OPEN/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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user