feat(wechat): 绑定 UI 并入主 SPA(左栏 rail「微信」按钮 + 扫码 modal)+ bump 0.22.2
上版绑定页是独立 /static/wechat_bind.html、主界面没入口、用户找不到。集成: rail 加「微信」按钮(hd-wechat)→ 扫码绑定 modal(wechat-modal),复用 api() 调已有 5 端点(起码/轮询/查/解绑/自检),仿 crons.js 范式;二维码过期自动换码。 独立页 wechat_bind.html 保留作嵌入/兜底入口。 文件:web/static/js/wechat.js(新)、dev.html(rail 按钮 + modal + CSS)、 main.js(import 触发顶层绑定 + Esc 关闭);RUN/PROGRESS 同步去掉"未并入 SPA"。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c569438d5f
commit
95857ba687
10
PROGRESS.md
10
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-06-24(微信接入第一期 ClawBot 后端 + 自包含绑定页;双渠道设计 §8.7 + bump 0.22.0)
|
最后更新:2026-06-24(微信绑定 UI 并入主 SPA:左栏 rail「微信」按钮 + 扫码 modal + bump 0.22.2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,12 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-06-24 / 微信绑定 UI 并入主 SPA(bump 0.22.2)
|
||||||
|
|
||||||
|
- 上一版绑定页是独立 `/static/wechat_bind.html`,主界面没入口、用户找不到。
|
||||||
|
- 集成:左栏 rail 加「微信」按钮(`hd-wechat`)→ 扫码绑定 modal(`wechat-modal`),复用 `api()` 调已有 5 端点(起码/轮询/查/解绑/自检),仿 `crons.js` modal 范式;过期自动换码、绑定成功提示去微信开口。文件:`web/static/js/wechat.js`(新)、`web/static/dev.html`(rail 按钮 + modal + CSS)、`web/static/js/main.js`(import 触发绑定 + Esc 关闭)。
|
||||||
|
- 独立页 `web/static/wechat_bind.html` 保留作嵌入/兜底入口(同套端点)。
|
||||||
|
|
||||||
### 2026-06-24 / 修复顶栏 token 计量栏回复后不刷新(bump 0.22.1)
|
### 2026-06-24 / 修复顶栏 token 计量栏回复后不刷新(bump 0.22.1)
|
||||||
|
|
||||||
- 现象:提问→助手答完后,对话顶栏的「总 token · 缓存命中 · 花费」计量栏停在发问前旧值,要切到别的 task 再切回才更新。
|
- 现象:提问→助手答完后,对话顶栏的「总 token · 缓存命中 · 花费」计量栏停在发问前旧值,要切到别的 task 再切回才更新。
|
||||||
|
|
@ -34,7 +40,7 @@
|
||||||
- **关键设计决策**:入站对话→每用户一条 persistent「微信」task(连续性,token 靠 §8.2 压缩);凭据(bot_token/context_token)加密列(env `ZCBOT_WECHAT_SECRET_KEY`),绝不进沙箱/日志;**入站出站一体**——主动推送依赖入站给的 context_token,故 getupdates 长轮询常驻(既收对话又刷新 24h 窗口)。
|
- **关键设计决策**:入站对话→每用户一条 persistent「微信」task(连续性,token 靠 §8.2 压缩);凭据(bot_token/context_token)加密列(env `ZCBOT_WECHAT_SECRET_KEY`),绝不进沙箱/日志;**入站出站一体**——主动推送依赖入站给的 context_token,故 getupdates 长轮询常驻(既收对话又刷新 24h 窗口)。
|
||||||
- **文件**(后端全部 import/编译自测过):`core/wechat/{ilink.py 协议客户端, crypto.py 凭据加密, service.py 绑定CRUD+推送+send_to_user 渠道抽象, inbound.py 长轮询管理器+回复提取}`;`core/storage/models.py` 加 `WeChatBotBinding` + migration `0012_wechat_bot_bindings`;`tools/wechat_bot.py` `WechatPushTool` + `core/agent_builder.py` 注册(有开关才挂);`core/scheduler.py` `deliver_notify` 加 `wechat` 通道(未送达退邮件兜底);`web/app.py` lifespan 起入站管理器 + `_run_wechat_message` 回调 + 5 端点(`/v1/wechat/bind/qrcode|status`、`/v1/wechat/bind` GET/DELETE、`/v1/wechat/test`);`web/static/wechat_bind.html` 自包含绑定页;`requirements.txt` 加 segno+cryptography。
|
- **文件**(后端全部 import/编译自测过):`core/wechat/{ilink.py 协议客户端, crypto.py 凭据加密, service.py 绑定CRUD+推送+send_to_user 渠道抽象, inbound.py 长轮询管理器+回复提取}`;`core/storage/models.py` 加 `WeChatBotBinding` + migration `0012_wechat_bot_bindings`;`tools/wechat_bot.py` `WechatPushTool` + `core/agent_builder.py` 注册(有开关才挂);`core/scheduler.py` `deliver_notify` 加 `wechat` 通道(未送达退邮件兜底);`web/app.py` lifespan 起入站管理器 + `_run_wechat_message` 回调 + 5 端点(`/v1/wechat/bind/qrcode|status`、`/v1/wechat/bind` GET/DELETE、`/v1/wechat/test`);`web/static/wechat_bind.html` 自包含绑定页;`requirements.txt` 加 segno+cryptography。
|
||||||
- **env**:`ZCBOT_WECHAT_BOT_ENABLED=1`(渠道开关)+ `ZCBOT_WECHAT_SECRET_KEY=<串>`(凭据加密,缺则退明文标记)+ 可选 `ZCBOT_WECHAT_BASE_URL`。
|
- **env**:`ZCBOT_WECHAT_BOT_ENABLED=1`(渠道开关)+ `ZCBOT_WECHAT_SECRET_KEY=<串>`(凭据加密,缺则退明文标记)+ 可选 `ZCBOT_WECHAT_BASE_URL`。
|
||||||
- **待办(部署后联调)**:migration `0012` 上库;起 web 进程端到端验(扫码绑定→对话→主动推→定时简报推);**SPA 集成**绑定 UI(当前是独立 `/static/wechat_bind.html`,后续并入主 SPA 设置,仿 crons.js modal 范式);**渠道 B 企业微信**(无条件推送,补 ClawBot 24h 窗口短板)按 §8.7「渠道 B」实现。
|
- **待办(部署后联调)**:migration `0012` 上库;起 web 进程端到端验(扫码绑定→对话→主动推→定时简报推);**渠道 B 企业微信**(无条件推送,补 ClawBot 24h 窗口短板)按 §8.7「渠道 B」实现。SPA 集成已落(见下条)。
|
||||||
|
|
||||||
### 2026-06-23 / 平台渲染层 rendering/:三 skill docx 统一 + chromium md→pdf(bump 0.21.0)
|
### 2026-06-23 / 平台渲染层 rendering/:三 skill docx 统一 + chromium md→pdf(bump 0.21.0)
|
||||||
|
|
||||||
|
|
|
||||||
2
RUN.md
2
RUN.md
|
|
@ -62,7 +62,7 @@
|
||||||
```
|
```
|
||||||
> litellm 在 import 时副作用加载 .env;入口走 `main.py`,`.env` 自动生效。直跑 `python -c "from core.storage import ..."` 不经 litellm 链路时记得自己 `import litellm` 触发,或手动 `export ZCBOT_DB_URL=...`。
|
> litellm 在 import 时副作用加载 .env;入口走 `main.py`,`.env` 自动生效。直跑 `python -c "from core.storage import ..."` 不经 litellm 链路时记得自己 `import litellm` 触发,或手动 `export ZCBOT_DB_URL=...`。
|
||||||
- **依赖**:`pip install -r requirements.txt`(已在 `.venv` 里;含 `bcrypt`、`segno`、`cryptography`)。
|
- **依赖**:`pip install -r requirements.txt`(已在 `.venv` 里;含 `bcrypt`、`segno`、`cryptography`)。
|
||||||
- **微信接入(ClawBot,§8.7)**:① `main.py db upgrade head` 带上 migration `0012`;② `.env` 设 `ZCBOT_WECHAT_BOT_ENABLED=1` + `ZCBOT_WECHAT_SECRET_KEY=<串>`;③ 用户登录后开 `/static/wechat_bind.html` 扫码绑定(需个人微信 8.0.70+ 且灰度到 ClawBot 插件)。绑定后在微信「微信 ClawBot」对话即走 zcbot;**主动推送需用户近 24h 在微信开口过一次**(冷启动/超期推不出,退邮件兜底)。绑定页目前独立、未并入主 SPA。
|
- **微信接入(ClawBot,§8.7)**:① `main.py db upgrade head` 带上 migration `0012`;② `.env` 设 `ZCBOT_WECHAT_BOT_ENABLED=1` + `ZCBOT_WECHAT_SECRET_KEY=<串>`;③ 用户登录后点**左栏 rail「微信」按钮**(`/static/wechat_bind.html` 仍保留作独立/嵌入入口)扫码绑定(需个人微信 8.0.70+ 且灰度到 ClawBot 插件)。绑定后在微信「微信 ClawBot」对话即走 zcbot;**主动推送需用户近 24h 在微信开口过一次**(冷启动/超期推不出,退邮件兜底)。
|
||||||
- **PG**:`ZCBOT_DB_URL` 必填。本地 docker compose / 远端 dev / 生产任选;未设置时启动清晰报错,不引导 docker(§7.4)。
|
- **PG**:`ZCBOT_DB_URL` 必填。本地 docker compose / 远端 dev / 生产任选;未设置时启动清晰报错,不引导 docker(§7.4)。
|
||||||
- **Auth env**:`PLATFORM_KEY` + `JWT_SECRET` 任一缺失 web 启动 fail-fast。生成随机串:`python -c "import secrets; print(secrets.token_urlsafe(48))"`。
|
- **Auth env**:`PLATFORM_KEY` + `JWT_SECRET` 任一缺失 web 启动 fail-fast。生成随机串:`python -c "import secrets; print(secrets.token_urlsafe(48))"`。
|
||||||
- **用户管理**(`users.email/password_hash/role`,0005 UNIQUE(email)、0009 role):dev SPA 登录后端。发用户两条路径任选:CLI `main.py user add`(下方),或在登录页右下角"+ 管理员添加用户"链接(需先设 `ZCBOT_ADMIN_TOKEN` env,弹窗输入 email/密码/管理员口令/角色)。撤用户 `DELETE FROM users WHERE email=...`(先 DELETE 该 user 的 tasks)。**用户自助改密**:登录后顶栏「改密码」按钮(走 `POST /v1/auth/change_password`,需知道旧密码);改邮箱 / 用户忘了旧密码无法自助 → 手动 SQL(见故障兜底)。
|
- **用户管理**(`users.email/password_hash/role`,0005 UNIQUE(email)、0009 role):dev SPA 登录后端。发用户两条路径任选:CLI `main.py user add`(下方),或在登录页右下角"+ 管理员添加用户"链接(需先设 `ZCBOT_ADMIN_TOKEN` env,弹窗输入 email/密码/管理员口令/角色)。撤用户 `DELETE FROM users WHERE email=...`(先 DELETE 该 user 的 tasks)。**用户自助改密**:登录后顶栏「改密码」按钮(走 `POST /v1/auth/change_password`,需知道旧密码);改邮箱 / 用户忘了旧密码无法自助 → 手动 SQL(见故障兜底)。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.22.1"
|
__version__ = "0.22.2"
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 定时任务 modal(只读 + 停用/删除,DESIGN §8.5)— 复用 .sk-item/.sk-badge/.sk-empty */
|
/* 定时任务 modal(只读 + 停用/删除,DESIGN §8.5)— 复用 .sk-item/.sk-badge/.sk-empty */
|
||||||
|
#wechat-modal { z-index: 112; }
|
||||||
|
#wechat-modal .card { width: 440px; max-width: 94vw; }
|
||||||
|
#wechat-modal h3 { display: flex; align-items: center; gap: 8px; }
|
||||||
|
#wechat-modal h3 .spacer { flex: 1; }
|
||||||
|
#wx-body { padding: 16px; overflow: auto; }
|
||||||
|
#wx-body .wx-status { padding: 10px 12px; border-radius: 8px; font-size: 14px; margin-bottom: 14px; background: var(--code-bg, #f6f8fa); }
|
||||||
|
#wx-body .wx-status.ok { background: #e6f4ea; color: #1a7f37; }
|
||||||
|
#wx-body .wx-status.err { background: #ffebe9; color: #cf222e; }
|
||||||
|
#wx-body .wx-status.wait { background: #fff8c5; color: #7d4e00; }
|
||||||
|
#wx-body .wx-acts { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
#wx-qrbox { text-align: center; margin-top: 16px; }
|
||||||
|
#wx-qrbox img { width: 220px; height: 220px; border: 1px solid var(--line, #d0d7de); border-radius: 8px; }
|
||||||
#crons-modal { z-index: 112; }
|
#crons-modal { z-index: 112; }
|
||||||
#crons-modal .card {
|
#crons-modal .card {
|
||||||
width: 880px; max-width: 94vw; height: 78vh; max-height: 78vh;
|
width: 880px; max-width: 94vw; height: 78vh; max-height: 78vh;
|
||||||
|
|
@ -1283,6 +1295,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ───── 微信绑定 modal(ClawBot 扫码,§8.7)───── -->
|
||||||
|
<div id="wechat-modal" class="modal">
|
||||||
|
<div class="card">
|
||||||
|
<h3>
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||||
|
<span>绑定微信</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button id="wx-close" class="sk-x" title="关闭">✕</button>
|
||||||
|
</h3>
|
||||||
|
<div id="wx-body">
|
||||||
|
<div class="muted" style="font-size:12px;margin-bottom:12px;">绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 有互动时,定时简报 / 结果也能主动推给你。</div>
|
||||||
|
<div id="wx-state" class="wx-status wait">加载中…</div>
|
||||||
|
<div class="wx-acts">
|
||||||
|
<button id="wx-bind" class="small">绑定 / 重新绑定</button>
|
||||||
|
<button id="wx-test" class="small" disabled>发送测试</button>
|
||||||
|
<button id="wx-unbind" class="small danger" disabled>解绑</button>
|
||||||
|
</div>
|
||||||
|
<div id="wx-qrbox" hidden>
|
||||||
|
<img id="wx-qrimg" alt="微信扫码二维码">
|
||||||
|
<div class="muted" style="font-size:12px;margin-top:6px;">用手机微信「扫一扫」上面的二维码,在手机上确认授权。</div>
|
||||||
|
</div>
|
||||||
|
<ul class="muted" style="font-size:12px;margin-top:14px;padding-left:18px;line-height:1.7;">
|
||||||
|
<li>需个人微信 8.0.70+ 且已灰度到「ClawBot」插件(设置→插件)。</li>
|
||||||
|
<li>绑定后先在微信给「微信 ClawBot」发句话,主动推送才开启(24h 窗口)。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ───── 记忆查看 modal(只读;改记忆走对话)───── -->
|
<!-- ───── 记忆查看 modal(只读;改记忆走对话)───── -->
|
||||||
<div id="memory-modal" class="modal">
|
<div id="memory-modal" class="modal">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -1381,6 +1422,10 @@
|
||||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
|
||||||
<span>定时</span>
|
<span>定时</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="hd-wechat" title="绑定微信(ClawBot):扫码后在微信里直接和 zcbot 对话">
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||||
|
<span>微信</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="split-left" class="splitter" role="separator" aria-orientation="vertical" title="拖拽调整任务栏宽度"></div>
|
<div id="split-left" class="splitter" role="separator" aria-orientation="vertical" title="拖拽调整任务栏宽度"></div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { closeChpwModal } from "./auth.js";
|
||||||
import { closeSkillsModal } from "./skills.js";
|
import { closeSkillsModal } from "./skills.js";
|
||||||
import { closeMemoryModal } from "./memory.js";
|
import { closeMemoryModal } from "./memory.js";
|
||||||
import { closeCronsModal } from "./crons.js";
|
import { closeCronsModal } from "./crons.js";
|
||||||
|
import { closeWechatModal } from "./wechat.js";
|
||||||
import { closeFilePreview, closeMiniPreview } from "./preview.js";
|
import { closeFilePreview, closeMiniPreview } from "./preview.js";
|
||||||
import { closeSrcPicker, loadFiles } from "./files.js";
|
import { closeSrcPicker, loadFiles } from "./files.js";
|
||||||
import { loadFolderSuggestions } from "./newtask.js";
|
import { loadFolderSuggestions } from "./newtask.js";
|
||||||
|
|
@ -89,6 +90,7 @@ document.addEventListener("keydown", (e) => {
|
||||||
if ($("skills-modal").classList.contains("show")) { closeSkillsModal(); return; }
|
if ($("skills-modal").classList.contains("show")) { closeSkillsModal(); return; }
|
||||||
if ($("memory-modal").classList.contains("show")) { closeMemoryModal(); return; }
|
if ($("memory-modal").classList.contains("show")) { closeMemoryModal(); return; }
|
||||||
if ($("crons-modal").classList.contains("show")) { closeCronsModal(); return; }
|
if ($("crons-modal").classList.contains("show")) { closeCronsModal(); return; }
|
||||||
|
if ($("wechat-modal").classList.contains("show")) { closeWechatModal(); return; }
|
||||||
if ($("mini-preview-modal").classList.contains("show")) { closeMiniPreview(); return; }
|
if ($("mini-preview-modal").classList.contains("show")) { closeMiniPreview(); return; }
|
||||||
if ($("src-picker-modal").classList.contains("show")) { closeSrcPicker(); return; }
|
if ($("src-picker-modal").classList.contains("show")) { closeSrcPicker(); return; }
|
||||||
if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; }
|
if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// 微信绑定 modal(ClawBot 扫码,DESIGN §8.7):扫码绑定 + 自检发送 + 解绑。
|
||||||
|
// 左侧 rail「微信」按钮触发。后端:POST /v1/wechat/bind/qrcode、GET /v1/wechat/bind/status、
|
||||||
|
// GET /v1/wechat/bind、DELETE /v1/wechat/bind、POST /v1/wechat/test。
|
||||||
|
import { $ } from "./dom.js";
|
||||||
|
import { api } from "./api.js";
|
||||||
|
|
||||||
|
let _polling = false;
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
function setState(cls, msg) {
|
||||||
|
const el = $("wx-state");
|
||||||
|
el.className = "wx-status " + cls;
|
||||||
|
el.textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
const b = await api("GET", "/v1/wechat/bind");
|
||||||
|
if (b.bound) {
|
||||||
|
const push = b.can_push ? "可主动推送" : "需在微信里发条消息以开启主动推送";
|
||||||
|
setState("ok", "已绑定" + (b.user_im_id ? "" : "(待首次开口)") + " · " + push);
|
||||||
|
$("wx-test").disabled = false;
|
||||||
|
$("wx-unbind").disabled = false;
|
||||||
|
} else {
|
||||||
|
setState("wait", "尚未绑定。点「绑定」生成二维码,用手机微信扫。");
|
||||||
|
$("wx-test").disabled = true;
|
||||||
|
$("wx-unbind").disabled = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState("err", "查询失败: " + e.message +
|
||||||
|
"(确认已上 migration 0012 + 开 ZCBOT_WECHAT_BOT_ENABLED)");
|
||||||
|
$("wx-test").disabled = true;
|
||||||
|
$("wx-unbind").disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWechatModal() {
|
||||||
|
$("wechat-modal").classList.add("show");
|
||||||
|
$("wx-qrbox").hidden = true;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
export function closeWechatModal() {
|
||||||
|
_polling = false;
|
||||||
|
$("wechat-modal").classList.remove("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bindFlow() {
|
||||||
|
if (_polling) return;
|
||||||
|
_polling = true;
|
||||||
|
$("wx-bind").disabled = true;
|
||||||
|
try {
|
||||||
|
while (_polling) { // 二维码过期自动换新
|
||||||
|
const q = await api("POST", "/v1/wechat/bind/qrcode");
|
||||||
|
$("wx-qrimg").src = q.qr_png;
|
||||||
|
$("wx-qrbox").hidden = false;
|
||||||
|
setState("wait", "等待扫码…(二维码约 1 分钟过期,会自动换新)");
|
||||||
|
let expired = false;
|
||||||
|
while (_polling && !expired) {
|
||||||
|
let s;
|
||||||
|
try {
|
||||||
|
s = await api("GET", "/v1/wechat/bind/status?qrcode_id=" +
|
||||||
|
encodeURIComponent(q.qrcode_id));
|
||||||
|
} catch (e) { await sleep(2000); continue; }
|
||||||
|
if (s.status === "confirmed") {
|
||||||
|
_polling = false;
|
||||||
|
$("wx-qrbox").hidden = true;
|
||||||
|
setState("ok", "绑定成功!去微信「微信 ClawBot」发句话试试。");
|
||||||
|
await refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.status === "expired") { expired = true; break; }
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState("err", "绑定出错: " + e.message);
|
||||||
|
} finally {
|
||||||
|
_polling = false;
|
||||||
|
$("wx-bind").disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───── 顶层绑定 ─────
|
||||||
|
$("hd-wechat").onclick = openWechatModal;
|
||||||
|
$("wx-close").onclick = closeWechatModal;
|
||||||
|
$("wechat-modal").addEventListener("click", (e) => {
|
||||||
|
if (e.target.id === "wechat-modal") closeWechatModal(); // 点遮罩关闭
|
||||||
|
});
|
||||||
|
$("wx-bind").onclick = bindFlow;
|
||||||
|
$("wx-unbind").onclick = async () => {
|
||||||
|
_polling = false;
|
||||||
|
if (!confirm("确定解绑微信?")) return;
|
||||||
|
try {
|
||||||
|
await api("DELETE", "/v1/wechat/bind");
|
||||||
|
$("wx-qrbox").hidden = true;
|
||||||
|
await refresh();
|
||||||
|
} catch (e) { setState("err", "解绑失败: " + e.message); }
|
||||||
|
};
|
||||||
|
$("wx-test").onclick = async () => {
|
||||||
|
$("wx-test").disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await api("POST", "/v1/wechat/test");
|
||||||
|
if (r.ok) setState("ok", "测试消息已发送,去微信查收。");
|
||||||
|
else setState("err", "推送未送达(" + r.reason +
|
||||||
|
")。多半是超 24h 没在微信互动:发条消息再试。");
|
||||||
|
} catch (e) {
|
||||||
|
setState("err", "测试失败: " + e.message);
|
||||||
|
} finally { $("wx-test").disabled = false; }
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue