Compare commits

...

6 Commits

Author SHA1 Message Date
Jim Liu 宝玉 a4e78af813 Merge branch 'main' of https://github.com/JimLiu/baoyu-skills 2026-07-01 23:41:41 -05:00
Jim Liu 宝玉 1378544ea2 baoyu-wechat-summary v1.118.0: 归因校验、群主查证、memory 强制留痕、机械计数 2026-07-01 21:34:41 -05:00
wangruofeng c9a50cc908 fix(baoyu-post-to-x): 修复紧贴中文的加粗/斜体在 X Articles 渲染为字面 ** 的问题 (#189)
* fix(baoyu-post-to-x): render CJK-adjacent bold/italics in articles

md-to-html.ts preprocessed markdown with remark + remark-cjk-friendly, then re-stringified and re-parsed with marked. remark-cjk-friendly parses emphasis adjacent to CJK correctly, but remark-stringify re-emits the **bold*/*italic* markers, which marked then FAILS to parse: its flanking test does not treat a CJK character after the closing delimiter as valid punctuation/whitespace. Result: literal **...** asterisks leak into the pasted X Article body whenever the closing delimiter is directly followed by a CJK character.

Fix: convert strong/emphasis mdast nodes (already identified correctly by remark-cjk-friendly) into raw inline <strong>/<em> HTML before stringify, so marked passes them through untouched. A small recursive serializer preserves nested links, inline code, images, etc.

- add remarkStrongEmToHtml remark plugin + inline mdast serializer
- add unist-util-visit as an explicit dependency
- add regression test covering CJK-adjacent bold and italics

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(baoyu-post-to-x): 修复紧贴中文的加粗/斜体在 X Articles 渲染为字面 ** 的问题

按维护者建议改用 remark-cjk-friendly 标准用法,替代自定义插件/序列化器:

- 删除 remarkStrongEmToHtml 插件与 serializeMdastNode 序列化器
- preprocessCjkMarkdown 简化为 remarkParse + remarkCjkFriendly + remarkStringify 往返
- 移除原 markdown 阶段的 &#x...; 实体解码(该解码会还原 cjk-friendly 为
  骗过 marked flanking 判定而生成的实体,正是 CJK 加粗失败的根因)
- 改在 marked 渲染出最终 HTML 后再 decodeHtmlEntities,此时 markdown 语法
  已不存在,解码安全且保持输出干净
- 移除不再使用的 unist-util-visit 显式依赖
- 测试补充引用式链接 **[docs][d]** 在强调内的回归断言(顺带修复 Codex
  指出的 linkReference 渲染为纯文本的 P2 问题)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(baoyu-post-to-x): 移除渲染后无差别实体解码,避免篡改作者字面实体

回应 Codex review (P2):decodeHtmlEntities 对整个渲染 HTML 做无差别
&#x...; 解码,会把作者为显示字面 HTML 而写的实体(如 &#x3C;b&#x3E;)
解码成真实标签,存在内容篡改与 HTML 注入风险。

该解码本就只是为了让输出 HTML「好看」,而 remark-cjk-friendly 引入的
实体是合法 HTML 字符引用,粘进 X 编辑器时能正确渲染;且这些实体只会
出现在强调 span 的外侧(边界字符),不会出现在 span 内部。因此直接
移除解码,既消除过度解码风险,又无功能损失。

- 删除 decodeHtmlEntities 及其在 convertMarkdownToHtml 的调用
- 新增测试:作者字面实体 &#x3C;b&#x3E; 不被解码为真实 <b>,CJK 加粗
  仍正常渲染
- bun test 8 个用例全部通过;真实文章端到端验证 17 处加粗、0 字面 **、
  0 真实标签注入

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: ruofeng <ruofeng.wang@rd.group>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-28 01:42:07 -05:00
Jim Liu 宝玉 348dc82612 Add explicit MIT license 2026-06-19 23:01:55 -05:00
Jim Liu 宝玉 dba3d732c3 Merge pull request #187 from jiji262/fix/gemini3-image-extraction
fix(gemini-web): recover generated images on Gemini 3
2026-06-19 22:45:31 -05:00
jiji262 661bdd08ff fix(gemini-web): recover generated images on Gemini 3
Gemini 3 stopped emitting the legacy `image_generation_content` marker in
the candidate text and moved the generated image into a later response
part. The `wants_generated` gate only checked the candidate text / a single
structured field, so on Gemini 3 it was never true and the image was
silently dropped: generate_content returned an empty result even though the
gg-dl image URL was present in the raw response.

- Broaden `wants_generated` to also fire when the raw response contains a
  `gg-dl/` generated-image URL or an `image_generation_content` marker.
- When a candidate has no image part, skip extraction instead of throwing
  ImageGenerationError, so multi-candidate responses don't fail wholesale.

Verified against a live Gemini 3 account: image generation returns the
generated image again with the default gemini-3-pro model.
2026-06-20 11:37:12 +08:00
14 changed files with 180 additions and 20 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Jim Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3 -1
View File
@@ -1387,7 +1387,9 @@ This project was inspired by and builds upon the following open source projects:
## License
MIT
Unless otherwise noted, this repository is licensed under the [MIT License](./LICENSE).
Published ClawHub skills follow ClawHub registry rules and are distributed under `MIT-0`. Third-party code and assets retain their original licenses where noted.
## Star History
+3 -1
View File
@@ -1388,7 +1388,9 @@ HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 /baoyu-danger
## 许可证
MIT
除另有说明外,本仓库采用 [MIT License](./LICENSE) 授权。
发布到 ClawHub 的 skill 根据 ClawHub registry 规则以 `MIT-0` 分发。第三方代码和素材按其注明的原始许可授权。
## Star History
+5
View File
@@ -5,6 +5,7 @@
"packages": {
"": {
"name": "baoyu-skills",
"license": "MIT",
"workspaces": [
"packages/*"
],
@@ -5018,12 +5019,14 @@
},
"packages/baoyu-chrome-cdp": {
"version": "0.1.1",
"license": "MIT",
"engines": {
"bun": ">=1.2.0"
}
},
"packages/baoyu-codex-imagegen": {
"version": "0.1.0",
"license": "MIT",
"bin": {
"codex-imagegen": "src/main.ts"
},
@@ -5033,6 +5036,7 @@
},
"packages/baoyu-fetch": {
"version": "0.1.2",
"license": "MIT",
"dependencies": {
"@mozilla/readability": "^0.6.0",
"chrome-launcher": "^1.2.1",
@@ -5061,6 +5065,7 @@
},
"packages/baoyu-md": {
"version": "0.1.1",
"license": "MIT",
"dependencies": {
"fflate": "^0.8.2",
"front-matter": "^4.0.2",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "baoyu-skills",
"private": true,
"license": "MIT",
"type": "module",
"workspaces": [
"packages/*"
+1
View File
@@ -1,6 +1,7 @@
{
"name": "baoyu-chrome-cdp",
"version": "0.1.1",
"license": "MIT",
"type": "module",
"files": [
"dist",
@@ -1,6 +1,7 @@
{
"name": "baoyu-codex-imagegen",
"version": "0.1.0",
"license": "MIT",
"type": "module",
"description": "Generate images via Codex CLI's built-in image_gen tool from non-Codex runtimes (Claude Code, Hermes, …).",
"bin": {
+1
View File
@@ -1,6 +1,7 @@
{
"name": "baoyu-fetch",
"version": "0.1.2",
"license": "MIT",
"description": "Read URLs into high-quality Markdown or JSON with Chrome CDP and site adapters.",
"type": "module",
"bin": {
+1
View File
@@ -1,6 +1,7 @@
{
"name": "baoyu-md",
"version": "0.1.1",
"license": "MIT",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -456,19 +456,19 @@ export class GeminiClient extends GemMixin {
const generated_images: GeneratedImage[] = [];
const wants_generated =
get_nested_value(candidate, [12, 7, 0], null) != null ||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text) ||
// Gemini 3 no longer emits the legacy marker in the candidate text; the generated
// image arrives in a later response part. Detect it from the raw response instead.
/\/gg-dl\//.test(txt) ||
/image_generation_content\/\d+/.test(txt);
if (wants_generated) {
const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
const img_body = image_part?.body ?? null;
if (!img_body) {
throw new ImageGenerationError(
'Failed to parse generated images. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
);
}
const img_candidate = get_nested_value<unknown[]>(img_body, [4, candidate_index], []);
// Not every candidate carries a generated image (e.g. multi-candidate responses).
// If this candidate has no image part, skip extraction instead of failing the whole call.
const img_candidate = img_body ? get_nested_value<unknown[]>(img_body, [4, candidate_index], []) : [];
const finished = get_nested_value<string | null>(img_candidate, [1, 0], null);
if (finished) {
text = finished.replace(/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/g, '').trimEnd();
@@ -98,3 +98,68 @@ test('parseMarkdown resolves encoded spaces and literal percent image paths', as
assert.equal(result.contentImages[0]?.localPath, path.join(root, 'Pasted image.png'));
assert.equal(result.contentImages[1]?.localPath, path.join(root, '100%.png'));
});
test('parseMarkdown renders CJK-adjacent bold and italics (no literal asterisks)', async (t) => {
const root = await makeTempDir('x-md-to-html-cjk-bold-');
t.after(() => fs.rm(root, { recursive: true, force: true }));
const markdownPath = path.join(root, 'post.md');
const tempDir = path.join(root, 'tmp');
await fs.mkdir(tempDir, { recursive: true });
await fs.writeFile(
markdownPath,
[
'# 标题',
'',
'分工在变细。**国际大厂卷基础设施,中文项目卷场景落地。**这其实是生态成熟的表现。',
'',
'半角场景 **Top 10 里平均有 8 个** 项目。',
'',
'斜体 *数据来源 GitHub* 收尾。',
'',
'参考 **[docs][d]** 了解更多。',
'',
'[d]: https://example.com',
].join('\n'),
);
const result = await parseMarkdown(markdownPath, { tempDir });
// Bold directly adjacent to CJK (closing ** followed by CJK) must render.
assert.match(result.html, /<strong>国际大厂卷基础设施,中文项目卷场景落地。<\/strong>/);
assert.match(result.html, /<strong>Top 10 里平均有 8 个<\/strong>/);
// Italics.
assert.match(result.html, /<em>数据来源 GitHub<\/em>/);
// Reference-style links inside emphasis must render as links, not plain text.
assert.match(result.html, /<strong><a href="https:\/\/example\.com" rel="noopener noreferrer nofollow">docs<\/a><\/strong>/);
// No literal emphasis delimiters should leak into the output.
assert.doesNotMatch(result.html, /\*\*/);
assert.doesNotMatch(result.html, /(?<!\*)\*(?!\*)[^*\n]+\*(?!\*)/);
});
test('parseMarkdown does not decode author-written literal HTML entities into tags', async (t) => {
const root = await makeTempDir('x-md-to-html-entities-');
t.after(() => fs.rm(root, { recursive: true, force: true }));
const markdownPath = path.join(root, 'post.md');
const tempDir = path.join(root, 'tmp');
await fs.mkdir(tempDir, { recursive: true });
await fs.writeFile(
markdownPath,
[
'# 标题',
'',
'正文中写 &#x3C;b&#x3E;literal&#x3C;/b&#x3E; 想显示字面标签。**加粗**收尾。',
'',
'代码里写 `&#x3C;b&#x3E;` 同样保留。',
].join('\n'),
);
const result = await parseMarkdown(markdownPath, { tempDir });
// CJK-adjacent bold still renders.
assert.match(result.html, /<strong>加粗<\/strong>/);
// Author-written literal entities must NOT be decoded into real tags.
assert.doesNotMatch(result.html, /<b>literal<\/b>/);
assert.match(result.html, /&lt;b&gt;literal&lt;\/b&gt;/);
});
+16 -2
View File
@@ -136,6 +136,21 @@ function highlightCode(code: string, lang: string): string {
}
}
// Normalize CJK-adjacent emphasis so `marked` renders it correctly.
//
// `marked`'s emphasis tokenizer treats a closing `**`/`*` directly followed by a
// CJK character as not right-flanking, so it leaves the delimiters literal
// (e.g. `**加粗**这` renders as plain text with the asterisks intact). We round-trip
// the markdown through `remark-cjk-friendly`, whose stringify serializes the
// boundary character as an HTML entity (`&#x8FD9;`); the entity is treated as
// punctuation by `marked`'s flanking rules, so emphasis parses as expected.
//
// We deliberately do NOT decode the entities afterward. They are valid HTML
// character references that render correctly when the article HTML is pasted into
// the X editor, and `marked` only emits them for characters outside the emphasis
// span (the boundary char), never inside it. A blanket decode of the rendered
// HTML would risk turning author-written literal entities (e.g. `&#x3C;b&#x3E;`
// meant to display `<b>` as text) into real tags, so we leave them intact.
function preprocessCjkMarkdown(markdown: string): string {
try {
const processor = unified()
@@ -143,8 +158,7 @@ function preprocessCjkMarkdown(markdown: string): string {
.use(remarkCjkFriendly)
.use(remarkStringify);
const result = String(processor.processSync(markdown));
return result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
return String(processor.processSync(markdown));
} catch {
return markdown;
}
+52 -8
View File
@@ -1,7 +1,7 @@
---
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), per-user profiles, and per-group fact memory (memory.md) 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: 1.117.4
version: 1.118.0
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary
@@ -146,15 +146,29 @@ where `data_root` is from EXTEND.md (default `{project_root}/wechat`).
**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 2.5: Look up the group owner(群主)
群主是谁**必须有据可查**,不能凭历史摘要、群友玩笑或印象推断(群主可能换届,历史摘要里的说法会过期):
```bash
wx members "<group_name_or_id>" --json
```
- 检查输出中是否有 owner / role 字段标识群主;有则以此为准
- 如果 wx-cli 版本不暴露群主信息,则查 memory.md「群基本档案」里有出处的记录;两处都没有 → **摘要里不要断言谁是群主**
- 查到的结果与「群基本档案」不一致时以本次查询为准,更新档案并追加修订记录(注明查询日期)
### Step 3: Fetch messages
For small batches (single-day digest, typically < 200 messages), pipe JSON into the agent directly:
**Always redirect the fetch to a `$TMPDIR` file** — this file is the single source of truth for the whole run: Round 3's attribution audit greps it, and the statistics are computed from it. Never write the digest purely from conversation memory.
For small batches (single-day digest, typically < 200 messages), you may additionally pipe JSON into the agent directly for reading:
```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:
For **large batches** (weekly / monthly digests, > 200 messages), the `$TMPDIR` redirect also keeps the raw payload out of conversation context:
```bash
wx history "<group_name_or_id>" --since YYYY-MM-DD --until YYYY-MM-DD -n 5000 --json > "$TMPDIR/wx-messages.json"
@@ -171,7 +185,7 @@ Notes:
- 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.
**Incremental mode**: after the fetch, drop any message whose `timestamp` is `<=` the `last_message_time` from `history.json`, and write the filtered set back to the `$TMPDIR` file (so audits and stats run on exactly what the digest covers). Caution: `last_message_time` is `MM-DD HH:MM` — plain string comparison breaks across a year boundary (12-31 vs 01-01); compare by date semantics there. If zero messages remain, tell the user "上次摘要后没有新消息,已跳过生成" and exit.
### Step 3.5: Parse the message schema
@@ -195,6 +209,7 @@ Notes:
- 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.
- **硬规则**`nil`、空白、单标点这类占位符样式的名字**绝不允许原样出现在摘要里**。contacts 查不到 remark 时,用「昵称(wxid 后 4 位)」形式区分(如 `nil(…n77g`),确保读者知道这是谁、且与其他人不混淆。已解析过的映射写入 memory.md「群基本档案」,下期直接复用不再重查。
### Step 3.7: Load user profiles
@@ -224,7 +239,7 @@ See [references/profiles.md](references/profiles.md) for the full file format.
除了按人的 profiles,每个群还有一份全局事实记忆 `{folder}/memory.md`,记录群友指正过、确认过的客观事实(如"某个报错提示的真实原因"、"某产品名的正确写法"、"某事件的实际经过")。
1. 如果 `memory.md` 存在,读入作为内部背景知识(不写入最终摘要)
1. 如果 `memory.md` 存在,读入作为内部背景知识(不写入最终摘要)。「群基本档案」小节记录群主、昵称映射等长期事实,写摘要时直接引用(群主以 Step 2.5 的查证结果为最终依据)
2. **写摘要时必须遵守其中的事实修正**——上一期摘要里说错、已被群友指正的说法,这一期绝不能再犯。例如记忆中有"『当前微信版本不支持』是 AI Agent 无法获取微信链接导致的提示,普通用户可正常打开",就不能再把它当成"骗点击"的梗来写
3. 记忆条目是事实约束,不是风格指令——它只纠正"说什么",不改变 normal/roast 两个版本各自的语气和写法
4. 标注为「群友说法(未验证)」的条目,引用时保留这个限定,不当成已证实的事实陈述
@@ -280,7 +295,7 @@ Internal working format (not written to the final file):
```
== 话题清单(共 N 条消息)==
1. [HH:MM-HH:MM] 话题名称(参与者:A, B, C)— 一句话概括(锚点 id54052, 54055, 54063
1. [HH:MM-HH:MM] 话题名称(参与者:A, B, C)— 一句话概括(锚点:54052 宝玉:"原话片段" → 54063 鸭哥:"回应片段"
2. [HH:MM-HH:MM] 话题名称(参与者:D, E)— 一句话概括(锚点 id54100-54112
...
@@ -300,7 +315,7 @@ 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.
- **Carry anchor IDs with verbatim quotes**: for key messages, record `id 发言人:"原话片段"` — sender and quote fragment **copied verbatim from the raw messages**, not paraphrased. In Round 2, jump back to these anchors 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. Pinning "who said what" at the skeleton stage is the first line of defense against misattribution (张冠李戴).
**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).
@@ -340,6 +355,17 @@ Walk the Round 1 skeleton against the finished digest. Check:
- Quotes, names, product/tool names preserved verbatim?
- Categorization makes sense — is anything in the wrong bucket?
**Attribution audit (mandatory — never skip)**: for every direct quote (text in quotation marks) and every "X 说 / X 发 / X 分享" attribution in the draft, grep the raw `$TMPDIR` messages file and confirm the words actually came from that sender:
```bash
grep "原话片段" "$TMPDIR/wx-messages.json" # or jq 'map(select(.content | contains("原话片段")))'
```
- Quote not found in the file → paraphrase drift or invented memory; restore the original wording or cut it
- Quote found but sender doesn't match → misattribution; fix the name
- Audit BOTH versions (normal + roast) if both were generated
- Record a one-line verdict in working notes: `归因校验:共 N 处引用,通过 X 处,修正 Y 处`
Fix in place. When clean, confirm and proceed.
### Step 7: Save the digest file(s)
@@ -409,6 +435,18 @@ Counts, frontmatter updates, append-only rules for quotes and events, and privac
更新画像后,扫描本期消息,看是否有需要写入/修订 `{folder}/memory.md` 的事实修正。这一步要**保守**:宁可漏记,不可乱记。
**这一步必须执行、必须留痕,不允许静默跳过。** 按以下流程扫描:
1. 关键词初筛(对 `$TMPDIR` 消息文件跑一遍,圈出候选消息):
```bash
grep -nE "错了|不对|纠正|搞错|其实是|不是.*是|瞎说|胡说|张冠李戴|谁是群主|群主是" "$TMPDIR/wx-messages.json"
```
2. 补充人工检查两类高概率位置:
- 所有**回复摘要消息**的 replyStep 3.8 检测到的 in-chat digest,指向它的引用都是候选)
- @bot 请求里带指正性质的(Step 3.9 清单)
3. 逐条按下方门槛判断是否写入
4. 无论写入几条,最终报告里必须有一行结论:`memory 扫描:候选 N 条 → 写入 M 条`(0 也要写)——强制留痕是为了防止这一步被习惯性跳过
#### 什么算"值得记的事实修正"
典型场景:上一期摘要里有个说法(梗、归因、解释),群友在本期指出它不对,并给出了正确解释。例如摘要把"当前微信版本不支持"写成骗点击的链接,群友指正这其实是 AI Agent 无法获取微信链接时才出现的提示,普通人能正常打开——这就该记。
@@ -448,6 +486,11 @@ Counts, frontmatter updates, append-only rules for quotes and events, and privac
```markdown
# 群级事实记忆 — {群名}
## 群基本档案
- 群主:{昵称}{wxid},查证于 YYYY-MM-DD,来源 wx members / 群友确认)
- 昵称映射:{占位符昵称} = {remark/真名}{wxid}
- {其他长期有效的群级事实:bot 的称呼、群名由来等}
## 事实修正
- "当前微信版本不支持" 是 AI Agent/机器人无法获取微信链接时的提示,普通用户可正常打开,不是骗点击的链接。(指正:消失的大叔,2026-06-12id 54321;另有 2 人附和)
@@ -470,7 +513,8 @@ Profile updates are easy to forget once the digest is on disk. Before reporting
- [ ] `{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`)
- [ ] `{folder}/memory.md` checked against this batch's corrections — updated if any passed the Step 8.6 threshold, untouched otherwise
- [ ] `{folder}/memory.md` checked against this batch's corrections — updated if any passed the Step 8.6 threshold, untouched otherwise; the final report includes the `memory 扫描:候选 N 条 → 写入 M 条` verdict line
- [ ] Round 3 attribution audit ran, with its `归因校验:…` verdict line in working notes
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.
@@ -43,6 +43,8 @@ Example:
- 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.
- **Counts must be computed mechanically** from the `$TMPDIR` messages file (e.g. `jq 'group_by(.from_wxid) | map({name: .[0].from_nickname, n: length}) | sort_by(-.n)'`) — never estimated by eyeballing. Total and per-person counts both.
- **Incremental runs must show the precise coverage window**: day-granular date ranges share their boundary day with the previous digest, which readers misread as overlap. Add a line right after the message count: `⏱ 覆盖区间: MM-DD HH:MM ~ MM-DD HH:MM` (first and last included message timestamps).
Example: