基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 - 但风偏偏,雨渐渐 - 博客园

mikel阅读(457)

来源: 基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 – 但风偏偏,雨渐渐 – 博客园

 

    本次演示部署环境:Windows 10专业版,转载请说明出处

下载安装Docker

  Docker官网:https://www.docker.com/

自定义Docker安装路径

Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。

新建安装目录:E:\MySoftware\Docker并将Docker安装包放在目录内,这里有个小细节,安装包名称一定要改下,官网下载下来的名称叫:Docker Desktop Installer.exe,一定要修改一下,不能用这个名字,否则等下在CMD命令安装的时候就会报错说被资源占用,因为Docker在安装时会解压一个一模一样名称的exe程序,重名就会导致安装失败,所以一定要改下名字。

  在文件路径输入cmd回车

输入:


.\"Docker.exe" install --installation-dir=E:\MySoftware\Docker
语法:.\”安装程序名称” install --installation-dir=指定Docker安装的路径

   安装完成后会提示Installation sueceeded

  桌面会出现Docker图标

  启动Docker这里很多人会报这个错,这个是因为电脑没有WSL导致无法启动Docker容器。

下载WSL

  进入微软官网按步骤执行即解决,几分钟完成。

https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-linux-kernel-update-package

  用管理员身份打开Powershell窗口,粘贴微软官网的命令执行下载即可

粘贴执行:

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
wsl --set-default-version 2

  下载完后会启动一个这个页面,可以关闭。

  注:完成内核更新包安装后需重启系统(微软官方指南),再启动下Docker,出现这个界面就代表WSL和Docker都启动成功了。

Docker镜像存储迁移

这时进入Docker设置中,将Docker的镜像资源存放路径改一下,不然都会下载都C盘。

Docker镜像源加速配置

至关重要的地方来了,打开Docker的设置中的–>Docker Engine,然后把国内的镜像源复制进去保存,我这里提供一些,如果失效了就百度找新的。

复制代码
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://noohub.ru",
"https://huecker.io",
"https://dockerhub.timeweb.cloud",
"https://0c105db5188026850f80c001def654a0.mirror.swr.myhuaweicloud.com",
"https://5tqw56kt.mirror.aliyuncs.com",
"https://docker.1panel.live",
"http://mirrors.ustc.edu.cn/",
"http://mirror.azure.cn/",
"https://hub.rat.dev/",
"https://docker.ckyl.me/",
"https://docker.chenby.cn",
"https://docker.hpcloud.cloud",
"https://docker.m.daocloud.io"
]
}
复制代码

保存镜像源后就可以试一下拉取镜像,如果拉取不成功也可以重启下Docker,还是不行提示超时就说明镜像源失效了,就在网上搜索下新的镜像源。

测试拉取镜像

在cmd命令窗口输入:

docker pull hello-world

出现这个Status: Dowloaded newer image for hello-world:latest就代表镜像源没有问题。

安装Dify

下载Dify代码包

进入github下载Dify代码包:https://github.com/langgenius/dify

解压代码包后,把压缩后的文件夹复制到自己想要安装的目录下,这里复制一下.env.example文件,然后重命名一下改成.env

  在当前文件路径下输入CMD回车

拉取Dify依赖镜像资源

  粘贴以下命令回车,会自动下载一些依赖资源。如果你的下载失败就是镜像源失效了,换一个镜像源,重新拉取镜像。

docker-compose up -d

下载完成

  回到Docker可以都看到已经下载好的镜像全部都显示了,并且都在运行。

进入Dify后台

输入http://127.0.0.1/会自动打开Dify的页面,有人会遇到这个Internal Server Error报错,这是因为镜像下载来后,有部分镜像还在启动中或未启动,这时候将所有镜像重启一次才可以。

  重启所有镜像

创建管理员用户

  重新进入Dify管理后台,首次进入需创建管理员用户。

创建管理员用户后,将进入登录界面。

登录成功

添加AI模型

点击右上角头像-设置

成员这里可以创建企业内成员进行登录使用。

选择模型供应商

这里我本地已经安装部署好了Ollama和Deepseek R1和BGE-M3模型,如果没有部署好的请看我这篇文章本地电脑部署DeepSeek 大模型AI

  由于我本地已经安装好了Ollama,所以就找到Ollama,点击安装插件,其他供应商选择对应安装。下载可能稍慢,请耐心等待。

添加模型时,若不知模型名称,可在CMD中输入Ollama list查看本地模型名称并复制。

模型类型:

推理模型 → LLM
嵌入模型 → Text Embedding

模型名称就把刚刚复制下来的粘贴上去就可以了。

这里IP地址要注意了,由于我没有用Docker容器里部署Ollama,而是在本地电脑安装的Ollama,这里对IP就要进行特殊处理下了,需要改为:http://host.docker.internal:11434。

网络通信原理:
host.docker.internal为Docker内置域名解析,实现容器与宿主机服务的桥接。简单说就是Docker自己和我们电脑主机网络做了一个映射关系。

模型添加完成

创建应用

其实我也不太会使用,就简单粗糙的做个示范吧,要深入研究需要找下资料学习下。

这里我选择创建聊天助手(每个不同应用的作用不一样,选择与自己相符的就行)

添加一些提示词、变量、知识库、模型,设置好后在右边可以调试预览效果,调试完成后就可以发布应用了。

这里提一句,由于我自己的电脑资源很一般,所以每次一提问的时候资源就占比很高,不过等AI思考完毕后资源占用会下降。

测试结果,虽然回答是错误的。

知识库测试

我这里测试了下知识库检索,上传了6个本地文档。

然后我简单的定义了提示词后,对模型提出问题:结合知识库帮我找出住在向XXX街道人员的电话和姓名。

 

 

然后真的回答对了,全体起立!

这是源文件里的内容。(虚拟信息,如有雷同纯属巧合)

 

WSL资源控制

由于我是针对个人学习,在学习完后我发现我的电脑内存占比一直居高不下,在任务管理器查到了是一个Vmmem的进程占用,大概也知道应该是虚拟机类的占用。

搜索了下网上资源了解到vmmem是一个由WSL(Windows Subsystem for Linux)创建的虚拟进程,主要用于管理WSL2的内存和CPU资源。当WSL2运行时,Vmmem进程会占用大量的内存和CPU资源,以确保虚拟机的最佳性能。然而,这可能会导致主机系统的其他应用程序运行缓慢或无法正常运行‌。

关闭WSL服务

所以如果不用的时候可以关闭掉WSL服务。

在cmd里输入:

wsl --shutdown

关闭后电脑资源就回到正常状态了。

启动WSL服务

那如果我们后再用的时候就重新启动WSL服务就可以。

在cmd输入:

wsl

最后的最后

关于Dify的作用文中提到的只是冰山一角,它真正的厉害之处是它的工作流,由于博主知识有限,只能教大家部署应用,具体的功能开发使用还要大家自行学习,后续博主也会去学习Dify的相关知识,有机会的话就再开一贴。如有讲的不对的地方,敬请指正。

附上Dify的官方操作手册地址:https://docs.dify.ai/zh-hans

这是我整个学习过程中遇到的问题,最后结合百度和AI最后都完成解决了。

总结几个小坑:

1、WSL2的安装。

2、Docker容器镜像源的设置。

3、Dify依赖镜像的拉取。

4、Dify添加模型时IP映射设置。

C#.NET Rsa私钥加密公钥解密 - runliuv - 博客园

mikel阅读(241)

来源: C#.NET Rsa私钥加密公钥解密 – runliuv – 博客园

C#中,RSA私钥只能签名,不能加密,如果要加密,要借助BouncyCastle库。

nuget 中引用 Portable.BouncyCastle。

工具类:

RsaEncryptUtil

复制代码
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.Security.Cryptography;
using System.Text;

namespace CommonUtils
{
    public static class RsaEncryptUtil
    {
        /// <summary>
        /// 私钥加密 .Net平台默认是使用公钥进行加密,私钥进行解密。私钥加密需要自己实现或者使用第三方dll
        /// </summary>
        /// <param name="data"></param>
        /// <param name="key">私钥,格式:PKCS8</param>
        /// <returns></returns>
        public static byte[] encryptByPrivateKey(String data, String key)
        {
            String priKey = key.Trim();
            String xmlPrivateKey = RSAPrivateKeyJava2DotNet(priKey);
            //加载私钥  
            RSACryptoServiceProvider privateRsa = new RSACryptoServiceProvider();
            privateRsa.FromXmlString(xmlPrivateKey);
            //转换密钥  
            AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetKeyPair(privateRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(true, keyPair.Private); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Encoding.UTF8.GetBytes(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//加密  
            return outBytes;

        }

        /// <summary>
        /// 私钥加密
        /// </summary>
        /// <param name="data">明文</param>
        /// <param name="key">私钥</param>
        /// <param name="keyFormat">私钥格式:PKCS1,PKCS8</param>
        /// <returns></returns>
        public static byte[] encryptByPrivateKey(String data, String key,string keyFormat)
        {
            String priKey = key.Trim();
             
            //加载私钥  
            RSACryptoServiceProvider privateRsa = RsaUtil.LoadPrivateKey(key,keyFormat);
             
            //转换密钥  
            AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetKeyPair(privateRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(true, keyPair.Private); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Encoding.UTF8.GetBytes(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//加密  
            return outBytes;

        }

        /// <summary>
        /// RSA私钥格式转换,java->.net
        /// </summary>
        /// <param name="privateKey"></param>
        /// <returns></returns>
        private static string RSAPrivateKeyJava2DotNet(string privateKey)
        {
            RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));

            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
                Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
        }

        /// <summary>
        /// 用公钥解密
        /// </summary>
        /// <param name="data"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static byte[] decryptByPublicKey(String data, String key)
        {
            String pubKey = key.Trim();
            String xmlPublicKey = RSAPublicKeyJava2DotNet(pubKey);

            RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
            publicRsa.FromXmlString(xmlPublicKey);

            AsymmetricKeyParameter keyPair = DotNetUtilities.GetRsaPublicKey(publicRsa);
            //转换密钥  
            // AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(publicRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(false, keyPair); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Convert.FromBase64String(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//解密  
            return outBytes;
        }

        /// <summary>
        /// RSA公钥格式转换,java->.net
        /// </summary>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        private static string RSAPublicKeyJava2DotNet(string publicKey)
        {
            RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
                Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
        }


    }
}
复制代码

RsaUtil

复制代码
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace CommonUtils
{
    public static class RsaUtil
    {
        #region 加载私钥


        /// <summary>
        /// 转换私钥字符串为RSACryptoServiceProvider
        /// </summary>
        /// <param name="privateKeyStr">私钥字符串</param>
        /// <param name="keyFormat">PKCS8,PKCS1</param>
        /// <param name="signType">RSA 私钥长度1024 ,RSA2 私钥长度2048</param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKey(string privateKeyStr, string keyFormat)
        {
            string signType = "RSA";
            if (privateKeyStr.Length > 1024)
            {
                signType = "RSA2";
            }
            //PKCS8,PKCS1
            if (keyFormat == "PKCS1")
            {
                return LoadPrivateKeyPKCS1(privateKeyStr, signType);
            }
            else
            {
                return LoadPrivateKeyPKCS8(privateKeyStr);
            }
        }

        /// <summary>
        /// PKCS1 格式私钥转 RSACryptoServiceProvider 对象
        /// </summary>
        /// <param name="strKey">pcsk1 私钥的文本内容</param>
        /// <param name="signType">RSA 私钥长度1024 ,RSA2 私钥长度2048 </param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKeyPKCS1(string privateKeyPemPkcs1, string signType)
        {
            try
            {
                privateKeyPemPkcs1 = privateKeyPemPkcs1.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();
                privateKeyPemPkcs1 = privateKeyPemPkcs1.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

                byte[] data = null;
                //读取带

                data = Convert.FromBase64String(privateKeyPemPkcs1);


                RSACryptoServiceProvider rsa = DecodeRSAPrivateKey(data, signType);
                return rsa;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return null;
        }

        private static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey, string signType)
        {
            byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;

            // --------- Set up stream to decode the asn.1 encoded RSA private key ------
            MemoryStream mem = new MemoryStream(privkey);
            BinaryReader binr = new BinaryReader(mem);  //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
            int elems = 0;
            try
            {
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();    //advance 2 bytes
                else
                    return null;

                twobytes = binr.ReadUInt16();
                if (twobytes != 0x0102) //version number
                    return null;
                bt = binr.ReadByte();
                if (bt != 0x00)
                    return null;


                //------ all private key components are Integer sequences ----
                elems = GetIntegerSize(binr);
                MODULUS = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                E = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                D = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                P = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                Q = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DP = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DQ = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                IQ = binr.ReadBytes(elems);


                // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                CspParameters CspParameters = new CspParameters();
                CspParameters.Flags = CspProviderFlags.UseMachineKeyStore;

                int bitLen = 1024;
                if ("RSA2".Equals(signType))
                {
                    bitLen = 2048;
                }

                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(bitLen, CspParameters);
                //RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();

                RSAParameters RSAparams = new RSAParameters();
                RSAparams.Modulus = MODULUS;
                RSAparams.Exponent = E;
                RSAparams.D = D;
                RSAparams.P = P;
                RSAparams.Q = Q;
                RSAparams.DP = DP;
                RSAparams.DQ = DQ;
                RSAparams.InverseQ = IQ;
                RSA.ImportParameters(RSAparams);
                return RSA;
            }
            catch (Exception ex)
            {
                throw ex;
                // return null;
            }
            finally
            {
                binr.Close();
            }
        }

        private static int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)        //expect integer
                return 0;
            bt = binr.ReadByte();

            if (bt == 0x81)
                count = binr.ReadByte();    // data size in next byte
            else
                if (bt == 0x82)
            {
                highbyte = binr.ReadByte(); // data size in next 2 bytes
                lowbyte = binr.ReadByte();
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                count = BitConverter.ToInt32(modint, 0);
            }
            else
            {
                count = bt;     // we already have the data size
            }

            while (binr.ReadByte() == 0x00)
            {    //remove high order zeros in data
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);        //last ReadByte wasn't a removed zero, so back up a byte
            return count;
        }

        /// <summary>
        /// PKCS8 文本转RSACryptoServiceProvider 对象
        /// </summary>
        /// <param name="privateKeyPemPkcs8"></param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKeyPKCS8(string privateKeyPemPkcs8)
        {

            try
            {
                //PKCS8是“BEGIN PRIVATE KEY”
                privateKeyPemPkcs8 = privateKeyPemPkcs8.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();
                privateKeyPemPkcs8 = privateKeyPemPkcs8.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

                //pkcs8 文本先转为 .NET XML 私钥字符串
                string privateKeyXml = RSAPrivateKeyJava2DotNet(privateKeyPemPkcs8);

                RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
                publicRsa.FromXmlString(privateKeyXml);
                return publicRsa;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// PKCS8 私钥文本 转 .NET XML 私钥文本
        /// </summary>
        /// <param name="privateKeyPemPkcs8"></param>
        /// <returns></returns>
        public static string RSAPrivateKeyJava2DotNet(string privateKeyPemPkcs8)
        {
            RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKeyPemPkcs8));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
            Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
        }

        #endregion


        /// <summary>
        /// 加载公钥证书
        /// </summary>
        /// <param name="publicKeyCert">公钥证书文本内容</param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPublicCert(string publicKeyCert)
        {

            publicKeyCert = publicKeyCert.Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", "").Replace("\r", "").Replace("\n", "").Trim();

            byte[] bytesCerContent = Convert.FromBase64String(publicKeyCert);
            X509Certificate2 x509 = new X509Certificate2(bytesCerContent);
            RSACryptoServiceProvider rsaPub = (RSACryptoServiceProvider)x509.PublicKey.Key;
            return rsaPub;

        }

        /// <summary>
        /// pem 公钥文本 转  .NET RSACryptoServiceProvider。
        /// </summary>
        /// <param name="publicKeyPem"></param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPublicKey(string publicKeyPem)
        {

            publicKeyPem = publicKeyPem.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

            //pem 公钥文本 转  .NET XML 公钥文本。
            string publicKeyXml = RSAPublicKeyJava2DotNet(publicKeyPem);

            RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
            publicRsa.FromXmlString(publicKeyXml);
            return publicRsa;


        }

        /// <summary>
        /// pem 公钥文本 转  .NET XML 公钥文本。
        /// </summary>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        private static string RSAPublicKeyJava2DotNet(string publicKey)
        {
            RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
                Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
        }


    }
}
复制代码

 

使用:

这个方法只能加密 私钥长度/8 -11 个字符,分段加密的代码要自己处理了。

私钥加密:

复制代码
private void btnPrivateKeyEncrypt_Click(object sender, EventArgs e)
        {
            try
            {
                //byte[] rst = RsaEncryptUtil.encryptByPrivateKey(txtMingWen.Text, txtPrivateKey.Text);
                byte[] rst = RsaEncryptUtil.encryptByPrivateKey(txtMingWen.Text, txtPrivateKey.Text,cbxPrivateKeyFormat.Text);

                //加密后一般转Base64String ,Base64FormattingOptions.InsertLineBreaks.
                string base64str = Convert.ToBase64String(rst, Base64FormattingOptions.InsertLineBreaks);

                txtJiaMiHou.Text = base64str;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
复制代码

公钥解密:

复制代码
private void btnPubKeyDecrypt_Click(object sender, EventArgs e)
        {
            try
            {
                byte[] rst = RsaEncryptUtil.decryptByPublicKey(txtJiaMiHou.Text, txtPubKey.Text);
                 
                string strRst = Encoding.UTF8.GetString(rst);
                txtJieMiHou.Text = strRst;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

Java 实现微信小程序不同人员生成不同小程序码并追踪扫码来源 - VipSoft - 博客园

mikel阅读(190)

来源: Java 实现微信小程序不同人员生成不同小程序码并追踪扫码来源 – VipSoft – 博客园

下面我将详细介绍如何使用Java后台实现这一功能。

一、整体架构设计

  1. 前端:微信小程序
  2. 后端:Java (Spring Boot)
  3. 数据库:MySQL/其他
  4. 微信接口:调用微信小程序码生成API

二、数据库设计

1. 推广人员表(promoter)

CREATE TABLE `promoter` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '推广人员姓名',
  `mobile` varchar(20) COMMENT '联系电话',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 用户-推广关系表(user_promoter_relation)

CREATE TABLE `user_promoter_relation` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64) NOT NULL COMMENT '小程序用户openid',
  `promoter_id` bigint NOT NULL COMMENT '推广人员ID',
  `first_scan_time` datetime NOT NULL COMMENT '首次扫码时间',
  `last_scan_time` datetime NOT NULL COMMENT '最近扫码时间',
  `scan_count` int NOT NULL DEFAULT '1' COMMENT '扫码次数',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_promoter` (`user_id`,`promoter_id`),
  KEY `idx_promoter` (`promoter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、Java后端实现

1. 添加微信小程序Java SDK依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>4.1.0</version>
</dependency>

2. 配置微信小程序参数

@Configuration
public class WxMaConfiguration {
    
    @Value("${wx.miniapp.appid}")
    private String appid;
    
    @Value("${wx.miniapp.secret}")
    private String secret;
    
    @Bean
    public WxMaService wxMaService() {
        WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
        config.setAppid(appid);
        config.setSecret(secret);
        
        WxMaService service = new WxMaServiceImpl();
        service.setWxMaConfig(config);
        return service;
    }
}

3. 生成带参数的小程序码

@RestController
@RequestMapping("/api/qrcode")
public class QrCodeController {
    
    @Autowired
    private WxMaService wxMaService;
    
    @Autowired
    private PromoterService promoterService;
    
    /**
     * 生成推广二维码
     * @param promoterId 推广人员ID
     * @return 二维码图片字节流
     */
    @GetMapping("/generate")
    public void generatePromoterQrCode(@RequestParam Long promoterId, 
                                     HttpServletResponse response) throws IOException {
        // 验证推广人员是否存在
        Promoter promoter = promoterService.getById(promoterId);
        if (promoter == null) {
            throw new RuntimeException("推广人员不存在");
        }
        
        // 生成小程序码
        String scene = "promoterId=" + promoterId;
        WxMaQrcodeService qrcodeService = wxMaService.getQrcodeService();
        File qrCodeFile = qrcodeService.createWxaCodeUnlimit(scene, "pages/index/index", 430, true, null, false);
        
        // 返回图片流
        response.setContentType("image/jpeg");
        try (InputStream in = new FileInputStream(qrCodeFile);
             OutputStream out = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
}

4. 处理扫码进入事件

@RestController
@RequestMapping("/api/track")
public class TrackController {
    
    @Autowired
    private UserPromoterRelationService relationService;
    
    /**
     * 记录用户扫码行为
     * @param dto 包含用户信息和推广信息
     * @return 操作结果
     */
    @PostMapping("/scan")
    public Result trackScan(@RequestBody ScanTrackDTO dto) {
        // 解析scene参数
        String scene = dto.getScene();
        Map<String, String> sceneParams = parseScene(scene);
        String promoterIdStr = sceneParams.get("promoterId");
        
        if (StringUtils.isBlank(promoterIdStr)) {
            return Result.fail("缺少推广人员参数");
        }
        
        try {
            Long promoterId = Long.parseLong(promoterIdStr);
            relationService.recordUserScan(dto.getOpenid(), promoterId);
            return Result.success();
        } catch (NumberFormatException e) {
            return Result.fail("推广人员参数格式错误");
        }
    }
    
    private Map<String, String> parseScene(String scene) {
        Map<String, String> params = new HashMap<>();
        if (StringUtils.isBlank(scene)) {
            return params;
        }
        
        String[] pairs = scene.split("&");
        for (String pair : pairs) {
            String[] kv = pair.split("=");
            if (kv.length == 2) {
                params.put(kv[0], kv[1]);
            }
        }
        return params;
    }
}

5. 用户-推广关系服务

@Service
public class UserPromoterRelationServiceImpl implements UserPromoterRelationService {
    
    @Autowired
    private UserPromoterRelationMapper relationMapper;
    
    @Override
    @Transactional
    public void recordUserScan(String openid, Long promoterId) {
        // 查询是否已有记录
        UserPromoterRelation relation = relationMapper.selectByUserAndPromoter(openid, promoterId);
        
        Date now = new Date();
        if (relation == null) {
            // 新建关系记录
            relation = new UserPromoterRelation();
            relation.setUserId(openid);
            relation.setPromoterId(promoterId);
            relation.setFirstScanTime(now);
            relation.setLastScanTime(now);
            relation.setScanCount(1);
            relationMapper.insert(relation);
        } else {
            // 更新已有记录
            relation.setLastScanTime(now);
            relation.setScanCount(relation.getScanCount() + 1);
            relationMapper.updateById(relation);
        }
    }
}

四、小程序前端处理

在小程序的app.js中处理扫码进入的场景:

App({
  onLaunch: function(options) {
    // 处理扫码进入的情况
    if (options.scene === 1047 || options.scene === 1048 || options.scene === 1049) {
      // 这些scene值表示是通过扫码进入
      const scene = decodeURIComponent(options.query.scene);
      
      // 上报扫码信息到后端
      wx.request({
        url: 'https://yourdomain.com/api/track/scan',
        method: 'POST',
        data: {
          scene: scene,
          openid: this.globalData.openid // 需要先获取用户openid
        },
        success: function(res) {
          console.log('扫码记录成功', res);
        }
      });
    }
  }
})

五、数据统计接口实现

@RestController
@RequestMapping("/api/stat")
public class StatController {
    
    @Autowired
    private UserPromoterRelationMapper relationMapper;
    
    /**
     * 获取推广人员业绩统计
     * @param promoterId 推广人员ID
     * @param startDate 开始日期
     * @param endDate 结束日期
     * @return 统计结果
     */
    @GetMapping("/promoter")
    public Result getPromoterStats(@RequestParam Long promoterId,
                                 @RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date startDate,
                                 @RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date endDate) {
        
        // 构建查询条件
        QueryWrapper<UserPromoterRelation> query = new QueryWrapper<>();
        query.eq("promoter_id", promoterId);
        
        if (startDate != null) {
            query.ge("first_scan_time", startDate);
        }
        if (endDate != null) {
            query.le("first_scan_time", endDate);
        }
        
        // 执行查询
        int totalUsers = relationMapper.selectCount(query);
        List<Map<String, Object>> dailyStats = relationMapper.selectDailyStatsByPromoter(promoterId, startDate, endDate);
        
        // 返回结果
        Map<String, Object> result = new HashMap<>();
        result.put("totalUsers", totalUsers);
        result.put("dailyStats", dailyStats);
        
        return Result.success(result);
    }
}

六、安全注意事项

  1. 参数校验:所有传入的promoterId需要验证是否存在
  2. 防刷机制:限制同一用户频繁上报扫码记录
  3. HTTPS:确保所有接口使用HTTPS协议
  4. 权限控制:推广数据统计接口需要添加权限验证
  5. 日志记录:记录所有二维码生成和扫码行为

七、扩展功能建议

  1. 二级分销:可以扩展支持多级推广关系
  2. 奖励机制:根据扫码用户的活动情况给推广人员奖励
  3. 实时通知:当有新用户扫码时,实时通知推广人员
  4. 数据分析:提供更详细的数据分析报表

通过以上Java实现,你可以完整地构建一个支持不同人员生成不同小程序码并能追踪扫码来源的系统。

Web性能优化:从 2 秒到200毫秒 - ASER_1989 - 博客园

mikel阅读(184)

来源: Web性能优化:从 2 秒到200毫秒 – ASER_1989 – 博客园

前不久发布了个人笔记软件 Nebula Note 的Web预览版(传送门),整体开发体验和使用效果都很满意。但作为Web工程师的我习惯性的打开了浏览器开发者工具的Network面板,主要想观察首次加载时间。2 秒+!显然,这个加载速度无法接受。于是便开始了一轮深入优化,目标是:将首页加载时间控制在 1 秒内,真正的实现秒开。

 

性能瓶颈分析

从浏览器开发者工具的Network面板上可以很明显的观察到是首屏资源体积过大所致。项目技术栈为:

  • 前端框架:React
  • 服务端框架:NodeJsKoa
  • 构建工具:Vite
  • UI 组件:自研的 Nebula UI,由于功能过于简单,所以没有用主流的UI库。

排除自研代码后,问题可能出在集成的第三方组件上。使用打包分析工具检查产物体积,结果如下:

  • react-codemirror在线代码编辑器:体积最大
  • toast-ui Markdown 编辑器:第二大
  • 自研逻辑:占比极小

使用source-map-explorer对构架结果进行分析,截图如下:

打包分析图

 

基础优化:开启 Brotli 压缩

当前服务部署在 99 元云服务器 上的 Kubernetes 环境中,所有服务通过自定义模板的 Deployment 文件部署(配置传送门),IngressRoute 中默认启用了Gzip 压缩。Gzip还是有点温柔,考虑进一步压榨传输体积,于是启用了Brotli压缩。构建结果对比测试效果如下:

压缩对比图

Brotli的实力毋庸置疑,比Gzip多压缩了近200KB。而且这次是在打包的时候就对资源进行了压缩,理论上应该能有效缩减服务器的响应时间。但即便如此页面加载时间仍未突破 1 秒。

 

深度优化:移除冗余语言包(AST静态裁剪)

根据打包分析结果,react-codemirror 是最大“重量级选手”。主要原因是其默认引入了大量编程语言的语法高亮支持,而目前 Nebula Note 实际仅使用少数几种。因此,静态分析源码后,通过自定义 Vite 插件在构建阶段识别未使用的语言包,然后再利用 AST(抽象语法树)移除无关代码,最后打包体积减少1MB+。效果是相当的炸裂。优化后打包体积对比图如下:

优化后图1

构建分析结果:

优化后图2

此轮优化后react-codemirror 从第一名降至第二,首屏加载时间也成功挤进 1 秒以内。最终成果如下:

秒开效果

 

更近一步:延迟加载非首屏组件

虽然“秒开”目标已实现,但从打包占比来看 react-codemirror 与 toast-ui 两大组件仍占据 近 80% 体积,并且这两个包在第一屏中是非必需的,或只需其一。于是采用 React 的 Suspense + lazy 机制,针对这两个组件实现延迟加载:

import React, { Suspense, lazy } from 'react';
import SuspenseLoading from '@client/components/suspenseLoading';
import type { Props as IProps } from './codeMirror';

const Editor = lazy(() => import('./codeMirror'));

export type Props = IProps;
const CodeEditor = (props: IProps) => {
    return (
        <Suspense fallback={<SuspenseLoading />}>
            <Editor {...props} />
        </Suspense>
    );
};
export default CodeEditor;

使用Suspense后,懒加载的模块在构建的时候会被拆成独立的包,这对于首屏的加载非常的友好。通过对比可以看到不仅是JS文件从262KB降到了93KB,首次加载的CSS文件更是从83.5KB降到了2.1KB。

 

最后

有一个很奇怪的现象,CSS的TTFB很不稳定,在约40ms和100ms间反复横跳。其他资源,尤其是Http请求相关的资源表现很稳定。有知道原因的朋友,还请在评论区分享一下。最后附上博客所述内容资源,欢迎点赞支持~✌️。

Nebula Note预览版:https://note.aser1989.cn/
Nebula Note源代码: https://github.com/ASER1989/nebula-note

【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用(一) - 郑知鱼 - 博客园

mikel阅读(226)

来源: 【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用(一) – 郑知鱼 – 博客园

前言

在移动应用开发领域,Flutter以其出色的跨平台能力和高效的开发体验赢得了众多开发者的青睐,是许多移动开发者混合开发的首选。

随着HarmonyOS的崛起,许多开发者开始探索如何将Flutter应用迁移到鸿蒙生态。本文将带你从零开始,使用Flutter开发HarmonyOS应用,并借助强大的AI编程助手Cursor来加速UI开发过程。

这是“【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用”系列的第一篇。

一、环境准备:搭建Flutter for HarmonyOS开发环境

1.1 基础工具安装

首先确保你的开发机器已安装以下基础工具:

  • Flutter SDK(推荐3.13.0或更高版本)
    # 下载Flutter SDK
    git clone https://github.com/flutter/flutter.git -b stable
    
    # 添加环境变量
    export PATH="$PATH:`pwd`/flutter/bin"
    
    # 运行flutter doctor检查依赖
    flutter doctor
    
  • HarmonyOS开发工具
    API12, deveco-studio-5.0 或 command-line-tools-5.0 (推荐使用5.0.0 Release或更新版本)
  • JDK17
  • 配置环境变量
 export TOOL_HOME=/Applications/DevEco-Studio.app/Contents # mac环境
 export DEVECO_SDK_HOME=$TOOL_HOME/sdk # command-line-tools/sdk
 export PATH=$TOOL_HOME/tools/ohpm/bin:$PATH # command-line-tools/ohpm/bin
 export PATH=$TOOL_HOME/tools/hvigor/bin:$PATH # command-line-tools/hvigor/bin
 export PATH=$TOOL_HOME/tools/node/bin:$PATH # command-line-tools/tool/node/bin

📢📢📢注意
这里有一个问题需要注意,支持鸿蒙的Flutter SDK版本目前是3.22.0版本。在开发适配iOS和Android时,我们使用的Flutter版本可能高于3.22.0。这时,因此需要使用fvm对Flutter版本进行管理,可以同时维护和切换多个Flutter版本。同时也建议,Flutter的鸿蒙应用仓库最好和其他端仓库分开,因为目前支持鸿蒙的Flutter版本是滞后于Flutter官方版本的。

使用fvm维护多个版本示例:

4001

1.2 配置Flutter鸿蒙支持

目前Flutter对HarmonyOS的支持主要通过开源项目OpenHarmony-TPC/flutter_flutter实现。

该仓库是基于Flutter官方仓库的3.22.0版本,对于OpenHarmony平台的兼容拓展。可支持IDE或者终端使用Flutter Tools指令编译和构建OpenHarmony应用程序。

Build前需使用fvm切换Flutter版本:

fvm use custom_3.22.0

1.3 环境验证

创建新项目并检查鸿蒙支持:

flutter create my_harmony_app
cd my_harmony_app
flutter run -d harmony

如果看到鸿蒙模拟器或真机上运行着Flutter的默认启动页面,说明环境配置成功!

二、Flutter鸿蒙开发框架解析

2.1 架构概览

Flutter在HarmonyOS上的运行架构分为三层:

  1. 框架层:Dart实现的Flutter框架
  2. 引擎层:Skia渲染引擎+鸿蒙适配层
  3. 系统层:HarmonyOS的ACE(Ability Cross-platform Environment)

2.2 关键差异点

Android/iOS平台相比,Flutter在鸿蒙上需要注意:

  • 页面导航:使用HarmonyOS的Page Ability而非Activity/ViewController
  • 权限系统:鸿蒙特有的权限声明方式
  • 原生交互:通过ffi与鸿蒙的Native API通信

2.3 常用适配组件

import 'package:flutter_harmony/harmony.dart';

// 鸿蒙特色的组件
HarmonyApp(
  config: HarmonyConfig(
    abilityName: 'MainAbility', // 对应的鸿蒙Ability名称
  ),
  home: MyHomePage(),
);

三、Flutter与鸿蒙原生交互

3.1 导入原生插件

与其他端类似,ohos工程中需要GeneratedPluginRegistrant.ets文件导入Flutter生成的原生插件。

3.2 实现FlutterPlugin

定义一个类作为FlutterPlugin的实现,并在onAttachedToEngine(binding: FlutterPluginBinding): void方法中使用MethodChannel监听Flutter调用的方法。

export default class HMFlutterPlugin implements FlutterPlugin {
  private channel?: MethodChannel;
  private basicChannel?: BasicMessageChannel<Any>;
  private context?: common.UIAbilityContext;

  setContext(context: common.UIAbilityContext) {
    this.context = context;
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.xxx.app/message");
    this.channel.setMethodCallHandler({
      onMethodCall: (call: MethodCall, result: MethodResult) => {
        if (!this.context) {
          result.error("CONTEXT_NOT_INITIALIZED", "Context is not initialized", null);
        }

        const argsRec = call.args as Map<string, Object>;
        LogUtil.info(`[flutter-call-ohos]\nmethod: ${call.method}\nargs: ${JSONUtil.toJSONString(argsRec)}`);
        switch (call.method) {
          case "xxx": {
            break;
          }
          default: break;
        }
}

3.3 配置FlutterEngine

在EntryAbility.ets中实现configureFlutterEngine(flutterEngine: FlutterEngine)方法。

configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    const plugin = new HMFlutterPlugin();
    plugin.setContext(this.context);
    this.addPlugin(plugin);
  }

3.4 回调返回给Flutter端

使用result.success(res);回调执行成功的数据;使用result.error(errorCode, error.message, null);回调执行失败的错误信息。

try {
    // xxx接口功能实现
    result.success(res);
} catch (err) {
    let error = err as BusinessError;
    result.error(errorCode, error.message, null);
}

3.5 Flutter调用鸿蒙原生方法并接收回调数据

try {
    final res = await _channel.invokeMethod('method_ohos');
    return res.toString().toLowerCase() == 'true' || res.toString() == '1';
} catch (e) {
    printError('get network status error: $e');
}

总结

本篇主要介绍了Flutter开发鸿蒙应用的环境搭建和交互,我们将在下篇介绍如何借助Cursor提高开发效率让我们的开发之旅轻松而有趣。

我是郑知鱼🐳,欢迎大家讨论与指教。
如果你觉得有所收获,也请点赞👍🏻收藏⭐️关注🔍我吧~~

Dify、n8n、Coze、Fastgpt和Ragflow对比分析,如何选择? | AI工具集

mikel阅读(1384)

来源: Dify、n8n、Coze、Fastgpt和Ragflow对比分析,如何选择? | AI工具集

以下文章来源于袋鼠帝AI客栈,作者袋鼠帝

原文链接:Dify、n8n、Coze、Fastgpt、Ragflow到底该怎么选?超详细指南~

一直以来,我们分享了不少关于工作流平台、LLM应用平台的文章。

主要包含:Dify、Coze、n8n、Fastgpt、Ragflow。

但是几乎每一篇文章的评论区都有小伙伴问,xxx平台和xxx平台比怎么样,该怎么选?



确实,面对日新月异的AI技术,还有飞速发展的各种LLM平台,我们很容易患上选择困难症

但我想说的是,每个平台各有优势,需要根据自身需求,选择合适的即可。

这篇文章会从实用角度出发,通过详细的功能对比、真实的使用体验和具体的应用场景,帮助你在Dify、Coze、n8n、FastGPT和RAGFlow这五款主流平台中找到最适合自己的那一个。

无论你是AI开发者、企业用户,还是刚接触AI的新手,这篇对比分析都能为你提供清晰的选择指南。

本篇文章5000字,干货满满,建议收藏

首先我们要明确一下

LLM应用平台有:Dify、Coze、Fastgpt、ragflow

n8n比较特殊一点,它是以工作流为主的LLM平台。

LLM应用平台的核心价值在于大大降低了AI应用的开发门槛,加速从概念到产品的落地过程,并为开发者提供整合、管理和优化AI能力的工具集(插件、MCP工具等等)。

通过这些平台,咱们可以更专注于业务逻辑和用户体验创新,而非重复性的底层技术构建

先简单了解一下这几个平台的特点

n8n: 以其强大的通用工作流自动化能力著称,近年来积极拥抱AI,允许用户将LLM节点嵌入复杂的自动化流程中。

Coze (扣子): 由字节跳动推出,主打低代码/无代码的AI Agent开发,强调快速构建和部署对话式AI应用。

FastGPT: 一个开源的AI Agent构建平台,专注于知识库问答系统的构建,提供数据处理、模型调用和可视化工作流编排能力。

Dify: 开源的LLM应用开发平台,融合BaaS和LLMOps理念,旨在提供一站式的AI应用快速开发与运营能力,包括Agent工作流、RAG Pipeline等。

RAGFlow: 基于深度文档理解的开源RAG引擎,专注于解决复杂格式文档的知识提取与高质量问答。

各平台详细介绍

Dify:LLM平台中的瑞士军刀

先给Dify 3个关键词吧

#开源  #LLMOps  #生产就绪

一句话: Dify 是个23年4月开源的LLM应用开发平台,如果想整点专业的、能上生产的AI应用,还想把后端、模型运维的事全搞定?用它就OK了。

地址:dify.ai

Dify 主打“Backend-as-a-Service”和“LLMOps”,目标是让开发者和不懂技术的创新者都能轻松上手,快速鼓捣出实用的AI解决方案。

它把 RAG(检索增强生成)管道、AI工作流、监控工具、模型管理,MCP这些功能都塞进一个平台里。

确实像瑞士军刀一样,想要什么功能基本都有。

主打一个“你只管创新,其他交给Dify”。

顺便插播一下,Dify最近做了一下品牌焕新。

支持使用Docker私有化部署,运行起来的服务器最低配置是2核4G

社区活跃度也不错,目前在Github已经有98.3K Star

但是总给我一种样样通,”样样松”的感觉,好像没有特别突出的地方。

还有一个缺点就是Dify里面创建的Bot,如果想对外提供服务的话,其API没有兼容OpenAI API,就会导致外部应用想要对接会相对困难。

另外,对于只想快速实现一些小功能的用户来说是有点重了

大型企业集成的话,应该还是需要自己在上面二次开发的。

适合人群: 有一定技术的开发者、追求专业、效率的团队、需要定制化AI解决方案的企业。

Coze:LLM平台界的“乐高”

#无代码 #智能体构建 #多平台发布

先来一句话总结:Coze(扣子)是字节跳动旗下的,主打一个“人人都是AI开发者”,内置上千款工具插件,让你像搭积木一样简单地创建和发布AI Agent。

地址:coze.cn

不管你懂不懂编程,Coze都能让你把脑洞里的AI智能体快速实现。

可视化搭建、丰富的插件、知识库、工作流一应俱全,还支持一键发布到抖音、飞书、微信公众号、小程序、Discord、Telegram等各大平台。

有海外版(Coze)和国内版(扣子)

Coze是闭源的,但它的功能比Dify更丰富。

我比较中意的有代码插件,零代码小程序、web页面,定时任务等功能。

适合人群: AI入门用户、产品经理、运营人员、想快速搭建个性化AI Agent的创作者、以及预算、技术有限的个人和小型团队等。

FastGPT:知识库小能手

#开源 #RAG知识库

一句话: FastGPT是个免费开源的AI知识库平台,让AI根据你的私有数据精准回答问题,是你的第二个”大脑”

地址:tryfastgpt.ai

FastGPT 提供数据处理、模型调用、RAG检索和可视化AI工作流,MCP一条龙服务。

你可以导入各种格式的文档(Word、PDF、网页链接等),用最短的时间打造出特定领域的AI问答助手。

Fastgpt的RAG效果是相当不错的,它能够简单、快速构建一个高质量知识库,我之前用它做我的微信AI助理产品的客服,挺棒的。

一些企业级客户我也是帮助他们用fastgpt来构建知识库,轻量,简单,好用。

它还提供与OpenAI兼容的API,可以非常方便的把它集成到现有的其他应用里。

支持Docker私有化部署,最好用2核4G的服务器来跑。

相比Dify来说,优点在于更轻量、知识库效果更好、API兼容OpenAI API,更方便集成到其他应用。

但是在功能的丰富度、和一些体验上是不如Dify的,社区也不如Dify活跃,目前在Github是24.2K Star

但是如果你是想快速打造知识库为主的AI应用,我都推荐先试试Fastgpt。

适合人群:需要构建企业内部知识库、AI客服、的开发者或企业,以及对RAG技术感兴趣的AI爱好者。

RAGFlow:知识库专家

标签:#开源  #RAG引擎  #深度文档理解

一句话: RAGFlow 是个开源的RAG引擎

地址:ragflow.io

RAGFlow的核心竞争力在于“深度文档理解”,比如能从合同里提取条款、总结长篇报告。以及支持10多种类型的数据预处理,不管是在RAG的知识库构建,还是问答阶段都有非常丰富的参数去调整。还支持知识图谱功能。

RAG的颗粒度细,知识库效果上限很高。

如果说Fastgpt是知识库小能手,那么Ragflow就是知识库专家(从它的名字里面就能看出来)。

支持Docker部署,但是比较重,需要至少4核16G配置的服务器才能流畅使用。目前在Github有53.1K Star

适合对答案准确性和可追溯性有高要求的行业(如法律、医疗、金融)、需要处理大量复杂文档的企业、以及RAG技术的研究者和开发者。

n8n:最强开源工作流平台

#开源 #工作流自动化 #低代码

一句话总结: n8n 是一个开源的低代码工作流自动化工具,专注于将各种应用和服务连接起来,形成自动化的业务流程。

地址:n8n.io

n8n 的核心是通过可视化节点(Node)来构建自动化流程,同时每个节点所提供的配置参数丰富,定制化程度高。

它提供了超过400个预置集成,覆盖各类SaaS服务和数据库。既可以通过简单的拖拽操作构建工作流,也可以通过js或Python代码进行更复杂的定制。

它包含Agent节点,能够快速接入各种大模型,同样支持了MCP。

在实际业务中,n8n能极大提高工作效率

比如Delivery Hero使用n8n每月节省了200多小时的工作时间

https://n8n.io/case-studies/delivery-hero/

StepStone也靠它运行了200多个关键任务流程

https://n8n.io/case-studies/stepstone/

虽然n8n有很多优点,但毕竟是工作流平台,主打工作流。在LLM这块丝滑程度还是比不上其他专业的LLM应用平台,LLM这块该有的也都有,就是用起来感觉更麻烦一些。

同时上手难度也是这些个平台里面最大的了,需要一些逻辑思维,和前期的学习成本,但上手之后效率将会极大的提升。

也支持Docker私有化部署,完全不吃配置,1核1G的服务器应该都能跑。

适合人群: 需要高度定制自动化流程的团队、开发者、以及追求效率最大化的中小企业。

5大平台功能横向对比分析

为了帮助大家更清晰地了解这五个平台的区别和优势,我整理了一张详细的对比表,从多个维度进行客观分析:

其中Coze目前不是免费的了

平台选择实用建议:

从我的实际体验来看,如果你是刚接触AI应用开发,希望快速看到成果,Coze是最容易上手的选择。

如果你的工作或者业务涉及多个系统和服务之间的数据流转,需要自动化处理,n8n的强大自动化工作流会为你节省超多时间。

想搭建企业内部智能知识库或者Q&A系统,FastGPT、Ragflow可以优先考虑,它们在RAG方面都比较强,FastGPT更轻量、Ragflow更重(但上限更高)

对于有长期规划、需要构建可扩展企业级AI应用的团队,Dify的完整生态系统和企业级功能是好的选择。

为了更直观,基于我的实际使用体验和各平台特点,我整理了下面这张”用户适用性评分图”(满分5分),希望可以帮助大家快速定位自己的需求对应哪个平台:

还有下面这个图,也可以参考参考

选型考量要素

在最终做出选择之前,建议大家考虑以下几个关键要素,它们会直接影响你的使用体验和长期效果:

预算:

开源平台可以免费自托管使用,但需要考虑服务器和维护成本;云服务则是按使用量或订阅付费,前期成本低但长期可能更高。根据你的资源状况和业务规模选择合适的方案。

技术能力:

评估你或团队的技术背景、学习意愿。如果技术实力有限,选择像Coze这样的无代码平台会更合适;如果有较强的技术团队,就可以考虑Dify或n8n等提供更多定制能力的平台。

部署:

考虑是否需要数据本地私有化。自托管方案提供更高的数据安全性和隐私保护,但需要更多的技术支持;云服务则提供快速部署和低维护成本,但可能存在数据安全风险。

核心功能需求:

详细列出你最核心的需求,看哪个平台能够最好地满足这些关键点。比如如果RAG能力是最重要的,那么FastGPT或RAGFlow可能比Coze更合适;如果需要复杂工作流,n8n或Dify会是更好的选择。

平台可持续性:

评估平台的更新频率、社区活跃度和长期支持情况。开源项目要看社区活跃度和贡献者数量;商业产品看公司背景和市场表现。这直接关系到你选择的平台能否长期发展并跟上技术变化。

数据安全与合规方面:

特别是对企业用户来说,数据隐私保护、访问控制和合规性至关重要。开源自托管平台在数据安全方面更有优势,因为数据可以完全保留在自己的环境中;商业平台则需要仔细阅读其隐私政策和数据处理协议等等。

通过认真评估上面这些因素,结合前面的对比分析,相信大家应该能够找到最符合自身需求的LLM应用平台了吧。

「最后」

经过这次全方位的对比分析

希望大家对Dify、Coze、n8n、FastGPT和RAGFlow这五个平台有了更清晰的认识。

没有绝对完美的工具,只有最适合当前需求和发展阶段的选择。

我的建议是:

如果可能的话,可以先从使用门槛较低的平台(如:Coze)开始尝试,熟悉LLM应用开发的基本概念和流程;

后面需求越来越复杂,技术也有一定提升之后,再逐步过渡到更专业的平台(如Dify或n8n)。

AI Agent是一个快速发展的领域,各平台也在飞速进化和完善。

希望这篇分析能为大家提供一个基础的参考框架

帮助大家在这个充满机遇和挑战的AI时代找到适合自己的工具和方向。

Agent Neo - Flowith推出的AI Agent,能持续不断地执行任务 | AI工具集

mikel阅读(396)

来源: Agent Neo – Flowith推出的AI Agent,能持续不断地执行任务 | AI工具集

Agent Neo是什么

Agent Neo是Flowith推出的创新 AI Agent。Agent Neo具备无限步骤,无限上下文,无限工具的核心能力,能持续执行复杂任务、处理海量信息和调用多种大模型与工具。Agent Neo 结合 Flowith 的知识库功能,用户上传知识库,能快速构建数字分身或生成高质量内容。Flowith 提供Agent社区,用户能将自己的工作流做成 Recipe 分享到社区里。Agent Neo交互界面美观且富有创意,提供透明化的工作流程,适用于复杂任务自动化、知识管理与分享等场景。Agent Neo目前为邀请制,需激活码使用。

Agent Neo

Agent Neo的主要功能

  • 无限步骤(Unlimited Steps):Agent Neo能进行无限深度的推理,支持持续不断地工作,能执行需要长时间运行的复杂任务。
  • 多步骤优化(Multi-step Refinement):基于多个步骤优化网页,提供最佳的结果。
  • 24/7云端执行(24/7 Cloud Execution):支持全天候云端执行任务,用户的设备处于休眠状态,任务也能不间断地运行。
  • 无限输出长度(Unlimited Output Length):支持生成任意长度的响应,不会出现内容截断的情况。
  • 超智能重新规划(Super-Intelligent Re-Planning):在执行过程中,根据最终目标智能地调整计划。

Agent Neo的官方示例

  • Prompt:Please generate a detailed ‘The Hunger Games’ setting collection, and draw rich and detailed illustrations based on the content of the book. The final presentation form is an immersive experience website with rich animation effects. Please ensure that all key content has correct diagrams. You need to generate relevant content in batches to ensure that each major element has a relevant visual image and picture.(请生成详细的《饥饿游戏》背景集,根据书中的内容绘制丰富而详细的插图。最终呈现形式是一个具有丰富动画效果的沉浸式体验网站。请确保所有关键内容都有正确的图表。你需要批量生成相关内容,确保每个主要元素都有相关的视觉图像和图片。”)

Agent Neo

  • Prompt:introduce flowith 2.0.(介绍 Flowith 2.0)

Agent Neo

Agent Neo的性能表现

Agent Neo 在通用 AI Agent 能力测试 GAIA 中表现出色,刷新所有难度级别的最新最佳性能评分。

Agent Neo

如何使用Agent Neo

  • 获取邀请码:Agent Neo目前为邀请制,需获取激活码后使用。
  • 注册并登录:访问 Flowith 官方网站,完成注册,用邀请码登录。
  • 进入 Agent Neo 模式:在 Flowith 平台中找到打开 Agent Mode,调用 Agent Neo。
  • 设置任务
    • 输入任务描述:告诉 Agent Neo 想要完成的任务,例如生成报告、创建网页、续写故事等。
    • 选择或上传知识库:如果任务需要特定的知识背景,选择已有的知识库或上传相关文档,让 Agent Neo 从中获取信息。
  • 任务规划:Agent Neo 自动规划任务的工作流,包括信息搜集、内容生成、工具调用等。
  • 实时交互:在任务执行过程中,Agent Neo 根据需要与用户交互,例如确认信息、获取反馈或调整任务方向。
  • 查看结果:任务完成后,将结果呈现给用户,例如生成的网页、文档或报告。
  • 修改结果:用户根据需要对生成的内容进行修改或优化,Agent Neo 支持用户直接在可视化界面或代码层面进行调整。
  • 保存工作流:用户将完成的任务保存为工作流(Recipe),方便后续复用或分享给其他用户。
  • 社区分享:将工作流发布到 Flowith 的 Agent 社区,与其他用户共享经验和创意

Agent Neo的应用场景

  • 自动化任务执行:自动执行重复性任务,如数据收集、报告生成和监控任务,提高效率和准确性。
  • 复杂项目管理:基于无限步骤和深度推理来规划和管理项目,直至完成。
  • 内容创作与编辑:续写故事、生成文章或优化网页内容,支持创意写作和多步骤内容精炼。
  • 知识库构建与应用:用户上传和分析知识库,提高任务执行的精准性和效率。
  • 数字分身创建:创建具有专业知识和历史记忆的数字分身,模拟对话或自动化客户服务。

PHP RSA2加密和解密以及接口签名和验签_php rsa和rsa2加密算法-CSDN博客

mikel阅读(241)

来源: PHP RSA2加密和解密以及接口签名和验签_php rsa和rsa2加密算法-CSDN博客

上述代码中,为了演示我给$sign$data两个变量赋值,实际上这两项值是从客户端传递过来的,一般是post提交过来的,这里代码中不做演示了,大家可以自己练习。

运行上述代码后,成功输出“ok”,即验签成功。

以上。

 

本文来源于:helloweba.net

原文链接:https://www.helloweba.net/php/630.html

dify+MCP多应用,构建灵活的AI应用生态系统 - 肖祥 - 博客园

mikel阅读(269)

来源: dify+MCP多应用,构建灵活的AI应用生态系统 – 肖祥 – 博客园

一、概述

前面几篇文章写很多MCP应用,基本上一个dify工作流使用一个MCP应用。

那么一个dify工作流,同时使用多个MCP应用,是否可以呢?答案是可以的。

 

先来看一下效果图

说明:

这里使用了问题分类器,用来判断用户的问题,应该调用哪个MCP应用

AGENT1~4,分别对应一个MCP应用,例如:public-ip-mcp-server,mySQL8-mcp-server,desensitization-mcp-server,searxng-mcp-server

针对mySQL查询输出的内容,会进行脱敏处理。

二、问题分类器

定义

通过定义分类描述,问题分类器能够根据用户输入,使用 LLM 推理与之相匹配的分类并输出分类结果,向下游节点提供更加精确的信息。

场景

常见的使用情景包括客服对话意图分类、产品评价分类、邮件批量分类等。

在一个典型的产品客服问答场景中,问题分类器可以作为知识库检索的前置步骤,对用户输入问题意图进行分类处理,分类后导向下游不同的知识库查询相关的内容,以精确回复用户的问题。

设置

对于比较精确的条件,一般使用条件分支。但是对于我这种场景,条件比较模糊,所以需要使用问题分类器

 

这里定义了3个分类:

公网ip相关问题
mysql 数据库相关查询,涉及学生、教师、成绩、班级、课程等
其他问题

效果如下:

说明:

公网ip相关问题,会直接调用MCP应用public-ip-mcp-server

mysql相关问题,会调用MCP应用mysql8-mcp-server

其他问题,会调用MCP应用searxng-mcp-server,这个是一个联网搜索引擎,你可以理解为百度,想搜什么都可以。

三、环境说明

dify版本

这里使用的是最新版本1.4.0,如果你的版本没有这么高,1.3.0以上版本也可以。

mcp插件

确保已经安装了以下插件:

Agent 策略(支持 MCP 工具)

MCP SSE / StreamableHTTP

确保插件版本,已经升级到最新版本

mcp应用

这里的所有MCP应用,统一使用Streamable HTTP模式,全部部署在k8s里面。

当然,使用docker运行也是可以的。

mcp插件设置

点击插件MCP SSE / StreamableHTTP,输入MCP 服务配置

完整内容如下:

复制代码
{
    "mysql8-mcp-server": {
        "transport": "streamable_http",
        "url": "http://mysql8-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    },
    "desensitization-mcp-server": {
        "transport": "streamable_http",
        "url": "http://desensitization-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    },
    "public-ip-mcp-server": {
        "transport": "streamable_http",
        "url": "http://public-ip-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    },
    "searxng-mcp-server": {
        "transport": "streamable_http",
        "url": "http://searxng-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    }
}
复制代码

注意:这里的url使用的是k8s内部地址,如果使用的是docker方式运行,请根据实际情况修改。

四、public-ip-mcp-server设置

public-ip-mcp-server核心代码如下:

server.py

复制代码
from fastmcp import FastMCP
import json
import requests

mcp = FastMCP("public-ip-address")


@mcp.tool()
def get_public_ip_address() -> str:
    """
    获取公网ip地址
    返回:
        str: 当前网络的公网ip地址
    """
    response = requests.get("http://ip-api.com/json")
    content = json.loads(response.text)
    return content["query"]


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=9000, path="/mcp")
复制代码

Agent配置

Agent 1详细配置如下:

MCP服务配置

复制代码
{
  "public-ip-mcp-server": {
        "transport": "streamable_http",
        "url": "http://public-ip-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    }
}
复制代码

指令

使用MCP工具,获取服务器公网ip

 

最后直接回复,注意选择变量Agent1 text

 

五、mysql8-mcp-server设置

核心代码

mysql8-mcp-server核心代码如下:

server.py

复制代码
from fastmcp import FastMCP
from mysql.connector import connect, Error
import os

mcp = FastMCP("operateMysql")


def get_db_config():
    """从环境变量获取数据库配置信息

    返回:
        dict: 包含数据库连接所需的配置信息
        - host: 数据库主机地址
        - port: 数据库端口
        - user: 数据库用户名
        - password: 数据库密码
        - database: 数据库名称

    异常:
        ValueError: 当必需的配置信息缺失时抛出
    """

    config = {
        "host": os.getenv("MYSQL_HOST", "localhost"),
        "port": int(os.getenv("MYSQL_PORT", "3306")),
        "user": os.getenv("MYSQL_USER"),
        "password": os.getenv("MYSQL_PASSWORD"),
        "database": os.getenv("MYSQL_DATABASE"),
    }
    print(config)
    if not all(
        [
            config["host"],
            config["port"],
            config["user"],
            config["password"],
            config["database"],
        ]
    ):
        raise ValueError("缺少必需的数据库配置")

    return config


@mcp.tool()
def execute_sql(query: str) -> list:
    """执行SQL查询语句

    参数:
        query (str): 要执行的SQL语句,支持多条语句以分号分隔

    返回:
        list: 包含查询结果的TextContent列表
        - 对于SELECT查询:返回CSV格式的结果,包含列名和数据
        - 对于SHOW TABLES:返回数据库中的所有表名
        - 对于其他查询:返回执行状态和影响行数
        - 多条语句的结果以"---"分隔

    异常:
        Error: 当数据库连接或查询执行失败时抛出
    """
    config = get_db_config()
    try:
        with connect(**config) as conn:
            with conn.cursor() as cursor:
                statements = [stmt.strip() for stmt in query.split(";") if stmt.strip()]
                results = []

                for statement in statements:
                    try:
                        cursor.execute(statement)

                        # 检查语句是否返回了结果集 (SELECT, SHOW, EXPLAIN, etc.)
                        if cursor.description:
                            columns = [desc[0] for desc in cursor.description]
                            rows = cursor.fetchall()

                            # 将每一行的数据转换为字符串,特殊处理None值
                            formatted_rows = []
                            for row in rows:
                                formatted_row = [
                                    "NULL" if value is None else str(value)
                                    for value in row
                                ]
                                formatted_rows.append(",".join(formatted_row))

                            # 将列名和数据合并为CSV格式
                            results.append(
                                "\n".join([",".join(columns)] + formatted_rows)
                            )

                        # 如果语句没有返回结果集 (INSERT, UPDATE, DELETE, etc.)
                        else:
                            conn.commit()  # 只有在非查询语句时才提交
                            results.append(f"查询执行成功。影响行数: {cursor.rowcount}")

                    except Error as stmt_error:
                        # 单条语句执行出错时,记录错误并继续执行
                        results.append(
                            f"执行语句 '{statement}' 出错: {str(stmt_error)}"
                        )
                        # 可以在这里选择是否继续执行后续语句,目前是继续

                return ["\n---\n".join(results)]

    except Error as e:
        print(f"执行SQL '{query}' 时出错: {e}")
        return [f"执行查询时出错: {str(e)}"]


@mcp.tool()
def get_table_name(text: str) -> list:
    """根据表的中文注释搜索数据库中的表名

    参数:
        text (str): 要搜索的表中文注释关键词

    返回:
        list: 包含查询结果的TextContent列表
        - 返回匹配的表名、数据库名和表注释信息
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    sql = "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COMMENT "
    sql += f"FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{config['database']}' AND TABLE_COMMENT LIKE '%{text}%';"
    return execute_sql(sql)


@mcp.tool()
def get_table_desc(text: str) -> list:
    """获取指定表的字段结构信息

    参数:
        text (str): 要查询的表名,多个表名以逗号分隔

    返回:
        list: 包含查询结果的列表
        - 返回表的字段名、字段注释等信息
        - 结果按表名和字段顺序排序
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    # 将输入的表名按逗号分割成列表
    table_names = [name.strip() for name in text.split(",")]
    # 构建IN条件
    table_condition = "','".join(table_names)
    sql = "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_COMMENT "
    sql += (
        f"FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{config['database']}' "
    )
    sql += f"AND TABLE_NAME IN ('{table_condition}') ORDER BY TABLE_NAME, ORDINAL_POSITION;"
    return execute_sql(sql)


@mcp.tool()
def get_lock_tables() -> list:
    """
    获取当前mysql服务器InnoDB 的行级锁

    返回:
        list: 包含查询结果的TextContent列表
    """
    sql = """SELECT
    p2.`HOST` AS 被阻塞方host,
    p2.`USER` AS 被阻塞方用户,
    r.trx_id AS 被阻塞方事务id,
    r.trx_mysql_thread_id AS 被阻塞方线程号,
    TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP) AS 等待时间,
    r.trx_query AS 被阻塞的查询,
    l.OBJECT_NAME AS 阻塞方锁住的表,
    m.LOCK_MODE AS 被阻塞方的锁模式,
    m.LOCK_TYPE AS '被阻塞方的锁类型(表锁还是行锁)',
    m.INDEX_NAME AS 被阻塞方锁住的索引,
    m.OBJECT_SCHEMA AS 被阻塞方锁对象的数据库名,
    m.OBJECT_NAME AS 被阻塞方锁对象的表名,
    m.LOCK_DATA AS 被阻塞方事务锁定记录的主键值,
    p.`HOST` AS 阻塞方主机,
    p.`USER` AS 阻塞方用户,
    b.trx_id AS 阻塞方事务id,
    b.trx_mysql_thread_id AS 阻塞方线程号,
    b.trx_query AS 阻塞方查询,
    l.LOCK_MODE AS 阻塞方的锁模式,
    l.LOCK_TYPE AS '阻塞方的锁类型(表锁还是行锁)',
    l.INDEX_NAME AS 阻塞方锁住的索引,
    l.OBJECT_SCHEMA AS 阻塞方锁对象的数据库名,
    l.OBJECT_NAME AS 阻塞方锁对象的表名,
    l.LOCK_DATA AS 阻塞方事务锁定记录的主键值,
    IF(p.COMMAND = 'Sleep', CONCAT(p.TIME, ' 秒'), 0) AS 阻塞方事务空闲的时间
    FROM performance_schema.data_lock_waits w
    INNER JOIN performance_schema.data_locks l ON w.BLOCKING_ENGINE_LOCK_ID = l.ENGINE_LOCK_ID
    INNER JOIN performance_schema.data_locks m ON w.REQUESTING_ENGINE_LOCK_ID = m.ENGINE_LOCK_ID
    INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.PROCESSLIST p ON p.ID = b.trx_mysql_thread_id
    INNER JOIN information_schema.PROCESSLIST p2 ON p2.ID = r.trx_mysql_thread_id
    ORDER BY 等待时间 DESC;"""

    return execute_sql(sql)


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=9000, path="/mcp")
复制代码

Agent配置

Agent 2详细配置如下:

 

MCP服务配置

复制代码
{
    "mysql8-mcp-server": {
        "transport": "streamable_http",
        "url": "http://mysql8-mcp-server-svc.mcp:9000/mcp/",
        "timeout": 60
    }
}
复制代码

指令

 View Code

 

六、desensitization-mcp-server设置

核心代码

desensitization-mcp-server核心代码如下:

server.py

 View Code

Agent配置

Agent 4详细配置如下:

注意:查询要选择变量Agent 2 text

 

MCP服务配置

复制代码
{ 
  "desensitization-mcp-server": {
        "transport": "streamable_http",
        "url": "http://desensitization-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    }
}
复制代码

指令

使用MCP工具,对文本进行脱敏处理

 

最后直接回复,注意选择变量Agent4 text

 

七、searxng-mcp-server设置

核心代码

searxng-mcp-server核心代码如下:

server.py

复制代码
from fastmcp import FastMCP
import requests
import os

mcp = FastMCP("searxng")


@mcp.tool()
def search(query: str) -> str:
    """
    搜索关键字,调用searxng的API接口
    参数:
        query (str): 要搜索的关键词
    返回:
        str: 查询结果
    """
    api_server = os.getenv("API_SERVER", None)
    if not api_server:
        print("缺少必需的API_SERVER配置")
        raise ValueError("缺少必需的API_SERVER配置")

    # API URL
    url = "%s/search?q=%s&format=json" % (api_server, query)
    print(url)

    try:
        # 发送GET请求
        response = requests.get(url)

        # 检查请求是否成功
        if response.status_code == 200:
            # 将响应内容解析为JSON
            data = response.json()
            # print("JSON内容:")
            # print(data,type(data))
            result_list = []
            for i in data["results"]:
                # print(i["content"])
                result_list.append(i["content"])
            content = "\n".join(result_list)
            # print(content)
            return content
        else:
            print(f"请求失败,状态码: {response.status_code}")
            return False

    except requests.exceptions.RequestException as e:
        print(f"请求过程中发生错误: {e}")
        return False


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=9000, path="/mcp")
复制代码

Agent配置

Agent 3详细配置如下:

MCP服务配置

复制代码
{
  "searxng-mcp-server": {
        "transport": "streamable_http",
        "url": "http://searxng-mcp-server-svc.mcp:9000/mcp/",
        "headers": {},
        "timeout": 60
    }
}
复制代码

指令

复制代码
## 技能
### 技能1:使用MCP工具进行联网搜索,获取到的相关内容进行总结分析
## 限制
- 如果没有相关内容,再进行联网搜索
- 你的回答应严格针对分析任务。使用结构化语言,逐步思考
- 使用的语言应和用户提问的语言相同
- 搜索的关键词必须和用户提问的内容一致
复制代码

 

最后直接回复,注意选择变量Agent3 text

八、dify测试

点击右上角的预览按钮

公网ip多少

注意:这里可以看到绿色的连接线条,可以清晰的看到工作流的走向,它确实是按照我预期的方向在走。

 

李华的老师,查询一下个人详细信息

可以看到通过问题分类器,分别走向AGENT 2,AGENT 4,最终得到的答案,是进行了脱敏处理。

上海今天天气如何

这里直接联网搜索答案了

 

PHP发送/接收JSON请求_php header location 能发送 json吗-CSDN博客

mikel阅读(215)

来源: PHP发送/接收JSON请求_php header location 能发送 json吗-CSDN博客

现在API模式,经常使用headers里传JSON数据,作为POST请求传递参数的方式,在参数量较多时POST JSON要比POST FormData便于开发和测试

PHP发送JSON POST
$url = “http://example.com/request/post/json”;
$data = json_encode([“foo” => “bar”]);
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(“Content-type: application/json”));
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_exec($curl);
curl_close($curl);
或者

/**
* PHP发送Json对象数据
* 例子 list($returnCode, $returnContent) = http_post_json($url, $jsonStr);
* @param $url 请求url
* @param $jsonStr 发送的json字符串
* @return array
*/
function http_post_json($url, $jsonStr)
{

$ch = curl_init();
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonStr);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
‘Content-Type: application/json; charset=utf-8’,
‘Content-Length: ‘ . strlen($jsonStr)
)
);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

return array($httpCode, $response);

}
PHP接受JSON POST
$data = json_decode(file_get_contents(‘php://input’), true);
php://input 是个可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input 来代替

$GLOBALS[‘HTTP_RAW_POST_DATA’]
,因为它不依赖于特定的 php.ini 指令。 而且,这样的情况下

$GLOBALS[‘HTTP_RAW_POST_DATA’]
默认没有填充, 比激活 always_populate_raw_post_data 潜在需要更少的内存。 enctype=”multipart/form-data” 的时候 php://input 是无效的。

在 PHP 5.6 之前 php://input 打开的数据流只能读取一次; 数据流不支持 seek 操作。 不过,依赖于 SAPI 的实现,请求体数据被保存的时候, 它可以打开另一个 php://input 数据流并重新读取。 通常情况下,这种情况只是针对 POST 请求,而不是其他请求方式,比如 PUT 或者 PROPFIND。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/yule117737767/article/details/119138023