📄articles 📋snippets 📁categories ⚙️uses ✏️about 🔍search

把 1000 行代码变成开源项目:git 历史清理、单实例保护和“多少行值得开源”

系列最后一篇。前四篇讲了权限模型下载产品减法UI 实现——那些都是“把它做出来”。这篇讲怎么把它开源出去,两个状态的差距比我想的大。

从“自己能用”到“能开源”,补的东西里没有一行是功能代码——全是历史上、文档上、工程上的洞。

四关的全貌先摆在这儿:

开源发布前四关:从自己能用到能开源的完整清单

第一关:421MB 的 git 历史,和它教我的两课

准备开源时做的第一件事:看看仓库多大了。

du -sh .git
# 421M

一个 1000 行的项目,历史 421MB——因为开发过程中有两样大东西进过 git:模型文件(380MB)和虚拟环境。虽然早就写了 .gitignore,但 git 的规则是“进去过就算数”:.gitignore 只管未来,不管历史。

清理用 git-filter-repo(官方推荐替代已废弃的 filter-branch):

brew install git-filter-repo   # 或 pip install git-filter-repo

# 动手前先备份历史(重写不可逆)
cp -r .git /tmp/talk2clip_git_backup_$(date +%s)

# 移除大文件与个人配置的历史
git-filter-repo --force \
  --path models/ --path config.json --path .talk2clip.pid --invert-paths

421MB → 66MB。然后我顺手 git ls-files 看了一眼当前跟踪的文件,发现另一个尴尬:

.venv/bin/activate
.venv/bin/ct2-transformers-converter
...

.venv 也被跟踪着。原因值得记一笔:我后来更新了 .gitignore 排除 .venv/,但之前有一个提交已经把它加进去过。第二次 filter-repo:

git rm -r --cached .venv        # 从当前索引移除
git-filter-repo --force --path .venv/ --invert-paths   # 再清历史

最终 .git 从 421MB 到 288KB。三步实录(含每一步的体积变化)都在这张图里:

git 历史清理:421M 到 288K 的终端实录(开源发布前必做)

两课总结:

  1. .gitignore 只管未来:敏感文件/大文件一旦提交过,必须清历史
  2. 清完历史要重新核对 git ls-files:你以为忽略的东西,可能更早的提交里躺着

(现在仓库带着 icon、截图、demo.gif 这些“真该进库”的资产,也就 988KB。)

第二关:单实例保护——一个 pgrep 坑引出的双实例事故

发布前修的最“脏”的 bug 出现在进程管理上。

talk2clip 的核心场景是常驻后台(监听热键),所以必须防“启动两份”。第一版用的是最直觉的方案:

subprocess.run(["pgrep", "-f", "talk2clip.py"])

看起来没问题。实际有连环炸:

炸点一:开机自启是通过 AppleScript 开一个隐藏终端执行的,命令是这个——

cd ~/Desktop/talk2clip && ./talk2clip.py

pgrep -f 匹配的是整个命令行,而父 shell 的命令行里就含 “talk2clip.py” 这几个字。于是新启动的进程总能看到“已有一个实例在跑”(其实是它爹),直接自杀。加了这段检测之后,开机自启就再没起来过,而我一直以为它在工作

炸点二:有一次我手动双击了启动器,恰逢另一份在后台跑——两个实例同时监听右 ⌘。按一次热键,识别结果被粘贴了两遍。一开始我怀疑是粘贴逻辑,查了半天才反应过来是双实例。

修法是 pidfile + 存活检测,不再猜命令行:

def _already_running():
    try:
        with open(_PID_FILE, encoding="utf-8") as f:
            old_pid = int(f.read().strip())
        if old_pid != os.getpid() and _pid_alive(old_pid):
            return True
    except Exception:
        pass
    return False

def _pid_alive(pid):
    try:
        os.kill(pid, 0)     # 信号 0:只探测存在,不打扰
        return True
    except (OSError, ProcessLookupError):
        return False

启动时写入自己的 pid,退出时(./run.sh quit 子命令)清理。“进程是否在跑”应该问系统(os.kill),而不是猜字符串pgrep -f 匹配子串这个特性,在别处是功能,在自匹配场景是坑。

第三关:README 该写给谁

发布前我把 README 整个重写了一遍,最大的决策是语言顺序

原来的 README 是英文的(GitHub 默认显示 README.md)。想清楚受众之后换成了中文为首

  • 项目解决的是中文场景(识别是中文语音、繁体转简体、国内网络下载模型)
  • 系列文章的读者、真正的目标用户是中文开发者
  • README 里大量内容在讲“国内怎么下模型”,英文用户看着也迷惑

做法:README.md(中文,GitHub 默认页)+ README.en.md(英文版),两版互链。这不损失国际受众,又让目标用户第一眼看到熟悉的语言。

然后是。纯文字的 README 对“这个工具长什么样”完全没有说服力,加了三个:

  • demo.gif:按住说话 → 波形跳动 → 文字落光标,3 秒演示(顶部第一屏)
  • 设置界面截图,回答“怎么配置”
  • 架构图,回答“它怎么工作”

README 里每一节都该能回答一个具体问题:这是什么/怎么装/怎么用/坏了怎么办。

第四关:让“克隆下来能跑”

“在我机器上能跑”和“别人克隆下来能跑”之间的差距,大概需要补这些文件(一个都不能少):

文件 解决什么
install.sh 一键:建 .venv → 装依赖 → 从模板生成 config.json
requirements.txt 依赖清单(别让人从代码里 import 反推)
config.example.json 配置模板:真实的 config.json 里有个人设置,不该进库
.gitignore 排除 .venv/ config.json __pycache__/ .DS_Store
LICENSE 没有 license = 别人法律上不能用你的代码(选了 MIT)
make_app.sh 构建可双击的 .app。不然 README 里写的“双击 app”对别人是空头支票

最后一个特别值得说:make_app.sh 是发布完 v1.0.0 之后才补的。因为我在 README 里写了“双击 talk2clip.app 启动”,但仓库里既没有 app 也没有构建方法:对我是“我机器上有”,对读者是“哪来的 app?”

“多少行值得开源”——问错了问题

做完上面这些,v1.0.0 发布。回头看那个纠结最久的问题:“这个项目才 1000 行(核心 454 行),值得开源吗?”

我的实际答案:这个问题本身问错了。值得开源的门槛不是行数,是这两条:

  1. 它完整地解决了一个具体问题——talk2clip 解决“Mac 上按住说话出文字”,从热键到离线识别到自动粘贴,链路是闭环的,不是 demo;
  2. 过程中有别人能用的经验——这个项目的价值有很多在代码外:macOS 15 权限的坑、国内下模型的路、NSTimer 在 rumps 里不触发……这些教训写进 README 和文章里,比代码本身值钱。

反过来说,我上一版 10+ 文件、功能齐全的语音助手没有开源,也不打算:它功能多但每条链路都依赖外部环境(API 密钥、云服务、CLI 工具),别人拿到手跑不起来,维护成本我自己都扛不住。

行数呢?1000 行的项目,一个下午能读完,反而是一种亲和力——“这么小我也能看懂”。

发布之后

v1.0.0 之后很快跟了个 v1.0.1,全是工程完善(构建脚本、设置界面修复、文档配图),核心功能没动。GitHub Release 的 changelog 就按“新增/修复/文档”分类写,读者(和我自己)未来都能对上账。

系列到此收束:权限、模型、产品、UI、开源,五篇。项目还在跑,以后有新东西,继续写。


项目地址:github.com/Charlielyo/talk2clip(MIT,v1.0.1)。系列四篇前文:权限排查 · 模型下载 · 产品减法 · PyObjC 动画

© 2026 把 1000 行代码变成开源项目:git 历史清理、单实例保护和“多少行值得开源” · 本文由 Charlie 原创撰写,发布于 sodebug.com。 未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。
GK
独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

related相关文章

#01Nginx FastCGI Cache 缓存清除完整指南:为什么改了首页却看不到更新3 min#02[问题记录]Unity引擎报错:Assertion failed on expression、Asset database transaction committed twice!1 min#03TSDF算法原理及源码解析1 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
$ subscribe →