UNPKG

n8n-nodes-weixin-wechat

Version:

西羊石AI微信插件 - 支持企业微信机器人、个人微信自动化 | WeChat integration for n8n with enterprise bot and personal WeChat automation

1,081 lines (986 loc) 31.6 kB
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, NodeOperationError, IHttpRequestMethods, NodeConnectionType, } from 'n8n-workflow'; import { readFileSync, existsSync } from 'fs'; import { basename } from 'path'; // 从URL提取文件名的工具函数 function extractFileNameFromUrl(url: string): string { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const fileName = pathname.split('/').pop() || 'file'; // 如果没有扩展名,尝试从查询参数中获取 if (!fileName.includes('.')) { const contentType = urlObj.searchParams.get('content-type'); if (contentType) { const ext = mimeTypeToExtension(contentType); return fileName + ext; } } return fileName; } catch { return 'file'; } } // MIME类型转文件扩展名的工具函数 function mimeTypeToExtension(mimeType: string): string { const mimeMap: { [key: string]: string } = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'video/mp4': '.mp4', 'video/avi': '.avi', 'audio/mp3': '.mp3', 'audio/wav': '.wav', 'application/pdf': '.pdf', 'application/msword': '.doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', 'text/plain': '.txt', }; return mimeMap[mimeType] || ''; } // 嵌入式轻量微信服务 - 内嵌版本 let embeddedServicePort: number | null = null; let embeddedServer: any = null; let isServiceInitialized = false; // 服务持久化存储文件路径 const serviceStateFile = require('path').join(require('os').tmpdir(), 'n8n-wechat-service-port.json'); // 保存服务状态 function saveServiceState(port: number) { try { require('fs').writeFileSync(serviceStateFile, JSON.stringify({ port, timestamp: Date.now() })); } catch (error) { console.warn('保存服务状态失败:', error); } } // 加载服务状态 function loadServiceState(): { port: number; timestamp: number } | null { try { const data = require('fs').readFileSync(serviceStateFile, 'utf8'); const state = JSON.parse(data); // 检查状态是否太旧(超过1小时重新启动) if (Date.now() - state.timestamp > 3600000) { return null; } return state; } catch (error) { return null; } } async function startEmbeddedWechatService(): Promise<number> { const express = require('express'); const cors = require('cors'); const axios = require('axios'); const app = express(); let port = 3000; // 设置Express app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // 健康检查 app.get('/health', (req: any, res: any) => { res.json({ status: 'ok', service: 'embedded-wechat-service', services: { 'enterprise-wechat-bot': 'ready', 'personal-wechat': 'ready' }, timestamp: new Date().toISOString() }); }); // 发送文本消息 app.post('/send/text', async (req: any, res: any) => { try { const { service, text, toType, toIds, batchOptions } = req.body; if (service === 'enterprise-wechat-bot') { // 企业微信机器人发送 const { webhook, messageType, enterpriseText, enterpriseMarkdown } = req.body; if (!webhook || webhook.includes('YOUR_KEY')) { return res.status(400).json({ success: false, error: '请在节点中配置企业微信Webhook地址' }); } let payload: any; // 根据消息类型构建不同的payload if (messageType === 'markdown') { payload = { msgtype: 'markdown', markdown: { content: enterpriseMarkdown || text || '# 标题\n**粗体文本**' } }; } else { // 默认为text类型 payload = { msgtype: 'text', text: { content: enterpriseText || text || '消息内容不能为空' } }; } const response = await axios.post(webhook, payload); res.json({ success: true, message: `企业微信${messageType === 'markdown' ? 'Markdown' : '文本'}消息发送成功`, messageType: messageType || 'text', response: response.data }); } else if (service === 'personal-wechat') { // 个人微信自动化 - 直接处理(不再使用嵌入式服务代理) return res.status(400).json({ success: false, error: '个人微信服务应该直接连接,不通过嵌入式服务', help: '请在凭证中配置个人微信服务地址' }); } else { throw new Error('不支持的服务类型'); } } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); // 发送文件 app.post('/send/file', async (req: any, res: any) => { try { const { service, url, filename, fileData, toType, toIds } = req.body; if (service === 'enterprise-wechat-bot') { // 企业微信文件发送 - 简化版:发送文件链接 const { webhook } = req.body; if (!webhook || webhook.includes('YOUR_KEY')) { return res.status(400).json({ success: false, error: '请在节点中配置企业微信Webhook地址' }); } const response = await axios.post(webhook, { msgtype: 'text', text: { content: `📎 文件分享\n文件名: ${filename}\n链接: ${url}` } }); res.json({ success: true, message: '企业微信文件发送成功', response: response.data }); } else if (service === 'personal-wechat') { // 个人微信文件发送 - 直接处理(不再使用嵌入式服务代理) return res.status(400).json({ success: false, error: '个人微信服务应该直接连接,不通过嵌入式服务', help: '请在凭证中配置个人微信服务地址' }); } else { throw new Error('不支持的服务类型'); } } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); // 查找可用端口 const checkPort = (port: number): Promise<boolean> => { return new Promise((resolve) => { const server = require('net').createServer(); server.listen(port, (err: any) => { if (err) { resolve(false); } else { server.once('close', () => resolve(true)); server.close(); } }); server.on('error', () => resolve(false)); }); }; // 寻找可用端口 for (let p = 3000; p < 3100; p++) { if (await checkPort(p)) { port = p; break; } } return new Promise((resolve, reject) => { embeddedServer = app.listen(port, '0.0.0.0', () => { console.log(`🚀 嵌入式微信服务已启动: http://0.0.0.0:${port}`); saveServiceState(port); // 保存服务状态 // 确保服务器引用不丢失 embeddedServer.keepAlive = true; // 防止未处理的异常导致进程退出 process.on('uncaughtException', (err) => { console.error('嵌入式服务未捕获异常:', err); }); process.on('unhandledRejection', (reason, promise) => { console.error('嵌入式服务未处理的Promise拒绝:', reason); }); resolve(port); }); embeddedServer.on('error', (err: any) => { console.error('嵌入式服务启动失败:', err); reject(err); }); // 监听连接关闭 embeddedServer.on('close', () => { console.log('嵌入式服务已关闭'); embeddedServicePort = null; embeddedServer = null; }); }); } async function ensureEmbeddedServiceRunning(): Promise<number> { // 如果服务已经运行,直接返回 if (embeddedServicePort && embeddedServer) { return embeddedServicePort; } // 尝试从持久化状态恢复 const savedState = loadServiceState(); if (savedState) { try { // 测试保存的端口是否仍然有效 const net = require('net'); const isPortOpen = await new Promise((resolve) => { const socket = new net.Socket(); socket.setTimeout(1000); socket.on('connect', () => { socket.destroy(); resolve(true); }); socket.on('timeout', () => { socket.destroy(); resolve(false); }); socket.on('error', () => { socket.destroy(); resolve(false); }); socket.connect(savedState.port, 'localhost'); }); if (isPortOpen) { console.log(`🔄 检测到嵌入式微信服务运行在端口: ${savedState.port}`); embeddedServicePort = savedState.port; return savedState.port; } } catch (error) { console.log('保存的服务端口检测失败,重新启动服务...'); } } // 启动新的服务实例 if (!isServiceInitialized) { try { isServiceInitialized = true; // 使用child_process启动独立的服务进程 const { spawn } = require('child_process'); const path = require('path'); // 获取服务守护进程路径 const serviceDaemonPath = path.join(__dirname, '..', 'embedded-service', 'service-daemon.js'); // 启动服务守护进程 const serviceProcess = spawn('node', [serviceDaemonPath], { detached: true, stdio: 'ignore' }); // 分离进程,让它独立运行 serviceProcess.unref(); // 等待服务启动 await new Promise(resolve => setTimeout(resolve, 2000)); // 检测服务是否启动成功 const savedState = loadServiceState(); if (savedState) { embeddedServicePort = savedState.port; console.log(`🚀 嵌入式微信服务守护进程已启动在端口: ${embeddedServicePort}`); } else { // 回退到内嵌服务 embeddedServicePort = await startEmbeddedWechatService(); console.log(`🚀 内嵌微信服务已启动在端口: ${embeddedServicePort}`); } } catch (error) { console.warn('启动嵌入式服务失败,将使用用户配置的服务:', error); isServiceInitialized = false; } } return embeddedServicePort || 3000; } // 导出函数供凭据测试使用 module.exports.ensureEmbeddedServiceRunning = ensureEmbeddedServiceRunning; async function requestWithAuth( thisArg: IExecuteFunctions, path: string, method: IHttpRequestMethods = 'GET', body?: any, ) { const credentials = await thisArg.getCredentials('weixinWechatApi'); // 🔒 强制API Key检查 - 防止用户绕过公众号获取步骤 if (!credentials?.apiKey || String(credentials.apiKey).trim() === '') { throw new NodeOperationError( thisArg.getNode(), `❌ 个人微信功能需要API Key!👉 获取方式:关注公众号【西羊石AI视频】,回复【API】`, { description: '必须获取API Key才能使用个人微信自动化功能' } ); } let baseUrl = ''; // 优先使用用户在凭证中配置的serviceUrl (解决Docker连接问题) if (credentials?.serviceUrl) { baseUrl = (credentials.serviceUrl as string).replace(/\/+$/, ''); console.log(`🔗 使用凭证配置的服务地址: ${baseUrl}`); } else { // 如果没有配置serviceUrl,才尝试使用嵌入式服务 try { const servicePort = await ensureEmbeddedServiceRunning(); baseUrl = `http://localhost:${servicePort}`; console.log(`🔧 使用嵌入式服务: ${baseUrl}`); } catch (error) { console.error('嵌入式服务启动失败:', error); baseUrl = 'http://localhost:3000'; // 回退到默认端口 console.log(`↩️ 回退到默认端口: ${baseUrl}`); } } const headers: { [key: string]: string } = { 'Content-Type': 'application/json', }; if (credentials?.apiKey) { headers['x-api-key'] = credentials.apiKey as string; } // 根据请求类型设置超时时间 const isFileRequest = path.includes('/send/file'); const isBatchRequest = body?.toIds && Array.isArray(body.toIds); let timeout = 30000; // 默认30秒 if (isFileRequest) { timeout = 120000; // 文件发送2分钟 } if (isBatchRequest) { // 批量发送:基础时间 + 每个目标的延迟时间 const targetCount = body.toIds.length; const delayPerTarget = (body.batchOptions?.sendDelay || 3) * 1000; const randomDelayMax = body.batchOptions?.randomDelay ? 5000 : 0; timeout = 60000 + (targetCount * (delayPerTarget + randomDelayMax)); // 动态超时 } const options = { method, url: `${baseUrl}${path}`, headers, json: true, timeout, body: body || undefined, }; try { return await thisArg.helpers.request(options); } catch (error: any) { // 只有在没有用户配置serviceUrl且使用默认localhost时,才尝试启动嵌入式服务 if (error.code === 'ECONNREFUSED' && !credentials?.serviceUrl && baseUrl === 'http://localhost:3000') { try { console.log('🔄 检测到连接失败,尝试启动嵌入式服务...'); const servicePort = await ensureEmbeddedServiceRunning(); options.url = options.url.replace('localhost:3000', `localhost:${servicePort}`); console.log(`🔄 重试请求到嵌入式服务: ${options.url}`); return await thisArg.helpers.request(options); } catch (embeddedError) { console.error('嵌入式服务启动失败:', embeddedError); } } // 检测API Key相关错误,提供公众号引导 const isApiKeyMissing = !credentials?.apiKey || credentials.apiKey === ''; const isApiKeyError = error.message?.includes('api-key') || error.message?.includes('unauthorized') || error.message?.includes('401') || error.status === 401; if (isApiKeyMissing || isApiKeyError) { throw new NodeOperationError( thisArg.getNode(), `Missing API Key. 👉 获取方式:关注公众号【西羊石AI视频】,回复【API】。`, { description: '需要API Key才能使用个人微信功能' } ); } throw new NodeOperationError( thisArg.getNode(), `WeChat API request failed: ${error.message}`, { description: error.description } ); } } export class WeixinWechatSend implements INodeType { description: INodeTypeDescription = { displayName: 'WeChat Send (西羊石AI)', name: 'weixinWechatSend', icon: 'file:wechat.png', group: ['communication'], version: 1, description: '西羊石AI微信插件 - 企业微信机器人、个人微信自动化 | 关注公众号"西羊石AI视频"获取API', defaults: { name: 'WeChat Send', }, inputs: [{ displayName: '', type: NodeConnectionType.Main }], outputs: [{ displayName: '', type: NodeConnectionType.Main }], credentials: [ { name: 'weixinWechatApi', required: true, }, ], properties: [ { displayName: '微信服务类型', name: 'service', type: 'options', default: 'personal-wechat', options: [ { name: '🙋‍♂️ 个人微信自动化 (推荐)', value: 'personal-wechat', description: '真实微信控制,功能全面!支持联系人/群聊/文件发送,使用面广', }, { name: '🏢 企业微信机器人', value: 'enterprise-wechat-bot', description: '简单易用,发送到企业微信群,无需额外部署', }, ], description: '💡 个人微信功能更全面!🔑 必须先获取API:关注公众号"西羊石AI视频"→发送"API"<br/>🏢 企业微信用户可直接使用,无需API Key', }, // 企业微信webhook配置 { displayName: '企业微信Webhook地址', name: 'enterpriseWebhook', type: 'string', typeOptions: { password: true }, default: '', displayOptions: { show: { service: ['enterprise-wechat-bot'] } }, placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY', description: '企业微信群机器人的Webhook地址 | 群设置 → 机器人 → 添加机器人', required: true, }, { displayName: '🚀 个人微信服务部署 (3分钟完成)', name: 'personalWechatNotice', type: 'notice', default: '', displayOptions: { show: { service: ['personal-wechat'] } }, typeOptions: { theme: 'info', }, description: '🔑 <b>1. 获取API Key:</b>关注公众号"西羊石AI视频" → 发送"API" → 复制密钥<br/>📦 <b>2. 下载服务:</b><a href="https://github.com/Standed/n8n-nodes-weixin-wechat" target="_blank">GitHub仓库</a> → personal-wechat-service目录<br/>🖱️ <b>3. Windows一键启动:</b>双击 一键启动.bat 即可 (自动安装依赖)<br/>🔌 <b>4. 配置地址:</b>本地 http://localhost:3000 | Docker: http://host.docker.internal:3000 | 云端: http://您的IP:3000', }, // 企业微信消息类型配置 { displayName: '消息类型', name: 'enterpriseMessageType', type: 'options', default: 'text', options: [ { name: '💬 文本消息', value: 'text', description: '发送纯文本消息', }, { name: '📝 Markdown消息', value: 'markdown', description: '发送支持markdown格式的富文本消息', }, { name: '🖼️ 图片消息', value: 'image', description: '发送图片文件', }, { name: '📰 图文消息', value: 'news', description: '发送图文卡片消息', }, { name: '📎 文件消息', value: 'file', description: '发送文件附件', }, ], displayOptions: { show: { service: ['enterprise-wechat-bot'] } }, description: '企业微信支持的消息类型', }, // 个人微信消息类型配置 { displayName: 'Message Type', name: 'resource', type: 'options', default: 'message', options: [ { name: '💬 Text Message', value: 'message', description: 'Send text message', }, { name: '🖼️ Image', value: 'image', description: 'Send image file', }, { name: '🎥 Video', value: 'video', description: 'Send video file', }, { name: '📄 Document', value: 'document', description: 'Send document file', }, { name: '🎵 Audio', value: 'audio', description: 'Send audio file', }, { name: '📎 File', value: 'file', description: 'Send any file type', }, ], displayOptions: { show: { service: ['personal-wechat'] } }, description: 'Type of message to send', }, // 个人微信目标配置 { displayName: '发送目标', name: 'chatType', type: 'options', default: 'filehelper', options: [ { name: '📁 文件传输助手 (推荐)', value: 'filehelper', description: '发送到微信文件传输助手,最安全可靠', }, { name: '👤 联系人', value: 'contact', description: '发送给微信好友联系人', }, { name: '👥 微信群', value: 'room', description: '发送到微信群聊', }, ], displayOptions: { show: { service: ['personal-wechat'], }, }, description: '个人微信自动化发送目标 - 西羊石AI', }, { displayName: '联系人/群名称', name: 'chatId', type: 'string', default: '', required: true, displayOptions: { show: { service: ['personal-wechat'], chatType: ['contact', 'room'], }, }, description: '支持多个目标,用英文逗号分隔(如:张三,李四,工作群)', placeholder: '例如: 张三,李四 或 工作群,家庭群', }, { displayName: 'Batch Options', name: 'batchOptions', type: 'collection', placeholder: 'Add Option', default: {}, displayOptions: { show: { service: ['personal-wechat'], chatType: ['contact', 'room'], }, }, options: [ { displayName: '发送间隔(秒)', name: 'sendDelay', type: 'number', default: 3, description: '多个联系人之间的发送间隔,防止被封号', typeOptions: { minValue: 1, maxValue: 60, }, }, { displayName: '随机延迟', name: 'randomDelay', type: 'boolean', default: true, description: '在基础延迟上添加随机时间(1-5秒)', }, ], }, // 企业微信文本消息配置 { displayName: '消息内容', name: 'enterpriseText', type: 'string', typeOptions: { rows: 4, }, default: '', required: true, displayOptions: { show: { service: ['enterprise-wechat-bot'], enterpriseMessageType: ['text'], }, }, description: '要发送的文本内容', }, // 企业微信Markdown消息配置 { displayName: 'Markdown内容', name: 'enterpriseMarkdown', type: 'string', typeOptions: { rows: 6, }, default: '**粗体** *斜体* \n- 列表项1\n- 列表项2\n\n[链接](https://example.com)', required: true, displayOptions: { show: { service: ['enterprise-wechat-bot'], enterpriseMessageType: ['markdown'], }, }, description: '支持Markdown格式的富文本内容', }, // 个人微信消息内容配置 { displayName: 'Message Text', name: 'text', type: 'string', typeOptions: { rows: 4, }, default: '', required: true, displayOptions: { show: { service: ['personal-wechat'], resource: ['message'], }, }, description: 'Text content to send', }, // 文件输入方式选择 { displayName: 'File Input Method', name: 'fileInputMethod', type: 'options', default: 'url', options: [ { name: '🔗 URL地址', value: 'url', description: '通过URL链接发送文件', }, { name: '📎 上传文件', value: 'upload', description: '上传本地文件或来自上游节点的文件', }, ], displayOptions: { show: { resource: ['image', 'video', 'document', 'audio', 'file'], }, }, description: '选择文件输入方式', }, // 文件URL输入 (URL方式时显示) { displayName: 'File URL', name: 'fileUrl', type: 'string', default: '', required: true, displayOptions: { show: { resource: ['image', 'video', 'document', 'audio', 'file'], fileInputMethod: ['url'], }, }, description: 'URL of the file to send', placeholder: 'https://example.com/file.jpg', }, // 文件上传 (上传方式时显示) { displayName: 'Input Binary Field', name: 'inputBinaryField', type: 'string', default: 'data', required: true, displayOptions: { show: { resource: ['image', 'video', 'document', 'audio', 'file'], fileInputMethod: ['upload'], }, }, description: 'Binary field name containing the file data from previous node', placeholder: 'data', }, { displayName: 'File Name', name: 'fileName', type: 'string', default: '', displayOptions: { show: { resource: ['image', 'video', 'document', 'audio', 'file'], }, }, description: 'Optional custom filename (will use original if not provided)', }, { displayName: 'Additional Options', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['image', 'video', 'document', 'audio', 'file'], }, }, options: [ { displayName: 'Caption/Description', name: 'caption', type: 'string', typeOptions: { rows: 2, }, default: '', description: 'Caption or description for the file', }, ], }, ], }; async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; for (let i = 0; i < items.length; i++) { try { const service = this.getNodeParameter('service', i) as string; let response: any; if (service === 'enterprise-wechat-bot') { // 企业微信处理 - 直接调用webhook,不通过requestWithAuth避免路由到个人微信 const messageType = this.getNodeParameter('enterpriseMessageType', i) as string; const webhook = this.getNodeParameter('enterpriseWebhook', i) as string; let messageContent: string; if (messageType === 'markdown') { messageContent = this.getNodeParameter('enterpriseMarkdown', i) as string; } else { messageContent = this.getNodeParameter('enterpriseText', i) as string; } // 构建企业微信标准payload const payload: any = { msgtype: messageType }; if (messageType === 'markdown') { payload.markdown = { content: messageContent }; } else { payload.text = { content: messageContent }; } // 直接调用企业微信webhook response = await this.helpers.request({ method: 'POST', url: webhook, json: payload, timeout: 30000 }); // 格式化返回结果保持一致性 response = { success: true, message: `企业微信${messageType === 'markdown' ? 'Markdown' : '文本'}消息发送成功`, messageType: messageType, webhook_response: response }; } else if (service === 'personal-wechat') { // 个人微信处理 const resource = this.getNodeParameter('resource', i) as string; if (resource === 'message') { // 发送文本消息 const text = this.getNodeParameter('text', i) as string; const requestBody: any = { service, text }; const chatType = this.getNodeParameter('chatType', i) as string; requestBody.toType = chatType; if (chatType !== 'filehelper') { const chatId = this.getNodeParameter('chatId', i) as string; const batchOptions = this.getNodeParameter('batchOptions', i) as any; if (chatId) { // 支持多联系人(逗号分隔) const targets = chatId.split(',').map(id => id.trim()).filter(id => id); requestBody.toIds = targets; // 使用复数形式传递多个目标 requestBody.batchOptions = { sendDelay: batchOptions?.sendDelay || 3, randomDelay: batchOptions?.randomDelay !== false }; } } response = await requestWithAuth(this, '/send/text', 'POST', requestBody); } else { // 个人微信文件发送 (image, video, document, audio, file) const fileInputMethod = this.getNodeParameter('fileInputMethod', i) as string; const fileName = this.getNodeParameter('fileName', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as any; // 构建请求体 const requestBody: any = { service }; if (fileInputMethod === 'url') { // URL方式 const fileUrl = this.getNodeParameter('fileUrl', i) as string; requestBody.url = fileUrl; requestBody.filename = fileName || extractFileNameFromUrl(fileUrl); } else { // 文件上传方式 const inputBinaryField = this.getNodeParameter('inputBinaryField', i) as string; const binaryData = items[i].binary?.[inputBinaryField]; if (!binaryData) { throw new NodeOperationError( this.getNode(), `No binary data found in field "${inputBinaryField}"`, { itemIndex: i } ); } // 使用原始文件名或用户指定的文件名 const originalFileName = binaryData.fileName || 'file'; const finalFileName = fileName || originalFileName; requestBody.fileData = { id: binaryData.id, data: binaryData.data, mimeType: binaryData.mimeType, fileName: finalFileName }; requestBody.filename = finalFileName; } // 添加个人微信特定参数 const chatType = this.getNodeParameter('chatType', i) as string; requestBody.toType = chatType; if (chatType !== 'filehelper') { const chatId = this.getNodeParameter('chatId', i) as string; const batchOptions = this.getNodeParameter('batchOptions', i) as any; if (chatId) { // 支持多联系人(逗号分隔) const targets = chatId.split(',').map(id => id.trim()).filter(id => id); requestBody.toIds = targets; // 使用复数形式传递多个目标 requestBody.batchOptions = { sendDelay: batchOptions?.sendDelay || 3, randomDelay: batchOptions?.randomDelay !== false }; } } // 添加说明文字(如果有) if (additionalFields?.caption) { requestBody.caption = additionalFields.caption; } response = await requestWithAuth(this, '/send/file', 'POST', requestBody); } } // 构建返回数据 let messageTypeForReturn: string; if (service === 'enterprise-wechat-bot') { messageTypeForReturn = this.getNodeParameter('enterpriseMessageType', i) as string; } else { messageTypeForReturn = this.getNodeParameter('resource', i) as string; } returnData.push({ json: { success: true, service, messageType: messageTypeForReturn, response, }, pairedItem: i, }); } catch (error: any) { if (this.continueOnFail()) { returnData.push({ json: { success: false, error: error.message, }, pairedItem: i, }); continue; } throw error; } } return [returnData]; } } async function uploadFileHelper( this: IExecuteFunctions, service: string, fileData: any, fileName: string ) { const credentials = await this.getCredentials('weixinWechatApi'); const baseUrl = String(credentials?.baseUrl || '').replace(/\/+$/, ''); // 获取文件的二进制数据 const buffer = await this.helpers.getBinaryDataBuffer(fileData.id, fileData.data); // 准备 FormData const formData = { service: service, filename: fileName, file: { value: buffer, options: { filename: fileName, contentType: fileData.mimeType || 'application/octet-stream' } } }; const headers: { [key: string]: string } = {}; if (credentials?.apiKey) { headers['x-api-key'] = credentials.apiKey as string; } const options = { method: 'POST' as IHttpRequestMethods, url: `${baseUrl}/upload/file`, headers, formData, timeout: 300000, // 5分钟超时,适合大文件 }; try { return await this.helpers.request(options); } catch (error: any) { throw new NodeOperationError( this.getNode(), `File upload failed: ${error.message}`, { description: error.description } ); } }