Merge PR #455: fix UTF-8 encoding and ensure_ascii for non-ASCII support

This commit is contained in:
Re-bin
2026-02-20 08:00:32 +00:00
12 changed files with 26 additions and 26 deletions

View File

@@ -206,7 +206,7 @@ class AgentLoop:
"type": "function", "type": "function",
"function": { "function": {
"name": tc.name, "name": tc.name,
"arguments": json.dumps(tc.arguments) "arguments": json.dumps(tc.arguments, ensure_ascii=False)
} }
} }
for tc in response.tool_calls for tc in response.tool_calls

View File

@@ -146,7 +146,7 @@ class SubagentManager:
"type": "function", "type": "function",
"function": { "function": {
"name": tc.name, "name": tc.name,
"arguments": json.dumps(tc.arguments), "arguments": json.dumps(tc.arguments, ensure_ascii=False),
}, },
} }
for tc in response.tool_calls for tc in response.tool_calls
@@ -159,7 +159,7 @@ class SubagentManager:
# Execute tools # Execute tools
for tool_call in response.tool_calls: 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("Subagent [{}] executing: {} with arguments: {}", task_id, tool_call.name, args_str) logger.debug("Subagent [{}] executing: {} with arguments: {}", task_id, tool_call.name, args_str)
result = await tools.execute(tool_call.name, tool_call.arguments) result = await tools.execute(tool_call.name, tool_call.arguments)
messages.append({ messages.append({

View File

@@ -116,7 +116,7 @@ class WebFetchTool(Tool):
# Validate URL before fetching # Validate URL before fetching
is_valid, error_msg = _validate_url(url) is_valid, error_msg = _validate_url(url)
if not is_valid: 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: try:
async with httpx.AsyncClient( async with httpx.AsyncClient(
@@ -131,7 +131,7 @@ class WebFetchTool(Tool):
# JSON # JSON
if "application/json" in ctype: 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 # HTML
elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")): elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")):
doc = Document(r.text) doc = Document(r.text)
@@ -146,9 +146,9 @@ class WebFetchTool(Tool):
text = text[:max_chars] text = text[:max_chars]
return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code, return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code,
"extractor": extractor, "truncated": truncated, "length": len(text), "text": text}) "extractor": extractor, "truncated": truncated, "length": len(text), "text": text}, ensure_ascii=False)
except Exception as e: except Exception as e:
return json.dumps({"error": str(e), "url": url}) return json.dumps({"error": str(e), "url": url}, ensure_ascii=False)
def _to_markdown(self, html: str) -> str: def _to_markdown(self, html: str) -> str:
"""Convert HTML to markdown.""" """Convert HTML to markdown."""

View File

@@ -208,7 +208,7 @@ class DingTalkChannel(BaseChannel):
"msgParam": json.dumps({ "msgParam": json.dumps({
"text": msg.content, "text": msg.content,
"title": "Nanobot Reply", "title": "Nanobot Reply",
}), }, ensure_ascii=False),
} }
if not self._http: if not self._http:

View File

@@ -390,7 +390,7 @@ class FeishuChannel(BaseChannel):
if key: if key:
await loop.run_in_executor( await loop.run_in_executor(
None, self._send_message_sync, None, self._send_message_sync,
receive_id_type, msg.chat_id, "image", json.dumps({"image_key": key}), receive_id_type, msg.chat_id, "image", json.dumps({"image_key": key}, ensure_ascii=False),
) )
else: else:
key = await loop.run_in_executor(None, self._upload_file_sync, file_path) key = await loop.run_in_executor(None, self._upload_file_sync, file_path)
@@ -398,7 +398,7 @@ class FeishuChannel(BaseChannel):
media_type = "audio" if ext in self._AUDIO_EXTS else "file" media_type = "audio" if ext in self._AUDIO_EXTS else "file"
await loop.run_in_executor( await loop.run_in_executor(
None, self._send_message_sync, None, self._send_message_sync,
receive_id_type, msg.chat_id, media_type, json.dumps({"file_key": key}), receive_id_type, msg.chat_id, media_type, json.dumps({"file_key": key}, ensure_ascii=False),
) )
if msg.content and msg.content.strip(): if msg.content and msg.content.strip():

View File

@@ -87,7 +87,7 @@ class WhatsAppChannel(BaseChannel):
"to": msg.chat_id, "to": msg.chat_id,
"text": msg.content "text": msg.content
} }
await self._ws.send(json.dumps(payload)) await self._ws.send(json.dumps(payload, ensure_ascii=False))
except Exception as e: except Exception as e:
logger.error("Error sending WhatsApp message: {}", e) logger.error("Error sending WhatsApp message: {}", e)

View File

@@ -243,7 +243,7 @@ Information about the user goes here.
for filename, content in templates.items(): for filename, content in templates.items():
file_path = workspace / filename file_path = workspace / filename
if not file_path.exists(): 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]") console.print(f" [dim]Created {filename}[/dim]")
# Create memory directory and MEMORY.md # Create memory directory and MEMORY.md
@@ -266,12 +266,12 @@ This file stores important information that should persist across sessions.
## Important Notes ## Important Notes
(Things to remember) (Things to remember)
""") """, encoding="utf-8")
console.print(" [dim]Created memory/MEMORY.md[/dim]") console.print(" [dim]Created memory/MEMORY.md[/dim]")
history_file = memory_dir / "HISTORY.md" history_file = memory_dir / "HISTORY.md"
if not history_file.exists(): if not history_file.exists():
history_file.write_text("") history_file.write_text("", encoding="utf-8")
console.print(" [dim]Created memory/HISTORY.md[/dim]") console.print(" [dim]Created memory/HISTORY.md[/dim]")
# Create skills directory for custom user skills # Create skills directory for custom user skills

View File

@@ -31,7 +31,7 @@ def load_config(config_path: Path | None = None) -> Config:
if path.exists(): if path.exists():
try: try:
with open(path) as f: with open(path, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
data = _migrate_config(data) data = _migrate_config(data)
return Config.model_validate(data) return Config.model_validate(data)
@@ -55,8 +55,8 @@ def save_config(config: Config, config_path: Path | None = None) -> None:
data = config.model_dump(by_alias=True) data = config.model_dump(by_alias=True)
with open(path, "w") as f: with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2, ensure_ascii=False)
def _migrate_config(data: dict) -> dict: def _migrate_config(data: dict) -> dict:

View File

@@ -66,7 +66,7 @@ class CronService:
if self.store_path.exists(): if self.store_path.exists():
try: try:
data = json.loads(self.store_path.read_text()) data = json.loads(self.store_path.read_text(encoding="utf-8"))
jobs = [] jobs = []
for j in data.get("jobs", []): for j in data.get("jobs", []):
jobs.append(CronJob( 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: async def start(self) -> None:
"""Start the cron service.""" """Start the cron service."""

View File

@@ -65,7 +65,7 @@ class HeartbeatService:
"""Read HEARTBEAT.md content.""" """Read HEARTBEAT.md content."""
if self.heartbeat_file.exists(): if self.heartbeat_file.exists():
try: try:
return self.heartbeat_file.read_text() return self.heartbeat_file.read_text(encoding="utf-8")
except Exception: except Exception:
return None return None
return None return None

View File

@@ -176,7 +176,7 @@ def _convert_messages(messages: list[dict[str, Any]]) -> tuple[str, list[dict[st
if role == "tool": if role == "tool":
call_id, _ = _split_tool_call_id(msg.get("tool_call_id")) call_id, _ = _split_tool_call_id(msg.get("tool_call_id"))
output_text = content if isinstance(content, str) else json.dumps(content) output_text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False)
input_items.append( input_items.append(
{ {
"type": "function_call_output", "type": "function_call_output",

View File

@@ -121,7 +121,7 @@ class SessionManager:
created_at = None created_at = None
last_consolidated = 0 last_consolidated = 0
with open(path) as f: with open(path, encoding="utf-8") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if not line: if not line:
@@ -151,7 +151,7 @@ class SessionManager:
"""Save a session to disk.""" """Save a session to disk."""
path = self._get_session_path(session.key) path = self._get_session_path(session.key)
with open(path, "w") as f: with open(path, "w", encoding="utf-8") as f:
metadata_line = { metadata_line = {
"_type": "metadata", "_type": "metadata",
"created_at": session.created_at.isoformat(), "created_at": session.created_at.isoformat(),
@@ -159,9 +159,9 @@ class SessionManager:
"metadata": session.metadata, "metadata": session.metadata,
"last_consolidated": session.last_consolidated "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: 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 self._cache[session.key] = session
@@ -181,7 +181,7 @@ class SessionManager:
for path in self.sessions_dir.glob("*.jsonl"): for path in self.sessions_dir.glob("*.jsonl"):
try: try:
# Read just the metadata line # Read just the metadata line
with open(path) as f: with open(path, encoding="utf-8") as f:
first_line = f.readline().strip() first_line = f.readline().strip()
if first_line: if first_line:
data = json.loads(first_line) data = json.loads(first_line)