chat plugin (fully AI I'm not writing ts)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "PLUGIN_DIR=%~dp0"
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%native"
|
||||
set "PLUGIN_NAME=chat"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\plugin.cpp" (
|
||||
echo [error] native\plugin.cpp not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - chat plugin is Windows-only (Win32 user32/gdi32)
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if not "!TARGET_OS!"=="windows" (
|
||||
echo [error] chat plugin only supports windows targets, got !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "OUTFILE=%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll"
|
||||
if exist "!OUTFILE!" del /f /q "!OUTFILE!"
|
||||
|
||||
set "CL_MACHINE=/machine:X64"
|
||||
set "GXX_CMD=x86_64-w64-mingw32-g++"
|
||||
if "!TARGET_ARCH!"=="arm64" (
|
||||
set "CL_MACHINE=/machine:ARM64"
|
||||
set "GXX_CMD=aarch64-w64-mingw32-g++"
|
||||
)
|
||||
|
||||
echo [build] cl /LD /EHsc /O2 plugin.cpp /Fe:!OUTFILE!
|
||||
pushd "%NATIVE_DIR%"
|
||||
cl /nologo /LD /EHsc /O2 plugin.cpp /Fe:"!OUTFILE!" user32.lib gdi32.lib uxtheme.lib dwmapi.lib /link !CL_MACHINE! >nul 2>&1
|
||||
set "CL_STATUS=!errorlevel!"
|
||||
popd
|
||||
if not "!CL_STATUS!"=="0" (
|
||||
echo [build] cl failed, trying !GXX_CMD!...
|
||||
!GXX_CMD! -shared -O2 -s -static -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp" -luser32 -lgdi32 -luxtheme -ldwmapi
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM Clean MSVC build artifacts
|
||||
if exist "%NATIVE_DIR%\plugin.obj" del /f /q "%NATIVE_DIR%\plugin.obj"
|
||||
if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp"
|
||||
if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib"
|
||||
|
||||
echo [ok] !OUTFILE!
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
)
|
||||
|
||||
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.js'"
|
||||
if exist "%PLUGIN_DIR%config.json" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%config.json'"
|
||||
if exist "%PLUGIN_DIR%server.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%server.js'"
|
||||
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip creation failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if defined PLUGIN_SIGN_KEY (
|
||||
where bun >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
set "SIGN_SCRIPT=%PLUGIN_DIR%..\..\Overlord-Server\scripts\plugin-sign.ts"
|
||||
if exist "!SIGN_SCRIPT!" (
|
||||
echo [sign] Signing with key: %PLUGIN_SIGN_KEY%
|
||||
bun run "!SIGN_SCRIPT!" --key "%PLUGIN_SIGN_KEY%" "%ZIP_OUT%"
|
||||
) else (
|
||||
echo [warn] plugin-sign.ts not found, skipping signing
|
||||
)
|
||||
) else (
|
||||
echo [warn] bun not found, skipping signing
|
||||
)
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,269 @@
|
||||
.chat-wrap {
|
||||
max-width: 720px;
|
||||
margin: 30px auto;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 24px;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.chat-sub {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7a8d;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.chat-card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
/* Config panel */
|
||||
.chat-config-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #9aa4b2;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.chat-config-toggle i {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.chat-config-toggle.collapsed i {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.chat-config-body {
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
|
||||
.chat-config-body.hidden {
|
||||
max-height: 0 !important;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-grid label {
|
||||
font-size: 13px;
|
||||
color: #8892a2;
|
||||
}
|
||||
|
||||
.chat-field {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.chat-field:focus {
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.chat-checkboxes {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-checkboxes label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
color: #b0b8c8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-config-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.chat-btn {
|
||||
padding: 8px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
}
|
||||
|
||||
.chat-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-btn-blue {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chat-btn-blue:hover:not(:disabled) {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.chat-btn-red {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chat-btn-red:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.chat-btn-muted {
|
||||
background: #262c3a;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.chat-btn-muted:hover:not(:disabled) {
|
||||
background: #303848;
|
||||
}
|
||||
|
||||
/* Status */
|
||||
.chat-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #6b7a8d;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.chat-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #555;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-status-dot.open {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* Messages area */
|
||||
.chat-messages {
|
||||
min-height: 250px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.chat-messages:empty::before {
|
||||
content: "No messages yet";
|
||||
color: #444d5e;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
max-width: 80%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-msg.outgoing {
|
||||
background: #1e3a5f;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-msg.incoming {
|
||||
background: #2a2d38;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-msg-sender {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.outgoing .chat-msg-sender {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.incoming .chat-msg-sender {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.chat-msg-text {
|
||||
color: #dce1ea;
|
||||
}
|
||||
|
||||
.chat-msg-time {
|
||||
font-size: 10px;
|
||||
color: #505a6a;
|
||||
text-align: right;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Input area */
|
||||
.chat-input-area {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-msg-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #2a3346;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #3a4a60;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Chat</title>
|
||||
<link rel="stylesheet" href="/plugins/chat/assets/chat.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="chat-wrap">
|
||||
<header class="chat-header">
|
||||
<h1><i class="fa-solid fa-comments"></i> Chat</h1>
|
||||
<p class="chat-sub">Two-way chat with target user (Windows only)</p>
|
||||
</header>
|
||||
|
||||
<!-- Config Panel -->
|
||||
<section class="chat-card chat-config" id="config-panel">
|
||||
<div class="chat-config-toggle" id="config-toggle">
|
||||
<span>Configuration</span>
|
||||
<i class="fa-solid fa-chevron-down" id="config-chevron"></i>
|
||||
</div>
|
||||
<div class="chat-config-body" id="config-body">
|
||||
<div class="chat-grid">
|
||||
<label>Client ID</label>
|
||||
<input id="client-id" type="text" class="chat-field" placeholder="paste client ID" />
|
||||
|
||||
<label>Your Name</label>
|
||||
<input id="operator-name" type="text" class="chat-field" value="Operator" />
|
||||
|
||||
<label>Target Name</label>
|
||||
<input id="target-name" type="text" class="chat-field" value="User" />
|
||||
|
||||
<label>Window Title</label>
|
||||
<input id="window-title" type="text" class="chat-field" value="Chat" />
|
||||
|
||||
<label>Options</label>
|
||||
<div class="chat-checkboxes">
|
||||
<label><input type="checkbox" id="opt-closable" checked /> Closable</label>
|
||||
<label><input type="checkbox" id="opt-ontop" /> Always on Top</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-config-actions">
|
||||
<button id="btn-open" class="chat-btn chat-btn-blue">Open Chat</button>
|
||||
<button id="btn-close" class="chat-btn chat-btn-red" disabled>Close Chat</button>
|
||||
<button id="btn-clear" class="chat-btn chat-btn-muted">Clear History</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="chat-status" id="status">
|
||||
<span class="chat-status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Chat not opened</span>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<section class="chat-card chat-messages" id="messages"></section>
|
||||
|
||||
<!-- Input -->
|
||||
<section class="chat-input-area">
|
||||
<input type="text" id="msg-input" class="chat-field chat-msg-field" placeholder="Type a message..." disabled />
|
||||
<button id="btn-send" class="chat-btn chat-btn-blue" disabled>Send</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/chat/assets/chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,208 @@
|
||||
(() => {
|
||||
const PLUGIN_ID = "chat";
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const operatorNameIn = document.getElementById("operator-name");
|
||||
const targetNameIn = document.getElementById("target-name");
|
||||
const windowTitleIn = document.getElementById("window-title");
|
||||
const optClosable = document.getElementById("opt-closable");
|
||||
const optOnTop = document.getElementById("opt-ontop");
|
||||
const btnOpen = document.getElementById("btn-open");
|
||||
const btnClose = document.getElementById("btn-close");
|
||||
const btnClear = document.getElementById("btn-clear");
|
||||
const statusDot = document.getElementById("status-dot");
|
||||
const statusText = document.getElementById("status-text");
|
||||
const messagesEl = document.getElementById("messages");
|
||||
const msgInput = document.getElementById("msg-input");
|
||||
const btnSend = document.getElementById("btn-send");
|
||||
const configToggle = document.getElementById("config-toggle");
|
||||
const configBody = document.getElementById("config-body");
|
||||
|
||||
let chatOpen = false;
|
||||
let sseStream = null;
|
||||
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
|
||||
function getClientId() {
|
||||
return clientIdInput.value.trim();
|
||||
}
|
||||
|
||||
function setStatus(open) {
|
||||
chatOpen = open;
|
||||
statusDot.className = "chat-status-dot" + (open ? " open" : "");
|
||||
statusText.textContent = open ? "Chat is open" : "Chat not opened";
|
||||
btnOpen.disabled = open;
|
||||
btnClose.disabled = !open;
|
||||
msgInput.disabled = !open;
|
||||
btnSend.disabled = !open;
|
||||
if (open) msgInput.focus();
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function appendMessage(sender, text, direction, timestamp) {
|
||||
const isOut = direction === "to_target";
|
||||
const div = document.createElement("div");
|
||||
div.className = "chat-msg " + (isOut ? "outgoing" : "incoming");
|
||||
|
||||
const senderDiv = document.createElement("div");
|
||||
senderDiv.className = "chat-msg-sender";
|
||||
senderDiv.textContent = sender;
|
||||
|
||||
const textDiv = document.createElement("div");
|
||||
textDiv.className = "chat-msg-text";
|
||||
textDiv.textContent = text;
|
||||
|
||||
const timeDiv = document.createElement("div");
|
||||
timeDiv.className = "chat-msg-time";
|
||||
timeDiv.textContent = formatTime(timestamp || Date.now());
|
||||
|
||||
div.appendChild(senderDiv);
|
||||
div.appendChild(textDiv);
|
||||
div.appendChild(timeDiv);
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
async function rpc(method, rpcParams) {
|
||||
const res = await fetch(`/api/plugins/${PLUGIN_ID}/rpc`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ method, params: rpcParams }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function sendPluginEvent(clientId, event, payload) {
|
||||
const res = await fetch(
|
||||
`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) {
|
||||
const t = await res.text();
|
||||
throw new Error(`Event failed: ${res.status} ${t}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
const cid = getClientId();
|
||||
if (!cid) return;
|
||||
const { ok, result } = await rpc("get_history", { clientId: cid });
|
||||
if (!ok || !result) return;
|
||||
messagesEl.innerHTML = "";
|
||||
for (const m of result) {
|
||||
appendMessage(m.sender, m.text, m.direction, m.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
function connectSSE() {
|
||||
if (sseStream) sseStream.close();
|
||||
sseStream = new EventSource(`/api/plugins/${PLUGIN_ID}/stream`);
|
||||
|
||||
sseStream.addEventListener("new_message", (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.clientId !== getClientId()) return;
|
||||
appendMessage(data.sender, data.text, data.direction, data.timestamp);
|
||||
});
|
||||
|
||||
sseStream.addEventListener("chat_status", (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.clientId !== getClientId()) return;
|
||||
setStatus(data.status === "opened");
|
||||
});
|
||||
|
||||
sseStream.addEventListener("history_cleared", (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.clientId !== getClientId()) return;
|
||||
messagesEl.innerHTML = "";
|
||||
});
|
||||
}
|
||||
|
||||
// --- Event handlers ---
|
||||
|
||||
btnOpen.addEventListener("click", async () => {
|
||||
const cid = getClientId();
|
||||
if (!cid) { alert("Enter a Client ID first."); return; }
|
||||
try {
|
||||
await sendPluginEvent(cid, "open_chat", {
|
||||
operatorName: operatorNameIn.value.trim() || "Operator",
|
||||
targetName: targetNameIn.value.trim() || "User",
|
||||
title: windowTitleIn.value.trim() || "Chat",
|
||||
closable: optClosable.checked,
|
||||
alwaysOnTop: optOnTop.checked,
|
||||
});
|
||||
setStatus(true);
|
||||
loadHistory();
|
||||
connectSSE();
|
||||
} catch (err) {
|
||||
alert("Failed to open chat: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
btnClose.addEventListener("click", async () => {
|
||||
const cid = getClientId();
|
||||
if (!cid) return;
|
||||
try {
|
||||
await sendPluginEvent(cid, "close_chat", {});
|
||||
setStatus(false);
|
||||
} catch (err) {
|
||||
alert("Failed to close chat: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
btnSend.addEventListener("click", async () => {
|
||||
const cid = getClientId();
|
||||
const text = msgInput.value.trim();
|
||||
if (!cid || !text) return;
|
||||
|
||||
const sender = operatorNameIn.value.trim() || "Operator";
|
||||
msgInput.value = "";
|
||||
msgInput.focus();
|
||||
|
||||
try {
|
||||
await rpc("store_message", { clientId: cid, sender, text });
|
||||
await sendPluginEvent(cid, "chat_message", { from: sender, text });
|
||||
} catch (err) {
|
||||
appendMessage("System", "Failed to send: " + err.message, "from_target", Date.now());
|
||||
}
|
||||
});
|
||||
|
||||
msgInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
btnSend.click();
|
||||
}
|
||||
});
|
||||
|
||||
btnClear.addEventListener("click", async () => {
|
||||
const cid = getClientId();
|
||||
if (!cid) return;
|
||||
await rpc("clear_history", { clientId: cid });
|
||||
});
|
||||
|
||||
configToggle.addEventListener("click", () => {
|
||||
configToggle.classList.toggle("collapsed");
|
||||
configBody.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
// --- Init ---
|
||||
|
||||
if (getClientId()) {
|
||||
loadHistory();
|
||||
connectSSE();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* Chat Plugin — spawns a Win32 chat window on the target (Windows only).
|
||||
*
|
||||
* Build (MSVC):
|
||||
* cl /LD /EHsc /O2 plugin.cpp /Fe:chat-windows-amd64.dll user32.lib gdi32.lib uxtheme.lib dwmapi.lib
|
||||
*
|
||||
* Build (MinGW):
|
||||
* x86_64-w64-mingw32-g++ -shared -O2 -o chat-windows-amd64.dll plugin.cpp -luser32 -lgdi32 -luxtheme -ldwmapi
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#define EXPORT extern "C" __declspec(dllexport)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <uxtheme.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Constants */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define WM_CHAT_APPEND (WM_APP + 1)
|
||||
#define WM_CHAT_CLOSE (WM_APP + 2)
|
||||
|
||||
#define IDC_LOG 101
|
||||
#define IDC_INPUT 102
|
||||
#define IDC_SEND 103
|
||||
|
||||
static const char *WND_CLASS_NAME = "OverlordChatWnd";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Chat configuration */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
struct ChatConfig {
|
||||
char operatorName[128];
|
||||
char targetName[128];
|
||||
char title[256];
|
||||
bool closable;
|
||||
bool alwaysOnTop;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Global state */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static host_callback_t g_callback = nullptr;
|
||||
static CRITICAL_SECTION g_cs;
|
||||
static bool g_cs_init = false;
|
||||
static char g_client_id[256] = {0};
|
||||
static ChatConfig g_config = {"Operator", "User", "Chat", true, false};
|
||||
static HWND g_hwnd = NULL;
|
||||
static HWND g_hwnd_log = NULL;
|
||||
static HWND g_hwnd_input = NULL;
|
||||
static HWND g_hwnd_send = NULL;
|
||||
static HANDLE g_thread = NULL;
|
||||
static bool g_class_reg = false;
|
||||
static HFONT g_font = NULL;
|
||||
static HBRUSH g_bg_brush = NULL;
|
||||
static HBRUSH g_log_brush = NULL;
|
||||
static HBRUSH g_input_brush = NULL;
|
||||
static WNDPROC g_orig_input_proc = NULL;
|
||||
static HINSTANCE g_hInstance = NULL;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DllMain */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)reserved;
|
||||
if (reason == DLL_PROCESS_ATTACH) g_hInstance = hDll;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* JSON helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static std::string json_extract(const char *json, int len, const char *key) {
|
||||
if (!json || len <= 0) return "";
|
||||
std::string hay(json, (size_t)len);
|
||||
std::string needle = std::string("\"") + key + "\":\"";
|
||||
auto pos = hay.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
needle = std::string("\"") + key + "\": \"";
|
||||
pos = hay.find(needle);
|
||||
}
|
||||
if (pos == std::string::npos) return "";
|
||||
pos += needle.size();
|
||||
std::string result;
|
||||
while (pos < hay.size() && hay[pos] != '"') {
|
||||
if (hay[pos] == '\\' && pos + 1 < hay.size()) {
|
||||
pos++;
|
||||
switch (hay[pos]) {
|
||||
case '"': result += '"'; break;
|
||||
case '\\': result += '\\'; break;
|
||||
case 'n': result += '\n'; break;
|
||||
case 'r': result += '\r'; break;
|
||||
case 't': result += '\t'; break;
|
||||
default: result += hay[pos]; break;
|
||||
}
|
||||
} else {
|
||||
result += hay[pos];
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool json_extract_bool(const char *json, int len, const char *key, bool def) {
|
||||
if (!json || len <= 0) return def;
|
||||
std::string hay(json, (size_t)len);
|
||||
std::string needle = std::string("\"") + key + "\":";
|
||||
auto pos = hay.find(needle);
|
||||
if (pos == std::string::npos) return def;
|
||||
pos += needle.size();
|
||||
while (pos < hay.size() && hay[pos] == ' ') pos++;
|
||||
if (pos >= hay.size()) return def;
|
||||
return hay[pos] == 't';
|
||||
}
|
||||
|
||||
static std::string json_escape(const std::string &s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 16);
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if ((unsigned char)c >= 0x20) out += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* send_event — call host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
host_callback_t cb = g_callback;
|
||||
if (!cb) return;
|
||||
int elen = event ? (int)strlen(event) : 0;
|
||||
int plen = payload ? (int)strlen(payload) : 0;
|
||||
cb(event, (uintptr_t)elen, payload, (uintptr_t)plen);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Chat window helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void append_to_log(const char *text) {
|
||||
if (!g_hwnd_log) return;
|
||||
int len = GetWindowTextLengthA(g_hwnd_log);
|
||||
SendMessageA(g_hwnd_log, EM_SETSEL, (WPARAM)len, (LPARAM)len);
|
||||
SendMessageA(g_hwnd_log, EM_REPLACESEL, FALSE, (LPARAM)text);
|
||||
SendMessageA(g_hwnd_log, EM_SCROLLCARET, 0, 0);
|
||||
}
|
||||
|
||||
static void do_send_message() {
|
||||
char buf[4096];
|
||||
int len = GetWindowTextA(g_hwnd_input, buf, sizeof(buf));
|
||||
if (len <= 0) return;
|
||||
SetWindowTextA(g_hwnd_input, "");
|
||||
|
||||
std::string text(buf, (size_t)len);
|
||||
std::string display = std::string(g_config.targetName) + ": " + text + "\r\n";
|
||||
append_to_log(display.c_str());
|
||||
|
||||
std::string payload = "{\"from\":\"" + json_escape(g_config.targetName) +
|
||||
"\",\"text\":\"" + json_escape(text) + "\"}";
|
||||
send_event("chat_message", payload.c_str());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Input subclass — Enter key sends message */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static LRESULT CALLBACK InputSubclassProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
if (msg == WM_KEYDOWN && wp == VK_RETURN) {
|
||||
do_send_message();
|
||||
return 0;
|
||||
}
|
||||
return CallWindowProcA(g_orig_input_proc, hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Window procedure */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static LRESULT CALLBACK ChatWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
switch (msg) {
|
||||
|
||||
case WM_CREATE: {
|
||||
g_bg_brush = CreateSolidBrush(RGB(30, 33, 40));
|
||||
g_log_brush = CreateSolidBrush(RGB(18, 20, 26));
|
||||
g_input_brush = CreateSolidBrush(RGB(35, 38, 48));
|
||||
g_font = CreateFontA(-15, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, "Segoe UI");
|
||||
|
||||
g_hwnd_log = CreateWindowExA(
|
||||
0, "EDIT", "",
|
||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL,
|
||||
0, 0, 0, 0, hwnd, (HMENU)(uintptr_t)IDC_LOG, g_hInstance, NULL);
|
||||
|
||||
SetWindowTheme(g_hwnd_log, L"DarkMode_Explorer", NULL);
|
||||
|
||||
g_hwnd_input = CreateWindowExA(
|
||||
0, "EDIT", "",
|
||||
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL | WS_TABSTOP,
|
||||
0, 0, 0, 0, hwnd, (HMENU)(uintptr_t)IDC_INPUT, g_hInstance, NULL);
|
||||
|
||||
SetWindowTheme(g_hwnd_input, L"DarkMode_Explorer", NULL);
|
||||
|
||||
g_hwnd_send = CreateWindowExA(
|
||||
0, "BUTTON", "Send",
|
||||
WS_CHILD | WS_VISIBLE | BS_OWNERDRAW | WS_TABSTOP,
|
||||
0, 0, 0, 0, hwnd, (HMENU)(uintptr_t)IDC_SEND, g_hInstance, NULL);
|
||||
|
||||
SendMessageA(g_hwnd_log, WM_SETFONT, (WPARAM)g_font, TRUE);
|
||||
SendMessageA(g_hwnd_input, WM_SETFONT, (WPARAM)g_font, TRUE);
|
||||
SendMessageA(g_hwnd_send, WM_SETFONT, (WPARAM)g_font, TRUE);
|
||||
|
||||
g_orig_input_proc = (WNDPROC)SetWindowLongPtrA(
|
||||
g_hwnd_input, GWLP_WNDPROC, (LONG_PTR)InputSubclassProc);
|
||||
|
||||
if (!g_config.closable) {
|
||||
HMENU sys = GetSystemMenu(hwnd, FALSE);
|
||||
if (sys) EnableMenuItem(sys, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_SIZE: {
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
int w = rc.right, h = rc.bottom;
|
||||
int pad = 10, inputH = 32, btnW = 65, gap = 8;
|
||||
|
||||
MoveWindow(g_hwnd_log, pad, pad, w - 2 * pad, h - inputH - 3 * pad, TRUE);
|
||||
MoveWindow(g_hwnd_input, pad, h - inputH - pad, w - btnW - 2 * pad - gap, inputH, TRUE);
|
||||
MoveWindow(g_hwnd_send, w - btnW - pad, h - inputH - pad, btnW, inputH, TRUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_GETMINMAXINFO: {
|
||||
MINMAXINFO *mmi = (MINMAXINFO *)lp;
|
||||
mmi->ptMinTrackSize.x = 300;
|
||||
mmi->ptMinTrackSize.y = 250;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_COMMAND:
|
||||
if (LOWORD(wp) == IDC_SEND && HIWORD(wp) == BN_CLICKED) {
|
||||
do_send_message();
|
||||
SetFocus(g_hwnd_input);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CTLCOLOREDIT: {
|
||||
HDC hdc = (HDC)wp;
|
||||
HWND ctrl = (HWND)lp;
|
||||
SetTextColor(hdc, RGB(220, 225, 234));
|
||||
if (ctrl == g_hwnd_log) {
|
||||
SetBkColor(hdc, RGB(18, 20, 26));
|
||||
return (LRESULT)g_log_brush;
|
||||
}
|
||||
SetBkColor(hdc, RGB(35, 38, 48));
|
||||
return (LRESULT)g_input_brush;
|
||||
}
|
||||
|
||||
case WM_CTLCOLORSTATIC: {
|
||||
HDC hdc = (HDC)wp;
|
||||
SetTextColor(hdc, RGB(220, 225, 234));
|
||||
SetBkColor(hdc, RGB(18, 20, 26));
|
||||
return (LRESULT)g_log_brush;
|
||||
}
|
||||
|
||||
case WM_ERASEBKGND: {
|
||||
HDC hdc = (HDC)wp;
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
FillRect(hdc, &rc, g_bg_brush);
|
||||
return 1;
|
||||
}
|
||||
|
||||
case WM_DRAWITEM: {
|
||||
DRAWITEMSTRUCT *dis = (DRAWITEMSTRUCT *)lp;
|
||||
if (dis->CtlID == IDC_SEND) {
|
||||
COLORREF col = (dis->itemState & ODS_SELECTED)
|
||||
? RGB(29, 78, 216)
|
||||
: RGB(37, 99, 235);
|
||||
HBRUSH br = CreateSolidBrush(col);
|
||||
FillRect(dis->hDC, &dis->rcItem, br);
|
||||
DeleteObject(br);
|
||||
|
||||
HPEN pen = CreatePen(PS_SOLID, 1, RGB(30, 64, 175));
|
||||
HPEN oldPen = (HPEN)SelectObject(dis->hDC, pen);
|
||||
HBRUSH oldBr = (HBRUSH)SelectObject(dis->hDC, GetStockObject(NULL_BRUSH));
|
||||
RoundRect(dis->hDC, dis->rcItem.left, dis->rcItem.top,
|
||||
dis->rcItem.right, dis->rcItem.bottom, 6, 6);
|
||||
SelectObject(dis->hDC, oldPen);
|
||||
SelectObject(dis->hDC, oldBr);
|
||||
DeleteObject(pen);
|
||||
|
||||
SetTextColor(dis->hDC, RGB(255, 255, 255));
|
||||
SetBkMode(dis->hDC, TRANSPARENT);
|
||||
SelectObject(dis->hDC, g_font);
|
||||
DrawTextA(dis->hDC, "Send", -1, &dis->rcItem,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_CHAT_APPEND: {
|
||||
char *text = (char *)lp;
|
||||
if (text) {
|
||||
append_to_log(text);
|
||||
free(text);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_CHAT_CLOSE:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
|
||||
case WM_CLOSE:
|
||||
if (!g_config.closable) return 0;
|
||||
send_event("chat_closed", "{}");
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
|
||||
case WM_DESTROY:
|
||||
if (g_orig_input_proc && g_hwnd_input) {
|
||||
SetWindowLongPtrA(g_hwnd_input, GWLP_WNDPROC, (LONG_PTR)g_orig_input_proc);
|
||||
g_orig_input_proc = NULL;
|
||||
}
|
||||
g_hwnd_log = NULL;
|
||||
g_hwnd_input = NULL;
|
||||
g_hwnd_send = NULL;
|
||||
g_hwnd = NULL;
|
||||
if (g_font) { DeleteObject(g_font); g_font = NULL; }
|
||||
if (g_bg_brush) { DeleteObject(g_bg_brush); g_bg_brush = NULL; }
|
||||
if (g_log_brush) { DeleteObject(g_log_brush); g_log_brush = NULL; }
|
||||
if (g_input_brush) { DeleteObject(g_input_brush); g_input_brush = NULL; }
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProcA(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Window thread */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static DWORD WINAPI WindowThreadProc(LPVOID) {
|
||||
if (!g_class_reg) {
|
||||
WNDCLASSEXA wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wc.lpfnWndProc = ChatWndProc;
|
||||
wc.hInstance = g_hInstance;
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
wc.lpszClassName = WND_CLASS_NAME;
|
||||
wc.hbrBackground = NULL;
|
||||
if (RegisterClassExA(&wc)) g_class_reg = true;
|
||||
else return 1;
|
||||
}
|
||||
|
||||
int screenW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int screenH = GetSystemMetrics(SM_CYSCREEN);
|
||||
int winW = 420, winH = 520;
|
||||
int x = (screenW - winW) / 2;
|
||||
int y = (screenH - winH) / 2;
|
||||
|
||||
DWORD style = WS_OVERLAPPEDWINDOW;
|
||||
if (!g_config.closable) {
|
||||
style &= ~WS_SYSMENU;
|
||||
style |= WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME;
|
||||
}
|
||||
|
||||
g_hwnd = CreateWindowExA(
|
||||
g_config.alwaysOnTop ? WS_EX_TOPMOST : 0,
|
||||
WND_CLASS_NAME, g_config.title,
|
||||
style,
|
||||
x, y, winW, winH,
|
||||
NULL, NULL, g_hInstance, NULL);
|
||||
|
||||
if (!g_hwnd) return 1;
|
||||
|
||||
BOOL darkTitleBar = TRUE;
|
||||
if (FAILED(DwmSetWindowAttribute(g_hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&darkTitleBar, sizeof(darkTitleBar)))) {
|
||||
DwmSetWindowAttribute(g_hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
|
||||
&darkTitleBar, sizeof(darkTitleBar));
|
||||
}
|
||||
|
||||
ShowWindow(g_hwnd, SW_SHOW);
|
||||
UpdateWindow(g_hwnd);
|
||||
SetForegroundWindow(g_hwnd);
|
||||
|
||||
send_event("chat_opened", "{}");
|
||||
|
||||
MSG msg;
|
||||
while (GetMessageA(&msg, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Open / close helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void close_chat_window() {
|
||||
if (g_hwnd) {
|
||||
PostMessageA(g_hwnd, WM_CHAT_CLOSE, 0, 0);
|
||||
}
|
||||
if (g_thread) {
|
||||
if (WaitForSingleObject(g_thread, 5000) == WAIT_TIMEOUT) {
|
||||
TerminateThread(g_thread, 0);
|
||||
}
|
||||
CloseHandle(g_thread);
|
||||
g_thread = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void open_chat_window() {
|
||||
close_chat_window();
|
||||
g_thread = CreateThread(NULL, 0, WindowThreadProc, NULL, 0, NULL);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin ABI exports */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime() {
|
||||
return "cpp";
|
||||
}
|
||||
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
|
||||
if (!g_cs_init) {
|
||||
InitializeCriticalSection(&g_cs);
|
||||
g_cs_init = true;
|
||||
}
|
||||
|
||||
std::string cid = json_extract(hostInfo, hostInfoLen, "clientId");
|
||||
strncpy(g_client_id, cid.c_str(), sizeof(g_client_id) - 1);
|
||||
g_client_id[sizeof(g_client_id) - 1] = '\0';
|
||||
|
||||
fprintf(stderr, "[chat] loaded, clientId=%s\n", g_client_id);
|
||||
send_event("ready", "{\"message\":\"chat plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
std::string ev(event, (size_t)eventLen);
|
||||
std::string pl(payload ? payload : "", payload ? (size_t)payloadLen : 0u);
|
||||
|
||||
if (ev == "open_chat") {
|
||||
std::string opName = json_extract(pl.c_str(), (int)pl.size(), "operatorName");
|
||||
std::string tgName = json_extract(pl.c_str(), (int)pl.size(), "targetName");
|
||||
std::string title = json_extract(pl.c_str(), (int)pl.size(), "title");
|
||||
bool closable = json_extract_bool(pl.c_str(), (int)pl.size(), "closable", true);
|
||||
bool onTop = json_extract_bool(pl.c_str(), (int)pl.size(), "alwaysOnTop", false);
|
||||
|
||||
EnterCriticalSection(&g_cs);
|
||||
if (!opName.empty()) strncpy(g_config.operatorName, opName.c_str(), sizeof(g_config.operatorName) - 1);
|
||||
if (!tgName.empty()) strncpy(g_config.targetName, tgName.c_str(), sizeof(g_config.targetName) - 1);
|
||||
if (!title.empty()) strncpy(g_config.title, title.c_str(), sizeof(g_config.title) - 1);
|
||||
g_config.closable = closable;
|
||||
g_config.alwaysOnTop = onTop;
|
||||
LeaveCriticalSection(&g_cs);
|
||||
|
||||
open_chat_window();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "chat_message") {
|
||||
std::string from = json_extract(pl.c_str(), (int)pl.size(), "from");
|
||||
std::string text = json_extract(pl.c_str(), (int)pl.size(), "text");
|
||||
if (from.empty()) from = g_config.operatorName;
|
||||
std::string display = from + ": " + text + "\r\n";
|
||||
|
||||
if (g_hwnd) {
|
||||
char *dup = _strdup(display.c_str());
|
||||
PostMessageA(g_hwnd, WM_CHAT_APPEND, 0, (LPARAM)dup);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "close_chat") {
|
||||
if (g_hwnd) send_event("chat_closed", "{}");
|
||||
close_chat_window();
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[chat] unhandled event: %s\n", ev.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload() {
|
||||
fprintf(stderr, "[chat] unloading\n");
|
||||
close_chat_window();
|
||||
g_callback = nullptr;
|
||||
g_client_id[0] = '\0';
|
||||
if (g_cs_init) {
|
||||
DeleteCriticalSection(&g_cs);
|
||||
g_cs_init = false;
|
||||
}
|
||||
if (g_class_reg) {
|
||||
UnregisterClassA(WND_CLASS_NAME, g_hInstance);
|
||||
g_class_reg = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export default {
|
||||
setup(ctx) {
|
||||
ctx.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id TEXT NOT NULL,
|
||||
sender TEXT NOT NULL,
|
||||
direction TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
try {
|
||||
ctx.db.exec(
|
||||
`CREATE INDEX IF NOT EXISTS msg_by_client ON messages(client_id, timestamp)`
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
onEvent(ctx, clientId, event, payload) {
|
||||
if (event === "chat_message") {
|
||||
const ts = Date.now();
|
||||
ctx.db
|
||||
.prepare(
|
||||
`INSERT INTO messages(client_id, sender, direction, text, timestamp) VALUES (?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
clientId,
|
||||
payload.from || "Unknown",
|
||||
"from_target",
|
||||
payload.text || "",
|
||||
ts
|
||||
);
|
||||
ctx.broadcast("new_message", {
|
||||
clientId,
|
||||
sender: payload.from || "Unknown",
|
||||
direction: "from_target",
|
||||
text: payload.text || "",
|
||||
timestamp: ts,
|
||||
});
|
||||
}
|
||||
if (event === "chat_opened") {
|
||||
ctx.broadcast("chat_status", { clientId, status: "opened" });
|
||||
}
|
||||
if (event === "chat_closed") {
|
||||
ctx.broadcast("chat_status", { clientId, status: "closed" });
|
||||
}
|
||||
},
|
||||
|
||||
rpc: {
|
||||
get_history(ctx, params) {
|
||||
return ctx.db
|
||||
.prepare(
|
||||
`SELECT * FROM messages WHERE client_id = ? ORDER BY timestamp ASC LIMIT 500`
|
||||
)
|
||||
.all(params.clientId);
|
||||
},
|
||||
|
||||
store_message(ctx, params) {
|
||||
const ts = Date.now();
|
||||
ctx.db
|
||||
.prepare(
|
||||
`INSERT INTO messages(client_id, sender, direction, text, timestamp) VALUES (?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(params.clientId, params.sender, "to_target", params.text, ts);
|
||||
ctx.broadcast("new_message", {
|
||||
clientId: params.clientId,
|
||||
sender: params.sender,
|
||||
direction: "to_target",
|
||||
text: params.text,
|
||||
timestamp: ts,
|
||||
});
|
||||
return { ok: true, timestamp: ts };
|
||||
},
|
||||
|
||||
clear_history(ctx, params) {
|
||||
ctx.db
|
||||
.prepare(`DELETE FROM messages WHERE client_id = ?`)
|
||||
.run(params.clientId);
|
||||
ctx.broadcast("history_cleared", { clientId: params.clientId });
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user