跪拜 Guibai
← Back to the summary

A WeChat Group Chat AI Built on GUI Automation: 5 Real Failures and 7 Anti-Robot Design Rules

Using GUI Automation to Write a Group Chat AI: 5 Real Incidents, and 7 Designs That Make AI Not Act Like a Robot

Author: 假菌

Why it started

The initial goal was simple: wanted to make the group chat livelier.

That's a high school alumni group, from graduation to now, about ten people scattered in different cities for college. Before it was active daily, but later everyone got busy, the group gradually quieted down—occasionally someone says something, and only half an hour later someone replies.

So I thought, why not put something in there that at least responds.

Thus came "Ajun", a fish.

It doesn't do real work—doesn't answer questions, search info, summarize meetings. It only does one thing: catching jokes in the group.

But it's not the kind of "bot that connects an API". I gave it a real WeChat account, a computer, and a pair of "hands" that can move the mouse—it can see images sent by group members, hear voice messages, send emoji, and the messages it sends appear to group members as ordinary chat history, with zero mechanical trace.

Sounds simple: throw a joke line, ten lines of code to call an LLM API.

But getting it running took me three days; after that, most of the time was spent teaching it when to shut up.

This post covers the three days of development pitfalls, plus the problems that only appeared after it ran. If you want to build something similar, hope you can avoid some detours.


About the AI in this piece, I'm not being vague. The skeleton came from me organizing a few days of logs and spoken notes; AI helped structure it; sentences were shortened by it, numbers and logs were cross-checked line by line, and it helped look up API and library usage. (Models used: deepseek-v4.1-flash plus a few helper tools.)

But the core isn't from it: "Limiting the AI's ability to speak is ten times more important than making it good at speaking" is a conclusion I hit by running on real hardware. 229 group messages vs. 9 private, 51 deliberate silences, 445 tests all green—those raw numbers came from my own machine. I really stepped on all five incidents, and I really changed all seven designs.

I checked all facts and quotes one by one; anything the AI got wrong or made up I deleted; any miswording is on me.

Desensitization note: All group members in this post are referred to as "group member X" (group member C, group member E...). Real nicknames and chat text do not appear in this post—this matches the approach of the project itself (real nicknames stay only local, and the model is always sent aliases).

Part 1: Why "being able to reply" is not the hard part

Why it had to be WeChat

If you've done Slack bots or Discord bots, they all share a common path: official open API, webhook callbacks, standard event formats.

WeChat has none of that.

But it's not just "WeChat has no API"—more precisely: the official openings for bots are not where I needed to go.

I checked all the WeChat-ecosystem channels that touch "bot". Their boundaries are:

Channel Can enter a regular WeChat group? Can be @-triggered?
Official AI channel for personal accounts (ClawBot / 龙虾) ❌ Officially states "prohibited from being added to groups" ❌
Work WeChat "group bot" (Webhook) ❌ Can only one-way push to the group ❌ Cannot receive group messages
Work WeChat "smart bot" ⚠️ Can enter groups, but only internal enterprise groups ✅
Work WeChat customer service ❌ Not in group (separate 1-on-1 window) ❌

These aren't my guesses; the officials said so:

Note the last one's wording: not "not yet supported", but "prohibited". This is a design choice, not a missing feature.

One more: WeChat's own built-in AI ("Xiao Wei") is the same—test reports say it can only passively respond to commands, cannot proactively send group messages.

Even Tencent's own AI has no say in group chats.

Now you see the problem: it's not lack of capability—officially, there is no place for a bot in a WeChat group chat. The personal-account channel is positioned as a "personal private AI assistant"; the Work WeChat route requires enterprise verification and enters internal groups—it serves enterprise employees, not my high school alumni group.

Work messages are on WeChat, family group on WeChat, bill notices on WeChat, and of course the high school alumni group is on WeChat. A group pet living in Discord, no matter how fully featured, won't make us move there.

So this isn't a question of "which route is more capable"—those API routes are indeed more capable (sending images, emoji, quote replies, thread topics, emoji reactions—all first-class citizens, and they don't occupy a computer or fear screen lock). But they can't go where I need to go.

And there's a more fundamental difference, which I only realized after finishing:

Official bot API My approach
What it is A service attached to the group It is an account
How it enters a group Needs to be "added" (and regular groups don't allow adding) That account is already in the group
Is it a group member No, it's a plugin Yes, it's a group member

I didn't "pull a bot into the group"; I made an existing account in the group able to talk by itself. That also explains why it can see group history, catch jokes, and be "present"—for it, that isn't a "connected session", it's its everyday life.

So the first hurdle isn't the model, it's the channel: first someone has to be there, then you can talk about having it live there.

I gave it a "manual operation seat"

Here's a key choice I want to be clear about: I didn't connect to WeChat's API. I gave the model a real WeChat account + a screen + a pair of hands.

Every one of its messages goes out like this:

① Find the WeChat window → switch to foreground
② Locate the input box → click it
③ Write the reply text to the system clipboard
④ Ctrl+V paste
⑤ Ctrl+A → Delete to clear (in case of leftovers)
⑥ Press Enter
⑦ Read back the input box, confirm it's actually empty, then consider sending successful

This is CV engineering—copy, paste. No API, no protocol, just a program clicking and typing for you.

Full capability list:

Capability Implementation
See Local vision model (Ollama) reads images, understands screenshots and emoji from group members
Hear Local speech-to-text (faster-whisper) turns voice messages into text
Speak Clipboard + Ctrl+V + Enter
Send images Local images go through clipboard paste, WeChat inserts them as image messages
Read quotes Parses the quoted original text in group members' quote messages (can only read, can't quote-reply yet)

In plain terms: it's not a "bot that connected an API"; it's the person sitting at the computer using the mouse to click WeChat.

Why this step specifically

Let me state upfront: these API routes could also do these things. The four below aren't "API can't do it"; they're "in this no-API place, I forced them out":

⚠️ Saying "exactly like a real human" would be bragging. A real human can retract a message, scroll chat history, and reply even when the screen is locked—it can't do any of these. The only claim that truly holds: at the moment that message is seen, no one can tell it was machine-generated.

A fair word on the side: that label isn't a flaw; it's a deliberate information disclosure. I bypass it not because my tech is stronger, but because WeChat never left an opening for this scenario. I know this doesn't sound good—bypassing the label is the thing I'm most unsure whether I should be doing here. The only accounting I can give is two things: it only runs on a secondary account and in a small group of people I know; and the moment I get a platform warning, I will shut it down.

The cost is also real—it occupies a computer's desktop, and there's a bunch of "physical" constraints (details below).

This is also why getting it running took three days: It isn't a pure software problem; it's "how to make a program impersonate a person sitting at a computer".

And this route is fragile. A minimized window, being covered by another window, a moved input box—any of these can fail. That's why the flow has so many "read-back validations": confirm after sending, retry on failure, otherwise replies silently vanish.

Several pitfalls only this route hits

① The WeChat window can't be maximized.

Because it computes relative coordinates—locates the input box by ratio to window size. When maximized to 2590px wide, the ratio-calculated position is off, it can't click the input box, and then it hangs for a full 180-second timeout.

Set the window to around 1100×759, and the same operation takes 3 seconds.

② It steals your clipboard.

Because it's "copy → paste", the instant it sends a message, your clipboard content is replaced.

③ The window must be in the foreground.

If another window covers it, it can't click. So while it's working, that machine basically can't do anything else.

④ Screen lock kills everything.

Once the screen locks, GUI operations have no target; nothing works.

These four together mean one thing: this is not a "service running on a server"; it's something that needs a person to be present. Eventually I accepted that—it's fundamentally a "group member", not a "service".

Why "group", not "private chat"

This is something I only understood after doing it: a group chat is far easier to sustain than a private chat.

At first I thought private chat was simpler—one on one, no need to compete for a turn, no quota worries, no pressure of an audience. Actually the opposite.

The biggest problem with private chat: finding a topic.

A group doesn't need topic-finding. Topics are already there—someone's complaining about the timetable, someone's posting a lunch photo, someone's shouting for a game. It just has to keep up.

Private chat? Two people staring at each other. What do you say first? "What are you up to"? That's the most customer-service-like opener.

Dimension Group chat Private chat
Where topics come from Already there, someone else throws them Have to make them up
Is silence awkward? No, the group is already noisy Yes, staring at the screen in silence is weird
Wrong reply Someone follows up with a joke, atmosphere stays One-on-one cold, it's over
Rhythm Short lines in bursts, can interject Back and forth, like an interview
Cost of wrong People treat it as a meme The other person may think "what is this"

Real data confirms this too—ran for five days:

Number of messages it said
Group chat 229
Private chat 9 (7 of which were on the day friend request was accepted)

That's a 25x difference.

image3_groupVprivate.png

And of those 9, most were replies after the other person spoke first. Its proactive topic initiation in private chat—basically zero.

So the conclusion: this thing naturally belongs in a group. The group gives it a position to "be present but not speak", and private chat has no such position— in one-on-one, not speaking is a kind of rudeness.

That's also why the opening line said "group members don't want an assistant, they want a living thing that can catch jokes"— catching jokes requires a joke to exist. And jokes grow in a group.

Four designs forced by the environment

Because we took this route, all subsequent designs had to change thinking:

World with open APIs The world it lives in
Process events when they arrive Must actively poll, and also deduplicate
Messages carry full context Only bare text, context must be assembled
Sending a message is idempotent Once sent, can't take it back, a wrong send is a real mistake
There's a users API Who is who must be recognized by itself
There's a rate limit header No feedback at all, only rely on delay to pace itself

"Who is who must be recognized by itself" directly determined half of my subsequent design.

And it needs to see and hear

Half of group chat isn't text. An AI that only reads text is half-blind in a group.

So it connects to two local models:

Capability What's used Cost
See images Local Ollama running a vision model ~5-15 seconds per image, uses 4GB VRAM
Hear voice Local faster-whisper transcription ~0.5 seconds per message, uses 1GB

Why local: anything a group member might send—household photos, ID cards, screenshots, random snaps. Those shouldn't leave your computer.

Also the vision model eats VRAM. Since it stays resident just to "occasionally look at an image", I added an idle auto-unload: automatically unload after not being used for a while, freeing the VRAM.

The biggest takeaway from this part: multimodality isn't adding a feature; it's changing how you listen to people. A group member posts a photo of "cafeteria roast duck rice" with "so good"—reading only text you know it's good; being able to see the image, you know they're bragging about their school.

Back to catching jokes

Technically it's three steps:

  1. Listen for messages
  2. Assemble a prompt and send to the model
  3. Send the reply

That can run in half a day. Then you'll find—the moment it opens its mouth, it sounds like a robot.

Specifically:

Phenomenon Why it's not human
Instant reply A real person needs to see the message, think, type—fastest is a few seconds delay
Says a huge block at once Group chat is pacing of short bursts
Responds to whoever speaks Real people don't; they have closeness
Answers everything Real people don't know everything
Emoji in every sentence Real people use them occasionally

The real difficulty of "not behaving like a robot" isn't whether it can talk; it's whether it knows when not to talk.


Part 2: Five real incidents

Lead with the conclusion: of the pitfalls I hit, 80% were not in "how to say something", but in "why the message never got sent".

List first so you can compare with your own case:

# Incident One-line summary
1 The same sentence was queued twice Looks like a concurrency bug, but actually ran two instances
2 Replied to a message from 86 minutes ago Replying to an old message is worse than not replying
3 Took 186 seconds to send A good reply went stale while waiting in line
4 A good reply was replaced by a new message Most hidden—you don't even know what was lost
5 It didn't know it hadn't been sent Most severe—it will say back "didn't I already say that?"

Each section below is real, with logs and numbers.


Incident 1 · The same sentence was queued twice

Symptom: two identical messages appear in the group.

First reaction: check dedup logic. Checked, it's fine—after decideInbound() is immediately marked as handled, a second entry in the same process will definitely hit "already handled", and tests pin that order.

Real explanation: two instances ran at the same time.

Both processes read the same database, each enqueued, each sent at its turn. Evidence: both queue lengths went from 0 to 1—so each side from then on went through "burst protection" and didn't repeat, and the end result was exactly two messages. That shape can't be produced by a single instance.

Fix: single-instance lock.

// Criterion is "is the holder process alive", not "does the file exist"
process.kill(pid, 0)

A few details worth noting:

The counter-intuitive point of this one: it reads like a concurrency bug, but it's actually a deployment problem. Looking in the wrong direction.

Incident 2 · Replied to a message from 86 minutes ago

Symptom: the group @ at 18:18, it replied at 19:44.

In those 86 minutes, the group had already talked about "so it's AI" and "what is the big fat fish". Then it suddenly said "here. what's up"—the most typical robot stupidity.

Key insight: replying to an old message is worse than not replying.

And this is a high-frequency scenario—laptop lid closed for a few hours, wakes up, dozens of messages piled up, all stale.

Fix: stale message gate.

staleMessageMs: 600000   # 10 minutes, 0 = off

Details:

Detail Why
Placed after "late-night silence" and before "quota" It's a different dimension from "how many replied today", but same family as "what time is it now"
ts must be truly numeric for the check Number(null) === 0 is finite, so direct conversion treats it as 1970 → every message judged stale
Return code + plain text Logs must say "stale by X duration", not just a code
Threshold 10 minutes, not 2 In a normal group, replying after a dozen minutes is completely fine, don't kill that

This is complementary to "when the queue is full, evict the oldest" as two layers: that layer manages whether it gets in line (when capacity is full, old ones are squeezed out); this layer manages whether this one is worth replying to (too old, stay quiet).

Incident 3 · Took 186 seconds to send

Symptom: logs show "queued: sent at 186s".

Cause: several messages each occupied "minimum interval" slots, stacking into a three-minute queue.

And this led to a more painful problem—

Incident 4 · A good reply was replaced by a new message

This is the most invisible kind of loss.

Queuing itself is fine—at least you know it's waiting.

What's truly terrible: a really good reply gets squeezed out by a later message, and you have no idea.

Here's how it goes: the queue has a capacity limit. When full, what happens to new messages?

The old approach was "drop the new one"—reasoning was "there are already two replies queued, skip this one".

Sounds reasonable, but the result is absurd:

[Queue] Two replies generated 1-3 minutes ago, still waiting for a slot
[New] Someone @'s it, it thinks of a particularly good reply
        → queue full → this good reply gets dropped
[Group] It answers the previous round, doesn't acknowledge this one at all

You can't see how good the dropped reply was. The log only says "queue full, skipped".

I only later realized: this is the exact opposite of real human behavior. A busy human doesn't do this. They'll swallow the stale thought and answer the newest message. Because "answering the wrong question" is worse than "not answering"—the former is foolish, the latter is just busy.

Fix: change from "drop the new" to "evict the oldest".

① Queue full → evict the oldest (it's already stale)
② Put the newest one in
③ Exception: if the new one is "proactive interjection" and the queue is full of "someone called me" → let the new one yield
④ Replies to a named person: never evict, count toward a separate higher ceiling

③ and ④ are two faces of the same principle:

So the queue's shape now:

[Queue] 
      ├─ Named replies ×N   ← protected first, higher ceiling
      └─ Proactive interjections ×M ← squeezed first when full

Incident 5 · It thought it had said it

This is the most severe.

After all the above, there's a deeper pit—I hadn't thought of it at all.

The model's reply is "generated", and once generated, it's written into the chat history. It will think:

"I already said that."

But sending is a separate later step. That message might:

And in the chat history, it has already "said" it.

The result: the next time the topic comes up in the group, it says "didn't I already say that?"—but you never saw it.

This is worse than "not replied":

Situation Group sees Severity
No reply It said nothing Normal, real people miss things too
Thinks it replied It turns around and questions you for not seeing Social damage

An AI firmly believing it said something nobody ever saw—this isn't a technical problem; it's a "character collapse" problem.

My current observation: this requires separating "what to say" and "what was actually sent" into two ledgers—only messages truly sent count as "things it said".

I saw evictOneQueued at the place where the queue evicts—it releases the slot, writes a log, removes the message from the waiting set, but no step tells the session layer "this one wasn't sent".

Strictly speaking, I haven't really solved this one; I've only located it.

I'm writing it out because it's more hidden than the previous three: For the first three, you can at least see them in logs; this one can only be inferred from "it talks nonsense".

One thing I'm certain of now:

The model's world must match the group's world. Where they don't match, it starts talking nonsense—and with great confidence.

By the way: the eviction logs a message (new message matters more).

Because "what was lost" must leave a trace—otherwise you'll never discover you've lost something.


Part 3: The designs that actually make it not behave like a robot

Once incidents are fixed, that's where this thing gets interesting. Core principle in one line:

Limiting its ability to speak is ten times more important than making it good at speaking.

1. Delay is not a bug, it's a feature

Real humans take time to type. So replies aren't sent immediately; they have a random delay (30 seconds to 2 minutes).

But the lower bound must be held—instant reply is the strongest robot feature.

This changed once: originally 45 seconds to 5 minutes, but actual logs showed "queued: sent after 218s", plus message-reading/decision time, the group felt like "we asked and it ignored us for ages". The long tail is a experience killer. So I cut the upstream long tail, but kept the 30-second floor.

The distribution shape also matters—not uniform, but truncated log-normal. Because real human reply intervals are long-tailed: most are fast, occasional ones are long.

2. Bursts should be like a human

What's common in a group isn't "one complete sentence", but short bursts:

我打算翘了
全勤是什么东西

So let it use line breaks to mean "pause here", and the code turns line breaks into actually sending multiple messages, with a few seconds in between.

But the cap is three. Beyond that it's spam; and splitting every message is another kind of un-human.

3. Judging "has the information been fully given"

This is the design I find most interesting.

Problem: real people rarely say a whole thing in one sentence when talking in a group. It's often:

甲:那个比赛
甲:就是上次说的那个
甲:你报不报

If the AI receives the first message and eagerly replies, it will answer "which competition?"—and the answer arrives two seconds later. That kind of preemptive response is the most typical robot feature.

But you can't just wait forever—if you wait too long, the other person thinks you've gone unresponsive.

So it needs to judge: is this message "finished" or "has a second half".

Three layers of handling

Layer 1: wait for them to finish typing (debounce)

Messages from the same person sent consecutively are merged into one batch before processing:

Situation Wait
Was @'ed 1.5 seconds (he's waiting for you, don't drag)
Normal chat 6 seconds (let him finish)

If someone sends another message, the timer resets—until they stop.

Layer 2: what gets merged must still be remembered

Here I hit a pitfall worth telling separately:

甲:那个比赛        ← merged away
甲:就是上次说的那个  ← merged away
甲:你报不报        ← this one triggers a reply

On the surface, "merging" means dropping the first two and only processing the last. But then it only sees "you in or not", with no idea which competition.

So merged messages must be added to the background board. The original comment says it directly:

"Wait until he finishes" becoming "only see the last sentence" is worse than per-message processing.

Layer 3: seize the timing, but don't miss it

Another case: the message itself is fine, but now isn't the right time to speak (e.g., it just said something and the group hasn't digested it yet).

Then it doesn't drop the message; it schedules a time to reconsider:

节奏未到,92s 后重新考虑

It can defer at most twice. And there's a natural upper bound—if it drags too long, the message expires by itself (the stale gate mentioned earlier), so it won't hang forever.

By the way: it only carries 20 messages of context

When sending, its "background board" isn't the whole chat log; it's the most recent 20 messages.

Why not 100? Two reasons:

This is different from its "memory": 20 messages are "current context", memory is "long-term people and events".

(This changed from 8 to 20: 8 messages in an active group is only one or two minutes, the model can't see where the topic came from, and can only answer the current sentence isolated—that's the direct cause of "answering without understanding context".)

4. Emoji need a quota

Emoji are the stuff of group hype. So:

The last rule was added from a pitfall: it once sent a cartoon emoji in a one-on-one private chat, and the other person asked "did you message the wrong person?"

5. "Who is who" must be recognized by itself

This is what I mentioned earlier—WeChat groups don't give a users API, so who each person is has to be read from the chat itself.

So it builds a profile for each group member:

{
  "alias": "群友C",
  "displayName": "(real nickname only local, not in prompt)",
  "traits": ["loves to tease AI", "sharp tongue", "loves to break"],
  "bond": "often jokes with the fish setting",
  "callsMe": "big fat fish",
  "facts": [...]
}

Note alias and displayName are two fields—real nicknames stay only in local files; the model is always sent aliases like "group member C".

(This is not something I added later; it was set from the start: group members' privacy shouldn't be sent into the model just because it's technically convenient. And it incidentally solves an engineering problem—the model doesn't need real names, only knows "this is the same person".)

After running for a few days, what it concluded on its own was more accurate than I expected:

Member Traits it wrote itself
C Loves to tease AI, sharp tongue, loves to break
E Loves to hype, loves memes, loves emoji
G Sharp tongue, loves to argue, loves to manage the big fat fish
H Foodie, asks what to eat, loves to play King of Glory
K Homework-cramming student, loves to complain, loves memes

Note the third row's "loves to manage the big fat fish" (big fat fish is its group nickname)—it read that person's behavior pattern. That's not my writing; it summarized it itself.

Pitfall hit: a ghost member named "?" once got into the profile, whose only record was "so-and-so patted so-and-so". Another time, it recorded itself as a group member.

The fix: filter before data enters the model—system events (pat, recall, join) are not collected, its own sends are not collected, and names like ?, 我, 未知 are not allowed to be stored as nicknames.

This kind of "who is real, who is noise" judgement is necessary in any group; WeChat just forces you to face it earlier.

6. Memory must split "traits" and "facts"

Build a profile for each member, but split fields into two types:

Type Example Might change
Traits stays up late, sharp tongue, studies CS Rarely changes
Facts wants to test into Shenzhen University, family in Guangzhou Might change; new statement overrides old

Key design: every fact must carry the original quote as evidence + a timestamp. When verifying, the quote is checked word-for-word against the chat history; if it doesn't match, it's discarded. Rather missing than wrong.

This is the most effective way to prevent hallucination—not by telling the prompt "don't make things up", but by verbatim post-hoc checking.

7. Minefields: better left unwritten

The profile also has a "minefield" field—topics not to touch.

But I added a hard rule:

Only accept explicit signals, forbid making vague guesses. If written wrong, it will make it avoid people when it should care. Better not write at all.

In practice, this field is empty for most people—as expected. If nobody in the group has revealed a minefield, it should be empty.


Part 4: For people who want to build something similar

A few pitfalls, written for convenience

① Output quota and timeout must be tuned together.

Using a reasoning model, it "thinks" then "answers", and these two share the same output quota. I initially gave the summarization task 4096, but it spent all the quota on thinking and returned an empty string for the body—the error looked like a broken model, but it was just truncation.

So I raised the quota to 16K. Then it immediately timed out—because the timeout was still the 60 seconds set for casual chat, while 16K-quota thinking takes minutes.

Changing one parameter always drags out another. These two must be set together.

image2_silenceLog.png

② In logs, "silence" needs more trace than "speaking".

This pitfall stuck with me the most.

One day someone said in the group "is the AI stuck?", and I went to check—took half an hour, only to deduce by elimination. Because the most frequent result (it choosing to stay silent) had no record at all. The console had long scrolled away, and the disk files had not a word.

After adding it, I could see: it wasn't stuck—it decided not to speak.

③ Tests written thicker than source code isn't shameful.

7000 lines of source code with 6500 lines of tests. Sounds overkill, but when I changed gate rules, I genuinely dared to change—because I knew hundreds of test cases were watching.

Oh, and one more

The real world is dirtier than any test suite. I encountered:

None of these could I have thought of while writing code. All of them came from being beaten by real group data.


After running, the data looks like this

Let it sit a few days, flipped through logs, counted.

One premise to state clearly: the "it said" column is counted from logs, precise to the message; the "group messages" column is estimated by order of magnitude—flashes fast, didn't count each. So the precision differs between columns; look at the magnitude relationship:

Date Group messages (est.) It said (exact)
Day 1 ~100+ 7
Day 2 200+ 65
Day 3 ~210 95
Day 4 100+ 25
Day 5 100+ 46

It speaks only about one-tenth to one-third of the group's messages—the exact ratio depends on how active the group is that day. The quieter the group, the higher its share; once the group blows up, it steps back.

And this part is what it considered "worth saying". The rest it held back—the top reason in logs for "didn't speak" is it deliberately chose silence (51 times): after reading the message and judging "I can't run with this or shouldn't", it proactively stayed out. Then come the mechanism-level blocks: dedup (27 times "already processed this") and anti-self-trigger (10 times "this is my own message").

What I'm most satisfied with isn't how much it said; it's that it explicitly decided not to say anything 51 times.


Back to costs and boundaries

Everything above was about making it human-like; this section covers the cost of this route itself—because the first question every reader has is "won't this get banned?", so I'll say it myself.

Does this actually have risk?

Yes. I won't sugarcoat.

WeChat's personal account rules don't welcome automation. I can't guarantee "absolutely safe", but I can state my actual boundaries:

Ran for a few days, no notices. But this is "hasn't happened yet", not "won't happen". I know the difference.

Technically, what it is and isn't

I care about this because it directly decides whether this counts as a "cheat tool":

It is not It is
Doesn't parse or forge WeChat's network protocol A desktop program that reads/writes the screen, clicks mouse, presses keys
Doesn't crack the client, doesn't inject processes Uses the most ordinary GUI automation
Doesn't modify database, doesn't fake login state (read-only) Operates the client I'm logged into
Doesn't call any "WeChat bot API"—WeChat doesn't have one Screenshots, copy, paste, Enter—same as you using the mouse

Plainly: it's a program that sits at the computer and clicks for you. What you can do with your hands, it can do; what you can't do with your hands, it can't either. It gained no "beyond-human" permissions—which is also why it's so fragile (covered window = blind, screen lock = dead).

A contradiction I must admit

Earlier I said the API route carries an "APP/bot" label and my route doesn't—I said that's the only thing I actually bought.

But that label isn't a flaw; it's a deliberate information disclosure. I can bypass it not because my tech is stronger, but because WeChat never left an opening for this scenario.

So bypassing it leaves me slightly guilty. The only accounting I can give is the four boundaries above, plus one sentence: if this approach ever makes anyone uncomfortable in the group, I will stop it directly; I won't optimize it to keep going.

Technically interesting things are many; it's not worth making the people around me uncomfortable.


Conclusion

Back to the original purpose: I just wanted the group to be livelier.

After writing this whole thing, looking back at what I did most—it wasn't making it talk; it was making it know when not to talk.

Group @'s it, it can not reply; Message older than 10 minutes, it shouldn't reply; Someone sends three in a row, it must wait; Nobody's paying attention, it doesn't need to force itself in.

These are all "no" actions.

But it's these "no" actions that turned it from "a robot that can reply" into "that slightly interesting thing in the group".

What makes it human isn't what it says; it's what it restrains itself from saying.

Another truth I learned only by doing:

It needs "presence", not "service".

The group gives it a position—can listen, can occasionally chip in, or stay silent all day. That position is unique to group chat. In one-on-one there's no such thing as "presence"; there's only "serving" or "offline".

So when it messages me privately, it always seems a bit dry—because that's not the scenario where I need it.

It doesn't need a "user"; it needs a lively place.


This post contains AI-assisted content.

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

afumu 1 likes

Incident 5, that 'it thought it had said it' — I stepped in the same pit on the other side: I thought it hadn't replied, but it actually had. On the web-end streaming response there's no reliable message ID, so the only way is to take a baseline snapshot of the reply list before sending and count the extra containers. But the outer container gets wholesale replaced before and after rendering, and the reference is already detached from the document, so what's read is still the previous round — in the group it looks like 'why is it repeating what it just said'. Switching to deduplication by text hash, when both rounds reply '好的' (okay) it gets falsely killed, and it keeps waiting until timeout. The judgement finally comes down to one criterion: the reply text is sampled twice in a row without changing, plus the send button returns to clickable. One different view: these two accounts shouldn't be charged to the conversation layer. Letting the model itself remember 'I said' is exactly the cause of incident 5. On my side, delivery success only writes an external marker, and I check it before retrying — keep it inside, and it will cover up lies for you.

假菌

A hug to a fellow professional.