Commit 47134990 by Nixieboluo Committed by GitHub

feat(outlink): new configuration page (#7456)

* feat(outlink): add channel counts

* feat(publish): show channel connection counts

* refactor(outlink): query optimization

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(outlink): a11y optimizations

Signed-off-by: Nixieboluo <me@sagirii.me>

* test(outlink): fix test failure

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(outlink): add required

Signed-off-by: Nixieboluo <me@sagirii.me>

* refactor(outlink): address review comments

Signed-off-by: Nixieboluo <me@sagirii.me>

---------

Signed-off-by: Nixieboluo <me@sagirii.me>
parent 549230b0
...@@ -98,6 +98,50 @@ export const OutLinkListResponseSchema = z.array(OutLinkSchema).meta({ ...@@ -98,6 +98,50 @@ export const OutLinkListResponseSchema = z.array(OutLinkSchema).meta({
export type OutLinkListResponseType = z.infer<typeof OutLinkListResponseSchema>; export type OutLinkListResponseType = z.infer<typeof OutLinkListResponseSchema>;
/* ============================================================================ /* ============================================================================
* API: 获取发布渠道连接数量
* Route: GET /api/support/outLink/count
* Method: GET
* Description: 获取应用侧栏展示的发布渠道连接数量。
* Tags: ['发布渠道']
* ============================================================================ */
export const OutLinkCountQuerySchema = z.object({
appId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
});
export type OutLinkCountQueryType = z.infer<typeof OutLinkCountQuerySchema>;
export const OutLinkCountResponseSchema = z.object({
[PublishChannelEnum.share]: z.number().int().nonnegative().meta({
example: 2,
description: '分享链接数量'
}),
[PublishChannelEnum.feishu]: z.number().int().nonnegative().meta({
example: 1,
description: '飞书机器人数量'
}),
[PublishChannelEnum.dingtalk]: z.number().int().nonnegative().meta({
example: 1,
description: '钉钉机器人数量'
}),
[PublishChannelEnum.wecom]: z.number().int().nonnegative().meta({
example: 1,
description: '企业微信机器人数量'
}),
[PublishChannelEnum.wechat]: z.number().int().nonnegative().meta({
example: 1,
description: '微信机器人数量'
}),
[PublishChannelEnum.officialAccount]: z.number().int().nonnegative().meta({
example: 1,
description: '微信公众号数量'
})
});
export type OutLinkCountResponseType = z.infer<typeof OutLinkCountResponseSchema>;
/* ============================================================================
* API: 创建发布渠道 * API: 创建发布渠道
* Route: POST /api/support/outLink/create * Route: POST /api/support/outLink/create
* Method: POST * Method: POST
......
...@@ -3,6 +3,8 @@ import { DevApiTagsMap } from '../../tag'; ...@@ -3,6 +3,8 @@ import { DevApiTagsMap } from '../../tag';
import { import {
OutLinkCreateBodySchema, OutLinkCreateBodySchema,
OutLinkCreateResponseSchema, OutLinkCreateResponseSchema,
OutLinkCountQuerySchema,
OutLinkCountResponseSchema,
OutLinkDeleteQuerySchema, OutLinkDeleteQuerySchema,
OutLinkDeleteResponseSchema, OutLinkDeleteResponseSchema,
OutLinkListQuerySchema, OutLinkListQuerySchema,
...@@ -18,6 +20,26 @@ import { ...@@ -18,6 +20,26 @@ import {
} from './api'; } from './api';
export const OutLinkPath: OpenAPIPath = { export const OutLinkPath: OpenAPIPath = {
'/support/outLink/count': {
get: {
summary: '获取应用的发布渠道连接数量',
description: '查询侧栏展示的六种发布渠道连接数量',
tags: [DevApiTagsMap.publishChannel],
requestParams: {
query: OutLinkCountQuerySchema
},
responses: {
200: {
description: '成功返回发布渠道连接数量',
content: {
'application/json': {
schema: OutLinkCountResponseSchema
}
}
}
}
}
},
'/support/outLink/list': { '/support/outLink/list': {
get: { get: {
summary: '获取应用的发布渠道列表', summary: '获取应用的发布渠道列表',
......
...@@ -113,6 +113,7 @@ const logger = getLogger(LogCategories.INFRA.MONGO); ...@@ -113,6 +113,7 @@ const logger = getLogger(LogCategories.INFRA.MONGO);
defineIndex(OutLinkSchema, { key: { shareId: -1 } }); defineIndex(OutLinkSchema, { key: { shareId: -1 } });
defineIndex(OutLinkSchema, { key: { teamId: 1, tmbId: 1, appId: 1 } }); defineIndex(OutLinkSchema, { key: { teamId: 1, tmbId: 1, appId: 1 } });
defineIndex(OutLinkSchema, { key: { teamId: 1, appId: 1, type: 1 } });
// Wechat polling recovery: find online channels on startup // Wechat polling recovery: find online channels on startup
defineIndex(OutLinkSchema, { defineIndex(OutLinkSchema, {
key: { type: 1, 'app.status': 1 }, key: { type: 1, 'app.status': 1 },
......
...@@ -500,6 +500,7 @@ export const iconPaths = { ...@@ -500,6 +500,7 @@ export const iconPaths = {
'support/config/configFill': () => import('./icons/support/config/configFill.svg'), 'support/config/configFill': () => import('./icons/support/config/configFill.svg'),
'support/config/configLight': () => import('./icons/support/config/configLight.svg'), 'support/config/configLight': () => import('./icons/support/config/configLight.svg'),
'support/outlink/apikeyFill': () => import('./icons/support/outlink/apikeyFill.svg'), 'support/outlink/apikeyFill': () => import('./icons/support/outlink/apikeyFill.svg'),
'support/outlink/share': () => import('./icons/support/outlink/share.svg'),
'support/permission/collaborator': () => import('./icons/support/permission/collaborator.svg'), 'support/permission/collaborator': () => import('./icons/support/permission/collaborator.svg'),
'support/permission/privateLight': () => import('./icons/support/permission/privateLight.svg'), 'support/permission/privateLight': () => import('./icons/support/permission/privateLight.svg'),
'support/permission/publicLight': () => import('./icons/support/permission/publicLight.svg'), 'support/permission/publicLight': () => import('./icons/support/permission/publicLight.svg'),
......
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M816 416a174.816 174.816 0 0 1-133.6-62.624l-219.616 104.672a222.944 222.944 0 0 1-1.216 174.656l173.696 89.984A179.84 179.84 0 1 1 611.2 784l-185.056-96a224 224 0 1 1 2.912-284.8l221.44-105.6A175.552 175.552 0 1 1 816 416z" fill="currentColor" />
</svg>
...@@ -18,12 +18,14 @@ ...@@ -18,12 +18,14 @@
"feishu_bot_desc": "Connect to Feishu Bot directly via API", "feishu_bot_desc": "Connect to Feishu Bot directly via API",
"key_alias": "Key alias, for display only", "key_alias": "Key alias, for display only",
"link_name": "Share Link Name", "link_name": "Share Link Name",
"native_channels": "Native Channels",
"new_feishu_bot": "add_new Feishu Bot", "new_feishu_bot": "add_new Feishu Bot",
"official_account.create_modal_title": "Create WeChat Official Account Integration", "official_account.create_modal_title": "Create WeChat Official Account Integration",
"official_account.desc": "Connect to WeChat Official Account directly via API", "official_account.desc": "Connect to WeChat Official Account directly via API",
"official_account.edit_modal_title": "Edit WeChat Official Account Integration", "official_account.edit_modal_title": "Edit WeChat Official Account Integration",
"official_account.name": "WeChat Official Account Integration", "official_account.name": "WeChat Official Account Integration",
"official_account.params": "WeChat Official Account Parameters", "official_account.params": "WeChat Official Account Parameters",
"official_account.title": "WeChat Official Account",
"private_config": "Visibility configuration", "private_config": "Visibility configuration",
"publish_name": "Name", "publish_name": "Name",
"qpm_is_empty": "QPM cannot be empty", "qpm_is_empty": "QPM cannot be empty",
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
"token_auth": "Token Authentication", "token_auth": "Token Authentication",
"token_auth_tips": "Token authentication server URL. If provided, a request will be sent to the specified server for authentication before each conversation.", "token_auth_tips": "Token authentication server URL. If provided, a request will be sent to the specified server for authentication before each conversation.",
"token_auth_use_cases": "View Token Authentication Guide", "token_auth_use_cases": "View Token Authentication Guide",
"third_party_publish_channels": "Third-Party Channels",
"use_default_domain": "Use Default Domain", "use_default_domain": "Use Default Domain",
"wechat.bot": "WeChat Personal", "wechat.bot": "WeChat Personal",
"wechat.bot_desc": "Connect to WeChat personal chat via ClawBot", "wechat.bot_desc": "Connect to WeChat personal chat via ClawBot",
......
...@@ -18,12 +18,14 @@ ...@@ -18,12 +18,14 @@
"feishu_bot_desc": "通过 API 直接接入飞书机器人", "feishu_bot_desc": "通过 API 直接接入飞书机器人",
"key_alias": "key 的别名,仅用于展示", "key_alias": "key 的别名,仅用于展示",
"link_name": "分享链接的名字", "link_name": "分享链接的名字",
"native_channels": "原生渠道",
"new_feishu_bot": "新增飞书机器人", "new_feishu_bot": "新增飞书机器人",
"official_account.create_modal_title": "创建微信公众号接入", "official_account.create_modal_title": "创建微信公众号接入",
"official_account.desc": "通过 API 直接接入微信公众号", "official_account.desc": "通过 API 直接接入微信公众号",
"official_account.edit_modal_title": "编辑微信公众号接入", "official_account.edit_modal_title": "编辑微信公众号接入",
"official_account.name": "微信公众号接入", "official_account.name": "微信公众号接入",
"official_account.params": "微信公众号参数", "official_account.params": "微信公众号参数",
"official_account.title": "微信公众号",
"private_config": "可见度配置", "private_config": "可见度配置",
"publish_name": "名称", "publish_name": "名称",
"qpm_is_empty": "QPM 不能为空", "qpm_is_empty": "QPM 不能为空",
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
"token_auth": "身份验证", "token_auth": "身份验证",
"token_auth_tips": "身份校验服务器地址", "token_auth_tips": "身份校验服务器地址",
"token_auth_use_cases": "查看身份验证使用说明", "token_auth_use_cases": "查看身份验证使用说明",
"third_party_publish_channels": "第三方发布渠道",
"use_default_domain": "使用默认域名", "use_default_domain": "使用默认域名",
"wechat.bot": "微信个人号", "wechat.bot": "微信个人号",
"wechat.bot_desc": "通过 ClawBot 接入微信个人号聊天", "wechat.bot_desc": "通过 ClawBot 接入微信个人号聊天",
......
...@@ -18,12 +18,14 @@ ...@@ -18,12 +18,14 @@
"feishu_bot_desc": "透過 API 直接連結 Lark 聊天機器人", "feishu_bot_desc": "透過 API 直接連結 Lark 聊天機器人",
"key_alias": "金鑰別名,僅供顯示", "key_alias": "金鑰別名,僅供顯示",
"link_name": "分享連結名稱", "link_name": "分享連結名稱",
"native_channels": "原生渠道",
"new_feishu_bot": "新增 Lark 聊天機器人", "new_feishu_bot": "新增 Lark 聊天機器人",
"official_account.create_modal_title": "建立微信公眾號整合", "official_account.create_modal_title": "建立微信公眾號整合",
"official_account.desc": "透過 API 直接連結微信公眾號", "official_account.desc": "透過 API 直接連結微信公眾號",
"official_account.edit_modal_title": "編輯微信公眾號整合", "official_account.edit_modal_title": "編輯微信公眾號整合",
"official_account.name": "微信公眾號整合", "official_account.name": "微信公眾號整合",
"official_account.params": "微信公眾號參數", "official_account.params": "微信公眾號參數",
"official_account.title": "微信公眾號",
"private_config": "顯示設定", "private_config": "顯示設定",
"publish_name": "名稱", "publish_name": "名稱",
"qpm_is_empty": "QPM 不可為空白", "qpm_is_empty": "QPM 不可為空白",
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
"token_auth": "身分驗證", "token_auth": "身分驗證",
"token_auth_tips": "身分驗證伺服器網址。若有提供,每次對話前將向指定伺服器傳送驗證請求。", "token_auth_tips": "身分驗證伺服器網址。若有提供,每次對話前將向指定伺服器傳送驗證請求。",
"token_auth_use_cases": "檢視身分驗證指南", "token_auth_use_cases": "檢視身分驗證指南",
"third_party_publish_channels": "第三方發布渠道",
"use_default_domain": "使用預設域名", "use_default_domain": "使用預設域名",
"wechat.bot": "微信個人號", "wechat.bot": "微信個人號",
"wechat.bot_desc": "透過 ClawBot 連結微信個人號聊天", "wechat.bot_desc": "透過 ClawBot 連結微信個人號聊天",
......
...@@ -64,7 +64,15 @@ const AgentEdit = () => { ...@@ -64,7 +64,15 @@ const AgentEdit = () => {
); );
return ( return (
<Flex h={'100%'} minH={0} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}> <Flex
h={'100%'}
minH={0}
flexDirection={'column'}
px={[3, 0]}
pr={[3, 3]}
borderLeft={currentTab === TabEnum.publish ? '1px solid' : undefined}
borderColor={'myGray.200'}
>
<Header <Header
appForm={appForm} appForm={appForm}
forbiddenSaveSnapshot={forbiddenSaveSnapshot} forbiddenSaveSnapshot={forbiddenSaveSnapshot}
......
...@@ -200,7 +200,7 @@ const Header = ({ ...@@ -200,7 +200,7 @@ const Header = ({
</Flex> </Flex>
)} )}
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}> <Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
<Box flex={'1'}> <Box flex={'1'} ml={'16px'}>
<FolderPath <FolderPath
rootName={t('common:All')} rootName={t('common:All')}
paths={paths} paths={paths}
......
...@@ -61,7 +61,15 @@ const SimpleEdit = () => { ...@@ -61,7 +61,15 @@ const SimpleEdit = () => {
); );
return ( return (
<Flex h={'100%'} minH={0} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}> <Flex
h={'100%'}
minH={0}
flexDirection={'column'}
px={[3, 0]}
pr={[3, 3]}
borderLeft={currentTab === TabEnum.publish ? '1px solid' : undefined}
borderColor={'myGray.200'}
>
<Header <Header
appForm={appForm} appForm={appForm}
forbiddenSaveSnapshot={forbiddenSaveSnapshot} forbiddenSaveSnapshot={forbiddenSaveSnapshot}
......
...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc'; ...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const DingTalkEditModal = dynamic(() => import('./DingTalkEditModal')); const DingTalkEditModal = dynamic(() => import('./DingTalkEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal')); const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const DingTalk = ({ appId }: { appId: string }) => { const DingTalk = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -66,10 +72,10 @@ const DingTalk = ({ appId }: { appId: string }) => { ...@@ -66,10 +72,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null); const [showShareLink, setShowShareLink] = useState<string | null>(null);
return ( return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}> <Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row"> <Flex justifyContent={'space-between'} flexDirection="row">
<HStack> <HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:dingtalk.title')} {t('publish:dingtalk.title')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
...@@ -112,14 +118,9 @@ const DingTalk = ({ appId }: { appId: string }) => { ...@@ -112,14 +118,9 @@ const DingTalk = ({ appId }: { appId: string }) => {
<Tr> <Tr>
<Th>{t('common:Name')}</Th> <Th>{t('common:Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th> <Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:expired_time')}</Th>
</>
)}
<Th>{t('common:last_use_time')}</Th> <Th>{t('common:last_use_time')}</Th>
<Th></Th> <Th>{t('common:Action')}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
...@@ -137,14 +138,11 @@ const DingTalk = ({ appId }: { appId: string }) => { ...@@ -137,14 +138,11 @@ const DingTalk = ({ appId }: { appId: string }) => {
: ''} : ''}
</Td> </Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td> <Td>
{item?.limit?.expiredTime {item.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'} : '-'}
</Td> </Td>
</>
)} )}
<Td> <Td>
{item.lastTime {item.lastTime
...@@ -200,7 +198,10 @@ const DingTalk = ({ appId }: { appId: string }) => { ...@@ -200,7 +198,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetchShareChatList(); void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -221,7 +222,10 @@ const DingTalk = ({ appId }: { appId: string }) => { ...@@ -221,7 +222,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
<DingTalkEditModal <DingTalkEditModal
appId={appId} appId={appId}
defaultData={editDingTalkLinkData} defaultData={editDingTalkLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])} onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditDingTalkLinkData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])} onEdit={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onClose={() => setEditDingTalkLinkData(undefined)} onClose={() => setEditDingTalkLinkData(undefined)}
isEdit={isEdit} isEdit={isEdit}
......
...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc'; ...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const FeiShuEditModal = dynamic(() => import('./FeiShuEditModal')); const FeiShuEditModal = dynamic(() => import('./FeiShuEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal')); const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const FeiShu = ({ appId }: { appId: string }) => { const FeiShu = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -65,10 +71,10 @@ const FeiShu = ({ appId }: { appId: string }) => { ...@@ -65,10 +71,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null); const [showShareLink, setShowShareLink] = useState<string | null>(null);
return ( return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}> <Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row"> <Flex justifyContent={'space-between'} flexDirection="row">
<HStack> <HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('common:core.app.publish.Fei shu bot publish')} {t('common:core.app.publish.Fei shu bot publish')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
...@@ -111,14 +117,9 @@ const FeiShu = ({ appId }: { appId: string }) => { ...@@ -111,14 +117,9 @@ const FeiShu = ({ appId }: { appId: string }) => {
<Tr> <Tr>
<Th>{t('common:Name')}</Th> <Th>{t('common:Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th> <Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:expired_time')}</Th>
</>
)}
<Th>{t('common:last_use_time')}</Th> <Th>{t('common:last_use_time')}</Th>
<Th></Th> <Th>{t('common:Action')}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
...@@ -136,14 +137,11 @@ const FeiShu = ({ appId }: { appId: string }) => { ...@@ -136,14 +137,11 @@ const FeiShu = ({ appId }: { appId: string }) => {
: ''} : ''}
</Td> </Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td> <Td>
{item?.limit?.expiredTime {item.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'} : '-'}
</Td> </Td>
</>
)} )}
<Td> <Td>
{item.lastTime {item.lastTime
...@@ -199,7 +197,10 @@ const FeiShu = ({ appId }: { appId: string }) => { ...@@ -199,7 +197,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetchShareChatList(); void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -220,7 +221,10 @@ const FeiShu = ({ appId }: { appId: string }) => { ...@@ -220,7 +221,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
<FeiShuEditModal <FeiShuEditModal
appId={appId} appId={appId}
defaultData={editFeiShuLinkData} defaultData={editFeiShuLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])} onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditFeiShuLinkData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])} onEdit={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onClose={() => setEditFeiShuLinkData(undefined)} onClose={() => setEditFeiShuLinkData(undefined)}
isEdit={isEdit} isEdit={isEdit}
......
...@@ -50,7 +50,14 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest'; ...@@ -50,7 +50,14 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal')); const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { const Share = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
type: PublishChannelEnum;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setIsLoading } = useLoading(); const { setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -72,13 +79,13 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -72,13 +79,13 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
); );
return ( return (
<MyBox h={'100%'} isLoading={isFetching} position={'relative'} pt={3} px={5} minH={'50vh'}> <MyBox h={'100%'} isLoading={isFetching} position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row"> <Flex justifyContent={'space-between'} flexDirection="row">
<HStack> <HStack>
<Box> <Box>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box color={'myGray.900'} fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('common:core.app.Share link')} {t('common:share_link')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
<Link <Link
...@@ -95,7 +102,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -95,7 +102,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
</Link> </Link>
)} )}
</Flex> </Flex>
<Box fontSize={'mini'} color={'myGray.600'}> <Box fontSize={'mini'} fontWeight={'normal'} color={'myGray.600'}>
{t('common:core.app.Share link desc detail')} {t('common:core.app.Share link desc detail')}
</Box> </Box>
</Box> </Box>
...@@ -121,36 +128,16 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -121,36 +128,16 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
<Thead> <Thead>
<Tr> <Tr>
<Th>{t('common:Name')}</Th> <Th>{t('common:Name')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:expired_time')}</Th>
</>
)}
<Th>{t('common:support.outlink.Usage points')}</Th> <Th>{t('common:support.outlink.Usage points')}</Th>
<Th>{t('common:core.app.share.Is response quote')}</Th> {feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:core.app.share.Role check')}</Th>
</>
)}
<Th>{t('common:last_use_time')}</Th> <Th>{t('common:last_use_time')}</Th>
<Th></Th> <Th>{t('common:Action')}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{shareChatList.map((item) => ( {shareChatList.map((item) => (
<Tr key={item._id}> <Tr key={item._id}>
<Td>{item.name}</Td> <Td>{item.name}</Td>
{feConfigs?.isPlus && (
<>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY-MM-DD HH:mm')
: '-'}
</Td>
</>
)}
<Td> <Td>
{Math.round(item.usagePoints)} {Math.round(item.usagePoints)}
{feConfigs?.isPlus {feConfigs?.isPlus
...@@ -161,13 +148,12 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -161,13 +148,12 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
}` }`
: ''} : ''}
</Td> </Td>
<Td>{item.showCite ? '✔' : '✖'}</Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<> <Td>
<Td>{item?.limit?.QPM || '-'}</Td> {item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY-MM-DD HH:mm')
<Th>{item?.limit?.hookUrl ? '✔' : '✖'}</Th> : '-'}
</> </Td>
)} )}
<Td> <Td>
{item.lastTime {item.lastTime
...@@ -221,7 +207,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -221,7 +207,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetchShareChatList(); void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -251,7 +240,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { ...@@ -251,7 +240,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
onCreate={(id) => { onCreate={(id) => {
const url = `${location.origin}/chat/share?shareId=${id}`; const url = `${location.origin}/chat/share?shareId=${id}`;
copyData(url, t('common:core.app.share.Create link tip')); copyData(url, t('common:core.app.share.Create link tip'));
refetchShareChatList(); void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditLinkData(undefined); setEditLinkData(undefined);
}} }}
onEdit={() => { onEdit={() => {
...@@ -348,7 +337,9 @@ function EditLinkModal({ ...@@ -348,7 +337,9 @@ function EditLinkModal({
{t('publish:basic_info')} {t('publish:basic_info')}
</Box> </Box>
<Flex alignItems={'center'} mt={4}> <Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel> <FormLabel flex={'0 0 90px'} required>
{t('common:Name')}
</FormLabel>
<Input <Input
placeholder={t('publish:link_name')} placeholder={t('publish:link_name')}
maxLength={100} maxLength={100}
......
...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc'; ...@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const OffiAccountEditModal = dynamic(() => import('./OffiAccountEditModal')); const OffiAccountEditModal = dynamic(() => import('./OffiAccountEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal')); const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const OffiAccount = ({ appId }: { appId: string }) => { const OffiAccount = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -66,11 +72,11 @@ const OffiAccount = ({ appId }: { appId: string }) => { ...@@ -66,11 +72,11 @@ const OffiAccount = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null); const [showShareLink, setShowShareLink] = useState<string | null>(null);
return ( return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}> <Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row"> <Flex justifyContent={'space-between'} flexDirection="row">
<HStack> <HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:official_account.name')} {t('publish:official_account.title')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
...@@ -114,14 +120,9 @@ const OffiAccount = ({ appId }: { appId: string }) => { ...@@ -114,14 +120,9 @@ const OffiAccount = ({ appId }: { appId: string }) => {
<Tr> <Tr>
<Th>{t('common:Name')} </Th> <Th>{t('common:Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th> <Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && <Th>{t('common:expired_time')} </Th>}
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:expired_time')} </Th>
</>
)}
<Th>{t('common:last_use_time')} </Th> <Th>{t('common:last_use_time')} </Th>
<Th> </Th> <Th>{t('common:Action')} </Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
...@@ -139,14 +140,11 @@ const OffiAccount = ({ appId }: { appId: string }) => { ...@@ -139,14 +140,11 @@ const OffiAccount = ({ appId }: { appId: string }) => {
: ''} : ''}
</Td> </Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td> <Td>
{item?.limit?.expiredTime {item.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'} : '-'}
</Td> </Td>
</>
)} )}
<Td> <Td>
{item.lastTime {item.lastTime
...@@ -202,7 +200,10 @@ const OffiAccount = ({ appId }: { appId: string }) => { ...@@ -202,7 +200,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetchShareChatList(); void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -223,7 +224,10 @@ const OffiAccount = ({ appId }: { appId: string }) => { ...@@ -223,7 +224,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
<OffiAccountEditModal <OffiAccountEditModal
appId={appId} appId={appId}
defaultData={editOffiAccountData} defaultData={editOffiAccountData}
onCreate={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])} onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditOffiAccountData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])} onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
onClose={() => setEditOffiAccountData(undefined)} onClose={() => setEditOffiAccountData(undefined)}
isEdit={isEdit} isEdit={isEdit}
......
...@@ -82,8 +82,8 @@ const PlaygroundVisibilityConfig = ({ appId }: { appId: string }) => { ...@@ -82,8 +82,8 @@ const PlaygroundVisibilityConfig = ({ appId }: { appId: string }) => {
}; };
return ( return (
<Flex flexDirection="column"> <Flex flexDirection="column" p={6}>
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'} mb={3}> <Box fontSize={'lg'} fontWeight={'medium'} color={'myGray.900'} mb={3}>
{t('app:publish.playground_link')} {t('app:publish.playground_link')}
</Box> </Box>
......
...@@ -33,7 +33,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index'; ...@@ -33,7 +33,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
const WechatEditModal = dynamic(() => import('./WechatEditModal')); const WechatEditModal = dynamic(() => import('./WechatEditModal'));
const QRLoginModal = dynamic(() => import('./QRLoginModal')); const QRLoginModal = dynamic(() => import('./QRLoginModal'));
const Wechat = ({ appId }: { appId: string }) => { const Wechat = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -64,10 +70,10 @@ const Wechat = ({ appId }: { appId: string }) => { ...@@ -64,10 +70,10 @@ const Wechat = ({ appId }: { appId: string }) => {
}; };
return ( return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}> <Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'}> <Flex justifyContent={'space-between'}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:wechat.title')} {t('publish:wechat.title')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
...@@ -109,7 +115,7 @@ const Wechat = ({ appId }: { appId: string }) => { ...@@ -109,7 +115,7 @@ const Wechat = ({ appId }: { appId: string }) => {
<Th>{t('publish:wechat.status')}</Th> <Th>{t('publish:wechat.status')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th> <Th>{t('common:support.outlink.Usage points')}</Th>
<Th>{t('common:last_use_time')}</Th> <Th>{t('common:last_use_time')}</Th>
<Th /> <Th>{t('common:Action')}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
...@@ -197,7 +203,7 @@ const Wechat = ({ appId }: { appId: string }) => { ...@@ -197,7 +203,7 @@ const Wechat = ({ appId }: { appId: string }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetch(); void Promise.all([refetch(), onRefreshOutLinkCounts()]);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
...@@ -224,7 +230,7 @@ const Wechat = ({ appId }: { appId: string }) => { ...@@ -224,7 +230,7 @@ const Wechat = ({ appId }: { appId: string }) => {
defaultData={editData} defaultData={editData}
isEdit={isEdit} isEdit={isEdit}
onCreate={async (shareId) => { onCreate={async (shareId) => {
const newList = await refetch(); const [newList] = await Promise.all([refetch(), onRefreshOutLinkCounts()]);
return newList?.find((i) => i.shareId === shareId)?._id; return newList?.find((i) => i.shareId === shareId)?._id;
}} }}
onEdit={() => refetch()} onEdit={() => refetch()}
......
...@@ -34,7 +34,13 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils'; ...@@ -34,7 +34,13 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils';
const WecomEditModal = dynamic(() => import('./WecomEditModal')); const WecomEditModal = dynamic(() => import('./WecomEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal')); const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const Wecom = ({ appId }: { appId: string }) => { const Wecom = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
...@@ -68,10 +74,10 @@ const Wecom = ({ appId }: { appId: string }) => { ...@@ -68,10 +74,10 @@ const Wecom = ({ appId }: { appId: string }) => {
}); });
return ( return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}> <Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row"> <Flex justifyContent={'space-between'} flexDirection="row">
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:wecom.title')} {t('publish:wecom.title')}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
...@@ -127,14 +133,9 @@ const Wecom = ({ appId }: { appId: string }) => { ...@@ -127,14 +133,9 @@ const Wecom = ({ appId }: { appId: string }) => {
<Tr> <Tr>
<Th>{t('common:Name')} </Th> <Th>{t('common:Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th> <Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && <Th>{t('common:expired_time')} </Th>}
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:expired_time')} </Th>
</>
)}
<Th>{t('common:last_use_time')} </Th> <Th>{t('common:last_use_time')} </Th>
<Th> </Th> <Th>{t('common:Action')} </Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
...@@ -152,14 +153,11 @@ const Wecom = ({ appId }: { appId: string }) => { ...@@ -152,14 +153,11 @@ const Wecom = ({ appId }: { appId: string }) => {
: ''} : ''}
</Td> </Td>
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td> <Td>
{item?.limit?.expiredTime {item.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'} : '-'}
</Td> </Td>
</>
)} )}
<Td> <Td>
{item.lastTime {item.lastTime
...@@ -215,7 +213,10 @@ const Wecom = ({ appId }: { appId: string }) => { ...@@ -215,7 +213,10 @@ const Wecom = ({ appId }: { appId: string }) => {
setIsLoading(true); setIsLoading(true);
try { try {
await delShareChatById(item._id); await delShareChatById(item._id);
refetchShareChatList(); void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -237,7 +238,7 @@ const Wecom = ({ appId }: { appId: string }) => { ...@@ -237,7 +238,7 @@ const Wecom = ({ appId }: { appId: string }) => {
appId={appId} appId={appId}
defaultData={editWecomData} defaultData={editWecomData}
onCreate={async (shareId: string) => { onCreate={async (shareId: string) => {
const newList = await refetchShareChatList(); const [newList] = await Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
const newItem = newList.find((item) => item.shareId === shareId); const newItem = newList.find((item) => item.shareId === shareId);
return newItem?._id; return newItem?._id;
}} }}
......
...@@ -5,7 +5,7 @@ import { useContextSelector } from 'use-context-selector'; ...@@ -5,7 +5,7 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context'; import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import Header from './Header'; import Header from './Header';
import { Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants'; import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
...@@ -44,15 +44,17 @@ const WorkflowEdit = () => { ...@@ -44,15 +44,17 @@ const WorkflowEdit = () => {
flexDirection={'column'} flexDirection={'column'}
flex={1} flex={1}
minH={0} minH={0}
mt={'72px'} mt={['8px', '72px']}
px={4}
pb={4}
bg={'white'} bg={'white'}
overflowY={'auto'} overflowY={'auto'}
overflowX={'hidden'} overflowX={'hidden'}
> >
{currentTab === TabEnum.publish && <PublishChannel />} {currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />} {currentTab === TabEnum.logs && (
<Box px={4} pb={4} h={'full'}>
<Logs />
</Box>
)}
</Flex> </Flex>
)} )}
</Flex> </Flex>
......
import { Types } from '@fastgpt/service/common/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { NextAPI } from '@/service/middleware/entry';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import {
OutLinkCountQuerySchema,
OutLinkCountResponseSchema,
type OutLinkCountQueryType,
type OutLinkCountResponseType
} from '@fastgpt/global/openapi/support/outLink/api';
const countTypes = [
PublishChannelEnum.share,
PublishChannelEnum.feishu,
PublishChannelEnum.dingtalk,
PublishChannelEnum.wecom,
PublishChannelEnum.wechat,
PublishChannelEnum.officialAccount
] as const;
type OutLinkCountType = (typeof countTypes)[number];
/** 获取侧栏展示的发布渠道连接数,缺失渠道补零。 */
async function handler(
req: ApiRequestProps<undefined, OutLinkCountQueryType>
): Promise<OutLinkCountResponseType> {
const { appId } = parseApiInput({
req,
querySchema: OutLinkCountQuerySchema
}).query;
const { teamId } = await authApp({
req,
authToken: true,
appId,
per: ManagePermissionVal
});
const groupedCounts = await MongoOutLink.aggregate<{ _id: OutLinkCountType; count: number }>([
{
$match: {
teamId: new Types.ObjectId(teamId),
appId: new Types.ObjectId(appId),
type: { $in: countTypes }
}
},
{
$group: {
_id: '$type',
count: { $sum: 1 }
}
}
]);
const counts: Record<OutLinkCountType, number> = {
[PublishChannelEnum.share]: 0,
[PublishChannelEnum.feishu]: 0,
[PublishChannelEnum.dingtalk]: 0,
[PublishChannelEnum.wecom]: 0,
[PublishChannelEnum.wechat]: 0,
[PublishChannelEnum.officialAccount]: 0
};
groupedCounts.forEach(({ _id, count }) => {
counts[_id] = count;
});
return OutLinkCountResponseSchema.parse(counts);
}
export default NextAPI(handler);
...@@ -11,6 +11,8 @@ import type { ...@@ -11,6 +11,8 @@ import type {
import type { import type {
OutLinkCreateBodyType, OutLinkCreateBodyType,
OutLinkCreateResponseType, OutLinkCreateResponseType,
OutLinkCountQueryType,
OutLinkCountResponseType,
OutLinkDeleteQueryType, OutLinkDeleteQueryType,
OutLinkDeleteResponseType, OutLinkDeleteResponseType,
OutLinkListQueryType, OutLinkListQueryType,
...@@ -37,6 +39,9 @@ export function getShareChatList(data: OutLinkListQueryType) { ...@@ -37,6 +39,9 @@ export function getShareChatList(data: OutLinkListQueryType) {
return GET<OutLinkListResponseType>(`/support/outLink/list`, data); return GET<OutLinkListResponseType>(`/support/outLink/list`, data);
} }
export const getOutLinkCounts = (data: OutLinkCountQueryType) =>
GET<OutLinkCountResponseType>('/support/outLink/count', data);
// delete a shareChat // delete a shareChat
export function delShareChatById(id: OutLinkDeleteQueryType['id']) { export function delShareChatById(id: OutLinkDeleteQueryType['id']) {
return DELETE<OutLinkDeleteResponseType>(`/support/outLink/delete?id=${id}`); return DELETE<OutLinkDeleteResponseType>(`/support/outLink/delete?id=${id}`);
......
import type { OutLinkCountResponseType } from '@fastgpt/global/openapi/support/outLink/api';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { getRootUser } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { beforeEach, describe, expect, it } from 'vitest';
import * as countApi from '@/pages/api/support/outLink/count';
describe('OutLink Count API', () => {
let rootUser: Awaited<ReturnType<typeof getRootUser>>;
let testApp: Awaited<ReturnType<typeof MongoApp.create>>;
let otherApp: Awaited<ReturnType<typeof MongoApp.create>>;
let emptyApp: Awaited<ReturnType<typeof MongoApp.create>>;
beforeEach(async () => {
rootUser = await getRootUser();
const appData = {
type: 'simple' as const,
tmbId: rootUser.tmbId,
teamId: rootUser.teamId
};
[testApp, otherApp, emptyApp] = await Promise.all([
MongoApp.create({ ...appData, name: 'Test App for OutLink Count' }),
MongoApp.create({ ...appData, name: 'Other App for OutLink Count' }),
MongoApp.create({ ...appData, name: 'Empty App for OutLink Count' })
]);
});
const createOutLink = (appId: string, type: PublishChannelEnum) =>
MongoOutLink.create({
shareId: `${appId}-${type}-${Math.random()}`,
teamId: rootUser.teamId,
tmbId: rootUser.tmbId,
appId,
name: `Test ${type}`,
type
});
it('aggregates counted channels, fills missing values, excludes other types and isolates apps', async () => {
await Promise.all([
createOutLink(testApp._id, PublishChannelEnum.share),
createOutLink(testApp._id, PublishChannelEnum.share),
createOutLink(testApp._id, PublishChannelEnum.feishu),
createOutLink(testApp._id, PublishChannelEnum.wecom),
createOutLink(testApp._id, PublishChannelEnum.officialAccount),
createOutLink(testApp._id, PublishChannelEnum.apikey),
createOutLink(testApp._id, PublishChannelEnum.playground),
createOutLink(testApp._id, PublishChannelEnum.iframe),
createOutLink(otherApp._id, PublishChannelEnum.share),
createOutLink(otherApp._id, PublishChannelEnum.dingtalk),
createOutLink(otherApp._id, PublishChannelEnum.wechat)
]);
const res = await Call<OutLinkCountResponseType>(countApi.default, {
auth: rootUser,
query: { appId: testApp._id }
});
expect(res.code).toBe(200);
expect(res.data).toEqual({
share: 2,
feishu: 1,
dingtalk: 0,
wecom: 1,
wechat: 0,
official_account: 1
});
});
it('returns zero for every countable channel without configurations', async () => {
const res = await Call<OutLinkCountResponseType>(countApi.default, {
auth: rootUser,
query: { appId: emptyApp._id }
});
expect(res.code).toBe(200);
expect(res.data).toEqual({
share: 0,
feishu: 0,
dingtalk: 0,
wecom: 0,
wechat: 0,
official_account: 0
});
});
it('rejects a missing appId', async () => {
const res = await Call<OutLinkCountResponseType>(countApi.default, {
auth: rootUser,
query: {}
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('rejects unauthenticated requests', async () => {
const res = await Call<OutLinkCountResponseType>(countApi.default, {
query: { appId: testApp._id }
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
});
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