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
81d39199
authored
Jul 06, 2026
by
Archer
Committed by
GitHub
Jul 06, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: secure wechat outlink binding (#7260)
parent
1862abfa
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
343 additions
and
51 deletions
+343
-51
packages/global/openapi/support/outLink/api.ts
+63
-0
packages/global/openapi/support/outLink/index.ts
+75
-1
packages/service/support/outLink/wechat/ilinkClient.ts
+24
-6
packages/service/support/outLink/wechat/mq.ts
+37
-2
packages/service/support/outLink/wechat/qrcode.ts
+13
-0
packages/service/support/outLink/wechat/utils.ts
+15
-0
projects/app/src/pageComponents/app/detail/Publish/Wechat/QRLoginModal.tsx
+10
-9
projects/app/src/pageComponents/app/detail/Publish/Wechat/index.tsx
+8
-8
projects/app/src/pages/api/support/outLink/wechat/logout.ts
+27
-6
projects/app/src/pages/api/support/outLink/wechat/qrcode/generate.ts
+34
-10
projects/app/src/pages/api/support/outLink/wechat/qrcode/status.ts
+37
-9
No files found.
packages/global/openapi/support/outLink/api.ts
View file @
81d39199
...
...
@@ -158,3 +158,66 @@ export const OutLinkDeleteResponseSchema = z.undefined().meta({
description
:
'删除成功'
});
export
type
OutLinkDeleteResponseType
=
z
.
infer
<
typeof
OutLinkDeleteResponseSchema
>
;
const
WechatOutLinkIdSchema
=
ObjectIdSchema
.
meta
({
description
:
'微信发布渠道 ID'
});
/* ============================================================================
* API: 生成微信发布渠道登录二维码
* Route: POST /api/support/outLink/wechat/qrcode/generate
* Method: POST
* Description: 为当前团队有管理权限的微信发布渠道生成 iLink 登录二维码。
* Tags: ['发布渠道', '微信发布渠道']
* ============================================================================ */
export
const
WechatQrcodeGenerateBodySchema
=
z
.
object
({
outLinkId
:
WechatOutLinkIdSchema
});
export
type
WechatQrcodeGenerateBodyType
=
z
.
infer
<
typeof
WechatQrcodeGenerateBodySchema
>
;
export
const
WechatQrcodeGenerateResponseSchema
=
z
.
object
({
qrcode
:
z
.
string
().
meta
({
description
:
'iLink 二维码标识'
}),
qrcode_img_content
:
z
.
string
().
meta
({
description
:
'二维码内容'
}),
expireTime
:
z
.
number
().
meta
({
example
:
480
,
description
:
'二维码有效期,单位秒'
})
});
export
type
WechatQrcodeGenerateResponseType
=
z
.
infer
<
typeof
WechatQrcodeGenerateResponseSchema
>
;
/* ============================================================================
* API: 查询微信发布渠道登录二维码状态
* Route: GET /api/support/outLink/wechat/qrcode/status
* Method: GET
* Description: 查询当前登录成员发起的微信发布渠道二维码登录状态,确认后写入机器人凭据。
* Tags: ['发布渠道', '微信发布渠道']
* ============================================================================ */
export
const
WechatQrcodeStatusQuerySchema
=
z
.
object
({
outLinkId
:
WechatOutLinkIdSchema
});
export
type
WechatQrcodeStatusQueryType
=
z
.
infer
<
typeof
WechatQrcodeStatusQuerySchema
>
;
export
const
WechatQrcodeStatusResponseSchema
=
z
.
object
({
status
:
z
.
enum
([
'wait'
,
'scaned'
,
'confirmed'
,
'expired'
]).
meta
({
example
:
'wait'
,
description
:
'二维码登录状态'
})
});
export
type
WechatQrcodeStatusResponseType
=
z
.
infer
<
typeof
WechatQrcodeStatusResponseSchema
>
;
/* ============================================================================
* API: 登出微信发布渠道
* Route: POST /api/support/outLink/wechat/logout
* Method: POST
* Description: 将当前团队有管理权限的微信发布渠道下线并清空机器人凭据。
* Tags: ['发布渠道', '微信发布渠道']
* ============================================================================ */
export
const
WechatLogoutBodySchema
=
z
.
object
({
outLinkId
:
WechatOutLinkIdSchema
});
export
type
WechatLogoutBodyType
=
z
.
infer
<
typeof
WechatLogoutBodySchema
>
;
export
const
WechatLogoutResponseSchema
=
z
.
undefined
().
meta
({
description
:
'登出成功'
});
export
type
WechatLogoutResponseType
=
z
.
infer
<
typeof
WechatLogoutResponseSchema
>
;
packages/global/openapi/support/outLink/index.ts
View file @
81d39199
...
...
@@ -8,7 +8,13 @@ import {
OutLinkListQuerySchema
,
OutLinkListResponseSchema
,
OutLinkUpdateBodySchema
,
OutLinkUpdateResponseSchema
OutLinkUpdateResponseSchema
,
WechatLogoutBodySchema
,
WechatLogoutResponseSchema
,
WechatQrcodeGenerateBodySchema
,
WechatQrcodeGenerateResponseSchema
,
WechatQrcodeStatusQuerySchema
,
WechatQrcodeStatusResponseSchema
}
from
'./api'
;
export
const
OutLinkPath
:
OpenAPIPath
=
{
...
...
@@ -99,5 +105,73 @@ export const OutLinkPath: OpenAPIPath = {
}
}
}
},
'/support/outLink/wechat/qrcode/generate'
:
{
post
:
{
summary
:
'生成微信发布渠道登录二维码'
,
description
:
'为当前团队有管理权限的微信发布渠道生成 iLink 登录二维码'
,
tags
:
[
DevApiTagsMap
.
publishChannel
],
requestBody
:
{
content
:
{
'application/json'
:
{
schema
:
WechatQrcodeGenerateBodySchema
}
}
},
responses
:
{
200
:
{
description
:
'成功生成二维码'
,
content
:
{
'application/json'
:
{
schema
:
WechatQrcodeGenerateResponseSchema
}
}
}
}
}
},
'/support/outLink/wechat/qrcode/status'
:
{
get
:
{
summary
:
'查询微信发布渠道登录二维码状态'
,
description
:
'查询当前登录成员发起的微信发布渠道二维码登录状态'
,
tags
:
[
DevApiTagsMap
.
publishChannel
],
requestParams
:
{
query
:
WechatQrcodeStatusQuerySchema
},
responses
:
{
200
:
{
description
:
'成功返回二维码状态'
,
content
:
{
'application/json'
:
{
schema
:
WechatQrcodeStatusResponseSchema
}
}
}
}
}
},
'/support/outLink/wechat/logout'
:
{
post
:
{
summary
:
'登出微信发布渠道'
,
description
:
'将当前团队有管理权限的微信发布渠道下线并清空机器人凭据'
,
tags
:
[
DevApiTagsMap
.
publishChannel
],
requestBody
:
{
content
:
{
'application/json'
:
{
schema
:
WechatLogoutBodySchema
}
}
},
responses
:
{
200
:
{
description
:
'成功登出微信发布渠道'
,
content
:
{
'application/json'
:
{
schema
:
WechatLogoutResponseSchema
}
}
}
}
}
}
};
packages/service/support/outLink/wechat/ilinkClient.ts
View file @
81d39199
...
...
@@ -6,6 +6,27 @@ const BOT_TYPE = '3';
const
LONG_POLL_TIMEOUT_MS
=
35
_000
;
const
SEND_TIMEOUT_MS
=
15
_000
;
const
formatFetchError
=
(
err
:
unknown
)
=>
{
if
(
!
(
err
instanceof
Error
))
return
String
(
err
);
const
cause
=
err
.
cause
as
|
{
code
?:
string
;
message
?:
string
;
name
?:
string
;
}
|
undefined
;
return
[
`
${
err
.
name
}
:
${
err
.
message
}
`
,
cause
?.
code
?
`causeCode=
${
cause
.
code
}
`
:
''
,
cause
?.
name
?
`causeName=
${
cause
.
name
}
`
:
''
,
cause
?.
message
?
`causeMessage=
${
cause
.
message
}
`
:
''
]
.
filter
(
Boolean
)
.
join
(
'; '
);
};
export
type
WeixinMessage
=
{
msgid
:
string
;
from_user_id
:
string
;
...
...
@@ -59,15 +80,12 @@ export class ILinkClient {
return
Buffer
.
from
(
String
(
uint32
),
'utf-8'
).
toString
(
'base64'
);
}
private
buildHeaders
(
body
?:
string
):
Record
<
string
,
string
>
{
private
buildHeaders
():
Record
<
string
,
string
>
{
const
headers
:
Record
<
string
,
string
>
=
{
'Content-Type'
:
'application/json'
,
AuthorizationType
:
'ilink_bot_token'
,
'X-WECHAT-UIN'
:
this
.
randomUin
()
};
if
(
body
)
{
headers
[
'Content-Length'
]
=
String
(
Buffer
.
byteLength
(
body
,
'utf-8'
));
}
if
(
this
.
token
)
{
headers
[
'Authorization'
]
=
`Bearer
${
this
.
token
}
`
;
}
...
...
@@ -82,7 +100,7 @@ export class ILinkClient {
try
{
const
res
=
await
fetch
(
url
,
{
method
:
'POST'
,
headers
:
this
.
buildHeaders
(
body
),
headers
:
this
.
buildHeaders
(),
body
,
signal
:
controller
.
signal
});
...
...
@@ -92,7 +110,7 @@ export class ILinkClient {
return
text
;
}
catch
(
err
)
{
clearTimeout
(
timer
);
throw
err
;
throw
new
Error
(
`iLink POST
${
endpoint
}
failed:
${
formatFetchError
(
err
)}
`
)
;
}
}
...
...
packages/service/support/outLink/wechat/mq.ts
View file @
81d39199
...
...
@@ -54,6 +54,7 @@ async function processWechatPollJob(job: Job<WechatPollJobData>): Promise<void>
async
function
pollImpl
(
job
:
Job
<
WechatPollJobData
>
):
Promise
<
void
>
{
const
{
shareId
}
=
job
.
data
;
logger
.
debug
(
'Wechat poll job started'
,
{
shareId
,
jobId
:
job
.
id
});
const
outLink
=
(
await
MongoOutLink
.
findOne
({
shareId
...
...
@@ -75,7 +76,21 @@ async function pollImpl(job: Job<WechatPollJobData>): Promise<void> {
}
const
client
=
new
ILinkClient
(
app
.
baseUrl
,
app
.
token
);
const
resp
=
await
client
.
getUpdates
(
app
.
syncBuf
||
''
);
const
resp
=
await
client
.
getUpdates
(
app
.
syncBuf
||
''
).
catch
((
error
)
=>
{
logger
.
error
(
'Wechat getUpdates request failed'
,
{
shareId
,
baseUrl
:
app
.
baseUrl
,
error
:
String
(
error
)
});
throw
error
;
});
logger
.
debug
(
'Wechat getUpdates returned'
,
{
shareId
,
msgCount
:
resp
.
msgs
?.
length
??
0
,
hasNextBuf
:
Boolean
(
resp
.
get_updates_buf
),
ret
:
resp
.
ret
,
errcode
:
resp
.
errcode
});
const
isError
=
(
resp
.
ret
!==
undefined
&&
resp
.
ret
!==
0
)
||
...
...
@@ -194,7 +209,7 @@ async function processWechatReplyJob(job: Job<WechatReplyJobData>): Promise<void
// - 如果链已死(无 job),add 正常入队
async
function
scheduleNextPoll
(
shareId
:
string
,
delayMs
?:
number
):
Promise
<
void
>
{
const
queue
=
getQueue
<
WechatPollJobData
>
(
QueueNames
.
wechatPoll
);
await
queue
.
add
(
const
job
=
await
queue
.
add
(
POLL_JOB_NAME
,
{
shareId
},
{
...
...
@@ -204,6 +219,12 @@ async function scheduleNextPoll(shareId: string, delayMs?: number): Promise<void
removeOnFail
:
true
}
);
logger
.
debug
(
'Wechat poll job scheduled'
,
{
shareId
,
jobId
:
job
.
id
,
delayMs
,
jobState
:
await
job
.
getState
().
catch
(()
=>
undefined
)
});
}
/**
...
...
@@ -253,6 +274,11 @@ export const initWechatPollWorker = async () => {
pollWorker
.
on
(
'failed'
,
async
(
job
)
=>
{
if
(
!
job
||
job
.
name
!==
POLL_JOB_NAME
)
return
;
const
{
shareId
}
=
job
.
data
as
WechatPollJobData
;
logger
.
warn
(
'Wechat poll job failed'
,
{
shareId
,
jobId
:
job
.
id
,
failedReason
:
job
.
failedReason
});
try
{
await
retryFn
(
async
()
=>
{
if
(
!
(
await
shouldContinuePolling
(
shareId
)))
return
;
...
...
@@ -303,6 +329,15 @@ async function resumeAllWechatPolling(): Promise<void> {
* 启动某个渠道的轮询
*/
export
const
startWechatPolling
=
async
(
shareId
:
string
):
Promise
<
void
>
=>
{
const
queue
=
getQueue
<
WechatPollJobData
>
(
QueueNames
.
wechatPoll
);
// 重新登录后优先丢弃旧的 offline/delayed poll job,避免确定 jobId 被旧任务占用。
await
queue
.
remove
(
pollJobId
(
shareId
)).
catch
((
error
)
=>
{
logger
.
warn
(
'Remove old wechat poll job before start failed (job may be active)'
,
{
shareId
,
error
:
String
(
error
)
});
});
await
scheduleNextPoll
(
shareId
);
logger
.
info
(
'Wechat polling started'
,
{
shareId
});
};
...
...
packages/service/support/outLink/wechat/qrcode.ts
0 → 100644
View file @
81d39199
/**
* 生成微信 iLink 二维码登录缓存 key。
*
* outLinkId 绑定具体发布渠道,tmbId 绑定发起操作的团队成员,确保状态确认只能消费
* 当前登录成员自己创建的二维码。
*/
export
const
getWechatQrcodeCacheKey
=
({
outLinkId
,
tmbId
}:
{
outLinkId
:
string
;
tmbId
:
string
;
})
=>
`publish:wechat:qrcode:
${
outLinkId
}
:
${
tmbId
}
`
;
packages/service/support/outLink/wechat/utils.ts
0 → 100644
View file @
81d39199
import
{
PublishChannelEnum
}
from
'@fastgpt/global/support/outLink/constant'
;
import
{
OutLinkErrEnum
}
from
'@fastgpt/global/common/error/code/outLink'
;
import
{
type
OutLinkSchemaType
}
from
'@fastgpt/global/support/outLink/type'
;
/**
* 确保后台微信接口只能操作微信发布渠道。
*
* authOutLinkCrud 负责登录、teamId 和应用权限校验;这里补齐微信接口自己的渠道类型边界,
* 避免同团队成员把其他发布渠道 ID 传入微信登录/登出接口后写入微信配置字段。
*/
export
const
assertWechatOutLink
=
(
outLink
:
Pick
<
OutLinkSchemaType
,
'type'
>
)
=>
{
if
(
outLink
.
type
!==
PublishChannelEnum
.
wechat
)
{
return
Promise
.
reject
(
OutLinkErrEnum
.
linkUnInvalid
);
}
};
projects/app/src/pageComponents/app/detail/Publish/Wechat/QRLoginModal.tsx
View file @
81d39199
import
React
,
{
useEffect
,
useState
,
useRef
,
useCallback
}
from
'react'
;
import
{
Box
,
Button
,
Flex
,
ModalBody
,
ModalFooter
,
Spinner
,
Text
}
from
'@chakra-ui/react'
;
import
{
Box
,
Button
,
Flex
,
ModalBody
,
ModalFooter
,
Text
}
from
'@chakra-ui/react'
;
import
MyModal
from
'@fastgpt/web/components/v2/common/MyModal'
;
import
{
useTranslation
}
from
'next-i18next'
;
import
{
useToast
}
from
'@fastgpt/web/hooks/useToast'
;
import
{
POST
,
GET
}
from
'@/web/common/api/request'
;
import
QRCode
from
'qrcode'
;
import
MyLoading
from
'@fastgpt/web/components/common/MyLoading'
;
import
{
formatFileSize
}
from
'@fastgpt/global/common/file/tools'
;
type
QRStatus
=
'loading'
|
'wait'
|
'scanned'
|
'confirmed'
|
'expired'
|
'error'
;
const
QRLoginModal
=
({
share
Id
,
outLink
Id
,
onSuccess
,
onClose
}:
{
share
Id
:
string
;
outLink
Id
:
string
;
onSuccess
:
()
=>
void
;
onClose
:
()
=>
void
;
})
=>
{
...
...
@@ -40,7 +39,7 @@ const QRLoginModal = ({
while
(
pollingRef
.
current
&&
mountedRef
.
current
)
{
try
{
const
data
=
await
GET
<
{
status
:
string
}
>
(
'/support/outLink/wechat/qrcode/status'
,
{
share
Id
outLink
Id
});
if
(
!
mountedRef
.
current
||
!
pollingRef
.
current
)
return
;
...
...
@@ -76,7 +75,7 @@ const QRLoginModal = ({
};
poll
();
},
[
share
Id
,
toast
,
t
,
onSuccess
]);
},
[
outLink
Id
,
toast
,
t
,
onSuccess
]);
// 用 qrcode 库渲染二维码到 canvas
const
drawQRCode
=
useCallback
((
text
:
string
)
=>
{
...
...
@@ -105,7 +104,7 @@ const QRLoginModal = ({
const
data
=
await
POST
<
{
qrcode
:
string
;
qrcode_img_content
:
string
;
}
>
(
'/support/outLink/wechat/qrcode/generate'
,
{
share
Id
});
}
>
(
'/support/outLink/wechat/qrcode/generate'
,
{
outLink
Id
});
if
(
!
mountedRef
.
current
)
return
;
...
...
@@ -118,7 +117,7 @@ const QRLoginModal = ({
setStatus
(
'error'
);
setErrMsg
(
t
(
'publish:wechat.qr_generate_failed'
));
}
},
[
share
Id
,
startPolling
,
stopPolling
,
t
]);
},
[
outLink
Id
,
startPolling
,
stopPolling
,
t
]);
// qrText 变化时重新渲染二维码
useEffect
(()
=>
{
...
...
@@ -127,12 +126,14 @@ const QRLoginModal = ({
useEffect
(()
=>
{
mountedRef
.
current
=
true
;
// eslint-disable-next-line react-hooks/set-state-in-effect
generateQR
();
return
()
=>
{
mountedRef
.
current
=
false
;
stopPolling
();
};
},
[]);
},
[
generateQR
,
stopPolling
]);
const
renderContent
=
()
=>
{
switch
(
status
)
{
...
...
projects/app/src/pageComponents/app/detail/Publish/Wechat/index.tsx
View file @
81d39199
...
...
@@ -39,7 +39,7 @@ const Wechat = ({ appId }: { appId: string }) => {
const
{
feConfigs
}
=
useSystemStore
();
const
[
editData
,
setEditData
]
=
useState
<
OutLinkEditType
<
WechatAppType
>>
();
const
[
isEdit
,
setIsEdit
]
=
useState
(
false
);
const
[
login
ShareId
,
setLoginShare
Id
]
=
useState
<
string
>
();
const
[
login
OutLinkId
,
setLoginOutLink
Id
]
=
useState
<
string
>
();
const
{
data
:
shareChatList
=
[],
...
...
@@ -130,7 +130,7 @@ const Wechat = ({ appId }: { appId: string }) => {
mr=
{
3
}
colorScheme=
"green"
onClick=
{
()
=>
{
setLogin
ShareId
(
item
.
shareI
d
);
setLogin
OutLinkId
(
item
.
_i
d
);
}
}
>
{
t
(
'publish:wechat.login'
)
}
...
...
@@ -144,7 +144,7 @@ const Wechat = ({ appId }: { appId: string }) => {
setIsLoading
(
true
);
try
{
await
POST
(
'/support/outLink/wechat/logout'
,
{
shareId
:
item
.
shareI
d
outLinkId
:
item
.
_i
d
});
refetch
();
}
finally
{
...
...
@@ -160,7 +160,7 @@ const Wechat = ({ appId }: { appId: string }) => {
mr=
{
3
}
variant=
{
'whitePrimary'
}
onClick=
{
()
=>
{
setLogin
ShareId
(
item
.
shareI
d
);
setLogin
OutLinkId
(
item
.
_i
d
);
}
}
>
{
t
(
'publish:wechat.relogin'
)
}
...
...
@@ -232,14 +232,14 @@ const Wechat = ({ appId }: { appId: string }) => {
/>
)
}
{
login
Share
Id
&&
(
{
login
OutLink
Id
&&
(
<
QRLoginModal
shareId=
{
loginShare
Id
}
outLinkId=
{
loginOutLink
Id
}
onSuccess=
{
()
=>
{
refetch
();
setLogin
Share
Id
(
undefined
);
setLogin
OutLink
Id
(
undefined
);
}
}
onClose=
{
()
=>
setLogin
Share
Id
(
undefined
)
}
onClose=
{
()
=>
setLogin
OutLink
Id
(
undefined
)
}
/>
)
}
...
...
projects/app/src/pages/api/support/outLink/wechat/logout.ts
View file @
81d39199
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
MongoOutLink
}
from
'@fastgpt/service/support/outLink/schema'
;
import
{
authOutLinkValid
}
from
'@fastgpt/service/support/permission/publish/authLink'
;
import
type
{
WechatAppType
}
from
'@fastgpt/global/support/outLink/type'
;
import
{
authOutLinkCrud
}
from
'@fastgpt/service/support/permission/publish/authLink'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
WechatLogoutBodySchema
,
WechatLogoutResponseSchema
,
type
WechatLogoutBodyType
,
type
WechatLogoutResponseType
}
from
'@fastgpt/global/openapi/support/outLink/api'
;
import
{
ManagePermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
assertWechatOutLink
}
from
'@fastgpt/service/support/outLink/wechat/utils'
;
async
function
handler
(
req
:
ApiRequestProps
<
{
shareId
:
string
}
>
):
Promise
<
void
>
{
const
{
shareId
}
=
req
.
body
;
async
function
handler
(
req
:
ApiRequestProps
<
WechatLogoutBodyType
>
):
Promise
<
WechatLogoutResponseType
>
{
const
{
outLinkId
}
=
parseApiInput
({
req
,
bodySchema
:
WechatLogoutBodySchema
}).
body
;
await
authOutLinkValid
<
WechatAppType
>
({
shareId
});
const
{
outLink
}
=
await
authOutLinkCrud
({
req
,
authToken
:
true
,
outLinkId
,
per
:
ManagePermissionVal
});
await
assertWechatOutLink
(
outLink
);
await
MongoOutLink
.
updateOne
(
{
shareI
d
},
{
_id
:
outLink
.
_i
d
},
{
$set
:
{
'app.status'
:
'offline'
,
...
...
@@ -19,6 +38,8 @@ async function handler(req: ApiRequestProps<{ shareId: string }>): Promise<void>
}
}
);
return
WechatLogoutResponseSchema
.
parse
(
undefined
);
}
export
default
NextAPI
(
handler
);
projects/app/src/pages/api/support/outLink/wechat/qrcode/generate.ts
View file @
81d39199
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
ILinkClient
}
from
'@fastgpt/service/support/outLink/wechat/ilinkClient'
;
import
{
authOutLinkValid
}
from
'@fastgpt/service/support/permission/publish/authLink'
;
import
type
{
WechatAppType
}
from
'@fastgpt/global/support/outLink/type'
;
import
{
authOutLinkCrud
}
from
'@fastgpt/service/support/permission/publish/authLink'
;
import
{
setRedisCache
}
from
'@fastgpt/service/common/redis/cache'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
WechatQrcodeGenerateBodySchema
,
WechatQrcodeGenerateResponseSchema
,
type
WechatQrcodeGenerateBodyType
,
type
WechatQrcodeGenerateResponseType
}
from
'@fastgpt/global/openapi/support/outLink/api'
;
import
{
ManagePermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
getWechatQrcodeCacheKey
}
from
'@fastgpt/service/support/outLink/wechat/qrcode'
;
import
{
assertWechatOutLink
}
from
'@fastgpt/service/support/outLink/wechat/utils'
;
const
EXPIRE_TIME
=
480
;
async
function
handler
(
req
:
ApiRequestProps
<
{
shareId
:
string
}
>
):
Promise
<
{
qrcode
:
string
;
qrcode_img_content
:
string
;
expireTime
:
number
}
>
{
const
{
shareId
}
=
req
.
body
;
req
:
ApiRequestProps
<
WechatQrcodeGenerateBodyType
>
):
Promise
<
WechatQrcodeGenerateResponseType
>
{
const
{
outLinkId
}
=
parseApiInput
({
req
,
bodySchema
:
WechatQrcodeGenerateBodySchema
}).
body
;
await
authOutLinkValid
<
WechatAppType
>
({
shareId
});
const
{
tmbId
,
outLink
}
=
await
authOutLinkCrud
({
req
,
authToken
:
true
,
outLinkId
,
per
:
ManagePermissionVal
});
await
assertWechatOutLink
(
outLink
);
const
client
=
new
ILinkClient
();
const
qrData
=
await
client
.
getQRCode
();
await
setRedisCache
(
`publish:wechat:qrcode:
${
shareId
}
`
,
JSON
.
stringify
(
qrData
),
480
);
await
setRedisCache
(
getWechatQrcodeCacheKey
({
outLinkId
,
tmbId
}),
JSON
.
stringify
(
qrData
),
EXPIRE_TIME
);
return
{
return
WechatQrcodeGenerateResponseSchema
.
parse
(
{
qrcode
:
qrData
.
qrcode
,
qrcode_img_content
:
qrData
.
qrcode_img_content
,
expireTime
:
480
};
expireTime
:
EXPIRE_TIME
}
)
;
}
export
default
NextAPI
(
handler
);
projects/app/src/pages/api/support/outLink/wechat/qrcode/status.ts
View file @
81d39199
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
type
{
ApiRequestProps
,
ApiResponseType
}
from
'@fastgpt/service/type/next'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
ILinkClient
}
from
'@fastgpt/service/support/outLink/wechat/ilinkClient'
;
import
{
getRedisCache
,
delRedisCache
}
from
'@fastgpt/service/common/redis/cache'
;
import
{
MongoOutLink
}
from
'@fastgpt/service/support/outLink/schema'
;
import
{
startWechatPolling
}
from
'@fastgpt/service/support/outLink/wechat/mq'
;
import
{
authOutLinkCrud
}
from
'@fastgpt/service/support/permission/publish/authLink'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
WechatQrcodeStatusQuerySchema
,
WechatQrcodeStatusResponseSchema
,
type
WechatQrcodeStatusQueryType
,
type
WechatQrcodeStatusResponseType
}
from
'@fastgpt/global/openapi/support/outLink/api'
;
import
{
ManagePermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
getWechatQrcodeCacheKey
}
from
'@fastgpt/service/support/outLink/wechat/qrcode'
;
import
{
assertWechatOutLink
}
from
'@fastgpt/service/support/outLink/wechat/utils'
;
async
function
handler
(
req
:
ApiRequestProps
<
{},
{
shareId
:
string
}
>
):
Promise
<
{
status
:
string
}
>
{
const
{
shareId
}
=
req
.
query
;
async
function
handler
(
req
:
ApiRequestProps
<
Record
<
string
,
never
>
,
WechatQrcodeStatusQueryType
>
,
res
:
ApiResponseType
):
Promise
<
WechatQrcodeStatusResponseType
>
{
res
.
setHeader
(
'Cache-Control'
,
'no-store'
);
const
raw
=
await
getRedisCache
(
`publish:wechat:qrcode:
${
shareId
}
`
);
const
{
outLinkId
}
=
parseApiInput
({
req
,
querySchema
:
WechatQrcodeStatusQuerySchema
}).
query
;
const
{
tmbId
,
outLink
}
=
await
authOutLinkCrud
({
req
,
authToken
:
true
,
outLinkId
,
per
:
ManagePermissionVal
});
await
assertWechatOutLink
(
outLink
);
const
cacheKey
=
getWechatQrcodeCacheKey
({
outLinkId
,
tmbId
});
const
raw
=
await
getRedisCache
(
cacheKey
);
if
(
!
raw
)
{
return
{
status
:
'expired'
}
;
return
WechatQrcodeStatusResponseSchema
.
parse
({
status
:
'expired'
})
;
}
const
qrData
=
JSON
.
parse
(
raw
);
...
...
@@ -19,7 +47,7 @@ async function handler(req: ApiRequestProps<{}, { shareId: string }>): Promise<{
if
(
statusData
.
status
===
'confirmed'
&&
statusData
.
bot_token
&&
statusData
.
ilink_bot_id
)
{
await
MongoOutLink
.
updateOne
(
{
shareI
d
},
{
_id
:
outLink
.
_i
d
},
{
$set
:
{
'app.token'
:
statusData
.
bot_token
,
...
...
@@ -34,11 +62,11 @@ async function handler(req: ApiRequestProps<{}, { shareId: string }>): Promise<{
}
);
await
delRedisCache
(
`publish:wechat:qrcode:
${
shareId
}
`
);
await
startWechatPolling
(
shareId
);
await
delRedisCache
(
cacheKey
);
await
startWechatPolling
(
outLink
.
shareId
);
}
return
{
status
:
statusData
.
status
}
;
return
WechatQrcodeStatusResponseSchema
.
parse
({
status
:
statusData
.
status
})
;
}
export
default
NextAPI
(
handler
);
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