Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
545c91a0ff | ||
|
|
7149541a92 | ||
|
|
a868bf94fd | ||
|
|
a46430b088 | ||
|
|
1ce9cd65ea | ||
|
|
6e4d6c2f88 | ||
|
|
6abca9f412 | ||
|
|
c9fb46f5a9 | ||
|
|
edc34a8a8c | ||
|
|
83e0b863fc | ||
|
|
9df86cb05b | ||
|
|
d096d45c58 | ||
|
|
073e124344 | ||
|
|
d6e08a29f9 | ||
|
|
18ea5b17b8 | ||
|
|
d119d6b555 | ||
|
|
31fb52cb06 | ||
|
|
6061c16396 | ||
|
|
cfe508ff14 | ||
|
|
b8c5e1c86e | ||
|
|
907f64472a | ||
|
|
6293fe25d5 | ||
|
|
8d82aab213 | ||
|
|
029633f00f | ||
|
|
0ff4ede91f | ||
|
|
23303bd39a | ||
|
|
31eb44caee | ||
|
|
5c015c6577 | ||
|
|
138d3d976b | ||
|
|
d30b5decbb | ||
|
|
2526b98323 | ||
|
|
36643f5cf0 | ||
|
|
52188d5b12 | ||
|
|
fef2de1d37 | ||
|
|
bdaf02f9ca | ||
|
|
c7d1e7b975 | ||
|
|
550f6d15b4 | ||
|
|
d4fe273812 | ||
|
|
0b0307b12f | ||
|
|
3e377adc7f | ||
|
|
63c9c8ad4a | ||
|
|
8e74bead62 | ||
|
|
5bf0c327bf | ||
|
|
6538311387 | ||
|
|
cdd9c61641 | ||
|
|
e1fc8bd64c | ||
|
|
cc12e016ab | ||
|
|
ed473e996a | ||
|
|
c772236d1a | ||
|
|
a89c443c25 | ||
|
|
23b79a2473 | ||
|
|
8274180f81 | ||
|
|
3e09a362cc | ||
|
|
8debd99e61 | ||
|
|
be87917443 | ||
|
|
aaa9cd9274 | ||
|
|
2f7cbcf7bd | ||
|
|
6096a9dafe | ||
|
|
59eda420b3 | ||
|
|
740ec470fa | ||
|
|
3822e150a8 | ||
|
|
26bbb66a72 | ||
|
|
2cab243891 | ||
|
|
c5cdcd5bf9 | ||
|
|
b0a3881725 | ||
|
|
859cac19a2 | ||
|
|
8d221ab36e | ||
|
|
c4bdf46614 | ||
|
|
280654f9e3 | ||
|
|
328ab68f18 | ||
|
|
227bc24797 | ||
|
|
7c1462bde0 | ||
|
|
5c814f0866 | ||
|
|
8f01d825b7 | ||
|
|
f8562510e6 | ||
|
|
952e68e6ea | ||
|
|
46d248877f | ||
|
|
f5232b7c03 | ||
|
|
264148f0a6 | ||
|
|
5be09b6f32 | ||
|
|
de42fed52e | ||
|
|
845c45aa11 | ||
|
|
0e06c25f67 | ||
|
|
3b951d3de6 | ||
|
|
9702c9faf4 | ||
|
|
2e33a7cd2e | ||
|
|
3050b0b959 | ||
|
|
a761cf8129 | ||
|
|
bbb52b9e24 | ||
|
|
8571acdc8b | ||
|
|
127f349fb2 | ||
|
|
d8997c3c72 | ||
|
|
44626f8edc | ||
|
|
54769bcfdf | ||
|
|
541710b50a | ||
|
|
2b8353837a | ||
|
|
3b65cc47ec | ||
|
|
ca7d71325c | ||
|
|
d41fcc058c | ||
|
|
50c7595838 | ||
|
|
bb29c8abaf | ||
|
|
19b2390cbf | ||
|
|
983527a79d | ||
|
|
23ea8e5a55 | ||
|
|
57f19d855b | ||
|
|
a2395be379 | ||
|
|
90b8d0fbc0 | ||
|
|
4e24fe510f | ||
|
|
2bfe2ef3db | ||
|
|
814b138f89 | ||
|
|
c307285a25 | ||
|
|
8ec1712135 | ||
|
|
22930cfffd | ||
|
|
5d18591967 | ||
|
|
c1e3d30954 | ||
|
|
975cf5e58b | ||
|
|
7c5455278b | ||
|
|
6b35356e91 | ||
|
|
a9da765cb1 | ||
|
|
3941a34f04 | ||
|
|
3a59edc79a | ||
|
|
73c31ffb2a | ||
|
|
215760c38a | ||
|
|
55cfc73cb7 | ||
|
|
d00ca7e447 | ||
|
|
363cc616f7 | ||
|
|
0b6524f53c | ||
|
|
71b8985c17 | ||
|
|
3e73fe957e | ||
|
|
810cf44d3d | ||
|
|
0c569b0b31 | ||
|
|
1fdd0cc162 | ||
|
|
aa050d8035 | ||
|
|
a347ce8baf | ||
|
|
8fe9455c23 | ||
|
|
b18d019a2e | ||
|
|
f57b1bde5f | ||
|
|
e57c853f20 | ||
|
|
a10141dfd8 | ||
|
|
bcfd0c35bf | ||
|
|
48a2383efa | ||
|
|
709b18eb45 | ||
|
|
dd1a59bc21 | ||
|
|
ad731aea69 | ||
|
|
93992cb515 | ||
|
|
2802278be6 | ||
|
|
147f00fe1c | ||
|
|
f8add5ad15 | ||
|
|
ad5f9d3d95 | ||
|
|
0e792a70cc | ||
|
|
cef648db9a | ||
|
|
40346c3f21 | ||
|
|
ce587236e2 | ||
|
|
90574edce7 | ||
|
|
3bd8369eda | ||
|
|
b789d30a5d | ||
|
|
5efbbb2f18 | ||
|
|
6f711819e2 | ||
|
|
cc4a71b968 | ||
|
|
9db1ce11e5 | ||
|
|
86c9e105f2 | ||
|
|
3479e53789 | ||
|
|
37ecb2e97b |
No files matched your search
@@ -0,0 +1,92 @@
|
|||||||
|
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-10-25 20:02
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 320px 超窄 en 残溢记录为已知边界(现实机型 ≥360)
|
||||||
|
- 正文排版断点为可读性台阶(有意保留)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 裸 clone 构建改造(Sveltia 自动上线硬前置)
|
||||||
|
- T2 移动端布局全页排查(用户实测报告问题)
|
||||||
|
- T3 Lighthouse 移动端真测(Phase 4 欠账)
|
||||||
|
- T4 /en/archives/ 列表页补缺(现 404)
|
||||||
|
- T5 验证收官
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:构建仓 fetch+reset 硬跟 main;Header clamp 替换阶梯;字体 ?url 非阻塞+按轨挂载
|
||||||
|
- 暂不引入:hamburger 菜单、流体排版(正文有意台阶)
|
||||||
|
- 不做的抽象/配置化:多构建源
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:5/5
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-10-25 20:02
|
||||||
|
- 范围:Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页
|
||||||
|
- 功能:T1 裸 clone 构建改造(Sveltia 自动上线硬前置);T2 移动端布局全页排查(用户实测报告问题);T3 Lighthouse 移动端真测(Phase 4 欠账);T4 /en/archives/ 列表页补缺(现 404);T5 验证收官
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:48a2383efa796b36275f39380e2bd2c805f49690..48a2383efa796b36275f39380e2bd2c805f49690
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
响应式纪律:布局层流式(clamp/auto-fill/容器查询)+固定行 clamp 收缩+超窄内滚;验证=密集宽度扫描(320-768)。@fontsource CSS ?url+preload 非阻塞+按轨挂载(614 @font-face 曾致 Perf 62)。裸 clone 后 staging=origin/main,未推代码用本地 dist 临时口验证。spec 纪律:spec.md 必须当轮写完(模板占位会在归档门禁连环炸);verify 与 complete 之间零提交。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 8.418, "stdout_tail_sha256": "058885ce03c380062ca1fd981422cd0035ec6519c81f42ba7c284ea017c29328", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:14+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.408, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:29+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:29+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.983, "stdout_tail_sha256": "716c18bb1350481ad0930179291341dd6d0622a6c2cc029d84cd4e5fd67f9e79", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:35+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.512, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:49+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.931, "stdout_tail_sha256": "69918ee818f11f5a3ec97f35763d7feea09c48198237dc83711c555916ed6e9b", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:50:41+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 19.577, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:00+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:00+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 6.947, "stdout_tail_sha256": "e23c70f1eaae7d14f05c341a03368cd098d32940deb8ec85842378195894e5d0", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:07+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 1, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:22+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.536, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:52:36+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 7.396, "stdout_tail_sha256": "6e25b47f32694e788da65f94e8e04ad1b4e989b0a43c95599a2810c5bb05cb56", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:16+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.559, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:30+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:30+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.268, "stdout_tail_sha256": "f785c12360c62b0f6859cb2fb4b44d8e5e43df5c3e69c77cef10734b46b51404", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:35+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.491, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:50+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.389, "stdout_tail_sha256": "b81e216373ca24382cdc40ca95fef60f49354bfd7503f9e1dfc331f29a06ac2f", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:21+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.469, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:36+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:36+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.504, "stdout_tail_sha256": "4cce1df670e136194f3948ab1e2adea77fcc503397cfe10bb308f0a88cd25cf3", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:41+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.425, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:56+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.665, "stdout_tail_sha256": "d32d454fb8b15f14778a5dadabf84e948a7599e56e9e6b71a0461170ada259a0", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:25+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.424, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:39+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:39+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.079, "stdout_tail_sha256": "1fc2c0b4b5b5d5d14eb765e97d0bbcac5cc9a79b3cfb7c4dac486d84ba211333", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:45+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.408, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:59+00:00"}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:发布管线确定性(构建源=origin/main);移动端全站溢出修复(用户实测报告);Lighthouse 真分数与 P0 性能优化落地;en/archives 404 补页
|
||||||
|
- **目标用户**:站长(发布/管理)与移动端读者
|
||||||
|
- **核心价值**:Sveltia 保存即上线的硬前置落地;手机体验从"可晃动破版"到全宽段合身
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- 移动审计实锤:Header 7 项一行在 390px 溢出 51-88px(B1);.prose-tech 缺断词(M1)
|
||||||
|
- Lighthouse 诊断:614 个 @font-face 708KB 渲染阻塞=Perf 62 元凶
|
||||||
|
- blog 仓公开 → 裸 clone 匿名 fetch 可行
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 320px 超窄 en 残溢记录为已知边界(现实机型 ≥360)
|
||||||
|
- 正文排版断点为可读性台阶(有意保留)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 页面自适应方式 → 结论:布局层流式(clamp/auto-fill)+ Header clamp 连续收缩 + <350px 导航内滚兜底(用户设计诉求"根据设备自适应"驱动)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- clamp 流式 > 阶梯断点补丁(任意宽度合身,无逐档维护)-> 选流式
|
||||||
|
- 导航内滚 < hamburger 抽屉(保单行设计语言,改动最小)-> 选内滚
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 阶梯断点逐档补丁(430/370 两轮后仍要第三档——治标)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 裸 clone 构建改造
|
||||||
|
- [x] T2 移动端布局全页排查与修复
|
||||||
|
- [x] T3 Lighthouse 真测与 P0/P1 优化落地
|
||||||
|
- [x] T4 /en/archives/ 补页
|
||||||
|
- [x] T5 验证收官
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:构建仓 fetch+reset 硬跟 main;Header clamp 替换阶梯;字体 ?url 非阻塞+按轨挂载
|
||||||
|
- 暂不引入:hamburger 菜单、流体排版(正文有意台阶)
|
||||||
|
- 不做的抽象/配置化:多构建源
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:git fetch/reset、CSS clamp、@fontsource ?url、Playwright 密集宽度扫描
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:infra/deploy/、src/components/Header.astro、src/styles/global.css、src/pages/、scripts/lighthouse/
|
||||||
|
- 本轮不应触碰:nginx、gitea 运行时、生产内容
|
||||||
|
- Git integration branch:`spec/2026-10-25_add-blog-5c`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:构建管线、Header 流式化、移动端缺陷修复面、字体加载策略、en/archives 页
|
||||||
|
- 快速检查:`pnpm astro build`
|
||||||
|
- 集成检查:`node tests/e2e.mjs`(30 项)+ 移动溢出密集扫描(宽段 320-768 × scrollWidth 断言)
|
||||||
|
- 全项目检查:Lighthouse 复跑对账(Perf 62→88 基线)+ 生产移动端抽查;仅在构建链或 Header 结构再变更时升级范围
|
||||||
|
- 升级触发:仅在 nginx 面、构建链、发布载荷变更时升级验证范围并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 移动溢出密集扫描 44 检全过(320-768) -> verify: task_0002
|
||||||
|
- [x] staging 恒等 origin/main(裸 clone 构建) -> verify: task_0001
|
||||||
|
- [x] Lighthouse Perf 62→88 实测 -> verify: task_0003
|
||||||
|
- [x] en/archives 200 -> verify: task_0004
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- ESA 边缘缓存旧页会伪装修复失效(刷新后为准);构建仓 node_modules 留存做增量(loader 逻辑变更须清 .astro 数据仓——前包教训)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 裸 clone 构建改造(Sveltia 自动上线硬前置)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 只改 infra/deploy/build.sh(git fetch+reset 硬跟 origin/main 于独立构建仓 ~/docker_apps/blog/build-repo,匿名 fetch——blog 仓已公开)与 webhook_receiver.py 的 BUILD 常量;webhook 触发实测 main 提交自动上线
|
||||||
|
- verify: `bash infra/deploy/build.sh && grep -q "0e792a7\|[a-f0-9]\{7\}" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh`
|
||||||
|
|
||||||
|
- [x] T2 移动端布局全页排查(用户实测报告问题)
|
||||||
|
- claim: orchestrator + 视觉车道
|
||||||
|
- boundary: 390px/360px 双宽 × zh/en × 亮/暗全页截图交视觉模型;发现的缺陷修复;不重构整体布局只修缺陷
|
||||||
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
|
||||||
|
|
||||||
|
- [x] T3 Lighthouse 移动端真测(Phase 4 欠账)
|
||||||
|
- claim: op-gitea(mesh 车道)
|
||||||
|
- boundary: lighthouse CI 对本机 staging 18743 跑 mobile emulation 五页型(首页/文章/图库/en 首/terminal);记录分数;<90 项出可执行优化清单(不改代码,修复下一轮)
|
||||||
|
- verify: `test -f /tmp/e2e-shots/lighthouse-report.json`
|
||||||
|
|
||||||
|
- [x] T4 /en/archives/ 列表页补缺(现 404)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/pages/en/archives/index.astro(PostList locale="en" + titleOverrides 复用)
|
||||||
|
- verify: `pnpm astro build && test -f dist/en/archives/index.html`
|
||||||
|
|
||||||
|
- [x] T5 验证收官
|
||||||
|
- depends-on: task_0001, task_0002, task_0003, task_0004
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: E2E + 移动端修复后视觉复审 + 归档推送上线
|
||||||
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前进度**:由脚本计算,无需手动维护
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Phase 5b:/terminal 彩蛋 + /books 书评画廊 + Sveltia 管理台试水 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-10-25 13:46
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- Sveltia 内建图片优化保持关闭(开了会抹 EXIF 断构建期提取管线)
|
||||||
|
- 书评 description 由正文首段精炼可接受(用户后续可自行策展润色)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 /terminal 终端彩蛋页
|
||||||
|
- T2 /books 书评画廊
|
||||||
|
- T3 Sveltia 管理台试水
|
||||||
|
- T5 /opensource 开源独立页(用户追加需求)
|
||||||
|
- T4 验证收官
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:三车道并行按预研方案实施;Sveltia 试水不改现有任何流(只加不改)
|
||||||
|
- 暂不引入:终端插件化命令系统、书封真实图片扫描
|
||||||
|
- 不做的抽象/配置化:多管理后端适配
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- moments PWA 上传端点、/stack·/colophon 页(后续池)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- moments PWA 上传端点、/stack·/colophon 页(后续池)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:5/5
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git Records
|
||||||
|
- Date Time:2026-10-25 13:46
|
||||||
|
- Scope:Phase 5b:/terminal 彩蛋 + /books 书评画廊 + Sveltia 管理台试水
|
||||||
|
- Feature:T1 /terminal 终端彩蛋页;T2 /books 书评画廊;T3 Sveltia 管理台试水;T5 /opensource 开源独立页(用户追加需求);T4 验证收官
|
||||||
|
- Action:Commit / Push
|
||||||
|
- Effect:Development Record 行为门禁通过
|
||||||
|
- Commit:pending local commit
|
||||||
|
- 提交区间:f1ee07838d577125f8f1677e6b662a3bb320ce32..1b610f0bbf430aae3f001cf0c8138ab775fc0662
|
||||||
|
- Push:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Astro 7 内容层双坑(EXIF 全空调试半日定位):①loader 包装层在 baseLoader.load() 之后 ctx.store.set 的字段会被解析阶段静默丢弃——必须拦截 baseLoader 的写入时机内联增强(代理 store.set,enrich Promise 收集后 await);②持久化数据仓(node_modules/.astro)增量复用旧条目——改 loader 逻辑后必须清数据仓或换 loader name,否则旧值复利胜出。团队协作:车道临时服务端口要与主验证端口分离(18742 Sveltia 冲突教训);多车道并发时 add -A 会卷入他车道文件(提交信息如实备案或收窄 add)。Sveltia×gitea 机制:整文件 JSON=sidecar 同构、删条目连带独占 jpg(实测推翻文档)、omit_empty_optional_fields 会丢必需空字段。裸 clone 构建改造是 Sveltia 自动上线的硬前置(下一包首任务)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"terminal\" dist/terminal/index.html", "exit": 0, "duration_s": 5.636, "stdout_tail_sha256": "8523a98a4dd54e5d8b6298e9f9e56fc6eb56afd1d70821ce5c1a0a8d871673a7", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:42:41+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "pnpm astro build && test -f dist/books/index.html -a -f dist/en/books/index.html", "exit": 0, "duration_s": 4.88, "stdout_tail_sha256": "b05fcfb369a91ea39a036fc67dd5a29bcefe1db26e624bfd9b7a678b40f57b87", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:42:46+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f public/admin/config.yml && docker exec gitea grep -q \"cors\" /data/gitea/conf/app.ini", "exit": 0, "duration_s": 0.094, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:42:46+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.517, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:43:01+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"terminal\" dist/terminal/index.html", "exit": 0, "duration_s": 10.825, "stdout_tail_sha256": "39e841a84204b8c060604577adb41f13605ef755f90a4dac28ae4ed7f6b07dee", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:45:10+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "pnpm astro build && test -f dist/books/index.html -a -f dist/en/books/index.html", "exit": 0, "duration_s": 4.703, "stdout_tail_sha256": "167f39f390b7753e6f76bab112f849fca4f16d8fa06e319b0e5363c6fb7ba8eb", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:45:15+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f public/admin/config.yml && docker exec gitea grep -q \"cors\" /data/gitea/conf/app.ini", "exit": 0, "duration_s": 0.088, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:45:15+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/opensource/index.html -a -f dist/en/opensource/index.html", "exit": 0, "duration_s": 5.218, "stdout_tail_sha256": "0975341de4c977c9261a1b80c4619480804a65775c330f6bb6a389c73d158868", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:45:20+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.389, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "9325637dacb5cbd2a4a86329e669a85bbf5428735a2bb72c5ad7a82e00ea1624", "at": "2026-10-25T05:45:34+00:00"}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Phase 5b:/terminal 彩蛋 + /books 书评画廊 + Sveltia 管理台试水 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:三个预研方案落地——终端彩蛋(极客人格记忆点)、书评索引画廊(阅读轨策展面)、Sveltia 管理台(照片上传/编辑体验升级试水)
|
||||||
|
- **目标用户**:站长(写作/管理体验)与读者(浏览乐趣)
|
||||||
|
- **核心价值**:二期池前三项交付;Sveltia 试水失败可全身而退
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- 三方案预研完成(op-artalk A+B / op-gitea Sveltia,含坑清单与工作量)
|
||||||
|
- Sveltia 机制全对上:整文件 JSON=sidecar 同构、media_folder '' 裸文件名、token 登录、保存即 commit→webhook 自动构建
|
||||||
|
- gitea CORS 未配是唯一硬前置
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- Sveltia 内建图片优化保持关闭(开了会抹 EXIF 断构建期提取管线)
|
||||||
|
- 书评 description 由正文首段精炼可接受(用户后续可自行策展润色)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] Sveltia 试水体验是否合手 → 结论:值得留(双冒烟过,编辑器 aria/草稿保存扎实);生产自动化待裸 clone 改造(下一包首任务)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- /books 不进主导航(书评列表行内入口)> 导航加第五项 -> 防膨胀
|
||||||
|
- 色脊排版卡 > 满色书封卡(mood 深浅不一跨亮暗风险)-> 色脊
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 /terminal 彩蛋页
|
||||||
|
- [x] T2 /books 书评画廊(zh/en)
|
||||||
|
- [x] T3 Sveltia 管理台试水(含 gitea CORS)
|
||||||
|
- [x] T5 /opensource 开源独立页(2026-10-25 用户追加:与文章/图库/关于平权)
|
||||||
|
- [x] T4 验证收官
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- /books 进导航(若用户要求)
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- moments PWA 上传端点、/stack·/colophon 页(后续池)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:三车道并行按预研方案实施;Sveltia 试水不改现有任何流(只加不改)
|
||||||
|
- 暂不引入:终端插件化命令系统、书封真实图片扫描
|
||||||
|
- 不做的抽象/配置化:多管理后端适配
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:预研方案原样(vanilla TS 终端 / 色脊卡静态页 / Sveltia 官方集成)
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/pages/、src/content.config.ts、content/posts/(五篇补字段)、public/admin/、infra/gitea/README、gitea 运行时 [cors]
|
||||||
|
- 本轮不应触碰:content/photos 既有 sidecar 结构、发布链、生产 nginx
|
||||||
|
- Git integration branch:`spec/2026-10-25_add-terminal-books-admin`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:两个新页面、posts schema 扩展、admin 静态目录、gitea 运行时
|
||||||
|
- 快速检查:`pnpm astro build`
|
||||||
|
- 集成检查:terminal/books 产物断言 + Sveltia 双冒烟走通 commit→webhook→build
|
||||||
|
- 全项目检查:E2E 30 项 + 新页视觉复核;仅在 gitea 运行时或发布链变更时升级范围
|
||||||
|
- 升级触发:只有 Sveltia 冒烟引发 gitea 行为异常时,才升级验证范围并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] /terminal 可交互(help/ls/theme/sudo 彩蛋实跑) -> verify: task_0004
|
||||||
|
- [x] /books zh/en 五书呈现(色脊+mood) -> verify: task_0002
|
||||||
|
- [x] Sveltia 冒烟双通过(改+建全链) -> verify: task_0003
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- gitea restart 窗口(秒级,公告无需);Sveltia 保存直连 main——已有 webhook 管线为安全网
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Phase 5b:/terminal 彩蛋 + /books 书评画廊 + Sveltia 管理台试水 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言;三车道并行(mesh_reserve 各自路径)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 /terminal 终端彩蛋页
|
||||||
|
- claim: op-artalk(预研方案 A 已在案:命令注册表/真实 input/历史翻阅/Tab 补全/sudo 彩蛋/XSS=textContent 纪律/isComposing 守卫)
|
||||||
|
- boundary: 新建 src/pages/terminal.astro 单文件组件(--code-bg 恒深色例外区);不进主导航(? 帮助浮层加一行暗入口提示);零外部依赖
|
||||||
|
- verify: `pnpm astro build && grep -q "terminal" dist/terminal/index.html`
|
||||||
|
|
||||||
|
- [x] T2 /books 书评画廊
|
||||||
|
- claim: orchestrator(预研方案 B 在案:色脊排版卡/年份分组/规整网格)
|
||||||
|
- boundary: posts schema 加可选 description/book_author 两字段;新建 src/pages/books.astro(+en/books);五篇书评补字段(description 用正文首段精炼,book_author 按书实填);导航"文章"旁不加新项——/books 从书评列表行入口链接(防导航膨胀)
|
||||||
|
- verify: `pnpm astro build && test -f dist/books/index.html -a -f dist/en/books/index.html`
|
||||||
|
|
||||||
|
- [x] T3 Sveltia 管理台试水
|
||||||
|
- claim: op-gitea(预研方案在案:CORS 前置/config.yml 草案/双冒烟/五坑清单)
|
||||||
|
- boundary: gitea app.ini 加 [cors] 段并重启(运行时操作,README 台账补记);public/admin/{index.html,config.yml,sveltia 本地 js};双冒烟(改旧 sidecar 补 alt / 新建条目传图走全链);不加站内链接(试水期)
|
||||||
|
- verify: `test -f public/admin/config.yml && docker exec gitea grep -q "cors" /data/gitea/conf/app.ini`
|
||||||
|
|
||||||
|
- [x] T5 /opensource 开源独立页(用户追加需求)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: ProjectWall 加 limit prop(默认 6 不变首页);opensource.astro + en/opensource.astro(全量仓+热力图+开源叙事);Header 导航第四项(i18n nav_opensource);BooksPage 回退契约与自托管徽标随后续提交补强
|
||||||
|
- verify: `pnpm astro build && test -f dist/opensource/index.html -a -f dist/en/opensource/index.html`
|
||||||
|
|
||||||
|
- [x] T4 验证收官
|
||||||
|
- depends-on: task_0001, task_0002, task_0003
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: E2E 复跑 + terminal/books 截图视觉复核 + Sveltia 冒烟结论 + 归档推送
|
||||||
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前进度**:由脚本计算,无需手动维护
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-01 09:51
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- nginx sudo 与 ESA 控制台由用户操作(本包只备好仓库侧与操作单)
|
||||||
|
- 视觉裁定为终态依据;实现与裁定冲突时以裁定为准并在完成总结记录
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单)
|
||||||
|
- T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
|
||||||
|
- T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
|
||||||
|
- T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
|
||||||
|
- T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
|
||||||
|
- T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
|
||||||
|
- T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
|
||||||
|
- T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:token 值级 CSS 改动为主 + locale.ts 单函数修 + 两个构建脚本;无新依赖
|
||||||
|
- 暂不引入:blur-up 占位(需管线)、独立设置面板
|
||||||
|
- 不做的抽象:主题引擎化/样式体系重构(tokens 增量演化)
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:8/8
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-01 09:51
|
||||||
|
- 范围:Phase 5f:SEO 解封与功能优化批 + 美化首批
|
||||||
|
- 功能:T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单);T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一;T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载;T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化);T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点);T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框);T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring);T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:975cf5e58bf2846fc0c1e34a600725bcfe915f0e..23ea8e5a555a984cf0e8be46efecb718d26d3cdc
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5f 教训与资产:①收官门禁代码化(spec_gate.py 首秀连环拦下五宗——E2E 口径错配/竞态雷/verify 管道笔误/5.1 双字段措辞/字段改名丢失,每次给出可执行下一步);②E2E_BASE 覆写为 spec 验收标准姿势;③视觉双轨闭环(token 方案→flash:max 像素验收);④? 浮层焦点竞态根治;⑤车道 verify 落点偏差随回执报告;⑥add -A 卷入在途车道第四次变体秒拆;⑦verify 断言禁用 -q 接管道;⑧spec 模板字段名不可创意改写(升级触发≠升级条件)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 1, "duration_s": 0.669, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.06, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 1, "duration_s": 0.637, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:24+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 1, "duration_s": 0.675, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:25+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 0.638, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:25+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 1, "duration_s": 0.656, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:26+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 1, "duration_s": 0.66, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:27+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:31:48+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 15.103, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:03+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.073, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:04+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 1, "duration_s": 14.313, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:18+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.413, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:32+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 14.48, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:47+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 1, "duration_s": 14.192, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:33:01+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.446, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:33:15+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:34:48+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.521, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:02+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.056, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:02+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.398, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:17+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.495, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:31+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 14.653, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:46+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.506, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:36:00+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.668, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:36:15+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:05+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.603, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:19+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.071, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:19+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:34+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.324, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:48+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.598, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:03+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 15.156, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:18+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:32+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:01+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.593, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:16+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.058, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:16+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.429, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:31+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.396, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:45+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.486, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:59+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.479, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:43:14+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.333, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:43:28+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:49:52+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.485, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:06+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.062, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:06+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.474, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:21+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.495, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:35+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.482, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:50+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.59, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:51:04+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.452, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:51:19+00:00"}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:①解除生产全站 noindex 封印并补齐 SEO 基建(事故级:上线至今搜索引擎不可见)②修双语暗伤(localePath 双 en/canonical/尾斜杠)③构建期内容 lint+生产冒烟防线 ④reader 纯文本模式 ⑤美化首批 K-Q(流距 token 化/灰阶补档/首屏 tagline/图库质感/微交互/代码块窗条/书评纸面板,全部经视觉模型裁定定案)
|
||||||
|
- **目标用户**:站长(写作/发布/被搜索到)与读者(阅读质感/双语导航正确性)
|
||||||
|
- **核心价值**:站点从"隐身可用"到"可被发现+有质感";发布链自带质检
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- noindex 来源=/etc/nginx/snippets/esa-origin-ssl.conf:11(我已 curl+grep 实锤);robots.txt 生产 404;ESA 头缓存 30 天
|
||||||
|
- localePath 在 en→en 场景产出 /en/en/...(404)
|
||||||
|
- Artalk 评论数端点已实测(GET /api/v2/comments?site_name=wait4x.online&limit=0 → count)
|
||||||
|
- 美化七问已由 glm-5.3-flash:max 逐图裁定(docs/research/08 §6)
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- nginx sudo 与 ESA 控制台由用户操作(本包只备好仓库侧与操作单)
|
||||||
|
- 视觉裁定为终态依据;实现与裁定冲突时以裁定为准并在完成总结记录
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 美化两案之争 → 视觉模型裁定完成(08 文档 §6),无悬案
|
||||||
|
- [x] 本批范围 → 用户 2026-10-26 全批(A/B/C/E+K-Q;R 热力图暗色与 S 并入 N 后的下一批)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 纸面板=全边框圆角卡(裁定)优于通栏 hairline——保留标题区外置结构
|
||||||
|
- 代码块窗条仅 ≥4 行块;三点换灰阶(红线优先于 macOS 梗)
|
||||||
|
- 图库亮色直贴/暗色 4% 衬底(双模式分判)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 首屏 banner 化/大字标语(视觉裁定否决——克制红线)
|
||||||
|
- 热力图亮色换绿(裁定否决——全页最大色块压灰阶骨架)
|
||||||
|
- 页面标题 27→22px(裁定否决——层级锚点,要轻减字重不减号)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 SEO 仓库侧三件:robots.txt 新建 + /admin 局部 noindex 预案 + 用户操作单(nginx/ESA)
|
||||||
|
- [x] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
|
||||||
|
- [x] T3 构建期内容 lint(断链/图片/frontmatter/双语配对)+ 生产冒烟脚本 + build.sh 守卫挂载
|
||||||
|
- [x] T4 reader 纯文本模式(html.reader 单 class + 命令面板 reader + r 键 + localStorage)
|
||||||
|
- [x] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点,单行裸 pill)
|
||||||
|
- [x] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框防长文竖线感)
|
||||||
|
- [x] T7 版式基底(K/L/O 卡:正文流距 token 化+灰阶补档+暗色 hairline 降档+微交互曲线统一+focus ring 收尾)
|
||||||
|
- [x] T8 首屏 tagline + 图库质感(M/N 卡:IdCard 身份语≤20 字+图库圆角统一+暗色 4% 衬底)
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无(R 热力图暗色补救列下一批)
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:token 值级 CSS 改动为主 + locale.ts 单函数修 + 两个构建脚本;无新依赖
|
||||||
|
- 暂不引入:blur-up 占位(需管线)、独立设置面板
|
||||||
|
- 不做的抽象:主题引擎化/样式体系重构(tokens 增量演化)
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:既有 Astro+tokens 体系;lint 用 node 脚本(复用 build-perf 目录与守卫模式)
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/lib/locale.ts、src/layouts/BaseLayout.astro、src/styles/{tokens,global}.css、src/components/{CommandPalette,IdCard*}.astro、src/components/geek/Keyboard.astro、GalleryPage.astro、prose 相关组件、public/robots.txt、scripts/build-perf/、infra/deploy/build.sh、tests/e2e.mjs
|
||||||
|
- 本轮不应触碰:nginx/ESA(用户)、Header/Footer 结构、sw.js(无涉)、热力图组件
|
||||||
|
- Git integration branch:`spec/2026-10-26_blog-5f`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:SEO 基建文件、locale 工具、构建链 lint 挂载、reader/美化涉及的样式与组件
|
||||||
|
- 快速检查:`pnpm astro build`
|
||||||
|
- 集成检查:`node tests/e2e.mjs`(33 项)+ 本包新增断言(canonical/localePath/reader/窗条/纸面板 computed-style)
|
||||||
|
- 全项目检查:生产 Lighthouse SEO≥95 复测(依赖用户 nginx/ESA 操作单完成)+ 移动溢出密集扫描;仅在 nginx/ESA 面、构建链或 Header 布局再变更时升级范围
|
||||||
|
- 升级触发:T7 灰阶补档若波及 Header/导航布局,或用户操作单完成后生产复测不达标,升级为全站密集扫描与 LH 多轮中位并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] dist 每页含自指 canonical 且 en 页无 /en/en/ -> verify: task_0002
|
||||||
|
- [x] robots.txt 产物存在且含 Sitemap 行 -> verify: task_0001
|
||||||
|
- [x] 人为断链能让 build 输出 FAIL 清单(warn 一周策略首版即 fail 可议) -> verify: task_0003
|
||||||
|
- [x] reader 触发后 .bg-glow 隐藏/ICP 可见/刷新保持 -> verify: task_0004
|
||||||
|
- [x] 多行代码块有窗条与灰阶三点、单行无 -> verify: task_0005
|
||||||
|
- [x] 书评正文为圆角纸卡(computed border-radius+背景) -> verify: task_0006
|
||||||
|
- [x] 流距 token 生效(h2 前后距为变量值)+focus ring 统一 -> verify: task_0007
|
||||||
|
- [x] 首屏 tagline 渲染≤20 字+图库圆角统一 -> verify: task_0008
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶红线:所有美化以 08 §6 裁定为终态依据
|
||||||
|
- build.sh 引用新脚本守卫先行(车道纪律);lint 首版 fail 可降 warn 一周(发布链安全优先)
|
||||||
|
- 多车道并行:T2/T3(op-gitea)、T5/T6(op-artalk)、T1/T4/T7/T8(orchestrator);共享文件按 boundary 划界,global.css/tokens 仅 orchestrator 动
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 新建 public/robots.txt(Allow 全站;Disallow /admin/ /draft/;Sitemap 行);docs/上线检查单.md 追加「SEO 解封操作单」节(nginx snippet 行号/内部 vhost 补回清单/ESA purge+TTL+绕缓存三动作);nginx/ESA 实操归用户
|
||||||
|
- verify: `test -f public/robots.txt && grep -q "Sitemap: https://wait4x.online/sitemap-index.xml" public/robots.txt && grep -q "Disallow: /admin" public/robots.txt && grep -q "SEO 解封操作单" docs/上线检查单.md`
|
||||||
|
|
||||||
|
- [x] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
|
||||||
|
- claim: op-gitea(mesh 车道)
|
||||||
|
- boundary: src/lib/locale.ts(先归一 zh 再拼前缀);BaseLayout.astro 加自指 canonical(Astro.site+withSlash);Header.astro nav-lang href 尾斜杠补齐;E2E 可加断言
|
||||||
|
- verify: `! grep -r "/en/en/" dist/ && grep -q 'rel="canonical"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS"`
|
||||||
|
|
||||||
|
- [x] T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
|
||||||
|
- claim: op-gitea(mesh 车道)
|
||||||
|
- boundary: 新建 scripts/build-perf/content-lint.mjs(dist 断链:站内 a[href] 逐一对产物;图片存在性;zh/en 配对警告)+ scripts/build-perf/prod-smoke.sh(curl robots/sitemap/首页头三断言);build.sh 守卫行(存在性守卫铁律);首版 fail 但提供 SKIP 环境变量逃生门
|
||||||
|
- verify: `node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh`
|
||||||
|
|
||||||
|
- [x] T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: global.css 尾部集中 html.reader 段(按 08 文档 §4.4 清单:隐 bg-glow/进度条/收 Header 尾部钮/Footer 收单行 ICP 必留;正文字号微升行宽收窄);CommandPalette data-cmd="reader" + ui.ts cmd_reader 双语键;Keyboard r 单键;localStorage 持久化;reduced-motion/无 JS 天然兼容(纯 class)
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q 'data-cmd="reader"' src/components/CommandPalette.astro && grep -q "html.reader" src/styles/global.css`
|
||||||
|
|
||||||
|
- [x] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
|
||||||
|
- claim: op-artalk(mesh 车道)
|
||||||
|
- boundary: 技术轨代码块渲染层(自查源码定落点:prose-tech 容器/全局 pre 样式);≥4 行块加窗条(灰阶三点非红黄绿+mono 语言/文件标签),单行块裸 pill;亮暗双态;scoped 样式优先不动 global.css(与 T7 划界)
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rqE "code-win|codebar|窗条" src/pages/archives/`
|
||||||
|
|
||||||
|
- [x] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
|
||||||
|
- claim: op-artalk(mesh 车道)
|
||||||
|
- boundary: 书评正文容器(prose-reading 所在组件 scoped 样式):圆角纸卡+米灰 #f4f4f2 系亮/暖棕暗态;边框 hairline 级防长文竖线感(08 裁定 §6-2);标题区外置结构不动;--paper 变量需求提给 orchestrator 入 tokens(T7 协同),不直接改 tokens.css
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "paper" src/components/`
|
||||||
|
|
||||||
|
- [x] T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
|
||||||
|
- claim: orchestrator(tokens/global 专属车道)
|
||||||
|
- boundary: tokens.css(--flow-* 流距五变量+灰阶补档评估+--paper 纸色变量);global.css(h2/h3/p+p 改用流距变量、暗色 hairline 降一档、过渡曲线统一 token :where 一段、focus-visible ring 统一);标题字重 700→600(裁定 #6);行距 +2~4px(裁定 #7);移动端密集扫描复测(涉布局面)
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q "flow-h2-pre" src/styles/tokens.css && grep -q "focus-visible" src/styles/global.css`
|
||||||
|
|
||||||
|
- [x] T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 首页身份区(IdCard 或等位组件)加 tagline ≤20 字(措辞备两案入完成总结供用户选,默认「写作者 · 读者 · 快门按压者」);GalleryPage 圆角统一 --radius-img 暗色 4% 衬底(裁定 §6-4 双模式分判;衬底圆角≤照片圆角防框中框);亮色直贴不动
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "tagline" src/pages/index.astro src/components/ && grep -q "radius-img" src/styles/tokens.css`
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-02 19:19
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 块渲染器受控,破版风险低于整页 HTML 直编(方案 10 §2 对比表)
|
||||||
|
- 字体子集化(pyftsubset 按页实际用字)能把艺术字体单页增量压到 ≤150KB 阈值内
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 blocks 骨架(关于页块式页面构建器)
|
||||||
|
- T2 块型扩充与图片管线
|
||||||
|
- T3 样式即数据(theme.yaml + 对比度闸门)
|
||||||
|
- T4 首页细化与去重
|
||||||
|
- T5 开源页收尾(缓存/回落/告警)
|
||||||
|
- T6 文档与后台 hint 补齐
|
||||||
|
- T7 字体子集化工具
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:块类型先用 3 类跑通闭环(prose/tracks/divider),再扩到 9 类;theme 只做 12 个白名单旋钮
|
||||||
|
- 暂不引入:可视化块预览、块级动画配置
|
||||||
|
- 不做的抽象:通用页面 DSL(只服务现有页面)
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:7/7
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-02 19:19
|
||||||
|
- 范围:Phase 5g:页面后台化(blocks 模型)与字体子集化
|
||||||
|
- 功能:T1 blocks 骨架(关于页块式页面构建器);T2 块型扩充与图片管线;T3 样式即数据(theme.yaml + 对比度闸门);T4 首页细化与去重;T5 开源页收尾(缓存/回落/告警);T6 文档与后台 hint 补齐;T7 字体子集化工具
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:3822e150a87ac5fee56c9b2724eae23389e7c34e..23b79a2473674e75608736e2cac491b163a49c35
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5g 资产:①块式页面构建器(九类 discriminatedUnion + 宽松分支容错:已知块严格校验、未知块 WARN 丢弃不炸构建);②样式即数据(theme.yaml 九组白名单旋钮 + :root:root 特异性注入 + 产物级对比度闸门,负例 1.61:1 被拦);③首页抽 HomePage 消 zh/en 双份;④字体子集化工具(汇文 694→420KB −40%;快乐体实测反向 −9% 故不启用,结论入档避免后人重试);⑤config.yml 重复键结构缺陷的教训——Python yaml 静默覆盖,必须用 StrictLoader/Node yaml 校验,已固化为 build 闸门 config-check.py;⑥并发构建擦 dist 的解法:build→cp -a dist 快照→快照起服务跑 E2E。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && grep -q \"discriminatedUnion\" src/content.config.ts && grep -q \"BlockRenderer\" src/components/pages/AboutPage.astro && grep -q \"typeKey\" public/admin/config.yml", "exit": 0, "duration_s": 18.466, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:21+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && ls src/components/blocks/ | grep -qC1 \"Cards\\|Timeline\" && grep -q \"image()\" src/content.config.ts", "exit": 0, "duration_s": 17.177, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:39+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && test -f content/site/theme.yaml && node scripts/build-perf/contrast-check.mjs dist", "exit": 0, "duration_s": 17.193, "stdout_tail_sha256": "f66c66c7ce879dad3f789790f8607fbd50e7a5a894e12912a55fd47d082e4f77", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:56+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && test -f src/components/pages/HomePage.astro && grep -q \"HomePage\" src/pages/en/index.astro", "exit": 0, "duration_s": 17.189, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:13+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && python3 -c \"import json;d=json.load(open('src/data/projects-fallback.json'));assert len(d)>=5\"", "exit": 0, "duration_s": 17.214, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:30+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "test -f docs/管理台.md && grep -q \"blocks\\|区块\" docs/管理台.md && grep -c \"hint\" public/admin/config.yml | awk '$1>=20'", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "913f5d1da2feaf4deeccc9e55cbb350a20f12b3f507e87be85dbb77fdd3cb9bc", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:30+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "test -f scripts/build-perf/subset-fonts.py && python3 -c \"import fontTools; print('fonttools ok')\" && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\"", "exit": 0, "duration_s": 17.19, "stdout_tail_sha256": "3bcefe2ef2e30a5bbe067d31368a9765d7843d0cc24738a60c946c020e581348", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:47+00:00"}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:①按 docs/research/10 方案 E 把关于页改造成「块式页面构建器」(结构可增删排序)②样式即数据(theme.yaml → 构建期 CSS 变量,含对比度闸门)③首页字段细化并抽 HomePage 消 zh/en 双份 ④开源页收尾(README 缓存/回落/告警)⑤后台文档与 hint 补齐 ⑥字体子集化工具(把艺术字体单页增量从数百 KB 降到数十 KB)
|
||||||
|
- **目标用户**:站长(希望整页可编辑、能上更多艺术字体)与读者(版式与可读性不退化)
|
||||||
|
- **核心价值**:内容与结构的编辑权交回后台,同时守住灰阶体系/双语/SEO/构建期零运行时四条红线
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- Sveltia 0.252 支持 list 的 types + typeKey(方案 10 §3.4 从本仓 bundle 取证),块模型可行
|
||||||
|
- 页面文案 markdown 净化(D1/D2)已在 2cab243 落地(P0 完成)
|
||||||
|
- 字体实测:@fontsource CJK 切片粒度导致单页 +163KB~+8605KB;现有 6 自托管族为基线
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 块渲染器受控,破版风险低于整页 HTML 直编(方案 10 §2 对比表)
|
||||||
|
- 字体子集化(pyftsubset 按页实际用字)能把艺术字体单页增量压到 ≤150KB 阈值内
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 关于页整页可编辑路线 → 用户 2026-11-02 批准方案 E(blocks + theme.yaml + 局部自由区)
|
||||||
|
- [x] 字体扩充路线 → 先落 6 族,其余按子集化工具就绪后再评估(本包 T7 交付工具)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 关于页保留 intro markdown 通道作为 compat(blocks 为空时三级回落:blocks → intro → 默认文案)
|
||||||
|
- 样式即数据只开放白名单旋钮(约 12 项),不开放整份 tokens.css
|
||||||
|
- 图片只走 Astro image() 管线,禁止块内填外链/绝对路径(schema 层拦)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 整页富文本直编(C 方案):XSS 面大、无设计兜底,弃
|
||||||
|
- 新增 webfont 直接上(不子集化):单页 +535KB~+8605KB,超阈值,弃(改走 T7 工具)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
|
||||||
|
- [x] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
|
||||||
|
- [x] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
|
||||||
|
- [x] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
|
||||||
|
- [x] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
|
||||||
|
- [x] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
|
||||||
|
- [x] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:块类型先用 3 类跑通闭环(prose/tracks/divider),再扩到 9 类;theme 只做 12 个白名单旋钮
|
||||||
|
- 暂不引入:可视化块预览、块级动画配置
|
||||||
|
- 不做的抽象:通用页面 DSL(只服务现有页面)
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro 7.4.4 + zod discriminatedUnion + Sveltia list/types + pyftsubset(fonttools)
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/content.config.ts、src/lib/{page-blocks,theme,site-content}.ts、src/components/blocks/*、src/components/pages/{AboutPage,HomePage}.astro、src/pages/{index,en/index,opensource,en/opensource}.astro、src/layouts/BaseLayout.astro、public/admin/*、content/site/*.yaml、scripts/build-perf/*、infra/deploy/build.sh、docs/*、tests/e2e.mjs
|
||||||
|
- 本轮不应触碰:nginx/ESA、图库灯箱(GalleryPage/PhotoZoom)、Header/Footer 结构
|
||||||
|
- Git integration branch:`spec/2026-11-02_blog-5g-pages`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:页面数据模型与渲染器、后台配置、样式变量生成、字体产物、文档
|
||||||
|
- 快速检查:`pnpm astro build`(node26)
|
||||||
|
- 集成检查:`node tests/e2e.mjs`(37 项,本包预期新增块/主题断言)+ content-lint 0/0/0
|
||||||
|
- 全项目检查:字体体积账(三页增量)+ 视觉复核(glm-5.3-flash:max,块版式与主题变量双态);仅在构建链或 BaseLayout 结构再变更时升级范围
|
||||||
|
- 升级触发:若 theme 变量波及全局排版(字体字号全站变化)或 blocks 改动触及首页/文章页结构,升级为全站视觉复核与 E2E 全量并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
|
||||||
|
- [x] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
|
||||||
|
- [x] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
|
||||||
|
- [x] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
|
||||||
|
- [x] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
|
||||||
|
- [x] 后台文档与 hint 完整 -> verify: task_0006
|
||||||
|
- [x] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶体系与双语红线不变;彩色仅正文强调
|
||||||
|
- 块渲染器未知类型跳过 + WARN,绝不炸构建
|
||||||
|
- 字体子集化需保证字形覆盖(裁错字会出现豆腐块)→ 断言:子集后页面文本逐字命中
|
||||||
|
- 多车道并行:共享文件(content.config.ts/admin config/docs)按 boundary 划界,tokens/global 由 orchestrator 或指定单车道
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
|
||||||
|
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 blocks 骨架(关于页块式页面构建器)
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: src/content.config.ts(siteAbout 加 blocks: z.array(discriminatedUnion))、新增 src/lib/page-blocks.ts(getAboutBlocks:过滤 enabled、未知 type 跳过+WARN、文本 pickBi 回落、块数≤24/单块≤4000 字)、新增 src/components/blocks/{BlockRenderer,Prose,Tracks,Divider}.astro、AboutPage.astro 改由 blocks 驱动(blocks 空→intro markdown→默认文案三级回落)、public/admin/config.yml 加 list+types(typeKey: type)
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && grep -q "discriminatedUnion" src/content.config.ts && grep -q "BlockRenderer" src/components/pages/AboutPage.astro && grep -q "typeKey" public/admin/config.yml`
|
||||||
|
|
||||||
|
- [x] T2 块型扩充与图片管线
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: 新增 src/components/blocks/{Cards,Timeline,Quote,Figure,LinkList,Cta}.astro + 图标键白名单(复用 SocialLinks 注册表或新建 icons 注册表)、schema 块型补全(图片字段用 image(),禁止外链字符串)、content/site/assets/ 目录、near docs 图表说明
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && ls src/components/blocks/ | grep -qC1 "Cards\|Timeline" && grep -q "image()" src/content.config.ts`
|
||||||
|
|
||||||
|
- [x] T3 样式即数据(theme.yaml + 对比度闸门)
|
||||||
|
- claim: op-artalk
|
||||||
|
- boundary: 新增 content/site/theme.yaml(12 个白名单旋钮:prose-size/measure/radius/accent/leading 等)、新增 src/lib/theme.ts(getTheme + themeVarsCss 生成 :root:root{…})、BaseLayout.astro head 内联 theme 变量(构建期静态)、新增 scripts/build-perf/contrast-check.mjs(断言 theme 结果下正文对比度 ≥4.5)、public/admin/config.yml 加 theme 单例、build.sh 守卫挂载
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && test -f content/site/theme.yaml && node scripts/build-perf/contrast-check.mjs dist`
|
||||||
|
|
||||||
|
- [x] T4 首页细化与去重
|
||||||
|
- claim: op-artalk
|
||||||
|
- boundary: 新增 src/components/pages/HomePage.astro(承接现 index.astro 区块逻辑,locale prop)、src/pages/{index,en/index}.astro 改为薄壳、home.yaml 增 projectsLimit / announcement(markdown)/ postsTracks(轨开关细分),site-content 访问器同步
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && test -f src/components/pages/HomePage.astro && grep -q "HomePage" src/pages/en/index.astro`
|
||||||
|
|
||||||
|
- [x] T5 开源页收尾(缓存/回落/告警)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: src/lib/site-content.ts(README 缓存策略与超时、回落矩阵)、content/.github-readme.md 与 README HTML 剥离策略(复用 rehype-content-sanitize 已挡危险标签;补: 外链徽章不阻断)、src/data/projects-fallback.json 回填 5-8 条真实仓、projects.ts include 未命中 console.warn
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && python3 -c "import json;d=json.load(open('src/data/projects-fallback.json'));assert len(d)>=5"`
|
||||||
|
|
||||||
|
- [x] T6 文档与后台 hint 补齐
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: docs/管理台.md(块编辑怎么用、增删排序、预览限制)、docs/写作指南.md(块字段表/受限类/主题项)、public/admin/config.yml 全字段中文 hint(含 T1-T4 新增字段)
|
||||||
|
- verify: `test -f docs/管理台.md && grep -q "blocks\|区块" docs/管理台.md && grep -c "hint" public/admin/config.yml | awk '$1>=20'`
|
||||||
|
|
||||||
|
- [x] T7 字体子集化工具
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 新增 scripts/build-perf/subset-fonts.py(pyftsubset 按指定字体 + 页面实际用字生成子集 woff2 + 重写 fonts.css 指向子集文件)、infra/deploy/build.sh 守卫挂载、docs/写作指南加字流程补"子集化"一节
|
||||||
|
- verify: `test -f scripts/build-perf/subset-fonts.py && python3 -c "import fontTools; print('fonttools ok')" && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS"`
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Phase 5h:内容表达能力与社交反馈 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-02 21:16
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto)
|
||||||
|
- 每条瞬间一个 Artalk page_key(`/moments/<slug>/`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突
|
||||||
|
- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 文章置顶(契约 + 排序 + 标记 + 后台)
|
||||||
|
- T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
|
||||||
|
- T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
|
||||||
|
- T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数)
|
||||||
|
- T5 开源页重构(原生 GitHub 档案卡,去外链图依赖)
|
||||||
|
- T6 关于页承接 README 的个人叙述
|
||||||
|
- T7 写作模板示例文章 + 文档与 hint 收尾
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数
|
||||||
|
- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线
|
||||||
|
- 不做的抽象:通用「内容社交层」框架(只服务现有页面)
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:7/7
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-02 21:16
|
||||||
|
- 范围:Phase 5h:内容表达能力与社交反馈
|
||||||
|
- 功能:T1 文章置顶(契约 + 排序 + 标记 + 后台);T2 瞬间富文本(字体可换 / 颜色可换 / 多形式);T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载);T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数);T5 开源页重构(原生 GitHub 档案卡,去外链图依赖);T6 关于页承接 README 的个人叙述;T7 写作模板示例文章 + 文档与 hint 收尾
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:ed473e996a3707f98015a40e4388aa4fa58b79ac..bdaf02f9cae774e2a094eb716a95be3f0eddab56
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5h 资产:①置顶契约(sortPosts 统一四处排序,只改顺序不动 pubDate/URL/RSS)②瞬间富文本(复用 renderRich 净化链,危险标签整棵丢弃)③瞬间逐条评论(展开才挂 Artalk;评论键用虚拟路径 /moments/<slug>/——实测 # 在查询串会被截断成页面级键,导致所有瞬间共用一个评论流)④计数徽标 ViewMeta(眼睛/气泡、零第三方图片;修同载双计与同页数字差 1 两个自伤 bug,口径=「请求次数对」不等于「展示自洽」)⑤开源页去 README 外链依赖(7 类第三方图与 align 居中失效是「没居中/图片不显示」的根因)⑥灰阶纪律清剿:项目墙 15 个彩色语言点、并列 36% 单着色、全局 .tap 泄漏致卡片居中 ⑦视觉验收新增两条可复用断言法:Range.getClientRects 判折行、scrollWidth==innerWidth 判横向溢出。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 21.239, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:58:45+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:07+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q \"moments/#\" src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.073, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:28+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 23.015, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:51+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:52+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.067, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 20.732, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:09+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 20.695, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:30+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.501, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:50+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.83, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.719, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 21.074, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:12:48+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 20.656, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:09+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.386, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:29+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.357, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.589, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 20.396, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:14:57+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 21.377, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:19+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.908, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:40+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.399, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+00:00"}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Phase 5h:内容表达能力与社交反馈 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:①文章支持置顶(作者指定优先)②瞬间的文字与文章同级表达力(字体可换、颜色可换、多段落/列表/强调)③瞬间每条可独立评论 ④浏览量与评论数用统一的「眼睛/气泡」计数徽标呈现(对齐开源页 GitHub 档案的眼图标语汇)⑤开源页开篇从 GitHub README 直接移植导致:`<div align="center">` 内联对齐失效、shields/vercel/snake/komarev 等外链图在慢网下空白破版、README 的「About Me」与开源页主题无关——重构为原生组件并从 README 解耦 ⑥关于页承接读完 README 后仍想保留的个人叙述 ⑦新增一篇「写作模板」示例文章,把可用排版能力一次演示完
|
||||||
|
- **目标用户**:站长(写得更自由,少打字面 HTML)、读者(读到更丰富且不破版的页)
|
||||||
|
- **核心价值**:内容表达力与后台编辑力对上,同时把外链依赖与版式事故面砍掉;社交反馈(浏览/评论)在灰阶体系内可量化可见
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- Artalk v2 匿名可读:`GET /artalk/api/v2/comments?site_name=…&page_key=…&limit=1` 返回 `{count, roots_count, page:{pv}}`(生产实测,2026-11-02);`POST /artalk/api/v2/pages/pv` 返回 `{pv}`(现有组件在用)
|
||||||
|
- 富文本管线已存在且已净化:`renderRich`(markdown + rehype-content-sanitize 标签/属性/协议三层白名单)与 `rehype-class-whitelist`(35 项排版类,单一事实源 `public/admin/typography-classes.js`)
|
||||||
|
- 现有 6 自托管字族(含 font-huiwen 子集化 420KB 单页)与 16 彩色语义类 + 4 灰阶语义类
|
||||||
|
- 开源页 README 现由 `intro_from: "off"` 关闭,缓存文件 `content/.github-readme.md`(3138B)含 6 类外链图(readme-typing-svg / shields.io / github-readme-stats / streak-stats / activity-graph / snake / komarev)
|
||||||
|
- 首页/归档/书架三处列表排序均为 `pubDate` 降序,无置顶概念(`grep pinned` 只命中开源页仓库白名单)
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto)
|
||||||
|
- 每条瞬间一个 Artalk page_key(`/moments/<slug>/`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突
|
||||||
|
- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 瞬间文字是否允许 HTML 类名(字体/颜色)→ 用户原话「字体可换、颜色可换等」,按富文本管线放行 35 项白名单类
|
||||||
|
- [x] 开源页要不要与 README 完全一致 → 用户原话「可以自行进行美化,无需完全一致」,按原生组件重构
|
||||||
|
- [x] About Me 类内容放哪 → 用户原话「有些可以直接移植到关于页面中」,迁到 about.yaml blocks
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 置顶只作用于列表排序与视觉标记,不改 URL / 不改 pubDate 语义(订阅源按时间仍正确)
|
||||||
|
- 计数徽标一次只拉当前页的 pv(POST 计一次)与评论数(GET 读);瞬间列表按可视区懒拉,避免 N 次请求把首屏拖慢
|
||||||
|
- 开源页保留 `intro_from` 开关作为 compat,但默认 off 且文档标注「不推荐」;README 渲染通道不删除(后台仍可切回)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 直接渲染 README HTML 并试图用 CSS 修 `align="center"`:内联属性与 sanitize 的交互不可控,且外链图仍会空白破版 → 弃,改原生组件
|
||||||
|
- 每条瞬间都 init 一个 Artalk 实例常驻:内存与请求成本高、且 Artalk 对多实例的支持未验证 → 改为展开时挂载
|
||||||
|
- 用第三方徽章服务显示访客数(komarev 等):外链依赖 + 灰阶体系外颜色 → 弃,用 Artalk pv 自绘
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 文章置顶:posts/postsEn schema `pinned`、四处列表排序(首页双轨/归档/书架)、视觉标记(灰阶 pin)、后台开关
|
||||||
|
- [x] T2 瞬间富文本:moments `text` 支持 markdown + 白名单排版类(字体/颜色/强调/多段/列表)、净化管线复用、后台改 markdown 编辑器、旧纯文本零回归
|
||||||
|
- [x] T3 瞬间逐条评论:每条 moment 独立 page_key 的 Artalk 评论区(折叠、展开时挂载)、评论数联动、双语页共用同一 key
|
||||||
|
- [x] T4 计数徽标:新增 `ViewMeta` 组件(眼睛图标 = 浏览数、气泡图标 = 评论数),接入文章页/瞬间页/关于页/开源页
|
||||||
|
- [x] T5 开源页重构:原生 GitHub 档案卡(头像/名字/关注者/星标/语言占比,本地数据自绘)替换 README 外链图;`align` 失效与破图问题消失;README 通道降级为可选
|
||||||
|
- [x] T6 关于页承接:把 README 的 About Me / Find Me 迁为 about.yaml blocks(prose + linklist),开源页只留开源相关
|
||||||
|
- [x] T7 写作模板示例:中英双语示例文章,演示标题层级/彩色强调/字族/引用/代码/图/表/链接卡片/折叠,并作为置顶功能的活样本;docs 与后台 hint 同步
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数
|
||||||
|
- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线
|
||||||
|
- 不做的抽象:通用「内容社交层」框架(只服务现有页面)
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro 7.4.4 + zod schema + Artalk v2 HTTP API + GitHub GraphQL(已有 token)
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/content.config.ts、src/components/{PostList,ArtalkComments}.astro、src/components/pages/{HomePage,MomentsPage,BooksPage}.astro、src/components/geek/{ViewMeta,GithubProfile}.astro(新)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro、src/pages/{about,opensource,en/about,en/opensource}.astro、src/lib/{projects,site-content,heatmap}.ts、content/site/{about,opensource}.yaml、content/posts/*(新增示例文章)、content/posts-en/*、public/admin/{config.yml,typography-classes.js}、docs/*、tests/e2e.mjs
|
||||||
|
- 本轮不应触碰:nginx/ESA、灯箱与图库(GalleryPage/PhotoZoom)、Header/Footer 结构、构建链顺序、tokens.css/global.css 基础尺度
|
||||||
|
- Git integration branch:`spec/2026-11-02_blog-5h-content-social`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:内容模型(置顶/瞬间富文本)、渲染组件(计数/评论/档案卡)、后台配置、示例内容
|
||||||
|
- 快速检查:`pnpm astro build`(node26)+ `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:`E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(37 项,本包新增置顶顺序/徽标存在/瞬间富文本断言)+ content-lint 0/0/0
|
||||||
|
- 全项目检查:生产实测四条(开源页零第三方外链图、瞬间评论可发可见、徽标数值与 Artalk 端点对账一致、关于页承接内容渲染);**升级条件**——仅当改动触及 BaseLayout/全局样式(tokens/字体栈)或 Artalk 主题接管层时才执行,本包因往 tokens 补 emoji 回退链已触发一次并记录了理由
|
||||||
|
- 升级触发:若动到 BaseLayout/全局样式或 Artalk 主题接管层,升级为全站视觉复核(glm-5.3-flash:max,≤6 图/轮)
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 置顶文章排在列表首位且带可见标记,取消置顶后恢复时间序 -> verify: task_0001
|
||||||
|
- [x] 瞬间文字可用字体类与彩色强调,危险标签被净化丢弃,旧纯文本渲染不变 -> verify: task_0002
|
||||||
|
- [x] 每条瞬间可展开评论区并成功发帖,双语页共用同一评论流 -> verify: task_0003
|
||||||
|
- [x] 文章/瞬间/关于/开源四页显示「浏览数 + 评论数」徽标且数值与 Artalk 一致 -> verify: task_0004
|
||||||
|
- [x] 开源页零第三方图片请求,档案卡数据来自本地/GraphQL,居中与破图问题不再出现 -> verify: task_0005
|
||||||
|
- [x] README 的个人叙述出现在关于页,开源页只保留开源相关内容 -> verify: task_0006
|
||||||
|
- [x] 示例文章渲染全部演示能力且构建通过,docs/hint 同步 -> verify: task_0007
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶外壳红线不变:徽标图标与数字用 tokens(`--ink-*`/`--line`),不引入第三方彩色徽章
|
||||||
|
- 彩色仅正文强调(≤3 处/页)——示例文章本身要守纪律,成为正面样本而非反例
|
||||||
|
- 瞬间富文本必须复用 `renderRich` 净化链,禁止新增 set:html 直通路径
|
||||||
|
- Artalk 多实例风险:评论区折叠 + 展开挂载,若实测冲突则退回「逐条跳转到锚点页」
|
||||||
|
- 双语链路:所有新文案走 `t()`/`pickBi`,en 轨不得出现中文硬编码
|
||||||
|
- 多车道并行:content.config.ts / public/admin/config.yml 由 orchestrator 先落契约再放行,车道只改各自 boundary 内文件
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Phase 5h:内容表达能力与社交反馈 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
|
||||||
|
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)+ `pnpm astro build`
|
||||||
|
- **契约先行**:`src/content.config.ts` 与 `public/admin/config.yml` 由 orchestrator 先落契约(T1 时一并提交),车道不得再改这两个文件;需要新字段一律回报 orchestrator
|
||||||
|
- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e → 用 18925 端口起快照服务 → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 文章置顶(契约 + 排序 + 标记 + 后台)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/content.config.ts(postsSchema 加 `pinned: z.boolean().optional()`,posts/postsEn 共用)、src/components/PostList.astro(置顶优先排序 + pin 标记)、src/pages/archives/index.astro(排序)、src/components/pages/{HomePage,BooksPage}.astro(排序)、public/admin/config.yml(posts 集合 pinned 开关 + 中文 hint)、src/i18n/ui.ts(置顶标签)
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS" && grep -q "pinned" src/content.config.ts && grep -q "pinned" src/components/PostList.astro && grep -q "pinned" public/admin/config.yml`
|
||||||
|
|
||||||
|
- [x] T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: src/components/pages/MomentsPage.astro(`text` 由纯文本 `<p>` 改为走 `renderRich` 净化+排版类管线,保留 inline 语义;空文本老数据零回归)、public/admin/config.yml 中 moments 集合 `text` 字段改 markdown widget + 提示可用排版类(**仅在 orchestrator 提交的字段块内改 widget 类型,不改结构**)、docs 提示留给 T7
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS" && grep -qE "renderRich|renderMarkdown" src/components/pages/MomentsPage.astro && python3 -c "import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t"`
|
||||||
|
|
||||||
|
- [x] T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: 新增 src/components/MomentComments.astro(折叠 disclosure,展开时首次挂载 Artalk;page_key 约定 `/moments/#<slug>`,en 页同一 key 不分裂;评论数徽标复用 T4 的 ViewMeta 计数接口)、src/components/pages/MomentsPage.astro(每条卡片挂评论区)
|
||||||
|
- verify: `test -f src/components/MomentComments.astro && grep -q "MomentComments" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数)
|
||||||
|
- claim: op-artalk
|
||||||
|
- boundary: src/components/geek/ViewMeta.astro(**契约组件,由 orchestrator 在 T1 提交时一并落盘**,车道只消费不改语义)、src/components/ArtalkComments.astro(pv 小字行改为 ViewMeta,评论数取 `comments?limit=1` 的 count)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro(挂 ViewMeta)
|
||||||
|
- verify: `test -f src/components/geek/ViewMeta.astro && grep -q "ViewMeta" src/components/ArtalkComments.astro && grep -q "ViewMeta" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T5 开源页重构(原生 GitHub 档案卡,去外链图依赖)
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: 新增 src/components/geek/GithubProfile.astro(头像/名字/简介/关注者/总星标/语言占比,数据来自 projects.ts 与 GraphQL;零第三方图片与徽章)、src/lib/projects.ts(补统计聚合:followers/stars/language 汇总,失败降级不炸构建)、src/pages/opensource.astro、src/pages/en/opensource.astro(用 GithubProfile 替换 readme 渲染段;README 通道保留开关但默认 off 且标注不推荐)、content/site/opensource.yaml(注释说明 intro_from 现状)
|
||||||
|
- verify: `grep -q "GithubProfile" src/pages/opensource.astro && python3 -c "import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad"`
|
||||||
|
|
||||||
|
- [x] T6 关于页承接 README 的个人叙述
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: content/site/about.yaml(新增 blocks:关于/兴趣 prose、站点与项目 linklist、Find Me 社交块;双语 zh/en;测试值不得留在文件里)、src/pages/about.astro 与 en(仅当块渲染需要时改,优先零改动)
|
||||||
|
- verify: `python3 -c "import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T7 写作模板示例文章 + 文档与 hint 收尾
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: content/posts/writing-template.md、content/posts-en/writing-template.md(en 手写 locked:true 直译,不交给 TMT 重译)、docs/写作指南.md(模板用法 + 排版类清单去重指引)、docs/管理台.md(瞬间富文本/逐条评论/置顶/徽标说明)、public/admin/config.yml(moments text hint、posts pinned 已含)
|
||||||
|
- verify: `test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q "pinned: true" content/posts/writing-template.md && grep -qE "font-kai|c-blue" content/posts/writing-template.md && grep -q "写作模板" docs/写作指南.md`
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-03 12:46
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 字体按「内容实际用到的字族」按需加载即可兼顾体积与正确性(构建期扫正文/页面文案里的 `font-*` 类)
|
||||||
|
- README 动效图均为 SVG(含 SMIL/CSS 动画),可在构建期抓取并自托管,运行时零第三方请求,动效保留
|
||||||
|
- 灯箱信息栏与关闭钮的层叠冲突用 z-index + 命中区域划分即可解,无需改结构
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 字族按需加载根治
|
||||||
|
- T2 Artalk 失败误报修复
|
||||||
|
- T3 瞬间评论区配色同步
|
||||||
|
- T4 灯箱关闭修复 + 右侧文字可排版
|
||||||
|
- T5 取消彩色强调次数限制
|
||||||
|
- T6 开源页项目墙恢复居中
|
||||||
|
- T7 书架页重做
|
||||||
|
- T8 README 动效图自托管与展示创新
|
||||||
|
- T9 字族扩展(款式待用户拍板)
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:字体先修门禁(用现有 6 族验证通过),再谈扩展;灯箱只改层级与命中;README 动效先接 3 类(typing / stats / snake),验证体积与稳定性后再补
|
||||||
|
- 暂不引入:字体子集化的自动重跑编排、灯箱信息栏的富文本编辑器 UI
|
||||||
|
- 不做的抽象:通用「动效资源托管」服务
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 换 CMS、评论系统替换、Artalk 服务端改造
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 换 CMS、评论系统替换、Artalk 服务端改造
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:9/9
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-03 12:46
|
||||||
|
- 范围:Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计
|
||||||
|
- 功能:T1 字族按需加载根治;T2 Artalk 失败误报修复;T3 瞬间评论区配色同步;T4 灯箱关闭修复 + 右侧文字可排版;T5 取消彩色强调次数限制;T6 开源页项目墙恢复居中;T7 书架页重做;T8 README 动效图自托管与展示创新;T9 字族扩展(款式待用户拍板)
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:36643f5cf087b9f8b46f598516b30c6b80fe9e6b..83e0b863fc117d2f4d05e144331bc748b1881988
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5i 资产:①字体切换失效的根因=加载门禁(四份字体 CSS 被关在阅读轨分支,技术轨与全站页面从不加载)→ 改为「按本页内容实际用到的字族」注入;②六款自托管字族(OFL)全部过单页 ≤150KB 闸门,关键取舍=@fontsource 中文切片粒度太粗(一句话 308–442KB)→ 改原始 TTF 字频序细切(19 字/片,族总量 0.5–1.2MB);③评论失败提示误报的状态机修法(脚本标签在但库未就绪即判失败);④Artalk init 后加 .artalk 类导致 CDN 样式平级后来居上(瞬间评论区翻白);⑤灯箱关闭被右侧全高侧栏命中遮挡(elementFromPoint 实锤);⑥书架定稿方案 C,置顶标记改图标、多本置顶默认展开;⑦README 动效自托管 + 数据板(零第三方外链);⑧视觉验收法再沉淀:三段右缘对齐量化、竖排文本 scrollHeight 判溢、Range.getClientRects 判折行。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{"task": "task_0001", "cmd": "grep -q \"fontsForPage\" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.681, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:42:37+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "grep -q \"onerror\" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.242, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:08+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -qE \"data-artalk-panel|mom-atk\" src/components/MomentComments.astro && grep -q \"at-color-bg\" src/components/geek/ArtalkThemeStyles.astro && grep -q \"ArtalkThemeStyles\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:08+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"lb-close\" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.028, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "! grep -rq \"一页 ≤3 处\\|≤3 处/页\" docs/ content/posts/writing-template.md && grep -q \"彩色\" docs/写作指南.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "grep -q \"align-items: center\" src/components/geek/ProjectWall.astro", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.38, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q \"github-assets\" scripts/build-perf/fetch-readme-assets.mjs && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.033, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
|
||||||
|
{"task": "task_0009", "cmd": "grep -c \"font-\" public/admin/typography-classes.js | awk '$1>=40'", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"fontsForPage\" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.962, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:26+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "grep -q \"onerror\" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.872, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:56+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -qE \"data-artalk-panel|mom-atk\" src/components/MomentComments.astro && grep -q \"at-color-bg\" src/components/geek/ArtalkThemeStyles.astro && grep -q \"ArtalkThemeStyles\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:56+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"lb-close\" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.988, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "! grep -rq \"一页 ≤3 处\\|≤3 处/页\" docs/ content/posts/writing-template.md && grep -q \"彩色\" docs/写作指南.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "grep -q \"align-items: center\" src/components/geek/ProjectWall.astro", "exit": 0, "duration_s": 0.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.968, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q \"github-assets\" scripts/build-perf/fetch-readme-assets.mjs && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.032, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
|
||||||
|
{"task": "task_0009", "cmd": "grep -c \"font-\" public/admin/typography-classes.js | awk '$1>=40'", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:①**字体切换全部失效的根因修复**(不是字体没做,是加载门禁错了)②按需扩展更多字族 ③Artalk「加载失败」误报与瞬间评论区配色脱同步 ④图库灯箱右上关闭被遮挡点不动,且右侧文字要能换字体/颜色 ⑤取消「彩色 ≤3 处/页」限制 ⑥开源页项目墙恢复居中 ⑦书架页重做 ⑧GitHub README 的动效图与更多展示形式回归(自托管,不再外链)
|
||||||
|
- **目标用户**:站长(写得更自由、字体真的能换)与读者(交互不再有假失败、书架更耐看)
|
||||||
|
- **核心价值**:把「看起来能用」变成「真的能用」——本轮多数是用户实测踩到的静默失效,根因都在加载门禁与层级覆盖
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实(本轮实测取证,2026-11-02)
|
||||||
|
- **字体失效根因**:`src/layouts/BaseLayout.astro:76` 把 serif/kai/kuaile/hwmc 四份字体 CSS 全部关在 `track === 'reading'` 里;技术轨与全站页面(默认 `track='tech'`)从不加载它们 → `.font-kai` 计算值虽为 `"LXGW WenKai Screen"` 但该 @font-face 不在文档字体集内(`document.fonts` 实测仅 Noto Sans/JetBrains),浏览器回退系统宋体/衬线,表现为「字体没换」
|
||||||
|
- **汇文明朝链路**:`/fonts/subset/hwmc/fonts.css` 由 public 手工切片提供(339 片 unicode-range),仅在阅读轨被 preload;技术轨整条链路不加载
|
||||||
|
- **Artalk 误报根因**:`ArtalkComments.astro:126` `if (document.getElementById('artalk-lib')) return initArtalk();` —— 软导航后脚本标签已在但 `window.Artalk` 尚未就绪,走 `initArtalk()` → `!lib` → `showRetry(true)` 永久显示失败提示;而评论随后正常渲染(用户所见「总显示失败但实际正常」)
|
||||||
|
- **瞬间评论区配色脱同步**:Artalk 主题变量块选择器是 `#artalk-container` 与 `body .atk-layer-wrap`;瞬间的容器 id 为 `mom-atk-<slug>`,不在作用域内 → 回落到 Artalk 默认色
|
||||||
|
- **灯箱关闭失灵根因**:`#lb-close` 位于 (1220,16,44×44),`document.elementFromPoint` 命中的是 `.lb-meta.lb-l2`(右侧信息栏压在按钮上),点击被信息栏吃掉 → `lb.close()` 永不触发(生产实测 `open` 保持 true)
|
||||||
|
- 书架页现状:色脊卡 + 年份分组网格;信息密度低、悬停无反馈、无简介/评分等可读线索
|
||||||
|
- README 动效资源清单(7 类):readme-typing-svg、github-readme-stats、streak-stats、activity-graph、贡献蛇(daily Action 产出的 SVG)、shields 徽章、komarev 访客
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 字体按「内容实际用到的字族」按需加载即可兼顾体积与正确性(构建期扫正文/页面文案里的 `font-*` 类)
|
||||||
|
- README 动效图均为 SVG(含 SMIL/CSS 动画),可在构建期抓取并自托管,运行时零第三方请求,动效保留
|
||||||
|
- 灯箱信息栏与关闭钮的层叠冲突用 z-index + 命中区域划分即可解,无需改结构
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 字体扩展款式(用户选:宋系 / 楷系 / 手写系 / 现代黑系各补几款)
|
||||||
|
- [x] 书架重做方向(A 色脊墙精修 / B 封面网格 / C 书脊展开卡)
|
||||||
|
- [x] README 动效图确认「构建期抓取 + 自托管」路线
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 字体:只按需加载,不做「全站全字族常开」(那会把单页体积拉到数 MB)
|
||||||
|
- README 动效:自托管后仍保留原服务生成逻辑(GitHub Action / 第三方接口),只是把产物入仓并在构建期更新
|
||||||
|
- 彩色限制取消后仍保留「灰阶外壳」红线(导航/页脚/按钮/背景不染色),只取消正文次数限制
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 「字族没做」判断错误:@font-face 与切片文件都在,问题在加载门禁(本轮实测推翻)
|
||||||
|
- 外链 README 图直接保留:既有 502/超时与外链依赖问题(上一阶段已判弃),改自托管
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 字族按需加载根治 + 字族扩展机制:BaseLayout 由「轨道门禁」改为「内容诉求驱动」,扫描正文与页面文案中的 `font-*` 类决定加载哪些字体 CSS;补字体扩展清单与接入流程
|
||||||
|
- [x] T2 Artalk 误报与配色同步:失败提示改为「真失败才提示」(脚本 onerror / 库未就绪超时且未挂载才提示),重置挂载状态机;主题变量覆盖全部 Artalk 实例容器(含瞬间逐条)
|
||||||
|
- [x] T3 灯箱:右上关闭恢复可点(层级与命中区域),右侧信息栏文字支持 markdown + 排版类(字体/颜色)
|
||||||
|
- [x] T4 取消彩色强调次数限制(文档 + 后台 hint + 示例文章同步)
|
||||||
|
- [x] T5 开源页项目墙恢复居中排版
|
||||||
|
- [x] T6 书架页重做(按用户选定方向)
|
||||||
|
- [x] T7 README 动效图自托管与展示创新(typing / stats / streak / activity-graph / snake 等构建期抓取入仓,配合原生数据卡与热力图组合呈现)
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 字体扩展的实际接入(款式待用户定)
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 换 CMS、评论系统替换、Artalk 服务端改造
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:字体先修门禁(用现有 6 族验证通过),再谈扩展;灯箱只改层级与命中;README 动效先接 3 类(typing / stats / snake),验证体积与稳定性后再补
|
||||||
|
- 暂不引入:字体子集化的自动重跑编排、灯箱信息栏的富文本编辑器 UI
|
||||||
|
- 不做的抽象:通用「动效资源托管」服务
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro 7.4.4 + @fontsource(自托管字体 CSS)+ Artalk v2 + 构建期 SVG 抓取缓存(复用 README 缓存模式)
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/layouts/BaseLayout.astro、src/styles/{global,tokens}.css、src/components/{ArtalkComments,MomentComments}.astro、src/components/geek/{ViewMeta,ProjectWall,GithubProfile}.astro、src/components/pages/{GalleryPage,BooksPage}.astro、src/pages/{gallery,books}.astro、src/lib/{projects,site-content}.ts、content/site/*.yaml、content/photos/*.json(caption 富文本)、content/.github-assets/(新缓存目录)、public/admin/*、scripts/build-perf/*、infra/deploy/build.sh、docs/*、tests/e2e.mjs
|
||||||
|
- 本轮不应触碰:nginx/ESA、构建发布链主干顺序、双语路由结构、Artalk 服务端配置
|
||||||
|
- Git integration branch:`spec/2026-11-02_blog-5i-fonts-polish`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:字体加载策略、Artalk 挂载状态机与主题作用域、灯箱层级与富文本、书架与开源页版式、自托管动效资源
|
||||||
|
- 快速检查:`pnpm astro build`(node26)+ `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E(当前 46 项,本包新增字族加载/灯箱关闭/Artalk 误报断言)+ content-lint 0/0/0
|
||||||
|
- 升级触发:**已触发两次并记录理由**——①往 tokens.css 补 emoji 回退链(触及全局字体栈)②BaseLayout 字体加载策略改写(触及全站首屏资源)。两次都执行了全站视觉复核(glm-5.3-flash:max,≤6 图/轮,两轮共 9 张)与生产实测。以下情况同样升级:Artalk 挂载状态机、灯箱层级、构建闸门顺序。
|
||||||
|
- 全项目检查:生产实测五条(字体真加载 `document.fonts` 命中、灯箱点关闭真关、评论失败提示不再误报、瞬间评论区配色与页面一致、开源页动效图零外链);**升级条件**——仅当改动触及 BaseLayout 字体策略或 Artalk 挂载状态机时升级为全站视觉复核,本包因两者都动,已排一轮视觉终审
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 技术轨文章里的 `.font-kai/.font-song/.font-huiwen/.font-kuaile` 真加载对应字体(`document.fonts` 命中且渲染变化) -> verify: task_0001
|
||||||
|
- [x] 评论失败提示只在真失败时出现,正常挂载不提示 -> verify: task_0002
|
||||||
|
- [x] 瞬间评论区配色与页面主题一致(明暗双态) -> verify: task_0003
|
||||||
|
- [x] 灯箱右上关闭按钮点击必关(含缩放/信息栏展开态) -> verify: task_0004
|
||||||
|
- [x] 灯箱右侧文字支持字体与颜色类 -> verify: task_0005
|
||||||
|
- [x] 彩色强调不再有限次约束且文档同步 -> verify: task_0006
|
||||||
|
- [x] 开源页项目墙内容居中 -> verify: task_0007
|
||||||
|
- [x] 书架页信息密度与视觉达设计稿并过视觉终审 -> verify: task_0008
|
||||||
|
- [x] README 动效图自托管且生产零第三方请求 -> verify: task_0009
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶外壳红线不变,仅取消正文彩色次数限制
|
||||||
|
- 自制切片/新增字体必须守单页增量 ≤150KB 阈值,超阈值只做子集化或降级为系统回退
|
||||||
|
- README 动效自托管:抓取失败必须回落(隐藏该块,绝不炸构建),且产物必须入仓保证构建确定性
|
||||||
|
- Artalk 状态机改动需覆盖三条触发路径(IO / 锚点 / 滚底),不得回归「评论区空着」
|
||||||
|
- 多车道并行:BaseLayout、ArtalkComments、GalleryPage、BooksPage 各归单一车道,admin config 与 docs 由 orchestrator 统一改
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
|
||||||
|
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)+ `pnpm astro build`
|
||||||
|
- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e-5i → gz-server.py 18925 /tmp/dist-e2e-5i → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 字族按需加载根治
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: src/layouts/BaseLayout.astro(去掉 `track === 'reading'` 的字体门禁,改为「本页内容实际用到的字族」驱动:构建期扫正文/页面文案里的 `font-kai|font-song|font-fangsong|font-huiwen|font-kuaile|font-serif` 决定注入哪几份字体 CSS;零命中时只加载 sans/mono)、src/lib/ 新增字体诉求探测工具
|
||||||
|
- verify: `grep -q "fontsForPage" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T2 Artalk 失败误报修复
|
||||||
|
- claim: op-artalk
|
||||||
|
- boundary: src/components/ArtalkComments.astro(状态机重写:脚本 onerror 才是失败;`artalk-lib` 已存在但库未就绪时等 load 事件而非立即判失败;失败提示只在「确实未挂载且已超时」出现;`mounting` 在所有失败/成功路径复位)
|
||||||
|
- verify: `grep -q "onerror" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T3 瞬间评论区配色同步
|
||||||
|
- claim: op-artalk
|
||||||
|
- boundary: src/components/MomentComments.astro(Artalk 主题变量作用域覆盖瞬间容器 `[data-artalk-panel]`/`mom-atk-*`,复用文章页那套 `--at-color-*` 映射:建议抽出共享样式片段避免两份漂移)
|
||||||
|
- verify: `grep -qE "data-artalk-panel|mom-atk" src/components/MomentComments.astro && grep -q "at-color-bg" src/components/geek/ArtalkThemeStyles.astro && grep -q "ArtalkThemeStyles" src/components/ArtalkComments.astro`
|
||||||
|
|
||||||
|
- [x] T4 灯箱关闭修复 + 右侧文字可排版
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/components/pages/GalleryPage.astro(关闭钮层级/命中区域;右侧信息栏不再拦截点击)、content/photos/*.json(caption 允许 markdown + 排版类)、必要时 src/lib/exif.ts 注释同步
|
||||||
|
- verify: `grep -q "lb-close" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T5 取消彩色强调次数限制
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: docs/写作指南.md、docs/管理台.md、public/admin/config.yml(hint 里去掉「≤3 处」表述)、content/posts/writing-template.md + posts-en 同名(示例文章说明同步)
|
||||||
|
- verify: `! grep -rq "一页 ≤3 处\|≤3 处/页" docs/ content/posts/writing-template.md && grep -q "彩色" docs/写作指南.md`
|
||||||
|
|
||||||
|
- [x] T6 开源页项目墙恢复居中
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: src/components/geek/ProjectWall.astro(`align-items: center` + `text-align: center`,与用户审美偏好一致)
|
||||||
|
- verify: `grep -q "align-items: center" src/components/geek/ProjectWall.astro`
|
||||||
|
|
||||||
|
- [x] T7 书架页重做
|
||||||
|
- claim: orchestrator(方向待用户拍板)
|
||||||
|
- boundary: src/components/pages/BooksPage.astro、可选 content/site/books.yaml 新配置、src/i18n/ui.ts
|
||||||
|
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T8 README 动效图自托管与展示创新
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: 新增 scripts/build-perf/fetch-readme-assets.mjs(构建期抓 typing/stats/streak/activity-graph/snake 等 SVG 到 content/.github-assets/,失败回落隐藏该块)、src/components/geek/GithubProfile.astro 或新增动效展示组件、build.sh 守卫挂载、docs 说明
|
||||||
|
- verify: `test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q "github-assets" scripts/build-perf/fetch-readme-assets.mjs && python3 -c "import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad"`
|
||||||
|
|
||||||
|
- [x] T9 字族扩展(款式待用户拍板)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: src/styles/tokens.css、src/styles/global.css、public/admin/typography-classes.js、public/fonts/(新增字体切片)、docs/写作指南.md 字体清单
|
||||||
|
- verify: `grep -c "font-" public/admin/typography-classes.js | awk '$1>=40'`
|
||||||
@@ -3,6 +3,9 @@ import { defineConfig } from 'astro/config';
|
|||||||
import pagefind from 'astro-pagefind';
|
import pagefind from 'astro-pagefind';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
|
import rehypeLinkCards, { linkCardsReport } from './src/lib/rehype-link-cards';
|
||||||
|
import rehypeClassWhitelist from './src/lib/rehype-class-whitelist';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
||||||
// 构建透明(T7):commit/时间经 vite.define 注入,页脚与 /about 引用。
|
// 构建透明(T7):commit/时间经 vite.define 注入,页脚与 /about 引用。
|
||||||
// 四重回退:CI 环境变量优先(GITEA_ACTIONS/GITHUB_SHA)→ 本地 git → dev 占位。
|
// 四重回退:CI 环境变量优先(GITEA_ACTIONS/GITHUB_SHA)→ 本地 git → dev 占位。
|
||||||
@@ -17,7 +20,13 @@ const date =
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://wait4x.online',
|
site: 'https://wait4x.online',
|
||||||
integrations: [pagefind(), sitemap()],
|
integrations: [pagefind(), sitemap(), linkCardsReport()],
|
||||||
|
markdown: {
|
||||||
|
// 独立成段的裸链接 → OG 预览卡(构建期抓取,失败回落普通链接;见 src/lib/rehype-link-cards.ts)
|
||||||
|
// 顺序:rehypeRaw 先把 markdown 里的 raw HTML 解析成 hast element(否则白名单看不到 span),
|
||||||
|
// 再白名单净化(剥 style/on*/非白名单类),最后链接卡片(节点由插件自建,不经净化)
|
||||||
|
rehypePlugins: [rehypeRaw, rehypeClassWhitelist, rehypeLinkCards],
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'zh',
|
defaultLocale: 'zh',
|
||||||
locales: ['zh', 'en'],
|
locales: ['zh', 'en'],
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=26&duration=3000&pause=1000&color=00D9FF¢er=true&vCenter=true&random=false&width=620&lines=Hi+there+%F0%9F%91%8B+I'm+wait4x;Open+Source+Enthusiast+%E2%9D%A4%EF%B8%8F;Meteorology+%E2%9B%85" alt="Typing SVG" />
|
||||||
|
<br>
|
||||||
|
<a href="https://github.com/wait4xx?tab=followers"><img src="https://img.shields.io/github/followers/wait4xx?label=Followers&style=for-the-badge&color=00D9FF&labelColor=0D1117" /></a>
|
||||||
|
<a href="https://github.com/wait4xx?tab=stars"><img src="https://img.shields.io/github/stars/wait4xx?label=Stars&style=for-the-badge&color=00D9FF&labelColor=0D1117" /></a>
|
||||||
|
<a href="https://github.com/wait4xx"><img src="https://img.shields.io/badge/Open%20Source-%E2%9D%A4%EF%B8%8F-00D9FF?style=for-the-badge&labelColor=0D1117" /></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 👋 About Me
|
||||||
|
|
||||||
|
- ⛅ An aspiring forecaster rooted in **meteorology**.
|
||||||
|
- 💻 Passionate about **computer science** and coding.
|
||||||
|
- 🌐 My web: **[wait4x.online](https://wait4x.online/)**
|
||||||
|
- ⚡ *"Embrace open source, love open source."*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 GitHub Stats
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img height="170" src="https://github-readme-stats.vercel.app/api?username=wait4xx&show_icons=true&hide_border=true&bg_color=0D1117&title_color=00D9FF&icon_color=00D9FF&text_color=C9D1D9&include_all_commits=true&count_private=true&line_height=28" alt="stats" />
|
||||||
|
<img height="170" src="https://github-readme-stats.vercel.app/api/top-langs/?username=wait4xx&layout=compact&hide_border=true&bg_color=0D1117&title_color=00D9FF&text_color=C9D1D9&langs_count=8" alt="top-langs" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://streak-stats.demolab.com?user=wait4xx&hide_border=true&background=0D1117&ring=00D9FF&fire=00D9FF&currStreakLabel=00D9FF&sideLabels=C9D1D9&dates=C9D1D9&currStreakNum=C9D1D9" alt="streak" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📈 Activity Graph
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://github-readme-activity-graph.vercel.app/graph?username=wait4xx&bg_color=0D1117&color=C9D1D9&line=00D9FF&point=00D9FF&area=true&area_color=00D9FF&hide_border=true" alt="activity-graph" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🐍 Contribution Snake
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/wait4xx/wait4xx/output/github-contribution-grid-snake.svg" alt="snake" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> *This snake is auto-fed every day by a GitHub Action in this repo 🍎*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📫 Find Me
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/wait4xx"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white" /></a>
|
||||||
|
<a href="mailto:X@alumni.nudt.edu.cn"><img src="https://img.shields.io/badge/Email-D14836?style=for-the-badge&logo=gmail&logoColor=white" /></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://komarev.com/ghpvc/?username=wait4xx&color=00D9FF&style=flat-square&label=%F0%9F%91%80+Visitors" />
|
||||||
|
<br><br>
|
||||||
|
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&size=14&duration=2000&pause=500&color=8B949E¢er=true&vCenter=true&random=false&width=300&lines=Thanks+for+visiting!;See+you+in+the+commits+%F0%9F%8C%BF" alt="footer" />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"_readme": "链接卡片抓取缓存(构建确定性需求,入仓)。键=URL,值={title,desc,image,favicon,ts};ts 超 30 天刷新。手工改动安全:删条目即下次构建重抓。",
|
||||||
|
"https://www.cnblogs.com/Lenbrother/p/18673403": {
|
||||||
|
"title": "Debian 安装 Docker - 神奇小宝 - 博客园",
|
||||||
|
"desc": "卸载已有 Docker 如果你之前安装过 Docker Engine 之前,你需要卸载旧版本,避免冲突: for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get rem",
|
||||||
|
"image": "https://assets.cnblogs.com/images/wechat-share.jpg",
|
||||||
|
"favicon": "https://assets.cnblogs.com/favicon_v3_3.ico",
|
||||||
|
"ts": 1793593758751
|
||||||
|
},
|
||||||
|
"https://wait4x.online/": {
|
||||||
|
"title": "wait4x",
|
||||||
|
"desc": "wait4x 的博客:阅读 · 技术 · 摄影",
|
||||||
|
"image": "",
|
||||||
|
"favicon": "https://wait4x.online/favicon.svg",
|
||||||
|
"ts": 1793593759184
|
||||||
|
},
|
||||||
|
"https://blog.csdn.net/qq_40605167/article/details/122888580": {
|
||||||
|
"title": "Docker版 E5续订的E5调用API续订服务:Microsoft 365 E5 Renew X_e5 docker",
|
||||||
|
"desc": "文章浏览阅读2.7w次,点赞10次,收藏43次。本文介绍了如何使用Docker轻松部署Microsoft365E5续订服务,包括提供小巧的镜像、一键部署命令及邮箱通知功能。用户需要具备Docker基础,并按照步骤修改邮箱和密码等参数。此外,还提供了不同CPU架构的支持。注意,已有部分账号出现被关停的情况。文章附带了Docker环境搭建教程和相关资源链接。",
|
||||||
|
"image": "https://i-blog.csdnimg.cn/blog_migrate/cover/dc9f6540d4d4330e797f1ebd0261f736.png",
|
||||||
|
"favicon": "https://g.csdnimg.cn/static/logo/favicon32.ico",
|
||||||
|
"ts": 1793593759739
|
||||||
|
},
|
||||||
|
"https://astro.build/": {
|
||||||
|
"title": "Astro",
|
||||||
|
"desc": "Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.",
|
||||||
|
"image": "https://astro.build/og/astro.jpg",
|
||||||
|
"favicon": "https://astro.build/favicon.svg",
|
||||||
|
"ts": 1793593759816
|
||||||
|
},
|
||||||
|
"https://astro.build": {
|
||||||
|
"title": "Astro",
|
||||||
|
"desc": "Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.",
|
||||||
|
"image": "https://astro.build/og/astro.jpg",
|
||||||
|
"favicon": "https://astro.build/favicon.svg",
|
||||||
|
"ts": 1793621147252
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
pubDate: 2026-04-29
|
|
||||||
tags: []
|
|
||||||
text: "@wait4x"
|
|
||||||
---
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
pubDate: 2026-04-29
|
|
||||||
tags: []
|
|
||||||
text: "@wait4x"
|
|
||||||
---
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
tags: [站点更新]
|
||||||
|
text: |
|
||||||
|
瞬间现在和文章同级表达力:`text` 支持 markdown 与排版类,<span class="c-teal font-kai">这一句是青色 + 楷体</span>,也可以**加粗**。
|
||||||
|
|
||||||
|
另外每条瞬间自带评论区——展开才加载,不刷时间线就不为它付成本。
|
||||||
|
---
|
||||||
|
|
||||||
|
站点更新记录:瞬间富文本与逐条评论上线。
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "",
|
"title": "防波堤暮色",
|
||||||
"image": "./1766156983093.jpg",
|
"image": "./1766156983093.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "暮色海岸与远处防波堤(T3 冒烟补写)。",
|
||||||
"exif": {}
|
"caption": "落日把海与城一齐烫成金箔,海鸟的剪影恰好掠过日轮"
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156992123.jpg",
|
"image": "./1766156992123.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "书架暗处,那本宫崎骏分镜集被灯光单独打亮"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156992265.jpg",
|
"image": "./1766156992265.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "一面白墙只问一个问题,笔锋落下处是想活出的人生"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156992559.jpg",
|
"image": "./1766156992559.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "黑暗中独一句发光的话,像有人在夜里旋起一阵热风"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156992840.jpg",
|
"image": "./1766156992840.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "一抹红墙,几笔白线,龙猫的轮廓就是吉卜力的门牌"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156997939.jpg",
|
"image": "./1766156997939.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "夜游船驮着绿光驶过,东方明珠在它身后蓝得像天外来物"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156998072.jpg",
|
"image": "./1766156998072.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "对岸灯火压得很低,渡轮拖着一线亮色划开墨色的江面"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156998205.jpg",
|
"image": "./1766156998205.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "海关大楼的钟面亮成金色,人群在灯下攒动成剪影"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156998344.jpg",
|
"image": "./1766156998344.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "旗在风里翻卷,塔身亮得像一支随时点火的火箭"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766156998590.jpg",
|
"image": "./1766156998590.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "大楼只占一个角落,剩下的天空都交给云慢慢写"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157003595.jpg",
|
"image": "./1766157003595.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "草坡绿得发暗,牦牛散落其间,低头啃食高原的安静"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157003732.jpg",
|
"image": "./1766157003732.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "灰白天色下,雪与黑岩互相渗透,山脊线细得像一笔勾勒"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157003865.jpg",
|
"image": "./1766157003865.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "雾把山谷对折,只留下碎石和雪在白茫茫里彼此确认"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157003992.jpg",
|
"image": "./1766157003992.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "一队人影在乱石坡上缓缓上移,雪山在他们头顶沉默俯视"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157004116.jpg",
|
"image": "./1766157004116.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "雪原中央蹲着两团颜色,出发前把装备一件件理顺"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./1766157012003.jpg",
|
"image": "./1766157012003.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "太阳缩成一粒橘色的句点,悬在灰橙色的茫茫天幕上"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./mmexport1771816358254.jpg",
|
"image": "./mmexport1771816358254.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "成群海鸥同时起飞,蓝海与红屋顶都被翅膀搅动"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./mmexport1771816377628.jpg",
|
"image": "./mmexport1771816377628.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "垂钓的人立在礁石上,几根鱼竿替他守着整片海"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./mmexport1771816382115.jpg",
|
"image": "./mmexport1771816382115.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "长堤探进海里,堤上的人站成一小排,各自望着各自的远方"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./mmexport1776564084994.jpg",
|
"image": "./mmexport1776564084994.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "海鸥飞过之处,玻璃高塔与红瓦老城在同一帧里相遇"
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"image": "./mmexport1776564092433.jpg",
|
"image": "./mmexport1776564092433.jpg",
|
||||||
"group": "landscape",
|
"group": "landscape",
|
||||||
"alt": "",
|
"alt": "",
|
||||||
"exif": {}
|
"exif": {},
|
||||||
}
|
"caption": "画面只留一圈亮处:老楼、松树、栏杆边与海鸥相伴的人群"
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: "Agensh: Scaling Organizational Intelligence to 1,024 Agents"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [Paper Notes, AI Collaboration]
|
||||||
|
locked: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Multi-agent systems compress the latency of complex tasks through concurrency, but mainstream frameworks share one ceiling: the central orchestrator. Task allocation, progress aggregation, and result adjudication all squeeze through a single context — the bigger the scale, the sooner the orchestrator dies. In October 2026, Microsoft Research open-sourced [Agensh](https://github.com/microsoft/Agensh) — a self-organized multi-agent harness **with no central orchestrator**, and the paper [*Scaling Organizational Intelligence to 1,024 Agents*](https://arxiv.org/abs/2609.26781) pushed it to a thousand and twenty-four agents.
|
||||||
|
|
||||||
|
Its answer is disarmingly simple: instead of building a smarter scheduling brain, hand the agents the coordination infrastructure of a human organization, as is.
|
||||||
|
|
||||||
|
> 📄 **[Read the full paper PDF online →](/en/papers/agensh/)** · arXiv 2609.26781 · Microsoft Research
|
||||||
|
|
||||||
|
## 1. Three pillars: an organization's coordination infrastructure
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Agensh provides exactly three pieces of infrastructure; everything else self-organizes:
|
||||||
|
|
||||||
|
- **Shared workspace**: a git platform carries the organization's in-progress and integrated work — branches, PRs, and merge conflicts, twenty years of human machinery reused as-is;
|
||||||
|
- **Message interface**: a team channel plus direct messages with priority delivery — organization-wide broadcasts and urgent collision resolution each take their own lane;
|
||||||
|
- **Shared context**: a typed-entry board holding the organization's findings and work intentions.
|
||||||
|
|
||||||
|
The open-source reference implementation runs Gitea for the workspace and Mattermost for channel and DMs — a fully self-hosted "office" with no proprietary services. No orchestrator assigns tasks: workers find work themselves in the workspace and the context board, claim it, verify it, and merge it. The natural bottleneck of scaling — that ever-busier hub — simply does not exist.
|
||||||
|
|
||||||
|
## 2. The cooperation loop: every worker runs their own lap
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Every worker concurrently and asynchronously runs the same loop: **gather context → claim a sub-task → take action → verify → merge progress**. Gathering context means reading the shared workspace and the board's accumulated knowledge; claiming is a CLAIM declaration, with collisions resolved over direct messages; verification is running the tests yourself — evidence talks; merging goes through PRs.
|
||||||
|
|
||||||
|
The most interesting design is the **economy** of the shared context: entries are deliberately tiny (about 100 characters, 300 for PATCH_SUMMARY), with the long version in a `detail` field fetched on demand; `board_read` serves only the latest 2,000 entries, and anything older is reachable only through boolean search (`,` = OR, `&` = AND) — recent memory stays small, the long tail is searched. The paper calls **FAIL the highest-value entry**: one documented falsified path stops the whole team from re-burning budget on the same dead end. New entries are pushed to peers as higher-priority updates at their next tool-call boundary.
|
||||||
|
|
||||||
|
This shared context is not Agensh's invention — §2.3 of the paper states plainly that "the shared context follows the core idea of DeLM". [DeLM](https://github.com/yuzhenmao/DeLM) ([arXiv:2606.10662](https://arxiv.org/abs/2606.10662)) ran decentralized multi-agent coordination through a shared context and task queue: its board is an append-only event log (items, claims, completions, FACT/FAIL notes, status) with bodies stored in a separate database beside the log, hard limits (≤40 files, ≤400 KB total), and a blocking `read --wait` that wakes when a peer posts. On a 10-task Terminal-Bench 4.0 subset, two agents coordinating through the shared context averaged +12.5 percentage points accuracy and a 1.78× speedup. Agensh scaled that thread to a thousand agents.
|
||||||
|
|
||||||
|
## 3. The numbers: does scaling actually help?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Two datasets answer "do more agents actually move the needle": on the five hardest ProgramBench tasks, 1 → 128 agents raised the pass rate from 19.31% to 28.78% (~49% relative); on building pandoc from scratch (6-hour budget, no Internet), 1 → 1,024 agents raised it from 33.89% to 55.06%. For problems where raw compute has hit diminishing returns, **the marginal return of organization is still there**.
|
||||||
|
|
||||||
|
Engineering readiness arrived too: v1.0 shipped on 2026-10-23, and v1.0.1 six days later supports Copilot, Codex, Claude Code, and Pi as the underlying single-agent harness — use whatever mainline model you like underneath. The open-source release is a single-node harness, and the v1.0 package ships with a reproduction guide, the paper, and a demo — from paper to runnable repository with no "coming soon" in between.
|
||||||
|
|
||||||
|
## 4. From paper to desktop: the story of one issue
|
||||||
|
|
||||||
|
For us this paper was never just on paper. After digesting it, on 2026-10-23 I filed [issue #13](https://github.com/cgarrot/pi-mesh/issues/13) on the pi-mesh repository: pi-mesh already had the message interface (`mesh_send`, rooms, priorities) and the shared workspace (git + `mesh_reserve`), but lacked the third pillar, shared context — proposing `board_write / board_read / board_grep` in exactly the Agensh/DeLM shape.
|
||||||
|
|
||||||
|
The maintainer cgarrot's reply is the most instructive part of the story: the direction was right, but **the hash-only ledger makes pi-mesh structurally incapable of being that board** — embedding persistence in the broker would turn a router into a database. The right shape was a separate package: join the room as a normal peer, own your storage, and let the mesh keep routing. Thus [pi-board](https://github.com/wait4xx/pi-board): v0.1 landed within a day — 55 tests, zero failures, a nine-step smoke run on a real broker — and when the issue finally closed, the official pointer landed on it. The memory layer of the [four-piece toolkit](/en/archives/pi-mesh-stack/) introduced in the previous post grew straight out of this paper.
|
||||||
|
|
||||||
|
## 5. Closing
|
||||||
|
|
||||||
|
Agensh's insight is not the number "1024" but its decomposition of organizational intelligence: workspace, messages, shared context — three pillars holding up self-organization, none of which needs a smarter brain. The decomposition holds at thousand-agent scale, holds for a local four-piece toolkit, and holds for a one-person shop with a few agent terminals. Scale changes; the pillars stay.
|
||||||
|
|
||||||
|
Related reading: [Spec Harness in Depth](/en/archives/spec-harness/) · [The pi-mesh Tool Family](/en/archives/pi-mesh-stack/)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: "The pi-mesh Tool Family: A Four-Piece Toolkit for Local Multi-Agent Coordination"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [Engineering, AI Collaboration]
|
||||||
|
locked: true
|
||||||
|
---
|
||||||
|
|
||||||
|
[pi](https://github.com/earendil-works/pi) is the coding-agent harness we use daily. With a single terminal it is a quiet assistant; with three or four terminals open at once, each running an agent over the same repository, questions pile up: who is online? Who is editing which file? Was that message actually seen? Who has the right to stop the build running at midnight?
|
||||||
|
|
||||||
|
A family of small tools grew around [pi-mesh](https://github.com/cgarrot/pi-mesh) to turn that coordination layer into infrastructure. Each owns one concern, and they interlock:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 1. pi-mesh: the communication backbone
|
||||||
|
|
||||||
|
pi-mesh is a local bus: a broker detached from any session (NDJSON frames over a unix socket), each pi terminal a peer with an alias, organized into rooms. Messages arrive in under 50 ms; zero runtime dependencies, Node ≥ 20.
|
||||||
|
|
||||||
|
Its most admirable trait is the **honest status model**: `delivered` (written to the recipient's socket) ≠ `read` (injected into the recipient's session) ≠ `answered` (an explicit reply came back) — three states never conflated, none of them meaning "done". The companion **durable ledger is hash-only**: message bodies are never persisted; after a restart you can audit who sent what to whom and when, but never read the content. This self-restraint later became the family's design axiom: **the mesh stays a router, never a database.**
|
||||||
|
|
||||||
|
Three coordination needs live in the backbone:
|
||||||
|
|
||||||
|
- **`mesh_reserve` path reservations**: reserve before editing; other peers' `edit`/`write` tools get blocked on those paths — concurrent editing is prevented by mechanism, not by shouting.
|
||||||
|
- **`mesh_claim` task leases**: a single-writer lock with an `epoch:fence` (a broker restart invalidates old fences); commits carry `Mesh-Lease: task#leaseId#epoch:fence`, and the bundled commit-msg hook rejects stale-fence commits outright.
|
||||||
|
- **Capability routing `cap:`**: `mesh_send({ "to": "cap:deploy", ... })` routes the task to any idle peer advertising that capability instead of waiting for a busy teammate. Three message priorities: normal queues, urgent interrupts, force aborts the recipient's current turn and delivers once it settles (force is a control class and requires a reason).
|
||||||
|
|
||||||
|
Add `mesh_wait_all` for group verdicts (one call returns who answered and who is missing), `mesh_status` for peer activity (● working / ○ idle / ✕ stuck), and a `pimesh` CLI for session-less access (`pimesh attach` can adopt a dead session's identity). Multi-machine deployment is supported: one machine runs the broker, the rest connect as clients.
|
||||||
|
|
||||||
|
The honesty reaches down to exit codes: in the CLI, 3 means `expired` — a late reply is still delivered, you just stopped waiting; 5 means a stale lease, and the `commit-msg` hook rejects the commit on that basis. Even the host-specific edge of ESC purging the message queue is covered: a batch injected just as a turn aborts is re-delivered through an abort-safe channel once the turn settles (bounded retries), with a re-delivery counter visible in status.
|
||||||
|
|
||||||
|
## 2. pi-board: the shared context board
|
||||||
|
|
||||||
|
Messages are ephemeral; the ledger is hash-only. Where does "what we learned" live? [pi-board](https://github.com/wait4xx/pi-board) fills that layer. Five typed entries:
|
||||||
|
|
||||||
|
- **OBSERVED** — something seen, no commitment
|
||||||
|
- **FACT** — a verified truth
|
||||||
|
- **FAIL** — a falsified path (the highest-value kind: the whole team stops re-burning budget on it)
|
||||||
|
- **CLAIM** — an ownership note, paired with a `mesh_claim` lease (the board records; it never enforces)
|
||||||
|
- **PATCH_SUMMARY** — what a landed change did
|
||||||
|
|
||||||
|
The design is bounded and honest throughout: append-only local storage, refuse-not-evict (over-limit writes are refused, never silently evicted), short entries by default with a `detail` field unfolded only by id, boolean search (`,` = OR, `&` = AND) across the full history while `board_read` serves only the recent window; a search miss returns an honest empty, never a guess. Writes go to the room as ordinary broadcast frames; a new-FAIL digest line rides every turn boundary; the room's member / observer roles follow mesh semantics, so a read-only peer is never re-admitted as a writer after a reconnect. It joins the room as a normal peer — **all storage stays at the board's home; zero persistence in the broker**. A bundled `piboard` CLI does it in five verbs — join (member / observer) / watch / write / read / grep, one verb per action — so even a session-less terminal can join a room read-only and observe. The typology and the "small entries + on-demand detail + recent memory / full-history search" split come straight from the research literature (the lineage story is in [the next post](/en/archives/agensh/)).
|
||||||
|
|
||||||
|
## 3. pi-jobd: the background job daemon
|
||||||
|
|
||||||
|
`bash` blocks the current turn; `/reload` kills the process. [pi-jobd](https://github.com/cgarrot/pi-jobd) runs long commands in their own process group supervised by a per-user daemon: it returns in ~150 ms, and the job outlives the session. When to use it? Three criteria: the command will run longer than 60–90 seconds; you must keep working while it runs; or it must survive `/reload` and session end — plus "another agent should be able to stop it": `mesh_job_control` lets any authorized peer remotely stop/pause/resume someone else's background job.
|
||||||
|
|
||||||
|
The state machine is equally honest: `running / paused / stopped / exited / failed`, plus `lost` — when the daemon restarts and cannot prove pid ownership, it refuses to stop rather than signal an unproven process. One more clever touch: installing the package is not enough — the model must *know when* to use it. Three layers feed the model the decision rule: a prompt snippet, guidelines on all seven tools, and an on-demand skill.
|
||||||
|
|
||||||
|
## 4. pi-subagents: worker control
|
||||||
|
|
||||||
|
[pi-subagents](https://github.com/nicobailon/pi-subagents) manages in-process child agents: foreground children stream inside the conversation; background children live in a detached runner process you can check later. It works right after install with no slash commands to learn — just say "use reviewer to review this diff". It ships common roles like scout and reviewer, and supports saved workflows and parallel reviews. Its mesh interface is `mesh_run_control`: remotely stop or steer a peer's RPC worker.
|
||||||
|
|
||||||
|
## 5. One rehearsal of the four-piece suite
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 6. Five minutes to onboard
|
||||||
|
|
||||||
|
One line each: mesh and subagents install as pi packages — `pi install npm:pi-mesh-extension`, `pi install npm:pi-subagents`; pi-board is not on npm yet — clone, `npm ci`, `npm run build` — and it works CLI-only (reading and writing the local board needs no mesh at all); pi-jobd builds from source the same way, with `pijob run / list / log / stop` working out of the box. Open two pi sessions in one directory and you have two peers visible to each other in `mesh_status` — "zero config" is not a slogan here.
|
||||||
|
|
||||||
|
Conversation over mesh, memory on the board, long work to jobd, exclusivity by lease — four packages, each doing its job, none overstepping. This blog's redesign was their proving ground: two mesh terminals (op-gitea, op-artalk) ran in parallel with the mainline, nineteen commits in one day; during the publish-chain outage, one lane reserved build.sh for emergency repair while an incoming hotfix edit was correctly blocked by the reservation — collisions are prevented by rule, not by reflex.
|
||||||
|
|
||||||
|
Together the four pieces contain no database and no central brain, yet pin down all four questions: who is online, who is doing what, what was learned, and who watches the long jobs. Small tools, hard boundaries, honest states — that philosophy is worth copying more than any individual package.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: "Spec Harness in Depth: Packaging Long-Horizon Development into Verifiable Task Packages"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [Engineering, AI Collaboration]
|
||||||
|
locked: true
|
||||||
|
---
|
||||||
|
|
||||||
|
What AI coding agents lack is not bandwidth for writing code — it is memory. A task that spans several sessions accumulates half-finished plans, already-falsified approaches, and acceptance criteria that get lost in chat history; restart the terminal and the context resets to zero. [Spec Harness](https://github.com/MicTx/spec-harness) targets exactly this: a task-package workflow tool for AI coding agents that persists long-horizon changes as a **recoverable, verifiable Development Record**, governed by scope, evidence, and Git gates. It provides no model and makes no product decisions for you.
|
||||||
|
|
||||||
|
Its worldview in one sentence: **when a coding task outlives one conversation, the missing tool is not a smarter model — it is a contract that everyone (including your future self) can read.**
|
||||||
|
|
||||||
|
## 1. The core abstraction: the task package
|
||||||
|
|
||||||
|
All work lives in a `.spec/specs/YYYY-MM-DD_slug/` directory with two core files: `spec.md` records scope, assumptions, and trade-offs (including a remarkable "falsified paths" section, more on that below), while `tasks.md` holds the task list. After closeout the whole package is archived under `.spec/specs/archive/` — the archive directory is the project's engineering log.
|
||||||
|
|
||||||
|
Every task must carry a two-line contract: **boundary** (which files this task may touch) and **verify** (how completion is proven). The verify is a read-only assertion that must exit 0 before the checkbox goes on. A real example from this blog's archived `2026-10-26_blog-5f` package, task T3:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Boundary delimits "what may change"; verify delimits "how completion is proven" — both lines present, or the task stays unchecked. This is not formalism: with multiple agents running in parallel, boundary is the guardrail between lanes (in our practice, global style files are mainline-only; lanes request new variables instead of reaching in). Verify separates "looks done" from "proven done" — a verbal "passed" never counts.
|
||||||
|
|
||||||
|
The most interesting section of `spec.md` is **falsified paths**: rejected proposals go into the archive together with the reason for rejection. Our blog-5f package records three — hero banner treatment (rejected by visual adjudication), a green heatmap (broke the grayscale skeleton), and shrinking the title from 27px to 22px (a hierarchy anchor; reduce weight, not size). Whoever wants to try again reads the file first and saves a round of budget.
|
||||||
|
|
||||||
|
## 2. The stage pipeline: five core commands and the gates they keep
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Five core commands form one verifiable chain: **goal → scope → tasks → verification → archive → commit → push**. Gates stand between stages: `run` refuses while `spec.md` still has template placeholders; `done` requires every task to have boundary/verify, a fully green acceptance surface, and at least one non-placeholder piece of evidence (script output, test results); `push` merges only `spec/`-prefixed integration branches, never combines file changes with a commit, and forbids `--no-verify`.
|
||||||
|
|
||||||
|
A blocked task gets a `!` marker and surfaces to the user — never a silent skip. Actionable issues found during `check` must be written back into the package and fixed in the same round; "to-dos" are not deliverables.
|
||||||
|
|
||||||
|
**Interruption recovery is a designed norm** — the most direct answer to "tasks outlive conversations": re-run `/spec:run` at any moment and it resumes from the checked state in `tasks.md`, depending on no chat memory. Going further, `/spec:autoplan` runs interactive planning (clarify the goal, then framework, detail, and review passes; it stops when planning is ready — no auto-start), and `/spec:autorun` executes package by package, capped at 5 auto rounds by default (`--max-rounds` adjustable), resuming from the same step after a pause; the one-shot entry `/spec:goal` runs planning, execution, review, archiving, and the commit in a single pass.
|
||||||
|
|
||||||
|
The command surface totals 12 stage commands: the core profile installs the five core commands, and full adds the seven extensions — goal / autoplan / autorun / update / status / doctor / organize — where `doctor` self-checks and repairs the environment and `organize` audits structure. One easily-missed detail: installing only lays down files; it never auto-evaluates every task. If you want it to proactively ask "should this get a package?" before you start, wire the activation hooks once with `install_activation_hooks.py --mode on`. The criterion is delivery shape — an independent acceptance surface, actual implementers, a recoverable in-flight task; small fixes, read-only investigations, and one-off experiments just get done. It guards against "forgot to evaluate", not against deliberate under-reporting — the documentation is refreshingly candid about this.
|
||||||
|
|
||||||
|
## 3. Multi-agent orchestration: one orchestrator, bounded helpers
|
||||||
|
|
||||||
|
Spec Harness is deliberately conservative about multi-agent work: **the main session is the single orchestrator** — routing, integration, acceptance, and the `done`/`push` gates never leave the mainline. Sidecar work (exploration, building, reviewing — context-independent slices) may be delegated, but only after a machine route decision (`scripts/route_decision.py`) and a written assignment contract: goal, scope, excluded areas, output, and verification method, all five present. An optional `handoff.md` covers cross-session boundaries — a generative snapshot header plus an append-only entry log, with required sections that adapt to solo / multi-person / agent / cluster collaboration modes.
|
||||||
|
|
||||||
|
Extension slots plug other team processes in: `team-loop` for team cycles and `workflow-runner` for workflow fan-out, each with its README protocol and tests. The source repo also ships four orchestration agent definitions: orchestrator, planner, reviewer, confirmer.
|
||||||
|
|
||||||
|
## 4. Installation: one installer, a dozen-plus hosts
|
||||||
|
|
||||||
|
Spec Harness is a Python 3.9+ standard-library implementation (PolyForm Noncommercial license), installed in three steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/MicTx/spec-harness.git /tmp/spec-harness
|
||||||
|
cd /tmp/spec-harness
|
||||||
|
git checkout v0.22.2
|
||||||
|
bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer supports a dozen-plus hosts: Claude Code, Codex, Gemini CLI, Grok, OpenCode, OpenClaw, Hermes, Pi, ZCode, DeepSeek Harness, Devin, Cursor… auto-detecting installed hosts when unspecified. Install profiles come in core / full: core ships the five core stage commands plus Git hooks; full adds all 12 stage commands and the slots/agents extensions. We use `INSTALL_HOSTS=pi bash install.sh` on Pi — the `/spec` commands work right after installation.
|
||||||
|
|
||||||
|
## 5. Running on a real blog
|
||||||
|
|
||||||
|
This blog is a user. The archive directory holds a chain of packages — from the bare-clone build rework and mobile layout fixes to the SEO unblocking: blog-5f packed eight tasks on three lanes (two mesh terminals plus the mainline) running in parallel, every task's verify a command-line assertion, page tasks wired to E2E — `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs`, all 34 passing. Packages relayed one after another; nineteen green commits landed in a single day.
|
||||||
|
|
||||||
|
Tools iterate, but the idea does not expire: **write "what to do" as a contract, write "done" as evidence, archive the process as a log.** Whether the collaborators are humans or agents, those three things are the foundation of long-horizon engineering.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
title: "Writing template: this post is the template"
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: tech
|
||||||
|
tags: [writing, template]
|
||||||
|
pinned: true
|
||||||
|
locked: true
|
||||||
|
description: One post that exercises every typographic capability on the site — heading levels, colour emphasis, typefaces, quotes, code windows, tables, images, link cards, folding. Copy it to start a new post.
|
||||||
|
---
|
||||||
|
|
||||||
|
This is not a tutorial — it is a **template you can copy**. Every section below actually runs: what you see is what Markdown produces. To start a new post, copy this file, change the slug, and delete what you do not need.
|
||||||
|
|
||||||
|
It is also the live sample for **pinning**: `pinned: true` in the frontmatter lifts a post to the front of the home page, the archive and the bookshelf, while its date, URL and RSS timestamps stay untouched.
|
||||||
|
|
||||||
|
## 1. Heading levels and paragraphs
|
||||||
|
|
||||||
|
Use `##` for a section and `###` for a subsection. **Never use `#`** — the post title comes from the frontmatter `title`, and a second level-one heading confuses both screen readers and search snippets.
|
||||||
|
|
||||||
|
Leave a blank line between paragraphs. Inline you can use **bold**, *italic*, ~~strikethrough~~, `inline code`, and [ordinary links](https://astro.build).
|
||||||
|
|
||||||
|
## 2. Colour emphasis: as much as you need
|
||||||
|
|
||||||
|
Colour is **no longer capped** (the old "three per page" limit was lifted on 2026-11-02). The one rule that remains is the shell: navigation, footer, buttons and background stay greyscale — the shell is temperament, the body is expression.
|
||||||
|
|
||||||
|
<span class="c-blue">First use: state one key conclusion in blue.</span> Then carry on normally.
|
||||||
|
|
||||||
|
<span class="c-red">Second use: warn about a trap in red.</span> For example, “after changing a Markdown plugin you must clear `node_modules/.astro`, or the change silently does nothing”.
|
||||||
|
|
||||||
|
<span class="c-green">Confirm a working path in green.</span> If a whole paragraph deserves emphasis, mark the whole paragraph.
|
||||||
|
|
||||||
|
There are eight hues (blue / teal / green / yellow / orange / red / pink / purple), each with a standard and a `-soft` variant, plus four greyscale classes (`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`) for lowering tone — those do not count against the colour budget.
|
||||||
|
|
||||||
|
## 3. Typefaces: change only what you wrap
|
||||||
|
|
||||||
|
Body text is sans-serif by default. To give one sentence a different temperament, wrap it:
|
||||||
|
|
||||||
|
<span class="font-kai">This sentence is in Kai. Good for epigraphs, quoted lines in a review, or paragraphs that should be read slowly.</span>
|
||||||
|
|
||||||
|
<span class="font-huiwen">This sentence is in Huiwen Ming. Over a long paragraph it noticeably slows reading, so use it for short lines only.</span>
|
||||||
|
|
||||||
|
Six families are available (`font-kai`, `font-song`, `font-hei-sys`, `font-fangsong`, `font-huiwen`, `font-kuaile`), plus an artistic heading class `font-art-kuaile`. **Mind the weight**: switching a whole page to a family makes every visitor download a CJK font, so avoid changing the body default casually.
|
||||||
|
|
||||||
|
### A level-three heading looks like this
|
||||||
|
|
||||||
|
One step smaller than level two, bold, with tighter spacing. Use it to split a section into topics.
|
||||||
|
|
||||||
|
## 4. Quotes and lists
|
||||||
|
|
||||||
|
> Block quotes are for quoting someone. An attribution can follow as italics.
|
||||||
|
>
|
||||||
|
> — an invented attribution
|
||||||
|
|
||||||
|
Unordered list:
|
||||||
|
|
||||||
|
- First item
|
||||||
|
- Second item, with `inline code`
|
||||||
|
- Third item, nested:
|
||||||
|
- Child one
|
||||||
|
- Child two
|
||||||
|
|
||||||
|
Ordered lists suit steps:
|
||||||
|
|
||||||
|
1. Copy the template
|
||||||
|
2. Change the slug and title in the frontmatter
|
||||||
|
3. Delete the sections you do not need
|
||||||
|
|
||||||
|
## 5. Code blocks
|
||||||
|
|
||||||
|
Blocks of four lines or more get a greyscale window bar (three dots); shorter fragments do not, so small snippets do not look like screenshots.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Sort contract: pinned first → newest first → id as a tie-breaker
|
||||||
|
export function sortPosts<T extends Post>(posts: T[]): T[] {
|
||||||
|
return [...posts].sort((a, b) => {
|
||||||
|
const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true);
|
||||||
|
return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to variables inline like `posts`.
|
||||||
|
|
||||||
|
## 6. Tables
|
||||||
|
|
||||||
|
| Capability | Syntax | Note |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Colour emphasis | `<span class="c-blue">` | no limit (shell stays greyscale) |
|
||||||
|
| Typeface | `<span class="font-kai">` | a whole page adds font weight |
|
||||||
|
| Image | `` | see the writing guide for size rules |
|
||||||
|
| Video | frontmatter `video:` | lives in public/media, ≤50MB |
|
||||||
|
| Link card | a bare URL on its own line | OG fetched at build time, falls back to a plain link |
|
||||||
|
|
||||||
|
## 7. Images
|
||||||
|
|
||||||
|
Reference images by relative path, stored under `content/posts/assets/<slug>/`; they expand on click and vectors stay sharp:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 8. Link cards
|
||||||
|
|
||||||
|
A bare URL on its own line becomes an OG preview card at build time; if the fetch fails it degrades to a plain link, never breaking the layout:
|
||||||
|
|
||||||
|
https://astro.build
|
||||||
|
|
||||||
|
## 9. Folding long content
|
||||||
|
|
||||||
|
Long appendices, full logs and big configs can fold away with zero JavaScript, using native `details`:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Open the folded example</summary>
|
||||||
|
|
||||||
|
A folded region can hold anything — paragraphs, lists, code blocks.
|
||||||
|
|
||||||
|
- Best for “read on demand” detail
|
||||||
|
- Not for conclusions (readers may never open it)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 10. Closing after the rule
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Three sentences are enough to close: what this covered, what the reader should take away, and where to go next. The template is not a demand to write ten sections — it is a promise that **the capabilities are at hand when you need to express something**.
|
||||||
|
|
||||||
|
Next time, copy this file and delete until only your point remains.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: "Agensh:把组织智能扩展到 1,024 个智能体"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [论文笔记, AI 协作]
|
||||||
|
---
|
||||||
|
|
||||||
|
多智能体系统能靠并发压缩复杂任务的时延,但主流框架有一个共同的天花板:中心编排器。任务分配、进度汇总、结果裁决全部挤过同一个上下文,规模越大,编排器先死。2026 年 10 月,微软研究院开源了 [Agensh](https://github.com/microsoft/Agensh)——一个**没有中心编排器**的自组织多智能体 harness,论文[《Scaling Organizational Intelligence to 1,024 Agents》](https://arxiv.org/abs/2609.26781)把它推到了一千零二十四个 agent 的规模。
|
||||||
|
|
||||||
|
它的答案出人意料地朴素:与其造一个更聪明的调度大脑,不如把一个人类组织的协同基建原样搬给 agent 们。
|
||||||
|
|
||||||
|
> 📄 **[在线研读全文 PDF →](/papers/agensh/)** · arXiv 2609.26781 · 微软研究院
|
||||||
|
|
||||||
|
## 一、三支柱:组织的协同基建
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Agensh 只给三样基础设施,其余全部自组织:
|
||||||
|
|
||||||
|
- **共享工作区**:git 平台承载组织的进行中工作与集成结果,分支、PR、合并冲突这些人类用了二十年的机制原样复用;
|
||||||
|
- **消息接口**:团队频道加私信,带优先级送达——组织级广播与紧急碰撞调解各走各的道;
|
||||||
|
- **共享上下文**:一块类型化条目板,存组织的发现与工作意图。
|
||||||
|
|
||||||
|
开源参考实现用的是 Gitea 承载工作区、Mattermost 承载频道与私信——一套完全开源自接的“办公室”,没有任何私有服务。没有编排器分配任务:worker 自己从工作区和上下文里找活、认领、验证、合并。规模化时天生的瓶颈(那个越来越忙的中枢)根本不存在。
|
||||||
|
|
||||||
|
## 二、协作环:每个 worker 各跑一圈
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
每个 worker 并发、异步地执行同一个协作环:**收集上下文 → 认领子任务 → 执行 → 验证 → 合并进度**。收集上下文时读共享工作区和条目板的存量知识;认领靠 CLAIM 声明,撞车了私聊解决;验证自己跑测试,证据说话;合并走 PR。
|
||||||
|
|
||||||
|
最有意思的设计在共享上下文的**经济学**:条目刻意短小(约 100 字符,PATCH_SUMMARY 放宽到 300),长版本放 `detail` 字段按需展开;`board_read` 只服务最近 2000 条,更早的历史只能靠布尔检索(`,` = OR,`&` = AND)翻出来——近期记忆保持小,长尾靠搜索。论文称 **FAIL 是价值最高的条目**:一条被证伪的路径写下来,就能止住全队在同一个死胡同里重复烧预算。新条目会作为较高优先级的更新,在同伴的下一个工具调用边界推送过去。
|
||||||
|
|
||||||
|
这套共享上下文不是 Agensh 的发明——论文 §2.3 明说“共享上下文遵循 DeLM 的核心思想”。[DeLM](https://github.com/yuzhenmao/DeLM)([arXiv:2606.10662](https://arxiv.org/abs/2606.10662))用共享上下文加任务队列跑去中心化多智能体:它的板是一张 append-only 事件日志(条目、认领、完成、FACT/FAIL 笔记、状态),正文存在日志旁的独立数据库,带硬上限(≤40 个文件、≤400KB 总量),还有一个阻塞式的 `read --wait`——同伴发帖即唤醒等待者。在 Terminal-Bench 4.0 的 10 项子集上,两个 agent 通过共享上下文协同,平均提升 12.5 个百分点准确率、1.78 倍加速。Agensh 把这条线索放大到了千级规模。
|
||||||
|
|
||||||
|
## 三、数字:扩容真的有用吗
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
两组数据回答"加 agent 到底加不加得动":ProgramBench 最难 5 题,1 → 128 个 agent,通过率 19.31% → 28.78%(相对提升约 49%);从零构建 pandoc(6 小时预算、无外网),1 → 1,024 个 agent,通过率 33.89% → 55.06%。对纯堆算力已经边际递减的难题,**组织化的边际收益还在**。
|
||||||
|
|
||||||
|
工程可用性也到位了:v1.0 于 2026-10-23 发布,六天后的 v1.0.1 已支持 Copilot、Codex、Claude Code、Pi 作为底层单智能体 harness——单 agent 侧你可以用任何顺手的主力模型。开源版是单节点 harness,v1.0 发布包里就带着复现指南、论文与演示——从论文到可跑的仓库,中间没有隔着一句“敬请期待”。
|
||||||
|
|
||||||
|
## 四、从论文到桌面:一个 issue 的故事
|
||||||
|
|
||||||
|
这篇论文对我们不只在纸上。把它读透之后,2026-10-23 我在 pi-mesh 仓库提了[第 13 号 issue](https://github.com/cgarrot/pi-mesh/issues/13):pi-mesh 已有消息接口(`mesh_send`/房间/优先级)和共享工作区(git + `mesh_reserve` 预约),独缺第三支柱共享上下文——提议按 Agensh/DeLM 的形状加 `board_write / board_read / board_grep`。
|
||||||
|
|
||||||
|
维护者 cgarrot 的回复是整个故事里最值得学的一课:方向对,但**hash-only 台账让 pi-mesh 结构上当不了这块板**——把持久化塞进 broker 等于把路由器改造成数据库。正确的形状是独立包:以普通 peer 入房、自有存储、mesh 继续只做路由。于是有了 [pi-board](https://github.com/wait4xx/pi-board):一天内出 v0.1,55 个测试零失败、真实 broker 上九步冒烟,issue 最终关闭时官方指针就落在它身上。上一篇文章介绍的[四件套](/archives/pi-mesh-stack/),其记忆层就是从这篇论文长出来的。
|
||||||
|
|
||||||
|
## 五、结语
|
||||||
|
|
||||||
|
Agensh 的启示不是"1024"这个数字,而是它对"组织智能"的拆解:工作区、消息、共享上下文——三根支柱撑起自组织,任何一根都不需要一个更聪明的大脑。这个拆解在千级规模成立,在单机四件套成立,在单人加几个 agent 终端的小作坊同样成立。规模会变,支柱不变。
|
||||||
|
|
||||||
|
相关阅读:[Spec Harness 详解](/archives/spec-harness/) · [pi-mesh 全家桶详解](/archives/pi-mesh-stack/)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="330" fill="#ffffff"/>
|
||||||
|
<text x="480" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">多智能体协作环:每个 worker 并发、异步地各跑一圈</text>
|
||||||
|
|
||||||
|
<!-- 环上 5 步 -->
|
||||||
|
<g font-size="12.5" fill="#33343a">
|
||||||
|
<rect x="46" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="121" y="182" text-anchor="middle" font-weight="600" fill="#111113">① 收集上下文</text>
|
||||||
|
<text x="121" y="204" text-anchor="middle">读工作区 + 检索条目板</text>
|
||||||
|
|
||||||
|
<rect x="226" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="301" y="182" text-anchor="middle" font-weight="600" fill="#111113">② 认领子任务</text>
|
||||||
|
<text x="301" y="204" text-anchor="middle">声明 CLAIM,冲突私聊解</text>
|
||||||
|
|
||||||
|
<rect x="406" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="481" y="182" text-anchor="middle" font-weight="600" fill="#111113">③ 执行</text>
|
||||||
|
<text x="481" y="204" text-anchor="middle">本地改动推送分支</text>
|
||||||
|
|
||||||
|
<rect x="586" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="661" y="182" text-anchor="middle" font-weight="600" fill="#111113">④ 验证</text>
|
||||||
|
<text x="661" y="204" text-anchor="middle">跑测试,证据说话</text>
|
||||||
|
|
||||||
|
<rect x="766" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="841" y="182" text-anchor="middle" font-weight="600" fill="#111113">⑤ 合并进度</text>
|
||||||
|
<text x="841" y="204" text-anchor="middle">PR 合入共享工作区</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g stroke="#6b6e78" stroke-width="1.5">
|
||||||
|
<line x1="196" y1="188" x2="224" y2="188"/><polygon points="224,188 215,183.5 215,192.5" fill="#6b6e78"/>
|
||||||
|
<line x1="376" y1="188" x2="404" y2="188"/><polygon points="404,188 395,183.5 395,192.5" fill="#6b6e78"/>
|
||||||
|
<line x1="556" y1="188" x2="584" y2="188"/><polygon points="584,188 575,183.5 575,192.5" fill="#6b6e78"/>
|
||||||
|
<line x1="736" y1="188" x2="764" y2="188"/><polygon points="764,188 755,183.5 755,192.5" fill="#6b6e78"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 841 226 L 841 262 L 121 262 L 121 228" fill="none" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="121,228 116,237 126,237" fill="#6b6e78"/>
|
||||||
|
<text x="481" y="256" text-anchor="middle" font-size="12" fill="#6b6e78" dy="-6">下一圈</text>
|
||||||
|
|
||||||
|
<!-- 共享上下文条 -->
|
||||||
|
<rect x="120" y="70" width="720" height="52" rx="10" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.2"/>
|
||||||
|
<text x="480" y="92" text-anchor="middle" font-size="13" font-weight="600" fill="#3454d1">共享上下文(类型化条目:OBSERVED / FACT / FAIL / CLAIM / PATCH_SUMMARY)</text>
|
||||||
|
<text x="480" y="112" text-anchor="middle" font-size="11.5" fill="#6b6e78">全程发布发现;条目刻意短小(约 100 字符),长文 detail 按需展开;近 2000 条常驻,全史靠布尔检索</text>
|
||||||
|
<line x1="200" y1="122" x2="150" y2="148" stroke="#a0a3ad" stroke-dasharray="4 3"/>
|
||||||
|
<line x1="760" y1="122" x2="820" y2="148" stroke="#a0a3ad" stroke-dasharray="4 3"/>
|
||||||
|
<text x="480" y="308" text-anchor="middle" font-size="12.5" fill="#6b6e78">论文称 FAIL 是"价值最高的条目":一条被证伪的路,能止住全队的重复预算</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,64 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="420" viewBox="0 0 960 420" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="420" fill="#ffffff"/>
|
||||||
|
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">从中心编排到自组织:Agensh 的三支柱</text>
|
||||||
|
|
||||||
|
<!-- 左:中心编排器 -->
|
||||||
|
<rect x="40" y="70" width="360" height="290" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="220" y="98" text-anchor="middle" font-size="13.5" font-weight="600" fill="#6b6e78">传统多智能体:中心编排器瓶颈</text>
|
||||||
|
<rect x="150" y="116" width="140" height="40" rx="8" fill="#ffffff" stroke="#6b6e78"/>
|
||||||
|
<text x="220" y="141" text-anchor="middle" font-size="12.5" fill="#33343a">orchestrator</text>
|
||||||
|
<g stroke="#a0a3ad">
|
||||||
|
<line x1="220" y1="156" x2="100" y2="230"/>
|
||||||
|
<line x1="220" y1="156" x2="180" y2="230"/>
|
||||||
|
<line x1="220" y1="156" x2="260" y2="230"/>
|
||||||
|
<line x1="220" y1="156" x2="340" y2="230"/>
|
||||||
|
</g>
|
||||||
|
<g font-size="11.5" fill="#6b6e78">
|
||||||
|
<rect x="72" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="100" y="249" text-anchor="middle">w1</text>
|
||||||
|
<rect x="152" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="180" y="249" text-anchor="middle">w2</text>
|
||||||
|
<rect x="232" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="260" y="249" text-anchor="middle">w3</text>
|
||||||
|
<rect x="312" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="340" y="249" text-anchor="middle">…</text>
|
||||||
|
</g>
|
||||||
|
<text x="220" y="300" text-anchor="middle" font-size="12" fill="#6b6e78">任务分配、进度汇总、结果裁决</text>
|
||||||
|
<text x="220" y="320" text-anchor="middle" font-size="12" fill="#6b6e78">全部挤过同一个上下文——</text>
|
||||||
|
<text x="220" y="340" text-anchor="middle" font-size="12" fill="#6b6e78">规模越大,编排器先成为天花板</text>
|
||||||
|
|
||||||
|
<!-- 中间箭头 -->
|
||||||
|
<line x1="410" y1="215" x2="460" y2="215" stroke="#6b6e78" stroke-width="1.6"/>
|
||||||
|
<polygon points="460,215 450,210 450,220" fill="#6b6e78"/>
|
||||||
|
|
||||||
|
<!-- 右:自组织三支柱 -->
|
||||||
|
<rect x="470" y="70" width="450" height="290" rx="10" fill="#ffffff" stroke="#3454d1" stroke-width="1.4"/>
|
||||||
|
<text x="695" y="98" text-anchor="middle" font-size="13.5" font-weight="600" fill="#3454d1">Agensh:无中心编排,三支柱基础设施</text>
|
||||||
|
<g font-size="12" fill="#33343a">
|
||||||
|
<rect x="490" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="555" y="140" text-anchor="middle" font-weight="600" fill="#111113">共享工作区</text>
|
||||||
|
<text x="555" y="160" text-anchor="middle">git 平台</text>
|
||||||
|
<text x="555" y="178" text-anchor="middle">分支 / PR / 合并</text>
|
||||||
|
<rect x="630" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="695" y="140" text-anchor="middle" font-weight="600" fill="#111113">消息接口</text>
|
||||||
|
<text x="695" y="160" text-anchor="middle">团队频道 + 私信</text>
|
||||||
|
<text x="695" y="178" text-anchor="middle">优先级送达</text>
|
||||||
|
<rect x="770" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="835" y="140" text-anchor="middle" font-weight="600" fill="#111113">共享上下文</text>
|
||||||
|
<text x="835" y="160" text-anchor="middle">类型化条目板</text>
|
||||||
|
<text x="835" y="178" text-anchor="middle">近期记忆 + 全史检索</text>
|
||||||
|
</g>
|
||||||
|
<g stroke="#a0a3ad">
|
||||||
|
<line x1="555" y1="200" x2="520" y2="240"/>
|
||||||
|
<line x1="695" y1="200" x2="660" y2="240"/>
|
||||||
|
<line x1="835" y1="200" x2="800" y2="240"/>
|
||||||
|
<line x1="555" y1="200" x2="620" y2="240"/>
|
||||||
|
<line x1="695" y1="200" x2="740" y2="240"/>
|
||||||
|
<line x1="835" y1="200" x2="880" y2="240"/>
|
||||||
|
</g>
|
||||||
|
<g font-size="11.5" fill="#6b6e78">
|
||||||
|
<rect x="492" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="520" y="259" text-anchor="middle">w1</text>
|
||||||
|
<rect x="592" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="620" y="259" text-anchor="middle">w2</text>
|
||||||
|
<rect x="692" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="740" y="259" text-anchor="middle">w3</text>
|
||||||
|
<rect x="792" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="820" y="259" text-anchor="middle">w4</text>
|
||||||
|
<rect x="852" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="880" y="259" text-anchor="middle">…</text>
|
||||||
|
</g>
|
||||||
|
<text x="695" y="308" text-anchor="middle" font-size="12" fill="#6b6e78">worker 自己跑协作环:收集上下文 → 认领 → 执行 → 验证 → 合并</text>
|
||||||
|
<text x="695" y="330" text-anchor="middle" font-size="12" fill="#6b6e78">发现发布到共享上下文,同伴在下一个工具调用边界收到广播</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="380" viewBox="0 0 960 380" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="380" fill="#ffffff"/>
|
||||||
|
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">规模收益:扩容 agent 带来的通过率提升</text>
|
||||||
|
<text x="480" y="58" text-anchor="middle" font-size="11.5" fill="#a0a3ad">数据:arXiv:2609.26781</text>
|
||||||
|
|
||||||
|
<!-- 组 1:ProgramBench -->
|
||||||
|
<text x="235" y="92" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">ProgramBench 最难 5 题</text>
|
||||||
|
<text x="235" y="112" text-anchor="middle" font-size="12" fill="#6b6e78">1 → 128 agents</text>
|
||||||
|
<g>
|
||||||
|
<!-- 柱底 y=300,柱顶 = 300 - 值*3.4 -->
|
||||||
|
<rect x="115" y="235" width="100" height="65" fill="#a0a3ad"/>
|
||||||
|
<rect x="255" y="202" width="100" height="98" fill="#3454d1"/>
|
||||||
|
<text x="165" y="227" text-anchor="middle" font-size="14" font-weight="600" fill="#33343a">19.31%</text>
|
||||||
|
<text x="165" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1 agent</text>
|
||||||
|
<text x="305" y="194" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1">28.78%</text>
|
||||||
|
<text x="305" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">128 agents</text>
|
||||||
|
</g>
|
||||||
|
<text x="235" y="352" text-anchor="middle" font-size="12" fill="#33343a">相对提升约 49%</text>
|
||||||
|
|
||||||
|
<!-- 组 2:pandoc -->
|
||||||
|
<text x="645" y="92" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">从零构建 pandoc(6h 预算 · 无外网)</text>
|
||||||
|
<text x="645" y="112" text-anchor="middle" font-size="12" fill="#6b6e78">1 → 1,024 agents</text>
|
||||||
|
<g>
|
||||||
|
<rect x="525" y="187" width="100" height="113" fill="#a0a3ad"/>
|
||||||
|
<rect x="665" y="113" width="100" height="187" fill="#3454d1"/>
|
||||||
|
<text x="575" y="179" text-anchor="middle" font-size="14" font-weight="600" fill="#33343a">33.89%</text>
|
||||||
|
<text x="575" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1 agent</text>
|
||||||
|
<text x="715" y="105" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1">55.06%</text>
|
||||||
|
<text x="715" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1,024 agents</text>
|
||||||
|
</g>
|
||||||
|
<text x="645" y="352" text-anchor="middle" font-size="12" fill="#33343a">+21.2 个百分点</text>
|
||||||
|
|
||||||
|
<line x1="480" y1="84" x2="480" y2="330" stroke="#e3e5ea"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="980" height="330" viewBox="0 0 980 330" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="980" height="330" fill="#ffffff"/>
|
||||||
|
<text x="490" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">一次典型合奏:四件套怎么咬合</text>
|
||||||
|
|
||||||
|
<!-- 5 nodes -->
|
||||||
|
<rect x="30" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="109" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">mesh_send</text>
|
||||||
|
<text x="109" y="132" text-anchor="middle" font-size="12" fill="#33343a">契约式派单 / 对话</text>
|
||||||
|
<text x="109" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">五字段任务包 + 期限</text>
|
||||||
|
|
||||||
|
<rect x="216" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="295" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">board_grep</text>
|
||||||
|
<text x="295" y="132" text-anchor="middle" font-size="12" fill="#33343a">先查团队记忆</text>
|
||||||
|
<text x="295" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">FAIL 可检索不重烧</text>
|
||||||
|
|
||||||
|
<rect x="402" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="481" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">job_run</text>
|
||||||
|
<text x="481" y="132" text-anchor="middle" font-size="12" fill="#33343a">长活交后台</text>
|
||||||
|
<text x="481" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">turn 不阻塞</text>
|
||||||
|
|
||||||
|
<rect x="588" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="667" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">mesh_claim</text>
|
||||||
|
<text x="667" y="132" text-anchor="middle" font-size="12" fill="#33343a">单写者租约</text>
|
||||||
|
<text x="667" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">epoch:fence 围栏</text>
|
||||||
|
|
||||||
|
<rect x="774" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="853" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">board_write</text>
|
||||||
|
<text x="853" y="132" text-anchor="middle" font-size="12" fill="#33343a">留痕</text>
|
||||||
|
<text x="853" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">CLAIM 带租约号</text>
|
||||||
|
|
||||||
|
<!-- 正向箭头 -->
|
||||||
|
<line x1="188" y1="128" x2="214" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="214,128 205,123.5 205,132.5" fill="#6b6e78"/>
|
||||||
|
<line x1="374" y1="128" x2="400" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="400,128 391,123.5 391,132.5" fill="#6b6e78"/>
|
||||||
|
<line x1="560" y1="128" x2="586" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="586,128 577,123.5 577,132.5" fill="#6b6e78"/>
|
||||||
|
<line x1="746" y1="128" x2="772" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="772,128 763,123.5 763,132.5" fill="#6b6e78"/>
|
||||||
|
|
||||||
|
<!-- 回环箭头 -->
|
||||||
|
<path d="M 853 176 L 853 216 L 109 216 L 109 178" fill="none" stroke="#a0a3ad" stroke-width="1.4" stroke-dasharray="5 4"/>
|
||||||
|
<polygon points="109,178 104,187 114,187" fill="#a0a3ad"/>
|
||||||
|
<text x="481" y="238" text-anchor="middle" font-size="12" fill="#6b6e78">发现与结论回写团队记忆,下一轮派单从存量知识起步</text>
|
||||||
|
|
||||||
|
<text x="490" y="290" text-anchor="middle" font-size="13.5" fill="#33343a">对话走 mesh · 记忆上 board · 长活交 jobd · 排他靠租约</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="470" viewBox="0 0 960 470" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="470" fill="#ffffff"/>
|
||||||
|
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">pi-mesh 工具族:一个路由器,三个卫星</text>
|
||||||
|
|
||||||
|
<!-- 顶部:pi 终端 -->
|
||||||
|
<rect x="80" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="180" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pi 终端 @op-gitea</text>
|
||||||
|
<text x="180" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">edit/write 受预约拦截</text>
|
||||||
|
<rect x="380" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="480" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pi 终端 @op-artalk</text>
|
||||||
|
<text x="480" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">读回执 · 忙闲状态</text>
|
||||||
|
<rect x="680" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="780" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pimesh attach / CLI</text>
|
||||||
|
<text x="780" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">无会话独立 peer</text>
|
||||||
|
|
||||||
|
<line x1="180" y1="120" x2="180" y2="150" stroke="#a0a3ad"/>
|
||||||
|
<line x1="480" y1="120" x2="480" y2="150" stroke="#a0a3ad"/>
|
||||||
|
<line x1="780" y1="120" x2="780" y2="150" stroke="#a0a3ad"/>
|
||||||
|
|
||||||
|
<!-- mesh 骨干 -->
|
||||||
|
<rect x="60" y="150" width="840" height="110" rx="12" fill="#ffffff" stroke="#3454d1" stroke-width="1.6"/>
|
||||||
|
<text x="90" y="180" font-size="15" font-weight="600" fill="#3454d1">pi-mesh —— 通信骨干(github.com/cgarrot/pi-mesh)</text>
|
||||||
|
<text x="90" y="206" font-size="12.5" fill="#33343a">broker(内存态:peers / rooms / 邮箱 / 限速)· 房间与角色 member/observer</text>
|
||||||
|
<text x="90" y="228" font-size="12.5" fill="#33343a">mesh_reserve 路径预约 · mesh_claim 单写者租约(epoch:fence,commit-msg 钩子拒过期围栏)</text>
|
||||||
|
<text x="90" y="250" font-size="12.5" fill="#33343a">优先级 normal / urgent / force · 状态诚实三档 delivered ≠ read ≠ answered · 台账 hash-only</text>
|
||||||
|
|
||||||
|
<!-- 连接线到卫星 -->
|
||||||
|
<line x1="240" y1="260" x2="240" y2="300" stroke="#a0a3ad"/>
|
||||||
|
<line x1="480" y1="260" x2="480" y2="300" stroke="#a0a3ad"/>
|
||||||
|
<line x1="720" y1="260" x2="720" y2="300" stroke="#a0a3ad"/>
|
||||||
|
|
||||||
|
<!-- 三个卫星 -->
|
||||||
|
<rect x="70" y="300" width="330" height="110" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="90" y="328" font-size="14" font-weight="600" fill="#111113">pi-board —— 团队记忆板</text>
|
||||||
|
<text x="90" y="350" font-size="12" fill="#33343a">OBSERVED/FACT/FAIL/CLAIM/PATCH_SUMMARY</text>
|
||||||
|
<text x="90" y="370" font-size="12" fill="#33343a">有界 append-only 存储 · 布尔检索 board_grep</text>
|
||||||
|
<text x="90" y="390" font-size="11.5" fill="#6b6e78">以普通 peer 入房;自有存储,broker 零持久化</text>
|
||||||
|
|
||||||
|
<rect x="430" y="300" width="330" height="110" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="450" y="328" font-size="14" font-weight="600" fill="#111113">pi-jobd —— 后台作业守护</text>
|
||||||
|
<text x="450" y="350" font-size="12" fill="#33343a">脱离 turn 的进程组 · 存活于 /reload 与会话之外</text>
|
||||||
|
<text x="450" y="370" font-size="12" fill="#33343a">诚实状态机(lost = 无法证明归属则拒操作)</text>
|
||||||
|
<text x="450" y="390" font-size="11.5" fill="#6b6e78">mesh_job_control 跨 peer 远程 stop/pause/resume</text>
|
||||||
|
|
||||||
|
<rect x="70" y="424" width="690" height="38" rx="8" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.1"/>
|
||||||
|
<text x="415" y="448" text-anchor="middle" font-size="12" fill="#33343a">pi-subagents(worker 控制:spawn/steer/abort,mesh_run_control)与上面三者同族协作</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="440" viewBox="0 0 960 440" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="440" fill="#ffffff"/>
|
||||||
|
<text x="480" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">任务包解剖:.spec/specs/YYYY-MM-DD_slug/</text>
|
||||||
|
|
||||||
|
<!-- 左:目录树 -->
|
||||||
|
<rect x="40" y="70" width="400" height="250" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="60" y="102" font-size="15" font-weight="600" fill="#111113" font-family="ui-monospace, monospace">2026-10-26_blog-5f/</text>
|
||||||
|
<text x="80" y="140" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">├── spec.md</text>
|
||||||
|
<text x="240" y="140" font-size="13" fill="#6b6e78">范围 · 假设 · 已证伪</text>
|
||||||
|
<text x="80" y="180" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">├── tasks.md</text>
|
||||||
|
<text x="240" y="180" font-size="13" fill="#6b6e78">boundary + verify</text>
|
||||||
|
<text x="80" y="220" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">└── handoff.md</text>
|
||||||
|
<text x="240" y="220" font-size="13" fill="#6b6e78">跨会话交接(可选)</text>
|
||||||
|
<line x1="80" y1="248" x2="400" y2="248" stroke="#e3e5ea"/>
|
||||||
|
<text x="80" y="278" font-size="13" fill="#6b6e78">收尾后整包归档至 .spec/specs/archive/,</text>
|
||||||
|
<text x="80" y="300" font-size="13" fill="#6b6e78">归档目录就是项目的工程日志。</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="440" y1="195" x2="500" y2="195" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="500,195 490,190 490,200" fill="#6b6e78"/>
|
||||||
|
<text x="470" y="180" text-anchor="middle" font-size="12" fill="#6b6e78">每个任务</text>
|
||||||
|
|
||||||
|
<!-- 右:任务卡片 -->
|
||||||
|
<rect x="500" y="70" width="420" height="250" rx="10" fill="#ffffff" stroke="#3454d1" stroke-width="1.5"/>
|
||||||
|
<rect x="500" y="70" width="420" height="40" rx="10" fill="rgba(52,84,209,0.06)"/>
|
||||||
|
<text x="520" y="96" font-size="13.5" font-weight="600" fill="#111113">- [x] T3 构建期内容 lint + build.sh 守卫</text>
|
||||||
|
<text x="520" y="136" font-size="12.5" fill="#33343a" font-family="ui-monospace, monospace">claim: op-gitea(mesh 车道)</text>
|
||||||
|
<text x="520" y="164" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">boundary: 新建 content-lint.mjs + 守卫挂载</text>
|
||||||
|
<text x="520" y="200" font-size="12.5" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">verify:</text>
|
||||||
|
<text x="540" y="224" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">node scripts/build-perf/content-lint.mjs dist</text>
|
||||||
|
<text x="540" y="246" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">&& bash -n infra/deploy/build.sh</text>
|
||||||
|
<text x="520" y="290" font-size="12.5" fill="#6b6e78">verify 是只读断言:跑出退出码 0 才许勾选,</text>
|
||||||
|
<text x="520" y="310" font-size="12.5" fill="#6b6e78">口头"过了"永远不算数。</text>
|
||||||
|
|
||||||
|
<text x="480" y="372" text-anchor="middle" font-size="13.5" fill="#6b6e78">boundary 划定"能动什么",verify 划定"怎么证明做完"——两行合同,缺一不勾</text>
|
||||||
|
<line x1="360" y1="392" x2="600" y2="392" stroke="#e3e5ea"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="350" viewBox="0 0 960 350" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
|
||||||
|
<rect width="960" height="350" fill="#ffffff"/>
|
||||||
|
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">阶段流水线:五个核心命令与它们把守的门</text>
|
||||||
|
|
||||||
|
<!-- 5 个节点 -->
|
||||||
|
<!-- new -->
|
||||||
|
<rect x="36" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="112" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:new</text>
|
||||||
|
<text x="112" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">建任务包</text>
|
||||||
|
<text x="112" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">spec.md + tasks.md</text>
|
||||||
|
<text x="112" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">模板占位未填 → 拒 run</text>
|
||||||
|
<!-- run -->
|
||||||
|
<rect x="220" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="296" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:run</text>
|
||||||
|
<text x="296" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">逐任务执行</text>
|
||||||
|
<text x="296" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">verify 跑通才勾选</text>
|
||||||
|
<text x="296" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">阻塞任务打 ! 上浮</text>
|
||||||
|
<!-- check -->
|
||||||
|
<rect x="404" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="480" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:check</text>
|
||||||
|
<text x="480" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">复核轮</text>
|
||||||
|
<text x="480" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">问题当轮回写修复</text>
|
||||||
|
<text x="480" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">闭环项五选一登记</text>
|
||||||
|
<!-- done -->
|
||||||
|
<rect x="588" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="664" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:done</text>
|
||||||
|
<text x="664" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">归档 + 包提交</text>
|
||||||
|
<text x="664" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">证据随包入档</text>
|
||||||
|
<text x="664" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">验收面证据必须真实</text>
|
||||||
|
<!-- push -->
|
||||||
|
<rect x="772" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
|
||||||
|
<text x="848" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:push</text>
|
||||||
|
<text x="848" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">合并 + 推送</text>
|
||||||
|
<text x="848" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">删已并分支</text>
|
||||||
|
<text x="848" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">只认 spec/ 前缀分支</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="188" y1="132" x2="218" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="218,132 209,127.5 209,136.5" fill="#6b6e78"/>
|
||||||
|
<line x1="372" y1="132" x2="402" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="402,132 393,127.5 393,136.5" fill="#6b6e78"/>
|
||||||
|
<line x1="556" y1="132" x2="586" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="586,132 577,127.5 577,136.5" fill="#6b6e78"/>
|
||||||
|
<line x1="740" y1="132" x2="770" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
|
||||||
|
<polygon points="770,132 761,127.5 761,136.5" fill="#6b6e78"/>
|
||||||
|
|
||||||
|
<!-- 恢复车道 -->
|
||||||
|
<rect x="36" y="228" width="888" height="76" rx="10" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.2"/>
|
||||||
|
<text x="480" y="258" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">中断恢复是常态设计</text>
|
||||||
|
<text x="480" y="284" text-anchor="middle" font-size="12.5" fill="#33343a">任意时刻重跑 /spec:run,从 tasks.md 勾选状态续跑;/spec:autorun 可续自动链(默认 5 轮上限,--max-rounds 可调)</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -4,6 +4,8 @@ pubDate: 2026-03-31
|
|||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags: [read]
|
||||||
mood: '#b45309'
|
mood: '#b45309'
|
||||||
|
bookAuthor: '萨缪尔·贝克特'
|
||||||
|
description: '无望之待里,陪伴是囚笼唯一的呼吸缝隙。'
|
||||||
---
|
---
|
||||||
|
|
||||||
### 谁是戈多?为何等待?
|
### 谁是戈多?为何等待?
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: "排版能力样例"
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: tech
|
||||||
|
tags: [test, hidden]
|
||||||
|
# 排版/斜体/白名单测试样张(fix3 车道):hidden 不列出,构建与 E2E 验证用
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
本篇验证三件事:斜体行内渲染、受限排版类白名单、图注类。
|
||||||
|
|
||||||
|
## 斜体(应为行内,不换行不居中)
|
||||||
|
|
||||||
|
这一句里有 *行内斜体强调* 夹在文字中间,它必须与正文同处一行。
|
||||||
|
|
||||||
|
*这一整段是斜体独占段,它不该变成块级居中。*
|
||||||
|
|
||||||
|
## 受限排版类(白名单)
|
||||||
|
|
||||||
|
<span class="ink-muted">这一段是 ink-muted 灰字。</span>
|
||||||
|
<span class="font-kai">这一段是霞鹜文楷字族。</span>
|
||||||
|
<span class="font-mono">这一段是等宽字族 mono。</span>
|
||||||
|
|
||||||
|
<p class="drop-cap">首字下沉段落:秋天的海面像一块被风抚平的铁皮,落日把它的边缘烧红了一圈。</p>
|
||||||
|
|
||||||
|
<p class="quote-line">所谓暮色,就是光慢慢地承认自己要走。</p>
|
||||||
|
|
||||||
|
## 白名单剔除(非法类与内联样式必须被剥离)
|
||||||
|
|
||||||
|
<span class="evil-class ink-2" style="color:#ff0000" onclick="alert(1)">这一段同时带了非法类、内联颜色与 onclick,构建后应只剩 ink-2 且 style/onclick 消失。</span>
|
||||||
|
|
||||||
|
## 图注(类驱动)
|
||||||
|
|
||||||
|
<p class="fig-cap">这是显式图注样式(fig-cap),居中灰字。</p>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: "字体族样例"
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: reading
|
||||||
|
tags: [test, hidden]
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
字体测试样张(hidden):验证系统回退字族的渲染。
|
||||||
|
|
||||||
|
<p class="font-song">宋体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="font-kai-sys">楷体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="font-hei-sys">黑体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="font-fangsong">仿宋族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="ink-warm">暖调正文:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="ink-cool">冷调正文:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="ink-faint">最淡可读档:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="ink-accent-soft">弱强调:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="c-blue">蓝色标准:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
<p class="c-yellow">黄色标准(已压暗):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
<p class="c-red-soft">红色 soft:秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="font-huiwen">汇文明朝体(OFL,切片自托管):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
|
|
||||||
|
<p class="font-kuaile">站酷快乐体(OFL,艺术字):秋天的海面像一块被风抚平的铁皮。</p>
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: "海风中失落的血色馈赠"
|
title: 海风中失落的血色馈赠
|
||||||
pubDate: 2026-03-25
|
pubDate: 2026-03-25
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags:
|
||||||
|
- read
|
||||||
mood: '#0f766e'
|
mood: '#0f766e'
|
||||||
|
bookAuthor: 阿利斯泰尔·麦克劳德
|
||||||
|
description: 秋是萧瑟之感:父亲、老马与少年,在失落后各自直面现实。
|
||||||
|
cover: ./assets/海风中失落的血色馈赠.png
|
||||||
|
hiddenTag: false
|
||||||
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
### 秋-斯科特之秋、父亲之秋、少年之秋(冬的预演)
|
### 秋-斯科特之秋、父亲之秋、少年之秋(冬的预演)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: "链接卡片样例"
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: tech
|
||||||
|
tags: [test, hidden]
|
||||||
|
# 链接卡片(OG 预览)测试样张:hidden 不列出,构建验证用;保留为活样例
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
本篇验证链接卡片:独立成段的裸链接转 OG 预览卡,内联链接保持普通样式。
|
||||||
|
|
||||||
|
## 正例:有 OG 的站点
|
||||||
|
|
||||||
|
https://wait4x.online/
|
||||||
|
|
||||||
|
https://astro.build/
|
||||||
|
|
||||||
|
## 反例:404 与无 OG 站点(应回落普通链接,构建不报错)
|
||||||
|
|
||||||
|
https://wait4x.online/definitely-not-a-real-page-404/
|
||||||
|
|
||||||
|
https://example.com/
|
||||||
|
|
||||||
|
## 内联链接保持普通样式(不应变卡)
|
||||||
|
|
||||||
|
这里有一个内联链接 [Astro 官网](https://astro.build/) 夹在句子中间,它必须保持普通链接样式,不能整篇变卡片墙。
|
||||||
|
|
||||||
|
## 卡片首行文字(markdown 链接形态同样触发)
|
||||||
|
|
||||||
|
[wait4x.online 首页](https://wait4x.online/)
|
||||||
@@ -14,7 +14,7 @@ draft: false
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
*图注写法:斜体单独成段,photos 目录样片副本(1600×2400 竖幅)。*
|
<p class="fig-cap">图注新写法:fig-cap 类,photos 目录样片副本(1600×2400 竖幅)。</p>
|
||||||
|
|
||||||
## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成)
|
## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
---
|
---
|
||||||
title: "面纱"
|
title: 面纱
|
||||||
pubDate: 2026-03-09
|
pubDate: 2026-03-09
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags:
|
||||||
|
- read
|
||||||
|
pinned: true
|
||||||
mood: '#7c3aed'
|
mood: '#7c3aed'
|
||||||
|
bookAuthor: 威廉·萨默塞特·毛姆
|
||||||
|
description: 面纱之下:理想的殉道者与真实的幸存者。
|
||||||
|
cover: ./assets/面纱.png
|
||||||
|
hiddenTag: false
|
||||||
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
****面纱之下:理想的殉道者和真实的幸存者****
|
**_面纱之下:理想的殉道者和真实的幸存者_**
|
||||||
|
|
||||||
### 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
### 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
||||||
|
|
||||||
这是译者在后记中写下的一句话。很好印证了《面纱》这本书或者说这个题目所蕴含的真相。人们活在面纱之下,以此遮蔽他人的窥探,却也因此困于自己的面纱之下。
|
这是译者在后记中写下的一句话。很好印证了《面纱》这本书或者说这个题目所蕴含的真相。人们活在面纱之下,以此遮蔽他人的窥探,却也因此困于自己的面纱之下。
|
||||||
|
|
||||||
见沃尔特像是见最温柔的爱和最悲痛的爱情,以最炽热的心和最真挚的感情去爱,爱上了一个不爱自己的人,最后在爱中将自己陷入漩涡🌀,不得解脱。他富有智慧,又富有对这个世界和社会以及人性最明锐的洞察力,他了解世界的苦难,了解社会的种种表象以及深层次的真相,了解人的“伪装”背后的真实人性。他像是个圣洁的存在,却同样又有着拙劣和“恶毒”的想法,以至于至死都未能从感情漩涡中游出。在我看来,沃尔特对于凯蒂的爱是真切的,对于背叛的恨也是真切的,但从始至终爱始终贯穿永远。在去湄潭府之前,他是冷静的旁观者,他是理性的化身,他有真对于真理的精准见解,有着自己衡量一切的道德标准和不可被践踏的信仰,这是源于理性的不可被磨灭的印记。在感知到背叛,并带着赴死的姿态奔赴湄潭府的过程中,他被感情所裹挟,他既爱又恨,恨凯蒂的背叛,也恨自己爱上了一个与自己原则背道而驰的爱人,他从一开始想让凯蒂也走向死亡,到最后让她离开,看似对于婚姻和这段感情有了一点点解脱,可实际上到死他都未能跨过自己给自己筑起的情感高墙。他是矛盾的,他至死未能宽恕自己,也正如他对世界最后的留言所述:“死的是一条狗”。这不仅是对凯蒂的隐喻,更是他对自己灵魂的终极审判。他终于承认,那个满怀怨恨、试图以爱之名行毁灭之实的自己,才是那条被仇恨吞噬的‘疯狗’。沃尔特的一生,是用最锋利的理性去剖析世界,却唯独不敢直视自己内心那片荒芜的废墟。他死于霍乱,实则死于那层由傲慢与偏见织就的、永远无法揭下的面纱。
|
见沃尔特像是见最温柔的爱和最悲痛的爱情,以最炽热的心和最真挚的感情去爱,爱上了一个不爱自己的人,最后在爱中将自己陷入漩涡🌀,不得解脱。他富有智慧,又富有对这个世界和社会以及人性最明锐的洞察力,他了解世界的苦难,了解社会的种种表象以及深层次的真相,了解人的“伪装”背后的真实人性。他像是个圣洁的存在,却同样又有着拙劣和“恶毒”的想法,以至于至死都未能从感情漩涡中游出。在我看来,沃尔特对于凯蒂的爱是真切的,对于背叛的恨也是真切的,但从始至终爱始终贯穿永远。在去湄潭府之前,他是冷静的旁观者,他是理性的化身,他有着对于真理的精准见解,有着自己衡量一切的道德标准和不可被践踏的信仰,这是源于理性的不可被磨灭的印记。在感知到背叛,并带着赴死的姿态奔赴湄潭府的过程中,他被感情所裹挟,他既爱又恨,恨凯蒂的背叛,也恨自己爱上了一个与自己原则背道而驰的爱人,他从一开始想让凯蒂也走向死亡,到最后让她离开,看似对于婚姻和这段感情有了一点点解脱,可实际上到死他都未能跨过自己给自己筑起的情感高墙。他是矛盾的,他至死未能宽恕自己,也正如他对世界最后的留言所述:“死的是一条狗”。这不仅是对凯蒂的隐喻,更是他对自己灵魂的终极审判。他终于承认,那个满怀怨恨、试图以爱之名行毁灭之实的自己,才是那条被仇恨吞噬的‘疯狗’。沃尔特的一生,是用最锋利的理性去剖析世界,却唯独不敢直视自己内心那片荒芜的废墟。他死于霍乱,实则死于那层由傲慢与偏见织就的、永远无法揭下的面纱。
|
||||||
|
|
||||||
相比之下,凯蒂是幸运的,也最终得到了自由。在虚荣的面纱下,遮住的是世界的残酷和社会的复杂以及人性的不可知。一开始的虚荣心作祟,让她步入了和沃尔特的婚姻,却发现婚后的生活并非是自己所期望的那样,甚至从一开始她就清楚的知道自己并不爱沃尔特。于是,当她到了香港,并遇上查理的时候,她沦陷了,她在查理身上看见了自己所期待的伴侣所拥有的那些特性,幽默风趣,性感迷人,但是却忽视了人性。在去往湄潭府之前,她虚荣,她害怕,她怕沃尔特发现一切,她软弱,她想逃离这样的婚姻和查理相拥,但是她不敢付诸行动。当沃尔特推着她去面对一切的时候,当一切都败露的时候,她终究是看见了人性的恶,她见到了查理的无情和所谓的爱只是言语上的“美好”,她也真正的感到了恐惧,面对死亡的恐惧和面对沃尔特不可捉摸的想法的恐惧。在湄潭府之行的初期,她依旧活在对于过去的回忆之中,她害怕恐惧,她看不见未来。但被圣光照耀的庙堂始终矗立在彼岸,她向往那迷人的色彩,她想要去追求新的生活。当她跨出第一步并开始在修道院工作的时候,她的觉醒之路从此慢慢开始,她慢慢和自己和解,慢慢看淡背叛,慢慢看清查理,慢慢找寻着自己在生活中该有的样子,她开始慢慢摘掉自己的面纱,看清自己的虚伪和软弱,开始追寻道的意义,她感悟周围的一切,甚至想着能够将沃尔特从他的痛苦中将他拉出。然而直到沃尔特死去,他们都没能和解,她愧对沃尔特,她看到了自己的轻贱,可是她自始至终都跨不过沃尔特竖起的高墙,她在墙外,沃尔特在墙内,各自在各自的面纱下痛苦的活着。沃尔特的死,是沃尔特的一种“解脱”,也是凯蒂的一种解脱。
|
相比之下,凯蒂是幸运的,也最终得到了自由。在虚荣的面纱下,遮住的是世界的残酷和社会的复杂以及人性的不可知。一开始的虚荣心作祟,让她步入了和沃尔特的婚姻,却发现婚后的生活并非是自己所期望的那样,甚至从一开始她就清楚的知道自己并不爱沃尔特。于是,当她到了香港,并遇上查理的时候,她沦陷了,她在查理身上看见了自己所期待的伴侣所拥有的那些特性,幽默风趣,性感迷人,但是却忽视了人性。在去往湄潭府之前,她虚荣,她害怕,她怕沃尔特发现一切,她软弱,她想逃离这样的婚姻和查理相拥,但是她不敢付诸行动。当沃尔特推着她去面对一切的时候,当一切都败露的时候,她终究是看见了人性的恶,她见到了查理的无情和所谓的爱只是言语上的“美好”,她也真正的感到了恐惧,面对死亡的恐惧和面对沃尔特不可捉摸的想法的恐惧。在湄潭府之行的初期,她依旧活在对于过去的回忆之中,她害怕恐惧,她看不见未来。但被圣光照耀的庙堂始终矗立在彼岸,她向往那迷人的色彩,她想要去追求新的生活。当她跨出第一步并开始在修道院工作的时候,她的觉醒之路从此慢慢开始,她慢慢和自己和解,慢慢看淡背叛,慢慢看清查理,慢慢找寻着自己在生活中该有的样子,她开始慢慢摘掉自己的面纱,看清自己的虚伪和软弱,开始追寻道的意义,她感悟周围的一切,甚至想着能够将沃尔特从他的痛苦中将他拉出。然而直到沃尔特死去,他们都没能和解,她愧对沃尔特,她看到了自己的轻贱,可是她自始至终都跨不过沃尔特竖起的高墙,她在墙外,沃尔特在墙内,各自在各自的面纱下痛苦的活着。沃尔特的死,是沃尔特的一种“解脱”,也是凯蒂的一种解脱。
|
||||||
|
|
||||||
@@ -20,7 +27,7 @@ mood: '#7c3aed'
|
|||||||
>
|
>
|
||||||
> 所谓《面纱》,遮住的不仅是真相,更是我们面对真实自我的勇气。沃尔特缺乏这份勇气去接纳不完美的爱与人性,所以他困死其中;凯蒂在生死关头被迫拥有了这份勇气,所以她获得了自由。
|
> 所谓《面纱》,遮住的不仅是真相,更是我们面对真实自我的勇气。沃尔特缺乏这份勇气去接纳不完美的爱与人性,所以他困死其中;凯蒂在生死关头被迫拥有了这份勇气,所以她获得了自由。
|
||||||
>
|
>
|
||||||
> 故事的最后,死去的人得到了安息,活着的人带着记忆与教训,继续前行。这或许就是毛姆想告诉我们的:****人生没有完美的结局,只有在认清生活的残酷真相后,依然选择热爱并真实地活下去,才是唯一的救赎。****
|
> 故事的最后,死去的人得到了安息,活着的人带着记忆与教训,继续前行。这或许就是毛姆想告诉我们的:**_人生没有完美的结局,只有在认清生活的残酷真相后,依然选择热爱并真实地活下去,才是唯一的救赎。_**
|
||||||
|
|
||||||
从背叛到自我的救赎,凯蒂在远离家乡的异国,没能逃脱欲望作祟,再次回到香港之后又一次沦陷在了查理的怀抱中,但此刻的她早已不是去湄潭府的之前的自己,她的思想早已骤变。她厌恶自己肉体对自己灵魂的出卖,她选择远离这个是非之地,她选择去重新追求自由,亦如从湄潭府离开一样,她再一次走上了追寻自由的路上。她回到故土,见母亲离世,见妹妹怀有身孕,见父亲终于脱离过去的自己,她亦想脱离过去的自己,拥抱一个崭新的未来。活在面纱之下的她和她的父亲,在故事的结局,两人袒露心声,一同前往新的地方开始新的生活,去活出新的自己。
|
从背叛到自我的救赎,凯蒂在远离家乡的异国,没能逃脱欲望作祟,再次回到香港之后又一次沦陷在了查理的怀抱中,但此刻的她早已不是去湄潭府的之前的自己,她的思想早已骤变。她厌恶自己肉体对自己灵魂的出卖,她选择远离这个是非之地,她选择去重新追求自由,亦如从湄潭府离开一样,她再一次走上了追寻自由的路上。她回到故土,见母亲离世,见妹妹怀有身孕,见父亲终于脱离过去的自己,她亦想脱离过去的自己,拥抱一个崭新的未来。活在面纱之下的她和她的父亲,在故事的结局,两人袒露心声,一同前往新的地方开始新的生活,去活出新的自己。
|
||||||
|
|
||||||
@@ -30,7 +37,7 @@ mood: '#7c3aed'
|
|||||||
|
|
||||||
沃尔特死在了他的‘正确’里,而凯蒂活在了她的‘真实’中。沃尔特的故事是带有悲惨的个人主义的牺牲,凯蒂的故事是带有幸运的个人主义的新生。沃尔特自觉没有希望,在困顿的泥潭中深陷,他没有勇气去面对新的生活和新的世界,他将自己紧紧隐藏在自己的面纱之后,他活在自己为自己构建的信念世界之中,为自己的信念,"悲壮”赴死。凯蒂活在纷繁的社会中,从小我见大我,从假我见真我,从困局中得自由。她一步一步从虚伪的面纱下得见人性、社会冷暖以及新世界的希望,湄潭府之前的生活让她自省,湄潭府之行让她感知到一切,在死亡和黑暗中得到感悟和觉醒,得见光明,再次回到香港,欲望再次作祟,这一次她没有沉沦于虚妄的现实,她终于携手勇气做出了自己的决定,踏上了"离开”的归程。
|
沃尔特死在了他的‘正确’里,而凯蒂活在了她的‘真实’中。沃尔特的故事是带有悲惨的个人主义的牺牲,凯蒂的故事是带有幸运的个人主义的新生。沃尔特自觉没有希望,在困顿的泥潭中深陷,他没有勇气去面对新的生活和新的世界,他将自己紧紧隐藏在自己的面纱之后,他活在自己为自己构建的信念世界之中,为自己的信念,"悲壮”赴死。凯蒂活在纷繁的社会中,从小我见大我,从假我见真我,从困局中得自由。她一步一步从虚伪的面纱下得见人性、社会冷暖以及新世界的希望,湄潭府之前的生活让她自省,湄潭府之行让她感知到一切,在死亡和黑暗中得到感悟和觉醒,得见光明,再次回到香港,欲望再次作祟,这一次她没有沉沦于虚妄的现实,她终于携手勇气做出了自己的决定,踏上了"离开”的归程。
|
||||||
|
|
||||||
故事的最后,面纱已落。凯蒂带着伤痕,牵着父亲的手,走向巴哈马的阳光。那里没有豪门的虚妄,没有激情的幻象,只有平凡而坚实的生活。这,或许就是毛姆留给所有在虚荣与迷茫中挣扎的人们,最温柔的答案:****唯有真实,可得自由。****
|
故事的最后,面纱已落。凯蒂带着伤痕,牵着父亲的手,走向巴哈马的阳光。那里没有豪门的虚妄,没有激情的幻象,只有平凡而坚实的生活。这,或许就是毛姆留给所有在虚荣与迷茫中挣扎的人们,最温柔的答案:**_唯有真实,可得自由。_**
|
||||||
|
|
||||||
在《面纱》的故事中,沃尔特是极具“英雄主义”的个人,为自己固有的信仰慷然赴死;凯蒂是在纷杂的社会中逐步成长,在欲望和虚荣的交叉间度过早年,湄潭府一行,在黑暗中见光明,在死亡中见希望,在异国见真我,在“新世界”得自由。
|
在《面纱》的故事中,沃尔特是极具“英雄主义”的个人,为自己固有的信仰慷然赴死;凯蒂是在纷杂的社会中逐步成长,在欲望和虚荣的交叉间度过早年,湄潭府一行,在黑暗中见光明,在死亡中见希望,在异国见真我,在“新世界”得自由。
|
||||||
|
|
||||||
@@ -39,51 +46,28 @@ mood: '#7c3aed'
|
|||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
- 我不了解你,我一点儿都不了解你。
|
- 我不了解你,我一点儿都不了解你。
|
||||||
|
|
||||||
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
|
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
|
||||||
|
|
||||||
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
|
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
|
||||||
|
|
||||||
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
|
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
|
||||||
|
|
||||||
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
|
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
|
||||||
|
|
||||||
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
|
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
|
||||||
|
|
||||||
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
|
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
|
||||||
|
|
||||||
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
|
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
|
||||||
|
|
||||||
- 安宁只存在于人的灵魂中。
|
- 安宁只存在于人的灵魂中。
|
||||||
|
|
||||||
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
|
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
|
||||||
|
|
||||||
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
|
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
|
||||||
|
|
||||||
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
|
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
|
||||||
|
|
||||||
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
|
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
|
||||||
|
|
||||||
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
|
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
|
||||||
|
|
||||||
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
|
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
|
||||||
|
|
||||||
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
|
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
|
||||||
|
|
||||||
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
|
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
|
||||||
|
|
||||||
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
|
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
|
||||||
|
|
||||||
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
|
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
|
||||||
|
|
||||||
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
|
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
|
||||||
|
|
||||||
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
|
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
|
||||||
|
|
||||||
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
|
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
|
||||||
|
|
||||||
- 我有希望,也有勇气。
|
- 我有希望,也有勇气。
|
||||||
|
|
||||||
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: "泥潭"
|
title: 泥潭
|
||||||
pubDate: 2026-02-09
|
pubDate: 2026-02-09
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags:
|
||||||
mood: '#166534'
|
- read
|
||||||
|
pinned: false
|
||||||
|
mood: '#686868'
|
||||||
|
bookAuthor: 刘楚昕
|
||||||
|
description: 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
||||||
|
cover: ./assets/泥潭.png
|
||||||
|
hiddenTag: false
|
||||||
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: "pi-mesh 全家桶详解:单机多智能体协同的四件套"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [工程实践, AI 协作]
|
||||||
|
---
|
||||||
|
|
||||||
|
[pi](https://github.com/earendil-works/pi) 是我们日常用的编码智能体宿主。单开一个终端时它是个安静的助手;但同时开三四个终端、每个里面一个 agent 抢同一个仓库时,问题就来了:谁在线?谁在改哪个文件?消息发出去了对方到底看没看见?半夜跑着的构建谁有权停?
|
||||||
|
|
||||||
|
围绕 [pi-mesh](https://github.com/cgarrot/pi-mesh) 长出来的一族小工具把这层协同做成了基建。它们各管一摊,咬合成套:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 一、pi-mesh:通信骨干
|
||||||
|
|
||||||
|
pi-mesh 是一条本机总线:一个脱离会话存活的 broker(NDJSON 帧走 unix socket),每个 pi 终端以别名成为 peer,加入房间(room)。消息 < 50ms 到达,零运行时依赖,Node ≥ 20。
|
||||||
|
|
||||||
|
它最值得称道的是**诚实状态模型**:`delivered`(写进了对方套接字)≠ `read`(注入了对方会话)≠ `answered`(拿到了显式回复)——三者永不混淆,没有一个是"完成"的意思。配套的**持久台账是 hash-only 的**:消息正文永不落盘,重启后能审计"谁在何时给谁发过什么",但看不到内容。这个自我约束后来成了整个工具族的设计公理:**mesh 只做路由器,永不做数据库。**
|
||||||
|
|
||||||
|
协同刚需有三个都在骨干里:
|
||||||
|
|
||||||
|
- **`mesh_reserve` 路径预约**:改文件前先预约,其他 peer 的 `edit`/`write` 工具在该路径上直接被拦——并发编辑靠机制防,不靠喊话。
|
||||||
|
- **`mesh_claim` 任务租约**:单写者锁,带 `epoch:fence`(broker 重启即作废旧围栏);提交信息里写 `Mesh-Lease: task#leaseId#epoch:fence`,配套 commit-msg 钩子直接拒绝过期围栏的提交。
|
||||||
|
- **能力路由 `cap:`**:`mesh_send({ "to": "cap:deploy", ... })` 把任务发给任意一个空闲且自报该能力的 peer,而不是等忙着的队友。消息优先级三档:normal 排队、urgent 插队、force 先中止对方当前回合再送达(force 属管控类,必须带 reason)。
|
||||||
|
|
||||||
|
另有 `mesh_wait_all` 做群裁(一次收回“谁答了、谁没答”的名单)、`mesh_status` 看 peer 忙闲(● 干活 / ○ 空闲 / ✕ 卡死)、`pimesh` CLI 支持无会话接入(`pimesh attach` 能收养死掉会话的身份)。跨机部署也支持:一台起 broker,其余机器做客户端。
|
||||||
|
|
||||||
|
诚实的颗粒度细到退出码:CLI 里 3 表示 `expired`——晚到的答复仍会送达,只是你等起见超时了;5 表示租约已过期,`commit-msg` 钩子据此把提交拒之门外。甚至 ESC 清空消息队列这种宿主边界也考虑到了:批量注入若撞上回合中止,broker 会在该回合结束后经中止安全通道重投(有界重试),状态页能看到“中止清除恢复重投”计数。
|
||||||
|
|
||||||
|
## 二、pi-board:团队记忆板
|
||||||
|
|
||||||
|
消息是易逝的,台账是 hash-only 的,那"我们学到了什么"存哪?[pi-board](https://github.com/wait4xx/pi-board) 补上这层。五种类型化条目:
|
||||||
|
|
||||||
|
- **OBSERVED**——看到了什么(不承诺)
|
||||||
|
- **FACT**——验证过的事实
|
||||||
|
- **FAIL**——一条被证伪的路(价值最高:全队不必再烧第二次预算)
|
||||||
|
- **CLAIM**——认领声明,与 `mesh_claim` 租约配对(板只记录,从不执行)
|
||||||
|
- **PATCH_SUMMARY**——落了什么改动
|
||||||
|
|
||||||
|
设计上处处是“有界与诚实”:append-only 本地存储、refuse-not-evict(超限拒绝写入而非静默淘汰)、条目默认短小、长文 `detail` 只能按 id 展开、布尔检索(`,` = OR,`&` = AND)覆盖全史而 `board_read` 只看近期窗口;检索无果时返回诚实的空,绝不猜一条充数。写入以普通广播帧发进房间,turn 边界有新 FAIL 摘要行;房间角色 member / observer 沿用 mesh 语义,只读 peer 永远不会在重连后被误升为写者。它以普通 peer 身份入房——**存储全在板自己家里,broker 里零持久化**。附带的 `piboard` CLI 五个动词一把梭:join(member / observer)/ watch / write / read / grep,一动词一动作,无会话的终端也能进房只读围观。这套类型学与"小条目+按需展开+近期记忆/全史检索"的分工直接来自学术论文(血统故事见[下一篇](/archives/agensh/))。
|
||||||
|
|
||||||
|
## 三、pi-jobd:后台作业守护
|
||||||
|
|
||||||
|
`bash` 会阻塞当前回合,`/reload` 会杀进程。[pi-jobd](https://github.com/cgarrot/pi-jobd) 把长命令放进独立进程组,由 per-user 守护监管:~150ms 返回,作业存活于会话之外。什么时候用它?三条判据:命令预计超过 60-90 秒;你必须边跑边干别的;或者它得扛过 `/reload` 和会话结束——以及"别的 agent 应该能停它"这一条:`mesh_job_control` 让任一授权 peer 远程 stop/pause/resume 别人的后台作业。
|
||||||
|
|
||||||
|
状态机同样诚实:`running / paused / stopped / exited / failed`,外加一个 `lost`——守护重启后无法证明 pid 归属时,宁可拒绝 stop 也绝不信号一个来路不明的进程。这个包还有个巧思:装包不够,模型还得**知道什么时候用**——它靠 prompt 快照、七个工具上的决策守则和一份按需加载的 skill 三层把使用时机喂给模型。
|
||||||
|
|
||||||
|
## 四、pi-subagents:worker 控制
|
||||||
|
|
||||||
|
[pi-subagents](https://github.com/nicobailon/pi-subagents) 管的是进程内子代理:前台子代理流式跑在对话里,后台子代理活在独立的 runner 进程中随时可查。装完即用,不需要学斜杠命令——直接说"用 reviewer 审这个 diff"就行。它预置了 scout(侦察)、reviewer(审查)这类常用角色,也支持保存的工作流和并行审查。与 mesh 的接口是 `mesh_run_control`:远程 stop 或 steer 一个 peer 的 RPC worker。
|
||||||
|
|
||||||
|
## 五、四件套的一次合奏
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 六、上手五分钟
|
||||||
|
|
||||||
|
安装各一行:mesh 与 subagents 走 pi 包管理——`pi install npm:pi-mesh-extension`、`pi install npm:pi-subagents`;pi-board 还没发 npm,源码三步(clone、`npm ci`、`npm run build`),且可 CLI 独立用(不需要 mesh 也能读写本地板);pi-jobd 同样源码构建,`pijob run / list / log / stop` 开箱即用。装完在同一目录开两个 pi 会话,就是两个 peer,`mesh_status` 互相可见——协同基建的“零配置”不是口号。
|
||||||
|
|
||||||
|
对话走 mesh,记忆上 board,长活交 jobd,排他靠租约——四个包各司其职,没有任何一个越界。这个博客的重设计期就是这套东西的实战场:两个 mesh 终端(op-gitea、op-artalk)与主线并行,一天 19 连发;发布链中断的修复窗口里,主线预约了 build.sh 紧急处置,另一边赶去补刀的编辑被预约机制正确拦下——撞车靠规则防住,不靠谁手快。
|
||||||
|
|
||||||
|
四件套加起来没有一个数据库、没有一个中心大脑,却把"谁在线、谁在干啥、学到过啥、长活谁看着"四件事全部钉死。小工具、硬边界、诚实状态——这套哲学比任何一个具体包都值得抄。
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: "赎罪"
|
title: 赎罪
|
||||||
pubDate: 2026-02-26
|
pubDate: 2026-02-26
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags:
|
||||||
mood: '#9f1239'
|
- read
|
||||||
|
pinned: false
|
||||||
|
mood: '#bf002f'
|
||||||
|
bookAuthor: 伊恩·麦克尤恩
|
||||||
|
description: 亲爱的塞西莉亚,我们缘分未尽
|
||||||
|
cover: ./assets/赎罪.png
|
||||||
|
hiddenTag: false
|
||||||
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
### 你要回来,我会等你。
|
### 你要回来,我会等你。
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: "Spec Harness 详解:把长程开发装进可验收的任务包"
|
||||||
|
pubDate: 2026-11-01
|
||||||
|
category: tech
|
||||||
|
tags: [工程实践, AI 协作]
|
||||||
|
---
|
||||||
|
|
||||||
|
AI 编程智能体最不缺的是写代码的带宽,最缺的是记忆。一个跨了好几轮会话的任务,改到一半的方案、已经否决的路线、验收口径,散落在聊天记录里就等于丢了——换一个会话、重启一次终端,上下文归零。[Spec Harness](https://github.com/MicTx/spec-harness) 解决的就是这个问题:它是一个面向 AI 编程智能体的任务包工作流工具,把长程改动保存为**可恢复、可验证的 Development Record**,用范围、证据和 Git 门禁管住全程。它不提供模型,也不替你做产品决策。
|
||||||
|
|
||||||
|
一句话概括它的世界观:**当一个编码任务的寿命超过一段对话,缺的不是更聪明的模型,而是一份大家(包括未来的你)都能读的合同。**
|
||||||
|
|
||||||
|
## 一、核心抽象:任务包
|
||||||
|
|
||||||
|
所有工作装进 `.spec/specs/YYYY-MM-DD_slug/` 目录,核心是两个文件:`spec.md` 写范围、假设与取舍(还有一节很特别的"已证伪路径",后面细说),`tasks.md` 写任务清单。收尾后整包归档到 `.spec/specs/archive/`,归档目录本身就是项目的工程日志。
|
||||||
|
|
||||||
|
每个任务必须带两行合同——**boundary**(这个任务能动哪些文件)和 **verify**(怎么证明做完了)。verify 是只读断言,跑出退出码 0 才许勾选。拿这个博客真实归档的 `2026-10-26_blog-5f` 包里 T3 任务举例:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
boundary 划定"能动什么",verify 划定"怎么证明做完"——两行齐备,缺一不勾。这不是形式主义:多 agent 并行时,boundary 就是车道之间的护栏(我们的实践里,全局样式文件只许主线动,车道要新变量走申请);verify 则把"看起来好了"和"证明做完了"区分开,口头"过了"永远不算数。
|
||||||
|
|
||||||
|
`spec.md` 里最有味道的一节是**已证伪路径**:被否决的方案连否定理由一起入库。我们的 blog-5f 包里记着三条——首屏 banner 化(视觉裁定否决)、热力图换绿(破坏灰阶骨架)、标题 27px 收到 22px(层级锚点,要轻减字重不减号)。下次谁想再试一遍,先读档案,少烧一轮预算。
|
||||||
|
|
||||||
|
## 二、阶段流水线:五个核心命令与它们把守的门
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
五个核心命令串起一条可验证链:**目标 → 范围 → 任务 → 验证 → 归档 → 提交 → 推送**。每个阶段之间都有门把守:`spec.md` 有模板占位符就拒绝进入 run;`done` 要求每个任务 boundary/verify 齐备、验收面全绿且至少有一条非占位证据(脚本输出、测试结果);`push` 只合并 `spec/` 前缀的集成分支,文件改动与提交永远分开做,禁用 `--no-verify`。
|
||||||
|
|
||||||
|
被阻塞的任务会打 `!` 标记上浮给用户,绝不静默跳过;复核轮(check)发现的可执行问题必须当轮写回任务包并修复,不允许把"待办"当交付物甩给用户。
|
||||||
|
|
||||||
|
**中断恢复是常态设计**——这是它对“任务寿命超过对话”最直接的回答:任意时刻重跑 `/spec:run`,从 `tasks.md` 的勾选状态续跑,不依赖任何对话记忆。更进一步,`/spec:autoplan` 做交互式规划(澄清目标后过框架、细节、复核三轮,规划就绪即停,不自动开工),`/spec:autorun` 按规划逐包执行,默认最多 5 轮自动链(`--max-rounds` 可调),暂停后从同一步恢复;一键式入口 `/spec:goal` 则把规划、执行、复核、归档、提交一次跑完。
|
||||||
|
|
||||||
|
命令面共 12 个阶段命令:core 档装五个核心命令,full 档补齐 goal / autoplan / autorun / update / status / doctor / organize 七个扩展件——`doctor` 做环境自检与修复,`organize` 做结构审计。还有一个容易被忽略的细节:安装只是放文件,不会主动逐任务评估;想让它在你动手前主动问一句“这事要不要开包”,需要显式接线一次 `install_activation_hooks.py --mode on`。判据看交付形状——独立验收面、实际实施者、可恢复的在途任务;小修、只读调查、一次性实验都直接做。它防的是“忘了评估”,不防刻意瞒报——文档里对此毫不讳言。
|
||||||
|
|
||||||
|
## 三、多 agent 编排:一个编排者,有边界的帮工
|
||||||
|
|
||||||
|
Spec Harness 对多 agent 的态度很克制:**主会话是唯一的编排者**,路由、集成、验收和 `done`/`push` 门禁永远不出主线。旁支工作(探索、构建、审查这类上下文无关的活)可以委托,但要先过机器路由决策(`scripts/route_decision.py`)并写下任务合同:目标、范围、排除区、产出、验证方式五项齐备才许派。它甚至为"跨会话交接"准备了可选的 `handoff.md`——生成式快照头加 append-only 条目日志,单人、多人、agent、集群四种协作模式各有一套必填小节。
|
||||||
|
|
||||||
|
扩展位(slots)机制则把"别的团队流程"插进来:`team-loop` 管团队循环、`workflow-runner` 管工作流扇出,各自带 README 协议和测试。源码库里还附了 orchestrator/planner/reviewer/confirmer 四个编排代理定义。
|
||||||
|
|
||||||
|
## 四、安装:一个安装器,十余个宿主
|
||||||
|
|
||||||
|
Spec Harness 是 Python 3.9+ 的标准库实现(PolyForm Noncommercial 许可),安装就三步:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/MicTx/spec-harness.git /tmp/spec-harness
|
||||||
|
cd /tmp/spec-harness
|
||||||
|
git checkout v0.22.2
|
||||||
|
bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
安装器支持十余个宿主:Claude Code、Codex、Gemini CLI、Grok、OpenCode、OpenClaw、Hermes、Pi、ZCode、DeepSeek Harness、Devin、Cursor……不指定时自动探测已装的宿主。安装档位分 core / full:core 只装五个核心阶段命令加 Git 钩子;full 补齐全部 12 个阶段命令和 slots、agents 扩展组件。我们在 Pi 上用 `INSTALL_HOSTS=pi bash install.sh`,装完 `/spec` 系列命令直接可用。
|
||||||
|
|
||||||
|
## 五、跑在一个真实博客上
|
||||||
|
|
||||||
|
这个博客就是它的用户。归档目录里躺着从裸 clone 构建改造、移动端布局修复到 SEO 解封的一串任务包:blog-5f 一个包八个任务、三条车道(两条 mesh 终端加主线)并行,每个任务的 verify 都是能在命令行跑出退出码的断言,页面类任务统一挂 E2E——`E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs`,34 项全过才算数。包与包接力,一天 19 个提交全绿落地。
|
||||||
|
|
||||||
|
门禁也真的拦过烂账:一次一个全 manual、从未执行的任务包混进了 `.spec/specs/`,check 门禁直接判 blocked,连带拦下所有携带归档路径的提交——直到把它移出。门禁防的不只是烂尾,还有“看起来有规划”的空包。
|
||||||
|
|
||||||
|
工具会迭代,但这套思路不会过期:**把"要干什么"写成合同,把"做完了"写成证据,把过程归档成日志。** 无论协作方是人还是 agent,这三件事都是长程工程的地基。
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: 写作模板:这篇文章本身就是模板
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: tech
|
||||||
|
tags:
|
||||||
|
- 写作
|
||||||
|
- 模板
|
||||||
|
pinned: true
|
||||||
|
description: 一篇文章演示全站可用的排版能力:标题层级、彩色强调、字族、引用、代码窗、表格、图、链接卡、折叠。复制它开始写新文。
|
||||||
|
hiddenTag: true
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
这篇不是教程,是**可复制的模板**。下面每一节都在真跑——你看到的效果,就是 Markdown 源码能产出的效果。写新文章时,把本文件复制一份、改个 slug、删掉不需要的段落即可。
|
||||||
|
|
||||||
|
它同时是**置顶功能**的活样本:`frontmatter` 里的 `pinned: true` 会让它排在首页、归档与书架的最前面,而日期、URL、订阅源里的时间都不变。
|
||||||
|
|
||||||
|
## 一、标题层级与段落
|
||||||
|
|
||||||
|
正文用 `##` 作一级节标题,`###` 作二级。**不要用 `#`**——文章标题由 frontmatter 的 `title` 渲染,正文再写 `#` 会出现两个一级标题,屏幕阅读器与搜索摘要都会困惑。
|
||||||
|
|
||||||
|
段落之间空一行即可。行内可以**加粗**、*斜体*、~~删除线~~、`行内代码`,也可以放[普通链接](https://astro.build)。
|
||||||
|
|
||||||
|
## 二、彩色强调:想用多少用多少
|
||||||
|
|
||||||
|
颜色现在**没有次数限制**(2026-11-02 起取消了原来的「一页最多三处」)。唯一还守着的是外壳:导航、页脚、按钮、背景保持灰阶——外壳是气质,正文是表达。
|
||||||
|
|
||||||
|
<span class="c-blue">第一处:一个关键结论用蓝色提出。</span> 后面照常叙述。
|
||||||
|
|
||||||
|
<span class="c-red">第二处:需要警示的坑用红色。</span> 比如「改完 Markdown 插件必须清 `node_modules/.astro`,否则改动静默不生效」。
|
||||||
|
|
||||||
|
<span class="c-green">确认可行的路径用绿色。</span> 想让整段都是重点也不必克制,把它们都标出来即可。
|
||||||
|
|
||||||
|
可用色相共 8 组(blue / teal / green / yellow / orange / red / pink / purple),每组有标准与 `-soft` 两档;另有 4 个灰阶层级类(`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`)用于降调,不算彩色额度。
|
||||||
|
|
||||||
|
## 三、字族:换字体只作用于被包裹的文字
|
||||||
|
|
||||||
|
正文默认是无衬线。想给某句话换气质,用类包起来就行:
|
||||||
|
|
||||||
|
<span class="font-kai">这一句是楷体。适合题记、书评里引的原句,或者需要慢一点读的段落。</span>
|
||||||
|
|
||||||
|
<span class="font-huiwen">这一句是汇文明朝体。长段落用它会明显降低阅读速度,所以只用在短句上。</span>
|
||||||
|
|
||||||
|
可用字族共 6 族(`font-kai` 楷、`font-song` 宋、`font-hei-sys` 黑、`font-fangsong` 仿宋、`font-huiwen` 汇文明朝、`font-kuaile` 快乐体),另有 `font-art-kuaile` 艺术字标题类。**注意体积**:整页换字族会让访问者下载一份中文字体,正文默认字体不要轻易改。
|
||||||
|
|
||||||
|
### 三级标题长这样
|
||||||
|
|
||||||
|
比二级小一档、加粗、上下留白更紧。用于把一节拆成几个小话题。
|
||||||
|
|
||||||
|
## 四、引用与列表
|
||||||
|
|
||||||
|
> 引用块用于摘录别人的话。署名可以换成斜体放在最后。
|
||||||
|
>
|
||||||
|
> —— 一句虚构的署名
|
||||||
|
|
||||||
|
无序列表:
|
||||||
|
|
||||||
|
- 第一项
|
||||||
|
- 第二项,可以带 `行内代码`
|
||||||
|
- 第三项,可以嵌套:
|
||||||
|
- 子项一
|
||||||
|
- 子项二
|
||||||
|
|
||||||
|
有序列表适合步骤:
|
||||||
|
|
||||||
|
1. 复制模板
|
||||||
|
2. 改 frontmatter 的 slug 与标题
|
||||||
|
3. 删掉不需要的段落
|
||||||
|
|
||||||
|
## 五、代码块
|
||||||
|
|
||||||
|
四行及以上的代码块会带上灰阶窗条(三个点),短代码块不加——避免小片段看着像截图。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 排序契约:置顶优先 → 时间倒序 → id 兜底
|
||||||
|
export function sortPosts<T extends Post>(posts: T[]): T[] {
|
||||||
|
return [...posts].sort((a, b) => {
|
||||||
|
const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true);
|
||||||
|
return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
行内提到变量时用 `posts` 这种形式即可。
|
||||||
|
|
||||||
|
## 六、表格
|
||||||
|
|
||||||
|
| 能力 | 写法 | 注意 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 彩色强调 | `<span class="c-blue">` | 不限次数(外壳仍灰阶) |
|
||||||
|
| 换字族 | `<span class="font-kai">` | 整页换会拉字体体积 |
|
||||||
|
| 图片 | `` | 体积纪律见写作指南 |
|
||||||
|
| 视频 | frontmatter `video:` | 放 public/media,≤50MB |
|
||||||
|
| 链接卡片 | 裸链接单独成段 | 构建期抓 OG,失败回落普通链接 |
|
||||||
|
|
||||||
|
## 七、图片
|
||||||
|
|
||||||
|
图片用相对路径引用,挂在 `content/posts/assets/<slug>/` 下,点击可放大(矢量图不糊):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 八、链接卡片
|
||||||
|
|
||||||
|
一个裸链接单独成段,构建期会抓取 OG 预览生成卡片;抓不到就退回普通链接,不会坏版:
|
||||||
|
|
||||||
|
https://astro.build
|
||||||
|
|
||||||
|
## 九、折叠长内容
|
||||||
|
|
||||||
|
长附录、完整日志、大段配置可以折叠,零 JS,原生 `details`:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>展开看一个被折叠的长例子</summary>
|
||||||
|
|
||||||
|
折叠区里可以放任何东西——段落、列表、代码块。
|
||||||
|
|
||||||
|
- 适合放「按需查看」的细节
|
||||||
|
- 不适合放结论(读者可能永远不展开)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 十、分隔线之后收尾
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
收尾三句就够:这篇讲了什么、读者该带走什么、下一步去哪。模板不是要你写满十节,而是**在你需要表达时,能力都在手边**。
|
||||||
|
|
||||||
|
下一篇开始,复制本文件,删到只剩你要说的。
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# 关于页(后台「页面文案 → 关于页」可改)
|
||||||
|
# ── 一级:blocks 块式正文(按顺序渲染,可拖拽排序;某块 enabled: false 即隐藏)
|
||||||
|
# ── 二级:intro 多段 markdown(仅在 blocks 为空时使用)
|
||||||
|
# ── 三级:内置默认文案(两者都空时兜底,构建永不炸)
|
||||||
|
layout:
|
||||||
|
# 名片区(头像/名字/身份语/社交)开关:头像等数据事实源在「主页设置」
|
||||||
|
showIdCard: true
|
||||||
|
|
||||||
|
blocks:
|
||||||
|
- type: prose
|
||||||
|
width: narrow
|
||||||
|
body:
|
||||||
|
zh: |
|
||||||
|
<span class="font-huiwen c-red">写作者</span> · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
|
|
||||||
|
所有内容都有一个明确去处:
|
||||||
|
|
||||||
|
- 长文沉淀在文章与书架
|
||||||
|
- 短句与随手拍落在瞬间
|
||||||
|
- 影像收进图库,配一点 EXIF 与一句策展
|
||||||
|
|
||||||
|
本站本身即开源,构建与发布全链都在仓里。
|
||||||
|
en: |
|
||||||
|
<span class="font-huiwen c-red">Writer</span> · Reader · Shutter-presser, embracing open source.
|
||||||
|
|
||||||
|
This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** I pressed that land in the gallery.
|
||||||
|
|
||||||
|
Everything has a clear destination:
|
||||||
|
|
||||||
|
- Long-form writing goes to posts and the bookshelf
|
||||||
|
- Short lines and snapshots land in moments
|
||||||
|
- Images go to the gallery, with a bit of EXIF and one line of curation
|
||||||
|
|
||||||
|
This site is itself open source — the full build and deploy pipeline lives in the repo.
|
||||||
|
|
||||||
|
- type: tracks
|
||||||
|
title:
|
||||||
|
zh: 兴趣版图
|
||||||
|
en: Interests
|
||||||
|
items:
|
||||||
|
- icon: book
|
||||||
|
name:
|
||||||
|
zh: 阅读
|
||||||
|
en: Reading
|
||||||
|
desc:
|
||||||
|
zh: 文学与戏剧为主,读到哪写到哪。
|
||||||
|
en: Mostly literature and drama; I write as I read.
|
||||||
|
- icon: terminal
|
||||||
|
name:
|
||||||
|
zh: 技术
|
||||||
|
en: Tech
|
||||||
|
desc:
|
||||||
|
zh: Linux、容器与自托管,把家里的小机器折腾成顺手的工具。
|
||||||
|
en: Linux, containers, and self-hosting — turning the little machines at home into handy tools.
|
||||||
|
- icon: camera
|
||||||
|
name:
|
||||||
|
zh: 摄影
|
||||||
|
en: Photography
|
||||||
|
desc:
|
||||||
|
zh: 街拍与山野,随身一台无反,在等光的间隙按快门。
|
||||||
|
en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light.
|
||||||
|
|
||||||
|
- type: prose
|
||||||
|
title:
|
||||||
|
zh: 关于我
|
||||||
|
en: About me
|
||||||
|
body:
|
||||||
|
zh: |
|
||||||
|
气象是我起点的专业——一个想做预报的人,依旧把云图当日常读物。
|
||||||
|
|
||||||
|
写代码是第二条主线:Linux、容器、自托管,把日常工具一件件磨顺手。
|
||||||
|
|
||||||
|
作品大多在 [wait4x.online](https://wait4x.online/)。
|
||||||
|
|
||||||
|
信条:**拥抱开源,热爱开源**。
|
||||||
|
en: |
|
||||||
|
Meteorology is where I started — an aspiring forecaster who still reads weather charts for fun.
|
||||||
|
|
||||||
|
Coding is the second thread: Linux, containers, self-hosting — polishing everyday tools one by one.
|
||||||
|
|
||||||
|
Most of my work lives at [wait4x.online](https://wait4x.online/).
|
||||||
|
|
||||||
|
Motto: **embrace open source, love open source**.
|
||||||
|
|
||||||
|
- type: linklist
|
||||||
|
title:
|
||||||
|
zh: 找到我
|
||||||
|
en: Find me
|
||||||
|
items:
|
||||||
|
- label:
|
||||||
|
zh: 本站
|
||||||
|
en: Website
|
||||||
|
href: 'https://wait4x.online/'
|
||||||
|
note:
|
||||||
|
zh: 文章、图库与瞬间
|
||||||
|
en: Posts, gallery and moments
|
||||||
|
- label:
|
||||||
|
zh: 邮件
|
||||||
|
en: Email
|
||||||
|
href: 'mailto:X@alumni.nudt.edu.cn'
|
||||||
|
note:
|
||||||
|
zh: 有话直说
|
||||||
|
en: Say hello directly
|
||||||
|
- label:
|
||||||
|
zh: GitHub
|
||||||
|
en: GitHub
|
||||||
|
href: 'https://github.com/wait4xx'
|
||||||
|
note:
|
||||||
|
zh: 开源项目与提交
|
||||||
|
en: Open-source projects and commits
|
||||||
|
- label:
|
||||||
|
zh: 自托管 gitea
|
||||||
|
en: Self-hosted gitea
|
||||||
|
href: 'https://git.wait4x.online/wait4x/blog'
|
||||||
|
note:
|
||||||
|
zh: 本站源码
|
||||||
|
en: Source of this site
|
||||||
|
- label:
|
||||||
|
zh: RSS
|
||||||
|
en: RSS
|
||||||
|
href: /rss.xml
|
||||||
|
note:
|
||||||
|
zh: 订阅更新
|
||||||
|
en: Subscribe to updates
|
||||||
|
|
||||||
|
- type: prose
|
||||||
|
title:
|
||||||
|
zh: 本站如何构建
|
||||||
|
en: How this site is built
|
||||||
|
body:
|
||||||
|
zh: 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管 Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。
|
||||||
|
en: Pages are statically generated by Astro with no runtime framework JS. Content lives on a self-hosted gitea — my local editor and the gitea web UI write to the same git repository, and each push is built and published automatically by the homelab. Comments run on self-hosted Artalk. The built-from line in the footer links to the commit behind this build, keeping the process transparent and traceable.
|
||||||
|
|
||||||
|
# ---- 二级回退(blocks 为空时使用)----
|
||||||
|
intro:
|
||||||
|
zh: |
|
||||||
|
写作者 · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
|
en: |
|
||||||
|
Writer · Reader · Shutter-presser, embracing open source.
|
||||||
|
|
||||||
|
This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** I pressed that land in the gallery.
|
||||||
|
|
||||||
|
# 页面级排版覆盖(层 2)
|
||||||
|
# 页面级排版覆盖(后台「关于页 → 页面排版覆盖」可改):
|
||||||
|
# 整页换字族会带来字体体积成本(如 font-huiwen 整页 ≈694KB),默认留空=全站默认排版
|
||||||
|
# typography:
|
||||||
|
# font: font-huiwen # 字族:font-serif/font-sans/font-kai/font-huiwen/font-kuaile…
|
||||||
|
# accent: c-red # 强调色类:c-blue/c-red/…(外壳仍灰阶)
|
||||||
|
# scale: 1.05 # 字号缩放 0.8–1.25
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
tagline:
|
||||||
|
zh: 拥抱开源,热爱开源
|
||||||
|
en: Embracing open source, loving open source.
|
||||||
|
latestPhotos: 4
|
||||||
|
perTrackLimit: 6
|
||||||
|
sections:
|
||||||
|
tracks: true
|
||||||
|
photos: true
|
||||||
|
heatmap: true
|
||||||
|
projects: true
|
||||||
|
favicon: /favicon.svg
|
||||||
|
socials:
|
||||||
|
- label: GitHub
|
||||||
|
url: https://github.com/wait4xx
|
||||||
|
icon: github
|
||||||
|
- label: 邮件
|
||||||
|
url: mailto:X@alumni.nudt.edu.cn
|
||||||
|
icon: email
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
typing:
|
||||||
|
- zh: 气象出身,写代码为乐
|
||||||
|
en: Trained in meteorology, coding for fun
|
||||||
|
- zh: 自托管一切能自托管的东西
|
||||||
|
en: Self-hosting whatever can be self-hosted
|
||||||
|
- zh: 拥抱开源,热爱开源
|
||||||
|
en: Embrace open source, love open source
|
||||||
|
blurb:
|
||||||
|
zh: 本站本身即开源:
|
||||||
|
en: 'This site is itself open source:'
|
||||||
|
repo_url: https://git.wait4x.online/wait4x/blog
|
||||||
|
repo_label: git.wait4x.online/wait4x/blog
|
||||||
|
blurb_after:
|
||||||
|
zh: —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。
|
||||||
|
en: — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year.
|
||||||
|
mode: all
|
||||||
|
sort: pinned-first
|
||||||
|
perSource: github
|
||||||
|
intro_from: 'off'
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 主题旋钮(样式即数据 · 层 4):只放白名单内的 9 组旋钮。
|
||||||
|
# 红线:--measure(版心)与断点/纸墨/code 配色不在此列——它们牵动版面结构与分轨身份。
|
||||||
|
# 全部留空 = 沿用 src/styles/tokens.css 的既有值(零配置零变化)。
|
||||||
|
# 钳制与单调性在 src/lib/theme.ts 里做:越界值被夹取而非报错,构建不会因主题配置失败。
|
||||||
|
|
||||||
|
# ① 强调色(明/暗两值;构建期闸门校验正文对比度 ≥4.5,不达标回落默认)
|
||||||
|
accent_light: '#3454d1'
|
||||||
|
accent_dark: '#8ea2ff'
|
||||||
|
|
||||||
|
# ② 图片圆角(4–16;08 文档裁定上限 14)
|
||||||
|
radius_img: 10
|
||||||
|
|
||||||
|
# ③ 标题字号(22–32;必须 h1 > h2 > h3,否则整体回落默认)
|
||||||
|
text_h1: 27
|
||||||
|
text_h2: 22
|
||||||
|
text_h3: 19
|
||||||
|
|
||||||
|
# ④ 正文字号(16–22)
|
||||||
|
prose_size_reading: 20
|
||||||
|
prose_size_tech: 19
|
||||||
|
|
||||||
|
# ⑤ 正文行高(1.5–2.0)
|
||||||
|
leading_reading: 1.75
|
||||||
|
leading_tech: 1.7
|
||||||
|
|
||||||
|
# ⑥ 间距梯度(8–80;必须严格递增 3 < 4 < 5 < 6,否则回落默认)
|
||||||
|
space_3: 16
|
||||||
|
space_4: 24
|
||||||
|
space_5: 40
|
||||||
|
space_6: 64
|
||||||
|
|
||||||
|
# ⑦ 段落流距(0–2em)
|
||||||
|
flow_p: 0.9
|
||||||
|
flow_h2_post: 0.6
|
||||||
|
|
||||||
|
# ⑧ 过渡时长(150–600ms)
|
||||||
|
dur: 350
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>灯箱 v2.1 提案预览 · wait4x</title>
|
||||||
|
<style>
|
||||||
|
/* ===== 灰阶基底(对齐站 tokens.css:四档灰 + 唯一曲线族) ===== */
|
||||||
|
:root{
|
||||||
|
--bg:#fff; --bg-soft:#f7f7f8; --ink-1:#111113; --ink-2:#33343a; --ink-3:#6b6e78;
|
||||||
|
--ink-4:#a0a3ad; --line:#e6e7eb; --radius-img:10px;
|
||||||
|
--mono:"JetBrains Mono",ui-monospace,Consolas,monospace;
|
||||||
|
--sans:"Noto Sans SC Variable","Noto Sans SC",system-ui,-apple-system,"PingFang SC",sans-serif;
|
||||||
|
--ez:cubic-bezier(.4,0,.22,1); /* PhotoSwipe 333ms 节奏同族曲线 */
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;background:var(--bg);color:var(--ink-2);font-family:var(--sans);}
|
||||||
|
body.locked{overflow:hidden}
|
||||||
|
.wrap{max-width:896px;margin:0 auto;padding:28px 20px 72px}
|
||||||
|
kbd{font-family:var(--mono);font-size:11.5px;border:1px solid var(--line);background:#fff;
|
||||||
|
border-radius:4px;padding:1px 6px;color:var(--ink-3)}
|
||||||
|
|
||||||
|
/* ===== 顶部说明:这是提案预览稿 ===== */
|
||||||
|
.note{background:var(--bg-soft);border:1px solid var(--line);border-radius:10px;
|
||||||
|
padding:14px 18px;font-size:13.5px;line-height:1.7;color:var(--ink-2);margin:18px 0 30px}
|
||||||
|
.note strong{color:var(--ink-1)}
|
||||||
|
.note ul{margin:6px 0 0;padding-left:18px}
|
||||||
|
.note li{margin:2px 0}
|
||||||
|
h1{font-size:27px;color:var(--ink-1);margin:8px 0 2px;font-weight:600}
|
||||||
|
.sub{font-family:var(--mono);font-size:12.5px;color:var(--ink-4)}
|
||||||
|
|
||||||
|
/* ===== 影像墙:cover 裁切(与线上一致,起飞动画的前提) ===== */
|
||||||
|
.stage{background:rgba(255,255,255,.04);border-radius:6px;padding:14px}
|
||||||
|
.wall{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr);
|
||||||
|
grid-auto-rows:8px;grid-auto-flow:dense;gap:14px}
|
||||||
|
.shot{position:relative;overflow:hidden;border-radius:var(--radius-img);grid-row-end:span var(--sp,13)}
|
||||||
|
.shot button{display:block;width:100%;height:100%;padding:0;border:none;background:none;cursor:zoom-in}
|
||||||
|
.shot img{display:block;width:100%;height:100%;object-fit:cover}
|
||||||
|
@media (max-width:820px){.wall{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
@media (max-width:560px){.wall{grid-template-columns:1fr 1fr;gap:10px}}
|
||||||
|
|
||||||
|
/* ===== 灯箱:fixed 覆盖层(生产将等价几何搬回原生 <dialog>) ===== */
|
||||||
|
.lbx{position:fixed;inset:0;z-index:80}
|
||||||
|
.lbx[hidden]{display:none}
|
||||||
|
.lbx-bg{position:absolute;inset:0;background:rgba(8,10,14,.96);opacity:0;
|
||||||
|
transition:opacity 300ms var(--ez)}
|
||||||
|
.lbx.on .lbx-bg{opacity:1}
|
||||||
|
/* 双盒:win=裁切窗(动画 left/top/w/h),pic=内图(动画几何 + 缩放 transform) */
|
||||||
|
.lbx-win{position:fixed;overflow:hidden;border-radius:6px;transform-origin:0 0;
|
||||||
|
box-shadow:0 12px 48px rgba(0,0,0,.5);touch-action:none}
|
||||||
|
.lbx-pic{position:absolute;transform-origin:50% 50%;user-select:none;-webkit-user-drag:none;
|
||||||
|
transition:transform 90ms linear;cursor:zoom-in;display:block}
|
||||||
|
.lbx.landing .lbx-win,.lbx.landing .lbx-pic{
|
||||||
|
transition:left 300ms var(--ez),top 300ms var(--ez),width 300ms var(--ez),
|
||||||
|
height 300ms var(--ez),transform 300ms var(--ez),opacity 300ms var(--ez)}
|
||||||
|
.lbx-pic.zoomed{cursor:grab}.lbx-pic.grabbing{cursor:grabbing}
|
||||||
|
.lbx-pic.swap{opacity:0}
|
||||||
|
|
||||||
|
/* chrome 层 1:关闭/箭头/计数(+140ms 淡入) */
|
||||||
|
.c1{opacity:0;transition:opacity 220ms var(--ez)}
|
||||||
|
.lbx.on .c1{opacity:1;transition-delay:140ms}
|
||||||
|
.lbx-btn{position:fixed;width:44px;height:44px;border-radius:10px;cursor:pointer;
|
||||||
|
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#e6e9ee;
|
||||||
|
backdrop-filter:blur(4px);box-shadow:0 2px 10px rgba(0,0,0,.35);display:grid;place-items:center}
|
||||||
|
.lbx-btn:hover{background:rgba(255,255,255,.12)}
|
||||||
|
.lbx-close{top:16px;right:16px}
|
||||||
|
.lbx-prev{left:14px;top:50%;translate:0 -50%}
|
||||||
|
.lbx-next{right:14px;top:50%;translate:0 -50%}
|
||||||
|
.lbx-count{position:fixed;top:26px;left:20px;font-family:var(--mono);font-size:12.5px;color:#98a2b3}
|
||||||
|
|
||||||
|
/* chrome 层 2:caption / title+EXIF / 动作(+300ms 淡入) */
|
||||||
|
.c2{opacity:0;transition:opacity 260ms var(--ez)}
|
||||||
|
.lbx.on .c2{opacity:1;transition-delay:300ms}
|
||||||
|
.lbx-meta{position:fixed;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;
|
||||||
|
gap:3px;padding:14px 72px 20px;text-align:center;text-shadow:0 1px 4px rgba(0,0,0,.6)}
|
||||||
|
.lbx-caption{margin:0;font-family:var(--sans);font-size:13.5px;line-height:1.6;color:#dfe6f3;max-width:640px}
|
||||||
|
.lbx-row{display:flex;align-items:center;flex-wrap:wrap;justify-content:center;gap:14px;
|
||||||
|
font-family:var(--mono);font-size:12.5px}
|
||||||
|
.lbx-title{color:#fff;font-family:var(--sans);font-size:13px;font-weight:600}
|
||||||
|
.lbx-exif{color:#98a2b3}
|
||||||
|
.lbx-acts{display:inline-flex;gap:10px}
|
||||||
|
.lbx-act{display:inline-flex;align-items:center;gap:5px;padding:6px 11px;border-radius:10px;
|
||||||
|
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#dfe6f3;
|
||||||
|
font-family:var(--mono);font-size:12px;text-decoration:none;backdrop-filter:blur(4px)}
|
||||||
|
.lbx-act:hover{background:rgba(40,48,60,.75);color:#fff}
|
||||||
|
|
||||||
|
/* 沉浸态:zoom>1 或按 i —— chrome 全隐,舞台纯净 */
|
||||||
|
.lbx.clean .c1,.lbx.clean .c2,.lbx.noui .c1,.lbx.noui .c2{opacity:0;transition-delay:0ms}
|
||||||
|
|
||||||
|
/* 首开提示 + 缩放指示 */
|
||||||
|
.lbx-hint{position:fixed;left:50%;bottom:128px;translate:-50% 0;padding:7px 13px;border-radius:10px;
|
||||||
|
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);backdrop-filter:blur(4px);
|
||||||
|
font-family:var(--mono);font-size:12px;color:#dfe6f3;opacity:0;transition:opacity 400ms var(--ez);
|
||||||
|
pointer-events:none}
|
||||||
|
.lbx-hint.show{opacity:1}
|
||||||
|
.lbx-zind{position:fixed;right:20px;bottom:128px;font-family:var(--mono);font-size:12px;color:#98a2b3;
|
||||||
|
opacity:0;transition:opacity 300ms var(--ez);pointer-events:none}
|
||||||
|
.lbx-zind.show{opacity:1}
|
||||||
|
|
||||||
|
@media (max-width:560px){
|
||||||
|
.lbx-prev{left:6px}.lbx-next{right:6px}
|
||||||
|
.lbx-meta{padding:12px 16px 16px}
|
||||||
|
.lbx-hint{bottom:150px}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion:reduce){
|
||||||
|
.lbx-bg,.c1,.c2,.lbx-pic,.lbx-win,.lbx-hint,.lbx-zind{transition:none!important}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>图库 · 灯箱 v2.1 提案预览</h1>
|
||||||
|
<p class="sub">预览稿 —— 交互为提案演示(非线上行为)· 图片为 content/photos 实拍,EXIF 为示例数据</p>
|
||||||
|
<div class="note">
|
||||||
|
<strong>这是给浏览器直接打开的交互演示</strong>,对应《09-灯箱美化探索》提案 #1/#2/#3/#6/#7:
|
||||||
|
<ul>
|
||||||
|
<li><strong>原位起飞开合</strong>:点击缩略图,照片从原位放大入舞台;关闭回落原位(<kbd>Esc</kbd> / 背景点击 / 关闭钮)</li>
|
||||||
|
<li><strong>chrome 双层淡入</strong>:开箱后层 1(箭头/计数)先入、层 2(caption/EXIF)随入;<kbd>i</kbd> 隐藏全部;放大后自动隐层</li>
|
||||||
|
<li><strong>连拍导航</strong>:<kbd>←</kbd> <kbd>→</kbd> / 边缘箭头 / 触屏快滑;计数 n / N</li>
|
||||||
|
<li><strong>缩放</strong>:滚轮锚点缩放 1–4x、<kbd>双击</kbd> 1x↔2.5x、放大后拖拽、双指缩放;右下角缩放指示</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="stage"><ul class="wall" id="wall"></ul></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 灯箱:fixed 覆盖层(生产等价搬回原生 <dialog>,focus-trap 由 dialog 免费提供) -->
|
||||||
|
<div class="lbx" id="lbx" hidden>
|
||||||
|
<div class="lbx-bg"></div>
|
||||||
|
<div class="lbx-win" id="win"><img class="lbx-pic" id="pic" alt=""></div>
|
||||||
|
<button class="lbx-btn lbx-close c1" id="bClose" aria-label="关闭(Esc)">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m5 5 14 14M19 5 5 19"/></svg></button>
|
||||||
|
<button class="lbx-btn lbx-prev c1" id="bPrev" aria-label="上一张(←)">
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 5.5-6.5 6.5 6.5 6.5"/></svg></button>
|
||||||
|
<button class="lbx-btn lbx-next c1" id="bNext" aria-label="下一张(→)">
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></svg></button>
|
||||||
|
<div class="lbx-count c1" id="count">1 / 5</div>
|
||||||
|
<div class="lbx-meta c2">
|
||||||
|
<p class="lbx-caption" id="cap"></p>
|
||||||
|
<div class="lbx-row"><strong class="lbx-title" id="ttl"></strong><span class="lbx-exif" id="exif"></span>
|
||||||
|
<span class="lbx-acts"><a class="lbx-act" id="aOrig" href="#" target="_blank" rel="noopener">原图 ↗</a><a class="lbx-act" id="aDl" href="#" download>下载 ↓</a></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="lbx-hint" id="hint">滚轮缩放 · 双击放大 · Esc 关闭 · i 隐藏界面</div>
|
||||||
|
<div class="lbx-zind" id="zind">100%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ===== 数据:仓内实拍(含 2.36:1 长横幅与 2:3 竖图两种 letterbox 形态) ===== */
|
||||||
|
const PHOTOS=[
|
||||||
|
{src:'../content/photos/1766156983093.jpg',w:2400,h:1600,sp:13,title:'防波堤暮色',
|
||||||
|
cap:'落日把海与城一齐烫成金箔,海鸟的剪影恰好掠过日轮',exif:'X-T5 · XF35mmF1.4 · f/5.6 · 1/500s · ISO 200'},
|
||||||
|
{src:'../content/photos/mmexport1771816382115.jpg',w:2400,h:1018,sp:6,title:'长滩低云',
|
||||||
|
cap:'涨潮前的长滩,云层压得很低',exif:'X100VI · f/8 · 1/1000s · ISO 160'},
|
||||||
|
{src:'../content/photos/1766156998344.jpg',w:1600,h:2400,sp:20,title:'旗与塔',
|
||||||
|
cap:'旗在风里翻卷,塔身亮得像一支随时点火的火箭',exif:'GR III · f/2.8 · 1/250s · ISO 400'},
|
||||||
|
{src:'../content/photos/1766157004116.jpg',w:2400,h:1536,sp:9,title:'暮色撤离',
|
||||||
|
cap:'灯柱次第亮起,暮色像退潮一样撤离',exif:'X-T5 · XF56mmF1.2 · f/1.4 · 1/120s · ISO 800'},
|
||||||
|
{src:'../content/photos/mmexport1776564092433.jpg',w:2400,h:1600,sp:13,title:'夜航',
|
||||||
|
cap:'货轮的灯在雾里连成一条迟疑的线',exif:'X-T5 · XF70-300mm · f/4 · 1/60s · ISO 1250'}];
|
||||||
|
const N=PHOTOS.length;
|
||||||
|
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
const lbx=$('lbx'),win=$('win'),pic=$('pic'),wall=$('wall');
|
||||||
|
const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
let idx=0,open_=false,hintShown=false;
|
||||||
|
let zoom={s:1,tx:0,ty:0};
|
||||||
|
|
||||||
|
/* ===== 影像墙 ===== */
|
||||||
|
PHOTOS.forEach((p,i)=>{
|
||||||
|
const li=document.createElement('li');
|
||||||
|
li.className='shot'; li.style.setProperty('--sp',p.sp);
|
||||||
|
const b=document.createElement('button'); b.type='button'; b.setAttribute('aria-label',p.title);
|
||||||
|
b.dataset.i=i;
|
||||||
|
const im=document.createElement('img'); im.src=p.src; im.alt=p.title; im.loading=i<3?'eager':'lazy';
|
||||||
|
b.appendChild(im); li.appendChild(b); wall.appendChild(li);
|
||||||
|
b.addEventListener('click',()=>openLbx(i));
|
||||||
|
});
|
||||||
|
const thumbImg=i=>wall.querySelector(`button[data-i="${i}"] img`);
|
||||||
|
|
||||||
|
/* ===== 几何:舞台 fit 矩形 / cover-contain 双盒(PhotoSwipe data-cropped 路线) ===== */
|
||||||
|
function stageBox(){const W=innerWidth,H=innerHeight;
|
||||||
|
const side=W>760?56:12,top=64,bot=innerWidth>560?124:150;
|
||||||
|
return{x:side,y:top,w:W-side*2,h:H-top-bot};}
|
||||||
|
function fitRect(p,box){const r=p.w/p.h,br=box.w/box.h;let w,h;
|
||||||
|
if(r>br){w=box.w;h=w/r;}else{h=box.h;w=h*r;}
|
||||||
|
return{x:box.x+(box.w-w)/2,y:box.y+(box.h-h)/2,w,h};}
|
||||||
|
function coverIn(p,winR){const r=p.w/p.h,wr=winR.w/winR.h;let w,h;
|
||||||
|
if(wr>r){h=winR.h;w=h*r;}else{w=winR.w;h=w/r;}
|
||||||
|
return{x:(winR.w-w)/2,y:(winR.h-h)/2,w,h};} /* 相对 win 的坐标 */
|
||||||
|
const setBox=(el,g)=>{el.style.left=g.x+'px';el.style.top=g.y+'px';
|
||||||
|
el.style.width=g.w+'px';el.style.height=g.h+'px';};
|
||||||
|
|
||||||
|
/* ===== 开箱:从缩略图原位起飞 ===== */
|
||||||
|
function fillMeta(p){$('cap').textContent=p.cap;$('ttl').textContent=p.title;
|
||||||
|
$('exif').textContent=p.exif;$('count').textContent=`${idx+1} / ${N}`;
|
||||||
|
$('aOrig').href=p.src;$('aDl').href=p.src;$('aDl').setAttribute('download',p.title+'.jpg');}
|
||||||
|
function openLbx(i){
|
||||||
|
idx=i;const p=PHOTOS[idx];pic.src=p.src;pic.alt=p.title;fillMeta(p);
|
||||||
|
const t=thumbImg(i).getBoundingClientRect();
|
||||||
|
const tr=fitRect(p,stageBox());
|
||||||
|
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed','grabbing','swap');
|
||||||
|
lbx.hidden=false;open_=true;document.body.classList.add('locked');
|
||||||
|
lbx.classList.add('landing');
|
||||||
|
setBox(win,t);setBox(pic,coverIn(p,t)); /* 起点:与缩略图逐像素同框(cover 裁切) */
|
||||||
|
if(reduced){lbx.classList.add('on');lbx.classList.remove('landing');setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});}
|
||||||
|
else{
|
||||||
|
void win.offsetWidth; /* 强制 reflow,起点先落定 */
|
||||||
|
lbx.classList.add('on');
|
||||||
|
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h}); /* 终点:舞台 contain */
|
||||||
|
setTimeout(()=>lbx.classList.remove('landing'),330);
|
||||||
|
}
|
||||||
|
if(!hintShown){hintShown=true;const h=$('hint');h.classList.add('show');
|
||||||
|
setTimeout(()=>h.classList.remove('show'),2600);}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 关闭:回落到缩略图原位 ===== */
|
||||||
|
function closeLbx(){
|
||||||
|
if(!open_)return;
|
||||||
|
const p=PHOTOS[idx],t=thumbImg(idx)?.getBoundingClientRect();
|
||||||
|
instantResetZoom();
|
||||||
|
if(reduced||!t){lbx.hidden=true;lbx.classList.remove('on','clean','noui');open_=false;
|
||||||
|
document.body.classList.remove('locked');return;}
|
||||||
|
lbx.classList.add('landing');
|
||||||
|
setBox(win,t);setBox(pic,coverIn(p,t));
|
||||||
|
lbx.classList.remove('on','clean','noui');
|
||||||
|
setTimeout(()=>{lbx.hidden=true;lbx.classList.remove('landing');open_=false;
|
||||||
|
document.body.classList.remove('locked');},320);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 换片:旧图淡出、新图自侧向 10% 淡入(spacing 节奏) ===== */
|
||||||
|
function show(i,dir){
|
||||||
|
idx=((i%N)+N)%N;const p=PHOTOS[idx];instantResetZoom();fillMeta(p);
|
||||||
|
pic.src=p.src;pic.alt=p.title;
|
||||||
|
const tr=fitRect(p,stageBox());setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});
|
||||||
|
if(!reduced){pic.classList.add('swap');
|
||||||
|
pic.style.transform=`translateX(${dir*-10}%)`;
|
||||||
|
void pic.offsetWidth;pic.classList.remove('swap');
|
||||||
|
pic.style.transform='none';}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 缩放:滚轮锚点 / 双击 / 双指 / 拖拽(复用线上 v2 数学) ===== */
|
||||||
|
function clampPan(){const mx=Math.max(0,pic.clientWidth*(zoom.s-1)/2),
|
||||||
|
my=Math.max(0,pic.clientHeight*(zoom.s-1)/2);
|
||||||
|
zoom.tx=Math.min(mx,Math.max(-mx,zoom.tx));zoom.ty=Math.min(my,Math.max(-my,zoom.ty));}
|
||||||
|
function applyZoom(){clampPan();
|
||||||
|
pic.style.transform=zoom.s===1?'none':`translate(${zoom.tx}px,${zoom.ty}px) scale(${zoom.s})`;
|
||||||
|
pic.classList.toggle('zoomed',zoom.s!==1);
|
||||||
|
lbx.classList.toggle('clean',zoom.s!==1);}
|
||||||
|
function instantResetZoom(){const keep=pic.style.transition;pic.style.transition='none';
|
||||||
|
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed');
|
||||||
|
lbx.classList.remove('clean');void pic.offsetWidth;pic.style.transition=keep||'';}
|
||||||
|
function zoomAt(next,cx,cy){const r=pic.getBoundingClientRect();
|
||||||
|
const ax=cx-r.left-r.width/2-zoom.tx,ay=cy-r.top-r.height/2-zoom.ty;
|
||||||
|
if(next<=1){zoom={s:1,tx:0,ty:0};applyZoom();zindShow();return;}const k=next/zoom.s;zoom.tx-=ax*(k-1);zoom.ty-=ay*(k-1);zoom.s=next;applyZoom();zindShow();}
|
||||||
|
let zt;function zindShow(){const z=$('zind');z.textContent=Math.round(zoom.s*100)+'%';
|
||||||
|
z.classList.add('show');clearTimeout(zt);zt=setTimeout(()=>z.classList.remove('show'),900);}
|
||||||
|
lbx.addEventListener('wheel',e=>{if(!open_)return;e.preventDefault();
|
||||||
|
const f=Math.exp(-e.deltaY*0.0015);
|
||||||
|
const next=Math.min(4,Math.max(1,zoom.s*f));
|
||||||
|
if(next!==zoom.s)zoomAt(next,e.clientX,e.clientY);},{passive:false});
|
||||||
|
win.addEventListener('dblclick',e=>{if(zoom.s===1)zoomAt(2.5,e.clientX,e.clientY);
|
||||||
|
else instantResetZoom();}); /* 挂 win:放大态 setPointerCapture 会把 click 派发给 win,pic 收不到 */
|
||||||
|
|
||||||
|
/* 拖拽平移(放大态)+ 双指缩放:pointer 事件统一 */
|
||||||
|
const pts=new Map();let drag=null,pinch=null;
|
||||||
|
win.addEventListener('pointerdown',e=>{
|
||||||
|
pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
|
||||||
|
if(pts.size===2){const a=[...pts.values()];
|
||||||
|
pinch={d0:Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y),s0:zoom.s,
|
||||||
|
mx:(a[0].x+a[1].x)/2,my:(a[0].y+a[1].y)/2};drag=null;return;}
|
||||||
|
if(zoom.s>1){win.setPointerCapture(e.pointerId);
|
||||||
|
drag={x:e.clientX,y:e.clientY,tx:zoom.tx,ty:zoom.ty};pic.classList.add('grabbing');}
|
||||||
|
});
|
||||||
|
win.addEventListener('pointermove',e=>{
|
||||||
|
if(pts.has(e.pointerId))pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
|
||||||
|
if(pinch&&pts.size===2){const a=[...pts.values()];
|
||||||
|
const d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);
|
||||||
|
const next=Math.min(4,Math.max(1,pinch.s0*d/pinch.d0));
|
||||||
|
if(next!==zoom.s)zoomAt(next,pinch.mx,pinch.my);return;}
|
||||||
|
if(drag){zoom.tx=drag.tx+e.clientX-drag.x;zoom.ty=drag.ty+e.clientY-drag.y;applyZoom();}
|
||||||
|
});
|
||||||
|
const endPt=e=>{pts.delete(e.pointerId);if(pts.size<2)pinch=null;
|
||||||
|
if(drag){drag=null;pic.classList.remove('grabbing');}};
|
||||||
|
win.addEventListener('pointerup',endPt);win.addEventListener('pointercancel',endPt);
|
||||||
|
|
||||||
|
/* 触屏快滑翻页(zoom=1 时) */
|
||||||
|
let x0=null;lbx.addEventListener('touchstart',e=>{x0=e.touches[0].clientX;},{passive:true});
|
||||||
|
lbx.addEventListener('touchend',e=>{if(x0===null||zoom.s!==1){x0=null;return;}
|
||||||
|
const dx=e.changedTouches[0].clientX-x0;
|
||||||
|
if(Math.abs(dx)>40)show(idx+(dx<0?1:-1),dx<0?1:-1);x0=null;},{passive:true});
|
||||||
|
|
||||||
|
/* ===== 键盘 / 背景点击 / chrome 召唤 ===== */
|
||||||
|
document.addEventListener('keydown',e=>{
|
||||||
|
if(!open_)return;
|
||||||
|
if(e.key==='Escape')closeLbx();
|
||||||
|
else if(e.key==='ArrowLeft')show(idx-1,-1);
|
||||||
|
else if(e.key==='ArrowRight')show(idx+1,1);
|
||||||
|
else if(e.key==='i'||e.key==='I')lbx.classList.toggle('noui');
|
||||||
|
});
|
||||||
|
lbx.addEventListener('click',e=>{
|
||||||
|
if(e.target.closest('.lbx-win')||e.target.closest('.lbx-btn')||
|
||||||
|
e.target.closest('.lbx-meta')||e.target.closest('a'))return;
|
||||||
|
closeLbx(); /* 背景点击关闭(bgClickAction:'close' 同款) */
|
||||||
|
});
|
||||||
|
$('bClose').addEventListener('click',closeLbx);
|
||||||
|
$('bPrev').addEventListener('click',()=>show(idx-1,-1));
|
||||||
|
$('bNext').addEventListener('click',()=>show(idx+1,1));
|
||||||
|
|
||||||
|
/* 开箱中窗口变化:舞台即时重排(不做动画,避免与缩放打架) */
|
||||||
|
addEventListener('resize',()=>{if(!open_||lbx.classList.contains('landing'))return;
|
||||||
|
const p=PHOTOS[idx];const tr=fitRect(p,stageBox());
|
||||||
|
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# 博客体验研究与优化方案调研(六站)
|
||||||
|
|
||||||
|
> 任务产出。数据来源:2026-10-26 直连抓取六站 HTML(文本提取后人工分析)。抓取失败的站已如实标注。
|
||||||
|
> 站点快照存于 /tmp/uxstudy/*.html(临时,会话级)。目标站背景:Astro SSG 双语、灰阶极客风、命令面板/键盘弦/终端彩蛋/热力图/PWA/Artalk、自建 gitea+Sveltia 照片管线。
|
||||||
|
|
||||||
|
## 一、六站逐站笔记
|
||||||
|
|
||||||
|
### 1. qh1009.net(清凉药)✅ 已抓取(66KB,WordPress + WiThemes)
|
||||||
|
- **首页 IA**:纯内容流——俳句/诗全文直接铺在首页(非标题列表),编号制(#219…#2008),每条极短 + 标签。首页即作品。
|
||||||
|
- **排版**:文学向极简,正文居中窄栏,无目录/无进度条/无评论组件可见(0 评论数展示)。
|
||||||
|
- **特色交互**:几乎零交互,靠"编号+日期+分类词(俳/诗/译/博物)"的仪式感取胜。
|
||||||
|
- **导航/移动端**:顶部 Toggle Menu,Home/关于/作品/杂记/博物/俳/诗/译/Podcast,移动折叠。
|
||||||
|
- **内容组织**:标签云非常细(50+ 自然意象词),年归档分页。
|
||||||
|
- **管理**:WordPress(页脚 "Designed by WiThemes"),推断经典 WP 后台。
|
||||||
|
- **可借鉴**:瞬间(moments)可学"编号制"给每条瞬间一个序号,制造连续感;标签的"意象化命名"比功能化命名更有味道。
|
||||||
|
|
||||||
|
### 2. nbfox.com(麦田艺术)✅ 已抓取(187KB,WordPress 画廊站)
|
||||||
|
- **首页 IA**:题材/流派双维下拉菜单(风景画/肖像画… × 印象派/巴洛克…),首屏专题卡片(手机壁纸/梵高向日葵/浮岳三十六景)+ 大图瀑布。
|
||||||
|
- **排版**:图为主体,每图带结构化元数据卡:创作者/年代/图片原始尺寸(像素)/风格——元数据即说明文字。
|
||||||
|
- **特色**:①双维分类导航(题材×流派)②图片标注精确像素尺寸,管理下载预期 ③"超高清/热门收藏/精选"策展位。
|
||||||
|
- **导航/移动端**:粘性双搜索框 + 汉堡菜单,功能/会员/收藏夹分离。
|
||||||
|
- **内容组织**:博物馆聚合、公域版权资源库、网盘分发——内容型产品打法。
|
||||||
|
- **管理**:WordPress + WooCommerce(会员/网盘),重后台。
|
||||||
|
- **可借鉴**:**图库(gallery)加"双维筛选"**(地点×器材 或 主题×年份);每张照片展示 EXIF 元数据卡(机型/焦段/光圈/地点)——我们已有 exifr 管线,只差展示层。
|
||||||
|
|
||||||
|
### 3. yongyuan.name ✅ 已抓取(56KB,静态技术博客)
|
||||||
|
- **首页 IA**:一句个人格言 + "All Posts" 按年份倒序全量清单(2015→2024,一页全列),每条:日期/标题/分类。
|
||||||
|
- **排版**:无摘要、无图,纯列表——十年技术文一目了然,靠"全量可见"建立信任。
|
||||||
|
- **特色交互**:无。反例价值:极简到牺牲发现性(无搜索/无热门/无相关文)。
|
||||||
|
- **内容组织**:分类词前置在每行(CV/推荐系统/C++/ANN),每年末一篇"年终总结"锚点文。
|
||||||
|
- **可借鉴**:**归档页全量清单模式**(我们有 archives/,确认是否一页可览十年);"年终总结"惯例值得立为个人传统。
|
||||||
|
|
||||||
|
### 4. ruanyifeng.com/blog ✅ 已抓取(首页 20KB + 周刊分类页 176KB,自建 MT 系统)
|
||||||
|
- **首页 IA**:最新一篇全文摘要 + "继续阅读全文 »" + 右栏:最新文章 5 条 / 最新留言 / 关于 / 邮件订阅 / 授权声明 / 社交账号——**首页即门户**,信息密度极高但层级清晰。
|
||||||
|
- **文章列表**:周刊独立分类页,416 篇按年分组倒序,纯标题+日期列表。首页摘要带"分类: 周刊"标签 + 留言数(30)——**留言数前置是社交证明**。
|
||||||
|
- **内容组织(标杆点)**:①周刊作为**固定节奏产品**(周五发布,416 期,编号化)②完整档案页(分类/按月/2004 至今)③邮件订阅+RSS 双通道 ④留言 79533 条展示在侧栏(社区感)⑤版权/授权声明独立成块。
|
||||||
|
- **排版详情**:正文窄栏衬线、目录在长教程文中手写小节链接、代码块朴素无高亮(极客反潮流)。
|
||||||
|
- **可借鉴**:①**固定栏目化**——我们的"瞬间"可升格为编号制轻周刊(每周 3-5 条瞬间聚合 + 一句话评);②侧栏"最新留言"(Artalk API 可拉);③留言数展示在列表页。
|
||||||
|
|
||||||
|
### 5. antfu.me ✅ 已抓取(首页/列表页/文章页,Astro + UnoCSS,同栈)
|
||||||
|
- **首页 IA**:一段自我介绍正文 + "Recent work" 项目动态(直接拉 GitHub 数据)+ 一图流头部。无文章列表——博客藏在 /posts。
|
||||||
|
- **文章列表**:按年份分组,每条:日期 · 阅读时长("Sep 16 · 15min")· 语言徽标(中文/日本語)——**双语不是双站,是同一列表内并列两行互链**("Hello Tokyo!" / 「おはよう、東京!」相邻)。
|
||||||
|
- **排版/交互(值得抄)**:①`slide-enter-content` 入场动画(我们已有同类)②magic-link:GitHub 链接自动渲染成卡片、图片自动预加载(grep 实锤 markdown-magic-link 类)③单篇文章页有 "Demo of …" 双语切换按钮 + **Plaintext 纯文本模式**(去样式阅读)④暗色以 class 切换,全站色调极克制(text-gray-700/200)。
|
||||||
|
- **导航**:极简 5 项(Blog/Projects/Talks/Sponsors),移动端同构。
|
||||||
|
- **管理**:纯 git 仓 + 自己写的构建,无 CMS——证明"无后台"路线在顶级生产者处成立。
|
||||||
|
- **可借鉴**:①**双语互链并列**(比我们 /en/ 双轨更轻,可作文章页"读中文版"强化);②**阅读时长估算**列表页展示;③**纯文本/阅读模式**(灰阶站天然适配,一行 toggle + CSS);④首页 "Recent work" 拉 gitea commit 流(我们已有"最近心跳",可扩展为开源项目动态)。
|
||||||
|
|
||||||
|
### 6. innei.in ✅ 已抓取(141KB,Mix Space + Shiro 主题,React SSR)——中文现代个人站代表
|
||||||
|
- **首页 IA**:问候 + 一言(随机引言)+ 统计条(X 篇 · X 万字 · X 天)+ "近期笔墨"5 条(类型徽标:笔记/文章/技术)+ 页脚多宫格(关于/照片廊/联系/订阅/版式开关)。
|
||||||
|
- **特色(值得抄)**:①**站内搜索/主题/背景音乐/背景效果/余白开关全在设置面板**("版式"区:浅色·随系统·深色 / 背景音乐 关·开 / 背景效果 关·开)——把实验性开关收纳成一个面板;②多内容类型(文稿/手记/时光/思考)+ 订阅通信(Newsletter 入口);③留印记(留言板)、朋友们(友链)、一言。
|
||||||
|
- **导航**:顶部 4 项 + "更多",移动端宫格化页脚导航。
|
||||||
|
- **管理**:Mix Space 自带 admin 后台(草稿/评论/监控),有 API——对比样本。
|
||||||
|
- **可借鉴**:①**设置面板收纳实验开关**(我们的终端彩蛋/辉光/动效可聚成一个"偏好"面板并 localStorage 持久化——比散落各处更克制);②Newsletter 订阅入口;③友链页。
|
||||||
|
|
||||||
|
### 抓取情况汇总
|
||||||
|
六站全部抓取成功(qh1009/nbfox/yongyuan/ruanyifeng/antfu/innei),无备选站需补。注:本机代理(7897) TLS 握手故障,全部改直连抓取;innei.in 首屏计数显示 0(数据懒加载),已从 DOM 结构推断功能。
|
||||||
|
|
||||||
|
## 二、共性模式清单(优质站普遍做、我们没做/未确认)
|
||||||
|
|
||||||
|
| # | 是什么 | 价值 | 灰阶极客风适配建议 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **阅读时长估算**(antfu "15min") | 列表页决策成本↓ | PostList 每条加 `~8 min` 等宽字体角标,构建期按字数/分钟计算,零运行时 |
|
||||||
|
| 2 | **双语条目互链并列**(antfu 同列表 zh/en 相邻互指) | 双语发现性↑,用户不会漏另一语版 | 文章页标题下加一行 `→ EN` 静态链接即可(已有 /en/ 路由,缺的是入口可见性) |
|
||||||
|
| 3 | **纯文本/阅读模式**(antfu Plaintext) | 极客好评、灰阶天然契合 | 单条 CSS class 剥离装饰(辉光/动效/侧栏),命令面板加 `reader` 命令 + 快捷键 |
|
||||||
|
| 4 | **留言数/评论数前置到列表**(阮一峰"留言(30)") | 社交证明,激活 Artalk 存量数据 | Artalk API 构建期拉取各页评论数,写进 frontmatter 化的 JSON,列表页静态渲染 |
|
||||||
|
| 5 | **侧栏/首页"最新留言"**(阮一峰) | 站点"活着"的信号 | 首页"最近心跳"旁并列"最新评论"一条流(构建期快照,接受 24h 陈旧) |
|
||||||
|
| 6 | **固定节奏栏目化**(阮一峰周刊 416 期编号) | 读者形成期待,更新压力可控 | "瞬间"页顶加"本周瞬间 #N"聚合块,编号来自构建期统计 |
|
||||||
|
| 7 | **图片元数据卡**(nbfox 尺寸/创作者/年代) | 摄影轨的专业感 | gallery 详情展示 exifr 已提取的 EXIF(机型/焦段/光圈/ISO/时间/地点)等宽字体表 |
|
||||||
|
| 8 | **归档全量清单**(yongyuan 十年一页) | 纵深感与可扫读 | 确认 archives/ 是全量单页;不够则按年分组渲染 |
|
||||||
|
| 9 | **偏好/实验开关集中面板**(innei 版式区) | 克制与自由兼得,避免装饰散弹 | 命令面板加 `settings` 命令:辉光/动效/热力图/背景音乐各自 toggle,localStorage 持久化 |
|
||||||
|
| 10 | **Newsletter 订阅入口**(innei/antfu sponsor+订阅、阮邮件订阅) | 回访通道不被算法绑架 | P2→P1 观望:无邮件服务前先放 RSS 说明页强化;接入自托管 listmonk 是远期项 |
|
||||||
|
| 11 | **友链页**(innei 朋友们) | 个人站社区礼节,互推 | /about 或独立 /links,静态 YAML 驱动 |
|
||||||
|
| 12 | **首页项目动态**(antfu Recent work 拉 GitHub) | 开源轨的活性证明 | "最近心跳"已含 commit 脉冲,可加"开源动态"一列(gitea API,构建期) |
|
||||||
|
|
||||||
|
## 三、差异化机会(我们独有,保持强化)
|
||||||
|
|
||||||
|
1. **命令面板 Ctrl+K + vim 键位弦(g g / g s 双击 chord)**——六站无一做到;持续补齐 chord E2E 矩阵即可,是护城河。
|
||||||
|
2. **终端彩蛋页 + 页脚 commit hash**——antfu 级极客也只做了 slide 动画;我们的终端隐喻是身份标识,新功能优先以"命令"形态暴露(如 reader、settings 走命令面板而非按钮)。
|
||||||
|
3. **构建期零 JS 摄影 masonry(exifr+sharp)**——innei 照片廊是 React 重运行时;我们的构建期管线是自托管价值观的最佳注脚,配上 EXIF 元数据卡(共性#7)即成完整叙事。
|
||||||
|
4. **构建期 LLM 预翻译 + .en.md 入仓 + AI 标注**——antfu 双语靠手写;我们的管线差异化在"过程可审计"(入仓),强化 AI 译徽标的诚实性。
|
||||||
|
5. **PWA 离线全量 + 安装 CTA 三态**——六站均无 PWA;已有,勿再投入,保持零回归即可。
|
||||||
|
6. **灰阶克制本身**——阮一峰证明了"朴素不等于廉价";我们与 antfu 的 gray-700/200 同谱系,方向正确。
|
||||||
|
|
||||||
|
## 四、管理后台便利性专题
|
||||||
|
|
||||||
|
### Sveltia vs 主流后台(WordPress/Typecho/Halo)差距面
|
||||||
|
| 能力 | WP/Halo/Typecho | Sveltia(git 仓) | 补齐方案 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 草稿预览 | 即时预览 | 编辑侧预览,但布局/组件 ≠ 成品 | **/draft/ 构建路由**:draft frontmatter 的文章构建进 `/draft/<slug>/`,nginx 层加 BasicAuth 或长随机路径;发布=移除 draft 标志 |
|
||||||
|
| 媒体库 | 中央媒体库、检索、批量 | 文件夹浏览、上传即入 git | 我们照片走 gitea web 上传已等价;补 **构建期图片 lint**(exifr 校验尺寸/格式/命名规范,违规即构建警告清单页) |
|
||||||
|
| 定时发布 | 原生 | 无 | **构建期 publishDate 判定**:CI 触发频率内生效(webhook+定时 cron 双触发 gitea actions / receiver),文章带未来日期默认不出现在正式列表 |
|
||||||
|
| 批量操作 | 后台多选 | git 原生(批量改文件+一次 commit) | 无需补;可在 scripts/ 加批量 frontmatter 工具(改日期/标签) |
|
||||||
|
| 版本历史 | 修订版 | git 全量历史 | 已胜出;页脚 commit hash 已把它可视化了 |
|
||||||
|
|
||||||
|
### "自托管+git 仓"架构补齐清单(按性价比排序)
|
||||||
|
1. `/draft/` 预览路由(上面方案)——写作体验最大缺口。
|
||||||
|
2. 构建期内容 lint:断链检查、图片存在性、frontmatter schema(typebox 已在栈内)、双语文件配对校验(zh 无 en 时警告)。
|
||||||
|
3. 构建期质量报告页(仅 draft 可见):字数/阅读时长/图片体积/未翻译词条清单,写作完成度仪表。
|
||||||
|
4. Sveltia config.yml 优化:为 moments/gallery 集合配 i18n 双语字段映射、媒体文件夹预排序(按日期倒序,避免长列表翻找)。
|
||||||
|
5. 发布检查单自动化:构建尾跑 LH 冒烟(gz-server.py 已在仓)+ 死链扫描,fail 即 webhook 通知 gitea。
|
||||||
|
|
||||||
|
## 五、分级建议表
|
||||||
|
|
||||||
|
| 级 | 项 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| **P0** | 阅读时长角标(#1) | 一次构建期计算,全站收益 |
|
||||||
|
| **P0** | 文章页"→ EN/中文版"互链(#2) | 双语投入的回报被入口缺失吃掉 |
|
||||||
|
| **P0** | /draft/ 预览路由(四-1) | 写作流最大缺口,git 架构易实现 |
|
||||||
|
| **P0** | 构建期内容 lint(四-2) | 防线前移,typebox 已在栈内 |
|
||||||
|
| **P1** | 命令面板 reader 纯文本模式(#3) | 极客好评且实现轻,走命令面板零 UI 负担 |
|
||||||
|
| **P1** | 偏好面板收纳实验开关(#9) | 辉光/动效已有但散落,收纳更克制 |
|
||||||
|
| **P1** | gallery EXIF 元数据卡(#7) | exifr 数据已在手,纯展示层 |
|
||||||
|
| **P1** | 列表评论数 + 首页最新评论流(#4/#5) | 激活 Artalk 存量;接受构建期快照陈旧 |
|
||||||
|
| **P1** | 瞬间编号化"本周瞬间 #N"(#6) | 低成本仪式感 |
|
||||||
|
| **P1** | 归档全量清单复核(#8) | 只需确认,可能已达标 |
|
||||||
|
| **P1** | 友链页(#11) | 静态 YAML,一晚工作量 |
|
||||||
|
| **P2** | Newsletter/邮件订阅(#10) | 需自托管邮件栈(listmonk+发信域名),维护成本先于价值;先强化 RSS |
|
||||||
|
| **P2** | 背景音乐/一言/背景特效(innei 同款) | 与灰阶克制冲突:多感官装饰稀释"终端"隐喻的一致性 |
|
||||||
|
| **P2** | 首页大图专题卡、瀑布流重化(nbfox 同款视觉) | nbfox 是内容电商型画廊;我们零 JS masonry 已是更优解,加重视觉与"灰阶骨架×分轨正文"定稿相悖 |
|
||||||
|
| **P2** | 阅读进度条/浮动 TOC 大改 | 六站中仅少数有 TOC;我们长文少,现有目录足够,勿为趋势加常驻 UI |
|
||||||
|
|
||||||
|
## 风险与备注
|
||||||
|
- antfu.me 文章页部分组件(Plaintext 按钮)为客户端渲染,快照中由类名/结构推断,实现前建议人工核对 antfu 仓(antfu.me 开源)。
|
||||||
|
- innei.in 首屏计数 0 为懒加载,统计功能存在但数值未取到。
|
||||||
|
- 评论数/最新评论若走 Artalk 需注意 ESA 缓存(此前已有 /artalk/api 绕缓存议题),构建期拉取可绕开运行时问题。
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# 博客优化方案 v2(深度版)
|
||||||
|
|
||||||
|
> 最终落位:`docs/research/07-博客优化方案v2.md`(本文件为 scout 产出原稿,内容即完整方案)。
|
||||||
|
> 数据来源:2026-10-26 12:05-12:30 生产实测(curl 直抓 wait4x.online + 本机 nginx/Artalk 容器 + 仓库源码)与境外站直连抓取(本机 clash 7897 节点故障,`--noproxy '*'` 直连成功,复现 v1 教训)。抓取失败项已如实标注。
|
||||||
|
> 前置:v1 六站研究见 docs/research/06;本项目历史调研 01-05。
|
||||||
|
|
||||||
|
## 1. 执行摘要(推荐立即做的 8 件事)
|
||||||
|
|
||||||
|
1. **解除全站 noindex 封印**(0.5d):nginx 共享 snippet 给博客 vhost 注入 `X-Robots-Tag: noindex`,全站被标记不可索引——**这是生产 SEO 60-63 的第一主因,所有内容工作在这之前等于闭门写**。
|
||||||
|
2. **补 robots.txt + ESA 全站 purge 一次**(0.5d):public/ 从未有 robots.txt(生产 404);noindex 响应头已被 ESA 边缘缓存 30 天,修完必须 purge。
|
||||||
|
3. **修 localePath 双 en bug + 加自指 canonical**(0.5d):en 页 hreflang 自指 `/en/en/archives/…`(404);同页带/不带尾斜杠双 200。
|
||||||
|
4. **构建期内容 lint**(1d):断链/图片存在性/frontmatter schema/双语配对,build.sh 守卫挂钩(防线前移,v1 P0 坚持项)。
|
||||||
|
5. **/draft/ 预览路由**(1d):写作流最大缺口;长随机路径 + robots noindex + 不入 sitemap。
|
||||||
|
6. **reader 纯文本模式**(0.5d):`html.reader` 单 class 方案,落点已全部排明(§4.4);命令面板 `reader` 命令 + `r` 键。
|
||||||
|
7. **列表页评论数前置**(1d):Artalk API 已实测(`GET /api/v2/comments?…&site_name=wait4x.online` 返回 `count`),构建期拉取静态渲染,零运行时。
|
||||||
|
8. **首页 title/description 语义化**(0.5d):现全站共用 `wait4x` + 一句通用 description,列表/文章页 title 已对,home 与 moments/gallery 等仍弱。
|
||||||
|
|
||||||
|
## 2. v1 P0/P1 批判性复审
|
||||||
|
|
||||||
|
| v1 项 | v1 级 | 复审结论 | 理由 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 阅读时长角标 | P0 | **降 P1**:文章页已有(`[...slug].astro:39` 汉字数/400 取整,`:60` 已渲染"阅读时长 N 分钟");v1 说的其实是列表页角标,页内已有 → 列表是锦上添花 |
|
||||||
|
| →EN 互链 | P0 | **关闭**:Header `nav-lang` EN 链接已每页常驻(车道 G 出舱后任何宽度可见)。但复查揪出真 bug:en 页 hreflang 双 en(见 §4.1-3)——**从"加互链"转化为"修 localePath"** |
|
||||||
|
| /draft/ 预览路由 | P0 | **坚持**:git 架构最大写作缺口。实现降级:长随机路径优先于 BasicAuth(nginx 现无 auth 依赖,别为 draft 引入) |
|
||||||
|
| 构建期 lint | P0 | **坚持并升级**:SEO 三连漏检证明"只在本地测"会漏生产层故障——lint 应加"生产冒烟"(curl robots/sitemap/头)一档 |
|
||||||
|
| reader 纯文本 | P1 | **坚持升 P0 批**(与 SEO 同批做):antfu 同款 + 灰阶天然适配,本轮落点清单已备好,半天可收 |
|
||||||
|
| 偏好面板 | P1 | **降级收窄**:先只做 reader + 辉光/动效开关两个 toggle,防"设置中心"过度设计(innei 面板是 React 重站的路数) |
|
||||||
|
| gallery EXIF 卡 | P1 | **坚持**:photos_ingest.py 构建期 exifr 数据已在手,纯展示层 |
|
||||||
|
| 评论数前置+最新评论 | P1 | **坚持,风险清零**:v1 担心的"API 形状/ESA 缓存"本轮实测解决——构建期从 127.0.0.1:13030 内网直拉(构建机=Artalk 宿主),完全不碰 ESA |
|
||||||
|
| 瞬间编号化 | P1 | **保留 P1 尾部**:仪式感成立(diygod `#年终总结` 6 篇、阮一峰 416 期都验证了节奏感),但数据量小(21 图+1 瞬间),收益后置 |
|
||||||
|
| 归档全量复核 | P1 | **关闭(已达标)**:实测 /archives/ 全量单页,zh 6 条 + en 6 条(7 个 md 减 media-sample 的 hidden 标记),无分页,一页可扫 |
|
||||||
|
| 友链页 | P1 | **保留**:diygod `/friends` 友邻页再次验证(中文个人站礼节),YAML 驱动一晚 |
|
||||||
|
| Newsletter | P2 | **维持观望**:xeiaso 用 Patreon/signalboost 但那是有 400 文+社群的体量;我们 RSS 刚起步,先 RSS 说明页 |
|
||||||
|
|
||||||
|
**v1 最大盲区(自我批判)**:v1 调研全部基于"站点代码应该没问题"的假设,只测了视觉/交互,没测生产响应头与爬虫视角——本轮 SEO 60-63 的三个根因(noindex 头、robots 缺失、hreflang 404)全部存在于 v1 调研期间,一个都没被 v1 发现。教训:**体验研究必须包含"爬虫/机器人视角"的生产实测**,已补进 §4。
|
||||||
|
|
||||||
|
## 3. 新探索站增量发现(v2 补面,8 站)
|
||||||
|
|
||||||
|
> 本机 clash 7897 节点当日故障(google/github 经代理均 000),全部改 `--noproxy '*'` 直连成功——与 v1"代理 TLS 故障改直连"同款环境病,抓境外站先直连再代理。
|
||||||
|
|
||||||
|
1. **rauno.me**(9.7KB 瘦 HTML,实测)——交互工艺标杆,但真正的标杆在子页:`/craft` 是**按月编年的工艺案例库**(2020→2026,30+ 条:Toolbar Morph/X-Ray Interaction/⌘K Breadcrumbs…,每条三种出口 View Production/View Prototype/Read Essay)。首页即一句信条循环("Make it fast. Make it beautiful… Make it.")+ **Email 点击即复制**("Copied" 内联反馈)+ **自我版本化**(2023/2022 年度快照链接)。可借鉴:①把 terminal 页或独立页做成"工艺/极客案例编年体";②页脚 email 用复制微交互替代 mailto;③站内 About 写"信条列表"比自我介绍段更有身份感。
|
||||||
|
2. **diygod.cc**(RSSHub 作者站,实测)——中文极客个人站代表:首页格言 + **分类计数前置**(创作集26/分享境18/研习坊16/日记本13——中文意象化命名,与 v1 qh1009 结论互证);/posts **90 篇前端关键词搜索**(零后端);**双语 /en 切换**;`#年终总结` 6 篇的节奏传统。**彩蛋级发现:页面内嵌一段给 AI 爬虫的 persona prompt**("你是一个擅于模仿真实人类写博客的 AI,名字叫 DIYgod…")——对 AI 爬虫的幽默防御/人格注入,我们 terminal 页可同款玩。RSS 姿态:作者即 RSSHub 之父,站内 RSS 极简入口 + 完整摘要输出(15/15 全文)。
|
||||||
|
3. **brianlovin.com**——**抓取失败(如实标注)**:Vercel Security Checkpoint JS 挑战挡 curl(HTTP 429),本机 c4ai 无头浏览器因代理故障不可用。其"写作/项目/梯次"结构引用 v1 常识不采信,标注为待人工复核。
|
||||||
|
4. **vincentwill.com**(实测,选它弃 sabrinacodes)——sabrinacodes 实抓后确认是 PHP 教师 portfolio(Resume/Education/Mini Games),与我们三轨内容零交集,弃;vincentwill 为柏林 web 开发者:**微产品墙一行一产品**(名+一句话+链接,[Sold] 标记),"latest projects" 精选 6 + "More Projects" 全量折叠——与开源项目墙"双源"同构,可借鉴其**精选/全量两档**而非平铺。
|
||||||
|
5. **xeiaso.net**(实测)——400+ 文的终端极客活化石:blog 列表页顶部**RSS+JSONFeed 双提示语**("If you have a compatible reader…");**系列文仪式**("No way to prevent this" 同标题连发=美国枪击梗的技术圈复刻,系列性本身就是品牌);页脚**最新 commit hash 链回 GitHub**(与我们 T7 同款,互证方向);多身份自我定义(technical educator/vtuber/philosopher)。可借鉴:①系列文(CS 配置/书评都可系列化命名);②blog 顶部 RSS 友好提示语;③signalboost(转发求职/求助信息)是社群位远期选项。
|
||||||
|
6. **shud.in**(自选①,理由:中文 design engineer、Vercel 核心贡献者的极简样本,与"灰阶骨架"同谱系)——实测四页 IA(About/Thoughts/Projects/Guestbook),首页一段自我叙述+社交链接,零装饰零动效。**Guestbook 留言板**=轻量社区位(Artalk 现成能力);"Thoughts"命名比"博客"更有随笔感。启示:**我们的极简已达标,不需再减;缺的是 diygod/xeiaso 级的内容节奏与仪式**。
|
||||||
|
7. **Artalk 生态站**——**artalk.org 域名已易主**(实测标题"𝕜✺𝕫 𝕤𝕥𝕦𝕕𝕚𝕠 // healing arts therapy",Squarespace 疗愈站)⚠️;但官方实为 **artalk.js.org**,npm 包 `artalk` **2.10.1 于 2026-10-25(昨天)发布**,上游活跃。结论:我们自托管 artalk-go 2.10.0 容器不受域名易主影响;升级通道=Docker Hub;官方 demo 站本轮未访(js.org 子域未测),评论体验最佳实践改以"本机 2.10.0 API 实测"为准(§4.3)。**行动项:文档/书签里的 artalk.org 引用全部改 artalk.js.org**。
|
||||||
|
8. 增量共性(补 v1 十二条之外):①**分类/标签中文意象命名+计数前置**(diygod×qh1009 双验证)→ 我们 PostList 的 reading/tech/photo 三轨 zh 词条已是意象词,可加计数;②**系列文/编号传统**(xeiaso×diygod×阮一峰三方验证)→ "瞬间"编号化(v1 #6)证据更强;③**RSS 可见性**(diygod 全文输出+xeiaso 列表页提示语)→ 我们 rss.xml.ts 存在但入口只在命令面板+页脚,/articles/ 页顶可加一行订阅提示。
|
||||||
|
|
||||||
|
## 4. 自查实证(生产 × 仓库,全部本轮实测)
|
||||||
|
|
||||||
|
### 4.1 SEO 60-63 根因(三连,全实锤)
|
||||||
|
|
||||||
|
| # | 事实 | 证据 | 影响 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **全站 `X-Robots-Tag: noindex`** | `curl -I https://wait4x.online/` 与文章页均带该头;源=`/etc/nginx/snippets/esa-origin-ssl.conf:11`(`add_header X-Robots-Tag noindex always;`),博客 server 块(`sites-enabled/default:92` wait4x.online→18090)include 了这个给内部服务(api/oj/token/fc/latex/cpa)设计的 snippet | Lighthouse `is-crawlable` fail(重权重);**搜索引擎视角全站不可索引** |
|
||||||
|
| 2 | **robots.txt 不存在** | 生产 404(nginx);`public/` 与 `dist/` 均无 | Lighthouse robots 审计扣分;无 Sitemap 入口 |
|
||||||
|
| 3 | **en 页 hreflang 自指双 en** | 生产 HTML:`hreflang="en" href="…/en/en/archives/mian-sha/"`(实测 404);源=`src/lib/locale.ts` 的 `localePath('/en/…','en')` 未剥旧前缀,`BaseLayout.astro:91` 直接调用 | Lighthouse hreflang fail;每页必扣 |
|
||||||
|
|
||||||
|
次级问题:④无自指 canonical,且 `/en/archives/mian-sha`(Header nav-lang 的 href)与 `/en/archives/mian-sha/`(sitemap/hreflang 形状)**双 200 不重定向**(nginx try_files 直出),重复内容风险;⑤home `<title>wait4x</title>` 过薄;⑥**ESA 对 text/html 缓存 2592000s(30 天)**(`X-Swift-CacheTime`),当前靠 Last-Modified/ETag 回源重验证拿新内容(实测 Last-Modified=当日部署时刻),但响应头(含 noindex)固化在缓存对象里——**修复后不 purge 则一个月内爬虫仍见 noindex**。本地曾 100 的原因:测试床无 nginx snippet、无 ESA、robots.txt 404 在旧版 Lighthouse 不判 fail。
|
||||||
|
|
||||||
|
**修法(A 卡片)**:① snippet 剥离 noindex 行,6 个内部服务 vhost 各自 server 块补回(它们确实该 noindex),博客 vhost 不加;博客私有路径(/admin/、/draft/)单独 location 加 noindex;② `public/robots.txt`(Allow 全站,Disallow /admin/ /draft/,Sitemap 指向 sitemap-index.xml);③ localePath 改为先 normalize 到 zh 再拼目标前缀;④ BaseLayout 加自指 canonical(Astro.site + withSlash(rawPath)),Header nav-lang href 补尾斜杠;⑤ ESA 控制台对 wait4x.online 执行一次全站 purge,并评估 HTML 边缘 TTL 调至 ≤1h 或接 purge API(发布可见性保险)。
|
||||||
|
|
||||||
|
### 4.2 /archives/ 归档现状(v1 #8 复核)
|
||||||
|
|
||||||
|
实测:`/archives/` 全量单页,zh 6 条 en 6 条,年份分组+三轨分节(PostList.astro),无分页;`content/posts` 7 文,差 1 = media-sample(`hidden` tag,`archives/index.astro` 过滤条件 `!data.draft && !data.tags.includes('hidden')`,注释声明"视觉测试完成后应删除")。**结论:达标,零改动**;唯一尾单:视觉验收完成后删 media-sample(它仍占 sitemap 一条 URL)。
|
||||||
|
|
||||||
|
### 4.3 Artalk 评论数 API(列表页前置数据源,已实测)
|
||||||
|
|
||||||
|
- 形状:`GET /api/v2/comments?page_key=/archives/<slug>/&type=page&limit=0&site_name=wait4x.online` → `{"comments":[],"count":0,"roots_count":0,"page":{…,"pv":4,…}}`。**注意参数是 `site_name` 不是 `site`**(`site` 报 err_no_site)。匿名可读,无需管理员。
|
||||||
|
- site 值来源:`~/docker_apps/artalk/data/artalk.yml` `site_default: wait4x.online`;前端 ArtalkComments.astro 的 pv POST 已带 `site_name`,pageKey 口径一致(`/archives/<id>/`,中英共用不分裂)。
|
||||||
|
- **构建期直拉可行**:构建机=Artalk 宿主(同机 127.0.0.1:13030),绕开 ESA/nginx,零缓存语义问题;构建产物静态渲染数字,接受"构建时刻快照"陈旧(v1 已定此取舍)。
|
||||||
|
- 最新评论流(首页"最新心跳"旁):同端点 `type=all` 时仍要求 page_key,全站维度改用 `GET /api/v2/comments?type=all&site_name=…&limit=5` 需带任一 page_key 的坑已遇("Page_key必须填写")——**实施时以管理员 token 走 `/api/v2/pages`(需登录,`need_login:true` 实测)或退回逐页轮询 count 取 top-N**,两路都写在卡片验收里。
|
||||||
|
- 上游健康:artalk-go 2.10.0(在跑)→ npm artalk 2.10.1(2026-10-25 发);artalk.org 域名易主(§3-7),文档引用改 artalk.js.org。
|
||||||
|
|
||||||
|
### 4.4 reader 纯文本模式落点(html.reader 单 class 方案)
|
||||||
|
|
||||||
|
Body 结构(BaseLayout.astro):`.bg-glow`(背景辉光)→ `.skip-link` → `header.site-header.safe-area`(含 nav-actions 三钮)→ `main#main.container`(正文+`.toc`侧栏+`.pn`上下篇+`#artalk-container`)→ `footer.site-footer`。会被覆盖/剥离的选择器清单(global.css + 组件 scoped):
|
||||||
|
- 隐藏:`.bg-glow`、`.read-progress`(顶进度条)、Header `.nav-actions`(保留品牌+导航词)或整个 `.site-header` 收为一行、Footer 收为备案单行(ICP 陕ICP备2026014860号-1 **不可隐**——合规底线);
|
||||||
|
- 中性化:正文入场动画类(global.css 动效层 `prefers-reduced-motion` 已有的降级规则可复用同款写法)、列表微浮 hover、`.atk-pv-line`/`#artalk-container` 隐藏或保留(建议保留评论——reader 是去装饰不是去内容,antfu Plaintext 同款语义);
|
||||||
|
- 增强:`.prose-reading/.prose-tech` 字号微升一档、行宽略收(tokens.css 变量覆写)。
|
||||||
|
- 入口:CommandPalette `data-cmd="reader"`(照 `data-cmd="theme"` 模式,`src/components/CommandPalette.astro:42`)+ 键盘 `r`(Keyboard.astro 弦位表)+ localStorage 持久化;VT 软导航下 class 挂 `documentElement` 天然存活。
|
||||||
|
|
||||||
|
## 5. 实施方案卡片
|
||||||
|
|
||||||
|
**A|SEO 解封三件套 + ESA purge**(工作量 1d;风险:nginx 重载失误→保留 .bak 回滚,ESA purge 不可逆但无害)
|
||||||
|
- 目标:搜索引擎可索引,Lighthouse SEO ≥95。
|
||||||
|
- 做法:`/etc/nginx/snippets/esa-origin-ssl.conf` 删 noindex 行;`api/oj/token/fc/latex/cpa/git` 各 vhost server 块补 `add_header X-Robots-Tag noindex always;`;博客 vhost 加 `location /admin/ { add_header X-Robots-Tag noindex always; }`;`public/robots.txt` 新建;ESA 全站 purge + HTML TTL 评估 ≤1h。
|
||||||
|
- 验收:`curl -sI https://wait4x.online/ | grep -ci x-robots-tag` = 0;`curl -s https://wait4x.online/robots.txt` 含 `Sitemap: https://wait4x.online/sitemap-index.xml`;`curl -sI https://wait4x.online/admin/ | grep -ci noindex` = 1;Lighthouse 生产 home SEO ≥95。
|
||||||
|
|
||||||
|
**B|localePath 双 en + canonical + 尾斜杠**(0.5d;风险低,纯前端)
|
||||||
|
- 做法:`src/lib/locale.ts` localePath 先归一到 zh 路径再拼;`BaseLayout.astro` 加 `<link rel="canonical" href={new URL(withSlash(rawPath), Astro.site)}>`;Header nav-lang href 与所有站内链接统一带尾斜杠。
|
||||||
|
- 验收:`grep -c '/en/en/' dist/en/archives/mian-sha/index.html` = 0;`dist/` 内每页有自指 canonical;playwright `link[hreflang="en"]` 的 href 与自身 URL 相等;`curl -sI https://wait4x.online/en/archives/mian-sha`(无斜杠)→ 301 或 canonical 唯一。
|
||||||
|
|
||||||
|
**C|构建期内容 lint + 生产冒烟**(1d;风险:lint 误报阻断发布→先 warn 一周再 fail)
|
||||||
|
- 做法:`scripts/build-perf/content-lint.mjs`(断链:a href 站内前缀逐一对 dist 产物;图片存在性;frontmatter typebox schema;zh/en 配对警告);`build.sh` 守卫挂钩 `[ -f scripts/build-perf/content-lint.mjs ] && node … || echo skip`(铁律:存在性守卫必须是第一笔改动);生产冒烟脚本 curl robots/sitemap/首页头三断言。
|
||||||
|
- 验收:人为引入断链 md → build 输出 FAIL 清单;正常仓 build 通过;`sh infra/deploy/build.sh` 全链绿。
|
||||||
|
|
||||||
|
**D|/draft/ 预览路由**(1d;风险:草稿 URL 泄露→长随机段+noindex+不入 sitemap)
|
||||||
|
- 做法:posts 集合 frontmatter 加 `draft: boolean`;`src/pages/draft/[slug].astro`(draft:true 文章构建进 `/draft/<token>/<slug>/`,token=构建期环境变量注入的随机串);getStaticPaths 正式路由已过滤 draft(`!data.draft` 现成);robots.txt Disallow /draft/;nginx 无需改(静态目录直出)。
|
||||||
|
- 验收:draft 文章不出现在 `/archives/` 与 rss.xml/sitemap;`/draft/<token>/<slug>/` 200;`curl -sI …/draft/ | grep -ci noindex` = 1;playwright 断言 draft 页无 nav 中"文章"aria-current。
|
||||||
|
|
||||||
|
**E|reader 纯文本模式**(0.5d;风险:覆盖不全→按 §4.4 清单逐条断言)
|
||||||
|
- 做法:`html.reader` 单 class(global.css 尾部集中一段,全部 `html.reader xxx` 前缀);CommandPalette 加 `data-cmd="reader"` 词条(i18n/ui 双语);Keyboard `r` 单键;localStorage `reader` 持久化。
|
||||||
|
- 验收:playwright 触发命令后 `.bg-glow`/`.read-progress` 不可见、备案号可见、正文段落数不变;`html` 有 `reader` class 且刷新(reload)后仍在;E2E zh/en 各跑一遍。
|
||||||
|
|
||||||
|
**F|列表评论数 + 阅读时长角标**(1d;风险:Artalk 不可用时构建卡死→超时 3s 降级 `–`,lint warn 不 fail)
|
||||||
|
- 做法:`scripts/build-perf/artalk-counts.mjs` 构建期对每篇 `GET 127.0.0.1:13030/api/v2/comments?…&site_name=wait4x.online&limit=0` 取 count 写 `dist` 内联进 props(或 src/data/ 快照 JSON,构建前生成);PostList.astro li 加 `<span class="cnt">N</span>` 与阅读时长角标(复用 `[...slug].astro:39` 的汉字/400 算法抽成 lib);首页"最近心跳"旁"最新评论"流(§4.3 两路择一)。
|
||||||
|
- 验收:构建日志含每篇 count 拉取;dist 列表 HTML 含 `class="cnt"`;断 Artalk(停容器 30s)build 仍成功且角标 `–`;playwright 列表页可见"N 条"与"~8 min"。
|
||||||
|
|
||||||
|
**G|gallery EXIF 元数据卡**(1d;风险:老图无 EXIF→字段缺省隐藏不渲染空行)
|
||||||
|
- 做法:photos_ingest.py 产物已含 exifr 数据,确认 JSON 字段(机型/焦段/光圈/ISO/时间/地点)→ gallery 详情/灯箱下加等宽字体表(mono,ink-3);nbfox 式"元数据即说明"。
|
||||||
|
- 验收:任一有 EXIF 的图渲染 6 字段中 ≥4;无 EXIF 图无空表壳;E2E 断言 mono 类名。
|
||||||
|
|
||||||
|
**H|home title/description 语义化 + RSS 可见性**(0.5d)
|
||||||
|
- 做法:home `<title>wait4x · 阅读 技术 摄影</title>` 级别语义;`/archives/` 顶部加一行 RSS 提示(xeiaso 式,链 /rss.xml 与 /en/rss.xml);description 按页级注入(BaseLayout 已有 prop,逐页补实)。
|
||||||
|
- 验收:playwright 断言每类页 title 唯一;archives 页含 rss 链接且 hreflang 正确。
|
||||||
|
|
||||||
|
**I|瞬间编号化 + 友链页**(各 0.5d)
|
||||||
|
- I1 moments 每条构建期序号 `#N`(diygod/qh1009/阮一峰三方验证的节奏感)+ 页顶"本周瞬间"聚合块(可选)。
|
||||||
|
- I2 `src/pages/links.astro` + `content/links.yaml`(name/url/desc/avatar),导航不加项(克制),入口放 about 与页脚。验收:links 页渲染全部条目且 a[href] 全 200(lint 断链覆盖)。
|
||||||
|
|
||||||
|
**J|偏好雏形 + craft 编年页(观望)**:reader/辉光/动效三 toggle 收进命令面板 `settings` 分组(不做独立面板 UI);terminal 页扩"工艺案例编年体"(rauno /craft 式)列为远期。邮件复制微交互(rauno "Copied")0.2d 可顺手做进 Footer。
|
||||||
|
|
||||||
|
## 6. 排期建议(三批)
|
||||||
|
|
||||||
|
- **立即(本周,SEO 解封是硬闸门)**:A → B → C(C 的生产冒烟依赖 A/B 上线)→ E;ESA purge 随 A 同日执行。依赖:A 需 homelab nginx 操作权限(用户或运维车道);ESA 控制台 purge 需用户操作单。
|
||||||
|
- **本月**:D(draft 路由)→ F(评论数,依赖 Artalk 在线策略确认)→ G(EXIF)→ H;尾单:删 media-sample。
|
||||||
|
- **观望(触发条件)**:I1/I2(内容节奏成型后)、J(terminal 页改版时合并)、Artalk 2.10.1 升级(下个维护窗口)、Newsletter(RSS 订阅量证明需求后)、GitHub PAT(热力图真实数据,用户素材依赖,scratchpad 在案)。
|
||||||
|
- 用户操作单(非代码):① ESA 全站 purge + HTML TTL 调整 + /artalk/api 绕缓存规则复核;② GitHub PAT(热力图);③ nginx 重载窗口确认。
|
||||||
|
|
||||||
|
## 7. 明确不做的(与灰阶克制冲突,v1 P2 续封 + 新增)
|
||||||
|
|
||||||
|
- 背景音乐/一言/背景特效(v1 P2,innei 同款):稀释终端隐喻一致性。
|
||||||
|
- 首页大图专题卡/瀑布流重化(v1 P2,nbfox 同款):内容电商型视觉,与"灰阶骨架×分轨正文"定稿相悖。
|
||||||
|
- 常驻浮动 TOC/阅读进度大改(v1 P2):现 .toc 桌面 sticky 已够,长文少。
|
||||||
|
- **新增:评论数实时化/SWR**(构建期快照已够,实时=引入运行时查询+ESA 缓存语义复杂度)。
|
||||||
|
- **新增:AI persona 注入彩蛋进正式页**(diygod 同款虽有趣,但正式内容层注 prompt 有被搜索引擎误读风险——只进 terminal 彩蛋页)。
|
||||||
|
- **新增:独立设置面板 UI**(innei 式面板):命令面板分组已覆盖收纳需求,独立面板是 UI 负担。
|
||||||
|
|
||||||
|
## 附:本轮实测命令与产物索引
|
||||||
|
|
||||||
|
- 生产:`curl -sI https://wait4x.online/{,archives/mian-sha/,robots.txt,admin/}`(noindex 头、404、30 天缓存头);`/sitemap-0.xml` 34 URL 全 200。
|
||||||
|
- 本机:`/etc/nginx/snippets/esa-origin-ssl.conf`、`sites-enabled/default:92`、`docker_apps/artalk/data/artalk.yml`、`127.0.0.1:13030/api/v2/comments?…` 响应原文。
|
||||||
|
- 外站快照:/tmp/v2/*.html(会话级):rauno.me(含 /craft /projects)、diygod.cc(含 /posts)、xeiaso.net(含 /blog)、shud.in、vincentwill.com、sabrinacodes.com、artalk.org(易主证据);brianlovin.com=Vercel 挑战页(失败标注)。
|
||||||
|
- 仓库:BaseLayout.astro / locale.ts / PostList.astro / CommandPalette.astro / ArtalkComments.astro / [slug] 与 [...slug].astro / archives/index.astro / build.sh / global.css / tokens.css。
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# 博客美化专项方案(视觉打磨批次)
|
||||||
|
|
||||||
|
> 侦察员 @agent-591c3f | 2026-10-26
|
||||||
|
> 证据链:仓库源码逐文件审计(tokens/global.css + 9 组件 + 2 文章页)× design/mockup.html 丙路线对照 × 本地重建 34 页实测(dist 内 Shiki 内联样式实抓)× WCAG 对比度数学计算 × 5 站补充抓取。
|
||||||
|
> 截图证据床 /tmp/beauty-shots/(22 张,1280×900 fullPage,SW 阻断,gz-server 18800 测试床):现状 12 张 `{home,reading,tech,books,gallery,moments}-{light,dark}.png`;mockup 基线 10 张 `mock-{home,reading,tech,gallery,moments}-{light,dark}.png`。
|
||||||
|
> 纪律分工:token 值/选择器/对比度数字=本报告结构性结论;"观感好坏"=⚠视觉待验,交视觉模型按 §5 问题清单复核,卡片产出不受阻。
|
||||||
|
|
||||||
|
## 0. 补充探索(每站聚焦视觉细节)
|
||||||
|
|
||||||
|
**抓取失败如实标注**:veselinov.pro、xivudong.me、photos.antfu.me 三域本机 DNS 无解析(getent 空记录,代理 7897 亦 000),已按任务预案换站并注明理由。
|
||||||
|
|
||||||
|
1. **rauno.me** ✅(HTML+2 CSS 实抓)——灰阶密度才是质感:stitches 体系 12 级灰(gray1–12 双主题 hsl 全套);间距阶严格 8px 步进(--space-1..11 = 8→88px);圆角三档 4/8/16+round;阴影三档全部 12% 黑透明(阴影当层级语言而非装饰);全站唯一动效曲线 `cubic-bezier(.2,.8,.2,1)`;focus-ring 变量化。字体=自研 X + JetBrains Mono + Georgia 点缀。
|
||||||
|
2. **paco.me** ✅(3 CSS 实抓)——灰阶走**黑/白 alpha 阶梯**(--black-a1..a12 = 5%–95% 共 12 档):灰由前景色透明度派生,暗色主题零重复维护;Newsreader 斜体做强调词是签名手法;字号全站仅 13/14/16/1.25rem/4rem 五档;边框唯一 `--border-thin:1px`;页边距 `max(24px, env(safe-area-inset-*))` 安全区与留白合一。
|
||||||
|
3. **mattdesl.com** ✅(换站理由见上)——全站等宽身份(BerkleyMono→Courier 回退)+ 97% 灰底/10% 墨字;标题 font-weight **normal**、统一 margin-top 1.5em——层级全靠字号与节奏不靠字重;标题小写化;标题锚 `#` 前缀常显;a:active 才变灰。与我们的 mono 轨同源:等宽当身份时,字重必须让位给间距。
|
||||||
|
4. **tw93.fun** ✅(替代 xivudong.me,DNS 失败)——body 15px,身份字体藏锋金楷 TsangerJinKai02(标题正文混排),中文个人站"手写体温"代表;内联 prefers-reduced-motion 全量豁免(与我们同款纪律);header 图标化导航。启示:中文身份字体走金楷/文楷一脉成立,我们 `--font-kai` 已在栈内,金句正是它的岗位。
|
||||||
|
5. **antfu.me/photos** ✅(photos 子域 DNS 失败,主域 /photos 200)——照片 img 带 inline `radial-gradient` **blur-up 占位**:图未加载先出主色近似色块,加载即有构图而非白洞;导航相机图标常驻;照片**无圆角无边框无阴影**直贴(摄影=内容本体不加框)。启示:图库质感优先做加载占位,而非边框阴影装饰。
|
||||||
|
|
||||||
|
## 1. 现状视觉审计(元素/现状/差距/目标)
|
||||||
|
|
||||||
|
| # | 元素 | 现状(源码事实) | 差距 | 目标 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | 间距阶 | `--space-1..6`=4/8/16/24/40/64,跳档不均(缺 12/32) | rauno 8px 步进全阶 | 主阶不动,补正文流距 token(K) |
|
||||||
|
| 2 | 标题节奏 | `.prose-* h2/h3` 仅 margin-top 1.8/1.5em,**无下距**(global.css:69-79) | 标题贴死正文;mockup h2 为 `1.8em 0 .6em` | h2/h3 补下距(K) |
|
||||||
|
| 3 | 正文段距 | `p+p{margin-top:.9em}` 双轨 6 处字面量 | 不可一处调 | `--flow-*` token 化(K) |
|
||||||
|
| 4 | 灰阶层级 | ink-1..4+line 五档;**ink-4(2.52:1) 用于 13px 日期/图注正文级文字**(PostList row-date、tl-date、g-note)不达 AA;无 border-strong 档 | ink-3 5.09:1 达标;ink-4 应退纯装饰位 | ink-4 停用于文字,新增 `--line-strong`(L) |
|
||||||
|
| 5 | 圆角 | 4/6/8/10/12/999 字面量散落 7+ 处组件 | 未 token | `--radius-s/m/l` 三档(O) |
|
||||||
|
| 6 | 动效时长 | 0.15s/160ms/0.4s/0.6s/2.4s 与 `--dur:350ms` 并存;**且 `.postlist .row` hover 1px 上浮是死规则**(global.css:196 类名应为 `post-list`,dist 实证从未生效) | 时长纪律散 + 微交互断线 | 修死规则 + `--dur-fast/slow` 补阶(O) |
|
||||||
|
| 7 | 书评纸面板 | `.prose-reading` 仅上下 hairline + bg(global.css:52-63) | mockup 丙=全边框圆角卡(pad 44/radius 14)形态差 | 两案裁定(Q)⚠视觉待验 |
|
||||||
|
| 8 | 代码块 | Astro 默认 Shiki github-dark,dist 实测内联 `background:#24292e` **压过 token `--code-bg:#0d1117`**;无语言标签;`.term-dots` 为死选择器 | token 与实际分裂 | Shiki css-variables 主题接管 + 灰阶窗条 + 语言标签(P) |
|
||||||
|
| 9 | 表格 | global.css **零表格规则**;内容 3 篇含 markdown 表(dist 实证 `<table>` 裸渲染) | UA 默认裸表 | 分隔线制表格样式(R) |
|
||||||
|
| 10 | 列表 | reset `*{margin:0}` 后无 prose 列表规则;内容 5 篇含列表 | li 间距仅靠行高,紧贴 | li+li 间距 + marker 色(R) |
|
||||||
|
| 11 | 金句 | `.pull-quote` 全仓无使用(死选择器);blockquote 只有 A 墨线模式 | D 金句模式从未落地 | 写作指南模板 + mood 注入(Q) |
|
||||||
|
| 12 | 引文规则 | global.css 与 [...slug].astro:172-177 **镜像双份** | 漂移风险 | 单源化(Q) |
|
||||||
|
| 13 | 图库衬底 | `.stage` 仅 container-type,无背景 | mockup `--stage:#f4f4f2` 面板+radius 14 | 两案裁定(N)⚠视觉待验 |
|
||||||
|
| 14 | 页面标题 | books/gallery/moments 用 `--text-h1` 27px | mockup g-title 20px/700 | 降一档 22px 或保持(K)⚠ |
|
||||||
|
| 15 | 首屏 | IdCard=60px 渐变头像(紫→accent 彩色占位)+21px 名字,slot 被 socials 占用,**无身份陈述行** | mockup hero 有 bio 15px 行 | M 两案 |
|
||||||
|
| 16 | 热力图 | ink 灰四档(组件注释明示决策) | mockup 原型为绿四档 | S 裁定 ⚠视觉待验 |
|
||||||
|
| 17 | 达标项(不动) | 灯箱 backdrop 95%、focus ring 全局 2px、::selection 半透明、明暗双套 tokens、reduced-motion 全豁免(row hover 上浮除外——行 6 实锤为死规则) | — | 保持 |
|
||||||
|
|
||||||
|
## 2. 美化卡片(K–S,续 v2 A–J)
|
||||||
|
|
||||||
|
### K|正文流距 token 化与标题下距
|
||||||
|
- **目标**:标题-正文、段落-段落的呼吸成为可调参数;治愈 h2 贴正文。
|
||||||
|
- **改法**(tokens.css + global.css):新增 `--flow-p: 0.9em; --flow-h2-pre: 1.8em; --flow-h2-post: 0.6em; --flow-h3-pre: 1.5em; --flow-h3-post: 0.45em;`;global.css 双轨 h2/h3 改用变量并补 margin-bottom;`p+p` 改 `var(--flow-p)`。页面标题(Books/Gallery/Moments h1.page-title)27px→`--text-h2`(22px) 降档对齐 mockup g-title(视觉待验§5-6)。
|
||||||
|
- **涉及**:`src/styles/tokens.css`、`src/styles/global.css`、三页组件 `page-title` 规则。
|
||||||
|
- **验收**:playwright 断言 `.prose-tech h2` computed marginBottom ≈13.2px(22×0.6)、marginTop≈39.6px;`:root` `--flow-p`===' 0.9em';books 页 `h1.page-title` fontSize==='22px'。
|
||||||
|
- **工作量**:0.5d。
|
||||||
|
|
||||||
|
### L|灰阶达标与 border-strong
|
||||||
|
- **目标**:全部正文级文字 ≥4.5:1(WCAG AA 实测数:ink-3 亮 5.09/暗 5.22 ✓,ink-4 亮 2.52/暗 2.67 ✗);分隔线获得强弱两档。
|
||||||
|
- **改法**:PostList `.row-date`、MomentsPage `.tl-date`、GalleryPage `.g-note`、index `.lab` 的 `color: var(--ink-4)`→`var(--ink-3)`;tokens 新增 `--line-strong: #d4d6db` / dark `#3d434f`,供 kbd 边框、chip 选中态(现用 ink-3 充当)、TOC 选中线。ink-4 仅保留纯装饰(track 前方块、beat-dot)。
|
||||||
|
- **涉及**:tokens.css、PostList.astro、MomentsPage.astro、GalleryPage.astro、index.astro。
|
||||||
|
- **验收**:`.row-date` computed color==='rgb(107, 110, 120)';对比度数学 5.09:1;`--line-strong` 存在且 chip:checked border-color===rgb(212,214,219)。
|
||||||
|
- **工作量**:0.5d。
|
||||||
|
|
||||||
|
### M|首屏叙事两案(⚠视觉待验 §5-1 定案)
|
||||||
|
- **案1 极简维持+身份陈述**(推荐先行):IdCard 结构改两行——名字 21px 下加 `.idcard-bio`(15px/ink-3,一句身份陈述,文案用户定);socials 移到 bio 行下;头像渐变占位灰阶化(`--mood-3/accent` 渐变→`ink-3→ink-1` 灰渐变,真头像接入前不破纪律)。`margin-block` 40→56px 加呼吸。
|
||||||
|
- **案2 hero 强化**:头像 72px、名字 `--text-h1` 27px、bio 一行 mono 14px tagline(如 `阅读 · 技术 · 摄影`),首屏区块底加 `--line` 分隔。视觉重量↑,与"骨架克制"张力↑。
|
||||||
|
- **涉及**:IdCard.astro、Avatar.astro、index.astro。
|
||||||
|
- **验收**:案1 `.idcard-bio` 存在且 fontSize==='15px'、color===ink-3;头像 svg 首个 stop 颜色为灰阶(无 hue)。
|
||||||
|
- **工作量**:案1 0.5d / 案2 1d。
|
||||||
|
|
||||||
|
### N|图片质感与灯箱纯度
|
||||||
|
- **目标**:照片墙获得"策展台"层次而非散图直贴;灯箱进入"暗房"纯度。
|
||||||
|
- **改法**:tokens 新增 `--stage: #f4f4f2` / dark `#101318`(mockup 丙同位);GalleryPage `.stage` 加 `background: var(--stage); padding: var(--space-3); border-radius: var(--radius-l, 14px)`(⚠两案:有衬底 vs 无衬底,§5-4);新增 `--radius-img: 10px` 统一 .shot/.thumb/.mom-fig/prose img 圆角(现 8/8/8/10 混用);灯箱 backdrop `rgba(8,10,14,.95)`→`.97` + 底条 mono 字距 `0.02em`;blur-up 占位列观望(antfu 手法需管线配合,非纯 CSS)。
|
||||||
|
- **涉及**:tokens.css、GalleryPage.astro、MomentsPage.astro、index.astro、global.css。
|
||||||
|
- **验收**:`.stage` computed backgroundColor==='rgb(244, 244, 242)';`:root` `--radius-img` 存在且 `.thumb` borderRadius==='10px';dialog#lb backdrop 不便断言→断言 CSS 源含 0.97。
|
||||||
|
- **工作量**:1d。
|
||||||
|
|
||||||
|
### O|微交互 token 化与 focus ring 收尾
|
||||||
|
- **改法**:先修死规则——global.css `.postlist .row`→`.post-list .row`(hover 1px 上浮即刻复活);tokens 新增 `--dur-fast: 150ms; --dur-slow: 600ms;`;替换字面量——row hover `0.15s ease`→`var(--dur-fast) var(--ease)`、GalleryPage `#lb-img` 160ms→`var(--dur-fast)`、`.bg-glow` 0.6s→`var(--dur-slow)`、rise-in 0.4s→`calc(var(--dur) + 50ms)`;圆角 token:`--radius-s:6px; --radius-m:10px; --radius-l:14px`,全局字面量归位;`:focus-visible` 补 `border-radius: 2px`(mockup:29,防直角 outline 扎圆角钮)。
|
||||||
|
- **涉及**:tokens.css、global.css、Header.astro、GalleryPage.astro、各组件 radius 字面量。
|
||||||
|
- **验收**:`.post-list .row` computed transitionDuration==='0.15s' 且 hover 后 transform==='matrix(1, 0, 0, 1, 0, -1)' 且 timingFunction 含 'cubic-bezier(0.19, 1, 0.22, 1)';`:focus-visible` computed borderRadius==='2px';`grep -c 'px' radius 字面量` 下降(构建审计)。
|
||||||
|
- **工作量**:0.5d。
|
||||||
|
|
||||||
|
### P|代码块窗口化(Shiki 接管 + 语言标签 + 灰阶窗条)
|
||||||
|
- **目标**:`--code-bg/--code-ink` token 恢复统治(现被 Shiki 内联 #24292e 压制);代码块获得窗口身份。
|
||||||
|
- **改法**:astro.config.mjs `markdown.shikiConfig.theme: 'css-variables'`(Shiki 官方主题,颜色全走 `--shiki-*` 变量);global.css 定义 `--shiki-default/--shiki-key/...` 对齐现有 github-dark 色系但 bg/ink 用 `var(--code-bg)/var(--code-ink)`,亮暗同底纪律恢复;`pre.astro-code[data-language]::after{content:attr(data-language); …mono 11px; color:#8b949e; text-align:right; display:block}` 语言标签(零 JS,dist 实证 data-language 属性已在);窗条三灰点用 `pre::before{content:'';display:block;width:10px;height:10px;border-radius:50%;background:#3f4650;box-shadow:16px 0 0 #3f4650,32px 0 0 #3f4650;margin-bottom:8px}`(灰阶三档 `#3f4650/#4a515b/#565f6a` 递进,macOS 彩点不采——红线);pre padding 顶加 `6px`。删除 `.term-dots` 死规则。**行号不做**(短命令块为主,320px 移动端宽度宝贵,见 §4)。
|
||||||
|
- **涉及**:astro.config.mjs、global.css。
|
||||||
|
- **验收**:dist 文章页 `pre.astro-code` 无内联 background-color(`getAttribute('style')` 不含 'background-color');pre computed backgroundColor==='rgb(13, 17, 23)';`getComputedStyle(pre,'::after').content` 非 'none' 且含 attr;`getComputedStyle(pre,'::before')` width==='10px'。
|
||||||
|
- **工作量**:1d。
|
||||||
|
|
||||||
|
### Q|书评轨:纸面板形态 + 金句实装 + 引文单源
|
||||||
|
- **目标**:书评页的"纸感"从色块升级为形态;D 金句模式真正可用。
|
||||||
|
- **改法**:
|
||||||
|
- 纸面板两案:**A 卡化**=`.prose-reading{border:1px solid var(--paper-line);border-radius:var(--radius-l);padding:var(--space-5) clamp(20px,4vw,32px)}`(mockup sheet 形态);**B 维持通栏** hairline 仅把上下线升级 `--line-strong`。(⚠§5-2 裁定)
|
||||||
|
- 金句:内容约定 HTML 直书 `<blockquote class="pull-quote"><p>…</p><span class="quote-cite">——作者《书名》</span></blockquote>`(写作指南已有 raw HTML 先例);pull-quote 样式修 62px→`3.2em` 相对化 + `--mood` 从文章 frontmatter 经 [...slug].astro `article style="--mood:…"` 注入(现 PostList 已用同源 mood,接线一致);docs/写作指南.md 加模板一节。
|
||||||
|
- 引文单源:删除 [...slug].astro:172-177 镜像规则,markdown 裸 blockquote 全靠 global.css。
|
||||||
|
- **涉及**:global.css、pages/archives/[...slug].astro、en 同文件、docs/写作指南.md。
|
||||||
|
- **验收**:`grep -c 'blockquote' src/pages/archives/[...slug].astro`===0;含 pull-quote 的页面 computed fontFamily 含 'WenKai'、`::before` fontSize==='64px'(20×3.2);A 案 `.prose-reading` computed borderRadius==='14px'。
|
||||||
|
- **工作量**:0.5d。
|
||||||
|
|
||||||
|
### R|表格与列表基建(从零到精)
|
||||||
|
- **改法**(global.css,作用于 `.prose-reading/.prose-tech`):表格 `table{border-collapse:collapse;width:100%;font-size:.92em;margin-block:1.2em} th{text-align:left;font-weight:600;color:var(--ink-1)?→书评轨用 paper-ink} th,td{padding:6px 12px 6px 0;border-bottom:1px solid var(--line)} tbody tr:last-child>*{border-bottom:0} td{font-variant-numeric:tabular-nums}`——分隔线制,**不做 Zebra**(灰阶密度已够,见 §4);列表 `ul,ol{padding-left:1.5em;margin-block:1.2em} li+li{margin-top:.35em} li::marker{color:var(--ink-3)}`;tech 轨 kbd 已有样式保持。
|
||||||
|
- **涉及**:global.css(含 3 篇含表内容无需改动)。
|
||||||
|
- **验收**:tech 文章页 `th` computed borderBottomWidth==='1px';`li+li` marginTop≈6.65px(.35em@19px);`li::marker` color==='rgb(107, 110, 120)'。
|
||||||
|
- **工作量**:0.5d。
|
||||||
|
|
||||||
|
### S|热力图色系裁定(⚠视觉待验 §5-5)
|
||||||
|
- **现状**:ink 灰四档(hm-l1..4=ink-4..1),组件注释明示"灰阶纪律"决策;mockup 原型为 GitHub 绿四档。绿=贡献量语义直觉(外部认知零成本),灰=骨架纯度。
|
||||||
|
- **改法(若裁定换绿)**:tokens 新增 `--hm-0:#ebedf0/--hm-1:#9be9a8/--hm-2:#40c463/--hm-3:#30a14e/--hm-4:#216e39` + dark 套(#22272e/#0e4429/#006d32/#26a641/#39d353),Heatmap.astro 四行替换。默认建议**维持灰阶**(热力图是骨架层不是内容层;绿色块会成为首页唯一大面积彩色,重心被劫持)。
|
||||||
|
- **验收**:若换绿 `--hm-4`===' #216e39' 且 `.hm-l4` backgroundColor 对应;维持则零改动,S 卡关闭。
|
||||||
|
- **工作量**:0.2d。
|
||||||
|
|
||||||
|
## 3. 优先级(视觉收益 × 工作量)
|
||||||
|
|
||||||
|
| 卡 | 视觉收益 | 工作量 | 象限 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| L 灰阶达标 | 高(可读性硬指标) | 0.5d | ★高收益低成本 |
|
||||||
|
| R 表格列表 | 高(3+5 篇内容直接受益,现裸样式) | 0.5d | ★ |
|
||||||
|
| K 流距/标题下距 | 高(全文页节奏) | 0.5d | ★ |
|
||||||
|
| M 案1 首屏陈述 | 高(第一屏人格) | 0.5d | ★ |
|
||||||
|
| P 代码块窗口化 | 高(技术轨视觉主件) | 1d | ★(成本略高但收益覆盖全部技术文) |
|
||||||
|
| Q 纸面板+金句 | 中高 | 0.5d | 次批 |
|
||||||
|
| O 微交互 token | 中(一致性负债清偿) | 0.5d | 次批 |
|
||||||
|
| N 图库质感 | 中 | 1d | 次批(含⚠两案) |
|
||||||
|
| S 热力图 | 低(大概率维持现状) | 0.2d | 随批 |
|
||||||
|
|
||||||
|
**前 5(高收益低成本)**:L → R → K → M案1 → P。合计 ≈3d,其中 L/R/K 纯 global+tokens 层,一次提交可收。
|
||||||
|
|
||||||
|
## 4. 明确不做(与灰阶克制红线冲突,含理由)
|
||||||
|
|
||||||
|
1. **玻璃拟态/大面积毛玻璃**:backdrop-filter 大面积使用与"纸墨实体感"冲突;微信内置浏览器兼容与滚动性能翻车史;Header 8px blur 轻量例外保留。
|
||||||
|
2. **渐变色文字/渐变大块**:全站唯一强调色(--accent 单色)是丙路线根基;渐变=多色并存,01 报告诊断的多色病复发。
|
||||||
|
3. **大圆角卡片风(≥16px)**:圆阶上限钉 `--radius-l:14px`;大圆角是消费 App 友好风,与终端/制版感相反。
|
||||||
|
4. **macOS 三彩圆点/彩色窗条**:代码窗条采灰阶三档(P 卡);彩点是全站唯一彩点的地位争夺者。
|
||||||
|
5. **噪点/纸纹覆盖层**(02 报告 Tier1 曾列,本批否决):纸感已由 `--paper` 米白承担;噪点在低分屏/微信内染摩尔纹,reduced-motion 需额外豁免,收益/成本比不成立。
|
||||||
|
6. **表格 Zebra 斑马纹**:隔行灰块密度过高,与分隔线骨感二选一取线(R 卡)。
|
||||||
|
7. **代码行号**:内容以短命令块为主,行号吃掉 320px 移动端宽度;Shiki attrs 可后补不锁死。
|
||||||
|
8. **行尾点号悬挂/首行缩进**:03 报告已证伪(无出版惯例/避头尾风险)。
|
||||||
|
9. **弹跳缓动/大面积视差/入场加码**:红线"不弹跳宁轻勿闹";现有 12px rise-in + 1px hover 是上限。
|
||||||
|
10. **自定义光标**:02 Tier3 已判,正文必须标准 I-beam。
|
||||||
|
|
||||||
|
## 5. 截图问题清单(⚠视觉待验——交视觉模型,附证据文件)
|
||||||
|
|
||||||
|
| # | 问题 | 证据对 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 首屏 M 案裁定:现状 IdCard+列表直连的密度 vs mockup hero 的呼吸,哪个更"被设计过"?案2 hero 是否过度? | home-{light,dark}.png ↔ mock-home-{light,dark}.png |
|
||||||
|
| 2 | 纸面板 Q-A/B:通栏 hairline(现状)vs 全边框圆角卡(mockup),哪个更"书页"?暗色下 paper #f8f6f1↔#201d18 转换是否自然? | reading-{light,dark}.png ↔ mock-reading-{light,dark}.png |
|
||||||
|
| 3 | 代码块 P:加灰阶窗条+语言标签后是否加分?(可先看 mockup term 窗对比现裸块) | tech-{light,dark}.png ↔ mock-tech-{light,dark}.png |
|
||||||
|
| 4 | 图库 N:stage 米灰衬底 vs 直贴,哪个衬托照片更好? | gallery-{light,dark}.png ↔ mock-gallery-{light,dark}.png |
|
||||||
|
| 5 | 热力图 S:灰四档 vs 绿四档的量级辨识与首页色彩重心 | home-{light,dark}.png(热力图区) |
|
||||||
|
| 6 | 页面标题降档(27→22px):books/gallery/moments 三页标题现值是否过重? | books-{light,dark}.png(无 mock 对照,独立判断) |
|
||||||
|
| 7 | 现状整体灰阶层级是否"够用"(rauno 12 档 vs 我们 5 档):列表行 hairline+日期档位是否单薄? | home-*.png 列表区 ↔ mock-home-*.png |
|
||||||
|
|
||||||
|
## 附:测试床与复现
|
||||||
|
|
||||||
|
- 截图环境:`npm run build`(34 页)→ `python3 scripts/build-perf/gz-server.py 18800 dist`(写稿时仍在跑,用后可杀:`pgrep -f gz-server` 取 PID kill);脚本 `/tmp/beauty-shots/shoot.cjs` 与 `shoot-mockup.cjs`(`NODE_PATH=$(pwd)/node_modules node …`)。
|
||||||
|
- 对比度计算:sRGB 相对亮度公式手算(§1-4 数值即此口径),可直接进验收注释。
|
||||||
|
|
||||||
|
|
||||||
|
## 6. 视觉裁定结果(glm-5.3-flash:max 视觉轨,2026-10-26 定案)
|
||||||
|
|
||||||
|
> §5 七问全部裁定完毕,⚠视觉待验标记以此为准失效;卡片 M/N/S 的两案之争已定。
|
||||||
|
|
||||||
|
裁定基准:灰阶克制(不彩色不弹跳宁轻勿闹)· 专业质感(被设计过≠被装饰过)· 双模式同判。
|
||||||
|
证据:/tmp/beauty-shots/ 现状 12 张 + mock 10 张全部读图;另对书评页头部(亮/暗)与热力图区(亮/暗)做 2× 特写裁片 4 张复核细节。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #1 首屏:现状 IdCard 直连 vs mock hero
|
||||||
|
**裁定:两案融合**(取 mock 的 tagline + 标题升一档;保留现状的信息密度与紧凑行距,不 banner 化)| 信心:高
|
||||||
|
理由:现状 IdCard(头像+名+3 图标)到 2026 列表之间无一句身份语,读感偏"档案页";mock hero 仅一行「写作者 · 读者 · 快门按压者,拥抱开源。」+社交图标,无插画无 CTA,克制成立——不过度、不空洞,且"被设计过"确实更强。
|
||||||
|
落地注意:tagline 控制在 ≤20 字级,移动端随 hero 收紧行高;禁加背景辉光、大字标语等 banner 元素。
|
||||||
|
|
||||||
|
## #2 书评纸面板:通栏 hairline vs 圆角纸卡
|
||||||
|
**裁定:选 mockup**(保留现状"标题区外置"结构;正文列由 hairline+微纸底升级为圆角米灰纸卡)| 信心:高
|
||||||
|
理由:现状正文列只有极淡暖底 + 顶部通栏 hairline,"一页纸"的物体感弱;mock 的细边框+圆角+米灰底让纸卡成立。暗色不冲突:现状暗列与 mock 暗卡都已转暖棕纸色(夜灯感),转换自然。
|
||||||
|
落地注意:本文实测 7000px 长文——卡片左右边框在视口内就是两根竖线,边框用 1px hairline 级、圆角收敛;移动端收 padding 保字宽。
|
||||||
|
|
||||||
|
## #3 代码块:裸块 vs 终端窗条
|
||||||
|
**裁定:选 mockup(窗条加分),但只给多行块** | 信心:高
|
||||||
|
理由:现状暗色技术页一排单行裸块退化为"纹理条",块与底仅靠细边框区分;mock 窗条提供 `bash · deploy@vps` 标题(是信息不是装饰)+ 一条灰阶色带,暗色下块轮廓明显更立。
|
||||||
|
落地注意:≥4 行的块才挂窗条,单行命令保持裸 pill;红黄绿三点可换灰阶三点——窗条+mono 标题已足够传达终端梗,守住红线更稳。
|
||||||
|
|
||||||
|
## #4 图库衬底:直贴 vs stage 米灰衬底
|
||||||
|
**裁定:两案融合**(亮色维持白底直贴;暗色给照片加轻衬底/描边)| 信心:中
|
||||||
|
理由:亮色现状就是干净的白盒画廊(21 张彩照自供色),灰衬板会成全页最大 UI 块、抢照片的戏;暗色实拍夜景照(明珠塔/暗楼群/暗日落)边缘融底,轻衬底恰好能救。mock 图库两图均截在动画中途(扇形堆叠+页脚透字),终态布局证据不足。
|
||||||
|
落地注意:暗色衬底用 ~4% 白底或 1px hairline,衬底圆角不得大于照片圆角,避免"框中框"。
|
||||||
|
|
||||||
|
## #5 热力图:灰档量级辨识 & 绿系利弊(只评不站队)
|
||||||
|
**裁定:双模式分别判——亮色灰档够用;暗色低两档实缺。绿系利弊各半** | 信心:高
|
||||||
|
理由:2× 特写下亮色灰档可辨、密集黑簇(周峰值列)一目了然;暗色最低两档与底色粘连、接近丢失。绿系=明度+色相双编码、暗色读数大增、geek 熟悉感;代价是成为全页最大色块,与照片条/彩点连成"颜色带",压灰阶骨架重心、违"宁轻勿闹"。
|
||||||
|
落地注意:若动,只动暗色或用去饱和苔绿(sage);亮色不建议换绿。
|
||||||
|
|
||||||
|
## #6 书架页标题 27px 是否过重
|
||||||
|
**裁定:维持现状(不过重,不降到 22px)** | 信心:中
|
||||||
|
理由:书架页唯一层级锚点,27px 与 17px 级书名卡的差距正是层级本身;降到 22 与卡题贴脸,稀疏页面会更散。暗色下白字光晕(halation)放大视觉重量,是"显得重"的主因。
|
||||||
|
落地注意:与图库/文章页标题保持同轨字号(一致性优先于单页微调);要轻就减字重(700→600)不减号。
|
||||||
|
|
||||||
|
## #7 灰阶层级:hairline+日期单薄?5 档够不够
|
||||||
|
**裁定:两案融合**(hairline 保留但更淡,行距向 mock 靠拢;5 档灰够用,不追 12 档)| 信心:高
|
||||||
|
理由:现状 hairline+右对齐日期是"档案感"资产,并不单薄;mock 全靠行距分组更透气,但长列表会失锚。页面可感知灰阶本就 4-6 阶,rauno 的 12 档是设计体系储备,页面级硬凑只会出"渐变灰"噪感。
|
||||||
|
落地注意:暗色 hairline 再降一档亮度(现状略亮、切分感偏强);行距 +2~4px 优先于删线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三行总评
|
||||||
|
1. **当前视觉成熟度:约 7.5/10**——已是完整自洽的灰阶克制系统(暖纸正文列/彩色书脊卡/mono 元数据/双模式纸色转换),缺口集中在 hero 身份语、暗色低灰阶辨识度、代码块"仪器感"。
|
||||||
|
2. **最值的一张卡:代码块终端窗条**——信息(上下文标题)+质感双收益、暗色收益最大、实现风险最低;书评纸卡次之。
|
||||||
|
3. **最危险的过度设计倾向:把画廊"道具化"**(stage 衬底+扇形堆叠动效是 mock 截图里已现形的方向)——与 hero banner 化、绿热力图同属"为设计感加戏",画廊应是白盒,动效层越薄越贵。
|
||||||
|
|
||||||
|
## 证据瑕疵备注
|
||||||
|
mock-home-dark 列表缺《海风中失落的血色馈赠》一行(截图态);mock 图库亮/暗两图均抓在动画中途。以上不影响各问主证据链,仅 #4 信心降为中。
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 灯箱美化探索:图片查看体验天花板五站研究(v2.1 输入)
|
||||||
|
|
||||||
|
> 只读调研 + 提案,不动代码。基线 = 灯箱 v2 车道(fit-contain / 滚轮锚点缩放 0.5–5x / 拖拽平移 /
|
||||||
|
> 双击 1x↔2.5x / caption+EXIF 分层 / ←→ 与触摸快滑),提案全部**叠加在 v2 之上,不推翻缩放引擎**。
|
||||||
|
> 配套可交互演示:`design/lightbox-mockup.html`(浏览器直接打开,交互为提案演示)。
|
||||||
|
> 标记:✅ = 静态抓取并核对源码/文档;◐ = 站点可达但查看器 JS 渲染,基于已知稳定形态记录。
|
||||||
|
|
||||||
|
## 一、五站笔记
|
||||||
|
|
||||||
|
### 1. PhotoSwipe(photoswipe.com)✅ 业界标杆 lightbox
|
||||||
|
|
||||||
|
- **A 开合过渡**:`showHideAnimationType: zoom`(默认)= 从缩略图原位起飞到舞台、关闭回落原位;
|
||||||
|
无缩略图时降级 fade;`prefers-reduced-motion` 自动 none。时长 333ms ×3(show/hide/zoom 同拍),
|
||||||
|
曲线 `cubic-bezier(.4,0,.22,1)`,双击缩放共用同曲线(节奏统一的关键)。
|
||||||
|
**cover 裁切缩略图路线**:`object-fit:cover` + `data-cropped` —— 我们的 masonry 正是这种缩略图。
|
||||||
|
- **B 舞台纯净度**:底色实色 + `bgOpacity`(默认 .8,透明度走 JS 合成不走 rgba,性能纪律);
|
||||||
|
文档明示**禁止 backdrop blur**(重性能坑)。`tapAction:'toggle-controls'`:单击即隐全部 chrome,纯看图。
|
||||||
|
- **C 信息层**:核心无 caption(a11y 原因删出 v5),官方 dynamic-caption 插件——"依剩余空放置于
|
||||||
|
图下或图侧":竖图自动把 caption 挪进侧边 letterbox,横图回底部。计数器 `n / N` 极简。
|
||||||
|
- **D 导航**:边缘箭头 + loop + `arrowKeys`;幻灯片间距 `spacing:.1`(10% 视口,换片有"路过"感);
|
||||||
|
`allowPanToNext`:放大态继续滑 = 翻页(先耗尽平移余量再走片)。
|
||||||
|
- **E 微交互**:`preload:[1,2]` 相邻预载;`preloaderDelay:2000`——加载指示 2s 后才出现(快图不打扰);
|
||||||
|
`maxWidthToAnimate:4000` 超宽图不做起飞动画(防掉帧)。图标自带描边阴影,深浅底通吃。
|
||||||
|
- **F 移动端**:`pinchToClose`(捏合关闭)、`closeOnVerticalDrag`(竖拖关闭)、双指缩放到 max 档;
|
||||||
|
`trapFocus + returnFocus`(焦点圈与归还,原生 `<dialog>` 免费同款)。
|
||||||
|
|
||||||
|
### 2. medium-zoom(francoischalifour.com)✅ Medium 的克制哲学
|
||||||
|
|
||||||
|
- **A**:点击 = 从缩略图位置 FLIP 放大到视口居中(margin 默认 0 贴边),关闭回落原位。
|
||||||
|
与 PhotoSwipe 同为"原位起飞",但它只有这一种动画——整库只做一件事。
|
||||||
|
- **B**:默认底色 `#fff`(Medium 白,页面即舞台);无任何 chrome、无 caption、无导航。
|
||||||
|
**克制到"查看器不存在"**:这就是它的设计立场。
|
||||||
|
- **C**:零信息层;`template` 选项可注入自定义布局(Facebook / Dropbox Paper 官方范例)——
|
||||||
|
证明"纯放大内核 + 自定义信息壳"是可行分层。
|
||||||
|
- **D**:无连拍导航(单图语义);`scrollOffset:40`:**滚动页面即关闭**,把"关"交给最顺手的手势。
|
||||||
|
- **E**:HD 模式 `data-zoom-src`:页内小图、开箱瞬间换高清(同宽高比前提),预览零等待。
|
||||||
|
- **F**:移动端点按开/点按关,无多余手势;无缩放(一档即满)。
|
||||||
|
|
||||||
|
### 3. Unsplash 照片页 ◐(curl 被 bot 墙拦截,未逐项复核)
|
||||||
|
|
||||||
|
- **A**:列表→详情是页面跳转非灯箱;大图页首屏即图主体,顶部细导航浮层滚动隐现。
|
||||||
|
- **B**:图外留白极小,信息全靠**悬浮层**:右侧竖排操作列(下载/尺寸选择/点赞/加合集/ⓘ),
|
||||||
|
不占图也不占底。
|
||||||
|
- **C**:ⓘ 召唤 Info 面板:相机/镜头/光圈/快门/ISO/焦距/发布时间/浏览数全量 EXIF;
|
||||||
|
平时全隐——"轻常驻 + 重召唤"的范本。下载默认给适配尺寸,原图藏在尺寸菜单。
|
||||||
|
- **D**:集合/相册内 ←→ 键连续浏览;相关照片网格殿后(发现回路)。
|
||||||
|
- **E**:渐进加载 + blur-up 占位是全站基建;L 键点赞的快捷键 affordance。
|
||||||
|
- **F**:移动端操作列收进底部栏,Info 面板变全屏抽屉。
|
||||||
|
|
||||||
|
### 4. 图虫 ◐(主站可达,查看器 JS 渲染未复核)
|
||||||
|
|
||||||
|
- **A**:信息流点击 → 全屏查看器直开(社区产品重内容直达,弱化过渡本身)。
|
||||||
|
- **B**:舞台全黑,chrome 依赖手势(边缘滑动)而非常驻按钮。
|
||||||
|
- **C**:摄影师视角分层:caption/描述 + **醒目完整 EXIF(机身/镜头/参数条)** + 作者卡(头像/
|
||||||
|
关注)+ 标签 + 社区数据(赞/藏)——信息密度最高,但靠 JS 分层收纳(常驻只留作者卡)。
|
||||||
|
- **D**:同组连拍左右滑 + 组内缩略导航;键盘支持弱于 PhotoSwipe/Flickr。
|
||||||
|
- **E**:加载有占位图(信息流即高清小图,开箱升级)。
|
||||||
|
- **F**:App 化手势(快滑翻页带跟手位移);双指缩放齐全。
|
||||||
|
- 教训面:信息全堆常驻会让暗房变"仪表盘"——我们只取它的 **EXIF 完整度**,不取密度。
|
||||||
|
|
||||||
|
### 5. Flickr(自选)✅ SSR 复核 + ◐ 视觉层
|
||||||
|
|
||||||
|
- **A**:照片 URL **直接开进 lightbox**(无中间页):暗房全屏沉浸,从网格进入有原位起飞感。
|
||||||
|
- **B**:舞台近纯黑(#212121 系);chrome 默认轻常驻:底部左侧 title/作者小卡(可隐),
|
||||||
|
其余全靠召唤。
|
||||||
|
- **C**:三段式:常驻小卡(title+作者)→ 右缘动作列(收藏/加画册/分享/下载尺寸/ⓘ)→
|
||||||
|
ⓘ 滑出右侧 EXIF 全量面板(机身/镜头/参数/软件/日期)。20 年沉淀的"常驻最轻、重的全召唤"。
|
||||||
|
- **D**:边缘 hover 箭头 + ←→/Esc/f/i 全键盘;f=收藏的键位文化。
|
||||||
|
- **E**:大图渐进加载;浏览数/评论在下折区(不抢舞台)。
|
||||||
|
- **F**:移动端 swipe 翻页、双指缩放、上滑收 chrome——三手势覆盖 95% 操作。
|
||||||
|
- 选它的理由:唯一同时满足"成熟产品 + SSR 可复核 + 键盘流 + 信息分层范本"。
|
||||||
|
|
||||||
|
## 二、差距矩阵(现状 v2 vs 天花板)
|
||||||
|
|
||||||
|
| 维度 | 我们现状(v2) | 天花板做法(出处) | 差距 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A 开合 | dialog 直开直关,零过渡 | 原位起飞+回落 333ms 单曲线(PS/MZ) | **大** |
|
||||||
|
| B 舞台 | backdrop .95 常驻;meta 恒占底条 | toggle-controls 沉浸(PS);轻常驻重召唤 | 中 |
|
||||||
|
| C 信息层 | 一层底条:caption+title+EXIF+动作平铺 | 双层:常驻轻卡 + 召唤 EXIF(FL/US);caption 就空放置(PS 插件) | 中 |
|
||||||
|
| D 导航 | ←→ 键/两侧钮/触摸滑动/loop ✅ | 同款 + spacing 换片节奏 + 放大态滑透翻页(PS) | 小 |
|
||||||
|
| E 微交互 | 切图 160ms 淡入;光标三态 ✅ | 相邻预载 [1,2]、2s 延迟加载指示、缩放档位语义(PS);HD 交换(MZ) | 中 |
|
||||||
|
| F 移动端 | 快滑翻页 + 双击缩放;**无双指缩放** | pinch 缩放/捏合关/竖拖关(PS)三件套 | **大** |
|
||||||
|
|
||||||
|
## 三、v2.1 增强提案(叠加式,不动 v2 引擎)
|
||||||
|
|
||||||
|
| # | 提案 | 做法 | 灰阶适配 | 工作量 | 优先级 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | **原位起飞开合** | 点击缩略图:FLIP 动画双盒(外盒=裁切窗从缩略图矩形→舞台矩形,内图=cover 几何→contain 几何同步过渡,等价 PS `data-cropped` 路线);300ms `cubic-bezier(.4,0,.22,1)`(与站 `--ease` 同族);关闭回落到当前缩略图位置;reduced-motion 直开直关 | 起飞的是图不是 chrome;backdrop 照旧恒深 | 0.5–1 天 | **P0** |
|
||||||
|
| 2 | **背景点击关闭** | 点击落在图外区域即关(PS `bgClickAction:'close'` 同款),Esc 保留 | 无新增视觉 | 0.5h | **P0** |
|
||||||
|
| 3 | **chrome 双层淡入 + 召唤** | 开箱:层1(关闭/箭头/计数)+140ms、层2(caption/EXIF/动作)+300ms 淡入;`i` 键或图上单击 toggle 全隐(toggle-controls);zoom>1 自动隐、回 1x 淡回 | 淡入只有 opacity,无位移无色彩 | 0.5 天 | P1 |
|
||||||
|
| 4 | **相邻预载** | 开箱后 idle 预载 ±1 张 fullSrc(PS `preload` 精神,我们取 [1,1] 克制档) | 纯网络层 | 数行 | P1 |
|
||||||
|
| 5 | **双指缩放(移动端)** | pointer events 双指距离比→锚点缩放(复用 v2 `zoomAt` 数学);v2 已有 `touch-action:none` 地基 | 同 v2 | 0.5 天 | P1 |
|
||||||
|
| 6 | **换片过渡** | 切片:旧图 140ms 淡出、新图自侧向 10% 位移淡入(PS `spacing:.1` 节奏);v2 换图已回正缩放,天然兼容 | 仅 opacity/transform | 0.5 天 | P2 |
|
||||||
|
| 7 | **缩放指示 + 首开提示** | 缩放停稳后右下 90% 淡入 `168%` 900ms 淡出;首开箱 hint 胶囊"滚轮缩放 · 双击放大 · Esc 关闭"2.6s 自隐 | mono 小字,与 EXIF 同语言 | 数行 | P2 |
|
||||||
|
| 8 | **长图侧置 meta** | 竖图(ratio<0.9)且视口 ≥820px:caption+EXIF 移入侧 letterbox(PS dynamic-caption"就空放置");横图维持底部条 | 文字色不变,只挪位 | 0.5–1 天 | P2 |
|
||||||
|
| 9 | **HD 交换** | zoom>1.5x 懒换 `/photos-originals/` 原图,<1.5x 回 1440(MZ `data-zoom-src` 模式);404 兜底留 1440 | 纯数据层 | 0.5 天 | P2 |
|
||||||
|
| 10 | **移动竖拖关闭** | zoom=1 时竖向拖 >80px 关闭并回落缩略图(PS `closeOnVerticalDrag`) | 同关闭回落 | 0.5 天 | P2 |
|
||||||
|
|
||||||
|
落地注:生产保持原生 `<dialog>`(免费 focus-trap/returnFocus,PS 同款能力);起飞动画在 dialog 内
|
||||||
|
用 fixed 双盒实现,mockup 用 fixed 覆盖层纯为演示等价几何。
|
||||||
|
|
||||||
|
## 四、明确不做清单(与灰阶克制冲突)
|
||||||
|
|
||||||
|
- 粒子/彩光/视差/自动播放/轮播——舞台只属于照片
|
||||||
|
- backdrop blur——PhotoSwipe 文档明示重性能坑,且暗底已够沉浸
|
||||||
|
- 缩略图条(thumbstrip)——21 张规模用不上,常驻 chrome +1 与克制冲突(>50 张再议)
|
||||||
|
- 社交分享按钮——PS v5 自己都删了;博客无此回路
|
||||||
|
- tile deep-zoom / 万像素级缩放——仓内照片 2400px 封顶,用不到
|
||||||
|
- 全屏按钮——PS v5 已删(浏览器 F11 更顺);Esc + 沉浸足够
|
||||||
|
- 音效、进度条、弹跳物理回弹——动效纪律:全站唯一曲线
|
||||||
|
- chrome 染 accent 色——灯箱恒深恒灰,强调色止于站内链接
|
||||||
|
|
||||||
|
## 五、验证记录
|
||||||
|
|
||||||
|
- PhotoSwipe:photoswipe.com 七页 + unpkg v5.4.4 `photoswipe.esm.js` defaultOptions 源码核对(✅)
|
||||||
|
- medium-zoom:francoischalifour.com options/HD 指南 + GitHub README(✅)
|
||||||
|
- Unsplash:curl 被 bot 检测墙拦截,标记 ◐;图虫:详情页 JS 渲染不可静态复核,标记 ◐
|
||||||
|
- Flickr:照片页 SSR 抓取成功(photo-model/person-model JSON 在源码中),视觉层标 ◐
|
||||||
|
- mockup:单文件无依赖;图片引用 `../content/photos/` 仓内实拍(相对路径,需与仓库同置打开)
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
# 页面后台化方案:内容 / 布局 / 样式的可编辑边界
|
||||||
|
|
||||||
|
> 调研 + 提案,不改代码。基线 = 工作树 2026-11-02(含在途未提交改动)。
|
||||||
|
> 口径:所有结论落到本仓文件/字段;"能编辑什么"和"不能编辑什么"同等重要。
|
||||||
|
> **在途关系**:工作树里另有一条未提交车道(`git diff --stat`:home.yaml / opensource.yaml / projects.ts / site-content.ts / content.config.ts / ProjectWall.astro / index.astro / en-index / tokens.css / global.css,共 306 增)已落地「首页区块开关 + 开源页可见性 + README 开篇」。本文把它当**既有事实**评估,只补缺口与验收,不重复设计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 现状盘点(文件级锚点)
|
||||||
|
|
||||||
|
| 关注点 | 现有实现 | 位置 |
|
||||||
|
|---|---|---|
|
||||||
|
| 集合/单例 schema | `siteHome` / `siteAbout` / `siteOpenSource` 三个 `glob(pattern 精确到单文件)` 单例 | `src/content.config.ts:78,114,119` |
|
||||||
|
| 后台字段 | `site_pages` 一集合三 files,字段与 schema 一一对应 | `public/admin/config.yml:110-260` |
|
||||||
|
| 文案访问层(含字段级回落) | `pick()/pickBi()` + `SITE_DEFAULTS` 逐字对齐历史文案 | `src/lib/site-content.ts:18-62` |
|
||||||
|
| markdown 渲染 | `createMarkdownProcessor()` **无参**(关键,见 §7) | `src/lib/site-content.ts:122-127` |
|
||||||
|
| 关于页结构 | h1 → intro(markdown) → `IdCard` → tracks(3 条硬编码) → build 段 | `src/components/pages/AboutPage.astro:1-70` |
|
||||||
|
| 关于页可变文案 | tracks/build 段全部来自 `t()` 字典(**非后台可改**) | `src/i18n/ui.ts:73-84` |
|
||||||
|
| 首页 | 结构化区块:IdCard/PostList/最新影像/Heatmap/ProjectWall/心跳 | `src/pages/index.astro`(zh/en 两份重复) |
|
||||||
|
| 开源页 | blurb + Heatmap + ProjectWall(limit 99) | `src/pages/opensource.astro` |
|
||||||
|
| 项目数据 | 构建期双源 fetch + 去重 + 过滤/排序 + 仓内 fallback JSON(现仅 1 条 `blog`) | `src/lib/projects.ts:24-134`、`src/data/projects-fallback.json` |
|
||||||
|
| README 开篇 | `GET /repos/wait4xx/wait4xx/readme`(Accept raw)+ `content/.github-readme.md` 缓存 | `src/lib/site-content.ts:204-256` |
|
||||||
|
| markdown 净化 | 仅文章管线挂了白名单(class/style/on*) | `astro.config.mjs:28`、`src/lib/rehype-class-whitelist.ts` |
|
||||||
|
| 设计 tokens | 全部 CSS 变量,明暗双套 | `src/styles/tokens.css`(124 行,含 dark 覆盖) |
|
||||||
|
|
||||||
|
**结论**:后台能力当前是「**平铺标量字段**」形态——能改文案与几个开关,不能改**结构、顺序、字段种类**。关于页的骨架(tracks/build 两段)完全在代码与 i18n 字典里,这正是用户"整页可编辑"诉求的断点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 需求拆解(可验收能力清单)
|
||||||
|
|
||||||
|
| 编号 | 能力 | 现状 | 缺口 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| C1 | 关于页**所有文案**可改(含 tracks/build 段) | intro 可改,其余在 `ui.ts` | 全部结构化区块未入 CMS |
|
||||||
|
| C2 | 关于页**结构可改**(增删段、调顺序、开关名片区) | 硬编码 | 无块模型 |
|
||||||
|
| C3 | 首页区块开关 / 条数 / 副标题 | 在途已实现(`sections.*`、`perTrackLimit`、`latestPhotos`、`subtitle`) | 缺"展示策略"类字段(见 §6) |
|
||||||
|
| C4 | 开源页"展示哪些项目" | 在途已实现(`mode/include/exclude/sort/perSource`) | 语义定稿 + 断言 + 现有 6 条截断副作用(见 §4) |
|
||||||
|
| C5 | 开源页开篇 = GitHub profile README | 在途已实现(`intro_from: github-readme`) | 缓存位置被 `git clean` 抹掉(见 §4.4) |
|
||||||
|
| C6 | **样式**可调(字体/字号/间距/灰阶/圆角) | 全在 `tokens.css` | 无数据层 |
|
||||||
|
| C7 | 双语 en 轨保真 | 字段级 `{zh,en}` + 回落 | 块模型需延续同规矩 |
|
||||||
|
| C8 | 构建期零运行时 + SEO/灰阶不回归 | 现状成立 | 需把"不回归"写成断言 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 五方案对比
|
||||||
|
|
||||||
|
| | A 结构化单例字段扩展 | B 块构建器 | C 富文本整页 | D 样式即数据 | E 混合(推荐) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 能力上限 | 低:只能改预定义字段的值 | 高:增删/排序/差异化结构 | 最高:任意内容 | 中:只动感官参数 | 高(结构)+ 中(局部自由) |
|
||||||
|
| 破版风险 | 极低 | 低(渲染器受控) | **高**:作者可写出非设计语言结构 | 中(对比度/尺度) | 低-中 |
|
||||||
|
| XSS 面 | 0(无 HTML) | 小(组件渲染,URL/图片字段受控) | **大**:`set:html` 直通 | 0 | 小(唯一自由区走净化管线) |
|
||||||
|
| 编辑体验 | 顺手但僵硬;20 字段后 config.yml 成清单墙 | 顺手(新增时选块型、拖拽排序) | 最自由但无护栏 | 顺手(几个 color/number) | 顺手(块为主,自由区为辅) |
|
||||||
|
| 工作量 | 0.5d/页 | 3-4d(渲染器 + 9 类块 + 后台 types) | 0.5d(已有 intro 通道)但治不住质量 | 1d | 4-5d(含 B+D+净化) |
|
||||||
|
| 维护成本 | 每加一项改两处(yml+schema) | 每加块型改三处(yml+schema+组件),但**互不干扰** | 内容问题无代码兜底,靠人审 | 用户改坏颜色需回滚 git | 同 B,另加 token 白名单 |
|
||||||
|
| 对我们仓的结论 | **只适合开关/条数**(首页现状即其上限形态),**不作为关于页主路线** | 关于页主力 | **不作为主路线**(灰阶/双语/SEO 三条红线都靠人守) | 采纳为**独立一层**(受限白名单) | **采纳** |
|
||||||
|
|
||||||
|
补充结论(逐条):
|
||||||
|
- **A 的极限量化**:`public/admin/config.yml` 现已 260 行、覆盖 4 集合。关于页若走 A(名片/技能卡/时间线各 4-6 字段),预估 +60 行 yml、+40 行 schema、+30 行组件分支;且"某段删掉"只能靠 `enabled: false` 假装,顺序固定。→ 只保留给首页(首页是聚合页,区块种类稳定)。
|
||||||
|
- **B 的关键前提**:Sveltia 0.252 支持 **`list` 的 `types` + `typeKey`**(异构块),证据见 §3.4;无此能力则 B 退化成"同构对象列表"(每块字段全冗)。
|
||||||
|
- **C 的现实定位**:`about.yaml` 已有 markdown 整块通道(`intro`),可立刻扩成"多段 markdown 长文"。问题:长文无法表达 tracks 卡片(图标+双行)、无法控制段间节奏,且 raw HTML 通道当前**未净化**(§7.1)——直接放开等于开洞。
|
||||||
|
- **D 的边界**:可调项必须白名单化(约 12 个),不是整份 tokens.css;`--measure`(9 处引用)、`--accent`(38 处)属高放大倍率旋钮,见 §3.6。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 推荐方案 E:结构与落点
|
||||||
|
|
||||||
|
### 3.1 四层数据分层
|
||||||
|
|
||||||
|
| 层 | 载体 | 谁能改 | 例子 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| L1 页面骨架 | `content/site/*.yaml` 的 `blocks: []` | 后台(增删排序) | 关于页整页 |
|
||||||
|
| L2 区块字段 | 每块 `type` 决定字段集(zod discriminatedUnion) | 后台 | prose / tracks / cards / timeline / quote / figure / linklist / cta / divider |
|
||||||
|
| L3 局部自由区 | `prose` / `richtext` 块内的 markdown(受白名单类 + tag 白名单) | 后台 | 一段带列表的说明 |
|
||||||
|
| L4 感官参数 | `content/site/theme.yaml` → 构建期生成 CSS 变量 | 后台(白名单旋钮) | 正文字号、圆角、强调色 |
|
||||||
|
|
||||||
|
### 3.2 yaml 结构总览(一眼看全)
|
||||||
|
|
||||||
|
```
|
||||||
|
content/site/
|
||||||
|
├── home.yaml # 已有:tagline/avatar/favicon/socials/subtitle/latestPhotos/perTrackLimit/sections/*
|
||||||
|
│ # 新增:heroStyle / projectsLimit / photosSource / postsTracks / announcement(markdown)
|
||||||
|
├── about.yaml # 已有:intro{zh,en}(保留为兼容字段,见 §5.5)
|
||||||
|
│ # 新增:layout{showIdCard,container} + blocks:[]
|
||||||
|
├── opensource.yaml # 已有(在途):blurb/blurb_after/repo_url/repo_label/pinned/mode/include/exclude/sort/perSource/intro_from
|
||||||
|
│ # 新增:projectsTitle / showHeatmap / intro_fallback_blurb(README 取不到时的专用文案)
|
||||||
|
└── theme.yaml # 新增:白名单感官参数(§3.6)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 组件与读取层落点(写代码时照此建文件)
|
||||||
|
|
||||||
|
| 落点 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/page-blocks.ts`(新) | `getAboutBlocks(locale)`:读 `siteAbout` → 过滤 `enabled:false` → 未知 `type` 跳过 + `console.warn` → 文本字段 `pickBi` 回落 zh → 长度上限(块数 ≤ 24、单块文本 ≤ 4000 字) |
|
||||||
|
| `src/components/blocks/BlockRenderer.astro`(新) | `switch (block.type)` → 渲染子组件;统一外壳 `<section class="blk" data-block={block.type}>`(**data 属性是 E2E 断言的抓手**) |
|
||||||
|
| `src/components/blocks/{Prose,Tracks,Cards,Timeline,Quote,Figure,LinkList,Cta,Divider}.astro`(新) | 每类一个组件,样式从 `AboutPage.astro` 现状 `<style>` 段搬迁(`.sec-title`/`.track-row`/`.track-icon` 原样保留) |
|
||||||
|
| `src/components/pages/AboutPage.astro`(改) | 保留 `h1` + 可选 `IdCard`;`blocks` 为空 → `intro` markdown → 若再空则 `SITE_DEFAULTS.aboutIntro`(**零回归三级回落**) |
|
||||||
|
| `src/content.config.ts`(改) | `siteAbout` schema 扩展:`blocks` 用 `z.array(z.discriminatedUnion('type',[...]))`,图片字段用 `image()` |
|
||||||
|
| `public/admin/config.yml`(改) | `site_pages → about` 加 `blocks` 的 `list`+`types` 声明,`typeKey: type` |
|
||||||
|
| `src/lib/site-content.ts`(改) | `renderMarkdown` 补 rehype 插件(§7.2 一笔修两洞) |
|
||||||
|
| `src/lib/theme.ts`(新) | `getTheme()` + `themeVarsCss()` 生成 `:root:root{…}` 字符串 |
|
||||||
|
| `src/layouts/BaseLayout.astro`(改) | head 内联 `<style set:html={themeVarsCss()}>`(构建期静态,零运行时);`main` 容器档位由页面 `layout.container` 决定(`.container` / `.container-wide`,`global.css:43-44`) |
|
||||||
|
|
||||||
|
### 3.4 Sveltia 0.252 能力边界(本仓 bundle 实测取证)
|
||||||
|
|
||||||
|
证据文件:`public/admin/index.html`(pinned 本地 bundle 0.252.0,2.5MB)。
|
||||||
|
|
||||||
|
| 能力 | 结论 | 取证 |
|
||||||
|
|---|---|---|
|
||||||
|
| `list` 异构块(`types` + `typeKey`) | **支持**:新增条目时弹「Select List Type」,按 `name` 选型 | bundle 内 i18n 键 `select_list_type`、`list_type` 组件、`{types, typeKey='type'}` 解析路径 |
|
||||||
|
| 内建 widget 全集 | `boolean/color/code/compute/datetime/file/hidden/image/keyvalue/list/map/markdown/number/object/relation/richtext/select/string/text/uuid` | bundle 中 `ok=[…]` 常量 |
|
||||||
|
| `richtext`(所见即所得) | 支持,**落盘默认 markdown**(`field_defaults.richtext.format`,非 html) | bundle 中 `t===\`html\`?\`html\`:\`markdown\`` |
|
||||||
|
| 嵌套深度硬限制 | **未发现**:bundle 里唯一 `nestedDepth` 属 folder 集合路径深度,与字段嵌套无关 | 全量 grep |
|
||||||
|
| `list` 内再放 `list` | 类型表未排除,但**无保证** → 必须做一次 10 分钟后台实测冒烟(见 §8 P1 验收) | — |
|
||||||
|
| 拖拽排序 / 每块 summary | 支持(`summary` 模板 + reorder) | 现网 posts/moments 已在用 |
|
||||||
|
|
||||||
|
编辑体验判断:`blocks` 用 `types` 后,后台表现为「+ 添加 → 选块型 → 填该块字段」,列表可拖拽,每块可折叠。**代价**:字段标签必须写成中文短标签,否则块型选择弹窗会退化成英文 key。
|
||||||
|
|
||||||
|
### 3.5 双语策略(三条路线,选 A)
|
||||||
|
|
||||||
|
| 方案 | 做法 | 判断 |
|
||||||
|
|---|---|---|
|
||||||
|
| **A 字段级 `{zh,en}`(选)** | 每个文本字段仍是 `{zh,en}`,`en` 留空回落 `zh` | 与现状全站一致(`bilingual` schema + `pickBi`),后台一屏可见双语对照;缺点是块内字段数 ×2 |
|
||||||
|
| B 两套块数组(`blocks_zh`/`blocks_en`) | 结构可漂移 | **否决**:块数与顺序会分叉,维护地狱 |
|
||||||
|
| C en 构建期自动翻译 | 复用 `scripts/translate/` | **否决**:违反构建确定性(产物含未提交内容),且页面级译文无人校对 |
|
||||||
|
|
||||||
|
配套:`pickBi` 之外新增构建期**缺译清单**(§7.3 断言 #8),把"哪些 en 字段回落成了 zh"打成 WARN,避免静默全英文页变中文。
|
||||||
|
|
||||||
|
### 3.6 样式即数据(L4)边界表
|
||||||
|
|
||||||
|
| 允许(12 项) | 形态 | 风险控制 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--accent` | color(明/暗两值) | 构建期算对比度(对 `--bg`)< 4.5 → 报警并回落默认(红线"全站唯一强调色"只能调色相,不能变多色) |
|
||||||
|
| `--radius-img` | number 4-16 | 钳制(08 文档裁定圆角上限 14) |
|
||||||
|
| `--text-h1/h2/h3` | number 22-32 | 单调性校验 h1>h2>h3,否则回落 |
|
||||||
|
| `--prose-size-reading/tech` | number 16-22 | 钳制 |
|
||||||
|
| `--leading-reading/leading-tech` | number 1.5-2.0 | 钳制 |
|
||||||
|
| `--space-3/4/5/6` | number 8-80 | 单调递增校验 |
|
||||||
|
| `--flow-p` / `--flow-h2-post` | number 0-2(em) | 钳制 |
|
||||||
|
| `--dur` | number 150-600(ms) | 钳制 |
|
||||||
|
| `--font-*` | select(内置四族,不含任意字体名) | **必须**枚举:任意字体名会让 614 个 @font-face 瘦身成果失效 |
|
||||||
|
|
||||||
|
**禁止**:`--measure`(9 处引用,改版心会连带 TOC/图库/首页栅格)、`--bp-*`(断点是媒体查询字面量)、`--code-bg/--code-ink`(Shiki `css-variables` 主题在 `astro.config.mjs` 之外耦合)、`--paper*`(书评纸墨是分轨身份)。
|
||||||
|
**注入方式**:`BaseLayout` head 内联 `<style>`,选择器用 `:root:root{…}`(0,2,0)与 `[data-theme="dark"]:root:root{…}`(0,3,0)压过 `tokens.css` 的 `:root`(0,1,0)/`[data-theme="dark"]`(0,1,0)——**不能靠 DOM 顺序**(`global.css` 的 `<link>` 注入位置与 inline `<style>` 的相对次序不可控)。断言:明暗两态 `getComputedStyle(html).getPropertyValue('--accent')` 都等于 yaml 值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 开源页:项目可见性 + GitHub README
|
||||||
|
|
||||||
|
### 4.1 在途现状(已实现,不再设计,只定稿语义)
|
||||||
|
|
||||||
|
- 字段:`mode(all|only|exclude)` / `include[]` / `exclude[]` / `sort(pinned-first|stars|updated)` / `perSource(both|github|gitea)` / `pinned[]` / `intro_from(off|github-readme)`(`content/site/opensource.yaml`、`src/content.config.ts:128-138`、`src/lib/site-content.ts:180-201`)。
|
||||||
|
- 过滤实现:`perSource` → `mode` → `sort`,**不截断**,上限交给调用方(`src/lib/projects.ts:99-134`;这修掉了旧 `orderPinned` 内 `.slice(0,6)` 导致 `/opensource` 文案说"全部仓库"实际只显示 6 条的旧账)。
|
||||||
|
- 匹配键 = **仓库短名**(`name`),跨源同名先去重(同名优先 gitea 叙事,star 取 GitHub 真值,`src/lib/projects.ts:76-88`)。
|
||||||
|
|
||||||
|
### 4.2 语义定稿(必须写进后台 hint,否则用户会填错)
|
||||||
|
|
||||||
|
1. `mode: only` 时 **忽略** `exclude`;`mode: exclude` 时**忽略** `include`(v1 不做两者求交)。
|
||||||
|
2. `include/exclude` 填不存在的名字 = 静默无效(后台无下拉可选,因为仓库清单是构建期才拿到);→ 建议 P2 阶段把 `widget: string` 改成 `widget: select, options` 的**静态镜像**(yml 里手抄一份当前仓名)不划算,**v1 保持手填 + 构建期 WARN 打印未命中的名字**。
|
||||||
|
3. `sort: pinned-first` 仍是默认(首页要"置顶在前"叙事),`sort: stars|updated` 时 `pinned` 让位(现实现如此:`arrange` 里 stars/updated 分支直接 sort,不看 pinned)。
|
||||||
|
4. 首页固定 `limit=6`,开源页 `limit=99`(`ProjectWall.astro` props)。若仓库总数 >99 需调大,非阻塞。
|
||||||
|
|
||||||
|
### 4.3 是否用 GitHub profile README 作为开源页开篇 —— **可行,推荐按"默认关、用户开"上线**
|
||||||
|
|
||||||
|
| 评估项 | 结论 |
|
||||||
|
|---|---|
|
||||||
|
| 接口 | `GET /repos/wait4xx/wait4xx/readme` + `Accept: application/vnd.github.raw` = 一次请求返回 markdown 原文(现实现已如此,`site-content.ts:235-238`) |
|
||||||
|
| 限流 | 认证 5000 req/h;构建按 push 触发(webhook ~10 秒级),每天最多几十次 → 无压力。匿名 60 req/h 会更紧张,**必须走 token** |
|
||||||
|
| token | 复用 `~/docker_apps/blog/github-token.txt`(`src/lib/heatmap.ts:21` 同一份,0600,read 权限足够);**缺位是合法降级路径**,不报错只 WARN |
|
||||||
|
| CORS | **不涉及**:node 侧构建期 fetch,不是浏览器请求。(若哪天想做成客户端拉取,CORS 会直接堵死,且违反"构建期零运行时") |
|
||||||
|
| 许可 | 内容属本人 profile 仓(`wait4xx/wait4xx`),无第三方版权问题;README 若嵌第三方徽章图 = 外链图片,**构建期不下载、浏览器直连**,需接受 |
|
||||||
|
| 渲染质量 | README 常见 `<p align="center">`、徽章 `<img>`、`<details>`、表格、外链图片的**相对路径**(渲染到 `/opensource/` 下会 404)。现状只做了基础排版类(`opensource.astro` 的 `.readme-intro :global(...)` 段) |
|
||||||
|
| 破版风险 | 中:README 常含居中对齐/大标题/emoji 徽章,与"灰阶克制 + 版心 720px"张力大。→ 建议 **P5 做一次视觉复核**,必要时给 `.readme-intro` 加 `img{max-height}`、`h1{display:none}` 折叠 |
|
||||||
|
|
||||||
|
**缓存实现缺陷(必改)**:`README_CACHE = 'content/.github-readme.md'`(`site-content.ts:204`)写在仓内,而构建仓每次 `git fetch && reset --hard && git clean -fd`(`infra/deploy/build.sh:26`)会**抹掉未入仓缓存**——即"网络失败回落缓存"这层在生产构建里**永远命中不了**,实际回落链退化为"网络 → 无 → yaml blurb"。
|
||||||
|
修法(二选一,推荐后者):
|
||||||
|
- (a) 把缓存文件提交入仓,靠人工/定时提交更新 —— 引入"构建机写仓"脏状态,与"构建仓硬跟 origin/main"纪律冲突;
|
||||||
|
- (b) **移到仓外**:`~/docker_apps/blog/github-readme-cache.md`(与 token 同目录,天然持久,不被 clean 波及),TTL 24h(符合 GitHub API 缓存建议),并在 `writeFileSync` 失败时只 WARN。
|
||||||
|
|
||||||
|
### 4.4 失败回落矩阵(开源页)
|
||||||
|
|
||||||
|
| 失败点 | 现状行为 | 目标 |
|
||||||
|
|---|---|---|
|
||||||
|
| token 缺位 | WARN → 缓存 → null → 用 yaml blurb | 同(缓存改仓外后才有意义) |
|
||||||
|
| README 404 / 仓不存在 | WARN → 缓存 → blurb | 同 |
|
||||||
|
| 网络超时 | 现实现**无 AbortSignal 超时**(`projects.ts` 有 10s,README 没有)→ 挂起风险 | 补 `AbortSignal.timeout(8000)` |
|
||||||
|
| README 为空 | WARN → 缓存 → blurb | 同 |
|
||||||
|
| gitea 源整站 522(历史事故) | `Promise.allSettled` 单源失败不影响另一源 | 保持 |
|
||||||
|
| 双源全失败 | 仓内 `projects-fallback.json`(**现仅 1 条**) | 建议 P5 用一次真实构建产物回填 5-8 条 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关于页整页化
|
||||||
|
|
||||||
|
### 5.1 现有结构 → 区块映射(逐元素,无遗漏)
|
||||||
|
|
||||||
|
| 现有元素 | 文案来源 | 现状可否后台改 | 对应块型 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `<h1>关于` | `ui.ts:73` | ✗ | 页面 `title` 字段(或沿用字典) |
|
||||||
|
| intro 多段 markdown | `about.yaml intro.{zh,en}` | ✓ | **prose 块**(`intro` 字段保留为兼容回退) |
|
||||||
|
| 名片区(头像+名字+tagline+社交) | `IdCard` + `home.yaml` | 部分 ✓(tagline/socials 来自 home) | **layout.showIdCard 开关**(不拆成块:头像数据单一事实源在 home.yaml) |
|
||||||
|
| 「兴趣版图」标题 | `ui.ts:74` | ✗ | tracks 块的 `title` |
|
||||||
|
| 3 条 track(图标+名称+描述) | `ui.ts:75-80` | ✗ | tracks 块的 `items[]`(icon 键 + name/desc 双语) |
|
||||||
|
| 「本站如何构建」标题+正文 | `ui.ts:81-83` | ✗ | prose 块的 `title` + `body` |
|
||||||
|
|
||||||
|
→ **覆盖现有 About 只需 3 个块**(prose + tracks + prose)。其余块型是为"用户以后想加东西"准备的(§5.2)。
|
||||||
|
|
||||||
|
### 5.2 区块类型清单(v1 九类,够用且不失控)
|
||||||
|
|
||||||
|
| type | 字段(* = `{zh,en}`) | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `prose` | `title*?` `body*`(richtext/markdown) `width(narrow|full)` | 主力自由区(受 §7 净化) |
|
||||||
|
| `tracks` | `title*` `items[]{icon, name*, desc*}` | 覆盖现有兴趣版图;`icon` 走内置键白名单 |
|
||||||
|
| `cards` | `title*?` `columns(2|3)` `items[]{title*, desc*, href?, tag?}` | 技能/资源卡(复用 `.track-row` 视觉语言) |
|
||||||
|
| `timeline` | `title*?` `items[]{time, title*, desc*?}` | 经历/项目时间线(复用 `MomentsPage` 的 `.tl-*` 竖线语言) |
|
||||||
|
| `quote` | `text*` `cite?` | 复用 `.quote-line` + `--font-kai`(08 文档 D 金卡) |
|
||||||
|
| `figure` | `image`(image()) `caption*?` `alt` | 必须走 Astro 图片管线 |
|
||||||
|
| `linklist` | `title*?` `items[]{label*, href, note*?}` | 外链清单(比 markdown 裸链接可控) |
|
||||||
|
| `cta` | `text*` `href` `label*` | 单按钮行动点(克制:同屏最多 1 个) |
|
||||||
|
| `divider` | `style(line|space)` | 节奏控制(非装饰色彩) |
|
||||||
|
|
||||||
|
**不做**(放进 §9):`html` 块、`style` 块、`columns[grid]` 栅格构建器、任意 svg 块。
|
||||||
|
|
||||||
|
### 5.3 示例 yaml(直接照抄可跑通现有关于页)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# content/site/about.yaml
|
||||||
|
layout:
|
||||||
|
showIdCard: true
|
||||||
|
container: narrow # narrow=.container(720) / wide=.container-wide(+200px)
|
||||||
|
# 以下为可选:不填则沿用标题「关于」
|
||||||
|
# title: { zh: 关于, en: About }
|
||||||
|
blocks:
|
||||||
|
- type: prose
|
||||||
|
title: { zh: '', en: '' } # 留空不渲染标题
|
||||||
|
width: narrow
|
||||||
|
body:
|
||||||
|
zh: |
|
||||||
|
写作者 · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
|
en: |
|
||||||
|
Writer · Reader · Shutter-presser, embracing open source.
|
||||||
|
- type: tracks
|
||||||
|
title: { zh: 兴趣版图, en: Interests }
|
||||||
|
items:
|
||||||
|
- icon: book
|
||||||
|
name: { zh: 阅读, en: Reading }
|
||||||
|
desc: { zh: 文学与戏剧为主,读到哪写到哪。, en: 'Mostly literature and drama; I write as I read.' }
|
||||||
|
- icon: terminal
|
||||||
|
name: { zh: 技术, en: Tech }
|
||||||
|
desc: { zh: Linux、容器与自托管……, en: 'Linux, containers, and self-hosting — …' }
|
||||||
|
- icon: camera
|
||||||
|
name: { zh: 摄影, en: Photography }
|
||||||
|
desc: { zh: 街拍与山野……, en: 'Street and landscapes with a mirrorless camera, …' }
|
||||||
|
- type: prose
|
||||||
|
title: { zh: 本站如何构建, en: How this site is built }
|
||||||
|
body:
|
||||||
|
zh: 静态生成,构建即发布;字体自托管,评论自托管,全文索引在构建期生成。
|
||||||
|
en: Statically generated, publish-on-build; self-hosted fonts, comments, and a build-time search index.
|
||||||
|
```
|
||||||
|
|
||||||
|
`icon` 键白名单(代码侧注册,同 `SocialLinks` 纪律 `src/components/SocialLinks.astro:7-19`):`book / terminal / camera / code / git / rss / email / github / bilibili / x / star / link`。后台填表外的键 → 渲染纯文字(不破版)。
|
||||||
|
|
||||||
|
### 5.4 视觉保留(不动设计体系)
|
||||||
|
|
||||||
|
- 类名与样式从 `AboutPage.astro:70-143` **原样搬迁**到 `src/components/blocks/*.astro`:`.sec-title`(mono 14px/ink-3/0.08em)、`.track-row`(1px `--line` + radius 10px + `--space-3/4` 内距)、`.track-icon`(36×36 / `--bg-soft`)。
|
||||||
|
- 新增块**只允许**用既有 token;新组件不得引入新颜色字面量(用 `--ink-*`/`--line`/`--bg-soft`)。这条要在评审时按 `grep '#[0-9a-fA-F]\{6\}' src/components/blocks/` 结果把关(`ProjectWall` 的语言色点是既存例外,不得扩散)。
|
||||||
|
- 关于页 `--measure` 版心不变(prose 块 `max-width: var(--measure)`,`AboutPage.astro:85`)。
|
||||||
|
|
||||||
|
### 5.5 兼容与零回归
|
||||||
|
|
||||||
|
- `intro` 字段**不删**:`getAboutBlocks()` 见 `blocks` 缺省/空数组 → 走旧路径(`intro` markdown → 默认文案)。这样 Sveltia 里手滑清空 blocks 不会白页。
|
||||||
|
- `t('about_track_*')` 等字典键**不删**:作为 `SITE_DEFAULTS` 的英文默认源;字典继续服务"没有 blocks 的旧数据"。
|
||||||
|
- zh/en 双路由共用 `AboutPage.astro`(`src/pages/about.astro`、`src/pages/en/about.astro`),**块数据只存一份**,locale 在读取层选字段 —— 这是 en 轨不漂移的结构保证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 首页扩展(结构化,不上 blocks)
|
||||||
|
|
||||||
|
理由:首页是**聚合页**(列表/影像/热力图/项目墙都是"取集合切片"),块构建器在这里只会增加"选块型 → 填聚合配置"的绕路,且首页 zh/en 是两份重复文件(`src/pages/index.astro` vs `src/pages/en/index.astro`),更适合"字段少而稳"。
|
||||||
|
|
||||||
|
| 已有(在途) | 建议补 | 字段形态 | 落地 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `tagline` `subtitle` | — | `{zh,en}` | `site-content.ts:160-175` |
|
||||||
|
| `sections{tracks,photos,heatmap,projects}` | `heartbeat`(心跳行开关) | boolean | 同上 |
|
||||||
|
| `perTrackLimit`(4-8) | `postsTracks`(显示哪几条轨,默认 reading+tech) | select multiple | 需给 `PostList` 加 `tracks` prop |
|
||||||
|
| `latestPhotos`(0-6) | `photosSource(curated|latest)`:curated 读 `photos` 集合的 `pinned` 标记 | select | 需给 photos schema 加 `pinned` |
|
||||||
|
| — | `heroStyle(idcard|plain)` | select | `IdCard` 加 variant |
|
||||||
|
| — | `projectsLimit`(0-8,0=隐藏) | number | 传给 `ProjectWall limit` |
|
||||||
|
| — | `announcement`(一段 markdown 短公告,渲染在 tagline 下) | `{zh,en}` markdown | 复用 `renderMarkdown`(须走净化) |
|
||||||
|
|
||||||
|
**风险**:`index.astro` 与 `en/index.astro` 是**手抄双份**,任何新字段必须在两处同时接;建议 P4 顺手抽出 `src/components/pages/HomePage.astro`(吃 `locale` prop),双页变薄壳 —— 这是"双语不漂移"的结构性修法,先于新字段做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 安全与构建可靠性
|
||||||
|
|
||||||
|
### 7.1 现状漏洞(三条,均已定位到行)
|
||||||
|
|
||||||
|
1. **白名单对页面文案不生效**:`renderMarkdown()` 调 `createMarkdownProcessor()` **无参**(`src/lib/site-content.ts:124`),`@astrojs/markdown-remark` 用默认 `rehypePlugins: []`(`node_modules/.pnpm/@astrojs+internal-helpers@0.13.0/.../markdown.js:21`),且默认 `allowDangerousHtml: true` + 结尾无条件挂 `rehype-raw`(`@astrojs/markdown-remark/dist/index.js:53-56,80`)。→ **`about.yaml` 的 raw HTML 直通 `set:html`**(`AboutPage.astro:20`),`<script>`/`style`/`on*` 全不被剥。文章管线有白名单(`astro.config.mjs:28`),页面文案管线**没有**。
|
||||||
|
2. **README 是远程内容**:`githubReadmeMarkdown()` 把网上下来的 markdown 直接进 `set:html`(`src/pages/opensource.astro:17`)。即便 README 属本人仓,也等于把"远程内容"当可信 HTML 处理。
|
||||||
|
3. **`style` 属性会用内联样式破灰阶**:现白名单剥 `style`,但只对文章生效;块模型若不统一走净化管线,同样的洞会出现在关于页。
|
||||||
|
|
||||||
|
### 7.2 三道防线(按顺序实施)
|
||||||
|
|
||||||
|
**D1 修管线(一处修两洞,P0)**
|
||||||
|
```ts
|
||||||
|
// src/lib/site-content.ts
|
||||||
|
const { createMarkdownProcessor } = await import('@astrojs/markdown-remark');
|
||||||
|
mdProcessor ??= createMarkdownProcessor({
|
||||||
|
rehypePlugins: [rehypeRaw, rehypeClassWhitelist, rehypeLinkCards], // 与 astro.config.mjs:28 同序
|
||||||
|
});
|
||||||
|
```
|
||||||
|
副作用(正向):页面 markdown 里的独立成段链接**也会变成 OG 卡片**(`rehypeLinkCards` 现已带 `content/.link-cache.json` 缓存),这是"免费能力",需在写作指南里说明。
|
||||||
|
|
||||||
|
**D2 加 tag 级允许清单(P0,扩展 `src/lib/rehype-class-whitelist.ts` 或新建 `rehype-content-sanitize.ts`)**
|
||||||
|
- 允许标签:`p br strong em del code pre blockquote ul ol li a img h2 h3 h4 hr table thead tbody tr th td figure figcaption span div`(**不含** `script style iframe object embed form input link meta svg`)。
|
||||||
|
- 属性允许:`a[href,title,rel]`、`img[src,alt,width,height,loading]`、其余仅 `class`(白名单)+ `id`(heading 锚)。
|
||||||
|
- 协议允许:`http: https: mailto:` + 站内相对路径;`javascript:`/`data:`(除 `image/*` 小图外)一律剥。
|
||||||
|
- 未知块 `type` → 跳过 + WARN,**不炸构建**。
|
||||||
|
|
||||||
|
**D3 图片必须走管线(P1,随块模型)**
|
||||||
|
- 块内 `image` 字段声明为 `image()`(Astro helper),媒体目录 `content/site/assets`(`public_folder: ./assets`,与 avatar 现状一致,`config.yml:117-118`)。
|
||||||
|
- 禁止在块里填 `/media/...` 或外链图(**schema 层拦**:`z.string().refine(s => !/^https?:/.test(s))`,只放行 `image()` 类型)。体积纪律沿用写作指南(单张 ≤1MB)。
|
||||||
|
|
||||||
|
### 7.3 验收断言清单(可直接写成 Playwright / grep 断言)
|
||||||
|
|
||||||
|
| # | 断言 | 手段 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 关于页块数与顺序 == yaml(含 `enabled:false` 被滤掉) | `page.locator('[data-block]')` 逐个取 `data-block` 属性拼串比对 |
|
||||||
|
| 2 | 零配置零回归:`about.yaml` 只有 `intro` 时,`/about/` 与改动前快照一致 | 旧产物 dist 对比(`diff <(grep -o 'class="[^"]*"' old) new`) |
|
||||||
|
| 3 | 注入负向测试:`intro` 写 `<script>alert(1)</script>` 与 `<span style="color:red">` → 构建后 dist 不含 `alert(1)`、不含 `style="color` | `grep -c` 必须为 0 |
|
||||||
|
| 4 | 类白名单:`<span class="font-kai foo">` → 只留 `font-kai` | 同上 grep |
|
||||||
|
| 5 | 未知块型不炸:yaml 写 `type: not-a-block` → 构建成功 + stderr 含 WARN + 页面无该块 | 手跑一次 build |
|
||||||
|
| 6 | 缺译可见:`en` 全空的页面 → 渲染 zh 文案 + 构建 WARN 列出字段路径 | 静态检查 build 输出 |
|
||||||
|
| 7 | tokens 生效与压过:明暗两态 `--accent` == yaml 值;`--radius-img` 同 | `getComputedStyle(document.documentElement).getPropertyValue('--accent')` |
|
||||||
|
| 8 | 对比度闸门:token 化颜色对 `--bg` ≥4.5:1(含暗色态) | 构建期脚本 `scripts/build-perf/contrast-check.mjs`(相对亮度公式,08 文档同口径) |
|
||||||
|
| 9 | 图片走管线:dist 中关于页图片 `src` 含 `/_astro/` 且带 `width/height` | grep |
|
||||||
|
| 10 | 项目墙可见性:`mode: only, include: [blog, pi-board]` → dist `/opensource/` 的 `.repo-name` 恰 2 条;`mode: exclude` 同理 | Playwright 计数 |
|
||||||
|
| 11 | README 回落:把 token 文件改名后构建 → `/opensource/` 仍出开篇(缓存或 blurb),构建 exit 0 | 手工一次 |
|
||||||
|
| 12 | 构建期零运行时:`dist/about/index.html` 新增 inline `<script>` 数为 0(相对基线) | grep 计数对比 |
|
||||||
|
|
||||||
|
### 7.4 失败回落纪律(写进读取层,不散在组件里)
|
||||||
|
|
||||||
|
| 数据 | 缺省/损坏时 | 目标 |
|
||||||
|
|---|---|---|
|
||||||
|
| `blocks` 空/缺 | → `intro` → `SITE_DEFAULTS.aboutIntro` | 页面永不空 |
|
||||||
|
| 某块缺必填字段 | zod 报错即构建失败(**块内必填**要谨慎:`title` 设 optional,`body` 必填) | 宁可构建失败也不出半块 |
|
||||||
|
| 块文本超长 | 读取层截断 + WARN | 防"一顿贴整篇长文" |
|
||||||
|
| theme.yaml 非法值 | 钳制/回落默认 + WARN | 绝不让一个 `#zzz` 拖垮构建 |
|
||||||
|
| 项目 API 全挂 | fallback JSON | 已有 |
|
||||||
|
| README 全挂 | 仓外缓存 → yaml blurb | §4.4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 分阶段实施清单
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 工作量 | 依赖 | 落地文件 | 验收 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **P0** | 修 markdown 管线(D1)+ tag 白名单(D2)+ 负向断言 #3/#4 | 0.5d | 无 | `src/lib/site-content.ts`、`src/lib/rehype-content-sanitize.ts`(新)、`tests/e2e.mjs` 或 `scripts/build-perf/content-lint.mjs` | 断言 3/4 绿,现有文章渲染零回归 |
|
||||||
|
| **P0.5** | 后台实测冒烟:`list` 内嵌 `list`、`richtext` 嵌套在 `object` 内是否可用 | 0.2d | 无(需 token 登录一次) | 无代码,结论回写本文 §3.4 | 能新增/拖拽/保存并构建通过 |
|
||||||
|
| **P1** | blocks 骨架:schema + `page-blocks.ts` + `BlockRenderer` + prose/tracks/divider 三块 + 后台 `types` 声明 | 2d | P0、P0.5 | `src/content.config.ts`、`src/lib/page-blocks.ts`、`src/components/blocks/*`、`src/components/pages/AboutPage.astro`、`public/admin/config.yml` | 断言 1/2/5/6 |
|
||||||
|
| **P2** | 块型扩充(cards/timeline/quote/figure/linklist/cta)+ 图标键白名单 + 图片走管线(D3) | 1.5d | P1 | 同上 + `content/site/assets/` | 断言 9;视觉复核一轮(glm-5.3-flash) |
|
||||||
|
| **P3** | theme.yaml + `theme.ts` + BaseLayout 内联变量 + 对比度闸门脚本 | 1d | P0(净化不相关,可并行) | `content/site/theme.yaml`、`src/lib/theme.ts`、`src/layouts/BaseLayout.astro`、`scripts/build-perf/contrast-check.mjs`、`public/admin/config.yml` | 断言 7/8 |
|
||||||
|
| **P4** | 首页字段细化 + 抽 `HomePage.astro` 消双份 | 1d | P0 | `src/components/pages/HomePage.astro`(新)、`src/pages/index.astro`、`src/pages/en/index.astro`、`site-content.ts` | zh/en 区块数一致;开关组合抽测 |
|
||||||
|
| **P5** | 开源页收尾:README 缓存移仓外 + 超时 + fallback JSON 回填 5-8 条 + `include` 未命中 WARN | 0.5d | 在途车道合并 | `src/lib/site-content.ts`、`src/data/projects-fallback.json` | 断言 10/11 |
|
||||||
|
| **P6** | 文档:`docs/管理台.md`(块编辑怎么用)、`docs/写作指南.md`(受限类 + 块字段)、`public/admin/config.yml` hint 全量中文 | 0.5d | P1-P4 | 三文件 | 后台每字段都有中文 hint |
|
||||||
|
|
||||||
|
合计 ≈7.2d(不含视觉复核排队)。**建议顺序**:P0 → P0.5 →(P1 与 P3 可并行)→ P2 → P5 → P4 → P6。P5 最便宜且能立刻消掉在途车道的生产隐患,可提前插队。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 明确不做(附理由)
|
||||||
|
|
||||||
|
1. **任意 HTML/`style` 块**:等于把灰阶骨架与 XSS 面交给后台输入;08 文档红线(禁渐变/玻璃/大圆角)无法用代码守住。
|
||||||
|
2. **可视化栅格/自由拖拽布局(columns、grid 编辑器)**:Astro 侧要一套运行时布局引擎,违"构建期零运行时";且移动端塌陷规则无法交给后台。
|
||||||
|
3. **可编辑颜色集(多个强调色/任意 hex 用于文本)**:全站唯一强调色是丙路线根基(`tokens.css:21-22`),只允许 `--accent` 一个旋钮并过对比度闸门。
|
||||||
|
4. **`--measure` / 断点可调**:`--measure` 9 处引用、断点写死在媒体查询里;改版心会连带 TOC/栅格/图库。
|
||||||
|
5. **任意字体名输入**:字体子集化(`scripts/build-perf/subset-font-css.mjs`)依赖固定四族,自由输入会静默失效或拖大 CSS。
|
||||||
|
6. **块模型用于首页**:首页是聚合页(§6),结构化字段更稳、更好审。
|
||||||
|
7. **`blocks_zh`/`blocks_en` 双数组**:结构会分叉。
|
||||||
|
8. **构建期自动翻译页面文案**:破坏构建确定性(产物含未提交内容),且无人校对。译文必须入仓(沿用 `posts-en` 的 `locked: true` 纪律)。
|
||||||
|
9. **运行时/客户端渲染页面内容**(含客户端拉 GitHub README):CORS、SEO、性能三杀。
|
||||||
|
10. **后台新建任意页面路由(v1 不做)**:需 `[...slug].astro` + 导航/站点地图/双语配对/`hidden` 语义,收益低于成本;等关于页验证过一轮再议(那时复用同一套 BlockRenderer,成本会低很多)。
|
||||||
|
11. **换 CMS / 上 Netlify 式可视化编辑器**:现有 Sveltia 0.252 已支持 `types` 异构列表(§3.4),够用;迁移成本远高于收益。
|
||||||
|
12. **每页独立 tokens**(页面级主题覆盖):双层变量来源会让"某页为什么和别的页不一样"变成排查噩梦;v1 只有全局一套。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附:待用户拍板的三个岔路
|
||||||
|
|
||||||
|
1. **关于页块模型 vs 长文**:块模型(本文推荐,结构可控、视觉一致)vs 单一 markdown 长文(改起来最快,但 tracks 卡片做不出来)。
|
||||||
|
2. **README 开篇是否开**:开(省手工维护,但版式受 GitHub README 影响)vs 关(`intro_from: "off"`,全手控)。
|
||||||
|
3. **样式旋钮范围**:只给 3 个(accent/radius/字号)还是一整套 12 个(§3.6)——旋钮越多越需要对比度闸门与视觉复核,建议先给 3 个、跑稳一版再扩。
|
||||||
@@ -36,3 +36,28 @@ sudo bash infra/deploy/rollback.sh --exec # 秒级回 Halo
|
|||||||
- 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布
|
- 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布
|
||||||
- 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit
|
- 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit
|
||||||
- 翻译:新文章 push 后跑 `python3 scripts/translate/translate_posts.py`(幂等)
|
- 翻译:新文章 push 后跑 `python3 scripts/translate/translate_posts.py`(幂等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SEO 解封操作单(Phase 5f T1,2026-10-26 立案)
|
||||||
|
|
||||||
|
**背景**:生产全站自上线起带 `X-Robots-Tag: noindex`(/etc/nginx/snippets/esa-origin-ssl.conf:11 共享片段误伤博客 vhost)+ robots.txt 404 + ESA 将带 noindex 的响应头缓存 30 天——搜索引擎全站不可见。
|
||||||
|
|
||||||
|
**第一步:nginx(需 sudo)**
|
||||||
|
1. 编辑 `/etc/nginx/snippets/esa-origin-ssl.conf`:删除第 11 行 `add_header X-Robots-Tag noindex always;`(改前备份 .bak)
|
||||||
|
2. 给确实需要 noindex 的内部服务 vhost(api/oj/token/fc/latex/cpa/git)各 server 块补回该行
|
||||||
|
3. 博客 vhost 补局部:`location /admin/ { add_header X-Robots-Tag noindex always; }`
|
||||||
|
4. `sudo nginx -t && sudo systemctl reload nginx`
|
||||||
|
|
||||||
|
**第二步:ESA 控制台(不做则 noindex 再活 30 天)**
|
||||||
|
1. 全站 purge `wait4x.online`
|
||||||
|
2. HTML 边缘 TTL 调 ≤1h(当前 2592000s,靠回源重验证更新——头会固化)
|
||||||
|
3. 加规则:`/artalk/api/*` 绕过缓存(GET stats 有 Age 头实锤,pv 数字会冻)
|
||||||
|
|
||||||
|
**第三步:验证**
|
||||||
|
```bash
|
||||||
|
curl -sI https://wait4x.online/ | grep -ci x-robots-tag # 期望 0
|
||||||
|
curl -s https://wait4x.online/robots.txt | head -3 # 期望 User-agent + Sitemap
|
||||||
|
curl -sI https://wait4x.online/admin/ | grep -ci noindex # 期望 1(局部保留)
|
||||||
|
```
|
||||||
|
仓库侧已就绪:public/robots.txt(本提交);生产 Lighthouse SEO≥95 复测随后入档。
|
||||||
@@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
> 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。
|
> 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。
|
||||||
|
|
||||||
|
## 写作模板(从这里开始,2026-11-02 起)
|
||||||
|
|
||||||
|
别从空白文件起步——**复制现成的模板**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp content/posts/writing-template.md content/posts/my-new-post.md
|
||||||
|
# 改 frontmatter 的 title / pubDate / tags / description,删掉不需要的段落
|
||||||
|
```
|
||||||
|
|
||||||
|
模板本身是一篇真文章(线上读:`/archives/writing-template/`),每节都在跑一种能力,你看到的效果就是源码的效果:标题层级、彩色强调、字族切换、引用、列表、代码窗条、表格、图片放大、链接卡片、`<details>` 折叠、分隔线。
|
||||||
|
|
||||||
|
写之前只需记住三条纪律:
|
||||||
|
- **彩色不限次数**(2026-11-02 用户拍板取消原来的"一页最多三处"):正文想用多少处颜色都可以;仍守的是另一条——**外壳灰阶**(导航/页脚/按钮/背景不染色),因为外壳是气质,正文是表达
|
||||||
|
- **整页换字族要谨慎**:会给访问者拉一份中文字体(汇文明朝整页约 420KB,已子集化)
|
||||||
|
- **折叠放细节不放结论**:`<details>` 里的内容可能永远没人展开
|
||||||
|
|
||||||
|
## 置顶文章
|
||||||
|
|
||||||
|
首页、归档、书架三处列表默认按日期倒序。想让某篇长期排在最前,frontmatter 加一行:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pinned: true
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界(刻意设计成这样):
|
||||||
|
- **只影响列表顺序**,不改 `pubDate`、不改 URL、不影响 RSS 的时间序列(订阅者不会看到时间错乱)
|
||||||
|
- 多篇同时置顶时,它们内部仍按日期倒序
|
||||||
|
- 列表里会显示一个灰阶「置顶」小徽标,取消置顶后立刻回到时间序
|
||||||
|
- 后台「文章」编辑页有「置顶」开关,不必手改 frontmatter
|
||||||
|
|
||||||
## 目录约定
|
## 目录约定
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -22,10 +52,11 @@ content/posts/
|
|||||||
|
|
||||||
```markdown
|
```markdown
|
||||||

|

|
||||||
*图:外滩的傍晚* ← 紧跟媒体的斜体独立段 = 图注(居中小字)
|
<p class="fig-cap">图:外滩的傍晚</p> ← 显式图注类(居中小字)
|
||||||
```
|
```
|
||||||
|
|
||||||
- 图注可选;写法固定为 `*…*` 单独成段,紧跟图片
|
- 图注可选;**必须用 `<p class="fig-cap">…</p>`**(2026-11-02 起)
|
||||||
|
- ⚠️ 旧写法 `*斜体独立段*` 已不再自动当图注:此前它会把正文里所有独占成段的斜体都变成块级居中(用户实测的排版 bug),现在斜体恢复为正常行内强调
|
||||||
- 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩)
|
- 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩)
|
||||||
|
|
||||||
## 视频 / 音频
|
## 视频 / 音频
|
||||||
@@ -65,7 +96,357 @@ public/media/my-post/clip.mp4 ← 文件放这里
|
|||||||
|
|
||||||
见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。
|
见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。
|
||||||
|
|
||||||
|
## 灯箱说明文字(图库 caption)
|
||||||
|
|
||||||
|
图库照片的 `caption` 字段走同一套富文本管线:markdown(`**加粗**`、换行)+ 排版类(`<span class="c-blue font-kai">`)。灯箱里深底上的彩色会自动取暗底档。
|
||||||
|
|
||||||
|
图库灯箱关闭:右上 `×`、`Esc`、点背景、点图(1× 时)都能关;缩放后移动鼠标召唤控件。
|
||||||
|
|
||||||
|
## 链接卡片(OG 预览卡)
|
||||||
|
|
||||||
|
**一行只放一个链接、且它独立成段**时,构建期会抓取该页的 OG 信息渲染成卡片(标题 / 描述 / 站点图标 / 可选缩略图):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
https://astro.build/
|
||||||
|
|
||||||
|
[也可以是 markdown 链接形态](https://astro.build/)
|
||||||
|
```
|
||||||
|
|
||||||
|
规则与边界:
|
||||||
|
|
||||||
|
- **内联链接不变**:夹在句子里的链接保持普通样式(`这是一句带 [链接](https://example.com) 的话` → 不会变卡),所以不会把整篇变成卡片墙
|
||||||
|
- **抓取失败一律回落普通链接**:404、无 OG、超时(4s)、站点拒爬都不影响构建
|
||||||
|
- **每篇最多 10 张卡**,超出的回落普通链接(防止文章变卡片流)
|
||||||
|
- **缓存**:结果存 `content/.link-cache.json`(入仓,保证构建确定性);条目 30 天后自动重抓。想强制刷新某条——删掉该 URL 的条目即可
|
||||||
|
- 缩略图取 `og:image`,无图不占位;图标取站点而非第三方 favicon 服务
|
||||||
|
|
||||||
|
## 文章排版类(受限白名单)
|
||||||
|
|
||||||
|
正文里可用 `<span class="…">` / `<p class="…">` 做**受限**排版。构建期有白名单过滤:**只放行下表类名,其它 class 与 `style`、`on*` 属性一律被剥离**(防注入 + 保护灰阶骨架)。
|
||||||
|
|
||||||
|
### 灰阶层级类(全部实测亮暗对比度 ≥4.5:1)
|
||||||
|
|
||||||
|
| 类名 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `ink-1` / `ink-2` / `ink-3` | 指定灰阶层(标题/正文/次要) |
|
||||||
|
| `ink-muted` | 最淡灰(注释感) |
|
||||||
|
| `ink-faint` | 最淡**可读**档(再淡即不达 AA) |
|
||||||
|
| `ink-warm` | 暖调正文(书评气质) |
|
||||||
|
| `ink-cool` | 冷调正文(技术气质) |
|
||||||
|
| `ink-accent` | 强调(链接色,少量点缀) |
|
||||||
|
| `ink-accent-soft` | 弱强调(比强调淡一档) |
|
||||||
|
|
||||||
|
### 语义色板(8 色相 × 标准/soft;亮暗各自取值,实测全部 ≥4.5:1)
|
||||||
|
|
||||||
|
| 色相 | 标准类 | soft 类 | 亮态对比度(标准 / soft) | 暗态(标准 / soft) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 蓝 | `c-blue` | `c-blue-soft` | 6.46 / 4.58 | 7.70 / 4.61 |
|
||||||
|
| 青 | `c-teal` | `c-teal-soft` | 5.00 / 4.57 | 9.77 / 4.52 |
|
||||||
|
| 绿 | `c-green` | `c-green-soft` | 5.38 / 4.67 | 10.38 / 4.67 |
|
||||||
|
| 黄 | `c-yellow` | `c-yellow-soft` | 6.45 / 4.57 | 11.73 / 4.52 |
|
||||||
|
| 橙 | `c-orange` | `c-orange-soft` | 6.26 / 4.65 | 9.44 / 4.58 |
|
||||||
|
| 红 | `c-red` | `c-red-soft` | 6.68 / 4.55 | 7.51 / 4.50 |
|
||||||
|
| 粉 | `c-pink` | `c-pink-soft` | 6.96 / 4.73 | 8.87 / 4.52 |
|
||||||
|
| 紫 | `c-purple` | `c-purple-soft` | 7.46 / 4.53 | 8.25 / 4.64 |
|
||||||
|
|
||||||
|
- 黄色在亮态被压暗到 `#7a5900`(原色在白底只有 ~1.8:1),这是**必须**的:彩色文字要能读
|
||||||
|
- `soft` 档是"阈值内最淡值",适合次要强调;标准档用于关键强调
|
||||||
|
|
||||||
|
> **使用纪律(重要)**:彩色**只作用于文章正文的强调文字**。站点外壳(导航、页脚、按钮、背景、卡片边框)一律保持灰阶——整站不因正文用色而花掉。写作时一个页面里的彩色强调建议不超过 3 处。
|
||||||
|
|
||||||
|
### 字体类
|
||||||
|
|
||||||
|
| 类名 | 作用 | 字体 | 许可证 | 加载 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `font-serif` | 书评正文族 | Noto Serif SC | OFL-1.1 | 自托管 |
|
||||||
|
| `font-kai` | 金句族 | 霞鹜文楷 Screen | OFL-1.1 | 自托管 |
|
||||||
|
| `font-sans` | UI/技术族 | Noto Sans SC | OFL-1.1 | 自托管 |
|
||||||
|
| `font-mono` | 标签/数值 | JetBrains Mono | OFL-1.1 | 自托管 |
|
||||||
|
| `font-song` | 宋体族 | Noto Serif SC 优先,系统宋体兜底 | OFL-1.1 | **自托管优先** |
|
||||||
|
| `font-kai-sys` | 楷体族 | 霞鹜文楷优先,系统楷体兜底 | OFL-1.1 | **自托管优先** |
|
||||||
|
| `font-hei-sys` | 黑体族 | Noto Sans SC 优先,系统黑体兜底 | OFL-1.1 | **自托管优先** |
|
||||||
|
| `font-fangsong` | 仿宋族 | FangSong/STFangsong | — | ⚠️ **纯系统回退**(暂无 OFL 自托管仿宋包) |
|
||||||
|
|
||||||
|
### 文学排版类(段落级)
|
||||||
|
|
||||||
|
| 类名 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `drop-cap` | 段落首字下沉(书评开篇) |
|
||||||
|
| `quote-line` | 金句块(左侧竖线 + 文楷) |
|
||||||
|
| `fig-cap` | 图注(居中小字) |
|
||||||
|
|
||||||
|
### 首批字体(已落地,6 族 + 3 档自托管优先)
|
||||||
|
|
||||||
|
| 类名 | 字体 | 许可证 | webfont 嵌入 | 用到该字体的页面增量 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `font-serif` | Noto Serif SC | OFL-1.1 | ✅ OFL 允许 | 基线内 |
|
||||||
|
| `font-sans` | Noto Sans SC | OFL-1.1 | ✅ | 基线内 |
|
||||||
|
| `font-kai` | 霞鹜文楷 Screen | OFL-1.1 | ✅ | 基线内 |
|
||||||
|
| `font-mono` | JetBrains Mono | OFL-1.1 | ✅ | 基线内 |
|
||||||
|
| `font-huiwen` | 汇文明朝体(文风切片包 339 片) | OFL-1.1 | ✅ | **535KB** |
|
||||||
|
| `font-kuaile` | 站酷快乐体 | OFL-1.1 | ✅ | **163KB** |
|
||||||
|
| `font-song` / `font-kai-sys` / `font-hei-sys` | 上述自托管字体优先,系统字族兜底 | OFL-1.1 | ✅ | **+0KB** |
|
||||||
|
|
||||||
|
体积账结论(2026-11-02 实测,本地无 gzip 的相对值):**未使用某字体的页面,字体字节增量恒为 0**(构建期按页子集化 + unicode-range 按需拉取)。汇文采用文风开源的 cn-font-split 切片包(339 片),实测只拉用到字符的 19 片。
|
||||||
|
|
||||||
|
**再裁一刀(按本站实际用字子集化)**:切片包本身粒度粗(一片含数百字),只做标题/短句的艺术字体往往只用到几十个字符。`scripts/build-perf/subset-fonts.py` 读字体 CSS 的 `@font-face`、收集内容里真实出现的字符、逐片求交集后用 `pyftsubset` 重写——实测汇文整页 **694KB → 420KB**(约 −40%)。命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build-perf/subset-fonts.py \
|
||||||
|
--css node_modules/@fontsource/huiwen-mincho/index.css \
|
||||||
|
--out public/fonts/subset/huiwen \
|
||||||
|
--chars-from content/posts content/moments content/site/*.yaml
|
||||||
|
# 可选:--chars-mode all|headings(只统计标题字)、--min-bytes 4096(小于该值的片不裁)
|
||||||
|
```
|
||||||
|
|
||||||
|
子集化产物输出到 `--out` 指定目录(如 `public/fonts/subset/`);把它接进站点(构建链挂守卫 + 字体 CSS 指向子集)是独立一步,接线后随构建自动生成,未接线前需手动跑。**换字/加字后重跑一次**,否则新字会回落到下一个切片或系统字体。
|
||||||
|
|
||||||
|
### 5i 字族扩展六款(2026-11-03,全站共 12 族)
|
||||||
|
|
||||||
|
| 类名 | 字体 | 适用场景 | 许可证 / 来源 | 切片 | 单页增量(实测) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `font-xiaowei` | 站酷小薇 ZCOOL XiaoWei | 文学味标题、书摘 | OFL-1.1 / google/fonts `ofl/zcoolxiaowei` | 100 片 | 44.9KB |
|
||||||
|
| `font-huangyou` | 站酷庆科黄油体 | 圆润标题、栏目名 | OFL-1.1 / `ofl/zcoolqingkehuangyou` | 100 片 | 27.1KB |
|
||||||
|
| `font-mashanzheng` | 马善政毛笔楷书 | 书法短句、题记 | OFL-1.1 / `ofl/mashanzheng` | 100 片 | 45.7KB |
|
||||||
|
| `font-liujianmaocao` | 柳建毛草 | 草书点缀(只宜 1-2 字) | OFL-1.1 / `ofl/liujianmaocao` | 100 片 | 30.0KB |
|
||||||
|
| `font-smiley` | 得意黑 Smiley Sans(Oblique) | 现代倾斜标题、强调句 | OFL-1.1 / `atelier-anchor/smiley-sans` v2.0.1 | 100 片 | 27.1KB |
|
||||||
|
| `font-neoxihei` | 霞鹜新晰黑 LXGW Neo XiHei | 比思源黑更清晰的正文黑体 | OFL / `lxgw/LxgwNeoXiHei` | 96 片 | 30.0KB |
|
||||||
|
|
||||||
|
- 六款均按**站内用字**细切(约 19 字/片,单片 2–12KB),且只在内容里写到对应类时才加载(探测在 `src/lib/font-needs.ts`)——首页、归档列表这类无字体类的页面**零增量**。
|
||||||
|
- **覆盖边界**:切片只含站内出现过的字(当前约 1900 字)。新写了字典外的新字(尤其生僻字)要先重跑切片,否则该字回落系统字族。
|
||||||
|
- **重切命令**(字体源文件不入仓,按需从上面的来源下载):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build-perf/subset-fonts.py \
|
||||||
|
--split-ttf /path/to/YourFont.ttf --family "Font Family Name" \
|
||||||
|
--slices 100 --order-from public/fonts/subset/hwmc/fonts.css \
|
||||||
|
--out public/fonts/<shortname> \
|
||||||
|
--chars-from 'content/posts/*.md' 'content/site/*.yaml' 'content/moments/*.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
- **体积闸门**:单页增量 >150KB 必须子集化或降级。本次六款实测 27–46KB(单句场景)全部达标;@fontsource 官方中文包切片粒度约 33 字/片、单片 40KB+,一句话就 300–440KB/页,**已弃用**(改从原始 TTF 细切)。
|
||||||
|
|
||||||
|
### 候补字体清单(未落地,加一款约"一条命令 + 一次体积账")
|
||||||
|
|
||||||
|
| 字体 | 许可证 | 允许 webfont 嵌入 | 未落地原因 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| HarmonyOS Sans | 华为字体许可(免费商用) | ✅ 明文允许"嵌入、捆绑、重新分发",需标注 | npm 无包,官网整包 52MB |
|
||||||
|
| MiSans | 小米字体许可(免费商用) | ✅ 官方 FAQ 明确 "embedded fonts: Yes",需注明 | npm 无包 |
|
||||||
|
| OPPO Sans | 免费授权(含商用) | ✅(附带"不向他方提供下载渠道"限制) | npm 无包 |
|
||||||
|
| 阿里巴巴普惠体 | 免费商用全场景 | ✅ | npm 无包,9 字重体积巨大 |
|
||||||
|
| 得意黑 Smiley Sans | OFL-1.1 | ✅ | 仅 GitHub release 整包,直链在当前网络不可达 |
|
||||||
|
| 更纱黑体 Sarasa Gothic / Sarasa Mono | OFL-1.1 | ✅ | npm 包为空壳(需 cli 自生成切片) |
|
||||||
|
| 花园明朝 HanaMinA/B | IPA Font License | ✅(允许嵌入) | 无 npm 包,仅兜底用 |
|
||||||
|
| 站酷高端黑 / 站酷快乐体(已落) | OFL-1.1 | ✅ | 高端黑无 @fontsource 版本 |
|
||||||
|
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
|
||||||
|
| 霞鹜文楷 Light/Bright | OFL-1.1 | ✅ | 实测 **+8605KB**(CJK 切片粒度所致) |
|
||||||
|
|
||||||
|
> 加新字体的流程:①核许可证与 **web 嵌入条款**(很多"个人免费"字体明文禁 @font-face,只能进系统回退)②npm 装或放入 `public/fonts/<family>/`(优选 cn-font-split 类切片包)③`global.css` 加类 + `rehype-class-whitelist` 放行 + `public/admin/typography-classes.js` 加一项 ④跑体积账,超 150KB/页 的要评估是否值得;展示类字体再跑一次 `scripts/build-perf/subset-fonts.py` 按实际用字裁切(命令见上节)。
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<span class="ink-muted">次要说明</span>
|
||||||
|
<span class="font-kai">金句可以换文楷</span>
|
||||||
|
<p class="drop-cap">开篇段落,首字会下沉。</p>
|
||||||
|
<p class="quote-line">所谓暮色,就是光慢慢地承认自己要走。</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **颜色只能走上面这些类**(内部用 tokens,亮暗双态都可读);写 `style="color:red"` 会被构建期剔除
|
||||||
|
- 斜体就用普通 markdown `*斜体*`——行内强调,不会换行也不会居中
|
||||||
|
|
||||||
|
### 后台可视化选择(推荐,不用写 HTML)
|
||||||
|
|
||||||
|
后台编辑器里:**选中文字 → Insert 菜单 → 「排版(字体/颜色)」→ 面板里点一个类**,文字即被 `<span class="…">` 包住;点已套用的片段可改类,Backspace 可整体删除。
|
||||||
|
面板里的两类与上表一一对应(字体 8 / 颜色 9),类清单单一事实源在 `public/admin/typography-classes.js`。
|
||||||
|
|
||||||
|
## 三层排版架构(全站,不只文章正文)
|
||||||
|
|
||||||
|
**层 1 · 页面文案富文本**:后台「页面文案」里的 tagline / subtitle / blurb / blurb_after / intro / 关于页 blocks 的 prose 块,全部走同一条富文本管线(markdown + raw + 内容净化 + class 白名单 + 链接卡片)。可以直接写:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tagline:
|
||||||
|
zh: '<span class="font-kai c-blue">写作者</span> · 读者 · 快门按压者,拥抱开源。'
|
||||||
|
```
|
||||||
|
|
||||||
|
渲染入口唯一:`src/lib/site-content.ts` 的 `renderRich()`(行内场景传 `{ inline: true }` 自动剥外层 `<p>`,否则 `<p>` 套 `<p>` 会被浏览器拆散)。
|
||||||
|
**不开放**:导航/页脚/按钮等硬编码界面外壳——那是 UI 不是内容。
|
||||||
|
|
||||||
|
**层 2 · 页面级排版覆盖**:每个页面单例(`content/site/*.yaml`)可加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
typography:
|
||||||
|
font: font-kai # 本页正文默认字体类(可选)
|
||||||
|
accent: c-blue # 本页强调色类(可选)
|
||||||
|
scale: 1.1 # 字号缩放 0.8–1.25(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
实现是在该页内容容器上挂 `.page-typo` + CSS 变量(`--page-font / --page-accent / --page-scale`),**不写全站 tokens**——其它页面零影响(已在图库/书架/文章/装备页验证隔离)。白名单外的类名会被忽略。
|
||||||
|
后台入口:「页面文案 → 各自页面 → 页面排版覆盖」。
|
||||||
|
|
||||||
|
**层 3 · 艺术字体**:`font-art-kuaile`(站酷快乐体,艺术字专用类)。⚠️ **仅用于标题/短句**:艺术字字形大,CJK 切片体积高(实测用到页 +163KB),长段使用会显著抬高单页字体成本。
|
||||||
|
|
||||||
|
## 瞬间(/moments)怎么写
|
||||||
|
|
||||||
|
瞬间与文章一样支持富文本,但入口在后台「瞬间」集合(`content/moments/*.md`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
tags: [随手拍]
|
||||||
|
text: 今天把**置顶**做完了,顺手试了 <span class="c-teal">彩色</span>
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- `text` 支持 markdown(`**加粗**`、列表、`> 引用`)与**同一套排版类**(字体/颜色),因为走的是与全站文案相同的净化管线:危险标签整棵丢弃,只放行 42 项白名单类
|
||||||
|
- 一条瞬间 = 一行时间线卡片,**每条都带独立评论区**(展开才挂载,双语页共用同一评论流,评论不分裂)
|
||||||
|
- 每条的「浏览 / 评论」数字是只读统计(不会因为别人点开评论而虚增浏览)
|
||||||
|
- `text` 与 `image` 至少填一项;图片建议先压缩
|
||||||
|
|
||||||
|
## 页面块(关于页 blocks,字段速查)
|
||||||
|
|
||||||
|
关于页正文可由九个块拼成(后台拖拽排序,每块有「显示」开关)。完整操作见 [管理台](./管理台.md);这里是写内容时对照的字段表——文本字段一律 `{zh, en}`。
|
||||||
|
|
||||||
|
| 块型 | 必填 | 可选 | 一句话 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `prose` | `body` | `title`、`width`(narrow/full) | markdown 长文 |
|
||||||
|
| `tracks` | `title`、`items[].name` | `items[].icon`、`items[].desc` | 主线/能力面 |
|
||||||
|
| `cards` | `items[].title` | `title`、`columns`(2/3)、`items[].desc/href/tag` | 并列卡片墙 |
|
||||||
|
| `timeline` | `items[].time`、`items[].title` | `title`、`items[].desc` | 时间线(时间可自由文本) |
|
||||||
|
| `quote` | `text` | `cite` | 单句引文 |
|
||||||
|
| `figure` | `image` | `alt`、`caption` | 单图(必须上传,禁外链) |
|
||||||
|
| `linklist` | `items[].label`、`items[].href` | `title`、`items[].note` | 链接清单 |
|
||||||
|
| `cta` | `text`、`href`、`label` | — | 行动按钮 |
|
||||||
|
| `divider` | — | `style`(line/space) | 分隔 |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
blocks:
|
||||||
|
- type: prose
|
||||||
|
body: { zh: 正文 markdown,可含排版类 span, en: Body markdown }
|
||||||
|
- type: cards
|
||||||
|
columns: 3
|
||||||
|
items:
|
||||||
|
- { title: { zh: 写作, en: Writing }, desc: { zh: 书评与技术文, en: Reviews & tech } }
|
||||||
|
```
|
||||||
|
|
||||||
|
边界:块数 ≤24、单块文本 ≤4000 字(超限截断并 WARN);未知块型跳过 + WARN;`blocks` 为空时回落 `intro` markdown,再空回落内置文案。图片字段走 `image()`,只能后台上传进 `content/site/assets/`。
|
||||||
|
|
||||||
## 发布提醒
|
## 发布提醒
|
||||||
|
|
||||||
- push main → webhook 自动构建发布(约 10 秒)
|
- push main → webhook 自动构建发布(约 10 秒)
|
||||||
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
|
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
|
||||||
|
|
||||||
|
|
||||||
|
## 书封图(/books 书架)
|
||||||
|
|
||||||
|
阅读轨文章可选加书封:
|
||||||
|
|
||||||
|
1. 图片放 `content/posts/assets/books/<slug>.jpg`(建议宽 480px、高宽比约 2:3)
|
||||||
|
2. 对应文章 frontmatter 加一行:`cover: ./assets/books/<slug>.jpg`
|
||||||
|
3. 构建自动优化出 96/192 两档;**没有 cover 的文章优雅回退现有色脊卡设计**,无需改任何代码
|
||||||
|
|
||||||
|
示例 frontmatter:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "面纱"
|
||||||
|
pubDate: 2024-06-01
|
||||||
|
category: reading
|
||||||
|
bookAuthor: "毛姆"
|
||||||
|
mood: "#8a6f4a"
|
||||||
|
cover: ./assets/books/mian-sha.jpg
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后台上传(Sveltia 后台,推荐路径)
|
||||||
|
|
||||||
|
后台地址 `/admin/`(gitea 账号登录)。三类媒体都有后台入口,不必手写路径:
|
||||||
|
|
||||||
|
### 图片
|
||||||
|
|
||||||
|
文章编辑器里插入图片 → 自动上传到 `content/posts/assets/` 并写入 `./assets/...`
|
||||||
|
相对路径(走 Astro 图片管线,自动优化)。**不要手改成 `/assets/...` 绝对路径**。
|
||||||
|
|
||||||
|
### 视频(frontmatter 字段,正文后自动渲染)
|
||||||
|
|
||||||
|
1. 文章编辑页「附视频」字段上传(自动落 `public/media/`,路径写 `/media/<name>.mp4`)
|
||||||
|
2. 正文读完后自动出现 `<video controls preload="metadata" playsinline>`,全宽圆角黑底
|
||||||
|
3. 「视频封面」填 `/media/xxx.jpg` 可自定首帧;留空浏览器自取
|
||||||
|
4. 无视频字段 → 该区块不渲染(零占位)
|
||||||
|
|
||||||
|
体积纪律:mp4/webm ≤50MB 走 `public/media/`(构建原样拷贝,不转码);
|
||||||
|
更大的请按上文「体积纪律」放仓外 store(该场景 build.sh 同步段待首次出现时补)。
|
||||||
|
|
||||||
|
### 后台列表日期
|
||||||
|
|
||||||
|
文章/瞬间列表显示「日期 · 标题」,可按日期排序;图库照片没有 frontmatter 日期
|
||||||
|
(时间在 EXIF/文件侧),列表按「分组 · 标题」展示、按标题/分组排序。
|
||||||
|
|
||||||
|
### 链接
|
||||||
|
|
||||||
|
markdown 原生语法,无需任何配置:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[文字](https://example.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
想让链接显示成 OG 预览卡(带标题/描述/图标),把它**单独放一行**即可——规则见上文「链接卡片(OG 预览卡)」。
|
||||||
|
|
||||||
|
### 页面文案(主页/关于/开源)
|
||||||
|
|
||||||
|
后台「页面文案」下三个入口:
|
||||||
|
|
||||||
|
| 入口 | 文件 | 可改内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 主页设置 | `content/site/home.yaml` | 身份语 tagline、**头像 avatar**、**站点图标 favicon**、**社交链接 socials** |
|
||||||
|
| 关于页 | `content/site/about.yaml` | **markdown 正文 intro**(多段/列表/粗斜体/链接) |
|
||||||
|
| 开源页 | `content/site/opensource.yaml` | 叙述 blurb/blurb_after、仓链接、置顶项目 pinned |
|
||||||
|
|
||||||
|
#### 头像与站点图标
|
||||||
|
|
||||||
|
- **头像**:主页设置里「头像」上传 → 落 `content/site/assets/`,构建期优化成 60px WebP;
|
||||||
|
留空则显示内置渐变占位(w4 徽标)
|
||||||
|
- **站点图标 favicon**:先经「图标」媒体库上传(`public/icons/`),再把路径粘到
|
||||||
|
「站点图标」字段(如 `/icons/my.ico`、`/icons/my.svg`);留空回退 `/favicon.svg`。
|
||||||
|
支持 `.svg` / `.ico` / `.png`(直出,不经图片优化)
|
||||||
|
|
||||||
|
#### 社交链接
|
||||||
|
|
||||||
|
主页设置里「社交链接」是列表:可增删、拖动排序。每条 = 名称(无障碍文字)+ 链接 + 图标。
|
||||||
|
图标可选 `github / rss / email / bilibili / x`,**留空则显示纯文字按钮**。
|
||||||
|
首页名片区、以及任何引用该组件的页面都吃同一份数据(单一事实源,改一次全站生效)。
|
||||||
|
|
||||||
|
#### 首页可配项(home.yaml)
|
||||||
|
|
||||||
|
| 字段 | 取值 | 默认 | 作用 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `subtitle` | `{zh, en}`(markdown 短文本) | 空 | 身份语下方的副标题;留空不渲染 |
|
||||||
|
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
|
||||||
|
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
|
||||||
|
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
|
||||||
|
| `projectsLimit` | 1-99 | 6 | 首页项目墙条数(越界夹取) |
|
||||||
|
| `announcement` | `{zh, en}`(markdown 富文本) | 空 | 名片区下方公告;空 = 不渲染 |
|
||||||
|
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨则该轨文章不在首页列出(归档页仍可见) |
|
||||||
|
|
||||||
|
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
|
||||||
|
|
||||||
|
#### 开源页项目墙(opensource.yaml)
|
||||||
|
|
||||||
|
| 字段 | 取值 | 默认 | 作用 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mode` | `all` / `only` / `exclude` | all | 可见性模式 |
|
||||||
|
| `include` | 仓短名数组 | 空 | mode=only 时的白名单 |
|
||||||
|
| `exclude` | 仓短名数组 | 空 | mode=exclude 时的黑名单 |
|
||||||
|
| `sort` | `pinned-first` / `stars` / `updated` | pinned-first | 排序方式 |
|
||||||
|
| `perSource` | `both` / `github` / `gitea` | both | 来源过滤 |
|
||||||
|
| `intro_from` | `"off"` / `github-readme` | off | 开篇文案来源 |
|
||||||
|
|
||||||
|
> `intro_from` 建议**带引号写**:YAML 1.1 里裸 `off` 会被解析成布尔 false。
|
||||||
|
> 选 `github-readme` 时构建期取 `wait4xx/wait4xx` 的 README 渲染为开篇,
|
||||||
|
> 成功后缓存到 `content/.github-readme.md`(入仓);token 缺位/取失败/仓无 README
|
||||||
|
> 都会打印日志并回落 yaml 的 blurb。注意 README 里的相对链接与外部徽章会原样输出。
|
||||||
|
|
||||||
|
#### 首页文章列表
|
||||||
|
|
||||||
|
首页现在分两段:**阅读轨 6 条 + 技术轨 6 条**(各自带「全部文章」出口:阅读→书架、技术→归档)。
|
||||||
|
照片轨文章不进首页(在图库看)。完整列表见 `/archives/`。
|
||||||
|
|
||||||
|
- 中文/英文各一个字段,页面按语言取;**留空则该字段回落默认文案**(构建不会炸)
|
||||||
|
- `pinned` 填仓库短名(如 `blog`、`pi-board`),**顺序即展示顺序**;留空回落 `blog`
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# 管理台(Sveltia CMS)使用指南
|
||||||
|
|
||||||
|
> 地址:`https://wait4x.online/admin/`(页脚「管理」入口)| 用途:照片上传与 sidecar 编辑的图形界面 | 保存即 commit 到 main → webhook 自动构建发布(约 10 秒上线)
|
||||||
|
>
|
||||||
|
> 这份是**操作手册**:每个入口能改什么、字段怎么填、改完会发生什么。写文章正文的排版技巧见 [写作指南](./写作指南.md)——两份分工:这里讲"后台怎么用",那里讲"正文怎么写"。
|
||||||
|
|
||||||
|
## 登录
|
||||||
|
|
||||||
|
选 **Sign In with Token** → 用 gitea 的 access token(宿主 `~/docker_apps/gitea/init-templates.txt` 里的 api-token,或 gitea 网页新建 scope=read/write:repository)。token 存浏览器 localStorage,下次免登。
|
||||||
|
|
||||||
|
## 四类内容集合
|
||||||
|
|
||||||
|
| 入口 | 文件位置 | 用来放什么 | 关键约束 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 文章 | `content/posts/<slug>.md` | 书评 / 技术文 / 摄影札记三轨 | `category` 三选一(`reading` / `tech` / `photo`),首页分轨由它派生 |
|
||||||
|
| 瞬间 | `content/moments/*.md` | 一句话 + 一张图的时间线 | `text` 与 `image` **至少填一项**(后台无法表达条件必填,保存前自查) |
|
||||||
|
| 照片 | `content/photos/*.json` + 图 | 图库(街拍 / 风景两组) | 标题**必填**(空标题会让构建炸,实测坑);EXIF 由构建期 loader 自动补全 |
|
||||||
|
| 站点页面 | `content/site/*.yaml` | 首页、关于页、开源页、主题 | 后台改的是**单例文件**,没有"新建" |
|
||||||
|
|
||||||
|
几条通用规则:
|
||||||
|
|
||||||
|
- **双语字段成对出现**:中文 / 英文两个输入框。英文留空不是错误——构建期回落中文并在日志里打 WARN(`[blocks] 缺译…`),页面不会破版。
|
||||||
|
- **留空 = 用默认**:任何字段清空都回到仓库自带的默认文案 / 默认值,构建永远成功。想让某段彻底消失,请看字段自带的"留空则不渲染"提示。
|
||||||
|
- **文章英文版**:不在后台写。跑 `python3 scripts/translate/translate_posts.py` 生成 `content/posts-en/` 同名文件;手写润色过的译文在 frontmatter 写 `locked: true`,翻译脚本会无条件跳过它。
|
||||||
|
- **保存即提交**:后台保存等于向仓库提交,push 后约 10 秒自动构建上线。
|
||||||
|
|
||||||
|
## 文章置顶(5h 新增)
|
||||||
|
|
||||||
|
文章编辑页有一个「置顶」开关。打开后这篇文章排在首页分轨、归档、书架三处列表的最前面,列表里显示一个灰阶「置顶」小徽标。
|
||||||
|
|
||||||
|
语义边界(刻意如此,别当 bug 报):
|
||||||
|
- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱
|
||||||
|
- **支持多篇/多本同时置顶**:置顶组内部仍按日期倒序;书架页(/books)里每个置顶的书评都会排在前面并**默认展开**
|
||||||
|
- 书架页用「↑ + 横线」的回到顶部图标表示置顶(不写文字),与右侧展开箭头形状不同,不会混读
|
||||||
|
- 关掉开关立即恢复时间序,没有"冷却期"
|
||||||
|
|
||||||
|
## 瞬间(5h 起与文章同级)
|
||||||
|
|
||||||
|
「瞬间」集合的「文字」字段是 **markdown 编辑器**,与文章同一套表达力:
|
||||||
|
|
||||||
|
- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` ``
|
||||||
|
- 排版类 HTML:`<span class="c-blue font-kai">彩色 + 楷体</span>`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单)
|
||||||
|
- 彩色**不再限次**(原来的"一页最多三处"已取消);仍守外壳灰阶:导航/页脚/按钮/背景不染色
|
||||||
|
|
||||||
|
**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。
|
||||||
|
|
||||||
|
**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。
|
||||||
|
|
||||||
|
## 照片说明文字(灯箱右侧)支持排版(5i 新增)
|
||||||
|
|
||||||
|
照片的「说明」(caption)现在支持 markdown 与排版类,与文章正文同一套:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
caption: 落日把海与城一齐烫成金箔,**这一帧等了二十分钟**
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="c-blue">蓝色</span>、<span class="font-kai">楷体</span> 这些类都能用。
|
||||||
|
```
|
||||||
|
|
||||||
|
灯箱是深底,颜色类会自动取柔和对暗底的那一档,不会刺眼。EXIF 那几行是相机数据,不做排版。
|
||||||
|
|
||||||
|
## 照片上传流
|
||||||
|
|
||||||
|
1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt
|
||||||
|
2. 图片字段 → 资产库上传 jpg(与 sidecar 同目录落位,零重压缩)
|
||||||
|
3. Save → 自动 commit + 构建 → 画廊出现(EXIF 构建期自动提取,exif 字段留空即可)
|
||||||
|
|
||||||
|
## 块编辑:关于页的块式页面构建器
|
||||||
|
|
||||||
|
关于页(`content/site/about.yaml`)的正文由 **blocks 列表**构成,后台里是一串可拖拽排序的卡片。每个块都有「显示」开关;关掉只是不渲染,块本身留在数据里。
|
||||||
|
|
||||||
|
两条渲染纪律:
|
||||||
|
|
||||||
|
- 列表为空(或全关)时,页面回落到 `intro` 字段的 markdown 正文;`intro` 也空则用内置默认文案。三级回落,任何一级为空都不会出白页。
|
||||||
|
- 未知块型(手工编辑 yaml 写错的 `type`)会被**跳过并打印 `[blocks]` 警告**,不会炸构建;块数上限 24,单块文本上限 4000 字,超限截断并 WARN。
|
||||||
|
|
||||||
|
九类块与字段(文本字段一律 `{zh, en}` 双语对象):
|
||||||
|
|
||||||
|
| 块型 | 必填 | 可选 | 用途 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `prose` 正文 | `body` | `title`、`width`(`narrow` 版心 / `full` 通栏) | markdown 长文,最常用 |
|
||||||
|
| `tracks` 版图 | `title`、`items[].name` | `items[].icon`、`items[].desc` | 几条主线 / 能力面,图标白名单外或留空显示首字占位 |
|
||||||
|
| `cards` 卡片墙 | `items[].title` | `title`、`columns`(2 或 3,默认 3)、`items[].desc/href/tag` | 并列展示若干条目 |
|
||||||
|
| `timeline` 时间线 | `items[].time`、`items[].title` | `title`、`items[].desc` | 时间列是自由文本(`2024 — 2026` 也行) |
|
||||||
|
| `quote` 引文 | `text` | `cite` | 单句金句,配 `font-kai` 更出效果 |
|
||||||
|
| `figure` 图片 | `image` | `alt`、`caption` | **只能后台上传**(落 `content/site/assets/`,走图片管线),禁止外链 |
|
||||||
|
| `linklist` 链接清单 | `items[].label`、`items[].href` | `title`、`items[].note` | 相关阅读 / 外部资源 |
|
||||||
|
| `cta` 行动按钮 | `text`、`href`、`label` | — | 一页建议不超过一个 |
|
||||||
|
| `divider` 分隔 | — | `style`(`line` 细线 / `space` 纯留白) | 长页分段 |
|
||||||
|
|
||||||
|
yaml 形态示例(手工编辑时照此写):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
blocks:
|
||||||
|
- type: prose
|
||||||
|
title: { zh: 关于这个站点, en: About this site }
|
||||||
|
body:
|
||||||
|
zh: 这里写 markdown 正文,支持列表、粗斜体、链接与排版类。
|
||||||
|
en: Markdown body here.
|
||||||
|
width: narrow
|
||||||
|
- type: quote
|
||||||
|
text: { zh: 所谓暮色,就是光慢慢地承认自己要走。, en: Dusk is light admitting it has to go. }
|
||||||
|
cite: { zh: 本站, en: this site }
|
||||||
|
- type: figure
|
||||||
|
image: ./assets/desk.jpg # 后台会替你写成这个相对路径
|
||||||
|
caption: { zh: 工作台一角, en: A corner of the desk }
|
||||||
|
```
|
||||||
|
|
||||||
|
其它可用字段:`layout.showIdCard`(关掉则不渲染名片区,默认开)、`typography`(见下一节)。
|
||||||
|
|
||||||
|
**预览限制**:后台只提供字段编辑与列表摘要(摘要显示块型名),块的真实排版以下次构建后的页面为准——想调间距 / 字号,请用「主题设置」而不是在块里硬凑空行。
|
||||||
|
|
||||||
|
## 页面排版覆盖(每页一处)
|
||||||
|
|
||||||
|
首页 / 关于页 / 开源页各自有一个「页面排版覆盖(可选)」对象:
|
||||||
|
|
||||||
|
| 字段 | 取值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `font` | 字族下拉(10 项) | 本页**正文**默认字体类 |
|
||||||
|
| `accent` | 颜色下拉(18 项) | 本页强调色类 |
|
||||||
|
| `scale` | 0.8–1.25 | 字号缩放,1 = 全站默认 |
|
||||||
|
|
||||||
|
- 只作用于**该页内容容器**(挂 `.page-typo` + CSS 变量),不写全站 tokens,其它页面零影响。
|
||||||
|
- **体积成本提醒**:整页换艺术/展示字族(`font-huiwen` 汇文、`font-kuaile` 快乐体)意味着整页正文都要下载对应切片——单页字体成本会明显抬升,见写作指南的体积账。
|
||||||
|
- 白名单外的类名会被忽略(构建期过滤),写错不报错、也不生效。
|
||||||
|
|
||||||
|
## 主题设置(全站 17 个旋钮)
|
||||||
|
|
||||||
|
「主题设置」入口写 `content/site/theme.yaml`,17 项全部可选,留空 = 仓库默认值。**配错不会炸站**:越界值回落默认,组内单调性被破坏则整组回落。
|
||||||
|
|
||||||
|
| 组 | 字段 | 范围 | 约束语义 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 强调色 | `accent_light` / `accent_dark` | `#rrggbb` | 明暗各一份;构建期对比度闸门断言正文 ≥4.5:1,不达标构建失败 |
|
||||||
|
| 圆角 | `radius_img` | 4–16 | 图片圆角,建议 ≤14 |
|
||||||
|
| 标题字号 | `text_h1` / `text_h2` / `text_h3` | h1/h2:22–32,h3:16–32 | **必须严格递减**(h1 > h2 > h3),否则三档一起回落默认 |
|
||||||
|
| 正文字号 | `prose_size_reading` / `prose_size_tech` | 16–22 | 书评 / 技术文分轨独立 |
|
||||||
|
| 行高 | `leading_reading` / `leading_tech` | 1.5–2.0 | 同上分轨 |
|
||||||
|
| 间距梯度 | `space_3` / `space_4` / `space_5` / `space_6` | 8–80 | **必须严格递增**,否则整组回落 |
|
||||||
|
| 流距 | `flow_p` / `flow_h2_post` | 0–2(em) | 段间距 / 标题下间距 |
|
||||||
|
| 动效 | `dur` | 150–600(ms) | 过渡时长;`prefers-reduced-motion` 下自动归零 |
|
||||||
|
|
||||||
|
不能配的东西(有意为止):版心宽度、断点、纸墨色、代码配色——它们牵动版面结构与分轨身份,属于设计红线。注入方式是构建期内联 `:root:root{…}`,运行时零 JS、零请求。
|
||||||
|
|
||||||
|
## 首页与开源页的其余旋钮
|
||||||
|
|
||||||
|
### 首页三字段(5g 新增)
|
||||||
|
|
||||||
|
| 字段 | 取值 | 默认 | 作用 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `projectsLimit` | 1–99 | 6 | 首页项目墙条数(越界夹取) |
|
||||||
|
| `announcement` | `{zh, en}` markdown | 空 | 名片区下方公告;**空 = 不渲染**,走富文本管线(可写排版类 span) |
|
||||||
|
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨,该轨文章不在首页列出(归档页仍可见) |
|
||||||
|
|
||||||
|
### 开源页(opensource.yaml)
|
||||||
|
|
||||||
|
**开篇是原生「GitHub 档案卡」(5h 起)**:头像 / 名字@handle / 定位叙述 / 社交图标 / 统计四格(关注者、公开仓、总星标)/ 语言占比灰阶条(只有第一名用强调色,其余灰阶)。**零第三方图片、零 iframe**。数据失败只隐藏对应数字,不会破版。
|
||||||
|
|
||||||
|
想换那段「定位叙述」,改本文件的 `blurb` + `blurb_after`(双语,后台可改)——它们现在同时供给档案卡与项目墙下方的说明。
|
||||||
|
|
||||||
|
下面的 `intro_from: "github-readme"` **仍可用但不推荐**:那会把 GitHub 专用 README 原样渲染,带 7 类外链图(shields.io、readme-typing-svg、github-readme-stats、streak-stats、activity-graph、snake、komarev 访客徽章)与 `<div align="center">` 居中——本站按普通块渲染,故居中失效;外链图在慢网或被墙时全空白。这也是当初"部分内容没居中、有些图片没显示"的根因。
|
||||||
|
|
||||||
|
| 字段 | 取值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `pinned` | 仓短名数组 | 置顶顺序即展示顺序 |
|
||||||
|
| `mode` | `all` / `only` / `exclude` | 可见性模式 |
|
||||||
|
| `include` / `exclude` | 仓短名数组 | 对应模式的白 / 黑名单 |
|
||||||
|
| `sort` | `pinned-first` / `stars` / `updated` | 排序 |
|
||||||
|
| `perSource` | `both` / `github` / `gitea` | 来源过滤 |
|
||||||
|
| `intro_from` | `"off"` / `"github-readme"` | 开篇文案来源(**带引号写**,YAML 1.1 里裸 `off` 是布尔 false) |
|
||||||
|
|
||||||
|
**`mode: only` 的"看不见"排查**:构建日志会给每个没显示出来的 include 项打一行 `[opensource] projects:include 里的「X」…`,并区分两种原因——双源结果里根本没有这个仓(拼写 / 私有仓 / 该源刚失败),或者命中了但被 `perSource` 过滤掉。看到哪一行就知道该改什么。
|
||||||
|
|
||||||
|
**README 开篇(`intro_from: "github-readme"`)的回落矩阵**,构建日志逐级点名:
|
||||||
|
|
||||||
|
1. 取到 → 内容有变化才刷新缓存 `content/.github-readme.md`(入仓,保证构建确定性)
|
||||||
|
2. token 缺位 / HTTP 非 2xx / 内容为空 / 超时(10 秒)/ 网络异常 → 改用缓存(日志标注"可能陈旧")
|
||||||
|
3. 缓存也不在 → 回落本文件 `blurb` 的叙述文案
|
||||||
|
4. 缓存回写失败(构建仓只读挂载等)→ 只告警,本次渲染照常
|
||||||
|
|
||||||
|
token 位在 `~/docker_apps/blog/github-token.txt`(本机,0600)。
|
||||||
|
|
||||||
|
**两条已知限制**:
|
||||||
|
|
||||||
|
- **README 里的相对链接不重写**(如 `./img/x.png`、`docs/foo.md`):它们相对 GitHub 仓根解析,在本站没有对应产物,点了多半 404。保原样比猜一个基址可信,所以不做改写。
|
||||||
|
- **外部徽章图片加固但不参与构建**:shields.io、readme-typing-svg 这类第三方徽章在构建期**不抓取**,渲染时统一补 `loading="lazy"`(缺 alt 补空),它们慢、被墙或 404 都不影响页面渲染;尺寸已由样式限制在内容宽度内。
|
||||||
|
|
||||||
|
## 浏览与评论计数(5h 新增)
|
||||||
|
|
||||||
|
文章页、瞬间页、开源页、关于页都用同一枚灰阶徽标:**眼睛图标 = 浏览数,气泡图标 = 条评论**,数据来自自建 Artalk,不依赖任何第三方徽章服务。
|
||||||
|
|
||||||
|
几条行为约定(省得当成 bug):
|
||||||
|
|
||||||
|
- **一页只计一次浏览**:「计一次」发生在这页唯一那个 `page` 模式徽标(文章页在标题下方),同一页其余徽标都是只读镜像,显示的是同一个数字——不会出现"页头 17、评论区 16"
|
||||||
|
- **开评论不算浏览**:瞬间逐条徽标是只读的,读者展开评论区不会给那条动态虚增浏览量
|
||||||
|
- **接口挂掉显示「–」而不是 0**:宁可留白,也不给读者看一个错误的零
|
||||||
|
- **换页重计、同页刷新重计**:这是浏览量的定义,不是重复计数的 bug(软导航回同一页也算新一次访问)
|
||||||
|
|
||||||
|
## 纪律(与写作指南同源)
|
||||||
|
|
||||||
|
- **图片体积**:上传前自行压缩(长边 ≤2400px)——Sveltia 内建优化**刻意关闭**(会抹 EXIF 断构建期提取管线)
|
||||||
|
- **删除**:删条目会连带删除被其独占引用的 jpg(实测行为,官方文档说的"不连带"是错的)
|
||||||
|
- EXIF 覆盖字段一般留空;文章 markdown 仍建议本地/gitea web 写(Sveltia 只管照片)
|
||||||
|
- 保存直连 main 无分支缓冲——webhook 构建是安全网,构建失败 gitea 的 webhook 投递记录可查
|
||||||
|
|
||||||
|
## 已知限制
|
||||||
|
|
||||||
|
- 启动时探测 /api/forgejo 报 404 属良性回落(日志噪音,无影响)
|
||||||
|
- 大视频仍走仓外 store + /media/ 绝对路径(见写作指南体积纪律表)
|
||||||
@@ -1,20 +1,64 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# 构建管线:gitea push(main) → webhook → 本脚本 → dist rsync 到 www-staging
|
# 构建管线(Phase 5c 裸 clone 改造):构建源 = origin/main 硬跟,非本地工作树。
|
||||||
# Phase 1:真实 astro build;webhook 侧 HMAC 验签仍留 Phase 4 实装(见 webhook-receive.sh)
|
#
|
||||||
|
# 为什么:此前直接构建 ~/Code/Github/my-blog 工作树——分支/未提交状态会
|
||||||
|
# 意外上线(Sveltia 冒烟期实测发生过);且 Sveltia 的 main 提交无法自动
|
||||||
|
# 发布。改为独立构建仓 fetch+reset 硬跟 main:线上=远端 main,确定性闭环。
|
||||||
|
# blog 仓已公开,匿名 fetch 无需凭据。
|
||||||
set -e
|
set -e
|
||||||
REPO_DIR="$HOME/Code/Github/my-blog" # Phase 4 构建机改裸 clone 路径
|
# 抓取 URL:默认公网;同机构建可由宿主环境变量覆盖为内网
|
||||||
|
# (2026-10-26 事故:ESA 边缘对 git.wait4x.online 522 时公网 fetch 失败,
|
||||||
|
# set -e 下静默死、staging 冻结——内网通道必须可用)
|
||||||
|
BLOG_GIT_URL="${BLOG_GIT_URL:-https://git.wait4x.online/wait4x/blog.git}"
|
||||||
|
BUILD_REPO="$HOME/docker_apps/blog/build-repo"
|
||||||
STAGING="$HOME/docker_apps/blog/www-staging"
|
STAGING="$HOME/docker_apps/blog/www-staging"
|
||||||
|
|
||||||
echo "[build] pnpm install --frozen-lockfile + astro build"
|
if [ ! -d "$BUILD_REPO/.git" ]; then
|
||||||
cd "$REPO_DIR"
|
echo "[build] 首次克隆构建仓"
|
||||||
|
git clone "$BLOG_GIT_URL" "$BUILD_REPO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$BUILD_REPO"
|
||||||
|
# 每次构建把 origin 指向配置的 URL(同机构建走内网通道;公网 522 时仍可发布)
|
||||||
|
git remote set-url origin "$BLOG_GIT_URL"
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
git clean -fd --exclude node_modules
|
||||||
|
SHA=$(git rev-parse --short HEAD)
|
||||||
|
echo "[build] 构建源: origin/main @ $SHA"
|
||||||
|
|
||||||
pnpm install --frozen-lockfile >/dev/null
|
pnpm install --frozen-lockfile >/dev/null
|
||||||
pnpm astro build
|
pnpm astro build
|
||||||
|
|
||||||
|
# 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链;
|
||||||
|
# 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉
|
||||||
|
[ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过"
|
||||||
|
[ -f scripts/build-perf/config-check.py ] && python3 scripts/build-perf/config-check.py || echo "[build] config-check 未就绪,跳过"
|
||||||
|
[ -f scripts/build-perf/contrast-check.mjs ] && node scripts/build-perf/contrast-check.mjs dist || echo "[build] contrast 脚本未就绪,跳过"
|
||||||
|
# 首页阻塞 CSS 内联(车道 D):守卫先行同上——脚本未到 main 前绝不炸链
|
||||||
|
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
||||||
|
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
||||||
|
|
||||||
|
# README 动效资产(5i T8):**不在此跑抓取**(避免发布链被外网抖动拖累),只做存在性守卫 + 缺资产告警
|
||||||
|
if [ -f public/github-assets/manifest.json ]; then
|
||||||
|
echo "[build] github-assets 就绪:$(find public/github-assets -name '*.svg' | wc -l) 个自托管 SVG"
|
||||||
|
else
|
||||||
|
echo "[build] WARN public/github-assets/manifest.json 缺失——开源页数据板将只显示原生统计(可手动跑 node scripts/build-perf/fetch-readme-assets.mjs 补齐)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
|
||||||
|
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
|
||||||
|
|
||||||
echo "[build] rsync -> $STAGING"
|
echo "[build] rsync -> $STAGING"
|
||||||
mkdir -p "$STAGING"
|
mkdir -p "$STAGING"
|
||||||
rsync -a --delete "$REPO_DIR/dist/" "$STAGING/"
|
rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
|
||||||
# 原图仓独立同步(不进 git/dist):/photos-originals/ 供图库"查看原图/下载"
|
|
||||||
ORIG="$HOME/docker_apps/blog/photos-originals"
|
ORIG="$HOME/docker_apps/blog/photos-originals"
|
||||||
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
||||||
printf 'built at %s\n' "$(date -Is)" > "$STAGING/.build-stamp"
|
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
|
||||||
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files"
|
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
|
||||||
|
|
||||||
|
# 生产冒烟(T3):staging 三断言。首版容忍失败(robots 待 T1 落地,smoke FAIL 属预期),
|
||||||
|
# T1 收编后改硬门:去掉 `|| echo` 让断言失败直接炸构建
|
||||||
|
if [ -f scripts/build-perf/prod-smoke.sh ] && [ "${SMOKE_BASE:-http://127.0.0.1:18743}" != "off" ]; then
|
||||||
|
bash scripts/build-perf/prod-smoke.sh "${SMOKE_BASE:-http://127.0.0.1:18743}" || echo "[build] prod-smoke 未过(见上;T1 robots 落地后转硬门)"
|
||||||
|
fi
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
ESA 只代理 80/443 vhost,该端口不对公网;HMAC 是硬门槛。
|
ESA 只代理 80/443 vhost,该端口不对公网;HMAC 是硬门槛。
|
||||||
|
|
||||||
触发链:gitea push(main) → 本接收器验签 → build.sh → www-staging 更新。
|
触发链:gitea push(main) → 本接收器验签 → build.sh → www-staging 更新。
|
||||||
|
部署形态:systemd 运行副本 ~/docker_apps/blog/webhook_receiver.py——改本文件
|
||||||
|
必须同步两份(仓内源 + 运行副本)并 restart 单元,漂移即事故。
|
||||||
常驻方式:systemd --user(blog-webhook.service);reboot 自启需一次性
|
常驻方式:systemd --user(blog-webhook.service);reboot 自启需一次性
|
||||||
`sudo loginctl enable-linger wait4x`(已列入上线检查单)。
|
`sudo loginctl enable-linger wait4x`(已列入上线检查单)。
|
||||||
"""
|
"""
|
||||||
@@ -21,9 +23,21 @@ from pathlib import Path
|
|||||||
|
|
||||||
PORT = 13021
|
PORT = 13021
|
||||||
SECRET_FILE = Path.home() / "docker_apps/gitea/webhook-secret.txt"
|
SECRET_FILE = Path.home() / "docker_apps/gitea/webhook-secret.txt"
|
||||||
BUILD = Path.home() / "Code/Github/my-blog/infra/deploy/build.sh"
|
BUILD_REPO = Path.home() / "docker_apps/blog/build-repo"
|
||||||
BUILD_LOCK = threading.Lock()
|
BUILD_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
# 构建脚本 = 构建仓自己的 origin/main 版本(架构修正 2026-10-25:此前指向
|
||||||
|
# 工作树副本,车道在途半成品状态会断生产链——20:31 事故实锤)。构建仓缺失时
|
||||||
|
# 首克引导(blog 仓公开,匿名 clone)。
|
||||||
|
def build_script() -> Path:
|
||||||
|
s = BUILD_REPO / "infra/deploy/build.sh"
|
||||||
|
if not s.exists():
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", "https://git.wait4x.online/wait4x/blog.git", str(BUILD_REPO)],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def read_secret() -> bytes:
|
def read_secret() -> bytes:
|
||||||
return SECRET_FILE.read_text().strip().encode()
|
return SECRET_FILE.read_text().strip().encode()
|
||||||
@@ -71,11 +85,15 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def _run_build(self) -> None:
|
def _run_build(self) -> None:
|
||||||
with BUILD_LOCK:
|
with BUILD_LOCK:
|
||||||
subprocess.run(
|
# 输出落盘:capture_output 丢弃是调试黑洞(2026-10-26 ESA 522
|
||||||
["bash", str(BUILD)],
|
# 事件中构建静默死、staging 冻结无任何可见痕迹)
|
||||||
capture_output=True, timeout=1200,
|
log = Path.home() / "docker_apps/blog/build.log"
|
||||||
cwd=str(BUILD.parent),
|
with open(log, "wb") as fh:
|
||||||
)
|
subprocess.run(
|
||||||
|
["bash", str(build_script())],
|
||||||
|
stdout=fh, stderr=subprocess.STDOUT, timeout=1200,
|
||||||
|
cwd=str(BUILD_REPO),
|
||||||
|
)
|
||||||
|
|
||||||
def log_message(self, fmt: str, *args: object) -> None:
|
def log_message(self, fmt: str, *args: object) -> None:
|
||||||
# 静默常规访问日志(健康探针会刷屏);错误仍走默认 stderr
|
# 静默常规访问日志(健康探针会刷屏);错误仍走默认 stderr
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ sqlite 直接热拷贝会撕裂,备份前 `docker compose down` 或用 `sqlite
|
|||||||
|
|
||||||
## 已知坑
|
## 已知坑
|
||||||
|
|
||||||
|
- **[cors] 段已启用(2026-10-25,Sveltia CMS 试水)**:`ALLOW_DOMAIN = https://wait4x.online`(试水期的本地源已于收尾移除);`HEADERS` 必须含 `Authorization`(token 认证跨源必需,默认仅 Content-Type,User-Agent)。依据:docs.gitea.com config-cheat-sheet#cors
|
||||||
|
|
||||||
- **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403,
|
- **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403,
|
||||||
而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。
|
而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。
|
||||||
- nginx 的 `client_max_body_size 100M`(git push 大 pack)与 app.ini
|
- nginx 的 `client_max_body_size 100M`(git push 大 pack)与 app.ini
|
||||||
|
|||||||
@@ -12,15 +12,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/rss": "^4.0.19",
|
"@astrojs/rss": "^4.0.19",
|
||||||
"@astrojs/sitemap": "^3.7.4",
|
"@astrojs/sitemap": "^3.7.4",
|
||||||
|
"@fontsource-variable/noto-sans-sc": "^5.3.1",
|
||||||
|
"@fontsource-variable/noto-serif-sc": "^5.3.1",
|
||||||
"@fontsource/jetbrains-mono": "^5.3.0",
|
"@fontsource/jetbrains-mono": "^5.3.0",
|
||||||
"@fontsource/noto-sans-sc": "^5.3.1",
|
"@fontsource/noto-sans-sc": "^5.3.1",
|
||||||
"astro": "^7.3.8",
|
"@fontsource/zcool-kuaile": "^5.3.0",
|
||||||
|
"astro": "^7.4.4",
|
||||||
"astro-pagefind": "^2.0.1",
|
"astro-pagefind": "^2.0.1",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"lxgw-wenkai-screen-webfont": "^1.7.0",
|
"lxgw-wenkai-screen-webfont": "^1.7.0",
|
||||||
|
"pdfjs-dist": "^6.4.299",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
"sharp": "^0.35.5"
|
"sharp": "^0.35.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@astrojs/markdown-remark": "^7.3.5",
|
||||||
"@fontsource/noto-serif-sc": "^5.3.1",
|
"@fontsource/noto-serif-sc": "^5.3.1",
|
||||||
"playwright": "^1.64.1"
|
"playwright": "^1.64.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,626 @@
|
|||||||
|
# yaml-language-server: $schema=https://unpkg.com/@sveltia/cms/schema/sveltia-cms.json
|
||||||
|
# Sveltia CMS 配置(T3 试水):仅 photos 集合,见 docs 预研报告
|
||||||
|
# 纪律:不开内建图片优化(transformations 会把原图转 webp 抹掉 EXIF,构建期提取报废)
|
||||||
|
backend:
|
||||||
|
name: gitea
|
||||||
|
repo: wait4x/blog
|
||||||
|
branch: main
|
||||||
|
# 走博客域同源反代(nginx /gitea/ → 127.0.0.1:13020):
|
||||||
|
# ①绕开 git.wait4x.online 的 ESA 522 ②同源无 CORS ③ESA 缓存规则已排除 /artalk/api
|
||||||
|
base_url: https://wait4x.online/gitea
|
||||||
|
api_root: https://wait4x.online/gitea/api/v1
|
||||||
|
|
||||||
|
media_folder: content/photos
|
||||||
|
site_url: https://wait4x.online
|
||||||
|
|
||||||
|
output:
|
||||||
|
# Astro zod optional 兼容:空可选字段不写文件(官方 Astro 指南建议)
|
||||||
|
omit_empty_optional_fields: true
|
||||||
|
|
||||||
|
collections:
|
||||||
|
# 文章(含书评/技术/照片三轨;书=category:reading 的文章,同一集合管理)
|
||||||
|
- name: posts
|
||||||
|
label: 文章
|
||||||
|
label_singular: 文章
|
||||||
|
folder: content/posts
|
||||||
|
create: true
|
||||||
|
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
||||||
|
slug: '{{slug}}'
|
||||||
|
summary: '{{pubDate}} · {{title}}'
|
||||||
|
sortable_fields: [pubDate, title]
|
||||||
|
# 注意:Sveltia 对 folder 集合的 media_folder 按"相对集合目录"解析
|
||||||
|
# (写成 content/posts/assets 会二次嵌套成 content/posts/content/posts/assets,
|
||||||
|
# 实测 2026-11-02 首上传即踩),故这里必须写 assets
|
||||||
|
media_folder: assets
|
||||||
|
public_folder: ./assets
|
||||||
|
fields:
|
||||||
|
- { label: 标题, name: title, widget: string }
|
||||||
|
- { label: 日期, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
||||||
|
- { label: 分轨, name: category, widget: select, options: [{label: 阅读, value: reading}, {label: 技术, value: tech}, {label: 照片, value: photo}] }
|
||||||
|
- { label: 标签, name: tags, widget: list, required: false }
|
||||||
|
- { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶文章排在首页/归档/书架最前;只影响顺序,不改日期、URL 与 RSS 时间语义' }
|
||||||
|
- { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' }
|
||||||
|
- { label: 原书作者, name: bookAuthor, widget: string, required: false }
|
||||||
|
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false }
|
||||||
|
- { label: 书封图, name: cover, widget: image, required: false, hint: '上传后自动生成,无需手填路径;仅阅读轨展示' }
|
||||||
|
- label: 附视频(可选)
|
||||||
|
name: video
|
||||||
|
widget: file
|
||||||
|
required: false
|
||||||
|
media_folder: public/media
|
||||||
|
public_folder: /media
|
||||||
|
hint: '展示在正文之后;mp4/webm ≤50MB(走 public/media,构建时原样拷贝,不做转码)'
|
||||||
|
- label: 视频封面(可选)
|
||||||
|
name: poster
|
||||||
|
widget: string
|
||||||
|
required: false
|
||||||
|
hint: '填 /media/xxx.jpg(先上传到 public/media 取路径);缺省浏览器自取首帧'
|
||||||
|
- { label: 隐藏(不进列表/sitemap), name: hiddenTag, widget: boolean, required: false, hint: '对应 tags 含 hidden;保存后请人工核对' }
|
||||||
|
- { label: 草稿, name: draft, widget: boolean, required: false, default: false }
|
||||||
|
- { label: 正文, name: body, widget: markdown }
|
||||||
|
|
||||||
|
# 主题旋钮(样式即数据 · 层 4):只放白名单内的 9 组,越界值会被构建期钳制
|
||||||
|
- name: site_theme
|
||||||
|
label: 主题设置
|
||||||
|
# 文件集合必须是 files: [{file,label,fields}] 结构;把 file 直接写在集合层级会被 Sveltia 判非法(2026-11-02 实测报 "The file option ... is invalid")
|
||||||
|
files:
|
||||||
|
- name: theme
|
||||||
|
label: 主题设置
|
||||||
|
file: content/site/theme.yaml
|
||||||
|
format: yaml
|
||||||
|
fields:
|
||||||
|
- { label: 强调色(亮), name: accent_light, widget: color, required: false, hint: '亮色模式强调色,如 #3454d1;构建期校验正文对比度 ≥4.5' }
|
||||||
|
- { label: 强调色(暗), name: accent_dark, widget: color, required: false, hint: '暗色模式强调色,如 #8ea2ff' }
|
||||||
|
- { label: 图片圆角, name: radius_img, widget: number, required: false, value_type: int, min: 4, max: 16, hint: '范围 4–16(建议 ≤14)' }
|
||||||
|
- { label: 一级标题字号, name: text_h1, widget: number, required: false, value_type: int, min: 22, max: 32, hint: '22–32,且必须大于 h2' }
|
||||||
|
- { label: 二级标题字号, name: text_h2, widget: number, required: false, value_type: int, min: 22, max: 32, hint: '22–32,必须小于 h1、大于 h3' }
|
||||||
|
- { label: 三级标题字号, name: text_h3, widget: number, required: false, value_type: int, min: 16, max: 32, hint: '16–32,必须小于 h2' }
|
||||||
|
- { label: 书评正文字号, name: prose_size_reading, widget: number, required: false, value_type: int, min: 16, max: 22, hint: '16–22' }
|
||||||
|
- { label: 技术文正文字号, name: prose_size_tech, widget: number, required: false, value_type: int, min: 16, max: 22, hint: '16–22' }
|
||||||
|
- { label: 书评行高, name: leading_reading, widget: number, required: false, value_type: float, min: 1.5, max: 2.0, step: 0.05, hint: '1.5–2.0' }
|
||||||
|
- { label: 技术文行高, name: leading_tech, widget: number, required: false, value_type: float, min: 1.5, max: 2.0, step: 0.05, hint: '1.5–2.0' }
|
||||||
|
- { label: 间距 3, name: space_3, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 < 间距 4' }
|
||||||
|
- { label: 间距 4, name: space_4, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 3 且 < 间距 5' }
|
||||||
|
- { label: 间距 5, name: space_5, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 4 且 < 间距 6' }
|
||||||
|
- { label: 间距 6, name: space_6, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 5' }
|
||||||
|
- { label: 段间距, name: flow_p, widget: number, required: false, value_type: float, min: 0, max: 2, step: 0.05, hint: '0–2(em)' }
|
||||||
|
- { label: 标题下间距, name: flow_h2_post, widget: number, required: false, value_type: float, min: 0, max: 2, step: 0.05, hint: '0–2(em)' }
|
||||||
|
- { label: 过渡时长, name: dur, widget: number, required: false, value_type: int, min: 150, max: 600, hint: '150–600(毫秒)' }
|
||||||
|
|
||||||
|
# 瞬间:随手发(text/image 至少其一——Sveltia 无法表达条件必填,两者都设可选,
|
||||||
|
# 保存前自查;pubDate 必填)。图片走图库媒体库(content/photos 同源可复用)
|
||||||
|
- name: moments
|
||||||
|
label: 瞬间
|
||||||
|
folder: content/moments
|
||||||
|
create: true
|
||||||
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||||
|
summary: '{{fields.pubDate}} · {{fields.text}}'
|
||||||
|
sortable_fields: [pubDate, text]
|
||||||
|
fields:
|
||||||
|
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
||||||
|
- label: 文字
|
||||||
|
name: text
|
||||||
|
widget: markdown
|
||||||
|
required: false
|
||||||
|
hint: '与图片至少填一项。支持 markdown(**加粗**、列表、> 引用)与排版类 HTML,例如 <span class="c-blue font-kai">彩色+楷体</span>;装饰色仍守“外壳灰阶、彩色只作强调”的纪律'
|
||||||
|
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' }
|
||||||
|
- label: 标签
|
||||||
|
name: tags
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
- name: photos
|
||||||
|
label: 图库照片
|
||||||
|
folder: content/photos
|
||||||
|
# 整文件 JSON(非 frontmatter)= 现有 sidecar 同构;不设 format 条目不列出
|
||||||
|
format: json
|
||||||
|
create: true
|
||||||
|
# 入口相对:jpg 与 sidecar 同目录;image 引用写裸文件名(loader new URL 等价解析)
|
||||||
|
media_folder: ''
|
||||||
|
public_folder: ''
|
||||||
|
summary: '{{fields.group}} · {{fields.title}}'
|
||||||
|
# 照片无 frontmatter 日期(JSON sidecar,时间在 exif/文件侧)——列表按标题/分组排序
|
||||||
|
sortable_fields: [title, group]
|
||||||
|
fields:
|
||||||
|
# title 必填:Astro schema z.string() 无 .optional(),omit 纪律下必需且有值才会落键(空 title 会让 main 不可构建)
|
||||||
|
- { label: 标题, name: title, widget: string, required: true }
|
||||||
|
- { label: 图片, name: image, widget: image }
|
||||||
|
- { label: 分组, name: group, widget: select, options: [street, landscape] }
|
||||||
|
- { label: 替代文本, name: alt, widget: string, required: false }
|
||||||
|
- { label: 人为描述, name: caption, widget: text, required: false, hint: 展示在灯箱里的人为描述(一句话策展文案) }
|
||||||
|
- label: EXIF 覆盖(一般留空,构建期自动从图提取)
|
||||||
|
name: exif
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 机身, name: make, widget: string, required: false }
|
||||||
|
- { label: 镜头, name: lens, widget: string, required: false }
|
||||||
|
- { label: 光圈值, name: fNumber, widget: number, required: false }
|
||||||
|
- { label: 曝光秒, name: exposureTime, widget: number, required: false }
|
||||||
|
- { label: ISO, name: iso, widget: number, required: false }
|
||||||
|
|
||||||
|
# 页面文案单例(cms 车道 A):三个文件型入口,保存即写 content/site/*.yaml,
|
||||||
|
# 字段与 Astro siteHome/siteAbout/siteOpenSource 的 schema 一一对应(zh/en 双字段,留空回落默认)
|
||||||
|
- name: site_pages
|
||||||
|
label: 页面文案
|
||||||
|
files:
|
||||||
|
- name: home
|
||||||
|
label: 主页设置
|
||||||
|
file: content/site/home.yaml
|
||||||
|
format: yaml
|
||||||
|
# 头像上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线优化)
|
||||||
|
media_folder: content/site/assets
|
||||||
|
public_folder: ./assets
|
||||||
|
fields:
|
||||||
|
- label: 身份语
|
||||||
|
name: tagline
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 首页大字身份语(双语,markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 页面排版覆盖(可选)
|
||||||
|
name: typography
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
|
||||||
|
fields:
|
||||||
|
- label: 正文字体类
|
||||||
|
name: font
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
hint: 整页正文换字族;艺术/展示类字族体积大。5i 新增六款(小薇/庆科黄油/马善政/柳建毛草/得意黑/新晰黑)均按站内用字切片,单页增量 27–46KB;站外用字(生僻字)会回落系统字族,需按 docs 里的命令重切
|
||||||
|
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
|
||||||
|
- label: 强调色类
|
||||||
|
name: accent
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||||
|
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||||
|
- label: 公告(可选,markdown)
|
||||||
|
name: announcement
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 渲染在名片区下方;留空则不显示公告块
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
|
- { label: 英文, name: en, widget: markdown, required: false }
|
||||||
|
- { label: 项目墙条数, name: projectsLimit, widget: number, required: false, value_type: int, min: 1, max: 99, hint: '首页项目墙显示几条(1–99,缺省 6)' }
|
||||||
|
- label: 轨细分开关(首页文章列表)
|
||||||
|
name: postsTracks
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 关掉某轨则该轨文章不在首页列出(全开=现状)
|
||||||
|
fields:
|
||||||
|
- { label: 阅读轨, name: reading, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 技术轨, name: tech, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 照片轨, name: photo, widget: boolean, required: false, default: true }
|
||||||
|
- label: 副标题(可选,markdown 短文本)
|
||||||
|
name: subtitle
|
||||||
|
widget: object
|
||||||
|
hint: 渲染在身份语下方;留空不渲染
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 最新影像张数
|
||||||
|
name: latestPhotos
|
||||||
|
widget: number
|
||||||
|
required: false
|
||||||
|
default: 4
|
||||||
|
min: 0
|
||||||
|
max: 6
|
||||||
|
hint: '1-6;填 0 = 隐藏「最新影像」区块。留空默认 4'
|
||||||
|
- label: 首页每轨文章条数
|
||||||
|
name: perTrackLimit
|
||||||
|
widget: number
|
||||||
|
required: false
|
||||||
|
default: 6
|
||||||
|
min: 4
|
||||||
|
max: 8
|
||||||
|
hint: 阅读轨/技术轨各显示条数,留空默认 6
|
||||||
|
- label: 区块开关
|
||||||
|
name: sections
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 关掉的区块不渲染(默认全开)
|
||||||
|
fields:
|
||||||
|
- { label: 文章列表, name: tracks, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 最新影像, name: photos, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 贡献热力图, name: heatmap, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 项目墙, name: projects, widget: boolean, required: false, default: true }
|
||||||
|
- label: 头像(可选)
|
||||||
|
name: avatar
|
||||||
|
widget: image
|
||||||
|
required: false
|
||||||
|
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
||||||
|
- label: 站点图标 favicon
|
||||||
|
name: favicon
|
||||||
|
widget: string
|
||||||
|
required: false
|
||||||
|
hint: 'public 下的路径,如 /icons/my.ico 或 /icons/my.svg;留空回退 /favicon.svg。经「图标」媒体库上传'
|
||||||
|
- label: 社交链接(顺序即展示顺序)
|
||||||
|
name: socials
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
summary: '{{fields.label}} → {{fields.url}}'
|
||||||
|
fields:
|
||||||
|
- { label: 名称(无障碍文字), name: label, widget: string }
|
||||||
|
- { label: 链接, name: url, widget: string }
|
||||||
|
- label: 图标
|
||||||
|
name: icon
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [github, rss, email, bilibili, x]
|
||||||
|
hint: '不在列表里的留空 → 显示纯文字按钮'
|
||||||
|
- name: about
|
||||||
|
label: 关于页
|
||||||
|
file: content/site/about.yaml
|
||||||
|
format: yaml
|
||||||
|
# 块内图片上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线)
|
||||||
|
media_folder: content/site/assets
|
||||||
|
public_folder: ./assets
|
||||||
|
fields:
|
||||||
|
- label: 页面块(拖拽排序;每块可折叠)
|
||||||
|
name: blocks
|
||||||
|
widget: list
|
||||||
|
typeKey: type
|
||||||
|
required: false
|
||||||
|
summary: '{{fields.type}}'
|
||||||
|
hint: '点「+ 添加」选块型。关掉某块用该块内的「显示」开关'
|
||||||
|
types:
|
||||||
|
- label: 正文(markdown)
|
||||||
|
name: prose
|
||||||
|
widget: object
|
||||||
|
hint: markdown 正文块;可选小标题与版心/通栏宽度,长文分段改内容用
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 小标题(可选)
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 正文(markdown)
|
||||||
|
name: body
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
|
- { label: 英文, name: en, widget: markdown, required: false }
|
||||||
|
- label: 宽度
|
||||||
|
name: width
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: narrow
|
||||||
|
options: [{ label: 版心宽度, value: narrow }, { label: 通栏, value: full }]
|
||||||
|
- label: 版图(图标+名称+描述)
|
||||||
|
name: tracks
|
||||||
|
widget: object
|
||||||
|
hint: 版图块:图标 + 名称 + 描述,适合「几条主线/能力面」式介绍
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 标题
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 条目
|
||||||
|
name: items
|
||||||
|
widget: list
|
||||||
|
summary: '{{fields.name.zh}}'
|
||||||
|
fields:
|
||||||
|
- label: 图标
|
||||||
|
name: icon
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [book, terminal, camera, code, git, rss, email, github, bilibili, x, star, link]
|
||||||
|
hint: 白名单外或留空 → 显示首字占位
|
||||||
|
- label: 名称
|
||||||
|
name: name
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 描述
|
||||||
|
name: desc
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: text, required: false }
|
||||||
|
- { label: 英文, name: en, widget: text, required: false }
|
||||||
|
- label: 卡片(2/3 列)
|
||||||
|
name: cards
|
||||||
|
widget: object
|
||||||
|
hint: 卡片墙(2/3 列):每卡 = 标题 + 描述 + 可选链接/角标
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 标题(可选)
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- { label: 列数, name: columns, widget: number, required: false, default: 3, min: 2, max: 3, value_type: int }
|
||||||
|
- label: 卡片条目
|
||||||
|
name: items
|
||||||
|
widget: list
|
||||||
|
summary: '{{fields.title.zh}}'
|
||||||
|
fields:
|
||||||
|
- label: 标题
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 描述
|
||||||
|
name: desc
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: text, required: false }
|
||||||
|
- { label: 英文, name: en, widget: text, required: false }
|
||||||
|
- { label: 链接(可选), name: href, widget: string, required: false }
|
||||||
|
- label: 角标(可选)
|
||||||
|
name: tag
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 时间线
|
||||||
|
name: timeline
|
||||||
|
widget: object
|
||||||
|
hint: 时间线:一条时间 + 标题 + 描述;时间可写自由文本(如 2024 — 2026)
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 标题(可选)
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 条目
|
||||||
|
name: items
|
||||||
|
widget: list
|
||||||
|
summary: '{{fields.time}} · {{fields.title.zh}}'
|
||||||
|
fields:
|
||||||
|
- { label: 时间, name: time, widget: string, hint: '自由文本,如 2024 — 2026' }
|
||||||
|
- label: 标题
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 描述(可选)
|
||||||
|
name: desc
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: text, required: false }
|
||||||
|
- { label: 英文, name: en, widget: text, required: false }
|
||||||
|
- label: 引文
|
||||||
|
name: quote
|
||||||
|
widget: object
|
||||||
|
hint: 单句引文 + 可选署名;配 font-kai 排版效果更好
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 引文
|
||||||
|
name: text
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: text, required: false }
|
||||||
|
- { label: 英文, name: en, widget: text, required: false }
|
||||||
|
- label: 出处(可选)
|
||||||
|
name: cite
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 图片
|
||||||
|
name: figure
|
||||||
|
widget: object
|
||||||
|
hint: 单图 + 可选说明;图片只能后台上传(落 content/site/assets),禁外链
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' }
|
||||||
|
- { label: 替代文本(无障碍), name: alt, widget: string, required: false }
|
||||||
|
- label: 图注(可选)
|
||||||
|
name: caption
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 链接清单
|
||||||
|
name: linklist
|
||||||
|
widget: object
|
||||||
|
hint: 链接清单(标签 + 链接 + 备注),适合「相关阅读 / 外部资源」
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 标题(可选)
|
||||||
|
name: title
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 链接条目
|
||||||
|
name: items
|
||||||
|
widget: list
|
||||||
|
summary: '{{fields.label.zh}} → {{fields.href}}'
|
||||||
|
fields:
|
||||||
|
- label: 名称
|
||||||
|
name: label
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- { label: 链接, name: href, widget: string }
|
||||||
|
- label: 备注(可选)
|
||||||
|
name: note
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 行动按钮
|
||||||
|
name: cta
|
||||||
|
widget: object
|
||||||
|
hint: 行动按钮(文案 + 链接 + 按钮字);一页建议不超过一个
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 说明文字
|
||||||
|
name: text
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- { label: 按钮链接, name: href, widget: string }
|
||||||
|
- label: 按钮文字
|
||||||
|
name: label
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 分隔
|
||||||
|
name: divider
|
||||||
|
widget: object
|
||||||
|
hint: 分隔块:细分隔线或纯留白,给长页分段
|
||||||
|
fields:
|
||||||
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
|
- label: 样式
|
||||||
|
name: style
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: line
|
||||||
|
options: [{ label: 细分隔线, value: line }, { label: 纯留白, value: space }]
|
||||||
|
- label: 名片区开关
|
||||||
|
name: layout
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 关掉则不显示头像/名字/社交(它们的数据在「主页设置」)
|
||||||
|
fields:
|
||||||
|
- { label: 显示名片区, name: showIdCard, widget: boolean, required: false, default: true }
|
||||||
|
- label: 正文回退(blocks 为空时用,markdown,多段)
|
||||||
|
name: intro
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 仅在「页面块」为空时渲染;与块同时存在时以块为准
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
|
- { label: 英文, name: en, widget: markdown, required: false }
|
||||||
|
- label: 页面排版覆盖(可选)
|
||||||
|
name: typography
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
|
||||||
|
fields:
|
||||||
|
- label: 正文字体类
|
||||||
|
name: font
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
|
||||||
|
- label: 强调色类
|
||||||
|
name: accent
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||||
|
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 }
|
||||||
|
- name: opensource
|
||||||
|
label: 开源页
|
||||||
|
file: content/site/opensource.yaml
|
||||||
|
format: yaml
|
||||||
|
fields:
|
||||||
|
- label: 打字机动效文案(可选,≤6 条)
|
||||||
|
name: typing
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
summary: '{{fields.zh}}'
|
||||||
|
hint: '档案卡顶部逐字打出的几行文案,2–6 条最佳;留空用内置默认。中英各一行,切换语言同步'
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string }
|
||||||
|
- { label: English, name: en, widget: string }
|
||||||
|
- label: 叙述(链接前)
|
||||||
|
name: blurb
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 富文本(markdown + 排版类),渲染在仓库链接之前
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
|
- label: 页面排版覆盖(可选)
|
||||||
|
name: typography
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
|
||||||
|
fields:
|
||||||
|
- label: 正文字体类
|
||||||
|
name: font
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
hint: 整页正文换字族;艺术/展示类字族体积大。5i 新增六款(小薇/庆科黄油/马善政/柳建毛草/得意黑/新晰黑)均按站内用字切片,单页增量 27–46KB;站外用字(生僻字)会回落系统字族,需按 docs 里的命令重切
|
||||||
|
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
|
||||||
|
- label: 强调色类
|
||||||
|
name: accent
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||||
|
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||||
|
- { label: 仓库链接地址, name: repo_url, widget: string, required: false }
|
||||||
|
- { label: 仓库链接文字, name: repo_label, widget: string, required: false }
|
||||||
|
- label: 叙述(链接后)
|
||||||
|
name: blurb_after
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: text, required: false }
|
||||||
|
- { label: 英文, name: en, widget: text, required: false }
|
||||||
|
- label: 置顶项目(仓库短名,顺序即展示顺序)
|
||||||
|
name: pinned
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
hint: '如 blog;留空则回落默认 blog。填写的名字须与仓库短名一致'
|
||||||
|
- label: 项目墙模式
|
||||||
|
name: mode
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: all
|
||||||
|
options: [{ label: 全部, value: all }, { label: 仅白名单, value: only }, { label: 排除黑名单, value: exclude }]
|
||||||
|
hint: only/exclude 需配合下面的名单
|
||||||
|
- label: 白名单(仓库短名)
|
||||||
|
name: include
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
hint: 'mode=仅白名单时生效,如 pi-board'
|
||||||
|
- label: 黑名单(仓库短名)
|
||||||
|
name: exclude
|
||||||
|
widget: list
|
||||||
|
required: false
|
||||||
|
hint: 'mode=排除黑名单时生效,如 blog'
|
||||||
|
- label: 排序
|
||||||
|
name: sort
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: pinned-first
|
||||||
|
options:
|
||||||
|
- { label: 置顶优先,其余按星数, value: pinned-first }
|
||||||
|
- { label: 星数降序, value: stars }
|
||||||
|
- { label: 最近更新, value: updated }
|
||||||
|
- label: 来源过滤
|
||||||
|
name: perSource
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: both
|
||||||
|
options: [{ label: 双源合并, value: both }, { label: 仅 GitHub, value: github }, { label: 仅自托管 gitea, value: gitea }]
|
||||||
|
- label: 开篇文案来源
|
||||||
|
name: intro_from
|
||||||
|
widget: select
|
||||||
|
required: false
|
||||||
|
default: 'off'
|
||||||
|
options:
|
||||||
|
- { label: 用本页叙述(默认), value: 'off' }
|
||||||
|
- { label: GitHub profile README, value: github-readme }
|
||||||
|
hint: 'github-readme = 构建期取 wait4xx/wait4xx 的 README 渲染为开篇;缓存入仓 content/.github-readme.md,取不到自动回落叙述'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>Sveltia CMS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- pinned 本地 bundle(0.252.0):官方模板仅此一个 script,无 CSS link、无 type=module -->
|
||||||
|
<script src="./sveltia-cms.js"></script>
|
||||||
|
<!-- 可视化排版选择:类清单(单一数据源)→ 编辑器组件注册(须在 CMS 之后、init 之前注册) -->
|
||||||
|
<script src="./typography-classes.js"></script>
|
||||||
|
<script src="./typography-editor.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||