@@ -2,11 +2,11 @@
// 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" | "project-mdtodo-summary" ;
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "timeline" | "project-summary" | "project-mdtodo-summary" ;
export function parseNodeWebProbeObserveCollectView ( value : string ) : NodeWebProbeObserveCollectView {
if ( value === "files" || value === "turn-summary" || value === "trace-frame" || value === "project-summary" || value === "project-mdtodo-summary" ) return value ;
throw new Error ( ` web-probe observe collect --view must be files, turn-summary, trace-frame, project-summary, or project-mdtodo-summary; got ${ value } ` ) ;
if ( value === "files" || value === "turn-summary" || value === "trace-frame" || value === "timeline" || value === "project-summary" || value === "project-mdtodo-summary" ) return value ;
throw new Error ( ` web-probe observe collect --view must be files, turn-summary, trace-frame, timeline, project-summary, or project-mdtodo-summary; got ${ value } ` ) ;
}
export function nodeWebObserveCollectViewNodeScript ( input : {
@@ -16,12 +16,15 @@ export function nodeWebObserveCollectViewNodeScript(input: {
sampleSeq : number | null ;
timestamp : string | null ;
turn : number | null ;
commandId : string | null ;
windowMs : number | null ;
} ) : string {
const requestedSampleSeq = input . sampleSeq === null ? "null" : String ( input . sampleSeq ) ;
const requestedTurn = input . turn === null ? "null" : String ( input . turn ) ;
const requestedWindowMs = input . windowMs === null ? "null" : String ( input . windowMs ) ;
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 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 requestedCommandId= ${ JSON . stringify ( input . commandId ) } ; const requestedWindowMs= ${ requestedWindowMs } ;
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||'');
@@ -349,6 +352,181 @@ function renderTraceFrame(sample,rows){
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 firstPresent(values){
for(const value of values){
if(value!==null&&value!==undefined&&String(value)!=='') return value;
}
return null;
}
function commandIdOf(item){return item?.commandId||item?.id||item?.detail?.commandId||item?.input?.commandId||null}
function controlSessionId(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
return commandSessionId(item)||detail.sessionId||detail.result?.sessionId||detail.observer?.sessionId||null;
}
function controlTraceId(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
return commandTraceId(item)||detail.traceId||detail.chatSubmit?.traceId||detail.result?.traceId||detail.result?.chatSubmit?.traceId||null;
}
function safeListJsonFiles(rel){
const root=path.join(dir,rel); let entries=[];
try{entries=fs.readdirSync(root,{withFileTypes:true});}catch{return [];}
return entries.filter((entry)=>entry.isFile()&&entry.name.endsWith('.json')).map((entry)=>rel+'/'+entry.name);
}
function readCommandFiles(){
const buckets=['pending','processing','done','failed','abandoned'];
const rows=[];
for(const bucket of buckets){
for(const rel of safeListJsonFiles('commands/'+bucket)){
const data=readJson(rel)||{}; let stat=null;
try{stat=fs.statSync(path.join(dir,rel));}catch{}
const name=path.basename(rel);
rows.push({bucket,relative:rel,name,data,mtime:stat?stat.mtime.toISOString():null,valuesRedacted:true});
}
}
return rows.sort((a,b)=>(tsMs(commandFileTs(a))??0)-(tsMs(commandFileTs(b))??0)||a.relative.localeCompare(b.relative));
}
function commandFileId(row){return row?.data?.id||row?.data?.commandId||String(row?.name||'').replace(/[.]json $ /u,'')||null}
function commandFileType(row){return row?.data?.type||row?.data?.commandType||row?.data?.input?.type||row?.data?.result?.type||null}
function commandFileTs(row){return firstPresent([row?.data?.createdAt,row?.data?.completedAt,row?.data?.failedAt,row?.data?.abandonedAt,row?.data?.ts,row?.mtime])}
function commandFileSessionId(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const command=data.command&&typeof data.command==='object'?data.command:{};
return data.sessionId||data.input?.sessionId||command.sessionId||result.sessionId||result.observer?.sessionId||sessionIdFromUrl(result.afterUrl)||null;
}
function commandFileTraceId(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const error=data.error&&typeof data.error==='object'?data.error:{};
return data.traceId||data.input?.traceId||result.traceId||result.chatSubmit?.traceId||error.traceId||null;
}
function commandFileSummary(row){
const data=row?.data||{}; const result=data.result&&typeof data.result==='object'?data.result:{}; const error=data.error&&typeof data.error==='object'?data.error:{};
const parts=['file='+short(row.relative,48),'ok='+String(data.ok===true),'bucket='+row.bucket];
const duration=firstPresent([result.durationMs,result.observer?.durationMs,error.durationMs]);
if(duration!==null) parts.push('durationMs='+String(duration));
const status=firstPresent([result.status,result.phase,error.name,error.failureKind]);
if(status!==null) parts.push('status='+short(status,32));
const message=firstPresent([error.message,result.message,result.error?.message]);
if(message!==null) parts.push('message='+short(message,120));
return parts.join(' ');
}
function controlSummary(item){
const detail=item?.detail&&typeof item.detail==='object'?item.detail:{};
const input=item?.input&&typeof item.input==='object'?item.input:{};
const error=detail.error&&typeof detail.error==='object'?detail.error:null;
const parts=[];
if(input.textHash||input.hash) parts.push('textHash='+short(input.textHash||input.hash,24));
if(input.textBytes!==undefined) parts.push('textBytes='+String(input.textBytes));
if(detail.durationMs!==undefined) parts.push('durationMs='+String(detail.durationMs));
if(detail.retry||detail.retryLabel) parts.push('retry='+short(detail.retry||detail.retryLabel,24));
if(error?.message) parts.push('error='+short(error.message,120));
const afterPath=pathOnly(item?.afterUrl||detail.afterUrl);
if(afterPath) parts.push('afterPath='+short(afterPath,64));
return parts.join(' ')||'-';
}
function sampleSummary(sample){
const traceIds=traceIdsFromSamples([sample]).slice(0,4);
const counts='turns='+(Array.isArray(sample.turns)?sample.turns.length:0)+' traceRows='+(Array.isArray(sample.traceRows)?sample.traceRows.length:0)+' messages='+(Array.isArray(sample.messages)?sample.messages.length:0);
const pathText=sample.path||pathOnly(sample.url)||pathOnly(sample.currentUrl)||'-';
return 'path='+short(pathText,64)+' pageRole='+short(sample.pageRole||'-',18)+' '+counts+' traceIds='+(traceIds.join(',')||'-');
}
function isoOrNull(ms){return ms===null||ms===Infinity||ms===-Infinity?null:new Date(ms).toISOString()}
function timelineAnchor(rows,commandFiles){
if(requestedCommandId){
const controls=control.filter((item)=>String(commandIdOf(item)||'')===requestedCommandId);
const files=commandFiles.filter((item)=>String(commandFileId(item)||'')===requestedCommandId);
const times=[...controls.map((item)=>tsMs(item.ts)),...files.map((item)=>tsMs(commandFileTs(item)))].filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'command-id',value:requestedCommandId,startMs:start,endMs:end,anchorMs:end??start,matchedControlCount:controls.length,matchedCommandFileCount:files.length,valuesRedacted:true};
}
if(requestedTurn!==null&&rows[requestedTurn-1]){
const row=rows[requestedTurn-1]; const segment=samples.filter((sample)=>{const seq=numOrNull(sample.seq); return seq!==null&&row.firstSeq!==null&&row.lastSeq!==null&&seq>=Number(row.firstSeq)&&seq<=Number(row.lastSeq);});
const times=[tsMs(row.lastTs),...segment.map((sample)=>tsMs(sample.ts))].filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'turn',value:String(requestedTurn),commandId:row.commandId||null,traceId:row.traceId||null,startMs:start,endMs:end,anchorMs:end??start,valuesRedacted:true};
}
if(requestedTraceId){
const matched=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId));
const times=matched.map((sample)=>tsMs(sample.ts)).filter((value)=>value!==null);
const start=times.length?Math.min(...times):null; const end=times.length?Math.max(...times):null;
return {mode:'trace-id',value:requestedTraceId,startMs:start,endMs:end,anchorMs:end??start,matchedSampleCount:matched.length,valuesRedacted:true};
}
if(requestedSampleSeq!==null){
const sample=samples.find((item)=>Number(item.seq)===requestedSampleSeq)||null;
const anchorMs=tsMs(sample?.ts);
return {mode:'sample-seq',value:String(requestedSampleSeq),startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
if(requestedTimestamp){
const anchorMs=tsMs(requestedTimestamp);
return {mode:'timestamp',value:requestedTimestamp,startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
const latestTimes=[...samples.map((sample)=>tsMs(sample.ts)),...control.map((item)=>tsMs(item.ts)),...commandFiles.map((item)=>tsMs(commandFileTs(item)))].filter((value)=>value!==null);
const anchorMs=latestTimes.length?Math.max(...latestTimes):null;
return {mode:'latest',value:null,startMs:anchorMs,endMs:anchorMs,anchorMs,valuesRedacted:true};
}
function timelineWindow(anchor){
const windowMs=requestedWindowMs??120000;
if(anchor.startMs!==null&&anchor.endMs!==null&&anchor.startMs!==anchor.endMs){
return {fromMs:Math.max(0,anchor.startMs-1000),toMs:anchor.endMs+1000,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
if(anchor.anchorMs!==null){
return {fromMs:Math.max(0,anchor.anchorMs-windowMs),toMs:anchor.anchorMs+windowMs,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
return {fromMs:null,toMs:null,windowMs,anchorMode:anchor.mode,valuesRedacted:true};
}
function inTimelineWindow(ts,window){
const ms=tsMs(ts);
if(ms===null) return window.fromMs===null&&window.toMs===null;
if(window.fromMs!==null&&ms<window.fromMs) return false;
if(window.toMs!==null&&ms>window.toMs) return false;
return true;
}
function timelineEventRows(anchor,window,commandFiles){
const commandId=anchor.mode==='command-id'?anchor.value:null;
const matchedControl=control.filter((item)=>(commandId&&String(commandIdOf(item)||'')===commandId)||inTimelineWindow(item.ts,window));
const matchedSamples=samples.filter((sample)=>inTimelineWindow(sample.ts,window));
const matchedCommands=commandFiles.filter((item)=>(commandId&&String(commandFileId(item)||'')===commandId)||inTimelineWindow(commandFileTs(item),window));
const rows=[
...matchedControl.map((item)=>({ts:item.ts||null,seq:item.seq??null,kind:'control',phase:item.phase||'-',type:item.type||'-',commandId:commandIdOf(item),sessionId:controlSessionId(item),traceId:controlTraceId(item),summary:controlSummary(item),valuesRedacted:true})),
...matchedSamples.map((sample)=>({ts:sample.ts||null,seq:sample.seq??null,kind:'sample',phase:'sample',type:sample.reason||sample.pageRole||'-',commandId:sample.commandId||null,sessionId:sample.routeSessionId||sample.activeSessionId||null,traceId:traceIdsFromSamples([sample])[0]||null,summary:sampleSummary(sample),valuesRedacted:true})),
...matchedCommands.map((item)=>({ts:commandFileTs(item),seq:null,kind:'command-file',phase:item.bucket,type:commandFileType(item)||'-',commandId:commandFileId(item),sessionId:commandFileSessionId(item),traceId:commandFileTraceId(item),summary:commandFileSummary(item),valuesRedacted:true})),
].filter((item)=>item.ts!==null||item.commandId!==null)
.sort((a,b)=>(tsMs(a.ts)??0)-(tsMs(b.ts)??0)||String(a.kind).localeCompare(String(b.kind))||String(a.commandId||'').localeCompare(String(b.commandId||'')));
const cap=12;
let visible=rows;
if(rows.length>cap){
const evidence=rows.filter((row)=>row.kind!=='sample');
const sampleRows=rows.filter((row)=>row.kind==='sample');
visible=evidence.length>=cap?evidence.slice(evidence.length-cap):[...evidence,...sampleRows.slice(sampleRows.length-(cap-evidence.length))]
.sort((a,b)=>(tsMs(a.ts)??0)-(tsMs(b.ts)??0)||String(a.kind).localeCompare(String(b.kind))||String(a.commandId||'').localeCompare(String(b.commandId||'')));
}
return {rows:visible,omittedRowCount:Math.max(0,rows.length-visible.length),matchedControlCount:matchedControl.length,matchedSampleCount:matchedSamples.length,matchedCommandFileCount:matchedCommands.length,valuesRedacted:true};
}
function renderTimeline(anchor,window,eventRows){
const lines=['Observe artifact timeline '+(manifest.jobId||'-'),'======================================================='];
lines.push('stateDir='+dir);
lines.push('anchor='+anchor.mode+' value='+(anchor.value||'-')+' from='+(isoOrNull(window.fromMs)||'-')+' to='+(isoOrNull(window.toMs)||'-')+' windowMs='+String(window.windowMs));
lines.push('counts control='+String(eventRows.matchedControlCount)+' samples='+String(eventRows.matchedSampleCount)+' commandFiles='+String(eventRows.matchedCommandFileCount)+' shown='+String(eventRows.rows.length)+' omitted='+String(eventRows.omittedRowCount));
lines.push('', 'TIMELINE');
lines.push(pad('TS',24)+' '+pad('SEQ',5)+' '+pad('KIND',12)+' '+pad('PHASE',12)+' '+pad('TYPE',24)+' '+pad('COMMAND',28)+' '+pad('SESSION',18)+' '+pad('TRACE',18)+' SUMMARY');
if(eventRows.rows.length===0) lines.push('(no artifact rows matched; widen --window-ms or choose another --command-id/--timestamp/--turn)');
for(const row of eventRows.rows){
lines.push(pad(row.ts||'-',24)+' '+pad(row.seq??'-',5)+' '+pad(row.kind,12)+' '+pad(row.phase,12)+' '+pad(row.type,24)+' '+pad(row.commandId||'-',28)+' '+pad(row.sessionId||'-',18)+' '+pad(row.traceId||'-',18)+' '+short(row.summary,100));
}
const id=manifest.jobId||'<observer>';
lines.push('', 'NEXT');
lines.push(' turn summary: bun scripts/cli.ts web-probe observe collect '+id+' --view turn-summary');
lines.push(' trace frame: bun scripts/cli.ts web-probe observe collect '+id+' --view trace-frame --trace-id <traceId> --sample-seq <seq>');
lines.push(' command window: bun scripts/cli.ts web-probe observe collect '+id+' --view timeline --command-id <commandId>');
lines.push(' raw control grep: bun scripts/cli.ts web-probe observe collect '+id+' --view files --file control.jsonl --grep <commandId>');
lines.push('', 'DISCLOSURE valuesRedacted=true valuesPrinted=false source=existing observe artifacts');
return lines.join(' \\ n');
}
function timelineView(rows){
const commandFiles=readCommandFiles();
const anchor=timelineAnchor(rows,commandFiles);
const window=timelineWindow(anchor);
const eventRows=timelineEventRows(anchor,window,commandFiles);
const timelineRows=eventRows.rows.map((row)=>({ts:row.ts?short(row.ts,32):null,seq:row.seq??null,kind:row.kind,phase:short(row.phase,24),type:short(row.type,40),commandId:row.commandId?short(row.commandId,64):null,sessionId:row.sessionId?short(row.sessionId,64):null,traceId:row.traceId?short(row.traceId,64):null,summary:short(row.summary,140),valuesRedacted:true}));
return {anchor:{...anchor,from:isoOrNull(anchor.startMs),to:isoOrNull(anchor.endMs),startMs:undefined,endMs:undefined,anchorMs:undefined},window:{from:isoOrNull(window.fromMs),to:isoOrNull(window.toMs),windowMs:window.windowMs,anchorMode:window.anchorMode,valuesRedacted:true},counts:{control:control.length,samples:samples.length,commandFiles:commandFiles.length,matchedControl:eventRows.matchedControlCount,matchedSamples:eventRows.matchedSampleCount,matchedCommandFiles:eventRows.matchedCommandFileCount,shown:timelineRows.length,omitted:eventRows.omittedRowCount,valuesRedacted:true},timelineRows,renderedText:renderTimeline(anchor,window,eventRows),valuesRedacted:true};
}
function pathOnly(value){try{return value?new URL(String(value),'http://x').pathname:null}catch{return null}}
function projectSummaryFromSamples(){
const projectSamples=samples.filter((sample)=>sample?.projectManagement&&typeof sample.projectManagement==='object');
@@ -419,6 +597,11 @@ 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);
}
if(view==='timeline'){
const timeline=timelineView(rows);
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,...timeline,sourceFiles:['samples.jsonl','control.jsonl','commands/pending/*.json','commands/processing/*.json','commands/done/*.json','commands/failed/*.json','commands/abandoned/*.json'],valuesRedacted:true}));
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" ` ;