普通视图

发现新文章,点击刷新页面。
昨天以前依云's Blog

使用wayvnc远程访问无头Wayfire会话

作者 依云
2026年4月2日 13:59

本文来自依云'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

本文来自依云'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来跑,支持动态加载和卸载模型——但这对于长期运行的服务型用途来说并不是很适合,我还得传个参数让它不要一直加载卸载。

使用 Restic 备份数据

作者 依云
2025年9月23日 18:13

本文来自依云's Blog,转载请注明。

我很早就知道 Restic 这个备份工具了,但是因为我有 rsync 和 btrfs send/receive 方案所以一直没用过。某天,突然有个走 AWS s3 协议的服务器备份需求,这才把它翻了出来。

既然是 s3,首先设置环境变量 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,然后仓库的地址是 s3:域名/存储桶名。好像一个存储桶里只能初始化一个 Restic 仓库?我还不清楚,也没有能给我玩的环境,就不管了。仓库地址可以设置到环境变量 RESTIC_REPOSITORY 中,这样就不用每次用 -r 参数指定了。执行 restic init 进行初始化。Restic 只支持加密备份,所以必须设置一个备份密码——倒也可以设置为空字符串,但是加密仍旧会进行,并且需要额外的参数来允许空密码。密码也可以放在 RESTIC_PASSWORD 环境变量里。

然后就是执行备份命令了,很简单的,比如:

restic backup -x --exclude-caches --exclude='/home/*/.cache/' --exclude=/var/cache/pacman/pkg/'*' ... /

这样就备份了整个系统,并排除了一些目录。可以先加 -v --dry-run 参数测试一遍。确定好备份的参数之后,就可以设定计划、反复执行了:

[Unit]
Description=backup the system
After=network-online.target

[Service]
Type=oneshot
Environment=HOME=/root
EnvironmentFile=/etc/restic.conf
ExecStart=restic backup -x --exclude-caches ... /
[Unit]
Description=backup system daily

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
AccuracySec=6h
Persistent=true

[Install]
WantedBy=timers.target

仓库地址和各种密码之类的放环境变量文件里了。把这个文件设置为只有 root 才能读,避免别的用户看到不该看的内容。然后 systemctl enable --now sysbackup.timer 就好了。

之后可以用 restic snapshots 查看备份的数据。使用 restic forget 删除旧的备份,比如 restic forget -d 30 -w 10 -m 10 -l 5 在最近30天、10周、10个月中各保留一份,外边最新的五个。这个命令只是删除这个备份的元数据,其关联的数据不会被删除。之后还要执行 restic prune 来实际删除数据——这个过程会比较费时费力。加 -v 可以查看诸如减小了多少空间占用的数据。

Restic 的备份是打包成块、压缩、加密存储的,带去重功能。但是增量备份会和主机名和路径相同的上一个备份进行比较。一个仓库里可以备份多台机器的相同或者不同的目录,但为了有效率地增量备份,每次增量备份时的目录不能变。这最后一点给我出了个难题。

我把 Arch Linux 中文论坛(以及维基)也用 Restic 备份到 sftp 目标里了。但这俩都是重数据库应用,因此直接对着 / 执行 restic backup 不行,数据有可能因为数据库的不同文件来自于不同的时间点而损坏。该系统在 btrfs 上并且有快照,对着快照备份就好了。问题是 Restic 执意不支持设置源路径,只能从文件系统上读取。我只好用 mount 命名空间把快照弄到 / 上再执行备份。脚本在这里:https://github.com/archlinuxcn/misc_scripts/blob/master/wiki/restic-backup-snapshot。由于权限的问题,用 bwrap 是不行的。对快照再做一个固定路径的可写快照来备份倒是能把路径固定到一个指定的位置,就是不太优雅,事后删掉快照也有额外的性能消耗。

关于备份数据的大小,restic snapshots 显示的是所有文件的名义大小的总和——是压缩前的大小,并且硬链接会算多次。restic stats --mode=restore-size 则是按硬链接去重后的未压缩大小。restic stats --mode=raw-data 才是压缩后的、备份实际占用的空间。

Restic 的备份仓库是加密的,因此无法直接查看,但它有 restic mount 子命令可以把备份仓库挂载了查看内容,就是性能比较差,只适合检查和恢复特定的少量文件。恢复大量数据推荐用 restic restore。另有 restic diff 命令可以查看不同快照之间的文件差异(新增、修改、删除了哪些文件,不含内容),方便在大小出现异常时查找元凶。

最后说说配置只能使用 sftp 的用户的方法。

首先在 sshd_config 里设置 Subsystem sftp internal-sftp。因为之后要进行 chroot,访问不到外部 sftp 服务的可执行文件的。然后设置指定的用户组只能用 sftp:

Match Group sftp-users
     ChrootDirectory /srv/sftp
     X11Forwarding no
     AllowTcpForwarding no
     AllowAgentForwarding no
     ForceCommand internal-sftp -d /%u

最后创建用户,按常规设置 authorized_keys,并按之前配置的路径,在 /srv/sftp 下创建该用户与其用户名同名的存储数据的目录,记得要 chown 到该用户。注意 ChrootDirectory 的目标目录(也就是这里的 /srv/sftp)应当只能由 root 写入(就跟 /home 目录那样)。

Arch Linux 中文论坛迁移杂记

作者 依云
2025年8月26日 21:53

本文来自依云's Blog,转载请注明。

本篇只是心得体会加上赞美和吐槽。技术性的迁移记录在这里。

一个多月前,肥猫在 Arch Linux 中文群里说:

(希望有好心人研究一下php74,最好加到archlinuxcn里,因为咱中文社区论坛卡在这个老版本了

然后话题自然就又到了论坛迁移的事情上来——毕竟 FluxBB 年久失修也不是一两天的事了。Arch Linux 官方讨论了几年还没结果,但中文社区这边并不是卡在往哪迁上,而是

基于什么的都可以迁移,但得有人干活

然后又过了些天,论坛使用的本就递送困难的 Sendgrid 停止了免费服务,导致中文论坛完全无法注册新用户了。虽然这件事通过更改为使用我们自己的邮件服务器就解决了,但迁移论坛的想法在我脑中开始成长。

至于迁移到哪个软件,我早已有了想法——包括 OpenSUSE 中文社区Debian 中文社区NixOSOpenSUSEFedoraUbuntuManjaroGaruda LinuxCachyOSKDEGNOMEPythonRustAtuinF-DroidOpenWrtLet's EncryptMozillaCloudflareGrafanaDocker 等等(还有一堆我没有那么熟悉的就不列了)大家都在用的 Discourse。这么多开源社区和商业组织都选择了它,试试总不会错的,用户也会比较熟悉。令我惊喜的是,它甚至有个 FluxBB 导入脚本。

于是在虚拟机里安装尝试。安装过程是由他们的脚本驱动构建的 docker 镜像,没什么特别的——除了比较耗资源。Docker 嘛,硬盘要吃好几 GiB,然后它是现场编译前端资源文件,CPU 和内存也消耗不少。默认的构建是包含 PostgreSQL、Redis、Nginx 的,但 PostgreSQL 也可以用外边的,就是得监听 TCP,并且构建出来的镜像里依旧会存在 PostgreSQL 的服务文件。Nginx 可以改成监听 UNIX 域套接字,然后让外边我自己的 Nginx proxy_pass 过来,这样证书也按自己的方式管理。Redis 我本来也是想拆出来的,但是为安全起见要设个密码嘛,然后构建就失败了……不过算了,反正也没别人用 Redis。

本地测的没有问题,于是去服务器上部署。由于发现它比较吃资源,所以给服务器加了几十G内存、100G 硬盘,还有闲置的 CPU 核心也分配上去了。后来发现运行起来其实也还好,就是内存吃得比较多——每个 Rails 进程大几百,32个加起来就快 10G 了。运行起来之后 CPU 不怎么吃,甚至性能比 MediaWiki 要好上不少,以至于我把反 LLM 爬虫的机制给降级到大部分用户都不需要做了。哦它的 nginx 会起 $(nproc) 个,太多了,被我 sed 了一下,只留下了八只(但其实也完全用不上,毕竟是异步的;Rails 那些进程可是同步的啊)。

run:
  - exec: sed -i "/^worker_processes/s/auto/8/" /etc/nginx/nginx.conf

说回部署。跑起来是没什么问题的。问题出在那个 FluxBB 导入脚本——本来导入就不快,它还跑到一半崩了,修了还崩。然后它不支持导入个性签名、用户头像、置顶帖,还遇到著名的 MySQL「utf8 不 mb4」的问题。来来回回修了又改,花了我好些天。等保留数据测试的时候,发现有好多帖子的作者变成「system」了。一查才发现我刻意没有导入被封禁的用户造成的。都已经配得差不多了,实在是不想删库重来,研究了一下,直接在 Rails 控制台写了段脚本更正了。这个 Rails 控制台能在 Rails 的上下文里交互式地执行代码,很方便改数据,我很喜欢,比 PHP 方便太多了!

另外这个导入脚本有一点好的是,它能够反复执行来更新数据——虽然这种支持反复执行的操作在我写的脚本里是常有的事,基本上从头开始成本太高的都会有,但别人写的脚本能考虑到这一点的可太少了。

用户的密码也导入了!帖子的重定向服务也写好了!虽然后来发现用户个人页面是登录用户才能访问的,给它重定向没什么用……反而是 RSS 重定向更有用,后来也补上了!

后来还发现有些用户名包含空格或者特殊符号啥的,被自动改名了。好在可以用邮箱认用户,不管了,等受影响的用户出现了再改。另外正式迁移之后才发现还有些数据没有迁移——版块的对应关系、用户的主题订阅,不是很重要,算了。

然后就迁移结束啦~所有到旧论坛的访问全部重定向到新论坛啦~不过我还是保留了一个不跳转的后门以便有需要时回去看看。为此我折腾了好久神奇的 nginx 的配置文件,最终得到以下片段:

set $up 'redir';
if ($http_cookie ~ "noredir=1") {
    set $up 'noredir';
    proxy_pass https://104.245.9.3;
}
if ($up = redir) {
    proxy_pass http://127.0.0.1:9009;
}

就是根据 cookie 来 proxy_pass 到不同的服务啦。这样就可以访问一下 /noredir 设置上 cookie,就可以访问旧论坛,再访问一下 /yesredir 清一下 cookie 就恢复跳转到新论坛了。

说起 nginx,Discourse 还在这方面坑了我一下。它文档里给的设置是:

proxy_set_header Host $http_host;

这个配置在 HTTP/3 时是坏的,应该用 $host。别问我 HTTP/2 也没有 Host 头啊,为什么它在用 HTTP/2 时就不会出错。我也不知道 ¯\(ツ)/¯。

于是新论坛上线啦~很多中国大陆用户的首次加载时间变成几十秒啦……还好这只是无缓存加载的时间,就当是下载软件了吧。之后每个标签页大约需要一两秒加载整个 SPA,在不同页面之间跳转并不慢。而这代价付出之后的回报是更现代的界面、丰富的功能。相比于旧论坛,现在:

  • 终于有手机版啦,甚至还支持 PWA,体验非常丝滑。
  • 实时预览的 markdown + bbcode 编辑器,还支持上传图片!再也不会有用户问论坛怎么传图片和日志了!
  • 编辑器还支持草稿功能!不用怕写一半弄丢了,甚至可以换个设备接着写。
  • 代码块角落里有个复制按钮,我再也不需要拖半天鼠标来复制日志然后粘贴进 Vim 里分析了!
  • 快速、简单的搜索体验,用户再也不会找不到搜索功能在哪里了!
  • 实时显示在回复的人。有人在回复的时候就可以等一等,发布帖子之后会立刻出现。发帖不需要跳转到不知道干什么的跳转页面了,读帖也不需要反复刷新了。
  • 有收藏功能了,用户不用发一个根本没什么用的「mark 一下」的帖子了。
  • 有「标记为已解决」的功能了。用户再也不需要问怎么把帖子标记为「已解决」了。
  • 不用自己跑脚本解析 HTML 来向群里发新帖通知了。Discourse 的「聊天集成」功能配一下就好了。
  • 甚至还有「RSS Polling」插件,可以把主站新闻转到论坛里,方便大家讨论。

Discourse 的邮件集成功能也挺不错的。配好之后,可以检测到退信,也可以直接回复邮件通知来回帖。甚至还有个邮件列表模式,就是把所有帖子都给用户发一遍,用户也可以直接回帖。通过邮件发布新主题的功能也有,但我没有启用——不同版块需要配不同的收件地址,有点麻烦,我不觉得有人会想用……就是这个邮件传回 Discourse 部分坑了我一把,但不是 Discourse 的错。

是 maddy 的文档太缺欠了。我要把 forum+...@archlinuxcn.org 这种地址给重写到 noreply@archlinuxcn.org,按例子像这样

table.chain local_rewrites {                                                                                                                   
    optional_step regexp "forum\+(.+)@(.+)" "noreply@$2"
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
    optional_step file /etc/maddy/aliases
    step sql_query {
        driver postgres
        dsn "user=maddy host=/run/postgresql dbname=maddy sslmode=disable"
        lookup "SELECT mailname FROM mailusers.mailinfo WHERE $1 = ANY(alias) and new = false"
    }
}

这里第二行是我加的(虽然一开始把 $2 照着下边那个已有的写成了 $3)。结果是报错「用户不存在」、被退信。我开 debug 选项研究了好久,才意识到最后一步写的是 step,所以它总是要执行的——然而 noreply 这个用户并不在数据库里,所以就找不到了。

那把最后一行改成 optional_step 就好啦——我是这么想的,也是这么做的。然后就有人报告说 admin 邮箱拒收邮件了……又是一通研究,才发现因为这里的步骤全是 optional_step,所以 maddy 第一次用整个邮件地址来查的时候,无论如何都是会通过的——不会返回「目标不存在」,所以也就不会触发去掉域名、只用用户名查询的步骤,而数据库里记录的只有用户名,就导致 admin 邮箱的映射查不到了(映射到它本身,然而并没有以它为名的邮箱)。把 SQL 查询那一行改成这样子就好了:

lookup "SELECT mailname || '@archlinuxcn.org' FROM mailusers.mailinfo WHERE regexp_replace($1, '@archlinuxcn.org$', '') = ANY(alias) and new = false"

然后是把收件的邮件交给 Discourse。他们有个 mail-receiver 容器用来干这事,但这个容器的主要部分其实是 Postfix。我读了一下它的代码,实际上只需要把邮件通过 API 发过去就行了。于是我用 Python 写了一个服务——imap2discourse。这部分的坑在于,这个 API 是把邮件全文用指定的参数 base64 或者不 base64 用 form-data 传过去的,我以为是用上传文件的方式来传,搞了半天它都报奇奇怪怪的错,后来一步一步在 irb 里按 mail-receiver 的代码对照检查,才发现原来是按传字符串的方式传的……

Discourse 的中文翻译不怎么样,好多随意的空格,也好多看不懂的翻译。好在它像 MediaWiki,支持修改界面文本。于是就一点一点地修了好多。上游使用的是 Crowdin 翻译平台,并不能直接 pr 翻译,所以等我什么时候研究一下才能把翻译贡献给上游了。

Discourse 的通知功能挺全面的。可以选择回帖之后要不要通知,邮件通知是只在没访问时发、完全不要还是全都要,网站图标上要不要显示通知计数,还可以开启浏览器的推送通知(然后我就发现 Android 火狐的推送通知无法切换到 PWA 窗口)。

至于管理功能,比 FluxBB 丰富好用太多啦~有各种访问统计报表。设置项有搜索功能。有管理员操作日志,也有选项变更日志,还有邮件收发日志(看看谁又把自己的邮箱域名拼错了)。能给用户添加备注,也能切换成指定的用户看看他们看到的论坛是什么样子的。能给用户添加字段,让用户填写他们用的操作系统和桌面环境,省得回帖时经常要询问。还能加载自定义的 JavaScript 和 CSS,甚至是加强版本。还有暂时用不上的 API 和 webhook。哦对了,我发现它还会自己拒绝一些常见的讨厌爬虫。

除了 FluxBB 之外,还有一个叫 planetplanet 的 RSS 聚合软件也是死了好多年,导致 planet.archlinuxcn.org 多年不更新了。Discourse 正好有从 RSS 发帖的功能,于是将星球也复活了一下,将大家的 RSS 作为帖子在专门的版块发出。虽然界面不是很理想,但将就着用啦。RSS 聚合也是有的。Discourse 的 RSS 功能相当完善,几乎在所有合理的网址后边添加 .rss 就能订阅。

也给旧论坛做了个静态存档站。暂时还没上线,因为肥猫又跑掉了

Discourse 的备份功能会报错,因为它的容器里的 pg_dump 版本比较旧,和我在外边 Arch Linux 里运行的版本不一致。不过我觉得这样也挺好的——因为管理员是可以生成和下载备份文件的,也就是说,如果有管理员的权限被人恶意获取,那么他就能通过下载备份文件的方式获取整个 Discourse 数据库的内容。备份不了就少了这么个风险啦。当然备份我肯定是做了的,至于是如何做的,就等下一篇啦。

用 Android 手机当电脑的话筒

作者 依云
2025年1月11日 14:50

本文来自依云's Blog,转载请注明。

我之前是使用 ROC 来做这件事的。手机上安装 roc-droid,电脑上安装 pipewire-roc 然后执行 pactl load-module module-roc-source source_name=roc-source 就行。

但是这样会有一个问题:手机上的 roc-droid 会被休眠。换手机之前用的 Android 10 还好一点,可以设置半小时的「超长」关屏时间,并且屏幕关闭之后 roc-droid 还能活跃一段时间。现在换 Android 14 了,关屏之后 roc-droid 会立刻被休眠,也不能把 roc-droid 切到后台,否则录音会停止。为了让录音不中断,只能让手机「喝点咖啡因」来保持亮屏,于是不光网络和录音费电,屏幕也要费电。其实这个问题不是不能解决,放个持久通知就可以了,但是我不会 Android 开发呀。

ROC 方案另外的小问题有:网络会持续占用,即使没在使用。手机要么录音、要么播放,需要手工切换。roc-droid 时不时会崩溃。

后来从群友那里了解到可以在 termux 里跑 PulseAudio,我试了试,比 ROC 方案好用多啦。

手机上除了需要安装 termux 和 pulseaudio 外,还需要安装 Termux:API。为了方便启动,我还安装了 Termux:Widget。记得给 Termux:API 话筒权限。然后编辑 PulseAudio 配置文件 /data/data/com.termux/files/usr/etc/pulse/default.pa.d/my.pa:

load-module module-sles-source
load-module module-native-protocol-tcp auth-ip-acl=电脑的IP地址 auth-anonymous=true

这里的 sles 模块是用来录音的。

编辑 /data/data/com.termux/files/usr/etc/pulse/daemon.conf 文件,设置一小时不用才自动退出(默认20秒太短了):

exit-idle-time = 3600

然后在需要的时候执行 pulseaudio 命令就可以了。

电脑上的话,其实设置 PULSE_SERVER 环境变量就可以用上了。不过为了更好的集成,我们创建个 tunnel:

pactl load-module module-tunnel-source server=tcp:手机的IP地址

source 就是把手机当话筒用,改成 sink 的话则是把手机当音箱用了。

执行之后,在 PulseAudio / PipeWire 里就会多出来相应的 source(或者 sink)设备了。想怎么用就可以怎么用了~

但若是要同时使用另外的音箱来播放声音的话,手机话筒会把音箱播放的声音录进去,造成「回声」。这时候,就需要设置一下回声消除了。我参考了 ArchWiki,PipeWire 配置如下:

context.modules = [
    {   name = libpipewire-module-echo-cancel
        args = {
            monitor.mode = true
            source.props = {
                node.name = "source_ec"
                node.description = "Echo-cancelled source"
            }
        }
    }
]

然后去 pavucontrol 里设置一下它生成的两个录音操作的设备(一个是选话筒,另一个是选外放的音箱的 monitor 设备),并把消除了回声的 source 设备设置为默认音频输入设备就好了。

使用 ffmpeg 对音频文件进行响度归一化

作者 依云
2024年12月11日 11:43

本文来自依云's Blog,转载请注明。

我喜欢用本地文件听歌:没有广告、没有延迟、没有厂商锁定。但是有个问题:有的歌曲文件音量挺大的,比如 GARNiDELiA 和桃色幸运草Z的都感觉特别吵,需要调小音量,但有的音量又特别小,以至于我时常怀疑音频输出是不是出了问题。

这时候就要用到响度归一化了。响度衡量的是人的主观感知的音量大小,和声强——也就是声波的振幅大小——并不一样。ffmpeg 自带了一个 loudnorm 过滤器,用来按 EBU R128 标准对音频做响度归一化。于是调整好参数,用它对所有文件跑一遍就好了——我最初是这么想的,也是这么做的。

以下是我最初使用的脚本的最终改进版。是的,改进过好多次。小的改进如排除软链接、反复执行时不重做以前完成的工作;大的改进如使用 sem 并行化、把测量和调整两个步骤分开。之所以有两个步骤,是因为我要线性地调整响度——不要让同一个音频不同部分受到不同程度的调整。第一遍是测量出几个参数,这样第二遍才知道怎么调整。只过一遍的是动态调整,会导致调整程度不一,尤其是开头。

至于参数的选择,整体响度 I=-14 听说是 YouTube 它们用的,而真峰值 TP=0 和响度范围 LRA=50 是因为我不想给太多限制。

#!/bin/zsh -e

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  if [[ -s $json || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json
  sem -j+0 'ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n ''/^{$/,/^}$/p'' > $json; echo "Done with $f"'
done

sem --wait

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  output=$f:r_loudnorm.$f:e
  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json output
  sem -j+0 'ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) -vcodec copy $output </dev/null; echo "Done with $f"'
done

sem --wait

不得不说 zsh 的路径处理是真方便。相对地,sem 就没那么好用了。一开始我没加 </dev/null,结果 sem 起的进程全部 T 在那里不动,strace 还告诉我是 SIGTTOU 导致的——我一直是 -tostop 的啊,也没见着别的时候收到 SIGTTOU。后来尝试了重定向 stdin,才发现其实是 SIGTTIN——也不知道 ffmpeg 读终端干什么。另外,给 sem 的命令传数据也挺不方便的:直接嵌在命令里,空格啥的会出问题,最后只好用环境变量了。

等全部处理完毕,for f in **/*_loudnorm.*; do ll -tr $f:r:s/_loudnorm//.$f:e $f; done | vim - 看了一眼,然后就发现问题了:有的文件变大了好多,有的文件变小了好多!检查之后发现是编码参数变了:mp3 文件全部变成 128kbps 了,而 flac 的采样格式从 s16 变成了 s32。

于是又写了个脚本带上参数重新处理。这次考虑到以后我还需要对单个新加的歌曲文件处理,所以要处理的文件通过命令行传递。

#!/bin/zsh -e

doit () {
  local f=$1
  local json=$f:r.json
  local output=$f:r_loudnorm.$f:e

  echo "Processing $f"

  if [[ -s $json || $f == *_loudnorm.* ]]; then
  else
    ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n '/^{$/,/^}$/p' > $json
  fi

  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
  else
    local args=()
    if [[ $f == *.mp3 || $f == *.m4a || $f == *.wma ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.m4a ]]; then
      local src_profile=$(ffprobe -v error -select_streams a:0 -show_entries stream=profile -of json $f | jq -r '.streams[0].profile')
      if [[ $src_profile == HE-AAC ]]; then
        args=($args -acodec libfdk_aac -profile:a aac_he)
      fi
    fi
    if [[ $f == *.opus ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.ogg ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      if [[ $src_bitrate == null ]]; then
        src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      fi
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.flac ]]; then
      local src_sample_fmt=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_fmt -of json $f | jq -r '.streams[0].sample_fmt')
      args=($args -sample_fmt:a $src_sample_fmt)
    fi
    ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) $args -vcodec copy $output </dev/null
    touch -r $f $output
  fi

}

for f in "$@"; do
  doit $f
done

然后我就神奇地发现,sem 不好用的问题突然没有了——我直接 parallel loudnorm ::: 文件们 就好了嘛……

使用 PipeWire 实现自动应用均衡器

作者 依云
2024年6月22日 15:51

本文来自依云's Blog,转载请注明。

之前我写过一篇文章,讲述我使用 EasyEffects 的均衡器来调整 Bose 音箱的音效。最近读者 RNE 留言说可以直接通过 PipeWire 实现,于是前几天我实现了一下。

先说一下换了之后的体验。相比于 EasyEffects,使用 PipeWire 实现此功能获得了以下好处:

  • 少用一个软件(虽然并没有多大)。
  • 不依赖图形界面。EasyEffects 没有图形界面是启动不了的。
  • 占用内存少。EasyEffects 有时候会占用很多内存,不知道是什么问题。
  • 自己实现的切换逻辑,更符合自己的需求。EasyEffects 只能针对指定设备加载指定的配置,不能指定未知设备加载什么配置。因此,当我的内置扬声器名称在「alsa_output.pci-0000_00_1f.3.analog-stereo」、「alsa_output.pci-0000_00_1f.3.7.analog-stereo」或者「alsa_output.pci-0000_00_1f.3.13.analog-stereo」等之间变化时,我需要一个个名称地指定要加载的配置。
  • 只要打开 pavucontrol 就能确认均衡器是否被应用了。EasyEffects 需要按两下Shift-Tab和空格(或者找找鼠标)来切换界面。

缺点嘛,就是我偶尔使用的「自动增益」功能没啦。不过自动增益的效果并不太好,我都是手动按需开关的。没了就没了吧。

配置方法

首先要定义均衡器。创建「~/.config/pipewire/pipewire.conf.d/bose-eq.conf」文件,按《Linux好声音(干净的均衡器)》一文的方式把均衡器定义写进去就好了。我的文件见 GitHub

然后需要在合适的时候使用这个均衡器。实际上上述配置加载之后,PipeWire 里就会多出来一对名叫「Bose Equalizer Sink」的设备,一个 source 一个 sink。把 source 接到音箱,播放声音的程序接到 sink,就用上了。别问我为什么 source 的名字也是「Sink」,我不会分开定义它们的名字……

自动化应用使用的是 WirePlumber 脚本。它应该放在「~/.local/share/wireplumber/scripts」里,但是我为了方便放到 dotconfig 仓库里管理,在这里放了个到「~/.config/wireplumber/scripts」的软链接。脚本干的事情很简单:在选择输出设备的时候,看看当前默认设备是不是 Bose 音箱;如果是,就选择之前定义的「Bose Equalizer Sink」作为输出目标。不过因为文档匮乏,为了干成这件事花了我不少精力,翻看了不少 WirePlumber 自带脚本和源码。最终的脚本也在 GitHub 上

结语

PipeWire 挺强大的,就是文档太「瘦弱」啦。能用脚本配置的软件都很棒~

再次感谢读者 RNE 的留言~

使用 atuin 管理 shell 命令历史

作者 依云
2024年1月13日 21:54

本文来自依云's Blog,转载请注明。

atuin 是最近在群里看到的工具。功能和我自己用 skim 糊的脚本一样,搜索并执行 shell 的命令历史用的。但是,它的数据存储使用的是 SQLite3,并且它是使用 Rust 编程语言编写的。于是事情有了一些好的变化。

首先,因为 atuin 并不像 Web 服务那样,会持续打开并操作数据库,所以 SQLite3 并发容易报错的问题并不需要担心。而 atuin 会记录执行时间、耗时、工作目录和退出码等信息。更多的元信息,能给之后的搜索和分析提供更多帮助。

其次,因为搜索走的数据库查询,因此并不需要像我用 skim 那样,每次把全部历史加载到内存。这样就可以保留更多的历史记录而不用怕越用越慢了。不知道 SQLite3 的搜索功能效率如何,但我几万条记录,已经可以明显感觉到加载耗时的差异了。

最后,它是 Rust 写的——这点很重要,因为这大大地方便了我对它进行修改(而不像某 Zig 写的工具,我翻了半天文档都没改对最后只好放弃了)。

当然,让我没多犹豫就决定尝试 atuin 的最重要的原因是:它独立于 shell 原本的历史记录功能,并不会取而代之。所以它要是不合我意,我只要把它删掉就好了,原本的历史记录丝毫不受影响。

于是我到现在已经用半个月了,结果非常满意。不过也已经对它做了好多修改了。比较重要的如下:

  1. 支持 Shift+Del 键删除记录。有时候会不小心打错命令。这种命令记在历史里时不时被翻出来,不但占用显示空间,还容易不小心选错然后相同的错误又犯一遍,甚至因为没看清命令细节而不小心删掉好多文件(还好我有快照)。atuin 的作者最近也加了删除功能,但是是在另一个界面操作,对于我这种经常在找命令的时候要删除多条命令的用法来说,不光麻烦,而且上下文切换的代价很大,会忘记自己原本是要干什么的。

  2. 精确匹配模式,这是 skim 的叫法。你叫它多子字符串匹配也行。自从 fzf 流行以来,大家都迷上了子序列匹配的所谓「模糊匹配(fuzzy match)」。但是我不喜欢这种匹配方法,会给出太多不相关的结果,加大脑力负担。真正好的模糊匹配是 agrep 那种基于编辑距离的算法,打错点字没关系那种。所以我给 atuin 也加上了精确匹配模式,同时还提升了查询性能呢。

  3. 反转 UI 的 --invert 命令行选项。像我之前使用 skim 那样,当光标位于终端窗口的上半部分时,我希望我搜索时打字的地方在上方;而当光标来到终端窗口的下半部分时,搜索时光标也放下边。这样关注的焦点就不会跳很远,有连贯性,节省认知脑力。atuin 本身有反转 UI 的功能,但是是写在配置文件里的,而我需要视情况决定要不要反转 UI,所以还是得加个命令行选项。

  4. 更改了选中项的颜色。atuin 原本用的是红色,我总觉得是哪里有报错……

还有些不太重要的修改,可以来我的分支查看:https://github.com/lilydjwg/atuin/commits/lilydjwg。注意这个分支我会经常 rebase 的。

另外我修改过的 zsh 插件在 https://github.com/lilydjwg/dotzsh/blob/master/plugins/atuin.zsh

值得一提的是,atuin 还支持同步。同步的数据本身是加密的,但是还是会泄露一堆诸如什么时候在跑命令之类的元数据,所以我自己跑了个服务。服务很好跑,但是同步似乎有些问题。我两个系统,两边都导入了之前的历史记录并同步,但是后同步的那个系统上的历史,很难被同步过去。甚至 atuin 发现本地比远程多,就从最新开始慢慢上传,直到两边一样多;如果远程比本地多,那就把远程的删掉一些(我也不知道它删了哪些,我是看到访问日志里巨量的 DELETE 请求才意识到问题的)。总之经过我不懈地反复运行,最终它只比远程多几条记录了,并且绝大部分历史记录已经两边都有了。我猜它可能没想到我会从不同的系统上同步已有且已分歧的数据吧……

让离线软件真正离线

作者 依云
2022年9月7日 15:12

本文来自依云's Blog,转载请注明。

去年我做了个索引 Telegram 群组的软件——落絮,终于可以搜索到群里的中文消息了。然而后来发现,好多消息群友都是通过截图发送的,落絮就索引不到了。也不能不让人截图嘛,毕竟很多人描述能力有限,甚至让复制粘贴都能粘出错,截图就相对客观真实可靠多了。

所以落絮想要 OCR。我知道百度有 OCR 服务,但是我显然不会在落絮上使用。我平常使用的 OCR 工具是 tesseract,不少开源软件也用的它。它对英文的识别能力还可以,尤其是可自定义字符集所以识别 IP 地址的效果非常好,但是对中文的识别能力不怎么样,图片稍有不清晰(比如被 Telegram JPEG 压缩)、变形(比如拍照),它就乱得一塌糊涂,就不说它给汉字之间加空格是啥奇怪行为了。

后来听群友说 PaddleOCR 的中文识别效果非常好。我实际测试了一下,确实相当不错,而且完全离线工作还开源。但是,开源是开源了,我又没能力审查它所有的代码,用户量太小也不能指望「有足够多的眼睛」。作为基于机器学习的软件,它也继承了该领域十分复杂难解的构建过程,甚至依赖了个叫「opencv-contrib-python」的自带了 ffmpeg、Qt5、OpenSSL、XCB 各种库的、不知道干什么的组件,试图编译某个旧版 numpy 结果由于太旧不支持 Python 3.10 而失败。所以我决定在 Debian chroot 里安装,那边有 Python 3.9 可以直接使用预编译包。所以问题来了:这么一大堆来源不明的二进制库,用起来真的安全吗?

我不知道。但是我知道,如果它联不上网的话,那还是相对安全的。毕竟我最关心的就是隐私安全——一定不能把群友发的图片泄漏给未知的第三方。而且联不上网的话,不管你是要 DDoS 别人、还是想挖矿,收不到指令、传不出数据,都行不通了嘛。我只要它能从外界读取图片,然后把识别的结果返回给我就好了。

于是一个简单的办法是,拿 bwrap 给它个只能访问自己的独立网络空间它不就访问不了互联网了吗?不过说起来简单,做起来还真不容易。首先,debootstrap 需要使用 root 执行,执行完之后再 chown。为了进一步限制权限,我使用了 subuid,但这也使得事情复杂了起来——我自己都难以访问到它了。几经摸索,我找到了让我进入这个 chroot 环境的方法:

#!/bin/bash -e

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --cap-add ALL \
  --uid 0 \
  --gid 0 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --share-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  -- \
  /bin/bash -l

这里给了联网权限,是因为我需要安装 PaddleOCR。没有在创建好 chroot 之后、chown 之前安装,是因为我觉得拿着虽然在 chroot 里但依旧真实的 root 权限装不信任的软件实在是风险太大了。装好之后,再随便找个图,每种语言都识别一遍,让它下载好各种语言的模型,接下来它就再也上不了网啦(为避免恶意代码储存数据在有网的时候再发送):

#!/bin/bash -e

dir="$(dirname $2)"
file="$(basename $2)"

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --uid 1000 \
  --gid 1000 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --unshare-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  --ro-bind "$dir" /workspace --chdir /workspace \
  --setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  --setenv HOME /home/worker \
  -- \
  /home/worker/paddleocr/ocr.py "$1" "$file"

kill $child_pid

这个脚本会把指定文件所在的目录挂载到 chroot 内部,然后对着这个文件调用 PaddleOCR 来识别并通过返回结果。这个调用 PaddleOCR 的 ocr.py 脚本位于我的 paddleocr-web 项目

不过这也太复杂了。后来我又使用 systemd 做了个服务,简单多了:

[Unit]
Description=PaddleOCR HTTP service

[Service]
Type=exec
RootDirectory=/var/lib/machines/lxc-debian/
ExecStart=/home/lilydjwg/PaddleOCR/paddleocr-http --loglevel=warn -j 2
Restart=on-failure
RestartSec=5s

User=1000
NoNewPrivileges=true
PrivateTmp=true
CapabilityBoundingSet=
IPAddressAllow=localhost
IPAddressDeny=any
SocketBindAllow=tcp:端口号
SocketBindDeny=any
SystemCallArchitectures=native
SystemCallFilter=~connect

[Install]
WantedBy=multi-user.target

这里的「paddleocr-http」脚本就是 paddleocr-web 里那个「server.py」。

但它的防护力也差了一些。首先这里只限制了它只能访问本地网络,TCP 方面只允许它绑定指定的端口、不允许调用 connect 系统调用,但是它依旧能向本地发送 UDP 包。其次运行这个进程的用户就是我自己的用户,虽然被 chroot 到了容器里应该出不来。嗯,我大概应该给它换个用户,比如 uid 1500,应该能起到跟 subuid 差不多的效果。

顺便提一句,这个 PaddleOCR 说的是支持那么多种语言,但实际上只有简体中文等少数语言支持得好(繁体都不怎么样),别的语言甚至连语言名和缩写都弄错,越南语识别出来附加符号几乎全军覆没。

我所讨厌的网页行为

作者 依云
2022年8月18日 17:14

本文来自依云's Blog,转载请注明。

有些网页的行为通常不被视为 bug,甚至是故意为之,但很令人讨厌。这里记录一些我所讨厌的网页「特性」。它们被归为两类,要么导致某些场景下用不了,或者用着很不方便,要么很打扰人。

可访问性问题

忽视系统、浏览器设置,在浏览器使用浅色主题的情况下默认使用深色主题,或者在浅色主题下代码部分使用深色主题。反过来问题不大,因为我有 DarkReader

主体文本不支持选择和复制。选择和复制之后,用户能做很多事情,比如查生字、翻译、搜索相关主题。

已访问链接与未访问链接显示没有差别。

消除可交互元素(链接、按钮)的 outline。这个 outline 以前是虚线框,现在火狐改成了蓝色框,用于标识当前键盘交互的对象。

搜索框不支持回车确认,必须换鼠标点击。

位于文本框后的按钮不支持使用 Tab 键切换过去,并且 Tab 键在此文本框中也没有任何显著的作用。必须换鼠标点击。

需要交互的元素不能被 vimium 插件识别为可点击。这大概是使用非交互元素来处理交互事件,甚至事件监听器都不在元素本身上。

使用 JavaScript 实现原本可以直接用链接实现的内容(链接目标是某个 JavaScript 函数调用)。这导致我无法使用中键来在新标签页中打开。

显著不同的内容没有独立的 URL。尤其见于一些单页应用(SPA)。要到达特定内容(比如加书签或者分享给别人),就只能记录先点哪里、再点哪里等。

预设用户的屏幕大小,导致浏览器窗口过小的时候部分关键内容(如登录按钮)看不到、无法操作。

交互元素没有无障碍标签。成堆的「未加标签 按钮」。

通过 User-Agent 判断浏览器,并拒绝为某些 User-Agent 服务(但实际上去除这个限制之后,功能是完全没有问题的)。

当没有带声音自动播放权限时,无声播放主体内容(而非等待用户操作使其获得权限)。说的就是 Bilibili。

为大屏幕用户(如桌面用户)展示为手机屏幕设计的页面。这些页面中字体特别巨大,并且不能被浏览器缩放影响。交互元素上鼠标指针不改变为手状,甚至只支持触摸操作而不支持鼠标点击。

悬浮于主内容之上的「在App中打开」。点名批评 imgur。它的按钮不光挡住图片,而且用户放大图片的时候它也被放大,挡住更多图片内容。

不能禁用的图片懒加载,或者视频内容被移出画面、切换到后台就停止加载。点名批评 Telegram、维基百科。我等你加载呢,你非要我盯着看你加载浪费时间?现在网好,你赶紧给我加载好,进电梯或者地铁或者山洞了,我再慢慢看你的内容啊。

视频内容被移出画面就停止播放。点名批评知乎。我让你播放你就给我播放。我不看视频,是因为视频画面没啥可看的,可是我听音频部分呀。

覆盖浏览器的 Ctrl-F 查找快捷键,并不提供方案来避免覆盖。我就搜索当前页面,不要你的站内搜索功能。

注册前请务必先阅读用户条款和规则,用户条款和规则页面需登录后才可访问。

简体中文内容指定繁体中文的字体,或者添加繁体中文的标签。或者反过来。

打扰用户

在内容页面,任何会动的非主体内容,包括但不限于广告、内容推荐。形式可以是动态 GIF、滚动动画、视频等。用于首页渲染效果的背景动画和视频不算,作为主体内容者也不算。

针对非音视频网站,自动或者非用户明确表达地(比如在用户点击不相关内容时)播放带音频的内容。

消耗 CPU 的背景特效。如 canvas-nest。会让 CPU 很吵,也会浪费能源、加剧气候变化。

tmux 状态栏优化

作者 依云
2022年8月10日 18:43

本文来自依云's Blog,转载请注明。

在 tmux 的状态栏里,通常会显示当前时间。配置起来也非常简单,%Y-%m-%d %H:%M:%S这样的时间格式化字符串扔过去就可以了。然而这样做有个小问题:这个时间只能精确到秒。我的意思不是说我想让它显示毫秒,而是希望它像电视台和广播电台的时间一样,显示(播报)「12:00:00」的时候,就刚好是这一秒的开始。

一般来说,这么延迟个一秒以内的随机数问题不大,除了你有多个这种时间戳的时候——

tmux inside tmux inside tmux

这些时间戳哪个先更新、哪个后更新可完全说不准的,你可能看到明明在地球另一边的服务器上先到某一秒,本地才跟上。甚至同一个 tmux 的不同客户端里,这个时间戳的更新时间都可能会有差异。

我想优化这个的另一个原因是,我经常使用 extrace 来查看程序调用另一程序使用的命令行参数,然而我本地连了多少个 tmux,每秒便会有多少个 sh + awk 进程出来读系统负载。尤其是我从 Awesome 换到 Wayfire 之后,顶栏改用 waybar 了,很多指示器都是内建或者自己写的外部脚本,不再需要每隔几秒跑个子进程去获取信息,这样 tmux 调用子进程来刷新状态造成的干扰就突显了出来。

于是就有了 accurate-time 程序。它每个整秒会去读系统负载,然后和当前时间一起送给 tmux 来显示。每秒一个进程,已经少了很多啦。

既然是我的程序自己来读负载,也就方便做更多事情了,比如根据负载情况使用不同的文字颜色:绿色表示低负载,灰白是稍微有点活干,蓝色和 cyan 是比较忙碌,黄色、品红表示已经忙不过来啦,红色就是要累趴下啦。之前偶然间发现 qemu-git 这个包使用 ninja、但是链接的时候又套了一层 make,造成系统负载冲到了两百多。但是无论高低,tmux 的负载显示都是红色,所以我可能之前已经视而不见许多次了。加上颜色之后,这类异常就更容易被注意到了。以前我本地每次风扇呼呼地转才发现系统负载高,但是我要是用耳机的话就听不到了,现在也多了个高负载的指示。

安装和配置很简单,cargo build --release 编译,然后把编译出来的 target/release/accurate-time 扔到 $PATH 里,再如下配置 tmux 状态栏右边即可:

if-shell "accurate-time tmux" {
  set -g status-interval 0
} {
  set -g status-interval 1
  set -g status-right "#[fg=red]#(awk '{print $1, $2, $3}' /proc/loadavg) #[fg=colour15]%Y-%m-%d %H:%M:%S"
}

本来我还打算给 waybar 上的时间也这么做一下的,不过程序写好了才发现 waybar 自己已经把时间对齐到更新间隔了。

微信消息通知的困扰

作者 依云
2022年3月26日 17:55

本文来自依云's Blog,转载请注明。

一直以来,不得不用的微信以其糟糕的通知体验让我十分不爽。

在手机上,我使用的是 Google Play 商店里的微信。在电脑上,我使用的是通过 Wine 运行的 Windows 版本微信([archlinuxcn] 仓库里的 wine-wechat-setup 脚本可用于安装)。

消息通知不及时

这个问题是最近我的手机日渐陈旧之后我才注意到的。表现是,在一段时间(比如一两天)不使用微信之后,收到新的微信消息或者视频通话,可能会延迟几个小时收到通知。在 Android 通知日志中可以确认,收到通知的时间和消息在微信中展示的时间有数小时之差,并不是因为我没有及时看手机。

我的 Telegram 从来不这样丢消息,即使因为后台进程过多 Telegram 被杀之后,通知只会不能在其它端阅读之后被清除,而不会延迟那么久。而微信,即使它还在后台运行着,却经常占着资源不干活,何况消息通知本应走 FCM。

打开微信即清除所有通知

我有一条微信消息,但是我现在不方便立即回复(比如需要使用电脑而我正出门在外),所以我会让那条通知一直留着。其实以前版本的 Android 系统更方便,可以将通知延后一段时间,只是不能自动指定延后的时间比较遗憾,后来被移除真的太可惜了。

然后呢,比如我要进个超市,或者测个核酸,付个钱啥的,只好打开微信扫码呗。结果所有还未处理的通知全部不见了!等忙完当时的事情,回到家里的时候,我就不一定还记得我还有几条微信消息还没处理了。

多端不同步容易错过

我在用电脑的时候,如果不登录电脑版微信的话,那么我将不会注意到手机上的微信有新消息。那就登录电脑版吧,然后我去上个厕所吃个饭,不用电脑的时候又会错过消息。在电脑端登录的时候让手机上也显示消息通知?那样所有消息都要看两遍,而且消息多的时候还得仔细回想某条消息到底是不是已经处理了。

Telegram 的消息同步做得真好啊。你在哪端用,哪端先给你发通知。其它端的消息会晚几秒出现。一旦在任意端读取了消息,另外的端上的相应消息全部都会被取消掉,不会有消息重复的问题(不过最近好像 Android 上的通知取消变得不那么可靠了)。

手机不可离远

现在电脑版微信终于不需要天天在手机上确认登录了。只要我每两三天登录一次,就可以避免把手机扔去充电了、来到电脑前、又去找手机的麻烦事。——我一开始是这么以为的。直到我发现,我刚刚看到通知、正要处理的消息,并没有在电脑版微信里同步出来。

不知道为什么,微信跟电脑用户有仇似的。明明我有电脑了,不需要凑合于手机的小屏幕和戳戳戳的屏幕键盘了,微信还非得把手机给拉过来。

语音通话不支持耳机接听

某天,我因为沉迷于放置型游戏,把手机扔桌子上充上电让它自个儿玩,自己去睡觉了。第二天早上睡正酣的时候,来了个电话,我拿耳机给接了。然后需要通话的另一方不知道怎么想的,没有商量就发起了微信语音通话,这个耳机根本接不到……

我也尝试过让 Google 助理回拨电话,不过使用不熟练,并没有成功。你说我为什么不起床去接?我睡着被电话惊醒了,还没回过神来啊 QAQ。

Wayfire 迁移进展(四):不那么 high 的 DPI

作者 依云
2022年2月2日 17:23

本文来自依云's Blog,转载请注明。

使用24寸4k屏幕作为主屏的时候很简单,设置 scale 为 2 就好了。但是,当 2 嫌太大、1 嫌太小的时候,问题就来了。比如我希望使用 120dpi,把 scale 设置为 1.25 可好?

scale=1.25 text

而这才是理想的效果:

120dpi text

看不出来差别?放大八倍,你看差别多明显:

8x compare

正常 120dpi 渲染出来的文字边缘清晰犀利,次像素平滑左红右蓝。再看看 scale=1.25 的文字,线条经常糊掉,次像素平滑效果几乎完全被抹掉。实际看上去的效果就是跟透明麿沙玻璃看屏幕似的,线条边缘总是有点糊糊的感觉,1080p 的屏幕被降级成了 720p 似的。

之所以出现这样的情况,是因为 Wayland 只支持整数倍缩放。因为,Wayland 混成器不能告诉客户端你得把窗口给画成 1.25 倍的,而客户端也无法告诉混成器我这个图像画的是 1.25 倍。所以,混成器只好告诉客户端你给我画个 2 倍的图像吧。混成器拿到图像之后再缩小 0.625 倍,自然有些逻辑像素就不能对应到单个的物理像素上去了。

所以,我还是设置 scale=1,不要混成器帮我去缩放。我自己通过另外的办法告诉客户端把字写大点儿。图标之类的就顾不上啦,反而大点小点都还能看。比如我要 1.25 倍大小的文字,就这样做:

  • GTK 3:在 dconf 里设置org.gnome.desktop.interface.text-scaling-factor=1.25就好了。最开始的截图就是 dconf-editor 里这一项配置。
  • Qt:设置环境变量 QT_WAYLAND_FORCE_DPI=120
  • Telegram:除了上边这个环境变量外,额外地在它自己的设置里设置 150% 的缩放(Telegram 的字偏小所以要设置得大一些)。设置环境变量是为了 fcitx5。
  • waybar:config 文件中设置 heightstyle.css 中设置 font-size
  • Xwayland:和 X11 下的 HiDPI 设置差不多的。比如 GTK 2 设置 Xresources Xft.dpi: 120 就好了。

我遇到的差不多就这些了。没办法,Linux 就是这么乱 QAQ。不过虽然 Wayland 协议不支持,好歹还有绕过的办法。

❌
❌