# 动态主题色 + 头像图片化 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 客服挂件与独立聊天页支持按网站自定义品牌色(` ``` L12:`
🤖
` → `
🤖
` L36/L40:`stroke="#8C4D1D"` → `stroke="currentColor"` L50:删除整行(``) - [ ] **Step 2: chat.js——文件顶部追加 bootstrap(在现有逻辑之前)** ```javascript // ---- 动态主题色 bootstrap(spec §3):?color= → 派生 → :root 变量 ---- (function () { if (typeof ChatTheme === "undefined") return; // theme.js 加载失败→style.css :root 默认橙兜底 var m = /(?:^|[?&])color=([^&]*)/.exec(location.search); var palette = ChatTheme.derivePalette(m ? decodeURIComponent(m[1]) : ChatTheme.DEFAULT_COLOR); var root = document.documentElement.style; root.setProperty("--w-primary", palette.primary); root.setProperty("--w-primary-rgb", palette.primaryRgb); root.setProperty("--w-gradient-end", palette.gradientEnd); root.setProperty("--w-deep-text", palette.deepText); root.setProperty("--w-caption-text", palette.captionText); root.setProperty("--w-soft-bg", palette.softBg); root.setProperty("--w-border-soft", palette.borderSoft); // 头像兜底(与挂件同机制) Array.prototype.forEach.call(document.querySelectorAll(".avatar-img"), function (img) { function hideEmoji() { var s = img.parentElement && img.parentElement.querySelector(".avatar-emoji"); if (s) s.style.display = "none"; } if (img.complete && img.naturalWidth > 0) { hideEmoji(); return; } img.addEventListener("load", hideEmoji); img.addEventListener("error", function () { img.remove(); }); }); })(); ``` - [ ] **Step 3: style.css 追加头像规则(若 Task 5 未含)** ```css .avatar-emoji { line-height: 1; } .avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; border-radius: 50%; } ``` - [ ] **Step 4: 全量 pytest + Commit** Run: `python -m pytest tests/ -q` → Expected: 全 PASS ```bash git add static/chat.html static/js/chat.js static/css/style.css git commit -m "feat(theme): 独立页接入——?color=参数+theme.js+头像图片+SVG currentColor" ``` --- ### Task 10: widget-test.html 重写(预览 + 断言面板) **Files:** - Modify: `static/widget-test.html`(整页重写) - [ ] **Step 1: 重写页面——保留原说明,新增控制区与断言区** 结构:原 info 卡片保留;`` 中**必须**加 ``(断言面板依赖 `window.ChatTheme`,漏掉则面板永远显示钩子未就绪);新增: ```html

Theme Color Preview

Assertion Panel

``` - [ ] **Step 2: teardown/重建逻辑(spec §7.1 陷阱;基础属性对象 + extra 合并,缓存戳防缓存)** ```javascript var BASE_ATTRS = { "data-position": "bottom-right", "data-theme": "light", "data-mobile": "true", "data-debug": "true", "data-whatsapp": "https://wa.me/8613800138000", "data-email": "mailto:demo@hesu-auto.com" }; function rebuildWidget(extra) { var host = document.getElementById("cc-widget-host"); if (host) host.remove(); document.querySelectorAll("script[src*='widget.js']").forEach(function (s) { s.remove(); }); try { sessionStorage.removeItem("widget_invite_dismissed_" + window.location.pathname); } catch (e) {} var s = document.createElement("script"); s.src = "/static/widget.js?" + Date.now(); var attrs = Object.assign({}, BASE_ATTRS, extra || {}); Object.keys(attrs).forEach(function (k) { s.setAttribute(k, attrs[k]); }); s.onload = runAssertions; document.body.appendChild(s); } function applyColor(c) { rebuildWidget({ "data-color": c || document.getElementById("colorPicker").value }); } function applyBrokenAvatar() { rebuildWidget({ "data-color": "#1a73e8", "data-avatar": "/static/nope.png" }); } ``` - [ ] **Step 3: 断言面板(spec §8.2;页面加载后自动跑一次)** ```javascript var CURRENT_HARDCODED = { // 现状代表值(spec §4),Δ ≤ 8/通道 primary: "#f08a31", gradientEnd: "#F08A34", deepText: "#8C4D1D", captionText: "#C16C2E", softBg: "#FFF8F3", borderSoft: "#F0D5BF" }; function hexToRgb6(h) { /* 复用 theme.js 的 hexToRgb 逻辑或简化实现 */ } function channelDelta(a, b) { /* 返回 max 通道差 */ } function runAssertions() { var rows = []; function check(name, ok, detail) { rows.push((ok ? "✅ " : "❌ ") + name + (detail ? " — " + detail : "")); } var t = window.ChatTheme, d = window.__chatWidgetDebug; if (!t || !d) { rows.push("❌ theme.js / widget debug 钩子未就绪"); } else { var p = t.derivePalette("#f08a31"); Object.keys(CURRENT_HARDCODED).forEach(function (k) { var delta = channelDelta(p[k], CURRENT_HARDCODED[k]); check("default " + k + " Δ≤8 (" + p[k] + " vs " + CURRENT_HARDCODED[k] + ")", delta <= 8, "Δ=" + delta); }); check("derive(default) === DEFAULT_PALETTE", JSON.stringify(p) === JSON.stringify(t.DEFAULT_PALETTE)); check("inline copy === theme.js copy", d.derivePalette.toString().replace(/\s+/g, "") === t.derivePalette.toString().replace(/\s+/g, "")); check("invalid color → DEFAULT_PALETTE", JSON.stringify(t.derivePalette("not-a-color")) === JSON.stringify(t.DEFAULT_PALETTE)); } document.getElementById("assertions").innerHTML = rows.map(function (r) { return "
" + r + "
"; }).join(""); } ``` 页面初始嵌入(替代原 L69 的静态 script 标签)同样带 `data-debug="true"` 并挂 `onload="runAssertions()"`,属性与 BASE_ATTRS 一致。 - [ ] **Step 4: 手动验收(浏览器,无自动化,如实记录结果)** 1. 打开 `/static/widget-test.html`:断言面板全 ✅ 2. 默认橙:邀请卡(1.8s)+ 聊天面板 + 头像 5 处中挂件侧 4 处,与改造前截图对比无可见差异 3. Blue/Green/Purple:全套配色跟随,无橙色残留 4. Test Broken Avatar:头像回退 🤖 5. `/`(独立页)与 `/?color=1a73e8`:独立页跟随;头像替换;SVG 图标为深主题色 6. `?color=red`(非法):回退默认橙,console 有 warn - [ ] **Step 5: Commit** ```bash git add static/widget-test.html git commit -m "feat(test): widget-test重写——换色预览+teardown重建+浏览器断言面板" ``` --- ### Task 11: changelog 文档 + spec 校准 + 终验 **Files:** - Create: `docs/2026-08-31-dynamic-theme-changelog.md` - Modify: `docs/superpowers/specs/2026-08-31-dynamic-theme-color-design.md`(三处校准) - [ ] **Step 1: 写 changelog(按仓库惯例,含嵌入方用法)** ```markdown # 2026-08-31 动态主题色 + 头像图片化 ## 嵌入方用法 - 品牌色:`