Add AI trading supervisor with WeChat push and daily session
Proactive monitoring for manual/hub closes and new opens prevents overtrading via in-app alerts, configurable WeChat links, and supervisor chat. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4541,6 +4541,7 @@ body.hub-page-ai #page-ai {
|
||||
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-panel,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-panel,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="supervisor"] .ai-chat-panel,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="history"] .ai-chat-panel {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
@@ -4551,12 +4552,14 @@ body.hub-page-ai #page-ai {
|
||||
}
|
||||
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-history-panel,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-history-panel {
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-history-panel,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="supervisor"] .ai-chat-history-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-main,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-main {
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-main,
|
||||
body.hub-page-ai .ai-layout[data-ai-mobile-tab="supervisor"] .ai-chat-main {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -5182,6 +5185,31 @@ body.hub-page-ai #page-ai {
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border-soft));
|
||||
}
|
||||
.ai-chat-history-badge.supervisor {
|
||||
color: #c27803;
|
||||
border-color: color-mix(in srgb, #c27803 45%, var(--border-soft));
|
||||
}
|
||||
.ai-msg-row-system {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ai-bubble-system {
|
||||
background: color-mix(in srgb, var(--surface-2) 88%, #c27803 12%);
|
||||
border: 1px solid color-mix(in srgb, var(--border-soft) 70%, #c27803 30%);
|
||||
font-size: 0.92rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ai-bubble-warn {
|
||||
border-color: color-mix(in srgb, var(--danger) 45%, var(--border-soft));
|
||||
}
|
||||
.ai-chat-history-panel.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.ai-chat-new-btn.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.supervisor-settings-grid {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.ai-chat-history-del {
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
|
||||
@@ -82,6 +82,28 @@
|
||||
syncNavVisibility(data);
|
||||
}
|
||||
|
||||
function syncSupervisorSettingsUI(data) {
|
||||
const s = (data && data.supervisor) || {};
|
||||
const enabled = document.getElementById("supervisor-enabled");
|
||||
const prog = document.getElementById("supervisor-wechat-program");
|
||||
const webhook = document.getElementById("supervisor-wechat-webhook");
|
||||
const link = document.getElementById("supervisor-wechat-link");
|
||||
const prefix = document.getElementById("supervisor-wechat-prefix");
|
||||
const daily = document.getElementById("supervisor-daily-warn");
|
||||
const interval = document.getElementById("supervisor-interval-warn");
|
||||
const freq30 = document.getElementById("supervisor-freq-30m");
|
||||
const reopen = document.getElementById("supervisor-reopen-min");
|
||||
if (enabled) enabled.checked = s.enabled !== false;
|
||||
if (prog) prog.checked = s.wechat_on_program_tp_sl !== false;
|
||||
if (webhook) webhook.value = s.wechat_webhook || "";
|
||||
if (link) link.value = s.wechat_link_base || "";
|
||||
if (prefix) prefix.value = s.wechat_prefix || "【交易监管】";
|
||||
if (daily) daily.value = Number(s.manual_close_daily_warn) || 2;
|
||||
if (interval) interval.value = Number(s.interval_warn_minutes) || 15;
|
||||
if (freq30) freq30.value = Number(s.freq_30m_count) || 2;
|
||||
if (reopen) reopen.value = Number(s.reopen_after_close_minutes) || 30;
|
||||
}
|
||||
|
||||
function positionTableHeadHtml(compact) {
|
||||
const pnlTh = showAccountPnlPref() ? "<th>浮盈</th>" : "";
|
||||
const cls = compact ? " data-table data-table-positions" : "";
|
||||
@@ -1085,6 +1107,7 @@
|
||||
syncHubAiMobileViewport();
|
||||
if (page === "monitor") startMonitorPoll();
|
||||
else stopMonitorPoll();
|
||||
if (page !== "ai") closeSupervisorStream();
|
||||
if (page === "dashboard" && window.hubDashboardPage) {
|
||||
window.hubDashboardPage.init();
|
||||
} else if (window.hubDashboardPage && window.hubDashboardPage.destroy) {
|
||||
@@ -1506,7 +1529,22 @@
|
||||
}
|
||||
|
||||
const AI_MOBILE_TAB_KEY = "hub_ai_mobile_tab";
|
||||
const AI_MOBILE_CHAT_TABS = new Set(["trading", "general"]);
|
||||
const AI_MOBILE_CHAT_TABS = new Set(["trading", "general", "supervisor"]);
|
||||
let aiSupervisorSessionCache = null;
|
||||
let supervisorEventSource = null;
|
||||
let localSupervisorVersion = 0;
|
||||
let supervisorReconnectTimer = null;
|
||||
|
||||
function isSupervisorMode() {
|
||||
return aiSelectedBotMode === "supervisor";
|
||||
}
|
||||
|
||||
function normalizeAiBotMode(mode) {
|
||||
const m = (mode || "").trim().toLowerCase();
|
||||
if (m === "general") return "general";
|
||||
if (m === "supervisor") return "supervisor";
|
||||
return "trading";
|
||||
}
|
||||
|
||||
function normalizeAiMobileTab(tab) {
|
||||
const raw = (tab || "").trim().toLowerCase();
|
||||
@@ -1540,6 +1578,11 @@
|
||||
});
|
||||
if (AI_MOBILE_CHAT_TABS.has(active)) {
|
||||
updateAiBotTabs(active);
|
||||
if (active === "supervisor") {
|
||||
void loadAiSupervisorSession().then(() => connectSupervisorStream());
|
||||
} else {
|
||||
closeSupervisorStream();
|
||||
}
|
||||
scrollAiChatToEnd();
|
||||
}
|
||||
if (active === "history") {
|
||||
@@ -1556,8 +1599,16 @@
|
||||
const tab = btn.dataset.aiTab || "trading";
|
||||
if (tab === "new") {
|
||||
const prev = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading");
|
||||
const botMode = prev === "general" ? "general" : "trading";
|
||||
void newAiChat(botMode);
|
||||
const botMode = prev === "general" ? "general" : prev === "supervisor" ? "supervisor" : "trading";
|
||||
if (botMode === "supervisor") {
|
||||
void switchToSupervisorMode();
|
||||
} else {
|
||||
void newAiChat(botMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (tab === "supervisor") {
|
||||
void switchToSupervisorMode();
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(AI_MOBILE_TAB_KEY, tab);
|
||||
@@ -3745,6 +3796,7 @@
|
||||
loadMacroCalendarUI();
|
||||
loadSettings().then((data) => {
|
||||
syncDisplayPrefsUI(data);
|
||||
syncSupervisorSettingsUI(data);
|
||||
renderSettingsList(data);
|
||||
});
|
||||
}
|
||||
@@ -3785,6 +3837,15 @@
|
||||
const archiveCb = document.getElementById("pref-show-nav-archive");
|
||||
const aiCb = document.getElementById("pref-show-nav-ai");
|
||||
const calcCb = document.getElementById("pref-show-nav-calculator");
|
||||
const supEnabled = document.getElementById("supervisor-enabled");
|
||||
const supProg = document.getElementById("supervisor-wechat-program");
|
||||
const supWebhook = document.getElementById("supervisor-wechat-webhook");
|
||||
const supLink = document.getElementById("supervisor-wechat-link");
|
||||
const supPrefix = document.getElementById("supervisor-wechat-prefix");
|
||||
const supDaily = document.getElementById("supervisor-daily-warn");
|
||||
const supInterval = document.getElementById("supervisor-interval-warn");
|
||||
const supFreq30 = document.getElementById("supervisor-freq-30m");
|
||||
const supReopen = document.getElementById("supervisor-reopen-min");
|
||||
return {
|
||||
version: 1,
|
||||
display: {
|
||||
@@ -3796,6 +3857,17 @@
|
||||
show_nav_ai: aiCb ? !!aiCb.checked : true,
|
||||
show_nav_calculator: calcCb ? !!calcCb.checked : true,
|
||||
},
|
||||
supervisor: {
|
||||
enabled: supEnabled ? !!supEnabled.checked : true,
|
||||
wechat_webhook: supWebhook ? supWebhook.value.trim() : "",
|
||||
wechat_link_base: supLink ? supLink.value.trim() : "",
|
||||
wechat_prefix: supPrefix ? supPrefix.value.trim() : "【交易监管】",
|
||||
wechat_on_program_tp_sl: supProg ? !!supProg.checked : true,
|
||||
manual_close_daily_warn: supDaily ? Number(supDaily.value) || 2 : 2,
|
||||
interval_warn_minutes: supInterval ? Number(supInterval.value) || 15 : 15,
|
||||
freq_30m_count: supFreq30 ? Number(supFreq30.value) || 2 : 2,
|
||||
reopen_after_close_minutes: supReopen ? Number(supReopen.value) || 30 : 30,
|
||||
},
|
||||
exchanges: rows.map((card) => {
|
||||
const caps = [];
|
||||
if (card.querySelector(".cap-key").checked) caps.push("key");
|
||||
@@ -3830,6 +3902,7 @@
|
||||
if (j.settings) {
|
||||
settingsCache = j.settings;
|
||||
syncDisplayPrefsUI(j.settings);
|
||||
syncSupervisorSettingsUI(j.settings);
|
||||
renderSettingsList(j.settings);
|
||||
loadSettingsMetaLine();
|
||||
} else {
|
||||
@@ -4036,19 +4109,26 @@
|
||||
}
|
||||
|
||||
function updateAiBotTabs(mode) {
|
||||
const m = mode === "general" ? "general" : "trading";
|
||||
const m = normalizeAiBotMode(mode);
|
||||
aiSelectedBotMode = m;
|
||||
document.querySelectorAll(".ai-bot-tab").forEach((btn) => {
|
||||
const on = (btn.dataset.bot || "trading") === m;
|
||||
const on = normalizeAiBotMode(btn.dataset.bot || "trading") === m;
|
||||
btn.classList.toggle("is-active", on);
|
||||
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||||
});
|
||||
const newBtn = document.getElementById("btn-ai-chat-new");
|
||||
if (newBtn) newBtn.classList.toggle("hidden", m === "supervisor");
|
||||
const histPanel = document.querySelector(".ai-chat-history-panel");
|
||||
if (histPanel) histPanel.classList.toggle("hidden", m === "supervisor");
|
||||
const input = document.getElementById("ai-chat-input");
|
||||
if (input) {
|
||||
input.placeholder =
|
||||
m === "general"
|
||||
? "随便聊点什么,不绑交易数据…可直接 Ctrl+V 粘贴截图"
|
||||
: "聊聊行情、心态、纪律、执行…;可直接 Ctrl+V 粘贴截图";
|
||||
if (m === "general") {
|
||||
input.placeholder = "随便聊点什么,不绑交易数据…可直接 Ctrl+V 粘贴截图";
|
||||
} else if (m === "supervisor") {
|
||||
input.placeholder = "回应监管提醒、说说为什么又开了一单…";
|
||||
} else {
|
||||
input.placeholder = "聊聊行情、心态、纪律、执行…;可直接 Ctrl+V 粘贴截图";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4090,20 +4170,33 @@
|
||||
|
||||
function renderAiChatRow(role, content, extraClass, attachments, rowOpts) {
|
||||
const opts = rowOpts || {};
|
||||
const botMode = opts.botMode === "general" ? "general" : "trading";
|
||||
const botMode = normalizeAiBotMode(opts.botMode || aiSelectedBotMode);
|
||||
const isUser = role === "user";
|
||||
const label = isUser ? "主人" : botMode === "general" ? "助手" : "交易教练";
|
||||
const rowCls = isUser ? "ai-msg-row-user" : "ai-msg-row-coach";
|
||||
const bubbleCls = isUser ? "ai-bubble-user" : "ai-bubble-assistant";
|
||||
const isSystem = role === "system";
|
||||
let label = "主人";
|
||||
if (isSystem) label = "监管";
|
||||
else if (!isUser) label = botMode === "general" ? "助手" : botMode === "supervisor" ? "监管AI" : "交易教练";
|
||||
const rowCls = isUser
|
||||
? "ai-msg-row-user"
|
||||
: isSystem
|
||||
? "ai-msg-row-system"
|
||||
: "ai-msg-row-coach";
|
||||
const bubbleCls = isUser
|
||||
? "ai-bubble-user"
|
||||
: isSystem
|
||||
? "ai-bubble-system"
|
||||
: "ai-bubble-assistant";
|
||||
const isThinking = extraClass && String(extraClass).includes("ai-bubble-thinking");
|
||||
const isError =
|
||||
!isUser &&
|
||||
!isSystem &&
|
||||
!isThinking &&
|
||||
/^(AI 调用失败|AI 生成失败)/.test(String(content || "").trim());
|
||||
const mdKey =
|
||||
!isUser && !isThinking && opts.cacheKey ? String(opts.cacheKey) : "";
|
||||
const bubbleInner = isUser || isThinking ? esc(content || "") : renderHubMarkdown(content || "", mdKey);
|
||||
const mdCls = !isUser && !isThinking ? " ai-result-md" : "";
|
||||
!isUser && !isSystem && !isThinking && opts.cacheKey ? String(opts.cacheKey) : "";
|
||||
const bubbleInner =
|
||||
isUser || isThinking || isSystem ? esc(content || "") : renderHubMarkdown(content || "", mdKey);
|
||||
const mdCls = !isUser && !isSystem && !isThinking ? " ai-result-md" : "";
|
||||
const attList = Array.isArray(attachments) ? attachments : [];
|
||||
const attHtml = attList.length
|
||||
? `<div class="ai-msg-attachments">${attList
|
||||
@@ -4129,15 +4222,20 @@
|
||||
const box = document.getElementById("ai-chat-messages");
|
||||
const title = document.getElementById("ai-chat-title");
|
||||
if (!box) return;
|
||||
const msgs = (session && session.messages) || [];
|
||||
const botMode = (session && session.bot_mode) || aiSelectedBotMode || "trading";
|
||||
const activeSession = isSupervisorMode() ? aiSupervisorSessionCache || session : session;
|
||||
const msgs = (activeSession && activeSession.messages) || [];
|
||||
const botMode = normalizeAiBotMode((activeSession && activeSession.bot_mode) || aiSelectedBotMode);
|
||||
if (title) {
|
||||
const modeLabel = botMode === "general" ? "普通聊天" : "交易教练";
|
||||
const sessionTitle = session && session.title ? String(session.title) : "";
|
||||
const modeLabel =
|
||||
botMode === "general" ? "普通聊天" : botMode === "supervisor" ? "交易监管" : "交易教练";
|
||||
const sessionTitle = activeSession && activeSession.title ? String(activeSession.title) : "";
|
||||
if (isMobileAiLayout()) {
|
||||
title.textContent = sessionTitle && sessionTitle !== "新对话"
|
||||
? sessionTitle
|
||||
: modeLabel;
|
||||
title.textContent =
|
||||
botMode === "supervisor"
|
||||
? sessionTitle || "今日监管"
|
||||
: sessionTitle && sessionTitle !== "新对话"
|
||||
? sessionTitle
|
||||
: modeLabel;
|
||||
} else {
|
||||
title.textContent = sessionTitle
|
||||
? `${modeLabel} · ${sessionTitle}`
|
||||
@@ -4150,21 +4248,24 @@
|
||||
const hint =
|
||||
botMode === "general"
|
||||
? "普通聊天不注入交易快照;发消息后可点气泡下方「复制」。可粘贴截图或上传附件。"
|
||||
: "交易教练会结合四户监控数据陪聊;发消息后可点气泡下方「复制」。可粘贴截图或点「附件」上传图片/文档。";
|
||||
: botMode === "supervisor"
|
||||
? "今日监管为长会话:手动/中控开平仓与新开仓会自动推送;程序止盈止损会鼓励性提醒。可直接回复继续聊。"
|
||||
: "交易教练会结合四户监控数据陪聊;发消息后可点气泡下方「复制」。可粘贴截图或点「附件」上传图片/文档。";
|
||||
box.innerHTML = `<p class="ai-placeholder">${hint}</p>`;
|
||||
return;
|
||||
}
|
||||
const sessionId = session && session.id ? String(session.id) : "local";
|
||||
const sessionId = activeSession && activeSession.id ? String(activeSession.id) : "local";
|
||||
let html = msgs
|
||||
.map((m, idx) =>
|
||||
renderAiChatRow(
|
||||
m.role === "user" ? "user" : "assistant",
|
||||
.map((m, idx) => {
|
||||
const role = m.role === "user" ? "user" : m.role === "system" ? "system" : "assistant";
|
||||
return renderAiChatRow(
|
||||
role,
|
||||
m.content || "",
|
||||
null,
|
||||
m.level === "warn" ? "ai-bubble-warn" : null,
|
||||
m.attachments,
|
||||
{ botMode, msgIdx: idx, cacheKey: sessionId + ":" + idx }
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
if (options.pendingUser) {
|
||||
html += renderAiChatRow("user", options.pendingUser, null, options.pendingAttachments);
|
||||
@@ -4187,6 +4288,66 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAiSupervisorSession() {
|
||||
const r = await apiFetch("/api/ai/supervisor/session");
|
||||
const j = await r.json();
|
||||
aiSupervisorSessionCache = j.session || null;
|
||||
if (isSupervisorMode()) {
|
||||
renderAiChatMessages(aiSupervisorSessionCache);
|
||||
}
|
||||
updateAiBotTabs("supervisor");
|
||||
return j;
|
||||
}
|
||||
|
||||
async function switchToSupervisorMode() {
|
||||
updateAiBotTabs("supervisor");
|
||||
if (isMobileAiLayout()) {
|
||||
localStorage.setItem(AI_MOBILE_TAB_KEY, "supervisor");
|
||||
applyAiMobileTab("supervisor");
|
||||
}
|
||||
try {
|
||||
await loadAiSupervisorSession();
|
||||
connectSupervisorStream();
|
||||
scrollAiChatToEnd();
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSupervisorStream() {
|
||||
if (supervisorEventSource) {
|
||||
supervisorEventSource.close();
|
||||
supervisorEventSource = null;
|
||||
}
|
||||
if (supervisorReconnectTimer) {
|
||||
clearTimeout(supervisorReconnectTimer);
|
||||
supervisorReconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function connectSupervisorStream() {
|
||||
closeSupervisorStream();
|
||||
if (currentPage() !== "ai" || !isSupervisorMode()) return;
|
||||
supervisorEventSource = new EventSource("/api/ai/supervisor/stream");
|
||||
supervisorEventSource.addEventListener("supervisor", (ev) => {
|
||||
try {
|
||||
const st = JSON.parse(ev.data || "{}");
|
||||
const ver = Number(st.supervisor_version) || 0;
|
||||
if (ver !== localSupervisorVersion) {
|
||||
localSupervisorVersion = ver;
|
||||
void loadAiSupervisorSession();
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
supervisorEventSource.onerror = () => {
|
||||
closeSupervisorStream();
|
||||
if (supervisorReconnectTimer) clearTimeout(supervisorReconnectTimer);
|
||||
supervisorReconnectTimer = setTimeout(() => {
|
||||
if (currentPage() === "ai" && isSupervisorMode()) connectSupervisorStream();
|
||||
}, 8000);
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAiChatSession() {
|
||||
const r = await apiFetch("/api/ai/chat/session");
|
||||
const j = await r.json();
|
||||
@@ -4313,8 +4474,15 @@
|
||||
|
||||
async function loadAiPage() {
|
||||
applyAiMobileTab();
|
||||
await loadAiChatSession();
|
||||
await consumeArchiveQuoteAiPending();
|
||||
const params = new URLSearchParams(window.location.search || "");
|
||||
const modeParam = (params.get("mode") || "").trim().toLowerCase();
|
||||
if (modeParam === "supervisor") {
|
||||
await switchToSupervisorMode();
|
||||
} else {
|
||||
closeSupervisorStream();
|
||||
await loadAiChatSession();
|
||||
await consumeArchiveQuoteAiPending();
|
||||
}
|
||||
const mobTab = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading");
|
||||
if (isMobileAiLayout() && AI_MOBILE_CHAT_TABS.has(mobTab)) {
|
||||
const input = document.getElementById("ai-chat-input");
|
||||
@@ -4325,7 +4493,8 @@
|
||||
}
|
||||
|
||||
async function newAiChat(botMode) {
|
||||
const mode = botMode === "general" ? "general" : "trading";
|
||||
const mode = normalizeAiBotMode(botMode);
|
||||
if (mode !== "supervisor") closeSupervisorStream();
|
||||
try {
|
||||
const r = await apiFetch("/api/ai/chat/new", {
|
||||
method: "POST",
|
||||
@@ -4342,7 +4511,13 @@
|
||||
localStorage.setItem(AI_MOBILE_TAB_KEY, mode);
|
||||
applyAiMobileTab(mode);
|
||||
}
|
||||
showToast(mode === "general" ? "已开始普通聊天" : "已开始交易教练对话");
|
||||
showToast(
|
||||
mode === "general"
|
||||
? "已开始普通聊天"
|
||||
: mode === "supervisor"
|
||||
? "已打开今日监管"
|
||||
: "已开始交易教练对话"
|
||||
);
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
}
|
||||
@@ -4353,6 +4528,38 @@
|
||||
if (aiChatLoading) return;
|
||||
const input = document.getElementById("ai-chat-input");
|
||||
const text = (input && input.value || "").trim();
|
||||
if (isSupervisorMode()) {
|
||||
if (!text) return;
|
||||
const savedText = text;
|
||||
if (input) input.value = "";
|
||||
setAiChatBusy(true);
|
||||
renderAiChatMessages(aiSupervisorSessionCache, {
|
||||
pendingUser: text,
|
||||
thinking: true,
|
||||
});
|
||||
try {
|
||||
const r = await apiFetch("/api/ai/supervisor/chat/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok) throw new Error(j.detail || j.msg || "发送失败");
|
||||
aiSupervisorSessionCache = j.session || null;
|
||||
renderAiChatMessages(aiSupervisorSessionCache);
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
if (input && savedText) input.value = savedText;
|
||||
try {
|
||||
await loadAiSupervisorSession();
|
||||
} catch (_) {
|
||||
renderAiChatMessages(aiSupervisorSessionCache);
|
||||
}
|
||||
} finally {
|
||||
setAiChatBusy(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const files = aiChatPendingFiles.slice();
|
||||
if (!text && !files.length) return;
|
||||
const pendingAttachments = files.map((f) => ({
|
||||
@@ -4463,7 +4670,12 @@
|
||||
if (btn._aiBotBound) return;
|
||||
btn._aiBotBound = true;
|
||||
btn.addEventListener("click", () => {
|
||||
const mode = btn.getAttribute("data-bot") || "trading";
|
||||
const mode = normalizeAiBotMode(btn.getAttribute("data-bot") || "trading");
|
||||
if (mode === "supervisor") {
|
||||
void switchToSupervisorMode();
|
||||
return;
|
||||
}
|
||||
closeSupervisorStream();
|
||||
newAiChat(mode);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -648,11 +648,12 @@
|
||||
<div id="page-ai" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1><span class="head-tag">AI</span> 教练</h1>
|
||||
<p class="page-desc">交易教练 / 普通聊天 · 右侧可回看历史会话</p>
|
||||
<p class="page-desc">交易教练 / 普通聊天 / 交易监管 · 右侧可回看历史会话</p>
|
||||
</div>
|
||||
<div class="ai-mobile-tabs" role="tablist" aria-label="AI 教练视图">
|
||||
<button type="button" class="ai-mobile-tab is-active" data-ai-tab="trading" role="tab" aria-selected="true">交易教练</button>
|
||||
<button type="button" class="ai-mobile-tab" data-ai-tab="general" role="tab" aria-selected="false">普通聊天</button>
|
||||
<button type="button" class="ai-mobile-tab" data-ai-tab="supervisor" role="tab" aria-selected="false">交易监管</button>
|
||||
<button type="button" class="ai-mobile-tab" data-ai-tab="history" role="tab" aria-selected="false">历史</button>
|
||||
<button type="button" class="ai-mobile-tab ai-mobile-tab-action" data-ai-tab="new" role="tab" aria-selected="false" title="新开对话">新开</button>
|
||||
</div>
|
||||
@@ -662,6 +663,7 @@
|
||||
<div class="ai-bot-bar" role="tablist" aria-label="聊天机器人">
|
||||
<button type="button" class="ai-bot-tab is-active" data-bot="trading" role="tab" aria-selected="true">交易教练</button>
|
||||
<button type="button" class="ai-bot-tab" data-bot="general" role="tab" aria-selected="false">普通聊天</button>
|
||||
<button type="button" class="ai-bot-tab" data-bot="supervisor" role="tab" aria-selected="false">交易监管</button>
|
||||
</div>
|
||||
<button type="button" id="btn-ai-chat-new" class="primary ai-chat-new-btn">新开对话</button>
|
||||
</div>
|
||||
@@ -902,6 +904,50 @@
|
||||
</form>
|
||||
<div id="macro-event-list" class="macro-event-list"></div>
|
||||
</div>
|
||||
<div class="settings-supervisor-panel card">
|
||||
<h3 class="settings-display-title">交易监管 · 企业微信</h3>
|
||||
<p class="settings-display-hint">
|
||||
与四所实例策略通知独立;手动/中控开平仓与新开仓会推送至此 Webhook。链接可在下方单独修改。
|
||||
</p>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="supervisor-enabled" checked />
|
||||
启用交易监管推送
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="supervisor-wechat-program" checked />
|
||||
程序止盈/止损也发微信(鼓励向)
|
||||
</label>
|
||||
<div class="settings-grid supervisor-settings-grid">
|
||||
<div class="field field-wide">
|
||||
<label>企业微信 Webhook</label>
|
||||
<input id="supervisor-wechat-webhook" type="text" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." autocomplete="off" />
|
||||
</div>
|
||||
<div class="field field-wide">
|
||||
<label>微信消息跳转链接(可改)</label>
|
||||
<input id="supervisor-wechat-link" type="text" placeholder="https://你的域名/ai?mode=supervisor" autocomplete="off" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>消息前缀</label>
|
||||
<input id="supervisor-wechat-prefix" type="text" value="【交易监管】" autocomplete="off" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>日手动平警告阈值</label>
|
||||
<input id="supervisor-daily-warn" type="number" min="1" step="1" value="2" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>最短两笔间隔(分钟)</label>
|
||||
<input id="supervisor-interval-warn" type="number" min="1" step="1" value="15" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>30 分钟内笔数阈值</label>
|
||||
<input id="supervisor-freq-30m" type="number" min="1" step="1" value="2" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>平仓后再开仓(分钟)</label>
|
||||
<input id="supervisor-reopen-min" type="number" min="1" step="1" value="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-settings-save" class="primary">保存设置</button>
|
||||
<button type="button" id="btn-settings-add">添加交易所</button>
|
||||
|
||||
Reference in New Issue
Block a user