# 外部顾问审阅全景资料包:blog.hoelee.com 提取至 LinkedIn 发布动态 > **文档目标**:本资料包汇总了从技术博客 `blog.hoelee.com` 提取并改编用于 LinkedIn 发布的全部 15 篇技术动态文案(其中 12 篇已在 LinkedIn 线上发布,3 篇已撰写并在排队队列中)。 > 请外部顾问在审阅这批动态内容时,**严格对照以下【背景定位】与【前置限制约束】**进行综合评估。如发现不合适、人设违规、数据不严密或表达偏颇之处,请直接指出并给出修改建议,我们将同步更新线上已发布的 LinkedIn 动态及后续排队文案。 --- ## 一、 战略背景与核心业务目标 博客网站 `blog.hoelee.com` 与 LinkedIn 个人主页是作者技术资产与个人品牌的双核阵地。所有提取自博客并在 LinkedIn 发布的动态,必须协同满足以下两大约束目标: 1. **最高优先级:高端技术求职招聘(Senior 全栈 / DevOps / AI 架构师)** - **目标画像**:面向 6-10 年经验的远程(Remote)或本地/混合(Hybrid)资深全栈工程师、平台工程师、DevOps 负责人及 AI 智能体应用架构师岗位。 - **【最关键职业红线】绝不可把自己写成“做生意的老板/创始人 (Founder/Business Owner/Agency Lead)”**: - 在招聘猎头与企业 HR 的简历筛选系统中,一旦动态内容或个人简介充斥“我在做生意”、“我的客户代理模式”、“我的数字商品电商”等创业者视角的自述,候选人会被判定为“有自己生意、不可能全身心投入全职雇佣”而直接在第一轮被筛除。 - **强制转换规范**:所有涉及商业实战的项目,必须将其表述为**“生产系统研发与高可用保障”、“为企业客户交付的生产级工程项目”、“自建自动化运维体系”**。重点展示架构设计、状态机、数据流、异常排查与工程韧性,落脚于高级技术雇员与技术专家的专业交付能力。 2. **次要目标:B2B 中小企业技术咨询与高可靠方案获客(Freelance & Advisory)** - 面向需要自托管私有化系统、高吞吐媒体管线、自动化 AI 智能体、高可用邮件及反代基础设施的中小企业主与技术团队。 - 动态需通过真实的工程复盘展示“不可伪造的实操硬实力(Proof of Expertise)”,让潜在客户感知到作者解决复杂生产疑难杂症的确定性与可靠性。 --- ## 二、 必须严格遵循的内容前提与限制约束(铁律清单) ### 1. 核心人设与语调纯洁性(杜绝业余爱好者/玩具化口吻) - ❌ **绝对禁用词**:`homelab`, `home lab`, `家用`, `小主机`, `玩具`, `折腾`, `四台全中`, `玩票`。 - ✅ **标准专业替代词**: - 英文:`the infrastructure I manage`, `my self-hosted production nodes`, `on-premise dedicated server`, `production host`. - 中文:“我管理的私有基础设施”、“自托管生产节点”、“企业级自建网关”、“生产服务器”。 - **作者第一人称单数**:统一使用第一人称单数(`I` / `我`),杜绝含糊不清或虚张声势的复数(`we` / `我们`),强化架构师个人对系统与代码的掌控力和责任感。 - **杜绝自嘲与“草台班子”话术**:坚决清除如“靠胶带勉强粘在一起 (held together with duct tape)”、“粗糙应付”等业余自嘲表达,统一升级为“受控的受试会话”、“严格边界下的容错设计”。 ### 2. 事实真相与数据严密性(不可伪造、杜绝夸大) - **严禁捏造未经测量的虚假数字**:LinkedIn 动态中引用的所有性能倍数、内存开销、提交代码量、故障排查用时,必须与博客源文章及底层真实终端日志、Git Commit 历史完全一致。 - **严密区分“耗时减少比例”与“吞吐加速倍数”**: - 例如:耗时从 352s 缩短至 218s,是**耗时减少 38%**,对应的**吞吐加速倍数是 1.61×**(352/218)。绝对不能将 38% 偷换混淆为“提速 38 倍”或“速度提升 38%”。 - **跨仓库统计范围与算术自洽**: - 例如在《50 万行提交重构》一文中,51.8 万行新增是跨 5 个生产仓库的汇总增量,删除与重构约 13 万行,主干留存约 23.5 万行。数字的统计口径必须严密对齐,经得起资深架构师背景调查与技术推敲。 - **物理与时间戳边界**: - 视频与音频处理对齐不可妄称“微秒级精确”,物理帧率上限(如 30fps 单帧 33.3ms)是严格的边界。 ### 3. 生产安全与隐私脱敏(OPSEC 安全边界) - **严禁泄露内部私密路径**:绝不在文案或展示片段中出现本地绝对路径(如 `D:/dev/...` 或 `C:/Users/...`),统一抽象为通用项目路径。 - **严禁泄露真实内网 IP 与生产资产标识**:内网 IP(如 `192.168.1.x`)一律替换为规范内网域名(如 `node.internal`),公网 IP 统一脱敏为 RFC 5737 文档保留地址(`203.0.113.x`)。 - **凭据与令牌严格脱敏**:任何涉及认证的命令保持变量引用形式(如 `Bearer $TOKEN`),严禁携带真实密钥。 ### 4. LinkedIn 平台受众交互与传播适配 - **去除 AI 模板感与空洞废话**:不发空泛的行业吹捧与无意义的 AI 鸡汤,开篇直切具体的生产故障现象、真实的指标矛盾或反直觉的技术挑战。 - **结尾深度互动引导**:不搞生硬的营销硬广,结尾统一抛出面向工程师、技术管理者的实战思辨问题(例如探讨测试看门狗、版本升级防护、代码生成边界等),并附带对应博客深度长文的权威溯源链接:`Write-up: https://blog.hoelee.com/posts//`。 --- ## 三、 顾问审阅开刀维度与反馈指引 请顾问重点对以下四个核心维度进行严苛诊断,并在每篇文章末尾的【意见回填栏】提供结论: 1. **【职业形象与求职契合度】**:这篇动态读起来像一个可独当一面的 Senior 全栈/DevOps 架构师吗?是否不小心流露出了“做生意/小公司老板”而影响雇主录用的倾向? 2. **【技术严密性与逻辑自洽】**:技术判断是否准确?数据与因果链条是否经得起推敲?有无前后矛盾或夸大其词? 3. **【人设纯洁性与表达去油】**:是否存在业余玩家(homelab/折腾/玩具)的口吻残留?英语母语表达是否自然地道、干练硬核? 4. **【具体修改意见与优化稿】**:若当前文案存在问题,请给出具体的修改批注或建议的优化润色版本。如果该文案已在线上发布,我们将根据您的意见通过 LinkedIn API/后台同步执行线上修正。 --- ## 四、 15 篇动态文案全文与审阅明细 ### 1. The authentik upgrade that quietly killed single sign-on - **当前状态**:`已发布上线(发布时间: 2026-10-16)` - **系统标识 Key**:`authentik-major-upgrade-gotchas` - **博客源文章**:https://blog.hoelee.com/posts/authentik-major-upgrade-gotchas/ - **正文字符数**:1395 chars #### LinkedIn 动态正文: ```text I upgraded authentik, the SSO gate in front of my self-hosted Docker stack — email, dashboard, Synology apps — from 2025.8.3 to 2026.8.1 in one sitting. Six major hops, no downgrade path, so I backed up Postgres first. Then SSO broke. Already-authenticated users got walked through identification, password and MFA again on each new app. The cause was one field: I had pointed each provider's `authorization_flow` at a per-app flow for branding, when the login page is actually `authentication_flow`. That was the expensive one, not the only one. 2025.12 moved brand assets from `/media` to `/data/media`, so every logo 404'd until I remounted. The RBAC rework deleted the `User_groups` model while rows in `authentik_core_session` still referenced it, throwing a LookupError until I truncated that table. And 2026.8 made trusted proxies opt-in. The stack is managed by Portainer, not the compose file on disk. The `.env` was stale, and the real path was the Portainer API — pull images first, remove the fixed-name containers, then PUT the stack. Colliding containers and a network-attach failure ate the evening. **Read the field name before you touch the config.** A major-version upgrade on auth infrastructure is 10% changing the image tag and 90% the data model, storage and proxy rules shifting underneath you. Write-up: https://blog.hoelee.com/posts/authentik-major-upgrade-gotchas/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 2. The five-step AI roadmap everyone shares — and what I actually run - **当前状态**:`待发布(排队中)` - **系统标识 Key**:`five-step-ai-roadmap` - **博客源文章**:https://blog.hoelee.com/posts/five-step-ai-roadmap-what-i-actually-run/ - **正文字符数**:1820 chars #### LinkedIn 动态正文: ```text A five-step AI roadmap circulates on every feed: fundamentals, prompting, RAG, agents, then fine-tuning, with a salary band attached to each step. It is not wrong. It is a syllabus — and a syllabus tells you what to study, never what survives contact with a real system. Here is the honest version, checked against what is running on my hardware. Prompting is the step with the lowest ceiling. I have replaced agent prompts with forty lines of Python more than once, because a task with one correct answer and a deterministic input wants a script, not a model. What actually survives on the prompt side is interface design and evaluation — and the roadmap never mentions evaluation. RAG is the step I really run: mem0, LiteLLM and PostgreSQL with pgvector on my own NAS, memories scoped per user, answered in two HTTP calls. The hard half is ingestion, not retrieval. The last thing I pushed through it was a 57-second video on my desktop, which came back as text in about eight seconds on a 12 GB card. And the failure that cost me a day was not the model: it was a multipart upload with no Content-Type, which made the broker decode raw audio as UTF-8 text and destroy it before any of my code ran. Agents are where the work is, but the agent is the smallest part of it. Most of my scheduled automation has no AI in it at all, and those are precisely the jobs that have never needed rescuing. Fine-tuning is the step I have never done, on purpose. None of my failures have been model-behaviour failures — they were bytes, mounts and licences. A LoRA does not fix a schema drift. Three of those five steps run in production on one desktop GPU and one NAS. Step 5 stays empty, and I would rather say that plainly than pad a roadmap. Write-up: https://blog.hoelee.com/posts/five-step-ai-roadmap-what-i-actually-run/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 3. LinkedIn Post Draft v2 — Loop Engineering Without a Coding Agent - **当前状态**:`已发布上线(2026-10-02 首发/已校验)` - **系统标识 Key**:`loop-engineering` - **博客源文章**:(由博客综合案例与工程复盘派生) - **正文字符数**:1073 chars #### LinkedIn 动态正文: ```text --- I ran an AI agent 29 times for a task that a string comparison could have done. The agent kept failing — every time, same result. I kept asking it differently. Three weeks, 11 scheduled loops, and the one that cost the most was also the one that failed most embarrassingly — 29 times in a row. Why? I put an AI agent in a loop that only needed to check if a number was greater than zero. The moment I stopped treating AI as the default answer to every automation problem was the moment the automation actually started working. 8 of my 11 loops run with zero AI. Zero tokens. Zero hallucination risk. They just run. The useful version of "loop engineering" isn't a fleet of agents. It's one cron job that: → knows when to stop → says when it can't see → has an exit path for the failure you didn't anticipate I wrote the full breakdown — 11 jobs, the three-layer autonomy model, and how 80% of it costs nothing to run. Link in comments. Have you caught yourself doing the same thing? 👇 #LoopEngineering #AIautomation #DevOps #SelfHosting #SoftwareEngineering ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 4. Why two CodeIgniter routes crashed the moment I left IIS - **当前状态**:`已发布上线(发布时间: 2026-10-19)` - **系统标识 Key**:`migrating-codeigniter-iis-to-openlitespeed` - **博客源文章**:https://blog.hoelee.com/posts/migrating-codeigniter-iis-to-openlitespeed/ - **正文字符数**:1364 chars #### LinkedIn 动态正文: ```text I moved a CodeIgniter 4 app off Windows Server and IIS onto OpenLiteSpeed on CyberPanel, on Linux, on a different domain. The very first unauthenticated request to the new host fatally crashed on two routes that had run fine for years. Both bugs were real, and both had been in the code the whole time. IIS never showed them because an authentik SSO gate sat in front of exactly the routes that were broken — every request hit a 302 redirect and the controller never executed. The first was mine: I had called env() in app/Config/Constants.php, which CI4 loads before the helper that defines env(), so PHP died with "Call to undefined function env()" on a clean boot. The second was a genuine pre-existing bug — a controller calling parent::__construct() when CodeIgniter\Controller has no constructor, only initController(). **An auth gate in front of a route means that route has no working test coverage for its own code.** Before I called the migration done I hit every previously-gated route unauthenticated — a ten-line curl loop that would have found both in seconds instead of two hours. The step most guides skip: point the docroot at public/, not the project root. Extract into public_html and stop there and the app works fine while quietly serving .env over HTTP. Write-up: https://blog.hoelee.com/posts/migrating-codeigniter-iis-to-openlitespeed/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 5. NocoDB SSO is a licensed feature — and the env vars won't tell you - **当前状态**:`已发布上线(发布时间: 2026-10-12)` - **系统标识 Key**:`nocodb-sso-is-licensed` - **博客源文章**:https://blog.hoelee.com/posts/nocodb-sso-is-a-licensed-feature/ - **正文字符数**:1073 chars #### LinkedIn 动态正文: ```text If you self-host NocoDB and search for "nocodb sso self-hosted", you'll find the docs, the environment variables, and forum answers saying it works. All of it is technically true. The variables are real, and NocoDB enforces them at startup — leave them out and the container refuses to boot with a message that reads like a configuration mistake you can fix. Then you hit the OIDC route on an unlicensed build and the process exits. Not an error page, not a polite "SSO is not enabled on this instance" — the whole server. The lesson I keep re-learning on self-hosted software: **a feature that is implemented but not licensed looks identical to a feature that works, right up until you try it.** Reading the docs carefully is not the same as reading the licence line. If what you actually want is "the internet can't reach this login form", a forward-auth proxy in front of the hostname gets you there on the open-source build — the app keeps its own login, and the door is locked at the edge. Write-up: https://blog.hoelee.com/posts/nocodb-sso-is-a-licensed-feature/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 6. One hostname for a public tracker and an SSO-gated dashboard - **当前状态**:`已发布上线(发布时间: 2026-10-10)` - **系统标识 Key**:`one-hostname-tracker-sso` - **博客源文章**:https://blog.hoelee.com/posts/one-hostname-public-tracker-sso-dashboard/ - **正文字符数**:1070 chars #### LinkedIn 动态正文: ```text Every analytics tool has the same awkward shape: the half that collects data must be reachable by a stranger's browser on every page load, and the half that shows the data must be reachable by nobody but me. The conventional answer is two hostnames — an ingest endpoint and a private dashboard. I wanted one DNS record and one certificate, so I put both halves behind the same hostname and split them by path. That works, and it fails in a very specific way: the moment you put the *path* behind auth instead of the *host*, the public half starts serving 404s to real visitors and you don't find out for days, because a tracker that returns 404 collects nothing and looks exactly like a tracker that works. Two things I'd check first next time: whether the ingest path is actually excluded from the auth layer, and whether the header your app reads to identify clients survives the proxy hop. Both are boring, both are where the afternoon went. Full write-up with the config and the two traps: https://blog.hoelee.com/posts/one-hostname-public-tracker-sso-dashboard/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 7. Patching MySQL Workbench 26 so it stops lying about MariaDB - **当前状态**:`已发布上线(发布时间: 2026-10-21)` - **系统标识 Key**:`patching-workbench-26-for-mariadb` - **博客源文章**:https://blog.hoelee.com/posts/patching-workbench-26-for-mariadb/ - **正文字符数**:1393 chars #### LinkedIn 动态正文: ```text I run four database servers from one Windows machine: two MySQL 8.4 instances, a local install, and a MariaDB 10.11 box behind CyberPanel. Three opened fine in MySQL Workbench 26.7.0. The fourth returned a Python TypeError about a missing argument — nothing in it about databases or credentials. That was the first clue. It wasn't the bug; it was the error reporter crashing while trying to report the bug, once per failed connection. Workbench's own bundled mysqlsh binary connected to the same host, so the server, grants and password were fine and the fault was in the client. With the reporter patched, the truth surfaced: MariaDB has no gtid_mode variable, and Workbench assumed a version number could tell MySQL from MariaDB. MariaDB reports 10.x and 11.x, so every version >= 56000 check passes against it — and the same assumption hid in three files. Three patches later — a tolerant gtid_mode probe, the error handler's argument count, and a guard on explain_json_format_version — all four connections work. A separate TLS fault needed one value in connections.xml. These patch a closed app's bundled code, so the next Workbench update wipes them. Back up, clear read-only, delete the stale __pycache__, and test the path with a fake session. **Version numbers are the easiest thing to compare wrongly.** Write-up: https://blog.hoelee.com/posts/patching-workbench-26-for-mariadb/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 8. Wiring NocoDB to a database on another machine without copying a row - **当前状态**:`已发布上线(发布时间: 2026-10-18)` - **系统标识 Key**:`read-only-nocodb-dashboard-for-a-remote-database` - **博客源文章**:https://blog.hoelee.com/posts/read-only-nocodb-dashboard-for-a-remote-database/ - **正文字符数**:1390 chars #### LinkedIn 动态正文: ```text The person who runs the business wanted to look at the leads and orders my app writes into MariaDB — filter, sort, export — without SSH-ing into the VM or asking me. The tempting fix is a second copy of the data. I pointed NocoDB at the real database instead, through a MySQL account that can only SELECT — always live, zero duplicated rows. The first grant never matched. NocoDB runs in Docker on a Synology NAS on a user-defined bridge, but the MariaDB sits at 192.168.1.124, outside that subnet. When a container reaches an address outside its bridge, Docker's masquerade rule NATs the traffic through the host, so the database sees the NAS's IP, not the container's. One SELECT CURRENT_USER() proved it. Once the source address was measured, the grant wrote itself: one host, one schema, SELECT only. The proof it works is that NocoDB's own attempt to drop a table comes back DROP command denied — from the database, not from NocoDB. The meta API had two traps: an external source returns a job id with no job-status route, so verify the outcome instead of polling; and POST /meta/bases/{id}/tables creates a new empty table instead of attaching one. When you hand a client a dashboard, **point it at the real rows through an account that physically cannot write, then verify both halves.** Write-up: https://blog.hoelee.com/posts/read-only-nocodb-dashboard-for-a-remote-database/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 9. Replacing RDPGuard with IPBan: the traps nobody documents - **当前状态**:`待发布(排队中)` - **系统标识 Key**:`replacing-rdpguard-with-ipban` - **博客源文章**:https://blog.hoelee.com/posts/replacing-rdpguard-with-ipban/ - **正文字符数**:1379 chars #### LinkedIn 动态正文: ```text Put a Windows machine on the public internet with Remote Desktop enabled and the log fills with failed logins within minutes. I'd run RDPGuard 7.8.7 for years — paid and closed-source — and replaced it with IPBan, the MIT-licensed alternative. The migration took an evening, and one step would have quietly unbanned twelve active attackers. RDPGuard owns a single Windows Firewall rule, and all twelve of its bans live inside it. Uninstall first and the rule goes with it. I exported the bans first, fed them into IPBan's ban.txt, and it consumed them within seconds and rewrote its own rule. Two config traps matter. --install-service does not exist in IPBan 4.1.0; service registration is sc.exe, where the space after every = is load-bearing. And ExpireTime is not the ban duration, it's how long a failed-login count is remembered. BanTime is the key you want. IPBan blocked sixteen attacker IPs in its first hour, and the ban records mattered more than the IPs. Two had guessed the machine owner's actual handle. That's **reconnaissance aimed at a person**, not ambient noise. A blocker is a mitigation. Every tool here reacts after failed logins; each attacker still gets its full threshold of guesses from unlimited IPs. If a port doesn't need to face the internet, closing it beats defending it. Write-up: https://blog.hoelee.com/posts/replacing-rdpguard-with-ipban/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 10. Speech-to-text on my own GPU, and the four traps that cost me a day - **当前状态**:`已发布上线(发布时间: 2026-10-14)` - **系统标识 Key**:`self-hosted-speech-to-text-api` - **博客源文章**:https://blog.hoelee.com/posts/self-hosted-speech-to-text-api/ - **正文字符数**:1392 chars #### LinkedIn 动态正文: ```text I wanted dictation that never leaves my network, so I put whisper.cpp with CUDA on a desktop PC with an RTX 3060, wrapped it in n8n and an nginx gateway, and gave every device its own API key. A Windows PC, an iPhone, an iPad and Android dictate through it, returning a transcript in about two seconds. Every trap looked like something else. The CUDA Toolkit installer ships nvcc without the headers it needs to compile, so the build failed on my config, not a partial toolchain. Whisper binds to 127.0.0.1 by default, so I spent two hours testing the firewall for a loopback-only connection. n8n discards binary data in Code nodes, so my auth step threw the audio away until I passed the data0 reference through — and by default it logs that audio to its database, so **the privacy fix is a setting an unrelated API edit silently resets.** The stock nginx default.conf also declares port 80 and wins as default, so my correct routes kept returning 404. Once the bind, firewall and logging were sorted, one GPU served the whole office; with the model unloading when idle, GPU memory drops from 4,906 MiB to 1,321 MiB, so the machine goes back to development work. The gain is bounded: roughly 5x faster than typing for prose, less for code. The work offices drown in — email, notes, contracts — is where it pays off. Write-up: https://blog.hoelee.com/posts/self-hosted-speech-to-text-api/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 11. Teaching an AI agent to remember: a self-hosted mem0 stack - **当前状态**:`待发布(排队中)` - **系统标识 Key**:`self-hosting-mem0-memory-stack` - **博客源文章**:https://blog.hoelee.com/posts/self-hosting-mem0-memory-stack/ - **正文字符数**:1356 chars #### LinkedIn 动态正文: ```text My AI assistant used to start every conversation from zero. It could run a workflow end to end, but couldn't recall a single fact from the session before — I reintroduced myself and the project every time. Memory fixes that, and I wanted it private. I self-hosted mem0 as a Portainer compose stack on my Synology DS1821+: the mem0 API on port 20015, LiteLLM as the gateway to the fact-extraction model, and PostgreSQL 17 with pgvector in the same database. The API is small. A POST to /memories stores a fact scoped to a user_id so customers don't cross-talk; a search call returns the most relevant by vector similarity. Inject those into the prompt and the loop closes. Two details cost me a day: the routes carry no /v1 prefix, and auth is a plain X-Api-Key header. The gotcha was infer. The default infer=true runs an LLM extractor on every write — smart, but slow and billed per call. For a throwaway note I pass infer=false: verbatim, near-instant, no model round-trip. **Set that from day one.** A memory layer is only as durable as its Postgres, so the DB sits on a protected NAS volume with a backup job, and the API key lives in env secrets, not the compose file. My agent now remembers preferences and decisions across sessions — two HTTP calls, behind my own network. Write-up: https://blog.hoelee.com/posts/self-hosting-mem0-memory-stack/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 12. My SSDs weren't failing — queued TRIM was erasing both RAID mirrors - **当前状态**:`已发布上线(发布时间: 2026-10-13)` - **系统标识 Key**:`the-cause-was-trim-not-the-ssds` - **博客源文章**:https://blog.hoelee.com/posts/the-cause-was-trim-not-the-ssds/ - **正文字符数**:1384 chars #### LinkedIn 动态正文: ```text Two weeks after a btrfs RAID1 pool on healthy NVMe drives corrupted a Windows VM, the same checksum fingerprint returned on a different drive stack. This time it was two Seagate IronWolf 110 SATA SSDs in a btrfs RAID1 pool on Unraid. SMART flagged one as failing, but every real counter was zero and the self-test passed. The damage was elsewhere: btrfs device stats and dmesg showed a checksum equal to the CRC32C of 4 KiB of zeros — empty blocks on both mirrors at the same offsets. RAID1 keeps the same free-space map on both members, discarding the same extents on both at once. Linux auto-TRIM mounts btrfs with discard=async — on SATA that means queued TRIM (NCQ). **FreeBSD blacklisted the IronWolf 110 for it; Linux never did.** It zeroes the same live blocks on both mirrors: the checksum fails on both and self-heal has nothing to copy from. The fix was one variable: auto-TRIM off, then a live remount with nodiscard — no array stop, 36 containers stayed up. A scrub found 18 bad blocks; 16 healed, two were one 4 KiB block bad on both mirrors. Read, verify and super errors were zero: the hardware was fine. What I'd keep: watch all three corruption signals — btrfs device stats, dmesg, scrubs — because they move independently, and check a drive's queued-TRIM record before enabling auto-TRIM. Write-up: https://blog.hoelee.com/posts/the-cause-was-trim-not-the-ssds/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 13. How I vet an open-source dependency before betting on it - **当前状态**:`已发布上线(发布时间: 2026-10-11)` - **系统标识 Key**:`vetting-an-oss-dependency` - **博客源文章**:https://blog.hoelee.com/posts/vetting-an-open-source-dependency-before-you-bet-on-it/ - **正文字符数**:989 chars #### LinkedIn 动态正文: ```text I was handed a 1,300-line architecture spec that named a self-hosted engine as the attribution and commission layer. It assumed three things about that engine: that it spoke OIDC, that its commission rules covered five business shapes, and that white-label was shipped. Ten minutes of `curl` and `grep` later, two of those were false and the third was half-true. The distinction that matters: **the README describes the product; the repository describes the software.** The gap between them is exactly where integration projects die, and it is cheap to measure before you write any code. What I actually check now, in order: does the endpoint I need exist in the routing table, does the feature flag appear in the code or only in the docs, and does the licence allow the thing I'm planning? Three greps, and the spec gets corrected while it's still a document instead of a project plan. Full method: https://blog.hoelee.com/posts/vetting-an-open-source-dependency-before-you-bet-on-it/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 14. Why 500,000 Lines of Commit Churn Only Left 230,000 in Production - **当前状态**:`已发布上线(发布时间: 2026-10-17)` - **系统标识 Key**:`why-500000-lines-of-commit-churn-only-left-230000-in-production` - **博客源文章**:https://blog.hoelee.com/posts/why-500000-lines-of-commit-churn-only-left-230000-in-production/ - **正文字符数**:1526 chars #### LinkedIn 动态正文: ```text Over the last 30 days, my Git commit log recorded +518,000 lines of code added across 5 production repositories. The current codebase, however, only sits at ~235,000 lines. That means more than 130,000 lines were deleted, rewritten, or thrown away. In the era of autonomous coding agents and modern LLMs, generating code has become cheap. What is expensive is pruning it. Here is what running AI coding agents across multiple production systems actually taught me: 1. Velocity without verification is technical debt on steroids. If you don't have automated test harnesses and rigid verification gates, an agent will generate 5,000 lines of plausible-looking bugs before you finish your coffee. 2. Architecture is about what you delete. Modern agents tend to duplicate logic and create sprawling implementations. The senior engineer's job has shifted from typing boilerplate to enforcing boundaries, refactoring, and pruning dead weight. 3. Keep deterministic work deterministic. Knowing when NOT to use an LLM (and using shell scripts, cron jobs, or database constraints instead) saves 10x more engineering hours than any clever prompt. AI didn't make me write 500,000 lines of code. It gave me the throughput to iterate, test, fail, and rewrite systems until only the leanest 235,000 lines remained. How has your team adapted testing and code review practices as AI generation tools enter your daily workflows? Write-up: https://blog.hoelee.com/posts/why-500000-lines-of-commit-churn-only-left-230000-in-production/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: --- ### 15. Why Chrome forgets its tabs in a container — and how I fixed it - **当前状态**:`已发布上线(发布时间: 2026-10-07)` - **系统标识 Key**:`chrome-tabs-in-a-container` - **博客源文章**:https://blog.hoelee.com/posts/why-chrome-forgets-its-tabs-in-a-container/ - **正文字符数**:1249 chars #### LinkedIn 动态正文: ```text I run a real Google Chrome inside a Docker container on my NAS — not a scraper, a browser I actually use: signed into accounts a headless bot should never touch, watchable from any device, drivable over the DevTools Protocol when I want to automate part of a job. It works well, with one exception. Every restart, the browser came back with a single blank tab. Chrome has a setting for exactly this. It was enabled. It did nothing — on a clean stop the desktop session kills the browser, so it never sees the exit it would need to save the session, and a hand-edited preference gets ignored because Chrome treats that file as a hijack target. What finally worked was a ~70-line startup keeper that reopens the saved URLs over CDP and snapshots them every minute. Two landmines on the way: stale `Singleton*` files in the profile silently stop Chrome from starting at all, and the restore has to write an empty snapshot when the user closes everything — otherwise you fight the person using the browser. When a container browser is your workspace, **the tabs are the state.** A browser that opens on about:blank costs you the first ten minutes of every restart. Write-up: https://blog.hoelee.com/posts/why-chrome-forgets-its-tabs-in-a-container/ ``` #### 顾问诊断回填: - [ ] **1. 职场形象与求职定位诊断(Senior 工程师契合度)**: - [ ] **2. 技术逻辑、数据指标与因果严密性**: - [ ] **3. 人设口吻(有无 homelab / 折腾 / 业余玩具感残留)**: - [ ] **4. 判定结论(通过 / 建议微调 / 需重写)及修改建议**: ---