post: 30 days of agent churn kept 23.5万 of 51.8万 lines (EN + ZH)
Deploy / build (push) Successful in 56s

The commit-churn post the owner read and approved today, published in the
first-person voice he asked for.

- Voice rewrite before publishing: the draft opened on "when managers or
  recruiters ask how productive an engineer is" and addressed the reader in the
  second person throughout (you/your 12 -> 2, both in the CTA; ZH 你 5 -> 2).
  It now reads as a log entry: "Last week I did something I almost never bother
  to do: I counted my own commits." Headings moved from essay titles to actions.
  Numbers, the 5-repo table and every claim are unchanged.
- Title changed at the owner's request:
  "Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production"
  -> "I Committed 518,000 Lines in 30 Days. Production Kept 235,000."
  (ZH: 30 天提交 51.8 万行,生产环境只留下 23.5 万行), pubDate 2026-10-18.
- Fixed two real defects found while making the card: the draft claimed "four
  automated gates" and listed three; and og-gen split titles at the word-count
  midpoint, breaking at "…Lines in / 30 Days" — the splitter now prefers a
  sentence boundary near the middle and falls back to the midpoint.
- og PNG regenerated for the new title, thumbs and banner WebP re-derived.
  Measured: card 2 title lines, overflowY 0, measure_og_banner PASS (tags 1 row,
  banner delta 2), build clean, /posts/ monotonic with this post on top.

Also queued the post at the head of the dev.to drip (queue 69 -> 70).
This commit is contained in:
hoelee committed 2026-10-18 17:46:50 +08:00
1 parent 2060aaa166
commit 0d7384224d
9 files changed
+325 -23

No files matched your search

+22 -20
View File
@@ -176,9 +176,10 @@ the URL — the daily dev.to drip below is the first distribution channel outsid
- [x] ✅ **dev.to cross-posting is automated (2026-10-17).** `scripts/devto-crosspost.py` in the Hermes scripts dir
(`C:\Users\hoelee\AppData\Local\hermes\scripts\`) publishes **one queued post per day** with `canonical_url`
pointing at the blog original, from a Hermes **no_agent cron** (`devto daily crosspost`, id `ce8fc8bd23e6`,
`25 12 * * *`, deliver Telegram). Queue = **69 ranked titles / every live post except three** — `hello-world` (blog intro page),
`25 12 * * *`, deliver Telegram). Queue = **70 ranked titles / every live post except two** — `hello-world` (blog intro page),
`how-i-made-my-own-songs-with-suno-ai` (8 `<audio>` players + a lyric `<script>` that dev.to's sanitiser strips)
and the held-for-review post — at **one per day ≈ 2.3 months**. `devto-state.json` makes it idempotent per slug;
— at **one per day ≈ 2.3 months**. The post that had been held for review joined the queue on top on 2026-10-18,
the day it published. `devto-state.json` makes it idempotent per slug;
the API key lives in `~/.secrets/devto-api-key.txt`, never in the repo. **Two guards added the same day:** an
entry is refused while its frontmatter still says `draft: true` or its blog URL is not HTTP 200 (a transient
5xx after 3 retries aborts the whole run instead of skipping, so queue order is never reshuffled by a tunnel
@@ -759,31 +760,32 @@ start, so publishing added no pages. The number in that message is wrong; the de
carry **no prose links** — every post they would naturally cite is dated later, and a backdated post may only link
backwards. Their "related posts" rows are the theme's automatic ones, which every backdated post already has.
**Step B2p — (unplanned) The commit-churn post, held for the read (written 2026-10-17 04:00).** 🔵
**Step B2p — (unplanned) The commit-churn post.** ✅ Published 2026-10-18 (`55b1f08`), after the owner's read and a voice rewrite
| Slug | Category | `pubDate` | Source |
|---|---|---|---|
| `why-500000-lines-of-commit-churn-only-left-230000-in-production` | `engineering` | 2026-10-17 | Hermes session 2026-10-17 |
| `why-500000-lines-of-commit-churn-only-left-230000-in-production` | `engineering` | 2026-10-18 | Hermes session 2026-10-17 |
- **What it argues:** thirty days of autonomous-agent work across five self-hosted Gitea repos = 865 commits, 518,233
additions, 130,892 deletions — and production kept 235,000 lines. Verification gates and pruning, not generation
speed, are what survive.
- **State (all of it measured 2026-10-17, not carried over from the writing session):** EN + ZH written;
og + banner generated and **measured** (`scripts/measure_og_banner.py` → OG `rows=1 overflow=0`, banner 7 rows
`delta=2 overflow=0`); local build clean at **250 `index.html` pages** with both `dist/` pages present; the generator
changes are **purely additive** (`git diff --numstat` 8/0 and 18/0, `node --check` OK on both).
**Not committed, not pushed** — four files (EN + ZH `.md`, og + banner PNG, plus the generator delta) sit in the working
tree on purpose: publication gate, the owner has not read it.
- ⚠ **This entry is the record of that hold.** Without it the next session reads the untracked files as someone else's
in-flight work (or as already-published) — the same failure the skill warns about.
- **Preview for the owner:** `cd dist && python -m http.server 8899 --bind 127.0.0.1`, then
`/posts/why-500000-lines-of-commit-churn-only-left-230000-in-production/` and its `/posts/zh/…` twin.
- **To publish on his go-ahead:** confirm the real date (`date` + the newest `<time>` on the live `/posts/`), then
`git add` the four explicit paths, commit, push `origin main` **and** `github main`, verify the four URLs 200 and the
`/posts/` order still monotonic. The `TERMINALS`/`BANNERS` entries ride along (whole-file staging) and stay inert until
that slug is generated.
- ⚠ **Kill the 8899 preview server once he has read it** — a wedged `python -m http.server` from the writing session was
still holding the port on 2026-10-17 and served an empty response.
- **The owner's read changed two things (both before publishing):**
1. **Voice.** The draft opened *"When managers or recruiters ask how productive an engineer is…"* and kept addressing
the reader in the second person (`you have to move beyond the chat box`, `If your Git log shows…`) — it read like a
LinkedIn think-piece rather than a log entry, and it framed the post as written *for* recruiters. Rewritten in the
first person (EN `you/your` 12 → 2, both in the CTA; ZH 你 5 → 2), headings changed from essay titles to actions
(`What the agents actually touch`, `What changed in how I work`). All numbers, the table and the claims are
unchanged — this was a voice edit, not a new draft.
2. **Title.** `Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production` →
**`I Committed 518,000 Lines in 30 Days. Production Kept 235,000.`** (ZH: `30 天提交 51.8 万行,生产环境只留下 23.5 万行`).
- **Two real defects found while fixing the card:** the draft said *"four automated gates"* and then listed **three**
(now fixed to three); and `og-gen`'s title splitter broke at the word-count midpoint, which put the break at
`…Lines in / 30 Days` — the splitter now prefers a sentence boundary near the middle and falls back to the midpoint
(`.og/gen` measured afterwards: 2 title lines, `overflowY 0`, `measure_og_banner.py` PASS · tags 1 row · banner delta 2).
- **State:** EN + ZH written and rewritten; og PNG regenerated for the new title, thumbs + banner WebP re-derived
(`optimize-images.py`), build clean, `/posts/` order monotonic with the post on top at 2026-10-18.
- ⚠ The four files were held **untracked** for two days on purpose (publication gate). Nothing else is held now.
**Step B2m — The next-session topic queue (user decision 2026-10-16).** ⬜
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+18 -3
View File
@@ -449,11 +449,26 @@ tags = stripped
.filter(Boolean);
// ---------- build template substitutions ----------
// split the long title into two balanced lines for readability
// split the long title into two balanced lines for readability.
// Prefer a sentence boundary near the middle (". " / "? " / "! " / " — " / ": ") so a
// line never dangles on a preposition ("…Lines in / 30 Days") when the title states a
// headline and then a punchline; fall back to the word-count midpoint.
const words = title.split(' ');
const mid = Math.ceil(words.length / 2);
const line1 = words.slice(0, mid).join(' ');
const line2 = words.slice(mid).join(' ');
let line1 = words.slice(0, mid).join(' ');
let line2 = words.slice(mid).join(' ');
const boundary = /[.?!#—:]\s+/g;
let best = -1;
let bm;
while ((bm = boundary.exec(title)) !== null) {
const cut = bm.index + 1; // keep the punctuation on the first line
if (cut < title.length * 0.3 || cut > title.length * 0.75) continue;
if (best === -1 || Math.abs(cut - title.length / 2) < Math.abs(best - title.length / 2)) best = cut;
}
if (best > 0) {
line1 = title.slice(0, best).trim();
line2 = title.slice(best).trim();
}
// escape HTML
const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// title size: short titles can be larger
@@ -0,0 +1,180 @@
---
title: "I Committed 518,000 Lines in 30 Days. Production Kept 235,000."
description: "Over 30 days, autonomous coding agents added 518,000 lines across 5 repos, yet production kept 235,000. Here is why verification gates matter more than generation."
pubDate: "2026-10-18"
category: "engineering"
tags: ["ai", "git", "devops", "testing", "refactoring"]
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
---
Last week I did something I almost never bother to do: I counted my own
commits. Not the ones I remember writing — the ones the repositories
actually recorded. Five production repos on my self-hosted Gitea, thirty
days, one person: 865 commits, 518,233 lines added, 130,892 deleted.
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
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
did.
## The numbers I didn't believe
Here is the exact commit breakdown across the five repositories, between 17
September and 17 October 2026:
| Repository | Commits (30d) | Additions (+) | Deletions (-) | Net Change | Current Total LOC | Primary Stack |
|---|---|---|---|---|---|---|
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
| **Total** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
Two lines are easy to check yourself; this is the whole measurement:
```bash
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
```
In a human team, churn like that usually means trouble: requirements moving
under the code, a rewrite nobody admits to, an architecture that keeps
collapsing. None of those applied here. Every one of these five repos is
something I run and depend on, and every commit was authored in a session
where I could read the diff before it landed.
The churn is what refactoring looks like when generation is cheap. An agent
drafts a slice of the change, I run the gates, the gates disagree, and the
slice gets thrown away or rewritten smaller. On the days I got the gates
right, the size drop came faster than the features arrived.
## What the agents actually touch
Most people's first experience of AI code is a browser tab: paste a function
into a chat window, copy the answer back into an editor. The bottleneck
there is the clipboard. It also keeps the model far away from the two things
that matter — the codebase's own conventions and the ability to run what it
wrote.
So mine run in a terminal, on the machine, with real tools:
1. **Filesystem and syntax tools.** They read existing modules, patch by
fuzzy match instead of overwriting whole files, and run syntax or AST
checks before anything is saved.
2. **Test and execution runners.** PHPUnit suites, pytest harnesses, and
headless Chrome probes, executed in the foreground with the output read
back.
3. **Version control.** They read `git diff`, stage explicit paths, and look
at the history of a file to see why the previous decision was made.
That combination is fast — an order of magnitude faster than me typing the
first draft. It is also, on its own, a way to make a codebase rot faster,
which is the part nobody warns you about.
## Where the bloat comes from
Unsupervised models love to write code. Give one a small edge case and its
instinct is to add a private helper class, clone an existing utility with
one extra parameter, or wrap a library in three layers of abstraction that
only make sense to the diff that introduced them. Left alone for a week,
that is a few thousand lines of confident, plausible, unnecessary code.
I know because I have that week in my own history. Two things came out of
reading these diffs every day:
- A new class has to earn its place against a standard-library call I
already have.
- Anything generated is guilty until a deterministic check says otherwise.
"Looks correct" from a model is not evidence; a red test is.
## The gates that actually deleted the code
During the V2 architecture overhaul of my main production application, more
than 98,000 lines went out in thirty days. Almost none of that was me
sitting down to delete things. It went because three gates kept rejecting
what came back:
### 1. Deterministic harnesses instead of "does this look right?"
Asking a model to review its own code mostly produces confidence, not
correctness. The useful move is to wire the work to something that cannot be
talked out of failing:
- In the PHP application, every public route parameter has to carry an
explicit default, enforced by a reflection probe
(`ControllerAutoRouteSafetyTest.php`) — that test exists because a bare
route crashed with a 500 in production.
- In the report pipeline, a headless PDF extractor counts the rendered
Chinese characters before a build is allowed to pass, so a font or layout
change that silently drops glyphs fails the build instead of shipping a
broken PDF.
- In the transcoder, the ledger compares exact byte counts rather than
approximating in megabytes, because rounded numbers hid a size regression
for weeks.
If a gate goes red, the agent either fixes the change or drops it. Nothing
reaching a commit is exempt from that.
### 2. Pruning as a standing task, not a cleanup project
A portion of every session goes to deleting. Can a 200-line helper be
replaced by a ten-line call into something that already exists? Is this new
mapper duplicating a function that has lived in `app/Libraries/` for a year?
The 130,000 deletions above are not an accident of a rewrite; they are
scaffolding that outlived its purpose, migration layers with one caller, and
logic that arrived twice under two names.
### 3. Keeping deterministic work deterministic
The trap I keep seeing in AI-assisted projects is a model being asked to do
arithmetic it should never touch. If a shell pipeline, a SQL constraint, an
idempotent script, or a cron job can answer it, an LLM has no business
there.
Models are good at drafting an initial implementation, generating a
three-language dictionary, or mapping a third-party API's quirks. They are
not where I put state transitions, money, or queue scheduling, because a
probabilistic answer to a deterministic question is a bug with a delay fuse.
## What changed in how I work
For most of my career the physical constraint was typing speed and
remembering syntax. Output was the metric, and a productive week looked like
a lot of new files.
Syntax is free now. I can get a 500-line migration with tests drafted while
I read the diff of the previous one. So the constraint moved to the three
things a model cannot check for me:
- **Boundary design** — where components separate, and which state is
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.
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
deleted lines, the refactor underneath them, and a suite that is still green
tell me whether the month was worth anything.
## Building something that has to stay up?
I run **Hoelee Enterprise**, a website design and development studio for
Malaysian SMEs. The agent workflow, the test harnesses and the self-hosted
infrastructure in this post are the same setup I build and maintain client
systems on.
If you need full-stack engineering, workflow automation, or infrastructure
that survives its own maintenance:
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
- **Email:** [[email protected]](mailto:[email protected])
- **Website:** [hoelee.com](https://hoelee.com)
@@ -0,0 +1,105 @@
---
title: "30 天提交 51.8 万行,生产环境只留下 23.5 万行"
description: "过去 30 天,自主编程 Agent 在 5 个仓库中累计新增了 51.8 万行代码,但最终留存仅 23.5 万行。为什么在 AI 时代,验证闸门与代码剪枝比生成速度重要十倍?"
pubDate: "2026-10-18"
category: "engineering"
tags: ["ai", "git", "devops", "testing", "refactoring"]
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
---
上周我做了一件平时几乎不会做的事:数了数自己的提交。不是数我记得写过的那些,而是数仓库里真实记录下来的那些。自建 Gitea 上 5 个生产仓库、三十天、一个人:865 次 Commit、新增 518,233 行、删除 130,892 行。
然后我数了数活下来的部分。对主生产项目的最新版本跑一次干净扫描,有效代码总量是 234,976 行;同一时间窗口里净增 58,240 行的视频转码器,今天整个仓库只有 43,050 行。
那 28 万行进哪儿去了?没有丢。它们被写出来、跑过测试、被判为不对,然后删掉——或者被一个更小、做同一件事的东西替换掉。我想把这条循环记下来,因为它这一年对我工作方式的影响,比任何一次模型升级都大。
## 一开始我不信的这几个数字
以下是 2026 年 9 月 17 日至 10 月 17 日,名下 5 个活跃仓库的精确 Git 统计:
| 仓库名称 | 30 天 Commit | 累计新增 (+Add) | 累计删除 (-Del) | 净变动 (Net) | 当前存量有效代码/文本 | 核心技术栈 |
|---|---|---|---|---|---|---|
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
| **合计** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
其中两行你可以自己复算,这就是完整的量法:
```bash
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
```
在人类团队里,这样的更替量通常意味着出事了:需求在代码底下不停移动、有人偷偷在重写、架构反复塌陷。这几种情况我一条都不占——这 5 个仓库都是我每天在跑、离不开的东西,每一次提交都发生在能读到 diff 的会话里。
生成成本变低之后,这种更替量就是重构的样子:Agent 起草一段改动,我跑闸门,闸门不同意,这段就被丢掉或者改小重写。凡是闸门写对的那几天,代码总量往下掉得比功能上来的速度还快。
## Agent 实际接手的是什么
大多数人第一次让 AI 写代码,是在一个浏览器标签页里:把函数粘进对话框,再把答案拷回编辑器。这条路的瓶颈是剪贴板,而且模型离真正重要的两样东西都很远——仓库自己的约定,以及运行它刚写出来的东西的能力。
所以我的是跑在终端里、跑在这台机器上的,手里有真工具:
1. **文件与语法工具**:直接读已有模块,用模糊匹配打补丁而不是整文件覆盖,保存前先过语法与 AST 校验。
2. **测试与执行器**:PHPUnit 单元测试、pytest 套件、无头 Chrome 探针,在前台跑起来并把输出读回来。
3. **版本控制上下文**:读 `git diff`、显式暂存指定路径、翻一个文件的历史看上一个决定为什么这么做。
这套组合很快,比我亲手打第一版快一个数量级;但它本身也是让代码库腐烂得更快的方法——这一半通常没人提醒。
## 臃肿是从哪里长出来的
不受约束的模型天生爱写代码。给它一个细小的边界情况,它的第一反应是新建一个私有辅助类、把一个已有工具函数复制一份改个参数,或者给一个库套上三层抽象——那三层只有在这段 diff 里才讲得通。放它自己跑一周,就是几千行语气笃定、看起来合理、其实没必要的代码。
我这么讲是因为我自己的历史里就有那一周。每天读这些 diff,最后落成两条规矩:
- 新建一个类,要跟一个我本来就有的标准库调用竞争位置。
- 生成出来的东西默认有罪,直到某个确定性检查说它没问题。模型说「看起来对」不算证据,红的测试才算。
## 真正把代码删掉的是三道闸门
在最近一次主生产系统的 V2 架构重构中,三十天里删掉了 98,000 多行。这些几乎不是我坐下来专门去删的,而是三道闸门不断把回来的东西打回,最后留下的就是更小的版本:
### 1. 用确定性脚手架,而不是问「这段看着对不对」
让模型审查自己写的代码,多数时候产出的是自信,不是正确。有用的做法是把它接到一个没法被说服成绿灯的东西上:
- PHP 应用里,所有公开路由参数必须带显式默认值,由专用的反射探针(`ControllerAutoRouteSafetyTest.php`)守着——这个测试存在,是因为一条裸路由在生产上真的 500 过。
- 报告生成管线里,无头 PDF 解析器先数一遍渲染出的汉字数量,数不对就不许构建通过;这样字体或排版改动如果悄悄漏字,就是构建失败,而不是发出一份坏 PDF。
- 转码队列里,台账比的是精确字节数而不是 MB 估算,因为圆整过的数字把一次体积回退藏了好几个星期。
闸门红了,Agent 要么改到过,要么把这段撤掉。没有任何东西能豁免。
### 2. 把剪枝当成日常动作,而不是一次大扫除
每次会话都会分出一段时间专门删东西:这个 200 行的辅助类,能不能换成一行已有的原生调用?这个新的映射层,是不是在重复 `app/Libraries/` 里放了一年的某个函数?上面那 13 万行删除不是重写的副产品,而是活过头了的脚手架、只有一个调用方的迁移层,以及换了名字出现两遍的同一段逻辑。
### 3. 确定性的活留给确定性代码
我在 AI 项目里反复看到的坑,是让模型去做它根本不该碰的计算。如果一条 Shell 管道、一个 SQL 约束、一个幂等脚本或者一个 Cron 任务就能给出答案,那 LLM 就不该出现在那里。
模型擅长起草初始实现、生成三语词库、摸清三方 API 的脾气;但状态流转、钱、队列调度,我不会交给它——一个确定性问题上概率性的答案,是一颗带延时引信的 bug。
## 我的工作方式变了什么
我职业生涯的大部分时间里,物理瓶颈是打字速度和记语法。产出就是指标,一个高效的一周看起来就是新建了很多文件。
现在语法是免费的。我可以一边读上一次改动的 diff,一边让 Agent 起草一份带单元测试的 500 行迁移。于是瓶颈转移到了模型替我看不了的三件事上:
- **边界设计**:模块从哪里分开,哪些状态允许跨过去。
- **验证体系**:那个能抓住类型检查看不见的语义漂移的脚手架。
- **剪枝的决断力**:愿不愿意删掉那 28 万行探索性、冗余、已死掉的代码,只把 23.5 万行送进生产。
所以我现在不再把提交曲线当生产率图表看。一个月新增 50 万行,只能说明工具在转;真正说明这一个月值不值的是那删掉的 13 万行、底下的重构,和一套仍然全绿的测试。
## 需要一套能长期不倒的系统?
我运营 **Hoelee Enterprise**,为马来西亚中小企业做网站设计与系统开发。上面这套 Agent 工作流、测试脚手架和自托管基础设施,就是我给客户交付并长期维护系统时用的同一套东西。
如果你需要全栈系统研发、业务流程自动化,或者能扛住自己维护周期的自建基础设施:
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
- **Email:** [[email protected]](mailto:[email protected])
- **官网:** [hoelee.com](https://hoelee.com)