9.5 KiB
9.5 KiB
任务:实现 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() 中创建新表:
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:
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, StreamingResponse
在"命令行入口"注释之前,添加以下内容:
常量:
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):
- 调用
email_manager.get_client(email_addr)获取客户端 - 调用
client.get_messages_with_content(top=30)获取最近 30 封 INBOX 邮件 - 遍历邮件,匹配
sender.emailAddress.address== ANTHROPIC_SENDER(不区分大小写) - 匹配标题关键词,记录最新的 payment_time 和 refund_time
- 确定状态:两者都有则比较时间取最新;只有支付=paid;只有退款=refunded;都没有=unknown
- 调用
db_manager.set_claude_payment_status()写入数据库 - 同时双写标签:获取现有标签,移除旧的"已支付Claude"/"已退款"标签,根据状态添加新标签
- 返回
{email, status, payment_time, refund_time} - 出错时记录 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 的"导出"按钮之后、"刷新"按钮之前,添加:
<button class="btn btn-primary" id="claudePaymentBtn">
<i class="bi bi-credit-card"></i>
<span>Claude检测</span>
</button>
表格表头:在"令牌"和"操作"之间,添加新列:
<th>Claude状态</th>
同时把所有 colspan="6" 改为 colspan="7"(空状态行)。
4. static/script.js — SSE 扫描 + 状态渲染
constructor 中新增:
this.claudePaymentStatuses = {};
bindAccountEvents() 中新增按钮绑定:
document.getElementById('claudePaymentBtn').addEventListener('click', () => this.startClaudePaymentCheck());
loadAccounts() 中:在 renderAccounts() 调用前,先 await this.loadClaudePaymentStatuses()。同时把所有 colspan="6" 改为 colspan="7"。
renderAccounts() 中:在令牌列 </td> 和操作列 <td> 之间,插入新列:
<td>${this.renderClaudeStatusBadge(email)}</td>
新增 4 个方法:
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 '<span class="claude-badge claude-unknown">未检测</span>';
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 `<span class="claude-badge ${s.cls}"${title}>${s.label}</span>`;
}
async startClaudePaymentCheck() {
const btn = document.getElementById('claudePaymentBtn');
if (!btn) return;
btn.disabled = true;
const origHtml = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i><span>检测中...</span>';
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 = `<i class="bi bi-hourglass-split"></i><span>${data.current}/${data.total}</span>`;
} 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 = '<i class="bi bi-check-circle"></i><span>完成</span>';
setTimeout(() => { btn.innerHTML = origHtml; btn.disabled = false; }, 2000);
}
} catch (e) {}
}
}
} catch (err) {
console.error('Claude支付检测失败:', err);
btn.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>失败</span>';
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 之前或之后均可)添加:
/* 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; }
验证步骤
python mail_api.py web→ 访问http://localhost:5001/- 表格应多出一列"Claude状态",初始都显示灰色"未检测"
- 点击导航栏"Claude检测"按钮 → 按钮显示进度
1/N、2/N... - 扫描完成后,有 Anthropic 收据的账号显示绿色"已支付",有退款的显示红色"已退款"
- 鼠标悬停徽章可看到支付/退款时间
- 刷新页面后状态仍然保留(数据库持久化)