fix(bridge): resolve missing Discord embed bodies by extracting detailed RPC payload from step_probe
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
|
||||
---
|
||||
|
||||
### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload
|
||||
- **증상**: 대규모 UI 마이그레이션 후, 디스코드 승인 메시지 본문에 실행할 코드/명령어가 완전히 누락되고 "Step #15"와 같은 디폴트 텍스트만 전송됨.
|
||||
- **원인**: Native UI 변경으로 인해 DOM observer가 추출한 버튼 텍스트("Always run")가 `http-bridge.ts` 필터 우회 및 bot.py에서 지연(defer) 처리됨. 반면 `step-probe.ts`가 `GetAllCascadeTrajectories` 폴링을 통해 동시에 발생시킨 dummy pending payload (명령어 상세 내용이 없이 `Step #XX` 라는 텍스트만 포함)가 봇에 의해 먼저 자동 승인되면서 정작 실제 코드 영역 정보가 증발함.
|
||||
- **해결**: `step-probe.ts` 내에 `formatStepProbeCommand` 헬퍼 함수를 추가하여, WAITING 상태 스텝의 `argumentsJson` 데이터를 직접 파싱하고 `CommandLine`, `TargetFile` 등 실제 명령어와 상세 인자/코드를 `command`와 `description`으로 할당하여 브릿지로 넘기도록 패치함. DOM 옵저버의 불안정성과 관계없이 일관된 본문 전달 보장.
|
||||
- **주의**: UI 스크래핑에 의존하는 DOM Observer 방식은 UI 레이아웃, 아이콘 삽입 등에 취약하므로, 상세 페이로드 추출은 항상 100% 신뢰 가능한 SDK RPC(`step-probe.ts`) 데이터를 우선 사용하도록 구성해야 함.
|
||||
|
||||
## 포맷
|
||||
|
||||
### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
| 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ |
|
||||
| 003 | 23:15 | Native UI 아이콘 글루잉 대응 스캐너 픽스 (DOM Regex 매칭 강화) | `HEAD` | ✅ |
|
||||
| 004 | 00:10 | Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단) | "HEAD" | ✅ |
|
||||
| 005 | 23:00 | fix: Resolve empty Discord embed body by populating detailed step-probe payload | \\ | ? |
|
||||
|
||||
@@ -213,7 +213,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
}
|
||||
// "Run" button → step_probe handles these with full command detail
|
||||
// Only let through if session is stalled AND step_probe hasn't created a pending yet
|
||||
if (/^Run\b/i.test(cmd)) {
|
||||
if (/^(?:Always\s*)?Run\b/i.test(cmd)) {
|
||||
if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) {
|
||||
ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
@@ -102,6 +102,36 @@ export function resetPendingStateForReconnect(): void {
|
||||
|
||||
// handleDiffReviewResponse → moved to ./approval-handler.ts
|
||||
|
||||
export function formatStepProbeCommand(toolName: string, actualIndex: number, stepType: string, toolCall: any): { cmd: string, desc: string, isSafe: boolean } {
|
||||
let cmd = toolName;
|
||||
let desc = `Step #${actualIndex} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
|
||||
let isSafe = false;
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
isSafe = args.SafeToAutoRun === true;
|
||||
if (args.CommandLine) {
|
||||
cmd = toolName;
|
||||
desc = args.CommandLine;
|
||||
} else if (args.TargetFile) {
|
||||
cmd = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`;
|
||||
if (args.CodeContent) desc = args.CodeContent;
|
||||
else if (args.ReplacementChunks) desc = JSON.stringify(args.ReplacementChunks, null, 2);
|
||||
else desc = toolCall.argumentsJson;
|
||||
} else {
|
||||
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
|
||||
if (val) {
|
||||
cmd = toolName;
|
||||
desc = String(val);
|
||||
} else {
|
||||
cmd = `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
return { cmd, desc, isSafe };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a registration file for the Bot to discover session → project mapping.
|
||||
* Called automatically on first step event per session.
|
||||
@@ -481,20 +511,8 @@ function setupMonitor() {
|
||||
if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
||||
const toolCall = oStep?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
let command = toolName;
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
|
||||
else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`;
|
||||
else {
|
||||
// Show first meaningful value (path, query, etc.)
|
||||
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
|
||||
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
} catch { command = toolName; }
|
||||
}
|
||||
const actualIndex = offset + osi;
|
||||
const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall);
|
||||
ctx.logToFile(`[STEP-PROBE] ★ WAITING (via offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
|
||||
if (actualIndex !== ctx.lastPendingStepIndex) {
|
||||
ctx.stallProbed = true;
|
||||
@@ -512,13 +530,14 @@ function setupMonitor() {
|
||||
writePendingApproval({
|
||||
conversation_id: ctx.activeSessionId,
|
||||
command,
|
||||
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
||||
description: detailedDescription,
|
||||
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
||||
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
||||
: ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent'
|
||||
: toolName,
|
||||
step_index: actualIndex,
|
||||
source: 'step_probe_offset',
|
||||
safe_to_auto_run: isSafeToAutoRun,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -543,27 +562,9 @@ function setupMonitor() {
|
||||
// Extract command from metadata.toolCall or direct fields
|
||||
const toolCall = step?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
let command = toolName;
|
||||
|
||||
let isSafeToAutoRun = false;
|
||||
const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, si, stepType, toolCall);
|
||||
|
||||
// Parse argumentsJson for command details
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
isSafeToAutoRun = args.SafeToAutoRun === true;
|
||||
if (args.CommandLine) {
|
||||
command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
|
||||
} else if (args.TargetFile) {
|
||||
command = `${toolName}: ${args.TargetFile}`;
|
||||
} else {
|
||||
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
|
||||
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
} catch { command = toolName; }
|
||||
}
|
||||
|
||||
const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
|
||||
ctx.logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`);
|
||||
|
||||
if (si !== ctx.lastPendingStepIndex) {
|
||||
@@ -629,21 +630,8 @@ function setupMonitor() {
|
||||
foundWaitingInOffset = true;
|
||||
const toolCall = oStep?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
let command = toolName;
|
||||
let isSafeToAutoRun = false;
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
isSafeToAutoRun = args.SafeToAutoRun === true;
|
||||
if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
|
||||
else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`;
|
||||
else {
|
||||
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
|
||||
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
} catch { command = toolName; }
|
||||
}
|
||||
const actualIndex = utf8Offset + osi;
|
||||
const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall);
|
||||
ctx.logToFile(`[STEP-PROBE] ★ WAITING (via UTF-8 offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
|
||||
if (actualIndex !== ctx.lastPendingStepIndex) {
|
||||
ctx.stallProbed = true;
|
||||
@@ -654,7 +642,7 @@ function setupMonitor() {
|
||||
writePendingApproval({
|
||||
conversation_id: ctx.activeSessionId,
|
||||
command,
|
||||
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
||||
description: detailedDescription,
|
||||
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
||||
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
||||
: ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent'
|
||||
|
||||
47
test.js
Normal file
47
test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const jsdom = require('jsdom');
|
||||
const { JSDOM } = jsdom;
|
||||
const dom = new JSDOM($html);
|
||||
const document = dom.window.document;
|
||||
|
||||
function extractContext(b){
|
||||
var curr = b.parentElement;
|
||||
var bestDesc = '';
|
||||
var btnText = (b.innerText || b.textContent || '').trim();
|
||||
|
||||
for (var i = 0; i < 15 && curr; i++) {
|
||||
var codeEl = curr.querySelector('pre, code, [class*="command"], [class*="terminal"], [class*="code"]');
|
||||
if (codeEl && codeEl !== b && !b.contains(codeEl)) {
|
||||
var codeText = (codeEl.innerText || codeEl.textContent || '').trim();
|
||||
if (codeText.length > 0 && codeText !== btnText) {
|
||||
return codeText.substring(0, 500);
|
||||
}
|
||||
}
|
||||
|
||||
var full = (curr.innerText || curr.textContent || '');
|
||||
var btnRawText = (b.textContent || '');
|
||||
var desc = full.replace(btnRawText, '').trim();
|
||||
if (desc.length > 5 && desc !== btnText && bestDesc.length < desc.length) {
|
||||
bestDesc = desc;
|
||||
}
|
||||
|
||||
var cname = curr.className;
|
||||
if (typeof cname === 'string' && (cname.includes('message') || cname.includes('step') || cname.includes('markdown'))) {
|
||||
break;
|
||||
}
|
||||
curr = curr.parentElement;
|
||||
}
|
||||
return bestDesc.substring(0, 500);
|
||||
}
|
||||
|
||||
const btns = document.querySelectorAll('button');
|
||||
let ran = false;
|
||||
for(let b of btns) {
|
||||
let t = (b.textContent||'').trim();
|
||||
if(t === 'Always run' || t === 'Run') {
|
||||
const desc = extractContext(b);
|
||||
console.log("Found button: " + t);
|
||||
console.log("Extracted Description: " + desc);
|
||||
ran = true;
|
||||
}
|
||||
}
|
||||
if(!ran) console.log("No matching button found");
|
||||
Reference in New Issue
Block a user