feat: 支持工作流文件节点多格式文档导出
- 补齐 md/html/pdf/docx 导出与统一渲染服务 - 收口文件生成节点配置与格式校验 - 修复 PDF 中文字体与 Markdown 渲染链路
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getMakeFileNodeFormPatch,
|
||||
getMakeFileTargetFormatOptions,
|
||||
isMakeFileFormatCombinationAllowed,
|
||||
resolveMakeFileNodeTargetFormat,
|
||||
resolveMakeFileNodeTemplateStyle,
|
||||
sanitizeMakeFileNodeFileName,
|
||||
} from '../makeFileNode';
|
||||
|
||||
describe('makeFileNode helpers', () => {
|
||||
it('清洗文件名中的非法字符与首尾空白', () => {
|
||||
expect(sanitizeMakeFileNodeFileName(' a/b\\\\c:*?<>| ')).toBe('abc');
|
||||
});
|
||||
|
||||
it('识别受支持与不受支持的格式组合', () => {
|
||||
expect(isMakeFileFormatCombinationAllowed('markdown', 'pdf')).toBe(true);
|
||||
expect(isMakeFileFormatCombinationAllowed('html', 'html')).toBe(true);
|
||||
expect(isMakeFileFormatCombinationAllowed('html', 'md')).toBe(false);
|
||||
expect(isMakeFileFormatCombinationAllowed('html', 'docx')).toBe(false);
|
||||
});
|
||||
|
||||
it('按当前 targetFormat 回显目标格式', () => {
|
||||
expect(resolveMakeFileNodeTargetFormat({ targetFormat: 'pdf' })).toBe('pdf');
|
||||
expect(resolveMakeFileNodeTargetFormat({ targetFormat: 'html' })).toBe('html');
|
||||
});
|
||||
|
||||
it('对未知 templateStyle 做默认回退', () => {
|
||||
expect(resolveMakeFileNodeTemplateStyle({ templateStyle: 'fancy' })).toBe('default');
|
||||
});
|
||||
|
||||
it('在 html 源格式下仅暴露 html 与 pdf 目标格式', () => {
|
||||
expect(getMakeFileTargetFormatOptions('html').map((item) => item.value)).toEqual([
|
||||
'html',
|
||||
'pdf',
|
||||
]);
|
||||
});
|
||||
|
||||
it('在 sourceFormat 切换为 html 时自动修正非法目标格式', () => {
|
||||
expect(
|
||||
getMakeFileNodeFormPatch('sourceFormat', 'html', {
|
||||
sourceFormat: 'markdown',
|
||||
targetFormat: 'docx',
|
||||
}),
|
||||
).toEqual({
|
||||
sourceFormat: 'html',
|
||||
targetFormat: 'html',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,117 @@ import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
const INVALID_FILE_NAME_PATTERN = /[\\/:*?"<>|\u0000-\u001f\u007f]/g;
|
||||
|
||||
export const makeFileTargetFormatValues = ['md', 'html', 'pdf', 'docx'] as const;
|
||||
export const makeFileSourceFormatValues = ['plain_text', 'markdown', 'html'] as const;
|
||||
const DEFAULT_TARGET_FORMAT = 'docx';
|
||||
const DEFAULT_SOURCE_FORMAT = 'markdown';
|
||||
const DEFAULT_TEMPLATE_STYLE = 'default';
|
||||
|
||||
type MakeFileTargetFormat = (typeof makeFileTargetFormatValues)[number];
|
||||
type MakeFileSourceFormat = (typeof makeFileSourceFormatValues)[number];
|
||||
|
||||
function isMakeFileTargetFormat(value: unknown): value is MakeFileTargetFormat {
|
||||
return typeof value === 'string' && makeFileTargetFormatValues.includes(value as MakeFileTargetFormat);
|
||||
}
|
||||
|
||||
function isMakeFileSourceFormat(value: unknown): value is MakeFileSourceFormat {
|
||||
return typeof value === 'string' && makeFileSourceFormatValues.includes(value as MakeFileSourceFormat);
|
||||
}
|
||||
|
||||
function getConfiguredTargetFormat(data?: Record<string, any>) {
|
||||
const candidate = data?.targetFormat;
|
||||
return isMakeFileTargetFormat(candidate) ? candidate : DEFAULT_TARGET_FORMAT;
|
||||
}
|
||||
|
||||
export function sanitizeMakeFileNodeFileName(value?: string) {
|
||||
return (value ?? '').replace(INVALID_FILE_NAME_PATTERN, '').trim();
|
||||
}
|
||||
|
||||
export function isMakeFileFormatCombinationAllowed(
|
||||
sourceFormat = DEFAULT_SOURCE_FORMAT,
|
||||
targetFormat = 'docx',
|
||||
) {
|
||||
return !(
|
||||
sourceFormat === 'html' &&
|
||||
(targetFormat === 'md' || targetFormat === 'docx')
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveMakeFileNodeSourceFormat(data?: Record<string, any>): MakeFileSourceFormat {
|
||||
return isMakeFileSourceFormat(data?.sourceFormat) ? data.sourceFormat : DEFAULT_SOURCE_FORMAT;
|
||||
}
|
||||
|
||||
export function resolveMakeFileNodeTargetFormat(data?: Record<string, any>): MakeFileTargetFormat {
|
||||
const sourceFormat = resolveMakeFileNodeSourceFormat(data);
|
||||
const targetFormat = getConfiguredTargetFormat(data);
|
||||
if (isMakeFileFormatCombinationAllowed(sourceFormat, targetFormat)) {
|
||||
return targetFormat;
|
||||
}
|
||||
return sourceFormat === 'html' ? 'html' : DEFAULT_TARGET_FORMAT;
|
||||
}
|
||||
|
||||
export function resolveMakeFileNodeTemplateStyle(data?: Record<string, any>) {
|
||||
return data?.templateStyle === DEFAULT_TEMPLATE_STYLE ? DEFAULT_TEMPLATE_STYLE : DEFAULT_TEMPLATE_STYLE;
|
||||
}
|
||||
|
||||
export function getMakeFileTargetFormatOptions(sourceFormat = DEFAULT_SOURCE_FORMAT) {
|
||||
return makeFileTargetFormatValues
|
||||
.filter((targetFormat) => isMakeFileFormatCombinationAllowed(sourceFormat, targetFormat))
|
||||
.map((value) => ({
|
||||
label: value,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getMakeFileNodeFormPatch(
|
||||
name: string,
|
||||
nextValue: string | number | boolean | undefined,
|
||||
data?: Record<string, any>,
|
||||
) {
|
||||
if (name === 'sourceFormat') {
|
||||
const sourceFormat = isMakeFileSourceFormat(nextValue) ? nextValue : DEFAULT_SOURCE_FORMAT;
|
||||
const currentTargetFormat = getConfiguredTargetFormat(data);
|
||||
if (isMakeFileFormatCombinationAllowed(sourceFormat, currentTargetFormat)) {
|
||||
return { sourceFormat };
|
||||
}
|
||||
return {
|
||||
sourceFormat,
|
||||
targetFormat: sourceFormat === 'html' ? 'html' : DEFAULT_TARGET_FORMAT,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'targetFormat') {
|
||||
const sourceFormat = resolveMakeFileNodeSourceFormat(data);
|
||||
const targetFormat = isMakeFileTargetFormat(nextValue) ? nextValue : DEFAULT_TARGET_FORMAT;
|
||||
return {
|
||||
targetFormat: isMakeFileFormatCombinationAllowed(sourceFormat, targetFormat)
|
||||
? targetFormat
|
||||
: sourceFormat === 'html'
|
||||
? 'html'
|
||||
: DEFAULT_TARGET_FORMAT,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'templateStyle') {
|
||||
return { templateStyle: resolveMakeFileNodeTemplateStyle({ templateStyle: nextValue }) };
|
||||
}
|
||||
|
||||
return { [name]: nextValue };
|
||||
}
|
||||
|
||||
function handleFileNameInput(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const sanitized = sanitizeMakeFileNodeFileName(target.value);
|
||||
if (target.value !== sanitized) {
|
||||
target.value = sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
[nodeNames.makeFileNode]: {
|
||||
title: $t('aiWorkflow.fileGeneration'),
|
||||
@@ -9,8 +120,8 @@ export default {
|
||||
description: $t('aiWorkflow.descriptions.fileGeneration'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z"></path></svg>',
|
||||
sortNo: 802,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: true,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
@@ -18,16 +129,42 @@ export default {
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: $t('documentCollection.splitterDoc.fileType'),
|
||||
description: $t('aiWorkflow.descriptions.fileType'),
|
||||
name: 'suffix',
|
||||
defaultValue: 'docx',
|
||||
label: $t('aiWorkflow.targetFormat'),
|
||||
description: $t('aiWorkflow.descriptions.targetFormat'),
|
||||
name: 'targetFormat',
|
||||
defaultValue: DEFAULT_TARGET_FORMAT,
|
||||
resolveValue: resolveMakeFileNodeTargetFormat,
|
||||
resolveOptions: (data: Record<string, any>) =>
|
||||
getMakeFileTargetFormatOptions(resolveMakeFileNodeSourceFormat(data)),
|
||||
onValueChange: (value: string | number | boolean | undefined, data: Record<string, any>) =>
|
||||
getMakeFileNodeFormPatch('targetFormat', value, data),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: $t('aiWorkflow.fileName'),
|
||||
description: $t('aiWorkflow.descriptions.fileName'),
|
||||
name: 'fileName',
|
||||
placeholder: $t('aiWorkflow.fileNamePlaceholder'),
|
||||
attrs: {
|
||||
maxlength: 128,
|
||||
oninput: handleFileNameInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: $t('aiWorkflow.templateStyle'),
|
||||
description: $t('aiWorkflow.descriptions.templateStyle'),
|
||||
name: 'templateStyle',
|
||||
defaultValue: DEFAULT_TEMPLATE_STYLE,
|
||||
resolveValue: resolveMakeFileNodeTemplateStyle,
|
||||
options: [
|
||||
{
|
||||
label: 'docx',
|
||||
value: 'docx',
|
||||
label: $t('aiWorkflow.defaultTemplateStyle'),
|
||||
value: DEFAULT_TEMPLATE_STYLE,
|
||||
},
|
||||
],
|
||||
onValueChange: (value: string | number | boolean | undefined, data: Record<string, any>) =>
|
||||
getMakeFileNodeFormPatch('templateStyle', value, data),
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
|
||||
Reference in New Issue
Block a user