diff --git a/src/composables/useAiChat.ts b/src/composables/useAiChat.ts
index 2f8a2f8..0d62323 100644
--- a/src/composables/useAiChat.ts
+++ b/src/composables/useAiChat.ts
@@ -20,10 +20,16 @@ export interface AiCitation {
generatedAt?: string;
}
+export interface AiThinking {
+ id: number;
+ content: string;
+}
+
export interface AiChatMessage {
id: number;
role: AiMessageRole;
content: string;
+ thinkingList: AiThinking[];
citationList: AiCitation[];
createdAt: number;
statusText?: string;
@@ -37,6 +43,14 @@ export interface UseAiChatOptions {
}
const conversationExpiredText = "AI 会话不存在或已过期";
+const thinkOpenMarkers = ["", ""];
+const thinkCloseMarkers = ["", ""];
+
+interface AssistantResponseState {
+ rawText: string;
+ eventThinkingList: AiThinking[];
+ taggedThinkingList: AiThinking[];
+}
/** 管理门店智能助手的会话、流式消息和取消操作。 */
export function useAiChat(options: UseAiChatOptions) {
@@ -50,6 +64,7 @@ export function useAiChat(options: UseAiChatOptions) {
const canRetry = computed(() => !!lastError.value && !!lastQuestion.value && !isGenerating.value);
let activeController: AbortController | undefined;
let messageSequence = 0;
+ const responseStateMap = new Map();
async function sendMessage(message: string): Promise {
const normalizedMessage = message.trim();
@@ -61,6 +76,7 @@ export function useAiChat(options: UseAiChatOptions) {
id: nextMessageId(),
role: "user",
content: normalizedMessage,
+ thinkingList: [],
citationList: [],
createdAt: Date.now()
};
@@ -68,6 +84,7 @@ export function useAiChat(options: UseAiChatOptions) {
id: nextMessageId(),
role: "assistant",
content: "",
+ thinkingList: [],
citationList: [],
createdAt: Date.now()
};
@@ -106,6 +123,7 @@ export function useAiChat(options: UseAiChatOptions) {
}
isGenerating.value = false;
activeToolStatus.value = "";
+ responseStateMap.delete(assistantMessage.id);
await scrollToBottom();
}
}
@@ -126,6 +144,7 @@ export function useAiChat(options: UseAiChatOptions) {
activeController?.abort();
activeController = undefined;
messages.value = [];
+ responseStateMap.clear();
conversationId.value = "";
isGenerating.value = false;
activeToolStatus.value = "";
@@ -161,11 +180,20 @@ export function useAiChat(options: UseAiChatOptions) {
async function handleStreamEvent(event: GaoAiStreamEvent, assistantId: number, hasDoneEvent: boolean): Promise {
const payload = parseSseData(event.data);
switch (event.event) {
+ case "think":
+ case "thinking":
+ appendAssistantThinking(assistantId, extractThinkingText(payload));
+ return hasDoneEvent;
+ case "agent_thought":
+ appendAssistantThinking(assistantId, extractThinkingText(payload), true);
+ return hasDoneEvent;
case "message":
- appendAssistantText(assistantId, extractText(payload));
+ appendAssistantResponse(assistantId, extractText(payload));
return hasDoneEvent;
case "tool_status":
- activeToolStatus.value = formatToolStatus(payload);
+ {
+ activeToolStatus.value = formatToolStatus(payload);
+ }
return hasDoneEvent;
case "citation":
appendCitation(assistantId, normalizeCitation(payload));
@@ -195,15 +223,58 @@ export function useAiChat(options: UseAiChatOptions) {
});
}
- function appendAssistantText(assistantId: number, text: string): void {
+ function appendAssistantResponse(assistantId: number, text: string): void {
if (!text) {
return;
}
- const message = messages.value.find(item => item.id === assistantId);
- if (!message) {
+ const state = getAssistantResponseState(assistantId);
+ state.rawText = mergeResponseText(state.rawText, text);
+ const parsedResponse = parseAssistantResponse(state.rawText);
+ state.taggedThinkingList = parsedResponse.thinkingList.map((content, index) => ({
+ id: state.taggedThinkingList[index]?.id || nextThinkingId(),
+ content
+ }));
+ updateMessage(assistantId, {
+ content: parsedResponse.answer,
+ thinkingList: [...state.eventThinkingList, ...state.taggedThinkingList],
+ statusText: undefined
+ });
+ }
+
+ function getAssistantResponseState(assistantId: number): AssistantResponseState {
+ const currentState = responseStateMap.get(assistantId);
+ if (currentState) {
+ return currentState;
+ }
+ const nextState: AssistantResponseState = {
+ rawText: "",
+ eventThinkingList: [],
+ taggedThinkingList: []
+ };
+ responseStateMap.set(assistantId, nextState);
+ return nextState;
+ }
+
+ function appendAssistantThinking(assistantId: number, text: string, createSegment = false): void {
+ if (!text) {
return;
}
- updateMessage(assistantId, { content: message.content + text, statusText: undefined });
+ const state = getAssistantResponseState(assistantId);
+ const thinkingList = [...state.eventThinkingList];
+ const lastThinking = thinkingList[thinkingList.length - 1];
+ if (createSegment || !lastThinking) {
+ thinkingList.push({ id: nextThinkingId(), content: text });
+ } else {
+ thinkingList[thinkingList.length - 1] = {
+ ...lastThinking,
+ content: lastThinking.content + text
+ };
+ }
+ state.eventThinkingList = thinkingList;
+ updateMessage(assistantId, {
+ thinkingList: [...state.eventThinkingList, ...state.taggedThinkingList],
+ statusText: undefined
+ });
}
function appendCitation(assistantId: number, citation?: AiCitation): void {
@@ -222,8 +293,10 @@ export function useAiChat(options: UseAiChatOptions) {
}
function resetAssistantMessage(id: number): void {
+ responseStateMap.delete(id);
updateMessage(id, {
content: "",
+ thinkingList: [],
citationList: [],
statusText: undefined,
duration: undefined,
@@ -242,6 +315,7 @@ export function useAiChat(options: UseAiChatOptions) {
function clearMessages(): void {
messages.value = [];
+ responseStateMap.clear();
}
function nextMessageId(): number {
@@ -249,6 +323,13 @@ export function useAiChat(options: UseAiChatOptions) {
return messageSequence;
}
+ let thinkingSequence = 0;
+
+ function nextThinkingId(): number {
+ thinkingSequence += 1;
+ return thinkingSequence;
+ }
+
async function scrollToBottom(): Promise {
await options.scrollToBottom?.();
}
@@ -290,7 +371,91 @@ function extractText(payload: unknown): string {
if (!isRecord(payload)) {
return "";
}
- return getString(payload.answer) || getString(payload.text) || getString(payload.content) || "";
+ return getRawString(payload.answer) || getRawString(payload.text) || getRawString(payload.content) || "";
+}
+
+function mergeResponseText(currentText: string, nextText: string): string {
+ if (currentText && nextText.startsWith(currentText)) {
+ return nextText;
+ }
+ return currentText + nextText;
+}
+
+function parseAssistantResponse(value: string): { answer: string; thinkingList: string[] } {
+ let answer = "";
+ let thinking = "";
+ let mode: "answer" | "thinking" = "answer";
+ let cursor = 0;
+ const thinkingList: string[] = [];
+
+ while (cursor < value.length) {
+ const marker = findMarker(value.slice(cursor), [...thinkOpenMarkers, ...thinkCloseMarkers]);
+ if (!marker) {
+ const rest = value.slice(cursor);
+ if (mode === "thinking") {
+ thinking += rest;
+ } else {
+ answer += rest;
+ }
+ break;
+ }
+
+ const content = value.slice(cursor, cursor + marker.index);
+ if (mode === "thinking") {
+ thinking += content;
+ if (thinking) {
+ thinkingList.push(thinking);
+ }
+ thinking = "";
+ mode = "answer";
+ } else if (isThinkOpenMarker(marker.value)) {
+ answer += content;
+ mode = "thinking";
+ } else {
+ if (content) {
+ thinkingList.push(content);
+ }
+ mode = "answer";
+ }
+ cursor += marker.index + marker.value.length;
+ }
+
+ if (mode === "thinking" && thinking) {
+ thinkingList.push(thinking);
+ }
+
+ return { answer, thinkingList };
+}
+
+function findMarker(value: string, markerList: string[]): { index: number; value: string } | undefined {
+ const lowerValue = value.toLowerCase();
+ return markerList.reduce<{ index: number; value: string } | undefined>((current, marker) => {
+ const index = lowerValue.indexOf(marker.toLowerCase());
+ if (index < 0 || (current && current.index <= index)) {
+ return current;
+ }
+ return { index, value: marker };
+ }, undefined);
+}
+
+function isThinkOpenMarker(value: string): boolean {
+ return thinkOpenMarkers.some(marker => marker.toLowerCase() === value.toLowerCase());
+}
+
+function extractThinkingText(payload: unknown): string {
+ if (typeof payload === "string") {
+ return payload === "[DONE]" ? "" : payload;
+ }
+ if (!isRecord(payload)) {
+ return "";
+ }
+ return getRawString(payload.thought)
+ || getRawString(payload.thinking)
+ || getRawString(payload.reasoning)
+ || getRawString(payload.observation)
+ || getRawString(payload.text)
+ || getRawString(payload.content)
+ || "";
}
function formatToolStatus(payload: unknown): string {
@@ -364,6 +529,10 @@ function getString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
+function getRawString(value: unknown): string {
+ return typeof value === "string" ? value : "";
+}
+
function getDisplayText(value: unknown): string | undefined {
const text = getString(value);
if (text) {
diff --git a/src/pages/ai/components/AiChatComposer.vue b/src/pages/ai/components/AiChatComposer.vue
index 0f50029..2541476 100644
--- a/src/pages/ai/components/AiChatComposer.vue
+++ b/src/pages/ai/components/AiChatComposer.vue
@@ -4,14 +4,14 @@
-
+
-
-
+
-
+
+
+
@@ -57,6 +67,7 @@
+