本教程适用于拥有自己的域名并且通过Halo部署好了自己的个人博客,想实现在线LobeChat服务的场景

LobeChat官方给了两种部署方式:

  • 客户端数据库的模式,该模式的优势是一行指令 / 一个按钮即可快捷完成部署,便于你快速上手与体验 LobeChat,但是数据均保留在用户本地,不会跨多端同步,也不支持文件上传、知识库等进阶功能。

  • 服务端数据库的模式,部署稍复杂,支持多端用户,三方认证,知识库等,比客户端数据库模式功能强大得多

按照官方文档的要求:

一般来讲,想要完整的运行 LobeChat 数据库版本,你需要至少拥有如下四个服务

  • LobeChat 数据库版本自身

  • 带有 PGVector 插件的 PostgreSQL 数据库

  • 支持 S3 协议的对象存储服务

  • 受 LobeChat 支持的 SSO 登录鉴权服务

开始部署前,喜欢此项目可以去点个星

https://github.com/lobehub/lobe-chat

安装 Casdoor 身份验证服务

  1. 从1panel商店中搜索casdoor,默认参数安装即可image-xvzv.png

  2. 在网站中用OpenResty创建一个反向代理,代理地址填为本地8000端口对应casdoor服务

  3. 通过反代域名打开casdoor管理平台,默认账号admin密码123,登录后记得修改下管理员的默认密码。点击用户管理组织,创建一个新的组织lobe-chat。密码类型选择bcrypt,其他参数按照个人喜好来。默认应用这里先空着,我们稍后创建应用后才能关联。

  4. 打开身份认证应用,新建一个应用为lobe-chat用于认证。组织选择刚才创建的lobe-chat,记录好客户端ID和客户端密钥。重定向url格式为:http(s)://你的lobechat域名/api/auth/callback/casdoor,关闭注册功能。由于我做了邮箱认证所以登录方式多选了一个验证码,如果只需要用户密码的话可以删除。关于邮箱认证可以参考casdoor的文档。

    https://casdoor.org/zh/docs/provider/email/overview

    另外lobechat提供一些还有一些不必需但是可以提高用户体验的字段,详情见:

    https://lobehub.com/zh/docs/self-hosting/advanced/auth/next-auth/casdoor

注意下此处我们还没部署好minio,还有webhook,用户同步还没做。

安装MinIO

  1. 从1panel商店中搜索,安装默认参数安装。安装好后参数中会有用户密码,9001端口是webui的,9000是api的。同样的创建两个反代,webui配置完毕后就可以关闭了避免暴露在公网。

  2. 创建一个桶名字为lobechat,权限改为public。

  3. 然后创建access key,创建好后会自动下载一个json文件,保存好。

安装带有 PGVector 插件的 PostgreSQL 数据库

  1. 在1panel容器界面,点击镜像,拉取镜像pgvector/pgvector:pg17。如果拉取失败配置下docker镜像加速https://docker.1panel.live,此操作会重启docker服务。image-QCVH.pngimage-ZzPw.png

  2. 启动容器,按照如下设置,ip可以不填会自动分配一个空闲ip。挂载目录:本机/data/1panel/apps/postgresql/my-postgre/data,容器:/var/lib/postgresql/data。我的1panel安装目录在/data,请自行修改。环境变量:POSTGRES_PASSWORD=自行定义

    PGDATA=/var/lib/postgresql/data

  3. 添加postgresql,并创建lobechat数据库。

安装LobeChat 数据库版本

  1. 1panel容器界面,点击镜像,拉取镜像lobehub/lobe-chat-database:latest。

  2. 启动容器,部分参数如下。

  3. 重点说下环境变量,给大家一个模板参考:

    #lobechat的域名,http(s)根据自己实际修改
    APP_URL=https://chat.xxx.cn
    #加密秘钥可以不变
    KEY_VAULTS_SECRET='dvy9O/6gMcy8k7Jnfbb1mI8he03efDQ6m0iknR8XppQ='
    #postgresql数据库连接信息,postgres://用户名:密码@数据库地址(此处用容器名解析):端口号/数据库名称
    DATABASE_URL=postgres://mylobehub:xxx@postgres:5432/mylobehub
    #加密秘钥可以不变
    NEXT_AUTH_SECRET=3904039cd41ea1bdf6c93db0db96e250
    #sso认证类型指定casdoor
    NEXT_AUTH_SSO_PROVIDERS=casdoor
    #在配置casdoor中获取的客户端id和客户端秘钥
    AUTH_CASDOOR_ID=xxx
    AUTH_CASDOOR_SECRET=xxx
    #在配置casdoor中创建的反代域名
    AUTH_CASDOOR_ISSUER=https://auth.xxx.cn
    #app_url拼接/api/auth生成
    NEXTAUTH_URL=https://chat.xxx.cn/api/auth
    #casdoor上的webhook用于更新用户信息的秘钥
    CASDOOR_WEBHOOK_SECRET=xxxx
    #开启认证debug
    NEXT_AUTH_DEBUG=1
    #在配置minio时获取的access key
    S3_ACCESS_KEY_ID=xxx
    S3_SECRET_ACCESS_KEY=xxx
    #在配置minio中创建的反代域名(API接口)
    S3_ENDPOINT=https://s3.xxx.cn
    S3_BUCKET=lobechat
    #同api端点
    S3_PUBLIC_DOMAIN=https://s3.xxxx.cn
    #必须,否则文件上传时请求地址会变为https://S3_BUCKET.s3.xxx.cn
    S3_ENABLE_PATH_STYLE=1
    #上传的图片存储为base64格式
    LLM_VISION_IMAGE_USE_BASE64=1
    #lobechat有联网搜索,可以通过部署searxng容器实现,这里是指定searxng容器
    SEARXNG_URL=http://searxng:8080
  4. 查看容器日志,如下就启动成功了。

    [Database] Start to migration...
    ✅ database migration pass.
    -------------------------------------
      ▲ Next.js 14.x.x
      - Local:        https://localhost:3210
      - Network:      http://0.0.0.0:3210
      ✓ Starting...
      ✓ Ready in 95ms

到此lobechat服务端数据库版本就部署完毕了,通过OpenResty反向代理配置,将lobechat、minio、casdoor内部端口和域名进行绑定就可以使用了。

小知识:可以申请一个域名后,通过泛域名*.domain解析到自己服务器。后续新建网站只需要将*替换,比如lobechat.domain.cn、minio.domain.cn等,同理ssl证书申请也是一样的。

还可以在halo中新建一个页面,将lobechat的链接放入网页中实现在线工具。参考:

https://www.yuque.com/liuzhihangs/halo-theme-hao/wahnl65yq0dc6ybk

效果如下:

后面扩展下,由于lobechat、casdoor、halo分别是三个不同的数据库,需要对数据库进行同步。

配置数据库触发器

我们将halo的数据库作为源数据库进行同步。

  1. 在halo数据库新建一张表users

    CREATE TABLE users (
        id CHAR(36) DEFAULT (UUID()) PRIMARY KEY COMMENT 'UUID 主键',
        name VARCHAR(255) NOT NULL UNIQUE COMMENT '用户名',
        email VARCHAR(255) NOT NULL COMMENT '邮箱',
        password VARCHAR(255) NOT NULL COMMENT '密码哈希',
        display_name VARCHAR(255) NULL COMMENT '显示名称',
        avatar VARCHAR(255) NULL COMMENT '头像路径',
        created_time DATETIME DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
  2. 创建三个触发器,插入、更新、删除,用于从源表extensions表中同步用户到users。

    BEGIN
      -- 仅处理有效用户数据
      IF 
        NEW.name LIKE '/registry/users/%' 
        AND JSON_VALID(CONVERT(NEW.data USING latin1)) 
        AND JSON_UNQUOTE(JSON_EXTRACT(CONVERT(NEW.data USING latin1), '$.spec.password')) IS NOT NULL 
      THEN
        -- 解析 JSON 数据
        SET @data_latin1 = CONVERT(NEW.data USING latin1);
        SET @name = JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.metadata.name'));
        SET @email = JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.spec.email'));
        -- 移除密码中的 {bcrypt} 前缀(假设前缀固定为 8 字符)
        SET @password = SUBSTRING(
          JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.spec.password')),
          9  -- 从第 9 个字符开始截取(跳过 '{bcrypt}')
        );
        SET @display_name = JSON_UNQUOTE(JSON_EXTRACT(CONVERT(NEW.data USING utf8mb4), '$.spec.displayName'));
        SET @avatar = JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.spec.avatar'));
        SET @created_time = CONCAT(
          SUBSTRING_INDEX(
            JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.metadata.creationTimestamp')),
            '.', 1
          ),
          '+08:00'
        );
    
        -- 插入或更新 users 表
        INSERT INTO users (name, email, password, display_name, avatar, created_time)
        VALUES (@name, @email, @password, @display_name, @avatar, @created_time)
        ON DUPLICATE KEY UPDATE
          email = VALUES(email),
          password = VALUES(password),
          display_name = VALUES(display_name),
          avatar = VALUES(avatar),
          created_time = VALUES(created_time);
      END IF;
    END

    BEGIN
      -- 仅处理路径为 '/registry/users/%' 的有效记录
      IF 
        NEW.name LIKE '/registry/users/%' 
        AND JSON_VALID(CONVERT(NEW.data USING latin1)) 
        AND JSON_UNQUOTE(JSON_EXTRACT(CONVERT(NEW.data USING latin1), '$.spec.password')) IS NOT NULL 
      THEN
        -- 解析旧数据(用于处理主键变更)
        SET @old_data_latin1 = CONVERT(OLD.data USING latin1);
        SET @old_name = JSON_UNQUOTE(JSON_EXTRACT(@old_data_latin1, '$.metadata.name'));
        
        -- 解析新数据(移除密码前缀)
        SET @new_data_latin1 = CONVERT(NEW.data USING latin1);
        SET @new_name = JSON_UNQUOTE(JSON_EXTRACT(@new_data_latin1, '$.metadata.name'));
        SET @new_email = JSON_UNQUOTE(JSON_EXTRACT(@new_data_latin1, '$.spec.email'));
        -- 关键修改:移除 {bcrypt} 前缀
        SET @new_password = SUBSTRING(
          JSON_UNQUOTE(JSON_EXTRACT(@new_data_latin1, '$.spec.password')),
          9  -- 从第9个字符开始截取
        );
        SET @new_display_name = JSON_UNQUOTE(JSON_EXTRACT(CONVERT(NEW.data USING utf8mb4), '$.spec.displayName'));
        SET @new_avatar = JSON_UNQUOTE(JSON_EXTRACT(@new_data_latin1, '$.spec.avatar'));
        SET @new_created_time = CONCAT(
          SUBSTRING_INDEX(
            JSON_UNQUOTE(JSON_EXTRACT(@new_data_latin1, '$.metadata.creationTimestamp')),
            '.', 1
          ),
          '+08:00'
        );
    
        -- 如果用户名变更,先删除旧记录
        IF @old_name != @new_name THEN
          DELETE FROM users WHERE name = @old_name;
        END IF;
    
        -- 插入或更新新记录(已处理密码前缀)
        INSERT INTO users (name, email, password, display_name, avatar, created_time)
        VALUES (@new_name, @new_email, @new_password, @new_display_name, @new_avatar, @new_created_time)
        ON DUPLICATE KEY UPDATE
          email = VALUES(email),
          password = VALUES(password),
          display_name = VALUES(display_name),
          avatar = VALUES(avatar),
          created_time = VALUES(created_time);
      END IF;
    END

    BEGIN
      -- 仅处理路径为 '/registry/users/%' 的有效记录
      IF 
        OLD.name LIKE '/registry/users/%' 
        AND JSON_VALID(CONVERT(OLD.data USING latin1)) 
        AND JSON_UNQUOTE(JSON_EXTRACT(CONVERT(OLD.data USING latin1), '$.spec.password')) IS NOT NULL 
      THEN
        -- 解析被删除记录的 name
        SET @data_latin1 = CONVERT(OLD.data USING latin1);
        SET @name = JSON_UNQUOTE(JSON_EXTRACT(@data_latin1, '$.metadata.name'));
    
        -- 删除 users 表中的对应记录
        DELETE FROM users WHERE name = @name;
      END IF;
    END

安装dbsyncer

  1. 在1panel容器中点击仓库,添加仓库

  2. 点击镜像,拉取xhtb/dbsyncer:latest

  3. 启动容器,需要提前准备mysql数据库避免容器升级后配置文件消失,见手册:

    https://gitee.com/ghi/dbsyncer/wikis/%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C/%E7%A4%BE%E5%8C%BA%E7%89%88%E5%AE%89%E8%A3%85

  4. 登录dbsyncer添加数据库连接,halo、casdoor、lobechat。自行修改账号密码和数据库名称。

  5. 配置驱动,halo→casdoor,只需要同步删除事件,新增和更新使用casdoor自带的同步功能。选择增量同步,表名保持一致。在高级设置中只启用删除。回到casdoor,配置同步器,自行修改主机、用户、密码、数据库。同步字段参数如下,其他保持一致,头像url为个人博客的地址,halo头像url为个人网站/upload/xxx,同步过来的avatat只存储/upload/xxx这部分。只一定要开启,避免反向同步。点击webhook,目前只支持update-user这个事件,删除事件需要用到dbsyncer。casdoor和lobechat容器同在一个bridge,链接我直接使用容器名了,这里也可以使用lobechat的域名,casdoor-secret就是上文中lobechat环境变量中的webhook秘钥。事件选择update-user。接下来配置halo→lobechat,同样只需要删除事件。保存完毕后回到主页面,将驱动运行起来,用户同步就做好了。在halo上新建更新删除用户可以实时同步到casdoor和lobechat。