zhangliang 13 часов назад
Родитель
Сommit
7ca9641b97

+ 3 - 3
examples/content_finder/tools/__init__.py

@@ -4,11 +4,11 @@
 
 
 from .douyin_search import douyin_search
 from .douyin_search import douyin_search
 from .douyin_user_videos import douyin_user_videos
 from .douyin_user_videos import douyin_user_videos
-from .hotspot_profile import get_video_audience_profile, get_user_fans_profile
+from .hotspot_profile import get_content_fans_portrait, get_account_fans_portrait
 
 
 __all__ = [
 __all__ = [
     "douyin_search",
     "douyin_search",
     "douyin_user_videos",
     "douyin_user_videos",
-    "get_video_audience_profile",
-    "get_user_fans_profile",
+    "get_content_fans_portrait",
+    "get_account_fans_portrait",
 ]
 ]

+ 80 - 151
examples/content_finder/tools/douyin_search.py

@@ -1,165 +1,94 @@
 """
 """
-抖音搜索工具
+抖音关键词搜索工具(示例)
+
+调用内部爬虫服务进行抖音关键词搜索。
 """
 """
+import asyncio
+import json
+from datetime import datetime
+from typing import Optional, Any, Dict, List
+
+import httpx
+
+from agent.tools import tool, ToolResult
 
 
-from typing import List, Dict, Any, Optional
-from datetime import datetime, timedelta
-from agent.tools import tool, ToolResult, ToolContext
+
+# API 基础配置
+DOUYIN_SEARCH_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/keyword"
+DEFAULT_TIMEOUT = 60.0
 
 
 
 
 @tool(description="通过关键词搜索抖音视频内容")
 @tool(description="通过关键词搜索抖音视频内容")
 async def douyin_search(
 async def douyin_search(
-    keywords: str,
-    max_results: int = 20,
-    min_likes: Optional[int] = None,
-    ctx: Optional[ToolContext] = None,
+    keyword: str,
+    content_type: str = "视频",
+    sort_type: str = "综合排序",
+    publish_time: str = "不限",
+    cursor: str = "0",
+    account_id: str = "",
+    timeout: Optional[float] = None,
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
-    通过关键词搜索抖音视频内容
-
-    ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
-    实际使用时需要对接真实的抖音API
+    抖音关键词搜索
 
 
     Args:
     Args:
-        keywords: 搜索关键词
-        max_results: 最大结果数,默认20
-        min_likes: 最小点赞数筛选
-        ctx: 工具上下文
+        keyword: 搜索关键词
+        content_type: 内容类型(默认 "视频")
+        sort_type: 排序方式(默认 "综合排序")
+        publish_time: 发布时间范围(默认 "不限")
+        cursor: 分页游标(默认 "0")
+        account_id: 账号ID(可选)
+        timeout: 超时时间(秒),默认 60
 
 
     Returns:
     Returns:
-        ToolResult: 包含搜索到的视频列表(模拟数据)
+        ToolResult: 搜索结果 JSON
     """
     """
-    # TODO: 实际实现需要调用抖音API或爬虫服务
-    # 这里返回模拟数据
-
-    results = await _mock_douyin_search(keywords, max_results, min_likes)
-
-    summary = f"⚠️ [模拟数据] 搜索关键词「{keywords}」,找到 {len(results)} 条视频"
-    if min_likes:
-        summary += f"(点赞数 ≥ {min_likes})"
-    summary += "\n\n⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实抖音API。"
-
-    # 构建详细输出
-    output_lines = [summary, ""]
-    for i, item in enumerate(results[:5], 1):  # 只显示前5条
-        days_ago = item.get('days_since_publish', 0)
-        time_desc = f"{days_ago}天前" if days_ago > 0 else "今天"
-
-        output_lines.append(f"{i}. {item['title']}")
-        output_lines.append(f"   作者: {item['author_name']} (ID: {item['author_id']})")
-        output_lines.append(f"   发布: {time_desc}")
-        output_lines.append(f"   数据: 点赞 {item['like_count']:,} | 评论 {item['comment_count']:,} | 分享 {item['share_count']:,}")
-
-        # 计算互动比例
-        like_count = item['like_count']
-        comment_ratio = (item['comment_count'] / like_count * 100) if like_count > 0 else 0
-        share_ratio = (item['share_count'] / like_count * 100) if like_count > 0 else 0
-        output_lines.append(f"   互动: 评论率 {comment_ratio:.1f}% | 分享率 {share_ratio:.1f}%")
-        output_lines.append("")
-
-    if len(results) > 5:
-        output_lines.append(f"... 还有 {len(results) - 5} 条结果")
-
-    return ToolResult(
-        title=f"[模拟数据] 抖音搜索: {keywords}",
-        output="\n".join(output_lines),
-        metadata={
-            "total": len(results),
-            "items": results,
-            "is_mock_data": True,
-            "warning": "这是模拟数据,实际使用需对接真实API",
-        },
-    )
-
-
-async def _mock_douyin_search(
-    keywords: str,
-    max_results: int,
-    min_likes: Optional[int],
-) -> List[Dict[str, Any]]:
-    """模拟抖音搜索(实际应调用真实API)"""
-
-    now = datetime.now()
-
-    # 模拟数据(包含不同时效性的内容)
-    mock_data = [
-        {
-            "video_id": "7234567890123456789",
-            "title": f"感人至深!{keywords}相关视频1",
-            "author_id": "user_001",
-            "author_name": "感动中国",
-            "like_count": 8000,
-            "comment_count": 1200,
-            "share_count": 500,
-            "collect_count": 1500,
-            "create_time": (now - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S"),
-            "days_since_publish": 3,
-            "video_url": "https://www.douyin.com/video/7234567890123456789",
-            "cover_url": "https://example.com/cover1.jpg",
-        },
-        {
-            "video_id": "7234567890123456790",
-            "title": f"{keywords}经典片段合集",
-            "author_id": "user_002",
-            "author_name": "历史回忆",
-            "like_count": 15000,
-            "comment_count": 2300,
-            "share_count": 1200,
-            "collect_count": 3000,
-            "create_time": (now - timedelta(days=15)).strftime("%Y-%m-%d %H:%M:%S"),
-            "days_since_publish": 15,
-            "video_url": "https://www.douyin.com/video/7234567890123456790",
-            "cover_url": "https://example.com/cover2.jpg",
-        },
-        {
-            "video_id": "7234567890123456791",
-            "title": f"真实{keywords}故事",
-            "author_id": "user_001",
-            "author_name": "感动中国",
-            "like_count": 5200,
-            "comment_count": 800,
-            "share_count": 350,
-            "collect_count": 1000,
-            "create_time": (now - timedelta(days=45)).strftime("%Y-%m-%d %H:%M:%S"),
-            "days_since_publish": 45,
-            "video_url": "https://www.douyin.com/video/7234567890123456791",
-            "cover_url": "https://example.com/cover3.jpg",
-        },
-        {
-            "video_id": "7234567890123456792",
-            "title": f"最新{keywords}短片",
-            "author_id": "user_003",
-            "author_name": "时代记忆",
-            "like_count": 3500,
-            "comment_count": 450,
-            "share_count": 200,
-            "collect_count": 800,
-            "create_time": (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"),
-            "days_since_publish": 1,
-            "video_url": "https://www.douyin.com/video/7234567890123456792",
-            "cover_url": "https://example.com/cover4.jpg",
-        },
-        {
-            "video_id": "7234567890123456793",
-            "title": f"{keywords}珍贵影像",
-            "author_id": "user_002",
-            "author_name": "历史回忆",
-            "like_count": 2800,
-            "comment_count": 350,
-            "share_count": 150,
-            "collect_count": 600,
-            "create_time": (now - timedelta(days=120)).strftime("%Y-%m-%d %H:%M:%S"),
-            "days_since_publish": 120,
-            "video_url": "https://www.douyin.com/video/7234567890123456793",
-            "cover_url": "https://example.com/cover5.jpg",
-        },
-    ]
-
-    # 应用筛选条件
-    filtered = []
-    for item in mock_data:
-        if min_likes and item["like_count"] < min_likes:
-            continue
-        filtered.append(item)
-
-    return filtered[:max_results]
+    try:
+        payload = {
+            "keyword": keyword,
+            "content_type": content_type,
+            "sort_type": sort_type,
+            "publish_time": publish_time,
+            "cursor": cursor,
+            "account_id": account_id
+        }
+
+        request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
+
+        async with httpx.AsyncClient(timeout=request_timeout) as client:
+            response = await client.post(
+                DOUYIN_SEARCH_API,
+                json=payload,
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
+        return ToolResult(
+            title=f"抖音搜索: {keyword}",
+            output=json.dumps(data, ensure_ascii=False, indent=2),
+            long_term_memory=f"Searched Douyin for '{keyword}'"
+        )
+    except httpx.HTTPStatusError as e:
+        return ToolResult(
+            title="抖音搜索失败",
+            output="",
+            error=f"HTTP error {e.response.status_code}: {e.response.text}"
+        )
+    except Exception as e:
+        return ToolResult(
+            title="抖音搜索失败",
+            output="",
+            error=str(e)
+        )
+
+
+#
+# async def main():
+#     result = await douyin_search(
+#         keyword="宁艺卓",
+#         account_id = "771431186"
+#     )
+#     print(result.output)
+#
+# if __name__ == "__main__":
+#     asyncio.run(main())

+ 67 - 201
examples/content_finder/tools/douyin_user_videos.py

@@ -1,217 +1,83 @@
 """
 """
-抖音用户作品列表工具
+抖音账号历史作品工具(示例)
+
+调用内部爬虫服务获取指定账号的历史作品列表。
 """
 """
+import json
+from typing import Optional
+
+import asyncio
+import httpx
+
+from agent.tools import tool, ToolResult
+
 
 
-from typing import List, Dict, Any, Optional
-from datetime import datetime, timedelta
-from agent.tools import tool, ToolResult, ToolContext
+# API 基础配置
+DOUYIN_BLOGGER_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/blogger"
+DEFAULT_TIMEOUT = 60.0
 
 
 
 
-@tool(description="获取抖音用户的作品列表")
+@tool(description="根据账号ID获取抖音历史作品,支持排序与游标")
 async def douyin_user_videos(
 async def douyin_user_videos(
-    author_id: str,
-    max_results: int = 20,
-    ctx: Optional[ToolContext] = None,
+    account_id: str,
+    sort_type: str = "最新",
+    cursor: str = "",
+    timeout: Optional[float] = None,
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
-    根据抖音用户ID获取该用户的作品列表
-
-    ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
-    实际使用时需要对接真实的抖音API
+    抖音账号历史作品查询
 
 
     Args:
     Args:
-        author_id: 抖音用户ID
-        max_results: 最大返回数量,默认20
-        ctx: 工具上下文
+        account_id: 抖音账号ID(account_id)
+        sort_type: 排序方式(默认 "最新")
+        cursor: 分页游标(默认 "")
+        timeout: 超时时间(秒),默认 60
 
 
     Returns:
     Returns:
-        ToolResult: 包含用户作品列表(模拟数据)
+        ToolResult: 作品列表 JSON
     """
     """
-    # TODO: 实际实现需要调用抖音API或爬虫服务
-    # 这里返回模拟数据
-
-    results = await _mock_user_videos(author_id, max_results)
+    try:
+        payload = {
+            "account_id": account_id,
+            "sort_type": sort_type,
+            "cursor": cursor,
+        }
+
+        request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
+
+        async with httpx.AsyncClient(timeout=request_timeout) as client:
+            response = await client.post(
+                DOUYIN_BLOGGER_API,
+                json=payload,
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
 
 
-    if not results:
         return ToolResult(
         return ToolResult(
-            title=f"[模拟数据] 用户作品: {author_id}",
-            output=f"⚠️ 未找到用户 {author_id} 的作品(模拟数据)",
-            metadata={"total": 0, "items": [], "is_mock_data": True},
+            title=f"账号作品: {account_id}",
+            output=json.dumps(data, ensure_ascii=False, indent=2),
+            long_term_memory=f"Fetched Douyin blogger videos for '{account_id}'",
         )
         )
-
-    author_name = results[0]["author_name"]
-    summary = f"⚠️ [模拟数据] 用户「{author_name}」(ID: {author_id}) 共有 {len(results)} 个作品"
-
-    # 统计时效性
-    recent_count = sum(1 for v in results if v.get('days_since_publish', 999) < 30)
-    if recent_count > 0:
-        summary += f",其中 {recent_count} 个是近30天内发布"
-
-    summary += "\n\n⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实抖音API。"
-
-    # 构建详细输出
-    output_lines = [summary, ""]
-    for i, item in enumerate(results[:5], 1):  # 只显示前5条
-        days_ago = item.get('days_since_publish', 0)
-        time_desc = f"{days_ago}天前" if days_ago > 0 else "今天"
-
-        output_lines.append(f"{i}. {item['title']}")
-        output_lines.append(f"   发布: {time_desc}")
-        output_lines.append(f"   数据: 点赞 {item['like_count']:,} | 评论 {item['comment_count']:,} | 分享 {item['share_count']:,}")
-
-        # 计算互动比例
-        like_count = item['like_count']
-        comment_ratio = (item['comment_count'] / like_count * 100) if like_count > 0 else 0
-        share_ratio = (item['share_count'] / like_count * 100) if like_count > 0 else 0
-        output_lines.append(f"   互动: 评论率 {comment_ratio:.1f}% | 分享率 {share_ratio:.1f}%")
-        output_lines.append("")
-
-    if len(results) > 5:
-        output_lines.append(f"... 还有 {len(results) - 5} 个作品")
-
-    return ToolResult(
-        title=f"[模拟数据] 用户作品: {author_name}",
-        output="\n".join(output_lines),
-        metadata={
-            "author_id": author_id,
-            "author_name": author_name,
-            "total": len(results),
-            "recent_count": recent_count,
-            "items": results,
-            "is_mock_data": True,
-            "warning": "这是模拟数据,实际使用需对接真实API",
-        },
-    )
-
-
-async def _mock_user_videos(
-    author_id: str,
-    max_results: int,
-) -> List[Dict[str, Any]]:
-    """模拟获取用户作品(实际应调用真实API)"""
-
-    now = datetime.now()
-
-    # 模拟不同用户的数据(包含时效性信息)
-    user_data = {
-        "user_001": {
-            "author_name": "感动中国",
-            "videos": [
-                {
-                    "video_id": "7234567890123456789",
-                    "title": "感人至深!孩子军抗日故事",
-                    "like_count": 8000,
-                    "comment_count": 1200,
-                    "share_count": 500,
-                    "collect_count": 1500,
-                    "create_time": (now - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 3,
-                },
-                {
-                    "video_id": "7234567890123456791",
-                    "title": "真实抗战故事",
-                    "like_count": 5200,
-                    "comment_count": 800,
-                    "share_count": 350,
-                    "collect_count": 1000,
-                    "create_time": (now - timedelta(days=45)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 45,
-                },
-                {
-                    "video_id": "7234567890123456792",
-                    "title": "历史不能忘记",
-                    "like_count": 6500,
-                    "comment_count": 950,
-                    "share_count": 420,
-                    "collect_count": 1200,
-                    "create_time": (now - timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 20,
-                },
-                {
-                    "video_id": "7234567890123456794",
-                    "title": "铭记历史,珍爱和平",
-                    "like_count": 4200,
-                    "comment_count": 600,
-                    "share_count": 280,
-                    "collect_count": 800,
-                    "create_time": (now - timedelta(days=8)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 8,
-                },
-            ],
-        },
-        "user_002": {
-            "author_name": "历史回忆",
-            "videos": [
-                {
-                    "video_id": "7234567890123456790",
-                    "title": "抗日经典片段合集",
-                    "like_count": 15000,
-                    "comment_count": 2300,
-                    "share_count": 1200,
-                    "collect_count": 3000,
-                    "create_time": (now - timedelta(days=15)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 15,
-                },
-                {
-                    "video_id": "7234567890123456793",
-                    "title": "那些年的英雄",
-                    "like_count": 9500,
-                    "comment_count": 1500,
-                    "share_count": 800,
-                    "collect_count": 2000,
-                    "create_time": (now - timedelta(days=25)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 25,
-                },
-                {
-                    "video_id": "7234567890123456795",
-                    "title": "珍贵历史影像资料",
-                    "like_count": 12000,
-                    "comment_count": 1800,
-                    "share_count": 950,
-                    "collect_count": 2500,
-                    "create_time": (now - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 5,
-                },
-            ],
-        },
-        "user_003": {
-            "author_name": "时代记忆",
-            "videos": [
-                {
-                    "video_id": "7234567890123456796",
-                    "title": "最新抗战短片",
-                    "like_count": 3500,
-                    "comment_count": 450,
-                    "share_count": 200,
-                    "collect_count": 800,
-                    "create_time": (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 1,
-                },
-                {
-                    "video_id": "7234567890123456797",
-                    "title": "感动瞬间合集",
-                    "like_count": 5800,
-                    "comment_count": 720,
-                    "share_count": 380,
-                    "collect_count": 1100,
-                    "create_time": (now - timedelta(days=12)).strftime("%Y-%m-%d %H:%M:%S"),
-                    "days_since_publish": 12,
-                },
-            ],
-        },
-    }
-
-    user_info = user_data.get(author_id)
-    if not user_info:
-        return []
-
-    # 为每个视频添加作者信息
-    results = []
-    for video in user_info["videos"][:max_results]:
-        video_copy = video.copy()
-        video_copy["author_id"] = author_id
-        video_copy["author_name"] = user_info["author_name"]
-        video_copy["video_url"] = f"https://www.douyin.com/video/{video['video_id']}"
-        results.append(video_copy)
-
-    return results
+    except httpx.HTTPStatusError as e:
+        return ToolResult(
+            title="账号作品获取失败",
+            output="",
+            error=f"HTTP error {e.response.status_code}: {e.response.text}",
+        )
+    except Exception as e:
+        return ToolResult(
+            title="账号作品获取失败",
+            output="",
+            error=str(e),
+        )
+# async def main():
+#     result = await douyin_user_videos(
+#         account_id="MS4wLjABAAAAPRCMGPAFM1VGcJrxRuvTXgJp0Sk95EW1DynNmbKSPg8",
+#         sort_type="最新",
+#         cursor=""
+#     )
+#     print(result.output)
+#
+# if __name__ == "__main__":
+#     asyncio.run(main())

+ 132 - 203
examples/content_finder/tools/hotspot_profile.py

@@ -1,215 +1,144 @@
 """
 """
-热点宝画像数据工具
+热点宝画像数据工具(示例)
+
+调用内部爬虫服务获取账号/内容的粉丝画像。
 """
 """
+import asyncio
+import json
+from typing import Optional
 
 
-from typing import Dict, Any, Optional, List
-from agent.tools import tool, ToolResult, ToolContext
+import httpx
 
 
+from agent.tools import tool, ToolResult
 
 
-@tool(description="获取抖音视频的点赞观众画像数据")
-async def get_video_audience_profile(
-    video_id: str,
-    ctx: Optional[ToolContext] = None,
-) -> ToolResult:
-    """
-    获取抖音视频的点赞观众画像数据
 
 
-    ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
-    实际使用时需要对接真实的热点宝API
+ACCOUNT_FANS_PORTRAIT_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/re_dian_bao/account_fans_portrait"
+CONTENT_FANS_PORTRAIT_API = "http://crawapi.piaoquantv.com/crawler/dou_yin/re_dian_bao/video_like_portrait"
+DEFAULT_TIMEOUT = 60.0
 
 
-    包括:
-    - 观众地域分布(占比&偏好度)
-    - 观众城市等级(占比&偏好度)
-    - 观众性别分布(占比&偏好度)
-    - 观众年龄分布(占比&偏好度)
 
 
-    Args:
-        video_id: 抖音视频ID
-        ctx: 工具上下文
-
-    Returns:
-        ToolResult: 包含观众画像数据(模拟数据)
+@tool(description="获取抖音账号粉丝画像(热点宝),支持选择画像维度")
+async def get_account_fans_portrait(
+    account_id: str,
+    need_province: bool = False,
+    need_city: bool = False,
+    need_city_level: bool = False,
+    need_gender: bool = False,
+    need_age: bool = True,
+    need_phone_brand: bool = False,
+    need_phone_price: bool = False,
+    timeout: Optional[float] = None,
+) -> ToolResult:
     """
     """
-    # TODO: 实际实现需要调用热点宝API
-    # 这里返回模拟数据
-
-    profile = await _mock_video_audience_profile(video_id)
-
-    # 构建输出
-    output_lines = [
-        f"⚠️ [模拟数据] 视频 {video_id} 的点赞观众画像:",
-        "",
-        "⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实热点宝API。",
-        "",
-        "【年龄分布】",
-    ]
-
-    for age_range, data in profile["age_distribution"].items():
-        output_lines.append(f"  {age_range}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【性别分布】",
-    ])
-    for gender, data in profile["gender_distribution"].items():
-        output_lines.append(f"  {gender}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【城市等级】",
-    ])
-    for city_level, data in profile["city_level_distribution"].items():
-        output_lines.append(f"  {city_level}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【地域分布 TOP5】",
-    ])
-    for i, (region, data) in enumerate(list(profile["region_distribution"].items())[:5], 1):
-        output_lines.append(f"  {i}. {region}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    return ToolResult(
-        title=f"[模拟数据] 视频观众画像: {video_id}",
-        output="\n".join(output_lines),
-        metadata={**profile, "is_mock_data": True, "warning": "这是模拟数据,实际使用需对接真实API"},
-    )
-
-
-@tool(description="获取抖音用户的粉丝画像数据")
-async def get_user_fans_profile(
-    author_id: str,
-    ctx: Optional[ToolContext] = None,
+    获取抖音账号粉丝画像
+    """
+    try:
+        payload = {
+            "account_id": account_id,
+            "need_province": need_province,
+            "need_city": need_city,
+            "need_city_level": need_city_level,
+            "need_gender": need_gender,
+            "need_age": need_age,
+            "need_phone_brand": need_phone_brand,
+            "need_phone_price": need_phone_price,
+        }
+
+        request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
+
+        async with httpx.AsyncClient(timeout=request_timeout) as client:
+            response = await client.post(
+                ACCOUNT_FANS_PORTRAIT_API,
+                json=payload,
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        return ToolResult(
+            title=f"账号粉丝画像: {account_id}",
+            output=json.dumps(data, ensure_ascii=False, indent=2),
+            long_term_memory=f"Fetched fans portrait for account '{account_id}'",
+        )
+    except httpx.HTTPStatusError as e:
+        return ToolResult(
+            title="账号粉丝画像获取失败",
+            output="",
+            error=f"HTTP error {e.response.status_code}: {e.response.text}",
+        )
+    except Exception as e:
+        return ToolResult(
+            title="账号粉丝画像获取失败",
+            output="",
+            error=str(e),
+        )
+
+
+@tool(description="获取抖音内容粉丝画像(热点宝),支持选择画像维度")
+async def get_content_fans_portrait(
+    content_id: str,
+    need_province: bool = False,
+    need_city: bool = False,
+    need_city_level: bool = False,
+    need_gender: bool = False,
+    need_age: bool = True,
+    need_phone_brand: bool = False,
+    need_phone_price: bool = False,
+    timeout: Optional[float] = None,
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
-    获取抖音用户的粉丝画像数据
-
-    ⚠️ 注意:当前返回的是模拟数据,仅用于测试和演示
-    实际使用时需要对接真实的热点宝API
-
-    包括:
-    - 粉丝地域分布(占比&偏好度)
-    - 粉丝城市等级(占比&偏好度)
-    - 粉丝性别分布(占比&偏好度)
-    - 粉丝年龄分布(占比&偏好度)
-
-    Args:
-        author_id: 抖音用户ID
-        ctx: 工具上下文
-
-    Returns:
-        ToolResult: 包含粉丝画像数据(模拟数据)
+    获取抖音内容粉丝画像
     """
     """
-    # TODO: 实际实现需要调用热点宝API
-    # 这里返回模拟数据
-
-    profile = await _mock_user_fans_profile(author_id)
-
-    # 构建输出
-    output_lines = [
-        f"⚠️ [模拟数据] 用户 {author_id} 的粉丝画像:",
-        "",
-        "⚠️ 注意:以下是模拟数据,仅用于测试。实际使用需对接真实热点宝API。",
-        "",
-        f"粉丝总数: {profile['total_fans']:,}",
-        "",
-        "【年龄分布】",
-    ]
-
-    for age_range, data in profile["age_distribution"].items():
-        output_lines.append(f"  {age_range}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【性别分布】",
-    ])
-    for gender, data in profile["gender_distribution"].items():
-        output_lines.append(f"  {gender}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【城市等级】",
-    ])
-    for city_level, data in profile["city_level_distribution"].items():
-        output_lines.append(f"  {city_level}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    output_lines.extend([
-        "",
-        "【地域分布 TOP5】",
-    ])
-    for i, (region, data) in enumerate(list(profile["region_distribution"].items())[:5], 1):
-        output_lines.append(f"  {i}. {region}: {data['percentage']:.1f}% (偏好度: {data['preference']:.2f})")
-
-    return ToolResult(
-        title=f"[模拟数据] 用户粉丝画像: {author_id}",
-        output="\n".join(output_lines),
-        metadata={**profile, "is_mock_data": True, "warning": "这是模拟数据,实际使用需对接真实API"},
-    )
-
-
-async def _mock_video_audience_profile(video_id: str) -> Dict[str, Any]:
-    """模拟视频观众画像数据"""
-    return {
-        "video_id": video_id,
-        "age_distribution": {
-            "18-24岁": {"percentage": 15.2, "preference": 0.85},
-            "25-30岁": {"percentage": 22.5, "preference": 1.05},
-            "31-40岁": {"percentage": 28.3, "preference": 1.15},
-            "41-50岁": {"percentage": 20.8, "preference": 1.25},
-            "51-60岁": {"percentage": 10.5, "preference": 1.45},
-            "60岁以上": {"percentage": 2.7, "preference": 1.65},
-        },
-        "gender_distribution": {
-            "男性": {"percentage": 58.5, "preference": 1.12},
-            "女性": {"percentage": 41.5, "preference": 0.88},
-        },
-        "city_level_distribution": {
-            "一线城市": {"percentage": 18.5, "preference": 0.92},
-            "新一线城市": {"percentage": 25.3, "preference": 1.05},
-            "二线城市": {"percentage": 28.7, "preference": 1.15},
-            "三线城市": {"percentage": 18.2, "preference": 1.08},
-            "四线及以下": {"percentage": 9.3, "preference": 0.95},
-        },
-        "region_distribution": {
-            "广东": {"percentage": 12.5, "preference": 1.08},
-            "江苏": {"percentage": 9.8, "preference": 1.12},
-            "山东": {"percentage": 8.5, "preference": 1.05},
-            "浙江": {"percentage": 7.2, "preference": 1.15},
-            "河南": {"percentage": 6.8, "preference": 0.98},
-            "四川": {"percentage": 5.5, "preference": 1.02},
-        },
-    }
-
-
-async def _mock_user_fans_profile(author_id: str) -> Dict[str, Any]:
-    """模拟用户粉丝画像数据"""
-    return {
-        "author_id": author_id,
-        "total_fans": 125000,
-        "age_distribution": {
-            "18-24岁": {"percentage": 12.5, "preference": 0.78},
-            "25-30岁": {"percentage": 18.8, "preference": 0.95},
-            "31-40岁": {"percentage": 25.5, "preference": 1.08},
-            "41-50岁": {"percentage": 24.2, "preference": 1.28},
-            "51-60岁": {"percentage": 14.5, "preference": 1.52},
-            "60岁以上": {"percentage": 4.5, "preference": 1.85},
-        },
-        "gender_distribution": {
-            "男性": {"percentage": 62.5, "preference": 1.18},
-            "女性": {"percentage": 37.5, "preference": 0.82},
-        },
-        "city_level_distribution": {
-            "一线城市": {"percentage": 15.2, "preference": 0.88},
-            "新一线城市": {"percentage": 22.8, "preference": 1.02},
-            "二线城市": {"percentage": 30.5, "preference": 1.18},
-            "三线城市": {"percentage": 20.5, "preference": 1.12},
-            "四线及以下": {"percentage": 11.0, "preference": 1.05},
-        },
-        "region_distribution": {
-            "广东": {"percentage": 11.8, "preference": 1.05},
-            "江苏": {"percentage": 10.2, "preference": 1.15},
-            "山东": {"percentage": 9.5, "preference": 1.08},
-            "浙江": {"percentage": 7.8, "preference": 1.18},
-            "河南": {"percentage": 7.2, "preference": 1.02},
-            "四川": {"percentage": 6.5, "preference": 1.08},
-        },
-    }
+    try:
+        payload = {
+            "content_id": content_id,
+            "need_province": need_province,
+            "need_city": need_city,
+            "need_city_level": need_city_level,
+            "need_gender": need_gender,
+            "need_age": need_age,
+            "need_phone_brand": need_phone_brand,
+            "need_phone_price": need_phone_price,
+        }
+
+        request_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
+
+        async with httpx.AsyncClient(timeout=request_timeout) as client:
+            response = await client.post(
+                CONTENT_FANS_PORTRAIT_API,
+                json=payload,
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        return ToolResult(
+            title=f"内容粉丝画像: {content_id}",
+            output=json.dumps(data, ensure_ascii=False, indent=2),
+            long_term_memory=f"Fetched fans portrait for content '{content_id}'",
+        )
+    except httpx.HTTPStatusError as e:
+        return ToolResult(
+            title="内容粉丝画像获取失败",
+            output="",
+            error=f"HTTP error {e.response.status_code}: {e.response.text}",
+        )
+    except Exception as e:
+        return ToolResult(
+            title="内容粉丝画像获取失败",
+            output="",
+            error=str(e),
+        )
+
+# async def main():
+#     # result = await get_account_fans_portrait(
+#     #     account_id="MS4wLjABAAAAXvRdWJsdPKkh9Ja3ZirxoB8pAaxNXUXs1KUe14gW0IoqDz-D-fG0xZ8c5kSfTPXx"
+#     # )
+#     # print(result.output)
+#     result = await get_content_fans_portrait(
+#         content_id="7495776724350405928"
+#     )
+#     print(result.output)
+#
+# if __name__ == "__main__":
+#     asyncio.run(main())