@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 +142 -39
- package/dist/index.js +1670 -569
- package/dist/index.js.map +1 -1
- package/dist/models/chatserver-models.js +116 -59
- package/dist/models/chatserver-models.js.map +1 -1
- package/package.json +1 -1
- package/schemas/config.schema.json +14 -13
- package/schemas/models.schema.json +2 -2
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
|
|
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 **
|
|
11
|
-
provider, and a running, logged-in [agent-wechat server](https://
|
|
12
|
-
with
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
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` |
|
|
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
|
-
| `
|
|
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
|
|
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,
|
|
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
|
-
**
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
media
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
- **
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
- **
|
|
534
|
-
|
|
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
|