feat(wechat): smart-reply panel — AI drafts, user approves on phone#83
Open
feat(wechat): smart-reply panel — AI drafts, user approves on phone#83
Conversation
Inbound messages from whitelisted contacts no longer auto-reply via the
agent. Instead the auxiliary cheap model drafts 3 candidate replies and
pushes a panel to the user's `filehelper` (文件传输助手) chat:
💬 [AA] Alice (大学同学) → 「周末有空吗」
[1] 有的,周六下午行
[2] 周末出差,下周可以吗
[3] 在忙,晚点回你
回 1/2/3 发送 · 直接打字自定义 · x 跳过 · q 看队列
The user replies on their phone with `1`/`2`/`3`/`x`/freeform/queue
commands; the bridge sends the chosen text to the original sender.
Off by default; opt-in via `wechat_smart_reply: true` plus a contact
whitelist.
Captures most of the "AI replies on my behalf" value while keeping the
user as the last-button-pressed (so impersonation, regulatory, and
liability concerns from the original full-auto idea all evaporate).
Five composable subsystems:
1. **SQLite persistence** — `~/.cheetahclaws/wx_smart_reply.db` with
three additive tables (`wx_panels`, `wx_reply_history`,
`wx_id_counter`). Bridge restart no longer drops in-flight panels;
reply history survives across daemon recycles so style mimicking has
data to draw from immediately. Init failure auto-falls-back to an
identical-API in-memory store; nothing crashes.
2. **Multi-panel queue UI** — every panel carries a 2-letter ID (`AA`,
`AB`, … rolling at `ZZ`). `q` / `queue` / `队列` lists pending
panels. `<ID> <choice>` addresses panels explicitly (e.g.
`AA 2`, `AB x`, `AA 我自己写的`). Plain `1`/`2`/`3`/`x` still
defaults to the latest active panel for simple cases.
3. **Style mimicking** — every confirmed send (candidate or freeform)
appends to `wx_reply_history`. The candidate-generation prompt
reads the last 10 entries (excluding the current contact) and tells
the auxiliary model to mirror the user's tone and length. Builds
over a few days without any explicit user action. Janitor prunes
rows older than 30 days.
4. **Per-contact relationship context** — `~/.cheetahclaws/wx_contacts.json`
maps `wxid_*` → `{label, relationship, notes}`; injected into the
generation prompt when present. File-mtime watch reloads on edits
without bridge restart. `ContactsStore` is the loader/saver.
5. **Group @-only trigger** — `wechat_smart_reply_groups_at_only` plus
`wechat_self_nickname` makes group messages silent unless the text
contains `@<nickname>` with a word boundary (`@李` won't match
`@李明`, etc.). Empty nickname blocks all group messages safely.
Behaviour change for existing WeChat bridge users: NONE — defaults
preserve current auto-reply. Enabling requires explicit opt-in.
Wire-up: `bridges/wechat.py` gets a 45-line patch — initialize store +
contacts in the poll loop; before the existing agent dispatch, check
`is_smart_reply_target(uid, config, text=...)` and route through
`trigger_smart_reply` instead. Filehelper inbound messages are
intercepted early via `handle_filehelper_message`; falls through to
normal dispatch when there's no active panel (so `!jobs` etc. from
filehelper still work).
Cost: one auxiliary-model call per inbound smart-reply message,
capped at 200 tokens, `thinking=False`. Whitelist gate keeps the
billing surface small.
Tests: 75 unit tests covering identity heuristics, gating rules,
panel-ID generation, both store backends (SQLite + in-memory),
filehelper input parsing, panel + queue formatting, candidate
extraction, prompt construction (style + contact), trigger / handle
flows, contacts file roundtrip / mtime reload / corruption recovery,
and the new config defaults. Full suite 734 passing on this branch.
Files:
* `bridges/wechat_smart_reply.py` (new, 473 LoC) — entry points,
parsing, formatting, prompt building, generation
* `bridges/wechat_smart_reply_store.py` (new, 595 LoC) — SQLite +
in-memory stores, contacts JSON loader, ID generation
* `tests/test_wechat_smart_reply.py` (new, 840 LoC) — 75 cases
* `bridges/wechat.py` (+45) — wire smart-reply
into the poll loop
* `cc_config.py` (+13) — six new config keys
What's still out of v1 scope:
* No "send proactively when user is away" (smart-reply only triggers on
inbound messages).
* No analytics / metrics on candidate acceptance rate (would need to
track "1 vs freeform vs skip" ratios; happy to add if useful).
* No slash command UX for editing contacts (manual JSON edit; could add
`/wechat contact add/edit/list` later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Inbound messages from whitelisted contacts no longer auto-reply via the agent. Instead the auxiliary cheap model drafts 3 candidate replies and pushes a panel to the user's
filehelper(文件传输助手) chat:The user replies on their phone with
1/2/3/x/freeform/queue commands; the bridge sends the chosen text to the original sender. Off by default; opt-in viawechat_smart_reply: trueplus a contact whitelist.Captures most of the "AI replies on my behalf" value while keeping the user as the last-button-pressed (so impersonation, regulatory, and liability concerns from the original full-auto idea all evaporate).
Five composable subsystems:
SQLite persistence —
~/.cheetahclaws/wx_smart_reply.dbwith three additive tables (wx_panels,wx_reply_history,wx_id_counter). Bridge restart no longer drops in-flight panels; reply history survives across daemon recycles so style mimicking has data to draw from immediately. Init failure auto-falls-back to an identical-API in-memory store; nothing crashes.Multi-panel queue UI — every panel carries a 2-letter ID (
AA,AB, … rolling atZZ).q/queue/队列lists pending panels.<ID> <choice>addresses panels explicitly (e.g.AA 2,AB x,AA 我自己写的). Plain1/2/3/xstill defaults to the latest active panel for simple cases.Style mimicking — every confirmed send (candidate or freeform) appends to
wx_reply_history. The candidate-generation prompt reads the last 10 entries (excluding the current contact) and tells the auxiliary model to mirror the user's tone and length. Builds over a few days without any explicit user action. Janitor prunes rows older than 30 days.Per-contact relationship context —
~/.cheetahclaws/wx_contacts.jsonmapswxid_*→{label, relationship, notes}; injected into the generation prompt when present. File-mtime watch reloads on edits without bridge restart.ContactsStoreis the loader/saver.Group @-only trigger —
wechat_smart_reply_groups_at_onlypluswechat_self_nicknamemakes group messages silent unless the text contains@<nickname>with a word boundary (@李won't match@李明, etc.). Empty nickname blocks all group messages safely.Behaviour change for existing WeChat bridge users: NONE — defaults preserve current auto-reply. Enabling requires explicit opt-in.
Wire-up:
bridges/wechat.pygets a 45-line patch — initialize store + contacts in the poll loop; before the existing agent dispatch, checkis_smart_reply_target(uid, config, text=...)and route throughtrigger_smart_replyinstead. Filehelper inbound messages are intercepted early viahandle_filehelper_message; falls through to normal dispatch when there's no active panel (so!jobsetc. from filehelper still work).Cost: one auxiliary-model call per inbound smart-reply message, capped at 200 tokens,
thinking=False. Whitelist gate keeps the billing surface small.Tests: 75 unit tests covering identity heuristics, gating rules, panel-ID generation, both store backends (SQLite + in-memory), filehelper input parsing, panel + queue formatting, candidate extraction, prompt construction (style + contact), trigger / handle flows, contacts file roundtrip / mtime reload / corruption recovery, and the new config defaults. Full suite 734 passing on this branch.
Files:
bridges/wechat_smart_reply.py(new, 473 LoC) — entry points,parsing, formatting, prompt building, generation
bridges/wechat_smart_reply_store.py(new, 595 LoC) — SQLite +in-memory stores, contacts JSON loader, ID generation
tests/test_wechat_smart_reply.py(new, 840 LoC) — 75 casesbridges/wechat.py(+45) — wire smart-replyinto the poll loop
cc_config.py(+13) — six new config keysWhat's still out of v1 scope:
/wechat contact add/edit/listlater).