271 lines
7.3 KiB
C++
271 lines
7.3 KiB
C++
#include "cloud_ime.h"
|
|
#include <Windows.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <winhttp.h>
|
|
#include <fmt/xchar.h>
|
|
#include "utils/common_utils.h"
|
|
|
|
#pragma comment(lib, "winhttp.lib")
|
|
|
|
namespace
|
|
{
|
|
constexpr auto kIdleDelay = std::chrono::milliseconds(500);
|
|
constexpr int kTimeoutMs = 2000;
|
|
|
|
std::mutex g_mutex;
|
|
std::condition_variable g_cv;
|
|
std::thread g_worker;
|
|
std::atomic<bool> g_running{false};
|
|
std::atomic<uint64_t> g_generation{0};
|
|
std::string g_latest_pinyin;
|
|
bool g_latest_japanese = false;
|
|
std::chrono::steady_clock::time_point g_last_input_time;
|
|
CloudIme::ApplyCallback g_apply_callback;
|
|
|
|
std::string UrlEncode(const std::string &input)
|
|
{
|
|
static const char *kHex = "0123456789ABCDEF";
|
|
std::string out;
|
|
out.reserve(input.size() * 3);
|
|
for (unsigned char c : input)
|
|
{
|
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' ||
|
|
c == '.' || c == '~')
|
|
{
|
|
out.push_back(static_cast<char>(c));
|
|
}
|
|
else
|
|
{
|
|
out.push_back('%');
|
|
out.push_back(kHex[(c >> 4) & 0x0F]);
|
|
out.push_back(kHex[c & 0x0F]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool HttpGet(const std::wstring &host, const std::wstring &path, std::string &response_out)
|
|
{
|
|
HINTERNET hSession = WinHttpOpen(L"MetasequoiaImeServer/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
|
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
|
if (!hSession)
|
|
return false;
|
|
|
|
WinHttpSetTimeouts(hSession, kTimeoutMs, kTimeoutMs, kTimeoutMs, kTimeoutMs);
|
|
|
|
HINTERNET hConnect = WinHttpConnect(hSession, host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0);
|
|
if (!hConnect)
|
|
{
|
|
WinHttpCloseHandle(hSession);
|
|
return false;
|
|
}
|
|
|
|
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path.c_str(), NULL, WINHTTP_NO_REFERER,
|
|
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
|
|
if (!hRequest)
|
|
{
|
|
WinHttpCloseHandle(hConnect);
|
|
WinHttpCloseHandle(hSession);
|
|
return false;
|
|
}
|
|
|
|
BOOL send_ok = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
|
|
if (!send_ok)
|
|
{
|
|
WinHttpCloseHandle(hRequest);
|
|
WinHttpCloseHandle(hConnect);
|
|
WinHttpCloseHandle(hSession);
|
|
return false;
|
|
}
|
|
|
|
BOOL recv_ok = WinHttpReceiveResponse(hRequest, NULL);
|
|
if (!recv_ok)
|
|
{
|
|
WinHttpCloseHandle(hRequest);
|
|
WinHttpCloseHandle(hConnect);
|
|
WinHttpCloseHandle(hSession);
|
|
return false;
|
|
}
|
|
|
|
std::string response;
|
|
DWORD bytes_available = 0;
|
|
do
|
|
{
|
|
if (!WinHttpQueryDataAvailable(hRequest, &bytes_available))
|
|
break;
|
|
if (bytes_available == 0)
|
|
break;
|
|
std::string buffer(bytes_available, '\0');
|
|
DWORD bytes_read = 0;
|
|
if (!WinHttpReadData(hRequest, buffer.data(), bytes_available, &bytes_read))
|
|
break;
|
|
response.append(buffer.data(), bytes_read);
|
|
} while (bytes_available > 0);
|
|
|
|
WinHttpCloseHandle(hRequest);
|
|
WinHttpCloseHandle(hConnect);
|
|
WinHttpCloseHandle(hSession);
|
|
|
|
response_out = std::move(response);
|
|
return !response_out.empty();
|
|
}
|
|
|
|
std::string ParseCandidate(const std::string &response)
|
|
{
|
|
nlohmann::json root;
|
|
try
|
|
{
|
|
root = nlohmann::json::parse(response);
|
|
}
|
|
catch (...)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
if (!root.is_array() || root.size() < 2)
|
|
return "";
|
|
if (!root[0].is_string() || root[0].get<std::string>() != "SUCCESS")
|
|
return "";
|
|
if (!root[1].is_array() || root[1].empty())
|
|
return "";
|
|
|
|
const auto &first_item = root[1][0];
|
|
if (!first_item.is_array() || first_item.size() < 2 || !first_item[1].is_array())
|
|
return "";
|
|
|
|
const auto &cand_list = first_item[1];
|
|
if (cand_list.empty() || !cand_list[0].is_string())
|
|
return "";
|
|
|
|
return cand_list[0].get<std::string>();
|
|
}
|
|
|
|
// TODO: 可以添加其他的云服务接口,甚至是自己搭建的接口,暂时先只支持 Google 的接口,Google 带善人
|
|
std::string FetchCloudCandidate(const std::string &pinyin, bool japanese, uint64_t generation)
|
|
{
|
|
if (pinyin.empty())
|
|
return "";
|
|
|
|
if (!g_running || g_generation.load() != generation)
|
|
return "";
|
|
|
|
std::string encoded = UrlEncode(pinyin);
|
|
const char *input_tool = japanese ? "ja-t-i0-und" : "zh-t-i0-pinyin";
|
|
std::string path_utf8 = "/request?text=" + encoded + "&itc=" + input_tool + "&num=1&ie=utf-8&oe=utf-8";
|
|
std::wstring path(path_utf8.begin(), path_utf8.end());
|
|
|
|
std::string response;
|
|
if (!HttpGet(L"inputtools.google.com", path, response))
|
|
{
|
|
(void)0;
|
|
return "";
|
|
}
|
|
|
|
if (!g_running || g_generation.load() != generation)
|
|
return "";
|
|
|
|
// (void)0;
|
|
std::string candidate = ParseCandidate(response);
|
|
// (void)0;
|
|
return candidate;
|
|
}
|
|
|
|
void WorkerLoop()
|
|
{
|
|
uint64_t observed_generation = g_generation.load();
|
|
while (g_running)
|
|
{
|
|
std::unique_lock lock(g_mutex);
|
|
g_cv.wait(lock, [&] { return !g_running || g_generation.load() != observed_generation; });
|
|
if (!g_running)
|
|
break;
|
|
|
|
observed_generation = g_generation.load();
|
|
std::string pinyin = g_latest_pinyin;
|
|
bool japanese = g_latest_japanese;
|
|
auto target_time = g_last_input_time + kIdleDelay;
|
|
|
|
while (g_running)
|
|
{
|
|
if (g_cv.wait_until(lock, target_time,
|
|
[&] { return !g_running || g_generation.load() != observed_generation; }))
|
|
{
|
|
if (!g_running)
|
|
return;
|
|
observed_generation = g_generation.load();
|
|
pinyin = g_latest_pinyin;
|
|
japanese = g_latest_japanese;
|
|
target_time = g_last_input_time + kIdleDelay;
|
|
continue;
|
|
}
|
|
break; // debounce timeout
|
|
}
|
|
|
|
lock.unlock();
|
|
|
|
if (!g_running)
|
|
break;
|
|
|
|
if (pinyin.empty())
|
|
continue;
|
|
|
|
uint64_t request_generation = observed_generation;
|
|
std::string candidate = FetchCloudCandidate(pinyin, japanese, request_generation);
|
|
if (candidate.empty())
|
|
continue;
|
|
if (g_generation.load() != request_generation)
|
|
continue;
|
|
|
|
if (g_apply_callback)
|
|
{
|
|
g_apply_callback(candidate, pinyin, request_generation);
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
namespace CloudIme
|
|
{
|
|
void Start(ApplyCallback apply_callback)
|
|
{
|
|
if (g_running)
|
|
return;
|
|
g_apply_callback = std::move(apply_callback);
|
|
g_running = true;
|
|
g_worker = std::thread(WorkerLoop);
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
if (!g_running)
|
|
return;
|
|
g_running = false;
|
|
g_cv.notify_all();
|
|
if (g_worker.joinable())
|
|
g_worker.join();
|
|
}
|
|
|
|
void OnInputChanged(const std::string &pure_pinyin, bool japanese)
|
|
{
|
|
{
|
|
std::lock_guard lock(g_mutex);
|
|
g_latest_pinyin = pure_pinyin;
|
|
g_latest_japanese = japanese;
|
|
g_last_input_time = std::chrono::steady_clock::now();
|
|
g_generation.fetch_add(1);
|
|
}
|
|
g_cv.notify_one();
|
|
}
|
|
|
|
void Clear()
|
|
{
|
|
OnInputChanged("", false);
|
|
}
|
|
} // namespace CloudIme
|