Commit a5cd8229 by Archer Committed by GitHub

perf: wechat channel (#6800)

* perf: wechat channel

* fix: review
parent b2cfada9
---
title: 'V4.14.14'
description: 'FastGPT V4.14.14 更新说明'
---
## 升级指南
### 1. 更新镜像 tag
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.14.14
## 🐛 修复
## ⚙️ 优化
1. 个人微信发布渠道,优化轮询策略(拉取与回复解耦),避免数据量超大时出现阻塞。
2. 新增环境变量 `WECHAT_CHANNEL_CONCURRENCY`(默认 1000)用于控制微信渠道 poll worker 并发数,建议 ≥ online channel 峰值。
\ No newline at end of file
...@@ -117,6 +117,7 @@ description: FastGPT 文档目录 ...@@ -117,6 +117,7 @@ description: FastGPT 文档目录
- [/docs/self-host/upgrading/4-14/41411](/docs/self-host/upgrading/4-14/41411) - [/docs/self-host/upgrading/4-14/41411](/docs/self-host/upgrading/4-14/41411)
- [/docs/self-host/upgrading/4-14/41412](/docs/self-host/upgrading/4-14/41412) - [/docs/self-host/upgrading/4-14/41412](/docs/self-host/upgrading/4-14/41412)
- [/docs/self-host/upgrading/4-14/41413](/docs/self-host/upgrading/4-14/41413) - [/docs/self-host/upgrading/4-14/41413](/docs/self-host/upgrading/4-14/41413)
- [/docs/self-host/upgrading/4-14/41414](/docs/self-host/upgrading/4-14/41414)
- [/docs/self-host/upgrading/4-14/4142](/docs/self-host/upgrading/4-14/4142) - [/docs/self-host/upgrading/4-14/4142](/docs/self-host/upgrading/4-14/4142)
- [/docs/self-host/upgrading/4-14/4143](/docs/self-host/upgrading/4-14/4143) - [/docs/self-host/upgrading/4-14/4143](/docs/self-host/upgrading/4-14/4143)
- [/docs/self-host/upgrading/4-14/4144](/docs/self-host/upgrading/4-14/4144) - [/docs/self-host/upgrading/4-14/4144](/docs/self-host/upgrading/4-14/4144)
......
...@@ -230,6 +230,7 @@ ...@@ -230,6 +230,7 @@
"document/content/docs/self-host/upgrading/4-14/41412.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41412.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41413.en.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41413.en.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41413.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/self-host/upgrading/4-14/41413.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/self-host/upgrading/4-14/41414.mdx": "2026-04-22T16:09:29+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
...@@ -250,6 +251,7 @@ ...@@ -250,6 +251,7 @@
"document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00", "document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.en.mdx": "2026-03-23T12:17:04+08:00", "document/content/docs/self-host/upgrading/4-14/4149.en.mdx": "2026-03-23T12:17:04+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-04-07T21:01:52+08:00", "document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-04-07T21:01:52+08:00",
"document/content/docs/self-host/upgrading/4-15/4150.mdx": "2026-04-22T14:36:14+08:00",
"document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00",
...@@ -391,7 +393,7 @@ ...@@ -391,7 +393,7 @@
"document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-04-20T13:51:34+08:00", "document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-04-20T13:51:34+08:00",
"document/content/docs/toc.en.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/toc.en.mdx": "2026-04-21T23:04:26+08:00",
"document/content/docs/toc.mdx": "2026-04-21T23:04:26+08:00", "document/content/docs/toc.mdx": "2026-04-22T16:09:29+08:00",
"document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00",
......
...@@ -36,6 +36,7 @@ export enum QueueNames { ...@@ -36,6 +36,7 @@ export enum QueueNames {
// Publish // Publish
wechatPoll = 'wechatPoll', wechatPoll = 'wechatPoll',
wechatReply = 'wechatReply',
/** @deprecated */ /** @deprecated */
websiteSync = 'websiteSync' websiteSync = 'websiteSync'
......
...@@ -113,6 +113,12 @@ export const env = createEnv({ ...@@ -113,6 +113,12 @@ export const env = createEnv({
/** Redis 内存水位检测缓存时长(毫秒),避免每个流请求都调用 INFO MEMORY */ /** Redis 内存水位检测缓存时长(毫秒),避免每个流请求都调用 INFO MEMORY */
STREAM_RESUME_REDIS_MEMORY_CHECK_INTERVAL_MS: IntSchema.positive().default(5000), STREAM_RESUME_REDIS_MEMORY_CHECK_INTERVAL_MS: IntSchema.positive().default(5000),
// ===== Wechat outLink =====
/** 微信渠道 poll worker 并发数,需 ≥ online shareId 峰值,否则消息延迟会线性恶化 */
WECHAT_CHANNEL_CONCURRENCY: NumSchema.int().positive().default(1000).meta({
description: '微信渠道 poll worker 并发数'
}),
// Beta features // Beta features
// Whether the Skill feature is enabled (frontend entries + backend runtime) // Whether the Skill feature is enabled (frontend entries + backend runtime)
SHOW_SKILL: BoolSchema.default(false), SHOW_SKILL: BoolSchema.default(false),
......
...@@ -8,7 +8,7 @@ export type ParsedMessageGroup = { ...@@ -8,7 +8,7 @@ export type ParsedMessageGroup = {
userId: string; userId: string;
text: string; text: string;
contextToken: string; contextToken: string;
msgIds: string[]; lastMsgId: string;
}; };
export function extractTextFromItem(item: NonNullable<WeixinMessage['item_list']>[number]): string { export function extractTextFromItem(item: NonNullable<WeixinMessage['item_list']>[number]): string {
...@@ -42,11 +42,11 @@ export function groupMessagesByUser(msgs: WeixinMessage[]): ParsedMessageGroup[] ...@@ -42,11 +42,11 @@ export function groupMessagesByUser(msgs: WeixinMessage[]): ParsedMessageGroup[]
if (!text) continue; if (!text) continue;
const userId = msg.from_user_id ?? 'unknown'; const userId = msg.from_user_id ?? 'unknown';
const existing = groups.get(userId); const existing = groups.get(userId);
if (existing) { if (existing) {
existing.text += '\n' + text; existing.text += '\n' + text;
existing.msgIds.push(msg.msgid); existing.lastMsgId = msg.msgid;
if (msg.context_token) { if (msg.context_token) {
existing.contextToken = msg.context_token; existing.contextToken = msg.context_token;
} }
...@@ -55,7 +55,7 @@ export function groupMessagesByUser(msgs: WeixinMessage[]): ParsedMessageGroup[] ...@@ -55,7 +55,7 @@ export function groupMessagesByUser(msgs: WeixinMessage[]): ParsedMessageGroup[]
userId, userId,
text, text,
contextToken: msg.context_token ?? '', contextToken: msg.context_token ?? '',
msgIds: [msg.msgid] lastMsgId: msg.msgid
}); });
} }
} }
......
export type WechatPollJobData = { export type WechatPollJobData = {
shareId: string; shareId: string;
}; };
export type WechatReplyJobData = {
shareId: string;
userId: string;
text: string;
contextToken: string;
lastMsgId: string;
};
...@@ -160,6 +160,8 @@ CHECK_INTERNAL_IP=false ...@@ -160,6 +160,8 @@ CHECK_INTERNAL_IP=false
APP_FOLDER_MAX_AMOUNT=1000 APP_FOLDER_MAX_AMOUNT=1000
# 数据集文件夹最大数量 # 数据集文件夹最大数量
DATASET_FOLDER_MAX_AMOUNT=1000 DATASET_FOLDER_MAX_AMOUNT=1000
# 微信渠道 poll worker 并发数(默认 1000),需 ≥ online channel 数;channel 数超过该值时消息延迟会线性恶化
WECHAT_CHANNEL_CONCURRENCY=1000
# ==================== 上传与账号策略 ==================== # ==================== 上传与账号策略 ====================
# 最大上传文件大小(MB) # 最大上传文件大小(MB)
......
...@@ -10,10 +10,9 @@ import { useDebounceEffect, useMount } from 'ahooks'; ...@@ -10,10 +10,9 @@ import { useDebounceEffect, useMount } from 'ahooks';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt'; import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants'; import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow, appWorkflow2Form } from './utils'; import { form2AppWorkflow, appWorkflow2Form } from './utils';
import PublishChannel from '../../Publish';
const Edit = dynamic(() => import('./Edit')); import Logs from '../../Logs';
const Logs = dynamic(() => import('../../Logs/index')); import Edit from './Edit';
const PublishChannel = dynamic(() => import('../../Publish'));
const SimpleEdit = () => { const SimpleEdit = () => {
const { t } = useTranslation(); const { t } = useTranslation();
......
...@@ -6,6 +6,7 @@ import { NextAPI } from '@/service/middleware/entry'; ...@@ -6,6 +6,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util';
import { stopWechatPolling } from '@fastgpt/service/support/outLink/wechat/mq';
export type OutLinkDeleteQuery = { export type OutLinkDeleteQuery = {
id: string; id: string;
...@@ -25,7 +26,15 @@ async function handler( ...@@ -25,7 +26,15 @@ async function handler(
per: OwnerPermissionVal per: OwnerPermissionVal
}); });
await MongoOutLink.findByIdAndDelete(id); const outlink = await MongoOutLink.findById(id);
if (outlink && outlink.type === 'wechat') {
await stopWechatPolling(outlink.shareId).catch((error) => {
console.warn('Stop wechat polling failed', error);
});
}
await outlink?.deleteOne();
(async () => { (async () => {
addAuditLog({ addAuditLog({
......
...@@ -61,7 +61,7 @@ describe('groupMessagesByUser', () => { ...@@ -61,7 +61,7 @@ describe('groupMessagesByUser', () => {
userId: 'u1', userId: 'u1',
text: 'default text', text: 'default text',
contextToken: 'ctx1', contextToken: 'ctx1',
msgIds: ['m1'] lastMsgId: 'm1'
}); });
}); });
...@@ -84,7 +84,7 @@ describe('groupMessagesByUser', () => { ...@@ -84,7 +84,7 @@ describe('groupMessagesByUser', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].text).toBe('first\nsecond'); expect(result[0].text).toBe('first\nsecond');
expect(result[0].msgIds).toEqual(['m1', 'm2']); expect(result[0].lastMsgId).toBe('m2');
expect(result[0].contextToken).toBe('ctx2'); // 取最后一条的 expect(result[0].contextToken).toBe('ctx2'); // 取最后一条的
}); });
...@@ -107,7 +107,7 @@ describe('groupMessagesByUser', () => { ...@@ -107,7 +107,7 @@ describe('groupMessagesByUser', () => {
const result = groupMessagesByUser(msgs); const result = groupMessagesByUser(msgs);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].msgIds).toEqual(['m2']); expect(result[0].lastMsgId).toBe('m2');
}); });
it('should skip messages with no extractable text', () => { it('should skip messages with no extractable text', () => {
...@@ -122,7 +122,7 @@ describe('groupMessagesByUser', () => { ...@@ -122,7 +122,7 @@ describe('groupMessagesByUser', () => {
const result = groupMessagesByUser(msgs); const result = groupMessagesByUser(msgs);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].msgIds).toEqual(['m2']); expect(result[0].lastMsgId).toBe('m2');
}); });
it('should handle empty message list', () => { it('should handle empty message list', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment