fix: 收口管理端页面权限与工作流运行授权
- 页面选项接口改用所属页面权限并返回最小数据视图 - 统一校验工作流引用、租户、状态与定时任务执行主体 - 补充聊天记录权限迁移和权限隔离回归测试
This commit is contained in:
@@ -78,7 +78,7 @@ export interface AgentChatCapabilityPayload {
|
||||
}
|
||||
|
||||
export function getPublishedAgents() {
|
||||
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/options', {
|
||||
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/session/options', {
|
||||
params: { publishedOnly: true },
|
||||
});
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export function getAgentSession(sessionId: number | string) {
|
||||
|
||||
export function getPublishedKnowledges() {
|
||||
return api.get<RequestResult<AgentChatKnowledgeView[]>>(
|
||||
'/api/v1/agent/knowledgeOptions',
|
||||
'/api/v1/agent/session/knowledgeOptions',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import pageSource from './index.vue?raw';
|
||||
|
||||
describe('管理端智能体聊天历史契约', () => {
|
||||
it('使用智能体候选接口并保留 assistantId 查询参数', () => {
|
||||
expect(pageSource).toContain("'/api/v1/agent/options'");
|
||||
expect(pageSource).toContain("'/api/v1/chatHistory/agentOptions'");
|
||||
expect(pageSource).not.toContain('/api/v1/bot/list');
|
||||
expect(pageSource).toContain('label: item.name');
|
||||
expect(pageSource).toContain('assistantId: query.value.assistantId');
|
||||
|
||||
@@ -103,7 +103,9 @@ onMounted(async () => {
|
||||
|
||||
async function fetchAgents() {
|
||||
agentLoading.value = true;
|
||||
const [error, res] = await tryit(api.get)('/api/v1/agent/options');
|
||||
const [error, res] = await tryit(api.get)(
|
||||
'/api/v1/chatHistory/agentOptions',
|
||||
);
|
||||
agentLoading.value = false;
|
||||
if (error || res?.errorCode !== 0) {
|
||||
agentOptions.value = [];
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
@@ -102,6 +101,8 @@ const normalizeEntity = (raw: any = {}) => {
|
||||
const entity = ref<any>(normalizeEntity(defaultEntity));
|
||||
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
collectionType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
@@ -145,7 +146,7 @@ const collectionTypeList = [
|
||||
},
|
||||
];
|
||||
|
||||
function openDialog(row: any = {}) {
|
||||
async function openDialog(row: any = {}) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
entity.value = normalizeEntity(row);
|
||||
@@ -154,6 +155,17 @@ function openDialog(row: any = {}) {
|
||||
entity.value = normalizeEntity(defaultEntity);
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollectionCategory/visibleList',
|
||||
);
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -251,10 +263,14 @@ defineExpose({
|
||||
prop="categoryId"
|
||||
:label="$t('documentCollection.categoryId')"
|
||||
>
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiDocumentCollectionCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
|
||||
@@ -375,15 +375,9 @@ const loadModels = async (preferredId?: string) => {
|
||||
|
||||
const loadPublishBaseUrl = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysOption/list', {
|
||||
params: {
|
||||
keys: ['chat_publish_base_url'],
|
||||
},
|
||||
});
|
||||
const res = await api.get('/api/v1/model/gatewayConfig');
|
||||
if (res.errorCode === 0) {
|
||||
publishBaseUrl.value = String(
|
||||
res.data?.chat_publish_base_url || '',
|
||||
).trim();
|
||||
publishBaseUrl.value = String(res.data?.publishBaseUrl || '').trim();
|
||||
}
|
||||
} catch {
|
||||
publishBaseUrl.value = '';
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElMessage} from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import {onMounted, ref} from 'vue';
|
||||
|
||||
@@ -37,6 +44,8 @@ const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
resourceType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
@@ -57,7 +66,7 @@ const rules = ref({
|
||||
});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
@@ -65,6 +74,15 @@ function openDialog(row: any) {
|
||||
status: row?.status ?? 0,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/resourceCategory/visibleList');
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
@@ -158,10 +176,14 @@ function uploadError() {
|
||||
<ElInput v-model.trim="entity.resourceName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiResource.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiResourceCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
|
||||
@@ -151,9 +151,7 @@ async function initializeWorkflow() {
|
||||
}
|
||||
await Promise.all([
|
||||
loadCustomNode(),
|
||||
getLlmList(),
|
||||
getKnowledgeList(),
|
||||
getCodeEngineList(),
|
||||
getDesignerOptions(),
|
||||
getWorkflowInfo(workflowId.value),
|
||||
]);
|
||||
showTinyFlow.value = true;
|
||||
@@ -574,24 +572,14 @@ function reconcileWorkflowDraftAfterSave(savedContentSignature: string) {
|
||||
pendingDraftContent = normalizedContent;
|
||||
persistPendingWorkflowDraft();
|
||||
}
|
||||
async function getLlmList() {
|
||||
return api.get('/api/v1/model/list').then((res) => {
|
||||
llmList.value = res.data;
|
||||
});
|
||||
}
|
||||
async function getKnowledgeList() {
|
||||
return api.get('/api/v1/documentCollection/list').then((res) => {
|
||||
knowledgeList.value = res.data;
|
||||
});
|
||||
}
|
||||
async function getCodeEngineList() {
|
||||
return api.get('/api/v1/workflow/supportedCodeEngines').then((res) => {
|
||||
if (
|
||||
res?.errorCode === 0 &&
|
||||
Array.isArray(res.data) &&
|
||||
res.data.length > 0
|
||||
) {
|
||||
codeEngineList.value = res.data;
|
||||
async function getDesignerOptions() {
|
||||
return api.get('/api/v1/workflow/designer/options').then((res) => {
|
||||
llmList.value = Array.isArray(res.data?.models) ? res.data.models : [];
|
||||
knowledgeList.value = Array.isArray(res.data?.knowledges)
|
||||
? res.data.knowledges
|
||||
: [];
|
||||
if (Array.isArray(res.data?.codeEngines) && res.data.codeEngines.length > 0) {
|
||||
codeEngineList.value = res.data.codeEngines;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -785,7 +773,7 @@ function handleChoose(nodeName: string, value: any) {
|
||||
function handleWorkflowNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/workflowNode/getChainParams', {
|
||||
.get('/api/v1/workflow/designer/childWorkflow', {
|
||||
params: {
|
||||
currentId: workflowId.value,
|
||||
workflowId: chooseId,
|
||||
@@ -799,7 +787,7 @@ function handleWorkflowNodeUpdate(chooseId: any) {
|
||||
function handlePluginNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/pluginItem/getTinyFlowData', {
|
||||
.get('/api/v1/workflow/designer/pluginTinyFlow', {
|
||||
params: {
|
||||
id: chooseId,
|
||||
},
|
||||
@@ -848,7 +836,7 @@ function onAsyncExecute(info: any) {
|
||||
:title="$t('menus.ai.plugin')"
|
||||
width="730"
|
||||
ref="pluginSelectRef"
|
||||
page-url="/api/v1/plugin/page?availableOnly=true"
|
||||
page-url="/api/v1/workflow/designer/plugins"
|
||||
:has-parent="true"
|
||||
single-select
|
||||
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
||||
|
||||
@@ -201,7 +201,7 @@ const actions: ActionButton[] = [
|
||||
icon: Share,
|
||||
text: $t('button.share'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
placement: 'menu',
|
||||
disabled: (row: any) =>
|
||||
row.publishStatus !== 'PUBLISHED' || sharingWorkflowId.value === row.id,
|
||||
loading: (row: any) => sharingWorkflowId.value === row.id,
|
||||
|
||||
@@ -49,6 +49,8 @@ const createDefaultEntity = () => ({
|
||||
});
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
@@ -88,7 +90,7 @@ const rules = computed(() => ({
|
||||
}),
|
||||
}));
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
async function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
@@ -96,6 +98,15 @@ function openDialog(row: any, importMode = false) {
|
||||
...row,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/workflowCategory/visibleList');
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
@@ -227,10 +238,14 @@ function closeDialog() {
|
||||
<ElInput v-model.trim="entity.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
|
||||
@@ -76,17 +76,12 @@ function dedupeManagedDatasetOptions(options: ManagedDatasetOption[]) {
|
||||
export async function loadManagedDatasetOptions(): Promise<
|
||||
ManagedDatasetOption[]
|
||||
> {
|
||||
const sourceRes = await api.get('/api/v1/datacenterSource/page', {
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 200,
|
||||
},
|
||||
});
|
||||
const sources = sourceRes.data?.records || [];
|
||||
const sourceRes = await api.get('/api/v1/workflow/designer/dataSources');
|
||||
const sources = sourceRes.data || [];
|
||||
const options: ManagedDatasetOption[] = [];
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const catalogRes = await api.get('/api/v1/datacenterSource/catalogs', {
|
||||
const catalogRes = await api.get('/api/v1/workflow/designer/catalogs', {
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
},
|
||||
@@ -94,7 +89,7 @@ export async function loadManagedDatasetOptions(): Promise<
|
||||
const catalogs = catalogRes.data || [];
|
||||
for (const catalog of catalogs) {
|
||||
const tableRes = await api.get(
|
||||
'/api/v1/datacenterDataset/managedTables',
|
||||
'/api/v1/workflow/designer/managedTables',
|
||||
{
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
@@ -176,7 +171,7 @@ export async function loadManagedDatasetSchema(
|
||||
fields: [],
|
||||
};
|
||||
}
|
||||
const res = await api.get('/api/v1/datacenterDataset/schema', {
|
||||
const res = await api.get('/api/v1/workflow/designer/schema', {
|
||||
params: datasetRef,
|
||||
});
|
||||
const data = res.data || {};
|
||||
@@ -188,8 +183,8 @@ export async function loadManagedDatasetSchema(
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
tableName: data.table?.tableName || datasetRef.tableName,
|
||||
tableDesc: data.table?.tableDesc,
|
||||
tableName: data.tableName || datasetRef.tableName,
|
||||
tableDesc: data.tableDesc,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,18 +28,6 @@ interface ProviderOption {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ModelProvider {
|
||||
key: string;
|
||||
options?: ProviderOptionExtra;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface LlmOption {
|
||||
extra: Map<string, string | undefined>;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SettingsEntity {
|
||||
chatgpt_api_key: string;
|
||||
chatgpt_chatPath: string;
|
||||
@@ -50,18 +38,6 @@ interface SettingsEntity {
|
||||
}
|
||||
|
||||
const providerOptions = ref<ProviderOption[]>(providerList as ProviderOption[]);
|
||||
const brands = ref<ModelProvider[]>([]);
|
||||
const llmOptions = ref<LlmOption[]>([]);
|
||||
|
||||
// 获取品牌接口数据
|
||||
function getBrands() {
|
||||
api.get('/api/v1/modelProvider/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
brands.value = (res.data ?? []) as ModelProvider[];
|
||||
llmOptions.value = formatLlmList(brands.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getOptions() {
|
||||
api
|
||||
.get(
|
||||
@@ -78,7 +54,6 @@ function getOptions() {
|
||||
}
|
||||
onMounted(() => {
|
||||
getOptions();
|
||||
getBrands();
|
||||
});
|
||||
|
||||
const entity = ref<SettingsEntity>({
|
||||
@@ -90,19 +65,6 @@ const entity = ref<SettingsEntity>({
|
||||
chat_publish_base_url: '',
|
||||
});
|
||||
|
||||
function formatLlmList(data: ModelProvider[]): LlmOption[] {
|
||||
return data.map((item) => {
|
||||
const extra = new Map([
|
||||
['chatPath', item.options?.chatPath],
|
||||
['llmEndpoint', item.options?.llmEndpoint],
|
||||
]);
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.key,
|
||||
extra,
|
||||
};
|
||||
});
|
||||
}
|
||||
function handleChangeModel(value: string) {
|
||||
const extra = providerList.find((item) => item.value === value);
|
||||
entity.value.chatgpt_chatPath = extra?.options?.chatPath ?? '';
|
||||
|
||||
@@ -4,7 +4,7 @@ import pageSource from './index.vue?raw';
|
||||
|
||||
describe('管理端工作台智能体数据契约', () => {
|
||||
it('使用 Agent 候选接口和名称字段', () => {
|
||||
expect(pageSource).toContain("'/api/v1/agent/options'");
|
||||
expect(pageSource).toContain("'/api/v1/dashboard/agentOptions'");
|
||||
expect(pageSource).not.toContain('/api/v1/bot/list');
|
||||
expect(pageSource).toContain('label: item.name');
|
||||
expect(pageSource).not.toContain('publishedOnly');
|
||||
|
||||
@@ -332,7 +332,7 @@ async function loadAssistantOptions() {
|
||||
try {
|
||||
const agents =
|
||||
await requestClient.get<Array<{ id?: number | string; name?: string }>>(
|
||||
'/api/v1/agent/options',
|
||||
'/api/v1/dashboard/agentOptions',
|
||||
);
|
||||
const nextOptions: AssistantOptionItem[] = [
|
||||
{ label: '全部智能体', value: '' },
|
||||
|
||||
@@ -114,14 +114,14 @@ const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
||||
{ label: $t('approval.assignee.user'), value: 'USER' },
|
||||
{ label: $t('approval.assignee.dept'), value: 'DEPT' },
|
||||
];
|
||||
const ENABLED_DATA_STATUS = 1;
|
||||
const ASSIGNEE_VISIBLE_TAG_COUNT = 2;
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryLoaded = ref(false);
|
||||
const resourceScopeOptionsLoaded = ref(false);
|
||||
let resourceScopeOptionsRequest: null | Promise<void> = null;
|
||||
const roleLoaded = ref(false);
|
||||
const accountLoading = ref(false);
|
||||
const deptTreeOptions = ref<any[]>([]);
|
||||
@@ -178,7 +178,7 @@ watch(
|
||||
return scope;
|
||||
});
|
||||
if (resourceType) {
|
||||
void ensureCategoryOptions();
|
||||
void ensureResourceScopeOptions();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -212,8 +212,7 @@ async function openDialog(row: any = {}) {
|
||||
formModel.value = buildDefaultForm();
|
||||
dialogVisible.value = true;
|
||||
await Promise.all([
|
||||
ensureCategoryOptions(),
|
||||
ensureDeptOptions(),
|
||||
ensureResourceScopeOptions(),
|
||||
ensureRoleOptions(),
|
||||
]);
|
||||
if (!row?.id) {
|
||||
@@ -276,40 +275,46 @@ async function openDialog(row: any = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryLoaded.value) {
|
||||
async function ensureResourceScopeOptions() {
|
||||
if (resourceScopeOptionsLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const [agentRes, workflowRes, knowledgeRes] = await Promise.all([
|
||||
api.get('/api/v1/agentCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
]);
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
|
||||
BOT: [],
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryLoaded.value = true;
|
||||
}
|
||||
|
||||
async function ensureDeptOptions() {
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
status: ENABLED_DATA_STATUS,
|
||||
},
|
||||
});
|
||||
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
if (!resourceScopeOptionsRequest) {
|
||||
resourceScopeOptionsRequest = api
|
||||
.get('/api/v1/approvalFlow/resourceScopeOptions')
|
||||
.then((res) => {
|
||||
const categories = res.data?.categories || {};
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
|
||||
BOT: [],
|
||||
KNOWLEDGE: normalizeCategoryOptions(
|
||||
categories.KNOWLEDGE,
|
||||
'categoryName',
|
||||
),
|
||||
WORKFLOW: normalizeCategoryOptions(
|
||||
categories.WORKFLOW,
|
||||
'categoryName',
|
||||
),
|
||||
};
|
||||
deptTreeOptions.value = Array.isArray(res.data?.departments)
|
||||
? res.data.departments
|
||||
: [];
|
||||
resourceScopeOptionsLoaded.value = true;
|
||||
})
|
||||
.catch(() => {
|
||||
categoryOptions.value = {
|
||||
AGENT: [],
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
WORKFLOW: [],
|
||||
};
|
||||
deptTreeOptions.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
resourceScopeOptionsRequest = null;
|
||||
});
|
||||
}
|
||||
await resourceScopeOptionsRequest;
|
||||
}
|
||||
|
||||
async function ensureRoleOptions() {
|
||||
|
||||
@@ -5,7 +5,15 @@ import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getCredentialKeyApi } from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
@@ -26,6 +34,10 @@ const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const departmentOptions = ref<any[]>([]);
|
||||
const roleOptions = ref<any[]>([]);
|
||||
const positionOptions = ref<any[]>([]);
|
||||
let formOptionsLoaded = false;
|
||||
function createDefaultEntity() {
|
||||
return {
|
||||
deptId: '',
|
||||
@@ -110,7 +122,7 @@ const rules = ref({
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
@@ -123,6 +135,23 @@ function openDialog(row: any) {
|
||||
entity.value.positionIds = [];
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
if (!formOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysAccount/formOptions');
|
||||
departmentOptions.value = Array.isArray(res.data?.departments)
|
||||
? res.data.departments
|
||||
: [];
|
||||
roleOptions.value = Array.isArray(res.data?.roles) ? res.data.roles : [];
|
||||
positionOptions.value = Array.isArray(res.data?.positions)
|
||||
? res.data.positions
|
||||
: [];
|
||||
formOptionsLoaded = true;
|
||||
} catch {
|
||||
departmentOptions.value = [];
|
||||
roleOptions.value = [];
|
||||
positionOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
@@ -197,7 +226,14 @@ watch(
|
||||
<UploadAvatar v-model="entity.avatar" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="deptId" :label="$t('sysAccount.deptId')">
|
||||
<DictSelect v-model="entity.deptId" dict-code="sysDept" />
|
||||
<ElTreeSelect
|
||||
v-model="entity.deptId"
|
||||
:data="departmentOptions"
|
||||
:props="{ label: 'deptName', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="loginName" :label="$t('sysAccount.loginName')">
|
||||
<ElInput v-model.trim="entity.loginName" />
|
||||
@@ -244,14 +280,24 @@ watch(
|
||||
:label="$t('sysAccount.roleIds')"
|
||||
:required="isAdd"
|
||||
>
|
||||
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
||||
<ElSelect v-model="entity.roleIds" multiple clearable filterable>
|
||||
<ElOption
|
||||
v-for="role in roleOptions"
|
||||
:key="role.id"
|
||||
:label="role.roleName"
|
||||
:value="role.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
||||
<DictSelect
|
||||
multiple
|
||||
v-model="entity.positionIds"
|
||||
dict-code="sysPosition"
|
||||
/>
|
||||
<ElSelect v-model="entity.positionIds" multiple clearable filterable>
|
||||
<ElOption
|
||||
v-for="position in positionOptions"
|
||||
:key="position.id"
|
||||
:label="position.positionName"
|
||||
:value="position.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
|
||||
@@ -5,7 +5,13 @@ import { ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -22,6 +28,8 @@ const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const parentDepartmentOptions = ref<any[]>([]);
|
||||
let parentDepartmentOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
parentId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
@@ -45,13 +53,31 @@ function buildDefaultEntity() {
|
||||
status: ENABLED_STATUS,
|
||||
};
|
||||
}
|
||||
function openDialog(row: any = {}) {
|
||||
async function openDialog(row: any = {}) {
|
||||
isAdd.value = !row.id;
|
||||
entity.value = {
|
||||
...buildDefaultEntity(),
|
||||
...row,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!parentDepartmentOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
},
|
||||
});
|
||||
parentDepartmentOptions.value = [
|
||||
{ id: 0, deptName: $t('sysDept.root'), children: [] },
|
||||
...(Array.isArray(res.data) ? res.data : []),
|
||||
];
|
||||
parentDepartmentOptionsLoaded = true;
|
||||
} catch {
|
||||
parentDepartmentOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (btnLoading.value) {
|
||||
@@ -104,11 +130,14 @@ function closeDialog() {
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem prop="parentId" :label="$t('sysDept.parentId')">
|
||||
<DictSelect
|
||||
<ElTreeSelect
|
||||
:disabled="entity.deptCode === 'root_dept'"
|
||||
:extra-options="[{ label: $t('sysDept.root'), value: 0 }]"
|
||||
v-model="entity.parentId"
|
||||
dict-code="sysDept"
|
||||
:data="parentDepartmentOptions"
|
||||
:props="{ label: 'deptName', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="deptName" :label="$t('sysDept.deptName')">
|
||||
|
||||
@@ -78,7 +78,14 @@ vi.mock('element-plus', () => ({
|
||||
},
|
||||
}),
|
||||
ElInput: defineComponent({ name: 'ElInput', setup: () => () => h('input') }),
|
||||
ElMessage: { success: vi.fn() },
|
||||
ElMessage: { error: vi.fn(), success: vi.fn() },
|
||||
ElOption: defineComponent({ name: 'ElOption', setup: () => () => h('div') }),
|
||||
ElSelect: defineComponent({
|
||||
name: 'ElSelect',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('sys job modal', () => {
|
||||
@@ -112,7 +119,8 @@ describe('sys job modal', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(apiMocks.get).toHaveBeenCalledWith(
|
||||
'/api/v1/workflow/getRunningParameters?id=101',
|
||||
'/api/v1/sysJob/workflowRunningParameters',
|
||||
{ params: { id: '101' } },
|
||||
);
|
||||
expect(wrapper.get('form').attributes('data-loading')).toBe('false');
|
||||
expect(wrapper.text()).toContain('所选工作流已不可用,请重新选择');
|
||||
|
||||
@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElAlert, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElAlert,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CronPicker from '#/components/cron/CronPicker.vue';
|
||||
@@ -39,6 +47,8 @@ const initEntity = {
|
||||
};
|
||||
const entity = ref<any>(initEntity);
|
||||
const btnLoading = ref(false);
|
||||
const workflowOptions = ref<any[]>([]);
|
||||
let workflowOptionsLoaded = false;
|
||||
// 基础验证规则
|
||||
const baseRules = ref({
|
||||
jobName: [
|
||||
@@ -85,8 +95,22 @@ function openDialog(row: any) {
|
||||
} else {
|
||||
entity.value = { ...initEntity };
|
||||
}
|
||||
void ensureWorkflowOptions();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
async function ensureWorkflowOptions() {
|
||||
if (workflowOptionsLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysJob/workflowOptions');
|
||||
workflowOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
workflowOptionsLoaded = true;
|
||||
} catch {
|
||||
workflowOptions.value = [];
|
||||
ElMessage.error('工作流列表加载失败');
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
if (btnLoading.value || workflowParamsSubmissionBlocked.value) {
|
||||
return;
|
||||
@@ -146,7 +170,9 @@ async function getWorkflowParams(v: any) {
|
||||
workflowParams.value = [];
|
||||
workflowParamsLoadError.value = '';
|
||||
try {
|
||||
const res = await api.get(`/api/v1/workflow/getRunningParameters?id=${v}`);
|
||||
const res = await api.get('/api/v1/sysJob/workflowRunningParameters', {
|
||||
params: { id: v },
|
||||
});
|
||||
if (requestId !== workflowParamsRequestId) {
|
||||
return;
|
||||
}
|
||||
@@ -209,11 +235,19 @@ const str = '"param"';
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
]"
|
||||
>
|
||||
<DictSelect
|
||||
<ElSelect
|
||||
v-model="entity.jobParams.workflowId"
|
||||
dict-code="aiWorkFlow"
|
||||
clearable
|
||||
filterable
|
||||
@change="workflowChange"
|
||||
/>
|
||||
>
|
||||
<ElOption
|
||||
v-for="workflow in workflowOptions"
|
||||
:key="workflow.id"
|
||||
:label="workflow.title"
|
||||
:value="workflow.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElAlert
|
||||
v-if="workflowParamsLoadError"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
@@ -44,6 +45,8 @@ const entity = ref<any>({
|
||||
remark: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const parentMenuOptions = ref<any[]>([]);
|
||||
let parentMenuOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
parentId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
@@ -62,12 +65,26 @@ const rules = ref({
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
if (!parentMenuOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysMenu/list', {
|
||||
params: { asTree: true },
|
||||
});
|
||||
parentMenuOptions.value = [
|
||||
{ id: 0, menuTitle: $t('sysMenu.root'), children: [] },
|
||||
...(Array.isArray(res.data) ? res.data : []),
|
||||
];
|
||||
parentMenuOptionsLoaded = true;
|
||||
} catch {
|
||||
parentMenuOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
@@ -121,10 +138,13 @@ function closeDialog() {
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem prop="parentId" :label="$t('sysMenu.parentId')">
|
||||
<DictSelect
|
||||
:extra-options="[{ label: $t('sysMenu.root'), value: 0 }]"
|
||||
<ElTreeSelect
|
||||
v-model="entity.parentId"
|
||||
dict-code="sysMenu"
|
||||
:data="parentMenuOptions"
|
||||
:props="{ label: 'menuTitle', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="menuType" :label="$t('sysMenu.menuType')">
|
||||
|
||||
@@ -19,7 +19,6 @@ defineExpose({
|
||||
|
||||
type ResourceType =
|
||||
| 'AGENT'
|
||||
| 'BOT'
|
||||
| 'KNOWLEDGE'
|
||||
| 'PLUGIN'
|
||||
| 'RESOURCE'
|
||||
@@ -47,7 +46,6 @@ const RESOURCE_SCOPE_GROUPS: Array<{
|
||||
resourceType: ResourceType;
|
||||
}> = [
|
||||
{ resourceType: 'AGENT', label: $t('menus.ai.agents') },
|
||||
{ resourceType: 'BOT', label: $t('bot.chatAssistant') },
|
||||
{ resourceType: 'PLUGIN', label: $t('menus.ai.plugin') },
|
||||
{ resourceType: 'WORKFLOW', label: $t('menus.ai.workflow') },
|
||||
{ resourceType: 'KNOWLEDGE', label: $t('menus.ai.documentCollection') },
|
||||
@@ -58,11 +56,11 @@ const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const categoryScopeLoaded = ref(false);
|
||||
const formOptionsLoaded = ref(false);
|
||||
const menuOptions = ref<any[]>([]);
|
||||
const categoryScopeEditable = ref(false);
|
||||
const categoryOptions = ref<Record<ResourceType, CategoryOption[]>>({
|
||||
AGENT: [],
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
PLUGIN: [],
|
||||
RESOURCE: [],
|
||||
@@ -135,7 +133,7 @@ function openDialog(row: any = {}) {
|
||||
getMenuIds(row.id);
|
||||
}
|
||||
getCategoryScopeDetail(row.id);
|
||||
void ensureCategoryOptions();
|
||||
void ensureFormOptions();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -189,40 +187,32 @@ function getMenuIds(roleId: any) {
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryScopeLoaded.value) {
|
||||
async function ensureFormOptions() {
|
||||
if (formOptionsLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const requests = [
|
||||
api.get('/api/v1/agentCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/botCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/pluginCategory/list'),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/resourceCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
];
|
||||
|
||||
const [agentRes, botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
|
||||
await Promise.all(requests);
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
|
||||
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
PLUGIN: normalizeCategoryOptions(pluginRes.data, 'name'),
|
||||
RESOURCE: normalizeCategoryOptions(resourceRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryScopeLoaded.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysRole/formOptions');
|
||||
const categories = res.data?.categories || {};
|
||||
menuOptions.value = Array.isArray(res.data?.menus) ? res.data.menus : [];
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(categories.KNOWLEDGE, 'categoryName'),
|
||||
PLUGIN: normalizeCategoryOptions(categories.PLUGIN, 'categoryName'),
|
||||
RESOURCE: normalizeCategoryOptions(categories.RESOURCE, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(categories.WORKFLOW, 'categoryName'),
|
||||
};
|
||||
formOptionsLoaded.value = true;
|
||||
} catch {
|
||||
menuOptions.value = [];
|
||||
categoryOptions.value = {
|
||||
AGENT: [],
|
||||
KNOWLEDGE: [],
|
||||
PLUGIN: [],
|
||||
RESOURCE: [],
|
||||
WORKFLOW: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCategoryOptions(data: any[] = [], labelKey: string) {
|
||||
@@ -296,18 +286,24 @@ function getCategoryScopeDetail(roleId: number | string) {
|
||||
categoryScopeEditable.value = !!res.data?.editable;
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
scopes: (res.data?.scopes || buildDefaultScopes()).map((item: any) => ({
|
||||
categoryIds: item.categoryIds || [],
|
||||
resourceType: item.resourceType,
|
||||
scopeMode: item.scopeMode || 'CUSTOM',
|
||||
})),
|
||||
scopes: (res.data?.scopes || buildDefaultScopes())
|
||||
.filter((item: any) =>
|
||||
RESOURCE_SCOPE_GROUPS.some(
|
||||
(group) => group.resourceType === item.resourceType,
|
||||
),
|
||||
)
|
||||
.map((item: any) => ({
|
||||
categoryIds: item.categoryIds || [],
|
||||
resourceType: item.resourceType,
|
||||
scopeMode: item.scopeMode || 'CUSTOM',
|
||||
})),
|
||||
};
|
||||
syncCategoryTreeCheckedKeys(categoryScopeDetail.value.scopes);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCategoryScope(roleId: number | string) {
|
||||
await ensureCategoryOptions();
|
||||
await ensureFormOptions();
|
||||
const scopes = buildScopeItemsFromTree();
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
@@ -353,7 +349,7 @@ async function saveCategoryScope(roleId: number | string) {
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('sysRole.menuPermission')">
|
||||
<Tree
|
||||
data-url="/api/v1/sysMenu/list?asTree=true"
|
||||
:data="menuOptions"
|
||||
v-model="entity.menuIds"
|
||||
:default-props="{
|
||||
label: 'menuTitle',
|
||||
|
||||
Reference in New Issue
Block a user