feat(automation): app.state + screenshot handlers with base64

This commit is contained in:
SoftFever
2026-06-03 02:07:30 +08:00
parent 5a2f03adee
commit b0325c999a
2 changed files with 93 additions and 3 deletions

View File

@@ -128,6 +128,46 @@ std::vector<KeyChord> parse_keys(const nlohmann::json& params) {
}
} // namespace
namespace {
std::string base64_encode(const std::vector<unsigned char>& data) {
static const char* tbl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve(((data.size() + 2) / 3) * 4);
size_t i = 0;
for (; i + 2 < data.size(); i += 3) {
const unsigned n = (data[i] << 16) | (data[i+1] << 8) | data[i+2];
out.push_back(tbl[(n >> 18) & 63]);
out.push_back(tbl[(n >> 12) & 63]);
out.push_back(tbl[(n >> 6) & 63]);
out.push_back(tbl[n & 63]);
}
if (i < data.size()) {
unsigned n = data[i] << 16;
const bool two = (i + 1 < data.size());
if (two) n |= data[i+1] << 8;
out.push_back(tbl[(n >> 18) & 63]);
out.push_back(tbl[(n >> 12) & 63]);
out.push_back(two ? tbl[(n >> 6) & 63] : '=');
out.push_back('=');
}
return out;
}
std::optional<int> opt_int(const nlohmann::json& p, const char* key) {
if (p.is_object() && p.contains(key) && p.at(key).is_number_integer())
return p.at(key).get<int>();
return std::nullopt;
}
nlohmann::json image_to_json(const PngImage& img) {
if (img.png.empty())
throw AutomationError(kErrScreenshotFail, "screenshot produced no data");
return { {"png_base64", base64_encode(img.png)},
{"width", img.width}, {"height", img.height} };
}
} // namespace
nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) {
return { {"version", kAutomationVersion},
{"protocol", "2.0"},
@@ -252,8 +292,30 @@ nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json& params) {
}
nlohmann::json JsonRpcDispatcher::m_sync_wait_for(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
nlohmann::json JsonRpcDispatcher::m_app_state(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
nlohmann::json JsonRpcDispatcher::m_screenshot_viewport3d(const nlohmann::json&){ throw AutomationError(kMethodNotFound, "not implemented"); }
nlohmann::json JsonRpcDispatcher::m_app_state(const nlohmann::json&) {
return app_state_to_json(m_backend.app_state());
}
nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json& params) {
m_backend.refresh_ui();
const UiNode* target_ptr = nullptr;
UiNode resolved;
if (params.is_object() && params.contains("target")) {
UiNode tree = m_backend.dump_tree(DumpOptions{});
int count = 0;
const UiNode* n = resolve_unique(tree, parse_target(params.at("target")), count);
if (count == 0) throw AutomationError(kErrNotFound, "target not found");
if (count > 1) throw AutomationError(kErrNotFound, "target is ambiguous");
resolved = *n;
target_ptr = &resolved;
}
return image_to_json(m_backend.screenshot_window(target_ptr));
}
nlohmann::json JsonRpcDispatcher::m_screenshot_viewport3d(const nlohmann::json& params) {
return image_to_json(m_backend.screenshot_viewport3d(
opt_int(params, "plate"), opt_int(params, "width"), opt_int(params, "height")));
}
}}} // namespace

View File

@@ -142,3 +142,31 @@ TEST_CASE("input.key parses array form [\"ctrl\",\"s\"]", "[automation][rpc]") {
REQUIRE(mock.sent_keys[0][0].modifiers.size() == 1);
CHECK(mock.sent_keys[0][0].key == "s");
}
TEST_CASE("app.state returns serialized state", "[automation][rpc]") {
MockUiBackend mock;
mock.state.active_tab = "prepare"; mock.state.project_loaded = true;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","app.state"}});
CHECK(resp.at("result").at("active_tab") == "prepare");
CHECK(resp.at("result").at("project_loaded") == true);
}
TEST_CASE("screenshot.window returns base64 + dims", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","screenshot.window"}});
CHECK(mock.screenshot_window_count == 1);
CHECK(resp.at("result").at("width") == 4);
CHECK(resp.at("result").at("png_base64").is_string());
CHECK_FALSE(resp.at("result").at("png_base64").get<std::string>().empty());
}
TEST_CASE("screenshot.viewport3d returns base64 + dims", "[automation][rpc]") {
MockUiBackend mock;
JsonRpcDispatcher d(mock);
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","screenshot.viewport3d"},
{"params",{{"width",256},{"height",256}}}});
CHECK(mock.screenshot_viewport_count == 1);
CHECK(resp.at("result").at("png_base64").is_string());
}