feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url) - 实时查询强制 web skill,router fallback 与 await session Finish - Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -192,6 +192,7 @@ export function ChatPanel({
|
||||
remainingHeight: number;
|
||||
}>(null);
|
||||
const prevMessageCountRef = useRef(messages.length);
|
||||
const showAssistantTyping = chatState === 'waiting' || chatState === 'streaming';
|
||||
|
||||
inputRef.current = input;
|
||||
pendingImagesRef.current = pendingImages;
|
||||
@@ -293,13 +294,14 @@ export function ChatPanel({
|
||||
}
|
||||
|
||||
const messageCountIncreased = messages.length > prevMessageCountRef.current;
|
||||
if ((nearBottomRef.current || chatState === 'streaming') && messageCountIncreased) {
|
||||
if ((nearBottomRef.current || showAssistantTyping) && messageCountIncreased) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
nearBottomRef.current = true;
|
||||
}
|
||||
prevMessageCountRef.current = messages.length;
|
||||
}, [
|
||||
chatState,
|
||||
showAssistantTyping,
|
||||
historyHasMore,
|
||||
historyLoadingMore,
|
||||
messages.length,
|
||||
@@ -308,6 +310,13 @@ export function ChatPanel({
|
||||
session?.id,
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!showAssistantTyping) return;
|
||||
const container = mainRef.current;
|
||||
if (!container || !nearBottomRef.current) return;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, [showAssistantTyping, messages.length]);
|
||||
|
||||
const busy =
|
||||
chatState === 'streaming' ||
|
||||
chatState === 'loading' ||
|
||||
@@ -331,9 +340,7 @@ export function ChatPanel({
|
||||
? '正在上传图片,发出后会自动继续…'
|
||||
: chatState === 'connecting'
|
||||
? '正在创建会话…'
|
||||
: chatState === 'waiting'
|
||||
? '请求已发出,正在等待后台开始处理…'
|
||||
: null;
|
||||
: null;
|
||||
const sendButtonLabel =
|
||||
uploadingImage
|
||||
? '上传中…'
|
||||
@@ -672,7 +679,7 @@ export function ChatPanel({
|
||||
)}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
streaming={chatState === 'streaming'}
|
||||
streaming={showAssistantTyping}
|
||||
onAvatarClick={compact ? undefined : openAvatarPicker}
|
||||
onSaveAsPage={openSaveActions}
|
||||
onShareToPlaza={openPlazaPublish}
|
||||
|
||||
@@ -144,6 +144,31 @@ function isToolOnlyAssistantMessage(message: Message) {
|
||||
);
|
||||
}
|
||||
|
||||
function TypingBubble() {
|
||||
return (
|
||||
<div className="msg-bubble msg-bubble-assistant msg-typing">
|
||||
<span className="typing-dots">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowEndTypingIndicator(messages: Message[], streaming: boolean) {
|
||||
if (!streaming) return false;
|
||||
const lastUserIndex = messages.findLastIndex((message) => message.role === 'user');
|
||||
if (lastUserIndex < 0) return true;
|
||||
const assistantReply = messages
|
||||
.slice(lastUserIndex + 1)
|
||||
.find((message) => message.role === 'assistant');
|
||||
if (!assistantReply) return true;
|
||||
if (getDisplayText(assistantReply).trim() || getThinking(assistantReply)) return false;
|
||||
if (hasToolActivity(assistantReply)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function ToolBadge({ message, active }: { message: Message; active: boolean }) {
|
||||
if (message.role !== 'assistant') return null;
|
||||
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
|
||||
@@ -265,6 +290,7 @@ function MessageRow({
|
||||
sessionId,
|
||||
compact = false,
|
||||
activeToolMessage = false,
|
||||
showInlineTyping = false,
|
||||
}: {
|
||||
message: Message;
|
||||
avatarUrl: string | null;
|
||||
@@ -279,6 +305,7 @@ function MessageRow({
|
||||
sessionId?: string;
|
||||
compact?: boolean;
|
||||
activeToolMessage?: boolean;
|
||||
showInlineTyping?: boolean;
|
||||
}) {
|
||||
const [actionsOpen, setActionsOpen] = useState(false);
|
||||
const rawText = getDisplayText(message);
|
||||
@@ -295,7 +322,11 @@ function MessageRow({
|
||||
<div className={`msg-row msg-row-assistant`}>
|
||||
<TKMindAvatar />
|
||||
<div className="msg-content">
|
||||
<ToolBadge message={message} active={activeToolMessage} />
|
||||
{showInlineTyping ? (
|
||||
<TypingBubble />
|
||||
) : (
|
||||
<ToolBadge message={message} active={activeToolMessage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -471,6 +502,20 @@ export function MessageList({
|
||||
const visibleMessages = renderableMessages.filter(
|
||||
(message) => !isToolOnlyAssistantMessage(message) || message === activeToolMessage,
|
||||
);
|
||||
const showEndTyping = shouldShowEndTypingIndicator(renderableMessages, streaming);
|
||||
const lastUserIndex = renderableMessages.findLastIndex((message) => message.role === 'user');
|
||||
const inlineTypingMessage =
|
||||
lastUserIndex >= 0
|
||||
? renderableMessages
|
||||
.slice(lastUserIndex + 1)
|
||||
.find(
|
||||
(message) =>
|
||||
message.role === 'assistant' &&
|
||||
!getDisplayText(message).trim() &&
|
||||
!getThinking(message) &&
|
||||
!hasToolActivity(message),
|
||||
) ?? null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="message-list">
|
||||
@@ -499,21 +544,16 @@ export function MessageList({
|
||||
publishUsername={publishUsername}
|
||||
compact={compact}
|
||||
activeToolMessage={message === activeToolMessage}
|
||||
showInlineTyping={showEndTyping && message === inlineTypingMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{streaming && (
|
||||
<div className="msg-row msg-row-assistant">
|
||||
{showEndTyping && !inlineTypingMessage && (
|
||||
<div className="msg-row msg-row-assistant" role="status" aria-live="polite" aria-label="助手正在回复">
|
||||
<TKMindAvatar />
|
||||
<div className="msg-content">
|
||||
<div className="msg-bubble msg-bubble-assistant msg-typing">
|
||||
<span className="typing-dots">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
<TypingBubble />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -112,7 +112,7 @@ export function MindSpaceSpaceChat({
|
||||
) : null}
|
||||
<SpaceChatFab
|
||||
open={open}
|
||||
streaming={chatState === 'streaming'}
|
||||
streaming={chatState === 'waiting' || chatState === 'streaming'}
|
||||
pendingTool={Boolean(pendingTool)}
|
||||
onClick={() => setOpen(!open)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user