历史遗留约束清单
项目 2026-07 由上一版实现(Flask 单体)整体重写为当前的 Next.js 实现。 2026-09 做过一轮全仓注释清理,把**「向已删除的实现看齐」的注释写法清干净了 —— 记录见 §5。 本文留下的是清理之后仍然动不得的东西**,以及它们为什么动不得。
⚠️ 判据只有一条:「是不是因为用了 Flask 才长这样」。 大量所谓「兼容层」其实是我们自己的设计与对外契约 —— 换任何框架写一遍值都一样。 把它们叫「框架包袱」会误导后来人,也会让真正的技术约束藏在噪声里(见 §1.2)。
1. 继承的约束(不能删)
1.1 上一版技术栈留下的物理形态(真·框架痕迹)
这六项的共同点:如果当年用的不是 Flask,它们根本不会长这样。
| 项 | 位置 | 为什么 |
|---|---|---|
| werkzeug 密码哈希格式 | src/lib/password.ts |
库里 password_hash 躺的是 werkzeug 生成的 scrypt:N:r:p$salt$hex。生态库的默认选择,不是我们的设计。新哈希也保持 werkzeug 可读(双向过渡),改动即等于让存量用户重置密码 |
Fernet 密钥派生自 SECRET_KEY |
src/lib/secret-box.ts |
派生方式 base64url(sha256(keySource)) 来自旧实现,且当年的密钥源是旧站的 SECRET_KEY。它当年服务两处凭证:用户发往账户微服务的 API Key、回调签名密钥。前者随账户服务搬进站内一起消失,后者还是活的 —— 改了派生,fish_webhook_endpoints.secret_encrypted 全解不开,商户再也收不到回调、且不可逆。⚠️ 密钥源这一半已经可以摘掉:写成 FISH_ENCRYPTION_KEY、跑 fish webhook-rekey 把存量重封过去即可(新行已经不碰 SECRET_KEY 了,见 fish-webhook-service.ts 的「回调签名密钥的钥匙」)—— 冻的只是派生公式,不是「必须挂在会话密钥上」 |
users.fish_api_key_encrypted 列 |
prisma/schema.prisma |
站外账户微服务签发的用户 API Key 的 Fernet 密文。该服务已搬进站内,这一列没有任何代码再读它,但列与存量密文留着不删(删列要迁移,而迁移的风险大于它占的空间)。看到它别以为还有远端 |
account_sync_ledger 表名 |
prisma/schema.prisma、src/lib/fish-idempotency.ts |
名字说的是「账户服务同步账本」(outbox),而那个机制已经不存在 —— 现在它只是一张幂等记录表,新行一律 status='synced'。表刻意不改名:里面有真的历史账(那些行的 payload/status 是当年对账的唯一凭据)。语义以 fish-idempotency.ts 头部为准 |
| 时间戳的 TEXT 存储形态 | src/lib/fish-service.ts、src/lib/db-time.ts |
SQLAlchemy 把 datetime.now() 按 "YYYY-MM-DD HH:MM:SS.ffffff" 写成 TEXT;新行是 INTEGER,混存。SQLite 跨存储类型比较按类型序不按数值,裸 SQL 日期函数一律不可靠 |
| 物理库由 Alembic 建出 | prisma/migrations/0_init/migration.sql |
530 行基线是从 Alembic 管出来的真库反向生成的。接手已有库必须先 mark 不能 up |
instance/ 目录名 |
项目根 | 旧框架的 instance 目录约定,Next 直接沿用。存量文件全在里面 |
?next= 回跳语义 |
src/lib/safe-url.ts、src/lib/guard.ts |
源自旧站的 login_view 行为,现在是既定契约(登录后必须回原页) |
⚠️
session_version失效机制不属于这一类 —— 它常被说成「对齐 Flask-Login」, 但 Flask-Login 并不提供这个字段。它是旧版自建的、Next 继承的安全设计,见 §1.2。
1.2 我们自己的设计与对外契约 —— 不是框架痕迹
这八项经常被误列进「框架兼容层」。当年就算用 Django / Rails / 纯 WSGI 写,值也一模一样。 它们不能改,是因为存量数据与对外契约按这个口径存在,不是因为框架。
| 项 | 位置 | 真正的理由(与框架无关) |
|---|---|---|
| identicon 算法 | src/lib/identicon.ts |
md5 前 6 位取色、8×8 镜像网格、底色 (240,240,240)。改则老用户头像图案全变 |
| 字数统计正则 | src/lib/blog-service.ts 的 countMarkdownWords |
5 条正则(只有代码块那条跨行)。改则存量文章字数集体变化 |
| 两步式签到 | src/lib/checkin-service.ts |
checkIn() 建记录时 fortune_value 留 NULL,翻牌才填。是我们刻意这么设计的,库里真实存在这类行 |
| 短 ID / 邀请码字符集 | src/lib/short-id.ts、src/lib/invite-code.ts |
要能继续校验存量已发出的码(12 位 base62,注册侧按 length===12 校验) |
| 幂等键格式 | src/lib/fish-idempotency.ts、fish-compensate.ts |
客户端幂等键「1–48 位、[A-Za-z0-9_.:-]」是对外契约(docs/bot/fish-bot.md §6 按它写)。群发补偿的批次 ID 逐字节稳定是刻意的,为了让迁移前跑了一半的批次能续跑。注意:键之所以长这样(含 xfer-{8位哈希} 前缀、48 字上限)是因为当年要发往账户微服务、对方限 64 字符 —— 那边界没了,但格式不能改:存量 account_sync_ledger 里的键与新键共用一个命名空间 |
| API JSON snake_case 形状 | src/lib/blog-service.ts 等 |
对外契约:站外机器人按这个形状写死了。对外文档见 docs/bot/ |
| 权限档位语义 | src/lib/guard.ts、src/lib/admin-user-service.ts |
角色阶梯(user → core → admin → owner)是产品设计,见 docs/architecture.md §8 |
session_version 失效机制 |
src/lib/session.ts、src/lib/auth.ts |
改密 / 禁言 / 强制下线要让旧会话立即失效 —— 安全需求 |
其余同样不能删、但同属「契约」而非「框架」的:next.config.mjs 的两条旧直链
rewrite(存量 55 篇 / 110 处写死的图床地址,见 tests/e2e/legacy-urls.spec.ts)、
snake_case 表名(@map,改则落到不存在的列)、_raricy_migrations 跟踪表、
CLI 退出码与 LEGACY_COMMANDS 旧命令名。
2. 唯一保留的框架字面量
scripts/check-links.mjs 里有一个 if (/FLASK_ORIGIN/.test(txt)) 的守卫。
这是全仓仅剩的 flask 字样,而且是刻意留的:FLASK_ORIGIN 是被检查对象的名字,
不是归因 —— 它当年是「回源旧后端」的源站地址,代码读到它就是拿旧部署的 origin 拼地址。
删掉这个 if 是删守卫(行为变更),不是清理注释。
今天更可能的错误是有人照旧习惯把这个变量加回去,留着零成本。
3. 命名与目录的结构性遗留
| 项 | 位置 | 判定 |
|---|---|---|
instance/ |
项目根 | §1.1 的目录约定。整套数据全在里面,不动 |
public/static/** |
public/static/{img,js} |
「Next 的 public/ + 旧站的 static/」两层叠。全站 30+ 处引用 /static/...,4 个 e2e 用例盯着 —— 改造成本远大于收益,不建议动 |
static/js/core/base.js、static/js/cattca.js |
public/static/js/ |
沿用的手写 JS,src/app/layout.tsx 用 <Script> 加载。是在跑的代码,不是残留 |
instance/blogs/ |
instance/blogs/ |
6195 个空目录。scripts/check-instance.mjs 仍会创建它「以兼容老路径」 |
| 环境变量名 | IMAGE_UPLOAD_FOLDER / AVATARS_DIR / STORIES_DIR / STICKERS_DIR |
全大写 + _FOLDER 后缀是旧站风格。改名会打断生产 .env.production,别动 |
css:probe 的产物名 |
src/styles-scss/compiled/probe.css |
2026-09 由 flask.css 改名而来(唯一「名字里带 flask」的产物)。gitignored 的离线调试产物,消费方是 tests/.tmp/*.html 的探针 |
4. Python 源码本体:HEAD 已清零,git 历史仍可检出
- HEAD 里
.py= 0(git ls-tree -r HEAD实测)。requirements.txt/Pipfile/pyproject.toml/setup.py/__pycache__/venv一条都没有。 - 删除发生在
7d7be1c(2026-07-18),一次提交干掉 436 个文件 (其中 174 个.py、79 个.html模板)。同一次还把web-next/摊平到了项目根。 - 这些源码仍在 git 历史里(
.git约 31 MB),随时可检出:
git show 7d7be1c^:app/models.py # 看某个旧文件
git ls-tree -r 7d7be1c^ --name-only app/ # 列出旧 app/ 全树
不建议重写历史(filter-repo / BFG):代价是全仓 commit hash 变更,收益只是让一个 已删除的目录不出现在历史里。真正在意的是工作区,而工作区已经干净。
5. 2026-09 注释清理:记录与口径
清理覆盖 785 行 / 220 文件(git grep -i flask 实测)。过程见 99f89bf(重写本文、
把那本旧的 flask 遗留清单整本删掉,并改 ../CLAUDE.md 的注释约定)、c08221f、a157b08
三个提交 —— 细节不在这里留副本。
再遇到这类注释时的铁律:绝不整行删(整行纯溯源除外)—— 「悬空引用」与「契约」常写在
同一条里,整行删会把契约一起删掉。判例:src/lib/checkin-service.ts 那条「跨 UTC+8 午夜
会显示『今天还没有签到』,这不是 bug」,删了下一个开发者真会当 bug 修。做法是
去出处、留契约(// 对齐 Flask @authenticated_required:需核心用户 → // 需核心用户(core+))。
刻意保留的:docs/architecture.md 开篇边界声明与 §9「迁移史速查」、docs/cli.md 里
~~flask fish compensate~~ 那种对旧命令名的删除线标注 —— 那是历史记录,不是参照系,
改掉反而认不出旧命令;§1.1 的技术事实(werkzeug / SQLAlchemy / Alembic / itsdangerous)
留给运维排障;指本仓现行代码的「对齐」照旧写(如「对齐 src/lib/guard.ts 的现行语义」)。
背景:切到 Next 之后的提交数是 520,整个 Flask 时期是 353 —— Next 侧早就不是 「新版本」,而是这个项目的标准版本。继续把已删除的实现当参照物,对读者是悬空引用 (那些文件不在工作区),对决策是错误归因。现行约定见
../CLAUDE.md「项目概述」。
6. 假阳性清单(搜索时会误伤)
搜 flask / python / \.py 时下面这些不是残留:
| 命中形态 | 位置 | 实际是什么 |
|---|---|---|
.py-4 / .py-5 |
src/styles-scss/utilities/_spacing.scss |
padding-block 工具类,Bootstrap 命名 |
dragStartRef.current.py |
src/app/components/ImageLightbox.tsx |
鼠标事件的 pageY 简写字段 |
aliases:["jinja"] |
public/static/vditor/dist/ 下的 highlight.js / markmap |
vendored 第三方库里的语法高亮语言定义 |
makeLegacyCheckin / legacyRoleCommands |
tests/、scripts/cli/commands/roles.ts |
「旧」指本应用自己的旧实现 |
| 「旧实现 / 原实现」 | src/lib/chat-service.ts、tests/global-setup.ts 等 |
同上 —— 绝大多数是 Next-vs-Next 的历史,是解释回归的正当线索,别扫 |
account_sync_ledger ~ fish_api_key_encrypted ~ ACCOUNT_SERVICE_* |
prisma/、src/lib/fish-idempotency.ts、.env* 的注释 |
站外 FastAPI 账户微服务(独立仓库)的遗物 —— 它是 Python,但不是 Flask,也不是本仓的代码。那个服务已于 2026-09 搬进站内,见 §1.1 的三行 |
{{ / {% |
各处 .tsx |
JSX 的 style={{…}} 双花括号,不是 Jinja |
Bootstrap 风格 / Bootstrap-like / no Bootstrap |
十几处 src/styles-scss/** 注释 |
本站完全不用 Bootstrap,这些是类比:类名(.card / .table / .m*-*)沿用了 Bootstrap 的命名与外观习惯,实现全是我们自己的。删了这些注释不会少一行样式 |
Bootstrap 的 show / 不是 Bootstrap 那套 |
CommentSection.tsx、ImagePickerModal.tsx、两个 e2e 用例 |
防混淆警告,价值高:站内 modal 的展开类是 is-open / show,与 Bootstrap 的不是一回事,写错就点不开 |
Bootstrap Icons |
src/lib/poster.ts、src/styles-scss/pages/blog/_menu.scss |
一个图标库的名字,与 Bootstrap 框架无关 |
7. 待决事项(需站长定夺)
src/app/tool/translate/是孤儿(0 引用,同样不在ToolMenu里)。页面没有界面, 只有一句服务端redirect('http://116.62.179.232:9198')(page.tsx:5)。未删, 留待你定 —— 同类的tool/redirect、new_redirect已作为孤儿页删掉(a157b08), 它成了最后一个。写死的「IP + 端口」是刻意的(存在无法做 DNS 解析、只能用 IP 访问的用户),不是待清理的旧站地址 —— 同 §1.2 的两条旧直链 rewrite。src/app/components/CheckinCard.tsx:22的FORTUNE_LABELS[5]多一个尾随空格 ('运势爆棚 '),服务端src/lib/checkin-service.ts:55那份没有。服务端有单测钉着, 客户端那份没有,于是漂了 —— 翻牌弹窗渲染的是客户端这份。疑似真 bug,未改。tests/e2e/fish-layout.spec.ts:60有一条既有的 tsc 报错 (el.innerText不存在于SVGElement | HTMLElement),与 2026-09 清理无关,当时未动。