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 連結微信個人號聊天",
......
...@@ -78,6 +78,17 @@ type ApiKeyTableProps = { ...@@ -78,6 +78,17 @@ type ApiKeyTableProps = {
appId?: string; appId?: string;
}; };
const apiKeyTableFields = [
'name',
'apiKey',
'usagePoints',
'expiredTime',
'lastUsedTime',
'createTime',
'actions'
] as const;
type ApiKeyTableField = (typeof apiKeyTableFields)[number];
const isSameTagIds = (left: string[], right: string[]) => { const isSameTagIds = (left: string[], right: string[]) => {
if (left.length !== right.length) return false; if (left.length !== right.length) return false;
...@@ -284,6 +295,158 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => { ...@@ -284,6 +295,158 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => {
} }
); );
const tableHeaders: Record<ApiKeyTableField, { label: React.ReactNode; width?: string }> = {
name: { label: t('common:Name'), width: '240px' },
apiKey: { label: 'API KEY', width: '130px' },
usagePoints: { label: t('common:support.outlink.Usage points'), width: '150px' },
expiredTime: { label: t('common:expired_time'), width: '120px' },
lastUsedTime: { label: t('account_apikey:last_used_time'), width: '160px' },
createTime: { label: t('account_apikey:create_time'), width: '160px' },
actions: { label: t('common:Action'), width: '92px' }
};
const tableColumns = apiKeyTableFields
.filter((field) => {
if (field === 'usagePoints' || field === 'expiredTime') return hasUsagePlan;
return field !== 'createTime' || !isPublishMode;
})
.map((field) => ({ field, ...tableHeaders[field] }));
const renderTableCell = (field: ApiKeyTableField, item: (typeof apiKeys)[number]) => {
switch (field) {
case 'name':
return (
<Td key={field} maxW={'240px'}>
<Flex flexDirection={'column'} minW={0}>
<MyTooltip label={item.name} showOnlyWhenOverflow>
<Box
maxW={'220px'}
overflow={'hidden'}
textOverflow={'ellipsis'}
whiteSpace={'nowrap'}
>
{item.name}
</Box>
</MyTooltip>
<ApiKeyTagEditor
key={`${item._id}-${(item.tagIds || []).join(',')}`}
apiKeyId={item._id}
appName={item.appName}
tagIds={item.tagIds || []}
allTags={openApiTags}
onSave={async (apiKeyId, tagIds) => {
await onUpdateApiKeyTags({
apiKeyId,
tagIds
});
}}
onManage={() => setShowTagManage(true)}
onCreateTag={onCreateTagFromSelect}
isLoading={isGettingTags || isUpdatingApiKeyTags}
/>
</Flex>
</Td>
);
case 'apiKey':
return (
<Td key={field} maxW={'130px'}>
<Flex alignItems={'center'} gap={1} role={'group'} minW={0}>
<Box minW={0} overflow={'hidden'} textOverflow={'ellipsis'} whiteSpace={'nowrap'}>
{maskApiKey(item.apiKey)}
</Box>
{item.canCopy && (
<MyIcon
name={copyingApiKeyId === item._id ? 'common/loading' : 'copy'}
w={'15px'}
flexShrink={0}
aria-label={t('common:Copy')}
role={'button'}
tabIndex={0}
color={'myGray.600'}
opacity={copyingApiKeyId === item._id ? 1 : 0}
visibility={copyingApiKeyId === item._id ? 'visible' : 'hidden'}
cursor={'pointer'}
transition={'opacity 0.15s ease, color 0.15s ease'}
_groupHover={{ opacity: 1, visibility: 'visible' }}
_hover={{ color: 'primary.600' }}
onClick={() => onCopyApiKey(item._id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onCopyApiKey(item._id);
}
}}
/>
)}
</Flex>
</Td>
);
case 'usagePoints':
return (
<Td key={field} whiteSpace={'nowrap'}>
{Math.round(item.usagePoints)}/
{item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? `${item.limit.maxUsagePoints}`
: t('common:Unlimited')}
</Td>
);
case 'expiredTime':
return (
<Td key={field} whiteSpace={'pre-wrap'}>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
);
case 'lastUsedTime':
return (
<Td key={field} whiteSpace={'normal'}>
{item.lastUsedTime
? dayjs(item.lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
: t('common:un_used')}
</Td>
);
case 'createTime':
return (
<Td key={field} whiteSpace={'normal'}>
{dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss')}
</Td>
);
case 'actions':
return (
<Td key={field} w={'92px'}>
<Flex alignItems={'center'} gap={2}>
<MyTooltip label={t('common:Edit')}>
<IconButton
icon={<MyIcon name={'edit'} w={4} />}
variant={'whitePrimary'}
size={'sm'}
aria-label={t('common:Edit')}
onClick={() =>
setEditData({
_id: item._id,
name: item.name,
limit: item.limit,
authProxy: item.authProxy,
tags: item.tagIds || []
})
}
/>
</MyTooltip>
<MyTooltip label={t('common:Delete')}>
<IconButton
icon={<MyIcon name={'delete'} w={4} />}
variant={'whiteDanger'}
size={'sm'}
aria-label={t('common:Delete')}
onClick={() => openConfirm({ onConfirm: () => onclickRemove(item._id) })()}
/>
</MyTooltip>
</Flex>
</Td>
);
}
};
return ( return (
<MyBox <MyBox
isLoading={isGetting} isLoading={isGetting}
...@@ -291,14 +454,17 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => { ...@@ -291,14 +454,17 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => {
flexDirection={'column'} flexDirection={'column'}
h={'100%'} h={'100%'}
position={'relative'} position={'relative'}
pt={0} p={isPublishMode ? 6 : 0}
px={0}
minH={isPublishMode ? '50vh' : undefined} minH={isPublishMode ? '50vh' : undefined}
> >
<Flex flexDirection={'column'} alignItems={'stretch'} gap={3}> <Flex flexDirection={'column'} alignItems={'stretch'} gap={3}>
<Flex minW={0} alignItems={'center'}> <Flex minW={0} alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}> <Box
{t('common:support.openapi.Api manager')}({apiKeys.length}) color={isPublishMode ? 'myGray.900' : undefined}
fontWeight={isPublishMode ? 'medium' : 'bold'}
fontSize={isPublishMode ? 'lg' : ['md', 'lg']}
>
{`${t('common:support.openapi.Api manager')}(${apiKeys.length})`}
</Box> </Box>
{feConfigs?.docUrl && ( {feConfigs?.docUrl && (
<Link <Link
...@@ -316,7 +482,7 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => { ...@@ -316,7 +482,7 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => {
)} )}
</Flex> </Flex>
<Flex <Flex
alignItems={['stretch', 'center']} alignItems={['stretch', 'flex-end']}
justifyContent={'space-between'} justifyContent={'space-between'}
gap={3} gap={3}
minW={0} minW={0}
...@@ -435,158 +601,19 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => { ...@@ -435,158 +601,19 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => {
<Table sx={{ tableLayout: 'fixed' }}> <Table sx={{ tableLayout: 'fixed' }}>
<Thead> <Thead>
<Tr> <Tr>
<Th w={'240px'}>{t('common:Name')}</Th> {tableColumns.map(({ field, width, label }) => (
<Th w={'130px'}>API KEY</Th> <Th key={field} w={width}>
{hasUsagePlan && <Th w={'150px'}>{t('common:support.outlink.Usage points')}</Th>} {label}
{hasUsagePlan && ( </Th>
<> ))}
<Th w={'120px'}>{t('common:expired_time')}</Th>
</>
)}
<Th w={'160px'}>{t('account_apikey:last_used_time')}</Th>
<Th w={'160px'}>{t('account_apikey:create_time')}</Th>
<Th w={'92px'} />
</Tr> </Tr>
</Thead> </Thead>
<Tbody fontSize={'sm'}> <Tbody fontSize={'sm'}>
{apiKeys.map( {apiKeys.map((item) => (
({ <Tr key={item._id}>
_id, {tableColumns.map(({ field }) => renderTableCell(field, item))}
name, </Tr>
limit, ))}
usagePoints,
apiKey,
canCopy,
createTime,
lastUsedTime,
authProxy,
appName,
tagIds
}) => (
<Tr key={_id}>
<Td maxW={'240px'}>
<Flex flexDirection={'column'} minW={0}>
<MyTooltip label={name} showOnlyWhenOverflow>
<Box
maxW={'220px'}
overflow={'hidden'}
textOverflow={'ellipsis'}
whiteSpace={'nowrap'}
>
{name}
</Box>
</MyTooltip>
<ApiKeyTagEditor
key={`${_id}-${(tagIds || []).join(',')}`}
apiKeyId={_id}
appName={appName}
tagIds={tagIds || []}
allTags={openApiTags}
onSave={async (apiKeyId, tagIds) => {
await onUpdateApiKeyTags({
apiKeyId,
tagIds
});
}}
onManage={() => setShowTagManage(true)}
onCreateTag={onCreateTagFromSelect}
isLoading={isGettingTags || isUpdatingApiKeyTags}
/>
</Flex>
</Td>
<Td maxW={'130px'}>
<Flex alignItems={'center'} gap={1} role={'group'} minW={0}>
<Box
minW={0}
overflow={'hidden'}
textOverflow={'ellipsis'}
whiteSpace={'nowrap'}
>
{maskApiKey(apiKey)}
</Box>
{canCopy && (
<MyIcon
name={copyingApiKeyId === _id ? 'common/loading' : 'copy'}
w={'15px'}
flexShrink={0}
aria-label={t('common:Copy')}
role={'button'}
tabIndex={0}
color={'myGray.600'}
opacity={copyingApiKeyId === _id ? 1 : 0}
visibility={copyingApiKeyId === _id ? 'visible' : 'hidden'}
cursor={'pointer'}
transition={'opacity 0.15s ease, color 0.15s ease'}
_groupHover={{ opacity: 1, visibility: 'visible' }}
_hover={{ color: 'primary.600' }}
onClick={() => onCopyApiKey(_id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onCopyApiKey(_id);
}
}}
/>
)}
</Flex>
</Td>
{hasUsagePlan && (
<Td whiteSpace={'nowrap'}>
{Math.round(usagePoints)}/
{limit?.maxUsagePoints && limit?.maxUsagePoints > -1
? `${limit?.maxUsagePoints}`
: t('common:Unlimited')}
</Td>
)}
{hasUsagePlan && (
<>
<Td whiteSpace={'pre-wrap'}>
{limit?.expiredTime
? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td whiteSpace={'normal'}>
{lastUsedTime
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
: t('common:un_used')}
</Td>
<Td whiteSpace={'normal'}>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td w={'92px'}>
<Flex alignItems={'center'} gap={2}>
<MyTooltip label={t('common:Edit')}>
<IconButton
icon={<MyIcon name={'edit'} w={4} />}
variant={'whitePrimary'}
size={'sm'}
aria-label={t('common:Edit')}
onClick={() =>
setEditData({
_id,
name,
limit,
authProxy,
tags: tagIds || []
})
}
/>
</MyTooltip>
<MyTooltip label={t('common:Delete')}>
<IconButton
icon={<MyIcon name={'delete'} w={4} />}
variant={'whiteDanger'}
size={'sm'}
aria-label={t('common:Delete')}
onClick={() => openConfirm({ onConfirm: () => onclickRemove(_id) })()}
/>
</MyTooltip>
</Flex>
</Td>
</Tr>
)
)}
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
...@@ -733,7 +760,9 @@ function EditKeyModal({ ...@@ -733,7 +760,9 @@ function EditKeyModal({
> >
<Flex flexDirection={'column'} gap={4}> <Flex flexDirection={'column'} gap={4}>
<Flex alignItems={'center'} gap={4}> <Flex alignItems={'center'} gap={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:key_alias') || 'key_alias'} placeholder={t('publish:key_alias') || 'key_alias'}
maxLength={50} maxLength={50}
......
...@@ -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>
<Td>{item?.limit?.QPM || '-'}</Td> {item.limit?.expiredTime
<Td> ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
{item?.limit?.expiredTime : '-'}
? 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>
<Td>{item?.limit?.QPM || '-'}</Td> {item.limit?.expiredTime
<Td> ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
{item?.limit?.expiredTime : '-'}
? 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>
<Td>{item?.limit?.QPM || '-'} </Td> {item.limit?.expiredTime
<Td> ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
{item?.limit?.expiredTime : '-'}
? 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>
<Td>{item?.limit?.QPM || '-'} </Td> {item.limit?.expiredTime
<Td> ? dayjs(item.limit.expiredTime).format('YYYY/MM/DD\nHH:mm')
{item?.limit?.expiredTime : '-'}
? 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;
}} }}
......
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Box, HStack } from '@chakra-ui/react'; import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { OutLinkCountResponseType } from '@fastgpt/global/openapi/support/outLink/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MyRadio from '@/components/common/MyRadio';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
...@@ -13,6 +14,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; ...@@ -13,6 +14,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { UserTagsSchema } from '@fastgpt/global/support/user/type'; import { UserTagsSchema } from '@fastgpt/global/support/user/type';
import ProTag from '@/components/ProTip/Tag'; import ProTag from '@/components/ProTip/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { getOutLinkCounts } from '@/web/support/outLink/api';
const Link = dynamic(() => import('./Link')); const Link = dynamic(() => import('./Link'));
const API = dynamic(() => import('./API')); const API = dynamic(() => import('./API'));
...@@ -23,6 +27,15 @@ const OffiAccount = dynamic(() => import('./OffiAccount')); ...@@ -23,6 +27,15 @@ const OffiAccount = dynamic(() => import('./OffiAccount'));
const Wechat = dynamic(() => import('./Wechat')); const Wechat = dynamic(() => import('./Wechat'));
const Playground = dynamic(() => import('./Playground')); const Playground = dynamic(() => import('./Playground'));
type PublishListItem = {
icon: string;
title: React.ReactNode;
value: PublishChannelEnum;
isProFn: boolean;
group: 'native' | 'thirdParty';
countKey?: keyof OutLinkCountResponseType;
};
const OutLink = () => { const OutLink = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs, setShowProModal } = useSystemStore(); const { feConfigs, setShowProModal } = useSystemStore();
...@@ -30,34 +43,41 @@ const OutLink = () => { ...@@ -30,34 +43,41 @@ const OutLink = () => {
const appId = useContextSelector(AppContext, (v) => v.appId); const appId = useContextSelector(AppContext, (v) => v.appId);
const isPro = !!feConfigs.isPlus; const isPro = !!feConfigs.isPlus;
const { data: outLinkCounts, refetch: refetchOutLinkCounts } = useQuery(
['outLinkCounts', appId],
() => getOutLinkCounts({ appId }),
{ enabled: !!appId }
);
const publishList = useMemo( const publishList = useMemo<PublishListItem[]>(
() => [ () => [
{ {
icon: '/imgs/modal/shareFill.svg', icon: 'support/outlink/share',
title: t('common:core.app.Share link'), title: t('common:core.app.Share link'),
desc: t('common:core.app.Share link desc'),
value: PublishChannelEnum.share, value: PublishChannelEnum.share,
isProFn: false isProFn: false,
group: 'native',
countKey: PublishChannelEnum.share
}, },
{ {
icon: 'support/outlink/apikeyFill', icon: 'support/outlink/apikeyFill',
title: t('common:core.app.Api request'), title: t('common:core.app.Api request'),
desc: t('common:core.app.Api request desc'),
value: PublishChannelEnum.apikey, value: PublishChannelEnum.apikey,
isProFn: false isProFn: false,
group: 'native'
},
{
icon: 'core/chat/sidebar/home',
title: (
<HStack gap={1}>
<Box>{t('common:navbar.Chat')}</Box>
{!isPro && <ProTag />}
</HStack>
),
value: PublishChannelEnum.playground,
isProFn: true,
group: 'native'
}, },
...(feConfigs?.show_publish_wechat !== false
? [
{
icon: 'core/app/publish/wechat',
title: t('publish:wechat.bot'),
desc: t('publish:wechat.bot_desc'),
value: PublishChannelEnum.wechat,
isProFn: false
}
]
: []),
...(feConfigs?.show_publish_feishu !== false && ...(feConfigs?.show_publish_feishu !== false &&
!userInfo?.tags?.includes(UserTagsSchema.enum.wecom) !userInfo?.tags?.includes(UserTagsSchema.enum.wecom)
? [ ? [
...@@ -69,9 +89,10 @@ const OutLink = () => { ...@@ -69,9 +89,10 @@ const OutLink = () => {
{!isPro && <ProTag />} {!isPro && <ProTag />}
</HStack> </HStack>
), ),
desc: t('publish:feishu_bot_desc'),
value: PublishChannelEnum.feishu, value: PublishChannelEnum.feishu,
isProFn: true isProFn: true,
group: 'thirdParty' as const,
countKey: PublishChannelEnum.feishu as PublishChannelEnum.feishu
} }
] ]
: []), : []),
...@@ -86,9 +107,10 @@ const OutLink = () => { ...@@ -86,9 +107,10 @@ const OutLink = () => {
{!isPro && <ProTag />} {!isPro && <ProTag />}
</HStack> </HStack>
), ),
desc: t('publish:dingtalk.bot_desc'),
value: PublishChannelEnum.dingtalk, value: PublishChannelEnum.dingtalk,
isProFn: true isProFn: true,
group: 'thirdParty' as const,
countKey: PublishChannelEnum.dingtalk as PublishChannelEnum.dingtalk
} }
] ]
: []), : []),
...@@ -102,9 +124,22 @@ const OutLink = () => { ...@@ -102,9 +124,22 @@ const OutLink = () => {
{!isPro && <ProTag />} {!isPro && <ProTag />}
</HStack> </HStack>
), ),
desc: t('publish:wecom.bot_desc'),
value: PublishChannelEnum.wecom, value: PublishChannelEnum.wecom,
isProFn: true isProFn: true,
group: 'thirdParty' as const,
countKey: PublishChannelEnum.wecom as PublishChannelEnum.wecom
}
]
: []),
...(feConfigs?.show_publish_wechat !== false
? [
{
icon: 'core/app/publish/wechat',
title: t('publish:wechat.bot'),
value: PublishChannelEnum.wechat,
isProFn: false,
group: 'thirdParty' as const,
countKey: PublishChannelEnum.wechat as PublishChannelEnum.wechat
} }
] ]
: []), : []),
...@@ -118,70 +153,195 @@ const OutLink = () => { ...@@ -118,70 +153,195 @@ const OutLink = () => {
{!isPro && <ProTag />} {!isPro && <ProTag />}
</HStack> </HStack>
), ),
desc: t('publish:official_account.desc'),
value: PublishChannelEnum.officialAccount, value: PublishChannelEnum.officialAccount,
isProFn: true isProFn: true,
group: 'thirdParty' as const,
countKey: PublishChannelEnum.officialAccount as PublishChannelEnum.officialAccount
} }
] ]
: []), : [])
{
icon: 'core/chat/sidebar/home',
title: (
<HStack gap={1}>
<Box>{t('common:navbar.Chat')}</Box>
{!isPro && <ProTag />}
</HStack>
),
desc: t('app:publish.chat_desc'),
value: PublishChannelEnum.playground,
isProFn: true
}
], ],
[t, feConfigs, isPro, userInfo?.tags] [t, feConfigs, isPro, userInfo?.tags]
); );
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share); const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
const [isNativeChannelOpen, setIsNativeChannelOpen] = useState(true);
const [isThirdPartyChannelOpen, setIsThirdPartyChannelOpen] = useState(true);
return ( return (
<Box> <Flex h={'full'} borderTop={'1px solid'} borderColor={'myGray.200'}>
<Box mx={[4, 8]} py={[4, 6]} borderBottom={'1px solid'} borderColor={'myGray.150'}> <Box
<MyRadio w={'220px'}
gridTemplateColumns={[ h={'full'}
'repeat(1,1fr)', borderRight={'1px solid'}
'repeat(2, 1fr)', borderColor={'myGray.200'}
'repeat(3, 1fr)', pt={4}
'repeat(3, 1fr)', pb={2.5}
'repeat(4, 1fr)' userSelect={'none'}
]} >
iconSize={'20px'} <Box px={2.5}>
gridGap={[2, 3]} {[
list={publishList} {
value={linkType} name: t('publish:native_channels'),
onChange={(e) => { group: 'native',
const config = publishList.find((v) => v.value === e)!; isOpen: isNativeChannelOpen,
if (!feConfigs.isPlus && config.isProFn) { onClick: () => setIsNativeChannelOpen((state) => !state)
setShowProModal(true); },
} else { {
setLinkType(e as PublishChannelEnum); name: t('publish:third_party_publish_channels'),
group: 'thirdParty',
isOpen: isThirdPartyChannelOpen,
onClick: () => setIsThirdPartyChannelOpen((state) => !state)
} }
}} ].map(({ name, group, isOpen, onClick }) => {
/> const items = publishList
.filter((item) => item.group === group)
.sort((a, b) => {
if (group !== 'thirdParty' || !outLinkCounts) return 0;
return (
(b.countKey ? outLinkCounts[b.countKey] : 0) -
(a.countKey ? outLinkCounts[a.countKey] : 0)
);
});
if (items.length === 0) return null;
return (
<Box key={group}>
<Button
variant={'unstyled'}
display={'flex'}
w={'full'}
h={'auto'}
py={2}
pl={2}
pr={2}
fontSize={'sm'}
fontWeight={500}
rounded={'md'}
color={'myGray.700'}
cursor={'pointer'}
mb={0.5}
_hover={{
bg: 'rgba(17, 24, 36, 0.05)'
}}
justifyContent={'space-between'}
alignItems={'center'}
onClick={onClick}
aria-expanded={isOpen}
_focusVisible={{
bg: 'rgba(17, 24, 36, 0.05)'
}}
>
<Box minW={0} fontWeight={'medium'} textAlign={'left'} whiteSpace={'normal'}>
{name}
</Box>
<MyIcon
name={'core/chat/chevronDown'}
w={'1rem'}
transform={isOpen ? undefined : 'rotate(-90deg)'}
/>
</Button>
{isOpen &&
items.map((item) => (
<Button
key={item.value}
variant={'unstyled'}
display={'flex'}
w={'full'}
h={'auto'}
fontSize={'sm'}
fontWeight={500}
rounded={'md'}
py={2}
pl={'30px'}
pr={2}
cursor={'pointer'}
mb={0.5}
justifyContent={'space-between'}
{...(linkType === item.value
? {
bg: 'primary.50',
color: 'primary.600',
_hover: { bg: 'primary.50' },
_focusVisible: { bg: 'primary.50' }
}
: {
bg: 'transparent',
color: 'myGray.500',
_hover: {
bg: 'rgba(17, 24, 36, 0.05)'
},
_focusVisible: {
bg: 'rgba(17, 24, 36, 0.05)'
}
})}
onClick={() => {
if (!feConfigs.isPlus && item.isProFn) {
setShowProModal(true);
} else {
setLinkType(item.value);
}
}}
aria-pressed={linkType === item.value}
alignItems={'center'}
>
<Avatar src={item.icon} w={'1rem'} mr={1} />
<Box flex={1} minW={0} textAlign={'left'} whiteSpace={'normal'}>
{item.title}
</Box>
{item.countKey && outLinkCounts && outLinkCounts[item.countKey] > 0 && (
<Box
ml={'auto'}
bg={'rgba(17, 24, 36, 0.15)'}
color={'white'}
w={4}
h={4}
borderRadius={'full'}
textAlign={'center'}
lineHeight={4}
fontWeight={500}
fontSize={11}
flexShrink={0}
>
{outLinkCounts[item.countKey]}
</Box>
)}
</Button>
))}
</Box>
);
})}
</Box>
</Box> </Box>
<Box mt={2} px={[4, 8]} py={[4, 6]}> <Box flex={1} minW={0} overflowY={'auto'}>
{linkType === PublishChannelEnum.share && ( {linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} /> <Link
appId={appId}
type={PublishChannelEnum.share}
onRefreshOutLinkCounts={refetchOutLinkCounts}
/>
)} )}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />} {linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />} {linkType === PublishChannelEnum.feishu && (
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />} <FeiShu appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />} )}
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />} {linkType === PublishChannelEnum.dingtalk && (
{linkType === PublishChannelEnum.wechat && <Wechat appId={appId} />} <DingTalk appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
)}
{linkType === PublishChannelEnum.wecom && (
<Wecom appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
)}
{linkType === PublishChannelEnum.officialAccount && (
<OffiAccount appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
)}
{linkType === PublishChannelEnum.wechat && (
<Wechat appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
)}
{linkType === PublishChannelEnum.playground && <Playground appId={appId} />} {linkType === PublishChannelEnum.playground && <Playground appId={appId} />}
</Box> </Box>
</Box> </Flex>
); );
}; };
......
...@@ -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