🔄 卡若AI 同步 2026-03-31 19:29 | 更新:Cursor规则、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 14 个
This commit is contained in:
@@ -46,6 +46,7 @@ alwaysApply: true
|
||||
- **卡若**:仅接手 **Agent 物理上不能代劳** 的动作:短信/支付/人脸/企业 SSO、不可逆删除的最终确认、需本人法律意思表示的操作、以及 **第三方强人机校验**(如 Turnstile、必须逐格输入的 OTP、仅人类可过的风控)。
|
||||
- **当用户明确说「方案定了我能百分百完成」**:Agent 必须交付 **可勾选清单**(步骤序、每步做什么、成功长什么样、失败改走哪条 B 方案),**不得**用空话代替步骤。
|
||||
- **浏览器 / 注册类**:Agent 仍须完成 **收信(mail.tm/IMAP)、解析邮件链接、打开正确 URL、轮询验证码**;若页面因多输入框/人机验证拒绝自动化,须在清单中写清 **为何** 需卡若手动及 **具体操作**(例如「6 位验证码逐格输入,勿整段粘贴」)。
|
||||
- **Vercel / v0 + mail.tm 取码循环**:**不要跟单邮箱死磕**。本轮收不到「Sign-up Verification」或明显走错登录页时,**放弃该 mail.tm、换新邮箱再注册**;可执行 `03_卡木(木)/木根_逆向分析/全网AI自动注册/脚本/vercel_mailtm_signup_loop.py`(见该目录 `SKILL.md` 与 `水溪_整理归档/经验库/待沉淀/2026-03-31_Vercel与v0_mail.tm注册_OTP与团队创建.md`)。
|
||||
|
||||
### 经验沉淀(出了问题要入账 · 可复用)
|
||||
|
||||
|
||||
@@ -23,3 +23,23 @@
|
||||
|
||||
- Turnstile / 企业策略可能拦截自动化或一次性域名。
|
||||
- 客观无法代劳时:向卡若交付 **可勾选清单**(含「逐格 OTP」等),符合 `.cursor/rules/karuo-ai.mdc` 问题自治分工。
|
||||
|
||||
## 循环策略(取码不行就换邮箱 · 做通)
|
||||
|
||||
**原则**:不要跟**同一封收不到码 / 只有 Attempted Sign-in / OTP 总失败**的邮箱死磕——**超时无「Sign-up Verification」→ 放弃该 mail.tm → 脚本自动生成下一邮箱 → 继续注册**。
|
||||
|
||||
**脚本(木根)**:`03_卡木(木)/木根_逆向分析/全网AI自动注册/脚本/vercel_mailtm_signup_loop.py`
|
||||
|
||||
```bash
|
||||
cd "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木根_逆向分析/全网AI自动注册/脚本"
|
||||
# 每轮打印新邮箱 + 注册链接;你在浏览器点 Continue with Email;收不到注册验证码则自动下一轮
|
||||
python3 vercel_mailtm_signup_loop.py --max-rounds 8 --poll-timeout 180
|
||||
|
||||
# 需要先手动点发码再轮询时:
|
||||
python3 vercel_mailtm_signup_loop.py --pause-enter --poll-timeout 240
|
||||
|
||||
# 每轮结果落盘备查:
|
||||
python3 vercel_mailtm_signup_loop.py --out-jsonl ./vercel_mailtm_rounds.jsonl
|
||||
```
|
||||
|
||||
说明:脚本**忽略**环境变量 `MAILTM_ADDRESS` / `MAILTM_PASSWORD`,每轮都是**全新** mail.tm,避免固定邮箱卡死。
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -36,6 +37,10 @@ _BASE_RGB = (26, 28, 34) # 与字同系深灰蓝
|
||||
_WASH_ALPHA = int(255 * 0.05) # 整屏极淡底雾(约 95% 透)
|
||||
_TITLE_ALPHA = int(255 * 0.09)
|
||||
_BODY_ALPHA = int(255 * 0.11)
|
||||
# 第二页底部「轻行动」略亮于正文,仍属低调(普通人能扫到,不抢 SEO 藏词主责)
|
||||
_ACTION_ALPHA = int(255 * 0.22)
|
||||
|
||||
_STATE_NAME = ".soul_seo_tail_state.json"
|
||||
|
||||
|
||||
def load_keywords(path: Path) -> list[str]:
|
||||
@@ -114,6 +119,40 @@ def probe_size_rate(main_mp4: Path) -> tuple[int, int, int]:
|
||||
return width, height, sr
|
||||
|
||||
|
||||
def probe_duration_sec(mp4: Path) -> float:
|
||||
r = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
str(mp4),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
return float((r.stdout or "").strip() or 0.0)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def load_tail_state(state_path: Path) -> dict:
|
||||
if not state_path.is_file():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(state_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_tail_state(state_path: Path, state: dict) -> None:
|
||||
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def render_keyword_page_subtle_rgba(
|
||||
width: int,
|
||||
height: int,
|
||||
@@ -121,8 +160,13 @@ def render_keyword_page_subtle_rgba(
|
||||
png_path: Path,
|
||||
font_path: Path | None,
|
||||
page_label: str,
|
||||
footer_line: str | None = None,
|
||||
footer_alpha: int | None = None,
|
||||
) -> None:
|
||||
"""全透明底 + 极淡同色系雾 + 同色系略亮字(低对比、高透明)。"""
|
||||
"""全透明底 + 极淡同色系雾 + 同色系略亮字(低对比、高透明)。
|
||||
|
||||
footer_line:可选,多画在底部(用于第二页轻引导,不替代正片 cta_ending)。
|
||||
"""
|
||||
if Image is None:
|
||||
raise RuntimeError("需要安装 Pillow: pip install pillow")
|
||||
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
@@ -134,8 +178,12 @@ def render_keyword_page_subtle_rgba(
|
||||
try:
|
||||
title_font = ImageFont.truetype(str(fp), 26)
|
||||
body_font = ImageFont.truetype(str(fp), 20)
|
||||
try:
|
||||
foot_font = ImageFont.truetype(str(fp), 18)
|
||||
except OSError:
|
||||
foot_font = body_font
|
||||
except OSError:
|
||||
title_font = body_font = ImageFont.load_default()
|
||||
title_font = body_font = foot_font = ImageFont.load_default()
|
||||
|
||||
# 标题:与底同系,仅略亮 + 极低 alpha
|
||||
tr, tg, tb = _BASE_RGB
|
||||
@@ -154,6 +202,15 @@ def render_keyword_page_subtle_rgba(
|
||||
y += 32
|
||||
if y > height - 36:
|
||||
break
|
||||
fl = (footer_line or "").strip()
|
||||
if fl:
|
||||
fa = int(footer_alpha) if footer_alpha is not None else _ACTION_ALPHA
|
||||
ar, ag, ab = min(tr + 18, 255), min(tg + 20, 255), min(tb + 24, 255)
|
||||
bbox = draw.textbbox((0, 0), fl, font=foot_font)
|
||||
tw = bbox[2] - bbox[0]
|
||||
fx = max(24, (width - tw) // 2)
|
||||
fy = height - 46
|
||||
draw.text((fx, fy), fl, fill=(ar, ag, ab, fa), font=foot_font)
|
||||
img.save(str(png_path), "PNG")
|
||||
|
||||
|
||||
@@ -286,6 +343,21 @@ def main() -> None:
|
||||
nargs="*",
|
||||
help="可选:只处理这些文件名(相对于 --dir),不填则处理目录下全部 .mp4",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--ignore-state",
|
||||
action="store_true",
|
||||
help="忽略状态文件、不做「已含尾帧」跳过(仅当你已换回无尾帧母片时再开,否则会叠双尾帧)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--action-line",
|
||||
default="点头像进房 · 每晚派对直播",
|
||||
help="第二页底部轻引导(略亮于藏词正文);设为空字符串可关",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--no-action-line",
|
||||
action="store_true",
|
||||
help="不绘制第二页底部引导(仍保留两页 SEO 藏词)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
words = load_keywords(args.keywords)
|
||||
font_try = args.font
|
||||
@@ -297,6 +369,9 @@ def main() -> None:
|
||||
break
|
||||
|
||||
d = args.dir.resolve()
|
||||
state_path = d / _STATE_NAME
|
||||
state: dict = load_tail_state(state_path)
|
||||
|
||||
if args.files:
|
||||
mp4s = []
|
||||
for f in args.files:
|
||||
@@ -311,10 +386,22 @@ def main() -> None:
|
||||
return
|
||||
|
||||
dur_each = args.duration / max(1, args.pages)
|
||||
page2_footer = ""
|
||||
if not args.no_action_line:
|
||||
page2_footer = (args.action_line or "").strip()
|
||||
|
||||
for main in mp4s:
|
||||
idx = clip_index_from_name(main.name)
|
||||
try:
|
||||
dur_before = probe_duration_sec(main)
|
||||
if (
|
||||
not args.ignore_state
|
||||
and main.name in state
|
||||
and dur_before > 1.0
|
||||
and abs(float(state[main.name]) - dur_before) < 1.05
|
||||
):
|
||||
print(f"⏭ 已含尾帧(时长与状态一致,跳过防叠双尾): {main.name}")
|
||||
continue
|
||||
w, h, sr = probe_size_rate(main)
|
||||
block_a, block_b = pick_two_blocks(words, idx, args.per_page)
|
||||
with tempfile.TemporaryDirectory(prefix="seo_tail_") as td:
|
||||
@@ -329,7 +416,13 @@ def main() -> None:
|
||||
w, h, block_a, png_a, font_try, "搜索关键词 1/2"
|
||||
)
|
||||
render_keyword_page_subtle_rgba(
|
||||
w, h, block_b, png_b, font_try, "搜索关键词 2/2"
|
||||
w,
|
||||
h,
|
||||
block_b,
|
||||
png_b,
|
||||
font_try,
|
||||
"搜索关键词 2/2",
|
||||
footer_line=page2_footer or None,
|
||||
)
|
||||
make_tail_clip_from_rgba_overlay(
|
||||
png_a, ta, dur_each, w, h, sr
|
||||
@@ -352,6 +445,9 @@ def main() -> None:
|
||||
tails.append(one)
|
||||
|
||||
concat_videos_list(main, [main, *tails])
|
||||
new_d = probe_duration_sec(main)
|
||||
state[main.name] = round(new_d, 2)
|
||||
save_tail_state(state_path, state)
|
||||
nkw = len(block_a) + len(block_b)
|
||||
print(
|
||||
f"✅ 已加关键字尾帧×{args.pages}: {main.name}(序号 {idx},共 {nkw} 词,"
|
||||
|
||||
@@ -500,8 +500,10 @@ MIN_SILENCE_TRIM_TOTAL_SEC = 0.12
|
||||
COVER_TITLE_MAX_CJK = 6
|
||||
# 封面优先用 hook_3sec(吸睛高光句),可略长于纯标题字数上限
|
||||
COVER_HOOK_MAX_CJK = 16
|
||||
CTA_END_MIN_SEC = 2.0
|
||||
CTA_END_MAX_SEC = 3.8
|
||||
CTA_END_MIN_SEC = 2.2
|
||||
CTA_END_MAX_SEC = 4.5
|
||||
# 最后一条口播字幕后,CTA 再停留一小段(多为静音),方便读完 + 过渡到无声 SEO 尾帧
|
||||
CTA_HOLD_AFTER_VOICE_SEC = 0.32
|
||||
|
||||
# ============ 工具函数 ============
|
||||
|
||||
@@ -1872,14 +1874,19 @@ def append_cta_ending_subtitle(
|
||||
if anchor_end < CTA_END_MIN_SEC + float(subtitle_overlay_start) * 0.5:
|
||||
return sub_images
|
||||
|
||||
span = min(CTA_END_MAX_SEC, max(CTA_END_MIN_SEC, anchor_end * 0.08))
|
||||
span = min(CTA_END_MAX_SEC, max(CTA_END_MIN_SEC, anchor_end * 0.11))
|
||||
cta_start = max(float(subtitle_overlay_start), anchor_end - span)
|
||||
if cta_start >= anchor_end - 0.15:
|
||||
cta_start = max(0.0, anchor_end - CTA_END_MIN_SEC)
|
||||
|
||||
# 收口:CTA 结束略晚于最后一条语音字幕结束(不超过片长),便于「读完再断声」
|
||||
cta_end = min(anchor_end + float(CTA_HOLD_AFTER_VOICE_SEC), dur - 0.04)
|
||||
if cta_end <= cta_start + 0.2:
|
||||
cta_end = min(anchor_end, dur - 0.02)
|
||||
|
||||
img_path = os.path.join(temp_dir, "sub_cta_ending.png")
|
||||
create_subtitle_image(cta, out_w, out_h, img_path)
|
||||
sub_images.append({"path": img_path, "start": cta_start, "end": anchor_end})
|
||||
sub_images.append({"path": img_path, "start": cta_start, "end": cta_end})
|
||||
sub_images.sort(key=lambda x: (float(x["start"]), float(x["end"])))
|
||||
return sub_images
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Soul 切片一体化流水线
|
||||
视频制作(封面/Hook格式)+ 视频切片
|
||||
|
||||
流程:转录 → 字幕转简体 → 高光识别(AI) → 批量切片 → 增强(封面+字幕+CTA) → 快速混剪(可选)
|
||||
流程:转录 → 字幕转简体 → 高光识别(AI) → 批量切片 → 增强(封面+字幕+CTA) → [two-folders 默认] SEO 两页尾帧 → 快速混剪(可选)
|
||||
"""
|
||||
import argparse
|
||||
import atexit
|
||||
@@ -179,6 +179,16 @@ def main():
|
||||
action="store_true",
|
||||
help="传给 soul_enhance:去静音参数更温和",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-seo-tail",
|
||||
action="store_true",
|
||||
help="成片后不自动跑 append_seo_keyword_tail(默认 --two-folders 且出成片时会拼 SEO 两页)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seo-tail-force",
|
||||
action="store_true",
|
||||
help="SEO 忽略 .soul_seo_tail_state.json(须已换回无尾帧母片,否则会叠双尾帧)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if getattr(args, "ops_short", False):
|
||||
@@ -412,6 +422,30 @@ def main():
|
||||
]
|
||||
run(montage_cmd, "生成快速混剪", timeout=600, check=False)
|
||||
|
||||
# 5. 成片 SEO 尾帧(两页藏词 + 第二页轻引导):与 soul_enhance 分离,默认在 two-folders 成片后执行
|
||||
run_seo_tail = (
|
||||
use_two_folders
|
||||
and not getattr(args, "slices_only", False)
|
||||
and not getattr(args, "no_seo_tail", False)
|
||||
and enhanced_count > 0
|
||||
)
|
||||
kw_file = SKILL_DIR / "参考资料" / "视频尾帧_SEO关键词200.txt"
|
||||
if run_seo_tail and kw_file.is_file():
|
||||
seo_cmd = [
|
||||
sys.executable,
|
||||
str(SCRIPT_DIR / "append_seo_keyword_tail.py"),
|
||||
"--dir",
|
||||
str(enhanced_dir),
|
||||
"--keywords",
|
||||
str(kw_file),
|
||||
]
|
||||
if getattr(args, "seo_tail_force", False):
|
||||
seo_cmd.append("--ignore-state")
|
||||
seo_timeout = max(900, 180 * max(1, enhanced_count))
|
||||
run(seo_cmd, "成片末尾 SEO 尾帧(×2 静帧,无声)", timeout=seo_timeout, check=False)
|
||||
elif run_seo_tail and not kw_file.is_file():
|
||||
print(f" ⚠ 跳过 SEO 尾帧:未找到词表 {kw_file}", flush=True)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("✅ 流水线完成")
|
||||
|
||||
@@ -68,6 +68,11 @@ python3 auto_register.py add-key -p cerebras -k csk-xxx # 手动添加
|
||||
|
||||
# ====== 网关健康检查 ======
|
||||
python3 ../../运营中枢/scripts/karuo_ai_gateway/key_health_check.py
|
||||
|
||||
# ====== Vercel / v0(mail.tm 多轮:取码失败换新邮箱)======
|
||||
python3 vercel_mailtm_signup_loop.py --max-rounds 8 --poll-timeout 180
|
||||
python3 vercel_mailtm_signup_loop.py --pause-enter --poll-timeout 240 # 先回车再轮询
|
||||
python3 vercel_mailtm_signup_loop.py --out-jsonl ./vercel_mailtm_rounds.jsonl
|
||||
```
|
||||
|
||||
### 自动化全链路
|
||||
@@ -109,6 +114,13 @@ python3 ../../运营中枢/scripts/karuo_ai_gateway/key_health_check.py
|
||||
6. 验证 Key 可用性 → 存入 key_pool.db
|
||||
7. 网关下次请求自动读取新 Key
|
||||
|
||||
### Vercel / v0 邮箱注册(mail.tm 循环)
|
||||
|
||||
1. **不要单邮箱死磕**:登录页试邮只会收到「Attempted Sign-in」+ signup 链接;**注册页** `Continue with Email` 才会收到 **Sign-up Verification** 六位码。
|
||||
2. **循环**:本轮 `poll-timeout` 内无注册验证邮件 → **放弃该 mail.tm** → 脚本 `vercel_mailtm_signup_loop.py` 自动生成下一邮箱 → 重复,直到取到码或达 `--max-rounds`。
|
||||
3. OTP 建议**逐格输入**;团队 URL 需全局唯一。
|
||||
4. 经验与命令见 `水溪_整理归档/经验库/待沉淀/2026-03-31_Vercel与v0_mail.tm注册_OTP与团队创建.md`。
|
||||
|
||||
### 新增平台支持
|
||||
|
||||
1. 在 `providers/` 创建 `平台名_provider.py`,继承 `BaseProvider`
|
||||
|
||||
227
03_卡木(木)/木根_逆向分析/全网AI自动注册/脚本/vercel_mailtm_signup_loop.py
Normal file
227
03_卡木(木)/木根_逆向分析/全网AI自动注册/脚本/vercel_mailtm_signup_loop.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Vercel / v0 同账号:mail.tm 多轮注册循环。
|
||||
|
||||
策略:本轮在超时内拿不到「注册验证码邮件」→ 放弃该邮箱 → 自动再建一个新 mail.tm → 继续下一轮。
|
||||
不跟单封死磕;与 config 里 fixed mail.tm 无关(脚本内不读 MAILTM_* 固定账号)。
|
||||
|
||||
人工配合:每轮打印注册链接后,在浏览器打开并对该邮箱走「注册页 → Continue with Email」。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
MAILTM_API = "https://api.mail.tm"
|
||||
RE_OTP = re.compile(r"(?<!\d)(\d{6})(?!\d)")
|
||||
|
||||
|
||||
def _clear_mailtm_env() -> None:
|
||||
os.environ.pop("MAILTM_ADDRESS", None)
|
||||
os.environ.pop("MAILTM_PASSWORD", None)
|
||||
|
||||
|
||||
def create_fresh_mailtm() -> tuple[str, str, str]:
|
||||
"""返回 (email, password, bearer_token)。"""
|
||||
t = 30
|
||||
r = httpx.get(f"{MAILTM_API}/domains", timeout=t)
|
||||
r.raise_for_status()
|
||||
domains = r.json().get("hydra:member", [])
|
||||
if not domains:
|
||||
raise RuntimeError("mail.tm 无可用域名")
|
||||
domain = domains[0]["domain"]
|
||||
prefix = "".join(random.choices(string.ascii_lowercase + string.digits, k=14))
|
||||
email = f"{prefix}@{domain}"
|
||||
password = "".join(random.choices(string.ascii_letters + string.digits, k=18))
|
||||
r = httpx.post(
|
||||
f"{MAILTM_API}/accounts",
|
||||
json={"address": email, "password": password},
|
||||
timeout=t,
|
||||
)
|
||||
if r.status_code not in (200, 201):
|
||||
raise RuntimeError(f"mail.tm 创建失败: {r.status_code} {r.text[:120]}")
|
||||
tr = httpx.post(
|
||||
f"{MAILTM_API}/token",
|
||||
json={"address": email, "password": password},
|
||||
timeout=t,
|
||||
)
|
||||
tr.raise_for_status()
|
||||
token = tr.json().get("token") or ""
|
||||
if not token:
|
||||
raise RuntimeError("mail.tm token 为空")
|
||||
return email, password, token
|
||||
|
||||
|
||||
def poll_signup_verification_code(
|
||||
token: str,
|
||||
timeout_sec: int,
|
||||
interval_sec: float,
|
||||
) -> tuple[str | None, str]:
|
||||
"""
|
||||
只认「注册验证」类邮件里的 6 位码。
|
||||
返回 (code, reason);code 为 None 时 reason 为 TIMEOUT / NO_SIGNUP_MAIL。
|
||||
"""
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
deadline = time.time() + timeout_sec
|
||||
seen_subjects: set[str] = set()
|
||||
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
r = httpx.get(f"{MAILTM_API}/messages", headers=headers, timeout=20)
|
||||
r.raise_for_status()
|
||||
for msg in r.json().get("hydra:member", []):
|
||||
mid = msg.get("id") or ""
|
||||
sub = (msg.get("subject") or "").strip()
|
||||
if not mid or not sub:
|
||||
continue
|
||||
key = f"{mid}:{sub}"
|
||||
if key in seen_subjects:
|
||||
continue
|
||||
seen_subjects.add(key)
|
||||
|
||||
dr = httpx.get(f"{MAILTM_API}/messages/{mid}", headers=headers, timeout=20)
|
||||
dr.raise_for_status()
|
||||
data = dr.json()
|
||||
text = data.get("text") or ""
|
||||
html = data.get("html")
|
||||
if isinstance(html, list) and html:
|
||||
text = text + "\n" + str(html[0])
|
||||
elif isinstance(html, str):
|
||||
text = text + "\n" + html
|
||||
blob = sub + "\n" + text
|
||||
sl = sub.lower()
|
||||
|
||||
# 仅注册验证;登录试邮 / 无账号说明 不算成功取码
|
||||
if "attempted" in sl and "sign-in" in sl:
|
||||
continue
|
||||
if "sign-up verification" in sl or "signup verification" in sl:
|
||||
m = RE_OTP.search(blob)
|
||||
if m and m.group(1) != "666666":
|
||||
return m.group(1), "OK"
|
||||
# 主题即「123456 - …」且正文含 sign up / verify your email
|
||||
if "vercel" in sl and (
|
||||
"sign up" in blob.lower() or "sign-up" in blob.lower()
|
||||
):
|
||||
m = RE_OTP.search(sub) or RE_OTP.search(blob)
|
||||
if m and m.group(1) != "666666":
|
||||
return m.group(1), "OK"
|
||||
except Exception as e:
|
||||
print(f"[poll] 异常(将重试): {e}", file=sys.stderr)
|
||||
time.sleep(interval_sec)
|
||||
|
||||
return None, "TIMEOUT"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Vercel 注册 mail.tm 多轮循环(取码失败换新邮箱)")
|
||||
ap.add_argument("--max-rounds", type=int, default=8, help="最多换几轮邮箱")
|
||||
ap.add_argument("--poll-timeout", type=int, default=180, help="每轮等待注册验证码秒数")
|
||||
ap.add_argument("--interval", type=float, default=3.0, help="轮询间隔秒")
|
||||
ap.add_argument(
|
||||
"--pause-enter",
|
||||
action="store_true",
|
||||
help="打印链接后等待按回车再开始收信(方便你先点 Continue with Email)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--out-jsonl",
|
||||
type=str,
|
||||
default="",
|
||||
help="每轮结果追加写入 JSONL(路径)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--ref",
|
||||
type=str,
|
||||
default="https://v0.app/ref/WQ9P9N",
|
||||
help="邀请链接(仅打印参考)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
out_path = Path(args.out_jsonl).expanduser() if args.out_jsonl else None
|
||||
|
||||
print("=== Vercel / v0 · mail.tm 注册循环 ===", flush=True)
|
||||
print("规则:本轮超时无「注册验证码」→ 放弃该邮箱 → 自动下一邮箱。\n", flush=True)
|
||||
|
||||
for rnd in range(1, args.max_rounds + 1):
|
||||
_clear_mailtm_env()
|
||||
print(f"--- 第 {rnd}/{args.max_rounds} 轮 ---", flush=True)
|
||||
try:
|
||||
email, password, token = create_fresh_mailtm()
|
||||
except Exception as e:
|
||||
print(f"[失败] 无法创建 mail.tm: {e}", flush=True)
|
||||
rec = {
|
||||
"round": rnd,
|
||||
"ok": False,
|
||||
"reason": "MAILTM_CREATE_FAIL",
|
||||
"error": str(e),
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
if out_path:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(out_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
continue
|
||||
|
||||
enc = urllib.parse.quote(email, safe="")
|
||||
signup = f"https://vercel.com/signup?email={enc}"
|
||||
print(f"邮箱: {email}", flush=True)
|
||||
print(f"mail.tm 密码(查信用): {password}", flush=True)
|
||||
print(f"注册页: {signup}", flush=True)
|
||||
print(f"邀请参考: {args.ref}", flush=True)
|
||||
print(
|
||||
"请在浏览器打开「注册页」,对该邮箱点 Continue with Email 发码。\n",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if args.pause_enter:
|
||||
input("准备好后按回车开始轮询收信…")
|
||||
|
||||
code, reason = poll_signup_verification_code(
|
||||
token, args.poll_timeout, args.interval
|
||||
)
|
||||
rec = {
|
||||
"round": rnd,
|
||||
"email": email,
|
||||
"mailtm_password": password,
|
||||
"signup_url": signup,
|
||||
"code": code,
|
||||
"poll_reason": reason,
|
||||
"ok": code is not None,
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
if out_path:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(out_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
|
||||
if code:
|
||||
print(f"\n[成功] 注册验证码: {code}", flush=True)
|
||||
print(
|
||||
"下一步:在 Vercel 注册页逐格输入 OTP,完成团队创建;再打开 v0 邀请链接。\n",
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"\n[放弃本轮] 未收到注册验证码({reason}),已丢弃该邮箱,进入下一轮。\n",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
print("[结束] 已达最大轮数仍未取到注册验证码。", flush=True)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -500,3 +500,4 @@
|
||||
| 2026-03-31 18:12:24 | 🔄 卡若AI 同步 2026-03-31 18:12 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-31 18:15:25 | 🔄 卡若AI 同步 2026-03-31 18:15 | 更新:金仓、金盾、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-31 19:04:30 | 🔄 卡若AI 同步 2026-03-31 19:04 | 更新:金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-31 19:20:26 | 🔄 卡若AI 同步 2026-03-31 19:20 | 更新:Cursor规则、金仓、水溪整理归档、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
|
||||
@@ -503,3 +503,4 @@
|
||||
| 2026-03-31 18:12:24 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-31 18:12 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:13000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:13000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-31 18:15:25 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-31 18:15 | 更新:金仓、金盾、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:13000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:13000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-31 19:04:30 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-31 19:04 | 更新:金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:13000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:13000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-31 19:20:26 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-31 19:20 | 更新:Cursor规则、金仓、水溪整理归档、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:13000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:13000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user