feat: 完成工作流 Public API 授权闭环
- 新增访问令牌工作流 API 全局授权与 Public Workflow API 权限断言 - 补齐 API Key 执行记录归属、状态查询与下线后不可恢复边界 - 增加管理端接口调用说明与访问令牌授权开关
This commit is contained in:
@@ -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, '"');
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user