{"openapi":"3.1.0","info":{"title":"TwtAPI Backend","description":"API proxy & billing platform for Twitter/RapidAPI","version":"0.1.0"},"paths":{"/captcha":{"get":{"summary":"Captcha Image","description":"生成验证码图片，并把答案写入当前会话。\n\n参数:\n    request: 用于读取 session_id Cookie。\n\n返回:\n    image/png 响应体；若本次新建了会话，额外下发 Set-Cookie: session_id。\n\n副作用:\n    写 Redis（新增或覆盖会话中的 captcha 字段）。这里刻意复用已有会话而不是\n    每次重建，否则用户在同一浏览器里开多个页面时，后一个页面的会话会把前一\n    个页面的会话顶掉，导致前一个页面提交的验证码必然失效。","operationId":"captcha_image_captcha_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/oauth/google/start":{"get":{"summary":"Google Oauth Start","description":"OAuth 第一步：生成 state 并 302 跳转到 Google 授权页。\n\n参数 lang：用户界面语言；returnUrl：登录成功后希望回到的前端站内路径。\n\n为什么要用 state：回调请求是从 Google 域名跳回来的，浏览器带不回本站的任何\n上下文，所以把「语言 + 回跳地址」连同随机 state 一起存进 Redis，回调时凭 state\n取回。state 同时充当 CSRF 令牌，防止别人伪造回调把攻击者的 Google 账号\n绑到受害者会话上。\n返回：RedirectResponse 到 Google 授权页；未配置 Google 凭证时返回 error(500)。\n副作用：写 Redis（state 记录，TTL 10 分钟）。","operationId":"google_oauth_start_auth_oauth_google_start_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}},{"name":"returnUrl","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Returnurl"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/oauth/google/callback":{"get":{"summary":"Google Oauth Callback","description":"OAuth 第二步：处理 Google 回调，换用户信息、建会话，再 302 回前端。\n\n主流程：校验 state（一次性）-> 用 code 换 userinfo -> 校验邮箱已验证\n-> 绑定/创建本站用户 -> 风控记录 -> 建 session 并种 cookie -> 跳回 return_url。\n\n参数 code/state 来自 Google 回调 query；error_description 是用户在 Google 侧\n点了「拒绝授权」时 Google 带回来的原因。\n返回：成功是带 Set-Cookie 的 302；任何失败都 302 回登录页并附 oauth_error 文案。\n  —— 这里刻意不向外抛 5xx，因为回调是浏览器直接导航，返回 JSON 报错用户看不懂。\n副作用：写库（用户/绑定/last_lang/风控事件）、写 Redis（session）、外部 HTTP 请求。\n\n注意：整个流程被一个宽泛的 try/except 包住，异常只记日志、对外统一提示\n\"Google login failed\"，避免把栈信息或 token 细节泄露到 URL 上。","operationId":"google_oauth_callback_auth_oauth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error_description","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Description"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sendEmailCode":{"post":{"summary":"Send Email Code","description":"发送注册邮箱验证码。同一邮箱 60 秒内只能请求一次；验证码 5 分钟内有效。\n\n返回：成功是 success；命中冷却返回 40008；邮箱已注册返回 40003；发信失败返回 50011。\n副作用：写 Redis（验证码 + 冷却键）、向外部邮件服务发起发送请求。\n\n顺序上先写 Redis 再发信：这样即使发信失败，冷却键也已经落下，\n用户必须等冷却结束才能重试 —— 用「可能多等 60 秒」换「不会因发信故障被无限刷」。\n另外，对已注册邮箱直接返回 email_exists，等于暴露了「该邮箱是否已注册」，\n这是产品上为了方便用户（提示去登录）而接受的取舍。","operationId":"send_email_code_auth_sendEmailCode_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendEmailCodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/register":{"post":{"summary":"Register","description":"注册账号：校验密码长度 -> 图形验证码 -> 邮箱验证码 -> 建号并记录风控。\n\n返回：成功时回传 user_id / api_key / mcp_key；各类失败返回带 sub_code 的 error。\n副作用：写库（建 User、last_lang、风控事件/画像）、写 Redis（消费验证码）。\n注意：注册成功后不会自动登录、不下发 session cookie，前端需要再调一次 /login。","operationId":"register_auth_register_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"post":{"summary":"Login","description":"账号密码登录，成功后下发 session cookie。\n\n主流程：\n  1. 按 email 读登录失败计数（Redis），决定本次是否必须带图形验证码；\n  2. 校验密码；\n  3. 校验账号状态（冻结即拒）；\n  4. 跑一次登录风控，风控要求验证码而请求没带则中断；\n  5. 清空失败计数，创建新 session 并种 cookie。\n\n返回：成功回传 user_id / api_key / mcp_key；失败返回带 sub_code 的 error\n（40111 密码错、40301 账号冻结、40002 需要验证码）。\n副作用：写 Redis（失败计数、session）、写库（last_lang、风控事件）。\n\n安全设计：失败计数达到 CAPTCHA_FAIL_THRESHOLD 后强制人机校验；\n注意计数是按 email 而非 IP 维度，属于「宁可误伤也要挡住撞库」的取舍。","operationId":"login_auth_login_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/getInfo":{"get":{"summary":"Get Info","description":"前端启动时拉取当前登录态，未登录也会给一个空 session。\n\n返回：已登录 -> {is_login: True, user_id, email, api_key, is_admin}；\n      未登录 -> {is_login: False, is_admin: False}。\n副作用：写 Redis（未登录时新建空 session；stage_user_last_lang 是暂存，稍后批量落库）。","operationId":"get_info_auth_getInfo_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/logout":{"post":{"summary":"Logout","description":"退出登录：删除 Redis 中的会话并清掉 cookie。\n\n返回：success(退出成功文案)。\n副作用：写 Redis（删 session）。\n注意：只失效当前这一个 session_id，用户在其他设备/浏览器的登录态不受影响。","operationId":"logout_auth_logout_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/checkCaptchaRequired":{"post":{"summary":"Check Captcha Required","description":"供前端在提交登录前询问「这次要不要显示图形验证码」。\n\n判定来源有两处：Redis 登录失败计数（先看，便宜），以及风控画像里的历史风险标记。\n返回：success(data={\"captcha_required\": bool})。\n副作用：只读（风控画像是读缓存/DB）。","operationId":"check_captcha_required_auth_checkCaptchaRequired_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckCaptchaRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/sendResetPasswordEmail":{"post":{"summary":"Send Reset Password Email","description":"发起找回密码：校验图形验证码后生成一次性重置 token。\n\n返回：success(reset_email_sent)；邮箱不存在返回 40401。\n副作用：写 Redis（reset_token:{token} -> user_id，TTL 1800 秒），并写日志。\n\n注意：目前真正的邮件通道还没接上，token 只打在日志里（见下方 TODO），\n也就是说这个接口现在只能靠运维从日志里取链接；同时「邮箱不存在返回 404」\n同样会暴露账号是否注册。","operationId":"send_reset_password_email_auth_sendResetPasswordEmail_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResetEmailRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/resetPassword":{"post":{"summary":"Reset Password","description":"凭邮件里的 token 重置密码（无需登录）。\n\n流程：密码长度 -> 图形验证码 -> 校验 token -> 改密 -> 删除 token。\n返回：成功 success；密码过短 40005；验证码错 40001；token 无效 40006。\n副作用：写库（更新 password_hash）、写 Redis（消费 token）。\n\n安全性：token 用后即删、30 分钟过期，且必须配合图形验证码提交，\n因此光拿到 token 还不能重置；但重置成功后并不会踢掉已有的登录会话。","operationId":"reset_password_auth_resetPassword_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/changePassword":{"post":{"summary":"Change Password","description":"已登录用户改密：先验旧密码，再写入新密码。\n\nLogged-in user: verify current password, then set new password.\n\n参数 session 由 require_login 注入，未登录会直接 401（sub_code=40101）。\n返回：成功 success；新密码过短 40005；用户不存在 40402；旧密码错 40111。\n副作用：写库（更新 password_hash）—— 用户身份取 session 而非请求体，避免越权改他人密码。","operationId":"change_password_auth_changePassword_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/order":{"post":{"summary":"Create Order","description":"创建订单（下单入口），只落 pending 单，不涉及任何实际收款。\n\n已登录则关联 user_id，未登录（匿名）也能下单，此时 user_id 为空。\nPrice comes from the plans table.\n\n价格与计费方式一律以后端 plans 表为准，前端传来的 plan_type/plan_name 只用来定位套餐，\n杜绝篡改价格。\n\n主流程：\n  1. 按 plan_type + plan_name 查启用中的套餐，查不到即拒（40007）；\n  2. 解析套餐 features（展示用快照）；\n  3. 决定计费方式与成交价：订阅单且订阅价 > 0 用 subscription_price，否则用 price；\n  4. 0 元单一律降级成一次性（不得成为订阅）；\n  5. api 类型的免费套餐不允许下单（注册时已赠送，兼容旧前端）；\n  6. 登录用户领 0 元试用：先过风控 deny_free_trial，再加行锁做重复领取检查；\n  7. 登录用户买付费订阅：加行锁做重复下单检查；\n  8. 落一张 pending 单，expires_at = 现在 + ORDER_PENDING_TIMEOUT_MINUTES。\n\n返回：success(data={order_id, order_no, status})；各类重复/非法下单返回带 sub_code 的 error。\n副作用：写库（orders、风控事件/画像）；锁行会占用数据库连接直到事务结束。\n\n并发说明：查询与插入之间不是原子的，靠 _for_update 锁住 users 行把同一用户的\n下单流程串行化；匿名（user_id 为空）路径拿不到任何锁，因此不享受这层保护。","operationId":"create_order_auth_order_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/getApiKey":{"get":{"summary":"Get Api Key","description":"返回当前用户的 api_key（通用 HTTP 网关凭据）。\n\n用户不存在时返回 404 + sub_code=40402（例如 session 里的用户已被删除）。","operationId":"get_api_key_myapi_getApiKey_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/getMcpKey":{"get":{"summary":"Get Mcp Key","description":"返回当前用户的 mcp_key（MCP 协议接入凭据）。","operationId":"get_mcp_key_myapi_getMcpKey_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/getSkillKey":{"get":{"summary":"Get Skill Key","description":"返回当前用户的 skill_key（Agent / Skill 工具调用凭据）。","operationId":"get_skill_key_myapi_getSkillKey_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/resetApiKey":{"post":{"summary":"Reset Api Key","description":"重新生成 api_key。\n\nValueError（用户不存在）会被吞掉并转成 404；不向外暴露 service 层的异常类型，\n也避免 500。","operationId":"reset_api_key_myapi_resetApiKey_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/resetMcpKey":{"post":{"summary":"Reset Mcp Key","description":"重新生成 mcp_key。","operationId":"reset_mcp_key_myapi_resetMcpKey_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/resetSkillKey":{"post":{"summary":"Reset Skill Key","description":"重新生成 skill_key。","operationId":"reset_skill_key_myapi_resetSkillKey_post","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/dashboard":{"get":{"summary":"Dashboard","description":"Return all data needed by the user dashboard in a single call.\n\n中文：一次返回 dashboard 所需的全部数据（前端首屏只发一个请求）。\n\n为什么要先单独查一次 user：缓存命中时 _build_dashboard_data 不会执行，\n那样被删除的用户会继续被返回缓存数据。所以「用户存在性」这一步必须\n在缓存之前做，属于刻意为之的额外查询。","operationId":"dashboard_myapi_dashboard_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/status":{"get":{"summary":"Api Status","description":"Return account status, credit balance, and current quota usage for an API key.\n\n中文：给「持 API key 的调用方」自查账户状态用（即文档里的 status 接口），\n因此鉴权走 get_api_key_user，与后台的 Cookie 会话互不依赖。\n\n返回:\n    service/account_status/credits/套餐与限速/当前用量/月配额等；其中\n    monthly_remaining 与 quota_period_start 便于调用方自己做额度告警。\n\n注意:\n    本接口读到的 credits 与 /dashboard 的来源可能不同（见下方注释），\n    这是「热路径账本」与「展示型缓存」两套数据源造成的差异。","operationId":"api_status_myapi_status_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/orders":{"get":{"summary":"My Orders","description":"Return paginated order history for the current user.\n\n中文：当前用户的订单分页列表（/orders）。\n\n参数:\n    page/size: 页码（从 1 开始）与每页条数，size 上限 100，防止一次拉全表。\n    lang: 文案语言，用于把失败原因翻译成对应语种。\n\n返回:\n    total/page/size/items。每项除订单原始字段外，还额外带两个字段：\n    status_reason_code（机器可读）与 status_reason（已本地化文案）。\n\n副作用:\n    只读数据库；另有一次 count 查询用于分页总数。","operationId":"my_orders_myapi_orders_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Size"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/requests":{"get":{"summary":"My Requests","description":"Return the current user's request log with sanitized parameters.\n\n中文：当前用户的调用日志分页列表（/requests），参数已脱敏。\n\n参数:\n    page/size: 分页。\n    endpoint: 可选，按接口标识精确过滤（会先 strip，空串视为不过滤）。\n    status: 状态筛选，all / success / error，由 Query 的 pattern 约束取值。\n\n返回:\n    total/page/size/items；每项的 params 是 _sanitize_request_params 清洗后的结构。\n\n注意:\n    本接口没有 lang 参数（与 /orders 不同），且不返回本地化文案，只回原始字段，\n    因此不涉及语言判定。\n\n副作用:\n    只读数据库。","operationId":"my_requests_myapi_requests_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Size"}},{"name":"endpoint","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Endpoint"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","pattern":"^(all|success|error)$","default":"all","title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/usage":{"get":{"summary":"My Usage","description":"Return usage trends without mixing credits, calls, and rate limits.\n\n中文：按时间窗口返回用量趋势（/usage），供前端的图表页使用。\n\n参数:\n    period: 统计窗口，24h / 7d / 30d / current（当前配额周期），\n            由 Query 的 pattern 约束；未知值不会走到这里。\n\n返回:\n    period/period_start/period_end、总量与成功率、消耗额度、平均耗时、\n    按天与按接口的两组明细，以及 limits 子对象（余额、配额、限速、有效期）。\n\n为什么要把 credits / calls / rate limits 分开放在不同区块：三者的语义和\n量纲完全不同（额度是货币化的，调用次数是计数，限速是速率），混在一个\n指标里会让前端无法正确表达，也容易让用户误以为「调用次数 = 余额」。\n\n副作用:\n    只读数据库；共 3 条聚合查询（汇总、按天、按接口）。","operationId":"my_usage_myapi_usage_get","parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(24h|7d|30d|current)$","default":"7d","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/myapi/plans":{"get":{"summary":"Public Plans","description":"Return active API plans for public display (no auth required).\n\n中文：公开套餐列表接口，营销页使用，无需登录（因此不能返回任何用户数据）。\n\n返回:\n    按语言缓存的套餐列表；TTL 30 分钟。\n\n缓存注意:\n    key 一定带语言后缀（``public:plans:api:{lang}``），因为套餐名与按钮文案\n    已本地化，不同语言必须是不同的缓存条目。后台改套餐时由\n    admin.invalidate_plan_cache 主动清除（但它只清了部分语种，\n    其余语种最长要等 TTL 到期）。","operationId":"public_plans_myapi_plans_get","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/twitter/{path}":{"get":{"summary":"Proxy","description":"网关主入口（步骤 2~7 都在这里），把请求转发到上游并把结果回给用户。\n\n参数 path：被通配符捕获的端点路径，如 \"UserTweets\" 或 \"UserTweets/xxx\"。\n参数 user：由 get_api_key_user 完成鉴权（未带/无效 key 时它已抛 401）。\n\n返回：成功时是上游原始 body + 若干 X-TwtAPI-* 元信息头；\n上游 4xx/5xx 被改写成 502（业务码在 body 里）；上游超时/不可达返回 200 + 20401。\n副作用：写 Redis（限流、信用点、缓存、配额快照）、写库（风控事件；调用日志走后台任务）、\n发起上游 HTTP 请求。\n\n计费语义：进入这里的请求在预检阶段就已经扣过费，所以每个失败出口都必须退款。\n先扣后转的代价是失败路径要显式补偿，好处是并发下不会出现「先转发后扣费、\n余额不足时白送一次上游调用」的超发。","operationId":"proxy_api_v1_twitter__path__get","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Proxy","description":"网关主入口（步骤 2~7 都在这里），把请求转发到上游并把结果回给用户。\n\n参数 path：被通配符捕获的端点路径，如 \"UserTweets\" 或 \"UserTweets/xxx\"。\n参数 user：由 get_api_key_user 完成鉴权（未带/无效 key 时它已抛 401）。\n\n返回：成功时是上游原始 body + 若干 X-TwtAPI-* 元信息头；\n上游 4xx/5xx 被改写成 502（业务码在 body 里）；上游超时/不可达返回 200 + 20401。\n副作用：写 Redis（限流、信用点、缓存、配额快照）、写库（风控事件；调用日志走后台任务）、\n发起上游 HTTP 请求。\n\n计费语义：进入这里的请求在预检阶段就已经扣过费，所以每个失败出口都必须退款。\n先扣后转的代价是失败路径要显式补偿，好处是并发下不会出现「先转发后扣费、\n余额不足时白送一次上游调用」的超发。","operationId":"proxy_api_v1_twitter__path__get","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Proxy","description":"网关主入口（步骤 2~7 都在这里），把请求转发到上游并把结果回给用户。\n\n参数 path：被通配符捕获的端点路径，如 \"UserTweets\" 或 \"UserTweets/xxx\"。\n参数 user：由 get_api_key_user 完成鉴权（未带/无效 key 时它已抛 401）。\n\n返回：成功时是上游原始 body + 若干 X-TwtAPI-* 元信息头；\n上游 4xx/5xx 被改写成 502（业务码在 body 里）；上游超时/不可达返回 200 + 20401。\n副作用：写 Redis（限流、信用点、缓存、配额快照）、写库（风控事件；调用日志走后台任务）、\n发起上游 HTTP 请求。\n\n计费语义：进入这里的请求在预检阶段就已经扣过费，所以每个失败出口都必须退款。\n先扣后转的代价是失败路径要显式补偿，好处是并发下不会出现「先转发后扣费、\n余额不足时白送一次上游调用」的超发。","operationId":"proxy_api_v1_twitter__path__get","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Proxy","description":"网关主入口（步骤 2~7 都在这里），把请求转发到上游并把结果回给用户。\n\n参数 path：被通配符捕获的端点路径，如 \"UserTweets\" 或 \"UserTweets/xxx\"。\n参数 user：由 get_api_key_user 完成鉴权（未带/无效 key 时它已抛 401）。\n\n返回：成功时是上游原始 body + 若干 X-TwtAPI-* 元信息头；\n上游 4xx/5xx 被改写成 502（业务码在 body 里）；上游超时/不可达返回 200 + 20401。\n副作用：写 Redis（限流、信用点、缓存、配额快照）、写库（风控事件；调用日志走后台任务）、\n发起上游 HTTP 请求。\n\n计费语义：进入这里的请求在预检阶段就已经扣过费，所以每个失败出口都必须退款。\n先扣后转的代价是失败路径要显式补偿，好处是并发下不会出现「先转发后扣费、\n余额不足时白送一次上游调用」的超发。","operationId":"proxy_api_v1_twitter__path__get","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Proxy","description":"网关主入口（步骤 2~7 都在这里），把请求转发到上游并把结果回给用户。\n\n参数 path：被通配符捕获的端点路径，如 \"UserTweets\" 或 \"UserTweets/xxx\"。\n参数 user：由 get_api_key_user 完成鉴权（未带/无效 key 时它已抛 401）。\n\n返回：成功时是上游原始 body + 若干 X-TwtAPI-* 元信息头；\n上游 4xx/5xx 被改写成 502（业务码在 body 里）；上游超时/不可达返回 200 + 20401。\n副作用：写 Redis（限流、信用点、缓存、配额快照）、写库（风控事件；调用日志走后台任务）、\n发起上游 HTTP 请求。\n\n计费语义：进入这里的请求在预检阶段就已经扣过费，所以每个失败出口都必须退款。\n先扣后转的代价是失败路径要显式补偿，好处是并发下不会出现「先转发后扣费、\n余额不足时白送一次上游调用」的超发。","operationId":"proxy_api_v1_twitter__path__get","parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/agent/tools":{"get":{"summary":"Tools","description":"列出所有可用工具（需要 skill_key 鉴权）。\n\n返回:\n    ``{\"tools\": [...]}``。注意响应体不是统一的 success() 包装，\n    与 /invoke 保持一致的裸结构，这是 Agent 客户端的既有契约。\n\n副作用:\n    无（只读内存中的工具目录），但仍受 AGENT_ENABLED 开关约束。","operationId":"tools_api_v1_agent_tools_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/agent/invoke":{"post":{"summary":"Invoke","description":"执行一次 Agent 工具调用（需要 skill_key 鉴权，调用成功即扣费）。\n\n参数:\n    body: tool（工具名）、params（工具参数）、session_id（可选，仅用于日志串联）。\n    background_tasks: 用于在响应返回后异步写调用日志，不阻塞用户。\n\n返回:\n    AgentInvokeResponse(code=200, tool, data, credits_used)。\n\n异常（均为 ApiException，由全局处理器转成统一错误体）:\n    404 40420 功能未开启；404 40421 工具不存在；400 40021 必填参数缺失；\n    403 40321 被风控拦截；429/503 限流相关（见 _check_limits）；\n    402 40201 余额不足；502 50201 上游异常（且已退款）。","operationId":"invoke_api_v1_agent_invoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentInvokeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentInvokeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users":{"get":{"summary":"List Users","description":"用户列表（分页），后台首页用。\n\n参数：\n    page/size: 分页，size 上限 100，避免后台误传大 size 拖垮 DB。\n    email: 支持全量邮箱（含 @ 走等值命中索引）和子串（走 ilike 模糊匹配）。\n\n返回：{total, page, size, items}；items 里同时给出 snake_case 与 camelCase\n两种字段名（前端历史命名不统一，属于对旧客户端的兼容，不要删任一份）。\n\n性能说明：一次请求内对所有 user 做固定次数的批量查询（调用次数、累计消费、\n注册 IP、风控画像各一次），不存在逐用户的 N+1。","operationId":"list_users_admin_users_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Size"}},{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by full email or email substring","title":"Email"},"description":"Filter by full email or email substring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}":{"get":{"summary":"Get User","description":"单个用户详情。\n\ninclude_risk 控制是否附带风控明细（该计算有一定开销，按需关闭）。\n\n副作用：rate_limit_service.get_user_usage 内部可能读 Redis 甚至回算 DB。\n用户不存在时返回 404 错误体（HTTP 状态码仍是 200，由统一响应包装处理）。","operationId":"get_user_admin_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"include_risk","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Risk"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/risk":{"get":{"summary":"Get User Risk","description":"只读该用户的风控详情（命中缓存的用户信息 + 风控画像）。\n\n用户不存在时返回 404 错误体；无写库、无外部请求。","operationId":"get_user_risk_admin_users__user_id__risk_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/credits":{"post":{"summary":"Adjust Credits","description":"人工增减积分（amount 正数=加、负数=扣）。\n\n副作用（三处，顺序重要）：\n1. 改 MySQL 并写一条 CreditLog（credit_service 内部用 SELECT ... FOR UPDATE\n   行锁，本身就在事务里，并会 commit）；\n2. 清该用户的鉴权缓存 + C 端看板缓存，让前台立刻看到新余额；\n3. 若开启 CREDIT_USE_REDIS，再把增量同步到 Redis 余额。\n\n一致性取舍：第 3 步失败（Redis 不可用）只记异常日志、不抛错——此时\nMySQL 已是权威数据，Redis 只是加速层，失败会由后续读路径回源修正；\n反过来若抛错会让运营以为\"操作失败\"而重复操作，造成重复加减。\n\n异常：金额/用户不存在等业务错误被 credit_service 抛成 ValueError，\n这里转成 400 返回。","operationId":"adjust_credits_admin_users__user_id__credits_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreditAdjust"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/status":{"post":{"summary":"Update Status","description":"冻结/解冻用户（只允许 active 与 frozen 两种状态）。\n\n白名单校验必须放在最前面：status 会直接写进 User.status 并被鉴权路径读取，\n传入未定义的值会让用户既不算冻结也不算正常，属于非法状态防护。\n\n副作用：写库（commit）+ 清该用户鉴权缓存（缓存里的 status 决定能否调用，\n不清缓存会导致冻结不立即生效）。","operationId":"update_status_admin_users__user_id__status_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStatusUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/limits":{"post":{"summary":"Update Limits","description":"单独调整用户的两级限流（每秒/每小时请求数）。\n\n用 None 判断而非真值判断：显式的 0 是合法值（表示\"不限速/封禁\"），\n若写成 `if body.rate_limit_per_second:` 会导致传 0 被静默忽略。\n\n副作用：写库（commit）+ 清该用户鉴权缓存（限流值缓存在其中）。","operationId":"update_limits_admin_users__user_id__limits_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLimitUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/plan":{"post":{"summary":"Update User Plan","description":"把用户切换到指定 API 套餐（同时覆盖限流值、可选重置积分、刷新到期时间）。\n\n参数：\n    body.plan_id: 目标套餐，必须是 plan_type='api' 且 is_active 的套餐，\n        否则 404，避免把用户挂到已下架/非 API 套餐上。\n    body.reset_credits: 是否把积分重置为套餐额度（按 monthly_limit 换算）。\n    body.validity_period: 有效期档位，未传则用套餐自带档位，再兜底 \"1m\"。\n\n副作用：一次事务内改 User + 追加 CreditLog 后 commit；随后清用户鉴权缓存和\nC 端看板缓存，最后（如需）把积分增量同步进 Redis。Redis 失败只记日志不抛错，\n与 adjust_credits 的取舍一致：MySQL 为准，Redis 是加速层。\n\n事务边界：所有字段改动都攒到同一次 commit，中间不落库；因此下面的\nvalidity_period 校验失败直接 return 时，本次会话里的改动不会被提交（被丢弃）。","operationId":"update_user_plan_admin_users__user_id__plan_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserPlanUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/orders":{"get":{"summary":"List Orders","description":"订单列表（分页，可按状态过滤），按 id 倒序 = 最新在前。\n\nstatus 过滤逻辑收在 _apply_order_filters，保证 total 与列表口径一致。\n用户邮箱用一次 IN 查询批量取回再在内存里映射，避免逐单查 User（N+1）。\n订单可能没有 user_id（游客单/后台代下单），所以先过滤 falsy 再 IN。","operationId":"list_orders_admin_orders_get","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Size"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"pending | paid | cancelled","title":"Status"},"description":"pending | paid | cancelled"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/orders/{order_id}":{"get":{"summary":"Get Order","description":"订单详情。\n\nlang 会被 get_effective_lang 覆盖：优先取请求头/query 的语言，仅在没有\n任何语言信息时才用函数默认值，因此不能把这里的默认值当作\"已定语言\"。\n\nstatus_reason 由 infer_order_status_reason 反推，包含对历史遗留失败单\n（如 EasyGo 0 元单）的兼容推断，见 app/services/order_status_reason.py。\n\n副作用：只读。订单不存在返回 404 错误体。","operationId":"get_order_admin_orders__order_id__get","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"integer","title":"Order Id"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"zh","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/logs":{"get":{"summary":"Get Call Logs","description":"全局调用日志（分页），可按 user_id 过滤。\n\n注意 user_id 用 `is not None` 判断而非真值判断：这样\"未传参（不过滤）\"和\n\"传了 0\"能区分开，避免将来引入合法 id 为 0 的场景时静默失效。\n\n性能取舍：查询与计数分成两条 SQL；大表上 count(*) 成本不低，但后台需要\n真实总数。列表按 id 倒序，走主键即可，无需额外索引。\n\n脱敏：request_params 走 _public_params，密钥字段不会出现在响应里。","operationId":"get_call_logs_admin_logs_get","parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/activity":{"get":{"summary":"Get User Activity","description":"单个用户的活动视图：最近调用明细 + 端点/搜索词 Top10。\n\n与 /logs 的区别：这里会把数字 user_id 还原成 @用户名，并做聚合统计。\n\n关键取舍：用户名只读缓存（_resolve_cached_twitter_usernames），不发上游请求，\n防止后台页面被上游 RT 拖住；冷缓存时优先展示数字 id。\n\n聚合口径：先对原始兴趣点做统计、再逐条做用户名替换。替换是一一对应的\n（同一 id 只会替换成同一个 @name），所以不会把同一次搜索拆成两条。\n传给替换函数的是 dict(...) 浅拷贝，正是为了不污染 raw_interests。\n\n副作用：只读（User 存在性校验 + 日志查询 + Redis 读）。","operationId":"get_user_activity_admin_users__user_id__activity_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":30,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/revenue":{"get":{"summary":"Revenue Stats","description":"营收看板接口，参数经 pattern 正则限定，非法值在进入业务逻辑前就被 FastAPI 拒掉。","operationId":"revenue_stats_admin_revenue_get","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","pattern":"^(day|month)$","default":"day","title":"View"}},{"name":"include_free","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Free"}},{"name":"include_empty","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Empty"}},{"name":"billing_mode","in":"query","required":false,"schema":{"type":"string","pattern":"^(all|one_time|subscription)$","default":"all","title":"Billing Mode"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":366,"minimum":1,"default":31,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/stats":{"get":{"summary":"Global Stats","description":"Aggregate stats for the admin dashboard. Cached for 5 minutes.\n\n缓存 5 分钟是刻意的：统计口径不要求实时，而这个接口内部有十几条 count，\n后台首页频繁刷新时靠缓存顶住。","operationId":"global_stats_admin_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/growth-funnel":{"get":{"summary":"Growth Funnel","description":"Return user-level activation, retention, and first-payment cohorts.\n\n具体聚合逻辑在 growth_service.build_growth_funnel，本路由只做参数校验与\n统一响应包装。days 上限 180 用于限制大范围扫描的开销。","operationId":"growth_funnel_admin_growth_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/rapidapi-quota":{"get":{"summary":"Rapidapi Quota","description":"返回最近一次写入的 RapidAPI 套餐配额信息（每次真实代理请求会覆盖写入）。\n\n数据源是 Redis 里的单条快照（写入方在网关侧），所以这里只反映\"最后一次\n代理请求发生时\"的配额，长时间没有流量就不会更新。\n\n返回 None 的三种情况（都属于\"无可用数据\"，不是错误）：\n缓存不存在/不是 dict、缺 limit 或 remaining、值无法转 int。\n\n副作用：只读 Redis。usage_percent 用 limit > 0 守卫除法，避免除零。","operationId":"rapidapi_quota_admin_rapidapi_quota_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/leak-audit":{"get":{"summary":"Leak Audit","description":"查看网关成功响应中的供应商关键词审计事件（Redis 列表聚合）。\n\n用途：排查上游响应里是否泄露了供应商（如 RapidAPI 供应商名）关键词。\n\n参数：\n    hours: 时间窗口，1~168 小时（上限一周）。\n    limit: 最多扫描 Redis 列表的前 N 条（列表由写入方 LPUSH 最新在前）。\n\n返回：原始事件列表 items + 按 path 聚合的 by_endpoint（按命中数倒序）。\n\n数据不可信是这里最大的特点：Redis 里的 JSON 可能被截断、字段类型可能漂移，\n因此每一条都做了 try/except 与类型收敛，脏数据一律跳过或降级为 0，\n绝不让后台页面 500。\n\n副作用：只读 Redis。注意这里不写回、不清理列表（列表长度由写入方控制）。","operationId":"leak_audit_admin_leak_audit_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/plans":{"get":{"summary":"Get Plans","description":"Return all plans from the plans table.\n\n这里返回**所有**套餐（含已下架的 is_active=False），与 C 端公开接口只返回\n生效套餐的口径不同：后台需要看到下架套餐才能重新启用。\nplan_type 为空则不过滤。","operationId":"get_plans_admin_plans_get","parameters":[{"name":"plan_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Create Plan","description":"Create a new plan.\n\n注意：credits 不接受请求传入，而是由 monthly_limit 反推（见\ninternal_plan_credits），保证\"额度\"与\"积分\"不会被人为配歪。\nfeatures 以 JSON 文本落库，ensure_ascii=False 是为了让中文特性名在库里可读。\n\n副作用：写库 + 清套餐缓存（含 C 端公开套餐列表）。","operationId":"create_plan_admin_plans_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPlanUpsert"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/plans/{plan_id}":{"put":{"summary":"Update Plan","description":"Update an existing plan.\n\n与创建的区别：credits 的换算带 fallback_credits=plan.credits，即当套餐不是\nAPI 类型（不参与反推）时保留原有积分值，避免改个名字就把积分清零。\n\n改完会调 _sync_plan_user_entitlements 给在期用户补差，让新额度立刻生效；\n返回体里带 synced_users 告知运营影响了多少人。\n\n副作用：写库 + 给用户补积分（会写 CreditLog）+ 清套餐缓存与相关用户缓存。\n顺序上先 commit 套餐本身、再补用户权益，保证套餐改动不会被后面的失败带崩。","operationId":"update_plan_admin_plans__plan_id__put","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"integer","title":"Plan Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPlanUpsert"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Plan","description":"删除套餐。默认软删（is_active=False），hard=True 才是物理删除。\n\n默认软删的原因：订单/用户可能仍引用该套餐 id，物理删除会让历史订单的\n套餐信息失去关联、也让 _load_plan_map 之外的地方出现悬空引用。\n\n注意：硬删不会清理 User.current_plan_id 的引用，也不会给受影响用户补差，\n属于\"确需清理脏数据\"时才用的操作。\n\n副作用：写库（硬删=DELETE，软删=UPDATE）+ 清套餐缓存。","operationId":"delete_plan_admin_plans__plan_id__delete","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"integer","title":"Plan Id"}},{"name":"hard","in":"query","required":false,"schema":{"type":"boolean","description":"True = permanently delete, False = soft-delete (set is_active=False)","default":false,"title":"Hard"},"description":"True = permanently delete, False = soft-delete (set is_active=False)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment/create-checkout":{"post":{"summary":"Create Checkout","description":"为已存在的 pending 订单创建渠道收银台会话，返回 checkout_url。\n\n校验顺序：订单存在 → 属于当前用户 → 未过期 → 仍为 pending →\n免费 API 单拦截 → 订阅单只允许刷卡 → 支付方式可用。\n任何一步不过都直接返回业务错误码，不会改动订单。\n\n渠道下单失败时会把订单置为 cancelled 并打上失败原因标记，\n避免订单一直悬在 pending 占位；成功后只写回 provider_session_id。","operationId":"create_checkout_payment_create_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment/webhook/{provider_name}":{"post":{"summary":"Payment Webhook","description":"Receive and process payment callbacks from providers.","operationId":"payment_webhook_payment_webhook__provider_name__post","parameters":[{"name":"provider_name","in":"path","required":true,"schema":{"type":"string","title":"Provider Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment/status/{order_no}":{"get":{"summary":"Payment Status","description":"查询订单状态，供前端支付页轮询。\n\n不只读库：若订单处于「可能已成功但回调丢了」的状态（_SUCCESS_RECOVERABLE_STATUSES）\n且存有渠道会话信息，会主动向渠道查一次实时状态并做补偿\n—— 渠道返回 succeeded 就补记为已支付，返回 failed 且本地仍为 pending 则置为 cancelled。\n查渠道失败只记日志，不影响本次按本地状态返回，避免渠道抖动把查询接口打挂。","operationId":"payment_status_payment_status__order_no__get","parameters":[{"name":"order_no","in":"path","required":true,"schema":{"type":"string","title":"Order No"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en","title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment/cancel-subscription":{"post":{"summary":"Cancel Subscription","description":"Cancel a Stripe subscription (at period end). Only for paid subscription orders of current user.","operationId":"cancel_subscription_payment_cancel_subscription_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelSubscriptionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","description":"健康检查/根路径，供探针或反向代理确认服务存活。","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AdminCreditAdjust":{"properties":{"amount":{"type":"number","title":"Amount"},"description":{"type":"string","title":"Description","default":""}},"type":"object","required":["amount"],"title":"AdminCreditAdjust","description":"管理员手动调整用户余额。amount 可正可负，会写入 CreditLog 审计。"},"AdminLimitUpdate":{"properties":{"rate_limit_per_second":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rate Limit Per Second"},"rate_limit_per_hour":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rate Limit Per Hour"}},"type":"object","title":"AdminLimitUpdate","description":"管理员覆盖用户级限流。传 None 表示清除覆盖、回退到套餐默认值。"},"AdminPlanUpsert":{"properties":{"plan_type":{"type":"string","title":"Plan Type","default":"api"},"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price","default":0},"subscription_price":{"type":"number","title":"Subscription Price","default":0},"currency":{"type":"string","title":"Currency","default":"usd"},"monthly_limit":{"type":"integer","title":"Monthly Limit","default":0},"rate_limit_per_second":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rate Limit Per Second"},"rate_limit_per_hour":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rate Limit Per Hour"},"features":{"items":{"type":"string"},"type":"array","title":"Features","default":[]},"is_popular":{"type":"boolean","title":"Is Popular","default":false},"button_text":{"type":"string","title":"Button Text","default":"立即开始"},"button_style":{"type":"string","title":"Button Style","default":"blue"},"billing_mode":{"type":"string","title":"Billing Mode","default":"one_time"},"trial_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trial Period"},"validity_period":{"type":"string","title":"Validity Period","default":"1m"},"overage_price_per_call":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Overage Price Per Call"},"sort_order":{"type":"integer","title":"Sort Order","default":0},"is_active":{"type":"boolean","title":"Is Active","default":true}},"type":"object","required":["name"],"title":"AdminPlanUpsert","description":"Create or update a plan row.\n\n中文：后台套餐的新增/编辑入参（同一模型服务两个动作）。\n注意这里没有 ``id``：更新时由路径参数指定目标，新增时不带 id。\n字段与 models.Plan 一一对应，改动需两侧同步。"},"AdminStatusUpdate":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"AdminStatusUpdate","description":"管理员冻结/解冻账户。改完必须失效用户缓存，否则最长 5 分钟不生效。"},"AdminUserPlanUpdate":{"properties":{"plan_id":{"type":"integer","title":"Plan Id"},"reset_credits":{"type":"boolean","title":"Reset Credits","default":true},"validity_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Validity Period"}},"type":"object","required":["plan_id"],"title":"AdminUserPlanUpdate","description":"管理员为用户换套餐。\n\nreset_credits=True 时会按新套餐重置剩余额度（默认行为，符合「买套餐」直觉）；\n请按需关闭以免抹掉用户已充值购买的余额。\nvalidity_period 为空表示沿用套餐自身的有效期。"},"AgentInvokeRequest":{"properties":{"tool":{"type":"string","title":"Tool"},"params":{"additionalProperties":true,"type":"object","title":"Params"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["tool"],"title":"AgentInvokeRequest","description":"Agent 接口调用入参。\n\nparams 用 default_factory 而非直接 ``= {}``：可变默认值会在\n多次请求间共享同一对象，是典型的串数据隐患。\nsession_id 为空时由服务端开启新会话。"},"AgentInvokeResponse":{"properties":{"code":{"type":"integer","title":"Code","default":0},"tool":{"type":"string","title":"Tool"},"data":{"title":"Data"},"credits_used":{"type":"number","title":"Credits Used"}},"type":"object","required":["tool","credits_used"],"title":"AgentInvokeResponse","description":"Agent 接口出参。credits_used 便于前端即时展示本次消耗。"},"CancelSubscriptionRequest":{"properties":{"order_no":{"type":"string","title":"Order No"},"lang":{"type":"string","title":"Lang","default":"en"}},"type":"object","required":["order_no"],"title":"CancelSubscriptionRequest","description":"取消订阅。lang 用于把取消结果文案按用户语言返回。"},"ChangePasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordRequest","description":"Logged-in user change password: current + new.\n\n中文：已登录用户改密。要求提供原密码，因此此接口调用前\n必须先通过会话鉴权（require_login）。"},"CheckCaptchaRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"CheckCaptchaRequest","description":"按邮箱探测「本次登录是否需要图形验证码」，用于前端决定是否展示输入框。"},"CreateCheckoutRequest":{"properties":{"order_no":{"type":"string","title":"Order No"},"payment_method":{"type":"string","title":"Payment Method","default":"credit_card"},"lang":{"type":"string","title":"Lang","default":"en"},"source":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Source"},"page_slug":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Page Slug"},"entry_path":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Entry Path"}},"type":"object","required":["order_no"],"title":"CreateCheckoutRequest","description":"为已存在的 pending 订单发起支付（生成渠道支付页/会话）。\n\nsource / page_slug / entry_path 是前端埋点：记录用户从哪个入口下单，\n带长度上限是为了防止有人塞超长字符串撑爆日志与统计表。"},"CreateOrderRequest":{"properties":{"plan_type":{"type":"string","title":"Plan Type"},"plan_name":{"type":"string","title":"Plan Name"},"price":{"type":"number","title":"Price"},"currency":{"type":"string","title":"Currency","default":"usd"},"features":{"items":{"type":"string"},"type":"array","title":"Features","default":[]},"payment_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Method"},"billing_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Billing Mode"}},"type":"object","required":["plan_type","plan_name","price"],"title":"CreateOrderRequest","description":"创建订单。\n\n安全提示：price 由客户端提交，服务端必须与 Plan 表的值比对后\n再落库，不能直接采信。"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginRequest":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"captcha":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Captcha"}},"type":"object","required":["email","password"],"title":"LoginRequest","description":"登录请求。captcha 可选：风控判定需要时才要求前端带图形验证码。"},"RegisterRequest":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"captcha":{"type":"string","title":"Captcha"},"email_code":{"type":"string","title":"Email Code"}},"type":"object","required":["email","password","captcha","email_code"],"title":"RegisterRequest","description":"注册请求。图形验证码 + 邮箱验证码双重校验，两者缺一不可。"},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","title":"Password"},"captcha":{"type":"string","title":"Captcha"}},"type":"object","required":["token","password","captcha"],"title":"ResetPasswordRequest","description":"凭邮件里的 token 重置密码。\n\ntoken 由 generate_reset_token() 生成、存于 Redis 且一次性使用；\n这里只承载它，有效性由 auth_service 判断。"},"SendEmailCodeRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"SendEmailCodeRequest","description":"请求发送邮箱验证码。发送频率受 EMAIL_CODE_COOLDOWN 限制。"},"SendResetEmailRequest":{"properties":{"email":{"type":"string","title":"Email"},"captcha":{"type":"string","title":"Captcha"}},"type":"object","required":["email","captcha"],"title":"SendResetEmailRequest","description":"发送重置密码邮件，同样需要图形验证码防刷。"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}