feat(memory): Tap auf Memory-Bubble oeffnet Detail+Edit-Modal in der App (Etappen 2+3)
Stefans naechste Wunsch-Etappe — komplettes Edit eines Memory-Eintrags
aus der App heraus, inkl. Anhang-Upload, ohne Diagnostic-Browser
auszuklappen.
Backend-Fundament (Phase A):
- Brain bekommt GET /memory/get/{id} fuer Einzel-Lookup mit allen Feldern
- RVS ALLOWED_TYPES um brain_request + brain_response erweitert
- Bridge implementiert generischen RVS-Brain-Proxy:
payload {requestId, method, path, body|bodyBase64, contentType}
→ ruft Brain-HTTP-API → broadcastet brain_response {requestId,
status, json|text|base64+contentType}. Damit kann die App
beliebige Brain-Endpoints ueber RVS adressieren — nicht nur Memory.
App-Service (Phase B):
- services/brainApi.ts: Promise-basierter Client. _send() schickt
brain_request mit requestId, _ensureListener() filtert die passende
brain_response. Methoden: getMemory, listMemories, searchText,
searchSemantic, saveMemory, updateMemory, deleteMemory,
uploadAttachment (Base64), deleteAttachment, getAttachmentBytes.
App-UI (Phasen C+D):
- components/MemoryDetailModal.tsx: Modal mit zwei Modi.
- Read: Titel, Type, Category, Tags, voller Content, Anhang-Liste
(Tap = Bild im Vollbild oder Datei-Info), Stift-Icon → Edit.
- Edit: Titel/Content/Category/Tags/Pinned editierbar, Save via
brainApi.updateMemory.
- DocumentPicker + RNFS.readFile(base64) → uploadAttachment(...).
- Anhang loeschen, kompletter Memory loeschen (mit Alert-confirm).
- ChatScreen: TouchableOpacity-Wrapper um die memorySaved-Bubble,
Tap setzt memoryDetailId → Modal oeffnet. Hint im Footer
"tippen für Details" wenn die Bubble eine ID hat.
Etappen 4 (Notizen-Inbox neben Lupe) + 5 (Memory-Editor in App-
Settings) folgen — diese nutzen die gleiche MemoryDetailModal-
Komponente, sind also schnell aufgesetzt sobald 2+3 verifiziert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1824,6 +1824,72 @@ class ARIABridge:
|
||||
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
|
||||
return
|
||||
|
||||
elif msg_type == "brain_request":
|
||||
# Generischer RVS-Proxy fuer die Brain-HTTP-API.
|
||||
# payload: {requestId, method, path, body?, bodyBase64?, contentType?}
|
||||
# - method: GET | POST | PATCH | DELETE
|
||||
# - path: z.B. "/memory/list" oder "/memory/get/<id>"
|
||||
# - body: JSON-Objekt (wird als JSON encoded)
|
||||
# - bodyBase64: rohe Bytes als Base64 (fuer Upload mit contentType)
|
||||
# - contentType: default application/json
|
||||
# Antwort als brain_response {requestId, status, json?, base64?}.
|
||||
req_id = payload.get("requestId") or ""
|
||||
method = (payload.get("method") or "GET").upper()
|
||||
path = payload.get("path") or ""
|
||||
if not req_id or not path or not path.startswith("/"):
|
||||
logger.warning("[rvs] brain_request ungueltig: %r", payload)
|
||||
return
|
||||
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
|
||||
url = brain_url.rstrip("/") + path
|
||||
headers: dict[str, str] = {}
|
||||
data: Optional[bytes] = None
|
||||
ct = payload.get("contentType") or "application/json"
|
||||
if payload.get("bodyBase64"):
|
||||
try:
|
||||
data = base64.b64decode(payload["bodyBase64"])
|
||||
except Exception:
|
||||
data = None
|
||||
if data is not None:
|
||||
headers["Content-Type"] = ct
|
||||
elif payload.get("body") is not None:
|
||||
data = json.dumps(payload["body"]).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
logger.info("[rvs] brain_request %s %s (%d Byte)", method, path, len(data or b""))
|
||||
|
||||
def _do_call():
|
||||
try:
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
return r.status, r.read(), r.headers.get("Content-Type", "")
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
body = e.read()
|
||||
except Exception:
|
||||
body = b""
|
||||
return e.code, body, e.headers.get("Content-Type", "") if e.headers else ""
|
||||
except Exception as exc:
|
||||
return None, str(exc).encode("utf-8"), "text/plain"
|
||||
|
||||
status, body_bytes, response_ct = await asyncio.get_event_loop().run_in_executor(None, _do_call)
|
||||
out: dict = {"requestId": req_id, "status": status or 0}
|
||||
if response_ct and "json" in response_ct:
|
||||
try:
|
||||
out["json"] = json.loads(body_bytes.decode("utf-8", errors="ignore"))
|
||||
except Exception:
|
||||
out["text"] = body_bytes.decode("utf-8", errors="ignore")[:2000]
|
||||
elif response_ct and "text" in response_ct:
|
||||
out["text"] = body_bytes.decode("utf-8", errors="ignore")[:4000]
|
||||
else:
|
||||
# Binaer (z.B. attachment-download) → base64 zurueck
|
||||
out["base64"] = base64.b64encode(body_bytes).decode("ascii")
|
||||
out["contentType"] = response_ct or "application/octet-stream"
|
||||
await self._send_to_rvs({
|
||||
"type": "brain_response",
|
||||
"payload": out,
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
return
|
||||
|
||||
elif msg_type == "file_list_request":
|
||||
# App fragt die Liste aller /shared/uploads/-Dateien an.
|
||||
logger.info("[rvs] file_list_request von App")
|
||||
|
||||
Reference in New Issue
Block a user