更新
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** AI 助手(MCP)后台接口:挂在 /mcp/admin 下,沿用后台登录令牌 */
|
||||
const opts = { urlPrefix: 'mcp' }
|
||||
|
||||
/** AI 授权列表(有 ai.grant/lists 看全部,否则只看自己的) */
|
||||
export function aiGrantLists(params: any) {
|
||||
return request.get({ url: '/admin/grants', params }, opts)
|
||||
}
|
||||
|
||||
/** 撤销 AI 授权 */
|
||||
export function aiGrantRevoke(params: { id: number }) {
|
||||
return request.post({ url: '/admin/revoke', params }, opts)
|
||||
}
|
||||
|
||||
/** AI 访问日志 */
|
||||
export function aiAccessLogLists(params: any) {
|
||||
return request.get({ url: '/admin/logs', params }, opts)
|
||||
}
|
||||
|
||||
/** AI 数据目录与覆盖情况 */
|
||||
export function aiCatalogLists(params: any) {
|
||||
return request.get({ url: '/admin/catalog', params }, opts)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="ai-access-log-page">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="时间">
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
@change="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结果">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部" class="w-[120px]" @change="resetPage">
|
||||
<el-option v-for="(label, value) in STATUS" :key="value" :label="label" :value="value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据资源">
|
||||
<el-input v-model="queryParams.resource" placeholder="如 tcm.diagnosis" clearable class="w-[180px]" @keyup.enter="resetPage" @clear="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item label="记录ID">
|
||||
<el-input v-model="queryParams.record_id" placeholder="如诊单ID" clearable class="w-[130px]" @keyup.enter="resetPage" @clear="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务号">
|
||||
<el-input v-model="queryParams.client_task_id" placeholder="行知任务号" clearable class="w-[200px]" @keyup.enter="resetPage" @clear="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="page-hint">
|
||||
记录每一次 AI 查询:谁、通过哪个客户端任务、查了哪个数据资源、返回了哪些记录。按“记录ID”可以查某个诊单被哪些账号通过 AI 看过。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-3" shadow="never">
|
||||
<el-table :data="pager.lists" v-loading="pager.loading" size="default" stripe>
|
||||
<el-table-column label="时间" width="165" prop="create_time_text" />
|
||||
<el-table-column label="账号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.admin_name || (row.admin_id ? '#' + row.admin_id : '—') }}</div>
|
||||
<div class="cell-sub">{{ row.ip }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工具" width="170" prop="tool" />
|
||||
<el-table-column label="数据资源" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.resource_name || row.resource || '—' }}</div>
|
||||
<div class="cell-sub">{{ row.resource }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ok' ? 'success' : 'warning'" size="small">{{ STATUS[row.status] || row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="条数" width="70" align="right" prop="result_rows" />
|
||||
<el-table-column label="记录ID / 原因" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.record_ids" :content="row.record_ids" placement="top">
|
||||
<span class="ellipsis">{{ row.record_ids }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="cell-sub">{{ row.message }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.arguments" :content="row.arguments" placement="top">
|
||||
<span class="ellipsis">{{ row.arguments }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.duration_ms }}ms</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务号" min-width="160" prop="client_task_id" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="aiMcpAccessLog">
|
||||
import { aiAccessLogLists } from '@/api/ai_mcp'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
const STATUS: Record<string, string> = { ok: '成功', denied: '拒绝', invalid: '参数错误', limited: '超限', error: '失败' }
|
||||
const timeRange = ref<[string, string] | null>(null)
|
||||
const queryParams = reactive({ start_time: '', end_time: '', status: '', resource: '', record_id: '', client_task_id: '' })
|
||||
|
||||
watch(timeRange, (val) => {
|
||||
queryParams.start_time = val?.[0] ?? ''
|
||||
queryParams.end_time = val?.[1] ?? ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: aiAccessLogLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const handleReset = () => {
|
||||
timeRange.value = null
|
||||
resetParams()
|
||||
}
|
||||
|
||||
onMounted(() => getLists())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.cell-sub {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.ellipsis {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="ai-catalog-page">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div class="summary">
|
||||
<div class="summary-item">
|
||||
<div class="summary-value success">{{ counts.open ?? 0 }}</div>
|
||||
<div class="summary-label">已开放</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-value warning">{{ counts.pending ?? 0 }}</div>
|
||||
<div class="summary-label">待整改</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-value">{{ counts.excluded ?? 0 }}</div>
|
||||
<div class="summary-label">不开放</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :inline="true" class="mt-3">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部" class="w-[120px]" @change="resetPage">
|
||||
<el-option label="已开放" value="open" />
|
||||
<el-option label="待整改" value="pending" />
|
||||
<el-option label="不开放" value="excluded" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务分组">
|
||||
<el-select v-model="queryParams.domain" clearable filterable placeholder="全部" class="w-[180px]" @change="resetPage">
|
||||
<el-option v-for="d in domains" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="queryParams.keyword" placeholder="名称或资源标识" clearable class="w-[200px]" @keyup.enter="resetPage" @clear="resetPage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="page-hint">
|
||||
后台每个只读接口都是一个“数据资源”。已开放的资源由 AI 以调用账号自己的权限和数据范围执行原有接口;
|
||||
待整改的资源写明了原因(如缺少逐条权限校验、会调用外部接口、权限点未登记),整改或审核通过后开放。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-3" shadow="never">
|
||||
<el-table :data="pager.lists" v-loading="pager.loading" size="default" stripe>
|
||||
<el-table-column label="资源" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.name }}</div>
|
||||
<div class="cell-sub">{{ row.resource }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="业务分组" width="150" prop="domain" />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">{{ KIND[row.kind] || row.kind }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="STATUS[row.status]?.type" size="small">{{ STATUS[row.status]?.label || row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原因" min-width="260" prop="reason" />
|
||||
<el-table-column label="已审核" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.reviewed ? '是' : '自动' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="aiMcpCatalog">
|
||||
import { aiCatalogLists } from '@/api/ai_mcp'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { computed, onMounted, reactive } from 'vue'
|
||||
|
||||
const KIND: Record<string, string> = { list: '列表', detail: '详情', report: '统计', other: '其他', write: '写操作' }
|
||||
const STATUS: Record<string, { label: string; type: 'success' | 'warning' | 'info' }> = {
|
||||
open: { label: '已开放', type: 'success' },
|
||||
pending: { label: '待整改', type: 'warning' },
|
||||
excluded: { label: '不开放', type: 'info' }
|
||||
}
|
||||
const queryParams = reactive({ status: '', domain: '', keyword: '' })
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: aiCatalogLists,
|
||||
params: queryParams,
|
||||
size: 50
|
||||
})
|
||||
|
||||
const counts = computed<Record<string, number>>(() => pager.extend?.counts || {})
|
||||
const domains = computed<string[]>(() => pager.extend?.domains || [])
|
||||
|
||||
onMounted(() => getLists())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
display: flex;
|
||||
gap: 48px;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.summary-value.success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.summary-value.warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
.summary-label,
|
||||
.page-hint,
|
||||
.cell-sub {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-hint {
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="ai-grant-page">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
v-if="pager.extend && pager.extend.enabled === false"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
title="AI 助手接口未启用:服务器 .env 的 [AI_MCP] ENABLED 为 false,客户端暂时无法绑定和查询。"
|
||||
/>
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部" class="w-[120px]" @change="resetPage">
|
||||
<el-option label="有效" value="1" />
|
||||
<el-option label="已撤销" value="2" />
|
||||
<el-option label="已过期" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="姓名 / 账号 / 备注"
|
||||
clearable
|
||||
class="w-[220px]"
|
||||
@keyup.enter="resetPage"
|
||||
@clear="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="page-hint">
|
||||
员工在行知等 AI 助手里用甄养堂账号密码绑定后,会在这里生成一条只读授权。AI 只能按该账号自己的权限和数据范围查询;
|
||||
撤销后立即失效。账号改密、停用、失去“允许 AI 助手查询”权限时,授权也会自动失效。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-3" shadow="never">
|
||||
<el-table :data="pager.lists" v-loading="pager.loading" size="default" stripe>
|
||||
<el-table-column label="账号" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.admin_name || '—' }}</div>
|
||||
<div class="cell-sub">{{ row.admin_account }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户端" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.client }}<span v-if="row.client_instance"> · {{ row.client_instance }}</span></div>
|
||||
<div class="cell-sub">{{ row.label }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="令牌" width="130" prop="token_prefix" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status_text === '有效' ? 'success' : 'info'" size="small">{{ row.status_text }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近使用" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.last_used_time_text || '—' }}</div>
|
||||
<div class="cell-sub">{{ row.last_used_ip }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期时间" width="150" prop="expire_time_text" />
|
||||
<el-table-column label="签发时间" width="150" prop="create_time_text" />
|
||||
<el-table-column label="撤销" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.revoke_time_text">
|
||||
<div>{{ row.revoke_time_text }}</div>
|
||||
<div class="cell-sub">{{ reasonText(row.revoke_reason) }}</div>
|
||||
</template>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.can_revoke" type="danger" link @click="handleRevoke(row)">撤销</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="aiMcpGrant">
|
||||
import { aiGrantLists, aiGrantRevoke } from '@/api/ai_mcp'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
|
||||
const queryParams = reactive({ status: '', keyword: '' })
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: aiGrantLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const REASONS: Record<string, string> = {
|
||||
client_revoke: '客户端解绑',
|
||||
admin_revoke: '后台撤销',
|
||||
rebind: '重新绑定',
|
||||
expired: '到期/闲置失效',
|
||||
password_changed: '账号改密',
|
||||
admin_disabled: '账号停用',
|
||||
admin_deleted: '账号删除'
|
||||
}
|
||||
const reasonText = (reason: string) => REASONS[reason] || reason || ''
|
||||
|
||||
const handleRevoke = async (row: any) => {
|
||||
await feedback.confirm(`确认撤销 ${row.admin_name || row.admin_account} 的这条 AI 授权?撤销后该客户端需要重新绑定。`)
|
||||
await aiGrantRevoke({ id: row.id })
|
||||
feedback.msgSuccess('已撤销')
|
||||
getLists()
|
||||
}
|
||||
|
||||
onMounted(() => getLists())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.cell-sub {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user