// Cursor2API Log Viewer v4 - Client JS // ===== Theme Toggle ===== function getTheme(){return document.documentElement.getAttribute('data-theme')||'light'} function applyThemeIcon(){const btn=document.getElementById('themeToggle');if(btn)btn.textContent=getTheme()==='dark'?'☀️':'🌙'} function toggleTheme(){const t=getTheme()==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',t);localStorage.setItem('cursor2api_theme',t);applyThemeIcon()} applyThemeIcon(); let reqs=[],rmap={},logs=[],selId=null,cFil='all',cLv='all',sq='',curTab='logs',curPayload=null,timeFil='all'; const PC={receive:'var(--blue)',convert:'var(--cyan)',send:'var(--purple)',response:'var(--purple)',thinking:'#a855f7',refusal:'var(--yellow)',retry:'var(--yellow)',truncation:'var(--yellow)',continuation:'var(--yellow)',toolparse:'var(--orange)',sanitize:'var(--orange)',stream:'var(--green)',complete:'var(--green)',error:'var(--red)',intercept:'var(--pink)',auth:'var(--t3)'}; // ===== Token Auth ===== const urlToken = new URLSearchParams(window.location.search).get('token'); if (urlToken) localStorage.setItem('cursor2api_token', urlToken); const authToken = localStorage.getItem('cursor2api_token') || ''; function authQ(base) { return authToken ? (base.includes('?') ? base + '&token=' : base + '?token=') + encodeURIComponent(authToken) : base; } function logoutBtn() { if (authToken) { const b = document.createElement('button'); b.textContent = '退出'; b.className = 'hdr-btn'; b.onclick = () => { localStorage.removeItem('cursor2api_token'); window.location.href = '/logs'; }; document.querySelector('.hdr-r').prepend(b); } } // ===== Init ===== async function init(){ try{ const[a,b]=await Promise.all([fetch(authQ('/api/requests?limit=100')),fetch(authQ('/api/logs?limit=500'))]); if (a.status === 401) { localStorage.removeItem('cursor2api_token'); window.location.href = '/logs'; return; } reqs=await a.json();logs=await b.json();rmap={};reqs.forEach(r=>rmap[r.requestId]=r); renderRL();updCnt();updStats(); // 默认显示实时日志流 renderLogs(logs.slice(-200)); }catch(e){console.error(e)} connectSSE(); logoutBtn(); } // ===== SSE ===== let es; function connectSSE(){ if(es)try{es.close()}catch{} es=new EventSource(authQ('/api/logs/stream')); es.addEventListener('log',e=>{ const en=JSON.parse(e.data);logs.push(en); if(logs.length>5000)logs=logs.slice(-3000); if(!selId||selId===en.requestId){if(curTab==='logs')appendLog(en)} }); es.addEventListener('summary',e=>{ const s=JSON.parse(e.data);rmap[s.requestId]=s; const i=reqs.findIndex(r=>r.requestId===s.requestId); if(i>=0)reqs[i]=s;else reqs.unshift(s); renderRL();updCnt(); if(selId===s.requestId)renderSCard(s); }); es.addEventListener('stats',e=>{applyStats(JSON.parse(e.data))}); es.onopen=()=>{const c=document.getElementById('conn');c.className='conn on';c.querySelector('span').textContent='已连接'}; es.onerror=()=>{const c=document.getElementById('conn');c.className='conn off';c.querySelector('span').textContent='重连中...';setTimeout(connectSSE,3000)}; } // ===== Stats ===== function updStats(){fetch(authQ('/api/stats')).then(r=>r.json()).then(applyStats).catch(()=>{})} function applyStats(s){document.getElementById('sT').textContent=s.totalRequests;document.getElementById('sS').textContent=s.successCount;document.getElementById('sE').textContent=s.errorCount;document.getElementById('sA').textContent=s.avgResponseTime||'-';document.getElementById('sF').textContent=s.avgTTFT||'-'} // ===== Time Filter ===== function getTimeCutoff(){ if(timeFil==='all')return 0; const now=Date.now(); const map={today:now-now%(86400000)+new Date().getTimezoneOffset()*-60000,'2d':now-2*86400000,'7d':now-7*86400000,'30d':now-30*86400000}; if(timeFil==='today'){const d=new Date();d.setHours(0,0,0,0);return d.getTime()} return map[timeFil]||0; } function setTF(f,btn){timeFil=f;document.querySelectorAll('#tbar .tb').forEach(b=>b.classList.remove('a'));btn.classList.add('a');renderRL();updCnt()} // ===== Search & Filter ===== function mS(r,q){ const s=q.toLowerCase(); return r.requestId.includes(s)||r.model.toLowerCase().includes(s)||r.path.toLowerCase().includes(s)||(r.title||'').toLowerCase().includes(s); } function updCnt(){ const q=sq.toLowerCase();const cut=getTimeCutoff(); let a=0,s=0,e=0,p=0,i=0; reqs.forEach(r=>{ if(cut&&r.startTimeb.classList.remove('a'));btn.classList.add('a');renderRL()} // ===== Format helpers ===== function fmtDate(ts){const d=new Date(ts);return (d.getMonth()+1)+'/'+d.getDate()+' '+d.toLocaleTimeString('zh-CN',{hour12:false})} function timeAgo(ts){const s=Math.floor((Date.now()-ts)/1000);if(s<5)return'刚刚';if(s<60)return s+'s前';if(s<3600)return Math.floor(s/60)+'m前';return Math.floor(s/3600)+'h前'} function fmtN(n){if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n)} function escH(s){if(!s)return'';const d=document.createElement('div');d.textContent=String(s);return d.innerHTML} function syntaxHL(data){ try{const s=typeof data==='string'?data:JSON.stringify(data,null,2); return s.replace(/&/g,'&').replace(//g,'>') .replace(/"([^"]+)"\s*:/g,'"$1":') .replace(/:\s*"([^"]*?)"/g,': "$1"') .replace(/:\s*(\d+\.?\d*)/g,': $1') .replace(/:\s*(true|false)/g,': $1') .replace(/:\s*(null)/g,': null') }catch{return escH(String(data))} } function copyText(text){navigator.clipboard.writeText(text).then(()=>{}).catch(()=>{})} // ===== Request List ===== function renderRL(){ const el=document.getElementById('rlist');const q=sq.toLowerCase();const cut=getTimeCutoff(); let f=reqs; if(cut)f=f.filter(r=>r.startTime>=cut); if(q)f=f.filter(r=>mS(r,q)); if(cFil!=='all')f=f.filter(r=>r.status===cFil); if(!f.length){el.innerHTML='
📡

'+(q?'无匹配':'暂无请求')+'

';return} el.innerHTML=f.map(r=>{ const ac=r.requestId===selId; const dur=r.endTime?((r.endTime-r.startTime)/1000).toFixed(1)+'s':'...'; const durMs=r.endTime?r.endTime-r.startTime:Date.now()-r.startTime; const pct=Math.min(100,durMs/30000*100),dc=!r.endTime?'pr':durMs<3000?'f':durMs<10000?'m':durMs<20000?'s':'vs'; const ch=r.responseChars>0?fmtN(r.responseChars)+' chars':''; const tt=r.ttft?r.ttft+'ms':''; const title=r.title||r.model; const dateStr=fmtDate(r.startTime); let bd='';if(r.stream)bd+='Stream';if(r.hasTools)bd+='T:'+r.toolCount+''; if(r.retryCount>0)bd+='R:'+r.retryCount+'';if(r.continuationCount>0)bd+='C:'+r.continuationCount+''; if(r.status==='error')bd+='ERR';if(r.status==='intercepted')bd+='INTERCEPT'; const fm=r.apiFormat||'anthropic'; return '
' +'
' +'
'+escH(title)+'
' +'
'+dateStr+' · '+dur+(tt?' · ⚡'+tt:'')+'
' +'
'+r.requestId+' '+fm+'' +(ch?'→ '+ch+'':'')+'
' +'
'+bd+'
' +'
'; }).join(''); } // ===== Select Request ===== async function selReq(id){ if(selId===id){desel();return} selId=id;renderRL(); const s=rmap[id]; if(s){document.getElementById('dTitle').textContent=s.title||'请求 '+id;renderSCard(s)} document.getElementById('tabs').style.display='flex'; // ★ 保持当前 tab(不重置为 logs) const tabEl=document.querySelector('.tab[data-tab="'+curTab+'"]'); if(tabEl){setTab(curTab,tabEl)}else{setTab('logs',document.querySelector('.tab'))} // Load payload try{const r=await fetch(authQ('/api/payload/'+id));if(r.ok)curPayload=await r.json();else curPayload=null}catch{curPayload=null} // Re-render current tab with new data const tabEl2=document.querySelector('.tab[data-tab="'+curTab+'"]'); if(tabEl2)setTab(curTab,tabEl2); } function desel(){ selId=null;curPayload=null;renderRL(); document.getElementById('dTitle').textContent='实时日志流'; document.getElementById('scard').style.display='none'; document.getElementById('ptl').style.display='none'; document.getElementById('tabs').style.display='none'; curTab='logs'; renderLogs(logs.slice(-200)); } function renderSCard(s){ const c=document.getElementById('scard');c.style.display='block'; const dur=s.endTime?((s.endTime-s.startTime)/1000).toFixed(2)+'s':'进行中...'; const sc={processing:'var(--yellow)',success:'var(--green)',error:'var(--red)',intercepted:'var(--pink)'}[s.status]||'var(--t3)'; const items=[['状态',''+s.status.toUpperCase()+''],['耗时',dur],['模型',escH(s.model)],['格式',(s.apiFormat||'anthropic').toUpperCase()],['消息数',s.messageCount],['响应字数',fmtN(s.responseChars)],['TTFT',s.ttft?s.ttft+'ms':'-'],['API耗时',s.cursorApiTime?s.cursorApiTime+'ms':'-'],['停止原因',s.stopReason||'-'],['重试',s.retryCount],['续写',s.continuationCount],['工具调用',s.toolCallsDetected]]; if(s.thinkingChars>0)items.push(['Thinking',fmtN(s.thinkingChars)+' chars']); if(s.error)items.push(['错误',''+escH(s.error)+'']); document.getElementById('sgrid').innerHTML=items.map(([l,v])=>'
'+l+''+v+'
').join(''); renderPTL(s); } function renderPTL(s){ const el=document.getElementById('ptl'),bar=document.getElementById('pbar'); if(!s.phaseTimings||!s.phaseTimings.length){el.style.display='none';return} el.style.display='block';const tot=(s.endTime||Date.now())-s.startTime;if(tot<=0){el.style.display='none';return} bar.innerHTML=s.phaseTimings.map(pt=>{const d=pt.duration||((pt.endTime||Date.now())-pt.startTime);const pct=Math.max(1,d/tot*100);const bg=PC[pt.phase]||'var(--t3)';return '
'+escH(pt.label)+' '+d+'ms'+(pct>10?''+pt.phase+'':'')+'
'}).join(''); } // ===== Tabs ===== function setTab(tab,el){ curTab=tab; document.querySelectorAll('.tab').forEach(t=>t.classList.remove('a')); el.classList.add('a'); const tc=document.getElementById('tabContent'); if(tab==='logs'){ tc.innerHTML='
'; if(selId){renderLogs(logs.filter(l=>l.requestId===selId))}else{renderLogs(logs.slice(-200))} } else if(tab==='request'){ renderRequestTab(tc); } else if(tab==='prompts'){ renderPromptsTab(tc); } else if(tab==='response'){ renderResponseTab(tc); } } function renderRequestTab(tc){ if(!curPayload){tc.innerHTML='
📥

暂无请求数据

';return} let h=''; const s=selId?rmap[selId]:null; if(s){ h+='
📋 请求概要
'; h+='
'+syntaxHL({method:s.method,path:s.path,model:s.model,stream:s.stream,apiFormat:s.apiFormat,messageCount:s.messageCount,toolCount:s.toolCount,hasTools:s.hasTools})+'
'; } if(curPayload.tools&&curPayload.tools.length){ h+='
🔧 工具定义 '+curPayload.tools.length+' 个
'; curPayload.tools.forEach(t=>{h+='
'+escH(t.name)+'
'+(t.description?'
'+escH(t.description)+'
':'')+'
'}); h+='
'; } if(curPayload.cursorRequest){ h+='
🔄 Cursor 请求(转换后)
'; h+='
'+syntaxHL(curPayload.cursorRequest)+'
'; } if(curPayload.cursorMessages&&curPayload.cursorMessages.length){ h+='
📨 Cursor 消息列表 '+curPayload.cursorMessages.length+' 条
'; curPayload.cursorMessages.forEach((m,i)=>{ const collapsed=m.contentPreview.length>500; h+='
'+m.role+' #'+(i+1)+''+fmtN(m.contentLength)+' chars '+(collapsed?'▶ 展开':'▼ 收起')+'
'+escH(m.contentPreview)+'
'; }); h+='
'; } tc.innerHTML=h||'
📥

暂无请求数据

'; } function renderPromptsTab(tc){ if(!curPayload){tc.innerHTML='
💬

暂无提示词数据

';return} let h=''; const s=selId?rmap[selId]:null; // ===== 转换摘要 ===== if(s){ const origMsgCount=curPayload.messages?curPayload.messages.length:0; const cursorMsgCount=curPayload.cursorMessages?curPayload.cursorMessages.length:0; const origToolCount=s.toolCount||0; const sysPLen=curPayload.systemPrompt?curPayload.systemPrompt.length:0; const cursorTotalChars=curPayload.cursorRequest?.totalChars||0; // 计算工具指令占用的字符数(第一条 cursor 消息 减去 原始第一条用户消息) const firstCursorMsg=curPayload.cursorMessages?.[0]; const firstOrigUser=curPayload.messages?.find(m=>m.role==='user'); const toolInstructionChars=firstCursorMsg&&firstOrigUser?Math.max(0,firstCursorMsg.contentLength-(firstOrigUser?.contentLength||0)):0; h+='
🔄 转换摘要
'; h+='
'; h+='
原始工具数'+origToolCount+'
'; h+='
Cursor 工具数0 (嵌入消息)
'; h+='
工具指令占用'+(toolInstructionChars>0?fmtN(toolInstructionChars)+' chars':origToolCount>0?'嵌入第1条消息':'N/A')+'
'; h+='
原始消息数'+origMsgCount+'
'; h+='
Cursor 消息数'+cursorMsgCount+'
'; h+='
总上下文大小'+(cursorTotalChars>0?fmtN(cursorTotalChars)+' chars':'—')+'
'; h+='
'; if(origToolCount>0){ h+='
⚠️ Cursor API 不支持原生 tools 参数。'+origToolCount+' 个工具定义已转换为文本指令,嵌入在 user #1 消息中'+(toolInstructionChars>0?'(约 '+fmtN(toolInstructionChars)+' chars)':'')+'
'; } h+='
'; } // ===== 原始请求 ===== h+='
📥 客户端原始请求
'; if(curPayload.question){ h+='
❓ 用户问题摘要 '+fmtN(curPayload.question.length)+' chars
'; h+='
'+escH(curPayload.question)+'
'; } if(curPayload.systemPrompt){ h+='
🔒 原始 System Prompt '+fmtN(curPayload.systemPrompt.length)+' chars
'; h+='
'+escH(curPayload.systemPrompt)+'
'; } if(curPayload.messages&&curPayload.messages.length){ h+='
💬 原始消息列表 '+curPayload.messages.length+' 条
'; curPayload.messages.forEach((m,i)=>{ const imgs=m.hasImages?' 🖼️':''; const collapsed=m.contentPreview.length>500; h+='
'+m.role+imgs+' #'+(i+1)+''+fmtN(m.contentLength)+' chars '+(collapsed?'▶ 展开':'▼ 收起')+'
'+escH(m.contentPreview)+'
'; }); h+='
'; } // ===== 转换后 Cursor 请求 ===== if(curPayload.cursorMessages&&curPayload.cursorMessages.length){ h+='
📤 Cursor 最终消息(转换后) '+curPayload.cursorMessages.length+' 条
'; h+='
⬇️ 以下是清洗后实际发给 Cursor 模型的消息(已清除身份声明、注入工具指令、添加认知重构)
'; curPayload.cursorMessages.forEach((m,i)=>{ const collapsed=m.contentPreview.length>500; h+='
'+m.role+' #'+(i+1)+''+fmtN(m.contentLength)+' chars '+(collapsed?'▶ 展开':'▼ 收起')+'
'+escH(m.contentPreview)+'
'; }); h+='
'; } else if(curPayload.cursorRequest) { h+='
📤 Cursor 最终请求(转换后)
'; h+='
'+syntaxHL(curPayload.cursorRequest)+'
'; } tc.innerHTML=h||'
💬

暂无提示词数据

'; } function renderResponseTab(tc){ if(!curPayload){tc.innerHTML='
📤

暂无响应数据

';return} let h=''; if(curPayload.answer){ const title=curPayload.answerType==='tool_calls'?'✅ 最终结果(工具调用摘要)':'✅ 最终回答摘要'; h+='
'+title+' '+fmtN(curPayload.answer.length)+' chars
'; h+='
'+escH(curPayload.answer)+'
'; } if(curPayload.toolCallNames&&curPayload.toolCallNames.length&&!curPayload.toolCalls){ h+='
🔧 工具调用名称 '+curPayload.toolCallNames.length+' 个
'; h+='
'+escH(curPayload.toolCallNames.join(', '))+'
'; } if(curPayload.thinkingContent){ h+='
🧠 Thinking 内容 '+fmtN(curPayload.thinkingContent.length)+' chars
'; h+='
'+escH(curPayload.thinkingContent)+'
'; } if(curPayload.rawResponse){ h+='
📝 模型原始返回 '+fmtN(curPayload.rawResponse.length)+' chars
'; h+='
'+escH(curPayload.rawResponse)+'
'; } if(curPayload.finalResponse&&curPayload.finalResponse!==curPayload.rawResponse){ h+='
✅ 最终响应(处理后)'+fmtN(curPayload.finalResponse.length)+' chars
'; h+='
'+escH(curPayload.finalResponse)+'
'; } if(curPayload.toolCalls&&curPayload.toolCalls.length){ h+='
🔧 工具调用结果 '+curPayload.toolCalls.length+' 个
'; h+='
'+syntaxHL(curPayload.toolCalls)+'
'; } if(curPayload.retryResponses&&curPayload.retryResponses.length){ h+='
🔄 重试历史 '+curPayload.retryResponses.length+' 次
'; curPayload.retryResponses.forEach(r=>{h+='
第 '+r.attempt+' 次重试 — '+escH(r.reason)+'
'+escH(r.response.substring(0,1000))+(r.response.length>1000?'\n... ('+fmtN(r.response.length)+' chars)':'')+'
'}); h+='
'; } if(curPayload.continuationResponses&&curPayload.continuationResponses.length){ h+='
📎 续写历史 '+curPayload.continuationResponses.length+' 次
'; curPayload.continuationResponses.forEach(r=>{h+='
续写 #'+r.index+' (去重后 '+fmtN(r.dedupedLength)+' chars)
'+escH(r.response.substring(0,1000))+(r.response.length>1000?'\n...':'')+'
'}); h+='
'; } tc.innerHTML=h||'
📤

暂无响应数据

'; } // ===== Log rendering ===== function renderLogs(ll){ const el=document.getElementById('logList');if(!el)return; const fil=cLv==='all'?ll:ll.filter(l=>l.level===cLv); if(!fil.length){el.innerHTML='
📋

暂无日志

';return} const autoExp=document.getElementById('autoExpand').checked; // 如果是全局视图(未选中请求),在不同 requestId 之间加分隔线 let lastRid=''; el.innerHTML=fil.map(l=>{ let sep=''; if(!selId&&l.requestId!==lastRid&&lastRid){ const title=rmap[l.requestId]?.title||l.requestId; sep='
'+escH(title)+' ('+l.requestId+')
'; } lastRid=l.requestId; return sep+logH(l,autoExp); }).join(''); el.scrollTop=el.scrollHeight; } function logH(l,autoExp){ const t=new Date(l.timestamp).toLocaleTimeString('zh-CN',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}); const d=l.duration!=null?'+'+l.duration+'ms':''; let det=''; if(l.details){ const raw=typeof l.details==='string'?l.details:JSON.stringify(l.details,null,2); const show=autoExp; det='
'+(show?'▼ 收起':'▶ 详情')+'
'+syntaxHL(l.details)+'
'; } return '
'+t+''+d+''+l.level+''+l.source+''+l.phase+'
'+escH(l.message)+det+'
'; } function escAttr(s){return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'')} function appendLog(en){ const el=document.getElementById('logList');if(!el)return; if(el.querySelector('.empty'))el.innerHTML=''; if(cLv!=='all'&&en.level!==cLv)return; const autoExp=document.getElementById('autoExpand').checked; // 分隔线(实时模式) if(!selId){ const children=el.children; if(children.length>0){ const lastEl=children[children.length-1]; const lastRid=lastEl.getAttribute('data-rid')||''; if(lastRid&&lastRid!==en.requestId){ const title=rmap[en.requestId]?.title||en.requestId; const sep=document.createElement('div'); sep.innerHTML='
'+escH(title)+' ('+en.requestId+')
'; while(sep.firstChild)el.appendChild(sep.firstChild); } } } const d=document.createElement('div');d.innerHTML=logH(en,autoExp); const n=d.firstElementChild;n.classList.add('ani');n.setAttribute('data-rid',en.requestId); el.appendChild(n); while(el.children.length>500)el.removeChild(el.firstChild); el.scrollTop=el.scrollHeight; } // ===== Utils ===== function togDet(el){const d=el.nextElementSibling;if(d.style.display==='none'){d.style.display='block';el.textContent='▼ 收起'}else{d.style.display='none';el.textContent='▶ 详情'}} function togMsg(el){const b=el.nextElementSibling;const isHidden=b.style.display==='none';b.style.display=isHidden?'block':'none';const m=el.querySelector('.msg-meta');if(m){const t=m.textContent;m.textContent=isHidden?t.replace('▶ 展开','▼ 收起'):t.replace('▼ 收起','▶ 展开')}} function sL(lv,btn){cLv=lv;document.querySelectorAll('#lvF .lvb').forEach(b=>b.classList.remove('a'));btn.classList.add('a');if(curTab==='logs'){if(selId)renderLogs(logs.filter(l=>l.requestId===selId));else renderLogs(logs.slice(-200))}} // ===== Clear logs ===== async function clearLogs(){ if(!confirm('确定清空所有日志?此操作不可恢复。'))return; try{ await fetch(authQ('/api/logs/clear'),{method:'POST'}); reqs=[];rmap={};logs=[];selId=null;curPayload=null; renderRL();updCnt();updStats();desel(); }catch(e){console.error(e)} } // ===== Keyboard ===== document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&e.key==='k'){e.preventDefault();document.getElementById('searchIn').focus();return} if(e.key==='Escape'){if(document.activeElement===document.getElementById('searchIn')){document.getElementById('searchIn').blur();document.getElementById('searchIn').value='';sq='';renderRL();updCnt()}else{desel()}return} if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const q=sq.toLowerCase();const cut=getTimeCutoff();let f=reqs;if(cut)f=f.filter(r=>r.startTime>=cut);if(q)f=f.filter(r=>mS(r,q));if(cFil!=='all')f=f.filter(r=>r.status===cFil);if(!f.length)return;const ci=selId?f.findIndex(r=>r.requestId===selId):-1;let ni;if(e.key==='ArrowDown')ni=ci0?ci-1:f.length-1;selReq(f[ni].requestId);const it=document.querySelector('[data-r="'+f[ni].requestId+'"]');if(it)it.scrollIntoView({block:'nearest'})} }); document.getElementById('searchIn').addEventListener('input',e=>{sq=e.target.value;renderRL();updCnt()}); document.getElementById('rlist').addEventListener('click',e=>{const el=e.target.closest('[data-r]');if(el)selReq(el.getAttribute('data-r'))}); setInterval(renderRL,30000); init();