fix: 收口管理端页面权限与工作流运行授权

- 页面选项接口改用所属页面权限并返回最小数据视图

- 统一校验工作流引用、租户、状态与定时任务执行主体

- 补充聊天记录权限迁移和权限隔离回归测试
This commit is contained in:
2026-08-07 12:51:21 +08:00
parent 6ad004da9b
commit d244a0404d
55 changed files with 3350 additions and 387 deletions

View File

@@ -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',
);
}

View File

@@ -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');

View File

@@ -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 = [];

View File

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

View File

@@ -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 = '';

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
};
}