feat: add approve/reject buttons for wait_for_approval
- CommentSection shows explicit approve/reject buttons when waiting - Reject aborts the workflow, approve continues with optional feedback - Backend parses approved:/rejected: prefixes from comment content
This commit is contained in:
parent
938ba83f37
commit
47546a9d15
29
src/agent.rs
29
src/agent.rs
@ -1177,7 +1177,27 @@ async fn run_agent_loop(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("[workflow {}] Approval received: {}", workflow_id, approval_content);
|
tracing::info!("[workflow {}] Approval response: {}", workflow_id, approval_content);
|
||||||
|
|
||||||
|
// Check if user rejected
|
||||||
|
if approval_content.starts_with("rejected:") {
|
||||||
|
let reason = approval_content.strip_prefix("rejected:").unwrap_or("").trim();
|
||||||
|
tracing::info!("[workflow {}] User rejected: {}", workflow_id, reason);
|
||||||
|
if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) {
|
||||||
|
step.status = StepStatus::Failed;
|
||||||
|
step.user_feedbacks.push(format!("用户终止: {}", reason));
|
||||||
|
}
|
||||||
|
log_execution(pool, broadcast_tx, workflow_id, cur, "wait_for_approval", "rejected", reason, "failed").await;
|
||||||
|
// Return error to end the agent loop; caller sets workflow to "failed"
|
||||||
|
return Err(anyhow::anyhow!("用户终止了执行: {}", reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approved — extract feedback after "approved:" prefix if present
|
||||||
|
let feedback = if approval_content.starts_with("approved:") {
|
||||||
|
approval_content.strip_prefix("approved:").unwrap_or("").trim().to_string()
|
||||||
|
} else {
|
||||||
|
approval_content.clone()
|
||||||
|
};
|
||||||
|
|
||||||
// Resume: restore Running status
|
// Resume: restore Running status
|
||||||
if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) {
|
if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) {
|
||||||
@ -1197,8 +1217,13 @@ async fn run_agent_loop(
|
|||||||
steps: plan_infos_from_state(&state),
|
steps: plan_infos_from_state(&state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tool_msg = if feedback.is_empty() {
|
||||||
|
"用户已确认,继续执行。".to_string()
|
||||||
|
} else {
|
||||||
|
format!("用户已确认。反馈: {}", feedback)
|
||||||
|
};
|
||||||
state.current_step_chat_history.push(
|
state.current_step_chat_history.push(
|
||||||
ChatMessage::tool_result(&tc.id, &format!("用户已确认。反馈: {}", approval_content))
|
ChatMessage::tool_result(&tc.id, &tool_msg)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,12 +15,8 @@ const emit = defineEmits<{
|
|||||||
const input = ref('')
|
const input = ref('')
|
||||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
function submit() {
|
function buildText(): string {
|
||||||
if (props.disabled) return
|
|
||||||
const text = input.value.trim()
|
const text = input.value.trim()
|
||||||
if (!text && !props.quotes.length) return
|
|
||||||
|
|
||||||
// Build final text: quotes as block references, then user text
|
|
||||||
let final = ''
|
let final = ''
|
||||||
for (const q of props.quotes) {
|
for (const q of props.quotes) {
|
||||||
final += `> ${q}\n`
|
final += `> ${q}\n`
|
||||||
@ -29,8 +25,27 @@ function submit() {
|
|||||||
final += '\n'
|
final += '\n'
|
||||||
}
|
}
|
||||||
final += text
|
final += text
|
||||||
|
return final.trim()
|
||||||
|
}
|
||||||
|
|
||||||
emit('submit', final.trim())
|
function submit() {
|
||||||
|
if (props.disabled) return
|
||||||
|
const text = input.value.trim()
|
||||||
|
if (!text && !props.quotes.length) return
|
||||||
|
|
||||||
|
emit('submit', buildText())
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function approve() {
|
||||||
|
const feedback = buildText()
|
||||||
|
emit('submit', feedback ? `approved: ${feedback}` : 'approved:')
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function reject() {
|
||||||
|
const feedback = buildText()
|
||||||
|
emit('submit', feedback ? `rejected: ${feedback}` : 'rejected: 用户终止')
|
||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +68,7 @@ defineExpose({ focusInput })
|
|||||||
<template>
|
<template>
|
||||||
<div class="comment-section" :class="{ 'waiting-approval': waitingApproval }">
|
<div class="comment-section" :class="{ 'waiting-approval': waitingApproval }">
|
||||||
<div v-if="waitingApproval" class="approval-banner">
|
<div v-if="waitingApproval" class="approval-banner">
|
||||||
⏳ Agent 正在等待你的确认,请在下方输入反馈后发送
|
⏳ Agent 正在等待你的确认
|
||||||
</div>
|
</div>
|
||||||
<div v-if="quotes.length" class="quotes-bar">
|
<div v-if="quotes.length" class="quotes-bar">
|
||||||
<div v-for="(q, i) in quotes" :key="i" class="quote-chip">
|
<div v-for="(q, i) in quotes" :key="i" class="quote-chip">
|
||||||
@ -65,11 +80,15 @@ defineExpose({ focusInput })
|
|||||||
<textarea
|
<textarea
|
||||||
ref="textareaRef"
|
ref="textareaRef"
|
||||||
v-model="input"
|
v-model="input"
|
||||||
:placeholder="quotes.length ? '添加你的评论...' : '输入反馈或调整指令... (Ctrl+Enter 发送)'"
|
:placeholder="waitingApproval ? '可选:附加反馈或修改意见...' : (quotes.length ? '添加你的评论...' : '输入反馈或调整指令... (Ctrl+Enter 发送)')"
|
||||||
rows="3"
|
rows="3"
|
||||||
@keydown="onKeydown"
|
@keydown="onKeydown"
|
||||||
/>
|
/>
|
||||||
<button class="btn-send" :disabled="disabled || (!input.trim() && !quotes.length)" @click="submit">发送</button>
|
<div v-if="waitingApproval" class="approval-buttons">
|
||||||
|
<button class="btn-approve" @click="approve">继续执行</button>
|
||||||
|
<button class="btn-reject" @click="reject">终止</button>
|
||||||
|
</div>
|
||||||
|
<button v-else class="btn-send" :disabled="disabled || (!input.trim() && !quotes.length)" @click="submit">发送</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -178,4 +197,40 @@ defineExpose({ focusInput })
|
|||||||
.btn-send:hover {
|
.btn-send:hover {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.approval-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve {
|
||||||
|
background: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve:hover {
|
||||||
|
background: #43a047;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--error, #ef5350);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 20px;
|
||||||
|
border: 1px solid var(--error, #ef5350);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:hover {
|
||||||
|
background: rgba(239, 83, 80, 0.1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user