fix(webui): encode automation update values

This commit is contained in:
chengyongru 2026-06-15 15:14:24 +08:00
parent 5892c6913b
commit f8bf6aea51
5 changed files with 41 additions and 16 deletions

View File

@ -17,6 +17,7 @@ import time
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from urllib.parse import unquote
from loguru import logger from loguru import logger
from websockets.http11 import Request as WsRequest from websockets.http11 import Request as WsRequest
@ -782,7 +783,10 @@ def _automation_values_from_request(request: WsRequest) -> dict[str, Any] | None
try: try:
values = json.loads(raw) values = json.loads(raw)
except Exception: except Exception:
return None try:
values = json.loads(unquote(raw))
except Exception:
return None
return values if isinstance(values, dict) else None return values if isinstance(values, dict) else None

View File

@ -10,7 +10,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from urllib.parse import urlencode from urllib.parse import quote, urlencode
import httpx import httpx
import pytest 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"]["expr"] == "0 9 * * *"
assert by_id[user_job.id]["schedule"]["tz"] == "UTC" 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( malformed_update = await _http_get(
f"{base_url}/api/webui/automations/update?id={user_job.id}", f"{base_url}/api/webui/automations/update?id={user_job.id}",
headers={ 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 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( invalid_cron_update = await _http_get(
f"{base_url}/api/webui/automations/update?id={user_job.id}", f"{base_url}/api/webui/automations/update?id={user_job.id}",

View File

@ -89,6 +89,10 @@ function mcpValuesHeader(values: Record<string, unknown>): HeadersInit | undefin
return { "X-Nanobot-MCP-Values": JSON.stringify(payload) }; 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 } { function splitKey(key: string): { channel: string; chatId: string } {
const idx = key.indexOf(":"); const idx = key.indexOf(":");
if (idx === -1) return { channel: "", chatId: key }; if (idx === -1) return { channel: "", chatId: key };
@ -226,9 +230,7 @@ export async function updateAutomation(
`${base}/api/webui/automations/update?${query}`, `${base}/api/webui/automations/update?${query}`,
token, token,
{ {
headers: { headers: automationValuesHeader(values),
"X-Nanobot-Automation-Values": JSON.stringify(values),
},
}, },
API_READ_TIMEOUT_MS, API_READ_TIMEOUT_MS,
); );

View File

@ -125,25 +125,24 @@ describe("webui API helpers", () => {
}); });
it("serializes workspace automation updates", async () => { it("serializes workspace automation updates", async () => {
await updateAutomation("tok", "job 1/2", { const values = {
name: "Daily quiz", name: "每日测验",
message: "Ask the quiz", message: "Ask 今日 quiz",
schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" }, schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" },
}); } as const;
await updateAutomation("tok", "job 1/2", values);
expect(fetch).toHaveBeenCalledWith( expect(fetch).toHaveBeenCalledWith(
"/api/webui/automations/update?id=job+1%2F2", "/api/webui/automations/update?id=job+1%2F2",
expect.objectContaining({ expect.objectContaining({
headers: { headers: {
Authorization: "Bearer tok", Authorization: "Bearer tok",
"X-Nanobot-Automation-Values": JSON.stringify({ "X-Nanobot-Automation-Values": encodeURIComponent(JSON.stringify(values)),
name: "Daily quiz",
message: "Ask the quiz",
schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" },
}),
}, },
}), }),
); );
const header = vi.mocked(fetch).mock.calls[0][1]?.headers as Record<string, string>;
expect(header["X-Nanobot-Automation-Values"]).not.toContain("每日");
}); });
it("fetches the WebUI skill summary", async () => { it("fetches the WebUI skill summary", async () => {

View File

@ -518,7 +518,7 @@ describe("App layout", () => {
); );
expect(updateCall).toBeTruthy(); expect(updateCall).toBeTruthy();
const headers = updateCall?.[1]?.headers as Record<string, string>; const headers = updateCall?.[1]?.headers as Record<string, string>;
expect(JSON.parse(headers["X-Nanobot-Automation-Values"])).toEqual({ expect(JSON.parse(decodeURIComponent(headers["X-Nanobot-Automation-Values"]))).toEqual({
name: "Past one-shot", name: "Past one-shot",
message: "Updated one-shot message", message: "Updated one-shot message",
}); });