feat: add chat visibility controls and improve quote reader permissions (#6102)

* feat: add chat visibility controls and improve quote reader permissions

* fix test

* zod

* fix

* test & openapi

* frontend filter

* update name

* fix

* fix

* rename variables

* fix

* test

* fix build

* fix

* fix

---------

Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
heheer 2025-12-22 18:16:44 +08:00 committed by GitHub
parent ab743b9358
commit 32affb0df0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 1571 additions and 282 deletions

View File

@ -1,6 +1,8 @@
import type { OpenAPIPath } from '../../type';
import { AppLogPath } from './log';
import { PublishChannelPath } from './publishChannel';
export const AppPath: OpenAPIPath = {
...AppLogPath
...AppLogPath,
...PublishChannelPath
};

View File

@ -0,0 +1,5 @@
import { PlaygroundPath } from './playground';
export const PublishChannelPath = {
...PlaygroundPath
};

View File

@ -0,0 +1,52 @@
import { z } from 'zod';
import { ObjectIdSchema } from '../../../../../common/type/mongo';
// Playground Visibility Config Fields
const PlaygroundVisibilityConfigFieldsSchema = z.object({
showRunningStatus: z.boolean().meta({
example: true,
description: '是否显示运行状态'
}),
showCite: z.boolean().meta({
example: true,
description: '是否显示引用'
}),
showFullText: z.boolean().meta({
example: true,
description: '是否显示全文'
}),
canDownloadSource: z.boolean().meta({
example: true,
description: '是否可下载来源'
})
});
// Get Playground Visibility Config Parameters
export const GetPlaygroundVisibilityConfigParamsSchema = z.object({
appId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
});
export type GetPlaygroundVisibilityConfigParamsType = z.infer<
typeof GetPlaygroundVisibilityConfigParamsSchema
>;
// Playground Visibility Config Response
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigFieldsSchema;
export type PlaygroundVisibilityConfigResponseType = z.infer<
typeof PlaygroundVisibilityConfigResponseSchema
>;
// Update Playground Visibility Config Parameters
export const UpdatePlaygroundVisibilityConfigParamsSchema = z
.object({
appId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '应用 ID'
})
})
.extend(PlaygroundVisibilityConfigFieldsSchema.shape);
export type UpdatePlaygroundVisibilityConfigParamsType = z.infer<
typeof UpdatePlaygroundVisibilityConfigParamsSchema
>;

View File

@ -0,0 +1,109 @@
import { z } from 'zod';
import type { OpenAPIPath } from '../../../../type';
import {
GetPlaygroundVisibilityConfigParamsSchema,
PlaygroundVisibilityConfigResponseSchema,
UpdatePlaygroundVisibilityConfigParamsSchema
} from './api';
import { TagsMap } from '../../../../tag';
export const PlaygroundPath: OpenAPIPath = {
'/api/support/outLink/playground/config': {
get: {
summary: '获取门户配置',
description:
'获取指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置',
tags: [TagsMap.publishChannel],
requestParams: {
query: GetPlaygroundVisibilityConfigParamsSchema
},
responses: {
200: {
description: '成功返回门户配置',
content: {
'application/json': {
schema: PlaygroundVisibilityConfigResponseSchema
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: z.object({
code: z.literal(500),
statusText: z.literal('Invalid Params'),
message: z.string(),
data: z.null()
})
}
}
},
401: {
description: '用户未授权',
content: {
'application/json': {
schema: z.object({
code: z.literal(401),
statusText: z.literal('unAuthorization'),
message: z.string(),
data: z.null()
})
}
}
}
}
}
},
'/api/support/outLink/playground/update': {
post: {
summary: '更新门户配置',
description:
'更新指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置。如果配置不存在则创建新配置',
tags: [TagsMap.publishChannel],
requestBody: {
content: {
'application/json': {
schema: UpdatePlaygroundVisibilityConfigParamsSchema
}
}
},
responses: {
200: {
description: '成功更新门户配置',
content: {
'application/json': {
schema: z.null()
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: z.object({
code: z.literal(500),
statusText: z.literal('Invalid Params'),
message: z.string(),
data: z.null()
})
}
}
},
401: {
description: '用户未授权',
content: {
'application/json': {
schema: z.object({
code: z.literal(401),
statusText: z.literal('unAuthorization'),
message: z.string(),
data: z.null()
})
}
}
}
}
}
}
};

View File

@ -26,7 +26,7 @@ export const openAPIDocument = createDocument({
'x-tagGroups': [
{
name: 'Agent 应用',
tags: [TagsMap.appLog]
tags: [TagsMap.appLog, TagsMap.publishChannel]
},
{
name: '对话管理',

View File

@ -13,6 +13,9 @@ export const TagsMap = {
pluginToolTag: '工具标签',
pluginTeam: '团队插件管理',
// Publish Channel
publishChannel: '发布渠道',
/* Support */
// Wallet
walletBill: '订单',

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import type { HistoryItemType } from '../../core/chat/type.d';
import type { OutLinkSchema } from './type.d';
import type { OutLinkSchema, PlaygroundVisibilityConfigType } from './type.d';
import { PlaygroundVisibilityConfigSchema } from './type.d';
export type AuthOutLinkInitProps = {
outLinkUid: string;
@ -10,3 +12,20 @@ export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSch
export type AuthOutLinkResponse = {
uid: string;
};
export const UpdatePlaygroundVisibilityConfigBodySchema = PlaygroundVisibilityConfigSchema.extend({
appId: z.string().min(1, 'App ID is required')
});
export type UpdatePlaygroundVisibilityConfigBody = z.infer<
typeof UpdatePlaygroundVisibilityConfigBodySchema
>;
export const PlaygroundVisibilityConfigQuerySchema = z.object({
appId: z.string().min(1, 'App ID is required')
});
export type PlaygroundVisibilityConfigQuery = z.infer<typeof PlaygroundVisibilityConfigQuerySchema>;
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigSchema;
export type PlaygroundVisibilityConfigResponse = z.infer<
typeof PlaygroundVisibilityConfigResponseSchema
>;

View File

@ -5,5 +5,6 @@ export enum PublishChannelEnum {
feishu = 'feishu',
dingtalk = 'dingtalk',
wecom = 'wecom',
officialAccount = 'official_account'
officialAccount = 'official_account',
playground = 'playground'
}

View File

@ -1,3 +1,4 @@
import { z } from 'zod';
import { AppSchema } from '../../core/app/type';
import type { PublishChannelEnum } from './constant';
import { RequireOnlyOne } from '../../common/type/utils';
@ -63,14 +64,14 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
lastTime: Date;
type: PublishChannelEnum;
// whether the response content is detailed
responseDetail: boolean;
// whether to hide the node status
showNodeStatus?: boolean;
// wheter to show the full text reader
// showFullText?: boolean;
// whether to show the complete quote
showRawSource?: boolean;
// whether to show the quote
showCite: boolean;
// whether to show the running status
showRunningStatus: boolean;
// whether to show the full text reader
showFullText: boolean;
// whether can download source
canDownloadSource: boolean;
// response when request
immediateResponse?: string;
@ -93,10 +94,10 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
export type OutLinkEditType<T = undefined> = {
_id?: string;
name: string;
responseDetail?: OutLinkSchema<T>['responseDetail'];
showNodeStatus?: OutLinkSchema<T>['showNodeStatus'];
// showFullText?: OutLinkSchema<T>['showFullText'];
showRawSource?: OutLinkSchema<T>['showRawSource'];
showCite?: OutLinkSchema<T>['showCite'];
showRunningStatus?: OutLinkSchema<T>['showRunningStatus'];
showFullText?: OutLinkSchema<T>['showFullText'];
canDownloadSource?: OutLinkSchema<T>['canDownloadSource'];
// response when request
immediateResponse?: string;
// response when error or other situation
@ -106,3 +107,12 @@ export type OutLinkEditType<T = undefined> = {
// config for specific platform
app?: T;
};
export const PlaygroundVisibilityConfigSchema = z.object({
showRunningStatus: z.boolean(),
showCite: z.boolean(),
showFullText: z.boolean(),
canDownloadSource: z.boolean()
});
export type PlaygroundVisibilityConfigType = z.infer<typeof PlaygroundVisibilityConfigSchema>;

View File

@ -43,19 +43,21 @@ const OutLinkSchema = new Schema({
type: Date
},
responseDetail: {
showRunningStatus: {
type: Boolean,
default: false
},
showNodeStatus: {
showCite: {
type: Boolean,
default: true
default: false
},
// showFullText: {
// type: Boolean
// },
showRawSource: {
type: Boolean
showFullText: {
type: Boolean,
default: false
},
canDownloadSource: {
type: Boolean,
default: false
},
limit: {
maxUsagePoints: {

View File

@ -301,6 +301,8 @@
"pro_modal_subtitle": "Join the business edition now to unlock more premium features",
"pro_modal_title": "Business Edition Exclusive!",
"pro_modal_unlock_button": "Unlock Now",
"publish.chat_desc": "After logging into the portal, users can talk directly to the application",
"publish.playground_link": "Redirect Link",
"publish_channel": "Publish",
"publish_success": "Publish Successful",
"question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.",

View File

@ -234,7 +234,7 @@
"core.ai.model.Vector Model": "Index model",
"core.ai.model.doc_index_and_dialog": "Document Index & Dialog Index",
"core.app.Api request": "API Request",
"core.app.Api request desc": "Integrate into existing systems through API, or WeChat Work, Feishu, etc.",
"core.app.Api request desc": "Connect to existing systems via API",
"core.app.App intro": "App Introduction",
"core.app.Auto execute": "Auto execute",
"core.app.Chat Variable": "Chat Variable",
@ -264,7 +264,7 @@
"core.app.Set a name for your app": "Set a Name for Your App",
"core.app.Setting ai property": "Click to Configure AI Model Properties",
"core.app.Share link": "Login-Free Window",
"core.app.Share link desc": "Share the link with other users, they can use it directly without logging in",
"core.app.Share link desc": "Create shareable links and support login-free use",
"core.app.Share link desc detail": "You can directly share this model with other users for conversation, they can use it directly without logging in. Note, this feature will consume your account balance, please keep the link safe!",
"core.app.TTS": "Voice Playback",
"core.app.TTS Tip": "After enabling, you can use the voice playback function after each conversation. Using this feature may incur additional costs.",
@ -315,6 +315,7 @@
"core.app.share.Amount limit tip": "Up to 10 groups",
"core.app.share.Create link": "Create New Link",
"core.app.share.Create link tip": "Creation successful. The share address has been copied and can be shared directly.",
"core.app.share.Download source": "Download/open source original text",
"core.app.share.Ip limit title": "IP Rate Limit (people/minute)",
"core.app.share.Is response quote": "Return Quote",
"core.app.share.Not share link": "No Share Link Created",
@ -1102,9 +1103,11 @@
"support.outlink.Max usage points tip": "The maximum number of points allowed for this link. It cannot be used after exceeding the limit. -1 means unlimited.",
"support.outlink.Usage points": "Points Consumption",
"support.outlink.share.Chat_quote_reader": "Full text reader",
"support.outlink.share.Download source tips": "Download the original file of the knowledge base, or jump to the source website",
"support.outlink.share.Full_text tips": "Allows reading of the complete dataset from which the referenced fragment is derived",
"support.outlink.share.Response Quote": "Return Quote",
"support.outlink.share.Response Quote": "View quoted snippets",
"support.outlink.share.Response Quote tips": "Return quoted content in the share link, but do not allow users to download the original document",
"support.outlink.share.Show full text tips": "View the complete file to which the quoted content belongs. You cannot view the original file or jump to the source website.",
"support.outlink.share.running_node": "Running node",
"support.outlink.share.show_complete_quote": "View original source",
"support.outlink.share.show_complete_quote_tips": "View and download the complete citation document, or jump to the citation website",

View File

@ -313,6 +313,8 @@
"pro_modal_subtitle": "即刻加入商业版,解锁更多高级功能",
"pro_modal_title": "商业版专享!",
"pro_modal_unlock_button": "去解锁",
"publish.chat_desc": "用户登录门户后可直接与应用对话",
"publish.playground_link": "跳转链接",
"publish_channel": "发布渠道",
"publish_channel.wecom.empty": "发布到企业微信机器人,请先 <a>绑定自定义域名</a>,并且通过域名校验。",
"publish_success": "发布成功",

View File

@ -237,7 +237,7 @@
"core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文档索引 & 对话索引",
"core.app.Api request": "API 访问",
"core.app.Api request desc": "通过 API 接入已有系统中,或企微、飞书等",
"core.app.Api request desc": "通过 API 接入已有系统",
"core.app.App intro": "应用介绍",
"core.app.Auto execute": "自动执行",
"core.app.Chat Variable": "对话框变量",
@ -267,7 +267,7 @@
"core.app.Set a name for your app": "给应用设置一个名称",
"core.app.Setting ai property": "点击配置 AI 模型相关属性",
"core.app.Share link": "免登录窗口",
"core.app.Share link desc": "分享链接给其他用户,无需登录即可直接进行使用",
"core.app.Share link desc": "创建可分享的链接,支持免登录使用",
"core.app.Share link desc detail": "可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!",
"core.app.TTS": "语音播放",
"core.app.TTS Tip": "开启后,每次对话后可使用语音播放功能。使用该功能可能产生额外费用。",
@ -318,10 +318,12 @@
"core.app.share.Amount limit tip": "最多创建 10 组",
"core.app.share.Create link": "创建新链接",
"core.app.share.Create link tip": "创建成功。已复制分享地址,可直接分享使用",
"core.app.share.Download source": "下载/打开来源原文",
"core.app.share.Ip limit title": "IP 限流(人/分钟)",
"core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "没有创建分享链接",
"core.app.share.Role check": "身份校验",
"core.app.share.Show full text": "查看引用全文",
"core.app.switch_to_template_market": "跳转模板市场",
"core.app.tip.Add a intro to app": "快来给应用一个介绍~",
"core.app.tip.chatNodeSystemPromptTip": "在此输入提示词",
@ -1110,9 +1112,11 @@
"support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。",
"support.outlink.Usage points": "积分消耗",
"support.outlink.share.Chat_quote_reader": "全文阅读器",
"support.outlink.share.Download source tips": "下载知识库原文件,或跳转来源网站",
"support.outlink.share.Full_text tips": "允许阅读该引用片段来源的完整数据集",
"support.outlink.share.Response Quote": "引用内容",
"support.outlink.share.Response Quote": "查看引用片段",
"support.outlink.share.Response Quote tips": "查看知识库搜索的引用内容,不可查看完整引用文档或跳转引用网站",
"support.outlink.share.Show full text tips": "查看引用内容所属的完整文件,不可查看原文件或跳转来来源网站",
"support.outlink.share.running_node": "运行节点",
"support.outlink.share.show_complete_quote": "查看来源原文",
"support.outlink.share.show_complete_quote_tips": "查看及下载完整引用文档,或跳转引用网站",

View File

@ -299,6 +299,8 @@
"pro_modal_subtitle": "即刻加入商業版,解鎖更多高級功能",
"pro_modal_title": "商業版專享!",
"pro_modal_unlock_button": "去解鎖",
"publish.chat_desc": "用戶登錄門戶後可直接與應用對話",
"publish.playground_link": "跳轉鏈接",
"publish_channel": "發布通道",
"publish_success": "發布成功",
"question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。",

View File

@ -234,7 +234,7 @@
"core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文件索引與對話索引",
"core.app.Api request": "API 存取",
"core.app.Api request desc": "透過 API 整合到現有系統中,或整合到企業微信、飛書等",
"core.app.Api request desc": "通過 API 接入已有系統",
"core.app.App intro": "應用程式介紹",
"core.app.Auto execute": "自動執行",
"core.app.Chat Variable": "對話變數",
@ -264,7 +264,7 @@
"core.app.Set a name for your app": "為您的應用程式命名",
"core.app.Setting ai property": "點選設定 AI 模型相關屬性",
"core.app.Share link": "免登入視窗",
"core.app.Share link desc": "分享連結給其他使用者,無需登入即可直接使用",
"core.app.Share link desc": "創建可分享的鏈接,支持免登錄使用",
"core.app.Share link desc detail": "您可以直接分享此模型給其他使用者進行對話,對方無需登入即可直接使用。請注意,此功能會消耗您帳戶的餘額,請妥善保管連結!",
"core.app.TTS": "語音播放",
"core.app.TTS Tip": "開啟後,每次對話後可使用語音播放功能。使用此功能可能會產生額外費用。",
@ -315,6 +315,7 @@
"core.app.share.Amount limit tip": "最多 10 組",
"core.app.share.Create link": "建立新連結",
"core.app.share.Create link tip": "建立成功。已複製分享網址,可直接分享使用",
"core.app.share.Download source": "下載/打開來源原文",
"core.app.share.Ip limit title": "IP 限流(人/分鐘)",
"core.app.share.Is response quote": "返回引用",
"core.app.share.Not share link": "尚未建立分享連結",
@ -1099,9 +1100,11 @@
"support.outlink.Max usage points tip": "此連結最多允許使用多少點數,超出後將無法使用。-1 代表無限制。",
"support.outlink.Usage points": "點數消耗",
"support.outlink.share.Chat_quote_reader": "全文閱讀器",
"support.outlink.share.Download source tips": "下載知識庫原文件,或跳轉來源網站",
"support.outlink.share.Full_text tips": "允許閱讀該引用片段來源的完整資料集",
"support.outlink.share.Response Quote": "回傳引用",
"support.outlink.share.Response Quote": "查看引用片段",
"support.outlink.share.Response Quote tips": "在分享連結中回傳引用內容,但不允許使用者下載原始文件",
"support.outlink.share.Show full text tips": "查看引用內容所屬的完整文件,不可查看原文件或跳轉來來源網站",
"support.outlink.share.running_node": "執行節點",
"support.outlink.share.show_complete_quote": "檢視原始內容",
"support.outlink.share.show_complete_quote_tips": "檢視及下載完整引用文件,或跳轉至引用網站",

View File

@ -163,12 +163,12 @@ const ChatItem = (props: Props) => {
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatItemContext, (v) => v.showNodeStatus);
const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData);
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
const isShowFullText = useContextSelector(ChatItemContext, (v) => v.isShowFullText);
const { totalQuoteList: quoteList = [] } = useMemo(
() => addStatisticalDataToHistoryItem(chat),
@ -258,7 +258,7 @@ const ChatItem = (props: Props) => {
setCiteModalData({
rawSearch: quoteList,
metadata:
item?.collectionId && isShowReadRawSource
item?.collectionId && isShowFullText
? {
appId: appId,
chatId: chatId,
@ -322,7 +322,7 @@ const ChatItem = (props: Props) => {
<ChatAvatar src={avatar} type={type} />
{/* Workflow status */}
{!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && (
{!!chatStatusMap && statusBoxData && isLastChild && showRunningStatus && (
<Flex
alignItems={'center'}
px={3}

View File

@ -26,7 +26,7 @@ const QuoteList = React.memo(function QuoteList({
chatId: v.chatId,
...(v.outLinkAuthData || {})
}));
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource);
const showRouteToDatasetDetail = useContextSelector(
ChatItemContext,
(v) => v.showRouteToDatasetDetail
@ -87,7 +87,7 @@ const QuoteList = React.memo(function QuoteList({
>
<QuoteItem
quoteItem={item}
canViewSource={showRawSource}
canDownloadSource={canDownloadSource}
canEditData={showRouteToDatasetDetail}
canEditDataset={showRouteToDatasetDetail}
{...RawSourceBoxProps}

View File

@ -14,6 +14,7 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
export type CitationRenderItem = {
type: 'dataset' | 'link';
@ -48,12 +49,22 @@ const ResponseTags = ({
const chatTime = historyItem.time || new Date();
const durationSeconds = historyItem.durationSeconds || 0;
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const {
totalQuoteList: quoteList = [],
llmModuleAccount = 0,
historyPreviewLength = 0,
toolCiteLinks = []
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
} = useMemo(() => {
return {
...addStatisticalDataToHistoryItem(historyItem),
...(isShowCite
? {
totalQuoteList: []
}
: {})
};
}, [historyItem, isShowCite]);
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
@ -78,6 +89,7 @@ const ResponseTags = ({
: true;
const citationRenderList: CitationRenderItem[] = useMemo(() => {
if (!isShowCite) return [];
// Dataset citations
const datasetItems = Object.values(
quoteList.reduce((acc: Record<string, SearchDataResponseItemType[]>, cur) => {
@ -116,7 +128,7 @@ const ResponseTags = ({
}));
return [...datasetItems, ...linkItems];
}, [quoteList, toolCiteLinks, onOpenCiteModal]);
}, [quoteList, toolCiteLinks, onOpenCiteModal, isShowCite]);
const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0);

View File

@ -31,9 +31,13 @@ import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { type OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext';
import {
type OnOpenCiteModalProps,
ChatItemContext
} from '@/web/core/chat/context/chatItemContext';
import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext';
import { useCreation } from 'ahooks';
import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
const accordionButtonStyle = {
w: 'auto',
@ -102,13 +106,16 @@ const RenderText = React.memo(function RenderText({
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const source = useMemo(() => {
if (!text) return '';
// Remove quote references if not showing response detail
return text;
}, [text]);
if (isShowCite) {
return text;
}
return removeDatasetCiteText(text, isShowCite);
}, [text, isShowCite]);
const chatAuthData = useCreation(() => {
return { appId, chatId, chatItemDataId, ...outLinkAuthData };
@ -329,6 +336,8 @@ const AIResponseBox = ({
isChatting: boolean;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) => {
const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
if (value.type === ChatItemValueTypeEnum.text && value.text) {
return (
<RenderText
@ -348,7 +357,7 @@ const AIResponseBox = ({
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
if (value.type === ChatItemValueTypeEnum.tool && value.tools && showRunningStatus) {
return <RenderTool showAnimation={isChatting} tools={value.tools} />;
}
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {

View File

@ -91,13 +91,13 @@ export const formatScore = (score: ScoreItemType[]) => {
const QuoteItem = ({
quoteItem,
canViewSource,
canDownloadSource,
canEditData,
canEditDataset,
...RawSourceBoxProps
}: {
quoteItem: SearchDataResponseItemType;
canViewSource?: boolean;
canDownloadSource?: boolean;
canEditData?: boolean;
canEditDataset?: boolean;
} & Omit<readCollectionSourceBody, 'collectionId'>) => {
@ -208,7 +208,7 @@ const QuoteItem = ({
collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
canView={canDownloadSource}
{...RawSourceBoxProps}
/>
<Box flex={1} />

View File

@ -205,10 +205,10 @@ const Render = ({
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
isResponseDetail={true}
// isShowFullText={true}
showNodeStatus
canDownloadSource={true}
isShowCite={true}
isShowFullText={true}
showRunningStatus={true}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest

View File

@ -264,10 +264,10 @@ const Render = (props: Props) => {
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
isResponseDetail={true}
// isShowFullText={true}
showNodeStatus
canDownloadSource={true}
isShowCite={true}
isShowFullText={true}
showRunningStatus={true}
>
<ChatRecordContextProvider params={params} feedbackRecordId={feedbackRecordId}>
<DetailLogsModal

View File

@ -182,10 +182,10 @@ const Render = ({
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
isResponseDetail={true}
// isShowFullText={true}
showNodeStatus
canDownloadSource={true}
isShowCite={true}
isShowFullText={true}
showRunningStatus={true}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest currentTool={currentTool} url={url} headerSecret={headerSecret} />

View File

@ -186,7 +186,7 @@ const DingTalk = ({ appId }: { appId: string }) => {
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
showCite: item.showCite,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});

View File

@ -185,7 +185,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
showCite: item.showCite,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});

View File

@ -140,7 +140,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
}`
: ''}
</Td>
<Td>{item.responseDetail ? '✔' : '✖'}</Td>
<Td>{item.showCite ? '✔' : '✖'}</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
@ -182,10 +182,10 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setEditLinkData({
_id: item._id,
name: item.name,
responseDetail: item.responseDetail ?? false,
showRawSource: item.showRawSource ?? false,
// showFullText: item.showFullText ?? false,
showNodeStatus: item.showNodeStatus ?? false,
showCite: item.showCite,
canDownloadSource: item.canDownloadSource,
showFullText: item.showFullText,
showRunningStatus: item.showRunningStatus,
limit: item.limit
})
},
@ -281,9 +281,9 @@ function EditLinkModal({
defaultValues: defaultData
});
const responseDetail = watch('responseDetail');
// const showFullText = watch('showFullText');
const showRawSource = watch('showRawSource');
const showCite = watch('showCite');
const showFullText = watch('showFullText');
const canDownloadSource = watch('canDownloadSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
@ -413,7 +413,7 @@ function EditLinkModal({
</Box>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<FormLabel>{t('publish:show_node')}</FormLabel>
<Switch {...register('showNodeStatus')} />
<Switch {...register('showRunningStatus')} />
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
@ -424,56 +424,56 @@ function EditLinkModal({
></QuestionTip>
</Flex>
<Switch
{...register('responseDetail', {
{...register('showCite', {
onChange(e) {
if (!e.target.checked) {
// setValue('showFullText', false);
setValue('showRawSource', false);
setValue('showFullText', false);
setValue('canDownloadSource', false);
}
}
})}
isChecked={responseDetail}
isChecked={showCite}
/>
</Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Chat_quote_reader')}</FormLabel>
<FormLabel>{t('common:core.app.share.Show full text')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.Full_text tips')}
label={t('common:support.outlink.share.Show full text tips')}
></QuestionTip>
</Flex>
<Switch
{...register('showFullText', {
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
if (!e.target.checked) {
setValue('canDownloadSource', false);
} else {
setValue('showRawSource', false);
setValue('showCite', true);
}
}
})}
isChecked={showFullText}
/>
</Flex> */}
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
<FormLabel>{t('common:core.app.share.Download source')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.show_complete_quote_tips')}
label={t('common:support.outlink.share.Download source tips')}
></QuestionTip>
</Flex>
<Switch
{...register('showRawSource', {
{...register('canDownloadSource', {
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
// setValue('showFullText', true);
setValue('showFullText', true);
setValue('showCite', true);
}
}
})}
isChecked={showRawSource}
isChecked={canDownloadSource}
/>
</Flex>
</Box>

View File

@ -188,7 +188,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
showCite: item.showCite,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});

View File

@ -0,0 +1,186 @@
import React, { useMemo } from 'react';
import { Box, Flex, Grid, Switch } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
getPlaygroundVisibilityConfig,
updatePlaygroundVisibilityConfig
} from '@/web/support/outLink/api';
import type { PlaygroundVisibilityConfigType } from '@fastgpt/global/support/outLink/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
const defaultPlaygroundVisibilityForm: PlaygroundVisibilityConfigType = {
showRunningStatus: true,
showCite: true,
showFullText: true,
canDownloadSource: true
};
const PlaygroundVisibilityConfig = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const { register, watch, setValue, reset } = useForm({
defaultValues: defaultPlaygroundVisibilityForm
});
const showCite = watch('showCite');
const showFullText = watch('showFullText');
const canDownloadSource = watch('canDownloadSource');
const playgroundLink = useMemo(() => {
if (typeof window !== 'undefined') {
return `${window.location.origin}/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`;
}
return '';
}, [appId]);
useRequest2(() => getPlaygroundVisibilityConfig({ appId }), {
onSuccess: (data) => {
reset({
showRunningStatus: data.showRunningStatus,
showCite: data.showCite,
showFullText: data.showFullText,
canDownloadSource: data.canDownloadSource
});
},
manual: false
});
const { runAsync: saveConfig } = useRequest2(
async (data: PlaygroundVisibilityConfigType) => {
return await updatePlaygroundVisibilityConfig({
appId,
...data
});
},
{
successToast: t('common:save_success')
}
);
const autoSave = async () => {
const values = watch();
await saveConfig(values);
};
return (
<Flex flexDirection="column" h="100%">
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'} mb={3}>
{t('app:publish.playground_link')}
</Box>
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'} mb={6}>
<Flex
p={3}
bg={'myWhite.500'}
border="base"
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
alignItems={'center'}
>
<Box flex={1} fontSize={'xs'} color={'myGray.600'}>
{t('common:core.app.outLink.Link block title')}
</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => copyData(playgroundLink)}
/>
</Flex>
<Box whiteSpace={'nowrap'} p={3} overflowX={'auto'}>
{playgroundLink}
</Box>
</Box>
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'}>
{t('publish:private_config')}
</Box>
<Grid templateColumns={'1fr 1fr'} gap={4} mt={4}>
<Flex alignItems={'center'}>
<FormLabel fontSize={'12px'} flex={'0 0 127px'}>
{t('publish:show_node')}
</FormLabel>
<Switch
{...register('showRunningStatus', {
onChange: autoSave
})}
/>
</Flex>
<Flex>
<Flex alignItems={'center'} flex={'0 0 158px'}>
<FormLabel fontSize={'12px'}>
{t('common:support.outlink.share.Response Quote')}
</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Response Quote tips')} />
</Flex>
<Switch
{...register('showCite', {
onChange(e) {
if (!e.target.checked) {
setValue('showFullText', false);
setValue('canDownloadSource', false);
}
autoSave();
}
})}
isChecked={showCite}
/>
</Flex>
</Grid>
<Grid templateColumns={'1fr 1fr'} gap={4} mt={4}>
<Flex>
<Flex alignItems={'center'} flex={'0 0 127px'}>
<FormLabel fontSize={'12px'}>{t('common:core.app.share.Show full text')}</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Show full text tips')} />
</Flex>
<Switch
{...register('showFullText', {
onChange(e) {
if (!e.target.checked) {
setValue('canDownloadSource', false);
} else {
setValue('showCite', true);
}
autoSave();
}
})}
isChecked={showFullText}
/>
</Flex>
<Flex>
<Flex alignItems={'center'} flex={'0 0 158px'}>
<FormLabel fontSize={'12px'} fontWeight={'medium'}>
{t('common:core.app.share.Download source')}
</FormLabel>
<QuestionTip ml={1} label={t('common:support.outlink.share.Download source tips')} />
</Flex>
<Switch
{...register('canDownloadSource', {
onChange(e) {
if (e.target.checked) {
setValue('showFullText', true);
setValue('showCite', true);
}
autoSave();
}
})}
isChecked={canDownloadSource}
/>
</Flex>
</Grid>
</Flex>
);
};
export default PlaygroundVisibilityConfig;

View File

@ -200,7 +200,7 @@ const Wecom = ({ appId }: { appId: string }) => {
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
showCite: item.showCite,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});

View File

@ -19,6 +19,7 @@ const FeiShu = dynamic(() => import('./FeiShu'));
const DingTalk = dynamic(() => import('./DingTalk'));
const Wecom = dynamic(() => import('./Wecom'));
const OffiAccount = dynamic(() => import('./OffiAccount'));
const Playground = dynamic(() => import('./Playground'));
const OutLink = () => {
const { t } = useTranslation();
@ -85,7 +86,14 @@ const OutLink = () => {
isProFn: true
}
]
: [])
: []),
{
icon: 'core/chat/sidebar/home',
title: t('common:navbar.Chat'),
desc: t('app:publish.chat_desc'),
value: PublishChannelEnum.playground,
isProFn: false
}
]);
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
@ -141,6 +149,7 @@ const OutLink = () => {
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />}
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />}
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
{linkType === PublishChannelEnum.playground && <Playground appId={appId} />}
</Flex>
</Box>
);

View File

@ -118,10 +118,10 @@ const Render = ({ appForm, setRenderEdit }: Props) => {
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
isResponseDetail={true}
// isShowFullText={true}
showNodeStatus
canDownloadSource={true}
isShowCite={true}
isShowFullText={true}
showRunningStatus={true}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />

View File

@ -206,10 +206,10 @@ const Render = (Props: Props) => {
return (
<ChatItemContextProvider
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
isResponseDetail={true}
// isShowFullText={true}
showNodeStatus
canDownloadSource={true}
isShowCite={true}
isShowFullText={true}
showRunningStatus={true}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} chatId={chatId} />

View File

@ -23,6 +23,8 @@ import { getCollectionQuote } from '@/web/core/chat/api';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { useContextSelector } from 'use-context-selector';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const CollectionReader = ({
rawSearch,
@ -37,6 +39,8 @@ const CollectionReader = ({
const router = useRouter();
const { userInfo } = useUserStore();
const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource);
const { collectionId, datasetId, chatItemDataId, sourceId, sourceName, quoteId } = metadata;
const [quoteIndex, setQuoteIndex] = useState(0);
@ -175,11 +179,13 @@ const CollectionReader = ({
{sourceName || t('common:unknow_source')}
</Box>
<Box ml={3}>
<DownloadButton
canAccessRawData={true}
onDownload={handleDownload}
onRead={handleRead}
/>
{canDownloadSource && (
<DownloadButton
canAccessRawData={true}
onDownload={handleDownload}
onRead={handleRead}
/>
)}
</Box>
</Flex>
<MyIconButton

View File

@ -45,6 +45,7 @@ const AppChatWindow = ({ myApps }: Props) => {
const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
@ -106,7 +107,8 @@ const AppChatWindow = ({ myApps }: Props) => {
variables,
responseChatItemId,
appId,
chatId
chatId,
retainDatasetCite: isShowCite
},
abortCtrl: controller,
onMessage: generatingMessage
@ -122,7 +124,7 @@ const AppChatWindow = ({ myApps }: Props) => {
return { responseText, isNewChat: forbidLoadChat.current };
},
[appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat]
[appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat, isShowCite]
);
return (

View File

@ -84,6 +84,7 @@ const HomeChatWindow = ({ myApps }: Props) => {
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const pane = useContextSelector(ChatSettingContext, (v) => v.pane);
const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings);
@ -216,7 +217,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
variables,
responseChatItemId,
appId,
chatId
chatId,
retainDatasetCite: isShowCite
},
abortCtrl: controller,
onMessage: generatingMessage
@ -264,6 +266,7 @@ const HomeChatWindow = ({ myApps }: Props) => {
appId,
appName: t('chat:home.chat_app'),
chatId,
retainDatasetCite: isShowCite,
...form2AppWorkflow(formData, t)
},
onMessage: generatingMessage,
@ -394,7 +397,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
setSelectedToolIds,
setChatBoxData,
isPc,
isQuickApp
isQuickApp,
isShowCite
]
);

View File

@ -449,7 +449,7 @@ const TestResults = React.memo(function TestResults({
<Box mt={1} gap={4}>
{datasetTestItem?.results.map((item, index) => (
<Box key={item.id} p={3} borderRadius={'lg'} bg={'myGray.100'} _notLast={{ mb: 2 }}>
<QuoteItem quoteItem={item} canViewSource canEditData />
<QuoteItem quoteItem={item} canDownloadSource canEditData />
</Box>
))}
</Box>

View File

@ -5,36 +5,56 @@ import type { S3MQJobData } from '@fastgpt/service/common/s3/mq';
import { addLog } from '@fastgpt/service/common/system/log';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { connectionMongo } from '@fastgpt/service/common/mongo';
export type ResponseType = {
message: string;
retriedCount: number;
failedCount: number;
shareLinkMigration: {
totalRecords: number;
updatedRecords: number;
updateResults: Array<{
operation: string;
updated: number;
}>;
};
};
async function handler(
req: ApiRequestProps,
res: ApiResponseType<ResponseType>
): Promise<ResponseType> {
await authCert({ req, authRoot: true });
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete);
/**
* 4.14.5
* 1. S3
* 2. share OutLink showFullText
* 3.
* - showNodeStatus -> showRunningStatus
* - responseDetail -> showCite
* - showRawSource -> canDownloadSource
*/
// Get all failed jobs and retry them
/**
* 功能1: 重试所有失败的 S3
*/
async function retryFailedS3DeleteJobs(): Promise<{
retriedCount: number;
failedCount: number;
}> {
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete);
const failedJobs = await queue.getFailed();
console.log(`Found ${failedJobs.length} failed jobs`);
console.log(`Found ${failedJobs.length} failed S3 delete jobs`);
let retriedCount = 0;
await batchRun(
failedJobs,
async (job) => {
addLog.debug(`Retrying job with 3 new attempts`, { retriedCount });
addLog.debug(`Retrying S3 delete job with new attempts`, { retriedCount });
try {
// Remove old job and recreate with new attempts
const jobData = job.data;
await job.remove();
// Add new job with 3 more attempts
// Add new job with more attempts
await queue.add('delete-s3-files', jobData, {
attempts: 10,
removeOnFail: {
@ -49,19 +69,217 @@ async function handler(
});
retriedCount++;
console.log(`Retried job ${job.id} with 3 new attempts`);
console.log(`Retried S3 delete job ${job.id} with new attempts`);
} catch (error) {
console.error(`Failed to retry job ${job.id}:`, error);
console.error(`Failed to retry S3 delete job ${job.id}:`, error);
}
},
100
);
return {
message: 'Successfully retried all failed S3 delete jobs with 3 new attempts',
retriedCount,
failedCount: failedJobs.length
};
}
/**
* 功能2和3: 处理 OutLink
* - showFullText
* -
*/
async function migrateOutLinkData(): Promise<{
totalRecords: number;
updatedRecords: number;
updateResults: Array<{
operation: string;
updated: number;
}>;
}> {
let totalUpdated = 0;
const updateResults: Array<{
operation: string;
updated: number;
}> = [];
// 获取 MongoDB 原生集合,绕过 Mongoose 的严格模式
const db = connectionMongo.connection.db;
if (!db) {
throw new Error('Database connection not established');
}
const outLinkCollection = db.collection('outlinks');
// 1. 为所有 share 类型的记录添加 showFullText 字段
const shareLinks = await outLinkCollection
.find({
type: PublishChannelEnum.share,
showFullText: { $exists: false } // 只查找没有 showFullText 字段的记录
})
.toArray();
if (shareLinks.length > 0) {
// 批量更新添加 showFullText 字段
const showFullTextOps = shareLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: { $set: { showFullText: link.showRawSource ?? true } }
}
}));
const showFullTextResult = await outLinkCollection.bulkWrite(showFullTextOps);
totalUpdated += showFullTextResult.modifiedCount;
updateResults.push({
operation: 'Add showFullText field',
updated: showFullTextResult.modifiedCount
});
console.log(`Added showFullText field to ${showFullTextResult.modifiedCount} share links`);
}
// 2. 重命名字段showNodeStatus -> showRunningStatus
const showNodeStatusLinks = await outLinkCollection
.find({
showNodeStatus: { $exists: true },
showRunningStatus: { $exists: false }
})
.toArray();
if (showNodeStatusLinks.length > 0) {
const renameNodeStatusOps = showNodeStatusLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { showRunningStatus: '$showNodeStatus' }
},
{
$unset: 'showNodeStatus'
}
]
}
}));
const renameNodeStatusResult = await outLinkCollection.bulkWrite(renameNodeStatusOps);
totalUpdated += renameNodeStatusResult.modifiedCount;
updateResults.push({
operation: 'Rename showNodeStatus to showRunningStatus',
updated: renameNodeStatusResult.modifiedCount
});
console.log(
`Renamed showNodeStatus to showRunningStatus for ${renameNodeStatusResult.modifiedCount} links`
);
}
// 3. 重命名字段responseDetail -> showCite
const responseDetailLinks = await outLinkCollection
.find({
responseDetail: { $exists: true },
showCite: { $exists: false }
})
.toArray();
if (responseDetailLinks.length > 0) {
const renameResponseDetailOps = responseDetailLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { showCite: '$responseDetail' }
},
{
$unset: 'responseDetail'
}
]
}
}));
const renameResponseDetailResult = await outLinkCollection.bulkWrite(renameResponseDetailOps);
totalUpdated += renameResponseDetailResult.modifiedCount;
updateResults.push({
operation: 'Rename responseDetail to showCite',
updated: renameResponseDetailResult.modifiedCount
});
console.log(
`Renamed responseDetail to showCite for ${renameResponseDetailResult.modifiedCount} links`
);
}
// 4. 重命名字段showRawSource -> canDownloadSource
const showRawSourceLinks = await outLinkCollection
.find({
showRawSource: { $exists: true },
canDownloadSource: { $exists: false }
})
.toArray();
if (showRawSourceLinks.length > 0) {
const renameRawSourceOps = showRawSourceLinks.map((link: any) => ({
updateOne: {
filter: { _id: link._id },
update: [
{
$set: { canDownloadSource: '$showRawSource' }
},
{
$unset: 'showRawSource'
}
]
}
}));
const renameRawSourceResult = await outLinkCollection.bulkWrite(renameRawSourceOps);
totalUpdated += renameRawSourceResult.modifiedCount;
updateResults.push({
operation: 'Rename showRawSource to canDownloadSource',
updated: renameRawSourceResult.modifiedCount
});
console.log(
`Renamed showRawSource to canDownloadSource for ${renameRawSourceResult.modifiedCount} links`
);
}
return {
totalRecords: totalUpdated,
updatedRecords: totalUpdated,
updateResults
};
}
/**
*
*/
async function handler(
req: ApiRequestProps,
_res: ApiResponseType<ResponseType>
): Promise<ResponseType> {
await authCert({ req, authRoot: true });
// 执行功能1: 重试 S3 删除任务
const s3JobResult = await retryFailedS3DeleteJobs();
// 执行功能2&3: OutLink 数据迁移
let shareLinkMigration = {
totalRecords: 0,
updatedRecords: 0,
updateResults: [] as Array<{ operation: string; updated: number }>
};
try {
shareLinkMigration = await migrateOutLinkData();
} catch (error) {
console.error('Failed to migrate outLink data:', error);
// 即使迁移失败,也继续返回 S3 任务处理的结果
}
return {
message: `Completed v4.14.5 initialization: S3 job retries and outLink migration`,
retriedCount: s3JobResult.retriedCount,
failedCount: s3JobResult.failedCount,
shareLinkMigration
};
}
export default NextAPI(handler);

View File

@ -40,7 +40,7 @@ async function handler(
};
}
const [app, { responseDetail, showNodeStatus, authType }] = await Promise.all([
const [app, { showCite, showRunningStatus, authType }] = await Promise.all([
MongoApp.findById(appId, 'type').lean(),
authChatCrud({
req,
@ -81,16 +81,16 @@ async function handler(
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({
nodeRespones: item.responseData,
responseDetail
responseDetail: showCite
});
if (showNodeStatus === false) {
if (showRunningStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
}
}
});
}
if (!responseDetail) {
if (!showCite) {
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.value = removeAIResponseCite(item.value, false);
@ -99,7 +99,7 @@ async function handler(
}
return {
list: isPlugin ? histories : transformPreviewHistories(histories, responseDetail),
list: isPlugin ? histories : transformPreviewHistories(histories, showCite),
total
};
}

View File

@ -50,7 +50,7 @@ async function handler(
};
}
const [app, { responseDetail, showNodeStatus, authType }] = await Promise.all([
const [app, { showCite, showRunningStatus, authType }] = await Promise.all([
MongoApp.findById(appId, 'type').lean(),
authChatCrud({
req,
@ -93,16 +93,16 @@ async function handler(
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({
nodeRespones: item.responseData,
responseDetail
responseDetail: showCite
});
if (showNodeStatus === false) {
if (showRunningStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
}
}
});
}
if (!responseDetail) {
if (!showCite) {
result.histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.value = removeAIResponseCite(item.value, false);
@ -110,9 +110,7 @@ async function handler(
});
}
const list = isPlugin
? result.histories
: transformPreviewHistories(result.histories, responseDetail);
const list = isPlugin ? result.histories : transformPreviewHistories(result.histories, showCite);
return {
list: list.map((item) => ({

View File

@ -27,7 +27,7 @@ async function handler(
return [];
}
const [{ responseDetail }, chatData, nodeResponses] = await Promise.all([
const [{ showCite }, chatData, nodeResponses] = await Promise.all([
authChatCrud({
req,
authToken: true,
@ -57,7 +57,7 @@ async function handler(
const flowResponses = chatData.responseData?.length ? chatData.responseData : nodeResponses;
return req.query.shareId
? filterPublicNodeResponseData({
responseDetail,
responseDetail: showCite,
nodeRespones: flowResponses
})
: flowResponses;

View File

@ -57,7 +57,7 @@ async function handler(
const limitedPageSize = Math.min(pageSize, 30);
const [collection, { chat, showRawSource }, chatItem] = await Promise.all([
const [collection, { chat, showFullText }, chatItem] = await Promise.all([
getCollectionWithDataset(collectionId),
authChatCrud({
req,
@ -73,7 +73,7 @@ async function handler(
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]);
if (!showRawSource || !chat || !chatItem || initialAnchor === undefined) {
if (!showFullText || !chat || !chatItem || initialAnchor === undefined) {
return Promise.reject(ChatErrEnum.unAuthChat);
}

View File

@ -39,7 +39,7 @@ async function handler(req: ApiRequestProps<GetQuoteProps>): Promise<GetQuotesRe
datasetDataIdList
} = req.body;
const [{ chat, responseDetail }, chatItem] = await Promise.all([
const [{ chat, showCite }, chatItem] = await Promise.all([
authChatCrud({
req,
authToken: true,
@ -53,7 +53,7 @@ async function handler(req: ApiRequestProps<GetQuoteProps>): Promise<GetQuotesRe
MongoChatItem.findOne({ appId, chatId, dataId: chatItemDataId }, 'time').lean(),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: collectionIdList })
]);
if (!chat || !chatItem || !responseDetail) return Promise.reject(ChatErrEnum.unAuthChat);
if (!chat || !chatItem || !showCite) return Promise.reject(ChatErrEnum.unAuthChat);
const list = await MongoDatasetData.find(
{ _id: { $in: datasetDataIdList }, collectionId: { $in: collectionIdList } },

View File

@ -67,7 +67,7 @@ async function handler(req: ApiRequestProps<ExportCollectionBody, {}>, res: Next
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]);
if (!authRes.showRawSource) {
if (!authRes.canDownloadSource) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
}

View File

@ -63,7 +63,7 @@ async function handler(
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]);
if (!authRes.showRawSource) {
if (!authRes.canDownloadSource) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
}

View File

@ -51,7 +51,7 @@ async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuot
return Promise.reject(i18nT('common:data_not_found'));
}
const [collection, { responseDetail }] = await Promise.all([
const [collection, { showCite }] = await Promise.all([
MongoDatasetCollection.findById(datasetData.collectionId).lean(),
authChatCrud({
req,
@ -73,7 +73,7 @@ async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuot
if (!collection) {
return Promise.reject('Can not find the collection');
}
if (!responseDetail) {
if (!showCite) {
return Promise.reject(ChatErrEnum.unAuthChat);
}

View File

@ -0,0 +1,42 @@
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type PlaygroundVisibilityConfigQuery,
type PlaygroundVisibilityConfigResponse,
PlaygroundVisibilityConfigQuerySchema,
PlaygroundVisibilityConfigResponseSchema
} from '@fastgpt/global/support/outLink/api.d';
async function handler(
req: ApiRequestProps<{}, PlaygroundVisibilityConfigQuery>
): Promise<PlaygroundVisibilityConfigResponse> {
const { appId } = PlaygroundVisibilityConfigQuerySchema.parse(req.query);
await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
const existingConfig = await MongoOutLink.findOne(
{
appId,
type: PublishChannelEnum.playground
},
'showRunningStatus showCite showFullText canDownloadSource'
).lean();
return PlaygroundVisibilityConfigResponseSchema.parse({
showRunningStatus: existingConfig?.showRunningStatus ?? true,
showCite: existingConfig?.showCite ?? true,
showFullText: existingConfig?.showFullText ?? true,
canDownloadSource: existingConfig?.canDownloadSource ?? true
});
}
export default NextAPI(handler);

View File

@ -0,0 +1,45 @@
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type UpdatePlaygroundVisibilityConfigBody,
UpdatePlaygroundVisibilityConfigBodySchema
} from '@fastgpt/global/support/outLink/api.d';
async function handler(req: ApiRequestProps<UpdatePlaygroundVisibilityConfigBody, {}>) {
const { appId, showRunningStatus, showCite, showFullText, canDownloadSource } =
UpdatePlaygroundVisibilityConfigBodySchema.parse(req.body);
const { teamId, tmbId } = await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
await MongoOutLink.updateOne(
{ appId, type: PublishChannelEnum.playground },
{
$setOnInsert: {
shareId: `playground-${appId}`,
teamId,
tmbId,
name: 'Playground Chat'
},
$set: {
appId,
type: PublishChannelEnum.playground,
showRunningStatus: showRunningStatus,
showCite: showCite,
showFullText: showFullText,
canDownloadSource: canDownloadSource
}
},
{ upsert: true }
);
}
export default NextAPI(handler);

View File

@ -26,7 +26,8 @@ export type OutLinkUpdateResponse = string;
async function handler(
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
): Promise<OutLinkUpdateResponse> {
const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body;
const { _id, name, showCite, limit, app, canDownloadSource, showRunningStatus, showFullText } =
req.body;
if (!_id) {
return Promise.reject(CommonErrEnum.missingParams);
@ -46,10 +47,10 @@ async function handler(
const doc = await MongoOutLink.findByIdAndUpdate(_id, {
name,
responseDetail,
showRawSource,
showNodeStatus,
// showFullText,
showCite,
canDownloadSource,
showRunningStatus,
showFullText,
limit,
app
});

View File

@ -91,8 +91,8 @@ type AuthResponseType = {
teamId: string;
tmbId: string;
app: AppSchema;
responseDetail?: boolean;
showNodeStatus?: boolean;
showCite?: boolean;
showRunningStatus?: boolean;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
responseAllData: boolean;
@ -157,13 +157,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId,
tmbId,
app,
responseDetail,
showCite,
authType,
sourceName,
apikey,
responseAllData,
outLinkUserId = customUid,
showNodeStatus
showRunningStatus
} = await (async () => {
// share chat
if (shareId && outLinkUid) {
@ -205,7 +205,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
pushTrack.teamChatQPM({ teamId });
retainDatasetCite = retainDatasetCite && !!responseDetail;
retainDatasetCite = retainDatasetCite && !!showCite;
const isPlugin = app.type === AppTypeEnum.workflowTool;
// Check message type
@ -275,7 +275,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail,
streamResponse: stream,
id: chatId,
showNodeStatus
showNodeStatus: showRunningStatus
});
const saveChatId = chatId || getNanoid(24);
@ -388,7 +388,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
/* select fe response field */
const feResponseData = responseAllData
? flowResponses
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail });
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite });
if (stream) {
workflowResponseWrite({
@ -508,7 +508,7 @@ const authShareChat = async ({
shareId: string;
chatId?: string;
}): Promise<AuthResponseType> => {
const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } =
await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean();
@ -530,9 +530,9 @@ const authShareChat = async ({
apikey: '',
authType,
responseAllData: false,
responseDetail,
showCite,
outLinkUserId: uid,
showNodeStatus
showRunningStatus
};
};
const authTeamSpaceChat = async ({
@ -569,7 +569,7 @@ const authTeamSpaceChat = async ({
authType: AuthUserTypeEnum.outLink,
apikey: '',
responseAllData: false,
responseDetail: true,
showCite: true,
outLinkUserId: uid
};
};
@ -651,7 +651,7 @@ const authHeaderRequest = async ({
authType,
sourceName,
responseAllData: true,
responseDetail: true
showCite: true
};
};

View File

@ -92,8 +92,8 @@ type AuthResponseType = {
teamId: string;
tmbId: string;
app: AppSchema;
responseDetail?: boolean;
showNodeStatus?: boolean;
showCite?: boolean;
showRunningStatus?: boolean;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
responseAllData: boolean;
@ -158,13 +158,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId,
tmbId,
app,
responseDetail,
showCite,
authType,
sourceName,
apikey,
responseAllData,
outLinkUserId = customUid,
showNodeStatus
showRunningStatus
} = await (async () => {
// share chat
if (shareId && outLinkUid) {
@ -206,7 +206,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
pushTrack.teamChatQPM({ teamId });
retainDatasetCite = retainDatasetCite && !!responseDetail;
retainDatasetCite = retainDatasetCite && !!showCite;
const isPlugin = app.type === AppTypeEnum.workflowTool;
// Check message type
@ -275,7 +275,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail,
streamResponse: stream,
id: chatId,
showNodeStatus
showNodeStatus: showRunningStatus
});
const saveChatId = chatId || getNanoid(24);
@ -321,7 +321,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite,
responseAllData,
responseDetail
responseDetail: showCite
});
}
return Promise.reject('您的工作流版本过低,请重新发布一次');
@ -390,7 +390,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
/* select fe response field */
const feResponseData = responseAllData
? flowResponses
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail });
: filterPublicNodeResponseData({ nodeRespones: flowResponses, responseDetail: showCite });
if (stream) {
workflowResponseWrite({
@ -503,7 +503,7 @@ const authShareChat = async ({
shareId: string;
chatId?: string;
}): Promise<AuthResponseType> => {
const { teamId, tmbId, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
const { teamId, tmbId, appId, authType, showCite, showRunningStatus, uid, sourceName } =
await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean();
@ -525,9 +525,9 @@ const authShareChat = async ({
apikey: '',
authType,
responseAllData: false,
responseDetail,
showCite,
outLinkUserId: uid,
showNodeStatus
showRunningStatus
};
};
const authTeamSpaceChat = async ({
@ -564,7 +564,7 @@ const authTeamSpaceChat = async ({
authType: AuthUserTypeEnum.outLink,
apikey: '',
responseAllData: false,
responseDetail: true,
showCite: true,
outLinkUserId: uid
};
};
@ -646,7 +646,7 @@ const authHeaderRequest = async ({
authType,
sourceName,
responseAllData: true,
responseDetail: true
showCite: true
};
};

View File

@ -29,6 +29,9 @@ import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp';
import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { LoginSuccessResponse } from '@/global/support/api/userRes';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { addLog } from '@fastgpt/service/common/system/log';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const { isPc } = useSystem();
@ -88,7 +91,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
);
};
const Render = (props: { appId: string; isStandalone?: string }) => {
const Render = (props: {
appId: string;
isStandalone?: string;
showRunningStatus: boolean;
showCite: boolean;
showFullText: boolean;
canDownloadSource: boolean;
}) => {
const { appId, isStandalone } = props;
const { chatId } = useChatStore();
const { setUserInfo } = useUserStore();
@ -138,9 +148,10 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider
showRouteToDatasetDetail={isStandalone !== '1'}
isShowReadRawSource={true}
isResponseDetail={true}
showNodeStatus
showRunningStatus={props.showRunningStatus}
canDownloadSource={props.canDownloadSource}
isShowCite={props.showCite}
isShowFullText={props.showFullText}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat myApps={myApps} />
@ -151,14 +162,38 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
);
};
export default Render;
export async function getServerSideProps(context: any) {
const appId = context?.query?.appId || '';
const chatQuoteReaderConfig = await (async () => {
try {
if (!appId) return null;
const config = await MongoOutLink.findOne(
{
appId,
type: PublishChannelEnum.playground
},
'showRunningStatus showCite showFullText canDownloadSource'
).lean();
return config;
} catch (error) {
addLog.error('getServerSideProps', error);
return null;
}
})();
return {
props: {
appId: context?.query?.appId || '',
isStandalone: context?.query?.isStandalone || '',
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow', 'user', 'login']))
appId,
showRunningStatus: chatQuoteReaderConfig?.showRunningStatus ?? true,
showCite: chatQuoteReaderConfig?.showCite ?? true,
showFullText: chatQuoteReaderConfig?.showFullText ?? true,
canDownloadSource: chatQuoteReaderConfig?.canDownloadSource ?? true,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
}
};
}
export default Render;

View File

@ -53,10 +53,10 @@ type Props = {
shareId: string;
authToken: string;
customUid: string;
showRawSource: boolean;
responseDetail: boolean;
// showFullText: boolean;
showNodeStatus: boolean;
canDownloadSource: boolean;
isShowCite: boolean;
isShowFullText: boolean;
showRunningStatus: boolean;
};
const OutLink = (props: Props) => {
@ -95,7 +95,7 @@ const OutLink = (props: Props) => {
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@ -175,7 +175,7 @@ const OutLink = (props: Props) => {
responseChatItemId,
chatId: completionChatId,
...outLinkAuthData,
retainDatasetCite: isResponseDetail
retainDatasetCite: isShowCite
},
onMessage: generatingMessage,
abortCtrl: controller
@ -213,7 +213,7 @@ const OutLink = (props: Props) => {
chatId,
customVariables,
outLinkAuthData,
isResponseDetail,
isShowCite,
onUpdateHistoryTitle,
setChatBoxData,
forbidLoadChat,
@ -388,10 +388,10 @@ const Render = (props: Props) => {
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider
showRouteToDatasetDetail={false}
isShowReadRawSource={props.showRawSource}
isResponseDetail={props.responseDetail}
// isShowFullText={props.showFullText}
showNodeStatus={props.showNodeStatus}
canDownloadSource={props.canDownloadSource}
isShowCite={props.isShowCite}
isShowFullText={props.isShowFullText}
showRunningStatus={props.showRunningStatus}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<OutLink {...props} />
@ -416,7 +416,7 @@ export async function getServerSideProps(context: any) {
{
shareId
},
'appId showRawSource showNodeStatus responseDetail'
'appId canDownloadSource showCite showFullText showRunningStatus'
)
.populate<{ associatedApp: AppSchema }>('associatedApp', 'name avatar intro')
.lean();
@ -432,10 +432,10 @@ export async function getServerSideProps(context: any) {
appName: app?.associatedApp?.name ?? 'AI',
appAvatar: app?.associatedApp?.avatar ?? '',
appIntro: app?.associatedApp?.intro ?? 'AI',
showRawSource: app?.showRawSource ?? false,
responseDetail: app?.responseDetail ?? false,
// showFullText: app?.showFullText ?? false,
showNodeStatus: app?.showNodeStatus ?? false,
canDownloadSource: app?.canDownloadSource ?? false,
isShowCite: app?.showCite ?? false,
isShowFullText: app?.showFullText ?? false,
showRunningStatus: app?.showRunningStatus ?? false,
shareId: shareId ?? '',
authToken: authToken ?? '',
customUid,

View File

@ -24,9 +24,10 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
Chat没有读写的权限之分
*/
export const defaultResponseShow = {
responseDetail: true,
showNodeStatus: true,
showRawSource: true
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true
};
type AuthChatCommonProps = {
appId: string;
@ -54,9 +55,10 @@ export async function authChatCrud({
tmbId: string;
uid: string;
chat?: ChatSchemaType;
responseDetail: boolean;
showNodeStatus: boolean;
showRawSource: boolean;
showCite: boolean;
showRunningStatus: boolean;
showFullText: boolean;
canDownloadSource: boolean;
authType?: `${AuthUserTypeEnum}`;
}> {
if (!appId) return Promise.reject(ChatErrEnum.unAuthChat);
@ -109,9 +111,11 @@ export async function authChatCrud({
teamId: String(outLinkConfig.teamId),
tmbId: String(outLinkConfig.tmbId),
uid,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus ?? true,
showRawSource: outLinkConfig.showRawSource ?? false,
showCite: outLinkConfig.showCite ?? false,
showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink
};
}
@ -123,9 +127,10 @@ export async function authChatCrud({
teamId: String(outLinkConfig.teamId),
tmbId: String(outLinkConfig.tmbId),
uid,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus ?? true,
showRawSource: outLinkConfig.showRawSource ?? false,
showCite: outLinkConfig.showCite ?? false,
showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink
};
}
@ -135,9 +140,10 @@ export async function authChatCrud({
tmbId: String(outLinkConfig.tmbId),
chat,
uid,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus ?? true,
showRawSource: outLinkConfig.showRawSource ?? false,
showCite: outLinkConfig.showCite ?? false,
showRunningStatus: outLinkConfig.showRunningStatus ?? true,
showFullText: outLinkConfig.showFullText ?? false,
canDownloadSource: outLinkConfig.canDownloadSource ?? false,
authType: AuthUserTypeEnum.outLink
};
}

View File

@ -63,8 +63,10 @@ export async function authOutLinkChatStart({
teamId: outLinkConfig.teamId,
tmbId: outLinkConfig.tmbId,
authType: AuthUserTypeEnum.token,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus,
showCite: outLinkConfig.showCite,
showRunningStatus: outLinkConfig.showRunningStatus,
showFullText: outLinkConfig.showFullText,
canDownloadSource: outLinkConfig.canDownloadSource,
appId,
uid
};

View File

@ -26,10 +26,10 @@ export const defaultApp: AppDetailType = {
export const defaultOutLinkForm: OutLinkEditType = {
name: '',
showNodeStatus: true,
responseDetail: false,
// showFullText: false,
showRawSource: false,
showRunningStatus: true,
showCite: false,
showFullText: false,
canDownloadSource: false,
limit: {
QPM: 100,
maxUsagePoints: -1

View File

@ -13,10 +13,10 @@ import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch
type ContextProps = {
showRouteToDatasetDetail: boolean;
isShowReadRawSource: boolean;
isResponseDetail: boolean;
// isShowFullText: boolean;
showNodeStatus: boolean;
canDownloadSource: boolean;
isShowCite: boolean;
isShowFullText: boolean;
showRunningStatus: boolean;
};
type ChatBoxDataType = {
chatId?: string;
@ -120,10 +120,10 @@ export const ChatItemContext = createContext<ChatItemContextType>({
const ChatItemContextProvider = ({
children,
showRouteToDatasetDetail,
isShowReadRawSource,
isResponseDetail,
// isShowFullText,
showNodeStatus
canDownloadSource,
isShowCite,
isShowFullText,
showRunningStatus
}: {
children: ReactNode;
} & ContextProps) => {
@ -196,10 +196,10 @@ const ChatItemContextProvider = ({
resetVariables,
clearChatRecords,
showRouteToDatasetDetail,
isShowReadRawSource,
isResponseDetail,
// isShowFullText,
showNodeStatus,
canDownloadSource,
isShowCite,
isShowFullText,
showRunningStatus,
datasetCiteData,
setCiteModalData,
@ -214,10 +214,10 @@ const ChatItemContextProvider = ({
resetVariables,
clearChatRecords,
showRouteToDatasetDetail,
isShowReadRawSource,
isResponseDetail,
// isShowFullText,
showNodeStatus,
canDownloadSource,
isShowCite,
showRunningStatus,
isShowFullText,
datasetCiteData,
setCiteModalData,
isVariableVisible,

View File

@ -1,3 +1,8 @@
import type {
PlaygroundVisibilityConfigQuery,
PlaygroundVisibilityConfigResponse,
UpdatePlaygroundVisibilityConfigBody
} from '@fastgpt/global/support/outLink/api';
import { GET, POST, DELETE } from '@/web/common/api/request';
import type {
OutlinkAppType,
@ -36,6 +41,14 @@ export function updateShareChat<T extends OutlinkAppType>(data: OutLinkEditType<
return POST<string>(`/support/outLink/update`, data);
}
export function getPlaygroundVisibilityConfig(data: PlaygroundVisibilityConfigQuery) {
return GET<PlaygroundVisibilityConfigResponse>('/support/outLink/playground/config', data);
}
export function updatePlaygroundVisibilityConfig(data: UpdatePlaygroundVisibilityConfigBody) {
return POST<string>(`/support/outLink/playground/update`, data);
}
// /**
// * create a shareChat
// */

View File

@ -0,0 +1,175 @@
import type { PlaygroundVisibilityConfigResponse } from '@fastgpt/global/support/outLink/api.d';
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 { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import * as configApi from '@/pages/api/support/outLink/playground/config';
describe('Playground Visibility Config API', () => {
let rootUser: any;
let testApp: any;
beforeAll(async () => {
rootUser = await getRootUser();
// Create a test app owned by the root user
testApp = await MongoApp.create({
name: 'Test App for Playground Config',
type: 'simple',
tmbId: rootUser.tmbId,
teamId: rootUser.teamId
});
});
afterEach(async () => {
// Clean up any created OutLink configs
await MongoOutLink.deleteMany({
appId: testApp._id,
type: PublishChannelEnum.playground
});
});
afterAll(async () => {
// Clean up test data
await MongoApp.deleteOne({ _id: testApp._id });
});
it('should return default config values when no existing config found', async () => {
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
auth: rootUser,
query: {
appId: testApp._id
}
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
expect(res.data).toEqual({
showRunningStatus: true,
showCite: true,
showFullText: true,
canDownloadSource: true
});
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should return existing config values when config exists', async () => {
// Create an existing config
await MongoOutLink.create({
shareId: `playground-${testApp._id}`,
teamId: rootUser.teamId,
tmbId: rootUser.tmbId,
appId: testApp._id,
name: 'Playground Chat',
type: PublishChannelEnum.playground,
showRunningStatus: false,
showCite: false,
showFullText: false,
canDownloadSource: false,
usagePoints: 0,
lastTime: new Date()
});
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
auth: rootUser,
query: {
appId: testApp._id
}
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
expect(res.data).toEqual({
showRunningStatus: false,
showCite: false,
showFullText: false,
canDownloadSource: false
});
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should return 500 when appId is missing', async () => {
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
auth: rootUser,
query: {}
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should return 500 when appId is empty string', async () => {
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
auth: rootUser,
query: {
appId: ''
}
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should handle mixed config values correctly', async () => {
// Create config with mixed true/false values
await MongoOutLink.create({
shareId: `playground-${testApp._id}`,
teamId: rootUser.teamId,
tmbId: rootUser.tmbId,
appId: testApp._id,
name: 'Playground Chat',
type: PublishChannelEnum.playground,
showRunningStatus: true,
showCite: false,
showFullText: true,
canDownloadSource: false,
usagePoints: 0,
lastTime: new Date()
});
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
auth: rootUser,
query: {
appId: testApp._id
}
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
expect(res.data).toEqual({
showRunningStatus: true,
showCite: false,
showFullText: true,
canDownloadSource: false
});
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should return error when user is not authenticated', async () => {
const res = await Call<PlaygroundVisibilityConfigResponse>(configApi.default, {
query: {
appId: testApp._id
}
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
});

View File

@ -0,0 +1,294 @@
import type { UpdatePlaygroundVisibilityConfigBody } from '@fastgpt/global/support/outLink/api.d';
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 { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import * as updateApi from '@/pages/api/support/outLink/playground/update';
describe('Playground Visibility Update API', () => {
let rootUser: any;
let testApp: any;
beforeAll(async () => {
rootUser = await getRootUser();
// Create a test app owned by the root user
testApp = await MongoApp.create({
name: 'Test App for Playground Update',
type: 'simple',
tmbId: rootUser.tmbId,
teamId: rootUser.teamId
});
});
afterEach(async () => {
// Clean up any created OutLink configs
await MongoOutLink.deleteMany({
appId: testApp._id,
type: PublishChannelEnum.playground
});
});
afterAll(async () => {
// Clean up test data
await MongoApp.deleteOne({ _id: testApp._id });
});
it('should handle update request with valid data', async () => {
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: testApp._id,
showRunningStatus: false,
showCite: false,
showFullText: false,
canDownloadSource: false
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
// Verify the config was created in database
const createdConfig = await MongoOutLink.findOne({
appId: testApp._id,
type: PublishChannelEnum.playground
}).lean();
if (createdConfig) {
expect(createdConfig.appId).toBe(testApp._id);
expect(createdConfig.type).toBe(PublishChannelEnum.playground);
expect(createdConfig.showRunningStatus).toBe(false);
expect(createdConfig.showCite).toBe(false);
expect(createdConfig.showFullText).toBe(false);
expect(createdConfig.canDownloadSource).toBe(false);
}
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should handle update request with true values', async () => {
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: testApp._id,
showRunningStatus: true,
showCite: true,
showFullText: true,
canDownloadSource: true
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
// Verify true values were set
const createdConfig = await MongoOutLink.findOne({
appId: testApp._id,
type: PublishChannelEnum.playground
}).lean();
if (createdConfig) {
expect(createdConfig.showRunningStatus).toBe(true);
expect(createdConfig.showCite).toBe(true);
expect(createdConfig.showFullText).toBe(true);
expect(createdConfig.canDownloadSource).toBe(true);
}
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should handle update request with mixed boolean values', async () => {
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: testApp._id,
showRunningStatus: false,
showCite: true,
showFullText: false,
canDownloadSource: true
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
// Verify mixed values were set
const createdConfig = await MongoOutLink.findOne({
appId: testApp._id,
type: PublishChannelEnum.playground
}).lean();
if (createdConfig) {
expect(createdConfig.showRunningStatus).toBe(false);
expect(createdConfig.showCite).toBe(true);
expect(createdConfig.showFullText).toBe(false);
expect(createdConfig.canDownloadSource).toBe(true);
}
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
});
it('should return 500 when appId is missing', async () => {
const updateData = {
showRunningStatus: false
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should return 500 when appId is empty string', async () => {
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: '',
showRunningStatus: false,
showCite: false,
showFullText: false,
canDownloadSource: false
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should return error when user is not authenticated', async () => {
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: testApp._id,
showRunningStatus: false,
showCite: false,
showFullText: false,
canDownloadSource: false
};
const res = await Call(updateApi.default, {
body: updateData
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should validate all boolean fields are required', async () => {
// Test with missing boolean fields (should fail validation)
const updateData = {
appId: testApp._id,
showRunningStatus: false
// Missing other boolean fields
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
it('should handle updates for different apps independently', async () => {
// Create a second test app
const testApp2 = await MongoApp.create({
name: 'Test App 2 for Playground Update',
type: 'simple',
tmbId: rootUser.tmbId,
teamId: rootUser.teamId
});
// Create config for first app
await MongoOutLink.create({
shareId: `playground-${testApp._id}`,
teamId: rootUser.teamId,
tmbId: rootUser.tmbId,
appId: testApp._id,
name: 'Playground Chat',
type: PublishChannelEnum.playground,
showRunningStatus: true,
showCite: true,
showFullText: true,
canDownloadSource: true,
usagePoints: 0,
lastTime: new Date()
});
// Update config for second app
const updateData: UpdatePlaygroundVisibilityConfigBody = {
appId: testApp2._id,
showRunningStatus: false,
showCite: false,
showFullText: true,
canDownloadSource: true
};
const res = await Call(updateApi.default, {
auth: rootUser,
body: updateData
});
// Check if the request was processed successfully
if (res.code === 200) {
expect(res.error).toBeUndefined();
// Verify first app config is unchanged
const config1 = await MongoOutLink.findOne({
appId: testApp._id,
type: PublishChannelEnum.playground
}).lean();
if (config1) {
expect(config1.showRunningStatus).toBe(true);
expect(config1.showCite).toBe(true);
}
// Verify second app config was created with new values
const config2 = await MongoOutLink.findOne({
appId: testApp2._id,
type: PublishChannelEnum.playground
}).lean();
if (config2) {
expect(config2.showRunningStatus).toBe(false);
expect(config2.showCite).toBe(false);
expect(config2.showFullText).toBe(true);
expect(config2.canDownloadSource).toBe(true);
}
} else {
// If there are permission issues, we still expect the API to validate parameters
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
}
// Cleanup second app
await MongoOutLink.deleteOne({ appId: testApp2._id });
await MongoApp.deleteOne({ _id: testApp2._id });
});
});

View File

@ -80,9 +80,10 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'user1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -116,9 +117,10 @@ describe('authChatCrud', () => {
tmbId: 'tmb1',
uid: 'user1',
chat: mockChat,
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -145,9 +147,10 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'user1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -186,9 +189,9 @@ describe('authChatCrud', () => {
outLinkConfig: {
teamId: 'team1',
tmbId: 'tmb1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true
showCite: true,
showRunningStatus: true,
canDownloadSource: true
},
uid: 'user1',
appId: 'app1'
@ -206,19 +209,20 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'user1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: false,
canDownloadSource: true,
authType: AuthUserTypeEnum.outLink
});
});
it('should auth outLink with default showNodeStatus and showRawSource', async () => {
it('should auth outLink with default showRunningStatus and canDownloadSource', async () => {
vi.mocked(authOutLink).mockResolvedValue({
outLinkConfig: {
teamId: 'team1',
tmbId: 'tmb1',
responseDetail: false,
showCite: false,
shareId: 'share1',
outLinkUid: 'user1'
},
@ -238,9 +242,9 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'user1',
responseDetail: false,
showNodeStatus: true, // default
showRawSource: false, // default
showCite: false,
showRunningStatus: true, // default
canDownloadSource: false, // default
authType: AuthUserTypeEnum.outLink
});
});
@ -255,9 +259,9 @@ describe('authChatCrud', () => {
outLinkConfig: {
teamId: 'team1',
tmbId: 'tmb1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true
showCite: true,
showRunningStatus: true,
canDownloadSource: true
},
uid: 'user1',
appId: 'app1'
@ -281,9 +285,10 @@ describe('authChatCrud', () => {
tmbId: 'tmb1',
uid: 'user1',
chat: mockChat,
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: false,
canDownloadSource: true,
authType: AuthUserTypeEnum.outLink
});
});
@ -293,9 +298,9 @@ describe('authChatCrud', () => {
outLinkConfig: {
teamId: 'team1',
tmbId: 'tmb1',
responseDetail: true,
showNodeStatus: false,
showRawSource: true
showCite: true,
showRunningStatus: false,
canDownloadSource: true
},
uid: 'user1',
appId: 'app1'
@ -318,9 +323,10 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'user1',
responseDetail: true,
showNodeStatus: false,
showRawSource: true,
showCite: true,
showRunningStatus: false,
showFullText: false,
canDownloadSource: true,
authType: AuthUserTypeEnum.outLink
});
});
@ -335,7 +341,10 @@ describe('authChatCrud', () => {
outLinkConfig: {
teamId: 'team1',
tmbId: 'tmb1',
responseDetail: true
showCite: true,
showFullText: true,
showRunningStatus: true,
canDownloadSource: true
},
uid: 'user1',
appId: 'app1'
@ -428,9 +437,10 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'tmb1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -467,9 +477,10 @@ describe('authChatCrud', () => {
tmbId: 'tmb1',
uid: 'tmb1',
chat: mockChat,
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -507,9 +518,10 @@ describe('authChatCrud', () => {
tmbId: 'tmb1',
uid: 'tmb1',
chat: mockChat,
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});
@ -539,9 +551,10 @@ describe('authChatCrud', () => {
teamId: 'team1',
tmbId: 'tmb1',
uid: 'tmb1',
responseDetail: true,
showNodeStatus: true,
showRawSource: true,
showCite: true,
showRunningStatus: true,
showFullText: true,
canDownloadSource: true,
authType: AuthUserTypeEnum.teamDomain
});
});