diff --git a/app.vue b/app.vue index b94a8cb..f9d3fba 100644 --- a/app.vue +++ b/app.vue @@ -8,6 +8,9 @@ + + + diff --git a/components/Markdown2Html/index.vue b/components/Markdown2Html/index.vue new file mode 100644 index 0000000..5ae2e9b --- /dev/null +++ b/components/Markdown2Html/index.vue @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + 已关联节点 #{{ data.inputs.markdown[0] }} + + + + + + + + + + + + + + + diff --git a/composables/worklfow/node/Markdown2HtmlNode.ts b/composables/worklfow/node/Markdown2HtmlNode.ts new file mode 100644 index 0000000..7f4dbe5 --- /dev/null +++ b/composables/worklfow/node/Markdown2HtmlNode.ts @@ -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 // 节点组件 + } +} diff --git a/composables/worklfow/node/PluginBaseNode.ts b/composables/worklfow/node/PluginBaseNode.ts new file mode 100644 index 0000000..c63465c --- /dev/null +++ b/composables/worklfow/node/PluginBaseNode.ts @@ -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 + + 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 + + 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 {} + } +} diff --git a/composables/worklfow/node/node.interface.ts b/composables/worklfow/node/node.interface.ts new file mode 100644 index 0000000..1112663 --- /dev/null +++ b/composables/worklfow/node/node.interface.ts @@ -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 { + 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 +} + +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 // 请求体 + 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[] +} + diff --git a/docs/DEVELOPERS-WORKFLOWNODE.md b/docs/DEVELOPERS-WORKFLOWNODE.md new file mode 100644 index 0000000..b968679 --- /dev/null +++ b/docs/DEVELOPERS-WORKFLOWNODE.md @@ -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. 关联节点 + + + diff --git a/docs/images/WorkflowNode-AddNode.png b/docs/images/WorkflowNode-AddNode.png new file mode 100644 index 0000000..c03c1d1 Binary files /dev/null and b/docs/images/WorkflowNode-AddNode.png differ diff --git a/docs/images/WorkflowNode-Associated.png b/docs/images/WorkflowNode-Associated.png new file mode 100644 index 0000000..ca8cb13 Binary files /dev/null and b/docs/images/WorkflowNode-Associated.png differ diff --git a/manifest/Markdown2HtmlNode.ts b/manifest/Markdown2HtmlNode.ts new file mode 100644 index 0000000..05a0929 --- /dev/null +++ b/manifest/Markdown2HtmlNode.ts @@ -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; + diff --git a/package.json b/package.json index 4f63d00..c0e8968 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@nuxtjs/tailwindcss": "^6.14.0", "@vueuse/core": "^13.6.0", "ant-design-vue": "4.2.6", + "markdown-it": "^14.1.0", "nuxt": "^4.0.3", "vue": "^3.5.14", "vue-router": "^4.5.1" @@ -66,4 +67,4 @@ "vite-plugin-top-level-await": "^1.6.0" }, "packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8caba64..0b43206 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,15 @@ importers: ant-design-vue: specifier: 4.2.6 version: 4.2.6(vue@3.5.18(typescript@5.9.2)) + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 nuxt: 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) + prismjs: + specifier: ^1.30.0 + version: 1.30.0 vue: specifier: ^3.5.14 version: 3.5.18(typescript@5.9.2) @@ -3892,6 +3898,9 @@ packages: lines-and-columns@1.2.4: 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: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true @@ -3978,6 +3987,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3991,6 +4004,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4936,6 +4952,10 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} 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: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4957,6 +4977,10 @@ packages: pump@3.0.3: 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: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5535,6 +5559,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -9865,6 +9892,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + listhen@1.9.0: dependencies: '@parcel/watcher': 2.5.1 @@ -9971,6 +10002,15 @@ snapshots: '@babel/types': 7.28.2 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: {} mdn-data@2.0.14: {} @@ -9979,6 +10019,8 @@ snapshots: mdn-data@2.12.2: {} + mdurl@2.0.0: {} + media-typer@0.3.0: {} merge-options@3.0.4: @@ -11075,6 +11117,8 @@ snapshots: pretty-bytes@6.1.1: {} + prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -11093,6 +11137,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} qs@6.14.0: @@ -11735,6 +11781,8 @@ snapshots: typescript@5.9.2: {} + uc.micro@2.1.0: {} + ufo@1.6.1: {} ultrahtml@1.6.0: {} diff --git a/server/api/markdown2html.post.ts b/server/api/markdown2html.post.ts new file mode 100644 index 0000000..9650930 --- /dev/null +++ b/server/api/markdown2html.post.ts @@ -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 }; + } +});