mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-03 19:06:10 +00:00
fix command type
This commit is contained in:
@@ -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("命令模型未启用,请开启后再试。");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -771,7 +771,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "object"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
frontend/src/api/types/schema.d.ts
vendored
2
frontend/src/api/types/schema.d.ts
vendored
@@ -1484,7 +1484,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"*/*": {
|
"*/*": {
|
||||||
[key: string]: Record<string, never>;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user