Skip to content

feat(wechat): smart-reply panel — AI drafts, user approves on phone#83

Open
chauncygu wants to merge 1 commit intomainfrom
feature/wechat-smart-reply
Open

feat(wechat): smart-reply panel — AI drafts, user approves on phone#83
chauncygu wants to merge 1 commit intomainfrom
feature/wechat-smart-reply

Conversation

@chauncygu
Copy link
Copy Markdown
Contributor

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 triggerwechat_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).

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant