nt_helper.node(5 个平台)和加密后的装扮资源 resources/dress/*.dat 不再入库。
它们由私有 native 仓 H3CoF6/nt_helper 构建后发布到公开仓
H3CoF6/nt_helper_release,WeQ 这边按需取回。
这两类文件每次 native 构建都会变:.node 是 5 个平台各十几 MB 的 Rust cdylib,
*.dat 是重新 AES 加密过的几十 MB 的包。历史上每次构建都往主仓 PR 一次,
把 .git 顶到 538 MB —— 其中 1204 MB(未压缩内容)是 native/**、222 MB 是 dress/*.dat。
清洗后仓库降到 61 MB(见文末清洗记录)。
| 路径 | 是否入库 | 来源 | 更新方式 |
|---|---|---|---|
native/<platform>/<arch>/nt_helper.node |
否 | H3CoF6/nt_helper 的 Rust 构建 |
pnpm native:fetch |
resources/dress/{bubble,widget,font}.dat |
否 | 同上(build.rs 打包 + AES 加密) |
pnpm native:fetch |
native/<platform>/<arch>/ninebird/* |
是 | H3CoF6/nt_helper 的 NineBird 构建 |
手工同步(另一条链,未自动化) |
resources/dress/screen/**、ranking-*.json |
是 | 手工整理的静态素材 | 正常提交 |
resources/dress/*_resources |
否 | scripts/fetch_dress_resources.ts 抓取 |
本地生成,喂给 native 构建 |
.node与.dat必须同源。.dat的 AES key 由 nt_helper 的构建 commit 派生 (DRESS_KEY_SEED,见 nt_helper 的build.rs)并烘焙进.node。跨版本混用会让queryDressResourceUrl()永远返回null—— 装扮离线查询静默退化成联网协议。 所以pnpm native:fetch永远从同一个 release tag 成对取。.node有有效期。 CI 构建带BUILD_TIMESTAMP,getInitStatus()在过期后返回-1;本地cargo/napi自编的不带时间戳,不受此限。有效期长度只由 nt_helper 的 Rust 定义一处(common::BUILD_VALIDITY_SECS),二进制通过getBuildInfo()自报buildTime/expireTime;WeQ 侧(脚本、CI、UI)一律按这个时间戳判断,不抄天数。 所以发布时间到了就得换一份新的,而--require-fresh(剩余不足 1/5 有效期)会把过期 构建挡在打包之前。
pnpm native:fetch 默认取的是 native/pinned.json 里锚定的那个 tag,不是 latest。
为什么:WeQ 的代码是照着某一份 native 构建写的(类型、能力、甚至装扮资源的 AES 密钥都由 它决定)。一直跟着 latest 漂,会在"WeQ 已经用到新字段、而本地那份还是旧的"这类情况下 静默降级 —— 比如 native 新增了分块扫描的键探针,旧构建不认识它,只会退化成慢路径。 锚文件是这个仓库里唯一一处"我们认哪一份 native"的记录,改动它是一次显式提交。
{ "tag": "nt-helper-20260918-5c4d8b2", "commit": "5c4d8b2…", "pinnedAt": "2026-09-19", "note": "…" }只有 tag 有校验意义(校验靠那个 tag 的 manifest.json 里的 sha256);commit / note
是给人看的。升级锚点的完整流程:
node scripts/fetch-native.mjs --latest --verify # 1. 试用新构建:装完当场 require + getInitStatus()
# (跑测试确认没问题)
node scripts/fetch-native.mjs --latest --write-pin # 2. 把这次取到的 tag 写回 pinned.json
# 3. 提交 pinned.json(外加 dist 里需要跟着走的 native/** 产物)pnpm native:check 发现本地与锚点不一致(缺失 / 落后 / 内容被换过 / 已过期)就退出码 1,
并会提醒上游有没有更新的构建。手动丢一份自编 .node 进去也躲不过:sha256 对不上。
pnpm native:fetch # 本机平台 + 装扮资源(锚定的 tag)
pnpm native:check # 只比对:缺失 / 落后 / 被改 / 已过期 → 退出码 1
pnpm native:fetch --all # 五个平台全量(全平台测试、做镜像时用)
pnpm native:fetch --platform linux-arm64
pnpm native:fetch --latest # 忽略锚点,取上游最新(试用新构建用)
pnpm native:fetch --dress-only # 只补装扮资源
pnpm native:fetch --verify # 装完 require 一次并跑 getInitStatus()
pnpm native:fetch --version nt-helper-20260916-86cb5a4
pnpm native:fetch --from-dir ./dist # 从本地目录装(离线 / 镜像;目录里要有 manifest.json)| 环境变量 | 作用 |
|---|---|
NT_HELPER_RELEASE_REPO |
覆盖发布仓,默认 H3CoF6/nt_helper_release |
NT_HELPER_RELEASE_BASE_URL |
覆盖下载前缀(镜像 / 代理),默认 GitHub Releases |
NT_HELPER_VERSION |
钉死 tag,等价于 --version(CI 里临时试构建用) |
HTTPS_PROXY / HTTP_PROXY |
出网代理(另支持 NO_PROXY)。脚本自己读这几个变量走 CONNECT 隧道 —— Node 内置 fetch 不认它们,Clash 之类只开 HTTP 代理又没开 TUN 时直连 GitHub 会超时 |
拉完之后 native/.installed.json 记着当前装的是哪个 tag、哪些文件、各自 sha256;
它是机器相关状态,已被 gitignore(入库的那个是 native/pinned.json,见上一节)。
装扮资源缺失不会让程序崩:queryDressResourceUrl() 返回 null,调用方退化成在线协议。
纯前端 / 服务端开发不跑 native:fetch 也能干活,只是拿不到装扮离线数据。
H3CoF6/nt_helper (私有) H3CoF6/nt_helper_release (公开)
Rust-Release-Build (workflow_dispatch)
build × 5 平台 ─────────────────────► GitHub Release: nt-helper-<yyyymmdd>-<sha7>
nt_helper.node × 5 ├ nt_helper-win32-x64.node
resources/dress/*.dat ├ nt_helper-linux-x64.node
publish-release job ├ nt_helper-linux-arm64.node
(stage → manifest → Release) ├ nt_helper-darwin-x64.node
├ nt_helper-darwin-arm64.node
├ dress-{bubble,widget,font}.dat
├ manifest.json
└ SHA256SUMS
main 分支: README.md + latest.json
↑ 与 Release 里的 manifest 同源
稳定入口(不需要 API、不需要 token):
https://github.com/H3CoF6/nt_helper_release/releases/latest/download/manifest.json
https://github.com/H3CoF6/nt_helper_release/releases/download/<tag>/<asset>
manifest.json 结构:
{
"tag": "nt-helper-20260916-86cb5a4",
"commit": "<nt_helper commit sha>",
"builtAt": "2026-09-16T05:12:33Z",
"runId": "1234567890",
"source": "H3CoF6/nt_helper@<sha>",
"dressKeySeed": "<sha>",
"platforms": {
"linux-x64": { "asset": "nt_helper-linux-x64.node", "sha256": "…", "size": 13924216 }
},
"dress": { "bubble": { "asset": "dress-bubble.dat", "sha256": "…", "size": 24530053 } }
}发布需要 GitHub App 在 nt_helper_release 上有 contents: write(安装时把这个仓库勾上)。
发布仓里的提交由 App 自己的 bot 身份署名(ninebird-bot[bot],workflow 里用安装令牌调
/user 动态取,不要写死用户名 —— 否则会撞上同名的真实 GitHub 账号)。
.github/workflows/release.yml 的 6 个打包 job(windows / linux-x64 / linux-arm64 /
macos-x64 / macos-arm64 / web)在 pnpm install 之后各插了一步:
- name: Fetch nt_helper native artifacts
run: node scripts/fetch-native.mjs --require-fresh --verify每个 runner 只取自己平台那一份 .node + 三个 .dat(按 native/pinned.json 锚定的 tag,
不是 latest)。--require-fresh 在剩余有效期不足 1/5 时失败 —— 也就是说锚点太旧时,发布会在
打包之前就失败,而不是发出一个起不来的安装包;--verify 在 runner 上真的 require() 一次
并跑 getInitStatus(),二进制有问题当场失败,而不是等用户装上才发现。
pnpm native:fetch --write-pin 会把二进制自报的 builtAt / expiresAt 一起写进
native/pinned.json;WeQ 的 Release 说明就取这两个值(不再单独跑构建去问)。
需要跑最新的 native 构建前,先把锚点升级(见上一节),否则 CI 打出来的仍是旧的那份。
| 指标 | 清洗前 | 清洗后 |
|---|---|---|
本地 .git |
538 MB(pack 534.49 MiB) | 63 MB(pack 62.20 MiB) |
| 从 GitHub 全新 clone | — | 65 MB / 6 秒 |
| commit 数 | 777 | 777(除下述 8 个文件外逐字节不变) |
全历史 nt_helper.node / dress/*.dat |
108 / 24 个 blob | 0 / 0 |
GitHub 页面上的 "Repository size" 是异步重算的,强推之后一段时间里仍会显示旧值 (当时看到的就是 539 MB 没变)—— 以
git clone的实际体积为准。
清洗范围(代码与素材的当前版本全部保留):
native/**/nt_helper.node,含历史里两种旧命名native/darwin/arm|intel/;resources/dress/{bubble,widget,font}.dat;resources/emoji/<数字>/**与resources/emoji.zip(早已不在追踪的历史垃圾;market.csv/emoji_config.json保留);native/**/ninebird/*不动 —— 当前版本继续入库,历史也一并保留。
最终 HEAD 树相对清洗前只少 8 个文件(5 个 nt_helper.node + 3 个 .dat),其余提交
逐字节不变;47 个 tag(含只在远端存在的 v1.0.0)全部重指到重写后的等价提交。
# 0) 留档:重写会把二进制从工作区一起删掉;另外先在别处镜像一份远端作为回退点
mkdir -p /tmp/weq-native-keep
cp -a native /tmp/weq-native-keep/native
cp -a resources/dress/bubble.dat resources/dress/widget.dat resources/dress/font.dat /tmp/weq-native-keep/
git clone --mirror https://github.com/H3CoF6/WeQ /tmp/weq-remote-backup.git
# 1) 重写(filter-branch,777 commits 实测 34 秒;没装 filter-repo 也够用)
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force \
--index-filter "git rm -r --cached --ignore-unmatch -q ':(glob)native/**/nt_helper.node' \
resources/dress/bubble.dat resources/dress/widget.dat resources/dress/font.dat \
resources/emoji.zip 'resources/emoji/[0-9]*'" \
--tag-name-filter cat -- --all
rm -rf .git/refs/original && git reflog expire --expire=now --all
# 2) 放回留档的二进制(此时它们已是 ignored 的本地文件),然后重打包
cp -a /tmp/weq-native-keep/native/. native/
cp -a /tmp/weq-native-keep/bubble.dat /tmp/weq-native-keep/widget.dat \
/tmp/weq-native-keep/font.dat resources/dress/
rm -f .git/ORIG_HEAD .git/FETCH_HEAD
git repack -adf --window=250 --depth=50 && git gc --prune=now
git count-objects -vH # 期望 62 MB 上下
# 3) 强推(分支 + tag)
git push --force origin main dev
git push --force origin --tags注意 pathspec 用的是 :(glob)native/**/nt_helper.node:默认 pathspec 的 * 会跨 /,
不用 glob magic 容易误伤同名前缀的其它文件。
验证(在全新 clone 里做,才是别人实际拿到的结果):
git clone https://github.com/H3CoF6/WeQ /tmp/weq-fresh && cd /tmp/weq-fresh
du -sh .git # 65 MB
git rev-list --count --all # 768(本地另有 9 个私有分支的提交)
git tag | wc -l # 47
git rev-list --objects --all | grep -c nt_helper.node # 0
git rev-list --objects --all | grep -c 'resources/dress/.*\.dat' # 0
git fsck
node scripts/fetch-native.mjs --verify # 取最新产物并当场跑 getInitStatus()- 对象被"看不见的地方"吊住,
gc之后.git只瘦了一半:.git/ORIG_HEAD/.git/FETCH_HEAD里还记着重写前的 tip;- 其它 worktree 的 index(
.git/worktrees/<name>/index)里缓存着旧 tree —— 本仓库有个挂在dev2上的WeQ2worktree,它的 index 里那 8 个文件还是 staged 状态。 这些都会让 git 认为旧的大 blob 仍然可达(repack -a会原样带上)。清掉这两个文件、 并把对应 worktree 的 index 刷新(git -C <worktree> reset)之后,再repack -adf才能回到 62 MB。
- 远端独有的 tag:
v1.0.0只存在于 GitHub(本地没有),强推 tag 不会动它,它会 继续指向旧提交、把整条旧历史留在服务端。要按「author/committer 时间戳 + 提交信息」 找到等价提交再git tag -f重指。 - PR refs:GitHub 的
refs/pull/*不受强推影响(当时 71 个),旧对象还会被它们 引用着。想让 GitHub 侧的体积真正降下来,得等它自己 GC,必要时找 Support 清 dangling。 - 老 clone 只
git fetch不会回收本地 pack,要么重新 clone,要么git fetch --prune --tags --force && git reflog expire --expire=now --all && git gc --prune=now。
- 重写改掉了所有 commit sha,47 个 tag 全部强推。GitHub Release 的 tag 名不变、 资产与下载链接照旧,但 Release 指向的 commit 变了。
- fork 与已关闭 PR 里的副本清不掉;已开的 PR 会失效(动手前确认
gh pr list为空)。 - 本地
dev2/docs/history/feat/qzone/tmp等私有分支和 stash 会被一起重写: 废弃的先删,要留的(stash)先导成 patch。 LICENSE一个字节都别改:它的 sha256 烘在.node里,改了所有构建都会报 「组件已损坏或被篡改」。