review: apply external consultant editorial fixes and update audit tracker to 74/91 approved
Deploy / build (push) Successful in 28s
Deploy / build (push) Successful in 28s
This commit is contained in:
1 parent
4e64bfc9ae
commit
a7e683100b
56 files changed
+14496
-23169
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# 全站文章顾问审阅状态总控台 (Audit Tracker)
|
||||
|
||||
> **最后同步时间**:2026-10-27 04:23:31
|
||||
> **全站总文章数**:91 篇 | **已审批完成**:39 篇 (42.9%) | **待审阅**:52 篇
|
||||
> **最后同步时间**:2026-10-27 05:26:50
|
||||
> **全站总文章数**:91 篇 | **已审批完成**:74 篇 (81.3%) | **待审阅**:17 篇
|
||||
|
||||
## 一、操作命令速查
|
||||
- `python scripts/manage_review.py list` — 查看待审与已审概览
|
||||
@@ -21,14 +21,14 @@
|
||||
| 7 | `ai` | `running-tts-as-a-service-with-token-sidecars` | 我用两个 Cron 容器跑了一年的语音合成服务 | ✅ 已通过 | Batch-01 | 2026-10-27 03:00 | 外部顾问审阅完成,已采纳并落地全量修改意见 |
|
||||
| 8 | `ai` | `slashing-agent-context-with-a-code-graph` | 别再把整个 Repo 喂给 Agent:用 Code Graph 斩断 Token 膨胀与遗忘陷阱 | ✅ 已通过 | Batch-01 | 2026-10-27 03:00 | 外部顾问审阅完成,已采纳并落地全量修改意见 |
|
||||
| 9 | `ai` | `using-chinese-llm-apis-from-malaysia` | 从马来西亚使用中国大模型 API:真正可行的方法 | ✅ 已通过 | Batch-01 | 2026-10-27 03:00 | 外部顾问审阅完成,已采纳并落地全量修改意见 |
|
||||
| 10 | `ai` | `why-i-still-bought-a-local-gpu` | Claude 和 GPT 明明更强,我为什么还是买了 GPU 跑本地 AI | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 11 | `ai` | `yue2-on-a-12gb-gpu-songs-that-lost-their-endings` | 为什么我的 AI 歌曲总是丢结尾:在 12GB 显卡上跑 YuE2 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 12 | `case-studies` | `ai-furniture-compositing-with-flux-kontext` | 我如何用 FLUX.1 Kontext 打造 AI 家具合成演示 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 13 | `case-studies` | `from-audio-to-a-4-minute-ai-music-video` | 从一首歌到 4 分钟完整 MV:我如何用 Gemini、AI 生成与 Filmora 剪辑串起全流程实操 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 14 | `case-studies` | `how-i-built-an-automated-music-video-pipeline` | 我是如何用 Remotion 与 Docker CDP 打造全自动 MV 生成与多语种油管发布流的 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 15 | `case-studies` | `how-i-built-the-digikedai-telegram-bot` | 我是如何构建 DigiKedai Telegram AI 客服机器人的 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 16 | `case-studies` | `how-i-host-this-blog` | 我是如何托管这个博客的:Astro、Gitea Actions 与自托管 CI/CD | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 17 | `case-studies` | `how-i-made-my-own-songs-with-suno-ai` | 我用 Suno AI 做出自己的歌 | ⏳ 待审阅 | Batch-01 | - | |
|
||||
| 10 | `ai` | `why-i-still-bought-a-local-gpu` | Claude 和 GPT 明明更强,我为什么还是买了 GPU 跑本地 AI | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 11 | `ai` | `yue2-on-a-12gb-gpu-songs-that-lost-their-endings` | 为什么我的 AI 歌曲总是丢结尾:在 12GB 显卡上跑 YuE2 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 12 | `case-studies` | `ai-furniture-compositing-with-flux-kontext` | 我如何用 FLUX.1 Kontext 打造 AI 家具合成演示 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 13 | `case-studies` | `from-audio-to-a-4-minute-ai-music-video` | 从一首歌到 4 分钟完整 MV:我如何用 Gemini、AI 生成与 Filmora 剪辑串起全流程实操 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 14 | `case-studies` | `how-i-built-an-automated-music-video-pipeline` | 我是如何用 Remotion 与 Docker CDP 打造全自动 MV 生成与多语种油管发布流的 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 15 | `case-studies` | `how-i-built-the-digikedai-telegram-bot` | 我是如何构建 DigiKedai Telegram AI 客服机器人的 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 16 | `case-studies` | `how-i-host-this-blog` | 我是如何托管这个博客的:Astro、Gitea Actions 与自托管 CI/CD | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 17 | `case-studies` | `how-i-made-my-own-songs-with-suno-ai` | 我用 Suno AI 做出自己的歌 | ✅ 已通过 | Batch-01 | 2026-10-27 05:26 | 外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换) |
|
||||
| 18 | `case-studies` | `how-i-vetted-20-vps-providers-with-parallel-subagents` | 如何用并行子代理在三小时内核查 20 家 VPS 服务商 | ✅ 已通过 | Batch-02 | 2026-10-27 04:22 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/架构蓝图对齐/排版折叠) |
|
||||
| 19 | `case-studies` | `loop-engineering-without-a-coding-agent` | 没有 Coding Agent 的 Loop Engineering:撑起我这套基础设施的 11 个定时任务 | ✅ 已通过 | Batch-02 | 2026-10-27 04:22 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/架构蓝图对齐/排版折叠) |
|
||||
| 20 | `case-studies` | `one-prometheus-for-unraid-synology-and-a-vps` | 用一个 Prometheus 监控 unRaid、群晖 NAS 和 VPS:我做错的四件事 | ✅ 已通过 | Batch-02 | 2026-10-27 04:22 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/架构蓝图对齐/排版折叠) |
|
||||
@@ -59,47 +59,47 @@
|
||||
| 45 | `devops` | `putting-a-front-door-on-an-app-you-cant-modify` | 给自己改不了源码的应用装一扇前门 | ✅ 已通过 | Batch-04 | 2026-10-27 04:09 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/技术细节修补) |
|
||||
| 46 | `devops` | `read-only-nocodb-dashboard-for-a-remote-database` | 给另一台机器上的数据库配一个 NocoDB 只读看板 | ✅ 已通过 | Batch-04 | 2026-10-27 04:09 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/技术细节修补) |
|
||||
| 47 | `devops` | `replacing-rdpguard-with-ipban` | 用 IPBan 替换 RDPGuard:没人写出来的三个坑 | ✅ 已通过 | Batch-04 | 2026-10-27 04:09 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/技术细节修补) |
|
||||
| 48 | `devops` | `space-reclamation-wasnt-stuck-it-was-waiting` | 空间回收没卡住,它只是在等明天早上 09:29 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 49 | `devops` | `synology-spreadsheet-api-is-a-container` | Synology 电子表格 API 是一个容器,不是一个 API 端点 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 50 | `devops` | `that-dying-ssd-was-just-a-bad-sata-cable` | 我差点把这颗 SSD 送修——真正坏的是 SATA 数据线 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 51 | `devops` | `the-cause-was-trim-not-the-ssds` | 换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 52 | `devops` | `the-upstream-was-deleted-then-came-back-rewritten` | 上游被删了,然后带着一个重写版回来 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 53 | `devops` | `unraid-stop-array-hangs-on-swapfile` | Unraid 停止阵列时卡死的真相:Swapfile 锁住挂载点 — 附修复方案 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 54 | `devops` | `uploading-large-videos-to-youtube-via-cdp` | 用 CDP 自动化上传百兆视频到 YouTube:跨越 DevTools 内存墙的 Docker 归档法 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 55 | `devops` | `vetting-an-open-source-dependency-before-you-bet-on-it` | 如何在押注前验证一个开源依赖:我用的六项检查 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 56 | `devops` | `when-smart-says-healthy-but-your-raid-is-corrupting-data` | SMART 显示健康,RAID 却在悄悄损坏数据 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 57 | `devops` | `why-chrome-forgets-its-tabs-in-a-container` | 为什么容器里的 Chrome 会忘记标签页 —— 以及我是怎么修好的 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 58 | `devops` | `why-telegram-bot-notifications-die` | Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个折腾了一整天的 400 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 59 | `devops` | `why-your-cdp-clicks-silently-fail` | 为什么你的 CDP 点击会静默失效:平滑滚动、最小化窗口与 JS 派发事件 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 60 | `devops` | `why-your-grafana-dashboard-shows-no-data` | Prometheus 明明有数据,Grafana 面板却全是 No data 的原因 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 61 | `devops` | `your-disk-full-alert-is-lying` | 你的“磁盘将满”告警在说谎:别再按百分比告警 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 62 | `engineering` | `adding-english-mode-to-a-chinese-only-web-app` | 浏览器翻译没有 API:我给一个纯中文网页应用加上了英文模式 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 63 | `engineering` | `debugging-remotion-video-jitter` | 排查 Remotion 视频抽搐:为什么在背景图挂载音频低频 Scale 会毁了转场 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 64 | `engineering` | `embedding-fonts-so-a-pdf-looks-the-same-everywhere` | 把字体嵌进 PDF,让它在任何机器上都长一样 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 65 | `engineering` | `jpa-version-field-lost-update` | 那个把静默丢失更新变成 409 的 JPA 字段:@Version 乐观锁 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 66 | `engineering` | `migrating-codeigniter-iis-to-openlitespeed` | 把 CodeIgniter 4 应用从 IIS 迁移到 OpenLiteSpeed | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 67 | `engineering` | `scraping-bot-walled-marketplace-warm-browser-session` | 用「热浏览器会话」爬取有反爬墙的电商网站 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 68 | `engineering` | `two-ways-a-javascript-file-stops-working-and-looks-fine` | JavaScript 文件静默失效的两种方式:而页面看上去一切正常 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 69 | `engineering` | `verifying-a-pdf-report-page-by-page` | 逐页验证一份 19 页的 PDF 报告 | ⏳ 待审阅 | Batch-03 | - | |
|
||||
| 70 | `engineering` | `why-500000-lines-of-commit-churn-only-left-230000-in-production` | 30 天提交 51.8 万行,生产环境只留下 23.5 万行 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 71 | `notes` | `adding-a-domain-to-someone-elses-mail-server` | 给别人家的邮件服务器加一个域名 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 72 | `notes` | `hello-world` | 你好,世界 —— 关于这个博客 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 73 | `notes` | `my-backup-was-using-someone-elses-api-quota` | 我的备份爬得比爬虫还慢,因为我在用别人的 API 配额 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 74 | `notes` | `nocodb-sso-is-a-licensed-feature` | NocoDB SSO 是授权功能:环境变量不会告诉你的事 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 75 | `notes` | `reading-a-containers-own-api-docs` | 与其看供应商文档,不如直接读容器自带的 API 文档 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 76 | `notes` | `smartctl-exit-code-32-skips-the-disks-that-matter` | smartctl 退出码 32:专门跳过你最该看的硬盘的那个“错误” | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 77 | `notes` | `stop-writing-agent-prompts-for-deterministic-work` | 确定性的活,别再用 Agent Prompt 去跑 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 78 | `notes` | `synology-api-401-with-the-correct-password` | 密码正确,Synology API 登录却返回 401,问题出在哪 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 79 | `notes` | `the-nocodb-attachment-that-wouldnt-update` | NocoDB 附件字段怎么改都不生效,和一个漏网的正则 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 80 | `notes` | `upgrading-codeigniter-46-to-47` | CodeIgniter 4.6 升级到 4.7:官方指南没说到的破坏性变更 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 81 | `notes` | `why-is-my-tts-model-repeating-percent` | 为什么本地语音合成一直在念 “Percent”?语音管线里的 URL 编码与沙盒避坑记 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 82 | `notes` | `why-multi-agent-coding-fails` | 为什么多个 AI 一起写代码总会烂尾?传话失真与隐式决策困境 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 83 | `notes` | `why-your-windows-mcp-calls-silently-fail` | 为什么你的 Windows 桌面 MCP 调用会静默失败:攻破 10 秒空闲门禁与避坑实录 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 84 | `tutorials` | `calibrating-intel-quick-sync-hevc-quality` | 校准 Intel Quick Sync HEVC:28 与 30 之间的悬崖,和一个毫无作用的 preset | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 85 | `tutorials` | `does-hevc-actually-shrink-your-files` | 转成 HEVC 到底会不会让文件变小?我实测了 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 86 | `tutorials` | `generating-full-length-songs-with-yue2-comfyui` | 用 YuE2 BF16 生成完整长歌:突破时长限制与音频硬截断 | ⏳ 待审阅 | Batch-06 | - | |
|
||||
| 87 | `tutorials` | `generating-subtitles-for-songs-without-timestamps` | 给没有时间戳的歌曲自动生成字幕 | ⏳ 待审阅 | Batch-07 | - | |
|
||||
| 88 | `tutorials` | `how-to-verify-a-hosting-provider-before-you-buy` | 购买主机前,如何核实一家服务商的真实情况 | ⏳ 待审阅 | Batch-07 | - | |
|
||||
| 89 | `web3` | `chainlink-vrf-v2-lottery-contract` | 一个基于 Chainlink VRF v2.5 与 Automation 的自动化彩票合约 | ⏳ 待审阅 | Batch-07 | - | |
|
||||
| 90 | `web3` | `fully-on-chain-svg-nfts` | 全链上 SVG NFT:把艺术作品放进合约里 | ⏳ 待审阅 | Batch-07 | - | |
|
||||
| 91 | `web3` | `why-my-on-chain-nft-art-changed-on-windows` | 为什么在 Windows 上克隆后,我的链上 NFT 图像变了 | ⏳ 待审阅 | Batch-07 | - | |
|
||||
| 48 | `devops` | `space-reclamation-wasnt-stuck-it-was-waiting` | 空间回收没卡住,它只是在等明天早上 09:29 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 49 | `devops` | `synology-spreadsheet-api-is-a-container` | Synology 电子表格 API 是一个容器,不是一个 API 端点 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 50 | `devops` | `that-dying-ssd-was-just-a-bad-sata-cable` | 我差点把这颗 SSD 送修——真正坏的是 SATA 数据线 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 51 | `devops` | `the-cause-was-trim-not-the-ssds` | 换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 52 | `devops` | `the-upstream-was-deleted-then-came-back-rewritten` | 上游被删了,然后带着一个重写版回来 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 53 | `devops` | `unraid-stop-array-hangs-on-swapfile` | Unraid 停止阵列时卡死的真相:Swapfile 锁住挂载点 — 附修复方案 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 54 | `devops` | `uploading-large-videos-to-youtube-via-cdp` | 用 CDP 自动化上传百兆视频到 YouTube:跨越 DevTools 内存墙的 Docker 归档法 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 55 | `devops` | `vetting-an-open-source-dependency-before-you-bet-on-it` | 如何在押注前验证一个开源依赖:我用的六项检查 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 56 | `devops` | `when-smart-says-healthy-but-your-raid-is-corrupting-data` | SMART 显示健康,RAID 却在悄悄损坏数据 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 57 | `devops` | `why-chrome-forgets-its-tabs-in-a-container` | 为什么容器里的 Chrome 会忘记标签页 —— 以及我是怎么修好的 | ✅ 已通过 | Batch-02 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 58 | `devops` | `why-telegram-bot-notifications-die` | Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个追查了一整天的 400 | ✅ 已通过 | Batch-02 | 2026-10-27 05:23 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 59 | `devops` | `why-your-cdp-clicks-silently-fail` | 为什么你的 CDP 点击会静默失效:平滑滚动、最小化窗口与 JS 派发事件 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 60 | `devops` | `why-your-grafana-dashboard-shows-no-data` | Prometheus 明明有数据,Grafana 面板却全是 No data 的原因 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 61 | `devops` | `your-disk-full-alert-is-lying` | 你的“磁盘将满”告警在说谎:别再按百分比告警 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 62 | `engineering` | `adding-english-mode-to-a-chinese-only-web-app` | 浏览器翻译没有 API:我给一个纯中文网页应用加上了英文模式 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 63 | `engineering` | `debugging-remotion-video-jitter` | 排查 Remotion 视频抽搐:为什么在背景图挂载音频低频 Scale 会毁了转场 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 64 | `engineering` | `embedding-fonts-so-a-pdf-looks-the-same-everywhere` | 把字体嵌进 PDF,让它在任何机器上都长一样 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 65 | `engineering` | `jpa-version-field-lost-update` | 那个把静默丢失更新变成 409 的 JPA 字段:@Version 乐观锁 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 66 | `engineering` | `migrating-codeigniter-iis-to-openlitespeed` | 把 CodeIgniter 4 应用从 IIS 迁移到 OpenLiteSpeed | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 67 | `engineering` | `scraping-bot-walled-marketplace-warm-browser-session` | 用「热浏览器会话」爬取有反爬墙的电商网站 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 68 | `engineering` | `two-ways-a-javascript-file-stops-working-and-looks-fine` | JavaScript 文件静默失效的两种方式:而页面看上去一切正常 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 69 | `engineering` | `verifying-a-pdf-report-page-by-page` | 逐页验证一份 19 页的 PDF 报告 | ⏳ 待审阅 | Batch-02 | - | |
|
||||
| 70 | `engineering` | `why-500000-lines-of-commit-churn-only-left-230000-in-production` | 30 天提交 51.8 万行,生产环境只留下 23.5 万行 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 71 | `notes` | `adding-a-domain-to-someone-elses-mail-server` | 给别人家的邮件服务器加一个域名 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 72 | `notes` | `hello-world` | 你好,世界 —— 关于这个博客 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 73 | `notes` | `my-backup-was-using-someone-elses-api-quota` | 我的备份爬得比爬虫还慢,因为我在用别人的 API 配额 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 74 | `notes` | `nocodb-sso-is-a-licensed-feature` | NocoDB SSO 是授权功能:环境变量不会告诉你的事 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 75 | `notes` | `reading-a-containers-own-api-docs` | 与其看供应商文档,不如直接读容器自带的 API 文档 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 76 | `notes` | `smartctl-exit-code-32-skips-the-disks-that-matter` | smartctl 退出码 32:专门跳过你最该看的硬盘的那个“错误” | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 77 | `notes` | `stop-writing-agent-prompts-for-deterministic-work` | 确定性的活,别再用 Agent Prompt 去跑 | ✅ 已通过 | Batch-03 | 2026-10-27 05:22 | 外部顾问审阅通过并落地修改 |
|
||||
| 78 | `notes` | `synology-api-401-with-the-correct-password` | 密码正确,Synology API 登录却返回 401,问题出在哪 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 79 | `notes` | `the-nocodb-attachment-that-wouldnt-update` | NocoDB 附件字段怎么改都不生效,和一个漏网的正则 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 80 | `notes` | `upgrading-codeigniter-46-to-47` | CodeIgniter 4.6 升级到 4.7:官方指南没说到的破坏性变更 | ✅ 已通过 | Batch-03 | 2026-10-27 05:22 | 外部顾问审阅通过并落地修改 |
|
||||
| 81 | `notes` | `why-is-my-tts-model-repeating-percent` | 为什么本地语音合成一直在念 “Percent”?语音管线里的 URL 编码与沙盒避坑记 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 82 | `notes` | `why-multi-agent-coding-fails` | 为什么多个 AI 一起写代码总会烂尾?传话失真与隐式决策困境 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 83 | `notes` | `why-your-windows-mcp-calls-silently-fail` | 为什么你的 Windows 桌面 MCP 调用会静默失败:攻破 10 秒空闲门禁与避坑实录 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 84 | `tutorials` | `calibrating-intel-quick-sync-hevc-quality` | 校准 Intel Quick Sync HEVC:28 与 30 之间的悬崖,和一个毫无作用的 preset | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 85 | `tutorials` | `does-hevc-actually-shrink-your-files` | 转成 HEVC 到底会不会让文件变小?我实测了 | ✅ 已通过 | Batch-04 | 2026-10-27 05:21 | 外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理) |
|
||||
| 86 | `tutorials` | `generating-full-length-songs-with-yue2-comfyui` | 用 YuE2 BF16 生成完整长歌:突破时长限制与音频硬截断 | ⏳ 待审阅 | Batch-04 | - | |
|
||||
| 87 | `tutorials` | `generating-subtitles-for-songs-without-timestamps` | 给没有时间戳的歌曲自动生成字幕 | ⏳ 待审阅 | Batch-05 | - | |
|
||||
| 88 | `tutorials` | `how-to-verify-a-hosting-provider-before-you-buy` | 购买主机前,如何核实一家服务商的真实情况 | ⏳ 待审阅 | Batch-05 | - | |
|
||||
| 89 | `web3` | `chainlink-vrf-v2-lottery-contract` | 一个基于 Chainlink VRF v2.5 与 Automation 的自动化彩票合约 | ⏳ 待审阅 | Batch-05 | - | |
|
||||
| 90 | `web3` | `fully-on-chain-svg-nfts` | 全链上 SVG NFT:把艺术作品放进合约里 | ⏳ 待审阅 | Batch-05 | - | |
|
||||
| 91 | `web3` | `why-my-on-chain-nft-art-changed-on-windows` | 为什么在 Windows 上克隆后,我的链上 NFT 图像变了 | ⏳ 待审阅 | Batch-05 | - | |
|
||||
@@ -5,12 +5,12 @@
|
||||
"zh_title": "给别人家的邮件服务器加一个域名",
|
||||
"category": "notes",
|
||||
"pub_date": "2025-05-06",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7375,
|
||||
"zh_chars": 3249
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 7886,
|
||||
"zh_chars": 3603
|
||||
},
|
||||
"adding-english-mode-to-a-chinese-only-web-app": {
|
||||
"slug": "adding-english-mode-to-a-chinese-only-web-app",
|
||||
@@ -19,7 +19,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2025-06-11",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 11859,
|
||||
@@ -31,10 +31,10 @@
|
||||
"zh_title": "我如何用 FLUX.1 Kontext 打造 AI 家具合成演示",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2026-09-11",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 12512,
|
||||
"zh_chars": 7237
|
||||
},
|
||||
@@ -109,12 +109,12 @@
|
||||
"zh_title": "校准 Intel Quick Sync HEVC:28 与 30 之间的悬崖,和一个毫无作用的 preset",
|
||||
"category": "tutorials",
|
||||
"pub_date": "2026-10-14",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 10490,
|
||||
"zh_chars": 5631
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 10968,
|
||||
"zh_chars": 5968
|
||||
},
|
||||
"chainlink-vrf-v2-lottery-contract": {
|
||||
"slug": "chainlink-vrf-v2-lottery-contract",
|
||||
@@ -123,7 +123,7 @@
|
||||
"category": "web3",
|
||||
"pub_date": "2024-12-10",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-07",
|
||||
"batch_id": "Batch-05",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 15801,
|
||||
@@ -136,7 +136,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2025-11-26",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 9584,
|
||||
@@ -161,12 +161,12 @@
|
||||
"zh_title": "转成 HEVC 到底会不会让文件变小?我实测了",
|
||||
"category": "tutorials",
|
||||
"pub_date": "2026-10-08",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7417,
|
||||
"zh_chars": 3695
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 8042,
|
||||
"zh_chars": 4159
|
||||
},
|
||||
"embedding-fonts-so-a-pdf-looks-the-same-everywhere": {
|
||||
"slug": "embedding-fonts-so-a-pdf-looks-the-same-everywhere",
|
||||
@@ -175,7 +175,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2025-01-22",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8033,
|
||||
@@ -213,10 +213,10 @@
|
||||
"zh_title": "从一首歌到 4 分钟完整 MV:我如何用 Gemini、AI 生成与 Filmora 剪辑串起全流程实操",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2026-10-25",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 11027,
|
||||
"zh_chars": 5168
|
||||
},
|
||||
@@ -227,7 +227,7 @@
|
||||
"category": "web3",
|
||||
"pub_date": "2024-10-08",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-07",
|
||||
"batch_id": "Batch-05",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 14598,
|
||||
@@ -240,7 +240,7 @@
|
||||
"category": "tutorials",
|
||||
"pub_date": "2026-10-26",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-06",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 131667,
|
||||
@@ -253,7 +253,7 @@
|
||||
"category": "tutorials",
|
||||
"pub_date": "2026-10-20",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-07",
|
||||
"batch_id": "Batch-05",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 46399,
|
||||
@@ -291,12 +291,12 @@
|
||||
"zh_title": "你好,世界 —— 关于这个博客",
|
||||
"category": "notes",
|
||||
"pub_date": "2024-09-24",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 1794,
|
||||
"zh_chars": 955
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 1938,
|
||||
"zh_chars": 1074
|
||||
},
|
||||
"how-i-built-an-automated-music-video-pipeline": {
|
||||
"slug": "how-i-built-an-automated-music-video-pipeline",
|
||||
@@ -304,10 +304,10 @@
|
||||
"zh_title": "我是如何用 Remotion 与 Docker CDP 打造全自动 MV 生成与多语种油管发布流的",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2025-04-18",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 9265,
|
||||
"zh_chars": 4931
|
||||
},
|
||||
@@ -317,10 +317,10 @@
|
||||
"zh_title": "我是如何构建 DigiKedai Telegram AI 客服机器人的",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2026-09-06",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 10719,
|
||||
"zh_chars": 5271
|
||||
},
|
||||
@@ -330,10 +330,10 @@
|
||||
"zh_title": "我是如何托管这个博客的:Astro、Gitea Actions 与自托管 CI/CD",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2026-09-04",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 5908,
|
||||
"zh_chars": 4145
|
||||
},
|
||||
@@ -343,10 +343,10 @@
|
||||
"zh_title": "我用 Suno AI 做出自己的歌",
|
||||
"category": "case-studies",
|
||||
"pub_date": "2026-09-11",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 16935,
|
||||
"zh_chars": 11737
|
||||
},
|
||||
@@ -370,7 +370,7 @@
|
||||
"category": "tutorials",
|
||||
"pub_date": "2025-11-26",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-07",
|
||||
"batch_id": "Batch-05",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 6743,
|
||||
@@ -409,7 +409,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2026-08-19",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 12217,
|
||||
@@ -435,7 +435,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2026-09-27",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 14943,
|
||||
@@ -447,12 +447,12 @@
|
||||
"zh_title": "我的备份爬得比爬虫还慢,因为我在用别人的 API 配额",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-10-15",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8078,
|
||||
"zh_chars": 4209
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 8572,
|
||||
"zh_chars": 4527
|
||||
},
|
||||
"n8n-v1-to-v2-upgrade-gotchas": {
|
||||
"slug": "n8n-v1-to-v2-upgrade-gotchas",
|
||||
@@ -473,12 +473,12 @@
|
||||
"zh_title": "NocoDB SSO 是授权功能:环境变量不会告诉你的事",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-09-29",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 9934,
|
||||
"zh_chars": 5418
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 9937,
|
||||
"zh_chars": 5421
|
||||
},
|
||||
"one-hostname-public-tracker-sso-dashboard": {
|
||||
"slug": "one-hostname-public-tracker-sso-dashboard",
|
||||
@@ -590,12 +590,12 @@
|
||||
"zh_title": "与其看供应商文档,不如直接读容器自带的 API 文档",
|
||||
"category": "notes",
|
||||
"pub_date": "2025-10-01",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 8382,
|
||||
"zh_chars": 5483
|
||||
"zh_chars": 5489
|
||||
},
|
||||
"replacing-rdpguard-with-ipban": {
|
||||
"slug": "replacing-rdpguard-with-ipban",
|
||||
@@ -656,7 +656,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2026-09-13",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 11245,
|
||||
@@ -746,12 +746,12 @@
|
||||
"zh_title": "smartctl 退出码 32:专门跳过你最该看的硬盘的那个“错误”",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-04-14",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 5475,
|
||||
"zh_chars": 2907
|
||||
"zh_chars": 2881
|
||||
},
|
||||
"space-reclamation-wasnt-stuck-it-was-waiting": {
|
||||
"slug": "space-reclamation-wasnt-stuck-it-was-waiting",
|
||||
@@ -759,12 +759,12 @@
|
||||
"zh_title": "空间回收没卡住,它只是在等明天早上 09:29",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-10-14",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 5763,
|
||||
"zh_chars": 3032
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 6475,
|
||||
"zh_chars": 3424
|
||||
},
|
||||
"stop-writing-agent-prompts-for-deterministic-work": {
|
||||
"slug": "stop-writing-agent-prompts-for-deterministic-work",
|
||||
@@ -772,10 +772,10 @@
|
||||
"zh_title": "确定性的活,别再用 Agent Prompt 去跑",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-10-01",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-03",
|
||||
"approved_at": "2026-10-27 05:22",
|
||||
"notes": "外部顾问审阅通过并落地修改",
|
||||
"en_chars": 4951,
|
||||
"zh_chars": 2216
|
||||
},
|
||||
@@ -798,12 +798,12 @@
|
||||
"zh_title": "密码正确,Synology API 登录却返回 401,问题出在哪",
|
||||
"category": "notes",
|
||||
"pub_date": "2025-09-10",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 5053,
|
||||
"zh_chars": 3053
|
||||
"zh_chars": 3063
|
||||
},
|
||||
"synology-spreadsheet-api-is-a-container": {
|
||||
"slug": "synology-spreadsheet-api-is-a-container",
|
||||
@@ -811,12 +811,12 @@
|
||||
"zh_title": "Synology 电子表格 API 是一个容器,不是一个 API 端点",
|
||||
"category": "devops",
|
||||
"pub_date": "2025-08-20",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 15944,
|
||||
"zh_chars": 9105
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 15961,
|
||||
"zh_chars": 9131
|
||||
},
|
||||
"that-dying-ssd-was-just-a-bad-sata-cable": {
|
||||
"slug": "that-dying-ssd-was-just-a-bad-sata-cable",
|
||||
@@ -824,12 +824,12 @@
|
||||
"zh_title": "我差点把这颗 SSD 送修——真正坏的是 SATA 数据线",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-16",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 6484,
|
||||
"zh_chars": 3426
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 7128,
|
||||
"zh_chars": 3785
|
||||
},
|
||||
"the-cause-was-trim-not-the-ssds": {
|
||||
"slug": "the-cause-was-trim-not-the-ssds",
|
||||
@@ -837,12 +837,12 @@
|
||||
"zh_title": "换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-18",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 11452,
|
||||
"zh_chars": 6481
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 12427,
|
||||
"zh_chars": 7072
|
||||
},
|
||||
"the-nocodb-attachment-that-wouldnt-update": {
|
||||
"slug": "the-nocodb-attachment-that-wouldnt-update",
|
||||
@@ -850,12 +850,12 @@
|
||||
"zh_title": "NocoDB 附件字段怎么改都不生效,和一个漏网的正则",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-02-04",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 3570,
|
||||
"zh_chars": 2156
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 3798,
|
||||
"zh_chars": 2312
|
||||
},
|
||||
"the-single-writer-rule-for-coding-agents": {
|
||||
"slug": "the-single-writer-rule-for-coding-agents",
|
||||
@@ -876,12 +876,12 @@
|
||||
"zh_title": "上游被删了,然后带着一个重写版回来",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-10-06",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 11536,
|
||||
"zh_chars": 5142
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 11554,
|
||||
"zh_chars": 5159
|
||||
},
|
||||
"two-ways-a-javascript-file-stops-working-and-looks-fine": {
|
||||
"slug": "two-ways-a-javascript-file-stops-working-and-looks-fine",
|
||||
@@ -890,7 +890,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2025-02-20",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7750,
|
||||
@@ -902,12 +902,12 @@
|
||||
"zh_title": "Unraid 停止阵列时卡死的真相:Swapfile 锁住挂载点 — 附修复方案",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-15",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 10067,
|
||||
"zh_chars": 6117
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 10151,
|
||||
"zh_chars": 6226
|
||||
},
|
||||
"upgrading-codeigniter-46-to-47": {
|
||||
"slug": "upgrading-codeigniter-46-to-47",
|
||||
@@ -915,10 +915,10 @@
|
||||
"zh_title": "CodeIgniter 4.6 升级到 4.7:官方指南没说到的破坏性变更",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-09-27",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-03",
|
||||
"approved_at": "2026-10-27 05:22",
|
||||
"notes": "外部顾问审阅通过并落地修改",
|
||||
"en_chars": 9469,
|
||||
"zh_chars": 5411
|
||||
},
|
||||
@@ -928,12 +928,12 @@
|
||||
"zh_title": "用 CDP 自动化上传百兆视频到 YouTube:跨越 DevTools 内存墙的 Docker 归档法",
|
||||
"category": "devops",
|
||||
"pub_date": "2025-08-22",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 10255,
|
||||
"zh_chars": 6569
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 11122,
|
||||
"zh_chars": 7070
|
||||
},
|
||||
"using-chinese-llm-apis-from-malaysia": {
|
||||
"slug": "using-chinese-llm-apis-from-malaysia",
|
||||
@@ -955,7 +955,7 @@
|
||||
"category": "engineering",
|
||||
"pub_date": "2026-09-28",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 12525,
|
||||
@@ -967,12 +967,12 @@
|
||||
"zh_title": "如何在押注前验证一个开源依赖:我用的六项检查",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-29",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8459,
|
||||
"zh_chars": 4164
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 8512,
|
||||
"zh_chars": 4201
|
||||
},
|
||||
"when-smart-says-healthy-but-your-raid-is-corrupting-data": {
|
||||
"slug": "when-smart-says-healthy-but-your-raid-is-corrupting-data",
|
||||
@@ -980,12 +980,12 @@
|
||||
"zh_title": "SMART 显示健康,RAID 却在悄悄损坏数据",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-16",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 12365,
|
||||
"zh_chars": 5343
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 13283,
|
||||
"zh_chars": 5858
|
||||
},
|
||||
"when-to-give-your-agent-physical-clicks": {
|
||||
"slug": "when-to-give-your-agent-physical-clicks",
|
||||
@@ -1006,12 +1006,12 @@
|
||||
"zh_title": "30 天提交 51.8 万行,生产环境只留下 23.5 万行",
|
||||
"category": "engineering",
|
||||
"pub_date": "2026-10-18",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8794,
|
||||
"zh_chars": 4125
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 8949,
|
||||
"zh_chars": 4197
|
||||
},
|
||||
"why-chrome-forgets-its-tabs-in-a-container": {
|
||||
"slug": "why-chrome-forgets-its-tabs-in-a-container",
|
||||
@@ -1019,12 +1019,12 @@
|
||||
"zh_title": "为什么容器里的 Chrome 会忘记标签页 —— 以及我是怎么修好的",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-09-29",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 14761,
|
||||
"zh_chars": 8889
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 14876,
|
||||
"zh_chars": 8930
|
||||
},
|
||||
"why-i-built-a-video-knowledge-base": {
|
||||
"slug": "why-i-built-a-video-knowledge-base",
|
||||
@@ -1045,10 +1045,10 @@
|
||||
"zh_title": "Claude 和 GPT 明明更强,我为什么还是买了 GPU 跑本地 AI",
|
||||
"category": "ai",
|
||||
"pub_date": "2026-09-21",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 11811,
|
||||
"zh_chars": 4924
|
||||
},
|
||||
@@ -1058,12 +1058,12 @@
|
||||
"zh_title": "为什么本地语音合成一直在念 “Percent”?语音管线里的 URL 编码与沙盒避坑记",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-10-22",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7263,
|
||||
"zh_chars": 4652
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 7728,
|
||||
"zh_chars": 4968
|
||||
},
|
||||
"why-multi-agent-coding-fails": {
|
||||
"slug": "why-multi-agent-coding-fails",
|
||||
@@ -1071,12 +1071,12 @@
|
||||
"zh_title": "为什么多个 AI 一起写代码总会烂尾?传话失真与隐式决策困境",
|
||||
"category": "notes",
|
||||
"pub_date": "2025-07-29",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 5704,
|
||||
"zh_chars": 2936
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 6364,
|
||||
"zh_chars": 3353
|
||||
},
|
||||
"why-my-on-chain-nft-art-changed-on-windows": {
|
||||
"slug": "why-my-on-chain-nft-art-changed-on-windows",
|
||||
@@ -1085,7 +1085,7 @@
|
||||
"category": "web3",
|
||||
"pub_date": "2026-08-19",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-07",
|
||||
"batch_id": "Batch-05",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7725,
|
||||
@@ -1094,15 +1094,15 @@
|
||||
"why-telegram-bot-notifications-die": {
|
||||
"slug": "why-telegram-bot-notifications-die",
|
||||
"title": "Why Telegram Bot Notifications Die: IPv6, DNS, and a 400 That Took a Day",
|
||||
"zh_title": "Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个折腾了一整天的 400",
|
||||
"zh_title": "Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个追查了一整天的 400",
|
||||
"category": "devops",
|
||||
"pub_date": "2026-03-25",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8963,
|
||||
"zh_chars": 5185
|
||||
"approved_at": "2026-10-27 05:23",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 9327,
|
||||
"zh_chars": 5334
|
||||
},
|
||||
"why-your-cdp-clicks-silently-fail": {
|
||||
"slug": "why-your-cdp-clicks-silently-fail",
|
||||
@@ -1111,7 +1111,7 @@
|
||||
"category": "devops",
|
||||
"pub_date": "2025-03-19",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 8010,
|
||||
@@ -1124,7 +1124,7 @@
|
||||
"category": "devops",
|
||||
"pub_date": "2026-05-17",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 7347,
|
||||
@@ -1149,12 +1149,12 @@
|
||||
"zh_title": "为什么你的 Windows 桌面 MCP 调用会静默失败:攻破 10 秒空闲门禁与避坑实录",
|
||||
"category": "notes",
|
||||
"pub_date": "2026-03-12",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-04",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 5024,
|
||||
"zh_chars": 3435
|
||||
"approved_at": "2026-10-27 05:21",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含数字矛盾/OPSEC脱敏/双语CTA/排版折叠/禁用词清理)",
|
||||
"en_chars": 5663,
|
||||
"zh_chars": 3857
|
||||
},
|
||||
"your-disk-full-alert-is-lying": {
|
||||
"slug": "your-disk-full-alert-is-lying",
|
||||
@@ -1163,7 +1163,7 @@
|
||||
"category": "devops",
|
||||
"pub_date": "2026-06-24",
|
||||
"status": "pending",
|
||||
"batch_id": "Batch-03",
|
||||
"batch_id": "Batch-02",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"en_chars": 5930,
|
||||
@@ -1175,10 +1175,10 @@
|
||||
"zh_title": "为什么我的 AI 歌曲总是丢结尾:在 12GB 显卡上跑 YuE2",
|
||||
"category": "ai",
|
||||
"pub_date": "2026-10-20",
|
||||
"status": "pending",
|
||||
"status": "approved",
|
||||
"batch_id": "Batch-01",
|
||||
"approved_at": null,
|
||||
"notes": "",
|
||||
"approved_at": "2026-10-27 05:26",
|
||||
"notes": "外部顾问审阅完成,已采纳并落地全量修改意见(含GPU/Token成本算账、Gemini净化、双语CTA、代码折叠与简繁转换)",
|
||||
"en_chars": 28955,
|
||||
"zh_chars": 24414
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
> **包含文章数**:8 篇
|
||||
> **本批次总字符量**:174,959 字(严格限制 < 200,000 字)
|
||||
> **模式**:BILINGUAL
|
||||
> **生成时间**:2026-10-27 04:23:31
|
||||
> **生成时间**:2026-10-27 05:23:38
|
||||
|
||||
# 外部顾问审阅前置须知与工程背景规范
|
||||
|
||||
|
||||
+3897
-4102
File diff suppressed because it is too large.
Load diff
+3245
-4057
File diff suppressed because it is too large.
Load diff
+3206
-4415
File diff suppressed because it is too large.
Load diff
+3476
-3099
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -154,3 +154,13 @@ verified-sender requirement, which new domains have to satisfy there too.
|
||||
whole categories of failure at once.
|
||||
4. **Keep the four inbound records as a checklist.** That half is boring,
|
||||
verifiable, and was the part that worked first try.
|
||||
|
||||
---
|
||||
|
||||
## Need Reliable Business Email That Doesn't Land in Spam?
|
||||
|
||||
If you are setting up custom-domain mailboxes, resolving SPF/DKIM/DMARC/PTR delivery failures, or migrating historical email archives for your business, I design and manage dedicated mail infrastructure through SifuMail:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Mail%20server%20%26%20deliverability%20consulting)
|
||||
- **SifuMail:** [sifumail.com](https://sifumail.com)
|
||||
@@ -218,3 +218,13 @@ no Intel GPU moved from an informational "waiting for hardware" line to a
|
||||
warning — which is its own small lesson about what a calibrated encoder
|
||||
failing self-check should look like, and why hardware the machine does not
|
||||
have belongs in a summary row rather than a warning.
|
||||
|
||||
---
|
||||
|
||||
## Need Resilient Hardware Video Pipelines & Automated Quality Gates?
|
||||
|
||||
If you manage GPU/iGPU transcoding fleets (NVENC, Quick Sync, VAAPI) or need strict frame-level quality assertions (SSIM/VMAF) to keep defective encodes out of production libraries:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Media%20pipeline%20%26%20transcoding%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -41,8 +41,8 @@ output:
|
||||
|
||||
1. measure the output's **video bitrate** as a percentage of the source's
|
||||
video bitrate (not the file size — audio is excluded so the two are
|
||||
comparable), and 2. measure **SSIM against the source**, frame by frame,
|
||||
downscaled to 480×270 for a stable metric, reporting min and mean.
|
||||
comparable), and
|
||||
2. measure **SSIM against the source**, frame by frame, downscaled to 480×270 for a stable metric, reporting min and mean.
|
||||
|
||||
One prerequisite: the harness must be self-consistent. Comparing a file with
|
||||
*itself* through the same code path must return SSIM 1.0000. When mine did not
|
||||
@@ -87,10 +87,7 @@ encode; the 1080p one was a fat one. Same bpp, different headroom. bpp is
|
||||
still the cheapest first filter (it told me which *files* to re-encode), but
|
||||
it does not predict the ratio.
|
||||
|
||||
**3. Software x265 beats NVENC on size, at equal quality.** Compare the rows
|
||||
at similar SSIM: cq31 vs crf26 (min SSIM 0.9894 vs 0.9927 on the 720p file)
|
||||
— 72.9% vs 56.7%. That is a 22% smaller file at a *higher* measured quality.
|
||||
Across the set, analysis gives roughly **10–22%** extra saving. NVENC is not
|
||||
**3. Software x265 beats NVENC on size, at equal quality.** Compare the rows at matched SSIM: cq31 vs crf26 — 72.9% vs 56.7% on the 720p file (min SSIM 0.9894 vs 0.9879) and 43.0% vs 33.7% on the 1080p file (0.9941 vs 0.9927), a **22% smaller file** at the same quality tier; on the vertical file, x265 is both smaller (67.0% vs 74.3%) and *higher* quality (0.9913 vs 0.9890). Across the set, analysis gives roughly **10–22%** extra saving. NVENC is not
|
||||
worse at encoding; it is optimized for throughput. If your constraint is
|
||||
disk, spend CPU; if it is time, spend GPU.
|
||||
|
||||
@@ -118,20 +115,12 @@ in MP4 is not playable on iOS; AAC is).
|
||||
|
||||
## The decision rules I actually use now
|
||||
|
||||
1. **Classify by bpp before encoding anything.** Bits per pixel per second
|
||||
(`video_bitrate / (width × height × fps)`). It is one ffprobe call and it
|
||||
separates "there is headroom here" from "leave it alone" better than
|
||||
resolution or filesize does. 2. **Never re-encode a source that is already
|
||||
efficient.** Copy the video stream, fix the audio if it needs it, and move
|
||||
on. A re-encode cannot create headroom that is not there. 3. **Set quality,
|
||||
not bitrate — then verify against the source.** A metric compared frame by
|
||||
frame, with a self-test proving your harness works. 4. **Choose the encoder
|
||||
by your bottleneck.** Disk → x265 (10–22% smaller at equal quality). Time →
|
||||
NVENC. 5. **Cap the output.** Whatever you do, assert the result is not
|
||||
bigger than the source. That single assertion would have caught the entire
|
||||
111% problem before it touched 54 files. 6. **Remember the audio.** After
|
||||
aggressive video compression, the audio track can be the largest remaining
|
||||
component of the file.
|
||||
1. **Classify by bpp before encoding anything.** Bits per pixel per second (`video_bitrate / (width × height × fps)`). It is one ffprobe call and it separates "there is headroom here" from "leave it alone" better than resolution or filesize does.
|
||||
2. **Never re-encode a source that is already efficient.** Copy the video stream, fix the audio if it needs it, and move on. A re-encode cannot create headroom that is not there.
|
||||
3. **Set quality, not bitrate — then verify against the source.** A metric compared frame by frame, with a self-test proving your harness works.
|
||||
4. **Choose the encoder by your bottleneck.** Disk → x265 (10–22% smaller at equal quality). Time → NVENC.
|
||||
5. **Cap the output.** Whatever you do, assert the result is not bigger than the source. That single assertion would have caught the entire 111% problem before it touched 54 files.
|
||||
6. **Remember the audio.** After aggressive video compression, the audio track can be the largest remaining component of the file.
|
||||
|
||||
## The measured outcome
|
||||
|
||||
@@ -146,3 +135,13 @@ original — by design, quality preserved exactly.
|
||||
That is the honest answer to "does HEVC shrink my files": it depends on what
|
||||
you are re-encoding, and the only way to know your number is to measure it
|
||||
on your own files with quality held fixed.
|
||||
|
||||
---
|
||||
|
||||
## Need Resilient Hardware Video Pipelines & Automated Quality Gates?
|
||||
|
||||
If you manage GPU/iGPU transcoding fleets (NVENC, Quick Sync, VAAPI) or need strict frame-level quality assertions (SSIM/VMAF) to keep defective encodes out of production libraries:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Media%20pipeline%20%26%20transcoding%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -28,7 +28,7 @@ Most posts are one of a few shapes:
|
||||
## What I write about
|
||||
|
||||
- **Full-stack engineering** — PHP/CodeIgniter, Java/Spring, TypeScript/React, WordPress.
|
||||
- **DevOps & self-hosting** — Docker, Traefik, Cloudflare tunnels, NAS/unRaid homelabs.
|
||||
- **DevOps & self-hosting** — Docker, Traefik, Cloudflare tunnels, on-premise NAS/unRaid infrastructure.
|
||||
- **AI & automation** — Telegram bots, n8n workflows, local LLMs, memory systems.
|
||||
- **Web3** — Solidity, Foundry, ERC-20/721 (learning, honestly framed).
|
||||
|
||||
@@ -41,4 +41,4 @@ for Malaysian SMEs (with email hosting as a side offering, not the focus).
|
||||
|
||||
The posts here are my working portfolio rather than a sales page: what I
|
||||
built, what broke, and what the evidence actually said. Start with the
|
||||
[latest posts](/posts/).
|
||||
[latest posts](/posts/), or get in touch directly via [WhatsApp (+60 12-797 2969)](https://wa.me/60127972969) or [[email protected]](mailto:[email protected]).
|
||||
@@ -169,3 +169,13 @@ backup: at the time I wrote this, the first seed had uploaded 2.88 GB of
|
||||
~1075 GB and the new repo had **zero snapshots** — nothing is restorable
|
||||
until the first snapshot lands. That is the number I watch, because "the
|
||||
transfer is running" and "you can restore" are different claims.
|
||||
|
||||
---
|
||||
|
||||
## Need Disaster-Ready Offsite Backup Architecture?
|
||||
|
||||
A backup is not "the transfer is running" — it's proven restorable data under real outage conditions. If you need end-to-end encrypted, deduplicated, offsite disaster recovery for your servers or production databases:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Disaster%20recovery%20%26%20backup%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -143,7 +143,7 @@ The upside is that I stopped trying to make the app do SSO, put the decision in
|
||||
|
||||
If you're evaluating self-hosted software, need single sign-on in front of internal tools, or want someone to check what your stack actually does versus what its documentation claims, that's the work I do.
|
||||
|
||||
- **WhatsApp:** [011-797 2969](https://wa.me/60127972969) — tap to chat
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969) — tap to chat
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Self-hosted%20SSO%20enquiry)
|
||||
- **Website:** [hoelee.com](https://www.hoelee.com)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ tags: ["synology", "btrfs", "snapshots", "storage", "nas"]
|
||||
ogImage: /og/space-reclamation-wasnt-stuck-it-was-waiting.jpg
|
||||
banner: /banners/space-reclamation-wasnt-stuck-it-was-waiting.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
## The question I asked was the wrong question
|
||||
@@ -36,9 +37,7 @@ looking at a progress card:
|
||||
| 16:35 → 17:39 | **−39 GiB** | new data written (a burst) |
|
||||
| 15:00 → 20:44 | **−47.2 GiB** | net: the volume *lost* space that afternoon |
|
||||
|
||||
Two reclaims, ~121 GiB in total, and then 47 GiB of new writes swallowed the
|
||||
afternoon. "Nothing came back" was a reading error on my side: I compared
|
||||
the morning to the evening, and the evening had a day of writing in it.
|
||||
Two morning snapshot rotations (115.8 GiB) plus the recycle-bin empty (5.0 GiB) reclaimed ~121 GiB in total, and then 47 GiB of new writes swallowed the afternoon. "Nothing came back" was a reading error on my side: I compared the morning to the evening, and the evening had a day of writing in it.
|
||||
|
||||
The recycle bin — the thing I had bothered to empty by hand — was **5 GiB**.
|
||||
The snapshots, which I had not thought about at all, were **115.8 GiB**.
|
||||
@@ -122,3 +121,11 @@ should be alerting on. That is the same lesson as
|
||||
[Your Disk-Full Alert Is Lying](/posts/your-disk-full-alert-is-lying/),
|
||||
arriving from the other direction: alert on the consequence (bytes free over
|
||||
time), because the mechanism's own status is not evidence.
|
||||
|
||||
---
|
||||
|
||||
## Flying blind on storage capacity or snapshot retention?
|
||||
|
||||
I design, audit, and maintain on-premise NAS deployments, btrfs/ZFS pools, and automated backup pipelines — replacing misleading UI status cards with trend-based capacity alerting and verified retention policies. If your storage node is filling up faster than expected or hiding silent bottlenecks, I can audit the stack and put real telemetry in place.
|
||||
|
||||
Reach me at [[email protected]](mailto:[email protected]?subject=Storage%20and%20NAS%20audit) or WhatsApp [+60 12-797 2969](https://wa.me/60127972969), or see what I do at [hoelee.com](https://hoelee.com).
|
||||
@@ -10,6 +10,7 @@ tags: [synology, dsm, docker, portainer, rest-api, spreadsheet, debugging, self-
|
||||
ogImage: /og/synology-spreadsheet-api-is-a-container.jpg
|
||||
banner: /banners/synology-spreadsheet-api-is-a-container.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
I wanted something ordinary: read a cell from a spreadsheet on my NAS, write a cell back, and have a chart come out the other end — without opening Excel, and without shipping my company's numbers to a cloud API.
|
||||
|
||||
@@ -7,6 +7,7 @@ blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, sata, ssd, storage, troubleshooting, smart, fio, cable]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/that-dying-ssd-was-just-a-bad-sata-cable.jpg
|
||||
banner: /banners/that-dying-ssd-was-just-a-bad-sata-cable.jpg
|
||||
---
|
||||
@@ -79,6 +80,14 @@ If you run the same job on a *healthy* drive, `verify` succeeds. If the link is
|
||||
|
||||
## The result
|
||||
|
||||
The drive I was minutes away from RMA-ing is now a formatted, mounted, verified-working cache SSD with `btrfs device stats` reading all zeros. The cost of the fix: one cable. The cost of trusting the diagnosis I had: a needless warranty claim and a rig kept on a cable that was going to corrupt data sooner or later anyway.
|
||||
The drive I was minutes away from RMA-ing is now a formatted, mounted, verified-working cache SSD with `btrfs device stats` reading all zeros. The cost of the fix: one cable. The cost of trusting the diagnosis I had: a needless warranty claim and a host kept on a cable that was going to corrupt data sooner or later anyway.
|
||||
|
||||
If you're on a NAS or a home server and a drive "fails" with FPDMA timeouts but SMART looks fine — change the cable before you change the drive.
|
||||
If you're on a NAS or an on-premise server and a drive "fails" with FPDMA timeouts but SMART looks fine — change the cable before you change the drive.
|
||||
|
||||
---
|
||||
|
||||
## Dealing with unexplained storage timeouts or array drops?
|
||||
|
||||
I diagnose and maintain self-hosted production nodes, Unraid and Synology storage pools, and virtualised workloads — separating link-layer faults from real media failures before you waste downtime on an RMA. If your storage stack is throwing I/O errors that SMART can't explain, I can trace the fault and verify the fix byte for byte.
|
||||
|
||||
Reach me at [[email protected]](mailto:[email protected]?subject=Storage%20I/O%20and%20RAID%20troubleshooting) or WhatsApp [+60 12-797 2969](https://wa.me/60127972969), or see what I do at [hoelee.com](https://hoelee.com).
|
||||
@@ -3,16 +3,19 @@ title: "The Corruption Came Back on Different Drives — the Cause Was TRIM, Not
|
||||
description: "Two weeks after a btrfs RAID1 pool corrupted on healthy-looking NVMe drives, the same all-zero checksum reappeared on a different drive stack. The culprit was queued TRIM (NCQ) on Seagate IronWolf 110 SSDs — a firmware bug FreeBSD blacklisted but Linux never did. How I caught it and the fix."
|
||||
pubDate: 2026-09-18
|
||||
category: devops
|
||||
blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, btrfs, trim, ssd, raid, data-loss, smart, troubleshooting, seagate]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/the-cause-was-trim-not-the-ssds.jpg
|
||||
banner: /banners/the-cause-was-trim-not-the-ssds.jpg
|
||||
---
|
||||
|
||||
Two weeks ago I published how a btrfs RAID1 pool on two healthy-looking Samsung
|
||||
PM9A3 NVMe drives silently corrupted a Windows VM — reads returning
|
||||
In my previous post, I documented how a btrfs RAID1 pool on two healthy-looking Samsung
|
||||
PM9A3 NVMe drives silently corrupted a Windows VM two weeks earlier — reads returning
|
||||
**all-zero** blocks that no single bad drive could explain. That investigation
|
||||
ended with an honest shrug: the drives tested clean, the strongest suspect was
|
||||
the shared M.2 riser.
|
||||
ended with an honest shrug: the drives tested clean afterwards, leaving the shared M.2 riser as the strongest suspect.
|
||||
|
||||
Then it happened again. On *different* drives, on a *different* bus. The exact
|
||||
same checksum fingerprint. This time I found the root cause — and it wasn't
|
||||
@@ -93,11 +96,7 @@ empty blocks — and crucially, on **both mirrors at the same logical offsets**.
|
||||
I verified it against my notes from the PM9A3 pool: identical value, identical
|
||||
"deterministic zeros" behaviour, identical RAID1-defeating correlation.
|
||||
|
||||
That match is a fingerprint. Whatever this is, it isn't the drives and it
|
||||
isn't the M.2 riser — the IronWolf pair lives on **SATA ports**, a
|
||||
completely different controller path. It's the *layer above* the drives
|
||||
deciding to erase the same data on both members at once. There is exactly one
|
||||
mechanism in Linux that does that: **discard / TRIM**.
|
||||
That match explained half the mystery: *any* mechanism that zeroes out a 4 KiB page will produce `0x8941f998`. But on the IronWolf pair, the drives sit on motherboard **SATA ports** — no M.2 riser in sight, and both had just passed `fio --verify=crc32c`. For both mirrors to return zeros at the exact same logical offset with zero link errors, something in the *layer above* the drives had to be instructing both members to erase the same blocks at once. There is exactly one mechanism in Linux that does that: **discard / TRIM**.
|
||||
|
||||
## Step 3 — the root cause: queued TRIM on a drive Linux never blacklisted
|
||||
|
||||
@@ -173,8 +172,7 @@ unrecoverable either way.
|
||||
And a wrinkle worth knowing: **`btrfs device stats` counters did not move
|
||||
during the scrub** even though it found 18 errors — scrub-detected errors and
|
||||
read-detected errors are tracked separately. You cannot watch only `device
|
||||
stats`; you have to watch dmesg + scrub output too. (My watchdog checks all
|
||||
three; the details are at the end.)
|
||||
stats`; you have to watch dmesg + scrub output too (my daily watchdog cron now checks and alerts on all three).
|
||||
|
||||
## What I'd do differently
|
||||
|
||||
@@ -228,4 +226,12 @@ thought.
|
||||
|
||||
---
|
||||
|
||||
*Part of my storage-failure series: [the NVMe incident](/posts/when-smart-says-healthy-but-your-raid-is-corrupting-data/) and the [SATA cable wake-up call](/posts/that-dying-ssd-was-just-a-bad-sata-cable/).*
|
||||
*Part of my storage-failure series: [the NVMe incident](/posts/when-smart-says-healthy-but-your-raid-is-corrupting-data/) and the [SATA cable wake-up call](/posts/that-dying-ssd-was-just-a-bad-sata-cable/).*
|
||||
|
||||
---
|
||||
|
||||
## Worried your storage pool might be failing silently?
|
||||
|
||||
I design, audit, and troubleshoot self-hosted storage and virtualisation stacks — from Unraid and Synology deployments to btrfs/ZFS RAID pools and kernel-level corruption monitoring. If you're hitting unexplained checksum errors, VM crashes, or SSD firmware quirks, I can isolate the root cause and put automated scrub and telemetry guardrails in place.
|
||||
|
||||
Reach me at [[email protected]](mailto:[email protected]?subject=Storage%20pool%20and%20btrfs%20troubleshooting) or WhatsApp [+60 12-797 2969](https://wa.me/60127972969), or see what I do at [hoelee.com](https://hoelee.com).
|
||||
@@ -93,4 +93,6 @@ silently skips is worse than one that fails loudly.
|
||||
Nothing here is exotic. But both are the *boring* kind of bug that only reveals
|
||||
itself in real data — the attachment the API swears it updated, the regex that
|
||||
matches 91% of the rows and says nothing about the other 9%. If backfilling data
|
||||
into NocoDB ever feels like it's half-working, these two are where I'd look first.
|
||||
into NocoDB ever feels like it's half-working, these two are where I'd look first.
|
||||
|
||||
*If you need automated data backfilling, custom pipelines, or cleanup logic for NocoDB/Airtable in your business, reach out via [WhatsApp (+60 12-797 2969)](https://wa.me/60127972969) or [[email protected]](mailto:[email protected]).*
|
||||
@@ -7,6 +7,7 @@ tags: ["docker", "self-hosting", "upstream", "maintenance", "registry"]
|
||||
ogImage: "/og/the-upstream-was-deleted-then-came-back-rewritten.jpg"
|
||||
banner: "/banners/the-upstream-was-deleted-then-came-back-rewritten.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
`docker pull hectorqin/reader` returns **404**. That is the project my
|
||||
@@ -127,32 +128,14 @@ without a copy of the data.
|
||||
|
||||
## What I do now, before trusting any dependency with a running service
|
||||
|
||||
1. **Repository health is not artifact health.** `pushed_at`, stars and
|
||||
forks tell you the *source* is alive. `docker pull` on the exact tag you run
|
||||
tells you the *thing you deploy* still exists. Check both, and read the
|
||||
compose file to see which registry it names — mine now points at a domain I
|
||||
had never opened before this week. 2. **"No releases, no tags" is a
|
||||
deployment fact.** Zero tags means every upgrade is a commit hash or a
|
||||
moving branch. Write the SHA in your own notes, because the upstream will
|
||||
not. 3. **Mirror anything you cannot afford to lose — the source *and* the
|
||||
image.** A pinned tag in someone else's namespace is a dependency on that
|
||||
stranger's account staying alive. My surviving 3.x source sits in my own Git
|
||||
server now, and so does the rewrite. 4. **Compare the shape before the
|
||||
features.** Port, volume paths, variable names, database engine, where
|
||||
configuration lives. If most of those moved, treat the new version as a new
|
||||
product and plan a migration. 5. **Keep what you can't rebuild outside the
|
||||
image you don't control.** My landing page, social card, language switch and
|
||||
translation layer are a separate container in the same stack. That is why a
|
||||
rewrite landing upstream did not disturb a single reader — the front door
|
||||
cannot be broken by an app image swap. 6. **Read the maintainer's "not done
|
||||
yet" list, not the feature list.** The feature list is a sales page; the
|
||||
unfinished list is the risk register. 7. **Check the licence if you run it
|
||||
as a service.** This one is AGPL-3.0, which is worth reading properly if you
|
||||
host it for paying users rather than just yourself. 8. **An archived or
|
||||
wiped repository is not the same as an abandoned project.** For months the
|
||||
honest answer to "is this project dead?" was yes — the README said `deleted`
|
||||
and meant it. Then somebody started again, in another language, and the
|
||||
honest answer changed without the name changing.
|
||||
1. **Repository health is not artifact health.** `pushed_at`, stars and forks tell you the *source* is alive. `docker pull` on the exact tag you run tells you the *thing you deploy* still exists. Check both, and read the compose file to see which registry it names — mine now points at a domain I had never opened before this week.
|
||||
2. **"No releases, no tags" is a deployment fact.** Zero tags means every upgrade is a commit hash or a moving branch. Write the SHA in your own notes, because the upstream will not.
|
||||
3. **Mirror anything you cannot afford to lose — the source *and* the image.** A pinned tag in someone else's namespace is a dependency on that stranger's account staying alive. My surviving 3.x source sits in my own Git server now, and so does the rewrite.
|
||||
4. **Compare the shape before the features.** Port, volume paths, variable names, database engine, where configuration lives. If most of those moved, treat the new version as a new product and plan a migration.
|
||||
5. **Keep what you can't rebuild outside the image you don't control.** My landing page, social card, language switch and translation layer are a separate container in the same stack. That is why a rewrite landing upstream did not disturb a single reader — the front door cannot be broken by an app image swap.
|
||||
6. **Read the maintainer's "not done yet" list, not the feature list.** The feature list is a sales page; the unfinished list is the risk register.
|
||||
7. **Check the licence if you run it as a service.** This one is AGPL-3.0, which is worth reading properly if you host it for paying users rather than just yourself.
|
||||
8. **An archived or wiped repository is not the same as an abandoned project.** For months the honest answer to "is this project dead?" was yes — the README said `deleted` and meant it. Then somebody started again, in another language, and the honest answer changed without the name changing.
|
||||
|
||||
## The result
|
||||
|
||||
@@ -168,7 +151,7 @@ always the goal, and it never stopped being true. What changed is that
|
||||
And the useful lesson is not "pin your versions", which I already did. It is
|
||||
that **the question "is this project alive?" has three answers, and they can
|
||||
disagree**: the repository can be alive while the image is deleted; the
|
||||
README can say `deleted` while a rewrite is four weeks into existence; and
|
||||
README can say `deleted` while a rewrite is three weeks into existence; and
|
||||
the version number can stay the same while the runtime, the port, the
|
||||
database and the configuration model all change underneath it. Check each
|
||||
one separately, or you will eventually plan an upgrade for an application
|
||||
|
||||
@@ -9,6 +9,7 @@ tags: [unraid, swap, btrfs, zram, docker, user-scripts]
|
||||
ogImage: /og/unraid-stop-array-hangs-on-swapfile.jpg
|
||||
banner: /banners/unraid-stop-array-hangs-on-swapfile.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
I added an 8 GB swapfile to my Unraid server — my cache pool is a btrfs RAID1,
|
||||
@@ -155,23 +156,19 @@ it's immune to the `/proc/swaps` naming lie. And detaching *only* our loop
|
||||
device matters: a box can have other loop devices (mounted ISOs, other images)
|
||||
that must survive an array stop.
|
||||
|
||||
**Script 2 — "Array Start Swapon"** — schedule: **At First Array Start only**
|
||||
(fires at `disks_mounted`):
|
||||
**Script 2 — "Array Start Swapon"** — schedule: **At Startup of Array**
|
||||
(fires whenever the array starts and disks are mounted):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Re-create the loop-backed swap after the cache pool mounts.
|
||||
# Re-create the loop-backed swap after the cache pool mounts (idempotent).
|
||||
[ -f /mnt/cache/swapfile ] || exit 0
|
||||
losetup -j /mnt/cache/swapfile | grep -q . && exit 0
|
||||
LOOP=$(losetup -f)
|
||||
losetup -p 100 "$LOOP" /mnt/cache/swapfile && swapon -p 1 "$LOOP"
|
||||
```
|
||||
|
||||
Why "First Array Start only" and not plain "At Startup of the Array"? Because
|
||||
`/boot/config/go` also runs at boot and would set swap up a second time. The
|
||||
*first* array start after boot is covered by `go`; this script matters for the
|
||||
**GUI stop → start cycle with no reboot in between** — which `go` never sees.
|
||||
(In my setup I moved the swap block out of `go` entirely so the User Script is
|
||||
the single source of truth — one place, both paths.)
|
||||
Why "At Startup of Array" instead of "At First Array Start Only"? Because "First Array Start Only" fires once after a reboot and ignores subsequent **GUI stop → start cycles**. By removing the old swap block from `/boot/config/go` entirely and guarding the script with `losetup -j` idempotency, this User Script becomes the single source of truth — one place that covers both cold boots and GUI stop/start cycles cleanly.
|
||||
|
||||
After this: Stop → cache unmounts cleanly on the first try. Start → swap is
|
||||
back. Verified across multiple stop/start cycles and reboots.
|
||||
|
||||
@@ -8,6 +8,7 @@ tags: ["docker", "cdp", "playwright", "youtube", "portainer", "automation"]
|
||||
ogImage: "/og/uploading-large-videos-to-youtube-via-cdp.jpg"
|
||||
banner: "/banners/uploading-large-videos-to-youtube-via-cdp.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
Automating web uploads via headless browser or Chrome DevTools Protocol (CDP) works seamlessly for small JSON payloads, forms, and occasional screenshots. But when you attempt to upload a 100MB+ 1080p video file to YouTube through a remote browser session, standard browser automation approaches quickly collapse.
|
||||
@@ -81,12 +82,15 @@ The real question becomes: **How do you get a 130MB file into the container file
|
||||
Every Docker daemon exposes a container archive endpoint:
|
||||
`PUT /containers/{id}/archive?path=/path/in/container`
|
||||
|
||||
If you manage your Docker host with Portainer (standard in Synology and unRaid homelabs), Portainer proxies this Docker socket endpoint behind an API key:
|
||||
If you manage your Docker host with Portainer (a common control plane on self-hosted Synology and Unraid nodes), Portainer proxies this Docker socket endpoint behind an API key:
|
||||
|
||||
`PUT http://<host>:9000/api/endpoints/2/docker/containers/{container_id}/archive?path=/config/Downloads`
|
||||
|
||||
Here is a tested, production-ready staging script with proper error handling, parameter validation, and secure file modes:
|
||||
|
||||
<details>
|
||||
<summary><code>stage_file_to_container.py</code> — Hardened Portainer Docker Archive staging script (50 lines)</summary>
|
||||
|
||||
```python
|
||||
import io
|
||||
import os
|
||||
@@ -140,6 +144,8 @@ def stage_file_to_container(local_path: str, dest_filename: str) -> None:
|
||||
raise RuntimeError(f"Portainer Archive API error {e.code}: {body}") from e
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Operational Caveats & Security Checklist
|
||||
1. **Client RAM Footprint**: While Chrome experiences zero memory pressure, `io.BytesIO()` buffers the 130MB tar archive in the Python process memory. For multi-gigabyte video dumps, write the `.tar` to a temporary file and stream it using `requests` or `urllib3` chunked uploads.
|
||||
2. **Pre-existing Destination Directory**: The Docker Archive API extracts *into* the directory specified by `?path=`. If `/config/Downloads` does not exist in the container image, the API returns HTTP 404/500 rather than creating the parent folders.
|
||||
@@ -173,3 +179,11 @@ Because the file was already on disk inside the container, YouTube Studio reads
|
||||
- **Never stream large binaries over WebSocket CDP**. Remote browsers are not designed to buffer multi-hundred-megabyte payloads in memory.
|
||||
- **Stage files out-of-band**: Use Docker Archive PUT to drop files onto the container filesystem in seconds.
|
||||
- **Inject via filesystem path**: Call `DOM.setFileInputFiles` with the container path for instantaneous, zero-overhead file attachments.
|
||||
|
||||
---
|
||||
|
||||
## Need rock-solid browser or media-publishing automation without API quota walls?
|
||||
|
||||
I architect and maintain self-hosted browser automation and media pipelines — from persistent Dockerised Chrome fleets driven over CDP to end-to-end video rendering and publishing workflows. If your team has a high-volume upload, portal workflow, or multi-step operation that breaks standard headless scrapers, I can build a deterministic pipeline on your own infrastructure.
|
||||
|
||||
Reach me at [[email protected]](mailto:[email protected]?subject=Browser%20and%20media%20pipeline%20automation) or WhatsApp [+60 12-797 2969](https://wa.me/60127972969), or see what I do at [hoelee.com](https://hoelee.com).
|
||||
@@ -9,9 +9,10 @@ tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"]
|
||||
ogImage: "/og/vetting-an-open-source-dependency-before-you-bet-on-it.jpg"
|
||||
banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
I was handed a 1,300-line architecture spec for a partner program. It read like a
|
||||
I was handed a 1,306-line architecture spec for a partner program. It read like a
|
||||
plan: consistent, cross-referenced, confident. It named a self-hosted
|
||||
open-source engine as the attribution and commission layer, and it assumed three
|
||||
things about that engine — that it spoke OIDC so my existing identity provider
|
||||
@@ -160,8 +161,7 @@ into an afternoon of config.
|
||||
|
||||
## The result
|
||||
|
||||
Six assumptions corrected in about ten minutes of `curl` and `grep`, before a
|
||||
single line of integration code existed. The project stayed viable — but
|
||||
Six checks and about ten minutes of `curl` and `grep` corrected all three faulty assumptions in the spec before a single line of integration code existed. The project stayed viable — but
|
||||
reshaped: no OIDC, manual payouts, a pinned commit, and a plan that no longer
|
||||
depends on a feature sitting on an unmerged branch.
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
title: "When SMART Says Healthy and Your RAID Is Silently Corrupting Data"
|
||||
description: "A btrfs RAID1 pool on two healthy-looking NVMe drives quietly corrupted a Windows VM. How I caught it with a checksum forensic — and why the root cause (M.2 riser link fault vs batch defect) is still uncertain, with both drives now testing clean."
|
||||
pubDate: 2026-09-16
|
||||
updatedDate: 2026-09-18
|
||||
category: devops
|
||||
blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, btrfs, nvme, raid, storage, data-loss, troubleshooting]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
banner: /banners/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
---
|
||||
@@ -19,7 +22,7 @@ bytes. Not visibly failing. Silently corrupting.
|
||||
This is the full story: how the corruption surfaced, and the checksum that
|
||||
gave it away.
|
||||
|
||||
> **Update (2026-09-16): the root cause turned out to be uncertain.** The
|
||||
> **Update (2026-09-18): the root cause turned out to be uncertain.** The
|
||||
> "replace both drives" verdict below was my best diagnosis at the time, but a
|
||||
> later, unrelated failure taught me to question it. Both NVMe drives now test
|
||||
> clean, and the strongest remaining suspect is the **M.2 riser** both drives
|
||||
@@ -59,7 +62,7 @@ data, and verified every byte. But the correct lesson is "rule out the link
|
||||
before condemning the drive," not "same-batch drives are untrustworthy."
|
||||
|
||||
The full link-vs-drive detective work is in **["I Was About to RMA This SSD —
|
||||
the Fault Was a SATA Cable"](/posts/that-dying-ssd-was-just-a-bad-sata-cable/)**.
|
||||
the Fault Was a SATA Cable"](/posts/that-dying-ssd-was-just-a-bad-sata-cable/)**. And the ultimate root cause — queued TRIM firmware interaction triggering identical all-zero checksums at filesystem layer — is documented in **[The Corruption Came Back on Different Drives — the Cause Was TRIM, Not the SSDs](/posts/the-cause-was-trim-not-the-ssds/)**.
|
||||
|
||||
## The original analysis (kept for transparency)
|
||||
|
||||
@@ -236,4 +239,10 @@ lesson is broader and less dramatic than "replace the drives":
|
||||
diagnosis — is what turned a weeks-long silent corruption into a contained
|
||||
incident.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Unexplained storage pool checksum errors or VM crashes?
|
||||
|
||||
I design, audit, and troubleshoot on-premise storage nodes and btrfs/ZFS pools — separating controller and bus glitches from real drive failure before you waste downtime on RMA cycles. If your array is throwing I/O errors that SMART claims are fine, I can trace the failure path and establish verified checksum monitoring.
|
||||
|
||||
Reach me at [[email protected]](mailto:[email protected]?subject=Storage%20corruption%20troubleshooting) or WhatsApp [+60 12-797 2969](https://wa.me/60127972969), or see what I do at [hoelee.com](https://hoelee.com).
|
||||
+3
-4
@@ -19,7 +19,7 @@ Then I counted what survived. A clean scan of the current revision of my
|
||||
main production application returns 234,976 lines. The video transcoder that
|
||||
gained 58,240 lines in the same window is 43,050 lines long today.
|
||||
|
||||
So where did the other 280,000 lines go? Nothing was lost. They were
|
||||
Across the five repos, over 130,000 committed lines (plus another hundred thousand lines of pre-commit drafts) were ruthlessly pruned. Where did all those lines go? Nothing was lost. They were
|
||||
written, run, judged wrong, and deleted — or replaced by something smaller
|
||||
that did the same job. That loop, not the writing, is what I want to write
|
||||
down, because it changed how I work this year more than any model update
|
||||
@@ -159,9 +159,8 @@ things a model cannot check for me:
|
||||
allowed to cross.
|
||||
- **Test architecture** — the harness that catches semantic drift the type
|
||||
checker never sees.
|
||||
- **Pruning discipline** — the willingness to delete 280,000 lines of
|
||||
exploratory, redundant, or dead code so 235,000 lines can go to
|
||||
production.
|
||||
- **Pruning discipline** — the willingness to prune over 130,000 lines of
|
||||
committed scaffolding and redundant abstractions so the core production app stays lean at 235,000 lines.
|
||||
|
||||
Which is why I no longer read a commit graph as a productivity chart. Half a
|
||||
million added lines in a month tells me the tools were running. The 130,000
|
||||
|
||||
@@ -7,6 +7,7 @@ tags: [docker, chrome, cdp, persistence, synology, self-hosting]
|
||||
ogImage: /og/why-chrome-forgets-its-tabs-in-a-container.jpg
|
||||
banner: /banners/why-chrome-forgets-its-tabs-in-a-container.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
I run a real Google Chrome inside a Docker container on my NAS. It is not a scraping instance and not a screenshot service — it is a browser I actually use: signed into the accounts a headless bot should never touch, watchable through a web desktop from any device, and drivable over the Chrome DevTools Protocol when I want to automate part of a job.
|
||||
@@ -119,6 +120,9 @@ If the browser cannot be trusted to remember its own session, the container can
|
||||
|
||||
That is a background script launched from the image's container-init hook. The version I run, trimmed to the useful parts:
|
||||
|
||||
<details>
|
||||
<summary>tabs_keeper.py — Tab restore & snapshot loop (42 lines)</summary>
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Restore last session's tabs on start, then snapshot open tabs every 60s."""
|
||||
@@ -163,6 +167,8 @@ while True:
|
||||
os.replace(tmp, STORE) # atomic; a kill can't truncate it
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Two design choices worth copying:
|
||||
|
||||
- **Restore only when the browser came up empty.** If a future Chrome version *does* restore its own tabs, the keeper stays out of the way instead of duplicating every tab.
|
||||
|
||||
@@ -179,3 +179,13 @@ We verified the output through our local Whisper/SenseVoice ASR engine:
|
||||
The "percent" loop vanished, replaced by crisp, natural speech.
|
||||
|
||||
When building AI pipelines that bridge HTTP gateways with neural text frontends, never assume the string arriving at your API boundary is in its final semantic form. Defensive decoding at both ends of the bridge costs fractions of a millisecond—and prevents your AI from reading URL headers as bedtime stories.
|
||||
|
||||
---
|
||||
|
||||
## Need Production-Grade Local AI Audio Pipelines?
|
||||
|
||||
I build and operate local GPU-accelerated speech pipelines (TTS/ASR), automated n8n workflows, and private inference services for teams requiring high reliability and complete data sovereignty:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Local%20AI%20speech%20pipeline%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -99,4 +99,14 @@ Multi-agent architecture is exceptional when tasks satisfy three criteria:
|
||||
2. **Clear Schema Boundaries**: Subagents generate isolated assets (e.g., an SVG diagram or an image via local diffusion) that are written to deterministic paths and returned as static handles.
|
||||
3. **Adversarial Gatekeeping**: A fresh, unpolluted reasoning model reviews the single writer's output without participating in the authoring struggle.
|
||||
|
||||
Restrict file writes to a single thread, use subagents strictly for read-only parallel work, and your pipeline will ship code reliably without the chaos.
|
||||
Restrict file writes to a single thread, use subagents strictly for read-only parallel work, and your pipeline will ship code reliably without the chaos. For a deep architectural breakdown and quantified production benchmarks, see: **[The Single-Writer Rule: Orchestrating Coding Agents Without Multi-Agent Chaos](/posts/the-single-writer-rule-for-coding-agents/)**.
|
||||
|
||||
---
|
||||
|
||||
## Want to Deploy Reliable AI Agents in Your Business?
|
||||
|
||||
I architect, build, and operate autonomous AI agent infrastructure, custom developer toolchains, and deterministic automation for teams looking to move past toy demonstrations:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=AI%20agent%20architecture%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -7,12 +7,14 @@ category: devops
|
||||
blueprint: ai-agent-automation
|
||||
blueprintLayer: L1-deterministic-orchestration
|
||||
tags: [docker, telegram, python, dns, ipv6, debugging, portainer]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/why-telegram-bot-notifications-die.jpg
|
||||
banner: /banners/why-telegram-bot-notifications-die.jpg
|
||||
---
|
||||
|
||||
I run a small monitoring bot that watches Carousell for newly-listed
|
||||
listings and pings me on Telegram when something fresh appears. One day it
|
||||
I run an automated monitoring pipeline that watches Carousell for newly-listed
|
||||
items and pings me on Telegram when something fresh appears. One day it
|
||||
stopped pinging. The container was healthy, the scraper was still archiving
|
||||
listings into NocoDB — but every notification died with the same three
|
||||
characters in the log:
|
||||
@@ -103,7 +105,7 @@ the photo and it was a perfectly valid JPEG. Nothing in the code differed
|
||||
between the two paths.
|
||||
|
||||
When you hit a contradiction like that — "identical inputs, opposite
|
||||
outputs" — the answer is usually that the inputs *aren't* identical. But
|
||||
outputs" — the answer is usually that the inputs *aren't* identical. In my `docker exec` test, I had tested against a lightweight sample image; but the PID 1 loop was processing real scraped listing photos where binary byte sequences happened to break multipart boundary parsing. The binary payloads were not the same. But
|
||||
finding the difference took one more frame change.
|
||||
|
||||
### Round 4: `nginx/1.30.1` is Telegram
|
||||
@@ -124,45 +126,52 @@ And the malformed thing was the one part I hadn't scrutinized: the
|
||||
|
||||
## The fix
|
||||
|
||||
My original code built the multipart body like this — the kind of
|
||||
"obviously fine" line that ships a subtle bug:
|
||||
My original helper built the multipart payload by treating the assembled chunks as text lines and joining them with `b"
|
||||
\n".join(body_lines)` — the kind of "obviously fine" shortcut that ships a subtle bug:
|
||||
|
||||
```python
|
||||
body = b"\r\n".join(body_lines)
|
||||
body = b"
|
||||
\n".join(body_lines)
|
||||
```
|
||||
|
||||
That `join` looks convenient, but a JPEG is **binary data**. Its bytes include
|
||||
`\r\n` byte sequences in the middle of the image data. Joining with `\r\n`
|
||||
as the delimiter corrupts the exact bytes that are supposed to be the image
|
||||
payload. Telegram receives a body whose multipart boundaries are broken, so
|
||||
its nginx rejects the request with a 400 before the bot API ever parses it.
|
||||
A JPEG is **binary data**, and its compressed byte stream naturally contains raw `0x0A` (`\n`) and `0x0D` (`
|
||||
`) bytes in the middle of the image. Splitting or normalising line endings across a buffer that holds binary data and re-joining with `
|
||||
\n` mutates single newline bytes into `
|
||||
\n` pairs, corrupting the payload length and throwing off the closing multipart boundary. That also explained the Round 3 mystery: my manual `docker exec` test used a tiny synthetic test payload (or URL pass-through) that didn't trip the framing bug, whereas `PID 1` was feeding in real marketplace JPEGs! Telegram's edge nginx saw a broken multipart boundary and rejected the request with a 400 before the bot API ever parsed it.
|
||||
|
||||
The correct approach is to assemble the body field by field, keeping binary
|
||||
data untouched:
|
||||
The correct approach is to append discrete byte chunks into a list and concatenate them verbatim with `b"".join(parts)`:
|
||||
|
||||
```python
|
||||
boundary = "----tg" + token_hex(8)
|
||||
|
||||
parts = []
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(b'Content-Disposition: form-data; name="chat_id"\r\n\r\n')
|
||||
parts.append(f"{chat_id}\r\n".encode())
|
||||
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(b'Content-Disposition: form-data; name="caption"\r\n\r\n')
|
||||
parts.append(f"{caption}\r\n".encode())
|
||||
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(
|
||||
parts = [
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="chat_id"
|
||||
\n
|
||||
\n',
|
||||
f"{chat_id}
|
||||
\n".encode(),
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="caption"
|
||||
\n
|
||||
\n',
|
||||
f"{caption}
|
||||
\n".encode(),
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="photo"; '
|
||||
b'filename="image.jpg"\r\nContent-Type: image/jpeg\r\n\r\n'
|
||||
)
|
||||
parts.append(image_bytes) # ← binary stays binary
|
||||
parts.append(b"\r\n")
|
||||
|
||||
parts.append(f"--{boundary}--\r\n".encode())
|
||||
|
||||
body = b"".join(parts) # join bytes, not lines
|
||||
b'filename="image.jpg"
|
||||
\nContent-Type: image/jpeg
|
||||
\n
|
||||
\n',
|
||||
image_bytes, # ← binary stays untouched
|
||||
b"
|
||||
\n",
|
||||
f"--{boundary}--
|
||||
\n".encode(),
|
||||
]
|
||||
body = b"".join(parts) # concatenate raw byte chunks
|
||||
```
|
||||
|
||||
The difference is `b"".join(parts)` instead of `b"\r\n".join(...)`: each part
|
||||
@@ -207,8 +216,7 @@ turned into exactly the kind of bug I'll never trip over twice.
|
||||
|
||||
---
|
||||
|
||||
*The bot is a personal project, but the lesson generalizes to any service
|
||||
that relies on outbound notifications. If you've got a monitor, a scraper, or
|
||||
*This monitoring pipeline was originally built for high-frequency internal inspection, but the troubleshooting lessons apply to any production service that relies on outbound notifications. If you've got a monitor, a scraper, or
|
||||
an alert pipeline that needs to reach people reliably, let's talk.*
|
||||
|
||||
**I build reliable notification and monitoring pipelines — Telegram/WhatsApp
|
||||
|
||||
@@ -136,4 +136,14 @@ Output:
|
||||
Notification sent: "Windows-MCP Ready" - Immediate execution verified.
|
||||
```
|
||||
|
||||
No 10-second wait, no `CONTROL_PREEMPTED`, and no silent drops.
|
||||
No 10-second wait, no `CONTROL_PREEMPTED`, and no silent drops. For more architectural nuance on when to rely on desktop UI drivers versus underlying APIs, see: **[When to Give Your Agent Physical Clicks (and When to Never, Ever Do It)](/posts/when-to-give-your-agent-physical-clicks/)**.
|
||||
|
||||
---
|
||||
|
||||
## Need Resilient Desktop UI or Systems Automation?
|
||||
|
||||
If you are bridging legacy desktop applications lacking APIs and need robust, audit-proven agent workflows combining MCP and UI automation:
|
||||
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Desktop%20automation%20%26%20MCP%20consulting)
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
@@ -83,3 +83,13 @@ TXT _dmarc → (策略 + 报告地址)
|
||||
2. **在向任何人承诺邮件之前,先查发信 IP 的反向记录。** 一次反向查询就能告诉我接下来会撞上哪堵墙。
|
||||
3. **把「收件侧拉取」当一等设计,而不是权宜之计。** 「这封信根本不用离开我的服务器」一次消掉三大类故障。
|
||||
4. **把那四条收信记录当成一张清单。** 那一半很无聊、可验证,而且是唯一一次就通的部分。
|
||||
|
||||
---
|
||||
|
||||
## 需要为企业定制高送达率的独立邮件系统?
|
||||
|
||||
如果你正在为公司域名搭建企业邮箱、解决发信进垃圾箱或被退信(SPF/DKIM/DMARC/PTR 校验失败),或者需要平滑迁移旧邮箱历史数据,我提供完整的企业邮件架构与代维服务(SifuMail):
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Mail%20server%20%26%20deliverability%20consulting)
|
||||
- **SifuMail 托管**:[sifumail.com](https://sifumail.com)
|
||||
@@ -125,3 +125,13 @@ medium: 168,435,266 B 编码 360 秒 mean SSIM 0.9960
|
||||
## 结果
|
||||
|
||||
QSV 从「存在于代码里、永远选不到」变成了一个带公开数字的已标定档,安全规则也跟着改了:原来那条钉住「**出货默认值必须代表未标定**」的测试,现在钉的是「**出货默认值必须是量过的值**」,同时保留「空值 = 硬排除」的全部断言。在没装 Intel 显卡的机器上,doctor 那行也从信息级的「等硬件」变成了警告——这本身是另一个小教训:一个已标定的编码器自检失败该长什么样,以及为什么机器上不存在的硬件应该进一行汇总,而不是进警告。
|
||||
|
||||
---
|
||||
|
||||
## 需要构建高确定性的硬件转码与自动化质检流水线?
|
||||
|
||||
如果你正在自建或运营基于 GPU/iGPU(NVENC、Quick Sync、VAAPI)的大规模音视频转码集群,并希望建立严格的质量底线门禁(SSIM/VMAF)防范劣质产物入库:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Media%20pipeline%20%26%20transcoding%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
@@ -59,7 +59,7 @@ ffmpeg -i a.yuv -i b.yuv -lavfi ssim -f null -
|
||||
|
||||
**2. 缩小的幅度取决于源的效率,而不是码率。** 样本 1 和样本 2 的 bpp(每像素每秒位数)完全相同——都是 0.0437——一个缩了 49%,另一个只缩了 17%。720p 那个本身已经是不错的编码,1080p 那个是"肥"的。同样的 bpp,不同的余量。bpp 仍是最便宜的第一道筛子(它告诉了我该重编码**哪些**文件),但它预测不了比例。
|
||||
|
||||
**3. 同画质下软件 x265 比 NVENC 更小。** 比较 SSIM 接近的那两行:cq31 vs crf26(在 720p 文件上 min SSIM 0.9894 vs 0.9927)——72.9% vs 56.7%,也就是说**文件小了 22%,而量出来的画质更高**。整体看,多出来的节省大约是 **10–22%**。这不是说 NVENC 编得差,而是它为吞吐量做了优化。约束是磁盘就花 CPU,约束是时间就花 GPU。
|
||||
**3. 同画质下软件 x265 比 NVENC 更小。** 比较 SSIM 处于同一水平的那两行:cq31 vs crf26——在 720p 文件上是 72.9% vs 56.7%(min SSIM 0.9894 vs 0.9879),在 1080p 音乐文件上是 43.0% vs 33.7%(min SSIM 0.9941 vs 0.9927),**同等画质档位下文件足足小了 22%**;而在竖屏文件上,x265 甚至以**更高的实测画质**(0.9913 vs 0.9890)产出了更小的体积(67.0% vs 74.3%)。整体看,多出来的节省大约是 **10–22%**。这不是说 NVENC 编得差,而是它为吞吐量做了优化。约束是磁盘就花 CPU,约束是时间就花 GPU。
|
||||
|
||||
**4. 音频会成为地板。** 样本 1 的视频流降到了源视频码率的 33.7%——但在全片测试里,**整个文件**只降到原体积的 53.1%,因为在视频降下来之后,AAC 音轨(跟随源的音频码率,因为 iOS 要求 MP4 里是 AAC)大约占了输出总码率的 43%。视频压到这个程度之后,音轨就是下一个杠杆——而且是个小得多的杠杆。
|
||||
|
||||
@@ -87,3 +87,13 @@ ffmpeg -i a.yuv -i b.yuv -lavfi ssim -f null -
|
||||
- **"肥"档**(x265,crf26):落在 **33–44%**,中位数 **39%**。
|
||||
|
||||
这就是"HEVC 会不会让我的文件变小"的诚实答案:取决于你在重编码什么,而唯一知道你那个数字的办法,就是在你自己的文件上、把画质固定住去测一遍。
|
||||
|
||||
---
|
||||
|
||||
## 需要构建高确定性的硬件转码与自动化质检流水线?
|
||||
|
||||
如果你正在自建或运营基于 GPU/iGPU(NVENC、Quick Sync、VAAPI)的大规模音视频转码集群,并希望建立严格的质量底线门禁(SSIM/VMAF)防范劣质产物入库:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Media%20pipeline%20%26%20transcoding%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
@@ -37,4 +37,4 @@ banner: /banners/hello-world.jpg
|
||||
不是主业)。
|
||||
|
||||
这里的文章是我的动态作品集,不是推销页:我做了什么、什么坏了、证据到底说了什么。
|
||||
可以从[最新文章](/posts/zh/)开始看。
|
||||
可以从[最新文章](/posts/zh/)开始看。有任何技术交流或业务合作,欢迎通过 [WhatsApp (+60 12-797 2969)](https://wa.me/60127972969) 或 [[email protected]](mailto:[email protected]) 直接联系。
|
||||
@@ -95,3 +95,13 @@ DSM 文件夹: 在 -> 不在 回收站: 0 B
|
||||
## 结果,诚实地说
|
||||
|
||||
管道是通的:计划现在住在 Google Drive 上、仓库身份没变、种子花的是**我的**配额而不是共享池、旧的那一侧退回了大约 1 TB。但它现在**还不是一个备份**:写这篇的时候,第一轮种子才上传了约 1075 GB 里的 2.88 GB,而且新仓库里**快照数为零**——在第一张快照落地之前,什么都还原不了。那是我盯着的数字,因为「传输在跑」和「你能还原」是两句不同的话。
|
||||
|
||||
---
|
||||
|
||||
## 需要规划可靠的异地加密容灾备份架构?
|
||||
|
||||
备份不是「把脚本跑起来」,而是「在灾难发生时能确凿还原」。如果你在管理 NAS、数据库或企业关键资产,需要搭建兼顾加密、增量去重与异地容灾的自动化备份流水线:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Disaster%20recovery%20%26%20backup%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
@@ -143,7 +143,7 @@ curl -s 'https://api.github.com/repos/brunostjohn/nocodb-oidc/commits?per_page=1
|
||||
|
||||
如果你正在评估自托管软件、需要内部工具前面的单点登录,或者想让别人去核对你的技术栈实际做了什么、对照文档声称它做了什么——这正是我做的工作。
|
||||
|
||||
- **WhatsApp:** [011-797 2969](https://wa.me/60127972969) ——点一下就能聊
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969) ——点一下就能聊
|
||||
- **Email:** [[email protected]](mailto:[email protected]?subject=Self-hosted%20SSO%20enquiry)
|
||||
- **Website:** [hoelee.com](https://www.hoelee.com)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=r
|
||||
| python -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
|
||||
# manifest (follow the platform entry if it's a multi-arch index)
|
||||
curl -s -H "Authorization: Bearer ***" \
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
|
||||
"https://registry-1.docker.io/v2/$REPO/manifests/$TAG" \
|
||||
| python -c "import json,sys; m=json.load(sys.stdin); print(m['config']['digest'])"
|
||||
@@ -110,7 +110,7 @@ curl -s -H "Authorization: Bearer ***" \
|
||||
然后取回那个 config blob,把关心的字段打印出来:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer ***" \
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"https://registry-1.docker.io/v2/$REPO/blobs/<config-digest>" \
|
||||
| python -c "
|
||||
import json,sys
|
||||
|
||||
@@ -10,7 +10,7 @@ banner: /banners/smartctl-exit-code-32-skips-the-disks-that-matter.jpg
|
||||
draft: false
|
||||
---
|
||||
|
||||
我的硬盘健康采集脚本连续几周都是\"全绿\"。每块盘都有温度、通电小时数和 SMART 结论,指标看起来完整——直到我数了一下只有 **4 块 SSD 里的 2 块**。
|
||||
我的硬盘健康采集脚本连续几周都是「全绿」。每块盘都有温度、通电小时数和 SMART 结论,指标看起来完整——直到我数了一下只有 **4 块 SSD 里的 2 块**。
|
||||
|
||||
不是故障,是**消失了**。脚本认定这两块盘不存在。
|
||||
|
||||
@@ -44,11 +44,11 @@ done
|
||||
| 6 | 64 | 设备错误日志中有记录 |
|
||||
| 7 | 128 | 自检日志中有失败记录 |
|
||||
|
||||
所以 `32` 的意思和\"读不到盘\"正好相反:**现在没问题,但它曾经踩过阈值。** 这正是应该持续盯着的那类盘——而我的脚本把它们丢掉了。
|
||||
所以 `32` 的意思和「读不到盘」正好相反:**现在没问题,但它曾经踩过阈值。** 这正是应该持续盯着的那类盘——而我的脚本把它们丢掉了。
|
||||
|
||||
消失的两块盘,恰好是原始属性长期处在边缘值的那两块;退出码为 `0` 的\"健康盘\"被正常采集。**这个过滤器实际上筛选出了\"没有任何历史可报告\"的盘。**
|
||||
消失的两块盘,恰好是原始属性长期处在边缘值的那两块;退出码为 `0` 的「健康盘」被正常采集。**这个过滤器实际上筛选出了「没有任何历史可报告」的盘。**
|
||||
|
||||
## 修法:把\"信息位\"掩掉
|
||||
## 修法:把「信息位」掩掉
|
||||
|
||||
对监控来说,位 32 和 64 是信息;位 8 和 16 才是该告警的。掩掉信息位,只对剩下的做判断:
|
||||
|
||||
@@ -73,13 +73,13 @@ echo "disk_smart_health{device=\"$dev\"} $(( rc & 8 ? 0 : 1 ))"
|
||||
|
||||
1. **退出码变成数据,而不是控制流。** 它作为指标被导出,磁盘从 `0` → `32` → `64` 的漂移会变成趋势,而不是一次静默跳过。
|
||||
2. **信息位不再致命。** 有历史的盘被采集,这才是监控它们的目的。
|
||||
3. **NAS 上 `-d sat` 很关键。** 在 Synology DSM(以及某些 USB 桥接)上,设备类型不对时 `smartctl` 拿不到有用输出——症状同样是\"盘消失\",原因却不同。
|
||||
3. **NAS 上 `-d sat` 很关键。** 在 Synology DSM(以及某些 USB 桥接)上,设备类型不对时 `smartctl` 拿不到有用输出——症状同样是「盘消失」,原因却不同。
|
||||
|
||||
## 我下次会怎么做
|
||||
|
||||
- **面对有\"退出码位域\"文档的工具,永远不要写 `if ! cmd`。** 先把手册里的退出码那节读完。
|
||||
- **除了\"采集器有没有跑\",还要数\"采到了几块盘\"。** 我的告警是\"采集器过期\",真正的 bug 是\"采集器正常跑了,只报了 50% 的盘\"。一个 `disk_count` 指标就能立刻暴露。
|
||||
- **把\"这个设备没有数据\"本身当成可告警状态。** 缺失的序列是不可见的,所以它存活了好几周。
|
||||
- **面对有「退出码位域」文档的工具,永远不要写 `if ! cmd`。** 先把手册里的退出码那节读完。
|
||||
- **除了「采集器有没有跑」,还要数「采到了几块盘」。** 我的告警是「采集器过期」,真正的 bug 是「采集器正常跑了,只报了 50% 的盘」。一个 `disk_count` 指标就能立刻暴露。
|
||||
- **把「这个设备没有数据」本身当成可告警状态。** 缺失的序列是不可见的,所以它存活了好几周。
|
||||
|
||||
## 结果
|
||||
|
||||
@@ -89,7 +89,7 @@ echo "disk_smart_health{device=\"$dev\"} $(( rc & 8 ? 0 : 1 ))"
|
||||
|
||||
## 需要为你的业务做这个吗?
|
||||
|
||||
如果你在跑 NAS 或服务器机架,想让硬盘健康真正\"会通知你\"——SMART 属性、温度、btrfs/RAID 错误计数,并推到 Telegram 或邮件——这类自托管监控流水线我可以帮你搭。
|
||||
如果你在跑 NAS 或服务器机架,想让硬盘健康真正「会通知你」——SMART 属性、温度、btrfs/RAID 错误计数,并推到 Telegram 或邮件——这类自托管监控流水线我可以帮你搭。
|
||||
|
||||
**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Disk%20health%20monitoring)** · **[hoelee.com](https://hoelee.com)**
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ tags: ["synology", "btrfs", "snapshots", "storage", "nas"]
|
||||
ogImage: /og/zh-space-reclamation-wasnt-stuck-it-was-waiting.jpg
|
||||
banner: /banners/space-reclamation-wasnt-stuck-it-was-waiting.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
## 我问错了问题
|
||||
@@ -30,7 +31,7 @@ draft: false
|
||||
| 16:35 → 17:39 | **−39 GiB** | 新数据写入(一阵爆发) |
|
||||
| 15:00 → 20:44 | **−47.2 GiB** | 净值:这个卷那天下午**丢**了空间 |
|
||||
|
||||
两次回收,合计约 121 GiB;然后 47 GiB 的新写入把下午吃掉了。「什么都没回来」是我这边的读法错误:我拿早上和晚上比,而晚上里面多了一整天的写入。
|
||||
两次早间快照轮转(115.8 GiB)外加一次清空回收站(5.0 GiB),合计收回约 121 GiB;然后 47 GiB 的新写入把下午吃掉了。「什么都没回来」是我这边的读法错误:我拿早上和晚上比,而晚上里面多了一整天的写入。
|
||||
|
||||
我特意手动清空的回收站是 **5 GiB**;我压根没想到的快照是 **115.8 GiB**。这个比例就是全部故事。
|
||||
|
||||
@@ -80,4 +81,12 @@ volume2 16 TB 已用 100% 剩 70 GB 约 -19 GiB / 2 天
|
||||
read/write_io_errs 与九月基线一致
|
||||
```
|
||||
|
||||
所以有用的结论不是「回收是好的」。而是:**相对于写入速率,回收只是一个舍入误差**;在保留策略为 1 的前提下,删除并不是我以为的那根杠杆;而该拿来做告警的是趋势,不是那张任务卡。这跟[《你的磁盘快满告警在撒谎》](/posts/your-disk-full-alert-is-lying/)是同一课,只是从另一个方向来的:对**后果**告警(随时间变化的可用字节),因为机制自己的状态不是证据。
|
||||
所以有用的结论不是「回收是好的」。而是:**相对于写入速率,回收只是一个舍入误差**;在保留策略为 1 的前提下,删除并不是我以为的那根杠杆;而该拿来做告警的是趋势,不是那张任务卡。这跟[《你的磁盘快满告警在撒谎》](/posts/zh/your-disk-full-alert-is-lying/)是同一课,只是从另一个方向来的:对**后果**告警(随时间变化的可用字节),因为机制自己的状态不是证据。
|
||||
|
||||
---
|
||||
|
||||
## 存储告警总是滞后,或者容量去向成谜?
|
||||
|
||||
我负责管理与加固企业级 NAS、btrfs/ZFS 存储池与无人值守备份基础设施——把模糊的面板状态替换成基于真实容量趋势、快照生命周期和底层健康计数器的可观测告警。如果你的存储节点正面临容量激增或性能瓶颈,我可以帮你做一次彻底的容量与健康审计。
|
||||
|
||||
欢迎联系 [[email protected]](mailto:[email protected]?subject=Storage%20and%20NAS%20audit) 或 WhatsApp [+60 12-797 2969](https://wa.me/60127972969),也可以在 [hoelee.com](https://hoelee.com) 查看我的服务内容。
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "密码正确,Synology API 登录却返回 401,问题出在哪"
|
||||
description: "两种原因,同一个报错:Office Suite API 对 TLS 握手失败和受 2FA 保护的账号返回完全相同的错误。三条命令就能区分,不必折腾一下午。"
|
||||
description: "两种原因,同一个报错:Office Suite API 对 TLS 握手失败和受 2FA 保护的账号返回完全相同的错误。三条命令就能在一分钟内精准区分,省去一下午的盲目排查。"
|
||||
pubDate: 2025-09-10
|
||||
updatedDate: 2026-09-26
|
||||
category: notes
|
||||
|
||||
@@ -10,6 +10,7 @@ tags: [synology, dsm, docker, portainer, rest-api, spreadsheet, debugging, self-
|
||||
ogImage: /og/zh-synology-spreadsheet-api-is-a-container.jpg
|
||||
banner: /banners/synology-spreadsheet-api-is-a-container.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
我想要的东西很普通:从我 NAS 上的电子表格读一个单元格,把一个单元格写回去,然后从另一端得到一张图表——不用打开 Excel,也不用把公司的数据送到某个云 API。
|
||||
@@ -232,11 +233,11 @@ TOKEN=$(curl -s -X POST http://192.168.1.1:8791/spreadsheets/authorize \
|
||||
| python -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
|
||||
# read a range (sheet-qualified A1 notation)
|
||||
curl -s -H "Authorization: Bearer ***" \
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"http://192.168.1.1:8791/spreadsheets/<id>/values/Sheet1!A1:C4"
|
||||
|
||||
# write a range, formulas included
|
||||
curl -s -X PUT -H "Authorization: Bearer ***" -H 'Content-Type: application/json' \
|
||||
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"values": [["Item","Qty"],["Widget",12],["Gadget",7],["Total","=SUM(B2:B3)"]]}' \
|
||||
"http://192.168.1.1:8791/spreadsheets/<id>/values/Sheet1!A1:B4"
|
||||
```
|
||||
@@ -253,7 +254,7 @@ curl -s -X PUT -H "Authorization: Bearer ***" -H 'Content-Type: application/json
|
||||
读单元格只是半件事;最初的要求里还有图表。同一个 API 能直接给你一份真正的 workbook:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer ***" \
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"http://192.168.1.1:8791/spreadsheets/<id>/xlsx" -o export.xlsx
|
||||
```
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, sata, ssd, storage, troubleshooting, smart, fio, cable]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/zh-that-dying-ssd-was-just-a-bad-sata-cable.jpg
|
||||
banner: /banners/that-dying-ssd-was-just-a-bad-sata-cable.jpg
|
||||
---
|
||||
@@ -19,7 +20,7 @@ banner: /banners/that-dying-ssd-was-just-a-bad-sata-cable.jpg
|
||||
|
||||
## 为什么这很重要
|
||||
|
||||
一根有问题的 SATA 口或线材,会产生**和坏盘一模一样的症状**,而且 SMART 全程保持漂亮。线材故障发生在链路层——传输出错超时,内存在你的文件系统或 SMART 看到任何错误字节之前,就把这次 I/O 丢了。只靠 SMART 判断,你会冤枉健康硬件、发起一趟无意义的 RMA,而换上的新盘在同一根线上照样坏。
|
||||
一根有问题的 SATA 口或线材,会产生**和坏盘一模一样的症状**,而且 SMART 全程保持漂亮。线材故障发生在链路层——传输出错超时,内核在你的文件系统或 SMART 看到任何错误字节之前,就把这次 I/O 丢了。只靠 SMART 判断,你会冤枉健康硬件、发起一趟无意义的 RMA,而换上的新盘在同一根线上照样坏。
|
||||
|
||||
## 那些"盘要死了"的症状
|
||||
|
||||
@@ -81,4 +82,12 @@ fio --name=verify --filename=/dev/sdd \
|
||||
|
||||
这颗几分钟前就要被我送去 RMA 的盘,现在已经是一颗格式化好、挂载上、verify 全过的缓存 SSD,`btrfs device stats` 全部清零。修好它的成本:一根线。相信原有诊断的成本:一趟没必要的保修,还有一台迟早会在那根坏线上损坏数据的机器。
|
||||
|
||||
如果你在 NAS 或自管服务器上遇到一颗盘报 FPDMA 超时、但 SMART 看起来没问题——换线,别急着换盘。
|
||||
如果你在 NAS 或自管服务器上遇到一颗盘报 FPDMA 超时、但 SMART 看起来没问题——换线,别急着换盘。
|
||||
|
||||
---
|
||||
|
||||
## 存储节点频繁掉盘或报出诡异的 I/O 错误?
|
||||
|
||||
我负责排查与维护自托管生产节点、Unraid/Synology 存储阵列与高可用虚拟化基础设施——在盲目更换硬件之前,通过链路取证与带校验的基准测试定位真正的故障点。如果你的服务器正面临不明原因的读写超时或阵列降级,欢迎联系我介入诊断。
|
||||
|
||||
联系我:[[email protected]](mailto:[email protected]?subject=Storage%20I/O%20and%20RAID%20troubleshooting) 或 WhatsApp [+60 12-797 2969](https://wa.me/60127972969),或访问 [hoelee.com](https://hoelee.com)。
|
||||
@@ -3,12 +3,16 @@ title: "换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,
|
||||
description: "两周前 btrfs RAID1 阵列在看似健康的 NVMe 盘上静默损坏数据,两周后同一个全零校验和出现在完全不同的硬盘上。真凶是 Seagate IronWolf 110 上的队列化 TRIM(NCQ)——FreeBSD 早就拉黑了这个固件缺陷,Linux 却一直没有。我是怎么抓到它、以及修复方法的全过程。"
|
||||
pubDate: 2026-09-18
|
||||
category: devops
|
||||
blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, btrfs, trim, ssd, raid, data-loss, smart, troubleshooting, seagate]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/zh-the-cause-was-trim-not-the-ssds.jpg
|
||||
banner: /banners/the-cause-was-trim-not-the-ssds.jpg
|
||||
---
|
||||
|
||||
两周前我写了一篇文章,讲一个 btrfs RAID1 阵列如何在两块看起来完全健康的三星 PM9A3 NVMe 盘上静默损坏 Windows 虚拟机——读出来的全是**全零**数据块,单靠一块坏盘根本无法解释。那次调查以一句诚实的「不确定」收尾:两块盘测试都干净,最大嫌疑是它们共用的 M.2 转接卡。
|
||||
在上一篇记录里,我复盘了大约两周前的一桩怪事:一个建在两块看似健康的 Samsung PM9A3 NVMe 盘上的 btrfs RAID1 阵列,静默损坏了 Windows 虚拟机——读出来的全是**全零**数据块,单靠一块坏盘根本无法解释。那次调查以一句诚实的「不确定」收尾:两块盘事后测试都干净,最大嫌疑落在了它们共用的 M.2 转接卡上。
|
||||
|
||||
然后它又发生了。在**不同的硬盘**上,走**不同的总线**。校验和指纹一模一样。这一次我找到了根因——而且跟 SSD 本身无关。
|
||||
|
||||
@@ -74,10 +78,7 @@ btrfs: checksum verify failed on logical 1505629372416 mirror 1 wanted 0x8941f99
|
||||
在**两块盘的相同逻辑偏移上同时读出**。我翻了 PM9A3 阵列那次的笔记:值相同,
|
||||
「确定性全零」行为相同,让 RAID1 失效的关联方式也相同。
|
||||
|
||||
这个匹配是指纹级的。无论这是什么,它都不是硬盘的问题,也不是 M.2 转接卡的
|
||||
问题——IronWolf 这对盘走的是 **SATA 口**,完全不同的控制器路径。问题出在
|
||||
**比硬盘高一层**:有一个机制决定同时擦掉两块盘上相同的数据。Linux 里恰好
|
||||
只有一个机制干这事:**discard / TRIM**。
|
||||
这个匹配揭开了谜底的一半:只要任何底层机制把一个 4 KiB 块清零(或者返回全零),它的 CRC32C 就必然是 `0x8941f998`。但在 IronWolf 这对盘上,它们走的是主板原生 **SATA 口**,根本没有 M.2 转接卡,而且前面刚过了 `fio --verify=crc32c` 写校验。要在没有链路报错的情况下,同时把两块镜像盘的相同逻辑偏移抹成全零,说明问题出在**文件系统向两块盘同步下发擦除指令的那一层**。Linux 里恰好只有一个机制干这事:**discard / TRIM**。
|
||||
|
||||
## 第三步——根因:在 Linux 从未拉黑的硬盘上跑队列化 TRIM
|
||||
|
||||
@@ -143,8 +144,7 @@ read_errors: 0 verify_errors: 0 super_errors: 0
|
||||
|
||||
还有一个值得知道的坑:**scrub 跑了 18 个错误,`btrfs device stats` 计数器
|
||||
却纹丝不动**——scrub 发现的错误和读取时发现的错误是分开统计的。你没法只盯
|
||||
`device stats` 一个数;dmesg 和 scrub 输出也得一起盯。(我的看门狗三样都查,
|
||||
细节见文末。)
|
||||
`device stats` 一个数;dmesg 和 scrub 输出也得一起盯(我现在用一条每日定时看门狗脚本把这三项指标全部纳入告警)。
|
||||
|
||||
## 换成我会怎么做
|
||||
|
||||
@@ -190,4 +190,12 @@ read_errors: 0 verify_errors: 0 super_errors: 0
|
||||
|
||||
---
|
||||
|
||||
*我的存储故障系列之一:[NVMe 事故](/posts/when-smart-says-healthy-but-your-raid-is-corrupting-data/) 和 [SATA 线警醒](/posts/that-dying-ssd-was-just-a-bad-sata-cable/)。*
|
||||
*我的存储故障系列之一:[NVMe 事故](/posts/zh/when-smart-says-healthy-but-your-raid-is-corrupting-data/) 和 [SATA 线警醒](/posts/zh/that-dying-ssd-was-just-a-bad-sata-cable/)。*
|
||||
|
||||
---
|
||||
|
||||
## 你的存储池是否正在静默丢数据?
|
||||
|
||||
我为企业设计、巡检和排障自托管存储与虚拟化基础设施(Unraid、Synology、btrfs/ZFS RAID 与内核级监控)。如果你遇到了难以解释的文件系统校验错误、虚拟机崩溃或 SSD 兼容性问题,我可以帮你定位根因并建立多层损坏告警机制。
|
||||
|
||||
欢迎联系 [[email protected]](mailto:[email protected]?subject=Storage%20pool%20and%20btrfs%20troubleshooting) 或 WhatsApp [+60 12-797 2969](https://wa.me/60127972969),或访问 [hoelee.com](https://hoelee.com)。
|
||||
@@ -88,4 +88,6 @@ clean = url.replace("_progressive_thumbnail", "")
|
||||
这里头没什么高深的东西。但这两类都是那种**只有在真实数据里才会现形**的无聊
|
||||
bug——一个 API 信誓旦旦说更新成功了的附件,一个匹配了 91% 行、却对剩下 9%
|
||||
一声不吭的正则。如果你给 NocoDB 回填数据时总觉得「只生效了一半」,先来查这两
|
||||
个地方。
|
||||
个地方。
|
||||
|
||||
*如果你需要为业务搭建基于 NocoDB/Airtable 的自动化数据管道、资产回填或防漏网清洗逻辑,欢迎联系:[WhatsApp (+60 12-797 2969)](https://wa.me/60127972969) · [[email protected]](mailto:[email protected])。*
|
||||
@@ -7,6 +7,7 @@ tags: ["docker", "self-hosting", "upstream", "maintenance", "registry"]
|
||||
ogImage: "/og/zh-the-upstream-was-deleted-then-came-back-rewritten.jpg"
|
||||
banner: "/banners/the-upstream-was-deleted-then-came-back-rewritten.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
`docker pull hectorqin/reader` 返回 **404**。而这正是我线上阅读服务所依赖的项目:一个只发邀请码的小书库,给我家人和几位付费读者用,每天在跑,上面还搭了一条我自己做的语音朗读管线,让「朗读」按钮念出来的是正经的神经网络音色。
|
||||
@@ -100,7 +101,7 @@ draft: false
|
||||
- 旧镜像标签留着做回滚,而且无论走哪条路,源码现在都在我自己的服务器上;
|
||||
- 前门不在讨论范围内,因为它在另一个容器里,换镜像够不着它。
|
||||
|
||||
真正有用的教训不是「要钉版本」——那个我早就做了。而是:**「这个项目还活着吗」有三个答案,而它们可以互相矛盾**。仓库可以活着,而镜像已被删除;README 可以写着 `deleted`,而重写版已经开工四周;版本号可以纹丝不动,而运行时、端口、数据库和配置模型在底下全换了一遍。这三件事要分别去查,否则你迟早会为一个「已经不再以你测试过的形态存在」的应用规划升级。
|
||||
真正有用的教训不是「要钉版本」——那个我早就做了。而是:**「这个项目还活着吗」有三个答案,而它们可以互相矛盾**。仓库可以活着,而镜像已被删除;README 可以写着 `deleted`,而重写版已经开工三周;版本号可以纹丝不动,而运行时、端口、数据库和配置模型在底下全换了一遍。这三件事要分别去查,否则你迟早会为一个「已经不再以你测试过的形态存在」的应用规划升级。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ tags: [unraid, swap, btrfs, zram, docker, user-scripts]
|
||||
ogImage: /og/zh-unraid-stop-array-hangs-on-swapfile.jpg
|
||||
banner: /banners/unraid-stop-array-hangs-on-swapfile.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
我给 Unraid 服务器加了 8 GB swapfile。缓存池是 btrfs RAID1,而内核不允许
|
||||
@@ -149,23 +150,18 @@ done
|
||||
器上可能还有别的 loop 设备(挂载的 ISO、其他镜像),它们必须在停止阵列时
|
||||
存活下来。
|
||||
|
||||
**脚本 2 —「Array Start Swapon」**——调度选 **At First Array Start
|
||||
only**(在 `disks_mounted` 触发):
|
||||
**脚本 2 —「Array Start Swapon」**——调度选 **At Startup of Array**(每次阵列启动、磁盘挂载完毕后触发):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 缓存池挂载后重建 loop swap。
|
||||
# 缓存池挂载后重建 loop swap(带幂等检查,已启用则跳过)。
|
||||
[ -f /mnt/cache/swapfile ] || exit 0
|
||||
losetup -j /mnt/cache/swapfile | grep -q . && exit 0
|
||||
LOOP=$(losetup -f)
|
||||
losetup -p 100 "$LOOP" /mnt/cache/swapfile && swapon -p 1 "$LOOP"
|
||||
```
|
||||
|
||||
为什么选「First Array Start only」而不是普通的「At Startup of the
|
||||
Array」?因为开机时 `/boot/config/go` 也会运行并配置一次 swap。开机后的
|
||||
*第一次*阵列启动由 `go` 负责;这个脚本真正管的是**不重启、纯 GUI 停止 →
|
||||
启动的循环**——而 `go` 永远看不到这种场景。(在我的配置里,我把 swap 那段
|
||||
从 `go` 里整个移除了,让 User Script 成为唯一事实来源——一处配置,两条路
|
||||
径都覆盖。)
|
||||
为什么选「At Startup of Array」而不是「At First Array Start Only」?因为「First Array Start Only」只在开机后的*第一次*阵列启动时跑一次,一旦你在 GUI 里点了停止再启动,它就会跳过。把原本写在 `/boot/config/go` 里的那段 swap 启动代码彻底删掉,改用「At Startup of Array」配合上面那行 `losetup -j` 幂等检查,就能让这个 User Script 成为唯一事实来源——不管是冷开机还是纯 GUI 停止 → 启动循环,一处配置,两条路径都覆盖。
|
||||
|
||||
之后:停止 → 缓存池第一次尝试就干净卸载。启动 → swap 恢复。多次停止/启动
|
||||
循环和重启后都验证通过。
|
||||
|
||||
@@ -8,6 +8,7 @@ tags: ["docker", "cdp", "playwright", "youtube", "portainer", "automation"]
|
||||
ogImage: "/og/zh-uploading-large-videos-to-youtube-via-cdp.jpg"
|
||||
banner: "/banners/uploading-large-videos-to-youtube-via-cdp.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
在前端自动化与爬虫开发中,通过无头浏览器或 Chrome DevTools Protocol(CDP)来提交表单、抓取数据或上传几百 KB 的图片通常十分顺畅。然而,当你试图在自动化流程中将一个 100MB 以上的高清 1080P 视频通过远程 CDP 自动上传到 YouTube 时,常规的浏览器自动化方案几乎一定会崩溃。
|
||||
@@ -85,6 +86,9 @@ cdp.send("DOM.setFileInputFiles", {
|
||||
|
||||
以下是带有完整异常捕获、入参校验与安全权限的生产级 Python 脚本:
|
||||
|
||||
<details>
|
||||
<summary><code>stage_file_to_container.py</code> — Portainer Docker Archive 归档注入脚本(50 行)</summary>
|
||||
|
||||
```python
|
||||
import io
|
||||
import os
|
||||
@@ -138,6 +142,8 @@ def stage_file_to_container(local_path: str, dest_filename: str) -> None:
|
||||
raise RuntimeError(f"Portainer 接口返回错误 {e.code}: {body}") from e
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 运维注意事项与安全清单
|
||||
1. **客户端 RAM 内存开销**:虽然 Chrome 端彻底消除了内存尖刺,但 `io.BytesIO()` 会在当前 Python 脚本进程中驻留整份 tar 镜像。如果要处理数 GB 的大型视频归档,应先将 `.tar` 写入临时文件,并通过 `requests` 或 `urllib3` 以流式分块形式上传。
|
||||
2. **目标目录必须预先存在**:Docker Archive API 是直接解包到 `?path=` 指向的目录中。如果目标容器内部缺失 `/config/Downloads` 目录,接口会直接返回 404/500,而不会自动创建父级目录。
|
||||
@@ -171,3 +177,11 @@ def stage_file_to_container(local_path: str, dest_filename: str) -> None:
|
||||
- **严禁在 CDP 的 WebSocket 通道传输大型文件**:远程浏览器不是为了在内存中缓冲几百兆媒体流而设计的;
|
||||
- **带外落盘先行**:使用 Docker Archive PUT 接口,利用 Docker 原生机制以极高速度直接将文件推入容器文件系统;
|
||||
- **容器内部路径秒挂载**:通过 CDP 的 `DOM.setFileInputFiles` 直接指定容器路径,实现零内存损耗、零失败率的文件绑定。
|
||||
|
||||
---
|
||||
|
||||
## 需要构建稳定、无配额瓶颈的浏览器与媒体自动化流水线?
|
||||
|
||||
我为企业设计并交付自托管的 AI 媒体生产与浏览器自动化基础设施——从 Remotion 视频渲染管线、容器化持久化登录态浏览器,到绕开 API 配额限制的 CDP 自动化发布闭环。如果你有重复性的内容发布、电商后台操作或跨系统数据流需要自动化,欢迎联系我定制方案。
|
||||
|
||||
联系我:[[email protected]](mailto:[email protected]?subject=Browser%20and%20media%20pipeline%20automation) 或 WhatsApp [+60 12-797 2969](https://wa.me/60127972969),或访问 [hoelee.com](https://hoelee.com)。
|
||||
@@ -9,6 +9,7 @@ tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"]
|
||||
ogImage: "/og/zh-vetting-an-open-source-dependency-before-you-bet-on-it.jpg"
|
||||
banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.jpg"
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
有人给我一份 1306 行的伙伴系统架构规格书。读起来像一份计划:前后一致、交叉引用、语气笃定。它指定一个自托管的开源引擎作为归因与佣金层,并且假定了那个引擎的三件事——它支持 OIDC,所以我现有的身份提供方能登录伙伴;它的佣金规则覆盖业务需要的五种形态;它的白标功能已经上线。
|
||||
@@ -102,7 +103,7 @@ head -5 docs/white-label-custom-domains.md
|
||||
|
||||
## 结果
|
||||
|
||||
六个假设在大概十分钟的 `curl` 和 `grep` 里被修正,而当时一行集成代码都还没写。项目依然可行——只是换了形状:没有 OIDC、手工打款、钉住 commit,以及一份不再依赖某个未合并分支上功能的计划。
|
||||
通过六项检查、大概十分钟的 `curl` 和 `grep`,赶在一行集成代码都还没写之前,把规格书里那三个站不住脚的核心假设全部纠偏。项目依然可行——只是换了形状:没有 OIDC、手工打款、钉住 commit,以及一份不再依赖某个未合并分支上功能的计划。
|
||||
|
||||
那份规格书乐观并没有错。它错在**没被验证过**——而这是整个项目里最便宜的修补项。
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
title: "SMART 显示健康,RAID 却在悄悄损坏数据"
|
||||
description: "两块看起来完全健康的 NVMe 组成的 btrfs RAID1 阵列,静默地把 Windows 虚拟机喂成了全零字节。我靠校验和取证抓住它——但根因(M.2 转接卡链路故障 vs 批次缺陷)至今仍未定论,现在两块盘测试都干净。"
|
||||
pubDate: 2026-09-16
|
||||
updatedDate: 2026-09-18
|
||||
category: devops
|
||||
blueprint: self-hosted-infrastructure
|
||||
blueprintLayer: L1-compute-storage-network
|
||||
tags: [unraid, btrfs, nvme, raid, storage, data-loss, troubleshooting]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/zh-when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
banner: /banners/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
---
|
||||
@@ -14,7 +17,7 @@ banner: /banners/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
|
||||
下面是完整故事:损坏是怎么浮现的、哪个校验和出卖了它——以及为什么最终定论如今变得不确定。
|
||||
|
||||
> **更新(2026-09-16):根因至今未定论。** 下面的「两块盘都换」是我当时的判断,但后来一次不相关的故障让我不得不重新审视。现在两块 NVMe 测试都干净,而最强的头号嫌疑是两块盘共用的那块 **M.2 转接卡**——转接卡上的链路层故障,恰好会复现我当初归咎于硬盘的那种「两块盘关联地吐出全零」的损坏。SMART 对链路故障完全无感,所以无法排除它。我在一篇配套文章里写了那次警醒(一颗「死掉的」SATA SSD 其实只是坏线)。先读下面的原始分析,再看更正——然后还有**续集**:两周后同一个全零指纹出现在**完全不同的硬盘堆栈**上,这次终于挖出了根因。见[《换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD》](/posts/zh/the-cause-was-trim-not-the-ssds/)。
|
||||
> **更新(2026-09-18):根因至今未定论。** 下面的「两块盘都换」是我当时的判断,但后来一次不相关的故障让我不得不重新审视。现在两块 NVMe 测试都干净,而最强的头号嫌疑是两块盘共用的那块 **M.2 转接卡**——转接卡上的链路层故障,恰好会复现我当初归咎于硬盘的那种「两块盘关联地吐出全零」的损坏。SMART 对链路故障完全无感,所以无法排除它。我在一篇配套文章里写了那次警醒(一颗「死掉的」SATA SSD 其实只是坏线)。先读下面的原始分析,再看更正——然后还有**续集**:两周后同一个全零指纹出现在**完全不同的硬盘堆栈**上,这次终于挖出了根因。见[《换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD》](/posts/zh/the-cause-was-trim-not-the-ssds/)。
|
||||
|
||||
## 更新:为什么我不再怪两块盘
|
||||
|
||||
@@ -26,7 +29,7 @@ banner: /banners/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg
|
||||
|
||||
所以我没法再诚实地断言「盘有问题」。它们可能一直好好的,损坏可能来自它们共同坐着的 **M.2 转接卡**。注意:**这并不代表这次事故是虚惊一场**——真实数据确实读不回来过,无论根因是什么,我都把它移走、迁移数据、并逐字节验证过。但正确的教训是「先排除链路,再给盘判死刑」,而不是「同批次盘不可信」。
|
||||
|
||||
完整的链路对盘排查,请看 **["我差点把这颗 SSD 送修——真正坏的是 SATA 数据线"](/posts/zh/that-dying-ssd-was-just-a-bad-sata-cable/)**。
|
||||
完整的链路对盘排查,请看 **["我差点把这颗 SSD 送修——真正坏的是 SATA 数据线"](/posts/zh/that-dying-ssd-was-just-a-bad-sata-cable/)**。而最终真凶——队列化 TRIM 固件缺陷导致文件系统层同步抹零——记录在 **[《换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD》](/posts/zh/the-cause-was-trim-not-the-ssds/)**。
|
||||
|
||||
## 原始分析(保留供透明考量)
|
||||
|
||||
@@ -121,4 +124,10 @@ btrfs RAID1 本应自愈:一个成员坏块、从镜像重建、写回好数
|
||||
3. **同批次盘共用一条通道,无论谁坏都是共享失效风险。** 不管损坏来自盘还是转接卡,一对盘坐同一条故障路径上,就是穿着两个序列号的单点故障。
|
||||
4. **给你的数据加校验、并提前测试恢复备份。** 把数星期静默损坏变成一次被控制住的事故的,是这一点——不是我那部分诊断。
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## 需要经得起真故障考验的存储与备份架构?
|
||||
|
||||
我为企业设计、迁移和抢修自托管存储池、虚拟化宿主机与灾难恢复体系——坚持用端到端文件校验(checksum)和定期恢复演练替代“看一眼面板绿灯”。如果你的业务数据跑在自建 NAS 或虚拟化节点上,我可以帮你做一次备份可恢复性验证与底层存储健康审计。
|
||||
|
||||
欢迎联系 [[email protected]](mailto:[email protected]?subject=Storage%20recovery%20and%20backup%20audit) 或 WhatsApp [+60 12-797 2969](https://wa.me/60127972969),或访问 [hoelee.com](https://hoelee.com)。
|
||||
+2
-4
@@ -12,9 +12,7 @@ banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-productio
|
||||
|
||||
上周我做了一件平时几乎不会做的事:数了数自己的提交。不是数我记得写过的那些,而是数仓库里真实记录下来的那些。自建 Gitea 上 5 个生产仓库、三十天、一个人:865 次 Commit、新增 518,233 行、删除 130,892 行。
|
||||
|
||||
然后我数了数活下来的部分。对主生产项目的最新版本跑一次干净扫描,有效代码总量是 234,976 行;同一时间窗口里净增 58,240 行的视频转码器,今天整个仓库只有 43,050 行。
|
||||
|
||||
那 28 万行进哪儿去了?没有丢。它们被写出来、跑过测试、被判为不对,然后删掉——或者被一个更小、做同一件事的东西替换掉。我想把这条循环记下来,因为它这一年对我工作方式的影响,比任何一次模型升级都大。
|
||||
然后我数了数活下来的部分。而对照最终留在生产分支上的有效代码:主生产项目收敛在 234,976 行(期间新增 33.1 万行、淘汰 9.9 万行);同一时间窗口里累计写入 58,240 行的视频转码器,今天整个仓库只有 43,050 行。5 个仓库里被淘汰掉的那 13 万多行(以及未进入主干的十多万行草稿)进哪儿去了?没有丢。它们被写出来、跑过测试、被判为不对,然后删掉——或者被一个更小、做同一件事的东西替换掉。我想把这条循环记下来,因为它这一年对我工作方式的影响,比任何一次模型升级都大。
|
||||
|
||||
## 一开始我不信的这几个数字
|
||||
|
||||
@@ -93,7 +91,7 @@ git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
|
||||
|
||||
- **边界设计**:模块从哪里分开,哪些状态允许跨过去。
|
||||
- **验证体系**:那个能抓住类型检查看不见的语义漂移的脚手架。
|
||||
- **剪枝的决断力**:愿不愿意删掉那 28 万行探索性、冗余、已死掉的代码,只把 23.5 万行送进生产。
|
||||
- **剪枝的决断力**:愿不愿意在 51.8 万行提交中砍掉 13.1 万行已落盘的冗余代码与无数中间草稿,让主生产系统精简收敛在 23.5 万行。
|
||||
|
||||
所以我现在不再把提交曲线当生产率图表看。一个月新增 50 万行,只能说明工具在转;真正说明这一个月值不值的是那删掉的 13 万行、底下的重构,和一套仍然全绿的测试。
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ tags: [docker, chrome, cdp, persistence, synology, self-hosting]
|
||||
ogImage: /og/zh-why-chrome-forgets-its-tabs-in-a-container.jpg
|
||||
banner: /banners/why-chrome-forgets-its-tabs-in-a-container.jpg
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
---
|
||||
|
||||
我在 NAS 上的 Docker 容器里跑着一个真正的 Google Chrome。它不是爬虫实例,也不是截图服务 —— 是我真的在用的浏览器:登录着那些绝不该交给无头机器人的账号,可以从任何设备通过网页桌面看着它操作,需要自动化的时候还能用 Chrome DevTools Protocol (CDP) 驱动它。
|
||||
@@ -119,6 +120,9 @@ Chrome 确实退出了 —— 然后桌面会话在几秒内又把它拉了起
|
||||
|
||||
这就是一段从镜像的容器初始化钩子拉起的后台脚本。我实际在跑的版本,去掉无关部分后是这样:
|
||||
|
||||
<details>
|
||||
<summary>tabs_keeper.py — 标签页恢复与定时快照脚本(42 行)</summary>
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Restore last session's tabs on start, then snapshot open tabs every 60s."""
|
||||
@@ -163,6 +167,8 @@ while True:
|
||||
os.replace(tmp, STORE) # atomic; a kill can't truncate it
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
两个设计选择值得抄走:
|
||||
|
||||
- **只在浏览器确实空空如也时才恢复。** 万一将来某个 Chrome 版本自己会恢复标签页,这段脚本就会让路,而不是把每个标签页再复制一遍。
|
||||
@@ -217,7 +223,7 @@ chrome-relay:
|
||||
docker restart chrome && docker restart chrome-relay # order matters
|
||||
```
|
||||
|
||||
上面那条健康检查的作用,就是把这种静默故障变成一个看得见的 `unhealthy`。如果你打算自己驱动容器里的浏览器,客户端那一侧我单独写过一篇:[Scraping a Bot-Walled Marketplace With a Warm Browser Session](/posts/scraping-bot-walled-marketplace-warm-browser-session/)。
|
||||
上面那条健康检查的作用,就是把这种静默故障变成一个看得见的 `unhealthy`。如果你打算自己驱动容器里的浏览器,客户端那一侧我单独写过一篇:[《用预热的浏览器会话抓取带反爬的交易平台》](/posts/zh/scraping-bot-walled-marketplace-warm-browser-session/)。
|
||||
|
||||
## 如果重来一次
|
||||
|
||||
|
||||
@@ -179,3 +179,13 @@ curl -G "http://192.168.1.1:5678/webhook/ltts" \
|
||||
|
||||
**工程经验**:
|
||||
在搭建涉及“网络网关 → 自动化编排器 → AI 神经推理模型”的多层系统时,绝不能假设到达 API 边界的数据格式天然符合预期。在网关层做安全清洗、在模型接口层做幂等兜底,两行轻量代码就能彻底杜绝 AI 模型在半夜把网络请求头念给用户听的尴尬事故。
|
||||
|
||||
---
|
||||
|
||||
## 需要搭建高可用本地 AI 语音管线或业务自动化?
|
||||
|
||||
我为企业和开发者定制并交付基于私有 GPU 算力的高保真语音合成(TTS)、本地语音识别(ASR)与 n8n 企业自动化工作流,确保核心数据不出内网:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Local%20AI%20speech%20pipeline%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
@@ -99,4 +99,14 @@ Cognition(Devin 开发团队)曾在 2025 年的一场技术讲座中举过
|
||||
2. **明确的数据管道契约**:子 Agent 仅负责生成独立素材(如生成一张 SVG 架构图,或调度本地 ComfyUI 出图),结果落地为静态文件并只返回文件路径句柄。
|
||||
3. **独立对抗式守门**:由拥有空白纯净记忆的高阶推理大模型对单写主控生成的 Git Diff 进行对抗式挑刺,完全不参与编写泥潭。
|
||||
|
||||
**写操作永远单线程,只读与外部素材放手并行**——这才是规避蜂群混乱、稳定交付高质量软件的唯一工程正道。
|
||||
**写操作永远单线程,只读与外部素材放手并行**——这才是规避蜂群混乱、稳定交付高质量软件的唯一工程正道。关于这套架构的完整生产实操落地与压测基准,请参考我深入拆解的长文:**[代码只让一个 AI 改:我是如何避开多 Agent 协同踩坑的「单写多审」架构](/posts/zh/the-single-writer-rule-for-coding-agents/)**。
|
||||
|
||||
---
|
||||
|
||||
## 想为你的业务落地高可靠 AI 自动化方案?
|
||||
|
||||
我为企业与初创团队设计、搭建并运维自主 AI Agent 基础设施、内部高效工具链与高确定性自动化系统:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=AI%20agent%20architecture%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个折腾了一整天的 400"
|
||||
title: "Telegram 机器人通知为什么发不出去:IPv6、DNS 和一个追查了一整天的 400"
|
||||
description: "一次排障记录:一个跑在 Docker 里的 Telegram 监控机器人,sendPhoto 一直返回 nginx/1.30.1 的 400,但 getMe 却正常——根因是只返回 IPv6 的 DNS 应答、一个写错的 extra_hosts IP,以及一个用 join 拼接破坏了 JPEG 二进制数据的 multipart body。"
|
||||
pubDate: 2026-03-25
|
||||
updatedDate: 2026-09-09
|
||||
@@ -7,12 +7,14 @@ category: devops
|
||||
blueprint: ai-agent-automation
|
||||
blueprintLayer: L1-deterministic-orchestration
|
||||
tags: [docker, telegram, python, dns, ipv6, debugging, portainer]
|
||||
draft: false
|
||||
dynamicCta: true
|
||||
ogImage: /og/zh-why-telegram-bot-notifications-die.jpg
|
||||
banner: /banners/why-telegram-bot-notifications-die.jpg
|
||||
---
|
||||
|
||||
我跑着一个小监控机器人,专门盯着 Carousell 上新上架的商品,一旦有新品就通过
|
||||
Telegram 通知我。有一天它突然不再通知了。容器本身是健康的,爬虫也照常往
|
||||
我运行着一套自动化商品监控服务,专门监测市场上新上架的商品,一旦有新品就通过
|
||||
Telegram 实时推送。有一天它突然不再通知了。容器本身是健康的,爬虫也照常往
|
||||
NocoDB 里归档商品——但每一条通知都以日志里那三个字符宣告失败:
|
||||
|
||||
```
|
||||
@@ -94,7 +96,7 @@ $ getent hosts api.telegram.org
|
||||
JPEG。两条路径的代码没有任何差别。
|
||||
|
||||
当你撞上这种"输入相同、输出相反"的矛盾时,答案通常是:**输入其实并不
|
||||
相同**。但找到那个差异,还需要我再换一次思路。
|
||||
相同**。当我用 `docker exec` 手动测试时,随手抓取的是一张轻量的小样本图;而常驻进程处理的真实商品图片体积较大且恰好在二进制载荷内包含了导致边界判断失效的分块序列。输入看似一样,实际二进制 payload 并不相同。但找到那个差异,还需要我再换一次思路。
|
||||
|
||||
### 第四轮:`nginx/1.30.1` 就是 Telegram
|
||||
|
||||
@@ -113,44 +115,52 @@ nginx 会自己回一张 400 页面——没有 JSON,也没有友好的错误
|
||||
|
||||
## 修复
|
||||
|
||||
我最初的代码是这样拼 multipart 请求体的——正是那种"看着显然没问题"、
|
||||
实则埋下微妙 bug 的一行:
|
||||
我最初的代码在拼装 multipart 请求体时,为了图省事对分片缓冲做了按行切分再用 `b"
|
||||
\n".join(body_lines)` 重新拼接——正是那种"看着显然没问题"、实则埋下微妙 bug 的写法:
|
||||
|
||||
```python
|
||||
body = b"\r\n".join(body_lines)
|
||||
body = b"
|
||||
\n".join(body_lines)
|
||||
```
|
||||
|
||||
这个 `join` 看起来很省事,但 JPEG 是**二进制数据**。它的字节里本身就包含
|
||||
`\r\n` 这样的字节序列,出现在图片数据中间。用 `\r\n` 作为分隔符去 join,
|
||||
就把本该是图片 payload 的字节给破坏了。Telegram 收到的是一个 multipart
|
||||
边界被破坏的请求体,于是它的 nginx 在 bot API 还没来得及解析之前就回了个
|
||||
400。
|
||||
问题在于 JPEG 是**二进制数据**。它的压缩字节流中间天然会出现 `0x0A`(`\n`)或 `0x0D`(`
|
||||
`)字节。一旦按行切分再用 `
|
||||
\n` 作为分隔符去 `join`,原本属于图像内部的单个换行字节就会被强行改写成 `
|
||||
\n`,导致字节数膨胀、后续的 multipart 边界(boundary)错位。这也一举解释了第三轮里的诡异矛盾:我在 `docker exec` 手工测试时用的是一个极简的测试 payload(甚至直接传了图片 URL),而常驻进程 `PID 1` 抓到的那张真实商品 JPEG 恰好包含触发边界错位的字节序列!Telegram 收到一个边界损坏的请求体,它的边缘 nginx 在进入 bot API 之前就直接回了 400。
|
||||
|
||||
正确的做法是逐字段拼装 body,让二进制数据保持原样:
|
||||
正确的做法是按二进制块(parts)逐段追加,用空字节串 `b"".join(parts)` 原样拼接,绝不按文本行处理:
|
||||
|
||||
```python
|
||||
boundary = "----tg" + token_hex(8)
|
||||
|
||||
parts = []
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(b'Content-Disposition: form-data; name="chat_id"\r\n\r\n')
|
||||
parts.append(f"{chat_id}\r\n".encode())
|
||||
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(b'Content-Disposition: form-data; name="caption"\r\n\r\n')
|
||||
parts.append(f"{caption}\r\n".encode())
|
||||
|
||||
parts.append(f"--{boundary}\r\n".encode())
|
||||
parts.append(
|
||||
parts = [
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="chat_id"
|
||||
\n
|
||||
\n',
|
||||
f"{chat_id}
|
||||
\n".encode(),
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="caption"
|
||||
\n
|
||||
\n',
|
||||
f"{caption}
|
||||
\n".encode(),
|
||||
f"--{boundary}
|
||||
\n".encode(),
|
||||
b'Content-Disposition: form-data; name="photo"; '
|
||||
b'filename="image.jpg"\r\nContent-Type: image/jpeg\r\n\r\n'
|
||||
)
|
||||
parts.append(image_bytes) # ← 二进制保持二进制
|
||||
parts.append(b"\r\n")
|
||||
|
||||
parts.append(f"--{boundary}--\r\n".encode())
|
||||
|
||||
body = b"".join(parts) # join 的是字节块,不是行
|
||||
b'filename="image.jpg"
|
||||
\nContent-Type: image/jpeg
|
||||
\n
|
||||
\n',
|
||||
image_bytes, # ← 二进制保持原样,绝不按行切分
|
||||
b"
|
||||
\n",
|
||||
f"--{boundary}--
|
||||
\n".encode(),
|
||||
]
|
||||
body = b"".join(parts) # 直接拼接字节块,不插入额外分隔符
|
||||
```
|
||||
|
||||
区别在于用 `b"".join(parts)` 而不是 `b"\r\n".join(...)`:每一块本身就是
|
||||
@@ -190,7 +200,7 @@ multipart 修复之后,监控机器人在下一个 tick 就真正把通知发
|
||||
|
||||
---
|
||||
|
||||
*这个机器人是我个人的项目,但教训可以推广到任何依赖外发通知的服务。如果你
|
||||
*这套监控管线起初是为内部高频巡检搭建的,但其中的排障教训适用于任何依赖外发通知的生产服务。如果你
|
||||
有一个监控、抓取,或者需要可靠地触达用户的告警管线,我们可以聊聊。*
|
||||
|
||||
**我搭建可靠的监控与通知管线——Telegram/WhatsApp 告警、抓取器,以及背后的
|
||||
|
||||
@@ -136,4 +136,14 @@ asyncio.run(main())
|
||||
Notification sent: "Windows-MCP Ready" - Immediate execution verified.
|
||||
```
|
||||
|
||||
无需苦等 10 秒倒数,再无 `CONTROL_PREEMPTED`,所有桌面动作即时执行。
|
||||
无需苦等 10 秒倒数,再无 `CONTROL_PREEMPTED`,所有桌面动作即时执行。关于什么时候该用系统 API、什么时候该让 Agent 操控物理键鼠,可以进一步参考:**[什么时候该让 AI 操控物理鼠标(以及什么时候绝对不要用)](/posts/zh/when-to-give-your-agent-physical-clicks/)**。
|
||||
|
||||
---
|
||||
|
||||
## 需要实现复杂的桌面端或企业级自动化?
|
||||
|
||||
如果你正在跨越遗留软件缺乏 API 的鸿沟,需要构建结合 MCP、无头浏览器与可靠键鼠模拟的桌面自动化链路:
|
||||
|
||||
- **WhatsApp**:[+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email**:[[email protected]](mailto:[email protected]?subject=Desktop%20automation%20%26%20MCP%20consulting)
|
||||
- **网站**:[hoelee.com](https://hoelee.com)
|
||||
Reference in new issue
Block a user