@xisui-pa/chatserver 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,17 @@
2
2
 
3
3
  A TypeScript bridge between chat apps and LLM models. It filters incoming messages,
4
4
  generates short replies, and retains compact conversation memory. The built-in
5
- WeChat adapter connects to agent-wechat using outbound REST requests; ChatServer
6
- opens no listening ports.
5
+ WeChat adapter connects to agent-wechat using outbound REST requests and its
6
+ v2.1 server-sent event stream; ChatServer opens no listening ports.
7
7
 
8
8
  ## Quick start: WeChat
9
9
 
10
- Requires Node.js **22.22.2+ (22.x) or 24.15.0+**, a Responses API compatible model
11
- provider, and a running, logged-in [agent-wechat server](https://thisnick.github.io/agent-wechat/reference/api/)
12
- with its bearer token. WeChat login and agent-wechat setup are managed separately.
10
+ Requires Node.js **24.15+**, a Responses API compatible model
11
+ provider, and a running, logged-in [agent-wechat server](https://github.com/thisnick/agent-wechat/tree/v2.1.0)
12
+ **v2.1.0 or newer** with a bearer token that grants the `read`, `send`, and `media`
13
+ scopes for its account. Incoming messages arrive over the daemon's authenticated
14
+ `/v1/accounts/{account}/events/stream` SSE endpoint; REST is used for sends, media,
15
+ and name lookups. WeChat login and agent-wechat setup are managed separately.
13
16
 
14
17
  1. Install and build from the repository root:
15
18
 
@@ -43,6 +46,7 @@ with its bearer token. WeChat login and agent-wechat setup are managed separatel
43
46
  "apiTools": [],
44
47
  "options": {
45
48
  "baseUrl": "http://127.0.0.1:6174",
49
+ "accountId": "your-account-id",
46
50
  "chatIds": ["wxid_example"]
47
51
  }
48
52
  },
@@ -52,9 +56,14 @@ with its bearer token. WeChat login and agent-wechat setup are managed separatel
52
56
  }
53
57
  ```
54
58
 
55
- Replace `replier` with the printed reference and `wxid_example` with a direct
56
- chat or group ID from agent-wechat's `/api/chats`. Set `baseUrl` to your
57
- agent-wechat server root, without `/api`. `localTools: []` disables local
59
+ Replace `replier` with the printed reference and `wxid_example` with a thread ID
60
+ from `GET /v1/accounts/<account-id>/threads`: a contact username such as
61
+ `wxid_example`, or a group ID ending in `@chatroom`. Set `baseUrl` to your
62
+ agent-wechat server root; the adapter appends `/v1` itself, and a base URL that
63
+ already ends in `/v1` is accepted. `GET /v1/accounts` lists the daemon account
64
+ IDs with their `ownUsername` and `authenticated` flags; paste an ID into
65
+ `accountId`, or omit it when exactly one authenticated account is visible to the
66
+ token (more than one is rejected as ambiguous). `localTools: []` disables local
58
67
  functions; omit it to enable the built-ins. `apiTools` defaults to `[]`.
59
68
 
60
69
  4. Set the **agent-wechat bearer token** and start ChatServer.
@@ -78,7 +87,10 @@ with its bearer token. WeChat login and agent-wechat setup are managed separatel
78
87
  before the first reply. With no filters, the reply model decides whether to
79
88
  respond. Stop with Ctrl+C.
80
89
 
81
- The WeChat bearer token and the model provider's API key are separate credentials.
90
+ Create the agent-wechat token with the `read`, `send`, and `media` scopes for the
91
+ configured accounts; sending text needs `send`, lazy images and voice need `media`,
92
+ and the event stream needs `read`. The WeChat bearer token and the model provider's API key
93
+ are separate credentials.
82
94
  For LAN model servers on Windows, use the full hostname such as `llm-host.local`
83
95
  rather than a bare hostname.
84
96
 
@@ -230,9 +242,15 @@ call occurred. Ambiguous provider failures retain their reservation; failed
230
242
  delivery or rejected image bytes do not refund generation charges. Once an image
231
243
  is generated, further local-tool rounds and model retries cannot generate another
232
244
  for that reply. When quota is exhausted, image generation is removed from the
233
- request and text replies remain available. WeChat image delivery uses the existing
234
- [`/api/messages/send` image payload](https://github.com/thisnick/agent-wechat/blob/5472a168956ad5d09d3063342e691448980a5c31/packages/agent-server-rust/src/router/messages.rs#L144)
235
- and is never automatically retried after an uncertain send.
245
+ request and text replies remain available. WeChat image delivery stages the PNG with
246
+ `POST /v1/accounts/{account}/uploads`, then sends
247
+ `{ "kind": "image", "threadId": ..., "sourceId": ... }` to `POST /sends` with a
248
+ fresh `Idempotency-Key`, following the
249
+ [v2 media contract](https://github.com/thisnick/agent-wechat/blob/0f4ad16e4a245e3f8a57c0b8b800d304cb425c85/docs/src/content/docs/use/media.mdx).
250
+ The v2 API has no combined image/text send, so a caption is delivered as its own
251
+ text message after the images. Delivery is confirmed by the media job's `job.updated`
252
+ event over the adapter's existing event stream; an uncertain send is never
253
+ automatically retried.
236
254
 
237
255
  Provider URL citations are preserved as complete source links in outgoing text,
238
256
  including after a formatting repair. Text is shortened to reserve room for links;
@@ -271,9 +289,11 @@ Friend wxid_alice: Alice is learning photography and prefers brief, practical ti
271
289
  Channel 123456@chatroom: A photography club discussing weekend outings.
272
290
  ```
273
291
 
274
- Model inputs include the WeChat display name when available, and replies prefer
275
- that name when referring to a person. Stable wxids still identify speakers and
276
- control `mainAccount`, ignore rules, and delivery. Names may change or be shared;
292
+ Model inputs include a sender's WeChat display name when available, preferring a
293
+ group member's chatroom nickname, then the friend remark or contact name, then the
294
+ WeChat-wide nickname. Replies prefer that name when referring to a person. Stable
295
+ wxids still identify speakers and control `mainAccount`, ignore rules, and
296
+ delivery; names are never used for those checks. Names may change or be shared;
277
297
  use wxids in background entries to keep them unambiguous.
278
298
 
279
299
  The background is included in model filter and replier inputs. It is shared by
@@ -288,17 +308,18 @@ conversation memory.
288
308
 
289
309
  | Option | Default | Meaning |
290
310
  | --- | --- | --- |
291
- | `baseUrl` | `http://127.0.0.1:6174` | Agent-wechat root URL, without `/api` |
311
+ | `baseUrl` | `http://127.0.0.1:6174` | agent-wechat server root; a trailing `/v1` is also accepted |
312
+ | `accountId` | single authenticated account | Daemon account ID from `GET /v1/accounts` (not a WeChat username); omit only when exactly one authenticated account is visible |
292
313
  | `tokenEnv` | `AGENT_WECHAT_TOKEN` | Environment variable containing the bearer token |
293
314
  | `tokenFile` | unset | Token file used when the environment token is absent; relative to the config directory |
294
315
  | `chatIds` | all chats | Exact conversation IDs to monitor; `[]` monitors none except `mainAccount`, if configured |
295
- | `pollIntervalMs` | `30000` | Delay after each completed poll |
296
- | `timeoutMs` | `30000` | Per-request API timeout |
297
- | `bundleGapMs` | `20000` | Maximum gap between adjacent same-sender messages in a bundle; `0` disables bundling |
316
+ | `reconnectIntervalMs` | `1000` | Base delay before reconnecting the event stream; consecutive failures back off exponentially up to a bounded cap |
317
+ | `timeoutMs` | `30000` | Per-request API timeout, and the deadline for opening the event stream or waiting on its next chunk or heartbeat |
298
318
 
299
- Tokens are read at startup. Group IDs typically end in `@chatroom`.
319
+ Tokens are read at startup and need the `read`, `send`, and `media` scopes for the
320
+ selected account. Group IDs typically end in `@chatroom`.
300
321
  `mainAccount`, when set, is monitored even if omitted from `chatIds`. Its exact
301
- `/status` command returns local queue, delivery, error, and polling information
322
+ `/status` command returns local queue, delivery, event-stream, and error information
302
323
  without calling a model; it uses the normal reply delay. Other senders and group
303
324
  status requests are ignored.
304
325
 
@@ -332,7 +353,7 @@ to inspect cached images, but receive no other local or hosted tools.
332
353
 
333
354
  Provider URLs, model IDs/aliases, and provider-specific `timeoutMs` live in
334
355
  `models.json`. Each provider requires an explicit `baseUrl`; requests default to a
335
- **3-minute** timeout. API keys are stored separately under the provider name:
356
+ **5-minute** timeout. API keys are stored separately under the provider name:
336
357
  macOS Keychain, Linux Secret Service (`secret-tool`), or Windows Credential Manager
337
358
  (requires the `CredentialManager` PowerShell module).
338
359
 
@@ -446,12 +467,21 @@ exclusions, before model filters and the replier. Transcripts retain the origina
446
467
  sender, timestamp, and message ID and are reused across retained snapshots. Raw
447
468
  audio is not saved in conversation memory. Configure `tts` separately for voice replies.
448
469
 
449
- Agent-wechat normally [converts received voice to MP3](https://github.com/thisnick/agent-wechat/blob/main/packages/agent-server-rust/src/tools/wechat_media.rs).
450
- Audio is limited to 20 MiB per message. Pending media gets bounded retries;
451
- unsupported audio (including raw SILK when upstream conversion fails), unavailable
452
- media, and recordings with no recognized speech are skipped with a log. STT
453
- request failures stop the turn rather than generating a reply from missing audio.
454
- Without `stt`, voice messages are ignored without downloads or model requests.
470
+ Voice media stays lazy: receiving events never downloads audio. When a turn needs the
471
+ recording, ChatServer lists `GET /v1/accounts/{account}/messages/{id}/media`, picks
472
+ the best copy, and asks for it with
473
+ `POST /messages/{id}/media-transfers` (`{ "messageId": ..., "assetId": ... }`). While
474
+ the download job runs the asset reports `pending`, and the existing bounded retries
475
+ defer the turn. Once an asset is `available`, its bytes are streamed from
476
+ `GET /v1/accounts/{account}/media/{assetId}/content`; the account-scoped route is
477
+ built locally rather than taken from the response. See the
478
+ [v2 incoming media contract](https://github.com/thisnick/agent-wechat/blob/0f4ad16e4a245e3f8a57c0b8b800d304cb425c85/docs/src/content/docs/use/quality.mdx).
479
+ The daemon's decoded voice copy is WAV, so STT models receive it directly; raw
480
+ `audio/silk` is rejected. Audio is limited to 20 MiB per message. Unavailable or
481
+ unsupported media and recordings with no recognized speech are skipped with a log.
482
+ STT request failures stop the turn rather than generating a reply from missing
483
+ audio. Without `stt`, voice messages are ignored without downloads or model
484
+ requests.
455
485
 
456
486
  ### Voice replies (TTS)
457
487
 
@@ -478,12 +508,16 @@ The same short-reply limits apply to the spoken text. ChatServer adds independen
478
508
  random **0.5–2 second** silences before and after the audio, without requiring
479
509
  FFmpeg on the ChatServer machine.
480
510
 
481
- Voice replies use the normal serial queue and 1–3 minute pacing. The adapter uploads
482
- audio to agent-wechat's voice-job API and waits for confirmed completion, up to
483
- three minutes; this requires an agent-wechat version with voice-job support.
484
- Failure or uncertain delivery is logged without resending or sending an extra
485
- text reply. Pending jobs receive best-effort cancellation on failure or shutdown.
486
- Memory records the spoken text only after confirmed delivery; audio is not saved.
511
+ Voice replies use the normal serial queue and 1–3 minute pacing. The adapter stages
512
+ the padded WAV with `POST /uploads`, sends
513
+ `{ "kind": "voice", "threadId": ..., "sourceId": ... }` to `POST /sends` with a
514
+ fresh `Idempotency-Key`, and waits up to three minutes for the matching
515
+ `job.updated` event on the adapter's event stream; this requires agent-wechat
516
+ v2.1.0 or newer. Failure or uncertain delivery is logged without resending or
517
+ sending an extra text reply.
518
+ Unfinished jobs receive a best-effort `POST /jobs/{id}/cancel` on failure or
519
+ shutdown. Memory records the spoken text only after confirmed delivery; audio is
520
+ not saved.
487
521
  Omitting `tts` keeps replies in text. Restart after configuration changes.
488
522
 
489
523
  ## Runtime behavior
@@ -492,9 +526,55 @@ Omitting `tts` keeps replies in text. Restart after configuration changes.
492
526
  successful reply or replier decision to stay silent, capped at the latest 30 source
493
527
  messages. Filter denials retain messages for later turns. New arrivals update
494
528
  pending context. Processing and sending are sequential, paced at 1–3 minutes.
495
- - **History:** Messages from before startup, including those received while stopped,
496
- are skipped. Self messages provide context without triggering replies; manual
497
- replies arriving during generation invalidate the pending answer.
529
+ - **Event stream:** `start` performs one
530
+ `GET /v1/accounts/{account}/threads?limit=1` lookup and keeps only its
531
+ `asOfEventCursor`; the returned threads are not paged and no message history is
532
+ fetched. It then opens
533
+ `GET /v1/accounts/{account}/events/stream?after=<cursor>` with a `read`-scoped
534
+ bearer token. Event kinds are inside the JSON payload (`message.committed`,
535
+ `account.updated`, `send.updated`, `job.updated`, ...);
536
+ `message.committed` supplies `data.messageId` and `data.threadId`, and the full
537
+ message is read with `GET /v1/accounts/{account}/messages/{id}`. Each decimal cursor
538
+ is kept as a string (it may exceed JavaScript's safe integer range) and
539
+ checkpointed after the message is read and dispatched, or after an intentional
540
+ filter/skip decision. The daemon needs no consumer acknowledgement. Processing is
541
+ sequential; a failed message read does not advance the checkpoint, so reconnecting
542
+ replays that event. A committed event whose message row the daemon has already
543
+ deleted (`GET` answers 404 or 410) is logged as an intentional skip and remembered
544
+ by message ID, so the checkpoint advances and later events in the same stream are
545
+ still handled; transient or authorization failures stay replayable. Heartbeat
546
+ comments every 15 seconds keep the idle deadline open. Frames use the default SSE
547
+ message event as the daemon sends them, with the kind inside the JSON payload:
548
+ unknown payload kinds need no read and are checkpointed after safe handling. A named
549
+ SSE event other than `message` is ignored without being checkpointed, except
550
+ `event: error`, which is decoded for `event_cursor_expired` and otherwise fails the
551
+ stream for a reconnect.
552
+ - **Live messages:** Only events after startup are delivered, and only that exact
553
+ message route is read — never a message listing. A `message.committed` event is
554
+ handed off as its own turn; the server queue then aggregates each conversation's
555
+ buffered messages into one snapshot per turn, so a burst still arrives as bounded
556
+ context even though every event was delivered individually. The cutoff is the
557
+ message's saved `createdAt`: a message whose source time precedes startup, or that
558
+ the daemon reports without a time, is skipped and remembered so a replay never
559
+ delivers it. Equal source times are never merged or dropped. Self messages provide
560
+ context without triggering replies; manual replies arriving during generation
561
+ invalidate the pending answer. A replayed cursor at or below the checkpoint is
562
+ compared as an integer string (never as a float) and skipped, so the checkpoint never
563
+ regresses. Recently handled message IDs are kept in a bounded in-memory cache — the
564
+ cursor checkpoint is the primary de-duplication — and the cache is cleared on stop,
565
+ reset, or an identity switch.
566
+ The daemon account ID and the signed-in `ownUsername` are captured around every
567
+ awaited event and status read; an identity change stops that reader and
568
+ tail-bootstraps the new login instead of checkpointing or delivering the previous
569
+ login's events, and it settles that login's pending sends.
570
+ If the daemon reports `event_cursor_expired` (HTTP before headers or an in-stream
571
+ `event:error`), the adapter performs one more `threads?limit=1` lookup, rebases the
572
+ checkpoint on that current tail and deliberately skips the missed window rather than
573
+ backfilling history; it never resets to cursor zero. The capped reconnect backoff
574
+ still applies, so repeated expiry against an unchanged tail cannot spin. A reconnect
575
+ that is not an expiry resumes from the last processed cursor, so events the daemon
576
+ still retains replay and are de-duplicated. Startup and expiry bootstrap the stream;
577
+ ChatServer never pages `/threads/{thread}/messages` for history and never backfills.
498
578
  - **Media:** Text, images, and optional STT voice input are supported; video,
499
579
  stickers, and files are skipped. The latest four new images per snapshot, each at most 5 MiB, are attached directly.
500
580
  Models receiving them must support image input. Pending media gets bounded
@@ -530,8 +610,31 @@ Omitting `tts` keeps replies in text. Restart after configuration changes.
530
610
  Page reading supports public HTTP(S) text/HTML/JSON, not PDFs or authenticated
531
611
  pages. Set `messager.localTools` to select a subset or disable local tools.
532
612
  `messager.apiTools` separately enables provider-hosted tools; its default is empty.
533
- - **Failures:** Transient model errors retry the full generation up to three times,
534
- ten seconds apart. Polling backs off on API errors. Failed sends are never
613
+ - **Identity:** The daemon account ID and the signed-in WeChat username are tracked
614
+ separately. Every reply and lazy media load is fenced to both values captured when
615
+ it originated, re-checked after each request, so a re-login on the same daemon
616
+ profile cannot deliver someone else's queued reply or fetch their attachments.
617
+ Conversation queues, memory files, and image caches are scoped by that same pair, so
618
+ a cache written under an account-only identity is never adopted by a named login;
619
+ this is a security fix with no migration of existing account-only caches.
620
+ - **Reconnect and status:** A closed, idle, or failed event stream reconnects with
621
+ capped exponential backoff starting at `reconnectIntervalMs`. Heartbeat comments and
622
+ received chunks reset the idle deadline; the backoff resets only after an event is
623
+ handled, so a stream that connects but keeps failing to read a message backs off
624
+ further. Every retry after the first re-reads the account and revalidates both the
625
+ daemon account ID and `ownUsername`; a switch or re-login clears cursors, resets
626
+ the startup cutoff, and re-bootstraps the stream from the new login's current tail.
627
+ `/status` reports the last dispatched event, last stream activity, consecutive
628
+ stream failures, and the last stream error. Transient model errors retry the full
629
+ generation up to three times, ten seconds apart.
630
+ Outbound confirmation arrives over the same event stream: a nonterminal
631
+ `POST /sends` is settled by the matching `send.updated` (text) or `job.updated`
632
+ (media) payload, with one `GET /sends/{id}` or `GET /jobs/{id}` read after
633
+ registration and after each reconnect to reconcile a submission response or a
634
+ confirmation that arrived while detached. There is no interval polling. A
635
+ submission response that is already terminal is accepted or rejected immediately.
636
+ IDs are opaque strings under `/v1/accounts/{account}/...`, text and media
637
+ confirmations can never settle each other, and failed or uncertain sends are never
535
638
  automatically retried because delivery may already have succeeded remotely.
536
639
 
537
640
  Avoid overlapping pipelines for the same conversations, which can produce duplicate