main
py 341 lines 12 KB
Raw
1 import json
2 import os
3 import subprocess
4 import sys
5
6 from helpers.api import ApiHandler, Request, Response
7 from helpers import plugins, files, extension
8 from helpers.localization import Localization
9
10
11 class Plugins(ApiHandler):
12 """
13 Core plugin management API.
14 Actions: get_config, save_config
15 """
16
17 async def process(self, input: dict, request: Request) -> dict | Response:
18 action = input.get("action", "")
19
20 if action == "get_config":
21 return self._get_config(input)
22
23 if action == "get_toggle_status":
24 return self._get_toggle_status(input)
25
26 if action == "list_configs":
27 return self._list_configs(input)
28
29 if action == "delete_config":
30 return self._delete_config(input)
31
32 if action == "delete_plugin":
33 return self._delete_plugin(input)
34
35 if action == "get_default_config":
36 return self._get_default_config(input)
37
38 if action == "save_config":
39 return self._save_config(input)
40
41 if action == "toggle_plugin":
42 return self._toggle_plugin(input)
43
44 if action == "get_doc":
45 return self._get_doc(input)
46
47 if action == "run_execute_script":
48 return self._run_execute_script(input)
49
50 if action == "get_execute_record":
51 return self._get_execute_record(input)
52
53 return Response(status=400, response=f"Unknown action: {action}")
54
55 @extension.extensible
56 def _get_config(self, input: dict) -> dict | Response:
57 plugin_name = input.get("plugin_name", "")
58 project_name = input.get("project_name", "")
59 agent_profile = input.get("agent_profile", "")
60 if not plugin_name:
61 return Response(status=400, response="Missing plugin_name")
62
63 result = plugins.find_plugin_assets(
64 plugins.CONFIG_FILE_NAME,
65 plugin_name=plugin_name,
66 project_name=project_name,
67 agent_profile=agent_profile,
68 only_first=True,
69 )
70 if result:
71 entry = result[0]
72 path = entry.get("path", "")
73 settings = files.read_file_json(path) if path else {}
74 loaded_project_name = entry.get("project_name", "")
75 loaded_agent_profile = entry.get("agent_profile", "")
76 else:
77 settings = plugins.get_plugin_config(plugin_name, agent=None) or {}
78 default_path = files.get_abs_path(
79 plugins.find_plugin_dir(plugin_name), plugins.CONFIG_DEFAULT_FILE_NAME
80 )
81 path = default_path if files.exists(default_path) else ""
82 loaded_project_name = ""
83 loaded_agent_profile = ""
84
85 return {
86 "ok": True,
87 "loaded_path": path,
88 "loaded_project_name": loaded_project_name,
89 "loaded_agent_profile": loaded_agent_profile,
90 "data": settings,
91 }
92
93 @extension.extensible
94 def _get_toggle_status(self, input: dict) -> dict | Response:
95 plugin_name = input.get("plugin_name", "")
96 project_name = input.get("project_name", "")
97 agent_profile = input.get("agent_profile", "")
98 if not plugin_name:
99 return Response(status=400, response="Missing plugin_name")
100
101 meta = plugins.get_plugin_meta(plugin_name)
102 if not meta:
103 return Response(status=404, response="Plugin not found")
104
105 if meta.always_enabled:
106 return {
107 "ok": True,
108 "status": "enabled",
109 "loaded_project_name": project_name,
110 "loaded_agent_profile": agent_profile,
111 "loaded_path": "",
112 }
113
114 result = plugins.find_plugin_assets(
115 plugins.TOGGLE_FILE_PATTERN,
116 plugin_name=plugin_name,
117 project_name=project_name,
118 agent_profile=agent_profile,
119 only_first=True,
120 )
121
122 if result:
123 entry = result[0]
124 path = entry.get("path", "")
125 status = (
126 "enabled" if path.endswith(plugins.ENABLED_FILE_NAME) else "disabled"
127 )
128 return {
129 "ok": True,
130 "status": status,
131 "loaded_project_name": entry.get("project_name", ""),
132 "loaded_agent_profile": entry.get("agent_profile", ""),
133 "loaded_path": path,
134 }
135
136 return {
137 "ok": True,
138 "status": "enabled",
139 "loaded_project_name": "",
140 "loaded_agent_profile": "",
141 "loaded_path": "",
142 }
143
144 @extension.extensible
145 def _list_configs(self, input: dict) -> dict | Response:
146 plugin_name = input.get("plugin_name", "")
147 asset_type = input.get("asset_type", "config")
148 if not plugin_name:
149 return Response(status=400, response="Missing plugin_name")
150
151 configs = plugins.find_plugin_assets(
152 (
153 plugins.CONFIG_FILE_NAME
154 if asset_type == "config"
155 else plugins.TOGGLE_FILE_PATTERN
156 ),
157 plugin_name=plugin_name,
158 project_name="*",
159 agent_profile="*",
160 only_first=False,
161 )
162
163 return {"ok": True, "data": configs}
164
165 @extension.extensible
166 def _delete_config(self, input: dict) -> dict | Response:
167 plugin_name = input.get("plugin_name", "")
168 path = input.get("path", "")
169 if not plugin_name:
170 return Response(status=400, response="Missing plugin_name")
171 if not path:
172 return Response(status=400, response="Missing path")
173
174 configs = plugins.find_plugin_assets(
175 plugins.CONFIG_FILE_NAME,
176 plugin_name=plugin_name,
177 project_name="*",
178 agent_profile="*",
179 only_first=False,
180 )
181 toggles = plugins.find_plugin_assets(
182 plugins.TOGGLE_FILE_PATTERN,
183 plugin_name=plugin_name,
184 project_name="*",
185 agent_profile="*",
186 only_first=False,
187 )
188 allowed_paths = {c.get("path", "") for c in configs + toggles}
189 if path not in allowed_paths:
190 return Response(status=400, response="Invalid path")
191
192 if not files.exists(path):
193 return {"ok": True}
194
195 try:
196 os.remove(path)
197 except Exception as e:
198 return Response(status=500, response=f"Failed to delete config: {str(e)}")
199
200 return {"ok": True}
201
202 @extension.extensible
203 def _delete_plugin(self, input: dict) -> dict | Response:
204 plugin_name = input.get("plugin_name", "")
205 if not plugin_name:
206 return Response(status=400, response="Missing plugin_name")
207 try:
208 plugins.uninstall_plugin(plugin_name)
209 except FileNotFoundError as e:
210 return Response(status=404, response=str(e))
211 except ValueError as e:
212 return Response(status=400, response=str(e))
213 except Exception as e:
214 return Response(status=500, response=f"Failed to delete plugin: {str(e)}")
215 return {"ok": True}
216
217 @extension.extensible
218 def _get_default_config(self, input: dict) -> dict | Response:
219 plugin_name = input.get("plugin_name", "")
220 if not plugin_name:
221 return Response(status=400, response="Missing plugin_name")
222 settings = plugins.get_default_plugin_config(plugin_name)
223 return {"ok": True, "data": settings or {}}
224
225 @extension.extensible
226 def _save_config(self, input: dict) -> dict | Response:
227 plugin_name = input.get("plugin_name", "")
228 project_name = input.get("project_name", "")
229 agent_profile = input.get("agent_profile", "")
230 settings = input.get("settings", {})
231 if not plugin_name:
232 return Response(status=400, response="Missing plugin_name")
233 if not isinstance(settings, dict):
234 return Response(status=400, response="settings must be an object")
235 plugins.save_plugin_config(plugin_name, project_name, agent_profile, settings)
236 return {"ok": True}
237
238 @extension.extensible
239 def _toggle_plugin(self, input: dict) -> dict | Response:
240 plugin_name = input.get("plugin_name", "")
241 enabled = input.get("enabled")
242 project_name = input.get("project_name", "")
243 agent_profile = input.get("agent_profile", "")
244 clear_overrides = bool(input.get("clear_overrides", False))
245
246 if not plugin_name:
247 return Response(status=400, response="Missing plugin_name")
248 if enabled is None:
249 return Response(status=400, response="Missing enabled state")
250
251 try:
252 plugins.toggle_plugin(
253 plugin_name, bool(enabled), project_name, agent_profile, clear_overrides
254 )
255 except ValueError as exc:
256 return Response(status=400, response=str(exc))
257 return {"ok": True}
258
259 @extension.extensible
260 def _get_doc(self, input: dict) -> dict | Response:
261 plugin_name = input.get("plugin_name", "")
262 doc = input.get("doc", "")
263 if not plugin_name:
264 return Response(status=400, response="Missing plugin_name")
265 if doc not in ("readme", "license"):
266 return Response(status=400, response="doc must be 'readme' or 'license'")
267
268 plugin_dir = plugins.find_plugin_dir(plugin_name)
269 if not plugin_dir:
270 return Response(status=404, response="Plugin not found")
271
272 filename = "README.md" if doc == "readme" else "LICENSE"
273 file_path = files.get_abs_path(plugin_dir, filename)
274 if not files.exists(file_path):
275 return Response(status=404, response=f"{filename} not found")
276
277 return {"ok": True, "content": files.read_file(file_path), "filename": filename}
278
279 @extension.extensible
280 def _run_execute_script(self, input: dict) -> dict | Response:
281 plugin_name = input.get("plugin_name", "")
282 if not plugin_name:
283 return Response(status=400, response="Missing plugin_name")
284
285 plugin_dir = plugins.find_plugin_dir(plugin_name)
286 if not plugin_dir:
287 return Response(status=404, response="Plugin not found")
288
289 execute_script = files.get_abs_path(plugin_dir, "execute.py")
290 if not files.exists(execute_script):
291 return Response(status=404, response="execute.py not found")
292
293 executed_at = Localization.get().now_iso()
294 try:
295 result = subprocess.run(
296 [sys.executable, execute_script],
297 stdout=subprocess.PIPE,
298 stderr=subprocess.STDOUT,
299 text=True,
300 cwd=plugin_dir,
301 timeout=120,
302 )
303 exit_code = result.returncode
304 output = result.stdout or ""
305 except subprocess.TimeoutExpired:
306 exit_code = -1
307 output = "Error: script timed out after 120 seconds"
308 except Exception as e:
309 exit_code = -1
310 output = f"Error: {str(e)}"
311
312 execute_record = {"executed_at": executed_at, "exit_code": exit_code}
313 execute_record_path = plugins.determine_plugin_asset_path(
314 plugin_name, "", "", "execute_record.json"
315 )
316 if execute_record_path:
317 files.write_file(execute_record_path, json.dumps(execute_record))
318
319 return {
320 "ok": exit_code == 0,
321 "output": output,
322 "exit_code": exit_code,
323 "executed_at": executed_at,
324 }
325
326 @extension.extensible
327 def _get_execute_record(self, input: dict) -> dict | Response:
328 plugin_name = input.get("plugin_name", "")
329 if not plugin_name:
330 return Response(status=400, response="Missing plugin_name")
331
332 execute_record_path = plugins.determine_plugin_asset_path(
333 plugin_name, "", "", "execute_record.json"
334 )
335 if execute_record_path and files.exists(execute_record_path):
336 try:
337 data = json.loads(files.read_file(execute_record_path))
338 return {"ok": True, "data": data}
339 except Exception:
340 pass
341 return {"ok": True, "data": None}