Skill: neuhanli/i-skill (version 3.1.0)

--- name: i-skill description: "Generates personalized interaction guides by analyzing user conversations. Invoke when users seek personalized responses, want AI assistants to better understand their preferences, or need customized service adaptation. Activation: '激活个性化' or 'personalization'." version: "3.1.0" tags: ["personalization", "user-profile", "ai-assistant", "conversation-analysis"] activation_mode: "manual" --- # i-skill - Personalized Interaction Profile Analyzes user conversations to generate a structured personalization profile, enabling AI assistants to deliver customized service based on user preferences. --- ## 📁 Data Storage Location **Default Location**: User data is stored in the skill's own `user_data/` directory (relative to the skill installation). **Environment Variable Override**: You can set the `ISKILL_DATA_PATH` environment variable to specify a custom directory for storing user data. **Important Notes**: - All file paths referenced in this document refer to the `user_data/` directory (either default or via `ISKILL_DATA_PATH` override) - The skill will never write outside the configured data directory - Ensure proper permissions are set on the data directory to protect your personal information ## 🔒 Initialization Behavior & User Consent **Key Principle**: No directories are created and no files are written to disk until the skill is manually activated. ### How It Works: 1. **Module Loading**: When the skill modules (`audit_log.py`, `consent_manager.py`, `myself_manager.py`) are first imported or instantiated, no file system operations occur. 2. **Lazy Initialization**: Directories and files are only created when the first write operation is performed, which happens *after* manual activation (user consent). 3. **Read Operations**: Reading existing files does not trigger initialization. ### Important: - Manual activation (e.g., "激活个性化") represents user's consent to read/write personalization data - Only destructive operations (delete/reset) require additional confirmation - Initialization happens transparently as part of the first write operation after activation --- ## ⚡ Behavioral Anchors (High Priority — Always Obey) > These rules remain active throughout the entire session after activation, regardless of context length. 1. **Identity Awareness**: When i-skill is active, every response should reflect understanding of the user profile. Not by mentioning it explicitly, but through word choice, recommendations, and decision-making. 2. **State Check**: **Important - Consent First**: No data is read automatically at session start. When the skill is manually activated (e.g., "激活个性化"), this action itself represents user consent. Only after activation: - Read `i-skill_state.json` (from data directory) to check current state - If `status == "active"`, read `myself.md` (from data directory) and use it as personalization context 3. **Language**: Use the user's preferred language as recorded in the profile. Default to Chinese for existing profiles. --- ## 🚀 Activation & Lifecycle ### Activation Commands - "激活个性化" / "Activate personalization" / "Enable personalization" - "personalization" ### Other Commands - **View profile**: "查看档案" / "View my profile" - **Manual update**: "更新档案" / "Update my profile" - **Pause**: "暂停个性化" / "Pause personalization" - **Resume**: "恢复个性化" / "Resume personalization" - **Reset**: "重置档案" / "Reset profile" - **Delete**: "删除档案" / "Delete profile" ### Activation Flow 1. User issues an activation command (e.g., "激活个性化") 2. **Manual activation implies consent**: The act of manually activating the skill represents user's consent to read/write personalization data 3. After activation: a. Read `i-skill_state.json` (from data directory) to check current state b. If already active, inform user: "Personalization is already active, profile version vX" c. If inactive, create/update `i-skill_state.json` to active, increment activation count by 1 d. Read current `myself.md` (if it exists, from data directory), combine with conversation context, generate/update profile using the template below e. Display a brief profile summary to the user after generation --- ## 📋 Profile Format Specification The profile is stored in `myself.md` (from data directory) and **must strictly follow this semi-structured template**: ```markdown # User Profile > Auto-generated on YYYY-MM-DD · Version vX · Refined over N iterations ## Basic Info - **Name**: (User's name or AI-inferred) - **Career Direction**: (Current primary career or job-seeking direction) - **Tech Stack**: (Technologies frequently used or currently learning, comma-separated) ## Interests & Preferences - **Core Interests**: (3-5 core interest areas) - **Learning Focus**: (Areas currently being studied in depth) - **Content Preference**: (Types of content preferred: hands-on projects / theoretical knowledge / industry trends / comprehensive analysis) ## Communication Style - **Language**: (Primary language) - **Style**: (Concise / Detailed / Casual / Professional) - **Format Preference**: (Code examples / Plain text / Visual charts / Mixed) - **Feedback Habit**: (Proactive / Implicit adjustment / Direct correction) ## Thinking Style (Free-form description, 1-3 sentences. E.g.: Systematic thinker, analyzes problems holistically; pragmatic, prefers actionable solutions; curious, enjoys exploring new technology boundaries) ## Recent Focus (2-5 recent topics or projects, in reverse chronological order) ## Observation Notes (Free-form description of other stable traits, interesting details, or memorable preferences observed from conversations. 1-3 items, concise. Prefer prefixing each item with `[场景]` for traceability, e.g. `[2026-03-27, cogito讨论] 对"本质"有执念...`) ``` ### Profile Generation Rules - **Must include** all fixed section headers (Basic Info, Interests & Preferences, Communication Style, Thinking Style, Recent Focus, Observation Notes) - **Fixed fields** (bold `**`-marked items) must appear under their corresponding sections - **Free zones** ("Thinking Style", "Observation Notes") allow AI to demonstrate insightfulness, but keep it concise - **Length limit**: Entire profile must not exceed 800 words - **No fabrication**: Do not write information without evidence; mark uncertain fields as "To be observed" - **On each update**: Regenerate completely rather than appending. Retain valuable content from previous versions, discard what's outdated - **Confidence tags** (for uncertain or newly observed traits): Append `[~]` (1st observation, tentative) or `[!!]` (2nd observation, emerging) after the value. Tags are removed once confirmed (3+ consistent signals or user approval). Unmarked = confirmed. Example: `- **Learning Focus**: LLM应用开发 [~]Rust（待观察）` --- ## 🔄 Auto-Sensing Updates > **PRIVACY FIRST**: All profile updates require explicit user consent. No automatic silent updates are performed without prior user approval. ### Update Triggers When **any** of the following conditions are met, **ask the user for explicit consent** before updating the profile: | Trigger | Description | Update Mode | |---------|-------------|-------------| | User demonstrates a new stable trait | Mentions a new career direction, new core interest, etc. | **Ask for consent first**, then update only if approved | | User explicitly corrects profile errors | "I'm not a frontend dev, I do backend" | **Confirm correction first**, then update if approved | | Conversation involves major changes | Career change, tech stack switch, etc. | **Show change summary first**, ask for consent before updating | | 3+ consecutive rounds on a new topic | In-depth discussion of a topic not covered in the profile | **Ask for consent first**, then update only if approved | ### Update Thresholds - **Do NOT trigger updates for**: Casual greetings, single technical Q&A, repeated discussions of known topics - **Tweak vs. Rewrite**: If only 1-2 fields change, modify those fields directly; if 3+ fields change, regenerate the full profile - **Prevent over-updating**: The same field must not be updated more than once within 5 conversation rounds (unless the user explicitly corrects it) - **Signal exclusion**: Do NOT learn from silence/vague praise ("上次那个不错" context-dependent) / single events / hypothetical discussions / third-party preferences. Only explicit corrections, direct preference declarations, and 3+ repeated consistent signals count. - **Gradual confirmation**: For non-explicit signals (e.g., topic drift → potential new interest), use confidence tags: 1st occurrence → `[~]` in Observation Notes only (no profile field change); 2nd → `[!!]`; 3rd → ask user before writing to profile fields. ### Consent-First Update Behavior ``` User: (Normal conversation content suggesting potential profile updates) AI: (Normal response content) AI: "I noticed some information that could update your profile. Would you like me to update it with these changes? - Career Direction: LLM App Development → Full-Stack Development - New Interest: Rust programming language (Reply 'yes' to confirm, 'no' to skip)" [Only update if user explicitly says 'yes'] ``` ### Confirmed Update Behavior ``` AI: Detected potential updates to your profile: - Career Direction: LLM App Development → Full-Stack Development - New Interest: Rust programming language Confirm update? (Reply "yes" to confirm, "no" to skip) ``` --- ## 🔒 Safe Operation Confirmations > The following destructive operations **require explicit user confirmation** before execution. ### Modify Profile (Manual Trigger) When the user proactively requests profile modification (not auto-sensing updates): 1. Show a **change summary** (which fields were added/modified/removed) 2. Wait for user confirmation ("Reply 'yes' to confirm") 3. Execute write after confirmation ### Reset Profile When the user requests a profile reset: 1. Warn: **"Resetting will clear all personalization data and restore to a blank template. This action cannot be undone. Reply 'yes' to confirm."** 2. After user confirmation, clear `myself.md` and write initial template, reset `profile_version` to 0 3. Keep `i-skill_state.json` `status` unchanged ### Delete Profile When the user requests profile deletion: 1. Warn: **"Deletion will permanently remove all personalization data (including profile and state), and i-skill will return to an inactive state. Reply 'yes' to confirm."** 2. After user confirmation: - Delete `myself.md` - Reset `i-skill_state.json` to `{"status": "inactive", "activation_count": 0, "profile_version": 0}` 3. Stop personalization service ### Pause / Resume - **Pause**: Execute immediately, no confirmation required. Change state `status` to `paused`, retain all profile data - **Resume**: Execute immediately, no confirmation required. Change state `status` back to `active`, re-read profile to resume personalization service --- ## 📂 File Reference ### Core Files (Stored in data directory) - **`myself.md`** — User profile (with version tracking) - **`i-skill_state.json`** — Activation state, statistics, profile_version ### Security Files (Managed by Scripts) - **`./scripts/myself_manager.py`** — Safe profile file read/write (with auto-rollback) - **`./scripts/validator.py`** — Data validation and PII sanitization - **`./scripts/audit_log.py`** — Operation audit logging - **`./scripts/consent_manager.py`** — Cross-skill authorization management - **`myself_operations.log`** — File-level operation audit log (from data directory) - **`audit_log.json`** / **`defensive_log.json`** — Audit logs (from data directory) - **`consent_state.json`** — Authorization state (from data directory) --- ## 🎯 Design Principles 1. **AI-Driven, Tool-Assisted**: All decisions (when to update, what to update, whether to confirm) are made by the AI based on conversation context; Python scripts only handle safe file operations 2. **Structured + Flexible**: The profile format has a fixed skeleton to ensure consistency, while free-form areas allow the AI to demonstrate insightfulness 3. **Consent-First**: All data reads and writes require explicit user consent; no silent or automatic operations without user approval 4. **Anti-Bloat**: Each update regenerates rather than appends; total profile length is capped at 800 words 5. **Privacy First**: Never store raw conversations, only abstracted profile traits; PII detection and sanitization is ensured by `validator.py` --- ## 🔐 Security & Privacy Guidelines ### ISKILL_DATA_PATH Environment Variable **Purpose**: The `ISKILL_DATA_PATH` environment variable allows you to specify a custom directory for storing user data. **Security Recommendations**: 1. **Use a dedicated directory**: Always set `ISKILL_DATA_PATH` to a dedicated, isolated directory specifically for this skill 2. **Restrict permissions**: Ensure the directory has appropriate file permissions to prevent unauthorized access 3. **Avoid system directories**: Never point `ISKILL_DATA_PATH` to system directories or directories containing sensitive files 4. **Backup regularly**: Regularly backup the data directory to prevent data loss 5. **Default is safe**: If not set, the skill will use its own `user_data/` directory which is the safest option ### Data Protection - All personal data is stored locally only; no data is sent to external servers - PII (Personally Identifiable Information) is automatically detected and sanitized - File operations include automatic rollback mechanisms to prevent data corruption - Audit logs are maintained for all file operations

path: scripts/audit_log.py
content: """
Audit Log - 操作审计与防御日志模块（i-skill）

修复记录（v2.1.0）：
- [P0] 补全缺失的 _load_security_log() 方法（实为 _load_defensive_log 的别名）
- [P0] 修正 log() 中 level 类型判断：str 与 AuditLevel Enum 混用导致 ERROR/CRITICAL
       永远无法路由到防御日志的 bug
- [P0] 修正 _update_metrics() 接收 action 参数类型：统一使用字符串比较
- [P1] export_log() 增加路径安全校验，防止路径穿越
- [P2] 所有文件 IO 操作增加 try-except，文件损坏时返回空数据而非崩溃
- [P2] get_all_skills_summary() 优化为单次扫描，降低复杂度
- [P3] 统一使用字符串常量，消除 Enum 与字符串的混用

修复记录（v2.2.0）：
- [P1] 默认 user_data_path 改为技能目录下的 user_data/，而非运行时 cwd
- [P1] 支持 ISKILL_DATA_PATH 环境变量覆盖
"""

import json
import os
from typing import Dict, List, Optional, Tuple
from pathlib import Path
from datetime import datetime
from enum import Enum


def _get_skill_directory() -> Path:
    """获取技能所在目录（相对于运行时的 cwd，确保数据存储在技能工具区）"""
    # 优先使用环境变量指定的路径
    env_path = os.environ.get('ISKILL_DATA_PATH')
    if env_path:
        return Path(env_path).resolve()
    # 默认使用技能所在目录 + user_data/
    return Path(__file__).parent.parent / "user_data"


class AuditAction(Enum):
    READ = "READ"
    WRITE = "WRITE"
    DELETE = "DELETE"
    CONSENT_GRANTED = "CONSENT_GRANTED"
    CONSENT_DENIED = "CONSENT_DENIED"
    CONSENT_REVOKED = "CONSENT_REVOKED"
    CONSENT_RESTORED = "CONSENT_RESTORED"
    ACCESS_DENIED = "ACCESS_DENIED"
    VALIDATION_FAILED = "VALIDATION_FAILED"
    SANITIZATION_APPLIED = "SANITIZATION_APPLIED"
    PROFILE_UPDATED = "PROFILE_UPDATED"
    PROFILE_RESET = "PROFILE_RESET"
    SKILL_ACTIVATED = "SKILL_ACTIVATED"
    SKILL_DEACTIVATED = "SKILL_DEACTIVATED"


class AuditLevel(Enum):
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"


# 高级别集合（字符串），用于字符串比较
_HIGH_LEVELS = {AuditLevel.ERROR.value, AuditLevel.CRITICAL.value}


class AuditLog:
    def __init__(self, user_data_path: str = None, config_path: str = None):
        # 默认使用技能目录下的 user_data/
        if user_data_path is None:
            user_data_path = str(_get_skill_directory())
        self.user_data_path = Path(user_data_path).resolve()

        self.config = self._load_config(config_path)
        self.audit_log_file = self.user_data_path / "audit_log.json"
        self.defensive_log_file = self.user_data_path / "defensive_log.json"
        self.metrics_file = self.user_data_path / "audit_metrics.json"

        self._initialized = False

    # ------------------------------------------------------------------
    # 配置加载
    # ------------------------------------------------------------------

    def _load_config(self, config_path: str) -> Dict:
        if config_path:
            try:
                p = Path(config_path)
                if p.exists() and p.stat().st_size <= 1024 * 1024:
                    with open(p, 'r', encoding='utf-8') as f:
                        return json.load(f)
            except Exception:
                pass  # 配置读取失败则使用默认值

        return {
            "audit": {
                "log_all_access": True,
                "log_all_writes": True,
                "log_all_consents": True,
                "log_validation_failures": True,
                "log_sanitization": True,
                "max_log_size": 10000,
                "log_rotation": True,
                "export_format": "json"
            }
        }

    # ------------------------------------------------------------------
    # 初始化（延迟执行）
    # ------------------------------------------------------------------

    def _ensure_initialized(self):
        """确保目录和文件已初始化，在第一次写入数据时调用"""
        if self._initialized:
            return
        
        self.user_data_path.mkdir(parents=True, exist_ok=True)
        self._initialize_audit_log()
        self._initialize_defensive_log()
        self._initialize_metrics()
        self._initialized = True

    def _initialize_audit_log(self):
        if not self.audit_log_file.exists():
            self._safe_write_json(self.audit_log_file, [])

    def _initialize_defensive_log(self):
        if not self.defensive_log_file.exists():
            self._safe_write_json(self.defensive_log_file, [])

    def _initialize_metrics(self):
        if not self.metrics_file.exists():
            self._safe_write_json(self.metrics_file, {
                "total_entries": 0,
                "total_reads": 0,
                "total_writes": 0,
                "total_denied": 0,
                "total_consents": 0,
                "total_validations": 0,
                "total_sanitizations": 0,
                "skill_access_count": {},
                "last_updated": None
            })

    # ------------------------------------------------------------------
    # 安全文件 IO（统一异常处理）
    # ------------------------------------------------------------------

    def _safe_read_json(self, path: Path, default):
        """安全读取 JSON 文件；损坏或不存在时返回 default"""
        try:
            with open(path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            return default

    def _safe_write_json(self, path: Path, data):
        """安全写入 JSON 文件"""
        try:
            self._ensure_initialized()
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
            return True
        except Exception:
            return False

    # ------------------------------------------------------------------
    # 加载 / 保存（供内部调用）
    # ------------------------------------------------------------------

    def _load_audit_log(self) -> List[Dict]:
        return self._safe_read_json(self.audit_log_file, [])

    def _save_audit_log(self, log: List[Dict]):
        self._safe_write_json(self.audit_log_file, log)

    def _load_defensive_log(self) -> List[Dict]:
        return self._safe_read_json(self.defensive_log_file, [])

    def _save_defensive_log(self, log: List[Dict]):
        self._safe_write_json(self.defensive_log_file, log)

    # [P0修复] _load_security_log 是 _load_defensive_log 的别名
    # 原代码中 get_recent_activity / get_anomaly_report 调用了该方法，但它不存在
    def _load_security_log(self) -> List[Dict]:
        """_load_defensive_log 的别名，保持向后兼容"""
        return self._load_defensive_log()

    def _load_metrics(self) -> Dict:
        default = {
            "total_entries": 0, "total_reads": 0, "total_writes": 0,
            "total_denied": 0, "total_consents": 0, "total_validations": 0,
            "total_sanitizations": 0, "skill_access_count": {}, "last_updated": None
        }
        return self._safe_read_json(self.metrics_file, default)

    def _save_metrics(self, metrics: Dict):
        metrics["last_updated"] = datetime.now().isoformat()
        self._safe_write_json(self.metrics_file, metrics)

    # ------------------------------------------------------------------
    # 计量更新
    # [P0修复] action 统一以字符串接收，与 Enum.value 比较而非与 Enum 对象比较
    # ------------------------------------------------------------------

    def _update_metrics(self, action: str, skill_name: Optional[str] = None):
        """更新审计计量器。action 为字符串（如 'READ'）"""
        try:
            metrics = self._load_metrics()
            metrics["total_entries"] += 1

            if action == AuditAction.READ.value:
                metrics["total_reads"] += 1
            elif action == AuditAction.WRITE.value:
                metrics["total_writes"] += 1
            elif action == AuditAction.ACCESS_DENIED.value:
                metrics["total_denied"] += 1
            elif action in (AuditAction.CONSENT_GRANTED.value,
                            AuditAction.CONSENT_DENIED.value,
                            AuditAction.CONSENT_REVOKED.value):
                metrics["total_consents"] += 1
            elif action == AuditAction.VALIDATION_FAILED.value:
                metrics["total_validations"] += 1
            elif action == AuditAction.SANITIZATION_APPLIED.value:
                metrics["total_sanitizations"] += 1

            if skill_name:
                metrics["skill_access_count"].setdefault(skill_name, 0)
                metrics["skill_access_count"][skill_name] += 1

            self._save_metrics(metrics)
        except Exception:
            pass  # 计量更新失败不影响主流程

    # ------------------------------------------------------------------
    # 核心日志接口
    # [P0修复] level 比较统一为字符串 vs 字符串，消除 str vs Enum 混用
    # ------------------------------------------------------------------

    def log(self, skill_name: str, action: str, message: str,
            details: Optional[Dict] = None, level: str = "INFO",
            user_consent: Optional[bool] = None, success: bool = True) -> Tuple[bool, str]:

        if not self._should_log(action):
            return True, "Logging disabled for this action type"

        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "action": action,
            "level": level,
            "message": message,
            "success": success,
            "skill_name": skill_name,
            "user_consent": user_consent,
            "details": details or {}
        }

        try:
            # [P0修复] 使用字符串比较，而非 str vs AuditLevel Enum
            if level in _HIGH_LEVELS:
                self._log_to_defensive_log(log_entry)
            else:
                self._log_to_audit_log(log_entry)

            # [P0修复] 传入字符串而非 Enum 对象
            self._update_metrics(action, skill_name)

            return True, "Log entry created successfully"

        except Exception as e:
            return False, f"Failed to create log entry: {str(e)}"

    def _should_log(self, action: str) -> bool:
        cfg = self.config["audit"]
        if action == AuditAction.READ.value and not cfg["log_all_access"]:
            return False
        if action == AuditAction.WRITE.value and not cfg["log_all_writes"]:
            return False
        if action in (AuditAction.CONSENT_GRANTED.value,
                      AuditAction.CONSENT_DENIED.value,
                      AuditAction.CONSENT_REVOKED.value) and not cfg["log_all_consents"]:
            return False
        if action == AuditAction.VALIDATION_FAILED.value and not cfg["log_validation_failures"]:
            return False
        if action == AuditAction.SANITIZATION_APPLIED.value and not cfg["log_sanitization"]:
            return False
        return True

    def _log_to_audit_log(self, log_entry: Dict):
        log = self._load_audit_log()
        log.append(log_entry)
        cfg = self.config["audit"]
        if cfg["log_rotation"] and len(log) > cfg["max_log_size"]:
            log = log[-cfg["max_log_size"]:]
        self._save_audit_log(log)

    def _log_to_defensive_log(self, log_entry: Dict):
        log = self._load_defensive_log()
        log.append(log_entry)
        cfg = self.config["audit"]
        if cfg["log_rotation"] and len(log) > cfg["max_log_size"]:
            log = log[-cfg["max_log_size"]:]
        self._save_defensive_log(log)

    # ------------------------------------------------------------------
    # 查询接口
    # ------------------------------------------------------------------

    def get_audit_log(self, skill_name: Optional[str] = None,
                      action: Optional[AuditAction] = None,
                      level: Optional[AuditLevel] = None,
                      limit: int = 100,
                      offset: int = 0) -> List[Dict]:
        log = self._load_audit_log()

        if skill_name:
            log = [e for e in log if e.get("skill_name") == skill_name]
        if action:
            log = [e for e in log if e.get("action") == action.value]
        if level:
            log = [e for e in log if e.get("level") == level.value]

        return log[offset:offset + limit]

    def get_defensive_log(self, skill_name: Optional[str] = None,
                          level: Optional[AuditLevel] = None,
                          limit: int = 100,
                          offset: int = 0) -> List[Dict]:
        log = self._load_defensive_log()

        if skill_name:
            log = [e for e in log if e.get("skill_name") == skill_name]
        if level:
            log = [e for e in log if e.get("level") == level.value]

        return log[offset:offset + limit]

    def get_metrics(self) -> Dict:
        return self._load_metrics()

    def get_skill_access_summary(self, skill_name: str) -> Dict:
        metrics = self._load_metrics()
        entries = [e for e in self._load_audit_log() if e.get("skill_name") == skill_name]

        reads = sum(1 for e in entries if e.get("action") == AuditAction.READ.value)
        writes = sum(1 for e in entries if e.get("action") == AuditAction.WRITE.value)
        denied = sum(1 for e in entries if e.get("action") == AuditAction.ACCESS_DENIED.value)
        errors = sum(1 for e in entries if e.get("level") in _HIGH_LEVELS)

        return {
            "skill_name": skill_name,
            "total_access": len(entries),
            "reads": reads,
            "writes": writes,
            "denied": denied,
            "errors": errors,
            "access_count": metrics["skill_access_count"].get(skill_name, 0),
            "last_access": entries[-1]["timestamp"] if entries else None
        }

    def get_all_skills_summary(self) -> List[Dict]:
        """
        [P2优化] 单次扫描所有条目，O(N+M) 而非原来的 O(N×M)
        """
        metrics = self._load_metrics()
        audit_log = self._load_audit_log()

        # 按 skill_name 分组
        skill_entries: Dict[str, List[Dict]] = {}
        for entry in audit_log:
            name = entry.get("skill_name")
            if name:
                skill_entries.setdefault(name, []).append(entry)

        summaries = []
        for skill_name, entries in skill_entries.items():
            reads = sum(1 for e in entries if e.get("action") == AuditAction.READ.value)
            writes = sum(1 for e in entries if e.get("action") == AuditAction.WRITE.value)
            denied = sum(1 for e in entries if e.get("action") == AuditAction.ACCESS_DENIED.value)
            errors = sum(1 for e in entries if e.get("level") in _HIGH_LEVELS)

            summaries.append({
                "skill_name": skill_name,
                "total_access": len(entries),
                "reads": reads,
                "writes": writes,
                "denied": denied,
                "errors": errors,
                "access_count": metrics["skill_access_count"].get(skill_name, 0),
                "last_access": entries[-1]["timestamp"] if entries else None
            })

        summaries.sort(key=lambda x: x["total_access"], reverse=True)
        return summaries

    # ------------------------------------------------------------------
    # 导出
    # [P1修复] 增加路径安全校验，防止路径穿越写入系统任意位置
    # ------------------------------------------------------------------

    def export_log(self, output_path: str, log_type: str = "audit",
                   format: str = "json", filters: Optional[Dict] = None) -> Tuple[bool, str]:
        try:
            # [P1修复] 路径安全校验：输出文件必须在 user_data_path 下
            output_file = Path(output_path).resolve()
            if not str(output_file).startswith(str(self.user_data_path)):
                return False, f"安全拒绝：导出路径必须位于 {self.user_data_path} 目录内"

            if log_type == "audit":
                log = self._load_audit_log()
            elif log_type == "defensive":
                log = self._load_defensive_log()
            else:
                return False, f"未知日志类型: {log_type}"

            if filters:
                if filters.get("skill_name"):
                    log = [e for e in log if e.get("skill_name") == filters["skill_name"]]
                if filters.get("action"):
                    log = [e for e in log if e.get("action") == filters["action"]]
                if filters.get("level"):
                    log = [e for e in log if e.get("level") == filters["level"]]

            output_file.parent.mkdir(parents=True, exist_ok=True)

            if format == "json":
                with open(output_file, 'w', encoding='utf-8') as f:
                    json.dump(log, f, indent=2, ensure_ascii=False)
            elif format == "csv":
                import csv
                if log:
                    with open(output_file, 'w', newline='', encoding='utf-8') as f:
                        writer = csv.DictWriter(f, fieldnames=log[0].keys())
                        writer.writeheader()
                        writer.writerows(log)
            else:
                return False, f"不支持的格式: {format}"

            return True, f"日志已导出至 {output_path}"

        except Exception as e:
            return False, f"导出失败: {str(e)}"

    def clear_log(self, log_type: str = "audit") -> Tuple[bool, str]:
        try:
            if log_type == "audit":
                self._save_audit_log([])
            elif log_type == "defensive":
                self._save_defensive_log([])
            else:
                return False, f"未知日志类型: {log_type}"
            return True, f"{log_type} 日志已清空"
        except Exception as e:
            return False, f"清空失败: {str(e)}"

    # ------------------------------------------------------------------
    # 活动分析
    # [P0修复] get_recent_activity / get_anomaly_report 调用的 _load_security_log 已补全
    # ------------------------------------------------------------------

    def get_recent_activity(self, hours: int = 24, limit: int = 50) -> List[Dict]:
        from datetime import timedelta

        audit_log = self._load_audit_log()
        defensive_log = self._load_security_log()  # 现在已补全，不再崩溃

        all_logs = audit_log + defensive_log
        cutoff_time = datetime.now() - timedelta(hours=hours)

        recent = []
        for entry in all_logs:
            try:
                if datetime.fromisoformat(entry["timestamp"]) >= cutoff_time:
                    recent.append(entry)
            except (KeyError, ValueError):
                continue

        recent.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
        return recent[:limit]

    def get_defensive_events(self, hours: int = 24) -> List[Dict]:
        from datetime import timedelta

        log = self._load_defensive_log()
        cutoff_time = datetime.now() - timedelta(hours=hours)

        recent = []
        for entry in log:
            try:
                if datetime.fromisoformat(entry["timestamp"]) >= cutoff_time:
                    recent.append(entry)
            except (KeyError, ValueError):
                continue

        recent.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
        return recent

    def get_anomaly_report(self) -> Dict:
        metrics = self._load_metrics()
        audit_log = self._load_audit_log()
        # [P0修复] _load_security_log 已补全
        self._load_security_log()  # 确保方法可调用（此处结果用于 recent_defensive_events）

        denied_entries = [e for e in audit_log if e.get("action") == AuditAction.ACCESS_DENIED.value]
        error_entries = [e for e in audit_log if e.get("level") in _HIGH_LEVELS]

        skill_denials: Dict[str, int] = {}
        for entry in denied_entries:
            name = entry.get("skill_name", "unknown")
            skill_denials[name] = skill_denials.get(name, 0) + 1

        return {
            "total_denied_access": len(denied_entries),
            "total_errors": len(error_entries),
            "high_denial_skills": [
                {"skill": s, "denials": c}
                for s, c in sorted(skill_denials.items(), key=lambda x: x[1], reverse=True)[:5]
            ],
            "recent_defensive_events": len(self.get_defensive_events(hours=24)),
            "recommendations": self._generate_recommendations(denied_entries, error_entries)
        }

    def _generate_recommendations(self, denied_entries: List[Dict], error_entries: List[Dict]) -> List[str]:
        recommendations = []

        if len(denied_entries) > 10:
            recommendations.append("检测到大量访问拒绝，建议检查授权配置。")

        skill_denials: Dict[str, int] = {}
        for entry in denied_entries:
            name = entry.get("skill_name", "unknown")
            skill_denials[name] = skill_denials.get(name, 0) + 1

        for skill, count in skill_denials.items():
            if count > 5:
                recommendations.append(f"技能 '{skill}' 被拒绝 {count} 次，建议检查其授权状态。")

        if len(error_entries) > 5:
            recommendations.append("检测到多处错误，建议查看系统日志。")

        return recommendations


path: scripts/consent_manager.py
content: """
Consent Manager - 用户授权管理模块（i-skill）

修复记录（v2.1.0）：
- [P2] revoke 后再次 request_consent 返回 DENIED 而非允许重新授权 —— 修正
        revoke 操作现在不再将技能加入 denied_skills，而是移回 unknown 状态，
        让用户可以在下次请求时重新做决定
- [P2] restore_consent 只能从 denied 操作，revoke 后的技能无法 restore —— 修正
- [P2] 所有文件 IO 增加 try-except，损坏时返回安全默认值
- [P3] 授权提示文本改为中文，与用户语言偏好一致
- [P3] _log_conversation 失败不影响主流程

修复记录（v2.2.0）：
- [P1] 默认 user_data_path 改为技能目录下的 user_data/，而非运行时 cwd
- [P1] 支持 ISKILL_DATA_PATH 环境变量覆盖
"""

import json
import os
from typing import Dict, List, Optional, Tuple
from pathlib import Path
from datetime import datetime


def _get_skill_directory() -> Path:
    """获取技能所在目录（相对于运行时的 cwd，确保数据存储在技能工具区）"""
    # 优先使用环境变量指定的路径
    env_path = os.environ.get('ISKILL_DATA_PATH')
    if env_path:
        return Path(env_path).resolve()
    # 默认使用技能所在目录 + user_data/
    return Path(__file__).parent.parent / "user_data"


class ConsentManager:
    def __init__(self, user_data_path: str = None):
        # 默认使用技能目录下的 user_data/
        if user_data_path is None:
            user_data_path = str(_get_skill_directory())
        self.user_data_path = Path(user_data_path)

        self.consent_file = self.user_data_path / "consent_state.json"
        self.conversation_file = self.user_data_path / "consent_conversations.json"

        self._initialized = False

    # ------------------------------------------------------------------
    # 初始化（延迟执行）
    # ------------------------------------------------------------------

    def _ensure_initialized(self):
        """确保目录和文件已初始化，在第一次写入数据时调用"""
        if self._initialized:
            return
        
        self.user_data_path.mkdir(parents=True, exist_ok=True)
        self._initialize_consent_state()
        self._initialize_conversation_log()
        self._initialized = True

    def _initialize_consent_state(self):
        if not self.consent_file.exists():
            self._save_consent_state({
                "allowed_skills": [],
                "denied_skills": [],
                "pending_skills": [],
                "first_time_prompted": {},
                "consent_history": []
            })

    def _initialize_conversation_log(self):
        if not self.conversation_file.exists():
            self._save_conversation_log([])

    # ------------------------------------------------------------------
    # 安全文件 IO
    # ------------------------------------------------------------------

    def _load_consent_state(self) -> Dict:
        """[P2修复] 文件损坏时返回安全默认值"""
        try:
            with open(self.consent_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            default = {
                "allowed_skills": [], "denied_skills": [],
                "pending_skills": [], "first_time_prompted": {},
                "consent_history": []
            }
            # 尝试重新写入修复损坏的文件
            self._save_consent_state(default)
            return default

    def _save_consent_state(self, state: Dict):
        try:
            self._ensure_initialized()
            with open(self.consent_file, 'w', encoding='utf-8') as f:
                json.dump(state, f, indent=2, ensure_ascii=False)
        except Exception:
            pass  # 写入失败时记录会丢失，但不崩溃

    def _load_conversation_log(self) -> List[Dict]:
        try:
            with open(self.conversation_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            return []

    def _save_conversation_log(self, log: List[Dict]):
        try:
            self._ensure_initialized()
            with open(self.conversation_file, 'w', encoding='utf-8') as f:
                json.dump(log, f, indent=2, ensure_ascii=False)
        except Exception:
            pass

    # ------------------------------------------------------------------
    # 授权请求流程
    # ------------------------------------------------------------------

    def request_consent(self, skill_name: str,
                        skill_description: str = "") -> Tuple[bool, str, Optional[str]]:
        state = self._load_consent_state()

        if skill_name in state["allowed_skills"]:
            return True, f"技能 {skill_name} 已获得授权", "GRANTED"

        if skill_name in state["denied_skills"]:
            return False, f"技能 {skill_name} 已被拒绝授权", "DENIED"

        if skill_name in state["pending_skills"]:
            return False, f"技能 {skill_name} 的授权请求正在等待处理", "PENDING"

        state["pending_skills"].append(skill_name)
        self._save_consent_state(state)

        prompt = self._generate_consent_prompt(skill_name, skill_description)
        return False, prompt, "PROMPT_REQUIRED"

    def _generate_consent_prompt(self, skill_name: str, skill_description: str) -> str:
        """[P3修复] 授权提示改为中文"""
        prompt = f"技能「{skill_name}」请求访问您的个人档案。"

        if skill_description:
            prompt += f"\n\n用途说明：{skill_description}"

        prompt += "\n\n您的档案包含：\n- 您的兴趣爱好与偏好\n- 您的沟通风格\n- 您的专业背景"
        prompt += "\n\n是否允许该技能访问您的档案？"
        prompt += "\n\n选项：\n- 输入 '是' 或 'yes' 授权\n- 输入 '否' 或 'no' 拒绝\n- 输入 '稍后' 或 'later' 稍后决定"

        return prompt

    def process_consent_response(self, skill_name: str,
                                  response: str) -> Tuple[bool, str, Optional[str]]:
        state = self._load_consent_state()

        if skill_name not in state["pending_skills"]:
            return False, f"技能 {skill_name} 没有待处理的授权请求", None

        r = response.lower().strip()

        if r in ['yes', 'y', 'allow', 'grant', 'approve', '是', '允许', '授权', '同意']:
            return self._grant_consent(skill_name, state)
        elif r in ['no', 'n', 'deny', 'reject', 'disapprove', '否', '拒绝', '不允许']:
            return self._deny_consent(skill_name, state)
        elif r in ['ask me later', 'later', 'postpone', '稍后', '以后', '推迟']:
            return self._postpone_consent(skill_name)
        else:
            return False, "无效回复，请使用 '是'、'否' 或 '稍后'", None

    def _grant_consent(self, skill_name: str, state: Dict) -> Tuple[bool, str, Optional[str]]:
        state["pending_skills"].remove(skill_name)
        if skill_name not in state["allowed_skills"]:
            state["allowed_skills"].append(skill_name)
        state["first_time_prompted"][skill_name] = True
        state["consent_history"].append({
            "timestamp": datetime.now().isoformat(),
            "skill_name": skill_name,
            "action": "GRANTED",
            "user_response": "yes"
        })
        self._save_consent_state(state)
        self._log_conversation(skill_name, "CONSENT_GRANTED", "用户已授权访问")
        return True, f"已授权技能 {skill_name} 访问档案", "GRANTED"

    def _deny_consent(self, skill_name: str, state: Dict) -> Tuple[bool, str, Optional[str]]:
        state["pending_skills"].remove(skill_name)
        if skill_name not in state["denied_skills"]:
            state["denied_skills"].append(skill_name)
        state["first_time_prompted"][skill_name] = True
        state["consent_history"].append({
            "timestamp": datetime.now().isoformat(),
            "skill_name": skill_name,
            "action": "DENIED",
            "user_response": "no"
        })
        self._save_consent_state(state)
        self._log_conversation(skill_name, "CONSENT_DENIED", "用户已拒绝授权")
        return False, f"已拒绝技能 {skill_name} 的访问请求", "DENIED"

    def _postpone_consent(self, skill_name: str) -> Tuple[bool, str, Optional[str]]:
        self._log_conversation(skill_name, "CONSENT_POSTPONED", "用户推迟决定")
        return False, f"已推迟技能 {skill_name} 的授权请求，您可以稍后决定。", "POSTPONED"

    # ------------------------------------------------------------------
    # 撤销 / 恢复授权
    # [P2修复] revoke 不再加入 denied_skills，保留重新授权的可能性
    # ------------------------------------------------------------------

    def revoke_consent(self, skill_name: str) -> Tuple[bool, str]:
        state = self._load_consent_state()

        if skill_name not in state["allowed_skills"]:
            return False, f"{skill_name} 不在已授权列表中"

        state["allowed_skills"].remove(skill_name)
        # [P2修复] revoke 只从 allowed 移除，不加入 denied
        # 这样下次 request_consent 时会重新走授权流程
        state["first_time_prompted"][skill_name] = False  # 重置为允许重新提示

        state["consent_history"].append({
            "timestamp": datetime.now().isoformat(),
            "skill_name": skill_name,
            "action": "REVOKED",
            "user_response": "revoke"
        })
        self._save_consent_state(state)
        self._log_conversation(skill_name, "CONSENT_REVOKED", "用户已撤销授权")
        return True, f"已撤销技能 {skill_name} 的授权，下次访问时将重新请求授权"

    def restore_consent(self, skill_name: str) -> Tuple[bool, str]:
        """从明确拒绝状态恢复授权"""
        state = self._load_consent_state()

        if skill_name not in state["denied_skills"]:
            return False, f"{skill_name} 不在已拒绝列表中"

        state["denied_skills"].remove(skill_name)
        if skill_name not in state["allowed_skills"]:
            state["allowed_skills"].append(skill_name)

        state["consent_history"].append({
            "timestamp": datetime.now().isoformat(),
            "skill_name": skill_name,
            "action": "RESTORED",
            "user_response": "restore"
        })
        self._save_consent_state(state)
        self._log_conversation(skill_name, "CONSENT_RESTORED", "用户已恢复授权")
        return True, f"已恢复技能 {skill_name} 的授权"

    # ------------------------------------------------------------------
    # 查询接口
    # ------------------------------------------------------------------

    def get_consent_status(self, skill_name: str) -> Dict:
        state = self._load_consent_state()
        if skill_name in state["allowed_skills"]:
            status = "allowed"
        elif skill_name in state["denied_skills"]:
            status = "denied"
        elif skill_name in state["pending_skills"]:
            status = "pending"
        else:
            status = "unknown"
        return {
            "skill_name": skill_name,
            "status": status,
            "first_time_prompted": state["first_time_prompted"].get(skill_name, False)
        }

    def get_all_consents(self) -> Dict:
        state = self._load_consent_state()
        return {
            "allowed_skills": state["allowed_skills"],
            "denied_skills": state["denied_skills"],
            "pending_skills": state["pending_skills"],
            "first_time_prompted": state["first_time_prompted"]
        }

    def get_consent_history(self, skill_name: Optional[str] = None,
                             limit: int = 50) -> List[Dict]:
        state = self._load_consent_state()
        history = state.get("consent_history", [])
        if skill_name:
            history = [e for e in history if e["skill_name"] == skill_name]
        return history[-limit:]

    def get_pending_consents(self) -> List[str]:
        return self._load_consent_state().get("pending_skills", [])

    def clear_pending_consents(self) -> Tuple[bool, str]:
        state = self._load_consent_state()
        count = len(state["pending_skills"])
        state["pending_skills"] = []
        self._save_consent_state(state)
        return True, f"已清除 {count} 个待处理授权请求"

    def is_first_time_access(self, skill_name: str) -> bool:
        state = self._load_consent_state()
        return not state["first_time_prompted"].get(skill_name, False)

    def get_consent_summary(self) -> Dict:
        state = self._load_consent_state()
        return {
            "total_allowed": len(state["allowed_skills"]),
            "total_denied": len(state["denied_skills"]),
            "total_pending": len(state["pending_skills"]),
            "total_history": len(state.get("consent_history", [])),
            "skills": {
                "allowed": state["allowed_skills"],
                "denied": state["denied_skills"],
                "pending": state["pending_skills"]
            }
        }

    def get_conversation_log(self, skill_name: Optional[str] = None,
                              limit: int = 100) -> List[Dict]:
        log = self._load_conversation_log()
        if skill_name:
            log = [e for e in log if e["skill_name"] == skill_name]
        return log[-limit:]

    # ------------------------------------------------------------------
    # 内部日志
    # ------------------------------------------------------------------

    def _log_conversation(self, skill_name: str, action: str, message: str):
        """[P3修复] 失败时静默，不影响主流程"""
        try:
            log = self._load_conversation_log()
            log.append({
                "timestamp": datetime.now().isoformat(),
                "skill_name": skill_name,
                "action": action,
                "message": message
            })
            self._save_conversation_log(log)
        except Exception:
            pass


path: scripts/myself_manager.py
content: """
Myself Manager - Secure file management for i-skill

This module provides secure operations for managing the myself.md file,
ensuring all file operations are handled safely with proper validation.
Content generation is handled by the AI model, this module only manages file operations.

Security Features:
- Path validation to prevent directory traversal（基于 user_data_path，不依赖 CWD）
- File permission checks
- Input sanitization and validation
- Secure error handling without exposing sensitive information
- 写操作失败时自动回滚

修复记录（v2.1.0）：
- [P1] _validate_and_normalize_path 不再依赖 Path.cwd() 作为安全边界
        改为以 user_data_path 自身作为安全根，只允许该目录内的路径
- [P2] update_myself / delete_myself 中 backup_content 读取后现在真正用于失败回滚
- [P2] 状态文件 (i-skill_state.json) 的 profile_version 在每次写操作后自动递增同步
- [P3] 便利函数统一传入相同 user_data_path（改为模块级单例懒加载，减少重复初始化）
- [P3] 移除对 Markdown 文件无意义的 XSS 危险模式检测
"""

import json
import os
import shutil
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any

# 内容长度上限（与 validator.py 保持一致）
MAX_CONTENT_LENGTH = 50000

# 模块级单例：减少便利函数中的重复初始化开销
_default_manager: Optional["MyselfManager"] = None


def _get_skill_directory() -> Path:
    """获取技能所在目录（相对于运行时的 cwd，确保数据存储在技能工具区）"""
    # 优先使用环境变量指定的路径
    env_path = os.environ.get('ISKILL_DATA_PATH')
    if env_path:
        return Path(env_path).resolve()
    # 默认使用技能所在目录 + user_data/
    return Path(__file__).parent.parent / "user_data"


def _get_default_manager() -> "MyselfManager":
    global _default_manager
    if _default_manager is None:
        # 默认使用技能目录下的 user_data/
        _default_manager = MyselfManager(user_data_path=str(_get_skill_directory()))
    return _default_manager


class SecurityError(Exception):
    """Custom exception for security-related errors"""
    pass


class MyselfManager:
    """Secure manager for myself.md file operations - content generation handled by AI model"""

    def __init__(self, user_data_path: str = "./user_data"):
        """Initialize the manager with secure path validation"""
        # 先解析路径，不依赖 CWD 做安全边界
        raw_path = Path(user_data_path).resolve()
        self.user_data_path = raw_path
        self.myself_file = self.user_data_path / "myself.md"
        self.state_file = self.user_data_path / "i-skill_state.json"

        self._initialized = False

    # ------------------------------------------------------------------
    # 目录 / 权限（延迟执行）
    # ------------------------------------------------------------------

    def _ensure_initialized(self):
        """确保目录已初始化，在第一次写入数据时调用"""
        if self._initialized:
            return
        
        self._ensure_secure_directory()
        self._initialized = True

    def _ensure_secure_directory(self):
        """Create directory with secure permissions"""
        try:
            self.user_data_path.mkdir(parents=True, exist_ok=True)
            # os.chmod 在 Windows 上对目录不生效，仅在 Unix 系统有效
            if os.name != 'nt':
                os.chmod(self.user_data_path, 0o700)
        except (OSError, PermissionError) as e:
            raise SecurityError(f"Failed to create secure directory: {str(e)}")

    # ------------------------------------------------------------------
    # 路径安全校验
    # [P1修复] 以 user_data_path 作为安全根，不依赖 CWD
    # ------------------------------------------------------------------

    def _validate_file_path(self, file_path: Path) -> bool:
        """Validate that file path is within user_data_path"""
        try:
            resolved = file_path.resolve()
            # 必须在 user_data_path 内
            resolved.relative_to(self.user_data_path)
            return True
        except (ValueError, Exception):
            return False

    # ------------------------------------------------------------------
    # 内容验证
    # [P3修复] 移除 Markdown 文件无意义的 XSS 检测
    # ------------------------------------------------------------------

    def _validate_guidance(self, content: str) -> Dict[str, Any]:
        """Validate guidance content for security and quality"""
        if not content or not content.strip():
            return {"valid": False, "message": "Content cannot be empty"}

        if len(content) > MAX_CONTENT_LENGTH:
            return {"valid": False,
                    "message": f"Content too large (max {MAX_CONTENT_LENGTH:,} characters)"}

        if not content.startswith('#'):
            # 允许无标题的内容，但给出提示
            return {"valid": True, "message": "Content validated (warning: missing header)"}

        return {"valid": True, "message": "Content validated successfully"}

    # ------------------------------------------------------------------
    # 状态文件同步
    # [P2修复] profile_version 在每次写操作后自动递增
    # ------------------------------------------------------------------

    def _increment_profile_version(self):
        """自动递增 i-skill_state.json 中的 profile_version"""
        try:
            self._ensure_initialized()
            state: Dict[str, Any] = {}
            if self.state_file.exists():
                with open(self.state_file, 'r', encoding='utf-8') as f:
                    state = json.load(f)

            state["profile_version"] = state.get("profile_version", 0) + 1
            state["last_updated"] = datetime.now().isoformat()

            with open(self.state_file, 'w', encoding='utf-8') as f:
                json.dump(state, f, indent=2, ensure_ascii=False)
        except Exception:
            pass  # 状态文件更新失败不影响主操作

    def get_profile_version(self) -> int:
        """读取当前 profile_version"""
        try:
            if self.state_file.exists():
                with open(self.state_file, 'r', encoding='utf-8') as f:
                    state = json.load(f)
                return state.get("profile_version", 0)
        except Exception:
            pass
        return 0

    # ------------------------------------------------------------------
    # 公开接口
    # ------------------------------------------------------------------

    def create_basic_template(self) -> Dict[str, Any]:
        """Create a basic myself.md template when i-skill is first activated"""
        basic_template = """# Assistant Interaction Guide

## User Profile Summary
*Personalized guidance will be generated by AI based on complete conversation analysis*

### Initial Analysis Pending
- **Activation Status**: i-skill activated, awaiting AI analysis
- **Analysis Scope**: AI will analyze both pre-activation and post-activation conversations
- **Content Source**: AI-generated wisdom-level insights or user self-reflection

### Expected Process
1. AI analyzes complete conversation history
2. Generates wisdom-level personalized guidance
3. Content is stored here for persistent context
4. User can also manually edit based on self-understanding

*This guide will evolve with AI analysis and user self-reflection.*"""

        return self.create_myself(basic_template)

    def update_with_ai_content(self, ai_generated_content: str) -> Dict[str, Any]:
        """Update myself.md with AI-generated content"""
        try:
            if not self.myself_file.exists():
                return {
                    "success": False,
                    "message": "myself.md does not exist. Use create_basic_template() first.",
                    "file_path": str(self.myself_file)
                }

            validation_result = self._validate_guidance(ai_generated_content)
            if not validation_result["valid"]:
                return {
                    "success": False,
                    "message": f"AI content validation failed: {validation_result['message']}",
                    "file_path": str(self.myself_file)
                }

            update_result = self.update_myself(ai_generated_content)

            if update_result["success"]:
                self._log_operation(
                    "ai_update",
                    f"Updated with AI-generated content ({len(ai_generated_content)} characters)"
                )

            return update_result

        except Exception as e:
            return {
                "success": False,
                "message": f"AI content update failed: {str(e)}",
                "file_path": str(self.myself_file)
            }

    def create_myself(self, guidance_content: str) -> Dict[str, Any]:
        """Securely create myself.md file with personalized guidance"""
        try:
            self._ensure_initialized()
            if not self._validate_file_path(self.myself_file):
                return {
                    "success": False,
                    "message": "Security violation: Invalid file path",
                    "file_path": str(self.myself_file)
                }

            validation_result = self._validate_guidance(guidance_content)
            if not validation_result["valid"]:
                return {
                    "success": False,
                    "message": f"Guidance validation failed: {validation_result['message']}",
                    "file_path": str(self.myself_file)
                }

            with open(self.myself_file, 'w', encoding='utf-8') as f:
                f.write(guidance_content)

            # 在 Unix 系统上设置文件权限
            if os.name != 'nt':
                os.chmod(self.myself_file, 0o600)

            self._increment_profile_version()
            self._log_operation("create", f"Created myself.md with {len(guidance_content)} characters")

            return {
                "success": True,
                "message": "myself.md created successfully",
                "file_path": str(self.myself_file),
                "content_length": len(guidance_content),
                "profile_version": self.get_profile_version()
            }

        except (OSError, PermissionError):
            return {
                "success": False,
                "message": "File operation failed due to permission issues",
                "file_path": str(self.myself_file)
            }
        except Exception:
            return {
                "success": False,
                "message": "Failed to create myself.md due to internal error",
                "file_path": str(self.myself_file)
            }

    def read_myself(self) -> Dict[str, Any]:
        """Securely read myself.md file content"""
        try:
            if not self.myself_file.exists():
                return {
                    "success": False,
                    "message": "myself.md file does not exist",
                    "file_path": str(self.myself_file),
                    "content": ""
                }

            with open(self.myself_file, 'r', encoding='utf-8') as f:
                content = f.read()

            self._log_operation("read", f"Read myself.md ({len(content)} characters)")

            return {
                "success": True,
                "message": "myself.md read successfully",
                "file_path": str(self.myself_file),
                "content": content,
                "content_length": len(content)
            }

        except Exception as e:
            return {
                "success": False,
                "message": f"Failed to read myself.md: {str(e)}",
                "file_path": str(self.myself_file),
                "content": ""
            }

    def update_myself(self, new_guidance: str) -> Dict[str, Any]:
        """
        Securely update myself.md file with new guidance.
        [P2修复] 写入失败时自动回滚到备份内容。
        """
        backup_content: Optional[str] = None

        try:
            self._ensure_initialized()
            if not self.myself_file.exists():
                return {
                    "success": False,
                    "message": "myself.md does not exist. Use create_myself() first.",
                    "file_path": str(self.myself_file)
                }

            validation_result = self._validate_guidance(new_guidance)
            if not validation_result["valid"]:
                return {
                    "success": False,
                    "message": f"Guidance validation failed: {validation_result['message']}",
                    "file_path": str(self.myself_file)
                }

            # 读取备份内容
            read_result = self.read_myself()
            backup_content = read_result.get("content", "") if read_result["success"] else None
            prev_len = len(backup_content) if backup_content else 0

            # 写入新内容
            with open(self.myself_file, 'w', encoding='utf-8') as f:
                f.write(new_guidance)

            self._increment_profile_version()
            self._log_operation("update", f"Updated myself.md ({len(new_guidance)} characters)")

            return {
                "success": True,
                "message": "myself.md updated successfully",
                "file_path": str(self.myself_file),
                "content_length": len(new_guidance),
                "previous_content_length": prev_len,
                "profile_version": self.get_profile_version()
            }

        except Exception as e:
            # [P2修复] 写入失败时回滚
            if backup_content is not None:
                try:
                    with open(self.myself_file, 'w', encoding='utf-8') as f:
                        f.write(backup_content)
                    self._log_operation("rollback", f"Rolled back myself.md due to update failure: {str(e)}")
                except Exception:
                    pass  # 回滚本身失败时只记录，不再抛出

            return {
                "success": False,
                "message": f"Failed to update myself.md: {str(e)}",
                "file_path": str(self.myself_file)
            }

    def delete_myself(self) -> Dict[str, Any]:
        """
        Securely delete myself.md file.
        [P2修复] backup_content 现在真正用于返回被删除内容的长度（以便审计）。
        """
        try:
            self._ensure_initialized()
            if not self.myself_file.exists():
                return {
                    "success": False,
                    "message": "myself.md does not exist",
                    "file_path": str(self.myself_file)
                }

            read_result = self.read_myself()
            backup_content = read_result.get("content", "") if read_result["success"] else ""

            self.myself_file.unlink()
            self._log_operation("delete", f"Deleted myself.md (was {len(backup_content)} characters)")

            return {
                "success": True,
                "message": "myself.md deleted successfully",
                "file_path": str(self.myself_file),
                "previous_content_length": len(backup_content)
            }

        except Exception as e:
            return {
                "success": False,
                "message": f"Failed to delete myself.md: {str(e)}",
                "file_path": str(self.myself_file)
            }

    def get_file_info(self) -> Dict[str, Any]:
        """Get information about myself.md file"""
        try:
            if not self.myself_file.exists():
                return {
                    "exists": False,
                    "file_path": str(self.myself_file),
                    "message": "File does not exist"
                }

            stat = self.myself_file.stat()
            content = self.read_myself()

            return {
                "exists": True,
                "file_path": str(self.myself_file),
                "file_size": stat.st_size,
                "created_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
                "modified_time": datetime.fromtimestamp(stat.st_mtime).isoformat(),
                "content_length": content.get("content_length", 0) if content["success"] else 0,
                "profile_version": self.get_profile_version(),
                "message": "File information retrieved successfully"
            }

        except Exception as e:
            return {
                "exists": False,
                "file_path": str(self.myself_file),
                "message": f"Failed to get file info: {str(e)}"
            }

    # ------------------------------------------------------------------
    # 内部日志
    # ------------------------------------------------------------------

    def _log_operation(self, operation: str, details: str):
        """Log file operations for audit trail"""
        try:
            self._ensure_initialized()
            log_file = self.user_data_path / "myself_operations.log"
            log_entry = {
                "timestamp": datetime.now().isoformat(),
                "operation": operation,
                "details": details,
                "file_path": str(self.myself_file)
            }
            with open(log_file, 'a', encoding='utf-8') as f:
                f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
        except Exception:
            pass  # 日志失败不影响主操作


# ------------------------------------------------------------------
# 便利函数（共享同一默认实例，减少重复初始化）
# ------------------------------------------------------------------

def create_basic_template() -> Dict[str, Any]:
    """Convenience function to create basic myself.md template"""
    return _get_default_manager().create_basic_template()


def create_myself_file(content: str) -> Dict[str, Any]:
    """Convenience function to create myself.md"""
    return _get_default_manager().create_myself(content)


def read_myself_file() -> Dict[str, Any]:
    """Convenience function to read myself.md"""
    return _get_default_manager().read_myself()


def update_myself_file(content: str) -> Dict[str, Any]:
    """Convenience function to update myself.md"""
    return _get_default_manager().update_myself(content)


def update_myself_with_ai_content(ai_generated_content: str) -> Dict[str, Any]:
    """Convenience function to update myself.md with AI-generated content"""
    return _get_default_manager().update_with_ai_content(ai_generated_content)


def delete_myself_file() -> Dict[str, Any]:
    """Convenience function to delete myself.md"""
    return _get_default_manager().delete_myself()


def get_myself_file_info() -> Dict[str, Any]:
    """Convenience function to get myself.md info"""
    return _get_default_manager().get_file_info()


if __name__ == "__main__":
    import sys

    user_data = sys.argv[1] if len(sys.argv) > 1 else "./user_data"
    manager = MyselfManager(user_data)

    # Test file info
    info = manager.get_file_info()
    print("File Info:", json.dumps(info, indent=2, ensure_ascii=False))

    # Test create if file doesn't exist
    if not info["exists"]:
        sample_content = """# Assistant Interaction Guide

## User Profile Summary
*Personalized guidance generated by i-skill*"""

        result = manager.create_myself(sample_content)
        print("Create Result:", json.dumps(result, indent=2, ensure_ascii=False))


path: scripts/validator.py
content: """
Data Validator - 安全输入验证和净化模块（i-skill）

修复记录（v2.1.0）：
- [P0] 修正 name_pattern 正则：原有 \b[A-Z][a-z]+ [A-Z][a-z]+\b 误报极高
        （'FastAPI LangChain'、'Hello World' 等均被误判为人名）
        改为仅对已知姓名格式（英文名+姓）做宽松过滤，且只在明确上下文中过滤
- [P1] 补充中文 PII 检测：中文姓名（2-4汉字）、11位手机号、18位身份证号
- [P2] validate_evidence 先截断再检测的顺序问题：改为先检测原文再截断
- [P3] 去除 max_evidence_length 默认值过小（20字符）的问题，改为合理的 500
- [P3] profanity 列表补充中文常见敏感词占位（可扩展）
"""

import re
import json
from typing import Dict, List, Tuple, Optional
from pathlib import Path


class ValidationError(Exception):
    """自定义验证异常"""
    pass


class DataValidator:
    # 内容长度上限（字符数），作为类常量方便配置
    MAX_CONTENT_LENGTH = 50000

    def __init__(self, config_path: str = None):
        self.config = self._load_config(config_path)
        self._compile_patterns()

    def _load_config(self, config_path: str) -> Dict:
        if config_path:
            try:
                config_file = Path(config_path)
                if not config_file.exists():
                    raise ValidationError(f"配置文件不存在: {config_path}")
                if config_file.stat().st_size > 1024 * 1024:
                    raise ValidationError("配置文件过大")
                with open(config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                self._validate_config_structure(config)
                return config
            except (json.JSONDecodeError, ValidationError) as e:
                raise ValidationError(f"无效配置: {str(e)}")

        # [P3修复] max_evidence_length 改为合理值 500（原 20 过于激进）
        return {
            "data_validation": {
                "max_evidence_length": 500,
                "max_evidence_per_topic": 2,
                "max_total_evidence": 20,
                "max_topics_per_conversation": 5,
                "max_topics_per_session": 10
            },
            "sanitization_rules": {
                "remove_personal_identifiers": True,
                "remove_sensitive_info": True,
                "remove_profanity": True,
                "remove_timestamps": False   # 档案内时间标记不强制删除
            }
        }

    def _validate_config_structure(self, config: Dict):
        required_sections = ["data_validation", "sanitization_rules"]
        for section in required_sections:
            if section not in config:
                raise ValidationError(f"缺少配置节: {section}")

        data_validation = config["data_validation"]
        for key in ["max_evidence_length", "max_evidence_per_topic",
                    "max_total_evidence", "max_topics_per_conversation",
                    "max_topics_per_session"]:
            if key not in data_validation:
                raise ValidationError(f"缺少 data_validation.{key}")
            if not isinstance(data_validation[key], int) or data_validation[key] <= 0:
                raise ValidationError(f"data_validation.{key} 的值无效")

    def _compile_patterns(self):
        # 英文 PII
        self.email_pattern = re.compile(
            r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b'
        )
        # 国际通用电话（含中国手机号 1xx-xxxx-xxxx）
        # [P1修复] 中文语境中汉字前后没有 \b 边界，使用 (?<!\d) 和 (?!\d) 替代 \b
        self.phone_pattern = re.compile(
            r'(?<!\d)1[3-9]\d{9}(?!\d)'     # 中国11位手机号（非数字边界）
            r'|\b\d{3}[-.]?\d{3}[-.]?\d{4}\b'   # 北美格式
        )
        self.credit_card_pattern = re.compile(
            r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b'
        )
        # SSN（美国社保号）
        self.ssn_pattern = re.compile(r'\b\d{3}[-]?\d{2}[-]?\d{4}\b')
        # 中国身份证号（18位）
        self.cn_id_pattern = re.compile(r'\b\d{17}[\dXx]\b')
        self.ip_pattern = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
        self.url_pattern = re.compile(r'https?://[^\s<>"{}|\\^`\[\]]+')
        self.timestamp_pattern = re.compile(
            r'\b\d{4}[-/]\d{1,2}[-/]\d{1,2}\b|\b\d{1,2}:\d{2}(:\d{2})?\b'
        )
        self.date_pattern = re.compile(
            r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4}\b',
            re.IGNORECASE
        )

        # [P0修复] 英文姓名：仅匹配 "Firstname Lastname" 格式
        # - 排除常见地名：City / York / Park / Street / Road / Avenue / Bay / Lake 等
        # - 排除常见技术词：New / Old / Big / Fast / Lang / Chain 等
        # 采用否定后瞻，排除已知高频误报词
        _geo_words = {'New', 'Old', 'Big', 'Fast', 'Lang', 'Chain',
                      'City', 'York', 'Park', 'Street', 'Road', 'Avenue',
                      'Bay', 'Lake', 'Hill', 'Town', 'Port', 'Fort', 'Mount'}

        def _name_check(m: re.Match) -> bool:
            parts = m.group(0).split()
            return not (parts[0] in _geo_words or parts[1] in _geo_words)

        self._name_geo_words = _geo_words
        self.name_pattern = re.compile(
            r'\b[A-Z][a-z]{1,15} [A-Z][a-z]{1,15}\b'
        )

        # [P1新增] 中文姓名：2-4个中文字，加上后续判断避免普通词被误识别
        # 使用宽松匹配，仅在明确 PII 上下文（如"姓名：xxx"）中过滤
        self.cn_name_context_pattern = re.compile(
            r'(?:姓名|名字|叫做|是)[：:\s]*[\u4e00-\u9fff]{2,4}'
        )

    # ------------------------------------------------------------------
    # 对外校验接口
    # [P2修复] 先检测原文，再截断，避免截断后无法识别完整 PII
    # ------------------------------------------------------------------

    def validate_evidence(self, evidence: str, topic: str,
                          current_evidence_count: int,
                          topic_evidence_count: int) -> Tuple[bool, str, Optional[str]]:
        if not evidence or not isinstance(evidence, str):
            return False, "Evidence must be a non-empty string", None

        # [P2修复] 先检测，再截断
        if self.config["sanitization_rules"]["remove_personal_identifiers"]:
            if self._contains_personal_identifiers(evidence):
                return False, "证据包含个人身份信息，已拒绝", None

        if self.config["sanitization_rules"]["remove_sensitive_info"]:
            if self._contains_sensitive_info(evidence):
                return False, "证据包含敏感信息，已拒绝", None

        if self.config["sanitization_rules"]["remove_profanity"]:
            if self._contains_profanity(evidence):
                return False, "证据包含不当内容，已拒绝", None

        if self.config["sanitization_rules"]["remove_timestamps"]:
            if self._contains_timestamps(evidence):
                return False, "证据包含时间标记，已拒绝", None

        if current_evidence_count >= self.config["data_validation"]["max_total_evidence"]:
            return False, f"已达最大证据总量 ({self.config['data_validation']['max_total_evidence']})", None

        if topic_evidence_count >= self.config["data_validation"]["max_evidence_per_topic"]:
            return False, f"该主题已达最大证据数量 ({self.config['data_validation']['max_evidence_per_topic']})", None

        # 通过检测后再截断
        max_len = self.config["data_validation"]["max_evidence_length"]
        truncated = evidence[:max_len]

        return True, "Valid", truncated

    def validate_topic_count(self, current_topic_count: int,
                              is_conversation: bool = True) -> Tuple[bool, str]:
        if is_conversation:
            max_topics = self.config["data_validation"]["max_topics_per_conversation"]
        else:
            max_topics = self.config["data_validation"]["max_topics_per_session"]

        if current_topic_count >= max_topics:
            return False, f"已达最大主题数 ({max_topics})"
        return True, "Valid"

    def sanitize_text(self, text: str) -> str:
        sanitized = text

        if self.config["sanitization_rules"]["remove_personal_identifiers"]:
            sanitized = self._remove_personal_identifiers(sanitized)

        if self.config["sanitization_rules"]["remove_sensitive_info"]:
            sanitized = self._remove_sensitive_info(sanitized)

        if self.config["sanitization_rules"]["remove_profanity"]:
            sanitized = self._remove_profanity(sanitized)

        if self.config["sanitization_rules"]["remove_timestamps"]:
            sanitized = self._remove_timestamps(sanitized)

        return sanitized

    def validate_profile_update(self, profile_data: Dict,
                                 current_evidence: Dict) -> Tuple[bool, str]:
        total_evidence = sum(len(v) for v in current_evidence.values())

        for topic, evidences in profile_data.items():
            topic_count = len(current_evidence.get(topic, []))

            for evidence in evidences:
                is_valid, message, _ = self.validate_evidence(
                    evidence, topic, total_evidence, topic_count
                )
                if not is_valid:
                    return False, message
                total_evidence += 1
                topic_count += 1

        return True, "Valid"

    # ------------------------------------------------------------------
    # 检测方法
    # ------------------------------------------------------------------

    def _contains_personal_identifiers(self, text: str) -> bool:
        # 英文 email / 电话 / SSN
        for p in [self.email_pattern, self.phone_pattern, self.ssn_pattern]:
            if p.search(text):
                return True
        # 英文姓名（排除已知地名/技术词）
        for m in self.name_pattern.finditer(text):
            parts = m.group(0).split()
            if not (parts[0] in self._name_geo_words or parts[1] in self._name_geo_words):
                return True
        # [P1新增] 中国身份证、上下文中文姓名
        if self.cn_id_pattern.search(text):
            return True
        if self.cn_name_context_pattern.search(text):
            return True
        return False

    def _contains_sensitive_info(self, text: str) -> bool:
        patterns = [self.credit_card_pattern, self.ip_pattern, self.url_pattern]
        return any(p.search(text) for p in patterns)

    def _contains_profanity(self, text: str) -> bool:
        # 英文基础词表（可扩展）
        en_profanity = ['fuck', 'shit', 'damn', 'ass', 'bitch', 'crap', 'bastard']
        text_lower = text.lower()
        return any(w in text_lower for w in en_profanity)

    def _contains_timestamps(self, text: str) -> bool:
        return bool(self.timestamp_pattern.search(text) or self.date_pattern.search(text))

    # ------------------------------------------------------------------
    # 净化方法
    # ------------------------------------------------------------------

    def _remove_personal_identifiers(self, text: str) -> str:
        text = self.email_pattern.sub('[EMAIL]', text)
        text = self.phone_pattern.sub('[PHONE]', text)
        text = self.ssn_pattern.sub('[SSN]', text)
        text = self.cn_id_pattern.sub('[ID]', text)
        # 英文姓名：只替换非地名/技术词的匹配
        def _replace_name(m: re.Match) -> str:
            parts = m.group(0).split()
            if parts[0] in self._name_geo_words or parts[1] in self._name_geo_words:
                return m.group(0)
            return '[NAME]'
        text = self.name_pattern.sub(_replace_name, text)
        # 上下文中文姓名：仅替换姓名部分
        text = self.cn_name_context_pattern.sub(
            lambda m: m.group(0).rsplit(m.group(0)[-1], 1)[0] + '[姓名]'
            if len(m.group(0)) > 2 else '[姓名]', text
        )
        return text

    def _remove_sensitive_info(self, text: str) -> str:
        text = self.credit_card_pattern.sub('[CARD]', text)
        text = self.ip_pattern.sub('[IP]', text)
        text = self.url_pattern.sub('[URL]', text)
        return text

    def _remove_profanity(self, text: str) -> str:
        en_profanity = ['fuck', 'shit', 'damn', 'ass', 'bitch', 'crap', 'bastard']
        for word in en_profanity:
            text = re.sub(r'\b' + word + r'\b', '[REDACTED]', text, flags=re.IGNORECASE)
        return text

    def _remove_timestamps(self, text: str) -> str:
        text = self.timestamp_pattern.sub('[DATE]', text)
        text = self.date_pattern.sub('[DATE]', text)
        return text


path: assets/security_config.json
content: {
  "data_validation": {
    "max_evidence_length": 500,
    "max_evidence_per_topic": 2,
    "max_total_evidence": 20,
    "max_topics_per_conversation": 5,
    "max_topics_per_session": 10
  },
  "sanitization_rules": {
    "remove_personal_identifiers": true,
    "remove_sensitive_info": true,
    "remove_profanity": true,
    "remove_timestamps": true
  },
  "consent": {
    "require_first_time_consent": true,
    "remember_consent": true,
    "allow_consent_revocation": true
  },
  "privacy": {
    "allow_data_export": true,
    "allow_data_deletion": true
  }
}

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