49c0e5559c
Co-Authored-By: Claude Code <noreply@anthropic.com>
293 lines
14 KiB
Markdown
293 lines
14 KiB
Markdown
# 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](../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 执行:
|
||
```js
|
||
// 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 = ...` 之前)插入两行:
|
||
|
||
```js
|
||
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8619037179250";
|
||
var email = script.getAttribute("data-email") || "mailto:info@borderlesscar.com";
|
||
```
|
||
|
||
插入后该区域应为:
|
||
```js
|
||
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 行:
|
||
```js
|
||
'<a href="https://wa.me/+8619037179250" target="_blank" class="invite-action-btn whatsapp-btn">',
|
||
```
|
||
替换为:
|
||
```js
|
||
'<a href="' + whatsapp + '" target="_blank" class="invite-action-btn whatsapp-btn">',
|
||
```
|
||
|
||
- [ ] **Step 4: 替换邀请卡 Email href(第 203 行)**
|
||
|
||
第 203 行:
|
||
```js
|
||
'<a href="mailto:info@borderlesscar.com" class="invite-action-btn email-btn">',
|
||
```
|
||
替换为:
|
||
```js
|
||
'<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 执行:
|
||
```js
|
||
[...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/+8619037179250`
|
||
- `email-btn` → `mailto:info@borderlesscar.com`
|
||
|
||
与改动前完全一致 → **向后兼容验证通过**。
|
||
|
||
- [ ] **Step 7: 手动验证——自定义值覆盖(临时)**
|
||
|
||
临时编辑 `static/widget-test.html:69`,给组件 script 标签加上自定义值:
|
||
```html
|
||
<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/8613800138000`
|
||
- `email-btn` → `mailto:demo@hesu-auto.com`
|
||
|
||
覆盖路径验证通过后,**先不要回退**——这个改动正好是 Task 3 的成果,直接进入提交。
|
||
> 注:即使这里手滑回退了也无妨——Task 3 Step 1 会重新把它加回来。本 Task 的提交只包含 `widget.js`,不含 `widget-test.html`。
|
||
|
||
- [ ] **Step 8: 提交**
|
||
|
||
```bash
|
||
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 = ...` 之前,插入:
|
||
|
||
```js
|
||
// 联系方式配置(与 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`,晚于聊天容器 DOM `chat.html:10-78`),故同步 `querySelector` 时两个锚点已存在。`document.currentScript` 对普通(非 async/defer)`<script src>` 在 IIFE 顶部非空;`querySelector` 回退为防御性写法。
|
||
|
||
- [ ] **Step 2: `chat.html` 给 chat.js script 标签加 data 属性**
|
||
|
||
`static/chat.html:80` 当前:
|
||
```html
|
||
<script src="/static/js/chat.js"></script>
|
||
```
|
||
替换为(用与 Task 1 Step 7 一致的自定义演示值):
|
||
```html
|
||
<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 执行:
|
||
```js
|
||
[...document.querySelectorAll('a.whatsapp-btn, a.email-btn')].forEach(a => console.log(a.className, '->', a.href));
|
||
```
|
||
Expected:
|
||
- `call-btn whatsapp-btn` → `https://wa.me/8613800138000`
|
||
- `call-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/+8619037179250`
|
||
- `email-btn` → `mailto:info@borderlesscar.com`
|
||
|
||
回退验证通过后,**把 data 属性加回去**(恢复 Step 2 的状态)。
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
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 改成的):
|
||
```html
|
||
<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` 末尾追加一节(若文件不存在则新建并加标题 `# 测试清单`):
|
||
|
||
```markdown
|
||
## 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: 提交**
|
||
|
||
```bash
|
||
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 即可;未传属性的部署本来就无任何行为变化。
|