feat(dianziqian): url保存附件支持json格式 (#1517)

Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: TomyJan <tomyjan6@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1517
Co-authored-by: wanggang <1366376228@qq.com>
Co-committed-by: wanggang <1366376228@qq.com>
This commit is contained in:
wanggang 2024-09-11 11:48:51 +08:00 committed by TomyJan
parent dd38db675d
commit 3df751063d
6 changed files with 211 additions and 55 deletions

View File

@ -41,12 +41,30 @@ export default class extends Instruction {
placeholder: 'https://www.tachybase.com',
},
},
//是否携带Authorization在请求头中
needAuthorization: {
type: 'string',
required: true,
title: `{{t("needAuthorization", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
showSearch: false,
allowClear: false,
className: 'auto-width',
},
enum: [
{ label: '是', value: true },
{ label: '否', value: false },
],
default: true,
},
headers: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`,
description: `{{t('"Content-Type" only support "application/json", and no need to specify', { ns: "${NAMESPACE}" })}}`,
description: `{{t('"Content-Type" only support "application/json and mutipart/form-data" ', { ns: "${NAMESPACE}" })}}`,
items: {
type: 'object',
properties: {

View File

@ -273,13 +273,14 @@
"Withdrawn": "Withdrawn",
"Workflow todos": "Workflow todos",
"Workflow": "Workflow",
"\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" only support \"application/json\", and no need to specify",
"\"Content-Type\" only support \"application/json\" and \"multipart/form-data\" ": "\"Content-Type\" only support \"application/json\" and \"multipart/form-data\" ",
"concat": "concat",
"ms": "ms",
"Resubmit": "Resubmit",
"Are you sure you want to resubmit it?": "Are you sure you want to resubmit it?",
"MyLaunch":"MyLaunch",
"CarbonCopy":"CarbonCopy",
"ApprovalCarbonCopy":"Approval:CarbonCopy",
"Approval Launch":"Approval Launch"
}
"MyLaunch": "MyLaunch",
"CarbonCopy": "CarbonCopy",
"ApprovalCarbonCopy": "Approval:CarbonCopy",
"Approval Launch": "Approval Launch",
"needAuthorization": "needAuthorization"
}

View File

@ -355,13 +355,14 @@
"Workflow will be triggered before or after submitting succeeded based on workflow type.": "工作流会基于其类型在提交成功之前或之后触发。",
"Workflow will be triggered directly once the button clicked, without data saving.": "按钮点击后直接触发工作流,但不会保存数据。",
"Workflow": "工作流",
"\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" 请求头仅支持 \"application/json\",无需填写",
"\"Content-Type\" only support \"application/json\" and \"multipart/form-data\" ": "\"Content-Type\" 请求头仅支持 \"application/json\" 和 \"multipart/form-data \" ",
"concat": "连接",
"ms": "毫秒",
"Resubmit": "重新发起",
"Are you sure you want to resubmit it?": "是否重新复制一份?",
"MyLaunch":"我的发起",
"CarbonCopy":"抄送",
"ApprovalCarbonCopy":"审批:抄送",
"Approval Launch":"审批发起"
}
"MyLaunch": "我的发起",
"CarbonCopy": "抄送",
"ApprovalCarbonCopy": "审批:抄送",
"Approval Launch": "审批发起",
"needAuthorization": "是否携带权限校验"
}

View File

@ -1,7 +1,13 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { Gateway } from '@tachybase/server';
import { uid } from '@tachybase/utils';
import axios, { AxiosRequestConfig } from 'axios';
import FormData from 'form-data';
import _ from 'lodash';
import mime from 'mime-types';
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '../..';
@ -14,19 +20,54 @@ export type RequestConfig = Pick<AxiosRequestConfig, 'url' | 'method' | 'params'
headers: Array<Header>;
ignoreFail: boolean;
};
async function downloadToStream(resourceUrl, Headers, param, body) {
try {
// 发起 GET 请求,设置响应类型为流
const response = await axios({
method: 'GET',
url: resourceUrl,
headers: Headers,
params: param,
data: body,
responseType: 'stream',
});
// 根据 MIME 类型获取文件扩展名
const ext = mime.extension(Headers['Content-Type']);
let tempFileName;
do {
tempFileName = `${uid()}.${ext}`;
} while (fs.existsSync(tempFileName));
const tempFilePath = path.join(os.tmpdir(), tempFileName);
const writeStream = fs.createWriteStream(tempFilePath);
// 将响应流写入临时文件
response.data.pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('finish', () => {
// 完成写入后,返回一个读取流
resolve(fs.createReadStream(tempFilePath));
});
writeStream.on('error', (error) => {
fs.unlinkSync(tempFilePath);
reject(error);
});
});
} catch (error) {
console.error('下载文件时出错:', error);
throw error;
}
}
async function request(config, context) {
// default headers
const { token, origin } = context;
const { method = 'POST', data, timeout = 5000 } = config;
const { method = 'POST', needAuthorization = true, data, timeout = 5000 } = config;
const originUrl = config.url?.trim() || '';
const url = originUrl.startsWith('http') ? originUrl : `${origin}${originUrl}`;
let headers = (config.headers ?? []).reduce((result, header) => {
if (header.name.toLowerCase() === 'content-type') {
return result;
}
return Object.assign(result, { [header.name]: header.value });
}, {});
const params = (config.params ?? []).reduce(
@ -34,22 +75,52 @@ async function request(config, context) {
{},
);
// TODO(feat): only support JSON type for now, should support others in future
headers['Content-Type'] = 'application/json';
// let temp = context.get('headers');
headers = {
Authorization: 'Bearer ' + token,
...headers,
};
return axios.request({
const requestParams: AxiosRequestConfig = {
url,
method,
headers,
params,
data,
timeout,
});
};
// let temp = context.get('headers');
if (needAuthorization) {
requestParams.headers = {
Authorization: 'Bearer ' + token,
...headers,
};
}
if (headers['Content-Type'] === 'multipart/form-data') {
//workflow contentType类型
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
if (key === 'file') {
const { resourceUrl, params: Params, headers: Header, body: Body } = data[key];
if (Header['Content-Type'] === 'multipart/form-data' && Body) {
//resource contentType类型
const formData = new FormData();
Object.entries(Body).forEach(([key, value]) => {
formData.append(key, value);
});
config.data = formData;
} else {
config.data = Body;
}
const stream = await downloadToStream(resourceUrl, Header, Params, Body);
formData.append('file', stream);
headers = { ...formData.getHeaders() };
} else {
formData.append(key, data[key]);
}
}
requestParams.data = formData;
} else {
headers['Content-Type'] = 'application/json';
}
return axios.request(requestParams);
}
export default class extends Instruction {
@ -61,11 +132,7 @@ export default class extends Instruction {
const context = { token, origin };
const config = processor.getParsedValue(node.config, node.id) as RequestConfig;
// delete user token if outer http
if (config.url?.startsWith('http')) {
delete context.token;
delete context.origin;
}
const { workflow } = processor.execution;
const sync = this.workflow.isWorkflowSync(workflow);

View File

@ -2,7 +2,7 @@ import { parseCollectionName } from '@tachybase/data-source-manager';
import { Gateway } from '@tachybase/server';
import { uid } from '@tachybase/utils';
import axios from 'axios';
import axios, { AxiosRequestConfig } from 'axios';
import FormData from 'form-data';
import _ from 'lodash';
import mime from 'mime-types';
@ -33,11 +33,24 @@ export class CreateInstruction extends Instruction {
const userId = _.get(processor.getScope(node.id), '$context.user.id', '');
const token = this.workflow.app.authManager.jwt.sign({ userId });
const handleUrl = async (url) => {
const isJSON = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return false;
}
};
//目前可处理urljson对象base64
const handleResource = async (resource) => {
const parseRes = isJSON(resource);
const config: AxiosRequestConfig<any> = {
method: 'get',
url: resource,
responseType: 'stream',
};
const form = new FormData();
if (url.startsWith('data:')) {
const matches = url.match(/^data:(.+);base64,(.+)$/);
if (resource.startsWith('data:')) {
const matches = resource.match(/^data:(.+);base64,(.+)$/);
if (matches) {
const contentType = matches[1];
const base64Data = matches[2];
@ -52,13 +65,33 @@ export class CreateInstruction extends Instruction {
} else {
throw new Error('Invalid data URL format');
}
} else if (parseRes) {
const { resourceUrl, params, headers, body } = parseRes;
config.url = resourceUrl;
config.params = params;
config.headers = headers;
if (headers['content-type'] === 'multipart/form-data') {
const formData = new FormData();
Object.entries(body).forEach(([key, value]) => {
formData.append(key, value);
});
config.data = formData;
} else {
config.data = body;
}
const response = await axios(config);
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
// 创建 FormData 实例
form.append('file', response.data, {
filename,
contentType: response.headers['content-type'],
});
} else {
// 下载指定 URL 的内容
const response = await axios({
method: 'get',
url,
responseType: 'stream',
});
const response = await axios(config);
// 获取文件的 MIME 类型
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
@ -90,11 +123,11 @@ export class CreateInstruction extends Instruction {
const urls = options.values[attachmentField.options.name];
if (Array.isArray(urls)) {
for (const i in urls) {
urls[i] = await handleUrl(urls[i]);
urls[i] = await handleResource(urls[i]);
}
} else {
const url = options.values[attachmentField.options.name];
options.values[attachmentField.options.name] = await handleUrl(url);
options.values[attachmentField.options.name] = await handleResource(url);
}
}
}

View File

@ -1,8 +1,10 @@
import fs from 'fs';
import { Readable } from 'stream';
import { parseCollectionName } from '@tachybase/data-source-manager';
import { Gateway } from '@tachybase/server';
import { uid } from '@tachybase/utils';
import axios from 'axios';
import axios, { AxiosRequestConfig } from 'axios';
import FormData from 'form-data';
import _ from 'lodash';
import mime from 'mime-types';
@ -33,10 +35,24 @@ export class UpdateInstruction extends Instruction {
const userId = _.get(processor.getScope(node.id), '$context.user.id', '');
const token = this.workflow.app.authManager.jwt.sign({ userId });
const handleUrl = async (url) => {
const isJSON = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return false;
}
};
//目前可处理urljson对象base64
const handleResource = async (resource) => {
const parseRes = isJSON(resource);
const config: AxiosRequestConfig<any> = {
method: 'get',
url: resource,
responseType: 'stream',
};
const form = new FormData();
if (url.startsWith('data:')) {
const matches = url.match(/^data:(.+);base64,(.+)$/);
if (resource.startsWith('data:')) {
const matches = resource.match(/^data:(.+);base64,(.+)$/);
if (matches) {
const contentType = matches[1];
const base64Data = matches[2];
@ -51,13 +67,33 @@ export class UpdateInstruction extends Instruction {
} else {
throw new Error('Invalid data URL format');
}
} else if (parseRes) {
const { resourceUrl, params, headers, body } = parseRes;
config.url = resourceUrl;
config.params = params;
config.headers = headers;
if (headers['content-type'] === 'multipart/form-data') {
const formData = new FormData();
Object.entries(body).forEach(([key, value]) => {
formData.append(key, value);
});
config.data = formData;
} else {
config.data = body;
}
const response = await axios(config);
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
const ext = mime.extension(contentType);
const filename = `${uid()}.${ext}`;
// 创建 FormData 实例
form.append('file', response.data, {
filename,
contentType: response.headers['content-type'],
});
} else {
// 下载指定 URL 的内容
const response = await axios({
method: 'get',
url,
responseType: 'stream',
});
const response = await axios(config);
// 获取文件的 MIME 类型
const contentType = response.headers['content-type'];
// 根据 MIME 类型获取文件扩展名
@ -89,11 +125,11 @@ export class UpdateInstruction extends Instruction {
const urls = options.values[attachmentField.options.name];
if (Array.isArray(urls)) {
for (const i in urls) {
urls[i] = await handleUrl(urls[i]);
urls[i] = await handleResource(urls[i]);
}
} else {
const url = options.values[attachmentField.options.name];
options.values[attachmentField.options.name] = [await handleUrl(url)];
options.values[attachmentField.options.name] = [await handleResource(url)];
}
}
}