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