mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 02:00:09 +08:00
* feat(plugin): add webui metadata schema for plugins * feat(dashboard): serve plugin webui with scoped asset tokens * feat(dashboard): add plugin webui page and extension entry actions * test(dashboard): cover plugin webui auth and asset routing * fix(dashboard): use aiofiles for non-blocking plugin webui assets * fix(dashboard): streamline JWT extraction and validation for plugin webui paths * fix(dashboard): harden plugin webui bridge and auth cookie security * fix(dashboard): restore plugin webui bridge under sandbox iframe * refactor(dashboard): apply plugin webui review improvements * docs: 补充插件 WebUI 开发指南 * fix(plugin-webui): 统一 WebUI title 契约并修复桥接行为 * docs: 更新插件 WebUI 开发指南 * fix * feat: Introduce Plugin Pages feature - Added support for plugins to expose Dashboard pages via a `pages/` directory. - Updated `PluginDetailPage.vue` to include a button for opening plugin pages. - Refactored `useExtensionPage.js` to remove the deprecated `openPluginWebUI` function. - Updated documentation to replace references from "Plugin WebUI" to "Plugin Pages". - Created new documentation for Plugin Pages detailing structure, examples, and API usage. - Removed the old Plugin WebUI documentation. - Updated tests to reflect changes from Plugin WebUI to Plugin Pages, ensuring proper functionality and security checks. * feat: 增强插件页面功能,添加返回按钮逻辑并更新测试用例 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Soulter <905617992@qq.com> Co-authored-by: Weilong Liao <37870767+Soulter@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
182 lines
4.4 KiB
JavaScript
182 lines
4.4 KiB
JavaScript
(function attachAstrBotPluginPageBridge() {
|
|
const CHANNEL = "astrbot-plugin-page";
|
|
const SELF_ORIGIN = window.location.origin;
|
|
const pendingRequests = new Map();
|
|
const sseHandlers = new Map();
|
|
let requestCounter = 0;
|
|
let subscriptionCounter = 0;
|
|
let context = null;
|
|
let parentOrigin = null;
|
|
let resolveReady;
|
|
const readyPromise = new Promise((resolve) => {
|
|
resolveReady = resolve;
|
|
});
|
|
|
|
function getTargetOrigin() {
|
|
if (typeof parentOrigin === "string" && parentOrigin && parentOrigin !== "null") {
|
|
return parentOrigin;
|
|
}
|
|
if (SELF_ORIGIN !== "null") {
|
|
return SELF_ORIGIN;
|
|
}
|
|
return "*";
|
|
}
|
|
|
|
function isAllowedParentOrigin(origin) {
|
|
if (typeof origin !== "string" || !origin) {
|
|
return false;
|
|
}
|
|
if (parentOrigin) {
|
|
return origin === parentOrigin;
|
|
}
|
|
if (SELF_ORIGIN === "null") {
|
|
return true;
|
|
}
|
|
return origin === SELF_ORIGIN;
|
|
}
|
|
|
|
function send(kind, payload) {
|
|
window.parent.postMessage(
|
|
{
|
|
channel: CHANNEL,
|
|
kind,
|
|
...(payload || {}),
|
|
},
|
|
getTargetOrigin(),
|
|
);
|
|
}
|
|
|
|
function makeRequest(action, payload) {
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter += 1;
|
|
const requestId = `plugin_req_${requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
send("request", {
|
|
requestId,
|
|
action,
|
|
...(payload || {}),
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseMaybeJson(value) {
|
|
if (typeof value !== "string") {
|
|
return value;
|
|
}
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
window.addEventListener("message", (event) => {
|
|
if (event.source !== window.parent) {
|
|
return;
|
|
}
|
|
if (!isAllowedParentOrigin(event.origin)) {
|
|
return;
|
|
}
|
|
if (!parentOrigin) {
|
|
parentOrigin = event.origin;
|
|
}
|
|
|
|
const message = event.data;
|
|
if (!message || message.channel !== CHANNEL) {
|
|
return;
|
|
}
|
|
|
|
if (message.kind === "context") {
|
|
context = message.context || null;
|
|
if (resolveReady) {
|
|
resolveReady(context);
|
|
resolveReady = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.kind === "response") {
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (!pending) {
|
|
return;
|
|
}
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.ok) {
|
|
pending.resolve(message.data);
|
|
} else {
|
|
pending.reject(new Error(message.error || "Plugin bridge request failed."));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.kind === "sse_message") {
|
|
const handlers = sseHandlers.get(message.subscriptionId);
|
|
if (handlers?.onMessage) {
|
|
handlers.onMessage({
|
|
raw: message.data,
|
|
parsed: parseMaybeJson(message.data),
|
|
lastEventId: message.lastEventId,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.kind === "sse_state") {
|
|
const handlers = sseHandlers.get(message.subscriptionId);
|
|
if (message.state === "open" && handlers?.onOpen) {
|
|
handlers.onOpen();
|
|
}
|
|
if (message.state === "error" && handlers?.onError) {
|
|
handlers.onError();
|
|
}
|
|
}
|
|
});
|
|
|
|
window.AstrBotPluginPage = {
|
|
ready() {
|
|
return readyPromise;
|
|
},
|
|
getContext() {
|
|
return context;
|
|
},
|
|
apiGet(endpoint, params) {
|
|
return makeRequest("api:get", { endpoint, params });
|
|
},
|
|
apiPost(endpoint, body) {
|
|
return makeRequest("api:post", { endpoint, body });
|
|
},
|
|
upload(endpoint, file) {
|
|
return makeRequest("files:upload", {
|
|
endpoint,
|
|
file,
|
|
fileName: file?.name || "upload.bin",
|
|
});
|
|
},
|
|
download(endpoint, params, filename) {
|
|
return makeRequest("files:download", { endpoint, params, filename });
|
|
},
|
|
async subscribeSSE(endpoint, handlers, params) {
|
|
subscriptionCounter += 1;
|
|
const subscriptionId = `plugin_sse_${subscriptionCounter}`;
|
|
sseHandlers.set(subscriptionId, handlers || {});
|
|
try {
|
|
await makeRequest("sse:subscribe", {
|
|
endpoint,
|
|
params,
|
|
subscriptionId,
|
|
});
|
|
return subscriptionId;
|
|
} catch (error) {
|
|
sseHandlers.delete(subscriptionId);
|
|
throw error;
|
|
}
|
|
},
|
|
async unsubscribeSSE(subscriptionId) {
|
|
sseHandlers.delete(subscriptionId);
|
|
return makeRequest("sse:unsubscribe", { subscriptionId });
|
|
},
|
|
};
|
|
|
|
send("ready");
|
|
})();
|