MoFin 初始提交
完整数据采集+分析管道: - market_watch.py:90行业板块采集(同花顺/东方财富) - 市场精选推荐 cron:全市场分析+候选池+星级推荐 - price_monitor.py:持仓/自选高频价格监控 - refresh_mtf_cache.py:多周期K线缓存 - 策略评估/知识萃取管道 文档:docs/ 含完整需求+架构设计 注意:尚未配置 git remote,笑笑接手后自行配置
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
"""Dashboard 提示词管理视图 —— 集成到 MoFin Web Dashboard
|
||||
|
||||
注册 API 路由和前端页面。
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from .registry import (
|
||||
list_prompts, get_prompt, add_prompt, update_prompt,
|
||||
add_version, set_active_version, get_version_history, get_categories,
|
||||
)
|
||||
from .tracking import (
|
||||
get_strategy_version_stats, get_associations_for_stock,
|
||||
)
|
||||
from .analytics import (
|
||||
get_prompt_version_comparison, generate_report,
|
||||
)
|
||||
from .models import PromptDef, PromptVersion
|
||||
|
||||
|
||||
def register_routes(server):
|
||||
"""在主 server.py 中调用,注册所有 /api/prompts/* 路由
|
||||
|
||||
用法:
|
||||
from prompt_manager.dashboard_views import register_routes
|
||||
register_routes(app) # app = Flask instance
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 提示词列表
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts")
|
||||
def api_prompts_list():
|
||||
from flask import request, jsonify
|
||||
category = request.args.get("category")
|
||||
prompts = list_prompts(category)
|
||||
return jsonify({
|
||||
"prompts": prompts,
|
||||
"categories": get_categories(),
|
||||
})
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 单个提示词详情
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/<prompt_id>")
|
||||
def api_prompt_detail(prompt_id):
|
||||
from flask import jsonify
|
||||
from prompt_manager.registry import get_version_content
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
if not prompt:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
|
||||
# 从文件中加载完整内容(registry.json 只存了摘要)
|
||||
prompt_dict = prompt.to_dict()
|
||||
for v in prompt_dict.get("versions", []):
|
||||
if v.get("content_path"):
|
||||
full = get_version_content(prompt_id, v["version"])
|
||||
if full:
|
||||
v["content"] = full
|
||||
else:
|
||||
# 回退:从文件直接读取
|
||||
cp = v["content_path"]
|
||||
try:
|
||||
with open(cp, encoding="utf-8") as f:
|
||||
v["content"] = f.read()
|
||||
except:
|
||||
pass # 保持摘要
|
||||
|
||||
history = get_version_history(prompt_id)
|
||||
return jsonify({
|
||||
"prompt": prompt_dict,
|
||||
"version_history": history,
|
||||
})
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 新增提示词
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts", methods=["POST"])
|
||||
def api_prompt_create():
|
||||
from flask import request, jsonify
|
||||
data = request.get_json()
|
||||
if not data or "id" not in data:
|
||||
return jsonify({"error": "缺少 id"}), 400
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
prompt = PromptDef(
|
||||
id=data["id"],
|
||||
name=data.get("name", data["id"]),
|
||||
description=data.get("description", ""),
|
||||
category=data.get("category", ""),
|
||||
locations=data.get("locations", []),
|
||||
versions=[],
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
current_version="",
|
||||
)
|
||||
add_prompt(prompt)
|
||||
return jsonify({"status": "ok", "prompt": prompt.to_dict()})
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 添加版本
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/<prompt_id>/versions", methods=["POST"])
|
||||
def api_prompt_add_version(prompt_id):
|
||||
from flask import request, jsonify
|
||||
data = request.get_json()
|
||||
if not data or "version" not in data:
|
||||
return jsonify({"error": "缺少 version"}), 400
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
version = PromptVersion(
|
||||
version=data["version"],
|
||||
label=data.get("label", data["version"]),
|
||||
created_at=now,
|
||||
changelog=data.get("changelog", ""),
|
||||
content=data.get("content", ""),
|
||||
content_path="",
|
||||
author=data.get("author", "知微"),
|
||||
status=data.get("status", "active"),
|
||||
tags=data.get("tags", []),
|
||||
)
|
||||
try:
|
||||
add_version(prompt_id, version)
|
||||
return jsonify({"status": "ok"})
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 切换活跃版本
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/<prompt_id>/activate", methods=["POST"])
|
||||
def api_prompt_activate(prompt_id):
|
||||
from flask import request, jsonify
|
||||
data = request.get_json()
|
||||
if not data or "version" not in data:
|
||||
return jsonify({"error": "缺少 version"}), 400
|
||||
try:
|
||||
set_active_version(prompt_id, data["version"])
|
||||
return jsonify({"status": "ok"})
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 版本统计
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/stats")
|
||||
def api_prompt_stats():
|
||||
from flask import jsonify
|
||||
version_stats = get_strategy_version_stats()
|
||||
return jsonify(version_stats)
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 版本有效性对比
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/effectiveness")
|
||||
def api_prompt_effectiveness():
|
||||
from flask import jsonify
|
||||
comparison = get_prompt_version_comparison()
|
||||
return jsonify(comparison)
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 生成分析报告(文本)
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/report")
|
||||
def api_prompt_report():
|
||||
from flask import jsonify
|
||||
report = generate_report()
|
||||
return jsonify({"report": report})
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# API: 获取某只股票的策略关联记录
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
@server.route("/api/prompts/associations/<code>")
|
||||
def api_prompt_associations(code):
|
||||
from flask import jsonify
|
||||
records = get_associations_for_stock(code)
|
||||
return jsonify({"code": code, "records": records})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 前端页面模板
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
def get_dashboard_tab_html():
|
||||
"""返回提示词管理 Tab 的 HTML,嵌入到 Dashboard 的 <div id="prompt-manager-content"> 中"""
|
||||
return """\
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">📝 提示词管理</h5>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="loadPromptReport()">📊 版本有效性报告</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" id="prompt-manager-body">
|
||||
<div class="text-center text-muted py-4">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版本详情 Modal -->
|
||||
<div class="modal fade" id="promptVersionModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="versionModalTitle">版本详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="versionModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报告 Modal -->
|
||||
<div class="modal fade" id="promptReportModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">提示词版本有效性报告</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="promptReportBody" style="white-space: pre-wrap; font-family: monospace;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function loadPrompts() {
|
||||
const body = document.getElementById('prompt-manager-body');
|
||||
body.innerHTML = '<div class="text-center text-muted py-4">加载中...</div>';
|
||||
|
||||
Promise.all([
|
||||
fetch('/api/prompts').then(r => r.json()),
|
||||
fetch('/api/prompts/effectiveness').then(r => r.json())
|
||||
]).then(([data, effectiveness]) => {
|
||||
const prompts = data.prompts || [];
|
||||
const categories = data.categories || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// 分类导航
|
||||
const catSet = {};
|
||||
prompts.forEach(p => { catSet[p.category] = true; });
|
||||
html += '<div class="mb-3">';
|
||||
html += '<span class="badge bg-secondary me-1" style="cursor:pointer" onclick="filterPrompts(\'\')">全部</span>';
|
||||
Object.keys(catSet).sort().forEach(cat => {
|
||||
html += `<span class="badge bg-info me-1" style="cursor:pointer" onclick="filterPrompts('${cat}')">${categories[cat] || cat}</span>`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// 提示词列表
|
||||
if (prompts.length === 0) {
|
||||
html += '<div class="alert alert-info">还没有提示词记录,请先初始化注册表。</div>';
|
||||
} else {
|
||||
html += '<div class="table-responsive"><table class="table table-sm table-hover">';
|
||||
html += '<thead><tr><th>提示词名称</th><th>分类</th><th>当前版本</th><th>版本数</th><th>策略数</th><th>成功率</th><th>操作</th></tr></thead><tbody>';
|
||||
|
||||
prompts.forEach(p => {
|
||||
const verCount = (p.versions || []).length;
|
||||
const currentVer = p.current_version || '-';
|
||||
const eff = effectiveness[`${p.id}@${currentVer}`] || {};
|
||||
const sr = eff.success_rate !== undefined ? eff.success_rate + '%' : '-';
|
||||
const total = eff.total || 0;
|
||||
|
||||
html += `<tr data-category="${p.category}">
|
||||
<td><strong>${p.name}</strong><br><small class="text-muted">${p.id}</small></td>
|
||||
<td><span class="badge bg-light text-dark">${categories[p.category] || p.category}</span></td>
|
||||
<td><span class="badge bg-primary">${currentVer}</span></td>
|
||||
<td>${verCount}</td>
|
||||
<td>${total}</td>
|
||||
<td>${sr}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-info" onclick="showPromptVersions('${p.id}')">版本</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="showPromptDetail('${p.id}')">详情</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
body.innerHTML = html;
|
||||
}).catch(err => {
|
||||
body.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function filterPrompts(category) {
|
||||
const rows = document.querySelectorAll('#prompt-manager-body table tbody tr');
|
||||
rows.forEach(row => {
|
||||
if (!category || row.dataset.category === category) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showPromptVersions(promptId) {
|
||||
fetch(`/api/prompts/${promptId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const prompt = data.prompt || {};
|
||||
const history = data.version_history || [];
|
||||
let html = `<h6>${prompt.name} — 版本历史</h6>`;
|
||||
html += '<table class="table table-sm"><thead><tr><th>版本</th><th>标签</th><th>状态</th><th>时间</th><th>变更说明</th></tr></thead><tbody>';
|
||||
|
||||
history.forEach(h => {
|
||||
const statusBadge = h.is_current ? 'bg-success' :
|
||||
h.status === 'deprecated' ? 'bg-warning text-dark' : 'bg-secondary';
|
||||
html += `<tr>
|
||||
<td><code>${h.version}</code></td>
|
||||
<td>${h.label}</td>
|
||||
<td><span class="badge ${statusBadge}">${h.is_current ? '当前' : h.status}</span></td>
|
||||
<td><small>${h.created_at}</small></td>
|
||||
<td><small>${h.changelog}</small></td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
|
||||
document.getElementById('versionModalTitle').textContent = prompt.name + ' - 版本历史';
|
||||
document.getElementById('versionModalBody').innerHTML = html;
|
||||
new bootstrap.Modal(document.getElementById('promptVersionModal')).show();
|
||||
});
|
||||
}
|
||||
|
||||
function showPromptDetail(promptId) {
|
||||
fetch(`/api/prompts/${promptId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const prompt = data.prompt || {};
|
||||
|
||||
// 获取当前版本内容
|
||||
const currentVer = prompt.current_version;
|
||||
const currentVerData = (prompt.versions || []).find(v => v.version === currentVer);
|
||||
|
||||
let html = `<h6>${prompt.name}</h6>`;
|
||||
html += `<p class="text-muted">${prompt.description || '无描述'}</p>`;
|
||||
html += `<p><strong>ID:</strong> ${prompt.id} | <strong>分类:</strong> ${prompt.category} | <strong>当前版本:</strong> ${currentVer}`;
|
||||
html += ` | <strong>位置:</strong> ${(prompt.locations || []).join(', ') || '-'}</p>`;
|
||||
|
||||
if (currentVerData) {
|
||||
html += '<hr><h6>当前版本内容摘要</h6>';
|
||||
html += `<pre style="max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px;">${currentVerData.content || '无内容'}</pre>`;
|
||||
}
|
||||
|
||||
document.getElementById('versionModalTitle').textContent = prompt.name + ' - 详情';
|
||||
document.getElementById('versionModalBody').innerHTML = html;
|
||||
new bootstrap.Modal(document.getElementById('promptVersionModal')).show();
|
||||
});
|
||||
}
|
||||
|
||||
function loadPromptReport() {
|
||||
fetch('/api/prompts/report')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('promptReportBody').textContent = data.report || '暂无数据';
|
||||
new bootstrap.Modal(document.getElementById('promptReportModal')).show();
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载后自动加载
|
||||
if (document.getElementById('prompt-manager-body')) {
|
||||
loadPrompts();
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
Reference in New Issue
Block a user