fix(webui): encode automation update values
This commit is contained in:
@@ -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
|
||||||
@@ -781,6 +782,9 @@ def _automation_values_from_request(request: WsRequest) -> dict[str, Any] | None
|
|||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
values = json.loads(raw)
|
values = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
values = json.loads(unquote(raw))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
return values if isinstance(values, dict) else None
|
return values if isinstance(values, dict) else None
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user