Files
ai-kefu-jc/docs/superpowers/plans/2026-06-18-dynamic-contact-config.md
T
2026-09-29 14:15:12 +08:00

293 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 即可;未传属性的部署本来就无任何行为变化。