377 lines
32 KiB
TypeScript
377 lines
32 KiB
TypeScript
// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer.
|
||
// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts.
|
||
import { shellQuote } from "./ssh";
|
||
|
||
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "project-summary";
|
||
|
||
export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView {
|
||
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "project-summary") return value;
|
||
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, or project-summary; got ${value}`);
|
||
}
|
||
|
||
export function nodeWebObserveCollectViewNodeScript(input: {
|
||
maxFiles: number;
|
||
view: NodeWebProbeObserveCollectView;
|
||
traceId: string | null;
|
||
sampleSeq: number | null;
|
||
timestamp: string | null;
|
||
turn: number | null;
|
||
}): string {
|
||
const requestedSampleSeq = input.sampleSeq === null ? "null" : String(input.sampleSeq);
|
||
const requestedTurn = input.turn === null ? "null" : String(input.turn);
|
||
return `node -e ${shellQuote(`
|
||
const fs=require('fs'),path=require('path'),crypto=require('crypto');
|
||
const dir=process.argv[1]; const maxFiles=${input.maxFiles}; const view=${JSON.stringify(input.view)}; const requestedTraceId=${JSON.stringify(input.traceId)}; const requestedSampleSeq=${requestedSampleSeq}; const requestedTimestamp=${JSON.stringify(input.timestamp)}; const requestedTurn=${requestedTurn};
|
||
const sha=(value)=>'sha256:'+crypto.createHash('sha256').update(String(value||'')).digest('hex');
|
||
const short=(value,limit=96)=>{const text=String(value||'').replace(/\\s+/gu,' ').trim(); return text.length>limit?text.slice(0,Math.max(1,limit-1))+'...':text;};
|
||
const textOf=(value)=>String(value?.text||value?.textPreview||value?.preview||'');
|
||
const readJson=(rel)=>{try{return JSON.parse(fs.readFileSync(path.join(dir,rel),'utf8'))}catch{return null}};
|
||
const readJsonl=(rel)=>{try{return fs.readFileSync(path.join(dir,rel),'utf8').split(/\\r?\\n/u).filter(Boolean).map((line)=>{try{return JSON.parse(line)}catch{return {parseError:true,rawHash:sha(line)}}})}catch{return []}};
|
||
const out=[]; const walk=(p)=>{for(const ent of fs.readdirSync(p,{withFileTypes:true})){const full=path.join(p,ent.name); if(ent.isDirectory()) walk(full); else out.push(full); if(out.length>=maxFiles) return;}};
|
||
walk(dir);
|
||
const files=out.slice(0,maxFiles).map(file=>{const st=fs.statSync(file); const hash=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); return {path:file,relative:path.relative(dir,file),byteCount:st.size,sha256:'sha256:'+hash}});
|
||
const totalBytes=files.reduce((sum,item)=>sum+item.byteCount,0);
|
||
if(view==='files'){
|
||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,fileCount:files.length,totalBytes,files,valuesRedacted:true},null,2));
|
||
process.exit(0);
|
||
}
|
||
const samples=readJsonl('samples.jsonl');
|
||
const control=readJsonl('control.jsonl');
|
||
const manifest=readJson('manifest.json')||{};
|
||
const report=readJson('analysis/report.json')||{};
|
||
function unique(values){return Array.from(new Set(values.filter(Boolean)));}
|
||
function numOrNull(value){const n=Number(value); return Number.isFinite(n)?n:null}
|
||
function promptIndexOf(value){const n=Number(value); return Number.isInteger(n)&&n>0?n:null}
|
||
function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFinite(ms)?ms:null}
|
||
function sessionIdFromUrl(value){const match=String(value||'').match(/\\/workbench\\/sessions\\/(ses_[A-Za-z0-9_-]+)/u); return match?match[1]:null}
|
||
function commandSessionId(item){return item?.sessionId||item?.detail?.sessionId||item?.input?.sessionId||sessionIdFromUrl(item?.afterUrl)||sessionIdFromUrl(item?.detail?.afterUrl)||null}
|
||
function commandTraceId(item){return item?.traceId||item?.detail?.chatSubmit?.traceId||item?.detail?.traceId||item?.input?.traceId||null}
|
||
function promptCommands(){
|
||
const map=new Map();
|
||
for(const item of control){
|
||
if(item.type!=='sendPrompt'||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue;
|
||
const id=item.commandId||item.seq||String(map.size+1);
|
||
const existing=map.get(id)||{};
|
||
map.set(id,{...existing,...item,input:{...(existing.input||{}),...(item.input||{})},sessionId:existing.sessionId||commandSessionId(item),traceId:existing.traceId||commandTraceId(item),firstTs:existing.firstTs||item.ts,lastTs:item.ts});
|
||
}
|
||
const prompts=Array.from(map.values()).filter((item)=>tsMs(item.firstTs)!==null).sort((a,b)=>tsMs(a.firstTs)-tsMs(b.firstTs));
|
||
const latestSession=samples.slice().reverse().find((sample)=>sample.routeSessionId||sample.activeSessionId);
|
||
const sessionId=latestSession?.routeSessionId||latestSession?.activeSessionId||null;
|
||
if(!sessionId) return prompts;
|
||
const scoped=prompts.filter((item)=>item.sessionId===sessionId);
|
||
return scoped.length>0?scoped:prompts;
|
||
}
|
||
function segmentFor(index,prompts){
|
||
const start=tsMs(prompts[index]?.firstTs); const end=index+1<prompts.length?tsMs(prompts[index+1].firstTs):Infinity;
|
||
return samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms>=start&&ms<end});
|
||
}
|
||
function controlsFor(index,prompts){
|
||
const start=tsMs(prompts[index]?.firstTs); const end=index+1<prompts.length?tsMs(prompts[index+1].firstTs):Infinity;
|
||
return control.filter((item)=>{const ms=tsMs(item.ts); return ms!==null&&ms>=start&&ms<end});
|
||
}
|
||
function firstTraceId(text){const match=String(text||'').match(/\\btrc_[A-Za-z0-9_-]+\\b/u); return match?match[0]:null}
|
||
function itemTraceId(item){return item?.traceId||firstTraceId(textOf(item))||null}
|
||
function entryGroups(sample){
|
||
return [
|
||
...(Array.isArray(sample.turns)?sample.turns.map((item)=>({group:'turn',item})):[]),
|
||
...(Array.isArray(sample.traceRows)?sample.traceRows.map((item)=>({group:'traceRow',item})):[]),
|
||
...(Array.isArray(sample.messages)?sample.messages.map((item)=>({group:'message',item})):[]),
|
||
];
|
||
}
|
||
function traceIdStats(items){
|
||
const map=new Map();
|
||
for(const sample of items){
|
||
const seq=numOrNull(sample.seq);
|
||
for(const entry of entryGroups(sample)){
|
||
const id=itemTraceId(entry.item); if(!id) continue;
|
||
const current=map.get(id)||{traceId:id,count:0,firstSeq:null,lastSeq:null,terminalCount:0};
|
||
current.count+=1;
|
||
current.firstSeq=current.firstSeq===null?seq:seq===null?current.firstSeq:Math.min(current.firstSeq,seq);
|
||
current.lastSeq=current.lastSeq===null?seq:seq===null?current.lastSeq:Math.max(current.lastSeq,seq);
|
||
if(terminalText(textOf(entry.item))||String(entry.item?.status||'').match(/complete|fail|cancel|terminal/iu)) current.terminalCount+=1;
|
||
map.set(id,current);
|
||
}
|
||
}
|
||
return Array.from(map.values()).sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||String(a.traceId).localeCompare(String(b.traceId)));
|
||
}
|
||
function traceIdsFromSamples(items){return traceIdStats(items).map((item)=>item.traceId)}
|
||
function collectReportTurnColumns(value,out=[]){
|
||
if(!value||typeof value!=='object') return out;
|
||
if(Array.isArray(value)){for(const item of value) collectReportTurnColumns(item,out); return out;}
|
||
if(Array.isArray(value.turnColumns)){
|
||
for(const item of value.turnColumns){
|
||
if(!item||typeof item!=='object') continue;
|
||
const traceId=String(item.traceId||'').trim();
|
||
if(!traceId) continue;
|
||
out.push({...item,traceId,promptIndex:promptIndexOf(item.promptIndex),lastPromptIndex:promptIndexOf(item.lastPromptIndex),firstSeq:numOrNull(item.firstSeq),lastSeq:numOrNull(item.lastSeq)});
|
||
}
|
||
}
|
||
for(const key of ['sampleMetrics','analysis','analysisWindow','recentWindow','windows','recent','archive','summary']){
|
||
if(value[key]&&value[key]!==value) collectReportTurnColumns(value[key],out);
|
||
}
|
||
return out;
|
||
}
|
||
const reportTurnColumns=(()=>{const seen=new Set(); const rows=[]; for(const item of collectReportTurnColumns(report)){const key=[item.traceId,item.promptIndex,item.lastPromptIndex,item.firstSeq,item.lastSeq,item.source,item.pageRole,item.messageId].join('|'); if(seen.has(key)) continue; seen.add(key); rows.push(item);} return rows.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.promptIndex??9999)-(b.promptIndex??9999));})();
|
||
function columnOverlapsSegment(column,segment){
|
||
if(segment.length===0) return true;
|
||
const first=numOrNull(segment[0]?.seq); const last=numOrNull(segment[segment.length-1]?.seq);
|
||
if(first===null||last===null||column.firstSeq===null||column.lastSeq===null) return true;
|
||
return column.lastSeq>=first&&column.firstSeq<=last;
|
||
}
|
||
function reportColumnsForPrompt(index,segment){
|
||
const promptIndex=index+1;
|
||
const segmentIds=new Set(traceIdsFromSamples(segment));
|
||
return reportTurnColumns
|
||
.filter((column)=>(column.promptIndex===promptIndex||column.lastPromptIndex===promptIndex)&&columnOverlapsSegment(column,segment))
|
||
.filter((column)=>segmentIds.size===0||segmentIds.has(column.traceId)||column.source==='turn')
|
||
.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.lastSeq??0)-(b.lastSeq??0)||String(a.label||'').localeCompare(String(b.label||'')));
|
||
}
|
||
function choosePrimaryTraceId(index,segment,prompt=null){
|
||
const promptTraceId=commandTraceId(prompt)||prompt?.traceId||null;
|
||
if(promptTraceId){
|
||
const segmentIds=new Set(traceIdsFromSamples(segment));
|
||
if(segmentIds.size===0||segmentIds.has(promptTraceId)) return {traceId:promptTraceId,columns:reportColumnsForPrompt(index,segment)};
|
||
}
|
||
const columns=reportColumnsForPrompt(index,segment);
|
||
const promptIndex=index+1;
|
||
const promptColumns=columns.filter((column)=>column.promptIndex===promptIndex);
|
||
const preferred=promptColumns.length>0?promptColumns:columns;
|
||
if(preferred.length>0) return {traceId:preferred[0].traceId,columns};
|
||
const stats=traceIdStats(segment).sort((a,b)=>(b.lastSeq??0)-(a.lastSeq??0)||(b.firstSeq??0)-(a.firstSeq??0));
|
||
return {traceId:stats[0]?.traceId||null,columns:[]};
|
||
}
|
||
function samplesForTrace(items,traceId,maxSeq=null){
|
||
if(!traceId) return items;
|
||
return items.filter((sample)=>{
|
||
const seq=numOrNull(sample.seq);
|
||
return (maxSeq===null||seq===null||seq<=maxSeq)&&traceIdsFromSamples([sample]).includes(traceId);
|
||
});
|
||
}
|
||
function latestSampleForTrace(items,traceId,maxSeq=null){
|
||
return samplesForTrace(items,traceId,maxSeq).slice(-1)[0]||null;
|
||
}
|
||
function traceEntries(items,traceId){
|
||
const entries=[];
|
||
for(const sample of items){
|
||
for(const entry of entryGroups(sample)){
|
||
const text=textOf(entry.item);
|
||
const id=itemTraceId(entry.item);
|
||
if(!traceId||id===traceId||text.includes(traceId)) entries.push({...entry,sample,text});
|
||
}
|
||
}
|
||
return entries;
|
||
}
|
||
function textsFor(items,traceId=null){
|
||
const entries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
|
||
return entries.length>0?entries.map((entry)=>entry.text):[];
|
||
}
|
||
function parseElapsed(text){
|
||
const values=[]; const raw=String(text||'');
|
||
for(const m of raw.matchAll(/(?:耗时|总耗时|total\\s*=?)\\s*(?:(\\d+)\\s*(?:小时|h)\\s*)?(?:(\\d+)\\s*(?:分|分钟|m)\\s*)?(?:(\\d+)\\s*(?:秒|s))?/giu)){
|
||
const total=Number(m[1]||0)*3600+Number(m[2]||0)*60+Number(m[3]||0); if(total>0) values.push(total);
|
||
}
|
||
for(const m of raw.matchAll(/\\b(?:total|elapsed)\\s*=\\s*(?:(\\d{1,2}):)?(\\d{1,2}):(\\d{2})\\b/giu)){
|
||
values.push(Number(m[1]||0)*3600+Number(m[2]||0)*60+Number(m[3]||0));
|
||
}
|
||
return values.length?Math.max(...values):null;
|
||
}
|
||
function parseRecent(text){const m=String(text||'').match(/最近\\s*(\\d+)\\s*(秒|分钟|分|小时)前/u); if(!m)return null; const n=Number(m[1]); return m[2]==='小时'?n*3600:m[2]==='秒'?n:n*60}
|
||
function fmtDuration(seconds){if(seconds===null||seconds===undefined||!Number.isFinite(Number(seconds)))return '-'; const value=Math.max(0,Math.round(Number(seconds))); const h=Math.floor(value/3600), m=Math.floor((value%3600)/60), s=value%60; return (h>0?String(h).padStart(2,'0')+':':'')+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')}
|
||
function terminalText(text){return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|completed|failed|canceled|cancelled|terminal/iu.test(String(text||''))}
|
||
function normalizeLifecycleStatus(value){
|
||
const raw=String(value||'').trim().toLowerCase();
|
||
if(/^(canceled|cancelled)$/u.test(raw))return 'canceled';
|
||
if(/^(failed|failure|error)$/u.test(raw))return 'failed';
|
||
if(/^(completed|complete|succeeded|success|terminal)$/u.test(raw))return 'completed';
|
||
if(/^(running|admitting|queued|pending|in_progress|in-progress)$/u.test(raw))return 'running';
|
||
return null;
|
||
}
|
||
function statusFor(items,traceId=null){
|
||
const matchedEntries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})));
|
||
const lastTurn=matchedEntries.filter((entry)=>entry.group==='turn').slice(-1)[0]?.item||null;
|
||
const turnStatus=normalizeLifecycleStatus(lastTurn?.status);
|
||
if(turnStatus)return turnStatus;
|
||
const lastMessage=matchedEntries.filter((entry)=>entry.group==='message').slice(-1)[0]?.item||null;
|
||
const messageStatus=normalizeLifecycleStatus(lastMessage?.status);
|
||
if(messageStatus)return messageStatus;
|
||
return 'unknown';
|
||
}
|
||
function userMessageFor(items,prompt){
|
||
const hash=prompt?.input?.textHash||null;
|
||
for(const sample of items){
|
||
for(const message of Array.isArray(sample.messages)?sample.messages:[]){
|
||
if(hash&&message.textHash===hash)return {preview:short(textOf(message),90),textHash:message.textHash||hash,textBytes:message.textBytes??prompt?.input?.textBytes??null};
|
||
}
|
||
}
|
||
return {preview:short(prompt?.input?.textPreview||'',90)||'(hash only)',textHash:hash,textBytes:prompt?.input?.textBytes??null};
|
||
}
|
||
function cleanFinalResponseText(text){
|
||
const raw=String(text||'').trim();
|
||
if(!raw) return '';
|
||
if(/^Code Agent\\s*(?:running|completed|failed|canceled|cancelled)?$/iu.test(raw)) return '';
|
||
if(/Code Agent\\s*耗时[\\s\\S]*运行记录/iu.test(raw)) return '';
|
||
if(/^sent\\s+当前 AgentRun 请求已取消/u.test(raw)) return '';
|
||
if(/^(admitted|run|ok|error)\\s+/iu.test(raw)) return '';
|
||
if(/^(AgentRun|Code Agent turn was durably admitted|commandExecution|runner-job|command -v|node --version|npm --version|cd |mkdir -p|npm install|npx )/iu.test(raw)) return '';
|
||
if(/(?:^|\\s)轮次(完成|失败|取消)(?总耗时/iu.test(raw)) return '';
|
||
if(/^(轮次完成|轮次失败|轮次取消|已记录)$/u.test(raw.replace(/\\s+/gu,' ').trim())) return '';
|
||
return raw;
|
||
}
|
||
function emptyFinalResponse(){return {preview:'(空内容)',textHash:null,textBytes:0,empty:true}}
|
||
function terminalFinalStatus(value){return /^(completed|failed|canceled|cancelled|terminal)$/u.test(String(value||'').trim().toLowerCase())}
|
||
function entryRole(entry){return String(entry.item?.role||entry.item?.dataRole||entry.item?.authorRole||entry.item?.messageRole||entry.item?.speaker||'').toLowerCase()}
|
||
function finalCandidateText(entry){
|
||
const explicit=textOf(entry.item?.finalResponse);
|
||
if(explicit) return explicit;
|
||
if(entry.group==='message') return entry.text;
|
||
return '';
|
||
}
|
||
function sameAsUserPrompt(entry,text,user){
|
||
if(!user) return false;
|
||
if(user.textHash&&entry.item?.textHash===user.textHash) return true;
|
||
const userPrefix=short(user.preview||'',48).replace(/\\s+/gu,' ').trim().slice(0,32);
|
||
const textPrefix=String(text||'').replace(/\\s+/gu,' ').trim().slice(0,32);
|
||
return userPrefix.length>=18&&textPrefix===userPrefix;
|
||
}
|
||
function finalResponseFor(items,traceId,user=null){
|
||
if(!terminalFinalStatus(statusFor(items,traceId))) return emptyFinalResponse();
|
||
const candidates=(traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})))).slice().reverse();
|
||
for(const entry of candidates){
|
||
const role=entryRole(entry);
|
||
const entryStatus=String(entry.item?.status||'');
|
||
if(entry.group==='message'&&!/assistant|agent|system/u.test(role)) continue;
|
||
if(/用户|user/iu.test(role)||/用户|user/iu.test(entryStatus)) continue;
|
||
const status=String(entry.item?.status||'').toLowerCase(); const text=cleanFinalResponseText(finalCandidateText(entry));
|
||
if(!text) continue;
|
||
if(/^第\\d+轮\\/\\d+[::]?\\s*请/u.test(text)||/^请(执行|帮|回答|修复|调查|用|不要|直接)/u.test(text)) continue;
|
||
if(sameAsUserPrompt(entry,text,user)) continue;
|
||
if(status.includes('running')&&!terminalText(entry.text)&&entry.group==='turn')continue;
|
||
const preview=short(text,180);
|
||
if(preview)return {preview,textHash:entry.item?.textHash||sha(text),textBytes:entry.item?.textBytes??Buffer.byteLength(text),empty:false};
|
||
}
|
||
return emptyFinalResponse();
|
||
}
|
||
function turnSummaryRows(){
|
||
const prompts=promptCommands();
|
||
if(prompts.length===0){
|
||
const allTraceIds=traceIdsFromSamples(samples);
|
||
return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples,allTraceIds[0]||null),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}];
|
||
}
|
||
return prompts.map((prompt,index)=>{
|
||
const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const selected=choosePrimaryTraceId(index,segment,prompt); const traceId=selected.traceId; const user=userMessageFor(segment,prompt);
|
||
const primaryColumn=selected.columns.find((column)=>column.traceId===traceId)||null;
|
||
const traceSample=latestSampleForTrace(segment,traceId,primaryColumn?.lastSeq??null);
|
||
const texts=textsFor(segment,traceId);
|
||
const elapsedValues=texts.map(parseElapsed).filter((value)=>value!==null); const recentValues=texts.map(parseRecent).filter((value)=>value!==null);
|
||
const marks=unique(segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)).join(',')||'-';
|
||
return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,traceIds:unique([traceId,...selected.columns.map((column)=>column.traceId),...traceIdsFromSamples(segment)]).slice(0,8),status:statusFor(segment,traceId),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??primaryColumn?.firstSeq??null,lastSeq:traceSample?.seq??primaryColumn?.lastSeq??segment[segment.length-1]?.seq??null,lastTs:traceSample?.ts??segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId,user),valuesRedacted:true};
|
||
});
|
||
}
|
||
function pad(value,width){const text=short(value,width); return text+Array(Math.max(0,width-text.length)+1).join(' ')}
|
||
function renderTurnSummary(rows){
|
||
const title='Workbench Session '+(samples[samples.length-1]?.routeSessionId||samples[samples.length-1]?.activeSessionId||'-')+' observer '+(manifest.jobId||'-');
|
||
const header='轮次 用户消息 Trace 状态 耗时/最近 标记 Final Response';
|
||
const body=rows.map((row)=>pad(row.round,5)+' '+pad((row.userHash?row.userHash.slice(0,18)+' ':'')+row.userPreview,32)+' '+pad(row.traceId||'-',12)+' '+pad(row.status,10)+' '+pad(fmtDuration(row.elapsedSeconds)+'/'+(row.recentUpdateSeconds===null?'-':String(row.recentUpdateSeconds)+'s'),13)+' '+pad(row.marks,11)+' '+short(row.finalResponse?.preview||'(空内容)',80)).join('\\n');
|
||
return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts hwlab nodes web-probe observe collect '+(manifest.jobId||'<observer>')+' --view trace-frame --trace-id <traceId> --sample-seq <seq>'].join('\\n');
|
||
}
|
||
function diagnosticRows(label,items,limit=6){
|
||
const selected=items.slice(-limit);
|
||
const skipped=Math.max(0,items.length-selected.length);
|
||
const prefix=skipped>0?'last '+selected.length+'/'+items.length:items.length+'';
|
||
return selected.map((item,index)=>{
|
||
const traceId=itemTraceId(item)||item.traceId||'-';
|
||
const status=String(item?.status||item?.kind||item?.type||'-');
|
||
return label+' '+prefix+' #'+(skipped+index+1)+' trace='+short(traceId,22)+' status='+short(status,18)+' text='+short(textOf(item),120);
|
||
});
|
||
}
|
||
function traceFrameDiagnosticLines(sample,traceId,matchedTraceRows){
|
||
const allTraceRows=Array.isArray(sample.traceRows)?sample.traceRows:[];
|
||
const allTurns=Array.isArray(sample.turns)?sample.turns:[];
|
||
const allMessages=Array.isArray(sample.messages)?sample.messages:[];
|
||
const allIds=unique([...allTraceRows,...allTurns,...allMessages].map((item)=>itemTraceId(item)||item?.traceId||firstTraceId(textOf(item)))).slice(0,12);
|
||
return [
|
||
'TRACE DIAGNOSTIC',
|
||
'pageRole='+(sample.pageRole||'-')+' pageId='+(sample.pageId||'-')+' traceRows='+allTraceRows.length+' turns='+allTurns.length+' messages='+allMessages.length,
|
||
'requestedTraceId='+(traceId||'-')+' matchedTraceRows='+matchedTraceRows.length+' sampleTraceIds='+(allIds.join(',')||'-'),
|
||
...diagnosticRows('traceRow',allTraceRows),
|
||
...diagnosticRows('turn',allTurns,4),
|
||
...diagnosticRows('message',allMessages,4),
|
||
];
|
||
}
|
||
function selectSample(rows){
|
||
if(requestedSampleSeq!==null){const exact=samples.find((sample)=>Number(sample.seq)===requestedSampleSeq); if(exact)return exact;}
|
||
if(requestedTimestamp){const target=tsMs(requestedTimestamp); if(target!==null){const before=samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms<=target}).slice(-1)[0]; if(before)return before;}}
|
||
if(requestedTurn!==null&&rows[requestedTurn-1]?.traceId){const row=rows[requestedTurn-1]; const byTrace=latestSampleForTrace(samples,row.traceId,numOrNull(row.lastSeq)); if(byTrace)return byTrace;}
|
||
if(requestedTurn!==null&&rows[requestedTurn-1]?.lastSeq!==null){const byTurn=samples.find((sample)=>Number(sample.seq)===Number(rows[requestedTurn-1].lastSeq)); if(byTurn)return byTurn;}
|
||
if(requestedTraceId){const byTrace=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId)).slice(-1)[0]; if(byTrace)return byTrace;}
|
||
return samples[samples.length-1]||null;
|
||
}
|
||
function renderTraceFrame(sample,rows){
|
||
if(!sample)return {ok:false,renderedText:'TRACE_FRAME_BLOCKER\\nno sample matched --sample-seq/--timestamp/--trace-id/--turn',blocker:'sample-not-found'};
|
||
const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||(requestedTurn!==null?rows[requestedTurn-1]?.traceId:null)||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||sampleTraceIds[0]||null;
|
||
const traceRows=(Array.isArray(sample.traceRows)?sample.traceRows:[]).filter((row)=>!traceId||row.traceId===traceId||!row.traceId||textOf(row).includes(traceId));
|
||
const turns=(Array.isArray(sample.turns)?sample.turns:[]).filter((turn)=>!traceId||turn.traceId===traceId||textOf(turn).includes(traceId));
|
||
const texts=[...turns.map(textOf),...traceRows.map(textOf)];
|
||
const elapsed=Math.max(-1,...texts.map(parseElapsed).filter((value)=>value!==null)); const recent=Math.max(-1,...texts.map(parseRecent).filter((value)=>value!==null));
|
||
const status=statusFor([sample],traceId); const finalResponse=finalResponseFor([sample],traceId);
|
||
const visibleTraceRows=traceRows.slice(-24);
|
||
const rowLines=visibleTraceRows.map((row,index)=>{const text=textOf(row); return short((row.status?row.status+' ':'')+text,180)||('row#'+index+' '+(row.textHash||'-'));});
|
||
if(traceRows.length>visibleTraceRows.length) rowLines.unshift('(已省略 '+(traceRows.length-visibleTraceRows.length)+' 条较早 trace rows;需要原始数据请看 samples.jsonl)');
|
||
const missingRows=rowLines.length===0;
|
||
const bodyRows=missingRows?['(无 trace rows;这是 blocker,不能当业务通过证据)',...traceFrameDiagnosticLines(sample,traceId,traceRows)]:rowLines;
|
||
const rendered=['Code Agent 耗时 '+(elapsed>=0?fmtDuration(elapsed):'-')+' 最近 '+(recent>=0?String(recent)+' 秒前':'-')+' ('+status+')','=======================================================','sample seq='+(sample.seq??'-')+' ts='+(sample.ts||'-')+' traceId='+(traceId||'-')+' routeSession='+(sample.routeSessionId||'-')+' activeSession='+(sample.activeSessionId||'-'),...bodyRows,'==========================','Final Response',finalResponse.preview||'(空内容)'].join('\\n');
|
||
return {ok:!missingRows,renderedText:rendered,blocker:missingRows?'trace-rows-missing':null,sampleSeq:sample.seq??null,traceId,finalResponse,traceDiagnostic:missingRows?{pageRole:sample.pageRole||null,pageId:sample.pageId||null,traceRows:Array.isArray(sample.traceRows)?sample.traceRows.length:0,turns:Array.isArray(sample.turns)?sample.turns.length:0,messages:Array.isArray(sample.messages)?sample.messages.length:0,sampleTraceIds:traceIdsFromSamples([sample]).slice(0,12)}:null,valuesRedacted:true};
|
||
}
|
||
function projectSummaryFromSamples(){
|
||
const projectSamples=samples.filter((sample)=>sample?.projectManagement&&typeof sample.projectManagement==='object');
|
||
const latest=projectSamples[projectSamples.length-1]||null;
|
||
const latestProject=latest?.projectManagement||null;
|
||
const launches=control.filter((item)=>item.type==='launchWorkbenchFromTask'&&(item.phase==='completed'||item.phase==='failed')).map((item)=>{
|
||
const detail=item.detail&&typeof item.detail==='object'?item.detail:{};
|
||
const error=detail.error&&typeof detail.error==='object'?detail.error:{};
|
||
return {ts:item.ts||null,phase:item.phase||null,commandId:item.commandId||null,status:detail.launchStatus??error.details?.launchStatus??null,sessionId:detail.sessionId??error.details?.sessionId??null,workbenchUrl:detail.workbenchUrl??error.details?.workbenchUrl??null,otelTraceId:detail.otelTraceId??error.details?.otelTraceId??null,taskHash:detail.selectedTask?.hash??detail.projectBeforeClick?.selectedTaskRef?.hash??null,message:error.message?short(error.message,160):null,valuesRedacted:true};
|
||
});
|
||
const findings=Array.isArray(report.findings)?report.findings.filter((item)=>String(item?.id||item?.kind||'').match(/project-management|mdtodo|workbench-launch/u)).slice(0,20):[];
|
||
const summary=report.projectManagement?.summary||{};
|
||
const mdtodoSampleCount=projectSamples.filter((sample)=>sample.projectManagement?.pageKind==='project-management-mdtodo').length;
|
||
const derived={enabled:summary.enabled===true||projectSamples.length>0,projectSampleCount:Math.max(Number(summary.projectSampleCount??0),projectSamples.length),mdtodoSampleCount:Math.max(Number(summary.mdtodoSampleCount??0),mdtodoSampleCount),latestPageKind:summary.latestPageKind??latestProject?.pageKind??null,latestPath:summary.latestPath??latest?.path??null,latestSeq:summary.latestSeq??latest?.seq??null,latestTs:summary.latestTs??latest?.ts??null,latestSourceCount:summary.latestSourceCount??latestProject?.sourceCount??null,latestFileCount:summary.latestFileCount??latestProject?.fileCount??null,latestTaskCount:summary.latestTaskCount??latestProject?.taskCount??null,latestSelectedTaskRefHash:summary.latestSelectedTaskRefHash??latestProject?.selectedTaskRef?.hash??null,latestSelectedTaskRefPreview:summary.latestSelectedTaskRefPreview??latestProject?.selectedTaskRef?.preview??null,launchCommandCount:summary.launchCommandCount??launches.length,launchSuccessCount:summary.launchSuccessCount??launches.filter((item)=>Number(item.status)>=200&&Number(item.status)<300).length,launchFailureCount:summary.launchFailureCount??launches.filter((item)=>item.phase==='failed'||Number(item.status)>=400).length,launchWithOtelTraceHeaderCount:summary.launchWithOtelTraceHeaderCount??launches.filter((item)=>item.otelTraceId).length,projectApiResponseCount:summary.projectApiResponseCount??null,projectApiFailureCount:summary.projectApiFailureCount??null,projectApiRequestFailedCount:summary.projectApiRequestFailedCount??null,projectApiSlowPathCount:summary.projectApiSlowPathCount??null,valuesRedacted:true};
|
||
return {summary:derived,launches:launches.slice(-12),findings,sampleRows:projectSamples.slice(-12).map((sample)=>({seq:sample.seq??null,ts:sample.ts??null,pageRole:sample.pageRole??null,path:sample.path??null,pageKind:sample.projectManagement?.pageKind??null,sourceCount:sample.projectManagement?.sourceCount??null,fileCount:sample.projectManagement?.fileCount??null,taskCount:sample.projectManagement?.taskCount??null,selectedTaskRefHash:sample.projectManagement?.selectedTaskRef?.hash??null,selectedTaskStatus:sample.projectManagement?.selectedTaskStatus??null,launchButtonEnabled:sample.projectManagement?.launchButtonEnabled===true,workbenchLinkCount:sample.projectManagement?.workbenchLinkCount??0,valuesRedacted:true})),valuesRedacted:true};
|
||
}
|
||
function targetNodeFromStateDir(){
|
||
const parts=String(dir||'').split(/[\\\\/]+/u);
|
||
const index=parts.lastIndexOf('web-observe');
|
||
return index>=0&&parts[index+1]?parts[index+1]:null;
|
||
}
|
||
function renderProjectSummary(project){
|
||
const s=project.summary||{};
|
||
const lines=['Project management observer '+(manifest.jobId||'-'),'=======================================================','enabled='+String(s.enabled===true)+' samples='+String(s.projectSampleCount??0)+' mdtodo='+String(s.mdtodoSampleCount??0)+' latest='+String(s.latestPageKind||'-')+' path='+String(s.latestPath||'-'),'counts source='+String(s.latestSourceCount??'-')+' file='+String(s.latestFileCount??'-')+' task='+String(s.latestTaskCount??'-')+' selectedTask='+String(s.latestSelectedTaskRefHash||'-'),'launch commands='+String(s.launchCommandCount??0)+' success='+String(s.launchSuccessCount??0)+' failure='+String(s.launchFailureCount??0)+' otelTraceHeader='+String(s.launchWithOtelTraceHeaderCount??0),'api responses='+String(s.projectApiResponseCount??'-')+' failures='+String(s.projectApiFailureCount??'-')+'/'+String(s.projectApiRequestFailedCount??'-')+' slowPaths='+String(s.projectApiSlowPathCount??'-'),'','Recent samples'];
|
||
for(const row of project.sampleRows.slice(-12)) lines.push('#'+String(row.seq??'-')+' '+String(row.ts||'-')+' '+String(row.pageRole||'-')+' '+String(row.pageKind||'-')+' src='+String(row.sourceCount??'-')+' files='+String(row.fileCount??'-')+' tasks='+String(row.taskCount??'-')+' selected='+String(row.selectedTaskRefHash||'-')+' launch='+String(row.launchButtonEnabled===true)+' links='+String(row.workbenchLinkCount??0));
|
||
lines.push('','Launches');
|
||
if(project.launches.length===0) lines.push('-');
|
||
for(const item of project.launches.slice(-12)) lines.push(String(item.ts||'-')+' '+String(item.phase||'-')+' status='+String(item.status??'-')+' session='+String(item.sessionId||'-')+' otel='+String(item.otelTraceId||'-')+' task='+String(item.taskHash||'-'));
|
||
const otelLaunches=project.launches.filter((item)=>item.otelTraceId).slice(-4);
|
||
if(otelLaunches.length>0){
|
||
const target=targetNodeFromStateDir()||'<target>';
|
||
lines.push('','OTel trace drill-down');
|
||
for(const item of otelLaunches) lines.push('bun scripts/cli.ts platform-infra observability trace --target '+target+' --trace-id '+String(item.otelTraceId));
|
||
}
|
||
lines.push('','Findings');
|
||
if(project.findings.length===0) lines.push('-');
|
||
for(const item of project.findings.slice(0,12)) lines.push(String(item.severity||'-')+': '+String(item.id||item.kind||'-')+' count='+String(item.count??'-')+' '+short(item.summary||item.message||'',140));
|
||
return lines.join('\\n');
|
||
}
|
||
const rows=turnSummaryRows();
|
||
if(view==='project-summary'){
|
||
const project=projectSummaryFromSamples();
|
||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:project.summary,sampleRowCount:project.sampleRows.length,launchCount:project.launches.length,findingCount:project.findings.length,renderedText:renderProjectSummary(project),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true}));
|
||
process.exit(0);
|
||
}
|
||
if(view==='turn-summary'){
|
||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,turnCount:rows.length,rows:rows.slice(0,80),renderedText:renderTurnSummary(rows),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2));
|
||
process.exit(0);
|
||
}
|
||
const frame=renderTraceFrame(selectSample(rows),rows);
|
||
console.log(JSON.stringify({ok:frame.ok!==false,command:'web-probe-observe collect',view,stateDir:dir,...frame,sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2));
|
||
`)} "$state_dir"`;
|
||
}
|