@xisui-pa/chatserver 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +661 -2
- package/THIRD_PARTY_NOTICES.txt +179 -0
- package/dist/index.js +13396 -0
- package/dist/index.js.map +1 -0
- package/dist/models/chatserver-models.js +2797 -0
- package/dist/models/chatserver-models.js.map +1 -0
- package/package.json +67 -4
- package/schemas/config.schema.json +356 -0
- package/schemas/models.schema.json +103 -0
package/README.md
CHANGED
|
@@ -1,3 +1,662 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ChatServer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript bridge between chat apps and LLM models. It filters incoming messages,
|
|
4
|
+
generates short replies, and retains compact conversation memory. The built-in
|
|
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
|
+
|
|
8
|
+
## Quick start: WeChat
|
|
9
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
1. Install and build from the repository root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. Add a model using guided setup:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run model:add -- --config_dir ./chatserver-config
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Choose or create a provider, enter its API base URL and key, then select or
|
|
31
|
+
enter a model ID. For a local server, a URL might be
|
|
32
|
+
`http://llm-host.local:1234/v1`. The command creates the config directory and
|
|
33
|
+
prints a reference such as `local/your-model-id`.
|
|
34
|
+
If your local server ignores API keys, enter a nonempty placeholder such as `local`.
|
|
35
|
+
|
|
36
|
+
3. Create `chatserver-config/config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"chats": [
|
|
41
|
+
{
|
|
42
|
+
"messager": {
|
|
43
|
+
"name": "wechat",
|
|
44
|
+
"replier": "local/your-model-id",
|
|
45
|
+
"localTools": [],
|
|
46
|
+
"apiTools": [],
|
|
47
|
+
"options": {
|
|
48
|
+
"baseUrl": "http://127.0.0.1:6174",
|
|
49
|
+
"accountId": "your-account-id",
|
|
50
|
+
"chatIds": ["wxid_example"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"filters": []
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
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
|
|
67
|
+
functions; omit it to enable the built-ins. `apiTools` defaults to `[]`.
|
|
68
|
+
|
|
69
|
+
4. Set the **agent-wechat bearer token** and start ChatServer.
|
|
70
|
+
|
|
71
|
+
macOS / Linux:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export AGENT_WECHAT_TOKEN='your-agent-wechat-token'
|
|
75
|
+
npm start -- --config_dir ./chatserver-config
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Windows PowerShell:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
$env:AGENT_WECHAT_TOKEN = 'your-agent-wechat-token'
|
|
82
|
+
npm start -- --config_dir ./chatserver-config
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
5. Send a **new** text message to the bot in the configured conversation.
|
|
86
|
+
Replies are processed sequentially with a random **1–3 minute** delay, including
|
|
87
|
+
before the first reply. With no filters, the reply model decides whether to
|
|
88
|
+
respond. Stop with Ctrl+C.
|
|
89
|
+
|
|
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.
|
|
94
|
+
For LAN model servers on Windows, use the full hostname such as `llm-host.local`
|
|
95
|
+
rather than a bare hostname.
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
Use the same config directory for the server and model commands. Select it with
|
|
100
|
+
`--config_dir <directory>` (`-c`) or `CHATSERVER_CONFIG_DIR`. An explicitly selected
|
|
101
|
+
directory must contain `config.json` before starting the server. Without an
|
|
102
|
+
explicit directory, the server creates a default `{ "chats": [] }` on first run.
|
|
103
|
+
|
|
104
|
+
| Platform | Default directory |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| macOS | `~/Library/Application Support/chatserver` |
|
|
107
|
+
| Linux | `$XDG_CONFIG_HOME/chatserver` or `~/.config/chatserver` |
|
|
108
|
+
| Windows | `%APPDATA%\chatserver` |
|
|
109
|
+
|
|
110
|
+
The directory contains `config.json`, `models.json`, optional ignore/token files,
|
|
111
|
+
optional `<messager_name>_context` files, and automatically managed `conversations/`
|
|
112
|
+
memory and image-cache files. An empty `chats` array
|
|
113
|
+
runs no adapters.
|
|
114
|
+
|
|
115
|
+
Set `messager.memoryTokenLimit` to change the token budget for each conversation
|
|
116
|
+
file belonging to that pipeline, for example `"memoryTokenLimit": 4000`. The
|
|
117
|
+
default is 1,000; valid limits are 100–16,000 tokens.
|
|
118
|
+
Notes and confirmed replies share this budget, including their compact JSON
|
|
119
|
+
metadata. Memory has a separate input allowance from the message snapshot.
|
|
120
|
+
Restart after changing the limit; lowering it removes the oldest entries on
|
|
121
|
+
the next turn for each conversation. Three-day expiry still applies. JSON
|
|
122
|
+
indentation and the file version field are not counted.
|
|
123
|
+
|
|
124
|
+
Each `chats` entry has a `messager` and optional `filters`:
|
|
125
|
+
|
|
126
|
+
| Field | Meaning |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `messager.name` | Adapter name; currently `wechat` |
|
|
129
|
+
| `messager.replier` | `<provider>/<model id>` from `models.json`, using the actual ID, not its CLI alias |
|
|
130
|
+
| `messager.memoryTokenLimit` | Tokens per conversation for notes and confirmed replies; defaults to 1,000 |
|
|
131
|
+
| `messager.persona` | Optional replier identity, tone, and conversational style |
|
|
132
|
+
| `messager.mainAccount` | Optional exact sender ID allowed to query `/status` in a direct chat |
|
|
133
|
+
| `messager.localTools` | Local tool names or `{ "type": "name" }` objects; omitted enables all five built-ins, `[]` disables local tools |
|
|
134
|
+
| `messager.apiTools` | Hosted tool type strings or definition objects; omitted or `[]` enables none |
|
|
135
|
+
| `messager.imageGeneration` | Optional boolean; omitted enables generation when `apiTools` includes `image_generation`; `true` adds that tool, `false` removes it |
|
|
136
|
+
| `messager.imageWidthLimit` | Maximum generated image width in pixels; defaults to `1024` |
|
|
137
|
+
| `messager.imageHeightLimit` | Maximum generated image height in pixels; defaults to `1024` |
|
|
138
|
+
| `messager.dailyImageGenLimit` | Daily image generation allowance shared across this messager's conversations; defaults to `10`; `0` disables it |
|
|
139
|
+
| `messager.options` | Adapter settings below |
|
|
140
|
+
| `filters` | Ordered filters; omitted or `[]` skips filtering |
|
|
141
|
+
| `stt` | Optional object with `type: "model"`, required `model`, and optional `audioConversionCmd`; omitted disables voice input |
|
|
142
|
+
| `tts` | Optional `{ "type": "model", "model": "provider/model-id", "options": { ... } }` for explicitly requested voice replies |
|
|
143
|
+
|
|
144
|
+
The former `messager.tools` field has been replaced by `messager.localTools`;
|
|
145
|
+
rename it in existing configurations. The old field is rejected with a migration
|
|
146
|
+
error so that a previous `tools: []` cannot silently enable the default tools.
|
|
147
|
+
|
|
148
|
+
Local and provider-hosted tools can be used together. For example, this enables
|
|
149
|
+
local arithmetic and time plus the provider's hosted web search:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"localTools": ["calculate", { "type": "current_time" }],
|
|
154
|
+
"apiTools": [
|
|
155
|
+
{ "type": "web_search", "search_context_size": "low" }
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Put these fields inside `messager`. Both arrays accept strings, objects, or a mix.
|
|
161
|
+
For example, `"apiTools": ["web_search"]` is equivalent to
|
|
162
|
+
`"apiTools": [{ "type": "web_search" }]`. Local objects contain only `type`,
|
|
163
|
+
which must name a registered local tool; duplicate names are deduplicated across
|
|
164
|
+
both forms. Hosted strings expand to `{ "type": "..." }`; hosted objects require
|
|
165
|
+
a nonempty `type` and preserve their provider-specific options. The resulting
|
|
166
|
+
objects are sent in the Responses API `tools` array alongside local function
|
|
167
|
+
declarations. Use the selected provider/model's
|
|
168
|
+
documented tool configuration; hosted tools are not discovered automatically.
|
|
169
|
+
The provider validates support and tool-specific options, and unsupported requests
|
|
170
|
+
fail rather than silently falling back to local tools. `function` and `custom`
|
|
171
|
+
definitions are rejected here: register application-executed functions as local
|
|
172
|
+
tools instead. Hosted tools must finish at the provider; text and generated image
|
|
173
|
+
results are supported. Client execution, approval exchanges, and other generated
|
|
174
|
+
artifacts are not implemented. Filters and formatting repairs receive no hosted
|
|
175
|
+
tools. Set both fields to `[]` and omit or disable `imageGeneration` to disable
|
|
176
|
+
all tool calling.
|
|
177
|
+
|
|
178
|
+
Each generation attempt allows up to eight local calls and eight hosted calls
|
|
179
|
+
across its model rounds, under the provider's existing timeout. Hosted requests
|
|
180
|
+
send `max_tool_calls` with the remaining hosted budget; providers must support
|
|
181
|
+
this Responses API parameter to enforce the cap during a response. All output
|
|
182
|
+
items, including hosted calls and reasoning, are replayed when local calls need
|
|
183
|
+
another round. Full-generation retries can repeat hosted calls and their charges;
|
|
184
|
+
completed image artifacts are preserved and reused across these retries.
|
|
185
|
+
|
|
186
|
+
Image generation uses the replier's provider-hosted
|
|
187
|
+
[`image_generation` Responses tool](https://developers.openai.com/api/docs/guides/tools-image-generation).
|
|
188
|
+
The selected provider/model must support this tool; there is no separate image
|
|
189
|
+
provider or fallback. Put these settings inside `messager`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"imageGeneration": true,
|
|
194
|
+
"imageWidthLimit": 1024,
|
|
195
|
+
"imageHeightLimit": 1024,
|
|
196
|
+
"dailyImageGenLimit": 10
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Alternatively, include `"image_generation"` in `apiTools` and omit
|
|
201
|
+
`imageGeneration` to detect support from that selection. A definition object can
|
|
202
|
+
specify provider options such as `quality` or the hosted image `model`. Explicit
|
|
203
|
+
`imageGeneration: false` overrides that tool selection. The reply model generates
|
|
204
|
+
or edits images only for explicit requests. Model filters recognize these requests
|
|
205
|
+
when generation is enabled, and WeChat sends returned bytes as image messages,
|
|
206
|
+
with an optional caption. Image-only responses are supported.
|
|
207
|
+
|
|
208
|
+
`imageWidthLimit` and `imageHeightLimit` are independent integer pixel limits from
|
|
209
|
+
1 to 16384, each defaulting to 1024. ChatServer combines them into the provider's
|
|
210
|
+
`WIDTHxHEIGHT` size string; the provider must support the requested size. The tool
|
|
211
|
+
receives this size unless its configured concrete size fits within both limits.
|
|
212
|
+
`auto` and larger sizes are replaced by the combined limits. ChatServer requests PNG
|
|
213
|
+
and rejects returned images exceeding either dimension or the existing 5 MiB
|
|
214
|
+
image byte limit. Image bytes stay outside reply JSON and conversation memory;
|
|
215
|
+
confirmed memory records an image-delivery marker, any caption, and `imageIds`
|
|
216
|
+
linking the reply to its generated cache entries. These IDs count toward the
|
|
217
|
+
memory budget and allow `read_image` lookup after older labels leave `cachedImages`.
|
|
218
|
+
|
|
219
|
+
Generated images are saved in the conversation's existing image store as soon as
|
|
220
|
+
their provider output is validated, before later model rounds, formatting repair,
|
|
221
|
+
or delivery, including when `read_image` is disabled. If later model work fails,
|
|
222
|
+
the artifact remains cached; retries reuse the completed image without another
|
|
223
|
+
image generation call. They share the received-image
|
|
224
|
+
limits: three days from caching, at most 30 images, and 64 MiB of cache JSON per
|
|
225
|
+
conversation. Entries have their own generated-image IDs, assistant attribution,
|
|
226
|
+
`origin: "generated"`, and a `deliveryStatus` of `pending`, `sent`, or `failed`.
|
|
227
|
+
Only `sent` confirms delivery; failed or interrupted sends may remain unconfirmed.
|
|
228
|
+
Updating delivery status never extends retention. Cache write failures are logged
|
|
229
|
+
and do not block sending.
|
|
230
|
+
|
|
231
|
+
When `read_image` is enabled, generated image references appear in `cachedImages`
|
|
232
|
+
and their bytes can be inspected in later turns, including after a restart or
|
|
233
|
+
failed upload. Caching does not resend images, and no resend tool is provided.
|
|
234
|
+
|
|
235
|
+
Quota is kept only in memory for each configured messager pipeline, shared across
|
|
236
|
+
its conversations and generation retries. Usage resets on server restart and at
|
|
237
|
+
midnight in the server's local timezone. No quota files are read or written.
|
|
238
|
+
Before each image-capable provider request, ChatServer
|
|
239
|
+
reserves one slot and sets `max_tool_calls: 1` (also limiting other hosted calls
|
|
240
|
+
in that response). An unused slot is released when a response confirms no image
|
|
241
|
+
call occurred. Ambiguous provider failures retain their reservation; failed
|
|
242
|
+
delivery or rejected image bytes do not refund generation charges. Once an image
|
|
243
|
+
is generated, further local-tool rounds and model retries cannot generate another
|
|
244
|
+
for that reply. When quota is exhausted, image generation is removed from the
|
|
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.
|
|
254
|
+
|
|
255
|
+
Provider URL citations are preserved as complete source links in outgoing text,
|
|
256
|
+
including after a formatting repair. Text is shortened to reserve room for links;
|
|
257
|
+
if the citations alone cannot fit the reply budget, delivery fails. Required hosted
|
|
258
|
+
citations are included even when the sender did not ask for sources, as required
|
|
259
|
+
by [OpenAI's hosted web search](https://developers.openai.com/api/docs/guides/tools-web-search).
|
|
260
|
+
|
|
261
|
+
The `messager.persona` setting accepts a nonempty string with one or more sentences.
|
|
262
|
+
For example: `"You are a warm, lightly humorous friend. Give practical photography advice. Keep your tone relaxed."`
|
|
263
|
+
Use `\n` within the JSON string to separate paragraphs; sentence breaks and newlines
|
|
264
|
+
are preserved in the prompt.
|
|
265
|
+
Omitting it keeps the default helpful assistant. It affects only the replier;
|
|
266
|
+
reply-length, citation, tool, JSON, and memory rules still apply. Restart the server
|
|
267
|
+
after changing this setting.
|
|
268
|
+
|
|
269
|
+
For editor completion and validation, add `$schema` pointing to
|
|
270
|
+
[schemas/config.schema.json](schemas/config.schema.json) or
|
|
271
|
+
[schemas/models.schema.json](schemas/models.schema.json) in the corresponding
|
|
272
|
+
file. For example: `"$schema": "file:///absolute/path/to/ChatServer/schemas/config.schema.json"`.
|
|
273
|
+
Encode spaces in file URLs as `%20`.
|
|
274
|
+
|
|
275
|
+
Inspect configuration without starting adapters:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npm start -- --config_dir ./chatserver-config --print-config
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Background context
|
|
282
|
+
|
|
283
|
+
Optionally create `<config_dir>/<messager_name>_context` yourself, for example
|
|
284
|
+
`chatserver-config/wechat_context`. Use plain UTF-8 text describing friends or
|
|
285
|
+
channels, with their IDs to identify which background applies:
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
Friend wxid_alice: Alice is learning photography and prefers brief, practical tips.
|
|
289
|
+
Channel 123456@chatroom: A photography club discussing weekend outings.
|
|
290
|
+
```
|
|
291
|
+
|
|
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;
|
|
297
|
+
use wxids in background entries to keep them unambiguous.
|
|
298
|
+
|
|
299
|
+
The background is included in model filter and replier inputs. It is shared by
|
|
300
|
+
pipelines with the same messager name and re-read for each processed turn, so
|
|
301
|
+
edits apply without restarting. Up to 2,000 tokens are included separately from
|
|
302
|
+
the message snapshot budget; longer context is marked truncated. Missing or empty
|
|
303
|
+
files add no context, and ChatServer never creates them. Read failures log a warning
|
|
304
|
+
and continue without background. Context is not sent to the STT model or saved as
|
|
305
|
+
conversation memory.
|
|
306
|
+
|
|
307
|
+
### WeChat options
|
|
308
|
+
|
|
309
|
+
| Option | Default | Meaning |
|
|
310
|
+
| --- | --- | --- |
|
|
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 |
|
|
313
|
+
| `tokenEnv` | `AGENT_WECHAT_TOKEN` | Environment variable containing the bearer token |
|
|
314
|
+
| `tokenFile` | unset | Token file used when the environment token is absent; relative to the config directory |
|
|
315
|
+
| `chatIds` | all chats | Exact conversation IDs to monitor; `[]` monitors none except `mainAccount`, if configured |
|
|
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 |
|
|
318
|
+
|
|
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`.
|
|
321
|
+
`mainAccount`, when set, is monitored even if omitted from `chatIds`. Its exact
|
|
322
|
+
`/status` command returns local queue, delivery, event-stream, and error information
|
|
323
|
+
without calling a model; it uses the normal reply delay. Other senders and group
|
|
324
|
+
status requests are ignored.
|
|
325
|
+
|
|
326
|
+
### Filters
|
|
327
|
+
|
|
328
|
+
For example, replace the quick start's empty `filters` with:
|
|
329
|
+
|
|
330
|
+
```json
|
|
331
|
+
[
|
|
332
|
+
{ "type": "ignore" },
|
|
333
|
+
{
|
|
334
|
+
"type": "model",
|
|
335
|
+
"model": "local/your-model-id",
|
|
336
|
+
"options": { "instructions": "Allow questions; deny advertisements; otherwise abstain." }
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`ignore` (alias `ignore-sender`) reads `wechat_ignore` in the config directory:
|
|
342
|
+
one sender ID per line or comma-separated, with `#` comments. Changes reload
|
|
343
|
+
without restarting. Use `options.file` for a different file or `options.senderIds`
|
|
344
|
+
for an inline list. Ignored messages are removed before models see them.
|
|
345
|
+
|
|
346
|
+
Model filters return `allowed`, `denied`, or `optional`. The first decisive result
|
|
347
|
+
wins; if all abstain, the replier decides. Errors fail closed. Filter models have
|
|
348
|
+
thinking disabled (`reasoning.effort: none`); choose a provider that supports that
|
|
349
|
+
setting. When `read_image` is enabled for the pipeline, model filters can use it
|
|
350
|
+
to inspect cached images, but receive no other local or hosted tools.
|
|
351
|
+
|
|
352
|
+
## Models
|
|
353
|
+
|
|
354
|
+
Provider URLs, model IDs/aliases, and provider-specific `timeoutMs` live in
|
|
355
|
+
`models.json`. Each provider requires an explicit `baseUrl`; requests default to a
|
|
356
|
+
**5-minute** timeout. API keys are stored separately under the provider name:
|
|
357
|
+
macOS Keychain, Linux Secret Service (`secret-tool`), or Windows Credential Manager
|
|
358
|
+
(requires the `CredentialManager` PowerShell module).
|
|
359
|
+
|
|
360
|
+
Each entry in a provider's `models` array can set `supportsImageToolResults`.
|
|
361
|
+
It defaults to `false`; enable it only when that model endpoint accepts native
|
|
362
|
+
image content arrays in `function_call_output`:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"id": "gpt-6.1-sol",
|
|
367
|
+
"name": "sol",
|
|
368
|
+
"supportsImageToolResults": true
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
When enabled, the initial chat input contains image IDs and source labels without
|
|
373
|
+
image bytes. The model can call `read_image` when visual inspection is needed;
|
|
374
|
+
the image is returned directly inside that function's result. Keep `read_image`
|
|
375
|
+
enabled in `messager.localTools` for this mode. If image lookup is unavailable,
|
|
376
|
+
filters and repliers fall back to direct attachments. Pending optional images do
|
|
377
|
+
not defer a text-only decision; `read_image` can retry their source media if the
|
|
378
|
+
model asks to inspect them. Existing cached bytes are reused before fetching
|
|
379
|
+
source media again. With the default `false`, up to
|
|
380
|
+
four new images attach directly and older tool-retrieved images attach as a
|
|
381
|
+
separate image message on the next model round. Filter and replier models each
|
|
382
|
+
use their own setting, even when they share a chat snapshot. Restart the server
|
|
383
|
+
after changing model settings.
|
|
384
|
+
|
|
385
|
+
| Command | Purpose |
|
|
386
|
+
| --- | --- |
|
|
387
|
+
| `npm run model:add` | Guided provider/model setup; discovers advertised IDs or accepts manual input |
|
|
388
|
+
| `npm run model:update` | Guided provider/model editor; Enter keeps current values |
|
|
389
|
+
| `npm run model:list` | List configured providers and models; `--json` prints the configuration |
|
|
390
|
+
| `npm run model:test` | Send a test prompt to a configured model |
|
|
391
|
+
| `npm run model:test-stt` | Transcribe a local audio file with a configured STT model |
|
|
392
|
+
| `npm run model:test-tts` | Synthesize speech and save a WAV with the same padding as WeChat replies |
|
|
393
|
+
| `npm run model:remove` | Remove a model and its provider/key if no models remain |
|
|
394
|
+
|
|
395
|
+
Append `-- --config_dir ./chatserver-config` to select the quick start directory.
|
|
396
|
+
All commands support `--help`; add/update also support `--interactive` (`-i`).
|
|
397
|
+
Flags remain available for scripts:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
npm run model:add -- --config_dir ./chatserver-config --provider local --base-url http://llm-host.local:1234/v1 --model your-model-id
|
|
401
|
+
npm run model:test -- --config_dir ./chatserver-config --provider local --name your-model-id
|
|
402
|
+
npm run model:update -- --config_dir ./chatserver-config --provider local --timeout 300000
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Test speech models without sending any WeChat messages:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
npm run model:test-stt -- --provider local --name your-stt-model --file ./voice.wav
|
|
409
|
+
npm run model:test-tts -- --provider local --name your-tts-model --voice your-provider-voice --text "Hello!" --output ./reply.wav
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
STT supports `--json` for a JSON transcript and `--audio-conversion-cmd` with
|
|
413
|
+
`{input}`/`{output}` for WAV-only engines, using the same conversion as chat input.
|
|
414
|
+
TTS accepts `--options` with a JSON object matching `tts.options`; `--voice` overrides
|
|
415
|
+
its voice setting. Output defaults to `tts-test.wav`; use `--force` to overwrite.
|
|
416
|
+
Both commands accept `--timeout`, `--config_dir`, and the same provider keys as
|
|
417
|
+
`model:test`. File paths resolve from the current directory.
|
|
418
|
+
|
|
419
|
+
`--name` selects a model's CLI alias (defaults to its ID). Update accepts `--model`
|
|
420
|
+
to change its API ID and `--new-name` to rename its alias. If the API ID changes,
|
|
421
|
+
update corresponding `config.json` references. Add requires overwrite confirmation
|
|
422
|
+
or `--force` for an existing alias. Commands without a terminal require flags.
|
|
423
|
+
|
|
424
|
+
Existing provider keys are reused. For missing keys, add reads `--api-key`, then
|
|
425
|
+
`OPENAI_API_KEY`, then a hidden prompt. Replace a saved key explicitly with
|
|
426
|
+
`--api-key`; `OPENAI_API_KEY` does not overwrite it. At runtime and in model tests,
|
|
427
|
+
`CHATSERVER_MODEL_API_KEY` overrides the saved key. Keys never go in `models.json`.
|
|
428
|
+
|
|
429
|
+
### Incoming voice
|
|
430
|
+
|
|
431
|
+
Add a speech-to-text model using `model:add`, then set `stt` alongside `messager`
|
|
432
|
+
and `filters` in the chat definition:
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
{
|
|
436
|
+
"messager": { "name": "wechat", "replier": "local/your-model-id" },
|
|
437
|
+
"stt": {
|
|
438
|
+
"type": "model",
|
|
439
|
+
"model": "speech-provider/your-stt-model-id",
|
|
440
|
+
"audioConversionCmd": "ffmpeg -nostdin -y -i {input} -ar 16000 -ac 1 -c:a pcm_s16le {output}"
|
|
441
|
+
},
|
|
442
|
+
"filters": []
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
The STT provider must implement the [OpenAI-compatible transcription endpoint](https://developers.openai.com/api/reference/resources/audio/subresources/transcriptions/methods/create)
|
|
447
|
+
(`POST <baseUrl>/audio/transcriptions`, multipart `file`/`model`, JSON `text`). It
|
|
448
|
+
uses that provider's saved key, timeout, and normal model retries; it does not need
|
|
449
|
+
to support the Responses API. `model:test` sends chat prompts, not audio.
|
|
450
|
+
|
|
451
|
+
`type` is currently `"model"`. An explicit `model` is required and must support
|
|
452
|
+
transcription; it does not default to `messager.replier`. Replace
|
|
453
|
+
old string settings such as `"stt": "speech/model"` with the object above.
|
|
454
|
+
|
|
455
|
+
`audioConversionCmd` is optional. Set it for WAV-only engines such as speech.cpp;
|
|
456
|
+
omit it to upload the original audio. Install the converter (FFmpeg in this example)
|
|
457
|
+
on the **ChatServer machine**, available on PATH or specified by an absolute path.
|
|
458
|
+
The command runs in a temporary directory with `{input}` and `{output}` replaced
|
|
459
|
+
by generated filenames; it must write WAV to `{output}`. Quote executable paths
|
|
460
|
+
containing spaces using your operating system's shell syntax (escape double quotes
|
|
461
|
+
in JSON). Conversion has a 30-second deadline, supports shutdown cancellation, and
|
|
462
|
+
removes temporary files afterwards. Conversion errors stop the turn before any
|
|
463
|
+
STT request; cached successful transcripts do not repeat conversion.
|
|
464
|
+
|
|
465
|
+
Voice messages are transcribed sequentially after the reply delay and sender
|
|
466
|
+
exclusions, before model filters and the replier. Transcripts retain the original
|
|
467
|
+
sender, timestamp, and message ID and are reused across retained snapshots. Raw
|
|
468
|
+
audio is not saved in conversation memory. Configure `tts` separately for voice replies.
|
|
469
|
+
|
|
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.
|
|
485
|
+
|
|
486
|
+
### Voice replies (TTS)
|
|
487
|
+
|
|
488
|
+
Add `tts` to the chat entry, alongside `messager`, `filters`, and optional `stt`:
|
|
489
|
+
|
|
490
|
+
```json
|
|
491
|
+
"tts": {
|
|
492
|
+
"type": "model",
|
|
493
|
+
"model": "local/your-tts-model-id",
|
|
494
|
+
"options": { "voice": "your-provider-voice", "speed": 1 }
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Register the TTS model in `models.json` using the model CLI. Its provider must
|
|
499
|
+
support an OpenAI-compatible `/audio/speech` endpoint returning uncompressed WAV.
|
|
500
|
+
`options` passes speech settings supported by that provider, such as `voice`,
|
|
501
|
+
`speed`, or `instructions`; it defaults to `{}`. ChatServer controls `model`,
|
|
502
|
+
`input`, `response_format` (`wav`), and `stream_format` (`audio`). Provider keys,
|
|
503
|
+
timeouts, and model-request retries work as for other models.
|
|
504
|
+
|
|
505
|
+
The replier selects voice only when the message being answered explicitly asks
|
|
506
|
+
for a voice reply. An incoming voice message alone does not select voice output.
|
|
507
|
+
The same short-reply limits apply to the spoken text. ChatServer adds independent
|
|
508
|
+
random **0.5–2 second** silences before and after the audio, without requiring
|
|
509
|
+
FFmpeg on the ChatServer machine.
|
|
510
|
+
|
|
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.
|
|
521
|
+
Omitting `tts` keeps replies in text. Restart after configuration changes.
|
|
522
|
+
|
|
523
|
+
## Runtime behavior
|
|
524
|
+
|
|
525
|
+
- **Queue:** One FIFO entry per conversation. Each turn uses messages since the last
|
|
526
|
+
successful reply or replier decision to stay silent, capped at the latest 30 source
|
|
527
|
+
messages. Filter denials retain messages for later turns. New arrivals update
|
|
528
|
+
pending context. Processing and sending are sequential, paced at 1–3 minutes.
|
|
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.
|
|
578
|
+
- **Media:** Text, images, and optional STT voice input are supported; video,
|
|
579
|
+
stickers, and files are skipped. The latest four new images per snapshot, each at most 5 MiB, are attached directly.
|
|
580
|
+
Models receiving them must support image input. Pending media gets bounded
|
|
581
|
+
retries; unavailable images are labeled rather than guessed.
|
|
582
|
+
With `read_image` enabled, images in retained snapshot messages are also downloaded
|
|
583
|
+
into `<conversation-hash>.images.json` beside the conversation memory file.
|
|
584
|
+
Older images are represented by source labels in `cachedImages`; the tool attaches
|
|
585
|
+
a requested image as native image input on the next model round. Models with
|
|
586
|
+
`supportsImageToolResults: true` receive image IDs only in the initial input and
|
|
587
|
+
receive requested images directly in tool results. Both repliers
|
|
588
|
+
and model filters can use it. Lookups are restricted to the current account and
|
|
589
|
+
conversation and work after restart. Images expire three days after the original
|
|
590
|
+
source timestamp; lookup never refreshes expiry. Cleanup runs at startup and
|
|
591
|
+
hourly, and expired entries cannot be retrieved between cleanups. Each conversation
|
|
592
|
+
keeps at most 30 images and 64 MiB of cache JSON, evicting the oldest first.
|
|
593
|
+
Recent cached-image labels have a 1,000-token prompt allowance; IDs retained in
|
|
594
|
+
memory can still retrieve images omitted from that list. Without `read_image`,
|
|
595
|
+
received images are attached directly without persistent image caching.
|
|
596
|
+
Generated images are always cached before delivery; `read_image` is required
|
|
597
|
+
for model inspection of these cached artifacts.
|
|
598
|
+
- **Replies:** Normally 1–2 short sentences, with a 150-token target and 200-token
|
|
599
|
+
hard cap. Citations are included when explicitly requested or required by hosted tools. Oversized text
|
|
600
|
+
is truncated locally. Malformed reply JSON or memory updates are logged in
|
|
601
|
+
full and receive one formatting-only repair request without tools; a second
|
|
602
|
+
invalid result stops delivery.
|
|
603
|
+
- **Memory:** Compact notes and confirmed replies persist per conversation for
|
|
604
|
+
three days, capped at 1,000 tokens by default (`messager.memoryTokenLimit`). This is summarized context, not a raw
|
|
605
|
+
message archive. Replier memory updates are saved even when it chooses not to reply;
|
|
606
|
+
filter denials skip the replier and its memory updates. Image bytes live in the
|
|
607
|
+
separate image cache and do not count toward the text memory budget.
|
|
608
|
+
- **Tools:** `web_search` (DuckDuckGo), `read_url`, `calculate`, `current_time`, and `read_image`
|
|
609
|
+
are enabled by default. Models need Responses API function calling to use them.
|
|
610
|
+
Page reading supports public HTTP(S) text/HTML/JSON, not PDFs or authenticated
|
|
611
|
+
pages. Set `messager.localTools` to select a subset or disable local tools.
|
|
612
|
+
`messager.apiTools` separately enables provider-hosted tools; its default is empty.
|
|
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
|
|
638
|
+
automatically retried because delivery may already have succeeded remotely.
|
|
639
|
+
|
|
640
|
+
Avoid overlapping pipelines for the same conversations, which can produce duplicate
|
|
641
|
+
replies. Reply pacing is per process and does not guarantee protection from account
|
|
642
|
+
bans.
|
|
643
|
+
|
|
644
|
+
## Development
|
|
645
|
+
|
|
646
|
+
Runtime limits and defaults are grouped and documented in [src/limit.ts](src/limit.ts).
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
npm run dev # Auto-reload
|
|
650
|
+
npm run build # Bundle server and model CLI into dist/
|
|
651
|
+
npm run typecheck
|
|
652
|
+
npm run lint
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
VS Code debug configurations and build tasks are in `.vscode/`. To use the built
|
|
656
|
+
executables directly, run `npm link`, then `chatserver` or `chatserver-models`.
|
|
657
|
+
|
|
658
|
+
Implementation entry points:
|
|
659
|
+
[server](src/server.ts), [config](src/config.ts),
|
|
660
|
+
[messagers](src/messagers/interface.ts), [filters](src/filters/interface.ts),
|
|
661
|
+
[repliers](src/repliers/interface.ts), and [tools](src/tools/interface.ts).
|
|
662
|
+
Register custom implementations in the corresponding registry before loading config.
|