mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-09 12:41:32 +00:00
feat(baoyu-wechat-summary): add WeChat group chat summary skill
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -175,3 +175,4 @@ youtube-transcript/
|
||||
.omx/
|
||||
.codex-tmp/
|
||||
outputs/
|
||||
wechat/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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` 文件中设置:
|
||||
|
||||
@@ -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
|
||||
@@ -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 "<own nickname>"`, 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 "<group_name>" --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 "<group_name_or_id>" --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 "<group_name_or_id>" --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
|
||||
- `[链接] <title>` 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.
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user