10 changed files with 735 additions and 95 deletions
-
52src/main/java/com/wok/supportbot/app/AssistantApp.java
-
75src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
24src/main/java/com/wok/supportbot/controller/AiController.java
-
53src/main/java/com/wok/supportbot/controller/CustomerServiceRoleController.java
-
33src/main/java/com/wok/supportbot/controller/DocumentController.java
-
80src/main/java/com/wok/supportbot/service/CustomerServiceRoleService.java
-
51src/main/java/com/wok/supportbot/service/DocumentService.java
-
382src/main/resources/static/components/ChatPanel.js
-
63src/main/resources/static/css/main.css
-
17src/main/resources/static/js/api.js
@ -0,0 +1,53 @@ |
|||
package com.wok.supportbot.controller; |
|||
|
|||
import com.wok.supportbot.service.CustomerServiceRoleService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
|
|||
@RestController |
|||
public class CustomerServiceRoleController { |
|||
|
|||
@Autowired |
|||
private CustomerServiceRoleService customerServiceRoleService; |
|||
|
|||
@GetMapping("/role/list") |
|||
public ResponseEntity<Map<String, Object>> listRoles() { |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"data", customerServiceRoleService.listRoles() |
|||
)); |
|||
} |
|||
|
|||
@PutMapping("/role/{id}/categories") |
|||
public ResponseEntity<Map<String, Object>> updateRoleCategories( |
|||
@PathVariable("id") Long roleId, |
|||
@RequestBody Map<String, Object> body) { |
|||
List<Long> categoryIds = parseCategoryIds(body.get("categoryIds")); |
|||
customerServiceRoleService.updateRoleCategories(roleId, categoryIds); |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"message", "更新成功" |
|||
)); |
|||
} |
|||
|
|||
private List<Long> parseCategoryIds(Object rawCategoryIds) { |
|||
if (!(rawCategoryIds instanceof List<?> list)) { |
|||
return List.of(); |
|||
} |
|||
return list.stream() |
|||
.filter(Objects::nonNull) |
|||
.map(item -> item instanceof Number number ? number.longValue() : Long.parseLong(item.toString())) |
|||
.filter(id -> id > 0) |
|||
.distinct() |
|||
.toList(); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
package com.wok.supportbot.service; |
|||
|
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.LinkedHashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
|
|||
@Service |
|||
public class CustomerServiceRoleService { |
|||
|
|||
@Autowired |
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
public List<Map<String, Object>> listRoles() { |
|||
String roleSql = """ |
|||
SELECT id::text AS id, role_key, name, description, prompt, sort_order, enabled |
|||
FROM customer_service_role |
|||
WHERE is_delete = false AND enabled = true |
|||
ORDER BY sort_order ASC, id ASC |
|||
"""; |
|||
List<Map<String, Object>> roles = jdbcTemplate.queryForList(roleSql); |
|||
|
|||
String categorySql = """ |
|||
SELECT rc.role_id::text AS role_id, rc.category_id::text AS category_id, c.name AS category_name |
|||
FROM customer_service_role_category rc |
|||
LEFT JOIN knowledge_category c ON c.id = rc.category_id AND c.is_delete = false |
|||
WHERE rc.is_delete = false |
|||
ORDER BY rc.role_id ASC, rc.id ASC |
|||
"""; |
|||
List<Map<String, Object>> relations = jdbcTemplate.queryForList(categorySql); |
|||
|
|||
Map<String, List<String>> categoryIdsByRole = new LinkedHashMap<>(); |
|||
Map<String, List<Map<String, Object>>> categoriesByRole = new LinkedHashMap<>(); |
|||
for (Map<String, Object> relation : relations) { |
|||
String roleId = String.valueOf(relation.get("role_id")); |
|||
String categoryId = String.valueOf(relation.get("category_id")); |
|||
categoryIdsByRole.computeIfAbsent(roleId, key -> new ArrayList<>()).add(categoryId); |
|||
categoriesByRole.computeIfAbsent(roleId, key -> new ArrayList<>()).add(Map.of( |
|||
"id", categoryId, |
|||
"name", Objects.toString(relation.get("category_name"), "") |
|||
)); |
|||
} |
|||
|
|||
for (Map<String, Object> role : roles) { |
|||
String roleId = String.valueOf(role.get("id")); |
|||
role.put("categoryIds", categoryIdsByRole.getOrDefault(roleId, List.of())); |
|||
role.put("categories", categoriesByRole.getOrDefault(roleId, List.of())); |
|||
} |
|||
return roles; |
|||
} |
|||
|
|||
@Transactional(rollbackFor = Exception.class) |
|||
public void updateRoleCategories(Long roleId, List<Long> categoryIds) { |
|||
jdbcTemplate.update("UPDATE customer_service_role_category SET is_delete = true WHERE role_id = ?", roleId); |
|||
|
|||
if (categoryIds == null || categoryIds.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
List<Long> normalizedIds = categoryIds.stream() |
|||
.filter(Objects::nonNull) |
|||
.filter(id -> id > 0) |
|||
.distinct() |
|||
.toList(); |
|||
for (Long categoryId : normalizedIds) { |
|||
jdbcTemplate.update(""" |
|||
INSERT INTO customer_service_role_category (role_id, category_id, is_delete) |
|||
VALUES (?, ?, false) |
|||
ON CONFLICT (role_id, category_id) |
|||
DO UPDATE SET is_delete = false |
|||
""", roleId, categoryId); |
|||
} |
|||
} |
|||
} |
|||
@ -1,140 +1,362 @@ |
|||
/** |
|||
* 💬 智能客服对话面板 |
|||
* 智能客服对话面板 |
|||
*/ |
|||
import { ref, nextTick } from 'vue' |
|||
import { chatSync, chatRagSync, chatSSEUrl } from '../js/api.js' |
|||
import { ref, computed, nextTick, onMounted } from 'vue' |
|||
import { chatSync, chatRagSync, chatSSEUrl, getRoleList, updateRoleCategories } from '../js/api.js' |
|||
import { toast, readSSEStream } from '../js/utils.js' |
|||
import { store } from '../js/store.js' |
|||
|
|||
const FALLBACK_ROLE = { |
|||
id: '', |
|||
key: 'general', |
|||
name: '综合客服', |
|||
desc: '可检索全部知识库', |
|||
tone: '专业、耐心、简洁', |
|||
badge: '默认', |
|||
categoryIds: [] |
|||
} |
|||
|
|||
const QUICK_QUESTIONS = [ |
|||
'我的订单什么时候发货?', |
|||
'已经付款但订单状态没更新怎么办?', |
|||
'如何申请退款或退货?', |
|||
'物流显示签收但我没收到怎么办?', |
|||
'这个商品有哪些售后政策?' |
|||
] |
|||
|
|||
export default { |
|||
template: `
|
|||
<div class="card"> |
|||
<h2>💬 智能客服对话</h2> |
|||
<p style="color:var(--sub);font-size:13px;margin-bottom:12px;">基于通义千问 · 电商客服场景 · 支持多轮对话上下文记忆</p> |
|||
|
|||
<div class="input-row"> |
|||
<input class="input input-sm" v-model="chatId" placeholder="会话ID"> |
|||
<select class="select" v-model="mode"> |
|||
<option value="sync">🔵 同步调用</option> |
|||
<option value="sse">🟢 SSE 流式 (Flux)</option> |
|||
<option value="sse2">🟡 ServerSentEvent</option> |
|||
<option value="sse3">🟣 SseEmitter</option> |
|||
</select> |
|||
<button class="btn btn-outline btn-sm" @click="newChatId">🔄 新会话</button> |
|||
<button class="btn btn-outline btn-sm" @click="clearMessages">🗑️ 清屏</button> |
|||
</div> |
|||
|
|||
<div class="input-row" style="margin-top:8px;padding:12px;background:#f9fafb;border-radius:8px;border:1px solid var(--border);"> |
|||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;"> |
|||
<input type="checkbox" v-model="isRagMode" style="width:16px;height:16px;cursor:pointer;"> |
|||
<span style="font-weight:600;color:var(--primary);">📚 启用 RAG 知识库检索</span> |
|||
</label> |
|||
<select class="select" v-model="ragStrategy" v-show="isRagMode" style="margin-left:auto;"> |
|||
<option value="NONE">无重写</option> |
|||
<option value="REWRITE">查询重写</option> |
|||
<option value="TRANSLATION">翻译扩展</option> |
|||
<option value="COMPRESSION">查询压缩</option> |
|||
<option value="MULTI_QUERY">多路扩展</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div v-show="isRagMode" style="margin-bottom:12px;padding:8px 12px;background:#eef2ff;border-radius:6px;font-size:12px;color:var(--primary);border-left:3px solid var(--primary);"> |
|||
💡 当前已启用 RAG 知识库检索(策略:<strong>{{ ragStrategyNames[ragStrategy] || ragStrategy }}</strong>) |
|||
</div> |
|||
|
|||
<div class="msg-area" ref="msgArea"> |
|||
<div v-for="(m, i) in messages" :key="i" :class="['msg', m.role, m.streaming ? 'streaming' : '']"> |
|||
<div class="msg-avatar">{{ m.role === 'user' ? '👤' : '🤖' }}</div> |
|||
<div class="msg-bubble">{{ m.content }}</div> |
|||
<div class="chat-shell"> |
|||
<aside class="chat-sidebar"> |
|||
<div class="agent-card"> |
|||
<div class="agent-avatar">SB</div> |
|||
<div> |
|||
<div class="agent-name">Support Bot</div> |
|||
<div class="agent-status"><span></span>在线客服助手</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="side-section"> |
|||
<div class="side-label">客服角色</div> |
|||
<button |
|||
v-for="role in roles" |
|||
:key="role.key" |
|||
:class="['role-option', selectedRole === role.key ? 'active' : '']" |
|||
@click="selectRole(role.key)" |
|||
> |
|||
<span class="role-badge">{{ role.badge }}</span> |
|||
<span> |
|||
<strong>{{ role.name }}</strong> |
|||
<small>{{ role.desc }}</small> |
|||
</span> |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="side-section"> |
|||
<div class="side-label">知识库范围</div> |
|||
<label class="toggle-line"> |
|||
<input type="checkbox" v-model="isRagMode"> |
|||
<span>启用 RAG 检索</span> |
|||
</label> |
|||
<select class="select chat-select" v-model="ragStrategy" :disabled="!isRagMode"> |
|||
<option value="NONE">不重写查询</option> |
|||
<option value="REWRITE">查询重写</option> |
|||
<option value="TRANSLATION">翻译扩展</option> |
|||
<option value="COMPRESSION">查询压缩</option> |
|||
<option value="MULTI_QUERY">多路扩展</option> |
|||
</select> |
|||
<div class="kb-check-list"> |
|||
<label v-for="c in store.categories" :key="c.id" class="kb-check-item"> |
|||
<input |
|||
type="checkbox" |
|||
:checked="selectedCategoryIds.includes(String(c.id))" |
|||
@change="toggleRoleCategory(String(c.id), $event.target.checked)" |
|||
> |
|||
<span>{{ c.name }}</span> |
|||
</label> |
|||
<div v-if="!store.categories.length" class="kb-empty">暂无知识库分类</div> |
|||
</div> |
|||
<p class="side-note">{{ roleKnowledgeScopeText }}</p> |
|||
</div> |
|||
|
|||
<div class="side-section"> |
|||
<div class="side-label">当前会话</div> |
|||
<div class="session-box" :title="chatId">{{ shortChatId }}</div> |
|||
<div class="side-actions"> |
|||
<button class="btn btn-outline btn-sm" @click="newChatId">新会话</button> |
|||
<button class="btn btn-outline btn-sm" @click="clearMessages">清屏</button> |
|||
</div> |
|||
</div> |
|||
</aside> |
|||
|
|||
<section class="chat-main"> |
|||
<header class="chat-header"> |
|||
<div> |
|||
<h2>智能客服对话</h2> |
|||
<p>{{ currentRole.desc }} · {{ currentRole.tone }}</p> |
|||
</div> |
|||
<div class="chat-meta"> |
|||
<span :class="['rag-pill', isRagMode ? 'on' : '']">{{ ragStatusText }}</span> |
|||
<select class="select chat-mode" v-model="mode"> |
|||
<option value="sync">同步调用</option> |
|||
<option value="sse">SSE 流式</option> |
|||
<option value="sse2">ServerSentEvent</option> |
|||
<option value="sse3">SseEmitter</option> |
|||
</select> |
|||
</div> |
|||
</header> |
|||
|
|||
<div class="quick-row" v-if="messages.length <= 1"> |
|||
<button v-for="q in quickQuestions" :key="q" @click="useQuickQuestion(q)">{{ q }}</button> |
|||
</div> |
|||
|
|||
<div class="msg-area chat-msg-area" ref="msgArea"> |
|||
<div v-for="(m, i) in messages" :key="i" :class="['msg', m.role, m.streaming ? 'streaming' : '']"> |
|||
<div class="msg-avatar">{{ m.role === 'user' ? '我' : 'AI' }}</div> |
|||
<div class="msg-content"> |
|||
<div class="msg-bubble">{{ m.content || (m.streaming ? '正在思考...' : '') }}</div> |
|||
<div class="msg-tools"> |
|||
<span>{{ m.time }}</span> |
|||
<button v-if="m.role === 'assistant' && m.content" @click="copyMessage(m.content)">复制</button> |
|||
<button v-if="m.error" @click="retryLast">重试</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="input-row"> |
|||
<input class="input" v-model="userInput" placeholder="输入问题,Enter 发送..." @keydown.enter="send" :disabled="isSending"> |
|||
<button class="btn btn-primary" @click="send" :disabled="isSending">📨 发送</button> |
|||
</div> |
|||
<footer class="chat-composer"> |
|||
<textarea |
|||
class="textarea chat-textarea" |
|||
v-model="userInput" |
|||
placeholder="输入问题,Enter 发送,Shift + Enter 换行" |
|||
@keydown.enter.exact.prevent="send" |
|||
:disabled="isSending" |
|||
></textarea> |
|||
<button class="btn btn-primary send-btn" @click="send" :disabled="isSending || !userInput.trim()"> |
|||
{{ isSending ? '发送中' : '发送' }} |
|||
</button> |
|||
</footer> |
|||
</section> |
|||
</div> |
|||
`,
|
|||
setup() { |
|||
const chatId = ref('') |
|||
const mode = ref('sync') |
|||
const selectedRole = ref('general') |
|||
const roles = ref([FALLBACK_ROLE]) |
|||
const isRagMode = ref(false) |
|||
const ragStrategy = ref('REWRITE') |
|||
const userInput = ref('') |
|||
const lastUserInput = ref('') |
|||
const isSending = ref(false) |
|||
const messages = ref([ |
|||
{ role: 'assistant', content: '您好!我是电商智能客服助手。\n可以帮您解答商品、订单、支付、物流和售后问题。\n\n💡 提示:右侧下拉可切换对话模式,切换新会话开始全新对话。\n📚 如需启用知识库检索,请勾选上方的"启用 RAG 知识库检索"选项。', streaming: false } |
|||
{ |
|||
role: 'assistant', |
|||
content: '您好,我是电商智能客服助手。可以咨询商品、订单、支付、物流和售后问题;如果需要基于知识库回答,请先开启左侧 RAG 检索。', |
|||
streaming: false, |
|||
time: formatTime() |
|||
} |
|||
]) |
|||
const msgArea = ref(null) |
|||
|
|||
const ragStrategyNames = { |
|||
NONE: '无重写', REWRITE: '查询重写', TRANSLATION: '翻译扩展', |
|||
COMPRESSION: '查询压缩', MULTI_QUERY: '多路扩展' |
|||
const currentRole = computed(() => roles.value.find(role => role.key === selectedRole.value) || roles.value[0] || FALLBACK_ROLE) |
|||
const selectedCategoryIds = computed(() => currentRole.value.categoryIds || []) |
|||
const selectedCategoryNames = computed(() => { |
|||
const selected = new Set(selectedCategoryIds.value) |
|||
return store.categories |
|||
.filter(category => selected.has(String(category.id))) |
|||
.map(category => category.name) |
|||
}) |
|||
const roleKnowledgeScopeText = computed(() => { |
|||
if (!isRagMode.value) return '启用 RAG 后,会按当前客服勾选的知识库范围检索。' |
|||
if (selectedCategoryNames.value.length) return `当前客服可检索:${selectedCategoryNames.value.join('、')}。` |
|||
return '当前客服未绑定知识库,RAG 会检索全部知识库。' |
|||
}) |
|||
const shortChatId = computed(() => chatId.value ? chatId.value.replace(/^web_/, '') : '未创建') |
|||
const ragStatusText = computed(() => { |
|||
if (!isRagMode.value) return '普通对话' |
|||
const names = { |
|||
NONE: 'RAG:不重写', |
|||
REWRITE: 'RAG:查询重写', |
|||
TRANSLATION: 'RAG:翻译扩展', |
|||
COMPRESSION: 'RAG:查询压缩', |
|||
MULTI_QUERY: 'RAG:多路扩展' |
|||
} |
|||
return names[ragStrategy.value] || 'RAG 已启用' |
|||
}) |
|||
|
|||
function formatTime() { |
|||
return new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) |
|||
} |
|||
|
|||
function newChatId() { |
|||
chatId.value = 'web_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6) |
|||
chatId.value = 'web_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8) |
|||
} |
|||
|
|||
function selectRole(roleKey) { |
|||
selectedRole.value = roleKey |
|||
newChatId() |
|||
clearMessages(roleKey) |
|||
} |
|||
|
|||
function clearMessages(roleKey = selectedRole.value) { |
|||
const role = roles.value.find(item => item.key === roleKey) || FALLBACK_ROLE |
|||
messages.value = [{ |
|||
role: 'assistant', |
|||
content: `已切换到${role.name}。我会按“${role.tone}”的方式处理问题。`, |
|||
streaming: false, |
|||
time: formatTime() |
|||
}] |
|||
} |
|||
|
|||
function useQuickQuestion(question) { |
|||
userInput.value = question |
|||
send() |
|||
} |
|||
|
|||
async function loadRoles() { |
|||
try { |
|||
const json = await getRoleList() |
|||
if (json.success) { |
|||
roles.value = (json.data || []).map(role => ({ |
|||
id: role.id, |
|||
key: role.role_key || role.roleKey || String(role.id), |
|||
name: role.name, |
|||
desc: role.description || '', |
|||
tone: role.prompt || '专业、耐心、简洁', |
|||
badge: role.name ? role.name.slice(0, 2) : '角色', |
|||
categoryIds: (role.categoryIds || []).map(String) |
|||
})) |
|||
if (!roles.value.some(role => role.key === selectedRole.value)) { |
|||
selectedRole.value = roles.value[0]?.key || 'general' |
|||
} |
|||
} |
|||
} catch (e) { |
|||
toast('加载客服角色失败:' + e.message, 'error') |
|||
} |
|||
} |
|||
|
|||
async function toggleRoleCategory(categoryId, checked) { |
|||
const role = currentRole.value |
|||
if (!role.id) { |
|||
toast('当前角色未保存到数据库,不能配置知识库', 'error') |
|||
return |
|||
} |
|||
const current = new Set(role.categoryIds || []) |
|||
if (checked) { |
|||
current.add(categoryId) |
|||
} else { |
|||
current.delete(categoryId) |
|||
} |
|||
const nextIds = Array.from(current) |
|||
await updateRoleCategories(role.id, nextIds) |
|||
roles.value = roles.value.map(item => item.key === role.key ? { ...item, categoryIds: nextIds } : item) |
|||
toast('角色知识库范围已保存', 'success') |
|||
} |
|||
|
|||
function clearMessages() { |
|||
messages.value = [{ role: 'assistant', content: '已清屏,开始新的对话吧~', streaming: false }] |
|||
function buildRoleAwareMessage(text) { |
|||
const scope = selectedCategoryNames.value.length |
|||
? `仅使用这些知识库范围内的信息:${selectedCategoryNames.value.join('、')}` |
|||
: '可使用全部知识库范围内的信息' |
|||
return `当前客服角色:${currentRole.value.name}。回复风格:${currentRole.value.tone}。知识库范围:${scope}。用户问题:${text}` |
|||
} |
|||
|
|||
async function scrollToBottom() { |
|||
await nextTick() |
|||
if (msgArea.value) msgArea.value.scrollTop = msgArea.value.scrollHeight |
|||
} |
|||
|
|||
async function send() { |
|||
const text = userInput.value.trim() |
|||
if (!text || isSending.value) return |
|||
|
|||
userInput.value = '' |
|||
lastUserInput.value = text |
|||
isSending.value = true |
|||
messages.value.push({ role: 'user', content: text, streaming: false, time: formatTime() }) |
|||
|
|||
// 添加用户消息
|
|||
messages.value.push({ role: 'user', content: text, streaming: false }) |
|||
|
|||
// 添加助手占位
|
|||
const assistantMsg = { role: 'assistant', content: '', streaming: true } |
|||
const assistantMsg = { role: 'assistant', content: '', streaming: true, time: formatTime() } |
|||
messages.value.push(assistantMsg) |
|||
await scrollToBottom() |
|||
|
|||
const cid = chatId.value || ('web_' + Date.now()) |
|||
chatId.value = cid |
|||
const requestText = buildRoleAwareMessage(text) |
|||
|
|||
try { |
|||
if (isRagMode.value && mode.value === 'sync') { |
|||
// RAG 同步
|
|||
const result = await chatRagSync(text, cid, ragStrategy.value) |
|||
assistantMsg.content = result |
|||
assistantMsg.content = await chatRagSync(requestText, cid, ragStrategy.value, selectedCategoryIds.value) |
|||
} else if (isRagMode.value && mode.value !== 'sync') { |
|||
assistantMsg.content = '⚠️ RAG 模式仅支持同步调用' |
|||
assistantMsg.content = '当前后端 RAG 接口只提供同步调用,请切换为“同步调用”后再试。' |
|||
assistantMsg.error = true |
|||
toast('RAG 模式暂不支持流式调用', 'error') |
|||
} else if (mode.value === 'sync') { |
|||
// 普通同步
|
|||
const result = await chatSync(text, cid) |
|||
assistantMsg.content = result |
|||
assistantMsg.content = await chatSync(requestText, cid) |
|||
} else { |
|||
// SSE 流式
|
|||
const url = chatSSEUrl(text, cid, mode.value) |
|||
await readSSEStream(url, |
|||
(chunk) => { assistantMsg.content += chunk }, |
|||
() => {} |
|||
) |
|||
const url = chatSSEUrl(requestText, cid, mode.value) |
|||
await readSSEStream(url, async (chunk) => { |
|||
assistantMsg.content += chunk |
|||
await scrollToBottom() |
|||
}, () => {}) |
|||
} |
|||
} catch (e) { |
|||
assistantMsg.content = '请求失败:' + e.message |
|||
assistantMsg.error = true |
|||
toast('对话失败:' + e.message, 'error') |
|||
} finally { |
|||
assistantMsg.streaming = false |
|||
isSending.value = false |
|||
// 滚动到底部
|
|||
nextTick(() => { |
|||
if (msgArea.value) msgArea.value.scrollTop = msgArea.value.scrollHeight |
|||
}) |
|||
await scrollToBottom() |
|||
} |
|||
} |
|||
|
|||
function retryLast() { |
|||
if (!lastUserInput.value || isSending.value) return |
|||
userInput.value = lastUserInput.value |
|||
send() |
|||
} |
|||
|
|||
async function copyMessage(content) { |
|||
try { |
|||
await navigator.clipboard.writeText(content) |
|||
toast('已复制回复', 'success') |
|||
} catch (e) { |
|||
toast('复制失败', 'error') |
|||
} |
|||
} |
|||
|
|||
// 初始化会话 ID
|
|||
newChatId() |
|||
onMounted(() => { |
|||
newChatId() |
|||
store.loadCategories() |
|||
loadRoles() |
|||
}) |
|||
|
|||
return { |
|||
chatId, mode, isRagMode, ragStrategy, ragStrategyNames, |
|||
userInput, isSending, messages, msgArea, |
|||
newChatId, clearMessages, send |
|||
store, |
|||
roles, |
|||
quickQuestions: QUICK_QUESTIONS, |
|||
chatId, |
|||
mode, |
|||
selectedRole, |
|||
selectedCategoryIds, |
|||
isRagMode, |
|||
ragStrategy, |
|||
userInput, |
|||
isSending, |
|||
messages, |
|||
msgArea, |
|||
currentRole, |
|||
roleKnowledgeScopeText, |
|||
shortChatId, |
|||
ragStatusText, |
|||
newChatId, |
|||
selectRole, |
|||
clearMessages, |
|||
useQuickQuestion, |
|||
toggleRoleCategory, |
|||
send, |
|||
retryLast, |
|||
copyMessage |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue