feat(Workflow): 新增工作流节点组件
This commit is contained in:
parent
43e29a267e
commit
94952fb924
23
app.vue
23
app.vue
@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
<!-- 应用引导-款式融合-->
|
<!-- 应用引导-款式融合-->
|
||||||
<StyleFusionGuide/>
|
<StyleFusionGuide/>
|
||||||
|
|
||||||
|
<!-- 工作流节点-Markdown转Html-->
|
||||||
|
<Markdown2Html v-model:data="data"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -39,4 +42,24 @@ provide<GlobalInjectWorkflowAppStatus>(GlobalInjectKeyConst.AllWorkFlowApps, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
_meta: {
|
||||||
|
title: "markdown转html"
|
||||||
|
},
|
||||||
|
inputs: {},
|
||||||
|
data: {
|
||||||
|
method: 'POST', // 请求方法
|
||||||
|
url: 'http://localhost:3200/plugins/api/markdown2html', // 请求地址,节点的业务逻辑,需要在接口中完成
|
||||||
|
body: {
|
||||||
|
markdown: '1' // 接口请求体内容,输入的markdown内容
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
// 依据接口自行扩展,认证等信息
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
74
components/Markdown2Html/index.vue
Normal file
74
components/Markdown2Html/index.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { IExecutionNodeStatusItem } from "~/composables/worklfow/node/node.interface";
|
||||||
|
|
||||||
|
const data = defineModel<INodeData<NodeTypeEnum.Markdown2Html>>("data");
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nodeResult: IExecutionNodeStatusItem
|
||||||
|
}>(); // 节点执行产出结果
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本内容
|
||||||
|
*/
|
||||||
|
const textContent = computed(() => {
|
||||||
|
return props.nodeResult?.result?.output_content?.filter(
|
||||||
|
item => item.type === "text"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initInputs = () => {
|
||||||
|
if(!data.value?.inputs){
|
||||||
|
Object.assign(data.value.inputs, { markdown: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initInputs();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="w-full p-2 min-w-96"
|
||||||
|
@keydown.stop
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<!-- 需要输入的变量 -->
|
||||||
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="data.data.body"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<a-form-item
|
||||||
|
label="markdown内容"
|
||||||
|
name="markdown"
|
||||||
|
:rules="[{ required: true, message: `请输入内容` }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="data.data.body.markdown"
|
||||||
|
placeholder='请输入markdown'
|
||||||
|
class="border"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<div
|
||||||
|
v-if="data.inputs.markdown"
|
||||||
|
class="absolute inset-0 z-10 flex items-center justify-center bg-gray-500/40 rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-500/60 text-white text-xs rounded px-2 py-0.5 shadow"
|
||||||
|
>
|
||||||
|
已关联节点 #{{ data.inputs.markdown[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<div v-show="textContent?.length">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="转换结果">
|
||||||
|
<a-textarea v-for="(output, index) in textContent || []" :value="output.content" readonly class="border"/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
52
composables/worklfow/node/Markdown2HtmlNode.ts
Normal file
52
composables/worklfow/node/Markdown2HtmlNode.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { NodeCategoryEnum, PluginBaseNode } from "./PluginBaseNode";
|
||||||
|
import {
|
||||||
|
Markdown2Html
|
||||||
|
} from '#components'
|
||||||
|
|
||||||
|
import { NodeTypeEnum } from "~/composables/worklfow/node/node.interface";
|
||||||
|
|
||||||
|
export default class Markdown2HtmlNode extends PluginBaseNode {
|
||||||
|
static override nodeType = NodeTypeEnum.Markdown2Html // 节点类型
|
||||||
|
|
||||||
|
static override getNodeList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: Markdown2HtmlNode.nodeType, // 节点类型
|
||||||
|
label: 'Markdown转HTML', // 标签
|
||||||
|
description: 'Markdown转HTML', // 描述
|
||||||
|
category: NodeCategoryEnum.BASE, // 分类
|
||||||
|
icon: 'material-symbols-light:markdown-paste' // 图标
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
initData() {
|
||||||
|
// 数据类型 IApiPluginNodeData
|
||||||
|
return {
|
||||||
|
method: 'POST', // 请求方法
|
||||||
|
url: 'http://localhost:3200/plugins/api/markdown2html', // 请求地址,节点的业务逻辑,需要在接口中完成
|
||||||
|
body: {
|
||||||
|
markdown: '' // 接口请求体内容,输入的markdown内容
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
// 依据接口自行扩展,认证等信息
|
||||||
|
}
|
||||||
|
} // 初始化数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入信息关联父节点产出的数据类型及数据路径
|
||||||
|
*/
|
||||||
|
override createOutputSpec(): INodeOutputSpec {
|
||||||
|
return {
|
||||||
|
type: 'text', // 文本类型
|
||||||
|
defaultPath: ['output_content', '*', 'content'] // 数据路径,文本为 ['output_content', '*', 'content'],媒体类型为 ['output_content', '*', 'url']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画布节点 UI
|
||||||
|
static override renderNode() {
|
||||||
|
return Markdown2Html // 节点组件
|
||||||
|
}
|
||||||
|
}
|
||||||
79
composables/worklfow/node/PluginBaseNode.ts
Normal file
79
composables/worklfow/node/PluginBaseNode.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { type INodeData, type INodeOutputSpec, NodeTypeEnum } from "~/composables/worklfow/node/node.interface";
|
||||||
|
|
||||||
|
export interface NodeOptions {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeCategoryEnum = {
|
||||||
|
BASE: '基础',
|
||||||
|
FUNCTIONAL: '功能',
|
||||||
|
APP: '应用',
|
||||||
|
Agent: '智能体'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NodeCategoryEnum =
|
||||||
|
(typeof NodeCategoryEnum)[keyof typeof NodeCategoryEnum]
|
||||||
|
|
||||||
|
export interface NodeViewData {
|
||||||
|
type: NodeTypeEnum
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
category: NodeCategoryEnum
|
||||||
|
id?: string // appId / agentId // component
|
||||||
|
previewUrl?: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class PluginBaseNode {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
data: Record<string, unknown>
|
||||||
|
|
||||||
|
static nodeType = ''
|
||||||
|
static getNodeList(): NodeViewData[] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: NodeOptions) {
|
||||||
|
this.id = options.id
|
||||||
|
this.title = options.title
|
||||||
|
this.data = this.initData(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化节点数据 */
|
||||||
|
abstract initData(options?: NodeOptions): Record<string, unknown>
|
||||||
|
|
||||||
|
create(title: string, id?: string): INodeData {
|
||||||
|
return {
|
||||||
|
class_type: this.getNodeType() as NodeTypeEnum,
|
||||||
|
plugin_type: NodeTypeEnum.ApiPlugin, // 远程插件节点必填项
|
||||||
|
_meta: {
|
||||||
|
title
|
||||||
|
},
|
||||||
|
inputs: {},
|
||||||
|
data: this.data,
|
||||||
|
outputSpec: this.createOutputSpec(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 节点类型 */
|
||||||
|
getNodeType() {
|
||||||
|
return (this.constructor as typeof PluginBaseNode).nodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输出规格(可选) */
|
||||||
|
createOutputSpec(_id?: string): undefined | INodeOutputSpec {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 节点本体 UI */
|
||||||
|
static renderNode(): Component {
|
||||||
|
throw new Error('Not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 节点属性面板 UI(可选) */
|
||||||
|
static renderProperties(): Component {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
288
composables/worklfow/node/node.interface.ts
Normal file
288
composables/worklfow/node/node.interface.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* 节点类型枚举
|
||||||
|
*/
|
||||||
|
export enum NodeTypeEnum {
|
||||||
|
API = 'API',
|
||||||
|
POLL_API = 'POLL_API',
|
||||||
|
OUTPUT = 'OUTPUT',
|
||||||
|
INPUT = 'INPUT',
|
||||||
|
URL2FILE = 'URL2FILE',
|
||||||
|
OpenAIImage = 'OpenAIImage',
|
||||||
|
DrawApp = 'DrawApp',
|
||||||
|
Agent = 'Agent',
|
||||||
|
Preview = 'Preview',
|
||||||
|
ComponentInput = 'ComponentInput',
|
||||||
|
SAVETOLOCALFILE = 'SaveLocalFile',
|
||||||
|
ApiPlugin='ApiPlugin',
|
||||||
|
Markdown2Html='Markdown2Html'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点元数据
|
||||||
|
*/
|
||||||
|
export interface INodeMeta {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点数据接口
|
||||||
|
*/
|
||||||
|
export interface INodeData<T extends NodeTypeEnum = NodeTypeEnum> {
|
||||||
|
class_type: T
|
||||||
|
plugin_type?: NodeTypeEnum.ApiPlugin // 插件节点,必填
|
||||||
|
_meta: INodeMeta
|
||||||
|
inputs?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
output?: any
|
||||||
|
data?: NodeDataMap[T]
|
||||||
|
outputSpec?: INodeOutputSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeOutputSpec {
|
||||||
|
type: FileType
|
||||||
|
defaultPath: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点数据映射类型
|
||||||
|
* @type {NodeDataMap}
|
||||||
|
* @property {IAPINodeData} API - API 节点数据
|
||||||
|
* @property {IPollAPINodeData} POLL_API - 轮询 API 节点数据
|
||||||
|
* @property {IInputNodeData} INPUT - 输入节点数据
|
||||||
|
* @property {IOutputNodeData} OUTPUT - 输出节点数据
|
||||||
|
* @property {IUrl2FileNodeData} URL2FILE - URL转文件节点数据
|
||||||
|
* @property {IOpenAIImageNodeData} OpenAIImage - 云平台模型接口
|
||||||
|
* @property {IDrawAppNodeData} DrawApp - 绘图应用
|
||||||
|
* @property {IAgentNodeData} Agent - 智能体
|
||||||
|
* @property {IPreviewNodeData} Preview - 预览节点
|
||||||
|
*/
|
||||||
|
export type NodeDataMap = {
|
||||||
|
[NodeTypeEnum.API]: IAPINodeData
|
||||||
|
[NodeTypeEnum.POLL_API]: IPollAPINodeData
|
||||||
|
[NodeTypeEnum.INPUT]: IInputNodeData
|
||||||
|
[NodeTypeEnum.OUTPUT]: IOutputNodeData
|
||||||
|
[NodeTypeEnum.URL2FILE]: IUrl2FileNodeData
|
||||||
|
[NodeTypeEnum.OpenAIImage]: IOpenAIImageNodeData
|
||||||
|
[NodeTypeEnum.DrawApp]: IDrawAppNodeData
|
||||||
|
[NodeTypeEnum.Agent]: IAgentNodeData
|
||||||
|
[NodeTypeEnum.Preview]: IPreviewNodeData
|
||||||
|
[NodeTypeEnum.ComponentInput]: IComponentInputData
|
||||||
|
[NodeTypeEnum.ApiPlugin]: IApiPluginNodeData;
|
||||||
|
[NodeTypeEnum.Markdown2Html]: IApiPluginNodeData
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 节点数据类型
|
||||||
|
* @interface IAPINodeData
|
||||||
|
* @param userId - 用户 ID
|
||||||
|
* @param successCondition - 成功条件
|
||||||
|
* @param pollTime - 轮询时间
|
||||||
|
* @param pollTimeout - 轮询超时时间
|
||||||
|
* @param response - 响应数据
|
||||||
|
*/
|
||||||
|
export interface IAPINodeData {
|
||||||
|
headers?: { [key: string]: string }
|
||||||
|
params?: { [key: string]: string }
|
||||||
|
pathParams?: { [key: string]: string }
|
||||||
|
body?: { [key: string]: any }
|
||||||
|
successCondition?: { key: string[], value: string | number | boolean }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询 API 节点数据类型
|
||||||
|
* @interface IPollAPINodeData
|
||||||
|
* @param headers - 请求头
|
||||||
|
* @param body - 请求体
|
||||||
|
* @param params - 请求参数
|
||||||
|
* @param pathParams - 请求路径参数
|
||||||
|
* @param successCondition - 成功条件
|
||||||
|
* @param pollTime - 轮询时间
|
||||||
|
* @param pollTimeout - 轮询超时时间
|
||||||
|
*/
|
||||||
|
export interface IPollAPINodeData {
|
||||||
|
headers?: { [key: string]: string }
|
||||||
|
body?: { [key: string]: any }
|
||||||
|
params?: { [key: string]: string }
|
||||||
|
pathParams?: { [key: string]: string }
|
||||||
|
successCondition?: { key: string[], value: string | number | boolean }[]
|
||||||
|
pollTime?: number
|
||||||
|
pollTimeout?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入字段类型枚举
|
||||||
|
* @enum {string} InputFieldTypeEnum
|
||||||
|
* @property {string} boolean - 布尔类型
|
||||||
|
* @property {string} string - 字符串类型
|
||||||
|
* @property {string} number - 数字类型
|
||||||
|
*/
|
||||||
|
export enum InputFieldTypeEnum {
|
||||||
|
boolean = 'boolean',
|
||||||
|
string = 'string',
|
||||||
|
number = 'number'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入节点数据类型
|
||||||
|
* @interface IInputNodeData
|
||||||
|
* @param schema - 输入数据结构
|
||||||
|
*/
|
||||||
|
export interface IInputNodeData {
|
||||||
|
schema: IInputNodeDataSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInputNodeDataSchema {
|
||||||
|
[key: string]: {
|
||||||
|
type: InputFieldTypeEnum
|
||||||
|
default?: any
|
||||||
|
required?: boolean
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOutputNodeDataSchema {
|
||||||
|
[key: string]: {
|
||||||
|
outputType?: OutputType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出节点数据类型
|
||||||
|
* @interface IOutputNodeData
|
||||||
|
* @param schema - 输出数据
|
||||||
|
*/
|
||||||
|
export interface IOutputNodeData {
|
||||||
|
schema: IOutputNodeDataSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出节点数据类型
|
||||||
|
* @interface IUrl2FileNodeData
|
||||||
|
* @param output - 输出数据
|
||||||
|
*/
|
||||||
|
export interface IUrl2FileNodeData {
|
||||||
|
[key: string]: File
|
||||||
|
}
|
||||||
|
|
||||||
|
export type openAIImageNodeType =
|
||||||
|
| 'editImage'
|
||||||
|
| 'generateImage'
|
||||||
|
| 'generateVideo'
|
||||||
|
| 'multiViewGenerate3D'
|
||||||
|
| 'imageGenerate3D'
|
||||||
|
| 'textGenerate3D'
|
||||||
|
|
||||||
|
export interface IOpenAIImageNodeData extends INodeDataCondition {
|
||||||
|
type: openAIImageNodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDrawAppNodeData extends INodeDataCondition {
|
||||||
|
options: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAgentNodeData extends INodeDataCondition {
|
||||||
|
agentId: string
|
||||||
|
model: string
|
||||||
|
systemPrompt: string // 系统提示词中含有变量
|
||||||
|
userPrompt: string // 用户提示词(变量)
|
||||||
|
params: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPreviewNodeData {
|
||||||
|
sourceNode: {
|
||||||
|
id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
export interface IComponentInputData {
|
||||||
|
component: string
|
||||||
|
params: {
|
||||||
|
value: string
|
||||||
|
type: FileType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 插件需要继承 IApiPluginNodeData
|
||||||
|
// */
|
||||||
|
// export interface IMarkdown2HtmlData extends IApiPluginNodeData {
|
||||||
|
// input: ''
|
||||||
|
// output: ''
|
||||||
|
// }
|
||||||
|
export interface IApiPluginNodeData extends INodeDataCondition {
|
||||||
|
method: 'POST' // 请求方法
|
||||||
|
url: string; // 请求地址
|
||||||
|
body: Record<string, unknown> // 请求体
|
||||||
|
headers: { [key: string]: string } // 请求头
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type INodeDataCondition = Partial<
|
||||||
|
INodeDataSuccessCondition & INodeDataFailureCondition
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface INodeDataSuccessCondition {
|
||||||
|
successCondition?: { key: string[], value: string | number | boolean }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeDataFailureCondition {
|
||||||
|
failureCondition: { key: string[], value: string | number | boolean }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileType = 'image' | 'video' | 'audio' | '3d' | 'text' | 'file'
|
||||||
|
export type OutputType = 'image' | 'video' | 'text' | 'audio' | '3d'
|
||||||
|
|
||||||
|
|
||||||
|
export interface GeneralOutput {
|
||||||
|
/**
|
||||||
|
* The generated output type, defaults to `image`,support `image` and `video` etc.
|
||||||
|
*/
|
||||||
|
type?: FileType;
|
||||||
|
/**
|
||||||
|
* The base64-encoded JSON of the generated image. Default value for `gpt-image-1`,
|
||||||
|
* and only present if `response_format` is set to `b64_json` for `dall-e-2` and
|
||||||
|
* `dall-e-3`.
|
||||||
|
*/
|
||||||
|
b64_json?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For `dall-e-3` only, the revised prompt that was used to generate the image.
|
||||||
|
*/
|
||||||
|
revised_prompt?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using `dall-e-2` or `dall-e-3`, the URL of the generated image if
|
||||||
|
* `response_format` is set to `url` (default value). Unsupported for
|
||||||
|
* `gpt-image-1`.
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
/**
|
||||||
|
* The generated content.
|
||||||
|
*/
|
||||||
|
content?: string;
|
||||||
|
/**
|
||||||
|
* The role of the generated content.
|
||||||
|
*/
|
||||||
|
role?: string;
|
||||||
|
|
||||||
|
/** buffer */
|
||||||
|
buffer?: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExecutionNodeStatusItem {
|
||||||
|
status: 'started' | 'process' | 'success' | 'failed'
|
||||||
|
result: unknown
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点输出类型
|
||||||
|
*/
|
||||||
|
export interface NodeOutput {
|
||||||
|
output_content: GeneralOutput[]
|
||||||
|
}
|
||||||
|
|
||||||
84
docs/DEVELOPERS-WORKFLOWNODE.md
Normal file
84
docs/DEVELOPERS-WORKFLOWNODE.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# 工作流节点开发文档
|
||||||
|
> 参考示例:Markdown2HTML节点
|
||||||
|
|
||||||
|
## 节点入口文件
|
||||||
|
> composables/worklfow/node/Markdown2HtmlNode.ts
|
||||||
|
> 此文件定义了节点所需要的所有内容
|
||||||
|
- nodeType 节点类型
|
||||||
|
- getNodeList 节点清单,对应添加节点时,显示的内容
|
||||||
|
- initData 初始化数据,当前的插件节点,后端逻辑都是通过接口处理的
|
||||||
|
- 示例接口:`server/api/markdown2html.post.ts` **注意返回结果类型:NodeOutput**
|
||||||
|
- createOutputSpec 定义参数关联节点产出的路径
|
||||||
|
- 文本类型为 ['output_content', '*', 'content']
|
||||||
|
- 媒体类型为 ['output_content', '*', 'url']
|
||||||
|
- renderNode: 节点对应的前端组件`components/Markdown2Html/index.vue`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NodeCategoryEnum, PluginBaseNode } from "./PluginBaseNode";
|
||||||
|
import {
|
||||||
|
Markdown2Html
|
||||||
|
} from '#components'
|
||||||
|
|
||||||
|
import { NodeTypeEnum } from "~/composables/worklfow/node/node.interface";
|
||||||
|
|
||||||
|
export default class Markdown2HtmlNode extends PluginBaseNode {
|
||||||
|
static override nodeType = NodeTypeEnum.Markdown2Html // 节点类型
|
||||||
|
|
||||||
|
static override getNodeList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: Markdown2HtmlNode.nodeType, // 节点类型
|
||||||
|
label: 'Markdown转HTML', // 标签
|
||||||
|
description: 'Markdown转HTML', // 描述
|
||||||
|
category: NodeCategoryEnum.BASE, // 分类
|
||||||
|
icon: 'material-symbols-light:markdown-paste' // 图标
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
initData() {
|
||||||
|
// 数据类型 IApiPluginNodeData
|
||||||
|
return {
|
||||||
|
method: 'POST', // 请求方法
|
||||||
|
url: 'http://localhost:3200/plugins/api/markdown2html', // 请求地址,节点的业务逻辑,需要在接口中完成
|
||||||
|
body: {
|
||||||
|
markdown: '' // 接口请求体内容,输入的markdown内容
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
// 依据接口自行扩展,认证等信息
|
||||||
|
}
|
||||||
|
} // 初始化数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入信息关联父节点产出的数据类型及数据路径
|
||||||
|
*/
|
||||||
|
override createOutputSpec(): INodeOutputSpec {
|
||||||
|
return {
|
||||||
|
type: 'text', // 文本类型
|
||||||
|
defaultPath: ['output_content', '*', 'content'] // 数据路径,文本为 ['output_content', '*', 'content'],媒体类型为 ['output_content', '*', 'url']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画布节点 UI
|
||||||
|
static override renderNode() {
|
||||||
|
return Markdown2Html // 节点组件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 维护组件信息文件:
|
||||||
|
> manifest/Markdown2HtmlNode.ts
|
||||||
|
> 需要参照IComponentMateInfo类型约束文件进行定义
|
||||||
|
|
||||||
|
|
||||||
|
## 组件效果
|
||||||
|
|
||||||
|
1. 添加节点
|
||||||
|

|
||||||
|
2. 关联节点
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
BIN
docs/images/WorkflowNode-AddNode.png
Normal file
BIN
docs/images/WorkflowNode-AddNode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 589 KiB |
BIN
docs/images/WorkflowNode-Associated.png
Normal file
BIN
docs/images/WorkflowNode-Associated.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 KiB |
13
manifest/Markdown2HtmlNode.ts
Normal file
13
manifest/Markdown2HtmlNode.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 页面插件示例
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {ComponentSceneConst, type IComponentMateInfo} from "~/composables";
|
||||||
|
export default {
|
||||||
|
name: "Markdown2Html", // 插件名称 备注:带上特殊标识,不要和已有插件重名,比如Remote+插件名
|
||||||
|
path: "./composables/worklfow/node/Markdown2HtmlNode.ts", // 插件路径
|
||||||
|
scenes: ComponentSceneConst.Workflow, // 组件场景
|
||||||
|
description: "",
|
||||||
|
data: undefined,
|
||||||
|
} satisfies IComponentMateInfo<typeof ComponentSceneConst.Workflow>;
|
||||||
|
|
||||||
@ -20,6 +20,7 @@
|
|||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
"ant-design-vue": "4.2.6",
|
"ant-design-vue": "4.2.6",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"nuxt": "^4.0.3",
|
"nuxt": "^4.0.3",
|
||||||
"vue": "^3.5.14",
|
"vue": "^3.5.14",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|||||||
@ -23,9 +23,15 @@ importers:
|
|||||||
ant-design-vue:
|
ant-design-vue:
|
||||||
specifier: 4.2.6
|
specifier: 4.2.6
|
||||||
version: 4.2.6(vue@3.5.18(typescript@5.9.2))
|
version: 4.2.6(vue@3.5.18(typescript@5.9.2))
|
||||||
|
markdown-it:
|
||||||
|
specifier: ^14.1.0
|
||||||
|
version: 14.1.0
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.18)(db0@0.3.2)(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.77.7)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.77.7)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
|
version: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.18)(db0@0.3.2)(eslint@9.33.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.46.2)(sass@1.77.7)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.77.7)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
|
||||||
|
prismjs:
|
||||||
|
specifier: ^1.30.0
|
||||||
|
version: 1.30.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.14
|
specifier: ^3.5.14
|
||||||
version: 3.5.18(typescript@5.9.2)
|
version: 3.5.18(typescript@5.9.2)
|
||||||
@ -3892,6 +3898,9 @@ packages:
|
|||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
listhen@1.9.0:
|
listhen@1.9.0:
|
||||||
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
|
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -3978,6 +3987,10 @@ packages:
|
|||||||
magicast@0.3.5:
|
magicast@0.3.5:
|
||||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||||
|
|
||||||
|
markdown-it@14.1.0:
|
||||||
|
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3991,6 +4004,9 @@ packages:
|
|||||||
mdn-data@2.12.2:
|
mdn-data@2.12.2:
|
||||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
||||||
|
|
||||||
|
mdurl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -4936,6 +4952,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
prismjs@1.30.0:
|
||||||
|
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
@ -4957,6 +4977,10 @@ packages:
|
|||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
|
punycode.js@2.3.1:
|
||||||
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5535,6 +5559,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||||
|
|
||||||
@ -9865,6 +9892,10 @@ snapshots:
|
|||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
listhen@1.9.0:
|
listhen@1.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@parcel/watcher': 2.5.1
|
'@parcel/watcher': 2.5.1
|
||||||
@ -9971,6 +10002,15 @@ snapshots:
|
|||||||
'@babel/types': 7.28.2
|
'@babel/types': 7.28.2
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
markdown-it@14.1.0:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
entities: 4.5.0
|
||||||
|
linkify-it: 5.0.0
|
||||||
|
mdurl: 2.0.0
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
mdn-data@2.0.14: {}
|
mdn-data@2.0.14: {}
|
||||||
@ -9979,6 +10019,8 @@ snapshots:
|
|||||||
|
|
||||||
mdn-data@2.12.2: {}
|
mdn-data@2.12.2: {}
|
||||||
|
|
||||||
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
merge-options@3.0.4:
|
merge-options@3.0.4:
|
||||||
@ -11075,6 +11117,8 @@ snapshots:
|
|||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
|
|
||||||
|
prismjs@1.30.0: {}
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
@ -11093,6 +11137,8 @@ snapshots:
|
|||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
qs@6.14.0:
|
qs@6.14.0:
|
||||||
@ -11735,6 +11781,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.2: {}
|
typescript@5.9.2: {}
|
||||||
|
|
||||||
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
ultrahtml@1.6.0: {}
|
ultrahtml@1.6.0: {}
|
||||||
|
|||||||
38
server/api/markdown2html.post.ts
Normal file
38
server/api/markdown2html.post.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// server/api/markdown2html.post.ts
|
||||||
|
import { defineEventHandler, readBody } from "h3";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import type { NodeOutput } from "~/composables/worklfow/node/node.interface";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// 获取请求体
|
||||||
|
const body = await readBody<{ markdown: string }>(event);
|
||||||
|
|
||||||
|
if (!body?.markdown) {
|
||||||
|
return { error: "Missing markdown content" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 Markdown-it
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: true, // 支持 HTML 标签
|
||||||
|
linkify: true, // 自动识别 URL
|
||||||
|
typographer: true // 美化引号、破折号等
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换
|
||||||
|
const html = md.render(body.markdown);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点输出类型标准数据结构
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
output_content: [{
|
||||||
|
type: "text",
|
||||||
|
content: html
|
||||||
|
}]
|
||||||
|
} as NodeOutput;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Markdown to HTML conversion failed:", err);
|
||||||
|
return { error: "Conversion failed", details: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user