Speaches 部署

docker compose 安装

1
git clone https://github.com/speaches-ai/speaches.git

修改 compose.yaml 端口不要和内网其他端口重复

创建一个启动脚本方便执行 start.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash

# ================= 1. 检查管理员权限 =================
if [ "$EUID" -ne 0 ]; then
echo "❌ 错误: 请使用管理员权限运行此脚本!"
echo "👉 正确执行方式: sudo bash $0"
exit 1
fi

# ================= 2. 锁定当前脚本所在的目录 =================
# 确保无论你在哪里调用这个脚本,都能正确找到旁边的 compose.cuda.yaml
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" || exit 1

# ================= 3. 检查文件是否存在 =================
if [ ! -f "compose.cuda.yaml" ]; then
echo "❌ 错误: 当前目录下未找到 compose.cuda.yaml 文件!"
echo "路径: $SCRIPT_DIR/compose.cuda.yaml"
exit 1
fi

# ================= 4. 启动 GPU 服务 =================
echo "🚀 正在以 CUDA/GPU 模式启动 Speaches 服务..."

export COMPOSE_FILE=compose.cuda.yaml
docker compose up --detach

# 检查 Docker 命令运行结果
if [ $? -eq 0 ]; then
echo "✅ Speaches 服务已成功在后台启动!"
else
echo "❌ 启动失败,请检查 Docker 服务状态或日志。"
fi

通过 sudo start.sh 启动 docker 服务

初次使用通过 API 下载模型

1
2
3
4
5
6

# Systran/faster-whisper-small
# Systran/faster-whisper-medium
# Systran/faster-whisper-large-v3

http://192.168.48.177:15465/v1/models/Systran/faster-whisper-medium

FAQ

Q1: 只要启动 docker 服务,nvidia-smi 就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
speaches  |  when using [('CUDAExecutionProvider', {}), ('CPUExecutionProvider', {})]

speaches | Falling back to ['CPUExecutionProvider'] and retrying.

speaches | ****************************************

speaches | 2026-07-28 13:21:42,456:INFO:speaches.executors.shared.base_model_manager:_load:75:Model silero_vad_v5 loaded in 0.12s

speaches | 2026-07-28 13:21:42,456:DEBUG:speaches.executors.shared.base_model_manager:_increment_ref:83:Incremented ref count for silero_vad_v5, self.ref_count=1

speaches | 2026-07-28 13:21:49,379:DEBUG:speaches.executors.silero_vad_v5:__call__:138:VAD model inference took 6.8950s

speaches | 2026-07-28 13:21:49,698:DEBUG:speaches.executors.shared.base_model_manager:_decrement_ref:88:Decremented ref count for silero_vad_v5, self.ref_count=0

speaches | 2026-07-28 13:21:49,698:INFO:speaches.executors.shared.base_model_manager:_decrement_ref:98:Model silero_vad_v5 is idle, not unloading

speaches | 2026-07-28 13:21:49,698:DEBUG:speaches.executors.silero_vad_v5:get_speech_timestamps:307:VAD processing took 7.3658s for 3765.50s audio

speaches | 2026-07-28 13:21:49,701:DEBUG:speaches.executors.shared.base_model_manager:_load:72:Loading model Systran/faster-whisper-medium

A1: 显卡从休眠唤醒失败: 笔记本显卡在空闲时,Linux 驱动会自动把 GPU 置入 D3cold(深度省电模式)。当 Docker 容器启动并加载 CUDA/ONNX 运行时,会强行向 GPU 发送初始化指令,导致显卡从休眠唤醒时 PCIe 响应超时。

驱动失去响应: 唤醒失败后,NVIDIA 驱动抛出错误(通常会在内核日志里显示 GPU has fallen off the bus),导致 nvidia-smi 无法读取传感器温度、风扇和功耗信息,全显示为 ERR!。

1
2
3
4
5
# 临时开启常驻模式:
sudo nvidia-smi -pm 1

# 设置开机自启(使其永久生效):
sudo systemctl enable --now nvidia-persistenced

防止节点钓鱼

安全建议

  • 设置非全局模式,国内能访问的站点就不要使用代理
  • HTTP 站点如果要进行敏感操作,不要使用代理(安全的网站都是用 HTTPS 协议)
  • 千万别被安装了根证书,这是最重要的一条,会帔看个精光
  • 最好不要使用自带节点的第三方软件,某某加速器、某某 VPN 等这种软件可能会给你安装证书。
  • 经常性检查证书链,保障数据安全

链路分析

  • 创建一个根证书

    打开 Fiddler Classic,点击顶部菜单栏的 Tools -> Options…。

    切换到 HTTPS 选项卡。

    勾选 Capture HTTPS CONNECTs 和 Decrypt HTTPS traffic。

    勾选后,Fiddler 会自动弹窗问你是否要将根证书安装到系统受信任区域,点击 Yes 即可。

    如果需要导出证书文件(例如给手机安装):

    点击右侧的 Actions 按钮。

    选择 Export Root Certificate to Desktop。

    此时你的桌面就会出现一个名为 FiddlerRoot.cer 的证书文件。

    这个证书会被写入到第三方软件的内部,软件安装的时候会在目标机器上生成一个根证书。

  • 把证书传到目标电脑上执行安装来模拟证书被注入的过程:

    方法一:手动图形化安装(最适合单台电脑)

    启动安装向导:双击该证书文件,在弹出的窗口中点击 “安装证书…” (Install Certificate)。

    选择存储位置:

    选 “当前用户”(仅对当前登录账号生效,通常不需要管理员权限)。

    选 “本地计算机”(对电脑上所有账号生效,推荐,需要管理员权限)。

    点击“下一步”。

    【最关键的一步】选择证书存储:

    选择 “将所有的证书都放入下列存储”。

    点击 “浏览” 按钮。

    在列表中选中 受信任的根证书颁发机构 (Trusted Root Certification Authorities)。

    点击“确定”,然后“下一步”,最后点击“完成”。

    安全警告确认:如果系统弹出一个安全警告弹窗,询问是否要安装此证书,点击 “是” 即可。

    方法二:命令行极速安装(适合自动化 / 批处理脚本)

    以管理员身份运行命令提示符 (CMD) 或 PowerShell。 执行以下命令(将路径替换为你实际的证书文件路径):

    1
    2
    certutil -addstore -f "Root" "C:\路径\到\你的\FiddlerRoot.cer"
    # 看到“CertUtil: -addstore 命令成功完成”的提示,就说明安装好了。
  • 目标电脑 google 安装插件 SwitchyOmega 用于模拟开了的代理软件对数据进行转发

    配置转发 fillder所在的服务器IP地址:8888

    开启 Fiddler 允许远程连接:

    打开 Fiddler Classic,依次点击菜单栏 Tools -> Options -> Connections。

    勾选 Allow remote computers to connect(允许远程计算机连接)。

    (修改后需要重启一次 Fiddler 才能生效)。

    此时被注入的目标电脑就能通过代理转发到 fillder 服务上网了,但是使用的还是原有的证书,也并不能看见解密的信息

  • 安装了根证书并不等于开启了解密!Fiddler 默认只捕获 HTTP,对 HTTPS 流量只做透传(Tunnel),所以浏览器看到的还是原厂证书。

解决办法:

打开 Fiddler,点击菜单栏 Tools -> Options -> HTTPS 页签。

确保勾选了 Decrypt HTTPS traffic(解密 HTTPS 流量)。

下方的下拉菜单选择 from all processes(捕获所有进程)或 from browsers only。

点击 OK 保存并重启(如果是第一次勾选,它会提示你重新生成并安装一次证书)。

这样可以在左侧列表中看到被拦截的并解密的明文信息。

证书校验流程

当我们首次访问一个之前从未去过的网站(比如 https://example.com)时,你的电脑里确实没有 example.com 的证书。

那么浏览器到底是怎么敢信任这个网站的呢?核心原因在于:TLS 证书体系采用的是“信任链 (Chain of Trust)”机制。

你可以把证书验证想象成去办理出国签证:海关(浏览器)并不认识你个人(网站),但海关认识给你盖章的“大使馆/公安局(根证书颁发机构 CA)”。

证书的“家族树”:三层信任链结构,在真实的互联网中,证书通常分为三层:

1
2
3
4
5
6
7
[ 根证书 Root CA ]             ---- 存放在你的 Windows / macOS 系统信任库中(预装好的)
│ (签发/盖章)

[ 中级证书 Intermediate CA ] ---- 由 CA 机构持有,负责日常签发
│ (签发/盖章)

[ 站点证书 Server/Leaf ] ---- 你访问的网站(如 example.com)持有的证书

根证书 (Root CA):全球权威的“超级公章”(如 DigiCert, Sectigo, Let’s Encrypt)。它们数量很少(几百个),在你买电脑/装操作系统时,微软或苹果就已经预先打包安装在你的系统里了。

中级证书 (Intermediate CA):根证书的“代理人”。为了防止根证书的私钥泄露导致全球信任崩塌,根 CA 通常把私钥锁在保险柜里,平时只用中级证书来给具体的网站盖章。

站点证书 (Leaf Certificate):网站管理员买到的、写着 example.com 的证书。

当 Chrome 拿到服务器发过来的证书链后,它会在你的电脑本地进行以下 4 步严格审查:

步骤 A:查验域名与有效期 (Domain & Expiry Verification)

检查域名:这个证书里的 Subject Alternative Name (SAN) 字段写的是不是 example.com?(防止拿 baidu.com 的证书来冒充 example.com)。

检查时间:当前时间是不是在证书的 Not Before 和 Not After(有效期)之间?

步骤 B:沿着“信任链”向上溯源 (Chain Building)

浏览器看站点证书:“你是 example.com,谁给你签发的?” $\rightarrow$ 发现签名者是 “DigiCert Global TLS Intermediate CA”。

浏览器检查随请求一起发过来的中级证书,确认签名正确。

浏览器继续看中级证书:“你又是谁签发的?” $\rightarrow$ 发现签名者是 “DigiCert Global Root CA”。

步骤 C:对暗号,比对系统预装的“根证书库” (Trust Anchor Matching)

浏览器拿着寻找目标的名称 “DigiCert Global Root CA”,打开你的 Windows 系统内置的 “受信任的根证书颁发机构” 数据库。

结果:找到了! 你的 Windows 系统里早已躺着这个 DigiCert 的根证书公钥。

步骤 D:数学密码学验证(公钥验签)

用系统里的根证书公钥,去解密并验证中级证书上的数字签名 $\rightarrow$ 验证成功!(证明中级证书合法)。

用中级证书的公钥,去解密并验证 example.com 站点证书上的数字签名 $\rightarrow$ 验证成功!(证明站点证书合法)。

兜底检查:证书有没有被临时吊销?(Revocation Check)

即使证书没过期、签名也对,网站如果私钥泄露,CA 机构会提前吊销这张证书。浏览器还会通过以下方式快速确认:

OCSP Stapling(在线证书状态协议):服务器在握手时直接附带一份来自 CA 的“带防伪印章的证明”,证明这张证书今天依然有效。

CRL(证书吊销列表):浏览器定期下载的黑名单列表。

问题

  • Q: Chrome 走了 QUIC (HTTP/3) 协议,绕过了 HTTP 代理
    Google 自家的服务(Google 搜索、YouTube 等)默认会优先使用基于 UDP 的 QUIC 协议。
    Fiddler 只支持基于 TCP 的 HTTP/1.1 和 HTTP/2,无法拦截 UDP 流量。当 Chrome 走 QUIC 时,流量直接绕过代理直连了 Google,所以你看到的是官方证书。

    A: Chrome 的 QUIC 协议

    在 Chrome 地址栏输入并打开:chrome://flags/#enable-quic

    找到 Experimental QUIC protocol,将其状态改为 Disabled。

    点击右下角的 Relaunch 重启浏览器。

  • Q: 为什么本地电脑也受影响?
    当你在本地电脑上打开 Fiddler Classic 时,它在后台默默做了这件事:

    它把 Windows 系统的代理设置自动改成了 127.0.0.1:8888。

    因为 Chrome、Edge、IE 以及绝大多数 Windows 应用,默认都会读取系统全局代理,所以你本地电脑上的所有网页请求,都会强制自动走 Fiddler。

    只要请求走了 Fiddler,并且你开启了 HTTPS 解密,Fiddler 就会用它安装在本地的根证书去重签网页,因此你在本地电脑访问网页时就会看到 Fiddler 的证书。

    如果你只希望通过代理插件(比如 SwitchyOmega)或远程设备连过来,而不想让本地电脑的所有软件都被 Fiddler 劫持,可以关闭 Fiddler 的“系统代理”功能:

    操作步骤:
    关闭 Fiddler 的系统代理捕获:

    Fiddler Classic:点击左下角的 Capturing 字样(或者按 F12),取消它的勾选。此时左下角不会显示 Capturing。

    或者取消勾选顶部菜单 File -> Capture Traffic。

    保持 Fiddler 的监听端口开放:

    只要 Fiddler 软件开着(并且在 Tools -> Options -> Connections 里勾选了 Allow remote computers to connect),它的 8888 端口就在后台持续监听着。

    效果:

    此时你本地的普通浏览器、软件不再走 Fiddler,访问网站依然是原厂证书。

    只有开启了 SwitchyOmega 插件的浏览器,或者配置了代理(指向 你的IP:8888)的远程电脑/手机,流量才会进入 Fiddler 被拦截解密。

Windows 实用工具

Snipaste 截图工具

F1 快捷键截图

PowerToys

通过 windows ·商店下载

FancyZones 可实现高度自定义的窗口分屏.

将软件拖入分区:核心操作:按住 Shift 键.布局配置完成后,关闭 PowerToys。
接下来在日常使用中:用鼠标左键按住你要移动的软件窗口(比如浏览器)的标题栏。长按键盘上的 Shift 键。此时,屏幕上会浮现出你刚才画好的分区。将窗口拖入目标区域,当区域高亮显示时,松开鼠标左键,窗口就会瞬间完美贴合该区域。

接管 Windows 原生快捷键:如果你习惯用键盘操作分屏,可以在 FancyZones 设置页面的“窗口行为”选项卡中进行微调:勾选“覆盖 Windows 贴靠快捷键”。开启后,当你选中一个软件窗口并按下 Win + 左/右方向键 时,窗口会按照你 FancyZones 划定的区域进行移动,而不是 Windows 自带的默认 50% 居中分屏。

Flow Launcher + Everything

分别独立安装两个软件

打开 Flow Launcher setting => Plugins => Explorer => General setting 把搜索引擎修改为 Everything

O2OA 私有化部署

安装 mysql 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 安装服务
sudo apt install -y mysql-server

# 检查服务是否启动
systemctl status mysql

# 首次登录没有密码
sudo mysql -u root -p

# 创建对应的数据库

CREATE DATABASE my_database
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
# 查看所有数据库
SHOW DATABASES;

# 删除数据库
DROP DATABASE database_name;
# 修改字符集和排序
ALTER DATABASE database_name
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

# 创建新用户
# 'localhost':表示该用户只能从本地机器连接到数据库。如果希望该用户可以从任何主机连接,可以使用 '%',如 'new_user'@'%'。
CREATE USER 'new_user'@'localhost' IDENTIFIED BY 'your_password';
# 查看所有用户
SELECT User, Host FROM mysql.user;


#ALL PRIVILEGES:赋予所有权限。
# my_database.*:指示该用户可以对 my_database 数据库中的所有表进行操作。
# 'localhost':表示该用户只能从本地连接。
GRANT ALL PRIVILEGES ON my_database.* TO 'new_user'@'localhost';

# 刷新权限
FLUSH PRIVILEGES;

# 查看端口
SHOW VARIABLES LIKE 'port';

cat /etc/mysql/mysql.conf.d/mysqld.cnf

下载安装包

下载 安装包

解压并复制到 /opt

1
2
3
unzip  o2server-8.2.3-linux-x64.zip

mv o2server /opt

配置服务器端口

O2OA 服务器端口配置文件所在位置:o2server/config/node_127.0.0.1.json

如果目录里没有该文件或者没有 config 目录,可以新建一个 config 目录,然后从 configSample 目录里 COPY 一个到新建的 config 目录下。

1
# 自行修改web端口配置

自启动服务

因为需要用 root 用户启动服务,所以先配置开机启动服务

1
2
3
4
./service_linux.sh o2server start_linux.sh

systemctl enable o2server
systemctl start o2server

配置数据库链接

1
jdbc:mysql://127.0.0.1:3306/o2oa_db?autoReconnect=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8

Enternium 自动化脚本

环境配置

下载 airtest 新建 .airtest 项目, 创建的 .air 文件就是一个含有 python 文件的文件夹,可以使用 python 语法,也可以使用 airtest 封装的命令

文件拆分

可以创建多个 .air 文件,或是使用原生的 python 文件, 例如

1
2
3
4
5
6
7
# utils.air

# -*- encoding=utf8 -*-
from airtest.core.api import *

def multi_touch(pos, times=3, base_duration=0.135, base_interval=0.1):
logger.debug("点击坐标")

在主文件调用

1
2
3
4
5
6
7
8
# -*- encoding=utf8 -*-

from airtest.core.api import *

using("../utils.air")
from utils import multi_touch

multi_touch((target_x, target_y), times=2,base_duration=0.135, base_interval=3.2)

手势

为什么原生 ADB 无法实现连续的复杂手势,Minitouch 可以完美实现

每次你执行一句 adb shell input 命令,Android 底层都需要启动一个新的 Java 进程(app_process)来解析和执行这个动作。这个进程的启动和虚拟机初始化通常需要 100ms - 300ms 甚至更久。如果你想通过连续发送多条 ADB 命令来画一个圆,命令与命令之间会有明显的卡顿和停顿,手指(触点)会在屏幕上“断开”又“重连”,根本连不起来。

指令本身的局限性(缺乏流式控制)
原生 ADB 的 input 命令只支持离散的指令(点击一次,或者两点之间的一条直线滑动)。它没有提供“按下 (Down) -> 移动到点A (Move) -> 移动到点B (Move) -> 抬起 (Up)” 这种拆分状态的流式 (Streaming) 接口。

当你在 Airtest 中指定了 touch_method=MINITOUCH 时,Airtest 并不是在调用 ADB 命令,而是使用了完全不同的底层架构:

常驻的 Socket 连接(零启动延迟)
Airtest 会在设备端推入一个用 C/C++ 编写的 minitouch 守护进程,并在设备内部启动一个 Server。你的纯 Python 脚本会通过 ADB 端口转发(Port Forwarding)与这个 Server 建立一个 TCP Socket 连接。建立连接后,Python 脚本发送动作指令只是一次极快的网络 I/O,延迟在毫秒级,完全没有进程启动的开销。

直达 Linux 内核级别的事件注入 (evdev)
Minitouch 绕过了 Android 庞大臃肿的 Framework 层(InputManager),直接向 Android 底层的 Linux 驱动节点(通常是 /dev/input/eventX)写入原始的触摸事件。

基于状态机的连续指令
Minitouch 拥有极其精简的自定义协议,完美支持连续动作。在你的 Python 脚本底层,Airtest 可以通过 Socket 连续发送如下指令:

d 0 100 200 50 (第 0 个触控点,在坐标 100,200 按下,压力 50)
c (Commit,提交生效)
m 0 110 210 50 (触控点 0 移动到 110,210)
c (提交生效)
u 0 (触控点 0 抬起)
c (提交生效)

实现一个手势
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- encoding=utf8 -*-
from airtest.core.api import *
# 切换设备的触控驱动为 MINITOUCH 或 MAXTOUCH
auto_setup(__file__, devices=["Android://127.0.0.1:5037/emulator-5554?touch_method=MINITOUCH"])

# -*- encoding=utf8 -*-

from airtest.core.api import *

def draw_v_gesture(size=300, duration=0.5):
"""
用 dev.swipe_along 实现一笔画 V 字手势(中途绝不抬手)
"""
# 1. 获取当前设备对象
dev = device()
width, height = dev.get_current_resolution()

# 2. 计算中心点与 V 字的 3 个轨迹点
center_x = width / 2
center_y = height / 2
half_size = size / 2

p1 = (center_x - half_size, center_y - half_size) # 起点(左上)
p2 = (center_x, center_y + half_size) # 拐点(底部)
p3 = (center_x + half_size, center_y - half_size) # 终点(右上)

# 3. 调用设备的 swipe_along 接口,传入 3 个坐标构成的轨迹数组
# 这个接口在底层会依次 down -> move(p2) -> move(p3) -> up,保证一笔画完
dev.swipe_along([p1, p2, p3], duration=duration, steps=10)
  1. 如何一直向着“雾气”区域探索?
    从图片 image_2e587d.jpg 中可以看出,探索过的区域是深灰色的地面(带绿树),而未探索的雾气呈现出非常明显的黄褐色/土黄色。

解决方案:HSV 颜色掩膜(Color Masking)
我们不再判断“哪里亮”,而是专门寻找“黄褐色”。

提取雾气: 使用 OpenCV 将小地图从 BGR 转换为 HSV 颜色空间。设定黄褐色的阈值(Hue 约在 15-40 之间),过滤出一张只包含雾气的二值化黑白图(雾气部分是白色,其他全是黑色)。

计算重心: 找到这块白色区域的重心(质心),或者寻找离屏幕中心(玩家)最近的雾气边缘点。

输出向量: 算出从屏幕中心指向这个雾气重心的角度,这就是你要前进的方向。

  1. 陷入四周都是黑色边界的死胡同,如何找到出口?
    在图片右侧和下方,不可跨越的边界(虚空/墙壁)呈现出纯黑色或极暗的棕黑色。当你陷入死胡同时,你的四周绝大部分方向都是这种纯黑边界,只有出口方向是深灰色的地面。

解决方案:小地图射线投射法(Minimap Raycasting)
这是一种非常经典的 2D 寻路算法,它模仿了雷达扫描。

二值化墙壁: 把小地图转化为灰度图,设定一个极低的亮度阈值(比如像素值 < 15),把所有“纯黑边界”提取出来。

发射射线: 以小地图中心(玩家位置)为起点,向 8 个或 16 个方向(每隔 45 度或 22.5 度)发射虚拟射线。

测量距离: 射线在碰到“纯黑像素”时停止。记录每条射线延伸的长度。

决策出口: 哪条射线最长,说明那个方向的空地最大、路最远。 在死胡同里,最长的那条线必定指向出口。

核心代码实现
我们可以将这两个逻辑写成两个函数。以下是利用 OpenCV 分析你这张截图的实现思路:

Python
import cv2
import numpy as np
import math

def get_fog_direction(minimap):
“””
目标1:寻找雾气方向
“””
# 转换到 HSV 空间寻找黄褐色雾气
hsv = cv2.cvtColor(minimap, cv2.COLOR_BGR2HSV)

# 设定黄褐色雾气的 HSV 范围(需要根据实际截图微调)
lower_fog = np.array([15, 50, 50])
upper_fog = np.array([40, 255, 255])

mask = cv2.inRange(hsv, lower_fog, upper_fog)

# 寻找雾气的轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
    # 找到面积最大的雾气块
    largest_contour = max(contours, key=cv2.contourArea)
    M = cv2.moments(largest_contour)
    if M["m00"] != 0:
        # 计算雾气重心
        target_x = int(M["m10"] / M["m00"])
        target_y = int(M["m01"] / M["m00"])
        
        # 计算方向角
        h, w = minimap.shape[:2]
        center_x, center_y = w // 2, h // 2
        dx = target_x - center_x
        dy = center_y - target_y # Y轴向下反转
        angle = math.degrees(math.atan2(dy, dx))
        return angle % 360
return None

def find_exit_direction(minimap):
“””
目标2:陷入死胡同时,用射线法寻找出口
“””
# 转换为灰度图
gray = cv2.cvtColor(minimap, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
center_x, center_y = w // 2, h // 2

max_length = 0
best_angle = 0

# 向 16 个方向发射射线(每 22.5 度一根)
for angle in range(0, 360, 22):
    radian = math.radians(angle)
    length = 0
    
    # 逐步向外延伸射线,每次步进 2 个像素
    while True:
        test_x = int(center_x + length * math.cos(radian))
        test_y = int(center_y - length * math.sin(radian))
        
        # 边界检查
        if not (0 <= test_x < w and 0 <= test_y < h):
            break
            
        # 碰到黑色边界(像素值 < 20 判定为黑墙)停止延伸
        if gray[test_y, test_x] < 20:
            break
            
        length += 2
        
    # 记录最长的一根射线
    if length > max_length:
        max_length = length
        best_angle = angle
        
return best_angle

寻路逻辑的最佳整合
在你的主循环中,可以将这两个函数与之前讨论的状态机结合起来:

第一优先级: 检查小地图是否有红点(怪物),有则追踪。

第二优先级: 如果没怪物,调用 get_fog_direction()。如果有明确的雾气方向,向着雾气走。

第三优先级(兜底): 如果被卡住(连续判定移动失败),立刻调用 find_exit_direction(),程序会像雷达一样扫出最长的那条路(即死胡同出口),强制向那里突围。

这三个问题非常深刻,完全触及了自动寻路算法(尤其是基于未知环境的“SLAM/前沿探索”算法)的最核心痛点。

其实,上一轮给你写的那套结合了“最近雾气(Closest Fog)”与“射线避障(Raycast)”的代码,在底层逻辑上已经天然地、数学化地兼容了这三种极端情况。

我们把这三个问题逐一拆解,看看这套逻辑是如何在底层完美闭环的。

问题一:出生地背后是墙,前方是雾气,如何兼容“走向雾气”与“沿墙走”?
逻辑核心:雾气是“目的”,墙壁是“约束”。

这套算法并不是“盲目找墙去贴”,而是“直线走向雾气,遇到墙才贴墙”。

在出生地,你的目标角度(最近的雾气)一定在前方。

算法会向前方发射射线。因为你背对墙壁,前方的射线是畅通无阻的(check_ray_clear 返回 True)。

此时,系统根本不会去理会你背后的墙,而是直接下达指令:直线走向前方的雾气。

只有当雾气在墙角的另一边,你的直线射线“撞”到了墙,算法才会启动“沿墙寻找安全角度”的逻辑(向左右偏移 15°、30° 等),从而表现出“顺着墙壁边缘走过去”的动作。

结论: 这两者完全不冲突,系统会自动在“开阔地直线冲锋”和“有障碍物贴墙绕行”之间无缝切换。

问题二:死胡同的墙被藏在雾气下,探开后发现死路,如何走出去?
你提到的细节非常精准:“墙的边界在雾气下面,没探索是看不见的”。这正是机器人导航中经典的“局部感知死胡同困境”。

但别担心,上一轮代码中的 get_best_safe_angle(安全角度扫掠)就是专门为了破解这个困境而设计的。让我们推演一下死胡同里的运作过程:

陷入死胡同: 你把死胡同里的雾气全吃光了,周围暴露出了纯黑的墙壁。

目标在墙外: 此时,离你最近的下一片雾气,一定在死胡同外面(也就是隔着一堵黑墙)。

直线撞墙: 算法向着那片雾气发射直线射线,必然撞到死胡同的黑墙。

雷达扫掠寻找出口: 算法发现前方不通,开始向两侧展开测试(±15°, ±30°…)。它发射的射线会扫过死胡同的左墙、右墙,全部提示撞墙。

找到来时的路: 当算法扫掠到死胡同的“入口”(也就是你刚走进来的那个方向,那里是深灰色的已探索地面,没有黑墙)时,射线畅通无阻!

成功脱困: 算法会立刻返回这个指向出口的角度。你的角色会华丽地转个身,顺着死胡同的边缘,稳稳地走出去。

结论: 只要死胡同有出口,射线扫掠机制(从 0° 一直扫到 180°)就一定能把出口“摸”出来,强制角色回头。

问题三:没有墙的独立雾气区域,如何保证不留下雾气碎块?
当你遇到一片处于广场正中央、周围没有任何墙壁的独立大雾区时,你担心角色会像无头苍蝇一样乱穿,把雾气切成碎片。

这正是我们使用 “最近边缘优先法则(Closest Frontier First)” 的最大意义。这套数学逻辑是自带“强迫症”的:

永远吃最近的一口: get_closest_fog 函数计算了屏幕上每一个雾气像素点到你当前位置的绝对欧氏距离。它不仅指出了方向,还锁定了离你最近的那个像素。

连续推进,绝不跳跃: 当你向着最近的像素走了一步,这个像素被探开了(变成了深灰色),下一帧,紧挨着它的那一点,自然就成为了新的“最近像素”。

推土机效应: 这种逻辑在宏观上表现出来的行为,就像是一台推土机。它会沿着雾气的最外层边缘,一层一层地、均匀地向内推进。

绝不留碎块: 如果系统留下了一个碎块在身后,那么那个身后的碎块距离你的绝对距离,一定小于前方的大片雾气。系统会立刻调转枪头,先把这个小碎块“吃干抹净”,然后再继续向前推进。

结论: 在没有墙壁干扰的开阔地带,基于欧氏距离最短的“贪心吃雾”算法,是最完美的“无死角扫图”策略。它在几何学上保证了探索边界的连续性,绝对不会遗漏任何一块碎片。

推荐技术栈与实现步骤
技术选型
推流与通信:

推流获取: 使用 scrcpy 的底层 ffmpeg / PyAV 接口,或者模拟器的共享内存/ADB 截图(每秒 15-30 帧即可满足 RL 需求)。

动作发送: Minidriver / Minitouch / ADB(高频低延迟点击)。

强化学习框架:

Stable-Baselines3 (SB3): 目前 Python 最成熟的 RL 库。

算法首选:PPO (Proximal Policy Optimization) —— 容错率高、收敛稳定,非常适合处理屏幕图像输入(Nature-CNN 骨干网络)。

环境封装:

使用 Gymnasium (OpenAI Gym) 将游戏包装成标准环境:env.reset() 和 env.step(action)。

Window 常见问题

Windows 10 搜索框一直是白色

这通常是因为 Windows 10 的搜索框内嵌了必应 (Bing) 网络搜索的网页组件,而这个网页组件的加载卡在了亮色模式,导致它无视了你系统的本地主题设置。

你可以通过以下几个专门针对 Win10 的方法来彻底解决这个问题:

方法一:切断搜索框的网络连接(最有效的终极修复)
既然是网络搜索组件导致的白屏 Bug,我们可以通过注册表直接关闭搜索框的“网络搜索建议”功能。这不仅能 100% 解决白色背景问题,还能让本地搜索(搜文件、搜软件)变得非常快。

按 Win + R 打开运行窗口,输入 cmd,然后按 Ctrl + Shift + Enter 以管理员身份运行。

复制并粘贴以下命令(直接在黑框里右键即可粘贴),然后按回车:

1
reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f

提示“操作成功完成”后,再次复制并运行以下命令来重启资源管理器:

1
taskkill /f /im explorer.exe & start explorer.exe

此时你的任务栏会刷新,再次点击搜索框,它应该已经强制恢复成与系统一致的深色背景了。

win10输入法变成繁体中文

通常是因为你不小心误触了 Windows 10 微软拼音输入法自带的简繁切换快捷键。

确保当前处于中文输入状态,然后直接在键盘上按下 Ctrl + Shift + F

如何永久关闭这个快捷键(防止再次误触)如果你平时完全不需要打繁体字,建议直接把这个快捷键关掉,一劳永逸:

1.进入语言设置:按 Win + I 打开 Windows 设置。点击 时间和语言。在左侧菜单栏选择 语言。
2.打开微软拼音选项:在右侧找到“首选语言”下的 中文(简体,中国),点击它。点击展开后出现的 选项 按钮。向下滚动找到“键盘”列表下的 微软拼音,点击它。再次点击展开后出现的 选项 按钮。
3.关闭简繁切换快捷键:在微软拼音的设置页面,点击 按键。向下滚动到底部,找到“热键”部分。找到 简繁体中文输入切换。将后面的快捷键下拉菜单设置为 无,或者直接关闭开关(取决于你的 Win10 具体小版本号)。

在 Windows 任务管理器里看到同一个 iSCSI 硬盘被显示成了三四个

在 Windows 任务管理器里看到同一个 iSCSI 硬盘被显示成了三四个,这通常是因为 “多路径重影” (Multiple Sessions / Multi-pathing) 造成的。

当你把底层的存储(比如 TrueNAS 分配的块存储或游戏盘)通过 iSCSI 映射到 Windows 时,如果出现了以下两种情况,Windows 就会把同一个硬盘认成多个独立的物理硬盘:

重复点击了连接:在 Windows 的 iSCSI 发起程序中,你可能对同一个目标 (Target) 多次点击了“连接”,导致系统建立了多个独立的会话。

多网卡/多 IP 路径:你的电脑或 NAS 有多个 IP 地址,Windows 通过不同的网络路径都连上了这个 iSCSI 目标,但你没有在 Windows 中启用并配置 MPIO (多路径 I/O) 来把它们合并成一个盘。

1.在磁盘管理中让硬盘“脱机”:解除 Windows 对硬盘的文件占用.右键点击任务栏的开始菜单图标(或按 Win + X),在弹出的菜单中选择 磁盘管理。在下方的列表中找到你的 iSCSI 硬盘。由于现在的重复连接问题,你可能会看到好几个容量完全相同的盘(其中一些可能会提示“签名冲突”或已经处于脱机状态)。找到那个状态为 联机 的 iSCSI 硬盘,在其左侧写着“磁盘 1”、“磁盘 2”等字样的灰色方块区域上点击右键。选择 脱机。这一步会安全地卸载文件系统,解除所有的占用锁定。你的盘符会暂时消失,这是正常的。

2.断开多余的 iSCSI 会话:回到 iSCSI 发起程序操作.再次回到 iSCSI 发起程序的窗口。在“目标”选项卡中,选中你的目标,点击 属性。在“会话”列表中,选中多余的会话点击 断开连接。此时因为硬盘已经脱机,系统不会再提示“正在使用”。依次断开,请务必保留 1 个会话不要断开,维持基础连接。

3.重新上线硬盘:恢复正常使用.回到 磁盘管理 窗口。在刚刚脱机的硬盘灰色方块区域再次点击右键。选择 联机。此时你的盘符会重新出现,你可以打开任务管理器确认,现在应该只剩下一个物理硬盘的图表了。

4.清理“收藏的目标”缓存:防止每次开机又变出几个盘.为了防止你下次重启电脑时,系统又按之前的错误记忆去连接三四次:在 iSCSI 发起程序的主界面,切换到上方的 收藏的目标 (Favorite Targets) 选项卡。如果这里的列表中出现了好几个完全相同的目标名称,选中那些多余的,点击 删除 (Remove)。列表里只保留一条该目标的记录即可。

windows 定时备份

下载 Veeam Agent for Microsoft Windows FREE 安装后创建一个备份任务:

BackupMode: Entire computer 选择整机备份在恢复的时候可以选择恢复任何磁盘,或是文件

Destination: Shared folder 选择 Truenas 的 SMB 共享文件夹

永远向前增量,取消勾选 Create active full backups periodically
第 1 天(周一): 生成唯一的全量文件 [全量 VBK-周一]。
第 2 到 7 天: 每天只生成几十 MB 的增量文件 [增量 VIB]。到了第 7 天(周日)晚上,你的文件夹里有 1 个全量 + 6 个增量(正好 7 个点)。
第 8 天(下周一)的动作极其精妙:
Veeam 首先照常做一个新的增量备份:[增量 VIB-第8天]。此时链条暂时变成了 8 个点。
接着,合并(Merge)开始! Veeam 会在你的 NAS 后台,把最老的那个 [增量 VIB-第2天] 的数据,直接注入(Inject)到 [全量 VBK-周一] 里面去。
注入完成后,原本那个老全量文件的时间戳和内容,就自动“进化”成了周二的状态。
然后,Veeam 把那个没用的 [增量 VIB-第2天] 物理删除。
整个链条再次恢复成 1 个(进化的)全量 + 6 个增量,依然维持 7 个点。

Shared Folder: 设置一个保持备份的天数, 高级里面选择 Maintenance 标签,勾选 Perform backup files health check 设置时间为每周6,健康检查任务不会独自执行,当有备份任务执行后,他会顺势检查是否当日有健康检查任务,然后执行。

最后不要设置任何的定时任务,需要通过windows 定时任务来调度。

创建一个 AutoBackup.bat 文件, 保存为 UTF-8 的编码格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@echo off
chcp 65001 > nul

echo [%date% %time:~0,8%] [1] 正在静默暂停 PrimoCache 任务 1...
echo [%date% %time:~0,8%] 执行命令: "C:\Program Files\PrimoCache\rxpcc.exe" pause -c 1 -s
"C:\Program Files\PrimoCache\rxpcc.exe" pause -c 1 -s

echo [%date% %time:~0,8%] [2] 触发 Veeam 后台无感备份...
start /b "" "C:\Program Files\Veeam\Endpoint Backup\Veeam.EndPoint.Manager.exe" /backup

echo [%date% %time:~0,8%] [3] 等待 60 秒让备份服务启动...
timeout /t 60 > nul

echo [%date% %time:~0,8%] [4] 正在监控后台备份状态 (每 60 秒刷新)...

:CHECK_LOOP
:: 检查是否存在 VeeamAgent 进程
tasklist /NH /FI "IMAGENAME eq VeeamAgent.exe" | find /i "VeeamAgent.exe" > nul

:: 如果找不到进程,说明备份结束,跳出循环
if %errorlevel% neq 0 goto BACKUP_DONE

echo [%date% %time:~0,8%] VeeamAgent.exe 正在后台辛勤工作,请耐心等待...
timeout /t 60 > nul
goto CHECK_LOOP

:BACKUP_DONE
echo [%date% %time:~0,8%] 检测到备份进程已退出,备份圆满结束!

echo [%date% %time:~0,8%] [5] 正在静默恢复 PrimoCache...
echo [%date% %time:~0,8%] 执行命令: "C:\Program Files\PrimoCache\rxpcc.exe" resume -c 1 -s
"C:\Program Files\PrimoCache\rxpcc.exe" resume -c 1 -s

echo [%date% %time:~0,8%] ==========================
echo [%date% %time:~0,8%] 任务全自动执行完毕!
echo [%date% %time:~0,8%] ==========================

win + R 输入 taskschd.msc, 创建一个新的计划任务。 选择 Create Task 不要选择 Create Basic Task

勾选 Run whether user is logged on or not 和 Do not sotre password 这样可以保证任务在后台静默执行。

勾选 Run with highest privileges 保证一管理员权限执行。

Action 标签中添加脚本路径 cmd.exe , Add arguments 填写 /c ""C:\Program Files\PrimoCache\scripts\AutoBackup.bat" >> "C:\Program Files\PrimoCache\scripts\backup_log.txt" 2>&1"

windows 通过桌面小组件显示内网服务器(pve)的磁盘状态信息

安装 Rainmeter 通过 vscode 打开 skins 所在的文件夹路径

创建新的 skins 文件夹作为根路径,创建二级文件夹作为组件,内部创建一个 .ini 文件 ,编码格式选为UTF-16LE

选择 refreash 重新加载组件, 通过 About Rainmeter 可以看见日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
[Rainmeter]
; This section contains general settings that can be used to change how Rainmeter behaves.
; 这是组件的全局刷新频率,单位是毫秒 (ms)。
Update=1000
; 这是为整个小组件设置一张底层背景图片。
; 需要特别注意的是前面的 #@#:这是 Rainmeter 的一个内置魔法变量。它代表了当前组件的资源目录(通常路径是 你的组件文件夹\@Resources)。所以这行代码的意思是:去 @Resources 文件夹里读取一张叫 Background.png 的图片作为整体背景。
Background=#@#Background.png
; 这决定了背景图片的拉伸和缩放模式。
; 在 Rainmeter 中,3 代表开启“九宫格缩放”(Scale with Margins)模式。这意味着当你的组件内容变多、尺寸变大时,背景图片不会像普通的图片那样直接被拉伸到变形,而是配合下面的 BackgroundMargins 来智能缩放。
BackgroundMode=3
; 这个设置是专门配合 BackgroundMode=3 使用的,它定义了背景图片的“保护边缘”(也就是九宫格缩放的边界)。
; 这四个数字分别代表:左, 上, 右, 下 的像素距离。
; 0,34,0,14 的意思是:背景图片的顶部 34 个像素、底部 14 个像素在任何情况下都不参与拉伸变形,只有中间的部分会被拉长去填满空间。这通常用来保证圆角背景框的四角和边框边缘永远保持清晰,不会因为内容变长而变得模糊或被拉长。
BackgroundMargins=0,34,0,14

; ==================================
; 1. 全局变量配置区
; ==================================
[Variables]
; Variables declared here can be used later on between two # characters (e.g. #MyVariable#).
fontName=Trebuchet MS
textSize=8
colorBar=235,170,0,255
colorText=255,255,255,205
; 你的 PVE IP 地址
PveIP=pve.iftrue.club
; 节点名称
Node=pve
; 你要监控的物理磁盘名称(如 sda, nvme0n1)
Disk=/dev/sdc
; 在这里填入你的完整 API Token 凭证
Token=root@pam!pveAPIToken=4d7c041b-3a49-46c2-b3df-46d7e011cdd5

; 字体与背景颜色设置
FontName=Microsoft YaHei
BgColor=20,20,20,220

; ==================================
; 2. 数据源提取 (Measures)
; ==================================
; ==================================
; 2. 数据源提取 (Measures)
; ==================================
[MeasureSmartAPI]
Measure=WebParser
URL="https://#PveIP#/api2/json/nodes/#Node#/disks/smart?disk=#Disk#"
Header="Authorization: PVEAPIToken=#Token#"
; 使用正向预查并且修复了空白符匹配 (\s*),防止吞掉温度数字。
; 强制按这个顺序抓取数据:1.温度 2.坏道 3.健康状态
RegExp="(?si)(?=.*?Current Drive Temperature:\s*(\d+))(?=.*?Elements in grown defect list:\s*(\d+))(?=.*?\"health\"\s*:\s*\"(.*?)\")"
UpdateRate=60

[MeasureTemp]
Measure=WebParser
URL=[MeasureSmartAPI]
; 对应正则第 1 个括号:温度
StringIndex=1

[MeasureDefects]
Measure=WebParser
URL=[MeasureSmartAPI]
; 对应正则第 2 个括号:坏道
StringIndex=2

[MeasureHealth]
Measure=WebParser
URL=[MeasureSmartAPI]
; 对应正则第 3 个括号:健康状态
StringIndex=3

; ; ==================================
; ; 3. 动态颜色告警逻辑 (Calc)
; ; ==================================
[MeasureDefectColor]
Measure=Calc
; 去掉方括号,避免初始加载时因空值报错
Formula=MeasureDefects
; 如果坏道数大于 0,数字变红;等于 0,保持绿色
IfAboveValue=0
IfAboveAction=[!SetOption MeterDefectValue FontColor "255,70,70,255"]
IfEqualValue=0
IfEqualAction=[!SetOption MeterDefectValue FontColor "100,255,100,255"]
DynamicVariables=1

; 温度动态颜色 (超过 50 度变黄,超过 60 度变红)
[MeasureTempColor]
Measure=Calc
; 去掉方括号,直接引用 Measure 名称
Formula=MeasureTemp
IfAboveValue=60
IfAboveAction=[!SetOption MeterTempValue FontColor "255,70,70,255"]
; 去掉方括号,避免空值报错
IfCondition=(MeasureTemp > 50) && (MeasureTemp <= 60)
IfTrueAction=[!SetOption MeterTempValue FontColor "255,200,50,255"]
IfCondition2=MeasureTemp <= 50
IfTrueAction2=[!SetOption MeterTempValue FontColor "255,255,255,255"]
DynamicVariables=1

; ----------------------------------
; STYLES are used to "centralize" options
; ----------------------------------

[styleTitle]
StringAlign=Center
StringCase=Upper
StringStyle=Bold
StringEffect=Shadow
FontEffectColor=0,0,0,50
FontColor=#colorText#
FontFace=#fontName#
FontSize=10
AntiAlias=1
ClipString=1

[styleLeftText]
StringAlign=Left
; Meters using styleLeftText will be left-aligned.
StringCase=None
StringStyle=Bold
StringEffect=Shadow
FontEffectColor=0,0,0,20
FontColor=#colorText#
FontFace=#fontName#
FontSize=#textSize#
AntiAlias=1
ClipString=1

[styleRightText]
StringAlign=Right
StringCase=None
StringStyle=Bold
StringEffect=Shadow
FontEffectColor=0,0,0,20
FontColor=#colorText#
FontFace=#fontName#
FontSize=#textSize#
AntiAlias=1
ClipString=1

[styleBar]
BarColor=#colorBar#
BarOrientation=HORIZONTAL
SolidColor=255,255,255,15

; ==================================
; 4. UI 界面渲染 (Meters)
; ==================================

; 标题栏
[MeterTitle]
Meter=String
MeterStyle=styleTitle
; Using MeterStyle=styleTitle will basically "copy" the
; contents of the [styleTitle] section here during runtime.
X=100
Y=12
W=190
H=18
Text="iSCSI Monitor (#Disk#)"

; --- 健康状态 ---
[MeterHealthLabel]
Meter=String
MeterStyle=styleLeftText
X=10
Y=40
W=190
H=14
Text="Health:"

[MeterHealthValue]
Meter=String
MeterStyle=styleRightText
MeasureName=MeasureHealth
X=200
Y=0r
; r stands for relative. In this case, the Y postition of meterValueCPU is 0 pixels
; below the Y value of the previous meter (i.e it's the same as in meterLabelCPU).
W=190
H=14
Text="%1"
AutoScale=1

; --- 硬盘温度 ---
[MeterTempLabel]
Meter=String
MeterStyle=styleLeftText
X=10
Y=58
W=190
H=14
Text="Temperature:"

[MeterTempValue]
Meter=String
MeasureName=MeasureTemp
MeterStyle=styleRightText
X=200
Y=0r
; r stands for relative. In this case, the Y postition of meterValueCPU is 0 pixels
; below the Y value of the previous meter (i.e it's the same as in meterLabelCPU).
W=190
H=14
Text="%1 °C"
AntiAlias=1

; --- 增长缺陷列表 (坏道) ---
[MeterDefectLabel]
Meter=String
MeterStyle=styleLeftText
X=10
Y=76
W=190
H=14
Text="Grown Defects:"

[MeterDefectValue]
Meter=String
MeasureName=MeasureDefects
MeterStyle=styleRightText
X=200
Y=0r
W=190
H=14
Text="%1"

.exe 和 .msi 区别

.exe 和 .msi 都是常见的可执行/安装包格式,但它们的本质定位、运行机制和管理方式有着巨大的区别。

简而言之:.msi 是标准的“数据配置包”,依赖系统服务来安装;而 .exe 是“独立的可执行程序”,安装逻辑全由开发人员自己写。

特性 .msi (Microsoft Installer) .exe (Executable)
本质类型 数据库/配置文件(包含文件列表、注册表项及安装指令) 二进制可执行程序(引导程序、独立应用或自定义安装包)
执行引擎 由 Windows 内置的 Windows Installer 服务 (msiexec.exe) 统一解析和执行 操作系统直接创建进程运行,内部逻辑由开发商自行编写(如 Inno Setup, NSIS, InstallShield)
标准与规范 极度标准统一,遵守微软严格的安装包规范 没有统一标准,界面、安装逻辑和卸载行为完全由开发者决定
静默安装支持 原生统一(通用命令 msiexec /i package.msi /quiet 依赖开发者的实现(命令参差不齐,如 /S, /silent, /q 等)
卸载与修复 原生支持回滚(Rollback)、修复(Repair)和干净卸载,系统会自动记录状态 依赖开发者编写的卸载程序(uninstall.exe),若没写好容易留有残留
企业运维(域控) 原生支持,可通过 Windows Group Policy (GPO) 或 Active Directory 批量静默部署 无法直接通过 GPO 原生推送,通常需要包装成 .msi 或借助第三方工具 (SCCM/Intune)

.msi 像是一张“安装指南与零件清单”

.msi 本质上是一个使用微软 OLE 复合文档格式存储的关系型数据库。

当你双击 .msi 时,实际上是系统自带的 Windows Installer 服务(msiexec.exe)读取了这个数据库。msiexec 负责创建目录、解压文件、写入注册表。

安全与回滚机制:如果在安装过程中出现错误(如断电、磁盘空间不足),msiexec 可以利用事务机制自动回滚,将系统还原到安装之前的状态,不会留下垃圾文件。

.exe 像是“自带所有工具的自定义工程队”

.exe 是真正的 Windows PE(可执行)文件。如果它是一个安装包,通常是开发者使用 NSIS、Inno Setup 或 Advanced Installer 等打包工具生成的。

当你双击 .exe 安装包时,它会运行开发者写好的代码逻辑:可能包含自定义的炫酷 UI 界面、解压进度条、甚至在后台安装其他捆绑软件。

灵活性与风险:.exe 非常灵活,可以做任何系统允许的事(比如安装前检查复杂的网络环境、预装驱动等)。但也正因如此,它缺乏统一规范,一旦安装过程崩溃,可能无法自动清理临时文件。

如果你要为企业/域控环境部署软件,或者需要标准的静默安装/干净卸载,优先选择 .msi。

如果软件需要复杂的预检逻辑、自定义界面,或者它本身就是一个无需安装的绿色单文件工具,则选择 .exe。

nvm安装nodejs报错

1
2
3
4
5
6
7
npm : File C:\nvm4w\nodejs\npm.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.co 
m/fwlink/?LinkID=135170.
At line:1 char:1
+ npm -v
+ ~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess

这是一个非常经典的 Windows PowerShell 权限报错。Windows 系统出于安全考虑,默认处于严格模式,禁止执行任何 .ps1 脚本文件(包括 Node.js / nvm-windows 生成的 npm.ps1 脚本)。你只需要修改一下 PowerShell 的执行策略(Execution Policy)就可以解除这个限制。

点击 Windows 开始菜单,搜索 PowerShell,然后在搜索结果中点击以管理员身份运行。在终端中复制并粘贴以下命令,然后回车:

1
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

系统会弹出一长串提示,询问你是否要更改执行策略。输入大写字母 Y 并按下回车键确认。

Trading 短语

  • the really nitty-gritty details
    nitty-gritty 被当作了形容词来使用,意思是最本质的、最繁杂的、最底层的。

  • stick through it a little bit
    是一句用来鼓励别人的口语表达,核心意思是:“再坚持一下”、“再忍耐一小会儿”。
    当有人在做一件困难、无聊、或者让人感到不适的事情,并想要放弃时,你就可以用这句话来鼓励他们继续坚持下去。
    Stick it out: 坚持到底(直到事情结束)。
    Stick with it: 继续做某事(不放弃)。
    Push through it / Get through it: 熬过去、克服困难。

  • if you start just by being okay
    如果你起步时仅仅是让自己保持还行

  • It’s a little bit more nuanced
    情况要稍微复杂/微妙一点。

  • I need to do a shout out first
    我得先公开感谢/致意一下某人,Shout out 指的是一种“公开的致敬或感谢”。它不仅仅是私下的谢谢,而是在有听众或观众的场合,通过点名的方式给予某人认可或信誉。

  • That’s what got me into being passionate about orderflow in the first place.”
    (这就是)最初让我对订单流产生浓厚兴趣的原因。
    Being passionate about: 对……充满激情/狂热。在交易语境下,这通常意味着你不仅仅是在玩玩,而是已经进入了“深度痴迷”和“钻研”的状态。
    In the first place: 当初、起初、最初。这个词组放在句尾,起到一种“追根溯源”的强调作用。
    Into (being): 这里通常接在 “get into” 或 “got me into” 之后,表示“开始进入某种状态”或“开始对某事感兴趣”

  • not in the matter of buyers and seller
    这不仅仅是关于‘买家’和‘卖家’谁多谁少的问题。

Three.js 基础

模型类型

.fbx

FBX 是 FilmBoX 这套软件所使用的格式,后改称 Motionbuilder。因为 Motionbuilder 扮演的是动作制作的平台,所以在前端的 modeling 和后端的 rendering 也都有赖于其它软件的配合,所以 Motionbuilder 在档案的转换上自然下了一番功夫。

FBX 最大的用途是用在诸如在 Max、Maya、Softimage 等软件间进行模型、材质、动作和摄影机信息的互导,这样就可以发挥 Max 和 Maya 等软件的优势。可以说,FBX 方案是非常好的互导方案。

.glTF

glTF 是一种可以减少 3D 格式中与渲染无关的冗余数据并且在更加适合 OpenGL 簇加载的一种 3D 文件格式。glTF 的提出是源自于 3D 工业和媒体发展的过程中,对 3D 格式统一化的急迫需求。如果用一句话来描述:glTF 就是三维文件的 JPEG ,三维格式的 MP3。在没有 glTF 的时候,大家都要花很长的的时间来处理模型的载入。

很多的游戏引擎或者工控渲染引擎,都使用的是插件的方式来载入各种格式的模型。可是,各种格式的模型都包含了很多无关的信息。就 glTF 格式而言,虽然以前有很多 3D 格式,但是各种 3D 模型渲染程序都要处理很多种的格式。对于那些对载入格式不是那么重要的软件,可以显著减少代码量,所以也有人说,最大的受益者是那些对程序大小敏感的 3D Web 渲染引擎,只需要很少的代码就可以顺利地载入各种模型了。

此外,glTF 是对近二十年来各种 3D 格式的总结,使用最优的数据结构,来保证最大的兼容性以及可伸缩性。这就好比是本世纪初 xml 的提出。glTF 使用 json 格式进行描述,也可以编译成二进制的内容:bglTF。glTF 可以包括场景、摄像机、动画等,也可以包括网格、材质、纹理,甚至包括了渲染技术(technique)、着色器以及着色器程序。同时由于 json 格式的特点,它支持预留一般以及特定供应商的扩展。

.obj

OBJ 文件是 Alias|Wavefront 公司为它的一套基于工作站的 3D 建模和动画软件”Advanced Visualizer”开发的一种标准 3D 模型文件格式,很适合用于 3D 软件模型之间的互导。目前几乎所有知名的 3D 软件都支持 OBJ 文件的读写。OBJ 文件是一种文本文件,可以直接用写字板打开进行查看和编辑修改。

物体 位移/缩放/旋转

1
2
3
4
5
6
7
8
cube.position.x = 0;
cube.position.set(0, 0, 0);

cube.scale.y = 1;
cube.scale.set(0, 1, 0);

cube.rotate.z = Math.PI / 4;
cube.rotate.set(0, 0, Math.PI / 4);

一个物体的 position/scale/rotate 属性描述一个物体的 位置/缩放/旋转,当这个物体再世界坐标系中,那么他的 位移/缩放/旋转 相对于世界坐标系。

如果物体在另一个物体中,那么他的 位移/缩放/旋转 相对于父元素的位置。

1
2
3
4
// 由于物体位置相对于父元素,因此cube在坐标原点
parentCube.add(cube);
parentCube.position.x = -3;
cube.position.x = 3;

画布适应窗口的变化

1
2
3
4
5
6
7
8
window.addEventListener("resize", () => {
// 更新摄像头宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
});

通过 GUI 快速调试参数

通过安装 dat.gui (不推荐)

1
import * as dat from "dat.gui";

新方法是使用 threejs 自带的 GUI (推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min";

const gui = new GUI();
const handle = {
click: () => console.log("click"),
fullscreen: () => console.log("fullscreen"),
};
// 添加两个按钮,点击和全屏
gui.add(handle, "click").name("点击");
gui.add(handle, "fullscreen").name("全屏");

//修改数值,最小值-10,最大值10,步长1
gui.add(cube.position, "x").name("x轴位置").min(-10).max(10).step(1);

//使用 folder 分组
let folder = gui.addFolder("按钮组");
folder.add(handle, "click").name("点击");
folder.add(handle, "fullscreen").name("全屏");

//事件
gui.add(handle, "click").onChange(noop).onFinishChange(noop);

//颜色
const colors = {
cubeColor: "#ff0000",
};
gui
.addColor(colors, "cubeColor")
.name("颜色")
.onChange(() => cube.material.color.set(val));

Geometry 几何体

顶点/索引

threejs 中使用 position类型化数组描述顶点的位置信息。三个为一组,绘制时会将一组顶点绘制为一个三角形,复杂的几何体也是由多个三角形构成。

顶点的排列顺序(绕序)影响绘制的效果,逆时针 (Counter Clockwise, CCW) 排列的面视为正面, 顺时针排列视为反面。模型情况下,背面不会渲染以便提高性能。

Three.js 使用相机的视角和顶点位置之间的关系来判断一个面是正面还是反面。当从相机位置看时,顶点按照逆时针排列的面会被认为是正面;相反,顺时针排列的面会被认为是背面。顶点顺序的判断依赖于面法向量(normal vector)。法向量是垂直于面的一个矢量,它通过面顶点的排列顺序来决定。如果法向量指向相机方向,面被认为是正面,否则是反面。

1
2
3
4
5
6
7
8
const position = new Float32Array([
1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0, -1, -1, 0,
]);
const geometry = new THREE.BufferGeometry();

const indices = new Uint16Array();
geometry.setAttribute("position", new THREE.BufferAttribute(position, 3));
const mesh = new THREE.Mesh(geometry, material);

但是查看 position 属性会发现有 6 个顶点,这时可以通过建立索引,共用顶点优化顶点数量。

1
2
3
4
5
6
// 去除掉可以公用的顶点,
const position = new Float32Array([1, 1, 0, -1, -1, 0, 1, -1, 0, -1, 1, 0]);

// 使用索引来描述
const indices = new Uint16Array([0, 1, 2, 0, 3, 1]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
顶点分组

为几何体的顶点分组,可以为每个组设置单独的材质。

1
2
3
4
geometry.addGroup(0, 3, 0); // 开始位置,数量,材质索引
geometry.addGroup(3, 3, 1);

const mesh = new THREE.Mesh(geometry, [material, material2]);
顶点转换

对几何体的顶点操作(position, rotate, translate)通常是不推荐的,这会改变几何体原有的顶点信息(attribute.position), 通常在物体(Mesh)上修改。

UV

UV 决定了 2D 纹理如何映射到 3D 空间中,U V 分别代表 2D 纹理的横纵坐标(为了与 3D 中的 X Y Z 坐标区分)。在模型建好之后会将纹理展开到 2D 平面中,这一过程叫做 UV 展开。常用的算法有投影展开法(Projection Mapping),迭代展开法(Iterative Unwrapping),Least Squares Conformal Mapping (LSCM) 和 Angle-Based Flattening (ABF) 等。

如果一个物体缺失 UV 信息,那么纹理将无法映射到物体表面。如果 UV 坐标没有覆盖(1,1)区域,剩余位置颜色会自动按 UV 坐标边缘采集。

法线

法线是垂直与平面的向量,决定了光纤如何反射。

1
2
3
4
5
6
7
8
9
10
// 法线辅助器
import { VertexNormalsHelper } from "three/examples/jsm/helpers/VertexNormalsHelper.js";
const geometry = new THREE.BufferGeometry();
// ...
// 自动计算法线
geometry.computeVertexNormals();
// ...
// 添加法线辅助器
const helper = new VertexNormalsHelper(mesh, 0.2, 0x00ff00);
scene.add(helper);
包围盒

包围盒用于可视化的检视物体,或用于碰撞检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fbxLoader.load(floorFbx, function (group) {
// 通过名称或ID查找到模型中指定的物体
const mesh = group.getObjectById(40);
// const mesh = group.getObjectByName('a23')

// 由于物体的父级可能存在几何变化导致与物体的尺寸不同,因此需要更新物体的世界矩阵
// 更新父级, 更新子集
mesh.updateWorldMatrix(true, true);

// 需要手动调用几何体包围盒计算函数
const geometry = mesh.geometry;
geometry.computeBoundingBox();

// 获取包围盒计算结果
const box = geometry.boundingBox;

// 使用世界矩阵更新包围盒
box.applyMatrix4(mesh.matrixWorld);

// 创建包围盒辅助器显示包围盒
const boxHelper = new THREE.Box3Helper(box, 0xff0000);

// 添加到场景中
scene.add(boxHelper);
});

多个物体包围盒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const mesh = group.getObjectById(40);
const mesh1 = group.getObjectById(41);
const mesh2 = group.getObjectById(42);

const boxUnion = new THREE.Box3();
[mesh, mesh1, mesh2].forEach((mesh) => {
mesh.updateWorldMatrix(true, true);
const geometry = mesh.geometry;
geometry.computeBoundingBox();
const box = geometry.boundingBox;
box.applyMatrix4(mesh.matrixWorld);
const boxHelper = new THREE.Box3Helper(box, 0xff0000);
scene.add(boxHelper);

boxUnion.union(box);
});

const boxHelper = new THREE.Box3Helper(boxUnion, 0xff0000);
scene.add(boxHelper);

使用 setFromObject,自动计算和世界轴对齐的一个对象 Object3D (含其子对象)的包围盒,计算对象和子对象的世界坐标变换。

1
2
3
4
5
6
7
8
9
10
const boxUnion = new THREE.Box3();
[mesh, mesh1, mesh2].forEach((mesh) => {
const box = new THREE.Box3().setFromObject(mesh);
const boxHelper = new THREE.Box3Helper(box, 0xff0000);
scene.add(boxHelper);
boxUnion.union(box);
});

const boxHelper = new THREE.Box3Helper(boxUnion, 0xff0000);
scene.add(boxHelper);
几何体居中/获取中心
1
2
3
4
5
6
7
8
9
// 会将几何体的中心放到世界中心
geometry.center();

// 但是Mesh对象可能有几何变换,导致物体看上去仍然偏移
// 需要重置Mesh的几何信息
mesh.position.set(0, 0, 0);

// 获取包围盒的中心点
const vec3 = box.getCenter(new THREE.Vector3());

边缘几何体/线框几何体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 通过名称或ID查找到模型中指定的物体
const mesh = group.getObjectById(40);

const geometry = mesh.geometry;

// 创建边缘几何体
const edgeGeometry = new THREE.EdgesGeometry(geometry);

// 创建线框几何体
// const wireGeometry = new THREE.WireframeGeometry(geometry);

// 创建线段材质
const lineMaterial = new THREE.LineBasicMaterial({
color: 0xffffff,
});

// 创建线段物体
const lineMesh = new THREE.LineSegments(edgeGeometry, lineMaterial);

// 由于新创建的物体丢失了源物体的世界矩阵信息,因此位置大小可能表现不同
// 需要使用源物体的世界矩阵信息重新赋值

// 更新源物体的世界矩阵信息
// mesh.updateWorldMatrix();

// 应用源物体的世界矩阵
lineMesh.matrix.copy(mesh.matrixWorld);

//将矩阵信息解构到物体的变换信息上
lineMesh.matrix.decompose(
lineMesh.position,
lineMesh.quaternion,
lineMesh.scale
);
scene.add(lineMesh);

材质与纹理

three.js 中的材质就是几何体表面的材料。所有材质均继承自 Material。ThreeJS 的材质分为:基础材质、深度材质、法向量材质、琥珀材质、冯氏材质、标准材质、着色器材质、基础线材质以及虚线材质。材质就像物体的皮肤,让几何体看起来像金属还是木板,是否透明,什么颜色。

纹理的基类是 Texture,通过给其属性 Image 传入一个图片从而构造出一个纹理。纹理是材质的属性,材质和几何体 Gemotry 构成 Mesh

材质基类 Material
  • transparent: 定义此材质是否透明,可以配合 opacity 使用,控制透明程度。
纹理基类(Texture)
  • colorSpace

    1.THREE.NoColorSpace: 这意味着没有应用任何特定的颜色空间,纹理的颜色数据会被原样使用。这个选项通常用于已经处于需要的颜色空间中的纹理,或者那些不依赖于颜色空间的特定用途。

    2.THREE.SRGBColorSpace:在此颜色空间中,颜色数据以 SRGB 格式存储。SRGB 是一个 RGB 标准,它试图将色彩的表现和人眼感知到的颜色更好地匹配。相对于线性颜色空间,SRGB 颜色空间在暗区提供了更多的颜色级别。使用此颜色空间时,需要注意图像的颜色可能会被转换为非线性的 SRGB 格式。

    3.THREE.LinearSRGBColorSpace 这也是一个以 SRGB 格式存储颜色数据的颜色空间,但颜色数据被当作线线性性数据处理。在进行计算和处理时,这种颜色空间可以提供更精确的结果。但是由于人眼对光线的感知,50%感觉的亮度只需要 18%的发光强度,这可能导致物体颜色过浅。

  • needsUpdate 指定需要重新编译材质。如果动态设置纹理需要将此属性设置为true

基础材质 MeshBasicMaterial
  • map: 颜色贴图。默认会随几何体的大小拉伸。 开启 transparent 属性,会影响有透明通道图片的效果
MeshMatcapMaterial

由一个材质捕捉(MatCap,或光照球(Lit Sphere))纹理定义。mapcap 编码了光照,颜色等信息,因此不对光照做出反应。可以投射阴影,但是不会接受阴影。
是一个低成本实现光照效果得材质,缺点是固定了光照信息,不能对光照反应。

MeshLambertMaterial

该材质使用基于非物理的 Lambertian 模型来计算反射率。 这可以很好地模拟一些表面(例如未经处理的木材或石材),但不能模拟具有镜面高光的光泽表面(例如涂漆木材)

MeshPhongMaterial

该材质使用非物理的 Blinn-Phong 模型来计算反射率。可以模拟高光的或玻璃效果,但是由于不是物理模型,因此玻璃效果不能与场景中其他物体作用,只能通过透明度设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现玻璃效果,只限于与环境光线相互作用
// 加载环境贴图是必须的,映射模式需要修改为折射
envMap.mapping = THREE.EquirectangularRefractionMapping;
// scene.environment = envMap;
// scene.background = envMap;

const box = new THREE.BoxGeometry(1, 1, 1);
const bujianMatertal = new THREE.MeshPhongMaterial({
refractionRatio: 0.7, // 空气的折射|反射率
// 当设置为 THREE.EquirectangularRefractionMapping 值为折射系数
// 越高越像玻璃
reflectivity: 0.99,
envMap: envMap,
});

const boxMesh = new THREE.Mesh(box, bujianMatertal);

// 必须要有环境光,与环境光相互作用
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);

FOG 雾

通常会把背景颜色和雾的颜色设置成相同颜色,可以让雾融入到场景中

镭射光纤,选择物体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
window.addEventListener("pointermove", onPointerMove);
function onPointerMove(event) {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

function rayTest() {
raycaster.setFromCamera(pointer, camera);

const intersects = raycaster.intersectObjects(mesharr);
if (intersects.length === mesharr.length) return;

// intersects 是一个按深度排序的数组
for (let i = 0; i < intersects.length; i++) {}
}

function animate() {
rayTest();
//...
}

补间动画

THREE.js 集成了 Tween 动画库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Tween, Easing } from "three/examples/jsm/libs/tween.module.js";

const tween = new Tween(mesh.position);
// 移动到某个位置
tween.to({ x: 2 }, 1000);
// 位置更新时回调
tween.onUpdate(() => {
// console.log(mesh.position.x);
});
//重复次数
tween.repeat(10);
// 是否往返
tween.yoyo(true);
// 是否延迟
tween.delay(1000);

// 设置缓动
tween.easing(Easing.Bounce.In);
tween.start();

// 动画链
// tween.chain(tween2);
// tween2.chain(tween);

// 更新动画
function animate() {
tween.update();
}

FocalBoard 私有化部署

最后更新

2024-12-13

官方安装包

安装 官方包, [官方文档]

1
2
3
wget https://github.com/mattermost/focalboard/releases/download/v0.15.0/focalboard-server-linux-amd64.tar.gz
tar -xvzf focalboard-server-linux-amd64.tar.gz
sudo mv focalboard /opt

安装数据库

1
sudo apt install postgresql postgresql-contrib

以 postgres 用户创建一个新的数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sudo --login --user postgres
psql

CREATE DATABASE <your-db-user>;
CREATE USER <your-db-user> WITH PASSWORD <'your-password'>;

# 查看schema列表
\dn

# 查看用户列表
\du

# 查看数据库列表

\l+

# 赋予用于操作数据库的所有权限
GRANT ALL PRIVILEGES ON DATABASE <your-db> TO <your-db-user>;

GRANT ALL ON SCHEMA public TO <your-db-user>;

\c <your-db-user> postgres
# You are now connected to database "your-db-user" as user "postgres".

# 赋予有用操作public schema的权限
GRANT ALL ON SCHEMA public TO <your-db-user>;

# 退出 postgres 用户
\q

exit

配置链接信息

1
2
3
4
vi /opt/focalboard/config.json

"dbtype": "postgres",
"dbconfig": "postgres://your-db-user:your-db-password@localhost/boards?sslmode=disable&connect_timeout=10"

启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo vi /lib/systemd/system/focalboard.service

[Unit]
Description=Focalboard server

[Service]
Type=simple
Restart=always
RestartSec=5s
ExecStart=/opt/focalboard/bin/focalboard-server
WorkingDirectory=/opt/focalboard

[Install]
WantedBy=multi-user.target
1
2
3
4
5
sudo systemctl daemon-reload
sudo systemctl start focalboard.service
sudo systemctl enable focalboard.service

curl localhost:8000

nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
server {
listen 443;
listen [::]:443;
server_name focal.iftrue.me;

location ~ /ws/* {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 50M;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
client_body_timeout 60;
send_timeout 300;
lingering_timeout 5;
proxy_connect_timeout 1d;
proxy_send_timeout 1d;
proxy_read_timeout 1d;
proxy_pass http://192.168.48.148:8000;
}

location / {
client_max_body_size 50M;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
proxy_read_timeout 600s;
proxy_cache_revalidate on;
proxy_cache_min_uses 2;
proxy_cache_use_stale timeout;
proxy_cache_lock on;
proxy_http_version 1.1;
proxy_pass http://192.168.48.148:8000;
}
}

FAQ

  • error [2025-03-17 04:28:23.400 Z] Table creation / migration failed caller=”sqlstore/sqlstore.go:75” error=”pq: permission denied for schema public”

    参照数据库配置步骤,需要赋予用户操作 public schema 的权限

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信