diff --git a/scripts/src/hwlab-node-web-observe-collect.ts b/scripts/src/hwlab-node-web-observe-collect.ts index 8cf4fd5e..da935ee0 100644 --- a/scripts/src/hwlab-node-web-observe-collect.ts +++ b/scripts/src/hwlab-node-web-observe-collect.ts @@ -46,6 +46,20 @@ function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFin 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 completedNewSessionIdsBefore(ts){ + const limit=tsMs(ts); + return control + .filter((item)=>item.type==='newSession'&&item.phase==='completed') + .filter((item)=>limit===null||tsMs(item.ts)===null||tsMs(item.ts)<=limit) + .map(commandSessionId) + .filter(Boolean); +} +function authoritativeSessionIdForPrompts(prompts){ + const firstPromptTs=prompts[0]?.firstTs||null; + const newSessionIds=completedNewSessionIdsBefore(firstPromptTs); + const promptSessionIds=unique(prompts.map((item)=>item.sessionId)); + return newSessionIds.slice(-1)[0]||promptSessionIds[0]||null; +} function promptCommands(){ const map=new Map(); for(const item of control){ @@ -55,8 +69,7 @@ function promptCommands(){ 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; + const sessionId=authoritativeSessionIdForPrompts(prompts); if(!sessionId) return prompts; const scoped=prompts.filter((item)=>item.sessionId===sessionId); return scoped.length>0?scoped:prompts; @@ -264,12 +277,16 @@ function turnSummaryRows(){ 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}; + return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,sessionId:prompt.sessionId||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 prompts=promptCommands(); + const anchorSessionId=authoritativeSessionIdForPrompts(prompts)||rows.find((row)=>row.sessionId)?.sessionId||samples[0]?.routeSessionId||samples[0]?.activeSessionId||samples[samples.length-1]?.routeSessionId||samples[samples.length-1]?.activeSessionId||'-'; + const observedSessions=unique(samples.flatMap((sample)=>[sample.routeSessionId,sample.activeSessionId])); + const driftSuffix=observedSessions.length>1?' sessionDrift='+observedSessions.length:''; + const title='Workbench Session '+anchorSessionId+' observer '+(manifest.jobId||'-')+driftSuffix; 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||'')+' --view trace-frame --trace-id --sample-seq '].join('\\n'); diff --git a/scripts/src/hwlab-node-web-observe-runner-actions-source.ts b/scripts/src/hwlab-node-web-observe-runner-actions-source.ts index 6266603f..a7fdb87f 100644 --- a/scripts/src/hwlab-node-web-observe-runner-actions-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-actions-source.ts @@ -16,16 +16,40 @@ async function cancelRunningTurn() { await page.keyboard.press("Backspace").catch(() => {}); } } + const cancelSelectors = [ + 'button:has-text("取消")', + 'button:has-text("Cancel")', + '[data-testid*="cancel" i]', + '[aria-label*="cancel" i]', + '[aria-label*="取消"]' + ].join(", "); const primaryButton = page.locator('#command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]').last(); - const button = await primaryButton.isVisible().catch(() => false) - ? primaryButton - : page.locator([ - 'button:has-text("取消")', - 'button:has-text("Cancel")', - '[data-testid*="cancel" i]', - '[aria-label*="cancel" i]', - '[aria-label*="取消"]' - ].join(", ")).last(); + const explicitCancelButton = page.locator(cancelSelectors).last(); + let button = null; + if (await primaryButton.isVisible().catch(() => false)) { + const primaryLooksCancelable = await primaryButton.evaluate((element) => { + const parts = [ + element.textContent || "", + element.getAttribute("aria-label") || "", + element.getAttribute("title") || "", + element.getAttribute("data-testid") || "", + element.getAttribute("data-state") || "", + element.getAttribute("data-action") || "", + element.className || "" + ]; + return /cancel|取消|停止|stop|abort/i.test(parts.join(" ")); + }).catch(() => false); + if (primaryLooksCancelable) button = primaryButton; + } + if (button === null && await explicitCancelButton.isVisible().catch(() => false)) button = explicitCancelButton; + if (button === null) { + return { + beforeUrl, + afterUrl: currentPageUrl(), + cancelSubmit: { status: null, statusText: null, skipped: true, reason: "cancel-button-not-visible", responseObserved: false }, + pageId + }; + } await button.waitFor({ state: "visible", timeout: 15000 }); const cancelResponsePromise = page.waitForResponse((response) => { const request = response.request();