mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
* refactor: migrate to fastapi * structure refactor * fix: pyright fix * refactor: improve error handling and public messages in plugin services * feat(api): refactor API client integration and enhance request handling - Updated API client configuration to use a dedicated HTTP client. - Introduced utility functions for generating options, queries, and form data for API requests. - Refactored multiple API methods to utilize the new utility functions for improved consistency and readability. - Renamed types for clarity and updated import statements accordingly. feat(docs): add script to update OpenAPI JSON from YAML spec - Created a Python script to convert OpenAPI YAML specification to JSON format. - The script supports customizable input and output paths. - Ensured the script handles directory creation for output paths and validates the YAML structure. * fix * feat(auth): implement rate limiting for v1 login endpoint and enhance request handling * Refactor dashboard API routers to use legacy_router for backward compatibility - Changed all instances of dashboard_router to legacy_router across multiple API modules including platform, plugins, providers, sessions, skills, stats, subagents, t2i, tools, updates, and asgi_runtime. - Updated route definitions to ensure existing endpoints remain functional under the new router structure. - Introduced support for Quart request context in asgi_runtime to enhance compatibility with existing Quart-based plugins. - Added a test case to validate the functionality of the new Quart request context handling in plugin extensions. * chore: remove cli test * fix: update dashboard tests for fastapi migration * chore: satisfy ruff checks * fix: update openapi api key scopes * fix: sync config scope chip selection * fix: restore quart dependency * docs: clarify quart plugin api compatibility * docs: update openapi scope documentation * fix: use singular skill openapi scope * fix: hide update service exception details * fix: address fastapi review comments * fix: address dashboard review findings * docs: revert unrelated package deployment changes * docs: update agent api generation guidance * feat: add plugin page web api helpers * docs: add plugin page bridge demo * fix: type plugin upload files * fix: stabilize plugin page uploads * fix: type plugin web request proxy * docs: remove plugin page docs example * fix: authenticate plugin page SSE bridge
289 lines
7.0 KiB
JavaScript
289 lines
7.0 KiB
JavaScript
(function attachAstrBotPluginPageBridge() {
|
|
const CHANNEL = "astrbot-plugin-page";
|
|
const SELF_ORIGIN = window.location.origin;
|
|
const pendingRequests = new Map();
|
|
const sseHandlers = new Map();
|
|
const contextHandlers = new Set();
|
|
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, transfer) {
|
|
window.parent.postMessage(
|
|
{
|
|
channel: CHANNEL,
|
|
kind,
|
|
...(payload || {}),
|
|
},
|
|
getTargetOrigin(),
|
|
transfer || [],
|
|
);
|
|
}
|
|
|
|
function makeRequest(action, payload, transfer) {
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter += 1;
|
|
const requestId = `plugin_req_${requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
send(
|
|
"request",
|
|
{
|
|
requestId,
|
|
action,
|
|
...(payload || {}),
|
|
},
|
|
transfer,
|
|
);
|
|
});
|
|
}
|
|
|
|
function parseMaybeJson(value) {
|
|
if (typeof value !== "string") {
|
|
return value;
|
|
}
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function getByPath(source, key) {
|
|
if (!source || typeof source !== "object" || !key) {
|
|
return undefined;
|
|
}
|
|
|
|
return String(key)
|
|
.split(".")
|
|
.reduce((current, part) => {
|
|
if (!current || typeof current !== "object" || !(part in current)) {
|
|
return undefined;
|
|
}
|
|
return current[part];
|
|
}, source);
|
|
}
|
|
|
|
function translate(key, fallback) {
|
|
const locale = context?.locale;
|
|
const messages = context?.i18n;
|
|
const locales = [locale, "zh-CN", "en-US"].filter(Boolean);
|
|
let value;
|
|
for (const candidateLocale of locales) {
|
|
value = getByPath(messages?.[candidateLocale], key);
|
|
if (value !== undefined && value !== null) {
|
|
break;
|
|
}
|
|
}
|
|
if (value === undefined || value === null) {
|
|
return fallback || "";
|
|
}
|
|
return typeof value === "string" ? value : String(value);
|
|
}
|
|
|
|
function notifyContextHandlers() {
|
|
contextHandlers.forEach((handler) => {
|
|
try {
|
|
handler(context);
|
|
} catch (error) {
|
|
console.error("AstrBotPluginPage context handler failed:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyContext(nextContext) {
|
|
if (!nextContext || typeof nextContext !== "object") {
|
|
return;
|
|
}
|
|
context = {
|
|
...(context || {}),
|
|
...nextContext,
|
|
};
|
|
if (typeof nextContext.isDark === "boolean") {
|
|
document.documentElement.setAttribute(
|
|
"data-theme",
|
|
nextContext.isDark ? "dark" : "light",
|
|
);
|
|
}
|
|
if (resolveReady) {
|
|
resolveReady(context);
|
|
resolveReady = null;
|
|
}
|
|
notifyContextHandlers();
|
|
}
|
|
|
|
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") {
|
|
applyContext(message.context);
|
|
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),
|
|
eventType: message.eventType || "message",
|
|
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;
|
|
},
|
|
getLocale() {
|
|
return context?.locale || "zh-CN";
|
|
},
|
|
getI18n() {
|
|
return context?.i18n || {};
|
|
},
|
|
t(key, fallback) {
|
|
return translate(key, fallback);
|
|
},
|
|
onContext(handler) {
|
|
if (typeof handler !== "function") {
|
|
return () => {};
|
|
}
|
|
contextHandlers.add(handler);
|
|
if (context) {
|
|
handler(context);
|
|
}
|
|
return () => {
|
|
contextHandlers.delete(handler);
|
|
};
|
|
},
|
|
__setInitialContext(nextContext) {
|
|
applyContext(nextContext);
|
|
},
|
|
apiGet(endpoint, params) {
|
|
return makeRequest("api:get", { endpoint, params });
|
|
},
|
|
apiPost(endpoint, body) {
|
|
return makeRequest("api:post", { endpoint, body });
|
|
},
|
|
async upload(endpoint, file) {
|
|
if (!file || typeof file.arrayBuffer !== "function") {
|
|
throw new Error("Missing uploaded file payload.");
|
|
}
|
|
const fileBuffer = await file.arrayBuffer();
|
|
return makeRequest(
|
|
"files:upload",
|
|
{
|
|
endpoint,
|
|
fileName: file?.name || "upload.bin",
|
|
fileType: file?.type || "application/octet-stream",
|
|
fileLastModified:
|
|
typeof file?.lastModified === "number" ? file.lastModified : null,
|
|
fileBuffer,
|
|
},
|
|
[fileBuffer],
|
|
);
|
|
},
|
|
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");
|
|
})();
|