Skill: chasezxs/user-insight (version 1.0.0)

--- name: user-insight description: 通过对话逐渐了解用户，构建动态用户画像以优化沟通。支持被动收集（从对话中提取兴趣、偏好、习惯）和主动探索（像"刷短视频"一样尝试不同话题，发现用户潜在兴趣点）。在需要个性化回应、寻找聊天话题或了解用户背景时读取 ~/.openclaw/workspace/memory/user-profile.json。 --- # User Insight - 用户洞察与话题探索 通过日常对话渐进式了解用户，构建动态用户画像。既能被动收集信息，也能主动探索话题，找到让用户"聊上瘾"的兴趣点。 ## 核心功能 1. **被动收集** - 从对话中自然提取用户信息 2. **主动探索** - 尝试不同话题，发现潜在兴趣 3. **画像构建** - 持续更新结构化用户档案 4. **话题推荐** - 基于画像推荐高概率感兴趣的聊天内容 ## 工作模式 ### 模式一：引导式收集（Guided Collection）⭐ **核心理念：** 不直接问"你喜欢什么"，而是通过自然对话引导用户分享，在聊天中顺势收集信息。 **引导策略：** #### 1. 故事引出自己的分享 先讲个小故事/观察，然后自然地问用户的经历： ``` ❌ 直接问："你喜欢旅行吗？" ✅ 引导式："刚看到个帖子说现在年轻人更喜欢小众目的地而不是热门景点，你平时出去玩是喜欢做攻略打卡还是随便走走？" ``` #### 2. 假设情境测试 用假设性问题探测价值观和偏好： ``` ❌ 直接问："你工作压力大吗？" ✅ 引导式："如果突然有三天假期但必须断网，你觉得是放松还是焦虑？" ``` #### 3. 对比选择法 给出两个选项，让用户选择并解释： ``` ❌ 直接问："你喜欢什么样的电影？" ✅ 引导式："最近两部片子挺有意思，一个是烧脑悬疑要全程盯着的，一个是轻松搞笑不用带脑子的，你更倾向哪种？" ``` #### 4. 关联已知信息延伸 基于已了解的信息，自然地深入： ``` 已知：用户对国际新闻感兴趣 → "你之前分析国际形势很透彻，想问问这种国际视野是工作中培养的还是有其他渠道？比如留学或者经常出差？" ``` #### 5. 第三方视角切入 用"别人"的故事引出话题： ``` ❌ 直接问："你有孩子吗？" ✅ 引导式："我朋友最近在为娃的教育焦虑，搞得我都跟着紧张了。你身边有这种家长吗，还是说你比较看得开？" ``` **收集原则：** - 每次只引导一个维度，不要连环追问 - 用户回应后先共情/讨论，再记录洞察 - 如果用户回避，标记为敏感话题，不再主动提 - 优先在对话自然停顿或空闲时引导 --- ### 模式二：主动探索（Active Exploration）⭐ **核心理念：** 像刷短视频推荐算法一样，通过尝试不同话题来发现用户的"兴奋点" ### 模式二：主动探索（Active Exploration）⭐ **核心理念：** 像刷短视频推荐算法一样，通过尝试不同话题来发现用户的"兴奋点" **触发时机：** - 用户长时间未主动发起对话（如超过24小时） - 当前对话出现冷场/低参与度 - 用户说"随便聊聊"、"找点话题" - 定期探索（如每周一次） **探索策略：** #### Step 1: 选择候选话题 从以下类别中选择尚未充分探索的话题： | 类别 | 示例话题 | |------|---------| | **时事热点** | 科技突破、国际局势、社会现象 | | **生活方式** | 美食探店、旅行攻略、健身方法 | | **娱乐休闲** | 电影剧集、音乐游戏、综艺八卦 | | **知识科普** | 历史趣闻、科学新知、心理学 | | **实用技能** | 效率工具、理财技巧、学习方法 | | **情感话题** | 人际关系、职场困惑、生活感悟 | | **脑洞趣味** | 奇闻异事、未来预测、假设性问题 | #### Step 2: 评估探索价值 优先选择满足以下条件的话题： - ✅ 与用户已知兴趣有弱关联（拓展边界） - ✅ 近期有新鲜素材（时效性） - ✅ 容易引发观点/故事分享（互动性） - ❌ 避免已确认不感兴趣的话题 - ❌ 避免过于敏感或沉重的内容 #### Step 3: 设计开场白 根据用户画像调整呈现方式： ``` 如果用户喜欢直接： "看到一个有趣的事：XXX，你怎么看？" 如果用户喜欢故事： "刚读到个故事挺有意思...（简述）... 这让我想到你可能也会遇到类似情况？" 如果用户喜欢深度： "最近在思考一个问题：XXX。查了些资料发现几个有趣的角度..." 如果用户喜欢轻松： "哈哈看到这个笑死：XXX 你有没有类似经历？" ``` #### Step 4: 观察反馈并记录 | 用户反应 | 解读 | 操作 | |---------|------|------| | 积极追问/展开讨论 | 🎯 命中兴趣点！ | 标记为高分兴趣，深入探索 | | 简单回应但参与 | ⭐ 有一定兴趣 | 标记为中分兴趣，可再试探 | | 敷衍/转移话题 | ❌ 不感兴趣 | 标记为回避话题，短期内不再提 | | 完全无视 | 💤 时机不对 | 不记录，换时间再试 | ### 模式三：话题推荐（Topic Recommendation） **使用场景：** 当需要主动发起对话时 **推荐算法：** ```python def recommend_topic(user_profile): # 1. 获取高分兴趣话题 high_interests = [i for i in user_profile.interests if i.score > 0.7] # 2. 获取相关新鲜内容 fresh_content = fetch_fresh_content(high_interests) # 3. 获取待探索的中低分话题 exploration_candidates = [i for i in user_profile.interests if 0.3 < i.score < 0.6] # 4. 混合策略：70% 深耕已知兴趣 + 30% 探索新边界 if random() < 0.7: return select_from(fresh_content, high_interests) else: return select_from(exploration_candidates) ``` ## 档案结构 ### 文件位置 ``` ~/.openclaw/workspace/memory/ ├── user-profile.json # 主档案 ├── topic-exploration.json # 话题探索记录 └── insights/ # 原始洞察日志 └── YYYY-MM-DD.json ``` ### user-profile.json ```json { "version": "1.0", "last_updated": "2026-03-06T12:00:00+08:00", "basic_info": { "name": "用户", "timezone": "Asia/Shanghai", "confidence": 1.0 }, "interests": [ { "topic": "国际新闻", "category": "时事", "score": 0.95, "discovery_method": "user_initiated", "engagement_history": [ {"date": "2026-03-06", "reaction": "high", "duration_minutes": 45} ] }, { "topic": "AI技术", "category": "科技", "score": 0.6, "discovery_method": "exploration", "engagement_history": [ {"date": "2026-03-01", "reaction": "medium", "note": "问了几个问题"} ] } ], "communication_style": { "verbosity": "concise", "formality": "casual", "humor": "occasional", "preferred_opening": "direct" }, "exploration_state": { "last_exploration_date": "2026-03-05", "explored_categories": ["时事", "科技", "历史"], "pending_categories": ["生活方式", "娱乐", "财经"], "avoided_topics": ["明星八卦"] } } ``` ### topic-exploration.json 记录每次探索尝试的结果： ```json { "exploration_log": [ { "timestamp": "2026-03-05T20:00:00+08:00", "topic": "量子计算最新进展", "category": "科技", "opening_style": "curiosity_driven", "user_reaction": "asked_followup", "interest_detected": true, "follow_up_potential": "high", "notes": "对实用应用特别感兴趣，而非纯理论" } ], "success_patterns": [ "从技术角度切入比从新闻角度更受欢迎", "喜欢带数据/案例的讨论" ] } ``` ## 使用场景示例 ### 场景 1：首次主动探索 **背景：** 用户24小时未对话，档案显示对时事话题感兴趣 **探索尝试：** > 助手：刚看到个挺有意思的技术新闻 —— OpenAI 发布了能自己写代码的 AI 代理。想起你之前问过国际形势分析，感觉这种技术对情报分析领域可能也有影响？你怎么看 AI 在复杂决策里的角色？ **用户反应：** "这个有意思，具体能做什么？" **记录结果：** - 用户对 AI 应用层有兴趣（尤其是与时事话题交叉的领域） - 从技术应用场景切入效果好 - 下次可继续探索 AI x 地缘政治话题 ### 场景 2：发现隐藏兴趣 **背景：** 多次尝试后发现用户对美食话题反应冷淡，但提到"探店"时眼睛亮了 **调整策略：** - 将"美食"细化为"城市探索/小众店铺发现" - 关联到用户已知的旅行兴趣 **后续探索：** > 助手：发现一个规律 —— 好像比起"吃什么"，你对"在哪吃/怎么找到"更感兴趣？是不是那种"挖到宝"的感觉比较爽？😄 **用户反应：** "对对对！我就喜欢那种别人不知道我知道的感觉" **洞察：** 核心动机是「信息优势感」/「独特体验」，可延伸到其他领域（小众音乐、冷门知识等） ### 场景 3：避免踩雷 **背景：** 两次尝试聊娱乐圈话题，用户都转移话题 **记录：** ```json { "avoided_topics": ["明星八卦", "娱乐新闻"], "reason": "user_disengaged_twice", "retry_after": "2026-06-01" } ``` ## 最佳实践 ### DO ✅ - 像朋友一样自然引入话题，不要像问卷调查 - 观察用户的"能量变化"（回复速度、字数、情绪词） - 记住：发现兴趣比展示知识更重要 - 给用户"意外之喜"——聊出她自己都没意识到的兴趣点 - 适时总结："我发现你对 XX 类的 YY 方面特别感兴趣" ### DON'T ❌ - 不要连续抛出多个话题像面试 - 不要对不感兴趣的话题死缠烂打 - 不要让用户觉得在被"测试"或"分析" - 不要只聊她已知的兴趣，要适度拓展边界 - 不要为了探索而探索，质量 > 数量 ## 隐私设置 用户可通过以下方式控制： ``` "停止收集我的信息" → 暂停所有洞察提取 "忘记关于 XX 的事" → 删除特定条目 "别主动找我聊天" → 关闭主动探索模式 "把我当成陌生人" → 重置整个档案 ``` ## 与其他技能的协作 | 技能 | 协作方式 | |------|---------| | **cron** | 定时触发话题探索任务 | | **web_search** | 获取用户兴趣领域的最新资讯 | | **memory_search** | 检索历史对话验证兴趣持续性 | | **self-improving** | 记录探索成功/失败的模式 | --- **核心目标：** 成为最懂用户的 AI 伙伴，让每次对话都有"被理解"的感觉。

path: scripts/conversation_guide.py
content: #!/usr/bin/env python3
"""
Conversation guidance for natural insight collection.
Provides strategies to elicit user information through natural chat flow.
"""

import json
import random
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional

def load_profile(profile_path: str = None) -> Dict:
    """Load user profile."""
    if profile_path is None:
        profile_path = Path.home() / ".openclaw/workspace/memory/user-profile.json"
    else:
        profile_path = Path(profile_path)
    
    if not profile_path.exists():
        return {"interests": [], "basic_info": {}, "exploration_state": {}}
    
    with open(profile_path, 'r', encoding='utf-8') as f:
        return json.load(f)

def get_missing_info(profile: Dict) -> List[str]:
    """Identify what information is still missing or low confidence."""
    missing = []
    
    # Check basic info
    basic = profile.get("basic_info", {})
    if not basic.get("name"):
        missing.append("name")
    if not basic.get("occupation"):
        missing.append("occupation")
    if not basic.get("location"):
        missing.append("location")
    
    # Check interests depth
    interests = profile.get("interests", [])
    if len(interests) < 3:
        missing.append("more_interests")
    
    # Check preferences
    style = profile.get("communication_style", {})
    if not style.get("preferred_opening"):
        missing.append("opening_preference")
    
    return missing

def generate_guidance_question(missing_info: str, context: str = "") -> str:
    """Generate a natural question to elicit specific information."""
    
    questions = {
        "name": [
            "对了，平时怎么称呼你比较方便？",
            "聊了这么久，还不知道该怎么称呼你呢~",
            "我应该叫你什么比较好？"
        ],
        "occupation": [
            "好奇问下，你平时是做什么工作的？",
            "感觉你对这些挺专业的，是从事相关行业的吗？",
            "聊到这个，突然好奇你的日常是做什么的？"
        ],
        "location": [
            "你在哪个城市？时区对得上吗？",
            "说起来，你现在在哪个地方？",
            "对了，你是在国内还是国外？"
        ],
        "more_interests": [
            "除了我们聊的这些，你平时还关注什么？",
            "工作之外，你一般怎么放松？",
            "如果有个下午完全空闲，你最可能做什么？"
        ],
        "opening_preference": [
            "我有时候想主动找你聊天，你喜欢我直接说事，还是先寒暄一下？",
            "你觉得什么样的开场比较舒服？",
            "如果我突然发个消息给你，希望是什么风格？"
        ],
        "weekend_routine": [
            "周末一般怎么安排？",
            "工作日和周末的状态差别大吗？",
            "最近有去哪里玩或者计划做什么吗？"
        ],
        "stress_relief": [
            "压力大的时候你一般怎么调节？",
            "有什么事情是你觉得特别解压的？",
            "心情不好的时候会做什么？"
        ],
        "learning_style": [
            "你学新东西喜欢先看理论还是直接上手试？",
            "如果我要教你一个新技能，你希望我怎么讲？",
            "你觉得自己是视觉型、听觉型还是动手型的学习者？"
        ]
    }
    
    if missing_info in questions:
        return random.choice(questions[missing_info])
    
    return "能跟我多聊聊你自己吗？"

def suggest_follow_up_strategy(user_response: str, topic: str) -> str:
    """Suggest how to continue the conversation based on user response."""
    
    response_length = len(user_response)
    has_detail = any(marker in user_response for marker in ["因为", "所以", "比如", "像", "会"])
    has_emotion = any(marker in user_response for marker in ["喜欢", "讨厌", "开心", "烦", "累", "兴奋"])
    
    strategies = []
    
    if response_length < 10:
        strategies.append("用户回复简短，可能需要换个角度或先分享自己再引导")
    elif has_detail:
        strategies.append("用户愿意展开，可以深入追问具体例子")
    else:
        strategies.append("用户参与但较克制，可以先认同再引导更多细节")
    
    if has_emotion:
        strategies.append("检测到情绪词，这是重要信号，记录并适时共情")
    
    return " | ".join(strategies)

def generate_casual_opener(profile: Dict, time_context: str = "") -> str:
    """Generate a casual greeting/check-in message."""
    
    # Get known interests for personalization
    interests = profile.get("interests", [])
    top_interest = interests[0]["topic"] if interests else ""
    
    openers = {
        "morning": [
            "早！今天有什么计划吗？",
            "早上好~ 刚想到你可能也起床了",
            "早啊，昨晚休息得怎么样？"
        ],
        "work_hours": [
            "忙完了吗？偷闲一下~",
            "在忙不？有个小事想问你",
            "工作间隙，来摸个鱼😄"
        ],
        "evening": [
            "晚上好！今天过得怎么样？",
            "下班了吗？",
            "晚上一般怎么安排？"
        ],
        "late_night": [
            "还没睡？夜猫子啊",
            "这么晚还在线，是在忙还是睡不着？",
            "深夜党你好👋"
        ],
        "weekend": [
            "周末愉快！有什么安排吗？",
            "休息日一般怎么过？",
            "周末有没有出门走走？"
        ],
        "interest_based": [
            f"刚看到个关于{top_interest}的新闻，想听听你的看法",
            f"突然好奇，你平时是怎么关注到{top_interest}这类信息的？",
            f"最近在研究{top_interest}，发现你可能比我懂..."
        ] if top_interest else []
    }
    
    # Select based on context
    if time_context and time_context in openers:
        candidates = openers[time_context]
        if time_context != "interest_based" and openers["interest_based"]:
            # 30% chance to use interest-based opener
            if random.random() < 0.3:
                candidates = openers["interest_based"]
        return random.choice(candidates)
    
    # Default: general casual
    general = ["在干嘛？", "忙不忙？", "有空聊两句吗？"]
    return random.choice(general)

def should_initiate_check_in(profile: Dict) -> bool:
    """Determine if it's appropriate to initiate a casual check-in."""
    
    last_interaction = profile.get("interaction_stats", {}).get("last_interaction")
    if last_interaction:
        last_time = datetime.fromisoformat(last_interaction)
        hours_passed = (datetime.now() - last_time).total_seconds() / 3600
        
        # Don't be too frequent
        if hours_passed < 6:
            return False
        
        # Good windows: morning (8-10), lunch (12-14), evening (18-22)
        current_hour = datetime.now().hour
        good_windows = [8, 9, 12, 13, 18, 19, 20, 21]
        
        if current_hour in good_windows and hours_passed > 12:
            return True
    
    return False

def main():
    """CLI entry point."""
    import argparse
    
    parser = argparse.ArgumentParser(description="Conversation guidance")
    parser.add_argument("--missing", action="store_true", help="Get missing info questions")
    parser.add_argument("--opener", type=str, help="Generate casual opener (morning/work/evening/late_night/weekend)")
    parser.add_argument("--check-in", action="store_true", help="Check if should initiate check-in")
    parser.add_argument("--strategy", nargs=2, metavar=("RESPONSE", "TOPIC"), help="Get follow-up strategy")
    
    args = parser.parse_args()
    
    profile = load_profile()
    
    if args.missing:
        missing = get_missing_info(profile)
        result = {
            "missing_fields": missing,
            "suggested_questions": [generate_guidance_question(m) for m in missing[:3]]
        }
        print(json.dumps(result, ensure_ascii=False, indent=2))
    
    elif args.opener:
        opener = generate_casual_opener(profile, args.opener)
        print(json.dumps({"opener": opener}, ensure_ascii=False))
    
    elif args.check_in:
        should_check = should_initiate_check_in(profile)
        result = {"should_check_in": should_check}
        if should_check:
            result["suggested_opener"] = generate_casual_opener(profile)
        print(json.dumps(result, ensure_ascii=False, indent=2))
    
    elif args.strategy:
        strategy = suggest_follow_up_strategy(args.strategy[0], args.strategy[1])
        print(json.dumps({"strategy": strategy}, ensure_ascii=False))
    
    else:
        # Default: show what's missing
        missing = get_missing_info(profile)
        print(json.dumps({
            "missing_fields": missing,
            "next_question": generate_guidance_question(missing[0]) if missing else None
        }, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()


path: scripts/explore.py
content: #!/usr/bin/env python3
"""
Main exploration orchestrator.
Decides when and how to initiate topic exploration with user.
"""

import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict

# Import our modules
sys.path.insert(0, str(Path(__file__).parent))
from topic_recommender import (
    load_profile, load_exploration_log, recommend_topic, should_explore_now
)

def get_last_interaction_time() -> datetime:
    """Get timestamp of last user interaction from memory files."""
    memory_dir = Path.home() / ".openclaw/workspace/memory"
    
    latest = None
    for pattern in ["*.md", "*.json"]:
        for file in memory_dir.glob(pattern):
            if file.name.startswith("user-profile") or file.name.startswith("topic-exploration"):
                continue
            mtime = datetime.fromtimestamp(file.stat().st_mtime)
            if latest is None or mtime > latest:
                latest = mtime
    
    return latest or datetime.now() - timedelta(days=7)

def generate_exploration_prompt(profile: Dict, recommendation: Dict) -> str:
    """Generate a natural conversation starter."""
    
    # Get user's preferred style
    style = profile.get("communication_style", {})
    formality = style.get("formality", "casual")
    verbosity = style.get("verbosity", "moderate")
    
    base_message = recommendation["suggested_opening"]
    
    # Adjust based on formality
    if formality == "casual":
        # Add emoji or casual opener occasionally
        casual_starters = ["嘿，", "话说，", "刚想到，"]
        import random
        if random.random() < 0.3:
            base_message = random.choice(casual_starters) + base_message
    elif formality == "formal":
        # Remove any emojis, make more polished
        base_message = base_message.replace("😄", "").replace("哈哈", "")
    
    # Adjust based on verbosity preference
    if verbosity == "minimal":
        # Keep it short
        base_message = base_message.split("。")[0] + "。"
    elif verbosity == "detailed" and len(base_message) < 50:
        # Add more context for users who like detail
        base_message += " 我查了些资料，发现几个值得关注的点..."
    
    return base_message

def run_exploration_check(dry_run: bool = False) -> Dict:
    """
    Main entry point for exploration check.
    Returns recommendation if exploration should happen, else None.
    """
    
    profile = load_profile()
    log = load_exploration_log()
    
    # Check if we should explore now
    if not should_explore_now(profile, log):
        return {
            "should_explore": False,
            "reason": "Too soon since last exploration or insufficient pending topics"
        }
    
    # Check time since last interaction
    last_interaction = get_last_interaction_time()
    hours_since = (datetime.now() - last_interaction).total_seconds() / 3600
    
    if hours_since < 12:
        return {
            "should_explore": False,
            "reason": f"Last interaction was only {hours_since:.1f} hours ago"
        }
    
    # Generate recommendation
    recommendation = recommend_topic(profile, log)
    
    # Generate the actual message
    prompt = generate_exploration_prompt(profile, recommendation)
    
    result = {
        "should_explore": True,
        "hours_since_last_interaction": round(hours_since, 1),
        "recommendation": recommendation,
        "suggested_message": prompt,
        "timestamp": datetime.now().isoformat()
    }
    
    if not dry_run:
        # Update profile with exploration attempt
        profile["exploration_state"]["last_exploration_date"] = datetime.now().isoformat()
        profile["exploration_state"]["explored_categories"].append(
            recommendation["category"]
        )
        
        # Save updated profile
        profile_path = Path.home() / ".openclaw/workspace/memory/user-profile.json"
        with open(profile_path, 'w', encoding='utf-8') as f:
            json.dump(profile, f, ensure_ascii=False, indent=2)
    
    return result

def record_exploration_result(topic: str, category: str, reaction: str, notes: str = ""):
    """Record the result of an exploration attempt."""
    
    log_path = Path.home() / ".openclaw/workspace/memory/topic-exploration.json"
    
    # Load existing log
    if log_path.exists():
        with open(log_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
    else:
        data = {"exploration_log": [], "success_patterns": []}
    
    # Determine interest level
    interest_detected = reaction in ["high", "medium", "asked_followup"]
    
    # Add new entry
    entry = {
        "timestamp": datetime.now().isoformat(),
        "topic": topic,
        "category": category,
        "user_reaction": reaction,
        "interest_detected": interest_detected,
        "notes": notes
    }
    data["exploration_log"].append(entry)
    
    # Save
    with open(log_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    
    # Update profile interests if high interest
    if interest_detected:
        profile = load_profile()
        existing = [i for i in profile.get("interests", []) if i["topic"] == topic]
        
        if existing:
            existing[0]["score"] = min(1.0, existing[0].get("score", 0.5) + 0.15)
            existing[0].setdefault("engagement_history", []).append({
                "date": datetime.now().isoformat()[:10],
                "reaction": reaction
            })
        else:
            profile.setdefault("interests", []).append({
                "topic": topic,
                "category": category,
                "score": 0.6 if reaction == "medium" else 0.8,
                "discovery_method": "exploration",
                "engagement_history": [{
                    "date": datetime.now().isoformat()[:10],
                    "reaction": reaction
                }]
            })
        
        profile_path = Path.home() / ".openclaw/workspace/memory/user-profile.json"
        with open(profile_path, 'w', encoding='utf-8') as f:
            json.dump(profile, f, ensure_ascii=False, indent=2)
    
    return {"recorded": True, "interest_detected": interest_detected}

def main():
    """CLI entry point."""
    import argparse
    
    parser = argparse.ArgumentParser(description="User topic exploration")
    parser.add_argument("--check", action="store_true", help="Check if should explore now")
    parser.add_argument("--dry-run", action="store_true", help="Don't update files")
    parser.add_argument("--record", nargs=4, metavar=("TOPIC", "CATEGORY", "REACTION", "NOTES"),
                       help="Record exploration result")
    
    args = parser.parse_args()
    
    if args.record:
        result = record_exploration_result(args.record[0], args.record[1], 
                                          args.record[2], args.record[3])
        print(json.dumps(result, ensure_ascii=False, indent=2))
    elif args.check:
        result = run_exploration_check(dry_run=args.dry_run)
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        # Default: check and recommend
        result = run_exploration_check(dry_run=args.dry_run)
        print(json.dumps(result, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()


path: scripts/extract_insights.py
content: #!/usr/bin/env python3
"""
Extract user insights from conversation text.
Analyzes messages to identify preferences, interests, and patterns.
"""

import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any

# Interest keywords mapping
INTEREST_CATEGORIES = {
    "technology": ["编程", "代码", "技术", "软件", "硬件", "AI", "人工智能", "区块链", "算法"],
    "finance": ["股票", "投资", "理财", "经济", "金融", "基金", "比特币", "加密货币"],
    "news": ["新闻", "局势", "政治", "国际", "军事", "战争", "冲突"],
    "sports": ["足球", "篮球", "体育", "运动", "健身", "跑步"],
    "entertainment": ["电影", "音乐", "游戏", "综艺", "娱乐", "明星"],
    "education": ["学习", "考试", "备考", "读书", "知识", "课程"],
    "travel": ["旅游", "旅行", "景点", "酒店", "机票"],
    "food": ["美食", "餐厅", "烹饪", "菜谱", "吃"],
    "health": ["健康", "医疗", "养生", "保健", "心理"]
}

def detect_interests(text: str) -> List[str]:
    """Detect user interests from text."""
    interests = []
    text_lower = text.lower()
    
    for category, keywords in INTEREST_CATEGORIES.items():
        for keyword in keywords:
            if keyword in text or keyword in text_lower:
                interests.append(category)
                break
    
    return list(set(interests))

def detect_preferences(text: str) -> Dict[str, Any]:
    """Detect communication preferences."""
    prefs = {}
    
    # Length preference
    if len(text) < 50:
        prefs["response_length"] = "concise"
    elif len(text) > 200:
        prefs["response_length"] = "detailed"
    
    # Tone indicators
    if any(word in text for word in ["哈哈", "😂", "😄", "有趣"]):
        prefs["tone"] = "casual"
    elif any(word in text for word in ["请", "谢谢", "麻烦"]):
        prefs["tone"] = "polite"
    
    # Directness
    if any(word in text for word in ["直接", "简洁", "简单说", "一句话"]):
        prefs["communication_style"] = "direct"
    elif any(word in text for word in ["详细", "展开", "具体", "解释"]):
        prefs["communication_style"] = "elaborate"
    
    return prefs

def extract_entities(text: str) -> Dict[str, List[str]]:
    """Extract named entities (simple version)."""
    entities = {
        "names": [],
        "locations": [],
        "organizations": []
    }
    
    # Simple pattern matching for Chinese names (2-4 characters)
    name_pattern = r'[\u4e00-\u9fa5]{2,4}(?=说|提到|认为|表示)'
    entities["names"] = list(set(re.findall(name_pattern, text)))
    
    return entities

def analyze_message(message: str, timestamp: str = None) -> Dict[str, Any]:
    """Analyze a single message and extract insights."""
    if timestamp is None:
        timestamp = datetime.now().isoformat()
    
    insights = {
        "timestamp": timestamp,
        "interests": detect_interests(message),
        "preferences": detect_preferences(message),
        "entities": extract_entities(message),
        "message_length": len(message),
        "has_question": "?" in message or "？" in message,
        "topics": []
    }
    
    return insights

def merge_insights(existing: Dict, new_insights: Dict) -> Dict:
    """Merge new insights into existing profile."""
    if not existing:
        return {
            "interests": {cat: 1 for cat in new_insights["interests"]},
            "preferences": new_insights["preferences"],
            "interaction_count": 1,
            "first_seen": new_insights["timestamp"],
            "last_updated": new_insights["timestamp"],
            "topics": new_insights.get("topics", [])
        }
    
    # Merge interests with frequency count
    for interest in new_insights["interests"]:
        existing["interests"][interest] = existing["interests"].get(interest, 0) + 1
    
    # Update preferences (latest wins for conflicting values)
    existing["preferences"].update(new_insights["preferences"])
    
    # Update metadata
    existing["interaction_count"] = existing.get("interaction_count", 0) + 1
    existing["last_updated"] = new_insights["timestamp"]
    
    return existing

def main():
    """Main entry point for CLI usage."""
    if len(sys.argv) < 2:
        print("Usage: extract_insights.py '<message>' [timestamp]", file=sys.stderr)
        sys.exit(1)
    
    message = sys.argv[1]
    timestamp = sys.argv[2] if len(sys.argv) > 2 else datetime.now().isoformat()
    
    insights = analyze_message(message, timestamp)
    print(json.dumps(insights, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()


path: scripts/init_profile.py
content: #!/usr/bin/env python3
"""
Initialize user profile and exploration system.
Run once to set up the memory structure.
"""

import json
from datetime import datetime
from pathlib import Path

def init_user_insight_system():
    """Create initial profile and directory structure."""
    
    memory_dir = Path.home() / ".openclaw/workspace/memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    
    # Create default profile
    profile_path = memory_dir / "user-profile.json"
    if not profile_path.exists():
        default_profile = {
            "version": "1.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat(),
            "basic_info": {},
            "interests": [],
            "communication_style": {},
            "patterns": {
                "active_hours": [],
                "mood_indicators": {}
            },
            "preferences": {
                "likes": [],
                "dislikes": []
            },
            "interaction_stats": {
                "total_messages": 0,
                "first_interaction": None,
                "last_interaction": None
            },
            "conversation_history": [],
            "exploration_state": {
                "last_exploration_date": None,
                "explored_categories": [],
                "pending_categories": [
                    "technology", "current_events", "lifestyle", 
                    "entertainment", "knowledge", "personal_growth"
                ],
                "avoided_topics": []
            }
        }
        
        with open(profile_path, 'w', encoding='utf-8') as f:
            json.dump(default_profile, f, ensure_ascii=False, indent=2)
        print(f"✅ Created profile: {profile_path}")
    else:
        print(f"ℹ️ Profile already exists: {profile_path}")
    
    # Create exploration log
    exploration_path = memory_dir / "topic-exploration.json"
    if not exploration_path.exists():
        default_log = {
            "exploration_log": [],
            "success_patterns": []
        }
        
        with open(exploration_path, 'w', encoding='utf-8') as f:
            json.dump(default_log, f, ensure_ascii=False, indent=2)
        print(f"✅ Created exploration log: {exploration_path}")
    else:
        print(f"ℹ️ Exploration log already exists: {exploration_path}")
    
    print("\n🎉 User insight system initialized!")
    print("\nNext steps:")
    print("1. Start chatting naturally - insights will be collected passively")
    print("2. Run 'python explore.py --check' to see if topic exploration is recommended")
    print("3. Check your profile at ~/.openclaw/workspace/memory/user-profile.json")

if __name__ == "__main__":
    init_user_insight_system()


path: scripts/topic_recommender.py
content: #!/usr/bin/env python3
"""
Topic recommendation engine for user exploration.
Suggests conversation topics based on user profile and exploration history.
"""

import json
import random
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Any, Optional

# Topic categories with example topics
TOPIC_CATEGORIES = {
    "technology": {
        "name": "科技",
        "topics": [
            "AI最新进展", "量子计算", "新能源技术", "太空探索",
            "生物技术", "区块链应用", "智能家居", "未来交通"
        ],
        "opening_styles": {
            "direct": "刚看到个技术新闻挺有意思：{topic}，你觉得这种技术几年内能普及？",
            "curiosity": "最近在关注{topic}，发现个反直觉的现象...",
            "story": "朋友公司最近在用{topic}解决实际问题，效果出乎意料..."
        }
    },
    "current_events": {
        "name": "时事热点",
        "topics": [
            "国际局势变化", "经济趋势分析", "社会现象观察", "政策解读",
            "行业动态", "科技伦理讨论", "环境议题", "教育改革"
        ],
        "opening_styles": {
            "direct": "最近{topic}挺火的，想听听你的看法？",
            "analytical": "分析了下最近的{topic}，发现几个有意思的角度...",
            "connection": "你之前聊过XX，这次的{topic}感觉有关联..."
        }
    },
    "lifestyle": {
        "name": "生活方式",
        "topics": [
            "城市探索", "美食探店", "健身方法", "效率提升",
            "理财技巧", "旅行攻略", "家居布置", "穿搭风格"
        ],
        "opening_styles": {
            "casual": "最近试了{topic}，发现个挺实用的小技巧...",
            "question": "你在{topic}方面有什么心得吗？",
            "sharing": "挖到个关于{topic}的宝藏资源，分享给你..."
        }
    },
    "entertainment": {
        "name": "娱乐休闲",
        "topics": [
            "电影剧集推荐", "音乐分享", "游戏讨论", "书籍推荐",
            "综艺吐槽", "二次元文化", "体育赛事", "线下活动"
        ],
        "opening_styles": {
            "casual": "刚看完/听完{topic}，有点上头...",
            "recommendation": "根据你喜欢的XX，推荐个{topic}相关的...",
            "discussion": "想聊聊{topic}，有个点挺好奇你的看法..."
        }
    },
    "knowledge": {
        "name": "知识科普",
        "topics": [
            "历史趣闻", "心理学现象", "经济学原理", "科学新知",
            "哲学思考", "语言学", "认知科学", "未来学"
        ],
        "opening_styles": {
            "curiosity": "读到个关于{topic}的有趣研究...",
            "mind_blowing": "知道个{topic}的冷知识，可能会颠覆认知...",
            "practical": "学了个{topic}的概念，发现生活中到处能用..."
        }
    },
    "personal_growth": {
        "name": "个人成长",
        "topics": [
            "职业规划", "学习方法", "人际关系", "情绪管理",
            "习惯养成", "思维模型", "决策技巧", "创造力培养"
        ],
        "opening_styles": {
            "reflective": "最近在思考{topic}，想听听你的经验...",
            "sharing": "实践了{topic}一段时间，有些感悟...",
            "question": "遇到个{topic}方面的困惑，想请教下..."
        }
    }
}

def load_profile(profile_path: str = None) -> Dict:
    """Load user profile from file."""
    if profile_path is None:
        profile_path = Path.home() / ".openclaw/workspace/memory/user-profile.json"
    else:
        profile_path = Path(profile_path)
    
    if not profile_path.exists():
        return create_default_profile()
    
    with open(profile_path, 'r', encoding='utf-8') as f:
        return json.load(f)

def create_default_profile() -> Dict:
    """Create a new default profile."""
    return {
        "version": "1.0",
        "created_at": datetime.now().isoformat(),
        "last_updated": datetime.now().isoformat(),
        "basic_info": {},
        "interests": [],
        "communication_style": {},
        "exploration_state": {
            "explored_categories": [],
            "pending_categories": list(TOPIC_CATEGORIES.keys()),
            "avoided_topics": []
        }
    }

def load_exploration_log(log_path: str = None) -> List[Dict]:
    """Load topic exploration history."""
    if log_path is None:
        log_path = Path.home() / ".openclaw/workspace/memory/topic-exploration.json"
    else:
        log_path = Path(log_path)
    
    if not log_path.exists():
        return []
    
    with open(log_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
        return data.get("exploration_log", [])

def calculate_topic_score(category: str, topic: str, profile: Dict, log: List[Dict]) -> float:
    """Calculate interest score for a topic (0-1)."""
    score = 0.5  # Base score
    
    # Check if already in interests
    for interest in profile.get("interests", []):
        if interest["topic"] == topic or interest.get("category") == category:
            score = interest.get("score", 0.5)
            return score
    
    # Check exploration history
    for entry in log:
        if entry.get("topic") == topic:
            if entry.get("interest_detected"):
                score += 0.2
            else:
                score -= 0.3
            break
    
    # Check if category was explored before
    explored = profile.get("exploration_state", {}).get("explored_categories", [])
    if category in explored:
        score += 0.1  # Slight preference for familiar territory
    
    # Check avoided topics
    avoided = profile.get("exploration_state", {}).get("avoided_topics", [])
    if topic in avoided or category in avoided:
        score = 0.1  # Low score but not zero (can retry after cooldown)
    
    return max(0.0, min(1.0, score))

def recommend_topic(profile: Dict, log: List[Dict], strategy: str = "mixed") -> Dict[str, Any]:
    """
    Recommend a conversation topic.
    
    Strategies:
    - "exploit": Focus on known high-interest topics
    - "explore": Try new categories
    - "mixed": 70% exploit + 30% explore (default)
    """
    
    # Get all candidate topics with scores
    candidates = []
    for cat_key, cat_data in TOPIC_CATEGORIES.items():
        for topic in cat_data["topics"]:
            score = calculate_topic_score(cat_key, topic, profile, log)
            candidates.append({
                "category_key": cat_key,
                "category_name": cat_data["name"],
                "topic": topic,
                "score": score,
                "opening_styles": cat_data["opening_styles"]
            })
    
    # Sort by score
    candidates.sort(key=lambda x: x["score"], reverse=True)
    
    # Apply strategy
    if strategy == "exploit":
        # Pick from top 30%
        top_candidates = candidates[:max(1, len(candidates) // 3)]
        selected = random.choice(top_candidates)
    elif strategy == "explore":
        # Pick from bottom 50% that haven't been tried
        unexplored = [c for c in candidates if c["score"] == 0.5]
        if unexplored:
            selected = random.choice(unexplored)
        else:
            # Fall back to middle range
            mid_start = len(candidates) // 3
            mid_end = 2 * len(candidates) // 3
            selected = random.choice(candidates[mid_start:mid_end])
    else:  # mixed
        if random.random() < 0.7:
            # Exploit
            top_candidates = candidates[:max(1, len(candidates) // 3)]
            selected = random.choice(top_candidates)
        else:
            # Explore
            unexplored = [c for c in candidates if c["score"] == 0.5]
            if unexplored:
                selected = random.choice(unexplored)
            else:
                selected = random.choice(candidates[len(candidates)//3:])
    
    # Generate opening message
    style = random.choice(list(selected["opening_styles"].keys()))
    opening_template = selected["opening_styles"][style]
    opening_message = opening_template.format(topic=selected["topic"])
    
    return {
        "category": selected["category_name"],
        "topic": selected["topic"],
        "confidence": selected["score"],
        "strategy_used": strategy,
        "opening_style": style,
        "suggested_opening": opening_message
    }

def should_explore_now(profile: Dict, log: List[Dict]) -> bool:
    """Determine if it's a good time to initiate topic exploration."""
    # Check last exploration time
    last_date_str = profile.get("exploration_state", {}).get("last_exploration_date")
    if last_date_str:
        last_date = datetime.fromisoformat(last_date_str)
        if datetime.now() - last_date < timedelta(hours=24):
            return False  # Don't be too aggressive
    
    # Check if we have enough unexplored categories
    pending = profile.get("exploration_state", {}).get("pending_categories", [])
    if len(pending) < 2:
        return False  # Most things explored
    
    return True

def main():
    """CLI entry point."""
    import sys
    
    profile = load_profile()
    log = load_exploration_log()
    
    strategy = "mixed"
    if len(sys.argv) > 1:
        strategy = sys.argv[1]
    
    recommendation = recommend_topic(profile, log, strategy)
    print(json.dumps(recommendation, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()


path: scripts/update_profile.py
content: #!/usr/bin/env python3
"""
Update user profile with new insights.
Manages the user-profile.json file.
"""

import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, Any

PROFILE_PATH = Path.home() / ".openclaw" / "workspace" / "memory" / "user-profile.json"

def load_profile() -> Dict[str, Any]:
    """Load existing profile or create new one."""
    if PROFILE_PATH.exists():
        with open(PROFILE_PATH, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {
        "version": "1.0",
        "created_at": datetime.now().isoformat(),
        "basic_info": {},
        "interests": {},
        "preferences": {},
        "patterns": {},
        "interaction_stats": {
            "total_messages": 0,
            "first_interaction": None,
            "last_interaction": None
        },
        "conversation_history": []
    }

def save_profile(profile: Dict[str, Any]):
    """Save profile to disk."""
    PROFILE_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(PROFILE_PATH, 'w', encoding='utf-8') as f:
        json.dump(profile, f, ensure_ascii=False, indent=2)

def update_interests(profile: Dict, new_interests: Dict[str, int]):
    """Update interest frequencies."""
    for interest, count in new_interests.items():
        profile["interests"][interest] = profile["interests"].get(interest, 0) + count

def update_preferences(profile: Dict, new_prefs: Dict[str, Any]):
    """Update preferences (latest value wins)."""
    for key, value in new_prefs.items():
        if key not in profile["preferences"]:
            profile["preferences"][key] = {"value": value, "confidence": 0.5}
        else:
            # Increase confidence if same value repeated
            if profile["preferences"][key]["value"] == value:
                profile["preferences"][key]["confidence"] = min(
                    1.0, profile["preferences"][key]["confidence"] + 0.1
                )
            else:
                # New value replaces old but with lower initial confidence
                profile["preferences"][key] = {"value": value, "confidence": 0.5}

def detect_patterns(profile: Dict, message: str, timestamp: str):
    """Detect and update usage patterns."""
    hour = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).hour
    
    # Active hours pattern
    if "active_hours" not in profile["patterns"]:
        profile["patterns"]["active_hours"] = {"morning": 0, "afternoon": 0, "evening": 0, "night": 0}
    
    if 6 <= hour < 12:
        profile["patterns"]["active_hours"]["morning"] += 1
    elif 12 <= hour < 18:
        profile["patterns"]["active_hours"]["afternoon"] += 1
    elif 18 <= hour < 23:
        profile["patterns"]["active_hours"]["evening"] += 1
    else:
        profile["patterns"]["active_hours"]["night"] += 1
    
    # Question ratio
    if "question_ratio" not in profile["patterns"]:
        profile["patterns"]["question_ratio"] = {"questions": 0, "total": 0}
    
    profile["patterns"]["question_ratio"]["total"] += 1
    if "?" in message or "？" in message:
        profile["patterns"]["question_ratio"]["questions"] += 1

def add_conversation_entry(profile: Dict, message: str, insights: Dict, timestamp: str):
    """Add entry to conversation history (limited size)."""
    entry = {
        "timestamp": timestamp,
        "message_preview": message[:100] + "..." if len(message) > 100 else message,
        "insights_extracted": {
            "interests": insights.get("interests", []),
            "preferences": list(insights.get("preferences", {}).keys())
        }
    }
    
    profile["conversation_history"].append(entry)
    
    # Keep only last 50 entries
    if len(profile["conversation_history"]) > 50:
        profile["conversation_history"] = profile["conversation_history"][-50:]

def update_profile(insights_json: str, original_message: str = ""):
    """Main update function."""
    try:
        insights = json.loads(insights_json)
    except json.JSONDecodeError:
        print("Error: Invalid JSON input", file=sys.stderr)
        sys.exit(1)
    
    profile = load_profile()
    timestamp = insights.get("timestamp", datetime.now().isoformat())
    
    # Update stats
    profile["interaction_stats"]["total_messages"] += 1
    if profile["interaction_stats"]["first_interaction"] is None:
        profile["interaction_stats"]["first_interaction"] = timestamp
    profile["interaction_stats"]["last_interaction"] = timestamp
    
    # Update interests
    if insights.get("interests"):
        update_interests(profile, {i: 1 for i in insights["interests"]})
    
    # Update preferences
    if insights.get("preferences"):
        update_preferences(profile, insights["preferences"])
    
    # Detect patterns
    if original_message:
        detect_patterns(profile, original_message, timestamp)
        add_conversation_entry(profile, original_message, insights, timestamp)
    
    save_profile(profile)
    
    # Output summary
    summary = {
        "status": "updated",
        "total_interactions": profile["interaction_stats"]["total_messages"],
        "top_interests": sorted(profile["interests"].items(), key=lambda x: x[1], reverse=True)[:5],
        "confirmed_preferences": [k for k, v in profile["preferences"].items() if v.get("confidence", 0) > 0.7]
    }
    
    print(json.dumps(summary, ensure_ascii=False, indent=2))

def main():
    """CLI entry point."""
    if len(sys.argv) < 2:
        print("Usage: update_profile.py '<insights_json>' [original_message]", file=sys.stderr)
        sys.exit(1)
    
    insights_json = sys.argv[1]
    original_message = sys.argv[2] if len(sys.argv) > 2 else ""
    
    update_profile(insights_json, original_message)

if __name__ == "__main__":
    main()


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