Co-Authored-By: Claude Code <noreply@anthropic.com>
14 KiB
WhatsApp / Email 动态配置 Implementation Plan
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: 将写死的 WhatsApp / Email 联系方式改为通过 <script> 标签的 data-whatsapp / data-email 属性动态配置,未提供时回退现有默认值,保持 100% 向后兼容。
Architecture: 复用 widget.js 既有的 script.getAttribute("data-*") || "默认值" 配置范式,新增两个属性并替换 3 处写死的 href。独立页 chat.html(加载 chat.js,不加载 widget.js)采用同一套机制:chat.js 读取自身 <script> 标签的 data-whatsapp / data-email 并应用到两个锚点。
Tech Stack: 原生 JavaScript(ES5 风格 IIFE)、静态 HTML。无构建步骤、无打包器。
Spec: docs/superpowers/specs/2026-06-18-dynamic-contact-config-design.md
⚠️ 关于测试方式(重要)
本仓库为纯静态 JS 前端 + Python 后端,没有任何 JS 测试框架(无 package.json,tests/ 只有空的 __init__.py)。为本改动引入 JS 测试框架违反 YAGNI。
因此本计划不使用自动化 TDD,而是采用手动浏览器验证:每个 Task 末尾用浏览器打开页面、通过 DevTools 检查 href 实际值。验证标准写明「期望看到的值」,与自动化测试的断言等价。
验证通用方法:
- 方式 A(推荐,贴近真实部署):启动后端服务,浏览器访问页面,F12 打开 DevTools。
- 方式 B(最快,免启动服务):直接用浏览器打开本地 HTML 文件(
file://)。组件的 WebSocket 会连不上,但不影响href值的验证(href在初始化时就写好,与 WS 无关)。 - 检查 Shadow DOM 内的链接:DevTools → Elements → 展开组件 Shadow Root,或在 Console 执行:
// widget(shadow DOM 内) document.querySelector('#cc-widget-host').shadowRoot.querySelectorAll('a.whatsapp-btn, a.email-btn').forEach(a => console.log(a.className, a.href));
文件结构(File Structure)
| 文件 | 职责 | 本次改动 |
|---|---|---|
static/widget.js |
嵌入式组件(第三方站点通过 <script> 引入,渲染 Shadow DOM 聊天窗) |
读取 2 个新属性 + 替换 3 处 href |
static/js/chat.js |
独立整页 chat.html 的交互逻辑(WebSocket、消息渲染) |
新增读取自身 script 标签配置 + 设置 2 个锚点 href |
static/chat.html |
独立整页聊天页(加载 chat.js) |
给 chat.js 的 <script> 标签加 2 个 data 属性 |
static/widget-test.html |
嵌入式组件的本地测试/演示页 | 给组件 <script> 标签加 2 个 data 属性(演示用) |
边界说明:widget.js 与 chat.js 是两套独立运行时(前者在 Shadow DOM,后者操作 light DOM),但共用同一份配置契约(相同的属性名、相同的默认值、相同的「缺失则回退」语义)。两个文件各自独立持有默认值常量——它们是部署级常量,没有运行时共享的必要(也无打包器可共享),重复这两个字符串是可接受的、符合现有代码风格。
Task 1: widget.js 读取配置并接入 3 处 href
Files:
-
Modify:
static/widget.js:8-12(新增配置读取) -
Modify:
static/widget.js:170(面板 help-block,单行内含 WhatsApp + Email 两个href) -
Modify:
static/widget.js:199(邀请卡 WhatsApp) -
Modify:
static/widget.js:203(邀请卡 Email) -
Step 1: 新增配置读取
在 static/widget.js 第 8-12 行现有 getAttribute 块的末尾(第 11 行 var autoOpen = ... 之后、第 12 行 var wsBase = ... 之前)插入两行:
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8619037179250";
var email = script.getAttribute("data-email") || "mailto:info@borderlesscar.com";
插入后该区域应为:
var position = script.getAttribute("data-position") || "bottom-right";
var theme = script.getAttribute("data-theme") || "light";
var title = script.getAttribute("data-title") || "HESU Support";
var autoOpen = script.getAttribute("data-auto-open") === "true";
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8619037179250";
var email = script.getAttribute("data-email") || "mailto:info@borderlesscar.com";
var wsBase = new URL(script.src).origin;
- Step 2: 替换面板 help-block 内的两处 href(第 170 行)
第 170 行是单个字符串数组元素,里面同时含 WhatsApp 和 Email 两个 <a>。在该行内做两处子串替换(不要整行重写,避免破坏 SVG):
| 原子串 | 替换为 |
|---|---|
href="https://wa.me/+8619037179250" target="_blank" class="call-btn whatsapp-btn" |
href="' + whatsapp + '" target="_blank" class="call-btn whatsapp-btn" |
href="mailto:info@borderlesscar.com" class="call-btn email-btn" |
href="' + email + '" class="call-btn email-btn" |
原理:该数组元素用单引号
'...'包裹,内部 HTML 属性用双引号。href="' + whatsapp + '"即关闭单引号、拼接变量、重开单引号,与现有title变量在第 163 行的拼接方式一致。
- Step 3: 替换邀请卡 WhatsApp href(第 199 行)
第 199 行:
'<a href="https://wa.me/+8619037179250" target="_blank" class="invite-action-btn whatsapp-btn">',
替换为:
'<a href="' + whatsapp + '" target="_blank" class="invite-action-btn whatsapp-btn">',
- Step 4: 替换邀请卡 Email href(第 203 行)
第 203 行:
'<a href="mailto:info@borderlesscar.com" class="invite-action-btn email-btn">',
替换为:
'<a href="' + email + '" class="invite-action-btn email-btn">',
- Step 5: 全量自检——确认无残留写死值
Run: grep -n "wa.me/+8619037179250\|mailto:info@borderlesscar.com" static/widget.js
Expected: 无任何输出(3 处已全部替换为变量拼接)。
- Step 6: 手动验证——默认值(向后兼容)
此时 widget-test.html 尚未加新属性,等于「未提供」。
打开 static/widget-test.html(方式 A 或 B),DevTools Console 执行:
[...document.querySelector('#cc-widget-host').shadowRoot.querySelectorAll('a')].filter(a => a.className.includes('whatsapp-btn') || a.className.includes('email-btn')).forEach(a => console.log(a.className, '->', a.href));
Expected(应输出 4 条,邀请卡 + 面板各 2 个):
whatsapp-btn→https://wa.me/+8619037179250email-btn→mailto:info@borderlesscar.com
与改动前完全一致 → 向后兼容验证通过。
- Step 7: 手动验证——自定义值覆盖(临时)
临时编辑 static/widget-test.html:69,给组件 script 标签加上自定义值:
<script src="/static/widget.js" data-position="bottom-right" data-theme="light" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com"></script>
刷新页面,再次执行 Step 6 的 Console 命令。 Expected:
whatsapp-btn→https://wa.me/8613800138000email-btn→mailto:demo@hesu-auto.com
覆盖路径验证通过后,先不要回退——这个改动正好是 Task 3 的成果,直接进入提交。
注:即使这里手滑回退了也无妨——Task 3 Step 1 会重新把它加回来。本 Task 的提交只包含
widget.js,不含widget-test.html。
- Step 8: 提交
git add static/widget.js
git commit -m "$(cat <<'EOF'
feat(widget): WhatsApp/Email 改为 data-whatsapp/data-email 动态配置
组件读取自身 <script> 标签的 data-whatsapp / data-email 属性,
未提供时回退现有默认值,保持向后兼容。替换面板 help-block 与邀请卡
共 3 处 href。
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Task 2: chat.js + chat.html 独立页动态配置
Files:
-
Modify:
static/js/chat.js:1-2(IIFE 顶部插入配置读取与应用) -
Modify:
static/chat.html:80(给 chat.js script 标签加 data 属性) -
Step 1:
chat.js顶部新增配置读取与应用
在 static/js/chat.js 第 1 行 (function () { 之后、第 2 行 var sessionId = ... 之前,插入:
// 联系方式配置(与 widget.js 的 data-* 机制一致)
var chatScript = document.currentScript || document.querySelector('script[src*="chat.js"]');
var whatsapp = (chatScript && chatScript.getAttribute("data-whatsapp")) || "https://wa.me/+8619037179250";
var email = (chatScript && chatScript.getAttribute("data-email")) || "mailto:info@borderlesscar.com";
var waBtn = document.querySelector('.whatsapp-btn');
var emBtn = document.querySelector('.email-btn');
if (waBtn) waBtn.href = whatsapp;
if (emBtn) emBtn.href = email;
时序安全:
chat.js在<body>末尾加载(chat.html:80,晚于聊天容器 DOMchat.html:10-78),故同步querySelector时两个锚点已存在。document.currentScript对普通(非 async/defer)<script src>在 IIFE 顶部非空;querySelector回退为防御性写法。
- Step 2:
chat.html给 chat.js script 标签加 data 属性
static/chat.html:80 当前:
<script src="/static/js/chat.js"></script>
替换为(用与 Task 1 Step 7 一致的自定义演示值):
<script src="/static/js/chat.js" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com"></script>
chat.html:35与:39原有的静态href保留不动——作为chat.js未执行时的 fallback(默认值),chat.js启动后会覆盖为配置值。
- Step 3: 手动验证——自定义值生效
打开 static/chat.html(方式 A 或 B),DevTools Console 执行:
[...document.querySelectorAll('a.whatsapp-btn, a.email-btn')].forEach(a => console.log(a.className, '->', a.href));
Expected:
call-btn whatsapp-btn→https://wa.me/8613800138000call-btn email-btn→mailto:demo@hesu-auto.com
(chat.html 是 light DOM,不需要 shadowRoot。)
- Step 4: 手动验证——回退默认值(临时)
临时把 chat.html:80 的两个 data 属性删掉(恢复成 <script src="/static/js/chat.js"></script>),刷新页面,再次执行 Step 3 的 Console 命令。
Expected:
whatsapp-btn→https://wa.me/+8619037179250email-btn→mailto:info@borderlesscar.com
回退验证通过后,把 data 属性加回去(恢复 Step 2 的状态)。
- Step 5: 提交
git add static/js/chat.js static/chat.html
git commit -m "$(cat <<'EOF'
feat(chat): 独立页 WhatsApp/Email 改为 data-* 动态配置
chat.js 读取自身 <script> 标签的 data-whatsapp / data-email(与
widget.js 同一套机制),应用到页面两个锚点;chat.html script 标签
加上演示属性,原有静态 href 保留为 fallback 默认值。
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Task 3: widget-test.html 演示属性 + 更新测试清单
Files:
-
Modify:
static/widget-test.html:69(确认演示属性,Task 1 Step 7 已临时加,此处定稿) -
Modify:
docs/test-checklist.md(补充手动验证条目) -
Step 1: 确认
widget-test.html演示属性到位
确认 static/widget-test.html:69 已是(Task 1 Step 7 改成的):
<script src="/static/widget.js" data-position="bottom-right" data-theme="light" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com"></script>
若 Step 7 之后被改动过,恢复成上面这样。
- Step 2: 全局终检——确认无残留写死联系方式(前端)
Run: grep -rn "wa.me/+8619037179250\|mailto:info@borderlesscar.com" static/widget.js static/js/chat.js
Expected: 无输出(两处运行时文件都已参数化)。
注:static/chat.html:35 和 :39 仍含默认值字符串,这是有意保留的 fallback,属正常。
- Step 3: 更新测试清单文档
在 docs/test-checklist.md 末尾追加一节(若文件不存在则新建并加标题 # 测试清单):
## WhatsApp / Email 动态配置(2026-06-18)
- [ ] **默认值(未传属性)**:组件(widget-test.html 去掉 data 属性时)与独立页(chat.html 去掉 data 属性时)的 WhatsApp / Email 按钮分别指向 `https://wa.me/+8619037179250` 与 `mailto:info@borderlesscar.com`,与历史行为一致。
- [ ] **自定义值**:widget-test.html 传 `data-whatsapp` / `data-email` 后,组件内三处链接(面板 help-block 的 WhatsApp、面板 help-block 的 Email、邀请卡 WhatsApp、邀请卡 Email)均指向自定义地址。
- [ ] **独立页自定义值**:chat.html 传 `data-whatsapp` / `data-email` 后,两个按钮指向自定义地址。
- [ ] **空字符串**:`data-whatsapp=""` 等价于未传,回退默认值。
- [ ] **JS 未执行**:chat.html 在 JS 禁用 / 未执行时,静态 `href` 仍为默认值,不出现空链接。
- Step 4: 提交
git add static/widget-test.html docs/test-checklist.md
git commit -m "$(cat <<'EOF'
docs(test): widget-test 演示动态配置属性 + 补充测试清单
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
完成标准(Definition of Done)
static/widget.js与static/js/chat.js中不再有任何写死的wa.me/+8619037179250/mailto:info@borderlesscar.com(grep 无输出)。- 不传任何新属性时,所有联系方式链接与改动前完全一致(向后兼容)。
- 传入自定义
data-whatsapp/data-email时,组件 3 处 + 独立页 2 处链接均指向自定义值。 - 3 个 commit 已提交,每个 commit 自包含、可独立验证。
docs/test-checklist.md已补充对应手动验证条目。
风险与回滚
- 风险极低:纯前端字符串替换 + 新增属性读取,不触碰 WebSocket、后端、样式。
- 回滚:
git revert对应 commit 即可;未传属性的部署本来就无任何行为变化。