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({
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: 创建发布渠道
* Route: POST /api/support/outLink/create
* Method: POST
......
......@@ -3,6 +3,8 @@ import { DevApiTagsMap } from '../../tag';
import {
OutLinkCreateBodySchema,
OutLinkCreateResponseSchema,
OutLinkCountQuerySchema,
OutLinkCountResponseSchema,
OutLinkDeleteQuerySchema,
OutLinkDeleteResponseSchema,
OutLinkListQuerySchema,
......@@ -18,6 +20,26 @@ import {
} from './api';
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': {
get: {
summary: '获取应用的发布渠道列表',
......
......@@ -113,6 +113,7 @@ const logger = getLogger(LogCategories.INFRA.MONGO);
defineIndex(OutLinkSchema, { key: { shareId: -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
defineIndex(OutLinkSchema, {
key: { type: 1, 'app.status': 1 },
......
......@@ -500,6 +500,7 @@ export const iconPaths = {
'support/config/configFill': () => import('./icons/support/config/configFill.svg'),
'support/config/configLight': () => import('./icons/support/config/configLight.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/privateLight': () => import('./icons/support/permission/privateLight.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 @@
"feishu_bot_desc": "Connect to Feishu Bot directly via API",
"key_alias": "Key alias, for display only",
"link_name": "Share Link Name",
"native_channels": "Native Channels",
"new_feishu_bot": "add_new Feishu Bot",
"official_account.create_modal_title": "Create WeChat Official Account Integration",
"official_account.desc": "Connect to WeChat Official Account directly via API",
"official_account.edit_modal_title": "Edit WeChat Official Account Integration",
"official_account.name": "WeChat Official Account Integration",
"official_account.params": "WeChat Official Account Parameters",
"official_account.title": "WeChat Official Account",
"private_config": "Visibility configuration",
"publish_name": "Name",
"qpm_is_empty": "QPM cannot be empty",
......@@ -36,6 +38,7 @@
"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_use_cases": "View Token Authentication Guide",
"third_party_publish_channels": "Third-Party Channels",
"use_default_domain": "Use Default Domain",
"wechat.bot": "WeChat Personal",
"wechat.bot_desc": "Connect to WeChat personal chat via ClawBot",
......
......@@ -18,12 +18,14 @@
"feishu_bot_desc": "通过 API 直接接入飞书机器人",
"key_alias": "key 的别名,仅用于展示",
"link_name": "分享链接的名字",
"native_channels": "原生渠道",
"new_feishu_bot": "新增飞书机器人",
"official_account.create_modal_title": "创建微信公众号接入",
"official_account.desc": "通过 API 直接接入微信公众号",
"official_account.edit_modal_title": "编辑微信公众号接入",
"official_account.name": "微信公众号接入",
"official_account.params": "微信公众号参数",
"official_account.title": "微信公众号",
"private_config": "可见度配置",
"publish_name": "名称",
"qpm_is_empty": "QPM 不能为空",
......@@ -36,6 +38,7 @@
"token_auth": "身份验证",
"token_auth_tips": "身份校验服务器地址",
"token_auth_use_cases": "查看身份验证使用说明",
"third_party_publish_channels": "第三方发布渠道",
"use_default_domain": "使用默认域名",
"wechat.bot": "微信个人号",
"wechat.bot_desc": "通过 ClawBot 接入微信个人号聊天",
......
......@@ -18,12 +18,14 @@
"feishu_bot_desc": "透過 API 直接連結 Lark 聊天機器人",
"key_alias": "金鑰別名,僅供顯示",
"link_name": "分享連結名稱",
"native_channels": "原生渠道",
"new_feishu_bot": "新增 Lark 聊天機器人",
"official_account.create_modal_title": "建立微信公眾號整合",
"official_account.desc": "透過 API 直接連結微信公眾號",
"official_account.edit_modal_title": "編輯微信公眾號整合",
"official_account.name": "微信公眾號整合",
"official_account.params": "微信公眾號參數",
"official_account.title": "微信公眾號",
"private_config": "顯示設定",
"publish_name": "名稱",
"qpm_is_empty": "QPM 不可為空白",
......@@ -36,6 +38,7 @@
"token_auth": "身分驗證",
"token_auth_tips": "身分驗證伺服器網址。若有提供,每次對話前將向指定伺服器傳送驗證請求。",
"token_auth_use_cases": "檢視身分驗證指南",
"third_party_publish_channels": "第三方發布渠道",
"use_default_domain": "使用預設域名",
"wechat.bot": "微信個人號",
"wechat.bot_desc": "透過 ClawBot 連結微信個人號聊天",
......
......@@ -64,7 +64,15 @@ const AgentEdit = () => {
);
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
appForm={appForm}
forbiddenSaveSnapshot={forbiddenSaveSnapshot}
......
......@@ -200,7 +200,7 @@ const Header = ({
</Flex>
)}
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
<Box flex={'1'}>
<Box flex={'1'} ml={'16px'}>
<FolderPath
rootName={t('common:All')}
paths={paths}
......
......@@ -61,7 +61,15 @@ const SimpleEdit = () => {
);
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
appForm={appForm}
forbiddenSaveSnapshot={forbiddenSaveSnapshot}
......
......@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const DingTalkEditModal = dynamic(() => import('./DingTalkEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const DingTalk = ({ appId }: { appId: string }) => {
const DingTalk = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -66,10 +72,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:dingtalk.title')}
</Box>
{feConfigs?.docUrl && (
......@@ -112,14 +118,9 @@ const DingTalk = ({ appId }: { appId: string }) => {
<Tr>
<Th>{t('common:Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:expired_time')}</Th>
</>
)}
{feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
<Th>{t('common:last_use_time')}</Th>
<Th></Th>
<Th>{t('common:Action')}</Th>
</Tr>
</Thead>
<Tbody>
......@@ -137,14 +138,11 @@ const DingTalk = ({ appId }: { appId: string }) => {
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
)}
<Td>
{item.lastTime
......@@ -200,7 +198,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) {
console.log(error);
}
......@@ -221,7 +222,10 @@ const DingTalk = ({ appId }: { appId: string }) => {
<DingTalkEditModal
appId={appId}
defaultData={editDingTalkLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditDingTalkLinkData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onClose={() => setEditDingTalkLinkData(undefined)}
isEdit={isEdit}
......
......@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const FeiShuEditModal = dynamic(() => import('./FeiShuEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const FeiShu = ({ appId }: { appId: string }) => {
const FeiShu = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -65,10 +71,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('common:core.app.publish.Fei shu bot publish')}
</Box>
{feConfigs?.docUrl && (
......@@ -111,14 +117,9 @@ const FeiShu = ({ appId }: { appId: string }) => {
<Tr>
<Th>{t('common:Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:expired_time')}</Th>
</>
)}
{feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
<Th>{t('common:last_use_time')}</Th>
<Th></Th>
<Th>{t('common:Action')}</Th>
</Tr>
</Thead>
<Tbody>
......@@ -136,14 +137,11 @@ const FeiShu = ({ appId }: { appId: string }) => {
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
)}
<Td>
{item.lastTime
......@@ -199,7 +197,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) {
console.log(error);
}
......@@ -220,7 +221,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
<FeiShuEditModal
appId={appId}
defaultData={editFeiShuLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditFeiShuLinkData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onClose={() => setEditFeiShuLinkData(undefined)}
isEdit={isEdit}
......
......@@ -50,7 +50,14 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
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 { setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -72,13 +79,13 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
);
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">
<HStack>
<Box>
<Flex alignItems={'center'}>
<Box color={'myGray.900'} fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common:core.app.Share link')}
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('common:share_link')}
</Box>
{feConfigs?.docUrl && (
<Link
......@@ -95,7 +102,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
</Link>
)}
</Flex>
<Box fontSize={'mini'} color={'myGray.600'}>
<Box fontSize={'mini'} fontWeight={'normal'} color={'myGray.600'}>
{t('common:core.app.Share link desc detail')}
</Box>
</Box>
......@@ -121,36 +128,16 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
<Thead>
<Tr>
<Th>{t('common:Name')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:expired_time')}</Th>
</>
)}
<Th>{t('common:support.outlink.Usage points')}</Th>
<Th>{t('common:core.app.share.Is response quote')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:core.app.share.Role check')}</Th>
</>
)}
{feConfigs?.isPlus && <Th>{t('common:expired_time')}</Th>}
<Th>{t('common:last_use_time')}</Th>
<Th></Th>
<Th>{t('common:Action')}</Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
{feConfigs?.isPlus && (
<>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY-MM-DD HH:mm')
: '-'}
</Td>
</>
)}
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
......@@ -161,13 +148,12 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
}`
: ''}
</Td>
<Td>{item.showCite ? '✔' : '✖'}</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Th>{item?.limit?.hookUrl ? '✔' : '✖'}</Th>
</>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY-MM-DD HH:mm')
: '-'}
</Td>
)}
<Td>
{item.lastTime
......@@ -221,7 +207,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) {
console.log(error);
}
......@@ -251,7 +240,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
onCreate={(id) => {
const url = `${location.origin}/chat/share?shareId=${id}`;
copyData(url, t('common:core.app.share.Create link tip'));
refetchShareChatList();
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditLinkData(undefined);
}}
onEdit={() => {
......@@ -348,7 +337,9 @@ function EditLinkModal({
{t('publish:basic_info')}
</Box>
<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
placeholder={t('publish:link_name')}
maxLength={100}
......
......@@ -33,7 +33,13 @@ import { getDocPath } from '@/web/common/system/doc';
const OffiAccountEditModal = dynamic(() => import('./OffiAccountEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const OffiAccount = ({ appId }: { appId: string }) => {
const OffiAccount = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -66,11 +72,11 @@ const OffiAccount = ({ appId }: { appId: string }) => {
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('publish:official_account.name')}
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:official_account.title')}
</Box>
{feConfigs?.docUrl && (
......@@ -114,14 +120,9 @@ const OffiAccount = ({ appId }: { appId: string }) => {
<Tr>
<Th>{t('common:Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:expired_time')} </Th>
</>
)}
{feConfigs?.isPlus && <Th>{t('common:expired_time')} </Th>}
<Th>{t('common:last_use_time')} </Th>
<Th> </Th>
<Th>{t('common:Action')} </Th>
</Tr>
</Thead>
<Tbody>
......@@ -139,14 +140,11 @@ const OffiAccount = ({ appId }: { appId: string }) => {
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
)}
<Td>
{item.lastTime
......@@ -202,7 +200,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) {
console.log(error);
}
......@@ -223,7 +224,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
<OffiAccountEditModal
appId={appId}
defaultData={editOffiAccountData}
onCreate={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
onCreate={() => {
void Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
setEditOffiAccountData(undefined);
}}
onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
onClose={() => setEditOffiAccountData(undefined)}
isEdit={isEdit}
......
......@@ -82,8 +82,8 @@ const PlaygroundVisibilityConfig = ({ appId }: { appId: string }) => {
};
return (
<Flex flexDirection="column">
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'} mb={3}>
<Flex flexDirection="column" p={6}>
<Box fontSize={'lg'} fontWeight={'medium'} color={'myGray.900'} mb={3}>
{t('app:publish.playground_link')}
</Box>
......
......@@ -33,7 +33,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
const WechatEditModal = dynamic(() => import('./WechatEditModal'));
const QRLoginModal = dynamic(() => import('./QRLoginModal'));
const Wechat = ({ appId }: { appId: string }) => {
const Wechat = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -64,10 +70,10 @@ const Wechat = ({ appId }: { appId: string }) => {
};
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:wechat.title')}
</Box>
{feConfigs?.docUrl && (
......@@ -109,7 +115,7 @@ const Wechat = ({ appId }: { appId: string }) => {
<Th>{t('publish:wechat.status')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
<Th>{t('common:last_use_time')}</Th>
<Th />
<Th>{t('common:Action')}</Th>
</Tr>
</Thead>
<Tbody>
......@@ -197,7 +203,7 @@ const Wechat = ({ appId }: { appId: string }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetch();
void Promise.all([refetch(), onRefreshOutLinkCounts()]);
} finally {
setIsLoading(false);
}
......@@ -224,7 +230,7 @@ const Wechat = ({ appId }: { appId: string }) => {
defaultData={editData}
isEdit={isEdit}
onCreate={async (shareId) => {
const newList = await refetch();
const [newList] = await Promise.all([refetch(), onRefreshOutLinkCounts()]);
return newList?.find((i) => i.shareId === shareId)?._id;
}}
onEdit={() => refetch()}
......
......@@ -34,7 +34,13 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils';
const WecomEditModal = dynamic(() => import('./WecomEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const Wecom = ({ appId }: { appId: string }) => {
const Wecom = ({
appId,
onRefreshOutLinkCounts
}: {
appId: string;
onRefreshOutLinkCounts: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
......@@ -68,10 +74,10 @@ const Wecom = ({ appId }: { appId: string }) => {
});
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Box position={'relative'} p={6} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
<Box color={'myGray.900'} fontWeight={'medium'} fontSize={'lg'}>
{t('publish:wecom.title')}
</Box>
{feConfigs?.docUrl && (
......@@ -127,14 +133,9 @@ const Wecom = ({ appId }: { appId: string }) => {
<Tr>
<Th>{t('common:Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:expired_time')} </Th>
</>
)}
{feConfigs?.isPlus && <Th>{t('common:expired_time')} </Th>}
<Th>{t('common:last_use_time')} </Th>
<Th> </Th>
<Th>{t('common:Action')} </Th>
</Tr>
</Thead>
<Tbody>
......@@ -152,14 +153,11 @@ const Wecom = ({ appId }: { appId: string }) => {
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
)}
<Td>
{item.lastTime
......@@ -215,7 +213,10 @@ const Wecom = ({ appId }: { appId: string }) => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
void Promise.all([
refetchShareChatList(),
onRefreshOutLinkCounts()
]);
} catch (error) {
console.log(error);
}
......@@ -237,7 +238,7 @@ const Wecom = ({ appId }: { appId: string }) => {
appId={appId}
defaultData={editWecomData}
onCreate={async (shareId: string) => {
const newList = await refetchShareChatList();
const [newList] = await Promise.all([refetchShareChatList(), onRefreshOutLinkCounts()]);
const newItem = newList.find((item) => item.shareId === shareId);
return newItem?._id;
}}
......
......@@ -5,7 +5,7 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash-es';
......@@ -44,15 +44,17 @@ const WorkflowEdit = () => {
flexDirection={'column'}
flex={1}
minH={0}
mt={'72px'}
px={4}
pb={4}
mt={['8px', '72px']}
bg={'white'}
overflowY={'auto'}
overflowX={'hidden'}
>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
{currentTab === TabEnum.logs && (
<Box px={4} pb={4} h={'full'}>
<Logs />
</Box>
)}
</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 {
import type {
OutLinkCreateBodyType,
OutLinkCreateResponseType,
OutLinkCountQueryType,
OutLinkCountResponseType,
OutLinkDeleteQueryType,
OutLinkDeleteResponseType,
OutLinkListQueryType,
......@@ -37,6 +39,9 @@ export function getShareChatList(data: OutLinkListQueryType) {
return GET<OutLinkListResponseType>(`/support/outLink/list`, data);
}
export const getOutLinkCounts = (data: OutLinkCountQueryType) =>
GET<OutLinkCountResponseType>('/support/outLink/count', data);
// delete a shareChat
export function delShareChatById(id: OutLinkDeleteQueryType['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