Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
86758af5
authored
Mar 24, 2026
by
Archer
Committed by
GitHub
Mar 24, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feishu stream (#6629)
parent
25d6bb7d
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
580 additions
and
36 deletions
+580
-36
.claude/design/wechat-file-support.md
+539
-0
document/content/docs/self-host/upgrading/4-14/41410.mdx
+1
-0
document/data/doc-last-modified.json
+3
-2
packages/global/common/error/utils.ts
+4
-0
packages/service/common/api/axios.ts
+4
-0
packages/service/support/outLink/runtime/utils.ts
+28
-32
packages/service/support/outLink/wechat/mq.ts
+1
-2
No files found.
.claude/design/wechat-file-support.md
0 → 100644
View file @
86758af5
# 微信个人号文件接收方案设计
> 文件路径:`packages/service/support/outLink/wechat/`
## 一、背景与现状
### 当前架构
```
ILink API (长轮询) → mq.ts → messageParser.ts → outlinkInvokeChat()
```
-
**消息拉取**
:
`ILinkClient.getUpdates()`
长轮询,返回
`WeixinMessage[]`
-
**消息解析**
:
`messageParser.ts`
只处理
`text`
(type=1)和
`voice`
(type=3)
-
**查询构造**
:仅传递
`{ text: { content: group.text } }`
给 workflow
-
**文件支持**
:❌ 完全缺失
### FastGPT 已有基础设施
| 能力 | 实现位置 | 说明 |
|------|----------|------|
| 图片存储 |
`file/image/controller.ts`
→
`uploadMongoImg()`
| base64 存 MongoDB,返回内部 URL |
| 文件存储 |
`file/gridfs/`
(BucketNameEnum.chat) | GridFS 流式存储 |
| Chat 输入类型 |
`global/core/chat/type.ts`
→
`UserChatItemValueItemType`
| 支持
`text`
+
`file(image/file)`
|
### `UserChatItemValueItemType` 结构
```
typescript
type
UserChatItemValueItemType
=
{
text
?:
{
content
:
string
};
file
?:
{
type
:
'image'
|
'file'
;
// ChatFileTypeEnum
name
?:
string
;
key
?:
string
;
url
:
string
;
// 内部访问 URL
};
};
```
---
## 二、目标
支持微信用户通过 iLink 渠道向 FastGPT 发送:
| 类型 | 来源 | 处理方式 |
|------|------|----------|
| 图片 |
`item_list[].type = 2`
| 下载 → 存 MongoImage → 内部 URL |
| 文件/附件 |
`item_list[].type = 5`
| 下载 → 存 GridFS(chat) → 内部 URL |
| 视频 |
`item_list[].type = 4`
| 下载 → 存 GridFS(chat) → 内部 URL(可选)|
---
## 三、S3 存储方案
### 核心选择:`S3ChatSource.uploadChatFileByBuffer()`
| 字段 | 值 |
|------|----|
| 存储路径 |
`chat/{appId}/{uId}/{chatId}/{filename}`
|
| 存储桶 | Private Bucket(私有,需鉴权访问) |
| TTL | 1 小时(由
`MongoS3TTL`
自动清理) |
| 访问 URL |
`createGetChatFileURL({ key, expiredHours: 24 })`
生成预签名 URL |
### 调用关系
```
getS3ChatSource()
└── uploadChatFileByBuffer({ appId, chatId, uId, filename, buffer, contentType })
└── getFileS3Key.chat(...) → key = "chat/{appId}/{uId}/{chatId}/{filename}"
└── uploadFileByBuffer({ key, buffer, contentType })
├── MongoS3TTL.create({ expiredTime: +1h }) // 文件 1 小时后自动清理
├── client.uploadObject(...)
└── returns { key, accessUrl } // accessUrl 预签名 2h
```
### URL 传入 workflow
```
typescript
// UserChatItemValueItemType.file
{
type
:
'image'
|
'file'
,
// ChatFileTypeEnum
name
:
string
,
// 原始文件名
key
:
string
,
// S3 object key,用于后续删除/引用
url
:
string
// 预签名 URL(accessUrl,2h 内有效)
}
```
> 由于 workflow 在文件上传后立即执行,2 小时访问窗口完全够用。
---
## 四、ILink API 消息结构(推断)
基于 iLink WeChat Bot API 常见规范,图片/文件消息的
`item_list`
元素结构:
```
typescript
// 图片消息项(type = 2)
type
ImageMessageItem
=
{
type
:
2
;
image_item
?:
{
cdn_url
:
string
;
// 图片 CDN 直链(有效期有限)
aes_key
?:
string
;
// 加密 key(若内容加密)
file_size
?:
number
;
// 字节数
width
?:
number
;
height
?:
number
;
};
};
// 文件消息项(type = 5)
type
FileMessageItem
=
{
type
:
5
;
file_item
?:
{
file_id
?:
string
;
// 文件 ID(用于获取下载链接)
file_name
:
string
;
// 原始文件名
cdn_url
?:
string
;
// 文件 CDN 直链(有效期有限)
file_size
?:
number
;
file_ext
?:
string
;
// 扩展名 e.g. "pdf"
};
};
```
> **注意**:实际字段需与 iLink API 文档对齐。本方案设计为可扩展结构,字段名通过常量隔离,便于修正。
---
## 四、方案设计
### 4.1 整体流程
```
ILink getUpdates()
↓
MessageItem[]
↓ (messageParser)
ParsedMessageGroup {
userId, text, contextToken, msgIds,
files: [{ type, tempUrl, name }] ← 新增
}
↓ (mq.ts - processUserGroup)
downloadAndStoreFiles() ← 新增
↓
UserChatItemValueItemType[] = [
{ text: { content: "..." } },
{ file: { type: "image", url: "http://..." } },
{ file: { type: "file", url: "http://...", name: "report.pdf" } }
]
↓
outlinkInvokeChat({ query: [...] })
```
### 4.2 变更范围
共涉及
**4 个文件**
:
| 文件 | 变更类型 | 说明 |
|------|----------|------|
|
`ilinkClient.ts`
| 扩展类型 + 新增方法 | 添加图片/文件 Item 类型;添加
`downloadMedia()`
|
|
`messageParser.ts`
| 扩展逻辑 + 类型 | 解析图片/文件 item,收集
`tempUrl`
|
|
`wechat/fileHandler.ts`
|
**新建**
| 下载 → 存储 → 返回内部 URL |
|
`mq.ts`
| 扩展逻辑 | 调用 fileHandler,拼装 query 数组 |
---
## 五、详细设计
### 5.1 `ilinkClient.ts` — 扩展类型与下载能力
#### 新增类型
```
typescript
// 在 MessageItem 基础上扩展
export
type
MessageItem
=
{
type
:
number
;
text_item
?:
{
text
:
string
};
voice_item
?:
{
text
:
string
};
ref_msg
?:
{
title
?:
string
};
// 新增
image_item
?:
{
cdn_url
?:
string
;
file_size
?:
number
;
width
?:
number
;
height
?:
number
;
};
file_item
?:
{
file_id
?:
string
;
file_name
?:
string
;
cdn_url
?:
string
;
file_size
?:
number
;
file_ext
?:
string
;
};
};
// 媒体下载结果
export
type
MediaDownloadResult
=
{
buffer
:
Buffer
;
mimeType
:
string
;
fileName
?:
string
;
fileSize
:
number
;
};
```
#### 新增方法 `downloadMedia()`
```
typescript
// 最大允许下载文件大小(20MB)
const
MAX_MEDIA_SIZE
=
20
*
1024
*
1024
;
const
DOWNLOAD_TIMEOUT_MS
=
30
_000
;
async
downloadMedia
(
url
:
string
,
fileName
?:
string
):
Promise
<
MediaDownloadResult
>
{
const
controller
=
new
AbortController
();
const
timer
=
setTimeout
(()
=>
controller
.
abort
(),
DOWNLOAD_TIMEOUT_MS
);
try
{
const
res
=
await
fetch
(
url
,
{
headers
:
this
.
buildHeaders
(),
// 携带 token 鉴权
signal
:
controller
.
signal
});
clearTimeout
(
timer
);
if
(
!
res
.
ok
)
throw
new
Error
(
`Media download failed: HTTP
${
res
.
status
}
`
);
const
contentLength
=
Number
(
res
.
headers
.
get
(
'content-length'
)
||
0
);
if
(
contentLength
>
MAX_MEDIA_SIZE
)
{
throw
new
Error
(
`File too large:
${
contentLength
}
bytes`
);
}
const
arrayBuffer
=
await
res
.
arrayBuffer
();
const
buffer
=
Buffer
.
from
(
arrayBuffer
);
if
(
buffer
.
length
>
MAX_MEDIA_SIZE
)
{
throw
new
Error
(
`File too large:
${
buffer
.
length
}
bytes`
);
}
const
contentType
=
res
.
headers
.
get
(
'content-type'
)
||
'application/octet-stream'
;
const
mimeType
=
contentType
.
split
(
';'
)[
0
].
trim
();
return
{
buffer
,
mimeType
,
fileName
,
fileSize
:
buffer
.
length
};
}
catch
(
err
)
{
clearTimeout
(
timer
);
throw
err
;
}
}
```
---
### 5.2 `messageParser.ts` — 扩展解析逻辑
#### 新增常量与类型
```
typescript
const
MSG_ITEM_IMAGE
=
2
;
const
MSG_ITEM_FILE
=
5
;
// 解析出的媒体引用(尚未下载,只存 URL 和元信息)
export
type
ParsedMediaRef
=
{
type
:
'image'
|
'file'
;
tempUrl
:
string
;
// iLink CDN URL(有效期有限)
name
?:
string
;
// 原始文件名
};
// 扩展 ParsedMessageGroup
export
type
ParsedMessageGroup
=
{
userId
:
string
;
text
:
string
;
contextToken
:
string
;
msgIds
:
string
[];
mediaRefs
:
ParsedMediaRef
[];
// 新增:待下载的媒体引用
};
```
#### 更新 `extractTextFromItem` 和 `groupMessagesByUser`
```
typescript
// extractTextFromItem 保持不变,仍只返回文本
// 新增:从 item 提取媒体引用
export
function
extractMediaRefFromItem
(
item
:
NonNullable
<
WeixinMessage
[
'item_list'
]
>
[
number
]
):
ParsedMediaRef
|
null
{
if
(
item
.
type
===
MSG_ITEM_IMAGE
&&
item
.
image_item
?.
cdn_url
)
{
return
{
type
:
'image'
,
tempUrl
:
item
.
image_item
.
cdn_url
,
name
:
undefined
};
}
if
(
item
.
type
===
MSG_ITEM_FILE
&&
item
.
file_item
?.
cdn_url
)
{
return
{
type
:
'file'
,
tempUrl
:
item
.
file_item
.
cdn_url
,
name
:
item
.
file_item
.
file_name
};
}
return
null
;
}
```
在
`groupMessagesByUser`
中:
```
typescript
// 处理逻辑变更:消息中若包含文件但无文本,也纳入分组(原逻辑会跳过无文本消息)
for
(
const
msg
of
msgs
)
{
if
(
msg
.
message_type
!==
MSG_TYPE_USER
)
continue
;
let
text
=
''
;
const
mediaRefs
:
ParsedMediaRef
[]
=
[];
for
(
const
item
of
msg
.
item_list
??
[])
{
if
(
!
text
)
{
const
t
=
extractTextFromItem
(
item
);
if
(
t
)
text
=
t
;
}
const
ref
=
extractMediaRefFromItem
(
item
);
if
(
ref
)
mediaRefs
.
push
(
ref
);
}
// 关键变更:无文本但有媒体时也处理
if
(
!
text
&&
mediaRefs
.
length
===
0
)
continue
;
// ... 合并到分组(mediaRefs 追加合并)
}
```
---
### 5.3 `wechat/fileHandler.ts` — 新建文件处理模块
图片和文件
**统一存入 S3 私有桶**
(
`chat`
source),通过
`S3ChatSource.uploadChatFileByBuffer()`
写入,
上传完成后用
`createGetChatFileURL()`
生成预签名 URL 传给 workflow。
```
typescript
import
type
{
MediaDownloadResult
}
from
'./ilinkClient'
;
import
{
getS3ChatSource
}
from
'../../../common/s3/sources/chat'
;
import
type
{
UserChatItemFileItemType
}
from
'@fastgpt/global/core/chat/type'
;
import
{
ChatFileTypeEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
mime
from
'mime-types'
;
export
type
MediaStoreResult
=
UserChatItemFileItemType
;
/**
* 统一入口:下载 iLink CDN 文件 → 存入 S3(chat) → 返回预签名 URL
*
* S3 key: chat/{appId}/{uId}/{chatId}/{filename}
* TTL: 文件 1h(MongoS3TTL 自动清理);预签名 URL 24h
*/
export
async
function
downloadAndStoreMedia
(
params
:
{
type
:
'image'
|
'file'
;
tempUrl
:
string
;
name
?:
string
;
appId
:
string
;
chatId
:
string
;
uId
:
string
;
downloadFn
:
(
url
:
string
,
name
?:
string
)
=>
Promise
<
MediaDownloadResult
>
;
}):
Promise
<
MediaStoreResult
|
null
>
{
const
{
type
,
tempUrl
,
name
,
appId
,
chatId
,
uId
,
downloadFn
}
=
params
;
try
{
const
media
=
await
downloadFn
(
tempUrl
,
name
);
const
ext
=
mime
.
extension
(
media
.
mimeType
)
||
'bin'
;
const
filename
=
media
.
fileName
||
`wechat_
${
type
}
_
${
Date
.
now
()}
.
${
ext
}
`
;
const
chatSource
=
getS3ChatSource
();
// 上传到 S3,返回 { key, accessUrl }
// accessUrl 是 createExternalUrl({ key, expiredHours: 2 }) 的预签名 URL
const
{
key
,
accessUrl
}
=
await
chatSource
.
uploadChatFileByBuffer
({
appId
,
chatId
,
uId
,
filename
,
buffer
:
media
.
buffer
,
contentType
:
media
.
mimeType
});
// 生成更长有效期的预签名 URL 供 workflow 访问(24h)
const
url
=
await
chatSource
.
createGetChatFileURL
({
key
,
expiredHours
:
24
,
external
:
false
});
return
{
type
:
type
===
'image'
?
ChatFileTypeEnum
.
image
:
ChatFileTypeEnum
.
file
,
name
:
filename
,
key
,
url
};
}
catch
(
error
)
{
// 单个文件失败不中断整体流程,上层记录日志
return
null
;
}
}
```
---
### 5.4 `mq.ts` — 扩展 processUserGroup
```
typescript
async
function
processUserGroup
(
outLink
:
OutLinkSchema
<
WechatAppType
>
,
group
:
ParsedMessageGroup
):
Promise
<
void
>
{
const
app
=
outLink
.
app
;
const
chatId
=
`wechat_
${
outLink
.
shareId
}
_
${
group
.
userId
}
`
;
const
client
=
new
ILinkClient
(
app
.
baseUrl
,
app
.
token
);
// 1. 并发下载所有媒体文件(最多 5 个,防止超载)
const
maxFiles
=
5
;
const
mediaRefs
=
group
.
mediaRefs
.
slice
(
0
,
maxFiles
);
const
fileResults
=
await
Promise
.
allSettled
(
mediaRefs
.
map
((
ref
)
=>
downloadAndStoreMedia
({
type
:
ref
.
type
,
tempUrl
:
ref
.
tempUrl
,
name
:
ref
.
name
,
teamId
:
outLink
.
teamId
,
shareId
:
outLink
.
shareId
,
downloadFn
:
(
url
,
name
)
=>
client
.
downloadMedia
(
url
,
name
)
})
)
);
// 2. 构造 UserChatItemValueItemType[]
const
query
:
UserChatItemValueItemType
[]
=
[];
// 文本(如有)
if
(
group
.
text
)
{
query
.
push
({
text
:
{
content
:
group
.
text
}
});
}
// 成功上传的文件
for
(
const
result
of
fileResults
)
{
if
(
result
.
status
===
'fulfilled'
&&
result
.
value
)
{
query
.
push
({
file
:
result
.
value
});
}
}
// 若 query 为空(全部失败),回复提示
if
(
query
.
length
===
0
)
{
await
client
.
sendMessage
({
to_user_id
:
group
.
userId
,
text
:
'文件处理失败,请重试。'
,
context_token
:
group
.
contextToken
});
return
;
}
try
{
await
outlinkInvokeChat
({
outLinkConfig
:
outLink
,
chatId
,
query
,
messageId
:
group
.
msgIds
[
group
.
msgIds
.
length
-
1
],
chatUserId
:
group
.
userId
,
replyCallback
:
async
(
replyContent
:
string
)
=>
{
await
client
.
sendMessage
({
to_user_id
:
group
.
userId
,
text
:
replyContent
,
context_token
:
group
.
contextToken
});
return
{
errcode
:
0
};
}
});
}
catch
(
error
)
{
// 错误处理(原有逻辑不变)
...
}
}
```
---
## 六、文件大小与类型限制
| 配置项 | 建议值 | 说明 |
|--------|--------|------|
| 单文件最大 | 20 MB | WeChat 个人号文件上限通常 ≤ 20MB |
| 单消息最多文件 | 5 个 | 防止并发过多 |
| 允许图片 MIME |
`image/*`
| 统一走图片存储 |
| 允许文件类型 | 不限制(由 workflow 处理) | GridFS 存储,workflow 层可再过滤 |
| 下载超时 | 30s | 防止 CDN 链接失效导致卡死 |
---
## 七、错误处理策略
| 场景 | 处理方式 |
|------|----------|
| CDN URL 过期 / 无效 | 跳过该文件,继续处理文本;文件全部失败时回复提示 |
| 文件过大 | 抛出错误,日志记录,回复"文件过大"提示 |
| 图片 MIME 不合法 |
`uploadMongoImg`
内部校验,返回 null,跳过 |
| 单文件下载超时 | AbortController 控制,记录日志后 null |
| 全部文件失败但有文本 | 仍走 workflow,文件部分缺失 |
| 全部内容失败 | 发送固定错误消息给用户 |
---
## 八、待确认事项
在开始编码前,需要确认以下 iLink API 细节:
1.
**图片消息 item type 值**
:是否为
`2`
?字段名是否为
`image_item`
?
2.
**文件消息 item type 值**
:是否为
`5`
?字段名是否为
`file_item`
?
3.
**CDN URL 有效期**
:URL 可以直接 fetch 还是需要携带 token 鉴权?
4.
**是否有专用下载 API**
:是否有类似
`/ilink/bot/getmedia?file_id=xxx`
的接口?
5.
**文件大小限制**
:iLink 对单消息文件大小有何限制?
---
## 九、新增文件清单
```
packages/service/support/outLink/wechat/
├── ilinkClient.ts 修改:扩展 MessageItem 类型,新增 downloadMedia()
├── messageParser.ts 修改:新增 extractMediaRefFromItem(),扩展 ParsedMessageGroup
├── fileHandler.ts 新建:下载 + 存储媒体文件
├── mq.ts 修改:调用 fileHandler,拼装 query
└── type.ts 保持不变
```
---
## 十、测试用例设计
| 用例 | 输入 | 期望输出 |
|------|------|----------|
| 纯文字消息 | text item | 不变,query=
[
text
]
|
| 图片消息 | image item + 无文本 | query=
[
file(image)
]
|
| 图片 + 文字 | text + image | query=
[
text, file(image)
]
|
| 文件附件 | file item | query=
[
file(file)
]
|
| 多文件 | 3 个图片 | query=
[
file, file, file
]
|
| 超大文件 | 文件 > 20MB | 跳过,回复提示 |
| CDN 失效 | 404 URL | 跳过,文本正常走 |
| 全部文件失败 + 无文本 | 空 | 发送错误提示 |
document/content/docs/self-host/upgrading/4-14/41410.mdx
View file @
86758af5
...
@@ -6,6 +6,7 @@ description: 'FastGPT V4.14.10 更新说明'
...
@@ -6,6 +6,7 @@ description: 'FastGPT V4.14.10 更新说明'
## 🚀 新增内容
## 🚀 新增内容
1. 飞书发布渠道,支持流输出。
## ⚙️ 优化
## ⚙️ 优化
...
...
document/data/doc-last-modified.json
View file @
86758af5
...
@@ -220,6 +220,7 @@
...
@@ -220,6 +220,7 @@
"document/content/docs/self-host/upgrading/4-14/4140.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4140.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4141.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4141.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41410.mdx"
:
"2026-03-24T09:59:30+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"
,
...
@@ -239,7 +240,7 @@
...
@@ -239,7 +240,7 @@
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx"
:
"2026-03-09T12:02:02+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx"
:
"2026-03-09T12:02:02+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/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-03-23T23:3
6:27
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4149.mdx"
:
"2026-03-23T23:3
7:12
+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"
,
...
@@ -381,7 +382,7 @@
...
@@ -381,7 +382,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-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/toc.en.mdx"
:
"2026-03-23T12:10:44+08:00"
,
"document/content/docs/toc.en.mdx"
:
"2026-03-23T12:10:44+08:00"
,
"document/content/docs/toc.mdx"
:
"2026-03-2
3T11:57:05
+08:00"
,
"document/content/docs/toc.mdx"
:
"2026-03-2
4T09:59: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.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"
,
...
...
packages/global/common/error/utils.ts
View file @
86758af5
...
@@ -28,6 +28,10 @@ export const getErrText = (err: any, def = ''): any => {
...
@@ -28,6 +28,10 @@ export const getErrText = (err: any, def = ''): any => {
return
replaceSensitiveText
(
msg
);
return
replaceSensitiveText
(
msg
);
};
};
export
const
getErrResponse
=
(
err
:
any
):
any
=>
{
return
err
?.
response
?.
data
||
err
?.
response
||
err
;
};
export
class
UserError
extends
Error
{
export
class
UserError
extends
Error
{
constructor
(
message
:
string
)
{
constructor
(
message
:
string
)
{
super
(
message
);
super
(
message
);
...
...
packages/service/common/api/axios.ts
View file @
86758af5
import
_
,
{
type
AxiosRequestConfig
}
from
'axios'
;
import
_
,
{
type
AxiosRequestConfig
}
from
'axios'
;
import
{
ProxyAgent
}
from
'proxy-agent'
;
import
{
ProxyAgent
}
from
'proxy-agent'
;
import
{
isDevEnv
}
from
'@fastgpt/global/common/system/constants'
;
export
function
createProxyAxios
(
config
?:
AxiosRequestConfig
)
{
export
function
createProxyAxios
(
config
?:
AxiosRequestConfig
)
{
if
(
isDevEnv
)
{
return
_
.
create
(
config
);
}
const
agent
=
new
ProxyAgent
();
const
agent
=
new
ProxyAgent
();
return
_
.
create
({
return
_
.
create
({
...
...
packages/service/support/outLink/runtime/utils.ts
View file @
86758af5
...
@@ -21,7 +21,7 @@ import { authOutLinkLimit } from './auth';
...
@@ -21,7 +21,7 @@ import { authOutLinkLimit } from './auth';
import
{
addOutLinkUsage
}
from
'../../../support/outLink/tools'
;
import
{
addOutLinkUsage
}
from
'../../../support/outLink/tools'
;
import
{
getLogger
,
LogCategories
}
from
'../../../common/logger'
;
import
{
getLogger
,
LogCategories
}
from
'../../../common/logger'
;
import
{
appendRedisCache
}
from
'../../../common/redis/cache'
;
import
{
appendRedisCache
}
from
'../../../common/redis/cache'
;
import
{
getErrText
}
from
'@fastgpt/global/common/error/utils'
;
import
{
getErr
Response
,
getErr
Text
}
from
'@fastgpt/global/common/error/utils'
;
import
{
getUsageSourceByPublishChannel
}
from
'@fastgpt/global/support/wallet/usage/tools'
;
import
{
getUsageSourceByPublishChannel
}
from
'@fastgpt/global/support/wallet/usage/tools'
;
import
{
getChatSourceByPublishChannel
}
from
'@fastgpt/global/core/chat/utils'
;
import
{
getChatSourceByPublishChannel
}
from
'@fastgpt/global/core/chat/utils'
;
import
{
WORKFLOW_MAX_RUN_TIMES
}
from
'../../../core/workflow/constants'
;
import
{
WORKFLOW_MAX_RUN_TIMES
}
from
'../../../core/workflow/constants'
;
...
@@ -75,7 +75,10 @@ export type outLinkInvokeChatProps<T extends OutlinkAppType> = {
...
@@ -75,7 +75,10 @@ export type outLinkInvokeChatProps<T extends OutlinkAppType> = {
res
?:
NextApiResponse
;
res
?:
NextApiResponse
;
messageId
:
string
;
messageId
:
string
;
chatUserId
:
string
;
chatUserId
:
string
;
replyCallback
?:
(
replyContent
:
string
)
=>
Promise
<
any
>
;
// Called once with complete response content (all channels except wecom)
onReply
?:
(
replyContent
:
string
)
=>
Promise
<
void
>
;
// Called for each streaming chunk (feishu and other push channels)
onStreamChunk
?:
(
text
:
string
)
=>
Promise
<
void
>
;
streamId
?:
string
;
streamId
?:
string
;
};
};
...
@@ -91,7 +94,8 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -91,7 +94,8 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
res
,
res
,
messageId
,
messageId
,
chatUserId
,
chatUserId
,
replyCallback
,
onReply
,
onStreamChunk
,
streamId
streamId
}:
outLinkInvokeChatProps
<
T
>
)
{
}:
outLinkInvokeChatProps
<
T
>
)
{
const
streamResKey
=
`
${
STREAM_CACHE_KEY_PREFIX
}${
streamId
}
`
;
const
streamResKey
=
`
${
STREAM_CACHE_KEY_PREFIX
}${
streamId
}
`
;
...
@@ -112,7 +116,7 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -112,7 +116,7 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
const
userQuestion
=
query
.
find
((
item
)
=>
item
.
text
)?.
text
?.
content
||
''
;
const
userQuestion
=
query
.
find
((
item
)
=>
item
.
text
)?.
text
?.
content
||
''
;
if
(
RESET_CHAT_INPUT
[
userQuestion
])
{
if
(
RESET_CHAT_INPUT
[
userQuestion
])
{
await
resetChat
({
appId
:
outLinkConfig
.
appId
,
chatId
});
await
resetChat
({
appId
:
outLinkConfig
.
appId
,
chatId
});
await
replyCallback
?.(
RESET_CHAT_REPLY
);
await
onReply
?.(
RESET_CHAT_REPLY
);
if
(
streamId
)
{
if
(
streamId
)
{
await
appendRedisCache
(
streamResKey
,
RESET_CHAT_REPLY
,
60
);
await
appendRedisCache
(
streamResKey
,
RESET_CHAT_REPLY
,
60
);
await
appendRedisCache
(
streamResKey
,
STREAM_END_FLAG
,
60
);
await
appendRedisCache
(
streamResKey
,
STREAM_END_FLAG
,
60
);
...
@@ -144,7 +148,9 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -144,7 +148,9 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
ip
:
chatId
ip
:
chatId
});
});
const
workflowStreamResponse
=
streamId
const
enableStreaming
=
!!
streamId
||
!!
onStreamChunk
;
const
workflowStreamResponse
=
enableStreaming
?
async
({
?
async
({
write
,
write
,
event
,
event
,
...
@@ -158,8 +164,13 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -158,8 +164,13 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
try
{
try
{
const
text
=
data
.
choices
?.[
0
]?.
delta
?.
content
;
const
text
=
data
.
choices
?.[
0
]?.
delta
?.
content
;
if
(
text
)
{
if
(
text
)
{
if
(
streamId
)
{
await
appendRedisCache
(
streamResKey
,
text
,
60
);
await
appendRedisCache
(
streamResKey
,
text
,
60
);
}
}
if
(
onStreamChunk
)
{
await
onStreamChunk
(
text
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
logger
.
error
(
'Outlink real-time streaming failed'
,
{
logger
.
error
(
'Outlink real-time streaming failed'
,
{
streamId
,
streamId
,
...
@@ -171,8 +182,10 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -171,8 +182,10 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
}
}
:
undefined
;
:
undefined
;
// Append first
// Initialize Redis key only when needed
if
(
streamId
)
{
await
appendRedisCache
(
streamResKey
,
''
,
120
);
await
appendRedisCache
(
streamResKey
,
''
,
120
);
}
// Merge global variables from database
// Merge global variables from database
const
variables
=
chatDetail
?.
variables
??
{};
const
variables
=
chatDetail
?.
variables
??
{};
...
@@ -202,7 +215,7 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -202,7 +215,7 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
histories
,
histories
,
query
:
query
,
query
:
query
,
chatConfig
,
chatConfig
,
stream
:
streamId
?
true
:
false
,
stream
:
enableStreaming
,
workflowStreamResponse
,
workflowStreamResponse
,
runtimeEdges
:
storeEdges2RuntimeEdges
(
edges
),
runtimeEdges
:
storeEdges2RuntimeEdges
(
edges
),
runtimeNodes
:
storeNodes2RuntimeNodes
(
nodes
,
getWorkflowEntryNodeIds
(
nodes
)),
runtimeNodes
:
storeNodes2RuntimeNodes
(
nodes
,
getWorkflowEntryNodeIds
(
nodes
)),
...
@@ -225,31 +238,13 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -225,31 +238,13 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
const
replyResult
=
await
(
async
()
=>
{
const
replyResult
=
await
(
async
()
=>
{
try
{
try
{
if
(
streamId
)
{
if
(
streamId
)
{
// if streamId is provided, do not reply
// wecom model: Redis handles streaming, no reply needed
return
;
return
{
success
:
true
};
}
const
result
=
await
replyCallback
?.(
responseContent
);
if
(
result
.
errcode
!==
0
)
{
logger
.
error
(
'Outlink official account reply failed'
,
{
errmsg
:
result
.
errmsg
});
return
{
success
:
false
,
errmsg
:
result
.
errmsg
};
}
}
await
onReply
?.(
responseContent
);
logger
.
debug
(
'Outlink official account reply success'
,
{
return
{
success
:
true
};
responseContent
,
result
});
return
{
success
:
true
,
data
:
result
};
}
catch
(
error
)
{
}
catch
(
error
)
{
logger
.
error
(
'Outlink reply callback failed'
,
{
error
});
logger
.
error
(
'Outlink reply callback failed'
,
{
error
:
getErrResponse
(
error
)
});
return
{
return
{
success
:
false
,
success
:
false
,
errmsg
:
getErrText
(
error
)
errmsg
:
getErrText
(
error
)
...
@@ -306,9 +301,10 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
...
@@ -306,9 +301,10 @@ export async function outlinkInvokeChat<T extends OutlinkAppType>({
});
});
try
{
try
{
if
(
streamId
)
{
await
appendRedisCache
(
streamResKey
,
STREAM_END_FLAG
,
60
);
await
appendRedisCache
(
streamResKey
,
STREAM_END_FLAG
,
60
);
}
await
replyCallback
?.(
`App run error:
${
getErrText
(
error
)}
`
);
await
onReply
?.(
`App run error:
${
getErrText
(
error
)}
`
);
}
catch
(
error
)
{
}
catch
(
error
)
{
logger
.
error
(
'Outlink invoke chat fallback reply failed'
,
{
logger
.
error
(
'Outlink invoke chat fallback reply failed'
,
{
shareId
:
outLinkConfig
.
shareId
,
shareId
:
outLinkConfig
.
shareId
,
...
...
packages/service/support/outLink/wechat/mq.ts
View file @
86758af5
...
@@ -126,13 +126,12 @@ async function processUserGroup(
...
@@ -126,13 +126,12 @@ async function processUserGroup(
query
:
[{
text
:
{
content
:
group
.
text
}
}],
query
:
[{
text
:
{
content
:
group
.
text
}
}],
messageId
:
group
.
msgIds
[
group
.
msgIds
.
length
-
1
],
messageId
:
group
.
msgIds
[
group
.
msgIds
.
length
-
1
],
chatUserId
:
group
.
userId
,
chatUserId
:
group
.
userId
,
replyCallback
:
async
(
replyContent
:
string
)
=>
{
onReply
:
async
(
replyContent
:
string
)
=>
{
await
client
.
sendMessage
({
await
client
.
sendMessage
({
to_user_id
:
group
.
userId
,
to_user_id
:
group
.
userId
,
text
:
replyContent
,
text
:
replyContent
,
context_token
:
group
.
contextToken
context_token
:
group
.
contextToken
});
});
return
{
errcode
:
0
};
}
}
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment