Files
rhinelab-ui/verification/ARCHIVE-INTEGRATION.md
T

13 KiB
Raw Blame History

本地归档接入验证(2026-09-12)

本轮把 E:\归档(约 90 GB / 45,055 个文件 / 4,937 个目录)接入本项目:四十条精选档案改为由真实归档生成,新增 LOCAL ARCHIVE 面板做全量检索与安全写操作。

实现说明见 ../docs/LOCAL-ARCHIVE.md,数据维护见 ../content/README.md。

新增与改动

文件 类型 说明
archive.config.json 新增 归档根目录、端口、索引与日志位置
content/curated.mjs 新增 四十条档案的手写部分与对应的真实 path
scripts/archive-source.mjs 新增 扫描归档,生成 archives.json、archive-index.json、40 个 TXT
scripts/archive-service.mjs 新增 回环服务:令牌、路径围栏、检索、读写操作、目录监视
scripts/archive-dev.mjs 新增 一条命令同时启动服务与 Vite
src/archive-service.ts 新增 浏览器端客户端,含静态快照降级
src/archive-panel.ts 新增 检索与归档操作面板
src/archive-panel.css 新增 面板样式,全部走 --theme-* 变量
scripts/archive-content.mjs 改动 source 接受 file:;TXT 模板改为归档口径
src/main.ts 改动 导航新增 LOCAL ARCHIVE 按钮、点击分支、面板实例化;详情页 EXPORT 下载改为 SHOW IN FOLDER 定位
src/data.ts 改动 ArchiveRecord 增加可选的 path 与 stats(生成物写入的真实归档信息)
src/style.css、src/responsive.css 改动 .export-button 改为双语按钮并加宽;新增被截断的 .detail-path
vite.config.ts 改动 server/preview 增加 /archive-api 代理并注入令牌
package.json 改动 新增 dev:archive、archive:snapshot、archive:serve;钩子改跑 archive-source.mjs
.gitignore 改动 忽略 .archive-token
content/README.md 改动 说明 archives.json 现在是生成物;path 的两个用途
docs/LOCAL-ARCHIVE.md、verification/ARCHIVE-INTEGRATION.md 新增 用法、安全边界与实测记录

没有改动的:src/scene.ts、src/archive-loop.ts 与阵列数学。五列、每列八份、X-0NN 稳定编号的约束原样保留,archive-content.mjs 的三条硬校验(五分类 / 四十条 / 每类八条)未放宽。

证据

1. 数据校验与构建

$ npm run check:content
ℹ tests 18   ℹ pass 18   ℹ fail 0

$ npx tsc --noEmit
exit=0

$ npm run build
归档快照已更新:40 条精选档案,45055 个文件 / 4937 个目录 / 90.03 GB
dist/assets/index-KZVOVeb_.js   993.68 kB │ gzip: 301.66 kB
✓ built in 2.48s
Offline release 22cf358e6368842a: 818 files, 33.8 MiB.
build exit=0

check:content 的 18 项包含"下载 TXT 与 archiveText 逐字节一致(含 BOM)",即 40 个 TXT 与 archives.json 同步;放宽 source 协议后,rejects unsafe URL(javascript:)与 rejects invalid URL(example.com)两项仍如期失败,说明放宽没有引入注入面。

2. 服务接口实测

用例 结果
无令牌 GET /status 401,正确拒绝
GET /status {"root":"E:\\归档","watching":true,"totals":{"files":45055,"dirs":4937,...},"sevenZip":true}
POST /open {"path":"../Windows/system.ini"} 403(越界拦截)
POST /reveal {"path":"C:\\Windows\\win.ini"} 403(绝对路径拦截)
POST /open {"path":"不存在的目录/foo.txt"} 404
POST /plan {"op":"mkdir"} → apply blocked=null,目录创建成功
POST /plan {"op":"move"} → apply files=1 bytes=18,移动成功,源消失
目标已存在时 plan move blocked="目标已存在,请换一个名字"
plan move 到 D:\ 403(目标越界拦截)
POST /plan {"op":"extract","from":"07_日志与排查/LOG.7z"} → apply 解出 4 个文件,与包内条目数一致
POST /apply 缺少 confirm 400,且未创建任何目录
GET /index(gzip) 7,767,411 字节 → 698,629 字节(约 9 倍)
POST /refresh 返回新的快照时间

操作日志按预期落盘:

{"at":"2026-09-12T02:10:44.855Z","op":"mkdir","from":null,"to":"_测试_归档操作","result":"ok"}
{"at":"2026-09-12T02:10:44.872Z","op":"move","from":"_测试源文件.txt","to":"_测试_归档操作/测试源文件.txt","result":"ok"}
{"at":"2026-09-12T02:10:44.946Z","op":"extract","from":"07_日志与排查/LOG.7z","to":"_测试_归档操作/LOG_解压","result":"ok"}

测试产物已全部删除,归档中已无 _测试_* 条目。

3. 自动更新

在归档根新建 _自动更新测试.txt,不调用任何刷新接口,等待 8 秒后检索:

索引命中 1 条:_自动更新测试.txt
最近一次快照:09/12/2026 02:11:06

删除该文件后再次等待,快照随之更新为 02:11:14。递归监视器与防抖重生生效。

4. 开发链路

$ npm run dev
归档快照已更新:40 条精选档案,45055 个文件 / 4937 个目录 / 90.03 GB
VITE v7.3.6  ready in 426 ms
➜  Local:   http://127.0.0.1:5174/     (5173 已被占用)

GET http://127.0.0.1:5174/                         → 200, text/html
GET http://127.0.0.1:5174/archive-api/status       → 200, 返回真实 status(令牌由代理注入)
GET http://127.0.0.1:5174/archive-api/search?q=runs.7z → 命中 2 条,含 01_人工智能/01_训练产物/runs.7z
GET http://127.0.0.1:5174/archive-index.json       → 200(静态降级路径可用)
GET http://127.0.0.1:5174/src/archive-panel.ts     → 200, 57,329 字节(Vite 转译无错)
GET http://127.0.0.1:5174/src/archive-service.ts   → 200, 15,727 字节

5. 产物复核

dist/assets/index-KZVOVeb_.js : LOCAL ARCHIVE / local-archive / la-surface /
                               archive-index.json / archive-api / 正在检测归档服务  全部命中
dist/assets/index-DeuQtmYI.css : .la-surface 命中
dist/archives/*.txt : 40 个
dist/archive-index.json : 7.41 MB(未进入 PWA 预缓存,818 files / 33.8 MiB 与改动前同量级)

6. 详情页 EXPORT 改为定位文件位置

按用户要求,详情页右下角的 EXPORT(下载 TXT)改为 SHOW IN FOLDER / 文件位置:直接用该档案的真实 path 调 /reveal,在资源管理器中打开其所在位置。

改动点:

  • src/main.ts:513 的 <a class="export-button" href="archives/…" download> 换成 <button data-action="reveal-archive">;新增 revealSelectedArchive(),先 fetchStatus() 判断服务是否在线,离线时提示"需要本地归档服务:请运行 npm run dev:archive",不静默失败。
  • src/data.ts 的 ArchiveRecord 增加可选 path 与 stats。
  • .export-button 加宽到 190px 并把 <span> 从箭头字号改回双语副标题(11px、--theme-muted);紧凑/竖屏布局改为 132px。这处 span 样式只被该按钮使用,无外溢。
  • 顺带修一处因接入本地数据而失效的地方:详情页脚注原本是 <a href="${r.source}">设定参考 ↗</a>,而 source 现在是 file: 链接,浏览器不允许网页跳转 file:。改为显示归档内相对路径文本(过长省略,完整值在 title),新增 .detail-path。

验证:

40 条档案全部带 path(40/40),示例 01_人工智能\01_训练产物\runs.7z
POST /reveal {"path":"01_人工智能\\01_训练产物\\runs.7z"}  → HTTP 200, action=reveal
npx tsc --noEmit  → exit=0
npm run build     → exit=0, dist/assets/index-BoST1KaL.js 994.12 kB
开发服务器 /src/main.ts 已含 reveal-archive / SHOW IN FOLDER / revealSelectedArchive / detail-path
产物:SHOW IN FOLDER、reveal-archive、需要本地归档服务、.detail-path 全部命中;"EXPORT <span>" 已无残留

public/archives/*.txt 仍会生成(check:content 依赖它们),只是详情页不再有下载入口。

7. 修复"归档新增了却搜不到"

用户报告:外部智能体往 E:\归档 导入了三个大包(7.26 GB + 6.63 GB + 6.71 GB),界面里搜不到。

诊断过程(先证伪,再改代码):

检查 结果
文件是否在磁盘 三个包都在,01_人工智能 34.9 → 41.45 GB,03_代码与项目 7.12 → 20.69 GB
快照是否过期 否。NM2_原版...7z mtime 14:34:11 → 快照生成 14:34:25,14 秒后自动重建
服务是否报错 /status 的 lastError 为空
索引内容 完整复刻面板取数路径(scripts/_diag-search.mjs):dpskw → 3 条、NM2 → 1 条,服务路径与静态兜底都命中

结论:索引、监视、检索逻辑全部正确,bug 在前端状态生命周期。

根因:LocalArchivePanel 的索引只在「页面加载后第一次打开面板」时取一次,loaded 标志置真后永不复位;close() 还会 unsubscribe() 退掉服务端事件订阅,而重新订阅只发生在上面的 loadIndex() 里。因此只要用户在文件加进来之前开过面板,之后重开既不重取也不重订阅,永远是旧数据,除非整页刷新。这是实现缺陷,不是配置问题。

修复:

  1. src/archive-service.ts 新增共享索引缓存 loadArchiveIndex({ maxAgeMs }) 与 searchArchive() / topLevelNames() / invalidateArchiveIndex()。maxAgeMs: 0 表示不信任缓存、总是重取。
  2. 面板每次打开都重新取索引(服务在线时 maxAgeMs: 0;离线时退回静态快照只取一次),prepare() 末尾调 ensureSubscription() 重新挂事件流,close() 不再退订。写操作成功后 invalidateArchiveIndex() 丢弃共享缓存。
  3. 顺带清掉面板自己维护的 entries / lowerPaths 两份派生数组,改由 searchArchive() 统一处理(小写路径用 WeakMap 按索引实例缓存)。

主检索接入全量归档(用户批准的第二项):主检索弹窗(/ 或 ARCHIVE INDEX)在四十条列表下方新增「LOCAL ARCHIVE / 本地归档」一栏,显示全量索引最多 5 条命中,每条可【定位】【打开】,点「全部 →」关闭弹窗并打开 LOCAL ARCHIVE 面板并带入检索词。两条路径共用同一份内存索引,不会重复下载 7.4 MB。renderLocalHits() 带请求令牌,快速输入时丢弃过期结果;loadArchiveIndex({ maxAgeMs: 15_000 }) 让结果在 15 秒内保持新鲜。

布局:.terminal-modal.has-local-hits .search-results 由 387px 降到 240px,新增 .local-hits 140px,弹窗总高不变。

验证:

node scripts/_diag-search.mjs
  取数路径   : 服务 /archive-api/index
  条目数     : 49997
  'dpskw' -> 3 条   03_代码与项目/V2_dpskw-备份
  'NM2'   -> 1 条   01_人工智能/01_训练产物/NM2_原版_..._20260912_142722.7z
  静态兜底快照含 dpskw: 3 条,含 nm2: 1 条

npx tsc --noEmit  → exit=0
npm run build     → exit=0

同期补的文档:00_索引\归档索引.md 补登记 NM2_原版(另一智能体只写了 V2_dpskw)、更新分区体量与合计、新增「八、变更记录」逐笔列出整理后的导入(+20.12 GB);AGENTS.md §3.2 从「主界面永远搜不到」改写为「三维阵列看不到、但主检索下方本地归档栏能搜到」,并加入"先查服务与快照时间,再怀疑代码"的排查顺序。

遗留:scripts/_diag-search.mjs 是一次性诊断脚本,验证后已删除。

尚未验证

  • 浏览器内的实际呈现与交互:项目未安装 Playwright / Puppeteer,scripts/check-*.mjs 均为纯 Node 逻辑测试,无法自动化驱动页面。面板与详情页按钮的布局、明暗配色、按钮手感,以及资源管理器窗口在用户多显示器环境下的落点,需要人工在 npm run dev:archive 下确认。
  • /open 的成功路径:/reveal 的成功路径已用真实档案路径实测(弹出资源管理器并选中 runs.7z),/open 只验证了越界与不存在两种拦截,未实际唤起默认程序。
  • 大目录解压的耗时:仅用 LOG.7z(0.4 MB)验证了解压路径。对 runs.7z(26 GB)这类大包,/plan 只报包体大小,不会预估解压后的体积与耗时。
  • 非默认浏览器 / 无文件关联时的打开行为:取决于系统文件关联。

已知取舍

  • public/archive-index.json 约 7.4 MB 会随构建进入 dist。它是快照而非源码,且不进离线预缓存;若要进一步瘦身,可去掉每个条目的 mtime 或改由服务端按需检索。
  • 面板一次最多渲染 200 行命中,超出部分只计数不渲染,避免一次插入过多 DOM。
  • 服务不可用时面板为只读,这是刻意降级而非缺陷。
  • 归档内的 .archive-token 不存在:令牌写在项目根目录,不在归档内,避免污染归档。