feat(Workflow): 新增工作流节点组件

This commit is contained in:
chengcheng 2025-10-06 16:08:44 +08:00
parent 43e29a267e
commit 94952fb924
12 changed files with 701 additions and 1 deletions

23
app.vue
View File

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

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

View 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 // 节点组件
}
}

View 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 {}
}
}

View 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[]
}

View 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. 添加节点
![添加节点](./images/WorkflowNode-AddNode.png)
2. 关联节点
![关联节点](./images/WorkflowNode-Associated.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

View 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>;

View File

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

View File

@ -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: {}

View 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 };
}
});