diff --git a/client/public/js/iframe.js b/client/public/js/iframe.js index 2490ba73d..5b0e3421e 100644 --- a/client/public/js/iframe.js +++ b/client/public/js/iframe.js @@ -1,66 +1,61 @@ async function embedChatbot() { - const t = window.difyChatbotConfig; - if (t && t.token) { - var e = !!t.isDev; - const o = t.baseUrl || `https://${e ? 'dev.' : ''}udify.app`, - n = ` - - `, - i = ` - - `; - if (!document.getElementById('dify-chatbot-bubble-button')) { - e = document.createElement('div'); - (e.id = 'dify-chatbot-bubble-button'), - (e.style.cssText = - 'position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}'); - const d = document.createElement('div'); - (d.style.cssText = - 'display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;'), - (d.innerHTML = n), - e.appendChild(d), - document.body.appendChild(e), - e.addEventListener('click', function () { - var e = document.getElementById('dify-chatbot-bubble-window'); - e - ? 'none' === e.style.display - ? ((e.style.display = 'block'), (d.innerHTML = i)) - : ((e.style.display = 'none'), (d.innerHTML = n)) - : (((e = document.createElement('iframe')).allow = 'fullscreen;microphone'), - (e.title = 'dify chatbot bubble window'), - (e.id = 'dify-chatbot-bubble-window'), - (e.src = o + '/chatbot/' + t.token), - (e.style.cssText = - 'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;'), - document.body.appendChild(e), - (d.innerHTML = i)); - }); + const chatBtnId = 'fastgpt-chatbot-button'; + const chatWindowId = 'fastgpt-chatbot-window'; + const script = document.getElementById('fastgpt-iframe'); + const botSrc = script?.getAttribute('data-src'); + const primaryColor = script?.getAttribute('data-color') || '#4e83fd'; + + if (!botSrc) { + console.error(`Can't find appid`); + return; + } + if (document.getElementById(chatBtnId)) { + return; + } + + const MessageIcon = ``; + + const CloseIcon = ``; + + const ChatBtn = document.createElement('div'); + ChatBtn.id = chatBtnId; + ChatBtn.style.cssText = + 'position: fixed; bottom: 1rem; right: 1rem; width: 40px; height: 40px; cursor: pointer; z-index: 2147483647; '; + + const ChatBtnDiv = document.createElement('div'); + ChatBtnDiv.style.cssText = + 'transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);} display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 9999;'; + ChatBtnDiv.innerHTML = MessageIcon; + + ChatBtn.appendChild(ChatBtnDiv); + + const iframe = document.createElement('iframe'); + iframe.allow = 'fullscreen;microphone'; + iframe.title = 'FastGpt Chat Window'; + iframe.id = chatWindowId; + iframe.src = botSrc; + + iframe.style.cssText = + 'visibility: hidden; border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 4rem; right: 1rem; width: 24rem; height: 40rem; max-width: 90vw; max-height: 85vh; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;'; + + document.body.appendChild(iframe); + + iframe.onload = () => { + document.body.appendChild(ChatBtn); + }; + + ChatBtn.addEventListener('click', function () { + const chatWindow = document.getElementById(chatWindowId); + + if (!chatWindow) return; + const visibilityVal = chatWindow.style.visibility; + if (visibilityVal === 'hidden') { + chatWindow.style.visibility = 'unset'; + ChatBtnDiv.innerHTML = CloseIcon; + } else { + chatWindow.style.visibility = 'hidden'; + ChatBtnDiv.innerHTML = MessageIcon; } - } else console.error('difyChatbotConfig is empty or token is not provided'); + }); } document.body.onload = embedChatbot; diff --git a/client/src/api/chat.ts b/client/src/api/chat.ts index b3d428bc0..9fc5e125e 100644 --- a/client/src/api/chat.ts +++ b/client/src/api/chat.ts @@ -2,9 +2,8 @@ import { GET, POST, DELETE, PUT } from './request'; import type { ChatHistoryItemType } from '@/types/chat'; import type { InitChatResponse, InitShareChatResponse } from './response/chat'; import { RequestPaging } from '../types/index'; -import type { ShareChatSchema } from '@/types/mongoSchema'; +import type { OutLinkSchema } from '@/types/mongoSchema'; import type { ShareChatEditType } from '@/types/app'; -import type { QuoteItemType } from '@/types/chat'; import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory'; /** @@ -50,6 +49,12 @@ export const delChatRecordByIndex = (data: { chatId: string; contentId: string } export const putChatHistory = (data: UpdateHistoryProps) => PUT('/chat/history/updateChatHistory', data); +/** + * 初始化分享聊天 + */ +export const initShareChatInfo = (data: { shareId: string }) => + GET(`/chat/shareChat/init`, data); + /** * create a shareChat */ @@ -63,15 +68,9 @@ export const createShareChat = ( * get shareChat */ export const getShareChatList = (appId: string) => - GET(`/chat/shareChat/list`, { appId }); + GET(`/chat/shareChat/list`, { appId }); /** * delete a shareChat */ export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`); - -/** - * 初始化分享聊天 - */ -export const initShareChatInfo = (data: { shareId: string }) => - GET(`/chat/shareChat/init`, data); diff --git a/client/src/constants/user.ts b/client/src/constants/user.ts index af407832f..9d6d22dfb 100644 --- a/client/src/constants/user.ts +++ b/client/src/constants/user.ts @@ -1,6 +1,7 @@ export enum BillSourceEnum { fastgpt = 'fastgpt', - api = 'api' + api = 'api', + shareLink = 'shareLink' } export enum PageTypeEnum { login = 'login', @@ -10,7 +11,8 @@ export enum PageTypeEnum { export const BillSourceMap: Record<`${BillSourceEnum}`, string> = { [BillSourceEnum.fastgpt]: 'FastGpt 平台', - [BillSourceEnum.api]: 'Api' + [BillSourceEnum.api]: 'Api', + [BillSourceEnum.shareLink]: '免登录链接' }; export enum PromotionEnum { diff --git a/client/src/pages/api/openapi/v1/chat/completions.ts b/client/src/pages/api/openapi/v1/chat/completions.ts index 7d742c543..f82b5300c 100644 --- a/client/src/pages/api/openapi/v1/chat/completions.ts +++ b/client/src/pages/api/openapi/v1/chat/completions.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { connectToDatabase } from '@/service/mongo'; -import { authUser, authApp, authShareChat } from '@/service/utils/auth'; +import { authUser, authApp, authShareChat, AuthUserTypeEnum } from '@/service/utils/auth'; import { sseErrRes, jsonRes } from '@/service/response'; import { withNextCors } from '@/service/utils/tools'; import { ChatRoleEnum, ChatSourceEnum, sseResponseEventEnum } from '@/constants/chat'; @@ -25,7 +25,6 @@ import { pushTaskBill } from '@/service/events/pushBill'; import { BillSourceEnum } from '@/constants/user'; import { ChatHistoryItemResType } from '@/types/chat'; import { UserModelSchema } from '@/types/mongoSchema'; -import { getAIChatApi } from '@/service/ai/openai'; export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; type FastGptWebChatProps = { @@ -84,7 +83,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex if (!user) { throw new Error('Account is error'); } - if (authType !== 'token') { + if (authType === AuthUserTypeEnum.apikey || shareId) { user.openaiAccount = undefined; } @@ -208,7 +207,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex appName: app.name, appId, userId, - source: authType === 'apikey' ? BillSourceEnum.api : BillSourceEnum.fastgpt, + source: (() => { + if (authType === 'apikey') return BillSourceEnum.api; + if (shareId) return BillSourceEnum.shareLink; + return BillSourceEnum.fastgpt; + })(), response: responseData, shareId }); diff --git a/client/src/pages/app/detail/components/OutLink.tsx b/client/src/pages/app/detail/components/OutLink.tsx index 731f9bb4f..e167af8f9 100644 --- a/client/src/pages/app/detail/components/OutLink.tsx +++ b/client/src/pages/app/detail/components/OutLink.tsx @@ -19,11 +19,10 @@ import { } from '@chakra-ui/react'; import { QuestionOutlineIcon } from '@chakra-ui/icons'; import MyIcon from '@/components/Icon'; -import { useToast } from '@/hooks/useToast'; import { useLoading } from '@/hooks/useLoading'; import { useQuery } from '@tanstack/react-query'; import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat'; -import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools'; +import { formatTimeToChatTime, useCopyData } from '@/utils/tools'; import { useForm } from 'react-hook-form'; import { defaultShareChat } from '@/constants/model'; import type { ShareChatEditType } from '@/types/app'; @@ -67,8 +66,8 @@ const Share = ({ appId }: { appId: string }) => { onSuccess(id) { onCloseCreateShareChat(); refetchShareChatList(); - const url = `对话地址为:${location.origin}/chat/share?shareId=${id}`; - copyData(url, '已复制分享地址'); + const url = `${location.origin}/chat/share?shareId=${id}`; + copyData(url, '创建成功。已复制分享地址,可直接分享使用'); resetShareChat(defaultShareChat); } }); @@ -116,40 +115,53 @@ const Share = ({ appId }: { appId: string }) => { {item.name} {formatPrice(item.total)}元 {item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'} - - - - { - const url = `${location.origin}/chat/share?shareId=${item.shareId}`; - copyData(url, '已复制分享链接'); - }} - /> - - - { - setIsLoading(true); - try { - await delShareChatById(item._id); - refetchShareChatList(); - } catch (error) { - console.log(error); - } - setIsLoading(false); - }} - /> - - + + + { + const url = `${location.origin}/chat/share?shareId=${item.shareId}`; + const src = `${location.origin}/js/iframe.js`; + const script = ``; + copyData(script, '已复制嵌入 Script,可在应用 HTML 底部嵌入', 3000); + }} + /> + + + { + const url = `${location.origin}/chat/share?shareId=${item.shareId}`; + copyData(url, '已复制分享链接,可直接分享使用'); + }} + /> + + + { + setIsLoading(true); + try { + await delShareChatById(item._id); + refetchShareChatList(); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }} + /> + ))} diff --git a/client/src/pages/app/list/index.tsx b/client/src/pages/app/list/index.tsx index 318573cf5..c6d744c41 100644 --- a/client/src/pages/app/list/index.tsx +++ b/client/src/pages/app/list/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Box, Grid, diff --git a/client/src/pages/chat/components/ToolMenu.tsx b/client/src/pages/chat/components/ToolMenu.tsx index 90458c4e0..d74bd50ce 100644 --- a/client/src/pages/chat/components/ToolMenu.tsx +++ b/client/src/pages/chat/components/ToolMenu.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/router'; const ToolMenu = ({ history }: { history: ChatItemType[] }) => { const { onExportChat } = useChatBox(); const router = useRouter(); - const { appId } = router.query; + const { appId, shareId } = router.query; const menuList = useMemo( () => [ @@ -18,7 +18,8 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => { onClick: () => { router.replace({ query: { - appId + appId, + shareId } }); } @@ -35,7 +36,7 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => { }, { icon: 'pdf', label: 'PDF导出', onClick: () => onExportChat({ type: 'pdf', history }) } ], - [appId, history, onExportChat, router] + [appId, history, onExportChat, router, shareId] ); return history.length > 0 ? ( @@ -59,7 +60,9 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => { ))} - ) : null; + ) : ( + + ); }; export default ToolMenu; diff --git a/client/src/service/utils/auth.ts b/client/src/service/utils/auth.ts index 45fbcfacf..13aecb0dd 100644 --- a/client/src/service/utils/auth.ts +++ b/client/src/service/utils/auth.ts @@ -2,11 +2,14 @@ import type { NextApiRequest } from 'next'; import jwt from 'jsonwebtoken'; import Cookie from 'cookie'; import { App, OpenApi, User, OutLink, KB } from '../mongo'; -import type { AppSchema, UserModelSchema } from '@/types/mongoSchema'; -import { formatPrice } from '@/utils/user'; +import type { AppSchema } from '@/types/mongoSchema'; import { ERROR_ENUM } from '../errorCode'; -export type AuthType = 'token' | 'root' | 'apikey'; +export enum AuthUserTypeEnum { + token = 'token', + root = 'root', + apikey = 'apikey' +} export const parseCookie = (cookie?: string): Promise => { return new Promise((resolve, reject) => { @@ -124,28 +127,28 @@ export const authUser = async ({ let uid = ''; let appId = ''; - let authType: AuthType = 'token'; + let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token; if (authToken) { uid = await parseCookie(cookie); - authType = 'token'; + authType = AuthUserTypeEnum.token; } else if (authRoot) { uid = await parseRootKey(rootkey, userid); - authType = 'root'; + authType = AuthUserTypeEnum.root; } else if (cookie) { uid = await parseCookie(cookie); - authType = 'token'; + authType = AuthUserTypeEnum.token; } else if (apikey) { uid = await parseOpenApiKey(apikey); - authType = 'apikey'; + authType = AuthUserTypeEnum.apikey; } else if (authorization) { const authResponse = await parseAuthorization(authorization); uid = authResponse.uid; appId = authResponse.appId; - authType = 'apikey'; + authType = AuthUserTypeEnum.apikey; } else if (rootkey) { uid = await parseRootKey(rootkey, userid); - authType = 'root'; + authType = AuthUserTypeEnum.root; } else { return Promise.reject(ERROR_ENUM.unAuthorization); } @@ -229,6 +232,6 @@ export const authShareChat = async ({ shareId }: { shareId: string }) => { user, userId: String(shareChat.userId), appId: String(shareChat.appId), - authType: 'token' as AuthType + authType: AuthUserTypeEnum.token }; }; diff --git a/client/src/types/mongoSchema.d.ts b/client/src/types/mongoSchema.d.ts index 0b9a05e52..0a052b249 100644 --- a/client/src/types/mongoSchema.d.ts +++ b/client/src/types/mongoSchema.d.ts @@ -4,7 +4,7 @@ import type { DataType } from './data'; import { BillSourceEnum, InformTypeEnum } from '@/constants/user'; import { TrainingModeEnum } from '@/constants/plugin'; import type { AppModuleItemType } from './app'; -import { ChatSourceEnum } from '@/constants/chat'; +import { ChatSourceEnum, OutLinkTypeEnum } from '@/constants/chat'; export interface UserModelSchema { _id: string; @@ -144,6 +144,7 @@ export interface OutLinkSchema { name: string; total: number; lastTime: Date; + type: `${OutLinkTypeEnum}`; } export interface kbSchema { diff --git a/client/src/utils/tools.ts b/client/src/utils/tools.ts index c135dbb2d..212e62837 100644 --- a/client/src/utils/tools.ts +++ b/client/src/utils/tools.ts @@ -9,7 +9,7 @@ export const useCopyData = () => { const { toast } = useToast(); return { - copyData: async (data: string, title: string = '复制成功') => { + copyData: async (data: string, title: string = '复制成功', duration = 1000) => { try { if (navigator.clipboard) { await navigator.clipboard.writeText(data); @@ -28,7 +28,7 @@ export const useCopyData = () => { toast({ title, status: 'success', - duration: 1000 + duration }); } };