文件名一团糟?我用 Claude Code Skill 治好了这个毛病

7 阅读6分钟

前置条件:本文需要安装 Claude Code。国内用户推荐使用 pateway.ai/?ch=bd95e5#(博主实测稳定不注水,价格为官网8折起步,支持 OpenAI 及 Anthropic 格式),配置好 API 端点后跟着做即可。


你的桌面现在长什么样?

我猜大概是这样的:

截图 2024-03-21 下午3.44.12.png
新建文档 (3).docx
微信图片_20240301.jpg
final_v3_FINAL_USE_THIS_actually_final.pdf

这些文件你知道是什么,但三个月后你不会知道。搜索的时候也找不到。这是每个人都有的隐形债务,攒够了就炸。

手动改?十几个文件还行,一个文件夹三百个文件你试试。

写规则脚本?截图 2024-03-21 下午3.44.12.png 你告诉我规则怎么写——它该叫什么,只有看过这张截图的人才知道。

所以我用 LLM 做了一个 Claude Code Skill,说一句话,批量预览重命名结果,确认后执行。


为什么 LLM 能做这件事

规则处理的是格式,LLM 处理的是语义。

同样是一张截图,截图 2024-03-21 下午3.44.12.png 经过 LLM 分析内容后可以变成 2024-03-21_xcode-build-error.png。这一步规则做不到,因为规则不知道截图里是什么。

对于纯文件名(没有内容可读),LLM 也能做的事是:

  • 去掉冗余词:final副本newUSE_THIS
  • 统一命名风格:全部 snake_case 或驼峰
  • 加日期前缀:从文件名或内容里提取

核心代码

整个脚本不到 150 行,依赖只有一个:

pip install openai

完整代码如下:

#!/usr/bin/env python3
"""
smart-rename: LLM-powered file renaming tool.

Environment variables:
  LLM_API_KEY   - Required. Your API key.
  LLM_BASE_URL  - Optional. Custom API endpoint (e.g. a relay service).
  LLM_MODEL     - Optional. Model name (default: gpt-4o-mini).
"""

import argparse
import json
import os
import sys
from pathlib import Path

try:
    from openai import OpenAI
except ImportError:
    print("Error: openai package not found. Run: pip install openai", file=sys.stderr)
    sys.exit(1)


def get_client() -> OpenAI:
    api_key = os.environ.get("LLM_API_KEY")
    if not api_key:
        print("Error: LLM_API_KEY environment variable not set.", file=sys.stderr)
        print("  export LLM_API_KEY=your-key", file=sys.stderr)
        print("  export LLM_BASE_URL=https://your-relay-endpoint/v1  # if using a relay", file=sys.stderr)
        sys.exit(1)

    base_url = os.environ.get("LLM_BASE_URL")
    return OpenAI(api_key=api_key, base_url=base_url) if base_url else OpenAI(api_key=api_key)


def read_content_snippet(path: Path, max_chars: int = 500) -> str:
    text_extensions = {".txt", ".md", ".py", ".js", ".ts", ".java", ".go", ".rs", ".csv", ".json", ".xml", ".html"}
    if path.suffix.lower() not in text_extensions:
        return ""
    try:
        return path.read_text(encoding="utf-8", errors="ignore")[:max_chars]
    except Exception:
        return ""


def suggest_names(
    files: list[Path],
    style: str,
    date_prefix: bool,
    language: str,
    client: OpenAI,
    model: str,
) -> dict[str, str]:
    file_info = []
    for f in files:
        info = {"original_name": f.name, "extension": f.suffix}
        snippet = read_content_snippet(f)
        if snippet:
            info["content_snippet"] = snippet
        file_info.append(info)

    style_guide = {
        "snake_case": "lowercase words separated by underscores (e.g. meeting_notes_2024.pdf)",
        "camelCase": "camelCase (e.g. meetingNotes2024.pdf)",
        "kebab-case": "lowercase words separated by hyphens (e.g. meeting-notes-2024.pdf)",
        "chinese": "concise Chinese description (e.g. 会议记录_2024.pdf)",
    }

    prompt = f"""你是一个文件命名助手,为以下文件建议清晰的文件名。

命名风格:{style_guide.get(style, style)}
语言:{language}
日期前缀:{"有则添加 YYYY-MM-DD 前缀" if date_prefix else "不添加"}

规则:
- 保留原始扩展名
- 简洁但有描述性(不超过60字符)
- 去掉 截图/副本/copy/new/untitled 等冗余词
- 不使用空格

文件列表:
{json.dumps(file_info, ensure_ascii=False, indent=2)}

返回 JSON 对象,原文件名 -> 建议文件名。只返回 JSON,不要解释。"""

    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
        temperature=0.3,
    )
    return json.loads(response.choices[0].message.content)


def collect_files(paths: list[str], directory: str | None) -> list[Path]:
    files = []
    if directory:
        d = Path(directory)
        if not d.is_dir():
            print(f"Error: {directory} is not a directory", file=sys.stderr)
            sys.exit(1)
        files = [f for f in d.iterdir() if f.is_file() and not f.name.startswith(".")]
    else:
        for p in paths:
            path = Path(p)
            if not path.exists():
                print(f"Warning: {p} does not exist, skipping", file=sys.stderr)
                continue
            files.append(path)
    return files


def main():
    parser = argparse.ArgumentParser(description="Rename files using LLM")
    parser.add_argument("--files", nargs="+", default=[], help="File paths to rename")
    parser.add_argument("--dir", help="Directory to batch rename")
    parser.add_argument("--style", default="snake_case",
                        choices=["snake_case", "camelCase", "kebab-case", "chinese"])
    parser.add_argument("--date-prefix", action="store_true")
    parser.add_argument("--language", default="english", choices=["english", "chinese"])
    parser.add_argument("--dry-run", action="store_true", default=True)
    parser.add_argument("--execute", action="store_true")
    args = parser.parse_args()

    if args.execute:
        args.dry_run = False

    files = collect_files(args.files, args.dir)
    if not files:
        print("No files to process.")
        sys.exit(0)

    client = get_client()
    model = os.environ.get("LLM_MODEL", "gpt-4o-mini")

    print(f"Analyzing {len(files)} file(s) with model {model}...\n")

    suggestions = {}
    batch_size = 20
    for i in range(0, len(files), batch_size):
        batch = files[i:i + batch_size]
        suggestions.update(suggest_names(batch, args.style, args.date_prefix, args.language, client, model))

    col_width = max((len(f.name) for f in files), default=20) + 2
    print(f"{'原文件名':<{col_width}}  →  建议名称")
    print("-" * (col_width + 20))
    for f in files:
        suggested = suggestions.get(f.name, f.name)
        print(f"{f.name:<{col_width}}  →  {suggested}")

    if args.dry_run:
        print("\n[预览模式] 未执行重命名。使用 --execute 参数以实际重命名。")
        return

    print("\n执行重命名...")
    success, skipped = 0, 0
    for f in files:
        suggested = suggestions.get(f.name)
        if not suggested or suggested == f.name:
            skipped += 1
            continue
        new_path = f.parent / suggested
        if new_path.exists():
            print(f"  跳过 {f.name}:目标文件已存在")
            skipped += 1
            continue
        f.rename(new_path)
        print(f"  ✓ {f.name}{suggested}")
        success += 1

    print(f"\n完成:{success} 个文件已重命名,{skipped} 个跳过。")


if __name__ == "__main__":
    main()

几个设计决策值得说一下:

temperature=0.3:文件命名不需要创意,要的是稳定输出,低温度减少随机性。

response_format={"type": "json_object"}:强制 JSON 输出,省去解析正则表达式的麻烦。

内容片段只取前 500 字:够 LLM 理解语义,不浪费 token。

默认 dry-run:永远先预览,不自动执行。对文件系统的操作,确认前不动。


包装成 Claude Code Skill

光有脚本还不够,每次还要记参数、记路径,和手动改名没差多少。

把它包装成 Skill 之后,使用方式变成这样:

帮我把 ~/Downloads 里的文件名整理一下,用 snake_case

Claude Code 识别意图,自动调用脚本,展示预览表格,等你确认。

Skill 的结构很简单:

smart-rename/
├── SKILL.md       # 告诉 Claude 什么时候触发、怎么操作
└── scripts/
    └── rename.py  # 实际执行的脚本

SKILL.md 完整内容(用四个反引号包裹,避免与内部代码块冲突):

---
name: smart-rename
description: Intelligently rename files using LLM semantic analysis. Trigger when user says
             things like "帮我重命名文件", "rename these files", "给文件起个好名字",
             "整理文件名", "文件名太乱了", or pastes a list of messy filenames.
             Works for single files, multiple files, or entire directories.
             Supports snake_case, camelCase, Chinese naming, with optional date prefix.
---

# Smart File Renamer

## Workflow

1. **Clarify inputs**
   - File paths, or a directory path
   - Naming style: `snake_case` (default) | `camelCase` | `chinese` | `kebab-case`
   - Date prefix: yes / no (default: no)
   - Language: English (default) | Chinese

2. **Run the rename script**
   ```bash
   python3 ~/.claude/skills/smart-rename/scripts/rename.py \
     --files "path/to/file1" "path/to/file2" \
     --style snake_case \
     --dry-run
   ```
   For a directory:
   ```bash
   python3 ~/.claude/skills/smart-rename/scripts/rename.py \
     --dir "path/to/directory" \
     --style snake_case \
     --dry-run
   ```

3. **Show preview, then confirm before executing**
   - Always show the rename table first
   - Re-run with `--execute` only after user confirms

smart-rename 目录放到 ~/.claude/skills/ 下,Claude Code 重启后自动识别。


效果

Analyzing 6 file(s) with model gpt-4o-mini...

原文件名                                      建议名称
────────────────────────────────────────────────────────────────
截图 2024-03-21 下午3.44.12.png              2024-03-21_xcode-build-error.png
新建文档 (3).docx                             project-requirements-draft.docx
微信图片_20240301.jpg                         2024-03-01_wechat-photo.jpg
final_v3_FINAL_USE_THIS_actually_final.pdf    graduation-thesis.pdf
meeting record.txt                            2024-03-15_team-meeting-notes.txt
Untitled.py                                   data-preprocessing-pipeline.py

[预览模式] 未执行重命名。确认后输入 yes 继续。

End

这是本专题的第一篇,后续会持续做其他日常痛点的 Skill。有想要的场景欢迎评论区告诉我。