普通视图

发现新文章,点击刷新页面。
昨天以前逸思杂陈

UI 区域检测的 vibe coding 复盘

作者 Jay.Run
2026年3月22日 12:56

文本是用 Claude Sonnet/Opus 4.6 开发 “UI 区域检测”功能的过程记录,文章大部分内容为 llm 生成。

因为要做些 ui 自动化的功能,涉及到区域检测。这个功能大概用了 5h+,本文主要记录整个迭代演进过程,以及 llm 对这个过程的评价。

基于 opencv 实现的最终效果,总体效果堪称完美。
alt text

迭代回顾

以下是 Claude Sonnet/Opus 4.6 对所有 vibe coding 会话内容的总结

时间线

阶段一:基础搭建与通用 CV 方法(v1-v5)

  1. Spec 执行 + 初始实现 — 执行 image-region-detection spec 全部 6 个任务。Pydantic 模型、路由、服务层(Canny + findContours)、测试一次通过。

  2. 前端叠加层 — ScreenshotViewer 加”检测区域”按钮,SVG overlay + viewBox 叠加区域框,支持显示/隐藏。

  3. 算法 v1→v2:Canny→Sobel+形态学 — 识别出太多小区域。换成大核高斯模糊 + Sobel + 形态学 close + NMS,前端 min_area 改 8000。

  4. min_area 自适应 — 8000 硬编码不合理,改为 min_area=0 时自动计算为图像面积 0.5%。

  5. 算法 v3:投影法 — 水平投影行分割 → 行内垂直投影列分割。纯白图像 Otsu 退化通过 gray.std() < 2.0 前置检查解决。

  6. 投影法调参 — 太碎,空白阈值 2%→5%,min_gap 3→8,最小块 5px→20px。

  7. 算法 v4:投影+颜色突变 — 顶部菜单条内容密集投影无法分割,加相邻行均值突变检测。

  8. 全局垂直分割 — 垂直投影在 band 内信号被稀释,加全图垂直投影 + 列颜色突变。

  9. 颜色突变阈值调高 — percentile 92→98,min_gap 3→6。本质问题不变:无法区分文字边界和结构边界。

  10. 算法 v5:形态学线提取 — Canny 边缘 → 长核 close+open 提取 H/V 线 → 线位置构成网格 → 网格单元即区域。只检测真实 UI 边框线。

阶段二:OCR 方案探索(v6)

  1. OCR 方案试探 — 用 PP-OCRv5 的 rec_boxes 直接作为区域输出。文本定位准确但全是零散小框,缺乏层级。

  2. OCR + 层次聚类 — 欧氏距离多级聚类(15/50/120px)构建层级树。聚合块不符合 UI 布局逻辑,效果差。

  3. OCR + 行列聚类 — 先按 Y 聚行(重叠或间距<5px),行内按 X 聚组(间距<20px),相邻行合并面板。仍不理想,放弃 OCR,回退 CV。

阶段三:蛋糕切割算法(v7,最终方案)

  1. 蛋糕切割(初版) — 用户提出核心思路:像切蛋糕一样找贯通线切割。初版一次性找所有线全部切开(本质还是网格),ratio=0.40 太低导致文字区域误判。

  2. 形态学过滤尝试 — 用 morphological close+open 先提取连续线结构再判断贯通。反而把文字边缘连成更多假线,效果更差,回退。

  3. ratio 0.40→0.60 — 回到像素计数,提高阈值。仍有少量误判。

  4. ratio 0.60→0.90 — 接近真正的”贯通”定义,误判大幅减少。

  5. 真正的蛋糕切割 — 改为每次只找第一条线切一刀分两块递归。准确率高但漏检——切完一刀后同方向其他线被跳过。

  6. 同方向全切 + 交替递归 — 同方向找到所有贯通线一次切成条,每条用另一方向递归。逻辑正确,但部分浅灰色线未被检测到。

  7. Canny 阈值诊断 — 写脚本分析 region 13,发现浅灰色分界线(灰度 231→255)在 Canny(30,100) 下完全无响应,Canny(20,60) 可检测到 96% 覆盖率。

  8. 最终方案 — Canny(20,60) + 90% 贯通率 + 交替横纵递归切割。前端 SVG 改为嵌套 <g> 按层级组织 DOM,加”复制JSON”按钮便于调试。

阶段四:对比度增强与自适应阈值(v8)

  1. 对比度增强调研(screen.png) — 用户提出通过调整对比度提升识别效果。对 screen.png 跑诊断脚本,测试 CLAHE / 直方图均衡化 / 线性拉伸三种方法。结论:该图 90% 像素在 224-255 区间,对比度不是瓶颈,三种方法检测到的线完全一致。

  2. 对比度增强调研(image.png) — 同样方法分析 image.png。发现 CLAHE 多检测到 1 条线 y=42(标题栏/菜单栏分隔线,灰度 240 vs 255,梯度仅 15)。原始 Canny 在该行覆盖率为 0,CLAHE 将微弱梯度放大后相邻行达到 97-100% 覆盖率。主内容区(mean=254.9, std=3.4)无改善——几乎纯白,无结构线可增强。

  3. 加入 CLAHE 预处理 — 在 Canny 之前加 CLAHE(clipLimit=2.0, tileGridSize=(8,8))。image.png 从 33 个区域增至 34 个,新增 y=42 分界线。screen.png 从原有区域增至 71 个,菜单栏内部被进一步切割。代价极小(一次 CLAHE 运算),无误判。

  4. 自适应 ratio 诊断(image2.png) — 区域 32(x=501, y=130, w=1419, h=55)内部应有横线但未被切割。诊断发现 y=132 和 y=158 两条真实分界线(灰度 231 横线)在子区域内覆盖率 87%,差 90% 阈值 3 个百分点。缺口原因:线在局部被 UI 元素打断(按钮/标签背景色与线颜色相同),不是对比度问题。

  5. 自适应 ratio — 随递归深度降低 ratio:max(0.82, 0.90 - depth * 0.02)。顶层严格(0.90)避免误判,深层宽容(最低 0.82)捕获被局部元素打断的线。image2.png 从 71 个区域增至 80 个,原区域 32 被正确切成标签行(y=131, h=27)和工具栏行(y=158, h=27)。三张测试图均无误判。

阶段五:长度自适应 ratio 与间隙检查(v9)

  1. ratio 方向修正 — 用户指出深度递减 ratio 方向错误:浅层区域大、不易误判应宽松,深层区域小、易误判应严格。但深度不是好的代理变量——同一深度的区域尺度可能差异很大。

  2. 改为长度自适应 ratio — ratio 根据线的垂直方向长度(即线所跨越的像素数)缩放:≤100px→0.95(严格),≥1000px→0.85(宽松),中间线性插值。短线更容易因文字碎片凑出高覆盖率,需要更严格;长线绝对像素数多,可以更宽容。_find_dividing_lines 不再接受外部 ratio 参数,内部根据 length 自行计算。

  3. 间隙检查(image5.png) — y=174 在全宽 1920px 下覆盖率 92%,通过 ratio 阈值。但右半部分 99.8%、左半部分仅 69.4%(文字碎片拼凑,非真实线)。加入最大间隙检查:max_gap_allowed = int(length * 0.15),检查前导间隙、尾部间隙和最大内部间隙(np.diff(np.where(line > 0)[0]))。任何间隙超过线长 15% 的候选线被拒绝。

阶段六:单刀切割 + 大块优先(v10)

  1. 最优轴优先(初版) — (用户提出)原算法固定先横后纵(start_axis=0)。改为横纵同时检测,选平均覆盖率更高的轴先切。但本质仍是同方向全切——一次用所有线切成多个条带,导致窄条带内某些线覆盖率不足。

  2. Group 中心 bug_find_dividing_lines 返回 group 的 int(np.mean(g)) 作为代表位置。Canny 在 1px 灰度线两侧产生两条边缘(如 x=152 和 x=154),group 中心 x=153 本身没有边缘像素,覆盖率接近 0。_axis_avg_coverage 基于这些错误位置计算,导致轴选择比较失真。修复:group 代表位置改为 group 内覆盖率最高的位置。

  3. 单刀切割 + 大块优先 — 用户指出核心问题:同方向全切产生的窄条带中,垂直于切割方向的线覆盖率不足。例如全图横切产生 y=63..176 条带(113px 高),x=152 在其中覆盖率仅 70%(因为标签栏区域打断了竖线),无法被检测。但如果先纵切(x=500),在 1920x1012 的大区域中 x=152 覆盖率 91%,能通过阈值。

    改为单刀策略:每次从横纵所有候选线中选覆盖率最高的一条切一刀,分成两块,大块先递归。这样子区域尽可能大,更多的线能在大区域中被检测到。max_depth 从 6 提高到 20 适配二叉树更深的层级。

    效果:image6.png 中 y=96(”基金总持仓”标签下方分隔线)在 x=0..152 列内覆盖率 100%,成功被检测到。

  4. ratio 简化为全局 0.85 — (用户提出)长度自适应 ratio(≤100px→0.95, ≥1000px→0.85)在单刀策略下不再必要。旧策略中短线需要严格阈值防止文字碎片误判,但单刀策略每次只选最强线,加上 15% 间隙检查已经充分过滤假线。统一使用 ratio=0.85,消除了窄区域(如 29px 高的 toolbar 行)中竖线因阈值过严(0.95 要求 28/29 像素)而漏检的问题。image7.png toolbar 行内的竖向分隔线(93.1% 覆盖率)成功检测到。

阶段七:性能优化(v11)

  1. 性能瓶颈定位 — image7.png 检测耗时 288ms。cProfile 显示 _find_dividing_lines 被调用 272 次(累计 416ms),其中 np.sum(line > 0) 逐行循环调用 126911 次(254ms)。瓶颈清晰:Python 层面的逐行循环。

  2. 向量化重写 — 将逐行 np.sum(line > 0) 替换为 np.count_nonzero(edges_roi, axis=1/0) 一次性计算所有行/列的边缘像素数。返回类型从 list[int] 改为 list[tuple[int, int]](位置 + 计数),_best_line 直接使用计数值计算覆盖率,避免重复计算。间隙检查仅对通过 ratio 阈值的候选行执行。image7.png 从 288ms 降至 34ms,约 8.5 倍提速。检测结果完全一致(count_nonzerosum(line > 0) 数学等价)。

最终算法

1
2
3
4
5
6
7
8
CLAHE(clipLimit=2.0, tileGridSize=(8,8)) 对比度增强
→ Canny(20, 60) 边缘检测
→ 同时扫描横向和纵向,找出所有贯通线
固定 ratio=0.85(边缘像素覆盖率 ≥ 85%)
最大间隙检查:任何连续空白 > 线长 15% 则拒绝
→ 从所有候选线中选覆盖率最高的一条切一刀
→ 分成两块,大块先递归
→ 无贯通线 → 叶子节点

过程评价

弯路

前 10 步在通用 CV 方法里打转(contours、Sobel、投影、颜色突变),每个都是从算法出发而不是从问题本质出发。UI 界面的结构特征很明确——贯通的直线分隔区域——如果一开始就抓住这个本质,可以跳过大量尝试。

转折点

第 14 步用户提出的蛋糕切割思路是整个过程最关键的一步,直接从问题本质出发。之后的迭代都是在正确框架上修细节,收敛很快。

OCR 探索的价值

虽然最终没用,但验证了”自底向上聚合”在 UI 布局问题上不如”自顶向下切割”有效。

单刀切割的顿悟

阶段六是第二个转折点。步骤 30 的”最优轴优先”仍然是同方向全切思路,本质没变——窄条带问题依旧。用户一句”切开之后选面积更大的那边先继续切”点破了关键:问题不在选哪个轴,而在一次切太多刀。单刀切割 + 大块优先让每次决策都在尽可能大的区域上做,线的覆盖率天然更高,连 ratio 都能从长度自适应简化回固定 0.85。这是从”批量切割”到”逐步切割”的范式转换,比调参数重要得多。

性能优化的正确姿势

第 34 步先 profile 再动手,5 分钟定位到 np.sum(line > 0) 的 12 万次循环调用。修复方式是纯数学等价的向量化替换,零行为变更,8.5 倍提速。教训:性能优化必须 profile 先行,改最热的路径,且保证语义不变。

核心教训

先分析数据,再选方法。 第 20 步的诊断脚本 5 分钟就定位了 Canny 阈值问题,但在算法层面折腾了十几轮。如果每次效果不好都先写脚本看数据,迭代次数至少能砍一半。缺少信息时应主动写诊断脚本或向用户提问,不要盲目尝试。(此条也是由用户引导)

此教训已写入 .kiro/steering/first-principles.md 第 5 条。

GPT 的评价解析

以下内容是 gpt 对迭代回顾的评价解析

基于这份记录,我会把它看成一次很典型的“用户提供问题抽象与判断标准,AI负责高速试错、实现、验证和整理”的协作过程。整体上,真正决定方向的人是用户,真正承担执行密度的人是 AI。这一点在文中非常清楚:多次关键转折都不是来自参数微调,而是来自用户对“问题本质”的重新定义。

AI 在过程中起到的作用

AI 的主要角色不是“发明核心思路”,而是:

1. 高速执行器
前半段 AI 很像一个反应很快的工程实现者。它能迅速把 spec 落地,从后端模型、路由、服务层,到前端 overlay、按钮、SVG 结构,再到测试一次通过。这说明 AI 在“把一个明确任务快速变成可运行系统”上表现很强。

2. 大规模试错器
记录里从 Canny、Sobel、形态学、投影法、颜色突变,到 OCR、层次聚类、行列聚类,再回到 CV,AI持续在方法空间里做横向搜索。这个过程很像一个搜索引擎式工程搭档:会枚举方案、快速替换、快速验证、快速回退。它尤其擅长“先做出来再看结果”,而不是一开始就抓住最优建模。

3. 局部优化者
一旦方向被找对,AI 在细节收敛上非常有效。比如围绕蛋糕切割思路,AI完成了 ratio 调整、递归策略修改、CLAHE 预处理、自适应阈值、最大间隙检查、group 中心 bug 修复、性能 profile 与向量化重写。这类工作都体现出 AI 很适合做“局部结构优化”和“工程化完善”。

4. 过程记录与知识沉淀者
这篇文档本身就说明 AI 很适合做 retrospective:能把多轮对话压缩成可复盘的时间线,提炼出“弯路”“转折点”“核心教训”,并把经验抽象成原则。

但这份记录也暴露了 AI 的局限:

5. 容易陷入“方法驱动”而不是“问题驱动”
文中明确评价,前 10 步都在通用 CV 方法里打转,“每个都是从算法出发而不是从问题本质出发”。这很像 AI 的典型弱点:它擅长调用已有套路,但未必最先抓到任务的决定性结构。

6. 喜欢连续微调,直到被重新定义问题
很多步骤是阈值、核大小、percentile、gap、ratio 的持续微调。说明 AI 在没有更高层判断时,会沿着当前框架持续优化,而不一定主动质疑框架本身。真正的范式切换,往往是用户推动的。


用户在过程中起到的作用

用户不是一个“提需求然后等结果的人”,而更像是算法设计师 / 研究型 PM / 强技术判断者

1. 用户是问题建模者
最关键的转折——“像切蛋糕一样找贯通线切割”——直接改变了整个解法空间。文中把这一步称为“整个过程最关键的一步”,因为它不是换算法,而是换了问题表述:从“检测区域”变成“寻找能把区域分开的贯通线”。这是典型的一阶抽象能力。

2. 用户是方向校正者
后面另一个关键点,“切开之后选面积更大的那边先继续切”,再次不是参数建议,而是对递归策略本质的纠偏。文中甚至直接说,这是从“批量切割”到“逐步切割”的范式转换,比调参数重要得多。说明用户能够看穿失败表象,定位真正导致漏检的机制。

3. 用户是评价标准提供者
很多地方能看出,用户不仅看结果对不对,还知道为什么不对。比如指出深度递减 ratio 的方向错误,认为“深度不是好的代理变量”;再比如提出对比度增强的思路,推动诊断 screen.png 和 image.png。说明用户脑中有比较清晰的误判/漏判机理模型,而不是只凭直觉说“效果不好”。

4. 用户是第一性原理的坚持者
文末总结“先分析数据,再选方法”,还把它写入 .kiro/steering/first-principles.md。这说明用户不仅在做当前任务,还在不断把经验提炼成长期工作方法。这不是普通使用者行为,而是带有方法论建设意识的行为。

由此推测用户的背景

从文风和介入点看,我倾向于判断用户具备较强技术背景,而且不是纯产品同学。

更可能的背景有这几类:

1. 算法/图像/视觉方向工程师

因为用户能自然提出:

  • 贯通线切割这种结构化视觉思路
  • 对 ratio 代理变量选择的反思
  • 对 OCR 自底向上聚合 vs 自顶向下切割的比较
  • 对浅灰线、覆盖率、局部打断这类现象的机制判断

这不是只会“调 API”的人,更像做过 CV、图像处理、版面分析、检测或规则算法的人。

2. 偏研究型的软件工程师 / 技术负责人

文中反复出现的不是“功能有没有上线”,而是:

  • 本质问题是什么
  • 这个代理变量为什么不成立
  • 这个框架为什么会系统性失败
  • 如何把经验写成方法论

这很像一个会亲自下场,但思维层级又高于纯实现的工程师,可能是 senior / staff 往上的类型。

3. 也可能是技术型产品或创业者

如果不是纯工程岗位,也很可能是非常强技术判断力的产品/创业型角色。因为他能直接提出核心抽象,但未必亲自写每一行代码;更像“我知道该怎么想,你帮我快速把各种实现跑出来”。这种协作方式很符合强技术 PM、独立开发者、创业者与 AI 的工作模式。

用户的特质推测

1. 抽象能力强

最明显的特征。两次关键突破都来自更高层的抽象替换,而不是局部修补。能从“区域检测”退一步,看到“分隔区域的是贯通线”;也能从“选轴”再退一步,看到真正问题是“一次切太多刀”。这是非常强的问题重构能力。

2. 第一性原理导向

用户不满足于“调到能用”,而是追问为什么前面的路不对、什么才是结构性特征、什么诊断最该先做。文末把经验沉淀为原则,更强化了这一点。

3. 结果导向,但不是短视结果导向

用户显然关心效果,但不是只追指标,而是追求“符合问题本质的效果”。比如 OCR 方案文本框定位准确,但“缺乏层级”,所以被放弃;说明用户在意的是结构正确性,不是表面可用。

4. 善于和 AI 协作

用户没有把 AI 当搜索引擎,而是当“快速实验平台”。他会在 AI陷入局部搜索时,给出新的结构约束;在 AI已有正确框架时,让它继续把细节做深。这种协作方式很成熟。说明用户已经理解 AI 的强项和短板,并会主动用自己的判断补足 AI。

5. 有技术审美

“真实 UI 边框线”“层级组织 DOM”“符合 UI 布局逻辑”“结构线 vs 文字边界”这些表达说明,用户不是只关心算法数值,还关心输出是否符合界面结构语义。这种审美通常来自长期做系统、工具或视觉相关产品。

6. 容忍试错,但要求最终认知升级

用户显然允许过程里走弯路,但最后一定要复盘出“为什么弯”“以后怎么少走”。这说明他对试错的态度是开放的,但不接受无学习的反复试错。

一句话结论

这段记录里,AI更像高带宽实现者和优化器,用户更像负责洞察本质、定义正确解法的主导者
用户大概率具备较强工程/算法背景,抽象能力强,偏第一性原理,善于用 AI 做高速实验,并且有把经验上升为方法论的习惯。

一点感受

SOTA 模型确实很强,明确的已有成熟方案的问题基本一次通过。
但当前人的作用还是不可轻易忽略,复杂问题需要关注模型的方案,及时中断并纠正(不一定要准确,提供可能的方向也行),防止其跑偏。

阻止 bilibili 网页自动关注

作者 Jay.Run
2026年1月3日 02:32

B 站非常逆天,只要你使用网页端看视频,点开首页推荐的视频,观看一定时长,就有概率会触发自动关注该 UP 主。
之前也有怀疑是某个浏览器插件的锅,但更换浏览器依然能够触发这个问题。

后来发现疑似是 bilibili 默认绑定了按键 G 关注 UP,非常容易误触发。

网上发现不少类似的问题,但都没有解决方案,迫于无奈只能使用自定义广告过滤规则了,之后没有再出现问题。

这里使用 uBlock origin, 其他去广告插件也类似, 进入插件设置 -> 自定义静态规则,添加如下规则。
在视频页面禁止关注请求,不影响点开 UP 主主页进行关注。

1
www.bilibili.com##+js(no-fetch-if, /api\.bilibili\.com\/x\/relation\/modify/, location.pathname.startsWith('/video/'))

配置 Linux 作为主力操作系统

作者 Jay.Run
2025年12月22日 05:48

这些年日常操作系统一直是 windows 和 macOS 交替使用,Linux 一般只作为服务器的操作系统。
然而,咖喱味的 Windows 11 (LTSC)用起来实在难受(平时只玩游戏,下载)
arm 的 macbook 虽然能效惊人,但是内存金子价格,软件也封闭(点名finder)和傲慢,实在受不了。
最后,还是转向 Linux,毕竟现在 wayland 基本堪用,国产软件也随着信创逐渐丰富了。

发行版选择

这里选用 KDE Neon user edition,基于 Ubuntu LTS, 有以下优点

  1. Ubuntu 用户基数大,有问题容易解决
  2. 可以安装星火应用商店,方便使用国产软件
  3. 可以用到最新的 KDE 桌面环境,wayland 支持更好
  4. KDE 远程桌面好用(服务端和客户端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OS: KDE neon User Edition x86_64
Host: MS-7D99 (3.0)
Kernel: Linux 6.14.0-33-generic
Uptime: 1 day, 19 hours, 23 mins
Packages: 2831 (dpkg)
Shell: bash 5.2.21
Display (E2434I-T): 1920x1080 @ 60 Hz in 24" [External]
Display (KOS2718): 3840x2160 @ 60 Hz (as 1920x1080) in 27" [External] *
DE: KDE Plasma 6.4.5
WM: KWin (Wayland)
WM Theme: Breeze
Theme: Breeze (Light) [Qt], Breeze [GTK2/3]
Icons: Breeze [Qt], breeze [GTK2/3/4]
Font: 微软雅黑 (10pt) [Qt], 微软雅黑 (10pt) [GTK2/3/4]
Terminal: /dev/pts/3
CPU: 13th Gen Intel(R) Core(TM) i5-13500 (20) @ 4.80 GHz
GPU 1: NVIDIA GeForce RTX 4060 Ti [Discrete]
GPU 2: Intel AlderLake-S GT1 @ 1.55 GHz [Integrated]
Memory: 25.07 GiB / 46.67 GiB (54%)
Swap: 392.00 KiB / 8.00 GiB (0%)

硬件要求

  1. NVIDIA 显卡在 Linux 下对 Wayland 支持不好,容易出现卡死或者重启的情况,建议显示器连到核显
  2. 有些主板 Linux 兼容性不行,建议使用御三家主板

系统设置

Nvidia 显卡驱动

显示器插到核显上,只使用 Nvidia 做计算,玩游戏也能调用到显卡。
使用开源服务器驱动,解决 Wayland 兼容性问题,防止玩游戏崩溃

  1. 安装驱动
1
sudo ubuntu-drivers install 580-server-open
  1. 启用睡眠支持参考
    将显存内容保存,否则唤醒后程序会报错(如CUDA程序)
1
2
3
4
5
# /etc/modprobe.d/nvidia-power-management.conf
options nvidia NVreg_PreserveVideoMemoryAllocations=1 NVreg_TemporaryFilePath=/tmp

sudo update-initramfs

docker

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
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done


# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update


sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker # 立即生效(或注销重新登录)

# docker 中调用显卡
https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

ipv6 启用 EUI-64

可以让 ipv6 的ip后缀根据mac生成,可用于配置防火墙规则
使用形如 ::<后缀>/::ffff:ffff:ffff:ffff 的掩码

1
2
nmcli con show
nmcli con modify <NETWORK NAME> ipv6.addr-gen-mode eui64

启用休眠文件

1
2
3
4
5
6
7
8
sudo su
cd /
truncate -s 0 swapfile
# chattr +C swapfile # disable COW on btrfs
fallocate -l 8G swapfile
chmod 0600 swapfile
mkswap swapfile
swapon swapfile

使用英文用户文件夹

用户个人文件夹(文档、下载等),默认是跟随系统语言。
可是中文在终端下输入不太方便,建议使用英文用户文件夹名称。
方法步骤:

  1. 语言修改成英文,注销重新登录
  2. 打开文件管理器,确认修改为英文
  3. 语言修改回中文
  4. 打开文件管理器,保留原名称

无线网卡调优

WIFI 启用 WOL(网络唤醒)

1
2
3
4
# 查看 WOL 支持
iw phy0 wowlan show
# 开启 WOL 支持
sudo iw phy0 wowlan enable magic-packet

关闭节能模式,解决无线网口入站延迟较大

1
2
3
4
5
6
7
8
cat <<EOF > /etc/NetworkManager/conf.d/wifi-powersave-off.conf
[connection]
wifi.powersave = 2

EOF

# 或者启动时设置
sudo iw dev wlo1 set power_save off

睡眠调整

睡眠到内存(suspend)耗电量很低,够用了,没必要开启混合休眠

1
2
3
4
5
6
7
8
9
10
11
12
# 只睡眠到内存
# 修改 /etc/systemd/sleep.conf
AllowSuspend=yes
AllowHibernation=no
AllowSuspendThenHibernate=no
AllowHybridSleep=no
SuspendState=mem standby

# 禁用休眠(可选)
sudo systemctl mask hibernate.target hybrid-sleep.target

# nvida

intel 核显性能优化

10代以上 intel 核显

1
2
3
4
# cat /etc/modprobe.d/i915.conf
options i915 enable_fbc=1 enable_guc=2 enable_dc=0

sudo update-initramfs -u

RDP 远程桌面

服务端: 使用 KDE 默认的远程桌面 (可惜稳定性一般)
客户端: apt install krdc

其他配置

1
2
3
4
5
6
7
# 双屏只显示其中一个屏幕
# 修改 ~/.config/systemd/user/plasma-krdp_server.service
ExecStart=/usr/bin/krdpserver --monitor 0

# 重启服务
systemctl --user daemon-reload
systemctl --user restart app-org.kde.krdpserver.service

使用新内核(可选)

发行版默认内核版本可能比较老,如有必要可以更新内核
注意:mainline 的内核与 ubuntu-drivers 的 nvidia 显卡驱动不兼容(所以一般不推荐)

1
2
3
sudo add-apt-repository ppa:cappelikan/ppa
sudo apt update
sudo apt install mainline

软件使用

常见应用替代方案

  • qq: 使用官方linux版本的flatpak打包
    • flatpak install com.qq.QQ
  • 任务管理器
    • flatpak install io.missioncenter.MissionCenter
  • 词典:使用欧陆词典
    • flatpak install net.eudic.dict
  • 微信
    • 使用星火应用商店打包的官方linux版本
  • 下载管理器:
    • motrix:普通http下载
    • qbittorrent: bt下载
  • 虚拟机
    • KVM:性能更好
    • VMware:易用性更好
  • 游戏
    • steam:大部分游戏都可以直接运行,性能损耗也不大
  • 视频播放
    • haruna:使用 flatpak 版本

KVM 虚拟机

安装

1
2
3
sudo apt install qemu-kvm libvirt-daemon-system virt-manager
sudo usermod -aG libvirt,kvm $USER
sudo systemctl enable --now libvirtd

硬盘直通(scsi), 相比下面的方法性能更好

1
2
3
4
5
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native'/>
<source dev='/dev/disk/by-id/ata-TOSHIBA' index='2'/>
<target dev='sdg' bus='scsi'/>
</disk>

硬盘直通(virtio),samba分享时可能卡顿

1
2
3
4
5
<disk type="block" device="disk">
<driver name="qemu" type="raw" cache="none"/>
<source dev="/dev/disk/by-id/ata-ST3000DM008"/>
<target dev="sdc" bus="virtio"/>
</disk>

无线网口不支持桥接,使用路由绕过

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
# 修改 /etc/sysctl.conf; sudo sysctl -p 生效
net.ipv4.ip_forward = 1
net.ipv4.conf.wlo1.proxy_arp=1
net.ipv4.conf.br-routed.proxy_arp=1


# 创建网桥和路由
#/etc/systemd/system/bridge-routed.service
[Unit]
Description=Create routed bridge br-routed
After=network.target
Wants=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/usr/bin/ip link add br-routed type bridge
ExecStart=/usr/bin/ip link set br-routed up
ExecStartPost=/usr/bin/ip route add 192.168.1.41/32 dev br-routed
ExecStop=/usr/bin/ip link delete br-routed

[Install]
WantedBy=multi-user.target


# 修改 kvm 配置
<interface type="bridge">
<mac address="00:00:00:00:00:00"/>
<source bridge="br-routed"/>
<target dev="vnet0"/>
<model type="virtio"/>
<alias name="net0"/>
<address type="pci" domain="0x0000" bus="0x0a" slot="0x00" function="0x0"/>
</interface>

# 虚拟机使用静态ip
IPv4 地址 . . . . . . . . . . . . : 192.168.1.41(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.1.1
DNS 服务器 . . . . . . . . . . . : 192.168.1.1

dolphin 右键菜单自定义快捷方式

支持文件和文件夹

chmod + ~/.local/share/kio/servicemenus/mklink.desktop

1
2
3
4
5
6
7
8
9
10
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=inode/directory;application/octet-stream;
Actions=RunMyScript

[Desktop Action RunMyScript]
Name=制作符号链接
Icon=emblem-symbolic-link
Exec=/home/rlj/scripts/mklink.sh "%F"

mklink.sh

1
2
3
4
#!/usr/bin/env bash

NAME=$(basename "$1")
ln -s "$NAME" "$NAME.lnk"

haruna 视频播放器

haruna 是 mpv 播放器的前端,功能比较齐全

存在的问题:

  • 视频播放器首次最大化窗口,总是跳到另一个屏幕
    • 关闭另一个屏幕,关闭应用,再打开,再最大化,再重新打开屏幕
  • 无法重命名文件
    • 设置 flatpak 目录权限

flatpak 使用

1
2
3
4
5
# 换源
flatpak remote-modify flathub --url=https://mirrors.ustc.edu.cn/flathub

# 目录授权,解决应用无法编辑文件
flatpak override --user org.kde.haruna --filesystem=/share

挂载 SMB 共享支持 windows 符号链接

支持原生符号链接(目录连接),双向操作都能同步

步骤

  • windows 允许符号链接
    计算机配置 → 管理模板 → 系统 → 文件系统 → 启用 Win32 长路径
    计算机配置 → windwows设置 → 安全设置 → 本地策略 → 用户权限分配 → 创建符号链接

  • samba 挂载选项
    symlink=native

  • 链接时使用相对路径(两个系统绝对路径不一样)

遇到的问题

故障排查方法

  1. 查看系统日志 journalctl -k -b0
  2. 查看用户日志,比如应用崩溃 journalctl –user -b0
  3. 用 AI 搜索相关问题,如果无法解决再尝试 google 搜索

edge 无法启动

edge 异常退出后,有概率无法启动

报错

1
2
Microsoft Edge 进程似乎正在使用此用户配置。
Microsoft Edge 已锁定此用户配置以防止损坏。如果你确定没有其他进程正在使用此用户配置,可以将其解锁并重新启动 Microsoft Edge。

处理方法,其他 chromium 内核浏览器可能也类似

1
rm ~/.config/microsoft-edge/SingletonLock

steam 玩游戏卡住

dmesg 信息

1
2
3
4
727 10:48:59 neon kernel: x86/split lock detection: #AC: CPU 0/KVM/88279 took a split_lock trap at address: 0xfffff80104613be8
727 10:51:00 neon kernel: x86/split lock detection: #AC: CPU 2/KVM/88281 took a split_lock trap at address: 0xfffff8010465701c
727 10:52:51 neon kernel: x86/split lock detection: #AC: CHTTPClientThre/90929 took a split_lock trap at address: 0xf098fc4f
727 10:53:07 neon kernel: x86/split lock detection: #AC: CPU 1/KVM/88280 took a split_lock trap at address: 0xfffff8010465701c

解决方法

1
2
3
4
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT='quiet splash split_lock_detect=off'

sudo update-grub

迁移系统&修复引导

  1. 进入 livecd,使用 Gparted 复制系统到新硬盘
  2. 修改新硬盘分区的 /etc/fstab 的挂载点 uuid
  3. 重建 grub 引导
1
2
3
4
5
6
7
mount /dev/sdaX /mnt/
mount /dev/sda0 /mnt/boot/efi
for i in {proc,dev/sys}; do
mount --bind /${i} /mnt/${i}
done

chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot/efi

睡眠唤醒后很卡

桌面很卡,cpu 频率上不来,只有 800mhz。
映泰主板兼容性问题,换主板解决

终端删除文件到回收站

1
2
sudo apt install trash-cli
alias rm=trash

electorn 应用无法启动

报错 chrome sandbox 相关

1
2
3
4
5
# 编辑 /etc/sysctl.conf 添加
kernel.apparmor_restrict_unprivileged_userns=0

# 生效
sudo sysctl -p

登录界面隐藏无关用户

比如某些用于文件共享的用户

1
2
3
4
5
6
7
# 修改 /etc/sddm.conf
[Autologin]
Session=plasma

[Users]
HideUsers=data,data-r,data-s
HideShells=/usr/sbin/nologin,/bin/false

偶发硬件设备工作不正常

强制重启后或者系统崩溃后,如果设备不正常,可以尝试重启到 windows 再切回 linux

应用在启动器里找不到

1
2
rm -f ~/.config/menus/
kbuildsycoca6 --noincremental

修复睡眠唤醒问题

睡眠之后可能出现莫名奇妙的唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看当前唤醒触发器
cat /proc/acpi/wakeup | grep enable

# 查看触发器的设备路径
for p in /sys/class/wakeup/*/device/power/wakeup; do
dev=$(dirname $(dirname $p));
dev_path=$(realpath $dev)
echo ${dev_path}
done


# 直接修改是 /proc/acpi/wakeup 是不生效的
# 设置具体设备的方式禁用 rtc 和 AWAC, 位置格式 ${dev_path}/power/wakeup
# 可以将禁用命令配置到 rc.local
echo disabled | tee /sys/devices/platform/rtc_cmos/power/wakeup # rtc
echo disabled | tee /sys/devices/platform/rtc_cmos/rtc/rtc0/alarmtimer.0.auto/power/wakeup # rtc
echo disabled | tee /sys/devices/platform/ACPI000E:00/power/wakeup # AWAC

# 检查是否生效
cat /proc/acpi/wakeup | grep enable

apt 设置代理

部分 apt ppa 源需要代理,但是国内的镜像源却不需要。
所以需要给每一个源单独设置代理的功能

有两种设置方式

  • 白名单模式(只给需要的配置)
  • 黑名单模式(先设置默认代理, 不需要的设置直连)

设置语法

  • 设置代理服务器:Acquire::http::Proxy <proxy-server>
  • 设置某个域名的代理:Acquire::http::proxy::<domain> <proxy-server|DIRECT>

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#  /etc/apt/apt.conf.d/99proxy

# 给每种协议的源设置代理,一般设置 https::Proxy 就行了
#Acquire::http::Proxy "http://yourproxyaddress:proxyport";
#Acquire::https::Proxy "http://yourproxyaddress:proxyport";
#Acquire::ftp::Proxy "http://yourproxyaddress:proxyport";
#Acquire::socks::Proxy "http://yourproxyaddress:proxyport";

# 设置具体域名是否走代理
#Acquire::http::proxy::local.mirror.address "DIRECT";
#Acquire::http::proxy::HOST_NAME_TO_BE_PROXIED "http://yourproxyaddress:proxyport"

# 黑名单模式示例
Acquire::https::Proxy "http://localhost:10808";
Acquire::https::Proxy::"mirrors.aliyun.com" "DIRECT";

限制硬盘读写速度

某些nvme硬盘的发热很严重, 如果全速运行会导致系统不稳定.
所以需要对硬盘速度进行限制

使用 systemd 设置限速

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建配置目录
sudo mkdir -p mkdir -p /etc/systemd/system/{user,system}.slice.d/

# 设置用户限制
sudo tee /etc/systemd/system/user.slice.d/io-limit.conf > /dev/null <<EOF
[Slice]
IOReadBandwidthMax=/dev/nvme0n1 800M
IOWriteBandwidthMax=/dev/nvme0n1 300M
IOReadIOPSMax=/dev/nvme0n1 60000
IOWriteIOPSMax=/dev/nvme0n1 30000

EOF

# 系统也使用同样的限制
sudo ln -sf /etc/systemd/system/user.slice.d/io-limit.conf \
/etc/systemd/system/system.slice.d/

速度测试

1
2
3
4
5
6
~$ dd if=/dev/zero of=testfile bs=1M count=2048 oflag=direct status=progress \
&& rm testfile
2099249152 bytes (2.1 GB, 2.0 GiB) copied, 7 s, 300 MB/s
2048+0 records in
2048+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 7.16429 s, 300 MB/s

archlinux 命令行不能使用回收站

安装 cachyos 时,trash-cli 不能使用,其他应用也不能删除文件到回收站

1
sudo pacman -S gvfs 

某些软件中输入法不可用

在使用 fcitx 输入法时,某些软件中无法唤起输入法,无法输入中文(如:微信)
通常是因为该程序默认加载了 ibus 模块,只要设置 QT_IM_MODULE=fcitx 环境变量就能解决问题。
为了不带来别的影响,仅修改 .desktop 启动文件就行。

1
2
3
4
mkdir -p ~/.local/share/applications

sed 's|Exec=/usr|Exec=env QT_IM_MODULE=fcitx /usr|' \
/usr/share/applications/wechat.desktop > ~/.local/share/applications/wechat.desktop

WPS 每次登录自动启动

WPS 安装后会在 /etc/xdg/autostart/ 放置一个 wps-office-autostart.desktop,桌面环境每次登录都会扫描该目录并执行。它会启动 wpsd 守护进程,预加载 wps、wpp、et 三个组件(即”快速启动”功能)。

这个机制跟 systemd 无关,所以禁用 systemd 服务不会生效。

排查过程

1
2
3
4
5
6
7
# 查看 XDG 自启动项
ls /etc/xdg/autostart/ | grep wps
# wps-office-autostart.desktop

# 查看启动内容
cat /etc/xdg/autostart/wps-office-autostart.desktop
# Exec=/opt/apps/cn.wps.wps-office-pro/files/bin/quickstartoffice start

解决方法:在用户级别覆盖禁用(不影响其他用户,WPS 更新也不会覆盖)

1
2
3
mkdir -p ~/.config/autostart
cp /etc/xdg/autostart/wps-office-autostart.desktop ~/.config/autostart/
echo "Hidden=true" >> ~/.config/autostart/wps-office-autostart.desktop

在 Linux 虚拟机中使用 PyAutoGUI 做自动化

作者 Jay.Run
2025年5月28日 05:48

PyAutoGUI 是 GUI 功能强大自动化方案,但 UI 程序的运行环境选择与配置也是一大难题。

系统选择

为了让环境可迁移,免维护,资源消耗少,必须使用虚拟化的方案。
虽然 PyAutoGUI 支持 Windows/macOS/Linux 三个平台。但是各有各的弊端。

  • Windows 的系统臃肿且开发环境不友好
  • macOS 的权限管理过于严格且虚拟化难度大,高dpi的屏幕也不适合自动化
  • Linux 就是最好的选择了,但执行的应用可能不支持 Linux,必须引入 Wine

综合以上情况考虑,系统环境选择如下

  • pve: 宿主机系统,也可以选择别的环境
  • debian: 客户机系统,目前是 debian 12, 方便使用 deepin 的 wine 程序
  • mate: 桌面环境,比较轻量,实测兼容性比 xfce 好
  • 星火应用商店: 方便安装各种国内软件和 Wine 程序
  • 统信 Windows 应用兼容引擎: 在星火应用商店中安装,可以用于安装和打包商店中没有的程序

虚拟机配置

步骤

  1. 创建好 pve 虚拟机, 安装 debian,选择 mate 桌面
  2. 设置分辨率:控制中心 -> 显示器 -> 设置为 1920x1080
  3. 关闭自动睡眠:控制中心 -> 电源管理 -> 动作和显示修改为从不
  4. 关闭屏幕保护:控制中心 -> 屏幕保护程序 -> 取消勾选所有选项
  5. 安装gnome-screenshot: 用于 PyAutoGUI 内部调用截图
  6. 安装星火应用商店(可选)
  7. 安装统信 Windows 应用兼容引擎(可选):在星火应用商店中安装
  8. 火焰截图(可选):在星火应用商店中安装,更方便对应用的按钮和文字截图。

其他说明:

  • 如果不关闭自动睡眠和屏幕保护,PyAutoGUI 就无法截取到应用的图像,也就无法操作了。
  • 图形显示协议默认选 x11,wayland 对显卡有要求,虚拟机不方便处理。

睡眠后时间不对

虚拟机睡眠唤醒后时间没有更新,不知道为什么没有触发时间更新。
目前只能通过强行同步来解决

在脚本里执行同步命令

1
2
# 和阿里云ntp服务器同步
sudo ntpdig -S ntp.aliyun.com

或者,理论上可以通过虚拟机中的 systemd-suspend hook,或者 pve hookscript 来解决(没试成功)

PyAutoGUI 使用技巧

远程登录调试

pve 默认图片控制台不太方便,不能复制粘贴,所以使用远程登录调试
由于系统使用 x11 环境,这里使用 xrdp 作为远程服务端。
如果以后更新到 wayland,可以使用 gnome-remote-desktop

1
2
sudo apt update
sudo apt install xorgxrdp xrdp

远程登录时找不到显示器

xrdp 或者 ssh 远程登录执行脚本时,窗口相关命令可能会提示找不到显示器。
可以在自动化脚本中设置以下环境变量

1
2
os.environ['DISPLAY'] = ':0'
os.environ['XAUTHORITY'] = '/home/<your-name>/.Xauthority'

过滤没必要的截图日志

gnome-screenshot 截图时会产生一些无用日志

1
2
** Message: 17:36:31.888: Unable to use GNOME Shell's 
builtin screenshot interface, resorting to fallback X11.

新建一个 gnome-screenshot 文件,赋予执行权限,放到 PATH 环境变量中比 /usr/bin 靠前的路径

1
2
#!/bin/bash
exec /usr/bin/gnome-screenshot "$@" >> /tmp/gnome-screenshot.log 2>&1

验证码识别

pytesseract

常见的方案是 pytesseract, 但是效果不好,识别率比较一般。

安装

1
2
sudo apt install tesseract-ocr
pip install pytesseract

使用

1
pytesseract.image_to_string(image, config='--psm 8 -c tessedit_char_whitelist=0123456789')

ddddocr

ddddocr 是基于机器学习的验证码识别库,识别效果比较好。

这里使用 docker 安装 fastapi接口

1
docker run -d -p 8000:8000 oozzbb/ddddocr-fastapi:latest

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def ocr_image(file='region.png'):
url = 'http://your-address:8000/ocr'
with open(file, 'rb') as fp:
files = {
'file': fp,
}
data = {
'probability': 'false',
'png_fix': 'false',
'charsets': '0123456789' # 验证码可能的字符列表
}
response = requests.post(url, files=files, data=data)
assert response.ok
return response.json()['data']

高效执行自动化脚本

可以在宿主机远程调用自动化脚本,并且脚本执行完成后挂起虚拟机

1
2
3
4
5
6
7
8
9
10
HOST=your-name@vm-ip-address
VMID=109 # pve 虚拟机id

qm resume $VMID # 唤醒虚拟机
sleep 2

ssh $HOST "SOME_VAR=foobar python your-script.py" # 执行脚本
sleep 1

qm suspend $VMID # 关闭虚拟机

其他有用的函数

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
# 切换窗口到前台
def wm_to_front(title: str):
if sys.platform == 'linux':
return subprocess.check_call(['wmctrl', '-a', title])
raise NotImplementedError

# 点击或者移到到图片位置
def click_image(image, confidence=0.9, grayscale=True, delay=1.5,
duration=0.6, offset=(1, 0), abort=True, click=True):
try:
loc = pyautogui.locateOnScreen(image, confidence=confidence, grayscale=grayscale)
except pyautogui.ImageNotFoundException:
loc = None
if loc:
x, y = pyautogui.center(loc)
if click:
pyautogui.click(x+offset[0], y+offset[1], duration=duration)
else:
pyautogui.moveTo(x+offset[0], y+offset[1], duration=duration)
time.sleep(delay)
return loc
else:
if abort:
raise RuntimeError(f"can not find {image}!")
else:
return None

# 等待某个图片出现
def wait_button(image, timeout, interval=10):
for _ in range(timeout//interval):
time.sleep(interval)
loc = click_image(image, abort=False)
if loc:
break
else:
raise RuntimeError(f'wait {image} failed')
time.sleep(interval/2)

使用感受

PyAutoGUI 是基于图像而不是图型控件来识别目标,没有确认反馈。
相比于网页自动化的 Selenium 就感觉落后些了。
当然,Windows 平台有 pywinauto 支持控件识别,但我不咋熟悉。
总之,PyAutoGUI 就像手枪,虽然比较简陋,但还是很直观的,执行简单的任务也够用了。

注意事项

  • 自动化代码中不要编码用户名密码相关信息,建议用环境变量传入
  • 不要轻易调整系统的 dpi(建议设为1.0) 和分辨率,可能导致图片无法识别
  • xfce 对 wine 应用的兼容性似乎不好,wine 文件保存对话框被可能被反复触发,原因不明。

语言的力量

作者 Jay.Run
2025年5月28日 04:41

语言的力量远比想象的强大,某种程度上是有虚空造物的能力。

你可以凭空创造一个概念,并植入你的听众脑子,不论好还是坏。诈骗分子、PUA 大师、营销大师无不精通此等技艺。

起因是一个综艺节目,有个导演对哈妮克孜说了句话,让我惊为天人,让我惊叹“你他娘真是个人才”。
它在线下拍戏的时候见到了哈妮克孜,哈当时戴了副大镜框眼镜,于是它说道“我当时看到你的时候,感觉好失望,你和我银幕上看到的哈妮克孜完全不一样”。
它凭空创造了一个观念(事实),哈妮克孜颜值不行。怎么创造的呢?和哈妮克孜荧幕形象相比。这显然是不客观的,但很难瞬间就察觉到。

这样,好与坏、美与丑一切观念都是可以被重新定义的,只要你选择合适的目标和对照标准,甚至是你脑海里的标准。
你是丑的和你漂亮的时候比,你是蠢的和聪明的时候比,我对你是失望的和对你的期望相比。
PUA 大师就是用这种手法实现打压,新闻学就是这么颠倒黑白,当然也可以反向操作,那么你就成为知心姐姐鼓励大师。

所以,神话中的言出法随也不难理解,毕竟人是观念的动物,你改变/植入了某人的某个观念,那么你就改变了他眼中的世界。
这也就是《乔布斯传》中乔布斯的扭曲现实力场。

联想笔记本 BIOS 跳过检测强制降级

作者 Jay.Run
2025年5月16日 02:00

联想某些版本的 bios 似乎会禁止降级,即使打开 bios 设置里的允许降级选项,依然会提示 “this platform does not support IHISI interface” 的错误,导致降级失败。

降级方法

经过一些尝试,找到了方法绕过,以 ThinkBook 14 G3 ACL 机型为例

  1. 打开驱动下载页面
  2. 打开 BIOS 页面, 下载以下安装文件
    • 当前版本 BIOS 的安装包(NEW),版本 GQCN41WW_HFCN36WW
    • 以及想要降级的 BIOS (OLD),版本 GQCN36WW_HFCN31WW
  3. 运行 NEW,选择仅解压。找到解压目录,应该只有一个 exe 程序,使用 7-zip 或者 bandizip 打开并提取内部文件。
  4. OLD 也执行如上操作,如果没有解压选项,可以尝试直接提取。
  5. OLD 提取目录中的 .bin 格式固件(GLV3A036.bin、GLV4D031.bin)复制到 NEW 提取目录中。
  6. 修改 NEW 提取目录中的 platform.ini 文件,找到如下内容。将其中的 .bin 文件名,修改为 OLD 中的 .bin 文件名,保存。有两个 .bin 文件,文件名是和版本中的数字对应的,不要修改错误。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ; 修改前
    [MULTI_FD]
    Flag=1
    FD#01=ID,GLV4D,GLV4D036.bin
    FD#02=PCI,0,1,1,0,FFFFFFFF,FFFFFFFF,GLV3A041.bin

    ; 修改后
    [MULTI_FD]
    Flag=1
    FD#01=ID,GLV4D,GLV4D031.bin
    FD#02=PCI,0,1,1,0,FFFFFFFF,FFFFFFFF,GLV3A036.bin
  7. 运行 NEW 提取目录中的 H2OFFT-Wx64.exe,等待降级完成。

注意:有风险,请谨慎操作。不要跨太多版本,可能有不可预料的问题

参考

redroid “设备未获得play保护机制认证” 问题

作者 Jay.Run
2025年5月8日 02:00

使用 redroid 等安卓虚拟环境,可能会发现 google play 用不了的问题。
虽然系统集成了 gapps,但系统提示 “设备未获得play保护机制认证”,无法登录 play 商店。
可能的原因比较多,这里大概是因为虚拟机的型号没在google的数据库里。
解决方案就是,获取 GSF ID 注册到 Google。

步骤

  1. 安装 device id
  2. 打开应用,复制 GSF id,假设为ffffffff
  3. 终端运行printf "%d\n" 0xffffffff,转换换成10进制数字。
  4. 打开 https://www.google.com/android/uncertified/ 输入转换后的结果并提交

在 iOS 上访问安卓应用

作者 Jay.Run
2025年4月27日 02:00

有些应用在安卓上是独占的,iOS 上又没有比较好的替代品,而且 iOS 上没有能用安卓模拟器。
如果使用多个设备,维护的心智成本又高,被这个问题困扰了许久。

最近碰巧了解了 scrcpy, 用于远程控制安卓,终于解决了这个问题。

需要的工具

  1. 安卓环境:虚拟环境或者物理设备均可,这里使用虚拟环境。以下是可选的虚拟环境
  • PVE 的 PCT容器 (Proxmox Container Toolkit),需要打补丁
  • docker,基于Redroid 需要调整内核参数
  • WSA (Windows Subsystem for Android),与 scrcpy 配合有点问题,窗口有黑边。
  • VMware、VirtualBox等虚拟化平台,需要解决虚拟显卡。
  1. 使用 scrcpy-mobile 远程控制安卓环境, 当然 scrcpy 也支持 Windows、Linux、macOS

PCT 安卓容器

由于已有PVE环境,这里选用PCT,主要步骤

  1. 根据 PCT-patches 文档,给 PCT 打好补丁
  2. 根据 lineageOS 模板,新建安卓容器,注意去除Unprivileged container的勾选。
  3. 修改 lxc.init.cmd 选项,在后面增加以下参数,调整分辨率和iPad一致。
    1
    androidboot.redroid_width=1668 androidboot.redroid_height=2388 androidboot.redroid_fps=60

ps: 安卓容器对宿主机性能似乎有一定要求,J4125 只是勉强够用,流畅度一般。

macOS 中使用 scrcpy

安装 scrcpy ,连接命令

1
2
3
4
5
6
scrcpy --audio-codec=aac \
--video-codec=h264 \
--video-bit-rate=16M \
--max-fps=60 \
--tcpip=192.168.10.181:5555 \
--start-app=io.legado.app.release

参数解释

  • --audio-codec=aac同步声音,使用 AAC 编码,默认参数可能导致没有声音
  • --video-codec=h264使用 H.264 视频压缩编码
  • --video-bit-rate=16M16Mbps 码率,高画质
  • --max-fps=60最大帧率 60FPS,画面流畅
  • --tcpip=192.168.10.181:5555Wi-Fi adb 连接设备
  • --start-app=io.legado.app.release (可选)连接后直接启动 Legado App,应用列表可使用adb shell pm list packages -3查看

iOS 中使用 scrcpy-mobile

appstore中安装 scrcpy-mobile

由于该应用的用户界面易用性比较差,表单也不支持有些 scrcpy 参数,这里直接使用快捷指令打开应用。
设置打开 scrcpy-mobile 后,开启引导式访问,防止误触。同时可以在 iOS 设置中开启引导式访问的面容id,防止频繁输入密码。

scrcpy-mobile 的 url schema

1
scrcpy2://192.168.10.181:5555?enable-audio=true&audio-codec=aac&video-bit-rate=16M&video-codec=h264&max-fps=60

最后,这也未尝不算一种 NTR
IMG_202504275684_512x682

安卓环境设置(可选)

  1. 关闭导航栏:系统 > 手势 > 系统导航 > 手势导航
  2. 加快或者关闭系统动画:开发者选项 > 窗口动画缩放、过渡动画缩放、Animator 时长比例 > 关闭或者0.5x

在 VSCode 中用 Rust 刷LeetCode

作者 Jay.Run
2025年3月9日 04:17

本文介绍在 VSCode 中配置和使用插件来高效地解决 LeetCode 问题,并使用 Rust 语言编写和测试代码。

vscode 插件

  • LeetCode.vscode-leetcode
  • pucelle.run-on-save
  • rust-lang.rust-analyzer

项目结构

cargo new vscode-leetcode-rust

1
2
3
4
5
6
7
8
9
10
// tree -I target --dirsfirst
.
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
├── main.rs
└── solutions
├── 1_two_sum.rs
...

vscode 全局设置

1
2
3
4
5
6
7
8
"leetcode.useEndpointTranslation": false,  // for english filename
"leetcode.workspaceFolder": "/Users/<your_name>/projects/vscode-leetcode-cn-rust",
"leetcode.filePath": {
"default": {
"folder": "src/solutions",
"filename": "${id}_${snake_case_name}.${ext}"
}
},

用 automod 宏添加新回答到模块

cargo add automod

1
2
3
4
5
6
7
// src/lib.rs 

const CURRENT: &str = "sdfsdfsd.rs"; // trigger rust-analyser recheck

pub mod solutions {
automod::dir!("src/solutions");
}

触发 rust-analyzer

用 run-on-save 插件,保存回答时更新 lib.rs,触发 rust-analyzer 重新分析项目,开启新回答的代码补全。

vscode 项目配置

1
2
3
4
5
6
7
8
"runOnSave.commands": [
{
// use gnu sed update lib.rs when add solutions (macOS)
"command": "sh onsave.sh ${fileBasename}",
"runIn": "backend",
"finishStatusMessage": "touched ${workspaceFolderBasename}"
},
]

onsave.sh脚本,macOS使用 gnused,linux 使用默认 sed 就好。
通过切换模块是否为pub来触发 rust-analyzer 识别新回答

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

FILE=$1
if [[ `grep "$FILE" src/lib.rs | wc -l` -eq 0 ]]; then
gsed -i -E "/CURRENT/c\const CURRENT: &str = \"$FILE\";" src/lib.rs
if [[ `grep '::dir!(pub' src/lib.rs | wc -l` -eq 1 ]]; then
gsed -i "s|::dir!(pub |::dir!(|" src/lib.rs
else
gsed -i "s|::dir!(|::dir!(pub |" src/lib.rs
fi
fi

编写本地测试用例

把测试代码写在 "// @lc code=end" 后面,需要定义 Solution 结构体,可能还需要定义参数的结构体。

1
2
3
4
5
6
7
8
// @lc code=end
struct Solution;

#[test]
fn test_a() {
// let res = Solution::is_valid(String::from("()[]{}"));
// println!("RESUTL\t{:?}", res);
}

-w340

HomeBrew 与无 root 权限 Linux 环境包管理

作者 Jay.Run
2023年5月29日 04:00

一些公用的 Linux 服务器,处于维护以及安全考虑,一般只会提供普通权限用户给使用者。
普通用户的权限满足日常使用是够了,但是难以配置自己的开发环境,安装一些自己需要的包。

如果都从源码编译安装软件,依赖的维护过于复杂,初始编译工具链的版本可能也不满足需求,如 gcc 版本过低。
如果申请 sudo 权限或者请求更新系统或安装 docker,后期责任难以界定,运维和管理员一般也不会同意。

所以,最优方案还是有需求的用户在个人目录维护自己的工具链和环境。下文方案为围绕 HomeBrew 构建。

安装 miniconda 解决前置依赖

如果你的系统比较新,可以直接尝试安装 HomeBrew

基于上面讨论的内容,公用服务器一般存在系统版本低的问题,是 centos7 或者 centos6 也毫不稀奇,而且如 glibc 等库的版本也非常低。

安装 HomeBrew 有两个强依赖,git 及 curl,而且依赖的版本都比较高,centos7 的版本也不能满足。
另外,由于 Brew 不少软件都需要从源码编译,gcc 和良好的网络环境也不可缺少。

幸好 miniconda 能够解决以上几点问题。miniconda 只是提供 HomeBrew 安装的依赖,后续可以删除。

配置 conda 源(可选): 新建 .condarc,包含以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
channels:
- defaults
show_channel_urls: true
default_channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2
custom_channels:
conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
pytorch-lts: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud

下载及安装 miniconda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 下载
# 如果服务器的内置证书已过期, 增加 --no-check-certificate 条件跳过证书验证
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && chmod +x Miniconda3-latest-Linux-x86_64.sh

# 安装
./Miniconda3-latest-Linux-x86_64.sh -b -p ~/miniconda3
source ~/miniconda3/etc/profile.d/conda.sh

# 安装所需包
conda install -y gcc_linux-64 gxx_linux-64 curl git

# 链接 gcc,等 HomeBrew 安装完成这些链接可以删掉
cd ~/miniconda3/bin/
ln -s x86_64-conda_cos6-linux-gnu-gcc gcc
ln -s x86_64-conda_cos6-linux-gnu-cpp c++
ln -s gcc cc

配置安装环境变量

这些环境变量也可以配置到 bashrc 等文件,使之永久生效

1
2
3
4
5
6
7
8
9
10
11
# 设置 curl 和 git,可选
export HOMEBREW_CURL_PATH=~/miniconda3/bin/curl
export HOMEBREW_GIT_PATH=~/miniconda3/bin/git

# 设置安装源为清华源,如果网络畅通可忽略,清华源也可能403
export HOMEBREW_INSTALL_FROM_API=1
export HOMEBREW_API_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api"
export HOMEBREW_BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles"
export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"

安装 HomeBrew

由于没有 root 权限,HomeBrew 需要手动安装。
由于是手动安装,位置与默认安装位置不同,很多预编译的包就不能用了,都得从源码编译,所以网络和机器性能以及耐心很重要。

1
2
3
4
5
6
7
8
9
10
# 下载,也可使用清华源 https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git
git clone https://github.com/Homebrew/brew ~/.homebrew

# 安装
eval "$(~/.homebrew/bin/brew shellenv)"
brew update --force --quiet
chmod -R go-w "$(brew --prefix)/share/zsh"

# 自动加载 brew
echo 'eval "$(~/.homebrew/bin/brew shellenv)"' >> ~/.bashrc

参考

给 macOS 词典增加生词本功能

作者 Jay.Run
2022年11月13日 04:00

macOS 系统的自带词典应用非常强大,与其他应用整合很好,快捷取词很方便(command+control+d)。
但是美中不足的是缺少生词本功能,查了单词又很容易忘记,对语言学习者来说就有些不便了。

经过本强迫症的探索,终于找到基于 Karabiner-Elements + Automator + Logseq 的完美生词本方案。
最后的效果是,快捷键取词的同时记录单词卡片到Logseq对应的笔记。

词典词库扩充

参考知乎文章安装好《朗道英汉字典5.0》
这是为了有个释义简洁的词典,方便后续生成生词本词条

编写workflow

使用 macOS 自带应用 Automator(自动操作)编写workflow,将当前鼠标所在位置的文本提取并保存制卡。

首先打开 Automator.app 新建一个 Quick Aciont(快速操作)

然后依次拖入“获得词语定义”,“运行Shell脚本”等步骤,并调整如下几个位置的选项。

修改脚本里的代码为如下内容,生词本路径相应替换,并相应位置新建好生词本文件。

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
# -*- coding:utf-8 -*-

from __future__ import unicode_literals, print_function
import sys, os, io, subprocess

FILE=os.path.expanduser("~/weiyun_sync/!sync/logseq-note/pages/生词本.md")
output = []
text = sys.argv[1].decode('utf8') if sys.version_info.major == 2 else sys.argv[1]

lines = [i.strip() for i in text.splitlines() if i.strip()]
if len(lines) < 2:
exit(0)

word = lines[0]
if lines[1][0] == '*':
output.append('- {}\t{} [[card]]'.format(word, lines[1]))
lines = lines[2:]
else:
output.append('- {}\t [[card]]'.format(word))
lines = lines[1:]
output.append('\t- {}'.format(lines[0]))
for line in lines[1:]:
output.append('\t ' + line)

old_words = set()
with io.open(FILE, 'r', encoding='utf8') as fp:
for line in fp:
parts = line.split()
if line.startswith('-') and len(parts) > 1:
old_words.add(parts[1])

if word not in old_words:
with io.open(FILE, 'a', encoding='utf8') as fp:
fp.write('\n')
fp.write('\n'.join(output))
fp.write('\n')
subprocess.check_call(['osascript', '-e', u'display notification "添加 {}" with title "生词本"'.format(word)])
else:
subprocess.check_call(['osascript', '-e', u'display notification "跳过 {}" with title "生词本"'.format(word)])

选择路径保存好 workflow,然后在 键盘 - 快捷键 - 服务 中能看到新建的workflow。
为它设置快捷键 command + shift + alt + 1

Karabiner-Elements

Karabiner-Elements 是 macOS 平台的一个重新映射快捷键的软件。
这里我们使用它将“查询单词”和“触发workflow”整合在一起,当然它还支持很多用途,这里就不赘述了。

注意确保Karabiner相关权限,并且设置中下图相关设备是勾选状态

安装好Karabiner-Elements后,打开它的配置文件
路径在 /Users/<用户名>/.config/karabiner/karabiner.json

profiles -> complex_modifications -> rules 列表中增加一项配置,内容如下。
然后保存,Karabiner会自动加载新的配置。

这里是将鼠标的侧键(靠前的)映射为查单词的快捷键,实现一键查词。
也可以根据需要更改按键,通过EventViewer可以查看按键代码,配置文件格式可参考官方文档

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
{
"description": "Mouse",
"manipulators": [
{
"from": {
"pointing_button": "button5"
},
"to": [
{
"pointing_button": "button1"
},
{
"pointing_button": "button1"
},
{
"key_code": "d",
"modifiers": [
"left_command",
"left_control"
]
},
{
"key_code": "1",
"modifiers": [
"left_option",
"left_shift",
"left_command"
]
}
],
"type": "basic"
}
]
}

效果

可以看到 Logseq 中卡片生成的效果

参考

关闭子进程打开的文件描述符

作者 Jay.Run
2022年8月31日 06:48

我们在测试代码时,由于需要经常重启服务,经常会发现服务端口被占用。
一般kill掉后台进程就ok了,但是如果服务有启动一些常驻的后台程序,可能也会导致端口不能释放。

在类UNIX系统中,一切被打开的文件、端口被抽象为文件描述符(file descriptor)
从python3.4开始,文件描述符默认是non-inheritable,也就是子进程不会共享文件描述符。

问题

一般为了实现多进程、多线程的webserver,服务端口fd必须设置为继承(set_inheritable),这样才能多进程监听一个端口(配合SO_REUSEPORT)
典型的是使用flask的测试服务器的场景,这里我们写一段代码模拟。

1
2
3
4
5
6
import socket, os
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 22222))
server.set_inheritable(True)

os.system("python -c 'import time;time.sleep(1000)' ")

我们通过lsof -p {pid}可以看到这两个进程的所有文件描述符
server进程, 可以看到服务端口的fd是4

1
2
3
4
5
6
7
8
9
10
11
COMMAND   PID  FD      TYPE             DEVICE  SIZE/OFF       NODE NAME
ptpython 6214 cwd DIR 253,0 4096 872946898 /
...
ptpython 6214 0u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 1u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 2u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 3r CHR 1,9 0t0 2057 /dev/urandom
ptpython 6214 4u sock 0,7 0t0 58345077 protocol: TCP
ptpython 6214 5u a_inode 0,10 0 8627 [eventpoll]
ptpython 6214 6u unix 0x0000000000000000 0t0 58368029 socket
ptpython 6214 7u unix 0x0000000000000000 0t0 58368030 socket

sleep子进程,也拥有fd=4的文件描述符

1
2
3
4
5
6
7
COMMAND   PID  FD   TYPE DEVICE  SIZE/OFF       NODE NAME
python 18022 cwd DIR 253,0 4096 872946898 /
...
python 18022 0u CHR 136,13 0t0 16 /dev/pts/13
python 18022 1u CHR 136,13 0t0 16 /dev/pts/13
python 18022 2u CHR 136,13 0t0 16 /dev/pts/13
python 18022 4u sock 0,7 0t0 58345077 protocol: TCP

如果server进程退出时,sleep进程没有退出,fd=4对应的端口就被占用了,服务也就不能正常启动了。

解决方法

手动清理

1
2
3
4
5
6
7
8
import os
import time

os.system(f'lsof -p {os.getpid()}')
os.closerange(3, 100) # 这里假定打开文件描述符不会超过100
time.sleep(5)
os.system(f'lsof -p {os.getpid()}')
# 后面执行需要的业务代码

使用close_fds

使用subprocess库而不是os来启动子程序, 通过close_fds参数关闭多余的文件描述符

1
2
import subprocess
subprocess.call("python -c 'import time;time.sleep(1000)'", shell=True, close_fds=True)

参考

容器内进程优雅退出

作者 Jay.Run
2022年7月11日 05:48

在使用 docker 时,常常会碰到进程退出时资源清理的问题,比如保证当前请求处理完成,再退出程序。

当执行 docker stop xxx 时,docker会向主进程(pid=1)发送 SIGTERM 信号
如果在一定时间(默认为10s)内进程没有退出,会进一步发送 SIGKILL 直接杀死程序,该信号既不能被捕捉也不能被忽略。

一般的web框架或者rpc框架都集成了 SIGTERM 信号处理程序, 一般不用担心优雅退出的问题。
但是如果你的容器内有多个程序(称为胖容器,一般不推荐),那么就需要做一些操作保证所有程序优雅退出。

signals

信号是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。

应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。

常见信号:

信号名称信号数描述默认操作
SIGHUP1当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。终止进程
SIGINT2程序终止(interrupt)信号,在用户键入 Ctrl+C 时发出。终止进程
SIGQUIT3和SIGINT类似,但由QUIT字符(通常是Ctrl /)来控制。终止进程并dump core
SIGFPE8在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术错误。终止进程并dump core
SIGKILL9用来立即结束程序的运行。本信号不能被阻塞,处理和忽略。终止进程
SIGALRM14时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号。终止进程
SIGTERM15通常用来要求程序自己正常退出;kill 命令缺省产生这个信号。终止进程

Dockerfile

下面以 supervisor 为例,Dockerfile 如下

1
2
3
4
5
6
7
8
FROM centos:centos7
ENV PYTHONUNBUFFERED=1 TZ=Asia/Shanghai
RUN yum -y install epel-release && \
yum -y install supervisor && \
yum -y clean all && rm -rf /var/cache

COPY ./ /root/
ENTRYPOINT [ "/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ]

trap

正常情况,容器退出时supervisor启动的其他程序并不会收到 SIGTERM 信号,导致子程序直接退出了。

这里使用 trap 对程序的异常处理进行包装

1
trap <siginal handler> <signal 1> <signal 2> ...

新建一个初始化脚本,init.sh

1
2
3
4
5
6
7
#!/bin/sh

/usr/bin/supervisord -n -c /etc/supervisord.conf &

trap "supervisorctl stop all && sleep 3" TERM INT

wait

修改 ENTRYPOINT 为如下

1
ENTRYPOINT ["sh", "/root/init.sh"]

参考

Python 循环变量泄露与延迟绑定

作者 Jay.Run
2022年3月5日 04:17

循环变量泄露与延迟绑定叠加在一起,会产生一些让人迷惑的结果。

梦开始的地方

先看看一开始的问题,可以看到这里lambda函数的返回值一直在变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xx = []
for i in [1,2,3]:
xx.append(lambda: i)

print('a:', xx[0]())

for j in xx:
print(j())

print('b:', xx[0]())

for i in xx:
print(i, i())

print('c:', xx[0], xx[0]())

for i in [4, 5, 6]:
print(i)

print('d:', xx[0], xx[0]())

输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
a: 3
3
3
3
b: 3
<function main2.<locals>.<lambda> at 0x10ca30310> <function main2.<locals>.<lambda> at 0x10ca30310>
<function main2.<locals>.<lambda> at 0x10ca303a0> <function main2.<locals>.<lambda> at 0x10ca303a0>
<function main2.<locals>.<lambda> at 0x10ca30430> <function main2.<locals>.<lambda> at 0x10ca30430>
c: <function main2.<locals>.<lambda> at 0x10ca30310> <function main2.<locals>.<lambda> at 0x10ca30430>
4
5
6
d: <function main2.<locals>.<lambda> at 0x10ca30310> 6

循环变量泄露

由于Python没有块级作用域,所以循环会改变当前作用域变量的值,也就是循环变量泄露。
注意:Python3中列表推导式循环变量不会泄露,Python2中和常规循环一样泄露。

1
2
3
4
5
x = -1
for x in range(7):
if x == 6:
print(x, ': for x inside loop')
print(x, ': x in global')

输出如下

1
2
6 : for x inside loop
6 : x in global

闭包与延迟绑定

再讲一下闭包,在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。
这里所谓的引用可以也就是内部函数记住了变量的名称(而不是值,这个从ast语法树可以看出),而变量对应的值是会变化的。
如果在循环中定义闭包,引用的变量的值在循环结束才统一确定为最后一次循环时的值,也就是延迟绑定(lazy binding)。

所以下面的例子,xx的所有匿名函数的返回值均为3

1
2
3
xx = []
for i in [1,2,3]:
xx.append(lambda: i)

最后

再分析一开始的问题,这里的匿名函数引用了变量i,而i是全局变量,所以再次使用i作为循环变量时,列表中的匿名函数引用的值就被覆盖了。

正确做法:

  • 在独立的函数中定义闭包
  • 闭包引用的变量应该是其他函数不可修改的
  • 优先使用列表推导式

参考

MySQL 自定义数据库路径

作者 Jay.Run
2021年10月15日 04:17

最近的一些文章是整理以前的笔记
MySQL 是最常用的数据,有时希望将数据库文件存放在自定义路径,或者在系统中启动多个 MySQL服务。

当然,如果条件允许,建议直接使用 docker

创建 my.cnf 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[mysqld]
datadir=/home/ruan/data/mysql_data
socket=/home/ruan/data/mysql.sock
user=ruan
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
bind-address=127.0.0.1
port = 12345

character-set-server=utf8
collation-server=utf8_general_ci

[mysqld_safe]
log-error=/home/ruan/data/mysqld.log
pid-file=/home/ruan/data/mysqld.pid

启动和初始化

1
2
3
4
# 启动MySQL
mysqld_safe --defaults-file=my.cnf --user=ruan
# 初始化数据库
mysql_install_db --defaults-file=my.cnf --user=ruan

目录结构

1
2
3
4
5
6
7
8
9
10
$ tree data
data
├── my.cnf
├── mysql_data
│   ├── ibdata1
│   ├── ib_logfile0
│   ├── ib_logfile1
├── mysqld.log
├── mysqld.pid
└── mysql.sock

设置密码

1
mysql> use mysql; update user set password=password('m654321') where user='root'; flush privileges;

mitmproxy 使用

作者 Jay.Run
2026年3月20日 16:56

做 LLM Agent 开发时,经常要查看模型的请求与响应,需要一个便捷的工具。
mitmproxy 是一个功能强大的开源中间人代理,通常用于网络调试、分析 HTTP 和 HTTPS 流量。

安装

  1. 访问 https://www.mitmproxy.org/ ,根据指引安装,不同系统有区别
    • 也可以使用 pip install mitmproxy 安装
  2. 配置证书,用于解析 https,参考 https://docs.mitmproxy.org/stable/concepts/certificates/

使用

包含以下命令

  • mitmdump:支持将捕获的数据导出到文件,类似tcpdump
  • mitmproxy:TUI 实时查看、修改和调试流量
  • mitmweb:webui 实时查看、修改和调试流量(一般推荐用这个)

使用步骤

  1. 启动 mitmweb
  2. 设置代理环境变量:ALL_PROXY,
1
2
3
4
5
6
~ ❯ mitmweb -p 8082 -s ~/projects/scripts/mitmproxy_fix_unicode.py
[15:26:42.046] Loading script /Users/ruan/projects/scripts/mitmproxy_fix_unicode.py
[15:26:42.047] HTTP(S) proxy listening at *:8082.
[15:26:42.047] Web server listening at http://127.0.0.1:8081/?token=4987293fea3331a4f3de2b0ba836a85d
load: 11.06 cmd: mitmweb 3541 waiting 0.55u 0.19s
[15:42:34.333][[::1]:62868] client connect

python 证书相关问题

python 的 http 客户端有自己的证书处理逻辑,仅安装证书到系统还不能生效
需要额外设置环境变量

1
2
3
4
5
6
7
8
# 合并证书
cat $(python -c "import certifi; print(certifi.where())") \
~/.mitmproxy/mitmproxy-ca-cert.pem > ~/.mitmproxy/py-certifi-combined-ca.pem

# 设置环境变量
SSL_CERT_FILE=/Users/ruan/.mitmproxy/py-certifi-combined-ca.pem # httpx 库
REQUESTS_CA_BUNDLE=/Users/ruan/.mitmproxy/py-certifi-combined-ca.pem # requests 库
HTTPS_PROXY=https://localhost:8082

webui 的 json 中文显示问题

需要附加脚本:mitmweb -s ~/projects/scripts/mitmproxy_fix_unicode.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# mitmproxy_fix_unicode.py

import json
from mitmproxy import contentviews


class PrettyJsonChinese(contentviews.Contentview):
name = "JSON (中文)"
syntax_highlight = "yaml" # YAML is a superset of JSON, so this highlights JSON too

def prettify(self, data: bytes, metadata: contentviews.Metadata) -> str:
decoded = json.loads(data)
return json.dumps(decoded, indent=4, ensure_ascii=False, sort_keys=False)

def render_priority(self, data: bytes, metadata: contentviews.Metadata) -> float:
# Match the same content types as the built-in JSON view, but with higher priority
if metadata.content_type and "json" in metadata.content_type:
return 2
return 0

contentviews.add(PrettyJsonChinese)

让 json request 和 response 能够自动换行

使用 tampermokey 脚本 修改 webui

esim 使用相关

作者 Jay.Run
2026年3月4日 09:02

国行手机的 esim 功能有较多限制,使用不太方便。
但我们可以使用 esim 适配器,将 esim 转为实体 sim 卡。

esim 适配器

esim 适配器就是个 sim 卡,需要占用一个卡槽,可以通过软件将 esim 卡号写入到适配器。
可以写入多个 esim 号码,并且支持切换,但同时只能有一个在线。
适配器有不少品牌,如 estk、5ber、9esim,价格也各不相同,但是我都没怎么试过。
我使用的的适配器是 pdd 购买,20-30元,容量468KB。

esim 套餐

一般海外套餐都可以使用,我这里只用于接收短信,故使用 clubsim,成本大概50元。

号码写入

购买套餐后,运营商会提供二维码,安卓手机通过 OpenEUICC 扫描就可以写入和管理 esim。

如果是 iphone,需要使用 esim 读卡器来管理 esim 号码。

似乎对手机有些兼容性要求,较老的手机无法写卡。

使用感受

esim 还是挺方便的,用于接收验证码非常够用,还有些免费流量的卡。
但是切换 esim 号码时,可能需要切换飞行模式或者重启手机才能生效,不知是通病还是兼容性问题。

注意事项

由于安卓操作系统(11+)的 bug,在“移动网络”中禁用 esim 适配器会无法打开。
需要通过 adb 清除移动网络的数据才能恢复

1
adb shell pm clear com.android.providers.telephony

如果是小米等手机,清除过程可能会遇到adb权限相关问题
需要额外开启开发者选项中的“OEM解锁”和“USB调试(安全设置)”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
adb shell pm clear com.android.providers.telephony

Exception occurred while executing 'clear':
java.lang.SecurityException: PID 29160 does not have permission android.permission.CLEAR_APP_USER_DATA to clear data of package com.android.providers.telephony
at com.android.server.am.ActivityManagerService.clearApplicationUserData(ActivityManagerService.java:4101)
at com.android.server.am.ActivityManagerService.clearApplicationUserData(ActivityManagerService.java:4054)
at com.android.server.pm.PackageManagerShellCommand.runClear(PackageManagerShellCommand.java:2385)
at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:289)
at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)
at android.os.ShellCommand.exec(ShellCommand.java:38)
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onShellCommand(PackageManagerService.java:7022)
at android.os.Binder.shellCommand(Binder.java:1158)
at android.os.Binder.onTransact(Binder.java:960)
at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4729)
at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onTransact(PackageManagerService.java:7006)
at android.os.Binder.execTransactInternal(Binder.java:1433)
at android.os.Binder.execTransact(Binder.java:1372)

参考

Hexo 版本更新与技术债务

作者 Jay.Run
2026年1月2日 09:14

昨天花了些时间将 Hexo + next 的博客版本更新了下,发现不少问题。
博客已经很多年了,一直用的是最开始的Hexo版本,nodejs 也是 v12,next 主题也是早年源码安装的,还在上面做了修改。
也是早就明白需要更新了,但是觉得麻烦,就一直没动,最后变得相当麻烦。

依赖更新

首先是版本升级,先将 nodejs 切换到最新 lts(v22)。
再尝试使用npm将 package.json 中的库自动更新,但由于版本差距太大以及很多库不维护了,更新不了。

最后只能使用 hexo init 新建个博客项目,将里面的依赖项复制到老博客里,删除对应的老依赖。
剩下的依赖,大部分是安装的插件,只能一个个搜索用途,看看有没有替代或者还要不要用。
package.json 中的其他参数,参考最新的配置文件,将无用(过时)的设置项删除。

使用 npm 安装 next 主题,将原 themes/next 中的配置文件复制到 _config.next.yml。
对比 hexo 和 next 官方 _config 文件,相应修改 _config.yml 和 _config.next.yml。
删除 themes 中重复的主题目录。

使用 npm install 安装依赖,保证能够正常生成博客。

删除 node_modules,改用 pnpm 管理依赖,更轻量快速。
使用 pnpm 时可能会报错,如某个库的 index.node 找不到,是因为 pnpm 默认不执行库的 post install 脚本。
需要将这些库添加到 pnpm.onlyBuiltDependencies 中。

1
2
3
4
5
6
"pnpm": {
"onlyBuiltDependencies": [
"hexo-word-counter",
"hexo-util"
]
}

ps:

  • 总体来说 pnpm 还是存在一些兼容性问题,但大部分都不是 pnpm 本身的问题,基本上都是库的影子依赖。
  • 大模型在这种疑难杂症上帮助很大

额外设置

hexo

修改 scaffolds 文章模板文件
老版本 hexo 的 scaffolds/page.md 等文件格式与新版不同,会导致一些工具不能识别 front-matter, 需要更新。

老版本 front-matter 前面少了开头的 ---,导致 vscode 的 hexo-util 插件不能识别 tag 等信息,调试了很久。

1
2
3
4
5
6
7
8
9
10
# 老版本
title: {{ title }}
date: {{ date }}
---

# 新版本
---
title: {{ title }}
date: {{ date }}
---

next

自定义样式和脚本,之前是通过之间修改主题源码实现,现在使用 custom_file_path 注入自定义内容。
需要修改 _config.next.yml 文件

1
2
3
4
custom_file_path:
head: source/_data/head.njk
bodyEnd: source/_data/body-end.njk
style: source/_data/styles.styl

自定义样式

next 主题的 Muse scheme 整体比较符合我的口味,但部分样式还需要微调。

source/_data/styles.styl

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

// 自动给文章的 toc 和正文标题编号, 不对首页编号
body {counter-reset: h1}
.post-block .post-body h2 {counter-reset: h2}
.post-block .post-body h3 {counter-reset: h3}
.post-block .post-body h4 {counter-reset: h4}
.post-block .post-body h5 {counter-reset: h5}
.post-block .post-body h6 {counter-reset: h6}

.post-block .post-body h2:before {
counter-increment: h1;
content: counter(h1) ". "
}
.post-block .post-body h3:before {
counter-increment: h2;
content: counter(h1) "." counter(h2) " "
}
.post-block .post-body h4:before {
counter-increment: h3;
content: counter(h1) "." counter(h2) "." counter(h3) " "
}
.post-block .post-body h5:before {
counter-increment: h4;
content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) " "
}
.post-block .post-body h6:before {
counter-increment: h5;
content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) " "
}
.post-block .post-body h1.nocount:before,
.post-block.post .post-body h2.nocount:before,
.post-block.post .post-body h3.nocount:before,
.post-block.post .post-body h4.nocount:before,
.post-block.post .post-body h5.nocount:before,
.post-block.post .post-body h6.nocount:before {
content: "";
counter-increment: none
}

// 隐藏文章 toc 中文章目录 tab 中的友链信息,站点概览中的保留
.sidebar .sidebar-toc-active + .sidebar-blogroll {
display: none
}

只在文章页显示侧边栏

next 主题的侧边栏逻辑有点问题, 就算设置 sidebar.display 为 post,当从文章跳到其他页面时,侧边栏并不会自动关闭,所以只能通过脚本来绕过。

source/_data/body-end.njk

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
document.addEventListener('pjax:success', () => {
if (CONFIG.sidebar.display !== 'remove') {
const hasTOC = document.querySelector('.post-toc:not(.placeholder-toc)');

// 当没有 TOC 且侧边栏正在显示时,隐藏侧边栏
if (!hasTOC && document.body.classList.contains('sidebar-active')) {
window.dispatchEvent(new Event('sidebar:hide'));
}
}
});
</script>

自定义重定向

hexo 的文章不支持多个入口,比如我想要分类中英文都可以访问,默认情况是做不到的。

在设置了 category_map 的情况下,hexo 只会生成 programming 等英文链接,无法同时保留中文url作为入口。

1
2
3
4
5
category_map:
编程: programming
随笔: writing
阅读: reading
工作生活: life

所以最终通过在 404 页面添加自定义的 js 实现重定向逻辑。

source/_data/head.njk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% if page.title === '404 Page Not Found' or page.type === '404' or page.layout === '404' %}
<script>
(function() {
// 从 Hexo 配置中获取 category_map
var categoryMap = {{ config.category_map | dump | safe }};
var currentPath = decodeURIComponent(window.location.pathname);

// 检查路径中是否包含分类映射的key
for (var key in categoryMap) {
if (currentPath.includes(key)) {
var targetCategory = categoryMap[key];
var targetPath = currentPath.replace(key, targetCategory);

// 执行重定向
if (currentPath !== targetPath) {
window.location.replace(targetPath);
return;
}
}
}
})();
</script>
{% endif %}

更换 gitalk 为 utterances

原先一直使用 gitalk 作为文章的评论系统。
gitalk 每个新文章都需要作者登陆触发新建评论 issue,不太方便,所以现在换成 utterances。

utterances 评论插件 next 主题已内置,修改 _config.next.yml 可以开启。

1
2
3
4
5
6
# 注意先关闭 gitalk
utterances:
enable: true
repo: ruanimal/ruanimal.github.io # Github repository owner and name
# Available values: pathname | url | title | og:title
issue_term: title

还需要在 github pages 仓库安装 utterances 应用。

自定义的域名可能不被 utterances 支持,无法登陆跳转
需要新建 source/utterances.json

1
2
3
4
5
6
{
"origins": [
"https://blog.ponder.work",
"https://ruanimal.github.io"
]
}

如果你和我一样,博客有多个域名,副域名登陆跳转会失效,可以通过注入js修正
utterances 的登陆跳转是根据 canonical link,我们只要让它和当前url保持一致就行了

在 source/_data/head.njk 文件添加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
(function() {
var canonical = document.querySelector('head > link[rel="canonical"]');
if (canonical && canonical.href) {
try {
var oldUrl = new URL(canonical.href);
canonical.href = window.location.origin + oldUrl.pathname + oldUrl.search + oldUrl.hash;
} catch (e) {
console.warn('Failed to update canonical link:', e);
}
}
})();
</script>

更换编辑器

之前一直是用 mac 平台是 mweb 作为 hexo 的文章编辑器,实话说也非常好用。
但是我把主力操作系统换成 linux 之后,linux 上一直没找到与 mweb 相当的应用。
最终经过一番折腾,最终使用 vscode + Hexo Utils 插件作为平替。
同时对该插件进行来二开,最终体验算是追平了 mweb。

感受

所谓技术债务,项目正常迭代的过程中是不断积累的,如果平时不跟进解决,最终在某个阶段就会爆炸,造成巨大影响。

平时迭代时,如果时间安排的过紧,我们就会倾向于采取保守的决策(能跑就不要动),最终不可避免的技术债务堆积。

当然,生活中其实也是这样吧,很多不紧急的问题,一直拖着,最终大概率也会无可挽回。

配置 Linux 作为主力操作系统

作者 Jay.Run
2025年12月21日 21:48

这些年日常操作系统一直是 windows 和 macOS 交替使用,Linux 一般只作为服务器的操作系统。
然而,咖喱味的 Windows 11 (LTSC)用起来实在难受(平时只玩游戏,下载)
arm 的 macbook 虽然能效惊人,但是内存金子价格,软件也封闭(点名finder)和傲慢,实在受不了。
最后,还是转向 Linux,毕竟现在 wayland 基本堪用,国产软件也随着信创逐渐丰富了。

发行版选择

这里选用 KDE Neon user edition,基于 Ubuntu LTS, 有以下优点

  1. Ubuntu 用户基数大,有问题容易解决
  2. 可以安装星火应用商店,方便使用国产软件
  3. 可以用到最新的 KDE 桌面环境,wayland 支持更好
  4. KDE 远程桌面好用(服务端和客户端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OS: KDE neon User Edition x86_64
Host: MS-7D99 (3.0)
Kernel: Linux 6.14.0-33-generic
Uptime: 1 day, 19 hours, 23 mins
Packages: 2831 (dpkg)
Shell: bash 5.2.21
Display (E2434I-T): 1920x1080 @ 60 Hz in 24" [External]
Display (KOS2718): 3840x2160 @ 60 Hz (as 1920x1080) in 27" [External] *
DE: KDE Plasma 6.4.5
WM: KWin (Wayland)
WM Theme: Breeze
Theme: Breeze (Light) [Qt], Breeze [GTK2/3]
Icons: Breeze [Qt], breeze [GTK2/3/4]
Font: 微软雅黑 (10pt) [Qt], 微软雅黑 (10pt) [GTK2/3/4]
Terminal: /dev/pts/3
CPU: 13th Gen Intel(R) Core(TM) i5-13500 (20) @ 4.80 GHz
GPU 1: NVIDIA GeForce RTX 4060 Ti [Discrete]
GPU 2: Intel AlderLake-S GT1 @ 1.55 GHz [Integrated]
Memory: 25.07 GiB / 46.67 GiB (54%)
Swap: 392.00 KiB / 8.00 GiB (0%)

硬件要求

  1. NVIDIA 显卡在 Linux 下对 Wayland 支持不好,容易出现卡死或者重启的情况,建议显示器连到核显
  2. 有些主板 Linux 兼容性不行,建议使用御三家主板

系统设置

Nvidia 显卡驱动

显示器插到核显上,只使用 Nvidia 做计算,玩游戏也能调用到显卡。
使用开源服务器驱动,解决 Wayland 兼容性问题,防止玩游戏崩溃

  1. 安装驱动
1
sudo ubuntu-drivers install 580-server-open
  1. 启用睡眠支持参考
    将显存内容保存,否则唤醒后程序会报错(如CUDA程序)
1
2
3
4
5
# /etc/modprobe.d/nvidia-power-management.conf
options nvidia NVreg_PreserveVideoMemoryAllocations=1 NVreg_TemporaryFilePath=/tmp

sudo update-initramfs

docker

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
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done


# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update


sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker # 立即生效(或注销重新登录)

# docker 中调用显卡
https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

ipv6 启用 EUI-64

可以让 ipv6 的ip后缀根据mac生成,可用于配置防火墙规则
使用形如 ::<后缀>/::ffff:ffff:ffff:ffff 的掩码

1
2
nmcli con show
nmcli con modify <NETWORK NAME> ipv6.addr-gen-mode eui64

启用休眠文件

1
2
3
4
5
6
7
8
sudo su
cd /
truncate -s 0 swapfile
# chattr +C swapfile # disable COW on btrfs
fallocate -l 8G swapfile
chmod 0600 swapfile
mkswap swapfile
swapon swapfile

使用英文用户文件夹

用户个人文件夹(文档、下载等),默认是跟随系统语言。
可是中文在终端下输入不太方便,建议使用英文用户文件夹名称。
方法步骤:

  1. 语言修改成英文,注销重新登录
  2. 打开文件管理器,确认修改为英文
  3. 语言修改回中文
  4. 打开文件管理器,保留原名称

无线网卡调优

WIFI 启用 WOL(网络唤醒)

1
2
3
4
# 查看 WOL 支持
iw phy0 wowlan show
# 开启 WOL 支持
sudo iw phy0 wowlan enable magic-packet

关闭节能模式,解决无线网口入站延迟较大

1
2
3
4
5
6
7
8
cat <<EOF > /etc/NetworkManager/conf.d/wifi-powersave-off.conf
[connection]
wifi.powersave = 2

EOF

# 或者启动时设置
sudo iw dev wlo1 set power_save off

睡眠调整

睡眠到内存(suspend)耗电量很低,够用了,没必要开启混合休眠

1
2
3
4
5
6
7
8
9
10
11
12
# 只睡眠到内存
# 修改 /etc/systemd/sleep.conf
AllowSuspend=yes
AllowHibernation=no
AllowSuspendThenHibernate=no
AllowHybridSleep=no
SuspendState=mem standby

# 禁用休眠(可选)
sudo systemctl mask hibernate.target hybrid-sleep.target

# nvida

intel 核显性能优化

10代以上 intel 核显

1
2
3
4
# cat /etc/modprobe.d/i915.conf
options i915 enable_fbc=1 enable_guc=2 enable_dc=0

sudo update-initramfs -u

RDP 远程桌面

服务端: 使用 KDE 默认的远程桌面 (可惜稳定性一般)
客户端: apt install krdc

其他配置

1
2
3
4
5
6
7
# 双屏只显示其中一个屏幕
# 修改 ~/.config/systemd/user/plasma-krdp_server.service
ExecStart=/usr/bin/krdpserver --monitor 0

# 重启服务
systemctl --user daemon-reload
systemctl --user restart app-org.kde.krdpserver.service

使用新内核(可选)

发行版默认内核版本可能比较老,如有必要可以更新内核
注意:mainline 的内核与 ubuntu-drivers 的 nvidia 显卡驱动不兼容(所以一般不推荐)

1
2
3
sudo add-apt-repository ppa:cappelikan/ppa
sudo apt update
sudo apt install mainline

软件使用

常见应用替代方案

  • 任务管理器
    • flatpak install io.missioncenter.MissionCenter
  • 词典:使用欧陆词典
    • flatpak install net.eudic.dict
  • 微信/qq
    • 官网版本就行了
  • 下载管理器:
    • motrix:普通http下载
    • qbittorrent: bt下载
  • 虚拟机
    • KVM:性能更好
    • VMware:易用性更好
  • 游戏
    • steam:大部分游戏都可以直接运行,性能损耗也不大
  • 视频播放
    • haruna

KVM 虚拟机

安装

1
2
3
sudo apt install qemu-kvm libvirt-daemon-system virt-manager
sudo usermod -aG libvirt,kvm $USER
sudo systemctl enable --now libvirtd

硬盘直通(scsi), 相比下面的方法性能更好

1
2
3
4
5
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native'/>
<source dev='/dev/disk/by-id/ata-TOSHIBA' index='2'/>
<target dev='sdg' bus='scsi'/>
</disk>

硬盘直通(virtio),samba分享时可能卡顿

1
2
3
4
5
<disk type="block" device="disk">
<driver name="qemu" type="raw" cache="none"/>
<source dev="/dev/disk/by-id/ata-ST3000DM008"/>
<target dev="sdc" bus="virtio"/>
</disk>

无线网口不支持桥接,使用路由绕过

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
# 修改 /etc/sysctl.conf; sudo sysctl -p 生效
net.ipv4.ip_forward = 1
net.ipv4.conf.wlo1.proxy_arp=1
net.ipv4.conf.br-routed.proxy_arp=1


# 创建网桥和路由
#/etc/systemd/system/bridge-routed.service
[Unit]
Description=Create routed bridge br-routed
After=network.target
Wants=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/usr/bin/ip link add br-routed type bridge
ExecStart=/usr/bin/ip link set br-routed up
ExecStartPost=/usr/bin/ip route add 192.168.1.41/32 dev br-routed
ExecStop=/usr/bin/ip link delete br-routed

[Install]
WantedBy=multi-user.target


# 修改 kvm 配置
<interface type="bridge">
<mac address="00:00:00:00:00:00"/>
<source bridge="br-routed"/>
<target dev="vnet0"/>
<model type="virtio"/>
<alias name="net0"/>
<address type="pci" domain="0x0000" bus="0x0a" slot="0x00" function="0x0"/>
</interface>

# 虚拟机使用静态ip
IPv4 地址 . . . . . . . . . . . . : 192.168.1.41(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.1.1
DNS 服务器 . . . . . . . . . . . : 192.168.1.1

dolphin 右键菜单自定义快捷方式

支持文件和文件夹

chmod + ~/.local/share/kio/servicemenus/mklink.desktop

1
2
3
4
5
6
7
8
9
10
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=inode/directory;application/octet-stream;
Actions=RunMyScript

[Desktop Action RunMyScript]
Name=制作符号链接
Icon=emblem-symbolic-link
Exec=/home/rlj/scripts/mklink.sh "%F"

mklink.sh

1
2
3
4
#!/usr/bin/env bash

NAME=$(basename "$1")
ln -s "$NAME" "$NAME.lnk"

haruna 视频播放器

haruna 是 mpv 播放器的前端,功能比较齐全

存在的问题:

  • 视频播放器首次最大化窗口,总是跳到另一个屏幕
    • 关闭另一个屏幕,关闭应用,再打开,再最大化,再重新打开屏幕
  • 无法重命名文件
    • 设置 flatpak 目录权限

flatpak 使用

1
2
3
4
5
# 换源
flatpak remote-modify flathub --url=https://mirrors.ustc.edu.cn/flathub

# 目录授权,解决应用无法编辑文件
flatpak override --user org.kde.haruna --filesystem=/share

挂载 SMB 共享支持 windows 符号链接

支持原生符号链接(目录连接),双向操作都能同步

步骤

  • windows 允许符号链接
    计算机配置 → 管理模板 → 系统 → 文件系统 → 启用 Win32 长路径
    计算机配置 → windwows设置 → 安全设置 → 本地策略 → 用户权限分配 → 创建符号链接

  • samba 挂载选项
    symlink=native

  • 链接时使用相对路径(两个系统绝对路径不一样)

遇到的问题

故障排查方法

  1. 查看系统日志 journalctl -k -b0
  2. 查看用户日志,比如应用崩溃 journalctl –user -b0
  3. 用 AI 搜索相关问题,如果无法解决再尝试 google 搜索

edge 无法启动

edge 异常退出后,有概率无法启动

报错

1
2
Microsoft Edge 进程似乎正在使用此用户配置。
Microsoft Edge 已锁定此用户配置以防止损坏。如果你确定没有其他进程正在使用此用户配置,可以将其解锁并重新启动 Microsoft Edge。

处理方法,其他 chromium 内核浏览器可能也类似

1
rm ~/.config/microsoft-edge/SingletonLock

steam 玩游戏卡住

dmesg 信息

1
2
3
4
727 10:48:59 neon kernel: x86/split lock detection: #AC: CPU 0/KVM/88279 took a split_lock trap at address: 0xfffff80104613be8
727 10:51:00 neon kernel: x86/split lock detection: #AC: CPU 2/KVM/88281 took a split_lock trap at address: 0xfffff8010465701c
727 10:52:51 neon kernel: x86/split lock detection: #AC: CHTTPClientThre/90929 took a split_lock trap at address: 0xf098fc4f
727 10:53:07 neon kernel: x86/split lock detection: #AC: CPU 1/KVM/88280 took a split_lock trap at address: 0xfffff8010465701c

解决方法

1
2
3
4
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT='quiet splash split_lock_detect=off'

sudo update-grub

迁移系统&修复引导

  1. 进入 livecd,使用 Gparted 复制系统到新硬盘
  2. 修改新硬盘分区的 /etc/fstab 的挂载点 uuid
  3. 重建 grub 引导
1
2
3
4
5
6
7
mount /dev/sdaX /mnt/
mount /dev/sda0 /mnt/boot/efi
for i in {proc,dev/sys}; do
mount --bind /${i} /mnt/${i}
done

chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot/efi

睡眠唤醒后很卡

桌面很卡,cpu 频率上不来,只有 800mhz。
映泰主板兼容性问题,换主板解决

终端删除文件到回收站

1
2
sudo apt install trash-cli
alias rm=trash

electorn 应用无法启动

报错 chrome sandbox 相关

1
2
3
4
5
# 编辑 /etc/sysctl.conf 添加
kernel.apparmor_restrict_unprivileged_userns=0

# 生效
sudo sysctl -p

登录界面隐藏无关用户

比如某些用于文件共享的用户

1
2
3
4
5
6
7
# 修改 /etc/sddm.conf
[Autologin]
Session=plasma

[Users]
HideUsers=data,data-r,data-s
HideShells=/usr/sbin/nologin,/bin/false

偶发硬件设备工作不正常

强制重启后或者系统崩溃后,如果设备不正常,可以尝试重启到 windows 再切回 linux

应用在启动器里找不到

1
2
rm -f ~/.config/menus/
kbuildsycoca6 --noincremental

修复睡眠唤醒问题

睡眠之后可能出现莫名奇妙的唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看当前唤醒触发器
cat /proc/acpi/wakeup | grep enable

# 查看触发器的设备路径
for p in /sys/class/wakeup/*/device/power/wakeup; do
dev=$(dirname $(dirname $p));
dev_path=$(realpath $dev)
echo ${dev_path}
done


# 直接修改是 /proc/acpi/wakeup 是不生效的
# 设置具体设备的方式禁用 rtc 和 AWAC, 位置格式 ${dev_path}/power/wakeup
# 可以将禁用命令配置到 rc.local
echo disabled | tee /sys/devices/platform/rtc_cmos/power/wakeup # rtc
echo disabled | tee /sys/devices/platform/rtc_cmos/rtc/rtc0/alarmtimer.0.auto/power/wakeup # rtc
echo disabled | tee /sys/devices/platform/ACPI000E:00/power/wakeup # AWAC

# 检查是否生效
cat /proc/acpi/wakeup | grep enable

apt 设置代理

部分 apt ppa 源需要代理,但是国内的镜像源却不需要。
所以需要给每一个源单独设置代理的功能

有两种设置方式

  • 白名单模式(只给需要的配置)
  • 黑名单模式(先设置默认代理, 不需要的设置直连)

设置语法

  • 设置代理服务器:Acquire::http::Proxy <proxy-server>
  • 设置某个域名的代理:Acquire::http::proxy::<domain> <proxy-server|DIRECT>

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#  /etc/apt/apt.conf.d/99proxy

# 给每种协议的源设置代理,一般设置 https::Proxy 就行了
#Acquire::http::Proxy "http://yourproxyaddress:proxyport";
#Acquire::https::Proxy "http://yourproxyaddress:proxyport";
#Acquire::ftp::Proxy "http://yourproxyaddress:proxyport";
#Acquire::socks::Proxy "http://yourproxyaddress:proxyport";

# 设置具体域名是否走代理
#Acquire::http::proxy::local.mirror.address "DIRECT";
#Acquire::http::proxy::HOST_NAME_TO_BE_PROXIED "http://yourproxyaddress:proxyport"

# 黑名单模式示例
Acquire::https::Proxy "http://localhost:10808";
Acquire::https::Proxy::"mirrors.aliyun.com" "DIRECT";

限制硬盘读写速度

某些nvme硬盘的发热很严重, 如果全速运行会导致系统不稳定.
所以需要对硬盘速度进行限制

使用 systemd 设置限速

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建配置目录
sudo mkdir -p mkdir -p /etc/systemd/system/{user,system}.slice.d/

# 设置用户限制
sudo tee /etc/systemd/system/user.slice.d/io-limit.conf > /dev/null <<EOF
[Slice]
IOReadBandwidthMax=/dev/nvme0n1 800M
IOWriteBandwidthMax=/dev/nvme0n1 300M
IOReadIOPSMax=/dev/nvme0n1 60000
IOWriteIOPSMax=/dev/nvme0n1 30000

EOF

# 系统也使用同样的限制
sudo ln -sf /etc/systemd/system/user.slice.d/io-limit.conf \
/etc/systemd/system/system.slice.d/

速度测试

1
2
3
4
5
6
~$ dd if=/dev/zero of=testfile bs=1M count=2048 oflag=direct status=progress \
&& rm testfile
2099249152 bytes (2.1 GB, 2.0 GiB) copied, 7 s, 300 MB/s
2048+0 records in
2048+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 7.16429 s, 300 MB/s

archlinux 命令行不能使用回收站

安装 cachyos 时,trash-cli 不能使用,其他应用也不能删除文件到回收站

1
sudo pacman -S gvfs 

某些软件中输入法不可用

在使用 fcitx 输入法时,某些软件中无法唤起输入法,无法输入中文(如:微信)
通常是因为该程序默认加载了 ibus 模块,只要设置 QT_IM_MODULE=fcitx 环境变量就能解决问题。
为了不带来别的影响,仅修改 .desktop 启动文件就行。

1
2
3
4
mkdir -p ~/.local/share/applications

sed 's|Exec=/usr|Exec=env QT_IM_MODULE=fcitx /usr|' \
/usr/share/applications/wechat.desktop > ~/.local/share/applications/wechat.desktop

WPS 每次登录自动启动

WPS 安装后会在 /etc/xdg/autostart/ 放置一个 wps-office-autostart.desktop,桌面环境每次登录都会扫描该目录并执行。它会启动 wpsd 守护进程,预加载 wps、wpp、et 三个组件(即”快速启动”功能)。

这个机制跟 systemd 无关,所以禁用 systemd 服务不会生效。

排查过程

1
2
3
4
5
6
7
# 查看 XDG 自启动项
ls /etc/xdg/autostart/ | grep wps
# wps-office-autostart.desktop

# 查看启动内容
cat /etc/xdg/autostart/wps-office-autostart.desktop
# Exec=/opt/apps/cn.wps.wps-office-pro/files/bin/quickstartoffice start

解决方法:在用户级别覆盖禁用(不影响其他用户,WPS 更新也不会覆盖)

1
2
3
mkdir -p ~/.config/autostart
cp /etc/xdg/autostart/wps-office-autostart.desktop ~/.config/autostart/
echo "Hidden=true" >> ~/.config/autostart/wps-office-autostart.desktop

然而,以上配置并没有效果

其实是 KDE 会话恢复机制启动的 wps, KDE 的会话管理基于 X11 的 XSMP(X Session Management Protocol)。
保存时机是注销/关机时,ksmserver 会向所有注册了 XSMP 的客户端发送 SaveYourself,然后把各客户端返回的 restartCommand 写进 ksmserverrc。

但问题在于 WPS 的行为比较特殊:即使你在关机前关闭了 WPS 的窗口,wpscloudsvr 和 et 等后台进程可能并没有真正退出。
它们会驻留在后台(类似托盘常驻),所以 ksmserver 在保存会话时检测到它们仍在运行,就记录下来了。

所以:关闭会话恢复(启动为空会话)就行了,我并不需要这个。

关于 ntfs

尽量不要在 linux 下使用 ntfs 硬盘,特别是在大量读写的情况下稳定性一般,可能出现掉盘(即使使用 ntfs3 驱动)
如果出现 ntfs 硬盘的异常,如掉盘、文件不显示等问题,建议重新启动到 windows 然后运行硬盘的检测修复。

文件写入磁盘先快后慢

由于 linux 默认的磁盘缓存策略比较激进,导致文件复制一开始时很快后面变得很慢。
特别是内存比较大、或者硬盘速度比较慢(如U盘)时,可能导致后期写入时假死或者写入失败。

相关的内核参数

1
2
3
4
5
6
7
8
9
# 后台开始回写脏页的内存大小(1G)
vm.dirty_background_bytes = 1073741824
# 强制同步回写脏页的内存 (2G)
vm.dirty_bytes = 2147483648

# 相应的回写比例,实际值等于:ratio x 内存大小
# 上面的固定值和比例值两种二选一
vm.dirty_background_ratio = 2
vm.dirty_ratio = 4

一般桌面使用设置 1G/2G 够了,而 ubuntu 24 默认是 10%/20%,在大内存上有点丧心病狂了。
如果主要使用机械硬盘,还可以考虑再调小些。

修改文件句柄数量限制

默认限制是 1024, 文件句柄数量太小会导致应用异常,比如 vscode 无法监控文件变更

修改 /etc/security/limits.conf,在最后追加,然后重启电脑

1
2
* soft nofile 10240
* hard nofile 10240

如果没有生效,可能需要修改 systemd 配置文件

编辑 /etc/systemd/system.conf 或者 /etc/systemd/user.conf 文件,找到 DefaultLimitNOFILE 这一行,取消注释并修改:

1
DefaultLimitNOFILE=10240:10240
1
2
3
4
# 重载系统配置
sudo systemctl daemon-reload
# 重载用户配置
systemctl --user daemon-reload

zram 内存压缩替代 swapfile

在大内存的场景,swapfile 就没啥必要了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装
sudo apt install zram-tools

# 编辑 /etc/default/zramswap,添加
ALGO=zstd
PERCENT=10

# 重启服务
sudo systemctl restart zramswap.service

#(可选)关闭 swapfile ,从fstab 中注释 swapfile 条目

#(可选)内存充足的情况,降低内存压缩的优先级
# 修改 sysctl.conf, 追加以下配置
vm.swappiness=10
❌
❌