refactor(ai): AI助手优化和移除MCP插件

This commit is contained in:
Ray.Hao
2025-11-15 09:08:35 +08:00
parent a9f2697ef0
commit 5aa6773cac
10 changed files with 1101 additions and 722 deletions

View 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>

View File

@@ -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>

View File

@@ -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(() => {
// 如果有自动搜索,由 onAutoSearch 回调处理(会调用 handleQuery
// 如果有 AI 操作,先加载数据,然后由 useAiAction 处理操作(操作完成后会刷新)
// 如果都没有,正常加载数据
if (autoSearch !== "true") {
// 延迟一下,确保 useAiAction 先初始化
nextTick(() => {
handleQuery();
// 显示提示
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
}, 300);
} else {
handleQuery();
});
}
// 注意autoSearch === "true" 时onAutoSearch 回调会调用 handleQuery所以这里不需要再调用
});
</script>