From f8bf6aea51b96b492c5a2fb55c1b58ed06a48dd9 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Mon, 15 Jun 2026 15:14:24 +0800 Subject: [PATCH] fix(webui): encode automation update values --- nanobot/webui/ws_http.py | 6 ++++- tests/channels/test_websocket_http_routes.py | 24 ++++++++++++++++++-- webui/src/lib/api.ts | 8 ++++--- webui/src/tests/api.test.ts | 17 +++++++------- webui/src/tests/app-layout.test.tsx | 2 +- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/nanobot/webui/ws_http.py b/nanobot/webui/ws_http.py index 2fc00ec8..34ac0151 100644 --- a/nanobot/webui/ws_http.py +++ b/nanobot/webui/ws_http.py @@ -17,6 +17,7 @@ import time from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any +from urllib.parse import unquote from loguru import logger from websockets.http11 import Request as WsRequest @@ -782,7 +783,10 @@ def _automation_values_from_request(request: WsRequest) -> dict[str, Any] | None try: values = json.loads(raw) except Exception: - return None + try: + values = json.loads(unquote(raw)) + except Exception: + return None return values if isinstance(values, dict) else None diff --git a/tests/channels/test_websocket_http_routes.py b/tests/channels/test_websocket_http_routes.py index 6a8eaf12..885cc37a 100644 --- a/tests/channels/test_websocket_http_routes.py +++ b/tests/channels/test_websocket_http_routes.py @@ -10,7 +10,7 @@ import time from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock -from urllib.parse import urlencode +from urllib.parse import quote, urlencode import httpx import pytest @@ -942,6 +942,26 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions( assert by_id[user_job.id]["schedule"]["expr"] == "0 9 * * *" assert by_id[user_job.id]["schedule"]["tz"] == "UTC" + unicode_update = await _http_get( + f"{base_url}/api/webui/automations/update?id={user_job.id}", + headers={ + **auth, + "X-Nanobot-Automation-Values": quote( + json.dumps( + { + "name": "每日测验", + "message": "问今日测验", + }, + ensure_ascii=False, + ), + safe="", + ), + }, + ) + assert unicode_update.status_code == 200 + assert cron.get_job(user_job.id).name == "每日测验" + assert cron.get_job(user_job.id).payload.message == "问今日测验" + malformed_update = await _http_get( f"{base_url}/api/webui/automations/update?id={user_job.id}", headers={ @@ -950,7 +970,7 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions( }, ) assert malformed_update.status_code == 400 - assert cron.get_job(user_job.id).payload.message == "Ask the daily quiz" + assert cron.get_job(user_job.id).payload.message == "问今日测验" invalid_cron_update = await _http_get( f"{base_url}/api/webui/automations/update?id={user_job.id}", diff --git a/webui/src/lib/api.ts b/webui/src/lib/api.ts index b31879e3..dde14b0a 100644 --- a/webui/src/lib/api.ts +++ b/webui/src/lib/api.ts @@ -89,6 +89,10 @@ function mcpValuesHeader(values: Record): HeadersInit | undefin return { "X-Nanobot-MCP-Values": JSON.stringify(payload) }; } +function automationValuesHeader(values: AutomationUpdatePayload): HeadersInit { + return { "X-Nanobot-Automation-Values": encodeURIComponent(JSON.stringify(values)) }; +} + function splitKey(key: string): { channel: string; chatId: string } { const idx = key.indexOf(":"); if (idx === -1) return { channel: "", chatId: key }; @@ -226,9 +230,7 @@ export async function updateAutomation( `${base}/api/webui/automations/update?${query}`, token, { - headers: { - "X-Nanobot-Automation-Values": JSON.stringify(values), - }, + headers: automationValuesHeader(values), }, API_READ_TIMEOUT_MS, ); diff --git a/webui/src/tests/api.test.ts b/webui/src/tests/api.test.ts index 80619e73..19e3f86e 100644 --- a/webui/src/tests/api.test.ts +++ b/webui/src/tests/api.test.ts @@ -125,25 +125,24 @@ describe("webui API helpers", () => { }); it("serializes workspace automation updates", async () => { - await updateAutomation("tok", "job 1/2", { - name: "Daily quiz", - message: "Ask the quiz", + const values = { + name: "每日测验", + message: "Ask 今日 quiz", schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" }, - }); + } as const; + await updateAutomation("tok", "job 1/2", values); expect(fetch).toHaveBeenCalledWith( "/api/webui/automations/update?id=job+1%2F2", expect.objectContaining({ headers: { Authorization: "Bearer tok", - "X-Nanobot-Automation-Values": JSON.stringify({ - name: "Daily quiz", - message: "Ask the quiz", - schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" }, - }), + "X-Nanobot-Automation-Values": encodeURIComponent(JSON.stringify(values)), }, }), ); + const header = vi.mocked(fetch).mock.calls[0][1]?.headers as Record; + expect(header["X-Nanobot-Automation-Values"]).not.toContain("每日"); }); it("fetches the WebUI skill summary", async () => { diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index ccf3013a..33e5d413 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -518,7 +518,7 @@ describe("App layout", () => { ); expect(updateCall).toBeTruthy(); const headers = updateCall?.[1]?.headers as Record; - expect(JSON.parse(headers["X-Nanobot-Automation-Values"])).toEqual({ + expect(JSON.parse(decodeURIComponent(headers["X-Nanobot-Automation-Values"]))).toEqual({ name: "Past one-shot", message: "Updated one-shot message", });