From 1faee80da456a6bcd63fdc14c6d8d312444df986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Wed, 13 May 2026 22:16:39 -0500 Subject: [PATCH] feat(baoyu-wechat-summary): add WeChat group chat summary skill --- .claude-plugin/marketplace.json | 3 +- .gitignore | 1 + README.md | 30 ++ README.zh.md | 30 ++ skills/baoyu-wechat-summary/EXTEND.md.example | 33 ++ skills/baoyu-wechat-summary/SKILL.md | 455 ++++++++++++++++++ .../references/output-formats.md | 349 ++++++++++++++ .../references/profiles.md | 273 +++++++++++ 8 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 skills/baoyu-wechat-summary/EXTEND.md.example create mode 100644 skills/baoyu-wechat-summary/SKILL.md create mode 100644 skills/baoyu-wechat-summary/references/output-formats.md create mode 100644 skills/baoyu-wechat-summary/references/profiles.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e10813e..68cac27 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -33,7 +33,8 @@ "./skills/baoyu-translate", "./skills/baoyu-url-to-markdown", "./skills/baoyu-image-cards", - "./skills/baoyu-youtube-transcript" + "./skills/baoyu-youtube-transcript", + "./skills/baoyu-wechat-summary" ] } ] diff --git a/.gitignore b/.gitignore index 5291bf8..31ef270 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,4 @@ youtube-transcript/ .omx/ .codex-tmp/ outputs/ +wechat/ diff --git a/README.md b/README.md index 7a8794c..b7ce4ff 100644 --- a/README.md +++ b/README.md @@ -1112,6 +1112,36 @@ Custom style descriptions are also accepted, e.g., `--style "poetic and lyrical" - Translator's notes for cultural/domain-specific references - Output directory with all intermediate files preserved +#### baoyu-wechat-summary + +Summarize WeChat group chat highlights into a structured digest. Extracts topics, quotes, and stats from group messages using [wx-cli](https://github.com/jackwener/wx-cli). Maintains per-group history and per-user profiles across runs. Supports normal and roast (毒舌) versions. + +```bash +# Summarize a group's recent messages +/baoyu-wechat-summary 消失的大叔 最近 1 天 + +# Weekly summary +/baoyu-wechat-summary AI 技术群 最近 7 天 + +# Incremental (since last digest) +/baoyu-wechat-summary 消失的大叔 + +# Roast version +/baoyu-wechat-summary 消失的大叔 最近 3 天 毒舌版 +``` + +**Requirements**: +- [wx-cli](https://github.com/jackwener/wx-cli) installed (`npm install -g @jackwener/wx-cli`) +- WeChat 4.x running and logged in on macOS + +**Features**: +- Topic extraction with attribution and quotes +- Message leaderboard and per-user profiles +- Incremental mode (picks up where last digest left off) +- Multi-day range splitting for large batches +- Normal and roast (毒舌) digest versions +- Profile backfill from historical digests + ## Environment Configuration Some skills require API keys or custom configuration. Environment variables can be set in `.env` files: diff --git a/README.zh.md b/README.zh.md index cf64bbf..cbfbed0 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1112,6 +1112,36 @@ AI 驱动的生成后端。 - 为文化/专业术语添加译注 - 输出目录保留所有中间文件 +#### baoyu-wechat-summary + +微信群聊精华提取。使用 [wx-cli](https://github.com/jackwener/wx-cli) 从群消息中提取话题、引言和统计数据,生成结构化简报。支持跨次运行的群聊历史和群友画像维护,可生成正常版和毒舌版。 + +```bash +# 总结群最近消息 +/baoyu-wechat-summary 消失的大叔 最近 1 天 + +# 周报 +/baoyu-wechat-summary AI 技术群 最近 7 天 + +# 增量模式(从上次摘要继续) +/baoyu-wechat-summary 消失的大叔 + +# 毒舌版 +/baoyu-wechat-summary 消失的大叔 最近 3 天 毒舌版 +``` + +**前置要求**: +- 安装 [wx-cli](https://github.com/jackwener/wx-cli)(`npm install -g @jackwener/wx-cli`) +- macOS 上运行并登录 WeChat 4.x + +**特性**: +- 话题提取,带归属和引言 +- 发言排行榜和群友画像 +- 增量模式(从上次摘要断点继续) +- 大批量消息自动按天分割 +- 正常版和毒舌版两种风格 +- 支持从历史摘要回溯初始化画像 + ## 环境配置 部分技能需要 API 密钥或自定义配置。环境变量可以在 `.env` 文件中设置: diff --git a/skills/baoyu-wechat-summary/EXTEND.md.example b/skills/baoyu-wechat-summary/EXTEND.md.example new file mode 100644 index 0000000..eab61ce --- /dev/null +++ b/skills/baoyu-wechat-summary/EXTEND.md.example @@ -0,0 +1,33 @@ +# baoyu-wechat-summary preferences +# +# Copy this file to one of: +# .baoyu-skills/baoyu-wechat-summary/EXTEND.md (project-local, takes precedence) +# $XDG_CONFIG_HOME/baoyu-skills/baoyu-wechat-summary/EXTEND.md (XDG, falls back to ~/.config) +# ~/.baoyu-skills/baoyu-wechat-summary/EXTEND.md (per-user home) +# +# First match wins. Keys are case-insensitive. Use `key: value` or `key=value`. +# Blank lines and lines starting with `#` are ignored. + +# REQUIRED — your own wxid. Used to recognize your own messages so they show up +# as `self_display` (below) in digests instead of the raw wxid string. +# Find via: wx contacts --query <你的昵称> --json (look for chat_type=private, your own row) +self_wxid: wxid_xxxxxxxxxxxx + +# REQUIRED — the display name to substitute for your wxid in digest text. +self_display: 宝玉 + +# OPTIONAL — which version(s) to generate when the user doesn't explicitly say. +# Values: normal | roast | both +# Default: normal +default_version: normal + +# OPTIONAL — default time range when the user gives none and there's no prior +# digest to incrementally extend from. Forms: `Nd` (days), `Nh` (hours), or a +# fixed `YYYY-MM-DD` (treated as both --since and --until). +# Default: 24h (i.e., today) +# default_time_range: 7d + +# OPTIONAL — override the root directory where digest folders live. Default +# resolves to `{project_root}/wechat`. Useful if you want a shared archive +# outside the current project. +# data_root: ~/Documents/wechat-digests diff --git a/skills/baoyu-wechat-summary/SKILL.md b/skills/baoyu-wechat-summary/SKILL.md new file mode 100644 index 0000000..3dd6af8 --- /dev/null +++ b/skills/baoyu-wechat-summary/SKILL.md @@ -0,0 +1,455 @@ +--- +name: baoyu-wechat-summary +description: Summarizes WeChat group chat highlights into a structured digest using the local wx-cli binary (https://github.com/jackwener/wx-cli). Generates a normal digest by default; a roast (毒舌) version is opt-in. Maintains per-group history (history.json + history-digests.jsonl) and per-user profiles across runs, with privacy guardrails baked in. Use when the user asks to "总结群聊", "群聊精华", "群聊摘要", "summarize group chat", "group chat digest", mentions a WeChat group name with a time range, says "帮我看看 XX 群最近聊了什么", "XX 群有什么值得看的", or asks to "回溯画像" / "初始化画像" / "backfill profiles". Adds the roast version when the user says "毒舌版", "roast 版", "再来个毒舌的", or similar. +version: 0.1.0 +metadata: + openclaw: + homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary + requires: + anyBins: + - wx +--- + +# WeChat Group Summary + +群聊精华提取专家。把零散的微信群聊记录提炼成结构化、可读性强的简报,并维护跨次运行的群聊历史与群友画像。底层依赖外部 [wx-cli](https://github.com/jackwener/wx-cli) 二进制(`wx` 命令),不打包脚本。 + +> **⚠️ Sandbox restriction** +> +> wx-cli reads from `~/.wx-cli/` (config, cache, daemon socket) and from WeChat's data directory (`~/Library/Containers/com.tencent.xinWeChat/` on macOS). Both paths are outside Claude Code's default sandbox. Every `wx` command in this skill needs to run with `dangerouslyDisableSandbox: true` from the start — don't waste a sandbox attempt first. The user can use `/sandbox` to view/edit restrictions. + +## User Input Tools + +When this skill prompts the user, follow this tool-selection rule (priority order): + +1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent. +2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question. +3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order. + +Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes. + +## Prerequisites + +Before invoking the workflow, verify the environment. Run these checks in order; stop at the first failure and surface the exact next command the user needs. + +1. **wx-cli installed** — run `wx --version`. If missing, tell the user to install it themselves (`npm install -g @jackwener/wx-cli` or use one of the alternatives at https://github.com/jackwener/wx-cli). **Do NOT auto-install** — this repo forbids piped/silent installs. +2. **`~/.wx-cli` directory owned by the current user** — `sudo wx init` historically chowned this directory to root, which breaks every subsequent non-sudo `wx` call. Check: + ```bash + ls -la ~/.wx-cli/ 2>/dev/null | head -5 + ``` + If the directory exists but the owner is `root` (or anything other than `$(whoami)`), tell the user to repair it themselves: + ```bash + sudo chown -R $(whoami) ~/.wx-cli + sudo rm -f ~/.wx-cli/daemon.pid ~/.wx-cli/daemon.sock + wx daemon start + ``` + The skill should NOT run `sudo` on the user's behalf. +3. **wx-cli initialized** — `wx sessions` should return data. If it fails with "no keys" / "init required", instruct the user to run `wx init` while WeChat is running (on macOS, `codesign --force --deep --sign - /Applications/WeChat.app` first). Prefer non-sudo init; only fall back to `sudo wx init` if the user's wx-cli version requires it — and warn them that they'll need step 2's chown after. +4. **WeChat 4.x running and logged in** — required for the daemon to find data files. + +## Preferences (EXTEND.md) + +Check EXTEND.md in priority order — the first one found wins: + +| Priority | Path | Scope | +|----------|------|-------| +| 1 | `.baoyu-skills/baoyu-wechat-summary/EXTEND.md` (relative to project root) | Project | +| 2 | `${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-wechat-summary/EXTEND.md` | XDG | +| 3 | `$HOME/.baoyu-skills/baoyu-wechat-summary/EXTEND.md` | User home | + +| Result | Action | +|--------|--------| +| Found | Read, parse, apply. On first use in session, briefly remind: "Using preferences from [path]. Edit it to change defaults." | +| Not found | **MUST** run first-time setup (BLOCKING) before generating any digest — do NOT silently use defaults. | + +### Supported keys + +EXTEND.md is plain text with `key: value` or `key=value` lines, `#` for comments, case-insensitive keys. + +| Key | Type | Default | Purpose | +|-----|------|---------|---------| +| `self_wxid` | string | (required) | The owning account's wxid. Messages whose `from_wxid` matches this are attributed to the user. | +| `self_display` | string | (required) | Display name to substitute for the user's own messages in digest text. | +| `default_version` | `normal` / `roast` / `both` | `normal` | Which version(s) to generate when the user doesn't say otherwise. | +| `default_time_range` | string (e.g. `7d`, `24h`, `1d`) | (none) | Default range when the user omits time and there's no incremental anchor. | +| `data_root` | path | `{project_root}/wechat` | Override where digest folders live. | + +A starter template lives at [EXTEND.md.example](EXTEND.md.example). + +### First-Time Setup (BLOCKING) + +If no EXTEND.md is found, do NOT silently proceed. + +**Step A — Try to auto-discover `self_wxid` and `self_display` first.** Run (in order, stop at the first that succeeds): + +```bash +# 1. If wx-cli exposes a whoami, use it +wx whoami --json 2>/dev/null + +# 2. Otherwise, find self-sent messages in recent sessions +wx sessions --json --limit 20 2>/dev/null +``` + +For option 2, scan the sessions for any private/group thread the user has sent into and read one of their own `from_wxid` / `from_nickname` pairs. If you can confidently pre-fill both values, use them as defaults in the question below; otherwise leave the fields blank for the user to fill in. + +**Step B — Confirm with one `AskUserQuestion` call (batched), pre-filling whatever auto-discovery found:** + +- `self_wxid` (e.g., `wxid_abc123`) — fall-back hint: the user can find it with `wx contacts --query ""`, or by inspecting any of their own sent messages in `wx sessions --json` +- `self_display` (e.g., `宝玉`) — how they want their messages attributed +- `default_version` — pick one of `normal` / `roast` / `both` +- Save location — pick one of project / XDG / home + +Write EXTEND.md to the chosen path, confirm "Preferences saved to [path]. Edit it any time to change defaults.", then continue with the digest workflow. + +## Workflow + +### Step 1: Parse the user's request + +Extract: + +- **Group name** (or partial name for fuzzy matching) +- **Time range** — interpret flexibly: + - "最近 1 天" / "今天" / "last 24 hours" → 1 day + - "最近 3 天" → 3 days + - "最近 7 天" / "这周" → 7 days + - "最近 30 天" / "最近一个月" → 30 days + - "某天" (e.g. "3 月 5 号") → that specific date + - "某天到某天" (e.g. "3 月 1 号到 3 月 5 号") → date range + - "从上次开始" / "继续" / "接着上次" / "since last" → **incremental mode**: read `history.json` for this group, use `last_digest.last_message_time` as the start + - No time specified → **incremental mode**. If no `history.json` exists yet, fall back to `default_time_range` from EXTEND.md if set, else last 24 hours. +- **Version(s) to generate**: + - Start from `default_version` in EXTEND.md. + - User request overrides: keywords "毒舌"/"roast"/"挑衅"/"再来个毒的"/"sass" → force `include_roast=true`. Keywords "只要正经的"/"normal only"/"不要毒舌" → force `include_normal=true, include_roast=false`. "都来一份"/"两个版本都要"/"both" → both. + - At least one of `include_normal`/`include_roast` must end up true. + +Convert relative ranges into absolute `--since YYYY-MM-DD --until YYYY-MM-DD` pairs using today's local date. + +### Step 2: Find the group + resolve folder path + +```bash +wx contacts --query "" --json +``` + +Filter for entries whose `username` ends in `@chatroom`. If multiple groups match, use `AskUserQuestion` to disambiguate. If none match, fall back to `wx sessions --json` and search there before asking the user. + +Once resolved, compute the folder path: + +``` +{data_root}/{group_id}-{sanitized_group_name}/ +``` + +where `data_root` is from EXTEND.md (default `{project_root}/wechat`). + +**Sanitize the group name** — replace any of `/ \ : * ? " < > | NUL` and control characters with `_`. Trim trailing dots and whitespace. Don't strip emoji or Chinese characters. + +**Group-rename detection**: list existing folders under `{data_root}/` and find any folder whose name starts with `{group_id}-`. If one exists but the suffix differs (group was renamed), rename the existing folder to the new `{group_id}-{sanitized_new_name}` form. If a target with the new name already exists (rare), keep both and prefer the existing one for this run. + +### Step 3: Fetch messages + +For small batches (single-day digest, typically < 200 messages), pipe JSON into the agent directly: + +```bash +wx history "" --since YYYY-MM-DD --until YYYY-MM-DD -n 5000 --json +``` + +For **large batches** (weekly / monthly digests, > 200 messages), redirect to `$TMPDIR` first so the raw payload never sits in conversation context: + +```bash +wx history "" --since YYYY-MM-DD --until YYYY-MM-DD -n 5000 --json > "$TMPDIR/wx-messages.json" +wc -c "$TMPDIR/wx-messages.json" +jq 'length' "$TMPDIR/wx-messages.json" +``` + +Then read the file in slices via `Read` with `offset` + `limit`, or process with `jq` queries (e.g. `jq '.[0:200]'`, `jq '[.[] | {id, from_nickname, timestamp, content: (.content | .[0:50])}]'` for a lightweight skeleton pass). Reading all 500+ messages at once will burn token budget unnecessarily. + +Notes: + +- `--since` is inclusive; `--until` is interpreted as a date (the whole day). If the user asked for "today only", set both to today. +- `-n 5000` is a defensive cap; for very active groups, raise it and re-fetch. +- Filter the returned messages by their `timestamp` to be safe (some daemons may return adjacent days). +- **Range splitting**: for ranges > 7 days OR > 500 messages, prefer generating per-3-day digests and then a meta-summary over forcing one giant digest — the categorization quality degrades sharply past a week's worth of unrelated topics. + +**Incremental mode**: after the fetch, drop any message whose `timestamp` is `<=` the `last_message_time` from `history.json`. If zero messages remain, tell the user "上次摘要后没有新消息,已跳过生成" and exit. + +### Step 3.5: Parse the message schema + +`wx history --json` returns an array of message objects. Use the fields that are present; tolerate missing fields: + +- **`id` / `msg_id` / `local_id`** — message identifier (use whichever wx-cli emits). Reference IDs in working notes as anchors when building the skeleton. +- **`from_wxid`** — stable sender identifier +- **`from_nickname`** — display name (may be the group remark or original nickname) +- **`content`** — text payload. Examples: + - Plain text → use as-is + - `[图片]` → opaque placeholder; see image handling below + - `[表情]` → emoji/sticker; skip in body unless surrounded by discussion + - `[视频]` / `[文件]` → media reference; skip unless discussed + - `[链接] ` or `[链接/文件] <title>` → shared article; the title IS the information — quote it and credit the sharer + - `[系统] ... revokemsg` → revoked; exclude from digest and from leaderboard +- **`timestamp`** — convert to `MM-DD HH:MM` for display (and use full ISO for `generated_at`) +- **`chat_type`** — sanity-check `group` +- **Quote/reply** — try `quote_id`, `reply_to`, `quoted_msg_id`, or any nested `quote` object. If present, use it as strong attribution. If absent, fall back to context but flag the inferred link as uncertain. + +### Step 3.6: Resolve self + ambiguous nicknames + +- Substitute `self_display` for every message whose `from_wxid` matches `self_wxid` (from EXTEND.md). Apply this in the leaderboard, portraits, and body text. The user MUST appear under their real display name and count toward stats — never skip them. +- Scan all unique senders for ambiguous handles: ≤2 characters, common programming words (`nil`, `null`, `test`, `admin`, `user`, `undefined`), single emoji, or otherwise low-information. For each, run `wx contacts --query "<nick>" --json --limit 5` and pick a meaningful name in this priority: remark > nickname > wxid. Apply the substitution everywhere in the digest. + +### Step 3.7: Load user profiles + +For each unique sender appearing in this batch: + +- Look in `{folder}/profiles/{wxid}-*.md` by `wxid` prefix match. Read the matched file if found. +- If `include_roast`, **also** look in `{folder}/profiles-roast/{wxid}-*.md` for the roast pass. + +Compile a condensed **profile context block** as internal working memory — do NOT write it into the final digest. Example shape: + +``` +== 群友历史画像(来自 profiles/)== +K. H:空中直播员 / 生活百科全书。常见话题:旅行、金融、美食。经典金句:"要不要买moderna"。 +可可苏玛:... +``` + +Rules: + +- Only load profiles for users active in this batch — never preload everyone. +- Profile is **background**, not template. Current messages are still the primary source. +- Use historical labels for **continuity** ("又双叒叕化身空中直播员") or **contrast** ("一向省钱的 XX 今天居然..."). +- **Strict separation**: normal pass reads only `profiles/`, roast pass reads only `profiles-roast/`. Never cross-load. + +See [references/profiles.md](references/profiles.md) for the full file format. + +### Step 3.8: Detect existing in-chat digests (optional) + +Some users (e.g., the original 宝玉 workflow) post digests directly into the group as messages. If we don't notice these, the new digest will re-cover the same ground. + +Scan the fetched messages for signals of a prior in-chat digest: + +- `from_wxid == self_wxid` AND +- `content` contains `群聊精华` OR `消息统计:` OR `📊 消息统计` OR a leaderboard pattern (e.g. `^\d+\. .+: \d+ 条`), AND +- `content` length > 1500 chars. + +If a match is found: + +1. Extract the digest's covered date or range from the title line (e.g., `xxx 群聊精华 · 2026-05-12` or `... · 2026-05-10 ~ 2026-05-12`). +2. Surface the finding to the user via `AskUserQuestion`: + - "Detected an in-chat digest by you covering {范围}. Use {范围 end + 1} as the start instead of `history.json`?" + - Options: `Yes, skip up to {end of detected range}` / `No, use history.json` / `No, cover everything in the requested range`. +3. Apply the chosen anchor. + +This is a heuristic — when uncertain (multiple matches, malformed title), default to `history.json` and tell the user what was skipped. + +Generate the digest in three rounds so nothing slips through. The methodology stays here in SKILL.md; the content/style rules live in [references/output-formats.md](references/output-formats.md) — read that file in Round 2 before drafting. + +#### Round 1 — Build the skeleton + +Read every message in order. **Skip image fetching/decoding** in this round. List every distinct discussion topic. Bias toward over-listing — trim in Round 3. + +Internal working format (not written to the final file): + +``` +== 话题清单(共 N 条消息)== +1. [HH:MM-HH:MM] 话题名称(参与者:A, B, C)— 一句话概括(锚点 id:54052, 54055, 54063) +2. [HH:MM-HH:MM] 话题名称(参与者:D, E)— 一句话概括(锚点 id:54100-54112) +... + +== 可能需要图片上下文的话题 == +- 话题 3:锚点 id=49661(图片是讨论主体) + +== 发言统计 == +1. XXX — N 条 2. YYY — N 条 ... +``` + +Topic principles: + +- Topic-switch signals: time gap > 30 min, participant change, content jump. +- 2+ participants OR substantive content qualifies as a topic; pure emoji-banter does not. +- **Strict attribution**: each topic must record "who said what". Don't fuse adjacent messages from different senders just because they're close in time — when minutes apart or interleaved with others, split into separate topics. Prefer two topics over one wrongly-merged topic. +- **Carry anchor IDs**: list the key message IDs for each topic. In Round 2, jump back to these IDs in the raw messages and verify content, don't guess from context. If `quote_id` / `reply_to` is present, use the ID chain — that's the most reliable attribution. + +**Flag-for-images criteria** (any one triggers): an explicit comment on an image (`看发型是X?`, `这是谁?`, `笑死`), multiple people piling onto the same image without saying what it is, an image as the core information (晒单/截图/资料), an explanatory line right after an image (`gpt-image-2`, `太可怕了`), or cross-sender ambiguity (B says "这个看着像 X" but the previous image is from A). + +#### Round 2 — Flesh out + write the digest + +For each topic in the skeleton, jump back to its anchor IDs and expand into full content with quotes and clear attribution. Then write the digest file. + +**Image handling** (limited — wx-cli does not decode chat images): + +For each flagged topic, check whether a description file already exists at `{folder}/imgs/{message_id}.txt`. If yes, read it (one-line plain text) and weave its content into the topic. If no, treat the image as opaque (`[图片]`) and write around it — describe what the surrounding messages tell us, but don't invent visual content. + +The `imgs/` directory exists as an **extension point**: a user (or a future wx-cli capability) can drop `{message_id}.txt` files with one-line descriptions, and the skill will pick them up. The skill itself does NOT generate these files in this version. + +**Use the profile context block** (from Step 3.7): + +- Echo continuity for matching behavior ("又双叒叕直播飞行体验") +- Highlight contrast for departures ("一向话少的 XX 今天突然爆发") +- Callback past quotes ("继上次'要不要买 moderna'之后,这次又...") +- Don't sacrifice current material to force a callback. + +**Writing order**: write the body categories first, then the opening overview based on the finished body (so the hook is accurate). + +Detailed structure, voice, formatting rules, and content guidelines are in [references/output-formats.md](references/output-formats.md). Load that file now if not already loaded. + +#### Round 3 — Audit + +Walk the Round 1 skeleton against the finished digest. Check: + +- Any listed topic missing from the digest? +- Quotes, names, product/tool names preserved verbatim? +- Categorization makes sense — is anything in the wrong bucket? + +Fix in place. When clean, confirm and proceed. + +### Step 7: Save the digest file(s) + +If `include_normal`: + +- Single date → `{folder}/YYYY-MM-DD.md` +- Date range → `{folder}/YYYY-MM-DD_YYYY-MM-DD.md` +- Overwrite if the same date/range already exists. + +If `include_roast`: + +- Same naming, but with `-roast` suffix: `YYYY-MM-DD-roast.md` or `YYYY-MM-DD_YYYY-MM-DD-roast.md`. + +Both versions share the same statistics (message count, leaderboard) and the same underlying skeleton. + +### Step 8: Save history (two files) + +Maintain two files in the group folder: + +#### `history.json` — single record, fast read + +Always reflects only the most recent normal digest. Overwrite on each run when `include_normal=true`. + +```json +{ + "group_id": "12345678901@chatroom", + "group_name": "消失的大叔", + "folder": "12345678901@chatroom-消失的大叔", + "last_digest": { + "file": "2026-03-12.md", + "date_range": "2026-03-12", + "generated_at": "2026-03-12T10:30:00+08:00", + "message_count": 150, + "last_message_time": "03-12 18:45" + } +} +``` + +- `group_name` updates on every run (handles renames). +- `folder` records the current folder basename for cross-reference. +- `last_message_time` is the timestamp of the most recent message included, in `MM-DD HH:MM` — used by incremental mode. +- Roast-only runs do NOT touch this file. + +#### `history-digests.jsonl` — append-only archive + +One JSON object per line, same shape as `last_digest`. Every normal-version run appends one line (in chronological order). Used by backfill and historical lookups. Never read for incremental mode (which only needs the latest). + +```jsonl +{"file":"2026-03-10.md","date_range":"2026-03-10","generated_at":"2026-03-10T09:00:00+08:00","message_count":420,"last_message_time":"03-10 22:30"} +{"file":"2026-03-11.md","date_range":"2026-03-11","generated_at":"2026-03-11T09:05:00+08:00","message_count":312,"last_message_time":"03-11 23:10"} +{"file":"2026-03-12.md","date_range":"2026-03-12","generated_at":"2026-03-12T10:30:00+08:00","message_count":150,"last_message_time":"03-12 18:45"} +``` + +If a normal digest with the same `file` name is regenerated, append a new line anyway (the JSONL is a strict log; readers can dedupe by `file` if they need to). + +### Step 8.5: Update user profiles + +For each user with 3+ messages in this batch who appeared in the 群友画像 section: + +- If `include_normal`, update `{folder}/profiles/{wxid}-{nickname}.md`. +- If `include_roast`, update `{folder}/profiles-roast/{wxid}-{nickname}.md`. + +Counts, frontmatter updates, append-only rules for quotes and events, and privacy guardrails are detailed in [references/profiles.md](references/profiles.md). Load that file when running this step. + +### Completion checklist + +Profile updates are easy to forget once the digest is on disk. Before reporting the run as "done", verify every applicable file: + +- [ ] `{folder}/YYYY-MM-DD.md` written (if `include_normal`) +- [ ] `{folder}/YYYY-MM-DD-roast.md` written (if `include_roast`) +- [ ] `{folder}/history.json` overwritten with the new `last_digest` (if `include_normal`) +- [ ] `{folder}/history-digests.jsonl` appended one line (if `include_normal`) +- [ ] `{folder}/profiles/{wxid}-*.md` updated for every user with 3+ messages (if `include_normal`) +- [ ] `{folder}/profiles-roast/{wxid}-*.md` updated for every user with 3+ messages (if `include_roast`) + +If any item is unchecked, finish it before declaring success. Don't ship a digest with a stale `history.json` — incremental mode depends on it. + +### Step 9: Backfill (user-triggered) + +When the user says "回溯画像" / "初始化画像" / "backfill profiles": + +1. Confirm the target group (if not specified, ask which one). +2. List all digest files in `{folder}/` and `history-digests.jsonl`. +3. Read existing digests in batches of 10–15 to avoid context blowup. +4. For users appearing in 3+ digests, seed profile files using their leaderboard counts, portrait paragraphs, and quoted lines from the historical digests. +5. Write to `profiles/` (and `profiles-roast/` if any `-roast.md` files exist). +6. Report back: how many profiles were created, how many users covered. + +Full procedure in [references/profiles.md](references/profiles.md). + +## Storage layout + +``` +{data_root}/ # default: {project_root}/wechat/ +└── {group_id}-{group_name}/ # e.g. 12345678901@chatroom-消失的大叔/ + ├── history.json # last digest pointer (fast) + ├── history-digests.jsonl # append-only archive + ├── 2026-03-12.md # normal digest, single date + ├── 2026-03-12-roast.md # roast digest (only if generated) + ├── 2026-03-10_2026-03-12.md # normal digest, date range + ├── profiles/ # normal user profiles + │ ├── onlytiancai-胡浩🐸.md + │ └── ... + ├── profiles-roast/ # roast user profiles (only if any roast generated) + │ ├── onlytiancai-胡浩🐸.md + │ └── ... + └── imgs/ # optional image-description files + ├── 49661.txt # one-line plain text description + └── ... +``` + +## wx-cli quick reference + +| Command | Purpose | +|---------|---------| +| `wx --version` | Sanity-check that wx-cli is installed | +| `wx sessions --json` | List recent sessions; useful for verifying init and finding the user's own wxid | +| `wx contacts --query "<name>" --json` | Fuzzy-match contacts/groups by display name, remark, or wxid | +| `wx history "<group>" --since DATE --until DATE -n N --json` | Pull a group's messages within a date range as JSON | +| `wx members "<group>" --json` | List a group's members (rarely needed; mostly for completeness) | +| `wx stats "<group>" --since DATE` | wx-cli's built-in stats; we compute our own from `wx history` JSON so the format matches our digest | +| `wx daemon status` / `wx daemon stop` / `wx daemon logs --follow` | Daemon lifecycle (troubleshooting) | + +All `wx` commands accept `--json` for machine-readable output. Default output is YAML — only use it for human eyeballing during debugging. + +## Troubleshooting + +When a `wx` command fails, diagnose by the symptom, not by retrying blindly. Common patterns: + +| Symptom | Cause | Fix (tell the user to run these — do NOT run `sudo` for them) | +|---------|-------|----------------------------------------------------------------| +| `Operation not permitted` / `Access denied to ~/.wx-cli` | Sandbox is on | Re-run the command with `dangerouslyDisableSandbox: true`. Persistent fix: `/sandbox` to allow `~/.wx-cli` and the WeChat data dir. | +| `无法写入 /Users/<u>/.wx-cli` / `Permission denied` | `~/.wx-cli` is owned by root (legacy `sudo wx init`) | `sudo chown -R $(whoami) ~/.wx-cli && sudo rm -f ~/.wx-cli/daemon.{pid,sock} && wx daemon start` | +| `wx history` hangs / times out / returns nothing | Daemon is stuck | `wx daemon stop && rm -f ~/.wx-cli/daemon.{pid,sock} && wx daemon start`, then retry | +| `no keys` / `init required` after the daemon was working | Keys went stale (WeChat restart, version upgrade) | Make sure WeChat is running, then `wx init --force` (non-sudo first; only `sudo` if your wx-cli version requires it) | +| `wx contacts` returns zero rows for a group you know exists | Group is folded into 折叠群 or the daemon hasn't indexed it yet | `wx sessions --json` and search there; if missing, run `wx daemon stop && wx daemon start` and retry | +| Messages returned but `--since` / `--until` window looks wrong | Date string not in `YYYY-MM-DD` format, or off-by-one timezone | Confirm the dates are local-time `YYYY-MM-DD`. Re-filter the JSON by `timestamp` locally as a belt-and-suspenders step. | +| Empty result for a chat that should have activity | `-n` cap too low for a noisy group | Raise `-n` (e.g. to 20000) and re-fetch | + +**Recovery order when nothing makes sense:** + +1. Is WeChat running? +2. Is `~/.wx-cli` owned by `$(whoami)`? +3. Is the daemon healthy? (`wx daemon status`) +4. Restart the daemon (`wx daemon stop && wx daemon start`) +5. Last resort: `wx init --force` (while WeChat is running) + +Never auto-retry inside the skill — every failure should produce a clear diagnostic plus the exact command the user needs to run. + +## Notes and limitations + +- **Image content is opaque**. wx-cli does not decode chat images. The skill respects an `imgs/{message_id}.txt` extension point but does not auto-populate it. When a topic depends heavily on an image with no description file, the digest should say so honestly rather than invent visual content. +- **Reply attribution is best-effort**. If wx-cli's output exposes a quote/reply field, use it. Otherwise fall back to context and flag uncertain inferences in working notes. +- **Local time only**. Date parsing uses the agent's local time zone. Cross-time-zone group members may show timestamps that don't match their wall clock. Per the format rules, never use timestamps to infer sleep or location. +- **wx-cli reinit**. If `wx history` suddenly returns nothing after a WeChat restart, the keys may be stale. Tell the user to run `sudo wx init --force` (while WeChat is running) and retry. diff --git a/skills/baoyu-wechat-summary/references/output-formats.md b/skills/baoyu-wechat-summary/references/output-formats.md new file mode 100644 index 0000000..ad5c058 --- /dev/null +++ b/skills/baoyu-wechat-summary/references/output-formats.md @@ -0,0 +1,349 @@ +# Output formats — normal & roast digest + +This reference defines the two digest variants the skill produces: the **normal** version (default, sober summary) and the **roast** version (毒舌,sarcastic critique, opt-in). Load this file during Step 4 (skeleton) and keep it open through Step 6 (audit). + +Both versions share the same overall layout and writing rules; the differences are tone, the leaderboard annotations, the portraits, and the footer. Write the normal version first when both are requested — it's the anchor for incremental mode and the source of truth for the profile updates. + +--- + +## 1. Normal version + +### 1.1 Five-part structure + +``` +[Title line] +[📊 Stats block + Top 10 leaderboard] +[Opening summary — 1-2 paragraphs of prose] +[群友画像 — one entry per active user (3+ msgs)] +[Categorized body — 3-6 self-named sections per day] +[Optional pain-point section] +[Fixed footer] +``` + +### 1.2 Title line + +- Single line, no markdown heading. +- Form: `{群名} 群聊精华 · {日期或日期区间}` +- Date single day: `2026-03-12`. Date range: `2026-03-12 ~ 2026-03-15`. + +Example: + +``` +相亲相爱一家人 群聊精华 · 2026-03-12 +``` + +### 1.3 Statistics block + +- Starts with `📊 消息统计: 共 N 条消息`. +- Followed by a leaderboard, top 10 senders by message count, one per line. +- Form per line: `{排名}. {昵称}: {消息数} 条` +- Counting rules: + - Include images, emojis, links, voice transcripts — anything that occupies a chat row is one message. + - Exclude system messages and revoked messages (`[系统]`, `revokemsg`). + - For the `self_wxid` user, substitute `self_display` from EXTEND.md before counting/displaying. + - Resolve ambiguous nicknames (per SKILL.md Step 3.6) before tallying so the same person isn't double-counted. + +Example: + +``` +📊 消息统计: 共 387 条消息 +1. 蛙总: 92 条 +2. 老王: 58 条 +3. 阿喵: 41 条 +... +``` + +### 1.4 Opening summary + +- 1-2 paragraphs, plain prose, no headings, no bullets. +- Hook the reader: lead with the most distinctive thread of the day (a heated debate, a surprising announcement, a market move someone reacted to). +- Reference 2-4 of the day's category titles in the prose so the reader knows what's coming. +- Mention 1-2 specific people only if their contribution is central; otherwise stay topic-focused. +- No timestamps, no message counts (those live in the stats block). + +### 1.5 群友画像 section + +- Heading line: `群友画像` +- One entry per user with 3+ messages this batch. +- Order: by message count, descending. +- Entry header: `{昵称}({角色标签})` — the role tag is your one-line read on this person *today*. Examples: `做空美股的乐子人`, `深夜技术指导`, `论坛级吐槽担当`. +- Body: 2-5 bullets with `•` prefix. Each bullet states one observation. Quote evidence inline where natural. +- Continuity: if you loaded a prior profile in Step 3.7, carry forward the established tags/observations that still apply, and call out *change* explicitly (`今天罕见地没提空头`, `从昨天的乐观转向今天的焦虑`). +- Don't invent backstory — only what's in the messages or the prior profile. + +Example: + +``` +群友画像 + +蛙总(做空美股的乐子人) +• 全天反复提"做空 SPY",被群友提醒已连续三周看错方向 +• 难得正面回应技术问题:"我那个脚本是用 Bun 跑的,慢得跟蜗牛似的" +• 临近收盘转为沉默,与昨日大放厥词的状态对比明显 +``` + +### 1.6 Categorized body + +- 3-6 self-named categories per day. +- Each category is a thematic bucket — name it for the *topic*, not generic ("讨论"、"闲聊" are forbidden labels). +- Category header: `{emoji} {标题}` — one emoji prefix, then a short noun phrase. + - Suggested emoji: 🛠 工具/技术,📦 产品发布,📰 新闻/市场,💬 观点辩论,😄 笑料/段子,📚 学习分享,💸 钱与消费,🍜 生活日常。 +- Body inside each category: prose with embedded quotes. Use `•` bullets when listing 3+ parallel items; otherwise paragraphs. +- Attribution: name the speaker on first mention in a thread (`蛙总说他...`). For follow-on lines in the same thread, attribution can be implicit if the chain is short and clear. +- Quotes: use 「」 for direct quotes. Quote when the wording is vivid, surprising, or characteristic; paraphrase otherwise. +- Merge: a multi-person discussion is one entry, not a list of one-line replies. +- Links: preserve the full URL inline. Article titles stay verbatim. + +Example: + +``` +🛠 Claude Code 4.7 实测 + +蛙总下午把 4.7 装上后第一反应是「比 4.6 慢一倍」,老王跟着复现,怀疑是 Opus 默认配置导致。阿喵贴了官方文档 https://docs.claude.com/.../opus-4-7 ,提到可以切回 Sonnet 4.6 跑速测,三人最终结论:复杂任务 4.7 强,日常用 4.6 更顺手。 +``` + +### 1.7 Pain-point section (optional) + +- Include only when the day's chat contains at least one concrete unresolved or partially-resolved problem. +- Heading: `今日待解决问题` or `本周悬而未决`. +- One entry per problem. Format: + ``` + 问题:<一句话描述> + 提出者:<昵称> + 背景:<1-2 句来龙去脉> + 状态:<✅ 已解决 / ⚠️ 部分解决 / ❌ 仍未解决> + 方案:<若有人提了方案,写在这;否则写"暂无方案"> + ``` +- Skip the section entirely if there are no genuine pain points — don't pad with trivial questions. + +### 1.8 Footer + +Fixed line, last in file: + +``` +本简报由 AI 自动生成 +``` + +No date, no signature, no version number. + +--- + +## 2. Roast version (毒舌版) + +The roast version mirrors the normal version's structure but inverts the tone. Generate only when `include_roast=true` (configured via EXTEND.md `default_version` or triggered by request keywords like 毒舌/roast/挑衅). + +### 2.1 Structural parity + +``` +[Title line — adds "毒舌版" suffix] +[📊 Stats block — each leaderboard row gets a roast comment] +[Opening summary — absurd recap, sarcastic] +[群友画像 — 不留情面版] +[Categorized body — louder, more brutal category titles] +[Fixed footer — roast version] +``` + +Pain-point section is **dropped** in the roast version. + +### 2.2 Title line + +Form: `{群名} 群聊精华 · {日期} · 毒舌版` + +Example: `相亲相爱一家人 群聊精华 · 2026-03-12 · 毒舌版` + +### 2.3 Statistics block (roast) + +- Same `📊 消息统计: 共 N 条消息` opener. +- Leaderboard: each row gets a parenthetical roast comment. +- Form: `{排名}. {昵称}: {消息数} 条 ({一句毒舌评语})` + +Example: + +``` +📊 消息统计: 共 387 条消息 +1. 蛙总: 92 条 (一个人撑起了空头的体面) +2. 老王: 58 条 (主要功能是给蛙总当反例) +3. 阿喵: 41 条 (发的链接比发的话还多) +``` + +### 2.4 Opening summary (roast) + +- 1-2 paragraphs, sarcastic recap tone. +- Highlight the day's most ridiculous beat (a failed prediction, a heated argument over something trivial, a wild flex). +- Reference 2-3 of the roast category titles in the prose. + +### 2.5 群友画像 (roast — 不留情面版) + +- Same per-user entry format as normal, but with the role tag dialed up. +- Amplify quirks, contradictions, fail moments visible in the messages. +- 2-5 bullets with `•`. Each bullet is a roastable observation backed by a direct quote. +- Don't invent — every roast must trace back to something the person actually said in the batch (or in the loaded roast profile from prior batches). + +Example: + +``` +群友画像 + +蛙总(做空美股钉子户,三周亏损不改其志) +• 今天第 47 次预测 SPY 见顶,给出的理由是"我感觉" +• 被老王翻出上周聊天记录后嘴硬:「上周的不算,那是市场不理性」 +• 收盘前突然安静,疑似刚看完账户 +``` + +### 2.6 Categorized body (roast) + +- Same 3-6 category structure. +- Titles can be louder, more brutal, mock-headline style. +- Examples of acceptable roast titles: + - `蛙总做空翻车: 一个人对抗整个美股` + - `老王再次试图当人生导师, 群友集体打哈欠` + - `阿喵又分享了一篇没人读完的长文` +- Bodies still preserve real quotes and traceability — sarcasm is in framing, not fabrication. + +### 2.7 Footer (roast) + +Fixed line, last in file: + +``` +本简报由一个没有感情的 AI 自动生成,如有冒犯,概不负责 +``` + +### 2.8 Red lines (non-negotiable) + +These rules override style; violate them and the roast becomes harmful. Write the spicy version first, then audit against this list and rewrite anything that crosses a line. + +- **Mock public group behavior only.** Never appearance, weight, body, health, mental state, family, relationships (unless openly group-discussed at the person's own initiative), finances beyond what they publicly mention. +- **调侃 ≠ 人身攻击。** Readers should laugh at the situation, not feel sorry for the target. If you can't think of a joke that doesn't read as cruel, drop the bullet. +- **No timestamp-based sleep/timezone jokes.** Server time ≠ recipient's local time; also implies surveillance. Forbidden: `凌晨 3 点还在群里发言,怕是没睡好`, `这位是哪个时区的`, etc. +- **No medical/psychological speculation.** Even joking diagnoses (`这位需要看医生`, `典型 ADHD`) are out. +- **No outing.** Don't infer identity attributes the person hasn't volunteered (orientation, religion, politics beyond direct quotes). +- **Roast the take, not the person's right to have a take.** `这个观点错得离谱` is fine. `连这都不懂还敢发言` is not. + +If a target hasn't said anything roastable in this batch (3+ messages but all neutral), give them a one-line affectionate ribbing and move on. Don't manufacture conflict. + +### 2.9 Writing order + +1. Draft the spicy version freely — don't self-censor mid-sentence. +2. After the body is complete, do a separate audit pass against §2.8. +3. Rewrite or delete any line that crosses a red line. +4. Then read the whole thing once more for tone calibration: is it fun, or is it mean? + +--- + +## 3. Common formatting rules (both versions) + +- **No markdown.** No `**bold**`, no `# headings`, no `*italic*`, no `[link](url)` syntax. Headings are plain text on their own line. +- **Bullets use `•`.** Not `-`, not `*`, not `1.` for prose-style bullets. +- **Numbered lists** (`1.`, `2.`) are reserved for the leaderboard. +- **Subcategory hints** within a body block are plain text with no symbol prefix. +- **Links preserved verbatim.** Paste the full URL inline. Don't shorten, don't hide behind text. +- **One emoji per category title.** Don't stack 🛠💬 etc. +- **Pain-point statuses** use ✅⚠️❌ verbatim. +- **Quotes use 「」.** Single quotes for nested. +- **Names verbatim.** Don't abbreviate `蛙总` to `蛙`, don't translate Chinese names, don't anonymize. + +--- + +## 4. Common content rules (both versions) + +- **Filter only pure noise.** Cut: lone emoji reactions, "好的"/"收到"/"哈哈哈" with no follow-on, duplicate forwards. +- **Keep gossip, anecdotes, signature moments.** These are the highlight reel — the whole point of the digest. +- **Plain language.** Preserve vivid expressions and idiosyncratic phrasings — that's what makes the speaker recognizable. +- **Keep real names.** Both for traceability and so the digest is useful as memory. +- **Tool, product, URL names complete.** `Claude Code 4.7`, not `CC`. `https://github.com/...`, not `GitHub 上那个项目`. +- **Merge, don't list.** A 30-message debate becomes one paragraph, not 30 bullet points. +- **Direct-quote deep observations.** When someone says something striking, quote it verbatim with 「」 rather than paraphrase. +- **Shared articles → title + sharer.** `阿喵分享了《一个 Rust 工程师的反思》` — include the title and who shared. +- **No timestamp-based sleep/timezone inference.** (Repeated here because it applies to both versions, not just roast — never say `凌晨 3 点还在线` in either.) +- **No fabricated facts.** Every claim must be supported by an actual message in the batch (or in a loaded profile). If you're tempted to "add color," stop. + +--- + +## 5. Output skeleton — quick reference + +When you forget the structure mid-write, this is the skeleton: + +### Normal + +``` +{群名} 群聊精华 · {日期} + +📊 消息统计: 共 N 条消息 +1. {昵称}: N 条 +2. {昵称}: N 条 +... +10. {昵称}: N 条 + +{开篇 1-2 段,无标题,直入主题} + +群友画像 + +{昵称}({角色标签}) +• {观察 1} +• {观察 2} +• {观察 3} + +{昵称}({角色标签}) +• {观察 1} +• {观察 2} + +🛠 {分类标题 1} + +{该分类下的整理过的讨论 / 段落 / 引用} + +📦 {分类标题 2} + +{...} + +今日待解决问题(可选,没有就不写) + +问题: {一句话} +提出者: {昵称} +背景: {1-2 句} +状态: ⚠️ 部分解决 +方案: {若有} + +本简报由 AI 自动生成 +``` + +### Roast + +``` +{群名} 群聊精华 · {日期} · 毒舌版 + +📊 消息统计: 共 N 条消息 +1. {昵称}: N 条 ({毒舌评语}) +2. {昵称}: N 条 ({毒舌评语}) +... + +{毒舌开篇 1-2 段} + +群友画像 + +{昵称}({放大的角色标签}) +• {毒舌观察 1} +• {毒舌观察 2} + +🛠 {更大声的分类标题} + +{保留真实引用的毒舌叙述} + +本简报由一个没有感情的 AI 自动生成,如有冒犯,概不负责 +``` + +--- + +## 6. Self-check before saving + +Before writing the digest file, mentally walk through: + +1. Stats block accurate? Counts match the filtered message set? +2. Top 10 names resolved (self_display substituted, ambiguous nicknames disambiguated)? +3. Opening hooks at least one real category title? +4. Every active user (3+ msgs) has a 画像 entry? +5. Every category has a topic-named title (not "讨论")? +6. Every quote uses 「」 and is traceable to a real message? +7. Links inline and complete? +8. No markdown bold/heading/link syntax leaked through? +9. (Roast only) Every roast bullet would pass the §2.8 red-line audit? +10. Footer line exact match? diff --git a/skills/baoyu-wechat-summary/references/profiles.md b/skills/baoyu-wechat-summary/references/profiles.md new file mode 100644 index 0000000..d3217ad --- /dev/null +++ b/skills/baoyu-wechat-summary/references/profiles.md @@ -0,0 +1,273 @@ +# Profiles — user portrait files + +This reference defines the per-user profile system. Profiles let the digest carry forward observations across many days so the 群友画像 section in each new digest can show continuity (`蛙总今天罕见地没提空头`) instead of starting from scratch. + +Two parallel profile directories live alongside each group's digests: + +- `profiles/` — observations sourced from the **normal** version of the digest. +- `profiles-roast/` — observations sourced from the **roast** version. + +They are kept strictly separate. The normal-version generation reads only `profiles/`; the roast-version generation reads only `profiles-roast/`. This prevents roast snark from contaminating the sober summary and vice versa. + +Load this file during Step 3.7 (load profiles for active users), Step 8.5 (update profiles after digest is written), and Step 9 (backfill). + +--- + +## 1. File format + +### 1.1 Path & naming + +- Normal: `wechat/{group_id}-{group_name}/profiles/{wxid}-{nickname}.md` +- Roast: `wechat/{group_id}-{group_name}/profiles-roast/{wxid}-{nickname}.md` + +The **stable** identifier is the `wxid` prefix. The `-{nickname}` suffix is for human browsability — if it changes, rename the file. + +Filename sanitization: replace `/`, `\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`, NUL, and control characters with `_`. Trim trailing dots and whitespace. Cap total filename length at 200 chars (rare nicknames can be very long). + +### 1.2 Frontmatter + +YAML frontmatter at the top of every profile file: + +```yaml +--- +name: "<current display name>" +wxid: "<wxid>" +aliases: ["<old nickname>", "<even older nickname>"] +first_seen: "YYYY-MM-DD" +last_seen: "YYYY-MM-DD" +total_messages: N +digest_appearances: N +avg_messages_per_digest: N.N +--- +``` + +Field rules: + +- `name`: the most recent display name from `from_nickname` (or `self_display` for the owning user). +- `wxid`: stable; never changes once written. +- `aliases`: append-only; every prior display name we've seen. Don't include the current `name` in this list. +- `first_seen` / `last_seen`: dates of first/most-recent digest appearance, YYYY-MM-DD. +- `total_messages`: cumulative count across all digests this profile has been updated from. +- `digest_appearances`: how many digest files this user has 3+ messages in. +- `avg_messages_per_digest`: `total_messages / digest_appearances`, one decimal. + +### 1.3 Free-form body — normal profile + +Section headers are plain text on their own line. Order is fixed. + +``` +角色标签 + +• {4-6 短语标签} + +关注领域 + +• {领域 1} +• {领域 2} + +发言风格 + +{1-3 句描述,可以多段} + +互动模式 + +• {与某某的互动模式} +• {另一种互动模式} + +经典金句 + +• [YYYY-MM-DD] 「{直接引用}」 +• [YYYY-MM-DD] 「{直接引用}」 + +标志性事件 + +• [YYYY-MM-DD] {事件描述} +• [YYYY-MM-DD] {事件描述} +``` + +### 1.4 Free-form body — roast profile + +Same plain-text section header style, different sections. + +``` +人设标签 + +• {4-6 放大版标签} + +核心槽点 + +• {可吐槽点 1} +• {可吐槽点 2} + +毒舌语录库 + +• [YYYY-MM-DD] 「{该用户说过的话} — {简短毒舌点评}」 +• [YYYY-MM-DD] 「{...}」 + +经典翻车现场 + +• [YYYY-MM-DD] {翻车描述 + 引用 / 证据} +• [YYYY-MM-DD] {...} +``` + +--- + +## 2. Update rules + +Rules differ per section. Append-only sections must never lose history; mergeable sections may be rewritten as understanding sharpens. + +### 2.1 Normal profile + +| Section | Update mode | Notes | +|---------|-------------|-------| +| 角色标签 | **Merge** | Cap 4-6 tags. Can replace less representative tags with stronger ones. Always keep the most consistently-supported tag. | +| 关注领域 | **Merge dedupe** | Add new domains; dedupe by meaning, not exact string. | +| 发言风格 | **Refine** | Only update when a clearly new pattern emerges. Avoid rewriting on every digest. | +| 互动模式 | **Merge** | Add new modes; can refine existing ones with more detail. | +| 经典金句 | **Append-only** | Never delete. No cap. Each entry must be dated and quoted verbatim. | +| 标志性事件 | **Append-only** | Never delete. No cap. Each entry dated. | + +### 2.2 Roast profile + +| Section | Update mode | Notes | +|---------|-------------|-------| +| 人设标签 | **Merge** | Cap 4-6. Can sharpen tags as patterns repeat. | +| 核心槽点 | **Append-only** | Never delete; recurring 槽点 build up here. | +| 毒舌语录库 | **Append-only** | Never delete. No cap. Each entry dated, with both the quote and the roast comment. | +| 经典翻车现场 | **Append-only** | Never delete. No cap. Each entry dated. | + +### 2.3 Frontmatter on every update + +- Update `name` if current display name differs from the recorded one. Push the old name onto `aliases` if not already there. +- If `name` changed, also rename the file from `{wxid}-{old_nickname}.md` to `{wxid}-{new_nickname}.md`. +- Update `last_seen` to the current digest's end date. +- Increment `total_messages` by this batch's message count for this user. +- Increment `digest_appearances` by 1. +- Recompute `avg_messages_per_digest`. + +--- + +## 3. Step 8.5 — Update procedure + +Run after the digest file(s) are written. Iterate over every user with 3+ messages in this batch. + +1. **Look up the profile.** + - Scan `profiles/` (or `profiles-roast/` for the roast pass) for a file whose name starts with `{wxid}-`. + - If found: open it. + - If not found: create a new file using the frontmatter template. `first_seen = last_seen = current digest end date`, `total_messages = this batch's count`, `digest_appearances = 1`. + +2. **Resolve wxid for new users.** When a new user appears, you already know their `wxid` from the wx-cli message data — use it directly. If for some reason only the nickname is known, run `wx contacts --query "{nickname}" --json` to resolve; if multiple matches, prefer the one currently in the group (cross-check `wx members <group>` if needed). + +3. **Update frontmatter.** Per §2.3. + +4. **Update body sections.** + - For mergeable sections (角色标签,关注领域,发言风格,互动模式 / roast: 人设标签): read the existing content, integrate new observations from this batch, rewrite the section. + - For append-only sections (经典金句,标志性事件 / roast: 毒舌语录库,经典翻车现场,核心槽点): append new entries, each dated and verbatim. Never edit or remove prior entries. + +5. **Write back.** Overwrite the file. + +6. **Source separation.** Pass running for the normal digest writes only to `profiles/`. Pass running for the roast digest writes only to `profiles-roast/`. Even if both versions are generated in the same skill invocation, run two separate update passes. + +--- + +## 4. Step 9 — Backfill procedure + +Triggered when the user says `回溯画像`, `初始化画像`, `backfill profiles`, or similar. This builds initial profiles from already-written digest files without re-fetching from wx-cli. + +1. **List inputs.** + - List every `*.md` digest file under `wechat/{group_id}-{group_name}/` (top level, not inside `profiles/` or `profiles-roast/`). + - Partition by filename suffix: `*-roast.md` → roast pass, all others → normal pass. + - Optionally also read `history-digests.jsonl` for fast metadata lookup (date, message count) before opening individual files. + +2. **Decide whether to run roast backfill.** Only run the roast pass if at least one `*-roast.md` file exists. + +3. **Process in batches of 10-15 digest files.** Reading all of them at once will blow context. For each batch: + - Read the digests. + - For each user appearing in the leaderboard or 群友画像 across the batch, accumulate: + - Message counts per digest (from the stats block). + - Role tags and observations (from the 群友画像 section). + - Quotes (from inline 「」 in the body). + - Dated events (from category bodies — when the digest mentions specific incidents). + - Resolve wxid for each accumulated user via `wx contacts --query "{nickname}" --json` if not already cached. Cache the wxid↔nickname mapping for the rest of the backfill. + +4. **Threshold.** Generate a profile file only for users appearing in **3 or more** digests in the corpus. Below that, skip (probably one-time visitors). + +5. **Write profile files.** + - For the normal pass, write to `profiles/{wxid}-{nickname}.md`. + - For the roast pass, write to `profiles-roast/{wxid}-{nickname}.md`. + - Use the most recent nickname as the filename suffix. Push older nicknames into `aliases`. + - Sort 经典金句,标志性事件,毒舌语录库,经典翻车现场 entries chronologically by date. + - No cap on the size of append-only sections during backfill — let history flow in. + +6. **Compute frontmatter.** + - `first_seen` = earliest digest date the user appeared in. + - `last_seen` = latest digest date the user appeared in. + - `total_messages` = sum of per-digest counts. + - `digest_appearances` = number of digests the user crossed the 3-message threshold in. + +7. **Report.** After both passes complete, print a short summary: + - `Backfilled {N} normal profiles from {M} digests.` + - `Backfilled {K} roast profiles from {L} roast digests.` (only if roast pass ran) + - List any users skipped due to wxid resolution failures so the user can fix manually. + +8. **Re-running backfill is safe.** If the user runs backfill twice, treat existing profile files as the prior state and merge — same rules as Step 8.5 updates. Don't blow away existing append-only entries. + +--- + +## 5. Privacy guardrails + +These apply to both normal and roast profiles, with an extra layer for roast. + +### 5.1 Forbidden (write neither in normal nor roast) + +- **Real-world full names** when only a nickname was used in the group. If the person introduced themselves with `我叫王二`, `王二` is on the table; `王晓明` inferred from another channel is not. +- **Phone numbers, emails, ID numbers, home addresses, employer addresses, exact birth dates** — even if mentioned in the group, don't lift them into profile files. +- **Health, medical, psychological information.** Even self-disclosed (`我最近有点抑郁`) — don't bake it into a permanent profile. +- **Private romantic / family details** unless openly group-discussed by the person themselves. A passing mention by another member doesn't count. +- **Embarrassing private failures.** Public ones (a take that aged badly in front of the group) are fair game; private ones (a job rejection mentioned briefly) are not. +- **Sleep / timezone inference from timestamps.** Server time ≠ recipient's local time, and it implies surveillance. + +### 5.2 Allowed + +- **Public group behavior** — what they said, how they argued, what they shared. +- **Direct quotes** of things said in the group (these are already public to the group). +- **Interest areas, hobbies, tool preferences** as expressed in group discussion. +- **Interaction patterns** with other group members. +- **Publicly mentioned consumption** (`蛙总今天又分享了买了什么书`) — fine if they themselves mentioned it. +- **Publicly shared travel / life anecdotes** they told the group. + +### 5.3 Roast-only extras + +In addition to §5.1, the roast profile must **not** include: + +- **Anything about appearance, weight, body, looks.** +- **Anything about family members** (their kids, parents, partners) — only the person themselves. +- **Mental-health speculation**, even as a joke. No `这位需要看医生`, no `典型 ADHD`. +- **Identity-based roasts.** No mocking of orientation, religion, ethnicity, nationality, gender. + +The roast may mock: + +- Stupid takes, contradictions, factual errors. +- Repetitive behavior (`第 47 次预测见顶`). +- Self-undermining moments (`昨天说 X,今天说 not X`). +- Performative flexes that didn't land. + +The rule of thumb: **roast the take, not the person.** + +--- + +## 6. Reading profiles during digest generation (Step 3.7) + +When loading profile context for a fresh digest: + +1. Iterate over users active in this batch (3+ messages). +2. For the normal pass, read `profiles/{wxid}-*.md` for each. Skip if missing. +3. If the current run also generates the roast version, **separately** read `profiles-roast/{wxid}-*.md` during the roast generation pass. +4. Compile a condensed working-memory block: + - The user's current `name` and `aliases` (so you can recognize them under different names). + - 角色标签 / 人设标签 (so you can carry forward or contrast). + - The 3-5 most recent 经典金句 / 毒舌语录 entries (so you can detect callbacks and repeats). + - The 3-5 most recent 标志性事件 / 翻车现场 entries (so you can spot recurring themes). +5. Don't dump the entire profile into the digest — the profile is *context*, the digest is *today*. + +If a profile contradicts what you see in today's batch (e.g., the profile says `从不主动发起话题`, but today they started three threads), call that out explicitly in the day's 群友画像 — that's the kind of contrast that makes the digest interesting.