feat(A_memorix)&fix(Docker): 记忆搜索支持平台/用户筛选及界面优化
新增平台和用户账号维度的筛选能力,并对记忆/画像功能进行交互体验优化 前端:MemoryEpisodeManager 与 MemoryProfileManager 组件增加了平台和用户 ID 输入项,人员 ID 调整为可折叠的“高级”入口,新增原始 JSON 切换、人员名称展示,优化了标签文案和列表模式;搜索触发逻辑现已支持基于账号的查询和画像搜索结果的展示。 API客户端:统一记忆 API 的基础路径,请求携带凭证,增加 HTML 回退页面的检测和本地备用地址的重试机制,优化了错误提示信息。 服务端:记忆路由在返回片段和画像时,补充了来自数据库的人员名称字段,新增 /profiles/search 及兼容性接口,并在片段/画像列表接口中支持传入 platform 和 user_id 参数 其他优化:防止 SPA 路由劫持 /api 路径,入口脚本改用 venv 中的 Python,Dockerfile 中将 venv 加入 PATH 并调整了 uv sync 相关参数。
This commit is contained in:
@@ -197,6 +197,9 @@ def _setup_static_files(app: FastAPI):
|
||||
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def serve_spa(full_path: str):
|
||||
if full_path == "api" or full_path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail=t("core.not_found"))
|
||||
|
||||
if not full_path or full_path == "/":
|
||||
response = FileResponse(static_path / "index.html", media_type="text/html")
|
||||
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"
|
||||
|
||||
@@ -9,8 +9,11 @@ from typing import Any, Optional
|
||||
import tomlkit
|
||||
from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, Query, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlmodel import col, select
|
||||
|
||||
from src.A_memorix.host_service import a_memorix_host_service
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import PersonInfo
|
||||
from src.person_info.person_info import resolve_person_id_for_memory
|
||||
from src.services.memory_service import MemorySearchResult, memory_service
|
||||
from src.webui.dependencies import require_auth
|
||||
@@ -298,22 +301,49 @@ async def _episode_list(
|
||||
limit: int,
|
||||
source: str,
|
||||
person_id: str,
|
||||
platform: str,
|
||||
user_id: str,
|
||||
time_start: float | None,
|
||||
time_end: float | None,
|
||||
) -> dict:
|
||||
return await memory_service.episode_admin(
|
||||
clean_person_id = str(person_id or "").strip()
|
||||
if not clean_person_id and str(platform or "").strip() and str(user_id or "").strip():
|
||||
clean_person_id = resolve_person_id_for_memory(
|
||||
platform=str(platform or "").strip(),
|
||||
user_id=str(user_id or "").strip(),
|
||||
strict_known=False,
|
||||
)
|
||||
|
||||
payload = await memory_service.episode_admin(
|
||||
action="list",
|
||||
query=query,
|
||||
limit=limit,
|
||||
source=source,
|
||||
person_id=person_id,
|
||||
person_id=clean_person_id,
|
||||
time_start=time_start,
|
||||
time_end=time_end,
|
||||
)
|
||||
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
|
||||
return payload
|
||||
|
||||
items = []
|
||||
for item in payload["items"]:
|
||||
if not isinstance(item, dict):
|
||||
items.append(item)
|
||||
continue
|
||||
items.append(_enrich_episode_person_name(item))
|
||||
|
||||
payload = dict(payload)
|
||||
payload["items"] = items
|
||||
return payload
|
||||
|
||||
|
||||
async def _episode_get(episode_id: str) -> dict:
|
||||
return await memory_service.episode_admin(action="get", episode_id=episode_id)
|
||||
payload = await memory_service.episode_admin(action="get", episode_id=episode_id)
|
||||
if isinstance(payload, dict) and isinstance(payload.get("episode"), dict):
|
||||
payload = dict(payload)
|
||||
payload["episode"] = _enrich_episode_person_name(payload["episode"])
|
||||
return payload
|
||||
|
||||
|
||||
async def _episode_rebuild(payload: EpisodeRebuildRequest) -> dict:
|
||||
@@ -362,8 +392,118 @@ async def _profile_query(
|
||||
)
|
||||
|
||||
|
||||
def _get_person_name_for_person_id(person_id: str) -> str:
|
||||
clean_person_id = str(person_id or "").strip()
|
||||
if not clean_person_id:
|
||||
return ""
|
||||
try:
|
||||
with get_db_session(auto_commit=False) as session:
|
||||
statement = select(PersonInfo.person_name).where(col(PersonInfo.person_id) == clean_person_id).limit(1)
|
||||
person_name = session.exec(statement).first()
|
||||
return str(person_name or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _enrich_episode_person_name(item: dict) -> dict:
|
||||
enriched = dict(item)
|
||||
item_person_id = str(enriched.get("person_id", "") or "").strip()
|
||||
|
||||
participants = enriched.get("participants")
|
||||
if not item_person_id and isinstance(participants, list):
|
||||
for participant in participants:
|
||||
if isinstance(participant, dict):
|
||||
candidate = str(participant.get("person_id", "") or participant.get("id", "") or "").strip()
|
||||
else:
|
||||
candidate = str(participant or "").strip()
|
||||
if candidate:
|
||||
item_person_id = candidate
|
||||
break
|
||||
|
||||
enriched["person_id"] = item_person_id
|
||||
enriched["person_name"] = _get_person_name_for_person_id(item_person_id)
|
||||
return enriched
|
||||
|
||||
|
||||
async def _profile_list(limit: int) -> dict:
|
||||
return await memory_service.profile_admin(action="list", limit=limit)
|
||||
payload = await memory_service.profile_admin(action="list", limit=limit)
|
||||
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
|
||||
return payload
|
||||
|
||||
items = []
|
||||
for item in payload["items"]:
|
||||
if not isinstance(item, dict):
|
||||
items.append(item)
|
||||
continue
|
||||
enriched = dict(item)
|
||||
person_id = str(enriched.get("person_id", "") or "").strip()
|
||||
enriched["person_name"] = _get_person_name_for_person_id(person_id)
|
||||
items.append(enriched)
|
||||
|
||||
payload = dict(payload)
|
||||
payload["items"] = items
|
||||
return payload
|
||||
|
||||
|
||||
async def _profile_search(
|
||||
*,
|
||||
person_id: str,
|
||||
person_keyword: str,
|
||||
platform: str,
|
||||
user_id: str,
|
||||
limit: int,
|
||||
) -> dict:
|
||||
clean_person_id = str(person_id or "").strip()
|
||||
if not clean_person_id and str(platform or "").strip() and str(user_id or "").strip():
|
||||
clean_person_id = resolve_person_id_for_memory(
|
||||
platform=str(platform or "").strip(),
|
||||
user_id=str(user_id or "").strip(),
|
||||
strict_known=False,
|
||||
)
|
||||
|
||||
payload = await _profile_list(max(limit, 200))
|
||||
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
|
||||
return payload
|
||||
|
||||
keyword = str(person_keyword or "").strip().lower()
|
||||
|
||||
def _matches(item: dict) -> bool:
|
||||
if clean_person_id and str(item.get("person_id", "") or "").strip() != clean_person_id:
|
||||
return False
|
||||
if not keyword:
|
||||
return True
|
||||
|
||||
override = item.get("manual_override")
|
||||
override_text = ""
|
||||
if isinstance(override, dict):
|
||||
override_text = str(override.get("override_text", "") or override.get("text", "") or "")
|
||||
elif isinstance(override, str):
|
||||
override_text = override
|
||||
|
||||
haystack = "\n".join(
|
||||
[
|
||||
str(item.get("person_id", "") or ""),
|
||||
str(item.get("person_name", "") or ""),
|
||||
str(item.get("profile_text", "") or ""),
|
||||
str(item.get("source_note", "") or ""),
|
||||
override_text,
|
||||
]
|
||||
).lower()
|
||||
return keyword in haystack
|
||||
|
||||
items = [item for item in payload["items"] if isinstance(item, dict) and _matches(item)]
|
||||
items = items[:limit]
|
||||
return {
|
||||
"success": True,
|
||||
"items": items,
|
||||
"count": len(items),
|
||||
"query": {
|
||||
"person_id": clean_person_id,
|
||||
"person_keyword": person_keyword,
|
||||
"platform": platform,
|
||||
"user_id": user_id,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _profile_set_override(payload: ProfileOverrideRequest) -> dict:
|
||||
@@ -813,6 +953,8 @@ async def list_memory_episodes(
|
||||
limit: int = Query(20, ge=1, le=200),
|
||||
source: str = Query(""),
|
||||
person_id: str = Query(""),
|
||||
platform: str = Query(""),
|
||||
user_id: str = Query(""),
|
||||
time_start: float | None = Query(None),
|
||||
time_end: float | None = Query(None),
|
||||
):
|
||||
@@ -821,6 +963,8 @@ async def list_memory_episodes(
|
||||
limit=limit,
|
||||
source=source,
|
||||
person_id=person_id,
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
time_start=time_start,
|
||||
time_end=time_end,
|
||||
)
|
||||
@@ -870,6 +1014,23 @@ async def list_memory_profiles(limit: int = Query(50, ge=1, le=200)):
|
||||
return await _profile_list(limit)
|
||||
|
||||
|
||||
@router.get("/profiles/search")
|
||||
async def search_memory_profiles(
|
||||
person_id: str = Query(""),
|
||||
person_keyword: str = Query(""),
|
||||
platform: str = Query(""),
|
||||
user_id: str = Query(""),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
):
|
||||
return await _profile_search(
|
||||
person_id=person_id,
|
||||
person_keyword=person_keyword,
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/profiles/override")
|
||||
async def set_memory_profile_override(payload: ProfileOverrideRequest):
|
||||
return await _profile_set_override(payload)
|
||||
@@ -1289,6 +1450,8 @@ async def compat_list_episodes(
|
||||
limit: int = Query(20, ge=1, le=200),
|
||||
source: str = Query(""),
|
||||
person_id: str = Query(""),
|
||||
platform: str = Query(""),
|
||||
user_id: str = Query(""),
|
||||
time_start: float | None = Query(None),
|
||||
time_end: float | None = Query(None),
|
||||
):
|
||||
@@ -1297,6 +1460,8 @@ async def compat_list_episodes(
|
||||
limit=limit,
|
||||
source=source,
|
||||
person_id=person_id,
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
time_start=time_start,
|
||||
time_end=time_end,
|
||||
)
|
||||
@@ -1346,6 +1511,23 @@ async def compat_profile_list(limit: int = Query(50, ge=1, le=200)):
|
||||
return await _profile_list(limit)
|
||||
|
||||
|
||||
@compat_router.get("/person_profile/search")
|
||||
async def compat_profile_search(
|
||||
person_id: str = Query(""),
|
||||
person_keyword: str = Query(""),
|
||||
platform: str = Query(""),
|
||||
user_id: str = Query(""),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
):
|
||||
return await _profile_search(
|
||||
person_id=person_id,
|
||||
person_keyword=person_keyword,
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@compat_router.post("/person_profile/override")
|
||||
async def compat_set_profile_override(payload: ProfileOverrideRequest):
|
||||
return await _profile_set_override(payload)
|
||||
|
||||
Reference in New Issue
Block a user