wechatbridge-cli 1.6.0__tar.gz → 1.6.2__tar.gz
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.
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/PKG-INFO +36 -3
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/README.md +35 -2
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/tests/test_hardening.py +186 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/__init__.py +1 -1
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/agy.py +38 -11
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/runner_common.py +24 -6
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/PKG-INFO +36 -3
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/LICENSE +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/pyproject.toml +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/setup.cfg +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/tests/test_codex.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/tests/test_codex_auth.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/tests/test_dsh.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/tests/test_grok_auth.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/__main__.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/codex.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/config.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/dsh.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/grok.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/ilink.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/main.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge/update_check.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/SOURCES.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/requires.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wechatbridge-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Bridge WeChat messages to agy, Grok Build, or Codex CLIs — text/image/file/voice in, CLI replies and generated files back.
|
|
5
5
|
Author: WeChatBridge contributors
|
|
6
6
|
License: MIT
|
|
@@ -195,7 +195,7 @@ Key variables (all have defaults):
|
|
|
195
195
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (agy / grok / codex backends) |
|
|
196
196
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
197
197
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
198
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
198
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply (on small RAM hosts, lower this to avoid concurrent CLI memory spikes; see [Memory limits](#memory-limits)) |
|
|
199
199
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
200
200
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
201
201
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -261,6 +261,39 @@ sudo systemctl enable --now wechatbridge@bot3
|
|
|
261
261
|
|
|
262
262
|
Each instance reads its own config file (`~/.config/wechatbridge/bot2.env`) and keeps state under its own data directory (`~/.local/share/wechatbridge/bot2/`).
|
|
263
263
|
|
|
264
|
+
#### Memory limits
|
|
265
|
+
|
|
266
|
+
The multi-instance template (`deploy/wechatbridge@.service`) configures cgroup memory protection:
|
|
267
|
+
|
|
268
|
+
```ini
|
|
269
|
+
MemoryAccounting=yes
|
|
270
|
+
MemoryHigh=450M
|
|
271
|
+
MemoryMax=512M
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **Why these defaults?** Backend CLI child processes (such as `agy`) can reach ~276 MB+ RSS during a single turn, and spike even higher during image recognition or long conversational sessions. A tight hard cap (e.g. 300 MB) on ~1 GB RAM hosts triggers aggressive page thrashing, deep swap latency, and stream interruptions (`subscriber fell behind updates`). The `450M` soft limit (`MemoryHigh`) triggers gentle reclaim, while `512M` (`MemoryMax`) provides a hard ceiling preventing full-host OOM.
|
|
275
|
+
- **Overriding per-instance via drop-in:** Override memory limits without modifying the base unit template:
|
|
276
|
+
```bash
|
|
277
|
+
sudo mkdir -p /etc/systemd/system/wechatbridge@bot2.service.d/
|
|
278
|
+
cat << 'EOF' | sudo tee /etc/systemd/system/wechatbridge@bot2.service.d/memory.conf
|
|
279
|
+
[Service]
|
|
280
|
+
# for hosts with >= 2 GB RAM
|
|
281
|
+
MemoryHigh=600M
|
|
282
|
+
MemoryMax=768M
|
|
283
|
+
EOF
|
|
284
|
+
sudo systemctl daemon-reload
|
|
285
|
+
sudo systemctl restart wechatbridge@bot2
|
|
286
|
+
```
|
|
287
|
+
Verify active limits:
|
|
288
|
+
```bash
|
|
289
|
+
systemctl show wechatbridge@bot2 -p MemoryHigh,MemoryMax
|
|
290
|
+
```
|
|
291
|
+
- **Sizing guidance:** The defaults are tuned for small hosts (~1 GB RAM). On larger hosts with ample headroom, limits can be relaxed. When deploying multiple instances, note that each instance runs in its own service cgroup — budget for the worst-case combined ceiling of N × MemoryMax across running instances.
|
|
292
|
+
- **Interaction with concurrency:** Concurrent heavy turns (e.g. multiple users sending images at once) multiply backend CLI memory consumption. On memory-constrained hosts, prioritize lowering `WECHATBRIDGE_MAX_CONCURRENT` (e.g. to `2` or `1`) before increasing memory caps.
|
|
293
|
+
- **Swap configuration:** Avoid tightening `MemorySwapMax` — swap margin serves as a vital safety buffer that slows execution under pressure instead of triggering an immediate cgroup OOM kill.
|
|
294
|
+
- **Single-instance deployments:** The single-instance unit `deploy/wechatbridge.service` includes the same built-in memory limits (`MemoryHigh=450M` and `MemoryMax=512M`). To adjust them, override via a drop-in under `/etc/systemd/system/wechatbridge.service.d/` and replace the unit name in the `systemctl` commands above with `wechatbridge`. Takes effect when the unit file is installed or re-installed; `update.sh` upgrades do not touch unit files.
|
|
295
|
+
- **Platform scope:** These cgroup memory limits apply only to Linux systemd deployments; the macOS (launchd) and Windows (Task Scheduler) paths have no equivalent per-instance cap.
|
|
296
|
+
|
|
264
297
|
### macOS (launchd)
|
|
265
298
|
|
|
266
299
|
```bash
|
|
@@ -302,7 +335,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
302
335
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
303
336
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
304
337
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
305
|
-
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
338
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap (heavy concurrent tasks multiply subprocess memory; see [Memory limits](#memory-limits)).
|
|
306
339
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
307
340
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
308
341
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
@@ -163,7 +163,7 @@ Key variables (all have defaults):
|
|
|
163
163
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (agy / grok / codex backends) |
|
|
164
164
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
165
165
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
166
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
166
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply (on small RAM hosts, lower this to avoid concurrent CLI memory spikes; see [Memory limits](#memory-limits)) |
|
|
167
167
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
168
168
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
169
169
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -229,6 +229,39 @@ sudo systemctl enable --now wechatbridge@bot3
|
|
|
229
229
|
|
|
230
230
|
Each instance reads its own config file (`~/.config/wechatbridge/bot2.env`) and keeps state under its own data directory (`~/.local/share/wechatbridge/bot2/`).
|
|
231
231
|
|
|
232
|
+
#### Memory limits
|
|
233
|
+
|
|
234
|
+
The multi-instance template (`deploy/wechatbridge@.service`) configures cgroup memory protection:
|
|
235
|
+
|
|
236
|
+
```ini
|
|
237
|
+
MemoryAccounting=yes
|
|
238
|
+
MemoryHigh=450M
|
|
239
|
+
MemoryMax=512M
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- **Why these defaults?** Backend CLI child processes (such as `agy`) can reach ~276 MB+ RSS during a single turn, and spike even higher during image recognition or long conversational sessions. A tight hard cap (e.g. 300 MB) on ~1 GB RAM hosts triggers aggressive page thrashing, deep swap latency, and stream interruptions (`subscriber fell behind updates`). The `450M` soft limit (`MemoryHigh`) triggers gentle reclaim, while `512M` (`MemoryMax`) provides a hard ceiling preventing full-host OOM.
|
|
243
|
+
- **Overriding per-instance via drop-in:** Override memory limits without modifying the base unit template:
|
|
244
|
+
```bash
|
|
245
|
+
sudo mkdir -p /etc/systemd/system/wechatbridge@bot2.service.d/
|
|
246
|
+
cat << 'EOF' | sudo tee /etc/systemd/system/wechatbridge@bot2.service.d/memory.conf
|
|
247
|
+
[Service]
|
|
248
|
+
# for hosts with >= 2 GB RAM
|
|
249
|
+
MemoryHigh=600M
|
|
250
|
+
MemoryMax=768M
|
|
251
|
+
EOF
|
|
252
|
+
sudo systemctl daemon-reload
|
|
253
|
+
sudo systemctl restart wechatbridge@bot2
|
|
254
|
+
```
|
|
255
|
+
Verify active limits:
|
|
256
|
+
```bash
|
|
257
|
+
systemctl show wechatbridge@bot2 -p MemoryHigh,MemoryMax
|
|
258
|
+
```
|
|
259
|
+
- **Sizing guidance:** The defaults are tuned for small hosts (~1 GB RAM). On larger hosts with ample headroom, limits can be relaxed. When deploying multiple instances, note that each instance runs in its own service cgroup — budget for the worst-case combined ceiling of N × MemoryMax across running instances.
|
|
260
|
+
- **Interaction with concurrency:** Concurrent heavy turns (e.g. multiple users sending images at once) multiply backend CLI memory consumption. On memory-constrained hosts, prioritize lowering `WECHATBRIDGE_MAX_CONCURRENT` (e.g. to `2` or `1`) before increasing memory caps.
|
|
261
|
+
- **Swap configuration:** Avoid tightening `MemorySwapMax` — swap margin serves as a vital safety buffer that slows execution under pressure instead of triggering an immediate cgroup OOM kill.
|
|
262
|
+
- **Single-instance deployments:** The single-instance unit `deploy/wechatbridge.service` includes the same built-in memory limits (`MemoryHigh=450M` and `MemoryMax=512M`). To adjust them, override via a drop-in under `/etc/systemd/system/wechatbridge.service.d/` and replace the unit name in the `systemctl` commands above with `wechatbridge`. Takes effect when the unit file is installed or re-installed; `update.sh` upgrades do not touch unit files.
|
|
263
|
+
- **Platform scope:** These cgroup memory limits apply only to Linux systemd deployments; the macOS (launchd) and Windows (Task Scheduler) paths have no equivalent per-instance cap.
|
|
264
|
+
|
|
232
265
|
### macOS (launchd)
|
|
233
266
|
|
|
234
267
|
```bash
|
|
@@ -270,7 +303,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
270
303
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
271
304
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
272
305
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
273
|
-
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
306
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap (heavy concurrent tasks multiply subprocess memory; see [Memory limits](#memory-limits)).
|
|
274
307
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
275
308
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
276
309
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
@@ -2302,6 +2302,28 @@ class TestClassifyCliError(unittest.TestCase):
|
|
|
2302
2302
|
def test_network_unreachable(self):
|
|
2303
2303
|
self.assertEqual(self._cat("network is unreachable"), "network")
|
|
2304
2304
|
|
|
2305
|
+
# --- agent_stream_interrupted -------------------------------------------------
|
|
2306
|
+
|
|
2307
|
+
def test_agent_stream_interrupted_real_error(self):
|
|
2308
|
+
err = "Error: the connection to the agent was interrupted before the response finished: subscriber fell behind updates, stalled for 8s"
|
|
2309
|
+
self.assertEqual(self._cat(err), "agent_stream_interrupted")
|
|
2310
|
+
|
|
2311
|
+
def test_agent_stream_interrupted_stalled_variants(self):
|
|
2312
|
+
self.assertEqual(self._cat("subscriber fell behind updates, stalled for 5s"), "agent_stream_interrupted")
|
|
2313
|
+
self.assertEqual(self._cat("subscriber fell behind updates, stalled for 13s"), "agent_stream_interrupted")
|
|
2314
|
+
self.assertEqual(self._cat("subscriber fell behind updates, stalled for 13 s"), "agent_stream_interrupted")
|
|
2315
|
+
self.assertEqual(self._cat("stalled for 8s"), "agent_stream_interrupted")
|
|
2316
|
+
|
|
2317
|
+
def test_agent_stream_interrupted_conversation_stream_failed(self):
|
|
2318
|
+
self.assertEqual(self._cat("conversation update stream failed"), "agent_stream_interrupted")
|
|
2319
|
+
|
|
2320
|
+
def test_agent_stream_interrupted_connection_interrupted(self):
|
|
2321
|
+
self.assertEqual(self._cat("connection to the agent was interrupted"), "agent_stream_interrupted")
|
|
2322
|
+
|
|
2323
|
+
def test_agent_stream_interrupted_negative_installed(self):
|
|
2324
|
+
err = "package wechatbridge-cli 1.6.0, installed using Python 3.13.5"
|
|
2325
|
+
self.assertNotEqual(self._cat(err), "agent_stream_interrupted")
|
|
2326
|
+
|
|
2305
2327
|
# --- cascade_timeout ----------------------------------------------------------
|
|
2306
2328
|
|
|
2307
2329
|
def test_cascade_timeout(self):
|
|
@@ -2404,6 +2426,12 @@ class TestFormatCliErrorCategories(unittest.TestCase):
|
|
|
2404
2426
|
out = self._fmt("connection refused")
|
|
2405
2427
|
self.assertIn("网络错误", out)
|
|
2406
2428
|
|
|
2429
|
+
def test_agent_stream_interrupted_copy(self):
|
|
2430
|
+
out = self._fmt("Error: the connection to the agent was interrupted before the response finished: subscriber fell behind updates, stalled for 8s")
|
|
2431
|
+
self.assertIn("助手连接中断", out)
|
|
2432
|
+
self.assertIn("❌", out)
|
|
2433
|
+
self.assertIn("/new", out)
|
|
2434
|
+
|
|
2407
2435
|
def test_timeout_still_error(self):
|
|
2408
2436
|
out = self._fmt("timeout")
|
|
2409
2437
|
self.assertIn("超时", out)
|
|
@@ -2439,6 +2467,12 @@ class TestFormatCliErrorNoRawInReply(unittest.TestCase):
|
|
|
2439
2467
|
self.assertNotIn("rate", out.lower())
|
|
2440
2468
|
self.assertNotIn("limit", out.lower())
|
|
2441
2469
|
|
|
2470
|
+
def test_no_raw_agent_stream_interrupted(self):
|
|
2471
|
+
out = self._fmt("Error: the connection to the agent was interrupted before the response finished: subscriber fell behind updates, stalled for 8s")
|
|
2472
|
+
self.assertNotIn("subscriber", out.lower())
|
|
2473
|
+
self.assertNotIn("interrupted", out.lower())
|
|
2474
|
+
self.assertNotIn("stalled", out.lower())
|
|
2475
|
+
|
|
2442
2476
|
|
|
2443
2477
|
class TestFormatCliErrorLogging(unittest.TestCase):
|
|
2444
2478
|
"""format_cli_error logs category, not raw text."""
|
|
@@ -2797,5 +2831,157 @@ class TestUpdateScriptPackageMatch(unittest.TestCase):
|
|
|
2797
2831
|
shutil.rmtree(td, ignore_errors=True)
|
|
2798
2832
|
|
|
2799
2833
|
|
|
2834
|
+
class _FakeAgyProc:
|
|
2835
|
+
def __init__(self, stdout: str, stderr: str, rc: int = 0, pid: int = 4242):
|
|
2836
|
+
self._stdout = stdout.encode("utf-8")
|
|
2837
|
+
self._stderr = stderr.encode("utf-8")
|
|
2838
|
+
self.returncode = rc
|
|
2839
|
+
self.pid = pid
|
|
2840
|
+
|
|
2841
|
+
async def communicate(self):
|
|
2842
|
+
return self._stdout, self._stderr
|
|
2843
|
+
|
|
2844
|
+
def kill(self):
|
|
2845
|
+
pass
|
|
2846
|
+
|
|
2847
|
+
|
|
2848
|
+
class TestAgyStreamRetry(unittest.IsolatedAsyncioTestCase):
|
|
2849
|
+
"""Test agy runner retry behavior for stream interruption and cascade timeouts."""
|
|
2850
|
+
|
|
2851
|
+
def setUp(self):
|
|
2852
|
+
from wechatbridge.config import config
|
|
2853
|
+
|
|
2854
|
+
self.td = tempfile.mkdtemp(prefix="wb-agy-retry-")
|
|
2855
|
+
self._patchers = [
|
|
2856
|
+
mock.patch.object(config, "session_base_dir", self.td),
|
|
2857
|
+
]
|
|
2858
|
+
for p in self._patchers:
|
|
2859
|
+
p.start()
|
|
2860
|
+
|
|
2861
|
+
def tearDown(self):
|
|
2862
|
+
for p in self._patchers:
|
|
2863
|
+
p.stop()
|
|
2864
|
+
shutil.rmtree(self.td, ignore_errors=True)
|
|
2865
|
+
|
|
2866
|
+
def test_is_transient_stream_error_helper(self):
|
|
2867
|
+
from wechatbridge.agy import _is_transient_stream_error
|
|
2868
|
+
|
|
2869
|
+
self.assertIsNone(_is_transient_stream_error(""))
|
|
2870
|
+
self.assertEqual(_is_transient_stream_error("timeout waiting for cascade"), "cascade")
|
|
2871
|
+
self.assertEqual(_is_transient_stream_error("timeout waiting for response"), "cascade")
|
|
2872
|
+
self.assertEqual(
|
|
2873
|
+
_is_transient_stream_error("subscriber fell behind updates, stalled for 8s"),
|
|
2874
|
+
"stream",
|
|
2875
|
+
)
|
|
2876
|
+
self.assertEqual(_is_transient_stream_error("conversation update stream failed"), "stream")
|
|
2877
|
+
self.assertEqual(_is_transient_stream_error("connection to the agent was interrupted"), "stream")
|
|
2878
|
+
self.assertIsNone(_is_transient_stream_error("package wechatbridge-cli 1.6.0, installed using Python 3.13.5"))
|
|
2879
|
+
self.assertIsNone(_is_transient_stream_error("some random error"))
|
|
2880
|
+
|
|
2881
|
+
async def test_stream_interrupted_retries_and_recovers(self):
|
|
2882
|
+
from wechatbridge import agy as agy_mod
|
|
2883
|
+
|
|
2884
|
+
calls = 0
|
|
2885
|
+
stream_err = (
|
|
2886
|
+
"Error: the connection to the agent was interrupted before the response finished: "
|
|
2887
|
+
"subscriber fell behind updates, stalled for 8s"
|
|
2888
|
+
)
|
|
2889
|
+
|
|
2890
|
+
async def spawn(*_a, **_k):
|
|
2891
|
+
nonlocal calls
|
|
2892
|
+
calls += 1
|
|
2893
|
+
if calls == 1:
|
|
2894
|
+
return _FakeAgyProc("", stream_err, rc=1)
|
|
2895
|
+
return _FakeAgyProc("hello from retry", "", rc=0)
|
|
2896
|
+
|
|
2897
|
+
with mock.patch("asyncio.create_subprocess_exec", side_effect=spawn), \
|
|
2898
|
+
mock.patch.object(agy_mod, "terminate_process", new=AsyncMock()), \
|
|
2899
|
+
mock.patch.object(agy_mod.asyncio, "sleep", new=AsyncMock()) as mock_sleep:
|
|
2900
|
+
display, arts = await agy_mod.run_agy("hi", "u-stream-ok")
|
|
2901
|
+
|
|
2902
|
+
self.assertEqual(calls, 2)
|
|
2903
|
+
self.assertEqual(display, "hello from retry")
|
|
2904
|
+
self.assertEqual(arts, [])
|
|
2905
|
+
mock_sleep.assert_awaited_once_with(3)
|
|
2906
|
+
|
|
2907
|
+
async def test_stream_interrupted_retry_fails_returns_stream_interrupted_copy(self):
|
|
2908
|
+
from wechatbridge import agy as agy_mod
|
|
2909
|
+
|
|
2910
|
+
calls = 0
|
|
2911
|
+
stream_err = (
|
|
2912
|
+
"Error: the connection to the agent was interrupted before the response finished: "
|
|
2913
|
+
"subscriber fell behind updates, stalled for 8s"
|
|
2914
|
+
)
|
|
2915
|
+
|
|
2916
|
+
async def spawn(*_a, **_k):
|
|
2917
|
+
nonlocal calls
|
|
2918
|
+
calls += 1
|
|
2919
|
+
return _FakeAgyProc("", stream_err, rc=1)
|
|
2920
|
+
|
|
2921
|
+
with mock.patch("asyncio.create_subprocess_exec", side_effect=spawn), \
|
|
2922
|
+
mock.patch.object(agy_mod, "terminate_process", new=AsyncMock()), \
|
|
2923
|
+
mock.patch.object(agy_mod.asyncio, "sleep", new=AsyncMock()) as mock_sleep:
|
|
2924
|
+
display, arts = await agy_mod.run_agy("hi", "u-stream-fail")
|
|
2925
|
+
|
|
2926
|
+
self.assertEqual(calls, 2)
|
|
2927
|
+
self.assertEqual(arts, [])
|
|
2928
|
+
self.assertIn("助手连接中断", display)
|
|
2929
|
+
self.assertIn("❌", display)
|
|
2930
|
+
self.assertIn("/new", display)
|
|
2931
|
+
mock_sleep.assert_awaited_once_with(3)
|
|
2932
|
+
|
|
2933
|
+
async def test_stream_interrupted_retry_timeout_returns_stream_interrupted_copy(self):
|
|
2934
|
+
from wechatbridge import agy as agy_mod
|
|
2935
|
+
|
|
2936
|
+
calls = 0
|
|
2937
|
+
stream_err = (
|
|
2938
|
+
"Error: the connection to the agent was interrupted before the response finished: "
|
|
2939
|
+
"subscriber fell behind updates, stalled for 8s"
|
|
2940
|
+
)
|
|
2941
|
+
|
|
2942
|
+
class _TimeoutProc(_FakeAgyProc):
|
|
2943
|
+
async def communicate(self):
|
|
2944
|
+
raise asyncio.TimeoutError()
|
|
2945
|
+
|
|
2946
|
+
async def spawn(*_a, **_k):
|
|
2947
|
+
nonlocal calls
|
|
2948
|
+
calls += 1
|
|
2949
|
+
if calls == 1:
|
|
2950
|
+
return _FakeAgyProc("", stream_err, rc=1)
|
|
2951
|
+
return _TimeoutProc("", "", rc=0)
|
|
2952
|
+
|
|
2953
|
+
mock_term = AsyncMock()
|
|
2954
|
+
with mock.patch("asyncio.create_subprocess_exec", side_effect=spawn), \
|
|
2955
|
+
mock.patch.object(agy_mod, "terminate_process", new=mock_term), \
|
|
2956
|
+
mock.patch.object(agy_mod.asyncio, "sleep", new=AsyncMock()):
|
|
2957
|
+
display, arts = await agy_mod.run_agy("hi", "u-stream-timeout")
|
|
2958
|
+
|
|
2959
|
+
self.assertEqual(calls, 2)
|
|
2960
|
+
self.assertEqual(arts, [])
|
|
2961
|
+
self.assertIn("助手连接中断", display)
|
|
2962
|
+
mock_term.assert_awaited_once()
|
|
2963
|
+
|
|
2964
|
+
async def test_cascade_timeout_retries_and_fails_with_cascade_copy(self):
|
|
2965
|
+
from wechatbridge import agy as agy_mod
|
|
2966
|
+
|
|
2967
|
+
calls = 0
|
|
2968
|
+
cascade_err = "Timeout waiting for cascade response"
|
|
2969
|
+
|
|
2970
|
+
async def spawn(*_a, **_k):
|
|
2971
|
+
nonlocal calls
|
|
2972
|
+
calls += 1
|
|
2973
|
+
return _FakeAgyProc("", cascade_err, rc=1)
|
|
2974
|
+
|
|
2975
|
+
with mock.patch("asyncio.create_subprocess_exec", side_effect=spawn), \
|
|
2976
|
+
mock.patch.object(agy_mod, "terminate_process", new=AsyncMock()), \
|
|
2977
|
+
mock.patch.object(agy_mod.asyncio, "sleep", new=AsyncMock()) as mock_sleep:
|
|
2978
|
+
display, arts = await agy_mod.run_agy("hi", "u-cascade-fail")
|
|
2979
|
+
|
|
2980
|
+
self.assertEqual(calls, 2)
|
|
2981
|
+
self.assertEqual(arts, [])
|
|
2982
|
+
self.assertIn("模型响应超时", display)
|
|
2983
|
+
mock_sleep.assert_not_awaited()
|
|
2984
|
+
|
|
2985
|
+
|
|
2800
2986
|
if __name__ == "__main__":
|
|
2801
2987
|
unittest.main()
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""WeChatBridge — bridge WeChat messages to agy, Grok Build, or Codex CLIs."""
|
|
2
|
-
__version__ = "1.6.
|
|
2
|
+
__version__ = "1.6.2"
|
|
@@ -18,7 +18,7 @@ from .runner_common import (
|
|
|
18
18
|
sanitize_user_id, get_session_dir, ensure_session_dir, is_first_message, mark_initialized, clear_initialized,
|
|
19
19
|
clean_output, load_prefs, save_prefs, is_dangerous, parse_model_effort,
|
|
20
20
|
sanitize_env, terminate_process, update_active_prefs,
|
|
21
|
-
format_error, format_cli_error, is_bridge_formatted_reply, EMPTY_REPLY,
|
|
21
|
+
format_error, format_cli_error, _classify_cli_error, is_bridge_formatted_reply, EMPTY_REPLY,
|
|
22
22
|
ANSI_RE, HTML_TAG_RE, validate_add_dir,
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -55,6 +55,22 @@ def extract_artifacts(text: str) -> list[tuple[str, str]]:
|
|
|
55
55
|
return result
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _is_transient_stream_error(stderr_text: str) -> str | None:
|
|
59
|
+
"""Check if stderr indicates a transient stream error eligible for retry.
|
|
60
|
+
|
|
61
|
+
Returns "cascade" for cascade/response timeouts, "stream" for agent stream
|
|
62
|
+
interruption, or None if not retriable.
|
|
63
|
+
"""
|
|
64
|
+
if not stderr_text:
|
|
65
|
+
return None
|
|
66
|
+
cat = _classify_cli_error(stderr_text, backend="agy")
|
|
67
|
+
if cat == "cascade_timeout":
|
|
68
|
+
return "cascade"
|
|
69
|
+
if cat == "agent_stream_interrupted":
|
|
70
|
+
return "stream"
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
58
74
|
def ensure_user_gemini(user_id: str) -> str:
|
|
59
75
|
"""Ensure per-user .gemini directory with auth token and default persona.
|
|
60
76
|
|
|
@@ -235,8 +251,15 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
235
251
|
stderr_text,
|
|
236
252
|
)
|
|
237
253
|
if not stdout_text and stderr_text:
|
|
238
|
-
|
|
239
|
-
|
|
254
|
+
err_kind = _is_transient_stream_error(stderr_text)
|
|
255
|
+
if err_kind:
|
|
256
|
+
logger.warning(
|
|
257
|
+
"Transient stream error (%s) detected for user %s, retrying once automatically",
|
|
258
|
+
err_kind,
|
|
259
|
+
user_id,
|
|
260
|
+
)
|
|
261
|
+
if err_kind == "stream":
|
|
262
|
+
await asyncio.sleep(3)
|
|
240
263
|
retry_process = await asyncio.create_subprocess_exec(
|
|
241
264
|
*cmd,
|
|
242
265
|
stdout=asyncio.subprocess.PIPE,
|
|
@@ -257,10 +280,12 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
257
280
|
timeout, user_id,
|
|
258
281
|
)
|
|
259
282
|
await terminate_process(retry_process, graceful=True)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
if err_kind == "cascade":
|
|
284
|
+
return format_error(
|
|
285
|
+
"模型响应超时",
|
|
286
|
+
"模型响应超时,自动重试仍超时。请稍后重试或简化指令。",
|
|
287
|
+
), []
|
|
288
|
+
return format_cli_error(stderr_text, backend="agy"), []
|
|
264
289
|
except (asyncio.CancelledError, Exception):
|
|
265
290
|
await terminate_process(retry_process, graceful=False)
|
|
266
291
|
raise
|
|
@@ -278,10 +303,12 @@ async def run_agy(prompt: str, user_id: str, timeout: int = None) -> tuple[str,
|
|
|
278
303
|
):
|
|
279
304
|
mark_initialized(session_dir, backend="agy")
|
|
280
305
|
return r_display, r_artifacts
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
if err_kind == "cascade":
|
|
307
|
+
return format_error(
|
|
308
|
+
"模型响应超时",
|
|
309
|
+
"模型响应超时,自动重试仍失败。请稍后重试或简化指令。",
|
|
310
|
+
), []
|
|
311
|
+
return format_cli_error(r_stderr_text or stderr_text, backend="agy"), []
|
|
285
312
|
return format_cli_error(stderr_text, backend="agy"), []
|
|
286
313
|
# Non-zero exit: never treat raw stdout as a normal success reply
|
|
287
314
|
raw = stderr_text or stdout_text or display or "process exited abnormally"
|
|
@@ -298,9 +298,9 @@ def _classify_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
298
298
|
Returns one of:
|
|
299
299
|
payload_too_large, context_too_large, invalid_argument,
|
|
300
300
|
auth, resource_exhausted, rate_limit, bare_429, quota,
|
|
301
|
-
network, timeout, cascade_timeout,
|
|
302
|
-
session_not_found, model_invalid, command_not_found,
|
|
303
|
-
unknown
|
|
301
|
+
agent_stream_interrupted, network, timeout, cascade_timeout,
|
|
302
|
+
permission, session_not_found, model_invalid, command_not_found,
|
|
303
|
+
not_found, unknown
|
|
304
304
|
|
|
305
305
|
This is a pure function (no I/O, no logging). ``format_cli_error``
|
|
306
306
|
drives user-facing copy from the category; callers that need the
|
|
@@ -434,7 +434,16 @@ def _classify_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
434
434
|
):
|
|
435
435
|
return "bare_429"
|
|
436
436
|
|
|
437
|
-
# --- 6.
|
|
437
|
+
# --- 6. Agent stream interrupted ----------------------------------------------
|
|
438
|
+
if (
|
|
439
|
+
"subscriber fell behind updates" in lower
|
|
440
|
+
or "connection to the agent was interrupted" in lower
|
|
441
|
+
or "conversation update stream failed" in lower
|
|
442
|
+
or bool(re.search(r"\bstalled for \d+\s*s", lower))
|
|
443
|
+
):
|
|
444
|
+
return "agent_stream_interrupted"
|
|
445
|
+
|
|
446
|
+
# --- 7. Network ---------------------------------------------------------------
|
|
438
447
|
if (
|
|
439
448
|
"connection refused" in lower
|
|
440
449
|
or "connection reset" in lower
|
|
@@ -449,14 +458,14 @@ def _classify_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
449
458
|
):
|
|
450
459
|
return "network"
|
|
451
460
|
|
|
452
|
-
# ---
|
|
461
|
+
# --- 8. Timeout / cascade -----------------------------------------------------
|
|
453
462
|
if "timeout waiting for cascade" in lower or "timeout waiting for response" in lower:
|
|
454
463
|
return "cascade_timeout"
|
|
455
464
|
|
|
456
465
|
if "timeout" in lower or "timed out" in lower or "deadline exceeded" in lower:
|
|
457
466
|
return "timeout"
|
|
458
467
|
|
|
459
|
-
# ---
|
|
468
|
+
# --- 9. Permission / misc specific --------------------------------------------
|
|
460
469
|
if "permission" in lower and ("denied" in lower or "refuse" in lower or "rejected" in lower):
|
|
461
470
|
return "permission"
|
|
462
471
|
|
|
@@ -557,6 +566,15 @@ def format_cli_error(raw_message: str, *, backend: str = "") -> str:
|
|
|
557
566
|
if category == "network":
|
|
558
567
|
return format_error("网络错误", "连不上服务,请检查网络后重试。")
|
|
559
568
|
|
|
569
|
+
if category == "agent_stream_interrupted":
|
|
570
|
+
return format_error(
|
|
571
|
+
"助手连接中断",
|
|
572
|
+
(
|
|
573
|
+
"助手响应流意外中断。\n"
|
|
574
|
+
"请发 /new 开始新会话或稍后再试。"
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
|
|
560
578
|
if category == "cascade_timeout":
|
|
561
579
|
return format_error(
|
|
562
580
|
"模型响应超时",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wechatbridge-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Bridge WeChat messages to agy, Grok Build, or Codex CLIs — text/image/file/voice in, CLI replies and generated files back.
|
|
5
5
|
Author: WeChatBridge contributors
|
|
6
6
|
License: MIT
|
|
@@ -195,7 +195,7 @@ Key variables (all have defaults):
|
|
|
195
195
|
| `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (agy / grok / codex backends) |
|
|
196
196
|
| `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
|
|
197
197
|
| `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
|
|
198
|
-
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
|
|
198
|
+
| `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply (on small RAM hosts, lower this to avoid concurrent CLI memory spikes; see [Memory limits](#memory-limits)) |
|
|
199
199
|
| `WECHATBRIDGE_CONFIRM_TOKEN` | `y` | reply this token to approve a gated dangerous prompt |
|
|
200
200
|
| `WECHATBRIDGE_ENABLE_MCP` | `true` | enable the `/mcp` help text command |
|
|
201
201
|
| `WECHATBRIDGE_ENABLE_SUBAGENT` | `true` | enable the `/agent` prompt-rewrite command |
|
|
@@ -261,6 +261,39 @@ sudo systemctl enable --now wechatbridge@bot3
|
|
|
261
261
|
|
|
262
262
|
Each instance reads its own config file (`~/.config/wechatbridge/bot2.env`) and keeps state under its own data directory (`~/.local/share/wechatbridge/bot2/`).
|
|
263
263
|
|
|
264
|
+
#### Memory limits
|
|
265
|
+
|
|
266
|
+
The multi-instance template (`deploy/wechatbridge@.service`) configures cgroup memory protection:
|
|
267
|
+
|
|
268
|
+
```ini
|
|
269
|
+
MemoryAccounting=yes
|
|
270
|
+
MemoryHigh=450M
|
|
271
|
+
MemoryMax=512M
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **Why these defaults?** Backend CLI child processes (such as `agy`) can reach ~276 MB+ RSS during a single turn, and spike even higher during image recognition or long conversational sessions. A tight hard cap (e.g. 300 MB) on ~1 GB RAM hosts triggers aggressive page thrashing, deep swap latency, and stream interruptions (`subscriber fell behind updates`). The `450M` soft limit (`MemoryHigh`) triggers gentle reclaim, while `512M` (`MemoryMax`) provides a hard ceiling preventing full-host OOM.
|
|
275
|
+
- **Overriding per-instance via drop-in:** Override memory limits without modifying the base unit template:
|
|
276
|
+
```bash
|
|
277
|
+
sudo mkdir -p /etc/systemd/system/wechatbridge@bot2.service.d/
|
|
278
|
+
cat << 'EOF' | sudo tee /etc/systemd/system/wechatbridge@bot2.service.d/memory.conf
|
|
279
|
+
[Service]
|
|
280
|
+
# for hosts with >= 2 GB RAM
|
|
281
|
+
MemoryHigh=600M
|
|
282
|
+
MemoryMax=768M
|
|
283
|
+
EOF
|
|
284
|
+
sudo systemctl daemon-reload
|
|
285
|
+
sudo systemctl restart wechatbridge@bot2
|
|
286
|
+
```
|
|
287
|
+
Verify active limits:
|
|
288
|
+
```bash
|
|
289
|
+
systemctl show wechatbridge@bot2 -p MemoryHigh,MemoryMax
|
|
290
|
+
```
|
|
291
|
+
- **Sizing guidance:** The defaults are tuned for small hosts (~1 GB RAM). On larger hosts with ample headroom, limits can be relaxed. When deploying multiple instances, note that each instance runs in its own service cgroup — budget for the worst-case combined ceiling of N × MemoryMax across running instances.
|
|
292
|
+
- **Interaction with concurrency:** Concurrent heavy turns (e.g. multiple users sending images at once) multiply backend CLI memory consumption. On memory-constrained hosts, prioritize lowering `WECHATBRIDGE_MAX_CONCURRENT` (e.g. to `2` or `1`) before increasing memory caps.
|
|
293
|
+
- **Swap configuration:** Avoid tightening `MemorySwapMax` — swap margin serves as a vital safety buffer that slows execution under pressure instead of triggering an immediate cgroup OOM kill.
|
|
294
|
+
- **Single-instance deployments:** The single-instance unit `deploy/wechatbridge.service` includes the same built-in memory limits (`MemoryHigh=450M` and `MemoryMax=512M`). To adjust them, override via a drop-in under `/etc/systemd/system/wechatbridge.service.d/` and replace the unit name in the `systemctl` commands above with `wechatbridge`. Takes effect when the unit file is installed or re-installed; `update.sh` upgrades do not touch unit files.
|
|
295
|
+
- **Platform scope:** These cgroup memory limits apply only to Linux systemd deployments; the macOS (launchd) and Windows (Task Scheduler) paths have no equivalent per-instance cap.
|
|
296
|
+
|
|
264
297
|
### macOS (launchd)
|
|
265
298
|
|
|
266
299
|
```bash
|
|
@@ -302,7 +335,7 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
|
|
|
302
335
|
- **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
|
|
303
336
|
- **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
|
|
304
337
|
- **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
|
|
305
|
-
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
|
|
338
|
+
- **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap (heavy concurrent tasks multiply subprocess memory; see [Memory limits](#memory-limits)).
|
|
306
339
|
- **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
|
|
307
340
|
- **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
|
|
308
341
|
- **Retention:** session temps vs dialogue history use separate TTLs (`WECHATBRIDGE_SESSION_RETENTION_DAYS`, `WECHATBRIDGE_HISTORY_RETENTION_DAYS`). Prefs/auth are kept.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.2}/wechatbridge_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|