# 动态主题色 + 头像图片化 实施计划
> **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
```
- [ ] **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 动态主题色 + 头像图片化
## 嵌入方用法
- 品牌色:`