feat: 小财管家意图规划与报销提交编排增强
- 完善管家意图识别、模型计划构建与规划器调度 - 重构差旅报销提交编排器与管家计划流程前端交互 - 优化报销消息项样式与文档中心视图 - 新增小财管家与附件上传风险前置复核设计文档 - 补充管家规划器与文档中心测试覆盖
This commit is contained in:
@@ -13,7 +13,70 @@ const TASK_TYPE_LABELS = {
|
||||
|
||||
const AGENT_LABELS = {
|
||||
application_assistant: '申请助手',
|
||||
reimbursement_assistant: '报销助手'
|
||||
application: '申请助手',
|
||||
expense_application: '申请助手',
|
||||
reimbursement_assistant: '报销助手',
|
||||
reimbursement: '报销助手',
|
||||
expense: '报销助手'
|
||||
}
|
||||
|
||||
const FIELD_DISPLAY_CONFIG = {
|
||||
expense_type: {
|
||||
label: '费用类型',
|
||||
hint: '例如差旅、交通、住宿、业务招待'
|
||||
},
|
||||
time_range: {
|
||||
label: '发生时间',
|
||||
hint: '申请时填出差起止日期,报销时填费用发生日期'
|
||||
},
|
||||
location: {
|
||||
label: '地点',
|
||||
hint: '出差城市或费用发生地点'
|
||||
},
|
||||
reason: {
|
||||
label: '事由',
|
||||
hint: '出差、报销或业务活动的具体原因'
|
||||
},
|
||||
amount: {
|
||||
label: '金额',
|
||||
hint: '申请时为预计金额,报销时为实际报销金额'
|
||||
},
|
||||
transport_mode: {
|
||||
label: '出行方式',
|
||||
hint: '例如高铁、飞机、自驾、出租车'
|
||||
},
|
||||
attachments: {
|
||||
label: '附件/凭证',
|
||||
hint: '发票、行程单、付款截图或其他证明材料'
|
||||
},
|
||||
customer_name: {
|
||||
label: '客户或项目对象',
|
||||
hint: '涉及的客户、单位或项目名称'
|
||||
},
|
||||
merchant_name: {
|
||||
label: '商户/开票方',
|
||||
hint: '发票或付款凭证上的商户名称'
|
||||
},
|
||||
department_name: {
|
||||
label: '所属部门',
|
||||
hint: '申请人或费用归属部门'
|
||||
},
|
||||
employee_name: {
|
||||
label: '申请人',
|
||||
hint: '发起申请或报销的员工姓名'
|
||||
},
|
||||
employee_no: {
|
||||
label: '员工编号',
|
||||
hint: '公司内部员工编号'
|
||||
}
|
||||
}
|
||||
|
||||
const FIELD_ALIASES = {
|
||||
occurred_date: 'time_range',
|
||||
business_time: 'time_range',
|
||||
reason_value: 'reason',
|
||||
transport_type: 'transport_mode',
|
||||
application_transport_mode: 'transport_mode'
|
||||
}
|
||||
|
||||
export function buildStewardPlanRequest({ rawText = '', files = [], currentUser = {} } = {}) {
|
||||
@@ -47,6 +110,7 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
|
||||
planStatus: String(rawPlan.plan_status || rawPlan.planStatus || ''),
|
||||
summary: String(rawPlan.summary || ''),
|
||||
visibleThinkingEventCount,
|
||||
initialSummaryOnly: Boolean(rawPlan.initial_summary_only || rawPlan.initialSummaryOnly || options.initialSummaryOnly),
|
||||
thinkingEvents: Array.isArray(rawPlan.thinking_events)
|
||||
? rawPlan.thinking_events.map((item) => ({
|
||||
eventId: String(item.event_id || item.eventId || ''),
|
||||
@@ -57,22 +121,30 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
|
||||
}))
|
||||
: [],
|
||||
tasks: Array.isArray(rawPlan.tasks)
|
||||
? rawPlan.tasks.map((item) => ({
|
||||
taskId: String(item.task_id || item.taskId || ''),
|
||||
taskType: String(item.task_type || item.taskType || ''),
|
||||
taskTypeLabel: TASK_TYPE_LABELS[String(item.task_type || item.taskType || '')] || '财务任务',
|
||||
assignedAgent: String(item.assigned_agent || item.assignedAgent || ''),
|
||||
assignedAgentLabel: AGENT_LABELS[String(item.assigned_agent || item.assignedAgent || '')] || '财务助手',
|
||||
title: String(item.title || ''),
|
||||
summary: String(item.summary || ''),
|
||||
status: String(item.status || ''),
|
||||
confidence: Number(item.confidence || 0),
|
||||
ontologyFields: item.ontology_fields || item.ontologyFields || {},
|
||||
missingFields: Array.isArray(item.missing_fields || item.missingFields)
|
||||
? rawPlan.tasks.map((item) => {
|
||||
const taskType = String(item.task_type || item.taskType || '')
|
||||
const missingFields = Array.isArray(item.missing_fields || item.missingFields)
|
||||
? item.missing_fields || item.missingFields
|
||||
: [],
|
||||
confirmationRequired: item.confirmation_required ?? item.confirmationRequired ?? true
|
||||
}))
|
||||
: []
|
||||
return {
|
||||
taskId: String(item.task_id || item.taskId || ''),
|
||||
taskType,
|
||||
taskTypeLabel: TASK_TYPE_LABELS[taskType] || '财务任务',
|
||||
assignedAgent: String(item.assigned_agent || item.assignedAgent || ''),
|
||||
assignedAgentLabel:
|
||||
AGENT_LABELS[String(item.assigned_agent || item.assignedAgent || '')] ||
|
||||
AGENT_LABELS[taskType] ||
|
||||
'小财管家',
|
||||
title: String(item.title || ''),
|
||||
summary: String(item.summary || ''),
|
||||
status: String(item.status || ''),
|
||||
confidence: Number(item.confidence || 0),
|
||||
ontologyFields: item.ontology_fields || item.ontologyFields || {},
|
||||
missingFields,
|
||||
missingFieldItems: buildStewardFieldItems(missingFields, taskType),
|
||||
confirmationRequired: item.confirmation_required ?? item.confirmationRequired ?? true
|
||||
}
|
||||
})
|
||||
: [],
|
||||
attachmentGroups: Array.isArray(rawPlan.attachment_groups)
|
||||
? rawPlan.attachment_groups.map((item) => ({
|
||||
@@ -99,34 +171,67 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
|
||||
|
||||
export function buildStewardPlanMessageText(plan) {
|
||||
const normalized = normalizeStewardPlan(plan)
|
||||
const taskLines = normalized.tasks.map((task, index) =>
|
||||
`${index + 1}. ${task.title || task.taskTypeLabel},交给${task.assignedAgentLabel}。`
|
||||
const nextContext = resolveNextActionContext(normalized)
|
||||
const orderedTasks = buildOrderedStewardTasks(normalized, nextContext?.task)
|
||||
const taskLines = orderedTasks.map((task, index) =>
|
||||
`${index + 1}. **${buildTaskOrderVerb(index)}${buildTaskOrderTarget(task)}**\n - ${buildTaskOrderActionDescription(task)}`
|
||||
)
|
||||
return [
|
||||
'**小财管家已完成任务拆解。**',
|
||||
'### 我会这样推进',
|
||||
'',
|
||||
normalized.summary || `我识别到 ${normalized.tasks.length} 个待处理任务,请确认后继续执行。`,
|
||||
`我识别到 **${normalized.tasks.length} 个财务事项**,会按顺序逐步处理,不会一次性把所有动作都执行掉。`,
|
||||
'',
|
||||
...taskLines
|
||||
].join('\n')
|
||||
...taskLines,
|
||||
'',
|
||||
'如果这个顺序没问题,请回复 **确定**。我会先进入第一步,并在具体步骤里再判断需要你补充哪些信息。'
|
||||
].filter((line, index, lines) => line || lines[index - 1]).join('\n')
|
||||
}
|
||||
|
||||
export function buildStewardFieldItems(fields = [], taskType = '') {
|
||||
const safeFields = Array.isArray(fields) ? fields : []
|
||||
const seen = new Set()
|
||||
return safeFields
|
||||
.map((field) => normalizeFieldKey(field))
|
||||
.filter((field) => {
|
||||
if (!field || seen.has(field)) {
|
||||
return false
|
||||
}
|
||||
seen.add(field)
|
||||
return true
|
||||
})
|
||||
.map((field) => resolveFieldDisplay(field, taskType))
|
||||
}
|
||||
|
||||
export function formatStewardMissingFieldList(fields = [], taskType = '') {
|
||||
return buildStewardFieldItems(fields, taskType)
|
||||
.map((item) => item.hint ? `${item.label}(${item.hint})` : item.label)
|
||||
.join('、')
|
||||
}
|
||||
|
||||
export function formatStewardOntologyFields(fields = {}, taskType = '') {
|
||||
return Object.entries(fields || {})
|
||||
.filter(([, value]) => String(value || '').trim())
|
||||
.map(([key, value]) => {
|
||||
const field = resolveFieldDisplay(key, taskType)
|
||||
return `${field.label}:${value}`
|
||||
})
|
||||
.join(';')
|
||||
}
|
||||
|
||||
export function buildStewardSuggestedActions(plan) {
|
||||
const normalized = normalizeStewardPlan(plan)
|
||||
const taskById = new Map(normalized.tasks.map((task) => [task.taskId, task]))
|
||||
const groupById = new Map(normalized.attachmentGroups.map((group) => [group.groupId, group]))
|
||||
return normalized.confirmationGroups.map((action) => {
|
||||
const actionType = String(action.action_type || action.actionType || '').trim()
|
||||
const taskId = String(action.target_task_id || action.targetTaskId || '').trim()
|
||||
const groupId = String(action.attachment_group_id || action.attachmentGroupId || '').trim()
|
||||
const task = taskById.get(taskId)
|
||||
const group = groupById.get(groupId)
|
||||
const targetSessionType = actionType === 'confirm_create_application'
|
||||
? SESSION_TYPE_APPLICATION
|
||||
: SESSION_TYPE_EXPENSE
|
||||
return {
|
||||
label: String(action.label || '确认继续处理'),
|
||||
description: String(action.description || ''),
|
||||
const nextContext = resolveNextActionContext(normalized)
|
||||
if (!nextContext) {
|
||||
return []
|
||||
}
|
||||
const { action, actionType, task, group } = nextContext
|
||||
const targetSessionType = actionType === 'confirm_create_application'
|
||||
? SESSION_TYPE_APPLICATION
|
||||
: SESSION_TYPE_EXPENSE
|
||||
return [
|
||||
{
|
||||
label: buildNextActionLabel(actionType),
|
||||
description: buildNextActionDescription(actionType, normalized, task, group),
|
||||
icon: actionType === 'confirm_create_application'
|
||||
? 'mdi mdi-file-plus-outline'
|
||||
: actionType === 'confirm_attachment_group'
|
||||
@@ -135,17 +240,198 @@ export function buildStewardSuggestedActions(plan) {
|
||||
action_type: ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||
payload: {
|
||||
session_type: targetSessionType,
|
||||
carry_text: buildStewardCarryText(actionType, task, group),
|
||||
carry_text: buildStewardCarryText(actionType, task, group, normalized),
|
||||
carry_files: actionType !== 'confirm_create_application',
|
||||
auto_submit: true,
|
||||
steward_confirmation_id: String(action.confirmation_id || action.confirmationId || ''),
|
||||
steward_plan_id: normalized.planId
|
||||
steward_plan_id: normalized.planId,
|
||||
steward_next_task_id: task?.taskId || '',
|
||||
steward_remaining_task_count: normalized.tasks.filter((item) => item.taskId !== task?.taskId).length,
|
||||
steward_remaining_tasks: buildRemainingTaskPayload(normalized, task?.taskId)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
function buildStewardCarryText(actionType, task, group) {
|
||||
function resolveNextActionContext(normalized) {
|
||||
const applicationTask = normalized.tasks.find((task) => task.taskType === 'expense_application')
|
||||
const applicationAction = applicationTask
|
||||
? findConfirmationAction(normalized, 'confirm_create_application', applicationTask.taskId)
|
||||
: null
|
||||
if (applicationAction) {
|
||||
return {
|
||||
action: applicationAction,
|
||||
actionType: 'confirm_create_application',
|
||||
task: applicationTask,
|
||||
group: null
|
||||
}
|
||||
}
|
||||
|
||||
const reimbursementTask = normalized.tasks.find((task) => task.taskType === 'reimbursement')
|
||||
const reimbursementAction = reimbursementTask
|
||||
? findConfirmationAction(normalized, 'confirm_create_reimbursement_draft', reimbursementTask.taskId)
|
||||
: null
|
||||
if (reimbursementAction) {
|
||||
return {
|
||||
action: reimbursementAction,
|
||||
actionType: 'confirm_create_reimbursement_draft',
|
||||
task: reimbursementTask,
|
||||
group: findAttachmentGroupForTask(normalized, reimbursementTask.taskId)
|
||||
}
|
||||
}
|
||||
|
||||
const attachmentAction = normalized.confirmationGroups.find((action) =>
|
||||
normalizeActionType(action) === 'confirm_attachment_group'
|
||||
)
|
||||
if (attachmentAction) {
|
||||
const groupId = String(attachmentAction.attachment_group_id || attachmentAction.attachmentGroupId || '').trim()
|
||||
const group = normalized.attachmentGroups.find((item) => item.groupId === groupId)
|
||||
const task = normalized.tasks.find((item) => item.taskId === group?.targetTaskId)
|
||||
return {
|
||||
action: attachmentAction,
|
||||
actionType: 'confirm_attachment_group',
|
||||
task,
|
||||
group
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackAction = normalized.confirmationGroups[0]
|
||||
if (!fallbackAction) {
|
||||
return null
|
||||
}
|
||||
const actionType = normalizeActionType(fallbackAction)
|
||||
const taskId = String(fallbackAction.target_task_id || fallbackAction.targetTaskId || '').trim()
|
||||
return {
|
||||
action: fallbackAction,
|
||||
actionType,
|
||||
task: normalized.tasks.find((task) => task.taskId === taskId),
|
||||
group: null
|
||||
}
|
||||
}
|
||||
|
||||
function findConfirmationAction(normalized, actionType, taskId) {
|
||||
return normalized.confirmationGroups.find((action) =>
|
||||
normalizeActionType(action) === actionType
|
||||
&& String(action.target_task_id || action.targetTaskId || '').trim() === taskId
|
||||
) || normalized.confirmationGroups.find((action) => normalizeActionType(action) === actionType)
|
||||
}
|
||||
|
||||
function findAttachmentGroupForTask(normalized, taskId) {
|
||||
return normalized.attachmentGroups.find((group) => group.targetTaskId === taskId)
|
||||
|| normalized.attachmentGroups[0]
|
||||
|| null
|
||||
}
|
||||
|
||||
function normalizeActionType(action) {
|
||||
return String(action?.action_type || action?.actionType || '').trim()
|
||||
}
|
||||
|
||||
function buildStewardExecutionSummary(normalized) {
|
||||
const attachmentCount = normalized.attachmentGroups
|
||||
.reduce((total, group) => total + group.attachmentNames.length, 0)
|
||||
const summary = [`我识别到 **${normalized.tasks.length} 个待处理任务**`]
|
||||
if (attachmentCount) {
|
||||
summary.push(`并形成 ${attachmentCount} 份附件的归集建议`)
|
||||
}
|
||||
summary.push(`。${buildTaskOrderDescription(normalized)}`)
|
||||
return summary.join('')
|
||||
}
|
||||
|
||||
function buildOrderedStewardTasks(normalized, nextTask = null) {
|
||||
if (!nextTask?.taskId) {
|
||||
return normalized.tasks
|
||||
}
|
||||
return [
|
||||
nextTask,
|
||||
...normalized.tasks.filter((task) => task.taskId !== nextTask.taskId)
|
||||
]
|
||||
}
|
||||
|
||||
function buildTaskOrderVerb(index) {
|
||||
if (index === 0) {
|
||||
return '先'
|
||||
}
|
||||
if (index === 1) {
|
||||
return '再'
|
||||
}
|
||||
return '然后'
|
||||
}
|
||||
|
||||
function buildTaskOrderTarget(task) {
|
||||
const title = task.title || task.taskTypeLabel
|
||||
if (task.taskType === 'expense_application') {
|
||||
return `创建“${title}”`
|
||||
}
|
||||
if (task.taskType === 'reimbursement') {
|
||||
return `处理“${title}”`
|
||||
}
|
||||
return `处理“${title}”`
|
||||
}
|
||||
|
||||
function buildTaskOrderActionDescription(task) {
|
||||
const agent = task.assignedAgentLabel || '对应助手'
|
||||
if (task.taskType === 'expense_application') {
|
||||
return `交给${agent}生成申请单核对结果,确认无误后再进入后续动作。`
|
||||
}
|
||||
if (task.taskType === 'reimbursement') {
|
||||
return `交给${agent}整理报销核对结果,等前一步完成后再继续推进。`
|
||||
}
|
||||
return `交给${agent}处理,执行前会先让你确认。`
|
||||
}
|
||||
|
||||
function buildTaskOrderDescription(normalized) {
|
||||
const hasApplication = normalized.tasks.some((task) => task.taskType === 'expense_application')
|
||||
const hasReimbursement = normalized.tasks.some((task) => task.taskType === 'reimbursement')
|
||||
if (hasApplication && hasReimbursement) {
|
||||
return '处理顺序是:先创建申请单,再引导填写报销单。'
|
||||
}
|
||||
if (hasApplication) {
|
||||
return '我会先引导创建申请单并等待你确认。'
|
||||
}
|
||||
if (hasReimbursement) {
|
||||
return '我会引导填写报销单并等待你确认。'
|
||||
}
|
||||
return '我会按识别顺序逐项推进,并在执行前等待你确认。'
|
||||
}
|
||||
|
||||
function buildNextTaskLead(task) {
|
||||
if (task.taskType === 'expense_application') {
|
||||
return `先创建“${task.title || task.taskTypeLabel}”`
|
||||
}
|
||||
if (task.taskType === 'reimbursement') {
|
||||
return `继续填写“${task.title || task.taskTypeLabel}”`
|
||||
}
|
||||
return `处理“${task.title || task.taskTypeLabel}”`
|
||||
}
|
||||
|
||||
function buildNextActionLabel(actionType) {
|
||||
if (actionType === 'confirm_create_application') {
|
||||
return '确定,先创建申请单'
|
||||
}
|
||||
if (actionType === 'confirm_attachment_group') {
|
||||
return '确定,确认附件归集'
|
||||
}
|
||||
return '确定,继续填写报销单'
|
||||
}
|
||||
|
||||
function buildNextActionDescription(actionType, normalized, task, group) {
|
||||
const remainingCount = normalized.tasks.filter((item) => item.taskId !== task?.taskId).length
|
||||
if (actionType === 'confirm_create_application') {
|
||||
return remainingCount > 0
|
||||
? '申请助手会先生成申请单核对结果,完成后再继续引导后续报销。'
|
||||
: '申请助手会生成申请单核对结果,入库前仍需确认。'
|
||||
}
|
||||
if (actionType === 'confirm_attachment_group') {
|
||||
return group?.attachmentNames?.length
|
||||
? `先归集 ${group.attachmentNames.length} 份附件,再进入报销核对。`
|
||||
: '先确认附件归集,再进入报销核对。'
|
||||
}
|
||||
return group?.attachmentNames?.length
|
||||
? `报销助手会带入 ${group.attachmentNames.length} 份相关附件生成核对结果。`
|
||||
: '报销助手会根据当前任务生成报销核对结果。'
|
||||
}
|
||||
|
||||
function buildStewardCarryText(actionType, task, group, normalized = null) {
|
||||
if (actionType === 'confirm_attachment_group' && group) {
|
||||
return [
|
||||
`我确认将以下附件归集为${group.sceneLabel || '当前报销任务'},请继续整理报销核对信息。`,
|
||||
@@ -160,14 +446,79 @@ function buildStewardCarryText(actionType, task, group) {
|
||||
return '我确认继续处理这项财务任务,请按现有流程核对信息。'
|
||||
}
|
||||
|
||||
const fields = Object.entries(task.ontologyFields || {})
|
||||
.filter(([, value]) => String(value || '').trim())
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
return [
|
||||
`我确认处理“小财管家”识别的任务:${task.title || task.taskTypeLabel}。`,
|
||||
const fields = formatStewardOntologyFields(task.ontologyFields || {}, task.taskType)
|
||||
const missingFields = formatStewardMissingFieldList(task.missingFields || [], task.taskType)
|
||||
const lines = [
|
||||
actionType === 'confirm_create_application'
|
||||
? `小财管家已完成意图识别,请先创建申请单:${task.title || task.taskTypeLabel}。`
|
||||
: `小财管家已完成意图识别,请继续填写报销单:${task.title || task.taskTypeLabel}。`,
|
||||
task.summary ? `任务摘要:${task.summary}` : '',
|
||||
fields.length ? `本体字段:${fields.join(';')}` : '',
|
||||
task.missingFields.length ? `待补充字段:${task.missingFields.join('、')}` : '',
|
||||
'请按现有流程生成核对结果,并在需要入库、绑定附件或提交审批前让我再次确认。'
|
||||
].filter(Boolean).join('\n')
|
||||
fields ? `已识别信息:${fields}` : '',
|
||||
group?.attachmentNames?.length ? `相关附件:${group.attachmentNames.join('、')}` : '',
|
||||
group?.excludedAttachmentNames?.length ? `暂不归集附件:${group.excludedAttachmentNames.join('、')}` : '',
|
||||
missingFields ? `还需要补充:${missingFields}` : '',
|
||||
actionType === 'confirm_create_application'
|
||||
? '请直接生成申请单核对结果;信息足够时生成申请单,但在入库或提交审批前仍需让我确认。'
|
||||
: '请直接生成报销核对结果;需要创建草稿、绑定附件或提交审批前仍需让我确认。'
|
||||
]
|
||||
const remainingTaskText = normalized ? buildRemainingTaskText(normalized, task.taskId) : ''
|
||||
if (remainingTaskText) {
|
||||
lines.push(remainingTaskText)
|
||||
}
|
||||
return lines.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
function normalizeFieldKey(field) {
|
||||
const key = String(field || '').trim()
|
||||
return FIELD_ALIASES[key] || key
|
||||
}
|
||||
|
||||
function resolveFieldDisplay(field, taskType = '') {
|
||||
const key = normalizeFieldKey(field)
|
||||
const config = FIELD_DISPLAY_CONFIG[key] || {
|
||||
label: key.replace(/_/g, ' '),
|
||||
hint: ''
|
||||
}
|
||||
if (key === 'amount') {
|
||||
return {
|
||||
key,
|
||||
label: taskType === 'expense_application' ? '预计金额' : '报销金额',
|
||||
hint: taskType === 'expense_application'
|
||||
? '本次申请预计发生的费用'
|
||||
: '本次需要报销的实际金额'
|
||||
}
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label: config.label,
|
||||
hint: config.hint
|
||||
}
|
||||
}
|
||||
|
||||
function buildRemainingTaskText(normalized, currentTaskId) {
|
||||
const remainingTasks = normalized.tasks.filter((task) => task.taskId !== currentTaskId)
|
||||
if (!remainingTasks.length) {
|
||||
return ''
|
||||
}
|
||||
const taskLines = remainingTasks.map((task, index) =>
|
||||
`${index + 1}. ${task.title || task.taskTypeLabel}(${task.assignedAgentLabel}):${task.summary || '待继续核对'}`
|
||||
)
|
||||
return [
|
||||
'当前步骤完成后,请继续引导我处理后续任务:',
|
||||
...taskLines
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function buildRemainingTaskPayload(normalized, currentTaskId) {
|
||||
return normalized.tasks
|
||||
.filter((task) => task.taskId !== currentTaskId)
|
||||
.map((task) => ({
|
||||
task_id: task.taskId,
|
||||
task_type: task.taskType,
|
||||
title: task.title,
|
||||
summary: task.summary,
|
||||
assigned_agent: task.assignedAgent,
|
||||
ontology_fields: task.ontologyFields || {},
|
||||
missing_fields: task.missingFields || []
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user