MoreRSS

site iconAmeow | 阿猫修改

后端工程师,写Go 和 Python,运营「猫鱼周刊」。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Ameow | 阿猫的 RSS 预览

猫鱼周刊 vol. 083 扫街友好城市

2025-10-19 20:11:07

关于本刊

这是猫鱼周刊的第 84 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

头条

距离上次出门拍照又有一个多月了,这周还是没有头图,实在是有点苦恼。上次大芬油画村多少有点「诈骗」,坐一个小时地铁过去,看一个城中村,而且深圳就不乏这种虚无的「打卡点」。

下周五是 1024,「程序员节」,这里祝各位程序员读者节日快乐,bug--。

这周有一篇「产出」,这篇文章的来历有点神奇:在大概一年前,我因为做剪辑相关的工作,接触到了 FFmpeg 以及硬件加速相关的东西,在中文搜索结果里没什么人提到,有的文章也比较旧,只考虑到了 Intel 的 qsv 和 Nvidia 的 NVENC,没有 AMD 的 amf 和 Apple 的 VideoToolbox,正好当时折腾这个能适应不同平台自动启用硬件加速的功能,所以在博客的写作列表上列了。结果这篇文章一咕再咕,直到最近这个项目重新捡起来,于是我尝试用 Claude Code 结合我的代码库,帮我写成了这篇文章「FFmpeg 硬件加速小记」。

我在文章开头标明了「本文有 AI 参与编写」,之所以不说「由 AI 生成」,是因为文章的核心思路、代码都是以「我」为主导,是我自己思考得出的。

这种创作方式还挺有意思,因为很多时候在写完某个功能之后,会有很强的欲望想分享自己实现的方案,但是重新总结形成文字又是很消耗精力的过程(所以这篇才会被我拖了接近一年)。提供思路(文章大纲和代码),让 AI 代劳文字组织的过程,至少能形成一篇像模像样的文章。这跟「由 AI 生成」最大的区别就是,如果你让 AI 「写一篇 ffmpeg 硬件加速的文章」,这是没有你的功劳的。

文章

我为什么厌恶 Sora?

原文链接 1
原文链接 2

一共是两篇文章,分别从创作者和受众的角度评价 AIGC。有些观点很耐人寻味:

快消层面的内容或许将会被 AI 完全替代,而深度哲思的部分仍然(暂时)归人类所有。

AI 替代的不一定是所有创作者,但一定会替代的是不再愿意创作的创作者。

重新理解 AI 的功能性,它是放大器,而人可以作为源头,只要源头保持流动性,AI 就无法彻底取代你。

他的思考很有深度,感兴趣可以看看原文。

在「受众」这一块,我每周大概会读到上百篇有长有短的文章,但是我从来不用 AI 去做什么「摘要」之类的事情,其实摘要在筛选「有兴趣的引发思考的」上有种多此一举,我往往在打开网页,看完标题和首段或者大致扫过整篇文章的结构之后,我就有定夺我是否感兴趣看完(参考Yay or Nay)。「节约时间」的论点其实不太成立,读完一篇表达流畅的文章通常也只需要几分钟,比起作者创作的数十分钟至数小时、数天来说算是一种基本的尊重。

作为「创作者」,我的核心原则是「原创性」,我始终觉得 AI 无法提出真正「原创」的东西。但是在创作的时候是否应该使用 AI,或者说使用 AI 辅助创作是否会失去这种「原创性」,我觉得没有简单直接的答案。在头条的例子中,我其实做了大多数原创的检索和思考,这跟简单让 AI 「写一篇 ffmpeg 硬件加速的文章」是不一样的。

背包三年,我的旅行装备清单

原文链接

很喜欢这种「好物推荐」类型的文章,当然这种类型已经被各种商单泛滥,所以看到这种认真在推荐的会觉得更有意思。

我之前也考虑过在周刊中加一个好物推荐的栏目,不过不是每周都会购物,写集合的文章我又懒得写,所以偶尔买到好的东西我会在「想法」的地方推荐一下(例如Quote/0CyberBrick拓竹 P1SC等)。

衣服这块我觉得值得提一下。有些户外的面料,不在意搭配的话其实日常穿也很合适,舒适度和实用性比传统的材料好得多。我现在日常穿迪卡侬的速干 T 恤,虽然说是「运动装」,干爽透气以及轻盈有弹性的面料,非常适合广东炎热潮湿的天气,穿着上班、健身都适合。

揽物日志 Vol.7

原文链接

家居向的「好物推荐」。收拾、装饰家里真的是一个时不时做一下很舒服很解压的事情。

AI 编程在七猫的实践

AI 编程落地业务开发的探索与实践
AI 代码评审在七猫的实践
AI 时代的 Code Review 最佳实践

一共有好几篇文章,合在一块说了。感觉这个技术团队非常有意思,很鼓励成员去探索 AI 编程的用法,而且很注重分享。

我在公司跟同事分享过这几篇文章,有一个很有意思的反应:「他们真的落地了吗」。背景是类似的东西(例如 AI Code Review)我在 2023 年就搞过,反正在我公司的环境,这个事情最终没有推行下去,从人的角度来说好像技术同事没有很拥抱 AI,从公司的角度这件事不见得有「价值」。所以这几篇文章让我觉得七猫这家公司的技术团队还是很有「工程师文化」,会鼓励成员去做一些「技术上很酷」的事情。

话说回来,AI Code Review 这件事,当时遇到的一些瑕疵(例如行号的识别、nit(可改可不改) 的把握等),居然到 2025 年,模型更新了两三代还是存在。

想法

扫街友好城市

接着头条的话题。我比较喜欢「扫街」,我是很典型的 i 人,在街上游荡,捕捉一些小小的美好,是我比较喜欢的摄影风格。深圳这个城市就不是很友好,这个城市主要以「石屎森林」为主,缺乏自然风光(其实也有比较好的海边),最重要的是没什么文化沉淀、没什么多样性。

我觉得要提「扫街友好」,首先是香港。住在深圳,这算是最快到达能看到「异域风光」的地方,路牌、街景等等都跟国内有很鲜明的对比。文化沉淀就是,有一种很独特的风格,如果非要我概括的话,就是老旧和时尚毫不违和地融合,优雅又整洁。香港总会给人一种很旧的感觉,有很多东西从上个世纪一直沿用到现在,例如一些用语、标牌的设计等等,尤其是很多街道和建筑实际已经建成上百年仍未变更。另一方面这里的多样性也很足,一条路上可以有佛教寺庙、清真寺和教堂,路上有各色人种人来人往,这种景象在国内真的很少见。所以在这里做「人文摄影」会很有意思,我每次去香港都能拍很多照片。

再有就是广州。这是我土生土长的地方,所以说实话有一点点特殊加成。广州跟深圳最大的不同是有很多古迹,老城区很多地方都还是十几、甚至几十年前的样子,跟千篇一律的现代城市有很大反差。

还有一点是,香港和广州有这种景色的地方很多。例如在香港你可以深度去逛旺角、九龙、中环、坚尼地城、赤柱,在广州你可以逛公园前、东山口、上下九,每个地方都有不一样的景色。而在深圳,来来去去就是各种名字不一样但实际差不多的公园和商场。

当然,这几个地方只是我常去/熟悉的,不代表其他地方就不好。我之前去过潮州,有很多古迹,除了一些步行街以外商业化味道也不是很重,也算很出片。但是像长沙,城市的商业化味道就很重,到处是网红打卡点,但打卡这件事情本来就很千篇一律,反而是让我觉得有点反感(见城市旅游就是打卡吗?)。

也欢迎各位推荐一些深圳或者周边适合扫街的地方,真的很久没碰相机了。

项目

linearmouse

项目链接

用来分别设置鼠标和触控板滚动方向的工具。

macOS 这点真的非常蛋疼,多数人鼠标的习惯是滚轮向上、内容往下,而触控板的逻辑是「自然滚动」,即往上滑动、内容往下。其实这两种逻辑都是合理的,有点像游戏中是否需要反转输入方向的问题(感兴趣可以看这篇文章),只是多数人是先从 PC(Windows)学习使用电脑,也自然而然习惯对应的鼠标操控逻辑。

如果你是用的是罗技的鼠标,可以用自带的 Logi Options+ 设置鼠标滚动的方向,不需要这些软件。

Mos

项目链接

也是一个类似的软件,除了可以单独设置鼠标和触控板滚动方向以外,还有平滑的功能,对习惯用触控板的人来说,临时使用鼠标的滚动体验会舒服很多。

minimind

项目链接

从零开始训练自己的超小语言模型。比较有意思的动手教程,完全开源免费,写得也很详细有深度。

工具/网站

LLM 驱动的词典

网站链接

我觉得比传统词典更加「生动活泼」,具体的可以看作者的文章

最后

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

FFmpeg 硬件加速小记

2025-10-13 02:37:42

本文有 AI 参与编写。

什么是硬件加速?

硬件加速是指利用计算机中的专用硬件(如 GPU、专用编解码芯片)来执行视频编解码任务,而不是仅依赖 CPU 进行软件编码。相比纯软件编码,硬件加速具有以下优势:

  • 更快的处理速度:专用硬件针对视频编解码进行了优化,处理速度可以提升数倍
  • 更低的 CPU 占用:将负载转移到 GPU 或专用芯片,释放 CPU 资源
  • 更低的功耗:硬件编码通常比软件编码更节能,延长笔记本电脑续航时间

硬件加速的权衡

虽然硬件加速很快,但也有一些需要注意的地方:

  • 压缩效率略低:硬件编码器为了速度牺牲了一些压缩效率,相同质量下文件可能略大
  • 可控性较差:硬件编码器的参数调节选项通常少于软件编码器
  • 平台依赖性:不同平台和硬件支持的加速方式不同

主流硬件加速方案

1. VideoToolbox (macOS/iOS)

Apple 的硬件加速框架,支持 macOS 和 iOS 设备。

# 编码器
h264_videotoolbox
hevc_videotoolbox

# 使用示例
ffmpeg -i input.mp4 -c:v h264_videotoolbox -b:v 2M output.mp4

特点

  • 在 Apple Silicon (M1/M2/M3) 芯片上性能出色
  • 支持硬件加速的 H.264、HEVC、ProRes 编码
  • 低功耗,适合移动设备

2. NVENC (NVIDIA GPU)

NVIDIA GPU 内置的硬件编码器,从 GTX 600 系列开始支持。

# 编码器
h264_nvenc
hevc_nvenc
av1_nvenc  # RTX 40 系列及以上

# 使用示例
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset p4 output.mp4

特点

  • 性能强劲,编码质量较好
  • 支持多路并行编码
  • 新一代显卡支持 AV1 编码

3. QuickSync (Intel 集成显卡)

Intel 集成显卡的硬件编码器,从第二代酷睿开始支持。

# 编码器
h264_qsv
hevc_qsv
av1_qsv  # 12 代及以上

# 使用示例
ffmpeg -hwaccel qsv -i input.mp4 -c:v h264_qsv -preset medium output.mp4

特点

  • 在没有独立显卡的情况下性能不错
  • 功耗低
  • 新一代处理器支持 AV1 编码

4. AMF (AMD GPU)

AMD GPU 的硬件编码器。

# 编码器
h264_amf
hevc_amf
av1_amf  # RX 7000 系列及以上

# 使用示例
ffmpeg -hwaccel amf -i input.mp4 -c:v h264_amf output.mp4

5. VAAPI (Linux)

Linux 上的通用硬件加速接口,支持 Intel、AMD 等多种硬件。

# 使用示例
ffmpeg -hwaccel vaapi -vaapi_device /dev/dri/renderD128 -i input.mp4 \
  -vf 'format=nv12,hwupload' -c:v h264_vaapi output.mp4

在 Python 中检测和使用硬件加速

下面是一个完整的 Python 实现,可以自动检测系统可用的硬件加速方式并选择最佳方案:

完整实现

import subprocess
import logging
from typing import Optional, Tuple

logger = logging.getLogger(__name__)


def get_hardware_encoder(use_hwaccel: bool = True) -> Tuple[str, Optional[dict]]:
    """
    检测可用的硬件加速器并返回合适的编码器设置。

    Args:
        use_hwaccel: 是否启用硬件加速

    Returns:
        (video_codec, hwaccel_options) 元组
        - video_codec: 编码器名称,如 'h264_videotoolbox'
        - hwaccel_options: 硬件加速选项字典,如 {'hwaccel': 'videotoolbox'}
    """
    if not use_hwaccel:
        return "libx264", None

    try:
        # 检查可用的硬件加速器
        hwaccel_result = subprocess.run(
            ["ffmpeg", "-hwaccels"],
            capture_output=True,
            text=True,
            timeout=5
        )
        hwaccels = hwaccel_result.stdout.lower()

        # 检查可用的编码器
        encoder_result = subprocess.run(
            ["ffmpeg", "-encoders"],
            capture_output=True,
            text=True,
            timeout=5
        )
        encoders = encoder_result.stdout.lower()

        logger.debug(f"Available hardware accelerators: {hwaccels}")
        logger.debug(f"Available encoders: {encoders}")

        def test_encoder(codec: str) -> bool:
            """测试编码器是否真正可用"""
            try:
                result = subprocess.run(
                    [
                        "ffmpeg",
                        "-f", "lavfi",           # 使用虚拟输入
                        "-i", "testsrc=duration=1:size=320x240:rate=1",
                        "-frames:v", "1",        # 只编码一帧
                        "-c:v", codec,           # 指定编码器
                        "-f", "null",            # 输出到空设备
                        "-"
                    ],
                    capture_output=True,
                    text=True,
                    timeout=10
                )
                return result.returncode == 0
            except Exception as e:
                logger.debug(f"Failed to test encoder {codec}: {e}")
                return False

        # 按优先级检测硬件编码器

        # 1. VideoToolbox (macOS/Apple Silicon)
        if "h264_videotoolbox" in encoders and "videotoolbox" in hwaccels:
            if test_encoder("h264_videotoolbox"):
                logger.info("Using VideoToolbox hardware acceleration")
                return "h264_videotoolbox", {"hwaccel": "videotoolbox"}

        # 2. NVIDIA NVENC
        if "h264_nvenc" in encoders:
            if test_encoder("h264_nvenc"):
                logger.info("Using NVIDIA NVENC hardware acceleration")
                if "cuda" in hwaccels:
                    return "h264_nvenc", {"hwaccel": "cuda"}
                return "h264_nvenc", None

        # 3. Intel QuickSync
        if "h264_qsv" in encoders and "qsv" in hwaccels:
            if test_encoder("h264_qsv"):
                logger.info("Using Intel QuickSync hardware acceleration")
                return "h264_qsv", {"hwaccel": "qsv"}

        # 4. AMD AMF
        if "h264_amf" in encoders and "amf" in hwaccels:
            if test_encoder("h264_amf"):
                logger.info("Using AMD AMF hardware acceleration")
                return "h264_amf", {"hwaccel": "amf"}

        # 5. VAAPI (Linux)
        if "h264_vaapi" in encoders and "vaapi" in hwaccels:
            if test_encoder("h264_vaapi"):
                logger.info("Using VAAPI hardware acceleration")
                return "h264_vaapi", {"hwaccel": "vaapi"}

    except Exception as e:
        logger.warning(f"Error checking hardware encoders: {e}")
        logger.info("Falling back to software encoding")

    # 回退到软件编码
    logger.info("Using software encoding (libx264)")
    return "libx264", None


def get_system_info() -> dict:
    """获取系统硬件加速信息"""
    try:
        # 获取 FFmpeg 版本
        version_result = subprocess.run(
            ["ffmpeg", "-version"],
            capture_output=True,
            text=True,
            timeout=5
        )

        # 获取硬件加速列表
        hwaccel_result = subprocess.run(
            ["ffmpeg", "-hwaccels"],
            capture_output=True,
            text=True,
            timeout=5
        )

        # 获取编码器列表(只提取硬件编码器)
        encoder_result = subprocess.run(
            ["ffmpeg", "-encoders"],
            capture_output=True,
            text=True,
            timeout=5
        )

        hw_encoders = []
        for line in encoder_result.stdout.split('\n'):
            if any(hw in line.lower() for hw in ['nvenc', 'qsv', 'videotoolbox', 'amf', 'vaapi']):
                hw_encoders.append(line.strip())

        return {
            "ffmpeg_version": version_result.stdout.split('\n')[0],
            "hwaccels": hwaccel_result.stdout,
            "hw_encoders": hw_encoders
        }
    except Exception as e:
        return {"error": str(e)}

使用示例

1. 检测系统信息

import json

# 获取系统硬件加速信息
info = get_system_info()
print(json.dumps(info, indent=2))

2. 在 ffmpeg-python 中使用

import ffmpeg

def encode_video_with_hwaccel(input_path: str, output_path: str, use_hwaccel: bool = True):
    """使用硬件加速编码视频"""

    # 获取硬件编码器
    vcodec, hw_options = get_hardware_encoder(use_hwaccel)

    # 创建输入流
    if hw_options:
        # 使用硬件加速解码
        stream = ffmpeg.input(input_path, **hw_options)
    else:
        stream = ffmpeg.input(input_path)

    # 配置输出
    stream = ffmpeg.output(
        stream,
        output_path,
        vcodec=vcodec,           # 使用检测到的编码器
        acodec='aac',            # 音频编码器
        video_bitrate='2M',      # 视频比特率
        audio_bitrate='192k',    # 音频比特率
        preset='medium',         # 编码预设(硬件编码器可能忽略此参数)
        **{'crf': '23'}          # 质量参数(硬件编码器可能忽略此参数)
    )

    # 执行编码
    ffmpeg.run(stream, overwrite_output=True)
    print(f"Video encoded successfully using {vcodec}")


# 使用硬件加速
encode_video_with_hwaccel('input.mp4', 'output.mp4', use_hwaccel=True)

# 强制使用软件编码
encode_video_with_hwaccel('input.mp4', 'output_sw.mp4', use_hwaccel=False)

不同平台的硬件加速检测

macOS

def detect_macos_hwaccel():
    """检测 macOS 硬件加速"""
    import platform

    if platform.system() != 'Darwin':
        return None

    # 检测芯片类型
    machine = platform.machine()
    is_apple_silicon = machine == 'arm64'

    # Apple Silicon 性能更好
    if is_apple_silicon:
        return {
            'platform': 'Apple Silicon',
            'recommended_encoder': 'h264_videotoolbox',
            'performance': 'excellent',
            'codecs': ['h264_videotoolbox', 'hevc_videotoolbox', 'prores_videotoolbox']
        }
    else:
        return {
            'platform': 'Intel Mac',
            'recommended_encoder': 'h264_videotoolbox',
            'performance': 'good',
            'codecs': ['h264_videotoolbox', 'hevc_videotoolbox']
        }

Windows

def detect_windows_hwaccel():
    """检测 Windows 硬件加速"""
    import platform

    if platform.system() != 'Windows':
        return None

    available = []

    # 检测 NVIDIA
    try:
        result = subprocess.run(
            ['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'],
            capture_output=True,
            text=True,
            timeout=5
        )
        if result.returncode == 0:
            available.append({
                'type': 'NVIDIA',
                'encoder': 'h264_nvenc',
                'gpu': result.stdout.strip()
            })
    except:
        pass

    # 检测 Intel QuickSync(通过 FFmpeg)
    vcodec, _ = get_hardware_encoder(True)
    if 'qsv' in vcodec:
        available.append({
            'type': 'Intel QuickSync',
            'encoder': 'h264_qsv'
        })

    # 检测 AMD
    if 'amf' in vcodec:
        available.append({
            'type': 'AMD',
            'encoder': 'h264_amf'
        })

    return available

Linux

def detect_linux_hwaccel():
    """检测 Linux 硬件加速"""
    import platform
    import os

    if platform.system() != 'Linux':
        return None

    available = []

    # 检测 VAAPI 设备
    vaapi_devices = [f'/dev/dri/renderD{i}' for i in range(128, 140)]
    for device in vaapi_devices:
        if os.path.exists(device):
            available.append({
                'type': 'VAAPI',
                'device': device,
                'encoder': 'h264_vaapi'
            })
            break

    # 检测 NVIDIA
    try:
        result = subprocess.run(
            ['nvidia-smi'],
            capture_output=True,
            timeout=5
        )
        if result.returncode == 0:
            available.append({
                'type': 'NVIDIA',
                'encoder': 'h264_nvenc'
            })
    except:
        pass

    return available

性能对比和最佳实践

编码速度对比(参考数据)

以编码一个 1080p 60fps 视频为例:

编码器 相对速度 CPU 占用 质量评分
libx264 (软件) 1x 100% 10/10
h264_videotoolbox (M1) 5-8x 20% 8/10
h264_nvenc (RTX 3080) 8-12x 15% 8.5/10
h264_qsv (12 代 Intel) 4-6x 25% 7.5/10
h264_amf (RX 6800) 6-10x 20% 7.5/10

最佳实践

  1. 自动检测并回退

    • 始终先尝试硬件加速
    • 检测失败时自动回退到软件编码
    • 记录日志便于调试
  2. 选择合适的预设

    # NVENC 预设
    # p1 (fastest) -> p7 (slowest, best quality)
    stream = ffmpeg.output(stream, 'output.mp4', vcodec='h264_nvenc', preset='p4')
    
  3. 考虑批量处理

    • 硬件编码器通常支持多路并行
    • NVENC 可以同时处理多个视频流
  4. 监控编码质量

    • 硬件编码质量可能不如软件编码
    • 对质量要求高的场景考虑使用软件编码
    • 可以用 VMAF 等指标评估质量
  5. 处理兼容性问题

    def safe_encode(input_path, output_path):
        """带错误处理的编码"""
        try:
            # 尝试硬件加速
            encode_video_with_hwaccel(input_path, output_path, use_hwaccel=True)
        except Exception as e:
            logger.warning(f"Hardware encoding failed: {e}")
            logger.info("Retrying with software encoding")
            # 回退到软件编码
            encode_video_with_hwaccel(input_path, output_path, use_hwaccel=False)
    

调试技巧

查看详细的 FFmpeg 输出

def encode_with_debug(input_path, output_path):
    """启用详细日志的编码"""
    vcodec, hw_options = get_hardware_encoder(True)

    stream = ffmpeg.input(input_path, **hw_options) if hw_options else ffmpeg.input(input_path)
    stream = ffmpeg.output(stream, output_path, vcodec=vcodec)

    # 获取完整命令
    cmd = ffmpeg.compile(stream, overwrite_output=True)
    print(f"FFmpeg command: {' '.join(cmd)}")

    # 执行并查看输出
    try:
        ffmpeg.run(stream, overwrite_output=True, capture_stdout=False, capture_stderr=False)
    except ffmpeg.Error as e:
        print(f"stdout: {e.stdout.decode()}")
        print(f"stderr: {e.stderr.decode()}")
        raise

检查硬件支持

# 查看所有硬件加速方式
ffmpeg -hwaccels

# 查看所有编码器
ffmpeg -encoders | grep -E "(nvenc|qsv|videotoolbox|amf|vaapi)"

# 测试特定编码器
ffmpeg -f lavfi -i testsrc=duration=1:size=1920x1080:rate=30 \
  -c:v h264_videotoolbox -f null -

总结

硬件加速是视频处理中的重要优化手段,可以大幅提升处理速度和降低系统负载。通过自动检测和回退机制,我们可以构建一个跨平台的健壮视频处理系统。

关键要点:

  • 优先使用硬件加速,但保留软件编码作为回退方案
  • 不同平台选择对应的最佳硬件加速方式
  • 通过实际测试验证编码器可用性
  • 根据场景在速度和质量之间取得平衡

参考资源

猫鱼周刊 vol. 082 AI 遗忘国耻

2025-10-12 20:26:36

关于本刊

这是猫鱼周刊的第 83 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

头条

好久不见。前两周分别因为单休和国庆假期,而且也没太多内容,所以咕咕了。

放假前写了一篇 Colf 题解,讲的是一个叫Colf的编程挑战,使用最少的 token 数让 AI 通过类似 leetcode 的编程题。跟 AI 磨合了这么久,看起来确实比较有效果(目前还是排在 39 名)。

最近也在做一个叫 dotmate的项目,是之前买的 Quote/0的控制器,官方的 App 虽然能显示非常多信息源,但是编排、数据源没有自己实现来得灵活,所以自己搓了一个。

从这期开始尝试一个排版上的变化。因为之前有微信公众号的读者反馈「项目」部分的 gh-card 会被转成图片,导致最后外链部分排版混乱,所以从这期开始,去掉 gh-card,只提供项目链接。

文章

让 Claude Code 更自主地运行

原文链接

假期前其实 AI 这块还挺多变动,除了 Claude Sonnet 4.5、DeepSeek-V3.2-Exp、GLM-4.6 扎堆发布之外,Claude Code 也来了一波更新。我觉得最亮眼的是两个,一个是原生的 vscode 插件,另一个就是 checkpoint。

首先说插件这块,这让 Claude Code 的体验真的更上一层楼,因为原来的交互就是开一个终端放在旁边,现在更加像 Cursor 自带的了。这里有个小插曲,插件刚上线的时候我发现了一个 bug,严重到我回退到命令行了,就是插件似乎监听了回车键,在中英文混输的时候按下回车就会发送消息,好在这周更新之后发现修复了。

checkpoint 也是一个我觉得很惊喜的功能。Cursor 中有类似的功能,这点很舒服,在你 vibe 了一大堆代码之后,如果觉得不满意,还能通过一种保险的方式回退到原来的代码(还有一种不保险的方式就是让 LLM 帮你恢复,取决于代码是否还在上下文里面,而且不一定能原样恢复)。这个功能之前我就调研过怎么让 Claude Code 也用上。网上有人做了一个 ccundo的项目,原理是去解析 Claude Code 的 session 文件,然后把状态保存下来。我自己倒是想到一个不一样的方法,通过 hooks触发一个工具,把变更保存下来。其实也有人提过 issue让官方实现这个功能,但是在几轮讨论之后就被冷落了,直到这次更新突然把这个功能端出来。

不得不感叹这个领域的变化真的很快,作为偏下游的用户,一些早些时间调研做不了或者很难的事情,说不定某一次模型的更新或者某个功能的实现就能解决了。

有 1M 上下文谁还需要 git 呢?

原文链接

这个就是我上面提到的「不保险」的办法。

事情是作者写了一份草稿代码,实现了一个比较好的效果,于是开始重构成生产级别的代码,过程中出了点问题,再也没法复现原来的效果,他也没有 commit 过代码,所以无从下手。最后他想到 gemini-2.5-pro 有 1M 的上下文,也许它还记得最初的代码,于是让它还原了。

这个方法能成功有两个前提,一个是上下文还保存着,另一个是 AI 真的能原样恢复。「原样恢复」这个我有点钻牛角尖,但是 git 它是可以的,但 LLM 本身就不能产出「确定性」的结果,所以不能跟 git 相提并论。好的编程习惯在什么时候都是有用的,如果能在实现一个完整功能的时候 commit 一次,就没有这个麻烦了。

Cursor 的 Plan Mode

原文链接

Cursor 推出了一个计划模式,它大概是这样:

当你指示 Agent 制定计划时,Cursor 会研究你的代码库以查找相关文件、审阅文档并提出澄清问题。当你对计划满意后,它会创建一个包含文件路径和代码引用的 Markdown 文件。你可以直接编辑该计划,包括添加或删除待办事项。

这其实跟我使用 Claude Code 的习惯非常像,我有一个 slash command 就是做这个事:

Read the demands in $ARGUMENTS and come up with a detailed implementation plan. You should:

1. Read the demands in $ARGUMENTS
2. Read relevant files in the codebase
3. Think hard about the best way to implement the demands
4. List the data structures, database tables you are about to add or modify(if any)
5. List API changes you are about to make(if any)
6. List every file you are about to add or modify, including what logics each file is about to implement

If you have any confusion, ask the user for clarification. DO NOT write any codes yet before given clear instruction to do so.

各种语言的拟人化漫画

原文链接

如题,版权问题就不放图了,原图大家点进去看看。

虽然有很多没接触过的语言,但是 C/C++ 那个真的太典了,完全是心目中会写 C 的人的形象。

后量子时代密码学

原文链接

接触到这篇文章是在我更新机器上的 openssh 之后,push 代码的时候出现了一个 warning:

** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html

简单来说,现在使用的密钥交换算法(ECDH,椭圆曲线 Diffie-Hellman)基于离散对数问题的困难性,传统计算机需要指数时间破解,但量子计算机可以在多项式时间内解决,把破解时间从几百万年降至几小时。但为什么要现在就开始防范呢?因为攻击者可以在现在就截获数据,然后等十到二十年后量子计算机成熟再解密(store now, decrypt later)。

而 OpenSSH 默认的交换算法 mlkem768x25519-sha256 是一种混合的方案,基于 ML-KEM-768 和传统的 X25519,足够对抗传统计算机和量子计算机的攻击。唯一的代价是密钥会更大而且计算开销更大一点,但对现代计算机没有显著的影响。

future-proof 真的是工程实践上一个非常有意思的事情。不仅是防范眼下可能的攻击,还要提前预知数十年后可能产生的威胁并提出对应的防范方案。

想法

AI 遗忘国耻

这个标题有点唬人,但是正好说明一个观点:过分清洗语料,过分强调「安全」会让 AI 严重降智,而且这也是各种「评测」体现不了的。

「华人与狗不得入内」是一个大家都熟知的事情,是当年上海租界一个种族歧视的标牌,算是「国耻」。但用这个问题去问 AI 会被拒绝回答:

目前只发现 DeepSeek 所有版本都拒绝回答,其他的一些国产 AI 例如 GLM、豆包、千问等都能正常回答,OpenAI、Anthropic 的模型也能正常回答,并未进一步测试更多模型。

所以在使用 AI 的时候,一定要自己做事实考证,不只是幻觉,伦理和安全的需求会让 AI 有意无意遗忘一些事情。

另一个是,种族歧视也好,不是信息也好,这都是我们人类的一部分,在训练时刻意去掉,只让 AI 看到一个乌托邦,是不是也会有问题?但让 AI 看遍人间邪恶,是不是也会有问题?这也是一个研究领域(AI Safety)。

项目

chorme-devtools-mcp

项目链接

DevTools 官方做的 MCP 工具,在这之前我用了很多个,有的还需要安装插件,而且也没办法获取 DevTools 里的信息,这个似乎是最好用的。

dotmate

项目链接

上面提到的给 Quote/0 写的控制器。我目前主要用这么三个视图:

  • 还有多久下班:显示当前的时间以及还有多久下班
  • 编程时间统计:基于 wakatime 的统计
  • Umami 统计信息:不用专门打开看了

效果大概是这样:


mole

项目链接

一个 Mac 的命令行垃圾清理工具,居然是用 shell 写的。不过由于是用 AI vibe 出来的,作者也建议如果数据非常重要,等更加成熟之后再使用。我建议是用 dry-run 先试试。

最后

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

Colf 题解

2025-09-29 22:08:26

Colf 是一个面向 AI prompt 的 code golf,大概就是利用最少的代码(在这里是最少的 token 数)来解题。

使用起来很简单,在左边的框中输入 prompt,生成代码,然后会自动运行是否 AC,如果 AC 则按照输入和输出 token 数计分。

小小尝试了一下,AC 完之后排名还挺前,所以写篇文章记录一下题解和心得。

题解

#1 LRU Cache

function lruops(
  ops: [op: string, key: number, value?: number][],
  capacity: number,
): number[]

code golf
function lruops(o,c){let m=new Map(),r=[];for(let [op,k,v]of o){if(op=="get"){if(m.has(k)){r.push(m.get(k)),m.delete(k),m.set(k,r[r.length-1])}else r.push(-1)}else{if(m.size==c)m.delete(m.keys().next().value);m.set(k,v)}}return r}
Input 33 tokens, Output 85 tokens, Total 118 tokens

上来先展示一下流氓解法:复制函数签名,加上 code golf。

#2 Topological Sort with Cycle Detection

function toposort(
  n: number,
  edges: [u: number, v: number][],
): number[]

Code golf choose the smallest index
function toposort(n, edges) {
    const g = Array.from({ length: n }, () => []), inDeg = Array(n).fill(0);
    edges.forEach(([u, v]) => (g[u].push(v), inDeg[v]++));
    const q = [], res = [];
    for (let i = 0; i < n; i++) if (!inDeg[i]) q.push(i);
    while (q.length) {
        const u = q.splice(q.indexOf(Math.min(...q)), 1)[0];
        res.push(u);
        for (const v of g[u]) {
            if (--inDeg[v] === 0) q.push(v);
        }
    }
    return res.length === n ? res : [];
}
Input 33 tokens, Output 159 tokens, Total 192 tokens

同样是流氓解法,但是有 case 过不了,再审题干,要选择更小的 index,所以补一句。

#3 Longest Common Subsequence

code golf
function lcs(a,b: string): string
function lcs(a,b){let dp=Array.from({length:a.length+1},()=>Array(b.length+1).fill(0));for(let i=1;i<=a.length;i++)for(let j=1;j<=b.length;j++)dp[i][j]=a[i-1]==b[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);let res='';for(let i=a.length,j=b.length;i&&j;){if(a[i-1]==b[j-1]){res=a[i-1]+res;i--;j--;}else dp[i][j-1]>dp[i-1][j]?j--:i--;}return res;}

Input 12 tokens, Output 157 tokens, Total 169 tokens

同样的流氓解法,唯一区别是从函数签名偷了一点。

#4 Expression Evaluator

calc expr
round code golf
const calc=(e)=>Math.round(eval(e));
Input 6 tokens, Output 10 tokens, Total 16 tokens

还是一样的解法,复制函数签名,但是 AC 不过;以为是优先级的问题,又把优先级的条件复制进去,还是不行;看一下发现需要取整(round),加上关键词,完事。如果强调优先级,它会生成一个很复杂的递归去解释的逻辑。

#5 Single-Source Shortest Paths

code golf

function dijkstra(
  n: number,
  edges: [u: number, v: number, w: number][],
  s: number,
): (number|null)[]
function dijkstra(n,e,s){let d=Array(n).fill(Infinity);d[s]=0;let q=new Set([s]);while(q.size){let u=[...q].reduce((a,b)=>d[a]<d[b]?a:b);q.delete(u);for(let[_,v,w]of e.filter(x=>x[0]==u)){if(d[u]+w<d[v]){d[v]=d[u]+w;q.add(v)}}}return d.map(x=>x===Infinity?null:x)}
Input 41 tokens, Output 108 tokens, Total 149 tokens

没啥特别,照样流氓解法。

#6 N-Queens

function nqueens(N: number):number
one-liner
const nqueens = N => (function solve(row, cols, diag1, diag2) { return row === N ? 1 : Array.from({ length: N }).reduce((count, _, col) => (cols[col] || diag1[row + col] || diag2[row - col + N - 1]) ? count : count + solve(row + 1, { ...cols, [col]: true }, { ...diag1, [row + col]: true }, { ...diag2, [row - col + N - 1]: true }), 0); })(0, {}, {}, {});
Input 13 tokens, Output 126 tokens, Total 139 tokens

启用了一个新的技巧 one-liner,这个词只占 2 个 token,跟 code golf 一样,效果差不多(主要是 code golf 在这里不 work)。

#7 Range Sum with Updates

function segment(
  nums: number[],
  ops: [op: string, a: number, b: number][],
): number[]

set i v updates a[i] = v.
sum l r returns inclusive sum from l to r.
code golf
function segment(nums, ops) {
  const a = [...nums];
  return ops.map(([op, x, y]) => {
    if (op === 'set') a[x] = y;
    else if (op === 'sum') return a.slice(x, y + 1).reduce((s, v) => s + v, 0);
  }).filter(v => v !== undefined);
}
Input 52 tokens, Output 84 tokens, Total 136 tokens

同样的技巧,唯一的缝补是 set 和 sum 的操作看起来不是很标准,塞进去之后就能 AC。

#8 JSON Diff

function jsondiff(
  a: unknown,
  b: unknown,
): string[]

Return a lexicographically sorted list of JSON paths where two JSON-serializable values differ. Paths use '.' for keys and '[i]' for array indices; the root is ''. always start with .

jsondiff({"x":1,"y":2}, {"x":1,"y":3}) // -> [".y"]
jsondiff({"k":[1,2]}, {"k":[1]}) // -> [".k[1]"]
function jsondiff(a, b) {
    const diffs = [];

    function compare(objA, objB, path) {
        if (typeof objA !== typeof objB) {
            diffs.push(path);
            return;
        }
        if (typeof objA === 'object' && objA !== null && objB !== null) {
            if (Array.isArray(objA) && Array.isArray(objB)) {
                const maxLength = Math.max(objA.length, objB.length);
                for (let i = 0; i < maxLength; i++) {
                    compare(objA[i], objB[i], `${path}[${i}]`);
                }
            } else {
                const keysA = Object.keys(objA);
                const keysB = Object.keys(objB);
                const allKeys = new Set([...keysA, ...keysB]);
                allKeys.forEach(key => {
                    compare(objA[key], objB[key], `${path}.${key}`);
                });
            }
        } else if (objA !== objB) {
            diffs.push(path);
        }
    }

    compare(a, b, '');
    return diffs.sort();
}
Input 108 tokens, Output 232 tokens, Total 340 tokens

答得最拉的一题,复制粘贴大法搞不定,而且总是卡那个数组下标不能 AC,需要手动改一下第二个例子,属于是 few-shot 的技巧。

#9 Sudoku Solver

function sudoku(grid: ('1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'.')[][]): ('1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9')[][]
function sudoku(grid) {
    const isValid = (row, col, num) => {
        for (let i = 0; i < 9; i++) {
            if (grid[row][i] === num || grid[i][col] === num || grid[Math.floor(row / 3) * 3 + Math.floor(i / 3)][Math.floor(col / 3) * 3 + i % 3] === num) {
                return false;
            }
        }
        return true;
    };

    const solve = () => {
        for (let row = 0; row < 9; row++) {
            for (let col = 0; col < 9; col++) {
                if (grid[row][col] === '.') {
                    for (let num = 1; num <= 9; num++) {
                        const strNum = num.toString();
                        if (isValid(row, col, strNum)) {
                            grid[row][col] = strNum;
                            if (solve()) {
                                return true;
                            }
                            grid[row][col] = '.';
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    };

    solve();
    return grid;
}
Input 63 tokens, Output 254 tokens, Total 317 tokens

这题也卡了很久,直接复制粘贴函数签名就能 AC,但是没有优化的思路。

#10 Regex Matcher

full matching code golf regex10(s,p)
function regex10(s,p){return new RegExp('^'+p+'$').test(s);}
Input 9 tokens, Output 18 tokens, Total 27 tokens

这题是我排名最前的,目前正好是第十。还是一样的套路,唯一难点是那个 full matching。

心得

先说解题的技巧。

基本盘就是复制粘贴函数签名,由于都是一些很常见的算法题,所以一般 LLM 都能一遍过;如果没过,通常是因为题干中有一些特殊的要求,这个时候回去读一遍题干,把这个特殊点找出来放进 prompt,例如 2、7、8、10 这几题。

然后就是怎么压榨输出的长度。因为打题用的 JavaScript,其实有挺多技巧/语法糖可以使用,例如箭头函数(arrow)、三元运算符(ternary)等等。这个在初期有比较大的进步,然后后来想到更加机智的一行流(one-liner),再按图索骥找到王炸 code golf

另外有个不太起眼的技巧就是,你需要探索出一种超短的实现方式,通常跟你一开始写的有天壤之比,例如 4 里使用的 eval()。你可以把题干复制出来,让别的 AI 去提供解法,然后再尝试把某些关键词放进 prompt 中引诱它生成出来。当然如果你很熟悉 JavaScript,本身就知道对应的语法糖或者算法的关键词,那必然更快能 AC。

最后,还有一点个位数的空间可以压榨。实测标点符号并不重要,函数的签名多个参数可以合并一下写,很显然的参数类型可以省略等等。这些就是已经在榜单上冲得很前想再扣出来一点的技巧了。

再说说打这个题让我对 AI 编程产生的思考。

第一还是那个「AI 决定下限,人决定上限」的理论。在 8、9 这两题我表现并不佳,因为我真的没有思路怎么解这个题,自然没办法给 AI 指出一个优化的方向。当然 AI 的能力也很强,通过一个函数签名最多试几次就能 AC,而且我估计网站背后只是用的一个本地的开源小参数模型。这说明 AI 已经能有竞争力地完成一些任务。

第二就是跟 AI 的磨合。从最开始的 GitHub Copilot 的自动补全,到 Cursor 和 Claude Code,我一直在积极使用 AI 编程,就好像跟伙伴慢慢磨合,知道它的能力能很有信心地做出什么,做什么比较悬,怎么去写 prompt 效果更好。例如我一上来就知道,复制粘贴函数签名会很关键,事实也证明这能很快地 AC 大部分题。这些东西真的很难形成书面的经验,更多时候是一种感觉。

第三是编程方式和效率的变化。就像「copilot」这个名字所说的一样,LLM 时代的编程不再是人手敲代码的「刀耕火种」,人更多地需要考虑「方向」而不是具体的实施,就算你把算法倒背如流,键盘敲冒烟,依然无法比肩 LLM 几十 token/s 的输出。另外就是,如果这个时代你还不能使用 AI 编程,那你的效率跟使用 AI 的同行完全没法比。当然这个说法有点夸张,就算你使用 AI,你的「上下文」上限仍然取决于你的脑容量,或者时间精力等等。

最后是「AI 友好的代码」。在这个题库中,多数的题都是非常基础和常见的算法,所以 AI 的准确率非常高。在工作中,更多地应该使用开源、知名而且「不那么新」(要在知识截止日期前)的库,开源库也可以使用 context7 等提供文档,尽量不要重复造轮子,更加不要再搞一大堆公司的私有库。另外最好重视文档的建设,更全更准确的上下文能让 AI 更好发挥。

猫鱼周刊 vol. 081 开源是地狱

2025-09-21 18:21:16

关于本刊

这是猫鱼周刊的第 82 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

头条

这周天气比较差,尤其是周末两天都在下雨,所以这周又没有出门拍照,这周又没有头图咯。

这周写了一篇 Ghostty 折腾小记,其实最大的收获是了解了一些字体相关的东西。另外字体这个东西真的非常个性化,我最喜欢的类型是等宽无衬线;而且看惯某一款字体后,大脑识别起来会更快一点,主观上就是看着「更舒服」。

文章

医学考古:旧约、不孕不育和曼德拉草

原文链接

从圣经的记录去研究亚伯拉罕家族的不孕不育案例。说实话整篇文章我看得一知半解,但是就好像吃瓜一样看完了,确实蛮有意思。

像宗教、传说这一类故事,深挖的话其实能找到一个合理的现代科学解释,但是更多地在民间归因为「神力」。我是非常科学理性的人,但是之前生病的时候我还是去拜了神,说实话我其实说不上信或者不信,但是更加有意义的是「拜神」这件事其实更多是「心理安慰」,在现实残酷或者前途未卜的时候,坦然的心态显然比慌张更能面对未来。

这个作者也很有意思,之前关注到 ta 是几篇技术文章,冷不丁来一篇医学 x 宗教/历史的文章,也是很强的反差。

个性化广告的隐私边界(上)

原文链接

讲解了隐私、安全、匿名的概念,并且从几个常见误区来解释。

我觉得有几点需要补充:

一是你在用的免费服务都在用你的「用户画像」赚钱,诸如 Alphabet(Google 母公司)和 Meta(Facebook 母公司)的主要收入来源都是广告业务。

第二是,虽然用户画像不等价于个人信息,但是国内服务的匿名化和保护通常做得不够好,尤其是一些小公司的服务,经常导致个人信息泄露,这不是无心之失,而是故意而为之:匿名化、安全等需要额外的研发和投入,博弈之下,出事之后冷处理或者鞠躬道歉甚至全网封杀的成本更低。

最后是国内公司滥用隐私早成习惯,这从当年百度李彦宏说「中国人更加开放,对隐私问题没有那么敏感,很多情况下他们愿意用隐私交换便利性,那我们就可以用数据做一些事情。」就能窥见其端倪。早年都是不问自取,立法之后,变成半哄半骗地让你同意,更过甚有拼多多利用 0day 漏洞窃取数据等等。

这几条连起来其实很明晰:所有公司都会在法律边缘试探,以获取最大的利益,你的隐私在里面只是别人的垫脚石。

更新博客 License

原文链接

关注的作者的一个疑惑:

我是否遵循了引用文章的 License,是否存在侵权。

在博客的很早期我其实考虑过这个问题,也考虑给自己的博客用什么 license,但是这件事渐渐就被淡忘了:没有人会愿意抄袭我这个名不经传的小博客,况且我的博客写的都是一些自娱自乐的内容,一般没什么经济价值。不过我一直没有淡忘的是正式的引用,例如周刊会在每篇文章都给出原文链接,文章会在末尾给出 Reference 等。

这件事提醒了我,审视自己的博客,我最可能的 violation 是分享了一些有 copyright 的内容,但是确实「内容较多」已经无法一一再考究;更多的情况下,大家都没有标明版权,或者采用 CC-BY 等,我目前的做法应该是符合规范的。作为保底,如果你对某些内容有争议,可以联系我解决。

想法

反作弊与隐私

最近玩三角洲比较多,每次启动的时候都有一个弹框引导我安装开机自启的所谓「守护程序」,实现反作弊功能。开喷之前我先叠盾,不是说反作弊这件事情没有意义,只是厂商借着「反作弊」的名号越过了一个边界。

我觉得这产生了很多问题:只有我有正当理由(反作弊),我就可以光明正大抄你的家(扫盘)吗?如果我没有同意扫盘,但是扫出了作弊的证据,是否可以作为封禁的理由?更甚,如果在扫盘过程中,发现「违法证据」(例如盗版影视资源)是否也可以对用户作出行动?另外,扫盘造成的磁盘磨损、损坏,是否可以认定为损害用户的财产或者是入侵计算机信息系统?当然了,这些问题早被相应的法务包得圆圆的,估计在启动游戏的时候你就默认同意了这些不平等条款,怎么可能告得赢南山必胜客呢。

这就是我所说的边界,从技术上说,只要扫盘这个行为一旦发生,想要实现上面的功能轻而易举。也从技术上说,用扫盘这个手段来反作弊算是技术能力很差的保底手段,以及对游戏环境本身的嘲讽。游戏机制、玩家引导、更好的反作弊技术等等都是不错的努力方向,偏偏要出个下策。

开源是地狱

上期好好说话的后续。太讽刺了,这个人在国内社群拉帮结派,然后去 Discord 上喋喋不休追讨。最后人家取了这个截图,配上一句极为讽刺的话「那边的人心态就是这样」。

另外吃瓜了微信聊天和 Discord 聊天之后,发现一个很有意思的点:微信的聊天争论的点在 「HAS」的咬文嚼字和作者到底有没有遵守 GPLv3(虽然很大程度上是当事人引导的结果),而 Discord 的吃瓜群众则是好言相劝冷静一下好好说话。

我只能说很讽刺,也许在国内开源确实就是地狱,社区就是地狱,大家都没法好好说话,通过在互联网上满嘴喷粪去释放自己的负面情绪。

项目

obsidian-velocity

Gonzalo-D-Sales/obsidian-velocity - GitHub

项目链接

适配了新的 Liquid Glass 风格的 Obsidian 主题,很简洁优雅,已经换上了。

霞鹜文楷

lxgw/LxgwWenKai - GitHub

原文链接

一款开源的中文字体。之前在折腾终端字体的时候找到的,其实这个中文字体在阅读场景不错,用在博客或者电纸书上应该很舒服。

工具/网站

MCP Registry

网站链接

(终于又给这个名存实亡的板块写点东西。)

GitHub 出的 MCP Registry,其实没啥特别,就是准入门槛比其他 registry 更高,但是 install 功能就只能安装到 vscode,略显鸡肋。

最后

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

Ghostty 折腾小记

2025-09-17 21:22:15

刚入手 Macbook 的时候用的 Terminal.app,后来用 iTerm2,同时用 Termius 来连接服务器,再到后来用 Tabby + tssh 大一统,取代了 Termius。前两天心血来潮又换成了 Ghostty,说实话 Tabby 没什么不好,唯一的缺点就是不是原生。

Ghostty 号称「零配置」(Zero Configuration Philosophy),确实开箱的体验也足够好了。但是我有看惯了的配色和字体,所以这些还要折腾一下。先贴一下我的配置,以及效果,方便你抄作业。

theme = Catppuccin Mocha
font-family = Hack
font-family = Noto Sans SC
font-thicken = true
font-size = 12
window-inherit-font-size = false
window-padding-x = 10
window-padding-y = 10
shell-integration-features = no-cursor
cursor-style = block
cursor-style-blink = true
window-save-state = never
term = xterm-256color

它的配置采用的是一种宽松的类 ini 的格式,这点好评,不会一不小心就损坏配置没法打开。

主要修改了这些配置项:

  • theme 用了平常就在用的 Catppuccin Mocha
  • window-padding-x, window-padding-y 不喜欢元素贴着边边,有种拥挤的感觉
  • window-save-state 设成 never 每次重新打开都会变成默认大小,强迫症福音
  • term 这个选项控制 $TERM,它默认会把自己宣称 xterm-ghostty ,会导致有些 CLI 报兼容问题,所以改成 xterm-256color

比较值得一提的是字体的搭配。它支持设置多个 font-family 来决定字形的回落。

我之前一直使用的终端字体是 MesloLGS NF,编程字体是 Hack。我对字体的偏好其实就是等宽、无衬线。趁着折腾我又尝试了一些其他的组合:

  • Iosevka(Iosevka Term / Iosevka Term Extended):一款比较好评的终端字体,其中不带 Extended 的版本极窄,可以在更窄的屏幕内展示更多内容。但是实在窄得要命,接受不了。
  • Sarasa Gothic / 更纱黑体 (Sarasa Term SC):合成字体,是上面的 Iosevka + Source Han Sans 作为 CJK 增补合成的字体。其实用 Iosevka Extended + 这个还行,只是没有太好看。特点是中英文 2:1 等距。
  • JetBrains Maple Mono:也是一款合成字体,JetBrains Mono + Maple Mono。也是中英文 2:1 等距的。
  • Noto Sans:一款 CJK 字体,不怎么特别,但是在终端里比 PingFang SC 稍微耐看一点。
  • Fira Code:顾名思义一款为代码而生的字体,连字设计比较有特色。

有印象尝试过的自己大概就这些。折腾到最后,我使用了 Hack + Noto Sans 的组合。因为主要是英文字体用得居多,加上 vscode 里一直就是 Hack,所以看得顺眼的留下了。我发现在看惯某一款字体后,大脑识别起来会更快一点,过拟合不是虚的。

Anyway,趁这次折腾也对字体稍微有了一点了解,例如风格、等宽、连字、衬线、字形、字重等等,也许后面有机会再写一篇文章来讲这个。