wechatbridge-cli 1.4.7__tar.gz → 1.4.8__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.
Files changed (23) hide show
  1. {wechatbridge_cli-1.4.7/wechatbridge_cli.egg-info → wechatbridge_cli-1.4.8}/PKG-INFO +1 -1
  2. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/tests/test_codex.py +28 -0
  3. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/__init__.py +1 -1
  4. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/codex.py +59 -23
  5. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8/wechatbridge_cli.egg-info}/PKG-INFO +1 -1
  6. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/LICENSE +0 -0
  7. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/README.md +0 -0
  8. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/pyproject.toml +0 -0
  9. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/setup.cfg +0 -0
  10. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/tests/test_hardening.py +0 -0
  11. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/__main__.py +0 -0
  12. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/agy.py +0 -0
  13. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/config.py +0 -0
  14. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/grok.py +0 -0
  15. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/ilink.py +0 -0
  16. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/main.py +0 -0
  17. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/runner_common.py +0 -0
  18. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge/update_check.py +0 -0
  19. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge_cli.egg-info/SOURCES.txt +0 -0
  20. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
  21. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
  22. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/wechatbridge_cli.egg-info/requires.txt +0 -0
  23. {wechatbridge_cli-1.4.7 → wechatbridge_cli-1.4.8}/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.7
3
+ Version: 1.4.8
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
@@ -935,6 +935,34 @@ class TestCodexRunFakeCli(unittest.IsolatedAsyncioTestCase):
935
935
  result_entries = [a for a in artifacts if a[0] == "result.txt"]
936
936
  self.assertEqual(len(result_entries), 1, "no duplicate from fallback")
937
937
 
938
+ def test_shell_pdf_merged_with_file_change(self):
939
+ """file_change for helper.py plus a shell-written report.pdf — both return."""
940
+ display, artifacts = self._run("hi", "u-shell-mix", mode="ok_shell_mixed_docs")
941
+ self.assertEqual(display, "done")
942
+ names = {n for n, _ in artifacts}
943
+ self.assertIn("helper.py", names)
944
+ self.assertIn("report.pdf", names)
945
+ self.assertEqual(len([a for a in artifacts if a[0] == "helper.py"]), 1)
946
+ self.assertEqual(len([a for a in artifacts if a[0] == "report.pdf"]), 1)
947
+
948
+ def test_shell_overwrite_existing_pdf(self):
949
+ """Rewriting a pre-existing pdf must still collect it."""
950
+ from wechatbridge.runner_common import sanitize_user_id
951
+
952
+ uid = "u-shell-ow"
953
+ sd = os.path.join(self.td, sanitize_user_id(uid))
954
+ os.makedirs(sd)
955
+ pdf = os.path.join(sd, "report.pdf")
956
+ with open(pdf, "wb") as f:
957
+ f.write(b"%PDF-old")
958
+ os.utime(pdf, (1_000_000, 1_000_000))
959
+ display, artifacts = self._run("hi", uid, mode="ok_shell_mixed_docs")
960
+ self.assertEqual(display, "done")
961
+ names = {n for n, _ in artifacts}
962
+ self.assertIn("report.pdf", names)
963
+ with open(pdf, "rb") as f:
964
+ self.assertEqual(f.read(), b"%PDF-1.4")
965
+
938
966
  def test_shell_file_fallback_old_files_not_collected(self):
939
967
  """ok_shell_old_files: files with old mtimes are NOT collected."""
940
968
  display, artifacts = self._run("hi", "u-shell-old", mode="ok_shell_old_files")
@@ -1,2 +1,2 @@
1
1
  """WeChatBridge — bridge WeChat messages to agy, Grok Build, or Codex CLIs."""
2
- __version__ = "1.4.7"
2
+ __version__ = "1.4.8"
@@ -633,26 +633,52 @@ def _extract_codex_error_messages(stdout_text: str) -> list:
633
633
 
634
634
 
635
635
  def _collect_fallback_artifacts(session_dir: str, snapshot_before: dict, t0: float) -> list:
636
- """Scan session_dir for new files created during codex execution.
636
+ """Scan session_dir for files created or overwritten during this turn.
637
637
 
638
- Only runs when: returncode==0, parse_failed==False, no structured artifacts.
639
- Scans only session_dir (never add_dirs). Excludes internal metadata dirs.
640
- Each candidate realpath is verified to be under session_dir.
638
+ Codex often emits a ``file_change`` for a helper script and then writes
639
+ the real pdf/docx via the shell. Always merge this list with structured
640
+ artifacts (dedupe by realpath). Scans only session_dir (never add_dirs).
641
+ Excludes internal metadata dirs. Each candidate is verified under session_dir.
641
642
  """
642
643
  _internal_names = {".codex_thread_id"}
643
644
  fallback = []
644
645
  fallback_seen = set()
645
646
  for fp, fmtime in _snapshot_regular_files([session_dir]).items():
646
- if fmtime >= t0 and fp not in snapshot_before:
647
- name = os.path.basename(fp)
648
- if name in _internal_names or name.startswith(".initialized."):
647
+ prev = snapshot_before.get(fp)
648
+ if prev is None:
649
+ if fmtime < t0:
649
650
  continue
650
- key = (name, fp)
651
- if key not in fallback_seen:
652
- fallback_seen.add(key)
653
- fallback.append(key)
651
+ elif fmtime <= prev:
652
+ continue
653
+ name = os.path.basename(fp)
654
+ if name in _internal_names or name.startswith(".initialized."):
655
+ continue
656
+ key = (name, fp)
657
+ if key not in fallback_seen:
658
+ fallback_seen.add(key)
659
+ fallback.append(key)
654
660
  return fallback
655
661
 
662
+
663
+ def _merge_codex_artifacts(*groups) -> list:
664
+ """Dedupe (name, path) tuples by realpath, preserve first-seen order."""
665
+ merged = []
666
+ seen = set()
667
+ for group in groups:
668
+ for item in group or []:
669
+ if not isinstance(item, (tuple, list)) or len(item) != 2:
670
+ continue
671
+ name, path = item
672
+ try:
673
+ key = os.path.realpath(path)
674
+ except OSError:
675
+ key = path
676
+ if key in seen:
677
+ continue
678
+ seen.add(key)
679
+ merged.append((name, path))
680
+ return merged
681
+
656
682
  async def run_codex(prompt: str, user_id: str, timeout: int = None) -> tuple:
657
683
  """Execute codex CLI for a given user message.
658
684
 
@@ -831,12 +857,18 @@ async def run_codex(prompt: str, user_id: str, timeout: int = None) -> tuple:
831
857
  mark_initialized(session_dir, backend="codex")
832
858
  if r_thread_id:
833
859
  _write_codex_thread_id(session_dir, r_thread_id)
834
- # Fallback: shell 产物扫描(仅重试成功且无结构化 artifacts 时触发)
835
- if not r_failed and not r_artifacts:
836
- fallback = _collect_fallback_artifacts(session_dir, snapshot_before, t0)
837
- if fallback:
838
- r_artifacts = fallback
839
- logger.info("Fallback scan collected %d additional files on retry", len(r_artifacts))
860
+ # Fallback: 重试成功也扫本轮新建/覆盖的文件,与 file_change 合并
861
+ if not r_failed:
862
+ extra = _collect_fallback_artifacts(session_dir, snapshot_before, t0)
863
+ if extra:
864
+ before = len(r_artifacts)
865
+ r_artifacts = _merge_codex_artifacts(r_artifacts, extra)
866
+ added = len(r_artifacts) - before
867
+ if added:
868
+ logger.info(
869
+ "Fallback scan collected %d additional files on retry",
870
+ added,
871
+ )
840
872
  elapsed = time.time() - t0
841
873
  logger.info(
842
874
  "codex retry done: user=%s elapsed=%.1fs artifacts=%d output=%d chars failed=%s",
@@ -850,12 +882,16 @@ async def run_codex(prompt: str, user_id: str, timeout: int = None) -> tuple:
850
882
  if parsed_thread_id:
851
883
  _write_codex_thread_id(session_dir, parsed_thread_id)
852
884
 
853
- # Fallback: shell 产物扫描(仅在成功且无结构化 artifacts 时触发)
854
- if not failed and not artifacts:
855
- fallback = _collect_fallback_artifacts(session_dir, snapshot_before, t0)
856
- if fallback:
857
- artifacts = fallback
858
- logger.info("Fallback scan collected %d additional files", len(artifacts))
885
+ # Fallback: 成功就扫本轮新建/覆盖的文件,与 file_change 合并去重。
886
+ # 不能再要求 artifacts 为空——先写出脚本再终端生成 pdf 时会漏文档。
887
+ if not failed:
888
+ extra = _collect_fallback_artifacts(session_dir, snapshot_before, t0)
889
+ if extra:
890
+ before = len(artifacts)
891
+ artifacts = _merge_codex_artifacts(artifacts, extra)
892
+ added = len(artifacts) - before
893
+ if added:
894
+ logger.info("Fallback scan collected %d additional files", added)
859
895
 
860
896
  elapsed = time.time() - t0
861
897
  logger.info(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wechatbridge-cli
3
- Version: 1.4.7
3
+ Version: 1.4.8
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