feat(web): AI 意图规划置信度阈值与动作策略细化
- workbenchAiIntentPlannerModel 新增 WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD 与 isLowConfidenceTravelApplicationPlan,shouldRequestWorkbenchAiIntentPlan 增加业务关键词前置过滤 - resolveExecutableTravelApplicationPlan 区分 requestedSubmit 与提交确认(submitRequiresConfirmation),autoSubmit 不再直接置真 - workbenchIntentActionPolicy 改用 policyDecision 路由(need_confirmation/query_candidates),透传 riskLevel/requiresSelection/requiresConfirmation - workbenchIntentFrameModel 补充 query 动作识别,usePersonalWorkbenchAiMode/useWorkbenchAiActionRouter/useWorkbenchAiApplicationPreviewFlow 接入低置信度与确认流程 - 更新 intent-planner-model/intent-frame-model/application-gate-model/fast-preview 测试
This commit is contained in:
@@ -3,11 +3,12 @@ import { useSystemState } from '../useSystemState.js'
|
||||
import { useToast } from '../useToast.js'
|
||||
import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js'
|
||||
import { fetchSettings } from '../../services/settings.js'
|
||||
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
|
||||
import { calculateTravelReimbursement, fetchExpenseClaimDetail } from '../../services/reimbursements.js'
|
||||
import { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js'
|
||||
import {
|
||||
deleteAiWorkbenchConversation,
|
||||
loadAiWorkbenchConversationHistory,
|
||||
markAiWorkbenchConversationDocumentDeleted,
|
||||
saveAiWorkbenchConversation
|
||||
} from '../../utils/aiWorkbenchConversationStore.js'
|
||||
import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js'
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
import { useWorkbenchAiCommandIntents } from './useWorkbenchAiCommandIntents.js'
|
||||
import {
|
||||
buildRuleFallbackWorkbenchAiIntentPlan,
|
||||
isLowConfidenceTravelApplicationPlan,
|
||||
normalizeWorkbenchAiIntentPlan,
|
||||
resolveExecutableTravelApplicationPlan,
|
||||
shouldRequestWorkbenchAiIntentPlan
|
||||
@@ -558,6 +560,54 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
|
||||
function renderInlineConversationHtml(content) { return renderAiConversationHtml(content) }
|
||||
|
||||
function isUnavailableDocumentDetailError(error) {
|
||||
const message = String(error?.message || '').trim()
|
||||
return /not\s*found|不存在|已删除|不可访问|无权|forbidden|404/i.test(message)
|
||||
}
|
||||
|
||||
function resolveDocumentDetailLookupClaimId(detailRequest = {}) {
|
||||
return String(detailRequest.claimId || detailRequest.claim_id || '').trim()
|
||||
}
|
||||
|
||||
function markActiveAiDocumentDetailLinkDeleted(detailRequest = {}) {
|
||||
const nextHistory = markAiWorkbenchConversationDocumentDeleted(currentUser.value || {}, {
|
||||
claimId: detailRequest.claimId,
|
||||
claim_id: detailRequest.claimId,
|
||||
claimNo: detailRequest.claimNo,
|
||||
claim_no: detailRequest.claimNo,
|
||||
documentNo: detailRequest.documentNo,
|
||||
document_no: detailRequest.documentNo,
|
||||
id: detailRequest.id
|
||||
})
|
||||
emit('conversation-history-change', nextHistory)
|
||||
const activeConversation = nextHistory.find((item) => (
|
||||
String(item.id || item.conversationId || '').trim() === String(conversationId.value || '').trim()
|
||||
))
|
||||
if (activeConversation?.messages?.length) {
|
||||
conversationMessages.value = activeConversation.messages.map((message) => normalizeRuntimeMessage(message))
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAiDocumentDetailStillAvailable(detailRequest = {}) {
|
||||
const claimId = resolveDocumentDetailLookupClaimId(detailRequest)
|
||||
if (!claimId) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
await fetchExpenseClaimDetail(claimId)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (!isUnavailableDocumentDetailError(error)) {
|
||||
console.warn('AI document detail availability check failed, continuing navigation:', error)
|
||||
return true
|
||||
}
|
||||
markActiveAiDocumentDetailLinkDeleted(detailRequest)
|
||||
toast('该单据已经删除或不可访问,已将这条历史入口标记为不可查看。')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function buildInlinePromptText(rawPrompt, files = []) {
|
||||
const prompt = buildWorkbenchPromptText(rawPrompt)
|
||||
if (prompt) return prompt
|
||||
@@ -671,11 +721,70 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
pendingMessageId: plannerPendingMessage?.id,
|
||||
ontologyFields: travelApplicationRequest.ontologyFields,
|
||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
||||
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function startModelPlannedTravelApplicationConfirmation(travelApplicationRequest, plan, plannerPendingMessage) {
|
||||
const confirmText = buildLowConfidenceTravelApplicationConfirmationText(travelApplicationRequest, plan)
|
||||
const confirmAction = {
|
||||
label: '确认发起出差申请',
|
||||
description: '根据上面识别到的信息生成出差申请预览。',
|
||||
icon: 'mdi mdi-check-circle-outline',
|
||||
action_type: 'ai_application_confirm_intent',
|
||||
payload: {
|
||||
ontologyFields: travelApplicationRequest.ontologyFields,
|
||||
sourceText: travelApplicationRequest.sourceText,
|
||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
||||
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
||||
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
||||
}
|
||||
}
|
||||
replaceInlineMessage(plannerPendingMessage.id, createInlineMessage('assistant', confirmText, {
|
||||
id: plannerPendingMessage.id,
|
||||
suggestedActions: [confirmAction],
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: resolveInlineThinkingEvents(plannerPendingMessage).map((item) => ({ ...item, status: 'completed' }))
|
||||
}
|
||||
}))
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
}
|
||||
|
||||
function buildLowConfidenceTravelApplicationConfirmationText(request, plan) {
|
||||
const fields = request.ontologyFields || {}
|
||||
const summaryParts = []
|
||||
if (fields.time_range) {
|
||||
summaryParts.push(`时间:${fields.time_range}`)
|
||||
}
|
||||
if (fields.location) {
|
||||
summaryParts.push(`地点:${fields.location}`)
|
||||
}
|
||||
if (fields.reason) {
|
||||
summaryParts.push(`事由:${fields.reason}`)
|
||||
}
|
||||
if (fields.transport_mode) {
|
||||
summaryParts.push(`交通:${fields.transport_mode}`)
|
||||
}
|
||||
const summary = summaryParts.length ? `\n\n${summaryParts.join(';')}` : ''
|
||||
const confidenceNote = Number.isFinite(Number(plan?.confidence))
|
||||
? `(模型识别置信度较低,约 ${Math.round(Number(plan.confidence) * 100)}%)`
|
||||
: '(模型识别置信度较低)'
|
||||
return [
|
||||
'### 需要确认:您是要发起出差申请吗?',
|
||||
'',
|
||||
`小财管家把这句话理解成了“发起差旅申请”${confidenceNote},为避免误操作,先请您确认。`,
|
||||
summary,
|
||||
'',
|
||||
'点击下方「确认发起出差申请」即可继续;如果理解有误,请补充说明您的实际需求。'
|
||||
].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
async function executeModelPlannedWorkbenchIntent(cleanPrompt, entry = {}, files = []) {
|
||||
let intentPlan = null
|
||||
let modelPlan = null
|
||||
@@ -703,6 +812,10 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
|
||||
const travelApplicationRequest = resolveExecutableTravelApplicationPlan(intentPlan)
|
||||
if (travelApplicationRequest) {
|
||||
if (isLowConfidenceTravelApplicationPlan(intentPlan)) {
|
||||
startModelPlannedTravelApplicationConfirmation(travelApplicationRequest, intentPlan, plannerPendingMessage)
|
||||
return
|
||||
}
|
||||
startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage)
|
||||
return
|
||||
}
|
||||
@@ -722,7 +835,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files, { pendingMessageId: plannerPendingMessage.id })
|
||||
}
|
||||
|
||||
function handleAiAnswerMarkdownClick(event) {
|
||||
async function handleAiAnswerMarkdownClick(event) {
|
||||
const target = event?.target
|
||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||
if (!link) {
|
||||
@@ -735,7 +848,11 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('open-document', buildAiDocumentDetailRequest(detailReference))
|
||||
const detailRequest = buildAiDocumentDetailRequest(detailReference)
|
||||
if (!(await ensureAiDocumentDetailStillAvailable(detailRequest))) {
|
||||
return
|
||||
}
|
||||
emit('open-document', detailRequest)
|
||||
}
|
||||
|
||||
function startInlineConversation(prompt, entry = {}, files = []) {
|
||||
|
||||
Reference in New Issue
Block a user