From 8892f43b7c5a0902386bdd74baeeadb5903ed474 Mon Sep 17 00:00:00 2001 From: AlexFucuson9 Date: Thu, 16 Jul 2026 17:10:45 +0700 Subject: [PATCH] fix: add encoding='utf-8' to read_text/write_text in plugins/ Path.read_text() and .write_text() without encoding default to the system locale (cp1252 on Windows), silently corrupting non-ASCII content. The project's PLW1514 ruff rule only catches open() calls, not Path API variants. Fix 35 call sites across 13 plugin files covering: - Platform adapters: discord, telegram, feishu, whatsapp, google_chat - Memory plugins: mem0, hindsight, honcho - Utility plugins: disk-cleanup, google_meet, hermes-achievements --- plugins/disk-cleanup/disk_cleanup.py | 6 +++--- plugins/google_meet/realtime/openai_client.py | 7 ++++--- plugins/hermes-achievements/dashboard/plugin_api.py | 12 ++++++------ plugins/memory/hindsight/__init__.py | 8 ++++---- plugins/memory/honcho/__init__.py | 2 +- plugins/memory/mem0/__init__.py | 2 +- plugins/memory/mem0/_setup.py | 12 ++++++------ plugins/platforms/discord/adapter.py | 2 +- plugins/platforms/feishu/adapter.py | 2 +- plugins/platforms/google_chat/adapter.py | 4 ++-- plugins/platforms/google_chat/oauth.py | 4 ++-- plugins/platforms/telegram/adapter.py | 2 +- plugins/platforms/whatsapp/adapter.py | 8 ++++---- 13 files changed, 36 insertions(+), 35 deletions(-) diff --git a/plugins/disk-cleanup/disk_cleanup.py b/plugins/disk-cleanup/disk_cleanup.py index 1e1d453f80a4d..31f5779b1ff1e 100755 --- a/plugins/disk-cleanup/disk_cleanup.py +++ b/plugins/disk-cleanup/disk_cleanup.py @@ -110,12 +110,12 @@ def load_tracked() -> List[Dict[str, Any]]: return [] try: - return json.loads(tf.read_text()) + return json.loads(tf.read_text(encoding="utf-8")) except (json.JSONDecodeError, ValueError): bak = tf.with_suffix(".json.bak") if bak.exists(): try: - data = json.loads(bak.read_text()) + data = json.loads(bak.read_text(encoding="utf-8")) _log("WARN: tracked.json corrupted — restored from .bak") return data except Exception: @@ -129,7 +129,7 @@ def save_tracked(tracked: List[Dict[str, Any]]) -> None: tf = get_tracked_file() tf.parent.mkdir(parents=True, exist_ok=True) tmp = tf.with_suffix(".json.tmp") - tmp.write_text(json.dumps(tracked, indent=2)) + tmp.write_text(json.dumps(tracked, indent=2), encoding="utf-8") if tf.exists(): shutil.copy2(tf, tf.with_suffix(".json.bak")) tmp.replace(tf) diff --git a/plugins/google_meet/realtime/openai_client.py b/plugins/google_meet/realtime/openai_client.py index 24527603e5249..9facc601b8634 100644 --- a/plugins/google_meet/realtime/openai_client.py +++ b/plugins/google_meet/realtime/openai_client.py @@ -262,7 +262,7 @@ def _read_queue(self) -> list[dict]: if not self.queue_path.exists(): return [] out: list[dict] = [] - for line in self.queue_path.read_text().splitlines(): + for line in self.queue_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line: continue @@ -281,10 +281,11 @@ def _rewrite_queue(self, remaining: list[dict]) -> None: if not remaining: # Keep the file but empty — consumers may be watching for # new writes via mtime, and delete-then-recreate is a race. - self.queue_path.write_text("") + self.queue_path.write_text("", encoding="utf-8") return self.queue_path.write_text( - "\n".join(json.dumps(e) for e in remaining) + "\n" + "\n".join(json.dumps(e) for e in remaining) + "\n", + encoding="utf-8", ) def _append_processed(self, entry: dict, result: dict) -> None: diff --git a/plugins/hermes-achievements/dashboard/plugin_api.py b/plugins/hermes-achievements/dashboard/plugin_api.py index b419efc6c27ff..c2f69a22999bc 100644 --- a/plugins/hermes-achievements/dashboard/plugin_api.py +++ b/plugins/hermes-achievements/dashboard/plugin_api.py @@ -159,7 +159,7 @@ def load_state() -> Dict[str, Any]: if not path.exists(): return {"unlocks": {}} try: - return json.loads(path.read_text()) + return json.loads(path.read_text(encoding="utf-8")) except Exception: return {"unlocks": {}} @@ -167,7 +167,7 @@ def load_state() -> Dict[str, Any]: def save_state(state: Dict[str, Any]) -> None: path = state_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(state, indent=2, sort_keys=True)) + path.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8") def _json_safe(value: Any) -> Any: @@ -185,7 +185,7 @@ def load_snapshot() -> Optional[Dict[str, Any]]: if not path.exists(): return None try: - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): return data except Exception: @@ -196,7 +196,7 @@ def load_snapshot() -> Optional[Dict[str, Any]]: def save_snapshot(data: Dict[str, Any]) -> None: path = snapshot_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True)) + path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True), encoding="utf-8") def load_checkpoint() -> Dict[str, Any]: @@ -204,7 +204,7 @@ def load_checkpoint() -> Dict[str, Any]: if not path.exists(): return {"schema_version": 1, "generated_at": 0, "sessions": {}} try: - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): data.setdefault("schema_version", 1) data.setdefault("generated_at", 0) @@ -219,7 +219,7 @@ def load_checkpoint() -> Dict[str, Any]: def save_checkpoint(data: Dict[str, Any]) -> None: path = checkpoint_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True)) + path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True), encoding="utf-8") def session_fingerprint(meta: Dict[str, Any]) -> Dict[str, Any]: diff --git a/plugins/memory/hindsight/__init__.py b/plugins/memory/hindsight/__init__.py index 9f5974b7b5428..1c284f218b583 100644 --- a/plugins/memory/hindsight/__init__.py +++ b/plugins/memory/hindsight/__init__.py @@ -746,7 +746,7 @@ def save_config(self, values, hermes_home): existing = {} if config_path.exists(): try: - existing = json.loads(config_path.read_text()) + existing = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass existing.update(values) @@ -895,7 +895,7 @@ def post_setup(self, hermes_home: str, config: dict) -> None: env_path = Path(hermes_home) / ".env" existing_llm_key = "" if env_path.exists(): - for line in env_path.read_text().splitlines(): + for line in env_path.read_text(encoding="utf-8").splitlines(): if line.startswith("HINDSIGHT_LLM_API_KEY="): existing_llm_key = line.split("=", 1)[1] break @@ -925,7 +925,7 @@ def post_setup(self, hermes_home: str, config: dict) -> None: env_path.parent.mkdir(parents=True, exist_ok=True) existing_lines = [] if env_path.exists(): - existing_lines = env_path.read_text().splitlines() + existing_lines = env_path.read_text(encoding="utf-8").splitlines() updated_keys = set() new_lines = [] for line in existing_lines: @@ -938,7 +938,7 @@ def post_setup(self, hermes_home: str, config: dict) -> None: for k, v in env_writes.items(): if k not in updated_keys: new_lines.append(f"{k}={v}") - env_path.write_text("\n".join(new_lines) + "\n") + env_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8") if mode == "local_embedded": materialized_config = dict(provider_config) diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index c9ddc41bc8988..58d5a534f4c92 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -271,7 +271,7 @@ def save_config(self, values, hermes_home): existing = {} if config_path.exists(): try: - existing = json.loads(config_path.read_text()) + existing = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass existing.update(values) diff --git a/plugins/memory/mem0/__init__.py b/plugins/memory/mem0/__init__.py index 35f413ed37a72..b060a7bf7a8aa 100644 --- a/plugins/memory/mem0/__init__.py +++ b/plugins/memory/mem0/__init__.py @@ -240,7 +240,7 @@ def save_config(self, values, hermes_home): existing = {} if config_path.exists(): try: - existing = json.loads(config_path.read_text()) + existing = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass existing.update(values) diff --git a/plugins/memory/mem0/_setup.py b/plugins/memory/mem0/_setup.py index a331ef3a80ea4..0b4f8e2c18c4f 100644 --- a/plugins/memory/mem0/_setup.py +++ b/plugins/memory/mem0/_setup.py @@ -191,7 +191,7 @@ def _write_env(env_path: Path, env_writes: dict[str, str]) -> None: env_path.parent.mkdir(parents=True, exist_ok=True) existing_lines: list[str] = [] if env_path.exists(): - existing_lines = env_path.read_text().splitlines() + existing_lines = env_path.read_text(encoding="utf-8").splitlines() updated_keys: set[str] = set() new_lines: list[str] = [] @@ -206,7 +206,7 @@ def _write_env(env_path: Path, env_writes: dict[str, str]) -> None: if k not in updated_keys: new_lines.append(f"{k}={v}") - env_path.write_text("\n".join(new_lines) + "\n") + env_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8") def _save_mem0_json(hermes_home: str, data: dict) -> None: @@ -219,7 +219,7 @@ def _save_mem0_json(hermes_home: str, data: dict) -> None: except Exception: pass existing.update(data) - config_path.write_text(json.dumps(existing, indent=2) + "\n") + config_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8") def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> None: @@ -239,7 +239,7 @@ def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> No config_path = Path(hermes_home) / "mem0.json" if config_path.exists(): try: - existing_config = json.loads(config_path.read_text()) + existing_config = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass @@ -360,7 +360,7 @@ def _setup_selfhosted(hermes_home: str, config: dict, flags: dict[str, str]) -> config_path = Path(hermes_home) / "mem0.json" if config_path.exists(): try: - existing_config = json.loads(config_path.read_text()) + existing_config = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass @@ -491,7 +491,7 @@ def _prompt_api_key(label: str, env_var: str, hermes_home: str) -> str: if not existing: env_path = Path(hermes_home) / ".env" if env_path.exists(): - for line in env_path.read_text().splitlines(): + for line in env_path.read_text(encoding="utf-8").splitlines(): if line.startswith(f"{env_var}="): existing = line.split("=", 1)[1].strip() break diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index a97aef0677108..5a5403671a8cb 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -7161,7 +7161,7 @@ async def _respond( home = get_hermes_home() response_path = home / ".update_response" tmp = response_path.with_suffix(".tmp") - tmp.write_text(answer) + tmp.write_text(answer, encoding="utf-8") tmp.replace(response_path) logger.info( "Discord update prompt answered '%s' by %s", diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index 41e087069e85e..ceecd4910e3a9 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -2156,7 +2156,7 @@ def _build_resolved_update_prompt_card(*, answer: str, user_name: str) -> Dict[s def _write_update_prompt_response(answer: str) -> None: response_path = get_hermes_home() / ".update_response" tmp_path = response_path.with_suffix(".tmp") - tmp_path.write_text(answer) + tmp_path.write_text(answer, encoding="utf-8") tmp_path.replace(response_path) async def send_voice( diff --git a/plugins/platforms/google_chat/adapter.py b/plugins/platforms/google_chat/adapter.py index f63efeabebdee..9c50bfc0271c2 100644 --- a/plugins/platforms/google_chat/adapter.py +++ b/plugins/platforms/google_chat/adapter.py @@ -362,7 +362,7 @@ def load(self) -> None: self._counts = {} return try: - raw = self._path.read_text() + raw = self._path.read_text(encoding="utf-8") data = json.loads(raw) if raw.strip() else {} except json.JSONDecodeError as exc: logger.warning( @@ -417,7 +417,7 @@ def _save(self) -> None: try: self._path.parent.mkdir(parents=True, exist_ok=True) tmp = self._path.with_suffix(self._path.suffix + ".tmp") - tmp.write_text(json.dumps(self._counts, separators=(",", ":"))) + tmp.write_text(json.dumps(self._counts, separators=(",", ":")), encoding="utf-8") os.replace(tmp, self._path) except OSError as exc: logger.warning( diff --git a/plugins/platforms/google_chat/oauth.py b/plugins/platforms/google_chat/oauth.py index 277b2396f0c51..2c40ef752623a 100644 --- a/plugins/platforms/google_chat/oauth.py +++ b/plugins/platforms/google_chat/oauth.py @@ -420,7 +420,7 @@ def store_client_secret(path: str) -> None: sys.exit(1) try: - data = json.loads(src.read_text()) + data = json.loads(src.read_text(encoding="utf-8")) except json.JSONDecodeError: print("ERROR: File is not valid JSON.") sys.exit(1) @@ -460,7 +460,7 @@ def _load_pending_auth(email: Optional[str] = None) -> dict: print("ERROR: No pending OAuth session found. Run --auth-url first.") sys.exit(1) try: - data = json.loads(pending.read_text()) + data = json.loads(pending.read_text(encoding="utf-8")) except Exception as exc: print(f"ERROR: Could not read pending OAuth session: {exc}") print("Run --auth-url again to start a fresh session.") diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 75416d3e21d9c..a911145f0e039 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -5971,7 +5971,7 @@ async def _handle_callback_query( home = get_hermes_home() response_path = home / ".update_response" tmp = response_path.with_suffix(".tmp") - tmp.write_text(answer) + tmp.write_text(answer, encoding="utf-8") tmp.replace(response_path) logger.info("Telegram update prompt answered '%s' by user %s", answer, getattr(query.from_user, "id", "unknown")) diff --git a/plugins/platforms/whatsapp/adapter.py b/plugins/platforms/whatsapp/adapter.py index 7cf94b7c1e637..ed6612c3df2a1 100644 --- a/plugins/platforms/whatsapp/adapter.py +++ b/plugins/platforms/whatsapp/adapter.py @@ -167,7 +167,7 @@ def _kill_stale_bridge_by_pidfile(session_path: Path) -> None: try: # Format: line 1 = pid, optional line 2 = kernel start time. Legacy # files written before the guard existed have only the pid. - lines = pid_file.read_text().split("\n") + lines = pid_file.read_text(encoding="utf-8").split("\n") pid = int(lines[0].strip()) if len(lines) > 1 and lines[1].strip(): recorded_start = int(lines[1].strip()) @@ -208,7 +208,7 @@ def _write_bridge_pidfile(session_path: Path, pid: int) -> None: from gateway.status import get_process_start_time start = get_process_start_time(pid) text = str(pid) if start is None else "{}\n{}".format(pid, start) - (session_path / "bridge.pid").write_text(text) + (session_path / "bridge.pid").write_text(text, encoding="utf-8") except OSError: pass @@ -531,7 +531,7 @@ async def connect(self, *, is_reconnect: bool = False) -> bool: _deps_fresh = False if (bridge_dir / "node_modules").exists(): try: - _deps_fresh = (_dep_stamp.read_text().strip() == _pkg_hash) and bool(_pkg_hash) + _deps_fresh = (_dep_stamp.read_text(encoding="utf-8").strip() == _pkg_hash) and bool(_pkg_hash) except OSError: _deps_fresh = False if not _deps_fresh: @@ -557,7 +557,7 @@ async def connect(self, *, is_reconnect: bool = False) -> bool: print(f"[{self.name}] Dependencies installed") if _pkg_hash: try: - _dep_stamp.write_text(_pkg_hash) + _dep_stamp.write_text(_pkg_hash, encoding="utf-8") except OSError: pass # Stamp is an optimization; install still succeeded except Exception as e: