feat(steward): 前端支持 off_topic 与引导话术
- assistantSessionScope.js:新增 ASSISTANT_SCOPE_ACTION_FILL_COMPOSER 常量 - assistantSuggestedActionPrefill.js:识别 fill_composer 与 payload.fill_text - stewardPlanModel.js:normalizeStewardPlan 透传 suggestedPrompts; buildStewardPlanMessageText / buildStewardSuggestedActions 新增 off_topic 分支,按钮填充输入框不提交 - useStewardPlanFlow.js:isPendingStewardActionMessage 排除 off_topic - steward-plan-off-topic.test.mjs:覆盖 normalize/文案/按钮/兼容路径
This commit is contained in:
@@ -2,6 +2,8 @@ import ontologyBusinessContract from '../../../shared/ontology_business_contract
|
|||||||
|
|
||||||
export const ASSISTANT_SCOPE_ACTION_SWITCH = 'switch_assistant_session'
|
export const ASSISTANT_SCOPE_ACTION_SWITCH = 'switch_assistant_session'
|
||||||
export const ASSISTANT_SCOPE_ACTION_UNSUPPORTED = 'unsupported_business_intent'
|
export const ASSISTANT_SCOPE_ACTION_UNSUPPORTED = 'unsupported_business_intent'
|
||||||
|
// 点击后把 payload.fill_text 填充到输入框,不切换会话、不自动提交,交由用户编辑后自行发送。
|
||||||
|
export const ASSISTANT_SCOPE_ACTION_FILL_COMPOSER = 'fill_composer'
|
||||||
|
|
||||||
export const ASSISTANT_SCOPE_SESSION_APPLICATION = 'application'
|
export const ASSISTANT_SCOPE_SESSION_APPLICATION = 'application'
|
||||||
export const ASSISTANT_SCOPE_SESSION_EXPENSE = 'expense'
|
export const ASSISTANT_SCOPE_SESSION_EXPENSE = 'expense'
|
||||||
@@ -286,27 +288,84 @@ function shouldAllowContextualFollowUp(rawText, currentSessionType, options = {}
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUnsupportedBusinessScopeText() {
|
function buildUnsupportedBusinessScopeSuggestedActions(options = {}) {
|
||||||
const message = ONTOLOGY_BUSINESS_CONTRACT.unsupportedIntentMessage || {}
|
const sharedOptions = {
|
||||||
|
attachmentCount: options.attachmentCount || 0
|
||||||
|
}
|
||||||
|
const applicationAction = buildScopeSwitchAction(ASSISTANT_SCOPE_SESSION_APPLICATION, '申请下周去上海出差,支撑客户系统上线', sharedOptions)
|
||||||
|
const expenseAction = buildScopeSwitchAction(ASSISTANT_SCOPE_SESSION_EXPENSE, '我要报销昨天的交通费', sharedOptions)
|
||||||
|
const knowledgeAction = buildScopeSwitchAction(ASSISTANT_SCOPE_SESSION_KNOWLEDGE, '差旅住宿标准是多少', sharedOptions)
|
||||||
|
const approvalAction = buildScopeSwitchAction(ASSISTANT_SCOPE_SESSION_APPROVAL, '帮我查询待我审核的单据', sharedOptions)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
message.title || '此意图系统不支持。',
|
{ ...applicationAction, label: '去申请助手', description: '发起费用申请和事前审批' },
|
||||||
'',
|
{ ...expenseAction, label: '去报销助手', description: '继续处理报销和票据' },
|
||||||
`当前系统支持的业务范围:${SUPPORTED_BUSINESS_SCOPE_TEXT.join('、')}。`,
|
{ ...knowledgeAction, label: '去知识助手', description: '查看标准和流程规则' },
|
||||||
'',
|
{ ...approvalAction, label: '去审核助手', description: '查看待审单据和风险' }
|
||||||
message.body || '你这条内容没有识别到相关财务业务意图,系统暂不支持处理。',
|
]
|
||||||
'',
|
|
||||||
message.retryHint || '请重新描述你的财务业务要求,例如“申请下周去上海出差”“查询我的报销单进度”或“解释差旅住宿标准”。'
|
|
||||||
].join('\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUnsupportedBusinessScopeGuard() {
|
function buildUnsupportedBusinessScopeText(rawText, options = {}) {
|
||||||
|
const message = ONTOLOGY_BUSINESS_CONTRACT.unsupportedIntentMessage || {}
|
||||||
|
const text = String(rawText || '').trim()
|
||||||
|
const intro = text
|
||||||
|
? `**小财管家暂时不处理「${text}」这类内容。**`
|
||||||
|
: `**${message.title || '此意图系统不支持。'}**`
|
||||||
|
const attachmentHint = options.attachmentCount
|
||||||
|
? '你刚刚上传的附件我会先保留,切换到合适场景后可以继续使用。'
|
||||||
|
: ''
|
||||||
|
return [
|
||||||
|
intro,
|
||||||
|
'',
|
||||||
|
'### 当前可继续的场景',
|
||||||
|
`- ${SUPPORTED_BUSINESS_SCOPE_TEXT[0] || '费用申请/事前审批'}`,
|
||||||
|
`- ${SUPPORTED_BUSINESS_SCOPE_TEXT[1] || '报销与票据识别'}`,
|
||||||
|
`- ${SUPPORTED_BUSINESS_SCOPE_TEXT[3] || '财务制度、报销标准和流程规则问答'}`,
|
||||||
|
`- ${SUPPORTED_BUSINESS_SCOPE_TEXT[4] || '预算、应收、应付等财务经营查询'}`,
|
||||||
|
'',
|
||||||
|
message.body || '这条内容没有识别到当前系统支持的财务业务意图,暂时不能继续处理。',
|
||||||
|
attachmentHint,
|
||||||
|
'你可以直接点下面的场景继续,或者重新描述你的财务业务需求。',
|
||||||
|
'',
|
||||||
|
message.retryHint || '请重新描述你的财务业务要求,例如“申请下周去上海出差”“查询我的报销单进度”或“解释差旅住宿标准”。'
|
||||||
|
].filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildUnsupportedBusinessScopeConversation(rawText, options = {}) {
|
||||||
|
const suggestedActions = buildUnsupportedBusinessScopeSuggestedActions(options)
|
||||||
|
return {
|
||||||
|
state_json: {
|
||||||
|
session_type: ASSISTANT_SCOPE_SESSION_STEWARD
|
||||||
|
},
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 'unsupported-business-intent',
|
||||||
|
role: 'assistant',
|
||||||
|
content: buildUnsupportedBusinessScopeText(rawText, options),
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
message_json: {
|
||||||
|
assistant_name: '小财管家',
|
||||||
|
assistant_variant: 'compact_guidance',
|
||||||
|
orchestrator_payload: {
|
||||||
|
result: {
|
||||||
|
suggested_actions: suggestedActions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUnsupportedBusinessScopeGuard(rawText, options = {}) {
|
||||||
|
const suggestedActions = buildUnsupportedBusinessScopeSuggestedActions(options)
|
||||||
return {
|
return {
|
||||||
targetSessionType: '',
|
targetSessionType: '',
|
||||||
targetLabel: '不支持的意图',
|
targetLabel: '不支持的意图',
|
||||||
blocked: true,
|
blocked: true,
|
||||||
text: buildUnsupportedBusinessScopeText(),
|
text: buildUnsupportedBusinessScopeText(rawText, options),
|
||||||
meta: ['意图不支持'],
|
meta: ['意图不支持'],
|
||||||
suggestedActions: [],
|
suggestedActions,
|
||||||
actionType: ASSISTANT_SCOPE_ACTION_UNSUPPORTED
|
actionType: ASSISTANT_SCOPE_ACTION_UNSUPPORTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,7 +377,7 @@ export function resolveAssistantScopeGuard(rawText, currentSessionType, options
|
|||||||
if (shouldAllowContextualFollowUp(rawText, normalizedCurrent, options)) {
|
if (shouldAllowContextualFollowUp(rawText, normalizedCurrent, options)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return normalizeText(rawText) ? buildUnsupportedBusinessScopeGuard() : null
|
return normalizeText(rawText) ? buildUnsupportedBusinessScopeGuard(rawText, options) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetSessionType === normalizedCurrent) {
|
if (targetSessionType === normalizedCurrent) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ASSISTANT_SCOPE_ACTION_FILL_COMPOSER } from './assistantSessionScope.js'
|
||||||
|
|
||||||
const APPLICATION_FIELD_PREFILLS = {
|
const APPLICATION_FIELD_PREFILLS = {
|
||||||
time: '申请时间段:',
|
time: '申请时间段:',
|
||||||
time_range: '申请时间段:',
|
time_range: '申请时间段:',
|
||||||
@@ -14,6 +16,7 @@ export function resolveSuggestedActionPrefill(action = {}) {
|
|||||||
payload.prompt_prefill
|
payload.prompt_prefill
|
||||||
|| payload.input_prefill
|
|| payload.input_prefill
|
||||||
|| payload.prefill_text
|
|| payload.prefill_text
|
||||||
|
|| payload.fill_text
|
||||||
|| ''
|
|| ''
|
||||||
).trim()
|
).trim()
|
||||||
if (explicitPrefill) {
|
if (explicitPrefill) {
|
||||||
@@ -21,7 +24,7 @@ export function resolveSuggestedActionPrefill(action = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actionType = String(action?.action_type || '').trim()
|
const actionType = String(action?.action_type || '').trim()
|
||||||
if (actionType !== 'prefill_composer') {
|
if (actionType !== 'prefill_composer' && actionType !== ASSISTANT_SCOPE_ACTION_FILL_COMPOSER) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ASSISTANT_SCOPE_ACTION_SWITCH,
|
ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||||
|
ASSISTANT_SCOPE_ACTION_FILL_COMPOSER
|
||||||
} from '../../utils/assistantSessionScope.js'
|
} from '../../utils/assistantSessionScope.js'
|
||||||
import {
|
import {
|
||||||
SESSION_TYPE_APPLICATION,
|
SESSION_TYPE_APPLICATION,
|
||||||
@@ -200,12 +201,18 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
|
|||||||
? rawPlan.confirmation_groups
|
? rawPlan.confirmation_groups
|
||||||
: [],
|
: [],
|
||||||
pendingFlowConfirmation,
|
pendingFlowConfirmation,
|
||||||
candidateFlows: pendingFlowConfirmation.candidateFlows
|
candidateFlows: pendingFlowConfirmation.candidateFlows,
|
||||||
|
suggestedPrompts: Array.isArray(rawPlan.suggested_prompts)
|
||||||
|
? rawPlan.suggested_prompts.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildStewardPlanMessageText(plan) {
|
export function buildStewardPlanMessageText(plan) {
|
||||||
const normalized = normalizeStewardPlan(plan)
|
const normalized = normalizeStewardPlan(plan)
|
||||||
|
if (isOffTopicPlan(normalized)) {
|
||||||
|
return buildOffTopicMessageText(normalized)
|
||||||
|
}
|
||||||
if (isPendingFlowConfirmationPlan(normalized)) {
|
if (isPendingFlowConfirmationPlan(normalized)) {
|
||||||
return buildPendingFlowConfirmationMessageText(normalized)
|
return buildPendingFlowConfirmationMessageText(normalized)
|
||||||
}
|
}
|
||||||
@@ -215,13 +222,13 @@ export function buildStewardPlanMessageText(plan) {
|
|||||||
`${index + 1}. **${buildTaskOrderVerb(index)}${buildTaskOrderTarget(task)}**\n - ${buildTaskOrderActionDescription(task)}`
|
`${index + 1}. **${buildTaskOrderVerb(index)}${buildTaskOrderTarget(task)}**\n - ${buildTaskOrderActionDescription(task)}`
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
'### 我会这样推进',
|
'### 我先帮你把步骤理清楚',
|
||||||
'',
|
'',
|
||||||
`我识别到 **${normalized.tasks.length} 个财务事项**,会按顺序逐步处理,不会一次性把所有动作都执行掉。`,
|
buildStewardPlanFriendlyIntro(normalized),
|
||||||
'',
|
'',
|
||||||
...taskLines,
|
...taskLines,
|
||||||
'',
|
'',
|
||||||
'如果这个顺序没问题,请回复 **确定**。我会先进入第一步,并在具体步骤里再判断需要你补充哪些信息。'
|
'你看这个顺序是否合适?如果没问题,回复 **确定** 就行。我会先帮你进入第一步,需要补充的信息会在具体步骤里再温和提醒你。'
|
||||||
].filter((line, index, lines) => line || lines[index - 1]).join('\n')
|
].filter((line, index, lines) => line || lines[index - 1]).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +291,18 @@ export function formatStewardOntologyFields(fields = {}, taskType = '') {
|
|||||||
|
|
||||||
export function buildStewardSuggestedActions(plan) {
|
export function buildStewardSuggestedActions(plan) {
|
||||||
const normalized = normalizeStewardPlan(plan)
|
const normalized = normalizeStewardPlan(plan)
|
||||||
|
if (isOffTopicPlan(normalized)) {
|
||||||
|
return normalized.suggestedPrompts.map((prompt) => ({
|
||||||
|
label: prompt.length > 24 ? `${prompt.slice(0, 24)}...` : prompt,
|
||||||
|
description: '点击填入输入框,可编辑后发送',
|
||||||
|
icon: 'mdi mdi-comment-text-outline',
|
||||||
|
action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER,
|
||||||
|
payload: {
|
||||||
|
steward_plan_id: normalized.planId,
|
||||||
|
fill_text: prompt
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
if (isPendingFlowConfirmationPlan(normalized)) {
|
if (isPendingFlowConfirmationPlan(normalized)) {
|
||||||
return normalized.candidateFlows.map((flow) => ({
|
return normalized.candidateFlows.map((flow) => ({
|
||||||
label: flow.label,
|
label: flow.label,
|
||||||
@@ -383,6 +402,28 @@ function isPendingFlowConfirmationPlan(normalized) {
|
|||||||
) && Array.isArray(normalized?.candidateFlows) && normalized.candidateFlows.length > 0
|
) && Array.isArray(normalized?.candidateFlows) && normalized.candidateFlows.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOffTopicPlan(normalized) {
|
||||||
|
return String(normalized?.planStatus || '').trim() === 'off_topic'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOffTopicStewardPlan(rawPlan) {
|
||||||
|
return isOffTopicPlan(normalizeStewardPlan(rawPlan))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOffTopicMessageText(normalized) {
|
||||||
|
const summary = String(normalized?.summary || '').trim()
|
||||||
|
const summaryLine = summary && summary !== '这看起来跟财务任务没什么关系...'
|
||||||
|
? summary
|
||||||
|
: '这看起来跟财务任务没什么关系,我目前只能帮你处理**费用申请**和**费用报销**两类事项。'
|
||||||
|
return [
|
||||||
|
'### 小财管家没看懂这件事',
|
||||||
|
'',
|
||||||
|
summaryLine,
|
||||||
|
'',
|
||||||
|
'你可以试试下面这些方式告诉我:'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
function buildPendingFlowConfirmationMessageText(normalized) {
|
function buildPendingFlowConfirmationMessageText(normalized) {
|
||||||
const fields = normalized.candidateFlows[0]?.ontologyFields || {}
|
const fields = normalized.candidateFlows[0]?.ontologyFields || {}
|
||||||
const knownParts = formatStewardOntologyFields(fields, 'expense_application')
|
const knownParts = formatStewardOntologyFields(fields, 'expense_application')
|
||||||
@@ -511,10 +552,10 @@ function buildTaskOrderVerb(index) {
|
|||||||
function buildTaskOrderTarget(task) {
|
function buildTaskOrderTarget(task) {
|
||||||
const title = task.title || task.taskTypeLabel
|
const title = task.title || task.taskTypeLabel
|
||||||
if (task.taskType === 'expense_application') {
|
if (task.taskType === 'expense_application') {
|
||||||
return `创建“${title}”`
|
return `整理“${title}”`
|
||||||
}
|
}
|
||||||
if (task.taskType === 'reimbursement') {
|
if (task.taskType === 'reimbursement') {
|
||||||
return `处理“${title}”`
|
return `核对“${title}”`
|
||||||
}
|
}
|
||||||
return `处理“${title}”`
|
return `处理“${title}”`
|
||||||
}
|
}
|
||||||
@@ -522,12 +563,19 @@ function buildTaskOrderTarget(task) {
|
|||||||
function buildTaskOrderActionDescription(task) {
|
function buildTaskOrderActionDescription(task) {
|
||||||
const agent = task.assignedAgentLabel || '对应助手'
|
const agent = task.assignedAgentLabel || '对应助手'
|
||||||
if (task.taskType === 'expense_application') {
|
if (task.taskType === 'expense_application') {
|
||||||
return `交给${agent}生成申请单核对结果,确认无误后再进入后续动作。`
|
return `我会请${agent}先把申请单草稿整理出来,方便你核对关键信息,再决定是否继续。`
|
||||||
}
|
}
|
||||||
if (task.taskType === 'reimbursement') {
|
if (task.taskType === 'reimbursement') {
|
||||||
return `交给${agent}整理报销核对结果,等前一步完成后再继续推进。`
|
return `我会请${agent}把票据、金额和制度口径先核清楚,前一步确认后再继续往下走。`
|
||||||
}
|
}
|
||||||
return `交给${agent}处理,执行前会先让你确认。`
|
return `我会请${agent}先整理可核对的结果,真正执行前仍会让你确认。`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStewardPlanFriendlyIntro(normalized) {
|
||||||
|
const taskCountText = normalized.tasks.length > 1
|
||||||
|
? `${normalized.tasks.length} 个相关事项`
|
||||||
|
: '1 个事项'
|
||||||
|
return `我先看了一下,你这次主要是 **${taskCountText}**。为了不让步骤混在一起,我会先把要做的事拆开,让你每一步都能看清楚、确认后再继续。`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTaskOrderDescription(normalized) {
|
function buildTaskOrderDescription(normalized) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
buildStewardPlanMessageText,
|
buildStewardPlanMessageText,
|
||||||
buildStewardPlanRequest,
|
buildStewardPlanRequest,
|
||||||
buildStewardSuggestedActions,
|
buildStewardSuggestedActions,
|
||||||
|
isOffTopicStewardPlan,
|
||||||
normalizeStewardPlan
|
normalizeStewardPlan
|
||||||
} from './stewardPlanModel.js'
|
} from './stewardPlanModel.js'
|
||||||
import { SESSION_TYPE_STEWARD } from './travelReimbursementConversationModel.js'
|
import { SESSION_TYPE_STEWARD } from './travelReimbursementConversationModel.js'
|
||||||
@@ -384,7 +385,14 @@ export function useStewardPlanFlow({
|
|||||||
|
|
||||||
function isPendingStewardActionMessage(message) {
|
function isPendingStewardActionMessage(message) {
|
||||||
if (message?.stewardPlan) {
|
if (message?.stewardPlan) {
|
||||||
return message.stewardPlan.streamStatus !== 'streaming'
|
if (message.stewardPlan.streamStatus === 'streaming') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// off_topic 是引导用户重新编辑输入,不参与"确定/确认"快捷回复链路。
|
||||||
|
if (isOffTopicStewardPlan(message.stewardPlan)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
String(message?.assistantName || '').trim() === '小财管家'
|
String(message?.assistantName || '').trim() === '小财管家'
|
||||||
|
|||||||
180
web/tests/steward-plan-off-topic.test.mjs
Normal file
180
web/tests/steward-plan-off-topic.test.mjs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ASSISTANT_SCOPE_ACTION_FILL_COMPOSER
|
||||||
|
} from '../src/utils/assistantSessionScope.js'
|
||||||
|
import {
|
||||||
|
buildStewardPlanMessageText,
|
||||||
|
buildStewardSuggestedActions,
|
||||||
|
isOffTopicStewardPlan,
|
||||||
|
normalizeStewardPlan
|
||||||
|
} from '../src/views/scripts/stewardPlanModel.js'
|
||||||
|
import {
|
||||||
|
resolveSuggestedActionPrefill
|
||||||
|
} from '../src/utils/assistantSuggestedActionPrefill.js'
|
||||||
|
|
||||||
|
const OFF_TOPIC_PLAN = {
|
||||||
|
plan_id: 'steward-plan-off-topic',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
next_action: 'none',
|
||||||
|
tasks: [],
|
||||||
|
attachment_groups: [],
|
||||||
|
confirmation_groups: [],
|
||||||
|
candidate_flows: [],
|
||||||
|
summary: '这看起来跟财务任务没什么关系...',
|
||||||
|
suggested_prompts: [
|
||||||
|
'我想要申请明天去北京出差3天,支撑客户现场实施',
|
||||||
|
'我要报销上周去上海的高铁票',
|
||||||
|
'差旅住宿标准是多少'
|
||||||
|
],
|
||||||
|
thinking_events: [
|
||||||
|
{
|
||||||
|
event_id: 'off-topic-explain',
|
||||||
|
stage: 'intent_review',
|
||||||
|
title: '未识别到财务业务意图',
|
||||||
|
content: '用户输入的内容没有匹配到费用申请或费用报销相关的业务信号。',
|
||||||
|
status: 'completed'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
test('normalizeStewardPlan passes suggested_prompts through as trimmed strings', () => {
|
||||||
|
const normalized = normalizeStewardPlan({
|
||||||
|
plan_id: 'p-1',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
suggested_prompts: [
|
||||||
|
' 申请出差 ',
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
'报销高铁票'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assert.deepEqual(normalized.suggestedPrompts, ['申请出差', '报销高铁票'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizeStewardPlan falls back to empty suggested prompts when missing', () => {
|
||||||
|
const normalized = normalizeStewardPlan({ plan_id: 'p-2', plan_status: 'off_topic' })
|
||||||
|
assert.deepEqual(normalized.suggestedPrompts, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isOffTopicStewardPlan returns true only for off_topic plan status', () => {
|
||||||
|
assert.equal(isOffTopicStewardPlan({ plan_status: 'off_topic' }), true)
|
||||||
|
assert.equal(isOffTopicStewardPlan({ plan_status: 'needs_flow_confirmation' }), false)
|
||||||
|
assert.equal(isOffTopicStewardPlan({}), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardPlanMessageText renders friendly off_topic guidance', () => {
|
||||||
|
const text = buildStewardPlanMessageText(OFF_TOPIC_PLAN)
|
||||||
|
assert.match(text, /小财管家没看懂这件事/)
|
||||||
|
// 推荐话术本身不在正文里展示,而是作为按钮单独渲染,避免重复。
|
||||||
|
for (const prompt of OFF_TOPIC_PLAN.suggested_prompts) {
|
||||||
|
assert.equal(text.includes(prompt), false, `正文不应包含推荐话术:${prompt}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardPlanMessageText keeps off_topic branch ahead of pending flow branch', () => {
|
||||||
|
// 即使 summary 缺省,也走 off_topic 分支而非默认任务文案
|
||||||
|
const text = buildStewardPlanMessageText({
|
||||||
|
plan_id: 'p-off-topic-default',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
next_action: 'none',
|
||||||
|
suggested_prompts: ['申请出差']
|
||||||
|
})
|
||||||
|
assert.match(text, /小财管家没看懂这件事/)
|
||||||
|
assert.match(text, /费用申请.*费用报销|费用报销.*费用申请/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardSuggestedActions returns fill_composer actions for off_topic plan', () => {
|
||||||
|
const actions = buildStewardSuggestedActions(OFF_TOPIC_PLAN)
|
||||||
|
assert.equal(actions.length, OFF_TOPIC_PLAN.suggested_prompts.length)
|
||||||
|
for (const action of actions) {
|
||||||
|
assert.equal(action.action_type, ASSISTANT_SCOPE_ACTION_FILL_COMPOSER)
|
||||||
|
assert.equal(typeof action.payload.fill_text, 'string')
|
||||||
|
assert.ok(action.payload.fill_text.length > 0)
|
||||||
|
assert.equal(action.payload.steward_plan_id, OFF_TOPIC_PLAN.plan_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardSuggestedActions truncates long off_topic prompt labels', () => {
|
||||||
|
const longPrompt = '我想要申请明天去北京出差3天,支撑客户现场实施,需要预订酒店和高铁票'
|
||||||
|
const actions = buildStewardSuggestedActions({
|
||||||
|
plan_id: 'p-long',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
suggested_prompts: [longPrompt]
|
||||||
|
})
|
||||||
|
assert.equal(actions.length, 1)
|
||||||
|
assert.ok(actions[0].label.length <= 27) // 24 字符 + "..."
|
||||||
|
assert.ok(actions[0].label.endsWith('...'))
|
||||||
|
// payload.fill_text 必须保留完整话术,不被截断
|
||||||
|
assert.equal(actions[0].payload.fill_text, longPrompt)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardSuggestedActions keeps short off_topic prompt labels intact', () => {
|
||||||
|
const shortPrompt = '差旅住宿标准是多少'
|
||||||
|
const actions = buildStewardSuggestedActions({
|
||||||
|
plan_id: 'p-short',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
suggested_prompts: [shortPrompt]
|
||||||
|
})
|
||||||
|
assert.equal(actions[0].label, shortPrompt)
|
||||||
|
assert.equal(actions[0].payload.fill_text, shortPrompt)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStewardSuggestedActions returns empty array for off_topic plan without prompts', () => {
|
||||||
|
const actions = buildStewardSuggestedActions({
|
||||||
|
plan_id: 'p-empty',
|
||||||
|
plan_status: 'off_topic',
|
||||||
|
suggested_prompts: []
|
||||||
|
})
|
||||||
|
assert.deepEqual(actions, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('off_topic fill_composer action is resolved as composer prefill (fill not submit)', () => {
|
||||||
|
const actions = buildStewardSuggestedActions(OFF_TOPIC_PLAN)
|
||||||
|
const firstAction = actions[0]
|
||||||
|
const prefill = resolveSuggestedActionPrefill(firstAction)
|
||||||
|
assert.equal(prefill, OFF_TOPIC_PLAN.suggested_prompts[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveSuggestedActionPrefill reads payload.fill_text directly', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveSuggestedActionPrefill({
|
||||||
|
action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER,
|
||||||
|
payload: { fill_text: '帮我申请下周出差' }
|
||||||
|
}),
|
||||||
|
'帮我申请下周出差'
|
||||||
|
)
|
||||||
|
// 空字符串/空白应被忽略
|
||||||
|
assert.equal(
|
||||||
|
resolveSuggestedActionPrefill({
|
||||||
|
action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER,
|
||||||
|
payload: { fill_text: ' ' }
|
||||||
|
}),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('off_topic branch does not break pending flow confirmation actions', () => {
|
||||||
|
// pending flow 不应被 off_topic 分支拦截
|
||||||
|
const actions = buildStewardSuggestedActions({
|
||||||
|
plan_id: 'steward-plan-pending-flow',
|
||||||
|
plan_status: 'needs_flow_confirmation',
|
||||||
|
next_action: 'confirm_flow',
|
||||||
|
pending_flow_confirmation: {
|
||||||
|
status: 'pending',
|
||||||
|
reason: '缺少申请或报销动作词。',
|
||||||
|
candidate_flows: [
|
||||||
|
{
|
||||||
|
flow_id: 'travel_application',
|
||||||
|
label: '补办出差申请',
|
||||||
|
confidence: 0.6,
|
||||||
|
ontology_fields: { location: '上海' },
|
||||||
|
missing_fields: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert.equal(actions.length, 1)
|
||||||
|
assert.equal(actions[0].payload.flow_id, 'travel_application')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user