wechatbridge-cli 1.6.0__tar.gz → 1.6.1__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.1}/PKG-INFO +1 -1
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/tests/test_hardening.py +186 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/__init__.py +1 -1
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/agy.py +38 -11
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/runner_common.py +24 -6
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/PKG-INFO +1 -1
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/LICENSE +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/README.md +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/pyproject.toml +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/setup.cfg +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/tests/test_codex.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/tests/test_codex_auth.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/tests/test_dsh.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/tests/test_grok_auth.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/__main__.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/codex.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/config.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/dsh.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/grok.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/ilink.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/main.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge/update_check.py +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/SOURCES.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/requires.txt +0 -0
- {wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/top_level.txt +0 -0
|
@@ -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.1"
|
|
@@ -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
|
"模型响应超时",
|
|
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
|
|
File without changes
|
{wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{wechatbridge_cli-1.6.0 → wechatbridge_cli-1.6.1}/wechatbridge_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|