Markdown渲染 + PRD需求文档(H Tab)
- mdRender(): 支持h1-h6/table/list/code/bold/link/blockquote/hr - G 规范: dev-spec.md 以渲染后HTML展示 - H 需求: PRD.md 以渲染后HTML展示 - /api/prd: 新增路由读取PRD.md
This commit is contained in:
@@ -941,6 +941,29 @@ def api_spec():
|
||||
return jsonify({"ok": False, "error": str(e), "content": ""})
|
||||
|
||||
|
||||
@app.route("/api/prd")
|
||||
def api_prd():
|
||||
"""返回产品需求文档 (PRD) 内容"""
|
||||
candidates = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "docs", "PRD.md"),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "docs", "PRD.md"),
|
||||
]
|
||||
prd_path = None
|
||||
for c in candidates:
|
||||
p = os.path.normpath(c)
|
||||
if os.path.exists(p):
|
||||
prd_path = p
|
||||
break
|
||||
if not prd_path:
|
||||
return jsonify({"ok": False, "error": "PRD.md not found", "content": ""})
|
||||
try:
|
||||
with open(prd_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return jsonify({"ok": True, "content": content, "path": prd_path})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e), "content": ""})
|
||||
|
||||
|
||||
@app.route("/api/platform")
|
||||
@app.route("/api/health")
|
||||
def api_health():
|
||||
|
||||
@@ -66,7 +66,7 @@ th{color:var(--dim);font-weight:600;font-size:11px}
|
||||
<h1>AgentsMeeting</h1><div class="subtitle" id="subtitle">Dashboard</div>
|
||||
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
|
||||
<script>
|
||||
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"},{"id":"spec","label":"G 规范"}]}];
|
||||
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"},{"id":"spec","label":"G 规范"},{"id":"prd","label":"H 需求"}]}];
|
||||
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'git';
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||
function qs(id){return document.getElementById(id)}
|
||||
@@ -104,12 +104,26 @@ async function fE(){try{var r=await fetch('/api/metagrowth'),d=await r.json();fi
|
||||
async function fF(){try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}fill('exp',ok+'/'+d.expected.length+' compliant')}catch(e){}}
|
||||
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json(),n=(d.tasks||[]).length;fill('kanban',n?'<span>'+n+' tasks</span>':'<span class="dim">No tasks</span>')}catch(e){}}
|
||||
async function fI(){try{ var r=await fetch('/api/platform'),d=await r.json(); var h='<div class="ps"><h2>Platform</h2><div class="psv">'; for(var i=0;i<d.length;i++){h+='<div class="i"><span class="d '+(d[i].status==='running'?'ok':'stopped')+'"></span>'+esc(d[i].name)+'</div>';} h+='</div></div>'; try{var r2=await fetch('/api/ejabberd'),e=await r2.json();var ea=e.alive||e.xmpp_bot_connected; h+='<div class="ps"><h2>Ejabberd</h2><div class="psv"><div class="i"><span class="d '+(ea?'ok':'stopped')+'"></span>'+(ea?'ALIVE':'DOWN')+'</div></div></div>';}catch(e2){} fill('infra',h); /* EasyTier section */ try{var r3=await fetch('/api/easytier'),e3=await r3.json(),s=e3.status||{}; var et=document.createElement('div');et.className='ps';et.style.marginTop='16px'; var av=(e3.virtual_ips||{});var wip=av.windows||'';var tip=av['246']||''; et.innerHTML='<h2>EasyTier VPN</h2><div class=psv><div class=i><span class=d id=et-w></span>Windows <span id=et-w-ip>'+wip+'</span></div><div class=i><span class=d id=et-246></span>246 <span id=et-246-ip>'+tip+'</span></div></div><div style=margin-top:8px;display:flex;gap:8px;align-items:center><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off>Turn Off</button><span id=et-status style=font-size:12px;color:var(--dim)>-</span></div>'; var ci=document.getElementById('ct-infra');if(ci)ci.appendChild(et); var e1=document.getElementById('et-w');if(e1)e1.className='d '+(s.windows==='running'?'ok':'stopped'); var e2=document.getElementById('et-246');if(e2)e2.className='d '+(s['246']==='running'?'ok':'stopped'); var allRun=Object.values(s).every(function(v){return v==='running'||v==='pending'}); var ob=document.getElementById('btn-et-on');var ofb=document.getElementById('btn-et-off');var stx=document.getElementById('et-status'); if(ob){ob.disabled=allRun;ob.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('ET Fail','err')})};} if(ofb){ofb.disabled=!allRun;ofb.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('ET Fail','err')})};} if(stx)stx.textContent=allRun?'Connected':'Off';}catch(e3){} /* RDP section */ try{var r4=await fetch('/api/rdp'),e4=await r4.json(); var rd=document.createElement('div');rd.className='ps';rd.style.marginTop='16px'; rd.innerHTML='<h2>RDP Remote Desktop</h2><div class=psv><div class=i><span class=d id=rdp-st></span>RDP <span id=rdp-info>'+esc(e4.public_endpoint||'')+'</span></div><div class=i><span class=d id=rdp-tun></span>Tunnel</div></div><div style=margin-top:8px;display:flex;gap:8px;align-items:center><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off>Disable</button><span id=rdp-status-txt style=font-size:12px;color:var(--dim)>-</span></div>'; var ci2=document.getElementById('ct-infra');if(ci2)ci2.appendChild(rd); var ee1=document.getElementById('rdp-st');if(ee1)ee1.className='d '+(e4.rdp_enabled?'ok':'stopped'); var ee2=document.getElementById('rdp-tun');if(ee2)ee2.className='d '+(e4.tunnel_running?'ok':'stopped'); var fOn=e4.rdp_enabled&&e4.tunnel_running; var ob2=document.getElementById('btn-rdp-on');var ofb2=document.getElementById('btn-rdp-off');var stx2=document.getElementById('rdp-status-txt'); if(ob2){ob2.disabled=fOn;ob2.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('RDP Fail','err')})};} if(ofb2){ofb2.disabled=!fOn;ofb2.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('RDP Fail','err')})};} if(stx2)stx2.textContent=fOn?'Connected':e4.rdp_enabled?'Tunnel pending':'Disconnected';}catch(e4){} }catch(e){}}
|
||||
var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,spec:fS2,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
|
||||
async function fH(){try{var r=await fetch('/api/prd'),d=await r.json();if(!d.ok||!d.content){fill('prd','Failed');return}fill('prd','<div style=max-height:600px;overflow-y:auto;background:var(--bg);padding:16px;border-radius:6px>'+mdRender(d.content)+'</div>')}catch(e){fill('prd','Error')}}var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,spec:fS2,prd:fH,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
|
||||
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();await Promise.all(Object.keys(FETCHES).map(function(k){return FETCHES[k]()}));}
|
||||
|
||||
async function fE2(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var e1=document.getElementById('et-w');if(e1){e1.className='d '+(s.windows==='running'?'ok':'stopped')}var e2=document.getElementById('et-246');if(e2){e2.className='d '+(s['246']==='running'?'ok':'stopped')}if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}}catch(e){}}
|
||||
async function fR(){try{var r=await fetch('/api/rdp'),d=await r.json();var e1=document.getElementById('rdp-st');if(e1){e1.className='d '+(d.rdp_enabled?'ok':'stopped')}var e2=document.getElementById('rdp-tun');if(e2){e2.className='d '+(d.tunnel_running?'ok':'stopped')}var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||''}catch(e){}}
|
||||
async function fS2(){try{var r=await fetch('/api/spec'),d=await r.json();if(!d.ok||!d.content){fill('spec','Failed');return}fill('spec','<div class=mono style=font-size:11px;color:var(--dim);white-space:pre-wrap;max-height:500px;overflow-y:auto;background:var(--bg);padding:12px;border-radius:6px>'+esc(d.content)+'</div>')}catch(e){fill('spec','Error')}}
|
||||
async function fS2(){try{var r=await fetch('/api/spec'),d=await r.json();if(!d.ok||!d.content){fill('spec','Failed');return}fill('spec','<div style=max-height:600px;overflow-y:auto;background:var(--bg);padding:16px;border-radius:6px>'+mdRender(d.content)+'</div>')}catch(e){fill('spec','Error')}}
|
||||
|
||||
function mdRender(md){if(!md)return'';var h='';var blocks=md.split('\n\n');for(var b=0;b<blocks.length;b++){var block=blocks[b].trim();if(!block)continue;
|
||||
if(block.startsWith('```')){var code=block.replace(/^```[\s\S]*?\n/,'').replace(/```$/,'').trim();h+='<pre class=mono style=background:var(--bg);padding:12px;border-radius:6px;overflow-x:auto>'+esc(code)+'</pre>';continue;}
|
||||
if(block.startsWith('### ')){h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:15px>'+esc(block.slice(4))+'</h3>';continue;}
|
||||
if(block.startsWith('## ')){h+='<h2 style=color:var(--accent);margin:20px 0 8px;font-size:17px;border-bottom:1px solid var(--border);padding-bottom:4px>'+esc(block.slice(3))+'</h2>';continue;}
|
||||
if(block.startsWith('# ')){h+='<h1 style=color:var(--accent);margin:24px 0 10px;font-size:20px>'+esc(block.slice(2))+'</h1>';continue;}
|
||||
if(block.startsWith('> ')){h+='<blockquote style="border-left:3px solid var(--accent);padding:8px 16px;margin:8px 0;color:var(--dim);background:var(--bg);border-radius:4px">'+esc(block.slice(2))+'</blockquote>';continue;}
|
||||
if(block.startsWith('---')||block.startsWith('***')){h+='<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';continue;}
|
||||
if(block.startsWith('- ')||block.startsWith('* ')){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^[-*]\s*/,'').trim();if(li){if(!inList){h+='<ul style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ul>';continue;}
|
||||
if(/^\d+\.\s/.test(block)){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^\d+\.\s*/,'').trim();if(li){if(!inList){h+='<ol style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ol>';continue;}
|
||||
if(block.indexOf('|')>=0&&block.indexOf('\n|')>=0){var rows=block.split('\n'),inTable=false;for(var i=0;i<rows.length;i++){if(rows[i].indexOf('---')>=0)continue;var cells=rows[i].split('|').filter(function(c){return c.trim()});if(cells.length<2)continue;if(!inTable){h+='<table style=margin:8px 0;font-size:12px><tr>';for(var c=0;c<cells.length;c++){h+='<th style="padding:4px 8px;border:1px solid var(--border);color:var(--dim);font-weight:600;background:var(--bg)">'+inMd(cells[c].trim())+'</th>'}h+='</tr>';inTable=true}else{h+='<tr>';for(var c=0;c<cells.length;c++){h+='<td style="padding:4px 8px;border:1px solid var(--border)">'+inMd(cells[c].trim())+'</td>'}h+='</tr>'}}if(inTable)h+='</table>';continue;}
|
||||
h+='<p style="line-height:1.7;margin:6px 0">'+inMd(block)+'</p>';}
|
||||
return h;
|
||||
function inMd(t){return t.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent)" target=_blank>$1</a>')}}
|
||||
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
|
||||
async function fetchEasyTierStatus(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var c=document.getElementById('ct-infra');if(!c)return;var el=document.getElementById('easytier-section');if(!el){el=document.createElement('div');el.id='easytier-section';el.innerHTML='<div class=ps style=margin-top:16px><h2>EasyTier VPN</h2><div class=psv><div class=i><span class=d id=et-w></span>Windows <span id=et-w-ip></span></div><div class=i><span class=d id=et-246></span>246 <span id=et-246-ip></span></div></div><div style=margin-top:8px;display:flex;gap:8px;align-items:center><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off disabled>Turn Off</button><span id=et-status style=font-size:12px;color:var(--dim)>-</span></div></div>';c.appendChild(el);var ob=document.getElementById('btn-et-on');var ofb=document.getElementById('btn-et-off');if(ob)ob.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fetchEasyTierStatus,2000);toast('EasyTier On OK')}).catch(function(e){toast('EasyTier On FAIL','err')})};if(ofb)ofb.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fetchEasyTierStatus,2000);toast('EasyTier Off OK')}).catch(function(e){toast('EasyTier Off FAIL','err')})};}var e1=document.getElementById('et-w');if(e1)e1.className='d '+(s.windows==='running'?'ok':'stopped');var e2=document.getElementById('et-246');if(e2)e2.className='d '+(s['246']==='running'?'ok':'stopped');if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}var allRunning=Object.values(s).every(function(v){return v==='running'||v==='pending'});var onBtn=document.getElementById('btn-et-on');var offBtn=document.getElementById('btn-et-off');var stxt=document.getElementById('et-status');if(onBtn)onBtn.disabled=allRunning;if(offBtn)offBtn.disabled=!allRunning;if(stxt)stxt.textContent=allRunning?'Connected':'Off'}catch(e){}}
|
||||
async function fetchRdpStatus(){try{var r=await fetch('/api/rdp'),d=await r.json();var c=document.getElementById('ct-infra');if(!c)return;var el=document.getElementById('rdp-section');if(!el){el=document.createElement('div');el.id='rdp-section';el.innerHTML='<div class=ps style=margin-top:16px><h2>RDP Remote Desktop</h2><div class=psv><div class=i><span class=d id=rdp-st></span>RDP <span id=rdp-info></span></div><div class=i><span class=d id=rdp-tun></span>Tunnel</div></div><div style=margin-top:8px;display:flex;gap:8px;align-items:center><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off disabled>Disable</button><span id=rdp-status-txt style=font-size:12px;color:var(--dim)>-</span></div></div>';c.appendChild(el);var ob=document.getElementById('btn-rdp-on');var ofb=document.getElementById('btn-rdp-off');if(ob)ob.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fetchRdpStatus,2000);toast('RDP On OK')}).catch(function(e){toast('RDP On FAIL','err')})};if(ofb)ofb.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fetchRdpStatus,2000);toast('RDP Off OK')}).catch(function(e){toast('RDP Off FAIL','err')})};}var e1=document.getElementById('rdp-st');if(e1)e1.className='d '+(d.rdp_enabled?'ok':'stopped');var e2=document.getElementById('rdp-tun');if(e2)e2.className='d '+(d.tunnel_running?'ok':'stopped');var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||'';var fullyOn=d.rdp_enabled&&d.tunnel_running;var onBtn=document.getElementById('btn-rdp-on');var offBtn=document.getElementById('btn-rdp-off');var stxt=document.getElementById('rdp-status-txt');if(onBtn)onBtn.disabled=fullyOn;if(offBtn)offBtn.disabled=!fullyOn;if(stxt)stxt.textContent=fullyOn?'Connected':d.rdp_enabled?'Tunnel pending':'Disconnected'}catch(e){}}
|
||||
|
||||
Reference in New Issue
Block a user