mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-25 17:18:11 +00:00
fix(gateway): pass encoding="utf-8" to read_text/write_text in update path (#37423)
This commit is contained in:
parent
cbb1457606
commit
5b76ce169b
9 changed files with 81 additions and 30 deletions
|
|
@ -429,7 +429,7 @@ class DeliveryRouter:
|
|||
lines.append("")
|
||||
lines.append(content)
|
||||
|
||||
output_path.write_text("\n".join(lines))
|
||||
output_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
return {
|
||||
"path": str(output_path),
|
||||
|
|
@ -442,7 +442,7 @@ class DeliveryRouter:
|
|||
out_dir = get_hermes_home() / "cron" / "output"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = out_dir / f"{job_id}_{timestamp}.txt"
|
||||
path.write_text(content)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def _filter_silence_narration_enabled(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -1202,7 +1202,7 @@ class QQAdapter(BasePlatformAdapter):
|
|||
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(
|
||||
"QQ update prompt answered %r by %s",
|
||||
|
|
|
|||
|
|
@ -3778,7 +3778,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
|
||||
def _load_voice_modes(self) -> Dict[str, str]:
|
||||
try:
|
||||
data = json.loads(self._VOICE_MODE_PATH.read_text())
|
||||
data = json.loads(self._VOICE_MODE_PATH.read_text(encoding="utf-8"))
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
|
||||
|
|
@ -3806,7 +3806,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
try:
|
||||
self._VOICE_MODE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._VOICE_MODE_PATH.write_text(
|
||||
json.dumps(self._voice_mode, indent=2)
|
||||
json.dumps(self._voice_mode, indent=2), encoding="utf-8"
|
||||
)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to save voice modes: %s", e)
|
||||
|
|
@ -6956,7 +6956,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
|
||||
path = _hermes_home / self._STUCK_LOOP_FILE
|
||||
try:
|
||||
counts = json.loads(path.read_text()) if path.exists() else {}
|
||||
counts = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
||||
except Exception:
|
||||
counts = {}
|
||||
|
||||
|
|
@ -6986,7 +6986,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return 0
|
||||
|
||||
try:
|
||||
counts = json.loads(path.read_text())
|
||||
counts = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
|
@ -7032,7 +7032,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if not path.exists():
|
||||
return
|
||||
try:
|
||||
counts = json.loads(path.read_text())
|
||||
counts = json.loads(path.read_text(encoding="utf-8"))
|
||||
if session_key in counts:
|
||||
del counts[session_key]
|
||||
if counts:
|
||||
|
|
@ -10774,7 +10774,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
prompt_path = _hermes_home / ".update_prompt.json"
|
||||
try:
|
||||
tmp = response_path.with_suffix(".tmp")
|
||||
tmp.write_text(response_text)
|
||||
tmp.write_text(response_text, encoding="utf-8")
|
||||
tmp.replace(response_path)
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
except OSError as e:
|
||||
|
|
@ -10794,7 +10794,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
prompt_path = _hermes_home / ".update_prompt.json"
|
||||
try:
|
||||
tmp = response_path.with_suffix(".tmp")
|
||||
tmp.write_text("")
|
||||
tmp.write_text("", encoding="utf-8")
|
||||
tmp.replace(response_path)
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
logger.info(
|
||||
|
|
@ -14743,7 +14743,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
self._booted_from_restart = False
|
||||
return True
|
||||
return False
|
||||
data = json.loads(marker_path.read_text())
|
||||
data = json.loads(marker_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
|
@ -16697,7 +16697,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
for path in (claimed_path, pending_path):
|
||||
if path.exists():
|
||||
try:
|
||||
pending = json.loads(path.read_text())
|
||||
pending = json.loads(path.read_text(encoding="utf-8"))
|
||||
platform_str = pending.get("platform")
|
||||
chat_id = pending.get("chat_id")
|
||||
chat_type = pending.get("chat_type")
|
||||
|
|
@ -16736,7 +16736,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return
|
||||
await asyncio.sleep(poll_interval)
|
||||
if (pending_path.exists() or claimed_path.exists()) and not exit_code_path.exists():
|
||||
exit_code_path.write_text("124")
|
||||
exit_code_path.write_text("124", encoding="utf-8")
|
||||
await self._send_update_notification()
|
||||
return
|
||||
|
||||
|
|
@ -16778,7 +16778,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# Read any remaining output
|
||||
if output_path.exists():
|
||||
try:
|
||||
content = output_path.read_text()
|
||||
content = output_path.read_text(encoding="utf-8")
|
||||
if len(content) > bytes_sent:
|
||||
buffer += content[bytes_sent:]
|
||||
bytes_sent = len(content)
|
||||
|
|
@ -16788,7 +16788,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
|
||||
# Send final status
|
||||
try:
|
||||
exit_code_raw = exit_code_path.read_text().strip() or "1"
|
||||
exit_code_raw = exit_code_path.read_text(encoding="utf-8").strip() or "1"
|
||||
exit_code = int(exit_code_raw)
|
||||
if exit_code == 0:
|
||||
await adapter.send(
|
||||
|
|
@ -16817,7 +16817,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# Check for new output
|
||||
if output_path.exists():
|
||||
try:
|
||||
content = output_path.read_text()
|
||||
content = output_path.read_text(encoding="utf-8")
|
||||
if len(content) > bytes_sent:
|
||||
buffer += content[bytes_sent:]
|
||||
bytes_sent = len(content)
|
||||
|
|
@ -16835,7 +16835,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if (prompt_path.exists() and session_key
|
||||
and not self._update_prompt_pending.get(session_key)):
|
||||
try:
|
||||
prompt_data = json.loads(prompt_path.read_text())
|
||||
prompt_data = json.loads(prompt_path.read_text(encoding="utf-8"))
|
||||
prompt_text = prompt_data.get("prompt", "")
|
||||
default = prompt_data.get("default", "")
|
||||
if prompt_text:
|
||||
|
|
@ -16883,7 +16883,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# Timeout
|
||||
if not exit_code_path.exists():
|
||||
logger.warning("Update watcher timed out after %.0fs", timeout)
|
||||
exit_code_path.write_text("124")
|
||||
exit_code_path.write_text("124", encoding="utf-8")
|
||||
await _flush_buffer()
|
||||
try:
|
||||
await adapter.send(
|
||||
|
|
@ -16929,7 +16929,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
elif not claimed_path.exists():
|
||||
return True
|
||||
|
||||
pending = json.loads(claimed_path.read_text())
|
||||
pending = json.loads(claimed_path.read_text(encoding="utf-8"))
|
||||
platform_str = pending.get("platform")
|
||||
chat_id = pending.get("chat_id")
|
||||
chat_type = pending.get("chat_type")
|
||||
|
|
@ -16943,13 +16943,13 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
claimed_path.replace(pending_path)
|
||||
return False
|
||||
|
||||
exit_code_raw = exit_code_path.read_text().strip() or "1"
|
||||
exit_code_raw = exit_code_path.read_text(encoding="utf-8").strip() or "1"
|
||||
exit_code = int(exit_code_raw)
|
||||
|
||||
# Read the captured update output
|
||||
output = ""
|
||||
if output_path.exists():
|
||||
output = output_path.read_text()
|
||||
output = output_path.read_text(encoding="utf-8")
|
||||
|
||||
# Resolve adapter
|
||||
platform = Platform(platform_str)
|
||||
|
|
@ -17024,7 +17024,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(notify_path.read_text())
|
||||
data = json.loads(notify_path.read_text(encoding="utf-8"))
|
||||
platform_str = data.get("platform")
|
||||
chat_id = data.get("chat_id")
|
||||
chat_type = data.get("chat_type")
|
||||
|
|
|
|||
|
|
@ -4963,7 +4963,7 @@ class GatewaySlashCommandsMixin:
|
|||
if event.message_id:
|
||||
pending["message_id"] = event.message_id
|
||||
_tmp_pending = pending_path.with_suffix(".tmp")
|
||||
_tmp_pending.write_text(json.dumps(pending))
|
||||
_tmp_pending.write_text(json.dumps(pending), encoding="utf-8")
|
||||
_tmp_pending.replace(pending_path)
|
||||
exit_code_path.unlink(missing_ok=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -617,7 +617,7 @@ def _read_pid_record(pid_path: Optional[Path] = None) -> Optional[dict]:
|
|||
return None
|
||||
|
||||
try:
|
||||
raw = pid_path.read_text().strip()
|
||||
raw = pid_path.read_text(encoding="utf-8").strip()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
# File was deleted between exists() and read_text(), permission
|
||||
# flipped, or it holds non-UTF-8 / binary garbage.
|
||||
|
|
|
|||
|
|
@ -2180,7 +2180,7 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -6352,7 +6352,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
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"))
|
||||
|
|
|
|||
|
|
@ -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,9 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
_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 +559,7 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
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:
|
||||
|
|
|
|||
49
tests/gateway/test_gateway_utf8_encoding.py
Normal file
49
tests/gateway/test_gateway_utf8_encoding.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Static guard: every ``read_text`` / ``write_text`` call under ``gateway/``
|
||||
must pass an explicit ``encoding=`` keyword argument so non-UTF-8 Windows
|
||||
locales don't corrupt file IPC. Mirrors the AST-based guard pattern in
|
||||
``tests/tools/test_windows_compat.py``.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import pathlib
|
||||
import pytest
|
||||
|
||||
GATEWAY_DIR = pathlib.Path(__file__).resolve().parents[2] / "gateway"
|
||||
METHODS = {"read_text", "write_text"}
|
||||
SUPPRESSION = "# gateway-utf8: ok"
|
||||
|
||||
|
||||
def _find_violations():
|
||||
violations = []
|
||||
for py_file in sorted(GATEWAY_DIR.rglob("*.py")):
|
||||
source = py_file.read_text(encoding="utf-8")
|
||||
source_lines = source.splitlines()
|
||||
try:
|
||||
tree = ast.parse(source, filename=str(py_file))
|
||||
except SyntaxError:
|
||||
continue
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
func = node.func
|
||||
if not isinstance(func, ast.Attribute):
|
||||
continue
|
||||
if func.attr not in METHODS:
|
||||
continue
|
||||
if any(kw.arg == "encoding" for kw in node.keywords):
|
||||
continue
|
||||
lineno = node.lineno
|
||||
if lineno <= len(source_lines) and SUPPRESSION in source_lines[lineno - 1]:
|
||||
continue
|
||||
rel = py_file.relative_to(GATEWAY_DIR.parent)
|
||||
violations.append(f"{rel}:{lineno}")
|
||||
return violations
|
||||
|
||||
|
||||
def test_all_read_write_text_pass_encoding():
|
||||
violations = _find_violations()
|
||||
assert not violations, (
|
||||
"Bare read_text()/write_text() calls found (missing encoding= kwarg).\n"
|
||||
"Add encoding=\"utf-8\" or suppress with '# gateway-utf8: ok':\n"
|
||||
+ "\n".join(f" {v}" for v in violations)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue