feat(chat): 页面级 basePath 引导(路径模式静态面)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+32
-5
@@ -4,13 +4,42 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>AI Support</title>
|
<title>AI Support</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<script>
|
||||||
<script src="/static/js/theme.js"></script>
|
(function () {
|
||||||
|
/* spec 2026-10-19 §4:路径前缀模式(/t/<slug>/)按当前地址推导基础路径;
|
||||||
|
Host 模式(根路径 "/")推导为空串——全部引用零变化。
|
||||||
|
拼接安全:match 命中即字符集白名单([a-z0-9-]),base 只做 URL 属性
|
||||||
|
赋值,绝不拼 HTML(防线 §2/§3);找不到标记(如 /admin 域)同样得空串。 */
|
||||||
|
var m = location.pathname.match(/^\/t\/[a-z0-9-]+\/$/);
|
||||||
|
var base = m ? location.pathname.slice(0, -1) : "";
|
||||||
|
window.__BASE_PATH__ = base;
|
||||||
|
function css(href) {
|
||||||
|
var l = document.createElement("link");
|
||||||
|
l.rel = "stylesheet"; l.href = base + href;
|
||||||
|
document.head.appendChild(l);
|
||||||
|
}
|
||||||
|
function js(src) {
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.src = base + src; s.async = false; /* 保序:theme 先于 chat */
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
css("/static/css/style.css");
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
/* js 装载放 DOMContentLoaded:head 动态插入的 async=false 脚本与
|
||||||
|
解析进度无形式化顺序保证,且 chat.js 顶层有同步 DOM 查询——
|
||||||
|
DOM 就绪后装载与原 body 末尾形态等价且保序 */
|
||||||
|
js("/static/js/theme.js");
|
||||||
|
js("/static/js/chat.js");
|
||||||
|
var img = document.querySelector(".avatar-img");
|
||||||
|
if (img) img.src = base + "/api/public/avatar";
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="/api/public/avatar"></div>
|
<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt=""></div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h1>AI Support</h1>
|
<h1>AI Support</h1>
|
||||||
<div class="availability">Online now - Usually replies in minutes</div>
|
<div class="availability">Online now - Usually replies in minutes</div>
|
||||||
@@ -76,7 +105,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a target="_blank" rel="noopener">privacy policy</a>.</span></div>
|
<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a target="_blank" rel="noopener">privacy policy</a>.</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/chat.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+6
-2
@@ -1,8 +1,12 @@
|
|||||||
|
// ---- basePath(spec 2026-10-19 §4):chat.html 引导脚本按 location.pathname ----
|
||||||
|
// ---- 推导并挂载 window.__BASE_PATH__(路径模式 /t/<slug>,Host 模式空串)。 ----
|
||||||
|
var B = window.__BASE_PATH__ || "";
|
||||||
|
|
||||||
// ---- Task 10: brand 拉取(同源 /api/public/brand,下方两个 IIFE 共用) ----
|
// ---- Task 10: brand 拉取(同源 /api/public/brand,下方两个 IIFE 共用) ----
|
||||||
// 失败/超时(3s) → resolve(null) → 中性兜底,页面照常可用不白屏。
|
// 失败/超时(3s) → resolve(null) → 中性兜底,页面照常可用不白屏。
|
||||||
var chatBrandReady = (function () {
|
var chatBrandReady = (function () {
|
||||||
try {
|
try {
|
||||||
var p = fetch("/api/public/brand", { cache: "no-store" }).then(function (r) {
|
var p = fetch(B + "/api/public/brand", { cache: "no-store" }).then(function (r) {
|
||||||
return r.ok ? r.json() : null;
|
return r.ok ? r.json() : null;
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
return null;
|
return null;
|
||||||
@@ -129,7 +133,7 @@ var chatBrandReady = (function () {
|
|||||||
function getWsUrl() {
|
function getWsUrl() {
|
||||||
var loc = window.location;
|
var loc = window.location;
|
||||||
var proto = loc.protocol === "https:" ? "wss:" : "ws:";
|
var proto = loc.protocol === "https:" ? "wss:" : "ws:";
|
||||||
return proto + "//" + loc.host + "/ws/chat";
|
return proto + "//" + loc.host + B + "/ws/chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect(sessionKey) {
|
function connect(sessionKey) {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""chat.html 静态面前缀守卫(spec 2026-10-19 §4):页面级 __BASE_PATH__ 引导。
|
||||||
|
|
||||||
|
chat 是页面而非嵌入脚本(与 widget.js 的脚本地址自推导不同),由 chat.html
|
||||||
|
head 内联引导脚本按 location.pathname 推导 basePath 并动态装载 css/js/
|
||||||
|
avatar;Host 模式(根路径 "/")推导为空串,所有引用零变化。守卫四点:
|
||||||
|
引导脚本在场且走正则白名单 match、静态标签不再绝对引用、引导块零
|
||||||
|
innerHTML(拼接安全防线 §2/§3)、chat.js 端点构造全部经 __BASE_PATH__。
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATIC = Path(__file__).resolve().parent.parent / "static"
|
||||||
|
|
||||||
|
|
||||||
|
def read(rel):
|
||||||
|
return (STATIC / rel).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_引导脚本存在且定义_BASE_PATH():
|
||||||
|
src = read("chat.html")
|
||||||
|
assert "__BASE_PATH__" in src
|
||||||
|
# 代码锚(非注释):base 推导必须走正则白名单 match
|
||||||
|
assert "location.pathname.match(" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_不再有静态绝对引用标签():
|
||||||
|
src = read("chat.html")
|
||||||
|
assert 'href="/static/' not in src
|
||||||
|
assert 'src="/static/js/' not in src
|
||||||
|
assert 'src="/api/public/avatar"' not in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_引导脚本零innerHTML_拼接安全防线():
|
||||||
|
# 安全防线 §2/§3:base 只做 URL 属性赋值,绝不拼 HTML——
|
||||||
|
# 断言首个内联 <script>...</script> 块(引导脚本)内无 innerHTML
|
||||||
|
src = read("chat.html")
|
||||||
|
boot = src.split("<script>", 1)[1].split("</script>", 1)[0]
|
||||||
|
assert "innerHTML" not in boot
|
||||||
|
|
||||||
|
|
||||||
|
def test_chatjs_端点全部走_BASE_PATH():
|
||||||
|
src = read("js/chat.js")
|
||||||
|
assert "__BASE_PATH__" in src
|
||||||
|
assert 'fetch("/api/' not in src
|
||||||
Reference in New Issue
Block a user