Dify兼容低版本Chrome及iOS APP
目前dify使用的版本是1.1.3
1. 存在的问题
- Dify使用的js语法较新,打包部署时没有向下兼容。 错误表现:Application error: a client-side exception has occurred while loading 10.60.206.9 (see the browser console for more information).
- 由于iOS APP webview的安全限制,禁止自定义请求头。 错误表现:页面死循环刷新,401,404,应用不可用。
2. 针对Dify的js语法降级
经过测试目前,能够兼容到Chrome浏览器v75
版本,safari浏览器v14
2.1 修改web/next.config.js
文件
dify默认使用的是pnpm管理三方包,但是经过测试,transpilePackages
对pnpm的包存在兼容问题,就是放入到.pnpm
目录的三方包, next.js打包时是找不到的,也就不能编译,所以我把pnpm都退回到了使用npm
管理,对应的打包docker镜像配置文件web/Dockerfile
也修改了
# base image
FROM node:20-alpine3.20 AS base
# LABEL maintainer="takatost@gmail.com"
# if you located in China, you can use aliyun mirror to speed up
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata
# RUN npm install -g pnpm@9.12.2
# ENV PNPM_HOME="/pnpm"
# ENV PATH="$PNPM_HOME:$PATH"
# install packages
FROM base AS packages
WORKDIR /app/web
COPY package.json .
# COPY pnpm-lock.yaml .
# if you located in China, you can use taobao registry to speed up
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
RUN npm config set registry https://registry.npmmirror.com
# RUN pnpm install --frozen-lockfile
RUN npm i --legacy-peer-deps --ignore-scripts
# build resources
FROM base AS builder
WORKDIR /app/web
COPY --from=packages /app/web/ .
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=8192"
# RUN pnpm build
RUN npm run build
# production stage
FROM base AS production
ENV NODE_ENV=production
ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV APP_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_URL=http://127.0.0.1:5001
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
ENV PM2_INSTANCES=2
# set timezone
ENV TZ=UTC
RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone
WORKDIR /app/web
COPY --from=builder /app/web/public ./public
COPY --from=builder /app/web/.next/standalone ./
COPY --from=builder /app/web/.next/static ./.next/static
COPY docker/entrypoint.sh ./entrypoint.sh
# global runtime packages
# RUN pnpm add -g pm2 \
RUN npm i -g pm2 \
&& mkdir /.pm2 \
&& chown -R 1001:0 /.pm2 /app/web \
&& chmod -R g=u /.pm2 /app/web
ARG COMMIT_SHA
ENV COMMIT_SHA=${COMMIT_SHA}
USER 1001
EXPOSE 3000
ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]
出于性能考虑,next.js框架在配置打包时,默认会排除掉node_modules
中的三方包的编译,因此需要找到/chat/XXX
页面用到的所有不兼容的三方包
const nextConfig = {
transpilePackages: ['@tanstack/query-core', 'mermaid', 'marked', 'next/dist/client', 'mdast-util-gfm-autolink-literal', 'ky', 'react-i18next', 'tailwind-merge', 'emoji-mart', '@tanstack/virtual-core', '@reactflow/core', 'hast-util-from-html-isomorphic', 'mime', '@tanstack/react-query', 'hast-util-to-text', 'rehype-katex', 'zundo', '@svgdotjs/svg.js', '@monaco-editor/react', 'micromark-util-decode-numeric-character-reference', 'micromark-util-sanitize-uri'],
}
2.2 为了方便排查,关闭了代码的压缩和混淆
const nextConfig = {
webpack: (config, { dev, isServer }) => {
config.plugins.push(codeInspectorPlugin({ bundler: 'webpack' }))
config.optimization.minimize = false;
if (!isServer) {
config.optimization.minimizer = [];
}
return config
},
}
2.3 个别写法手动替换
由于有些三方包的正则表达式使用了零宽断言语法,即便配置了编译,也依旧转换不了,针对这一部分,暂时时在docker容器中替换修改,后期考虑写脚本替换。
源码位置实在 dify/web/node_modules/mdast-util-gfm-autolink-literal/lib/index.js
由于打包后,文件名已经混淆,真实位置需要根据在safari浏览器控制台报错的位置确定,或者在容器中全局搜索
搜索搜索函数名transformGfmAutolinkLiterals
或者findEmail
将正则表达式/(?<=^|\s|\p{P}|\p{S})([-.\w+]+)@([-\w]+(?:\.[-\w]+)+)/gu
替换为/(?:^|\s|[^\w\s])([-.\w+]+)@([-\w]+(?:\.[-\w]+)+)/gu
function transformGfmAutolinkLiterals(tree) {
findAndReplace(
tree,
[
[/(https?:\/\/|www(?=\.))([-.\w]+)([^ \t\r\n]*)/gi, findUrl],
// [/(?<=^|\s|\p{P}|\p{S})([-.\w+]+)@([-\w]+(?:\.[-\w]+)+)/gu, findEmail],源码修改前,编译后的零宽断言依旧没转
[/(?:^|\s|[^\w\s])([-.\w+]+)@([-\w]+(?:\.[-\w]+)+)/gu, findEmail] //修改后
],
{ignore: ['link', 'linkReference']}
)
}
3.解决iOS请求头问题
3.1 源码部署
根据项目的源码启动文档docker/README.md
,先进行源码启动,前后端及数据库启动完毕后,如果需要对外统一暴露80端口,还需要在本地额外搭建一个nginx服务器,根据不同的api前缀转发不同内部服务。下面是参考官方精简配置
server {
listen 80;
server_name Dify;
location /console/api {
proxy_pass http://api:5001;
}
location /api {
proxy_pass http://api:5001;
}
location /v1 {
proxy_pass http://api:5001;
}
location /files {
proxy_pass http://api:5001;
}
location /explore {
proxy_pass http://web:3000;
}
location /e/ {
proxy_pass http://plugin_daemon:5002;
proxy_set_header Dify-Hook-Url $scheme://$host$request_uri;
}
location / {
proxy_pass http://web:3000;
}
}
3.2 解决X-App-Code
缺失问题
这个值存放的是聊天应用的id,思路是从referer中获取,由于在有些跨站等情况下referer的地址不完整,所以添加了nginx配置,保证每次都可以稳定的从referer中获取id
add_header Referrer-Policy "unsafe-url";
添加允许自定义header头
add_header 'Access-Control-Allow-Headers' 'X-App-Code, Authorization';
为开发调试方便,禁用浏览器缓存
expires -1;
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
开发模式下需要兼容socket连接
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
nginx转发时,从referer中截取id信息,再把id赋值给请求头的X-App-Code中
# x-app-code的agentId
set $app_code "";
# referer中提取的agentId
set $referer_app_code "";
# 从referer中提取app_code(如果存在)
if ($http_referer ~* "/chat/([A-Za-z0-9]+)") {
set $referer_app_code $1;
}
# 设置最终使用的app_code - 优先使用header中的值
if ($http_x_app_code != "") {
set $app_code $http_x_app_code;
}
# 如果header中没有值,使用从referer提取的值
if ($http_x_app_code = "") {
set $app_code $referer_app_code;
}
proxy_set_header X-App-Code $app_code;
next.config.js中增加开发模式下,增加安全域名
const nextConfig = {
allowedDevOrigins: ['10.60.206.9', '20.60.128.21']
}
3.3 解决Authorization
缺失问题
解决了x-app-code
问题后,又报了很多/api/site
、/conversations?limit=100&pinned=false
、/api/parameters
和/api/meta
接口401问题
经过postman测试是缺少Authorization
,这个值是url中传入的token,而是经过dify后台拿到用户信息后生成的dify的token。所以当dify生成完新token后,需要由浏览器存入到浏览器的cookie中,再由nginx将该值提取出来放入Authorization
中
首先修改前端配置文件web/.env.local
和web/.env.example
,解决前端调后端接口跨域问题
NEXT_PUBLIC_API_PREFIX=/console/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=/api
修改web/app/components/share/utils.ts
文件
if (!accessTokenJson[sharedToken]) {
const res = await fetchAccessToken(sharedToken, fromToken)
if (res.access_token) {
accessTokenJson[sharedToken] = res.access_token
localStorage.setItem('token', JSON.stringify(accessTokenJson))
const expires = new Date()
expires.setFullYear(expires.getFullYear() + 10)
document.cookie = `token=${accessTokenJson[sharedToken]}; path=/; expires=${expires.toUTCString()}` //新增
}
} else {
const expires = new Date()
expires.setFullYear(expires.getFullYear() + 10)
document.cookie = `token=${accessTokenJson[sharedToken]}; path=/; expires=${expires.toUTCString()}` //新增
}
nginx进行数据转换
# 定义一个变量来存储token
set $auth_token "";
# 定义一个变量标记Authorization是否有效
set $auth_is_valid 0;
# 检查Authorization请求头是否存在且格式正确(以Bearer开头)
if ($http_authorization != "") {
set $auth_is_valid 1;
}
if ($auth_is_valid = 1) {
set $auth_token $http_authorization;
}
if ($auth_is_valid = 0) {
set $auth_token "Bearer $cookie_token";
}
proxy_set_header Authorization $auth_token;
完整的开发nginx配置如下:
# Dify
server {
listen 80;
server_name localhost:80;
location /console/api {
proxy_pass http://localhost:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 缓存设置
expires -1;
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
# 跨域支持
add_header 'Access-Control-Allow-Headers' 'X-App-Code, Authorization';
add_header Referrer-Policy "unsafe-url";
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api {
proxy_pass http://localhost:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 预先提取所有需要的变量
# x-app-code的agentId
set $app_code "";
# referer中提取的agentId
set $referer_app_code "";
# 从referer中提取app_code(如果存在)
if ($http_referer ~* "/chat/([A-Za-z0-9]+)") {
set $referer_app_code $1;
}
# 设置最终使用的app_code - 优先使用header中的值
if ($http_x_app_code != "") {
set $app_code $http_x_app_code;
}
# 如果header中没有值,使用从referer提取的值
if ($http_x_app_code = "") {
set $app_code $referer_app_code;
}
proxy_set_header X-App-Code $app_code;
# 定义一个变量来存储token
set $auth_token "";
# 定义一个变量标记Authorization是否有效
set $auth_is_valid 0;
# 检查Authorization请求头是否存在且格式正确(以Bearer开头)
if ($http_authorization != "") {
set $auth_is_valid 1;
}
if ($auth_is_valid = 1) {
set $auth_token $http_authorization;
}
if ($auth_is_valid = 0) {
set $auth_token "Bearer $cookie_token";
}
proxy_set_header Authorization $auth_token;
# 缓存设置
expires -1;
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
# 跨域支持
add_header 'Access-Control-Allow-Headers' 'X-App-Code, Authorization';
add_header Referrer-Policy "unsafe-url";
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /v1 {
proxy_pass http://localhost:5001;
}
location /files {
proxy_pass http://localhost:5001;
}
location /explore {
proxy_pass http://localhost:3000;
}
# location /e/ {
# proxy_pass http://plugin_daemon:5002;
# }
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
expires -1;
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
# 跨域支持
add_header 'Access-Control-Allow-Headers' 'X-App-Code, Authorization';
add_header Referrer-Policy "unsafe-url";
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
next.config.js
完成配置
const { codeInspectorPlugin } = require('code-inspector-plugin')
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
// If you use remark-gfm, you'll need to use next.config.mjs
// as the package is ESM only
// https://github.com/remarkjs/remark-gfm#install
remarkPlugins: [],
rehypePlugins: [],
// If you use `MDXProvider`, uncomment the following line.
// providerImportSource: "@mdx-js/react",
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { dev, isServer }) => {
config.plugins.push(codeInspectorPlugin({ bundler: 'webpack' }))
config.optimization.minimize = false;
if (!isServer) {
config.optimization.minimizer = [];
}
return config
},
productionBrowserSourceMaps: true, // enable browser source map generation during the production build
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
experimental: {
externalDir: true,
},
allowedDevOrigins: ['10.60.206.9', '20.60.128.21'],
transpilePackages: ['@tanstack/query-core', 'mermaid', 'marked', 'next/dist/client', 'mdast-util-gfm-autolink-literal', 'ky', 'react-i18next', 'tailwind-merge', 'emoji-mart', '@tanstack/virtual-core', '@reactflow/core', 'hast-util-from-html-isomorphic', 'mime', '@tanstack/react-query', 'hast-util-to-text', 'rehype-katex', 'zundo', '@svgdotjs/svg.js', '@monaco-editor/react', 'micromark-util-decode-numeric-character-reference', 'micromark-util-sanitize-uri'],
// fix all before production. Now it slow the develop speed.
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
dirs: ['app', 'bin', 'config', 'context', 'hooks', 'i18n', 'models', 'service', 'test', 'types', 'utils'],
},
typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
ignoreBuildErrors: true,
},
reactStrictMode: true,
async redirects() {
return [
{
source: '/',
destination: '/apps',
permanent: false,
},
]
},
output: 'standalone',
}
module.exports = withMDX(nextConfig)