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:
caoxiaozhu
2026-06-18 14:15:30 +08:00
parent cce19e4c40
commit 43432534d8
5 changed files with 323 additions and 25 deletions

View File

@@ -1,5 +1,6 @@
import {
ASSISTANT_SCOPE_ACTION_SWITCH,
ASSISTANT_SCOPE_ACTION_FILL_COMPOSER
} from '../../utils/assistantSessionScope.js'
import {
SESSION_TYPE_APPLICATION,
@@ -200,12 +201,18 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
? rawPlan.confirmation_groups
: [],
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) {
const normalized = normalizeStewardPlan(plan)
if (isOffTopicPlan(normalized)) {
return buildOffTopicMessageText(normalized)
}
if (isPendingFlowConfirmationPlan(normalized)) {
return buildPendingFlowConfirmationMessageText(normalized)
}
@@ -215,13 +222,13 @@ export function buildStewardPlanMessageText(plan) {
`${index + 1}. **${buildTaskOrderVerb(index)}${buildTaskOrderTarget(task)}**\n - ${buildTaskOrderActionDescription(task)}`
)
return [
'### 我会这样推进',
'### 我先帮你把步骤理清楚',
'',
`我识别到 **${normalized.tasks.length} 个财务事项**,会按顺序逐步处理,不会一次性把所有动作都执行掉。`,
buildStewardPlanFriendlyIntro(normalized),
'',
...taskLines,
'',
'如果这个顺序没问题,回复 **确定**。我会先进入第一步,并在具体步骤里再判断需要你补充哪些信息。'
'你看这个顺序是否合适?如果没问题,回复 **确定** 就行。我会先帮你进入第一步,需要补充的信息会在具体步骤里再温和提醒你。'
].filter((line, index, lines) => line || lines[index - 1]).join('\n')
}
@@ -284,6 +291,18 @@ export function formatStewardOntologyFields(fields = {}, taskType = '') {
export function buildStewardSuggestedActions(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)) {
return normalized.candidateFlows.map((flow) => ({
label: flow.label,
@@ -383,6 +402,28 @@ function isPendingFlowConfirmationPlan(normalized) {
) && 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) {
const fields = normalized.candidateFlows[0]?.ontologyFields || {}
const knownParts = formatStewardOntologyFields(fields, 'expense_application')
@@ -511,10 +552,10 @@ function buildTaskOrderVerb(index) {
function buildTaskOrderTarget(task) {
const title = task.title || task.taskTypeLabel
if (task.taskType === 'expense_application') {
return `创建${title}`
return `整理${title}`
}
if (task.taskType === 'reimbursement') {
return `处理${title}`
return `核对${title}`
}
return `处理“${title}`
}
@@ -522,12 +563,19 @@ function buildTaskOrderTarget(task) {
function buildTaskOrderActionDescription(task) {
const agent = task.assignedAgentLabel || '对应助手'
if (task.taskType === 'expense_application') {
return `交给${agent}生成申请单核对结果,确认无误后再进入后续动作`
return `我会请${agent}先把申请单草稿整理出来,方便你核对关键信息,再决定是否继续`
}
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) {