From 18ae09af12b53e962020b9254101bddaa1d340a1 Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Fri, 6 Mar 2026 14:10:06 +0800 Subject: [PATCH] Claude detection respects payment filter, add project skill - Backend check-claude-payment accepts optional emails list - Frontend sends filtered emails when filter is active - Button label updates to show current filter scope - Add project skill for development guidance Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/outlook-manager/SKILL.md | 168 ++++++++++++++++++++++++ mail_api.py | 16 ++- static/script.js | 37 +++++- 3 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/outlook-manager/SKILL.md diff --git a/.claude/skills/outlook-manager/SKILL.md b/.claude/skills/outlook-manager/SKILL.md new file mode 100644 index 0000000..4c186e1 --- /dev/null +++ b/.claude/skills/outlook-manager/SKILL.md @@ -0,0 +1,168 @@ +--- +name: outlook-manager +description: | + Outlook 邮箱管理系统全能助手。涵盖项目开发、部署运维、数据库操作、前端 UI、API 接口等所有方面。 + 当用户在本项目中进行任何开发、修改、调试、部署、排错时都应使用此 skill。 + 触发场景包括但不限于:添加功能、修改页面、数据库字段变更、Docker 部署、Redis 缓存、 + 邮件相关操作、Claude 支付检测、账号管理、前端表格/弹窗/筛选等。 +--- + +# Outlook 邮箱管理系统 — 项目全能助手 + +## 项目概览 + +基于 FastAPI 的 Outlook 邮箱管理系统,通过 IMAP + OAuth2 读取邮件,支持批量账号管理、Claude 支付状态检测、代理管理等功能。 + +**技术栈:** Python 3.12 / FastAPI / SQLite (WAL) / Redis / Docker / 原生 JS 前端 + +**Git 仓库:** `https://git.586vip.cn/huangzhenpc/claude-outlonok.git` + +**服务器部署路径:** `/opt/claude-outlonok` + +## 项目结构 + +``` +outlookmanager_v2/ +├── mail_api.py # 主应用入口,所有 API 路由(FastAPI) +├── config.py # 全局配置常量(OAuth、IMAP、管理员令牌) +├── database.py # SQLite 数据库管理器(DatabaseManager 单例 db_manager) +├── cache.py # Redis 缓存模块(RedisCache 单例 cache) +├── auth.py # OAuth2 令牌获取(httpx 异步) +├── imap_client.py # IMAP 邮件客户端(按需连接,自动重试) +├── models.py # Pydantic 请求/响应模型 +├── requirements.txt # Python 依赖 +├── Dockerfile # Docker 镜像构建 +├── docker-compose.yml # Docker 编排(host 网络模式) +├── .env.example # 环境变量示例 +├── data/ +│ └── outlook_manager.db # SQLite 数据库(git 跟踪,随项目迁移) +└── static/ + ├── index.html # 主页(账号列表 + 邮件查看器) + ├── script.js # 主页 JS(MailManager 类) + ├── style.css # 全局样式 + ├── admin.html # 管理后台页面 + └── admin.js # 管理后台 JS +``` + +## 数据库表结构(SQLite) + +### accounts(邮箱账号) +| 字段 | 类型 | 说明 | +|------|------|------| +| email | TEXT PK | 邮箱地址 | +| password | TEXT | 密码 | +| client_id | TEXT | OAuth 客户端 ID | +| refresh_token | TEXT | OAuth 刷新令牌 | +| created_at | TIMESTAMP | 创建时间 | + +### claude_payment_status(Claude 支付状态) +| 字段 | 类型 | 说明 | +|------|------|------| +| email | TEXT PK | 邮箱地址 | +| status | TEXT | 状态:paid/refunded/suspended/error/unknown | +| payment_time | TEXT | 支付时间 | +| refund_time | TEXT | 退款时间 | +| suspended_time | TEXT | 封号时间 | +| refund_received | TEXT | 退款是否到账:0/1 | +| refund_received_at | TEXT | 退款到账时间 | +| title | TEXT | 收据号 | +| remark | TEXT | 备注 | +| card_number | TEXT | 卡号后4位 | +| proxy | TEXT | 代理地址 | +| proxy_expire_days | INT | 代理有效天数 | +| proxy_share | TEXT | exclusive/shared | +| proxy_purchase_date | TEXT | 代理购买日期 | +| checked_at | TEXT | 最后检测时间 | + +### account_tags(账号标签) +| 字段 | 类型 | 说明 | +|------|------|------| +| email | TEXT UNIQUE | 邮箱(外键关联 accounts,级联删除) | +| tags | TEXT | JSON 数组字符串 | + +### email_cache(邮件缓存) +| 字段 | 类型 | 说明 | +|------|------|------| +| email | TEXT | 邮箱 | +| message_id | TEXT | 邮件 ID | +| data | TEXT | JSON 邮件数据 | + +### system_config(系统配置) +| 字段 | 类型 | 说明 | +|------|------|------| +| key | TEXT PK | 配置键 | +| value | TEXT | 配置值 | + +## Redis 缓存策略 + +连接地址写死在 `cache.py`:`redis://:redis_XMiXNa@127.0.0.1:6379/0` + +| 缓存键 | TTL | 说明 | +|--------|-----|------| +| cache:accounts | 5 分钟 | 全量账户列表 | +| cache:messages:{email}:{folder} | 3 分钟 | 邮件列表 | +| cache:payment_status | 10 分钟 | 支付状态 | + +缓存失效时机:导入/删除账号、检测支付、更新备注/代理时自动清除对应缓存。 + +Redis 不可用时自动降级,不影响功能。 + +## 核心 API 路由 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/accounts/detailed | 分页账号列表(支持搜索、缓存) | +| GET | /api/messages | 邮件列表(支持 refresh=true 跳过缓存) | +| POST | /api/accounts/import | 文本格式批量导入账号 | +| DELETE | /api/account/{email} | 删除单个账号 | +| GET | /api/export | 导出账号为 txt | +| POST | /api/tools/check-claude-payment | SSE 流式批量检测 Claude 支付 | +| POST | /api/tools/check-claude-payment/{email} | 单个检测 | +| GET | /api/tools/claude-payment-status | 获取所有支付状态 | +| POST | /api/tools/refund-received/{email} | 切换退款到账状态 | +| POST | /api/tools/claude-payment-note/{email} | 更新备注/代理 | + +## 前端架构 + +### 页面访问密码 +前端登录密码写死在 `script.js` 和 `admin.js`:`oadmin123`,使用 `sessionStorage` 保持登录状态。 + +### 主页 MailManager 类(script.js) +- **账号视图**:表格展示,支持搜索、分页、支付状态筛选 +- **邮件视图**:双栏布局(左侧列表 + 右侧详情 iframe) +- **弹窗组件**:导入、凭证查看、备注编辑、代理编辑、确认对话框 +- **筛选器**:全部/已支付/未支付/已退款/退款已到账/退款未到账/已封号/未检测 + +### 表格列顺序 +`#` → 邮箱 → 密码 → 凭证 → 支付状态 → 退款状态 → 支付时间 → 退款时间 → 封号时间 → 到账时间 → 备注/卡号 → 代理 → 操作 + +### colspan +当前表格 colspan 值为 **13**。增减列时需同步更新 `index.html` 和 `script.js` 中所有 `colspan` 值。 + +## 开发规范 + +### 新增数据库字段的完整流程 +1. `database.py` — CREATE TABLE 语句中添加字段 +2. `database.py` — 兼容旧表的 ALTER TABLE 循环中添加字段和默认值 +3. `database.py` — `allowed` 集合中添加字段(如果需要通过 `update_claude_payment_note` 更新) +4. `database.py` — INSERT OR REPLACE 语句中添加字段(保留旧值逻辑) +5. `database.py` — 所有 SELECT 查询和结果字典中添加字段 +6. `mail_api.py` — 相关 API 端点处理新字段 +7. `cache.py` — 数据变更时清除相关缓存 +8. 前端 — 表头、渲染、colspan 同步更新 + +### 提交代码 +- 提交到 `https://git.586vip.cn/huangzhenpc/claude-outlonok.git` +- 服务器更新命令:`cd /opt/claude-outlonok && git pull && docker compose up -d --build` + +### Docker 部署 +- 使用 `network_mode: host`,服务直接监听宿主机 **5001** 端口 +- 外部访问端口 **5001** +- SQLite 数据库通过 `./data:/app/data` 挂载持久化 +- 数据库文件 git 跟踪,随项目迁移 + +### 前端修改注意事项 +- 增减表格列时必须同步更新 `colspan` 值(index.html + script.js) +- `renderClaudeColumns` 中 no-info 和 has-info 两个分支的 `` 数量必须一致 +- `updateClaudeBadgeInTable` 中的 cells 索引(当前从 index 5 开始,共 8 列)需匹配实际列数 +- 操作列按钮通过 `renderRefundBtn` 动态渲染,仅已退款账号显示退款按钮 diff --git a/mail_api.py b/mail_api.py index c91a299..4d7980d 100644 --- a/mail_api.py +++ b/mail_api.py @@ -1222,10 +1222,20 @@ async def _check_claude_payment_for_account(email_addr: str) -> dict: } @app.post("/api/tools/check-claude-payment") -async def check_claude_payment(): - """SSE流式扫描所有账户的Claude支付状态""" +async def check_claude_payment(request: Request): + """SSE流式扫描账户的Claude支付状态,支持传入指定邮箱列表""" + try: + body = await request.json() + target_emails = body.get('emails', []) if body else [] + except Exception: + target_emails = [] + accounts = await load_accounts_config() - emails = list(accounts.keys()) + if target_emails: + # 只检测指定的邮箱(且必须在账户中存在) + emails = [e for e in target_emails if e in accounts] + else: + emails = list(accounts.keys()) async def event_generator(): total = len(emails) diff --git a/static/script.js b/static/script.js index 2ffe7e4..3a08e69 100644 --- a/static/script.js +++ b/static/script.js @@ -110,6 +110,7 @@ class MailManager { this.paymentFilter = e.target.value; this.page = 1; this.loadAccounts(); + this.updateClaudeBtnLabel(); }); // 刷新 @@ -1144,15 +1145,49 @@ class MailManager { } } + updateClaudeBtnLabel() { + const btn = document.getElementById('claudePaymentBtn'); + if (!btn || btn.disabled) return; + const filterSelect = document.getElementById('paymentFilter'); + const filterText = filterSelect.options[filterSelect.selectedIndex].text; + if (this.paymentFilter) { + btn.innerHTML = `检测(${filterText})`; + } else { + btn.innerHTML = 'Claude检测'; + } + } + async startClaudePaymentCheck() { const btn = document.getElementById('claudePaymentBtn'); if (!btn) return; + + // 根据筛选条件决定检测范围 + let targetEmails = []; + if (this.paymentFilter && this._allAccounts) { + targetEmails = this._allAccounts + .filter(acc => this._matchPaymentFilter(acc.email)) + .map(acc => acc.email); + } + + const label = this.paymentFilter + ? `检测 ${targetEmails.length} 个` + : '全部检测'; + + if (targetEmails.length === 0 && this.paymentFilter) { + this.showToast('当前筛选条件下没有账号', 'warning'); + return; + } + btn.disabled = true; const origHtml = btn.innerHTML; btn.innerHTML = '检测中...'; try { - const response = await fetch('/api/tools/check-claude-payment', { method: 'POST' }); + const response = await fetch('/api/tools/check-claude-payment', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ emails: targetEmails }) + }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = '';