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:
parent
dd38db675d
commit
3df751063d
@ -41,12 +41,30 @@ export default class extends Instruction {
|
|||||||
placeholder: 'https://www.tachybase.com',
|
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: {
|
headers: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'ArrayItems',
|
'x-component': 'ArrayItems',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`,
|
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: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -273,13 +273,14 @@
|
|||||||
"Withdrawn": "Withdrawn",
|
"Withdrawn": "Withdrawn",
|
||||||
"Workflow todos": "Workflow todos",
|
"Workflow todos": "Workflow todos",
|
||||||
"Workflow": "Workflow",
|
"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",
|
"concat": "concat",
|
||||||
"ms": "ms",
|
"ms": "ms",
|
||||||
"Resubmit": "Resubmit",
|
"Resubmit": "Resubmit",
|
||||||
"Are you sure you want to resubmit it?": "Are you sure you want to resubmit it?",
|
"Are you sure you want to resubmit it?": "Are you sure you want to resubmit it?",
|
||||||
"MyLaunch":"MyLaunch",
|
"MyLaunch": "MyLaunch",
|
||||||
"CarbonCopy":"CarbonCopy",
|
"CarbonCopy": "CarbonCopy",
|
||||||
"ApprovalCarbonCopy":"Approval:CarbonCopy",
|
"ApprovalCarbonCopy": "Approval:CarbonCopy",
|
||||||
"Approval Launch":"Approval Launch"
|
"Approval Launch": "Approval Launch",
|
||||||
|
"needAuthorization": "needAuthorization"
|
||||||
}
|
}
|
@ -355,13 +355,14 @@
|
|||||||
"Workflow will be triggered before or after submitting succeeded based on workflow type.": "工作流会基于其类型在提交成功之前或之后触发。",
|
"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 will be triggered directly once the button clicked, without data saving.": "按钮点击后直接触发工作流,但不会保存数据。",
|
||||||
"Workflow": "工作流",
|
"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": "连接",
|
"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":"审批:抄送",
|
"ApprovalCarbonCopy": "审批:抄送",
|
||||||
"Approval Launch":"审批发起"
|
"Approval Launch": "审批发起",
|
||||||
|
"needAuthorization": "是否携带权限校验"
|
||||||
}
|
}
|
@ -1,7 +1,13 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
import { Gateway } from '@tachybase/server';
|
import { Gateway } from '@tachybase/server';
|
||||||
|
import { uid } from '@tachybase/utils';
|
||||||
|
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import FormData from 'form-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import mime from 'mime-types';
|
||||||
|
|
||||||
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '../..';
|
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '../..';
|
||||||
|
|
||||||
@ -14,19 +20,54 @@ export type RequestConfig = Pick<AxiosRequestConfig, 'url' | 'method' | 'params'
|
|||||||
headers: Array<Header>;
|
headers: Array<Header>;
|
||||||
ignoreFail: boolean;
|
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) {
|
async function request(config, context) {
|
||||||
// default headers
|
// default headers
|
||||||
const { token, origin } = context;
|
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 originUrl = config.url?.trim() || '';
|
||||||
const url = originUrl.startsWith('http') ? originUrl : `${origin}${originUrl}`;
|
const url = originUrl.startsWith('http') ? originUrl : `${origin}${originUrl}`;
|
||||||
|
|
||||||
let headers = (config.headers ?? []).reduce((result, header) => {
|
let headers = (config.headers ?? []).reduce((result, header) => {
|
||||||
if (header.name.toLowerCase() === 'content-type') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return Object.assign(result, { [header.name]: header.value });
|
return Object.assign(result, { [header.name]: header.value });
|
||||||
}, {});
|
}, {});
|
||||||
const params = (config.params ?? []).reduce(
|
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
|
const requestParams: AxiosRequestConfig = {
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
|
|
||||||
// let temp = context.get('headers');
|
|
||||||
headers = {
|
|
||||||
Authorization: 'Bearer ' + token,
|
|
||||||
...headers,
|
|
||||||
};
|
|
||||||
return axios.request({
|
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
params,
|
params,
|
||||||
data,
|
data,
|
||||||
timeout,
|
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 {
|
export default class extends Instruction {
|
||||||
@ -61,11 +132,7 @@ export default class extends Instruction {
|
|||||||
const context = { token, origin };
|
const context = { token, origin };
|
||||||
|
|
||||||
const config = processor.getParsedValue(node.config, node.id) as RequestConfig;
|
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 { workflow } = processor.execution;
|
||||||
const sync = this.workflow.isWorkflowSync(workflow);
|
const sync = this.workflow.isWorkflowSync(workflow);
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { parseCollectionName } from '@tachybase/data-source-manager';
|
|||||||
import { Gateway } from '@tachybase/server';
|
import { Gateway } from '@tachybase/server';
|
||||||
import { uid } from '@tachybase/utils';
|
import { uid } from '@tachybase/utils';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mime from 'mime-types';
|
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 userId = _.get(processor.getScope(node.id), '$context.user.id', '');
|
||||||
const token = this.workflow.app.authManager.jwt.sign({ userId });
|
const token = this.workflow.app.authManager.jwt.sign({ userId });
|
||||||
|
const isJSON = (str) => {
|
||||||
const handleUrl = async (url) => {
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//目前可处理url,json对象,base64
|
||||||
|
const handleResource = async (resource) => {
|
||||||
|
const parseRes = isJSON(resource);
|
||||||
|
const config: AxiosRequestConfig<any> = {
|
||||||
|
method: 'get',
|
||||||
|
url: resource,
|
||||||
|
responseType: 'stream',
|
||||||
|
};
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
if (url.startsWith('data:')) {
|
if (resource.startsWith('data:')) {
|
||||||
const matches = url.match(/^data:(.+);base64,(.+)$/);
|
const matches = resource.match(/^data:(.+);base64,(.+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const contentType = matches[1];
|
const contentType = matches[1];
|
||||||
const base64Data = matches[2];
|
const base64Data = matches[2];
|
||||||
@ -52,13 +65,33 @@ export class CreateInstruction extends Instruction {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid data URL format');
|
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 {
|
} else {
|
||||||
// 下载指定 URL 的内容
|
// 下载指定 URL 的内容
|
||||||
const response = await axios({
|
const response = await axios(config);
|
||||||
method: 'get',
|
|
||||||
url,
|
|
||||||
responseType: 'stream',
|
|
||||||
});
|
|
||||||
// 获取文件的 MIME 类型
|
// 获取文件的 MIME 类型
|
||||||
const contentType = response.headers['content-type'];
|
const contentType = response.headers['content-type'];
|
||||||
// 根据 MIME 类型获取文件扩展名
|
// 根据 MIME 类型获取文件扩展名
|
||||||
@ -90,11 +123,11 @@ export class CreateInstruction extends Instruction {
|
|||||||
const urls = options.values[attachmentField.options.name];
|
const urls = options.values[attachmentField.options.name];
|
||||||
if (Array.isArray(urls)) {
|
if (Array.isArray(urls)) {
|
||||||
for (const i in urls) {
|
for (const i in urls) {
|
||||||
urls[i] = await handleUrl(urls[i]);
|
urls[i] = await handleResource(urls[i]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const url = options.values[attachmentField.options.name];
|
const url = options.values[attachmentField.options.name];
|
||||||
options.values[attachmentField.options.name] = await handleUrl(url);
|
options.values[attachmentField.options.name] = await handleResource(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { Readable } from 'stream';
|
||||||
import { parseCollectionName } from '@tachybase/data-source-manager';
|
import { parseCollectionName } from '@tachybase/data-source-manager';
|
||||||
import { Gateway } from '@tachybase/server';
|
import { Gateway } from '@tachybase/server';
|
||||||
import { uid } from '@tachybase/utils';
|
import { uid } from '@tachybase/utils';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mime from 'mime-types';
|
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 userId = _.get(processor.getScope(node.id), '$context.user.id', '');
|
||||||
const token = this.workflow.app.authManager.jwt.sign({ userId });
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//目前可处理url,json对象,base64
|
||||||
|
const handleResource = async (resource) => {
|
||||||
|
const parseRes = isJSON(resource);
|
||||||
|
const config: AxiosRequestConfig<any> = {
|
||||||
|
method: 'get',
|
||||||
|
url: resource,
|
||||||
|
responseType: 'stream',
|
||||||
|
};
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
if (url.startsWith('data:')) {
|
if (resource.startsWith('data:')) {
|
||||||
const matches = url.match(/^data:(.+);base64,(.+)$/);
|
const matches = resource.match(/^data:(.+);base64,(.+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const contentType = matches[1];
|
const contentType = matches[1];
|
||||||
const base64Data = matches[2];
|
const base64Data = matches[2];
|
||||||
@ -51,13 +67,33 @@ export class UpdateInstruction extends Instruction {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid data URL format');
|
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 {
|
} else {
|
||||||
// 下载指定 URL 的内容
|
// 下载指定 URL 的内容
|
||||||
const response = await axios({
|
const response = await axios(config);
|
||||||
method: 'get',
|
|
||||||
url,
|
|
||||||
responseType: 'stream',
|
|
||||||
});
|
|
||||||
// 获取文件的 MIME 类型
|
// 获取文件的 MIME 类型
|
||||||
const contentType = response.headers['content-type'];
|
const contentType = response.headers['content-type'];
|
||||||
// 根据 MIME 类型获取文件扩展名
|
// 根据 MIME 类型获取文件扩展名
|
||||||
@ -89,11 +125,11 @@ export class UpdateInstruction extends Instruction {
|
|||||||
const urls = options.values[attachmentField.options.name];
|
const urls = options.values[attachmentField.options.name];
|
||||||
if (Array.isArray(urls)) {
|
if (Array.isArray(urls)) {
|
||||||
for (const i in urls) {
|
for (const i in urls) {
|
||||||
urls[i] = await handleUrl(urls[i]);
|
urls[i] = await handleResource(urls[i]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const url = options.values[attachmentField.options.name];
|
const url = options.values[attachmentField.options.name];
|
||||||
options.values[attachmentField.options.name] = [await handleUrl(url)];
|
options.values[attachmentField.options.name] = [await handleResource(url)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user