# 任务:实现 Claude 支付检测工具 ## 需求 自动扫描邮箱收件箱,检测 Anthropic 的支付/退款邮件,自动给账号打标签并记录时间。 **匹配规则:** - 发件人:`invoice+statements@mail.anthropic.com` - 标题含 `Your receipt from Anthropic` → 标记 **已支付Claude**(status=`paid`),记录支付时间 - 标题含 `Your refund from Anthropic` → 标记 **已退款**(status=`refunded`),记录退款时间 - 时间取邮件的 `receivedDateTime` - 如果同时存在支付和退款邮件,比较时间取最新的作为当前状态 --- ## 实现方案(共 5 个改动点) ### 1. `database.py` — 新增表 + 4 个方法 在 `init_database()` 中创建新表: ```sql CREATE TABLE IF NOT EXISTS claude_payment_status ( email TEXT PRIMARY KEY, status TEXT DEFAULT 'unknown', -- paid / refunded / unknown / error payment_time TEXT, refund_time TEXT, checked_at TEXT ) ``` 新增 3 个 async 方法(参考现有 `get_account_tags` / `set_account_tags` 的写法): - `set_claude_payment_status(email, status, payment_time=None, refund_time=None)` — INSERT OR REPLACE,checked_at 取当前时间 - `get_claude_payment_status(email)` → Optional[Dict] - `get_all_claude_payment_statuses()` → Dict[str, Dict] 修改 `delete_account()`:在删除账户时联动删除 `claude_payment_status` 表中对应记录。 ### 2. `mail_api.py` — 检测逻辑 + 2 个新端点 添加 `StreamingResponse` 到 import: ```python from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, StreamingResponse ``` 在"命令行入口"注释之前,添加以下内容: **常量:** ```python ANTHROPIC_SENDER = "invoice+statements@mail.anthropic.com" RECEIPT_KEYWORD = "Your receipt from Anthropic" REFUND_KEYWORD = "Your refund from Anthropic" ``` **核心检测函数** `_check_claude_payment_for_account(email_addr)`: 1. 调用 `email_manager.get_client(email_addr)` 获取客户端 2. 调用 `client.get_messages_with_content(top=30)` 获取最近 30 封 INBOX 邮件 3. 遍历邮件,匹配 `sender.emailAddress.address` == ANTHROPIC_SENDER(不区分大小写) 4. 匹配标题关键词,记录最新的 payment_time 和 refund_time 5. 确定状态:两者都有则比较时间取最新;只有支付=paid;只有退款=refunded;都没有=unknown 6. 调用 `db_manager.set_claude_payment_status()` 写入数据库 7. 同时双写标签:获取现有标签,移除旧的"已支付Claude"/"已退款"标签,根据状态添加新标签 8. 返回 `{email, status, payment_time, refund_time}` 9. 出错时记录 error 状态并返回 `{email, status: "error", message: str(e)}` **端点 1:** `POST /api/tools/check-claude-payment` — SSE 流式扫描 - 获取所有账户 `load_accounts_config()` - 逐个调用 `_check_claude_payment_for_account()` - SSE 事件格式(每行 `data: {JSON}\n\n`): - `{type: "start", total: N}` - `{type: "progress", current: i, total: N, email: "..."}`(扫描前发送) - `{type: "result", current: i, total: N, email, status, payment_time, refund_time}`(扫描后发送) - `{type: "done", total: N}` - 账户之间 `await asyncio.sleep(0.5)` 避免限速 - 返回 `StreamingResponse(event_generator(), media_type="text/event-stream")` **端点 2:** `GET /api/tools/claude-payment-status` — 获取缓存状态 - 调用 `db_manager.get_all_claude_payment_statuses()` - 返回 `ApiResponse(success=True, data=statuses)` ### 3. `static/index.html` — 按钮 + 表格列 **导航栏**:在 `nav-actions` 的"导出"按钮之后、"刷新"按钮之前,添加: ```html ``` **表格表头**:在"令牌"和"操作"之间,添加新列: ```html Claude状态 ``` 同时把所有 `colspan="6"` 改为 `colspan="7"`(空状态行)。 ### 4. `static/script.js` — SSE 扫描 + 状态渲染 **constructor** 中新增: ```javascript this.claudePaymentStatuses = {}; ``` **bindAccountEvents()** 中新增按钮绑定: ```javascript document.getElementById('claudePaymentBtn').addEventListener('click', () => this.startClaudePaymentCheck()); ``` **loadAccounts()** 中:在 `renderAccounts()` 调用前,先 `await this.loadClaudePaymentStatuses()`。同时把所有 `colspan="6"` 改为 `colspan="7"`。 **renderAccounts()** 中:在令牌列 `` 和操作列 `` 之间,插入新列: ```javascript ${this.renderClaudeStatusBadge(email)} ``` **新增 4 个方法:** ```javascript async loadClaudePaymentStatuses() { try { const resp = await fetch('/api/tools/claude-payment-status'); if (resp.ok) { const result = await resp.json(); if (result.success) this.claudePaymentStatuses = result.data || {}; } } catch (e) { console.error('加载Claude支付状态失败:', e); } } renderClaudeStatusBadge(email) { const info = this.claudePaymentStatuses[email]; if (!info) return '未检测'; const map = { paid: { cls: 'claude-paid', label: '已支付' }, refunded: { cls: 'claude-refunded', label: '已退款' }, unknown: { cls: 'claude-unknown', label: '未知' }, error: { cls: 'claude-error', label: '错误' } }; const s = map[info.status] || map.unknown; let tips = []; if (info.payment_time) tips.push('支付: ' + info.payment_time); if (info.refund_time) tips.push('退款: ' + info.refund_time); if (info.checked_at) tips.push('检测: ' + info.checked_at); const title = tips.length ? ` title="${this.escapeHtml(tips.join('\n'))}"` : ''; return `${s.label}`; } async startClaudePaymentCheck() { const btn = document.getElementById('claudePaymentBtn'); if (!btn) return; btn.disabled = true; const origHtml = btn.innerHTML; btn.innerHTML = '检测中...'; try { const response = await fetch('/api/tools/check-claude-payment', { method: 'POST' }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const data = JSON.parse(line.slice(6)); if (data.type === 'progress') { btn.innerHTML = `${data.current}/${data.total}`; } else if (data.type === 'result') { this.claudePaymentStatuses[data.email] = { status: data.status, payment_time: data.payment_time || null, refund_time: data.refund_time || null, checked_at: new Date().toLocaleString('zh-CN') }; this.updateClaudeBadgeInTable(data.email); } else if (data.type === 'done') { btn.innerHTML = '完成'; setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000); } } catch (e) {} } } } catch (err) { console.error('Claude支付检测失败:', err); btn.innerHTML = '失败'; setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000); } } updateClaudeBadgeInTable(email) { const tbody = document.getElementById('accountTableBody'); if (!tbody) return; for (const row of tbody.querySelectorAll('tr')) { const firstTd = row.querySelector('td'); if (firstTd && firstTd.textContent.includes(email)) { // Claude状态列 = 第6个td(index 5,在令牌后、操作前) const cells = row.querySelectorAll('td'); if (cells.length >= 6) { cells[5].innerHTML = this.renderClaudeStatusBadge(email); } break; } } } ``` ### 5. `static/style.css` — 徽章样式 在文件末尾(`@media` 之前或之后均可)添加: ```css /* Claude支付状态徽章 */ .claude-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; cursor: default; white-space: nowrap; } .claude-paid { background: rgba(16, 185, 129, 0.12); color: #059669; } .claude-refunded { background: rgba(245, 87, 108, 0.12); color: #e11d48; } .claude-unknown { background: rgba(148, 163, 184, 0.15); color: #64748b; } .claude-error { background: rgba(245, 158, 11, 0.15); color: #d97706; } ``` --- ## 验证步骤 1. `python mail_api.py web` → 访问 `http://localhost:5001/` 2. 表格应多出一列"Claude状态",初始都显示灰色"未检测" 3. 点击导航栏"Claude检测"按钮 → 按钮显示进度 `1/N`、`2/N`... 4. 扫描完成后,有 Anthropic 收据的账号显示绿色"已支付",有退款的显示红色"已退款" 5. 鼠标悬停徽章可看到支付/退款时间 6. 刷新页面后状态仍然保留(数据库持久化)