Gemini Business 自动注册 & 2API 上传工具

基于大佬们的开源成果,整合了定时注册自动上传过期剔除等功能。实现全自动化的账号池维护。

核心逻辑优化

  • 自动维护:定时注册并直接传到 2API,自动剔除已过期账号,保留可用账号。
  • 失败重试:修改了注册机逻辑,设定申请 N 个,即使中间失败,也会一直重试直到成功申请到 N 个为止。
  • 丰俭由人:建议每 11 小时注册 2-3 个即可满足个人使用。避免对随机邮箱大佬提供不必要的压力。


感谢各位大佬的无私奉献:


环境准备

请确保已安装 Python 环境,并安装以下依赖库:

pip install undetected-chromedriver selenium beautifulsoup4 requests pystray pillow

配置说明

1. 先去 hf 部署一个 2api, 记住 API 地址admin_key
2. 新建并复制代码生成 py 文件,右键编辑,在顶部的配置区域填入你的 2API 信息:

 # 服务器 API 配置 API_HOST = "请输入你的服务器API地址" ADMIN_KEY = "请输入你的管理员密钥" # 无头模式开关 (True=后台运行无窗口, False=显示浏览器窗口) HEADLESS_MODE = True ##无头注册率会低点,但胜在静默,结合重试其实体验更好。 

运行脚本

在命令行中执行:

py gemini_auto.py

代码如下:

gemini_auto.py
"""
Gemini Business 自动注册上传工具
"""

# 标准库
import sys
import json
import time
import random
from pathlib import Path
from datetime import datetime, timedelta, timezone
from urllib.parse import urlparse, parse_qs
from concurrent.futures import ThreadPoolExecutor

# 第三方库
import requests
from bs4 import BeautifulSoup
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


# ==================== 配置区域 ====================
# 服务器 API 配置
API_HOST = "请输入你的服务器API地址"
ADMIN_KEY = "请输入你的管理员密钥"

# 临时邮箱 API 配置
MAIL_API = "https://mail.chatgpt.org.uk"
MAIL_KEY = "gpt-test"

# Gemini 登录页面
LOGIN_URL = "https://auth.business.gemini.google/login?continueUrl=https:%2F%2Fbusiness.gemini.google%2F&wiffid=CAoSJDIwNTlhYzBjLTVlMmMtNGUxZS1hY2JkLThmOGY2ZDE0ODM1Mg"

# 本地账号文件
ACCOUNTS_FILE = "accounts.json"

# 页面元素定位
XPATH = {
    "email_input": "/html/body/c-wiz/div/div/div[1]/div/div/div/form/div[1]/div[1]/div/span[2]/input",
    "continue_btn": "/html/body/c-wiz/div/div/div[1]/div/div/div/form/div[2]/div/button",
    "verify_btn": "/html/body/c-wiz/div/div/div[1]/div/div/div/form/div[2]/div/div[1]/span/div[1]/button",
}

# 随机姓名池
NAMES = [
    "James Smith", "John Johnson", "Robert Williams", "Michael Brown", "William Jones",
    "David Garcia", "Mary Miller", "Patricia Davis", "Jennifer Rodriguez", "Linda Martinez",
    "Elizabeth Taylor", "Richard Moore", "Susan Wilson", "Joseph Anderson", "Jessica Thomas",
    "Charles Jackson", "Sarah White", "Christopher Harris", "Karen Martin", "Daniel Thompson",
    "Thomas Garcia", "Nancy Martinez", "Matthew Robinson", "Lisa Clark", "Anthony Lewis",
    "Betty Walker", "Mark Young", "Margaret Allen", "Donald King", "Sandra Wright"
]

# 全局停止标志 (用于 GUI 停止任务)
STOP_FLAG = False

# 无头模式开关 (True=后台运行无窗口, False=显示浏览器窗口)
HEADLESS_MODE = True
# ==================================================


# ==================== 工具函数 ====================
def print_log(msg, level="INFO"):
    """统一日志输出格式"""
    icons = {"INFO": "→", "WARN": "⚠", "ERROR": "✗", "OK": "✓"}
    icon = icons.get(level, "•")
    print(f"{icon} {msg}")


def print_separator(char="=", length=80):
    """打印分隔线"""
    print(char * length)


def print_progress(current, total, success, fail, avg_time):
    """打印进度信息"""
    print(f"\n>>> 进度: {current}/{total} | 成功: {success} | 失败: {fail} | 平均耗时: {avg_time:.1f}s")


def log_error(email, error_msg):
    """记录错误到日志文件"""
    error_file = Path("errors.log")
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = f"[{timestamp}] 邮箱: {email} | 错误: {error_msg}\n"
    
    try:
        with open(error_file, "a", encoding="utf-8") as f:
            f.write(log_entry)
        print_log(f"错误已记录到 errors.log", "INFO")
    except Exception as e:
        print_log(f"写入错误日志失败: {e}", "WARN")


# ==================== 邮箱管理 ====================
email_queue = []


def create_temp_email():
    """创建临时邮箱地址"""
    try:
        response = requests.get(
            f"{MAIL_API}/api/generate-email",
            headers={"X-API-Key": MAIL_KEY},
            timeout=30
        )
        if response.status_code == 200 and response.json().get('success'):
            email = response.json()['data']['email']
            return email
    except Exception as e:
        print_log(f"邮箱服务异常: {e}", "❌")
    return None


def prefetch_email():
    """预创建邮箱并加入队列"""
    email = create_temp_email()
    if email:
        email_queue.append(email)


def get_email():
    """获取邮箱地址(优先使用队列中的)"""
    if email_queue:
        email = email_queue.pop(0)
        print_log(f"邮箱就绪 → {email}")
        return email
    
    email = create_temp_email()
    if email:
        print_log(f"已生成 → {email}")
    return email


def fetch_verification_code(email, timeout=60):
    """获取邮箱验证码"""
    print_log("等待邮件验证码...")
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        try:
            response = requests.get(
                f"{MAIL_API}/api/emails",
                params={"email": email},
                headers={"X-API-Key": MAIL_KEY},
                timeout=10
            )
            
            if response.status_code == 200:
                emails = response.json().get('data', {}).get('emails', [])
                if emails:
                    html_content = emails[0].get('html_content') or emails[0].get('content', '')
                    soup = BeautifulSoup(html_content, 'html.parser')
                    code_element = soup.find('span', class_='verification-code')
                    
                    if code_element:
                        code = code_element.get_text().strip()
                        if len(code) == 6:
                            print_log(f"验证码 → {code}", "OK")
                            return code
        except:
            pass
        
        elapsed = int(time.time() - start_time)
        print(f"  等待中... ({elapsed}s)", end='\r')
        time.sleep(2)
    
    print_log("验证码超时,请检查网络", "ERROR")
    return None


# ==================== 账号注册 ====================
def save_account_config(email, driver, timeout=10):
    """提取并保存账号配置信息"""
    print_log(f"提取账号配置中(最多 {timeout}s)...")
    start_time = time.time()
    account_data = None

    while time.time() - start_time < timeout:
        cookies = driver.get_cookies()
        current_url = driver.current_url
        parsed_url = urlparse(current_url)

        # 提取 config_id
        url_parts = current_url.split('/')
        config_id = None
        for i, part in enumerate(url_parts):
            if part == 'cid' and i + 1 < len(url_parts):
                config_id = url_parts[i + 1].split('?')[0]
                break

        # 提取关键 cookies
        cookie_map = {c['name']: c for c in cookies}
        session_cookie = cookie_map.get('__Secure-C_SES', {})
        host_cookie = cookie_map.get('__Host-C_OSES', {})

        # 提取 csesidx
        csesidx = parse_qs(parsed_url.query).get('csesidx', [None])[0]

        # 验证所有必需字段
        if all([
            session_cookie.get('value'),
            host_cookie.get('value'),
            csesidx,
            config_id
        ]):
            expiry_timestamp = session_cookie.get('expiry', 0) - 43200
            expires_at = datetime.fromtimestamp(expiry_timestamp).strftime('%Y-%m-%d %H:%M:%S') if expiry_timestamp > 0 else None
            
            account_data = {
                "id": email,
                "csesidx": csesidx,
                "config_id": config_id,
                "secure_c_ses": session_cookie.get('value'),
                "host_c_oses": host_cookie.get('value'),
                "expires_at": expires_at
            }
            
            elapsed = time.time() - start_time
            print_log(f"配置提取完成 ({elapsed:.1f}s)", "OK")
            break

        time.sleep(1)

    if not account_data:
        print_log(f"配置不完整,已跳过 → {email}", "WARN")
        return None

    # 保存到文件
    existing_accounts = []
    if Path(ACCOUNTS_FILE).exists():
        try:
            with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
                existing_accounts = json.load(f)
        except:
            pass
    
    existing_accounts.append(account_data)
    
    with open(ACCOUNTS_FILE, 'w', encoding='utf-8') as f:
        json.dump(existing_accounts, f, indent=2, ensure_ascii=False)
    
    print_log(f"已保存 → {ACCOUNTS_FILE}", "OK")
    return account_data


def fast_type(element, text, delay=0.02):
    """快速输入文本"""
    for c in text:
        element.send_keys(c)
        time.sleep(delay)


def register_single_account(driver, executor):
    """注册单个账号 (来自 app.py 的简洁版本)"""
    start_time = time.time()
    email = get_email()
    if not email:
        return None, False, None, 0

    wait = WebDriverWait(driver, 30)

    try:
        # 1. 访问登录页
        driver.get(LOGIN_URL)
        
        # 检测空白页
        time.sleep(2)
        page_source = driver.page_source
        if len(page_source) < 500 or "about:blank" in driver.current_url:
            raise Exception("页面加载空白,需要重启浏览器")

        # 2. 输入邮箱
        print_log("输入邮箱...")
        inp = wait.until(EC.element_to_be_clickable((By.XPATH, XPATH["email_input"])))
        inp.click()
        inp.clear()
        fast_type(inp, email)
        
        # 验证邮箱是否成功输入
        time.sleep(0.3)
        actual_value = inp.get_attribute("value")
        if actual_value != email:
            print_log(f"输入验证失败,清空后重新输入...", "WARN")
            # 清空后用 JS 输入
            driver.execute_script("arguments[0].value = '';", inp)
            time.sleep(0.1)
            driver.execute_script("arguments[0].value = arguments[1];", inp, email)
            # 触发 input 事件
            driver.execute_script("""
                var event = new Event('input', { bubbles: true });
                arguments[0].dispatchEvent(event);
            """, inp)
            time.sleep(0.3)
        
        print_log(f"邮箱 → {email}", "OK")

        # 3. 点击继续
        time.sleep(0.5)
        btn = wait.until(EC.element_to_be_clickable((By.XPATH, XPATH["continue_btn"])))
        driver.execute_script("arguments[0].click();", btn)
        print_log("继续下一步", "OK")

        # 异步预创建下一个邮箱
        executor.submit(prefetch_email)

        # 4. 获取验证码
        time.sleep(2)
        code = fetch_verification_code(email)
        if not code:
            return email, False, None, time.time() - start_time

        # 5. 输入验证码
        time.sleep(1)
        print_log(f"输入验证码 → {code}")
        try:
            pin = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='pinInput']")))
            pin.click()
            time.sleep(0.1)
            fast_type(pin, code, 0.05)
        except:
            try:
                span = driver.find_element(By.CSS_SELECTOR, "span[data-index='0']")
                span.click()
                time.sleep(0.2)
                driver.switch_to.active_element.send_keys(code)
            except Exception as e:
                print_log(f"验证码输入失败: {e}", "ERROR")
                return email, False, None, time.time() - start_time

        # 6. 点击验证
        time.sleep(0.5)
        try:
            vbtn = driver.find_element(By.XPATH, XPATH["verify_btn"])
            driver.execute_script("arguments[0].click();", vbtn)
        except:
            for btn in driver.find_elements(By.TAG_NAME, "button"):
                if '验证' in btn.text:
                    driver.execute_script("arguments[0].click();", btn)
                    break
        print_log("提交验证", "OK")

        # 7. 输入姓名
        print_log("等待姓名输入...")
        selectors = [
            "input[formcontrolname='fullName']",
            "input[placeholder='全名']",
            "input[placeholder='Full name']",
            "input#mat-input-0",
        ]
        name_inp = None

        # 轮询检测姓名输入框
        for _ in range(30):
            for sel in selectors:
                try:
                    name_inp = driver.find_element(By.CSS_SELECTOR, sel)
                    if name_inp.is_displayed():
                        break
                except:
                    continue
            if name_inp and name_inp.is_displayed():
                break
            time.sleep(1)

        if name_inp and name_inp.is_displayed():
            name = random.choice(NAMES)
            name_inp.click()
            time.sleep(0.2)
            name_inp.clear()
            fast_type(name_inp, name)
            print_log(f"姓名 → {name}", "OK")
            time.sleep(0.3)
            name_inp.send_keys(Keys.ENTER)
            time.sleep(1)
        else:
            print_log("未找到姓名输入框", "ERROR")
            return email, False, None, time.time() - start_time

        # 8. 等待进入工作台
        print_log("等待工作台...")
        for _ in range(30):
            time.sleep(1)
            url = driver.current_url
            if 'business.gemini.google' in url and '/cid/' in url:
                print_log("工作台加载完成", "OK")
                break
        else:
            print_log(f"未跳转到工作台 → {driver.current_url}", "WARN")

        # 9. 保存配置
        elapsed = time.time() - start_time
        config = save_account_config(email, driver)
        if config:
            print_log(f"注册成功 → {email} (耗时 {elapsed:.1f}s)", "OK")
            return email, True, config, elapsed
        return email, False, None, elapsed

    except Exception as e:
        print_log(f"注册异常: {e}", "ERROR")
        log_error(email, str(e))
        return email, False, None, time.time() - start_time


# ==================== 账号上传 ====================
class AccountUploader:
    """账号上传管理类"""
    
    def __init__(self, api_host, admin_key):
        self.api_host = api_host.rstrip('/')
        self.admin_key = admin_key
        self.session = requests.Session()
        
    def login(self):
        """登录到服务器"""
        print_log("连接服务器中...")
        login_url = f"{self.api_host}/login"
        
        try:
            response = self.session.post(
                login_url,
                data={"admin_key": self.admin_key},
                allow_redirects=True,
                timeout=30
            )
            
            if len(self.session.cookies) > 0:
                print_log("服务器连接成功", "OK")
                return True
            
            if response.status_code == 200 and '登录' in response.text:
                print_log("密钥验证失败", "ERROR")
                return False
            
            print_log("服务器连接失败", "ERROR")
            return False
                
        except Exception as e:
            print_log(f"连接异常: {e}", "ERROR")
            return False
    
    def upload_and_replace(self, file_path):
        """覆盖上传账号配置"""
        if not Path(file_path).exists():
            print_log(f"文件不存在 → {file_path}", "ERROR")
            return False
        
        print_log(f"读取本地文件 → {file_path}")
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                accounts_data = json.load(f)
        except Exception as e:
            print_log(f"文件读取异常: {e}", "ERROR")
            return False
        
        print_log(f"本地账号 → {len(accounts_data)} 个")
        print_log("开始上传...")
        
        upload_url = f"{self.api_host}/accounts-config"
        
        try:
            response = self.session.put(
                upload_url,
                json=accounts_data,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                print_log("上传完成!", "OK")
                print_log(f"{result.get('message', '配置已更新')}")
                print_log(f"服务器账号 → {result.get('account_count', len(accounts_data))} 个")
                
                print()
                print_separator()
                print_log("正在获取服务器账号状态...")
                print_separator()
                self.view_accounts()
                
                return True
            else:
                print_log(f"上传失败,状态码: {response.status_code}", "ERROR")
                return False
                
        except Exception as e:
            print_log(f"上传异常: {e}", "ERROR")
            return False
    
    def upload_and_merge(self, file_path):
        """合并上传账号配置(保留远程正常账号)"""
        print_log("智能合并模式启动...")
        
        # 读取本地账号
        if not Path(file_path).exists():
            print_log(f"本地文件缺失 → {file_path}", "ERROR")
            return False
        
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                local_accounts = json.load(f)
            print_log(f"本地账号 → {len(local_accounts)} 个")
        except Exception as e:
            print_log(f"读取本地文件失败: {e}", "ERROR")
            return False
        
        # 获取远程账号配置
        print_log("获取远程配置...")
        config_url = f"{self.api_host}/accounts-config"
        
        try:
            response = self.session.get(config_url, timeout=30)
            if response.status_code == 200:
                remote_config = response.json()
                remote_accounts = remote_config.get('accounts', [])
                print_log(f"远程账号 → {len(remote_accounts)} 个")
            else:
                print_log("远程配置获取失败,仅上传本地", "WARN")
                remote_accounts = []
        except Exception as e:
            print_log(f"远程连接异常: {e},仅上传本地", "WARN")
            remote_accounts = []
        
        # 筛选远程正常账号(未过期、未禁用)
        valid_remote_accounts = []
        for account in remote_accounts:
            if account.get('disabled', False):
                continue
            
            expires_at = account.get('expires_at')
            if expires_at and expires_at != '未设置':
                try:
                    beijing_tz = timezone(timedelta(hours=8))
                    expire_time = datetime.strptime(expires_at, "%Y-%m-%d %H:%M:%S")
                    expire_time = expire_time.replace(tzinfo=beijing_tz)
                    current_time = datetime.now(beijing_tz)
                    if expire_time <= current_time:
                        continue
                except:
                    pass
            
            valid_remote_accounts.append(account)
        
        print_log(f"有效远程账号 → {len(valid_remote_accounts)} 个")
        
        # 合并账号(去重)
        merged_accounts = list(valid_remote_accounts)
        remote_ids = {acc.get('id') for acc in valid_remote_accounts}
        
        new_count = 0
        for local_account in local_accounts:
            local_id = local_account.get('id')
            if local_id not in remote_ids:
                merged_accounts.append(local_account)
                new_count += 1
        
        print_log(f"合并结果 → 保留 {len(valid_remote_accounts)} 个,新增 {new_count} 个,共 {len(merged_accounts)} 个")
        
        # 上传合并后的配置
        print_log("上传合并配置...")
        upload_url = f"{self.api_host}/accounts-config"
        
        try:
            response = self.session.put(
                upload_url,
                json=merged_accounts,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                print_log("合并上传完成!", "OK")
                print_log(f"{result.get('message', '配置已更新')}")
                print_log(f"服务器账号 → {result.get('account_count', len(merged_accounts))} 个")
                
                print()
                print_separator()
                print_log("正在获取服务器账号状态...")
                print_separator()
                self.view_accounts()
                
                return True
            else:
                print_log(f"上传失败,状态码: {response.status_code}", "ERROR")
                return False
                
        except Exception as e:
            print_log(f"上传异常: {e}", "ERROR")
            return False
    
    def view_accounts(self):
        """查看远程账号状态"""
        print_log("查询远程账号...")
        
        view_url = f"{self.api_host}/accounts"
        
        try:
            response = self.session.get(view_url, timeout=30)
            
            if response.status_code == 200:
                data = response.json()
                accounts = data.get('accounts', [])
                total = data.get('total', len(accounts))
                
                if not accounts:
                    print_log("远程无账号配置", "INFO")
                    return True
                
                print(f"\n共 {total} 个账号")
                print_separator("=", 120)
                
                # 表头
                print(f"{'序号':<6} {'账号ID':<35} {'状态':<12} {'过期时间':<22} {'剩余时长':<15} {'累计对话':<10}")
                print_separator("-", 120)
                
                # 账号列表
                for i, account in enumerate(accounts, 1):
                    acc_id = account.get('id', 'N/A')
                    status = account.get('status', 'N/A')
                    expires_at = account.get('expires_at', '未设置')
                    remaining = account.get('remaining_display', 'N/A')
                    conversations = account.get('conversation_count', 0)
                    
                    if len(acc_id) > 33:
                        acc_id = acc_id[:30] + "..."
                    
                    print(f"{i:<6} {acc_id:<35} {status:<12} {expires_at:<22} {remaining:<15} {conversations:<10}")
                
                print_separator("=", 120)
                return True
            else:
                print_log(f"查询失败 → 状态码 {response.status_code}", "ERROR")
                return False
                
        except Exception as e:
            print_log(f"查询异常: {e}", "ERROR")
            return False


# ==================== 主程序流程 ====================
def run_batch_registration(target_count):
    """批量注册账号 (保底成功数模式)"""
    print()
    print_separator()
    print(f"目标: 成功注册 {target_count} 个账号")
    print_separator()
    print()
    
    # 清空旧文件
    if Path(ACCOUNTS_FILE).exists():
        Path(ACCOUNTS_FILE).unlink()
        print_log(f"已清空 → {ACCOUNTS_FILE}")
    
    driver = None
    executor = ThreadPoolExecutor(max_workers=2)
    success_count = 0
    fail_count = 0
    attempt_count = 0
    total_time = 0
    success_times = []

    # 预创建第一个邮箱
    executor.submit(prefetch_email)
    
    # 连续失败计数器(用于保护机制)
    consecutive_fails = 0
    MAX_CONSECUTIVE_FAILS = 20

    # 循环直到成功数达到目标
    while success_count < target_count:
        # 检查全局停止标志
        global STOP_FLAG
        if STOP_FLAG:
            print_log("收到停止信号,中止任务", "WARN")
            STOP_FLAG = False  # 重置标志
            break
        
        # 连续失败保护
        if consecutive_fails >= MAX_CONSECUTIVE_FAILS:
            print_log(f"连续失败 {MAX_CONSECUTIVE_FAILS} 次,中止本轮任务", "ERROR")
            break
        
        attempt_count += 1
        current_target = target_count + fail_count  # 动态调整显示的总数
        
        print()
        print_separator("#", 60)
        print(f"正在注册第 {attempt_count} 个账号 (成功: {success_count}/{target_count})")
        print_separator("#", 60)
        print()

        # 确保浏览器可用
        if driver is None:
            options = uc.ChromeOptions()
            if HEADLESS_MODE:
                print_log("启动无头浏览器...")
                options.add_argument("--headless=new")
                options.add_argument("--disable-gpu")
                options.add_argument("--no-sandbox")
                options.add_argument("--window-size=1200,800")
            else:
                print_log("启动浏览器...")
            driver = uc.Chrome(options=options, use_subprocess=True)
            if not HEADLESS_MODE:
                driver.set_window_size(100, 200)
                driver.set_window_position(50, 50)
            time.sleep(1)
        else:
            try:
                _ = driver.current_url
            except:
                print_log("浏览器已关闭,重启中...")
                try: 
                    driver.quit()
                except: 
                    pass
                options = uc.ChromeOptions()
                if HEADLESS_MODE:
                    options.add_argument("--headless=new")
                    options.add_argument("--disable-gpu")
                    options.add_argument("--no-sandbox")
                    options.add_argument("--window-size=1200,800")
                driver = uc.Chrome(options=options, use_subprocess=True)
                if not HEADLESS_MODE:
                    driver.set_window_size(100, 200)
                    driver.set_window_position(50, 50)
                time.sleep(1)

        try:
            email, success, config, elapsed = register_single_account(driver, executor)
            total_time += elapsed
            
            if success and config:
                success_count += 1
                success_times.append(elapsed)
                consecutive_fails = 0  # 重置连续失败计数
                print_log(f"进度: {success_count}/{target_count} 完成", "OK")
            else:
                fail_count += 1
                consecutive_fails += 1
                print_log(f"失败 +1 (连续失败: {consecutive_fails}/{MAX_CONSECUTIVE_FAILS})", "WARN")
                
        except Exception as e:
            error_msg = str(e).lower()
            print_log(f"注册异常: {e}", "ERROR")
            fail_count += 1
            consecutive_fails += 1
            
            # 检测空白页或页面加载问题
            if "blank" in error_msg or "timeout" in error_msg or "element" in error_msg:
                print_log("检测到页面异常,重启浏览器...", "WARN")
                if driver:
                    try: 
                        driver.quit()
                    except: 
                        pass
                    driver = None
            elif driver:
                try: 
                    driver.quit()
                except: 
                    pass
                driver = None

        avg_time = total_time / attempt_count if total_time > 0 else 0
        print_progress(success_count, target_count, success_count, fail_count, avg_time)

        if success_count < target_count and driver:
            try:
                driver.delete_all_cookies()
            except:
                pass
            time.sleep(random.randint(2, 3))

    executor.shutdown(wait=False)
    if driver:
        try: 
            driver.quit()
        except: 
            pass
        
        # Monkeypatch: 防止 __del__ 再次调用 quit 导致 WinError 6
        try:
            driver.quit = lambda: None
        except:
            pass
            
        driver = None

    # 统计信息
    avg_time = sum(success_times) / len(success_times) if success_times else 0
    min_time = min(success_times) if success_times else 0
    max_time = max(success_times) if success_times else 0
    
    print()
    print_separator()
    print(f"注册完成! 目标: {target_count}, 成功: {success_count}, 失败: {fail_count}, 总尝试: {attempt_count}")
    print(f"总耗时: {total_time:.1f}s | 平均: {avg_time:.1f}s | 最快: {min_time:.1f}s | 最慢: {max_time:.1f}s")
    print(f"账号已保存至: {ACCOUNTS_FILE}")
    print_separator()
    
    return {
        "success": success_count,
        "fail": fail_count,
        "attempts": attempt_count,
        "avg_time": avg_time,
        "success_times": success_times,
        "is_ok": success_count > 0
    }


def handle_task_execution(count, upload_mode, uploader):
    """执行一次完整的任务(注册+上传)"""
    stats = run_batch_registration(count)
    
    if stats.get('is_ok'):
        print()
        # 先登录再上传
        if not uploader.login():
            print_log("服务器登录失败,无法上传", "ERROR")
            return stats
        
        if upload_mode == 'replace':
            print_log("开始覆盖上传到服务器...")
            uploader.upload_and_replace(ACCOUNTS_FILE)
        elif upload_mode == 'merge':
            print_log("开始合并上传到服务器...")
            uploader.upload_and_merge(ACCOUNTS_FILE)
    else:
        print_log("注册流程未成功,取消上传", "WARN")
        
    return stats


def main():
    """主程序入口"""
    print_separator()
    print("Gemini Business 自动注册上传工具")
    print_separator()
    print()
    
    uploader = AccountUploader(API_HOST, ADMIN_KEY)
    
    # 登录服务器
    if not uploader.login():
        print_log("登录失败,无法继续", "ERROR")
        input("\n按回车键退出...")
        sys.exit(1)
    
    print()
    
    while True:
        print("\n请选择操作:")
        print("  1. 注册上传")
        print("  2. 查看远程账号状态")
        print("  3. 退出")
        print()
        
        choice = input("请输入选项 (1-3): ").strip()
        
        if choice == "1":
            # 确定上传模式
            upload_mode = 'merge'
            
            # 1. 询问数量
            count_str = input("\n请输入注册数量 (默认 5): ").strip()
            count = int(count_str) if count_str else 5
            
            # 2. 询问执行模式
            print("\n请选择执行模式:")
            print("  1. 立即执行一次")
            print("  2. 定时循环执行 (支持自定义间隔)")
            mode_choice = input("请输入选项 (1-2): ").strip()
            
            if mode_choice == "2":
                # 定时模式
                hours_str = input("\n请输入循环间隔小时 (默认 12): ").strip()
                try:
                    interval_hours = float(hours_str) if hours_str else 12.0
                except:
                    print_log("输入无效,使用默认值 12 小时", "WARN")
                    interval_hours = 12.0
                
                print(f"\n已选择: 定时循环模式 (间隔 {interval_hours} 小时)")
                run_now_str = input("是否在开始循环前立即运行一次? (y/n, 默认 y): ").strip().lower()
                run_now = run_now_str != 'n'
                
                print_log("定时任务已启动! 按 Ctrl+C 可随时停止", "INFO")
                
                loop_count = 0
                while True:
                    loop_count += 1
                    
                    if run_now or loop_count > 1:
                        print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] >>> 开始第 {loop_count} 次循环任务")
                        handle_task_execution(count, upload_mode, uploader)
                        print_log(f"第 {loop_count} 次任务完成", "INFO")
                    else:
                        print_log("跳过首次运行,直接进入等待", "INFO")

                    # 计算下次运行时间
                    next_run = datetime.now() + timedelta(hours=interval_hours)
                    print_log(f"下一次任务将在 {next_run.strftime('%Y-%m-%d %H:%M:%S')} 开始", "INFO")
                    
                    # 倒计时等待
                    total_seconds = int(interval_hours * 3600)
                    try:
                        while total_seconds > 0:
                            # 每分钟更新一次状态,显示剩余时间
                            if total_seconds % 60 == 0:
                                pass 
                            time.sleep(1)
                            total_seconds -= 1
                    except KeyboardInterrupt:
                        print("\n")
                        print_log("检测到中断, 停止定时任务", "WARN")
                        break
                        
            else:
                # 立即执行模式 (默认)
                print()
                handle_task_execution(count, upload_mode, uploader)
                
        elif choice == "2":
            print()
            uploader.view_accounts()
            
        elif choice == "3":
            print("\n再见!")
            break
            
        else:
            print_log("无效选项,请重试", "WARN")
        
        print()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n")
        print_log("用户中断程序", "INFO")
    except Exception as e:
        print_log(f"程序异常: {e}", "ERROR")
        input("\n按回车键退出...")


📌 转载信息
原作者:
zding
转载时间:
2026/1/14 18:06:03

一个简单易用的局域网唤醒工具,支持通过 Web 界面管理主机、发送 WOL 唤醒包、检测服务器状态以及测试端口连通性。

功能特性

  • 主机管理(添加、编辑、删除)
  • WOL 唤醒发送 Magic Packet
  • 服务器状态检测(ping/SSH/RDP 并发检测)
  • 端口连通性测试(3 秒超时)
  • 数据导入导出(JSON 格式)
  • 密码保护功能
  • 深色 / 浅色主题切换,兼容移动端

项目地址

https://github.com/dhjz/dwol

功能示例图



📌 转载信息
原作者:
ddjhh
转载时间:
2026/1/14 18:03:56

解决什么问题

想要使用本地 monorepo 开发 + 纯 npm 管理的项目,不想对项目进行 pnpm 改造的开发者

项目地址

https://github.com/alamhubb/mono

Mono

零侵入式 Monorepo 开发工具

直接使用 TypeScript 源码开发,无需构建,无需改造项目


什么是 Mono?

Mono 是一套零侵入式 monorepo 开发工具。它允许你在开发期间直接使用 TypeScript 源码,无需构建包或重构项目。

pnpm workspace 的「链式污染」问题

使用 pnpm workspace 意味着整条依赖链都被迫使用 pnpm

项目 A (pnpm) → 必须用 pnpm
  └── 项目 B → 必须用 pnpm(被污染)
        └── 项目 C → 必须用 pnpm(被污染)
              └── 项目 D → 必须用 pnpm(被污染)
  • 所有相关项目都必须改成 pnpm
  • 所有开发人员都必须安装 pnpm
  • 所有 CI/CD 环境都必须配置 pnpm
  • 新成员 npm install 直接报错:workspace:*

详细分析请阅读:「链式污染」—— 为什么一个 pnpm 项目会逼着整条依赖链都改成 pnpm

Mono 的解决方案

使用 Mono,你只需:

  • 运行 mono ./src/index.ts - 就这么简单!
  • 无需重构项目,无需 workspace:*
  • 无需构建包,直接使用源码
  • 修改立即生效,npm/yarn/pnpm 都兼容

pnpm workspace vs Mono

方面pnpm workspaceMono
安装必须安装 pnpm可选
配置文件需要 pnpm-workspace.yaml不需要
package.json需要修改为 workspace:*不需要修改
克隆后使用必须 pnpm installnpm/yarn/pnpm 都可以
依赖包需要先构建直接使用源码
团队协作所有人必须用 pnpm不强制


包列表

本仓库包含两个协同工作的包:

用途安装
mono-mjsNode.js CLI - 用于构建工具、Vite 插件npm install -g mono-mjs
vite-plugin-monoVite 插件 - 用于浏览器运行时npm install -D vite-plugin-mono

何时使用哪个?

场景工具
运行脚本、构建工具mono
Vite 插件、编译器mono
浏览器端导入vite-plugin-mono
Vue/React 组件vite-plugin-mono


快速开始

1. 全局安装 CLI

npm install -g mono-mjs

2. 运行项目

# 直接运行 TypeScript
mono ./src/index.ts

# 使用本地包运行 Vite
mono ./node_modules/vite/bin/vite.js

3. (可选)添加 Vite 插件用于浏览器端

npm install -D vite-plugin-mono
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { viteMono } from 'vite-plugin-mono' export default defineConfig({
  plugins: [
    viteMono(),  // 放第一个! vue()
  ]
})


特性

  • 零侵入 - 无需重构项目,无需配置文件
  • 自动发现 - 递归查找所有本地包
  • 即时生效 - 修改立即生效
  • 包管理器无关 - 支持 npm、yarn、pnpm、bun
  • 零配置 - 默认 ./src/index.ts,可选 local 字段


工作原理

包发现

直线向上查找距离最远的项目根目录 (.idea/.vscode/.git/package.json)
  └── 递归扫描
      └── 查找所有 package.json
          └── 根据 "name" 字段注册

导入拦截

// 你的代码 import { utils } from 'my-utils' // Mono 重定向到源码 // → /path/to/my-utils/src/index.ts 


配置

零配置(默认)

所有包默认使用 ./src/index.ts。无需任何配置!

自定义入口(可选)

package.json 中添加 local 字段:

{ "name": "my-package", "local": "./src/main.ts" } 


环境要求

  • Node.js >= 18.19.0
  • ESM 项目 - package.json 中需要 "type": "module"



📌 转载信息
转载时间:
2026/1/14 18:00:12

https://linux.do/t/topic/1436106?u=yeahhe

英伟达的 api 无法用浏览器网页纯前端调用,所以我用 GLM 搭了一个中转 URL,解决 CORS 问题

https://p1609eqjhck0-d.space.z.ai/api/nim/v1/chat/completions

效果:



📌 转载信息
原作者:
yeahhe
转载时间:
2026/1/14 18:00:10

前言

GPT5 发布后,OAI 的 ChatGPT 网页端终于可以舒适的长对话了。随之而来的,是 GPT5.2Pro 以及 thinking 重推理时,单次对话内容超长、且一个会话可能有上百条记录。默认使用 Chrome 等浏览器加载和滚动时严重卡顿,之前,已有大佬为解决这个问题,开发了懒加载和折叠插件,但仍然存在快速滚动时卡顿的问题。

教程

方法一

Mac 用户,换用 ChatGPT Altas 浏览器即可。

方法二

  1. 在 Chrome 浏览器设置中开启硬件加速
  2. 地址栏输入 chrome://flags(edge 也适用)
    3. 找到 Smooth Scrolling 和 Boost screen refresh rate when scrolling 两个选项,打开
  3. 找到 Partial swap,禁用
  4. 重启浏览器即可。

📌 转载信息
原作者:
moxiyan
转载时间:
2026/1/14 18:00:03

引言:加入 L 站以来,小白也算是慢慢折腾,了解机场 VPS 等一系列网络基础设施概念后,尝试自己当一回赛博工人,借鉴大伙的智慧,手把手从里到外开始建站,装点一下属于自己的博客。再就是自己在 L 站也水了太多,没有自己的产出,督促自己写一篇文档,记录自己折腾的日子。整篇文档仅供参考,望大家轻喷。

参考资料:
1.[教程] Cloudflare 单域名 SaaS 优选教程。让 Cloudflare 不再成为中国减速器,最低延迟低至 10ms!
2. 菜鸟教程零基础 Cloudflare 优选教程
3.0 成本 10 分钟用 Cloudflare + GitHub + Astro 搭建一个全球秒开、永久免费的顶级个人博客

技术栈简要

  • 博客托管:Netlify(稳定、免费、支持 Astro SSR)
  • 优选传输:Cloudflare SaaS(免费证书、自定义主机名)
  • 智能分流:DNSPod(国内优选,海外直连)

当然整个搭建过程也不是一帆风顺的,在我看来并不是按照大佬们的操作指示,一步一步来就会成功,这其中涉及到许多概念的理解,感谢新时代的 AI 技术为我答疑解惑。接着来分别阐述一下为什么选这些技术栈,具体技术就不做介绍了,这里着重说明为什么选 A 而不选 B 此类的问题。

整体流程

[ 用户浏览器]
      |
| 1. DNS查询: "主站在哪?"
v
[ DNSPod 智能解析 ] | |
| (国内用户) | (境外用户)
| 2a. 返回【优选IP】 | 2b. 返回【官方Anycast IP】
| (来自 saas.sin.fan) | (来自 cloudflare 回源域名)
v v
[ Cloudflare 边缘节点 ] <================================+ |
| 3. 携带请求头: Host: 主站
| 识别 SaaS 证书, 建立加密连接 (TLS)
|
| 4. 触发 Fallback Origin (回源后备)
| 通过内部骨干网转发至: 回源域名
v
[ Netlify 目标服务器 ] |
| 5. 匹配 Domain Alias (域名别名)
| 确认 Host: 主站域名是“自己人”
|
| 6. 读取 Astro 构建的静态文件 (dist)
| 返回数据 (且不触发 301 重定向)
v
[ 用户浏览器 (成功渲染) ]

用简单的词汇描述一下整个过程,总的来说需要四个域名,其中两个是我们自己准备并托管的(A 和 B),另外两个分别是第三方服务分配或者维护的(C 和 D),我们只需要拿来用即可:

  • 主域名 A(店面招牌):相当于你经营了一家店,大家记住了你的店名,全世界的人都根据这个店名来访问,只提供了一条引路的作用,在资源访问等涉及效率方面没有优化
  • 回源域名 B(内部通道):托管在 Cloudflare 上(后文简称 CF ),因为 CF 在世界范围内有大量机房(距报告说的是每天 20% 的流量都会从它家走,还是很恐怖的),加上 Anycast 技术可以让你接入延迟最低的 CF 网络。由于主域名 A 不在 CF 托管,CF 默认不让进门,于是需要一个 “内部员工” B 来为 A 担保,相当于访问 A 也可以走 CF 专线了
  • 优选域名 C(专业导航):由三方大佬维护,既不存博客内容,也不管店名,作用是每天都去看哪条路接入 CF 是最快的,然后把这些最快的路告诉你们,实现绕过公网堵塞,实现秒开
  • pages 域名 D(内容仓库):在各种平台站托管时会给你分配一个 pages 域名,这是真正存放博客网站源文件的地方,只负责把网页内容通过回源域名 B 输出去

总结:数据怎么跑的?

  1. 用户看你的招牌(A)
  2. 招牌(A)根据导航(C)的建议,把用户引向最快的 CF 大门
  3. 大门(CF)看到是自己人(B)担保的请求,立即放行
  4. 大门内部通过专线去仓库(D)获取网页内容
  5. 原路返回用户

博客托管

这里解决的是博客存储在哪的问题,刚开始时我以为博客文件都要存在 VPS 服务器上,然后请求的数据流是 [主机 → VPS],后来我才发现静态博客大多采取第三方托管的方式,这里比较有名的托管服务有 Cloudflare Pages, Netlify, Vercel, GitHub Pages 等等。

首先尝试了 Cloudflare Pages,但 Cloudflare Pages 的问题在于,不托管在 Cloudflare 上的域名无法添加到 Pages 主页的自定义域名,具体情况如下图所示。

Cloudflare Pages 行不通,就去看看别人家。由于我搭建博客的时候阅读了 Astro 的官方文档,其中建议我在 Netlify 进行托管,后续的操作中我也就这么做了,整体没有遇到 Cloudflare 那样不认域名的问题,在此不赘述有关 Netlify 具体如何托管博客站,文档非常明白。

图:Astro 官方推荐的托管方式

图:在 Netlify 托管可以手动添加三方域名

优选传输

为什么采取两个域名分开托管的方式?

这是由 CF 自己的托管逻辑决定的,一旦你开启了小黄云托管,CF 会自动接管域名流量,分配一组 Anycast IP,但在 CF 内部,用户是无法修改这一部分的 IP 的,也就是说无法根据外部线路实现动态优化,在大陆内的访问无法得到保障。

在这样的情况下,引入 CF 的 SaaS (Custom Hostnames)服务,即使我们的主域名不在 CF 托管,只要有一个回源域名在担保,也可以给主域名访问提供加速和 SSL 证书。

主域名在 CF 外,我们可以自己定义 DNS 怎么解析,分线路优化。回源域名在 CF 内,保留了海外访问加速通道。

具体到我的情况,我的主域名是在阿里云购买的,然后在 DNSHE 免费申请了一个.de5.net 的域名当回源,藏在后面的域名,不用太好看,能用就行。

图:DNSHE 免费域名

下列为托管的具体步骤,参考了 MIYUSAMA 的贴菜鸟教程零基础 Cloudflare 优选教程,自认为非常详细了,几乎一模一样,有不懂的可以问问 Gemini 等 AI,然后实在不知道的,我有空看到了也会回复大家。

智能分流

一开始我的主域名托管在阿里云上的,毕竟就是从阿里买的,平台的技术力也毋庸置疑,但是用着用着发现,阿里云 DNS 解析没办法指定海外线路,这一点在分流的时候影响还挺大的。相比而言,同样免费套餐的腾讯 DNSPod 默认有海外线路解析。

图:阿里云免费版不支持选取境外解析线路

图:DNSPod 解析列表有境外解析选项

DNS 托管迁移很简单,以我从阿里云托管到腾讯为例,首先找到腾讯的 NX 服务器,一般来说是一对,然后填入阿里云的 NX 服务器配置就行了,这一步的作用是,腾讯告诉阿里,我拿到了这个域名的解析权,后面你就不用管了。

优选结果

图:优选前,DNS 解析权托管在 CF 上

优选后,DNS 解析托管在 DNSPod 上,加速线路分别交给 CF 和 saas.sin.fan (看起来好像没什么变化)

失败的尝试

cloudflare 优选域名加速个人博客求助

L 站很多帖子是对子域名进行优化,技术路线是:CF 回源域名 + CF Pages + 主域名阿里云解析,这一套方法在我尝试下来行不通,原因和前文提到的一样,无法将托管在其他平台上的主域名,加入到 Pages 的自定义域名列表中。

Gemini 给出过解决方案,重写 rules 或者 用 workers,主要思路是改 Host 头为主域名,但实际没有成功过,只能访问回源域名,然后主域名解析不到内容,我猜测原因是在同一个 CF 账号下,Pages 的自动路由策略会干扰 SaaS 的手动回源策略,导致回源域名根本拉不动 Pages 的内容。

图:Gemini 的胡说八道

结束语

技术在于折腾,看起来好像没啥提速效果,但是从整个网络数据流走向,以及架构方面,我有了很多新的收获,也感谢 L 站的各位积极分享!


📌 转载信息
转载时间:
2026/1/14 17:59:59

微软2026年1月补丁星期二修复3个零日漏洞和114个安全缺陷

比利时AZ Monica医院遭网络攻击后关闭服务器

Target员工确认泄露源代码为真实数据

Betterment在加密货币诈骗邮件浪潮后确认数据泄露

门罗大学称2024年数据泄露事件影响32万人

乌克兰军队遭新型慈善主题恶意软件活动攻击

新型VoidLink恶意软件框架针对Linux云服务器

中缅因州医疗保健机构数据泄露影响超14.5万人

错误示例:

error":{“message”:“Post "https://open.bigmodel.cn/api/paas/v4/chat/completions\”: context canceled",“type”:“server_error”,“code”:“internal_server_error”}
错误代码:5XX

解决方法

  1. 调整 Claude Code 默认配置
    Cluade Code 新版本增加了许多 API 的超时检测,请确保在环境变量中完整添加以下所有字段:
 "API_TIMEOUT_MS": "600000", "BASH_DEFAULT_TIMEOUT_MS": "600000", "BASH_MAX_TIMEOUT_MS": "600000", "CLAUDE_API_TIMEOUT": "600000", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32000", "MCP_TIMEOUT": "300000", "MCP_TOOL_TIMEOUT": "600000" 
  1. 确保中间层(如 Cloudflare、Edgeone 等加速和防护服务)设置正确
    近期在 Claude Code 等客户端里出现的 5XX 报错(例如返回体含 “context canceled”“server_error”“internal_server_error”)在不少情况下并非模型服务本身异常,而是链路中间层的回源超时导致连接被提前断开。我们确认当请求经过 Cloudflare 或 EdgeOne 这类 CDN / 代理后,如果上游在长思考、长文本推理、工具调用或搜索阶段出现较长时间未产生任何响应字节,CDN / 代理可能会按 “源站无响应” 判定超时并主动断链;客户端侧就会表现为请求被取消、连接中止,进而映射成 5XX 或类似的 server_error。该问题在非流式响应更常见,尤其是 Claude Code 执行后台任务或子代理调用时往往走非流式路径,容易在几十秒到一分钟的空窗内触发超时。

处理建议:优先在 Cloudflare 或 EdgeOne 的规则引擎中,将相关接口的 “源站超时 / 回源超时 / HTTP 应答超时” 提高到能够覆盖最坏情况下的推理时长,建议至少 180 秒,必要时更高;并尽量只对特定域名或特定 Path 生效,避免影响全站。若所用方案或回源协议不支持将超时调到目标值,建议改为流式输出或加入周期性心跳字节(保持连接持续有数据流动),以避免被中间层误判为无响应。(CPA 新版本在流式输出时,提供此类问题的解决方案,例如持续发送空行作为流式返回内容)

  1. Nginx 配置 (若有)
    参考这位佬友的回复

解决在 claude code 等工具中接入 API 使用(尤其是 CLIProxyAPI 等服务)频发 500 报错的若干方法 - #3,来自 geq1fan


📌 转载信息
原作者:
moxiyan
转载时间:
2026/1/14 17:47:26

故事背景

用 SolidVPS 75 刀大鸡(没有 GPU),建了个 open-webui。知识库文件 3000 + 份。本地跑 bge-m3 时 cpu 负载 50%,且速度慢。因此转入 外部 API 方案。期间顺便换到 qwen-embeding-8B 试了试。

遇到速率限制问题

无论 Nvidia 还是硅基流动,都有 TPM RPM 限制。而我没找到 open-webui 里对外部嵌入模型发起请求的速率限制,因此知识库重建索引时,Nvidia 和 硅基 都容易因为 TPM 返回失败,进而导致 open-webui 无法获取到正确的向量。

解决方案

建站了,自然自带 nginx(我用 nginx-ui)进行管理。加一个流控


    # ======================================================
    # 区域 1: Nvidia (Embedding)
    # 限制: 40 RPM (每分钟40次)
    # 策略: 严格排队
    # ======================================================
    # 使用 "global" 作为 key,表示全局限制,不是按 IP
    limit_req_zone "global" zone=nvidia_limit:10m rate=40r/m;

    # ======================================================
    # 区域 2: SiliconFlow (Reranker)
    # 限制: 2000 RPM (每分钟2000次)
    # 策略: 允许突发
    # ======================================================
    limit_req_zone "global" zone=silicon_limit:10m rate=2000r/m;
    
       # ------------------------------------------------------
    # 服务 1: 代理 Nvidia (监听 8090)
    # ------------------------------------------------------
    server {
        listen 8090;
        
        location / {
            # 允许突发 100 个请求排队
            limit_req zone=nvidia_limit burst=100;
            
            proxy_pass https://integrate.api.nvidia.com;
            proxy_ssl_server_name on;
            proxy_set_header Host integrate.api.nvidia.com;
            
            # 增加超时时间,防止排队太久断开
            proxy_read_timeout 600s;
        }
    }

    # ------------------------------------------------------
    # 服务 2: 代理 SiliconFlow (监听 8091)
    # ------------------------------------------------------
    server {
        listen 8091;

        location / {
            # 允许突发 500 个请求排队
            # 2000r/m 很快,加上 nodelay 可以让请求瞬间转发,
            # 只有超过突发阈值时才开始排队或拒绝。
            # 这里不加 nodelay,保持平滑流控效果。
            limit_req zone=silicon_limit burst=500;

            proxy_pass https://api.siliconflow.cn;
            proxy_ssl_server_name on;
            proxy_set_header Host api.siliconflow.cn;
            
            
             # 增加超时时间,防止排队太久断开
            proxy_read_timeout 600s;
        }
    }

# Server 2: 回源专用 (origin-chat.example.com)
# 强制校验 Header,校验失败直接 444
server {
    listen 2083 ssl;
    listen [::]:2083 ssl;
    http2 on;
    server_name origin-chat.example.com;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;
    ssl_certificate /etc/nginx/ssl/cf_origin_server_2048/fullchain.cer;
    ssl_certificate_key /etc/nginx/ssl/cf_origin_server_2048/private.key;
    # --------------------------------------------------------
    # [核心] 请求头校验逻辑,仅限认证的前置CDN回源
    # --------------------------------------------------------
    # 假设 Header 为 X-Origin-Verify,值为 strict-token-123456
    # 如果不相等 (!=),则直接返回 403
    # if ($http_x_origin_verify != "strict-token-123456" ) {
    # return 444;
    # }
    # --------------------------------------------------------
    # 业务逻辑 (与上方保持一致,确保后端处理逻辑相同)
    location /.well-known/acme-challenge {
        proxy_set_header Host $host;
        proxy_set_header X-Real_IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
        proxy_pass http://127.0.0.1:9180;
    }
    location ~* ^/(auth|api|oauth|admin|signin|signup|signout|login|logout|sso)/ {
        proxy_pass http://openwebui:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        # 注意:这里依然传递 chat 域名给后端,保证应用识别正确的主站域名
        proxy_set_header Host chat.quarkmed.com;
        proxy_read_timeout 10m;
        proxy_buffering off;
        client_max_body_size 20M;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
        add_header Pragma "no-cache" always;
        expires -1;
    }
    location ~* \.(css|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        proxy_pass http://openwebui:8080;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host chat.quarkmed.com;
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
    location / {
        proxy_pass http://openwebui:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host chat.quarkmed.com;
        proxy_read_timeout 10m;
        proxy_buffering off;
        client_max_body_size 20M;
        add_header Cache-Control "public, max-age=300, must-revalidate";
    }
}

open-webui api 地址改为 nginx: 端口


📌 转载信息
原作者:
lekai
转载时间:
2026/1/14 17:47:23

之前在站里刷帖子看见一位佬友推荐了一个 github 项目。可以无需开启 TUN 模式让反重力稳定走代理。当时有其他的事,一直没体验。今天尝试了一下,的确能行。在 cursor 里也能使用。【项目地址】


使用方式
我用的是 windows,直接点击最新的发布版本,下载对应的压缩包。


解压后将 dll 文件和 json 文件拷贝至反重力 Antigravity.exe 程序同目录下,可通过右键单击桌面图标快捷方式。快速跳转至该目录


然后修改 config.json 文件,将端口改为你自己使用的代理工具的端口即可,作者还在 github 上贴出了常用代理软件的端口,大大点赞。

注意:
如果复制这两个文件,启动反重力失败,需要下载 安装 微软常用运行库合集,作者提供了一键下载地址。作者 github 已经提供了详细的教程。

经测试,在 cursor 中也可使用,同样将两个文件复制到 cursor.exe 同目录下,修改 config.json 即可。cursor 中使用的 json 如下:

总结

{
“proxy”: {
“host”: “127.0.0.1”,
“port”: 10808,
“type”: “socks5”
},
“child_injection”: true,
“target_processes”:
}

对于 WSL 环境。作者也提供了替代方案,非常 nice.
有兴趣的老友可以尝试一下,如果觉得好用,给作者一个 star。


📌 转载信息
原作者:
cainiaoxue
转载时间:
2026/1/14 17:47:14

如题

前提是过了 github 的学生认证,前前后后用了一个月吧,但实际沟通就 5 次左右,今天解决了,写一下我的账号的情况

学生认证过了,但是 Azure 官网不显示我的学生订阅,然后按照网上的教程提交了工单,然后第二天就有人联系我了,微软那边告诉我我的账号订阅生效了,但是没有显示

解决办法

  1. 我有两个邮箱,一个教育的,一个 163 邮箱是注册 github 的,

  2. 客服让我先登录微软,用的 163 的邮箱,按下图点击编辑账户信息看是否有绑定 github 的信息,如果有就删掉

  3. 打开 github 的设置 -> 邮箱,保证只有一个邮箱,就是你的教育邮箱

  4. 我是用 163 邮箱注册的 Azure 账号,点击右上角头像,点击切换目录,如果你又多个目录,可以切换试一下,我切换了另一个就有订阅了

有没有佬知道这个页面怎么弄出来,订阅成功后就一直不显示了 (刚成功那会我还看了一眼,可用额度显示 701, 是不是出 bug 了,现在想再看不知道咋看了)

接下来可以用它干点啥呢,有没有佬提提建议


📌 转载信息
原作者:
xiaowang_key
转载时间:
2026/1/14 17:47:12

Google 宣布推出 Veo 3.1 Ingredients to Video 升级版,该工具可基于参考图像生成视频内容。新版本在保持角色身份一致性、背景和物体连贯性方面显著改进,即使使用简单提示词也能生成更具表现力和创意的视频。

此次更新首次支持原生 9:16 竖屏格式输出,专为移动端短视频创作优化。同时新增最高 4K 分辨率的升级功能,1080p 版本提供更清晰的编辑效果,4K 版本则适用于高端制作和大屏幕播放。这些功能已在 Gemini 应用、YouTube、Flow、Google Vids、Gemini API 和 Vertex AI 中上线。


📌 转载信息
原作者:
tangdiao
转载时间:
2026/1/14 17:47:03

从 antigravity 发布以来在 wsl2 环境下访问浏览器一直报错 “Error during tool execution”。刚开始还以为是 antigravity 的 bug(心想 bug 很多也不在乎这一个),过了一个多月发现还没有修好就觉得有点不对劲,刚刚尝试了一下修复好了。

一共两步:

1. 在 windows chrome 里安装 antigravity 插件 https://chromewebstore.google.com/detail/eeijfnjmjelapkebgockoeaadonbchdd?utm_source=item-share-cb

2. 将 wsl2 的网络从默认的 nat 改为 mirrored

感谢这位 reddit 上的大佬
To anyone who use Antigravity on WSL Remote : r/google_antigravity


📌 转载信息
原作者:
ikb
转载时间:
2026/1/14 17:44:34

之前写了一篇关于 SKILL 设计最佳实践的话题,感受到佬们的热情

【SKILL 设计的最佳实践】什么是 SKILL?怎么快速写一个优秀的 SKILL? - 开发调优 - LINUX DO

也感觉好多佬们都对 SKILL 设计还蛮感兴趣的,加上最近一直在对自己的 SKILL 进行调优,研究跟看了不少官方文档以及实践示例,对 SKILL 的设计略有心得,所以就在之前的基础上,把最近关于 SKILL 的进阶设计模式简单分享一下自己的理解吧。

内容绝大多数参考了官方的最佳实践文档,然后也结合自身调优的一些体验。Claude 的文档写的还是不错的,有时间的佬友们还是蛮推荐读一读的,链接就放在下面了。

技能编写最佳实践 - Claude Docs — Skill authoring best practices - Claude Docs

还是一如既往的大纲 + 一图流,方便佬们快速了解文章的大致内容以及组织形式。

1. 引子

为了方便对 SKILL 还不太了解,或者没看过上期的佬们快速理解,我举一个例子再来说明一下什么是 SKILL。

假设你是一名喜欢到世界各地旅行,你需要准备很多装备 (对应 mcp tools) 以应对不同情况,例如:

  • 登山:登山包、山地靴、登山杖、…
  • 极地:防寒服、手套、护目镜、…
  • 雨林:驱虫喷雾、开山斧、…

那么对于特定场景我们将所需的东西都打包起来叫做 "登山套装" 或者 "极地套装"。这种为了应对特殊场景需求的工具套装,Claude 官方把他叫做 SKILL

SKILL 中还包含了一些工作流(workflow),也可以理解为在特殊场景的操作手册,告诉你在遇到突发情况时应该怎么处理。

所以通过安装 SKILL 的方式可以使得模型在特定场景发挥强大的作用,同时你也可以自己根据喜好重新安排对应的 SKILL 中工具或者进行工作流的微调。

2. 渐进式披露 & 二次披露

那么很自然的想法,SKILL 这种将现成工具打包起来的做法是不是多余的呢,为什么我们不能把所有工具跟手册都带上,那么就能处理所有情况呢?

很遗憾的是目前大模型的上下文是有限的,就像如果你带上所有的工具去旅行,那么不仅很笨重(有效上下文受限),而且遇到突发情况时,从一大堆工具中找到适合的工具的难度也上升了(上下文混淆)

所以 SKILL 的设计理念的核心在于 渐进式披露

  • 将所有的工具以及手册,先放到四次元口袋中,不随身携带(不加载上下文)
  • 但是为了能快速加载所需工具,使用目录来进行快速查找

这种记录可用工具套装的纸条(包含 name 以及 description),在遇到特定场景的时候就可以加载对应的工具(MCP tools、脚本)以及手册(项目文档、工作流规范)的模式就是 SKILL 的工作原理。

而这种目录结构加载资源的方式就称为 渐进式披露

“二次披露” 实在 渐进式披露 的基础上的进阶应用,可以进一步提高上下文的有效利用,避免宝贵的上下文资源浪费,为此 Claude 官方文档中提出三种模式来指导二次披露的使用

2.1. 模式一: 附参考资料的高级指南

同样以登山包为例,在一个 SKILL 中可以只包含基础工具以及新的索引,当基础工具不能应对情况的时候,可以再根据索引的信息加载高级工具来处理问题。

以官方示例为例:


---

name: pdf-processing

description: Extracts text and tables from PDF files, fills forms, and merges documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.

---

# PDF Processing ## Quick start

Extract text with pdfplumber:

```python

import pdfplumber

with pdfplumber.open("file.pdf") as pdf:

text = pdf.pages[0].extract_text()

```
## Advanced features **Form filling**: See [FORMS.md](FORMS.md) for complete guide **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns

这个示例中对于进阶的需求,并没有直接披露到 SKILL.md 的 body 中(就是 See xxx 这些内容),因此在 SKILL 调用的时候,这些额外的进阶内容并不会直接加载到上下文中挤占有限的上下文空间。

而当模型结合 SKILL 以及用户问题的时候,感知到可能需要 API reference 的时候,他也具备进一步读取 REFFERENCE.md 文档的能力,来补足上下文。这种二次披露的能力可以进一步使得 SKILL 的上下文得到更有效的利用。

在我的实践中还发现这种能力实际上可以诞生一种能力,我暂时将其称之为 sub-skill,具体的设计以及实现可以等之后发布了我的 SKILL 之后再详细介绍一下 嘻嘻

2.2. 模式二:领域特定组织的加载

官方文档的说法是涉及多个领域的技能,应按领域组织内容,避免加载无关的上下文。

例如,当用户询问销售指标时,Claude 只需要读取与销售相关的模式,而无需读取财务或市场营销数据。这样可以降低令牌使用量,并专注于上下文。
预先将相对独立的上下文进行分开存储,并且使用关键词来进行索引以及引导,当需要特定领域的上下文细节的时候 使用 grep 等工具实现上下文的全量加载

这实际上就是通过 文件组织 的形式,实现不同场景参考资料的分离,因此也是一种二次披露,每次只披露与使用者问题相关的场景上下文来补足信息。

与模式一不同的点在于:

  • 模式一是 根据功能 进行区分,
  • 模式二是根据 领域场景 进行区分

本质上都是对信息的有效整合以及分离调度,这里最需要学习的实际上是他参考资料的文件组织路径形式,将同类的参考进行归档,方便后续修改或者变更,也方便模型进行索引。

bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
├── finance.md (revenue, billing metrics)
├── sales.md (opportunities, pipeline)
├── product.md (API usage, features)
└── marketing.md (campaigns, attribution)
# BigQuery Data Analysis ## Available datasets **Finance**: Revenue, ARR, billing → See [reference/finance.md](reference/finance.md) **Sales**: Opportunities, pipeline, accounts → See [reference/sales.md](reference/sales.md) **Product**: API usage, features, adoption → See [reference/product.md](reference/product.md) **Marketing**: Campaigns, attribution, email → See [reference/marketing.md](reference/marketing.md) ## Quick search Find specific metrics using grep: ```bash
grep -i "revenue" reference/finance.md
grep -i "pipeline" reference/sales.md
grep -i "api usage" reference/product.md
```

2.3. 模式三:条件细节

这一点就更好理解了,就是在引用资源前添加条件,类似通过使用 For xxx 或者是 if xxx, then xxx 的方式来让模型显示的知道何时该主动二次披露相关内容

# DOCX Processing ## Creating documents

Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).

## Editing documents

For simple edits, modify the XML directly.

**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)

2.4. 反模式:避免深度嵌套引用

既然有二次披露,那么三次四次甚至更多次披露是不是能更省上下文呢?

Claude 的回答是:不!

原因在于虽然 Claude 具备嵌套引用,但是本身对于嵌套引用采取了 额外的限制 ,例如只读取前 100 行的内容(head -100),因此嵌套引用可能会导致 信息缺失 以及 逻辑结构混乱 的现象。

官方推荐的做法是最好只调用到 二次披露 ,即只在 SKILL.md 中进行索引,不要在更深的文件结构下进行索引的创建以及链接。

注意事项:

  • 对于二次披露的参考文献应当限制长度(行数小于 100 行,因为 claude 可能只读前 100 行)
  • 如果超过 100 行,建立目录来让 claude 具备感知全文内容的能力
  • PS: 但如果目录超 100 行那就无话可说了 哈哈哈


文章正文就先写到这吧 哈哈哈(下面是一些自己的感悟以及碎碎念

总的来说 SKILL 并不是一个非常高深或者难的技术,甚至对计算机没有一点基础的小白也能迅速上手,但是更多的如果想要设计一个非常优秀的 SKILL 还是需要对这些底层的调用机制有一定了解的。

笔者在自己构建 SKILL 的过程中发现,虽然 skill-creator 这个官方工具能快速搭建一个大致框架,但是内容的详细程度还是没有想象的那么优秀,很多都还蛮简单的,而且设计模式并没有很好的贯彻最佳实践中的要点,特别是二次披露以及文件组织,常常因为获取的内容跟信息太少而没办法有效组织。

而在后续修改的过程中,如果设计者不了解最佳实践的基础原理,由于 Claude Code 谄媚的现象,往往会导致 SKILL 的后续修改反而脱离了最佳实践的要求,导致性能变差。

顺便预告一下,下次更新可能会介绍 SKILL 设计中 工作流以及反馈循环 怎么设计以及调优的相关内容。

佬们周中愉快 哈哈哈


📌 转载信息
原作者:
guanhuhao
转载时间:
2026/1/14 17:43:27

最近被叫去帮小老板代上一节大一的思政课(话说,为什么我一个 AI 方向的博后要做这种事情 - -),选题选了浅淡辩证唯物主义的认识论,主要从 教员 文章 《人的正确思想是从哪里来的》展开,结合 AI 革命的时代背景,讨论青年如何借助 AI 学习与自我提升、如何积极实践,以及如何再用实践经验改造方法,最终形成自我提升的闭环。

在用 gemini3-pro-image 做 ppt 的时候,碰巧发现智谱发布了新的 image 模型。并宣称在多项关于文字渲染的 benchmark 上达到了 SOTA,于是我就起了尝试的心思。具体宣称指标如下:

先说结论,在经过了多轮测试后发现,在模型体量相近的情况下,智谱新模型的文字渲染能力确实还行,但离 gemini-3pro-image-2k/4k 还是有一些距离,且需要更多的提示词来告知图像生成的细节才能达到较好的效果,没有 gemini-3-pro-image,那种用简短的提示词就能生成让人眼前一亮效果的能力。可能并不适合用于制作 PPT。放两张控制变量下,不同模型的文生图让大家参考一下:
GLM-IMAGE:


Gemini-3-pro-image:


📌 转载信息
原作者:
AlexChu1996
转载时间:
2026/1/14 17:42:29

最近在用 iFlow 跑本地 AI / Agent,发现只能在终端里用,
于是写了一个 Android 客户端,可以用手机远程控制本地 iFlow。

这个项目主要是:

  • iFlow 运行在 Linux / 本地机器上
  • 通过 ACP(Agent Communication Protocol)启动服务
  • Android 作为客户端,通过网络连接并交互

启动方式很简单:

iflow --experimental-acp --port 8090

然后 Android 端连接本机或局域网 IP 即可。

项目已开源:

目前是一个可用的基础版本,适合:

  • 在 Linux 上跑 iFlow 的用户
  • 想用手机随时发 prompt / 看回复
  • 对本地 AI、Agent、Android 客户端感兴趣的人

欢迎 issue / PR,一起完善。




📌 转载信息
原作者:
lamb031226
转载时间:
2026/1/14 17:41:58

安装简单,git 下来之后直接 install 就好,里面有安装命令。

因为本人通过反重力反代可以使用 claude 另外加入了 gpt team, 有了三个都可以用的编程工具,感觉荒废不太好,就各取所长。本人比较喜欢反重力的 ui 和计划模式,能够掌控全局,而他的 planning 模式像是产品经理,而 cc 和 cx 是很好的执行者,于是尝试了这个工作流,让他能够创建计划后让用户审批,然后他会让 cx 或者 cc 去执行,最后他自行审批,然后让用户判断是否达标。
欢迎大家使用尝试并且反馈问题。


📌 转载信息
原作者:
N1nEmAn
转载时间:
2026/1/14 17:41:39

基于开源项目 homepage

使用 AI 工具辅助增加了点自己觉得还行的修改

  • 去掉了 tab 及服务书签的背景,组件没有用到,应该还是有默认磨玻璃背景的
  • 增加了动态背景 bing 壁纸获取,需要自定义的,各位大佬们自己实现吧哈哈
  • 添加了 APlayer 音乐播放,纯属瞎玩.. 哈哈
  • 鼠标悬停显示 url

效果展示


代码是 AI 的,成果是各位大佬的,有需要的佬配置自取
homepage 配置

首次发帖,战战兢兢,佬友们轻点喷…


📌 转载信息
原作者:
msxiaoice
转载时间:
2026/1/14 17:41:24

OpenAI 的网页版一直存在一个 bug,当点击复制按钮时,复制出的公式内容会进行错误的转义。

正确的 Markdown 格式ChatGPT 复制出来的结果
\[ f(x) = \frac{1}{x} \][ f(x) = \frac{1}{x} ]
\( a = \frac{3}{5} \)( a = \frac{3}{5} )

然后,我就借助 Antigravity 自带的浏览器控制功能,对这个 bug 进行了逆向分析

分析结果是:

  1. ChatGPT 页面中,Message 对象存储的原始 Markdown 是完整的,形如 "\\( a=\\frac{3}{5} \\)"
  2. 在点击复制时,触发一个名为 copyToClipboard 的函数
  3. 该函数会调用一个 stripEscapes() 函数,手动进行 转义符清洗,例如把 \# 变成 #
  4. 错误就出在这里,它也会错误地将 \[ 变成 [,导致复制出的 LaTeX 出问题

找到了错误,修正就很简单,这里直接搜索 React 组件的原始数据,找到 markdown 后直接复制即可。

我也是让 Antigravity Opus 写了个油猴脚本,先将原始的复制事件拦截,然后替换成正常的,效果很好。如果有更多需求,佬友们可以自行二次开发。

// ==UserScript== // @name         ChatGPT LaTeX 复制修复 // @namespace    https://github.com/theigrams // @version      1.0.0 // @description  直接从 React 状态读取原始 Markdown,绕过 ChatGPT 的错误转义逻辑 // @author       Antigravity // @match        https://chatgpt.com/* // @match        https://chat.openai.com/* // @icon         https://chat.openai.com/favicon.ico // @grant        none // @run-at       document-end // ==/UserScript==

(function () { "use strict";

  /**
* 从 React Fiber 中提取原始 Markdown
* @param {HTMLElement} turnElement - conversation-turn 元素
* @returns {string} 原始 Markdown 文本
* @throws {Error} 如果无法读取 React 状态
*/
function getOriginalMarkdownFromReact(turnElement) { // 获取 React Fiber 节点 const fiberKey = Object.keys(turnElement).find((k) => k.startsWith("__reactFiber$") ); if (!fiberKey) { throw new Error("无法找到 React Fiber 节点"); } let fiber = turnElement[fiberKey]; let depth = 0; const maxDepth = 50; // 向上遍历 Fiber 树,查找消息数据 while (fiber && depth < maxDepth) { const props = fiber.memoizedProps; if (props) { // 尝试多种可能的数据路径 // 路径 1: message.content.parts if (props.message?.content?.parts) { const parts = props.message.content.parts; if (Array.isArray(parts) && parts.length > 0) { console.log("[Markdown Copy] 找到数据路径: message.content.parts"); return parts.join("\n"); } } // 路径 2: displayParts if (props.displayParts) { const parts = props.displayParts; if (Array.isArray(parts) && parts.length > 0) { // displayParts 可能是对象数组 const text = parts .map((p) => (typeof p === "string" ? p : p.text || "")) .join(""); if (text) { console.log("[Markdown Copy] 找到数据路径: displayParts"); return text; } } } // 路径 3: text 属性 if (typeof props.text === "string" && props.text.includes("\\")) { console.log("[Markdown Copy] 找到数据路径: text"); return props.text; } // 路径 4: content 字符串 if (typeof props.content === "string" && props.content.includes("\\")) { console.log("[Markdown Copy] 找到数据路径: content"); return props.content; } // 路径 5: children 中的文本 if (props.children?.props?.message?.content?.parts) { const parts = props.children.props.message.content.parts; if (Array.isArray(parts) && parts.length > 0) { console.log( "[Markdown Copy] 找到数据路径: children.props.message.content.parts" ); return parts.join("\n"); } } } fiber = fiber.return; depth++; } throw new Error(`遍历了 ${depth} 层 Fiber 节点,未找到原始 Markdown 数据`); } /**
* 深度搜索 React 状态中的 Markdown 内容
* @param {HTMLElement} turnElement
* @returns {string}
*/
function deepSearchMarkdown(turnElement) { const fiberKey = Object.keys(turnElement).find((k) => k.startsWith("__reactFiber$") ); if (!fiberKey) { throw new Error("无法找到 React Fiber 节点"); } const visited = new Set(); let result = null; function search(obj, path = "", depth = 0) { if (depth > 20 || !obj || visited.has(obj)) return; if (typeof obj !== "object") return; visited.add(obj); // 检查是否是我们要找的数据 if (Array.isArray(obj) && obj.length > 0 && typeof obj[0] === "string") { const text = obj.join("\n"); // 检查是否包含 LaTeX 定界符(正确的格式) if ( text.includes("\\[") || text.includes("\\(") || text.includes("\\begin") ) { console.log(`[Markdown Copy] 深度搜索找到数据: ${path}`); result = text; return; } } if (typeof obj === "string" && obj.length > 50) { if ( obj.includes("\\[") || obj.includes("\\(") || obj.includes("\\begin") ) { console.log(`[Markdown Copy] 深度搜索找到字符串: ${path}`); result = obj; return; } } for (const key in obj) { if (result) return; // 已找到,停止搜索 try { search(obj[key], `${path}.${key}`, depth + 1); } catch (e) { // 忽略循环引用等错误 } } } let fiber = turnElement[fiberKey]; let fiberDepth = 0; while (fiber && fiberDepth < 30 && !result) { if (fiber.memoizedProps) { search(fiber.memoizedProps, `fiber[${fiberDepth}].memoizedProps`); } if (fiber.memoizedState && !result) { search(fiber.memoizedState, `fiber[${fiberDepth}].memoizedState`); } fiber = fiber.return; fiberDepth++; } if (!result) { throw new Error("深度搜索未找到包含 LaTeX 定界符的原始 Markdown"); } return result; } /**
* 显示提示消息
*/
function showToast(message, isError = false) { const toast = document.createElement("div"); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: ${ isError ? "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)" : "linear-gradient(135deg, #10a37f 0%, #1a7f64 100%)" }; color: white; padding: 12px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; z-index: 10000; box-shadow: 0 4px 12px ${ isError ? "rgba(239, 68, 68, 0.3)" : "rgba(16, 163, 127, 0.3)" }; animation: slideIn 0.3s ease; `; const style = document.createElement("style"); style.textContent = ` @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } `; document.head.appendChild(style); document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateY(10px)"; toast.style.transition = "all 0.3s ease"; setTimeout(() => toast.remove(), 300); }, 3000); } /**
* 拦截复制按钮点击
*/
function interceptCopyButtons() { document.addEventListener( "click", async (e) => { // 检查是否点击了复制按钮 const btn = e.target.closest( 'button[data-testid="copy-turn-action-button"]' ); if (!btn) return; // 阻止原始的复制行为 e.stopPropagation(); e.preventDefault(); console.log("[Markdown Copy] 拦截到复制按钮点击"); try { // 找到对应的消息容器 const turn = btn.closest('[data-testid^="conversation-turn-"]'); if (!turn) { throw new Error("无法找到消息容器元素 (conversation-turn)"); } console.log( "[Markdown Copy] 找到消息容器:", turn.getAttribute("data-testid") ); // 尝试从 React 状态读取原始 Markdown let markdown; try { markdown = getOriginalMarkdownFromReact(turn); } catch (e1) { console.log("[Markdown Copy] 常规路径失败,尝试深度搜索..."); markdown = deepSearchMarkdown(turn); } if (!markdown) { throw new Error("获取到的 Markdown 为空"); } // 写入剪贴板 await navigator.clipboard.writeText(markdown); console.log("[Markdown Copy] 成功复制原始 Markdown"); console.log( "[Markdown Copy] 预览:", markdown.substring(0, 200) + "..." ); showToast("✓ 已复制原始 Markdown"); } catch (error) { console.error("[Markdown Copy] 错误:", error); showToast(`✗ 复制失败: ${error.message}`, true); // 不 fallback,直接报错 throw error; } }, true ); // 使用捕获阶段,确保先于其他处理器执行 } // 初始化 console.log("[Markdown Copy] 脚本已加载 v1.0.0"); interceptCopyButtons(); console.log("[Markdown Copy] 复制按钮拦截已启用"); })();

📌 转载信息
原作者:
Theigrams
转载时间:
2026/1/14 17:40:35

前排声明:考虑到大家观感,文章经过 AI 润色排版。

前段时间美团月卡的推广返现力度很大,我也跟风注册了美团联盟。但在实际推广中遇到了一个巨大的痛点

  • 别人的推广:直接分享二维码海报,用户在微信扫码后自动跳转小程序,体验丝滑。

  • 我的推广:美团联盟小程序里只给了一串口令,用户需要 “复制口令 → 打开美团 App → 弹窗跳转”,步骤极其繁琐,严重影响转化率。

全网搜索 “口令转链接” 无果后,我尝试求助 Gemini,没想到真把这事儿搞定了!以下是全流程复盘:

第一步:逆向分析竞品二维码

首先,我找了一张别人分享的可以直接跳转的二维码海报,进行解码。 得到了一串看起来很乱的链接: http://i.meituan.com/wrapapi/qrcode/pt?url=%2Findex%2Fpages%2Fh5%2Fh5%3Fweburl%3Dhttps%253A%252F%252Fclick.meituan.com%252Ft%253Ft%253D1…

问题:这串链接直接在微信内点击是无法打开的。

第二步:AI 介入分析

我将链接投喂给 Gemini,它迅速给出了关键分析:

Gemini 的洞察: 这个链接是美团用于唤起微信小程序的中间层接口数据。 之所以无法直接点击打开,是因为其中的核心参数是小程序内部路径(相对路径),而非标准的 HTTP 网页链接。微信的 “扫一扫” 内置了特定解析逻辑,能识别并跳转,但直接点击不行。

Gemini 帮我将链接拆解为两部分:

  • 外壳(唤起接口)http://i.meituan.com/wrapapi/qrcode/pt?url=...

  • 内核(核心推广参数)click.meituan.com/t?t=1...

结论:只要能拿到属于我自己的 “内核” 链接,套进 “外壳” 里,就能生成二维码!

第三步:抓包获取核心参数

由于美团 App 的月卡购买界面没有直接的分享按钮(无法直接提取链接),我只能进行抓包操作。

  • 抓包过程:搜索 click 关键词无果。

  • Gemini 提示:尝试搜索 html 相关请求。

  • 成功定位:在 Gemini 的建议下,我成功抓到了两个链接,其中一个正是包含返利参数(Aff/P 值)的关键链接:

    https://click.meituan.com/t?p=oen2XLxziUb…
    
    

第四步:合成与验证

最后,Gemini 帮我生成了最终的组合链接格式。我将其转成二维码,找朋友实测购买。

结果:微信扫码直接跳转,后台成功收到返利!


📌 转载信息
原作者:
d.to
转载时间:
2026/1/14 17:40:24