@evomap/evolver-webui 2.0.0-beta.6 → 2.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/console.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** 3 栏主控台 HTML(自包含, 无构建步; 轮询 /api/* 渲染). 军杰 §9.5: Triggers|Timeline|Inspector. */
2
- export declare const CONSOLE_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>Evolver \u63A7\u53F0</title>\n<style>\n*{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(320px,1fr);overflow:auto}\nheader{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}\nheader b{color:#58a6ff}.muted{color:#8b949e}\n.cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;min-height:320px}\n.col{overflow:auto;border-right:1px solid #30363d;padding:8px}\nh2{font-size:12px;text-transform:uppercase;color:#8b949e;margin:4px 0 8px;letter-spacing:.05em}\n.row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px}\n.clickable{cursor:pointer}.clickable:hover,.row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}\n.ok{background:#1a3d1a;color:#6fdd6f}.fail{background:#3d1a1a;color:#ff7b72}.warn{background:#3d3a1a;color:#e3b341}\n.act{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}\nbutton:hover{border-color:#58a6ff}button:disabled{cursor:default;opacity:.5}pre{white-space:pre-wrap;word-break:break-word;background:#161b22;padding:8px;border-radius:6px}\na{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}\n.valuecard .big{font-size:18px;color:#6fdd6f;font-weight:600}.valuecard .est{color:#e3b341}.valuecard .lbl{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}\n#reviewqueue{max-height:156px;overflow:auto;align-items:flex-start}\n#assetbar{max-height:190px;overflow:auto;align-items:flex-start}.assetgrid{display:flex;gap:6px;flex-wrap:wrap}.assetbutton{text-align:left;max-width:360px}.pager{display:flex;gap:6px;align-items:center}.lineage-section{margin-top:12px;scroll-margin-top:8px}.lineage-section h3{font-size:11px;color:#8b949e;text-transform:uppercase}.lineage-empty{color:#8b949e}\n.observability{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));background:#0d1117;border-bottom:1px solid #30363d}.surface{padding:10px 14px;min-height:150px;border-right:1px solid #30363d;border-top:1px solid #30363d;overflow:auto}.surface-head{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}.surface-body{max-height:260px;overflow:auto}.surface h3{margin:0;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px}.metric{padding:6px;background:#161b22;border-radius:6px}.metric b{display:block;color:#8b949e;font-size:11px}.logview{max-height:220px;overflow:auto;margin:0}.relation-list{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.relation-card{padding:6px 8px;border:1px solid #30363d;border-radius:6px;min-width:180px}.section-nav{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}\n@media(max-width:760px){body{display:block;overflow:auto}.cols,.observability{display:block}.col{border-right:0;border-bottom:1px solid #30363d;min-height:240px}#reviewqueue{max-height:none}.surface{border-right:0}}\n</style></head><body>\n<header><b>EVOLVER</b> <span id=\"stat\" class=\"muted\">\u2026</span><span id=\"daily\" class=\"muted\"></span></header>\n<div class=\"valuecard\" id=\"valuecard\"><span class=\"muted\">\u4EF7\u503C / value \u2026</span></div>\n<div class=\"valuecard\" id=\"reviewqueue\"><span class=\"muted\">review queue \u2026</span></div>\n<div class=\"valuecard\" id=\"retentionbar\"><span class=\"muted\">retention health \u2026</span></div>\n<div class=\"diagnostics\">\n <div class=\"valuecard\" id=\"assetbar\"><span class=\"muted\">asset lineage \u2026</span></div>\n <div class=\"observability\" aria-label=\"deep observability\">\n <section class=\"surface\"><div class=\"surface-head\"><h3>Personality</h3><span id=\"personality-status\" class=\"muted\">\u2026</span></div><div id=\"personality\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>MemoryGraph</h3><span id=\"memorygraph-status\" class=\"muted\">\u2026</span></div><div id=\"memorygraph\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>Logs</h3><span id=\"logs-status\" class=\"muted\">\u2026</span></div><div id=\"logs\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>GitHub PR status</h3><span id=\"githubprs-status\" class=\"muted\">\u2026</span></div><div id=\"githubprs\" class=\"surface-body\"></div></section>\n </div>\n</div>\n<div class=\"cols\">\n <div class=\"col\"><h2>Triggers</h2><div id=\"triggers\"></div></div>\n <div class=\"col\"><h2>Now &amp; Timeline</h2><div id=\"cycles\"></div></div>\n <div class=\"col\"><h2>Inspector</h2><div id=\"inspector\" class=\"muted\">\u9009\u4E00\u4E2A cycle \u6216\u4E8B\u4EF6</div>\n <div class=\"act\"><button id=\"act-observe\">Observe</button><button id=\"act-nudge\">Nudge</button><button id=\"act-intervene\">Intervene</button><button id=\"act-teach\">Teach</button></div>\n </div>\n</div>\n<script>\nlet sel=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';\nconst FRAG=new URLSearchParams(location.hash.slice(1));\nconst QUERY=new URLSearchParams(location.search);\nlet STORED='';try{STORED=sessionStorage.getItem('evolver.dashboard.token')||''}catch{}\nconst TOK=FRAG.get('token')||QUERY.get('token')||STORED;\nif(TOK)try{sessionStorage.setItem('evolver.dashboard.token',TOK)}catch{}\nif(FRAG.has('token')||QUERY.has('token')){QUERY.delete('token');const query=QUERY.toString();history.replaceState(null,'',location.pathname+(query?'?'+query:''))}\nasync function j(url,options){options=options||{};options.headers=Object.assign({},options.headers||{});if(TOK)options.headers.authorization='Bearer '+TOK;options.credentials='same-origin';const response=await fetch(url,options);return response.json()}\n// Kept for compatibility with older embedded-console extensions. New observability surfaces use DOM text nodes only.\nconst esc=value=>String(value==null?'':value).replace(/[&<>\"']/g,char=>'&#'+char.charCodeAt(0)+';');\nconst q=value=>String(value==null?'':value).replace(/[^a-zA-Z0-9:_.-]/g,'');\nconst nf=value=>Math.round(Number(value)||0).toLocaleString('en-US');\nconst el=(tag,text,cls)=>{const node=document.createElement(tag);if(text!=null)node.textContent=String(text);if(cls)node.className=cls;return node};\nconst clear=node=>{while(node.firstChild)node.removeChild(node.firstChild)};\nconst appendLine=(parent,label,value)=>{const row=el('div');row.append(el('b',label+': '),document.createTextNode(value==null||value===''?'\u2014':String(value)));parent.append(row)};\nconst asArray=value=>Array.isArray(value)?value:[];\nfunction safeJson(value){try{return JSON.stringify(value,null,1)}catch{return '[unavailable]'}}\nfunction sourceSection(parent,title,source,render,id){const section=el('section',null,'lineage-section');if(id)section.id=id;section.append(el('h3',title));\n if(!source||source.available!==true){section.append(el('div',(source&&source.error)||'unavailable','tag warn'));parent.append(section);return;}\n render(section,source.data);parent.append(section);\n}\nfunction setSurfaceStatus(name,text,cls){const status=document.getElementById(name+'-status');status.textContent=text;status.className=cls||'muted'}\nfunction surfaceFailure(name,message){setSurfaceStatus(name,'unavailable','tag warn');const root=document.getElementById(name);clear(root);root.append(el('div',message||'unavailable','muted'))}\nfunction githubUrl(value){try{const url=new URL(String(value));const host=url.hostname.toLowerCase();if(url.protocol!=='https:'||(host!=='github.com'&&!host.endsWith('.github.com'))||url.port)return null;url.username='';url.password='';url.search='';url.hash='';return url.href}catch{return null}}\nfunction externalLink(url,label){const valid=githubUrl(url);if(!valid)return null;const link=el('a',label||valid);link.href=valid;link.target='_blank';link.rel='noopener noreferrer';return link}\nfunction relationItems(value){if(Array.isArray(value))return value;if(value&&value.available===true){if(Array.isArray(value.data))return value.data;if(value.data&&Array.isArray(value.data.items))return value.data.items}if(value&&Array.isArray(value.items))return value.items;return []}\nfunction relationLabel(value){if(value==null)return 'relation';if(typeof value!=='object')return String(value);const fields=['title','type','id','trajectoryId','traceId','sessionId','number','state'];const parts=[];for(const field of fields){if(value[field]!=null&&value[field]!=='')parts.push(field+' '+String(value[field]))}return parts.join(' \u00B7 ')||'relation'}\nfunction renderRelationCards(parent,items,kind){const list=el('div',null,'relation-list');for(const item of items){const card=el('div',null,'relation-card');if(kind==='asset'){\n const assetId=String(item&&item.assetId||'');if(!assetId)continue;const button=el('button',relationLabel(item));button.addEventListener('click',()=>void showAsset(assetId,1,1));card.append(button);\n }else if(kind==='pr'){\n const link=externalLink(item&&item.url,relationLabel(item));card.append(link||el('span',relationLabel(item)));\n }else card.append(el('span',relationLabel(item)));\n list.append(card)}if(list.childNodes.length)parent.append(list)}\nfunction renderOptionalRelations(parent,relations,scope){if(!relations)return;const assets=relationItems(relations.assets);const trajectories=relationItems(relations.trajectories);const prs=relationItems(relations.pullRequests);\n if(assets.length)sourceSection(parent,'related assets',{available:true,data:assets},(section,data)=>renderRelationCards(section,data,'asset'));\n if(trajectories.length)sourceSection(parent,'trajectories',{available:true,data:trajectories},(section,data)=>renderRelationCards(section,data,'trajectory'),scope?scope+'-trajectories':undefined);\n if(prs.length)sourceSection(parent,'related pull requests',{available:true,data:prs},(section,data)=>renderRelationCards(section,data,'pr'));\n}\nasync function refreshStatus(){try{const status=await j('/api/status');document.getElementById('stat').textContent='events='+status.totalEvents+' cycles='+status.cycles}catch{document.getElementById('stat').textContent='status unavailable'}}\nasync function refreshDaily(){try{const daily=await j('/api/daily-summary');document.getElementById('daily').textContent='\u4ECA\u65E5 '+daily.solidified+'\u2713/'+daily.failed+'\u2717 \u89E6\u53D1'+daily.triggered}catch{document.getElementById('daily').textContent='daily unavailable'}}\nasync function refreshValue(){const root=document.getElementById('valuecard');try{const value=await j('/api/value?window=7d');clear(root);\n const measured=el('div');measured.append(el('div','measured saved (7d)','lbl'),el('span',nf(value.totalTokensSaved)+' tokens','big'),document.createTextNode(' '),el('span','$'+Number(value.totalCostUsd||0).toFixed(4),'muted'));\n const estimated=el('div');estimated.append(el('div','estimated','lbl'),el('span',nf(value.estimated&&value.estimated.totalTokensSaved)+' tokens','est'),document.createTextNode(' '),el('span','(tracked separately)','muted'));\n const top=el('div');top.append(el('div','top reused','lbl'));const topText=asArray(value.topGenes).slice(0,3).map(g=>String(g.assetId)+' \u00D7'+String(g.reuses)).join(' \u00B7 ')||'\u2014';top.append(el('span',topText,'muted'));root.append(measured,estimated,top);\n }catch{clear(root);root.append(el('span','value unavailable','tag warn'))}}\nasync function refreshReview(){const root=document.getElementById('reviewqueue');try{const review=await j('/api/review');clear(root);const pending=asArray(review).filter(item=>item.state==='quarantined').length;const summary=el('div');summary.append(el('div','review queue','lbl'),el('span',pending,'big'),document.createTextNode(' '),el('span','\u5F85\u5BA1 pending','muted'));root.append(summary);\n const stateClass=state=>state==='quarantined'?'warn':state==='approved'?'ok':state==='rejected'?'fail':'muted';for(const item of asArray(review).slice(0,8)){const row=el('div');row.append(el('span',item.state,'tag '+stateClass(item.state)),document.createTextNode(' '),el('span',item.geneId));if(item.autoDrafted)row.append(document.createTextNode(' '),el('span','auto','muted'));\n if(item.state==='quarantined'){const assetId=String(item.assetId||'');const approve=el('button','Approve');const reject=el('button','Reject');approve.addEventListener('click',()=>void reviewAct(assetId,'approve'));reject.addEventListener('click',()=>void reviewAct(assetId,'reject'));row.append(document.createTextNode(' '),approve,reject)}root.append(row)}\n }catch{clear(root);root.append(el('span','review unavailable','tag warn'))}}\nasync function refreshTriggers(){const root=document.getElementById('triggers');try{const triggers=await j('/api/triggers');clear(root);for(const trigger of asArray(triggers)){const row=el('div',null,'row');row.append(el('span',trigger.triggered?'\u89E6\u53D1':'\u6291\u5236','tag '+(trigger.triggered?'ok':'warn')),document.createTextNode(' '+String(trigger.patternId)+' '));const value=Number(trigger.value);row.append(el('span','v='+(Number.isFinite(value)?value.toFixed(2):String(trigger.value)),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0','muted'))}catch{clear(root);root.append(el('div','triggers unavailable','muted'))}}\nasync function refreshCycles(){const root=document.getElementById('cycles');try{const cycles=await j('/api/cycles');clear(root);for(const cycle of asArray(cycles).slice(-30).reverse()){const cycleId=String(cycle.cycleId||'');const row=el('div',null,'row clickable');const stageClass=cycle.finalStage==='solidified'?'ok':cycle.finalStage==='failed'?'fail':'warn';row.append(el('span',cycle.finalStage,'tag '+stageClass),document.createTextNode(' '+String(cycle.cycleId)+' '),el('span','('+String(cycle.events)+')','muted'));row.addEventListener('click',()=>void showCycle(cycleId));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0 cycle','muted'))}catch{clear(root);root.append(el('div','cycles unavailable','muted'))}}\nfunction refresh(){void refreshStatus();void refreshDaily();void refreshValue();void refreshReview();void refreshTriggers();void refreshCycles()}\nasync function refreshRetention(){const root=document.getElementById('retentionbar');try{const retention=await j('/api/retention');clear(root);if(!retention||retention.available!==true){root.append(el('span','retention unavailable','muted'));return;}\n const safe=!!(retention.rootEvents&&retention.rootEvents.archiveRotationSafe&&retention.material&&retention.material.archiveRotationSafe);const summary=el('div');summary.append(el('div','retention health','lbl'),el('span',safe?'safe':'attention','tag '+(safe?'ok':'warn')),document.createTextNode(' '),el('span','warnings '+asArray(retention.warnings).length,'muted'));root.append(summary);\n const rootEvents=el('div');rootEvents.append(el('div','root events \u00B7 '+String(retention.rootEvents.state),'lbl'),el('span',nf(retention.rootEvents.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.rootEvents.archiveRecords)+' archive \u00B7 '+nf(retention.rootEvents.historyRecords)+' history','muted'));root.append(rootEvents);\n const material=el('div');material.append(el('div','material \u00B7 '+String(retention.material.state),'lbl'),el('span',nf(retention.material.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.material.archiveRecords)+' archive \u00B7 '+nf(retention.material.historyRecords)+' history \u00B7 '+nf(retention.material.pending)+' pending \u00B7 cursor '+String(retention.material.cursor),'muted'));root.append(material);\n }catch{clear(root);root.append(el('span','retention unavailable','tag warn'))}}\nasync function refreshAssets(){const root=document.getElementById('assetbar');try{const source=await j('/api/asset-lineage/assets?page='+assetPage+'&pageSize=12');clear(root);const title=el('div');title.append(el('div','asset lineage','lbl'));root.append(title);\n if(!source||source.available!==true){root.append(el('span',(source&&source.error)||'asset store unavailable','tag warn'));return;}const grid=el('div',null,'assetgrid');for(const asset of source.data.items){const assetId=String(asset.assetId||'');const button=el('button',asset.type+' \u00B7 '+asset.id+(asset.summary?' \u00B7 '+asset.summary:''),'assetbutton');button.addEventListener('click',()=>void showAsset(assetId,1,1));grid.append(button)}if(source.data.items.length===0)grid.append(el('span','no assets','muted'));root.append(grid);\n const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=source.data.page<=1;previous.addEventListener('click',()=>{assetPage=Math.max(1,assetPage-1);void refreshAssets()});const next=el('button','Next');next.disabled=!source.data.hasMore;next.addEventListener('click',()=>{assetPage+=1;void refreshAssets()});pager.append(previous,el('span','page '+source.data.page+(source.data.truncated?' \u00B7 bounded':''),'muted'),next);root.append(pager);\n }catch{clear(root);root.append(el('span','asset store unavailable','tag warn'))}}\nfunction renderPersonalityRecords(parent,title,records){if(!Array.isArray(records)||records.length===0)return;const section=el('section',null,'lineage-section');section.append(el('h3',title));for(const record of records.slice(-8).reverse()){const row=el('div',null,'row');if(record&&typeof record==='object'){for(const key of ['at','updatedAt','key','success','fail','avgScore','n','outcome','score'])if(record[key]!=null)appendLine(row,key,record[key]);if(!row.firstChild)row.append(el('span',safeJson(record)))}else row.append(el('span',record));section.append(row)}parent.append(section)}\nasync function refreshPersonality(){const root=document.getElementById('personality');try{const response=await j('/api/personality');if(!response||response.available!==true){surfaceFailure('personality',(response&&response.error)||'personality unavailable');return;}clear(root);setSurfaceStatus('personality','live','tag ok');const data=response.data||{};const current=data.current&&typeof data.current==='object'?data.current:{};const grid=el('div',null,'metricgrid');for(const key of Object.keys(current).slice(0,5)){const metric=el('div',null,'metric');metric.append(el('b',key),el('span',current[key]));grid.append(metric)}if(!grid.firstChild)grid.append(el('span','no current profile','muted'));root.append(grid);appendLine(root,'updated',data.updatedAt);renderPersonalityRecords(root,'stats',data.stats);renderPersonalityRecords(root,'history',data.history)}catch{surfaceFailure('personality','personality unavailable')}}\nasync function refreshMemoryGraph(){const root=document.getElementById('memorygraph');try{const response=await j('/api/memory-graph');if(!response||response.available!==true){surfaceFailure('memorygraph',(response&&response.error)||'memory graph unavailable');return;}clear(root);const recovery=String(response.recovery||'degraded');const gate=recovery==='healthy'?'ok':recovery==='degraded'?'fail':'warn';setSurfaceStatus('memorygraph',recovery,'tag '+gate);appendLine(root,'selection',response.selectionReason||'no scoped selection yet');appendLine(root,'compacted records',nf(response.compactedRecords));appendLine(root,'active records',nf(response.activeRecords));appendLine(root,'archives',nf(response.archives));appendLine(root,'corrupt lines',nf(response.corruptLines));appendLine(root,'oversized lines',nf(response.oversizedLines));appendLine(root,'oversized files',nf(response.oversizedFiles))}catch{surfaceFailure('memorygraph','memory graph unavailable')}}\nasync function refreshLogs(){const root=document.getElementById('logs');try{const response=await j('/api/logs');if(!response||response.available!==true){surfaceFailure('logs',(response&&response.error)||'logs unavailable');return;}clear(root);setSurfaceStatus('logs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const pre=el('pre',null,'logview');pre.append(document.createTextNode(asArray(response.data&&response.data.lines).join('\\n')));root.append(pre)}catch{surfaceFailure('logs','logs unavailable')}}\nfunction checksText(checks){checks=checks||{};return 'checks '+Number(checks.passed||0)+' passing \u00B7 '+Number(checks.failed||0)+' failing \u00B7 '+Number(checks.pending||0)+' pending / '+Number(checks.total||0)}\nasync function refreshGitHubPrs(){const root=document.getElementById('githubprs');try{const response=await j('/api/github-prs');if(!response||response.available!==true){surfaceFailure('githubprs',(response&&response.error)||'GitHub PR status unavailable');return;}clear(root);setSurfaceStatus('githubprs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const items=asArray(response.data&&response.data.prs);for(const item of items){const row=el('div',null,'row');const title='#'+String(item.number)+' '+String(item.title||'');const link=externalLink(item.url,title);row.append(link||el('span',title));const state=String(item.state||'unknown')+(item.isDraft?' \u00B7 draft':'');appendLine(row,'state',state);appendLine(row,'branch',String(item.head||'\u2014')+' \u2192 '+String(item.base||'\u2014'));appendLine(row,'review',item.reviewDecision);appendLine(row,'updated',item.updatedAt);row.append(el('div',checksText(item.checks),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','no pull requests','muted'))}catch{surfaceFailure('githubprs','GitHub PR status unavailable')}}\nfunction renderRows(section,page,fields){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of fields)appendLine(row,field[0],item[field[1]]);section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction renderEventRows(section,page){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of [['seq','seq'],['type','type'],['time','ts'],['title','title'],['why','why']])appendLine(row,field[0],item[field[1]]);const cycleId=String(item.cycleId||'');if(cycleId){const button=el('button','Open cycle '+String(item.cycleId));button.addEventListener('click',()=>void showCycle(cycleId));row.append(button)}section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction lineagePager(parent,label,page,data,onPage){const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=page<=1;previous.addEventListener('click',()=>onPage(page-1));const next=el('button','Next');next.disabled=!data||!data.hasMore;next.addEventListener('click',()=>onPage(page+1));pager.append(el('span',label,'muted'),previous,el('span','page '+page,'muted'),next);parent.append(pager)}\nfunction inspectorNav(parent,targetId,label){const button=el('button',label);button.addEventListener('click',()=>{const target=document.getElementById(targetId);if(target)target.scrollIntoView({block:'start'})});parent.append(button)}\nasync function showAsset(id,capsulePage,eventPage){lineageId=id;lineageCapsulePage=capsulePage||1;lineageEventPage=eventPage||1;sel=null;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading asset lineage \u2026','muted'));try{const response=await j('/api/asset-lineage?id='+encodeURIComponent(id)+'&capsulePage='+lineageCapsulePage+'&eventPage='+lineageEventPage+'&pageSize=20');const relations=response.relations||{trajectories:response.trajectories,pullRequests:response.pullRequests};clear(root);root.append(el('h2','Asset lineage'));const nav=el('div',null,'section-nav');inspectorNav(nav,'asset-source','Source');inspectorNav(nav,'asset-review','Review');if(relationItems(relations.trajectories).length)inspectorNav(nav,'asset-trajectories','Trajectory');root.append(nav);\n sourceSection(root,'asset',response.asset,(section,data)=>{if(!data){section.append(el('div','asset not found','tag warn'));return;}appendLine(section,'type',data.type);appendLine(section,'id',data.id);appendLine(section,'asset',data.assetId);appendLine(section,'category',data.category);appendLine(section,'summary',data.summary)});\n sourceSection(root,'review',response.review,(section,data)=>{if(!data){section.append(el('div','default eligible / no record','lineage-empty'));return;}appendLine(section,'state',data.state);appendLine(section,'at',data.at);appendLine(section,'by',data.by);appendLine(section,'reason',data.reason)},'asset-review');\n sourceSection(root,'provenance / source',response.provenance,(section,data)=>{if(!data){section.append(el('div','local default / no record','lineage-empty'));return;}appendLine(section,'source',data.source);appendLine(section,'trusted',data.trusted);appendLine(section,'at',data.at);appendLine(section,'promoted by',data.promotedBy);appendLine(section,'reason',data.reason)},'asset-source');\n sourceSection(root,'capsules',response.capsules,(section,data)=>renderRows(section,data,[['type','type'],['id','id'],['asset','assetId'],['summary','summary']]));sourceSection(root,'events',response.events,(section,data)=>renderEventRows(section,data));renderOptionalRelations(root,relations,'asset');lineagePager(root,'capsules',lineageCapsulePage,response.capsules&&response.capsules.data,page=>showAsset(lineageId,page,lineageEventPage));lineagePager(root,'events',lineageEventPage,response.events&&response.events.data,page=>showAsset(lineageId,lineageCapsulePage,page));\n }catch{clear(root);root.append(el('span','asset lineage unavailable','tag warn'))}}\nasync function showCycle(id){sel=id;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading cycle \u2026','muted'));try{const response=await j('/api/cycle?id='+encodeURIComponent(id));const relations=response.relations;clear(root);root.append(el('h2','Cycle '+String(id)));if(relationItems(relations&&relations.trajectories).length){const nav=el('div',null,'section-nav');inspectorNav(nav,'cycle-trajectories','Trajectory');root.append(nav)}renderOptionalRelations(root,relations,'cycle');for(const event of asArray(response.timeline)){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.seq)+' '),el('b',event.type));row.append(heading);if(event.title)row.append(el('div',event.title));if(event.why)appendLine(row,'why',event.why);if(event.payload!=null){const pre=el('pre');pre.append(document.createTextNode(safeJson(event.payload)));row.append(pre)}root.append(row)}if(!asArray(response.timeline).length)root.append(el('div','no timeline events','muted'))}catch{clear(root);root.append(el('span','cycle unavailable','tag warn'))}}\nasync function reviewAct(gene,action){const reason=prompt(action+' \u7406\u7531 reason:');if(reason===null)return;try{const response=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});if(!response||response.ok!==true){alert('review \u5931\u8D25 failed: '+((response&&response.error)||'unknown'));return;}}catch(error){alert('review \u5931\u8D25 failed: '+error);return;}refresh()}\nasync function act(kind){const note=prompt(kind+' \u5907\u6CE8:');if(note===null)return;await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh()}\nfor(const kind of ['observe','nudge','intervene','teach'])document.getElementById('act-'+kind).addEventListener('click',()=>void act(kind));\nrefresh();void refreshRetention();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();\nsetInterval(refresh,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);\n</script></body></html>";
2
+ export declare const CONSOLE_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>Evolver \u63A7\u53F0</title>\n<style>\n*{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(220px,auto) minmax(320px,1fr);overflow:auto}\nheader{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}\nheader b{color:#58a6ff}.muted{color:#8b949e}\n.cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;min-height:320px}\n.col{overflow:auto;border-right:1px solid #30363d;padding:8px}\nh2{font-size:12px;text-transform:uppercase;color:#8b949e;margin:4px 0 8px;letter-spacing:.05em}\n.row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px}\n.clickable{cursor:pointer}.clickable:hover,.row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}\n.ok{background:#1a3d1a;color:#6fdd6f}.fail{background:#3d1a1a;color:#ff7b72}.warn{background:#3d3a1a;color:#e3b341}\n.act{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}\nbutton:hover{border-color:#58a6ff}button:disabled{cursor:default;opacity:.5}pre{white-space:pre-wrap;word-break:break-word;background:#161b22;padding:8px;border-radius:6px}\na{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}\n.valuecard .big{font-size:18px;color:#6fdd6f;font-weight:600}.valuecard .est{color:#e3b341}.valuecard .lbl{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}\n#reviewqueue{max-height:156px;overflow:auto;align-items:flex-start}\n#assetbar{max-height:190px;overflow:auto;align-items:flex-start}.assetgrid{display:flex;gap:6px;flex-wrap:wrap}.assetbutton{text-align:left;max-width:360px}.pager{display:flex;gap:6px;align-items:center}.lineage-section{margin-top:12px;scroll-margin-top:8px}.lineage-section h3{font-size:11px;color:#8b949e;text-transform:uppercase}.lineage-empty{color:#8b949e}\n.observability{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));background:#0d1117;border-bottom:1px solid #30363d}.surface{padding:10px 14px;min-height:150px;border-right:1px solid #30363d;border-top:1px solid #30363d;overflow:auto}.surface-head{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}.surface-body{max-height:260px;overflow:auto}.surface h3{margin:0;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px}.metric{padding:6px;background:#161b22;border-radius:6px}.metric b{display:block;color:#8b949e;font-size:11px}.logview{max-height:220px;overflow:auto;margin:0}.relation-list{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.relation-card{padding:6px 8px;border:1px solid #30363d;border-radius:6px;min-width:180px}.section-nav{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}\n.workflowcard{display:grid;grid-template-columns:minmax(320px,.9fr) minmax(360px,1.4fr);gap:0;background:#11161d;border-bottom:1px solid #30363d;min-height:220px}\n.workflowpane{padding:10px 14px;overflow:auto;max-height:360px}.workflowpane:first-child{border-right:1px solid #30363d}\n.workflowrow{display:grid;grid-template-columns:minmax(120px,1fr) auto;gap:4px 10px}.workflowrow .meta{grid-column:1/-1}\n@media(max-width:760px){body{display:block;overflow:auto}.cols,.observability,.workflowcard{display:block}.col,.workflowpane{border-right:0;border-bottom:1px solid #30363d;min-height:240px;max-height:none}#reviewqueue{max-height:none}.surface{border-right:0}}\n</style></head><body>\n<header><b>EVOLVER</b> <span id=\"stat\" class=\"muted\">\u2026</span><span id=\"daily\" class=\"muted\"></span></header>\n<div class=\"valuecard\" id=\"valuecard\"><span class=\"muted\">\u4EF7\u503C / value \u2026</span></div>\n<div class=\"valuecard\" id=\"reviewqueue\"><span class=\"muted\">review queue \u2026</span></div>\n<div class=\"valuecard\" id=\"retentionbar\"><span class=\"muted\">retention health \u2026</span></div>\n<div class=\"diagnostics\">\n <div class=\"valuecard\" id=\"assetbar\"><span class=\"muted\">asset lineage \u2026</span></div>\n <div class=\"observability\" aria-label=\"deep observability\">\n <section class=\"surface\"><div class=\"surface-head\"><h3>Personality</h3><span id=\"personality-status\" class=\"muted\">\u2026</span></div><div id=\"personality\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>MemoryGraph</h3><span id=\"memorygraph-status\" class=\"muted\">\u2026</span></div><div id=\"memorygraph\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>Logs</h3><span id=\"logs-status\" class=\"muted\">\u2026</span></div><div id=\"logs\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>GitHub PR status</h3><span id=\"githubprs-status\" class=\"muted\">\u2026</span></div><div id=\"githubprs\" class=\"surface-body\"></div></section>\n </div>\n</div>\n<div class=\"workflowcard\">\n <section class=\"workflowpane\"><h2>Workflows</h2><div id=\"workflows\" class=\"muted\">workflow runs \u2026</div></section>\n <section class=\"workflowpane\"><h2>Workflow History</h2><div id=\"workflowhistory\" class=\"muted\">\u9009\u4E00\u4E2A workflow run</div></section>\n</div>\n<div class=\"cols\">\n <div class=\"col\"><h2>Triggers</h2><div id=\"triggers\"></div></div>\n <div class=\"col\"><h2>Now &amp; Timeline</h2><div id=\"cycles\"></div></div>\n <div class=\"col\"><h2>Inspector</h2><div id=\"inspector\" class=\"muted\">\u9009\u4E00\u4E2A cycle \u6216\u4E8B\u4EF6</div>\n <div class=\"act\"><button id=\"act-observe\">Observe</button><button id=\"act-nudge\">Nudge</button><button id=\"act-intervene\">Intervene</button><button id=\"act-teach\">Teach</button></div>\n </div>\n</div>\n<script>\nlet sel=null,selectedWorkflow=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';\nconst FRAG=new URLSearchParams(location.hash.slice(1));\nconst QUERY=new URLSearchParams(location.search);\nlet STORED='';try{STORED=sessionStorage.getItem('evolver.dashboard.token')||''}catch{}\nconst TOK=FRAG.get('token')||QUERY.get('token')||STORED;\nif(TOK)try{sessionStorage.setItem('evolver.dashboard.token',TOK)}catch{}\nif(FRAG.has('token')||QUERY.has('token')){QUERY.delete('token');const query=QUERY.toString();history.replaceState(null,'',location.pathname+(query?'?'+query:''))}\nasync function j(url,options){options=options||{};options.headers=Object.assign({},options.headers||{});if(TOK)options.headers.authorization='Bearer '+TOK;options.credentials='same-origin';const response=await fetch(url,options);return response.json()}\n// Kept for compatibility with older embedded-console extensions. New observability surfaces use DOM text nodes only.\nconst esc=value=>String(value==null?'':value).replace(/[&<>\"']/g,char=>'&#'+char.charCodeAt(0)+';');\nconst q=value=>String(value==null?'':value).replace(/[^a-zA-Z0-9:_.-]/g,'');\nconst nf=value=>Math.round(Number(value)||0).toLocaleString('en-US');\nconst el=(tag,text,cls)=>{const node=document.createElement(tag);if(text!=null)node.textContent=String(text);if(cls)node.className=cls;return node};\nconst clear=node=>{while(node.firstChild)node.removeChild(node.firstChild)};\nconst appendLine=(parent,label,value)=>{const row=el('div');row.append(el('b',label+': '),document.createTextNode(value==null||value===''?'\u2014':String(value)));parent.append(row)};\nconst asArray=value=>Array.isArray(value)?value:[];\nfunction safeJson(value){try{return JSON.stringify(value,null,1)}catch{return '[unavailable]'}}\nfunction sourceSection(parent,title,source,render,id){const section=el('section',null,'lineage-section');if(id)section.id=id;section.append(el('h3',title));\n if(!source||source.available!==true){section.append(el('div',(source&&source.error)||'unavailable','tag warn'));parent.append(section);return;}\n render(section,source.data);parent.append(section);\n}\nfunction setSurfaceStatus(name,text,cls){const status=document.getElementById(name+'-status');status.textContent=text;status.className=cls||'muted'}\nfunction surfaceFailure(name,message){setSurfaceStatus(name,'unavailable','tag warn');const root=document.getElementById(name);clear(root);root.append(el('div',message||'unavailable','muted'))}\nfunction githubUrl(value){try{const url=new URL(String(value));const host=url.hostname.toLowerCase();if(url.protocol!=='https:'||(host!=='github.com'&&!host.endsWith('.github.com'))||url.port)return null;url.username='';url.password='';url.search='';url.hash='';return url.href}catch{return null}}\nfunction externalLink(url,label){const valid=githubUrl(url);if(!valid)return null;const link=el('a',label||valid);link.href=valid;link.target='_blank';link.rel='noopener noreferrer';return link}\nfunction relationItems(value){if(Array.isArray(value))return value;if(value&&value.available===true){if(Array.isArray(value.data))return value.data;if(value.data&&Array.isArray(value.data.items))return value.data.items}if(value&&Array.isArray(value.items))return value.items;return []}\nfunction relationLabel(value){if(value==null)return 'relation';if(typeof value!=='object')return String(value);const fields=['title','type','id','trajectoryId','traceId','sessionId','number','state'];const parts=[];for(const field of fields){if(value[field]!=null&&value[field]!=='')parts.push(field+' '+String(value[field]))}return parts.join(' \u00B7 ')||'relation'}\nfunction renderRelationCards(parent,items,kind){const list=el('div',null,'relation-list');for(const item of items){const card=el('div',null,'relation-card');if(kind==='asset'){\n const assetId=String(item&&item.assetId||'');if(!assetId)continue;const button=el('button',relationLabel(item));button.addEventListener('click',()=>void showAsset(assetId,1,1));card.append(button);\n }else if(kind==='pr'){\n const link=externalLink(item&&item.url,relationLabel(item));card.append(link||el('span',relationLabel(item)));\n }else card.append(el('span',relationLabel(item)));\n list.append(card)}if(list.childNodes.length)parent.append(list)}\nfunction renderOptionalRelations(parent,relations,scope){if(!relations)return;const assets=relationItems(relations.assets);const trajectories=relationItems(relations.trajectories);const prs=relationItems(relations.pullRequests);\n if(assets.length)sourceSection(parent,'related assets',{available:true,data:assets},(section,data)=>renderRelationCards(section,data,'asset'));\n if(trajectories.length)sourceSection(parent,'trajectories',{available:true,data:trajectories},(section,data)=>renderRelationCards(section,data,'trajectory'),scope?scope+'-trajectories':undefined);\n if(prs.length)sourceSection(parent,'related pull requests',{available:true,data:prs},(section,data)=>renderRelationCards(section,data,'pr'));\n}\nasync function refreshStatus(){try{const status=await j('/api/status');document.getElementById('stat').textContent='events='+status.totalEvents+' cycles='+status.cycles}catch{document.getElementById('stat').textContent='status unavailable'}}\nasync function refreshDaily(){try{const daily=await j('/api/daily-summary');document.getElementById('daily').textContent='\u4ECA\u65E5 '+daily.solidified+'\u2713/'+daily.failed+'\u2717 \u89E6\u53D1'+daily.triggered}catch{document.getElementById('daily').textContent='daily unavailable'}}\nasync function refreshValue(){const root=document.getElementById('valuecard');try{const value=await j('/api/value?window=7d');clear(root);\n const measured=el('div');measured.append(el('div','measured saved (7d)','lbl'),el('span',nf(value.totalTokensSaved)+' tokens','big'),document.createTextNode(' '),el('span','$'+Number(value.totalCostUsd||0).toFixed(4),'muted'));\n const estimated=el('div');estimated.append(el('div','estimated','lbl'),el('span',nf(value.estimated&&value.estimated.totalTokensSaved)+' tokens','est'),document.createTextNode(' '),el('span','(tracked separately)','muted'));\n const top=el('div');top.append(el('div','top reused','lbl'));const topText=asArray(value.topGenes).slice(0,3).map(g=>String(g.assetId)+' \u00D7'+String(g.reuses)).join(' \u00B7 ')||'\u2014';top.append(el('span',topText,'muted'));root.append(measured,estimated,top);\n }catch{clear(root);root.append(el('span','value unavailable','tag warn'))}}\nasync function refreshReview(){const root=document.getElementById('reviewqueue');try{const review=await j('/api/review');clear(root);const pending=asArray(review).filter(item=>item.state==='quarantined').length;const summary=el('div');summary.append(el('div','review queue','lbl'),el('span',pending,'big'),document.createTextNode(' '),el('span','\u5F85\u5BA1 pending','muted'));root.append(summary);\n const stateClass=state=>state==='quarantined'?'warn':state==='approved'?'ok':state==='rejected'?'fail':'muted';for(const item of asArray(review).slice(0,8)){const row=el('div');row.append(el('span',item.state,'tag '+stateClass(item.state)),document.createTextNode(' '),el('span',item.geneId));if(item.autoDrafted)row.append(document.createTextNode(' '),el('span','auto','muted'));\n if(item.state==='quarantined'){const assetId=String(item.assetId||'');const approve=el('button','Approve');const reject=el('button','Reject');approve.addEventListener('click',()=>void reviewAct(assetId,'approve'));reject.addEventListener('click',()=>void reviewAct(assetId,'reject'));row.append(document.createTextNode(' '),approve,reject)}root.append(row)}\n }catch{clear(root);root.append(el('span','review unavailable','tag warn'))}}\nasync function refreshTriggers(){const root=document.getElementById('triggers');try{const triggers=await j('/api/triggers');clear(root);for(const trigger of asArray(triggers)){const row=el('div',null,'row');row.append(el('span',trigger.triggered?'\u89E6\u53D1':'\u6291\u5236','tag '+(trigger.triggered?'ok':'warn')),document.createTextNode(' '+String(trigger.patternId)+' '));const value=Number(trigger.value);row.append(el('span','v='+(Number.isFinite(value)?value.toFixed(2):String(trigger.value)),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0','muted'))}catch{clear(root);root.append(el('div','triggers unavailable','muted'))}}\nasync function refreshCycles(){const root=document.getElementById('cycles');try{const cycles=await j('/api/cycles');clear(root);for(const cycle of asArray(cycles).slice(-30).reverse()){const cycleId=String(cycle.cycleId||'');const row=el('div',null,'row clickable');const stageClass=cycle.finalStage==='solidified'?'ok':cycle.finalStage==='failed'?'fail':'warn';row.append(el('span',cycle.finalStage,'tag '+stageClass),document.createTextNode(' '+String(cycle.cycleId)+' '),el('span','('+String(cycle.events)+')','muted'));row.addEventListener('click',()=>void showCycle(cycleId));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0 cycle','muted'))}catch{clear(root);root.append(el('div','cycles unavailable','muted'))}}\nfunction refresh(){void refreshStatus();void refreshDaily();void refreshValue();void refreshReview();void refreshTriggers();void refreshCycles()}\nasync function refreshRetention(){const root=document.getElementById('retentionbar');try{const retention=await j('/api/retention');clear(root);if(!retention||retention.available!==true){root.append(el('span','retention unavailable','muted'));return;}\n const safe=!!(retention.rootEvents&&retention.rootEvents.archiveRotationSafe&&retention.material&&retention.material.archiveRotationSafe);const summary=el('div');summary.append(el('div','retention health','lbl'),el('span',safe?'safe':'attention','tag '+(safe?'ok':'warn')),document.createTextNode(' '),el('span','warnings '+asArray(retention.warnings).length,'muted'));root.append(summary);\n const rootEvents=el('div');rootEvents.append(el('div','root events \u00B7 '+String(retention.rootEvents.state),'lbl'),el('span',nf(retention.rootEvents.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.rootEvents.archiveRecords)+' archive \u00B7 '+nf(retention.rootEvents.historyRecords)+' history','muted'));root.append(rootEvents);\n const material=el('div');material.append(el('div','material \u00B7 '+String(retention.material.state),'lbl'),el('span',nf(retention.material.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.material.archiveRecords)+' archive \u00B7 '+nf(retention.material.historyRecords)+' history \u00B7 '+nf(retention.material.pending)+' pending \u00B7 cursor '+String(retention.material.cursor),'muted'));root.append(material);\n }catch{clear(root);root.append(el('span','retention unavailable','tag warn'))}}\nasync function refreshAssets(){const root=document.getElementById('assetbar');try{const source=await j('/api/asset-lineage/assets?page='+assetPage+'&pageSize=12');clear(root);const title=el('div');title.append(el('div','asset lineage','lbl'));root.append(title);\n if(!source||source.available!==true){root.append(el('span',(source&&source.error)||'asset store unavailable','tag warn'));return;}const grid=el('div',null,'assetgrid');for(const asset of source.data.items){const assetId=String(asset.assetId||'');const button=el('button',asset.type+' \u00B7 '+asset.id+(asset.summary?' \u00B7 '+asset.summary:''),'assetbutton');button.addEventListener('click',()=>void showAsset(assetId,1,1));grid.append(button)}if(source.data.items.length===0)grid.append(el('span','no assets','muted'));root.append(grid);\n const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=source.data.page<=1;previous.addEventListener('click',()=>{assetPage=Math.max(1,assetPage-1);void refreshAssets()});const next=el('button','Next');next.disabled=!source.data.hasMore;next.addEventListener('click',()=>{assetPage+=1;void refreshAssets()});pager.append(previous,el('span','page '+source.data.page+(source.data.truncated?' \u00B7 bounded':''),'muted'),next);root.append(pager);\n }catch{clear(root);root.append(el('span','asset store unavailable','tag warn'))}}\nfunction renderPersonalityRecords(parent,title,records){if(!Array.isArray(records)||records.length===0)return;const section=el('section',null,'lineage-section');section.append(el('h3',title));for(const record of records.slice(-8).reverse()){const row=el('div',null,'row');if(record&&typeof record==='object'){for(const key of ['at','updatedAt','key','success','fail','avgScore','n','outcome','score'])if(record[key]!=null)appendLine(row,key,record[key]);if(!row.firstChild)row.append(el('span',safeJson(record)))}else row.append(el('span',record));section.append(row)}parent.append(section)}\nasync function refreshPersonality(){const root=document.getElementById('personality');try{const response=await j('/api/personality');if(!response||response.available!==true){surfaceFailure('personality',(response&&response.error)||'personality unavailable');return;}clear(root);setSurfaceStatus('personality','live','tag ok');const data=response.data||{};const current=data.current&&typeof data.current==='object'?data.current:{};const grid=el('div',null,'metricgrid');for(const key of Object.keys(current).slice(0,5)){const metric=el('div',null,'metric');metric.append(el('b',key),el('span',current[key]));grid.append(metric)}if(!grid.firstChild)grid.append(el('span','no current profile','muted'));root.append(grid);appendLine(root,'updated',data.updatedAt);renderPersonalityRecords(root,'stats',data.stats);renderPersonalityRecords(root,'history',data.history)}catch{surfaceFailure('personality','personality unavailable')}}\nasync function refreshMemoryGraph(){const root=document.getElementById('memorygraph');try{const response=await j('/api/memory-graph');if(!response||response.available!==true){surfaceFailure('memorygraph',(response&&response.error)||'memory graph unavailable');return;}clear(root);const recovery=String(response.recovery||'degraded');const gate=recovery==='healthy'?'ok':recovery==='degraded'?'fail':'warn';setSurfaceStatus('memorygraph',recovery,'tag '+gate);appendLine(root,'selection',response.selectionReason||'no scoped selection yet');appendLine(root,'compacted records',nf(response.compactedRecords));appendLine(root,'active records',nf(response.activeRecords));appendLine(root,'archives',nf(response.archives));appendLine(root,'corrupt lines',nf(response.corruptLines));appendLine(root,'oversized lines',nf(response.oversizedLines));appendLine(root,'oversized files',nf(response.oversizedFiles))}catch{surfaceFailure('memorygraph','memory graph unavailable')}}\nasync function refreshLogs(){const root=document.getElementById('logs');try{const response=await j('/api/logs');if(!response||response.available!==true){surfaceFailure('logs',(response&&response.error)||'logs unavailable');return;}clear(root);setSurfaceStatus('logs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const pre=el('pre',null,'logview');pre.append(document.createTextNode(asArray(response.data&&response.data.lines).join('\\n')));root.append(pre)}catch{surfaceFailure('logs','logs unavailable')}}\nfunction checksText(checks){checks=checks||{};return 'checks '+Number(checks.passed||0)+' passing \u00B7 '+Number(checks.failed||0)+' failing \u00B7 '+Number(checks.pending||0)+' pending / '+Number(checks.total||0)}\nasync function refreshGitHubPrs(){const root=document.getElementById('githubprs');try{const response=await j('/api/github-prs');if(!response||response.available!==true){surfaceFailure('githubprs',(response&&response.error)||'GitHub PR status unavailable');return;}clear(root);setSurfaceStatus('githubprs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const items=asArray(response.data&&response.data.prs);for(const item of items){const row=el('div',null,'row');const title='#'+String(item.number)+' '+String(item.title||'');const link=externalLink(item.url,title);row.append(link||el('span',title));const state=String(item.state||'unknown')+(item.isDraft?' \u00B7 draft':'');appendLine(row,'state',state);appendLine(row,'branch',String(item.head||'\u2014')+' \u2192 '+String(item.base||'\u2014'));appendLine(row,'review',item.reviewDecision);appendLine(row,'updated',item.updatedAt);row.append(el('div',checksText(item.checks),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','no pull requests','muted'))}catch{surfaceFailure('githubprs','GitHub PR status unavailable')}}\nfunction renderRows(section,page,fields){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of fields)appendLine(row,field[0],item[field[1]]);section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction renderEventRows(section,page){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of [['seq','seq'],['type','type'],['time','ts'],['title','title'],['why','why']])appendLine(row,field[0],item[field[1]]);const cycleId=String(item.cycleId||'');if(cycleId){const button=el('button','Open cycle '+String(item.cycleId));button.addEventListener('click',()=>void showCycle(cycleId));row.append(button)}section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction lineagePager(parent,label,page,data,onPage){const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=page<=1;previous.addEventListener('click',()=>onPage(page-1));const next=el('button','Next');next.disabled=!data||!data.hasMore;next.addEventListener('click',()=>onPage(page+1));pager.append(el('span',label,'muted'),previous,el('span','page '+page,'muted'),next);parent.append(pager)}\nfunction inspectorNav(parent,targetId,label){const button=el('button',label);button.addEventListener('click',()=>{const target=document.getElementById(targetId);if(target)target.scrollIntoView({block:'start'})});parent.append(button)}\nasync function showAsset(id,capsulePage,eventPage){lineageId=id;lineageCapsulePage=capsulePage||1;lineageEventPage=eventPage||1;sel=null;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading asset lineage \u2026','muted'));try{const response=await j('/api/asset-lineage?id='+encodeURIComponent(id)+'&capsulePage='+lineageCapsulePage+'&eventPage='+lineageEventPage+'&pageSize=20');const relations=response.relations||{trajectories:response.trajectories,pullRequests:response.pullRequests};clear(root);root.append(el('h2','Asset lineage'));const nav=el('div',null,'section-nav');inspectorNav(nav,'asset-source','Source');inspectorNav(nav,'asset-review','Review');if(relationItems(relations.trajectories).length)inspectorNav(nav,'asset-trajectories','Trajectory');root.append(nav);\n sourceSection(root,'asset',response.asset,(section,data)=>{if(!data){section.append(el('div','asset not found','tag warn'));return;}appendLine(section,'type',data.type);appendLine(section,'id',data.id);appendLine(section,'asset',data.assetId);appendLine(section,'category',data.category);appendLine(section,'summary',data.summary)});\n sourceSection(root,'review',response.review,(section,data)=>{if(!data){section.append(el('div','default eligible / no record','lineage-empty'));return;}appendLine(section,'state',data.state);appendLine(section,'at',data.at);appendLine(section,'by',data.by);appendLine(section,'reason',data.reason)},'asset-review');\n sourceSection(root,'provenance / source',response.provenance,(section,data)=>{if(!data){section.append(el('div','local default / no record','lineage-empty'));return;}appendLine(section,'source',data.source);appendLine(section,'trusted',data.trusted);appendLine(section,'at',data.at);appendLine(section,'promoted by',data.promotedBy);appendLine(section,'reason',data.reason)},'asset-source');\n sourceSection(root,'capsules',response.capsules,(section,data)=>renderRows(section,data,[['type','type'],['id','id'],['asset','assetId'],['summary','summary']]));sourceSection(root,'events',response.events,(section,data)=>renderEventRows(section,data));renderOptionalRelations(root,relations,'asset');lineagePager(root,'capsules',lineageCapsulePage,response.capsules&&response.capsules.data,page=>showAsset(lineageId,page,lineageEventPage));lineagePager(root,'events',lineageEventPage,response.events&&response.events.data,page=>showAsset(lineageId,lineageCapsulePage,page));\n }catch{clear(root);root.append(el('span','asset lineage unavailable','tag warn'))}}\nconst workflowTag=status=>status==='succeeded'?'ok':(status==='failed'||status==='cancelled'||status==='unsafe_to_resume'||status==='resource_limit')?'fail':(status==='running'||status==='retry_wait'||status==='pause_requested'||status==='cancel_requested')?'warn':'muted';\nasync function refreshWorkflows(){const root=document.getElementById('workflows');try{const payload=await j('/api/workflows');const rows=asArray(payload&&payload.workflows);clear(root);for(const workflow of rows){const runId=String(workflow.runId||'');const row=el('div',null,'row workflowrow clickable');row.append(el('b',runId),el('span',workflow.status,'tag '+workflowTag(workflow.status)),el('span','workflow '+String(workflow.workflowId)+' \u00B7 step '+String(workflow.currentStep||'\u2014')+' \u00B7 '+String(workflow.updatedAt),'meta muted'));row.addEventListener('click',()=>void showWorkflow(runId));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0 workflow runs','muted'));if(selectedWorkflow&&rows.some(workflow=>workflow.runId===selectedWorkflow))await showWorkflow(selectedWorkflow)}catch{clear(root);root.append(el('span','workflow unavailable','tag warn'))}}\nasync function showWorkflow(id){selectedWorkflow=id;const root=document.getElementById('workflowhistory');clear(root);root.append(el('span','loading workflow \u2026','muted'));try{const result=await Promise.all([j('/api/workflow?id='+encodeURIComponent(id)),j('/api/workflow/history?id='+encodeURIComponent(id))]);const workflow=result[0]&&result[0].workflow;const history=asArray(result[1]&&result[1].history);clear(root);if(!workflow){root.append(el('span','workflow unavailable','tag warn'));return;}const summary=el('div');summary.append(el('b',workflow.runId),document.createTextNode(' '),el('span',workflow.status,'tag '+workflowTag(workflow.status)),el('div','workflow '+String(workflow.workflowId)+' \u00B7 current '+String(workflow.currentStep||'\u2014')+' \u00B7 created '+String(workflow.createdAt)+' \u00B7 updated '+String(workflow.updatedAt),'muted'));root.append(summary);for(const event of history){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.sequence)+' '),el('b',event.type));if(event.status)heading.append(document.createTextNode(' '),el('span',event.status,'tag '+workflowTag(event.status)));row.append(heading);const details=[String(event.timestamp)];if(event.stepId)details.push('step '+String(event.stepId));if(event.executionId)details.push('execution '+String(event.executionId));if(event.gateId)details.push('gate '+String(event.gateId));if(event.attempt!=null)details.push('attempt '+String(event.attempt));if(event.actorId)details.push('actor '+String(event.actorId));if(event.errorClass)details.push('error '+String(event.errorClass));row.append(el('div',details.join(' \u00B7 '),'muted'));root.append(row)}if(!history.length)root.append(el('div','\u6682\u65E0 durable operation history','muted'))}catch{clear(root);root.append(el('span','workflow unavailable','tag warn'))}}\nasync function showCycle(id){sel=id;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading cycle \u2026','muted'));try{const response=await j('/api/cycle?id='+encodeURIComponent(id));const relations=response.relations;clear(root);root.append(el('h2','Cycle '+String(id)));if(relationItems(relations&&relations.trajectories).length){const nav=el('div',null,'section-nav');inspectorNav(nav,'cycle-trajectories','Trajectory');root.append(nav)}renderOptionalRelations(root,relations,'cycle');for(const event of asArray(response.timeline)){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.seq)+' '),el('b',event.type));row.append(heading);if(event.title)row.append(el('div',event.title));if(event.why)appendLine(row,'why',event.why);if(event.payload!=null){const pre=el('pre');pre.append(document.createTextNode(safeJson(event.payload)));row.append(pre)}root.append(row)}if(!asArray(response.timeline).length)root.append(el('div','no timeline events','muted'))}catch{clear(root);root.append(el('span','cycle unavailable','tag warn'))}}\nasync function reviewAct(gene,action){const reason=prompt(action+' \u7406\u7531 reason:');if(reason===null)return;try{const response=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});if(!response||response.ok!==true){alert('review \u5931\u8D25 failed: '+((response&&response.error)||'unknown'));return;}}catch(error){alert('review \u5931\u8D25 failed: '+error);return;}refresh()}\nasync function act(kind){const note=prompt(kind+' \u5907\u6CE8:');if(note===null)return;await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh()}\nfor(const kind of ['observe','nudge','intervene','teach'])document.getElementById('act-'+kind).addEventListener('click',()=>void act(kind));\nrefresh();void refreshRetention();void refreshWorkflows();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();\nsetInterval(refresh,4000);setInterval(refreshWorkflows,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);\n</script></body></html>";
package/dist/console.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** 3 栏主控台 HTML(自包含, 无构建步; 轮询 /api/* 渲染). 军杰 §9.5: Triggers|Timeline|Inspector. */
2
2
  export const CONSOLE_HTML = `<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>Evolver 控台</title>
3
3
  <style>
4
- *{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(320px,1fr);overflow:auto}
4
+ *{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(220px,auto) minmax(320px,1fr);overflow:auto}
5
5
  header{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}
6
6
  header b{color:#58a6ff}.muted{color:#8b949e}
7
7
  .cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;min-height:320px}
@@ -17,7 +17,10 @@ a{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#
17
17
  #reviewqueue{max-height:156px;overflow:auto;align-items:flex-start}
18
18
  #assetbar{max-height:190px;overflow:auto;align-items:flex-start}.assetgrid{display:flex;gap:6px;flex-wrap:wrap}.assetbutton{text-align:left;max-width:360px}.pager{display:flex;gap:6px;align-items:center}.lineage-section{margin-top:12px;scroll-margin-top:8px}.lineage-section h3{font-size:11px;color:#8b949e;text-transform:uppercase}.lineage-empty{color:#8b949e}
19
19
  .observability{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));background:#0d1117;border-bottom:1px solid #30363d}.surface{padding:10px 14px;min-height:150px;border-right:1px solid #30363d;border-top:1px solid #30363d;overflow:auto}.surface-head{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}.surface-body{max-height:260px;overflow:auto}.surface h3{margin:0;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px}.metric{padding:6px;background:#161b22;border-radius:6px}.metric b{display:block;color:#8b949e;font-size:11px}.logview{max-height:220px;overflow:auto;margin:0}.relation-list{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.relation-card{padding:6px 8px;border:1px solid #30363d;border-radius:6px;min-width:180px}.section-nav{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}
20
- @media(max-width:760px){body{display:block;overflow:auto}.cols,.observability{display:block}.col{border-right:0;border-bottom:1px solid #30363d;min-height:240px}#reviewqueue{max-height:none}.surface{border-right:0}}
20
+ .workflowcard{display:grid;grid-template-columns:minmax(320px,.9fr) minmax(360px,1.4fr);gap:0;background:#11161d;border-bottom:1px solid #30363d;min-height:220px}
21
+ .workflowpane{padding:10px 14px;overflow:auto;max-height:360px}.workflowpane:first-child{border-right:1px solid #30363d}
22
+ .workflowrow{display:grid;grid-template-columns:minmax(120px,1fr) auto;gap:4px 10px}.workflowrow .meta{grid-column:1/-1}
23
+ @media(max-width:760px){body{display:block;overflow:auto}.cols,.observability,.workflowcard{display:block}.col,.workflowpane{border-right:0;border-bottom:1px solid #30363d;min-height:240px;max-height:none}#reviewqueue{max-height:none}.surface{border-right:0}}
21
24
  </style></head><body>
22
25
  <header><b>EVOLVER</b> <span id="stat" class="muted">…</span><span id="daily" class="muted"></span></header>
23
26
  <div class="valuecard" id="valuecard"><span class="muted">价值 / value …</span></div>
@@ -29,9 +32,13 @@ a{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#
29
32
  <section class="surface"><div class="surface-head"><h3>Personality</h3><span id="personality-status" class="muted">…</span></div><div id="personality" class="surface-body"></div></section>
30
33
  <section class="surface"><div class="surface-head"><h3>MemoryGraph</h3><span id="memorygraph-status" class="muted">…</span></div><div id="memorygraph" class="surface-body"></div></section>
31
34
  <section class="surface"><div class="surface-head"><h3>Logs</h3><span id="logs-status" class="muted">…</span></div><div id="logs" class="surface-body"></div></section>
32
- <section class="surface"><div class="surface-head"><h3>GitHub PR status</h3><span id="githubprs-status" class="muted">…</span></div><div id="githubprs" class="surface-body"></div></section>
35
+ <section class="surface"><div class="surface-head"><h3>GitHub PR status</h3><span id="githubprs-status" class="muted">…</span></div><div id="githubprs" class="surface-body"></div></section>
33
36
  </div>
34
37
  </div>
38
+ <div class="workflowcard">
39
+ <section class="workflowpane"><h2>Workflows</h2><div id="workflows" class="muted">workflow runs …</div></section>
40
+ <section class="workflowpane"><h2>Workflow History</h2><div id="workflowhistory" class="muted">选一个 workflow run</div></section>
41
+ </div>
35
42
  <div class="cols">
36
43
  <div class="col"><h2>Triggers</h2><div id="triggers"></div></div>
37
44
  <div class="col"><h2>Now &amp; Timeline</h2><div id="cycles"></div></div>
@@ -40,7 +47,7 @@ a{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#
40
47
  </div>
41
48
  </div>
42
49
  <script>
43
- let sel=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';
50
+ let sel=null,selectedWorkflow=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';
44
51
  const FRAG=new URLSearchParams(location.hash.slice(1));
45
52
  const QUERY=new URLSearchParams(location.search);
46
53
  let STORED='';try{STORED=sessionStorage.getItem('evolver.dashboard.token')||''}catch{}
@@ -117,10 +124,13 @@ async function showAsset(id,capsulePage,eventPage){lineageId=id;lineageCapsulePa
117
124
  sourceSection(root,'provenance / source',response.provenance,(section,data)=>{if(!data){section.append(el('div','local default / no record','lineage-empty'));return;}appendLine(section,'source',data.source);appendLine(section,'trusted',data.trusted);appendLine(section,'at',data.at);appendLine(section,'promoted by',data.promotedBy);appendLine(section,'reason',data.reason)},'asset-source');
118
125
  sourceSection(root,'capsules',response.capsules,(section,data)=>renderRows(section,data,[['type','type'],['id','id'],['asset','assetId'],['summary','summary']]));sourceSection(root,'events',response.events,(section,data)=>renderEventRows(section,data));renderOptionalRelations(root,relations,'asset');lineagePager(root,'capsules',lineageCapsulePage,response.capsules&&response.capsules.data,page=>showAsset(lineageId,page,lineageEventPage));lineagePager(root,'events',lineageEventPage,response.events&&response.events.data,page=>showAsset(lineageId,lineageCapsulePage,page));
119
126
  }catch{clear(root);root.append(el('span','asset lineage unavailable','tag warn'))}}
127
+ const workflowTag=status=>status==='succeeded'?'ok':(status==='failed'||status==='cancelled'||status==='unsafe_to_resume'||status==='resource_limit')?'fail':(status==='running'||status==='retry_wait'||status==='pause_requested'||status==='cancel_requested')?'warn':'muted';
128
+ async function refreshWorkflows(){const root=document.getElementById('workflows');try{const payload=await j('/api/workflows');const rows=asArray(payload&&payload.workflows);clear(root);for(const workflow of rows){const runId=String(workflow.runId||'');const row=el('div',null,'row workflowrow clickable');row.append(el('b',runId),el('span',workflow.status,'tag '+workflowTag(workflow.status)),el('span','workflow '+String(workflow.workflowId)+' · step '+String(workflow.currentStep||'—')+' · '+String(workflow.updatedAt),'meta muted'));row.addEventListener('click',()=>void showWorkflow(runId));root.append(row)}if(!root.firstChild)root.append(el('div','无 workflow runs','muted'));if(selectedWorkflow&&rows.some(workflow=>workflow.runId===selectedWorkflow))await showWorkflow(selectedWorkflow)}catch{clear(root);root.append(el('span','workflow unavailable','tag warn'))}}
129
+ async function showWorkflow(id){selectedWorkflow=id;const root=document.getElementById('workflowhistory');clear(root);root.append(el('span','loading workflow …','muted'));try{const result=await Promise.all([j('/api/workflow?id='+encodeURIComponent(id)),j('/api/workflow/history?id='+encodeURIComponent(id))]);const workflow=result[0]&&result[0].workflow;const history=asArray(result[1]&&result[1].history);clear(root);if(!workflow){root.append(el('span','workflow unavailable','tag warn'));return;}const summary=el('div');summary.append(el('b',workflow.runId),document.createTextNode(' '),el('span',workflow.status,'tag '+workflowTag(workflow.status)),el('div','workflow '+String(workflow.workflowId)+' · current '+String(workflow.currentStep||'—')+' · created '+String(workflow.createdAt)+' · updated '+String(workflow.updatedAt),'muted'));root.append(summary);for(const event of history){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.sequence)+' '),el('b',event.type));if(event.status)heading.append(document.createTextNode(' '),el('span',event.status,'tag '+workflowTag(event.status)));row.append(heading);const details=[String(event.timestamp)];if(event.stepId)details.push('step '+String(event.stepId));if(event.executionId)details.push('execution '+String(event.executionId));if(event.gateId)details.push('gate '+String(event.gateId));if(event.attempt!=null)details.push('attempt '+String(event.attempt));if(event.actorId)details.push('actor '+String(event.actorId));if(event.errorClass)details.push('error '+String(event.errorClass));row.append(el('div',details.join(' · '),'muted'));root.append(row)}if(!history.length)root.append(el('div','暂无 durable operation history','muted'))}catch{clear(root);root.append(el('span','workflow unavailable','tag warn'))}}
120
130
  async function showCycle(id){sel=id;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading cycle …','muted'));try{const response=await j('/api/cycle?id='+encodeURIComponent(id));const relations=response.relations;clear(root);root.append(el('h2','Cycle '+String(id)));if(relationItems(relations&&relations.trajectories).length){const nav=el('div',null,'section-nav');inspectorNav(nav,'cycle-trajectories','Trajectory');root.append(nav)}renderOptionalRelations(root,relations,'cycle');for(const event of asArray(response.timeline)){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.seq)+' '),el('b',event.type));row.append(heading);if(event.title)row.append(el('div',event.title));if(event.why)appendLine(row,'why',event.why);if(event.payload!=null){const pre=el('pre');pre.append(document.createTextNode(safeJson(event.payload)));row.append(pre)}root.append(row)}if(!asArray(response.timeline).length)root.append(el('div','no timeline events','muted'))}catch{clear(root);root.append(el('span','cycle unavailable','tag warn'))}}
121
131
  async function reviewAct(gene,action){const reason=prompt(action+' 理由 reason:');if(reason===null)return;try{const response=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});if(!response||response.ok!==true){alert('review 失败 failed: '+((response&&response.error)||'unknown'));return;}}catch(error){alert('review 失败 failed: '+error);return;}refresh()}
122
132
  async function act(kind){const note=prompt(kind+' 备注:');if(note===null)return;await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh()}
123
133
  for(const kind of ['observe','nudge','intervene','teach'])document.getElementById('act-'+kind).addEventListener('click',()=>void act(kind));
124
- refresh();void refreshRetention();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();
125
- setInterval(refresh,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);
134
+ refresh();void refreshRetention();void refreshWorkflows();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();
135
+ setInterval(refresh,4000);setInterval(refreshWorkflows,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);
126
136
  </script></body></html>`;
package/dist/server.d.ts CHANGED
@@ -16,6 +16,34 @@ export type MemoryGraphStatusResponse = ({
16
16
  available: false;
17
17
  error?: 'memory_graph_unavailable';
18
18
  };
19
+ type MaybePromise<T> = T | Promise<T>;
20
+ export interface WorkflowRunSummary {
21
+ runId: string;
22
+ workflowId: string;
23
+ status: string;
24
+ currentStep: string | null;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ completedAt: string | null;
28
+ }
29
+ export interface WorkflowHistoryEntry {
30
+ sequence: number;
31
+ timestamp: string;
32
+ type: string;
33
+ status?: string | null;
34
+ stepId?: string | null;
35
+ executionId?: string | null;
36
+ gateId?: string | null;
37
+ attempt?: number | null;
38
+ actorId?: string | null;
39
+ errorClass?: string | null;
40
+ }
41
+ /** Operator-safe workflow projection. Filesystem paths and raw durable state remain owned by the composition layer. */
42
+ export interface WorkflowProvider {
43
+ listRuns(): MaybePromise<readonly WorkflowRunSummary[]>;
44
+ getRun(runId: string): MaybePromise<WorkflowRunSummary | null>;
45
+ getHistory(runId: string): MaybePromise<readonly WorkflowHistoryEntry[] | null>;
46
+ }
19
47
  export interface WebUIServerDeps {
20
48
  eventsPath: string;
21
49
  ingestor?: ev.Ingestor;
@@ -48,6 +76,8 @@ export interface WebUIServerDeps {
48
76
  personalityDiagnostics?: () => unknown | Promise<unknown>;
49
77
  logDiagnostics?: () => unknown | Promise<unknown>;
50
78
  githubPrDiagnostics?: () => unknown | Promise<unknown>;
79
+ /** Durable workflow visibility provider. Only safe summaries/history metadata may cross this boundary. */
80
+ workflow?: WorkflowProvider;
51
81
  }
52
82
  /**
53
83
  * WebUI 控台(M7): 可观测 + 保活. node:http + 自包含 HTML, 仅绑 loopback.
@@ -75,5 +105,8 @@ export declare class WebUIServer {
75
105
  private handle;
76
106
  private readJson;
77
107
  private json;
108
+ private apiError;
109
+ private methodNotAllowed;
78
110
  private send;
79
- }
111
+ }
112
+ export {};
package/dist/server.js CHANGED
@@ -73,6 +73,52 @@ function requireGet(req, res) {
73
73
  res.end(JSON.stringify({ error: 'method_not_allowed' }));
74
74
  return false;
75
75
  }
76
+ const STABLE_WORKFLOW_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
77
+ function boundedString(value, maxLength = 256) {
78
+ return typeof value === 'string' ? value.slice(0, maxLength) : '';
79
+ }
80
+ function safeRunSummary(value) {
81
+ const runId = boundedString(value.runId, 128);
82
+ const workflowId = boundedString(value.workflowId, 128);
83
+ if (!STABLE_WORKFLOW_ID.test(runId) || !STABLE_WORKFLOW_ID.test(workflowId))
84
+ return null;
85
+ const currentStep = value.currentStep === null ? null : boundedString(value.currentStep);
86
+ const completedAt = value.completedAt === null ? null : boundedString(value.completedAt, 64);
87
+ return {
88
+ runId,
89
+ workflowId,
90
+ status: boundedString(value.status, 64),
91
+ currentStep,
92
+ createdAt: boundedString(value.createdAt, 64),
93
+ updatedAt: boundedString(value.updatedAt, 64),
94
+ completedAt,
95
+ };
96
+ }
97
+ function safeHistoryEntry(value) {
98
+ if (!Number.isSafeInteger(value.sequence) || value.sequence < 0)
99
+ return null;
100
+ const optional = (candidate, maxLength = 256) => {
101
+ if (candidate === undefined)
102
+ return undefined;
103
+ if (candidate === null)
104
+ return null;
105
+ return boundedString(candidate, maxLength);
106
+ };
107
+ return {
108
+ sequence: value.sequence,
109
+ timestamp: boundedString(value.timestamp, 64),
110
+ type: boundedString(value.type, 128),
111
+ ...(value.status !== undefined ? { status: optional(value.status, 64) } : {}),
112
+ ...(value.stepId !== undefined ? { stepId: optional(value.stepId) } : {}),
113
+ ...(value.executionId !== undefined ? { executionId: optional(value.executionId) } : {}),
114
+ ...(value.gateId !== undefined ? { gateId: optional(value.gateId) } : {}),
115
+ ...(value.attempt !== undefined && (value.attempt === null || (Number.isSafeInteger(value.attempt) && value.attempt >= 0))
116
+ ? { attempt: value.attempt }
117
+ : {}),
118
+ ...(value.actorId !== undefined ? { actorId: optional(value.actorId, 128) } : {}),
119
+ ...(value.errorClass !== undefined ? { errorClass: optional(value.errorClass, 64) } : {}),
120
+ };
121
+ }
76
122
  const HUMAN_ACTIONS = {
77
123
  observe: 'actor.human.observe', nudge: 'actor.human.nudge', intervene: 'actor.human.intervene', teach: 'actor.human.teach',
78
124
  };
@@ -271,6 +317,47 @@ export class WebUIServer {
271
317
  }));
272
318
  }
273
319
  }
320
+ if (p === '/api/workflows') {
321
+ if (req.method !== 'GET')
322
+ return this.methodNotAllowed(res, 'GET');
323
+ if (!this.deps.workflow)
324
+ return this.json(res, { workflows: [] });
325
+ try {
326
+ const workflows = (await this.deps.workflow.listRuns())
327
+ .map(safeRunSummary)
328
+ .filter((run) => run !== null);
329
+ return this.json(res, { workflows });
330
+ }
331
+ catch {
332
+ return this.apiError(res, 503, 'workflow_unavailable');
333
+ }
334
+ }
335
+ if (p === '/api/workflow' || p === '/api/workflow/history') {
336
+ if (req.method !== 'GET')
337
+ return this.methodNotAllowed(res, 'GET');
338
+ const runId = url.searchParams.get('id') ?? '';
339
+ if (!STABLE_WORKFLOW_ID.test(runId))
340
+ return this.apiError(res, 400, 'invalid_workflow_id');
341
+ if (!this.deps.workflow)
342
+ return this.apiError(res, 503, 'workflow_unavailable');
343
+ try {
344
+ if (p === '/api/workflow') {
345
+ const candidate = await this.deps.workflow.getRun(runId);
346
+ const run = candidate === null ? null : safeRunSummary(candidate);
347
+ return run ? this.json(res, { workflow: run }) : this.apiError(res, 404, 'workflow_not_found');
348
+ }
349
+ const history = await this.deps.workflow.getHistory(runId);
350
+ if (history === null)
351
+ return this.apiError(res, 404, 'workflow_not_found');
352
+ return this.json(res, {
353
+ runId,
354
+ history: history.map(safeHistoryEntry).filter((entry) => entry !== null),
355
+ });
356
+ }
357
+ catch {
358
+ return this.apiError(res, 503, 'workflow_unavailable');
359
+ }
360
+ }
274
361
  if (p === '/api/mailbox') {
275
362
  if (!this.deps.mailbox)
276
363
  return this.json(res, { pending: 0, dlq: 0, messages: [] });
@@ -407,5 +494,10 @@ export class WebUIServer {
407
494
  });
408
495
  }
409
496
  json(res, body) { this.send(res, 200, 'application/json', JSON.stringify(body)); }
497
+ apiError(res, code, error) { this.send(res, code, 'application/json', JSON.stringify({ error })); }
498
+ methodNotAllowed(res, allow) {
499
+ res.writeHead(405, { allow, 'content-type': 'application/json' });
500
+ res.end(JSON.stringify({ error: 'method_not_allowed' }));
501
+ }
410
502
  send(res, code, ct, body) { res.writeHead(code, { 'content-type': ct }); res.end(body); }
411
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-webui",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0-beta.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "保活/可视化 WebUI",
@@ -13,7 +13,7 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@evomap/evolver-core": "2.0.0-beta.6"
16
+ "@evomap/evolver-core": "2.0.0-beta.8"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",