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")
public Map<String, Object> actionChat(@RequestBody String message) {
public Map<String, String> actionChat(@RequestBody String message) {
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
if (!aiLlmConfig.getEnable()) {
throw new BusinessException("命令模型未启用,请开启后再试。");

View File

@@ -30,8 +30,8 @@ public class EmbeddingService {
private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig;
public Map<String, Object> searchAction(String message) {
Map<String, Object> result = new HashMap<>();
public Map<String, String> searchAction(String message) {
Map<String, String> result = new HashMap<>();
EmbeddingSearchRequest embeddingSearchRequest =
EmbeddingSearchRequest.builder()
.queryEmbedding(zhipuEmbeddingModel.embed(message).content())
@@ -39,7 +39,8 @@ public class EmbeddingService {
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
zhiPuEmbeddingStore.search(embeddingSearchRequest);
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;
}

View File

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

View File

@@ -1484,7 +1484,7 @@ export interface operations {
};
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);
await onSubmit(validatedData);
updateFormData(undefined);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<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 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']">
<img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image">
<div
@@ -11,15 +11,23 @@
<LoadingIcon :textColor="'text-gray-900'"
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
</div>
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
v-html="renderMarkdown(chatElement.content)">
<div>
<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>
</li>
</div>
<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="[
isCommandMode
@@ -67,27 +75,47 @@
</div>
</form>
</div>
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
userUpsertModal!.hide();
}">
</UserUpsertModal>
</template>
<script setup lang="ts">
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
import useAlertStore from "@/composables/store/useAlertStore";
import DOMPurify from "dompurify";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
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 Button from "../components/Button.vue";
import UserUpsertModal from "../components/UserUpsertModal.vue";
import { useAiChat } from "../composables/ai/useAiChat";
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 { user } = useUserStore();
const userUpsertModal = ref<ModalInterface>();
const inputMessage = ref("");
const chatContainer = ref<HTMLElement | null>(null);
const alertStore = useAlertStore();
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;
};
@@ -96,7 +124,7 @@ marked.setOptions({
breaks: true,
});
const renderMarkdown = (content: string) => {
const renderMarkdown = (content: string | undefined) => {
if (!content) return "";
const restoredContent = content
@@ -111,23 +139,23 @@ const renderMarkdown = (content: string) => {
const rawHtml = marked(processedContent);
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) => {
// console.log('原始消息:', newVal[newVal.length - 1]);
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
// }, { deep: true });
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
await userUpsert.upsertUser(data);
userUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
};
watch(
chatElements,
messages,
async () => {
await nextTick();
scrollToBottom();
@@ -146,6 +174,13 @@ const abortChat = () => {
};
const chatByMode = async (message: string) => {
inputMessage.value = "";
messages.value.push({
content: message,
type: "chat",
isUser: true,
username: user.username!,
});
if (isCommandMode.value) {
await actionChat(message);
} else {
@@ -159,12 +194,11 @@ const chatByMode = async (message: string) => {
const handleSendClick = async () => {
try {
scrollToBottom();
const validInputMessage = z
.string({ message: "消息不能为空" })
.min(1, "消息不能为空")
.parse(inputMessage.value);
scrollToBottom();
inputMessage.value = "";
await chatByMode(validInputMessage);
} catch (error) {
if (error instanceof z.ZodError) {
@@ -181,6 +215,19 @@ const handleSendClick = async () => {
onUnmounted(() => {
cancel();
});
onMounted(async () => {
initFlowbite();
const $upsertModalElement: HTMLElement | null =
document.querySelector("#user-upsert-modal");
userUpsertModal.value = new Modal(
$upsertModalElement,
{},
{
id: "user-upsert-modal",
},
);
});
</script>
<style lang="css">