From c7b5dd93502a829da18e2a7ef2253ae3298f2f28 Mon Sep 17 00:00:00 2001 From: chtangwin Date: Tue, 10 Feb 2026 17:31:45 +0800 Subject: [PATCH 1/3] Fix: Ensure UTF-8 encoding for all file operations --- nanobot/agent/loop.py | 3 ++- nanobot/agent/subagent.py | 4 ++-- nanobot/config/loader.py | 2 +- nanobot/cron/service.py | 4 ++-- nanobot/session/manager.py | 11 ++++++----- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index e5a5183..1cd5730 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -206,7 +206,7 @@ class AgentLoop: "type": "function", "function": { "name": tc.name, - "arguments": json.dumps(tc.arguments) + "arguments": json.dumps(tc.arguments, ensure_ascii=False) } } for tc in response.tool_calls @@ -388,6 +388,7 @@ class AgentLoop: ) final_content, _ = await self._run_agent_loop(initial_messages) + if final_content is None: final_content = "Background task completed." diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 203836a..ffefc08 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -146,7 +146,7 @@ class SubagentManager: "type": "function", "function": { "name": tc.name, - "arguments": json.dumps(tc.arguments), + "arguments": json.dumps(tc.arguments, ensure_ascii=False), }, } for tc in response.tool_calls @@ -159,7 +159,7 @@ class SubagentManager: # Execute tools for tool_call in response.tool_calls: - args_str = json.dumps(tool_call.arguments) + args_str = json.dumps(tool_call.arguments, ensure_ascii=False) logger.debug(f"Subagent [{task_id}] executing: {tool_call.name} with arguments: {args_str}") result = await tools.execute(tool_call.name, tool_call.arguments) messages.append({ diff --git a/nanobot/config/loader.py b/nanobot/config/loader.py index 560c1f5..134e95f 100644 --- a/nanobot/config/loader.py +++ b/nanobot/config/loader.py @@ -31,7 +31,7 @@ def load_config(config_path: Path | None = None) -> Config: if path.exists(): try: - with open(path) as f: + with open(path, encoding="utf-8") as f: data = json.load(f) data = _migrate_config(data) return Config.model_validate(data) diff --git a/nanobot/cron/service.py b/nanobot/cron/service.py index 14666e8..3c77452 100644 --- a/nanobot/cron/service.py +++ b/nanobot/cron/service.py @@ -66,7 +66,7 @@ class CronService: if self.store_path.exists(): try: - data = json.loads(self.store_path.read_text()) + data = json.loads(self.store_path.read_text(encoding="utf-8")) jobs = [] for j in data.get("jobs", []): jobs.append(CronJob( @@ -148,7 +148,7 @@ class CronService: ] } - self.store_path.write_text(json.dumps(data, indent=2)) + self.store_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") async def start(self) -> None: """Start the cron service.""" diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 752fce4..42df1b1 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -121,7 +121,7 @@ class SessionManager: created_at = None last_consolidated = 0 - with open(path) as f: + with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if not line: @@ -151,7 +151,7 @@ class SessionManager: """Save a session to disk.""" path = self._get_session_path(session.key) - with open(path, "w") as f: + with open(path, "w", encoding="utf-8") as f: metadata_line = { "_type": "metadata", "created_at": session.created_at.isoformat(), @@ -159,9 +159,10 @@ class SessionManager: "metadata": session.metadata, "last_consolidated": session.last_consolidated } - f.write(json.dumps(metadata_line) + "\n") + f.write(json.dumps(metadata_line, ensure_ascii=False) + "\n") for msg in session.messages: - f.write(json.dumps(msg) + "\n") + f.write(json.dumps(msg, ensure_ascii=False) + "\n") + self._cache[session.key] = session @@ -181,7 +182,7 @@ class SessionManager: for path in self.sessions_dir.glob("*.jsonl"): try: # Read just the metadata line - with open(path) as f: + with open(path, encoding="utf-8") as f: first_line = f.readline().strip() if first_line: data = json.loads(first_line) From a2379a08ac5467e4b3e628a7264427be73759a9f Mon Sep 17 00:00:00 2001 From: chtangwin Date: Wed, 18 Feb 2026 18:37:17 -0800 Subject: [PATCH 2/3] Fix: Ensure UTF-8 encoding and ensure_ascii=False for remaining file/JSON operations --- nanobot/agent/tools/web.py | 8 ++++---- nanobot/channels/dingtalk.py | 2 +- nanobot/cli/commands.py | 6 +++--- nanobot/config/loader.py | 4 ++-- nanobot/heartbeat/service.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index 9de1d3c..90cdda8 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -116,7 +116,7 @@ class WebFetchTool(Tool): # Validate URL before fetching is_valid, error_msg = _validate_url(url) if not is_valid: - return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}) + return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}, ensure_ascii=False) try: async with httpx.AsyncClient( @@ -131,7 +131,7 @@ class WebFetchTool(Tool): # JSON if "application/json" in ctype: - text, extractor = json.dumps(r.json(), indent=2), "json" + text, extractor = json.dumps(r.json(), indent=2, ensure_ascii=False), "json" # HTML elif "text/html" in ctype or r.text[:256].lower().startswith((" str: """Convert HTML to markdown.""" diff --git a/nanobot/channels/dingtalk.py b/nanobot/channels/dingtalk.py index 4a8cdd9..6b27af4 100644 --- a/nanobot/channels/dingtalk.py +++ b/nanobot/channels/dingtalk.py @@ -208,7 +208,7 @@ class DingTalkChannel(BaseChannel): "msgParam": json.dumps({ "text": msg.content, "title": "Nanobot Reply", - }), + }, ensure_ascii=False), } if not self._http: diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 2f4ba7b..d879d58 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -243,7 +243,7 @@ Information about the user goes here. for filename, content in templates.items(): file_path = workspace / filename if not file_path.exists(): - file_path.write_text(content) + file_path.write_text(content, encoding="utf-8") console.print(f" [dim]Created {filename}[/dim]") # Create memory directory and MEMORY.md @@ -266,12 +266,12 @@ This file stores important information that should persist across sessions. ## Important Notes (Things to remember) -""") +""", encoding="utf-8") console.print(" [dim]Created memory/MEMORY.md[/dim]") history_file = memory_dir / "HISTORY.md" if not history_file.exists(): - history_file.write_text("") + history_file.write_text("", encoding="utf-8") console.print(" [dim]Created memory/HISTORY.md[/dim]") # Create skills directory for custom user skills diff --git a/nanobot/config/loader.py b/nanobot/config/loader.py index 134e95f..c789efd 100644 --- a/nanobot/config/loader.py +++ b/nanobot/config/loader.py @@ -55,8 +55,8 @@ def save_config(config: Config, config_path: Path | None = None) -> None: data = config.model_dump(by_alias=True) - with open(path, "w") as f: - json.dump(data, f, indent=2) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) def _migrate_config(data: dict) -> dict: diff --git a/nanobot/heartbeat/service.py b/nanobot/heartbeat/service.py index 221ed27..a51e5a0 100644 --- a/nanobot/heartbeat/service.py +++ b/nanobot/heartbeat/service.py @@ -65,7 +65,7 @@ class HeartbeatService: """Read HEARTBEAT.md content.""" if self.heartbeat_file.exists(): try: - return self.heartbeat_file.read_text() + return self.heartbeat_file.read_text(encoding="utf-8") except Exception: return None return None From 124c611426eefe06aeb9a5b3d33338286228bb8a Mon Sep 17 00:00:00 2001 From: chtangwin Date: Wed, 18 Feb 2026 18:46:23 -0800 Subject: [PATCH 3/3] Fix: Add ensure_ascii=False to WhatsApp send payload The send() payload contains user message content (msg.content) which may include non-ASCII characters (e.g. CJK, German umlauts, emoji). The auth frame and Discord heartbeat/identify payloads are left unchanged as they only carry ASCII protocol fields. --- nanobot/channels/whatsapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanobot/channels/whatsapp.py b/nanobot/channels/whatsapp.py index 0cf2dd7..f799347 100644 --- a/nanobot/channels/whatsapp.py +++ b/nanobot/channels/whatsapp.py @@ -87,7 +87,7 @@ class WhatsAppChannel(BaseChannel): "to": msg.chat_id, "text": msg.content } - await self._ws.send(json.dumps(payload)) + await self._ws.send(json.dumps(payload, ensure_ascii=False)) except Exception as e: logger.error(f"Error sending WhatsApp message: {e}")