-
-
-
- 操作:
- {{ operation }}
-
-
-
-
-
-
-
-
+
+ @click="setActiveQuestion(questionIndex)"
+ >
+ {{ questionIndex + 1 }}
+
+
+
+
+
+
+
+ 操作:
+ {{ activeQuestion.operation }}
+
+
+
-
@@ -69,31 +92,111 @@
diff --git a/web/src/composables/useApproval.js b/web/src/composables/useApproval.js
index ac69b0a8..a0a1a951 100644
--- a/web/src/composables/useApproval.js
+++ b/web/src/composables/useApproval.js
@@ -2,55 +2,56 @@ import { reactive } from 'vue'
import { message } from 'ant-design-vue'
import { handleChatError } from '@/utils/errorHandler'
import { agentApi } from '@/apis'
-
-const normalizeOptions = (rawOptions) => {
- if (!Array.isArray(rawOptions)) return []
- return rawOptions
- .map((item) => {
- if (item && typeof item === 'object') {
- const label = String(item.label || item.value || '').trim()
- const value = String(item.value || item.label || '').trim()
- return label && value ? { label, value } : null
- }
- const text = String(item || '').trim()
- return text ? { label: text, value: text } : null
- })
- .filter(Boolean)
-}
+import { normalizeQuestions, buildLegacyQuestion } from '@/utils/questionUtils'
const extractQuestionPayload = (chunk) => {
const interruptInfo = chunk?.interrupt_info || {}
- const rawOptions = chunk?.options || interruptInfo?.options || []
- const options = normalizeOptions(rawOptions)
- const operation = chunk?.operation || interruptInfo?.operation || ''
-
+ const rawQuestions = chunk?.questions || interruptInfo?.questions || []
const source = chunk?.source || interruptInfo?.source || 'interrupt'
- const multiSelect = Boolean(chunk?.multi_select ?? interruptInfo?.multi_select ?? false)
- const allowOther = Boolean(chunk?.allow_other ?? interruptInfo?.allow_other ?? true)
- const questionId = chunk?.question_id || interruptInfo?.question_id || ''
- const question = chunk?.question || interruptInfo?.question || '请选择一个选项'
+ let questions = normalizeQuestions(rawQuestions)
+
+ if (!questions.length) {
+ const legacyQuestion = buildLegacyQuestion(chunk, interruptInfo)
+ if (legacyQuestion) {
+ questions = [legacyQuestion]
+ }
+ }
return {
- questionId,
- question,
- options,
- multiSelect,
- allowOther,
- source,
- operation
+ questions,
+ source
}
}
+const parseApprovedDecision = (answer) => {
+ const parseFromValue = (value) => {
+ if (typeof value === 'boolean') return value
+ if (typeof value === 'string') {
+ const normalized = value.trim().toLowerCase()
+ if (normalized === 'approve' || normalized === 'approved' || normalized === 'true') return true
+ if (normalized === 'reject' || normalized === 'rejected' || normalized === 'false') return false
+ }
+ return null
+ }
+
+ const direct = parseFromValue(answer)
+ if (direct !== null) return direct
+
+ if (answer && typeof answer === 'object' && !Array.isArray(answer)) {
+ const values = Object.values(answer)
+ if (values.length === 1) {
+ return parseFromValue(values[0])
+ }
+ }
+
+ return null
+}
+
export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessages }) {
const approvalState = reactive({
showModal: false,
- questionId: '',
- question: '',
- operation: '',
- options: [],
- multiSelect: false,
- allowOther: true,
- source: '',
+ questions: [],
+ status: '',
threadId: null
})
@@ -82,10 +83,20 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
const requestBody = {
thread_id: threadId,
- answer,
config: agentConfigId ? { agent_config_id: agentConfigId } : {}
}
+ if (approvalState.status === 'human_approval_required') {
+ const approved = parseApprovedDecision(answer)
+ if (approved !== null) {
+ requestBody.approved = approved
+ } else {
+ requestBody.answer = answer
+ }
+ } else {
+ requestBody.answer = answer
+ }
+
try {
const response = await agentApi.resumeAgentChat(currentAgentId, requestBody, {
signal: threadState.streamAbortController?.signal
@@ -120,17 +131,13 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
if (!threadState) return false
const payload = extractQuestionPayload(chunk)
+ if (!payload.questions.length) return false
threadState.isStreaming = false
approvalState.showModal = true
- approvalState.questionId = payload.questionId
- approvalState.question = payload.question
- approvalState.operation = payload.operation
- approvalState.options = payload.options
- approvalState.multiSelect = payload.multiSelect
- approvalState.allowOther = payload.allowOther
- approvalState.source = payload.source
+ approvalState.questions = payload.questions
+ approvalState.status = chunk.status || ''
approvalState.threadId = chunk.thread_id || threadId
fetchThreadMessages({ agentId: currentAgentId, threadId })
@@ -140,13 +147,8 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
const resetApprovalState = () => {
approvalState.showModal = false
- approvalState.questionId = ''
- approvalState.question = ''
- approvalState.operation = ''
- approvalState.options = []
- approvalState.multiSelect = false
- approvalState.allowOther = true
- approvalState.source = ''
+ approvalState.questions = []
+ approvalState.status = ''
approvalState.threadId = null
}
diff --git a/web/src/utils/questionUtils.js b/web/src/utils/questionUtils.js
new file mode 100644
index 00000000..62ef4b9d
--- /dev/null
+++ b/web/src/utils/questionUtils.js
@@ -0,0 +1,96 @@
+/**
+ * 问题和选项规范化工具
+ */
+
+const DEFAULT_OTHER_OPTION_VALUE = '__other__'
+
+/**
+ * 判断选项是否为"其他"选项
+ */
+export const isOtherOption = (option) => {
+ if (!option || typeof option !== 'object') return false
+ const label = String(option.label || '').trim().toLowerCase()
+ const value = String(option.value || '').trim().toLowerCase()
+
+ return (
+ value === DEFAULT_OTHER_OPTION_VALUE ||
+ value === 'other' ||
+ label.includes('其他') ||
+ label.includes('other')
+ )
+}
+
+/**
+ * 规范化选项列表
+ */
+export const normalizeOptions = (rawOptions) => {
+ if (!Array.isArray(rawOptions)) return []
+
+ return rawOptions
+ .map((item) => {
+ if (item && typeof item === 'object') {
+ const label = String(item.label || item.value || '').trim()
+ const value = String(item.value || item.label || '').trim()
+ return label && value ? { label, value } : null
+ }
+
+ const text = String(item || '').trim()
+ return text ? { label: text, value: text } : null
+ })
+ .filter(Boolean)
+}
+
+/**
+ * 规范化问题列表
+ */
+export const normalizeQuestions = (rawQuestions) => {
+ if (!Array.isArray(rawQuestions)) return []
+
+ return rawQuestions
+ .map((item, index) => {
+ if (!item || typeof item !== 'object') return null
+
+ const question = String(item.question || '').trim()
+ if (!question) return null
+
+ const questionId = String(item.questionId || item.question_id || '').trim() || `q-${index + 1}`
+ const operation = String(item.operation || '').trim()
+ const allowOther = Boolean(item.allowOther ?? item.allow_other ?? true)
+ const baseOptions = normalizeOptions(item.options || [])
+ const hasOtherOption = baseOptions.some((option) => isOtherOption(option))
+ const options = allowOther && !hasOtherOption
+ ? [...baseOptions, { label: '其他', value: DEFAULT_OTHER_OPTION_VALUE }]
+ : baseOptions
+
+ return {
+ questionId,
+ question,
+ options,
+ multiSelect: Boolean(item.multiSelect ?? item.multi_select ?? false),
+ allowOther,
+ operation
+ }
+ })
+ .filter(Boolean)
+}
+
+/**
+ * 从旧格式构建单个问题(向后兼容)
+ */
+export const buildLegacyQuestion = (chunk, interruptInfo) => {
+ const question = String(chunk?.question || interruptInfo?.question || '').trim()
+ if (!question) return null
+
+ const operation = String(chunk?.operation || interruptInfo?.operation || '').trim()
+
+ return {
+ questionId: String(chunk?.question_id || interruptInfo?.question_id || '').trim() || 'q-1',
+ question,
+ options: normalizeOptions(chunk?.options || interruptInfo?.options || []),
+ multiSelect: Boolean(chunk?.multi_select ?? interruptInfo?.multi_select ?? false),
+ allowOther: Boolean(chunk?.allow_other ?? interruptInfo?.allow_other ?? true),
+ operation
+ }
+}
+
+export { DEFAULT_OTHER_OPTION_VALUE }