gitea_connector.py aktualisiert
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
title: Gitea Connector
|
title: Gitea Connector
|
||||||
author: H5N3RG
|
author: H5N3RG
|
||||||
version: 0.2
|
version: 0.3
|
||||||
license: MIT
|
license: MIT
|
||||||
description: Allows Open WebUI LLMs to read and edit Gitea repositories.
|
description: Allows Open WebUI LLMs to read and edit Gitea repositories.
|
||||||
"""
|
"""
|
||||||
@@ -9,7 +9,7 @@ description: Allows Open WebUI LLMs to read and edit Gitea repositories.
|
|||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class GiteaAPI:
|
class GiteaAPI:
|
||||||
@@ -39,163 +39,280 @@ class GiteaAPI:
|
|||||||
class Tools:
|
class Tools:
|
||||||
|
|
||||||
class Valves(BaseModel):
|
class Valves(BaseModel):
|
||||||
|
GITEA_URL: str = Field(
|
||||||
|
default="", description="Gitea API URL, z.B. https://git.example.com/api/v1"
|
||||||
|
)
|
||||||
|
API_TOKEN: str = Field(default="", description="Gitea API Token")
|
||||||
|
|
||||||
GITEA_URL: str = "https://git.example.com/api/v1"
|
DEFAULT_OWNER: str = Field(
|
||||||
API_TOKEN: str = ""
|
default="", description="Standard-Repository-Owner/Organisation"
|
||||||
|
)
|
||||||
|
DEFAULT_REPO: str = Field(default="", description="Standard-Repository-Name")
|
||||||
|
DEFAULT_BRANCH: str = Field(default="main", description="Standard-Branch")
|
||||||
|
|
||||||
DEFAULT_OWNER: str = "user"
|
ENABLE_READ: bool = Field(default=True, description="Lesen erlauben")
|
||||||
DEFAULT_REPO: str = "repo"
|
ENABLE_WRITE: bool = Field(default=False, description="Schreiben erlauben")
|
||||||
DEFAULT_BRANCH: str = "main"
|
ENABLE_BRANCH: bool = Field(
|
||||||
|
default=True, description="Branch-Erstellung erlauben"
|
||||||
|
)
|
||||||
|
ENABLE_PR: bool = Field(
|
||||||
|
default=True, description="Pull-Request-Erstellung erlauben"
|
||||||
|
)
|
||||||
|
|
||||||
ENABLE_READ: bool = True
|
ALLOWED_PATH_PREFIX: str = Field(
|
||||||
ENABLE_WRITE: bool = False
|
default="",
|
||||||
ENABLE_BRANCH: bool = True
|
description="Erlaubtes Pfad-Präfix für Schreibzugriff (leer = alle Pfade erlaubt)",
|
||||||
ENABLE_PR: bool = True
|
)
|
||||||
|
MAX_FILE_SIZE_KB: int = Field(
|
||||||
ALLOWED_PATH_PREFIX: str = "docs/"
|
default=200, description="Maximale Dateigröße in KB"
|
||||||
|
)
|
||||||
MAX_FILE_SIZE_KB: int = 200
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
# FIX: GiteaAPI wird NICHT mehr hier instanziiert.
|
|
||||||
# Stattdessen wird self.api als Property definiert,
|
|
||||||
# damit die Valve-Werte immer aktuell sind wenn OWUI sie injiziert.
|
|
||||||
|
|
||||||
@property
|
def _get_valves(self, __user__: dict) -> Valves:
|
||||||
def api(self) -> GiteaAPI:
|
"""
|
||||||
"""Lazy property: baut GiteaAPI immer frisch mit den aktuellen Valve-Werten."""
|
Holt die Valves aus __user__ falls von OWUI injiziert,
|
||||||
return GiteaAPI(self.valves.GITEA_URL, self.valves.API_TOKEN)
|
sonst Fallback auf self.valves.
|
||||||
|
Dies ist der Workaround für den OWUI-Bug bei dem Valves
|
||||||
|
nicht korrekt in self.valves landen.
|
||||||
|
"""
|
||||||
|
user_valves = __user__.get("valves") if __user__ else None
|
||||||
|
if user_valves:
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
self.Valves(**user_valves)
|
||||||
|
if isinstance(user_valves, dict)
|
||||||
|
else user_valves
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self.valves
|
||||||
|
|
||||||
|
def _get_api(self, valves: Valves) -> GiteaAPI:
|
||||||
|
return GiteaAPI(valves.GITEA_URL, valves.API_TOKEN)
|
||||||
|
|
||||||
|
def _check_config(self, valves: Valves) -> Optional[str]:
|
||||||
|
if not valves.GITEA_URL:
|
||||||
|
return "❌ Bitte GITEA_URL in den Valve-Einstellungen eintragen."
|
||||||
|
if not valves.API_TOKEN:
|
||||||
|
return "❌ Bitte API_TOKEN in den Valve-Einstellungen eintragen."
|
||||||
|
return None
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# LIST FILES
|
# LIST FILES
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def list_repository_tree(self, path: str = ""):
|
def list_repository_tree(self, path: str = "", __user__: dict = {}):
|
||||||
"""
|
"""
|
||||||
List files in the repository.
|
List files and directories in the Gitea repository.
|
||||||
|
|
||||||
|
:param path: Optional subdirectory path to list. Leave empty for root.
|
||||||
|
:return: List of files and directories with name, path and type.
|
||||||
"""
|
"""
|
||||||
|
valves = self._get_valves(__user__)
|
||||||
|
|
||||||
if not self.valves.ENABLE_READ:
|
if not valves.ENABLE_READ:
|
||||||
return "Reading repository disabled."
|
return "❌ Lesen ist deaktiviert."
|
||||||
|
|
||||||
owner = self.valves.DEFAULT_OWNER
|
err = self._check_config(valves)
|
||||||
repo = self.valves.DEFAULT_REPO
|
if err:
|
||||||
branch = self.valves.DEFAULT_BRANCH
|
return err
|
||||||
|
|
||||||
|
owner = valves.DEFAULT_OWNER
|
||||||
|
repo = valves.DEFAULT_REPO
|
||||||
|
branch = valves.DEFAULT_BRANCH
|
||||||
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/contents/{path}?ref={branch}"
|
endpoint = f"/repos/{owner}/{repo}/contents/{path}?ref={branch}"
|
||||||
|
|
||||||
data = self.api.get(endpoint)
|
try:
|
||||||
|
data = self._get_api(valves).get(endpoint)
|
||||||
return [{"name": f["name"], "path": f["path"], "type": f["type"]} for f in data]
|
return [
|
||||||
|
{"name": f["name"], "path": f["path"], "type": f["type"]} for f in data
|
||||||
|
]
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
return f"❌ API-Fehler: HTTP {e.response.status_code} – {e.response.text[:300]}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Fehler: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# READ FILE
|
# READ FILE
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def read_file(self, path: str):
|
def read_file(self, path: str, __user__: dict = {}):
|
||||||
"""
|
"""
|
||||||
Read a file from the repository.
|
Read the content of a file from the Gitea repository.
|
||||||
|
|
||||||
|
:param path: Path to the file within the repository.
|
||||||
|
:return: The decoded file content as a string.
|
||||||
"""
|
"""
|
||||||
|
valves = self._get_valves(__user__)
|
||||||
|
|
||||||
if not self.valves.ENABLE_READ:
|
if not valves.ENABLE_READ:
|
||||||
return "Reading disabled."
|
return "❌ Lesen ist deaktiviert."
|
||||||
|
|
||||||
owner = self.valves.DEFAULT_OWNER
|
err = self._check_config(valves)
|
||||||
repo = self.valves.DEFAULT_REPO
|
if err:
|
||||||
branch = self.valves.DEFAULT_BRANCH
|
return err
|
||||||
|
|
||||||
|
owner = valves.DEFAULT_OWNER
|
||||||
|
repo = valves.DEFAULT_REPO
|
||||||
|
branch = valves.DEFAULT_BRANCH
|
||||||
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/contents/{path}?ref={branch}"
|
endpoint = f"/repos/{owner}/{repo}/contents/{path}?ref={branch}"
|
||||||
|
|
||||||
data = self.api.get(endpoint)
|
try:
|
||||||
|
data = self._get_api(valves).get(endpoint)
|
||||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
return base64.b64decode(data["content"]).decode("utf-8")
|
||||||
|
except requests.HTTPError as e:
|
||||||
return content
|
return f"❌ API-Fehler: HTTP {e.response.status_code} – {e.response.text[:300]}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Fehler: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# CREATE OR UPDATE FILE
|
# CREATE OR UPDATE FILE
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def create_or_update_file(
|
def create_or_update_file(
|
||||||
self, path: str, content: str, commit_message: str, branch: Optional[str] = None
|
self,
|
||||||
|
path: str,
|
||||||
|
content: str,
|
||||||
|
commit_message: str,
|
||||||
|
branch: Optional[str] = None,
|
||||||
|
__user__: dict = {},
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create or update a file in the repository.
|
Create or update a file in the Gitea repository.
|
||||||
|
|
||||||
|
:param path: Path of the file to create or update.
|
||||||
|
:param content: The new file content as a plain string.
|
||||||
|
:param commit_message: Git commit message.
|
||||||
|
:param branch: Target branch. Uses DEFAULT_BRANCH if omitted.
|
||||||
|
:return: Confirmation of the operation.
|
||||||
"""
|
"""
|
||||||
|
valves = self._get_valves(__user__)
|
||||||
|
|
||||||
if not self.valves.ENABLE_WRITE:
|
if not valves.ENABLE_WRITE:
|
||||||
return "Write access disabled."
|
return "❌ Schreiben ist deaktiviert."
|
||||||
|
|
||||||
if not path.startswith(self.valves.ALLOWED_PATH_PREFIX):
|
if valves.ALLOWED_PATH_PREFIX and not path.startswith(
|
||||||
return "Path not allowed."
|
valves.ALLOWED_PATH_PREFIX
|
||||||
|
):
|
||||||
|
return f"❌ Pfad nicht erlaubt. Erlaubtes Präfix: '{valves.ALLOWED_PATH_PREFIX}'"
|
||||||
|
|
||||||
if len(content) > self.valves.MAX_FILE_SIZE_KB * 1024:
|
if len(content) > valves.MAX_FILE_SIZE_KB * 1024:
|
||||||
return "File too large."
|
return f"❌ Datei zu groß (max. {valves.MAX_FILE_SIZE_KB} KB)."
|
||||||
|
|
||||||
owner = self.valves.DEFAULT_OWNER
|
err = self._check_config(valves)
|
||||||
repo = self.valves.DEFAULT_REPO
|
if err:
|
||||||
branch = branch or self.valves.DEFAULT_BRANCH
|
return err
|
||||||
|
|
||||||
|
owner = valves.DEFAULT_OWNER
|
||||||
|
repo = valves.DEFAULT_REPO
|
||||||
|
target_branch = branch or valves.DEFAULT_BRANCH
|
||||||
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/contents/{path}"
|
endpoint = f"/repos/{owner}/{repo}/contents/{path}"
|
||||||
|
api = self._get_api(valves)
|
||||||
|
|
||||||
# check if file exists
|
|
||||||
sha = None
|
|
||||||
try:
|
try:
|
||||||
existing = self.api.get(f"{endpoint}?ref={branch}")
|
sha = None
|
||||||
sha = existing["sha"]
|
try:
|
||||||
except:
|
existing = api.get(f"{endpoint}?ref={target_branch}")
|
||||||
pass
|
sha = existing["sha"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
encoded = base64.b64encode(content.encode()).decode()
|
encoded = base64.b64encode(content.encode()).decode()
|
||||||
|
payload = {
|
||||||
|
"message": commit_message,
|
||||||
|
"content": encoded,
|
||||||
|
"branch": target_branch,
|
||||||
|
}
|
||||||
|
if sha:
|
||||||
|
payload["sha"] = sha
|
||||||
|
|
||||||
payload = {"message": commit_message, "content": encoded, "branch": branch}
|
api.put(endpoint, payload)
|
||||||
|
action = "aktualisiert" if sha else "erstellt"
|
||||||
if sha:
|
return f"✅ Datei '{path}' erfolgreich {action} (Branch: {target_branch})."
|
||||||
payload["sha"] = sha
|
except requests.HTTPError as e:
|
||||||
|
return f"❌ API-Fehler: HTTP {e.response.status_code} – {e.response.text[:300]}"
|
||||||
return self.api.put(endpoint, payload)
|
except Exception as e:
|
||||||
|
return f"❌ Fehler: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# CREATE BRANCH
|
# CREATE BRANCH
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def create_branch(self, new_branch: str, from_branch: Optional[str] = None):
|
def create_branch(
|
||||||
|
self, new_branch: str, from_branch: Optional[str] = None, __user__: dict = {}
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Create a new branch.
|
Create a new branch in the Gitea repository.
|
||||||
|
|
||||||
|
:param new_branch: Name of the new branch to create.
|
||||||
|
:param from_branch: Source branch. Uses DEFAULT_BRANCH if omitted.
|
||||||
|
:return: Confirmation of the operation.
|
||||||
"""
|
"""
|
||||||
|
valves = self._get_valves(__user__)
|
||||||
|
|
||||||
if not self.valves.ENABLE_BRANCH:
|
if not valves.ENABLE_BRANCH:
|
||||||
return "Branch creation disabled."
|
return "❌ Branch-Erstellung ist deaktiviert."
|
||||||
|
|
||||||
owner = self.valves.DEFAULT_OWNER
|
err = self._check_config(valves)
|
||||||
repo = self.valves.DEFAULT_REPO
|
if err:
|
||||||
|
return err
|
||||||
|
|
||||||
from_branch = from_branch or self.valves.DEFAULT_BRANCH
|
owner = valves.DEFAULT_OWNER
|
||||||
|
repo = valves.DEFAULT_REPO
|
||||||
|
source = from_branch or valves.DEFAULT_BRANCH
|
||||||
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/branches"
|
endpoint = f"/repos/{owner}/{repo}/branches"
|
||||||
|
payload = {"new_branch_name": new_branch, "old_branch_name": source}
|
||||||
|
|
||||||
payload = {"new_branch_name": new_branch, "old_branch_name": from_branch}
|
try:
|
||||||
|
self._get_api(valves).post(endpoint, payload)
|
||||||
return self.api.post(endpoint, payload)
|
return f"✅ Branch '{new_branch}' erfolgreich aus '{source}' erstellt."
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
return f"❌ API-Fehler: HTTP {e.response.status_code} – {e.response.text[:300]}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Fehler: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# CREATE PR
|
# CREATE PR
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def create_pull_request(self, head: str, base: str, title: str, body: str = ""):
|
def create_pull_request(
|
||||||
|
self, head: str, base: str, title: str, body: str = "", __user__: dict = {}
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Create a pull request.
|
Create a pull request in the Gitea repository.
|
||||||
|
|
||||||
|
:param head: Source branch (the branch with your changes).
|
||||||
|
:param base: Target branch (e.g. 'main').
|
||||||
|
:param title: Title of the pull request.
|
||||||
|
:param body: Optional description for the pull request.
|
||||||
|
:return: Confirmation of the operation.
|
||||||
"""
|
"""
|
||||||
|
valves = self._get_valves(__user__)
|
||||||
|
|
||||||
if not self.valves.ENABLE_PR:
|
if not valves.ENABLE_PR:
|
||||||
return "PR creation disabled."
|
return "❌ Pull-Request-Erstellung ist deaktiviert."
|
||||||
|
|
||||||
owner = self.valves.DEFAULT_OWNER
|
err = self._check_config(valves)
|
||||||
repo = self.valves.DEFAULT_REPO
|
if err:
|
||||||
|
return err
|
||||||
|
|
||||||
|
owner = valves.DEFAULT_OWNER
|
||||||
|
repo = valves.DEFAULT_REPO
|
||||||
|
|
||||||
endpoint = f"/repos/{owner}/{repo}/pulls"
|
endpoint = f"/repos/{owner}/{repo}/pulls"
|
||||||
|
|
||||||
payload = {"title": title, "head": head, "base": base, "body": body}
|
payload = {"title": title, "head": head, "base": base, "body": body}
|
||||||
|
|
||||||
return self.api.post(endpoint, payload)
|
try:
|
||||||
|
result = self._get_api(valves).post(endpoint, payload)
|
||||||
|
pr_id = result.get("number", "?")
|
||||||
|
return f"✅ Pull Request #{pr_id} '{title}' erfolgreich erstellt ({head} → {base})."
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
return f"❌ API-Fehler: HTTP {e.response.status_code} – {e.response.text[:300]}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Fehler: {type(e).__name__}: {e}"
|
||||||
|
|||||||
Reference in New Issue
Block a user