mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-04-28 03:30:23 +00:00
Clear plugin cache & add API extension hooks
Add extension hooks and improve plugin cache handling. Changes include: - Add new JSON API extension points and a cache reset handler (extensions/webui/json_api_call_before, json_api_call_after/cache_reset.js) to clear frontend cache when backend cache_reset runs. - Rename webui extension hook directories to use fetch_api_call_* naming. - Refactor webui/js/api.js to normalize API URLs, lazily import ./extensions.js, and call extension hooks for json_api_call_before/error/after and fetch_api_call_before/after using a ctx object. - Rename invalidate_plugin_cache to clear_plugin_cache and call it after plugin install/delete/toggle/config save to keep plugin cache consistent. - Harden plugin name sanitization to replace non-alphanumeric chars with underscores. - Include extensions/**/*.js in jsconfig.json for editor tooling. These changes improve extensibility for API lifecycle events and ensure plugin-related cache is cleared when plugin state changes.
This commit is contained in:
parent
eb0595bb3f
commit
27730153ac
9 changed files with 86 additions and 38 deletions
0
extensions/webui/json_api_call_after/.gitkeep
Normal file
0
extensions/webui/json_api_call_after/.gitkeep
Normal file
14
extensions/webui/json_api_call_after/cache_reset.js
Normal file
14
extensions/webui/json_api_call_after/cache_reset.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { clear } from "/js/cache.js";
|
||||
|
||||
export default async function resetCache(ctx) {
|
||||
try {
|
||||
// clear frontend cache areas when backend caches are cleared via API
|
||||
if (ctx.endpoint == "cache_reset") {
|
||||
for (const area of ctx.data.areas) {
|
||||
clear(area);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
0
extensions/webui/json_api_call_before/.gitkeep
Normal file
0
extensions/webui/json_api_call_before/.gitkeep
Normal file
|
|
@ -71,7 +71,7 @@ class PluginListItem(BaseModel):
|
|||
toggle_state: ToggleState = "disabled"
|
||||
|
||||
|
||||
def invalidate_plugin_cache():
|
||||
def clear_plugin_cache():
|
||||
cache.clear("*(plugins)*")
|
||||
|
||||
|
||||
|
|
@ -189,6 +189,7 @@ def delete_plugin(plugin_name: str):
|
|||
if not files.is_in_dir(plugin_dir, custom_plugins_dir):
|
||||
raise ValueError("Only custom plugins can be deleted")
|
||||
files.delete_dir(plugin_dir)
|
||||
clear_plugin_cache()
|
||||
|
||||
|
||||
def get_plugin_paths(*subpaths: str) -> List[str]:
|
||||
|
|
@ -347,6 +348,7 @@ def toggle_plugin(
|
|||
files.write_file(enabled_file, "")
|
||||
else:
|
||||
files.write_file(disabled_file, "")
|
||||
clear_plugin_cache()
|
||||
|
||||
|
||||
def get_plugin_config(
|
||||
|
|
@ -401,6 +403,7 @@ def save_plugin_config(
|
|||
)
|
||||
if file_path:
|
||||
files.write_file(file_path, json.dumps(settings))
|
||||
clear_plugin_cache()
|
||||
|
||||
|
||||
def find_plugin_asset(
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
"/usr/plugins/*": ["usr/plugins/*"]
|
||||
}
|
||||
},
|
||||
"include": ["webui/**/*.js", "plugins/**/*.js", "usr/plugins/**/*.js"]
|
||||
"include": ["webui/**/*.js", "extensions/**/*.js", "plugins/**/*.js", "usr/plugins/**/*.js"]
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ from helpers import files
|
|||
from helpers.plugins import (
|
||||
META_FILE_NAME,
|
||||
PluginMetadata,
|
||||
invalidate_plugin_cache,
|
||||
clear_plugin_cache,
|
||||
)
|
||||
from helpers import yaml as yaml_helper
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ def _sanitize_plugin_name(name: str) -> str:
|
|||
Converts dots and dashes to underscores for Python import compatibility.
|
||||
Raises ValueError if the name is unsafe for filesystem use."""
|
||||
name = name.strip().strip(".")
|
||||
name = re.sub(r"[-.]", "_", name)
|
||||
name = re.sub(r"[^a-zA-Z0-9]+", "_", name)
|
||||
if not name or not _SAFE_NAME_RE.match(name):
|
||||
raise ValueError(
|
||||
f"Invalid plugin name: '{name}'. "
|
||||
|
|
@ -107,7 +107,7 @@ def install_from_zip(zip_path: str) -> dict:
|
|||
dest = os.path.join(_get_user_plugins_dir(), plugin_name)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
shutil.move(plugin_root, dest)
|
||||
invalidate_plugin_cache()
|
||||
clear_plugin_cache()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
|
@ -147,6 +147,7 @@ def install_from_git(url: str, token: Optional[str] = None) -> dict:
|
|||
except Exception as e:
|
||||
# Cleanup partial clone
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
clear_plugin_cache()
|
||||
raise ValueError(f"Git clone failed: {e}") from e
|
||||
|
||||
try:
|
||||
|
|
@ -154,9 +155,10 @@ def install_from_git(url: str, token: Optional[str] = None) -> dict:
|
|||
except ValueError:
|
||||
# No plugin.yaml — remove cloned repo
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
clear_plugin_cache()
|
||||
raise
|
||||
|
||||
invalidate_plugin_cache()
|
||||
clear_plugin_cache()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,22 @@
|
|||
let _extensionsModule = null;
|
||||
|
||||
async function _getExtensions() {
|
||||
if (!_extensionsModule) _extensionsModule = await import("./extensions.js");
|
||||
return _extensionsModule;
|
||||
}
|
||||
|
||||
async function _shouldCallApiExtensions(apiUrl) {
|
||||
const extensions = await _getExtensions();
|
||||
const excluded = extensions.API_EXTENSION_EXCLUDED_ENDPOINTS;
|
||||
return !(excluded instanceof Set && excluded.has(apiUrl));
|
||||
}
|
||||
|
||||
function _normalizeApiUrl(url) {
|
||||
return url.startsWith("/api/") || url.startsWith("api/")
|
||||
? `/${url.replace(/^\/+/, "")}`
|
||||
: `/api/${url.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a JSON-in JSON-out API endpoint
|
||||
* Data is automatically serialized
|
||||
|
|
@ -6,21 +25,54 @@
|
|||
* @returns {Promise<any>} The JSON response from the API
|
||||
*/
|
||||
export async function callJsonApi(endpoint, data) {
|
||||
const response = await fetchApi(endpoint, {
|
||||
const apiUrl = _normalizeApiUrl(endpoint);
|
||||
|
||||
/** @type {{ endpoint: string, data: any, response: Response | null, result: any, error: Error | null }} */
|
||||
const ctx = {
|
||||
endpoint,
|
||||
data,
|
||||
response: null,
|
||||
result: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
if (await _shouldCallApiExtensions(apiUrl)) {
|
||||
const extensions = await _getExtensions();
|
||||
await extensions.callJsExtensions("json_api_call_before", ctx);
|
||||
}
|
||||
|
||||
const response = await fetchApi(ctx.endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(ctx.data),
|
||||
});
|
||||
ctx.response = response;
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
ctx.error = new Error(error);
|
||||
|
||||
if (await _shouldCallApiExtensions(apiUrl)) {
|
||||
const extensions = await _getExtensions();
|
||||
await extensions.callJsExtensions("json_api_call_error", ctx);
|
||||
}
|
||||
|
||||
if (ctx.error) throw ctx.error;
|
||||
|
||||
return ctx.result;
|
||||
}
|
||||
const jsonResponse = await response.json();
|
||||
return jsonResponse;
|
||||
|
||||
ctx.result = await response.json();
|
||||
|
||||
if (await _shouldCallApiExtensions(apiUrl)) {
|
||||
const extensions = await _getExtensions();
|
||||
await extensions.callJsExtensions("json_api_call_after", ctx);
|
||||
}
|
||||
|
||||
return ctx.result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,25 +83,6 @@ export async function callJsonApi(endpoint, data) {
|
|||
* @returns {Promise<Response>} The fetch response
|
||||
*/
|
||||
export async function fetchApi(url, request) {
|
||||
async function _getExtensions() {
|
||||
try {
|
||||
return await import("./extensions.js");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiUrl
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function _shouldCallApiExtensions(apiUrl) {
|
||||
const extensions = await _getExtensions();
|
||||
if (!extensions) return false;
|
||||
const excluded = extensions.API_EXTENSION_EXCLUDED_ENDPOINTS;
|
||||
return !(excluded instanceof Set && excluded.has(apiUrl));
|
||||
}
|
||||
|
||||
async function _wrap(retry) {
|
||||
// get the CSRF token
|
||||
const token = await getCsrfToken();
|
||||
|
|
@ -64,7 +97,7 @@ export async function fetchApi(url, request) {
|
|||
finalRequest.headers["X-CSRF-Token"] = token;
|
||||
|
||||
// perform the fetch with the updated request
|
||||
const apiUrl = url.startsWith('/api/') || url.startsWith('api/') ? `/${url.replace(/^\/+/, '')}` : `/api/${url.replace(/^\/+/, '')}`;
|
||||
const apiUrl = _normalizeApiUrl(url);
|
||||
|
||||
/** @type {{ url: string, apiUrl: string, request: any, response: Response | null, retry: boolean }} */
|
||||
const ctx = {
|
||||
|
|
@ -77,19 +110,15 @@ export async function fetchApi(url, request) {
|
|||
|
||||
if (await _shouldCallApiExtensions(apiUrl)) {
|
||||
const extensions = await _getExtensions();
|
||||
if (extensions) {
|
||||
await extensions.callJsExtensions("api_call_before", ctx);
|
||||
}
|
||||
await extensions.callJsExtensions("fetch_api_call_before", ctx);
|
||||
}
|
||||
|
||||
const response = ctx.response || await fetch(ctx.apiUrl, ctx.request);
|
||||
const response = ctx.response || (await fetch(ctx.apiUrl, ctx.request));
|
||||
ctx.response = response;
|
||||
|
||||
if (await _shouldCallApiExtensions(apiUrl)) {
|
||||
const extensions = await _getExtensions();
|
||||
if (extensions) {
|
||||
await extensions.callJsExtensions("api_call_after", ctx);
|
||||
}
|
||||
await extensions.callJsExtensions("fetch_api_call_after", ctx);
|
||||
}
|
||||
|
||||
const finalResponse = ctx.response;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue