Resolve merge conflict in StandaloneChat.vue
This commit is contained in:
@@ -343,16 +343,12 @@ class ChatRoute(Route):
|
||||
|
||||
async with track_conversation(self.running_convs, webchat_conv_id):
|
||||
while True:
|
||||
try:
|
||||
result = await asyncio.wait_for(back_queue.get(), timeout=1)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
|
||||
result, should_break = await _poll_webchat_stream_result(
|
||||
back_queue, username
|
||||
)
|
||||
if should_break:
|
||||
client_disconnected = True
|
||||
except Exception as e:
|
||||
logger.error(f"WebChat stream error: {e}")
|
||||
|
||||
break
|
||||
if not result:
|
||||
continue
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -81,9 +81,6 @@ importers:
|
||||
stream-markdown:
|
||||
specifier: ^0.0.13
|
||||
version: 0.0.13(shiki@3.22.0)
|
||||
stream-monaco:
|
||||
specifier: ^0.0.17
|
||||
version: 0.0.17(monaco-editor@0.52.2)
|
||||
vee-validate:
|
||||
specifier: 4.11.3
|
||||
version: 4.11.3(vue@3.3.4)
|
||||
@@ -3300,6 +3297,7 @@ snapshots:
|
||||
'@shikijs/core': 3.22.0
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
optional: true
|
||||
|
||||
'@shikijs/themes@3.22.0':
|
||||
dependencies:
|
||||
@@ -3992,7 +3990,8 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
alien-signals@2.0.8: {}
|
||||
alien-signals@2.0.8:
|
||||
optional: true
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
@@ -5443,6 +5442,7 @@ snapshots:
|
||||
alien-signals: 2.0.8
|
||||
monaco-editor: 0.52.2
|
||||
shiki: 3.22.0
|
||||
optional: true
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
@@ -106,7 +106,7 @@
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
@@ -137,7 +137,7 @@
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="isStreaming"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
@@ -348,6 +348,12 @@ function setSendShortcut(mode: SendShortcut) {
|
||||
localStorage.setItem(SEND_SHORTCUT_STORAGE_KEY, mode);
|
||||
}
|
||||
|
||||
function focusChatInput() {
|
||||
nextTick(() => {
|
||||
chatInputRef.value?.focusInput?.();
|
||||
});
|
||||
}
|
||||
|
||||
// 检测是否为手机端
|
||||
function checkMobile() {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
@@ -505,6 +511,7 @@ async function handleSelectConversation(sessionIds: string[]) {
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
});
|
||||
focusChatInput();
|
||||
}
|
||||
|
||||
function handleNewChat() {
|
||||
@@ -514,6 +521,7 @@ function handleNewChat() {
|
||||
// 退出项目视图
|
||||
selectedProjectId.value = null;
|
||||
projectSessions.value = [];
|
||||
focusChatInput();
|
||||
}
|
||||
|
||||
async function handleDeleteConversation(sessionId: string) {
|
||||
@@ -671,6 +679,11 @@ async function handleSendMessage() {
|
||||
const selectedProviderId = selection?.providerId || '';
|
||||
const selectedModelName = selection?.modelName || '';
|
||||
|
||||
// 点击发送后立即将消息区滚到底部,确保用户看到最新消息
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
});
|
||||
|
||||
await sendMsg(
|
||||
promptToSend,
|
||||
filesToSend,
|
||||
@@ -680,6 +693,11 @@ async function handleSendMessage() {
|
||||
replyToSend
|
||||
);
|
||||
|
||||
// 发送流程结束后再兜底一次,处理异步渲染场景
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
});
|
||||
|
||||
// 如果在项目中创建了新会话,将其添加到项目
|
||||
if (isCreatingNewSession && currentProjectId && currSessionId.value) {
|
||||
await addSessionToProject(currSessionId.value, currentProjectId);
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isRunning" @click="$emit('stop')" variant="tonal" color="primary" class="send-btn">
|
||||
<v-btn icon v-if="isRunning && !canSend" @click="$emit('stop')" variant="tonal" color="primary" class="send-btn">
|
||||
<v-icon icon="mdi-stop" variant="text" plain></v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ tm('input.stopGenerating') }}
|
||||
@@ -373,6 +373,11 @@ function getCurrentSelection() {
|
||||
return providerModelMenuRef.value?.getCurrentSelection();
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (!inputField.value) return;
|
||||
inputField.value.focus();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (inputField.value) {
|
||||
inputField.value.addEventListener('paste', handlePaste);
|
||||
@@ -388,7 +393,8 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getCurrentSelection
|
||||
getCurrentSelection,
|
||||
focusInput
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<script>
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { enableKatex, enableMermaid, setCustomComponents } from 'markstream-vue'
|
||||
import { enableKatex, enableMermaid, MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import 'highlight.js/styles/github.css';
|
||||
@@ -194,8 +194,11 @@ import ActionRef from './message_list_comps/ActionRef.vue';
|
||||
enableKatex();
|
||||
enableMermaid();
|
||||
|
||||
// 注册自定义 ref 组件
|
||||
setCustomComponents('message-list', { ref: RefNode });
|
||||
// 注册 message-list 专用组件:引用节点 + Shiki 代码块渲染
|
||||
setCustomComponents('message-list', {
|
||||
ref: RefNode,
|
||||
code_block: MarkdownCodeBlockNode
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'MessageList',
|
||||
|
||||
@@ -63,8 +63,9 @@
|
||||
<!-- Text (Markdown) -->
|
||||
<MarkdownRender
|
||||
v-else-if="renderPart.part.type === 'plain' && renderPart.part.text && renderPart.part.text.trim()"
|
||||
:key="`${renderPart.key}-${isDark ? 'dark' : 'light'}`"
|
||||
custom-id="message-list" :custom-html-tags="['ref']" :content="renderPart.part.text" :typewriter="false"
|
||||
class="markdown-content" :is-dark="isDark" :monacoOptions="{ theme: isDark ? 'vs-dark' : 'vs-light' }" />
|
||||
class="markdown-content" :is-dark="isDark" />
|
||||
|
||||
<!-- Image -->
|
||||
<div v-else-if="renderPart.part.type === 'image' && renderPart.part.embedded_url" class="embedded-images">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
|
||||
<MarkdownRender :content="reasoning" class="reasoning-text markdown-content"
|
||||
<MarkdownRender :key="`reasoning-${isDark ? 'dark' : 'light'}`" :content="reasoning" class="reasoning-text markdown-content"
|
||||
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ This documentation may not cover all features comprehensively. If you have any q
|
||||
|
||||
### Discord
|
||||
|
||||
<https://discord.gg/PxgzhmxJ>
|
||||
<https://discord.gg/hAVk6tgV36>
|
||||
|
||||
### GitHub
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
### Discord
|
||||
|
||||
https://discord.gg/PxgzhmxJ
|
||||
https://discord.gg/hAVk6tgV36
|
||||
|
||||
### Astrbook
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # 只需要在第一次部署时执行,后续启动不需要执行
|
||||
astrbot
|
||||
astrbot run
|
||||
```
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.dashboard.routes.chat import _poll_webchat_stream_result
|
||||
|
||||
|
||||
class _QueueThatRaises:
|
||||
def __init__(self, exc: BaseException):
|
||||
self._exc = exc
|
||||
|
||||
async def get(self):
|
||||
raise self._exc
|
||||
|
||||
|
||||
class _QueueWithResult:
|
||||
def __init__(self, result):
|
||||
self._result = result
|
||||
|
||||
async def get(self):
|
||||
return self._result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_webchat_stream_result_breaks_on_cancelled_error():
|
||||
result, should_break = await _poll_webchat_stream_result(
|
||||
_QueueThatRaises(asyncio.CancelledError()),
|
||||
"alice",
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert should_break is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_webchat_stream_result_continues_on_generic_exception():
|
||||
result, should_break = await _poll_webchat_stream_result(
|
||||
_QueueThatRaises(RuntimeError("boom")),
|
||||
"alice",
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert should_break is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_webchat_stream_result_returns_queue_payload():
|
||||
payload = {"type": "end", "data": ""}
|
||||
|
||||
result, should_break = await _poll_webchat_stream_result(
|
||||
_QueueWithResult(payload),
|
||||
"alice",
|
||||
)
|
||||
|
||||
assert result == payload
|
||||
assert should_break is False
|
||||
Reference in New Issue
Block a user