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

14 KiB
Raw Blame History

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/+8619037179250
  • email-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/8613800138000
  • email-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,晚于聊天容器 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 当前:

    <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/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: 提交
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 即可;未传属性的部署本来就无任何行为变化。