refactor(ai): AI助手优化和移除MCP插件
This commit is contained in:
@@ -117,7 +117,6 @@
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-mock-dev-server": "^2.0.2",
|
||||
"vite-plugin-vue-mcp": "^0.3.2",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^2.2.12"
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useAppStore, useSettingsStore } from "@/store";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { ThemeMode, ComponentSize } from "@/enums";
|
||||
import AiAssistant from "@/components/AiAssistant/index.vue";
|
||||
import { AuthStorage } from "@/utils/auth";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -27,7 +28,13 @@ const settingsStore = useSettingsStore();
|
||||
const locale = computed(() => appStore.locale);
|
||||
const size = computed(() => appStore.size as ComponentSize);
|
||||
const showWatermark = computed(() => settingsStore.showWatermark);
|
||||
const enableAiAssistant = computed(() => settingsStore.enableAiAssistant);
|
||||
|
||||
// 只有在启用 AI 助手且用户已登录时才显示
|
||||
const enableAiAssistant = computed(() => {
|
||||
const isEnabled = settingsStore.enableAiAssistant;
|
||||
const isLoggedIn = !!AuthStorage.getAccessToken();
|
||||
return isEnabled && isLoggedIn;
|
||||
});
|
||||
|
||||
// 明亮/暗黑主题水印字体颜色适配
|
||||
const fontColor = computed(() => {
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface FunctionCall {
|
||||
* AI 命令解析响应
|
||||
*/
|
||||
export interface AiCommandResponse {
|
||||
/** 解析日志ID(用于关联执行记录) */
|
||||
parseLogId?: string;
|
||||
/** 是否成功解析 */
|
||||
success: boolean;
|
||||
/** 解析后的函数调用列表 */
|
||||
@@ -48,6 +50,10 @@ export interface AiCommandResponse {
|
||||
* AI 命令执行请求
|
||||
*/
|
||||
export interface AiExecuteRequest {
|
||||
/** 关联的解析日志ID */
|
||||
parseLogId?: string;
|
||||
/** 原始命令(用于审计) */
|
||||
originalCommand?: string;
|
||||
/** 要执行的函数调用 */
|
||||
functionCall: FunctionCall;
|
||||
/** 确认模式:auto=自动执行, manual=需要用户确认 */
|
||||
@@ -56,6 +62,8 @@ export interface AiExecuteRequest {
|
||||
userConfirmed?: boolean;
|
||||
/** 幂等性令牌(防止重复执行) */
|
||||
idempotencyKey?: string;
|
||||
/** 当前页面路由 */
|
||||
currentRoute?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,14 +80,60 @@ export interface AiExecuteResponse {
|
||||
affectedRows?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 审计ID(用于追踪) */
|
||||
auditId?: string;
|
||||
/** 记录ID(用于追踪) */
|
||||
recordId?: string;
|
||||
/** 需要用户确认 */
|
||||
requiresConfirmation?: boolean;
|
||||
/** 确认提示信息 */
|
||||
confirmationPrompt?: string;
|
||||
}
|
||||
|
||||
export interface AiCommandRecordPageQuery extends PageQuery {
|
||||
keywords?: string;
|
||||
executeStatus?: string;
|
||||
parseSuccess?: boolean;
|
||||
userId?: number;
|
||||
isDangerous?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
functionName?: string;
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
export interface AiCommandRecordVO {
|
||||
id: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
originalCommand: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
parseSuccess?: boolean;
|
||||
functionCalls?: string;
|
||||
explanation?: string;
|
||||
confidence?: number;
|
||||
parseErrorMessage?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
parseTime?: number;
|
||||
functionName?: string;
|
||||
functionArguments?: string;
|
||||
executeStatus?: string;
|
||||
executeResult?: string;
|
||||
executeErrorMessage?: string;
|
||||
affectedRows?: number;
|
||||
isDangerous?: boolean;
|
||||
requiresConfirmation?: boolean;
|
||||
userConfirmed?: boolean;
|
||||
executionTime?: number;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
currentRoute?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 命令 API
|
||||
*/
|
||||
@@ -102,10 +156,10 @@ class AiCommandApi {
|
||||
* 执行已解析的命令
|
||||
*
|
||||
* @param data 执行请求参数
|
||||
* @returns 执行结果
|
||||
* @returns 执行结果数据(成功时返回,失败时抛出异常)
|
||||
*/
|
||||
static executeCommand(data: AiExecuteRequest): Promise<AiExecuteResponse> {
|
||||
return request<any, AiExecuteResponse>({
|
||||
static executeCommand(data: AiExecuteRequest): Promise<any> {
|
||||
return request<any, any>({
|
||||
url: "/api/v1/ai/command/execute",
|
||||
method: "post",
|
||||
data,
|
||||
@@ -113,45 +167,22 @@ class AiCommandApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命令执行历史
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 历史记录列表
|
||||
* 获取命令记录分页列表
|
||||
*/
|
||||
static getCommandHistory(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}) {
|
||||
return request({
|
||||
url: "/api/v1/ai/command/history",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的函数列表(用于展示或调试)
|
||||
*
|
||||
* @returns 函数列表
|
||||
*/
|
||||
static getAvailableFunctions() {
|
||||
return request({
|
||||
url: "/api/v1/ai/command/functions",
|
||||
static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
|
||||
return request<any, PageResult<AiCommandRecordVO[]>>({
|
||||
url: "/api/v1/ai/command/records",
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销命令执行(如果支持)
|
||||
*
|
||||
* @param auditId 审计ID
|
||||
* @returns 撤销结果
|
||||
*/
|
||||
static rollbackCommand(auditId: string) {
|
||||
static rollbackCommand(recordId: string) {
|
||||
return request({
|
||||
url: `/api/v1/ai/command/rollback/${auditId}`,
|
||||
url: `/api/v1/ai/command/rollback/${recordId}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
v-model="command"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="试试说:查询姓名为张三用户 或者:跳转到用户管理 按 Ctrl+Enter 快速发送"
|
||||
placeholder="试试说:修改test用户的姓名为测试人员 或者:跳转到用户管理 按 Ctrl+Enter 快速发送"
|
||||
:disabled="loading"
|
||||
@keydown.ctrl.enter="handleExecute"
|
||||
/>
|
||||
@@ -71,6 +71,19 @@
|
||||
<el-tag type="warning" size="small">{{ response.action.query }}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="response.action.type === 'navigate-and-execute'">
|
||||
<el-icon><Position /></el-icon>
|
||||
跳转至:
|
||||
<strong>{{ response.action.pageName }}</strong>
|
||||
<span v-if="response.action.query" class="query-info">
|
||||
并搜索:
|
||||
<el-tag type="warning" size="small">{{ response.action.query }}</el-tag>
|
||||
</span>
|
||||
<el-divider direction="vertical" />
|
||||
<el-icon><Tools /></el-icon>
|
||||
执行:
|
||||
<strong>{{ response.action.functionCall.name }}</strong>
|
||||
</div>
|
||||
<div v-if="response.action.type === 'execute'">
|
||||
<el-icon><Tools /></el-icon>
|
||||
执行:
|
||||
@@ -94,21 +107,57 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
|
||||
type ToolFunctionCall = {
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
};
|
||||
|
||||
// 统一的动作描述(区分“跳转”、“跳转+执行”、“仅执行”三种场景)
|
||||
type AiAction =
|
||||
| {
|
||||
type: "navigate";
|
||||
path: string;
|
||||
pageName: string;
|
||||
query?: string;
|
||||
}
|
||||
| {
|
||||
type: "navigate-and-execute";
|
||||
path: string;
|
||||
pageName: string;
|
||||
query?: string;
|
||||
functionCall: ToolFunctionCall;
|
||||
}
|
||||
| {
|
||||
type: "execute";
|
||||
functionName: string;
|
||||
functionCall: ToolFunctionCall;
|
||||
};
|
||||
|
||||
type AiResponse = {
|
||||
explanation: string;
|
||||
action: AiAction | null;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 状态管理
|
||||
const dialogVisible = ref(false);
|
||||
const command = ref("");
|
||||
const loading = ref(false);
|
||||
const response = ref<any>(null);
|
||||
const response = ref<AiResponse | null>(null);
|
||||
|
||||
// 快捷命令示例
|
||||
const examples = ["查询姓名为张三的用户", "跳转到用户管理", "打开角色管理页面", "查看系统日志"];
|
||||
const examples = [
|
||||
"修改test用户的姓名为测试人员",
|
||||
"获取姓名为张三的用户信息",
|
||||
"跳转到用户管理",
|
||||
"打开角色管理页面",
|
||||
];
|
||||
|
||||
// 打开对话框
|
||||
const handleOpen = () => {
|
||||
@@ -126,17 +175,26 @@ const handleClose = () => {
|
||||
|
||||
// 执行命令
|
||||
const handleExecute = async () => {
|
||||
if (!command.value.trim()) {
|
||||
const rawCommand = command.value.trim();
|
||||
if (!rawCommand) {
|
||||
ElMessage.warning("请输入命令");
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先检测无需调用 AI 的纯跳转命令
|
||||
const directNavigation = tryDirectNavigate(rawCommand);
|
||||
if (directNavigation) {
|
||||
response.value = directNavigation;
|
||||
await executeAction(directNavigation.action);
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 调用 AI API 解析命令
|
||||
const result = await AiCommandApi.parseCommand({
|
||||
command: command.value,
|
||||
command: rawCommand,
|
||||
currentRoute: router.currentRoute.value.path,
|
||||
currentComponent: router.currentRoute.value.name as string,
|
||||
context: {
|
||||
@@ -150,9 +208,9 @@ const handleExecute = async () => {
|
||||
}
|
||||
|
||||
// 解析 AI 返回的操作类型
|
||||
const action = parseAction(result);
|
||||
const action = parseAction(result, rawCommand);
|
||||
response.value = {
|
||||
explanation: result.explanation,
|
||||
explanation: result.explanation ?? "命令解析成功,准备执行操作",
|
||||
action,
|
||||
};
|
||||
|
||||
@@ -168,50 +226,199 @@ const handleExecute = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 解析 AI 返回的操作类型
|
||||
const parseAction = (result: any) => {
|
||||
const cmd = command.value.toLowerCase();
|
||||
// 路由配置映射表(支持扩展)
|
||||
const routeConfig = [
|
||||
{ keywords: ["用户", "user", "user list"], path: "/system/user", name: "用户管理" },
|
||||
{ keywords: ["角色", "role"], path: "/system/role", name: "角色管理" },
|
||||
{ keywords: ["菜单", "menu"], path: "/system/menu", name: "菜单管理" },
|
||||
{ keywords: ["部门", "dept"], path: "/system/dept", name: "部门管理" },
|
||||
{ keywords: ["字典", "dict"], path: "/system/dict", name: "字典管理" },
|
||||
{ keywords: ["日志", "log"], path: "/system/log", name: "系统日志" },
|
||||
];
|
||||
|
||||
// 判断是否是导航命令
|
||||
// 根据函数名推断路由(如 getUserInfo -> /system/user)
|
||||
const normalizeText = (text: string) => text.replace(/\s+/g, " ").trim().toLowerCase();
|
||||
|
||||
const inferRouteFromFunction = (functionName: string) => {
|
||||
const fnLower = normalizeText(functionName);
|
||||
for (const config of routeConfig) {
|
||||
// 检查函数名是否包含关键词(如 getUserInfo 包含 user)
|
||||
if (config.keywords.some((kw) => fnLower.includes(kw.toLowerCase()))) {
|
||||
return { path: config.path, name: config.name };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 根据命令文本匹配路由
|
||||
const matchRouteFromCommand = (cmd: string) => {
|
||||
const normalized = normalizeText(cmd);
|
||||
for (const config of routeConfig) {
|
||||
if (config.keywords.some((kw) => normalized.includes(kw.toLowerCase()))) {
|
||||
return { path: config.path, name: config.name };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const extractKeywordFromCommand = (cmd: string): string => {
|
||||
const normalized = normalizeText(cmd);
|
||||
// 从 routeConfig 动态获取所有数据类型关键词
|
||||
const allKeywords = routeConfig.flatMap((config) =>
|
||||
config.keywords.map((kw) => kw.toLowerCase())
|
||||
);
|
||||
const keywordsPattern = allKeywords.join("|");
|
||||
|
||||
const patterns = [
|
||||
new RegExp(`(?:查询|获取|搜索|查找|找).*?([^\\s,,。]+?)(?:的)?(?:${keywordsPattern})`, "i"),
|
||||
new RegExp(`(?:${keywordsPattern}).*?([^\\s,,。]+?)(?:的|信息|详情)?`, "i"),
|
||||
new RegExp(
|
||||
`(?:姓名为|名字叫|叫做|名称为|名是|为)([^\\s,,。]+?)(?:的)?(?:${keywordsPattern})?`,
|
||||
"i"
|
||||
),
|
||||
new RegExp(`([^\\s,,。]+?)(?:的)?(?:${keywordsPattern})(?:信息|详情)?`, "i"),
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = normalized.match(pattern);
|
||||
if (match && match[1]) {
|
||||
let extracted = match[1].trim();
|
||||
extracted = extracted.replace(/姓名为|名字叫|叫做|名称为|名是|为|的|信息|详情/g, "");
|
||||
if (
|
||||
cmd.includes("跳转") ||
|
||||
cmd.includes("打开") ||
|
||||
cmd.includes("进入") ||
|
||||
cmd.includes("查询")
|
||||
extracted &&
|
||||
!allKeywords.some((type) => extracted.toLowerCase().includes(type.toLowerCase()))
|
||||
) {
|
||||
// 提取关键字
|
||||
let keyword = "";
|
||||
let routePath = "";
|
||||
let pageName = "";
|
||||
|
||||
if (cmd.includes("用户")) {
|
||||
routePath = "/system/user";
|
||||
pageName = "用户管理";
|
||||
|
||||
// 提取查询关键字
|
||||
const match = cmd.match(/查询.*?([^\s]+).*?用户|用户.*?([^\s]+)/);
|
||||
if (match) {
|
||||
keyword = match[1] || match[2] || "";
|
||||
// 去掉常见的修饰词
|
||||
keyword = keyword.replace(/姓名为|名字叫|叫做|为/g, "");
|
||||
return extracted;
|
||||
}
|
||||
} else if (cmd.includes("角色")) {
|
||||
routePath = "/system/role";
|
||||
pageName = "角色管理";
|
||||
} else if (cmd.includes("菜单")) {
|
||||
routePath = "/system/menu";
|
||||
pageName = "菜单管理";
|
||||
} else if (cmd.includes("部门")) {
|
||||
routePath = "/system/dept";
|
||||
pageName = "部门管理";
|
||||
} else if (cmd.includes("字典")) {
|
||||
routePath = "/system/dict";
|
||||
pageName = "字典管理";
|
||||
} else if (cmd.includes("日志")) {
|
||||
routePath = "/system/log";
|
||||
pageName = "系统日志";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const tryDirectNavigate = (rawCommand: string): AiResponse | null => {
|
||||
const navigationIntents = ["跳转", "打开", "进入", "前往", "去", "浏览", "查看"];
|
||||
const operationIntents = [
|
||||
"修改",
|
||||
"更新",
|
||||
"变更",
|
||||
"删除",
|
||||
"添加",
|
||||
"创建",
|
||||
"设置",
|
||||
"获取",
|
||||
"查询",
|
||||
"搜索",
|
||||
];
|
||||
|
||||
const hasNavigationIntent = navigationIntents.some((keyword) => rawCommand.includes(keyword));
|
||||
const hasOperationIntent = operationIntents.some((keyword) => rawCommand.includes(keyword));
|
||||
|
||||
if (!hasNavigationIntent || hasOperationIntent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const routeInfo = matchRouteFromCommand(rawCommand);
|
||||
if (!routeInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyword = extractKeywordFromCommand(rawCommand);
|
||||
const action: AiAction = {
|
||||
type: "navigate",
|
||||
path: routeInfo.path,
|
||||
pageName: routeInfo.name,
|
||||
query: keyword || undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
explanation: `检测到跳转命令,正在前往 ${routeInfo.name}`,
|
||||
action,
|
||||
};
|
||||
};
|
||||
|
||||
// 解析 AI 返回的操作类型
|
||||
const parseAction = (result: any, rawCommand: string): AiAction | null => {
|
||||
const cmd = normalizeText(rawCommand);
|
||||
const primaryCall = result.functionCalls?.[0];
|
||||
const functionName = primaryCall?.name;
|
||||
|
||||
// 优先从函数名推断路由,其次从命令文本匹配
|
||||
let routeInfo = functionName ? inferRouteFromFunction(functionName) : null;
|
||||
if (!routeInfo) {
|
||||
routeInfo = matchRouteFromCommand(cmd);
|
||||
}
|
||||
|
||||
const routePath = routeInfo?.path || "";
|
||||
const pageName = routeInfo?.name || "";
|
||||
const keyword = extractKeywordFromCommand(cmd);
|
||||
|
||||
if (primaryCall && functionName) {
|
||||
const fnNameLower = functionName.toLowerCase();
|
||||
|
||||
// 1) 查询类函数(query/search/list/get)-> 跳转并执行筛选操作
|
||||
const isQueryFunction =
|
||||
fnNameLower.includes("query") ||
|
||||
fnNameLower.includes("search") ||
|
||||
fnNameLower.includes("list") ||
|
||||
fnNameLower.includes("get");
|
||||
|
||||
if (isQueryFunction) {
|
||||
// 统一使用 keywords 参数(约定大于配置)
|
||||
const args = (primaryCall.arguments || {}) as Record<string, unknown>;
|
||||
const keywords =
|
||||
typeof args.keywords === "string" && args.keywords.trim().length > 0
|
||||
? args.keywords
|
||||
: keyword;
|
||||
|
||||
if (routePath) {
|
||||
return {
|
||||
type: "navigate-and-execute",
|
||||
path: routePath,
|
||||
pageName,
|
||||
functionCall: primaryCall,
|
||||
query: keywords || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 其他操作类函数(修改/删除/创建/更新等)-> 跳转并执行
|
||||
const isModifyFunction =
|
||||
fnNameLower.includes("update") ||
|
||||
fnNameLower.includes("modify") ||
|
||||
fnNameLower.includes("edit") ||
|
||||
fnNameLower.includes("delete") ||
|
||||
fnNameLower.includes("remove") ||
|
||||
fnNameLower.includes("create") ||
|
||||
fnNameLower.includes("add") ||
|
||||
fnNameLower.includes("save");
|
||||
|
||||
if (isModifyFunction && routePath) {
|
||||
return {
|
||||
type: "navigate-and-execute",
|
||||
path: routePath,
|
||||
pageName,
|
||||
functionCall: primaryCall,
|
||||
};
|
||||
}
|
||||
|
||||
// 3) 其他未匹配的函数,如果有路由则跳转,否则执行
|
||||
if (routePath) {
|
||||
return {
|
||||
type: "navigate-and-execute",
|
||||
path: routePath,
|
||||
pageName,
|
||||
functionCall: primaryCall,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "execute",
|
||||
functionName,
|
||||
functionCall: primaryCall,
|
||||
};
|
||||
}
|
||||
|
||||
// 4) 无函数调用,仅跳转
|
||||
if (routePath) {
|
||||
return {
|
||||
type: "navigate",
|
||||
@@ -220,22 +427,56 @@ const parseAction = (result: any) => {
|
||||
query: keyword || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是导航命令,则是执行命令
|
||||
if (result.functionCalls && result.functionCalls.length > 0) {
|
||||
return {
|
||||
type: "execute",
|
||||
functionName: result.functionCalls[0].name,
|
||||
functionCall: result.functionCalls[0],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 定时器引用(用于清理)
|
||||
let navigationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let executeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// 执行操作
|
||||
const executeAction = async (action: any) => {
|
||||
const executeAction = async (action: AiAction) => {
|
||||
// 🎯 新增:跳转并执行操作
|
||||
if (action.type === "navigate-and-execute") {
|
||||
ElMessage.success(`正在跳转到 ${action.pageName} 并执行操作...`);
|
||||
|
||||
// 清理之前的定时器
|
||||
if (navigationTimer) {
|
||||
clearTimeout(navigationTimer);
|
||||
}
|
||||
|
||||
// 跳转并传递待执行的操作信息
|
||||
navigationTimer = setTimeout(() => {
|
||||
navigationTimer = null;
|
||||
const queryParams: any = {
|
||||
// 通过 URL 参数传递 AI 操作信息
|
||||
aiAction: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
functionName: action.functionCall.name,
|
||||
arguments: action.functionCall.arguments,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
// 如果有查询关键字,也一并传递
|
||||
if (action.query) {
|
||||
queryParams.keywords = action.query;
|
||||
queryParams.autoSearch = "true";
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: action.path,
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
// 关闭对话框
|
||||
handleClose();
|
||||
}, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === "navigate") {
|
||||
// 检查是否已经在目标页面
|
||||
const currentPath = router.currentRoute.value.path;
|
||||
@@ -268,8 +509,14 @@ const executeAction = async (action: any) => {
|
||||
// 不在目标页面,正常跳转
|
||||
ElMessage.success(`正在跳转到 ${action.pageName}...`);
|
||||
|
||||
// 清理之前的定时器
|
||||
if (navigationTimer) {
|
||||
clearTimeout(navigationTimer);
|
||||
}
|
||||
|
||||
// 延迟一下让用户看到提示
|
||||
setTimeout(() => {
|
||||
navigationTimer = setTimeout(() => {
|
||||
navigationTimer = null;
|
||||
// 跳转并传递查询参数
|
||||
router.push({
|
||||
path: action.path,
|
||||
@@ -288,13 +535,31 @@ const executeAction = async (action: any) => {
|
||||
// 执行函数调用
|
||||
ElMessage.info("功能开发中,请前往 AI 命令助手页面体验完整功能");
|
||||
|
||||
// 清理之前的定时器
|
||||
if (executeTimer) {
|
||||
clearTimeout(executeTimer);
|
||||
}
|
||||
|
||||
// 可以跳转到完整的 AI 命令页面
|
||||
setTimeout(() => {
|
||||
executeTimer = setTimeout(() => {
|
||||
executeTimer = null;
|
||||
router.push("/function/ai-command");
|
||||
handleClose();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onBeforeUnmount(() => {
|
||||
if (navigationTimer) {
|
||||
clearTimeout(navigationTimer);
|
||||
navigationTimer = null;
|
||||
}
|
||||
if (executeTimer) {
|
||||
clearTimeout(executeTimer);
|
||||
executeTimer = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,3 +7,6 @@ export { useTokenRefresh } from "./auth/useTokenRefresh";
|
||||
export { useLayout } from "./layout/useLayout";
|
||||
export { useLayoutMenu } from "./layout/useLayoutMenu";
|
||||
export { useDeviceDetection } from "./layout/useDeviceDetection";
|
||||
|
||||
export { useAiAction } from "./useAiAction";
|
||||
export type { UseAiActionOptions, AiActionHandler } from "./useAiAction";
|
||||
|
||||
202
src/composables/useAiAction.ts
Normal file
202
src/composables/useAiAction.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
|
||||
/**
|
||||
* AI 操作处理器类型
|
||||
*/
|
||||
export type AiActionHandler = (args: any) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* AI 操作配置
|
||||
*/
|
||||
export interface UseAiActionOptions {
|
||||
/** 操作映射表:函数名 -> 处理器 */
|
||||
actionHandlers?: Record<string, AiActionHandler>;
|
||||
/** 数据刷新函数(操作完成后调用) */
|
||||
onRefresh?: () => Promise<void> | void;
|
||||
/** 自动搜索处理函数 */
|
||||
onAutoSearch?: (keywords: string) => void;
|
||||
/** 当前路由路径(用于执行命令时传递) */
|
||||
currentRoute?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 操作 Composable
|
||||
*
|
||||
* 统一处理 AI 助手传递的操作,支持:
|
||||
* - 自动搜索(通过 keywords + autoSearch 参数)
|
||||
* - 执行 AI 操作(通过 aiAction 参数)
|
||||
* - 配置化的操作处理器
|
||||
*/
|
||||
export function useAiAction(options: UseAiActionOptions = {}) {
|
||||
const route = useRoute();
|
||||
const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options;
|
||||
|
||||
// 用于跟踪是否已卸载,防止在卸载后执行回调
|
||||
let isUnmounted = false;
|
||||
// 存储待清理的 nextTick 回调
|
||||
let pendingCallbacks: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* 执行 AI 操作
|
||||
*/
|
||||
async function executeAiAction(action: any) {
|
||||
if (isUnmounted) return;
|
||||
// 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } }
|
||||
const fnCall = action.functionCall ?? {
|
||||
name: action.functionName,
|
||||
arguments: action.arguments,
|
||||
};
|
||||
|
||||
if (!fnCall?.name) {
|
||||
ElMessage.warning("未识别的 AI 操作");
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找对应的处理器
|
||||
const handler = actionHandlers[fnCall.name];
|
||||
if (!handler) {
|
||||
ElMessage.warning(`暂不支持操作: ${fnCall.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await handler(fnCall.arguments);
|
||||
// 操作成功后刷新数据
|
||||
if (onRefresh) {
|
||||
await onRefresh();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("AI 操作执行失败:", error);
|
||||
ElMessage.error(error.message || "操作执行失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行后端命令(通用方法)
|
||||
*/
|
||||
async function executeCommand(
|
||||
functionName: string,
|
||||
args: any,
|
||||
options: {
|
||||
originalCommand?: string;
|
||||
confirmMode?: "auto" | "manual";
|
||||
needConfirm?: boolean;
|
||||
confirmMessage?: string;
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
originalCommand = "",
|
||||
confirmMode = "manual",
|
||||
needConfirm = false,
|
||||
confirmMessage,
|
||||
} = options;
|
||||
|
||||
// 如果需要确认,先显示确认对话框
|
||||
if (needConfirm && confirmMessage) {
|
||||
const { ElMessageBox } = await import("element-plus");
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", {
|
||||
confirmButtonText: "确认执行",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
} catch {
|
||||
ElMessage.info("已取消操作");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await AiCommandApi.executeCommand({
|
||||
originalCommand,
|
||||
confirmMode,
|
||||
userConfirmed: true,
|
||||
currentRoute,
|
||||
functionCall: {
|
||||
name: functionName,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
|
||||
ElMessage.success("操作执行成功");
|
||||
} catch (error: any) {
|
||||
if (error !== "cancel") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自动搜索
|
||||
*/
|
||||
function handleAutoSearch(keywords: string) {
|
||||
if (onAutoSearch) {
|
||||
onAutoSearch(keywords);
|
||||
} else {
|
||||
ElMessage.info(`AI 助手已为您自动搜索:${keywords}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:处理 URL 参数中的 AI 操作
|
||||
*/
|
||||
function init() {
|
||||
if (isUnmounted) return;
|
||||
|
||||
// 检查是否有 AI 助手传递的搜索参数
|
||||
const keywords = route.query.keywords as string;
|
||||
const autoSearch = route.query.autoSearch as string;
|
||||
const aiActionParam = route.query.aiAction as string;
|
||||
|
||||
// 处理自动搜索
|
||||
if (autoSearch === "true" && keywords) {
|
||||
const callback = () => {
|
||||
if (!isUnmounted) {
|
||||
handleAutoSearch(keywords);
|
||||
}
|
||||
};
|
||||
pendingCallbacks.push(callback);
|
||||
nextTick(callback);
|
||||
}
|
||||
|
||||
// 处理 AI 操作
|
||||
if (aiActionParam) {
|
||||
const callback = () => {
|
||||
if (!isUnmounted) {
|
||||
try {
|
||||
const aiAction = JSON.parse(decodeURIComponent(aiActionParam));
|
||||
executeAiAction(aiAction);
|
||||
} catch (error) {
|
||||
console.error("解析 AI 操作失败:", error);
|
||||
ElMessage.error("AI 操作参数解析失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
pendingCallbacks.push(callback);
|
||||
nextTick(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时自动初始化
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounted = true;
|
||||
// 清理待执行的回调(虽然 nextTick 的回调无法直接取消,但至少标记为已卸载)
|
||||
pendingCallbacks = [];
|
||||
});
|
||||
|
||||
return {
|
||||
executeAiAction,
|
||||
executeCommand,
|
||||
handleAutoSearch,
|
||||
init,
|
||||
};
|
||||
}
|
||||
412
src/views/ai/command-record/index.vue
Normal file
412
src/views/ai/command-record/index.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item prop="keywords" label="关键字">
|
||||
<el-input
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="原始命令/函数名称/用户名"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="provider" label="AI提供商">
|
||||
<el-select
|
||||
v-model="queryParams.provider"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="通义千问" value="qwen" />
|
||||
<el-option label="OpenAI" value="openai" />
|
||||
<el-option label="DeepSeek" value="deepseek" />
|
||||
<el-option label="Gemini" value="gemini" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="model" label="AI模型">
|
||||
<el-input
|
||||
v-model="queryParams.model"
|
||||
placeholder="如 qwen-plus"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="parseSuccess" label="解析状态">
|
||||
<el-select
|
||||
v-model="queryParams.parseSuccess"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="成功" :value="true" />
|
||||
<el-option label="失败" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="executeStatus" label="执行状态">
|
||||
<el-select
|
||||
v-model="queryParams.executeStatus"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="待执行" value="pending" />
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="isDangerous" label="风险操作">
|
||||
<el-select
|
||||
v-model="queryParams.isDangerous"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="createTime" label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
:editable="false"
|
||||
type="daterange"
|
||||
range-separator="~"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="截止时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="handleResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card shadow="hover" class="data-table">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="pageData"
|
||||
highlight-current-row
|
||||
border
|
||||
class="data-table__content"
|
||||
>
|
||||
<el-table-column label="创建时间" prop="createTime" width="180" />
|
||||
<el-table-column label="用户名" prop="username" width="120" />
|
||||
<el-table-column
|
||||
label="原始命令"
|
||||
prop="originalCommand"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="函数名称"
|
||||
prop="functionName"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="AI提供商" prop="provider" width="120" />
|
||||
<el-table-column label="AI模型" prop="model" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="解析状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.parseSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ row.parseSuccess ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.executeStatus" :type="statusTagType[row.executeStatus]" size="small">
|
||||
{{ statusText[row.executeStatus] }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风险" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isDangerous" type="warning" size="small">风险</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置信度" prop="confidence" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.confidence !== undefined && row.confidence !== null">
|
||||
{{ (row.confidence * 100).toFixed(0) }}%
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析耗时(ms)" prop="parseTime" width="120" align="center" />
|
||||
<el-table-column label="执行耗时(ms)" prop="executionTime" width="120" align="center" />
|
||||
<el-table-column label="IP地址" prop="ipAddress" width="140" />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleViewDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="fetchData"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="AI 命令记录详情" width="880px" append-to-body>
|
||||
<el-descriptions v-if="currentRow" :column="2" border>
|
||||
<el-descriptions-item label="记录ID">
|
||||
{{ currentRow.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">
|
||||
{{ currentRow.username }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="AI提供商">
|
||||
{{ currentRow.provider || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="AI模型">
|
||||
{{ currentRow.model || "-" }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="解析状态">
|
||||
<el-tag :type="currentRow.parseSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ currentRow.parseSuccess ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="置信度">
|
||||
<span v-if="currentRow.confidence !== undefined && currentRow.confidence !== null">
|
||||
{{ (currentRow.confidence * 100).toFixed(0) }}%
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="解析耗时">
|
||||
{{ formatNumber(currentRow.parseTime) }} ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Token统计">
|
||||
输入 {{ currentRow.inputTokens || 0 }} / 输出 {{ currentRow.outputTokens || 0 }} / 总计
|
||||
{{ currentRow.totalTokens || 0 }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="原始命令" :span="2">
|
||||
<el-input :model-value="currentRow.originalCommand" type="textarea" :rows="2" readonly />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.explanation" label="AI说明" :span="2">
|
||||
{{ currentRow.explanation }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.functionCalls" label="函数调用" :span="2">
|
||||
<el-input
|
||||
:model-value="formatJson(currentRow.functionCalls)"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
readonly
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.parseErrorMessage" label="解析错误" :span="2">
|
||||
<el-alert :title="currentRow.parseErrorMessage" type="error" :closable="false" />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="函数名称">
|
||||
{{ currentRow.functionName || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行状态">
|
||||
<el-tag
|
||||
v-if="currentRow.executeStatus"
|
||||
:type="statusTagType[currentRow.executeStatus]"
|
||||
size="small"
|
||||
>
|
||||
{{ statusText[currentRow.executeStatus] }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="执行耗时">
|
||||
{{ formatNumber(currentRow.executionTime) }} ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="影响行数">
|
||||
{{ formatNumber(currentRow.affectedRows) }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.functionArguments" label="执行参数" :span="2">
|
||||
<el-input
|
||||
:model-value="formatJson(currentRow.functionArguments)"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
readonly
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.executeResult" label="执行结果" :span="2">
|
||||
<el-input
|
||||
:model-value="formatJson(currentRow.executeResult)"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
readonly
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.executeErrorMessage" label="执行错误" :span="2">
|
||||
<el-alert :title="currentRow.executeErrorMessage" type="error" :closable="false" />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="风险操作">
|
||||
<el-tag v-if="currentRow.isDangerous" type="warning" size="small">风险操作</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否确认">
|
||||
<span v-if="currentRow.requiresConfirmation">
|
||||
{{ currentRow.userConfirmed ? "已确认" : "待确认" }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="IP地址">
|
||||
{{ currentRow.ipAddress || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="页面路由">
|
||||
{{ currentRow.currentRoute || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="User-Agent" :span="2">
|
||||
{{ currentRow.userAgent || "-" }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentRow.createTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ currentRow.updateTime || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentRow.remark || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "AiCommandRecord",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import AiCommandApi, { AiCommandRecordVO, AiCommandRecordPageQuery } from "@/api/ai";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
const queryFormRef = ref();
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
|
||||
const queryParams = reactive<AiCommandRecordPageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
provider: "",
|
||||
model: "",
|
||||
parseSuccess: undefined,
|
||||
executeStatus: "",
|
||||
isDangerous: undefined,
|
||||
createTime: ["", ""],
|
||||
});
|
||||
|
||||
const pageData = ref<AiCommandRecordVO[]>([]);
|
||||
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRow = ref<AiCommandRecordVO>();
|
||||
|
||||
const statusText: Record<string, string> = {
|
||||
pending: "待执行",
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
};
|
||||
|
||||
const statusTagType: Record<string, "info" | "success" | "danger"> = {
|
||||
pending: "info",
|
||||
success: "success",
|
||||
failed: "danger",
|
||||
};
|
||||
|
||||
function fetchData() {
|
||||
loading.value = true;
|
||||
AiCommandApi.getCommandRecordPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleResetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.pageNum = 1;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleViewDetail(row: AiCommandRecordVO) {
|
||||
currentRow.value = row;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function formatJson(jsonStr?: string) {
|
||||
if (!jsonStr) return "-";
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(jsonStr), null, 2);
|
||||
} catch {
|
||||
return jsonStr;
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(value?: number | string | null) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return "-";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,599 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="ai-command-panel">
|
||||
<!-- 命令输入区 -->
|
||||
<el-card class="command-input-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
AI 命令助手
|
||||
</span>
|
||||
<el-tag v-if="mcpConnected" type="success" size="small">
|
||||
<el-icon><Connection /></el-icon>
|
||||
MCP 已连接
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
MCP 未连接
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="commandText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入自然语言命令,例如:删除姓名为张三的用户"
|
||||
:disabled="loading"
|
||||
@keydown.ctrl.enter="handleParseCommand"
|
||||
/>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="!commandText.trim()"
|
||||
@click="handleParseCommand"
|
||||
>
|
||||
<el-icon><Search /></el-icon>
|
||||
解析命令 (Ctrl+Enter)
|
||||
</el-button>
|
||||
<el-button @click="handleClear">
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空
|
||||
</el-button>
|
||||
<el-button @click="handleShowHistory">
|
||||
<el-icon><Clock /></el-icon>
|
||||
历史记录
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 解析结果展示 -->
|
||||
<el-card v-if="parseResult" class="result-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">解析结果</span>
|
||||
<el-tag v-if="parseResult.success" type="success" size="small">
|
||||
置信度: {{ ((parseResult.confidence ?? 0) * 100).toFixed(1) }}%
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger" size="small">解析失败</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- AI 理解说明 -->
|
||||
<el-alert
|
||||
v-if="parseResult.explanation"
|
||||
:title="parseResult.explanation"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="explanation-alert"
|
||||
/>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<el-alert
|
||||
v-if="parseResult.error"
|
||||
:title="parseResult.error"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="error-alert"
|
||||
/>
|
||||
|
||||
<!-- 函数调用列表 -->
|
||||
<div
|
||||
v-if="parseResult.functionCalls && parseResult.functionCalls.length > 0"
|
||||
class="function-calls"
|
||||
>
|
||||
<div
|
||||
v-for="(funcCall, index) in parseResult.functionCalls"
|
||||
:key="index"
|
||||
class="function-call-item"
|
||||
>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="function-header">
|
||||
<span class="function-name">
|
||||
<el-icon><Tools /></el-icon>
|
||||
{{ funcCall.name }}
|
||||
</span>
|
||||
<el-tag type="primary" size="small">步骤 {{ index + 1 }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="function-content">
|
||||
<div v-if="funcCall.description" class="function-description">
|
||||
<strong>说明:</strong>
|
||||
{{ funcCall.description }}
|
||||
</div>
|
||||
|
||||
<div class="function-arguments">
|
||||
<strong>参数:</strong>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item
|
||||
v-for="(value, key) in funcCall.arguments"
|
||||
:key="key"
|
||||
:label="key"
|
||||
>
|
||||
<el-tag v-if="typeof value === 'boolean'" :type="value ? 'success' : 'info'">
|
||||
{{ value }}
|
||||
</el-tag>
|
||||
<el-tag v-else-if="typeof value === 'number'" type="warning">
|
||||
{{ value }}
|
||||
</el-tag>
|
||||
<span v-else-if="typeof value === 'object'">
|
||||
<code>{{ JSON.stringify(value, null, 2) }}</code>
|
||||
</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="function-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="executingIndex === index"
|
||||
@click="handleExecute(funcCall, index)"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
执行此步骤
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="executeResults[index]"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleViewResult(index)"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
查看结果
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 执行结果 -->
|
||||
<div v-if="executeResults[index]" class="execute-result">
|
||||
<el-divider />
|
||||
<el-alert
|
||||
:title="executeResults[index].success ? '执行成功' : '执行失败'"
|
||||
:type="executeResults[index].success ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template v-if="executeResults[index].message">
|
||||
{{ executeResults[index].message }}
|
||||
</template>
|
||||
<template v-if="executeResults[index].affectedRows">
|
||||
(影响 {{ executeResults[index].affectedRows }} 条记录)
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 批量执行 -->
|
||||
<div class="batch-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="batchExecuting"
|
||||
:disabled="allExecuted"
|
||||
@click="handleBatchExecute"
|
||||
>
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
批量执行所有步骤
|
||||
</el-button>
|
||||
<el-button v-if="hasExecutedSteps" type="danger" @click="handleClearResults">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
清除结果
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 上下文信息(开发模式) -->
|
||||
<el-card v-if="showContext && contextInfo" class="context-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">当前上下文</span>
|
||||
<el-button type="info" size="small" @click="showContext = !showContext">
|
||||
{{ showContext ? "隐藏" : "显示" }}上下文
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="当前路由">
|
||||
{{ contextInfo.currentRoute }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前组件">
|
||||
{{ contextInfo.currentComponent }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="MCP 端点">
|
||||
{{ contextInfo.mcpEndpoint || "未配置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户角色">
|
||||
{{ contextInfo.userRole || "未知" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="footer-actions">
|
||||
<el-button type="info" @click="showContext = !showContext">
|
||||
{{ showContext ? "隐藏" : "显示" }}上下文
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import AiCommandApi, {
|
||||
type FunctionCall,
|
||||
type AiCommandResponse,
|
||||
type AiExecuteResponse,
|
||||
} from "@/api/ai";
|
||||
import { useUserStoreHook } from "@/store/modules/user-store";
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStoreHook();
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
const commandText = ref("");
|
||||
const loading = ref(false);
|
||||
const parseResult = ref<AiCommandResponse | null>(null);
|
||||
const executeResults = ref<Record<number, AiExecuteResponse>>({});
|
||||
const executingIndex = ref<number | null>(null);
|
||||
const batchExecuting = ref(false);
|
||||
const showContext = ref(false);
|
||||
const mcpConnected = ref(false);
|
||||
|
||||
// ==================== 上下文信息 ====================
|
||||
const contextInfo = computed(() => ({
|
||||
currentRoute: route.path,
|
||||
currentComponent: route.name as string,
|
||||
mcpEndpoint: import.meta.env.DEV
|
||||
? `http://localhost:${import.meta.env.VITE_APP_PORT}/__mcp/sse`
|
||||
: null,
|
||||
userRole: userStore.userInfo?.roles?.join(", ") || "未知",
|
||||
}));
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
const allExecuted = computed(() => {
|
||||
if (!parseResult.value?.functionCalls) return false;
|
||||
return parseResult.value.functionCalls.every((_, index) => executeResults.value[index]?.success);
|
||||
});
|
||||
|
||||
const hasExecutedSteps = computed(() => {
|
||||
return Object.keys(executeResults.value).length > 0;
|
||||
});
|
||||
|
||||
// ==================== 方法 ====================
|
||||
|
||||
/**
|
||||
* 解析命令
|
||||
*/
|
||||
const handleParseCommand = async () => {
|
||||
if (!commandText.value.trim()) {
|
||||
ElMessage.warning("请输入命令");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
parseResult.value = null;
|
||||
executeResults.value = {};
|
||||
|
||||
try {
|
||||
const response = await AiCommandApi.parseCommand({
|
||||
command: commandText.value,
|
||||
currentRoute: contextInfo.value.currentRoute,
|
||||
currentComponent: contextInfo.value.currentComponent,
|
||||
context: {
|
||||
userRoles: userStore.userInfo?.roles || [],
|
||||
},
|
||||
});
|
||||
|
||||
parseResult.value = response;
|
||||
|
||||
if (response.success && response.functionCalls.length > 0) {
|
||||
ElMessage.success(`成功解析为 ${response.functionCalls.length} 个操作步骤`);
|
||||
} else if (!response.success) {
|
||||
ElMessage.error(response.error || "命令解析失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("解析命令失败:", error);
|
||||
ElMessage.error(error.message || "命令解析失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行单个函数调用
|
||||
*/
|
||||
const handleExecute = async (funcCall: FunctionCall, index: number) => {
|
||||
// 危险操作需要确认
|
||||
const isDangerous = ["delete", "remove", "drop", "truncate"].some((keyword) =>
|
||||
funcCall.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
|
||||
if (isDangerous) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认执行此操作吗?\n\n函数:${funcCall.name}\n参数:${JSON.stringify(funcCall.arguments, null, 2)}`,
|
||||
"危险操作确认",
|
||||
{
|
||||
confirmButtonText: "确认执行",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: false,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
ElMessage.info("已取消操作");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
executingIndex.value = index;
|
||||
|
||||
try {
|
||||
const result = await AiCommandApi.executeCommand({
|
||||
functionCall: funcCall,
|
||||
confirmMode: isDangerous ? "manual" : "auto",
|
||||
userConfirmed: isDangerous,
|
||||
idempotencyKey: `${Date.now()}-${index}`,
|
||||
});
|
||||
|
||||
executeResults.value[index] = result;
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success(result.message || "执行成功");
|
||||
} else {
|
||||
ElMessage.error(result.error || "执行失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("执行命令失败:", error);
|
||||
ElMessage.error(error.message || "执行失败");
|
||||
} finally {
|
||||
executingIndex.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量执行所有步骤
|
||||
*/
|
||||
const handleBatchExecute = async () => {
|
||||
if (!parseResult.value?.functionCalls) return;
|
||||
|
||||
const confirmMessage = `确认批量执行 ${parseResult.value.functionCalls.length} 个步骤吗?`;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmMessage, "批量执行确认", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
batchExecuting.value = true;
|
||||
|
||||
for (let i = 0; i < parseResult.value.functionCalls.length; i++) {
|
||||
if (executeResults.value[i]?.success) {
|
||||
continue; // 跳过已成功执行的
|
||||
}
|
||||
|
||||
await handleExecute(parseResult.value.functionCalls[i], i);
|
||||
|
||||
// 如果执行失败,停止后续执行
|
||||
if (!executeResults.value[i]?.success) {
|
||||
ElMessage.warning(`步骤 ${i + 1} 执行失败,已停止后续步骤`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
batchExecuting.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查看执行结果
|
||||
*/
|
||||
const handleViewResult = (index: number) => {
|
||||
const result = executeResults.value[index];
|
||||
if (!result) return;
|
||||
|
||||
ElMessageBox.alert(`<pre>${JSON.stringify(result.data, null, 2)}</pre>`, "执行结果详情", {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: "关闭",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空输入
|
||||
*/
|
||||
const handleClear = () => {
|
||||
commandText.value = "";
|
||||
parseResult.value = null;
|
||||
executeResults.value = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除执行结果
|
||||
*/
|
||||
const handleClearResults = () => {
|
||||
executeResults.value = {};
|
||||
ElMessage.success("已清除执行结果");
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示历史记录
|
||||
*/
|
||||
const handleShowHistory = () => {
|
||||
ElMessage.info("历史记录功能开发中...");
|
||||
// TODO: 实现历史记录弹窗
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查 MCP 连接状态
|
||||
*/
|
||||
const checkMcpConnection = async () => {
|
||||
if (!import.meta.env.DEV) {
|
||||
mcpConnected.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = contextInfo.value.mcpEndpoint;
|
||||
if (!endpoint) {
|
||||
mcpConnected.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的连接检查(实际应该有更可靠的方式)
|
||||
const response = await fetch(endpoint, { method: "HEAD" });
|
||||
mcpConnected.value = response.ok;
|
||||
} catch {
|
||||
mcpConnected.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(() => {
|
||||
checkMcpConnection();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-command-panel {
|
||||
.command-input-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.explanation-alert,
|
||||
.error-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.function-calls {
|
||||
.function-call-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.function-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.function-name {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.function-content {
|
||||
.function-description {
|
||||
padding: 8px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.function-arguments {
|
||||
margin-bottom: 12px;
|
||||
|
||||
code {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--el-fill-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.function-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.execute-result {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px dashed var(--el-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -95,6 +95,7 @@
|
||||
stripe
|
||||
highlight-current-row
|
||||
class="data-table__content"
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
@@ -244,10 +245,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick } from "vue";
|
||||
import { useAppStore } from "@/store/modules/app-store";
|
||||
import { DeviceEnum } from "@/enums/settings/device-enum";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useAiAction } from "@/composables";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
|
||||
import UserAPI, { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api";
|
||||
import DeptAPI from "@/api/system/dept-api";
|
||||
@@ -329,19 +333,83 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AI 助手相关 ====================
|
||||
|
||||
// 使用 AI 操作 Composable
|
||||
useAiAction({
|
||||
actionHandlers: {
|
||||
/** AI 修改用户昵称 */
|
||||
updateUserNickname: async (args: any) => {
|
||||
const username = args?.username;
|
||||
const nickname = args?.nickname;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`AI 助手将执行以下操作:<br/>
|
||||
<strong>修改用户:</strong> ${username}<br/>
|
||||
<strong>新昵称:</strong> ${nickname}<br/><br/>
|
||||
确认执行吗?`,
|
||||
"AI 助手操作确认",
|
||||
{
|
||||
confirmButtonText: "确认执行",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: true,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await AiCommandApi.executeCommand({
|
||||
originalCommand: `修改用户 ${username} 的昵称为 ${nickname}`,
|
||||
confirmMode: "manual",
|
||||
userConfirmed: true,
|
||||
currentRoute: route.path,
|
||||
functionCall: {
|
||||
name: "updateUserNickname",
|
||||
arguments: { username, nickname },
|
||||
},
|
||||
});
|
||||
|
||||
ElMessage.success(result?.message || "修改用户昵称成功");
|
||||
} catch (error: any) {
|
||||
if (error !== "cancel") {
|
||||
ElMessage.error(error?.message || "操作失败");
|
||||
} else {
|
||||
ElMessage.info("已取消操作");
|
||||
}
|
||||
}
|
||||
},
|
||||
/** AI 查询用户(在列表中筛选) */
|
||||
queryUser: async (args: any) => {
|
||||
const keywords = args?.keywords;
|
||||
if (keywords) {
|
||||
queryParams.keywords = keywords;
|
||||
await handleQuery();
|
||||
ElMessage.success(`已搜索:${keywords}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
onRefresh: fetchData,
|
||||
onAutoSearch: (keywords: string) => {
|
||||
queryParams.keywords = keywords;
|
||||
setTimeout(() => {
|
||||
handleQuery();
|
||||
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
|
||||
}, 300);
|
||||
},
|
||||
});
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchData();
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryParams.pageNum = 1;
|
||||
queryParams.deptId = undefined;
|
||||
queryParams.createTime = undefined;
|
||||
fetchData();
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 选中项发生变化
|
||||
@@ -524,21 +592,18 @@ function handleExport() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否有 AI 助手传递的搜索参数
|
||||
const keywords = route.query.keywords as string;
|
||||
// 检查是否有自动搜索参数
|
||||
const autoSearch = route.query.autoSearch as string;
|
||||
|
||||
if (autoSearch === "true" && keywords) {
|
||||
// 自动填充搜索关键字
|
||||
queryParams.keywords = keywords;
|
||||
// 延迟一下,让用户看到自动填充的效果
|
||||
setTimeout(() => {
|
||||
handleQuery();
|
||||
// 显示提示
|
||||
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
|
||||
}, 300);
|
||||
} else {
|
||||
// 如果有自动搜索,由 onAutoSearch 回调处理(会调用 handleQuery)
|
||||
// 如果有 AI 操作,先加载数据,然后由 useAiAction 处理操作(操作完成后会刷新)
|
||||
// 如果都没有,正常加载数据
|
||||
if (autoSearch !== "true") {
|
||||
// 延迟一下,确保 useAiAction 先初始化
|
||||
nextTick(() => {
|
||||
handleQuery();
|
||||
});
|
||||
}
|
||||
// 注意:autoSearch === "true" 时,onAutoSearch 回调会调用 handleQuery,所以这里不需要再调用
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -11,10 +11,6 @@ import UnoCSS from "unocss/vite";
|
||||
import { resolve } from "path";
|
||||
import { name, version, engines, dependencies, devDependencies } from "./package.json";
|
||||
|
||||
// MCP 插件:为项目开启 MCP Server(仅开发环境)
|
||||
// 使用前请先安装:pnpm add -D vite-plugin-vue-mcp
|
||||
import { VueMcp } from "vite-plugin-vue-mcp";
|
||||
|
||||
// 平台的名称、版本、运行所需的 node 版本、依赖、构建时间的类型提示
|
||||
const __APP_INFO__ = {
|
||||
pkg: { name, version, engines, dependencies, devDependencies },
|
||||
@@ -59,8 +55,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
plugins: [
|
||||
vue(),
|
||||
...(env.VITE_MOCK_DEV_SERVER === "true" ? [mockDevServerPlugin()] : []),
|
||||
// MCP 插件:仅在开发环境启用,用于 AI 工具集成,让 Cursor AI 能够读取应用运行时的 Store 状态,帮助调试和理解代码
|
||||
...(!isProduction ? [VueMcp()] : []),
|
||||
UnoCSS(),
|
||||
// API 自动导入
|
||||
AutoImport({
|
||||
|
||||
Reference in New Issue
Block a user