feat(web): update views and services
Views: - AppShellRouteView.vue: update route view - SettingsView.vue: update settings view - TravelReimbursementCreateView.vue: update travel form view - scripts/SettingsView.js: update settings view logic - scripts/TravelReimbursementCreateView.js: update travel form logic Services: - services/orchestrator.js: update orchestrator service client
This commit is contained in:
@@ -6,3 +6,21 @@ export function runOrchestrator(payload) {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchLatestConversation(userId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
user_id: String(userId || '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiRequest(`/orchestrator/conversations/latest?${params.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearUserConversations(userId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
user_id: String(userId || '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiRequest(`/orchestrator/conversations?${params.toString()}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
:key="smartEntrySessionId"
|
:key="smartEntrySessionId"
|
||||||
:initial-prompt="smartEntryContext.prompt"
|
:initial-prompt="smartEntryContext.prompt"
|
||||||
:initial-files="smartEntryContext.files"
|
:initial-files="smartEntryContext.files"
|
||||||
|
:initial-conversation="smartEntryContext.conversation"
|
||||||
:entry-source="smartEntryContext.source"
|
:entry-source="smartEntryContext.source"
|
||||||
:request-context="smartEntryContext.request"
|
:request-context="smartEntryContext.request"
|
||||||
@close="closeSmartEntry"
|
@close="closeSmartEntry"
|
||||||
|
|||||||
@@ -183,6 +183,67 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="activeSection === 'session'">
|
||||||
|
<section class="settings-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h4>会话保留策略</h4>
|
||||||
|
<p>控制智能体会话在系统中的保留时长,超过保留期的历史会话会自动清理。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid compact-grid">
|
||||||
|
<label class="field">
|
||||||
|
<span><em>*</em> 保留会话天数</span>
|
||||||
|
<div
|
||||||
|
ref="sessionRetentionPickerRef"
|
||||||
|
class="session-picker-filter"
|
||||||
|
:class="{ open: sessionRetentionPickerOpen }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="session-picker-trigger"
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="sessionRetentionPickerOpen"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
@click="toggleSessionRetentionPicker"
|
||||||
|
>
|
||||||
|
<span class="session-picker-label">{{ pageState.sessionForm.conversationRetentionDays }} 天</span>
|
||||||
|
<i class="mdi mdi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="sessionRetentionPickerOpen"
|
||||||
|
class="session-picker-popover"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="选择会话保留天数"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<strong>选择会话保留天数</strong>
|
||||||
|
<button type="button" aria-label="关闭会话保留天数选择" @click="closeSessionRetentionPicker">
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="session-picker-option-list">
|
||||||
|
<button
|
||||||
|
v-for="option in sessionRetentionOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
class="session-picker-option"
|
||||||
|
:class="{ active: pageState.sessionForm.conversationRetentionDays === option.value }"
|
||||||
|
@click="selectSessionRetentionDays(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small>最小 1 天,最大 10 天,按会话最后活跃时间计算。</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeSection === 'llm'">
|
<template v-else-if="activeSection === 'llm'">
|
||||||
<div class="model-grid">
|
<div class="model-grid">
|
||||||
<section class="settings-card">
|
<section class="settings-card">
|
||||||
|
|||||||
@@ -90,6 +90,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block">
|
||||||
|
<strong>系统识别</strong>
|
||||||
|
<p class="review-summary">{{ message.reviewPayload.intent_summary }}</p>
|
||||||
|
<div v-if="message.reviewPayload.slot_cards?.length" class="review-mini-grid">
|
||||||
|
<article
|
||||||
|
v-for="item in message.reviewPayload.slot_cards.slice(0, 4)"
|
||||||
|
:key="`${message.id}-${item.key}`"
|
||||||
|
class="review-slot-card compact"
|
||||||
|
:class="item.status"
|
||||||
|
>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<strong>{{ item.value || '待补充' }}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div v-if="message.reviewPayload.confirmation_actions?.length" class="action-list compact">
|
||||||
|
<article
|
||||||
|
v-for="item in message.reviewPayload.confirmation_actions"
|
||||||
|
:key="`${message.id}-${item.action_type}-${item.label}`"
|
||||||
|
class="action-card"
|
||||||
|
:class="item.emphasis"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>{{ item.label }}</strong>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="message.role === 'assistant' && message.draftPayload" class="draft-preview">
|
<div v-if="message.role === 'assistant' && message.draftPayload" class="draft-preview">
|
||||||
<header>
|
<header>
|
||||||
<strong>{{ message.draftPayload.title }}</strong>
|
<strong>{{ message.draftPayload.title }}</strong>
|
||||||
@@ -193,6 +222,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>识别结果</h4>
|
||||||
|
</div>
|
||||||
|
<div class="note-block">
|
||||||
|
<span>{{ currentInsight.agent.reviewPayload.intent }}</span>
|
||||||
|
<strong>{{ currentInsight.agent.reviewPayload.intent_summary }}</strong>
|
||||||
|
<p v-if="currentInsight.agent.reviewPayload.missing_slots?.length">
|
||||||
|
当前仍缺少:{{ currentInsight.agent.reviewPayload.missing_slots.join('、') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="insight-card">
|
<section class="insight-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h4>运行明细</h4>
|
<h4>运行明细</h4>
|
||||||
@@ -217,6 +259,132 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload?.risk_briefs?.length" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>历史风险与注意事项</h4>
|
||||||
|
</div>
|
||||||
|
<div class="review-brief-list">
|
||||||
|
<article
|
||||||
|
v-for="item in currentInsight.agent.reviewPayload.risk_briefs"
|
||||||
|
:key="`${item.title}-${item.level}`"
|
||||||
|
class="review-brief-card"
|
||||||
|
:class="item.level"
|
||||||
|
>
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
<p>{{ item.content }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload?.slot_cards?.length" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>待确认字段</h4>
|
||||||
|
</div>
|
||||||
|
<div class="review-slot-grid">
|
||||||
|
<article
|
||||||
|
v-for="item in currentInsight.agent.reviewPayload.slot_cards"
|
||||||
|
:key="item.key"
|
||||||
|
class="review-slot-card"
|
||||||
|
:class="item.status"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<small>{{ item.source }}</small>
|
||||||
|
</header>
|
||||||
|
<strong>{{ item.value || '待补充' }}</strong>
|
||||||
|
<p v-if="item.hint">{{ item.hint }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload?.claim_groups?.length" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>分单建议</h4>
|
||||||
|
</div>
|
||||||
|
<div class="review-claim-list">
|
||||||
|
<article
|
||||||
|
v-for="item in currentInsight.agent.reviewPayload.claim_groups"
|
||||||
|
:key="item.group_code"
|
||||||
|
class="review-claim-card"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
<span>{{ item.scene_label }}</span>
|
||||||
|
</header>
|
||||||
|
<p>{{ item.rationale }}</p>
|
||||||
|
<div class="message-detail-chip-row">
|
||||||
|
<span class="message-action-chip">票据 {{ item.document_indexes.join('、') || '待补' }}</span>
|
||||||
|
<span class="message-action-chip">金额 {{ item.amount_total.toFixed(2) }} 元</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload?.document_cards?.length" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>票据识别结果</h4>
|
||||||
|
</div>
|
||||||
|
<div class="review-document-list">
|
||||||
|
<article
|
||||||
|
v-for="item in currentInsight.agent.reviewPayload.document_cards"
|
||||||
|
:key="`${item.index}-${item.filename}`"
|
||||||
|
class="review-document-card"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<strong>票据 {{ item.index }}</strong>
|
||||||
|
<span>{{ item.filename }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="message-action-chip">{{ item.scene_label }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="document-preview" :class="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.kind || 'file'">
|
||||||
|
<img
|
||||||
|
v-if="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.kind === 'image'"
|
||||||
|
:src="resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.url"
|
||||||
|
:alt="item.filename"
|
||||||
|
/>
|
||||||
|
<div v-else class="document-preview-placeholder">
|
||||||
|
<i class="mdi mdi-file-document-outline"></i>
|
||||||
|
<span>{{ resolveDocumentPreview(currentInsight.agent.filePreviews, item.filename)?.kind === 'pdf' ? 'PDF' : '附件' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ item.summary || '暂无摘要。' }}</p>
|
||||||
|
|
||||||
|
<div v-if="item.fields?.length" class="review-doc-field-grid">
|
||||||
|
<article v-for="field in item.fields" :key="`${item.filename}-${field.label}`" class="review-doc-field-card">
|
||||||
|
<span>{{ field.label }}</span>
|
||||||
|
<strong>{{ field.value }}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.warnings?.length" class="message-detail-chip-row">
|
||||||
|
<span v-for="warning in item.warnings" :key="warning" class="message-risk-chip">{{ warning }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="currentInsight.agent.reviewPayload?.confirmation_actions?.length" class="insight-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h4>确认动作</h4>
|
||||||
|
</div>
|
||||||
|
<div class="action-list">
|
||||||
|
<article
|
||||||
|
v-for="item in currentInsight.agent.reviewPayload.confirmation_actions"
|
||||||
|
:key="`${item.action_type}-${item.label}`"
|
||||||
|
class="action-card"
|
||||||
|
:class="item.emphasis"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>{{ item.label }}</strong>
|
||||||
|
<p>{{ item.description || item.action_type }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section v-if="currentInsight.agent.selectedCapabilityCodes?.length" class="insight-card">
|
<section v-if="currentInsight.agent.selectedCapabilityCodes?.length" class="insight-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h4>命中能力</h4>
|
<h4>命中能力</h4>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
import { fetchSettings, saveSettings, testModelConnectivity } from '../../services/settings.js'
|
import { fetchSettings, saveSettings, testModelConnectivity } from '../../services/settings.js'
|
||||||
@@ -27,6 +27,14 @@ const SECTION_DEFINITIONS = [
|
|||||||
longDesc: '集中管理管理员账号、邮箱和登录安全策略。密码仅在当前输入时可见,不会写入浏览器草稿。',
|
longDesc: '集中管理管理员账号、邮箱和登录安全策略。密码仅在当前输入时可见,不会写入浏览器草稿。',
|
||||||
actionLabel: '保存安全设置'
|
actionLabel: '保存安全设置'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'session',
|
||||||
|
label: '会话设置',
|
||||||
|
title: '会话留存设置',
|
||||||
|
desc: '会话保留天数',
|
||||||
|
longDesc: '统一配置智能体会话的保留天数。超过保留期的历史会话会在后端清理,避免上下文和管理成本无限增长。',
|
||||||
|
actionLabel: '保存会话设置'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'llm',
|
id: 'llm',
|
||||||
label: '大语言模型',
|
label: '大语言模型',
|
||||||
@@ -128,6 +136,10 @@ const MODEL_TEST_CONFIGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MODEL_API_KEY_CONFIGS = Object.values(MODEL_TEST_CONFIGS)
|
const MODEL_API_KEY_CONFIGS = Object.values(MODEL_TEST_CONFIGS)
|
||||||
|
const SESSION_RETENTION_OPTIONS = Array.from({ length: 10 }, (_item, index) => ({
|
||||||
|
value: index + 1,
|
||||||
|
label: `${index + 1} 天`
|
||||||
|
}))
|
||||||
|
|
||||||
function normalizeValue(value) {
|
function normalizeValue(value) {
|
||||||
return String(value ?? '').trim()
|
return String(value ?? '').trim()
|
||||||
@@ -180,6 +192,9 @@ function buildDefaultState(companyProfile, currentUser) {
|
|||||||
loginAlertEnabled: true,
|
loginAlertEnabled: true,
|
||||||
adminPasswordConfigured: false
|
adminPasswordConfigured: false
|
||||||
},
|
},
|
||||||
|
sessionForm: {
|
||||||
|
conversationRetentionDays: 3
|
||||||
|
},
|
||||||
llmForm: {
|
llmForm: {
|
||||||
mainProvider: 'Codex',
|
mainProvider: 'Codex',
|
||||||
mainModel: 'codex-mini-latest',
|
mainModel: 'codex-mini-latest',
|
||||||
@@ -266,6 +281,7 @@ function mergeState(baseState, overrideState) {
|
|||||||
return {
|
return {
|
||||||
companyForm: { ...baseState.companyForm, ...(overrideState?.companyForm || {}) },
|
companyForm: { ...baseState.companyForm, ...(overrideState?.companyForm || {}) },
|
||||||
adminForm: { ...baseState.adminForm, ...(overrideState?.adminForm || {}) },
|
adminForm: { ...baseState.adminForm, ...(overrideState?.adminForm || {}) },
|
||||||
|
sessionForm: { ...baseState.sessionForm, ...(overrideState?.sessionForm || {}) },
|
||||||
llmForm: mergedLlmForm,
|
llmForm: mergedLlmForm,
|
||||||
renderForm: { ...baseState.renderForm, ...(overrideState?.renderForm || {}) },
|
renderForm: { ...baseState.renderForm, ...(overrideState?.renderForm || {}) },
|
||||||
logForm: { ...baseState.logForm, ...(overrideState?.logForm || {}) },
|
logForm: { ...baseState.logForm, ...(overrideState?.logForm || {}) },
|
||||||
@@ -281,6 +297,7 @@ function sanitizeForStorage(state) {
|
|||||||
newPassword: '',
|
newPassword: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
},
|
},
|
||||||
|
sessionForm: { ...state.sessionForm },
|
||||||
llmForm: {
|
llmForm: {
|
||||||
...state.llmForm,
|
...state.llmForm,
|
||||||
mainApiKey: '',
|
mainApiKey: '',
|
||||||
@@ -378,6 +395,10 @@ function computeSectionStatus(state) {
|
|||||||
normalizeValue(state.adminForm.adminEmail) &&
|
normalizeValue(state.adminForm.adminEmail) &&
|
||||||
Number(state.adminForm.sessionTimeout) >= 5
|
Number(state.adminForm.sessionTimeout) >= 5
|
||||||
),
|
),
|
||||||
|
session: Boolean(
|
||||||
|
Number(state.sessionForm.conversationRetentionDays) >= 1 &&
|
||||||
|
Number(state.sessionForm.conversationRetentionDays) <= 10
|
||||||
|
),
|
||||||
llm: Boolean(
|
llm: Boolean(
|
||||||
isModelConfigReady(state.llmForm.mainProvider, state.llmForm.mainModel, state.llmForm.mainEndpoint) &&
|
isModelConfigReady(state.llmForm.mainProvider, state.llmForm.mainModel, state.llmForm.mainEndpoint) &&
|
||||||
isModelConfigReady(state.llmForm.backupProvider, state.llmForm.backupModel, state.llmForm.backupEndpoint) &&
|
isModelConfigReady(state.llmForm.backupProvider, state.llmForm.backupModel, state.llmForm.backupEndpoint) &&
|
||||||
@@ -416,6 +437,8 @@ export default {
|
|||||||
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
||||||
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
||||||
const activeSection = ref('profile')
|
const activeSection = ref('profile')
|
||||||
|
const sessionRetentionPickerOpen = ref(false)
|
||||||
|
const sessionRetentionPickerRef = ref(null)
|
||||||
const modelTestState = ref({
|
const modelTestState = ref({
|
||||||
main: { status: 'idle', message: '' },
|
main: { status: 'idle', message: '' },
|
||||||
backup: { status: 'idle', message: '' },
|
backup: { status: 'idle', message: '' },
|
||||||
@@ -426,6 +449,7 @@ export default {
|
|||||||
const sections = SECTION_DEFINITIONS
|
const sections = SECTION_DEFINITIONS
|
||||||
const logLevels = LOG_LEVELS
|
const logLevels = LOG_LEVELS
|
||||||
const providerOptions = PROVIDER_OPTIONS
|
const providerOptions = PROVIDER_OPTIONS
|
||||||
|
const sessionRetentionOptions = SESSION_RETENTION_OPTIONS
|
||||||
|
|
||||||
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
||||||
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
||||||
@@ -497,6 +521,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
companyForm: { ...pageState.value.companyForm },
|
companyForm: { ...pageState.value.companyForm },
|
||||||
adminForm: { ...pageState.value.adminForm },
|
adminForm: { ...pageState.value.adminForm },
|
||||||
|
sessionForm: { ...pageState.value.sessionForm },
|
||||||
llmForm: buildLlmPayload(pageState.value.llmForm),
|
llmForm: buildLlmPayload(pageState.value.llmForm),
|
||||||
renderForm: buildRenderPayload(pageState.value.renderForm),
|
renderForm: buildRenderPayload(pageState.value.renderForm),
|
||||||
logForm: { ...pageState.value.logForm },
|
logForm: { ...pageState.value.logForm },
|
||||||
@@ -517,6 +542,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activateSection(sectionId) {
|
function activateSection(sectionId) {
|
||||||
|
sessionRetentionPickerOpen.value = false
|
||||||
activeSection.value = sectionId
|
activeSection.value = sectionId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +550,32 @@ export default {
|
|||||||
pageState.value[formKey][field] = !pageState.value[formKey][field]
|
pageState.value[formKey][field] = !pageState.value[formKey][field]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSessionRetentionPicker() {
|
||||||
|
sessionRetentionPickerOpen.value = !sessionRetentionPickerOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSessionRetentionPicker() {
|
||||||
|
sessionRetentionPickerOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSessionRetentionDays(value) {
|
||||||
|
pageState.value.sessionForm.conversationRetentionDays = Number(value)
|
||||||
|
closeSessionRetentionPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentPointerDown(event) {
|
||||||
|
if (!sessionRetentionPickerOpen.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target
|
||||||
|
if (sessionRetentionPickerRef.value?.contains(target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSessionRetentionPicker()
|
||||||
|
}
|
||||||
|
|
||||||
function applyProviderPreset(testKey) {
|
function applyProviderPreset(testKey) {
|
||||||
const config = MODEL_TEST_CONFIGS[testKey]
|
const config = MODEL_TEST_CONFIGS[testKey]
|
||||||
const llmForm = pageState.value.llmForm
|
const llmForm = pageState.value.llmForm
|
||||||
@@ -664,6 +716,23 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveSessionSection() {
|
||||||
|
const sessionForm = pageState.value.sessionForm
|
||||||
|
const retentionDays = Number(sessionForm.conversationRetentionDays)
|
||||||
|
|
||||||
|
if (retentionDays < 1 || retentionDays > 10) {
|
||||||
|
toast('会话保留天数必须在 1 到 10 天之间。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistRemoteSettings('会话设置已保存。', {
|
||||||
|
preserveModelApiKeys: true,
|
||||||
|
preserveAdminPasswords: true,
|
||||||
|
preserveRenderSecret: true,
|
||||||
|
preserveMailPassword: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function saveLlmSection() {
|
async function saveLlmSection() {
|
||||||
const llmForm = pageState.value.llmForm
|
const llmForm = pageState.value.llmForm
|
||||||
const modelConfigs = [
|
const modelConfigs = [
|
||||||
@@ -762,6 +831,11 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeSection.value === 'session') {
|
||||||
|
await saveSessionSection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (activeSection.value === 'llm') {
|
if (activeSection.value === 'llm') {
|
||||||
await saveLlmSection()
|
await saveLlmSection()
|
||||||
return
|
return
|
||||||
@@ -781,9 +855,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
|
}
|
||||||
loadSettingsSnapshot()
|
loadSettingsSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeSection,
|
activeSection,
|
||||||
activeSectionConfig,
|
activeSectionConfig,
|
||||||
@@ -798,10 +881,16 @@ export default {
|
|||||||
modelTestState,
|
modelTestState,
|
||||||
pageState,
|
pageState,
|
||||||
providerOptions,
|
providerOptions,
|
||||||
|
sessionRetentionOptions,
|
||||||
|
sessionRetentionPickerOpen,
|
||||||
|
sessionRetentionPickerRef,
|
||||||
saveActiveSection,
|
saveActiveSection,
|
||||||
sectionStatus,
|
sectionStatus,
|
||||||
sections,
|
sections,
|
||||||
|
selectSessionRetentionDays,
|
||||||
testModelConnection,
|
testModelConnection,
|
||||||
|
toggleSessionRetentionPicker,
|
||||||
|
closeSessionRetentionPicker,
|
||||||
toggleBoolean
|
toggleBoolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
import { recognizeOcrFiles } from '../../services/ocr.js'
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
||||||
@@ -63,11 +63,29 @@ function createMessage(role, text, attachments = [], extras = {}) {
|
|||||||
citations: [],
|
citations: [],
|
||||||
suggestedActions: [],
|
suggestedActions: [],
|
||||||
draftPayload: null,
|
draftPayload: null,
|
||||||
|
reviewPayload: null,
|
||||||
riskFlags: [],
|
riskFlags: [],
|
||||||
...extras
|
...extras
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMessageTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return nowTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = new Date(value)
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
return nowTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeRequest(request) {
|
function sanitizeRequest(request) {
|
||||||
if (!request) return { ...DEFAULT_REQUEST }
|
if (!request) return { ...DEFAULT_REQUEST }
|
||||||
return {
|
return {
|
||||||
@@ -129,6 +147,22 @@ function buildMessageMeta(payload, fileNames = []) {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStoredMessageMeta(messageJson, attachmentNames = []) {
|
||||||
|
const payload = messageJson?.orchestrator_payload
|
||||||
|
if (payload) {
|
||||||
|
return buildMessageMeta(payload, attachmentNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = []
|
||||||
|
if (messageJson?.status) {
|
||||||
|
items.push(`状态: ${messageJson.status}`)
|
||||||
|
}
|
||||||
|
if (attachmentNames.length) {
|
||||||
|
items.push(`附件: ${attachmentNames.length}`)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOcrDocuments(payload) {
|
function normalizeOcrDocuments(payload) {
|
||||||
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||||||
return documents.slice(0, 5).map((item) => ({
|
return documents.slice(0, 5).map((item) => ({
|
||||||
@@ -149,6 +183,43 @@ function buildOcrSummary(payload) {
|
|||||||
return parts.join(';')
|
return parts.join(';')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferPreviewKind(file) {
|
||||||
|
const mediaType = String(file?.type || '').toLowerCase()
|
||||||
|
const filename = String(file?.name || '').toLowerCase()
|
||||||
|
if (mediaType.startsWith('image/') || /\.(png|jpg|jpeg|webp|bmp)$/i.test(filename)) {
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
if (mediaType.includes('pdf') || /\.pdf$/i.test(filename)) {
|
||||||
|
return 'pdf'
|
||||||
|
}
|
||||||
|
return 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilePreviews(files, previewRegistry) {
|
||||||
|
return files.map((file) => {
|
||||||
|
const kind = inferPreviewKind(file)
|
||||||
|
if (kind !== 'image') {
|
||||||
|
return {
|
||||||
|
filename: file.name,
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
previewRegistry.push(url)
|
||||||
|
return {
|
||||||
|
filename: file.name,
|
||||||
|
kind,
|
||||||
|
url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDocumentPreview(filePreviews, filename) {
|
||||||
|
if (!Array.isArray(filePreviews)) return null
|
||||||
|
return filePreviews.find((item) => item.filename === filename) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
function buildWelcomeInsight(entrySource, linkedRequest) {
|
function buildWelcomeInsight(entrySource, linkedRequest) {
|
||||||
return {
|
return {
|
||||||
intent: 'welcome',
|
intent: 'welcome',
|
||||||
@@ -163,6 +234,41 @@ function buildWelcomeInsight(entrySource, linkedRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveInitialConversationId(conversation) {
|
||||||
|
return String(conversation?.conversation_id || conversation?.conversationId || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInitialDraftClaimId(conversation) {
|
||||||
|
return String(conversation?.draft_claim_id || conversation?.draftClaimId || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInitialConversationMessages(conversation) {
|
||||||
|
const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : []
|
||||||
|
|
||||||
|
return rawMessages.map((item) => {
|
||||||
|
const messageJson = item?.message_json || item?.messageJson || {}
|
||||||
|
const attachmentNames = Array.isArray(messageJson?.attachment_names)
|
||||||
|
? messageJson.attachment_names.filter(Boolean)
|
||||||
|
: []
|
||||||
|
const orchestratorPayload = messageJson?.orchestrator_payload || null
|
||||||
|
const result = orchestratorPayload?.result || {}
|
||||||
|
|
||||||
|
return createMessage(item.role, item.content, attachmentNames, {
|
||||||
|
id: `restored-${item.id || ++messageSeed}`,
|
||||||
|
time: formatMessageTime(item.created_at || item.createdAt),
|
||||||
|
meta: item.role === 'assistant' ? buildStoredMessageMeta(messageJson, attachmentNames) : [],
|
||||||
|
citations: item.role === 'assistant' && Array.isArray(result?.citations) ? result.citations : [],
|
||||||
|
suggestedActions:
|
||||||
|
item.role === 'assistant' && Array.isArray(result?.suggested_actions)
|
||||||
|
? result.suggested_actions
|
||||||
|
: [],
|
||||||
|
draftPayload: item.role === 'assistant' ? result?.draft_payload || messageJson?.draft_payload || null : null,
|
||||||
|
reviewPayload: item.role === 'assistant' ? result?.review_payload || null : null,
|
||||||
|
riskFlags: item.role === 'assistant' && Array.isArray(result?.risk_flags) ? result.risk_flags : []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function buildErrorInsight(error, fileNames = []) {
|
function buildErrorInsight(error, fileNames = []) {
|
||||||
return {
|
return {
|
||||||
intent: 'agent',
|
intent: 'agent',
|
||||||
@@ -183,17 +289,19 @@ function buildErrorInsight(error, fileNames = []) {
|
|||||||
citations: [],
|
citations: [],
|
||||||
suggestedActions: [],
|
suggestedActions: [],
|
||||||
draftPayload: null,
|
draftPayload: null,
|
||||||
|
reviewPayload: null,
|
||||||
riskFlags: [],
|
riskFlags: [],
|
||||||
toolCount: 0,
|
toolCount: 0,
|
||||||
failedToolCount: 0,
|
failedToolCount: 0,
|
||||||
selectedCapabilityCodes: [],
|
selectedCapabilityCodes: [],
|
||||||
|
filePreviews: [],
|
||||||
statusLabel: '失败',
|
statusLabel: '失败',
|
||||||
statusTone: 'note'
|
statusTone: 'note'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAgentInsight(payload, fileNames = []) {
|
function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
|
||||||
const trace = payload?.trace_summary || {}
|
const trace = payload?.trace_summary || {}
|
||||||
const result = payload?.result || {}
|
const result = payload?.result || {}
|
||||||
const statusLabel = resolveStatusLabel(payload?.status)
|
const statusLabel = resolveStatusLabel(payload?.status)
|
||||||
@@ -219,12 +327,14 @@ function buildAgentInsight(payload, fileNames = []) {
|
|||||||
citations: Array.isArray(result?.citations) ? result.citations : [],
|
citations: Array.isArray(result?.citations) ? result.citations : [],
|
||||||
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
|
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
|
||||||
draftPayload: result?.draft_payload || null,
|
draftPayload: result?.draft_payload || null,
|
||||||
|
reviewPayload: result?.review_payload || null,
|
||||||
riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [],
|
riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [],
|
||||||
toolCount: Number(trace?.tool_count || 0),
|
toolCount: Number(trace?.tool_count || 0),
|
||||||
failedToolCount: Number(trace?.failed_tool_count || 0),
|
failedToolCount: Number(trace?.failed_tool_count || 0),
|
||||||
selectedCapabilityCodes: Array.isArray(trace?.selected_capability_codes)
|
selectedCapabilityCodes: Array.isArray(trace?.selected_capability_codes)
|
||||||
? trace.selected_capability_codes
|
? trace.selected_capability_codes
|
||||||
: [],
|
: [],
|
||||||
|
filePreviews,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
statusTone: resolveStatusTone(payload?.status)
|
statusTone: resolveStatusTone(payload?.status)
|
||||||
}
|
}
|
||||||
@@ -242,6 +352,10 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
initialConversation: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
entrySource: {
|
entrySource: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'requests'
|
default: 'requests'
|
||||||
@@ -260,8 +374,23 @@ export default {
|
|||||||
const composerDraft = ref('')
|
const composerDraft = ref('')
|
||||||
const attachedFiles = ref([])
|
const attachedFiles = ref([])
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const messages = ref([])
|
|
||||||
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
|
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
|
||||||
|
const restoredMessages = normalizeInitialConversationMessages(props.initialConversation)
|
||||||
|
const messages = ref(
|
||||||
|
restoredMessages.length
|
||||||
|
? restoredMessages
|
||||||
|
: [
|
||||||
|
createMessage(
|
||||||
|
'assistant',
|
||||||
|
props.entrySource === 'detail'
|
||||||
|
? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。你的提问会直接进入真实智能体链路。`
|
||||||
|
: '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const conversationId = ref(resolveInitialConversationId(props.initialConversation))
|
||||||
|
const draftClaimId = ref(resolveInitialDraftClaimId(props.initialConversation))
|
||||||
|
const previewRegistry = []
|
||||||
|
|
||||||
const currentInsight = ref(buildWelcomeInsight(props.entrySource, linkedRequest.value))
|
const currentInsight = ref(buildWelcomeInsight(props.entrySource, linkedRequest.value))
|
||||||
const sourceLabel = computed(() => SOURCE_LABELS[props.entrySource] ?? '来自 AI 工作台')
|
const sourceLabel = computed(() => SOURCE_LABELS[props.entrySource] ?? '来自 AI 工作台')
|
||||||
@@ -333,15 +462,6 @@ export default {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
messages.value = [
|
|
||||||
createMessage(
|
|
||||||
'assistant',
|
|
||||||
props.entrySource === 'detail'
|
|
||||||
? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。你的提问会直接进入真实智能体链路。`
|
|
||||||
: '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
currentInsight.value = buildWelcomeInsight(props.entrySource, linkedRequest.value)
|
currentInsight.value = buildWelcomeInsight(props.entrySource, linkedRequest.value)
|
||||||
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
||||||
@@ -353,6 +473,12 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
for (const url of previewRegistry) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
if (!messageListRef.value) return
|
if (!messageListRef.value) return
|
||||||
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
|
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
|
||||||
@@ -412,6 +538,7 @@ export default {
|
|||||||
const rawText = composerDraft.value.trim()
|
const rawText = composerDraft.value.trim()
|
||||||
const files = Array.from(attachedFiles.value)
|
const files = Array.from(attachedFiles.value)
|
||||||
const fileNames = files.map((file) => file.name)
|
const fileNames = files.map((file) => file.name)
|
||||||
|
const filePreviews = buildFilePreviews(files, previewRegistry)
|
||||||
const userText =
|
const userText =
|
||||||
rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
|
rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
|
||||||
|
|
||||||
@@ -451,6 +578,7 @@ export default {
|
|||||||
const payload = await runOrchestrator({
|
const payload = await runOrchestrator({
|
||||||
source: 'user_message',
|
source: 'user_message',
|
||||||
user_id: user.username || user.name || 'anonymous',
|
user_id: user.username || user.name || 'anonymous',
|
||||||
|
conversation_id: conversationId.value || null,
|
||||||
message: backendMessage,
|
message: backendMessage,
|
||||||
context_json: {
|
context_json: {
|
||||||
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
||||||
@@ -461,11 +589,16 @@ export default {
|
|||||||
request_context: linkedRequest.value,
|
request_context: linkedRequest.value,
|
||||||
attachment_names: fileNames,
|
attachment_names: fileNames,
|
||||||
attachment_count: fileNames.length,
|
attachment_count: fileNames.length,
|
||||||
|
draft_claim_id: draftClaimId.value || undefined,
|
||||||
ocr_summary: ocrSummary,
|
ocr_summary: ocrSummary,
|
||||||
ocr_documents: ocrDocuments
|
ocr_documents: ocrDocuments
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
|
||||||
|
draftClaimId.value =
|
||||||
|
String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
||||||
|
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
pendingMessage.id,
|
pendingMessage.id,
|
||||||
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
||||||
@@ -475,10 +608,11 @@ export default {
|
|||||||
? payload.result.suggested_actions
|
? payload.result.suggested_actions
|
||||||
: [],
|
: [],
|
||||||
draftPayload: payload?.result?.draft_payload || null,
|
draftPayload: payload?.result?.draft_payload || null,
|
||||||
|
reviewPayload: payload?.result?.review_payload || null,
|
||||||
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
|
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
currentInsight.value = buildAgentInsight(payload, fileNames)
|
currentInsight.value = buildAgentInsight(payload, fileNames, filePreviews)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
pendingMessage.id,
|
pendingMessage.id,
|
||||||
@@ -514,6 +648,7 @@ export default {
|
|||||||
composerPlaceholder,
|
composerPlaceholder,
|
||||||
currentIntentLabel,
|
currentIntentLabel,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
|
resolveDocumentPreview,
|
||||||
triggerFileUpload,
|
triggerFileUpload,
|
||||||
handleFilesChange,
|
handleFilesChange,
|
||||||
runShortcut,
|
runShortcut,
|
||||||
|
|||||||
Reference in New Issue
Block a user