feat(migrate): Halo 全量内容迁移(压缩版)——6文/21图/1瞬间 URL 1:1
- 正文源=线上渲染页(Halo 快照层 patch 残迹:泥潭/赎罪仅 2 字符),
pandoc html→gfm + tags API 显示名换算后分类:5 书评(五档情绪色)+1 技术
- 图库 21 张:长边 2400/q86 压缩入仓(原图 196M→9.5M——公开仓历史膨胀
与 nginx 100M 推送上限双杀;原图在 Phase 0 备份 tar 中永久留档)
- 瞬间 1 条;URL 1:1(6 slug 全等,零 301)
- 本提交由 7 个本地提交重写而成(清除已进历史的 196M blob,未推送过);
因 reset --soft 暂存竞态,一并包含 spec 归档文件
(.spec/specs/archive/2026-10-24_migrate-blog-content/)
验证: check_urls.py 6==6;astro build 11 页 exit 0(8.67s);
EXIF 管线跑通;staging 更新;halo healthy+200 全程未动
Co-authored-by: GLM-5.3
No files matched your search
@@ -13,3 +13,4 @@ dist/
|
|||||||
*.key
|
*.key
|
||||||
# 系统杂物
|
# 系统杂物
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.migration/
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Phase 2:Halo 内容迁移(6文/21图/1瞬间,URL 1:1)- 项目范围 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-10-25 00:35
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- Halo 正文内嵌图片走 /upload/ 相对路径,可经 127.0.0.1:18090 直下
|
||||||
|
- 引文(blockquote)经 pandoc 转 md 后结构保留,无需人工重排
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 Halo 全量导出脚本
|
||||||
|
- T2 文章转换入库
|
||||||
|
- T3 图库迁移
|
||||||
|
- T4 瞬间迁移
|
||||||
|
- T5 URL 1:1 断言
|
||||||
|
- T6 构建验收 + staging + 抽检清单 + 生产复查
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:导出/转换/断言脚本入 scripts/migrate/,一次性中间产物 .migration/(gitignore);真实内容直接覆盖同 slug 样例文件
|
||||||
|
- 暂不引入:评论迁移(存量为 0)、双语分流(Phase 3)、封面图 frontmatter 扩展
|
||||||
|
- 不做的抽象/配置化:不写通用 CMS 迁移框架,只服务本次 Halo 一次性迁移
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 双语翻译(Phase 3)、上线切换(Phase 4)、Halo 停用(Phase 5)、旧站评论(数为 0)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 双语翻译(Phase 3)、上线切换(Phase 4)、Halo 停用(Phase 5)、旧站评论(数为 0)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:6/6
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-10-25 00:35
|
||||||
|
- 范围:Phase 2:Halo 内容迁移(6文/21图/1瞬间,URL 1:1)- 项目范围
|
||||||
|
- 功能:T1 Halo 全量导出脚本;T2 文章转换入库;T3 图库迁移;T4 瞬间迁移;T5 URL 1:1 断言;T6 构建验收 + staging + 抽检清单 + 生产复查
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:729f4ac147adffc8fe37606508922d2f8bd2176f..729f4ac147adffc8fe37606508922d2f8bd2176f
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Halo 内容迁移路径:公共 API 只有摘要,正文真源=线上渲染页 article 块(DB snapshots 有 patch 残迹陷阱);标签内部名需 tags API 换显示名;moments schema 的 text 在 frontmatter。迁移器 scripts/migrate/ 幂等;spec verify 必须只读断言(重跑迁移器会改写产物弄过期指纹)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Phase 2 迁移抽检清单(T6)
|
||||||
|
|
||||||
|
## 自动断言(全过)
|
||||||
|
- URL 1:1:Halo 6 slugs == 新站 6 md(check_urls.py exit 0)
|
||||||
|
- 图库 21 jpg + sidecar;构建期 EXIF loader 跑通(构建 55.96s 含 21 张图片处理)
|
||||||
|
- 瞬间 1 条(@wait4x);生产 Halo healthy + 200
|
||||||
|
|
||||||
|
## 人工抽检建议(上线前用户过目)
|
||||||
|
- [ ] 泥潭/赎罪 两篇:Halo 快照层只有 2 字符 patch 残迹,迁移正文取自
|
||||||
|
线上渲染页全文(泥潭 1691 / 赎罪 1770 md 字符),请对读原站确认
|
||||||
|
- [ ] 书评封面图(等待戈多.png 等 5 张 /upload/ 图)未内嵌于正文(原站
|
||||||
|
亦如此,封面独立于正文);如需题图后续手动挂 frontmatter 扩展字段
|
||||||
|
- [ ] 瞬间正文仅 "@wait4x" 自提及(Halo 原数据如此),如需可删
|
||||||
|
- [ ] csge-lei-pei-zhi(技术文)确认标题「CS各类配置」与代码块渲染正常
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{"task": "task_0001", "cmd": "python3 scripts/migrate/halo_export.py && test -s .migration/posts.json -a -s .migration/photos.json -a -s .migration/moments.json", "exit": 0, "duration_s": 0.123, "stdout_tail_sha256": "699736f8797aff67c64059e37cba3aa37c1475775c72d5ba1c1dbf83eba3abd5", "tree_fp": "6793c11406ad40ef6df4c45c889f5cf5e126c90cbe4c4e3439b77f72388681d1", "at": "2026-10-24T16:31:05+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "python3 scripts/migrate/posts_to_md.py && pnpm astro build && ls content/posts/*.md | wc -l | grep -q 6", "exit": 0, "duration_s": 5.715, "stdout_tail_sha256": "1a67fee079b33106f1c7fd8dd7da59aee0ef6ee0cdd2d3efaa206b5c0c9a0a6b", "tree_fp": "6793c11406ad40ef6df4c45c889f5cf5e126c90cbe4c4e3439b77f72388681d1", "at": "2026-10-24T16:31:11+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "python3 scripts/migrate/photos_migrate.py && test $(ls content/photos/*.jpg | wc -l) -eq 21 && pnpm astro build", "exit": 0, "duration_s": 5.257, "stdout_tail_sha256": "da8745282748e678828d6f636c48e02d971f6c99168aebd83ddcc6d7a96b61e2", "tree_fp": "6793c11406ad40ef6df4c45c889f5cf5e126c90cbe4c4e3439b77f72388681d1", "at": "2026-10-24T16:31:17+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "python3 scripts/migrate/moments_migrate.py && test $(ls content/moments/*.md | wc -l) -ge 1 && pnpm astro build", "exit": 0, "duration_s": 5.518, "stdout_tail_sha256": "c9867e44e3eab1270dfbf958a6355b462312396629e36b89b6c56c2c6da58faf", "tree_fp": "da8f6bffaa1f34b8b18c67a01ad9c0506ad325f2cde2c38c2bea46252493eb73", "at": "2026-10-24T16:31:23+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "python3 scripts/migrate/check_urls.py", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "cf0053bb7b862b9081d57788becd7e9ae7cbaa8b672765c3f0e39843648d1176", "tree_fp": "da8f6bffaa1f34b8b18c67a01ad9c0506ad325f2cde2c38c2bea46252493eb73", "at": "2026-10-24T16:31:24+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "bash infra/deploy/build.sh && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy", "exit": 0, "duration_s": 8.065, "stdout_tail_sha256": "793c974ad37559720dfd96860b8ae4e24886465167e690258663037c6dc6f159", "tree_fp": "da8f6bffaa1f34b8b18c67a01ad9c0506ad325f2cde2c38c2bea46252493eb73", "at": "2026-10-24T16:31:32+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "python3 scripts/migrate/halo_export.py && test -s .migration/posts.json -a -s .migration/photos.json -a -s .migration/moments.json", "exit": 0, "duration_s": 0.124, "stdout_tail_sha256": "699736f8797aff67c64059e37cba3aa37c1475775c72d5ba1c1dbf83eba3abd5", "tree_fp": "90b9364bd579f0c17cc49becf9a9e63a2dca2a403456e4444cde7366d3a7e30a", "at": "2026-10-24T16:32:16+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "python3 scripts/migrate/posts_to_md.py && pnpm astro build && ls content/posts/*.md | wc -l | grep -q 6", "exit": 0, "duration_s": 5.468, "stdout_tail_sha256": "3de67faa5fc76d40243f6e91adb50985d0d1b721af6d6f643f0a79ff509d01db", "tree_fp": "90b9364bd579f0c17cc49becf9a9e63a2dca2a403456e4444cde7366d3a7e30a", "at": "2026-10-24T16:32:22+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "python3 scripts/migrate/photos_migrate.py && test $(ls content/photos/*.jpg | wc -l) -eq 21 && pnpm astro build", "exit": 0, "duration_s": 4.935, "stdout_tail_sha256": "ca7b421d1bb9417478eb42fb7657315f65e6bbb3b775b58b0485628b7dadf49b", "tree_fp": "90b9364bd579f0c17cc49becf9a9e63a2dca2a403456e4444cde7366d3a7e30a", "at": "2026-10-24T16:32:29+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "python3 scripts/migrate/moments_migrate.py && test $(ls content/moments/*.md | wc -l) -ge 1 && pnpm astro build", "exit": 0, "duration_s": 4.474, "stdout_tail_sha256": "6ddfdc0bddd6bc5ec3eea52b73adfdf867e5e04741fb34af81e243373a5ff3fc", "tree_fp": "fa616c9476889793315d463cfc6e5fcc7fb10a8482756cbf8a3c55dc98310714", "at": "2026-10-24T16:32:33+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "python3 scripts/migrate/check_urls.py", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "cf0053bb7b862b9081d57788becd7e9ae7cbaa8b672765c3f0e39843648d1176", "tree_fp": "fa616c9476889793315d463cfc6e5fcc7fb10a8482756cbf8a3c55dc98310714", "at": "2026-10-24T16:32:33+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "bash infra/deploy/build.sh && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy", "exit": 0, "duration_s": 7.919, "stdout_tail_sha256": "5cea5e72f4aeb6e1ee4583fadbf521d69e077a8f3ea60664786c496bbbf7e65d", "tree_fp": "fa616c9476889793315d463cfc6e5fcc7fb10a8482756cbf8a3c55dc98310714", "at": "2026-10-24T16:32:41+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "python3 scripts/migrate/halo_export.py && test -s .migration/posts.json -a -s .migration/photos.json -a -s .migration/moments.json", "exit": 0, "duration_s": 0.127, "stdout_tail_sha256": "699736f8797aff67c64059e37cba3aa37c1475775c72d5ba1c1dbf83eba3abd5", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:33:48+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "pnpm astro build && test $(ls content/posts/*.md | wc -l) -eq 6", "exit": 0, "duration_s": 6.308, "stdout_tail_sha256": "41f34d67a7ad55370fffe892eaac9b0e6b4b5de283469eb4d491fda0c8b77823", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:33:54+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "test $(ls content/photos/*.jpg | wc -l) -eq 21 && pnpm astro build", "exit": 0, "duration_s": 4.889, "stdout_tail_sha256": "7f54d189b311fb1e1648bb98c718b5899cddee3834f74cf9431d02ea0ad4a04b", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:33:59+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "test $(ls content/moments/*.md | wc -l) -ge 1 && pnpm astro build", "exit": 0, "duration_s": 4.676, "stdout_tail_sha256": "b867975420680f99cab4ef724663adc53b578c99d0264a6bf7d85befff6bfcdb", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:34:04+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "python3 scripts/migrate/check_urls.py", "exit": 0, "duration_s": 0.028, "stdout_tail_sha256": "cf0053bb7b862b9081d57788becd7e9ae7cbaa8b672765c3f0e39843648d1176", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:34:04+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "bash infra/deploy/build.sh && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy", "exit": 0, "duration_s": 8.207, "stdout_tail_sha256": "d7f276381ce632fc8fdeee35c60902ecc95258fd649aaf33f29ad68090be2561", "tree_fp": "f80c74c41196ec1ddc16ec0e72f4302e0cce0e5cefecdcf2733aa15a171e9af9", "at": "2026-10-24T16:34:12+00:00"}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Phase 2:Halo 内容迁移(6文/21图/1瞬间,URL 1:1)- 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:把 Halo 生产内容(6 篇文章 + 21 张图库照片 + 1 条瞬间)迁入 Astro 集合,URL 与旧站 1:1,生产 Halo 全程只读不动
|
||||||
|
- **目标用户**:站长(迁移完成后新站具备真实全量内容,具备上线条件)
|
||||||
|
- **核心价值**:内容资产零丢失转移;SEO 无损(同 slug)
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- Halo 公共 API 可匿名读:posts 列表含 status.content(HTML);photos 插件 /apis/api.photo.halo.run;moments /apis/api.moment.halo.run
|
||||||
|
- 现有 slugs:deng-dai-ge-duo/hai-feng-zhong-shi-luo-de-xie-se-kui-zeng/mian-sha/shu-zui/ni-tan/cs-ge-lei-pei-zhi(首查实测,其中技术文旧 slug 为 csge-lei-pei-zhi 需在迁移时核对)
|
||||||
|
- pandoc 可用(~/.local/bin)
|
||||||
|
- Phase 1 集合 schema:posts(category/tags/mood)/photos(json sidecar+jpg)/moments
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- Halo 正文内嵌图片走 /upload/ 相对路径,可经 127.0.0.1:18090 直下
|
||||||
|
- 引文(blockquote)经 pandoc 转 md 后结构保留,无需人工重排
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 文章内嵌图与图库照片是否同一批(书评封面 vs 独立照片)→ 结论:正文零内嵌图,封面图独立未迁(原站亦独立),已记 spot-check 人工项
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- pandoc + python 后处理 > 纯手写解析器:覆盖面与可维护性 -> 选 pandoc
|
||||||
|
- 迁移中间产物(导出 JSON/原始 HTML)不入库(.migration/ gitignore),脚本与最终内容入库
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 Halo 全量导出脚本(posts/photos/moments/附件,中间产物 .migration/)
|
||||||
|
- [x] T2 文章 HTML→MD 转换 + frontmatter 组装 + 图片本地化
|
||||||
|
- [x] T3 图库 21 张迁移(下载+sidecar+EXIF+分组)
|
||||||
|
- [x] T4 瞬间迁移
|
||||||
|
- [x] T5 URL 1:1 断言脚本(旧 slug 集 == 新 slug 集)
|
||||||
|
- [x] T6 全站构建 + staging 更新 + 抽检清单
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 双语翻译(Phase 3)、上线切换(Phase 4)、Halo 停用(Phase 5)、旧站评论(数为 0)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:导出/转换/断言脚本入 scripts/migrate/,一次性中间产物 .migration/(gitignore);真实内容直接覆盖同 slug 样例文件
|
||||||
|
- 暂不引入:评论迁移(存量为 0)、双语分流(Phase 3)、封面图 frontmatter 扩展
|
||||||
|
- 不做的抽象/配置化:不写通用 CMS 迁移框架,只服务本次 Halo 一次性迁移
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:python3 + pandoc + curl
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:scripts/migrate/、content/、.gitignore、.spec/、src/(仅消费侧微调如分组枚举)
|
||||||
|
- 本轮不应触碰:~/docker_apps/halo 运行实例与 postgres、nginx、其它 infra
|
||||||
|
- Git integration branch:`spec/2026-10-24_migrate-blog-content`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:content/ 三集合、scripts/migrate/ 脚本、.gitignore
|
||||||
|
- 快速检查:`pnpm astro build`
|
||||||
|
- 集成检查:URL 1:1 断言 + 全量路由产物存在(6 文章页)
|
||||||
|
- 全项目检查:staging 更新 + rollback-dryrun 仍绿;仅在内容 schema 或构建链变更时升级范围重跑
|
||||||
|
- 升级触发:只有改动集合 schema、构建链或部署脚本时,才升级验证范围并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 6 篇文章新站路由全存在且 slug 与旧站一致 -> verify: task_0005
|
||||||
|
- [x] 图库 21 张全量呈现(EXIF 提取成功数记录) -> verify: task_0003
|
||||||
|
- [x] 瞬间 1 条迁移 -> verify: task_0004
|
||||||
|
- [x] 生产 Halo 状态未变(healthy + 200) -> verify: task_0006
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- Halo HTML 含编辑器私有结构(嵌入块)→ pandoc 后人工抽检点写入 T6 清单 -> 抽检覆盖
|
||||||
|
- 附件 URL 形态不一(相对/绝对)→ 下载器双形态兼容
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Phase 2:Halo 内容迁移(6文/21图/1瞬间,URL 1:1)- 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- 每个任务 boundary/verify 齐备;verify 首个反引号 span 为执行命令
|
||||||
|
- 迁移中间产物只落 .migration/(gitignore)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 Halo 全量导出脚本
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 只写 scripts/migrate/halo_export.py 与 .migration/ 产物;对 Halo 仅 GET
|
||||||
|
- verify: `python3 scripts/migrate/halo_export.py && test -s .migration/posts.json -a -s .migration/photos.json -a -s .migration/moments.json`
|
||||||
|
|
||||||
|
- [x] T2 文章转换入库
|
||||||
|
- depends-on: task_0001
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 只写 scripts/migrate/posts_to_md.py 与 content/posts/*.md(图片落 content/posts/ 同名资产目录或引用 photos 集合);书评 mood 按篇分配五档色
|
||||||
|
- verify: `pnpm astro build && test $(ls content/posts/*.md | wc -l) -eq 6`
|
||||||
|
|
||||||
|
- [x] T3 图库迁移
|
||||||
|
- depends-on: task_0001
|
||||||
|
- boundary: 只写 scripts/migrate/photos_migrate.py 与 content/photos/(jpg+json sidecar;分组沿用 Halo 数据)
|
||||||
|
- verify: `test $(ls content/photos/*.jpg | wc -l) -eq 21 && pnpm astro build`
|
||||||
|
|
||||||
|
- [x] T4 瞬间迁移
|
||||||
|
- depends-on: task_0001
|
||||||
|
- boundary: 只写 scripts/migrate/moments_migrate.py 与 content/moments/
|
||||||
|
- verify: `test $(ls content/moments/*.md | wc -l) -ge 1 && pnpm astro build`
|
||||||
|
|
||||||
|
- [x] T5 URL 1:1 断言
|
||||||
|
- depends-on: task_0002
|
||||||
|
- boundary: 只写 scripts/migrate/check_urls.py(读 .migration/posts.json 与 content/posts/ 比对 slug 集)
|
||||||
|
- verify: `python3 scripts/migrate/check_urls.py`
|
||||||
|
|
||||||
|
- [x] T6 构建验收 + staging + 抽检清单 + 生产复查
|
||||||
|
- depends-on: task_0002, task_0003, task_0004, task_0005
|
||||||
|
- boundary: 跑 build.sh 更新 staging;抽检清单写 .spec/specs/2026-10-24_migrate-blog-content/evidence/spot-check.md;复查 halo 容器与本机 200
|
||||||
|
- verify: `bash infra/deploy/build.sh && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前进度**:由脚本计算,无需手动维护
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
pubDate: 2026-04-29
|
||||||
|
tags: []
|
||||||
|
text: "@wait4x"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
pubDate: 2026-04-29
|
||||||
|
tags: []
|
||||||
|
text: "@wait4x"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
pubDate: 2026-04-29
|
||||||
|
tags: []
|
||||||
|
text: "@wait4x"
|
||||||
|
---
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
pubDate: 2026-10-02
|
|
||||||
tags: [开源]
|
|
||||||
text: 给上游提的 doctor 误报 issue 当天被采纳,第三天随新版本发布——开源最好的正反馈。
|
|
||||||
---
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
pubDate: 2026-10-12
|
|
||||||
tags: [阅读, 一句话书评]
|
|
||||||
text: 《人鼠之间》——梦越简单,碎的时候越响。
|
|
||||||
---
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
pubDate: 2026-10-19
|
|
||||||
tags: [随手拍, 手机]
|
|
||||||
text: 傍晚的云压得很低,路灯提前亮了。
|
|
||||||
image: ./bang-wan-de-yun.jpg
|
|
||||||
---
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 277 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156983093",
|
||||||
|
"image": "./1766156983093.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 371 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156992123",
|
||||||
|
"image": "./1766156992123.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 248 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156992265",
|
||||||
|
"image": "./1766156992265.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 262 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156992559",
|
||||||
|
"image": "./1766156992559.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156992840",
|
||||||
|
"image": "./1766156992840.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 622 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156997939",
|
||||||
|
"image": "./1766156997939.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 381 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156998072",
|
||||||
|
"image": "./1766156998072.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 498 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156998205",
|
||||||
|
"image": "./1766156998205.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 396 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156998344",
|
||||||
|
"image": "./1766156998344.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 235 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766156998590",
|
||||||
|
"image": "./1766156998590.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 823 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157003595",
|
||||||
|
"image": "./1766157003595.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 719 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157003732",
|
||||||
|
"image": "./1766157003732.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 530 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157003865",
|
||||||
|
"image": "./1766157003865.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 846 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157003992",
|
||||||
|
"image": "./1766157003992.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 436 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157004116",
|
||||||
|
"image": "./1766157004116.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "1766157012003",
|
||||||
|
"image": "./1766157012003.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "晨光街口",
|
|
||||||
"image": "./chen-guang-jie-kou.jpg",
|
|
||||||
"group": "street",
|
|
||||||
"alt": "晨光斜切进街口,行人拖着长影子过马路",
|
|
||||||
"exif": {}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 470 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "mmexport1771816358254",
|
||||||
|
"image": "./mmexport1771816358254.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 654 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "mmexport1771816377628",
|
||||||
|
"image": "./mmexport1771816377628.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 619 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "mmexport1771816382115",
|
||||||
|
"image": "./mmexport1771816382115.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 468 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "mmexport1776564084994",
|
||||||
|
"image": "./mmexport1776564084994.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 456 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "mmexport1776564092433",
|
||||||
|
"image": "./mmexport1776564092433.jpg",
|
||||||
|
"group": "street",
|
||||||
|
"alt": "",
|
||||||
|
"exif": {}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "雪线",
|
|
||||||
"image": "./xue-xian.jpg",
|
|
||||||
"group": "landscape",
|
|
||||||
"alt": "山脊上的积雪线在蓝天下格外清晰"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "雨后屋檐",
|
|
||||||
"image": "./yu-hou-wu-yan.jpg",
|
|
||||||
"group": "street",
|
|
||||||
"alt": "雨刚停,屋檐还在滴水",
|
|
||||||
"exif": {
|
|
||||||
"make": "FUJIFILM",
|
|
||||||
"model": "X-T5",
|
|
||||||
"lens": "XF23mmF2 R WR",
|
|
||||||
"fNumber": 2.8,
|
|
||||||
"exposureTime": 0.004,
|
|
||||||
"iso": 1250,
|
|
||||||
"focalLength": 23,
|
|
||||||
"dateTimeOriginal": "2026-04-14T08:47:21.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
title: CS各类配置
|
|
||||||
pubDate: 2026-04-28
|
|
||||||
category: tech
|
|
||||||
tags: [linux, docker]
|
|
||||||
---
|
|
||||||
|
|
||||||
上周把一台 2 核 2G 的入门云服务器重装成 Debian 12,顺手把从裸系统到能跑服务的最小流程整理成清单。目标很简单:换一块干净的地基——初始化用户、换源、装 Docker,全程不超过二十分钟,之后所有服务一律进容器。
|
|
||||||
|
|
||||||
## 系统初始化
|
|
||||||
|
|
||||||
第一件事是建普通用户、配好密钥,再把密码登录关掉。用 `adduser deploy` 创建用户并加入 `sudo` 组,随后在 `/etc/ssh/sshd_config` 里设置 `PasswordAuthentication no` 与 `PermitRootLogin no`,重载 sshd 即可。UFW 只放行 22 与 80/443,云控制台的安全组记得同步。
|
|
||||||
|
|
||||||
## 更换软件源
|
|
||||||
|
|
||||||
官方源对国内机器不算友好,换到清华 TUNA 镜像几乎是必选项。改动集中在 `/etc/apt/sources.list`,把 `deb.debian.org` 整体替换即可,bookworm 与 bookworm/updates 两段都要覆盖。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 换源:备份后替换为清华 TUNA 镜像(Debian 12 / bookworm)
|
|
||||||
sudo cp /etc/apt/sources.list{,.bak}
|
|
||||||
sudo sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list
|
|
||||||
sudo apt update && sudo apt upgrade -y
|
|
||||||
|
|
||||||
# 安装 Docker Engine(官方 APT 仓库)
|
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
|
|
||||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list
|
|
||||||
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
||||||
sudo systemctl enable --now docker && sudo docker run --rm hello-world
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装 Docker
|
|
||||||
|
|
||||||
官方的 `get.docker.com` 脚本虽然省事,但会替你决定源的写入方式;按上面的官方 APT 仓库手动配置更可控,以后升级也只走同一条路。装完用 `docker version` 确认 Engine 与 compose 插件都在。
|
|
||||||
|
|
||||||
### 国内镜像加速
|
|
||||||
|
|
||||||
拉取镜像前,在 `/etc/docker/daemon.json` 里注册镜像加速地址,`systemctl restart docker` 生效;`docker info` 的末尾能看到 Registry Mirrors 是否挂载成功。
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
title: "CS各类配置"
|
||||||
|
pubDate: 2026-04-28
|
||||||
|
category: tech
|
||||||
|
tags: [Linux]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. 云服务器
|
||||||
|
|
||||||
|
## 1.1 Docker 类
|
||||||
|
|
||||||
|
- **Docker 安装**
|
||||||
|
|
||||||
|
<https://www.cnblogs.com/Lenbrother/p/18673403>
|
||||||
|
|
||||||
|
<https://mirrors.tuna.tsinghua.edu.cn/help/debian/>
|
||||||
|
|
||||||
|
<https://mirrors.tuna.tsinghua.edu.cn/help/docker-ce/>
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
apt install docker-ce \
|
||||||
|
docker-ce-cli \
|
||||||
|
containerd.io \
|
||||||
|
docker-buildx-plugin \
|
||||||
|
docker-compose-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Docker 修改默认存储位置**
|
||||||
|
|
||||||
|
### **1、查看docker现有的存储目录**
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
docker info
|
||||||
|
```
|
||||||
|
|
||||||
|
在打印的信息中查看Docker Root Dir,即为当前的根目录,默认是/var/lib/docker,如下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### **2、查看docker的service位置**
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
systemctl status docker.service
|
||||||
|
```
|
||||||
|
|
||||||
|
docker.service位置如下图所示,后面的步骤需要修改这个文件中的内容。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### **3、停止docker.service**
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
systemctl stop docker.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4、复制现有存储目录到新的存放位置**
|
||||||
|
|
||||||
|
选择一个磁盘空间足够的目录,用来作为docker新的根目录,将原有根目录的文件全部复制过来,假设新的存放目录是:/data/docker
|
||||||
|
|
||||||
|
现有存储目录由第一步可以知道是:/var/lib/docker,将里面的所有文件复制到新目录下,如下:
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
cp -r /var/lib/docker/* /data/docker
|
||||||
|
```
|
||||||
|
|
||||||
|
*注:新的存放文件目录,root账号需要有读写权限,可以直接放开所有授权 `chmod -R 777 /data/docker`*
|
||||||
|
|
||||||
|
### **5、修改根目录的路径**
|
||||||
|
|
||||||
|
如下有两种方式,使用任意一种即可。
|
||||||
|
|
||||||
|
- **修改docker.service**
|
||||||
|
|
||||||
|
在ExecStart=/usr/bin/dockerd后面增加`--data-root=/data/docker`,中间空格隔开,/data/docker即为第4步中复制的新的存放目录,如果后面存在其他配置,不要修改,如下所示:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **修改daemon.json**
|
||||||
|
|
||||||
|
也可以修改/etc/docker/daemon.json来实现(如果damon.json文件不存在,可以新增一个文件),示例如下:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{ "data-root": "/data/docker"}
|
||||||
|
```
|
||||||
|
|
||||||
|
注:上面两种方式最好不要同时使用,如果配置的根目录发生冲突,会导致docker服务启动失败。启动失败可以通过命令`journalctl -u docker.service`查看详细日志。
|
||||||
|
|
||||||
|
### **6、重启docker服务**
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
systemctl daemon-reloadsystemctl restart docker.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### **7、验证查看原有的镜像和容器**
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
docker imagesdocker ps -a
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Docker 换源**
|
||||||
|
|
||||||
|
由于国内无法使用dockerhub,需要更换镜像源
|
||||||
|
|
||||||
|
- 换源方法:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo mkdir -p /etc/docker
|
||||||
|
sudo vim /etc/docker/daemon.json
|
||||||
|
```
|
||||||
|
|
||||||
|
写入如下内容:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"registry-mirrors":[
|
||||||
|
"https://docker.1ms.run",
|
||||||
|
"https://docker.xuanyuan.me",
|
||||||
|
"https://hub.docker.hk.sqyyl.cn",
|
||||||
|
"https://docker.rainbond.cc",
|
||||||
|
"https://mirror.iscas.ac.cn",
|
||||||
|
"https://docker.mirrors.sjtug.sjtu.edu.cn",
|
||||||
|
"https://docker.m.daocloud.io"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
保存并退出后,重启docker服务
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.1.1 **Microsoft 365 E5 Renew X**
|
||||||
|
|
||||||
|
[Docker版 E5续订的E5调用API续订服务:Microsoft 365 E5 Renew X_e5 docker 定制-CSDN博客](https://blog.csdn.net/qq_40605167/article/details/122888580)
|
||||||
|
|
||||||
|
开启邮箱的**IMAP/STMP**功能或者**POP/STMP**功能
|
||||||
|
|
||||||
|
- docker部署命令如下:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker run -d -p 1066:1066 -e TZ=Asia/Shanghai -e [sender="XXX@xxx.com](mailto:sender=%22XXX@xxx.com)" -e pwd="LFQSdUsM4rJfUKQB" -e [receiver="XXX@xxx.com](mailto:receiver=%2XXX@xxx.com)" -e adminpwd="****" hanhongyong/ms365-e5-renew-x:pubemail
|
||||||
|
```
|
||||||
|
|
||||||
|
将其中的<XXX@xxx.com>替换为你的邮箱,\*\*\*\*\*替换为你的密码
|
||||||
|
|
||||||
|
### 1.1.2 DPanel
|
||||||
|
|
||||||
|
**DPanel** 是一个**轻量化容器管理面板**,可以用来可视化管理和查看Docker
|
||||||
|
|
||||||
|
- docker部署命令如下:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker run -d --name dpanel --restart=always \
|
||||||
|
-p 8807:8080 -e APP_NAME=dpanel \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v /home/dpanel:/dpanel dpanel/dpanel:lite
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.1.3 ZeroTier 客户端
|
||||||
|
|
||||||
|
- **步骤一:Docker拉取镜像**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo docker pull zerotier/zerotier
|
||||||
|
```
|
||||||
|
|
||||||
|
- **步骤二:创建并运行ZeroTier容器**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo docker run -d \
|
||||||
|
--name zerotier-one \
|
||||||
|
--restart=unless-stopped \
|
||||||
|
--device=/dev/net/tun \
|
||||||
|
--net=host \
|
||||||
|
--cap-add=NET_ADMIN \
|
||||||
|
--cap-add=SYS_ADMIN \
|
||||||
|
-v /home/wait4x/zerotier-one:/var/lib/zerotier-one \
|
||||||
|
zerotier/zerotier:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- **步骤三:用自建的Planet文件替换官方文件**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo cp … …
|
||||||
|
```
|
||||||
|
|
||||||
|
重启docker容器
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo docker restart zerotier-one
|
||||||
|
```
|
||||||
|
|
||||||
|
- **步骤四:加入私有的ZeroTier网络**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo docker exec zerotier-one zerotier-cli join XXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -1,29 +1,58 @@
|
|||||||
---
|
---
|
||||||
title: 等待戈多
|
title: "等待戈多"
|
||||||
pubDate: 2026-03-31
|
pubDate: 2026-03-31
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags: [read]
|
||||||
mood: '#b45309'
|
mood: '#b45309'
|
||||||
---
|
---
|
||||||
|
|
||||||
戈多是全书贯穿始终的人物,可并没有任何一个人见过戈多。爱斯特拉冈和弗拉季米尔在乡间小路上等他,等到靴子脱了又穿,等到胡萝卜啃完了又换饼干,等到波卓和幸运儿来了又走,戈多始终没有来。第二天他们接着等,仿佛头一天的一切都可以原封不动地再来一遍。
|
### 谁是戈多?为何等待?
|
||||||
|
|
||||||
「谁是戈多?为何等待?」贝克特对这两个问题一向敬谢不敏,据说他只回过一句:我要是知道,早在戏里说清楚了。于是戈多成了一个空位——有人读出上帝,有人读出希望,有人读出永远缺席的意义本身。空位越空,人们越急着往里填东西;这大概正是这出戏首演七十年后仍然刺人的原因。
|
**无望之待:自我救赎与囚笼困境**
|
||||||
|
|
||||||
> 希望迟迟不来,苦死了等的人。
|
戈多是本书贯穿始终的人物,可并没有任何一个人见过戈多。戈戈和迪迪只是一味的在等待戈多。戈多是一种希望吗?或者说是一种信念吗?等待着等待着,甚至可以让戈戈和迪迪不在意生命,甚至以自杀的方式来消磨等待的时光。那么等待戈多的意义是什么?不是生的希望,是一种精神上的追求和救赎吗?还是说,等待戈多已经成为了他们生的唯一追求,他们也不知道等待的意义是什么,但等待戈多是他们唯一可以去做的事情,也是唯一能够逃离现实的救命稻草。戈多是否存在已不重要,重要的是”等“这个动作本身,成了他们确认自身尚未彻底瓦解的生理节律。
|
||||||
|
|
||||||
——萨缪尔·贝克特《等待戈多》
|
也许戈多并不存在,戈多只是戈戈和迪迪逃离现实世界的一种方式,是能够让他们感觉到自己还有事情可干的唯一方式。像是一种荒诞的等待,一种毫无意义的等待。同时等待的过程本身也很荒诞,戈戈和迪迪像是为了等待而等待,等待的过程也是一段极其荒芜的时光,他们不知道做什么,他们不知道该如何消磨等待的时光,他们只是等,等待戈多。
|
||||||
|
|
||||||
就戏剧动作而言,《等待戈多》几乎什么都没有发生,而这正是它最残忍的地方:它不安排绝望的事件,只安排绝望的节奏。对话膨胀又塌缩,约定立起又被推翻,戏在「咱们走吧」与「他们不动」之间原地打转。等待本身成了全部的情节,也成了全部的刑罚。
|
人类在无意义中自我续命的方式,往往不是轰轰烈烈的反抗,而是琐碎、重复、甚至可笑的日常坚持。这既是妥协,也是一种悲壮的韧性。然而始终坚持这种无意义的等待,为什么不勇敢赴死,或是打破现状,去追随流逝的时间,而非困在此刻,困在过去。可能赴死需要决断,突围需要方向。而当两者都被抽空,”等“就成了唯一可执行的动词。
|
||||||
|
|
||||||
> 爱斯特拉冈:咱们走吧。
|
迪迪始终追问过去,寻求时间的一致性;而戈戈只想活在现在。我们永远在”试图理清意义“与”只能忍受此刻“之间撕扯。没有人活在未来,因为好似所有人都没有未来,未来只是等待,永远活在等待之中,活在日复一日重复的等待之中。每个人在日复一日的重复之中,以自己独有的方式反抗着现实世界和生活,与时间共生与时间对抗,像是在证明自己还存在,自己所做的事情还有意义。
|
||||||
>
|
|
||||||
> 弗拉季米尔:咱们不能。
|
|
||||||
>
|
|
||||||
> 爱斯特拉冈:为什么不能?
|
|
||||||
>
|
|
||||||
> 弗拉季米尔:咱们在等待戈多。
|
|
||||||
|
|
||||||
可我并不觉得这是全然的黑暗。两个流浪汉在无望之待里,仍然留住了几样东西:他们拌嘴,也互相系鞋带;他们想上吊,又舍不得用裤腰带;他们每天遇见,每天确认「你又来了」。无望之待没有被拔高成救赎,但陪伴让囚笼有了呼吸的缝隙——自我救赎未必是走出困境,有时只是在困境里替对方把靴子脱下来。
|
所以在荒诞的生活下?时间照常流逝,我们该做些什么?我们以什么去对抗虚无?当等待成为宿命,当未来被稀释为重复的"明天",我们究竟该以何种姿态面对这必然流逝的时间?
|
||||||
|
|
||||||
合上书那天傍晚,我在站台等一班迟迟不来的车,忽然想起幸运儿的思想独白:原来我们都站在各自的乡间小路上,书包里装着胡萝卜,等一个不会来的戈多。贝克特没有给我们答案,只留下一句温柔的嘲讽——他们许诺明天一切都会好起来。信不信,由等的人自己决定。
|
**枯荣之间:记忆之锚与时间之流**
|
||||||
|
|
||||||
|
剧中共有五个人物,但似乎只有迪迪一个人是清醒的,只有他记得过去,记得昨日曾经发生的事情,戈戈、波卓、幸运儿、少年都不记得昨日发生过什么,他们好似始终活在当下。而实际上时间一直在流逝着,那棵树生出绿叶就代表着一切始终在向前发展,他们活在当下,但见证了过往,也在向着未来迈步。只不过始终迈步走向的重复的今日,一如始终都在重复的上吊讨论,赴死只是消磨时间的一种方式,却没有真实赴死的勇气。
|
||||||
|
|
||||||
|
幸运儿在剧中无序的大段言论,像是理性奔溃的祭文,也像是荒诞的镜像。幸运儿被下达思考的命令时,他的独白开始是有逻辑框架的,引用了多种术语,但很快开始陷入混乱,语词难砌,毫无章法。就好似:人们想要以理性去对抗荒诞,想要以思考去解释失去逻辑的世界本身,但所有的努力都是徒劳的,一切终将归于虚无。
|
||||||
|
|
||||||
|
反过来看迪迪的清醒,也像是另一种形式的幸运儿,始终坚持用记忆的碎片去拼凑昨日,那个在其他人记忆中已经不存在的昨日。没有人记得昨日,只有迪迪记得。迪迪用记忆对抗的流逝,痛苦但清醒;戈戈用希望消解重量,麻木但轻松;其他人时间对他们而言本就不存在,是一种极致的荒诞。枯树生叶,见证着世界的变化,但人的处境却始终在原地循环。迪迪清醒的认识到时间的流逝,但却依旧困在时间的牢笼中,他能做的依旧只是静静地等待戈多。
|
||||||
|
|
||||||
|
***树可以是枯的,也可以生叶;***
|
||||||
|
***等待可以是徒劳的,也可以是存在的证明;***
|
||||||
|
***记忆可以是负担,也可以是锚点;***
|
||||||
|
***荒诞可以是绝望,也可以是自由的起点。***
|
||||||
|
|
||||||
|
也许在荒诞的世界中,在虚无的生活里,有时候不需要”战胜“虚无,有时候,”继续“本身,就是意义。亦如那棵树,枯荣之间,即是答案。
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- 爱斯特拉贡:(沉思了半晌)你说的事情正好相反,就是这个吗? 弗拉第米尔:气质的问题。 爱斯特拉贡:性格的问题。 弗拉第米尔:人们对此无能为力。 爱斯特拉贡:就是再奋斗也没有用。 弗拉第米尔:天生是什么样还是什么样。 爱斯特拉贡:就是再挣扎也没有用。 弗拉第米尔:江山易改,本性难移。
|
||||||
|
|
||||||
|
- 我见到的人越是多,我就越是高兴。即便是跟最卑贱的人打交道,你也能感到自己在长见识,在丰富阅历,在更好地品尝幸福。
|
||||||
|
|
||||||
|
- 世界上的眼泪自有其固定的量。某个地方有人哭起来,另一个地方就必然有人停住了哭。笑也一样。(他笑)如此,我们就不要去说我们时代的坏话了,它并不比以往的时代更糟糕。(沉默)我们也不要去说我们时代的好话了。
|
||||||
|
|
||||||
|
- 要是没有他,我的思维,我的感觉,恐怕永远也就局限在平庸的细碎小事中了,局限在养家糊口的俗事中——根本就没什么要紧的。至于什么才是至高无上的美,至高无上的善,至高无上的真,我真是无法企及,我也知道自己根本不配。
|
||||||
|
|
||||||
|
- 它到底有什么东西是如此的异乎寻常?它作为老天?它苍白而又明亮,像是白天这一时刻在任何地方的天空。(略顿)在相同方位上的天空。
|
||||||
|
|
||||||
|
- 灯光突然转暗。一时间里,天色变黑。月亮升起在舞台尽头,爬上了高天,一动不动,在舞台上撒下了银色的光芒。
|
||||||
|
|
||||||
|
- 人们不是每一天都需要我们的,说实话,人们不是恰恰需要我们的。别的人也能做这些事,即便说不上做得更好,至少也能做得同样好。我们刚才听到求援声,不如说就是他向整个人类发出来的。但是,在这个地点,在眼下这个时间,人类就是咱们俩,不管我们乐意还是不乐意。赶紧利用它,趁着现在时间还不晚。既然不幸的命运把我们扫荡进了这无耻的败类当中,那就让我们好好地代表一次他们吧。 确实,当我们交叉着胳膊衡量利弊得失时,我们同样无愧于我们的生存条件。老虎会不假思索地飞奔去援助它的同类。要不然,它就会逃进丛林的最深处。但是,问题并不在这里。我们在这里做什么,这才是我们必须问我们自己的问题。我们很荣幸,我们知道它的答案,是的,在这一场巨大的混乱中,只有一件事是清楚的:我们等待着戈多的来临。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
---
|
||||||
|
title: "海风中失落的血色馈赠"
|
||||||
|
pubDate: 2026-03-25
|
||||||
|
category: reading
|
||||||
|
tags: [read]
|
||||||
|
mood: '#0f766e'
|
||||||
|
---
|
||||||
|
|
||||||
|
### 秋-斯科特之秋、父亲之秋、少年之秋(冬的预演)
|
||||||
|
|
||||||
|
秋是萧瑟之感,意味着迟暮。父亲老了,老马斯科特也老了。生活的重担之下,曾经年轻力壮的父亲再难游刃有余、从容不迫。面对曾经的老伙计,他别无选择。而父亲的两个孩子目睹了这一切,看着他们曾经的玩伴不得不离他们远去,他们第一次直面了现实的残酷。于父亲而言,他失去了最忠诚的伙伴,那个陪他历经了多年磨难的伙伴斯科特,父亲舍得卖掉斯科特吗?不舍得!但他又必须卖掉。嗷嗷待哺的孩子还环顾在他的四周,他已不是曾经的自己了,在生活的重担面前他别无选择。他爱斯科特,也爱自己孩子们,爱自己的妻子,为了生存爱也需要让步。
|
||||||
|
|
||||||
|
少年们亲眼目睹生活的残酷和成长的无奈(***世界不是围着自己转的***)。对父亲而言,是英雄迟暮的无奈;对斯科特而言,是忠诚被辜负的悲剧;对孩子而言,是童真终结的时刻。
|
||||||
|
|
||||||
|
### 黑暗茫茫-詹姆斯的“远行”
|
||||||
|
|
||||||
|
年满18岁的詹姆斯告别父母,告别奶奶,踏上了逃离布雷顿角的征程。想要远离煤矿,想要脱离命运,却发现始终与煤矿相伴,始终离不开煤矿的束缚。似乎自出生开始就意味着煤矿是天生的使命,不可逃脱。
|
||||||
|
|
||||||
|
这是一个追寻未来的故事,也是一个渐渐迈进黑暗的故事,黑暗是源自地下不见光明的黑暗,黑暗是源自未来看清命运的黑暗,黑暗是源自己身无法逃离的黑暗。黑暗茫茫,想要离开,想要脱离宿命,却始终被茫茫黑暗包裹,被命运裹挟。
|
||||||
|
|
||||||
|
这是世代传承的枷锁,是源自这片土地-布雷顿角,难以被改变的命运,想要挣脱,却又好似被命运推着向前,向着茫茫黑暗。这是青年的向往,对命运的抵抗和无奈,和看清命运后的坦然。
|
||||||
|
|
||||||
|
### 海风中失落的血色馈赠-远去的家
|
||||||
|
|
||||||
|
当“我”以一个看似成功的身份,再次回到“我”出生的地方,那个承载着“我”的过去的海边。“我”见到“我”的父亲、母亲、曾经的“妻子”和“我”的儿子。过去“我”选择逃离这里,去到蒙特利尔。如今当我再次回到这里。海风凌厉,似乎在驱赶着“我”这个外来者,“我”无法与曾经的亲人相拥,也无法与他们拥有美好的生活。
|
||||||
|
|
||||||
|
于“我”而言,他们是“我”失落在海风中的血色馈赠,他们承载着我的过去,承载着青年时“我”的亲情、爱情。但现在,这些都已经消逝在了海风中。于他们而言,“我”是外来的陌生人,父亲母亲平静地招待“我”,曾经地恋人痛恨“我”,“我”的孩子不认识“我”。“我”的到来亦或是“我”的插足反倒像是会打破这现有的平衡、温暖和幸福。
|
||||||
|
|
||||||
|
这是成年后再次回到过去的无奈。成功远离了那个海边,等到再回来时却发现再也回不到过去,自己再也不属于那个地方,海风吹走了一切,凛冽的海风刺痛着每一个人的心。
|
||||||
|
|
||||||
|
***故乡不是一个可以回去的地方,而是一段再也无法抵达的过去。*** 我们都成了自己生命里的异乡人,在海风中,与那份失落的血色馈赠,遥遥相望。
|
||||||
|
|
||||||
|
### 回乡-回乡之旅
|
||||||
|
|
||||||
|
从大城市回到故乡。回乡不同于海风中失落的血色馈赠中的回归,回乡表述的是一个更为尖锐的矛盾,回乡的故事在基于血缘亲情的基础上更多了几分决裂,不同于海风中失落的血色馈赠中的平静。回乡的主体,是大城市的一家,父亲来自这个被灰暗深深绑定的乡下,母亲是活在光鲜亮丽的城市下的丽人,而作为孩子的少年在回乡之旅上需要面对的是两种情感的强烈对冲,父亲想要回到曾经的故土去见家人,再次踏上那片坚实的土地,母亲则是从心底厌恶这座灰暗的城市,不愿意让自己的孩子再次沾染这样的宿命和不堪。
|
||||||
|
|
||||||
|
然后少年时懵懂的,他不知道父亲母亲对这座灰暗城市的情感时怎样的,他与兄弟姐妹玩耍,在他素未谋面的故土寻找快乐。但在少年的祖母看来,他们已经不属于这里,他们早已远离了这座城市,他们已经和这个家族的血脉决裂。父亲想要融入过去,母亲想要孩子远离这灰暗。
|
||||||
|
|
||||||
|
当一切结束,***每个人都在挥手,但火车只管往前,因为它别无选择,也因为它不喜欢看人挥手道别***。父亲回不去,他不再是纯粹的布雷顿角人;母亲不想回,她从心底鄙视这片土地;孩子回不去,他对故乡只有短暂的猎奇,没有深刻的羁绊。
|
||||||
|
|
||||||
|
### 灰白的金色馈赠-苦涩的成长
|
||||||
|
|
||||||
|
少年的勇气和成长,但被父母误会的苦楚。
|
||||||
|
|
||||||
|
少年渴望通过自己的努力获得父母的认同,减去父母的压力,但获取金钱的方式不是传统观念上被认同的方式。父母希望杰西诚实,拥有道德感,希望他不误入歧途。他们也爱杰西,但不认同杰西通过类似赌博的方式获得金钱,也不认同在酒吧夜不归宿、宿醉的行为。
|
||||||
|
|
||||||
|
一边是少年以为自己通过自己的努力获得了金钱,想要得到父母的认可;一边是父母基于他们道德感和想法出发,认为自己孩子走向了歧途,想要纠正他的“错误”行为。少年接受了父母的说教,承受了父母给予他的“灰白的馈赠”(打击和管教),失去了“金色的馈赠”(虚荣和成就感),但也换来了”成长的馈赠“(孩子与父母的想法总会有冲突,一个孩子的勇敢行为有时在父母眼里可能一文不值,这是代际隔阂,亦是角色隔阂),苦涩但真实。
|
||||||
|
|
||||||
|
### 船-父辈的坚守、后辈的远去
|
||||||
|
|
||||||
|
***花一辈子去做自己厌烦的事,比永远自私地追逐梦想、随心所欲,要勇敢得多。***
|
||||||
|
|
||||||
|
父亲是热爱文学,心中怀揣着自由的人,但是面对生活,他选择将自己与这艘船牢牢的绑在一起,他将自己的儿女送出去,让他们不再被这艘船束缚,自己甘愿永远在这艘船上,在那个椅子上平静地度过这一生。他把自己的一生献给了生活,献给了自己的后辈。
|
||||||
|
|
||||||
|
主人公的我目睹父亲的选择和最终死亡,心中是对父亲的敬佩和对自己的鄙夷。我没有父亲的勇气,但也在最后选择了和父亲一样的决定,留在这艘船上,去继承父亲的衣钵。然而这一切,在母亲看来都只是徒劳,父亲走了,这艘船已经名不副实了,后辈们已经叛离了过去,已经不属于这艘船。
|
||||||
|
|
||||||
|
这是两代人的对话,是新时代子女对新生活的向往,和老一辈对过往生活的坚持,但父亲却像是沟通旧时代和新时代的桥梁,他托举子女走向新时代,自己甘愿永堕旧时代的牢笼。最后父亲的死,是那么无力,在大海中,一切都破碎了,新时代和旧时代的桥梁也就此断裂了。我,我的兄弟们,我的姐妹们,在旧时代看来,在这片土地看来,都是背叛者。
|
||||||
|
|
||||||
|
### 去乱岑角的路-活着和死亡的缠斗
|
||||||
|
|
||||||
|
濒临死亡的祖孙两,在弥留之际的深刻对话。祖母不想孤独的活着,也不愿被束缚在养老院中,她想被自己的孙子救赎。而孙子得知自己的生命就要结束,他在最后时刻也想依偎在祖母身旁,得到祖母的救赎。但世事总是事与愿违,两人对坐,中间隔着的是漫长的岁月,谁都无法救赎谁。
|
||||||
|
|
||||||
|
去乱岑角的路,是孙子寻找过去记忆的路,是追寻亲情的路;是祖母悲惨的回忆之路,是最后走向死亡的路。
|
||||||
|
|
||||||
|
祖母在年轻时看着丈夫离她而去,在年老时看着膝下的孩子离她远去,她一个人活在乱岑角崖边的屋里,陪伴她的只有牲畜、海浪拍打悬崖的涛声和无尽的孤独。在生命的最后时刻,她想被自己的孩孙们救赎,但是她失败了,同样面临死亡的孙子也只能让她短暂的享有这救赎和爱。短暂过后,是真正的孤独。
|
||||||
|
|
||||||
|
孙子从这里走出,走向城市,最后在生命的最后时刻,又踏上去乱岑角的路,去追寻最后的一丝爱意。他回想起祖父的死亡,目睹父辈们对待祖母的始终重复的方式,而后在祖母的弥留时刻见证了祖母生命的最后时刻。祖母走了,眼前的世界黑暗了,最后幻想的一丝爱也消逝了,内心的世界也黑暗了,两种黑暗开始交融,一切都没入黑暗。
|
||||||
|
|
||||||
|
### 血与海编织的锁链,成长与宿命交接的浪潮
|
||||||
|
|
||||||
|
布雷顿角是贫瘠、继承和宿命的象征。它见证了一代又一代人的坚守和传承,矿工命运的延续、捕鱼生活的接力,也见证了新生一代对宿命的抗争,他们逃离布雷顿角、逃离灰暗的城市、逃离大海,也见证了一代人与一代人的隔阂,祖辈与父辈对故土的坚守,祖辈与父辈对自己所认定的事实和想法的坚持,子女们对新生活的向往和对外部世界的憧憬,它更见证了在血脉传承下人们在现实境地下的无奈、痛苦、希望、怀念。
|
||||||
|
|
||||||
|
布雷顿角是一切的开端,也是最终一切的结束。故事从布雷顿角的海边开始,也在布雷顿角海边的崖边结束。这个关于成长、关于宿命、关于回归的故事与布雷顿角紧紧捆绑在一起。布雷顿角是血与海编织的锁链,也像是一座别样的围城,城里的新生命想出来,旧生命想要坚守,已经出来的人想回去。这是一片被海风吹拂的、充满悲伤和尊严的土地,有人坚守、有人痛苦,但却是他们永远的根,亦是许多人回不去的根,即便他看似始终没有挣脱这血与海编织的苏联。
|
||||||
|
|
||||||
|
布雷顿角是故事中生命的开始,也是走向死亡的终章,人们在这里开始成长,逃离这里又回到这里,在这里坚守,也从这里走向新的彼岸。它承载着人们对于现实困境的无奈;承载着人们想要冲破宿命牢笼的勇气;承载着离乡者对于故土的不可磨灭的血脉传承和再也回不去的双重矛盾;承载着这座海岛这座灰暗的城市时时刻刻发生的每一件事,每一个关于成长的故事,每一件关乎理想和现实的故事;承载着生存的困境、活着的希望、以及死亡的平静。一切都伴随着这座海岛,伴随着海风,在凌厉的、充满咸腥味、平静的海风中出现、成长、坚守、消亡。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### 秋
|
||||||
|
|
||||||
|
- 每个清晨的到来,都显得更为晦暗,其脸色也越发阴沉。大西洋灰蒙蒙的潮水,潮峰几乎是黄色的,带着脾气,毫不留情地拍打着岸边光滑的圆石;永不知退却的峭壁下散落的这些石头,就像是某个巨人不经意间丢下的。
|
||||||
|
|
||||||
|
- 大雨如同无数被风吹斜的珠帘,整片整片地泼下。
|
||||||
|
|
||||||
|
### 黑暗茫茫
|
||||||
|
|
||||||
|
- 那时我们都祈祷,包括他自己,希望肌腱没有坏,也不要有感染,因为那是他唯一能用的手了,在凶险莫测的汪洋里,我们所有人都是那只手上岌岌可危的乘客。
|
||||||
|
|
||||||
|
- 可所有的风暴都会消减成几阵强风,又终归于平静。或许没有风暴和强风,我们便得不了任何平静,又或许平静一定要前者的铺垫,才显出它本来的面貌。
|
||||||
|
|
||||||
|
- 一旦你喝了地下的水,它就成了你身体的一部分,就像男人留在女人身体里的血,能改变女人一辈子,永远摆脱不掉。那是男人的一部分在女人身体的最深处流淌啊。这种东西,能让你夜不能寐,到死都纠缠着你。
|
||||||
|
|
||||||
|
- 我向爷爷伸出手,于是就觉得他有股扭曲残缺的力量传来,都快要把我的手掌给捏碎了。他手指都奇形怪状的,张开的拇指是扁的,显得过于宽阔,隆起的伤疤早被磨砺得又硬又黑,还有那些硕大异常的圆球,是他扭折错位的关节,但这又是一只力量骇人的手。有一瞬间我产生一个惊悚的念头:或许我是走不了的,我永远不会被释放了。但最后爷爷松开手,我觉得我自由了。
|
||||||
|
|
||||||
|
- 即使是坑坑洼洼的街道,当你意识到不知何日才能重踏,甚至今生不会再见时,也会显得落寞寂寥。
|
||||||
|
|
||||||
|
- 我在他们脸上见到了爷爷的表情,见到了成百上千在我过往人生中出现过的人们的表情,甚至我自己,也曾遇见过这样的车子,而从玻璃和镜子的反光中看到同样的神色。此刻的情形是,我根本不属于他们的生活,我只是被装在这个半红半透明的移动展示盒里面,在他们铺满悲怆的街道逗留片刻,然后就会消失,而他们的生活依然如故,不会有任何改变,我只是与他们无涉地穿过他们的生活。又一条无关紧要的河流携着漂浮的残骸匆匆而去,只有河岸是永恒的。水流会转向不知名的去处,那块残骸的终点他们从未涉足,也无法前往。在那一瞥之间,已足够让他们把我归纳,然后轻描淡写把我挡在他们的世界之外:“我们的九死一生,我们坟冢里的那些亡灵,他能懂得些什么?”
|
||||||
|
|
||||||
|
- 我终于明白,我过往人生中的那些长者,比我对他们的判断要复杂得多。爷爷感性、浪漫、热爱煤矿,奶奶严厉、实际、痛恨煤矿。不是没有区别的。母亲缄默坚强、淡然顺服,父亲急躁,常因此粗暴得不着边际,却又有他不着言辞的深情。他们是如此的不同,但都以某种方式承受着,并将过去这十八年赋在我的身上;而除此之外,我并不知其他的生活和世界。他们的生命淌进我的生命,而我的生命,亦是他们的支流。其中自然有不同,但在很多方面却比我原以为的,要相似得多。我现在觉得一个人或许可以活在两种生命里,但见到同样的真相。
|
||||||
|
|
||||||
|
- 我好像没有一件事是真正知道的,但我真的懂得了我的过错:面对他人和自己,我不够坦诚。
|
||||||
|
|
||||||
|
- 车子前行,朝着夜色。车头灯寻觅着那条诱人的白线,白线隐隐抬起,似乎在拖着我们向前、向上、向着某个深处,而我们只顾追随,追进无边的黑暗中。
|
||||||
|
|
||||||
|
### 海风中失落的血色馈赠家
|
||||||
|
|
||||||
|
- 我们在暗中是温暖的,在风中是平静的。
|
||||||
|
|
||||||
|
- 黑暗压着我,却还是无法让我入眠。我所听所见的话音与光影都是虚幻的。它们只是记忆的墙,想象的星火不停地扑灭在墙上。
|
||||||
|
|
||||||
|
### 回乡
|
||||||
|
|
||||||
|
- 每个人都在挥手,但火车只管往前,因为它别无选择,也因为它不喜欢看人挥手道别。
|
||||||
|
|
||||||
|
### 灰白的金色馈赠
|
||||||
|
|
||||||
|
- 他于是想到,蕴藏在他父亲身体里的暴力是何等骇人,那种暴力在他体内深处隆隆作响,就像高山底下喧嚣的激流,在某个目光难以触及的深穴中,拍打着暗石,水花飞溅。
|
||||||
|
|
||||||
|
### 船
|
||||||
|
|
||||||
|
- 对她来说,所谓远景,不过就是她无畏的黑眼睛望去,那海天一线的地方。
|
||||||
|
|
||||||
|
- 花一辈子去做自己厌烦的事,比永远自私地追逐梦想、随心所欲,要勇敢得多。
|
||||||
|
|
||||||
|
- 孤窗之外,不管是冬天的冰雪,平和压抑的暑热,或是到了秋天,风起而浪涌,母亲只是一个人望着。天还未亮,她就醒了,听屋外通向码头的砾石路上,胶鞋声经过。她知道脚步不会停的,因为她家没有人要加入队伍。所有的琳家人,只有她没有儿子或女婿会出门走向渔船,那条会带他们入海的船。无法面对的,是想到母亲望着大海时的深情,而记起你,却满心苦涩:因为对她来说,前者代表忠诚,而你,代表着背叛。
|
||||||
|
|
||||||
|
### 去乱岑角的路
|
||||||
|
|
||||||
|
- “我们是自己心中绝望的后裔,斯凯、朗姆、巴拉、迪里是我们的过去。”
|
||||||
|
|
||||||
|
- 它是安静而优雅的,它翅膀的美是隐忍而坚硬的,它非凡的生命全淋漓尽致地表述在它的身姿里。
|
||||||
|
|
||||||
|
- 给奶奶剪她发黄的脏指甲的时候,我明白我已经被准许参与一种无声的、秘密的交流,这种交流是强者在与弱者共处中常体会到的。这种能力和认知上的落差奶奶曾如此可畏地施加在她孩子的身上,很多时候甚至是她孩子的孩子。这种落差又和我们那些难以启齿、不可告人的缺憾互相助长,大人总会把这种不够格挥舞在下一辈的头顶。记得脏尿布、湿床单,第一次想说话或走路,记得圣诞老人出现,圣诞老人死去,记得千奇百怪的幼稚愿望,害怕时光的逝去,记得孩童时的恐惧让你在噩梦中醒来,以及独自醒来时的尖声喊叫,记得夜里的释放,真实和假想的秘密罪孽:不仅记得,还始终不忘。能力和认知的落差还包括凭身体给予的实在支持,保持健康以维系这种付出,或许,还包括爱。以前想到奶奶,想起的更多是能力,现在却想到了爱。或许,我现在想,是奶奶的强大总是过于显眼了。
|
||||||
|
|
||||||
|
- 没有人说过人活着是容易的事情。人不过就是得活着。
|
||||||
|
|
||||||
|
- 我们坐在厨房餐桌的两头,对视着,中间似乎就隔着我们相差的漫长的岁月。
|
||||||
|
|
||||||
|
- 她抓我的手是如此用劲,好像我能把它从梦里幽暗的水中拉出来。我试着也回应她的力道,因为我不知怎么也幻想着她能救我。突然我们两人都落泪了。我们为对方也为自己哭。我们本想在对方身上找到力量,却在这一场脆弱的泪光中相逢了。
|
||||||
|
|
||||||
|
- 有时我们于无光的恐惧中,很难分辨梦和真实。我们或于夜阑之时醒来,却因为方才梦里的世界要好上太多,便硬凭自己意念的力量要回到那种忘忧的快慰中。有时情况正好相反,我们又会掐自己,或用指节去磕铁的床沿。有时,噩梦是没有边界的。
|
||||||
|
|
||||||
|
- 所有的期望和恐惧,过去的,当下的,都互相碰撞和纠缠。有时我们见到“当下”的尽头,“过去”会隐约显得更为重大,因为我们除此再没有别的东西,除了“过去”,我们觉得再没有什么可以懂得。我感觉我正朝往昔坠落,希望能拥有更多的过去,因为我的未来正变得越来越少。二十六年太短了,是不够的,我要沿着一代一代的先人走得再远些,好从此刻那看来微不足道的留存中获得更多。那些迷信、草药、听得出宿命的喊杀、萦绕心神的小提琴声和如蛛网般的癌症疗法,我想一样样回去了解。所谓“天眼”、通鬼神的视线、狗的直觉、海鸥的呼啸,它们关于生存及其终结说了什么,每一个我也都要知道。我想回到双手能施展法术的祭司那里。我想回到信仰疗法的医师那里,只是我的信仰恐怕不够。回到怎样的过去都可以,只是科学太过冷漠,我不愿死在它那双不由分说的手里。
|
||||||
|
|
||||||
|
- 就在这时,和音乐一样,内外的黑暗向着合二为一蔓延,它们涌向对方,交融,不分彼此,差别消弭成一种纯粹。没有间隙,没有声响,这个相逢让万物归一。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
title: "面纱"
|
||||||
|
pubDate: 2026-03-09
|
||||||
|
category: reading
|
||||||
|
tags: [read]
|
||||||
|
mood: '#7c3aed'
|
||||||
|
---
|
||||||
|
|
||||||
|
****面纱之下:理想的殉道者和真实的幸存者****
|
||||||
|
|
||||||
|
### 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
||||||
|
|
||||||
|
这是译者在后记中写下的一句话。很好印证了《面纱》这本书或者说这个题目所蕴含的真相。人们活在面纱之下,以此遮蔽他人的窥探,却也因此困于自己的面纱之下。
|
||||||
|
|
||||||
|
见沃尔特像是见最温柔的爱和最悲痛的爱情,以最炽热的心和最真挚的感情去爱,爱上了一个不爱自己的人,最后在爱中将自己陷入漩涡🌀,不得解脱。他富有智慧,又富有对这个世界和社会以及人性最明锐的洞察力,他了解世界的苦难,了解社会的种种表象以及深层次的真相,了解人的“伪装”背后的真实人性。他像是个圣洁的存在,却同样又有着拙劣和“恶毒”的想法,以至于至死都未能从感情漩涡中游出。在我看来,沃尔特对于凯蒂的爱是真切的,对于背叛的恨也是真切的,但从始至终爱始终贯穿永远。在去湄潭府之前,他是冷静的旁观者,他是理性的化身,他有真对于真理的精准见解,有着自己衡量一切的道德标准和不可被践踏的信仰,这是源于理性的不可被磨灭的印记。在感知到背叛,并带着赴死的姿态奔赴湄潭府的过程中,他被感情所裹挟,他既爱又恨,恨凯蒂的背叛,也恨自己爱上了一个与自己原则背道而驰的爱人,他从一开始想让凯蒂也走向死亡,到最后让她离开,看似对于婚姻和这段感情有了一点点解脱,可实际上到死他都未能跨过自己给自己筑起的情感高墙。他是矛盾的,他至死未能宽恕自己,也正如他对世界最后的留言所述:“死的是一条狗”。这不仅是对凯蒂的隐喻,更是他对自己灵魂的终极审判。他终于承认,那个满怀怨恨、试图以爱之名行毁灭之实的自己,才是那条被仇恨吞噬的‘疯狗’。沃尔特的一生,是用最锋利的理性去剖析世界,却唯独不敢直视自己内心那片荒芜的废墟。他死于霍乱,实则死于那层由傲慢与偏见织就的、永远无法揭下的面纱。
|
||||||
|
|
||||||
|
相比之下,凯蒂是幸运的,也最终得到了自由。在虚荣的面纱下,遮住的是世界的残酷和社会的复杂以及人性的不可知。一开始的虚荣心作祟,让她步入了和沃尔特的婚姻,却发现婚后的生活并非是自己所期望的那样,甚至从一开始她就清楚的知道自己并不爱沃尔特。于是,当她到了香港,并遇上查理的时候,她沦陷了,她在查理身上看见了自己所期待的伴侣所拥有的那些特性,幽默风趣,性感迷人,但是却忽视了人性。在去往湄潭府之前,她虚荣,她害怕,她怕沃尔特发现一切,她软弱,她想逃离这样的婚姻和查理相拥,但是她不敢付诸行动。当沃尔特推着她去面对一切的时候,当一切都败露的时候,她终究是看见了人性的恶,她见到了查理的无情和所谓的爱只是言语上的“美好”,她也真正的感到了恐惧,面对死亡的恐惧和面对沃尔特不可捉摸的想法的恐惧。在湄潭府之行的初期,她依旧活在对于过去的回忆之中,她害怕恐惧,她看不见未来。但被圣光照耀的庙堂始终矗立在彼岸,她向往那迷人的色彩,她想要去追求新的生活。当她跨出第一步并开始在修道院工作的时候,她的觉醒之路从此慢慢开始,她慢慢和自己和解,慢慢看淡背叛,慢慢看清查理,慢慢找寻着自己在生活中该有的样子,她开始慢慢摘掉自己的面纱,看清自己的虚伪和软弱,开始追寻道的意义,她感悟周围的一切,甚至想着能够将沃尔特从他的痛苦中将他拉出。然而直到沃尔特死去,他们都没能和解,她愧对沃尔特,她看到了自己的轻贱,可是她自始至终都跨不过沃尔特竖起的高墙,她在墙外,沃尔特在墙内,各自在各自的面纱下痛苦的活着。沃尔特的死,是沃尔特的一种“解脱”,也是凯蒂的一种解脱。
|
||||||
|
|
||||||
|
> 沃尔特死于未能摘下‘理性与道德’的面纱,他在清醒中自我毁灭,留给世界一个悲壮的背影;而凯蒂幸免于难,她在破碎中撕下了‘虚荣’的面纱,虽然满身伤痕,却终于看见了真实的阳光。
|
||||||
|
>
|
||||||
|
> 所谓《面纱》,遮住的不仅是真相,更是我们面对真实自我的勇气。沃尔特缺乏这份勇气去接纳不完美的爱与人性,所以他困死其中;凯蒂在生死关头被迫拥有了这份勇气,所以她获得了自由。
|
||||||
|
>
|
||||||
|
> 故事的最后,死去的人得到了安息,活着的人带着记忆与教训,继续前行。这或许就是毛姆想告诉我们的:****人生没有完美的结局,只有在认清生活的残酷真相后,依然选择热爱并真实地活下去,才是唯一的救赎。****
|
||||||
|
|
||||||
|
从背叛到自我的救赎,凯蒂在远离家乡的异国,没能逃脱欲望作祟,再次回到香港之后又一次沦陷在了查理的怀抱中,但此刻的她早已不是去湄潭府的之前的自己,她的思想早已骤变。她厌恶自己肉体对自己灵魂的出卖,她选择远离这个是非之地,她选择去重新追求自由,亦如从湄潭府离开一样,她再一次走上了追寻自由的路上。她回到故土,见母亲离世,见妹妹怀有身孕,见父亲终于脱离过去的自己,她亦想脱离过去的自己,拥抱一个崭新的未来。活在面纱之下的她和她的父亲,在故事的结局,两人袒露心声,一同前往新的地方开始新的生活,去活出新的自己。
|
||||||
|
|
||||||
|
### 我有希望,也有勇气。
|
||||||
|
|
||||||
|
觉醒不是一瞬间的神迹,而是一场漫长的、甚至充满反复的拉锯战。她会跌倒,会重蹈覆辙,会在欲望的泥沼中挣扎。但真正的勇敢,不是在从未犯错中保持纯洁,而是在看清自己的丑陋后,依然有勇气撕下面纱,转身走向光明。
|
||||||
|
|
||||||
|
沃尔特死在了他的‘正确’里,而凯蒂活在了她的‘真实’中。沃尔特的故事是带有悲惨的个人主义的牺牲,凯蒂的故事是带有幸运的个人主义的新生。沃尔特自觉没有希望,在困顿的泥潭中深陷,他没有勇气去面对新的生活和新的世界,他将自己紧紧隐藏在自己的面纱之后,他活在自己为自己构建的信念世界之中,为自己的信念,"悲壮”赴死。凯蒂活在纷繁的社会中,从小我见大我,从假我见真我,从困局中得自由。她一步一步从虚伪的面纱下得见人性、社会冷暖以及新世界的希望,湄潭府之前的生活让她自省,湄潭府之行让她感知到一切,在死亡和黑暗中得到感悟和觉醒,得见光明,再次回到香港,欲望再次作祟,这一次她没有沉沦于虚妄的现实,她终于携手勇气做出了自己的决定,踏上了"离开”的归程。
|
||||||
|
|
||||||
|
故事的最后,面纱已落。凯蒂带着伤痕,牵着父亲的手,走向巴哈马的阳光。那里没有豪门的虚妄,没有激情的幻象,只有平凡而坚实的生活。这,或许就是毛姆留给所有在虚荣与迷茫中挣扎的人们,最温柔的答案:****唯有真实,可得自由。****
|
||||||
|
|
||||||
|
在《面纱》的故事中,沃尔特是极具“英雄主义”的个人,为自己固有的信仰慷然赴死;凯蒂是在纷杂的社会中逐步成长,在欲望和虚荣的交叉间度过早年,湄潭府一行,在黑暗中见光明,在死亡中见希望,在异国见真我,在“新世界”得自由。
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- 我不了解你,我一点儿都不了解你。
|
||||||
|
|
||||||
|
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
|
||||||
|
|
||||||
|
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
|
||||||
|
|
||||||
|
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
|
||||||
|
|
||||||
|
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
|
||||||
|
|
||||||
|
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
|
||||||
|
|
||||||
|
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
|
||||||
|
|
||||||
|
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
|
||||||
|
|
||||||
|
- 安宁只存在于人的灵魂中。
|
||||||
|
|
||||||
|
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
|
||||||
|
|
||||||
|
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
|
||||||
|
|
||||||
|
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
|
||||||
|
|
||||||
|
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
|
||||||
|
|
||||||
|
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
|
||||||
|
|
||||||
|
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
|
||||||
|
|
||||||
|
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
|
||||||
|
|
||||||
|
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
|
||||||
|
|
||||||
|
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
|
||||||
|
|
||||||
|
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
|
||||||
|
|
||||||
|
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
|
||||||
|
|
||||||
|
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
|
||||||
|
|
||||||
|
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
|
||||||
|
|
||||||
|
- 我有希望,也有勇气。
|
||||||
|
|
||||||
|
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: "泥潭"
|
||||||
|
pubDate: 2026-02-09
|
||||||
|
category: reading
|
||||||
|
tags: [read]
|
||||||
|
mood: '#166534'
|
||||||
|
---
|
||||||
|
|
||||||
|
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
||||||
|
|
||||||
|
黏稠如泥潭的历史,裹挟着蝼蚁众生。在时代发展的洪流下,在社会发展的泥潭之中,恒龄选择了自杀,恒丰在逃难中被乱枪打死,恒妤在被救赎之后安然离去,关仲卿在一场大火后选择了隐忍,马修德神父在时代洪流的泥潭中救赎与自我救赎,最后扎根在异国他乡。
|
||||||
|
|
||||||
|
故事的开始是自马修德神父从比利时漂洋至武汉想要救赎这动荡年代中食不果腹的贫苦大众,关仲卿赴日留学参加革命斗争以拯救这泥潭中的国家。伴随着关仲卿回国后开展革命斗争,恒龄在一声枪响中结束了自己的生命。从那一刻起,恒家人与关仲卿以及马神父的命运开始有了羁绊,命运的泥潭开始旋转,人们开始在泥潭中挣扎求生。
|
||||||
|
|
||||||
|
恒丰在父亲死后,深陷生活的泥潭,最后踏上了寻找妹妹恒妤的路,但一切就像是错位时空一般,看似那么近,实则那么远,恒丰终其一生没能在生命的最后一刻找到妹妹,却因自己旗人的身份被一群士兵扫射至死。他的死是必然的,是历史发展的必然结果,被历史的洪流淹没于黑暗之中。在生命的最后时刻,恒丰与马神父的交谈之中,他回忆了自己的一生和曾经那个让自己始终无法忘却的罪恶,他从软弱的自我之中解脱,在那一刻变得勇敢起来,在那一刻他脱离了泥潭,内心在良知的引领下走向了光明。他的命运是旧社会与新时代更迭时最鲜明的写照,在命运泥潭的裹挟下随波逐流,未知迷茫,做过恶事,也存有一丝善意,他害怕,他软弱,只有在生命的最后时刻变得勇敢,想要用最后的良知换得内心的安定和自我的救赎,他猜测到了自己的死亡,在生命的最后他走出了泥潭,他有愧,但勇敢迈向了无尽的黑暗之中。
|
||||||
|
|
||||||
|
关仲卿作为那个时代最有文化和知识的一批人,他所代表的是新时代下的革命党人,旧时代太过落后,斗争和革命是迎接曙光的唯一途径,他敢于为这个光明的未来献出自己的生命。可是诚然是他,作为新时代的革命党人,依旧是在时代的泥潭中,被时代洪流裹挟着前进。在多个革命党派的争论中,在多股势力的抢夺下,他身处于各怀心思、充满尔虞我诈的新势力圈中。在被洪流裹挟着前进的过程中,他随被迫,却也有着自己的信念和良知,他永远守护并坚持着斗争的信念,推动着革命的向前发展。他存有善意,在良知的驱使下救出了深陷生活泥潭和厄运泥潭的恒妤。关仲卿是复杂的,却又有着单纯的一面。
|
||||||
|
|
||||||
|
马修德神父作为一切事件的亲历者和旁观者。他洞悉着事情发展的全貌,可作为一个以救赎他人为目的的神父,在这历史泥潭中,在人性的泥潭中,他会迟疑,亦会憎恶。在一切都成为历史的时候,他也在救赎之中救赎了自己。
|
||||||
|
|
||||||
|
泥潭,是时代的泥潭,是生活的泥潭,是复杂人性的泥潭。泥潭之下是无尽黑夜,泥潭之上是漫漫星空。相信善意、相信良知、相信信念,自顾自地走出泥潭。
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
||||||
|
|
||||||
|
- 窗户一旦打开,外界的一切都涌了进来。这天之后,他在阅读《新民丛报》时,第一次对保皇的观点产生了质疑。在那之前,他与不少学生一样,认为中国应当走温和的立宪道路,而激烈的革命会导致社会的混乱——大革命后的法国就是前车之鉴。然而他第一次动摇了——“保皇”究竟保的是谁的皇帝?内阁究竟是谁的内阁?他想起了自己脑后的辫子。那天过后,每当他对镜照见自己的辫子,或者在学校看见其他学生的辫发,他的心里总有一种别扭的感觉。那天之前,他觉得中国人有辫子是自然而然的事情,如同人生下来有一个鼻子、两个眼睛,但现在他突然觉得这辫子极为刺眼丑陋,就像人身上的赘余的器官,而有辫子的人就是畸形的怪胎。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: "赎罪"
|
||||||
|
pubDate: 2026-02-26
|
||||||
|
category: reading
|
||||||
|
tags: [read]
|
||||||
|
mood: '#9f1239'
|
||||||
|
---
|
||||||
|
|
||||||
|
### 你要回来,我会等你。
|
||||||
|
|
||||||
|
因为一个人,因为一张图,因为一句台词,看了一本书。
|
||||||
|
|
||||||
|
“亲爱的塞西莉亚,我们缘分未尽“
|
||||||
|
|
||||||
|
阴差阳错之下,一段坚不可摧的爱情,在一个小孩的诬告下成了最为荒诞的悲剧。塞西莉亚与罗比的爱情缘起于自小而起的两小无猜与长久相伴,开始于水池边那破碎花瓶引起的一系列不可思议的事情,当罗比第一次勇敢向塞西莉亚表达出自己那狂热的爱时,那一刻注定了两人从此便永远相拥在了一起。
|
||||||
|
|
||||||
|
布里奥妮的诬告让罗比锒铛入狱,但三年的牢狱之灾没有熄灭他们爱情的火焰,走出囚房像是罗比在那三年中的彼岸灯塔,支撑着他在困顿与黑暗之中坚持下去。三年囚徒生涯结束,本以为命运会让他们再次相聚,但是迎接罗比和塞西莉亚的是二战的残忍。出狱的罗比还未能和塞西莉亚多见几面便匆匆越过英吉利海峡,奔赴了反抗德国的战争一线。在残酷的战争之下,又回到了如同在囚狱间所处的生活,而在战场之上,一切危机四伏,罗比时刻处于死亡的边缘,塞西莉亚与他的书信往来支撑着他在战场上努力的活下去,为了她,活下去。一句你要回来,我会等你,见证着他们伟大忠贞的爱情。
|
||||||
|
|
||||||
|
***诬告和死亡阻碍不了伟大的爱情。***
|
||||||
|
|
||||||
|
### 在战争面前没有自我
|
||||||
|
|
||||||
|
麦克尤恩巧妙的写作手法,让读者非常容易就感受到了战争的残忍和个人的渺小与绝望。第一部分罗比·特纳还是那个可以被称作罗比的风华正茂的青年,到第二部分战争开始的时候,罗比·特纳已经仅仅只是一个代名词,他被呼作特纳,而非那个拥有着自我的罗比。在战争之中他别无选择,随着一次又一次战败去走,去逃亡。
|
||||||
|
|
||||||
|
在逃亡途中,他目睹了一切事物在战争面前变得支离破碎,即便他在某个瞬间选择自我,拥抱良知,但也无法改变现实,依旧目睹了少年的死亡和一个又一个弹坑。这与刚毕业时的他在小镇上所看见的一切都形成了鲜明的对比,没有清澈透亮的湖水,没有绿意盎然的草坪,没有秩序井然的街道与村落,有的只是一望无际的烟雾,时刻不停歇的巨响,和无休止流淌的鲜血,以及腐烂的尸体和它们所散发的臭味。
|
||||||
|
|
||||||
|
在战争中,活着成了唯一要紧的事,也是唯一困难的事。
|
||||||
|
|
||||||
|
### 赎罪
|
||||||
|
|
||||||
|
如果战争未曾到来,也许一切的不幸都是源于布里奥妮自以为是的”成人想法“。对于一个13岁的少女来说,她能明白所谓的爱情与眼前所见一切背后的真相吗?
|
||||||
|
|
||||||
|
*我垂垂老矣,我噤若寒蝉,而且太眷恋自己的余生。我面对的是汹涌的忘却浪潮,然后是永久的遗忘…… 有情人生生不息。只要我最后一稿的打印孤本留存于世,那么,我那纯洁率性而又奇缘的姐妹和他的医生王子定会相亲相爱,直到地老天荒。 …… 我深深觉得,让我小说中的有情人最终团团圆圆,生生不息,绝不是怯弱或者逃避,而是最后的一大善行,是对遗忘和绝望的抗衡。我给了他们幸福,但我不是私心作祟,要让他们宽恕我。*
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- 可是这年代什么叫有罪呢?这个问题已经没有意义了。每个人都是有罪的,每个人都是无罪的。没有人会因一次证词而得到拯救,因为,没有足够的人,没有最够的笔和纸,没有足够的和平和耐心来记录下所有证人的供述,来收集着彼此犯下的种种罪行。你今天没有杀人?可是对多少人的死你采取了听之任之?
|
||||||
|
|
||||||
|
- 河水卷着春潮,在涌动的蓝绿间奔向大海。
|
||||||
|
|
||||||
|
- 我垂垂老矣,我噤若寒蝉,而且太眷恋自己的余生。我面对的是汹涌的忘却浪潮,然后是永久的遗忘…… 有情人生生不息。只要我最后一稿的打印孤本留存于世,那么,我那纯洁率性而又奇缘的姐妹和他的医生王子定会相亲相爱,直到地老天荒。 …… 我深深觉得,让我小说中的有情人最终团团圆圆,生生不息,绝不是怯弱或者逃避,而是最后的一大善行,是对遗忘和绝望的抗衡。我给了他们幸福,但我不是私心作祟,要让他们宽恕我。不是这样的,还不至于如此呢。
|
||||||
|
|
||||||
|
- 我们磨难过后,爱情开始滋生。
|
||||||
|
|
||||||
|
- 你要回来,我会等你。
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""URL 1:1 断言(Phase 2 T5):Halo slugs == 新站文件 slugs。
|
||||||
|
|
||||||
|
新站文章路由 = /archives/{文件名去 .md}/,与 Halo permalink 同构;
|
||||||
|
集合差集非空即失败(多一篇少一篇都不行)。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
old = {p["spec"]["slug"] for p in json.load(open(".migration/posts.json", encoding="utf-8"))}
|
||||||
|
new = {f.stem for f in Path("content/posts").glob("*.md")}
|
||||||
|
|
||||||
|
missing = old - new # 旧有新无 → 404 风险
|
||||||
|
extra = new - old # 新有旧无 → 无害但记录
|
||||||
|
|
||||||
|
print(f"halo slugs={len(old)} new slugs={len(new)}")
|
||||||
|
for s in sorted(missing):
|
||||||
|
print(f" MISSING: {s}")
|
||||||
|
for s in sorted(extra):
|
||||||
|
print(f" extra : {s}")
|
||||||
|
sys.exit(1 if missing else 0)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Halo 全量导出(Phase 2 T1):posts/photos/moments → .migration/*.json
|
||||||
|
|
||||||
|
对生产仅 GET;正文优先取列表 status.content,缺则回落 head-content 端点。
|
||||||
|
产物为迁移中间件,不入库(.gitignore .migration/)。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = "http://127.0.0.1:18090"
|
||||||
|
OUT = Path(".migration")
|
||||||
|
OUT.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get(path: str) -> object:
|
||||||
|
with urllib.request.urlopen(BASE + path, timeout=30) as r:
|
||||||
|
return json.load(r)
|
||||||
|
|
||||||
|
|
||||||
|
def export_list(api: str, size: int = 100) -> list[dict]:
|
||||||
|
"""翻页取全量 items。"""
|
||||||
|
items, page = [], 1
|
||||||
|
while True:
|
||||||
|
data = get(f"{api}?page={page}&size={size}")
|
||||||
|
batch = data.get("items", [])
|
||||||
|
items.extend(batch)
|
||||||
|
if page * size >= data.get("total", 0) or not batch:
|
||||||
|
return items
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
posts = export_list("/apis/api.content.halo.run/v1alpha1/posts")
|
||||||
|
# 正文兜底:head-content 拿完整 HTML(status.content 可能是截断摘要)
|
||||||
|
for p in posts:
|
||||||
|
name = p["metadata"]["name"]
|
||||||
|
content = (p.get("status") or {}).get("content")
|
||||||
|
if not content:
|
||||||
|
try:
|
||||||
|
head = get(f"/apis/api.content.halo.run/v1alpha1/posts/{name}/head-content")
|
||||||
|
p["_fullContent"] = head.get("content", "")
|
||||||
|
except Exception as e: # noqa: BLE001 - 单篇失败不阻断全量导出
|
||||||
|
p["_fullContent"] = ""
|
||||||
|
print(f" warn: {name} head-content 失败: {e}", file=sys.stderr)
|
||||||
|
photos = export_list("/apis/api.photo.halo.run/v1alpha1/photos")
|
||||||
|
try:
|
||||||
|
groups = get("/apis/api.photo.halo.run/v1alpha1/groups")
|
||||||
|
except Exception: # noqa: BLE001 - 分组端点缺失时空兜底
|
||||||
|
groups = {"items": []}
|
||||||
|
moments = export_list("/apis/api.moment.halo.run/v1alpha1/moments")
|
||||||
|
|
||||||
|
(OUT / "posts.json").write_text(json.dumps(posts, ensure_ascii=False, indent=1))
|
||||||
|
(OUT / "photos.json").write_text(json.dumps(photos, ensure_ascii=False, indent=1))
|
||||||
|
(OUT / "photo_groups.json").write_text(json.dumps(groups, ensure_ascii=False, indent=1))
|
||||||
|
(OUT / "moments.json").write_text(json.dumps(moments, ensure_ascii=False, indent=1))
|
||||||
|
|
||||||
|
full = sum(1 for p in posts if p.get("_fullContent") or (p.get("status") or {}).get("content"))
|
||||||
|
print(f"exported: posts={len(posts)}(含正文 {full})photos={len(photos)} groups={len(groups.get('items', []))} moments={len(moments)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Halo DB 只读导出(Phase 2 T1 补充源):posts + snapshots → .migration/。
|
||||||
|
|
||||||
|
公共 API 只有摘要、head-content 404;正文真源在 postgres extensions 表:
|
||||||
|
/registry/blog.halo.run/posts/{name} → spec.headSnapshot 指针
|
||||||
|
/registry/content.halo.run/snapshots/{uuid} → raw/content 全量
|
||||||
|
data 列是 bytea,经 base64 通道取出后 json 解析(psql 文本输出会搅转义)。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path(".migration")
|
||||||
|
|
||||||
|
|
||||||
|
def psql_b64(name: str) -> dict:
|
||||||
|
sql = "SELECT encode(data,'base64') FROM extensions WHERE name = '%s';" % name
|
||||||
|
raw = subprocess.run(
|
||||||
|
["docker", "exec", "postgres-db", "psql", "-U", "wait4x", "-d", "halo_db", "-t", "-A", "-c", sql],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
).stdout.strip()
|
||||||
|
return json.loads(base64.b64decode(raw))
|
||||||
|
|
||||||
|
|
||||||
|
def names_like(pattern: str) -> list[str]:
|
||||||
|
sql = "SELECT name FROM extensions WHERE name LIKE '%s';" % pattern
|
||||||
|
raw = subprocess.run(
|
||||||
|
["docker", "exec", "postgres-db", "psql", "-U", "wait4x", "-d", "halo_db", "-t", "-A", "-c", sql],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
).stdout.split()
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
posts = [psql_b64(n) for n in names_like("%/posts/%")]
|
||||||
|
snaps = {n.rsplit("/", 1)[1]: psql_b64(n) for n in names_like("%/snapshots/%")}
|
||||||
|
|
||||||
|
# 每篇 post 挂上 head snapshot 的 raw/content 全文
|
||||||
|
# 结构:snapshot.spec.{rawPatch,contentPatch} 直接是字符串(实测解剖)
|
||||||
|
for p in posts:
|
||||||
|
spec = p.get("spec", {})
|
||||||
|
# head 与 release 各挂一个 snapshot;实测存在"head 空、release 满"与
|
||||||
|
# 反例(mian-sha),取 content 更长者作为迁移源
|
||||||
|
cands = [snaps.get(k, {}).get("spec", {}) for k in (spec.get("releaseSnapshot"), spec.get("headSnapshot")) if k]
|
||||||
|
snap = max(cands, key=lambda sp: len(sp.get("contentPatch", "") or ""), default={})
|
||||||
|
p["_snapshot"] = {
|
||||||
|
"rawType": snap.get("rawType"),
|
||||||
|
"raw": snap.get("rawPatch", ""),
|
||||||
|
"content": snap.get("contentPatch", ""),
|
||||||
|
"snapshotCount": len(cands),
|
||||||
|
}
|
||||||
|
|
||||||
|
(OUT / "posts_db.json").write_text(json.dumps(posts, ensure_ascii=False, indent=1))
|
||||||
|
ok = sum(1 for p in posts if p["_snapshot"]["content"])
|
||||||
|
print(f"db export: posts={len(posts)}(含正文 {ok});snapshots={len(snaps)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""瞬间迁移(Phase 2 T4):Halo moments → content/moments/*.md
|
||||||
|
|
||||||
|
Halo moment.content.html 为富文本(内联样式很脏),经 pandoc 提纯为
|
||||||
|
纯文本 md;medium(配图)当前库存为空,字段留待将来有图时扩展。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path("content/moments")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
moments = json.load(open(".migration/moments.json", encoding="utf-8"))
|
||||||
|
n = 0
|
||||||
|
for m in moments:
|
||||||
|
spec = m["spec"]
|
||||||
|
if spec.get("visible") == "PRIVATE":
|
||||||
|
continue
|
||||||
|
html = (spec.get("content") or {}).get("html", "")
|
||||||
|
text = subprocess.run(
|
||||||
|
["/home/wait4x/.local/bin/pandoc", "-f", "html", "-t", "plain", "--wrap=none"],
|
||||||
|
input=html.encode(), capture_output=True, check=True,
|
||||||
|
).stdout.decode("utf-8").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
ts = spec.get("releaseTime") or m["metadata"].get("creationTimestamp", "")
|
||||||
|
day = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(timezone.utc).strftime("%Y-%m-%d")
|
||||||
|
out = OUT / f"{day}.md"
|
||||||
|
i = 2
|
||||||
|
while out.exists():
|
||||||
|
out = OUT / f"{day}-{i}.md"
|
||||||
|
i += 1
|
||||||
|
tags = spec.get("tags") or []
|
||||||
|
# schema 约束 text/image 在 frontmatter(body 不参与校验),一句话即 text 字段
|
||||||
|
out.write_text(
|
||||||
|
"---\n"
|
||||||
|
f"pubDate: {day}\n"
|
||||||
|
f"tags: [{', '.join(tags)}]\n"
|
||||||
|
f'text: "{text.replace(chr(34), chr(39))}"\n'
|
||||||
|
"---\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
n += 1
|
||||||
|
print(f"migrated {n} moments")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""图库迁移(Phase 2 T3):Halo photos → content/photos/{jpg,json sidecar}
|
||||||
|
|
||||||
|
EXIF 不在迁移时提——构建期 photos loader(src/lib/exif.ts)会补全;
|
||||||
|
sidecar 里 exif 留空对象。分组:Halo displayName 含山/雪/land/snow →
|
||||||
|
landscape,否则 street(当前仅一组 snow mountain → landscape)。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
BASE = "http://127.0.0.1:18090"
|
||||||
|
OUT = Path("content/photos")
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(name: str) -> str:
|
||||||
|
stem = re.sub(r"\.(jpg|jpeg|png|webp)$", "", name, flags=re.I)
|
||||||
|
stem = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff-]+", "-", stem).strip("-")
|
||||||
|
return stem or "photo"
|
||||||
|
|
||||||
|
|
||||||
|
def group_of(display: str) -> str:
|
||||||
|
low = display.lower()
|
||||||
|
return "landscape" if any(k in low for k in ("山", "雪", "snow", "land", "mountain")) else "street"
|
||||||
|
|
||||||
|
|
||||||
|
def compress(data: bytes) -> bytes:
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
img = Image.open(BytesIO(data))
|
||||||
|
if img.mode not in ("RGB", "L"):
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.thumbnail((2400, 2400))
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, "JPEG", quality=86, optimize=True)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
# 真实照片取代样例:先清样例(jpg/json 成对;仅清本脚本管辖的产物)
|
||||||
|
for f in OUT.glob("*"):
|
||||||
|
if f.suffix in (".jpg", ".json"):
|
||||||
|
f.unlink()
|
||||||
|
|
||||||
|
photos = json.load(open(".migration/photos.json", encoding="utf-8"))
|
||||||
|
n = 0
|
||||||
|
for p in photos:
|
||||||
|
spec = p["spec"]
|
||||||
|
url = spec.get("url") or spec.get("cover") or ""
|
||||||
|
if not url.startswith("/upload/"):
|
||||||
|
print(f" skip 非本站图: {url[:60]}")
|
||||||
|
continue
|
||||||
|
fname = unquote(url.rsplit("/", 1)[1])
|
||||||
|
slug = slugify(spec.get("displayName") or fname)
|
||||||
|
data = urllib.request.urlopen(BASE + url, timeout=60).read()
|
||||||
|
# 入仓前压到长边 2400/q86:原图总计 196M(单张最大 28M)会永久膨胀
|
||||||
|
# 公开仓历史并撞死 nginx 100M 推送上限;原图已随 Phase 0 备份归档,
|
||||||
|
# 站内展示由 sharp 出 srcset,2400px 足够 lightbox 全屏
|
||||||
|
(OUT / f"{slug}.jpg").write_bytes(compress(data))
|
||||||
|
|
||||||
|
(OUT / f"{slug}.json").write_text(json.dumps({
|
||||||
|
"title": spec.get("displayName", fname).rsplit(".", 1)[0],
|
||||||
|
"image": f"./{slug}.jpg",
|
||||||
|
"group": group_of(spec.get("groupName", "")),
|
||||||
|
"alt": spec.get("description", ""),
|
||||||
|
"exif": {},
|
||||||
|
}, ensure_ascii=False, indent=1))
|
||||||
|
n += 1
|
||||||
|
print(f"migrated {n} photos")
|
||||||
|
return 0 if n else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""文章迁移(Phase 2 T2):Halo 渲染页 → content/posts/*.md
|
||||||
|
|
||||||
|
正文源用线上渲染页(reader 看到的即所得)——实测 DB snapshots 对
|
||||||
|
shu-zui/ni-tan 只有 2 字符(patch 残迹),而渲染页完整。
|
||||||
|
|
||||||
|
处理链:抓 /archives/{slug} → 剥主题噪音(button/nav/header) → 图片
|
||||||
|
/upload/* 下载到 content/posts/assets/{slug}/ 并改写引用 → pandoc
|
||||||
|
html→gfm → frontmatter(分类/标签沿用 Halo,书评分配五档情绪色)。
|
||||||
|
"""
|
||||||
|
import html as htmllib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = "http://127.0.0.1:18090"
|
||||||
|
ROOT = Path(".")
|
||||||
|
ASSETS = ROOT / "content" / "posts" / "assets"
|
||||||
|
|
||||||
|
# 书评五档情绪色(tokens --mood-1..5);技术文不分配
|
||||||
|
MOODS = ["#b45309", "#0f766e", "#7c3aed", "#9f1239", "#166534"]
|
||||||
|
|
||||||
|
# Halo 分类名 → 集合枚举
|
||||||
|
CAT_MAP = {"Reading": "reading", "Linux": "tech", "默认分类": "tech"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url: str) -> bytes:
|
||||||
|
with urllib.request.urlopen(url, timeout=30) as r:
|
||||||
|
return r.read()
|
||||||
|
|
||||||
|
|
||||||
|
def article_html(slug: str) -> str:
|
||||||
|
page = fetch(f"{BASE}/archives/{slug}").decode("utf-8")
|
||||||
|
m = re.search(r"<article[^>]*>(.*?)</article>", page, re.S)
|
||||||
|
if not m:
|
||||||
|
raise RuntimeError(f"no article for {slug}")
|
||||||
|
body = m.group(1)
|
||||||
|
# 主题噪音:higan 主题把主题切换钮等塞进 article;标题在 frontmatter 重建,正文 h1 去重
|
||||||
|
body = re.sub(r"<button.*?</button>", "", body, flags=re.S)
|
||||||
|
body = re.sub(r"<header.*?<h1[^>]*>.*?</h1>.*?</header>", "", body, count=1, flags=re.S)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def localize_images(slug: str, body: str) -> str:
|
||||||
|
def repl(m: re.Match) -> str:
|
||||||
|
src = m.group(1)
|
||||||
|
if not src.startswith("/upload/"):
|
||||||
|
return m.group(0)
|
||||||
|
dest_dir = ASSETS / slug
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
fname = src.rsplit("/", 1)[1]
|
||||||
|
data = fetch(BASE + src)
|
||||||
|
(dest_dir / fname).write_bytes(data)
|
||||||
|
return m.group(0).replace(src, f"./assets/{slug}/{fname}")
|
||||||
|
|
||||||
|
return re.sub(r'src="([^"]+)"', repl, body)
|
||||||
|
|
||||||
|
|
||||||
|
def to_markdown(body: str) -> str:
|
||||||
|
p = subprocess.run(
|
||||||
|
["/home/wait4x/.local/bin/pandoc", "-f", "html", "-t", "gfm-raw_html", "--wrap=none"],
|
||||||
|
input=body.encode(), capture_output=True, check=True,
|
||||||
|
)
|
||||||
|
return p.stdout.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_tag_display() -> dict[str, str]:
|
||||||
|
"""内部标签名(tag-xxx) → 显示名(read/linux/...)。"""
|
||||||
|
with urllib.request.urlopen(BASE + "/apis/api.content.halo.run/v1alpha1/tags") as r:
|
||||||
|
data = json.loads(r.read())
|
||||||
|
return {t["metadata"]["name"]: t["spec"]["displayName"] for t in data.get("items", [])}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
posts = json.load(open(".migration/posts.json", encoding="utf-8"))
|
||||||
|
tag_disp = load_tag_display()
|
||||||
|
mood_i = 0
|
||||||
|
report = []
|
||||||
|
for p in posts:
|
||||||
|
spec = p["spec"]
|
||||||
|
slug = spec["slug"]
|
||||||
|
tags = [tag_disp.get(t, t) if isinstance(t, str) else t.get("name", "") for t in (spec.get("tags") or [])]
|
||||||
|
cat_raw = spec.get("category") or ""
|
||||||
|
# 分类判定:Halo 分类名优先,回退看标签(read→reading / linux→tech)
|
||||||
|
if CAT_MAP.get(cat_raw):
|
||||||
|
category = CAT_MAP[cat_raw]
|
||||||
|
elif any("read" in t.lower() for t in tags):
|
||||||
|
category = "reading"
|
||||||
|
else:
|
||||||
|
category = "tech"
|
||||||
|
|
||||||
|
mood = None
|
||||||
|
if category == "reading":
|
||||||
|
mood = MOODS[mood_i % len(MOODS)]
|
||||||
|
mood_i += 1
|
||||||
|
|
||||||
|
body = localize_images(slug, article_html(slug))
|
||||||
|
md = to_markdown(body).strip()
|
||||||
|
md = re.sub(r"^(-{4,}|_{4,})\s*", "", md) # 原 hr 开头去掉(frontmatter 后首元素应是正文)
|
||||||
|
fm = [
|
||||||
|
"---",
|
||||||
|
f'title: "{htmllib.escape(spec["title"], quote=False)}"',
|
||||||
|
f"pubDate: {spec['publishTime'][:10]}",
|
||||||
|
f"category: {category}",
|
||||||
|
f"tags: [{', '.join(tags)}]",
|
||||||
|
]
|
||||||
|
if mood:
|
||||||
|
fm.append(f"mood: '{mood}'")
|
||||||
|
fm.append("---")
|
||||||
|
out = ROOT / "content" / "posts" / f"{slug}.md"
|
||||||
|
out.write_text("\n".join(fm) + "\n\n" + md + "\n", encoding="utf-8")
|
||||||
|
n_img = len(list((ASSETS / slug).glob("*"))) if (ASSETS / slug).exists() else 0
|
||||||
|
report.append((slug, category, len(md), n_img))
|
||||||
|
print(f" {slug} [{category}] {len(md)} md-chars, {n_img} imgs")
|
||||||
|
|
||||||
|
# 清理被真实内容取代的样例文件(真实技术文 slug=csge-lei-pei-zhi,样例为 cs-ge-lei-pei-zhi)
|
||||||
|
sample = ROOT / "content" / "posts" / "cs-ge-lei-pei-zhi.md"
|
||||||
|
if sample.exists():
|
||||||
|
sample.unlink()
|
||||||
|
print(" removed sample: cs-ge-lei-pei-zhi.md")
|
||||||
|
print(f"migrated {len(report)} posts")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||