diff --git a/deploy/args.json b/deploy/args.json index 953c817f0c..0ab98c1513 100644 --- a/deploy/args.json +++ b/deploy/args.json @@ -13,7 +13,8 @@ "milvus-minio": "RELEASE.2023-03-20T20-16-18Z", "milvus-etcd": "v3.5.5", "milvus-standalone": "v2.4.3", - "oceanbase": "4.3.5-lts" + "oceanbase": "4.3.5-lts", + "seekdb": "1.0.1.0-100000392025122619" }, "images": { "cn": { @@ -30,7 +31,8 @@ "milvus-minio": "minio/minio", "milvus-etcd": "quay.io/coreos/etcd", "milvus-standalone": "milvusdb/milvus", - "oceanbase": "oceanbase/oceanbase-ce" + "oceanbase": "oceanbase/oceanbase-ce", + "seekdb": "oceanbase/seekdb" }, "global": { "fastgpt": "ghcr.io/labring/fastgpt", @@ -46,7 +48,8 @@ "milvus-minio": "minio/minio", "milvus-etcd": "quay.io/coreos/etcd", "milvus-standalone": "milvusdb/milvus", - "oceanbase": "oceanbase/oceanbase-ce" + "oceanbase": "oceanbase/oceanbase-ce", + "seekdb": "oceanbase/seekdb" } } } diff --git a/deploy/docker/cn/docker-compose.seekdb.yml b/deploy/docker/cn/docker-compose.seekdb.yml new file mode 100644 index 0000000000..9c1447199f --- /dev/null +++ b/deploy/docker/cn/docker-compose.seekdb.yml @@ -0,0 +1,292 @@ +# 用于部署的 docker-compose 文件: +# - FastGPT 端口映射为 3000:3000 +# - FastGPT-mcp-server 端口映射 3005:3000 +# - 建议修改账密后再运行 + +# plugin auth token +x-plugin-auth-token: &x-plugin-auth-token 'token' +# aiproxy token +x-aiproxy-token: &x-aiproxy-token 'token' +# 数据库连接相关配置 +x-share-db-config: &x-share-db-config + MONGODB_URI: mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin + DB_MAX_LINK: 100 + REDIS_URL: redis://default:mypassword@redis:6379 + # @see https://fastgpt.cn/docs/introduction/development/object-storage + STORAGE_VENDOR: minio # minio | aws-s3 | cos | oss + STORAGE_REGION: us-east-1 + STORAGE_ACCESS_KEY_ID: minioadmin + STORAGE_SECRET_ACCESS_KEY: minioadmin + STORAGE_PUBLIC_BUCKET: fastgpt-public + STORAGE_PRIVATE_BUCKET: fastgpt-private + STORAGE_EXTERNAL_ENDPOINT: http://192.168.0.2:9000 # 一个服务器和客户端均可访问到存储桶的地址,可以是固定的宿主机 IP 或者域名,注意不要填写成 127.0.0.1 或者 localhost 等本地回环地址(因为容器里无法使用) + STORAGE_S3_ENDPOINT: http://fastgpt-minio:9000 # 协议://域名(IP):端口 + STORAGE_S3_FORCE_PATH_STYLE: true + STORAGE_S3_MAX_RETRIES: 3 + +# 向量库相关配置 +x-vec-config: &x-vec-config + SEEKDB_URL: mysql://root%40tenantname:tenantpassword@seekdb:3306/fastgpt + +version: '3.3' +services: + # Vector DB + vectorDB: + image: oceanbase/seekdb:1.0.1.0-100000392025122619 + container_name: seekdb + restart: always + # ports: # 生产环境建议不要暴露 + # - 3306:3306 + networks: + - fastgpt + environment: + # SeekDB 连接配置(兼容 MySQL 协议) + - MYSQL_ROOT_PASSWORD=seekdbpassword + # SeekDB 租户配置(与 OceanBase 兼容) + - OB_TENANT_NAME=tenantname + - OB_TENANT_PASSWORD=tenantpassword + # MODE分为MINI和NORMAL, 后者会最大程度使用主机资源 + - MODE=MINI + - OB_SERVER_IP=127.0.0.1 + volumes: + - ../seekdb/data:/var/lib/mysql + - ../seekdb/config:/etc/mysql/conf.d + healthcheck: + test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost'] + interval: 30s + timeout: 10s + retries: 1000 + start_period: 10s + + mongo: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/mongo:5.0.32 # cpu 不支持 AVX 时候使用 4.4.29 + container_name: mongo + restart: always + networks: + - fastgpt + command: mongod --keyFile /data/mongodb.key --replSet rs0 + environment: + - MONGO_INITDB_ROOT_USERNAME=myusername + - MONGO_INITDB_ROOT_PASSWORD=mypassword + volumes: + - ./mongo/data:/data/db + healthcheck: + test: + [ + 'CMD', + 'mongo', + '-u', + 'myusername', + '-p', + 'mypassword', + '--authenticationDatabase', + 'admin', + '--eval', + "db.adminCommand('ping')" + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + entrypoint: + - bash + - -c + - | + openssl rand -base64 128 > /data/mongodb.key + chmod 400 /data/mongodb.key + chown 999:999 /data/mongodb.key + echo 'const isInited = rs.status().ok === 1 + if(!isInited){ + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongo:27017" } + ] + }) + }' > /data/initReplicaSet.js + # 启动MongoDB服务 + exec docker-entrypoint.sh "$$@" & + + # 等待MongoDB服务启动 + until mongo -u myusername -p mypassword --authenticationDatabase admin --eval "print('waited for connection')"; do + echo "Waiting for MongoDB to start..." + sleep 2 + done + + # 执行初始化副本集的脚本 + mongo -u myusername -p mypassword --authenticationDatabase admin /data/initReplicaSet.js + + # 等待docker-entrypoint.sh脚本执行的MongoDB服务进程 + wait $$! + redis: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/redis:7.2-alpine + container_name: redis + networks: + - fastgpt + restart: always + command: | + redis-server --requirepass mypassword --loglevel warning --maxclients 10000 --appendonly yes --save 60 10 --maxmemory 4gb --maxmemory-policy noeviction + healthcheck: + test: ['CMD', 'redis-cli', '-a', 'mypassword', 'ping'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + volumes: + - ./redis/data:/data + fastgpt-minio: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/minio:RELEASE.2025-09-07T16-13-09Z + container_name: fastgpt-minio + restart: always + ports: + - 9000:9000 + - 9001:9001 + networks: + - fastgpt + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - ./fastgpt-minio:/data + command: server /data --console-address ":9001" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] + interval: 30s + timeout: 20s + retries: 3 + + fastgpt: + container_name: fastgpt + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.5.1 # git + ports: + - 3000:3000 + networks: + - fastgpt + depends_on: + - mongo + - sandbox + - vectorDB + restart: always + environment: + <<: [*x-share-db-config, *x-vec-config] + # 前端外部可访问的地址,用于自动补全文件资源路径。例如 https:fastgpt.cn,不能填 localhost。这个值可以不填,不填则发给模型的图片会是一个相对路径,而不是全路径,模型可能伪造Host。 + FE_DOMAIN: + # root 密码,用户名为: root。如果需要修改 root 密码,直接修改这个环境变量,并重启即可。 + DEFAULT_ROOT_PSW: 1234 + # 登录凭证密钥 + TOKEN_KEY: any + # root的密钥,常用于升级时候的初始化请求 + ROOT_KEY: root_key + # 文件阅读加密 + FILE_TOKEN_KEY: filetoken + # 密钥加密key + AES256_SECRET_KEY: fastgptkey + + # plugin 地址 + PLUGIN_BASE_URL: http://fastgpt-plugin:3000 + PLUGIN_TOKEN: *x-plugin-auth-token + # sandbox 地址 + SANDBOX_URL: http://sandbox:3000 + # AI Proxy 的地址,如果配了该地址,优先使用 + AIPROXY_API_ENDPOINT: http://aiproxy:3000 + # AI Proxy 的 Admin Token,与 AI Proxy 中的环境变量 ADMIN_KEY + AIPROXY_API_TOKEN: *x-aiproxy-token + + # 日志等级: debug, info, warn, error + LOG_LEVEL: info + STORE_LOG_LEVEL: warn + # 工作流最大运行次数 + WORKFLOW_MAX_RUN_TIMES: 1000 + # 批量执行节点,最大输入长度 + WORKFLOW_MAX_LOOP_TIMES: 100 + # 对话文件过期天数 + CHAT_FILE_EXPIRE_TIME: 7 + # 服务器接收请求,最大大小,单位 MB + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # HTML 转换最大字符数 + MAX_HTML_TRANSFORM_CHARS: 1000000 + volumes: + - ./config.json:/app/data/config.json + sandbox: + container_name: sandbox + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.5.1 + networks: + - fastgpt + restart: always + fastgpt-mcp-server: + container_name: fastgpt-mcp-server + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-mcp_server:v4.14.5.1 + networks: + - fastgpt + ports: + - 3005:3000 + restart: always + environment: + - FASTGPT_ENDPOINT=http://fastgpt:3000 + fastgpt-plugin: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-plugin:v0.4.0 + container_name: fastgpt-plugin + restart: always + networks: + - fastgpt + environment: + <<: *x-share-db-config + AUTH_TOKEN: *x-plugin-auth-token + # 工具网络请求,最大请求和响应体 + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # 最大 API 请求体大小 + MAX_API_SIZE: 10 + depends_on: + fastgpt-minio: + condition: service_healthy + # AI Proxy + aiproxy: + image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.3.2 + container_name: aiproxy + restart: unless-stopped + depends_on: + aiproxy_pg: + condition: service_healthy + networks: + - fastgpt + - aiproxy + environment: + # 对应 fastgpt 里的AIPROXY_API_TOKEN + ADMIN_KEY: *x-aiproxy-token + # 错误日志详情保存时间(小时) + LOG_DETAIL_STORAGE_HOURS: 1 + # 数据库连接地址 + SQL_DSN: postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy + # 最大重试次数 + RETRY_TIMES: 3 + # 不需要计费 + BILLING_ENABLED: false + # 不需要严格检测模型 + DISABLE_MODEL_CONFIG: true + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status'] + interval: 5s + timeout: 5s + retries: 10 + aiproxy_pg: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:0.8.0-pg15 # docker hub + restart: unless-stopped + container_name: aiproxy_pg + volumes: + - ./aiproxy_pg:/var/lib/postgresql/data + networks: + - aiproxy + environment: + TZ: Asia/Shanghai + POSTGRES_USER: postgres + POSTGRES_DB: aiproxy + POSTGRES_PASSWORD: aiproxy + healthcheck: + test: ['CMD', 'pg_isready', '-U', 'postgres', '-d', 'aiproxy'] + interval: 5s + timeout: 5s + retries: 10 +networks: + fastgpt: + aiproxy: + vector: diff --git a/deploy/docker/global/docker-compose.seekdb.yml b/deploy/docker/global/docker-compose.seekdb.yml new file mode 100644 index 0000000000..43b59de373 --- /dev/null +++ b/deploy/docker/global/docker-compose.seekdb.yml @@ -0,0 +1,283 @@ +# 用于部署的 docker-compose 文件: +# - FastGPT 端口映射为 3000:3000 +# - FastGPT-mcp-server 端口映射 3005:3000 +# - 建议修改账密后再运行 + +# plugin auth token +x-plugin-auth-token: &x-plugin-auth-token 'token' +# aiproxy token +x-aiproxy-token: &x-aiproxy-token 'token' +# 数据库连接相关配置 +x-share-db-config: &x-share-db-config + MONGODB_URI: mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin + DB_MAX_LINK: 100 + REDIS_URL: redis://default:mypassword@redis:6379 + # @see https://fastgpt.cn/docs/introduction/development/object-storage + STORAGE_VENDOR: minio # minio | aws-s3 | cos | oss + STORAGE_REGION: us-east-1 + STORAGE_ACCESS_KEY_ID: minioadmin + STORAGE_SECRET_ACCESS_KEY: minioadmin + STORAGE_PUBLIC_BUCKET: fastgpt-public + STORAGE_PRIVATE_BUCKET: fastgpt-private + STORAGE_EXTERNAL_ENDPOINT: http://192.168.0.2:9000 # 一个服务器和客户端均可访问到存储桶的地址,可以是固定的宿主机 IP 或者域名,注意不要填写成 127.0.0.1 或者 localhost 等本地回环地址(因为容器里无法使用) + STORAGE_S3_ENDPOINT: http://fastgpt-minio:9000 # 协议://域名(IP):端口 + STORAGE_S3_FORCE_PATH_STYLE: true + STORAGE_S3_MAX_RETRIES: 3 + +# 向量库相关配置 +x-vec-config: &x-vec-config + SEEKDB_URL: mysql://root%40tenantname:tenantpassword@seekdb:3306/fastgpt + + +version: '3.3' +services: + # Vector DB + vectorDB: + image: oceanbase/seekdb:1.0.1.0-100000392025122619 + container_name: seekdb + restart: always + # ports: # 生产环境建议不要暴露 + # - 3306:3306 + networks: + - fastgpt + environment: + # SeekDB 连接配置(兼容 MySQL 协议) + - MYSQL_ROOT_PASSWORD=seekdbpassword + # SeekDB 租户配置(与 OceanBase 兼容) + - OB_TENANT_NAME=tenantname + - OB_TENANT_PASSWORD=tenantpassword + # MODE分为MINI和NORMAL, 后者会最大程度使用主机资源 + - MODE=MINI + - OB_SERVER_IP=127.0.0.1 + volumes: + - ../seekdb/data:/var/lib/mysql + - ../seekdb/config:/etc/mysql/conf.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 1000 + start_period: 10s + + + mongo: + image: mongo:5.0.32 # cpu 不支持 AVX 时候使用 4.4.29 + container_name: mongo + restart: always + networks: + - fastgpt + command: mongod --keyFile /data/mongodb.key --replSet rs0 + environment: + - MONGO_INITDB_ROOT_USERNAME=myusername + - MONGO_INITDB_ROOT_PASSWORD=mypassword + volumes: + - ./mongo/data:/data/db + healthcheck: + test: ['CMD', 'mongo', '-u', 'myusername', '-p', 'mypassword', '--authenticationDatabase', 'admin', '--eval', "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + entrypoint: + - bash + - -c + - | + openssl rand -base64 128 > /data/mongodb.key + chmod 400 /data/mongodb.key + chown 999:999 /data/mongodb.key + echo 'const isInited = rs.status().ok === 1 + if(!isInited){ + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongo:27017" } + ] + }) + }' > /data/initReplicaSet.js + # 启动MongoDB服务 + exec docker-entrypoint.sh "$$@" & + + # 等待MongoDB服务启动 + until mongo -u myusername -p mypassword --authenticationDatabase admin --eval "print('waited for connection')"; do + echo "Waiting for MongoDB to start..." + sleep 2 + done + + # 执行初始化副本集的脚本 + mongo -u myusername -p mypassword --authenticationDatabase admin /data/initReplicaSet.js + + # 等待docker-entrypoint.sh脚本执行的MongoDB服务进程 + wait $$! + redis: + image: redis:7.2-alpine + container_name: redis + networks: + - fastgpt + restart: always + command: | + redis-server --requirepass mypassword --loglevel warning --maxclients 10000 --appendonly yes --save 60 10 --maxmemory 4gb --maxmemory-policy noeviction + healthcheck: + test: ['CMD', 'redis-cli', '-a', 'mypassword', 'ping'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + volumes: + - ./redis/data:/data + fastgpt-minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + container_name: fastgpt-minio + restart: always + ports: + - 9000:9000 + - 9001:9001 + networks: + - fastgpt + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - ./fastgpt-minio:/data + command: server /data --console-address ":9001" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] + interval: 30s + timeout: 20s + retries: 3 + + fastgpt: + container_name: fastgpt + image: ghcr.io/labring/fastgpt:v4.14.5.1 # git + ports: + - 3000:3000 + networks: + - fastgpt + depends_on: + - mongo + - sandbox + - vectorDB + restart: always + environment: + <<: [*x-share-db-config, *x-vec-config] + # 前端外部可访问的地址,用于自动补全文件资源路径。例如 https:fastgpt.cn,不能填 localhost。这个值可以不填,不填则发给模型的图片会是一个相对路径,而不是全路径,模型可能伪造Host。 + FE_DOMAIN: + # root 密码,用户名为: root。如果需要修改 root 密码,直接修改这个环境变量,并重启即可。 + DEFAULT_ROOT_PSW: 1234 + # 登录凭证密钥 + TOKEN_KEY: any + # root的密钥,常用于升级时候的初始化请求 + ROOT_KEY: root_key + # 文件阅读加密 + FILE_TOKEN_KEY: filetoken + # 密钥加密key + AES256_SECRET_KEY: fastgptkey + + # plugin 地址 + PLUGIN_BASE_URL: http://fastgpt-plugin:3000 + PLUGIN_TOKEN: *x-plugin-auth-token + # sandbox 地址 + SANDBOX_URL: http://sandbox:3000 + # AI Proxy 的地址,如果配了该地址,优先使用 + AIPROXY_API_ENDPOINT: http://aiproxy:3000 + # AI Proxy 的 Admin Token,与 AI Proxy 中的环境变量 ADMIN_KEY + AIPROXY_API_TOKEN: *x-aiproxy-token + + # 日志等级: debug, info, warn, error + LOG_LEVEL: info + STORE_LOG_LEVEL: warn + # 工作流最大运行次数 + WORKFLOW_MAX_RUN_TIMES: 1000 + # 批量执行节点,最大输入长度 + WORKFLOW_MAX_LOOP_TIMES: 100 + # 对话文件过期天数 + CHAT_FILE_EXPIRE_TIME: 7 + # 服务器接收请求,最大大小,单位 MB + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # HTML 转换最大字符数 + MAX_HTML_TRANSFORM_CHARS: 1000000 + volumes: + - ./config.json:/app/data/config.json + sandbox: + container_name: sandbox + image: ghcr.io/labring/fastgpt-sandbox:v4.14.5.1 + networks: + - fastgpt + restart: always + fastgpt-mcp-server: + container_name: fastgpt-mcp-server + image: ghcr.io/labring/fastgpt-mcp_server:v4.14.5.1 + networks: + - fastgpt + ports: + - 3005:3000 + restart: always + environment: + - FASTGPT_ENDPOINT=http://fastgpt:3000 + fastgpt-plugin: + image: ghcr.io/labring/fastgpt-plugin:v0.4.0 + container_name: fastgpt-plugin + restart: always + networks: + - fastgpt + environment: + <<: *x-share-db-config + AUTH_TOKEN: *x-plugin-auth-token + # 工具网络请求,最大请求和响应体 + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # 最大 API 请求体大小 + MAX_API_SIZE: 10 + depends_on: + fastgpt-minio: + condition: service_healthy + # AI Proxy + aiproxy: + image: ghcr.io/labring/aiproxy:v0.3.2 + container_name: aiproxy + restart: unless-stopped + depends_on: + aiproxy_pg: + condition: service_healthy + networks: + - fastgpt + - aiproxy + environment: + # 对应 fastgpt 里的AIPROXY_API_TOKEN + ADMIN_KEY: *x-aiproxy-token + # 错误日志详情保存时间(小时) + LOG_DETAIL_STORAGE_HOURS: 1 + # 数据库连接地址 + SQL_DSN: postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy + # 最大重试次数 + RETRY_TIMES: 3 + # 不需要计费 + BILLING_ENABLED: false + # 不需要严格检测模型 + DISABLE_MODEL_CONFIG: true + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status'] + interval: 5s + timeout: 5s + retries: 10 + aiproxy_pg: + image: pgvector/pgvector:0.8.0-pg15 # docker hub + restart: unless-stopped + container_name: aiproxy_pg + volumes: + - ./aiproxy_pg:/var/lib/postgresql/data + networks: + - aiproxy + environment: + TZ: Asia/Shanghai + POSTGRES_USER: postgres + POSTGRES_DB: aiproxy + POSTGRES_PASSWORD: aiproxy + healthcheck: + test: ['CMD', 'pg_isready', '-U', 'postgres', '-d', 'aiproxy'] + interval: 5s + timeout: 5s + retries: 10 +networks: + fastgpt: + aiproxy: + vector: + diff --git a/deploy/init.mjs b/deploy/init.mjs index cc95986b98..0fac931356 100644 --- a/deploy/init.mjs +++ b/deploy/init.mjs @@ -17,28 +17,8 @@ const VectorEnum = { pg: 'pg', milvus: 'milvus', zilliz: 'zilliz', - ob: 'ob' -}; - -/** - * @enum {string} Services - */ -const Services = { - fastgpt: 'fastgpt', - fastgptPlugin: 'fastgpt-plugin', - fastgptSandbox: 'fastgpt-sandbox', - fastgptMcpServer: 'fastgpt-mcp_server', - minio: 'minio', - mongo: 'mongo', - redis: 'redis', - aiproxy: 'aiproxy', - aiproxyPg: 'aiproxy-pg', - // vectors - pg: 'pg', - milvusMinio: 'milvus-minio', - milvusEtcd: 'milvus-etcd', - milvusStandalone: 'milvus-standalone', - oceanbase: 'oceanbase' + ob: 'ob', + seekdb: 'seekdb' }; // make sure the cwd @@ -105,7 +85,14 @@ configs: content: | ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30; ` - } + }, + seekdb: { + db: '', + config: `\ + SEEKDB_URL: mysql://root%40tenantname:tenantpassword@seekdb:3306/fastgpt +`, + extra: `` + }, }; /** @@ -148,6 +135,9 @@ const replace = (source, region, vec) => { const ob = fs.readFileSync(path.join(process.cwd(), 'templates', 'vector', 'ob.txt')); vector.ob.db = String(ob); + + const seekdb = fs.readFileSync(path.join(process.cwd(), 'templates', 'vector', 'seekdb.txt')); + vector.seekdb.db = String(seekdb); } const generateDevFile = async () => { @@ -211,6 +201,14 @@ const generateProdFile = async () => { fs.promises.writeFile( path.join(process.cwd(), 'docker', 'global', 'docker-compose.oceanbase.yml'), replace(template, 'global', VectorEnum.ob) + ), + fs.promises.writeFile( + path.join(process.cwd(), 'docker', 'cn', 'docker-compose.seekdb.yml'), + replace(template, 'cn', VectorEnum.seekdb) + ), + fs.promises.writeFile( + path.join(process.cwd(), 'docker', 'global', 'docker-compose.seekdb.yml'), + replace(template, 'global', VectorEnum.seekdb) ) ]); diff --git a/deploy/templates/docker-compose.prod.yml b/deploy/templates/docker-compose.prod.yml index c5edfaeca5..55c7486c10 100644 --- a/deploy/templates/docker-compose.prod.yml +++ b/deploy/templates/docker-compose.prod.yml @@ -12,7 +12,7 @@ x-share-db-config: &x-share-db-config MONGODB_URI: mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin DB_MAX_LINK: 100 REDIS_URL: redis://default:mypassword@redis:6379 - # @see https://fastgpt.cn/docs/introduction/development/object-storage + # @see https://doc.fastgpt.cn/docs/introduction/development/object-storage STORAGE_VENDOR: minio # minio | aws-s3 | cos | oss STORAGE_REGION: us-east-1 STORAGE_ACCESS_KEY_ID: minioadmin diff --git a/deploy/templates/vector/seekdb.txt b/deploy/templates/vector/seekdb.txt new file mode 100644 index 0000000000..2375bb057f --- /dev/null +++ b/deploy/templates/vector/seekdb.txt @@ -0,0 +1,26 @@ + vectorDB: + image: ${{seekdb.image}}:${{seekdb.tag}} + container_name: seekdb + restart: always + # ports: # 生产环境建议不要暴露 + # - 3306:3306 + networks: + - fastgpt + environment: + # SeekDB 连接配置(兼容 MySQL 协议) + - MYSQL_ROOT_PASSWORD=seekdbpassword + # SeekDB 租户配置(与 OceanBase 兼容) + - OB_TENANT_NAME=tenantname + - OB_TENANT_PASSWORD=tenantpassword + # MODE分为MINI和NORMAL, 后者会最大程度使用主机资源 + - MODE=MINI + - OB_SERVER_IP=127.0.0.1 + volumes: + - ../seekdb/data:/var/lib/mysql + - ../seekdb/config:/etc/mysql/conf.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 1000 + start_period: 10s diff --git a/document/content/docs/introduction/development/docker.mdx b/document/content/docs/introduction/development/docker.mdx index c57dc6f77e..f6b8f2a2b8 100644 --- a/document/content/docs/introduction/development/docker.mdx +++ b/document/content/docs/introduction/development/docker.mdx @@ -19,7 +19,7 @@ import { Alert } from '@/components/docs/Alert'; - MongoDB:用于存储除了向量外的各类数据 -- PostgreSQL/Milvus/Oceanbase:存储向量数据 +- PostgreSQL/Milvus/Oceanbase/SeekDB:存储向量数据 - AIProxy: 聚合各类 AI API,支持多模型调用 (任何模型问题,先自行通过 OneAPI 测试校验) @@ -54,6 +54,26 @@ Zilliz Cloud 由 Milvus 原厂打造,是全托管的 SaaS 向量数据库服 由于向量库使用了 Cloud,无需占用本地资源,无需太关注。 +### SeekDB版本 + +SeekDB 是基于 MySQL 协议的高性能向量数据库,与 OceanBase 协议完全兼容,支持高效的向量检索。 + +| 环境 | 最低配置(单节点) | 推荐配置 | +| -------------------------------- | ------------------ | ------------ | +| 测试(可以把计算进程设置少一些) | 2c4g | 2c8g | +| 100w 组向量 | 4c8g 50GB | 4c16g 50GB | +| 500w 组向量 | 8c32g 200GB | 16c64g 200GB | + + + +SeekDB 使用 MySQL 协议,与 OceanBase 完全兼容: +- 支持 1536 维向量检索 +- 内置 HNSW 索引算法 +- 提供批量插入和查询优化 +- 自动重试和连接池管理 + + + ## 前置工作 ### 1. 确保网络环境 @@ -103,7 +123,7 @@ brew install orbstack #### 方法一:使用脚本部署 - + 国内镜像(阿里云) @@ -163,6 +183,29 @@ bash <(curl -fsSL https://doc.fastgpt.cn/deploy/install.sh) --region=global --ve zilliz 还需要获取密钥,参考 [部署 Zilliz 版本获取账号和密钥](#部署-zilliz-版本获取账号和密钥) + + 国内镜像(阿里云) + + ```bash + bash <(curl -fsSL https://doc.fastgpt.cn/deploy/install.sh) --region=cn --vector=seekdb + ``` + + 非国内镜像(dockhub, ghcr) + + ```bash + bash <(curl -fsSL https://doc.fastgpt.cn/deploy/install.sh) --region=global --vector=seekdb + ``` + 需要在 Linux/MacOS/Windows WSL 环境下执行 + + + + SeekDB 使用 MySQL 协议,兼容 OceanBase 的所有特性: + - 端口:3306(默认) + - 连接字符串格式:`mysql://root%40tenantname:password@host:3306/database` + - 环境变量:`SEEKDB_URL` + + + #### 方法二:手动下载部署 @@ -181,6 +224,9 @@ bash <(curl -fsSL https://doc.fastgpt.cn/deploy/install.sh) --region=global --ve - Zilliz - 中国大陆地区镜像源(阿里云):[docker-compose.zilliz.yml](https://doc.fastgpt.cn/deploy/docker/cn/docker-compose.zilliz.yml) - 全球镜像源(dockerhub, ghcr):[docker-compose.zilliz.yml](https://doc.fastgpt.cn/deploy/docker/global/docker-compose.zilliz.yml) +- SeekDB + - 中国大陆地区镜像源(阿里云):[docker-compose.seekdb.yml](https://doc.fastgpt.cn/deploy/docker/cn/docker-compose.seekdb.yml) + - 全球镜像源(dockerhub, ghcr):[docker-compose.seekdb.yml](https://doc.fastgpt.cn/deploy/docker/global/docker-compose.seekdb.yml) 下载 config.json 文件 - [config.json](https://doc.fastgpt.cn/deploy/config/config.json) diff --git a/document/content/docs/upgrading/4-14/4146.mdx b/document/content/docs/upgrading/4-14/4146.mdx index 482051a4bf..d16cc90497 100644 --- a/document/content/docs/upgrading/4-14/4146.mdx +++ b/document/content/docs/upgrading/4-14/4146.mdx @@ -3,43 +3,49 @@ title: 'V4.14.6(进行中)' description: 'FastGPT V4.14.6 更新说明' --- - -## 🚀 新增内容 - -1. Markdown 表格支持导出 csv。 -2. 系统工具可配置自定义的分类属性 +## 更新指南 ### 1. 更新镜像: -- 更新 FastGPT 镜像tag: v4.14.6 -- 更新 FastGPT 商业版镜像tag: v4.14.6 +- 更新 FastGPT 镜像 tag: v4.14.6 +- 更新 FastGPT 商业版镜像 tag: v4.14.6 - 更新 fastgpt-plugin 镜像 tag: v0.4.0 - mcp_server 无需更新 -- Sandbox 无需更新 +- sandbox 无需更新 - AIProxy 无需更新 -- mongo 5.x 版本修改成 5.0.32 版本,解决 CVE-2025-14847 漏洞。直接修改镜像 tag 成 `5.0.32`。 +- mongo 无需更新 -### 2. 执行升级脚本 +## 🚀 新增内容 -从任意终端,发起 1 个 HTTP 请求。其中 `{{rootkey}}` 替换成环境变量里的 `rootkey`;`{{host}}` 替换成**FastGPT 域名**。 +1. 系统工具可配置自定义的分类属性。 +2. 订阅套餐支持配置最大文件上传数量和大小。 +3. 插件市场支持批量更新插件。 +4. 云服务支持企微特定版接入。 +5. Seekdb 向量库预设配置。 -```bash -curl --location --request POST 'https://{{host}}/api/admin/initv4146' \ ---header 'rootkey: {{rootkey}}' \ ---header 'Content-Type: application/json' -``` -1. 迁移系统工具的系统密钥配置 - ## ⚙️ 优化 +### 功能优化 1. 工作流触摸板移动时,遇到输入框后会被强制阻拦。 2. 工作流粘贴节点,精确按鼠标位置粘贴。 3. 精确移除请求 LLM 时多余的系统字段,避免部分模型接口报错。 +### 代码质量 + +1. useRequest2 替代 useRequest。 + + ## 🐛 修复 1. 系统工具工具集设置系统密钥后,子工具无法读取到设置的系统密钥 2. 日期选择器溢出问题,增加了动态位置适配。 +3. 工作流编排页面系统工具“探索更多”跳转地址错误 +4. 模型头像缺省值 /imgs/model/huggingface.svg 路径错误 ## 插件 + +1. 添加飞书多维表格的引导教程文档 +2. 企微相关的插件:获取企微企业 access_token; 企微智能表工具集 +3. 新增模型 qwen-flash +4. 调整 qwen3-max 和 qwen-plus 的预设参数 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 6b204f562e..ab067ef04d 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -20,7 +20,7 @@ "document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/design/dataset.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-11-06T14:47:55+08:00", - "document/content/docs/introduction/development/docker.mdx": "2026-01-09T18:25:02+08:00", + "document/content/docs/introduction/development/docker.mdx": "2026-01-30T16:01:13+08:00", "document/content/docs/introduction/development/faq.mdx": "2025-08-12T22:22:18+08:00", "document/content/docs/introduction/development/intro.mdx": "2025-09-29T11:34:11+08:00", "document/content/docs/introduction/development/migration/docker_db.mdx": "2025-07-23T21:35:03+08:00", @@ -124,7 +124,7 @@ "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00", "document/content/docs/upgrading/4-14/4145.mdx": "2026-01-18T23:59:15+08:00", "document/content/docs/upgrading/4-14/41451.mdx": "2026-01-20T11:53:27+08:00", - "document/content/docs/upgrading/4-14/4146.mdx": "2026-01-27T13:43:06+08:00", + "document/content/docs/upgrading/4-14/4146.mdx": "2026-01-26T17:46:44+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", @@ -198,7 +198,7 @@ "document/content/docs/use-cases/app-cases/google_search.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2025-12-10T20:07:05+08:00", "document/content/docs/use-cases/app-cases/multi_turn_translation_bot.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/use-cases/app-cases/submit_application_template.mdx": "2025-08-05T23:20:39+08:00", + "document/content/docs/use-cases/app-cases/submit_application_template.mdx": "2026-01-27T15:19:19+08:00", "document/content/docs/use-cases/app-cases/translate-subtitle-using-gpt.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/external-integration/dingtalk.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00", diff --git a/document/public/deploy/docker/cn/docker-compose.seekdb.yml b/document/public/deploy/docker/cn/docker-compose.seekdb.yml new file mode 100644 index 0000000000..3f61787d94 --- /dev/null +++ b/document/public/deploy/docker/cn/docker-compose.seekdb.yml @@ -0,0 +1,283 @@ +# 用于部署的 docker-compose 文件: +# - FastGPT 端口映射为 3000:3000 +# - FastGPT-mcp-server 端口映射 3005:3000 +# - 建议修改账密后再运行 + +# plugin auth token +x-plugin-auth-token: &x-plugin-auth-token 'token' +# aiproxy token +x-aiproxy-token: &x-aiproxy-token 'token' +# 数据库连接相关配置 +x-share-db-config: &x-share-db-config + MONGODB_URI: mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin + DB_MAX_LINK: 100 + REDIS_URL: redis://default:mypassword@redis:6379 + # @see https://fastgpt.cn/docs/introduction/development/object-storage + STORAGE_VENDOR: minio # minio | aws-s3 | cos | oss + STORAGE_REGION: us-east-1 + STORAGE_ACCESS_KEY_ID: minioadmin + STORAGE_SECRET_ACCESS_KEY: minioadmin + STORAGE_PUBLIC_BUCKET: fastgpt-public + STORAGE_PRIVATE_BUCKET: fastgpt-private + STORAGE_EXTERNAL_ENDPOINT: http://192.168.0.2:9000 # 一个服务器和客户端均可访问到存储桶的地址,可以是固定的宿主机 IP 或者域名,注意不要填写成 127.0.0.1 或者 localhost 等本地回环地址(因为容器里无法使用) + STORAGE_S3_ENDPOINT: http://fastgpt-minio:9000 # 协议://域名(IP):端口 + STORAGE_S3_FORCE_PATH_STYLE: true + STORAGE_S3_MAX_RETRIES: 3 + +# 向量库相关配置 +x-vec-config: &x-vec-config + SEEKDB_URL: mysql://root%40tenantname:tenantpassword@seekdb:3306/fastgpt + + +version: '3.3' +services: + # Vector DB + vectorDB: + image: oceanbase/seekdb:1.0.1.0-100000392025122619 + container_name: seekdb + restart: always + # ports: # 生产环境建议不要暴露 + # - 3306:3306 + networks: + - fastgpt + environment: + # SeekDB 连接配置(兼容 MySQL 协议) + - MYSQL_ROOT_PASSWORD=seekdbpassword + # SeekDB 租户配置(与 OceanBase 兼容) + - OB_TENANT_NAME=tenantname + - OB_TENANT_PASSWORD=tenantpassword + # MODE分为MINI和NORMAL, 后者会最大程度使用主机资源 + - MODE=MINI + - OB_SERVER_IP=127.0.0.1 + volumes: + - ../seekdb/data:/var/lib/mysql + - ../seekdb/config:/etc/mysql/conf.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 1000 + start_period: 10s + + + mongo: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/mongo:5.0.32 # cpu 不支持 AVX 时候使用 4.4.29 + container_name: mongo + restart: always + networks: + - fastgpt + command: mongod --keyFile /data/mongodb.key --replSet rs0 + environment: + - MONGO_INITDB_ROOT_USERNAME=myusername + - MONGO_INITDB_ROOT_PASSWORD=mypassword + volumes: + - ./mongo/data:/data/db + healthcheck: + test: ['CMD', 'mongo', '-u', 'myusername', '-p', 'mypassword', '--authenticationDatabase', 'admin', '--eval', "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + entrypoint: + - bash + - -c + - | + openssl rand -base64 128 > /data/mongodb.key + chmod 400 /data/mongodb.key + chown 999:999 /data/mongodb.key + echo 'const isInited = rs.status().ok === 1 + if(!isInited){ + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongo:27017" } + ] + }) + }' > /data/initReplicaSet.js + # 启动MongoDB服务 + exec docker-entrypoint.sh "$$@" & + + # 等待MongoDB服务启动 + until mongo -u myusername -p mypassword --authenticationDatabase admin --eval "print('waited for connection')"; do + echo "Waiting for MongoDB to start..." + sleep 2 + done + + # 执行初始化副本集的脚本 + mongo -u myusername -p mypassword --authenticationDatabase admin /data/initReplicaSet.js + + # 等待docker-entrypoint.sh脚本执行的MongoDB服务进程 + wait $$! + redis: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/redis:7.2-alpine + container_name: redis + networks: + - fastgpt + restart: always + command: | + redis-server --requirepass mypassword --loglevel warning --maxclients 10000 --appendonly yes --save 60 10 --maxmemory 4gb --maxmemory-policy noeviction + healthcheck: + test: ['CMD', 'redis-cli', '-a', 'mypassword', 'ping'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + volumes: + - ./redis/data:/data + fastgpt-minio: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/minio:RELEASE.2025-09-07T16-13-09Z + container_name: fastgpt-minio + restart: always + ports: + - 9000:9000 + - 9001:9001 + networks: + - fastgpt + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - ./fastgpt-minio:/data + command: server /data --console-address ":9001" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] + interval: 30s + timeout: 20s + retries: 3 + + fastgpt: + container_name: fastgpt + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.5.1 # git + ports: + - 3000:3000 + networks: + - fastgpt + depends_on: + - mongo + - sandbox + - vectorDB + restart: always + environment: + <<: [*x-share-db-config, *x-vec-config] + # 前端外部可访问的地址,用于自动补全文件资源路径。例如 https:fastgpt.cn,不能填 localhost。这个值可以不填,不填则发给模型的图片会是一个相对路径,而不是全路径,模型可能伪造Host。 + FE_DOMAIN: + # root 密码,用户名为: root。如果需要修改 root 密码,直接修改这个环境变量,并重启即可。 + DEFAULT_ROOT_PSW: 1234 + # 登录凭证密钥 + TOKEN_KEY: any + # root的密钥,常用于升级时候的初始化请求 + ROOT_KEY: root_key + # 文件阅读加密 + FILE_TOKEN_KEY: filetoken + # 密钥加密key + AES256_SECRET_KEY: fastgptkey + + # plugin 地址 + PLUGIN_BASE_URL: http://fastgpt-plugin:3000 + PLUGIN_TOKEN: *x-plugin-auth-token + # sandbox 地址 + SANDBOX_URL: http://sandbox:3000 + # AI Proxy 的地址,如果配了该地址,优先使用 + AIPROXY_API_ENDPOINT: http://aiproxy:3000 + # AI Proxy 的 Admin Token,与 AI Proxy 中的环境变量 ADMIN_KEY + AIPROXY_API_TOKEN: *x-aiproxy-token + + # 日志等级: debug, info, warn, error + LOG_LEVEL: info + STORE_LOG_LEVEL: warn + # 工作流最大运行次数 + WORKFLOW_MAX_RUN_TIMES: 1000 + # 批量执行节点,最大输入长度 + WORKFLOW_MAX_LOOP_TIMES: 100 + # 对话文件过期天数 + CHAT_FILE_EXPIRE_TIME: 7 + # 服务器接收请求,最大大小,单位 MB + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # HTML 转换最大字符数 + MAX_HTML_TRANSFORM_CHARS: 1000000 + volumes: + - ./config.json:/app/data/config.json + sandbox: + container_name: sandbox + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.5.1 + networks: + - fastgpt + restart: always + fastgpt-mcp-server: + container_name: fastgpt-mcp-server + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-mcp_server:v4.14.5.1 + networks: + - fastgpt + ports: + - 3005:3000 + restart: always + environment: + - FASTGPT_ENDPOINT=http://fastgpt:3000 + fastgpt-plugin: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-plugin:v0.4.0 + container_name: fastgpt-plugin + restart: always + networks: + - fastgpt + environment: + <<: *x-share-db-config + AUTH_TOKEN: *x-plugin-auth-token + # 工具网络请求,最大请求和响应体 + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # 最大 API 请求体大小 + MAX_API_SIZE: 10 + depends_on: + fastgpt-minio: + condition: service_healthy + # AI Proxy + aiproxy: + image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.3.2 + container_name: aiproxy + restart: unless-stopped + depends_on: + aiproxy_pg: + condition: service_healthy + networks: + - fastgpt + - aiproxy + environment: + # 对应 fastgpt 里的AIPROXY_API_TOKEN + ADMIN_KEY: *x-aiproxy-token + # 错误日志详情保存时间(小时) + LOG_DETAIL_STORAGE_HOURS: 1 + # 数据库连接地址 + SQL_DSN: postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy + # 最大重试次数 + RETRY_TIMES: 3 + # 不需要计费 + BILLING_ENABLED: false + # 不需要严格检测模型 + DISABLE_MODEL_CONFIG: true + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status'] + interval: 5s + timeout: 5s + retries: 10 + aiproxy_pg: + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/pgvector:0.8.0-pg15 # docker hub + restart: unless-stopped + container_name: aiproxy_pg + volumes: + - ./aiproxy_pg:/var/lib/postgresql/data + networks: + - aiproxy + environment: + TZ: Asia/Shanghai + POSTGRES_USER: postgres + POSTGRES_DB: aiproxy + POSTGRES_PASSWORD: aiproxy + healthcheck: + test: ['CMD', 'pg_isready', '-U', 'postgres', '-d', 'aiproxy'] + interval: 5s + timeout: 5s + retries: 10 +networks: + fastgpt: + aiproxy: + vector: + diff --git a/document/public/deploy/docker/global/docker-compose.seekdb.yml b/document/public/deploy/docker/global/docker-compose.seekdb.yml new file mode 100644 index 0000000000..43b59de373 --- /dev/null +++ b/document/public/deploy/docker/global/docker-compose.seekdb.yml @@ -0,0 +1,283 @@ +# 用于部署的 docker-compose 文件: +# - FastGPT 端口映射为 3000:3000 +# - FastGPT-mcp-server 端口映射 3005:3000 +# - 建议修改账密后再运行 + +# plugin auth token +x-plugin-auth-token: &x-plugin-auth-token 'token' +# aiproxy token +x-aiproxy-token: &x-aiproxy-token 'token' +# 数据库连接相关配置 +x-share-db-config: &x-share-db-config + MONGODB_URI: mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin + DB_MAX_LINK: 100 + REDIS_URL: redis://default:mypassword@redis:6379 + # @see https://fastgpt.cn/docs/introduction/development/object-storage + STORAGE_VENDOR: minio # minio | aws-s3 | cos | oss + STORAGE_REGION: us-east-1 + STORAGE_ACCESS_KEY_ID: minioadmin + STORAGE_SECRET_ACCESS_KEY: minioadmin + STORAGE_PUBLIC_BUCKET: fastgpt-public + STORAGE_PRIVATE_BUCKET: fastgpt-private + STORAGE_EXTERNAL_ENDPOINT: http://192.168.0.2:9000 # 一个服务器和客户端均可访问到存储桶的地址,可以是固定的宿主机 IP 或者域名,注意不要填写成 127.0.0.1 或者 localhost 等本地回环地址(因为容器里无法使用) + STORAGE_S3_ENDPOINT: http://fastgpt-minio:9000 # 协议://域名(IP):端口 + STORAGE_S3_FORCE_PATH_STYLE: true + STORAGE_S3_MAX_RETRIES: 3 + +# 向量库相关配置 +x-vec-config: &x-vec-config + SEEKDB_URL: mysql://root%40tenantname:tenantpassword@seekdb:3306/fastgpt + + +version: '3.3' +services: + # Vector DB + vectorDB: + image: oceanbase/seekdb:1.0.1.0-100000392025122619 + container_name: seekdb + restart: always + # ports: # 生产环境建议不要暴露 + # - 3306:3306 + networks: + - fastgpt + environment: + # SeekDB 连接配置(兼容 MySQL 协议) + - MYSQL_ROOT_PASSWORD=seekdbpassword + # SeekDB 租户配置(与 OceanBase 兼容) + - OB_TENANT_NAME=tenantname + - OB_TENANT_PASSWORD=tenantpassword + # MODE分为MINI和NORMAL, 后者会最大程度使用主机资源 + - MODE=MINI + - OB_SERVER_IP=127.0.0.1 + volumes: + - ../seekdb/data:/var/lib/mysql + - ../seekdb/config:/etc/mysql/conf.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 1000 + start_period: 10s + + + mongo: + image: mongo:5.0.32 # cpu 不支持 AVX 时候使用 4.4.29 + container_name: mongo + restart: always + networks: + - fastgpt + command: mongod --keyFile /data/mongodb.key --replSet rs0 + environment: + - MONGO_INITDB_ROOT_USERNAME=myusername + - MONGO_INITDB_ROOT_PASSWORD=mypassword + volumes: + - ./mongo/data:/data/db + healthcheck: + test: ['CMD', 'mongo', '-u', 'myusername', '-p', 'mypassword', '--authenticationDatabase', 'admin', '--eval', "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + entrypoint: + - bash + - -c + - | + openssl rand -base64 128 > /data/mongodb.key + chmod 400 /data/mongodb.key + chown 999:999 /data/mongodb.key + echo 'const isInited = rs.status().ok === 1 + if(!isInited){ + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongo:27017" } + ] + }) + }' > /data/initReplicaSet.js + # 启动MongoDB服务 + exec docker-entrypoint.sh "$$@" & + + # 等待MongoDB服务启动 + until mongo -u myusername -p mypassword --authenticationDatabase admin --eval "print('waited for connection')"; do + echo "Waiting for MongoDB to start..." + sleep 2 + done + + # 执行初始化副本集的脚本 + mongo -u myusername -p mypassword --authenticationDatabase admin /data/initReplicaSet.js + + # 等待docker-entrypoint.sh脚本执行的MongoDB服务进程 + wait $$! + redis: + image: redis:7.2-alpine + container_name: redis + networks: + - fastgpt + restart: always + command: | + redis-server --requirepass mypassword --loglevel warning --maxclients 10000 --appendonly yes --save 60 10 --maxmemory 4gb --maxmemory-policy noeviction + healthcheck: + test: ['CMD', 'redis-cli', '-a', 'mypassword', 'ping'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + volumes: + - ./redis/data:/data + fastgpt-minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + container_name: fastgpt-minio + restart: always + ports: + - 9000:9000 + - 9001:9001 + networks: + - fastgpt + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - ./fastgpt-minio:/data + command: server /data --console-address ":9001" + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] + interval: 30s + timeout: 20s + retries: 3 + + fastgpt: + container_name: fastgpt + image: ghcr.io/labring/fastgpt:v4.14.5.1 # git + ports: + - 3000:3000 + networks: + - fastgpt + depends_on: + - mongo + - sandbox + - vectorDB + restart: always + environment: + <<: [*x-share-db-config, *x-vec-config] + # 前端外部可访问的地址,用于自动补全文件资源路径。例如 https:fastgpt.cn,不能填 localhost。这个值可以不填,不填则发给模型的图片会是一个相对路径,而不是全路径,模型可能伪造Host。 + FE_DOMAIN: + # root 密码,用户名为: root。如果需要修改 root 密码,直接修改这个环境变量,并重启即可。 + DEFAULT_ROOT_PSW: 1234 + # 登录凭证密钥 + TOKEN_KEY: any + # root的密钥,常用于升级时候的初始化请求 + ROOT_KEY: root_key + # 文件阅读加密 + FILE_TOKEN_KEY: filetoken + # 密钥加密key + AES256_SECRET_KEY: fastgptkey + + # plugin 地址 + PLUGIN_BASE_URL: http://fastgpt-plugin:3000 + PLUGIN_TOKEN: *x-plugin-auth-token + # sandbox 地址 + SANDBOX_URL: http://sandbox:3000 + # AI Proxy 的地址,如果配了该地址,优先使用 + AIPROXY_API_ENDPOINT: http://aiproxy:3000 + # AI Proxy 的 Admin Token,与 AI Proxy 中的环境变量 ADMIN_KEY + AIPROXY_API_TOKEN: *x-aiproxy-token + + # 日志等级: debug, info, warn, error + LOG_LEVEL: info + STORE_LOG_LEVEL: warn + # 工作流最大运行次数 + WORKFLOW_MAX_RUN_TIMES: 1000 + # 批量执行节点,最大输入长度 + WORKFLOW_MAX_LOOP_TIMES: 100 + # 对话文件过期天数 + CHAT_FILE_EXPIRE_TIME: 7 + # 服务器接收请求,最大大小,单位 MB + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # HTML 转换最大字符数 + MAX_HTML_TRANSFORM_CHARS: 1000000 + volumes: + - ./config.json:/app/data/config.json + sandbox: + container_name: sandbox + image: ghcr.io/labring/fastgpt-sandbox:v4.14.5.1 + networks: + - fastgpt + restart: always + fastgpt-mcp-server: + container_name: fastgpt-mcp-server + image: ghcr.io/labring/fastgpt-mcp_server:v4.14.5.1 + networks: + - fastgpt + ports: + - 3005:3000 + restart: always + environment: + - FASTGPT_ENDPOINT=http://fastgpt:3000 + fastgpt-plugin: + image: ghcr.io/labring/fastgpt-plugin:v0.4.0 + container_name: fastgpt-plugin + restart: always + networks: + - fastgpt + environment: + <<: *x-share-db-config + AUTH_TOKEN: *x-plugin-auth-token + # 工具网络请求,最大请求和响应体 + SERVICE_REQUEST_MAX_CONTENT_LENGTH: 10 + # 最大 API 请求体大小 + MAX_API_SIZE: 10 + depends_on: + fastgpt-minio: + condition: service_healthy + # AI Proxy + aiproxy: + image: ghcr.io/labring/aiproxy:v0.3.2 + container_name: aiproxy + restart: unless-stopped + depends_on: + aiproxy_pg: + condition: service_healthy + networks: + - fastgpt + - aiproxy + environment: + # 对应 fastgpt 里的AIPROXY_API_TOKEN + ADMIN_KEY: *x-aiproxy-token + # 错误日志详情保存时间(小时) + LOG_DETAIL_STORAGE_HOURS: 1 + # 数据库连接地址 + SQL_DSN: postgres://postgres:aiproxy@aiproxy_pg:5432/aiproxy + # 最大重试次数 + RETRY_TIMES: 3 + # 不需要计费 + BILLING_ENABLED: false + # 不需要严格检测模型 + DISABLE_MODEL_CONFIG: true + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/status'] + interval: 5s + timeout: 5s + retries: 10 + aiproxy_pg: + image: pgvector/pgvector:0.8.0-pg15 # docker hub + restart: unless-stopped + container_name: aiproxy_pg + volumes: + - ./aiproxy_pg:/var/lib/postgresql/data + networks: + - aiproxy + environment: + TZ: Asia/Shanghai + POSTGRES_USER: postgres + POSTGRES_DB: aiproxy + POSTGRES_PASSWORD: aiproxy + healthcheck: + test: ['CMD', 'pg_isready', '-U', 'postgres', '-d', 'aiproxy'] + interval: 5s + timeout: 5s + retries: 10 +networks: + fastgpt: + aiproxy: + vector: + diff --git a/packages/global/common/string/time.ts b/packages/global/common/string/time.ts index cb0b7002d8..cb3fb7dadc 100644 --- a/packages/global/common/string/time.ts +++ b/packages/global/common/string/time.ts @@ -17,6 +17,12 @@ export const formatTime2YMD = (time?: Date | number) => time ? dayjs(time).format('YYYY-MM-DD') : ''; export const formatTime2HM = (time: Date = new Date()) => dayjs(time).format('HH:mm'); +/** + * 格式化为带时区偏移的 ISO-8601 字符串 + */ +export const formatToISOWithTimezone = (time?: Date | number) => + time ? dayjs(time).format('YYYY-MM-DDTHH:mm:ss.SSSZ') : ''; + /** * 格式化时间成聊天格式 */ diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 8805e84d39..d9da9d87c3 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -66,6 +66,7 @@ export type FastGPTFeConfigsType = { show_aiproxy?: boolean; show_coupon?: boolean; show_discount_coupon?: boolean; + showWecomConfig?: boolean; concatMd?: string; show_dataset_feishu?: boolean; @@ -105,14 +106,15 @@ export type FastGPTFeConfigsType = { tenantId?: string; customButton?: string; }; + wecom?: boolean; }; limit?: { exportDatasetLimitMinutes?: number; websiteSyncLimitMinuted?: number; }; - uploadFileMaxAmount?: number; - uploadFileMaxSize?: number; + uploadFileMaxAmount: number; + uploadFileMaxSize: number; // MB evalFileMaxLines?: number; // Compute by systemEnv.customPdfParse diff --git a/packages/global/core/app/tool/type.d.ts b/packages/global/core/app/tool/type.d.ts index 2553b3a22b..436d697ce6 100644 --- a/packages/global/core/app/tool/type.d.ts +++ b/packages/global/core/app/tool/type.d.ts @@ -10,6 +10,7 @@ import type { I18nStringStrictType } from '../../../common/i18n/type'; import type { I18nStringType } from '../../../common/i18n/type'; import type { ToolSimpleType, ToolDetailType } from '../../../sdk/fastgpt-plugin'; import type { PluginStatusType, SystemPluginToolTagType } from '../../plugin/type'; +import type { UserTagsEnum } from '../../../support/user/type'; export type AppToolRuntimeType = { id: string; @@ -61,6 +62,10 @@ export type AppToolTemplateItemType = WorkflowTemplateType & { inputListVal?: Record; hasSystemSecret?: boolean; + // User tag filtering + hideTags?: UserTagsEnum[] | null; + promoteTags?: UserTagsEnum[] | null; + // @deprecated use tags instead isActive?: boolean; templateType?: string; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 7f2e789d42..27835abf70 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -15,7 +15,7 @@ import type { AppPermission } from '../../support/permission/app/controller'; import type { ParentIdType } from '../../common/parentFolder/type'; import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant'; import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; -import type { SourceMemberType } from '../../support/user/type'; +import type { SourceMemberType, UserTagsEnum } from '../../support/user/type'; import type { JSONSchemaInputType, JSONSchemaOutputType } from './jsonschema'; export type AppSchema = { @@ -234,6 +234,8 @@ export type AppTemplateSchemaType = { author?: string; isActive?: boolean; isPromoted?: boolean; + promoteTags?: UserTagsEnum[]; + hideTags?: UserTagsEnum[]; recommendText?: string; userGuide?: { type: 'markdown' | 'link'; diff --git a/packages/global/core/plugin/admin/tool/type.ts b/packages/global/core/plugin/admin/tool/type.ts index 724299f038..5f93b26476 100644 --- a/packages/global/core/plugin/admin/tool/type.ts +++ b/packages/global/core/plugin/admin/tool/type.ts @@ -1,6 +1,7 @@ import { ParentIdSchema } from '../../../../common/parentFolder/type'; import { SystemToolBasicConfigSchema, ToolSecretInputItemSchema } from '../../tool/type'; import z from 'zod'; +import { UserTagsEnum } from '../../../../support/user/type'; export const AdminSystemToolListItemSchema = SystemToolBasicConfigSchema.extend({ id: z.string(), @@ -33,6 +34,8 @@ export const AdminSystemToolDetailSchema = AdminSystemToolListItemSchema.omit({ userGuide: z.string().nullish(), inputList: z.array(ToolSecretInputItemSchema).optional(), inputListVal: z.record(z.string(), z.any()).nullish(), - childTools: z.array(ToolsetChildSchema).optional() + childTools: z.array(ToolsetChildSchema).optional(), + promoteTags: z.array(UserTagsEnum).nullish().describe('对拥有这些 Tag 的用户推荐, 排序到前面'), + hideTags: z.array(UserTagsEnum).nullish().describe('对拥有这些 Tag 的用户隐藏') }); export type AdminSystemToolDetailType = z.infer; diff --git a/packages/global/core/plugin/tool/type.ts b/packages/global/core/plugin/tool/type.ts index 1ea135a797..8e302cc638 100644 --- a/packages/global/core/plugin/tool/type.ts +++ b/packages/global/core/plugin/tool/type.ts @@ -1,5 +1,6 @@ import z from 'zod'; import { PluginStatusEnum, PluginStatusSchema } from '../type'; +import { UserTagsEnum } from '../../../support/user/type'; // 无论哪种 Tool,都会有这一层配置 export const SystemToolBasicConfigSchema = z.object({ @@ -14,6 +15,8 @@ export const SystemToolBasicConfigSchema = z.object({ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.extend({ pluginId: z.string(), + promoteTags: z.array(UserTagsEnum).nullish(), + hideTags: z.array(UserTagsEnum).nullish(), customConfig: z .object({ name: z.string(), diff --git a/packages/global/openapi/core/plugin/admin/tool/api.ts b/packages/global/openapi/core/plugin/admin/tool/api.ts index a6a3b46935..f2d56c1171 100644 --- a/packages/global/openapi/core/plugin/admin/tool/api.ts +++ b/packages/global/openapi/core/plugin/admin/tool/api.ts @@ -6,6 +6,7 @@ import { import z from 'zod'; import { ParentIdSchema } from '../../../../../common/parentFolder/type'; import { PluginStatusSchema } from '../../../../../core/plugin/type'; +import { UserTagsEnum } from '../../../../../support/user/type'; // Admin tool list export const GetAdminSystemToolsQuery = z.object({ @@ -47,6 +48,8 @@ export const UpdateToolBodySchema = z.object({ hasTokenFee: z.boolean().optional(), inputListVal: z.record(z.string(), z.any()).nullish(), childTools: z.array(UpdateChildToolSchema).optional(), + promoteTags: z.array(UserTagsEnum).nullish(), + hideTags: z.array(UserTagsEnum).nullish(), // App tool fields name: z.string().optional(), diff --git a/packages/global/openapi/core/plugin/marketplace/api.ts b/packages/global/openapi/core/plugin/marketplace/api.ts index ebf5a1bccc..f1ebcb3084 100644 --- a/packages/global/openapi/core/plugin/marketplace/api.ts +++ b/packages/global/openapi/core/plugin/marketplace/api.ts @@ -57,7 +57,12 @@ export const GetSystemInstalledPluginsResponseSchema = z.object({ list: z.array( z.object({ id: z.string(), - version: z.string() + version: z.string(), + name: z.any().optional(), + description: z.any().optional(), + icon: z.string().optional(), + author: z.string().optional(), + tags: z.array(z.string()).optional() }) ) }); diff --git a/packages/global/openapi/plugin/api.ts b/packages/global/openapi/plugin/api.ts new file mode 100644 index 0000000000..ede22549b6 --- /dev/null +++ b/packages/global/openapi/plugin/api.ts @@ -0,0 +1,14 @@ +import z from 'zod'; + +export const PluginGetAccessTokenBodySchema = z.object({ + toolId: z.string(), + teamId: z.string(), + tmbId: z.string() +}); + +export const PluginGetAccessTokenResponseSchema = z.object({ + accessToken: z.string() +}); + +export type PluginGetAccessTokenBodyType = z.infer; +export type PluginGetAccessTokenResponseType = z.infer; diff --git a/packages/global/openapi/support/user/account/login/wecom/api.ts b/packages/global/openapi/support/user/account/login/wecom/api.ts new file mode 100644 index 0000000000..c4f091f43a --- /dev/null +++ b/packages/global/openapi/support/user/account/login/wecom/api.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const WecomGetRedirectURLBodySchema = z.object({ + redirectUri: z.string(), + state: z.string(), + isWecomWorkTerminal: z.boolean() +}); + +export const WecomGetRedirectURLResponseSchema = z.string(); + +export type WecomGetRedirectURLBodyType = z.infer; +export type WecomGetRedirectURLResponseType = z.infer; diff --git a/packages/global/openapi/support/user/team/api.ts b/packages/global/openapi/support/user/team/api.ts new file mode 100644 index 0000000000..ad0e91f12a --- /dev/null +++ b/packages/global/openapi/support/user/team/api.ts @@ -0,0 +1,10 @@ +import z from 'zod'; + +export const TeamChangeOwnerBodySchema = z.object({ + userId: z.string().describe("the New Owner's UserId.") +}); + +export const TeamChangeOwnerResponseSchema = z.object(); + +export type TeamChangeOwnerBodyType = z.infer; +export type TeamChangeOwnerResponseType = z.infer; diff --git a/packages/global/openapi/support/wallet/bill/api.ts b/packages/global/openapi/support/wallet/bill/api.ts index 781d339ee8..bd18e3e521 100644 --- a/packages/global/openapi/support/wallet/bill/api.ts +++ b/packages/global/openapi/support/wallet/bill/api.ts @@ -93,17 +93,21 @@ export const UpdateBillResponseSchema = z .object({ qrCode: z.string().optional().meta({ description: '支付二维码 URL' }), iframeCode: z.string().optional().meta({ description: '支付 iframe 代码' }), - markdown: z.string().optional().meta({ description: 'Markdown 格式的支付信息' }) + markdown: z.string().optional().meta({ description: 'Markdown 格式的支付信息' }), + payUrl: z.string().optional().meta({ description: '支付跳转 URL(企微支付)' }), + metadata: z.any().nullish().meta({ description: '支付元数据' }) }) - .refine((data) => data.qrCode || data.iframeCode || data.markdown, { - message: 'At least one of qrCode, iframeCode, or markdown must be provided' + .refine((data) => data.qrCode || data.iframeCode || data.markdown || data.payUrl, { + message: 'At least one of qrCode, iframeCode, markdown, or payUrl must be provided' }); export type UpdateBillResponseType = z.infer; export const CreateBillResponseSchema = UpdateBillResponseSchema.safeExtend({ - billId: z.string().meta({ description: '订单 ID' }), + billId: ObjectIdSchema.optional().meta({ description: '订单 ID' }), readPrice: z.number().min(0).meta({ description: '实际支付价格' }), payment: z.enum(BillPayWayEnum).meta({ description: '支付方式' }) +}).meta({ + description: '创建订单响应。企微支付时候,billId 为空' }); export type CreateBillResponseType = z.infer; diff --git a/packages/global/openapi/support/wecom/api.ts b/packages/global/openapi/support/wecom/api.ts new file mode 100644 index 0000000000..117f5360b8 --- /dev/null +++ b/packages/global/openapi/support/wecom/api.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const WecomGetCorpTokenBodySchema = z.object({}); + +export const WecomGetCorpTokenQuerySchema = z.object({}); + +export const WecomGetCorpTokenResponseSchema = z.object({ + access_token: z.string(), + expires_in: z.number() +}); + +export type WecomGetCorpTokenBodyType = z.infer; +export type WecomGetCorpTokenQueryType = z.infer; +export type WecomGetCorpTokenResponseType = z.infer; diff --git a/packages/global/package.json b/packages/global/package.json index 52e1cea072..5709f6dd62 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -2,7 +2,7 @@ "name": "@fastgpt/global", "version": "1.0.0", "dependencies": { - "@fastgpt-sdk/plugin": "0.2.17", + "@fastgpt-sdk/plugin": "0.3.6", "@apidevtools/swagger-parser": "^10.1.0", "@bany/curl-to-json": "^1.2.8", "axios": "^1.13.2", diff --git a/packages/global/support/user/constant.ts b/packages/global/support/user/constant.ts index 0fae675cba..4ae58494f5 100644 --- a/packages/global/support/user/constant.ts +++ b/packages/global/support/user/constant.ts @@ -16,5 +16,6 @@ export enum OAuthEnum { google = 'google', wechat = 'wechat', microsoft = 'microsoft', + wecom = 'wecom', sso = 'sso' } diff --git a/packages/global/support/user/team/controller.d.ts b/packages/global/support/user/team/controller.d.ts index ba1ba84bb6..88e5a0d9ee 100644 --- a/packages/global/support/user/team/controller.d.ts +++ b/packages/global/support/user/team/controller.d.ts @@ -1,4 +1,5 @@ import { PermissionValueType } from '../../permission/type'; +import type { TeamMetaType } from '../type'; import type { TeamMemberRoleEnum } from './constant'; import type { TeamMemberSchema, ThirdPartyAccountType } from './type'; import { LafAccountType } from './type'; @@ -14,6 +15,7 @@ export type CreateTeamProps = { memberName?: string; memberAvatar?: string; notificationAccount?: string; + meta?: TeamMetaType; }; export type UpdateTeamProps = Omit & { name?: string; diff --git a/packages/global/support/user/team/type.d.ts b/packages/global/support/user/team/type.d.ts index 1781466fb0..3058467eb0 100644 --- a/packages/global/support/user/team/type.d.ts +++ b/packages/global/support/user/team/type.d.ts @@ -1,4 +1,4 @@ -import type { UserModelSchema } from '../type'; +import type { TeamMetaType, UserModelSchema } from '../type'; import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant'; import type { LafAccountType } from './type'; import { PermissionValueType, ResourcePermissionType } from '../../permission/type'; @@ -23,6 +23,8 @@ export type TeamSchema = { lastWebsiteSyncTime: Date; }; notificationAccount?: string; + meta?: TeamMetaType; + deleteTime?: Date; } & ThirdPartyAccountType; export type tagsType = { @@ -68,6 +70,7 @@ export type TeamTmbItemType = { status: `${TeamMemberStatusEnum}`; notificationAccount?: string; permission: TeamPermission; + isWecomTeam?: boolean; } & ThirdPartyAccountType; export type TeamMemberItemType< diff --git a/packages/global/support/user/type.ts b/packages/global/support/user/type.ts index ea62d6140b..6563a4a23e 100644 --- a/packages/global/support/user/type.ts +++ b/packages/global/support/user/type.ts @@ -5,6 +5,13 @@ import { TeamMemberStatusEnum } from './team/constant'; import type { TeamTmbItemType } from './team/type'; import z from 'zod'; +export const UserTagsEnum = z.enum(['wecom']); +export type UserTagsEnum = z.infer; + +export type UserMetaType = { + isActivatedWecomLicense?: boolean; +}; + export type UserModelSchema = { _id: string; username: string; @@ -22,6 +29,8 @@ export type UserModelSchema = { keyword: string; }; contact?: string; + tags: UserTagsEnum[]; + meta?: UserMetaType; }; export type UserType = { @@ -34,6 +43,7 @@ export type UserType = { team: TeamTmbItemType; permission: TeamPermission; contact?: string; + tags?: UserTagsEnum[]; }; export const SourceMemberSchema = z.object({ @@ -44,3 +54,14 @@ export const SourceMemberSchema = z.object({ .meta({ example: TeamMemberStatusEnum.active, description: '成员状态' }) }); export type SourceMemberType = z.infer; + +export const TeamMetaSchema = z.object({ + wecom: z + .object({ + permanentCode: z.string(), + corpId: z.string() + }) + .optional() +}); + +export type TeamMetaType = z.infer; diff --git a/packages/global/support/wallet/bill/constants.ts b/packages/global/support/wallet/bill/constants.ts index 9d68174cb0..23702480d0 100644 --- a/packages/global/support/wallet/bill/constants.ts +++ b/packages/global/support/wallet/bill/constants.ts @@ -47,7 +47,8 @@ export enum BillPayWayEnum { wx = 'wx', alipay = 'alipay', bank = 'bank', - coupon = 'coupon' + coupon = 'coupon', + wecom = 'wecom' } export const billPayWayMap = { @@ -65,6 +66,9 @@ export const billPayWayMap = { }, [BillPayWayEnum.coupon]: { label: i18nT('account_bill:payway_coupon') + }, + [BillPayWayEnum.wecom]: { + label: i18nT('common:support.wallet.bill.payWay.wecom') } }; diff --git a/packages/global/support/wallet/sub/type.ts b/packages/global/support/wallet/sub/type.ts index 1e2d47343e..3cc9820880 100644 --- a/packages/global/support/wallet/sub/type.ts +++ b/packages/global/support/wallet/sub/type.ts @@ -22,6 +22,9 @@ export const TeamStandardSubPlanItemSchema = z.object({ ticketResponseTime: z.int().optional(), // 工单支持时间 customDomain: z.int().optional(), // 自定义域名数量 + maxUploadFileSize: z.int().optional(), // 最大上传文件大小(MB) + maxUploadFileCount: z.int().optional(), // 最大上传文件数量 + // 定制套餐 priceDescription: z.string().optional(), // 价格描述 customFormUrl: z.string().optional(), // 自定义表单 URL @@ -30,6 +33,14 @@ export const TeamStandardSubPlanItemSchema = z.object({ // Active annualBonusPoints: z.int().optional(), // 年度赠送积分 + /** 企微设置 */ + wecom: z + .object({ + price: z.number().describe('企微价格'), + points: z.number().describe('企微积分') + }) + .nullish(), + // @deprecated pointPrice: z.number().optional() }); @@ -88,7 +99,9 @@ export const TeamSubSchema = z.object({ appRegistrationCount: z.int().optional(), auditLogStoreDuration: z.int().optional(), ticketResponseTime: z.int().optional(), - customDomain: z.int().optional() + customDomain: z.int().optional(), + maxUploadFileSize: z.int().optional(), + maxUploadFileCount: z.int().optional() }); export type TeamSubSchemaType = z.infer; diff --git a/packages/service/common/bullmq/index.ts b/packages/service/common/bullmq/index.ts index 72aff04562..43987105ee 100644 --- a/packages/service/common/bullmq/index.ts +++ b/packages/service/common/bullmq/index.ts @@ -27,6 +27,7 @@ export enum QueueNames { // Delete Queue datasetDelete = 'datasetDelete', appDelete = 'appDelete', + teamDelete = 'teamDelete', // @deprecated websiteSync = 'websiteSync' } diff --git a/packages/service/common/file/multer.ts b/packages/service/common/file/multer.ts index 9b2bfe41b0..71e2ba9b3f 100644 --- a/packages/service/common/file/multer.ts +++ b/packages/service/common/file/multer.ts @@ -37,7 +37,7 @@ export const multer = { }, preservePath: true, storage: this._storage - }).array('file', global.feConfigs?.uploadFileMaxSize); + }).array('file', global.feConfigs.uploadFileMaxAmount); }, resolveFormData>({ diff --git a/packages/service/common/file/utils.ts b/packages/service/common/file/utils.ts index 2b1a641ff9..677f726374 100644 --- a/packages/service/common/file/utils.ts +++ b/packages/service/common/file/utils.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; export const getFileMaxSize = () => { - const mb = global.feConfigs?.uploadFileMaxSize || 1000; + const mb = global.feConfigs.uploadFileMaxSize || 1000; return mb * 1024 * 1024; }; diff --git a/packages/service/common/s3/constants.ts b/packages/service/common/s3/constants.ts index 470d5d17eb..9892c241d1 100644 --- a/packages/service/common/s3/constants.ts +++ b/packages/service/common/s3/constants.ts @@ -34,7 +34,7 @@ export const S3Buckets = { } as const; export const getSystemMaxFileSize = () => { - const config = global.feConfigs?.uploadFileMaxSize || 1024; // MB, default 1024MB + const config = global.feConfigs.uploadFileMaxSize || 1024; // MB, default 1024MB return config; // bytes }; diff --git a/packages/service/common/s3/sources/chat/index.ts b/packages/service/common/s3/sources/chat/index.ts index 46e66e8aa4..2ee627104c 100644 --- a/packages/service/common/s3/sources/chat/index.ts +++ b/packages/service/common/s3/sources/chat/index.ts @@ -58,11 +58,15 @@ export class S3ChatSource extends S3PrivateBucket { } async createUploadChatFileURL(params: CheckChatFileKeys) { - const { appId, chatId, uId, filename, expiredTime } = ChatFileUploadSchema.parse(params); + const { appId, chatId, uId, filename, expiredTime, maxFileSize } = + ChatFileUploadSchema.parse(params); const { fileKey } = getFileS3Key.chat({ appId, chatId, uId, filename }); return await this.createPresignedPutUrl( { rawKey: fileKey, filename }, - { expiredHours: expiredTime ? differenceInHours(expiredTime, new Date()) : 24 } + { + expiredHours: expiredTime ? differenceInHours(expiredTime, new Date()) : 24, + maxFileSize + } ); } diff --git a/packages/service/common/s3/sources/chat/type.ts b/packages/service/common/s3/sources/chat/type.ts index 0be39b0f27..3e77bf27f9 100644 --- a/packages/service/common/s3/sources/chat/type.ts +++ b/packages/service/common/s3/sources/chat/type.ts @@ -6,7 +6,8 @@ export const ChatFileUploadSchema = z.object({ chatId: z.string().nonempty(), uId: z.string().nonempty(), filename: z.string().nonempty(), - expiredTime: z.date().optional() + expiredTime: z.date().optional(), + maxFileSize: z.number().positive().optional() }); export type CheckChatFileKeys = z.infer; diff --git a/packages/service/common/s3/sources/dataset/index.ts b/packages/service/common/s3/sources/dataset/index.ts index 441d84e630..87ee0c11f7 100644 --- a/packages/service/common/s3/sources/dataset/index.ts +++ b/packages/service/common/s3/sources/dataset/index.ts @@ -44,9 +44,12 @@ export class S3DatasetSource extends S3PrivateBucket { // 上传链接 async createUploadDatasetFileURL(params: CreateUploadDatasetFileParams) { - const { filename, datasetId } = CreateUploadDatasetFileParamsSchema.parse(params); + const { filename, datasetId, maxFileSize } = CreateUploadDatasetFileParamsSchema.parse(params); const { fileKey } = getFileS3Key.dataset({ datasetId, filename }); - return await this.createPresignedPutUrl({ rawKey: fileKey, filename }, { expiredHours: 3 }); + return await this.createPresignedPutUrl( + { rawKey: fileKey, filename }, + { expiredHours: 3, maxFileSize } + ); } // 单个键删除 diff --git a/packages/service/common/s3/sources/dataset/type.ts b/packages/service/common/s3/sources/dataset/type.ts index 239bc153df..8a4a8db29d 100644 --- a/packages/service/common/s3/sources/dataset/type.ts +++ b/packages/service/common/s3/sources/dataset/type.ts @@ -4,7 +4,8 @@ import { z } from 'zod'; export const CreateUploadDatasetFileParamsSchema = z.object({ filename: z.string().nonempty(), - datasetId: ObjectIdSchema + datasetId: ObjectIdSchema, + maxFileSize: z.number().positive().optional() }); export type CreateUploadDatasetFileParams = z.infer; diff --git a/packages/service/common/system/tools.ts b/packages/service/common/system/tools.ts index 8f5a9ffbbe..de6ea59d92 100644 --- a/packages/service/common/system/tools.ts +++ b/packages/service/common/system/tools.ts @@ -16,6 +16,8 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => { !!config.systemEnv.customPdfParse?.textinAppId || !!config.systemEnv.customPdfParse?.doc2xKey; config.feConfigs.customPdfParsePrice = config.systemEnv.customPdfParse?.price || 0; + config.feConfigs.uploadFileMaxSize = Number(process.env.UPLOAD_FILE_MAX_SIZE || 1000); + config.feConfigs.uploadFileMaxAmount = Number(process.env.UPLOAD_FILE_MAX_AMOUNT || 1000); global.feConfigs = config.feConfigs; global.systemEnv = config.systemEnv; diff --git a/packages/service/common/vectorDB/constants.ts b/packages/service/common/vectorDB/constants.ts index 2d9a34075f..f0c637a86f 100644 --- a/packages/service/common/vectorDB/constants.ts +++ b/packages/service/common/vectorDB/constants.ts @@ -3,6 +3,7 @@ export const DatasetVectorTableName = 'modeldata'; export const PG_ADDRESS = process.env.PG_URL; export const OCEANBASE_ADDRESS = process.env.OCEANBASE_URL; +export const SEEKDB_ADDRESS = process.env.SEEKDB_URL; export const MILVUS_ADDRESS = process.env.MILVUS_ADDRESS; export const MILVUS_TOKEN = process.env.MILVUS_TOKEN; diff --git a/packages/service/common/vectorDB/controller.ts b/packages/service/common/vectorDB/controller.ts index 834d0eeec2..a7f9a2e6ed 100644 --- a/packages/service/common/vectorDB/controller.ts +++ b/packages/service/common/vectorDB/controller.ts @@ -1,10 +1,11 @@ /* vector crud */ import { PgVectorCtrl } from './pg'; import { ObVectorCtrl } from './oceanbase'; +import { SeekVectorCtrl } from './seekdb'; import { getVectorsByText } from '../../core/ai/embedding'; import type { VectorControllerType, InsertVectorControllerPropsType } from './type'; import { type EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d'; -import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS } from './constants'; +import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS, SEEKDB_ADDRESS } from './constants'; import { MilvusCtrl } from './milvus'; import { setRedisCache, @@ -17,9 +18,10 @@ import { import { throttle } from 'lodash'; import { retryFn } from '@fastgpt/global/common/system/utils'; -const getVectorObj = () => { - if (PG_ADDRESS) return new PgVectorCtrl(); +const getVectorObj = (): VectorControllerType => { + if (SEEKDB_ADDRESS) return new SeekVectorCtrl(); if (OCEANBASE_ADDRESS) return new ObVectorCtrl(); + if (PG_ADDRESS) return new PgVectorCtrl(); if (MILVUS_ADDRESS) return new MilvusCtrl(); return new PgVectorCtrl(); diff --git a/packages/service/common/vectorDB/seekdb/index.ts b/packages/service/common/vectorDB/seekdb/index.ts new file mode 100644 index 0000000000..a0474f489f --- /dev/null +++ b/packages/service/common/vectorDB/seekdb/index.ts @@ -0,0 +1,13 @@ +/** + * SeekDB Vector Database Controller + * + * SeekDB 使用 MySQL 协议,与 OceanBase 完全兼容 + * 直接复用 OceanBase 的控制器实现 + */ + +// 导出 OceanBase 控制器(复用) +export { ObClient as SeekClient } from '../oceanbase/controller'; +export { ObVectorCtrl as SeekVectorCtrl } from '../oceanbase'; + +// 导出类型 +export type { VectorControllerType } from '../type'; diff --git a/packages/service/core/ai/config/utils.ts b/packages/service/core/ai/config/utils.ts index 4d0b7947bf..233e83bd86 100644 --- a/packages/service/core/ai/config/utils.ts +++ b/packages/service/core/ai/config/utils.ts @@ -112,11 +112,10 @@ export const loadSystemModels = async (init = false, language = 'en') => { // Get model from db and plugin const [dbModels, systemModels] = await Promise.all([ MongoSystemModel.find({}).lean(), - pluginClient.model.list().then((res) => { - if (res.status === 200) return res.body; - console.error('Get fastGPT plugin model error'); - return []; - }) + pluginClient + .listModels() + .then((res) => res) + .catch(() => []) ]); // Load system model from local @@ -249,13 +248,9 @@ export const getSystemModelConfig = async (model: string): Promise { - if (res.status === 200) { - return res.body.find((item) => item.model === model) as SystemModelItemType; - } - - return Promise.reject('Can not get model config from plugin'); - }); + const modelDefaulConfig = await pluginClient + .listModels() + .then((models) => models.find((item) => item.model === model) as SystemModelItemType); return { ...modelDefaulConfig, diff --git a/packages/service/core/app/delete/index.ts b/packages/service/core/app/delete/index.ts index 3f55b7881d..04efc61c09 100644 --- a/packages/service/core/app/delete/index.ts +++ b/packages/service/core/app/delete/index.ts @@ -11,7 +11,8 @@ export const initAppDeleteWorker = () => { return getWorker(QueueNames.appDelete, appDeleteProcessor, { concurrency: 1, // 确保同时只有1个删除任务 removeOnFail: { - age: 30 * 24 * 60 * 60 // 保留30天失败记录 + age: 90 * 24 * 60 * 60, // 保留90天失败记录 + count: 10000 // 最多保留10000个失败任务 } }); }; diff --git a/packages/service/core/app/templates/register.ts b/packages/service/core/app/templates/register.ts index 54aafb6af6..42f58a27bb 100644 --- a/packages/service/core/app/templates/register.ts +++ b/packages/service/core/app/templates/register.ts @@ -6,9 +6,7 @@ import { pluginClient } from '../../../thirdProvider/fastgptPlugin'; import { addMinutes } from 'date-fns'; const getFileTemplates = async (): Promise => { - const res = await pluginClient.workflow.getTemplateList(); - if (res.status === 200) return res.body as AppTemplateSchemaType[]; - else return Promise.reject(res.body); + return (await pluginClient.listWorkflows()) as AppTemplateSchemaType[]; }; const getAppTemplates = async () => { diff --git a/packages/service/core/app/templates/templateSchema.ts b/packages/service/core/app/templates/templateSchema.ts index 1c428d4e1f..76512d8db6 100644 --- a/packages/service/core/app/templates/templateSchema.ts +++ b/packages/service/core/app/templates/templateSchema.ts @@ -1,5 +1,6 @@ import { type AppTemplateSchemaType } from '@fastgpt/global/core/app/type'; import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; +import { UserTagsEnum } from '@fastgpt/global/support/user/type'; const { Schema } = connectionMongo; export const collectionName = 'app_templates'; @@ -20,6 +21,14 @@ const AppTemplateSchema = new Schema({ type: String, isActive: Boolean, isPromoted: Boolean, + promoteTags: { + type: [String], + enum: UserTagsEnum.enum + }, + hideTags: { + type: [String], + enum: UserTagsEnum.enum + }, recommendText: String, userGuide: Object, isQuickTemplate: Boolean, diff --git a/packages/service/core/app/tool/api.ts b/packages/service/core/app/tool/api.ts index aacf9fb740..2cc6d1d584 100644 --- a/packages/service/core/app/tool/api.ts +++ b/packages/service/core/app/tool/api.ts @@ -1,43 +1,23 @@ import { RunToolWithStream } from '@fastgpt/global/sdk/fastgpt-plugin'; import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { pluginClient, PLUGIN_BASE_URL, PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin'; -import { addLog } from '../../../common/system/log'; import { retryFn } from '@fastgpt/global/common/system/utils'; export async function APIGetSystemToolList() { - const res = await pluginClient.tool.list(); + const tools = await pluginClient.listTools(); - if (res.status === 200) { - return res.body.map((item) => { - return { - ...item, - id: `${AppToolSourceEnum.systemTool}-${item.toolId}`, - parentId: item.parentId ? `${AppToolSourceEnum.systemTool}-${item.parentId}` : undefined, - avatar: item.icon - }; - }); - } - - return Promise.reject(res.body); + return tools.map((item) => { + return { + ...item, + id: `${AppToolSourceEnum.systemTool}-${item.toolId}`, + parentId: item.parentId ? `${AppToolSourceEnum.systemTool}-${item.parentId}` : undefined, + avatar: item.icon + }; + }); } -const runToolInstance = new RunToolWithStream({ - baseUrl: PLUGIN_BASE_URL, - token: PLUGIN_TOKEN -}); +const runToolInstance = new RunToolWithStream(PLUGIN_BASE_URL, PLUGIN_TOKEN); + export const APIRunSystemTool = runToolInstance.run.bind(runToolInstance); -export const getSystemToolTags = () => { - return retryFn(async () => { - const res = await pluginClient.tool.getTags(); - - if (res.status === 200) { - const toolTypes = res.body || []; - - return toolTypes; - } - - addLog.error('Get system tool type error', res.body); - return []; - }); -}; +export const getSystemToolTags = () => retryFn(async () => await pluginClient.getToolTags()); diff --git a/packages/service/core/app/tool/controller.ts b/packages/service/core/app/tool/controller.ts index a48723fc32..04e34866d4 100644 --- a/packages/service/core/app/tool/controller.ts +++ b/packages/service/core/app/tool/controller.ts @@ -66,10 +66,12 @@ export const getSystemTools = () => getCachedData(SystemCacheKeyEnum.systemTool) export const getSystemToolsWithInstalled = async ({ teamId, - isRoot + isRoot, + userTags = [] }: { teamId: string; isRoot: boolean; + userTags?: string[]; }) => { const [tools, { installedSet, uninstalledSet }] = await Promise.all([ getSystemTools(), @@ -89,25 +91,46 @@ export const getSystemToolsWithInstalled = async ({ }) ]); - return tools.map((tool) => { - const installed = (() => { - if (installedSet.has(tool.id)) { - return true; + return tools + .filter((tool) => { + // Filter out tools hidden by hideTags + if (userTags.length > 0 && tool.hideTags && tool.hideTags.length > 0) { + return !tool.hideTags.some((hideTag) => userTags.includes(hideTag)); } - if (isRoot && !uninstalledSet.has(tool.id)) { - return true; - } - if (tool.defaultInstalled && !uninstalledSet.has(tool.id)) { - return true; - } - return false; - })(); + return true; + }) + .map((tool) => { + const installed = (() => { + // 优先级1: 明确记录 + if (installedSet.has(tool.id)) { + return true; + } + // 优先级2: Root用户 + if (isRoot && !uninstalledSet.has(tool.id)) { + return true; + } + // 优先级3: 基于 promoteTags 的自动预安装 + if ( + userTags.length > 0 && + tool.promoteTags && + tool.promoteTags.length > 0 && + !uninstalledSet.has(tool.id) + ) { + const shouldAutoInstall = tool.promoteTags.some((tag) => userTags.includes(tag)); + if (shouldAutoInstall) return true; + } + // 优先级4: 全局默认安装 + if (tool.defaultInstalled && !uninstalledSet.has(tool.id)) { + return true; + } + return false; + })(); - return { - ...tool, - installed - }; - }); + return { + ...tool, + installed + }; + }); }; export const getSystemToolByIdAndVersionId = async ( @@ -622,7 +645,7 @@ export const refreshSystemTools = async (): Promise = author: item.author, courseUrl: item.courseUrl, instructions: dbPluginConfig?.customConfig?.userGuide, - tags: item.tags, + tags: dbPluginConfig?.customConfig?.tags || item.tags, workflow: { nodes: [], edges: [] @@ -637,7 +660,9 @@ export const refreshSystemTools = async (): Promise = currentCost: dbPluginConfig?.currentCost ?? 0, systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0, hasTokenFee: dbPluginConfig?.hasTokenFee ?? false, - pluginOrder: dbPluginConfig?.pluginOrder + pluginOrder: dbPluginConfig?.pluginOrder, + hideTags: dbPluginConfig?.hideTags, + promoteTags: dbPluginConfig?.promoteTags }; }); diff --git a/packages/service/core/dataset/delete/index.ts b/packages/service/core/dataset/delete/index.ts index 189b158e09..aa4000a20a 100644 --- a/packages/service/core/dataset/delete/index.ts +++ b/packages/service/core/dataset/delete/index.ts @@ -11,7 +11,8 @@ export const initDatasetDeleteWorker = () => { return getWorker(QueueNames.datasetDelete, datasetDeleteProcessor, { concurrency: 1, // 确保同时只有1个删除任务 removeOnFail: { - age: 30 * 24 * 60 * 60 // 保留30天失败记录 + age: 90 * 24 * 60 * 60, // 保留90天失败记录 + count: 10000 // 最多保留10000个失败任务 } }); }; diff --git a/packages/service/core/plugin/tool/systemToolSchema.ts b/packages/service/core/plugin/tool/systemToolSchema.ts index e8476458d1..f7e8a3a2a1 100644 --- a/packages/service/core/plugin/tool/systemToolSchema.ts +++ b/packages/service/core/plugin/tool/systemToolSchema.ts @@ -1,6 +1,7 @@ import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; const { Schema } = connectionMongo; import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; +import { UserTagsEnum } from '@fastgpt/global/support/user/type'; export const collectionName = 'system_plugin_tools'; @@ -38,6 +39,14 @@ const SystemToolSchema = new Schema({ }, customConfig: Object, inputListVal: Object, + promoteTags: { + type: [String], + enum: UserTagsEnum.enum + }, + hideTags: { + type: [String], + enum: UserTagsEnum.enum + }, // @deprecated inputConfig: Array, diff --git a/packages/service/support/permission/auth/plugin.ts b/packages/service/support/permission/auth/plugin.ts new file mode 100644 index 0000000000..9cf3d45aad --- /dev/null +++ b/packages/service/support/permission/auth/plugin.ts @@ -0,0 +1,25 @@ +import { PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin/index'; +import type { ApiRequestProps } from '../../../type/next'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; + +/** + * Auth plugin token from request header + * Check if the 'authtoken' header matches the PLUGIN_TOKEN environment variable + */ +export const authPluginToken = async ({ req }: { req: ApiRequestProps }) => { + const authtoken = req.headers.authtoken as string | undefined; + + if (!authtoken) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + if (!PLUGIN_TOKEN) { + return Promise.reject('PLUGIN_TOKEN is not configured'); + } + + if (authtoken !== PLUGIN_TOKEN) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return true; +}; diff --git a/packages/service/support/permission/auth/pluginAccessToken.ts b/packages/service/support/permission/auth/pluginAccessToken.ts new file mode 100644 index 0000000000..7a26f921e7 --- /dev/null +++ b/packages/service/support/permission/auth/pluginAccessToken.ts @@ -0,0 +1,63 @@ +import jwt from 'jsonwebtoken'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; +import { z } from 'zod'; +import type { NextApiRequest } from 'next'; + +const PLUGIN_ACCESS_TOKEN_SECRET = + process.env.PLUGIN_ACCESS_TOKEN_SECRET || 'plugin_access_token_secret'; +const PLUGIN_ACCESS_TOKEN_EXPIRES_IN: number = process.env.PLUGIN_ACCESS_TOKEN_EXPIRES_IN + ? parseInt(process.env.PLUGIN_ACCESS_TOKEN_EXPIRES_IN) + : 3600; // Default 1 hour (3600 seconds) + +export const PluginAccessTokenPayloadSchema = z.object({ + tmbId: z.string(), + teamId: z.string(), + toolId: z.string() +}); + +export type PluginAccessTokenPayload = z.infer; + +/** + * Generate plugin access token + * JWT with tmbId and toolId in payload + */ +export const generatePluginAccessToken = (payload: PluginAccessTokenPayload): string => { + const data = PluginAccessTokenPayloadSchema.parse(payload); + + const token = jwt.sign(data, PLUGIN_ACCESS_TOKEN_SECRET, { + expiresIn: PLUGIN_ACCESS_TOKEN_EXPIRES_IN + }); + + return token; +}; + +/** + * Verify and decode plugin access token + * Returns the payload if valid, otherwise rejects with error + */ +export const authPluginAccessToken = ({ + req +}: { + req: NextApiRequest; +}): Promise => { + const token = req.headers.authorization?.split(' ')[1]; + + return new Promise((resolve, reject) => { + if (!token) { + return reject(ERROR_ENUM.unAuthorization); + } + + jwt.verify(token, PLUGIN_ACCESS_TOKEN_SECRET, (err, decoded: any) => { + if (err) { + return reject(ERROR_ENUM.unAuthorization); + } + + try { + const payload = PluginAccessTokenPayloadSchema.parse(decoded); + return resolve(payload); + } catch (error) { + return reject(ERROR_ENUM.unAuthorization); + } + }); + }); +}; diff --git a/packages/service/support/user/controller.ts b/packages/service/support/user/controller.ts index 413bbff060..b59a5d999c 100644 --- a/packages/service/support/user/controller.ts +++ b/packages/service/support/user/controller.ts @@ -47,6 +47,7 @@ export async function getUserDetail({ team: tmb, permission: tmb.permission, contact: user.contact, - language: user.language + language: user.language, + tags: user.tags }; } diff --git a/packages/service/support/user/schema.ts b/packages/service/support/user/schema.ts index dcf92847cc..73d0f44874 100644 --- a/packages/service/support/user/schema.ts +++ b/packages/service/support/user/schema.ts @@ -1,7 +1,7 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; const { Schema } = connectionMongo; import { hashStr } from '@fastgpt/global/common/string/tools'; -import type { UserModelSchema } from '@fastgpt/global/support/user/type'; +import { UserTagsEnum, type UserModelSchema } from '@fastgpt/global/support/user/type'; import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; import { LangEnum } from '@fastgpt/global/common/i18n/type'; @@ -66,6 +66,11 @@ const UserSchema = new Schema({ phonePrefix: Number, contact: String, + tags: { + type: [String], + enum: UserTagsEnum.enum + }, + meta: Object, /** @deprecated */ avatar: String }); diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index ecae87871b..0d93757da9 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -52,7 +52,8 @@ async function getTeamMember(match: Record): Promise { + return getWorker(QueueNames.teamDelete, teamDeleteProcessor, { + concurrency: 1, // 确保同时只有1个删除任务 + removeOnFail: { + age: 90 * 24 * 60 * 60, // 保留90天失败记录 + count: 10000 // 最多保留10000个失败任务 + } + }); +}; + +// 添加删除任务 +export const addTeamDeleteJob = (data: TeamDeleteJobData) => { + // 创建删除队列 + const teamDeleteQueue = getQueue(QueueNames.teamDelete, { + defaultJobOptions: { + attempts: 10, + backoff: { + type: 'exponential', + delay: 5000 + }, + removeOnComplete: true, + removeOnFail: { age: 30 * 24 * 60 * 60 } // 保留30天失败记录 + } + }); + + const jobId = `${String(data.teamId)}`; + + // Use jobId to automatically prevent duplicate deletion tasks (BullMQ feature) + return teamDeleteQueue.add('delete_team', data, { + jobId, + delay: 1000 // Delay 1 second to ensure API response completes + }); +}; diff --git a/packages/service/support/user/team/delete/processor.ts b/packages/service/support/user/team/delete/processor.ts new file mode 100644 index 0000000000..5e6635d211 --- /dev/null +++ b/packages/service/support/user/team/delete/processor.ts @@ -0,0 +1,146 @@ +import type { Processor } from 'bullmq'; +import { type TeamDeleteJobData } from './index'; +import { addLog } from '../../../../common/system/log'; +import { MongoImage } from '../../../../common/file/image/schema'; +import { MongoOpenApi } from '../../../openapi/schema'; +import { MongoGroupMemberModel } from '../../../permission/memberGroup/groupMemberSchema'; +import { MongoMemberGroupModel } from '../../../permission/memberGroup/memberGroupSchema'; +import { MongoOrgMemberModel } from '../../../permission/org/orgMemberSchema'; +import { MongoOrgModel } from '../../../permission/org/orgSchema'; +import { MongoResourcePermission } from '../../../permission/schema'; +import { delUserAllSession } from '../../session'; +import { MongoTeamMember } from '../teamMemberSchema'; +import { MongoTeam } from '../teamSchema'; +import { MongoTeamTags } from '../teamTagsSchema'; +import { MongoMcpKey } from '../../../mcp/schema'; +import { MongoChatSetting } from '../../../../core/chat/setting/schema'; +import { MongoChatFavouriteApp } from '../../../../core/chat/favouriteApp/schema'; +import { MongoDiscountCoupon } from '../../../wallet/discountCoupon/schema'; +import { MongoTeamAudit } from '../../audit/schema'; +import { deleteTeamAllDatasets } from '../../../../core/dataset/delete/processor'; +import { onDelAllApp } from './utils'; +import { MongoEvaluation } from '../../../../core/app/evaluation/evalSchema'; +import { MongoEvalItem } from '../../../../core/app/evaluation/evalItemSchema'; +import { MongoTeamSub } from '../../../../support/wallet/sub/schema'; + +export const teamDeleteProcessor: Processor = async (job) => { + const { teamId } = job.data; + const startTime = Date.now(); + + addLog.info(`[Team Delete] Start deleting team: ${teamId}`); + + try { + // 1. 检查团队是否存在 + const team = await MongoTeam.findById(teamId); + if (!team) { + addLog.warn(`[Team Delete] Team not found: ${teamId}`); + return; + } + + // 2. 先删除知识库和应用(它们内部有自己的队列) + await deleteTeamAllDatasets(teamId); + await onDelAllApp(teamId); + // 删除评估 + await MongoEvaluation.deleteMany({ + teamId + }); + // 删除评估项 + await MongoEvalItem.deleteMany({ + teamId + }); + + // 删除图片(旧的了) + await MongoImage.deleteMany({ + teamId: teamId + }); + + // 3. 删除门户 + await MongoChatSetting.deleteMany({ + teamId + }); + await MongoChatFavouriteApp.deleteMany({ + teamId + }); + + // 4. 删除独立资源 + // 删除 API key + await MongoOpenApi.deleteMany({ + teamId + }); + // 删除 MCP + await MongoMcpKey.deleteMany({ + teamId + }); + // 审计日志 + await MongoTeamAudit.deleteMany({ + teamId + }); + + // 5. 删除财务相关 + // 删除优惠券 + await MongoDiscountCoupon.deleteMany({ + teamId + }); + + await MongoTeamSub.deleteMany({ + teamId + }); + // 删除使用记录(不删除,等待自动过期) + // 充值记录不删除 + + // 6. 删除团队信息 + // 删除权限 + await MongoResourcePermission.deleteMany({ + teamId + }); + + // 删除群组 + const groups = await MongoMemberGroupModel.find({ teamId }); + await MongoGroupMemberModel.deleteMany({ + groupId: { $in: groups.map((item) => item._id) } + }); + await MongoMemberGroupModel.deleteMany({ + teamId + }); + + // 删除组织 + await MongoOrgModel.deleteMany({ + teamId + }); + await MongoOrgMemberModel.deleteMany({ + teamId + }); + + // 删除 teamTags + await MongoTeamTags.deleteMany({ + teamId + }); + + // 7. 删除成员 session 和成员信息 + const members = await MongoTeamMember.find({ + teamId + }); + + // 删除所有成员的 session + await Promise.all(members.map((member) => delUserAllSession(member.userId))); + + await MongoTeamMember.deleteMany({ + teamId + }); + + // 8. 清理团队敏感信息 + team.notificationAccount = ''; + team.openaiAccount = undefined; + team.lafAccount = undefined; + team.externalWorkflowVariables = undefined; + team.meta = undefined; + await team.save(); + + addLog.info(`[Team Delete] Successfully deleted team: ${teamId}`, { + duration: Date.now() - startTime + }); + } catch (error: any) { + addLog.error(`[Team Delete] Failed to delete team: ${teamId}`, error); + throw error; + } +}; diff --git a/packages/service/support/user/team/delete/utils.ts b/packages/service/support/user/team/delete/utils.ts new file mode 100644 index 0000000000..62e9ccc817 --- /dev/null +++ b/packages/service/support/user/team/delete/utils.ts @@ -0,0 +1,41 @@ +import { MongoApp } from '../../../../core/app/schema'; +import { deleteAppsImmediate } from '../../../../core/app/controller'; +import { addAppDeleteJob } from '../../../../core/app/delete'; + +export const onDelAllApp = async (teamId: string) => { + // 取根目录所有应用 + const apps = await MongoApp.find( + { + teamId, + parentId: null + }, + '_id' + ); + const appIds = apps.map((app) => app._id); + + // Stop background tasks immediately + await deleteAppsImmediate({ + teamId, + appIds: appIds + }); + + // 标记所有应用为待删除 + await MongoApp.updateMany( + { + teamId + }, + { + $set: { + deleteTime: new Date() + } + } + ); + + // 添加到删除队列 + for (const appId of appIds) { + await addAppDeleteJob({ + teamId, + appId + }); + } +}; diff --git a/packages/service/support/user/team/teamMemberSchema.ts b/packages/service/support/user/team/teamMemberSchema.ts index 48d42182fc..5b5c3b2ac5 100644 --- a/packages/service/support/user/team/teamMemberSchema.ts +++ b/packages/service/support/user/team/teamMemberSchema.ts @@ -40,11 +40,14 @@ const TeamMemberSchema = new Schema({ type: Date }, - // Abandoned + /** @deprecated + * But some code still use this to judge whether the member is a owner. + * TODO: Remove this field and replace it with a more appropriate way to determine ownership. + */ role: { type: String }, - // Abandoned + /** @deprecated */ defaultTeam: { type: Boolean } diff --git a/packages/service/support/user/team/teamSchema.ts b/packages/service/support/user/team/teamSchema.ts index c12d89a8cc..bc09da66db 100644 --- a/packages/service/support/user/team/teamSchema.ts +++ b/packages/service/support/user/team/teamSchema.ts @@ -57,12 +57,19 @@ const TeamSchema = new Schema({ notificationAccount: { type: String, required: false + }, + meta: { + type: Object + }, + deleteTime: { + type: Date } }); try { TeamSchema.index({ name: 1 }); TeamSchema.index({ ownerId: 1 }); + TeamSchema.index({ 'meta.wecom.corpId': 1 }, { sparse: true, unique: true }); } catch (error) { console.log(error); } diff --git a/packages/service/support/wallet/sub/schema.ts b/packages/service/support/wallet/sub/schema.ts index 8b934124c0..133d433808 100644 --- a/packages/service/support/wallet/sub/schema.ts +++ b/packages/service/support/wallet/sub/schema.ts @@ -66,6 +66,9 @@ const SubSchema = new Schema({ ticketResponseTime: Number, customDomain: Number, + maxUploadFileSize: Number, + maxUploadFileCount: Number, + // stand sub and extra points sub. Plan total points totalPoints: Number, // plan surplus points diff --git a/packages/service/support/wallet/sub/utils.ts b/packages/service/support/wallet/sub/utils.ts index 9412135057..9e663ea477 100644 --- a/packages/service/support/wallet/sub/utils.ts +++ b/packages/service/support/wallet/sub/utils.ts @@ -11,7 +11,7 @@ import { } from '@fastgpt/global/support/wallet/sub/type'; import dayjs from 'dayjs'; import { type ClientSession } from '../../../common/mongo'; -import { addMonths } from 'date-fns'; +import { addMonths, addDays } from 'date-fns'; import { readFromSecondary } from '../../../common/mongo/utils'; import { setRedisCache, @@ -52,8 +52,12 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { const standard = plans[0]; const standardConstants = - standard?.currentSubLevel && standardPlans - ? standardPlans[standard.currentSubLevel] + standard.currentSubLevel && standardPlans + ? standardPlans[ + standard.currentSubLevel === StandardSubLevelEnum.custom + ? StandardSubLevelEnum.advanced + : standard.currentSubLevel + ] : undefined; return { @@ -83,12 +87,16 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { export const initTeamFreePlan = async ({ teamId, + isWecomTeam = false, session }: { teamId: string; + isWecomTeam?: boolean; session?: ClientSession; }) => { - const freePoints = global?.subPlans?.standard?.[StandardSubLevelEnum.free]?.totalPoints || 100; + const freePoints = isWecomTeam + ? Math.round((global.subPlans?.standard?.basic.totalPoints ?? 4000) / 2) + : global?.subPlans?.standard?.[StandardSubLevelEnum.free]?.totalPoints || 100; const freePlan = await MongoTeamSub.findOne({ teamId, @@ -96,6 +104,27 @@ export const initTeamFreePlan = async ({ currentSubLevel: StandardSubLevelEnum.free }); + // Get basic plan config for wecom mode + const specialConfig: Record | null = (() => { + const config = global?.subPlans?.standard?.[StandardSubLevelEnum.basic]; + if (isWecomTeam && config) { + return { + maxTeamMember: config.maxTeamMember, + maxApp: config.maxAppAmount, + maxDataset: config.maxDatasetAmount, + requestsPerMinute: config.requestsPerMinute, + chatHistoryStoreDuration: config.chatHistoryStoreDuration, + maxDatasetSize: config.maxDatasetSize, + websiteSyncPerDataset: config.websiteSyncPerDataset, + appRegistrationCount: config.appRegistrationCount, + auditLogStoreDuration: config.auditLogStoreDuration, + ticketResponseTime: config.ticketResponseTime, + customDomain: config.customDomain + } as TeamSubSchemaType; + } + return null; + })(); + // Reset one month free plan if (freePlan) { freePlan.currentMode = SubModeEnum.month; @@ -111,6 +140,14 @@ export const initTeamFreePlan = async ({ freePlan.surplusPoints && freePlan.surplusPoints < 0 ? freePlan.surplusPoints + freePoints : freePoints; + + // Apply basic plan config for wecom, but with limited points and dataset size + if (specialConfig) { + for (const key in specialConfig) { + (freePlan as any)[key] = specialConfig[key]; + } + } + return freePlan.save({ session }); } @@ -122,13 +159,14 @@ export const initTeamFreePlan = async ({ currentMode: SubModeEnum.month, nextMode: SubModeEnum.month, startTime: new Date(), - expiredTime: addMonths(new Date(), 1), + expiredTime: isWecomTeam ? addDays(new Date(), 15) : addMonths(new Date(), 1), currentSubLevel: StandardSubLevelEnum.free, nextSubLevel: StandardSubLevelEnum.free, totalPoints: freePoints, - surplusPoints: freePoints + surplusPoints: freePoints, + ...(specialConfig && specialConfig) } ], { session, ordered: true } @@ -178,7 +216,11 @@ export const getTeamPlanStatus = async ({ const standardMaxDatasetSize = standardPlan?.currentSubLevel && standardPlans ? standardPlan?.maxDatasetSize || - standardPlans[standardPlan.currentSubLevel]?.maxDatasetSize || + standardPlans[ + standardPlan.currentSubLevel === StandardSubLevelEnum.custom + ? StandardSubLevelEnum.advanced + : standardPlan.currentSubLevel + ]?.maxDatasetSize || Infinity : Infinity; const totalDatasetSize = @@ -187,13 +229,43 @@ export const getTeamPlanStatus = async ({ const standardConstants = standardPlan?.currentSubLevel && standardPlans - ? standardPlans[standardPlan.currentSubLevel] + ? standardPlans[ + standardPlan.currentSubLevel === StandardSubLevelEnum.custom + ? StandardSubLevelEnum.advanced + : standardPlan.currentSubLevel + ] : undefined; teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints }); return { - [SubTypeEnum.standard]: standardPlan, + [SubTypeEnum.standard]: + standardPlan.currentSubLevel === StandardSubLevelEnum.custom && standardConstants + ? { + ...standardPlan, + maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember, + maxApp: standardPlan?.maxApp ?? standardConstants.maxAppAmount, + maxDataset: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount, + requestsPerMinute: + standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute, + chatHistoryStoreDuration: + standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration, + maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize, + websiteSyncPerDataset: + standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset, + appRegistrationCount: + standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount, + auditLogStoreDuration: + standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, + ticketResponseTime: + standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime, + customDomain: standardPlan?.customDomain ?? standardConstants.customDomain, + maxUploadFileSize: + standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize, + maxUploadFileCount: + standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount + } + : standardPlan, standardConstants: standardConstants ? { ...standardConstants, @@ -212,7 +284,10 @@ export const getTeamPlanStatus = async ({ standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, ticketResponseTime: standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime, - customDomain: standardPlan?.customDomain ?? standardConstants.customDomain + customDomain: standardPlan?.customDomain ?? standardConstants.customDomain, + maxUploadFileSize: standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize, + maxUploadFileCount: + standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount } : undefined, diff --git a/packages/service/thirdProvider/fastgptPlugin/index.ts b/packages/service/thirdProvider/fastgptPlugin/index.ts index c38d9509fa..eefd583bb7 100644 --- a/packages/service/thirdProvider/fastgptPlugin/index.ts +++ b/packages/service/thirdProvider/fastgptPlugin/index.ts @@ -1,9 +1,9 @@ -import { createClient } from '@fastgpt/global/sdk/fastgpt-plugin'; +import { FastGPTPluginClient } from '@fastgpt/global/sdk/fastgpt-plugin'; export const PLUGIN_BASE_URL = process.env.PLUGIN_BASE_URL || ''; export const PLUGIN_TOKEN = process.env.PLUGIN_TOKEN || ''; -export const pluginClient = createClient({ +export const pluginClient = new FastGPTPluginClient({ baseUrl: PLUGIN_BASE_URL, token: PLUGIN_TOKEN }); diff --git a/packages/service/thirdProvider/fastgptPlugin/model.ts b/packages/service/thirdProvider/fastgptPlugin/model.ts index 33d1c954f7..925503b51f 100644 --- a/packages/service/thirdProvider/fastgptPlugin/model.ts +++ b/packages/service/thirdProvider/fastgptPlugin/model.ts @@ -1,11 +1,5 @@ import { pluginClient } from '.'; export const loadModelProviders = async () => { - const res = await pluginClient.model.getProviders(); - - if (res.status === 200) { - return res.body; - } - - return Promise.reject(res.body); + return await pluginClient.getModelProviders(); }; diff --git a/packages/service/type/env.d.ts b/packages/service/type/env.d.ts index 47cc83a1d3..837ff8c675 100644 --- a/packages/service/type/env.d.ts +++ b/packages/service/type/env.d.ts @@ -18,6 +18,7 @@ declare global { VECTOR_VQ_LEVEL: string; PG_URL: string; OCEANBASE_URL: string; + SEEKDB_URL: string; MILVUS_ADDRESS: string; MILVUS_TOKEN: string; diff --git a/packages/web/components/common/DateTimePicker/index.tsx b/packages/web/components/common/DateTimePicker/index.tsx index 875fee432f..109c5de26e 100644 --- a/packages/web/components/common/DateTimePicker/index.tsx +++ b/packages/web/components/common/DateTimePicker/index.tsx @@ -14,6 +14,7 @@ const DateTimePicker = ({ defaultDate, selectedDateTime, disabled, + isDisabled, ...props }: { onChange?: (dateTime: Date | undefined) => void; @@ -21,6 +22,7 @@ const DateTimePicker = ({ defaultDate?: Date; selectedDateTime?: Date; disabled?: Matcher[]; + isDisabled?: boolean; } & Omit) => { const containerRef = useRef(null); const popoverRef = useRef(null); @@ -111,11 +113,15 @@ const DateTimePicker = ({ pr={3} py={1} borderRadius={'sm'} - cursor={'pointer'} - bg={'myGray.50'} fontSize={'sm'} - onClick={() => setShowSelected((state) => !state)} alignItems={'center'} + {...(isDisabled + ? { cursor: 'not-allowed', bg: 'myGray.100', opacity: 0.6 } + : { + cursor: 'pointer', + bg: 'myGray.50', + onClick: () => setShowSelected((state) => !state) + })} {...props} > diff --git a/packages/web/components/core/plugin/tool/BatchUpdateDrawer.tsx b/packages/web/components/core/plugin/tool/BatchUpdateDrawer.tsx new file mode 100644 index 0000000000..b91a6d37c9 --- /dev/null +++ b/packages/web/components/core/plugin/tool/BatchUpdateDrawer.tsx @@ -0,0 +1,348 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { + Box, + Button, + Checkbox, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerOverlay, + Flex, + VStack, + Accordion +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import Avatar from '../../../common/Avatar'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import MyIconButton from '../../../common/Icon/button'; +import LightRowTabs from '../../../common/Tabs/LightRowTabs'; +import { type ToolCardItemType } from './ToolCard'; +import MyBox from '../../../common/MyBox'; +import Markdown from '../../../common/Markdown'; +import type { GetTeamToolDetailResponseType } from '@fastgpt/global/openapi/core/plugin/team/toolApi'; +import { useTableMultipleSelect } from '../../../../hooks/useTableMultipleSelect'; +import { + ParamSection, + SubToolAccordionItem, + useToolDetail, + drawerScrollbarStyles +} from './ToolDetail'; + +type ViewMode = 'list' | 'detail'; + +interface BatchUpdateDrawerProps { + isOpen: boolean; + onClose: () => void; + updatableTools: ToolCardItemType[]; + onBatchUpdate: (toolIds: string[]) => Promise; + isBatchUpdating: boolean; + onFetchDetail?: (toolId: string) => Promise; +} + +const BatchUpdateDrawer: React.FC = ({ + isOpen, + onClose, + updatableTools, + onBatchUpdate, + isBatchUpdating, + onFetchDetail +}) => { + const { t, i18n } = useTranslation(); + const [viewMode, setViewMode] = useState('list'); + const [selectedToolForDetail, setSelectedToolForDetail] = useState(null); + const [activeTab, setActiveTab] = useState<'guide' | 'params'>('params'); + const [isUpdatingSingle, setIsUpdatingSingle] = useState(false); + + // Use table multiple select hook + const { + selectedItems, + isSelecteAll, + selectAllTrigger, + hasSelections, + toggleSelect, + isSelected, + setSelectedItems + } = useTableMultipleSelect({ + list: updatableTools, + getItemId: (tool: ToolCardItemType) => tool.id + }); + + // Use tool detail hook + const { parentTool, isToolSet, subTools, readmeContent, loadingDetail } = useToolDetail({ + toolId: selectedToolForDetail?.id, + tags: selectedToolForDetail?.tags || undefined, + onFetchDetail, + autoFetch: viewMode === 'detail' + }); + + // Reset view mode when drawer closes + useEffect(() => { + if (!isOpen) { + setViewMode('list'); + setSelectedToolForDetail(null); + setActiveTab('params'); + setSelectedItems([]); + } + }, [isOpen, setSelectedItems]); + + const handleViewDetail = useCallback((tool: ToolCardItemType) => { + setSelectedToolForDetail(tool); + setViewMode('detail'); + }, []); + + const handleBack = useCallback(() => { + setViewMode('list'); + setSelectedToolForDetail(null); + setActiveTab('params'); + }, []); + + const handleUpdateSingle = useCallback(async () => { + if (!selectedToolForDetail) return; + + setIsUpdatingSingle(true); + try { + await onBatchUpdate([selectedToolForDetail.id]); + // Go back to list view after successful update + handleBack(); + } finally { + setIsUpdatingSingle(false); + } + }, [selectedToolForDetail, onBatchUpdate, handleBack]); + + return ( + + + + + {viewMode === 'list' ? ( + + + {t('app:toolkit_updatable_plugins')} + + + + + ) : ( + + + + {parseI18nString(parentTool?.name || '', i18n.language)} + + + + + )} + + + + {viewMode === 'list' ? ( + + {updatableTools.map((tool) => ( + toggleSelect(tool)} + > + e.stopPropagation()} mr={3} align="center"> + toggleSelect(tool)} /> + + + + + {parseI18nString(tool.name, i18n.language)} + + + { + e.stopPropagation(); + handleViewDetail(tool); + }} + > + {t('common:view_detail')} + + + ))} + + {/* Bottom action bar - Always visible */} + + + + + {t('common:select_count_num', { num: selectedItems.length })} + + + + + + ) : ( + + + {parentTool?.tags?.map((tag: string) => ( + + {tag} + + ))} + + + {parseI18nString(parentTool?.description || '', i18n.language)} + + + {`by ${parentTool?.author || 'FastGPT'}`} + + + + + + + + {t('app:toolkit_activation_label')} + + + {parentTool?.hasSystemSecret || + (parentTool?.secretInputConfig && parentTool?.secretInputConfig.length > 0) || + (parentTool?.inputList && parentTool?.inputList.length > 0) + ? t('app:toolkit_activation_required') + : t('app:toolkit_activation_not_required')} + + + + + { + if (value === 'guide' && parentTool?.courseUrl) { + window.open(parentTool?.courseUrl, '_blank'); + } else { + setActiveTab(value as 'guide' | 'params'); + } + }} + gap={4} + /> + + + + + {activeTab === 'guide' && ( + + {(readmeContent || parentTool?.userGuide) && ( + + + + )} + + )} + + {activeTab === 'params' && ( + + {isToolSet && subTools.length > 0 && ( + + {subTools.map((subTool) => ( + + ))} + + )} + + {!isToolSet && ( + <> + {parentTool?.versionList?.[0]?.inputs && + parentTool?.versionList?.[0]?.inputs.length > 0 && ( + + )} + {parentTool?.versionList?.[0]?.outputs && + parentTool?.versionList?.[0]?.outputs.length > 0 && ( + + )} + + )} + + )} + + + )} + + + + ); +}; + +export default React.memo(BatchUpdateDrawer); diff --git a/packages/web/components/core/plugin/tool/ToolDetail/components.tsx b/packages/web/components/core/plugin/tool/ToolDetail/components.tsx new file mode 100644 index 0000000000..819faab2c8 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetail/components.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { + Box, + Flex, + VStack, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant'; +import type { + FlowNodeInputItemType, + FlowNodeOutputItemType +} from '@fastgpt/global/core/workflow/type/io'; +import type { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import type { ToolDetailExtendedType } from './types'; + +export const ParamSection = ({ + title, + params +}: { + title: string; + params: (FlowNodeInputItemType | FlowNodeOutputItemType)[]; +}) => { + const { i18n } = useTranslation(); + + return ( + + + + + {title} + + + {params.map((param, index) => { + const isInput = 'required' in param; + return ( + + + {isInput && param.required && ( + + * + + )} + {parseI18nString(param.label || param.key, i18n.language)} + + {FlowValueTypeMap[param.valueType as WorkflowIOValueTypeEnum]?.label || 'String'} + + + {param.description && ( + + {parseI18nString(param.description, i18n.language)} + + )} + {index !== params.length - 1 && } + + ); + })} + + ); +}; + +export const SubToolAccordionItem = ({ tool }: { tool: ToolDetailExtendedType }) => { + const { t, i18n } = useTranslation(); + + return ( + + + + + {parseI18nString(tool.name, i18n.language)} + + + {tool.intro || parseI18nString(tool.description, i18n.language)} + + + + + + + {tool.versionList && tool.versionList.length > 0 && ( + + {tool.versionList[0]?.inputs && tool.versionList[0].inputs.length > 0 && ( + + )} + {tool.versionList[0]?.outputs && tool.versionList[0].outputs.length > 0 && ( + + )} + + )} + + + ); +}; diff --git a/packages/web/components/core/plugin/tool/ToolDetail/hooks.ts b/packages/web/components/core/plugin/tool/ToolDetail/hooks.ts new file mode 100644 index 0000000000..c99962ecb8 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetail/hooks.ts @@ -0,0 +1,109 @@ +import { useState, useEffect, useMemo } from 'react'; +import type { ToolDetailResponseType, ToolDetailExtendedType } from './types'; +import type { GetTeamToolDetailResponseType } from '@fastgpt/global/openapi/core/plugin/team/toolApi'; +import { useRequest } from '../../../../../hooks/useRequest'; + +export type UseToolDetailProps = { + toolId?: string; + tags?: string[]; + onFetchDetail?: (toolId: string) => Promise; + autoFetch?: boolean; +}; + +export const useToolDetail = ({ + toolId, + tags, + onFetchDetail, + autoFetch = true +}: UseToolDetailProps) => { + const [readmeContent, setReadmeContent] = useState(''); + + // 使用 useRequest2 替代手动的 useEffect,避免无限请求问题 + const { + data: toolDetail, + loading: loadingDetail, + run: fetchToolDetail + } = useRequest( + async (id: string) => { + if (!onFetchDetail) return undefined; + const detail = await onFetchDetail(id); + return detail as any as ToolDetailResponseType; + }, + { + manual: true, + errorToast: '' + } + ); + + // 自动获取工具详情 + useEffect(() => { + if (toolId && autoFetch && onFetchDetail) { + fetchToolDetail(toolId); + } + }, [toolId, autoFetch]); + + // Calculate tool structure + const isToolSet = useMemo(() => { + if (!toolDetail?.tools || !Array.isArray(toolDetail?.tools) || toolDetail?.tools.length === 0) { + return false; + } + const subTools = toolDetail?.tools.filter((subTool: any) => subTool.parentId); + return subTools.length > 0; + }, [toolDetail?.tools]); + + const parentTool = useMemo(() => { + const parentTool = toolDetail?.tools.find((tool: ToolDetailExtendedType) => !tool.parentId); + return { + ...parentTool, + tags + }; + }, [tags, toolDetail?.tools]); + + const subTools = useMemo(() => { + if (!isToolSet || !toolDetail?.tools) return []; + return toolDetail?.tools.filter((subTool: ToolDetailExtendedType) => !!subTool.parentId); + }, [isToolSet, toolDetail?.tools]); + + // Fetch README + useEffect(() => { + const fetchReadme = async () => { + if (!toolDetail) return; + const readmeUrl = parentTool?.readme; + if (!readmeUrl) return; + + try { + const response = await fetch(readmeUrl); + if (!response.ok) { + throw new Error(`Failed to fetch README: ${response.status}`); + } + let content = await response.text(); + + const baseUrl = readmeUrl.substring(0, readmeUrl.lastIndexOf('/') + 1); + + content = content.replace( + /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, + (match, alt, path) => `![${alt}](${baseUrl}${path})` + ); + content = content.replace( + /!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g, + (match, alt, path) => `![${alt}](${baseUrl}${path})` + ); + setReadmeContent(content); + } catch (error) { + console.error('Failed to fetch README:', error); + setReadmeContent(''); + } + }; + + fetchReadme(); + }, [toolDetail, parentTool?.readme]); + + return { + toolDetail, + loadingDetail, + readmeContent, + isToolSet, + parentTool, + subTools + }; +}; diff --git a/packages/web/components/core/plugin/tool/ToolDetail/index.ts b/packages/web/components/core/plugin/tool/ToolDetail/index.ts new file mode 100644 index 0000000000..d1176cde89 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetail/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './components'; +export * from './hooks'; +export * from './styles'; diff --git a/packages/web/components/core/plugin/tool/ToolDetail/styles.ts b/packages/web/components/core/plugin/tool/ToolDetail/styles.ts new file mode 100644 index 0000000000..93e40fce91 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetail/styles.ts @@ -0,0 +1,19 @@ +export const drawerScrollbarStyles = { + overflowY: 'overlay' as any, + '&::-webkit-scrollbar': { + width: '6px', + position: 'absolute' + }, + '&::-webkit-scrollbar-track': { + background: 'transparent' + }, + '&::-webkit-scrollbar-thumb': { + background: 'myGray.300', + borderRadius: '3px' + }, + '&::-webkit-scrollbar-thumb:hover': { + background: 'myGray.400' + }, + scrollbarWidth: 'thin', + scrollbarColor: 'var(--chakra-colors-myGray-300) transparent' +}; diff --git a/packages/web/components/core/plugin/tool/ToolDetail/types.ts b/packages/web/components/core/plugin/tool/ToolDetail/types.ts new file mode 100644 index 0000000000..cbf6ac75ae --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetail/types.ts @@ -0,0 +1,27 @@ +import type { ToolDetailType } from '@fastgpt/global/sdk/fastgpt-plugin'; +import type { + FlowNodeInputItemType, + FlowNodeOutputItemType +} from '@fastgpt/global/core/workflow/type/io'; + +export type ToolDetailExtendedType = ToolDetailType & { + versionList?: Array<{ + value: string; + description?: string; + inputs?: Array; + outputs?: Array; + }>; + courseUrl?: string; + readme?: string; + userGuide?: string; + currentCost?: number; + hasSystemSecret?: boolean; + secretInputConfig?: Array<{}>; + inputList?: Array; + intro?: string; +}; + +export type ToolDetailResponseType = { + tools: Array; + downloadUrl: string; +}; diff --git a/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx index c9f2b4944b..b9ef6d4d03 100644 --- a/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx +++ b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState } from 'react'; import { Box, Button, @@ -9,155 +9,23 @@ import { DrawerOverlay, Flex, VStack, - Accordion, - AccordionItem, - AccordionButton, - AccordionPanel, - AccordionIcon + Accordion } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import Avatar from '../../../common/Avatar'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import MyIconButton from '../../../common/Icon/button'; import LightRowTabs from '../../../common/Tabs/LightRowTabs'; -import type { - FlowNodeInputItemType, - FlowNodeOutputItemType -} from '@fastgpt/global/core/workflow/type/io'; import { type ToolCardItemType } from './ToolCard'; import MyBox from '../../../common/MyBox'; import Markdown from '../../../common/Markdown'; -import type { ToolDetailType } from '@fastgpt/global/sdk/fastgpt-plugin'; -import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant'; import type { GetTeamToolDetailResponseType } from '@fastgpt/global/openapi/core/plugin/team/toolApi'; -import type { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; - -type toolDetailType = ToolDetailType & { - versionList?: Array<{ - value: string; - description?: string; - inputs?: Array; - outputs?: Array; - }>; - courseUrl?: string; - readme?: string; - userGuide?: string; - currentCost?: number; - hasSystemSecret?: boolean; - secretInputConfig?: Array<{}>; - inputList?: Array; -}; - -const ParamSection = ({ - title, - params -}: { - title: string; - params: (FlowNodeInputItemType | FlowNodeOutputItemType)[]; -}) => { - const { i18n } = useTranslation(); - - return ( - - - - - {title} - - - {params.map((param, index) => { - const isInput = 'required' in param; - return ( - - - {isInput && param.required && ( - - * - - )} - {parseI18nString(param.label || param.key, i18n.language)} - - {FlowValueTypeMap[param.valueType as WorkflowIOValueTypeEnum]?.label || 'String'} - - - {param.description && ( - - {parseI18nString(param.description, i18n.language)} - - )} - {index !== params.length - 1 && } - - ); - })} - - ); -}; - -const SubToolAccordionItem = ({ tool }: { tool: any }) => { - const { t, i18n } = useTranslation(); - - return ( - - - - - {parseI18nString(tool.name, i18n.language)} - - - {tool.intro || parseI18nString(tool.description, i18n.language)} - - - - - - - {/* - - {!!tool?.currentCost ? ( - - {t('app:toolkit_call_points_label')} - {tool?.currentCost} - - ) : ( - t('app:toolkit_no_call_points') - )} - */} - {tool.versionList && tool.versionList.length > 0 && ( - - {tool.versionList[0]?.inputs && tool.versionList[0].inputs.length > 0 && ( - - )} - {tool.versionList[0]?.outputs && tool.versionList[0].outputs.length > 0 && ( - - )} - - )} - - - ); -}; +import { + ParamSection, + SubToolAccordionItem, + useToolDetail, + drawerScrollbarStyles +} from './ToolDetail'; const ToolDetailDrawer = ({ onClose, @@ -184,90 +52,23 @@ const ToolDetailDrawer = ({ }) => { const { t, i18n } = useTranslation(); const [activeTab, setActiveTab] = useState<'guide' | 'params'>('params'); - const [toolDetail, setToolDetail] = useState< - { tools: Array; downloadUrl: string } | undefined - >(undefined); - const [loadingDetail, setLoading] = useState(false); - const [readmeContent, setReadmeContent] = useState(''); const [isInstalled, setIsInstalled] = useState(selectedTool.installed); const isDownload = useMemo(() => { return mode === 'marketplace'; }, [mode]); - useEffect(() => { - const fetchToolDetail = async () => { - if (onFetchDetail && selectedTool?.id) { - setLoading(true); - try { - const detail = await onFetchDetail(selectedTool.id); - setToolDetail(detail as any); - } finally { - setLoading(false); - } - } - }; - - fetchToolDetail(); - }, []); - - const isToolSet = useMemo(() => { - if (!toolDetail?.tools || !Array.isArray(toolDetail?.tools) || toolDetail?.tools.length === 0) { - return false; - } - const subTools = toolDetail?.tools.filter((subTool: any) => subTool.parentId); - return subTools.length > 0; - }, [toolDetail?.tools]); - - const parentTool = useMemo(() => { - const parentTool = toolDetail?.tools.find((tool: toolDetailType) => !tool.parentId); - return { - ...parentTool, - tags: selectedTool.tags - }; - }, [selectedTool.tags, toolDetail?.tools]); - const subTools = useMemo(() => { - if (!isToolSet || !toolDetail?.tools) return []; - return toolDetail?.tools.filter((subTool: toolDetailType) => !!subTool.parentId); - }, [isToolSet, toolDetail?.tools]); - - useEffect(() => { - const fetchReadme = async () => { - if (!toolDetail) return; - const readmeUrl = parentTool?.readme; - if (!readmeUrl) return; - - try { - const response = await fetch(readmeUrl); - if (!response.ok) { - throw new Error(`Failed to fetch README: ${response.status}`); - } - let content = await response.text(); - - const baseUrl = readmeUrl.substring(0, readmeUrl.lastIndexOf('/') + 1); - - content = content.replace( - /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, - (match, alt, path) => `![${alt}](${baseUrl}${path})` - ); - content = content.replace( - /!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g, - (match, alt, path) => `![${alt}](${baseUrl}${path})` - ); - setReadmeContent(content); - } catch (error) { - console.error('Failed to fetch README:', error); - setReadmeContent(''); - } - }; - - fetchReadme(); - }, [parentTool?.readme]); + // Use tool detail hook + const { parentTool, isToolSet, subTools, readmeContent, loadingDetail } = useToolDetail({ + toolId: selectedTool.id, + tags: selectedTool.tags || undefined, + onFetchDetail + }); return ( - + @@ -279,28 +80,7 @@ const ToolDetailDrawer = ({ - + {parentTool?.tags?.map((tag: string) => ( @@ -419,7 +199,7 @@ const ToolDetailDrawer = ({ {activeTab === 'guide' && ( - + {(readmeContent || parentTool?.userGuide) && ( @@ -446,7 +226,7 @@ const ToolDetailDrawer = ({ allowMultiple {...(subTools.length === 1 ? { defaultIndex: [0] } : {})} > - {subTools.map((subTool: ToolDetailType) => ( + {subTools.map((subTool) => ( ))} diff --git a/packages/web/i18n/en/account_bill.json b/packages/web/i18n/en/account_bill.json index 810c2a87cf..7662d5abbc 100644 --- a/packages/web/i18n/en/account_bill.json +++ b/packages/web/i18n/en/account_bill.json @@ -41,5 +41,6 @@ "type": "type", "unit_code": "unified credit code", "unit_code_void": "Unified credit code format error", - "update": "renew" + "update": "renew", + "wecom_not_pay_tip": "Not paid, payment address will be redirected soon" } diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 4c2a8abb39..81bd3d898d 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -247,6 +247,12 @@ "transfer_app_ownership": "Transfer app ownership", "transfer_dataset_ownership": "Transfer dataset ownership", "transfer_ownership": "Transfer ownership", + "transfer_team_ownership": "Transfer Team", + "transfer_success": "Transfer successful", + "transfer_failed": "Transfer failed", + "select_new_owner": "Select new owner", + "confirm_transfer": "Confirm transfer", + "transfer_warning": "Warning: After transferring team ownership, you will lose all administrative privileges, and this action cannot be undone. Please proceed with caution.", "type.Folder": "Folder", "type.Http plugin": "HTTP Plugin", "type.Plugin": "Plugin", diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 630c68fad1..680ded1fbb 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -357,6 +357,7 @@ "templateMarket.Use": "Build now", "templateMarket.no_intro": "No introduction yet~", "templateMarket.templateTags.Recommendation": "Recommendation", + "templateMarket.templateTags.WecomZone": "WeChat Work Zone", "templateMarket.template_guide": "Guide", "template_market": "Templates", "template_market_description": "Explore more features in the template market, with configuration tutorials and usage guides to help you understand and get started with various applications.", @@ -447,9 +448,17 @@ "toolkit_tool_config": "{{name}} Configuration", "toolkit_tool_list": "Tool List", "toolkit_tool_name": "Tool Name", + "toolkit_promote_tags": "Promote Tags", + "toolkit_promote_tags_tip": "Users with the following tags will see the \"Promoted\" badge", + "toolkit_hide_tags": "Hide Tags", + "toolkit_hide_tags_tip": "Users with the following tags will not see this tool at all", + "toolkit_select_user_tags": "Select user tags", "toolkit_uninstall": "Uninstall", "toolkit_uninstalled": "Uninstalled", "toolkit_update_failed": "Update failed", + "toolkit_updatable": "Updates Available", + "toolkit_updatable_plugins": "Updatable Plugins", + "toolkit_batch_update": "Batch Update", "toolkit_user_guide": "User Guide", "tools_no_description": "This tool has not been introduced ~", "transition_to_workflow": "Convert to Workflow", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index b8a1aa208d..02cceb6c2d 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -85,6 +85,7 @@ "Select_App": "Select an application", "Select_all": "Select all", "Setting": "Setting", + "view_detail": "View Detail", "Status": "Status", "Submit": "Submit", "Success": "Success", @@ -956,6 +957,9 @@ "n_chat_records_retain": "{{amount}} Days of Chat History Retention", "n_custom_domain_amount": "{{amount}} Custom domains", "n_custom_domain_amount_tip": "The number of custom domain names that the team can configure, which can currently be used to access Wecom intelligent robots", + "n_max_upload_file_limit": "Upload up to {{count}} files of {{size}}MB each", + "n_max_upload_file_size": "Max {{amount}}MB per file", + "n_max_upload_file_count": "Upload up to {{amount}} files", "n_dataset_amount": "{{amount}} Dataset limit", "n_dataset_size": "{{amount}} Dataset Indexes", "n_team_audit_day": "{{amount}} days team operation log records", @@ -1151,6 +1155,7 @@ "support.user.login.Provider error": "Login Error, Please Try Again", "support.user.login.Username": "Username", "support.user.login.Wechat": "WeChat Login", + "support.user.login.Wecom": "Wecom Login", "support.user.login.can_not_login": "Cannot log in? Click here to contact us", "support.user.login.error": "Login Error", "support.user.login.security_failed": "Security Verification Failed", @@ -1181,6 +1186,7 @@ "support.wallet.bill.payWay.alipay": "Alipay Payment", "support.wallet.bill.payWay.balance": "Balance Payment", "support.wallet.bill.payWay.bank": "Bank Transfer", + "support.wallet.bill.payWay.wecom": "WeChat Work Payment", "support.wallet.bill.payWay.wx": "WeChat Payment", "support.wallet.bill.status.closed": "Closed", "support.wallet.bill.status.notpay": "Unpaid", @@ -1190,6 +1196,7 @@ "support.wallet.bill_tag.bill": "Bill Records", "support.wallet.bill_tag.default_header": "Default Header", "support.wallet.bill_tag.invoice": "Invoice Records", + "support.wallet.wecom_bill_tip": "Please go to WeCom - Checkout to query bills and apply for invoices", "support.wallet.billable_invoice": "Billable Invoice", "support.wallet.buy_ai_points": "Buy AI points", "support.wallet.buy_dataset_capacity": "Buy Knowledge Base Index", @@ -1224,6 +1231,7 @@ "support.wallet.subscription.Extra dataset unit": " Groups/1 Month", "support.wallet.subscription.Extra plan": "Extra Resource Pack", "support.wallet.subscription.Extra plan tip": "When the standard package is not enough, you can purchase extra resource packs to continue using", + "support.wallet.subscription.extra_plan_disabled_tip": "Please subscribe to a plan first before purchasing extra resource packs.", "support.wallet.subscription.FAQ": "FAQ", "support.wallet.subscription.Month amount": "Months", "support.wallet.subscription.Next plan": "Future Package", @@ -1231,6 +1239,7 @@ "support.wallet.subscription.Stand plan level": "Subscription Package", "support.wallet.subscription.Sub plan": "Subscription Package", "support.wallet.subscription.Sub plan tip": "Free to use [{{title}}] or upgrade to a higher package", + "support.wallet.subscription.Sub plan tip wecom": "Purchase a plan to enjoy application services", "support.wallet.subscription.Team plan and usage": "Package and Usage", "support.wallet.subscription.Training weight": "Training Priority: {{weight}}", "support.wallet.subscription.Update extra ai points": "Extra AI Points", @@ -1258,6 +1267,9 @@ "support.wallet.subscription.standardSubLevel.experience_desc": "Unlock the full functionality of FastGPT", "support.wallet.subscription.standardSubLevel.free": "Free", "support.wallet.subscription.standardSubLevel.free desc": "Free trial of core features. \nIf you haven't logged in for 30 days, the knowledge base will be cleared.", + "support.wallet.subscription.standardSubLevel.trial": "Trial", + "support.wallet.subscription.standardSubLevel.trial_desc": "Enterprises can try for free for 15 days, starting from the activation of the application, limited to one experience per enterprise.", + "support.wallet.subscription.per_year": "/ year", "support.wallet.subscription.standardSubLevel.team": "Team", "support.wallet.subscription.standardSubLevel.team_desc": "Suitable for small teams to build Dataset applications and provide external services", "support.wallet.subscription.status.active": "Active", diff --git a/packages/web/i18n/en/file.json b/packages/web/i18n/en/file.json index 279f6182f2..391f96815f 100644 --- a/packages/web/i18n/en/file.json +++ b/packages/web/i18n/en/file.json @@ -35,8 +35,6 @@ "some_file_count_exceeds_limit": "Exceeded {{maxCount}} files, automatically truncated", "some_file_size_exceeds_limit": "Some files exceed {{maxSize}}, filtered out", "support_file_type": "Supports {{fileType}} file types", - "support_max_count": "Supports up to {{maxCount}} files", - "support_max_size": "Maximum file size is {{maxSize}}", "template_csv_file_select_tip": "Only support {{fileType}} files that are strictly in accordance with template format", "template_strict_highlight": "Strictly follow the template", "total_files": "Total {{selectFiles.length}} files", diff --git a/packages/web/i18n/zh-CN/account_bill.json b/packages/web/i18n/zh-CN/account_bill.json index c9e6a09f57..02478ef474 100644 --- a/packages/web/i18n/zh-CN/account_bill.json +++ b/packages/web/i18n/zh-CN/account_bill.json @@ -41,5 +41,6 @@ "type": "类型", "unit_code": "统一信用代码", "unit_code_void": "统一信用代码格式错误", - "update": "更新" + "update": "更新", + "wecom_not_pay_tip": "未支付,即将跳转支付地址" } diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 7ef5c48b62..9c1ef64141 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -251,6 +251,12 @@ "transfer_app_ownership": "转移应用所有权", "transfer_dataset_ownership": "转移知识库所有权", "transfer_ownership": "转让所有者", + "transfer_team_ownership": "转让团队", + "transfer_success": "转让成功", + "transfer_failed": "转让失败", + "select_new_owner": "选择新的所有者", + "confirm_transfer": "确认转让", + "transfer_warning": "警告:转让团队所有权后,您将失去所有管理权限,且此操作不可撤销。请谨慎操作。", "type.Folder": "文件夹", "type.Http plugin": "HTTP 插件", "type.Http tool set": "HTTP 工具集", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 9090016be7..4d27da2e6b 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -371,6 +371,7 @@ "templateMarket.Use": "立即搭建", "templateMarket.no_intro": "还没有介绍~", "templateMarket.templateTags.Recommendation": "推荐", + "templateMarket.templateTags.WecomZone": "企微专区", "templateMarket.template_guide": "说明", "template_market": "模板市场", "template_market_description": "在模板市场探索更多玩法,配置教程与使用引导,带你理解并上手各种应用", @@ -464,9 +465,17 @@ "toolkit_tool_config": "{{name}}配置", "toolkit_tool_list": "工具列表", "toolkit_tool_name": "工具名", + "toolkit_promote_tags": "推荐标签", + "toolkit_promote_tags_tip": "拥有以下标签的用户会看到\"推荐\"标识", + "toolkit_hide_tags": "隐藏标签", + "toolkit_hide_tags_tip": "拥有以下标签的用户将完全看不到此工具", + "toolkit_select_user_tags": "选择用户标签", "toolkit_uninstall": "卸载", "toolkit_uninstalled": "未安装", "toolkit_update_failed": "更新失败", + "toolkit_updatable": "可更新", + "toolkit_updatable_plugins": "可更新的插件", + "toolkit_batch_update": "批量更新", "toolkit_user_guide": "使用说明", "tools_no_description": "这个工具没有介绍~", "transition_to_workflow": "转成工作流", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 38888f452f..20812dc263 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -86,6 +86,7 @@ "Select_App": "选择应用", "Select_all": "全选", "Setting": "设置", + "view_detail": "查看详情", "Status": "状态", "Submit": "提交", "Success": "成功", @@ -965,6 +966,9 @@ "n_custom_domain_amount_tip": "团队可以配置的自定义域名数量,目前可用于接入企微智能机器人", "n_dataset_amount": "{{amount}} 个知识库", "n_dataset_size": "{{amount}} 组知识库索引", + "n_max_upload_file_count": "单次可上传 {{amount}} 个文件", + "n_max_upload_file_limit": "单次可上传 {{count}} 个 {{size}} 的文件", + "n_max_upload_file_size": "单个文件最大 {{amount}}MB", "n_team_audit_day": "{{amount}} 天团队操作日志记录", "n_team_members": "{{amount}} 个团队成员", "n_team_qpm": "{{amount}} QPM", @@ -1160,6 +1164,7 @@ "support.user.login.Provider error": "登录异常,请重试", "support.user.login.Username": "用户名", "support.user.login.Wechat": "微信登录", + "support.user.login.Wecom": "企业微信登录", "support.user.login.can_not_login": "无法登录,点击联系", "support.user.login.error": "登录异常", "support.user.login.security_failed": "安全校验失败", @@ -1190,6 +1195,7 @@ "support.wallet.bill.payWay.alipay": "支付宝支付", "support.wallet.bill.payWay.balance": "余额支付", "support.wallet.bill.payWay.bank": "对公支付", + "support.wallet.bill.payWay.wecom": "企业微信支付", "support.wallet.bill.payWay.wx": "微信支付", "support.wallet.bill.status.closed": "已关闭", "support.wallet.bill.status.notpay": "未支付", @@ -1240,6 +1246,7 @@ "support.wallet.subscription.Stand plan level": "订阅套餐", "support.wallet.subscription.Sub plan": "订阅套餐", "support.wallet.subscription.Sub plan tip": "免费使用【{{title}}】或升级更高的套餐", + "support.wallet.subscription.Sub plan tip wecom": "购买套餐以享受应用服务", "support.wallet.subscription.Team plan and usage": "套餐与用量", "support.wallet.subscription.Training weight": "训练优先级:{{weight}}", "support.wallet.subscription.Update extra ai points": "额外 AI 积分", @@ -1250,6 +1257,7 @@ "support.wallet.subscription.Upgrade plan": "升级套餐", "support.wallet.subscription.ai_model": "AI语言模型", "support.wallet.subscription.eval_items_count": "单次评测数据条数: {{count}} 条", + "support.wallet.subscription.extra_plan_disabled_tip": "如需购买额外资源包,请先订阅套餐。", "support.wallet.subscription.function.Community support tip": "可前往 FastGPT 社区免费获取帮助和技术支持", "support.wallet.subscription.mode.Month": "按月", "support.wallet.subscription.mode.Period": "订阅周期", @@ -1268,8 +1276,11 @@ "support.wallet.subscription.standardSubLevel.experience_desc": "可解锁 FastGPT 完整功能", "support.wallet.subscription.standardSubLevel.free": "免费版", "support.wallet.subscription.standardSubLevel.free desc": "核心功能免费试用。30 天未登录,将会清空知识库。", + "support.wallet.subscription.per_year": "/ 年", "support.wallet.subscription.standardSubLevel.team": "团队版", "support.wallet.subscription.standardSubLevel.team_desc": "适合小团队构建知识库应用并提供对外服务", + "support.wallet.subscription.standardSubLevel.trial": "试用版", + "support.wallet.subscription.standardSubLevel.trial_desc": "企业可免费试用15天,自激活应用时起计,每个企业限体验一次。", "support.wallet.subscription.status.active": "生效中", "support.wallet.subscription.status.expired": "已过期", "support.wallet.subscription.status.inactive": "待使用", @@ -1297,6 +1308,7 @@ "support.wallet.usage.Total points": "AI 积分总消耗", "support.wallet.usage.Usage Detail": "使用详情", "support.wallet.usage.Whisper": "语音输入", + "support.wallet.wecom_bill_tip": "请前往企微-收银台进行账单查询和开票", "sure_delete_tool_cannot_undo": "是否确认删除该工具?该操作无法撤回", "sync_link": "同步链接", "sync_success": "同步成功", diff --git a/packages/web/i18n/zh-CN/file.json b/packages/web/i18n/zh-CN/file.json index ba13cd7720..c87ec0dc09 100644 --- a/packages/web/i18n/zh-CN/file.json +++ b/packages/web/i18n/zh-CN/file.json @@ -35,8 +35,6 @@ "some_file_count_exceeds_limit": "超出 {{maxCount}} 个文件,已自动截取", "some_file_size_exceeds_limit": "部分文件超出 {{maxSize}},已被过滤", "support_file_type": "支持 {{fileType}} 类型文件", - "support_max_count": "最多支持 {{maxCount}} 个文件", - "support_max_size": "单个文件最大 {{maxSize}}", "template_csv_file_select_tip": "仅支持严格按照模板填写的 {{fileType}} 文件", "template_strict_highlight": "严格按照模版", "total_files": "共{{selectFiles.length}}个文件", diff --git a/packages/web/i18n/zh-Hant/account_bill.json b/packages/web/i18n/zh-Hant/account_bill.json index 3cea5ee45f..0e7124dd81 100644 --- a/packages/web/i18n/zh-Hant/account_bill.json +++ b/packages/web/i18n/zh-Hant/account_bill.json @@ -41,5 +41,6 @@ "type": "類型", "unit_code": "統一信用代碼", "unit_code_void": "統一信用代碼格式錯誤", - "update": "更新" + "update": "更新", + "wecom_not_pay_tip": "未支付,即將跳轉支付地址" } diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index 1ed5dbae1b..88b0ea7d11 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -247,6 +247,12 @@ "transfer_app_ownership": "轉移應用程式所有權", "transfer_dataset_ownership": "轉移知識庫所有權", "transfer_ownership": "轉讓所有者", + "transfer_team_ownership": "轉讓團隊", + "transfer_success": "轉讓成功", + "transfer_failed": "轉讓失敗", + "select_new_owner": "選擇新的所有者", + "confirm_transfer": "確認轉讓", + "transfer_warning": "警告:轉讓團隊所有權後,您將失去所有管理權限,且此操作不可撤銷。請謹慎操作。", "type.Folder": "資料夾", "type.Http plugin": "HTTP 外掛", "type.Plugin": "外掛", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 6132cfbc94..da7cfb7ead 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -356,6 +356,7 @@ "templateMarket.Use": "立即搭建", "templateMarket.no_intro": "還沒有介紹~", "templateMarket.templateTags.Recommendation": "推薦", + "templateMarket.templateTags.WecomZone": "企微專區", "templateMarket.template_guide": "說明", "template_market": "範本市集", "template_market_description": "在範本市集探索更多玩法,設定教學與使用指引,帶您理解並上手各種應用程式", @@ -445,9 +446,17 @@ "toolkit_tool_config": "{{name}}配置", "toolkit_tool_list": "工具列表", "toolkit_tool_name": "工具名", + "toolkit_promote_tags": "推薦標籤", + "toolkit_promote_tags_tip": "擁有以下標籤的使用者會看到「推薦」標識", + "toolkit_hide_tags": "隱藏標籤", + "toolkit_hide_tags_tip": "擁有以下標籤的使用者將完全看不到此工具", + "toolkit_select_user_tags": "選擇使用者標籤", "toolkit_uninstall": "卸載", "toolkit_uninstalled": "未安裝", "toolkit_update_failed": "更新失敗", + "toolkit_updatable": "可更新", + "toolkit_updatable_plugins": "可更新的外掛程式", + "toolkit_batch_update": "批次更新", "toolkit_user_guide": "使用說明", "tools_no_description": "這個工具沒有介紹~", "transition_to_workflow": "轉換成工作流程", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index f7ae284058..bb115b4f9b 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -85,6 +85,7 @@ "Select_App": "選擇應用", "Select_all": "全選", "Setting": "設定", + "view_detail": "檢視詳情", "Status": "狀態", "Submit": "送出", "Success": "成功", @@ -956,6 +957,9 @@ "n_custom_domain_amount_tip": "團隊可以配置的自定義域名數量,目前可用於接入企微智能機器人", "n_dataset_amount": "{{amount}} 個知識庫", "n_dataset_size": "{{amount}} 組知識庫索引", + "n_max_upload_file_count": "單次可上傳 {{amount}} 個文件", + "n_max_upload_file_limit": "單次可上傳 {{count}} 個 {{size}}MB 的文件", + "n_max_upload_file_size": "單個文件最大 {{amount}}MB", "n_team_audit_day": "{{amount}} 天團隊操作日誌記錄", "n_team_members": "{{amount}} 個團隊成員", "n_team_qpm": "{{amount}} QPM", @@ -1178,6 +1182,7 @@ "support.wallet.bill.payWay.alipay": "支付寶支付", "support.wallet.bill.payWay.balance": "餘額支付", "support.wallet.bill.payWay.bank": "對公支付", + "support.wallet.bill.payWay.wecom": "企業微信支付", "support.wallet.bill.payWay.wx": "微信支付", "support.wallet.bill.status.closed": "已關閉", "support.wallet.bill.status.notpay": "未付款", @@ -1187,6 +1192,7 @@ "support.wallet.bill_tag.bill": "帳單紀錄", "support.wallet.bill_tag.default_header": "預設抬頭", "support.wallet.bill_tag.invoice": "發票紀錄", + "support.wallet.wecom_bill_tip": "請前往企微-收銀台進行帳單查詢和開票", "support.wallet.billable_invoice": "可開立發票的帳單", "support.wallet.buy_ai_points": "購買 AI 積分", "support.wallet.buy_dataset_capacity": "購買知識庫索引量", @@ -1221,6 +1227,7 @@ "support.wallet.subscription.Extra dataset unit": "組/1個月", "support.wallet.subscription.Extra plan": "額外資源包", "support.wallet.subscription.Extra plan tip": "當標準方案不足時,您可以購買額外資源包繼續使用", + "support.wallet.subscription.extra_plan_disabled_tip": "如需購買額外資源包,請先訂閱方案。", "support.wallet.subscription.FAQ": "常見問題", "support.wallet.subscription.Month amount": "月數", "support.wallet.subscription.Next plan": "未來方案", @@ -1228,6 +1235,7 @@ "support.wallet.subscription.Stand plan level": "訂閱方案", "support.wallet.subscription.Sub plan": "訂閱方案", "support.wallet.subscription.Sub plan tip": "免費使用【{{title}}】或升級更進階的方案", + "support.wallet.subscription.Sub plan tip wecom": "購買方案以享受應用服務", "support.wallet.subscription.Team plan and usage": "方案與使用量", "support.wallet.subscription.Training weight": "訓練優先權:{{weight}}", "support.wallet.subscription.Update extra ai points": "額外 AI 點數", @@ -1255,6 +1263,9 @@ "support.wallet.subscription.standardSubLevel.experience_desc": "可解鎖 FastGPT 完整功能", "support.wallet.subscription.standardSubLevel.free": "免費版", "support.wallet.subscription.standardSubLevel.free desc": "核心功能免費試用。 \n30 天未登錄,將會清空知識庫。", + "support.wallet.subscription.standardSubLevel.trial": "試用版", + "support.wallet.subscription.standardSubLevel.trial_desc": "企業可免費試用15天,自啟用應用時起計,每個企業限體驗一次。", + "support.wallet.subscription.per_year": "/ 年", "support.wallet.subscription.standardSubLevel.team": "團隊版", "support.wallet.subscription.standardSubLevel.team_desc": "適合小團隊建構知識庫應用並提供對外服務", "support.wallet.subscription.status.active": "使用中", diff --git a/packages/web/i18n/zh-Hant/file.json b/packages/web/i18n/zh-Hant/file.json index 61042ae84a..eaa08362c0 100644 --- a/packages/web/i18n/zh-Hant/file.json +++ b/packages/web/i18n/zh-Hant/file.json @@ -35,8 +35,6 @@ "some_file_count_exceeds_limit": "已超過 {{maxCount}} 個檔案上限,系統已自動截斷", "some_file_size_exceeds_limit": "部分檔案超過 {{maxSize}} 大小限制,已自動過濾", "support_file_type": "支援 {{fileType}} 格式的檔案", - "support_max_count": "最多可支援 {{maxCount}} 個檔案", - "support_max_size": "單一檔案大小上限為 {{maxSize}}", "template_csv_file_select_tip": "僅支持嚴格按照模板格式的 {{fileType}} 文件", "template_strict_highlight": "嚴格按照模版", "total_files": "共{{selectFiles.length}}個文件", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 376f17d029..1661f2929b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^1.2.8 version: 1.2.8 '@fastgpt-sdk/plugin': - specifier: 0.2.17 - version: 0.2.17(@types/node@20.14.0) + specifier: 0.3.6 + version: 0.3.6 axios: specifier: ^1.13.2 version: 1.13.2 @@ -2605,8 +2605,8 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastgpt-sdk/plugin@0.2.17': - resolution: {integrity: sha512-TU93FD9JIeAV+isoLVVbW+yX14J27Kgd5Sn8LPvYWkrorUEtWeVfd8rOzh/KXPd43hCgD3bSDZ1W3hC06Spnog==} + '@fastgpt-sdk/plugin@0.3.6': + resolution: {integrity: sha512-glaLoTzrK9uvzAaIshVD6KpAE1ky0QOtX2Crbpbz0qZ1ERnlgI1q8N3vjx/qYih5t1ocUYeMce1SN7j84OxTCg==} '@fastgpt-sdk/storage@0.6.15': resolution: {integrity: sha512-oPbm6EtXQ3ysad/OebF2ovwbIax6PeCvYqA3cGAVEHEJMBU3633ktl1ZaIIkmyjWJLsABZpMf6m7lPBMyISGrA==} @@ -4628,17 +4628,6 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@ts-rest/core@3.52.1': - resolution: {integrity: sha512-tAjz7Kxq/grJodcTA1Anop4AVRDlD40fkksEV5Mmal88VoZeRKAG8oMHsDwdwPZz+B/zgnz0q2sF+cm5M7Bc7g==} - peerDependencies: - '@types/node': ^18.18.7 || >=20.8.4 - zod: ^3.22.3 - peerDependenciesMeta: - '@types/node': - optional: true - zod: - optional: true - '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -14716,13 +14705,10 @@ snapshots: '@eslint/js@8.57.1': {} - '@fastgpt-sdk/plugin@0.2.17(@types/node@20.14.0)': + '@fastgpt-sdk/plugin@0.3.6': dependencies: '@fortaine/fetch-event-source': 3.0.6 - '@ts-rest/core': 3.52.1(@types/node@20.14.0)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - '@types/node' + zod: 4.1.12 '@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: @@ -17180,11 +17166,6 @@ snapshots: '@trysound/sax@0.2.0': {} - '@ts-rest/core@3.52.1(@types/node@20.14.0)(zod@3.25.76)': - optionalDependencies: - '@types/node': 20.14.0 - zod: 3.25.76 - '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -20213,17 +20194,6 @@ snapshots: - supports-color eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -20303,7 +20273,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/projects/app/.env.template b/projects/app/.env.template index 9c1bbe2cc2..44b1f9cdd8 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -98,6 +98,12 @@ WORKFLOW_MAX_LOOP_TIMES=50 SERVICE_REQUEST_MAX_CONTENT_LENGTH=10 # 启用内网 IP 检查 CHECK_INTERNAL_IP=false + +# 文件上传 +# 最大上传文件大小,单位 MB +UPLOAD_FILE_MAX_SIZE=1000 +# 最大上传文件数量 +UPLOAD_FILE_MAX_AMOUNT=1000 # 密码错误锁时长:s PASSWORD_LOGIN_LOCK_SECONDS= # 密码过期月份,不设置则不会过期 diff --git a/projects/app/package.json b/projects/app/package.json index cf01068569..32b1be5677 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.14.5.1", + "version": "4.14.6", "private": false, "scripts": { "dev": "npm run build:workers && next dev", diff --git a/projects/app/public/imgs/modal/huggingface.svg b/projects/app/public/imgs/model/huggingface.svg similarity index 100% rename from projects/app/public/imgs/modal/huggingface.svg rename to projects/app/public/imgs/model/huggingface.svg diff --git a/projects/app/src/components/CommunityModal/index.tsx b/projects/app/src/components/CommunityModal/index.tsx index 18ee726397..15acd12a89 100644 --- a/projects/app/src/components/CommunityModal/index.tsx +++ b/projects/app/src/components/CommunityModal/index.tsx @@ -4,10 +4,13 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; import Markdown from '../Markdown'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; const CommunityModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); + const { userInfo } = useUserStore(); + const isWecomTeam = !!userInfo?.team.isWecomTeam; return ( void }) => { title={t('common:system.Concat us')} > - + {isWecomTeam ? ( + '邮箱联系: archer@fastgpt.io' + ) : ( + + )} diff --git a/projects/app/src/components/Select/FileSelectorBox.tsx b/projects/app/src/components/Select/FileSelectorBox.tsx index c5db5c1dc6..30991496bd 100644 --- a/projects/app/src/components/Select/FileSelectorBox.tsx +++ b/projects/app/src/components/Select/FileSelectorBox.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'next-i18next'; import React, { type DragEvent, useCallback, useMemo, useState } from 'react'; import { getFileIcon } from '@fastgpt/global/common/file/icon'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; export type SelectFileItemType = { file: File; @@ -21,7 +22,6 @@ const FileSelector = ({ selectFiles, setSelectFiles, maxCount = 1000, - maxSize, FileTypeNode, ...props }: { @@ -29,19 +29,25 @@ const FileSelector = ({ selectFiles: SelectFileItemType[]; setSelectFiles: (files: SelectFileItemType[]) => void; maxCount?: number; - maxSize?: string; FileTypeNode?: React.ReactNode; } & FlexProps) => { const { t } = useTranslation(); const { toast } = useToast(); const { feConfigs } = useSystemStore(); + const { teamPlanStatus } = useUserStore(); - const systemMaxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; - const displayMaxSize = maxSize || formatFileSize(systemMaxSize); - const formatMaxCount = feConfigs.uploadFileMaxAmount - ? Math.min(maxCount, feConfigs.uploadFileMaxAmount) - : maxCount; + // 文件大小限制(B):团队套餐 || 系统配置 || 默认值 + const displayMaxSize = formatFileSize( + (teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs.uploadFileMaxSize) * + 1024 * + 1024 + ); + // 文件数量限制:组件传入的maxCount || 团队套餐 || 系统配置 + const formatMaxCount = Math.min( + maxCount, + teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount + ); const { File, onOpen } = useSelectFile({ fileType, @@ -208,10 +214,14 @@ const FileSelector = ({ )} - {/* max count */} - {formatMaxCount && <>{t('file:support_max_count', { maxCount: formatMaxCount })}, } - {/* max size */} - {t('file:support_max_size', { maxSize: displayMaxSize })} + {formatMaxCount && ( + <> + {t('common:n_max_upload_file_limit', { + count: formatMaxCount, + size: displayMaxSize + })} + + )} diff --git a/projects/app/src/components/core/app/FileSelect.tsx b/projects/app/src/components/core/app/FileSelect.tsx index 84d175e6cf..0e0a62d7b8 100644 --- a/projects/app/src/components/core/app/FileSelect.tsx +++ b/projects/app/src/components/core/app/FileSelect.tsx @@ -20,6 +20,7 @@ import ChatFunctionTip from './Tip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { useMount } from 'ahooks'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import MyTag from '@fastgpt/web/components/common/Tag/index'; import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants'; @@ -38,8 +39,14 @@ const FileSelect = ({ }) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); + const { teamPlanStatus } = useUserStore(); const { isOpen, onOpen, onClose } = useDisclosure(); - const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 30); + + // 文件数量限制:团队套餐 || 系统配置 || 默认值(这里是指对话中,最多上传多少文件) + const maxSelectFiles = Math.min( + teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount, + 50 + ); const [localValue, setLocalValue] = useState(value); diff --git a/projects/app/src/components/core/app/FileSelector/index.tsx b/projects/app/src/components/core/app/FileSelector/index.tsx index f793746d30..c5809856fd 100644 --- a/projects/app/src/components/core/app/FileSelector/index.tsx +++ b/projects/app/src/components/core/app/FileSelector/index.tsx @@ -16,9 +16,9 @@ import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; import { getFileIcon } from '@fastgpt/global/common/file/icon'; import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; import { getUploadFileType } from '@fastgpt/global/core/app/constants'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import { useTranslation } from 'next-i18next'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; @@ -53,6 +53,7 @@ const FileSelector = ({ isDisabled?: boolean; }) => { const { feConfigs } = useSystemStore(); + const { teamPlanStatus } = useUserStore(); const { toast } = useToast(); const { t } = useSafeTranslation(); @@ -88,8 +89,17 @@ const FileSelector = ({ canSelectCustomFileExtension, customFileExtensionList ]); - const maxSelectFiles = maxFiles ?? 10; - const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb + // 文件数量限制:组件参数 || 团队套餐 || 系统配置 || 默认值 + const maxSelectFiles = + maxFiles || + teamPlanStatus?.standardConstants?.maxUploadFileCount || + feConfigs?.uploadFileMaxAmount || + 10; + // 文件大小限制(MB):团队套餐 || 系统配置 || 默认值 + const maxSize = + (teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) * + 1024 * + 1024; const canSelectFileAmount = maxSelectFiles - value.length; const isMaxSelected = canSelectFileAmount <= 0; diff --git a/projects/app/src/components/core/app/formRender/TimeInput.tsx b/projects/app/src/components/core/app/formRender/TimeInput.tsx index 6a3a89cd1e..9361c19a9f 100644 --- a/projects/app/src/components/core/app/formRender/TimeInput.tsx +++ b/projects/app/src/components/core/app/formRender/TimeInput.tsx @@ -11,6 +11,7 @@ type TimeInputProps = { timeGranularity?: 'day' | 'hour' | 'minute' | 'second'; minDate?: Date; maxDate?: Date; + isDisabled?: boolean; }; const TimeInput: React.FC = ({ @@ -19,7 +20,8 @@ const TimeInput: React.FC = ({ popPosition = 'bottom', timeGranularity = 'second', minDate, - maxDate + maxDate, + isDisabled }) => { const formatValue = useMemo(() => { const val = initialValue ? new Date(initialValue) : undefined; @@ -89,6 +91,7 @@ const TimeInput: React.FC = ({ ...(minDate ? [{ before: minDate }] : []), ...(maxDate ? [{ after: maxDate }] : []) ]} + isDisabled={isDisabled} w={'168px'} h={8} borderColor={'myGray.200'} @@ -103,12 +106,12 @@ const TimeInput: React.FC = ({ w={'48px'} size={'sm'} hideStepper - isDisabled={!enableHour} + isDisabled={isDisabled || !enableHour} inputFieldProps={{ pr: '20px', pl: '8px', - bg: enableHour ? 'white' : 'myGray.100', - color: enableHour ? 'inherit' : 'myGray.400' + bg: isDisabled || !enableHour ? 'myGray.100' : 'white', + color: isDisabled || !enableHour ? 'myGray.400' : 'inherit' }} /> = ({ top={'50%'} transform={'translateY(-50%)'} fontSize={'12px'} - color={enableHour ? 'myGray.500' : 'myGray.300'} + color={isDisabled || !enableHour ? 'myGray.300' : 'myGray.500'} pointerEvents={'none'} zIndex={1} > @@ -133,12 +136,12 @@ const TimeInput: React.FC = ({ w={'48px'} size={'sm'} hideStepper - isDisabled={!enableMinute} + isDisabled={isDisabled || !enableMinute} inputFieldProps={{ pr: '20px', pl: '8px', - bg: enableMinute ? 'white' : 'myGray.100', - color: enableMinute ? 'inherit' : 'myGray.400' + bg: isDisabled || !enableMinute ? 'myGray.100' : 'white', + color: isDisabled || !enableMinute ? 'myGray.400' : 'inherit' }} /> = ({ top={'50%'} transform={'translateY(-50%)'} fontSize={'12px'} - color={enableMinute ? 'myGray.500' : 'myGray.300'} + color={isDisabled || !enableMinute ? 'myGray.300' : 'myGray.500'} pointerEvents={'none'} zIndex={1} > @@ -163,12 +166,12 @@ const TimeInput: React.FC = ({ w={'48px'} size={'sm'} hideStepper - isDisabled={!enableSecond} + isDisabled={isDisabled || !enableSecond} inputFieldProps={{ pr: '20px', pl: '8px', - bg: enableSecond ? 'white' : 'myGray.100', - color: enableSecond ? 'inherit' : 'myGray.400' + bg: isDisabled || !enableSecond ? 'myGray.100' : 'white', + color: isDisabled || !enableSecond ? 'myGray.400' : 'inherit' }} /> = ({ top={'50%'} transform={'translateY(-50%)'} fontSize={'12px'} - color={enableSecond ? 'myGray.500' : 'myGray.300'} + color={isDisabled || !enableSecond ? 'myGray.300' : 'myGray.500'} pointerEvents={'none'} zIndex={1} > diff --git a/projects/app/src/components/core/app/formRender/index.tsx b/projects/app/src/components/core/app/formRender/index.tsx index c20db803df..0da6ddf82b 100644 --- a/projects/app/src/components/core/app/formRender/index.tsx +++ b/projects/app/src/components/core/app/formRender/index.tsx @@ -13,7 +13,7 @@ import TimeInput from './TimeInput'; import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; import { isSecretValue } from '@fastgpt/global/common/secret/utils'; import FileSelector from '../FileSelector/index'; -import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; +import { formatTime2YMDHMS, formatToISOWithTimezone } from '@fastgpt/global/common/string/time'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'; @@ -274,10 +274,11 @@ const InputRender = (props: InputRenderProps) => { return ( onChange?.(date ? date.toISOString() : undefined)} + onDateTimeChange={(date) => onChange?.(date ? formatToISOWithTimezone(date) : undefined)} timeGranularity={props.timeGranularity} minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + isDisabled={isDisabled} /> ); } @@ -296,7 +297,7 @@ const InputRender = (props: InputRenderProps) => { value={startDate ? new Date(formatTime2YMDHMS(startDate)) : undefined} onDateTimeChange={(date) => { const newArray = [...rangeArray]; - newArray[0] = date ? date.toISOString() : undefined; + newArray[0] = date ? formatToISOWithTimezone(date) : undefined; onChange?.(newArray); }} timeGranularity={props.timeGranularity} @@ -304,6 +305,7 @@ const InputRender = (props: InputRenderProps) => { endDate ? new Date(endDate) : timeRangeEnd ? new Date(timeRangeEnd) : undefined } minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} + isDisabled={isDisabled} /> @@ -314,7 +316,7 @@ const InputRender = (props: InputRenderProps) => { value={endDate ? new Date(formatTime2YMDHMS(endDate)) : undefined} onDateTimeChange={(date) => { const newArray = [...rangeArray]; - newArray[1] = date ? date.toISOString() : undefined; + newArray[1] = date ? formatToISOWithTimezone(date) : undefined; onChange?.(newArray); }} timeGranularity={props.timeGranularity} @@ -326,6 +328,7 @@ const InputRender = (props: InputRenderProps) => { : undefined } maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + isDisabled={isDisabled} /> diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx index 9c52b1cd80..26aad8f18e 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx @@ -12,6 +12,7 @@ import { type UseFieldArrayReturn } from 'react-hook-form'; import { type ChatBoxInputFormType, type UserInputFileItemType } from '../type'; import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { getPresignedChatFileGetUrl, getUploadChatFilePresignedUrl } from '@/web/common/file/api'; import { getUploadFileType } from '@fastgpt/global/core/app/constants'; @@ -31,6 +32,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => { const { toast } = useToast(); const { t } = useTranslation(); const { feConfigs } = useSystemStore(); + const { teamPlanStatus } = useUserStore(); const { update: updateFiles, @@ -52,8 +54,17 @@ export const useFileUpload = (props: UseFileUploadOptions) => { showSelectVideo || showSelectAudio || showSelectCustomFileExtension; - const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10; - const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb + // 文件数量限制:配置的maxFiles || 团队套餐 || 系统配置 || 默认值 + const maxSelectFiles = + fileSelectConfig?.maxFiles || + teamPlanStatus?.standardConstants?.maxUploadFileCount || + feConfigs?.uploadFileMaxAmount || + 10; + // 文件大小限制(MB):团队套餐 || 系统配置 || 默认值 + const maxSize = + (teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) * + 1024 * + 1024; const canSelectFileAmount = maxSelectFiles - fileList.length; const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => { diff --git a/projects/app/src/components/support/wallet/QRCodePayModal.tsx b/projects/app/src/components/support/wallet/QRCodePayModal.tsx index 8d365510e5..d7136ca199 100644 --- a/projects/app/src/components/support/wallet/QRCodePayModal.tsx +++ b/projects/app/src/components/support/wallet/QRCodePayModal.tsx @@ -18,6 +18,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import type { CreateBillResponseType } from '@fastgpt/global/openapi/support/wallet/bill/api'; export type QRPayProps = CreateBillResponseType & { + billId: string; tip?: string; discountCouponName?: string; }; @@ -226,43 +227,59 @@ const QRCodePayModal = ({ )} - - {isWxConfigured && ( - - )} - {isAlipayConfigured && ( + {/* WeChat Work payment: only show WeChat Work option, no switching allowed */} + {payment === BillPayWayEnum.wecom ? ( + - )} - {isBankConfigured && ( - - )} - + + ) : ( + + {isWxConfigured && ( + + )} + {isAlipayConfigured && ( + + )} + {isBankConfigured && ( + + )} + + )} {feConfigs.payFormUrl && ( diff --git a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx index ff308ea26c..7f61c43f1a 100644 --- a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx +++ b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx @@ -8,9 +8,11 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'next-i18next'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import dynamic from 'next/dynamic'; -import type { TeamSubSchemaType } from '@fastgpt/global/support/wallet/sub/type'; import Markdown from '@/components/Markdown'; import MyPopover from '@fastgpt/web/components/common/MyPopover'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { formatFileSize } from '@fastgpt/global/common/file/tools'; +import type { TeamSubSchemaType } from '@fastgpt/global/support/wallet/sub/type'; const ModelPriceModal = dynamic(() => import('@/components/core/ai/ModelTable').then((mod) => mod.ModelPriceModal) @@ -26,20 +28,34 @@ const StandardPlanContentList = ({ standplan?: TeamSubSchemaType; }) => { const { t } = useTranslation(); - const { subPlans } = useSystemStore(); + + const { subPlans, feConfigs } = useSystemStore(); + const { userInfo } = useUserStore(); const planContent = useMemo(() => { - const plan = subPlans?.standard?.[level]; + const isWecomTeam = !!userInfo?.team?.isWecomTeam; + const formatMode = isWecomTeam ? SubModeEnum.year : mode; + + // For wecom teams, free plan should use basic plan config + const effectiveLevel = isWecomTeam && level === 'free' ? 'basic' : level; + const plan = subPlans?.standard?.[effectiveLevel]; if (!plan) return; + // For wecom free plan (trial), use WecomFreePlan constants + return { - price: plan.price * (mode === SubModeEnum.month ? 1 : 10), + price: plan.price * (formatMode === SubModeEnum.month ? 1 : 10), level: level as `${StandardSubLevelEnum}`, ...standardSubLevelMap[level as `${StandardSubLevelEnum}`], - totalPoints: - standplan?.totalPoints ?? plan.totalPoints * (mode === SubModeEnum.month ? 1 : 12), annualBonusPoints: - mode === SubModeEnum.month ? 0 : standplan?.annualBonusPoints ?? plan.annualBonusPoints, + formatMode === SubModeEnum.month + ? 0 + : standplan?.annualBonusPoints ?? plan.annualBonusPoints, + totalPoints: + standplan?.totalPoints ?? + (isWecomTeam + ? plan.wecom?.points ?? 2000 + : plan.totalPoints * (formatMode === SubModeEnum.month ? 1 : 12)), requestsPerMinute: standplan?.requestsPerMinute ?? plan.requestsPerMinute, maxTeamMember: standplan?.maxTeamMember ?? plan.maxTeamMember, maxAppAmount: standplan?.maxApp ?? plan.maxAppAmount, @@ -51,12 +67,19 @@ const StandardPlanContentList = ({ auditLogStoreDuration: standplan?.auditLogStoreDuration ?? plan.auditLogStoreDuration, appRegistrationCount: standplan?.appRegistrationCount ?? plan.appRegistrationCount, ticketResponseTime: standplan?.ticketResponseTime ?? plan.ticketResponseTime, - customDomain: standplan?.customDomain ?? plan.customDomain + customDomain: standplan?.customDomain ?? plan.customDomain, + maxUploadFileSize: formatFileSize( + (standplan?.maxUploadFileSize || plan.maxUploadFileSize || feConfigs.uploadFileMaxSize) * + 1024 ** 2 + ), + maxUploadFileCount: + standplan?.maxUploadFileCount || plan.maxUploadFileCount || feConfigs.uploadFileMaxAmount }; }, [ subPlans?.standard, level, mode, + userInfo?.team?.isWecomTeam, standplan?.totalPoints, standplan?.annualBonusPoints, standplan?.requestsPerMinute, @@ -69,7 +92,11 @@ const StandardPlanContentList = ({ standplan?.auditLogStoreDuration, standplan?.appRegistrationCount, standplan?.ticketResponseTime, - standplan?.customDomain + standplan?.customDomain, + standplan?.maxUploadFileSize, + standplan?.maxUploadFileCount, + feConfigs?.uploadFileMaxSize, + feConfigs?.uploadFileMaxAmount ]); return planContent ? ( @@ -225,6 +252,15 @@ const StandardPlanContentList = ({ )} + + + + {t('common:n_max_upload_file_limit', { + count: planContent.maxUploadFileCount, + size: planContent.maxUploadFileSize + })} + + ) : null; }; diff --git a/projects/app/src/pageComponents/account/bill/BillTable.tsx b/projects/app/src/pageComponents/account/bill/BillTable.tsx index 04a16cb5cd..d19863bcc0 100644 --- a/projects/app/src/pageComponents/account/bill/BillTable.tsx +++ b/projects/app/src/pageComponents/account/bill/BillTable.tsx @@ -90,6 +90,16 @@ const BillTable = () => { payWay }); + // 企微支付直接打开 URL + if (payWay === 'wecom' && paymentData.payUrl) { + toast({ + title: t('account_bill:wecom_not_pay_tip'), + status: 'success' + }); + window.open(paymentData.payUrl, '_blank'); + return; + } + setQRPayData({ billId: bill._id, readPrice: formatStorePrice2Read(bill.price), diff --git a/projects/app/src/pageComponents/account/info/standardDetailModal.tsx b/projects/app/src/pageComponents/account/info/standardDetailModal.tsx index f6adb0de46..6f83c403c6 100644 --- a/projects/app/src/pageComponents/account/info/standardDetailModal.tsx +++ b/projects/app/src/pageComponents/account/info/standardDetailModal.tsx @@ -27,6 +27,7 @@ import { import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { useUserStore } from '@/web/support/user/useUserStore'; type packageStatus = 'active' | 'inactive' | 'expired'; @@ -34,6 +35,8 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); const { Loading } = useLoading(); const { subPlans } = useSystemStore(); + const { userInfo } = useUserStore(); + const isWecomTeam = !!userInfo?.team.isWecomTeam; const { data: teamPlans = [], loading: isLoading } = useRequest( () => getTeamPlans().then((res) => { @@ -138,12 +141,14 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => { - - - - {t('account_info:package_usage_rules')} - - + {!isWecomTeam && ( + + + + {t('account_info:package_usage_rules')} + + + )} ); diff --git a/projects/app/src/pageComponents/account/team/MemberTable.tsx b/projects/app/src/pageComponents/account/team/MemberTable.tsx index d6f31606b2..a1f9731edf 100644 --- a/projects/app/src/pageComponents/account/team/MemberTable.tsx +++ b/projects/app/src/pageComponents/account/team/MemberTable.tsx @@ -30,7 +30,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import dynamic from 'next/dynamic'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { delLeaveTeam } from '@/web/support/user/team/api'; -import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api'; +import { postSyncMembers } from '@/web/support/user/api'; import { TeamMemberRoleEnum, TeamMemberStatusEnum @@ -38,7 +38,7 @@ import { import format from 'date-fns/format'; import OrgTags from '@/components/support/user/team/OrgTags'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState, useMemo } from 'react'; import { downloadFetch } from '@/web/common/system/utils'; import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { useToast } from '@fastgpt/web/hooks/useToast'; @@ -53,11 +53,12 @@ import MyIconButton from '@fastgpt/web/components/common/Icon/button'; const InviteModal = dynamic(() => import('./Invite/InviteModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); +const TransferOwnershipModal = dynamic(() => import('./TransferOwnershipModal')); function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { const { t } = useTranslation(); const { toast } = useToast(); - const { userInfo } = useUserStore(); + const { userInfo, initUserInfo } = useUserStore(); const { feConfigs } = useSystemStore(); const isSyncMode = feConfigs?.register_method?.includes('sync'); @@ -94,6 +95,16 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { onClose: onCloseTeamTagsAsync } = useDisclosure(); + const isWecomTeam = useMemo(() => { + return !!userInfo?.team?.isWecomTeam; + }, [userInfo?.team?.isWecomTeam]); + + const { + isOpen: isOpenTransferModal, + onOpen: onOpenTransferModal, + onClose: onCloseTransferModal + } = useDisclosure(); + // member action const [searchKey, setSearchKey] = useState(''); const { @@ -213,7 +224,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { {t('account_team:sync_immediately')} )} - {userInfo?.team.permission.hasManagePer && !isSyncMode && ( + {userInfo?.team.permission.hasManagePer && !isSyncMode && !isWecomTeam && ( + )} {userInfo?.team.permission.isOwner && isSyncMode && ( + + + + + ); +} + +export default TransferOwnershipModal; diff --git a/projects/app/src/pageComponents/app/detail/Publish/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/index.tsx index 2679bca41b..f9f52d8fd7 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/index.tsx @@ -9,9 +9,10 @@ import { useTranslation } from 'next-i18next'; import { useContextSelector } from 'use-context-selector'; import { AppContext } from '../context'; -import { cardStyles } from '../constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { UserTagsEnum } from '@fastgpt/global/support/user/type'; const Link = dynamic(() => import('./Link')); const API = dynamic(() => import('./API')); @@ -25,6 +26,7 @@ const OutLink = () => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); const { toast } = useToast(); + const { userInfo } = useUserStore(); const appId = useContextSelector(AppContext, (v) => v.appId); @@ -43,7 +45,8 @@ const OutLink = () => { value: PublishChannelEnum.apikey, isProFn: false }, - ...(feConfigs?.show_publish_feishu !== false + ...(feConfigs?.show_publish_feishu !== false && + !userInfo?.tags?.includes(UserTagsEnum.enum.wecom) ? [ { icon: 'core/app/publish/lark', @@ -54,7 +57,8 @@ const OutLink = () => { } ] : []), - ...(feConfigs?.show_publish_dingtalk !== false + ...(feConfigs?.show_publish_dingtalk !== false && + !userInfo?.tags?.includes(UserTagsEnum.enum.wecom) ? [ { icon: 'common/dingtalkFill', diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx index d644ffe2c0..74b38d6311 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ToolSelectModal.tsx @@ -504,7 +504,7 @@ const RenderList = React.memo(function RenderList({ _hover={{ color: 'primary.600' }} - onClick={() => router.push('/plugin/tool')} + onClick={() => router.push('/dashboard/systemTool')} gap={1} bottom={0} right={[3, 6]} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx index 67deaafe27..4ea348f622 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/header.tsx @@ -184,7 +184,7 @@ const NodeTemplateListHeader = ({ _hover={{ color: 'primary.600' }} - onClick={() => router.push('/plugin/tool')} + onClick={() => router.push('/dashboard/systemTool')} gap={1} ml={4} > diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx index f4cfc4b505..0447be6f81 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx @@ -38,6 +38,7 @@ import TimeInput from '@/components/core/app/formRender/TimeInput'; import MySlider from '@/components/Slider'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useUserStore } from '@/web/support/user/useUserStore'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import RadioGroup from '@fastgpt/web/components/common/Radio/RadioGroup'; import { DatasetSelectModal } from '@/components/core/app/DatasetSelectModal'; @@ -76,6 +77,7 @@ const InputTypeConfig = ({ const { t } = useTranslation(); const defaultListValue = { label: t('common:None'), value: '' }; const { feConfigs, llmModelList } = useSystemStore(); + const { teamPlanStatus } = useUserStore(); const availableModels = useMemoEnhance(() => { return llmModelList.map((model) => ({ @@ -120,7 +122,11 @@ const InputTypeConfig = ({ : undefined; const maxFiles = watch('maxFiles') ?? 5; - const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 50); + // 文件数量限制:团队套餐 || 系统配置 || 默认值 + const maxSelectFiles = Math.min( + teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount, + 50 + ); const canSelectFile = watch('canSelectFile') ?? true; const canSelectImg = watch('canSelectImg'); const canSelectVideo = watch('canSelectVideo'); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/FileSelect.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/FileSelect.tsx index 9e8669aec1..5bf9af49a8 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/FileSelect.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/FileSelect.tsx @@ -1,22 +1,15 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { RenderInputProps } from '../type'; -import { Box, Button, HStack, Input, InputGroup, useDisclosure, VStack } from '@chakra-ui/react'; -import type { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import SelectAppModal from '../../../../SelectAppModal'; +import { Box, HStack, Input, InputGroup, VStack } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useContextSelector } from 'use-context-selector'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { getAppDetailById } from '@/web/core/app/api'; import { WorkflowActionsContext } from '@/pageComponents/app/detail/WorkflowComponents/context/workflowActionsContext'; -import { AppContext } from '@/pageComponents/app/detail/context'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; import { getFileIcon } from '@fastgpt/global/common/file/icon'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; -import IconButton from '@/pageComponents/account/team/OrgManage/IconButton'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; const FileSelectRender = ({ item, nodeId }: RenderInputProps) => { const { t } = useTranslation(); @@ -29,6 +22,8 @@ const FileSelectRender = ({ item, nodeId }: RenderInputProps) => { } return []; }, [item.value]); + + // 文件数量限制:节点配置的maxFiles || 团队套餐 || 默认值 const maxSelectFiles = item.maxFiles || 10; const isMaxSelected = values.length >= maxSelectFiles; diff --git a/projects/app/src/pageComponents/config/ImportPluginModal.tsx b/projects/app/src/pageComponents/config/ImportPluginModal.tsx index b6797254ae..fd7e3a4a8e 100644 --- a/projects/app/src/pageComponents/config/ImportPluginModal.tsx +++ b/projects/app/src/pageComponents/config/ImportPluginModal.tsx @@ -210,7 +210,6 @@ const ImportPluginModal = ({ void; onClose: () => void; }) => { - const { t } = useTranslation(); - const { register, reset, handleSubmit, setValue, watch, control } = - useForm(); + const { t, i18n } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { toast } = useToast(); + const { register, reset, handleSubmit, setValue, watch } = useForm(); const { data: tool, loading } = useRequest(() => getAdminSystemToolDetail({ toolId }), { onSuccess(res) { - reset(res); + // 转换 AdminSystemToolDetailType 到 UpdateToolBodyType + const formData: Partial = { + status: res.status, + defaultInstalled: res.defaultInstalled, + inputListVal: res.inputListVal, + systemKeyCost: res.systemKeyCost, + childTools: res.childTools?.map((t) => ({ + pluginId: t.pluginId, + systemKeyCost: t.systemKeyCost + })), + promoteTags: res.promoteTags, + hideTags: res.hideTags, + tagIds: res.tags || [] + }; + reset(formData); + setSelectedTags(res.tags || []); }, manual: false }); - const [inputList, status, defaultInstalled, inputListVal, childTools] = watch([ - 'inputList', + // 从表单 watch 可变数据 + const [status, defaultInstalled, inputListVal, promoteTags, hideTags] = watch([ 'status', 'defaultInstalled', 'inputListVal', - 'childTools' + 'promoteTags', + 'hideTags' ]); + // 从 tool 读取只读数据 + const inputList = tool?.inputList; + const isFolder = tool?.isFolder; + + const { value: selectedTags, setValue: setSelectedTags } = useMultipleSelect( + tool?.tags ?? [], + false + ); + + useEffect(() => { + setValue('tagIds', selectedTags); + }, [selectedTags, setValue]); + // 是否显示系统密钥配置 const showSystemSecretInput = !!inputList && inputList.length > 0; + // 准备用户标签列表 + const userTagsList = UserTagsEnum.options.map((tag) => ({ + label: tag, + value: tag + })); + + const { data: toolTags = [], loading: loadingTags } = useRequest(getPluginToolTags, { + manual: false + }); + + const pluginTypeSelectList = useMemo( + () => + toolTags?.map((tag) => ({ + label: parseI18nString(tag.tagName, i18n.language), + value: tag.tagId + })) || [], + [i18n.language, toolTags] + ); + const { runAsync: onSubmit, loading: submitting } = useRequest( - (formData: AdminSystemToolDetailType) => + (formData: UpdateToolBodyType) => putAdminUpdateTool({ ...formData, - pluginId: toolId, - childTools: formData.childTools?.map((tool) => { - return { - pluginId: tool.pluginId, - systemKeyCost: tool.systemKeyCost - }; - }) + pluginId: toolId }), { successToast: t('common:Config') + t('common:Success'), @@ -143,7 +193,7 @@ const SystemToolConfigModal = ({ <> - {!tool?.isFolder && ( + {!isFolder && ( {t('app:toolkit_system_key_cost')} @@ -157,24 +207,24 @@ const SystemToolConfigModal = ({ /> )} - {tool?.inputList?.map(renderInputField)} + {inputList?.map(renderInputField)} ); return ( - {tool?.isFolder ? ( + {isFolder ? ( @@ -221,6 +271,28 @@ const SystemToolConfigModal = ({ /> + + + {t('app:custom_plugin_tags_label')} + + { + if (newTags.length > 3) { + toast({ + title: t('app:custom_plugin_tags_max_limit'), + status: 'warning' + }); + return; + } + setSelectedTags(newTags); + }} + placeholder={t('app:custom_plugin_tags_label')} + w={'100%'} + /> + + {showSystemSecretInput && ( <> @@ -242,6 +314,42 @@ const SystemToolConfigModal = ({ {systemConfigSection} )} + + {feConfigs?.showWecomConfig && ( + <> + + + {t('app:toolkit_promote_tags')} + + + {t('app:toolkit_promote_tags_tip')} + + setValue('promoteTags', val)} + placeholder={t('app:toolkit_select_user_tags')} + w={'100%'} + /> + + + + + {t('app:toolkit_hide_tags')} + + + {t('app:toolkit_hide_tags_tip')} + + setValue('hideTags', val)} + placeholder={t('app:toolkit_select_user_tags')} + w={'100%'} + /> + + + )} @@ -255,21 +363,18 @@ const SystemToolConfigModal = ({ {t('app:toolkit_tool_name')} - {/* - {t('common:Status')} - */} {t('app:toolkit_key_price')} - {childTools?.map((tool, index) => { + {tool?.childTools?.map((childTool, index) => { return ( - + - {parseI18nString(tool.name)} + {childTool.name} @@ -280,6 +385,11 @@ const SystemToolConfigModal = ({ name={`childTools.${index}.systemKeyCost`} {...COST_LIMITS} /> + ); @@ -342,10 +452,9 @@ const SystemToolConfigModal = ({ onChange={(e) => { const val = e.target.checked; if (val) { - // @ts-ignore setValue('inputListVal', {}); } else { - setValue('inputListVal', undefined); + setValue('inputListVal', null); } }} /> @@ -353,6 +462,73 @@ const SystemToolConfigModal = ({ {systemConfigSection} )} + + + + {t('app:custom_plugin_tags_label')} + + { + if (newTags.length > 3) { + toast({ + title: t('app:custom_plugin_tags_max_limit'), + status: 'warning' + }); + return; + } + setSelectedTags(newTags); + }} + placeholder={t('app:custom_plugin_tags_label')} + maxW={270} + h={9} + borderRadius={'sm'} + bg={'myGray.50'} + /> + + + {feConfigs?.showWecomConfig && ( + <> + + + {t('app:toolkit_promote_tags')} + + + {t('app:toolkit_promote_tags_tip')} + + setValue('promoteTags', val)} + placeholder={t('app:toolkit_select_user_tags')} + w={'100%'} + /> + + + + + {t('app:toolkit_hide_tags')} + + + {t('app:toolkit_hide_tags_tip')} + + setValue('hideTags', val)} + placeholder={t('app:toolkit_select_user_tags')} + w={'100%'} + /> + + + )} )} diff --git a/projects/app/src/pageComponents/dashboard/Container.tsx b/projects/app/src/pageComponents/dashboard/Container.tsx index 735fb5b126..5b21561cfb 100644 --- a/projects/app/src/pageComponents/dashboard/Container.tsx +++ b/projects/app/src/pageComponents/dashboard/Container.tsx @@ -13,6 +13,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { getTemplateMarketItemList, getTemplateTagList } from '@/web/core/app/api/template'; import type { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type'; import TeamPlanStatusCard from './TeamPlanStatusCard'; +import { useUserStore } from '@/web/support/user/useUserStore'; export enum TabEnum { agent = 'agent', @@ -38,6 +39,7 @@ const DashboardContainer = ({ const { isPc } = useSystem(); const { feConfigs } = useSystemStore(); const { isOpen: isOpenSidebar, onOpen: onOpenSidebar, onClose: onCloseSidebar } = useDisclosure(); + const { userInfo } = useUserStore(); // First tab const currentTab = useMemo(() => { @@ -60,7 +62,9 @@ const DashboardContainer = ({ ? getTemplateTagList().then((res) => [ { typeId: AppTemplateTypeEnum.recommendation, - typeName: t('app:templateMarket.templateTags.Recommendation'), + typeName: userInfo?.team.isWecomTeam + ? t('app:templateMarket.templateTags.WecomZone') + : t('app:templateMarket.templateTags.Recommendation'), typeOrder: 0 }, ...res diff --git a/projects/app/src/pageComponents/dashboard/TeamPlanStatusCard.tsx b/projects/app/src/pageComponents/dashboard/TeamPlanStatusCard.tsx index b01539f4cf..1d25c1f9e1 100644 --- a/projects/app/src/pageComponents/dashboard/TeamPlanStatusCard.tsx +++ b/projects/app/src/pageComponents/dashboard/TeamPlanStatusCard.tsx @@ -14,7 +14,7 @@ import { webPushTrack } from '@/web/common/middle/tracks/utils'; const TeamPlanStatusCard = () => { const { t } = useTranslation(); - const { teamPlanStatus } = useUserStore(); + const { teamPlanStatus, userInfo } = useUserStore(); const { operationalAd, loadOperationalAd, feConfigs, subPlans } = useSystemStore(); const router = useRouter(); @@ -40,13 +40,17 @@ const TeamPlanStatusCard = () => { } ); + const isWecomTeam = userInfo?.team.isWecomTeam; + const planName = useMemo(() => { if (!teamPlanStatus?.standard?.currentSubLevel) return ''; + if (isWecomTeam && teamPlanStatus.standard.currentSubLevel === StandardSubLevelEnum.free) + return t('common:support.wallet.subscription.standardSubLevel.trial'); return ( subPlans?.standard?.[teamPlanStatus.standard.currentSubLevel]?.name || standardSubLevelMap[teamPlanStatus.standard.currentSubLevel]?.label ); - }, [teamPlanStatus?.standard?.currentSubLevel, subPlans]); + }, [teamPlanStatus?.standard?.currentSubLevel, isWecomTeam, t, subPlans?.standard]); const aiPointsUsageMap = useMemo(() => { if (!teamPlanStatus) { diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TemplateImportModal.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TemplateImportModal.tsx index 113825eec0..11854a00ea 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/TemplateImportModal.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/TemplateImportModal.tsx @@ -98,7 +98,6 @@ const TemplateImportModal = ({ 系统配置 > 默认值(500MB) + const maxSize = + (teamPlanStatus?.standardConstants?.maxUploadFileSize ?? feConfigs?.uploadFileMaxSize ?? 500) * + 1024 * + 1024; const { File, onOpen } = useSelectFile({ fileType, @@ -220,10 +228,10 @@ const FileSelector = ({ {t('file:support_file_type', { fileType })} - {/* max count */} - {maxCount && t('file:support_max_count', { maxCount })} - {/* max size */} - {maxSize && t('file:support_max_size', { maxSize: formatFileSize(maxSize) })} + {t('common:n_max_upload_file_limit', { + count: maxCount, + size: formatFileSize(maxSize) + })} { return; } + if (item.provider === OAuthEnum.wecom) { + const redirectUrl = await POST( + '/proApi/support/user/account/login/wecom/getRedirectUrl', + { + redirectUri, + isWecomWorkTerminal, + state: state.current + } + ); + setLoginStore({ + provider: item.provider as OAuthEnum, + lastRoute: computedLastRoute, + state: state.current + }); + router.replace(redirectUrl, '_self'); + return; + } + if (item.redirectUrl) { setLoginStore({ provider: item.provider as OAuthEnum, @@ -155,7 +173,19 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => { const sso = oAuthList.find((item) => item.provider === OAuthEnum.sso); // sso auto login if (sso && (feConfigs?.sso?.autoLogin || isWecomWorkTerminal)) onClickOauth(sso); - }, [rootLogin, feConfigs?.sso?.autoLogin, isWecomWorkTerminal, onClickOauth, oAuthList]); + if (feConfigs.oauth?.wecom && isWecomWorkTerminal) { + onClickOauth({ + provider: OAuthEnum.wecom + } as any); + } + }, [ + rootLogin, + feConfigs?.sso?.autoLogin, + isWecomWorkTerminal, + onClickOauth, + oAuthList, + feConfigs.oauth?.wecom + ]); return ( diff --git a/projects/app/src/pageComponents/price/ExtraPlan.tsx b/projects/app/src/pageComponents/price/ExtraPlan.tsx index b52094379d..7bff78ed20 100644 --- a/projects/app/src/pageComponents/price/ExtraPlan.tsx +++ b/projects/app/src/pageComponents/price/ExtraPlan.tsx @@ -14,12 +14,20 @@ import MySelect from '@fastgpt/web/components/common/MySelect'; import { calculatePrice } from '@fastgpt/global/support/wallet/bill/tools'; import { formatNumberWithUnit } from '@fastgpt/global/common/string/tools'; import { formatActivityExpirationTime } from './utils'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants'; const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => { const { t, i18n } = useTranslation(); const { toast } = useToast(); const { subPlans } = useSystemStore(); const [qrPayData, setQRPayData] = useState(); + const { userInfo, teamPlanStatus } = useUserStore(); + + // For Wecom teams, free plan should not be able to buy extra plan + const isDisabledBuy = + userInfo?.team.isWecomTeam && + teamPlanStatus?.standard?.currentSubLevel === StandardSubLevelEnum.free; // 额外的知识库索引量 const extraDatasetPrice = subPlans?.extraDatasetSize?.price || 0; @@ -58,6 +66,7 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => { }); setQRPayData({ tip: t('common:button.extra_dataset_size_tip'), + billId: res.billId!, ...res }); }, @@ -95,6 +104,7 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => { setQRPayData({ tip: t('common:button.extra_points_tip'), + billId: res.billId!, ...res }); }, @@ -301,6 +311,12 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => { selectedPackageIndex === undefined || !extraPointsPackages[selectedPackageIndex] } onClick={() => { + if (isDisabledBuy) { + return toast({ + status: 'warning', + title: t('common:support.wallet.subscription.extra_plan_disabled_tip') + }); + } if (selectedPackageIndex !== undefined && extraPointsPackages[selectedPackageIndex]) { const selectedPackage = extraPointsPackages[selectedPackageIndex]; onclickBuyExtraPoints({ @@ -461,7 +477,15 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => { h={['40px', '44px']} variant={'primaryGhost'} isLoading={isLoadingBuyDatasetSize} - onClick={handleSubmitDatasetSize(onclickBuyDatasetSize)} + onClick={(e) => { + if (isDisabledBuy) { + return toast({ + status: 'warning', + title: t('common:support.wallet.subscription.extra_plan_disabled_tip') + }); + } + handleSubmitDatasetSize(onclickBuyDatasetSize)(e); + }} color={'primary.700'} fontSize={['14px', '16px']} > diff --git a/projects/app/src/pageComponents/price/Standard.tsx b/projects/app/src/pageComponents/price/Standard.tsx index c6bb607e51..336d70d0ab 100644 --- a/projects/app/src/pageComponents/price/Standard.tsx +++ b/projects/app/src/pageComponents/price/Standard.tsx @@ -12,12 +12,12 @@ import { postCreatePayBill } from '@/web/support/wallet/bill/api'; import { getDiscountCouponList } from '@/web/support/wallet/sub/discountCoupon/api'; import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants'; import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList'; -import MyBox from '@fastgpt/web/components/common/MyBox'; import { DiscountCouponStatusEnum, DiscountCouponTypeEnum } from '@fastgpt/global/support/wallet/sub/discountCoupon/constants'; import { formatActivityExpirationTime } from './utils'; +import { useUserStore } from '@/web/support/user/useUserStore'; export enum PackageChangeStatusEnum { buy = 'buy', @@ -39,6 +39,7 @@ const Standard = ({ onPaySuccess?: () => void; }) => { const { t } = useTranslation(); + const { userInfo } = useUserStore(); const packagePayTextMap = { [PackageChangeStatusEnum.buy]: t('common:pay.package_tip.buy'), @@ -46,15 +47,25 @@ const Standard = ({ [PackageChangeStatusEnum.upgrade]: t('common:pay.package_tip.upgrade') }; + // Check if it's a wecom team + const isWecomTeam = !!userInfo?.team?.isWecomTeam; + const [packageChange, setPackageChange] = useState(); const { subPlans, feConfigs } = useSystemStore(); - const [selectSubMode, setSelectSubMode] = useState<`${SubModeEnum}`>(SubModeEnum.month); + const [selectSubMode, setSelectSubMode] = useState<`${SubModeEnum}`>( + isWecomTeam ? SubModeEnum.year : SubModeEnum.month + ); const hasActivityExpiration = !!subPlans?.activityExpirationTime && selectSubMode === SubModeEnum.year; useEffect(() => { - setSelectSubMode(subPlans?.activityExpirationTime ? SubModeEnum.year : SubModeEnum.month); - }, [subPlans?.activityExpirationTime]); + // For WeCom teams, always default to yearly mode + if (isWecomTeam) { + setSelectSubMode(SubModeEnum.year); + } else { + setSelectSubMode(subPlans?.activityExpirationTime ? SubModeEnum.year : SubModeEnum.month); + } + }, [subPlans?.activityExpirationTime, isWecomTeam]); // 获取优惠券 const { data: coupons = [], runAsync: getCoupons } = useRequest( @@ -96,7 +107,10 @@ const Standard = ({ ...standardSubLevelMap[level as `${StandardSubLevelEnum}`], ...(value.desc ? { desc: value.desc } : {}), ...(value.name ? { label: value.name } : {}), - price: value.price * (selectSubMode === SubModeEnum.month ? 1 : 10), + price: + isWecomTeam && value.wecom + ? value.wecom.price + : value.price * (selectSubMode === SubModeEnum.month ? 1 : 10), level: level as `${StandardSubLevelEnum}`, maxTeamMember: myStandardPlan?.maxTeamMember || value.maxTeamMember, maxAppAmount: myStandardPlan?.maxApp || value.maxAppAmount, @@ -104,7 +118,10 @@ const Standard = ({ chatHistoryStoreDuration: value.chatHistoryStoreDuration, maxDatasetSize: value.maxDatasetSize, annualBonusPoints: selectSubMode === SubModeEnum.month ? 0 : value.annualBonusPoints, - totalPoints: value.totalPoints * (selectSubMode === SubModeEnum.month ? 1 : 12), + totalPoints: + isWecomTeam && value.wecom + ? value.wecom.points + : value.totalPoints * (selectSubMode === SubModeEnum.month ? 1 : 12), // custom plan priceDescription: value.priceDescription, @@ -115,6 +132,7 @@ const Standard = ({ : []; }, [ subPlans?.standard, + isWecomTeam, selectSubMode, myStandardPlan?.maxTeamMember, myStandardPlan?.maxApp, @@ -127,7 +145,16 @@ const Standard = ({ /* Get pay code */ const { runAsync: onPay, loading: isLoading } = useRequest(postCreatePayBill, { onSuccess(res) { - setQRPayData(res); + // For WeChat Work payment, open payment URL in new tab + if (res.payUrl) { + window.open(res.payUrl, '_blank'); + return; + } + // For other payment methods, show QR code modal + setQRPayData({ + ...res, + billId: res.billId! + }); } }); @@ -139,44 +166,46 @@ const Standard = ({ return ( <> - - - - {t('common:pay_year_tip')} + {!isWecomTeam && ( + + + + {t('common:pay_year_tip')} + + + {t('common:support.wallet.subscription.mode.Year')} + + ), + value: SubModeEnum.year + } + ]} + value={selectSubMode} + onChange={(e) => setSelectSubMode(e as `${SubModeEnum}`)} + /> - - {t('common:support.wallet.subscription.mode.Year')} - - ), - value: SubModeEnum.year - } - ]} - value={selectSubMode} - onChange={(e) => setSelectSubMode(e as `${SubModeEnum}`)} - /> - - - + + + )} {/* card */} - {t(item.label as any)} + {isWecomTeam && item.level === StandardSubLevelEnum.free + ? t('common:support.wallet.subscription.standardSubLevel.trial') + : t(item.label as any)} {item.level === StandardSubLevelEnum.custom ? ( @@ -339,6 +376,17 @@ const Standard = ({ ? matchedCoupon.discount * item.price : (matchedCoupon.discount * item.price).toFixed(1) : item.price} + {isWecomTeam && item.level !== StandardSubLevelEnum.free && ( + + {t('common:support.wallet.subscription.per_year')} + + )} {item.level !== StandardSubLevelEnum.free && matchedCoupon && ( - {t(item.desc as any, { title: feConfigs?.systemTitle })} + {isWecomTeam && item.level === StandardSubLevelEnum.free + ? t('common:support.wallet.subscription.standardSubLevel.trial_desc') + : t(item.desc as any, { title: feConfigs?.systemTitle })} {/* Button */} @@ -473,6 +523,24 @@ const Standard = ({ ); } + // For wecom teams with advanced plan, disable basic plan purchase + if (isWecomDowngrade) { + return ( + + ); + } return ( + )} {feConfigs?.docUrl && ( + {teamSubPlan?.standard?.teamId && ( + + )} + {(!isButtonInView || !teamSubPlan?.standard?.teamId) && ( + } + onClick={handleBack} + /> + )} + + {/* standard sub */} + + + {t('common:support.wallet.subscription.Sub plan')} + + + {isWecomTeam + ? t('common:support.wallet.subscription.Sub plan tip wecom') + : t('common:support.wallet.subscription.Sub plan tip', { + title: feConfigs?.systemTitle + })} + + + + + + {t('user:bill.standard_valid_tip')} + + + + + {/* extra plan */} + + + {t('common:support.wallet.subscription.Extra plan')} + + + {t('common:support.wallet.subscription.Extra plan tip')} + + + + + {/* points */} + + + {/* question */} + + )} - {!isButtonInView && teamSubPlan?.standard?.teamId && ( - } - onClick={handleBack} - /> - )} - - {/* standard sub */} - - - {t('common:support.wallet.subscription.Sub plan')} - - - {t('common:support.wallet.subscription.Sub plan tip', { - title: feConfigs?.systemTitle - })} - - - - - - {t('user:bill.standard_valid_tip')} - - - - - {/* extra plan */} - - - {t('common:support.wallet.subscription.Extra plan')} - - - {t('common:support.wallet.subscription.Extra plan tip')} - - - - - {/* points */} - - - {/* question */} - - + ); }; diff --git a/projects/app/src/service/common/bullmq/index.ts b/projects/app/src/service/common/bullmq/index.ts index 67990fcec7..c0ad0c1595 100644 --- a/projects/app/src/service/common/bullmq/index.ts +++ b/projects/app/src/service/common/bullmq/index.ts @@ -2,8 +2,14 @@ import { addLog } from '@fastgpt/service/common/system/log'; import { initS3MQWorker } from '@fastgpt/service/common/s3'; import { initDatasetDeleteWorker } from '@fastgpt/service/core/dataset/delete'; import { initAppDeleteWorker } from '@fastgpt/service/core/app/delete'; +import { initTeamDeleteWorker } from '@fastgpt/service/support/user/team/delete'; export const initBullMQWorkers = () => { addLog.info('Init BullMQ Workers...'); - return Promise.all([initS3MQWorker(), initDatasetDeleteWorker(), initAppDeleteWorker()]); + return Promise.all([ + initS3MQWorker(), + initDatasetDeleteWorker(), + initAppDeleteWorker(), + initTeamDeleteWorker() + ]); }; diff --git a/projects/app/src/service/common/frequencyLimit/api.ts b/projects/app/src/service/common/frequencyLimit/api.ts deleted file mode 100644 index 6bfd26be13..0000000000 --- a/projects/app/src/service/common/frequencyLimit/api.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type AuthFrequencyLimitProps } from '@fastgpt/global/common/frequenctLimit/type'; -import { POST } from '@fastgpt/service/common/api/plusRequest'; - -export const authFrequencyLimit = (data: AuthFrequencyLimitProps) => { - if (!global.feConfigs.isPlus) return; - - return POST('/common/freequencyLimit/auth', data); -}; diff --git a/projects/app/src/service/common/system/index.ts b/projects/app/src/service/common/system/index.ts index a2e2b42be3..d7f6c5e119 100644 --- a/projects/app/src/service/common/system/index.ts +++ b/projects/app/src/service/common/system/index.ts @@ -124,8 +124,9 @@ const defaultFeConfigs: FastGPTFeConfigsType = { }, scripts: [], favicon: '/favicon.ico', - uploadFileMaxSize: 500, - chineseRedirectUrl: process.env.CHINESE_IP_REDIRECT_URL || '' + chineseRedirectUrl: process.env.CHINESE_IP_REDIRECT_URL || '', + uploadFileMaxSize: Number(process.env.UPLOAD_FILE_MAX_SIZE || 1000), + uploadFileMaxAmount: Number(process.env.UPLOAD_FILE_MAX_AMOUNT || 1000) }; export async function initSystemConfig() { diff --git a/projects/app/src/web/common/system/useSystemStore.ts b/projects/app/src/web/common/system/useSystemStore.ts index 5a56c33ade..20ab3f9f35 100644 --- a/projects/app/src/web/common/system/useSystemStore.ts +++ b/projects/app/src/web/common/system/useSystemStore.ts @@ -146,7 +146,10 @@ export const useSystemStore = create()( }, initDataBufferId: undefined, - feConfigs: {}, + feConfigs: { + uploadFileMaxSize: 1000, + uploadFileMaxAmount: 1000 + }, subPlans: undefined, systemVersion: '0.0.0', diff --git a/projects/app/src/web/core/plugin/marketplace/api.ts b/projects/app/src/web/core/plugin/marketplace/api.ts index 14b2557a7f..5ef770eb88 100644 --- a/projects/app/src/web/core/plugin/marketplace/api.ts +++ b/projects/app/src/web/core/plugin/marketplace/api.ts @@ -24,3 +24,9 @@ export const getMarketPlaceToolTags = () => export const getMarketplaceDownloadURL = (toolId: string) => GET('/marketplace/api/tool/getDownloadUrl', { toolId }); + +export const getMarketplaceDownloadURLs = (toolIds: string[]) => + POST('/marketplace/api/tool/getDownloadUrl', { toolIds }); + +export const getMarketplaceToolVersions = () => + GET>('/marketplace/api/tool/versions'); diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts index 3c3a30a56d..9b6e925e90 100644 --- a/projects/app/src/web/support/user/team/api.ts +++ b/projects/app/src/web/support/user/team/api.ts @@ -1,6 +1,5 @@ import { GET, POST, PUT, DELETE } from '@/web/common/api/request'; import type { - CollaboratorItemType, CollaboratorListType, DeletePermissionQuery, UpdateClbPermissionProps @@ -37,6 +36,8 @@ export const postCreateTeam = (data: CreateTeamProps) => export const putUpdateTeam = (data: UpdateTeamProps) => PUT(`/support/user/team/update`, data); export const putSwitchTeam = (teamId: string) => PUT(`/proApi/support/user/team/switch`, { teamId }); +export const putTransferTeamOwnership = (userId: string) => + PUT(`/proApi/support/user/team/changeOwner`, { userId }); /* --------------- team member ---------------- */ export const getTeamMembers = ( diff --git a/projects/app/test/api/core/plugin/admin/marketplace/installed.test.ts b/projects/app/test/api/core/plugin/admin/marketplace/installed.test.ts index 71f747ef3e..571bb6a9a8 100644 --- a/projects/app/test/api/core/plugin/admin/marketplace/installed.test.ts +++ b/projects/app/test/api/core/plugin/admin/marketplace/installed.test.ts @@ -71,8 +71,24 @@ describe('handler (installed)', () => { expect(result).toEqual({ list: [ - { id: 'toolA', version: '1.2.3' }, - { id: 'toolB', version: '4.5.6' } + { + id: 'toolA', + version: '1.2.3', + name: { en: 'Tool A' }, + description: { en: 'desc' }, + icon: '', + author: undefined, + tags: undefined + }, + { + id: 'toolB', + version: '4.5.6', + name: { en: 'Tool B' }, + description: { en: 'desc' }, + icon: '', + author: undefined, + tags: undefined + } ] }); expect(APIGetSystemToolList).toHaveBeenCalledTimes(1); @@ -100,7 +116,17 @@ describe('handler (installed)', () => { const result = await handler(req, res); expect(result).toEqual({ - list: [{ id: 'my-tool-123', version: '0.0.1' }] + list: [ + { + id: 'my-tool-123', + version: '0.0.1', + name: { en: 'My Tool' }, + description: { en: 'desc' }, + icon: '', + author: undefined, + tags: undefined + } + ] }); }); @@ -126,7 +152,17 @@ describe('handler (installed)', () => { const result = await handler(req, res); expect(result).toEqual({ - list: [{ id: 'randomprefix-toolX', version: '9.9.9' }] + list: [ + { + id: 'randomprefix-toolX', + version: '9.9.9', + name: { en: 'Tool X' }, + description: { en: 'desc' }, + icon: '', + author: undefined, + tags: undefined + } + ] }); }); }); diff --git a/projects/marketplace/src/pages/api/tool/getDownloadUrl.ts b/projects/marketplace/src/pages/api/tool/getDownloadUrl.ts index 5cfd034be4..27dd1cae85 100644 --- a/projects/marketplace/src/pages/api/tool/getDownloadUrl.ts +++ b/projects/marketplace/src/pages/api/tool/getDownloadUrl.ts @@ -5,25 +5,32 @@ import { getPkgdownloadURL } from '@/service/s3'; import { increaseDownloadCount } from '@/service/downloadCount'; export type GetDownloadURLQuery = { - toolId: string; + toolId?: string; }; -export type GetDownloadURLBody = {}; -export type GetDownloadURLResponse = string; +export type GetDownloadURLBody = { + toolIds?: string[]; +}; +export type GetDownloadURLResponse = string | string[]; async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { const { toolId } = req.query; - if (!toolId) { - return Promise.reject('toolId is required'); + const { toolIds } = req.body; + if (!toolId && !toolIds) { + return Promise.reject('toolId or toolIds is required'); } - const tools = await getToolList(); - const tool = tools.find((item) => item.toolId === toolId); - if (!tool) { - return Promise.reject(`tool: ${toolId} not found`); + + const filterTools = toolIds && toolIds.length > 0 ? toolIds : toolId ? [toolId] : []; + const tools = (await getToolList()).filter((item) => filterTools.includes(item.toolId)); + + for await (const tool of tools) { + await increaseDownloadCount(tool.toolId, 'tool'); } - await increaseDownloadCount(toolId, 'tool'); - return getPkgdownloadURL(toolId); + + return toolId + ? getPkgdownloadURL(toolId) + : Array.from(tools.map((tool) => getPkgdownloadURL(tool.toolId))); } export default NextAPI(handler); diff --git a/projects/marketplace/src/pages/api/tool/versions.ts b/projects/marketplace/src/pages/api/tool/versions.ts new file mode 100644 index 0000000000..5293afda54 --- /dev/null +++ b/projects/marketplace/src/pages/api/tool/versions.ts @@ -0,0 +1,27 @@ +import { getToolList } from '@/service/tool/data'; +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; + +export type ToolListQuery = {}; +export type ToolListBody = {}; + +export type ToolListResponse = { toolId: string; version: string }[]; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + const data = await getToolList(); + + return data + .filter((item) => { + if (item.parentId) return false; + return true; + }) + .map(({ toolId, version }) => ({ + toolId, + version + })); +} + +export default NextAPI(handler); diff --git a/test/mocks/common/vector.ts b/test/mocks/common/vector.ts index 0d427014c0..274bc53d23 100644 --- a/test/mocks/common/vector.ts +++ b/test/mocks/common/vector.ts @@ -1,3 +1,4 @@ +import { SEEKDB_ADDRESS } from '@fastgpt/service/common/vectorDB/constants'; import { vi } from 'vitest'; /** @@ -60,7 +61,8 @@ vi.mock('@fastgpt/service/common/vectorDB/constants', () => ({ PG_ADDRESS: 'mock://pg', OCEANBASE_ADDRESS: undefined, MILVUS_ADDRESS: undefined, - MILVUS_TOKEN: undefined + MILVUS_TOKEN: undefined, + SEEKDB_ADDRESS: undefined })); // Export mocks for test assertions