Commit eed99aae by 赵月辉

feat: 迁移 lufa 定制 - 应用层(projects/)

基于 FastGPT v4.14.30 三向合并:
- API 接口:保留 userKey 传参、协作权限(app/dataset)、app 数量上限检查等 lufa 定制
- 前端组件/页面:按定制类型合并,品牌类保留 lufa,旧版代码采用官方最新
- Dockerfile:保留 xmldom 运行时修复(复制 @xmldom/xmldom 到容器)
- package.json:添加 @xmldom/xmldom 依赖
- 修复 loginByPassword 的 createJWT → createUserSession(适配官方新版 session 机制)
- 保留 shareLogin/postShareLogin 分享登录定制
parent a71e1ce4
LOG_DEPTH=3
# 默认用户密码,用户名为 root,每次重启时会自动更新。
DEFAULT_ROOT_PSW=123456
# 数据库最大连接数
DB_MAX_LINK=5
# token
TOKEN_KEY=dfdasfdas
# 文件阅读时的密钥
FILE_TOKEN_KEY=filetokenkey
# root key, 最高权限
ROOT_KEY=aiagentsflow_root_key
# openai 基本地址,可用作中转。
OPENAI_BASE_URL=https://api-2.xi-ai.cn/v1
# oneapi 地址,可以使用 oneapi 来实现多模型接入
# ONEAPI_URL=https://xxxx.cloud.sealos.io/openai/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 ONEAPI 的 key
CHAT_API_KEY=sk-63O8kJifnoa93w0Z563dB09cA18d46CcBaF515D07a8362Df
# Redis URL
REDIS_URL=redis://default:mypassword@127.0.0.1:6379/0
# mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。
MONGODB_URI=mongodb://root:moming1234@8.217.233.154:27017/htfastgpt?authSource=admin&directConnection=true
# 向量库优先级: pg > milvus
# PG 向量库连接参数
PG_URL=postgresql://root:moming1234@8.217.233.154:5432/postgres
# milvus 向量库连接参数
MILVUS_ADDRESS=https://in03-78bd7f60e6e2a7c.api.gcp-us-west1.zillizcloud.com
MILVUS_TOKEN=133964348b00b4b4e4b51bef680a61350950385c8c64a3ec16b1ab92d3c67dcc4e0370fb9dd15791bcd6dadaf765e98a98735d0d
# code sandbox url
SANDBOX_URL=http://localhost:3003
# 商业版地址
PRO_URL=
# 页面的地址,用于自动补全相对路径资源的 domain
FE_DOMAIN=http://localhost:3000
# 日志等级: debug, info, warn, error
LOG_LEVEL=debug
STORE_LOG_LEVEL=warn
# 工作流最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_RUN_TIMES=500
# Plugin
PLUGIN_BASE_URL=
PLUGIN_TOKEN=
\ No newline at end of file
# 加载的配置文件信息
CONFIG_INFO=.env.local
LOG_DEPTH=3
# 默认用户密码,用户名为 root,每次重启时会自动更新。
DEFAULT_ROOT_PSW=123456
# 数据库最大连接数
DB_MAX_LINK=5
# token
TOKEN_KEY=dfdasfdas
# 文件阅读时的秘钥
FILE_TOKEN_KEY=filetokenkey
# root key, 最高权限
ROOT_KEY=fdafasd
# openai 基本地址,可用作中转。
OPENAI_BASE_URL=https://api.openai.com/v1
# oneapi 地址,可以使用 oneapi 来实现多模型接入
# ONEAPI_URL=https://xxxx.cloud.sealos.io/openai/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 ONEAPI 的 key
CHAT_API_KEY=sk-xxxx
# mongo 数据库连接参数
MONGODB_URI=mongodb://root:moming1234@8.217.233.154:27017/fastgpt2?authSource=admin
# PG 数据库连接参数
PG_URL=postgresql://root:moming1234@8.217.233.154:5432/postgres
# 商业版地址
PRO_URL=
# 首页路径
HOME_URL=/
# Loki Log Path
# LOKI_LOG_URL=
# Plugin
PLUGIN_BASE_URL=
PLUGIN_TOKEN=
\ No newline at end of file
# 加载的配置文件信息
CONFIG_INFO=.env.production.local
LOG_DEPTH=3
# 默认用户密码,用户名为 root,每次重启时会自动更新。
DEFAULT_ROOT_PSW=123456
# 数据库最大连接数
DB_MAX_LINK=5
# token
TOKEN_KEY=dfdasfdas
# 文件阅读时的秘钥
FILE_TOKEN_KEY=filetokenkey
# root key, 最高权限
ROOT_KEY=fdafasd
# 七禾代理地址 基本地址,可用作中转。
OPENAI_BASE_URL=https://api.xi-ai.cn/v1
# oneapi 地址,可以使用 oneapi 来实现多模型接入
# ONEAPI_URL=https://xxxx.cloud.sealos.io/openai/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 ONEAPI 的 key
# 七禾key
ONEAPI_URL=https://api.xi-ai.cn/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 ONEAPI 的 key
# 七禾key
CHAT_API_KEY=sk-hkbUNTAVom56SgmB48187d0c8c5b4f5f8956F0Da88BbFfF5
# mongo 数据库连接参数
MONGODB_URI=mongodb://root:moming1234@mongo:27017/fastgpt
# PG 数据库连接参数
PG_URL=postgresql://root:moming1234@pg:5432/postgres
# 商业版地址
PRO_URL=
# 首页路径
HOME_URL=/
# Loki Log Path
LOKI_LOG_URL=https://admin.aiagentsflow.com/ms/tools/addGptCallLog
REDIS_HOST=47.100.235.116
REDIS_PASSWORD=admin1234
REDIS_PORT=6379
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
FROM node:24.16.0-alpine AS maindeps FROM node:24.16.0-alpine AS maindeps
WORKDIR /app WORKDIR /app
ARG proxy ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
...@@ -88,6 +89,12 @@ COPY --from=builder --chown=nextjs:nodejs /app/projects/app/worker /app/worker ...@@ -88,6 +89,12 @@ COPY --from=builder --chown=nextjs:nodejs /app/projects/app/worker /app/worker
# LiteParse is externalized from the worker bundle and loads this file at runtime. # LiteParse is externalized from the worker bundle and loads this file at runtime.
RUN test -f /app/worker/node_modules/@llamaindex/liteparse-wasm/pkg/liteparse_wasm_bg.wasm RUN test -f /app/worker/node_modules/@llamaindex/liteparse-wasm/pkg/liteparse_wasm_bg.wasm
# copy standload packages
COPY --from=maindeps /app/node_modules/tiktoken ./node_modules/tiktoken
# readFile worker 依赖(.next/server/worker 中 require('@xmldom/xmldom'))
# pnpm 使用 symlink,需从 store 复制实际文件而非链接
COPY --from=maindeps /app/node_modules/.pnpm/@xmldom+xmldom@0.8.10/node_modules/@xmldom ./node_modules/@xmldom
RUN rm -rf ./node_modules/tiktoken/encoders
COPY --from=maindeps /app/node_modules/@zilliz/milvus2-sdk-node ./node_modules/@zilliz/milvus2-sdk-node COPY --from=maindeps /app/node_modules/@zilliz/milvus2-sdk-node ./node_modules/@zilliz/milvus2-sdk-node
# copy package.json to version file # copy package.json to version file
...@@ -98,10 +105,10 @@ COPY --chown=nextjs:nodejs ./projects/app/data/GeoLite2-City.mmdb /app/data/GeoL ...@@ -98,10 +105,10 @@ COPY --chown=nextjs:nodejs ./projects/app/data/GeoLite2-City.mmdb /app/data/GeoL
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000 ENV PORT=3100
ENV NEXT_PUBLIC_BASE_URL=$base_url ENV NEXT_PUBLIC_BASE_URL=$base_url
EXPOSE 3000 EXPOSE 3100
USER nextjs USER nextjs
......
# FastGPT 插件服务配置
## 问题描述
如果你遇到以下错误:
```
Init system error TypeError: Failed to parse URL from /model/list
```
这是因为系统尝试连接插件服务,但 `PLUGIN_BASE_URL` 环境变量没有正确配置。
## 解决方案
### 方案1:设置环境变量(推荐)
`projects/app` 目录下创建 `.env.local` 文件:
```bash
# FastGPT Plugin 服务地址
PLUGIN_BASE_URL=http://localhost:3000
# 如果需要认证令牌
PLUGIN_TOKEN=
```
### 方案2:禁用插件服务
如果你不需要插件服务,可以在 `.env.local` 中设置:
```bash
PLUGIN_BASE_URL=
```
### 方案3:使用Docker Compose
如果你使用Docker部署,在 `docker-compose.yml` 中添加:
```yaml
environment:
- PLUGIN_BASE_URL=http://fastgpt-plugin:3000
- PLUGIN_TOKEN=your_token_here
```
## 插件服务说明
FastGPT插件服务提供:
- 系统工具列表
- 模型配置管理
- 插件热插拔功能
如果你不需要这些功能,可以安全地禁用插件服务。系统会使用数据库中的模型配置。
## 重启应用
修改环境变量后,需要重启应用才能生效。
## 当前修复状态
系统已经修复了插件服务不可用时的错误处理:
- 当插件服务不可用时,系统会优雅地跳过插件功能
- 使用数据库中的模型配置
- 系统工具列表返回空数组
- 应用可以正常启动和运行
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
*/ */
module.exports = { module.exports = {
i18n: { i18n: {
defaultLocale: 'en', defaultLocale: 'zh-CN',
locales: ['en', 'zh-CN', 'zh-Hant'], locales: ['en', 'zh-CN', 'zh-Hant'],
localeDetection: false localeDetection: false
}, },
......
const { i18n } = require('./next-i18next.config.js');
const path = require('path');
const fs = require('fs');
const isDev = process.env.NODE_ENV === 'development';
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_URL,
i18n,
output: 'standalone',
reactStrictMode: isDev ? false : true,
compress: true,
async headers() {
return [
{
source: '/((?!chat/share$).*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Permissions-Policy',
value: 'geolocation=(self), microphone=(self), camera=(self)'
}
]
}
];
},
webpack(config, { isServer, nextRuntime }) {
Object.assign(config.resolve.alias, {
'@mongodb-js/zstd': false,
'@aws-sdk/credential-providers': false,
snappy: false,
aws4: false,
'mongodb-client-encryption': false,
kerberos: false,
'supports-color': false,
'bson-ext': false,
'pg-native': false
});
config.module = {
...config.module,
rules: config.module.rules.concat([
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
]),
exprContextCritical: false,
unknownContextCritical: false
};
if (!config.externals) {
config.externals = [];
}
if (isServer) {
config.externals.push('@node-rs/jieba');
if (nextRuntime === 'nodejs') {
const oldEntry = config.entry;
config = {
...config,
async entry(...args) {
const entries = await oldEntry(...args);
return {
...entries,
...getWorkerConfig()
};
}
};
}
} else {
config.resolve = {
...config.resolve,
fallback: {
...config.resolve.fallback,
fs: false
}
};
}
config.experiments = {
asyncWebAssembly: true,
layers: true
};
return config;
},
// 需要转译的包
transpilePackages: ['@modelcontextprotocol/sdk', 'ahooks'],
experimental: {
// 优化 Server Components 的构建和运行,避免不必要的客户端打包。
serverComponentsExternalPackages: [
'mongoose',
'pg',
'bullmq',
'@zilliz/milvus2-sdk-node',
'tiktoken',
'@xmldom/xmldom'
],
outputFileTracingRoot: path.join(__dirname, '../../'),
instrumentationHook: true
}
};
module.exports = nextConfig;
function getWorkerConfig() {
const result = fs.readdirSync(path.resolve(__dirname, '../../packages/service/worker'));
// 获取所有的目录名
const folderList = result.filter((item) => {
return fs
.statSync(path.resolve(__dirname, '../../packages/service/worker', item))
.isDirectory();
});
const workerConfig = folderList.reduce((acc, item) => {
acc[`worker/${item}`] = path.resolve(
process.cwd(),
`../../packages/service/worker/${item}/index.ts`
);
return acc;
}, {});
return workerConfig;
}
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
"pnpm": "10.x" "pnpm": "10.x"
}, },
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.8.10",
"@chakra-ui/anatomy": "catalog:", "@chakra-ui/anatomy": "catalog:",
"@chakra-ui/icons": "catalog:", "@chakra-ui/icons": "catalog:",
"@chakra-ui/next-js": "catalog:", "@chakra-ui/next-js": "catalog:",
......
### AI AGENTS FLOW
\ No newline at end of file
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5938 10.9C21.5938 9.45229 21.1475 8.03706 20.3113 6.8333C19.4751 5.62954 18.2866 4.69132 16.8961 4.13729C15.5056 3.58326 13.9755 3.4383 12.4993 3.72075C11.0231 4.00319 9.66715 4.70034 8.60288 5.72406C7.53861 6.74777 6.81384 8.05206 6.52021 9.47199C6.22658 10.8919 6.37728 12.3637 6.95326 13.7013C7.52923 15.0388 8.50462 16.182 9.75606 16.9864C11.0075 17.7907 12.4788 18.22 13.9839 18.22V10.9H21.5938Z" fill="url(#paint0_linear_0_5622)"/> <rect width="48" height="48" rx="16" fill="url(#paint0_linear_4433_3040)"/>
<path d="M29.6263 10.9C29.6263 9.93877 29.4498 8.98691 29.1069 8.09882C28.764 7.21072 28.2613 6.40377 27.6277 5.72405C26.9941 5.04433 26.2418 4.50515 25.414 4.13729C24.5861 3.76943 23.6987 3.58009 22.8027 3.58009C21.9066 3.58009 21.0192 3.76943 20.1913 4.13729C19.3635 4.50515 18.6112 5.04434 17.9776 5.72406C17.344 6.40378 16.8413 7.21072 16.4984 8.09882C16.1555 8.98692 15.979 9.93877 15.979 10.9L29.6263 10.9Z" fill="url(#paint1_linear_0_5622)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M23.6129 16.8726L27.2486 23.25H19.9756L23.6129 16.8726ZM24.7503 15L29.4536 23.25H37.2374L32.5341 15H24.7503ZM30.3088 24.75H38.0925L42.7959 33H35.0121L30.3088 24.75ZM25.0233 33H32.8071L28.1038 24.75H20.32L25.0233 33ZM4.5 33H12.2656L22.5316 15H14.7618H14.7615L14.7617 15.0003L4.5 33Z" fill="white"/>
<path d="M28.3745 22.634C28.3745 21.8174 28.2342 21.0087 27.9617 20.2543C27.6892 19.4998 27.2898 18.8143 26.7863 18.2368C26.2828 17.6594 25.685 17.2013 25.0272 16.8888C24.3693 16.5763 23.6642 16.4155 22.9521 16.4155V22.634H28.3745Z" fill="url(#paint2_linear_0_5622)"/>
<path d="M13.9836 20.3912C12.9843 20.3912 11.9947 20.5635 11.0715 20.8985C10.1482 21.2334 9.30926 21.7242 8.60262 22.3431C7.89597 22.9619 7.33543 23.6966 6.95299 24.5051C6.57056 25.3137 6.37372 26.1803 6.37372 27.0554C6.37372 27.9306 6.57056 28.7972 6.95299 29.6057C7.33543 30.4143 7.89597 31.1489 8.60262 31.7678C9.30926 32.3866 10.1482 32.8775 11.0715 33.2124C11.9947 33.5473 12.9843 33.7197 13.9836 33.7197L13.9836 20.3912Z" fill="url(#paint3_linear_0_5622)"/>
<path d="M13.9837 10.6101L13.9837 26.9823L6.3736 26.9823L6.3736 10.6101H13.9837Z" fill="url(#paint4_linear_0_5622)"/>
<path d="M23.0327 10.8988L13.8973 10.8988L13.8973 3.58008L23.0327 3.58008V10.8988Z" fill="url(#paint5_linear_0_5622)"/>
<path d="M23.0327 22.6316H17.9771V16.4155L23.0327 16.4155V22.6316Z" fill="url(#paint6_linear_0_5622)"/>
<defs> <defs>
<linearGradient id="paint0_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_4433_3040" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/> <stop stop-color="#076CFC"/>
<stop offset="1" stop-color="#8EAEFF"/> <stop offset="0.859375" stop-color="#651EDD"/>
</linearGradient>
<linearGradient id="paint1_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint2_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint3_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint4_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint5_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient>
<linearGradient id="paint6_linear_0_5622" x1="18" y1="3.58008" x2="18" y2="33.7197" gradientUnits="userSpaceOnUse">
<stop stop-color="#326DFF"/>
<stop offset="1" stop-color="#8EAEFF"/>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
import React, { useEffect, useMemo } from 'react'; import React, { useEffect } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
import { useLoading } from '@fastgpt/web/hooks/useLoading'; import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getUnreadCount } from '@/web/support/user/inform/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useI18nLng } from '@fastgpt/web/hooks/useI18n'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import Auth from './auth';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import Auth from './auth';
import { useDebounceEffect, useMount } from 'ahooks'; import { useDebounceEffect, useMount } from 'ahooks';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
...@@ -77,7 +77,7 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = { ...@@ -77,7 +77,7 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/skill/detail': true '/skill/detail': true
}; };
export const navbarWidth = '64px'; export const navbarWidth = '0px';
const Layout = ({ children }: { children: JSX.Element }) => { const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter(); const router = useRouter();
...@@ -94,7 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -94,7 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
setShowProModal setShowProModal
} = useSystemStore(); } = useSystemStore();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const { userInfo } = useUserStore();
const { setUserDefaultLng, setShareDefaultLng } = useI18nLng(); const { setUserDefaultLng, setShareDefaultLng } = useI18nLng();
// Auto redeem coupon // Auto redeem coupon
...@@ -104,23 +104,28 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -104,23 +104,28 @@ const Layout = ({ children }: { children: JSX.Element }) => {
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0, () => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
[router.pathname, router.query] [router.pathname, router.query]
); );
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname]; const isHideNavbar = true;
// System hook // System hook
const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, { const unread = 0;
enabled: !!userInfo && !!feConfigs.isPlus,
refetchInterval: 30000
});
const unread = data?.unReadCount || 0;
const importantInforms = data?.importantInforms || [];
const showUpdateNotification = const showUpdateNotification =
isUpdateNotification &&
feConfigs?.bind_notification_method && feConfigs?.bind_notification_method &&
feConfigs?.bind_notification_method.length > 0 && feConfigs?.bind_notification_method.length > 0 &&
!userInfo?.contact && !userInfo?.contact &&
!!userInfo?.team.permission.isOwner; !!userInfo?.team.permission.isOwner;
const [isUpdateNotification, setIsUpdateNotification] = useState(false);
const [importantInforms, setImportantInforms] = useState([]);
// const { data: unreadCount = 0, refetch: refetchUnRead } = useQuery(
// ['getUnreadCount'],
// () => getUnreadCount(),
// {
// enabled: !!userInfo
// }
// );
useMount(() => { useMount(() => {
if (router.pathname === '/chat/share') { if (router.pathname === '/chat/share') {
setShareDefaultLng(); setShareDefaultLng();
...@@ -133,7 +138,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -133,7 +138,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
// Check model invalid // Check model invalid
useDebounceEffect( useDebounceEffect(
() => { () => {
if (userInfo?.username === 'root') { if (userInfo?.username === 'root' || userInfo?.username === 'admin') {
if (llmModelList.length === 0) { if (llmModelList.length === 0) {
toast({ toast({
status: 'warning', status: 'warning',
...@@ -185,7 +190,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -185,7 +190,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
)} )}
{isPc === false && ( {isPc === false && (
<> <>
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? ( {isHideNavbar || phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
<Auth>{children}</Auth> <Auth>{children}</Auth>
) : ( ) : (
<Auth> <Auth>
...@@ -209,9 +214,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -209,9 +214,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{showUpdateNotification && ( {showUpdateNotification && (
<UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" /> <UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" />
)} )}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)}
<ResetExpiredPswModal /> <ResetExpiredPswModal />
<SupportBot /> <SupportBot />
</> </>
......
...@@ -34,6 +34,32 @@ const EChartsCodeBlock = ({ code }: { code: string }) => { ...@@ -34,6 +34,32 @@ const EChartsCodeBlock = ({ code }: { code: string }) => {
}); });
useLayoutEffect(() => { useLayoutEffect(() => {
// 递归处理对象,将字符串形式的函数转换为实际函数
const parseFunctions = (obj: any): any => {
if (typeof obj === 'string' && obj.startsWith('function ')) {
try {
// 使用 Function 构造函数创建函数
return new Function('return ' + obj)();
} catch (error) {
console.warn('Failed to parse function:', obj, error);
return obj;
}
}
if (Array.isArray(obj)) {
return obj.map(parseFunctions);
}
if (obj && typeof obj === 'object') {
const result: any = {};
for (const key in obj) {
result[key] = parseFunctions(obj[key]);
}
return result;
}
return obj;
};
const option = (() => { const option = (() => {
try { try {
const userOption = json5.parse(code.trim()); const userOption = json5.parse(code.trim());
...@@ -52,8 +78,12 @@ const EChartsCodeBlock = ({ code }: { code: string }) => { ...@@ -52,8 +78,12 @@ const EChartsCodeBlock = ({ code }: { code: string }) => {
} }
}; };
return parse; // 解析字符串形式的函数
} catch (error) {} return parseFunctions(parse);
} catch (error) {
console.error('Failed to parse ECharts config:', error);
return null;
}
})(); })();
setOption(option ?? {}); setOption(option ?? {});
...@@ -83,7 +113,7 @@ const EChartsCodeBlock = ({ code }: { code: string }) => { ...@@ -83,7 +113,7 @@ const EChartsCodeBlock = ({ code }: { code: string }) => {
const { screenWidth } = useScreen(); const { screenWidth } = useScreen();
useEffect(() => { useEffect(() => {
findMarkdownDom(); findMarkdownDom();
}, [screenWidth]); }, [screenWidth, findMarkdownDom]);
useEffect(() => { useEffect(() => {
eChart.current?.resize(); eChart.current?.resize();
......
...@@ -3,6 +3,7 @@ import { Input, Button, Box, Textarea, Flex } from '@chakra-ui/react'; ...@@ -3,6 +3,7 @@ import { Input, Button, Box, Textarea, Flex } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/v2/common/MyModal'; import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
...@@ -28,11 +29,11 @@ const EditResourceModal = ({ ...@@ -28,11 +29,11 @@ const EditResourceModal = ({
onEdit: (data: EditResourceInfoFormType) => any; onEdit: (data: EditResourceInfoFormType) => any;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({ const { toast } = useToast();
const { register, watch, getValues, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
defaultValues: defaultForm defaultValues: defaultForm
}); });
const avatar = watch('avatar'); const avatar = watch('avatar');
const { runAsync: onSave, loading } = useRequest( const { runAsync: onSave, loading } = useRequest(
(data: EditResourceInfoFormType) => onEdit(data), (data: EditResourceInfoFormType) => onEdit(data),
{ {
......
import { Box, Text } from '@chakra-ui/react';
import { useEffect, useRef, useState } from 'react';
import type { ReactNode } from 'react';
interface WatermarkProps {
children: ReactNode;
width?: number;
height?: number;
rotate?: number;
zIndex?: number;
image?: string;
content?: string | string[];
font?: WatermarkFontProps;
gap?: [number, number];
offset?: [number, number];
}
interface WatermarkFontProps {
color?: string;
fontSize?: number;
fontWeight?: 'normal' | 'light' | 'bold' | number;
fontFamily?: string;
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique';
textAlign?: 'left' | 'right' | 'center' | 'start' | 'end';
}
const defaultFont: WatermarkFontProps = {
color: 'rgba(0, 0, 0, .15)',
fontSize: 16,
fontWeight: 'normal',
fontFamily: 'sans-serif',
fontStyle: 'normal',
textAlign: 'center'
};
const Watermark = ({
children,
width = 120,
height = 64,
rotate = -22,
zIndex = 99,
image = '',
content = '',
gap = [100, 100],
offset = [gap[0] / 2, gap[1] / 2],
font = {}
}: WatermarkProps) => {
const finalFont = { ...defaultFont, ...font };
const watermarkItems: any = [];
const wrapperRef = useRef<HTMLDivElement>(null);
const [wrapperSize, setWrapperSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!wrapperRef.current) return;
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setWrapperSize({ width, height });
});
observer.observe(wrapperRef.current);
return () => {
observer.disconnect();
};
}, []);
const cols = Math.ceil(wrapperSize.width / (gap[0] + width));
const rows = Math.ceil(wrapperSize.height / (gap[1] + height));
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const columnEven = c % 2 === 0;
const rowEven = r % 2 === 0;
if ((rowEven && columnEven) || (!rowEven && !columnEven)) {
watermarkItems.push(
<Box
key={`${r}-${c}`}
position="absolute"
userSelect="none"
left={`${c * (gap[0] + width)}px`}
top={`${r * (gap[1] + height)}px`}
transform={`rotate(${rotate}deg)`}
color={finalFont.color}
fontSize={finalFont.fontSize}
fontWeight={finalFont.fontWeight}
fontFamily={finalFont.fontFamily}
fontStyle={finalFont.fontStyle}
textAlign={finalFont.textAlign}
>
{content && Array.isArray(content) ? (
content.map((line, i) => <Text key={i}>{line}</Text>)
) : (
<Text>{content}</Text>
)}
</Box>
);
}
}
}
return (
<Box ref={wrapperRef} position="relative" height="100%" overflow="hidden">
{children}
<Box
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
pointerEvents="none"
transform={`translate(${offset[0]}px, ${offset[1]}px)`}
zIndex={zIndex}
>
{watermarkItems}
</Box>
</Box>
);
};
export default Watermark;
import { Box, Flex, Switch, type SwitchProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useTranslation } from 'next-i18next';
// question generator switch
const TCSwitch = (props: SwitchProps) => {
const { t } = useTranslation();
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
<FormLabel color={'myGray.600'}>是否显示工具</FormLabel>
<Box flex={1} />
<Switch {...props} />
</Flex>
);
};
export default TCSwitch;
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
import { Box, Card } from '@chakra-ui/react';
import React from 'react';
import dynamic from 'next/dynamic';
const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false });
const Empty = () => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
return (
<Box py={6} w={'85%'} maxW={'600px'} m={'auto'} alignItems={'center'} justifyContent={'center'}>
{/* version intro */}
<Card p={4} minH={'100px'}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
};
export default React.memo(Empty);
import { Box } from '@chakra-ui/react'; import ComplianceTip from '@/components/common/ComplianceTip/index';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import Markdown from '@/components/Markdown'; import Markdown from '@/components/Markdown';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { Box } from '@chakra-ui/react';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import AIResponseBox from '../../../components/AIResponseBox';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import ComplianceTip from '@/components/common/ComplianceTip/index'; import { useMemo } from 'react';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { useContextSelector } from 'use-context-selector';
import AIResponseBox from '../../../components/AIResponseBox';
import { PluginRunContext } from '../context';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
......
...@@ -651,7 +651,7 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => { ...@@ -651,7 +651,7 @@ const ApiKeyTable = ({ mode = 'account', appId }: ApiKeyTableProps) => {
onClose={() => setApiKey('')} onClose={() => setApiKey('')}
footer={ footer={
<Button variant="whiteBase" onClick={() => setApiKey('')}> <Button variant="whiteBase" onClick={() => setApiKey('')}>
{t('common:OK')} {t('common:common.OK')}
</Button> </Button>
} }
> >
......
...@@ -123,22 +123,16 @@ const CollaboratorContextProvider = ({ ...@@ -123,22 +123,16 @@ const CollaboratorContextProvider = ({
loading: isFetchingCollaborator loading: isFetchingCollaborator
} = useRequest( } = useRequest(
async () => { async () => {
if (feConfigs.isPlus) { const { clbs, parentClbs = [] } = await onGetCollaboratorList();
const { clbs, parentClbs = [] } = await onGetCollaboratorList();
return {
clbs: clbs.map((clb) => ({
...clb,
permission: new Permission({ role: clb.permission.role })
})),
parentClbs: parentClbs.map((clb) => ({
...clb,
permission: new Permission({ role: clb.permission.role })
}))
};
}
return { return {
clbs: [], clbs: clbs.map((clb) => ({
parentClbs: [] ...clb,
permission: new Permission({ role: clb.permission.role })
})),
parentClbs: parentClbs.map((clb) => ({
...clb,
permission: new Permission({ role: clb.permission.role })
}))
}; };
}, },
{ {
......
import React from 'react';
import MyRadio from '@/components/common/MyRadio'; import MyRadio from '@/components/common/MyRadio';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -30,7 +29,9 @@ const PermissionRadio = ({ ...@@ -30,7 +29,9 @@ const PermissionRadio = ({
} }
]} ]}
value={value} value={value}
onChange={(e) => onChange(e as `${PermissionTypeEnum}`)} onChange={(e) => {
onChange(e as `${PermissionTypeEnum}`);
}}
/> />
); );
}; };
......
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
//const TeamManageModal = dynamic(() => import('../TeamManageModal'));
const TeamMenu = () => {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button
variant={'whitePrimary'}
userSelect={'none'}
w={'100%'}
h={'34px'}
justifyContent={'space-between'}
px={3}
css={{
'& span': {
display: 'block'
}
}}
transform={'none !important'}
rightIcon={<MyIcon w={'1rem'} name={'common/select'} />}
onClick={() => {
onOpen();
}}
>
<MyTooltip label={t('common:user.team.Select Team')}>
<Flex w={'100%'} alignItems={'center'}>
{userInfo?.team ? (
<>
<Avatar src={userInfo.team.avatar} w={'1rem'} />
<Box ml={2}>{userInfo.team.teamName}</Box>
</>
) : (
<>
<Box w={'8px'} h={'8px'} mr={3} borderRadius={'50%'} bg={'#67c13b'} />
{t('common:user.team.Personal Team')}
</>
)}
</Flex>
</MyTooltip>
</Button>
</>
);
};
export default TeamMenu;
import type { GetChatTypeEnum } from '@/global/core/chat/constants';
import type { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import type { AdminFbkType } from '@fastgpt/global/core/chat/type';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { RequestPaging } from '@/types';
export type GetChatSpeechProps = OutLinkChatAuthProps & {
appId: string;
ttsConfig: AppTTSConfigType;
input: string;
shareId?: string;
};
/* ---------- chat ----------- */
export type InitChatProps = {
appId?: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
};
export type GetChatRecordsProps = OutLinkChatAuthProps & {
appId: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
type?: `${GetChatTypeEnum}`;
};
export type InitOutLinkChatProps = {
chatId?: string;
shareId: string;
outLinkUid: string;
};
export type InitTeamChatProps = {
teamId: string;
appId: string;
chatId?: string;
teamToken: string;
};
export type InitChatResponse = {
chatId?: string;
appId: string;
userAvatar?: string;
title?: string;
variables?: Record<string, any>;
app: {
chatConfig?: AppChatConfigType;
chatModels?: string[];
name: string;
avatar: string;
intro: string;
canUse?: boolean;
type: `${AppTypeEnum}`;
pluginInputs: FlowNodeInputItemType[];
};
sharePassword?: string;
};
/* ---------- history ----------- */
export type GetHistoriesProps = OutLinkChatAuthProps & {
appId?: string;
source?: `${ChatSourceEnum}`;
startCreateTime?: string;
endCreateTime?: string;
startUpdateTime?: string;
endUpdateTime?: string;
};
export type UpdateHistoryProps = OutLinkChatAuthProps & {
appId: string;
chatId: string;
title?: string;
customTitle?: string;
top?: boolean;
};
export type DelHistoryProps = OutLinkChatAuthProps & {
appId: string;
chatId: string;
};
export type ClearHistoriesProps = OutLinkChatAuthProps & {
appId: string;
};
/* -------- chat item ---------- */
export type DeleteChatItemProps = OutLinkChatAuthProps & {
appId: string;
chatId: string;
contentId?: string;
};
export type AdminUpdateFeedbackParams = AdminFbkType & {
appId: string;
chatId: string;
dataId: string;
};
export type CloseCustomFeedbackParams = {
appId: string;
chatId: string;
dataId: string;
index: number;
};
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import PageContainer from '@/components/PageContainer'; import PageContainer from '@/components/PageContainer';
import SideTabs from '@/components/SideTabs'; import SideTabs from '@/components/SideTabs';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { useTranslation } from 'next-i18next'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import React, { useCallback, useMemo, useRef } from 'react';
export enum TabEnum { export enum TabEnum {
'info' = 'info', 'info' = 'info',
...@@ -161,6 +161,7 @@ const AccountContainer = ({ ...@@ -161,6 +161,7 @@ const AccountContainer = ({
p={4} p={4}
h={'100%'} h={'100%'}
flex={'0 0 200px'} flex={'0 0 200px'}
display="none"
borderRight={theme.borders.base} borderRight={theme.borders.base}
> >
<SideTabs<TabEnum> <SideTabs<TabEnum>
...@@ -180,7 +181,7 @@ const AccountContainer = ({ ...@@ -180,7 +181,7 @@ const AccountContainer = ({
</Flex> </Flex>
</Flex> </Flex>
) : ( ) : (
<Box mb={3}> <Box mb={3} display="none">
<LightRowTabs<TabEnum> <LightRowTabs<TabEnum>
m={'auto'} m={'auto'}
w={'100%'} w={'100%'}
......
...@@ -60,7 +60,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { ...@@ -60,7 +60,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { defaultModels, feConfigs, getModelProviders, getModelProvider } = useSystemStore(); const { defaultModels, feConfigs, getModelProviders, getModelProvider } = useSystemStore();
const isRoot = userInfo?.username === 'root'; const isRoot = userInfo?.username === 'root' || userInfo?.username === 'admin';
const [provider, setProvider] = useState<string | ''>(''); const [provider, setProvider] = useState<string | ''>('');
const providerList = useMemo<{ label: React.ReactNode; value: string | '' }[]>( const providerList = useMemo<{ label: React.ReactNode; value: string | '' }[]>(
......
...@@ -107,7 +107,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode }) ...@@ -107,7 +107,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
defaultData={editTeamData} defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)} onClose={() => setEditTeamData(undefined)}
onSuccess={() => { onSuccess={() => {
refetchTeams(); // refetchTeams();
initUserInfo(); initUserInfo();
}} }}
/> />
......
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { AppContext } from './context';
import { resumeInheritPer } from '@/web/core/app/api'; import { resumeInheritPer } from '@/web/core/app/api';
import { import {
deleteAppCollaborators, deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator'; } from '@/web/core/app/api/collaborator';
import { getCollaboratorList } from '@/web/core/app/api/collaborator';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { import {
Box, Box,
Button, Button,
...@@ -21,7 +21,7 @@ import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; ...@@ -21,7 +21,7 @@ import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { AppSchemaType } from '@fastgpt/global/core/app/type'; import type { AppSchemaType } from '@fastgpt/global/core/app/type';
import { AppRoleList } from '@fastgpt/global/support/permission/app/constant'; import { AppRoleList } from '@fastgpt/global/support/permission/app/constant';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
...@@ -32,6 +32,7 @@ import { useContextSelector } from 'use-context-selector'; ...@@ -32,6 +32,7 @@ import { useContextSelector } from 'use-context-selector';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import { useUploadAvatar } from '@fastgpt/web/common/file/hooks/useUploadAvatar'; import { useUploadAvatar } from '@fastgpt/web/common/file/hooks/useUploadAvatar';
import { getUploadAvatarPresignedUrl } from '@/web/common/file/api'; import { getUploadAvatarPresignedUrl } from '@/web/common/file/api';
import { AppContext } from './context';
const InfoModal = ({ onClose }: { onClose: () => void }) => { const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -53,7 +54,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { ...@@ -53,7 +54,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
defaultValues: appDetail defaultValues: appDetail
}); });
const avatar = watch('avatar'); const avatar = watch('avatar');
// submit config // submit config
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest( const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest(
async (data: AppSchemaType) => { async (data: AppSchemaType) => {
...@@ -120,6 +120,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { ...@@ -120,6 +120,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
title={t('common:core.app.setting')} title={t('common:core.app.setting')}
> >
<ModalBody> <ModalBody>
<Box fontSize={'sm'}>应用ID: {appDetail._id}</Box>
<Box fontSize={'sm'}>{t('common:core.app.Name and avatar')}</Box> <Box fontSize={'sm'}>{t('common:core.app.Name and avatar')}</Box>
<Flex mt={2} alignItems={'center'}> <Flex mt={2} alignItems={'center'}>
<Avatar <Avatar
...@@ -152,7 +153,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { ...@@ -152,7 +153,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
bg={'myWhite.600'} bg={'myWhite.600'}
{...register('intro')} {...register('intro')}
/> />
{/* role */} {/* role */}
{appDetail.permission.hasManagePer && ( {appDetail.permission.hasManagePer && (
<> <>
......
import MyRadio from '@/components/common/MyRadio';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { FlexProps } from '@chakra-ui/react';
import { Box, Flex, Grid, Image, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import type { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
enum UsingWayEnum {
link = 'link',
iframe = 'iframe',
script = 'script'
}
const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose: () => void }) => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { File, onOpen } = useSelectFile({
multiple: false,
fileType: 'image/*'
});
const { feConfigs } = useSystemStore();
const VariableTypeList = [
{
title: <Image src={'/imgs/outlink/link.svg'} alt={''} />,
value: UsingWayEnum.link
},
{
title: <Image src={'/imgs/outlink/iframe.svg'} alt={''} />,
value: UsingWayEnum.iframe
},
{
title: <Image src={'/imgs/outlink/script.svg'} alt={''} />,
value: UsingWayEnum.script
}
];
const [refresh, setRefresh] = useState(false);
const { getValues, setValue, register, watch } = useForm({
defaultValues: {
usingWay: UsingWayEnum.link,
showHistory: true,
scriptIconCanDrag: true,
scriptDefaultOpen: true,
scriptOpenIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTMyNzg1NjY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxMzIiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDMyQzI0Ny4wNCAzMiAzMiAyMjQgMzIgNDY0QTQxMC4yNCA0MTAuMjQgMCAwIDAgMTcyLjQ4IDc2OEwxNjAgOTY1LjEyYTI1LjI4IDI1LjI4IDAgMCAwIDM5LjA0IDIyLjRsMTY4LTExMkE1MjguNjQgNTI4LjY0IDAgMCAwIDUxMiA4OTZjMjY0Ljk2IDAgNDgwLTE5MiA0ODAtNDMyUzc3Ni45NiAzMiA1MTIgMzJ6IG0yNDQuOCA0MTZsLTM2MS42IDMwMS43NmExMi40OCAxMi40OCAwIDAgMS0xOS44NC0xMi40OGw1OS4yLTIzMy45MmgtMTYwYTEyLjQ4IDEyLjQ4IDAgMCAxLTcuMzYtMjMuMzZsMzYxLjYtMzAxLjc2YTEyLjQ4IDEyLjQ4IDAgMCAxIDE5Ljg0IDEyLjQ4bC01OS4yIDIzMy45MmgxNjBhMTIuNDggMTIuNDggMCAwIDEgOCAyMi4wOHoiIGZpbGw9IiM0ZTgzZmQiIHAtaWQ9IjQxMzMiPjwvcGF0aD48L3N2Zz4=',
scriptCloseIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTM1NDQxNTI2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjYzNjciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDEwMjRBNTEyIDUxMiAwIDEgMSA1MTIgMGE1MTIgNTEyIDAgMCAxIDAgMTAyNHpNMzA1Ljk1NjU3MSAzNzAuMzk1NDI5TDQ0Ny40ODggNTEyIDMwNS45NTY1NzEgNjUzLjYwNDU3MWE0NS41NjggNDUuNTY4IDAgMSAwIDY0LjQzODg1OCA2NC40Mzg4NThMNTEyIDU3Ni41MTJsMTQxLjYwNDU3MSAxNDEuNTMxNDI5YTQ1LjU2OCA0NS41NjggMCAwIDAgNjQuNDM4ODU4LTY0LjQzODg1OEw1NzYuNTEyIDUxMmwxNDEuNTMxNDI5LTE0MS42MDQ1NzFhNDUuNTY4IDQ1LjU2OCAwIDEgMC02NC40Mzg4NTgtNjQuNDM4ODU4TDUxMiA0NDcuNDg4IDM3MC4zOTU0MjkgMzA1Ljk1NjU3MWE0NS41NjggNDUuNTY4IDAgMCAwLTY0LjQzODg1OCA2NC40Mzg4NTh6IiBmaWxsPSIjNGU4M2ZkIiBwLWlkPSI2MzY4Ij48L3BhdGg+PC9zdmc+'
}
});
const selectFile = useCallback(
async (files: File[], key: 'scriptOpenIcon' | 'scriptCloseIcon') => {
const file = files[0];
if (!file) return;
// image to base64
const base64 = await fileToBase64(file);
setValue(key, base64);
},
[setValue]
);
watch(() => {
setRefresh(!refresh);
});
const baseUrl = feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}/chat/share?shareId=${share?.shareId}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;
const wayMap = {
[UsingWayEnum.link]: {
blockTitle: t('common:core.app.outLink.Link block title'),
code: linkUrl
},
[UsingWayEnum.iframe]: {
blockTitle: t('common:core.app.outLink.Iframe block title'),
code: `<iframe
src="${linkUrl}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="*"
/>`
},
[UsingWayEnum.script]: {
blockTitle: t('common:core.app.outLink.Script block title'),
code: `<script
src="${baseUrl}/js/iframe.js"
id="chatbot-iframe"
data-bot-src="${linkUrl}"
data-default-open="${getValues('scriptDefaultOpen') ? 'true' : 'false'}"
data-drag="${getValues('scriptIconCanDrag') ? 'true' : 'false'}"
data-open-icon="${getValues('scriptOpenIcon')}"
data-close-icon="${getValues('scriptCloseIcon')}"
defer
></script>
<script>
console.log("Chat box loaded")
</script>`
}
};
const gridItemStyle: FlexProps = {
alignItems: 'center',
bg: 'myWhite.600',
p: 2,
borderRadius: 'md',
border: theme.borders.sm
};
return (
<MyModal
isOpen
isCentered
iconSrc="/imgs/modal/usingWay.svg"
title={t('common:core.app.outLink.Select Using Way')}
onClose={onClose}
maxW={['90vw', '700px']}
>
<ModalBody py={4}>
<MyRadio
gridGap={2}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(3,1fr)']}
value={getValues('usingWay')}
list={VariableTypeList}
hiddenCircle
p={0}
onChange={(e) => {
setValue('usingWay', e);
}}
/>
{/* config */}
<Grid
gridTemplateColumns={['repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={4}
my={5}
fontSize={'sm'}
>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Show History')}</Box>
<Switch {...register('showHistory')} />
</Flex>
{getValues('usingWay') === UsingWayEnum.script && (
<>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Can Drag')}</Box>
<Switch {...register('scriptIconCanDrag')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Default open')}</Box>
<Switch {...register('scriptDefaultOpen')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
<Image
src={getValues('scriptOpenIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptOpenIcon')}
/>
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
<Image
src={getValues('scriptCloseIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptCloseIcon')}
/>
</Flex>
</>
)}
</Grid>
{/* code */}
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
<Flex
p={3}
bg={'myWhite.500'}
border={theme.borders.base}
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
>
<Box flex={1}>{wayMap[getValues('usingWay')].blockTitle}</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => {
copyData(wayMap[getValues('usingWay')].code);
}}
/>
</Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
{wayMap[getValues('usingWay')].code}
</Box>
</Box>
</ModalBody>
<File onSelect={selectFile} />
</MyModal>
);
};
export default SelectUsingWayModal;
...@@ -19,6 +19,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; ...@@ -19,6 +19,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { getOutLinkCounts } from '@/web/support/outLink/api'; import { getOutLinkCounts } from '@/web/support/outLink/api';
const Link = dynamic(() => import('./Link')); const Link = dynamic(() => import('./Link'));
const AuthLink = dynamic(() => import('./AuthLink'));
const API = dynamic(() => import('./API')); const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu')); const FeiShu = dynamic(() => import('./FeiShu'));
const DingTalk = dynamic(() => import('./DingTalk')); const DingTalk = dynamic(() => import('./DingTalk'));
...@@ -323,6 +324,7 @@ const OutLink = () => { ...@@ -323,6 +324,7 @@ const OutLink = () => {
onRefreshOutLinkCounts={refetchOutLinkCounts} onRefreshOutLinkCounts={refetchOutLinkCounts}
/> />
)} )}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />} {linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && ( {linkType === PublishChannelEnum.feishu && (
<FeiShu appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} /> <FeiShu appId={appId} onRefreshOutLinkCounts={refetchOutLinkCounts} />
......
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
ModalBody,
Checkbox,
ModalFooter
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { type AppSchema, type AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pageComponents/app/detail/context';
import { useContextSelector } from 'use-context-selector';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { type SimpleAppSnapshotType } from './useSnapshots';
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
const AppCard = ({
appForm,
setPast
}: {
appForm: AppSimpleEditFormType;
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
async () => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: appForm.chatConfig,
isPublish: false,
versionName: t('app:transition_to_workflow')
});
return postTransition2Workflow({ appId, createNew: transitionCreateNew });
},
{
onSuccess: ({ id }) => {
if (id) {
router.replace({
query: {
appId: id
}
});
} else {
setPast([]);
router.reload();
}
},
successToast: t('common:Success')
}
);
return (
<>
{/* basic info */}
<Box px={[4, 6]} py={4} position={'relative'}>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'} color={'myGray.900'}>
{appDetail.name}
</Box>
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
应用ID: {appDetail._id}
</Box>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
</Box>
<HStack alignItems={'center'}>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() =>
router.push(`/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`)
}
>
{t('common:core.Chat')}
</Button>
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common:Setting')}
</Button>
)}
{appDetail.permission.isOwner && (
<MyMenu
size={'xs'}
Button={
<IconButton
variant={'whitePrimary'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
label: (
<Flex>
<ExportConfigPopover
appName={appDetail.name}
appForm={appForm}
chatConfig={appDetail.chatConfig}
/>
</Flex>
)
},
{
icon: 'core/app/type/workflow',
label: t('app:transition_to_workflow'),
onClick: () => setTransitionCreateNew(true)
},
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
icon: 'core/chat/fileSelect',
label: t('app:team_tags_set'),
onClick: () => setTeamTagsSet(appDetail)
}
]
: [])
]
},
{
children: [
{
icon: 'delete',
type: 'danger',
label: t('common:Delete'),
onClick: onDelApp
}
]
}
]}
/>
)}
<Box flex={1} />
{/* {isPc && ( */}
{/* <MyTag */}
{/* type="borderFill" */}
{/* colorSchema="gray" */}
{/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
{/* > */}
{/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
{/* </MyTag> */}
{/* )} */}
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={t('app:transition_to_workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{t('app:transition_to_workflow_create_new_tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox
isChecked={transitionCreateNew}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Box>{t('app:transition_to_workflow_create_new_placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
{t('common:Close')}
</Button>
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default React.memo(AppCard);
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, HStack, IconButton, useDisclosure } from '@chakra-ui/react'; import { Box, Flex, HStack, IconButton, useDisclosure } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context'; import { AppContext } from '../context';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic'; import MyBox from '@fastgpt/web/components/common/MyBox';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import React, { useCallback, useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { publishStatusStyle } from '../constants'; import { publishStatusStyle } from '../constants';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { WorkflowUtilsContext } from './context/workflowUtilsContext'; import { WorkflowUtilsContext } from './context/workflowUtilsContext';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
...@@ -54,19 +54,6 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved ...@@ -54,19 +54,6 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
<MyIcon name={'edit'} w={'16px'} mr={2} /> <MyIcon name={'edit'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:edit_info')}</Box> <Box fontSize={'sm'}>{t('app:edit_info')}</Box>
</MyBox> </MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'key'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} /> <Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox <MyBox
display={'flex'} display={'flex'}
......
...@@ -28,6 +28,8 @@ import { getTeamToolVersions } from '@/web/core/plugin/team/api'; ...@@ -28,6 +28,8 @@ import { getTeamToolVersions } from '@/web/core/plugin/team/api';
import { storeNode2FlowNode } from '@/web/core/workflow/utils'; import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getWorkflowCheckIssueUIStatus } from '@/web/core/workflow/workflowCheck'; import { getWorkflowCheckIssueUIStatus } from '@/web/core/workflow/workflowCheck';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants'; import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
......
import React, { useState, useCallback } from 'react';
import { Flex, Box, useDisclosure } from '@chakra-ui/react'; import { Flex, Box, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MarkdownExportButton from './MarkdownExportButton'; import MarkdownExportButton from './MarkdownExportButton';
import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import ToolMenu from './ToolMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
...@@ -18,6 +16,8 @@ import { ...@@ -18,6 +16,8 @@ import {
type GetResourceFolderListProps, type GetResourceFolderListProps,
type GetResourceListItemResponse type GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type'; } from '@fastgpt/global/common/parentFolder/type';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { useContextSelector } from 'use-context-selector';
import { getMyApps } from '@/web/core/app/api'; import { getMyApps } from '@/web/core/app/api';
import SelectOneResource from '@/components/common/folder/SelectOneResource'; import SelectOneResource from '@/components/common/folder/SelectOneResource';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
......
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, HStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { type NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import { type PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
const PluginCard = ({
item,
groups
}: {
item: NodeTemplateListItemType;
groups: PluginGroupSchemaType[];
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const type = groups.reduce<string | undefined>((acc, group) => {
const foundType = group.groupTypes.find((type) => type.typeId === item.templateType);
return foundType ? foundType.typeName : acc;
}, undefined);
return (
<MyBox
key={item.id}
lineHeight={1.5}
h="100%"
pt={4}
pb={3}
px={4}
border={'base'}
boxShadow={'2'}
bg={'white'}
borderRadius={'10px'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5'
}}
>
<HStack>
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
{item.name}
</Box>
<Box mr={'-1rem'}>
<Flex
bg={'myGray.100'}
color={'myGray.600'}
py={0.5}
pl={2}
pr={3}
borderLeftRadius={'sm'}
whiteSpace={'nowrap'}
>
<Box ml={1} fontSize={'mini'}>
{t(type as any)}
</Box>
</Flex>
</Box>
</HStack>
<Box
flex={['1 0 48px', '1 0 56px']}
mt={3}
pr={1}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
</Box>
<Flex w={'full'} fontSize={'mini'}>
<Flex flex={1}>
{(item.instructions || item.courseUrl) && (
<UseGuideModal
title={item.name}
iconSrc={item.avatar}
text={item.instructions}
link={item.courseUrl}
>
{({ onClick }) => (
<Flex
color={'primary.700'}
alignItems={'center'}
gap={1}
cursor={'pointer'}
onClick={onClick}
_hover={{ bg: 'myGray.100' }}
>
<MyIcon name={'book'} w={'14px'} />
{t('app:plugin.Instructions')}
</Flex>
)}
</UseGuideModal>
)}
</Flex>
<Box color={'myGray.500'}>{`by ${feConfigs.systemTitle}`}</Box>
</Flex>
</MyBox>
);
};
export default React.memo(PluginCard);
import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useRouter } from 'next/router';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById, getMyApps, putAppById } from '@/web/core/app/api';
import { type AppDetailType, type AppListItemType } from '@fastgpt/global/core/app/type';
import { getAppFolderPath } from '@/web/core/app/api/app';
import {
type GetResourceFolderListProps,
type ParentIdType,
type ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import { type AppUpdateParams } from '@/global/core/app/api';
import dynamic from 'next/dynamic';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
type AppListContextType = {
parentId?: string | null;
appType: AppTypeEnum | 'all';
myApps: AppListItemType[];
loadMyApps: () => Promise<AppListItemType[]>;
isFetchingApps: boolean;
folderDetail: AppDetailType | undefined | null;
paths: ParentTreePathItemType[];
onUpdateApp: (id: string, data: AppUpdateParams) => Promise<any>;
setMoveAppId: React.Dispatch<React.SetStateAction<string | undefined>>;
refetchFolderDetail: () => Promise<AppDetailType | null>;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
};
export const AppListContext = createContext<AppListContextType>({
parentId: undefined,
myApps: [],
loadMyApps: async function (): Promise<AppListItemType[]> {
throw new Error('Function not implemented.');
},
isFetchingApps: false,
folderDetail: undefined,
paths: [],
onUpdateApp: function (id: string, data: AppUpdateParams): Promise<any> {
throw new Error('Function not implemented.');
},
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
appType: 'all',
refetchFolderDetail: async function (): Promise<AppDetailType | null> {
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
}
});
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const router = useRouter();
const { parentId = null, type = 'all' } = router.query as {
parentId?: string | null;
type: AppTypeEnum;
};
const [searchKey, setSearchKey] = useState('');
const {
data = [],
runAsync: loadMyApps,
loading: isFetchingApps
} = useRequest2(
() => {
const formatType = (() => {
if (!type || type === 'all') return undefined;
if (type === AppTypeEnum.plugin)
return [AppTypeEnum.folder, AppTypeEnum.plugin, AppTypeEnum.httpPlugin];
return [AppTypeEnum.folder, type];
})();
return getMyApps({ parentId, type: formatType, searchKey });
},
{
manual: false,
refreshDeps: [searchKey, parentId, type],
throttleWait: 500,
refreshOnWindowFocus: true
}
);
const { data: paths = [], runAsync: refetchPaths } = useRequest2(
() => getAppFolderPath({ sourceId: parentId, type: 'current' }),
{
manual: false,
refreshDeps: [parentId]
}
);
const { data: folderDetail, runAsync: refetchFolderDetail } = useRequest2(
() => {
if (parentId) return getAppDetailById(parentId);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [parentId]
}
);
const { runAsync: onUpdateApp } = useRequest2((id: string, data: AppUpdateParams) =>
putAppById(id, data).then(async (res) => {
await Promise.all([refetchFolderDetail(), refetchPaths(), loadMyApps()]);
return res;
})
);
const [moveAppId, setMoveAppId] = useState<string>();
const onMoveApp = useCallback(
async (parentId: ParentIdType) => {
if (!moveAppId) return;
await onUpdateApp(moveAppId, { parentId });
},
[moveAppId, onUpdateApp]
);
const getAppFolderList = useCallback(({ parentId }: GetResourceFolderListProps) => {
return getMyApps({
parentId,
type: AppTypeEnum.folder
}).then((res) =>
res
.filter((item) => item.permission.hasWritePer)
.map((item) => ({
id: item._id,
name: item.name
}))
);
}, []);
const { setLastAppListRouteType } = useSystemStore();
useEffect(() => {
setLastAppListRouteType(type);
}, [setLastAppListRouteType, type]);
// Clear search key when parentId changes
useEffect(() => {
setSearchKey('');
}, [parentId]);
const contextValue: AppListContextType = {
parentId,
appType: type,
myApps: data,
loadMyApps,
refetchFolderDetail,
isFetchingApps,
folderDetail,
paths,
onUpdateApp,
setMoveAppId,
searchKey,
setSearchKey
};
return (
<AppListContext.Provider value={contextValue}>
{children}
{!!moveAppId && (
<MoveModal
moveResourceId={moveAppId}
server={getAppFolderList}
title={t('app:move_app')}
onClose={() => setMoveAppId(undefined)}
onConfirm={onMoveApp}
moveHint={t('app:move.hint')}
/>
)}
</AppListContext.Provider>
);
};
export default AppListContextProvider;
...@@ -176,7 +176,7 @@ const DataCard = () => { ...@@ -176,7 +176,7 @@ const DataCard = () => {
/> />
)} )}
</Box> </Box>
{feConfigs?.isPlus && !!collection?.tags?.length && ( {!feConfigs?.isPlus && !!collection?.tags?.length && (
<TagsPopOver currentCollection={collection} /> <TagsPopOver currentCollection={collection} />
)} )}
</Box> </Box>
......
...@@ -64,6 +64,10 @@ const Info = ({ datasetId }: { datasetId: string }) => { ...@@ -64,6 +64,10 @@ const Info = ({ datasetId }: { datasetId: string }) => {
useConfirm({ useConfirm({
title: t('common:action_confirm') title: t('common:action_confirm')
}); });
const { ConfirmModal: ConfirmDelModal } = useConfirm({
content: t('dataset:confirm_del_dataset_tip'),
type: 'delete'
});
const { runAsync: onSave } = useRequest( const { runAsync: onSave } = useRequest(
(data: DatasetItemType) => { (data: DatasetItemType) => {
...@@ -440,6 +444,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { ...@@ -440,6 +444,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
</> </>
)} )}
<ConfirmDelModal />
<ConfirmRebuildModal countDown={10} /> <ConfirmRebuildModal countDown={10} />
<ConfirmSyncScheduleModal /> <ConfirmSyncScheduleModal />
{editedDataset && ( {editedDataset && (
......
...@@ -24,15 +24,16 @@ import { ...@@ -24,15 +24,16 @@ import {
getCollaboratorList, getCollaboratorList,
postUpdateDatasetCollaborators postUpdateDatasetCollaborators
} from '@/web/core/dataset/api/collaborator'; } from '@/web/core/dataset/api/collaborator';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next'; import UserBox from '@fastgpt/web/components/common/UserBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useTranslation } from 'next-i18next';
import SideTag from './SideTag'; import SideTag from './SideTag';
import UserBox from '@fastgpt/web/components/common/UserBox';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList'; import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
......
import I18nLngSelector from '@/components/Select/I18nLngSelector';
import type { ResLogin } from '@/global/support/api/userRes';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { postShareLogin } from '@/web/support/user/api';
import { Box, Button, Flex, FormControl, Image, Input } from '@chakra-ui/react';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
interface Props {
shareId: string;
loginSuccess: (e: ResLogin) => void;
}
interface ShareFormType {
password: string;
}
const ShareForm = ({ shareId, loginSuccess }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { isPc } = useSystem();
const {
register,
handleSubmit,
formState: { errors }
} = useForm<ShareFormType>();
const [requesting, setRequesting] = useState(false);
const onclickLogin = useCallback(
async ({ password }: ShareFormType) => {
setRequesting(true);
try {
loginSuccess(
await postShareLogin({
shareId,
password
})
);
toast({
title: t('login:login_success'),
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || t('login:login_failed'),
status: 'error'
});
}
setRequesting(false);
},
[loginSuccess, t, toast, shareId]
);
const isCommunityVersion = !!(feConfigs?.register_method && !feConfigs?.isPlus);
const placeholder = (() => {
return [t('common:support.user.login.Username')]
.concat(
feConfigs?.login_method?.map((item) => {
switch (item) {
case 'email':
return t('common:support.user.login.Email');
case 'phone':
return t('common:support.user.login.Phone number');
}
}) ?? []
)
.join('/');
})();
return (
<Flex flexDirection={'column'} h={'100%'}>
<Flex alignItems={'center'} justify={'space-between'}>
<Flex>
<Flex
w={['48px', '56px']}
h={['48px', '56px']}
bg={'myGray.25'}
borderRadius={'xl'}
borderWidth={'1.5px'}
borderColor={'borderColor.base'}
alignItems={'center'}
justifyContent={'center'}
>
<Image src={LOGO_ICON} w={['24px', '28px']} alt={'icon'} />
</Flex>
<Box ml={3} fontSize={['2xl', '3xl']} fontWeight={'bold'}>
Ai Agents Flow
</Box>
</Flex>
{!isPc && <I18nLngSelector />}
</Flex>
<Box
mt={'42px'}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickLogin)();
}
}}
>
<FormControl mt={6} isInvalid={!!errors.password}>
<Input
bg={'myGray.50'}
type={'password'}
placeholder={
isCommunityVersion
? t('login:root_password_placeholder')
: t('common:support.user.login.Password')
}
{...register('password', {
required: true,
maxLength: {
value: 60,
message: t('login:password_condition')
}
})}
></Input>
</FormControl>
<Button
type="submit"
my={6}
w={'100%'}
size={['md', 'md']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickLogin)}
>
{t('login:Login')}
</Button>
</Box>
</Flex>
);
};
export default ShareForm;
...@@ -827,20 +827,6 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => { ...@@ -827,20 +827,6 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
return ( return (
<Box> <Box>
<Grid gridGap={4}> <Grid gridGap={4}>
{feConfigs?.docUrl && (
<Link
href={getDocPath('/guide/getting-started')}
target="_blank"
textDecoration={'none !important'}
{...ButtonStyles}
>
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{t('account_info:help_document')}
</Box>
</Link>
)}
{!isPc && {!isPc &&
feConfigs?.navbarItems feConfigs?.navbarItems
?.filter((item) => item.isActive) ?.filter((item) => item.isActive)
......
...@@ -70,7 +70,7 @@ export default ModelProvider; ...@@ -70,7 +70,7 @@ export default ModelProvider;
const ValidModelTable = ({ Tab }: { Tab: React.ReactNode }) => { const ValidModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root'; const isRoot = userInfo?.username === 'root' || userInfo?.username === 'admin';
return ( return (
<> <>
{isRoot && <Flex justifyContent={'space-between'}>{Tab}</Flex>} {isRoot && <Flex justifyContent={'space-between'}>{Tab}</Flex>}
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller';
import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import { NextAPI } from '@/service/middleware/entry';
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { addLog } from '@fastgpt/service/common/system/log';
import { authFrequencyLimit } from '@/service/common/frequencyLimit/api';
import { addSeconds } from 'date-fns';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type UploadChatFileProps = {
appId: string;
} & OutLinkChatAuthProps;
export type UploadDatasetFileProps = {
datasetId: string;
};
const authUploadLimit = (tmbId: string) => {
if (!global.feConfigs.uploadFileMaxAmount) return;
return authFrequencyLimit({
eventId: `${tmbId}-uploadfile`,
maxAmount: global.feConfigs.uploadFileMaxAmount * 2,
expiredTime: addSeconds(new Date(), 30) // 30s
});
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const filePaths: string[] = [];
try {
const start = Date.now();
/* Creates the multer uploader */
const upload = getUploadModel({
maxSize: global.feConfigs?.uploadFileMaxSize
});
const { file, bucketName, metadata, data } = await upload.getUploadFile<
UploadChatFileProps | UploadDatasetFileProps
>(req, res);
filePaths.push(file.path);
const { teamId, uid } = await (async () => {
if (bucketName === 'chat') {
const chatData = data as UploadChatFileProps;
const authData = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...chatData
});
return {
teamId: authData.teamId,
uid: authData.uid
};
}
if (bucketName === 'dataset') {
const chatData = data as UploadDatasetFileProps;
const authData = await authDataset({
datasetId: chatData.datasetId,
per: WritePermissionVal,
req,
authToken: true,
authApiKey: true
});
return {
teamId: authData.teamId,
uid: authData.tmbId
};
}
return Promise.reject('bucketName is empty');
})();
//await authUploadLimit(uid);
addLog.info(`Upload file success ${file.originalname}, cost ${Date.now() - start}ms`);
if (!bucketName) {
throw new Error('bucketName is empty');
}
const fileId = await uploadFile({
teamId,
uid,
bucketName,
path: file.path,
filename: file.originalname,
contentType: file.mimetype,
metadata: metadata
});
jsonRes(res, {
data: {
fileId,
previewUrl: `${ReadFileBaseUrl}/${file.originalname}?token=${await createFileToken({
bucketName,
teamId,
uid,
fileId
})}`
}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
removeFilesByPaths(filePaths);
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: false
}
};
...@@ -10,6 +10,7 @@ import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/au ...@@ -10,6 +10,7 @@ import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/au
import { authOutLinkInit } from '@fastgpt/service/support/outLink/runtime/auth'; import { authOutLinkInit } from '@fastgpt/service/support/outLink/runtime/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model'; import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { import {
CreateQuestionGuideBodySchema, CreateQuestionGuideBodySchema,
CreateQuestionGuideResponseSchema, CreateQuestionGuideResponseSchema,
...@@ -31,12 +32,17 @@ async function handler( ...@@ -31,12 +32,17 @@ async function handler(
authApiKey: true authApiKey: true
}); });
// 获取团队的openaiAccount信息
const team = await MongoTeam.findById(teamId, 'openaiAccount').lean();
const userKey = team?.openaiAccount;
const qgModel = getDefaultLLMModel(); const qgModel = getDefaultLLMModel();
const { result, inputTokens, outputTokens } = await createQuestionGuide({ const { result, inputTokens, outputTokens } = await createQuestionGuide({
messages: messages as ChatCompletionMessageParam[], messages: messages as ChatCompletionMessageParam[],
model: qgModel.model, model: qgModel.model,
teamId teamId,
userKey
}); });
pushQuestionGuideUsage({ pushQuestionGuideUsage({
......
...@@ -8,6 +8,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller'; ...@@ -8,6 +8,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model'; import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { import {
CreateQuestionGuideResponseSchema, CreateQuestionGuideResponseSchema,
CreateQuestionGuideV2BodySchema, CreateQuestionGuideV2BodySchema,
...@@ -71,13 +72,18 @@ async function handler( ...@@ -71,13 +72,18 @@ async function handler(
}); });
const messages = chats2GPTMessages({ messages: histories, reserveId: false }); const messages = chats2GPTMessages({ messages: histories, reserveId: false });
// 获取团队的openaiAccount信息
const team = await MongoTeam.findById(teamId, 'openaiAccount').lean();
const userKey = team?.openaiAccount;
const qgModel = questionGuide?.model || getDefaultLLMModel().model; const qgModel = questionGuide?.model || getDefaultLLMModel().model;
const { result, inputTokens, outputTokens } = await createQuestionGuide({ const { result, inputTokens, outputTokens } = await createQuestionGuide({
messages, messages,
model: qgModel, model: qgModel,
customPrompt: questionGuide?.customPrompt, customPrompt: questionGuide?.customPrompt,
teamId teamId,
userKey
}); });
pushQuestionGuideUsage({ pushQuestionGuideUsage({
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { type ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken';
export type tokenQuery = {};
export type tokenBody = {
messages: ChatCompletionMessageParam[];
};
export type tokenResponse = {};
async function handler(
req: ApiRequestProps<tokenBody, tokenQuery>,
res: ApiResponseType<any>
): Promise<tokenResponse> {
const start = Date.now();
await authCert({ req, authRoot: true });
const tokens = await countGptMessagesTokens(req.body.messages);
return {
tokens,
time: Date.now() - start,
memory: process.memoryUsage()
};
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: {
sizeLimit: '200mb'
},
responseLimit: '200mb'
}
};
...@@ -70,10 +70,10 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) { ...@@ -70,10 +70,10 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) {
}); });
// 上限校验 // 上限校验
await checkTeamAppTypeLimit({ //await checkTeamAppTypeLimit({
teamId, // teamId,
appCheckType: type === AppTypeEnum.workflowTool ? 'tool' : 'app' // appCheckType: type === AppTypeEnum.workflowTool ? 'tool' : 'app'
}); //});
const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId') const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId')
.populate<{ .populate<{
......
...@@ -11,6 +11,9 @@ import { ...@@ -11,6 +11,9 @@ import {
GetAppDetailResponseSchema, GetAppDetailResponseSchema,
type GetAppDetailResponseType type GetAppDetailResponseType
} from '@fastgpt/global/openapi/core/app/common/api'; } from '@fastgpt/global/openapi/core/app/common/api';
import { getUserAppCollaboratorPermission } from '@fastgpt/service/support/permission/app/getAppPermissions';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { sumPer } from '@fastgpt/global/support/permission/utils';
/* 获取应用详情 */ /* 获取应用详情 */
async function handler(req: NextApiRequest): Promise<GetAppDetailResponseType> { async function handler(req: NextApiRequest): Promise<GetAppDetailResponseType> {
...@@ -23,30 +26,59 @@ async function handler(req: NextApiRequest): Promise<GetAppDetailResponseType> { ...@@ -23,30 +26,59 @@ async function handler(req: NextApiRequest): Promise<GetAppDetailResponseType> {
Promise.reject(CommonErrEnum.missingParams); Promise.reject(CommonErrEnum.missingParams);
} }
// 凭证校验 // 凭证校验
const { app, teamId, isRoot } = await authApp({ const {
app,
teamId,
isRoot,
tmbId,
permission: basePermission
} = await authApp({
req, req,
authToken: true, authToken: true,
authRoot: true,
appId, appId,
per: ReadPermissionVal per: ReadPermissionVal
}); });
// 获取用户的直接协作权限
const collaboratorPermission = await getUserAppCollaboratorPermission({
appId,
tmbId
});
// 合并基础权限和协作权限,取更高的权限
const finalPermissionValue = collaboratorPermission
? sumPer(basePermission.role, collaboratorPermission)
: basePermission.role;
const permission = new AppPermission({
role: finalPermissionValue,
isOwner: String(app.tmbId) === String(tmbId) || basePermission.isOwner
});
// 更新app的permission
const updatedApp = {
...app,
permission
};
await rewriteAppWorkflowToDetail({ await rewriteAppWorkflowToDetail({
nodes: app.modules, nodes: updatedApp.modules,
teamId, teamId,
ownerTmbId: app.tmbId, ownerTmbId: updatedApp.tmbId,
isRoot, isRoot,
lang: getLocale(req) lang: getLocale(req)
}); });
if (!app.permission.hasWritePer) { if (!permission.hasWritePer) {
return GetAppDetailResponseSchema.parse({ return GetAppDetailResponseSchema.parse({
...app, ...updatedApp,
modules: [], modules: [],
edges: [] edges: []
}); });
} }
return GetAppDetailResponseSchema.parse(app); return GetAppDetailResponseSchema.parse(updatedApp);
} }
export default NextAPI(handler); export default NextAPI(handler);
...@@ -24,6 +24,9 @@ import { ...@@ -24,6 +24,9 @@ import {
type ListAppBodyType, type ListAppBodyType,
type ListAppResponseType type ListAppResponseType
} from '@fastgpt/global/openapi/core/app/common/api'; } from '@fastgpt/global/openapi/core/app/common/api';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { getAppPermissions } from '@fastgpt/service/support/permission/app/getAppPermissions';
import { AppDefaultRoleVal } from '@fastgpt/global/support/permission/app/constant';
/* /*
获取 APP 列表权限 获取 APP 列表权限
...@@ -100,6 +103,9 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe ...@@ -100,6 +103,9 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe
myGroupMap.has(String(item.groupId)) || myGroupMap.has(String(item.groupId)) ||
myOrgSet.has(String(item.orgId)) myOrgSet.has(String(item.orgId))
); );
// 检查用户是否为admin角色
const teamMember = await MongoTeamMember.findById(tmbId).lean();
const isAdmin = !!(teamMember && teamMember.role === 'admin');
const findAppsQuery = (() => { const findAppsQuery = (() => {
// Filter apps by permission, if not owner, only get apps that I have permission to access // Filter apps by permission, if not owner, only get apps that I have permission to access
...@@ -137,8 +143,8 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe ...@@ -137,8 +143,8 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe
...appPerQuery, ...appPerQuery,
teamId, teamId,
...searchMatch, ...searchMatch,
type: _type type: _type,
}; ...parseParentIdInMongo(parentId)
// @ts-ignore // @ts-ignore
delete data.parentId; delete data.parentId;
...@@ -156,7 +162,8 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe ...@@ -156,7 +162,8 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe
if (searchKey) return 50; if (searchKey) return 50;
return; return;
})(); })();
console.log('parentId222:', parentId);
console.log('findAppsQuery:', findAppsQuery);
const myApps = await MongoApp.find( const myApps = await MongoApp.find(
{ ...findAppsQuery, deleteTime: null }, { ...findAppsQuery, deleteTime: null },
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission modules', '_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission modules',
...@@ -168,6 +175,15 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe ...@@ -168,6 +175,15 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe
updateTime: -1 updateTime: -1
}) })
.lean(); .lean();
// 获取app权限信息
const { userCollaboratorPermissions, publicAppIds } = await getAppPermissions({
appIds: (myApps || []).map((item) => String(item._id)),
tmbId
});
// 处理角色信息
console.log('userCollaboratorPermissions:', userCollaboratorPermissions);
console.log('publicAppIds:', Array.from(publicAppIds));
// Add app permission and filter apps by read permission // Add app permission and filter apps by read permission
const formatApps = myApps const formatApps = myApps
...@@ -184,27 +200,31 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe ...@@ -184,27 +200,31 @@ async function handler(req: ApiRequestProps<ListAppBodyType>): Promise<ListAppRe
) )
.map((item) => item.permission) .map((item) => item.permission)
); );
// 获取当前登录用户对该app的协作权限
const rolePer = userCollaboratorPermissions.find(
(item) => item.appId === appId
)?.permission;
return new AppPermission({ return new AppPermission({
role: tmbRole ?? groupAndOrgRole, role: tmbRole ?? groupAndOrgRole ?? rolePer ?? AppDefaultRoleVal,
isOwner: String(app.tmbId) === String(tmbId) || teamPer.isOwner isOwner: String(app.tmbId) === String(tmbId) || teamPer.isOwner || isAdmin
}); });
}; };
// 不再继承父文件夹权限,直接使用应用自己的权限
// Inherit app, check parent folder clb and it's own clb // Inherit app, check parent folder clb and it's own clb
if (!AppFolderTypeList.includes(app.type) && app.parentId && app.inheritPermission) { // if (!AppFolderTypeList.includes(app.type) && app.parentId && app.inheritPermission) {
const resourceClbs = roleListMap.get(String(app._id)) ?? []; // const resourceClbs = roleListMap.get(String(app._id)) ?? [];
const parentClbs = roleListMap.get(String(app.parentId)) ?? []; // const parentClbs = roleListMap.get(String(app.parentId)) ?? [];
// return {
return { // Per: getPer(String(app.parentId)).addRole(getPer(String(app._id)).role),
Per: getPer(String(app.parentId)).addRole(getPer(String(app._id)).role), // privateApp: isPrivateResourceByCollaborators({
privateApp: isPrivateResourceByCollaborators({ // resourceClbs,
resourceClbs, // parentClbs,
parentClbs, // inheritPermission: true
inheritPermission: true // })
}) // };
}; // }
}
const resourceClbs = roleListMap.get(String(app._id)) ?? []; const resourceClbs = roleListMap.get(String(app._id)) ?? [];
......
...@@ -370,6 +370,6 @@ export const config = { ...@@ -370,6 +370,6 @@ export const config = {
bodyParser: { bodyParser: {
sizeLimit: '10mb' sizeLimit: '10mb'
}, },
responseLimit: '20mb' responseLimit: '200mb'
} }
}; };
...@@ -8,10 +8,9 @@ import { ...@@ -8,10 +8,9 @@ import {
type InitChatResponseType type InitChatResponseType
} from '@fastgpt/global/openapi/core/chat/controler/api'; } from '@fastgpt/global/openapi/core/chat/controler/api';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils'; import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils';
import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema'; import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema';
...@@ -24,6 +23,7 @@ import { authSkill } from '@fastgpt/service/support/permission/skill/auth'; ...@@ -24,6 +23,7 @@ import { authSkill } from '@fastgpt/service/support/permission/skill/auth';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { buildChatTargetResponse } from '@fastgpt/global/openapi/core/chat/api'; import { buildChatTargetResponse } from '@fastgpt/global/openapi/core/chat/api';
import { createChatAgentHelperAppConfig } from '@fastgpt/global/core/ai/auxiliaryGeneration/chatAgentHelper'; import { createChatAgentHelperAppConfig } from '@fastgpt/global/core/ai/auxiliaryGeneration/chatAgentHelper';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(req: NextApiRequest): Promise<InitChatResponseType> { async function handler(req: NextApiRequest): Promise<InitChatResponseType> {
const { sourceType, sourceId, chatId } = parseApiInput({ const { sourceType, sourceId, chatId } = parseApiInput({
......
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type { GetChatSpeechProps } from '@/global/core/chat/api.d';
import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechUsage } from '@/service/support/wallet/usage/push';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
import { getTTSModel } from '@fastgpt/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
/*
1. get tts from chatItem store
2. get tts from ai
4. push bill
*/
async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiResponse) {
try {
const { ttsConfig, input } = req.body;
if (!ttsConfig.model || !ttsConfig.voice) {
throw new Error('model or voice not found');
}
const { teamId, tmbId, authType } = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body
});
// 获取团队的openaiAccount信息
const team = await MongoTeam.findById(teamId, 'openaiAccount').lean();
const userKey = team?.openaiAccount;
const ttsModel = getTTSModel(ttsConfig.model);
const voiceData = ttsModel.voices?.find((item) => item.value === ttsConfig.voice);
if (!voiceData) {
throw new Error('voice not found');
}
const bufferId = `${ttsModel.model}-${ttsConfig.voice}`;
/* get audio from buffer */
const ttsBuffer = await MongoTTSBuffer.findOne(
{
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed })
},
'buffer'
);
if (ttsBuffer?.buffer) {
return res.end(new Uint8Array(ttsBuffer.buffer.buffer));
}
/* request audio */
await text2Speech({
res,
input,
model: ttsConfig.model,
voice: ttsConfig.voice,
speed: ttsConfig.speed,
userKey,
onSuccess: async ({ model, buffer }) => {
try {
/* bill */
pushAudioSpeechUsage({
model: model,
charsLength: input.length,
tmbId,
teamId,
source: authType2UsageSource({ authType })
});
/* create buffer */
await MongoTTSBuffer.create(
{
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed }),
buffer
},
ttsModel.requestUrl && ttsModel.requestAuth
? {
path: ttsModel.requestUrl,
headers: {
Authorization: `Bearer ${ttsModel.requestAuth}`
}
}
: {}
);
} catch (error) {}
},
onError: (err) => {
jsonRes(res, {
code: 500,
error: err
});
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// 不能使用 NextApiResponse
export default handler;
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { authOutLink } from '@/service/support/permission/auth/outLink'; import { authOutLink } from '@/service/support/permission/auth/outLink';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils'; import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
...@@ -19,6 +16,9 @@ import { ChatGenerateStatusEnum, ChatSourceTypeEnum } from '@fastgpt/global/core ...@@ -19,6 +16,9 @@ import { ChatGenerateStatusEnum, ChatSourceTypeEnum } from '@fastgpt/global/core
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { buildChatSourceQuery } from '@fastgpt/service/core/chat/source'; import { buildChatSourceQuery } from '@fastgpt/service/core/chat/source';
import { buildChatTargetResponse } from '@fastgpt/global/openapi/core/chat/api'; import { buildChatTargetResponse } from '@fastgpt/global/openapi/core/chat/api';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
async function handler(req: NextApiRequest): Promise<InitOutLinkChatResponseType> { async function handler(req: NextApiRequest): Promise<InitOutLinkChatResponseType> {
const { chatId, outLinkAuthData } = parseApiInput({ const { chatId, outLinkAuthData } = parseApiInput({
...@@ -28,7 +28,7 @@ async function handler(req: NextApiRequest): Promise<InitOutLinkChatResponseType ...@@ -28,7 +28,7 @@ async function handler(req: NextApiRequest): Promise<InitOutLinkChatResponseType
const { shareId, outLinkUid } = outLinkAuthData; const { shareId, outLinkUid } = outLinkAuthData;
// auth link permission // auth link permission
const { uid, appId } = await authOutLink({ shareId, outLinkUid }); const { uid, appId, outLinkConfig } = await authOutLink({ shareId, outLinkUid });
// auth app permission // auth app permission
const [chat, app] = await Promise.all([ const [chat, app] = await Promise.all([
......
import { NextAPI } from '@/service/middleware/entry';
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
import {
type DatasetCiteItemType,
type DatasetDataSchemaType
} from '@fastgpt/global/core/dataset/type';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import {
type LinkedListResponse,
type LinkedPaginationProps
} from '@fastgpt/web/common/fetch/type';
import { type FilterQuery, Types } from 'mongoose';
import { quoteDataFieldSelector } from '@/service/core/chat/constants';
import { processChatTimeFilter } from '@/service/core/chat/utils';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
import { getFormatDatasetCiteList } from '@fastgpt/service/core/dataset/data/controller';
export type GetCollectionQuoteProps = LinkedPaginationProps & {
chatId: string;
chatItemDataId: string;
collectionId: string;
appId: string;
shareId?: string;
outLinkUid?: string;
teamId?: string;
teamToken?: string;
};
export type GetCollectionQuoteRes = LinkedListResponse<DatasetCiteItemType>;
type BaseMatchType = FilterQuery<DatasetDataSchemaType>;
async function handler(
req: ApiRequestProps<GetCollectionQuoteProps>
): Promise<GetCollectionQuoteRes> {
const {
initialId,
initialIndex,
prevId,
prevIndex,
nextId,
nextIndex,
collectionId,
chatItemDataId,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken,
pageSize = 15
} = req.body;
const limitedPageSize = Math.min(pageSize, 30);
const [collection, { chat, showRawSource }, { chatItem }] = await Promise.all([
getCollectionWithDataset(collectionId),
authChatCrud({
req,
authToken: true,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken
}),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]);
if (!showRawSource) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
if (!chat) return Promise.reject(ChatErrEnum.unAuthChat);
const baseMatch: BaseMatchType = {
teamId: collection.teamId,
datasetId: collection.datasetId,
collectionId,
$or: [
{ updateTime: { $lt: new Date(chatItem.time) } },
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatItem.time) } } } }
]
};
if (initialId && initialIndex !== undefined) {
return await handleInitialLoad({
initialId,
initialIndex,
pageSize: limitedPageSize,
chatTime: chatItem.time,
baseMatch
});
}
if ((prevId && prevIndex !== undefined) || (nextId && nextIndex !== undefined)) {
return await handlePaginatedLoad({
prevId,
prevIndex,
nextId,
nextIndex,
pageSize: limitedPageSize,
chatTime: chatItem.time,
baseMatch
});
}
return { list: [], hasMorePrev: false, hasMoreNext: false };
}
export default NextAPI(handler);
async function handleInitialLoad({
initialId,
initialIndex,
pageSize,
chatTime,
baseMatch
}: {
initialId: string;
initialIndex: number;
pageSize: number;
chatTime: Date;
baseMatch: BaseMatchType;
}): Promise<GetCollectionQuoteRes> {
const centerNode = await MongoDatasetData.findOne(
{
_id: new Types.ObjectId(initialId)
},
quoteDataFieldSelector
).lean();
if (!centerNode) {
const list = await MongoDatasetData.find(baseMatch, quoteDataFieldSelector)
.sort({ chunkIndex: 1, _id: 1 })
.limit(pageSize)
.lean();
const hasMoreNext = list.length === pageSize;
return {
list: processChatTimeFilter(getFormatDatasetCiteList(list), chatTime),
hasMorePrev: false,
hasMoreNext
};
}
const prevHalfSize = Math.floor(pageSize / 2);
const nextHalfSize = pageSize - prevHalfSize - 1;
const { list: prevList, hasMore: hasMorePrev } = await getPrevNodes(
initialId,
initialIndex,
prevHalfSize,
baseMatch
);
const { list: nextList, hasMore: hasMoreNext } = await getNextNodes(
initialId,
initialIndex,
nextHalfSize,
baseMatch
);
const resultList = [...prevList, centerNode, ...nextList];
return {
list: processChatTimeFilter(getFormatDatasetCiteList(resultList), chatTime),
hasMorePrev,
hasMoreNext
};
}
async function handlePaginatedLoad({
prevId,
prevIndex,
nextId,
nextIndex,
pageSize,
chatTime,
baseMatch
}: {
prevId: string | undefined;
prevIndex: number | undefined;
nextId: string | undefined;
nextIndex: number | undefined;
pageSize: number;
chatTime: Date;
baseMatch: BaseMatchType;
}): Promise<GetCollectionQuoteRes> {
const { list, hasMore } =
prevId && prevIndex !== undefined
? await getPrevNodes(prevId, prevIndex, pageSize, baseMatch)
: await getNextNodes(nextId!, nextIndex!, pageSize, baseMatch);
const processedList = processChatTimeFilter(getFormatDatasetCiteList(list), chatTime);
return {
list: processedList,
hasMorePrev: !!prevId && hasMore,
hasMoreNext: !!nextId && hasMore
};
}
async function getPrevNodes(
initialId: string,
initialIndex: number,
limit: number,
baseMatch: BaseMatchType
): Promise<{
list: DatasetDataSchemaType[];
hasMore: boolean;
}> {
const match: BaseMatchType = {
...baseMatch,
$or: [
{ chunkIndex: { $lt: initialIndex } },
{ chunkIndex: initialIndex, _id: { $lt: new Types.ObjectId(initialId) } }
]
};
const list = await MongoDatasetData.find(match, quoteDataFieldSelector)
.sort({ chunkIndex: -1, _id: -1 })
.limit(limit)
.lean();
return {
list: list.filter((item) => String(item._id) !== initialId).reverse(),
hasMore: list.length === limit
};
}
async function getNextNodes(
initialId: string,
initialIndex: number,
limit: number,
baseMatch: BaseMatchType
): Promise<{
list: DatasetDataSchemaType[];
hasMore: boolean;
}> {
const match: BaseMatchType = {
...baseMatch,
$or: [
{ chunkIndex: { $gt: initialIndex } },
{ chunkIndex: initialIndex, _id: { $gt: new Types.ObjectId(initialId) } }
]
};
const list = await MongoDatasetData.find(match, quoteDataFieldSelector)
.sort({ chunkIndex: 1, _id: 1 })
.limit(limit)
.lean();
return {
list: list.filter((item) => String(item._id) !== initialId),
hasMore: list.length === limit
};
}
import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api.d';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { jsonRes } from '@fastgpt/service/common/response';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import type { NextApiResponse } from 'next';
async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResponse) {
let { teamId, appId, chatId, teamToken } = req.query;
if (!teamId || !appId || !teamToken) {
return Promise.reject('teamId, appId, teamToken are required');
}
const { uid } = await authTeamSpaceToken({
teamId,
teamToken
});
const [team, chat, app] = await Promise.all([
MongoTeam.findById(teamId, 'name avatar').lean(),
MongoChat.findOne({ teamId, appId, chatId }).lean(),
MongoApp.findById(appId).lean()
]);
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
// auth chat permission
if (chat && chat.outLinkUid !== uid) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
// get app and history
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title,
userAvatar: team?.avatar,
variables: chat?.variables,
sharePassword: '',
app: {
chatConfig: getAppChatConfig({
chatConfig,
systemConfigNode: getGuideModule(nodes),
storeVariables: chat?.variableList,
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro,
type: app.type,
pluginInputs
}
}
});
}
export default NextAPI(handler);
...@@ -73,7 +73,7 @@ async function handler(req: ApiRequestProps): Promise<CreateDatasetResponse> { ...@@ -73,7 +73,7 @@ async function handler(req: ApiRequestProps): Promise<CreateDatasetResponse> {
} }
// check limit // check limit
await checkTeamDatasetLimit(teamId); // await checkTeamDatasetLimit(teamId);
const datasetId = await mongoSessionRun(async (session) => { const datasetId = await mongoSessionRun(async (session) => {
const [dataset] = await MongoDataset.create( const [dataset] = await MongoDataset.create(
......
...@@ -48,6 +48,21 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteDataResponse> { ...@@ -48,6 +48,21 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteDataResponse> {
collectionIds: [datasetData.collectionId] collectionIds: [datasetData.collectionId]
}) })
]); ]);
// 验证该 collection 是否在聊天记录中被引用
// 如果验证失败,不阻止访问(因为用户已经通过了 chat 权限验证)
// try {
// await authCollectionInChat({
// appId,
// chatId,
// chatItemDataId,
// collectionIds: [String(datasetData.collectionId)]
// });
// } catch (error) {
// // 如果 collection 不在聊天记录中被引用,但仍然允许访问
// // 因为用户已经通过了 chat 权限验证,并且有 responseDetail 权限
// // 这允许用户查看聊天中可能间接引用的数据
// }
if (!collection) { if (!collection) {
return Promise.reject(new UserError('Can not find the collection')); return Promise.reject(new UserError('Can not find the collection'));
} }
......
...@@ -10,12 +10,19 @@ import { ...@@ -10,12 +10,19 @@ import {
import { getDatasetSyncDatasetStatus } from '@fastgpt/service/core/dataset/datasetSync'; import { getDatasetSyncDatasetStatus } from '@fastgpt/service/core/dataset/datasetSync';
import { filterApiDatasetServerPublicData } from '@fastgpt/global/core/dataset/apiDataset/utils'; import { filterApiDatasetServerPublicData } from '@fastgpt/global/core/dataset/apiDataset/utils';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { getUserDatasetCollaboratorPermission } from '@fastgpt/service/support/permission/dataset/getDatasetPermissions';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import { sumPer } from '@fastgpt/global/support/permission/utils';
async function handler(req: ApiRequestProps): Promise<GetDatasetDetailResponse> { async function handler(req: ApiRequestProps): Promise<GetDatasetDetailResponse> {
const { id: datasetId } = parseApiInput({ req, querySchema: GetDatasetDetailQuerySchema }).query; const { id: datasetId } = parseApiInput({ req, querySchema: GetDatasetDetailQuerySchema }).query;
// 凭证校验 // 凭证校验
const { dataset, permission } = await authDataset({ const {
dataset,
permission: basePermission,
tmbId
} = await authDataset({
req, req,
authToken: true, authToken: true,
authApiKey: true, authApiKey: true,
...@@ -23,6 +30,22 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetDetailResponse> ...@@ -23,6 +30,22 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetDetailResponse>
per: ReadPermissionVal per: ReadPermissionVal
}); });
// 获取用户的直接协作权限
const collaboratorPermission = await getUserDatasetCollaboratorPermission({
datasetId,
tmbId
});
// 合并基础权限和协作权限,取更高的权限
const finalPermissionValue = collaboratorPermission
? sumPer(basePermission.role, collaboratorPermission)
: basePermission.role;
const permission = new DatasetPermission({
role: finalPermissionValue,
isOwner: String(dataset.tmbId) === String(tmbId) || basePermission.isOwner
});
const { status, errorMsg } = await getDatasetSyncDatasetStatus(datasetId); const { status, errorMsg } = await getDatasetSyncDatasetStatus(datasetId);
return { return {
......
...@@ -35,20 +35,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) { ...@@ -35,20 +35,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { datasetId } = ExportDatasetQuerySchema.parse(req.query); const { datasetId } = ExportDatasetQuerySchema.parse(req.query);
// 凭证校验 // 凭证校验
const { teamId, dataset } = await authDataset({ // const { teamId } = await authDataset({
req, // req,
authToken: true, // authToken: true,
datasetId, // datasetId,
per: WritePermissionVal // per: WritePermissionVal
}); // });
await checkExportDatasetLimit({ // await checkExportDatasetLimit({
teamId, // teamId,
limitMinutes: global.feConfigs?.limit?.exportDatasetLimitMinutes // limitMinutes: global.feConfigs?.limit?.exportDatasetLimitMinutes
}); // });
const datasets = await findDatasetAndAllChildren({ const datasets = await findDatasetAndAllChildren({
teamId,
datasetId, datasetId,
fields: '_id' fields: '_id'
}); });
...@@ -93,7 +92,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) { ...@@ -93,7 +92,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
res.setHeader('Content-Type', 'text/csv; charset=utf-8;'); res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
res.setHeader( res.setHeader(
'Content-Disposition', 'Content-Disposition',
`attachment; filename=${encodeURIComponent(dataset.name)}-backup.csv;` `attachment; filename=${encodeURIComponent('dataset')}-backup.csv;`
); );
const cursor = MongoDatasetData.find<DataItemType>(match, 'q a indexes metadata', { const cursor = MongoDatasetData.find<DataItemType>(match, 'q a indexes metadata', {
...@@ -133,7 +132,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) { ...@@ -133,7 +132,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
res.end(); res.end();
}); });
updateExportDatasetLimit(teamId); // updateExportDatasetLimit(teamId);
} }
export default NextAPI(handler); export default NextAPI(handler);
......
...@@ -22,6 +22,9 @@ import { ...@@ -22,6 +22,9 @@ import {
GetDatasetListBodySchema, GetDatasetListBodySchema,
type GetDatasetListResponse type GetDatasetListResponse
} from '@fastgpt/global/openapi/core/dataset/api'; } from '@fastgpt/global/openapi/core/dataset/api';
import { getDatasetPermissions } from '@fastgpt/service/support/permission/dataset/getDatasetPermissions';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant';
async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> { async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> {
const { parentId, type, searchKey } = parseApiInput({ const { parentId, type, searchKey } = parseApiInput({
...@@ -49,6 +52,9 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> { ...@@ -49,6 +52,9 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> {
] ]
: []) : [])
]); ]);
// 检查用户是否为admin角色
const teamMember = await MongoTeamMember.findById(tmbId).lean();
const isAdmin = !!(teamMember && teamMember.role === 'admin');
// Get team all app permissions // Get team all app permissions
const [roleList, myGroupMap, myOrgSet] = await Promise.all([ const [roleList, myGroupMap, myOrgSet] = await Promise.all([
...@@ -134,6 +140,14 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> { ...@@ -134,6 +140,14 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> {
updateTime: -1 updateTime: -1
}) })
.lean(); .lean();
// 获取dataset权限信息
const { userCollaboratorPermissions, publicDatasetIds } = await getDatasetPermissions({
datasetIds: (myDatasets || []).map((item) => String(item._id)),
tmbId
});
console.log('userCollaboratorPermissions:', userCollaboratorPermissions);
console.log('publicDatasetIds:', Array.from(publicDatasetIds));
const formatDatasets = myDatasets const formatDatasets = myDatasets
.map((dataset) => { .map((dataset) => {
...@@ -149,29 +163,35 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> { ...@@ -149,29 +163,35 @@ async function handler(req: ApiRequestProps): Promise<GetDatasetListResponse> {
) )
.map((item) => item.permission) .map((item) => item.permission)
); );
// 获取当前登录用户对该dataset的协作权限
const rolePer = userCollaboratorPermissions.find(
(item) => item.datasetId === datasetId
)?.permission;
return new DatasetPermission({ return new DatasetPermission({
role: tmbRole ?? groupAndOrgRole, role: tmbRole ?? groupAndOrgRole ?? rolePer ?? DataSetDefaultRoleVal,
isOwner: String(dataset.tmbId) === String(tmbId) || teamPer.isOwner isOwner: String(dataset.tmbId) === String(tmbId) || teamPer.isOwner || isAdmin
}); });
}; };
// 不再继承父文件夹权限,直接使用知识库自己的权限
// inherit // inherit
if ( // if (
dataset.inheritPermission && // dataset.inheritPermission &&
dataset.parentId && // dataset.parentId &&
dataset.type !== DatasetTypeEnum.folder // dataset.type !== DatasetTypeEnum.folder
) { // ) {
const resourceClbs = roleListMap.get(String(dataset._id)) ?? []; // const resourceClbs = roleListMap.get(String(dataset._id)) ?? [];
const parentClbs = roleListMap.get(String(dataset.parentId)) ?? []; // const parentClbs = roleListMap.get(String(dataset.parentId)) ?? [];
// return {
return { // Per: getPer(String(dataset.parentId)).addRole(getPer(String(dataset._id)).role),
Per: getPer(String(dataset.parentId)).addRole(getPer(String(dataset._id)).role), // privateDataset: isPrivateResourceByCollaborators({
privateDataset: isPrivateResourceByCollaborators({ // resourceClbs,
resourceClbs, // parentClbs,
parentClbs, // inheritPermission: true
inheritPermission: true // })
}) // };
}; // }
}
const resourceClbs = roleListMap.get(String(dataset._id)) ?? []; const resourceClbs = roleListMap.get(String(dataset._id)) ?? [];
return { return {
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoChatFeedbackLog } from '@fastgpt/service/core/chat/chatfeedbacklogSchema';
import { MongoChatFeedbackLogRelation } from '@fastgpt/service/core/chat/chatfeedbacklogrelationSchema';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { ChatFeedbackStatusEnum } from '@fastgpt/global/core/chat/constants';
import { insertData2Dataset } from '@/service/core/dataset/data/controller';
import { simpleText } from '@fastgpt/global/common/string/tools';
import { connectionMongo } from '@fastgpt/service/common/mongo';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
const { Types } = connectionMongo;
export type ApproveBody = {
feedbackId: string;
datasetId: string;
q: string;
a: string;
status: string;
};
// 获取反馈日志
async function getFeedbackLog(feedbackId: string) {
const feedbackLog = await MongoChatFeedbackLog.findById(feedbackId).lean();
if (!feedbackLog) {
throw new Error('Feedback log not found');
}
return feedbackLog;
}
// 获取数据集
async function getDataset(datasetId: string) {
const dataset = await MongoDataset.findById(datasetId).lean();
if (!dataset) {
throw new Error('dataset not found');
}
return dataset;
}
// 获取或创建数据集集合
async function getOrCreateCollection(datasetId: string, feedbackLog: any) {
const relation = await MongoChatFeedbackLogRelation.findOne({ datasetId }).lean();
let collection = relation
? await MongoDatasetCollection.findById(relation.collectionId).lean()
: null;
let collectionId = collection?._id || '';
if (!relation || !collection) {
const { _id } = await createOneCollection({
datasetId,
name: '反馈增强',
parentId: '',
type: DatasetCollectionTypeEnum.virtual,
teamId: feedbackLog.teamId,
tmbId: feedbackLog.tmbId
});
collectionId = _id;
if (relation) {
await MongoChatFeedbackLogRelation.deleteOne({ _id: relation._id });
}
await MongoChatFeedbackLogRelation.insertOne({
datasetId,
collectionId: _id
});
}
return collectionId;
}
async function handler(req: ApiRequestProps<ApproveBody>) {
const { feedbackId, datasetId, q, a, status } = req.body;
const feedbackLog = await getFeedbackLog(feedbackId);
await MongoChatFeedbackLog.updateOne({ _id: feedbackId }, { $set: { status } });
if (status == ChatFeedbackStatusEnum.Stored) {
const dataset = await getDataset(datasetId);
const collectionId = await getOrCreateCollection(datasetId, feedbackLog);
// 入库
const formatQ = simpleText(q);
const formatA = simpleText(a);
try {
await insertData2Dataset({
teamId: feedbackLog.teamId,
tmbId: feedbackLog.tmbId,
datasetId,
collectionId,
q: formatQ,
a: formatA,
chunkIndex: 0,
embeddingModel: dataset.vectorModel
});
} catch (err) {
console.log(err);
}
}
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { MongoChatFeedbackLog } from '@fastgpt/service/core/chat/chatfeedbacklogSchema';
import { MongoChatFeedbackLogRelation } from '@fastgpt/service/core/chat/chatfeedbacklogrelationSchema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { ChatFeedbackStatusEnum } from '@fastgpt/global/core/chat/constants';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
export type ListFeedbackBody = {
searchKey?: string;
appId?: string;
status?: string;
};
async function handler(req: ApiRequestProps<ListFeedbackBody>) {
const { searchKey, appId, status } = req.body;
const filter: any = {};
if (searchKey) filter.q = { $regex: searchKey, $options: 'i' };
if (status) filter.status = status;
filter.responseData = {
$elemMatch: {
moduleType: FlowNodeTypeEnum.datasetSearchNode
}
};
if (appId) {
filter.appId = { $in: [appId] };
} else {
const { tmbId, teamId } = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
});
const apps = await MongoApp.find({
$or: [{ tmbId }, { teamId }]
}).lean();
filter.appId = { $in: (apps || []).map((item) => item._id) };
}
const { pageSize = 20, offset } = parsePaginationRequest(req);
const feedbackLogs = await MongoChatFeedbackLog.find(filter)
.sort({
time: -1
})
.skip(offset)
.limit(pageSize)
.lean();
const total = await MongoChatFeedbackLog.countDocuments(filter);
const apps = await MongoApp.find().lean();
const datasets = await MongoDataset.find().lean();
const feedbackLogRealtions = await MongoChatFeedbackLogRelation.find().lean();
const formatFeedbackLogs = feedbackLogs.map((log) => {
log.appName = apps.find((a) => String(a?._id) === String(log?.appId))?.name;
const quoteList = log.responseData?.find(
(item) => item?.moduleType === FlowNodeTypeEnum.datasetSearchNode
)?.quoteList;
// 对 quoteList 根据 datasetId 去重
const uniqueQuoteList = [
...new Map((quoteList ?? []).map((item: any) => [item.datasetId, item])).values()
];
log.datasetList = (uniqueQuoteList ?? []).map((item: any) => ({
datasetId: item?.datasetId,
name: datasets.find((d) => String(d?._id) === String(item?.datasetId))?.name
}));
log.datasetName = (log.datasetList ?? [])
.map((item) => item?.name)
.filter(Boolean)
.join(',');
log.q = (log.qvalue ?? [])
.filter((v: any) => v?.type === 'text' && v?.text?.content)
.map((v: any) => v?.text?.content)
.join('');
log.a = (log.value ?? [])
.filter((v: any) => v?.type === 'text' && v?.text?.content)
.map((v: any) => v?.text?.content)
.join('');
if (log.time) {
// 将 time 格式化为 "YYYY-MM-DD HH:mm:ss"
const date = new Date(log.time);
// const formattedTime = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
log.time = date;
}
return log;
});
return {
list: formatFeedbackLogs,
total
};
}
export default NextAPI(handler);
...@@ -138,8 +138,8 @@ export default NextAPI(handler); ...@@ -138,8 +138,8 @@ export default NextAPI(handler);
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {
sizeLimit: '20mb' sizeLimit: '200mb'
}, },
responseLimit: '20mb' responseLimit: '200mb'
} }
}; };
...@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('url is empty'); throw new Error('url is empty');
} }
if (!FastGPTProUrl) { if (!FastGPTProUrl) {
throw new Error(`未配置商业版链接: ${path}`); return;
} }
// 防御 protocol-relative URL 覆盖主机(如 path 含空段 → `//169.254...`) // 防御 protocol-relative URL 覆盖主机(如 path 含空段 → `//169.254...`)
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { Types } from 'mongoose';
import type { AppChangeOwnerBody } from '@/global/core/app/api';
import { types } from 'util';
async function handler(req: ApiRequestProps<AppChangeOwnerBody>) {
const { appId, ownerId } = req.body;
await MongoApp.findOneAndUpdate({ _id: new Types.ObjectId(appId) }, { $set: { tmbId: ownerId } });
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Types } from 'mongoose';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
export type CollaboratorQuery = {
tmbId: string;
appId: string;
};
async function handler(req: ApiRequestProps<any, CollaboratorQuery>) {
const { tmbId, appId } = req.query;
// 获取 teamId
const { teamId } = await parseHeaderCert({
req,
authToken: true,
authApiKey: true
});
let currentAppIds = [appId];
// 一直往下遍历 parentId,直到为 null
while (currentAppIds) {
const apps = await MongoApp.find({ parentId: { $in: currentAppIds } }).lean();
if (!apps || !apps.length) break;
currentAppIds = apps.map((item) => item._id);
for (const app of apps ?? []) {
await MongoResourcePermission.findOneAndDelete({
tmbId: new Types.ObjectId(tmbId),
resourceId: app._id,
resourceType: 'app'
});
}
}
await MongoResourcePermission.findOneAndDelete({
tmbId: new Types.ObjectId(tmbId),
resourceId: new Types.ObjectId(appId),
resourceType: 'app'
});
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { Types } from 'mongoose';
import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Permission } from '@fastgpt/global/support/permission/controller';
export type CollaboratorBody = {
appId: string;
};
export type CollaboratorType = {
avatar: string;
name: string;
teamId: string;
tmbId: string;
permission: number;
};
async function handler(req: ApiRequestProps<any, CollaboratorBody>) {
const { appId } = req.query;
if (!appId) return [];
const collaborators = (await MongoResourcePermission.aggregate([
{ $match: { resourceId: new Types.ObjectId(appId), resourceType: 'app' } },
{
$lookup: {
from: TeamMemberCollectionName, // 目标集合名
localField: 'tmbId', // 协作权限表中的字段
foreignField: '_id', // 团队成员表的主键
as: 'memberInfo' // 输出字段
}
},
{ $unwind: '$memberInfo' }, // 展开关联结果
{
$project: {
// 格式化输出
_id: 0, // 关键设置:不返回 _id
avatar: '$memberInfo.avatar',
name: '$memberInfo.name',
permission: '$permission',
teamId: '$memberInfo.teamId',
tmbId: '$tmbId'
}
}
])) as CollaboratorType[];
const list: CollaboratorItemType[] = collaborators.map((item) => ({
...item,
permission: new Permission({ role: item.permission })
}));
return list;
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import type { UpdateAppCollaboratorBody } from '@fastgpt/global/core/app/collaborator';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { Types } from 'mongoose';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
async function handler(req: ApiRequestProps<UpdateAppCollaboratorBody>) {
const { appId, groups, members, orgs, permission } = req.body;
// 获取 teamId 和 tmbId
const { teamId } = await parseHeaderCert({
req,
authToken: true,
authApiKey: true
});
let currentAppId = new Types.ObjectId(appId);
// 一直往上遍历 parentId,直到为 null
while (currentAppId) {
const app = await MongoApp.findById(currentAppId).lean();
if (!app) break;
if (!app.parentId) break;
currentAppId = new Types.ObjectId(app.parentId);
for (const tmbId of members ?? []) {
await MongoResourcePermission.findOneAndUpdate(
{
tmbId: new Types.ObjectId(tmbId),
resourceId: currentAppId,
resourceType: PerResourceTypeEnum.app
},
{
$set: {
teamId,
resourceType: PerResourceTypeEnum.app,
resourceId: currentAppId,
tmbId,
permission: ReadPermissionVal
}
},
{ upsert: true } // 不存在则插入
);
}
}
for (const tmbId of members ?? []) {
await MongoResourcePermission.findOneAndUpdate(
{
tmbId: new Types.ObjectId(tmbId),
resourceId: new Types.ObjectId(appId),
resourceType: PerResourceTypeEnum.app
},
{
$set: {
teamId,
resourceType: PerResourceTypeEnum.app,
resourceId: appId,
tmbId,
permission
}
},
{ upsert: true } // 不存在则插入
);
}
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getUserAppPermission } from '@fastgpt/service/support/permission/app/getUserAppPermission';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
export type CheckAppPermissionBody = {
appId: string;
};
export type CheckAppPermissionResponse = {
appId: string;
tmbId: string;
permission: {
isOwner: boolean;
hasReadPer: boolean;
hasWritePer: boolean;
hasManagePer: boolean;
permissionValue: number;
};
};
async function handler(
req: ApiRequestProps<CheckAppPermissionBody>
): Promise<CheckAppPermissionResponse> {
const { appId } = req.body;
if (!appId) {
throw new Error('appId is required');
}
// 1. 获取当前登录用户信息
const { tmbId } = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: 0 // 不需要特定权限,只需要验证用户身份
});
// 2. 查询用户对该app的权限
const userPermission = await getUserAppPermission({
tmbId,
appId: String(appId)
});
return {
appId: String(appId),
tmbId,
permission: {
isOwner: userPermission.isOwner,
hasReadPer: userPermission.hasReadPer,
hasWritePer: userPermission.hasWritePer,
hasManagePer: userPermission.hasManagePer,
permissionValue: userPermission.permission.role
}
};
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Types } from 'mongoose';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
async function handler(req: ApiRequestProps<any, DatasetCollaboratorDeleteParams>) {
const { tmbId, datasetId } = req.query;
// 获取 teamId
const { teamId } = await parseHeaderCert({
req,
authToken: true,
authApiKey: true
});
let currentDatasetIds = [datasetId];
// 一直往下遍历 parentId,直到为 null
while (currentDatasetIds) {
const datasets = await MongoDataset.find({ parentId: { $in: currentDatasetIds } }).lean();
if (!datasets || !datasets.length) break;
currentDatasetIds = datasets.map((item) => item._id);
for (const dataset of datasets ?? []) {
await MongoResourcePermission.findOneAndDelete({
tmbId: new Types.ObjectId(tmbId),
resourceId: dataset._id,
resourceType: 'dataset'
});
}
}
await MongoResourcePermission.findOneAndDelete({
tmbId: new Types.ObjectId(tmbId),
resourceId: new Types.ObjectId(datasetId),
resourceType: 'dataset'
});
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { Types } from 'mongoose';
import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Permission } from '@fastgpt/global/support/permission/controller';
export type CollaboratorQuery = {
datasetId: string;
};
export type CollaboratorType = {
avatar: string;
name: string;
teamId: string;
tmbId: string;
permission: number;
};
async function handler(req: ApiRequestProps<any, CollaboratorQuery>) {
const { datasetId } = req.query;
if (!datasetId) return [];
const collaborators = (await MongoResourcePermission.aggregate([
{ $match: { resourceId: new Types.ObjectId(datasetId), resourceType: 'dataset' } },
{
$lookup: {
from: TeamMemberCollectionName, // 目标集合名
localField: 'tmbId', // 协作权限表中的字段
foreignField: '_id', // 团队成员表的主键
as: 'memberInfo' // 输出字段
}
},
{ $unwind: '$memberInfo' }, // 展开关联结果
{
$project: {
// 格式化输出
_id: 0, // 关键设置:不返回 _id
avatar: '$memberInfo.avatar',
name: '$memberInfo.name',
permission: '$permission',
teamId: '$memberInfo.teamId',
tmbId: '$tmbId'
}
}
])) as CollaboratorType[];
const list: CollaboratorItemType[] = collaborators.map((item) => ({
...item,
permission: new Permission({ role: item.permission })
}));
return list;
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { Types } from 'mongoose';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import type { UpdateDatasetCollaboratorBody } from '@fastgpt/global/core/dataset/collaborator';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
async function handler(req: ApiRequestProps<UpdateDatasetCollaboratorBody>) {
const { datasetId, groups, members, orgs, permission } = req.body;
// 获取 teamId
const { teamId } = await parseHeaderCert({
req,
authToken: true,
authApiKey: true
});
let currentDatasetId = new Types.ObjectId(datasetId);
// 一直往上遍历 parentId,直到为 null
while (currentDatasetId) {
const dataset = await MongoDataset.findById(currentDatasetId).lean();
if (!dataset) break;
if (!dataset.parentId) break;
currentDatasetId = new Types.ObjectId(dataset.parentId);
for (const tmbId of members ?? []) {
await MongoResourcePermission.findOneAndUpdate(
{
tmbId: new Types.ObjectId(tmbId),
resourceId: currentDatasetId,
resourceType: PerResourceTypeEnum.dataset
},
{
$set: {
teamId,
resourceType: PerResourceTypeEnum.dataset,
resourceId: currentDatasetId,
tmbId,
permission: ReadPermissionVal
}
},
{ upsert: true } // 不存在则插入
);
}
}
for (const tmbId of members ?? []) {
await MongoResourcePermission.findOneAndUpdate(
{
tmbId: new Types.ObjectId(tmbId),
resourceId: new Types.ObjectId(datasetId),
resourceType: PerResourceTypeEnum.dataset
},
{
$set: {
teamId,
resourceType: PerResourceTypeEnum.dataset,
resourceId: datasetId,
tmbId,
permission
}
},
{ upsert: true } // 不存在则插入
);
}
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getUserDatasetCollaboratorPermission } from '@fastgpt/service/support/permission/dataset/getDatasetPermissions';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant';
export type CheckDatasetPermissionBody = { datasetId: string };
export type CheckDatasetPermissionResponse = {
datasetId: string;
tmbId: string;
permission: {
isOwner: boolean;
hasReadPer: boolean;
hasWritePer: boolean;
hasManagePer: boolean;
permissionValue: number;
};
};
async function handler(
req: ApiRequestProps<CheckDatasetPermissionBody>
): Promise<CheckDatasetPermissionResponse> {
const { datasetId } = req.body;
if (!datasetId) {
throw new Error('datasetId is required');
}
const { tmbId } = await authUserPer({ req, authToken: true, authApiKey: true, per: 0 });
// 获取dataset信息
const dataset = await MongoDataset.findById(datasetId).lean();
if (!dataset) {
throw new Error('Dataset not found');
}
// 检查用户是否为admin角色
const teamMember = await MongoTeamMember.findById(tmbId).lean();
const isAdmin = !!(teamMember && teamMember.role === 'admin');
// 检查是否为所有者或admin
const isOwner = String(dataset.tmbId) === String(tmbId) || isAdmin;
if (isOwner) {
const permission = new DatasetPermission({ isOwner: true });
return {
datasetId: String(datasetId),
tmbId,
permission: {
isOwner: true,
hasReadPer: permission.hasReadPer,
hasWritePer: permission.hasWritePer,
hasManagePer: permission.hasManagePer,
permissionValue: permission.role
}
};
}
// 获取协作权限
const collaboratorPermission = await getUserDatasetCollaboratorPermission({
datasetId: String(datasetId),
tmbId
});
const permissionValue = collaboratorPermission ?? DataSetDefaultRoleVal;
const permission = new DatasetPermission({ role: permissionValue });
return {
datasetId: String(datasetId),
tmbId,
permission: {
isOwner: false,
hasReadPer: permission.hasReadPer,
hasWritePer: permission.hasWritePer,
hasManagePer: permission.hasManagePer,
permissionValue: permission.role
}
};
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import type { SearchResult } from '@fastgpt/global/support/user/api.d';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type.d';
import type { TeamMemberSchema } from '@fastgpt/global/support/user/team/type.d';
import { Types } from 'mongoose';
export type SearchQuery = {
searchKey?: string;
};
async function handler(req: ApiRequestProps<any, SearchQuery>): Promise<SearchResult> {
const { searchKey } = req.query;
const { teamId } = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
});
const where = {
teamId: new Types.ObjectId(teamId),
...(searchKey && {
name: {
$regex: searchKey,
$options: 'i' // "i" 表示不区分大小写
}
})
};
const list = await MongoTeamMember.find(where);
const listWithTeamMember: TeamMemberItemType[] = list.map((item: TeamMemberSchema) => {
return {
avatar: item.avatar,
createTime: item.createTime,
memberName: item.name,
status: item.status,
teamId: item.teamId,
tmbId: item._id,
userId: item.userId
} as TeamMemberItemType;
});
return { groups: [], members: listWithTeamMember, orgs: [] };
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
export type GroupBody = {
searchKey?: string;
};
async function handler(req: ApiRequestProps<GroupBody>): Promise<any> {
const { searchKey } = req.body;
return [];
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import type { TeamMemberSchema } from '@fastgpt/global/support/user/team/type.d';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type.d';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { Types } from 'mongoose';
import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export type MemberBody = PaginationProps<{
status?: 'active' | 'inactive';
withOrgs?: boolean;
withPermission?: boolean;
searchKey?: string;
orgId?: string;
groupId?: string;
}>;
async function handler(
req: ApiRequestProps<MemberBody>
): Promise<PaginationResponse<TeamMemberItemType>> {
const { searchKey, status, withOrgs, withPermission, orgId, groupId } = req.body;
if (withOrgs === false) return { list: [], total: 0 };
const {
tmbId,
teamId,
permission: teamPer
} = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
});
const where = {
$expr: {
$and: [
{ $eq: [{ $toString: '$teamId' }, String(teamId)] },
{ $ne: [{ $toString: '$_id' }, String(tmbId)] }
]
},
...(status && { status }),
...(searchKey && {
name: {
$regex: searchKey,
$options: 'i' // "i" 表示不区分大小写
}
})
};
const [list, total] = await Promise.all([
MongoTeamMember.find(where),
MongoTeamMember.countDocuments(where)
]);
const listWithTeamMember: TeamMemberItemType[] = list.map((item: TeamMemberSchema) => {
return {
avatar: item.avatar,
createTime: item.createTime,
memberName: item.name,
orgs: [] as String[],
role: item.role,
status: item.status,
teamId: item.teamId,
tmbId: item._id,
userId: item.userId,
permission: teamPer
} as TeamMemberItemType;
});
return { list: listWithTeamMember, total };
}
export default NextAPI(handler);
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
export type OrgBody = {
searchKey?: string;
};
async function handler(req: ApiRequestProps<OrgBody>): Promise<any> {
const { searchKey } = req.body;
return [];
}
export default NextAPI(handler);
...@@ -20,7 +20,7 @@ async function handler(req: ApiRequestProps): Promise<McpCreateResponseType> { ...@@ -20,7 +20,7 @@ async function handler(req: ApiRequestProps): Promise<McpCreateResponseType> {
}); });
if (!permission.hasApikeyCreatePer) { if (!permission.hasApikeyCreatePer) {
return Promise.reject(TeamErrEnum.unPermission); // return Promise.reject(TeamErrEnum.unPermission);
} }
const { name, apps } = parseApiInput({ req, bodySchema: McpCreateBodySchema }).body; const { name, apps } = parseApiInput({ req, bodySchema: McpCreateBodySchema }).body;
......
import type { PostShareLoginProps } from '@fastgpt/global/support/outLink/api.d';
import type { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import { jsonRes } from '@fastgpt/service/common/response';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { shareId, password } = req.body as PostShareLoginProps;
if (!shareId || !password) {
throw new Error('缺少参数');
}
// 检测用户是否存在
const shareChat = (await MongoOutLink.findOne({ shareId }).lean()) as OutLinkSchema;
if (!shareChat) {
throw new Error('无效的分享链接');
}
// 如果没设置密码
if (!shareChat.limit?.password) {
return jsonRes(res, {
data: {
user: {},
token: shareChat.limit?.password
}
});
}
if (shareChat.limit.password !== password) {
throw new Error('密码错误,无法登录');
}
return jsonRes(res, {
data: {
user: {},
token: shareChat.limit.password
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { getUserDetail } from '@fastgpt/service/support/user/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
...@@ -69,39 +70,29 @@ async function handler( ...@@ -69,39 +70,29 @@ async function handler(
} }
); );
const token = await createUserSession({
userId: user._id,
teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId,
isRoot: username === 'root',
ip: getClientIpFromRequest(req)
});
setCookie(res, token);
void reportCRMVisitorIdentity({
visitorId: visitorIdentity.visitorId,
userId: String(user._id),
username: user.username,
contact: user.contact
});
pushTrack.login({ pushTrack.login({
type: 'password', type: 'password',
uid: user._id, uid: user._id,
teamId: userDetail.team.teamId, teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId tmbId: userDetail.team.tmbId
}); });
addAuditLog({
tmbId: userDetail.team.tmbId, const token = await createUserSession({
teamId: userDetail.team.teamId, userId: String(user._id),
event: AuditEventEnum.LOGIN teamId: String(userDetail.team.teamId),
tmbId: String(userDetail.team.tmbId),
isRoot: username === 'root' || username === 'admin'
}); });
setCookie(res, token);
return { return {
user: userDetail, user: userDetail,
token token
}; };
} }
export default NextAPI(handler); export default NextAPI(
useIPFrequencyLimit({ id: 'login-by-password', seconds: 120, limit: 10, force: true }),
handler
);
...@@ -15,7 +15,6 @@ async function resetExpiredPswHandler(req: ApiRequestProps): Promise<ResetExpire ...@@ -15,7 +15,6 @@ async function resetExpiredPswHandler(req: ApiRequestProps): Promise<ResetExpire
const { newPsw } = ResetExpiredPswBodySchema.parse(req.body); const { newPsw } = ResetExpiredPswBodySchema.parse(req.body);
const { userId, sessionId } = await authCert({ req, authToken: true }); const { userId, sessionId } = await authCert({ req, authToken: true });
const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean(); const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean();
if (!user) { if (!user) {
return Promise.reject('The password has not expired'); return Promise.reject('The password has not expired');
} }
......
...@@ -20,7 +20,8 @@ async function handler( ...@@ -20,7 +20,8 @@ async function handler(
): Promise<UpdatePasswordByOldResponseType> { ): Promise<UpdatePasswordByOldResponseType> {
const { oldPsw, newPsw } = UpdatePasswordByOldBodySchema.parse(req.body); const { oldPsw, newPsw } = UpdatePasswordByOldBodySchema.parse(req.body);
const { tmbId, teamId, sessionId } = await authCert({ req, authToken: true }); const { tmbId, teamId } = await authCert({ req, authToken: true });
const sessionId = req.headers['x-session-id'] as string;
const tmb = await MongoTeamMember.findById(tmbId); const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) { if (!tmb) {
return Promise.reject('can not find it'); return Promise.reject('can not find it');
......
...@@ -643,8 +643,8 @@ const authShareChat = async ({ ...@@ -643,8 +643,8 @@ const authShareChat = async ({
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {
sizeLimit: '10mb' sizeLimit: '200mb'
}, },
responseLimit: '10mb' responseLimit: '200mb'
} }
}; };
...@@ -643,8 +643,8 @@ const authShareChat = async ({ ...@@ -643,8 +643,8 @@ const authShareChat = async ({
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {
sizeLimit: '10mb' sizeLimit: '200mb'
}, },
responseLimit: '10mb' responseLimit: '200mb'
} }
}; };
import MyRadio from '@/components/common/MyRadio';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { FlexProps } from '@chakra-ui/react';
import { Box, Flex, Grid, Image, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import type { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
enum UsingWayEnum {
link = 'link',
iframe = 'iframe',
script = 'script'
}
const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose: () => void }) => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { File, onOpen } = useSelectFile({
multiple: false,
fileType: 'image/*'
});
const { feConfigs } = useSystemStore();
const VariableTypeList = [
{
title: <Image src={'/imgs/outlink/link.svg'} alt={''} />,
value: UsingWayEnum.link
},
{
title: <Image src={'/imgs/outlink/iframe.svg'} alt={''} />,
value: UsingWayEnum.iframe
},
{
title: <Image src={'/imgs/outlink/script.svg'} alt={''} />,
value: UsingWayEnum.script
}
];
const [refresh, setRefresh] = useState(false);
const { getValues, setValue, register, watch } = useForm({
defaultValues: {
usingWay: UsingWayEnum.link,
showHistory: true,
scriptIconCanDrag: true,
scriptDefaultOpen: true,
scriptOpenIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTMyNzg1NjY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxMzIiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDMyQzI0Ny4wNCAzMiAzMiAyMjQgMzIgNDY0QTQxMC4yNCA0MTAuMjQgMCAwIDAgMTcyLjQ4IDc2OEwxNjAgOTY1LjEyYTI1LjI4IDI1LjI4IDAgMCAwIDM5LjA0IDIyLjRsMTY4LTExMkE1MjguNjQgNTI4LjY0IDAgMCAwIDUxMiA4OTZjMjY0Ljk2IDAgNDgwLTE5MiA0ODAtNDMyUzc3Ni45NiAzMiA1MTIgMzJ6IG0yNDQuOCA0MTZsLTM2MS42IDMwMS43NmExMi40OCAxMi40OCAwIDAgMS0xOS44NC0xMi40OGw1OS4yLTIzMy45MmgtMTYwYTEyLjQ4IDEyLjQ4IDAgMCAxLTcuMzYtMjMuMzZsMzYxLjYtMzAxLjc2YTEyLjQ4IDEyLjQ4IDAgMCAxIDE5Ljg0IDEyLjQ4bC01OS4yIDIzMy45MmgxNjBhMTIuNDggMTIuNDggMCAwIDEgOCAyMi4wOHoiIGZpbGw9IiM0ZTgzZmQiIHAtaWQ9IjQxMzMiPjwvcGF0aD48L3N2Zz4=',
scriptCloseIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTM1NDQxNTI2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjYzNjciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDEwMjRBNTEyIDUxMiAwIDEgMSA1MTIgMGE1MTIgNTEyIDAgMCAxIDAgMTAyNHpNMzA1Ljk1NjU3MSAzNzAuMzk1NDI5TDQ0Ny40ODggNTEyIDMwNS45NTY1NzEgNjUzLjYwNDU3MWE0NS41NjggNDUuNTY4IDAgMSAwIDY0LjQzODg1OCA2NC40Mzg4NThMNTEyIDU3Ni41MTJsMTQxLjYwNDU3MSAxNDEuNTMxNDI5YTQ1LjU2OCA0NS41NjggMCAwIDAgNjQuNDM4ODU4LTY0LjQzODg1OEw1NzYuNTEyIDUxMmwxNDEuNTMxNDI5LTE0MS42MDQ1NzFhNDUuNTY4IDQ1LjU2OCAwIDEgMC02NC40Mzg4NTgtNjQuNDM4ODU4TDUxMiA0NDcuNDg4IDM3MC4zOTU0MjkgMzA1Ljk1NjU3MWE0NS41NjggNDUuNTY4IDAgMCAwLTY0LjQzODg1OCA2NC40Mzg4NTh6IiBmaWxsPSIjNGU4M2ZkIiBwLWlkPSI2MzY4Ij48L3BhdGg+PC9zdmc+'
}
});
const selectFile = useCallback(
async (files: File[], key: 'scriptOpenIcon' | 'scriptCloseIcon') => {
const file = files[0];
if (!file) return;
// image to base64
const base64 = await fileToBase64(file);
setValue(key, base64);
},
[setValue]
);
watch(() => {
setRefresh(!refresh);
});
const baseUrl = feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}/chat/share?shareId=${share?.shareId}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;
const wayMap = {
[UsingWayEnum.link]: {
blockTitle: t('common:core.app.outLink.Link block title'),
code: linkUrl
},
[UsingWayEnum.iframe]: {
blockTitle: t('common:core.app.outLink.Iframe block title'),
code: `<iframe
src="${linkUrl}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="*"
/>`
},
[UsingWayEnum.script]: {
blockTitle: t('common:core.app.outLink.Script block title'),
code: `<script
src="${baseUrl}/js/iframe.js"
id="chatbot-iframe"
data-bot-src="${linkUrl}"
data-default-open="${getValues('scriptDefaultOpen') ? 'true' : 'false'}"
data-drag="${getValues('scriptIconCanDrag') ? 'true' : 'false'}"
data-open-icon="${getValues('scriptOpenIcon')}"
data-close-icon="${getValues('scriptCloseIcon')}"
defer
></script>
<script>
console.log("Chat box loaded")
</script>`
}
};
const gridItemStyle: FlexProps = {
alignItems: 'center',
bg: 'myWhite.600',
p: 2,
borderRadius: 'md',
border: theme.borders.sm
};
return (
<MyModal
isOpen
isCentered
iconSrc="/imgs/modal/usingWay.svg"
title={t('common:core.app.outLink.Select Using Way')}
onClose={onClose}
maxW={['90vw', '700px']}
>
<ModalBody py={4}>
<MyRadio
gridGap={2}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(3,1fr)']}
value={getValues('usingWay')}
list={VariableTypeList}
hiddenCircle
p={0}
onChange={(e) => {
setValue('usingWay', e);
}}
/>
{/* config */}
<Grid
gridTemplateColumns={['repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={4}
my={5}
fontSize={'sm'}
>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Show History')}</Box>
<Switch {...register('showHistory')} />
</Flex>
{getValues('usingWay') === UsingWayEnum.script && (
<>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Can Drag')}</Box>
<Switch {...register('scriptIconCanDrag')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Default open')}</Box>
<Switch {...register('scriptDefaultOpen')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
<Image
src={getValues('scriptOpenIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptOpenIcon')}
/>
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
<Image
src={getValues('scriptCloseIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptCloseIcon')}
/>
</Flex>
</>
)}
</Grid>
{/* code */}
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
<Flex
p={3}
bg={'myWhite.500'}
border={theme.borders.base}
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
>
<Box flex={1}>{wayMap[getValues('usingWay')].blockTitle}</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => {
copyData(wayMap[getValues('usingWay')].code);
}}
/>
</Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
{wayMap[getValues('usingWay')].code}
</Box>
</Box>
</ModalBody>
<File onSelect={selectFile} />
</MyModal>
);
};
export default SelectUsingWayModal;
...@@ -52,6 +52,7 @@ import { postMarkChatRead } from '@/web/core/chat/history/api'; ...@@ -52,6 +52,7 @@ import { postMarkChatRead } from '@/web/core/chat/history/api';
import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
import type { GetHistoriesBodyType } from '@fastgpt/global/openapi/core/chat/history/api'; import type { GetHistoriesBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Watermark from '@/components/common/Watermark';
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM); const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
...@@ -77,6 +78,7 @@ const OutLink = (props: Props) => { ...@@ -77,6 +78,7 @@ const OutLink = (props: Props) => {
const router = useRouter(); const router = useRouter();
const { const {
shareId = '', shareId = '',
watermark = '',
showHistory = '1', showHistory = '1',
showHead = '1', showHead = '1',
authToken, authToken,
...@@ -86,6 +88,7 @@ const OutLink = (props: Props) => { ...@@ -86,6 +88,7 @@ const OutLink = (props: Props) => {
...customVariables ...customVariables
} = router.query as { } = router.query as {
shareId: string; shareId: string;
watermark: string;
showHistory: '0' | '1'; showHistory: '0' | '1';
showHead: '0' | '1'; showHead: '0' | '1';
authToken: string; authToken: string;
...@@ -94,7 +97,10 @@ const OutLink = (props: Props) => { ...@@ -94,7 +97,10 @@ const OutLink = (props: Props) => {
[key: string]: string; [key: string]: string;
}; };
const { isPc } = useSystem(); const { isPc } = useSystem();
const { outLinkAuthData, appId, chatId } = useChatStore(); let { outLinkAuthData, appId, chatId } = useChatStore();
if (props.customUid) {
chatId = props.customUid + '_' + chatId;
}
// Remove empty value field // Remove empty value field
const formatedCustomVariables = useMemo(() => { const formatedCustomVariables = useMemo(() => {
...@@ -141,6 +147,15 @@ const OutLink = (props: Props) => { ...@@ -141,6 +147,15 @@ const OutLink = (props: Props) => {
outLinkUid outLinkUid
} }
}); });
const sharePassword = res.sharePassword;
if (sharePassword) {
const password = localStorage.getItem('shareAuthCode_' + shareId);
if (!password || password !== sharePassword) {
// 密码错误, 跳转到密码输入页面
router.push('/login/sharelogin?shareId=' + shareId);
}
}
setChatBoxData({ setChatBoxData({
...res, ...res,
...@@ -199,7 +214,11 @@ const OutLink = (props: Props) => { ...@@ -199,7 +214,11 @@ const OutLink = (props: Props) => {
variables, variables,
responseChatItemId responseChatItemId
}: StartChatFnProps) => { }: StartChatFnProps) => {
const completionChatId = chatId || getNanoid(); let id = getNanoid();
if (props.customUid) {
id = props.customUid + '_' + id;
}
const completionChatId = chatId || id;
const histories = messages.slice(-1); const histories = messages.slice(-1);
//post message to report chat start //post message to report chat start
...@@ -297,6 +316,7 @@ const OutLink = (props: Props) => { ...@@ -297,6 +316,7 @@ const OutLink = (props: Props) => {
return ( return (
<> <>
<Watermark content={watermark}>
<NextHead <NextHead
title={props.appName || data?.app?.name || 'AI'} title={props.appName || data?.app?.name || 'AI'}
desc={props.appIntro || data?.app?.intro} desc={props.appIntro || data?.app?.intro}
...@@ -483,6 +503,7 @@ const OutLink = (props: Props) => { ...@@ -483,6 +503,7 @@ const OutLink = (props: Props) => {
</PageContainer> </PageContainer>
)} )}
</Flex> </Flex>
</Watermark>
</> </>
); );
}; };
......
...@@ -142,9 +142,7 @@ const Dataset = () => { ...@@ -142,9 +142,7 @@ const Dataset = () => {
{isPc && RenderSearchInput} {isPc && RenderSearchInput}
{(folderDetail {
? folderDetail.permission.hasWritePer
: userInfo?.team?.permission.hasDatasetCreatePer) && (
<Box pl={[0, 4]}> <Box pl={[0, 4]}>
<MultipleMenu <MultipleMenu
size="md" size="md"
...@@ -234,7 +232,7 @@ const Dataset = () => { ...@@ -234,7 +232,7 @@ const Dataset = () => {
]} ]}
/> />
</Box> </Box>
)} }
</Flex> </Flex>
{!isPc && <Box mt={2}>{RenderSearchInput}</Box>} {!isPc && <Box mt={2}>{RenderSearchInput}</Box>}
......
import React, { useState, useRef, useEffect } from 'react';
import {
Button,
ModalBody,
ModalFooter,
FormControl,
FormLabel,
Textarea,
Text,
Select
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { ChatFeedbackStatusEnum } from '@fastgpt/global/core/chat/constants';
import { feedbackApprove } from '@/web/core/feedback/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
interface FeedbackDetail {
_id: string;
q: string;
a: string;
status: string;
userGoodFeedback?: boolean;
userBadFeedback?: string;
datasetList?: Array<{
name: string;
datasetId: string;
}>;
}
type Props = {
isOpen: boolean;
onClose: (updated?: boolean) => void;
detail: FeedbackDetail;
};
const FeedbackEditor = ({ isOpen, onClose, detail }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const [ignoredLoading, setIgnoredLoading] = useState(false);
const [storedLoading, setStoredLoading] = useState(false);
const [form, setForm] = useState({
q: '',
a: '',
datasetId: ''
});
const [datasetOptions, setDatasetOptions] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
if (detail) {
setForm(() => ({
q: detail.q,
a: detail.a,
datasetId: detail.datasetList?.[0]?.datasetId || ''
}));
}
const options =
detail.datasetList?.map((item: { name: any; datasetId: any }) => ({
label: item.name,
value: item.datasetId
})) || [];
setDatasetOptions(options);
}, [isOpen, detail]);
const qRef = useRef<HTMLTextAreaElement>(null);
const aRef = useRef<HTMLTextAreaElement>(null);
const handleSubmit = async (status: string) => {
if (!form.q || !form.a) {
// 如果没有内容,光标聚焦到没有内容的输入框
if (!form.q && qRef.current) {
qRef.current.focus();
} else if (!form.a && aRef.current) {
aRef.current.focus();
}
return;
}
try {
if (status == ChatFeedbackStatusEnum.Ignored) setIgnoredLoading(true);
if (status == ChatFeedbackStatusEnum.Stored) setStoredLoading(true);
await feedbackApprove({ ...form, feedbackId: detail._id, status });
toast({
title: '审核成功',
status: 'success'
});
setForm((prev) => ({
q: '',
a: '',
datasetId: ''
}));
onClose(true);
} catch (err) {
toast({
title: '审核失败',
status: 'error'
});
} finally {
setIgnoredLoading(false);
setStoredLoading(false);
}
};
const handleCancel = () => {
setForm((prev) => ({
q: '',
a: '',
datasetId: ''
}));
onClose(false);
};
return (
<MyModal
isOpen={isOpen}
onClose={handleCancel}
iconSrc="modal/edit"
title="反馈详情"
w="full"
h="85vh"
isCentered
>
<ModalBody flex="1 0 0" overflow="auto">
<FormControl>
<FormLabel>状态: {detail.status} </FormLabel>
<FormLabel>类型: {detail.userGoodFeedback ? '点赞' : '反馈'} </FormLabel>
{detail.userBadFeedback && (
<>
<FormLabel>反馈详情 </FormLabel>
<Textarea defaultValue={detail.userBadFeedback} minHeight="100px" />
</>
)}
<FormLabel>知识库</FormLabel>
<MySelect
w={'100%'}
bg={'myGray.50'}
value={form.datasetId}
onChange={(e) => setForm((prev) => ({ ...prev, datasetId: e }))}
list={datasetOptions}
/>
<FormLabel htmlFor="q" sx={{ '&::after': { content: '" *"', color: 'red' } }}>
问题
</FormLabel>
<Textarea
id="q"
ref={qRef}
placeholder="最多 3000 字。"
value={form.q}
onChange={(e) => setForm((prev) => ({ ...prev, q: e.target.value }))}
maxLength={3000}
minHeight="170px"
/>
<FormLabel htmlFor="a" sx={{ '&::after': { content: '" *"', color: 'red' } }}>
答案
</FormLabel>
<Textarea
id="a"
ref={aRef}
placeholder="最多 3000 字。"
value={form.a}
onChange={(e) => setForm((prev) => ({ ...prev, a: e.target.value }))}
maxLength={3000}
minHeight="170px"
/>
</FormControl>
</ModalBody>
<ModalFooter>
<Button
mr={2}
onClick={() => handleSubmit(ChatFeedbackStatusEnum.Ignored)}
px={6}
bg="orange"
isLoading={ignoredLoading}
isDisabled={detail.status != ChatFeedbackStatusEnum.Pending || storedLoading}
>
忽略
</Button>
{Array.isArray(detail.datasetList) && detail.datasetList.length > 0 && (
<Button
mr={2}
onClick={() => handleSubmit(ChatFeedbackStatusEnum.Stored)}
px={6}
isLoading={storedLoading}
isDisabled={detail.status != ChatFeedbackStatusEnum.Pending || ignoredLoading}
>
入库
</Button>
)}
</ModalFooter>
</MyModal>
);
};
export default React.memo(FeedbackEditor);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment