# Baoyu Wechat Summary

> 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), per-user profiles, and per-group fact memory (memory.md) across runs, with privacy guardrails baked in. Use when the user asks…

- **Type:** Skill
- **Install:** `agentstack add skill-jimliu-baoyu-skills-baoyu-wechat-summary`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [JimLiu](https://agentstack.voostack.com/s/jimliu)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [JimLiu](https://github.com/JimLiu)
- **Source:** https://github.com/JimLiu/baoyu-skills/tree/main/skills/baoyu-wechat-summary

## Install

```sh
agentstack add skill-jimliu-baoyu-skills-baoyu-wechat-summary
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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. |
| `bot_aliases` | comma-separated strings | `bot, 精华bot` | Names that trigger the 「@bot 答疑」 section. A message containing `@` (case-insensitive) is treated as a question/request aimed at the digest bot. Pick names that do NOT match any real group member or existing bot, to avoid ambiguity. |

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`
- `data_root` — where digest folders live. Default: `{project_root}/wechat`. Enter a custom absolute path (e.g. `~/Documents/wechat-digests`) or leave blank for default.
- Save location — pick one of project / XDG / home

Write EXTEND.md to the chosen path. If the user provided a non-default `data_root`, include it as an uncommented line; otherwise omit it (the default applies automatically). 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 " --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 `` or `[链接/文件] ` → 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 "" --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.7.5: Load group memory（群级事实记忆）

除了按人的 profiles，每个群还有一份全局事实记忆 `{folder}/memory.md`，记录群友指正过、确认过的客观事实（如"某个报错提示的真实原因"、"某产品名的正确写法"、"某事件的实际经过"）。

1. 如果 `memory.md` 存在，读入作为内部背景知识（不写入最终摘要）
2. **写摘要时必须遵守其中的事实修正**——上一期摘要里说错、已被群友指正的说法，这一期绝不能再犯。例如记忆中有"『当前微信版本不支持』是 AI Agent 无法获取微信链接导致的提示，普通用户可正常打开"，就不能再把它当成"骗点击"的梗来写
3. 记忆条目是事实约束，不是风格指令——它只纠正"说什么"，不改变 normal/roast 两个版本各自的语气和写法
4. 标注为「群友说法（未验证）」的条目，引用时保留这个限定，不当成已证实的事实陈述
5. 文件不存在则跳过，属正常情况

### 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.

### Step 3.9: Detect @bot requests (if any)

Some group members address the digest bot directly — e.g. `@bot 帮我把昨天的讨论捋一下` or `@精华bot 这个链接讲了啥`. Catch these so each digest can answer them in a dedicated section instead of dropping them as noise.

**Trigger**: a message whose text contains `@` for any alias in `bot_aliases` (from EXTEND.md; default `bot`, `精华bot`; case-insensitive). Aliases are stored as bare names — match the `@` prefix plus the alias.

**Extract** into an internal worklist `== @bot 请求清单 ==` (working memory only — never written to the final digest):

- Asker's real name — after Step 3.6 resolution; substitute `self_display` for the `self_wxid` user.
- Request body — the text after stripping the `@` prefix. If the message is a reply (per Step 3.5's quote/reply fields), include the quoted message as context.
- Anchor `local_id` for back-reference.

**Misfire filtering**: if a real member's nickname happens to equal an alias, judge by context. Keep only messages genuinely aimed at the digest bot (a question or request for it); skip clear person-to-person talk — a reply to that real person, or banter teasing them. (Choosing a `bot_aliases` value no real member uses avoids this at the source; the filter is a backstop.) Pure greetings/banter (`@bot 在吗`) may be kept with a brief reply.

**Answer-source constraint** (honored when rendering the section per [references/output-formats.md](references/output-formats.md)): answer from the group chat context plus your own knowledge only — **no web access**. For any request needing real-time or external information you can't verify, say so honestly (`这个我查不到实时数据，需要联网确认`) rather than fabricating.

**No hits** → both versions omit the @bot 答疑 section entirely.

Do this in the same read-through as Round 1's skeleton (via its `== @bot 请求清单 ==` block) so the messages aren't scanned twice.

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）—

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [JimLiu](https://github.com/JimLiu)
- **Source:** [JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-jimliu-baoyu-skills-baoyu-wechat-summary
- Seller: https://agentstack.voostack.com/s/jimliu
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
