fix command type

This commit is contained in:
Chuck1sn
2025-05-26 11:27:00 +08:00
parent dde5fecd62
commit 3f4a5f2e8b
8 changed files with 106 additions and 44 deletions

View File

@@ -65,7 +65,7 @@ public class AiController {
} }
@PostMapping("/action/chat") @PostMapping("/action/chat")
public Map<String, Object> actionChat(@RequestBody String message) { public Map<String, String> actionChat(@RequestBody String message) {
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU); AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
if (!aiLlmConfig.getEnable()) { if (!aiLlmConfig.getEnable()) {
throw new BusinessException("命令模型未启用,请开启后再试。"); throw new BusinessException("命令模型未启用,请开启后再试。");

View File

@@ -30,8 +30,8 @@ public class EmbeddingService {
private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig; private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig;
public Map<String, Object> searchAction(String message) { public Map<String, String> searchAction(String message) {
Map<String, Object> result = new HashMap<>(); Map<String, String> result = new HashMap<>();
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder() EmbeddingSearchRequest.builder()
.queryEmbedding(zhipuEmbeddingModel.embed(message).content()) .queryEmbedding(zhipuEmbeddingModel.embed(message).content())
@@ -39,7 +39,8 @@ public class EmbeddingService {
EmbeddingSearchResult<TextSegment> embeddingSearchResult = EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest); zhiPuEmbeddingStore.search(embeddingSearchRequest);
if (!embeddingSearchResult.matches().isEmpty()) { if (!embeddingSearchResult.matches().isEmpty()) {
result = embeddingSearchResult.matches().getFirst().embedded().metadata().toMap(); Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata();
result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY));
} }
return result; return result;
} }

View File

@@ -771,7 +771,7 @@
"schema": { "schema": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "object" "type": "string"
} }
} }
} }

View File

@@ -1484,7 +1484,7 @@ export interface operations {
}; };
content: { content: {
"*/*": { "*/*": {
[key: string]: Record<string, never>; [key: string]: string;
}; };
}; };
}; };

View File

@@ -130,19 +130,10 @@ const handleSubmit = async () => {
}, },
); );
try {
const validatedData = userSchema.parse(formData.value); const validatedData = userSchema.parse(formData.value);
await onSubmit(validatedData); await onSubmit(validatedData);
updateFormData(undefined); updateFormData(undefined);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
}; };
onMounted(() => { onMounted(() => {

View File

@@ -1,23 +1,34 @@
import { fetchEventSource } from "@microsoft/fetch-event-source"; import { fetchEventSource } from "@microsoft/fetch-event-source";
import { ref } from "vue"; import { ref } from "vue";
import useAuthStore from "../store/useAuthStore";
import client from "../../api/client"; import client from "../../api/client";
import useAuthStore from "../store/useAuthStore";
const authStore = useAuthStore(); const authStore = useAuthStore();
export const useAiChat = () => { export const useAiChat = () => {
const messages = ref<string[]>([]); const messages = ref<
{
content: string;
type: "chat" | "action";
isUser: boolean;
username: string;
command?: string;
}[]
>([]);
const isLoading = ref(false); const isLoading = ref(false);
let currentController: AbortController | null = null; let currentController: AbortController | null = null;
const chat = async (message: string) => { const chat = async (message: string) => {
isLoading.value = true; isLoading.value = true;
messages.value.push(message);
messages.value.push("");
const ctrl = new AbortController(); const ctrl = new AbortController();
currentController = ctrl; currentController = ctrl;
messages.value.push({
content: "",
type: "chat",
isUser: false,
username: "知路智能体",
});
try { try {
const baseUrl = `${import.meta.env.VITE_BASE_URL}`; const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
await fetchEventSource(`${baseUrl}/ai/chat`, { await fetchEventSource(`${baseUrl}/ai/chat`, {
@@ -29,7 +40,7 @@ export const useAiChat = () => {
body: message, body: message,
signal: ctrl.signal, signal: ctrl.signal,
onmessage(ev) { onmessage(ev) {
messages.value[messages.value.length - 1] += ev.data; messages.value[messages.value.length - 1].content += ev.data;
}, },
onclose() { onclose() {
console.log("onclose"); console.log("onclose");
@@ -38,20 +49,26 @@ export const useAiChat = () => {
throw err; throw err;
}, },
}); });
} catch (error) {
messages.value.pop();
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
}; };
const actionChat = async (message: string) => { const actionChat = async (message: string) => {
messages.value.push(message);
messages.value.push("");
isLoading.value = true; isLoading.value = true;
try { try {
const { data } = await client.POST("/ai/action/chat", { const { data } = await client.POST("/ai/action/chat", {
body: message, body: message,
}); });
messages.value[messages.value.length - 1] += "接收到指令,请您执行。"; messages.value.push({
content: "接收到指令,请您执行。",
type: "action",
isUser: false,
username: "知路智能体",
command: data?.action,
});
return data; return data;
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

@@ -7,6 +7,7 @@ import {
RequestError, RequestError,
UnAuthError, UnAuthError,
} from "../types/error"; } from "../types/error";
import { z } from "zod";
const makeErrorHandler = const makeErrorHandler =
( (
@@ -44,6 +45,11 @@ const makeErrorHandler =
level: "error", level: "error",
content: err.detail ?? err.message, content: err.detail ?? err.message,
}); });
} else if (err instanceof z.ZodError) {
showAlert({
level: "error",
content: err.errors[0].message,
});
} else { } else {
showAlert({ showAlert({
level: "error", level: "error",

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col px-96 box-border pt-14 min-h-screen max-h-screen overflow-auto" ref="chatContainer"> <div class="flex flex-col px-96 box-border pt-14 min-h-screen max-h-screen overflow-auto" ref="chatContainer">
<div class="flex flex-col gap-y-5 flex-1 pt-14"> <div class="flex flex-col gap-y-5 flex-1 pt-14">
<li v-for="chatElement in chatElements" :key="chatElement.content" <li v-for="chatElement in messages" :key="chatElement.content"
:class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']"> :class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']">
<img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image"> <img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image">
<div <div
@@ -11,15 +11,23 @@
<LoadingIcon :textColor="'text-gray-900'" <LoadingIcon :textColor="'text-gray-900'"
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" /> v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
</div> </div>
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 " <div>
v-html="renderMarkdown(chatElement.content)"> <div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
v-html="renderMarkdown(chatElement.content)">
</div>
<button v-if="chatElement.type === 'action' && chatElement.command" type="button"
@click="commandActionMap[chatElement.command!]"
class="px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
{{
commandContentMap[chatElement.command!]
}}</button>
</div> </div>
</div> </div>
</li> </li>
</div> </div>
<form class="sticky bottom-4 mt-14"> <form class="sticky bottom-4 mt-14">
<button @click.prevent="toggleCommandMode" <button @click.prevent="toggleMode"
class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium text-gray-900 rounded-lg group focus:ring-4 focus:outline-none focus:ring-lime-200" class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium text-gray-900 rounded-lg group focus:ring-4 focus:outline-none focus:ring-lime-200"
:class="[ :class="[
isCommandMode isCommandMode
@@ -67,27 +75,47 @@
</div> </div>
</form> </form>
</div> </div>
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
userUpsertModal!.hide();
}">
</UserUpsertModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LoadingIcon from "@/components/icons/LoadingIcon.vue"; import LoadingIcon from "@/components/icons/LoadingIcon.vue";
import useAlertStore from "@/composables/store/useAlertStore"; import useAlertStore from "@/composables/store/useAlertStore";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { marked } from "marked"; import { marked } from "marked";
import { computed, nextTick, onUnmounted, ref, watch } from "vue"; import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { z } from "zod"; import { z } from "zod";
import Button from "../components/Button.vue"; import Button from "../components/Button.vue";
import UserUpsertModal from "../components/UserUpsertModal.vue";
import { useAiChat } from "../composables/ai/useAiChat"; import { useAiChat } from "../composables/ai/useAiChat";
import useUserStore from "../composables/store/useUserStore"; import useUserStore from "../composables/store/useUserStore";
import { useUserUpsert } from "../composables/user/useUserUpsert";
import type { UserUpsertSubmitModel } from "../types/user";
const { messages, chat, isLoading, cancel, actionChat } = useAiChat(); const { messages, chat, isLoading, cancel, actionChat } = useAiChat();
const { user } = useUserStore(); const { user } = useUserStore();
const userUpsertModal = ref<ModalInterface>();
const inputMessage = ref(""); const inputMessage = ref("");
const chatContainer = ref<HTMLElement | null>(null); const chatContainer = ref<HTMLElement | null>(null);
const alertStore = useAlertStore(); const alertStore = useAlertStore();
const isCommandMode = ref(false); const isCommandMode = ref(false);
const userUpsert = useUserUpsert();
const toggleCommandMode = () => { const commandActionMap: Record<string, () => void> = {
CREATE_USER: () => {
userUpsertModal.value?.show();
},
};
const commandContentMap: Record<string, string> = {
CREATE_USER: "创建新用户",
};
const toggleMode = () => {
isCommandMode.value = !isCommandMode.value; isCommandMode.value = !isCommandMode.value;
}; };
@@ -96,7 +124,7 @@ marked.setOptions({
breaks: true, breaks: true,
}); });
const renderMarkdown = (content: string) => { const renderMarkdown = (content: string | undefined) => {
if (!content) return ""; if (!content) return "";
const restoredContent = content const restoredContent = content
@@ -111,23 +139,23 @@ const renderMarkdown = (content: string) => {
const rawHtml = marked(processedContent); const rawHtml = marked(processedContent);
return DOMPurify.sanitize(rawHtml as string); return DOMPurify.sanitize(rawHtml as string);
}; };
const chatElements = computed(() => {
return messages.value.map((message, index) => {
return {
content: message,
username: index % 2 === 0 ? user.username : "知路智能体",
isUser: index % 2 === 0,
};
});
});
// watch(messages, (newVal) => { // watch(messages, (newVal) => {
// console.log('原始消息:', newVal[newVal.length - 1]); // console.log('原始消息:', newVal[newVal.length - 1]);
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1])); // console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
// }, { deep: true }); // }, { deep: true });
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
await userUpsert.upsertUser(data);
userUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
};
watch( watch(
chatElements, messages,
async () => { async () => {
await nextTick(); await nextTick();
scrollToBottom(); scrollToBottom();
@@ -146,6 +174,13 @@ const abortChat = () => {
}; };
const chatByMode = async (message: string) => { const chatByMode = async (message: string) => {
inputMessage.value = "";
messages.value.push({
content: message,
type: "chat",
isUser: true,
username: user.username!,
});
if (isCommandMode.value) { if (isCommandMode.value) {
await actionChat(message); await actionChat(message);
} else { } else {
@@ -159,12 +194,11 @@ const chatByMode = async (message: string) => {
const handleSendClick = async () => { const handleSendClick = async () => {
try { try {
scrollToBottom();
const validInputMessage = z const validInputMessage = z
.string({ message: "消息不能为空" }) .string({ message: "消息不能为空" })
.min(1, "消息不能为空") .min(1, "消息不能为空")
.parse(inputMessage.value); .parse(inputMessage.value);
scrollToBottom();
inputMessage.value = "";
await chatByMode(validInputMessage); await chatByMode(validInputMessage);
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
@@ -181,6 +215,19 @@ const handleSendClick = async () => {
onUnmounted(() => { onUnmounted(() => {
cancel(); cancel();
}); });
onMounted(async () => {
initFlowbite();
const $upsertModalElement: HTMLElement | null =
document.querySelector("#user-upsert-modal");
userUpsertModal.value = new Modal(
$upsertModalElement,
{},
{
id: "user-upsert-modal",
},
);
});
</script> </script>
<style lang="css"> <style lang="css">