2026-10-29 15:23:13
本文来自依云's Blog,转载请注明。
最近各种严重的安全漏洞层出不穷,而我更新完它们、停下来的时候,突然发现一件事:我的virtiofsd是使用abstract socket来连接的,而它所提供访问的文件系统并没有进行任何限制。这意味着任何本地用户,只要连上去就能得到virtiofs里的root访问权限,然后往里边丢一个suid的shell,最后从外边直接执行就能提权!
当初选择使用abstract socket,主要是因为这样不用管理一个socket文件,会比较方便。但其实现在socket文件交给systemd处理也很方便了。
加固方案,首先是不再使用abstract socket,以便进行权限控制——只允许我自己连接。另外再用一下用户命名空间的id映射——这样在文件系统上直接访问的话,它们都是某个普通用户的文件,没有特权了。
首先要把rootfs的uid和gid都改成使用高位id。可以使用systemd-nspawn来更改,但是它要求起始id是65536的整数倍,不能像以前那样用100000然后看着就很整、很容易看出对应关系了。
用以下命令就可以更新id了:
sudo systemd-nspawn -D /.../rootfs --private-users=655360:65536 \ --private-users-ownership=chown
另外要在/etc/subuid和/etc/subgid里更新授权给我的用户的id段,这样以我的用户权限运行的virtiofsd才能使用它们。
服务文件如下:
[Unit] Description=Share ArchKDE rootfs to virtual machines via virtio-fs [Service] Type=simple StandardInput=socket ExecStart=/usr/lib/virtiofsd --fd=0 --posix-acl --preserve-noatime --rlimit-nofile=524288 --shared-dir=/run/ssd/archkde-rootfs --uid-map :0:655360:65536: --gid-map :0:655360:65536:
以及systemd的socket单元文件:
[Socket] ListenStream=/run/user/%U/archkde.fs [Install] WantedBy=sockets.target
我另外有一个用于共享pacman包缓存的virtiofs。这个要和主系统共享,就不能用我的用户权限运行了。所以得作为系统服务运行,但是让我的用户可以访问到它的socket文件。为了排除安全隐患,我给这里的挂载点添加了nosuid,nodev,noexec挂载选项。
这里遇到了一个奇怪的问题:我使用/run/user/1000/pacman.fs这个路径来放socket文件,但是启动之后并看不到这个文件。可能是某种mount命名空间的原因。所以我改成了另外的位置。systemd的socket单元提供的选项挺齐全的。
[Unit] Description=Share pacman cache to virtual machines via virtio-fs [Service] Type=simple StandardInput=socket ExecStart=/usr/lib/virtiofsd --fd=0 --shared-dir=/var/cache/pacman/pkg --preserve-noatime
[Socket] ListenStream=/run/virtiofsd/pacman.fs SocketUser=lilydjwg SocketGroup=lilydjwg SocketMode=0660 [Install] WantedBy=sockets.target
接下来把两个socket单元都启用并启动,虚拟机就可以运行起来啦~
另,想从外面直接访问虚拟机某个用户的家目录的话,可以用idmap选项挂载,例如:
sudo mount --bind -o X-mount.idmap='u:656361:1000:1 g:655460:1000:1' \ /..../rootfs/home/user home-user
2026-06-09 17:11:54
本文来自依云's Blog,转载请注明。
现在YouTube首页的视频推荐越来越差了,好多标题党、clickbait,故意把讨论的主题藏起来。经常我点进去看了好久,才发现原来并没有讨论什么我之前不知道信息,又或者讨论的话题我完全不感兴趣。再不就是把我想知道的信息藏到不知道哪个片段里。虽然我有GlobalSpeed扩展可以依内容信息密度来方便地调整播放速度,但是调太快(超过2x)就听不清啦。总之是好多视频点开看之前十分吸引人,但看完或者看一半时就想骂人、点踩退出,十分浪费时间。
正好最近在尝试Gemini API,于是灵光一现,写了个脚本,使用yt-dlp下载视频字幕,然后调用Gemini API来总结内容。
#!/usr/bin/python3
import sys
import json
import subprocess
import tempfile
from pathlib import Path
import httpx
GEMINI_KEY = 'YOUR GEMINI KEY HERE'
URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite:streamGenerateContent?alt=sse'
PROMPT = '根据以下字幕文本总结视频内容。总结结果中请不要包含任何赞助商推广信息。'
def main(url, sublang):
with tempfile.TemporaryDirectory() as d:
subprocess.run([
'yt-dlp', '--sub-langs', sublang, '--write-subs', '--write-auto-subs', '--skip-download',
url,
],
cwd=d,
check=True,
)
p = Path(d)
try:
file = tuple(p.iterdir())[0]
except IndexError:
sys.exit('No subtitles.')
for _ in range(2):
try:
do_request(file)
break
except httpx.ReadError as e:
print(e, file=sys.stderr)
def do_request(filepath):
client = httpx.Client(http2=True)
filename = filepath.name
with filepath.open() as f:
subtitles = f.read()
parts = [{
'text': PROMPT,
}, {
'text': f'文件名:{filename}\n文件内容:\n{subtitles}',
}]
j = {
'contents': [{
'parts': parts
}],
}
with client.stream(
'POST', URL,
headers = {
"X-goog-api-key": GEMINI_KEY,
"Content-Type": "application/json",
},
json=j, timeout=120,
) as r:
for line in r.iter_lines():
if not line.startswith('data: '):
continue
line = line.removeprefix('data: ')
data = json.loads(line)
for a in data['candidates']:
for b in a['content']['parts']:
text = b['text']
if not text:
break
print(text, end='', flush=True)
print()
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('URL',
help='YouTube URL')
parser.add_argument('--lang', default='en',
help='choose subtitles language')
args = parser.parse_args()
main(args.URL, args.lang)
脚本依赖Python和httpx库。当然鉴于httpx已经不再更新,你换成httpx2应该也能用。Gemini Key可以去这里生成,然后填到脚本开头。我使用的是gemini-3.1-flash-lite这个模型,因为免费版本中,它的每日请求数配额比较充足。
当然啦,视频要有CC字幕这个脚本才能用,否则会报错。默认使用英文字幕,包含自动生成的版本。如果是中文视频,可以使用--lang zh.*参数指定用中文字幕。
2026年07月01日更新:脚本后续有些改进,包括支持传递yt-dlp参数、支持使用参数指定模型、支持本地模型、支持后续对话等。脚本放GitHub上了:yt-summarize。
2026-05-12 00:05:05
本文来自依云's Blog,转载请注明。
一开始使用Linux系统的时候,并没有多少自由开源的中文字体。那时候几乎所有人的选择都是文泉驿正黑。我就一直用啊用了好多年,直到后来截图时被网友说该换字体了,我才知道原来文泉驿项目已经停止很久了,网站上的新闻截止于2008年——都快20年啦。
文泉驿正黑的字形比较「旧时代」——以屏幕清晰度为优先,由于当时的屏幕普遍dpi低,笔画迁就像素风格,所以比较丑。另外也有些bug,比如「撨䑾詺㘃㞈㟯㫥」这几个字会有一片漆黑的区域。
文泉驿正黑看起来是这样的。注意截图中只有除标题外的中文部分用的是文泉驿正黑字体。
2014年,Google联合Adobe发布了思源黑体和Noto Sans CJK字体——这两款字体的汉字部分是相同的,区别只在于思源黑体会根据文本的区域设置来自动选择字形(通常不管使用哪个语言的字族名来指定),而Noto Sans CJK字族名只有英文版本,并且不同的字族名后缀会选择不同的地区字形。
我在2024年终于决定切换到思源黑体试试。它长这样:
图中的日文部分也是使用的思源黑体,只不过是日文字形。标题则使用的是思源宋体。读者可以在图片上点「右键」然后新建标签页打开图像,然后来回切换着对比。
可以看出,思源黑体是比文泉驿正黑好看多啦。而且思源字体有粗体版本,文泉驿正黑是没有粗体的,只有合成出来的所谓「伪粗体」。
但是,你有没有发现有什么地方不对劲?注意看大标题下方那行字,「条目」和「阅读」下边的装饰线与该行下方的分隔线有一段距离,而「大陆简体」和「工具」两处字偏低。事实上,思源黑体的问题远不止这些。思源黑体文字上方空出来的空间比下方多不少,造成行高太高、终端里文字不居中等各种问题。
(这个图片是使用命令pango-view --dpi=1024 --font=思源黑体 --annotate=glyph,layout,baselines -t A测试中文Test -o out.png生成的。)
因此我用了几天就换回文泉驿正黑了,但是把思源黑体作为部分网页字体在用(主要用于非简体中文内容,以及通过stylus指定的特定几个网站)。
最近,oldherl说更纱黑体修了行高的问题。于是我又试了几天这个基于思源黑体的字体。它的效果是这样的:
行高的问题确实解决了!但是——又要「但是」了——它的字怎么矮胖矮胖的?是我没看习惯的原因吗?于是我使用了几天,最后并没有习惯,反而是和思源黑体一对照,发现确实是许多字都变矮了一点。
于是我只好又换回已经用习惯了的文泉驿正黑。
诶等等!既然更纱黑体能修行高,我为什么不行?因为我不会,可是,今非昔比了呀——Gemini,给我来个脚本!
于是就有了这么个脚本——当然这个脚本是我改过的最终版本了。
#!/usr/bin/python3
from io import BytesIO
from fontTools.ttLib import TTCollection, TTFont
def adjust_font(font):
target_ascent = 1025
target_descent = -265
# 修改 hhea 表 (macOS/Pango 渲染常用)
font['hhea'].ascent = target_ascent
font['hhea'].descent = target_descent
font['hhea'].lineGap = 92
# 修改 OS/2 表 (Windows/Linux 合规性)
font['OS/2'].sTypoAscender = target_ascent
font['OS/2'].sTypoDescender = target_descent
font['OS/2'].sTypoLineGap = 92
# usWin 参数决定了红线(剪切区域),设为相同值可消除额外间距
font['OS/2'].usWinAscent = target_ascent
font['OS/2'].usWinDescent = abs(target_descent)
# 2. 修改 Font Family 名称
name_table = font['name']
for record in name_table.names:
name_str = record.toUnicode()
new_record_str = name_str.replace('Source Han', 'Lily Han') \
.replace('思源', '百合') \
.replace('源ノ角ゴシック', '百合ノ角ゴシック') \
.replace('본고딕', '백합고딕') \
.replace('SourceHan', 'LilyHan')
# 针对不同 ID 进行替换
# ID 1: Family Name, ID 4: Full Name, ID 6: PostScript Name 等
if name_str != new_record_str:
name_table.setName(new_record_str, record.nameID, record.platformID, record.platEncID, record.langID)
# Medium as Semibold
if font['OS/2'].usWeightClass == 500:
# save then read to copy the font without referencing existing data structures
buf = BytesIO()
font.save(buf)
buf.seek(0)
sb_font = TTFont(buf)
sb_font['OS/2'].usWeightClass = 600
for record in sb_font['name'].names:
name_str = record.toUnicode()
if "Medium" in name_str:
new_name = name_str.replace("Medium", "Semibold")
record.string = new_name.encode(record.getEncoding())
cff = sb_font['CFF '].cff
for i in range(len(cff.fontNames)):
if "Medium" in cff.fontNames[i]:
cff.fontNames[i] = cff.fontNames[i].replace("Medium", "Semibold")
# 修改 TopDict 内部的名称
for topDict in cff.topDictIndex:
for attr in ['FullName', 'FamilyName', 'Weight']:
if val := getattr(topDict, attr):
if isinstance(val, str) and "Medium" in val:
setattr(topDict, attr, val.replace("Medium", "Semibold"))
return sb_font
def main(input_ttc, output_ttc):
ttc = TTCollection(input_ttc)
new_fonts = list(ttc.fonts)
for font in ttc.fonts:
newfont = adjust_font(font)
if newfont:
new_fonts.append(newfont)
ttc.fonts = new_fonts
ttc.save(output_ttc)
print(f"成功保存至: {output_ttc}")
if __name__ == '__main__':
import nicelogger
nicelogger.enable_pretty_logging('DEBUG')
input_ttc = "SourceHanSans.ttc"
output_ttc = "LilyHanSans.otc"
main(input_ttc, output_ttc)
脚本使用fonttools这个Python库,把ascent、descent和lineGap这三个参数改成和文泉驿正黑一样的了。之所以要照着文泉驿正黑来改,是因为我的终端最大化之后,使用13pt字号差不多刚好填满35行(只空出来两行像素)。而使用别的参数,我调整字号好久,都会空出来小半到大半甚至接近一行文字的高度。
另外,这个脚本运行起来非常耗资源:它保存的时候会重新计算每一个字形,持续占用一个CPU核心长达六分多钟,内存只分配不释放,最终用掉接近20GiB。倒是生成的文件差异不大,用rsync很快就能同步回来。
最终效果图:
可惜的是思源黑体只有粗体和Medium,不像更纱黑体那样有半粗(Semibold)版本。思源黑体的粗体挺粗的。
由于我在使用思源黑体时遇到的问题都是在UI部分,而这里又不需要用到宋体,所以我没有去改思源宋体,其它地区的字形也还是用的原本的思源系列。
如果有人想试试我生成的这个字体的话,在这里下载(112 MiB)。
2026年05月13日更新:更新了脚本和字体文件。现在把Medium复制一份然后改叫Semibold了,因此有了半粗字重,在用到的地方会更接近设计者的意图,也看着更舒服。
2026-04-18 21:22:54
本文来自依云's Blog,转载请注明。
Wayland协议会告诉客户端程序缩放比率应该是多少,支持的就按指定的来,不支持的Wayland compositor就负责把窗口缩放到合适的大小。但因为是客户端渲染成位图之后,再由Wayland compositor缩放,大小是合适了,但却模糊了起来。然而很不幸地,Xwayland就是那个不支持Wayland缩放机制所以被Wayland compositor强行缩放的Wayland客户端。
但其实在X的时代,各个GUI框架都发展出来了自己的一套指定缩放的机制。比如一些老的程序会认Xft.dpi这个设置,GTK可以用GDK_SCALE环境变量,Qt认QT_AUTO_SCREEN_SCALE_FACTOR和QT_SCREEN_SCALE_FACTORS,Wine可以在winecfg里设置,等等。我想说的是,虽然不太统一,但大家其实都有自己的HiDPI方案了。
所以,Wayland compositor如果不强行缩放Xwayland的窗口的话,那些还不原生支持Wayland的程序其实也能显示得清晰好看的。但可惜的是Xwayland始终没有合并任何解决方案,我也就只好用着打过补丁的wlroots-lily-git、xorg-xwayland-lily了。KDE很早就引入不缩放Xwayland窗口的选项、让这些使用Xwayland的程序自己处理缩放。Hyprland后来也支持了。而现在,终于轮到Wayfire支持啦~
Wayfire的这个选项叫workarounds/force_xwayland_scaling。设置为true就是程序自己处理缩放、Wayfire不管它了。一开始没处理鼠标光标,后来也修好了。xorg-xwayland-lily终于完成了它的使命~
顺便说一下,workarounds这里我还设置了另外两个选项。一个是discard_command_output=false,这样能看到Wayfire启动的程序的报错信息。另一个是auto_reload_config=false,禁用自动重新加载配置文件,不用和git打架了。
[workarounds] discard_command_output = false auto_reload_config = false force_xwayland_scaling = true
2026-04-02 13:59:32
本文来自依云's Blog,转载请注明。
显示器送去维修了,因此我的台式机变成无头设备啦。现在用它来编译软件是没啥问题的,但是我的GUI软件的窗口全部访问不了啦,编译出来的wayfire也没法调试了。本来已经存在的窗口我打算等显示器修好回来再用的,但是Vim刚出了一个公开利用方式的RCE,我怕我的GVim留在那里忘记了然后不小心中招,直接关又不记得它有没有打开什么需要处理的文件。于是想到了远程桌面。
首先想到的是群友的reframe。之前显示器还在的时候它挺好用的,但是现在显示器不在了,它就连不上去啦。读了文档才知道需要配置一个虚拟显示器,要动内核参数所以需要重启,我的窗口们就回不来啦。另外等显示器回来,要去掉虚拟显示器还要再次重启。
其实我还有另一个显示器,也就是大上的E-ink屏幕。接上去之后,我的wayfire桌面回来了,但是我的窗口并没有……lswin发现它们还在「unknown」显示器上。于是我开始研究Wayfire IPC看看怎么把它们弄回来。
花了些时候,去Matrix频道里问了一下,窗口们是用configure_view找回来了,但是发现我怎么几百个叫「input-method-popup」的窗口啊(其实这就是我要调试Wayfire的原因)。另外发现有创建无头输出的接口,也就是Wayland compositor层级的虚拟显示器啦。
墨水屏用来阅读和写代码挺好的,但是用来测试GUI软件就不太适合了,更不用提打游戏了。所以就想着既然能创建无头输出了,那我是不是可以把无头输出VNC出来、在笔记本屏幕上看?于是读了一下wayvnc的文档,发现还真可以。
创建无头输出:
import wayfire, os
addr = os.environ.get('WAYFIRE_SOCKET', os.environ['XDG_RUNTIME_DIR'] + '/wayfire-wayland-1-.socket')
sock = wayfire.WayfireSocket(addr)
sock.create_headless_output(1920, 1080)
然后记下创建出来的输出的名字(HEADLESS-开头的那个)。几经尝试,最终使用的wayvnc命令是:
wayvnc -f 60 -r -g -o HEADLESS-2 IP PORT
其中-f 60指定帧率,默认是30fps;-r指定渲染光标,否则在服务端使用鼠标时不显示光标;-g启用需要GPU的特性;-o HEADLESS-2是选择之前创建的无头输出。后边的监听地址我指定了与笔记本直连的有线网口的IP,这样不需要设置密码、也不用加密就很安全。
然后使用tigervnc连接:
LANGUAGE=en_US vncviewer -Maximize IP:PORT -PreferredEncoding H.264
vncviewer显示中文有问题,因此使用英文界面。这里指定了编码方式为H.264,希望能比默认更好一些。我原本还想用Raw来着,但是问了一下Gemini,说是1Gbps不够1080p60用,另外序列化延迟也会更高。不过wayvnc和vncviewer都没有使用VAAPI的样子,不知道是不支持还是哪里有问题。
实际占用的带宽最高为27 MiB/s。延迟挺低的,反正我感觉很不明显,玩游戏也没有问题。不过可能是因为有压缩,文字的显示不是很清晰。
哦对了,因为vncviewer是Xwayland软件,因此它自己的键盘捕获是没有用的。我使用Wayfire的shortcuts-inhibit设置让它默认捕获键盘了,需要的时候再按快捷键临时取消一下。
[shortcuts-inhibit] break_grab = <ctrl> <alt> <shift> KEY_ESC inhibit_by_default = app_id is "Vncviewer"
至于为什么不用别的VNC客户端,gtk-vnc慢死了,remmina界面好复杂,virt-viewer我不知道怎么叫它连远程VNC。
2025-11-11 15:14:52
本文来自依云's Blog,转载请注明。
偶然间发现Discourse论坛支持利用文本嵌入模型来生成「相关话题」列表、提供语义化搜索。于是我给Arch Linux中文论坛试过了好几个模型,记录一下经验。
文本嵌入,英文叫「text embedding」,指的是将一段文本编码成语义空间中的向量,从而可以判断不同文本的语义相关性。编码出来的向量少则512维,多的能有4096维。而判断相关性有「余弦距离」(看两个向量的夹角大小)和「负内积」(一个向量和另一个向量的转置相乘,然后取负)两种方法,我都是看模型文档和示例来决定用哪个的。至于这些向量的存储和索引,Discourse使用的是pgvector这个PostgreSQL插件。
Discourse启用这个功能之后,会在每个话题下方推荐几个「相关话题」,很适合看看是不是有人问过相同的问题。语义化搜索则需要在搜索页面点按钮来显示。在搜索框里按两下回车,就能到搜索页面了(这时候语义化搜索就会进行了,虽然用户还看不到结果),或者点搜索框右边的按钮也行。
因为论坛以中文为主,所以没多少可以抄Discourse官方文档的地方。一开始我挑了好几个来尝试,bge-m3、all-mpnet-base-v2、gte-multilingual-base等。但是没想到它们体积不大,但跑起来却很吃资源。E5-2678 v3辛辛苦苦跑了好久,结果去数据库里一看,已索引的话题数量才几个、十几个,而且不见涨……后来写了API转换代理我才知道,原来是因为Discourse会批量并发请求,并发度会高达45左右,于是很容易导致本来就慢的请求因为排队太久而超时被放弃,CPU都白算了。
最终我找到gte-base-zh这个模型,是针对中文特化的。很小,才0.1B,但这CPU跑得动它。效果也还能接受。
后来了解到最近新出的Qwen3-Embedding系列,看评分效果是最好的。又有群友愿意提供显卡算力,于是试了试。
Qwen3-Embedding提供8B、4B、0.6B三种参数规模的模型。8B很重,我的6650XT的8G显存勉强能放下它的Q4_K_M量化版本。0.6B的只有Q8_0的量化版本,我的显卡跑起来轻松不少,就是不知道为什么它占了我4G+的显存,导致剩下的显存不够原神用了。另外运行的时候如果不用systemd的CPUWeight之类的手段降一下CPU优先级,会导致我的桌面也很卡——我没找到调整GPU优先级的方法,不过调整CPU优先级也管用。
这些模型在群友提供的RYZEN AI MAX+ 395上跑得就比较惨。这台设备有算力不错的核显——至少比用Linux的Apple M2 Ultra算得更快一些,也有核显能够使用大量内存的优势,但是!amdgpu驱动会在高负载时崩溃重置!这么久过去了,amdgpu依旧不待见核显啊(不过听说Intel那边新的xe驱动也有不少bug)。不过断断续续跑了几天之后,终于把大部分话题都索引好了。
后来我还是换0.6B模型了,因为群友提供的算力并不稳定,我想要更容易替代的方案。可能Qwen3-Embedding系列模型对我的用途来说实在是太优秀了,以至于不管是0.6B还是8B,我都没发现结果有什么明显的差异。但0.6B对性能的需求低很多,甚至编译机上的7950X3D也能跑——虽然编译机没那么多时间能跑它就是了。
我还尝试过Google家的embeddinggemma-300M模型。它的MTEB评分比gte-base-zh要高,但只比gte-base-zh大一倍。但实际用下来,呃,效果差很多,基本上没啥用,可能分数都得在别的语言上了吧。遂放弃。
目前的论坛文本嵌入算力主要由群友的RYZEN AI MAX+ 395提供。在它不在线的时候,则由另一位群友提供的Apple M2 Ultra编译机兼职。哪天要是它也有事不在了,还能由x86编译机接棒。在历史话题索引完毕之后,平时的请求其实挺少的。
哦对了,最近还接触过一个叫all-MiniLM-L6-v2的模型,超级小,只有22.7M参数,是火狐新加的地址栏语义化搜索用的。但是它只支持英文,对于中文来说纯粹在增加噪音,可以在about:config里搜索places.semanticHistory.featureGate关闭之。
最后说说运行这些模型的方式。对于给sentence-transformers用的模型,可以用ghcr.io/huggingface/text-embeddings-inference:cpu-latest这个容器来运行。缺点是,它只有支持CPU和CUDA的版本。所以我更喜欢找gguf格式的模型,然后用llama.cpp来运行,可以使用Vulkan或者ROCm。不过我测试发现llama.cpp用ROCm还不如用Vulkan的来得快,而ROCm有着极其巨大的依赖库群,我就不用它了。要是乐意用ROCm的话,也可以用ollama来跑,支持动态加载和卸载模型——但这对于长期运行的服务型用途来说并不是很适合,我还得传个参数让它不要一直加载卸载。