重评核心重构:ds-v4-pro + 原策略全文 + strategy_history + 前端三改造
后端(重评管线):
- 新增 llm_client.py 共享客户端: REASSESS_MODEL=deepseek-v4-pro 单点,
gateway预检(fail-fast), 150s超时+1次重试, 永不抛异常
- batch_reassess/per_stock_reassess: curl/urllib -> call_llm,
prompt传入原策略全文+当前参数+最近3条变更, 输出 维持/修改判断+
修改点理由+最终新策略, max_tokens 4096
- mofin_db: 新增 strategy_history 表 + snapshot_strategy_history(),
write_holding_strategy 覆写前自动快照(保留20条/code)
- mofin_db: holding_strategies 补 tag 列迁移 + 写入保留
(tag缺席=保留旧值, 显式传''=允许清除), 修复推荐标签被静默丢弃
- mo_data.read_decisions: SELECT 补 tag
- stale_detector/promote_candidates: 子进程超时 240/60 -> 480s
前端:
- 移除 报告Tab -> mofin_health 全部流程/Cron 表加 最后十次 列
(modal列表->详情), /api/reports 支持 cron+script 多路匹配
(jobs.json name->id 解析 + 文件名/标题子串兜底)
- 移除 决策库Tab
- 盯盘Tab 重构: 全部持仓+自选, sort_group 分组(推荐/持仓/自选),
推荐行琥珀高亮+🔥badge+行内策略, 新增 操作策略 列查看
最近3次完整策略(/api/strategy_history/<code>, 表缺失时降级当前行)
- 提示词Tab: registry.py 数据路径改回 /home/hmo/MoFin/data/prompts
(红线: 数据只在规范数据根), 空态提示初始化命令
This commit is contained in:
+241
-446
@@ -58,15 +58,13 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
<button class="tab-btn active px-4 py-2 text-sm rounded-lg" data-tab="overview">📈 概览</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="holdings">💼 持仓</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="watchlist">⭐ 自选</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="reports">📋 报告</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="decisions">📝 决策库</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="evaluation">📊 评估</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg blink-tab" data-tab="watch" style="color:#f97316;border-color:#f97316">👁️ 盯盘</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="prompts">📝 提示词</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="evaluation">📊 评估</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="health">🏥 健康</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="principles">📐 开发原则</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg blink-tab" data-tab="watch" style="color:#f97316;border-color:#f97316">👁️ 盯盘</button>
|
||||
<a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a>
|
||||
</div>
|
||||
|
||||
@@ -74,8 +72,6 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
<div id="tab-overview" class="tab-content"></div>
|
||||
<div id="tab-holdings" class="tab-content hidden"></div>
|
||||
<div id="tab-watchlist" class="tab-content hidden"></div>
|
||||
<div id="tab-reports" class="tab-content hidden"></div>
|
||||
<div id="tab-decisions" class="tab-content hidden"></div>
|
||||
<div id="tab-evaluation" class="tab-content hidden"></div>
|
||||
<div id="tab-prompts" class="tab-content hidden"></div>
|
||||
<div id="tab-market" class="tab-content hidden"></div>
|
||||
@@ -95,10 +91,24 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
</div>
|
||||
<div id="tab-watch" class="tab-content hidden">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-slate-200">👁️ 盯盘 · 有效操作建议</h2>
|
||||
<h2 class="text-lg font-semibold text-slate-200">👁️ 盯盘 · 全部策略</h2>
|
||||
<span class="text-xs text-slate-500"><span id="watchRefreshTime">—</span> <span id="watchCount" class="text-yellow-400 font-mono">0</span> 只</span>
|
||||
</div>
|
||||
<div id="watchContent" class="space-y-2"></div>
|
||||
<div id="watchContent" class="card overflow-hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy History Modal -->
|
||||
<div id="strategyModal" class="fixed inset-0 modal-overlay hidden items-center justify-center z-50" onclick="if(event.target===this)closeStrategyModal()">
|
||||
<div class="bg-[#1a1a24] border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[85vh] overflow-y-auto mx-4 p-6" onclick="event.stopPropagation()">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h2 id="strategyModalTitle" class="text-lg font-bold text-white">—</h2>
|
||||
<p id="strategyModalMeta" class="text-xs text-slate-500 mt-1">—</p>
|
||||
</div>
|
||||
<button onclick="closeStrategyModal()" class="text-slate-500 hover:text-white text-xl">×</button>
|
||||
</div>
|
||||
<div id="strategyModalBody" class="space-y-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -212,8 +222,6 @@ function renderTab(name) {
|
||||
if (name === 'overview') renderOverview();
|
||||
else if (name === 'holdings') renderHoldings();
|
||||
else if (name === 'watchlist') renderWatchlist();
|
||||
else if (name === 'reports') renderReports();
|
||||
else if (name === 'decisions') renderDecisions();
|
||||
else if (name === 'evaluation') renderEvaluation();
|
||||
else if (name === 'prompts') renderPrompts();
|
||||
else if (name === 'market') renderMarket();
|
||||
@@ -437,49 +445,6 @@ function renderWatchlist() {
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Reports Tab ──
|
||||
function renderReports() {
|
||||
const el = document.getElementById('tab-reports');
|
||||
if (!reportsData.length) {
|
||||
el.innerHTML = '<div class="card p-8 text-center text-slate-500">暂无报告</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-semibold">📋 报告列表</span>
|
||||
<span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('reports','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('reports','ai')">§</span></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
${reportsData.map(r => `
|
||||
<div class="card p-4 hover:border-slate-600 transition" onclick="viewReport('${r.id}')">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full ${r.type==='盘中'?'bg-blue-900/50 text-blue-300':r.type==='盘后'?'bg-purple-900/50 text-purple-300':'bg-slate-800 text-slate-400'}">${r.type||'其他'}</span>
|
||||
<span class="text-xs text-slate-500">${r.created_at?.slice(5,16)||''}</span>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-white">${r.title||r.id}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 line-clamp-2">${r.summary||''}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function viewReport(id) {
|
||||
const d = await fetchJSON(`/api/report/${id}`);
|
||||
if (d.error) return;
|
||||
const el = document.getElementById('tab-reports');
|
||||
el.innerHTML = `
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<button onclick="renderReports()" class="text-xs text-blue-400 hover:text-blue-300">← 返回报告列表</button>
|
||||
<span class="text-xs text-slate-500">${d.created_at||''}</span>
|
||||
</div>
|
||||
<div class="text-lg font-bold text-white mb-4">${d.title||'报告'}</div>
|
||||
<div class="text-sm text-slate-300 leading-relaxed whitespace-pre-wrap">${d.content||d.summary||'(无内容)'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Market Tab ──
|
||||
let marketRefreshInterval = null;
|
||||
|
||||
@@ -586,356 +551,6 @@ function renderMarket() {
|
||||
marketRefreshInterval = setInterval(refreshMarketData, 30000);
|
||||
}
|
||||
|
||||
// ── Decisions Tab ──
|
||||
let decisionsIsComposing = false;
|
||||
let decisionsSearchTerm = '';
|
||||
let decisionsRecommendOnly = false;
|
||||
let decisionsTagFilter = ''; // '' = all tags
|
||||
|
||||
function decisionsFilter() {
|
||||
if (decisionsIsComposing) return; // 跳过IME输入法组合中的中间值
|
||||
const el = document.getElementById('decisionsSearch');
|
||||
if (!el) return;
|
||||
decisionsSearchTerm = el.value.trim().toLowerCase();
|
||||
renderDecisionsFiltered();
|
||||
}
|
||||
|
||||
function toggleRecommendFilter() {
|
||||
decisionsRecommendOnly = !decisionsRecommendOnly;
|
||||
const btn = document.getElementById('recommendFilterBtn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('bg-yellow-900/30', decisionsRecommendOnly);
|
||||
btn.classList.toggle('border-yellow-400', decisionsRecommendOnly);
|
||||
btn.textContent = decisionsRecommendOnly ? '⭐ 推荐中' : '⭐ 当前推荐';
|
||||
}
|
||||
renderDecisionsFiltered();
|
||||
}
|
||||
|
||||
function decisionsTagFilterSet(tag) {
|
||||
decisionsTagFilter = (tag === decisionsTagFilter) ? '' : tag;
|
||||
renderDecisionsFiltered();
|
||||
// 重新聚焦搜索框
|
||||
document.getElementById('decisionsSearch')?.focus();
|
||||
}
|
||||
|
||||
/** 计算"距操作区距离"(越小越近),百分比 */
|
||||
function calcProximity(d) {
|
||||
const price = d.price || 0;
|
||||
const sl = d.stop_loss || 0;
|
||||
const tp = d.take_profit || 0;
|
||||
const el = d.entry_low || 0;
|
||||
const eh = d.entry_high || 0;
|
||||
if (!price) return 999;
|
||||
|
||||
let minDist = 999;
|
||||
|
||||
// 距止损的距离(百分比)
|
||||
if (sl > 0) {
|
||||
const dist = Math.abs((price - sl) / sl) * 100;
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
|
||||
// 距止盈的距离(百分比)
|
||||
if (tp > 0) {
|
||||
const dist = Math.abs((tp - price) / price) * 100;
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
|
||||
// 距买入区的距离
|
||||
if (el > 0 && eh > 0) {
|
||||
if (price >= el && price <= eh) {
|
||||
// 在买入区内,距边界距离
|
||||
const distToLow = (price - el) / (eh - el) * 100;
|
||||
const distToHigh = (eh - price) / (eh - el) * 100;
|
||||
minDist = Math.min(minDist, Math.min(distToLow, distToHigh));
|
||||
} else if (price < el) {
|
||||
const dist = (el - price) / el * 100;
|
||||
minDist = Math.min(minDist, dist);
|
||||
} else {
|
||||
const dist = (price - eh) / eh * 100;
|
||||
minDist = Math.min(minDist, dist);
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
function renderDecisionsFiltered() {
|
||||
const el = document.getElementById('tab-decisions');
|
||||
const items = decisionsCache;
|
||||
const term = decisionsSearchTerm;
|
||||
const recOnly = decisionsRecommendOnly;
|
||||
const tagFilter = decisionsTagFilter;
|
||||
|
||||
// 搜索过滤
|
||||
let filtered = term ? items.filter(x => {
|
||||
const name = (x.name || '').toLowerCase();
|
||||
const code = (x.code || '').toLowerCase();
|
||||
const action = (x._raw_action || x.action || '').toLowerCase();
|
||||
const note = (x.updated_reason || x.note || '').toLowerCase();
|
||||
return name.includes(term) || code.includes(term) || action.includes(term) || note.includes(term);
|
||||
}) : items;
|
||||
|
||||
// 当前推荐筛选(点击⭐按钮时启用)
|
||||
if (recOnly) {
|
||||
filtered = filtered.filter(x => x.tag === 'current_recommend' || x.tag === 'active_manual');
|
||||
}
|
||||
|
||||
// 标签过滤
|
||||
if (tagFilter) {
|
||||
filtered = filtered.filter(x => x.tag === tagFilter);
|
||||
}
|
||||
|
||||
// 只显示有策略的条目
|
||||
const withStrategy = filtered.filter(x => {
|
||||
const tg = x.trigger || {};
|
||||
return !!(x.stop_loss || tg.stop_loss || x.take_profit || tg.take_profit || x.entry_low || tg.entry_zone || x._raw_action);
|
||||
});
|
||||
|
||||
// 给每个条目算距离
|
||||
withStrategy.forEach(x => { x._proximity = calcProximity(x); });
|
||||
|
||||
// 分组(按优先级从高到低)
|
||||
// 1. 执行中(已建仓+部分退出,按距操作区距离排序)
|
||||
const executing = withStrategy.filter(x => x.execution?.status === 'executing' || x.execution?.status === 'partial_exit')
|
||||
.sort((a, b) => a._proximity - b._proximity);
|
||||
|
||||
// 2. 接近操作区(距操作区 < 5%,不论状态)
|
||||
const proxThreshold = 5;
|
||||
const proxKeys = new Set(executing.map(x => x.code));
|
||||
const nearZone = withStrategy.filter(x =>
|
||||
!proxKeys.has(x.code) && x._proximity < proxThreshold &&
|
||||
x.execution?.status !== 'observing'
|
||||
).sort((a, b) => a._proximity - b._proximity);
|
||||
nearZone.forEach(x => proxKeys.add(x.code));
|
||||
|
||||
// 3. 观察中
|
||||
const observing = withStrategy.filter(x =>
|
||||
!proxKeys.has(x.code) && x.execution?.status === 'observing'
|
||||
).sort((a, b) => a._proximity - b._proximity);
|
||||
observing.forEach(x => proxKeys.add(x.code));
|
||||
|
||||
// 4. 其余
|
||||
const others = withStrategy.filter(x => !proxKeys.has(x.code))
|
||||
.sort((a, b) => a._proximity - b._proximity);
|
||||
|
||||
// 统计标签
|
||||
const tagCounts = {};
|
||||
withStrategy.forEach(x => {
|
||||
const t = x.tag || '';
|
||||
if (t) tagCounts[t] = (tagCounts[t] || 0) + 1;
|
||||
});
|
||||
const hasTags = Object.keys(tagCounts).length > 0;
|
||||
|
||||
const matched = term ? `(搜索"${term}" 匹配 ${filtered.length} 只)` : '';
|
||||
const tagPills = hasTags ? `
|
||||
<div class="flex gap-1 flex-wrap mb-3">
|
||||
<button onclick="decisionsTagFilterSet('')"
|
||||
class="text-xs px-2 py-1 rounded-full transition ${!tagFilter ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-400 hover:bg-slate-700'}">
|
||||
全部
|
||||
</button>
|
||||
${Object.entries(tagCounts).map(([tag, count]) => {
|
||||
const label = tag === 'active_manual' ? '当前推荐操作' :
|
||||
tag === 'current_recommend' ? '知微推荐' : tag;
|
||||
const active = tagFilter === tag;
|
||||
const colors = (tag === 'active_manual' || tag === 'current_recommend')
|
||||
? (active ? 'bg-amber-500 text-white' : 'bg-amber-900/40 text-amber-300 hover:bg-amber-800/50')
|
||||
: (active ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-400 hover:bg-slate-700');
|
||||
return `<button onclick="decisionsTagFilterSet('${tag}')"
|
||||
class="text-xs px-2 py-1 rounded-full transition ${colors}">
|
||||
${label} (${count})
|
||||
</button>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
el.innerHTML = `
|
||||
${tagPills}
|
||||
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<span class="text-sm font-semibold">策略决策库 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('decisions','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('decisions','ai')">§</span></span></span>
|
||||
<span class="text-xs text-slate-500">· ${withStrategy.length} 只 ${matched}</span>
|
||||
<button id="recommendFilterBtn" onclick="toggleRecommendFilter()"
|
||||
class="text-xs px-2 py-1 rounded-full border transition ${decisionsRecommendOnly ? 'bg-amber-900/30 border-amber-400 text-amber-300' : 'bg-slate-800 border-slate-700 text-slate-400 hover:bg-slate-700'}">
|
||||
${decisionsRecommendOnly ? '⭐ 推荐中' : '⭐ 当前推荐'}
|
||||
</button>
|
||||
<span class="text-xs text-green-400">${executing.length} 执行中</span>
|
||||
<span class="text-xs text-orange-400">${nearZone.length} 接近操作</span>
|
||||
<span class="text-xs text-yellow-400">${observing.length} 观察</span>
|
||||
<span class="text-xs text-slate-500">${others.length} 其他</span>
|
||||
</div>
|
||||
<div class="relative mb-4">
|
||||
<input id="decisionsSearch" type="text" placeholder="搜索股票名或代码…" value="${term}"
|
||||
class="w-full px-3 py-2 pl-8 text-xs bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
||||
oninput="if(!this._composing)decisionsFilter()"
|
||||
oncompositionstart="this._composing=true"
|
||||
oncompositionend="this._composing=false;decisionsFilter()" />
|
||||
<span class="absolute left-2.5 top-2 text-slate-500 text-xs">🔍</span>
|
||||
${term ? `<button onclick="document.getElementById('decisionsSearch').value='';decisionsSearchTerm='';renderDecisionsFiltered()" class="absolute right-2 top-2 text-slate-500 hover:text-white text-xs">×</button>` : ''}
|
||||
</div>
|
||||
|
||||
${executing.length > 0 ? `
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-green-400 bg-green-900/30 px-2 py-0.5 rounded-full">▶ 正在执行</span>
|
||||
<span class="text-xs text-slate-500">已建仓 · 距操作区最近优先</span>
|
||||
</div>
|
||||
${renderDecCards(executing, true)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${nearZone.length > 0 ? `
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-orange-400 bg-orange-900/30 px-2 py-0.5 rounded-full">⚡ 接近操作区</span>
|
||||
<span class="text-xs text-slate-500">距关键位<5%,重点关注</span>
|
||||
</div>
|
||||
${renderDecCards(nearZone, true)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${observing.length > 0 ? `
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-yellow-400 bg-yellow-900/30 px-2 py-0.5 rounded-full">◐ 观察中</span>
|
||||
<span class="text-xs text-slate-500">自选关注,等待入场时机</span>
|
||||
</div>
|
||||
${renderDecCards(observing, false)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${others.length > 0 ? `
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-slate-400 bg-slate-800/50 px-2 py-0.5 rounded-full">○ 其他</span>
|
||||
<span class="text-xs text-slate-500">远离操作区</span>
|
||||
</div>
|
||||
${renderDecCards(others, false)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${withStrategy.length === 0 ? `<div class="card p-8 text-center text-slate-500">${term ? '未匹配到结果' : '暂无带策略的条目'}</div>` : ''}
|
||||
`;
|
||||
|
||||
if (term) document.getElementById('decisionsSearch')?.focus();
|
||||
}
|
||||
|
||||
async function renderDecisions() {
|
||||
const d = await fetchJSON('/api/decisions');
|
||||
decisionsCache = d.decisions || [];
|
||||
renderDecisionsFiltered();
|
||||
}
|
||||
|
||||
function renderDecCards(items, showExecution) {
|
||||
return `<div class="space-y-2">${items.map(d => {
|
||||
const exec = d.execution || {};
|
||||
const analysis = d.analysis || {};
|
||||
const timeline = d.advice_timeline || d.changelog || [];
|
||||
|
||||
// 策略区(兼容新旧字段名)
|
||||
const tg = d.trigger || {};
|
||||
const stopLoss = d.stop_loss || tg.stop_loss || '';
|
||||
const takeProfit = d.take_profit || tg.take_profit || '';
|
||||
const entryLow = d.entry_low || 0;
|
||||
const entryHigh = d.entry_high || 0;
|
||||
const techSnap = d.tech_snapshot || analysis.tech_basis || '';
|
||||
const timingSignal = d.timing_signal || analysis.timing_signal || '';
|
||||
const action = d.action || '';
|
||||
const hasTrigger = !!(stopLoss || (entryLow && entryHigh) || takeProfit);
|
||||
|
||||
// 执行区(实际)
|
||||
const execStatus = exec.status || 'none';
|
||||
const execPnl = exec.pnl_pct || '';
|
||||
const execPrice = exec.entry_price || '';
|
||||
const execShares = exec.shares || '';
|
||||
const execNotes = exec.notes || '';
|
||||
|
||||
return `<div class="card p-4">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<span class="font-medium text-white">${d.name}</span>
|
||||
<span class="text-xs text-slate-500 ml-2">${d.code}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded ml-2 ${d.type?.includes('持仓')?'bg-blue-900/50 text-blue-300':'bg-purple-900/50 text-purple-300'}">${d.type||'—'}</span>
|
||||
${execStatus === 'executing' ? '<span class="text-xs px-1.5 py-0.5 rounded bg-green-900/50 text-green-300 ml-1">已执行</span>' : ''}
|
||||
${execStatus === 'partial_exit' ? '<span class="text-xs px-1.5 py-0.5 rounded bg-orange-900/50 text-orange-300 ml-1">部分退出</span>' : ''}
|
||||
${execStatus === 'observing' ? '<span class="text-xs px-1.5 py-0.5 rounded bg-yellow-900/50 text-yellow-300 ml-1">观察中</span>' : ''}
|
||||
${d.tag === 'active_manual' || d.tag === 'current_recommend' ? (() => {
|
||||
let label = '⭐ ';
|
||||
if (d.tag === 'active_manual') {
|
||||
if (execStatus === 'executing') label += '执行中 持有';
|
||||
else if (execStatus === 'partial_exit') label += '执行中 部分退出';
|
||||
else label += '待买入';
|
||||
} else {
|
||||
if (execStatus === 'partial_exit') label += '部分退出 剩余半仓';
|
||||
else if (execStatus === 'observing') label += '推荐买入';
|
||||
else label += '推荐操作';
|
||||
}
|
||||
return `<span class="text-xs px-1.5 py-0.5 rounded bg-amber-900/50 text-amber-300 ml-1 cursor-pointer" onclick="decisionsTagFilterSet('${d.tag}')">${label}</span>`;
|
||||
})() : ''}
|
||||
</div>
|
||||
<span class="text-xs text-slate-500">${d.timestamp?.slice(5,16)||''}</span>
|
||||
</div>
|
||||
|
||||
<!-- 双维度:策略 vs 执行 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-2">
|
||||
<!-- 策略理论 -->
|
||||
<div class="bg-blue-900/10 rounded-lg p-3 border border-blue-900/30">
|
||||
<div class="text-xs font-semibold text-blue-400 mb-1">📐 策略理论</div>
|
||||
${hasTrigger ? `
|
||||
<div class="grid grid-cols-3 gap-1 text-xs">
|
||||
${stopLoss ? `<div><span class="text-red-400">止损</span> ${stopLoss}</div>` : ''}
|
||||
${entryLow ? `<div><span class="text-yellow-400">买入区</span> ${entryLow}~${entryHigh}</div>` : tg.entry_zone ? `<div><span class="text-yellow-400">买入区</span> ${tg.entry_zone}</div>` : ''}
|
||||
${takeProfit ? `<div><span class="text-green-400">止盈</span> ${takeProfit}</div>` : ''}
|
||||
</div>
|
||||
${action ? `<div class="text-xs text-slate-300 mt-1 font-medium">${action}</div>` : ''}
|
||||
${analysis.reasoning ? `<div class="text-xs text-slate-400 mt-1">${analysis.reasoning.slice(0,100)}</div>` : ''}
|
||||
${timingSignal && !timingSignal.includes('neutral') ? `<div class="text-xs text-yellow-400 mt-1">⏱ ${timingSignal}</div>` : ''}
|
||||
` : `<div class="text-xs text-slate-500">无策略</div>`}
|
||||
</div>
|
||||
|
||||
<!-- 执行实际 -->
|
||||
<div class="bg-green-900/10 rounded-lg p-3 border border-green-900/30">
|
||||
<div class="text-xs font-semibold text-green-400 mb-1">⚡ 实际执行</div>
|
||||
${execStatus === 'executing' || execStatus === 'partial_exit' ? `
|
||||
<div class="text-xs">
|
||||
<div class="flex justify-between"><span class="text-slate-400">建仓价</span><span class="font-mono text-white">¥${execPrice||'?'}</span></div>
|
||||
<div class="flex justify-between"><span class="text-slate-400">持仓</span><span class="font-mono text-white">${execShares||'?'}股</span></div>
|
||||
<div class="flex justify-between"><span class="text-slate-400">盈亏</span><span class="font-mono ${(execPnl||'').includes('-')?'text-red-400':'text-green-400'}">${execPnl||'?'}</span></div>
|
||||
${exec.remaining_shares ? `<div class="flex justify-between"><span class="text-slate-400">剩余</span><span class="font-mono text-white">${exec.remaining_shares}股</span></div>` : ''}
|
||||
${exec.realized_gain ? `<div class="flex justify-between"><span class="text-slate-400">已实现</span><span class="font-mono text-green-400">+${exec.realized_gain}</span></div>` : ''}
|
||||
${exec.notes ? `<div class="text-slate-400 mt-1">${exec.notes}</div>` : ''}
|
||||
</div>
|
||||
` : execStatus === 'observing' ? `
|
||||
<div class="text-xs text-slate-400">观察中,等待入场时机</div>
|
||||
<div class="text-xs text-slate-500 mt-1">${execNotes||'关注回调至买入区'}</div>
|
||||
` : `
|
||||
<div class="text-xs text-slate-500">尚未执行</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 建议记录 -->
|
||||
${timeline.length ? `
|
||||
<div class="border-t border-slate-800/50 pt-2 mt-1">
|
||||
<div class="text-xs text-slate-500 mb-1">📜 建议记录 (${timeline.length})</div>
|
||||
<div class="space-y-1">
|
||||
${timeline.slice(-3).map(a =>
|
||||
`<div class="text-xs text-slate-400 pl-2 border-l-2 border-blue-500/50 flex items-center gap-1">
|
||||
<span class="text-slate-500">${a.date?.slice(5,16)||''}</span>
|
||||
<span class="${a.direction==='买入'?'text-green-400':a.direction==='卖出'?'text-red-400':'text-slate-300'}">${a.direction||''}</span>
|
||||
<span>${a.summary?.slice(0,40)||''}</span>
|
||||
${a.status === 'pending' ? '<span class="text-yellow-500">⏳</span>' : a.status === 'confirmed' ? '<span class="text-green-500">✓</span>' : a.status === 'ignored' ? '<span class="text-slate-600">✗</span>' : ''}
|
||||
</div>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${d.updated_reason ? `<div class="text-xs text-slate-500 mt-1">📝 ${d.updated_reason}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
|
||||
// ── Evaluation Tab ──
|
||||
function renderEvaluation() {
|
||||
const el = document.getElementById('tab-evaluation');
|
||||
@@ -1127,7 +742,11 @@ function renderPrompts() {
|
||||
|
||||
// 提示词列表
|
||||
if (prompts.length === 0) {
|
||||
html += '<div class="card p-6 text-center text-slate-500">暂无提示词记录,请先初始化注册表。</div>';
|
||||
html += '<div class="card p-6 text-center">' +
|
||||
'<div class="text-slate-400 mb-2">⚠️ 提示词注册表未初始化</div>' +
|
||||
'<div class="text-xs text-slate-500">请在服务器运行 prompt_manager/init_registry.py 初始化注册表。</div>' +
|
||||
'<div class="text-xs text-slate-600 mt-2">路径: /home/hmo/MoFin/prompt_manager/init_registry.py</div>' +
|
||||
'</div>';
|
||||
} else {
|
||||
html += '<div class="card overflow-hidden"><table class="w-full text-sm">';
|
||||
html += '<thead><tr class="text-left text-slate-400 border-b border-slate-700/50"><th class="p-3 font-medium">提示词</th><th class="p-3 font-medium">分类</th><th class="p-3 font-medium">当前版本</th><th class="p-3 font-medium">版本数</th><th class="p-3 font-medium">关联策略</th><th class="p-3 font-medium">成功率</th><th class="p-3 font-medium">操作</th></tr></thead><tbody>';
|
||||
@@ -1161,7 +780,11 @@ function renderPrompts() {
|
||||
// 保存数据供版本弹窗使用
|
||||
window._promptData = data;
|
||||
}).catch(err => {
|
||||
el.innerHTML = `<div class="card p-6 text-center text-red-400">加载失败: ${err.message}</div>`;
|
||||
el.innerHTML = '<div class="card p-6 text-center">' +
|
||||
'<div class="text-amber-400 mb-2">⚠️ 提示词注册表加载失败</div>' +
|
||||
'<div class="text-xs text-slate-400">错误: ' + (err.message || 'unknown') + '</div>' +
|
||||
'<div class="text-xs text-slate-500 mt-2">请确认服务器已运行 prompt_manager/init_registry.py 初始化注册表。</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1710,68 +1333,240 @@ async function refreshHealth() {
|
||||
|
||||
// ── 盯盘 ──
|
||||
let watchTimer = null;
|
||||
|
||||
// 信号徽标颜色
|
||||
function sigBadgeClass(sig) {
|
||||
if (!sig) return 'bg-slate-800 text-slate-400';
|
||||
const buy = ['买入','可买入','可加仓'];
|
||||
const sell = ['卖出','止盈'];
|
||||
if (buy.some(k => sig.includes(k))) return 'bg-green-900/50 text-green-300';
|
||||
if (sell.some(k => sig.includes(k))) return 'bg-red-900/50 text-red-300';
|
||||
return 'bg-yellow-900/50 text-yellow-300';
|
||||
}
|
||||
|
||||
// 格式化策略摘要(2行截断)
|
||||
function fmtStrategy(s) {
|
||||
const action = s.action || '';
|
||||
const advice = s.position_advice || '';
|
||||
const parts = [];
|
||||
if (action) parts.push(action);
|
||||
if (advice) parts.push(advice);
|
||||
if (parts.length === 0) return '—';
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
// 格式化完整策略(modal 内用)
|
||||
function fmtFullStrategy(s) {
|
||||
const lines = [];
|
||||
if (s.action) lines.push('操作: ' + s.action);
|
||||
if (s.position_advice) lines.push('仓位建议: ' + s.position_advice);
|
||||
if (s.entry_low || s.entry_high) lines.push('买入区间: ' + (s.entry_low||'—') + '~' + (s.entry_high||'—'));
|
||||
if (s.stop_loss || s.take_profit) lines.push('止损: ' + (s.stop_loss||'—') + ' / 止盈: ' + (s.take_profit||'—'));
|
||||
if (s.rr_ratio) lines.push('RR: ' + s.rr_ratio.toFixed(2));
|
||||
if (s.timing_signal) lines.push('信号: ' + s.timing_signal);
|
||||
if (s.full_analysis) lines.push('\n分析: ' + s.full_analysis);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function renderWatch() {
|
||||
const el = document.getElementById('watchContent');
|
||||
if (!el) return;
|
||||
try {
|
||||
// 清除旧定时器,切走时停
|
||||
if (watchTimer) clearInterval(watchTimer);
|
||||
const data = await fetchJSON('/api/watch');
|
||||
const stocks = data.stocks || [];
|
||||
document.getElementById('watchCount').textContent = stocks.length;
|
||||
document.getElementById('watchRefreshTime').textContent = new Date().toLocaleTimeString();
|
||||
|
||||
if (stocks.length === 0) {
|
||||
el.innerHTML = '<div class="card p-6 text-center text-slate-500 text-sm">暂无有效操作建议</div>';
|
||||
el.innerHTML = '<div class="p-6 text-center text-slate-500 text-sm">暂无策略数据</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (const s of stocks) {
|
||||
const p = s.price || 0;
|
||||
const cp = s.change_pct || 0;
|
||||
const sig = s.timing_signal || '';
|
||||
const isBuy = sig.includes('买入') || sig.includes('加仓');
|
||||
const isSell = sig.includes('卖出') || sig.includes('止盈');
|
||||
const sigColor = isBuy ? 'bg-green-900/50 text-green-300' : isSell ? 'bg-red-900/50 text-red-300' : 'bg-yellow-900/50 text-yellow-300';
|
||||
const chgColor = cp >= 0 ? 'text-red-400' : 'text-green-400';
|
||||
const el_ = s.entry_low || 0;
|
||||
const eh_ = s.entry_high || 0;
|
||||
const sl_ = s.stop_loss || 0;
|
||||
const tp_ = s.take_profit || 0;
|
||||
const rr = s.rr_ratio || 0;
|
||||
const inZone = (p && el_ && eh_ && p >= el_ && p <= eh_) ? '✅区内' : (p && eh_ && p > eh_) ? '⬆️超出' : '⬇️低于';
|
||||
html += `
|
||||
<div class="card p-4 flex items-center gap-3" onclick="openStock('${s.code}')" style="cursor:pointer">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-sm text-slate-200">${s.code}</span>
|
||||
<span class="text-slate-400 text-xs">${s.name || ''}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded ${sigColor}">${sig}</span>
|
||||
<span class="text-xs text-slate-500">${s.decision_type==='持仓策略'?'💼':'⭐'}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-1 text-xs text-slate-400">
|
||||
<span class="font-mono">${p.toFixed(2)}</span>
|
||||
<span class="font-mono ${chgColor}">${cp >= 0 ? '+' : ''}${cp.toFixed(2)}%</span>
|
||||
<span>区${el_.toFixed(1)}~${eh_.toFixed(1)}</span>
|
||||
<span>${inZone}</span>
|
||||
<span>RR ${rr.toFixed(2)}</span>
|
||||
<span>损${sl_.toFixed(1)} 盈${tp_.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<div class="text-slate-400">仓位</div>
|
||||
<div class="text-yellow-400 font-mono">${s.position_advice || '—'}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// 分组渲染
|
||||
const groups = [
|
||||
{ label: '🔥 重点推荐', g: 0 },
|
||||
{ label: '💼 持仓策略', g: 1 },
|
||||
{ label: '⭐ 自选策略', g: 2 },
|
||||
];
|
||||
|
||||
let html = '<div class="overflow-x-auto"><table class="w-full text-xs">';
|
||||
html += '<thead><tr class="text-slate-500 border-b border-slate-800/50">' +
|
||||
'<th class="text-left p-2">股票</th>' +
|
||||
'<th class="text-right p-2">现价</th>' +
|
||||
'<th class="text-right p-2">涨跌%</th>' +
|
||||
'<th class="text-left p-2">信号</th>' +
|
||||
'<th class="text-right p-2">买入区间</th>' +
|
||||
'<th class="text-right p-2">止损/止盈</th>' +
|
||||
'<th class="text-right p-2">RR</th>' +
|
||||
'<th class="text-right p-2">仓位</th>' +
|
||||
'<th class="text-left p-2">当前操作策略</th>' +
|
||||
'<th class="text-center p-2">操作策略</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
for (const grp of groups) {
|
||||
const items = stocks.filter(s => s.sort_group === grp.g);
|
||||
if (items.length === 0) continue;
|
||||
|
||||
// 分组标题行
|
||||
html += '<tr class="border-b border-slate-700">' +
|
||||
'<td colspan="10" class="p-2 font-semibold text-slate-300">' + grp.label + ' (' + items.length + ')</td>' +
|
||||
'</tr>';
|
||||
|
||||
for (const s of items) {
|
||||
const p = s.price || 0;
|
||||
const cp = s.change_pct || 0;
|
||||
const sig = s.timing_signal || '';
|
||||
const isRec = s.sort_group === 0; // 推荐行
|
||||
|
||||
const sigCls = sigBadgeClass(sig);
|
||||
const chgColor = cp >= 0 ? 'text-red-400' : 'text-green-400';
|
||||
const el_ = s.entry_low || 0;
|
||||
const eh_ = s.entry_high || 0;
|
||||
const sl_ = s.stop_loss || 0;
|
||||
const tp_ = s.take_profit || 0;
|
||||
const rr = s.rr_ratio || 0;
|
||||
const shares = s.shares || 0;
|
||||
const posPct = s.position_pct || 0;
|
||||
|
||||
const buyZone = (el_ && eh_) ? el_.toFixed(1) + '~' + eh_.toFixed(1) : '—';
|
||||
const sltp = (sl_ || tp_) ? '损' + (sl_||'—') + '/盈' + (tp_||'—') : '—';
|
||||
const posDisplay = posPct ? posPct.toFixed(1) + '%' : '—';
|
||||
|
||||
// 当前操作策略摘要
|
||||
const strat = fmtStrategy(s);
|
||||
|
||||
// 推荐行高亮
|
||||
const rowCls = isRec ? 'bg-amber-900/15 border-l-2 border-l-amber-400' : 'border-b border-slate-800/30 hover:bg-slate-800/20';
|
||||
const recBadge = isRec ? '<span class="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400 ml-1">🔥推荐</span>' : '';
|
||||
|
||||
// 推荐行策略文本加粗醒目
|
||||
const stratCls = isRec ? 'font-semibold text-amber-200' : 'text-slate-300';
|
||||
|
||||
html += '<tr class="' + rowCls + '" onclick="openStock(\'' + s.code + '\')" style="cursor:pointer">' +
|
||||
'<td class="p-2"><span class="font-medium text-white">' + s.name + '</span><br><span class="text-slate-500">' + s.code + '</span>' + recBadge + '</td>' +
|
||||
'<td class="p-2 text-right font-mono">' + (p ? p.toFixed(2) : '—') + '</td>' +
|
||||
'<td class="p-2 text-right font-mono ' + chgColor + '">' + (cp >= 0 ? '+' : '') + (cp ? cp.toFixed(2) : '—') + '%</td>' +
|
||||
'<td class="p-2 text-left"><span class="text-xs px-2 py-1 rounded ' + sigCls + '">' + (sig || '—') + '</span></td>' +
|
||||
'<td class="p-2 text-right font-mono text-slate-300">' + buyZone + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-slate-300">' + sltp + '</td>' +
|
||||
'<td class="p-2 text-right font-mono ' + (rr >= 1.5 ? 'text-green-400' : rr > 0 ? 'text-yellow-400' : 'text-slate-500') + '">' + (rr ? rr.toFixed(2) : '—') + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-slate-300">' + posDisplay + '</td>' +
|
||||
'<td class="p-2 ' + stratCls + ' max-w-[200px] break-all line-clamp-2">' + strat + '</td>' +
|
||||
'<td class="p-2 text-center"><button class="text-xs px-2 py-1 rounded bg-blue-900/50 text-blue-400 hover:bg-blue-800/50 transition" onclick="event.stopPropagation();openStrategyHistory(\'' + s.code + '\')">📋 历史</button></td>' +
|
||||
'</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
// 更新时间
|
||||
html += '<div class="text-xs text-slate-600 mt-1">' + (data.updated_at ? '数据: ' + data.updated_at : '') + '</div>';
|
||||
|
||||
el.innerHTML = html;
|
||||
// 自动刷新:每30秒
|
||||
watchTimer = setInterval(renderWatch, 30000);
|
||||
} catch(e) {
|
||||
el.innerHTML = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
|
||||
el.innerHTML = '<div class="p-6 text-red-400 text-sm">加载失败: ' + e.message + '</div>';
|
||||
watchTimer = setInterval(renderWatch, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 操作策略历史 Modal ──
|
||||
async function openStrategyHistory(code) {
|
||||
const modal = document.getElementById('strategyModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
document.getElementById('strategyModalTitle').textContent = '加载中...';
|
||||
document.getElementById('strategyModalMeta').textContent = '';
|
||||
document.getElementById('strategyModalBody').innerHTML = '<div class="text-slate-500">正在获取策略历史...</div>';
|
||||
|
||||
try {
|
||||
const d = await fetchJSON('/api/strategy_history/' + code + '?limit=3');
|
||||
const history = d.history || [];
|
||||
if (history.length === 0) {
|
||||
document.getElementById('strategyModalTitle').textContent = '操作策略历史';
|
||||
document.getElementById('strategyModalMeta').textContent = code + ' · 无历史记录';
|
||||
document.getElementById('strategyModalBody').innerHTML = '<div class="text-slate-500 text-center py-8">暂无策略历史记录</div>';
|
||||
return;
|
||||
}
|
||||
// 找出股票名称
|
||||
const name = history[0].name || code;
|
||||
document.getElementById('strategyModalTitle').textContent = name + ' · ' + code;
|
||||
document.getElementById('strategyModalMeta').textContent = '最近 ' + history.length + ' 条策略记录';
|
||||
|
||||
let body = '';
|
||||
// 最新一条默认展开,其余可折叠
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const h = history[i];
|
||||
const isLatest = i === 0;
|
||||
const collapsed = !isLatest ? ' hidden' : '';
|
||||
const toggleId = 'strat-' + i;
|
||||
|
||||
// 策略头信息
|
||||
const headInfo = [];
|
||||
if (h.snapshotted_at) headInfo.push('快照: ' + h.snapshotted_at.slice(0, 16));
|
||||
if (h.reassessed_at) headInfo.push('重评: ' + h.reassessed_at.slice(0, 16));
|
||||
if (h.version) headInfo.push('版本: ' + h.version);
|
||||
if (h.is_current) headInfo.push('⚡ 当前策略');
|
||||
|
||||
const action = h.action || '';
|
||||
const timing = h.timing_signal || '';
|
||||
const sigCls = sigBadgeClass(timing);
|
||||
|
||||
body += '<div class="card p-4">' +
|
||||
'<div class="flex justify-between items-start mb-2">' +
|
||||
'<div>' +
|
||||
'<span class="font-semibold text-white">' + (action || '—') + '</span>' +
|
||||
'<span class="text-xs px-2 py-0.5 rounded ml-2 ' + sigCls + '">' + (timing || '—') + '</span>' +
|
||||
'<div class="text-xs text-slate-500 mt-1">' + headInfo.join(' · ') + '</div>' +
|
||||
'</div>' +
|
||||
(isLatest ? '' : '<button class="text-xs text-blue-400 hover:text-blue-300" onclick="toggleStrat(\'' + toggleId + '\')">展开</button>') +
|
||||
'</div>';
|
||||
|
||||
// 策略参数
|
||||
const params = [];
|
||||
if (h.entry_low || h.entry_high) params.push('买入区间: ' + (h.entry_low||'—') + '~' + (h.entry_high||'—'));
|
||||
if (h.stop_loss) params.push('止损: ¥' + h.stop_loss);
|
||||
if (h.take_profit) params.push('止盈: ¥' + h.take_profit);
|
||||
if (h.rr_ratio) params.push('RR: ' + h.rr_ratio.toFixed(2));
|
||||
if (h.position_advice) params.push('仓位: ' + h.position_advice);
|
||||
|
||||
if (params.length > 0) {
|
||||
body += '<div class="text-xs text-slate-300 mb-2">' + params.join(' · ') + '</div>';
|
||||
}
|
||||
|
||||
// 完整分析(折叠)
|
||||
const fa = h.full_analysis || '';
|
||||
if (fa) {
|
||||
body += '<details class="text-xs">' +
|
||||
'<summary class="text-slate-400 cursor-pointer hover:text-blue-400">📋 完整分析</summary>' +
|
||||
'<pre class="mt-1 text-slate-300 whitespace-pre-wrap bg-slate-900/50 rounded p-2 max-h-48 overflow-y-auto font-sans leading-relaxed">' + fa.replace(/</g, '<') + '</pre>' +
|
||||
'</details>';
|
||||
}
|
||||
|
||||
body += '</div>';
|
||||
}
|
||||
|
||||
document.getElementById('strategyModalBody').innerHTML = body;
|
||||
} catch(e) {
|
||||
document.getElementById('strategyModalTitle').textContent = '操作策略历史';
|
||||
document.getElementById('strategyModalMeta').textContent = code;
|
||||
document.getElementById('strategyModalBody').innerHTML = '<div class="text-red-400">加载失败: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function closeStrategyModal() {
|
||||
document.getElementById('strategyModal').classList.add('hidden');
|
||||
document.getElementById('strategyModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
function toggleStrat(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 开发原则 Tab(仿 AgentsMeeting: G规范/K测试/F健康/H需求)──
|
||||
let _princLoaded = {};
|
||||
function renderPrinciples() {
|
||||
|
||||
Reference in New Issue
Block a user