diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/features/request/RequestInstruction.tsx b/packages/plugins/@tachybase/plugin-workflow/src/client/features/request/RequestInstruction.tsx index 159e52e39..b902a6f8c 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/client/features/request/RequestInstruction.tsx +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/features/request/RequestInstruction.tsx @@ -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: { diff --git a/packages/plugins/@tachybase/plugin-workflow/src/locale/en-US.json b/packages/plugins/@tachybase/plugin-workflow/src/locale/en-US.json index dde37430d..ec8853957 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/locale/en-US.json +++ b/packages/plugins/@tachybase/plugin-workflow/src/locale/en-US.json @@ -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" +} \ No newline at end of file diff --git a/packages/plugins/@tachybase/plugin-workflow/src/locale/zh-CN.json b/packages/plugins/@tachybase/plugin-workflow/src/locale/zh-CN.json index 60f8b950b..c594147d9 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/locale/zh-CN.json +++ b/packages/plugins/@tachybase/plugin-workflow/src/locale/zh-CN.json @@ -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": "是否携带权限校验" +} \ No newline at end of file diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/request/RequestInstruction.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/request/RequestInstruction.ts index 301de2646..b9b82ecdc 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/server/features/request/RequestInstruction.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/request/RequestInstruction.ts @@ -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; 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); diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/CreateInstruction.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/CreateInstruction.ts index ecdee4c04..2be1af186 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/CreateInstruction.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/CreateInstruction.ts @@ -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; + } + }; + //目前可处理url,json对象,base64 + const handleResource = async (resource) => { + const parseRes = isJSON(resource); + const config: AxiosRequestConfig = { + 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); } } } diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/UpdateInstruction.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/UpdateInstruction.ts index 68aac1fbb..fd804d3b2 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/UpdateInstruction.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/instructions/UpdateInstruction.ts @@ -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; + } + }; + //目前可处理url,json对象,base64 + const handleResource = async (resource) => { + const parseRes = isJSON(resource); + const config: AxiosRequestConfig = { + 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)]; } } }