Skill: nic-yuan/apollo-dream (version 2.0.0)

--- name: apollo-dream description: > 像睡觉做梦一样整理记忆，把重要的留下，不重要的忘掉。 version: 2.0.0 read_when: - 对话超过20轮时 - Token快用完时 - 需要整理上下文时 - 任务完成后需要总结时 - AI开始表现迟钝时 - 需要建立记忆关联时 metadata: openclaw: emoji: "🌙" requires: bins: [] env: [] triggers: - 做梦 - 记忆整理 - 遗忘 - 记忆关联 - 上下文管理 - 上下文压缩 suite: apollo --- # Apollo Dream - 做梦记忆机制 ## 核心准则 **AI的记忆管理应该像人类睡眠中的做梦机制：在梦中整理记忆、强化重要连接、忘却无关细节。** 人类睡眠时大脑在做什么： - **强化**：把短期记忆巩固成长期记忆 - **关联**：找到记忆和记忆之间的联系 - **忘却**：删除不重要的细节 - **重组**：把碎片化的记忆重新组织 ## 三级做梦机制 | 层级 | 名称 | 类比 | 做什么 | |------|------|------|--------| | 1 | Microcompact | 浅睡呼吸 | 快速删token，不动结构 | | 2 | Session Memory | REM睡眠整理 | 基于会话记忆压缩，保留结构 | | 3 | Traditional Compact | 深度睡眠重组 | 完整摘要，建立记忆关联 | ## 触发时机 **应该触发做梦的场景：** - 对话超过20轮 - Token使用超过70% - 任务完成时 - 发现多个记忆点需要关联时 - AI开始表现迟钝/重复 ## 做梦的操作 ### ✅ 能实现的 ``` 1. 提炼关键结论 → 写到文件 2. 删除中间过程 → 只保留首尾状态 3. 保留重要上下文 → 关键决策、用户偏好 4. 跨会话关联 → 发现新旧记忆的联系 5. 忘却噪音 → 删除不重要的细节 ``` ### ❌ 待实现的（未来工作） ``` 1. 自动发现记忆关联 - 需要：跨会话知识图谱 - 状态：待设计 2. 主动忘却机制 - 需要：重要性评分系统 - 状态：待设计 3. 记忆可视化 - 需要：定期输出记忆地图 - 状态：待设计 ``` ## 人类睡眠vs AI做梦 | 人类睡眠 | AI做梦 | |---------|--------| | 短期记忆→长期记忆 | 对话→文件 | | 记忆关联 | 知识图谱 | | 忘却不重要的事 | 丢弃token | | REM整理 | 分层压缩 | ## 任务完成后的做梦流程 ``` 任务完成 → 1. 提炼核心结论（what was done, key decisions） 2. 提取重要发现（insights worth remembering） 3. 记录未解决问题（for follow-up） 4. 检查是否有记忆关联（与旧记忆的联系） 5. 忘却噪音（不重要细节删除） 6. 写入适当文件（memory/YYYY-MM-DD.md 或专题文件） 7. 清理对话上下文 ``` ## 应用检查表 - [ ] 当前对话轮数是否超过20轮？ - [ ] Token使用是否超过70%？ - [ ] 当前任务是否已有明确结论可以提炼？ - [ ] 是否有应该写入文件的长期知识？ - [ ] 新记忆是否与旧记忆有关联？ ## 待实现功能（TODO） | 功能 | 描述 | 优先级 | |------|------|--------| | 记忆关联发现 | 自动发现新旧记忆之间的联系 | 高 | | 重要性评分 | 给每条记忆打分，决定保留/忘却 | 中 | | 主动忘却 | 不重要的记忆自动降级/删除 | 中 | | 记忆可视化 | 输出记忆地图供人类阅读 | 低 | ## 参考 来源：Claude Code 上下文管理机制 + 人类睡眠记忆研究，512,000行源码研究 --- ## v2.0 实施规格（2026-04-07） ### v2新功能 1. **Token精度算法**：使用字符/词/标点综合估算（scripts/token-estimator.py） 2. **信息密度检测**：识别高价值/低价值内容（scripts/density-detector.py） 3. **三层压缩**：Microcompact/Session Memory/Deep Compact（scripts/compressor.py） 4. **7天快照**：自动创建/清理记忆快照（scripts/snapshot.py） 5. **决策追踪**：识别并记录关键决策（scripts/decision-tracker.py） ### v1 实施规格（2026-04-06） > 以下是 v1 最小可运行版本的实现规格。 ### 触发指标（v1） | 指标 | 定义 | 阈值 | 检测方式 | |------|------|------|---------| | 工作记忆积压 | 有 next_step 但 stale>=3轮 的任务数 | ≥ 3 | task-state.sh | | 未闭合话题 | status=active/pending 的话题数 | ≥ 3 | topic-tracker.sh | | 决策滞留率 | 有决策点但≥2轮未决策的任务数 | ≥ 2 | task-state.sh | | Token使用率 | 当前会话token估算 | > 70000 | 对话历史文件大小 | **触发规则：** - 软触发：任一 backlog/unclosed/decision_latency 超过阈值 - 硬触发：Token > 70000（不可绕过） ### 独立裁判机制 心跳cron每次触发时，自动运行碎片化检测脚本： ``` heartbeat-logger.sh → 调用 dream-fragmentation-check.sh ``` 检测结果写入： - `/tmp/.dream-trigger` — 存在则主AI必须整理 - `/tmp/heartbeat-realtime.json` — 含 dream_trigger 字段 ### 任务状态管理（主AI调用） ```bash # 注册任务（开始时） dream/task-state.sh register <id> <goal> <next_step> # 推进了一步（重置stale轮次） dream/task-state.sh update <id> --advance # 已做决策 dream/task-state.sh update <id> --decide # 完成任务（移除） dream/task-state.sh close <id> # 所有任务stale轮次+1（每次心跳后） dream/task-state.sh stale # 列出所有任务 dream/task-state.sh list ``` ### 话题追踪（主AI调用） ```bash # 标记新话题开始 dream/topic-tracker.sh mark <topic_name> # 切换话题（自动pause旧话题） dream/topic-tracker.sh switch <new_topic> # 标记当前话题完成 dream/topic-tracker.sh done # 标记当前话题暂停 dream/topic-tracker.sh pause # 列出所有话题 dream/topic-tracker.sh list ``` ### v1 整理流程 当 `/tmp/.dream-trigger` 存在时，主AI必须执行以下整理流程： ``` Step 1: 全量扫描（只读） → 读取 task-state + topic-state + 对话历史 → 识别：决策、任务、偏好、未闭合回路 Step 2: 结构化抽取 → 输出：任务列表、决策日志、实体表 Step 3: 冲突检测 + 去重 → 发现：自相矛盾、重复定义、版本漂移 Step 4: 写入长期记忆 → memory/YYYY-MM-DD.md → 相关专题文件 Step 5: 状态决策（关键步骤） → 逐个任务判断： - 推进（advance）：更新 next_step，重置 stale=0 - 关闭（close）：放弃或已完成，移出任务列表 - 保持（keep）：无法推进但不能关闭，stale 归零但保留 → 逐个话题判断： - 完成（done）：话题结束，移出未闭合列表 - 暂停（pause）：保持暂停状态 - 激活（activate）：继续深入讨论 → 调用 task-state.sh / topic-tracker.sh 执行状态变更 Step 6: 上下文压缩替换 → 丢弃：过程噪音 → 保留：结论 + 任务状态快照 + 索引 Step 7: 清理trigger → rm /tmp/.dream-trigger → 通知主AI整理完成 ``` ### v1 不做的事（v2+） ``` ❌ 冲突检测 + 去重（v2） ❌ 跨会话关联（v2） ❌ 情绪处理（长期） ❌ 加权多维指标体系（v2+） ❌ 自动发现记忆关联（v2+） ❌ 整理后自动状态决策（v2）— 端到端测试发现缺失 ``` ### 实施文件清单 | 文件 | 作用 | |------|------| | scripts/dream/dream-fragmentation-check.sh | 碎片化检测主脚本 | | scripts/dream/task-state.sh | 任务状态管理 | | scripts/dream/topic-tracker.sh | 话题追踪 | | scripts/heartbeat-logger.sh | 集成到心跳 | | .dream/task-state.json | 任务状态存储 | | .dream/topic-state.json | 话题状态存储 | | .dream/metrics.json | 最近检测指标 | | /tmp/.dream-trigger | 触发标志文件 |

path: scripts/compressor.py
content: #!/usr/bin/env python3
"""
compressor.py - 三层做梦压缩
Layer 1: Microcompact（浅睡） - 快速删token
Layer 2: Session Memory（REM） - 保留结构
Layer 3: Deep Compact（深睡） - 完整摘要
"""
import os
import sys
import re
import json
from datetime import datetime

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from token-estimator import estimate_tokens_from_text
from density-detector import get_density_level, is_high_density

WORKFLOW_DIR = "/root/.openclaw/workspace/.workflow"
STATE_DIR = "/root/.openclaw/workspace/.dream"

def compress_micro(text):
    """
    Layer 1: Microcompact
    触发：token > 50000
    动作：快速删除过程噪音，保留最后2-3轮关键上下文
    """
    if not text:
        return text
    
    tokens = estimate_tokens_from_text(text)
    if tokens <= 50000:
        return text
    
    lines = text.split('\n')
    
    # 保留高密度行
    kept_lines = []
    dropped = 0
    
    for line in lines[-50:]:  # 保留最后50行
        if is_high_density(line) or len(line) < 20:
            kept_lines.append(line)
        else:
            dropped += 1
    
    result = '\n'.join(kept_lines)
    return f"[Microcompact: 删除了{len(lines)-len(kept_lines)}行]\n{result}"

def compress_session(text):
    """
    Layer 2: Session Memory
    触发：token > 80000
    动作：保留任务状态、决策点、用户偏好
    """
    if not text:
        return text
    
    tokens = estimate_tokens_from_text(text)
    if tokens <= 80000:
        return text
    
    # 提取高密度段落
    lines = text.split('\n')
    important = []
    
    for line in lines:
        if is_high_density(line):
            important.append(line)
    
    result = '\n'.join(important)
    return f"[Session Memory: {len(lines)}行→{len(important)}行]\n{result}"

def compress_deep(text):
    """
    Layer 3: Deep Compact
    触发：token > 120000
    动作：完整摘要，建立关联
    """
    if not text:
        return text
    
    tokens = estimate_tokens_from_text(text)
    if tokens <= 120000:
        return text
    
    # 提取所有决策和结论
    lines = text.split('\n')
    decisions = []
    actions = []
    
    for line in lines:
        level = get_density_level(line)
        if level == 'high':
            if re.search(r'决定|决策|结论', line):
                decisions.append(line)
            elif re.search(r'任务|下一步|做', line):
                actions.append(line)
    
    summary = f"""[Deep Compact摘要]
时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
原始行数: {len(lines)}
保留决策: {len(decisions)}条
保留行动: {len(actions)}条

=== 决策 ===
{chr(10).join(decisions[:10])}

=== 行动 ===
{chr(10).join(actions[:10])}
"""
    
    return summary

def compress(text, layer=None):
    """
    主压缩函数
    
    Args:
        text: 要压缩的文本
        layer: 指定层(1/2/3)，None=自动选择
    
    Returns:
        tuple: (压缩后文本, 实际layer)
    """
    if not text:
        return text, 0
    
    tokens = estimate_tokens_from_text(text)
    
    if layer is None:
        if tokens > 120000:
            layer = 3
        elif tokens > 80000:
            layer = 2
        elif tokens > 50000:
            layer = 1
        else:
            return text, 0
    
    if layer == 1:
        return compress_micro(text), 1
    elif layer == 2:
        return compress_session(text), 2
    elif layer == 3:
        return compress_deep(text), 3
    else:
        return text, 0

def main():
    if len(sys.argv) < 2:
        print("用法: compressor.py <file> [layer]")
        print("   layer: 1(micro) / 2(session) / 3(deep) / auto")
        sys.exit(1)
    
    filepath = sys.argv[1]
    layer = int(sys.argv[2]) if len(sys.argv) > 2 else None
    
    with open(filepath, 'r') as f:
        text = f.read()
    
    compressed, actual_layer = compress(text, layer)
    
    print(f"压缩前[REDACTED_SECRET])}")
    print(f"压缩后[REDACTED_SECRET])}")
    print(f"使用Layer: {actual_layer}")
    print(f"\n--- 压缩结果 ---")
    print(compressed[:500] if len(compressed) > 500 else compressed)

if __name__ == '__main__':
    main()


path: scripts/decision-tracker.py
content: #!/usr/bin/env python3
"""
decision-tracker.py - 决策点追踪
识别并记录对话中的关键决策
"""
import os
import json
import re
from datetime import datetime

STATE_DIR = "/root/.openclaw/workspace/.dream"
DECISION_FILE = os.path.join(STATE_DIR, "decisions.json")

# 决策相关模式
DECISION_PATTERNS = [
    (r'决定：(.+)', 'decision'),
    (r'结论是：(.+)', 'conclusion'),
    (r'选([ABCD])：(.+)', 'choice'),
    (r'确认：(.+)', 'confirmation'),
    (r'同意：(.+)', 'agreement'),
    (r'用(.+)方案', 'adopted'),
    (r'不做(.+)', 'rejected'),
]

def init_state():
    """初始化状态目录"""
    os.makedirs(STATE_DIR, exist_ok=True)
    if not os.path.exists(DECISION_FILE):
        with open(DECISION_FILE, 'w') as f:
            json.dump({'decisions': [], 'pending': []}, f)

def add_decision(text, source="unknown"):
    """添加决策"""
    init_state()
    
    decisions = []
    for pattern, label in DECISION_PATTERNS:
        matches = re.findall(pattern, text)
        for match in matches:
            decisions.append({
                'type': label,
                'content': match if isinstance(match, str) else ' '.join(match),
                'source': source,
                'timestamp': datetime.now().isoformat(),
            })
    
    if decisions:
        with open(DECISION_FILE, 'r') as f:
            state = json.load(f)
        
        state['decisions'].extend(decisions)
        
        with open(DECISION_FILE, 'w') as f:
            json.dump(state, f, ensure_ascii=False, indent=2)
    
    return decisions

def get_decisions(limit=10):
    """获取最近决策"""
    init_state()
    with open(DECISION_FILE, 'r') as f:
        state = json.load(f)
    
    return state['decisions'][-limit:]

def list_decisions():
    """列出所有决策"""
    decisions = get_decisions(limit=100)
    
    if not decisions:
        print("暂无决策记录")
        return
    
    print(f"共 {len(decisions)} 条决策:")
    for d in decisions:
        print(f"  [{d['type']}] {d['content'][:50]}")
        print(f"    {d['timestamp'][:19]} | 来源: {d['source']}")

def extract_decisions_from_text(text):
    """
    从文本中提取决策点
    
    Returns:
        list: 提取到的决策
    """
    decisions = []
    for pattern, label in DECISION_PATTERNS:
        matches = re.findall(pattern, text)
        for match in matches:
            content = match if isinstance(match, str) else ' '.join(match)
            decisions.append({
                'type': label,
                'content': content,
                'matched_by': pattern,
            })
    return decisions

def main():
    import sys
    
    if len(sys.argv) < 2:
        print("用法: decision-tracker.py <add|list|extract> [args]")
        print("  add <text>")
        print("  list")
        print("  extract <text>")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    if cmd == 'add':
        if len(sys.argv) < 3:
            print("用法: decision-tracker.py add <text>")
            sys.exit(1)
        text = ' '.join(sys.argv[2:])
        decisions = add_decision(text)
        print(f"添加了 {len(decisions)} 条决策")
    
    elif cmd == 'list':
        list_decisions()
    
    elif cmd == 'extract':
        if len(sys.argv) < 3:
            print("用法: decision-tracker.py extract <text>")
            sys.exit(1)
        text = ' '.join(sys.argv[2:])
        decisions = extract_decisions_from_text(text)
        print(f"发现 {len(decisions)} 个决策点:")
        for d in decisions:
            print(f"  [{d['type']}] {d['content'][:50]}")
    
    else:
        print(f"未知命令: {cmd}")
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/density-detector.py
content: #!/usr/bin/env python3
"""
density-detector.py - 信息密度检测
识别高密度（决策、结论）和低密度（过程、噪音）内容
"""
import re

# 高价值标记
HIGH_VALUE_PATTERNS = [
    (r'决定|决策|结论|判断|结论是|所以|因此|综上', 'decision'),
    (r'任务|下一步|行动|执行|做|完成', 'action'),
    (r'用户说|用户要求|用户提到|老板说', 'user_signal'),
    (r'\d+[./年]|\d+%|约\d+|大约', 'quantitative'),
    (r'必须|一定|绝不|绝对', 'strong_assertion'),
    (r'其实|但是|不过|然而', 'contrast'),
]

# 低价值标记
LOW_VALUE_PATTERNS = [
    (r'^哈+|^嗯|^哦|^呃', 'casual_response'),
    (r'对|好的|行|可以', 'acknowledgment'),
    (r'我明白|我知道了|了解', 'acknowledgment_ai'),
    (r'\.\.\.|\?|？', 'trailing'),
]

def calculate_density(text):
    """
    计算信息密度
    
    返回:
        float: 密度分数 (0-1, 越高越重要)
        dict: 各维度分数
    """
    if not text or len(text.strip()) < 10:
        return 0.0, {}
    
    scores = {}
    
    # 高价值分数
    high_score = 0
    for pattern, label in HIGH_VALUE_PATTERNS:
        matches = len(re.findall(pattern, text))
        high_score += matches
    
    # 低价值分数
    low_score = 0
    for pattern, label in LOW_VALUE_PATTERNS:
        matches = len(re.findall(pattern, text))
        low_score += matches
    
    # 基础分：独特内容比例
    unique_ratio = len(set(text)) / max(len(text), 1)
    
    # 组合
    total_score = high_score * 2 - low_score * 0.5 + unique_ratio * 5
    max_possible = 20  # 归一化
    
    density = min(1.0, total_score / max_possible)
    
    scores = {
        'high_value_markers': high_score,
        'low_value_markers': low_score,
        'unique_ratio': round(unique_ratio, 3),
        'raw_score': round(total_score, 2)
    }
    
    return density, scores

def is_high_density(text):
    """判断是否高密度（需要保留）"""
    density, _ = calculate_density(text)
    return density > 0.3

def get_density_level(text):
    """获取密度等级"""
    density, _ = calculate_density(text)
    if density > 0.6:
        return 'high'
    elif density > 0.3:
        return 'medium'
    else:
        return 'low'

def main():
    import sys
    
    if len(sys.argv) < 2:
        print("用法: density-detector.py --text <文本>")
        print("   或: density-detector.py --file <文件>")
        sys.exit(1)
    
    if sys.argv[1] == '--text':
        text = ' '.join(sys.argv[2:])
    elif sys.argv[1] == '--file':
        with open(sys.argv[2], 'r') as f:
            text = f.read()
    else:
        print("未知参数")
        sys.exit(1)
    
    density, scores = calculate_density(text)
    level = get_density_level(text)
    
    print(f"信息密度: {density:.3f}")
    print(f"密度等级: {level}")
    print(f"高价值标记: {scores['high_value_markers']}")
    print(f"低价值标记: {scores['low_value_markers']}")
    print(f"独特字符比例: {scores['unique_ratio']}")

if __name__ == '__main__':
    main()


path: scripts/snapshot.py
content: #!/usr/bin/env python3
"""
snapshot.py - 7天快照机制
创建/读取/自动清理记忆快照
"""
import os
import json
import shutil
from datetime import datetime, timedelta

WORKFLOW_DIR = "/root/.openclaw/workspace/.workflow"
SNAPSHOT_DIR = os.path.join(WORKFLOW_DIR, "snapshots")
MAX_SNAPSHOTS = 7

def create_snapshot(name, content, metadata=None):
    """
    创建快照
    
    Args:
        name: 快照名称
        content: 快照内容
        metadata: 元数据(dict)
    
    Returns:
        str: 快照文件路径
    """
    os.makedirs(SNAPSHOT_DIR, exist_ok=True)
    
    timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
    filename = f"{timestamp}_{name}.json"
    filepath = os.path.join(SNAPSHOT_DIR, filename)
    
    snapshot = {
        'name': name,
        'created_at': datetime.now().isoformat(),
        'content': content,
        'metadata': metadata or {},
        'token_count': len(content) // 4,  # 粗略估算
    }
    
    with open(filepath, 'w') as f:
        json.dump(snapshot, f, ensure_ascii=False, indent=2)
    
    # 清理旧快照
    cleanup_old_snapshots()
    
    return filepath

def read_snapshot(filepath):
    """读取快照"""
    with open(filepath, 'r') as f:
        return json.load(f)

def list_snapshots():
    """列出所有快照"""
    if not os.path.exists(SNAPSHOT_DIR):
        return []
    
    snapshots = []
    for f in os.listdir(SNAPSHOT_DIR):
        if f.endswith('.json'):
            filepath = os.path.join(SNAPSHOT_DIR, f)
            with open(filepath, 'r') as fp:
                data = json.load(fp)
                snapshots.append({
                    'file': f,
                    'name': data.get('name', 'unknown'),
                    'created_at': data.get('created_at', 'unknown'),
                    'token_count': data.get('token_count', 0),
                })
    
    return sorted(snapshots, key=lambda x: x['created_at'], reverse=True)

def cleanup_old_snapshots():
    """清理超过7天的快照"""
    if not os.path.exists(SNAPSHOT_DIR):
        return
    
    cutoff = datetime.now() - timedelta(days=MAX_SNAPSHOTS)
    removed = 0
    
    for f in os.listdir(SNAPSHOT_DIR):
        if f.endswith('.json'):
            filepath = os.path.join(SNAPSHOT_DIR, f)
            mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
            if mtime < cutoff:
                os.remove(filepath)
                removed += 1
    
    return removed

def get_latest_snapshot():
    """获取最新快照"""
    snapshots = list_snapshots()
    if snapshots:
        return os.path.join(SNAPSHOT_DIR, snapshots[0]['file'])
    return None

def main():
    import sys
    
    if len(sys.argv) < 2:
        print("用法: snapshot.py <create|list|read|cleanup> [args]")
        print("  create <name> <content_file>")
        print("  list")
        print("  read <filepath>")
        print("  cleanup")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    if cmd == 'create':
        if len(sys.argv) < 4:
            print("用法: snapshot.py create <name> <content_file>")
            sys.exit(1)
        name = sys.argv[2]
        content_file = sys.argv[3]
        
        with open(content_file, 'r') as f:
            content = f.read()
        
        filepath = create_snapshot(name, content)
        print(f"快照创建: {filepath}")
    
    elif cmd == 'list':
        snapshots = list_snapshots()
        print(f"共 {len(snapshots)} 个快照:")
        for s in snapshots:
            print(f"  {s['created_at'][:19]} | {s['name']} | {s['token_count']} tokens")
    
    elif cmd == 'read':
        if len(sys.argv) < 3:
            print("用法: snapshot.py read <filepath>")
            sys.exit(1)
        filepath = sys.argv[2]
        data = read_snapshot(filepath)
        print(f"快照: {data['name']}")
        print(f"创建时间: {data['created_at']}")
        print(f"\n内容:\n{data['content'][:500]}...")
    
    elif cmd == 'cleanup':
        removed = cleanup_old_snapshots()
        print(f"清理完成: 删除了 {removed} 个旧快照")
    
    else:
        print(f"未知命令: {cmd}")
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/token-estimator.py
content: #!/usr/bin/env python3
"""
token-estimator.py - 快速Token估算
使用字符数/4 + 中文特殊处理
"""
import os
import sys
import re

def count_tokens(text):
    """
    估算token数
    - 中文：按字符计（约1-2字符=1token）
    - 英文：按单词计（约0.75词=1token）
    """
    if not text:
        return 0
    
    # 中文字符（粗略：每个汉字=1.5token）
    chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
    
    # 英文单词（约0.75token/词）
    english_words = len(re.findall(r'[a-zA-Z]+', text))
    
    # 其他字符（括号、标点等，约4字符=1token）
    other_chars = len(re.findall(r'[^\w\s]', text))
    
    # 空白字符
    whitespace = len(re.findall(r'\s', text))
    
    # 总估算
    # 中文贵：每个汉字1.5token
    # 英文便宜：每个词0.75token
    # 标点符号：4个约1token
    total = chinese_chars * 1.5 + english_words * 0.75 + other_chars / 4
    
    return int(total)

def estimate_tokens_from_text(text):
    """估算文本token数"""
    return count_tokens(text)

def estimate_tokens_from_file(filepath, max_chars=None):
    """
    估算文件token数
    
    Args:
        filepath: 文件路径
        max_chars: 最大字符数（None=全部）
    
    Returns:
        dict: {token_count, char_count, line_count, method}
    """
    if not os.path.exists(filepath):
        return None
    
    with open(filepath, 'r') as f:
        if max_chars:
            text = f.read(max_chars)
        else:
            text = f.read()
    
    char_count = len(text)
    line_count = text.count('\n') + 1
    
    return {
        'token_count': count_tokens(text),
        'char_count': char_count,
        'line_count': line_count,
        'method': 'char-word-punctuation'
    }

def main():
    if len(sys.argv) < 2:
        print("用法: token-estimator.py <file> [max_chars]")
        print("   或: token-estimator.py --text <文本>")
        sys.exit(1)
    
    if sys.argv[1] == '--text':
        text = ' '.join(sys.argv[2:])
        tokens = count_tokens(text)
        print(f"Token数: {tokens}")
        print(f"字符数: {len(text)}")
    else:
        filepath = sys.argv[1]
        max_chars = int(sys.argv[2]) if len(sys.argv) > 2 else None
        
        result = estimate_tokens_from_file(filepath, max_chars)
        if result:
            print(f"文件: {filepath}")
            print(f"Token数: {result['token_count']}")
            print(f"字符数: {result['char_count']}")
            print(f"行数: {result['line_count']}")
            print(f"方法: {result['method']}")
        else:
            print(f"文件不存在: {filepath}")

if __name__ == '__main__':
    main()


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