Add WeCom machine name for multi-node push labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,3 +59,5 @@ OPTION_QTY_ETH=2
|
|||||||
# 企业微信群机器人(系统设置页可改)
|
# 企业微信群机器人(系统设置页可改)
|
||||||
WECOM_ENABLED=0
|
WECOM_ENABLED=0
|
||||||
WECOM_WEBHOOK_URL=
|
WECOM_WEBHOOK_URL=
|
||||||
|
# 推送标题前缀,多机时区分,如 云A / 云B
|
||||||
|
WECOM_MACHINE_NAME=
|
||||||
|
|||||||
@@ -558,19 +558,20 @@ async def put_runtime_settings(
|
|||||||
class NotifySettingsBody(BaseModel):
|
class NotifySettingsBody(BaseModel):
|
||||||
enabled: bool | None = None
|
enabled: bool | None = None
|
||||||
webhook_url: str | None = None
|
webhook_url: str | None = None
|
||||||
|
machine_name: str | None = Field(default=None, max_length=64)
|
||||||
|
|
||||||
|
|
||||||
def _notify_payload() -> dict:
|
def _notify_payload() -> dict:
|
||||||
from ..notify import wecom
|
from ..notify import wecom
|
||||||
from ..env_store import mask_secret
|
from ..env_store import mask_secret
|
||||||
|
|
||||||
s = get_settings()
|
|
||||||
url = wecom.wecom_webhook_url()
|
url = wecom.wecom_webhook_url()
|
||||||
return {
|
return {
|
||||||
"enabled": wecom.wecom_enabled(),
|
"enabled": wecom.wecom_enabled(),
|
||||||
"webhook_configured": bool(url),
|
"webhook_configured": bool(url),
|
||||||
"webhook_url_masked": mask_secret(url) if url else None,
|
"webhook_url_masked": mask_secret(url) if url else None,
|
||||||
"venue_label": wecom.venue_label(),
|
"venue_label": wecom.venue_label(),
|
||||||
|
"machine_name": wecom.wecom_machine_name() or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -591,8 +592,13 @@ async def put_notify_settings(
|
|||||||
if body.webhook_url is not None and body.webhook_url.strip():
|
if body.webhook_url is not None and body.webhook_url.strip():
|
||||||
updates["WECOM_WEBHOOK_URL"] = body.webhook_url.strip()
|
updates["WECOM_WEBHOOK_URL"] = body.webhook_url.strip()
|
||||||
get_db().set_setting("wecom_webhook_url", body.webhook_url.strip())
|
get_db().set_setting("wecom_webhook_url", body.webhook_url.strip())
|
||||||
|
if body.machine_name is not None:
|
||||||
|
name = body.machine_name.strip()[:64]
|
||||||
|
updates["WECOM_MACHINE_NAME"] = name
|
||||||
|
get_db().set_setting("wecom_machine_name", name)
|
||||||
if updates:
|
if updates:
|
||||||
upsert_env_keys(updates)
|
upsert_env_keys(updates)
|
||||||
|
get_settings.cache_clear()
|
||||||
return _notify_payload()
|
return _notify_payload()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class Settings(BaseSettings):
|
|||||||
# 企业微信群机器人
|
# 企业微信群机器人
|
||||||
wecom_enabled: bool = False
|
wecom_enabled: bool = False
|
||||||
wecom_webhook_url: str = ""
|
wecom_webhook_url: str = ""
|
||||||
|
wecom_machine_name: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_sim(self) -> bool:
|
def is_sim(self) -> bool:
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ def wecom_webhook_url() -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def wecom_machine_name() -> str:
|
||||||
|
"""多机推送区分用的机器名(设置页 / WECOM_MACHINE_NAME)。"""
|
||||||
|
s = get_settings()
|
||||||
|
name = (getattr(s, "wecom_machine_name", None) or "").strip()
|
||||||
|
if name:
|
||||||
|
return name[:64]
|
||||||
|
try:
|
||||||
|
return (get_db().get_setting("wecom_machine_name", "") or "").strip()[:64]
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def venue_label() -> str | None:
|
def venue_label() -> str | None:
|
||||||
"""实盘时返回「实盘·交易所」;SIM 不展示盘口标签。"""
|
"""实盘时返回「实盘·交易所」;SIM 不展示盘口标签。"""
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
@@ -74,13 +86,21 @@ def venue_label() -> str | None:
|
|||||||
|
|
||||||
def build_markdown(*, tag: str, title: str, lines: list[str] | None = None) -> str:
|
def build_markdown(*, tag: str, title: str, lines: list[str] | None = None) -> str:
|
||||||
body = "\n".join(f"> {ln}" if not ln.startswith(">") else ln for ln in (lines or []))
|
body = "\n".join(f"> {ln}" if not ln.startswith(">") else ln for ln in (lines or []))
|
||||||
|
machine = wecom_machine_name()
|
||||||
venue = venue_label()
|
venue = venue_label()
|
||||||
head = f"## 【{venue}】{title}" if venue else f"## {title}"
|
prefix_parts: list[str] = []
|
||||||
|
if machine:
|
||||||
|
prefix_parts.append(f"【{machine}】")
|
||||||
|
if venue:
|
||||||
|
prefix_parts.append(f"【{venue}】")
|
||||||
|
head = f"## {''.join(prefix_parts)}{title}"
|
||||||
parts = [
|
parts = [
|
||||||
head,
|
head,
|
||||||
f"> **标识**: `{tag}`",
|
f"> **标识**: `{tag}`",
|
||||||
f"> **时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}",
|
f"> **时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
]
|
]
|
||||||
|
if machine:
|
||||||
|
parts.append(f"> **机器**: {machine}")
|
||||||
if body:
|
if body:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(body)
|
parts.append(body)
|
||||||
@@ -216,7 +236,10 @@ def notify_fault(*, title: str, detail: str, dedupe_key: str | None = None) -> N
|
|||||||
|
|
||||||
def notify_test() -> tuple[bool, str]:
|
def notify_test() -> tuple[bool, str]:
|
||||||
venue = venue_label()
|
venue = venue_label()
|
||||||
|
machine = wecom_machine_name()
|
||||||
lines = ["企业微信通知已连通。"]
|
lines = ["企业微信通知已连通。"]
|
||||||
|
if machine:
|
||||||
|
lines.append(f"机器名称: **{machine}**")
|
||||||
if venue:
|
if venue:
|
||||||
lines.append(f"当前盘口标签: **{venue}**")
|
lines.append(f"当前盘口标签: **{venue}**")
|
||||||
content = build_markdown(
|
content = build_markdown(
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ export type NotifySettings = {
|
|||||||
webhook_configured: boolean;
|
webhook_configured: boolean;
|
||||||
webhook_url_masked: string | null;
|
webhook_url_masked: string | null;
|
||||||
venue_label: string;
|
venue_label: string;
|
||||||
|
machine_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function downloadBackup(name: string): Promise<void> {
|
export async function downloadBackup(name: string): Promise<void> {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export default function SettingsPage() {
|
|||||||
const [runtimeOk, setRuntimeOk] = useState("");
|
const [runtimeOk, setRuntimeOk] = useState("");
|
||||||
const [wecomEnabled, setWecomEnabled] = useState(false);
|
const [wecomEnabled, setWecomEnabled] = useState(false);
|
||||||
const [wecomWebhook, setWecomWebhook] = useState("");
|
const [wecomWebhook, setWecomWebhook] = useState("");
|
||||||
|
const [wecomMachineName, setWecomMachineName] = useState("");
|
||||||
const [wecomMeta, setWecomMeta] = useState<NotifySettings | null>(null);
|
const [wecomMeta, setWecomMeta] = useState<NotifySettings | null>(null);
|
||||||
const [wecomOk, setWecomOk] = useState("");
|
const [wecomOk, setWecomOk] = useState("");
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ export default function SettingsPage() {
|
|||||||
.then((n) => {
|
.then((n) => {
|
||||||
setWecomMeta(n);
|
setWecomMeta(n);
|
||||||
setWecomEnabled(n.enabled === true);
|
setWecomEnabled(n.enabled === true);
|
||||||
|
setWecomMachineName(n.machine_name || "");
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
@@ -452,7 +454,10 @@ export default function SettingsPage() {
|
|||||||
setErr("");
|
setErr("");
|
||||||
setWecomOk("");
|
setWecomOk("");
|
||||||
try {
|
try {
|
||||||
const body: Record<string, unknown> = { enabled: wecomEnabled };
|
const body: Record<string, unknown> = {
|
||||||
|
enabled: wecomEnabled,
|
||||||
|
machine_name: wecomMachineName.trim(),
|
||||||
|
};
|
||||||
if (wecomWebhook.trim()) body.webhook_url = wecomWebhook.trim();
|
if (wecomWebhook.trim()) body.webhook_url = wecomWebhook.trim();
|
||||||
const n = await apiFetch<NotifySettings>("/api/settings/notify", {
|
const n = await apiFetch<NotifySettings>("/api/settings/notify", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -460,6 +465,7 @@ export default function SettingsPage() {
|
|||||||
});
|
});
|
||||||
setWecomMeta(n);
|
setWecomMeta(n);
|
||||||
setWecomEnabled(n.enabled === true);
|
setWecomEnabled(n.enabled === true);
|
||||||
|
setWecomMachineName(n.machine_name || "");
|
||||||
setWecomWebhook("");
|
setWecomWebhook("");
|
||||||
setWecomOk(
|
setWecomOk(
|
||||||
n.enabled
|
n.enabled
|
||||||
@@ -483,6 +489,7 @@ export default function SettingsPage() {
|
|||||||
);
|
);
|
||||||
setWecomMeta(n);
|
setWecomMeta(n);
|
||||||
setWecomOk(n.detail || "测试消息已发送,请查看企业微信群");
|
setWecomOk(n.detail || "测试消息已发送,请查看企业微信群");
|
||||||
|
if (n.machine_name != null) setWecomMachineName(n.machine_name || "");
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||||
}
|
}
|
||||||
@@ -1379,6 +1386,18 @@ export default function SettingsPage() {
|
|||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3>企业微信通知</h3>
|
<h3>企业微信通知</h3>
|
||||||
<div className="settings-fields">
|
<div className="settings-fields">
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="wecomMachine">机器名称</label>
|
||||||
|
<input
|
||||||
|
id="wecomMachine"
|
||||||
|
className="mono"
|
||||||
|
autoComplete="off"
|
||||||
|
maxLength={64}
|
||||||
|
placeholder="例如 云A / 云B(推送标题前缀)"
|
||||||
|
value={wecomMachineName}
|
||||||
|
onChange={(e) => setWecomMachineName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="wecomEn">启用通知</label>
|
<label htmlFor="wecomEn">启用通知</label>
|
||||||
<select
|
<select
|
||||||
@@ -1450,14 +1469,19 @@ export default function SettingsPage() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
企业微信:群机器人 Webhook,推送 Markdown,带标识(OPEN/CLOSE/START/PAUSE/FAULT)。
|
企业微信:群机器人 Webhook,推送 Markdown,带标识(OPEN/CLOSE/START/PAUSE/FAULT)。
|
||||||
当前标签:
|
可填「机器名称」区分多机,标题形如【云A】。【实盘·OKX】仅 LIVE 显示。
|
||||||
|
当前机器:
|
||||||
|
<span className="mono">
|
||||||
|
{wecomMachineName || wecomMeta?.machine_name || "未命名"}
|
||||||
|
</span>
|
||||||
|
;盘口标签:
|
||||||
<span className="mono">
|
<span className="mono">
|
||||||
{wecomMeta?.venue_label ||
|
{wecomMeta?.venue_label ||
|
||||||
(runtime?.mode === "LIVE"
|
(runtime?.mode === "LIVE"
|
||||||
? `实盘·${(runtime.exchange || "okx").toUpperCase()}`
|
? `实盘·${(runtime.exchange || "okx").toUpperCase()}`
|
||||||
: "模拟盘")}
|
: "模拟盘")}
|
||||||
</span>
|
</span>
|
||||||
。开仓/平仓/启动/暂停/故障均推送;模拟盘标题为「模拟盘」,实盘为「实盘·交易所」。
|
。开仓/平仓/启动/暂停/故障均推送。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
企业微信:在群里添加「自定义机器人」复制 Webhook。留空保存不覆盖已有
|
企业微信:在群里添加「自定义机器人」复制 Webhook。留空保存不覆盖已有
|
||||||
|
|||||||
Reference in New Issue
Block a user