From 82c435a9b2e6d850f5a830f2c639bb597585123c Mon Sep 17 00:00:00 2001 From: lhl Date: Sat, 12 Sep 2026 22:53:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(server,ui):=20=E6=A0=B9=E6=B2=BB=20HTML/?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E7=89=88=E6=9C=AC=E9=94=99=E9=85=8D=E2=80=94?= =?UTF-8?q?=E2=80=94/=20=E6=AF=8F=E8=AF=B7=E6=B1=82=E7=8E=B0=E8=AF=BB=20ch?= =?UTF-8?q?at.html+=E7=8E=B0=E7=AE=97=20=3Fv=EF=BC=9B=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=B8=83=E5=B1=80=E5=8F=8C=E4=BF=9D=E9=99=A9=EF=BC=9B?= =?UTF-8?q?Vite=20=E6=8F=92=E4=BB=B6=E5=AE=9E=E6=97=B6=E8=BD=AC=E6=8D=A2(?= =?UTF-8?q?=E5=8E=BB=20index.html)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - frontend/package.json | 1 - frontend/scripts/gen-dev-html.mjs | 21 -------------- frontend/src/chat.css | 4 +-- frontend/vite.config.ts | 44 +++++++++++++++++++++++++++++- src/genesis/server/app.py | 24 ++++++++++------ src/genesis/server/static/chat.css | 4 ++- tests/test_server_api.py | 20 ++++++++++++++ 8 files changed, 83 insertions(+), 36 deletions(-) delete mode 100644 frontend/scripts/gen-dev-html.mjs diff --git a/.gitignore b/.gitignore index 7ac1c3e..86c45fc 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,5 @@ conversations/ frontend/node_modules/ frontend/*.log # ---- 前端 dev 产物(Vite dev 入口 / 构建输出均不入库)---- -frontend/index.html frontend/dist/ frontend/.vite-verify/ diff --git a/frontend/package.json b/frontend/package.json index a449ce9..81440af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,6 @@ "version": "0.0.0", "type": "module", "scripts": { - "predev": "node scripts/gen-dev-html.mjs", "dev": "vite", "build": "node esbuild.config.mjs", "watch": "node esbuild.config.mjs --watch", diff --git a/frontend/scripts/gen-dev-html.mjs b/frontend/scripts/gen-dev-html.mjs deleted file mode 100644 index 1c32433..0000000 --- a/frontend/scripts/gen-dev-html.mjs +++ /dev/null @@ -1,21 +0,0 @@ -// 由规范页面 src/genesis/server/static/chat.html 生成本地开发用 frontend/index.html -// (仅用于 Vite dev;写入 index.html 供 Vite 作为入口,产物不入库) -import { readFileSync, writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; - -const here = dirname(fileURLToPath(import.meta.url)); -const frontend = resolve(here, '..'); -const canonical = resolve(frontend, '../src/genesis/server/static/chat.html'); - -let html = readFileSync(canonical, 'utf8'); -html = html - // 入口脚本 -> 指向 TS 源(Vite HMR) - .replace(/') - // 移除生产 CSS 链接(dev 由 main.ts 导入 chat.css,启用样式 HMR) - .replace(/\s*/, '') - // 去掉资源版本占位 - .replace(/\?v=__ASSET_VER__/g, ''); - -writeFileSync(resolve(frontend, 'index.html'), html); -console.log('generated frontend/index.html from src/genesis/server/static/chat.html'); diff --git a/frontend/src/chat.css b/frontend/src/chat.css index 586bb1d..351e960 100644 --- a/frontend/src/chat.css +++ b/frontend/src/chat.css @@ -66,7 +66,7 @@ body { /* === 侧边栏 (spec §2.1) === */ #sidebar { width: 264px; height: 100vh; background: var(--bg-surface); color: var(--text-primary); - display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; + display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; overflow-x: hidden; border-right: 1px solid var(--border-subtle); transition: width .25s var(--ease); position: relative; @@ -114,7 +114,7 @@ body { #sidebar.collapsed #new-chat .nc-text { display: none; } /* 侧边栏分区(项目 / 其他会话):品牌+新会话固定,两个列表共用一个滚动条 */ .side-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; } -.side-section { display: flex; flex-direction: column; min-height: 0; } +.side-section { display: flex; flex-direction: column; flex: 0 0 auto; min-height: 0; } #sidebar.collapsed .side-scroll { display: none; } .side-head { display: flex; align-items: center; justify-content: space-between; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 31d2e9c..6f454e5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,9 +1,51 @@ -import { defineConfig } from 'vite'; +import { defineConfig, type Plugin } from 'vite'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; // 开发用 Vite dev server:提供 HMR,并把 API / 静态资源代理到本地 FastAPI(:8000)。 // 生产构建仍走 esbuild(npm run build)。 +// +// genesis-dev-html 插件:请求 '/' 时实时读取规范页面 +// src/genesis/server/static/chat.html 并转换为开发入口(脚本指向 /src/main.ts、 +// 去掉生产 CSS link 与版本占位),并注入 Vite HMR 客户端。 +// 这样开发期不存在会过期的中间 HTML,改 chat.html 也会即时整页刷新。 +const frontendDir = dirname(fileURLToPath(import.meta.url)); +const chatHtmlPath = resolve(frontendDir, '../src/genesis/server/static/chat.html'); +const VITE_CLIENT = ''; + +function devHtmlPlugin(): Plugin { + const render = (): string => { + let html = readFileSync(chatHtmlPath, 'utf8'); + html = html + .replace( + /', + ) + .replace(/\s*/, '') + .replace(/\?v=__ASSET_VER__/g, ''); + return html.replace('', VITE_CLIENT + '\n'); + }; + return { + name: 'genesis-dev-html', + configureServer(server) { + server.watcher.add(chatHtmlPath); + server.watcher.on('change', (file) => { + if (resolve(file) === chatHtmlPath) server.ws.send({ type: 'full-reload' }); + }); + server.middlewares.use((req, res, next) => { + const url = (req.url || '').split('?')[0]; + if (req.method !== 'GET' || (url !== '/' && url !== '/index.html')) return next(); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end(render()); + }); + }, + }; +} + export default defineConfig({ root: '.', + plugins: [devHtmlPlugin()], server: { host: '127.0.0.1', port: 5173, diff --git a/src/genesis/server/app.py b/src/genesis/server/app.py index c363f19..fb0682b 100644 --- a/src/genesis/server/app.py +++ b/src/genesis/server/app.py @@ -116,15 +116,21 @@ def create_app( chat_agent = ChatAgent(service=service, fake=is_fake, engine=engine) static_dir = Path(__file__).parent / "static" - # 静态资源版本号:按 mtime 计算,随任何前端文件变更而变化(重启后生效) + # 前端页面:每次请求即时读取 chat.html 并现算资源版本号, + # 保证 HTML 与 /static/chat.css|chat.js 的版本始终一致(改 HTML 免重启)。 _asset_names = ("chat.html", "chat.css", "chat.js", "favicon.svg") - try: - _asset_ver = str(max(int((static_dir / n).stat().st_mtime_ns) - for n in _asset_names if (static_dir / n).exists())) - except Exception: # noqa: BLE001 - _asset_ver = "0" - chat_html = (static_dir / "chat.html").read_text(encoding="utf-8") if (static_dir / "chat.html").exists() else "Genesis Chat" - chat_html = chat_html.replace("__ASSET_VER__", _asset_ver) + + def _asset_version() -> str: + try: + return str(max(int((static_dir / n).stat().st_mtime_ns) + for n in _asset_names if (static_dir / n).exists())) + except Exception: # noqa: BLE001 + return "0" + + def _render_index() -> str: + html_path = static_dir / "chat.html" + html = html_path.read_text(encoding="utf-8") if html_path.exists() else "Genesis Chat" + return html.replace("__ASSET_VER__", _asset_version()) app = FastAPI(title="Genesis API", version=VERSION) @@ -136,7 +142,7 @@ def create_app( @app.get("/", response_class=HTMLResponse) def index(): - return HTMLResponse(chat_html, headers={"Cache-Control": "no-store", "Pragma": "no-cache"}) + return HTMLResponse(_render_index(), headers={"Cache-Control": "no-store", "Pragma": "no-cache"}) @app.get("/favicon.ico") def favicon(): diff --git a/src/genesis/server/static/chat.css b/src/genesis/server/static/chat.css index 26c87ed..f10b9c4 100644 --- a/src/genesis/server/static/chat.css +++ b/src/genesis/server/static/chat.css @@ -90,7 +90,8 @@ body { display: flex; flex-direction: column; flex-shrink: 0; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; border-right: 1px solid var(--border-subtle); transition: width .25s var(--ease); position: relative; @@ -217,6 +218,7 @@ body { .side-section { display: flex; flex-direction: column; + flex: 0 0 auto; min-height: 0; } #sidebar.collapsed .side-scroll { diff --git a/tests/test_server_api.py b/tests/test_server_api.py index cbfa65e..cf25287 100644 --- a/tests/test_server_api.py +++ b/tests/test_server_api.py @@ -388,3 +388,23 @@ def test_root_has_icon_link(client): assert r.status_code == 200 assert 'rel="icon"' in r.text assert "/static/favicon.svg" in r.text + +def test_root_fresh_and_wrapped(client): + # / 每请求即时读 HTML:应含 .side-scroll 包裹,且不含版本占位残留 + r = client.get("/") + assert r.status_code == 200 + assert 'class="side-scroll"' in r.text + assert "__ASSET_VER__" not in r.text + + +def test_root_version_matches_disk(client): + # / 注入的 ?v= 应等于当前磁盘 mtime 版本(HTML 与静态资源同源) + import re + from pathlib import Path + r = client.get("/") + m = re.search(r"chat\.css\?v=(\d+)", r.text) + assert m, r.text[:300] + static_dir = Path(__file__).resolve().parents[1] / "src" / "genesis" / "server" / "static" + names = ("chat.html", "chat.css", "chat.js", "favicon.svg") + expect = str(max(int((static_dir / n).stat().st_mtime_ns) for n in names if (static_dir / n).exists())) + assert m.group(1) == expect