MusicFree PikPak插件

背景

开了会员却还是有很多歌不能听,需要各个app跳转。还是自建曲库算了,正好熟悉一下Javascript和Typescript。

app用的musicfree,插件自己写了一个pikpak的dav插件。

因为pikpak的原因,插件使用门槛不低。

使用

仓库链接的readme界面即可。

  1. musicfree安装插件,插件地址:pikpak-dav

  2. 在插件管理配置用户变量(需要提前在pikpak的设置栏打开实验性功能拿到密码等信息)

  3. 歌曲放到pikpak的/music目录下,根目录设置为/music

  4. 可以点击导入歌单,会将目录下所有内容导出为一个歌单。或者从热门歌单-PikPak-全部歌曲实时获取全部歌单,收藏一下后面可以快速访问

自建曲库

如果已有的歌单数目非常大,先在你主用app里面把歌曲下载下来再分类后,上传到pikpak。麻烦但却是建自己曲库最快的办法。

关于mgg转ogg详见频道

引用

Java正则匹配失效

背景

最近生产上出现一个正则匹配失效的情况,排查后发现是因为包含终止符NEL,需要用DOTALL模式匹配。原本是以为客户错误输入导致,再深入排查发现是由于utf8字符错误解码成iso8859导致。

Java: jdk8

排查流程

问题复现

省略掉排查日志,本地复现的步骤,这里直接用抽象的核心问题代码复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void bugRecur(){
// 模拟实际情况中的utf8字节数组
byte[] bugBytes=new String("\r\n\r\n公司123\r\n").getBytes(StandardCharsets.UTF_8);
byte[] normalBytes=new String("\r\n\r\n普通字符123\r\n").getBytes(StandardCharsets.UTF_8);

// 模拟业务需要,以iso8859-1编码将字节数组转回字符串(实际上上面和下面在实际业务的不同系统做的)
String bugStr=new String(bugBytes,StandardCharsets.ISO_8859_1);
String normalStr=new String(normalBytes,StandardCharsets.ISO_8859_1);

// 进行正则匹配
String patternStr = "\\r\\n\\r\\n(.*?)\\r\\n";
Pattern pattern = Pattern.compile(patternStr);
Matcher bugMatcher = pattern.matcher(bugStr);
Matcher normalMatcher = pattern.matcher(normalStr);

System.out.println("问题字符匹配结果: " + bugMatcher.find());
System.out.println("普通字符匹配结果: " + normalMatcher.find());
}

结果:

1
2
问题字符匹配结果: false
普通字符匹配结果: true

通过上面代码很容易发现两段字符串一个能匹配一个不能匹配明显有问题,且应该就是字符集不一致导致的问题,而且此问题不是必现。实际上相关的业务代码之前仅处理英文不处理中文所以一直没有问题,直到最近业务改造有了中文且通过了联调测试阶段一直到生产才发现问题。

问题根源

既然发现了是字符集问题,尝试去找到问题根源

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
private static void deepReason(){
// 模拟实际情况中的utf8字节数组
byte[] bugBytes=new String("\r\n\r\n好公司123\r\n").getBytes(StandardCharsets.UTF_8);
byte[] normalBytes=new String("\r\n\r\n普通字符123\r\n").getBytes(StandardCharsets.UTF_8);

// 模拟业务需要,以iso8859-1编码将字节数组转回字符串(实际上上面和下面在实际业务的不同系统做的)
String bugStr=new String(bugBytes,StandardCharsets.ISO_8859_1);
String normalStr=new String(normalBytes,StandardCharsets.ISO_8859_1);


System.out.println("错误编码后的问题字符串字节数组: "+ Arrays.toString(bugStr.getBytes(StandardCharsets.ISO_8859_1)));
System.out.println("错误编码后的问题字符串字节数组: "+ Arrays.toString(normalStr.getBytes(StandardCharsets.ISO_8859_1)));
// 这里是往控制台输出的结果,一般流会通过utf8编码传输,对分析函数内部行为没什么意义
// System.out.println("错误编码后的问题字符串: "+bugStr);

// 更关键的是看字符串在java内存中的utf16编码后的结果
// System.out.println("错误编码后的问题字符串在java内存中的utf16编码字符数组: "+ Arrays.toString(bugStr.toCharArray())); // 这一行会输出失败
System.out.println("错误编码后的问题字符串在java内存中的utf16编码字节数组: "+ Arrays.toString(bugStr.getBytes(StandardCharsets.UTF_16)));
// System.out.println("错误编码后的普通字符串在java内存中的utf16编码字符数组: "+ Arrays.toString(normalStr.toCharArray())); // 这一行会输出失败
System.out.println("错误编码后的普通字符串在java内存中的utf16编码字节数组: "+ Arrays.toString(normalStr.getBytes(StandardCharsets.UTF_16)));

String patternStr = "\\r\\n\\r\\n(.*?)\\r\\n";
Pattern pattern = Pattern.compile(patternStr);
Matcher bugMatcher = pattern.matcher(bugStr);
Matcher normalMatcher = pattern.matcher(normalStr);

System.out.println("问题字符匹配结果: " + bugMatcher.find());
System.out.println("普通字符匹配结果: " + normalMatcher.find());

}

结果输出:

1
2
3
4
5
6
7
错误编码后的问题字符串字节数组: [13, 10, 13, 10, -27, -91, -67, -27, -123, -84, -27, -113, -72, 49, 50, 51, 13, 10]
错误编码后的问题字符串字节数组: [13, 10, 13, 10, -26, -103, -82, -23, -128, -102, -27, -83, -105, -25, -84, -90, 49, 50, 51, 13, 10]

错误编码后的问题字符串在java内存中的utf16编码字节数组: [-2, -1, 0, 13, 0, 10, 0, 13, 0, 10, 0, -27, 0, -91, 0, -67, 0, -27, 0, -123, 0, -84, 0, -27, 0, -113, 0, -72, 0, 49, 0, 50, 0, 51, 0, 13, 0, 10]
错误编码后的普通字符串在java内存中的utf16编码字节数组: [-2, -1, 0, 13, 0, 10, 0, 13, 0, 10, 0, -26, 0, -103, 0, -82, 0, -23, 0, -128, 0, -102, 0, -27, 0, -83, 0, -105, 0, -25, 0, -84, 0, -90, 0, 49, 0, 50, 0, 51, 0, 13, 0, 10]
问题字符匹配结果: false
普通字符匹配结果: true

分析上面的结果前需要知道一个前提,字符串在Java内部使用的utf16进行编码保存,pattern.matcher(bugStr)正则去匹配时,matcher是按String中的Unicode单元操作,也就是字符char(暂不考虑代理对)

回到字节数组的分析上,开头的[-2, -1]是0xFE 0xFF表示UTF-16 BOM(大端)说明后续每两个字节代表一个char,可以发现原来的单字节iso8859编码内容在Java的String里面是由utf16两字节表示的,且第一个字节都是用0补上。

所以原输入”公“的unicode为U+516C -> utf8编码传输到java内的字节流数据是”E5 85 AC“,转为对应的字节数组是[-27, -123, -84],这个字节数组如果按照utf8解码则会将其作为整体解析为字符“公”,但如果按iso8859解码则是一个字节一个字节解析,结果没意义,但也没有变更整个字节流结果。但是当把这个解析结果作为String对象存在内存里面时,“公”变成的三个无意义字符,会用utf16将其一个个编码。最终结果可以看到除了最前面两字节添加的标识,是在原字节基础上补一个字节,内容为0。utf16将iso8859解码的字符又编码保存到内存,此时新的字符的字节数组最终长度是原长度*2+2

在内存中调用matcher方法时,匹配的便是以[0, 13],[0, 10]这样两字节为一单位char的字符,而恰巧[0,-123]以utf16解码结果是0x85是换行终止符,也就导致输入的正常中文在一系列编码解码后的字符串,经过正则匹配时匹配失败,因为默认情况“.”匹配除了终止符的任意字符

1
2
3
4
5
6
7
8
Java byte: -123
无符号值: -123 & 0xFF = 133
十六进制: 133 = 0x85

字节数组: [0, -123]
十六进制: [0x00, 0x85]
UTF-16解码: (0x00 << 8) | 0x85 = 0x0085
结果: Unicode码点 U+0085 代表:NEL (Next Line) 终止符

结论:由于中文字符“公”的utf8编码的三个字节的某个字节用byte表示是-123,经过iso8859单字节解码为一个无意义字符完整保留,而该无意义字符char在java内存里面是以utf16编码保存(用byte表示则是[0,-123],转为unicode则是U+0085)恰好是终止符NEL,导致最后在匹配正则时匹配失败

解决办法

  1. 字符集一致:这个是最好的。但实际上之前这边采用iso8859解码就是因为采用utf8有问题才换的。具体原因是在业务中,字符串是作为表单的一部分传入且表单还有二进制的文件,且传入的字节数组是完整的表单数据,如果用utf8解码会损坏文件。为什么不用Spring提供的注解或方法提取表单?因为业务代码实际在非mvc的reactor网关,用的ModifyRequestBodyGatewayFilterFactory类直接拿的二进制数组进行处理

  2. 采用正则匹配DOTALL模式

    1
    2
    3
    Pattern pattern = Pattern.compile(patternStr);
    // 改为使用DOTALL模式
    Pattern compatiblePattern = Pattern.compile(patternStr, Pattern.DOTALL);

此种方式需要重新测试是否有异常情况

找到基本中文字符中存在这样问题的中文

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
public static void wordWith0x75(){
int count=0;
// CJK统一汉字基本区:U+4E00 ~ U+9FFF
for (int codePoint = 0x4E00; codePoint <= 0x9FFF; codePoint++) {
char ch = (char) codePoint;
String s = String.valueOf(ch);

byte[] bytes = s.getBytes(StandardCharsets.UTF_8);

// UTF-8 编码的汉字应该是 3 字节
if (bytes.length == 3) {
boolean has85 = false;
for (byte b : bytes) {
// Java byte 范围是 -128~127,需要转成无符号
int unsigned = b & 0xFF;
if (unsigned == 0x85) {
has85 = true;
break;
}
}
if (has85) {
// 打印字符 + UTF-8字节的十六进制表示
System.out.printf("U+%04X %s -> %s%n", codePoint, ch, bytesToHex(bytes));
count++;
}
}
}
System.out.printf("基本中文共有%d个符合要求",count);
}

private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < bytes.length; i++) {
if (i > 0) sb.append(" ");
sb.append(String.format("0x%02X", bytes[i] & 0xFF));
}
sb.append("]");
return sb.toString();
}

基础中文共有643个符合要求

kindle接入legado书源

背景

  • 阅读app自带的web服务不支持kindle,无法网页浏览,可以通过legado_kindle_bookshelf项目转发一下。和微信读书不一样,这个只是中转,真正的数据源是你的手机阅读app。

环境

  • kpw6
  • 公网ip

步骤

1
2
3
4
5
6
# 1.克隆仓库
git clone https://github.com/eastcukt/legado_kindle_bookshelf

# 2.添加nginx配置,参考下面的示例,将项目作为静态页面用nginx反代即可
sudo nano /etc/nginx/conf.d/book.conf

注意:不要给页面上https,保持http使用。

nginx配置参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
# 改成你的服务器公网 IP
server_name your_ip_address;

# 网站根目录,也就是你上面克隆仓库的地址
root /path/to/your/project;
index index.html;

# 静态资源访问
location / {
# 按请求文件或目录查找,找不到则返回 404
try_files $uri $uri/ =404;
}

# 可选:如果你有 API 或后端,需要反向代理,可在此添加
# location /api/ {
# proxy_pass http://127.0.0.1:5000/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# }
}

安装NVIDIA驱动和CUDA

背景

Linux环境跑模型项目需要安装CUDA依赖

环境

  • Debain12
  • 3070

安装cuda和驱动

官方链接:https://developer.nvidia.com/cuda-downloads

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo add-apt-repository contrib
sudo apt-get update

# cuda工具包
sudo apt-get -y install cuda-toolkit-12-6
# 驱动
sudo apt-get install -y cuda-drivers

# 改下环境变量
nano ~/.bashrc
# 加入下面内容
export PATH=/usr/local/cuda/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH

# 改完应用一下
source ~/.bashrc

# 查看是否安装成功
nvidia-smi

# 查看驱动编译器是否安装成功
nvcc --version

安装cuDNN

安装完上面的驱动后,cuDNN需要单独安装,官方链接(https://developer.nvidia.com/cudnn-downloads)

1
2
# 安装cuda12
sudo apt-get -y install cudnn-cuda-12

安装实时语音转文字WhisperStream

背景

想要实现实时语音转文字,并需要通过web共享出去,WhisperStreamWeb项目非常合适

环境

  • Debain12
  • 3070
  • CUDA:12.7
  • cuDNN:9.6.0

步骤

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

# 安装ffmpeg依赖
sudo apt install ffmpeg


# 建好conda虚拟环境,需要安装miniconda
conda create --name tts python=3.10.6

# 激活虚拟环境
conda activate tts

# 拉取目标仓库
git clone https://github.com/QuentinFuxa/whisper_streaming_web && cd whisper_streaming_web

# 安装依赖
pip install librosa soundfile

# Whisper streaming web required dependencies
pip install fastapi ffmpeg-python

# 安装后端依赖,这里我用的本地GPU所以选择faster-whisper, 需求有前置要求要安装好cuda,参考[官方链接](https://developer.nvidia.com/cuda-downloads) 或[安装cuda](luyublog.com/InstallNvidiaDriverAndCUDA)

# 安装后端
pip install faster-whisper

# 安装音频控制器用来检测是否有声音在说话
pip install torch torchaudio

# 安装uvicorn作为服务器
pip install uvicorn
pip install websockets

# 检测下是否正常输出
python whisper_fastapi_online_server.py -h

# 启动:
python whisper_fastapi_online_server.py --host 0.0.0.0 --port 8000 --model large-v3-turbo --backend faster-whisper --vac --vad

# 带上 --log-level DEBUG参数查看debug信息
--log-level DEBUG

报错

缺失ffmpeg

描述:FileNotFoundError: [Errno 2] No such file or directory: ‘ffmpeg’

原因:系统中缺少实际的 ffmpeg 可执行文件。ffmpeg-python 是 Python 的一个接口,用于与 ffmpeg 交互,但它需要依赖操作系统上安装的 ffmpeg 二进制程序。

解决:

1
2
# 安装ffmpeg
sudo apt install ffmpeg

libcudnn_ops.so无法加载

描述:Unable to load any of {libcudnn_ops.so.9.1.0, libcudnn_ops.so.9.1, libcudnn_ops.so.9, libcudnn_ops.so}
Invalid handle. Cannot load symbol cudnnCreateTensorDescriptor

原因:找不到libcudnn_ops.so系列库。缺乏正确版本的 cuDNN,CUDA Toolkit和cuDNN不是一个东西,CUDA Toolkit默认不会包含cuDNN库

解决:安装cuDNN

电脑浏览器访问本地端口识别失败

原因:

  • 本地ip是http,浏览器默认禁止了麦克风访问权限
  • 如果不是本机启动,需要将ws地址改为目标ip地址

解决:

  • 浏览器访问”edge://flags/“ 找到Insecure origins treated as secure特性,把地址加入白名单

  • 把ws地址改为实际地址

手机浏览器识别失败

原因:

  • 手机浏览器获取输入设备失败,可能是因为http的原因,尝试换成https

解决: 使用nginx开启https反代处理成功

Git多用户环境隔离提交

多用户仓库提交

背景

想要将开源项目和日常使用的两个账号区分开来,各自推送代码。

具体原理就是通过token结合远程仓库地址和本地用户信息配置完成git隔离。

步骤

  1. github个人页面选择setting->developer settings->personal access tokens创建两个账号各自的token,token记得赋予读写权限

  2. 修改工程的远程地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 查看一下当前仓库配置信息,主要是看下源仓库远程链接
    git config --local -l

    # 删除当前关联关系
    git remote rm origin

    # 关联新格式的远程仓库
    # 举个例子,假如我的仓库为https://github.com/luyublog/myrepo.git
    # 那么替换后的格式即为https://luyublog:token@github.com/luyublog/myrepo.git
    git remote add origin https://your_user_name:your_token@github.com/your_user_name/your_repo_name.git

    # 拉取仓库信息
    git fetch

    # 重设一下跟踪分支,这里我直接关联的main分支,创建的早的可能是master
    git branch --set-upstream-to=origin/main

遇到的问题

pull时报错:当前分支没有跟踪信息

这个错误信息表示当前分支没有和任何远程分支建立跟踪关系,所以Git不知道你想要从哪个远程分支pull更新,关联一个远程分支即可,这里我直接关联main分支

1
git branch --set-upstream-to=origin/main

git设置的本地提交名称失效

具体现象就是即使通过git config user.name name设置了名称,最后提交人员也是github的用户名。不想解决。

一些常用命令

1
2
3
4
5
6
7
8
9
# 查看仓库的本地配置
git config --local -l

# 全局配置
git config --global -l

# 系统级配置
git config --system -l

linux上启用comfast无线网卡

背景

用u盘给电脑装上debian12后发现之前的comfast无线网卡无法正常工作,之后查看商品页面发现确实没写支持linux设备,所以只能试试自己收集驱动启动

环境

  • 系统: debian12
  • 无线网卡型号:comfast 926AC

步骤

1. 收集固件

这里建议先跟随这篇文章查询一下自己的网卡信息,如果有具体信息则可以不用往下看,直接按文章操作即可。

但我的ID是0e8d:2870,在网站上只有一行描述:”This is the emulated CD-ROM driver storage device that comes with some WiFi adapters.”

CD-ROM一般自动挂载在/media下,我检查后发现里面是.exe后缀的驱动安装程序,那就只能自行寻找驱动了。

通过谷歌获取到0e8d:2870对应的芯片是Mediatek MT7612U,通常应该加载的是mt76系列模块

随后通过命令安装对应模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装通用模块,这里如果找不到包需要额外添加non-free软件源
sudo apt install firmware-misc-nonfree

# 加载我需要的mt76模块 MT7612U 属于 MT76x2 系列设备。
sudo modprobe mt76x2u

# 查看模块是否成功加载
lsmod | grep mt76

# 查看设备识别信息
sudo dmesg | grep mt76

# 最关键的查看是否出现无线网卡
ip link show

我运行ip link show后并没有看到对应网卡出现,通过lsusb发现网卡还是被识别为”Bus 001 Device 011: ID 0e8d:2870 MediaTek Inc. Љ” 和之前看到的一样,还是只是一个CD-ROM设备,也就是说可能光有驱动还不行,还要从软件层面开启

2. 转换网卡形态

目前虽然有了对应驱动,但是网卡还是被识别为CD-ROM,通过CHATGPT了解到应该是因为作为免驱无线网卡,默认以CD-ROM模式启动,我们需要将其转换为网卡形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装对应工具
sudo apt install usb-modeswitch

# 切换形态,以我为例,我的设备ID为0e8d:2870,那么我的切换命令为
sudo usb_modeswitch -v 0e8d -p 2870 -R

# 查看网卡信息,这时应该有一个新网卡出现
ip link show

# 再运行lsusb应该就会看到设备名称变更了 我的从"MediaTek Inc. Љ"变为"MediaTek Inc. MT7612U 802.11a/b/g/n/ac Wireless Adapter"
lsusb

# 也可以查看下设备识别日志里的相关信息
sudo dmesg | tail -n 50

3. 激活并连接wifi

网卡转换后可以插拔后运行”ip link show”来检查识别是否正常,正常的话就可以激活并连接wifi了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 激活对应网卡,这里的wln0是你运行ip link show对应的无线网卡名称
sudo ip link set wln0 up

# 安装网络管理工具,方便连接wifi
sudo apt install network-manager

# 查看可连接wifi列表
sudo nmcli dev wifi list

# 连接
sudo nmcli dev wifi connect "wifi名称" password "密码"

# 查看连接状态和网络接口信息:
nmcli connection show
nmcli dev status

4. 开不了机

有一段时间没运行debian,今天开机发现显卡在工作但无输出,显示器不显示,怀疑根本没开机,把无线网卡拔掉重启下就正常了,暂时没时间就不追究根因了

win11安装双系统

背景

记录一下win11装双系统流程

参考

根据Windows10+Ubuntu18.04双系统简单安装指北跟随操作即可

步骤

  1. windows系统下找到目标盘压缩卷
  2. 使用rufus制作启动盘
  3. 进入bois选择u盘启动
  4. 安装系统,选择graphic install,分盘方式选新手推荐
  5. bois按需调整启动顺序

问题

无线网卡没网

一般的windows平台免驱无线网卡都不支持linux,改为网线连接后执行”sudo dhclient enp4s0”通过dhcp自动获取网络

尝试激活无线网卡

参考:linux启用comfast无线网卡

apt更新失败

默认源来自于iso镜像,换为网络源后再update

创建news2tg的docker镜像

创建镜像

1.创建dockerfile文件

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
# 使用多阶段构建
FROM rust:latest AS rust-builder
WORKDIR /usr/src/news2tg

# 安装git,protobuf-compiler等工具
RUN apt-get update && apt-get install -y git && apt-get install -y protobuf-compiler
RUN protoc --version

# 从GitHub克隆Rust仓库
RUN git clone https://github.com/cheedonghu/news2tg.git .
RUN cargo build --release

FROM python:3.11
WORKDIR /app

RUN apt-get update && apt-get install -y git

# 从GitHub克隆Python仓库
RUN git clone https://github.com/cheedonghu/hacker-news-digest.git .

# 安装Python依赖
RUN pip install --no-cache-dir -r ./page_content_extractor/requirements.txt

# 从Rust构建阶段复制编译好的二进制文件
COPY --from=rust-builder /usr/src/news2tg/target/release/news2tg /app/news2tg

# 创建配置文件目录
RUN mkdir /config

# 创建日志目录
RUN mkdir /logs

# 设置环境变量指向配置文件位置
ENV RUST_CONFIG_PATH=/config/config.toml

# 暴露端口(其实没必要暴露)
EXPOSE 50051

# 下载 wait-for-it.sh 脚本
RUN curl -o /usr/local/bin/wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh

# 启动命令
CMD ["sh", "-c", "python -m page_content_extractor.main & wait-for-it.sh localhost:50051 -- ./news2tg -c $RUST_CONFIG_PATH >> /logs/news2tg.log 2>&1"]

2.构建镜像

1
2
# 最好在网络通畅环境下运行,避免网络问题
docker build -t news2tg .

3.(可选)推送到dockerhub

这里我build机器和运行的机器不一致所以需要推送后在另一台机器拉取镜像运行。

1
2
3
4
5
6
7
8
# 先登录
docker login

# 再给镜像加上标签
docker tag news2tg cheedonghu/news2tg:latest

# 推送镜像
docker push cheedonghu/news2tg:latest

其他方式:github workflow

  1. 在 GitHub 仓库的 Settings -> Secrets 页面中添加以下 Secrets
  • DOCKER_USERNAME:您的 Docker Hub 用户名
  • DOCKER_PASSWORD:您的 Docker Hub 密码或访问令牌
  • 确保 Dockerfile 位于仓库根目录中
  1. 选择项目下Actions->Docker image->Configure

  2. 创建yml

    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
    name: News2tg Docker Image CI

    on:
    push:
    branches: [ "main" ]

    jobs:

    build:

    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
    uses: actions/checkout@v2

    # 登录到 Docker Hub(需要在仓库的 Secrets 中配置 DOCKER_USERNAME 和 DOCKER_PASSWORD)
    - name: Login to Docker Registry
    uses: docker/login-action@v2
    with:
    username: ${{ secrets.DOCKER_USERNAME }}
    password: ${{ secrets.DOCKER_PASSWORD }}

    # 使用 docker/build-push-action 构建并推送镜像
    - name: Build and push Docker image
    uses: docker/build-push-action@v3
    with:
    context: .
    file: ./Dockerfile
    platforms: linux/amd64
    push: true
    tags: ${{ secrets.DOCKER_USERNAME }}/news2tg:latest

使用镜像

步骤

1
2
3
4
5
6
7
8
9
10
11
# 1. 创建docker-compose.yml 按需复制下面的yml
nano docker-compose.yml

# 2. 创建挂载的文件夹
mkdir ./logs && mkdir ./config

# 3. 把配置文件放在config文件夹下
nano ./config/config.toml

# 4. 运行
docker compose up -d

使用本地构建的镜像

1
2
3
4
5
6
7
services:
news2tg:
image: news2tg
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

使用dockerhub上的镜像

1
2
3
4
5
6
7
services:
news2tg:
image: cheedonghu/news2tg:latest
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

更新镜像

1
2
3
4
5
# 更新镜像至最新版本
1. sudo docker compose pull news2tg

# 在docker-compose.yml同级目录执行重建并启动
2. sudo docker compose up -d

其他方式

如果是本机直接跑则构建运行即可。

1
2
3
4
5
6
7
8
9
10
services:
news2tg:
build:
context: .
dockerfile: path/to/your/Dockerfile
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

问题

docker启动失败

原因:1.目录权限问题导致配置映射失败 2.老配置文件导致启动失败

SpringAI简单使用

SpringAI

介绍

SpringAI

了解原因

  1. AI趋势
  2. Spring生态

特性

  1. 支持大量模型OpenAI, Microsoft, Amazon, Google, and Huggingface.
  2. 支持大量模型输出:问答,做图,语音处理等
  3. 方便使用的已封装API接口:用户交互api,模型交互api
  4. 支持大量的向量数据库及相关交互
  5. 支持模型函数调用
  6. SpringBoot自动配置支持
  7. ETL(Extract, Transform,and Load) framework for Data Engineering

使用案例

  1. 基于大模型的问答
  2. 基于大模型的文生图

基于大模型的文档问答

实现方式

  1. Fine Tuning

  2. Prompt Stuffing

  3. Function Calling

Fine Tuning

将目标数据与模型一起训练

优点:

  • 与模型一体,不用额外处理

缺点:

  • 重新训练非常耗费资源

Prompt Stuffing

Prompt填充,代表技术:Retrieval Augmented Generation (RAG),先从向量数据库中获取相关信息然后填充到prompt中一起发送给大模型,从而达到大模型引用外部知识目的

将文档存入向量数据库这一步骤很重要,大致分为两步

  1. 将文档按照语义边界进行分割。避免将同一语义信息从中间进行分割(例如不要把一个function从中间分割了)
  2. 将文档进一步分割使其大小符合模型token限制

优点:

  • 方便灵活

缺点:

  • 受prompt限制,受相似度查询准确率限制,受大模型稳定性限制

Function Calling

大模型在训练完成后知识库便固定了正常无法获取外部数据,函数调用技术允许提供外部数据给大模型使用。

优点:

  • 灵活

缺点:

  • 不是所有大模型都有函数接口

SpringAI实现基于大模型的文档问答示例

环境信息

  • 框架:SpringAI 0.8.1
  • 大模型:LLAMA3-7B(ollama本地部署)
  • 检索方式:RAG - Prompt Stuffing
  • 向量数据库:Neo4j

核心代码

没啥特别的基本都有starter,按文档往pom和配置文件加下参数就能跑起来

这里更多的难点是在如何更好与大模型交互,在于prompt的编写

出现的问题记录

由于是SpringAI早期开发版本所以会有各种各样问题

1.Neo4j的向量维度限制

当前Neo4j的配置类Neo4jVectorStoreConfig源码将向量维度限制在了2048,而llama3模型为4096

1
2
3
4
5
public Builder withEmbeddingDimension(int newEmbeddingDimension) {
Assert.isTrue(newEmbeddingDimension >= 1 && newEmbeddingDimension <= 2048, "Dimension has to be withing the boundaries 1 and 2048 (inclusively)");
this.embeddingDimension = newEmbeddingDimension;
return this;
}

临时性的解决措施:通过反射修改bean的属性

2.Pinecone数据库相似度查询

当使用Pinecone作为向量数据库时,进行相似度查询时,相似度度量方式有特定限制,采用cosine度量可能会导致查询不到目标信息。这是PineconeVectorStorec.class源码,注意其中score部分限制

1
2
3
4
5
6
7
8
9
10
11
12
return queryResponse.getMatchesList()
.stream()
.filter(scoredVector -> scoredVector.getScore() >= request.getSimilarityThreshold()) // here my socres are negative numbers
.map(scoredVector -> {
var id = scoredVector.getId();
Struct metadataStruct = scoredVector.getMetadata();
var content = metadataStruct.getFieldsOrThrow(CONTENT_FIELD_NAME).getStringValue();
Map<String, Object> metadata = extractMetadata(metadataStruct);
metadata.put(DISTANCE_METADATA_FIELD_NAME, 1 - scoredVector.getScore());
return new Document(id, content, metadata);
})
.toList();

解决办法: 社区反馈会在1.0.0-M1版本支持更多度量方式,或者自己重写(然后去PR)

完整Demo

demo