feat: 增加失败分析功能,改进问题规划和解决方案生成

This commit is contained in:
catlog22
2026-01-21 17:46:22 +08:00
parent 2084ff3e21
commit a7c8ea04f1
6 changed files with 364 additions and 22 deletions

View File

@@ -2587,6 +2587,7 @@ async function doneAction(queueItemId: string | undefined, options: IssueOptions
/**
* retry - Reset failed items to pending for re-execution
* Syncs failure details to Issue.feedback for planning phase
*/
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
let queues: Queue[];
@@ -2609,6 +2610,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
}
let totalUpdated = 0;
const updatedIssues = new Set<string>();
for (const queue of queues) {
const items = queue.solutions || queue.tasks || [];
@@ -2618,6 +2620,41 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
// Retry failed items only
if (item.status === 'failed') {
if (!issueId || item.issue_id === issueId) {
// Sync failure details to Issue.feedback (persistent for planning phase)
if (item.failure_details && item.issue_id) {
const issue = findIssue(item.issue_id);
if (issue) {
if (!issue.feedback) {
issue.feedback = [];
}
// Add failure to feedback history
issue.feedback.push({
type: 'failure',
stage: 'execute',
content: JSON.stringify({
solution_id: item.solution_id,
task_id: item.failure_details.task_id,
error_type: item.failure_details.error_type,
message: item.failure_details.message,
stack_trace: item.failure_details.stack_trace,
queue_id: queue.id,
item_id: item.item_id
}),
created_at: item.failure_details.timestamp
});
// Keep issue status as 'failed' (or optionally 'pending_replan')
// This signals to planning phase that this issue had failures
updateIssue(item.issue_id, {
status: 'failed',
updated_at: new Date().toISOString()
});
updatedIssues.add(item.issue_id);
}
}
// Preserve failure history before resetting
if (item.failure_details) {
if (!item.failure_history) {
@@ -2626,7 +2663,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
item.failure_history.push(item.failure_details);
}
// Reset for retry
// Reset QueueItem for retry (but Issue status remains 'failed')
item.status = 'pending';
item.failure_reason = undefined;
item.failure_details = undefined;
@@ -2659,11 +2696,10 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
return;
}
if (issueId) {
updateIssue(issueId, { status: 'queued' });
}
console.log(chalk.green(`✓ Reset ${totalUpdated} item(s) to pending (failure history preserved)`));
if (updatedIssues.size > 0) {
console.log(chalk.cyan(`✓ Synced failure details to ${updatedIssues.size} issue(s) for planning phase`));
}
}
// ============ Main Entry ============

View File

@@ -279,6 +279,58 @@
color: hsl(var(--destructive));
}
/* Issue Failure Info */
.issue-failure-info {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
background: hsl(var(--destructive) / 0.08);
border: 1px solid hsl(var(--destructive) / 0.2);
border-radius: 0.375rem;
border-left: 3px solid hsl(var(--destructive));
}
.issue-failure-info .failure-header {
display: flex;
align-items: center;
gap: 0.375rem;
color: hsl(var(--destructive));
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.issue-failure-info .failure-label {
text-transform: uppercase;
letter-spacing: 0.02em;
}
.issue-failure-info .failure-task {
font-family: var(--font-mono);
background: hsl(var(--destructive) / 0.15);
padding: 0 0.25rem;
border-radius: 0.25rem;
font-size: 0.6875rem;
}
.issue-failure-info .failure-message {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
line-height: 1.4;
}
.issue-failure-info .failure-type {
font-family: var(--font-mono);
color: hsl(var(--destructive) / 0.8);
font-weight: 500;
}
.issue-failure-info .failure-text {
word-break: break-word;
}
/* Priority Badges */
.issue-priority {
display: inline-flex;
@@ -2014,6 +2066,41 @@
border-left: 3px solid hsl(0 84% 60%);
}
/* Queue Item Failure Info */
.queue-item-failure {
display: flex;
align-items: center;
gap: 0.25rem;
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.1);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
max-width: 250px;
overflow: hidden;
}
.queue-item-failure i {
flex-shrink: 0;
}
.queue-item-failure .failure-type {
font-family: var(--font-mono);
font-weight: 500;
flex-shrink: 0;
}
.queue-item-failure .failure-msg {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: hsl(var(--muted-foreground));
}
/* Hide failure in parallel view to save space */
.queue-items.parallel .queue-item .queue-item-failure {
display: none;
}
/* Blocked - Purple/violet blocked state */
.queue-item.blocked {
border-color: hsl(262 83% 58%);

View File

@@ -431,10 +431,60 @@ function renderIssueCard(issue) {
<span>Archived on ${archivedDate}</span>
</div>
` : ''}
${renderFailureInfo(issue)}
</div>
`;
}
// Render failure information for failed issues
function renderFailureInfo(issue) {
// Check if issue has failure feedback
if (!issue.feedback || issue.feedback.length === 0) {
return '';
}
// Extract failure feedbacks
const failures = issue.feedback.filter(f => f.type === 'failure' && f.stage === 'execute');
if (failures.length === 0) {
return '';
}
// Get latest failure
const latestFailure = failures[failures.length - 1];
let failureDetail;
try {
failureDetail = JSON.parse(latestFailure.content);
} catch {
return '';
}
const errorMessage = failureDetail.message || 'Unknown error';
const errorType = failureDetail.error_type || 'error';
const taskId = failureDetail.task_id;
const failureCount = failures.length;
return `
<div class="issue-failure-info">
<div class="failure-header">
<i data-lucide="alert-circle" class="w-3.5 h-3.5"></i>
<span class="failure-label">${failureCount > 1 ? `Failed ${failureCount} times` : 'Execution Failed'}</span>
${taskId ? `<span class="failure-task">${taskId}</span>` : ''}
</div>
<div class="failure-message">
<span class="failure-type">${errorType}:</span>
<span class="failure-text" title="${escapeHtml(errorMessage)}">${escapeHtml(truncateText(errorMessage, 80))}</span>
</div>
</div>
`;
}
// Helper: Truncate text to max length
function truncateText(text, maxLength) {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
function renderPriorityStars(priority) {
const maxStars = 5;
let stars = '';
@@ -879,6 +929,7 @@ function renderQueueItemWithDelete(item, index, total, queueId) {
<i data-lucide="link" class="w-3 h-3"></i>
</span>
` : ''}
${renderQueueItemFailureInfo(item)}
<button class="queue-item-delete btn-icon" onclick="event.stopPropagation(); deleteQueueItem('${safeQueueId}', '${safeItemId}')" title="Delete item">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
@@ -886,6 +937,47 @@ function renderQueueItemWithDelete(item, index, total, queueId) {
`;
}
// Render failure info for queue items
function renderQueueItemFailureInfo(item) {
// Only show for failed items
if (item.status !== 'failed') {
return '';
}
// Check failure_details or failure_reason
const failureDetails = item.failure_details;
const failureReason = item.failure_reason;
if (!failureDetails && !failureReason) {
return '';
}
let errorType = 'error';
let errorMessage = 'Unknown error';
if (failureDetails) {
errorType = failureDetails.error_type || 'error';
errorMessage = failureDetails.message || 'Unknown error';
} else if (failureReason) {
// Try to parse as JSON
try {
const parsed = JSON.parse(failureReason);
errorType = parsed.error_type || 'error';
errorMessage = parsed.message || failureReason;
} catch {
errorMessage = failureReason;
}
}
return `
<span class="queue-item-failure text-xs" title="${escapeHtml(errorMessage)}">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
<span class="failure-type">${escapeHtml(errorType)}:</span>
<span class="failure-msg">${escapeHtml(truncateText(errorMessage, 40))}</span>
</span>
`;
}
async function deleteQueueItem(queueId, itemId) {
if (!confirm('Delete this item from queue?')) return;