feat(widget): 脚本地址自推导 basePath(路径模式端点前缀)
spec 2026-10-19 §4: 路径模式(/t/<slug>/static/widget.js)下从 script.src 剥尾推导 basePath,brand/avatar 三分支/WS 四处端点构造统一加前缀; Host 模式 indexOf 命中在 0 → basePath="" 端点零变化。 test_theme_static.py 头像锚定守卫随构造更新(wsBase + basePath + …, 语义不变:绝对锚定 + 禁相对路径)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+15
-5
@@ -24,6 +24,16 @@
|
||||
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
|
||||
var isMobile = script.getAttribute("data-mobile") === "true";
|
||||
var wsBase = new URL(script.src).origin;
|
||||
// spec 2026-10-19 §4: 脚本地址自携带租户路径(/t/<slug>/static/widget.js)
|
||||
// → 剥掉 /static/widget.js 起的前缀得 basePath("" = Host 模式零变化)。
|
||||
// script.src 永远指向本文件,indexOf 命中即安全;query(?v=)不影响 pathname。
|
||||
// 拼接安全: basePath 仅进 URL 上下文(fetch/WebSocket/img.src),
|
||||
// 绝不进 innerHTML 字符串拼 HTML(防线 §2/§3)。
|
||||
var basePath = (function () {
|
||||
var p = new URL(script.src).pathname;
|
||||
var i = p.indexOf("/static/widget.js");
|
||||
return i > 0 ? p.slice(0, i) : "";
|
||||
})();
|
||||
|
||||
/* ===== 主题派生单元:与 static/js/theme.js 逐字同步,tests/test_theme_static.py 强制;只可整块替换,不可单边修改 ===== */
|
||||
var DEFAULT_COLOR = "#066fc0";
|
||||
@@ -129,7 +139,7 @@
|
||||
var brandSlug = "default";
|
||||
|
||||
function fetchBrand() {
|
||||
var url = wsBase + "/api/public/brand" + (siteAttr ? "?site=" + encodeURIComponent(siteAttr) : "");
|
||||
var url = wsBase + basePath + "/api/public/brand" + (siteAttr ? "?site=" + encodeURIComponent(siteAttr) : "");
|
||||
var p = fetch(url, { cache: "no-store" }).then(function (r) {
|
||||
return r.ok ? r.json() : null;
|
||||
}).catch(function () {
|
||||
@@ -161,9 +171,9 @@
|
||||
email = contactHref(emailAttr || b.email || "", "mail");
|
||||
palette = derivePalette(colorAttr || (b.color || "").trim() || DEFAULT_COLOR);
|
||||
avatarUrl = avatarAttr
|
||||
? (/^https?:\/\//i.test(avatarAttr) ? avatarAttr : wsBase + "/" + avatarAttr.replace(/^\//, ""))
|
||||
: (b.avatar ? wsBase + (b.avatar.charAt(0) === "/" ? b.avatar : "/" + b.avatar)
|
||||
: wsBase + DEFAULT_AVATAR_PATH);
|
||||
? (/^https?:\/\//i.test(avatarAttr) ? avatarAttr : wsBase + basePath + "/" + avatarAttr.replace(/^\//, ""))
|
||||
: (b.avatar ? wsBase + basePath + (b.avatar.charAt(0) === "/" ? b.avatar : "/" + b.avatar)
|
||||
: wsBase + basePath + DEFAULT_AVATAR_PATH);
|
||||
welcome = b.welcome || "";
|
||||
topics = Array.isArray(b.topics)
|
||||
? b.topics.filter(function (t) { return typeof t === "string" && t; })
|
||||
@@ -682,7 +692,7 @@
|
||||
function wConnect() {
|
||||
var proto = wsBase.indexOf("https") === 0 ? "wss" : "ws";
|
||||
var wsHost = wsBase.replace(/^https?/, proto);
|
||||
wWs = new WebSocket(wsHost + "/ws/chat");
|
||||
wWs = new WebSocket(wsHost + basePath + "/ws/chat");
|
||||
|
||||
wWs.onopen = function () {
|
||||
// Task 10: init 带 site(空域名站点如 es 靠 data-site 接入,服务端
|
||||
|
||||
@@ -124,7 +124,8 @@ def test_widget_avatar_src_uses_wsbase():
|
||||
src = _read("widget.js")
|
||||
assert 'DEFAULT_AVATAR_PATH = "/static/kefu-small.png"' in src
|
||||
assert 'src="/static/kefu-small.png"' not in src, "挂件禁用相对路径头像(客户站会 404)"
|
||||
assert "wsBase + DEFAULT_AVATAR_PATH" in src
|
||||
# spec 2026-10-19 §4: 默认头像同样走 basePath 前缀(Host 模式为空串零变化)
|
||||
assert "wsBase + basePath + DEFAULT_AVATAR_PATH" in src
|
||||
|
||||
|
||||
def test_style_root_defaults_match_theme_palette():
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""widget.js 静态守卫(spec 2026-10-19 §4):脚本地址自推导 basePath,
|
||||
三类端点构造(brand/avatar/WS)统一带前缀;Host 模式前缀为空串零变化。
|
||||
|
||||
路径模式(/t/<slug>/static/widget.js)下脚本地址自携带租户路径,剥掉
|
||||
/static/widget.js 尾部即得 basePath;Host 模式 pathname 以 /static/
|
||||
开头(indexOf 命中在 0)→ basePath="" 所有端点与现状零差异。守卫三点:
|
||||
推导逻辑在场、四处端点构造不再出现裸拼接、默认头像路径同走前缀。
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
STATIC = Path(__file__).resolve().parent.parent / "static"
|
||||
|
||||
|
||||
def read(rel):
|
||||
return (STATIC / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_有_basepath_推导():
|
||||
src = read("widget.js")
|
||||
assert 'indexOf("/static/widget.js")' in src # 从 script.src 剥尾推导
|
||||
|
||||
|
||||
def test_端点构造全部走_basePath():
|
||||
src = read("widget.js")
|
||||
# 不允许再出现裸 wsBase + "/api/ 或 wsHost + "/ws/(必须经 basePath)
|
||||
assert 'wsBase + "/api/' not in src
|
||||
assert 'wsHost + "/ws/' not in src
|
||||
assert "wsBase + basePath" in src
|
||||
|
||||
|
||||
def test_默认头像路径带前缀():
|
||||
src = read("widget.js")
|
||||
assert "wsBase + basePath + DEFAULT_AVATAR_PATH" in src
|
||||
Reference in New Issue
Block a user