From e35aaf14adcd518e2e3cb98f70e7a76cfc19cbd1 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Fri, 21 Nov 2025 14:34:54 +0800 Subject: [PATCH] perf(tool): wechat official account tool: support multiple articles upload --- .../packages/wechatOfficialAccount/README.md | 59 +++++ .../children/uploadMarkdownToDraft/config.ts | 136 +++++++++++- .../uploadMarkdownToDraft/src/index.ts | 204 +++++++++++++++--- 3 files changed, 372 insertions(+), 27 deletions(-) diff --git a/modules/tool/packages/wechatOfficialAccount/README.md b/modules/tool/packages/wechatOfficialAccount/README.md index 718217ff..6ca71e4e 100644 --- a/modules/tool/packages/wechatOfficialAccount/README.md +++ b/modules/tool/packages/wechatOfficialAccount/README.md @@ -2,3 +2,62 @@ 按照如图所示的方式获取密钥 ![](./assets/get-secrets.jpg) + +在 FastGPT 云服务版本使用时 +海外版用户(cloud.fastgpt.io)可以填写下面的 IP 白名单: + +``` +35.240.227.100 +34.124.237.188 +34.143.240.160 +34.87.51.146 +34.87.79.202 +35.247.163.68 +34.87.102.86 +35.198.192.104 +34.126.163.205 +34.124.189.116 +34.143.149.171 +34.87.173.252 +34.142.157.52 +34.87.180.104 +34.87.20.189 +34.87.110.152 +34.87.44.74 +34.87.152.33 +35.197.149.75 +35.247.161.35 +``` + +国内版用户(fastgpt.cn)可以填写下面的 IP 白名单: + +``` +47.97.1.240 +121.43.105.217 +121.41.178.7 +121.40.65.187 +47.97.59.172 +101.37.205.32 +120.55.195.90 +120.26.229.115 +120.55.193.112 +47.98.190.173 +112.124.41.79 +121.196.235.183 +121.41.75.88 +121.43.108.48 +112.124.12.6 +121.43.52.222 +121.199.162.43 +121.199.162.102 +120.55.94.163 +47.99.59.223 +112.124.46.5 +121.40.46.247 +120.26.145.73 +120.26.147.199 +121.43.125.163 +121.196.228.45 +121.43.126.202 +120.26.144.37 +``` diff --git a/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/config.ts b/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/config.ts index d57a4ef0..e5698103 100644 --- a/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/config.ts +++ b/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/config.ts @@ -13,9 +13,143 @@ export default defineTool({ toolDescription: '将 Markdown 内容转换为微信公众号图文消息格式,自动处理图片上传和封面图,然后保存到草稿箱。支持标题、作者、摘要等信息的自定义配置。', versionList: [ + { + value: '0.2.0', + description: '批量上传版本(支持多篇文档)', + inputs: [ + { + key: 'accessToken', + label: '访问令牌', + description: '微信公众号 API 访问令牌(可选,与 appId/appSecret 二选一)', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: false + }, + { + key: 'markdownContent', + label: 'Markdown 内容', + description: '要转换的 Markdown 格式文章内容,支持单个字符串或字符串数组(多篇文档)', + renderTypeList: [ + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.textarea, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + toolDescription: 'markdown format content or array of markdown contents', + required: true + }, + { + key: 'coverImage', + label: '封面图', + description: + '封面图片 URL 或 media_id,如果是 URL 将自动上传为永久素材。支持单个字符串或字符串数组(多篇文档对应多个封面图)', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + required: true, + toolDescription: 'cover image url or media_id or array of cover images' + }, + { + key: 'title', + label: '文章标题', + description: + '图文消息的标题,支持单个字符串或字符串数组。如果不填写将尝试从 Markdown 中提取', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + required: true, + toolDescription: 'article title or array of article titles' + }, + { + key: 'author', + label: '作者', + description: '文章作者信息,支持单个字符串或字符串数组', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + required: false, + toolDescription: 'article author or array of authors' + }, + { + key: 'digest', + label: '文章摘要', + description: '文章摘要信息,如果不填写将自动从内容中提取。支持单个字符串或字符串数组', + renderTypeList: [ + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.textarea, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + required: false, + toolDescription: + 'article digest or array of digests, optional, less than 120 characters each' + }, + { + key: 'contentSourceUrl', + label: '原文链接', + description: '原文阅读链接地址,支持单个字符串或字符串数组', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.string, + required: false, + toolDescription: 'original article link or array of links' + }, + { + key: 'needOpenComment', + label: '开启评论', + description: '是否开启评论功能,0 表示关闭,1 表示开启。支持单个数字或数字数组', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.number, + required: false + }, + { + key: 'onlyFansCanComment', + label: '仅粉丝评论', + description: + '是否仅允许粉丝评论,0 表示所有人可评论,1 表示仅粉丝可评论。支持单个数字或数字数组', + renderTypeList: [ + FlowNodeInputTypeEnum.input, + FlowNodeInputTypeEnum.reference, + FlowNodeInputTypeEnum.JSONEditor + ], + valueType: WorkflowIOValueTypeEnum.number, + required: false + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'media_id', + label: '素材ID', + description: '草稿箱中图文消息的媒体标识符' + }, + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'error_message', + label: '错误信息', + description: '处理过程中的错误信息' + } + ] + }, { value: '0.1.0', - description: 'Default version', + description: '单篇文章上传版本', inputs: [ { key: 'accessToken', diff --git a/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/src/index.ts b/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/src/index.ts index e065ca57..d2f76701 100644 --- a/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/src/index.ts +++ b/modules/tool/packages/wechatOfficialAccount/children/uploadMarkdownToDraft/src/index.ts @@ -8,6 +8,35 @@ import { downloadImageFromUrl } from '../../../lib/handler'; import { addInlineStyles } from './styles'; +import { addLog } from '@/utils/log'; + +// 辅助函数:解析字符串或字符串数组,支持 JSON 编码的数组 +function parseStringOrArray(val: unknown): string[] { + if (Array.isArray(val)) { + return val.map((item) => String(item)); + } + + if (typeof val === 'string') { + // 尝试解析 JSON 数组 + if (val.trim().startsWith('[') && val.trim().endsWith(']')) { + try { + const parsed = JSON.parse(val); + if (Array.isArray(parsed)) { + return parsed.map((item) => String(item)); + } + } catch { + // JSON 解析失败,当作普通字符串处理 + } + } + return [val]; + } + + // 其他类型转为字符串 + return [String(val)]; +} + +// 辅助类型:支持字符串或字符串数组(用于输入验证) +const StringOrArray = z.union([z.string(), z.array(z.string())]); export const InputType = z .object({ @@ -16,17 +45,39 @@ export const InputType = z appId: z.string().optional(), secret: z.string().optional(), - // 必需参数 - markdownContent: z.string().min(1, 'Markdown 内容不能为空'), - coverImage: z.string().min(1, '封面图不能为空'), - - // 可选参数 - title: z.string().optional(), - author: z.string().optional(), - digest: z.string().optional(), - contentSourceUrl: z.string().optional(), - needOpenComment: z.number().optional().default(0), - onlyFansCanComment: z.number().optional().default(0) + // 必需参数 - 支持单个或多个 + markdownContent: StringOrArray.refine( + (val) => { + if (Array.isArray(val)) { + return val.length > 0 && val.every((content) => content.trim().length > 0); + } + return val.trim().length > 0; + }, + { message: 'Markdown 内容不能为空' } + ), + coverImage: StringOrArray.refine( + (val) => { + if (Array.isArray(val)) { + return val.length > 0 && val.every((img) => img.trim().length > 0); + } + return val.trim().length > 0; + }, + { message: '封面图不能为空' } + ), + + // 可选参数 - 支持单个或多个 + title: StringOrArray.optional(), + author: StringOrArray.optional(), + digest: StringOrArray.optional(), + contentSourceUrl: StringOrArray.optional(), + needOpenComment: z + .union([z.number(), z.array(z.number())]) + .optional() + .default(0), + onlyFansCanComment: z + .union([z.number(), z.array(z.number())]) + .optional() + .default(0) }) .refine( (data) => { @@ -57,6 +108,7 @@ export async function tool({ needOpenComment = 0, onlyFansCanComment = 0 }: z.infer): Promise> { + addLog.info(`${markdownContent}${typeof markdownContent}`); // 1. 获取 access_token let token = accessToken; if (!token) { @@ -76,10 +128,120 @@ export async function tool({ } } - // 2. Markdown 转 HTML + // 2. 解析输入为数组格式(支持 JSON 编码的数组) + const markdownContents = parseStringOrArray(markdownContent); + const coverImages = parseStringOrArray(coverImage); + const titles = title ? parseStringOrArray(title) : undefined; + const authors = author ? parseStringOrArray(author) : undefined; + const digests = digest ? parseStringOrArray(digest) : undefined; + const contentSourceUrls = contentSourceUrl ? parseStringOrArray(contentSourceUrl) : undefined; + const needOpenComments = Array.isArray(needOpenComment) ? needOpenComment : [needOpenComment]; + const onlyFansCanComments = Array.isArray(onlyFansCanComment) + ? onlyFansCanComment + : [onlyFansCanComment]; + + // 3. 验证数组长度一致性 + const articleCount = markdownContents.length; + + if (coverImages.length !== articleCount && coverImages.length !== 1) { + return { + error_message: `封面图数量必须与 Markdown 内容数量一致,或提供一个通用封面图。封面图: ${coverImages.length}, 文章: ${articleCount}` + }; + } + + if (titles && titles.length !== articleCount && titles.length !== 1) { + return { + error_message: `标题数量必须与 Markdown 内容数量一致,或提供一个通用标题。标题: ${titles.length}, 文章: ${articleCount}` + }; + } + + // 4. 批量处理每篇文章 + const articles = []; + + for (let i = 0; i < articleCount; i++) { + try { + // 获取当前文章的配置(支持数组长度为1时的复用) + const currentMarkdown = markdownContents[i]; + const currentCoverImage = coverImages.length === 1 ? coverImages[0] : coverImages[i]; + const currentTitle = titles ? (titles.length === 1 ? titles[0] : titles[i]) : undefined; + const currentAuthor = authors ? (authors.length === 1 ? authors[0] : authors[i]) : undefined; + const currentDigest = digests ? (digests.length === 1 ? digests[0] : digests[i]) : undefined; + const currentContentSourceUrl = contentSourceUrls + ? contentSourceUrls.length === 1 + ? contentSourceUrls[0] + : contentSourceUrls[i] + : undefined; + const currentNeedOpenComment = + needOpenComments.length === 1 ? needOpenComments[0] : needOpenComments[i]; + const currentOnlyFansCanComment = + onlyFansCanComments.length === 1 ? onlyFansCanComments[0] : onlyFansCanComments[i]; + + // 处理单篇 markdown + const processedArticle = await processSingleArticle({ + token, + markdownContent: currentMarkdown, + coverImage: currentCoverImage, + title: currentTitle, + author: currentAuthor, + digest: currentDigest, + contentSourceUrl: currentContentSourceUrl, + needOpenComment: currentNeedOpenComment, + onlyFansCanComment: currentOnlyFansCanComment + }); + + articles.push(processedArticle); + } catch (error) { + return { + error_message: `处理第 ${i + 1} 篇文章失败: ${error instanceof Error ? error.message : '未知错误'}` + }; + } + } + + // 5. 批量上传到草稿箱 + try { + const result = await handleAddDraft({ + access_token: token, + articles: articles + }); + + return { + media_id: result.media_id + }; + } catch (error) { + return { + error_message: `批量上传草稿失败: ${error instanceof Error ? error.message : '未知错误'}` + }; + } +} + +/** + * 处理单篇文章的辅助函数 + */ +async function processSingleArticle({ + token, + markdownContent, + coverImage, + title, + author, + digest, + contentSourceUrl, + needOpenComment = 0, + onlyFansCanComment = 0 +}: { + token: string; + markdownContent: string; + coverImage: string; + title?: string; + author?: string; + digest?: string; + contentSourceUrl?: string; + needOpenComment?: number; + onlyFansCanComment?: number; +}) { + // 1. Markdown 转 HTML const html = convertMarkdownToHtml(markdownContent); - // 3. 提取并处理图片 + // 2. 提取并处理图片 const imageUrls = extractImageUrls(html); let processedHtml = html; @@ -93,11 +255,11 @@ export async function tool({ } } - // 4. 处理封面图 + // 3. 处理封面图 const thumbMediaId = await processCoverImage(token, coverImage); - // 5. 构建文章对象 - const article = { + // 4. 构建文章对象 + return { title: title, author: author, digest: digest, @@ -108,16 +270,6 @@ export async function tool({ only_fans_can_comment: onlyFansCanComment, article_type: 'news' as const }; - - // 6. 上传到草稿箱 - const result = await handleAddDraft({ - access_token: token, - articles: [article] - }); - - return { - media_id: result.media_id - }; } /**