mirror of
https://github.com/open-webui/oikb.git
synced 2026-07-09 16:00:53 +00:00
enhance: configurable fields, format, JQL for Jira connector
Match ServiceNow's configurability: custom JQL, field selection, markdown/json format, and limit. Supports query string params: jira:PROJ?jql=status=open&fields=summary,status&format=json&limit=50
This commit is contained in:
parent
ff37b83b14
commit
f3ca110358
2 changed files with 88 additions and 16 deletions
|
|
@ -96,7 +96,14 @@ def _resolve_connector(source: str, branch: str | None = None, path: str | None
|
|||
if source.startswith("jira:"):
|
||||
from oikb.connectors.jira import JiraConnector, parse_jira_source
|
||||
parsed = parse_jira_source(source)
|
||||
return JiraConnector(project_key=parsed["project_key"])
|
||||
fields = parsed["fields"].split(",") if parsed.get("fields") else None
|
||||
return JiraConnector(
|
||||
project_key=parsed["project_key"],
|
||||
jql=parsed.get("jql"),
|
||||
fields=fields,
|
||||
fmt=parsed.get("format", "markdown"),
|
||||
limit=int(parsed["limit"]) if parsed.get("limit") else None,
|
||||
)
|
||||
|
||||
if source.startswith("sharepoint:"):
|
||||
from oikb.connectors.sharepoint import SharePointConnector, parse_sharepoint_source
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
"""Jira connector — sync issues from a Jira project to a Knowledge Base.
|
||||
|
||||
Auth via JIRA_URL, JIRA_USER, JIRA_TOKEN env vars.
|
||||
|
||||
Configurable:
|
||||
- project_key: Jira project key
|
||||
- jql: custom JQL query (overrides default project filter)
|
||||
- fields: which fields to export (default: summary, description, status, issuetype)
|
||||
- format: output format — 'markdown' (default) or 'json'
|
||||
- limit: max issues to fetch (default: unlimited)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json as json_mod
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
|
@ -21,6 +28,10 @@ class JiraConnector(BaseConnector):
|
|||
def __init__(
|
||||
self,
|
||||
project_key: str,
|
||||
jql: str | None = None,
|
||||
fields: list[str] | None = None,
|
||||
fmt: str = "markdown",
|
||||
limit: int | None = None,
|
||||
base_url: str | None = None,
|
||||
user: str | None = None,
|
||||
token: str | None = None,
|
||||
|
|
@ -35,6 +46,10 @@ class JiraConnector(BaseConnector):
|
|||
if not self._token:
|
||||
raise ValueError("Jira API token required. Set JIRA_TOKEN env var.")
|
||||
|
||||
self._jql = jql or f"project={self.project_key}"
|
||||
self._fields = fields or ["summary", "description", "status", "issuetype", "priority", "assignee"]
|
||||
self._fmt = fmt
|
||||
self._limit = limit
|
||||
self._http = httpx.Client(
|
||||
base_url=self._base_url,
|
||||
auth=(self._user, self._token) if self._user else None,
|
||||
|
|
@ -45,37 +60,78 @@ class JiraConnector(BaseConnector):
|
|||
|
||||
def build_manifest(self) -> list[ManifestEntry]:
|
||||
entries: list[ManifestEntry] = []
|
||||
api_fields = list(set(self._fields + ["summary", "updated"]))
|
||||
start = 0
|
||||
fetched = 0
|
||||
while True:
|
||||
max_results = 100
|
||||
if self._limit:
|
||||
max_results = min(100, self._limit - fetched)
|
||||
if max_results <= 0:
|
||||
break
|
||||
|
||||
resp = self._http.get(
|
||||
"/rest/api/3/search",
|
||||
params={"jql": f"project={self.project_key}", "startAt": start, "maxResults": 100,
|
||||
"fields": "summary,description,status,updated,issuetype"},
|
||||
params={"jql": self._jql, "startAt": start, "maxResults": max_results,
|
||||
"fields": ",".join(api_fields)},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
for issue in data.get("issues", []):
|
||||
issues = data.get("issues", [])
|
||||
for issue in issues:
|
||||
key = issue["key"]
|
||||
fields = issue["fields"]
|
||||
updated = fields.get("updated", "")
|
||||
checksum = hashlib.sha256(f"{key}:{updated}".encode()).hexdigest()[:16]
|
||||
text = self._format_issue(issue)
|
||||
self._issue_cache[f"{key}.txt"] = text
|
||||
entries.append(ManifestEntry(filename=f"{key}.txt", path="", checksum=checksum, size=len(text.encode())))
|
||||
if start + len(data.get("issues", [])) >= data.get("total", 0):
|
||||
ext = ".json" if self._fmt == "json" else ".md"
|
||||
filename = f"{key}{ext}"
|
||||
self._issue_cache[filename] = text
|
||||
entries.append(ManifestEntry(filename=filename, path="", checksum=checksum, size=len(text.encode())))
|
||||
fetched += len(issues)
|
||||
if start + len(issues) >= data.get("total", 0):
|
||||
break
|
||||
start += len(data.get("issues", []))
|
||||
start += len(issues)
|
||||
entries.sort(key=lambda e: e.display_path)
|
||||
return entries
|
||||
|
||||
def _resolve_field(self, fields: dict, field_name: str) -> str:
|
||||
"""Extract display value from a Jira field."""
|
||||
val = fields.get(field_name)
|
||||
if val is None:
|
||||
return ""
|
||||
if isinstance(val, dict):
|
||||
# ADF document (description)
|
||||
if val.get("type") == "doc":
|
||||
return self._adf_to_text(val)
|
||||
# Object with name (status, issuetype, priority, assignee)
|
||||
return val.get("name", val.get("displayName", val.get("value", str(val))))
|
||||
if isinstance(val, list):
|
||||
return ", ".join(str(v.get("name", v) if isinstance(v, dict) else v) for v in val)
|
||||
return str(val)
|
||||
|
||||
def _format_issue(self, issue: dict) -> str:
|
||||
fields = issue["fields"]
|
||||
key = issue["key"]
|
||||
|
||||
if self._fmt == "json":
|
||||
data = {"key": key}
|
||||
for f in self._fields:
|
||||
data[f] = self._resolve_field(fields, f)
|
||||
return json_mod.dumps(data, indent=2, ensure_ascii=False)
|
||||
|
||||
# Default: markdown
|
||||
summary = fields.get("summary", "")
|
||||
status = fields.get("status", {}).get("name", "")
|
||||
issue_type = fields.get("issuetype", {}).get("name", "")
|
||||
desc = self._adf_to_text(fields.get("description"))
|
||||
lines = [f"# [{key}] {summary}", f"Type: {issue_type}", f"Status: {status}", "", desc]
|
||||
lines = [f"# [{key}] {summary}", ""]
|
||||
for f in self._fields:
|
||||
if f in ("summary", "description"):
|
||||
continue
|
||||
val = self._resolve_field(fields, f)
|
||||
if val:
|
||||
lines.append(f"**{f}**: {val}")
|
||||
desc = self._resolve_field(fields, "description") if "description" in self._fields else ""
|
||||
if desc:
|
||||
lines.extend(["", desc])
|
||||
return "\n".join(lines)
|
||||
|
||||
def _adf_to_text(self, doc: dict | None) -> str:
|
||||
|
|
@ -106,7 +162,16 @@ class JiraConnector(BaseConnector):
|
|||
|
||||
|
||||
def parse_jira_source(source: str) -> dict[str, str | None]:
|
||||
key = source.removeprefix("jira:")
|
||||
if not key:
|
||||
"""Parse jira:<PROJECT_KEY> or jira:<PROJECT_KEY>?jql=...&fields=...&format=..."""
|
||||
raw = source.removeprefix("jira:")
|
||||
parts = raw.split("?", 1)
|
||||
project_key = parts[0]
|
||||
if not project_key:
|
||||
raise ValueError("Invalid Jira source. Expected: jira:<PROJECT_KEY>")
|
||||
return {"project_key": key}
|
||||
result: dict[str, str | None] = {"project_key": project_key}
|
||||
if len(parts) > 1:
|
||||
for param in parts[1].split("&"):
|
||||
if "=" in param:
|
||||
k, v = param.split("=", 1)
|
||||
result[k] = v
|
||||
return result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue