wechatbridge-cli 1.4.1__tar.gz → 1.4.3__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.4.1/wechatbridge_cli.egg-info → wechatbridge_cli-1.4.3}/PKG-INFO +3 -3
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/README.md +2 -2
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/tests/test_codex.py +135 -6
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/tests/test_hardening.py +823 -1
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/__init__.py +1 -1
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/agy.py +10 -5
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/codex.py +242 -22
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/config.py +47 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/grok.py +13 -8
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/main.py +280 -12
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/runner_common.py +196 -11
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3/wechatbridge_cli.egg-info}/PKG-INFO +3 -3
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/LICENSE +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/pyproject.toml +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/setup.cfg +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/__main__.py +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/ilink.py +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge/update_check.py +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge_cli.egg-info/SOURCES.txt +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/wechatbridge_cli.egg-info/requires.txt +0 -0
- {wechatbridge_cli-1.4.1 → wechatbridge_cli-1.4.3}/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.4.
|
|
3
|
+
Version: 1.4.3
|
|
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
|
|
@@ -250,8 +250,8 @@ See [`deploy/wechatbridge-windows.md`](deploy/wechatbridge-windows.md).
|
|
|
250
250
|
| `/help` | list supported commands for the active backend |
|
|
251
251
|
| `/backend <agy\|grok\|codex>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag and codex `thread_id`/resume — so the next turn starts a fresh session; history files may remain until retention cleanup) |
|
|
252
252
|
| `/clear` or `/new` | drop continue flag so the next CLI turn is a new conversation (does not instantly delete history files) |
|
|
253
|
-
| `/model <name>` | set model (
|
|
254
|
-
| `/models` | list models — agy/grok query the live CLI
|
|
253
|
+
| `/model <name>` | set model (all backends validate against a live list: agy/grok via CLI `models`; codex via `codex debug models` [then `--bundled`]; unknown name or list-fetch failure refuse and do not write prefs; see `/models`) |
|
|
254
|
+
| `/models` | list models — agy/grok/codex all query the live CLI (codex: `debug models`; falls back to a built-in reference note only if the live list cannot be fetched) |
|
|
255
255
|
| `/fast` | set low reasoning effort (**on only** — not a toggle; no “off” command) |
|
|
256
256
|
| `/planning` | set planning mode (**on only** — not a toggle) |
|
|
257
257
|
| `/add-dir <path>` | **agy:** pass `--add-dir` on later runs if path is allowed. **grok:** recorded only; not passed to the CLI yet |
|
|
@@ -219,8 +219,8 @@ See [`deploy/wechatbridge-windows.md`](deploy/wechatbridge-windows.md).
|
|
|
219
219
|
| `/help` | list supported commands for the active backend |
|
|
220
220
|
| `/backend <agy\|grok\|codex>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag and codex `thread_id`/resume — so the next turn starts a fresh session; history files may remain until retention cleanup) |
|
|
221
221
|
| `/clear` or `/new` | drop continue flag so the next CLI turn is a new conversation (does not instantly delete history files) |
|
|
222
|
-
| `/model <name>` | set model (
|
|
223
|
-
| `/models` | list models — agy/grok query the live CLI
|
|
222
|
+
| `/model <name>` | set model (all backends validate against a live list: agy/grok via CLI `models`; codex via `codex debug models` [then `--bundled`]; unknown name or list-fetch failure refuse and do not write prefs; see `/models`) |
|
|
223
|
+
| `/models` | list models — agy/grok/codex all query the live CLI (codex: `debug models`; falls back to a built-in reference note only if the live list cannot be fetched) |
|
|
224
224
|
| `/fast` | set low reasoning effort (**on only** — not a toggle; no “off” command) |
|
|
225
225
|
| `/planning` | set planning mode (**on only** — not a toggle) |
|
|
226
226
|
| `/add-dir <path>` | **agy:** pass `--add-dir` on later runs if path is allowed. **grok:** recorded only; not passed to the CLI yet |
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import os
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
import time
|
|
10
11
|
import unittest
|
|
11
12
|
from unittest import mock
|
|
13
|
+
from unittest.mock import AsyncMock
|
|
12
14
|
|
|
13
15
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
14
16
|
_FIXTURES = os.path.join(_HERE, "fixtures")
|
|
@@ -519,24 +521,149 @@ class TestCodexSlash(unittest.IsolatedAsyncioTestCase):
|
|
|
519
521
|
self.assertEqual(_read_codex_thread_id(sd), "")
|
|
520
522
|
self.assertFalse(os.path.exists(os.path.join(sd, ".initialized.codex")))
|
|
521
523
|
|
|
522
|
-
async def
|
|
524
|
+
async def test_model_validated_stores(self):
|
|
525
|
+
"""Known model in live list → switch succeeds and prefs are written."""
|
|
523
526
|
from wechatbridge.codex import handle_codex_slash_command
|
|
524
527
|
from wechatbridge.runner_common import load_prefs
|
|
525
|
-
|
|
528
|
+
from wechatbridge import codex as codex_mod
|
|
529
|
+
|
|
530
|
+
with mock.patch.object(
|
|
531
|
+
codex_mod,
|
|
532
|
+
"_fetch_codex_model_list",
|
|
533
|
+
new=AsyncMock(return_value=["gpt-5.1-codex", "gpt-5.1", "gpt-5"]),
|
|
534
|
+
):
|
|
535
|
+
reply = await handle_codex_slash_command("/model gpt-5.1-codex", "u-model")
|
|
526
536
|
self.assertIn("模型已切换", reply)
|
|
527
|
-
self.
|
|
537
|
+
self.assertNotIn("未校验", reply)
|
|
528
538
|
self.assertEqual(load_prefs("u-model")["model"], "gpt-5.1-codex")
|
|
529
539
|
|
|
540
|
+
async def test_model_unknown_rejects(self):
|
|
541
|
+
"""Unknown model → refuse and prefs unchanged."""
|
|
542
|
+
from wechatbridge.codex import handle_codex_slash_command
|
|
543
|
+
from wechatbridge.runner_common import load_prefs, save_prefs
|
|
544
|
+
from wechatbridge import codex as codex_mod
|
|
545
|
+
|
|
546
|
+
uid = "u-model-unknown"
|
|
547
|
+
save_prefs(uid, {"model": "gpt-5.1", "effort": "", "mode": ""})
|
|
548
|
+
with mock.patch.object(
|
|
549
|
+
codex_mod,
|
|
550
|
+
"_fetch_codex_model_list",
|
|
551
|
+
new=AsyncMock(return_value=["gpt-5.1-codex", "gpt-5.1"]),
|
|
552
|
+
):
|
|
553
|
+
reply = await handle_codex_slash_command("/model not-a-real-model", uid)
|
|
554
|
+
self.assertIn("模型不存在", reply)
|
|
555
|
+
self.assertNotIn("模型已切换", reply)
|
|
556
|
+
self.assertEqual(load_prefs(uid)["model"], "gpt-5.1")
|
|
557
|
+
|
|
558
|
+
async def test_model_list_fail_rejects(self):
|
|
559
|
+
"""List fetch failure → refuse and do not write prefs (strict)."""
|
|
560
|
+
from wechatbridge.codex import handle_codex_slash_command
|
|
561
|
+
from wechatbridge.runner_common import load_prefs, save_prefs
|
|
562
|
+
from wechatbridge import codex as codex_mod
|
|
563
|
+
|
|
564
|
+
uid = "u-model-listfail"
|
|
565
|
+
save_prefs(uid, {"model": "gpt-5", "effort": "", "mode": ""})
|
|
566
|
+
with mock.patch.object(
|
|
567
|
+
codex_mod,
|
|
568
|
+
"_fetch_codex_model_list",
|
|
569
|
+
new=AsyncMock(return_value=None),
|
|
570
|
+
):
|
|
571
|
+
reply = await handle_codex_slash_command("/model gpt-5.1-codex", uid)
|
|
572
|
+
self.assertIn("无法获取模型列表", reply)
|
|
573
|
+
self.assertNotIn("模型已切换", reply)
|
|
574
|
+
self.assertEqual(load_prefs(uid)["model"], "gpt-5")
|
|
575
|
+
|
|
576
|
+
async def test_model_prefix_match(self):
|
|
577
|
+
"""Prefix match takes the first hit (same as agy)."""
|
|
578
|
+
from wechatbridge.codex import handle_codex_slash_command
|
|
579
|
+
from wechatbridge.runner_common import load_prefs
|
|
580
|
+
from wechatbridge import codex as codex_mod
|
|
581
|
+
|
|
582
|
+
with mock.patch.object(
|
|
583
|
+
codex_mod,
|
|
584
|
+
"_fetch_codex_model_list",
|
|
585
|
+
new=AsyncMock(return_value=["gpt-5.1-codex", "gpt-5.1", "gpt-5"]),
|
|
586
|
+
):
|
|
587
|
+
reply = await handle_codex_slash_command("/model gpt-5.1", "u-model-prefix")
|
|
588
|
+
# exact "gpt-5.1" is in list → exact match wins over longer prefixes
|
|
589
|
+
self.assertIn("模型已切换", reply)
|
|
590
|
+
self.assertEqual(load_prefs("u-model-prefix")["model"], "gpt-5.1")
|
|
591
|
+
|
|
592
|
+
with mock.patch.object(
|
|
593
|
+
codex_mod,
|
|
594
|
+
"_fetch_codex_model_list",
|
|
595
|
+
new=AsyncMock(return_value=["gpt-5.1-codex", "gpt-5-codex"]),
|
|
596
|
+
):
|
|
597
|
+
reply2 = await handle_codex_slash_command("/model gpt-5.1", "u-model-prefix2")
|
|
598
|
+
self.assertIn("模型已切换", reply2)
|
|
599
|
+
self.assertEqual(load_prefs("u-model-prefix2")["model"], "gpt-5.1-codex")
|
|
600
|
+
|
|
530
601
|
async def test_model_empty_shows_current(self):
|
|
531
602
|
from wechatbridge.codex import handle_codex_slash_command
|
|
603
|
+
from wechatbridge.runner_common import save_prefs
|
|
604
|
+
|
|
605
|
+
save_prefs("u-model-empty", {"model": "gpt-5.1-codex", "effort": "", "mode": ""})
|
|
532
606
|
reply = await handle_codex_slash_command("/model", "u-model-empty")
|
|
533
607
|
self.assertIn("当前模型", reply)
|
|
608
|
+
self.assertIn("gpt-5.1-codex", reply)
|
|
609
|
+
self.assertNotIn("不会校验", reply)
|
|
610
|
+
self.assertNotIn("未校验", reply)
|
|
611
|
+
|
|
612
|
+
async def test_models_live_list(self):
|
|
613
|
+
"""/models under mock returns the live list."""
|
|
614
|
+
from wechatbridge.codex import handle_codex_slash_command
|
|
615
|
+
from wechatbridge import codex as codex_mod
|
|
616
|
+
|
|
617
|
+
with mock.patch.object(
|
|
618
|
+
codex_mod,
|
|
619
|
+
"_fetch_codex_model_list",
|
|
620
|
+
new=AsyncMock(return_value=["gpt-5.1-codex", "gpt-5.1"]),
|
|
621
|
+
):
|
|
622
|
+
reply = await handle_codex_slash_command("/models", "u-models")
|
|
623
|
+
self.assertIn("gpt-5.1-codex", reply)
|
|
624
|
+
self.assertIn("gpt-5.1", reply)
|
|
625
|
+
self.assertIn("debug models", reply)
|
|
534
626
|
|
|
535
|
-
async def
|
|
627
|
+
async def test_models_fallback_builtin(self):
|
|
628
|
+
"""/models falls back to built-in note when live list fails."""
|
|
536
629
|
from wechatbridge.codex import handle_codex_slash_command
|
|
537
|
-
|
|
630
|
+
from wechatbridge import codex as codex_mod
|
|
631
|
+
|
|
632
|
+
with mock.patch.object(
|
|
633
|
+
codex_mod,
|
|
634
|
+
"_fetch_codex_model_list",
|
|
635
|
+
new=AsyncMock(return_value=None),
|
|
636
|
+
):
|
|
637
|
+
reply = await handle_codex_slash_command("/models", "u-models-fb")
|
|
638
|
+
self.assertIn("参考", reply)
|
|
538
639
|
self.assertIn("gpt-5.1-codex", reply)
|
|
539
640
|
|
|
641
|
+
async def test_parse_codex_models_json_and_lines(self):
|
|
642
|
+
from wechatbridge.codex import _parse_codex_models
|
|
643
|
+
|
|
644
|
+
j = json.dumps({
|
|
645
|
+
"models": [
|
|
646
|
+
{"slug": "gpt-5.1-codex"},
|
|
647
|
+
{"slug": "gpt-5"},
|
|
648
|
+
]
|
|
649
|
+
})
|
|
650
|
+
self.assertEqual(_parse_codex_models(j), ["gpt-5.1-codex", "gpt-5"])
|
|
651
|
+
|
|
652
|
+
nested = json.dumps({
|
|
653
|
+
"models": [{"slug": "a"}, {"id": "b", "name": "ignored-without-slug-only"}]
|
|
654
|
+
})
|
|
655
|
+
# models[] item with slug + item with id (no slug) both accepted
|
|
656
|
+
self.assertEqual(_parse_codex_models(nested), ["a", "b"])
|
|
657
|
+
|
|
658
|
+
# bare string list
|
|
659
|
+
self.assertEqual(_parse_codex_models(json.dumps(["m1", "m2"])), ["m1", "m2"])
|
|
660
|
+
|
|
661
|
+
lines = "* gpt-5.1-codex (default)\n- gpt-5\n"
|
|
662
|
+
self.assertEqual(_parse_codex_models(lines), ["gpt-5.1-codex", "gpt-5"])
|
|
663
|
+
|
|
664
|
+
self.assertEqual(_parse_codex_models(""), [])
|
|
665
|
+
self.assertEqual(_parse_codex_models("{}"), [])
|
|
666
|
+
|
|
540
667
|
async def test_fast_and_planning(self):
|
|
541
668
|
from wechatbridge.codex import handle_codex_slash_command
|
|
542
669
|
from wechatbridge.runner_common import load_prefs
|
|
@@ -684,9 +811,11 @@ class TestCodexRunFakeCli(unittest.IsolatedAsyncioTestCase):
|
|
|
684
811
|
# 续轮:thread.started 正常输出,但随后 turn.failed("rate limit exceeded"),
|
|
685
812
|
# returncode=1。这是普通限流错误,不是 resume/session 错误,不得 fallback,
|
|
686
813
|
# 旧 thread_id 必须保留,且只运行一次(无重试)。
|
|
814
|
+
# 限流最终用户文案用 🔔(不是 ❌)。
|
|
687
815
|
log = os.path.join(self.td, "inv-turn-fail.log")
|
|
688
816
|
display, _ = self._run("again", "u-turn-fail", mode="resume_turn_fail", log_path=log)
|
|
689
|
-
self.assertTrue(display.startswith("
|
|
817
|
+
self.assertTrue(display.startswith("🔔"), msg=display[:80])
|
|
818
|
+
self.assertIn("**请求较多**", display)
|
|
690
819
|
# 旧 thread_id 保留
|
|
691
820
|
self.assertEqual(_read_tid(sd), old_tid)
|
|
692
821
|
# 只运行一次(无降级重试)
|