✈️
ST Server Docs
检查服务状态个人主页
  • 😇快速开始
    • 🙄没有VPS?
  • 服务器配置
    • 👀初始化配置
    • 👨‍🔧一些运行环境的安装汇总
    • ⌨️系统相关配置
    • 👿一些疑难杂症
  • 🥳科学上网搭建
    • 📖原理讲解
    • 🖥️x-ui面板
    • 🕹️安装V2Ray
    • 🕹️Hysteria(歇斯底里)
    • 🕹️Reality
    • 🕹️OutLine
    • BBR加速
    • 💻关于客户端
  • 😖拯救被墙IP
    • 🍔CloudFlare CDN
    • 🍕Argo Tunnels
    • 🍟Gcore CDN
    • 🚠WARP双栈
    • 📡tailscale
  • 🖥️宝塔面板国际版
  • 💿Docker
    • 📀安装管理面板
    • 📀安装ward
    • 📀安装Nginx-Proxy-Manager
    • 📀安装https-portal(可选)
    • 📀配置Nginx+PHP环境
    • 📀安装AdGuard Home
  • ☁️云存储服务搭建
    • 🏢外部存储器
    • 🌥️搭建Next Cloud
    • ⛅搭建Alist
  • 🚇Warp
    • 🚀快速免费获取WARP+
    • 🚠配置WARP双栈
    • 😫曲线拯救IPv6-only
  • 😍无服务器平台
    • 🎮小白建站
    • 🏭Workers & Pages
    • 🕝其他SaaS平台
    • ⌚保活与负载均衡
  • 🤖AI
    • 🥇Pandora
    • 🥈ChatWeb-Next
    • OpenWebUI
    • OpenWebUI & Monitor
    • SearXNG
    • 🧿OneAPI
    • 🥉Bingo
    • 🏵️Chat-on-WeChat
    • 🐺Coze
    • 🎙️Bert-VITS2
    • 🎗️其他AI项目
  • 🐧投入Linux怀抱
    • 💽Linux的选择
  • 😘联系作者
由 GitBook 提供支持
在本页
  1. AI

OpenWebUI & Monitor

超级版OpenWebUI,带有用户管理系统

首先我们需要对compose文件进行调整,需要使用host模式,抛弃端口映射逻辑来方便各个服务之间的通讯,以及方便我们后续使用 ufw 防火墙来组织直接通过IP+端口的方式访问,提高安全性。

services:
  postgre:
    image: docker.1panel.live/library/postgres:latest
    container_name: postgre
    restart: always
    environment:
      - POSTGRES_USER=st
      - POSTGRES_PASSWORD=STshentong
      - POSTGRES_DB=openwebui
    volumes:
      - /data/postgres_data:/var/lib/postgresql/data
      - /data/postgres-init:/docker-entrypoint-initdb.d
    network_mode: host #端口5432

  open-webui:
    image: docker.1panel.live/dyrnq/open-webui:latest
    container_name: open-webui
    volumes:
      - /data/open-webui:/app/backend/data
    restart: always
    environment:
      - DATABASE_URL=postgresql://st:STshentong@localhost:5432/openwebui # 连接到 openwebui 数据库
      - ENABLE_WEBSOCKET_SUPPORT=True
      - WEBSOCKET_REDIS_URL=redis://localhost:6379
      - REDIS_URL=redis://localhost:6379
      - WEBUI_NAME=ChatST
      - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=1
      - AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST=1
      - USER_AGENT=${USER_AGENT:-Mozilla/5.0 (compatible; OpenWebUI/1.0; +https://github.com/open-webui)}
    build:
      args:
        USER_AGENT: $USER_AGENT
    network_mode: host #端口8080
    depends_on:
      - postgre
      - redis

  redis:
    image: docker.1panel.live/library/redis:latest
    container_name: redis
    restart: always
    network_mode: host #端口6379

  watchtower:
    image: docker.1panel.live/containrrr/watchtower
    container_name: open-webui-watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 3600 open-webui
    depends_on:
      - open-webui
    network_mode: host

在这个文件中我已经备注了各个服务的默认端口,注意OpenWebUI的默认端口是8080,而不是之前映射的3000;为了安装后面的监控,我们还需要一个数据库启动脚本,来创建 monitor 所需的数据库,先创建一个文件夹 /data/postgres-init ,然后在里面创建一个名为 init-extra-db.sql 的SQL文件,内容如下:

-- 创建额外的数据库
CREATE DATABASE openwebui_monitor;
-- 可选:如果你需要特定用户拥有这个库,可以在这里授权
-- GRANT ALL PRIVILEGES ON DATABASE openwebui_monitor TO st;

这样postgre在启动时就会创建这两个所需的数据库了。并且在这个compose文件中,我统一把所有的数据都存放在 /data 文件夹对应的目录中,而不是之前的docker volume,便于管理。

接下来我们开始部署监控系统,在 data 文件夹中创建一个 monitor 文件夹,里面放上两个文件,一个是compose文件,这个系统会用到我们刚才部署的数据库:

services:
  monitor:
    image: docker.1panel.live/variantconst/openwebui-monitor:latest
    container_name: openwebui-monitor
    env_file:
      - .env # 可以在 .env 文件中覆盖以下环境变量
    environment:
      # 默认连接到 host 网络下的 Postgre 容器(通过 localhost:5432)
      - POSTGRES_HOST=${POSTGRES_HOST:-localhost}
      - POSTGRES_PORT=${POSTGRES_PORT:-5432}
      # 默认使用第一个 Compose 文件中 Postgre 的用户和密码
      - POSTGRES_USER=${POSTGRES_USER:-st}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-STshentong}
      # 默认连接到 openwebui_monitor 数据库
      - POSTGRES_DATABASE=${POSTGRES_DATABASE:-openwebui_monitor}
    restart: always
    network_mode: host #端口3000

以及一个环境变量文件 .env ,注意前面的点 . :

# OpenWebUI Configuration
OPENWEBUI_DOMAIN=http://127.0.0.1:8080
OPENWEBUI_API_KEY=sk-dc2c745c8cd94faea80a5f3d9b348086 # OpenWebUI API key for fetching model list

# Access Control
ACCESS_TOKEN=STshentong # Used for Monitor page login
API_KEY=STshentong0722 # Used for authentication when sending requests to Monitor

# Price Configuration (Optional, $/million tokens)
INIT_BALANCE=5 # Initial balance for users, optional

# PostgreSQL Database Configuration (Optional, configure these if using external database)
# POSTGRES_HOST=172.21.0.2
# POSTGRES_PORT=5432
# POSTGRES_USER=postgres
# POSTGRES_PASSWORD=openwebui
# POSTGRES_DATABASE=openwebui_monitor

注意这里的上面一个API,是需要在OpenWebUI中开启API密钥,然后生成一个API,这个API是用来获取模型列表的;下面一个API是OpenWebUI前端函数调用这个后台模块时使用的,可以自己随便写;这里还有个设置项是初始额度,我这里设置成了5,后续通过脚本可以实现每天定时刷新重置成5元:

#!/bin/bash

# 容器名称
CONTAINER_NAME="postgre"

# 数据库连接信息
DB_USER="st"
DB_PASSWORD="STshentong"
DB_NAME="openwebui_monitor"

# SQL 更新语句
UPDATE_SQL="UPDATE users SET balance = 5;"

echo "尝试连接到容器 ${CONTAINER_NAME} 的数据库 ${DB_NAME}..."

# 使用 docker exec 在容器内部执行 psql 命令
# -U: 指定用户
# -d: 指定数据库
# -c: 执行 SQL 命令
# PGSSWORD: 设置环境变量,用于非交互式提供密码
PGPASSWORD="${DB_PASSWORD}" docker exec -it "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -c "${UPDATE_SQL}"

# 检查命令执行结果
if [ $? -eq 0 ]; then
  echo "成功将 users 表中的 balance 列所有数值重置为 5。"
else
  echo "执行 SQL 更新时出错,请检查错误信息!"
fi
"""
title: Usage Monitor
author: VariantConst & OVINC CN
git_url: https://github.com/VariantConst/OpenWebUI-Monitor.git
version: 0.3.6
requirements: httpx
license: MIT
"""

import logging
import time
from typing import Dict, Optional
from httpx import AsyncClient
from pydantic import BaseModel, Field
import json


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

TRANSLATIONS = {
    "en": {
        "request_failed": "Request failed: {error_msg}",
        "insufficient_balance": "Insufficient balance: Current balance `{balance:.4f}`",
        "cost": "Cost: ${cost:.4f}",
        "balance": "Balance: ${balance:.4f}",
        "tokens": "Tokens: {input}+{output}",
        "time_spent": "Time: {time:.2f}s",
        "tokens_per_sec": "{tokens_per_sec:.2f} T/s",
    },
    "zh": {
        "request_failed": "请求失败: {error_msg}",
        "insufficient_balance": "余额不足: 当前余额 `{balance:.4f}`",
        "cost": "费用: ¥{cost:.4f}",
        "balance": "余额: ¥{balance:.4f}",
        "tokens": "Token: {input}+{output}",
        "time_spent": "耗时: {time:.2f}s",
        "tokens_per_sec": "{tokens_per_sec:.2f} T/s",
    },
}


class CustomException(Exception):
    pass


class Filter:
    class Valves(BaseModel):
        api_endpoint: str = Field(default="", description="openwebui-monitor's base url")
        api_key: str = Field(default="", description="openwebui-monitor's api key")
        priority: int = Field(default=5, description="filter priority")
        language: str = Field(default="zh", description="language (en/zh)")
        show_time_spent: bool = Field(default=True, description="show time spent")
        show_tokens_per_sec: bool = Field(default=True, description="show tokens per second")
        show_cost: bool = Field(default=True, description="show cost")
        show_balance: bool = Field(default=True, description="show balance")
        show_tokens: bool = Field(default=True, description="show tokens")

    def __init__(self):
        self.type = "filter"
        self.name = "OpenWebUI Monitor"
        self.valves = self.Valves()
        self.outage_map: Dict[str, bool] = {}
        self.start_time: Optional[float] = None

    def get_text(self, key: str, **kwargs) -> str:
        lang = self.valves.language if self.valves.language in TRANSLATIONS else "en"
        text = TRANSLATIONS[lang].get(key, TRANSLATIONS["en"][key])
        return text.format(**kwargs) if kwargs else text

    async def request(self, client: AsyncClient, url: str, headers: dict, json_data: dict):
        json_data = json.loads(json.dumps(json_data, default=lambda o: o.dict() if hasattr(o, "dict") else str(o)))

        response = await client.post(url=url, headers=headers, json=json_data)
        response.raise_for_status()
        response_data = response.json()
        if not response_data.get("success"):
            logger.error(self.get_text("request_failed", error_msg=response_data))
            raise CustomException(self.get_text("request_failed", error_msg=response_data))
        return response_data

    async def inlet(self, body: dict, __metadata__: Optional[dict] = None, __user__: Optional[dict] = None) -> dict:
        __user__ = __user__ or {}
        __metadata__ = __metadata__ or {}
        self.start_time = time.time()
        user_id = __user__.get("id", "default")

        client = AsyncClient()

        try:
            response_data = await self.request(
                client=client,
                url=f"{self.valves.api_endpoint}/api/v1/inlet",
                headers={"Authorization": f"Bearer {self.valves.api_key}"},
                json_data={"user": __user__, "body": body},
            )
            self.outage_map[user_id] = response_data.get("balance", 0) <= 0
            if self.outage_map[user_id]:
                logger.info(self.get_text("insufficient_balance", balance=response_data.get("balance", 0)))
                raise CustomException(self.get_text("insufficient_balance", balance=response_data.get("balance", 0)))
            return body

        except Exception as err:
            logger.exception(self.get_text("request_failed", error_msg=err))
            if isinstance(err, CustomException):
                raise err
            raise Exception(f"error calculating usage, {err}") from err

        finally:
            await client.aclose()

    async def outlet(
        self,
        body: dict,
        __metadata__: Optional[dict] = None,
        __user__: Optional[dict] = None,
        __event_emitter__: Optional[callable] = None,
    ) -> dict:
        __user__ = __user__ or {}
        __metadata__ = __metadata__ or {}
        user_id = __user__.get("id", "default")

        if self.outage_map.get(user_id, False):
            return body

        client = AsyncClient()

        try:
            response_data = await self.request(
                client=client,
                url=f"{self.valves.api_endpoint}/api/v1/outlet",
                headers={"Authorization": f"Bearer {self.valves.api_key}"},
                json_data={"user": __user__, "body": body},
            )

            stats_list = []
            if self.valves.show_tokens:
                stats_list.append(self.get_text("tokens", input=response_data["inputTokens"], output=response_data["outputTokens"]))
            if self.valves.show_cost:
                stats_list.append(self.get_text("cost", cost=response_data["totalCost"]))
            if self.valves.show_balance:
                stats_list.append(self.get_text("balance", balance=response_data["newBalance"]))
            if self.start_time and self.valves.show_time_spent:
                elapsed = time.time() - self.start_time
                stats_list.append(self.get_text("time_spent", time=elapsed))
                if self.valves.show_tokens_per_sec:
                    tokens_per_sec = (response_data["outputTokens"] / elapsed if elapsed > 0 else 0)
                    stats_list.append(self.get_text("tokens_per_sec", tokens_per_sec=tokens_per_sec))

            stats = " | ".join(stats_list)
            if __event_emitter__:
                await __event_emitter__({"type": "status", "data": {"description": stats, "done": True}})

            logger.info("usage_monitor: %s %s", user_id, stats)
            return body

        except Exception as err:
            logger.exception(self.get_text("request_failed", error_msg=err))
            raise Exception(self.get_text("request_failed", error_msg=err))
        finally:
            await client.aclose()

然后我个人喜欢安装一个轻量化的系统监控 beszel ,方便我们实时掌握系统资源占用情况。同样在 /data 目录下创建一个文件夹 /beszel ,然后使用compose文件,我这里也修改成了统一使用host模式:

services:
  beszel:
    image: docker.1panel.live/henrygd/beszel:latest
    container_name: beszel
    restart: unless-stopped
    network_mode: host #端口 8090
    volumes:
      - ./beszel_data:/beszel_data
      - ./beszel_socket:/beszel_socket

  beszel-agent:
    image: docker.1panel.live/henrygd/beszel-agent:latest
    container_name: beszel-agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./beszel_socket:/beszel_socket
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      LISTEN: /beszel_socket/beszel.sock
      # 请勿删除密钥周围的引号
      KEY: '使用"添加系统"对话框复制的公钥进行更新'

这个时候他数据的默认存放位置就是在compose文件的那个目录中,所以不需要修改了,启动后使用主机格式 /beszel_socket/beszel.sock 得到一个密钥,然后替换掉compose文件中的KEY,重新部署compose即可。

由于使用这个监控系统需要启用API,这使得别人外部调用我们的网站变得非常简单,因此必须增加防火墙,我们使用宝塔WAF,因为这似乎是免费开源WAF中性能最好的,安装脚本:

URL=https://download.bt.cn/cloudwaf/scripts/install_cloudwaf.sh && if [ -f /usr/bin/curl ];then curl -sSO "$URL" ;else wget -O install_cloudwaf.sh "$URL";fi;bash install_cloudwaf.sh

直接一键安装即可,安装完成后会给出登录链接和初始用户名密码,配置好WAF后我们就可以启动系统的ufw防火墙来阻止直接对端口的访问了。

sudo ufw status  #查看当前防火墙状态
sudo ufw allow ssh  #先放行SSH端口
sudo ufw enable   #启动防火墙
sudo ufw default deny incoming   #阻止其他端口连接
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 8888   #假设我们已经将宝塔WAF配置到了8888端口

一般来说放行这些端口就已经足够了,其他网站都通过宝塔WAF进行反代,对于数据库的外部连接,我不建议放行5432端口,防止被爆破,需要使用时可以通过SSH隧道连接数据库。对于错误添加的规则,可以用下面的方法删除:

sudo ufw status numbered
sudo ufw delete 5
sudo ufw delete 13
上一页OpenWebUI下一页SearXNG

最后更新于2天前

都配置完成后再启动这个监控系统(先把OpenWebUI配置好,拿到API密钥),启动成功后再到OpenWebUI中添加函数(原链接: ):

🤖
https://github.com/VariantConst/OpenWebUI-Monitor/blob/main/resources/functions/openwebui_monitor.py