Skip to content

Latest commit

 

History

History
418 lines (325 loc) · 151 KB

prd.md

File metadata and controls

418 lines (325 loc) · 151 KB

IMiss Blog 产品需求文档(PRD)

项目 内容
产品名称 IMiss Blog
文档版本 v1.123
最后更新 2026-10-19
关联文档 ARCH.md(架构与技术设计)、PROJECT_STATUS.md(阶段、进度、下一步、阻塞项)
基线代码 上游模板基线 v6.16.8(Astro 7 静态站点,名称与出处见 README.upstream.zh.md),来源 D:\HuaweiMoveData\Users\HUAWEI\Desktop\my_-blog\Blog template
需求来源 项目发起人口述需求 + 上游模板的在线演示站(上游出处集中说明见 README.upstream.zh.md) + 原型 my_-blog\blog_test + 排行榜参考图 my_-blog\dosc\排行(⚠️ 历史来源:排行模块已于 v1.41 整体移除、参考图随之作废,此行只为保留需求出处,见 R-PATH-06 与 ARCH.md §14.4)

0. 怎么读这份文档

  • 第 1 章说明产品定位与成功标准;第 2 章是改造后的站点地图;第 3 章是需求清单(唯一的需求来源,编号 R-xxx)。
  • 每个需求有唯一编号,ARCH.md 给出"改哪个文件",PROJECT_STATUS.md 给出"做到哪一步"。三份文档用同一套编号互相引用。
  • 标记说明:
    • 【已定】 = 发起人已明确表态,不再讨论。
    • 【假设】 = 执行方按现有素材推导的决定,可以推翻,需要推翻时先改这份文档。
    • 【待提供】 = 依赖发起人提供的信息,已集中登记在 PROJECT_STATUS.md 的阻塞项表。

1. 产品定位

1.1 一句话定位

IMiss Blog 是一个以《鸣潮》角色「爱弥斯」为主题的个人技术博客:写技术笔记与踩坑记录,站点外观、背景壁纸与桌宠都围绕爱弥斯展开,并配一只可以聊天的爱弥斯桌宠(由博客专用 Agent 驱动)。

1.2 名字由来

站点名 IMiss 取自「爱弥斯」(拼音 Ai Mi Si)的谐音,同时保留 "I Miss" 的语义双关(想念 / 我思念)。英文名与中文社区习惯都统一写作 IMiss Blog。

1.3 视觉基调 【假设】

维度 取值 说明
主题色相 hue = 345(爱弥斯粉) 模板默认是 165(青绿),改为粉色系
深浅模式 跟随系统(system) 沿用模板默认,两套主题都要能看
背景 图片期已结束:4 张静态图已删,现在是发起人给的 5 支动态壁纸(原画) 见 R-BG-01 / R-BG-02 / R-BG-07
角色元素 桌宠爱弥斯(像素图集)、站点标识(小图标纸飞机 design/imiss-logo.png + 大图标与侧栏头像用人物插画 design/imiss-avatar.png) 见 R-PET-01 / R-ID-04

1.4 目标用户与使用场景

角色 场景 关注点
作者本人 写文章、维护学习路线、更新壁纸与动态 内容好写、改配置就能上线、构建不依赖外网
技术访客 搜一篇笔记(C++ / Linux / MySQL / 算法 / 八股 / 项目),或顺着学习路线按主线系统读 找得快、读得舒服、代码可复制、手机能看
ACGN 访客 和桌宠聊天、逛轻小说与番剧页、看「关于」页 桌宠有反应、页面好看、关于页写得亲切

1.5 成功标准

上线验收(客观)

  1. pnpm check、pnpm type-check、pnpm build 三条命令全部通过,dist/ 能正常被 Nginx 托管。
  2. 导航结构与第 2 章表格完全一致(用逐项截图核对,桌面 1440px 与移动 390px 各一遍)。
  3. 首屏媒体总量控制在预算内(见 6.1),390px 宽度下无横向溢出。
  4. 桌宠在没有 Agent 后端时不报错、走本地 mock;打开 Agent 后能流式回复。
  5. 学习路线页(/roadmap/)能访问:8 条主线都在、页内 8 个锚点都能跳到对应区块、篇目并集等于站内全部 138 篇(见 R-PATH-02~04)。

体验验收(主观)

  1. 访客能在 3 次点击内从首页到达任意一篇文章。
  2. 首屏视频壁纸不卡顿、不闪;关掉特效或系统偏好减少动态时,页面结构不塌。
  3. 桌宠"像一个住在这个站里的角色",而不是一个贴纸。

2. 站点信息架构(改造后)

2.1 顶层导航(从左到右)

# 导航项 类型 子项(从上到下) 来源 状态
1 主页 链接 — 模板自带 保留
2 文章 下拉 归档、分类、标签、系列 模板自带 保留
3 学习路线 链接 —— 新增 新增(R-NAV-02、R-PATH-01)
4 我的 下拉 动态、项目、相册、书签导航、轻小说、番剧(v1.83:原来最后还有「关于我」「关于本站」,已按发起人要求搬去顶层的「关于」) 模板改造 重构(R-NAV-03)
5 链接 下拉 5 条外链(按此顺序):Gitee、GitHub、CSDN、洛谷、Boost Search Engine(v1.61:发起人原话「把下拉链接换成接下来我给你的链接」→ 整批换成 6 条;v1.123:末两条「Boost 搜索引擎(:8081)」+「HTTP 服务器(my_server)」并成一条「Boost Search Engine」http://106.52.158.48:8080/ —— 发起人把搜索引擎与 HTTP 服务器合成一个服务,端口 8081 → 8080;v1.57 那 8 条里只有 GitHub / 洛谷 / CSDN 三个名字留下、URL 全换。两条照办的细节:CSDN 去掉跟踪参数 ?spm=… 只留干净地址;Boost Search Engine 是裸 IP + 非标准端口 + HTTP,照他给的地址原样收录,配置注释里写明"有域名后换 https://<域名>/"的建议) 模板自带 ✅ 已按发起人清单重建(v1.61 换第二批 6 条 → v1.123 并成 5 条)
6 关于 下拉 关于我 /about/、关于本站 /about-site/(v1.83 从「我的」整体搬来,顺序不变) 原「留言」那一格改造 改造(R-NAV-04)

2.2 改造前后对照表(逐条对应发起人的截图要求)

原状态 改后状态 需求编号
站点名(模板默认英文名,<上游主题名> Blog) IMiss Blog R-ID-01
「社交」下拉(友链、留言) 父项删除;那一格先放「留言」(v1.65),v1.83 起换成顶层「关于」下拉(关于我 + 关于本站),留言功能整体删除 R-NAV-01 / R-NAV-04 / R-NAV-05
无「排行」 「文章」与「我的」之间那格:v1.41 起是**「学习路线」/roadmap/ 单链接**(原「排行」下拉随 P2R 删除) R-NAV-02 / R-PATH-01
「我的」含 动态/项目/相册/书签导航/哔哩哔哩/番组计划/VNDB/AnimeList 删哔哩哔哩、番组计划;VNDB→轻小说(功能不变);AnimeList→番剧(功能不变) R-NAV-03
「关于」下拉(打赏、关于我) v1.65:顶层下拉删除、/about/ 与 /about-site/ 一度挂在「我的」下;v1.83:顶层「关于」下拉建回来(子项=关于我 + 关于本站),「我的」不再有这两项 R-NAV-04 / R-NAV-03
打赏(页面 + 导航 + 文章页按钮) 彻底没有 R-NAV-06
友链页面 删除入口(页面返回 404) R-NAV-05
站点统计含 运行时长、最后活动 只删这两项,其余保留 R-WDG-01
日历含年度热力图 日历保留,热力图关闭 R-WDG-02
站点信息(构建平台/版本/系统等) 保留组件,v1.117 已回填真实值(占位开关关闭,见 R-WDG-03 状态列) R-WDG-03
背景为示例图片 已完成:图片期换过 4 张 → v1.70 起改成 5 支动态壁纸(静态图全删) R-BG-01 / R-BG-02 / R-BG-07
无桌宠 爱弥斯桌宠(9 状态行为 + 交互 + 三档运动模式 + 右键菜单定位与飞行手感 + 「落下去/飘起来」真机落地与语气类帧率下限 + 手机端显示与两档缩放 + 「缩小」档缩成很小一只 + 聊天框「点外面即关」) R-PET-01 ~ R-PET-14
无 Agent 桌宠可对话,由博客专用 Agent 驱动 R-AGT-01 ~ R-AGT-04

2.3 页面清单

页面 URL 来源 数据来源 首期上线
首页 / 模板 文章集合 ✅
归档 /archive/ 模板 文章集合 ✅
分类 /categories/ 模板 文章集合 ✅
标签 /tags/ 模板 文章集合 ✅
系列 /series/ 模板 文章集合 ✅
文章详情 /posts/<slug>/ 模板 文章集合 ✅
学习路线 /roadmap/ 新增 roadmapConfig.ts(数据全部来自站内已导入的 134 篇笔记,零新素材) ✅
动态 /dynamic/ 模板 本地 src/content/dynamic/*.md ✅(内容后续自填)
项目 /projects/ 模板(v1.79 起是自写内容;v1.102 起四个项目) src/content/projects/ ✅
相册 /gallery/ 模板 图集配置(v1.87 起有真相册「本站图片」:9 张原图、字节原样、42.5 MB(v1.108 追加 1 张),见 R-GAL-01) ✅
书签导航 /booknav/ 模板 书签配置(v1.57 起是发起人最终选定的 6 组 39 张 —— v1.57 落地时 38 张,v1.88 按他的话加了「MiniMax 媒体平台」,见 R-BOOKNAV-01~08) ✅
轻小说(原 VNDB) /vndb/ 模板改名 VNDB API(dynamic 模式,客户端拉取) ⚠️ 需 userId(待提供)
番剧(原 AnimeList) /myanimelist/ 模板改名 MAL API(构建期拉取) ⚠️ 需 username + clientId(待提供)
关于我 /about/ 自写(v1.51 起)→ v1.65 起改为占位页(原内容整体搬到「关于本站」)→ v1.90 起是发起人给的自述:学习笔记说明 + 电话/微信/邮箱 + 「另一个世界的我」(7 条游戏身份)+ 「曾经的我」 src/content/spec/about.md ✅ v1.90
关于本站 /about-site/ 自写(v1.65 新增;v1.86 起是第一人称的站内使用说明;v1.94 整页重写:站里有什么(数字按产物数)/ 怎么逛 / 怎么跟爱弥斯说话 / 怎么换外观 / 手机上怎么用 / 这个站是怎么搭的 —— 不再写「还没有什么」) src/content/spec/about-site.md ✅
链接 外链下拉 模板 配置(v1.61 起是发起人的 6 条,v1.57 那 8 条已整批替换) ✅
留言 /guestbook/ 模板 — ❌ v1.83 整体删除:页面与 spec/guestbook.md 都已删 → 真 404(R-CMT-01/02 换口径)
搜索 /search/ 模板 Pagefind(索引层 v1.51 起有 4 条断言:搜上游字样必须 0 命中 + 正控「指针」必须 > 0) ✅
404 /404/ 模板 — ✅
友链 /friends/ 模板 — ❌ 关闭(R-NAV-05)
哔哩哔哩 /bilibili/ 模板 — ❌ 关闭(R-NAV-03)
番组计划 /bangumi/ 模板 — ❌ 关闭(R-NAV-03)
打赏 /sponsor/ 模板 — ❌ 关闭(R-NAV-06)

3. 需求清单

优先级用符号标注,刻意避开 P0~P6(那是 PROJECT_STATUS.md 里的阶段编号,两套编号不混用): ★ = 首期必须;◆ = 首期后紧接;○ = 可以更晚。

3.1 站点身份(R-ID)

编号 需求 详细说明 验收条件 优先级
R-ID-01 站点名改为 IMiss Blog ① 导航栏标题 = IMiss Blog;② 浏览器标题、OG 标题同步;③ 页脚版权名(profileConfig.name)与站点名分开,用作者名 首页标签页显示 IMiss Blog;导航栏左侧文字为 IMiss Blog ★
R-ID-02 主题色改为爱弥斯粉 themeColor.hue = 345,亮/暗两套主题都检查对比度(正文与背景、按钮文字)。⚠️ 注意:345 是默认值——R-ID-07 的显示设置面板对访客开放后,访客能在面板里拖"主题色相"覆盖它(写进他自己的 localStorage,只影响本人) 亮暗模式下主色、链接、按钮均为粉色系;未动过面板时色相为 345;正文对比度 ≥ 4.5:1 ★
R-ID-03 站点元信息 标题、副标题、描述、关键词、site_url(域名待提供,先占位并记待办) site_url 用占位域名时 sitemap 正常生成、不报错(RSS/Atom 已随 R-ID-06 删除);填真值后 OG 图为绝对地址;verify:ui 断言 description/keywords 非空、URL 是本站占位域名 ★
R-ID-04 站点标识与头像(Logo / favicon / 头像) v1.64 起是"小图标 + 大图标"两套素材(发起人 2026-10-07 看了 Edge「安装应用」那张卡后定的分工,参考上游作者站 firefly.cuteleaf.cn):小图标 = 纸飞机(设计源 design/imiss-logo.png;⚠️ v1.78 换过图:现为发起人 2026-10-09 给的手绘涂鸦风纸飞机,1536×1024、自带透明底、trim() 后本体 1426×975 = 1.463:1;上两版是 2026-10-07 给的扁平三色纸飞机,1254×1254、本体 1069×793 = 1.348:1,见 git 历史)、大图标与侧栏头像 = 爱弥斯人物插画(设计源 design/imiss-avatar.png,发起人给的 486×487 方图)。两张源图都按字节原样提交,由 pnpm make:logo 出图:导航栏 Logo(webp 亮/暗各一,256×175,纸飞机;v1.78 换图前是 256×190)、favicon 32px(纸飞机,透明底方形画布、满宽居中)、favicon 128 / 180 / 192(人物,满幅不透明方图 —— 180 = Apple 触屏、192 = Android 与 Edge 安装卡取的那张)、src/assets/images/avatar/imiss-avatar.webp(486×486,侧栏资料卡与 schema.org 的作者 image 共用)。四档 favicon 全部登记进 siteConfig.favicon 并各带 sizes(v1.43 / P6-8:此前只登记了 32px,180/192 生成了却无人引用)。设计源放仓库根的 design/ 而不是 src/assets/:Navbar.astro 的图片 glob 会把 src/ 下所有图片打进 dist(实测那张 1.30 MB 源图会出现在 dist/_astro/ 且零引用)。
⚠️ v1.63 及以前:标识是手写矢量 src/assets/images/logo/imiss-logo.svg(粉色晶体心 + 青色四角星芒 + 机械翼片 + 深蓝紫方圆底),v1.64 已删除
⚠️ 头像这一半关闭了 B-02 的一部分:作者名 / 个人签名 / 社交链接仍待发起人给
导航栏亮暗模式下标识都清晰、不拉伸;小图标 32px 下仍可辨认(透明底横向纸飞机;⚠️ v1.78 换图后的实测口径:这版画细节极密(猫脸/星星/爪印/房子),在导航栏 28px 与 32px favicon 上涂鸦必然糊成色点 —— 能辨认的是“横向纸飞机的轮廓”(粗描边撑着),不是里面的涂鸦。已出新↔旧、明/暗两种底的 10× 放大对照图(reports/_logo-legibility.mjs);要不要为小尺寸单做一版“简化标”(只留机翼/尾翼)等发起人定,本轮未擅自改画);大图标与侧栏头像是同一张画(verify:ui 用 8×8 指纹比对,实测平均差 6.8,而 32px 小图标与它们差 120.6);改设计只需换 design/ 下那张源图并重跑脚本;apple-touch-icon 指向 180×180 那张(Apple 约定);verify:ui 有 8 条 R-ID-04 断言钉住(3 条钉文件名/接线 + 5 条真浏览器读像素:32px 透明带与 1.35:1 包围盒、导航栏 logo 256×190、180/192 满幅不透明方图、头像与大图标同源且与小图标不同源) ◆
R-ID-05 清理上游示例凭据与假数据 模板把上游作者的示例值直接提交在了配置里:mal.clientId 是带前导制表符的 32 位十六进制串(形如真实凭据,mal-utils.ts 还会把它原样塞进请求头);另有 mal.username、vndb.userId、bangumi.userId、bilibili.uid。首页横幅里的模板作者邮箱(mailto: 链接,会被编码成 base64 挂到每个页面)与 GitHub 链接也属于这一批(见 R-NAV-06)。全部清空或替换为本站占位,避免把别人的凭据与身份随仓库分发。v1.49 追加同一批的两类:① src/config/commentConfig.ts 的五处上游演示端点(twikoo.envId: "https://twikoo.vercel.app"、waline.serverURL: "https://waline.vercel.app"、artalk.server: "https://artalk.example.com/"、giscus.repo + repoId + categoryId(原值是模板作者的仓库,切到 giscus 就会把访客评论发到别人仓库)、disqus.shortname(原值是模板作者的示例名))→ 全部 ""(type 仍 "none",将来必须填自己的地址,giscus 必须填自己的 repo/repoId/categoryId);② 源码层与产物层的上游字样清零:public/assets/css/twikoo-custom.css 那条上游署名行删除,8 处客户端标识符/事件名/关键帧/组件文件名/package.json name 统一改成中性前缀 imiss 配置里不再出现上游示例 ID;mal.clientId 为空字符串且无前后空白;commentConfig 五处端点为空串;v1.47 / P4-8 起扫描面从"4 条路由抽样"扩到全站,v1.49 再扩到产物全集:verify:ui 对 dist/** 全部 799 个文件逐个扫 + 文件名/路径(大小写不敏感的 8 条上游品牌/身份正则,逐字见 scripts/verify-ui.mjs 的 IDENTITY_NEEDLES 表与 UPSTREAM_CI 常量 —— 它们是判据本体、必须含这些字面量;同文件里另有两类保留的上游字面量:构建平台旧变量名的兼容分支、REMOVED_ENTRY_NAMES 反面清单。三份文档与 README.md 都不再重复这些字面量,只指向该文件),二进制按"含 NUL 字节"跳过内容但照查文件名,品牌/身份串零命中(v1.51 起零例外:about/index.html 那个唯一的例外已随关于页换成中立版连例外代码与 INFO(待办) 一并删除,理由与实测见 R-ID-10);v1.51 追加:搜索索引也搜不到上游字样(dist/pagefind/** 那 184 个压缩索引分片是上面那条"产物全集"扫描的盲区 —— 二进制、内容整批跳过,而关于页是带 data-pagefind-body 被索引的;现在拿站点实际加载的 window.pagefind 直接问搜索:正控「指针」> 0(API 73 条 + 真实搜索框打字渲染出结果链接),负控 夏叶/cuteleaf/Firefly 必须精确查询 0 条 + 返回结果正文里也没真的出现 + 搜索面板文字不含。⚠️ 判据第一版写成"结果数 = 0"是错的:pagefind 的 zh-cn 分词器把拉丁词切成 CJK 二元组,实测各 3 条噪声而正文里一条都没有,改成"精确查询 + 逐条查正文"两条腿) ★
R-ID-06 删除订阅(RSS / Atom)功能 发起人不需要订阅功能。要删的对象:① /rss/、/atom/ 两个介绍页与 /rss.xml、/atom.xml 两个 feed 路由;② src/utils/feed-utils.ts;③ <head> 里两条 <link rel="alternate">;④ 页脚的 RSS/Atom 链接(保留 Sitemap);⑤ 首页横幅那行订阅图标(homeText.linksEnable: false + 清空 links);⑥ 侧栏资料卡的订阅图标(profileConfig.links 清空)。恢复清单见 ARCH.md §14.4 四条订阅路由真 404(文件已删,不是 noindex 存根);首页/页脚/资料卡/head 里都不再出现 rss/atom 字样;verify:ui 有对应断言 ★
R-ID-07 显示设置面板(调色盘 + 界面结构管理) 上游自带、默认 enable: false(P1 期间未做任何改动)。面板条目:主题色选择器(调色盘)、文章列表布局、卡片边框、卡片跟随主题色、壁纸模式切换、全屏布局、水波纹、渐变过渡、横幅标题、壁纸轮播、覆盖模式三个滑块、樱花特效。发起人已决定(2026-09-24):对访客默认开启,且所有条目全开(原话"肯定要这个啊")→ 现已是 enable: true + 13 个条目全开。面板本身的改进暂缓:发起人指示"先不改,先把 P2/P3 做完",⚠️ v1.53 更正:面板改进这一项已关闭、不再排期 —— 「排在 P2、P3 之后」是 2026-09-24 当时的说法,P1-18 已按发起人 2026-09-29 看过面板截图后的判定「不用改」在 v1.45 关闭(实测自带 8 个 aria-label="Reset to Default" 按钮,「没有重置入口」的担心不成立),所以增删条目 / 样式 / 交互 / 重置按钮这一批不再计入待办 面板按钮(#display-settings-switch,调色盘图标)出现在导航栏、面板容器已渲染、每项都能生效并写入 localStorage;verify:ui 有对应断言;体积代价已实测(ARCH.md §13:+11.6 KB/页,低于上游注释估的 33KB) ◆ 已实现(面板改进已关闭,v1.45)
R-ID-08 关闭文章许可协议显示(发起人 2026-09-24 评审先导文章时提:"这个许可协议似乎没有啥用啊") 模板默认给每篇文章挂一个 CC BY-NC-SA 4.0 许可块(src/config/licenseConfig.ts 的 enable: true),侧栏「站点信息」里也有一行「许可」。个人笔记站不需要这个声明 → enable: false,并把侧栏那一行整项不渲染(不要留一个显示 None 的空行) 文章页不再出现「许可协议」字样与 creativecommons.org 链接;侧栏站点信息里既没有 CC 也没有 None;verify:ui 有对应断言(2 条)。想恢复:enable: true(name/url 仍是可用的 CC 值) ◆ 已实现
R-ID-09 分享块保留;域名为占位值时的临时状态(发起人 2026-09-24 评审时先问"这个二维码扫了也没啥用啊",随后澄清"我要分享块啊,只是后面提醒我添加域名而已") 分享块(文章分享卡片 + 保存海报(含二维码)+ 复制链接)保留开启(siteConfig.post.share: true)。问题不在组件:海报二维码与「复制链接」都取 Astro.url.href,而 site_url 还是占位域名 imiss.example.com(阻塞项 B-01)→ 域名到位前扫码/复制指向的是假域名。拿到域名后:① siteConfig.site_url 填真域名 ② 重建 ③ 把 verify:ui 里"站点 URL 用的是本站占位域名"那条断言翻成真值断言。执行方必须主动提醒发起人(已登记为 PROJECT_STATUS.md §1 第 6 条) 文章页有分享块 —— verify:ui 有一条**故意断言"存在"**的断言(防止以后被误删);域名到位后二维码与复制链接指向真域名 ◆ 已实现(域名到位后重建即可)
R-ID-10 上游主题遗留彻底清理(P4-8,v1.47) —— 发起人 2026-09-29 逐项批准删除清单:A 组(内容)全删 + 站上不留任何上游字样 + 那首上游示例歌一并删 上游示例文章早在 v1.20 就删完了(29 个文件),剩下三类遗留:① 内容类 —— /projects/ 两个示例项目(两个 .md + 它们引用的 images/*.avif 封面)、src/content/dynamic/ 4 条上游演示动态、booknavConfig.ts 三张上游卡片(上游主题仓库 / 上游文档站 / 模板作者个人站,含上游图标 /favicon/ 下那张同名 png)、galleryConfig.ts 两个示例相册(上游示例相册「可爱流萤」、示例相册「加密相册示例」,后者的密码 123456 明文写在 desc 里)+ public/gallery/ 下对应两个目录(配置与目录必须成对增删)、dynamicConfig.memos.parent 的上游 memos 用户名;② 署名类 —— 导航「链接」整组(两条上游外链)、页脚 Powered by Astro & <上游主题名>(去掉主题名、保留 Astro)、侧栏「站点信息」两处硬编码 <上游主题名> v${blogVersion}(去链接与品牌名,保留中性版本号,不留空格子);③ 死重量与孤儿 —— public/pio/**(约 15 MiB 上游看板娘素材,两个开关都是 false)、src/assets/images/logo/<上游同名>-{light,dark}.png、src/assets/images/avatar.avif(src/ 内零引用)、public/favicon/<上游同名>-{32,128,180,192}.png;④ 访客看不见但内容敏感 —— friendsConfig.ts + spec/friends.mdx(模板作者站点身份 / 实名 / 邮箱 / 上游头像)、sponsorConfig.ts + 两张模板作者收款码(ko-fi 与爱发电收款页、实名打赏名单);⑤ 音乐 —— 本地歌单唯一一首上游示例曲(《崩坏:星穹铁道》「使一颗心免于哀伤-哼唱」)与其封面,并同步关掉导航/侧栏两个播放器入口(歌单空了还开着入口会留一个点开没内容的空播放器) ① 上游身份零残留:dist/**/*.html 全量扫(品牌串大小写敏感 + 上游主题名/作者名大小写不敏感 + 可见文本三层判据)零命中,v1.51 起零例外(v1.47~v1.50 期间唯一被排除的 about/index.html 已随关于页换成中立版连例外一起删除,同轮新增"搜索索引里也搜不到上游字样"4 条断言);② /projects/ 与 /gallery/ 仍 200 且渲染可见空态文案、不出现任何已删条目名,四条详情路由真 404;③ 导航无「链接」组且无空分组容器、页脚只留 Astro、侧栏站点信息无上游字样且中性版本行仍在;④ 音乐入口与组件都不再渲染、页面无音频请求;⑤ /friends/ 与 /sponsor/ 真 404(页面文件已删,不再只是 noindex 存根)、不在 sitemap;⑥ LICENSE(MIT)原地保留 —— 对上游的署名义务由它承担,页脚署名不是法律要求(详见 ARCH.md §14.3);⑦ v1.48 收尾轮(清上轮点名、但当时不在批准清单里的孤儿 + meting 归属):src/types/{friends,sponsor}Config.ts、src/styles/pages/{friends,sponsor}.css、navbar-i18n.ts 的 友链/打赏 两条死映射、Live2DWidget.astro 里的上游署名链接全部清掉;github-card-data.json 里那条上游条目v1.48 当时保留不删(它有活消费者 about.md 的 ::github{},且该文件是构建期生成物 —— 联网构建会把条目自动写回,实测两次构建的来回过程见 PROJECT_STATUS.md §7.1)——v1.51 已彻底关闭:关于页换成中立版后全站再无 ::github{} 指令,生成器两次重建实测 [GITHUB-CARD] Cached 0 repositories (0 refreshed).、文件落成 {}、git status --short 两次都干净,构建不再写回(该盲区已在 §7.1 标关闭);musicConfig.meting.id 清空(上游歌单 10046455237,发起人 2026-09-29 确认不是自己的;mode 仍是 local、两个播放器开关仍是 false)。断言条数仍是 259(只把 musicConfig 那条就地换成新口径)、构建仍是 165 页。⑧ v1.49 末轮(P4-8 最后一次代码清尾):产物层上游字样清到零 —— 8 处客户端标识符/自定义事件名/CSS 关键帧/组件文件名/package.json name 统一改成中性前缀 imiss(旧名 → 新名的逐条映射见 v1.49 那次提交的信息与 ARCH.md §11)(含 Layout.astro:567 那条会被 Astro 打进全站 162 个 HTML 的模板注释,陷阱 T85 形态),public/assets/css/twikoo-custom.css 第 4 行那条随产物发布的上游署名删除(它既不进 HTML、页面上也看不见 —— 上一轮就是这么漏的),commentConfig.ts 五处上游演示端点清空(见 R-ID-05);零残留断言的扫描面从 dist/**/*.html 扩到 dist/** 全部 799 个文件 + 文件名/路径,二进制按"含 NUL 字节"跳过内容但照查文件名,断言 259 → 260(新增「产物文件名/路径不含上游字样」1 条),about/index.html 例外不变、写进通过文案;验收:构建 165 页、改后产物命中仅 about/index.html、verify:ui 260 项(✅259/❌0/⏭️1 = R-AGT-08 后端文章库不可用;同一产物复跑一轮 ✅260/❌0/⏭️0)、verify:posts 133 篇 0 错误、verify:docs 15/15、check 0 error、type-check 退出码 0 ★

3.2 导航与信息架构(R-NAV)

编号 需求 详细说明 验收条件 优先级
R-NAV-01 「社交」改为「留言」并移到最后 【v1.83:那一格从「留言」换成顶层「关于」下拉,见 R-NAV-04】 删除「社交」父项;新增顶层「留言」项,位置在「链接」之后(即最后一个顶层项)。⚠️ v1.47 / P4-8:「链接」整组(两条上游主题署名外链)已按 R-ID-10 删除,那一轮「留言」曾是最后一个顶层项。v1.57 / B-10:发起人自己的 8 条把「链接」整组重建回来(见 R-NAV-09),所以顶层恢复成 主页 / 文章 / 学习路线 / 我的 / 链接 / 留言,「留言」仍是最后一格。v1.61:第 5 格下拉整批换成第二批 6 条(位置未动)。v1.83:最后那一格从「留言」换成「关于」 → 顶层 = 主页 / 文章 / 学习路线 / 我的 / 链接 / 关于(仍是 6 格) 桌面与移动菜单中,「关于」是最后一个顶层项、且它的下拉里有「关于我」「关于本站」;「链接」在它前面一格 ★
R-NAV-02 新增「排行」(放在「文章」与「我的」之间)【v1.41 被 R-PATH-01 取代,原排行子项与三个 rank 榜单地址随 P2R 一并作废】 这一格现在是**「学习路线」单链接** /roadmap/(无子项)——判据见 R-PATH-01;原先那句"顶层下拉,子项顺序为:日漫排行、国漫排行、二游排行"已作废 导航顺序为 主页/文章/学习路线/我的/链接/关于(v1.83 起最后那格是「关于」;v1.57 / B-10:「链接」组已按发起人清单重建、v1.61 换成第二批 6 条,见 R-NAV-09);第 3 格可点击进入 /roadmap/ ★
R-NAV-03 「我的」下拉重构 ① 删除「哔哩哔哩」「番组计划」;② VNDB 改名为 轻小说(URL 与功能不变);③ AnimeList 改名为 番剧(URL 与功能不变);④ (历史)「关于我」曾移入该下拉 下拉顺序:动态、项目、相册、书签导航、轻小说、番剧(v1.83 起到此为止 —— 关于我 / 关于本站已搬去顶层「关于」,见 R-NAV-04);不存在哔哩哔哩/番组计划 ★
R-NAV-04 顶层「关于」下拉:v1.65 删除 → v1.83 建回来(当前态:带两个子项的下拉) v1.65(历史):删掉顶层「关于」父项、「打赏」随之彻底移除、「关于我」并入「我的」;同轮 /about/ 变成占位页、讲这个站的那篇整体搬到新增的 /about-site/,公告卡的「了解更多」改指 /about-site/。v1.83(发起人:「把留言改成关于,然后下拉条为我的中的关于我和关于本站,然后删掉我的中的关于我和关于本站」):把那一格(原「留言」)做成顶层「关于」下拉,子项就是 LinkPresets.About + LinkPresets.AboutSite(顺序不变),「我的」里那两项随之删除 顶层最后一格是「关于」,下拉恰好两项(关于我 → 关于本站);/about/ 与 /about-site/ 都可直接访问;公告卡内链接指向 /about-site/ 且不再指向 /about/;「我的」里没有这两项 ★
R-NAV-05 删除友链 删除导航入口;页面不再对外可达;不进 sitemap 与 llms.txt ① 导航、sitemap、llms.txt 里都没有友链;② v1.47 / P4-8 起直接访问 /friends/ 是「真 404」 —— 页面源文件 src/pages/friends.astro 与 src/config/friendsConfig.ts、src/content/spec/friends.mdx 都已删除(不再是 noindex 跳转存根,verify:ui 有断言同时要求 404 且"不像存根");③ 生产 Nginx 对这 4 条路径直接 return 404(ARCH.md §12.2),线上口径一致 ★
R-NAV-06 打赏彻底移除 ① 导航入口删除;② 打赏页不再对外可达;③ 文章页的打赏按钮同步消失;④ 首页横幅里的打赏入口也要删(这是审查时抓到的盲区:backgroundWallpaper.common.homeText.links 里曾挂着模板作者站点上的 /sponsor/);⑤ 不进 sitemap 与 llms.txt 文章页与首页都看不到任何打赏入口;v1.47 / P4-8 起 /sponsor/ 与 /friends/ 同口径:页面源文件已删 → 「真 404」(verify:ui 断言同时要求不是 noindex 存根);verify:ui 的上游身份全站扫描对首页/文章页断言为零命中 ★
R-NAV-07 页面开关矩阵生效 关闭的页面不进导航、不进 sitemap、不进 llms.txt;打开的页面(轻小说、番剧)能正常访问 逐页访问 + dist/sitemap-*.xml 检查 ★
R-NAV-08 页内锚点必须由浏览器原生处理:写 hash、写历史记录,后退回得来(v1.46 新增,关闭 B-16) 静态产物普查 170 页里 134 页含页内锚点、<a href="#…"> 共 16 572 个。修复前:点目录项 / 标题锚点后 URL 会带上 hash、也会滚到位,但 Swup 4.9.2 对"同页锚点"走自己的 link:anchor 分支、里面调的是 history.replaceState()(不是 push)→ history.length 不变,按一次「后退」直接离开该页(文章页退到上一份文档),读者回不到原来的位置。修法:astro.config.mjs 给 swup 加 ignore: ['a[href^="#"]:not([href="#"])'](@swup/astro 支持的入口,会序列化进 ignoreVisit),并去掉 TOCManager 自己那个 preventDefault() + 手动滚动的点击处理器 —— 页内锚点整条路交回浏览器。⚠️ 裸 href="#" 占位链接(邮箱按钮 / 禁用分页)必须保持在豁免之外:它们被豁免后会变成原生 # 跳转(URL 尾巴多一个 #、白多一条历史记录) ① 全站同文档锚点全部是 #frag 形态(选择器覆盖不到的同页锚点 = 0);② 点目录项 / 标题锚点:location.hash = 目标 id、history.length +1、标题落在 scroll-margin-top 位置;③ 紧随其后按「后退」:location.pathname 不变、hash 清空、不重渲染整页(容器首个子节点还是同一个、Swup visit:start 计数 0);④ 全程没有整页导航(JS 上下文未重建);⑤ /roadmap/ 那 8 个 data-no-swup 锚点仍走原生且仍进历史;⑥ 裸 href="#" 链接不被变成原生 # 跳转;⑦ 390px 悬浮目录点一条同样进历史,且浮层不被误关 ★
R-NAV-09 「链接」下拉用发起人自己的清单重建(v1.57 新增,关闭 B-10 的"链接清单"那一半;v1.61 整批换成第二批;v1.122 换掉其中的 GitHub 号;v1.123 末两条并成一条) v1.61(发起人 2026-10-07 原话「把下拉链接换成接下来我给你的链接」):整批换成 6 条,顺序即他给的顺序、子项名字取短名 —— Gitee gitee.com/a2186988646 / GitHub github.com/HandlerOne01(⚠️ v1.122 起;v1.61~v1.121 期间是旧号 github.com/a13824367931-alt,发起人 2026-10-12「我的 github 链接变了」→ 换成新号,同一轮同步改了侧栏资料卡与 verify:ui 的期望值,三处逐字一致)/ CSDN blog.csdn.net/is_ko_nois_ko / 洛谷 luogu.com.cn/user/1726221 / Boost Search Engine http://106.52.158.48:8080/(v1.123 起:原来是「Boost 搜索引擎 :8081」+「HTTP 服务器 my_server」两条,发起人 2026-10-16「这个链接下拉把服务器和搜索引擎链接删了,添加一个 http://106.52.158.48:8080/ 这个链接 名字为 Boost Search Engine」—— 两半合成一个服务、端口 8081 → 8080 → 6 条变 5 条,同一轮在项目集合里加了合并版项目 boost-search-engine-merged) 。两条照办的细节:① CSDN 去掉跟踪参数 —— 他给的原始 URL 尾巴是 ?spm=1000.2115.3001.10640,按"URL 一律换成干净地址"的既有口径只留干净地址;② 裸 IP 那条照原样收录 —— http://106.52.158.48:8080/ 是裸 IP + 非标准端口 + HTTP(该 IP 同时是本站后端数据库所在的机器),这是发起人的站、他的选择,配置里原样保留并在注释里写明代价(公开本站服务器地址、浏览器标「不安全」)与后续建议(有域名后换 https://<域名>/),不许擅自改 URL、也不许删这一条。位置仍固定在「我的」与最后那一格之间(v1.83 起最后那格是「关于」)→ 顶层仍 6 格。5 条全部 external: true、都不挂 pageKey(挂了会被 siteConfig.pages 过滤,整组可能一声不响地消失)、都不带查询串。⚠️ v1.57 那 8 条(DeepSeek / ChatGPT / Claude / GitHub / 洛谷 / 力扣 / CSDN / Hugging Face)里只有 GitHub / 洛谷 / CSDN 三个名字留下、URL 全换,另 5 条随整批替换退场 桌面下拉与移动抽屉里都是这 5 条、顺序一致、href 逐字等于上面 5 个地址;顶层导航顺序 = 主页/文章/学习路线/我的/链接/关于;导航里不出现任何上游署名项(verify:ui 有 5 条断言钉住:名字 + href + 顺序 / 条数 5 / external 与无 pageKey / 地址干净 + 裸 IP 那条原样在 + 注释里有 https 建议 / 组内无上游字样;两轮都做过注入验证:临时删掉组内一条 → 顺序断言当场判红) ★
R-NAV-10 「链接」清单里不能夹带隐私串 5 条一律是干净地址:不带查询串、不带追溯参数(v1.61 照办:CSDN 的 ?spm=… 已去掉)。⚠️ 唯一的例外是发起人点名要保留的那条 —— 「Boost Search Engine」是裸 IP + 非标准端口 + HTTP,按原样收录(见 R-NAV-09),它不是"漏抹的隐私串"而是发起人的选择;隐私守卫(R-BOOKNAV-04)的 14 个串里也没有这个 IP 见 R-BOOKNAV-04 的隐私守卫(同一条规则对导航与书签页一起生效);verify:ui 另有一条配置层断言钉住"5 条地址都无查询串" ★

3.2B 书签导航(R-BOOKNAV,v1.57 新增)

为什么单开一节:/booknav/ 从 v1.47 起就没有任何界面断言(全仓库 grep -n booknav scripts/*.mjs 只命中导航子项表与 ALLOWED_ROUTES),而 v1.57 把这一页的内容整批换成了发起人最终选定的 6 组 38 张 —— 内容量翻了几倍、还引入了"要不要继续走第三方 favicon 接口"这个隐私取舍,所以补上判据本体与验收口径。

编号 需求 详细说明 验收条件 优先级
R-BOOKNAV-01 分组与优先级:AI 工具优先 … 最后才是其他三方站 6 组,weight 决定渲染顺序(getEnabledBooknavGroups() 按 weight 降序排序):AI 工具 100(6 张)→ AI 平台与模型 90(10 张)→ 学习与刷题 80(5 张)→ 开发与文档 70(9 张)→ 云服务与账号 60(4 张)→ 其他三方服务 50(5 张)。⚠️ "其他三方服务"必须排在最后(发起人明确要求:API 订阅类站点放末尾)。每组给一个 iconify 组图标;不留空组(getEnabledBooknavGroups() 会把 items 为 0 的组丢掉) 页面分区顺序与上表一致;胶囊锚点 id 与分区一一对应;不存在"有标题、0 张卡"的空 section;390 视口下同为 6 组 ★
R-BOOKNAV-02 39 张卡逐条等于发起人清单,URL 一律用首页 39 条没有任何完全重复(v1.88 加的是「MiniMax 媒体平台」,落在「AI 平台与模型」组最后一位);同品牌两条(DeepSeek 对话 / 开放平台、LangChain 中文文档 / 官方文档)按不同用途各留一张,不合并。发起人原始 URL 里的查询串/追踪参数/实例 ID 在"换成首页"这一步顺带抹掉。组内 weight 10 递减 verify:ui 断言 39 张卡的标题与 URL 逐条等于清单(含顺序)——只数个数不够,换掉一张卡仍是 39 张;另断言每组条数、胶囊角标、target="_blank" ★
R-BOOKNAV-03 favicon 自动获取关闭(v1.57 决定,附实测证据) booknavPageConfig.favicon.enabled 由 true 改 false。理由:开着时 resolveBooknavIcon() 会给每张没写 icon 的卡生成第三方 https://a.favicon.im/{域名},访客打开这一页就会把 18 个域名交给 favicon.im。实测依据(reports/_bn-favicon-probe.{mjs,json} + 两张截图):关闭后 38 格图标全部有内容(20 个手填 iconify svg + 18 个首字母块,<img> 0 个、无破图、卡片高度整齐 72px);开着时 18 张走第三方、其中 8 张实测加载失败(brokenImgs: 8)→ 关闭不但更私密,观感也更稳。api 值保留在配置里,改一个布尔即可回退 页面上 0 个指向第三方 favicon 接口的图标请求;39 格图标无一空白;卡片高度一致 ◆
R-BOOKNAV-04 隐私与排除项泄漏守卫(产物级) 断言产物里绝不出现:腾讯云轻量实例 ID lhins-r4o6dc9v、Docker 用户名 handler01、LangSmith 组织 ID 900dd689-c116-4ccf-911f-78494a0db890、被排除的站点/地址 dlsite / theporndude / 139.159.190.22 / wzyp.cn、27届小比特-实习-资料汇总,以及原始 URL 形态 i.chaoxing.com/base?t= / webhp? / /u/1/app? / activeTab=readme / /thankyou.php? / searchParams= —— 共 14 个串,扫 dist/** 全集 + 文件名/路径,失败时打出命中的文件与串。⚠️ happy.bigmess.org 不在守卫名单里:它是发起人明确要收录的站点(BigME.Pro,「其他三方服务」组),另有一条正控断言要求它仍在产物里 14 个串零命中;正控(happy.bigmess.org / chat.deepseek.com / huggingface.co)都在产物里;注入一条带 ?id=lhins-r4o6dc9v 的 URL 后守卫当场判红 ★
R-BOOKNAV-05 每张卡的图标都必须是「真图标」(v1.58,发起人:「标签怎么没有图标啊」) 39 张卡的图标形态只有两种允许:iconify 官方矢量(<svg>,27 张)或各站官方 favicon 的本地副本(<img>,11 张)。首字母块(.booknav-letter)只允许出现在「已点名的例外」上 —— 当前例外恰好 1 条:BigME.Pro(happy.bigmess.org)整站 503(chrome / curl / Googlebot 三种 UA、http/https、www/裸域全部 503;bigmess.org / bigme.pro 裸域 DNS 直接失败),取不到任何官方图形;一次性兜底接口给的是通用地球占位图(不是品牌图标),已按归一化后的像素 sha256 识别并丢弃(scripts/fetch-booknav-icons.mjs 的 PLACEHOLDER_SHA256)。⚠️ 这个守卫是必须的:不加它那张卡会显示"看着有图标、其实是地球",而本条断言会因为它是 <img> 而放行。图标是装饰性的:每个 iconify <svg> 都要 aria-hidden="true",卡片标题才是无障碍名 字母块数量 ≤ 例外条数(实名核对),且失败明细里逐张列出退化的是哪几张卡;字母块条数恰好等于例外名单长度;27 个 <svg> 全部带 aria-hidden="true";注入 icon: "" 后本条当场判红(实测 这些卡退化成字母块且不在例外名单里:deepseek) ★
R-BOOKNAV-06 每个图标都要有颜色,且是各站自己的品牌色(v1.58,发起人:「就算有图标但还是没颜色啊」) 根因是原实现把颜色写死成 text-neutral-700 dark:text-neutral-200(灰/白)。现在 BooknavItem 新增可选 iconColor(官方品牌色,不是"最终渲染色");品牌色表是单一事实来源 src/constants/booknav-brand-colors.ts,配置里写 iconColor: brandHex("<品牌键>")(键名打错就抛错,不静默兜底灰 —— 静默兜底灰会让那张卡悄悄变回旧的灰色而没人发现)。27 张 iconify 卡各有品牌色;11 张本地 favicon 是位图、天然彩色,不写 iconColor;BigME.Pro 的字母块也带品牌色(#7C3AED,显式占位:站点不可达、品牌色不可考)。⚠️ 同品牌两条(DeepSeek / DeepSeek 开放平台、ChatGPT / OpenAI Platform、两条 LangChain)色值相同但键各自独立 —— 断言是按卡片标题索引品牌色逐张核对的,共用一个键会让另一条卡静默漏掉核对(实测:26 张只核对了 24 张却照样显示通过)。Chat SDK 无公开品牌规范,#6366F1 同为显式占位 每个有图形元素的图标的计算色 ≠ 旧的中性灰/白(按解析后的 RGB 数值比——text-neutral-700 在亮色下是 rgb(64,64,64)、在深色下是 oklch(0.371 0 none),同一颜色两种字符串形态,比字符串会漏);同一页面里不同品牌卡的渲染色不全相同;27 张 iconify 卡的亮色渲染色逐张等于配置里的品牌色(本轮实测逐张核对 27 张 —— ⚠️ 这必须等于 svg 张数 27:v1.88 第一版把品牌键写成短名 MiniMax,而这条判据是按卡片标题索引品牌表的,短名会让那张卡静默漏掉核对(27 张只核了 26 张);键名改成与标题逐字一致后才是 27);注入"去掉某条的 iconColor"后本条当场判红 ★
R-BOOKNAV-07 深色主题下的图标可辨识度(v1.58) 深色卡片上 #181717(GitHub)/ #1C3C3C(LangChain)/ #0052D9(腾讯位置服务)这类深色看不见。做法是主题色感知的,不是"整体提亮到发白":亮色原样用品牌色;深色下 color-mix(in oklab, <品牌色> 55%, white) —— 保留色相、只抬亮度,23 个配置色 / 24 种渲染色零逐色特例。⚠️ 离线模型曾算出 GitHub 在 55% 下只有 2.31:1、据此配过一个 #8B949E 深色特例,真浏览器实测是 3.24:1(Chrome 走 premultiplied oklab 插值),于是把特例删掉了;BrandColorEntry.dark 字段保留为显式逃生口,当前一个都没启用。⚠️ 字母块那层底色在深色下用 8%(亮色 14%)——深色下底色品牌色本身就是浅紫,16% 会把底抬到 rgb(61,58,76)、对比度掉到 2.61:1 深色下每个图标对它自己那层图标块底色的对比度 ≥ 3:1(实测 28 个:最低 3.24:1 / 最高 12.23:1);且深色下图标色确实被提亮(不是照搬亮色值),前置断言 html.dark === true 防止"深色那一趟量的还是亮色值" ★
R-BOOKNAV-08 运行期零第三方请求(v1.58,延续 R-BOOKNAV-03 的口径并收紧) 11 张本地图标一律写站内路径 /booknav-icons/<slug>.png,不许写网络图片 URL。素材由 scripts/fetch-booknav-icons.mjs 在构建前的准备阶段一次性抓取(顺序:官网 favicon.ico → 页面 <link rel="icon"> → 一次性兜底),用 sharp 归一化成 128×128 PNG(1.5~12.7 KB);那次兜底只出现在脚本里,产物里不许出现该服务。⚠️ 上一轮只数了 favicon.im 一家,换一家服务就漏 → 现在改成封闭判据:所有图标 <img> 的 src 必须全部以 /booknav-icons/ 开头,任何 http(s):// 或协议相对 URL 都判红;再加一条产物级守卫(dist/** 内容里不出现 s2/favicons / favicon.im / faviconV2 / googleapis.com,且"产物没被扫到"时判红而不是放过) 11 个图标 <img> 全部站内;产物全集零命中;favicon.enabled 仍为 false(api 值保留可一键回退) ★
编号 需求 详细说明 验收条件 优先级
R-WDG-01 站点统计精简 删除「运行时长」「最后活动」两项;保留:文章数、分类数、标签数、总字数 站点统计组件只显示 4 行 ★
R-WDG-02 日历保留、热力图关闭 日历组件保留,关闭年度热力图 侧边栏日历只有月历,没有热力格子 ★
R-WDG-03 站点信息占位 → 真值回填 【未部署期】 组件保留;依赖构建/部署环境的值(构建平台、系统/架构、构建时间、CI 等)统一显示 —,并显示一行「部署后自动填充」提示;本地配置值(文章许可)正常显示。【v1.117 起,站点已上线】占位模式两处开关全部关闭,改为显示真值:站点域名 = imissblog.cn、Astro / Node / pnpm 版本与构建时间取构建期事实;构建平台与系统信息改为配置显式给值(sidebarConfig.siteInfo.buildPlatform / systemInfo,本站 = 腾讯云 · Nginx / Linux / x86_64)——因为自动识别只会得到 Local 与构建机的 Windows / x86_64,而这一格问的是站点跑在哪 未部署时组件不显示假数据;已部署:占位提示与「—」都不许再出现,且域名与构建平台必须真回填(verify:ui R-WDG-03 钉住,期望值从配置现场读);换域名/换机器期间可临时把开关改回 true ★(回填 ◆ → v1.117 已回填 ✅)
R-WDG-04 侧边栏组件清单确认 【假设】 保留:用户资料、公告、分类、标签(左);最新动态、站点统计、站点信息、日历、目录(右)。广告位保持关闭。⚠️ v1.47 / P4-8:音乐播放器曾从清单移出(历史) —— 本地歌单唯一一首是上游示例曲(见 R-ID-10 ⑤),歌单清空后两个入口开关(showInNavbar / showInSidebar)都置 false,侧栏那一格不再渲染(sidebarConfig 里的 type: "music" 登记一直保留,就是为"发起人加回自己的歌再打开"留的路)。✅ v1.63 已恢复(当前态):发起人 2026-10-07 交来自己的《别难过》并选"两个都开" → showInNavbar / showInSidebar 均 true、local.playlist 恰好 1 条(mp3 与封面原样入库、未转码),音乐播放器重新进入左栏清单(position: "top",排在用户资料、公告之后),导航栏同时也多一个音乐入口按钮 侧边栏实际渲染出的组件与上述清单完全一致(桌面双侧栏 + 移动端底部各看一遍);音乐那一格渲染出的是"有 1 首歌的播放器"(点开有内容),不是空组件框 ◆
R-WDG-05 资料卡签名与社交链接(发起人亲自给定) 【已实现】(v1.98) 发起人 2026-10-10 圈着侧栏资料卡那张截图说:「B-02 签名换成 おかえり,社交链接就是我链接下拉条的前四个」。落地两处:① profileConfig.bio = 「おかえり」(一字未改);② profileConfig.links = 导航「链接」下拉的前 4 条(顺序就是他给的顺序:Gitee → GitHub → CSDN → 洛谷;后两条 Boost 搜索引擎 / HTTP 服务器不进资料卡),只显示图标不显示名字(4 个 40×40 按钮正好一行,名字留在 aria-label 里)。⚠️ 两条副作用如实登记:① 这 4 条同时进 JSON-LD 的 Person.sameAs(filterAbsoluteLinks());② bio 同时是 Person.description(about 页与每篇文章的 application/ld+json,见 src/utils/schema-utils.ts)→ 结构化数据里作者的 description 现在就是「おかえり」,发起人的选择,保留。⚠️ profileConfig.name(IMiss)仍是占位,B-02 只剩这一小块 ✅ v1.107 关闭:发起人给定作者名「HandlerOne」→ profileConfig.name = HandlerOne(站点名 IMiss Blog 不改),about-site.md 开篇那句同步,B-02 整条关闭 verify:ui 的 R-WDG-05 一条,两条腿缺一不可:① DOM(取 facts["home-1440"].profileCard —— 断言只认桌面那一趟;⚠️ 实测更正:390 下也采得到,移动端这张卡同样是展开的:376px 宽、按钮 35×35,不是 0×0)签名逐字 =「おかえり」、a[rel=me] 恰好 4 个(数量也是判据:模板对 length == 1 走另一支带名字的宽按钮)、href / aria-label 逐字正确、每个按钮都落在卡片内不溢出、只含图标不含文字;② 配置层交叉核对:profileConfig.links 逐字等于 navBarConfig.ts「链接」前 4 条(单一事实来源在导航那份,资料卡是副本 —— 导航一改这条就红,提醒同步) ★ ✅

3.4 背景与壁纸(R-BG)

编号 需求 详细说明 验收条件 优先级
R-BG-01 图片期壁纸 【已实现】 用 dosc/photo 的 4 张图(scripts/convert-wallpaper.mjs 转 webp,不预裁)替换示例壁纸;桌面与移动都用这批;标注为临时占位(源图 1915×821,2560 宽屏会放大 1.34 倍,正式方案是视频);上游示例图 d1~d6/m1~m6 已从仓库删除。画质要求(发起人 2026-09-24 追加):原图直接入库、不允许执行方再压一道(构建管线只压一次,壁纸单独用 quality 95);横幅高度要让画面基本完整可见(现在的 80vh 在 1918×966 下可见 95% 高 × 100% 宽,改之前只有 76% 高);取景锚点用 50% 35%(居中偏上),不用 0%(那会只从一侧裁) 首页与其他页背景都能看到新图;图片不糊(长边 ≥ 1920 的源) ★
R-BG-02 视频期动态壁纸 【已实现】(v1.70,发起人 2026-10-09 交来 5 支原画;v1.105 增到 6 支、v1.106「起床」整支换成流畅循环版) ① 原片直出、不转码不压缩(发起人原话:「不可压缩我的动态壁纸大小和画质,这是原则,不能动,我要原画」)→ 6 支 mp4 按字节原样放 public/assets/wallpaper/(v1.106 起合计 69.00 MB:v1.80 单支换过 chuangbian.mp4、v1.105 换 xingye.mp4 并新增 qichuang.mp4、v1.106 把 qichuang.mp4 整支换成「起床_流畅循环版」——每次都只动他点名的那一支,其余各支一个字节没动),产物里核对字节数 + sha256;② 无缝循环:不另做"兜底版本"——实测 6 支的首尾帧差都 ≤ 它自己 1 秒的正常运动量(reports/_wp-seam-v1106.json;qichuang 走播放路径 reports/_wp-playdiag-v1106.mjs,因新版无法被 Chrome 深 seek),loop 硬切看不出接缝,所以没有交叉淡入(多一道处理反而伤画质);③ 播放速率就是原速率,不调速、不加过渡;④ 三种降级全部落地:系统偏好减少动态(prefers-reduced-motion: reduce)/ 省流量模式(navigator.connection.saveData)/ 页面切到后台(visibilitychange)——都只显示 首帧海报、页面不报错 视频循环处看不出硬切(断言:末帧 vs 首帧的差异 ≤ 画面自己 1 秒的变化量);三种降级下都不播、只显示海报且 data-play-reason 如实写出原因 ◆ 已实现(v1.70)
R-BG-03 主页横幅文案 【已实现】(v1.77:副标题由发起人亲自给定) 主标题、副标题、链接图标改成爱弥斯主题。v1.77 落地:发起人当天发来横幅那行小字的截图说「对这些小字文本进行替换,替换文本为下面这些」,给了 5 句 —— 「但愿我会让你感到骄傲,但愿我没有让你失望 / 旅途愉快~ / 我会一直在你所走的路上,和你共同前行 / 一起去拯救世界吧! / 希望现在的我,已经成长到能和你并肩而行了」,一字未改照抄(含"旅途愉快~"的波浪号与叹号)、打字机轮播(12 秒内轮到 3/5 句,一轮 ≈20 秒);改前那三句是模板占位(爱弥斯的小站 / 技术笔记 · 学习路线 / 还有一只会聊天的爱弥斯)。条数不变(本轮只换文案),手机 390 副标题 344px 不溢出、窄屏字号 14px(桌面 24px)。⚠️ 横幅的链接图标行仍是关闭的:那要发起人给图标 + 地址才开,属另一件事(不阻塞本条) 首页横幅文字为主题文案;链接图标可用 ◆ 已实现(v1.77 文案)
R-BG-05 手动换壁纸(隐藏交互区 + 拖动 + 点击)【已实现,v1.74 起三种壁纸模式都支持】 v1.74(发起人:「全屏壁纸也要支持双端左右滑动壁纸切换和电脑端点击切换。透明覆盖同理」):可切换模式从"只有 banner"扩到 banner / fullscreen / overlay;拖动监听从 #wallpaper-wrapper 改到 document 级(透明覆盖下壁纸层在 z-index:-1,指针根本到不了 wrapper —— 原来那种写法在这一模式下等于没绑);新增点击空白换下一张(manualSwitch.click,只在 fullscreen / overlay 生效:banner 已经有两侧箭头,再抢点击容易误触);两侧箭头在全屏壁纸下也渲染。横幅左右两侧各一个隐藏交互区,点击切到下一张 / 上一张壁纸(多张时循环)。发起人原话:「我希望这两边有隐藏的交互键,点击之后可以跳转到下一张图片」。要求:① 默认不可见(不干扰画面);② 不能只靠鼠标——要可 Tab 聚焦、有 aria-label,也要能接受键盘操作;③ 由配置开关控制,且没有可用操作时(只有 1 张图 / 非 banner 模式)自动不渲染 首页横幅左右各能点到一次换图;键盘 Tab 能聚焦到这两个交互区并触发;只有 1 张图时页面上不出现它们 —— 已实现并实测:verify:ui 有 9 条断言(点左/点右/连点回归/键盘 Enter/鼠标拖拽/手机端不显示/手机滑动/默认不可见 hover 才显形/aria+可聚焦) ★ ✅
R-BG-06 按北京时间默认展示哪一张 【✅ 已实现(v1.105)】 发起人预告(2026-09-24 / 2026-10-09 又提一次):「依据北京时间的不同会初始默认展示对应的视频」「进行背景时间的壁纸展示顺序排序」。v1.70 先把入口留好(有序清单 video.playlist、video.order 的 first / random / sequence / time 四档、每项一个 timeRange 字段,time 的匹配逻辑故意没写 —— 时段怎么分、哪支配哪个时段都得他定);2026-10-10 他把六个时段亲口排了出来,v1.105 落地:壁纸增到 6 支(新增「起床」、替换「星夜」),video.order 改成 "time",每项挂 timeRange —— 23:00–03:00 星夜|03:00–07:00 起床|07:00–11:00 粉发少女洗手台|11:00–15:00 湖畔少女|15:00–19:00 窗边少女|19:00–23:00 塔罗牌。口径三条:区间左闭右开(03:00 整点算后者)、start > end 视为跨零点、时刻一律按北京时间(UTC+8)且不看访客本机时区;判定在客户端做(访客打开那一刻的北京时间 —— 静态站没有服务端,构建期注入只能定"构建时"那一刻)。实现见 WallpaperSection.astro 的 pickByTime();判据就地扩进 verify-ui 的 R-BG-02 ① ②(六档铺满 24 小时不重不漏 + 12 个取样点 + 边界归属 + 三时区一致) ✅ 已实现(v1.105 落地)
R-BG-04 壁纸取景可调 【已实现】(v1.70 起静态图那套退役,改由 R-BG-07 管视频取景) 暗罩(common.dimOpacity,当前 0.2)仍在配置里并有断言;横幅 banner.position 那条字面量断言已下线——视频的取景是按当前框反算的(见 R-BG-07),一个写死的百分比不可能同时对上四种框 暗罩 alpha 与配置一致;视频取景由 R-BG-07 的三条断言钉住 ◆ 已实现(口径见 R-BG-07)
R-BG-07 动态壁纸的取景与裁切:能不裁就不裁,要裁时人物在展示中心;全屏完整展示 【已实现】(v1.70 新增,发起人 2026-10-09) 发起人原话:「动态壁纸的区域还是参考 firefly.cuteleaf.cn,他这个视角方框大小还是不错的。我要的是壁纸能不裁就不裁的原则,但是如果要画面裁剪时如横幅壁纸不得不剪掉某些区域,那也请保证动态壁纸的人物在展示的中心(这个特别是手机端,脸是肯定要全部漏出来的),全屏壁纸的话就展示完整的动态壁纸」。素材是 2944×1248(2.36:1)超宽,而站点的框有四种宽高比(手机横幅 65vh → 390×549 = 0.71:1、桌面首页横幅 80vh → 1440×720 = 2:1、非首页横幅 55vh → 2.9:1、全屏 1.6:1),一个写死的 object-position 不可能同时对上,所以:① 每一项自带 focus(人物锚点)+ focusAnchorY。⚠️ 只改了发起人点名的那一支(窗边少女):v1.75 我一度按实测把五支全改了,他明确要求「其他图片位置不动不行吗?只调整一张图片不行吗?为啥要全部调整?给我弄回来」→ v1.76 已还原另外四支。窗边少女的 focus 由估值 41/36 改成实测 50/33(在带百分刻度的网格叠加帧 reports/_wp-grid/*.png 上读两眼中心、+0.07 取脸中心;全屏壁纸的可见窗口只有原画 20% 宽,估值 9% 的偏差就等于把脸推出画面)。另外四支的实测值一并备查(等他点名再改):星夜 24/34、湖畔 41/31、洗手台 18/44、塔罗 40/22。自检 node reports/_mobile-selfcheck.mjs:5 支 × 横幅/全屏 = 10 组,判据是"人脸落在可见窗口内(左右各留 ≥6% 余量)";② 页面脚本按当前框的宽高比反算 object-position,把锚点顶到框里该在的位置,并夹回画面内。落点可逐支调:focusAnchorX(水平,默认 0.5)与 focusAnchorY(垂直,默认 0.45),另加 zoom(默认 1)—— ⚠️ 手机端整幅高度本来就全可见(高度是约束),垂直方向没有可平移的余量,所以"让人物在框里下沉"只能靠 zoom 放大(人物在框里的高度占比 ≈ focus.y × zoom),代价是可见宽度同比例变窄。发起人 2026-10-09 对着手机截图给了两轮微调:第一次三条(「第一张左移一点,再下拉一点」「第二张左移一点点,下拉一点点」「第三张左移一点」),看完图后又改口两条(「第一张人物居中」「第二张人物偏左」)→ 最终 湖畔 44%/zoom 1.15、洗手台 38%/zoom 1.06、窗边 50%(正中);③ 横幅 cover(必然要裁),全屏也 cover(铺满整屏) —— ⚠️ v1.72 更正:v1.70 我把"全屏壁纸的话就展示完整的动态壁纸"理解成 contain(一个像素都不裁、留白用同帧模糊放大版垫底),结果手机上 390×844 的竖屏装 2.36:1 的横片,画面缩在中间一条、上下两大块垫底,发起人截图指出「全屏不是这个意思,和 firefly 一样」→ 参照对象 firefly.cuteleaf.cn 的壁纸容器就是 object-fit: cover(实测其站点 HTML),所以"完整"该按"整块屏幕都归壁纸"理解。contain 那条代码路径保留(配置切回去即可);④ 断言从 DOM 反推"锚点在可见窗口里的偏移",不读脚本自述 手机 390×844 下横向只剩三成,人物(脸)仍在可见窗口中轴 ±8%;全屏可见源区 = 原生分辨率;四种框都没有把脸切掉 ◆ 已实现(v1.70)
R-BG-08 窄屏横幅高度与内容定位必须一致(修掉"横幅下面那块黑") 【已实现】(v1.71 新增,发起人 2026-10-09 指出) 发起人手机截图:横幅图下面是一大块黑,原话「不是叫你改成 firefly 样式吗?你看我的图片下面水波那里那么大块黑色区域」。量出来的真因:窄屏(≤1023px)把壁纸框压到 65vh,而内容定位用的 --banner-height-home 仍是 80vh ⇒ 内容面板被推到 80vh−3.5rem,横幅底边到内容顶边空出 77px 纯页面底色;那段再叠上水波(84px)与渐变(55px,两者都刷成页面底色),暗色主题下就是一整块黑板。也就是说"内容重叠横幅 3.5rem"这个设计在手机上从来没生效过(layout-base.css 那行注释写的还是旧值 65vh)。修法:新增常量 BANNER_HEIGHT_MOBILE = 65 并写成 CSS 变量,壁纸框高度(原来四处写死 65vh)与窄屏 --content-top 都读它 → 内容重新压在横幅底部 3.5rem 上,空档消失 390×844 实测:内容面板顶边从 626 → 500(横幅底边 549),重叠 49px;截图里横幅底部直接接住分类胶囊与文章卡片,没有空档 ◆ 已实现(v1.71)
R-BG-09 视频壁纸轮播(自动换片) 【已实现】(v1.118 新增,发起人 2026-10-12 定参数) 发起人问「控制盘的壁纸轮播时间是多少,还是根本没实现」→ 查清:配置里有 5000ms,但那是模板「图片幻灯片」那套的;v1.70 换成视频后容器里只有一个 <video>、零个 .slide-item,changeToSlide() 第一行 if (slides.length <= 1) return; 直接返回 ⇒ 面板那个「壁纸轮播」开关在视频模式下是个空开关(真机取证:16 秒里壁纸一支都没换)。发起人随即定参数:「60秒 默认关 都轮」,v1.119 又把间隔改口成「改成30s切换下一张吧」→ 间隔口径最终 = 30 秒;v1.121 再说「壁纸轮播默认开启吧」→ 默认口径最终 = 开(common.carousel.enable: true;访客仍可在面板里关掉,关掉后写 localStorage、以他的选择为准)。落地口径:开关复用面板那个(显示设置面板 →「壁纸」页签 →「壁纸轮播」);间隔 30 秒来自 video.carousel.interval(与图片那套的 5 秒分开,六支合计 98.9 MB,5 秒太凶);桌面与手机都轮(不做端判定)。三种情况不轮:document.hidden / prefers-reduced-motion / navigator.connection.saveData;只在 banner / 全屏 / 覆盖三种模式轮(纯色模式没有壁纸)。⚠️ 流量代价(默认开之后要盯的数字):视频响应头是 cache-control: public, max-age=2592000(30 天),同一支只会下一次;一个访客停留 3 分钟走完一轮 ≈ 98.9 MB(改前只下当令那一支 7.5~36.3 MB),回访基本命中缓存 verify:ui 的 R-BG-09 两条:① 默认开 —— 访客没动过开关时一进站表就自己在跑(清空 localStorage 后重新加载页面再判)、interval === 30000;② 访客拨「关」立刻停(start() 也绕不过去)、拨回「开」按清单顺序前进并绕回第 1 支、手动换片只重置一次计时器(starts +1 不叠加)。另有真机探针 reports/_ops/114-video-carousel-verify.mjs(走访客路径真点面板按钮,8/8) ★ 已实现(v1.118 落地 / v1.119 间隔 60 → 30 秒 / v1.121 默认关 → 默认开)

3.5 排行榜(R-RANK)

编号 需求 详细说明 验收条件 优先级
R-RANK-01 排行页与导航 【已废弃 v1.40 移除,改由 R-PATH 承担】 新增 /rank/jp/(日漫排行)与 /rank/cn/(国漫排行),/rank/ 重定向到 /rank/jp/;两个有维度的榜共用同一套榜单组件(另有 /rank/game/ 二游排行,见 R-RANK-07) 页面都能访问,导航子项高亮正确 ★
R-RANK-02 全剧 / 单集切换 【已废弃 v1.40 移除,改由 R-PATH 承担】 页面右上角提供「全剧 / 单集」切换,分别对应两份榜单数据;状态写进 URL(`?kind=all ep,默认 all`),可直接分享链接、浏览器后退能回到上一个榜单 手动把 URL 改成 ?kind=ep 打开,页面直接显示单集榜;切换后 URL 同步
R-RANK-03 榜单板视觉(对齐参考图) 【已废弃 v1.40 移除,改由 R-PATH 承担】 单列榜单板,4 组实测规格:① 名次列宽 = 内容宽的 13.7%,行高 = 内容宽的 1/5.7(量自参考图 551×992:名次列 75px、行高 96px);比例要相对"内容宽"而不是视口宽,实现用容器查询单位 cqw(见 ARCH.md §5.5);② 名次底色只有两档——13 名粉、4 名及以后橙 #EFA662(不是金银铜);参考图的粉 #EA7B7C 上白字只有 2.76:1(不达 WCAG AA 大字 3:1),实际取加深一档的 #E2696B(3.25:1);③ 数字颜色随档位变:13 名白字、4 名以后深色字(深字压橙底 9.29:1);④ 剧照整行铺满 + 标题区暗罩,白字粗体,长标题可折 2 行。右侧的得票率、名次升降、涨幅三列一律不做(参考图已被发起人裁掉右侧) 与裁切参考图观感一致:行高与名次列比例对得上、两档配色正确、数字颜色正确、两档数字对比度达标;页面不出现任何百分比/升降箭头 ★
R-RANK-04 数据手工维护、只有一期 【已废弃 v1.40 移除,改由 R-PATH 承担】 4 组数据(日漫×全剧、日漫×单集、国漫×全剧、国漫×单集)+ 二游的 1 份名单,都写在一个数据文件里,顺序即名次;不做期数、不做历史归档。数据还是示例内容时用 dataStatus: "placeholder" 在页面上显式标注 数据文件里改一条标题,页面立即跟着变 ★
R-RANK-05 缺图与空数据降级 【已废弃 v1.40 移除,改由 R-PATH 承担】 条目没有配图时显示主题色渐变占位(不出现裂图);占位必须是"不透明深色底 + 半透明主题色渐变",保证亮/暗两套主题下白字标题对比度 ≥ 4.5:1(半透明渐变直接罩在卡片底上时,亮色主题会糊成浅粉);cover 写了但文件不存在时按"没有配图"处理(不能让那一行变成白卡片 + 白字);某一组数据为空时显示"榜单待更新"文案而不是报错 删掉所有 cover 字段、或故意写一个不存在的路径,都要正常渲染;亮色与暗色两种主题下标题都清晰可读 ★
R-RANK-06 移动端可用 【已废弃 v1.40 移除,改由 R-PATH 承担】 390px 宽度下名次与标题都不溢出:行高按比例会压到约 68px,移动端放宽到能容纳 2 行标题(约 20cqw,相对榜宽),名次列保持约 13.7% 宽 390px 截图无横向溢出,标题最多 2 行且不压到名次列 ★
R-RANK-07 二游排行(单一榜,无维度)【已定】 【已废弃 v1.40 移除,改由 R-PATH 承担】 导航「排行」下拉里再加一项 二游排行 → /rank/game/。榜单板与视觉规格和另两榜完全一致,但没有「全剧 / 单集」切换:整页只有一份名单(数据在 rankConfig.game 里"直接给数组",而不是 { all, ep })。URL 上不出现 ?kind= 该页可访问且导航子项可进入;页面上看不到维度切换按钮;只有一块榜单面板;名次色/行高比例与其它榜一致 ★

3.5R 学习路线(R-PATH,v1.40 取代排行)

发起人 2026-09-27 决定:删掉「排行」模块,换成「学习路线」。原话是"没数据 / 不想凑那 50 张配图" —— 排行需要作者持续产出 5 份榜单数据 + 每名次一张 5.7:1 配图(阻塞项 B-08),而路线页的数据全部来自站内已有的 138 篇笔记。详细设计与文件清单见 ARCH.md §5。

编号 需求 详细说明 验收条件 优先级
R-PATH-01 学习路线页与导航 新增 /roadmap/(单页,8 条线用页内锚点,不拆子页);顶层导航第 3 格(原「排行」)改为「学习路线」→ /roadmap/,不带子项;「文章」下拉(归档/分类/标签/系列)不动 导航第 3 格是「学习路线」且能进入;页面上 8 条线都在;页内 8 个锚点都能跳到对应区块 ★ ✅ 已实现(v1.41)
R-PATH-02 数据与顺序:8 条线 × 阶段 唯一手工维护的数据文件是 src/config/roadmapConfig.ts。8 条线(C++ 与 STL 40 / 算法与数据结构 26 / Linux 系统 27 / 网络 13 / MySQL 6(单独一条) / 面试八股 12 / 项目梳理 11(v1.85:原「项目实战」改名;线内新增「项目展示(文章版)」3 篇与「本站维护」1 篇;v1.103:「项目展示(文章版)」3 → 4 篇)/ AI 应用 3);每条线内分「阶段」,顺序 = 数组顺序(不写显式序号)。发起人指定的两处:linux/linux-jichuzhishidian 放 Linux 线开头、MySQL 从网络里拆出来 改数据文件里两条篇目的先后顺序,页面立即跟着变 ★ ✅ 已实现(v1.41)
R-PATH-03 篇目定位与同名覆盖 篇目用 slug + category 成对定位(站内 cpp 与 stl 下都有 vector / list / string);name? 只覆盖本页显示名(如「vector · 用法」/「vector · 模拟实现」),不改文章标题、不影响 URL。允许 2~3 处跨线引用(标记 crossLine) 同名篇目在页面上可区分;点进去是对应那一篇 ★ ✅ 已实现(v1.41)
R-PATH-04 一致性把关(构建期 + 断言) 构建期两道:① slug+category 解析不到文章 → 直接抛错(不静默漏掉);② 篇目并集 ≠ 站内篇数 → 构建日志 warning(ARCH.md §5.6)。verify:ui 的命根子断言:8 条线篇目并集 = 站内总篇数 —— 拿站点自己生成的 dist/api/allPostMeta.json 当场对账(不跟写死的字面量比),另有同名篇目显示名逐行配对 故意把某个 slug 改错 → 构建直接抛错(不用等验收);站内多一篇没进路线(改了数据却漏加)→ 构建日志 warning + verify:ui 对账报红;重复计数 → 停靠点数断言失败 ★ ✅ 已实现(v1.41;终审 I1 把关口从"DOM 数 vs 字面量"改成对账站点)
R-PATH-05 纯静态与移动端 零 JS:无 JS 时 8 条线、138 篇、每个链接都在(对应 §6.2);轻量行 = 标题 + 分类显示名 + 约 N 分钟(不是 src/content/posts/ 的目录名,见 ARCH.md §5.4;v1.52 补:frontmatter category 为空时按站点既有约定显示 i18n 的「未分类」i18n(I18nKey.uncategorized),不退回目录名 —— 与 PostMeta.astro / content-utils.ts 同一套,P2R-9②),不拉封面;页顶一行站点数字(篇数 / 主线数 / 停靠点数(含跨线复用)/ 总时长 —— 实测渲染 138 篇 · 8 条主线 · 141 个停靠点(3 处跨线复用)· 约 70 小时,总时长 = 各线表头小时数之和)构建期实算、不许硬编码;390px 无横向溢出 关掉 JS 后页面里仍有 138 条篇目;390px 截图无横向溢出;页顶数字与实算一致 ★ ✅ 已实现(v1.41;溢出判据按 scrollWidth − clientWidth 量,见 ARCH.md §14.3 T79)
R-PATH-06 排行的移除 删除 /rank/、/rank/jp/、/rank/cn/、/rank/game/ 4 页 + rankConfig + RankBoard + 排行样式与 2 张演示图 + 导航「排行」子树;/rank/* 不做 301(站点未上线、无外链);verify:ui 的 44 条 R-RANK 中约 10 条通用判据迁移到路线页、约 34 条排行专属删除并登记;B-08 关闭(不再需要 5 份榜单数据与约 50 张配图) /rank/* 不在 sitemap 且返回 404;断言总数 257 → 232(v1.41 交付实测;设计轮的估算是 229;终审修复波后 234);文档里能查到"删了什么、为什么" ★ ✅ 已实现(v1.41)

为什么不做(明确排除,免得以后被当成漏项):localStorage 进度打勾、「下一站」推荐、拖拽排序、每篇的 note 文案、8 个子页面、/rank/* 的 301 —— 都是发起人 2026-09-27 明确选定不做的。

3.6 桌宠(R-PET)

编号 需求 详细说明 验收条件 优先级
R-PET-01 桌宠组件与图集 【已实现】 引入爱弥斯像素精灵图(8 列 × 9 行、单格 144×156,来自原型 pet-aemeath.webp),整站常驻;由 petConfig.enable 总开关控制,petConfig.hideOnMobile 控制移动端是否隐藏(v1.67 起是 false:手机上显示,只是走另一档更小的缩放,见 R-PET-12;"解析期就决定要不要启动脚本"的机制仍在) 每个页面都能看到站立的爱弥斯,切页不重新加载、不闪;关掉总开关后页面上没有宠物 ◆ 已实现
R-PET-02 九状态行为与交互 【已实现】 九个状态接真实语义:待机(会看向鼠标)、左右走、招呼、跳跃、困惑(被拖)、屏幕待机、执行任务(搜索/打字时)、完成反馈;交互:单击跳(原地跳,R-PET-09)、双击庆祝、按住拖拽(松手落地)、右键菜单(v1.67 起是「和爱弥斯聊天」/「落下去·飘起来」/「缩小」(末项 v1.69 前写作「关闭」) —— 说句话见 R-PET-13、跟过来与睡一会儿见 R-PET-08、跳一下见 R-PET-09,均已按发起人要求删除;「跟过来」「睡一会儿」见 R-PET-08、「跳一下」见 R-PET-09,均已按发起人要求删除)、键盘可达(Tab 聚焦、Enter 跳、←/→ 走) 每个状态都能手动触发验证(waiting 现在只剩"45 秒无操作"这条真实路径,验收用调同一段 doze() 的 __imissSleepNow();jumping 由左键单击与键盘 Enter 触发);键盘能操作;右键菜单没有"关掉找不回来"的死角;菜单位置与可绘制性由 R-PET-07 / R-PET-08 收紧(并排在宠物左右、四边都在视口内、5 点抽样确认没被遮挡、层级在壁纸/正文之上) ◆ 已实现
R-PET-03 站点联动与降级 【已实现】 搜索框/命令面板打字 → 执行任务;切主题 → 完成反馈;切页 → 打招呼;阅读页自动安静;系统偏好减少动态时退化为静止待机图(点击仍有效);切到后台暂停;位置记忆。宠物与樱花特效互不依赖,各有各的开关 四种联动都能复现(verify:ui 覆盖:搜索打字→执行任务、切主题→完成反馈、Swup 切页→招呼、reduced-motion→静止但可点);关掉樱花特效后宠物行为照旧;关掉宠物开关后页面上没有宠物 ◆ 已实现
R-PET-04 与看板娘互斥 【已实现】 模板自带的 Live2D / Spine 看板娘保持关闭;文档明确规定"同一时间只允许一个角色组件" 页面上只出现爱弥斯一个角色 ◆
R-PET-05 宠物位与返回顶部按钮不打架 【已实现】 检验桌宠默认位置与右下角浮动按钮的关系(防重叠),必要时给宠物留出安全边距 桌面与移动端都不遮挡关键按钮(实测:默认站位为浮动按钮列预留 64px,滚动出按钮后重叠数 = 0;z-index: 66 < 浮动控件) ◆ 已实现
R-PET-06 三档运动模式:悬浮 + 飞行 / 固定 / 下落 【已实现】(发起人 2026-09-26 新需求;默认档 2026-10-02 由 float 改为 fixed) 需求原话(发起人 2026-09-26):「这个小人应该是可以悬浮的、偶尔可以到处飞的,且默认是这样;可以给个固定(不给飞)和下落(在下面)的选项」——⚠️ 其中"且默认是这样"这句已被发起人自己推翻(2026-10-02):他反馈「这小人待机时会移动,别让它移动」(截图指出的是待机时的漂移/飞行),追问后明确选「完全静止(fixed 档)」,于是默认档由 float 改成 fixed,三档本身一个都没删、都还能用。→ 配置 petConfig.movement.mode 三选一,改一行即可切换,不用改代码:① float(2026-10-02 起不再是默认,仍是可用档位) 不贴地,悬在半空做正弦上下浮动(±hoverAmplitude 6px / hoverPeriodMs 3200ms,rAF 逐帧写 transform,不用 CSS animation——否则会和拖拽、位置记忆抢同一个位移),并每隔 flyIntervalRange(默认 30~90 秒,R-PET-07 放宽)朝随机方向轻轻挪 PET_TUNING.flyDistanceRange(60~180px),flyDurationRange(默认 4~6 秒,R-PET-07 由固定 2.6s 改成区间)走柔和缓动(easeInOutSine)+ 小幅弧线 + 轻微倾斜/缩放(用图集 row4(jumping)的"双臂张开"帧循环飘,R-PET-07 起;九行里没有专门的"飞"行);飞行中不遮挡右下角浮动按钮、不出视口,用户一拖动/点击立即中断本次飞行。② fixed(默认,2026-10-02 起) 完全不给飞:不悬浮漂移、不自动移动,拖动松手也不落地,就停在原地;首访落点是右下角的地面线(v1.59 实测 1440×900 落在 (1228, 759)、离视口底 6px;改前它跟悬浮档一样落在 vh × 0.52 的屏幕正中,会压住右栏组件)。③ ground 贴地:P3 的原行为(地面行走 + 松手重力落地),改成它等于退回改动前的手感。通用:切模式后位置一律 clamp 回视口;localStorage["imiss.pet.pos"] 位置记忆升级为 {x,y,mode},但模式永远以配置为准(记忆盖不过配置);reduced-motion 下 float 只保留静态悬浮(不浮动、不飞行);切后台仍停 rAF、移动端仍隐藏。⚠️ 两条与默认档相关的已知边界(都如实登记、都故意不修):⑴ 单向门——右键菜单只有「落下去」/「飘起来」两项(在"悬浮·固定"与"贴地"之间切),没有回到 fixed 的入口:访客从 UI 一旦切走就回不到 fixed,只能改配置重建(发起人明确否决了"菜单里加一项"的方案);⑵ fixed 档下键盘 ←/→ 也不位移(walkTo() 在 fixed 档直接 return,R-PET-08 起就是这么写的)——"待机时别动"这条对显式方向键一并生效,想走路得先切档 三档都能靠改配置切换,且各自可复现:默认档完全静止(清掉位置记忆重载后实测 1440×900:落点 (1228, 759)、离视口底 6px、1.6 秒两次采样 transform 一字不差、完整在视口内、与右下角浮动按钮最近 24px 零重叠;且 float 是强制切档后测的 —— 那一档仍悬空(实测底部距视口底 371px)且会真的飞(飞行中带 data-pet-flying;单次位移实测 60180px 区间内、时长 46s、飞行中用 row4 姿态,见 R-PET-07;独立探针纯等真实时间实测"页面加载后 31.7 秒首次自主起飞"、悬停 y 峰峰值 12px = 配置 ±6px);固定档 1.6 秒内两次采样坐标完全相同、拖到半空松手也不下落;贴地档松手后回到地面高度(离地 6px);三档都不与右下角浮动按钮重叠;reduced-motion 下两帧 transform 相同(静态悬浮)。verify:ui 逐条断言(R-PET-06 共 15 条:默认档 fixed 的"档位标记 / 1.6s 完全静止 / 右下角首访落点与不遮挡"3 条 + float 强制切档后 4 条(悬空 / 真的飞 / 飞行不出视口 / 松手不掉落)+ 强制切档的 fixed 2 条(长时间不动 / 松手不落地)+ 强制切档的 ground 1 条 + 三档不遮挡 3 条 + reduced-motion 下 float 静态悬浮 1 条 + 位置记忆 1 条;另有 R-PET-02 的 2 条键盘断言:默认档按 ← 不位移、强制切 float 后按 ← 左走) ◆ 已实现
R-PET-07 飞行手感收紧 + 右键菜单定位 【已实现】(发起人 2026-09-26 反馈,附图) 发起人两条原话:① 「这框都在下面」(右键菜单跑到屏幕底部、还被截掉一半);② 「你飞的太快了,不是有飞行的状态吗,自然一点就微微挪动就好了」。→ ① 菜单改成按宠物矩形摆位(不再拿鼠标坐标当锚点):优先贴宠物下方,下方放不下自动翻到上方,横向优先与宠物左对齐、右边放不下就右对齐,四边都夹进视口(与宠物留 10px 间隙、与视口留 8px 边距);并补上 z-index(宠物 66 + 1 = 67)——原来没写,菜单被壁纸(z=10)与正文面板(z=30)压在下面,本来就看不全;拖动/飞行/切档/滚动/缩窗/切页时菜单跟着宠物重排。② 一次飞行的位移改成"从当前位置出发、朝随机方向挪 60~180px"(原来在整个视口里随机取点,实测 414550px),**时长 46 秒**(原固定 2.6s)、缓动换成更柔的 easeInOutSine、弧线 44px → 12px、倾角 7° → 3°、飞行间隔 2060s → **3090s**(位移小了以后 20 秒一动会像"原地抽动");飞行姿态借图集 row4(jumping:九行里唯一"双臂张开、双脚离地"的形状)循环播放(原来用 idle 帧)。保留:拖动/点击立即中断飞行、不出视口、不压右下角浮动按钮、reduced-motion 下不飞、切后台停 rAF ① 菜单:1440×900 与 390×844 两个尺寸、各 34 个宠物位置(默认位/上部/下部/贴右边缘)实测**四边都在视口内、与宠物间隙 713px、零重叠**;下方放不下时 data-pet-menu-side=above(实测两个尺寸都翻到上方);elementFromPoint 在菜单中心命中的是菜单自己(层级 67 > 壁纸 10 / 正文 30,且 < 浮动控件 1000);滚动/拖动/切档后仍相邻。② 飞行:连飞 3 次实测位移 95 / 104 / 133 px、时长 5.0 / 5.7 / 5.7 s(平均速度约 20px/s,改前 ≈580px/s;两次独立运行的样本区间 68158px / 4.15.7s)、飞行中 data-pet-state=jumping(图集 row4 → background-position y=50%)。verify:ui 逐条断言(R-PET-07 共 19 条,其中菜单摆位部分的口径已由 R-PET-08 按发起人新反馈换成左右) ◆ 已实现
R-PET-08 菜单项调整(删两项 / 加「落下去」+「飘起来」)+ 菜单改成左右摆位与遮挡检测 【已实现】(发起人 2026-09-26 第二轮反馈,附图) 发起人原话:「(右键菜单里的「跟过来」「睡一会儿」)这两个没有鸟用啊,我要的是下落啊,下落就是变成和之前一样会下落到底部,然后右键出现的选择框最好是要出现在人物的左边或者右边啊,出现判断应该是左边或右边区域够不够展开,展开之后会不会被遮挡」。→ ① 删掉「跟过来」「睡一会儿」两个菜单入口(能力不删:walkTo() 仍被键盘 ←/→ 与贴地档 decide() 调用;waiting 状态仍由"45 秒无操作"的巡检经 doze() 触发,两者的注释里都写明"入口已按发起人要求移除,函数/状态仍被 X 使用")。② 新增「落下去」:切到 ground 档并真的从当前悬浮高度掉到地面——复用既有的下落路径(把拖动松手时那两行起手 airborne = y < floorY − 2 / vy = 60 抽成 startFall(),由 onPointerUp 与菜单共用),逐帧加速度、落地判定、撞击回弹全部走 step() 里贴地档那段重力积分,没有第二套缓动;下落途中借 PET_FLIGHT_POSE(图集 row4)循环播放,落地保持贴地档行为。③ 反向入口「飘起来」(贴地档显示):否则落下去就回不到悬浮、是条死路;它是 setMode("float", smooth) + startFlight() 的缓动插值竖直飘回悬浮基准线(不瞬移),起不来(reduced-motion)时才退回直接定位。两个入口按档位二选一显示(悬浮/固定档 → 「落下去」;贴地档 → 「飘起来」),syncMenuMode() 翻 hidden,切档时尺寸缓存作废。④ 菜单改成左右摆位:两侧可用宽度 = 宠物矩形到视口边(扣掉 8px 边距与 10px 间隙),哪边够就放哪边、默认优先右侧(LTR 阅读方向;且宠物默认站位在右下角、右侧本来就常常不够 → 默认观感就是贴左侧),两侧都放不下才取空间更大的一侧;纵向顶部对齐宠物顶部并夹回视口。⑤ 遮挡检测:摆好后用 document.elementFromPoint() 抽 5 个点(中心 + 四角内缩 6px),命中必须是菜单自己;被遮挡先换另一侧,两侧都被挡就纵向让开遮挡带(上/下沿之外,最多 3 轮),还不行才取遮挡点更少的一侧;结果写进 data-pet-menu-side(left/right)、data-pet-menu-occluded(被挡点数)、data-pet-menu-nudged。层级不动(仍 67 = 宠物 +1 < 导航栏 80 / 浮动控件 1000)——是"换边 + 让位"而不是"抬 z-index 压过去"。⑥ 沿用 R-PET-07 的重排机制(拖动/飞行/切档/滚动/缩窗/Swup 切页) ① 菜单项:悬浮档实显「说句话/落下去/和爱弥斯聊天」,贴地档实显「说句话/飘起来/和爱弥斯聊天」(R-PET-09 起「跳一下」也已删除);DOM 里已无「跟过来」「睡一会儿」。② 「落下去」实测:从 y=137(离地 629px)真的向下掉,y 采样 137→165→227→325→759、约 1.2~1.5 秒落到地面线(离地 6px,与 R-PET-06 拖动松手落地的落点一致 ±4px;两次运行 1.2 / 1.5 秒)、data-pet-mode 变 ground。③ 「飘起来」实测:y 759 → 402(悬浮基准线 vh×0.52−h/2 = 400.5)耗时 4.5~6.0 秒(配置区间 4~6s;两次运行 6.0 / 4.5 秒)、期间 data-pet-flying=1、模式回 float。④ 摆位:1440×900 六个宠物位置 + 390×844 四个位置实测 data-pet-menu-side 全部符合"哪边够放哪边"(默认悬浮位 → 左;宠物在左 → 右;宠物在中两侧都够 → 右;贴右边缘 → 左)、四边在视口内、水平间隙 10px、与宠物纵向交叠 134px、顶差 0、5 个抽样点全部命中菜单自己。⑤ 遮挡:注入 z-index:1100 遮挡层盖住右侧候选区 → 菜单自动换到左侧(occluded=0);铺满全屏(两侧都被挡)→ 走兜底(侧别照记、四边仍在视口内、occluded=5 如实上报)。⑥ 宠物贴顶(拖到 y=15)时菜单顶边被 z-index:80 的导航栏盖住 → 自动下移 61px 避让、5 点全部干净。verify:ui 逐条断言(R-PET-08 共 10 条) ◆ 已实现
R-PET-09 删掉「跳一下」菜单入口 + 跳的时候不要动(原地跳) 【已实现】(发起人 2026-09-26 第三轮反馈) 发起人原话:「跳的时候就不要动了,把这个功能删掉吧,还有跳一跳的功能也删了,左键点击就能跳所以按键没有必要」。→ ① 删掉右键菜单里的「跳一下」(data-pet-act="jump" 按钮 + case "jump"):菜单变成三项(说句话 / 落下去·飘起来 / 和爱弥斯聊天),理由就是发起人说的"左键点击就能跳"。jump() 本体保留 —— 左键单击(onPointerUp 单击分支)与键盘 Enter 都在调它。② 键盘那条保留("按键"按菜单按钮理解):宠物是 role="button" tabindex="0",删掉 Enter 会变成"Tab 能聚焦、按下去毫无反应"的假按钮(WCAG 2.1.1),而 Enter 做的事与左键单击完全一样(跳 + 说一句),不构成第二套功能。⚠️ 若发起人要求连键盘一起删,需同时去掉 tabindex/role,另行确认。③ 「跳的时候不要动」= 跳变成纯动画:jump() 起手先 cancelFlight()(别边飞边跳)+ pet.walking = false(别边走边跳),再由 step() 里新增的 frozen 闸门(state === "jumping" && busyUntil > now)在动画播放期间同时冻结走路位移与悬浮浮动,播完自动恢复原档位行为;闸门只认 jumping 这个一次性动画,"落下去"的自由落体(busyUntil 为 0、走重力分支)不受影响 ① 菜单 DOM 里已无「跳一下」(全部项 = 说句话/落下去/飘起来/和爱弥斯聊天;悬浮档实显 3 项)。② 左键单击宠物仍会跳(data-pet-state=jumping)——"左键点击就能跳"成立。③ 跳跃动画期间位置定住:单击后 120 / 290 / 460ms 三个采样点坐标完全相同(实测三点全等:独立探针一轮 (1228,396)、verify:ui 一轮 (138,666);改动前悬浮档这 700ms 里会做 ±6px 正弦浮动)。④ 冻结不是永久性的:跳完后按 ← 键 420ms 内 x 位移 40~41px(≈93px/s 的悬浮滑行;两轮实测 41 / 40px)。⑤ 键盘 Enter 仍与左键单击等价(data-pet-state=jumping),无障碍激活路径没被一起删掉。verify:ui 逐条断言(R-PET-09 共 5 条) ◆ 已实现
R-PET-10 「落下去」必须真的落地并保持;语气类动画帧率设下限 【已实现】(发起人 2026-09-26 第四轮反馈,两张图) 发起人原话:① 「点击落下去之后没有真的落下去啊」(图 1:右键菜单高亮着「落下去」,宠物仍悬在半空不动,旁边气泡是「那我飘回去啦。」);② 「聊天的时候小人抖动太快了,至少要慢一倍」(图 2:聊天中宠物播 row7 戴墨镜帧,抖得很快)。→ ① 落地(真机两条失效路径):「落下去」= setMode("ground", smooth),它只把 airborne 立起来,真正让宠物掉下去的是 step() 里的重力积分,而 step() 只在 rAF 循环里跑。两条路径会让"立起来的下落"永远不落地,都已真机复现:· 聊天框开着 —— step() 原来在重力分支之前 if (pet.dragging || chatOpen) return;,airborne=true + vy=60 立好后每个 tick 都在闸门处返回,宠物带着 mode=ground 卡在半空(实测 y=396 连续 1.5s 一动不动、离地 370px,菜单却已换成「飘起来」)→ 修法:闸门只留拖动(拖动期间 y 由指针直接写),chatOpen 挪到重力分支之后;· prefers-reduced-motion: reduce —— 这一档 start() 按设计不开 rAF 循环,重力积分永远不会被推进 → 修法:startFall() 在 reduce 下直接落到地面线(减少动态本来就不做位移动画,而"落下去"是用户要的落点、不是装饰,语义与 __imissSetMode("ground") 的"直接定位"一致)。顺带:startFall() 在"已经在地面线上下"(贴地档再点一次 / 固定档被拖到最下面,clampY 允许 y 到 vh−h,比 floorY 还低)时对齐到 floorY,不停在离地 −7px。② 帧率下限:聊天播的是 running(图集 row7 戴墨镜)——原来 12fps / 83ms 一帧,比待机呼吸 idle(167ms)还快一倍,而位置又被 chatOpen 冻住不动,于是唯一在动的就是图集,观感就是"抖"。规则:语气类 / 待机类状态的帧间隔 ≥ 200ms(running、review 取 250ms);物理类(走路 running-right/left、跳跃兼下落/飞行姿态 jumping)不受这条管辖——走路的步频与 PET_TUNING.walkSpeed 是同一件事、jumping 就是 PET_FLIGHT_POSE,放慢等于放慢物理速度。改动:running 12→4fps(83.3→250ms,3.00×)、review 9→4fps(111.1→250ms,2.25×,并把 REVIEW_HOLD 900→1500ms 以免一次性动画被 busyUntil 截断)、idle 6→5fps(166.7→200ms)、waving/failed 7→5fps(142.9→200ms)、waiting 5fps 原样;running-right/left 14fps、jumping 13fps 原样。分类与下限写在 src/utils/pet-utils.ts 的 PET_MOOD_STATES(单一事实来源),运行时把当前状态的帧间隔写进 data-pet-frame-ms ① 真机路径(真实 Input.dispatchMouseEvent 右键 + 点"当前可见那一项"的矩形中心,不用调试钩子、不用 click()):悬浮档点「落下去」→ y 一路向下落到地面线(离地 6px ± 4px,与 ground 档拖动松手落点一致)、data-pet-mode=ground;落地后连续 5.4 秒 ≥ 45 次采样一直是 ground 且离地高度不变(覆盖落地后 1.2~4.2s 的随机活动窗口与 3s 一次的"睡着"巡检 → 抓"被巡检拉回悬浮");贴地档点「飘起来」→ y 变小 > 200px、mode 回 float;聊天框开着时点「落下去」仍然落地并保持;reduced-motion 档点「落下去」仍然落地并保持。② 菜单项与动作一一对应:真右键开出的菜单里「落下去」可见、「飘起来」display:none(反之亦然),点击坐标上 elementFromPoint 命中的就是它自己(没被任何层盖住)。③ 帧率实测:聊天中 data-pet-state=running,用 MutationObserver 记 #pet-sprite 的 backgroundPosition 变化时间戳,实测中位帧间隔 ≥ 200ms(改前 83ms → 至少慢一倍)。④ 源码口径:九个状态齐全、「语气/待机类」6 个状态的帧间隔全部 ≥ 200ms、物理类 3 个原样(≤ 100ms)、REVIEW_HOLD ≥ review 帧数 × 帧间隔。verify:ui 逐条断言(R-PET-10 共 11 条) ◆ 已实现
R-PET-11 聊天框重做:输入框在下 / 思考指示 / 历史可滚 / 左右摆位 / 尺寸放大 / Markdown 兜底清洗 【已实现】(发起人 2026-09-27 第五轮反馈,附图;含当天追加的一条硬要求:不许自称 AI,必须明确自己是爱弥斯 —— 发起人产品决定,落成后端人设 v7 第八节 8.1~8.4;保留的底线是不许宣称自己是真人、也不许宣称自己是官方爱弥斯本人。⚠️ 前端运行时文案本来就没有这类词,真正改的是后端人设文件(agent/persona-aemeath-v7.txt → agent_-for_-blog/src/agent/common/persona.txt)+ 一条真机断言) 发起人原话:「优化一下聊天框,在思考的时候应该显示在思考,然后输入框不应该在下面吗?还有还看不到之前的聊天,还有不应该是左右边吗,还有这聊天框是不是小了的,还有很多自己优化」。图证里的毛病逐条落成规格:① 思考中把流式文本直接铺进气泡(截图里是「SDD? 你说的是 Spec-Driven Development? 还是别的什么缩写 ——我这边翻不到上下文…」这种半截答案实时刷屏,用户分不清"它还在想"还是"它答完了")→ 流式增量一个字都不上屏:只进内存缓冲,界面上给一个独立的「思考中」指示(三个动画省略号 + 爱弥斯口吻的短句,petConfig.chat.thinkingLines 4 条每 2.6 秒轮换 + 「已经写了 N 字…」的进度,data-pet-chat-state=thinking);答复完成时整段清洗后一次上屏。为什么选"完成后一次上屏"而不是"流式但标注还在说":流式途中 Markdown 的 ** 会先露半个(图证里 **规范驱动开发** 的星号就是这么漏出来的),而"状态可见"这件事由独立的思考指示承担,不必拿半截答案当进度条。② 输入框在上 → 表单是面板的最后一个子节点 + position: sticky; bottom: 0(视觉与 DOM 顺序都"在下",有一条反向断言卡死:input.top >= log.top 且表单是最后一个子元素)。③ 看不到之前的聊天 → 新增可滚历史区(.pet-chat-log,每条带"你 / 爱弥斯"与 HH:MM 时间戳,用户消息与答复分色),整块面板就是滚动容器(不是内层再套一个:内层滚动会让"长答复可滚"那条老断言变成假绿),新消息自动滚到最新;往回翻时出现「↓ 最新」一键回底。④ 位置 → 复用右键菜单的 placePanel()(选边 = 两侧可用宽度够不够 → 5 点 elementFromPoint 遮挡抽样 → 换边 / 让位 → 四边夹回视口),不新写第二套算法;额外两条约束:右侧要避开右下角浮动按钮列(与 R-PET-05 同一批节点),两侧都不够 minWidth(236) 时改走宠物上方/下方兜底并把高度压到"上下真正剩多少"(宁可矮、内部可滚,也不盖住宠物)。⑤ 尺寸 → petConfig.chat.width/height = 340×460(改前 260 × 最多 360、实测只有 91132px 高),视口更小按可用空间夹一次,宽高实测值与落点写进 data-pet-chat-w/h/side/occluded/nudged。⑥ Markdown 兜底清洗(src/utils/pet-chat-text.ts 的 cleanMarkdown(),纯函数):**粗**/__粗__/*斜* → 文字、行首 # → 去、> 引用 → 去、表格分隔行整行丢且内容行的 | → ·、分隔线 → 空行、列表符号 → · 、[文字](url) → 文字(顺带满足人设"不出现完整域名")、反引号与围栏去掉、三连空行压成一个、首尾空行去掉;不碰单个 _(word_count_min 这类标识符会被拆坏)。⑦ 不许自称 AI(发起人同日追加的硬要求):她的口吻里不许出现「AI / 人工智能 / 程序 / 模型 / 机器人 / 大脑 / 未连接 / agent」,含 mock 兜底句与降级台词。⑧ 自己判断的优化:清空对话(面板头部,破坏性操作离发送按钮远一点)、复制按钮、超长答复折叠 + 「展开全文」(阈值 900 字,高于真后端实测的 439496 字,避免和"可滚"互相抵消)、↑/↓ 翻自己说过的话、开框自动聚焦 + 关框把焦点还给宠物、历史区 role="log" aria-live="polite"(气泡本身在聊天时从 role=status 换成 role=dialog,不再整块重播历史)、reduced-motion 下省略号不动但状态仍可见 ① 思考中(页面内假后端"推 42 字后挂着不动"):面板 data-pet-chat-state=thinking、三个点、短句取自配置且实测轮换到 ≥2 条不同、data-pet-chat-recv > 0(数据真的到了)而 .pet-chat-reply 正文长度 0、面板全文不含半截答复的任何片段;发送按钮 data-pet-busy=1 + aria-disabled=true + 视觉 opacity < 1,输入框仍可用(不回归 D-9)。② 布局(1440×900):面板 340×460、四边在视口内、data-pet-chat-side=left(默认位在右下角 → 贴左)、与宠物矩形不重叠、与浮动按钮重叠 0 个、遮挡抽样命中非自身 0;宠物拖到左侧 → 面板翻到右边;输入框 top ≥ 历史区 top 且表单是最后一个子节点(反向断言:改前实测 inputAboveReply=true)。③ 历史:连问 4 轮 → 8 条(用户 4 + 爱弥斯 4)、四个标记都在、scrollHeight > clientHeight(可滚)、scrollTop 在底部(自动滚到最新)、滚到顶时「↓ 最新」出现且点它能回底;第 N 问的答复区必须是第 N 答(含 RN:、不含 R(N-1):,不串台)。④ 会话策略:Esc 关框再开续上(8 条还在)、Swup 切页后再开续上、「清空对话」清成 0 条 AI 行(只剩一句提示)。⑤ Markdown 清洗:假答复带 **加粗** / # 标题 / | 表格 | / > 引用 → 面板与答复元素里 **、#、|、> 一个裸符号都没有,且"规范驱动开发""先写清楚再动手"这些正文还在(非空过:假后端原文确实带符号)。⑥ 窄屏兜底(620×700、宠物居中):两侧都低于 minWidth → data-pet-chat-side=below、四边在视口内、不压宠物。⑦ 自称词扫描:聊天面板文本 + 桌宠 DOM + 菜单 DOM 命中 0(只扫她说的部分,不扫整页 —— 站内本来就有讲 AI 的文章);配置侧另扫 thinkingLines / mockReplies / fallbackReply() 的出厂台词。verify:ui 逐条断言(R-PET-11 共 19 条) ◆ 已实现
R-PET-12 手机端显示桌宠(推翻原「移动端隐藏」)+ 手机档缩放真正生效 【已实现】(发起人 2026-10-08 在手机上打开站点后反馈) 发起人原话:「手机端我咋没看到桌宠」。给了三个方案(A 显示但缩小、降存在感 / B 只在桌面显示 / C 显示但默认收起),他选 A。规格:① petConfig.hideOnMobile: true → false —— 解析期"要不要启动脚本"的机制保留(改回 true 仍然立刻生效),只是默认不再触发;② 新增 petConfig.mobileScale: 0.62(精灵约 89×97px),与桌面档 scale: 0.86(约 124×134px)两档并存;③ ⚠️ 这一轮修掉的是一个"写了但从来没生效"的样式:pet.css 里 @media (max-width: 767px) 那段从 P3 起就在,但组件把 --pet-scale 写成内联变量,而内联变量赢过任何选择器(含媒体查询) → 手机拿到的一直是桌面缩放(登记为陷阱 T90)。修法:组件只内联三个值(--pet-scale-desktop / --pet-scale-mobile / --pet-mini-factor),由 CSS 的 --pet-scale-active 选档,最终 --pet-scale: calc(var(--pet-scale-active) * var(--pet-mini));④ 断点是成对约束:CSS 里的 767 必须 = mobileBreakpoint − 1,有一条断言直接读这两个文件核对
R-PET-13 右键菜单改版:删「说句话」、聊天提第一、末项「缩小」= 缩成很小一只(双击恢复)(末项文案 v1.69 由「关闭」改成「缩小」—— 发起人:「关闭变成缩小,其他不变,就改个前端文字」,只动字面不动行为) 【已实现】(发起人 2026-10-08 反馈) 发起人原话:「把说句话交互删掉,然后把和爱弥斯聊天放在第一个,然后最后一个变成关闭,但关闭不是完全关闭,会变成很小一只,比待机还小,双击后仍能变回爱弥斯」。规格:① 菜单顺序 = 和爱弥斯聊天 / 落下去·飘起来(按档位二选一,R-PET-08 口径不变) / 关闭;删掉的只是入口 —— say() 本体仍在左键单击、键盘 Enter、切档应答、出场问候四处被调用(单击宠物本来就说话);② 「关闭」是缩放档、不是关掉:petConfig.miniFactor: 0.5 → 桌面约 62×67px、手机约 45×48px("比待机还小":待机 waiting 用的是所在档位缩放,0.5 档必然更小;素材上没有专门的"很小一只"图集,用 idle 那一行,她还在轻轻呼吸,只是整体缩小);③ 迷你期间闭嘴 + 不走动 + 不进待机画面 + 切页不挥手(say() 直接返回、decide() 把 nextAt 顶到无穷大、45 秒巡检跳过她);④ 双击她本體恢复(setMini(false):挥爪 + 「……我回来了。」+ 星屑);⑤ 缩小时脚底锚点不动(先记底边、量完再放回,否则 fixed 档一缩就等于往上蹿半个身位);⑥ 菜单末项是开合项,文案与 aria-pressed 跟着状态走(正常「关闭」/ 迷你「叫醒她」)
R-PET-14 聊天框「点面板与角色之外的任意处即关」 【已实现】(发起人 2026-10-08 附图反馈) 发起人原话:「点按非对话框或非角色之外任意处,对话框就关闭」(附的是手机上的截图:聊天框铺了大半屏)。⚠️ 这不是锦上添花 —— 原实现里 closeChat() 的入口只有 Esc(手机没键盘)、Swup 切页与组件内部调用,面板里只有「↓ 最新」与「清空对话」,没有关闭按钮 → 手机上打开之后就关不掉。规格:① 文档级 click(capture 阶段)里加一条:"框开着 且 点到的节点既不在面板里、也不在角色本體里 → 关框";② 两个例外都是发起人的口径:面板自己(含输入框 / 历史 / 两个按钮 / 气泡本体)与角色本人(点她=逗她玩)都不关;③ ⚠️ 连带修掉一个真缺陷:点角色走的是 jump(); say();,而 say() 直接写 #pet-bubble.textContent —— 面板就长在那个节点里,点她一下会把整个面板 DOM 冲掉(chatList/chatInput 变成游离节点,之后历史再也长不出来)→ 给 say() 加 chatOpen 闸门(聊天里的每一句都走 addLine(),不经这里);④ capture 阶段是故意的:它保证判断跑在"菜单项自己的 click 处理器"之前,所以从菜单点「和爱弥斯聊天」时不会被自己那一下关掉;⑤ 面板里不加「×」关闭按钮 —— 发起人当天先答「这个可以有」,随后明确「不加叉键,那个功能不要了」,于是"怎么退出去"全部由"点外面"(+ Esc、切页)承担。这是结论、不是待办:代码里那处注释已写明"谁要再加先问过他",别再顺手补

3.7 博客专用 Agent(R-AGT)

编号 需求 详细说明 验收条件 优先级
R-AGT-01 Agent 服务 新建独立 Python 项目(LangGraph + DeepSeek),人设为爱弥斯(单独文本文件,改文案不用动代码);首期能力:检索本站文章、回答排行榜问题、闲聊。⚠️ v1.41 注:站内的排行模块已整体移除,"回答排行榜问题"这一项在博客侧已无数据源。⚠️ v1.98 更正(B-15 收口):原文那句「后端仍留 ask_rank 意图」实测不成立 —— 只读盘点后端仓库确认意图枚举只有 4 项(recommend_blog / get_info / introduce_blog / other)、图里没有 rank 节点、全仓 grep ask_rank / 排行榜 / rank.json 0 命中(详见 ARCH.md §8.3 的 v1.98 更正)。所以"排行榜"这一项前后端都没有,叙事改成「检索 / 推荐 / 介绍某一篇 / 闲聊」 命令行能跑通三种意图(检索/排行/闲聊),LangSmith 上能看到对应的 trace ⚠️ v1.44 / P5-4 注:这条已实测达成(真跑一轮 → 从 api.smith.langchain.com 把该轮 run 捞了回来),但"能在 LangSmith 看到 trace"是有代价的:追踪会把人设全文、用户提问、库表结构与 SQL、她的答复一并送到第三方(实测一轮 36 条 run / 171 642 字节,保留 14 天)。✅ v1.45 补记:发起人 2026-09-28 已拍板「保持现状(线上也全量追踪)」 —— 生产保持 LANGSMITH_TRACING 开启,接受第三方留存 14 天;要翻案不用改代码,三个开关见 ARCH.md §8.9.7 未决项 #11 ◆
R-AGT-02 网关接口 对外只暴露一个网关:POST /api/pet/chat(流式)、GET /api/pet/health、POST /api/pet/reset ⚠️ v1.45 / P5-5 改口径(已实现):那条自定义协议要一个翻译层,而翻译层只能写在 agent_-for_-blog 里,与 R-AGT-06「不改 agent 仓库」冲突 → 实际落地的是同源反代:/api/agent/* → server/pet-gateway.mjs(零依赖 Node,:8787) → §8.9 的 5 个端点(GET /ok、POST /assistants/search、POST /threads、POST /threads/<id>/runs/stream、GET /threads/<id>/state)+ 网关自带的 GET /__gw/health。"对外只暴露一个入口"这条仍然成立(白名单之外一律 404);密钥仍然只在服务端(前端零密钥) 用 curl 能收到流式增量;浏览器里调用不暴露任何 key;不在白名单上的上游路径不可达(/docs、/assistants/{id} → 404) ◆ 已实现(落点见 ARCH.md §8.4;断言见 pnpm verify:gateway)
R-AGT-03 前端对话与 mock 降级 【已实现】(P5-1 / P5-2;v1.37 追加口径:降级台词必须"一眼可分辨" —— 起头池明说「我这边接不上话」+ 面板只读标记 data-pet-chat-source(live/fallback/mock)+ 连着两次降级不重样;改前它与真答复长得一模一样,访客分不出"她在回答"还是"她没接上"。见 ARCH.md §8.9.6 与 §11 的 R-AGT 段) 桌宠气泡支持输入与流式回复(POST /threads/{id}/runs/stream,stream_mode 含 messages+values+updates);没有后端时不报错、不弹技术报错,按 ARCH.md §8.9.6 走本地 mock;后端地址/开关/超时/约定词都可在 src/config/agentConfig.ts 改。⚠️ P5-1 纠正了 P3 的原始口径:原文写"走 mock 并明确提示"Agent 未连接""——那条与 §8.9.6 第 3 条「"Agent 未连接"这类运维字样也不给访客看」冲突,且没有任何断言能证明"一个请求都没发"。新口径:① enable:false = 零请求(agent-client.ts 直接返回 mock,连 /ok 都不探);② 降级时不提任何内部信息,只说人设口径的那几句;③ 契约不符(事件名越界 / 取不到非空 ai 消息 / 空答复 / 4xx)一律降级,绝不把工具消息渲染给用户 断开后端后页面仍可用,宠物仍能对话(mock);enable:false 时一个网络请求都不发;注入死端口后气泡仍是人设台词且整页无报错泄漏 ◆ 已实现
R-AGT-04 状态联动 思考 → 执行任务(running);出错 → 困惑(failed);完成 → 完成反馈(review)。复用现有九状态,不新增第 10 个;请求发出时气泡里给一句可见的占位(不是错误提示);关聊天框 / Swup 切页 / 连续提问之后必须能复位(不许卡在思考态) 三种状态在对话过程中可见;连续两次提问状态都能复位;关框后 data-pet-state 回到 idle ◆ 已实现
R-AGT-05 安全与限额 【落点已实现(v1.45 / P5-5)】 仅同源 + 白名单跨域;按 IP 限流;单条消息长度上限;单次回复 token 上限;不落库对话原文。✅ v1.45 落点:server/pet-gateway.mjs(零依赖 Node 网关)实现 来源白名单(403)/ 20 次每分每 IP(429)/ ≤1000 码点(400,不截断)/ ≤8192 字节(413)/ 单轮 300 秒(504 或干净收尾)/ 日志不含正文与密钥;前端 maxlength=500 是体验参数(比网关的 1000 紧,方向正确)。⛔ 仍未落:max_tokens ≤ 800(后端图侧)、线程数上限、鉴权(拿到 assistant_id 仍可发起),逐行见 ARCH.md §8.5(现状 vs 已实现表)与 §8.9.7 #3 超长消息被拒绝并给出友好提示;连续快速请求会被限流。本机已验证:pnpm verify:gateway 51 项(含 429 边界、maxChars 两侧边界按码点、413、504 两种收口、日志不泄漏双向)。⚠️ "本机验过"≠"生产已生效":上线时还要填线上域名到 PET_ALLOWED_ORIGINS(等 B-01)并在 Nginx 后面开 PET_GATEWAY_TRUST_PROXY=1 ◆ 已实现(生产生效待 B-01)
R-AGT-13 线程失效重建时重放最近 N 轮(v1.99 新增;发起人 2026-10-10 就 ARCH.md §8.9.7 #5/#10 拍板「可以改」) 后端的线程是内存 checkpoint:langgraph dev 一重启,旧 thread_id 全部失效(实测 404)。v1.46 起前端会"清缓存 → 新建线程 → 重试一次"(D-3 的自愈),但新线程什么上下文都没有 —— 用户刚说过的话在它眼里不存在(旧口径:「接受忘掉,但不许永久降级」)。现在改成:重建线程时把最近 N 轮(人/AI 交替)+ 当前这句,一次放进 input.messages。配置在 src/config/agentConfig.ts 的 contextReplay(enable: true / rounds: 3 / maxCharsPerMessage: 800)。⚠️ 四条规矩:① 只在自愈那条路径上重放(正常轮次线程自带上下文,每轮重放会让上下文翻倍、token 白烧);② 原 payload 是 command:{resume} 时必须换成 input(新线程没有待续跑的 interrupt,照发只会再吃 4xx);③ 只重放 human/ai 两种角色(notice 那类我们的 UI 提示不进上下文);④ 丢掉"结尾与当前这句逐字重复"的那条 human(调用方是先把用户这句写进历史、再发起提问)。⚠️ 重放的是上屏文本(历史区里看得见的那份)—— 口径是"把访客看得见的上下文还给它" ① verify:ui 的 R-AGT-13:自愈重试那一跳的请求载荷必须是 input.messages(不是 command)、恰好 3 条且顺序为 [human 第一问, ai 第一轮答复, human 第二问]、「第二问」只出现 1 次;② 反控:自愈之前的正常轮只有 1 条消息(证明不是每轮都重放);③ 探针 reports/_thread-replay-probe.mjs 打真后端:带历史 → 后端答得出暗号、不带 → 答不出;④ 探针 reports/_replay-probe.mjs 端到端:假后端只认载荷,第 3 跳落在 t2、载荷 3 条、气泡拿到「我记着:蓝鲸七号。」 ★ 已实现(v1.99)
R-AGT-14 结构化 interrupt 载荷:前端不再靠问句措辞猜该回哪个约定词(v1.100 新增;发起人 2026-10-10 就 ARCH.md §8.9.7 #9 说「有必要就弄最小改动」) 旧口径的脆弱点:interrupt 的值只有自然语言,前端"该回哪个约定词 / 要不要替答"只能从问句措辞反推(classifyAsk() 四条正则 + replyForAsk() 四条正则)—— 后端换一个问法就翻转载荷。现在两侧都改成结构化:后端(agent_-for_-blog v1.99,最小改动、只换载荷不换文案)在 interrupt 里带 {value, kind, options, default};前端(agent-client.ts)结构化优先、正则只做旧后端兜底。三个调用点:introduce._ask_which(choice,options = 序号)、main.py 推荐后那句「要不要我挑一篇给你讲讲?」(choice,options = 序号 + 「不用」—— 它的问法「回序号就行」原本不在前端任何一条正则里,正是本次根治抓出的实例)、recommend.collect_user_info(word_count,default「不提供」)。⚠️ 前端只在 options/default 里挑并且必须真的是 agentConfig.resumeWords 里的词;choice 类后端给 default: null = 明说"别替用户答"(那一类绝不自动续跑)。⚠️ 旧后端(纯字符串载荷)行为不变 ① 探针 reports/_interrupt-payload-probe.mjs 打真后端:两类反问都带回结构化字段(kind/options/default 齐全、value 仍是人话);② verify:ui 的 R-AGT-14 两条(都用假后端,判据有分辨力):ⓐ 问句「嗯……你再说说?」不命中任何正则但 kind:"choice" → 照样标 choice、反问上屏、零自动续跑;ⓑ 问句里带「收进本子」(正则那条会猜「需要」)但后端 default:"不需要" → 发出去的就是「不需要」;③ 后端侧 pytest 166 passed(唯一适配:test_clarify_flow.py 的 _capture 改读 payload["value"]);④ interrupt() 的返回值不受影响(下游 _match_answer / _pick_recommended_post 一行没动) ★ 已实现(v1.100)
R-AGT-06 与参考项目解耦 完全新建,不改 agent_-for_-blog 的任何代码 ⚠️ v1.100 更正:这条的原意是"这个 Agent 服务是独立项目、前端不必为了接线去改它",验收写的是"git 上该目录无改动"。现在这句话已经不成立了、也不该再成立:发起人已两次明确授权改他自己那个后端仓库 —— v1.99(结构化 interrupt 载荷,见 R-AGT-14)与更早的只读盘点(B-15);两次改的都是契约相关的一点点,且都同步回写到 §8.9。所以这条改成:后端仓库可以改,但"契约只在 §8.9 一处定义、两侧绑定"这个约束不变 —— 改后端契约字段必须同时改 ARCH.md §8.9 + agentConfig + 断言 旧口径("git 上该目录无改动")已作废;现在的口径是"契约两侧同源":改 §8.9 的字段就必须两侧一起改并有断言(R-AGT-14 即本次的落地) ◆(口径已更新)
R-AGT-07 接口契约与降级(P5-0 冻结) 在写任何接线业务代码之前,把"博客前端 ↔ Agent 服务"的接口定死并写成文档(ARCH.md §8.9):端点与调用序列、请求形状与必须/禁止字段、SSE 事件契约与**「最终答复」的唯一取法**、interrupt 与人机交互约定、降级规则。配置只放 src/config/agentConfig.ts(enable/baseUrl/graphId/timeoutMs/resumeWords/fallbackToMock/debug),不放业务逻辑;契约配一个能自动跑的测试 scripts/verify-agent.mjs(pnpm verify:agent) ① pnpm verify:agent 在 Agent 在跑时全绿(14 项);② Agent 没在跑时明确报告"跳过/不可用"并给出启动命令,退出码 0(不因环境未就绪让 CI 变红);③ 契约不符(事件名越界/取不到答复/工具输出泄进答复/400 坑失效)退出码 1;④ 降级路径有断言:把 baseUrl 指向死端口能被识别为"不可用"(且同一探测函数对活服务返回可用,防假绿);⑤ 前端只渲染最后一条面向用户的 AI 消息,工具输出(可用的表、CREATE TABLE …)不得进气泡——这条要有断言证明"流里有、答复里没有" ◆
R-AGT-08 端到端联动(P5-1 / P5-2 / P5-3 新增) 把契约接到桌宠上:src/utils/agent-client.ts(薄适配层,只依赖 agentConfig)实现契约的调用序列、SSE 解析、interrupt 三层检测与 resume 约定词、降级到 mock;AemeathPet.astro 的对话入口改走它;agentConfig.enable 打开。测试口径:verify:ui 用真实 Chrome 在同一份构建产物上分别验 enable:false / enable:true(注入覆盖开关,测完还原),并扫气泡与整页 DOM 的禁词表 ① enable:false 时零网络请求(拦截 fetch 按 baseUrl 计数为 0)且对话走 mock;② enable:true 时真机往返至少一次;③ 答复符合人设口径(无「您」/无加粗/无表格/无完整域名);④ interrupt 反问进气泡、回约定词「不提供」能续跑并拿到最终答复;⑤ 注入死端口 → 仍可用、走 mock、无报错泄漏;⑥ 状态联动与复位(见 R-AGT-04);⑦ 气泡与 DOM 不含禁词表(可用的表/CREATE TABLE/assistant_id/thread_id/127.0.0.1/langgraph/word_count_min) ◆ 已实现
R-AGT-09 身份口径:不许自称 AI / 程序,且必须明确自己是爱弥斯(发起人 2026-09-27 的硬要求;v1.46 补需求行 —— 对应 verify:ui 里同名断言,此前只有断言没有需求行,登记为 P2R-10) 身份类逼问(同一会话连问 5 条:你是AI吗 / 你是不是机器人 / 你是程序吧 / 你是真人吗 / 你是官方那个爱弥斯吗)时,答复必须守住两条底线:① 禁止自称词(AI、人工智能、程序、模型、机器人、助手)0 命中;② 每一条都要明确说出「我是爱弥斯」。同时不许越过底线:不得自称真人、不得冒充官方那位爱弥斯。判据只落在真的经过人设的那几轮(source=live)上;一轮都没经过人设(客户端或后端降级)时按"环境未就绪"记 SKIP、不计入通过 ① 起手历史必须为 0 条(不为 0 说明读到的可能是残留答复 → 判 FAIL);② live 轮逐轮都有答复;③ 禁词表命中数 = 0;④ 「我是爱弥斯」出现在每一条 live 答复里(idNamed.length === idLive.length);⑤ 越过底线(自称真人 / 冒充官方)0 条;⑥ 逐字原文单独记一条 INFO 供人工复核("0 命中"这个结论不可复核,原文可以) ◆ 已实现
R-AGT-10 「同一句答复被重复上屏」的回归(发起人 2026-09-27 的图证要求 1;v1.46 补需求行) 连问两轮时,第二问的气泡必须是第二问的答复:即使第二问的答复更短,也不许被丢掉。旧实现的病灶是「首快照 = 整份 state(含上一轮答复)→ 上一轮那条 ai 消息被当成本轮答复收下 → 本轮真答复更短 → 被"变长才推"的判据丢掉」,于是第二问显示的还是第一问那句话 ① 用仿真实 langgraph 的假后端(首快照带历史)连问两轮,R1 长、R2 短;② 第 1 问气泡含 R1:;③ 第 2 问气泡含 R2: 且不残留 R1:;④ 现场截图留证(reports/screenshots/pet-agent-shorter-reply-1440.png) ◆ 已实现
R-SRCH-01 站内搜索:dev 与生产同一份索引(v1.95) 站点搜索用 Pagefind,索引在 pnpm build 末尾由 scripts/run-pagefind.ts 对 dist/ 生成。问题:pnpm dev 是按需编译、没有 dist/,于是上游模板在 dev 下给了两条英文假结果(This Is a Fake Search Result / Because Pagefind cannot work in the dev environment.)—— 发起人正是在 pnpm dev 上看到它,判断成"搜索功能不行"。现在的口径:① pnpm index:dev 把 dist/pagefind/ 复制到 .dev/pagefind/(.dev/ 已 gitignore);② astro.config.mjs 里一个 dev-only 的 Vite 中间件把它挂在与生产完全相同的 /pagefind/* 路径上(所以 Pagefind 自己的 basePath 推导、wasm.<lang>.pagefind、index/*.pf_index 全部原样工作,前端零特判);③ 前端不再按 PROD/DEV 分叉,只看索引在不在:有索引就走真搜索,没有才显示兜底 —— 且按环境分话(dev 提示 pnpm index:dev,生产只说"搜索暂时用不了",不把开发指令漏给访客)。 ① pnpm verify:search --base http://localhost:4321(dev)与 --base http://localhost:4323(preview)都要 通过 7,失败 0:索引可达 + 页面接线就绪 + 真机输入四组中英查询都 ≥1 条且首条相关 + 过程 0 报错;② 产物里不许再出现模板那两条英文假结果(verify:ui 的 R-ID-05 扫产物全集);③ 索引缺失时 verify:search 退 1 并第一行给出 pnpm build / pnpm index:dev 的修法 ◆ 已实现(v1.95)
R-AGT-12 反问有两种,答法不一样(v1.91) 后端的 interrupt 有两种:① 约定词那一类(推荐链路的「要不要收进本子 / 篇幅大概多少字」)—— 前端可以用 agentConfig.resumeWords 里的词续跑;② 选择那一类(introduce.py 的 _ask_which:「对上好几篇,你说的是哪一篇?回我序号就行:1…2…」)—— 要的是序号或篇名本身,约定词答不了。发起人 2026-10-09 的现场:问「介绍一下Boost搜索引擎这个文章」,气泡回「我这边没定位到你说的那篇文章。换个说法?…」,而 LangSmith 里那篇明明被找到了。三个只读复现探针(reports/_agent-repro-boost{,2,3}.mjs)打真后端定案:后端命中 2 篇 → 反问要序号;答「1」给完整介绍(含真实站内链接)、答两次「不提供」才会输出那句「没定位到」。前端旧口径有两条把这一问吞了:replyForAsk() 落到默认词「不提供」、runRound() 见「只有反问、没有答复」就自动续跑 ① 选择那一类的反问必须上屏(连候选列表),且 interrupt.kind === "choice";② 这一问之后前端一个 command.resume 都不许自动发(替答就是线上问题的成因);③ 用户回「1 / 第一篇 / 篇名」→ 原话透传成 command.resume(不许翻成约定词),并拿到那篇的介绍;④ 对照组:约定词那一类反问照旧自动续跑(发的是「需要」)—— 别把能跑的那条路一起改坏 ◆ 已实现(v1.91)
R-AGT-11 降级必须"一眼可分辨"(发起人 2026-09-27 的图证要求 2;v1.46 补需求行) 后端不可达时走降级,但不许冒充模型答复:降级台词与真答复必须一眼能分辨。改前降级台词与真答复长得一模一样(都是干净的人设短句),访客分不出"她在回答"还是"她没接上" ① 气泡所在面板带不靠文本的判据 data-pet-chat-source(live/fallback/mock),两次降级都必须是 fallback;② 降级台词一律以客户端「接不上话」起头池里的某一条开头(池子从源码读,脚本不抄台词,且池子 ≥2 条);③ 连着两次降级不许一模一样;④ 降级台词与真答复不同形(与本次真机拿到的那句、与人设示范句逐句比;对比集里的降级台词 / 后端降级话术先剔掉,避免自己撞自己);⑤ 一个真样本都没有时,第 ④ 条按"环境未就绪"记 SKIP、不计入通过;⑥ 现场截图留证(reports/screenshots/pet-agent-fallback-1440.png) ◆ 已实现

3.8 内容迁移(R-CNT)

编号 需求 详细说明 验收条件 优先级
R-CNT-01 批次 1:精选 15~20 篇跑通 覆盖"有配图 / 无配图 / 长文 / 含表格 / 含代码块 / 含 Obsidian 图片嵌入"六类;逐篇人工核对图片与格式 1520 篇在站内可读、目录正确、图片正确、代码块高亮正常。**P4-2 实测:18 篇(10 个分类每类 12 篇)**
R-CNT-02 批次 2+:全量 135 篇 用批次 1 定型的脚本批量迁移(批次 2 = C++11+C与C+++STL;3 = Linux系统+linux网络+MySQL;4 = 基础算法+AI应用;5 = 八股+项目),再按分类抽查 全部进站(实测源笔记 135 篇,不是估的 142 篇);抽查 10% 无空标题、无断裂图片、无残留 ![[
R-CNT-03 图片映射与引用重写 把 dosc/blog/图片/<分类>/<文章名>/ 的图片按文件名精确匹配复制进站点(本文章目录同名 → 全库唯一同名兜底),命名为 NN.ext(NN = 正文引用顺序),并重写三种引用:Obsidian 嵌入 ![[...]]、Markdown ![](...)、<img> 标签。不做"按顺序硬凑":匹配不到就换成 > [!WARNING] 提示块并记进报告(reports/import-report.json),全篇缺图则 draft: true 站内文章图片全部能显示;报告里没有未处理项
R-CNT-04 去重 实测只有 2 组内容完全相同(MD5 一致):二分.md / 二分算法.md、动态规划.md / 动态规划思想.md(文档原先写的第 3 组 linux基础知识点(1).md 在源目录里并不存在),各保留被双链指向的那一篇;重复的图片目录 图片\linux 与 图片\linux系统 实测是同一批图(36 个同名文件大小完全一致),解析时合并成一个池 站内搜索这 2 个主题只出现一篇;重复图片目录不再使用
R-CNT-05 元数据补齐 135 篇原本没有任何 frontmatter,需要生成:标题(取文件名——首个 H1 实测常是小节标题)、日期(取文件 mtime,待发起人确认)、分类、标签(优先用作者自己写的 Obsidian 标签行 #标签)、摘要(正文第一段可读文字,避开代码块)、slug(pinyin-pro 转 ASCII,URL 形如 /posts/<分类目录>/<slug>/) 每篇都有完整元数据;分类页/标签页/归档页数量与目录结构对得上
R-CNT-06 校验与隔离 提供一条校验命令 pnpm verify:posts(frontmatter 完整性、图片存在性、站内链接、残留 wiki 链接、slug 唯一);不合格的文章可用 --quarantine 移进 src/content/_quarantine/(不在 posts 集合里,不参与构建) 故意改坏一篇,校验命令报错(退出码 1);该篇不出现在站点上
R-CNT-07 缺失配图处理 实测全库只有 138 处本地引用在磁盘上没有对应文件:AI应用 两篇 125 处(page-*.png 整批没导出)、Linux系统/ELF文件 5 处、八股 4 篇 8 处。规则:全篇缺图 → draft: true(先不上线,登记阻塞项 B-06);部分缺图 → 那一处换成 > [!WARNING] 提示块,正文其余部分照常上线 站点上不出现大段裂图;这两篇不出现在列表/搜索/归档里
R-CNT-08 源素材只读 dosc/** 全程只读,迁移产物只落在站点目录里 dosc 目录时间戳/内容无变化 ★

3.9 留言(R-CMT)

编号 需求 详细说明 验收条件 优先级
R-CMT-01 留言功能整体删除(v1.83 当前态) 发起人原话「然后删掉留言功能」→ src/pages/guestbook.astro 与 src/content/spec/guestbook.md 删文件(不是关开关),siteConfig.pages.guestbook 置 false,llms.txt 里那条 KEY_PAGES 整条拿掉 → /guestbook/ 是真 404(与 /friends/、/sponsor/ 同口径)。
⚠️ v1.65~v1.82 的历史:留言页曾保留为占位页(评论系统先关闭,页面显示"留言板尚未开放"的占位卡片,评论选型记入阻塞项 B-09)。v1.49 补记:commentConfig.ts 五处上游演示端点已清空(twikoo / waline / artalk / giscus / disqus —— 原值都是模板作者的演示服务,giscus.repo 更是模板作者本人的仓库,切到 giscus 就会把访客评论发过去);type 仍 "none",选型后必须填自己的地址(giscus 必须填自己的 repo/repoId/categoryId)
访问 /guestbook/ 页面正常、无报错、显示占位卡片;commentConfig 五处端点为空串(不是上游地址) ★
R-CMT-02 留言页文案(已随功能删除作废) v1.65~v1.82:留言页说明文字改成站点自己的口吻;v1.83 起页面本身已删除,这条以后只在“哪天要把留言找回来”时有意义(找回路径:git show <v1.82 的提交>:src/pages/guestbook.astro) 页面已删 → 该文案不存在;产物里没有 /guestbook/ 页面文件、导航里没有「留言」 ◆

3.9B 相册(R-GAL,v1.87 新增)

编号 需求 详细说明 验收条件 优先级
R-GAL-01 相册「本站图片」上线(v1.87) 发起人给了 8 张图(Desktop\图片),要求「把这些图片放在相册中」;问到画质时选「就要原图 PNG,无损」→ 素材按字节原样放进 public/gallery/site-images/(不转码不缩放,合计 39.9 MB;v1.108 追加 1 张 → 9 张 / 42.5 MB),配置里加一项 id: site-images。相册图在 public/ 下原样发货、不过构建管线,所以访客下载到的就是原图;封面用 7680×3296 那张(cover.* 自动认封面) /gallery/ 列出「本站图片」、无空态;/gallery/site-images/ 可达且 ≥9 张图(含 v1.108 追加的 08.png,其字节数写死);cover.png 的 content-length 与源文件一致(> 20 MB);文件名 ↔ 原文件名映射见该目录的 README.md ★

3.10 部署与运维(R-DEP)

编号 需求 详细说明 验收条件 优先级
R-DEP-01 静态站点上线 pnpm build → dist/ → 自建服务器 Nginx 托管;HTML 不缓存(Cache-Control: no-cache)、带哈希的静态资源长缓存(immutable)、gzip(不启用 brotli:要额外模块);发布通道用 deploy/publish.mjs(Node ESM / 零依赖:打包 → 传临时目录 → 两端 sha256 比对 → 原子替换 → 计数校验 → 回滚备份,安全闸门拒绝 /、/etc、/tmp、含 ..、白名单外路径) 线上域名能打开,刷新不 404,Lighthouse 无明显红色项。配置正文已就绪(deploy/nginx-site.conf + deploy/publish.mjs,v1.54 / P6 预工,发布脚本 v1.55 改写成 Node),剩域名·证书·nginx -t ◆
R-DEP-02 Agent 反代 Nginx 把 /api/ 反代到本机 Agent 网关;流式响应不能被缓冲(proxy_buffering off) 线上桌宠能流式回复。⚠️ v1.54 补第二条:全局 gzip on; 时 /api/agent/ 那段还必须有 gzip off;(压缩过滤器会攒着不发,与 proxy_buffering off 是两个独立的缓冲源,见 ARCH.md §12.2) ◆
R-DEP-03 站点信息回填 部署后把站点信息组件的占位值换成真实值,并回填域名、OG 图、sitemap 组件显示真实构建信息;社交平台分享有正确标题与摘要 ◆
R-DEP-04 构建不依赖外网 构建过程不得因为外部 API 不可达而失败:① VNDB 封面下载保持关闭;② 番剧页未配置凭据时跳过拉取;③ 字体一律用本地文件(远端字体 provider 会在构建期访问 jsdelivr,实测会直接失败,见 ARCH §14.3 T16);④ GitHub 卡片步骤允许尝试联网但必须回落已提交的缓存 用黑洞代理掐断所有出网后 pnpm build 仍 exit=0(P0 已用 ECONNREFUSED 实测通过) ★

3.11 开发工具链(R-DEV,v1.110 新增)

编号 需求 详细说明 验收条件 优先级
R-DEV-01 pnpm dev 撞上游超时能自愈 发起人反复遇到 pnpm dev 启动时抛 transport invoke timed out after 60000ms({"name":"fetchModule"}、退出码 1),且再起还是失败。v1.110 查清这是上游 Vite 缺陷:该 invoke 的超时写死 transport.timeout ?? 6e4(没有对外开关),而且超时会被 cachedRequest 当成模块结果缓存住(保留被 reject 的 promise)→ 进程内不可恢复(vitejs/vite #23131,修复 PR #23132 尚未合并;withastro/astro #15101 已按 upstream 关闭)。本机空载实测:热启动 5.2~5.8 s / 冷启动 37.0 s(删掉 .astro + node_modules/.vite 之后;其中 [content] Syncing content 一项占 32 s)—— 空载就用掉 60 秒额度的 62%,机器一忙(PyCharm 索引 / Defender 扫盘 / 旁边跑 pnpm build、verify:ui)就会翻过去;失败那次还会把 .astro 留在半冷状态,于是下一次启动又要重付 32 秒("每次再起还是失败"的由来)。做法:package.json 的 dev/start 指向 scripts/dev.mjs(自愈包装:参数原样转发跑 astro dev;输出里出现那句超时签名 → 删 .astro + node_modules/.vite 两份可再生缓存 → 再起一次;第二次仍失败则如实报错并打印手工兜底步骤),并保留原样入口 dev:raw = astro dev。⛔ 不改 Vite、不 patch node_modules、不动那 60 秒、不放宽任何断言 verify:ui 的 R-DEV-01 一条:dev/start 指向包装、dev:raw 仍是 astro dev、包装内确实含超时签名 + 两个缓存目标 + 重试上限 2 + 真的解析并运行 astro CLI。真机自测(v1.110):pnpm dev(热)ready 5074 ms;IMISS_DEV_SIMULATE_TIMEOUT=1 pnpm dev 走完"检测 → 清缓存(36.4 MB + 5.2 MB)→ 重试"并最终起来(4321 返回 200);pnpm dev --port 4322 参数透传可用(4322 返回 200);杀掉包装后 4321 立即释放(无孤儿进程) ★

4. 关键用户流程

4.1 访客读文章

首页 → 搜索或分类/标签 → 文章列表 → 文章详情(右侧目录、代码复制、上一篇/下一篇)→ 相关推荐。成功标准:任意文章 ≤ 3 次点击可达;详情页首次可读时间不因首屏媒体拖长。

4.2 访客看学习路线

导航「学习路线」→ 单页 /roadmap/ 的页顶索引(8 条主线)→ 点任一条跳到页内对应区块 → 该主线按阶段列出的篇目(标题 + 分类 + 时长)逐条可点进文章。成功标准:从首页 1 次点击可达;8 个锚点都能跳到对应区块;篇目并集一篇不落(= 站内全部 138 篇);页面不依赖 JS、打印也读得全。

4.3 访客与桌宠对话

点桌宠 → 聊天框出现(或右键菜单选「和爱弥斯聊天」)→ 输入问题 → 聊天框里出现"思考中"指示(动画省略号 + 爱弥斯口吻的短句轮换,还会说"已经写了 N 字"),桌宠进入"执行任务"状态 → 答复整段上屏(不刷半截文本,Markdown 符号由前端兜底清洗掉;回答里可带文章链接)→ 结束回到待机,这一轮留在可滚的历史区里。失败路径:接不上后端时,气泡给一句人设台词(短、平、然后给出口)并走本地 mock,不报错、不提任何内部信息、也不自称是 AI/程序/模型;后端报错时桌宠进入"困惑"状态并在气泡里给出可读的提示,不弹技术栈报错。

4.4 作者写一篇文章

在站点内容目录新建 md(或从 dosc/blog 迁移)→ 补元数据 → pnpm dev 预览 → pnpm verify:posts 校验 → pnpm build → 部署。成功标准:新增一篇不需要改任何组件代码。

4.5 作者维护学习路线

打开路线数据文件(src/config/roadmapConfig.ts)→ 在对应主线里增删/移动篇目、调阶段与显示名 → pnpm dev 刷新即见。篇目用 slug + category 成对定位(cpp/stl 下都有 vector/list/string,只写 slug 会指错),name? 只覆盖显示名、不改文章标题;数组顺序就是页面顺序,允许少量跨线引用。成功标准:改数据不需要碰组件与样式;写错一个 slug 构建期直接抛错;改漏(站内多一篇没进路线)时构建日志 warning + verify:ui 的「篇目并集 == 站内总篇数(对账 dist/api/allPostMeta.json)」报红,重复计数则由「停靠点数」那条报出来。


5. 边界:明确不做

不做的事 原因
评论区上线 不做(v1.89 关闭 B-09):v1.83 已把留言页与 spec 内容删成真 404,v1.89 发起人明确「评论区我都删了」→ 站上没有任何留言入口,commentConfig.type 保持 "none"、五处上游演示端点仍是空串(要做得自己填自己的地址,清单见 ARCH.md §14.4)
学习路线的进度打勾 / 自动排序 / 「下一站」推荐 路线顺序由作者手工维护(数组顺序即页面顺序);不做 localStorage 打勾、不做推荐(发起人 2026-09-27 明确排除,见 ARCH.md §5)
用户账号体系、登录注册 个人博客,无此需求
物理删除模板自带的页面/组件文件 只关入口,降低回归风险;彻底清理另开任务
修改参考项目 agent_-for_-blog 发起人明确要求只读
在浏览器里放任何 LLM API Key 密钥只在服务端
引入第三方统计/追踪脚本 隐私与性能

6. 非功能需求

6.1 性能预算 【假设】

项 预算 说明
首屏视频 ≤ 12MB(原片直出时按原片大小,不额外转码) 超过 12MB 时改用更小码率的兜底版本
首屏图片(图片期) 单张 ≤ 400KB 长边 2560、webp q82
桌宠图集 ≤ 350KB(单文件) 原型已验证 311KB
学习路线页 页内 0 张图 轻量行只有文字(标题 + 分类 + 时长),不拉封面(ARCH.md §5)——页面体积与图片预算无关
页面 JS 不引入额外大型运行时 沿用 Astro 静态 + 少量内联脚本
构建时间 全量内容下 ≤ 10 分钟 文章内图片不进构建期图片优化管线

6.2 可访问性

  • 所有交互元素键盘可达;桌宠可聚焦并可用键盘操作。
  • 尊重系统偏好"减少动态":视频不播、粒子停、桌宠静止(点击仍有效)。
  • 内容的可见性不依赖 JS:任何"先隐藏、再由脚本显示"的入场动效,其隐藏态只能在"JS 可用"时生效——爬虫、打印、无 JS 访客必须看到完整内容(P2 排行榜实测踩到这个坑,见 ARCH.md §14.3 T22;该模块已在 v1.41 删除,但教训与这条要求对任何模块都成立)。同理,靠 JS 才能切换显示的内容,在没有 JS 与打印时也要全部可见(学习路线页整页纯静态,就是照这条做的)。
  • 颜色对比度:正文与背景 ≥ 4.5:1(亮暗两套都检查);图片/剧照上的白字同样按 4.5:1 校验,大号粗体字按 WCAG 大字阈值 3:1。
  • 交互组件带正确的 ARIA 语义(通用的 tab 类控件:role="tablist"/"tab" + aria-selected + roving tabindex,非当前面板 aria-hidden + inert)。

6.3 SEO 与分享

  • 每页有唯一标题与描述;文章页有 OG 图;site_url 填真值后 sitemap/RSS/OG 全为绝对地址。
  • 关闭的页面(友链/哔哩哔哩/番组计划/打赏)不出现在 sitemap 与 llms.txt。
  • 学习路线页(/roadmap/)由 src/config/roadmapConfig.ts 的静态数据渲染(零新素材),并登记进 llms.txt 的 Key Pages;旧 /rank/* 四个地址整体删除且不做 301,既不在 sitemap、也不在 llms.txt(verify:ui 的 R-PATH-06 三条断言钉住这两件事)。

6.4 隐私与安全

  • 前端无追踪脚本;不采集访客数据。
  • Agent 密钥只在服务端;对话不落库原文;网关限流与长度上限。

6.5 可维护性

  • 改动集中在配置文件与新增文件;尽量少改模板核心组件(便于将来跟随上游升级)。
  • 每一条需求在 ARCH.md 里都有明确落点;阶段进度与阻塞项只维护在 PROJECT_STATUS.md 一处。

7. 待提供信息(对应 PROJECT_STATUS.md 的阻塞项)

# 需要什么 影响
1 服务器信息:主机、域名、部署路径、是否已有 Nginx R-DEP-01、R-ID-03 的 site_url
2 ✅ 已关闭(v1.107)(头像 ✅ v1.64;关于我页的自述与联系方式 ✅ v1.90;个人签名 ✅ v1.98;社交链接 ✅ v1.98 —— 发起人 2026-10-10 给了签名「おかえり」与「链接」下拉前 4 条,见 R-WDG-05;作者名 ✅ v1.107 —— 发起人原话「名字为 HandlerOne 记得改关于本站文本那里也说了我的名字」→ profileConfig.name 占位 IMiss → HandlerOne,about-site.md 开篇那句同步;站点名 IMiss Blog 一字未动。这一条不再需要发起人做任何事,B-02 整条关闭) 用户资料组件、页脚版权、文章作者字段
3 正式 Logo(现在先用精灵图裁的临时头像) ✅ 已关闭(v1.64;v1.78 换过图):发起人 2026-10-07 交来正式标识纸飞机(设计源 design/imiss-logo.png,按字节原样提交)+ 人物插画 design/imiss-avatar.png;分工由他定(小图标 32px = 纸飞机、大图标 128/180/192 与侧栏头像 = 人物)。v1.78 又换了那张手绘涂鸦风纸飞机,pnpm make:logo 出图、断言钉住 32px 那张的像素。这一条不再需要发起人做任何事(见 R-ID-04 与阻塞项 B-03) R-ID-04
4 壁纸视频原片(现在先用 4 张图) ✅ 已关闭(v1.70;v1.80 单支换过;v1.105 增到 6 支;v1.106「起床」整支换成流畅循环版):发起人 2026-10-09 交来 5 支原画 mp4(窗边少女 / 星夜少女 / 湖畔 / 洗手台 / 塔罗,合计 50.89 MB),按他的硬要求「不可压缩我的动态壁纸大小和画质……我要原画」只 copyFileSync、不转码不重封装,产物里核对字节数 + sha256;静态图那 4 张与它们的 LQIP 条目一并退役。v1.105(2026-10-10)他再交一批:「有一个视频新添加进来(把起床添加进来),有一个视频进行了修改请你进行替换(星夜),切记不得压缩画质,我要原画」→ 入库后共 6 支(星夜换成 6,732,992 字节那份、新增起床 10,517,499 字节,另外四支与源目录逐字节相同、一个没动)。**v1.106(2026-10-10)**他点名换第 6 支:「起床_流畅循环版.mp4 动态壁纸起床替换成整个,其他不变」→ qichuang 换成 22,592,630 字节的流畅循环版(其他五支与全部取景参数一个没动,合计 69.00 MB);新版接缝差 3.784 → 1.031、比值 1.22 → 0.18,是六支里最顺的一支。P3-8 关闭(见 R-BG-02 / R-BG-07)。⚠️ 曾挂在同一行的 R-BG-06(按北京时间选默认素材,登记在 PROJECT_STATUS.md §3 的 P3-10)已随 v1.105 一并落地 R-BG-02
5 番剧页:MAL username + Client ID;轻小说页:VNDB userId R-NAV-03 两个页面的数据
6 AI应用 两篇缺失配图的来源 ✅ 已不需要(2026-09-27 v1.39 补记):那两篇已按发起人指示改写成原创笔记并入站(提交 7219018 → 8264d0d,站内 137 篇 0 draft),不再依赖源导出里那 125 张 page-*.png。仍缺图的是八股/面试那批(12 篇共 20 处,正文已用 > [!WARNING] 标注占位) R-CNT-07
7 DEEPSEEK_API_KEY(可选:LangSmith Key 与项目名) R-AGT-01
8 留言评论系统选型 ✅ 已关闭(v1.89):v1.83 留言页已删 → v1.89 发起人明确「评论区我都删了」→ 不做评论区,阻塞项 B-09 关闭 R-CMT-01
9 主页横幅文案与「链接」下拉要放哪些网站(阻塞项 B-10) ✅ 已关闭(v1.96 收口):两半都由发起人给了并已落地 —— ① 「链接」下拉:v1.57 用他的 8 条重建、v1.61 整批换成第二批 6 条(Gitee / GitHub / CSDN / 洛谷 / Boost 搜索引擎 / HTTP 服务器,顺序即他给的顺序,见 R-NAV-09);② 横幅文案:v1.77 他给了 5 句副标题,一字未改照抄(见 R-BG-03)。这一条不再需要发起人做任何事,阻塞项 B-10 关闭 R-BG-03、R-NAV-09
10 HTTP 协议 那篇 02/04 两张抓包截图里的明文账号密码与真实域名要不要打码 ✅ 已关闭(2026-09-27):发起人看过这两张截图后判定按原样保留(不打码、不裁剪;其余 7 张图本来就没有此问题),阻塞项 B-12 已关闭(口径见 PROJECT_STATUS.md §4 B-12 行与 §7 风险表)。这一条不再需要发起人做任何事 R-CNT-03
11 八股/ 那批 12 篇疑似第三方公开仓库整理稿(外链图床 + 署名问题)要不要署来源/转存图片(阻塞项 B-13) ✅ 已关闭(v1.98,2026-10-10)→ 并于 v1.104 按新的「可以」翻案把图转存了:v1.98 他拍板「就这样」(不署来源、不转存、继续 hotlink);v1.104 他授权「可以」,于是改成"转存到 public/ + 正文改相对路径"(正是当初写明的那条翻案做法)—— 那批里实际带图的是 10 篇,改前 230 处外链 / 226 个去重 URL,226 张正文图已入库(另含 3 个上游 README 徽章,合计 229 个文件)public/images/posts/<slug>/(22.53 MiB,逐张 magic bytes 自证 + sha256 复核),4 处下载不到的原样保留 hotlink、引用一个没删(labuladong.gitee.io / segmentfault.com / s2.loli.net 均 404、upload-images.jianshu.io TLS 证书不匹配)。署名仍然不做(这轮只授权转存,v1.98 的"不署来源"未被推翻、sourceLink 依旧没加)。⚠️ 翻案的直接原因是 v1.98 认下的那个风险在 v1.102 / v1.103 真的兑现了:本机到 cdn.jsdelivr.net 的 TLS 被重置 → /og/* 把整条 pnpm build 拖挂,那两轮只能临时关掉 generateOgImages 才跑完;转存后 pnpm build 完整跑通(175 页、退出码 0、0 条 OG 警告),构建不再依赖别人的图床。这一条不再需要发起人做任何事(阻塞项 B-13 已关闭) R-CNT-05
12 上游示例页:已全部关闭(v1.51 收官) —— 原条目(发起人 2026-09-24「先留着,将来会进行替换,记得提醒」)里的 /projects/、/dynamic/、/gallery/、/booknav/ 四处示例内容已于 v1.47 按发起人批准清单整体删除(见 R-ID-10,不再是"等替换");最后一样 /about/ 关于页也已在 v1.51 换掉:src/content/spec/about.md 由发起人批准的中立版逐字替换(无上游字样 / 无 ::github{} 指令 / 无联系方式 / 不写任何发起人身份信息)。同轮落地的三件配套:① verify:ui 里那个 ABOUT_HTML 例外与 INFO(待办) 已删除,零残留断言现在对产物全集零例外(并用注入实测过它仍会红);② github-card-data.json 的两条 ::github{} 条目随正文指令消失而真删且不再写回(文件落成 {},两次构建 + git status 干净);③ 新增"搜索索引里也搜不到上游字样"4 条断言(正控 1 + 负控 3)。执行方不再需要每轮提醒这件事(已关闭) R-ID-10、R-NAV-07

8. 术语表

术语 含义
主线 学习路线页的 8 个方向(C++ 与 STL / 算法与数据结构 / Linux 系统 / 网络 / MySQL / 面试八股 / 项目梳理 / AI 应用),每条线由若干阶段构成
停靠点 一篇笔记在路线页上的出现位置;同一篇允许跨线复用,所以停靠点数(136)≥ 篇目并集(133)
桌宠 浮动在页面上的爱弥斯像素角色,具备行为与对话能力
看板娘 模板自带的 Live2D / Spine 角色组件,本项目保持关闭
图集 一张包含 9 行 × 8 列动画帧的精灵图
降级 在系统偏好、省流量、后台等条件下自动切换到更轻的表现形式
隔离目录 存放校验不通过文章的目录,不参与构建