Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 26 additions & 33 deletions .dump/_experience_browser_check.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""V3 产品体验层的真实浏览器冒烟:首页叙事、快捷搜索、图谱阅读器。"""
"""Classic UI 真实浏览器冒烟:旧导航/旧首页/图谱仍可用,V2/V3 覆盖层不再出现。"""
from __future__ import annotations

import os
Expand Down Expand Up @@ -47,7 +47,6 @@ def chrome_dump(path: Path, hash_: str = "", budget: int = 7000) -> str:


def rendered_only(dom: str) -> str:
# 产物把组件 HTML 模板字符串内联在 <script> 中;先剥掉源码,避免字符串断言假通过。
dom = re.sub(r"<script\b[^>]*>.*?</script>", "", dom, flags=re.I | re.S)
dom = re.sub(r"<style\b[^>]*>.*?</style>", "", dom, flags=re.I | re.S)
return dom
Expand All @@ -59,60 +58,54 @@ def assert_has(dom: str, *patterns: str):
raise AssertionError("缺少渲染结果:%s" % p)


def assert_not_has(dom: str, *patterns: str):
for p in patterns:
if re.search(p, dom, re.S):
raise AssertionError("经典 UI 不应出现:%s" % p)


def check_home():
dom = rendered_only(chrome_dump(TARGET))
assert_has(
dom,
r'<button type="button" class="command-trigger"[^>]*aria-controls="commandPalette"',
r'<section class="v3-story panel"[^>]*aria-labelledby="v3StoryTitle"',
r'id="v3StoryTitle">先读懂这份知识图谱,再进入细节',
r'地点已定位', r'事件可纪年',
)
print("ok V3 首页叙事 + 全局搜索入口")
for label in ("总览", "分布", "图谱", "地点", "地图", "人物", "事件", "关系", "时间轴", "帝王", "年谱", "洞察"):
assert_has(dom, r'<button data-view="[^"]+"[^>]*>' + label + r'</button>')
assert_has(dom, r'class="summary-hero"', r'class="metric-grid"')
assert_not_has(dom, r'class="nav-cluster"', r'class="v3-story', r'class="command-trigger"')
print("ok Classic UI 首页 + 原 12 视图导航")


def check_command_palette():
def check_no_command_overlay():
doc = TARGET.read_text(encoding="utf-8")
probe = """<script>
setTimeout(function(){
document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true,bubbles:true}));
var i=document.getElementById('commandInput');
if(i){i.value='于谦';i.dispatchEvent(new Event('input',{bubbles:true}));}
},80);
</script>\n"""
if "</body>" not in doc:
raise AssertionError("产物缺少 </body>")
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "probe.html"
p.write_text(doc.replace("</body>", probe + "</body>", 1), encoding="utf-8")
dom = rendered_only(chrome_dump(p, budget=7500))
assert_has(
dom,
r'<div id="commandPalette" class="command-shell">',
r'<button type="button" role="option"[^>]*class="command-item active"',
r'<strong>于谦</strong>',
r'<span class="command-kind">人物</span>',
)
print("ok Ctrl+K 全局搜索 → 于谦")
assert_not_has(dom, r'id="commandPalette"', r'class="command-shell"')
print("ok Classic UI 不注入命令面板")


def check_graph_reader():
def check_graph_view_without_reader_overlay():
dom = rendered_only(chrome_dump(TARGET, "#view=visuals&net=full", 9000))
# 复用主浏览器套件已经稳定验证的图谱 DOM 契约,不依赖不存在的装饰类名。
assert_has(
dom,
r'<div class="v3-graph-reader" data-mode="full"[^>]*>',
r'人物总图怎么读',
r'data-v3-node-search',
r'高连接入口',
r'data-v3-focus=',
r'只突出一跳邻域',
r'id="visuals" class="view active"',
r'<option value="full" selected="">',
r'id="fullNet" style="display:block"',
r'<canvas id="fullGraph"[^>]*role="img"',
r'aria-label="全书人物关系图:\d+ 人、\d+ 条人物关系',
)
print("ok 人物总图阅读器 + 邻域聚焦入口")
assert_not_has(dom, r'class="v3-graph-reader"', r'data-v3-node-search')
print("ok 原图谱视图可用且无 V3 reader 覆盖层")


if __name__ == "__main__":
if not TARGET.exists():
raise SystemExit("目标文件不存在:%s" % TARGET)
check_home()
check_command_palette()
check_graph_reader()
check_no_command_overlay()
check_graph_view_without_reader_overlay()
142 changes: 142 additions & 0 deletions .dump/_web_browser_check_classic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
"""Classic UI + V13 现代在线交付的真实 Chrome 回归。

复用 V12 在线分层套件中与可见体验层无关的场景,只替换四条曾依赖
V3 command palette / graph reader 的断言:
- 首页:保持 boot,且不出现 V2/V3 可见覆盖层;
- search-index:直接调用底层 loader API,证明后端能力保留但不暴露旧命令面板;
- 人物详情:使用原生 deep link,而不是命令面板结果点击;
- 图谱:验证原始 full graph DOM,而不是 V3 reader。
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

import _web_browser_check as base

TARGET = (Path(sys.argv[1]) if len(sys.argv) > 1 else Path("dist/full/index.html")).resolve()
base.TARGET = TARGET


def check_home():
dom = base.chrome_dump(TARGET)
base.assert_has(
dom,
r'<html[^>]*data-ming-data="boot"',
r'data-ming-chunks=""',
r'data-ming-search="idle"',
r'<nav class="tabs"',
r'<button data-view="overview"[^>]*>总览</button>',
)
for forbidden in (r'class="command-trigger"', r'class="v3-story panel"', r'class="nav-cluster"'):
if re.search(forbidden, dom, re.S):
raise AssertionError("Classic UI 首页不应出现:%s" % forbidden)
if 'data-ming-data-chunk=' in dom or 'data-ming-search-chunk="index"' in dom:
raise AssertionError("首页意外加载按需块")
print("ok Classic UI 在线首页仅 boot")


def _search_loader_probe():
doc = TARGET.read_text(encoding="utf-8")
probe = """<script>
setTimeout(function(){
if(typeof window.__MING_ENSURE_SEARCH_INDEX!=='function'){
document.documentElement.dataset.classicSearchProbe='missing';return;
}
window.__MING_ENSURE_SEARCH_INDEX('classic-test').then(function(){
document.documentElement.dataset.classicSearchProbe='ready';
}).catch(function(){document.documentElement.dataset.classicSearchProbe='error';});
},120);
</script>\n"""
p = TARGET.parent / ".classic-search-probe.html"
p.write_text(doc.replace("</body>", probe + "</body>", 1), encoding="utf-8")
return p


def check_search_backend_only():
p = _search_loader_probe()
try:
dom = base.chrome_dump(p, budget=9000)
finally:
p.unlink(missing_ok=True)
base.assert_has(
dom,
r'data-classic-search-probe="ready"',
r'data-ming-data="boot"',
r'data-ming-search="ready"',
r'data-ming-search-chunk="index"',
)
if 'data-ming-data-chunk=' in dom:
raise AssertionError("只测试 search-index 时不应加载领域块")
if re.search(r'id="commandPalette"|class="command-shell"', dom, re.S):
raise AssertionError("Classic UI 不应恢复可见命令面板")
print("ok Classic UI 保留 lazy search-index 后端,不暴露命令面板")


def check_person_detail_deep_link():
dom = base.chrome_dump(
TARGET,
"#view=characters&person=%E4%BA%8E%E8%B0%A6&detail=1",
11000,
)
base.assert_has(
dom,
r'data-ming-chunks="characters,character-detail-13,insight"',
r'data-ming-data-chunk="character-detail-13"',
r'data-ming-data-chunk="insight"',
r'id="characters" class="view active"',
r'<dialog id="detailDialog" open',
r'于谦',
r'关系',
)
base.assert_only_shard(dom, base.CHAR_SHARDS, 'character-detail-13', '人物')
base.assert_no_chunk(
dom,
'events', *base.EVENT_SHARDS, 'locations', *base.LOC_SHARDS,
'place-chapters', 'voyages', 'relations', 'time', 'lifespans', 'graphs', 'meta',
)
print("ok Classic UI 于谦 deep link 只取人物目标 shard + insight")


def check_graph_classic():
dom = base.chrome_dump(TARGET, "#view=visuals&net=full", 9500)
base.assert_has(
dom,
r'data-ming-chunks="graphs"',
r'data-ming-data-chunk="graphs"',
r'id="visuals" class="view active"',
r'id="fullNet" style="display:block"',
r'<canvas id="fullGraph"[^>]*role="img"',
r'aria-label="全书人物关系图:\d+ 人、\d+ 条人物关系',
)
if re.search(r'data-v3-node-search|class="v3-graph-reader"', dom, re.S):
raise AssertionError("Classic UI 图谱不应出现 V3 reader")
base.assert_no_chunk(
dom,
'characters', *base.CHAR_SHARDS, 'locations', *base.LOC_SHARDS,
'events', *base.EVENT_SHARDS, 'time', 'lifespans', 'insight', 'meta',
)
print("ok Classic UI 原图谱仅 graphs,无 V3 reader")


if __name__ == "__main__":
if not TARGET.exists():
raise SystemExit("目标文件不存在:%s" % TARGET)

check_home()
check_search_backend_only()
base.check_character_index()
check_person_detail_deep_link()
base.check_location_index()
base.check_place_detail()
base.check_map_core_only()
base.check_location_chapter_mode()
base.check_voyage_mode()
base.check_chronicle()
check_graph_classic()
base.check_timeline()
base.check_dynasty()
base.check_event_index_core_only()
base.check_event_deep_link_one_shard()
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:

jobs:
check:
name: 语法 · 不变量 · 构建 · V13 浏览器
name: 语法 · 不变量 · 构建 · Classic UI / V13 浏览器
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -35,7 +35,7 @@ jobs:
echo "未发现原书全文文件 ✓"

- name: Python 语法检查
run: python -m compileall -q src tests .dump/_web_browser_check.py .dump/_relation_browser_check_v13.py
run: python -m compileall -q src tests .dump/_web_browser_check.py .dump/_web_browser_check_classic.py .dump/_experience_browser_check.py .dump/_relation_browser_check_v13.py

- name: 前端动态统计口径规范化
run: |
Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
python src/sync_theme.py
git diff --check -- web/template/index.html

- name: V3 / V13 产品体验镜像同步冒烟
- name: Classic UI / V13 体验镜像同步冒烟
run: |
python src/sync_experience.py
python src/sync_experience.py --check
Expand Down Expand Up @@ -127,11 +127,11 @@ jobs:
- name: 单文件无头浏览器自检
run: python .dump/_browser_check.py .ci/standalone.html

- name: V3 单文件产品体验浏览器自检
- name: Classic UI 单文件浏览器自检
run: python .dump/_experience_browser_check.py .ci/standalone.html

- name: V12 在线分层浏览器自检
run: python .dump/_web_browser_check.py .ci/site/index.html
- name: Classic UI 在线分层浏览器自检
run: python .dump/_web_browser_check_classic.py .ci/site/index.html

- name: V13 关系索引在线浏览器自检
run: python .dump/_relation_browser_check_v13.py .ci/site/index.html
16 changes: 11 additions & 5 deletions src/sync_lazy_data.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""把 V9 人物详情分片钩子安全写入 V3 experience.js。
"""兼容旧 V9 命令面板钩子;Classic UI 下保持 no-op。

V6 拆出 search-index,V7 改为实体领域加载,V9 再让人物实体把姓名传给 loader,
从而只加载该人物对应的 character-detail shard。
历史上本脚本会把实体分片加载钩子写入 V3 experience.js 的命令面板。
Classic UI 已主动撤掉可见命令面板,因此现代实体分片由 data-loader.js / lazy-data.js
独立负责;此脚本在 Classic UI 源上应幂等返回,不再强制恢复旧 UI。
"""
from __future__ import annotations

Expand All @@ -17,6 +18,7 @@
"/* V7_VIEW_CHUNK_COMMAND_DATA */",
)
MARKER = "/* V9_SHARDED_CHARACTER_DETAIL_COMMAND_DATA */"
CLASSIC_MARKER = "Classic UI mode."


def _sub_once(text: str, pattern: str, replacement: str, label: str) -> str:
Expand All @@ -27,6 +29,10 @@ def _sub_once(text: str, pattern: str, replacement: str, label: str) -> str:


def render_source(source: str) -> str:
# Classic UI 不再提供命令面板;实体分片逻辑已经完全位于 loader/gate,
# 因此这里必须 no-op,不能为了历史同步器重新注入可见 UI。
if CLASSIC_MARKER in source:
return source
if MARKER in source:
return source
old = next((marker for marker in OLD_MARKERS if marker in source), None)
Expand Down Expand Up @@ -65,13 +71,13 @@ def render_source(source: str) -> str:


def main(argv=None) -> int:
parser = argparse.ArgumentParser(description="同步 V9 人物详情分片钩子到 experience.js")
parser = argparse.ArgumentParser(description="同步历史命令面板实体分片钩子;Classic UI 下 no-op")
parser.add_argument("--check", action="store_true", help="只检查,不写入")
args = parser.parse_args(argv)
source = TARGET.read_text(encoding="utf-8")
updated = render_source(source)
if updated == source:
print("V9 人物详情分片命令钩子已同步")
print("实体详情分片钩子已满足当前 UI 模式")
return 0
if args.check:
print("V9 人物详情分片命令钩子未同步;请运行 python src/sync_lazy_data.py")
Expand Down
7 changes: 4 additions & 3 deletions sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
// v20:V11 时间分层:lifespans 从 timeline 独立;年谱只缓存 characters + lifespans。
// v21:V12 事件分层:events 仅摘要,单事件详情进入 8 个 event-detail-*;时间轴/帝王只缓存 time。
// v22:V13 关系分层:relations 仅可见 core;relation-meta 只在完整 DATA 路径按需缓存。
// v23:恢复 pre-V1 classic UI;只换视觉/导航层,现代分片交付与懒加载保持不变。
const CACHE_PREFIX = 'ming-report-';
const CACHE = CACHE_PREFIX + 'v22';
const CACHE = CACHE_PREFIX + 'v23';

self.addEventListener('install', () => { self.skipWaiting(); });
self.addEventListener('activate', (event) => {
Expand All @@ -26,8 +27,8 @@ self.addEventListener('fetch', (event) => {
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;

// V13 仍只缓存用户真实走过的路径:关系索引只取 relations core;full DATA 才取 relation-meta;
// 事件、人物、地点详情继续只缓存被真正访问的对应 shard。
// classic UI 只改变可见外观;V13 交付策略保持:关系索引只取 relations core;
// full DATA 才取 relation-meta;事件、人物、地点详情继续只缓存真正访问的对应 shard。
event.respondWith((async () => {
const cache = await caches.open(CACHE);
const cached = await cache.match(req);
Expand Down
Loading