Skill: nycxk/inspiration-material-organizer (version 1.0.0)

--- name: inspiration-material-organizer description: 将聊天记录、截图文字、链接和随手记等碎片灵感整理为可检索知识库。支持快速收录、智能分类、语义检索和灵感反刍。用户提到灵感整理、素材归档、笔记分类、找不到收藏内容、旧素材回顾时使用。 --- # Inspiration Material Organizer ## 适用场景 当用户提出以下需求时启用： - 整理分散的灵感素材 - 把截图文字或链接沉淀成卡片 - 给素材自动打标签并分组归档 - 通过关键词或模糊描述找历史素材 - 定期回顾旧素材，防止遗忘 ## 默认策略 1. 使用本技能的 Python 脚本作为唯一实现入口。 2. 所有素材落到本地 JSON 知识库，保证可检索和可迁移。 3. 分类使用外部规则文件（稳定、可解释），可直接编辑主题与关键词。 4. 检索使用关键词与简单语义近似（token overlap），避免外部依赖。 ## 工作流 复制并跟踪以下清单： ```text 任务进度: - [ ] 步骤1: 收录素材为标准卡片 - [ ] 步骤2: 自动分类并补充标签 - [ ] 步骤3: 检索验证可用性 - [ ] 步骤4: 反刍输出回顾列表 ``` ### 步骤1：快速收录 命令： ```bash python .cursor/skills/inspiration-material-organizer/scripts/organizer.py capture --text "内容" --source "chat" ``` 支持输入： - `--text` 文字内容（聊天片段、随手记、OCR 结果） - `--url` 链接（可选） - `--title` 标题（可选，不传则自动生成） - `--source` 来源：`chat` / `screenshot` / `link` / `note` / `other` ### 步骤2：智能分类 命令： ```bash python .cursor/skills/inspiration-material-organizer/scripts/organizer.py classify --ids all ``` 效果： - 自动按 `config/topic_rules.json` 分配主题分类 - 自动追加关键词标签 - 按主题写入分组字段 ### 步骤3：语义检索 命令： ```bash python .cursor/skills/inspiration-material-organizer/scripts/organizer.py search --query "我记得有个关于内容选题的方法" ``` 输出： - 按相关性排序的历史素材卡片 - 显示命中原因（标签/关键词） ### 步骤4：灵感反刍 命令： ```bash python .cursor/skills/inspiration-material-organizer/scripts/organizer.py ruminate --topic 写作 --count 5 ``` 可选： - `--date-from YYYY-MM-DD` - `--date-to YYYY-MM-DD` - `--topic 主题` - `--count 数量` ## 数据文件 默认数据库位置： ` .cursor/skills/inspiration-material-organizer/data/inspirations.json ` 可通过环境变量覆盖： `INSPIRATION_DB_PATH` 默认分类规则位置： ` .cursor/skills/inspiration-material-organizer/config/topic_rules.json ` 可通过环境变量覆盖： `INSPIRATION_TOPIC_RULES_PATH` ## 附加资源 - 详细字段说明与分类规则见 [reference.md](reference.md) - 常见命令与场景示例见 [examples.md](examples.md)

path: reference.md
content: # 参考说明

## 灵感卡片标准结构

每条素材保存为一张卡片：

```json
{
  "id": "uuid",
  "title": "自动或手动标题",
  "content": "正文内容",
  "url": "可选链接",
  "source": "chat|screenshot|link|note|other",
  "created_at": "ISO 时间",
  "updated_at": "ISO 时间",
  "topic": "主题分类",
  "tags": ["标签1", "标签2"],
  "group": "分组名"
}
```

## 主题分类规则（可配置）

默认读取：

` .cursor/skills/inspiration-material-organizer/config/topic_rules.json `

也可通过环境变量覆盖：

`INSPIRATION_TOPIC_RULES_PATH`

示例格式：

```json
{
  "写作": ["写作", "选题", "表达"],
  "产品": ["需求", "用户", "功能"],
  "你自定义的新主题": ["关键词1", "关键词2"]
}
```

内置默认规则：

- 写作：写作、选题、表达、文案、结构、标题
- 产品：需求、用户、功能、原型、体验、增长
- 运营：社群、活动、复盘、留存、转化
- 营销：品牌、传播、投放、渠道、广告
- 技术：代码、架构、接口、自动化、AI、Python
- 个人成长：习惯、效率、学习、目标、认知
- 未分类：未命中关键词

## 检索评分逻辑

总分由三部分组成：

1. 查询词在 `title/content` 的命中次数
2. 查询词与 `tags/topic/group` 的交集加权
3. 归一化后的 token overlap（简易语义近似）

## 灵感反刍建议

- 每日 3~5 条随机回顾
- 每周按主题定向回顾（如写作、产品）
- 对高价值素材补充“可执行下一步”，让收藏变可用


path: scripts/organizer.py
content: import argparse
import json
import os
import random
import re
import uuid
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple


DEFAULT_DB = ".cursor/skills/inspiration-material-organizer/data/inspirations.json"
DEFAULT_TOPIC_RULES_PATH = ".cursor/skills/inspiration-material-organizer/config/topic_rules.json"
DEFAULT_TOPIC_RULES: Dict[str, List[str]] = {
    "写作": ["写作", "选题", "表达", "文案", "结构", "标题"],
    "产品": ["需求", "用户", "功能", "原型", "体验", "增长"],
    "运营": ["社群", "活动", "复盘", "留存", "转化"],
    "营销": ["品牌", "传播", "投放", "渠道", "广告"],
    "技术": ["代码", "架构", "接口", "自动化", "ai", "python", "算法"],
    "个人成长": ["习惯", "效率", "学习", "目标", "认知", "复利"],
}


@dataclass
class Card:
    id: str
    title: str
    content: str
    url: str
    source: str
    created_at: str
    updated_at: str
    topic: str
    tags: List[str]
    group: str

    def to_dict(self) -> Dict:
        return {
            "id": self.id,
            "title": self.title,
            "content": self.content,
            "url": self.url,
            "source": self.source,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "topic": self.topic,
            "tags": self.tags,
            "group": self.group,
        }


def now_iso() -> str:
    return datetime.now().isoformat(timespec="seconds")


def db_path() -> Path:
    return Path(os.getenv("INSPIRATION_DB_PATH", DEFAULT_DB))


def topic_rules_path() -> Path:
    return Path(os.getenv("INSPIRATION_TOPIC_RULES_PATH", DEFAULT_TOPIC_RULES_PATH))


def ensure_db() -> None:
    path = db_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    if not path.exists():
        path.write_text("[]", encoding="utf-8")


def load_cards() -> List[Dict]:
    ensure_db()
    path = db_path()
    raw = path.read_text(encoding="utf-8").strip()
    if not raw:
        return []
    return json.loads(raw)


def save_cards(cards: List[Dict]) -> None:
    ensure_db()
    path = db_path()
    path.write_text(json.dumps(cards, ensure_ascii=False, indent=2), encoding="utf-8")


def load_topic_rules() -> Dict[str, List[str]]:
    path = topic_rules_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    if not path.exists():
        path.write_text(json.dumps(DEFAULT_TOPIC_RULES, ensure_ascii=False, indent=2), encoding="utf-8")

    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return DEFAULT_TOPIC_RULES

    if not isinstance(payload, dict):
        return DEFAULT_TOPIC_RULES

    normalized: Dict[str, List[str]] = {}
    for topic, keywords in payload.items():
        if not isinstance(topic, str):
            continue
        if not isinstance(keywords, list):
            continue
        valid_keywords = [kw.lower() for kw in keywords if isinstance(kw, str) and kw.strip()]
        if valid_keywords:
            normalized[topic] = valid_keywords

    return normalized or DEFAULT_TOPIC_RULES


def tokenize(text: str) -> List[str]:
    lowered = text.lower()
    chunks = re.findall(r"[\u4e00-\u9fff]+|[a-z0-9]+", lowered)
    return [c for c in chunks if c.strip()]


def infer_topic_and_tags(text: str) -> Tuple[str, List[str]]:
    lower = text.lower()
    topic_rules = load_topic_rules()
    best_topic = "未分类"
    best_score = 0
    tags: List[str] = []

    for topic, keywords in topic_rules.items():
        hit = [kw for kw in keywords if kw in lower]
        score = len(hit)
        if score > best_score:
            best_topic = topic
            best_score = score
        tags.extend(hit)

    token_tags = list(dict.fromkeys(tokenize(text)))[:8]
    all_tags = list(dict.fromkeys(tags + token_tags))[:10]
    group = best_topic
    return group, all_tags


def auto_title(content: str) -> str:
    plain = content.strip().replace("\n", " ")
    return (plain[:30] + "...") if len(plain) > 30 else plain


def cmd_capture(args: argparse.Namespace) -> None:
    cards = load_cards()
    ts = now_iso()
    title = args.title.strip() if args.title else auto_title(args.text)
    card = Card(
        id=str(uuid.uuid4()),
        title=title,
        content=args.text.strip(),
        url=args.url.strip() if args.url else "",
        source=args.source,
        created_at=ts,
        updated_at=ts,
        topic="未分类",
        tags=[],
        group="待整理",
    )
    cards.append(card.to_dict())
    save_cards(cards)
    print(f"已收录: {card.id} | {card.title}")


def cmd_classify(args: argparse.Namespace) -> None:
    cards = load_cards()
    if args.ids == "all":
        target_ids = {c["id"] for c in cards}
    else:
        target_ids = set([s.strip() for s in args.ids.split(",") if s.strip()])

    updated = 0
    for c in cards:
        if c["id"] not in target_ids:
            continue
        base_text = " ".join([c.get("title", ""), c.get("content", ""), c.get("url", "")])
        topic, tags = infer_topic_and_tags(base_text)
        c["topic"] = topic
        c["group"] = topic
        c["tags"] = tags
        c["updated_at"] = now_iso()
        updated += 1

    save_cards(cards)
    print(f"分类完成: {updated} 条")


def score_card(card: Dict, query: str) -> float:
    q_tokens = tokenize(query)
    if not q_tokens:
        return 0.0

    text = " ".join(
        [
            card.get("title", ""),
            card.get("content", ""),
            card.get("topic", ""),
            card.get("group", ""),
            " ".join(card.get("tags", [])),
        ]
    ).lower()

    base_hits = sum(text.count(t) for t in q_tokens)
    tag_hits = sum(1 for t in q_tokens if t in " ".join(card.get("tags", [])).lower())
    card_tokens = set(tokenize(text))
    overlap = len(set(q_tokens).intersection(card_tokens)) / max(len(set(q_tokens)), 1)
    return base_hits * 1.0 + tag_hits * 1.5 + overlap * 3.0


def cmd_search(args: argparse.Namespace) -> None:
    cards = load_cards()
    scored = []
    for c in cards:
        s = score_card(c, args.query)
        if s > 0:
            scored.append((s, c))
    scored.sort(key=lambda x: x[0], reverse=True)

    if not scored:
        print("没有找到相关素材。")
        return

    for idx, (score, c) in enumerate(scored[: args.limit], start=1):
        print(f"{idx}. [{c['topic']}] {c['title']}  (score={score:.2f})")
        print(f"   id={c['id']}")
        print(f"   tags={','.join(c.get('tags', []))}")
        print(f"   source={c['source']}  url={c.get('url', '')}")
        print(f"   content={c['content'][:120]}")


def in_date_range(card: Dict, date_from: str, date_to: str) -> bool:
    created = card.get("created_at", "")[:10]
    if date_from and created < date_from:
        return False
    if date_to and created > date_to:
        return False
    return True


def cmd_ruminate(args: argparse.Namespace) -> None:
    cards = load_cards()
    pool = []
    for c in cards:
        if args.topic and c.get("topic") != args.topic:
            continue
        if not in_date_range(c, args.date_from or "", args.date_to or ""):
            continue
        pool.append(c)

    if not pool:
        print("没有符合条件的素材可反刍。")
        return

    pick = random.sample(pool, k=min(args.count, len(pool)))
    print(f"本次反刍 {len(pick)} 条：")
    for i, c in enumerate(pick, start=1):
        print(f"{i}. [{c['topic']}] {c['title']} ({c['created_at'][:10]})")
        print(f"   下一步建议: 把这条素材改写成一个可执行动作。")
        print(f"   摘要: {c['content'][:100]}")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="灵感素材整理器 CLI")
    sub = parser.add_subparsers(dest="command", required=True)

    p_capture = sub.add_parser("capture", help="快速收录素材")
    p_capture.add_argument("--text", required=True, help="素材文本")
    p_capture.add_argument("--url", default="", help="素材链接")
    p_capture.add_argument("--title", default="", help="素材标题")
    p_capture.add_argument(
        "--source",
        default="other",
        choices=["chat", "screenshot", "link", "note", "other"],
        help="素材来源",
    )
    p_capture.set_defaults(func=cmd_capture)

    p_classify = sub.add_parser("classify", help="智能分类")
    p_classify.add_argument("--ids", default="all", help="逗号分隔卡片ID，默认 all")
    p_classify.set_defaults(func=cmd_classify)

    p_search = sub.add_parser("search", help="语义检索")
    p_search.add_argument("--query", required=True, help="检索关键词或描述")
    p_search.add_argument("--limit", type=int, default=10, help="返回数量上限")
    p_search.set_defaults(func=cmd_search)

    p_ruminate = sub.add_parser("ruminate", help="灵感反刍")
    p_ruminate.add_argument("--topic", default="", help="按主题过滤")
    p_ruminate.add_argument("--date-from", default="", help="起始日期 YYYY-MM-DD")
    p_ruminate.add_argument("--date-to", default="", help="结束日期 YYYY-MM-DD")
    p_ruminate.add_argument("--count", type=int, default=5, help="返回数量")
    p_ruminate.set_defaults(func=cmd_ruminate)

    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()


path: data/inspirations.json
content: [
  {
    "id": "2dc30c6c-5004-4dd7-8db2-1cac390d9fc6",
    "title": "内容选题可以从用户高频问题中提炼",
    "content": "内容选题可以从用户高频问题中提炼",
    "url": "",
    "source": "chat",
    "created_at": "2026-04-21T16:52:41",
    "updated_at": "2026-04-21T16:59:18",
    "topic": "写作",
    "tags": [
      "选题",
      "用户",
      "内容选题可以从用户高频问题中提炼"
    ],
    "group": "写作"
  }
]

path: config/topic_rules.json
content: {
  "写作": ["写作", "选题", "表达", "文案", "结构", "标题", "开头", "结尾", "金句", "语感", "改稿", "修辞", "短文案", "长文案", "朋友圈", "小红书", "公众号", "视频脚本", "销售页", "海报文案"],
  "产品": ["需求", "用户", "功能", "原型", "体验", "增长", "定价", "迭代", "用户画像", "用户痛点", "用户反馈", "用户故事", "用户分层", "拉新", "激活", "留存", "变现", "裂变", "复购"],
  "运营": ["社群", "活动", "复盘", "留存", "转化", "内容运营", "用户运营", "私域", "SOP", "社群运营", "活动运营", "数据分析", "运营复盘"],
  "营销": ["品牌", "传播", "投放", "渠道", "广告", "定位", "事件营销", "借势营销", "内容营销", "口碑营销", "品牌故事", "竞品分析"],
  "技术": ["代码", "架构", "接口", "自动化", "ai", "python", "算法", "部署", "效率脚本", "工作流", "调试", "API", "数据库", "前端", "后端"],
  "个人成长": ["习惯", "效率", "学习", "目标", "认知", "复利", "心力", "情绪管理", "压力应对", "焦虑处理", "自信心", "动力", "孤独感", "倦怠", "时间管理", "精力管理", "深度工作"],
  "商业": ["商业模式", "变现方式", "收入结构", "成本控制", "利润模型", "订阅制", "项目制", "平台模式", "一人公司", "副业", "创业", "自由职业", "定价策略"],
  "案例": ["成功案例", "失败案例", "竞品分析", "跨界案例", "行业标杆", "案例拆解", "增长案例", "营销案例", "产品案例"],
  "故事": ["人物故事", "品牌故事", "客户故事", "个人经历", "隐喻", "案例故事", "创始人故事", "用户故事", "转型故事"],
  "趋势": ["行业动态", "消费趋势", "技术趋势", "政策变化", "人群洞察", "新平台机会", "市场报告", "未来预测"],
  "工具": ["效率工具", "创作工具", "协作工具", "数据分析", "AI工具", "插件推荐", "工具测评", "自动化工具", "笔记软件", "时间管理工具"],
  "素材": ["金句", "数据", "视觉", "选题", "名言", "台词", "摘录", "截图", "配色", "排版", "封面图", "设计参考", "信息图", "配图灵感"],
  "状态": ["待处理", "已加工", "已使用", "待反刍", "可删除", "精华", "待分类", "待归档"],
  "来源": ["阅读", "课程", "播客", "对话", "社交媒体", "观察", "灵感一闪", "他人分享", "书籍", "文章", "视频", "演讲", "社群讨论"],
  "关系": ["客户关系", "合作关系", "同行交流", "导师", "人脉", "边界", "沟通", "谈判", "冲突处理", "信任建立"]
}

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