Skill: wang259797/ad-account-manager (version 1.0.0)

--- name: "ad-account-manager" description: "Manage ad accounts (广点通, 巨量引擎) via browser automation with cookie login. Invoke when user asks to manage ad accounts, login to ad platforms, or view account status." --- # 广告账户管理 Skill ## 功能描述 通过浏览器自动化（Cookie登录），实现多平台广告账户的统一管理，无需逐个登录广告后台，提升账户切换和状态查看效率。 ## 核心功能 1. **登录管理**：支持广点通（广告主/服务商）和巨量引擎的扫码登录，保存Cookie到本地 2. **一键登录**：使用已存储的Cookie自动登录对应广告后台 3. **账户状态总览**：拉取所有已登录账户的基础信息，异常账户标红提醒 ## 使用场景 - 用户发送"管理广告账户" - 用户请求登录广点通或巨量引擎 - 用户需要查看账户状态（余额、消耗、预算等）

path: ad_manager.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
广告账户管理核心模块
实现Cookie登录、账户管理和状态查看功能
"""

import os
import json
import time
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from enum import Enum


class Platform(Enum):
    """广告平台枚举"""
    GUANGDIANTONG = "guangdiantong"  # 广点通
    JULIANG = "juliang"  # 巨量引擎


class AccountType(Enum):
    """账户类型枚举"""
    ADVERTISER = "advertiser"  # 广告主
    AGENCY = "agency"  # 服务商


@dataclass
class AdAccount:
    """广告账户数据结构"""
    id: str
    platform: str
    account_type: str
    name: str
    cookies: List[Dict]
    created_at: float
    last_used: float
    balance: float = 0.0
    daily_cost: float = 0.0
    budget: float = 0.0
    status: str = "normal"  # normal, warning, error


class AdAccountManager:
    """广告账户管理器"""

    def __init__(self, storage_dir: Optional[str] = None):
        """
        初始化账户管理器
        
        Args:
            storage_dir: 存储目录，默认为当前目录下的data文件夹
        """
        if storage_dir is None:
            storage_dir = os.path.join(os.path.dirname(__file__), "data")
        
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(parents=True, exist_ok=True)
        
        self.accounts_file = self.storage_dir / "accounts.json"
        self.accounts: Dict[str, AdAccount] = {}
        
        # 加载已保存的账户
        self._load_accounts()

    def _load_accounts(self):
        """从文件加载账户数据"""
        if self.accounts_file.exists():
            try:
                with open(self.accounts_file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    for account_id, account_data in data.items():
                        self.accounts[account_id] = AdAccount(**account_data)
            except Exception as e:
                print(f"加载账户数据失败: {e}")

    def _save_accounts(self):
        """保存账户数据到文件"""
        data = {}
        for account_id, account in self.accounts.items():
            data[account_id] = asdict(account)
        
        with open(self.accounts_file, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    def get_platform_login_url(self, platform: Platform, account_type: Optional[AccountType] = None) -> str:
        """
        获取平台登录URL
        
        Args:
            platform: 广告平台
            account_type: 账户类型（仅广点通需要）
            
        Returns:
            登录URL
        """
        if platform == Platform.GUANGDIANTONG:
            if account_type == AccountType.AGENCY:
                return "https://e.qq.com/agency"
            return "https://e.qq.com/ads"
        elif platform == Platform.JULIANG:
            return "https://ad.oceanengine.com"
        return ""

    def add_account(self, platform: Platform, account_type: AccountType, 
                    name: str, cookies: List[Dict]) -> str:
        """
        添加新账户
        
        Args:
            platform: 广告平台
            account_type: 账户类型
            name: 账户名称
            cookies: Cookie列表
            
        Returns:
            账户ID
        """
        account_id = f"{platform.value}_{account_type.value}_{int(time.time())}"
        
        account = AdAccount(
            id=account_id,
            platform=platform.value,
            account_type=account_type.value,
            name=name,
            cookies=cookies,
            created_at=time.time(),
            last_used=time.time()
        )
        
        self.accounts[account_id] = account
        self._save_accounts()
        
        return account_id

    def get_account(self, account_id: str) -> Optional[AdAccount]:
        """获取指定账户"""
        return self.accounts.get(account_id)

    def list_accounts(self) -> List[AdAccount]:
        """列出所有账户"""
        return list(self.accounts.values())

    def update_account_cookies(self, account_id: str, cookies: List[Dict]) -> bool:
        """更新账户Cookie"""
        if account_id in self.accounts:
            self.accounts[account_id].cookies = cookies
            self.accounts[account_id].last_used = time.time()
            self._save_accounts()
            return True
        return False

    def delete_account(self, account_id: str) -> bool:
        """删除账户"""
        if account_id in self.accounts:
            del self.accounts[account_id]
            self._save_accounts()
            return True
        return False

    def update_account_status(self, account_id: str, balance: float, 
                              daily_cost: float, budget: float, status: str):
        """更新账户状态信息"""
        if account_id in self.accounts:
            account = self.accounts[account_id]
            account.balance = balance
            account.daily_cost = daily_cost
            account.budget = budget
            account.status = status
            account.last_used = time.time()
            self._save_accounts()

    def get_accounts_overview(self) -> List[Dict[str, Any]]:
        """获取所有账户总览信息"""
        overview = []
        for account in self.accounts.values():
            # 判断是否为异常账户
            is_abnormal = False
            if account.balance < 100 or account.status in ["error", "rejected"]:
                is_abnormal = True
            
            overview.append({
                "id": account.id,
                "name": account.name,
                "platform": account.platform,
                "account_type": account.account_type,
                "balance": account.balance,
                "daily_cost": account.daily_cost,
                "budget": account.budget,
                "status": account.status,
                "is_abnormal": is_abnormal,
                "last_used": account.last_used
            })
        return overview


path: browser_helper.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
浏览器自动化辅助模块
使用Playwright实现扫码登录、Cookie保存和一键登录功能
"""

import asyncio
import json
from typing import List, Dict, Optional, Callable
from pathlib import Path


class BrowserHelper:
    """浏览器辅助类"""

    def __init__(self, headless: bool = False):
        """
        初始化浏览器辅助类
        
        Args:
            headless: 是否无头模式运行
        """
        self.headless = headless
        self.browser = None
        self.context = None
        self.page = None

    async def start(self):
        """启动浏览器"""
        try:
            from playwright.async_api import async_playwright
            
            self.playwright = await async_playwright().start()
            self.browser = await self.playwright.chromium.launch(headless=self.headless)
            self.context = await self.browser.new_context()
            self.page = await self.context.new_page()
        except ImportError:
            raise ImportError("请安装playwright: pip install playwright && playwright install chromium")

    async def close(self):
        """关闭浏览器"""
        if self.page:
            await self.page.close()
        if self.context:
            await self.context.close()
        if self.browser:
            await self.browser.close()
        if hasattr(self, 'playwright'):
            await self.playwright.stop()

    async def get_cookies(self) -> List[Dict]:
        """获取当前页面的Cookies"""
        if self.context:
            return await self.context.cookies()
        return []

    async def set_cookies(self, cookies: List[Dict]):
        """设置Cookies"""
        if self.context:
            await self.context.add_cookies(cookies)

    async def navigate_and_wait_for_login(self, url: str, 
                                           check_login_callback: Optional[Callable] = None,
                                           timeout: int = 300) -> List[Dict]:
        """
        导航到登录页面并等待用户扫码登录
        
        Args:
            url: 登录页面URL
            check_login_callback: 检查是否登录成功的回调函数
            timeout: 超时时间（秒）
            
        Returns:
            登录后的Cookies
        """
        if not self.page:
            await self.start()

        # 导航到登录页面
        await self.page.goto(url)
        
        print(f"请在浏览器中扫码登录，超时时间: {timeout}秒")
        
        # 等待登录成功
        start_time = asyncio.get_event_loop().time()
        logged_in = False
        
        while not logged_in and (asyncio.get_event_loop().time() - start_time) < timeout:
            await asyncio.sleep(2)
            
            # 如果提供了检查回调，使用回调检查
            if check_login_callback:
                logged_in = await check_login_callback(self.page)
            else:
                # 默认检查：可以通过检查URL变化或特定元素来判断
                current_url = self.page.url
                if url not in current_url and 'login' not in current_url.lower():
                    logged_in = True
        
        if not logged_in:
            print("登录超时")
            return []
        
        print("登录成功！")
        return await self.get_cookies()

    async def login_with_cookies(self, url: str, cookies: List[Dict]) -> bool:
        """
        使用Cookies一键登录
        
        Args:
            url: 目标页面URL
            cookies: Cookie列表
            
        Returns:
            是否登录成功
        """
        if not self.page:
            await self.start()

        # 先访问一次域名以设置cookies
        await self.page.goto(url, wait_until="domcontentloaded")
        
        # 设置cookies
        await self.set_cookies(cookies)
        
        # 重新访问页面
        await self.page.goto(url)
        
        # 简单验证是否登录成功
        await asyncio.sleep(2)
        return True

    async def fetch_account_info(self, platform: str) -> Dict:
        """
        拉取账户信息（余额、消耗等）
        
        Args:
            platform: 平台名称
            
        Returns:
            账户信息字典
        """
        # 这里需要根据不同平台实现具体的信息拉取逻辑
        # 由于各平台页面结构不同，这里提供一个框架
        info = {
            "balance": 0.0,
            "daily_cost": 0.0,
            "budget": 0.0,
            "status": "normal"
        }
        
        # TODO: 根据不同平台实现具体的页面解析逻辑
        # 例如：
        # if platform == "guangdiantong":
        #     # 解析广点通页面
        # elif platform == "juliang":
        #     # 解析巨量引擎页面
        
        return info


def save_cookies_to_file(cookies: List[Dict], filepath: str):
    """保存Cookies到文件"""
    Path(filepath).parent.mkdir(parents=True, exist_ok=True)
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(cookies, f, ensure_ascii=False, indent=2)


def load_cookies_from_file(filepath: str) -> List[Dict]:
    """从文件加载Cookies"""
    if Path(filepath).exists():
        with open(filepath, 'r', encoding='utf-8') as f:
            return json.load(f)
    return []


path: main.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
广告账户管理 Skill 主入口
提供交互式命令行界面
"""

import asyncio
import sys
from ad_manager import AdAccountManager, Platform, AccountType
from browser_helper import BrowserHelper


class AdAccountSkill:
    """广告账户管理 Skill"""

    def __init__(self):
        self.manager = AdAccountManager()
        self.browser = None

    async def _init_browser(self):
        """初始化浏览器"""
        if not self.browser:
            self.browser = BrowserHelper(headless=False)
            await self.browser.start()

    async def close(self):
        """关闭资源"""
        if self.browser:
            await self.browser.close()

    async def handle_manage_ad_accounts(self):
        """处理管理广告账户请求"""
        print("请选择要管理的广告平台：")
        print("1. 广点通")
        print("2. 巨量引擎")
        
        choice = input("请输入选项 (1/2): ").strip()
        
        if choice == "1":
            await self.handle_guangdiantong()
        elif choice == "2":
            await self.handle_juliang()
        else:
            print("无效选项")

    async def handle_guangdiantong(self):
        """处理广点通账户"""
        print("\n请选择账户类型：")
        print("1. 广告主后台")
        print("2. 服务商后台")
        
        choice = input("请输入选项 (1/2): ").strip()
        
        account_type = AccountType.ADVERTISER if choice == "1" else AccountType.AGENCY
        login_url = self.manager.get_platform_login_url(Platform.GUANGDIANTONG, account_type)
        
        print(f"\n即将打开登录页面：{login_url}")
        await self._init_browser()
        
        # 打开登录页面等待扫码
        cookies = await self.browser.navigate_and_wait_for_login(login_url, timeout=300)
        
        if cookies:
            account_name = input("\n请输入账户名称：").strip()
            account_id = self.manager.add_account(
                Platform.GUANGDIANTONG,
                account_type,
                account_name,
                cookies
            )
            print(f"\n账户添加成功！账户ID: {account_id}")

    async def handle_juliang(self):
        """处理巨量引擎账户"""
        login_url = self.manager.get_platform_login_url(Platform.JULIANG)
        
        print(f"\n即将打开登录页面：{login_url}")
        await self._init_browser()
        
        # 打开登录页面等待扫码
        cookies = await self.browser.navigate_and_wait_for_login(login_url, timeout=300)
        
        if cookies:
            account_name = input("\n请输入账户名称：").strip()
            account_id = self.manager.add_account(
                Platform.JULIANG,
                AccountType.ADVERTISER,
                account_name,
                cookies
            )
            print(f"\n账户添加成功！账户ID: {account_id}")

    async def handle_one_click_login(self):
        """一键登录账户"""
        accounts = self.manager.list_accounts()
        
        if not accounts:
            print("没有已保存的账户")
            return
        
        print("\n请选择要登录的账户：")
        for i, account in enumerate(accounts, 1):
            platform_name = "广点通" if account.platform == "guangdiantong" else "巨量引擎"
            type_name = "广告主" if account.account_type == "advertiser" else "服务商"
            print(f"{i}. {account.name} ({platform_name} - {type_name})")
        
        choice = input("\n请输入选项: ").strip()
        
        try:
            index = int(choice) - 1
            if 0 <= index < len(accounts):
                account = accounts[index]
                login_url = self.manager.get_platform_login_url(
                    Platform(account.platform),
                    AccountType(account.account_type)
                )
                
                print(f"\n正在登录: {account.name}")
                await self._init_browser()
                success = await self.browser.login_with_cookies(login_url, account.cookies)
                
                if success:
                    print("登录成功！浏览器已打开")
                    # 更新最后使用时间
                    self.manager.update_account_cookies(account.id, account.cookies)
            else:
                print("无效选项")
        except ValueError:
            print("无效输入")

    async def handle_account_overview(self):
        """账户状态总览"""
        overview = self.manager.get_accounts_overview()
        
        if not overview:
            print("没有已保存的账户")
            return
        
        print("\n===== 账户状态总览 =====")
        for item in overview:
            platform_name = "广点通" if item["platform"] == "guangdiantong" else "巨量引擎"
            status_emoji = "🔴" if item["is_abnormal"] else "🟢"
            
            print(f"\n{status_emoji} {item['name']} ({platform_name})")
            print(f"   余额: ¥{item['balance']:.2f}")
            print(f"   当日消耗: ¥{item['daily_cost']:.2f}")
            print(f"   预算: ¥{item['budget']:.2f}")
            print(f"   状态: {item['status']}")
            
            if item["is_abnormal"]:
                print("   ⚠️  异常提醒：余额不足或状态异常！")

    async def run(self):
        """运行主循环"""
        while True:
            print("\n===== 广告账户管理 =====")
            print("1. 管理广告账户（扫码登录）")
            print("2. 一键登录账户")
            print("3. 账户状态总览")
            print("4. 退出")
            
            choice = input("\n请输入选项 (1-4): ").strip()
            
            if choice == "1":
                await self.handle_manage_ad_accounts()
            elif choice == "2":
                await self.handle_one_click_login()
            elif choice == "3":
                await self.handle_account_overview()
            elif choice == "4":
                await self.close()
                print("再见！")
                break
            else:
                print("无效选项")


async def main():
    """主函数"""
    skill = AdAccountSkill()
    try:
        await skill.run()
    except KeyboardInterrupt:
        await skill.close()
        print("\n再见！")


if __name__ == "__main__":
    asyncio.run(main())


path: README.md
content: # 广告账户管理 Skill

通过浏览器自动化（Cookie登录），实现多平台广告账户的统一管理。

## 功能特性

1. **扫码登录**：支持广点通（广告主/服务商）和巨量引擎的扫码登录
2. **Cookie保存**：登录后自动保存Cookie到本地，下次可直接使用
3. **一键登录**：使用已存储的Cookie自动登录对应广告后台
4. **账户状态总览**：查看所有账户的余额、消耗、预算等信息，异常账户标红提醒

## 安装依赖

```bash
pip install -r requirements.txt
playwright install chromium
```

## 使用方法

运行交互式命令行界面：

```bash
python main.py
```

## 目录结构

```
ad_accounts/
├── __init__.py          # 模块初始化
├── ad_manager.py        # 账户管理核心模块
├── browser_helper.py    # 浏览器自动化辅助模块
├── main.py              # 主入口，交互式命令行界面
├── requirements.txt     # 依赖包列表
├── README.md            # 使用说明
└── data/                # 数据存储目录（自动创建）
    └── accounts.json    # 账户数据文件
```

## 支持的平台

- **广点通**：广告主后台 (`https://e.qq.com/ads`)、服务商后台 (`https://e.qq.com/agency`)
- **巨量引擎**：`https://ad.oceanengine.com`


path: requirements.txt
content: # 广告账户管理 Skill 依赖
playwright>=1.40.0


path: test.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
广告账户管理 Skill 测试脚本
测试核心功能模块
"""

import sys
import os

# 添加当前目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from ad_manager import AdAccountManager, Platform, AccountType


def test_ad_manager():
    """测试账户管理器"""
    print("=" * 50)
    print("测试 AdAccountManager")
    print("=" * 50)
    
    # 初始化管理器
    manager = AdAccountManager()
    print("[OK] 管理器初始化成功")
    
    # 测试获取登录URL
    print("\n--- 测试获取登录URL ---")
    gdt_ad_url = manager.get_platform_login_url(Platform.GUANGDIANTONG, AccountType.ADVERTISER)
    print(f"广点通广告主URL: {gdt_ad_url}")
    
    gdt_agency_url = manager.get_platform_login_url(Platform.GUANGDIANTONG, AccountType.AGENCY)
    print(f"广点通服务商URL: {gdt_agency_url}")
    
    juliang_url = manager.get_platform_login_url(Platform.JULIANG)
    print(f"巨量引擎URL: {juliang_url}")
    
    # 测试列出账户
    print("\n--- 测试列出账户 ---")
    accounts = manager.list_accounts()
    print(f"当前账户数量: {len(accounts)}")
    
    # 测试账户总览
    print("\n--- 测试账户总览 ---")
    overview = manager.get_accounts_overview()
    print(f"总览数据: {overview}")
    
    print("\n" + "=" * 50)
    print("测试完成！")
    print("=" * 50)


def test_imports():
    """测试模块导入"""
    print("=" * 50)
    print("测试模块导入")
    print("=" * 50)
    
    try:
        import ad_manager
        print("[OK] ad_manager 导入成功")
    except Exception as e:
        print(f"[FAIL] ad_manager 导入失败: {e}")
        return False
    
    try:
        import browser_helper
        print("[OK] browser_helper 导入成功")
    except Exception as e:
        print(f"[FAIL] browser_helper 导入失败: {e}")
        return False
    
    print("\n[OK] 所有模块导入成功！")
    return True


if __name__ == "__main__":
    print("\n广告账户管理 Skill 测试\n")
    
    # 先测试导入
    if test_imports():
        # 再测试管理器
        test_ad_manager()
        
        print("\n提示: 运行 'python main.py' 可以启动交互式界面")
        print("提示: 在OpenClaw中说'管理广告账户'可以触发Skill")


path: __init__.py
content: # 广告账户管理模块
from .ad_manager import AdAccountManager

__all__ = ['AdAccountManager']


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)