From 3602569af81f607c2f576122c1520e16c5089acd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 21:13:21 +0000 Subject: [PATCH] =?UTF-8?q?U-1:=20/studio=20=E9=80=80=E5=A0=B4=20=E2=80=94?= =?UTF-8?q?=20=E7=A7=BB=E9=99=A4=E7=9B=B4=E9=80=A3=20Gemini=20SPA=EF=BC=8C?= =?UTF-8?q?307=20=E8=BD=89=E5=9D=80=E8=87=B3=20/app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /studio 的原 infoCard 前端 client-side 直連 Gemini,繞過後端計費 + review gate;其源碼不在本 repo 無法「改走後端」,且 U-2 已在 /app 補齊視覺站對等 (逐區 refine + 區域選擇)。依拍板「先補 /app 對等再退場 /studio」執行退場: - server/main.py:移除 /studio StaticFiles mount + SPA fallback,改無條件 GET /studio 與 /studio/{path} → 307 轉址 /app/(暫時轉址不被永久快取、保留 反悔餘地;無條件註冊=即使殘留 web/studio build 也不再被服務)。一舉關掉 繞過計費 + 繞過審查漏洞、舊書籤不 404。 - 移除 banner helper now-dead 的 studio 參數(僅 /ui 仍用 legacy banner)。 - landing.html:移除 /studio legacy 卡片 + 連帶 .warn CSS。 - tests/test_legacy_banner.py:banner 注入 4 測 + /studio 根與深連結 307→/app/ 2 測(TestClient 不依賴 build 產物)。 本機全套 2706 passed(剩 3 個 QR/journal 字型像素為容器缺 Noto CJK 假象, CI 權威)。 https://claude.ai/code/session_01PbVQ2LtjgjfgpujfszRG7B --- docs/PRODUCT_READINESS.md | 23 +++++++++++----- server/main.py | 54 +++++++++++++++---------------------- server/static/landing.html | 9 ------- tests/test_legacy_banner.py | 46 ++++++++++++++++++++----------- 4 files changed, 69 insertions(+), 63 deletions(-) diff --git a/docs/PRODUCT_READINESS.md b/docs/PRODUCT_READINESS.md index e0384e5..18a64a9 100644 --- a/docs/PRODUCT_READINESS.md +++ b/docs/PRODUCT_READINESS.md @@ -193,9 +193,17 @@ > client-side 直連 Gemini → 繞過後端計費 + 繞過 review gate。盤點見 > [EDUSTUDIO_UI_WIRING.md](EDUSTUDIO_UI_WIRING.md)。多為前端工。 -- [ ] 🔴 **U-1 `/studio` 直連 Gemini 改走後端**(offline,前端 + 確認後端端點)— 把 `/studio` - 仍 client-side 呼叫 Gemini 的路徑改打 `/api/generate` 等後端端點(後端大多現成),堵住 - 「繞過計費 + 繞過審查」漏洞。或者若 U-3 直接退場 /studio,則本項併入「功能搬進 /app」。 +- [x] 🔴 **U-1 `/studio` 直連 Gemini 改走後端 → 退場**(offline)— ✅ 2026-06-15 完成 + (走「退場」分支)。`/studio` 的原 infoCard 前端 **client-side 直連 Gemini**(繞過後端計費 + + 繞過 review gate),其**源碼不在本 repo**,無法「改走後端」;且 U-2 已把唯一「大」缺口(海報/ + 圖卡逐區 refine + 區域選擇)在 `/app` 補齊對等。故依拍板「先補 /app 對等再退場 /studio」執行 + 退場:`server/main.py` 移除 `/studio` 的 StaticFiles mount + SPA fallback,改**無條件** `GET + /studio` 與 `GET /studio/{path}` → **307 轉址 `/app/`**(暫時轉址=不被瀏覽器永久快取、保留 + 反悔餘地;無條件註冊=即使殘留 `web/studio` build 也不再被服務)。一舉關掉「繞過計費 + 繞過 + 審查」漏洞、舊書籤/連結不 404。`landing.html` 移除 `/studio` legacy 卡片;banner helper 移除 + now-dead 的 `studio` 參數(僅 `/ui` 仍用)。補/改 `tests/test_legacy_banner.py`(banner 注入 + 4 測 + `/studio` 根與深連結 307→`/app/` 2 測,TestClient 不依賴 build 產物)。本機全套 2706 + passed(剩 3 個 QR/journal 字型像素為容器缺 Noto CJK 假象,CI 權威)。 - [x] 🟡 **U-2 `/app` 補齊 `/studio` 缺的視覺功能 — 含逐區 refine**(offline,**拍板要做 2026-06-07**)— 盤點顯示 `/app` 視覺站缺「海報/圖卡逐區 refine、區域選擇」(後端 refine 圖卡未移植 = 唯一「大」缺口)。**定案:移植後端逐區 refine + 前端區域選擇 UI**(不是首發 @@ -215,9 +223,12 @@ regenerateImage}`、讀 `data.section`、patch 回 `result.data.sections[idx]`。**純前端一檔** (`frontend/edustudio/app.jsx`),`npm run build`(vite/node22)編譯通過;後端 8 測 (`test_infocards_refine.py`)已涵蓋端點,視覺由人後驗。至此 U-2 ①②③ 到齊。 -- [ ] 🟡 **U-3 `/ui` `/studio` 標 legacy / 退場**(offline)— 在舊 UI 頁頂加 banner「此介面 - 將退場,請用 /app」+ README/介面表標 legacy。完全移除 build 產物等 `/app` 功能對等確認後 - 再做(避免反悔)。 +- [x] 🟡 **U-3 `/ui` `/studio` 標 legacy / 退場**(offline)— ✅ 完成。banner 部分於 **PR #33 + (2026-06-08)** 落地:`/ui`·`/studio` 頁頂注入「此介面即將退場,請改用 /app」退場 banner + + `landing.html` 舊版介面卡片標 `legacy`(先前 checkbox 漏勾,此處補正)。`/studio` 進一步於 + **U-1(2026-06-15)退場**=307 轉址 `/app/`(已過 `/app` 功能對等確認,呼應本項「完全移除 + build 產物等對等確認後再做」)。`/ui`(影片站,已與 `/app` 對等)暫保留 banner 過渡,待後續 + 小 PR 視情況比照退場。 - [ ] 🟡 **U-4 成本面板真實化收尾**(offline,接 Phase 4)— 現況部分 mock。等 Phase 4 計費 補完後,把成本面板數字接真 `/api/usage`,移除「示意」假數字。 - [ ] 🟢 **U-5 發布站多語上傳驅動**(GATE)— 現況多語版本選擇只是視覺。要驅動真多語上傳碰 diff --git a/server/main.py b/server/main.py index e8151f0..6fb8578 100644 --- a/server/main.py +++ b/server/main.py @@ -63,40 +63,34 @@ # 對齊 vite.config.ts 的 build outDir WEB_DIST = PROJECT_ROOT / "web" / "dist" -# eduStudio 合併 C-4: infoCard 前端 build(base=/studio/)serve 在此。 -WEB_STUDIO = PROJECT_ROOT / "web" / "studio" # eduStudio 合併 C-4: 統一 app(Claude Design 設計、infoCard React19 build,base=/app/)。 WEB_EDUAPP = PROJECT_ROOT / "web" / "eduapp" # eduStudio 合併 C-4 方案 A: 統一入口 landing(外觀可獨立替換,待 Claude Design 重做)。 LANDING_PAGE = PROJECT_ROOT / "server" / "static" / "landing.html" -def _legacy_banner_html(*, studio: bool) -> str: - """legacy UI (/ui, /studio) 頂部退場提示 banner(U-3)。 +def _legacy_banner_html() -> str: + """legacy UI (`/ui`) 頂部退場提示 banner(U-3)。 收斂到 `/app` 單一介面前的過渡步驟:在舊介面頂部固定一條提示,導使用者改用 - `/app`。`/studio` 仍 client-side 直連 Gemini(繞過後端計費 + review gate,見 U-1), - 故額外標警告。純前端提示、不移除任何功能、可逆(避免反悔)。 + `/app`。純前端提示、不移除任何功能、可逆(避免反悔)。 + + (`/studio` 已於 U-1 退場 → 改 307 轉址至 `/app/`,不再注入 banner。) """ - extra = ( - "(此介面直連 Gemini、未走後端計費與 review 審查)" - if studio - else "" - ) return ( '
' - "⚠ 此介面為 legacy(即將退場)" + extra + ",請改用統一介面 " + "⚠ 此介面為 legacy(即將退場),請改用統一介面 " '/app。' "
" ) -def _inject_legacy_banner(html: str, *, studio: bool) -> str: +def _inject_legacy_banner(html: str) -> str: """把 legacy banner 注入 index.html `` 起始處(找不到 body 則前置)。""" - banner = _legacy_banner_html(studio=studio) + banner = _legacy_banner_html() lowered = html.lower() body_idx = lowered.find(" str: return html[: close + 1] + banner + html[close + 1 :] -def _serve_legacy_spa(root: Path, full_path: str, *, studio: bool) -> FileResponse | HTMLResponse: +def _serve_legacy_spa(root: Path, full_path: str) -> FileResponse | HTMLResponse: """legacy SPA 服務:實檔直接回,index.html / deep-link 回注入 banner 的 HTML。""" target = (root / full_path).resolve() try: @@ -117,7 +111,7 @@ def _serve_legacy_spa(root: Path, full_path: str, *, studio: bool) -> FileRespon if target.is_file() and target.name != "index.html": return FileResponse(target) html = (root / "index.html").read_text(encoding="utf-8") - return HTMLResponse(_inject_legacy_banner(html, studio=studio)) + return HTMLResponse(_inject_legacy_banner(html)) def create_app() -> FastAPI: @@ -179,23 +173,17 @@ def create_app() -> FastAPI: async def spa_fallback(full_path: str) -> Response: # 實檔 (例如 /ui/vite.svg) 直接回;index.html / deep-link 回注入退場 # banner (U-3) 的 HTML。防 path traversal 在 helper 內。 - return _serve_legacy_spa(WEB_DIST, full_path, studio=False) - - # eduStudio 合併 C-4: infoCard 前端 (vite build --base=/studio/) 服務 /studio/*。 - # 同 /ui 模式:mount assets + SPA fallback。前端目前仍 client-side 呼叫 Gemini - # (key 由 UI 設定);「前端改打本 server /api」為後續步驟。 - if WEB_STUDIO.exists(): - studio_assets = WEB_STUDIO / "assets" - if studio_assets.exists(): - app.mount( - "/studio/assets", - StaticFiles(directory=str(studio_assets)), - name="studio-assets", - ) - - @app.get("/studio/{full_path:path}", include_in_schema=False) - async def studio_spa(full_path: str) -> Response: - return _serve_legacy_spa(WEB_STUDIO, full_path, studio=True) + return _serve_legacy_spa(WEB_DIST, full_path) + + # U-1: `/studio` 退場。原 infoCard 前端 client-side 直連 Gemini(繞過後端計費 + + # review gate),其源碼不在本 repo,且視覺功能已於 U-2 在 `/app` 補齊對等(逐區 + # refine / 區域選擇)。故不再 serve 該 SPA,改一律 307 轉址到 `/app/`:關掉繞過 + # 漏洞、舊書籤/連結不 404。無條件註冊(即使殘留 web/studio build 也不會被服務)。 + # 307(暫時)而非 301/308:不被瀏覽器永久快取,保留反悔餘地。 + @app.get("/studio", include_in_schema=False) + @app.get("/studio/{full_path:path}", include_in_schema=False) + async def studio_sunset(full_path: str = "") -> RedirectResponse: + return RedirectResponse(url="/app/", status_code=307) # eduStudio 合併 C-4: 統一 app(Claude Design 設計)serve 在 /app/*(同 /ui 模式)。 if WEB_EDUAPP.exists(): diff --git a/server/static/landing.html b/server/static/landing.html index f38f734..0b60390 100644 --- a/server/static/landing.html +++ b/server/static/landing.html @@ -67,7 +67,6 @@ .badge-soon { font-size: 11px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 2px 8px; margin-left: 8px; } .card.legacy { opacity: 0.82; } .badge-legacy { font-size: 11px; color: #f0b86e; border: 1px solid #b45309; background: rgba(180,83,9,0.14); border-radius: 999px; padding: 2px 8px; margin-left: 8px; } - .card .warn { color: #f0b86e; font-size: 12px; margin-top: 8px; } .hero-cta { display: flex; align-items: center; justify-content: space-between; gap: 16px; text-decoration: none; color: inherit; margin-bottom: 28px; @@ -106,14 +105,6 @@ /ui - -
🎨
-
圖卡 · 簡報 · 漫畫legacy
-
把內容轉成資訊圖卡、教學簡報、漫畫分鏡(Gemini 生成)。
-
⚠ 直連 Gemini,未走後端計費與 review 審查,請改用 /app。
- /studio -
-
🌐
在地化 · 翻譯
diff --git a/tests/test_legacy_banner.py b/tests/test_legacy_banner.py index b4182b8..648765b 100644 --- a/tests/test_legacy_banner.py +++ b/tests/test_legacy_banner.py @@ -1,31 +1,30 @@ -"""U-3: legacy UI 退場 banner 注入測試。 +"""U-3 / U-1: legacy UI 退場測試。 -`/ui` `/studio` build 產物在測試環境通常不存在(route 不註冊),故直接單元測注入 -helper。banner 是收斂到 `/app` 前的非破壞性過渡提示。 +U-3:`/ui` 頂部注入「即將退場、改用 /app」banner(非破壞性過渡提示)。 +U-1:`/studio`(原 client-side 直連 Gemini、繞過後端計費 + review gate)退場 → +一律 307 轉址到 `/app/`。 + +`/ui` build 產物在測試環境通常不存在(route 不註冊),故 banner 直接單元測注入 +helper;`/studio` 轉址不依賴 build 產物(無條件註冊),用 TestClient 驗。 """ +from fastapi.testclient import TestClient + from server.main import ( _inject_legacy_banner, _legacy_banner_html, + create_app, ) def test_banner_links_to_app(): - html = _legacy_banner_html(studio=False) + html = _legacy_banner_html() assert 'href="/app/"' in html assert "legacy" in html -def test_studio_banner_warns_about_bypass(): - """/studio banner 額外標「繞過後端計費/審查」(U-1 漏洞警示)。""" - studio = _legacy_banner_html(studio=True) - ui = _legacy_banner_html(studio=False) - assert "計費" in studio and "審查" in studio - assert "計費" not in ui - - def test_inject_after_body_tag(): html = "
" - out = _inject_legacy_banner(html, studio=False) + out = _inject_legacy_banner(html) # banner 在 之後、#root 之前 body_close = out.find(">", out.find(" body_close @@ -36,13 +35,30 @@ def test_inject_after_body_tag(): def test_inject_without_body_prepends(): html = "
" - out = _inject_legacy_banner(html, studio=True) + out = _inject_legacy_banner(html) assert out.startswith("
" in out + + +def test_studio_root_redirects_to_app(): + """U-1: `/studio` 退場 → 307 轉址到 `/app/`,不再 serve 直連 Gemini 的 SPA。""" + client = TestClient(create_app(), follow_redirects=False) + resp = client.get("/studio") + assert resp.status_code == 307 + assert resp.headers["location"] == "/app/" + + +def test_studio_deeplink_redirects_to_app(): + """任何 `/studio/*` 子路徑(含舊書籤/asset)一律導回 `/app/`,不 404。""" + client = TestClient(create_app(), follow_redirects=False) + for path in ("/studio/", "/studio/poster", "/studio/assets/index.js"): + resp = client.get(path) + assert resp.status_code == 307, path + assert resp.headers["location"] == "/app/", path