阅读视图

发现新文章,点击刷新页面。
🔲 ☆

UI 区域检测的 vibe coding 复盘

文本是用 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 网页自动关注

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 作为主力操作系统

这些年日常操作系统一直是 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 做自动化

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 文件保存对话框被可能被反复触发,原因不明。
🔲 ☆

语言的力量

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

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

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

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

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

🔲 ☆

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

联想某些版本的 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保护机制认证” 问题

使用 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 上访问安卓应用

有些应用在安卓上是独占的,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

本文介绍在 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 环境包管理

一些公用的 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 词典增加生词本功能

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 中卡片生成的效果

参考

🔲 ☆

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

我们在测试代码时,由于需要经常重启服务,经常会发现服务端口被占用。
一般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)

参考

🔲 ⭐

容器内进程优雅退出

在使用 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 循环变量泄露与延迟绑定

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

梦开始的地方

先看看一开始的问题,可以看到这里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 自定义数据库路径

最近的一些文章是整理以前的笔记
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 使用

做 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 使用相关

国行手机的 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 版本更新与技术债务

昨天花了些时间将 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 作为主力操作系统

这些年日常操作系统一直是 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
❌