feat: 完成工作流 Public API 授权闭环

- 新增访问令牌工作流 API 全局授权与 Public Workflow API 权限断言

- 补齐 API Key 执行记录归属、状态查询与下线后不可恢复边界

- 增加管理端接口调用说明与访问令牌授权开关
This commit is contained in:
2026-05-14 20:41:34 +08:00
parent 47c2bad839
commit da58077d59
15 changed files with 919 additions and 62 deletions

View File

@@ -80,6 +80,15 @@
"publishStatusOffline": "Offline",
"publishStatusDeletePending": "Delete Pending",
"publishStatusLabel": "Release",
"apiInstruction": "API Instructions",
"apiInstructionPublishRequired": "Publish the workflow before viewing API instructions",
"apiEndpoint": "Endpoint",
"apiRequestExample": "Request Example",
"apiResponseExample": "Response Example",
"apiVariables": "Variables",
"apiVariablesEmpty": "This workflow has no start parameters",
"apiStatusExample": "Status Query Example",
"apiResumeExample": "Resume Example",
"submitPublishApprovalConfirm": "Publish the current workflow now?",
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
"submitOfflineApprovalConfirm": "Take the current workflow offline?",

View File

@@ -15,5 +15,6 @@
},
"permissions": "AuthInterface",
"knowledgeSharePermission": "Knowledge Share",
"workflowApiPermission": "Workflow API",
"addApiKeyNotice": "This operation will generate an API key. Please confirm whether to proceed"
}

View File

@@ -80,6 +80,15 @@
"publishStatusOffline": "已下线",
"publishStatusDeletePending": "删除审批中",
"publishStatusLabel": "发布状态",
"apiInstruction": "接口调用说明",
"apiInstructionPublishRequired": "发布后可获取 API 调用说明",
"apiEndpoint": "调用地址",
"apiRequestExample": "请求示例",
"apiResponseExample": "返回示例",
"apiVariables": "入参说明",
"apiVariablesEmpty": "当前工作流没有开始参数",
"apiStatusExample": "状态查询示例",
"apiResumeExample": "恢复执行示例",
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",

View File

@@ -15,5 +15,6 @@
},
"permissions": "授权接口",
"knowledgeSharePermission": "知识库分享授权",
"workflowApiPermission": "工作流 API 调用授权",
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成"
}

View File

@@ -5,11 +5,15 @@ import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
import { computed, h, markRaw, onMounted, ref } from 'vue';
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
import { useAccess } from '@easyflow/access';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { useAppConfig } from '@easyflow/hooks';
import {
Check,
@@ -17,6 +21,7 @@ import {
Delete,
Download,
Edit,
Link,
Lock,
OfficeBuilding,
Plus,
@@ -26,6 +31,7 @@ import {
VideoPlay,
} from '@element-plus/icons-vue';
import {
ElDialog,
ElForm,
ElFormItem,
ElIcon,
@@ -49,10 +55,7 @@ import { $t } from '#/locales';
import { router } from '#/router';
import { useDictStore } from '#/store';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
import {
buildOfflineImpactMessage,
type OfflineImpactCheck,
} from '#/views/ai/shared/offline-impact';
import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
import {
canAiResourceDelete,
canAiResourceOffline,
@@ -64,6 +67,8 @@ import {
import WorkflowModal from './WorkflowModal.vue';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
interface FieldDefinition {
// 字段名称
prop: string;
@@ -79,6 +84,15 @@ interface FieldDefinition {
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
interface ApiFieldDoc {
key: string;
label: string;
type: string;
required: boolean;
description: string;
placeholder: string;
}
const primaryAction: CardPrimaryAction = {
icon: DesignIcon,
text: $t('button.design'),
@@ -94,6 +108,8 @@ const canManageWorkflow = computed(() =>
);
const updatingScopeId = ref<null | number | string>(null);
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
const apiInstructionVisible = ref(false);
const apiInstructionRow = ref<any>(null);
const visibilityScopeMeta = computed(() => ({
PRIVATE: {
label: $t('aiWorkflow.visibilityScopePrivate'),
@@ -158,6 +174,14 @@ const actions: ActionButton[] = [
});
},
},
{
icon: Link,
text: $t('aiWorkflow.apiInstruction'),
placement: 'menu',
onClick: (row: any) => {
showApiInstruction(row);
},
},
{
icon: Download,
text: $t('button.export'),
@@ -199,7 +223,8 @@ const actions: ActionButton[] = [
text: $t('button.offline'),
permission: '/api/v1/workflow/save',
placement: 'menu',
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
visible: (row: any) =>
canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitOfflineAction(row);
},
@@ -210,7 +235,8 @@ const actions: ActionButton[] = [
tone: 'danger',
permission: '/api/v1/workflow/remove',
placement: 'menu',
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
visible: (row: any) =>
canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitDeleteApproval(row);
},
@@ -306,6 +332,321 @@ function resolveNavTitle(row: any) {
function isRepublishAction(row: any) {
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
}
function isWorkflowPublished(row: any) {
return (
resolveAiResourceDisplayStatus(
row.displayPublishStatus,
row.publishStatus,
) === 'PUBLISHED'
);
}
function showApiInstruction(row: any) {
if (!isWorkflowPublished(row) || !getPublishedWorkflowContent(row)) {
ElMessage.warning($t('aiWorkflow.apiInstructionPublishRequired'));
return;
}
apiInstructionRow.value = row;
apiInstructionVisible.value = true;
}
function getPublishedWorkflowContent(row: any) {
const snapshot = row?.publishedSnapshotJson;
if (snapshot && typeof snapshot === 'object' && snapshot.content) {
return snapshot.content;
}
return null;
}
function parseWorkflowContent(row: any) {
const content = getPublishedWorkflowContent(row);
if (!content) {
return null;
}
if (typeof content === 'object') {
return content;
}
try {
return JSON.parse(String(content));
} catch {
return null;
}
}
function normalizeApiOrigin(value?: string) {
const rawValue = String(value || '').trim();
if (!rawValue) {
return window.location.origin;
}
if (/^https?:\/\//i.test(rawValue)) {
return rawValue.replace(/\/+$/, '');
}
const normalizedPath = rawValue.startsWith('/') ? rawValue : `/${rawValue}`;
return `${window.location.origin}${normalizedPath}`.replace(/\/+$/, '');
}
function resolveApiBaseUrl() {
return `${normalizeApiOrigin(apiURL)}/public-api/workflow`;
}
function resolveStartNodeData(row: any) {
const workflow = parseWorkflowContent(row);
const nodes = Array.isArray(workflow?.nodes) ? workflow.nodes : [];
const startNode = nodes.find((node: any) => node?.type === 'startNode');
return startNode?.data || {};
}
function resolveApiFields(row: any): ApiFieldDoc[] {
const startNodeData = resolveStartNodeData(row);
const schema = Array.isArray(startNodeData.startFormSchema)
? startNodeData.startFormSchema
: [];
const parameters = Array.isArray(startNodeData.parameters)
? startNodeData.parameters
: [];
const source = schema.length > 0 ? schema : parameters;
return source
.map((item: any) => {
const key = String(item?.key || item?.name || '').trim();
if (!key) {
return null;
}
return {
key,
label: String(item?.label || item?.title || item?.name || key),
type: String(item?.type || item?.contentType || 'text'),
required: Boolean(item?.required),
description: String(item?.description || item?.formDescription || ''),
placeholder: String(item?.placeholder || item?.formPlaceholder || ''),
};
})
.filter(Boolean) as ApiFieldDoc[];
}
function buildExampleVariables(row: any) {
const variables: Record<string, any> = {};
for (const field of resolveApiFields(row)) {
if (field.type === 'file') {
variables[field.key] = [
{
fileName: 'example.pdf',
filePath: 'https://example.com/example.pdf',
},
];
continue;
}
if (field.type === 'checkbox') {
variables[field.key] = [];
continue;
}
variables[field.key] = field.placeholder || field.label;
}
return variables;
}
function buildRunRequestExample(row: any) {
return JSON.stringify(
{
id: row?.id,
variables: buildExampleVariables(row),
},
null,
2,
);
}
function buildRunResponseExample() {
return JSON.stringify(
{
errorCode: 0,
message: '成功',
data: '执行ID',
},
null,
2,
);
}
function buildResumeRequestExample() {
return JSON.stringify(
{
executeId: '执行ID',
confirmParams: {
confirm: true,
},
},
null,
2,
);
}
async function copyApiContent(content: string) {
try {
await navigator.clipboard.writeText(content);
ElMessage.success($t('message.copySuccess'));
} catch {
ElMessage.error($t('message.copyFail'));
}
}
function handleApiDocClick(e: MouseEvent) {
const target = (e.target as HTMLElement).closest('.api-url-copy-btn');
if (!target) return;
const url = (target as HTMLElement).dataset.copy;
if (url) copyApiContent(url);
}
function apiUrlLine(method: string, url: string) {
const escaped = url.replace(/"/g, '&quot;');
return `\`${method}\` \`${url}\` <button class="api-url-copy-btn" data-copy="${escaped}" title="复制">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>`;
}
const apiDocMarkdown = computed(() => {
const row = apiInstructionRow.value;
if (!row) return '';
const baseUrl = resolveApiBaseUrl();
const fields = resolveApiFields(row);
const lines: string[] = [];
// ---- 概述 ----
lines.push(`## 概述`);
lines.push(``);
lines.push(`通过 Public API 可异步执行已发布的工作流并获取执行结果。`);
lines.push(``);
lines.push(`**调用流程**:发起执行 → 轮询查询执行结果 → (若有确认节点)恢复执行`);
lines.push(``);
// ---- 鉴权 ----
lines.push(`## 鉴权`);
lines.push(``);
lines.push(`所有请求需在 Header 中携带访问令牌,访问令牌需开启 **工作流 API 调用授权**。`);
lines.push(``);
lines.push('```');
lines.push(`ApiKey: <your-api-key>`);
lines.push('```');
lines.push(``);
lines.push(`---`);
lines.push(``);
// ---- 1. 发起执行 ----
lines.push(`## 1. 发起执行`);
lines.push(``);
lines.push(apiUrlLine('POST', `${baseUrl}/runAsync`));
lines.push(``);
lines.push(`异步执行工作流,立即返回执行 ID。工作流必须已发布且存在发布快照。`);
lines.push(``);
lines.push(`### 请求体`);
lines.push(``);
lines.push('```json');
lines.push(buildRunRequestExample(row));
lines.push('```');
lines.push(``);
// 入参说明
lines.push(`### 入参说明`);
lines.push(``);
lines.push(`| 参数 | 类型 | 必填 | 说明 |`);
lines.push(`| --- | --- | --- | --- |`);
lines.push(`| id | string | 是 | 工作流 ID |`);
lines.push(`| variables | object | 否 | 运行参数,字段说明见下方 |`);
lines.push(``);
if (fields.length > 0) {
lines.push(`#### variables 字段明细`);
lines.push(``);
lines.push(`以下字段基于当前发布快照的开始节点配置生成。`);
lines.push(``);
lines.push(`| 参数 | 类型 | 必填 | 说明 |`);
lines.push(`| --- | --- | --- | --- |`);
for (const f of fields) {
const desc = [f.label, f.description].filter(Boolean).join(' · ');
lines.push(`| ${f.key} | ${f.type} | ${f.required ? '是' : '否'} | ${desc} |`);
}
lines.push(``);
}
lines.push(`### 响应`);
lines.push(``);
lines.push('```json');
lines.push(buildRunResponseExample());
lines.push('```');
lines.push(``);
lines.push(`\`data\` 为 **执行 ID**executeId后续查询和恢复均需使用此 ID。`);
lines.push(``);
lines.push(`---`);
lines.push(``);
// ---- 2. 查询执行结果 ----
lines.push(`## 2. 查询执行结果`);
lines.push(``);
lines.push(apiUrlLine('POST', `${baseUrl}/getChainStatus`));
lines.push(``);
lines.push(`查询工作流执行的整体状态与各节点执行详情。工作流为异步执行,建议**轮询**该接口直到状态为终态。`);
lines.push(``);
lines.push(`### 请求体`);
lines.push(``);
lines.push('```json');
lines.push(JSON.stringify({ executeId: '<runAsync 返回的执行 ID>' }, null, 2));
lines.push('```');
lines.push(``);
lines.push(`> \`nodes\` 参数可选。不传则只返回工作流整体状态;传入节点 ID 数组可额外获取对应节点的执行详情。`);
lines.push(``);
lines.push(`### 响应示例`);
lines.push(``);
lines.push('```json');
lines.push(JSON.stringify({
errorCode: 0,
message: '成功',
data: {
executeId: 'abc5358c-a310-4caa-97ec-455062b2235e',
status: 'FINISHED',
message: null,
result: { output: '工作流执行结果' },
nodes: {},
},
}, null, 2));
lines.push('```');
lines.push(``);
lines.push(`### 状态值说明`);
lines.push(``);
lines.push(`| 状态 | 说明 |`);
lines.push(`| --- | --- |`);
lines.push(`| READY | 就绪,尚未开始 |`);
lines.push(`| RUNNING | 执行中 |`);
lines.push(`| SUSPEND | 挂起,等待确认节点恢复 |`);
lines.push(`| FINISHED | 执行完成 |`);
lines.push(`| FAILED | 执行失败 |`);
lines.push(`| ERROR | 执行异常 |`);
lines.push(``);
// ---- 3. 恢复执行 ----
lines.push(`---`);
lines.push(``);
lines.push(`## 3. 恢复执行(确认节点)`);
lines.push(``);
lines.push(apiUrlLine('POST', `${baseUrl}/resume`));
lines.push(``);
lines.push(`当工作流包含**确认节点**时,执行到该节点后状态变为 \`SUSPEND\`,需要调用此接口传入确认参数后恢复执行。若工作流不包含确认节点则无需调用。`);
lines.push(``);
lines.push(`### 请求体`);
lines.push(``);
lines.push('```json');
lines.push(buildResumeRequestExample());
lines.push('```');
lines.push(``);
lines.push(`### 响应`);
lines.push(``);
lines.push('```json');
lines.push(JSON.stringify({ errorCode: 0, message: '成功', data: null }, null, 2));
lines.push('```');
lines.push(``);
lines.push(`恢复后可继续轮询 \`getChainStatus\` 获取后续执行状态。`);
lines.push(``);
// ---- 错误码 ----
lines.push(`---`);
lines.push(``);
lines.push(`## 错误处理`);
lines.push(``);
lines.push(`当请求失败时,\`errorCode\` 不为 0\`message\` 包含错误原因。常见错误:`);
lines.push(``);
lines.push(`| 场景 | 说明 |`);
lines.push(`| --- | --- |`);
lines.push(`| ApiKey 无效或过期 | 检查访问令牌状态与有效期 |`);
lines.push(`| 未授权工作流 API 调用 | 在访问令牌中开启「工作流 API 调用授权」 |`);
lines.push(`| 工作流尚未发布 | 仅已发布且存在发布快照的工作流可通过 API 调用 |`);
return lines.join('\n');
});
async function submitPublishAction(row: any) {
if (
@@ -329,12 +670,9 @@ async function submitPublishAction(row: any) {
} catch {
return;
}
const res = await api.post(
'/api/v1/workflow/submitPublishApproval',
{
id: row.id,
},
);
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
id: row.id,
});
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
pageDataRef.value?.reload?.();
@@ -350,17 +688,15 @@ async function submitOfflineAction(row: any) {
const impactRes = await api.get<{
data: OfflineImpactCheck;
errorCode: number;
}>(
'/api/v1/workflow/offlineImpactCheck',
{
params: { id: row.id },
},
);
}>('/api/v1/workflow/offlineImpactCheck', {
params: { id: row.id },
});
if (impactRes.errorCode !== 0) {
return;
}
try {
const sections = [];
let offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundBotsFooter');
if (impactRes.data?.hasBotBindings) {
sections.push(
buildOfflineImpactMessage(
@@ -383,32 +719,23 @@ async function submitOfflineAction(row: any) {
),
);
}
if (impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings) {
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundMixedFooter');
} else if (impactRes.data?.hasPluginBindings) {
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundPluginsFooter');
}
const impactMessage =
sections.length > 0
? h('div', [
...sections,
h(
'p',
{
style: 'margin-top: 12px;',
},
impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings
? $t('aiWorkflow.offlineImpactBoundMixedFooter')
: impactRes.data?.hasPluginBindings
? $t('aiWorkflow.offlineImpactBoundPluginsFooter')
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
),
h('p', { style: 'margin-top: 12px;' }, offlineImpactFooter),
])
: $t('aiWorkflow.submitOfflineApprovalConfirm');
await ElMessageBox.confirm(
impactMessage,
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
await ElMessageBox.confirm(impactMessage, $t('message.noticeTitle'), {
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
});
} catch {
return;
}
@@ -459,18 +786,18 @@ function resolvePublishStatusMetaByInstance(
tone: 'danger',
};
}
case 'OFFLINE_PENDING': {
return {
label: $t('aiWorkflow.publishStatusOfflinePending'),
tone: 'pending',
};
}
case 'OFFLINE': {
return {
label: $t('aiWorkflow.publishStatusOffline'),
tone: 'draft',
};
}
case 'OFFLINE_PENDING': {
return {
label: $t('aiWorkflow.publishStatusOfflinePending'),
tone: 'pending',
};
}
case 'PUBLISH_PENDING': {
return {
label: $t('aiWorkflow.publishStatusPublishPending'),
@@ -668,6 +995,25 @@ function handleHeaderButtonClick(data: any) {
<template>
<div class="flex h-full flex-col gap-6 p-6">
<WorkflowModal ref="saveDialog" @reload="reset" />
<ElDialog
v-model="apiInstructionVisible"
:title="$t('aiWorkflow.apiInstruction')"
width="780px"
class="workflow-api-dialog"
:footer="false"
>
<div
v-if="apiInstructionRow"
class="workflow-api-markdown-wrap"
@click="handleApiDocClick"
>
<ElXMarkdown
:markdown="apiDocMarkdown"
:allow-html="true"
:sanitize="false"
/>
</div>
</ElDialog>
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@@ -705,7 +1051,10 @@ function handleHeaderButtonClick(data: any) {
>
<span class="workflow-publish-chip__dot"></span>
<span>{{
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
resolvePublishStatusMetaByInstance(
item.displayPublishStatus,
item.publishStatus,
).label
}}</span>
</div>
</template>
@@ -1102,4 +1451,153 @@ button.workflow-scope-chip:disabled {
border-radius: 16px;
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 20%);
}
.workflow-api-markdown-wrap {
max-height: 65vh;
padding: 0 4px;
overflow-y: auto;
font-size: 14px;
line-height: 1.7;
color: hsl(var(--foreground));
}
.workflow-api-markdown-wrap::-webkit-scrollbar {
width: 6px;
}
.workflow-api-markdown-wrap::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground) / 20%);
border-radius: 3px;
}
.workflow-api-markdown-wrap :deep(h2) {
padding-bottom: 8px;
margin-top: 28px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 700;
color: hsl(var(--foreground));
border-bottom: 1px solid hsl(var(--border));
}
.workflow-api-markdown-wrap :deep(h2:first-child) {
margin-top: 0;
}
.workflow-api-markdown-wrap :deep(h3) {
margin-top: 20px;
margin-bottom: 10px;
font-size: 15px;
font-weight: 600;
color: hsl(var(--foreground));
}
.workflow-api-markdown-wrap :deep(h4) {
margin-top: 16px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: hsl(var(--foreground));
}
.workflow-api-markdown-wrap :deep(hr) {
margin: 24px 0;
border: none;
border-top: 1px solid hsl(var(--border));
}
.workflow-api-markdown-wrap :deep(p) {
margin: 8px 0;
}
.workflow-api-markdown-wrap :deep(code) {
padding: 2px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
color: hsl(var(--foreground));
background: hsl(var(--muted) / 50%);
border-radius: 4px;
}
.workflow-api-markdown-wrap :deep(pre) {
max-height: 280px;
padding: 14px 16px;
margin: 10px 0;
overflow: auto;
background: hsl(220 14% 96%);
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
.workflow-api-markdown-wrap :deep(pre code) {
padding: 0;
font-size: 12.5px;
color: hsl(var(--foreground));
background: transparent;
border-radius: 0;
}
.workflow-api-markdown-wrap :deep(table) {
width: 100%;
margin: 10px 0;
border-collapse: collapse;
}
.workflow-api-markdown-wrap :deep(th),
.workflow-api-markdown-wrap :deep(td) {
padding: 8px 12px;
font-size: 13px;
text-align: left;
border: 1px solid hsl(var(--border));
}
.workflow-api-markdown-wrap :deep(th) {
font-weight: 600;
color: hsl(var(--foreground));
background: hsl(var(--muted) / 40%);
}
.workflow-api-markdown-wrap :deep(td) {
color: hsl(var(--foreground) / 85%);
}
.workflow-api-markdown-wrap :deep(blockquote) {
padding: 8px 16px;
margin: 10px 0;
color: hsl(var(--muted-foreground));
border-left: 3px solid hsl(var(--primary) / 40%);
}
.workflow-api-markdown-wrap :deep(strong) {
font-weight: 600;
color: hsl(var(--foreground));
}
.workflow-api-markdown-wrap :deep(.api-url-copy-btn) {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
margin-left: 6px;
color: hsl(var(--muted-foreground));
vertical-align: middle;
cursor: pointer;
background: transparent;
border: 1px solid hsl(var(--border));
border-radius: 6px;
transition: all 0.15s;
}
.workflow-api-markdown-wrap :deep(.api-url-copy-btn:hover) {
color: hsl(var(--primary));
background: hsl(var(--muted) / 50%);
border-color: hsl(var(--primary) / 40%);
}
:global(.workflow-api-dialog .el-dialog__body) {
padding: 0 20px 20px;
overflow: hidden;
}
</style>

View File

@@ -33,6 +33,7 @@ interface Entity {
expiredAt: Date | null | string;
permissionIds: (number | string)[]; // 绑定值:权限 ID 数组
knowledgeShareEnabled: boolean;
workflowApiEnabled: boolean;
id?: number; // 编辑时的主键
}
@@ -51,6 +52,7 @@ const entity = ref<Entity>({
expiredAt: null,
permissionIds: [],
knowledgeShareEnabled: false,
workflowApiEnabled: false,
});
// 加载状态
const btnLoading = ref(false);
@@ -120,6 +122,7 @@ function getResourcePermissionList() {
function createDefaultEntity(row: Partial<Entity> = {}): Entity {
const permissionIds = row.permissionIds || [];
const knowledgeShareEnabled = Boolean(row.knowledgeShareEnabled);
const workflowApiEnabled = Boolean(row.workflowApiEnabled);
return {
apiKey: '',
status: '',
@@ -128,6 +131,7 @@ function createDefaultEntity(row: Partial<Entity> = {}): Entity {
...row,
permissionIds,
knowledgeShareEnabled,
workflowApiEnabled,
};
}
@@ -183,6 +187,7 @@ function closeDialog() {
expiredAt: null,
permissionIds: [],
knowledgeShareEnabled: false,
workflowApiEnabled: false,
};
isAdd.value = true;
dialogVisible.value = false;
@@ -261,6 +266,12 @@ defineExpose({
>
{{ $t('sysApiKey.knowledgeSharePermission') }}
</ElCheckbox>
<ElCheckbox
v-model="entity.workflowApiEnabled"
class="permission-checkbox"
>
{{ $t('sysApiKey.workflowApiPermission') }}
</ElCheckbox>
</div>
</ElFormItem>
</ElForm>