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 { useToast } from '../useToast.js'
|
||||||
import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js'
|
import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js'
|
||||||
import { fetchSettings } from '../../services/settings.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 { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js'
|
||||||
import {
|
import {
|
||||||
deleteAiWorkbenchConversation,
|
deleteAiWorkbenchConversation,
|
||||||
loadAiWorkbenchConversationHistory,
|
loadAiWorkbenchConversationHistory,
|
||||||
|
markAiWorkbenchConversationDocumentDeleted,
|
||||||
saveAiWorkbenchConversation
|
saveAiWorkbenchConversation
|
||||||
} from '../../utils/aiWorkbenchConversationStore.js'
|
} from '../../utils/aiWorkbenchConversationStore.js'
|
||||||
import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js'
|
import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js'
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
import { useWorkbenchAiCommandIntents } from './useWorkbenchAiCommandIntents.js'
|
import { useWorkbenchAiCommandIntents } from './useWorkbenchAiCommandIntents.js'
|
||||||
import {
|
import {
|
||||||
buildRuleFallbackWorkbenchAiIntentPlan,
|
buildRuleFallbackWorkbenchAiIntentPlan,
|
||||||
|
isLowConfidenceTravelApplicationPlan,
|
||||||
normalizeWorkbenchAiIntentPlan,
|
normalizeWorkbenchAiIntentPlan,
|
||||||
resolveExecutableTravelApplicationPlan,
|
resolveExecutableTravelApplicationPlan,
|
||||||
shouldRequestWorkbenchAiIntentPlan
|
shouldRequestWorkbenchAiIntentPlan
|
||||||
@@ -558,6 +560,54 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
|
|
||||||
function renderInlineConversationHtml(content) { return renderAiConversationHtml(content) }
|
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 = []) {
|
function buildInlinePromptText(rawPrompt, files = []) {
|
||||||
const prompt = buildWorkbenchPromptText(rawPrompt)
|
const prompt = buildWorkbenchPromptText(rawPrompt)
|
||||||
if (prompt) return prompt
|
if (prompt) return prompt
|
||||||
@@ -671,11 +721,70 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
pendingMessageId: plannerPendingMessage?.id,
|
pendingMessageId: plannerPendingMessage?.id,
|
||||||
ontologyFields: travelApplicationRequest.ontologyFields,
|
ontologyFields: travelApplicationRequest.ontologyFields,
|
||||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
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 = []) {
|
async function executeModelPlannedWorkbenchIntent(cleanPrompt, entry = {}, files = []) {
|
||||||
let intentPlan = null
|
let intentPlan = null
|
||||||
let modelPlan = null
|
let modelPlan = null
|
||||||
@@ -703,6 +812,10 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
|
|
||||||
const travelApplicationRequest = resolveExecutableTravelApplicationPlan(intentPlan)
|
const travelApplicationRequest = resolveExecutableTravelApplicationPlan(intentPlan)
|
||||||
if (travelApplicationRequest) {
|
if (travelApplicationRequest) {
|
||||||
|
if (isLowConfidenceTravelApplicationPlan(intentPlan)) {
|
||||||
|
startModelPlannedTravelApplicationConfirmation(travelApplicationRequest, intentPlan, plannerPendingMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage)
|
startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -722,7 +835,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files, { pendingMessageId: plannerPendingMessage.id })
|
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files, { pendingMessageId: plannerPendingMessage.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAiAnswerMarkdownClick(event) {
|
async function handleAiAnswerMarkdownClick(event) {
|
||||||
const target = event?.target
|
const target = event?.target
|
||||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||||
if (!link) {
|
if (!link) {
|
||||||
@@ -735,7 +848,11 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
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 = []) {
|
function startInlineConversation(prompt, entry = {}, files = []) {
|
||||||
|
|||||||
@@ -80,6 +80,19 @@ export function useWorkbenchAiActionRouter({
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (actionType === 'ai_application_confirm_intent') {
|
||||||
|
aiExpenseDraft.value = null
|
||||||
|
void applicationFlow.startAiApplicationPreview('travel', '差旅费', String(actionPayload.sourceText || '').trim(), {
|
||||||
|
userMessage: String(actionPayload.sourceText || '').trim() || '确认发起出差申请',
|
||||||
|
pushUserMessage: true,
|
||||||
|
ontologyFields: actionPayload.ontologyFields || {},
|
||||||
|
autoSubmit: Boolean(actionPayload.autoSubmit),
|
||||||
|
autoSaveDraft: Boolean(actionPayload.autoSaveDraft),
|
||||||
|
requestedSubmit: Boolean(actionPayload.requestedSubmit),
|
||||||
|
submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (actionType === 'open_application_detail') {
|
if (actionType === 'open_application_detail') {
|
||||||
const claimNo = String(actionPayload.claim_no || actionPayload.claimNo || '').trim()
|
const claimNo = String(actionPayload.claim_no || actionPayload.claimNo || '').trim()
|
||||||
const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim()
|
const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim()
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
|||||||
if (normalized.validationIssues?.length || normalized.missingFields?.length) {
|
if (normalized.validationIssues?.length || normalized.missingFields?.length) {
|
||||||
return buildApplicationPreviewFooterMessage(normalized)
|
return buildApplicationPreviewFooterMessage(normalized)
|
||||||
}
|
}
|
||||||
|
if (message?.submitRequiresConfirmation) {
|
||||||
|
return '已识别到您希望直接提交。系统不会自动提交申请,请先核对申请核对表;确认无误后,点击下方“直接提交”按钮再进入提交确认。'
|
||||||
|
}
|
||||||
return '申请核对表已补齐,费用测算已同步。您仍可点击表格继续修改;确认无误后,可以点击下方按钮保存草稿或直接提交,也可以直接回复“保存草稿”或“提交”。'
|
return '申请核对表已补齐,费用测算已同步。您仍可点击表格继续修改;确认无误后,可以点击下方按钮保存草稿或直接提交,也可以直接回复“保存草稿”或“提交”。'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +550,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
|||||||
id: pendingMessage.id,
|
id: pendingMessage.id,
|
||||||
applicationPreview: preview,
|
applicationPreview: preview,
|
||||||
suggestedActions: buildInlineApplicationPreviewSuggestedActions(preview),
|
suggestedActions: buildInlineApplicationPreviewSuggestedActions(preview),
|
||||||
|
requestedSubmit: Boolean(options.requestedSubmit),
|
||||||
|
submitRequiresConfirmation: Boolean(options.submitRequiresConfirmation),
|
||||||
stewardPlan: {
|
stewardPlan: {
|
||||||
streamStatus: 'completed',
|
streamStatus: 'completed',
|
||||||
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
||||||
@@ -554,13 +559,7 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
|||||||
text: content
|
text: content
|
||||||
})
|
})
|
||||||
replaceInlineMessage(pendingMessage.id, previewMessage)
|
replaceInlineMessage(pendingMessage.id, previewMessage)
|
||||||
if (options.autoSubmit && normalizeApplicationPreview(preview).readyToSubmit) {
|
if (options.autoSaveDraft) {
|
||||||
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, previewMessage, {
|
|
||||||
confirmed: true,
|
|
||||||
skipUserMessage: true,
|
|
||||||
userText: options.userMessage || '直接提交'
|
|
||||||
})
|
|
||||||
} else if (options.autoSaveDraft) {
|
|
||||||
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, previewMessage, {
|
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, previewMessage, {
|
||||||
skipUserMessage: true,
|
skipUserMessage: true,
|
||||||
userText: options.userMessage || '保存草稿'
|
userText: options.userMessage || '保存草稿'
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export function resolveInlineTravelApplicationRequest(prompt = '') {
|
|||||||
expenseType: 'travel',
|
expenseType: 'travel',
|
||||||
expenseTypeLabel: '差旅费',
|
expenseTypeLabel: '差旅费',
|
||||||
sourceText,
|
sourceText,
|
||||||
autoSubmit: /直接提交|提交申请|确认提交|提交审批/.test(compact)
|
autoSubmit: false,
|
||||||
|
requestedSubmit: /直接提交|提交申请|确认提交|提交审批/.test(compact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { resolveInlineTravelApplicationRequest } from './workbenchAiApplicationG
|
|||||||
export const WORKBENCH_AI_INTENT_SOURCE_MODEL = 'llm_function_call'
|
export const WORKBENCH_AI_INTENT_SOURCE_MODEL = 'llm_function_call'
|
||||||
export const WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK = 'rule_fallback'
|
export const WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK = 'rule_fallback'
|
||||||
|
|
||||||
|
export const WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD = 0.6
|
||||||
|
|
||||||
export const WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW = 'build_application_preview'
|
export const WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW = 'build_application_preview'
|
||||||
export const WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS = 'validate_required_fields'
|
export const WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS = 'validate_required_fields'
|
||||||
export const WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT = 'save_application_draft'
|
export const WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT = 'save_application_draft'
|
||||||
@@ -233,7 +235,7 @@ export function buildRuleFallbackWorkbenchAiIntentPlan(prompt = '') {
|
|||||||
if (!request) {
|
if (!request) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const requestedAction = request.autoSubmit ? 'submit' : normalizePromptAction(prompt)
|
const requestedAction = request.requestedSubmit ? 'submit' : normalizePromptAction(prompt)
|
||||||
return {
|
return {
|
||||||
source: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK,
|
source: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK,
|
||||||
intent: TRAVEL_APPLICATION_INTENT,
|
intent: TRAVEL_APPLICATION_INTENT,
|
||||||
@@ -255,9 +257,16 @@ export function shouldRequestWorkbenchAiIntentPlan(prompt = '') {
|
|||||||
if (compact.length < 2 || /^[\d\s.,,。::;;!?!?-]+$/.test(compact)) {
|
if (compact.length < 2 || /^[\d\s.,,。::;;!?!?-]+$/.test(compact)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (!WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN.test(compact)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN = (
|
||||||
|
/报销|报账|出差|差旅|申请|审批|审核|报销单|申请单|草稿|删除|提交|保存|查|看|找|列出|发起|新建|创建|驳回|退回|通过|多少|标准|制度|规则|政策/
|
||||||
|
)
|
||||||
|
|
||||||
export function resolveExecutableTravelApplicationPlan(plan = null) {
|
export function resolveExecutableTravelApplicationPlan(plan = null) {
|
||||||
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
|
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
|
||||||
return null
|
return null
|
||||||
@@ -265,12 +274,32 @@ export function resolveExecutableTravelApplicationPlan(plan = null) {
|
|||||||
if (!Array.isArray(plan.steps) || !plan.steps.includes(WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW)) {
|
if (!Array.isArray(plan.steps) || !plan.steps.includes(WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
const requestedSubmit = plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION)
|
||||||
return {
|
return {
|
||||||
expenseType: 'travel',
|
expenseType: 'travel',
|
||||||
expenseTypeLabel: '差旅费',
|
expenseTypeLabel: '差旅费',
|
||||||
sourceText: String(plan.sourceText || '').trim(),
|
sourceText: String(plan.sourceText || '').trim(),
|
||||||
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
|
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
|
||||||
autoSubmit: plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION),
|
autoSubmit: false,
|
||||||
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT)
|
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT),
|
||||||
|
requestedSubmit,
|
||||||
|
submitRequiresConfirmation: requestedSubmit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLowConfidenceTravelApplicationPlan(plan = null) {
|
||||||
|
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (plan.source === WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (plan.requestedAction === 'submit' || plan.requestedAction === 'save_draft') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const confidence = Number(plan.confidence)
|
||||||
|
if (!Number.isFinite(confidence)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return confidence < WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ export function createWorkbenchAiMessageRuntime() {
|
|||||||
? hydrateInlineAttachmentAssociationSuggestedActions(suggestedActions, normalizedContent)
|
? hydrateInlineAttachmentAssociationSuggestedActions(suggestedActions, normalizedContent)
|
||||||
: suggestedActions,
|
: suggestedActions,
|
||||||
applicationPreview: options.applicationPreview || null,
|
applicationPreview: options.applicationPreview || null,
|
||||||
|
requestedSubmit: Boolean(options.requestedSubmit),
|
||||||
|
submitRequiresConfirmation: Boolean(options.submitRequiresConfirmation),
|
||||||
draftPayload: options.draftPayload || null,
|
draftPayload: options.draftPayload || null,
|
||||||
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(options.attachmentAssociationJob || null),
|
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(options.attachmentAssociationJob || null),
|
||||||
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(options.linkedReimbursementDraftJob || null),
|
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(options.linkedReimbursementDraftJob || null),
|
||||||
@@ -167,6 +169,8 @@ export function createWorkbenchAiMessageRuntime() {
|
|||||||
stewardPlan: message.stewardPlan || null,
|
stewardPlan: message.stewardPlan || null,
|
||||||
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
|
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
|
||||||
applicationPreview: message.applicationPreview || null,
|
applicationPreview: message.applicationPreview || null,
|
||||||
|
requestedSubmit: Boolean(message.requestedSubmit),
|
||||||
|
submitRequiresConfirmation: Boolean(message.submitRequiresConfirmation),
|
||||||
draftPayload: message.draftPayload || null,
|
draftPayload: message.draftPayload || null,
|
||||||
attachmentAssociationJob: message.attachmentAssociationJob || null,
|
attachmentAssociationJob: message.attachmentAssociationJob || null,
|
||||||
linkedReimbursementDraftJob: message.linkedReimbursementDraftJob || null,
|
linkedReimbursementDraftJob: message.linkedReimbursementDraftJob || null,
|
||||||
@@ -185,6 +189,8 @@ export function createWorkbenchAiMessageRuntime() {
|
|||||||
stewardPlan: message.stewardPlan || null,
|
stewardPlan: message.stewardPlan || null,
|
||||||
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
|
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
|
||||||
applicationPreview: message.applicationPreview || null,
|
applicationPreview: message.applicationPreview || null,
|
||||||
|
requestedSubmit: Boolean(message.requestedSubmit),
|
||||||
|
submitRequiresConfirmation: Boolean(message.submitRequiresConfirmation),
|
||||||
draftPayload: message.draftPayload || null,
|
draftPayload: message.draftPayload || null,
|
||||||
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(message.attachmentAssociationJob || null),
|
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(message.attachmentAssociationJob || null),
|
||||||
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(message.linkedReimbursementDraftJob || null),
|
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(message.linkedReimbursementDraftJob || null),
|
||||||
|
|||||||
@@ -10,13 +10,21 @@ export function resolveWorkbenchIntentActionRoute(frame = null) {
|
|||||||
if (frame.action === 'ask_policy') {
|
if (frame.action === 'ask_policy') {
|
||||||
return { nextStep: 'pass_through' }
|
return { nextStep: 'pass_through' }
|
||||||
}
|
}
|
||||||
if (frame.targetMode === 'current_context' && frame.safetyLevel === 'confirm_required') {
|
if (frame.policyDecision === 'need_confirmation') {
|
||||||
return { nextStep: 'open_context_confirm' }
|
return {
|
||||||
|
nextStep: 'open_context_confirm',
|
||||||
|
riskLevel: frame.riskLevel,
|
||||||
|
requiresSelection: Boolean(frame.requiresSelection),
|
||||||
|
requiresConfirmation: Boolean(frame.requiresConfirmation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (frame.targetMode === 'filtered_candidates' && QUERY_CANDIDATE_ACTIONS.has(frame.action)) {
|
if (frame.policyDecision === 'query_candidates' && QUERY_CANDIDATE_ACTIONS.has(frame.action)) {
|
||||||
return {
|
return {
|
||||||
nextStep: 'query_candidates',
|
nextStep: 'query_candidates',
|
||||||
queryPrompt: frame.normalizedQuery || ''
|
queryPrompt: frame.normalizedQuery || '',
|
||||||
|
riskLevel: frame.riskLevel,
|
||||||
|
requiresSelection: Boolean(frame.requiresSelection),
|
||||||
|
requiresConfirmation: Boolean(frame.requiresConfirmation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { nextStep: 'pass_through' }
|
return { nextStep: 'pass_through' }
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ function resolveAction(text = '') {
|
|||||||
if (/审核|审批|通过|处理待办|去审批|去审核/.test(text)) {
|
if (/审核|审批|通过|处理待办|去审批|去审核/.test(text)) {
|
||||||
return 'approve'
|
return 'approve'
|
||||||
}
|
}
|
||||||
|
if (/查|看|列出|有哪些|找一下/.test(text)) {
|
||||||
|
return 'query'
|
||||||
|
}
|
||||||
if (/新建|发起|创建|我要报销|申请/.test(text)) {
|
if (/新建|发起|创建|我要报销|申请/.test(text)) {
|
||||||
return 'create'
|
return 'create'
|
||||||
}
|
}
|
||||||
if (/补充|修改|改成|填入/.test(text)) {
|
if (/补充|修改|改成|填入/.test(text)) {
|
||||||
return 'update'
|
return 'update'
|
||||||
}
|
}
|
||||||
if (/查|看|列出|有哪些|找一下/.test(text)) {
|
|
||||||
return 'query'
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +146,59 @@ function resolveSafetyLevel(action = '') {
|
|||||||
return 'read_only'
|
return 'read_only'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRiskLevel(action = '') {
|
||||||
|
if (CONFIRM_REQUIRED_ACTIONS.has(action)) {
|
||||||
|
return 'high'
|
||||||
|
}
|
||||||
|
if (['create', 'update'].includes(action)) {
|
||||||
|
return 'low'
|
||||||
|
}
|
||||||
|
return 'read_only'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExecutionPolicy({ action, targetMode, riskLevel }) {
|
||||||
|
if (action === 'ask_policy') {
|
||||||
|
return {
|
||||||
|
requiresCandidateSearch: false,
|
||||||
|
requiresSelection: false,
|
||||||
|
requiresConfirmation: false,
|
||||||
|
executionMode: 'answer_only',
|
||||||
|
policyDecision: 'answer_only'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const highRisk = riskLevel === 'high'
|
||||||
|
const requiresCandidateSearch = targetMode === 'filtered_candidates'
|
||||||
|
const requiresSelection = highRisk && requiresCandidateSearch
|
||||||
|
const requiresConfirmation = highRisk
|
||||||
|
|
||||||
|
if (requiresCandidateSearch) {
|
||||||
|
return {
|
||||||
|
requiresCandidateSearch,
|
||||||
|
requiresSelection,
|
||||||
|
requiresConfirmation,
|
||||||
|
executionMode: 'query_candidates',
|
||||||
|
policyDecision: 'query_candidates'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiresConfirmation) {
|
||||||
|
return {
|
||||||
|
requiresCandidateSearch,
|
||||||
|
requiresSelection,
|
||||||
|
requiresConfirmation,
|
||||||
|
executionMode: 'need_confirmation',
|
||||||
|
policyDecision: 'need_confirmation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
requiresCandidateSearch,
|
||||||
|
requiresSelection,
|
||||||
|
requiresConfirmation,
|
||||||
|
executionMode: 'pass_through',
|
||||||
|
policyDecision: 'pass_through'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildNormalizedQuery({ action, objectType, filters }) {
|
function buildNormalizedQuery({ action, objectType, filters }) {
|
||||||
if ((objectType === 'draft' || action === 'delete') && !filters.timeRange?.label && !filters.risk?.label && !filters.documentType) {
|
if ((objectType === 'draft' || action === 'delete') && !filters.timeRange?.label && !filters.risk?.label && !filters.documentType) {
|
||||||
return '我的草稿单据'
|
return '我的草稿单据'
|
||||||
@@ -199,12 +252,16 @@ export function resolveWorkbenchIntentFrame(prompt = '', options = {}) {
|
|||||||
const targetMode = action === 'ask_policy'
|
const targetMode = action === 'ask_policy'
|
||||||
? 'ambiguous'
|
? 'ambiguous'
|
||||||
: resolveTargetMode(action, text, filters)
|
: resolveTargetMode(action, text, filters)
|
||||||
|
const riskLevel = resolveRiskLevel(action)
|
||||||
|
const policy = resolveExecutionPolicy({ action, targetMode, riskLevel })
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
objectType,
|
objectType,
|
||||||
filters,
|
filters,
|
||||||
targetMode,
|
targetMode,
|
||||||
safetyLevel,
|
safetyLevel,
|
||||||
|
riskLevel,
|
||||||
|
...policy,
|
||||||
confidence: 0.86,
|
confidence: 0.86,
|
||||||
normalizedQuery: action === 'ask_policy' ? String(prompt || '').trim() : buildNormalizedQuery({ action, objectType, filters })
|
normalizedQuery: action === 'ask_policy' ? String(prompt || '').trim() : buildNormalizedQuery({ action, objectType, filters })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ test('application intent uses local preview instead of immediate orchestrator ca
|
|||||||
assert.match(buildLocalApplicationPreviewMessage(preview), /点击对应行即可直接编辑/)
|
assert.match(buildLocalApplicationPreviewMessage(preview), /点击对应行即可直接编辑/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('AI workbench routes compact travel direct-submit planner into application preview auto submit', () => {
|
test('AI workbench routes compact travel direct-submit planner into preview with confirmation required', () => {
|
||||||
assert.match(personalWorkbenchAiModeScript, /buildRuleFallbackWorkbenchAiIntentPlan/)
|
assert.match(personalWorkbenchAiModeScript, /buildRuleFallbackWorkbenchAiIntentPlan/)
|
||||||
assert.match(personalWorkbenchAiModeScript, /normalizeWorkbenchAiIntentPlan/)
|
assert.match(personalWorkbenchAiModeScript, /normalizeWorkbenchAiIntentPlan/)
|
||||||
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan/)
|
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan/)
|
||||||
@@ -262,7 +262,7 @@ test('AI workbench routes compact travel direct-submit planner into application
|
|||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
personalWorkbenchAiModeScript,
|
personalWorkbenchAiModeScript,
|
||||||
/modelPlan = await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files\)/
|
/modelPlan = await stewardFlow\.resolveInlineExecutionPlan\(cleanPrompt, entry, files,\s*\{/
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
personalWorkbenchAiModeScript,
|
personalWorkbenchAiModeScript,
|
||||||
@@ -272,8 +272,11 @@ test('AI workbench routes compact travel direct-submit planner into application
|
|||||||
personalWorkbenchAiModeScript,
|
personalWorkbenchAiModeScript,
|
||||||
/applicationFlow\.startAiApplicationPreview\([\s\S]*travelApplicationRequest\.expenseType[\s\S]*travelApplicationRequest\.expenseTypeLabel[\s\S]*travelApplicationRequest\.sourceText[\s\S]*ontologyFields:\s*travelApplicationRequest\.ontologyFields[\s\S]*autoSubmit:\s*travelApplicationRequest\.autoSubmit/
|
/applicationFlow\.startAiApplicationPreview\([\s\S]*travelApplicationRequest\.expenseType[\s\S]*travelApplicationRequest\.expenseTypeLabel[\s\S]*travelApplicationRequest\.sourceText[\s\S]*ontologyFields:\s*travelApplicationRequest\.ontologyFields[\s\S]*autoSubmit:\s*travelApplicationRequest\.autoSubmit/
|
||||||
)
|
)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /requestedSubmit:\s*travelApplicationRequest\.requestedSubmit/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /submitRequiresConfirmation:\s*travelApplicationRequest\.submitRequiresConfirmation/)
|
||||||
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
|
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
|
||||||
assert.match(applicationPreviewFlowScript, /if \(options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit\)/)
|
assert.doesNotMatch(applicationPreviewFlowScript, /if \(options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit\)/)
|
||||||
|
assert.match(applicationPreviewFlowScript, /submitRequiresConfirmation:\s*Boolean\(options\.submitRequiresConfirmation\)/)
|
||||||
assert.match(applicationPreviewFlowScript, /confirmed:\s*true/)
|
assert.match(applicationPreviewFlowScript, /confirmed:\s*true/)
|
||||||
assert.match(applicationPreviewFlowScript, /skipUserMessage:\s*true/)
|
assert.match(applicationPreviewFlowScript, /skipUserMessage:\s*true/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ test('workbench application gate detects compact travel application direct submi
|
|||||||
expenseType: 'travel',
|
expenseType: 'travel',
|
||||||
expenseTypeLabel: '差旅费',
|
expenseTypeLabel: '差旅费',
|
||||||
sourceText: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
|
sourceText: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
|
||||||
autoSubmit: true
|
autoSubmit: false,
|
||||||
|
requestedSubmit: true
|
||||||
})
|
})
|
||||||
assert.equal(resolveInlineTravelApplicationRequest('去上海出差,辅助国网仿生产服务器部署,交通火车')?.autoSubmit, false)
|
assert.equal(resolveInlineTravelApplicationRequest('去上海出差,辅助国网仿生产服务器部署,交通火车')?.autoSubmit, false)
|
||||||
|
assert.equal(resolveInlineTravelApplicationRequest('去上海出差,辅助国网仿生产服务器部署,交通火车')?.requestedSubmit, false)
|
||||||
assert.equal(resolveInlineTravelApplicationRequest('帮我查询上海差旅标准'), null)
|
assert.equal(resolveInlineTravelApplicationRequest('帮我查询上海差旅标准'), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import {
|
|||||||
WORKBENCH_AI_STEP_SUBMIT_APPLICATION,
|
WORKBENCH_AI_STEP_SUBMIT_APPLICATION,
|
||||||
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
|
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS,
|
||||||
buildRuleFallbackWorkbenchAiIntentPlan,
|
buildRuleFallbackWorkbenchAiIntentPlan,
|
||||||
|
isLowConfidenceTravelApplicationPlan,
|
||||||
normalizeWorkbenchAiIntentPlan,
|
normalizeWorkbenchAiIntentPlan,
|
||||||
resolveExecutableTravelApplicationPlan,
|
resolveExecutableTravelApplicationPlan,
|
||||||
shouldRequestWorkbenchAiIntentPlan
|
shouldRequestWorkbenchAiIntentPlan
|
||||||
} from '../src/composables/workbenchAiMode/workbenchAiIntentPlannerModel.js'
|
} from '../src/composables/workbenchAiMode/workbenchAiIntentPlannerModel.js'
|
||||||
import { buildInlineApplicationPreview } from '../src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js'
|
import { buildInlineApplicationPreview } from '../src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js'
|
||||||
|
import { createWorkbenchAiMessageRuntime } from '../src/composables/workbenchAiMode/workbenchAiMessageModel.js'
|
||||||
|
|
||||||
const personalWorkbenchAiModeScript = readFileSync(
|
const personalWorkbenchAiModeScript = readFileSync(
|
||||||
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
|
fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)),
|
||||||
@@ -144,7 +146,9 @@ test('workbench AI intent planner detects compact travel save-draft variant befo
|
|||||||
sourceText: prompt,
|
sourceText: prompt,
|
||||||
ontologyFields: {},
|
ontologyFields: {},
|
||||||
autoSubmit: false,
|
autoSubmit: false,
|
||||||
autoSaveDraft: true
|
autoSaveDraft: true,
|
||||||
|
requestedSubmit: false,
|
||||||
|
submitRequiresConfirmation: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -178,7 +182,9 @@ test('workbench AI intent planner turns model fields and action into executable
|
|||||||
transport_mode: '火车'
|
transport_mode: '火车'
|
||||||
},
|
},
|
||||||
autoSubmit: false,
|
autoSubmit: false,
|
||||||
autoSaveDraft: true
|
autoSaveDraft: true,
|
||||||
|
requestedSubmit: false,
|
||||||
|
submitRequiresConfirmation: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +226,9 @@ test('workbench AI intent planner turns single application candidate flow into e
|
|||||||
transport_mode: '火车'
|
transport_mode: '火车'
|
||||||
},
|
},
|
||||||
autoSubmit: false,
|
autoSubmit: false,
|
||||||
autoSaveDraft: false
|
autoSaveDraft: false,
|
||||||
|
requestedSubmit: false,
|
||||||
|
submitRequiresConfirmation: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,7 +253,7 @@ test('workbench AI application preview prefers model ontology fields over local
|
|||||||
assert.equal(preview.fields.transportMode, '火车')
|
assert.equal(preview.fields.transportMode, '火车')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('workbench AI intent planner rejects policy question and resolves executable application request', () => {
|
test('workbench AI intent planner rejects policy question and requires confirmation for direct-submit request', () => {
|
||||||
assert.equal(buildRuleFallbackWorkbenchAiIntentPlan('帮我查询上海差旅标准'), null)
|
assert.equal(buildRuleFallbackWorkbenchAiIntentPlan('帮我查询上海差旅标准'), null)
|
||||||
|
|
||||||
const request = resolveExecutableTravelApplicationPlan(
|
const request = resolveExecutableTravelApplicationPlan(
|
||||||
@@ -257,11 +265,30 @@ test('workbench AI intent planner rejects policy question and resolves executabl
|
|||||||
expenseTypeLabel: '差旅费',
|
expenseTypeLabel: '差旅费',
|
||||||
sourceText: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
|
sourceText: '去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交',
|
||||||
ontologyFields: {},
|
ontologyFields: {},
|
||||||
autoSubmit: true,
|
autoSubmit: false,
|
||||||
autoSaveDraft: false
|
autoSaveDraft: false,
|
||||||
|
requestedSubmit: true,
|
||||||
|
submitRequiresConfirmation: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('workbench AI message runtime persists direct-submit confirmation metadata', () => {
|
||||||
|
const { createInlineMessage, normalizeRuntimeMessage, serializeRuntimeMessage } = createWorkbenchAiMessageRuntime()
|
||||||
|
const message = createInlineMessage('assistant', '申请核对表', {
|
||||||
|
applicationPreview: { fields: { location: '上海' } },
|
||||||
|
requestedSubmit: true,
|
||||||
|
submitRequiresConfirmation: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const serialized = serializeRuntimeMessage(message)
|
||||||
|
assert.equal(serialized.requestedSubmit, true)
|
||||||
|
assert.equal(serialized.submitRequiresConfirmation, true)
|
||||||
|
|
||||||
|
const normalized = normalizeRuntimeMessage(serialized)
|
||||||
|
assert.equal(normalized.requestedSubmit, true)
|
||||||
|
assert.equal(normalized.submitRequiresConfirmation, true)
|
||||||
|
})
|
||||||
|
|
||||||
test('workbench AI mode asks steward model plan before fallback execution', () => {
|
test('workbench AI mode asks steward model plan before fallback execution', () => {
|
||||||
assert.match(stewardFlowScript, /async function resolveInlineExecutionPlan\(prompt, entry = \{\}, files = \[\], options = \{\}\)/)
|
assert.match(stewardFlowScript, /async function resolveInlineExecutionPlan\(prompt, entry = \{\}, files = \[\], options = \{\}\)/)
|
||||||
assert.match(stewardFlowScript, /fetchStewardPlan\(planRequest/)
|
assert.match(stewardFlowScript, /fetchStewardPlan\(planRequest/)
|
||||||
@@ -274,8 +301,11 @@ test('workbench AI mode asks steward model plan before fallback execution', () =
|
|||||||
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan\(intentPlan\)/)
|
assert.match(personalWorkbenchAiModeScript, /resolveExecutableTravelApplicationPlan\(intentPlan\)/)
|
||||||
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
|
assert.doesNotMatch(personalWorkbenchAiModeScript, /fallbackIntentPlan/)
|
||||||
assert.match(personalWorkbenchAiModeScript, /autoSaveDraft:\s*travelApplicationRequest\.autoSaveDraft/)
|
assert.match(personalWorkbenchAiModeScript, /autoSaveDraft:\s*travelApplicationRequest\.autoSaveDraft/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /requestedSubmit:\s*travelApplicationRequest\.requestedSubmit/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /submitRequiresConfirmation:\s*travelApplicationRequest\.submitRequiresConfirmation/)
|
||||||
assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/)
|
assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/)
|
||||||
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
|
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
|
||||||
|
assert.doesNotMatch(applicationPreviewFlowScript, /options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit/)
|
||||||
assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/)
|
assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/)
|
||||||
assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/)
|
assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/)
|
||||||
assert.match(applicationPreviewFlowScript, /buildInlineApplicationPreview\([\s\S]*ontologyFields:\s*options\.ontologyFields/)
|
assert.match(applicationPreviewFlowScript, /buildInlineApplicationPreview\([\s\S]*ontologyFields:\s*options\.ontologyFields/)
|
||||||
@@ -329,3 +359,72 @@ test('workbench AI mode reuses planning pending message for regular steward repl
|
|||||||
assert.match(stewardFlowScript, /const reusablePendingMessageId = String\(options\.pendingMessageId \|\| ''\)\.trim\(\)/)
|
assert.match(stewardFlowScript, /const reusablePendingMessageId = String\(options\.pendingMessageId \|\| ''\)\.trim\(\)/)
|
||||||
assert.match(stewardFlowScript, /reusablePendingMessageId \? replaceInlineMessage\(reusablePendingMessageId, pendingMessage\) : conversationMessages\.value\.push\(pendingMessage\)/)
|
assert.match(stewardFlowScript, /reusablePendingMessageId \? replaceInlineMessage\(reusablePendingMessageId, pendingMessage\) : conversationMessages\.value\.push\(pendingMessage\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('isLowConfidenceTravelApplicationPlan gates preview behind confirmation when model confidence is low', () => {
|
||||||
|
const lowConfidenceModelPlan = normalizeWorkbenchAiIntentPlan({
|
||||||
|
planning_source: 'llm_function_call',
|
||||||
|
tasks: [{
|
||||||
|
task_type: 'expense_application',
|
||||||
|
assigned_agent: 'application_assistant',
|
||||||
|
requested_action: 'preview',
|
||||||
|
confidence: 0.4,
|
||||||
|
ontology_fields: { location: '上海', reason: '部署' }
|
||||||
|
}]
|
||||||
|
}, { prompt: '上海那边好像要过去一趟搞部署' })
|
||||||
|
|
||||||
|
assert.equal(lowConfidenceModelPlan.source, WORKBENCH_AI_INTENT_SOURCE_MODEL)
|
||||||
|
assert.equal(isLowConfidenceTravelApplicationPlan(lowConfidenceModelPlan), true)
|
||||||
|
|
||||||
|
const highConfidenceModelPlan = normalizeWorkbenchAiIntentPlan({
|
||||||
|
planning_source: 'llm_function_call',
|
||||||
|
tasks: [{
|
||||||
|
task_type: 'expense_application',
|
||||||
|
assigned_agent: 'application_assistant',
|
||||||
|
requested_action: 'preview',
|
||||||
|
confidence: 0.9,
|
||||||
|
ontology_fields: { location: '上海' }
|
||||||
|
}]
|
||||||
|
}, { prompt: '去上海出差' })
|
||||||
|
|
||||||
|
assert.equal(isLowConfidenceTravelApplicationPlan(highConfidenceModelPlan), false)
|
||||||
|
|
||||||
|
const ruleFallbackPlan = buildRuleFallbackWorkbenchAiIntentPlan('去上海出差,辅助部署,交通火车')
|
||||||
|
assert.equal(ruleFallbackPlan.source, WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK)
|
||||||
|
assert.equal(isLowConfidenceTravelApplicationPlan(ruleFallbackPlan), false)
|
||||||
|
|
||||||
|
const explicitSubmitLowConfidence = normalizeWorkbenchAiIntentPlan({
|
||||||
|
planning_source: 'llm_function_call',
|
||||||
|
tasks: [{
|
||||||
|
task_type: 'expense_application',
|
||||||
|
assigned_agent: 'application_assistant',
|
||||||
|
requested_action: 'submit',
|
||||||
|
confidence: 0.3,
|
||||||
|
ontology_fields: { location: '上海' }
|
||||||
|
}]
|
||||||
|
}, { prompt: '直接提交' })
|
||||||
|
|
||||||
|
assert.equal(explicitSubmitLowConfidence.requestedAction, 'submit')
|
||||||
|
assert.equal(isLowConfidenceTravelApplicationPlan(explicitSubmitLowConfidence), false)
|
||||||
|
|
||||||
|
assert.equal(isLowConfidenceTravelApplicationPlan(null), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('workbench AI mode routes low confidence travel application plan to confirmation prompt', () => {
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /isLowConfidenceTravelApplicationPlan\(intentPlan\)/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /function startModelPlannedTravelApplicationConfirmation\(/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /action_type:\s*'ai_application_confirm_intent'/)
|
||||||
|
assert.match(personalWorkbenchAiModeScript, /需要确认:您是要发起出差申请吗/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shouldRequestWorkbenchAiIntentPlan skips chitchat and only triggers on business keywords', () => {
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('你好'), false)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('谢谢'), false)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('嗯'), false)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('ok'), false)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('1'), false)
|
||||||
|
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('帮我查报销'), true)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('我要出差'), true)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('帮我查询上海差旅标准'), true)
|
||||||
|
assert.equal(shouldRequestWorkbenchAiIntentPlan('删除3天前的草稿'), true)
|
||||||
|
})
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ test('workbench intent frame resolves contextual draft deletion as confirm-only
|
|||||||
assert.equal(frame?.objectType, 'draft')
|
assert.equal(frame?.objectType, 'draft')
|
||||||
assert.equal(frame?.targetMode, 'current_context')
|
assert.equal(frame?.targetMode, 'current_context')
|
||||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||||
|
assert.equal(frame?.riskLevel, 'high')
|
||||||
|
assert.equal(frame?.requiresCandidateSearch, false)
|
||||||
|
assert.equal(frame?.requiresSelection, false)
|
||||||
|
assert.equal(frame?.requiresConfirmation, true)
|
||||||
|
assert.equal(frame?.executionMode, 'need_confirmation')
|
||||||
|
assert.equal(frame?.policyDecision, 'need_confirmation')
|
||||||
assert.equal(frame?.filters.status?.label, '草稿')
|
assert.equal(frame?.filters.status?.label, '草稿')
|
||||||
assert.equal(frame?.normalizedQuery, '我的草稿单据')
|
assert.equal(frame?.normalizedQuery, '我的草稿单据')
|
||||||
})
|
})
|
||||||
@@ -34,11 +40,19 @@ test('workbench intent frame sends filtered draft deletion to candidate search',
|
|||||||
assert.equal(frame?.objectType, 'draft')
|
assert.equal(frame?.objectType, 'draft')
|
||||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||||
|
assert.equal(frame?.riskLevel, 'high')
|
||||||
|
assert.equal(frame?.requiresCandidateSearch, true)
|
||||||
|
assert.equal(frame?.requiresSelection, true)
|
||||||
|
assert.equal(frame?.requiresConfirmation, true)
|
||||||
|
assert.equal(frame?.executionMode, 'query_candidates')
|
||||||
|
assert.equal(frame?.policyDecision, 'query_candidates')
|
||||||
assert.equal(frame?.filters.timeRange?.start, '2026-06-21')
|
assert.equal(frame?.filters.timeRange?.start, '2026-06-21')
|
||||||
assert.equal(frame?.filters.timeRange?.end, '2026-06-21')
|
assert.equal(frame?.filters.timeRange?.end, '2026-06-21')
|
||||||
assert.equal(frame?.normalizedQuery, '我的 3天前 草稿单据')
|
assert.equal(frame?.normalizedQuery, '我的 3天前 草稿单据')
|
||||||
assert.equal(route.nextStep, 'query_candidates')
|
assert.equal(route.nextStep, 'query_candidates')
|
||||||
assert.equal(route.queryPrompt, '我的 3天前 草稿单据')
|
assert.equal(route.queryPrompt, '我的 3天前 草稿单据')
|
||||||
|
assert.equal(route.requiresSelection, true)
|
||||||
|
assert.equal(route.requiresConfirmation, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('workbench intent frame preserves application draft deletion filters', () => {
|
test('workbench intent frame preserves application draft deletion filters', () => {
|
||||||
@@ -52,6 +66,11 @@ test('workbench intent frame preserves application draft deletion filters', () =
|
|||||||
assert.equal(frame?.filters.status?.label, '草稿')
|
assert.equal(frame?.filters.status?.label, '草稿')
|
||||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||||
|
assert.equal(frame?.riskLevel, 'high')
|
||||||
|
assert.equal(frame?.requiresCandidateSearch, true)
|
||||||
|
assert.equal(frame?.requiresSelection, true)
|
||||||
|
assert.equal(frame?.requiresConfirmation, true)
|
||||||
|
assert.equal(frame?.executionMode, 'query_candidates')
|
||||||
assert.equal(route.queryPrompt, '我的 草稿 申请单')
|
assert.equal(route.queryPrompt, '我的 草稿 申请单')
|
||||||
assert.equal(queryIntent?.source, 'mine')
|
assert.equal(queryIntent?.source, 'mine')
|
||||||
assert.equal(queryIntent?.documentType, 'application')
|
assert.equal(queryIntent?.documentType, 'application')
|
||||||
@@ -99,11 +118,19 @@ test('workbench intent frame resolves compliant no-risk approval request as filt
|
|||||||
assert.equal(frame?.objectType, 'application')
|
assert.equal(frame?.objectType, 'application')
|
||||||
assert.equal(frame?.targetMode, 'filtered_candidates')
|
assert.equal(frame?.targetMode, 'filtered_candidates')
|
||||||
assert.equal(frame?.safetyLevel, 'confirm_required')
|
assert.equal(frame?.safetyLevel, 'confirm_required')
|
||||||
|
assert.equal(frame?.riskLevel, 'high')
|
||||||
|
assert.equal(frame?.requiresCandidateSearch, true)
|
||||||
|
assert.equal(frame?.requiresSelection, true)
|
||||||
|
assert.equal(frame?.requiresConfirmation, true)
|
||||||
|
assert.equal(frame?.executionMode, 'query_candidates')
|
||||||
|
assert.equal(frame?.policyDecision, 'query_candidates')
|
||||||
assert.equal(frame?.filters.risk?.level, 'none')
|
assert.equal(frame?.filters.risk?.level, 'none')
|
||||||
assert.equal(frame?.filters.documentType, 'application')
|
assert.equal(frame?.filters.documentType, 'application')
|
||||||
assert.equal(frame?.normalizedQuery, '待我审核 无风险 申请单')
|
assert.equal(frame?.normalizedQuery, '待我审核 无风险 申请单')
|
||||||
assert.equal(route.nextStep, 'query_candidates')
|
assert.equal(route.nextStep, 'query_candidates')
|
||||||
assert.equal(route.queryPrompt, '待我审核 无风险 申请单')
|
assert.equal(route.queryPrompt, '待我审核 无风险 申请单')
|
||||||
|
assert.equal(route.requiresSelection, true)
|
||||||
|
assert.equal(route.requiresConfirmation, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('workbench intent frame keeps approval policy questions out of document actions', () => {
|
test('workbench intent frame keeps approval policy questions out of document actions', () => {
|
||||||
@@ -112,5 +139,30 @@ test('workbench intent frame keeps approval policy questions out of document act
|
|||||||
|
|
||||||
assert.equal(frame?.action, 'ask_policy')
|
assert.equal(frame?.action, 'ask_policy')
|
||||||
assert.equal(frame?.safetyLevel, 'read_only')
|
assert.equal(frame?.safetyLevel, 'read_only')
|
||||||
|
assert.equal(frame?.riskLevel, 'read_only')
|
||||||
|
assert.equal(frame?.requiresCandidateSearch, false)
|
||||||
|
assert.equal(frame?.requiresSelection, false)
|
||||||
|
assert.equal(frame?.requiresConfirmation, false)
|
||||||
|
assert.equal(frame?.executionMode, 'answer_only')
|
||||||
|
assert.equal(frame?.policyDecision, 'answer_only')
|
||||||
assert.equal(route.nextStep, 'pass_through')
|
assert.equal(route.nextStep, 'pass_through')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('workbench intent frame keeps rules as policy guardrails instead of executable side effects', () => {
|
||||||
|
const highRiskFrame = resolveWorkbenchIntentFrame('审核合规没有风险的申请', { today })
|
||||||
|
const highRiskRoute = resolveWorkbenchIntentActionRoute(highRiskFrame)
|
||||||
|
const queryFrame = resolveWorkbenchIntentFrame('查3天前的申请单', { today })
|
||||||
|
const queryRoute = resolveWorkbenchIntentActionRoute(queryFrame)
|
||||||
|
|
||||||
|
assert.equal(highRiskFrame?.policyDecision, 'query_candidates')
|
||||||
|
assert.equal(highRiskFrame?.requiresSelection, true)
|
||||||
|
assert.equal(highRiskFrame?.requiresConfirmation, true)
|
||||||
|
assert.notEqual(highRiskRoute.nextStep, 'execute_allowed')
|
||||||
|
|
||||||
|
assert.equal(queryFrame?.riskLevel, 'read_only')
|
||||||
|
assert.equal(queryFrame?.requiresCandidateSearch, true)
|
||||||
|
assert.equal(queryFrame?.requiresSelection, false)
|
||||||
|
assert.equal(queryFrame?.requiresConfirmation, false)
|
||||||
|
assert.equal(queryFrame?.policyDecision, 'query_candidates')
|
||||||
|
assert.equal(queryRoute.nextStep, 'query_candidates')
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user