feat: add FastAPI manager API compatibility baseline

This commit is contained in:
Tyke Chen
2026-07-20 17:00:13 +08:00
parent 7c58fa37b2
commit 804ddb51f2
140 changed files with 47169 additions and 1 deletions
@@ -0,0 +1,665 @@
from __future__ import annotations
# All interpolated SQL fragments are selected from closed column/table allowlists.
# ruff: noqa: S608
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any
from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
AGENT_COLUMNS = (
"id",
"user_id",
"agent_code",
"agent_name",
"asr_model_id",
"vad_model_id",
"llm_model_id",
"slm_model_id",
"vllm_model_id",
"tts_model_id",
"tts_voice_id",
"tts_language",
"tts_volume",
"tts_rate",
"tts_pitch",
"mem_model_id",
"intent_model_id",
"chat_history_conf",
"system_prompt",
"summary_memory",
"lang_code",
"language",
"sort",
"creator",
"created_at",
"updater",
"updated_at",
)
AGENT_MUTABLE_COLUMNS = frozenset(AGENT_COLUMNS) - {"id", "user_id", "creator", "created_at"}
TEMPLATE_COLUMNS = (
"id",
"agent_code",
"agent_name",
"asr_model_id",
"vad_model_id",
"llm_model_id",
"vllm_model_id",
"tts_model_id",
"tts_voice_id",
"tts_language",
"tts_volume",
"tts_rate",
"tts_pitch",
"mem_model_id",
"intent_model_id",
"chat_history_conf",
"system_prompt",
"summary_memory",
"lang_code",
"language",
"sort",
"creator",
"created_at",
"updater",
"updated_at",
)
class AgentRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
@property
def is_sqlite(self) -> bool:
bind = self.session.get_bind()
return bool(bind is not None and bind.dialect.name == "sqlite")
async def get_agent(self, agent_id: str, *, for_update: bool = False) -> dict[str, Any] | None:
suffix = "" if self.is_sqlite or not for_update else " FOR UPDATE"
return await self.fetch_one(
f"SELECT {', '.join(AGENT_COLUMNS)} FROM ai_agent WHERE id=:id{suffix}", {"id": agent_id}
)
async def check_agent_owner(self, agent_id: str, user_id: int, *, super_admin: bool) -> bool:
if super_admin:
return bool(await self.scalar("SELECT 1 FROM ai_agent WHERE id=:id LIMIT 1", {"id": agent_id}))
return bool(
await self.scalar(
"SELECT 1 FROM ai_agent WHERE id=:id AND user_id=:user_id LIMIT 1",
{"id": agent_id, "user_id": user_id},
)
)
async def list_user_agents(self, user_id: int, keyword: str | None) -> list[dict[str, Any]]:
params: dict[str, Any] = {"user_id": user_id}
where = "a.user_id=:user_id"
if keyword is not None and keyword.strip():
params["keyword"] = f"%{keyword}%"
where += (
" AND (a.agent_name LIKE :keyword"
" OR EXISTS (SELECT 1 FROM ai_device d0 WHERE d0.agent_id=a.id"
" AND d0.user_id=:user_id AND d0.mac_address LIKE :keyword)"
" OR EXISTS (SELECT 1 FROM ai_agent_tag_relation tr0"
" JOIN ai_agent_tag t0 ON t0.id=tr0.tag_id"
" WHERE tr0.agent_id=a.id AND t0.deleted=0 AND t0.tag_name LIKE :keyword))"
)
return await self.fetch_all(
"SELECT a.*, mt.model_name AS tts_model_name, ml.model_name AS llm_model_name,"
" mv.model_name AS vllm_model_name, COALESCE(tv.name, vc.name) AS tts_voice_name,"
" (SELECT MAX(d.last_connected_at) FROM ai_device d WHERE d.agent_id=a.id) AS last_connected_at,"
" (SELECT COUNT(*) FROM ai_device d WHERE d.agent_id=a.id) AS device_count"
" FROM ai_agent a"
" LEFT JOIN ai_model_config mt ON mt.id=a.tts_model_id"
" LEFT JOIN ai_model_config ml ON ml.id=a.llm_model_id"
" LEFT JOIN ai_model_config mv ON mv.id=a.vllm_model_id"
" LEFT JOIN ai_tts_voice tv ON tv.id=a.tts_voice_id"
" LEFT JOIN ai_voice_clone vc ON vc.id=a.tts_voice_id"
f" WHERE {where} ORDER BY a.created_at DESC",
params,
)
async def list_admin_agents(
self, page: int, limit: int, order_field: str, ascending: bool
) -> tuple[list[dict[str, Any]], int]:
allowed = {"agent_name", "created_at", "updated_at", "sort", "id"}
selected = order_field if order_field in allowed else "agent_name"
direction = "ASC" if ascending else "DESC"
total = int(await self.scalar("SELECT COUNT(*) FROM ai_agent") or 0)
query = (
f"SELECT {', '.join(AGENT_COLUMNS)} FROM ai_agent "
f"ORDER BY {selected} {direction} LIMIT :limit OFFSET :offset"
)
rows = await self.fetch_all(
query,
{"limit": limit, "offset": (page - 1) * limit},
)
return rows, total
async def insert_agent(self, values: Mapping[str, Any]) -> int:
columns = [column for column in AGENT_COLUMNS if column in values]
placeholders = ", ".join(f":{column}" for column in columns)
return await self.execute(
f"INSERT INTO ai_agent ({', '.join(columns)}) VALUES ({placeholders})",
{column: values[column] for column in columns},
)
async def update_agent(self, agent_id: str, values: Mapping[str, Any], *, include_null: bool = False) -> int:
selected = {
key: value
for key, value in values.items()
if key in AGENT_MUTABLE_COLUMNS and (include_null or value is not None)
}
if not selected:
return 0
assignments = ", ".join(f"{column}=:{column}" for column in selected)
return await self.execute(
f"UPDATE ai_agent SET {assignments} WHERE id=:agent_id",
{**selected, "agent_id": agent_id},
)
async def get_agent_plugins(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT m.id,m.agent_id,m.plugin_id,m.param_info,p.provider_code"
" FROM ai_agent_plugin_mapping m LEFT JOIN ai_model_provider p ON p.id=m.plugin_id"
" WHERE m.agent_id=:agent_id ORDER BY m.id ASC",
{"agent_id": agent_id},
)
async def replace_plugins(self, agent_id: str, plugins: Sequence[Mapping[str, Any]]) -> None:
existing = await self.fetch_all(
"SELECT id,plugin_id FROM ai_agent_plugin_mapping WHERE agent_id=:agent_id",
{"agent_id": agent_id},
)
by_plugin = {str(row["plugin_id"]): int(row["id"]) for row in existing}
incoming = {str(item.get("plugin_id") or "") for item in plugins}
remove_ids = [int(row["id"]) for row in existing if str(row["plugin_id"]) not in incoming]
if remove_ids:
statement = text("DELETE FROM ai_agent_plugin_mapping WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
await self.execute(statement, {"ids": remove_ids})
for item in plugins:
plugin_id = str(item.get("plugin_id") or "")
params = {"agent_id": agent_id, "plugin_id": plugin_id, "param_info": item.get("param_info") or "{}"}
if plugin_id in by_plugin:
await self.execute(
"UPDATE ai_agent_plugin_mapping SET param_info=:param_info WHERE id=:id",
{"id": by_plugin[plugin_id], **params},
)
else:
await self.execute(
"INSERT INTO ai_agent_plugin_mapping (id,agent_id,plugin_id,param_info)"
" VALUES (:id,:agent_id,:plugin_id,:param_info)",
{"id": int(item["id"]), **params},
)
async def delete_plugins(self, agent_id: str) -> int:
return await self.execute("DELETE FROM ai_agent_plugin_mapping WHERE agent_id=:id", {"id": agent_id})
async def get_context_provider(self, agent_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id,agent_id,context_providers,creator,created_at,updater,updated_at"
" FROM ai_agent_context_provider WHERE agent_id=:id LIMIT 1",
{"id": agent_id},
)
async def upsert_context_provider(self, agent_id: str, encoded: str, new_id: str) -> None:
existing = await self.get_context_provider(agent_id)
if existing:
await self.execute(
"UPDATE ai_agent_context_provider SET context_providers=:value WHERE id=:id",
{"value": encoded, "id": existing["id"]},
)
else:
await self.execute(
"INSERT INTO ai_agent_context_provider (id,agent_id,context_providers)"
" VALUES (:id,:agent_id,:value)",
{"id": new_id, "agent_id": agent_id, "value": encoded},
)
async def get_correct_word_ids(self, agent_id: str) -> list[str]:
rows = await self.fetch_all(
"SELECT file_id FROM ai_agent_correct_word_mapping WHERE agent_id=:id", {"id": agent_id}
)
return [str(row["file_id"]) for row in rows]
async def replace_correct_words(
self, agent_id: str, file_ids: Sequence[str], user_id: int, now: datetime, ids: Sequence[str]
) -> None:
await self.execute("DELETE FROM ai_agent_correct_word_mapping WHERE agent_id=:id", {"id": agent_id})
await self.execute_many(
"INSERT INTO ai_agent_correct_word_mapping"
" (id,agent_id,file_id,creator,created_at,updater,updated_at)"
" VALUES (:id,:agent_id,:file_id,:user_id,:now,:user_id,:now)",
[
{"id": mapping_id, "agent_id": agent_id, "file_id": file_id, "user_id": user_id, "now": now}
for mapping_id, file_id in zip(ids, file_ids, strict=True)
],
)
async def get_agent_tags(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT t.id,t.tag_name,t.sort,r.sort AS relation_sort"
" FROM ai_agent_tag t JOIN ai_agent_tag_relation r ON t.id=r.tag_id"
" WHERE r.agent_id=:id AND t.deleted=0 ORDER BY r.sort ASC,r.created_at ASC",
{"id": agent_id},
)
async def get_tags_for_agents(self, agent_ids: Sequence[str]) -> list[dict[str, Any]]:
if not agent_ids:
return []
statement = text(
"SELECT t.id,t.tag_name,r.agent_id,r.sort AS relation_sort"
" FROM ai_agent_tag t JOIN ai_agent_tag_relation r ON t.id=r.tag_id"
" WHERE r.agent_id IN :ids AND t.deleted=0 ORDER BY r.sort ASC,r.created_at ASC"
).bindparams(bindparam("ids", expanding=True))
return await self.fetch_all(statement, {"ids": list(agent_ids)})
async def list_tags(self) -> list[dict[str, Any]]:
return await self.fetch_all("SELECT id,tag_name,sort FROM ai_agent_tag WHERE deleted=0 ORDER BY sort ASC")
async def get_tag(self, tag_id: str) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_agent_tag WHERE id=:id", {"id": tag_id})
async def find_active_tag_by_name(self, tag_name: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_agent_tag WHERE tag_name=:name AND deleted=0 LIMIT 1", {"name": tag_name}
)
async def find_any_tag_by_name(self, tag_name: str) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_agent_tag WHERE tag_name=:name LIMIT 1", {"name": tag_name})
async def insert_tag(self, values: Mapping[str, Any]) -> int:
return await self.execute(
"INSERT INTO ai_agent_tag"
" (id,tag_name,sort,deleted,creator,created_at,updater,updated_at)"
" VALUES (:id,:tag_name,:sort,:deleted,:creator,:created_at,:updater,:updated_at)",
values,
)
async def soft_delete_tag(self, tag_id: str, now: datetime) -> int:
return await self.execute(
"UPDATE ai_agent_tag SET deleted=1,updated_at=:now WHERE id=:id",
{"id": tag_id, "now": now},
)
async def replace_tag_relations(self, agent_id: str, relations: Sequence[Mapping[str, Any]]) -> None:
await self.execute("DELETE FROM ai_agent_tag_relation WHERE agent_id=:id", {"id": agent_id})
await self.execute_many(
"INSERT INTO ai_agent_tag_relation"
" (id,agent_id,tag_id,sort,creator,created_at,updater,updated_at)"
" VALUES (:id,:agent_id,:tag_id,:sort,:creator,:created_at,:updater,:updated_at)",
relations,
)
async def get_model_config(self, model_id: str | None) -> dict[str, Any] | None:
if not model_id:
return None
return await self.fetch_one("SELECT * FROM ai_model_config WHERE id=:id", {"id": model_id})
async def get_default_llm_config(self) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_model_config WHERE model_type='LLM' AND is_enabled=1"
" ORDER BY is_default DESC,sort ASC LIMIT 1"
)
async def get_model_provider(self, provider_id: str) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_model_provider WHERE id=:id", {"id": provider_id})
async def get_timbre(self, timbre_id: str | None) -> dict[str, Any] | None:
if not timbre_id:
return None
row = await self.fetch_one("SELECT * FROM ai_tts_voice WHERE id=:id", {"id": timbre_id})
if row is None:
row = await self.fetch_one("SELECT * FROM ai_voice_clone WHERE id=:id", {"id": timbre_id})
return row
async def find_timbre_by_voice_code(self, model_id: str, voice_code: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_tts_voice WHERE tts_model_id=:model_id AND tts_voice=:voice LIMIT 1",
{"model_id": model_id, "voice": voice_code},
)
async def get_device_by_mac(self, mac_address: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_device WHERE mac_address=:mac ORDER BY id DESC LIMIT 1", {"mac": mac_address}
)
async def get_agent_by_device_mac(self, mac_address: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {', '.join('a.' + column for column in AGENT_COLUMNS)}"
" FROM ai_device d LEFT JOIN ai_agent a ON d.agent_id=a.id"
" WHERE d.mac_address=:mac ORDER BY d.id DESC LIMIT 1",
{"mac": mac_address},
)
async def update_device_connection(self, device_id: str, now: datetime) -> int:
return await self.execute(
"UPDATE ai_device SET last_connected_at=:now WHERE id=:id", {"id": device_id, "now": now}
)
async def insert_chat_audio(self, audio_id: str, audio: bytes) -> int:
return await self.execute(
"INSERT INTO ai_agent_chat_audio (id,audio) VALUES (:id,:audio)", {"id": audio_id, "audio": audio}
)
async def get_chat_audio(self, audio_id: str) -> bytes | None:
value = await self.scalar("SELECT audio FROM ai_agent_chat_audio WHERE id=:id", {"id": audio_id})
return bytes(value) if value is not None else None
async def insert_chat_history(self, values: Mapping[str, Any]) -> int:
return await self.execute(
"INSERT INTO ai_agent_chat_history"
" (mac_address,agent_id,session_id,chat_type,content,audio_id,created_at)"
" VALUES (:mac_address,:agent_id,:session_id,:chat_type,:content,:audio_id,:created_at)",
values,
)
async def get_session_agent_id(self, session_id: str) -> str | None:
value = await self.scalar(
"SELECT agent_id FROM ai_agent_chat_history WHERE session_id=:id LIMIT 1", {"id": session_id}
)
return str(value) if value is not None else None
async def get_audio_agent_id(self, audio_id: str) -> str | None:
value = await self.scalar(
"SELECT agent_id FROM ai_agent_chat_history WHERE audio_id=:id LIMIT 1", {"id": audio_id}
)
return str(value) if value is not None else None
async def is_audio_owned(self, audio_id: str, agent_id: str) -> bool:
count = await self.scalar(
"SELECT COUNT(*) FROM ai_agent_chat_history WHERE audio_id=:audio_id AND agent_id=:agent_id",
{"audio_id": audio_id, "agent_id": agent_id},
)
return int(count or 0) == 1
async def get_audio_content(self, audio_id: str) -> str | None:
value = await self.scalar(
"SELECT content FROM ai_agent_chat_history WHERE audio_id=:id LIMIT 1", {"id": audio_id}
)
return str(value) if value is not None else None
async def get_chat_history(self, agent_id: str, session_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT created_at,chat_type,content,audio_id,mac_address"
" FROM ai_agent_chat_history WHERE agent_id=:agent_id AND session_id=:session_id"
" ORDER BY created_at ASC",
{"agent_id": agent_id, "session_id": session_id},
)
async def get_recent_user_history(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT content,audio_id FROM ai_agent_chat_history"
" WHERE agent_id=:id AND chat_type=1 AND audio_id IS NOT NULL ORDER BY id DESC LIMIT 50",
{"id": agent_id},
)
async def list_sessions(self, agent_id: str, page: int, limit: int) -> tuple[list[dict[str, Any]], int]:
total = int(
await self.scalar(
"SELECT COUNT(*) FROM (SELECT session_id FROM ai_agent_chat_history"
" WHERE agent_id=:id GROUP BY session_id) sessions",
{"id": agent_id},
)
or 0
)
rows = await self.fetch_all(
"SELECT h.session_id,MAX(h.created_at) AS created_at,COUNT(*) AS chat_count,"
" (SELECT t.title FROM ai_agent_chat_title t WHERE t.session_id=h.session_id LIMIT 1) AS title"
" FROM ai_agent_chat_history h WHERE h.agent_id=:id GROUP BY h.session_id"
" ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
{"id": agent_id, "limit": limit, "offset": (page - 1) * limit},
)
return rows, total
async def upsert_chat_title(self, session_id: str, title: str, now: datetime, title_id: str) -> None:
existing = await self.fetch_one(
"SELECT id FROM ai_agent_chat_title WHERE session_id=:session_id LIMIT 1", {"session_id": session_id}
)
if existing:
await self.execute(
"UPDATE ai_agent_chat_title SET title=:title,updated_at=:now WHERE id=:id",
{"id": existing["id"], "title": title, "now": now},
)
else:
await self.execute(
"INSERT INTO ai_agent_chat_title (id,session_id,title,created_at,updated_at)"
" VALUES (:id,:session_id,:title,:now,:now)",
{"id": title_id, "session_id": session_id, "title": title, "now": now},
)
async def delete_chat_history(self, agent_id: str, *, delete_audio: bool, delete_text: bool) -> None:
if delete_audio:
ids = await self.fetch_all(
"SELECT DISTINCT audio_id FROM ai_agent_chat_history WHERE agent_id=:id AND audio_id IS NOT NULL",
{"id": agent_id},
)
audio_ids = [str(row["audio_id"]) for row in ids]
for offset in range(0, len(audio_ids), 1000):
batch = audio_ids[offset : offset + 1000]
statement = text("DELETE FROM ai_agent_chat_audio WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
await self.execute(statement, {"ids": batch})
if delete_audio and not delete_text:
await self.execute("UPDATE ai_agent_chat_history SET audio_id=NULL WHERE agent_id=:id", {"id": agent_id})
if delete_text:
await self.execute("DELETE FROM ai_agent_chat_history WHERE agent_id=:id", {"id": agent_id})
async def delete_agent_cascade(self, agent_id: str) -> None:
devices = await self.fetch_all("SELECT mac_address FROM ai_device WHERE agent_id=:id", {"id": agent_id})
macs = [str(row["mac_address"]) for row in devices if row.get("mac_address") is not None]
await self.execute("DELETE FROM ai_device WHERE agent_id=:id", {"id": agent_id})
if macs:
statement = text(
"DELETE FROM ai_device_address_book WHERE mac_address IN :macs OR target_mac IN :targets"
).bindparams(bindparam("macs", expanding=True), bindparam("targets", expanding=True))
await self.execute(statement, {"macs": macs, "targets": macs})
await self.delete_chat_history(agent_id, delete_audio=True, delete_text=True)
for table in (
"ai_agent_plugin_mapping",
"ai_agent_context_provider",
"ai_agent_correct_word_mapping",
"ai_agent_tag_relation",
"ai_agent_snapshot",
):
await self.execute(f"DELETE FROM {table} WHERE agent_id=:id", {"id": agent_id})
await self.execute("DELETE FROM ai_agent WHERE id=:id", {"id": agent_id})
async def list_templates(
self, *, name: str | None = None, page: int | None = None, limit: int | None = None
) -> tuple[list[dict[str, Any]], int]:
params: dict[str, Any] = {}
where = ""
if name:
where = " WHERE agent_name LIKE :name"
params["name"] = f"%{name}%"
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_agent_template{where}", params) or 0)
paging = ""
if page is not None and limit is not None:
params.update(limit=limit, offset=(page - 1) * limit)
paging = " LIMIT :limit OFFSET :offset"
rows = await self.fetch_all(f"SELECT * FROM ai_agent_template{where} ORDER BY sort ASC{paging}", params)
return rows, total
async def get_template(self, template_id: str) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_agent_template WHERE id=:id", {"id": template_id})
async def get_default_template(self) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_agent_template ORDER BY sort ASC LIMIT 1")
async def next_template_sort(self) -> int:
rows = await self.fetch_all("SELECT sort FROM ai_agent_template WHERE sort IS NOT NULL ORDER BY sort ASC")
expected = 1
for row in rows:
value = int(row["sort"])
if value > expected:
return expected
expected = value + 1
return expected
async def insert_template(self, values: Mapping[str, Any]) -> int:
columns = [column for column in TEMPLATE_COLUMNS if column in values]
return await self.execute(
f"INSERT INTO ai_agent_template ({', '.join(columns)})"
f" VALUES ({', '.join(':' + column for column in columns)})",
{column: values[column] for column in columns},
)
async def update_template(self, template_id: str, values: Mapping[str, Any]) -> int:
selected = {
key: value for key, value in values.items() if key in TEMPLATE_COLUMNS and key != "id" and value is not None
}
if not selected:
return 0
return await self.execute(
f"UPDATE ai_agent_template SET {', '.join(key + '=:' + key for key in selected)} WHERE id=:id",
{**selected, "id": template_id},
)
async def delete_template(self, template_id: str) -> int:
return await self.execute("DELETE FROM ai_agent_template WHERE id=:id", {"id": template_id})
async def reorder_templates(self, deleted_sort: int) -> int:
return await self.execute("UPDATE ai_agent_template SET sort=sort-1 WHERE sort>:sort", {"sort": deleted_sort})
async def delete_templates(self, ids: Sequence[str]) -> int:
if not ids:
return 0
statement = text("DELETE FROM ai_agent_template WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))
return await self.execute(statement, {"ids": list(ids)})
async def list_voiceprints(self, agent_id: str, user_id: int) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id,audio_id,source_name,introduce,create_date"
" FROM ai_agent_voice_print WHERE agent_id=:agent_id AND creator=:user_id",
{"agent_id": agent_id, "user_id": user_id},
)
async def list_voiceprint_ids(self, agent_id: str) -> list[str]:
rows = await self.fetch_all("SELECT id FROM ai_agent_voice_print WHERE agent_id=:id", {"id": agent_id})
return [str(row["id"]) for row in rows]
async def get_voiceprint(self, voiceprint_id: str, user_id: int | None = None) -> dict[str, Any] | None:
where = "id=:id"
params: dict[str, Any] = {"id": voiceprint_id}
if user_id is not None:
where += " AND creator=:user_id"
params["user_id"] = user_id
return await self.fetch_one(f"SELECT * FROM ai_agent_voice_print WHERE {where} LIMIT 1", params)
async def insert_voiceprint(self, values: Mapping[str, Any]) -> int:
return await self.execute(
"INSERT INTO ai_agent_voice_print"
" (id,agent_id,audio_id,source_name,introduce,creator,create_date,updater,update_date)"
" VALUES (:id,:agent_id,:audio_id,:source_name,:introduce,:creator,:create_date,:updater,:update_date)",
values,
)
async def update_voiceprint(self, voiceprint_id: str, user_id: int, values: Mapping[str, Any]) -> int:
allowed = {"audio_id", "source_name", "introduce", "updater", "update_date"}
selected = {key: value for key, value in values.items() if key in allowed and value is not None}
if not selected:
return 0
return await self.execute(
f"UPDATE ai_agent_voice_print SET {', '.join(key + '=:' + key for key in selected)}"
" WHERE id=:id AND creator=:user_id",
{**selected, "id": voiceprint_id, "user_id": user_id},
)
async def delete_voiceprint(self, voiceprint_id: str, user_id: int) -> int:
return await self.execute(
"DELETE FROM ai_agent_voice_print WHERE id=:id AND creator=:user_id",
{"id": voiceprint_id, "user_id": user_id},
)
async def snapshot_max_version(self, agent_id: str) -> int:
return int(
await self.scalar(
"SELECT COALESCE(MAX(version_no),0) FROM ai_agent_snapshot WHERE agent_id=:id", {"id": agent_id}
)
or 0
)
async def latest_snapshot(self, agent_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_agent_snapshot WHERE agent_id=:id ORDER BY version_no DESC LIMIT 1", {"id": agent_id}
)
async def next_snapshot(self, agent_id: str, version_no: int) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_agent_snapshot WHERE agent_id=:id AND version_no>:version"
" ORDER BY version_no ASC LIMIT 1",
{"id": agent_id, "version": version_no},
)
async def get_snapshot(self, snapshot_id: str) -> dict[str, Any] | None:
return await self.fetch_one("SELECT * FROM ai_agent_snapshot WHERE id=:id", {"id": snapshot_id})
async def list_snapshots(
self, agent_id: str, page: int, limit: int, max_version_no: int | None
) -> tuple[list[dict[str, Any]], int]:
params: dict[str, Any] = {"id": agent_id, "limit": limit, "offset": (page - 1) * limit}
extra = ""
if max_version_no is not None:
extra = " AND version_no<=:max_version"
params["max_version"] = max_version_no
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_agent_snapshot WHERE agent_id=:id{extra}", params) or 0)
rows = await self.fetch_all(
f"SELECT * FROM ai_agent_snapshot WHERE agent_id=:id{extra}"
" ORDER BY version_no DESC LIMIT :limit OFFSET :offset",
params,
)
return rows, total
async def insert_snapshot_next_version(self, values: Mapping[str, Any]) -> int:
return await self.execute(
"INSERT INTO ai_agent_snapshot"
" (id,agent_id,user_id,version_no,snapshot_data,changed_fields,source,"
" restore_from_snapshot_id,restore_from_version_no,creator,created_at,redaction_version)"
" SELECT :id,:agent_id,:user_id,COALESCE(MAX(version_no),0)+1,:snapshot_data,:changed_fields,:source,"
" :restore_from_snapshot_id,:restore_from_version_no,:creator,:created_at,:redaction_version"
" FROM ai_agent_snapshot WHERE agent_id=:agent_id",
values,
)
async def prune_snapshots(self, agent_id: str, keep: int) -> int:
rows = await self.fetch_all(
"SELECT id FROM ai_agent_snapshot WHERE agent_id=:id ORDER BY version_no DESC LIMIT :keep",
{"id": agent_id, "keep": keep},
)
retained = [str(row["id"]) for row in rows]
if not retained:
return 0
statement = text("DELETE FROM ai_agent_snapshot WHERE agent_id=:agent_id AND id NOT IN :retained").bindparams(
bindparam("retained", expanding=True)
)
return await self.execute(statement, {"agent_id": agent_id, "retained": retained})
async def delete_snapshot(self, snapshot_id: str) -> int:
return await self.execute("DELETE FROM ai_agent_snapshot WHERE id=:id", {"id": snapshot_id})
async def list_legacy_snapshots(self, after_id: str | None, limit: int, version: int) -> list[dict[str, Any]]:
params: dict[str, Any] = {"version": version, "limit": limit}
extra = ""
if after_id is not None:
extra = " AND id>:after_id"
params["after_id"] = after_id
return await self.fetch_all(
f"SELECT id,snapshot_data,redaction_version FROM ai_agent_snapshot"
f" WHERE redaction_version<:version{extra} ORDER BY id ASC LIMIT :limit",
params,
)
async def update_redacted_snapshot(self, snapshot_id: str, snapshot_data: str, version: int) -> int:
return await self.execute(
"UPDATE ai_agent_snapshot SET snapshot_data=:data,redaction_version=:version"
" WHERE id=:id AND redaction_version<:version",
{"id": snapshot_id, "data": snapshot_data, "version": version},
)
@@ -0,0 +1,105 @@
from __future__ import annotations
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class ConfigRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def list_params(self) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT param_code, param_value, value_type FROM sys_params WHERE param_type = 1"
)
async def get_param_value(self, code: str) -> str | None:
value = await self.scalar(
"SELECT param_value FROM sys_params WHERE param_code = :code LIMIT 1",
{"code": code},
)
return None if value is None else str(value)
async def get_default_template(self) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, agent_code, agent_name, asr_model_id, vad_model_id, llm_model_id, "
"vllm_model_id, tts_model_id, tts_voice_id, tts_language, tts_volume, tts_rate, tts_pitch, "
"mem_model_id, intent_model_id, chat_history_conf, system_prompt, summary_memory, lang_code, "
"language, sort FROM ai_agent_template ORDER BY sort ASC LIMIT 1"
)
async def get_device_by_mac(self, mac_address: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, user_id, mac_address, board, agent_id, app_version, auto_update "
"FROM ai_device WHERE mac_address = :mac_address LIMIT 1",
{"mac_address": mac_address},
)
async def get_agent(self, agent_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, user_id, agent_code, agent_name, asr_model_id, vad_model_id, llm_model_id, slm_model_id, "
"vllm_model_id, tts_model_id, tts_voice_id, tts_language, tts_volume, tts_rate, tts_pitch, "
"mem_model_id, intent_model_id, chat_history_conf, system_prompt, summary_memory, lang_code, language "
"FROM ai_agent WHERE id = :id LIMIT 1",
{"id": agent_id},
)
async def get_model(self, model_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, model_type, model_code, model_name, is_default, is_enabled, config_json, doc_link, "
"remark, sort, creator, create_date, updater, update_date "
"FROM ai_model_config WHERE id = :id LIMIT 1",
{"id": model_id},
)
async def get_timbre(self, timbre_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, languages, name, remark, reference_audio, reference_text, sort, tts_model_id, "
"tts_voice, voice_demo FROM ai_tts_voice WHERE id = :id LIMIT 1",
{"id": timbre_id},
)
async def get_voice_clone(self, clone_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, name, model_id, voice_id, languages, user_id, train_status, train_error "
"FROM ai_voice_clone WHERE id = :id LIMIT 1",
{"id": clone_id},
)
async def get_plugin_mappings(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT m.id, m.agent_id, m.plugin_id, m.param_info, "
"(SELECT p.provider_code FROM ai_model_provider p WHERE p.id = m.plugin_id LIMIT 1) AS provider_code "
"FROM ai_agent_plugin_mapping m WHERE m.agent_id = :agent_id",
{"agent_id": agent_id},
)
async def get_dataset(self, dataset_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, dataset_id, rag_model_id, name, description, status "
"FROM ai_rag_dataset WHERE id = :id LIMIT 1",
{"id": dataset_id},
)
async def get_context_providers(self, agent_id: str) -> Any:
return await self.scalar(
"SELECT context_providers FROM ai_agent_context_provider WHERE agent_id = :agent_id LIMIT 1",
{"agent_id": agent_id},
)
async def get_voiceprints(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, agent_id, source_name, introduce, create_date "
"FROM ai_agent_voice_print WHERE agent_id = :agent_id ORDER BY create_date ASC",
{"agent_id": agent_id},
)
async def get_correct_word_items(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT i.source_word, i.target_word FROM ai_agent_correct_word_mapping m "
"JOIN ai_agent_correct_word_item i ON i.file_id = m.file_id WHERE m.agent_id = :agent_id",
{"agent_id": agent_id},
)
@@ -0,0 +1,115 @@
from __future__ import annotations
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class CorrectWordRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def name_exists(self, user_id: int, file_name: str, exclude_id: str | None = None) -> bool:
return bool(
await self.scalar(
"SELECT COUNT(*) FROM ai_agent_correct_word_file WHERE creator=:creator AND file_name=:file_name "
"AND (:exclude_id IS NULL OR id<>:exclude_id)",
{"creator": user_id, "file_name": file_name, "exclude_id": exclude_id},
)
)
async def insert_file(self, values: dict[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_agent_correct_word_file "
"(id, file_name, word_count, content, creator, created_at) "
"VALUES (:id, :file_name, :word_count, :content, :creator, :now)",
values,
)
async def insert_items(self, values: Sequence[dict[str, Any]]) -> None:
await self.execute_many(
"INSERT INTO ai_agent_correct_word_item (id, file_id, source_word, target_word) "
"VALUES (:id, :file_id, :source_word, :target_word)",
values,
)
async def get_file(self, file_id: str, *, for_update: bool = False) -> dict[str, Any] | None:
suffix = " FOR UPDATE" if for_update and self.session.get_bind().dialect.name != "sqlite" else ""
return await self.fetch_one(
f"SELECT * FROM ai_agent_correct_word_file WHERE id=:id{suffix}", # noqa: S608
{"id": file_id},
)
async def update_file(self, values: dict[str, Any]) -> int:
return await self.execute(
"UPDATE ai_agent_correct_word_file SET file_name=:file_name, word_count=:word_count, "
"content=:content, updater=:updater, updated_at=:now WHERE id=:id",
values,
)
async def list_files(
self, user_id: int, *, offset: int | None = None, limit: int | None = None
) -> tuple[list[dict[str, Any]], int]:
total = int(
await self.scalar(
"SELECT COUNT(*) FROM ai_agent_correct_word_file WHERE creator=:creator", {"creator": user_id}
)
or 0
)
if offset is None or limit is None:
rows = await self.fetch_all(
"SELECT * FROM ai_agent_correct_word_file WHERE creator=:creator ORDER BY created_at DESC",
{"creator": user_id},
)
else:
rows = await self.fetch_all(
"SELECT * FROM ai_agent_correct_word_file WHERE creator=:creator ORDER BY created_at DESC "
"LIMIT :offset, :limit",
{"creator": user_id, "offset": offset, "limit": limit},
)
return rows, total
async def delete_file_graph(self, file_id: str) -> None:
await self.execute("DELETE FROM ai_agent_correct_word_mapping WHERE file_id=:id", {"id": file_id})
await self.execute("DELETE FROM ai_agent_correct_word_item WHERE file_id=:id", {"id": file_id})
await self.execute("DELETE FROM ai_agent_correct_word_file WHERE id=:id", {"id": file_id})
async def delete_items(self, file_id: str) -> None:
await self.execute("DELETE FROM ai_agent_correct_word_item WHERE file_id=:id", {"id": file_id})
async def items_for_agent(self, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT i.source_word, i.target_word FROM ai_agent_correct_word_item i "
"JOIN ai_agent_correct_word_mapping m ON m.file_id=i.file_id WHERE m.agent_id=:agent_id",
{"agent_id": agent_id},
)
async def file_ids_for_agent(self, agent_id: str) -> list[str]:
rows = await self.fetch_all(
"SELECT file_id FROM ai_agent_correct_word_mapping WHERE agent_id=:agent_id", {"agent_id": agent_id}
)
return [str(row["file_id"]) for row in rows]
async def replace_agent_mappings(
self, agent_id: str, file_ids: Sequence[str], user_id: int, now: Any
) -> None:
await self.execute("DELETE FROM ai_agent_correct_word_mapping WHERE agent_id=:agent_id", {"agent_id": agent_id})
await self.execute_many(
"INSERT INTO ai_agent_correct_word_mapping "
"(id, agent_id, file_id, creator, created_at, updater, updated_at) "
"VALUES (:id, :agent_id, :file_id, :user_id, :now, :user_id, :now)",
[
{
"id": uuid.uuid4().hex,
"agent_id": agent_id,
"file_id": file_id,
"user_id": user_id,
"now": now,
}
for file_id in file_ids
],
)
@@ -0,0 +1,294 @@
from __future__ import annotations
# Every interpolated SQL fragment below is a module constant or a service-side allowlist.
# ruff: noqa: S608
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
DEVICE_COLUMNS = (
"id, user_id, mac_address, last_connected_at, auto_update, board, alias, "
"agent_id, app_version, sort, updater, update_date, creator, create_date"
)
OTA_COLUMNS = (
"id, firmware_name, type, version, size, remark, firmware_path, sort, "
"updater, update_date, creator, create_date"
)
ADDRESS_BOOK_COLUMNS = (
"mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date"
)
class DeviceRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def get_device(self, device_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {DEVICE_COLUMNS} FROM ai_device WHERE id = :id LIMIT 1",
{"id": device_id},
)
async def get_device_by_mac(self, mac_address: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {DEVICE_COLUMNS} FROM ai_device WHERE mac_address = :mac_address LIMIT 1",
{"mac_address": mac_address},
)
async def get_user_devices(self, user_id: int, agent_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
f"SELECT {DEVICE_COLUMNS} FROM ai_device WHERE user_id = :user_id AND agent_id = :agent_id",
{"user_id": user_id, "agent_id": agent_id},
)
async def insert_device(self, values: Mapping[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_device "
"(id, user_id, mac_address, last_connected_at, auto_update, board, alias, agent_id, app_version, "
"sort, updater, update_date, creator, create_date) "
"VALUES (:id, :user_id, :mac_address, :last_connected_at, :auto_update, :board, :alias, :agent_id, "
":app_version, :sort, :updater, :update_date, :creator, :create_date)",
values,
)
async def update_device_info(
self,
device_id: str,
*,
auto_update: int | None,
alias: str | None,
updater: int,
now: datetime,
) -> int:
assignments = ["updater = :updater", "update_date = :now"]
params: dict[str, Any] = {"id": device_id, "updater": updater, "now": now}
if auto_update is not None:
assignments.append("auto_update = :auto_update")
params["auto_update"] = auto_update
if alias is not None:
assignments.append("alias = :alias")
params["alias"] = alias
return await self.execute(
f"UPDATE ai_device SET {', '.join(assignments)} WHERE id = :id",
params,
)
async def update_connection(
self,
device_id: str,
*,
app_version: str | None,
now: datetime,
) -> int:
if app_version is None or not app_version.strip():
return await self.execute(
"UPDATE ai_device SET last_connected_at = :now WHERE id = :id",
{"id": device_id, "now": now},
)
return await self.execute(
"UPDATE ai_device SET last_connected_at = :now, app_version = :app_version WHERE id = :id",
{"id": device_id, "now": now, "app_version": app_version},
)
async def delete_device_for_user(self, device_id: str, user_id: int) -> int:
return await self.execute(
"DELETE FROM ai_device WHERE id = :id AND user_id = :user_id",
{"id": device_id, "user_id": user_id},
)
async def get_address_book(self, mac_address: str) -> list[dict[str, Any]]:
return await self.fetch_all(
f"SELECT {ADDRESS_BOOK_COLUMNS} FROM ai_device_address_book "
"WHERE mac_address = :mac_address ORDER BY update_date DESC",
{"mac_address": mac_address},
)
async def get_all_address_book(self) -> list[dict[str, Any]]:
return await self.fetch_all(f"SELECT {ADDRESS_BOOK_COLUMNS} FROM ai_device_address_book")
async def get_address_book_record(self, mac_address: str, target_mac: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {ADDRESS_BOOK_COLUMNS} FROM ai_device_address_book "
"WHERE mac_address = :mac_address AND target_mac = :target_mac LIMIT 1",
{"mac_address": mac_address, "target_mac": target_mac},
)
async def get_aliases(self, mac_address: str) -> list[str]:
rows = await self.fetch_all(
"SELECT alias FROM ai_device_address_book WHERE mac_address = :mac_address",
{"mac_address": mac_address},
)
return [str(row["alias"]) for row in rows if row.get("alias") not in (None, "")]
async def insert_address_book(
self,
*,
mac_address: str,
target_mac: str,
alias: str | None,
has_permission: bool | None,
actor: int,
now: datetime,
) -> None:
await self.execute(
"INSERT INTO ai_device_address_book "
"(mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date) "
"VALUES (:mac_address, :target_mac, :alias, :has_permission, :actor, :now, :actor, :now)",
{
"mac_address": mac_address,
"target_mac": target_mac,
"alias": alias,
"has_permission": has_permission,
"actor": actor,
"now": now,
},
)
async def update_address_alias(
self,
mac_address: str,
target_mac: str,
alias: str | None,
*,
now: datetime,
) -> int:
return await self.execute(
"UPDATE ai_device_address_book SET alias = :alias, update_date = :now "
"WHERE mac_address = :mac_address AND target_mac = :target_mac",
{
"mac_address": mac_address,
"target_mac": target_mac,
"alias": alias,
"now": now,
},
)
async def update_address_permission(
self,
mac_address: str,
target_mac: str,
has_permission: bool,
*,
now: datetime,
) -> int:
return await self.execute(
"UPDATE ai_device_address_book "
"SET has_permission = :has_permission, update_date = :now "
"WHERE mac_address = :mac_address AND target_mac = :target_mac",
{
"mac_address": mac_address,
"target_mac": target_mac,
"has_permission": has_permission,
"now": now,
},
)
async def delete_address_books_for_macs(self, mac_addresses: Sequence[str]) -> int:
if not mac_addresses:
return 0
placeholders = ", ".join(f":mac_{index}" for index in range(len(mac_addresses)))
params = {f"mac_{index}": mac for index, mac in enumerate(mac_addresses)}
return await self.execute(
f"DELETE FROM ai_device_address_book WHERE mac_address IN ({placeholders}) "
f"OR target_mac IN ({placeholders})",
params,
)
async def count_ota(self, firmware_name: str | None = None) -> int:
where = ""
params: dict[str, Any] = {}
if firmware_name is not None and firmware_name.strip():
where = " WHERE firmware_name LIKE :firmware_name"
params["firmware_name"] = f"%{firmware_name}%"
return int(await self.scalar(f"SELECT COUNT(*) FROM ai_ota{where}", params) or 0)
async def list_ota(
self,
*,
page: int,
limit: int,
firmware_name: str | None,
order_fields: Sequence[str],
ascending: bool,
) -> list[dict[str, Any]]:
where = ""
params: dict[str, Any] = {"limit": limit, "offset": max(page - 1, 0) * limit}
if firmware_name is not None and firmware_name.strip():
where = " WHERE firmware_name LIKE :firmware_name"
params["firmware_name"] = f"%{firmware_name}%"
direction = "ASC" if ascending else "DESC"
order_by = ", ".join(f"{field} {direction}" for field in order_fields)
return await self.fetch_all(
f"SELECT {OTA_COLUMNS} FROM ai_ota{where} ORDER BY {order_by} LIMIT :limit OFFSET :offset",
params,
)
async def get_ota(self, ota_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {OTA_COLUMNS} FROM ai_ota WHERE id = :id LIMIT 1",
{"id": ota_id},
)
async def get_first_ota_by_type(self, ota_type: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {OTA_COLUMNS} FROM ai_ota WHERE type = :type LIMIT 1",
{"type": ota_type},
)
async def get_latest_ota(self, ota_type: str) -> dict[str, Any] | None:
return await self.fetch_one(
f"SELECT {OTA_COLUMNS} FROM ai_ota WHERE type = :type ORDER BY update_date DESC LIMIT 1",
{"type": ota_type},
)
async def count_duplicate_ota(self, *, ota_id: str, ota_type: str | None, version: str | None) -> int:
return int(
await self.scalar(
"SELECT COUNT(*) FROM ai_ota WHERE type = :type AND version = :version AND id <> :id",
{"id": ota_id, "type": ota_type, "version": version},
)
or 0
)
async def insert_ota(self, values: Mapping[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_ota "
"(id, firmware_name, type, version, size, remark, firmware_path, sort, updater, update_date, creator, "
"create_date) VALUES (:id, :firmware_name, :type, :version, :size, :remark, :firmware_path, :sort, "
":updater, :update_date, :creator, :create_date)",
values,
)
async def update_ota(self, ota_id: str, values: Mapping[str, Any]) -> int:
allowed = {
"firmware_name",
"type",
"version",
"size",
"remark",
"firmware_path",
"sort",
"updater",
"update_date",
"creator",
"create_date",
}
selected = {key: value for key, value in values.items() if key in allowed and value is not None}
if not selected:
return 0
assignments = ", ".join(f"{key} = :{key}" for key in selected)
return await self.execute(
f"UPDATE ai_ota SET {assignments} WHERE id = :id",
{"id": ota_id, **selected},
)
async def delete_ota(self, ids: Sequence[str]) -> int:
if not ids:
return 0
placeholders = ", ".join(f":id_{index}" for index in range(len(ids)))
params = {f"id_{index}": value for index, value in enumerate(ids)}
return await self.execute(f"DELETE FROM ai_ota WHERE id IN ({placeholders})", params)
@@ -0,0 +1,344 @@
from __future__ import annotations
import json
import uuid
from collections.abc import Sequence
from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo
from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.core.database import Repository
class KnowledgeRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def dataset_page(
self, user_id: int, name: str | None, offset: int, limit: int
) -> tuple[list[dict[str, Any]], int]:
where = (
"WHERE creator=:creator AND (:name IS NULL OR :name='' OR name LIKE CONCAT('%', :name, '%'))"
)
params = {"creator": user_id, "name": name, "offset": offset, "limit": limit}
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_rag_dataset {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
f"SELECT * FROM ai_rag_dataset {where} ORDER BY created_at DESC LIMIT :offset, :limit", # noqa: S608
params,
)
return rows, total
async def get_dataset(self, identifier: str, *, for_update: bool = False) -> dict[str, Any] | None:
suffix = " FOR UPDATE" if for_update and self.session.get_bind().dialect.name != "sqlite" else ""
return await self.fetch_one(
f"SELECT * FROM ai_rag_dataset WHERE dataset_id=:id OR id=:id LIMIT 1{suffix}", # noqa: S608
{"id": identifier},
)
async def datasets_by_ids(self, identifiers: Sequence[str]) -> list[dict[str, Any]]:
if not identifiers:
return []
statement = text("SELECT * FROM ai_rag_dataset WHERE dataset_id IN :ids OR id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
result = await self.session.execute(statement, {"ids": list(identifiers)})
return [dict(row) for row in result.mappings().all()]
async def duplicate_dataset_name(self, user_id: int, name: str, exclude_id: str | None = None) -> bool:
return bool(
await self.scalar(
"SELECT COUNT(*) FROM ai_rag_dataset WHERE creator=:creator AND name=:name "
"AND (:exclude_id IS NULL OR id<>:exclude_id)",
{"creator": user_id, "name": name, "exclude_id": exclude_id},
)
)
async def dataset_id_conflict(self, dataset_id: str, exclude_id: str) -> bool:
return bool(
await self.scalar(
"SELECT COUNT(*) FROM ai_rag_dataset WHERE dataset_id=:dataset_id AND id<>:exclude_id",
{"dataset_id": dataset_id, "exclude_id": exclude_id},
)
)
async def insert_dataset(self, values: dict[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_rag_dataset "
"(id,dataset_id,rag_model_id,tenant_id,name,avatar,description,embedding_model,permission,chunk_method,"
"parser_config,chunk_count,document_count,token_num,status,creator,created_at,updater,updated_at) VALUES "
"(:id,:dataset_id,:rag_model_id,:tenant_id,:name,:avatar,:description,:embedding_model,:permission,"
":chunk_method,:parser_config,:chunk_count,:document_count,:token_num,:status,:creator,:created_at,"
":updater,:updated_at)",
values,
)
async def update_dataset(self, values: dict[str, Any]) -> int:
return await self.execute(
"UPDATE ai_rag_dataset SET dataset_id=COALESCE(:dataset_id,dataset_id),"
"rag_model_id=COALESCE(:rag_model_id,rag_model_id),name=COALESCE(:name,name),"
"avatar=COALESCE(:avatar,avatar),description=COALESCE(:description,description),"
"embedding_model=COALESCE(:embedding_model,embedding_model),"
"permission=COALESCE(:permission,permission),chunk_method=COALESCE(:chunk_method,chunk_method),"
"parser_config=COALESCE(:parser_config,parser_config),chunk_count=COALESCE(:chunk_count,chunk_count),"
"token_num=COALESCE(:token_num,token_num),status=COALESCE(:status,status),"
"creator=COALESCE(:creator,creator),created_at=COALESCE(:created_at,created_at),updater=:updater,"
"updated_at=:updated_at WHERE id=:id",
values,
)
async def delete_dataset_local(self, row: dict[str, Any]) -> None:
await self.execute("DELETE FROM ai_agent_plugin_mapping WHERE plugin_id=:id", {"id": row["id"]})
await self.execute("DELETE FROM ai_rag_dataset WHERE id=:id", {"id": row["id"]})
async def rag_models(self) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, model_name, config_json FROM ai_model_config WHERE model_type='RAG' AND is_enabled=1 "
"ORDER BY is_default DESC, create_date DESC"
)
async def rag_config(self, model_id: str) -> dict[str, Any]:
row = await self.fetch_one("SELECT config_json FROM ai_model_config WHERE id=:id", {"id": model_id})
if row is None or row.get("config_json") is None:
from app.core.errors import AppError
raise AppError(10164)
raw = row["config_json"]
if isinstance(raw, bytes):
raw = raw.decode("utf-8")
config = dict(raw) if isinstance(raw, dict) else dict(json.loads(str(raw)))
config.setdefault("type", "ragflow")
return config
async def documents_page(
self,
dataset_id: str,
*,
name: str | None,
status: str | None,
offset: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
where = (
"WHERE dataset_id=:dataset_id "
"AND (:name IS NULL OR :name='' OR name LIKE CONCAT('%', :name, '%')) "
"AND (:status IS NULL OR :status='' OR status=:status)"
)
params = {"dataset_id": dataset_id, "name": name, "status": status, "offset": offset, "limit": limit}
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_rag_knowledge_document {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
f"SELECT * FROM ai_rag_knowledge_document {where} " # noqa: S608
"ORDER BY created_at DESC LIMIT :offset, :limit",
params,
)
return rows, total
async def all_documents(self, dataset_id: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT * FROM ai_rag_knowledge_document WHERE dataset_id=:dataset_id", {"dataset_id": dataset_id}
)
async def documents_by_remote_ids(self, dataset_id: str, ids: Sequence[str]) -> list[dict[str, Any]]:
if not ids:
return []
statement = text(
"SELECT * FROM ai_rag_knowledge_document WHERE dataset_id=:dataset_id AND document_id IN :ids"
).bindparams(bindparam("ids", expanding=True))
result = await self.session.execute(statement, {"dataset_id": dataset_id, "ids": list(ids)})
return [dict(row) for row in result.mappings().all()]
async def upsert_document(self, dataset_id: str, remote: dict[str, Any], *, creator: int | None = None) -> bool:
document_id = str(remote.get("id") or remote.get("document_id") or "")
existing = await self.fetch_one(
"SELECT id,created_at FROM ai_rag_knowledge_document WHERE document_id=:id", {"id": document_id}
)
name = remote.get("name")
size = remote.get("size")
if size is None:
size = remote.get("file_size")
meta_fields = remote.get("meta_fields")
if meta_fields is None:
meta_fields = remote.get("meta")
error = remote.get("progress_msg")
if error is None:
error = remote.get("error")
synced_at = _shanghai_now_naive()
created_at = remote.get("created_at")
if not isinstance(created_at, datetime):
created_at = _millis_date(remote.get("create_time"))
updated_at = remote.get("updated_at")
if not isinstance(updated_at, datetime):
updated_at = _millis_date(remote.get("update_time"))
values = {
"id": existing["id"] if existing else uuid.uuid4().hex,
"dataset_id": remote.get("dataset_id") or dataset_id,
"document_id": document_id,
"name": name,
"size": size,
"type": _file_type(str(name or "")),
"chunk_method": remote.get("chunk_method"),
"parser_config": _json_dump(remote.get("parser_config")),
"status": _remote_status(remote.get("status")),
"run": remote.get("run"),
"progress": remote.get("progress"),
"thumbnail": remote.get("thumbnail"),
"process_duration": remote.get("process_duration"),
"meta_fields": _json_dump(meta_fields),
"source_type": remote.get("source_type"),
"error": error,
"chunk_count": remote.get("chunk_count") or 0,
"token_count": remote.get("token_count") or 0,
"enabled": 1,
"creator": creator,
"created_at": existing.get("created_at") if existing else (created_at or synced_at),
"updated_at": updated_at or synced_at,
"synced_at": synced_at,
}
if existing:
await self.execute(
"UPDATE ai_rag_knowledge_document SET dataset_id=:dataset_id,document_id=:document_id,name=:name,"
"size=:size,type=:type,chunk_method=:chunk_method,parser_config=:parser_config,status=:status,run=:run,"
"progress=:progress,thumbnail=:thumbnail,process_duration=:process_duration,meta_fields=:meta_fields,"
"source_type=:source_type,error=:error,chunk_count=:chunk_count,token_count=:token_count,enabled=:enabled,"
"updated_at=:updated_at,last_sync_at=:synced_at WHERE id=:id",
values,
)
return False
await self.execute(
"INSERT INTO ai_rag_knowledge_document "
"(id,dataset_id,document_id,name,size,type,chunk_method,parser_config,status,run,progress,thumbnail,"
"process_duration,meta_fields,source_type,error,chunk_count,token_count,enabled,creator,created_at,updated_at,"
"last_sync_at) VALUES (:id,:dataset_id,:document_id,:name,:size,:type,:chunk_method,:parser_config,:status,"
":run,:progress,:thumbnail,:process_duration,:meta_fields,:source_type,:error,:chunk_count,:token_count,"
":enabled,:creator,COALESCE(:created_at,:synced_at),COALESCE(:updated_at,:synced_at),:synced_at)",
values,
)
return True
async def update_stats(self, dataset_id: str, docs: int, chunks: int, tokens: int) -> None:
await self.execute(
"UPDATE ai_rag_dataset SET document_count=document_count+:docs,chunk_count=chunk_count+:chunks,"
"token_num=token_num+:tokens,updated_at=:now WHERE dataset_id=:dataset_id",
{
"dataset_id": dataset_id,
"docs": docs,
"chunks": chunks,
"tokens": tokens,
"now": _shanghai_now_naive(),
},
)
async def delete_document_shadows(self, dataset_id: str, ids: Sequence[str]) -> int:
if not ids:
return 0
statement = text(
"DELETE FROM ai_rag_knowledge_document WHERE dataset_id=:dataset_id AND document_id IN :ids"
).bindparams(bindparam("ids", expanding=True))
result = await self.session.execute(statement, {"dataset_id": dataset_id, "ids": list(ids)})
return int(getattr(result, "rowcount", 0) or 0)
async def mark_documents_running(self, dataset_id: str, ids: Sequence[str], now: datetime) -> int:
if not ids:
return 0
statement = text(
"UPDATE ai_rag_knowledge_document SET run='RUNNING',status='1',updated_at=:now "
"WHERE dataset_id=:dataset_id AND document_id IN :ids"
).bindparams(bindparam("ids", expanding=True))
result = await self.session.execute(statement, {"dataset_id": dataset_id, "ids": list(ids), "now": now})
return int(getattr(result, "rowcount", 0) or 0)
async def mark_document_remote_deleted(self, document_id: str, now: datetime) -> int:
return await self.execute(
"UPDATE ai_rag_knowledge_document SET run='CANCEL',error=:error,updated_at=:now,last_sync_at=:now "
"WHERE document_id=:document_id",
{
"document_id": document_id,
"error": "文档在远程服务中已被删除",
"now": now,
},
)
async def sync_running_document(
self,
dataset_id: str,
document_id: str,
remote: dict[str, Any],
now: datetime,
) -> int:
"""Update exactly the columns touched by Java's status-sync helper."""
updated_at = _millis_date(remote.get("update_time")) or now
meta_fields = remote.get("meta_fields")
assignments = (
"status=:status,run=:run,progress=:progress,chunk_count=:chunk_count,token_count=:token_count,"
"error=:error,process_duration=:process_duration,thumbnail=:thumbnail,updated_at=:updated_at,"
"last_sync_at=:now"
)
if meta_fields is not None:
assignments += ",meta_fields=:meta_fields"
return await self.execute(
f"UPDATE ai_rag_knowledge_document SET {assignments} " # noqa: S608
"WHERE document_id=:document_id AND dataset_id=:dataset_id",
{
"dataset_id": dataset_id,
"document_id": document_id,
"status": remote.get("status"),
"run": remote.get("run"),
"progress": remote.get("progress"),
"chunk_count": remote.get("chunk_count"),
"token_count": remote.get("token_count"),
"error": remote.get("progress_msg") if remote.get("progress_msg") is not None else remote.get("error"),
"process_duration": remote.get("process_duration"),
"thumbnail": remote.get("thumbnail"),
"meta_fields": _json_dump(meta_fields),
"updated_at": updated_at,
"now": now,
},
)
async def running_documents(self) -> list[dict[str, Any]]:
return await self.fetch_all("SELECT * FROM ai_rag_knowledge_document WHERE run='RUNNING' AND status='1'")
def _json_dump(value: Any) -> str | None:
if value is None:
return None
if isinstance(value, str):
return value
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _millis_date(value: Any) -> datetime | None:
if value is None:
return None
try:
timezone = ZoneInfo(get_settings().timezone)
return datetime.fromtimestamp(float(value) / 1000, timezone).replace(tzinfo=None)
except (TypeError, ValueError, OSError):
return None
def _file_type(name: str) -> str:
last_dot = name.rfind(".")
if last_dot <= 0 or last_dot == len(name) - 1:
return "unknown"
extension = name.rsplit(".", 1)[1].lower()
if extension in {"pdf", "doc", "docx", "txt", "md", "mdx"}:
return "document"
if extension in {"csv", "xls", "xlsx"}:
return "spreadsheet"
if extension in {"ppt", "pptx"}:
return "presentation"
return extension
def _remote_status(value: Any) -> str:
if value is None or (isinstance(value, str) and not value.strip()):
return "1"
return str(value)
def _shanghai_now_naive() -> datetime:
return datetime.now(ZoneInfo(get_settings().timezone)).replace(tzinfo=None)
@@ -0,0 +1,243 @@
from __future__ import annotations
import json
from collections.abc import Sequence
from typing import Any
from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class ModelRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def list_model_names(self, model_type: str, model_name: str | None) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, model_name FROM ai_model_config "
"WHERE model_type = :model_type AND is_enabled = 1 "
"AND (:model_name IS NULL OR :model_name = '' OR model_name LIKE CONCAT('%', :model_name, '%')) "
"ORDER BY sort ASC",
{"model_type": model_type, "model_name": model_name},
)
async def list_llm_names(self, model_name: str | None) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, model_name, config_json FROM ai_model_config "
"WHERE model_type = 'llm' AND is_enabled = 1 "
"AND (:model_name IS NULL OR :model_name = '' OR model_name LIKE CONCAT('%', :model_name, '%'))",
{"model_name": model_name},
)
async def list_providers_by_type(self, model_type: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT * FROM ai_model_provider WHERE model_type = :model_type ORDER BY sort ASC",
{"model_type": model_type or ""},
)
async def list_providers(
self,
*,
model_type: str | None,
name: str | None,
offset: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
where = (
"WHERE (:model_type IS NULL OR :model_type = '' OR model_type = :model_type) "
"AND (:name IS NULL OR :name = '' OR name LIKE CONCAT('%', :name, '%') "
"OR provider_code LIKE CONCAT('%', :name, '%'))"
)
params = {"model_type": model_type, "name": name, "offset": offset, "limit": limit}
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_model_provider {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
f"SELECT * FROM ai_model_provider {where} " # noqa: S608
"ORDER BY model_type ASC, sort ASC LIMIT :offset, :limit",
params,
)
return rows, total
async def list_model_configs(
self,
*,
model_type: str,
model_name: str | None,
offset: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
where = (
"WHERE model_type = :model_type AND "
"(:model_name IS NULL OR :model_name = '' OR model_name LIKE CONCAT('%', :model_name, '%'))"
)
params = {"model_type": model_type, "model_name": model_name, "offset": offset, "limit": limit}
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_model_config {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
f"SELECT * FROM ai_model_config {where} " # noqa: S608
"ORDER BY is_enabled DESC, sort ASC LIMIT :offset, :limit",
params,
)
return rows, total
async def get_provider(self, model_type: str, provider_code: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT * FROM ai_model_provider WHERE model_type = :model_type AND provider_code = :provider_code LIMIT 1",
{"model_type": model_type or "", "provider_code": provider_code or ""},
)
async def get_model(self, model_id: str, *, for_update: bool = False) -> dict[str, Any] | None:
suffix = " FOR UPDATE" if for_update and self.session.get_bind().dialect.name != "sqlite" else ""
return await self.fetch_one(
f"SELECT * FROM ai_model_config WHERE id = :id LIMIT 1{suffix}", # noqa: S608
{"id": model_id},
)
async def insert_model(self, values: dict[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_model_config "
"(id, model_type, model_code, model_name, is_default, is_enabled, config_json, doc_link, remark, sort) "
"VALUES (:id, :model_type, :model_code, :model_name, :is_default, COALESCE(:is_enabled, 0), "
":config_json, :doc_link, :remark, COALESCE(:sort, 0))",
values,
)
async def update_model(self, values: dict[str, Any]) -> int:
return await self.execute(
"UPDATE ai_model_config SET model_type=:model_type, model_code=:model_code, "
"model_name=COALESCE(:model_name, model_name), is_default=:is_default, "
"is_enabled=COALESCE(:is_enabled, is_enabled), config_json=:config_json, doc_link=:doc_link, "
"remark=COALESCE(:remark, remark), sort=COALESCE(:sort, sort) WHERE id=:id",
values,
)
async def delete_model(self, model_id: str) -> int:
return await self.execute("DELETE FROM ai_model_config WHERE id = :id", {"id": model_id})
async def model_agent_references(self, model_id: str) -> list[str]:
rows = await self.fetch_all(
"SELECT agent_name FROM ai_agent WHERE vad_model_id=:id OR asr_model_id=:id OR llm_model_id=:id "
"OR tts_model_id=:id OR mem_model_id=:id OR vllm_model_id=:id OR intent_model_id=:id",
{"id": model_id},
)
return [str(row.get("agent_name") or "") for row in rows]
async def intent_reference_count(self, model_id: str) -> int:
return int(
await self.scalar(
"SELECT COUNT(*) FROM ai_model_config WHERE model_type='Intent' AND CAST(config_json AS CHAR) LIKE "
"CONCAT('%', :id, '%')",
{"id": model_id},
)
or 0
)
async def set_models_default(self, model_type: str, value: int) -> None:
await self.execute(
"UPDATE ai_model_config SET is_default=:value WHERE model_type=:model_type",
{"value": value, "model_type": model_type},
)
async def set_model_enabled(self, model_id: str, status: int) -> int:
return await self.execute(
"UPDATE ai_model_config SET is_enabled=:status WHERE id=:id",
{"status": status, "id": model_id},
)
async def update_default_template_models(self, model_type: str, model_id: str) -> None:
columns = {
"ASR": ("asr_model_id",),
"VAD": ("vad_model_id",),
"LLM": ("llm_model_id",),
"TTS": ("tts_model_id", "tts_voice_id"),
"VLLM": ("vllm_model_id",),
"MEMORY": ("mem_model_id",),
"INTENT": ("intent_model_id",),
}.get(model_type.upper())
if not columns:
return
if columns == ("tts_model_id", "tts_voice_id"):
await self.execute(
"UPDATE ai_agent_template SET tts_model_id=:id, tts_voice_id=NULL WHERE sort >= 0",
{"id": model_id},
)
else:
column = columns[0]
await self.session.execute(
text(f"UPDATE ai_agent_template SET {column}=:id WHERE sort >= 0"), # noqa: S608
{"id": model_id},
)
async def insert_provider(self, values: dict[str, Any]) -> None:
if self.session.get_bind().dialect.name == "sqlite":
statement = (
"INSERT INTO ai_model_provider "
"(id, model_type, provider_code, name, fields, sort, creator, create_date, updater, update_date) "
"VALUES (:id, :model_type, :provider_code, :name, :fields, :sort, :creator, :now, :updater, :now)"
)
else:
statement = (
"INSERT INTO ai_model_provider "
"(id, model_type, provider_code, name, fields, sort, creator, create_date, updater, update_date) "
"VALUES (:id, :model_type, :provider_code, :name, CAST(:fields AS JSON), :sort, :creator, :now, "
":updater, :now)"
)
await self.execute(statement, values)
async def update_provider(self, values: dict[str, Any]) -> int:
if self.session.get_bind().dialect.name == "sqlite":
statement = (
"UPDATE ai_model_provider SET model_type=:model_type, provider_code=:provider_code, name=:name, "
"fields=:fields, sort=:sort, updater=:updater, update_date=:now WHERE id=:id"
)
else:
statement = (
"UPDATE ai_model_provider SET model_type=:model_type, provider_code=:provider_code, name=:name, "
"fields=CAST(:fields AS JSON), sort=:sort, updater=:updater, update_date=:now WHERE id=:id"
)
return await self.execute(statement, values)
async def delete_providers(self, ids: Sequence[str]) -> int:
if not ids:
return 0
statement = text("DELETE FROM ai_model_provider WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
result = await self.session.execute(statement, {"ids": list(ids)})
return int(getattr(result, "rowcount", 0) or 0)
async def list_plugins_for_user(self, user_id: int) -> list[dict[str, Any]]:
providers = await self.fetch_all("SELECT * FROM ai_model_provider WHERE model_type='Plugin'")
datasets = await self.fetch_all(
"SELECT id, name, created_at, updated_at FROM ai_rag_dataset WHERE creator=:creator AND status=1",
{"creator": user_id},
)
providers.extend(
{
"id": row["id"],
"model_type": "Rag",
"name": f"[知识库]{row['name']}",
"provider_code": "ragflow",
"fields": "[]",
"sort": 0,
"create_date": row.get("created_at"),
"update_date": row.get("updated_at"),
"creator": 0,
"updater": 0,
}
for row in datasets
)
return providers
def parse_json_object(value: Any) -> dict[str, Any] | None:
if value is None:
return None
if isinstance(value, dict):
return dict(value)
if isinstance(value, bytes):
value = value.decode("utf-8")
if isinstance(value, str):
parsed = json.loads(value)
return dict(parsed) if isinstance(parsed, dict) else None
return None
@@ -0,0 +1,153 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class SecurityRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def get_param_value(self, code: str) -> str | None:
value = await self.scalar(
"SELECT param_value FROM sys_params WHERE param_code = :code LIMIT 1",
{"code": code},
)
return None if value is None else str(value)
async def get_mobile_area_items(self) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT d.dict_label AS name, d.dict_value AS `key` "
"FROM sys_dict_data d "
"LEFT JOIN sys_dict_type t ON d.dict_type_id = t.id "
"WHERE t.dict_type = :dict_type ORDER BY d.sort ASC",
{"dict_type": "MOBILE_AREA"},
)
async def get_user_by_username(self, username: str | None) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, username, password, super_admin, status, creator, create_date, updater, update_date "
"FROM sys_user WHERE username = :username LIMIT 1",
{"username": username},
)
async def get_user_by_id(self, user_id: int) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, username, password, super_admin, status, creator, create_date, updater, update_date "
"FROM sys_user WHERE id = :id LIMIT 1",
{"id": user_id},
)
async def count_users(self) -> int:
return int(await self.scalar("SELECT COUNT(*) FROM sys_user") or 0)
async def insert_user(
self,
*,
user_id: int,
username: str | None,
password: str,
super_admin: int,
now: datetime,
) -> None:
await self.execute(
"INSERT INTO sys_user "
"(id, username, password, super_admin, status, creator, create_date, updater, update_date) "
"VALUES (:id, :username, :password, :super_admin, 1, NULL, :now, NULL, :now)",
{
"id": user_id,
"username": username,
"password": password,
"super_admin": super_admin,
"now": now,
},
)
async def get_token_by_user_id(self, user_id: int, *, for_update: bool = False) -> dict[str, Any] | None:
sql = (
"SELECT id, user_id, token, expire_date, update_date, create_date "
"FROM sys_user_token WHERE user_id = :user_id LIMIT 1 FOR UPDATE"
if for_update and self._supports_for_update()
else "SELECT id, user_id, token, expire_date, update_date, create_date "
"FROM sys_user_token WHERE user_id = :user_id LIMIT 1"
)
return await self.fetch_one(
sql,
{"user_id": user_id},
)
async def insert_token(
self,
*,
token_id: int,
user_id: int,
token: str,
now: datetime,
expire_date: datetime,
) -> None:
await self.execute(
"INSERT INTO sys_user_token (id, user_id, token, expire_date, update_date, create_date) "
"VALUES (:id, :user_id, :token, :expire_date, :now, :now)",
{
"id": token_id,
"user_id": user_id,
"token": token,
"expire_date": expire_date,
"now": now,
},
)
async def update_token(self, *, token_id: int, token: str, now: datetime, expire_date: datetime) -> None:
await self.execute(
"UPDATE sys_user_token SET token = :token, expire_date = :expire_date, update_date = :now "
"WHERE id = :id",
{"id": token_id, "token": token, "expire_date": expire_date, "now": now},
)
async def update_password(
self,
user_id: int,
password_hash: str,
now: datetime,
*,
preserve_audit_fields: bool = False,
) -> int:
return await self.execute(
"UPDATE sys_user SET password = :password, "
"update_date = CASE WHEN :preserve_audit = 1 THEN update_date ELSE :now END WHERE id = :id",
{
"id": user_id,
"password": password_hash,
"now": now,
"preserve_audit": int(preserve_audit_fields),
},
)
async def expire_user_token(self, user_id: int, expire_date: datetime) -> int:
return await self.execute(
"UPDATE sys_user_token SET expire_date = :expire_date WHERE user_id = :user_id",
{"user_id": user_id, "expire_date": expire_date},
)
def _supports_for_update(self) -> bool:
bind = self.session.get_bind()
return bind.dialect.name != "sqlite"
async def raw_user_token(session: AsyncSession, token: str) -> dict[str, Any] | None:
result = await session.execute(
text(
"SELECT t.id AS token_id, t.user_id, t.token, t.expire_date, "
"u.username, u.super_admin, u.status "
"FROM sys_user_token t JOIN sys_user u ON u.id = t.user_id "
"WHERE t.token = :token LIMIT 1"
),
{"token": token},
)
row = result.mappings().first()
return dict(row) if row is not None else None
@@ -0,0 +1,494 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class SysRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def page_users(self, *, mobile: str | None, page: int, limit: int) -> tuple[list[dict[str, Any]], int]:
pattern = f"%{mobile}%" if mobile else None
params = {"mobile": pattern, "offset": (page - 1) * limit, "limit": limit}
total = int(
await self.scalar(
"SELECT COUNT(*) FROM sys_user WHERE (:mobile IS NULL OR username LIKE :mobile)",
params,
)
or 0
)
rows = await self.fetch_all(
"SELECT u.id, u.username, u.status, u.create_date, "
"(SELECT COUNT(*) FROM ai_device d WHERE d.user_id = u.id) AS device_count "
"FROM sys_user u WHERE (:mobile IS NULL OR u.username LIKE :mobile) "
"ORDER BY u.id ASC LIMIT :limit OFFSET :offset",
params,
)
return rows, total
async def reset_user_password(
self,
user_id: int,
password_hash: str,
updater: int,
now: datetime,
) -> int:
return await self.execute(
"UPDATE sys_user SET password = :password, updater = :updater, update_date = :now WHERE id = :id",
{"id": user_id, "password": password_hash, "updater": updater, "now": now},
)
async def change_user_status(self, status: int, user_ids: list[int], updater: int, now: datetime) -> int:
statement = text(
"UPDATE sys_user SET status = :status, updater = :updater, update_date = :now WHERE id IN :ids"
).bindparams(bindparam("ids", expanding=True))
return await self.execute(
statement,
{"status": status, "updater": updater, "now": now, "ids": user_ids},
)
async def delete_user_cascade(self, user_id: int) -> None:
agent_rows = await self.fetch_all("SELECT id FROM ai_agent WHERE user_id = :user_id", {"user_id": user_id})
agent_ids = [str(row["id"]) for row in agent_rows]
await self.execute("DELETE FROM sys_user WHERE id = :id", {"id": user_id})
await self.execute("DELETE FROM ai_device WHERE user_id = :id", {"id": user_id})
for agent_id in agent_ids:
audio_rows = await self.fetch_all(
"SELECT DISTINCT audio_id FROM ai_agent_chat_history "
"WHERE agent_id = :agent_id AND audio_id IS NOT NULL",
{"agent_id": agent_id},
)
audio_ids = [str(row["audio_id"]) for row in audio_rows]
if audio_ids:
statement = text("DELETE FROM ai_agent_chat_audio WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
await self.execute(statement, {"ids": audio_ids})
for table in (
"ai_agent_chat_history",
"ai_agent_plugin_mapping",
"ai_agent_context_provider",
"ai_agent_correct_word_mapping",
"ai_agent_tag_relation",
"ai_agent_snapshot",
):
# Table names are a closed list mirroring AgentServiceImpl.deleteAgent.
await self.execute(
f"DELETE FROM {table} WHERE agent_id = :agent_id", # noqa: S608 - closed table list above
{"agent_id": agent_id},
)
await self.execute("DELETE FROM ai_device WHERE agent_id = :agent_id", {"agent_id": agent_id})
await self.execute("DELETE FROM ai_agent WHERE id = :agent_id", {"agent_id": agent_id})
async def page_devices(
self,
*,
keywords: str | None,
page: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
pattern = f"%{keywords}%" if keywords else None
params = {"keywords": pattern, "offset": (page - 1) * limit, "limit": limit}
total = int(
await self.scalar(
"SELECT COUNT(*) FROM ai_device WHERE (:keywords IS NULL OR alias LIKE :keywords)",
params,
)
or 0
)
rows = await self.fetch_all(
"SELECT d.id, d.user_id, d.mac_address, d.last_connected_at, d.auto_update, d.board, d.alias, "
"d.agent_id, d.app_version, d.sort, d.create_date, d.update_date, u.username AS bind_user_name "
"FROM ai_device d LEFT JOIN sys_user u ON u.id = d.user_id "
"WHERE (:keywords IS NULL OR d.alias LIKE :keywords) "
"ORDER BY d.mac_address ASC LIMIT :limit OFFSET :offset",
params,
)
return rows, total
async def page_params(
self,
*,
param_code: str | None,
page: int,
limit: int,
order_field: str | None,
order: str | None,
) -> tuple[list[dict[str, Any]], int]:
pattern = f"%{param_code}%" if param_code else None
params = {"pattern": pattern, "offset": (page - 1) * limit, "limit": limit}
where = "param_type = 1 AND (:pattern IS NULL OR param_code LIKE :pattern OR remark LIKE :pattern)"
total = int(await self.scalar(f"SELECT COUNT(*) FROM sys_params WHERE {where}", params) or 0) # noqa: S608
allowed = {
"id": "id",
"paramCode": "param_code",
"paramValue": "param_value",
"valueType": "value_type",
"createDate": "create_date",
"updateDate": "update_date",
}
order_column = allowed.get(order_field or "")
order_clause = ""
if order_column is not None:
direction = "ASC" if (order or "").lower() == "asc" else "DESC"
order_clause = f" ORDER BY {order_column} {direction}"
sql = (
"SELECT id, param_code, param_value, value_type, remark, create_date, update_date " # noqa: S608
f"FROM sys_params WHERE {where}{order_clause} LIMIT :limit OFFSET :offset"
)
return await self.fetch_all(sql, params), total # noqa: S608
async def list_config_params(self) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, param_code, param_value, value_type, remark, create_date, update_date "
"FROM sys_params WHERE param_type = 1"
)
async def get_param(self, param_id: int) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, param_code, param_value, value_type, remark, create_date, update_date "
"FROM sys_params WHERE id = :id",
{"id": param_id},
)
async def get_param_value(self, code: str) -> str | None:
value = await self.scalar("SELECT param_value FROM sys_params WHERE param_code = :code", {"code": code})
return None if value is None else str(value)
async def insert_param(
self,
*,
param_id: int,
param_code: str,
param_value: str,
value_type: str,
remark: str | None,
user_id: int,
now: datetime,
) -> None:
await self.execute(
"INSERT INTO sys_params "
"(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date) "
"VALUES (:id, :code, :value, :value_type, 1, :remark, :user_id, :now, :user_id, :now)",
{
"id": param_id,
"code": param_code,
"value": param_value,
"value_type": value_type,
"remark": remark,
"user_id": user_id,
"now": now,
},
)
async def update_param(
self,
*,
param_id: int,
param_code: str,
param_value: str,
value_type: str,
remark: str | None,
user_id: int,
now: datetime,
) -> int:
return await self.execute(
"UPDATE sys_params SET param_code = :code, param_value = :value, value_type = :value_type, "
"remark = CASE WHEN :has_remark = 1 THEN :remark ELSE remark END, updater = :user_id, update_date = :now "
"WHERE id = :id",
{
"id": param_id,
"code": param_code,
"value": param_value,
"value_type": value_type,
"has_remark": int(remark is not None),
"remark": remark,
"user_id": user_id,
"now": now,
},
)
async def update_param_value_by_code(self, code: str, value: str, user_id: int, now: datetime) -> int:
return await self.execute(
"UPDATE sys_params SET param_value = :value, updater = :user_id, update_date = :now "
"WHERE param_code = :code",
{"code": code, "value": value, "user_id": user_id, "now": now},
)
async def param_codes_for_ids(self, ids: list[int]) -> list[str]:
statement = text("SELECT param_code FROM sys_params WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
rows = await self.fetch_all(statement, {"ids": ids})
return [str(row["param_code"]) for row in rows]
async def delete_params(self, ids: list[int]) -> int:
statement = text("DELETE FROM sys_params WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))
return await self.execute(statement, {"ids": ids})
async def delete_plugin_mapping_by_plugin_id(self, plugin_id: str) -> int:
return await self.execute(
"DELETE FROM ai_agent_plugin_mapping WHERE plugin_id = :plugin_id",
{"plugin_id": plugin_id},
)
async def page_dict_types(
self,
*,
dict_type: str | None,
dict_name: str | None,
page: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
params = {
"dict_type": f"%{dict_type}%" if dict_type else None,
"dict_name": f"%{dict_name}%" if dict_name else None,
"offset": (page - 1) * limit,
"limit": limit,
}
where = (
"(:dict_type IS NULL OR t.dict_type LIKE :dict_type) "
"AND (:dict_name IS NULL OR t.dict_name LIKE :dict_name)"
)
total = int(await self.scalar(f"SELECT COUNT(*) FROM sys_dict_type t WHERE {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
"SELECT t.id, t.dict_type, t.dict_name, t.remark, t.sort, t.creator, t.create_date, t.updater, " # noqa: S608
"t.update_date, creator.username AS creator_name, updater.username AS updater_name "
"FROM sys_dict_type t LEFT JOIN sys_user creator ON creator.id = t.creator "
"LEFT JOIN sys_user updater ON updater.id = t.updater "
f"WHERE {where} ORDER BY t.sort ASC LIMIT :limit OFFSET :offset", # noqa: S608
params,
)
return rows, total
async def get_dict_type(self, type_id: int) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, dict_type, dict_name, remark, sort, creator, create_date, updater, update_date "
"FROM sys_dict_type WHERE id = :id",
{"id": type_id},
)
async def dict_type_exists(self, dict_type: str | None, *, exclude_id: int | None = None) -> bool:
count = await self.scalar(
"SELECT COUNT(*) FROM sys_dict_type WHERE dict_type = :dict_type "
"AND (:exclude_id IS NULL OR id <> :exclude_id)",
{"dict_type": dict_type, "exclude_id": exclude_id},
)
return int(count or 0) > 0
async def insert_dict_type(
self,
*,
type_id: int,
dict_type: str | None,
dict_name: str | None,
remark: str | None,
sort: int | None,
user_id: int,
now: datetime,
) -> None:
await self.execute(
"INSERT INTO sys_dict_type "
"(id, dict_type, dict_name, remark, sort, creator, create_date, updater, update_date) "
"VALUES (:id, :dict_type, :dict_name, :remark, :sort, :user_id, :now, :user_id, :now)",
{
"id": type_id,
"dict_type": dict_type,
"dict_name": dict_name,
"remark": remark,
"sort": sort,
"user_id": user_id,
"now": now,
},
)
async def update_dict_type(
self,
*,
type_id: int | None,
dict_type: str | None,
dict_name: str | None,
remark: str | None,
sort: int | None,
user_id: int,
now: datetime,
) -> int:
return await self.execute(
"UPDATE sys_dict_type SET "
"dict_type = CASE WHEN :has_dict_type = 1 THEN :dict_type ELSE dict_type END, "
"dict_name = CASE WHEN :has_dict_name = 1 THEN :dict_name ELSE dict_name END, "
"remark = CASE WHEN :has_remark = 1 THEN :remark ELSE remark END, "
"sort = CASE WHEN :has_sort = 1 THEN :sort ELSE sort END, updater = :user_id, update_date = :now "
"WHERE id = :id",
{
"id": type_id,
"has_dict_type": int(dict_type is not None),
"dict_type": dict_type,
"has_dict_name": int(dict_name is not None),
"dict_name": dict_name,
"has_remark": int(remark is not None),
"remark": remark,
"has_sort": int(sort is not None),
"sort": sort,
"user_id": user_id,
"now": now,
},
)
async def delete_dict_types(self, ids: list[int]) -> None:
statement_data = text("DELETE FROM sys_dict_data WHERE dict_type_id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
statement_types = text("DELETE FROM sys_dict_type WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
await self.execute(statement_data, {"ids": ids})
await self.execute(statement_types, {"ids": ids})
async def page_dict_data(
self,
*,
dict_type_id: int | None,
dict_label: str | None,
dict_value: str | None,
page: int,
limit: int,
) -> tuple[list[dict[str, Any]], int]:
params = {
"type_id": dict_type_id,
"dict_label": f"%{dict_label}%" if dict_label else None,
"dict_value": f"%{dict_value}%" if dict_value else None,
"offset": (page - 1) * limit,
"limit": limit,
}
where = (
"d.dict_type_id = :type_id AND (:dict_label IS NULL OR d.dict_label LIKE :dict_label) "
"AND (:dict_value IS NULL OR d.dict_value LIKE :dict_value)"
)
total = int(await self.scalar(f"SELECT COUNT(*) FROM sys_dict_data d WHERE {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
"SELECT d.id, d.dict_type_id, d.dict_label, d.dict_value, d.remark, d.sort, d.creator, " # noqa: S608
"d.create_date, d.updater, d.update_date, creator.username AS creator_name, "
"updater.username AS updater_name FROM sys_dict_data d "
"LEFT JOIN sys_user creator ON creator.id = d.creator "
"LEFT JOIN sys_user updater ON updater.id = d.updater "
f"WHERE {where} ORDER BY d.sort ASC LIMIT :limit OFFSET :offset", # noqa: S608
params,
)
return rows, total
async def get_dict_data(self, data_id: int) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, dict_type_id, dict_label, dict_value, remark, sort, creator, create_date, updater, update_date "
"FROM sys_dict_data WHERE id = :id",
{"id": data_id},
)
async def dict_data_label_exists(
self,
dict_type_id: int | None,
compared_label: str | None,
*,
exclude_id: int | None = None,
) -> bool:
count = await self.scalar(
"SELECT COUNT(*) FROM sys_dict_data WHERE dict_type_id = :type_id AND dict_label = :label "
"AND (:exclude_id IS NULL OR id <> :exclude_id)",
{"type_id": dict_type_id, "label": compared_label, "exclude_id": exclude_id},
)
return int(count or 0) > 0
async def dict_type_code(self, type_id: int | None) -> str | None:
value = await self.scalar("SELECT dict_type FROM sys_dict_type WHERE id = :id", {"id": type_id})
return None if value is None else str(value)
async def insert_dict_data(
self,
*,
data_id: int,
dict_type_id: int | None,
dict_label: str | None,
dict_value: str | None,
remark: str | None,
sort: int | None,
user_id: int,
now: datetime,
) -> None:
await self.execute(
"INSERT INTO sys_dict_data "
"(id, dict_type_id, dict_label, dict_value, remark, sort, creator, create_date, updater, update_date) "
"VALUES (:id, :type_id, :label, :value, :remark, :sort, :user_id, :now, :user_id, :now)",
{
"id": data_id,
"type_id": dict_type_id,
"label": dict_label,
"value": dict_value,
"remark": remark,
"sort": sort,
"user_id": user_id,
"now": now,
},
)
async def update_dict_data(
self,
*,
data_id: int | None,
dict_type_id: int | None,
dict_label: str | None,
dict_value: str | None,
remark: str | None,
sort: int | None,
user_id: int,
now: datetime,
) -> int:
return await self.execute(
"UPDATE sys_dict_data SET "
"dict_type_id = CASE WHEN :has_type_id = 1 THEN :type_id ELSE dict_type_id END, "
"dict_label = CASE WHEN :has_label = 1 THEN :label ELSE dict_label END, "
"dict_value = CASE WHEN :has_value = 1 THEN :value ELSE dict_value END, "
"remark = CASE WHEN :has_remark = 1 THEN :remark ELSE remark END, "
"sort = CASE WHEN :has_sort = 1 THEN :sort ELSE sort END, updater = :user_id, update_date = :now "
"WHERE id = :id",
{
"id": data_id,
"has_type_id": int(dict_type_id is not None),
"type_id": dict_type_id,
"has_label": int(dict_label is not None),
"label": dict_label,
"has_value": int(dict_value is not None),
"value": dict_value,
"has_remark": int(remark is not None),
"remark": remark,
"has_sort": int(sort is not None),
"sort": sort,
"user_id": user_id,
"now": now,
},
)
async def dict_type_codes_for_data_ids(self, ids: list[int]) -> list[str]:
statement = text(
"SELECT DISTINCT t.dict_type FROM sys_dict_type t JOIN sys_dict_data d ON d.dict_type_id = t.id "
"WHERE d.id IN :ids"
).bindparams(bindparam("ids", expanding=True))
rows = await self.fetch_all(statement, {"ids": ids})
return [str(row["dict_type"]) for row in rows]
async def delete_dict_data(self, ids: list[int]) -> int:
statement = text("DELETE FROM sys_dict_data WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))
return await self.execute(statement, {"ids": ids})
async def dict_items(self, dict_type: str) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT d.dict_label AS name, d.dict_value AS `key` FROM sys_dict_data d "
"LEFT JOIN sys_dict_type t ON d.dict_type_id = t.id "
"WHERE t.dict_type = :dict_type ORDER BY d.sort ASC",
{"dict_type": dict_type},
)
@@ -0,0 +1,71 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
class TimbreRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def page(
self, *, tts_model_id: str, name: str | None, offset: int, limit: int
) -> tuple[list[dict[str, Any]], int]:
where = (
"WHERE tts_model_id=:tts_model_id AND "
"(:name IS NULL OR :name='' OR name LIKE CONCAT('%', :name, '%'))"
)
params = {"tts_model_id": tts_model_id, "name": name, "offset": offset, "limit": limit}
total = int(await self.scalar(f"SELECT COUNT(*) FROM ai_tts_voice {where}", params) or 0) # noqa: S608
rows = await self.fetch_all(
f"SELECT * FROM ai_tts_voice {where} LIMIT :offset, :limit", # noqa: S608
params,
)
return rows, total
async def insert(self, values: dict[str, Any]) -> None:
await self.execute(
"INSERT INTO ai_tts_voice "
"(id, languages, name, remark, reference_audio, reference_text, sort, tts_model_id, tts_voice, "
"voice_demo, creator, create_date) VALUES (:id, :languages, :name, :remark, :reference_audio, "
":reference_text, :sort, :tts_model_id, :tts_voice, :voice_demo, :creator, :now)",
values,
)
async def update(self, values: dict[str, Any]) -> int:
return await self.execute(
"UPDATE ai_tts_voice SET languages=:languages, name=:name, remark=COALESCE(:remark, remark), "
"reference_audio=COALESCE(:reference_audio, reference_audio), "
"reference_text=COALESCE(:reference_text, reference_text), sort=:sort, "
"tts_model_id=:tts_model_id, tts_voice=:tts_voice, "
"voice_demo=COALESCE(:voice_demo, voice_demo), updater=:updater, "
"update_date=:now WHERE id=:id",
values,
)
async def delete(self, ids: Sequence[str]) -> int:
if not ids:
return 0
statement = text("DELETE FROM ai_tts_voice WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))
result = await self.session.execute(statement, {"ids": list(ids)})
return int(getattr(result, "rowcount", 0) or 0)
async def voices(
self, model_id: str, name: str | None, user_id: int
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
normal = await self.fetch_all(
"SELECT id, name, voice_demo, languages FROM ai_tts_voice WHERE tts_model_id=:model_id "
"AND (:name IS NULL OR :name='' OR name LIKE CONCAT('%', :name, '%'))",
{"model_id": model_id or "", "name": name},
)
clones = await self.fetch_all(
"SELECT id, name, voice_id AS voice_demo, languages FROM ai_voice_clone "
"WHERE model_id=:model_id AND user_id=:user_id AND train_status=2",
{"model_id": model_id, "user_id": user_id},
)
return normal, clones
@@ -0,0 +1,170 @@
from __future__ import annotations
# Every interpolated SQL fragment below is a module constant or a service-side allowlist.
# ruff: noqa: S608
from collections.abc import Mapping, Sequence
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import Repository
VOICE_COLUMNS = (
"id, name, model_id, voice_id, languages, user_id, voice, train_status, train_error, creator, create_date"
)
class VoiceCloneRepository(Repository):
def __init__(self, session: AsyncSession):
super().__init__(session)
async def count(self, *, name: str | None, user_id: str | None) -> int:
where, params = self._filters(name=name, user_id=user_id)
return int(await self.scalar(f"SELECT COUNT(*) FROM ai_voice_clone{where}", params) or 0)
async def page(
self,
*,
page: int,
limit: int,
name: str | None,
user_id: str | None,
order_fields: Sequence[str],
ascending: bool,
) -> list[dict[str, Any]]:
where, params = self._filters(name=name, user_id=user_id)
params.update(limit=limit, offset=max(page - 1, 0) * limit)
direction = "ASC" if ascending else "DESC"
order_by = ", ".join(f"{field} {direction}" for field in order_fields)
return await self.fetch_all(
f"SELECT {VOICE_COLUMNS} FROM ai_voice_clone{where} "
f"ORDER BY {order_by} LIMIT :limit OFFSET :offset",
params,
)
async def get(self, voice_id: str | None) -> dict[str, Any] | None:
if voice_id is None:
return None
return await self.fetch_one(
f"SELECT {VOICE_COLUMNS} FROM ai_voice_clone WHERE id = :id LIMIT 1",
{"id": voice_id},
)
async def list_by_user(self, user_id: int) -> list[dict[str, Any]]:
return await self.fetch_all(
f"SELECT {VOICE_COLUMNS} FROM ai_voice_clone "
"WHERE user_id = :user_id ORDER BY create_date DESC",
{"user_id": user_id},
)
async def voice_id_count(self, *, model_id: str, voice_id: str) -> int:
return int(
await self.scalar(
"SELECT COUNT(*) FROM ai_voice_clone WHERE voice_id = :voice_id AND model_id = :model_id",
{"model_id": model_id, "voice_id": voice_id},
)
or 0
)
async def insert_many(self, values: Sequence[Mapping[str, Any]]) -> int:
return await self.execute_many(
"INSERT INTO ai_voice_clone "
"(id, name, model_id, voice_id, languages, user_id, voice, train_status, train_error, creator, "
"create_date) VALUES (:id, :name, :model_id, :voice_id, :languages, :user_id, :voice, :train_status, "
":train_error, :creator, :create_date)",
values,
)
async def delete_many(self, ids: Sequence[str]) -> int:
if not ids:
return 0
placeholders = ", ".join(f":id_{index}" for index in range(len(ids)))
params = {f"id_{index}": value for index, value in enumerate(ids)}
return await self.execute(f"DELETE FROM ai_voice_clone WHERE id IN ({placeholders})", params)
async def update_voice(self, voice_id: str, data: bytes) -> int:
return await self.execute(
"UPDATE ai_voice_clone SET voice = :voice, train_status = 0 WHERE id = :id",
{"id": voice_id, "voice": data},
)
async def update_name(self, voice_id: str, name: str) -> int:
return await self.execute(
"UPDATE ai_voice_clone SET name = :name WHERE id = :id",
{"id": voice_id, "name": name},
)
async def update_training(
self,
voice_id: str,
*,
train_status: int,
train_error: str | None,
speaker_id: str | None = None,
) -> int:
if speaker_id is None:
return await self.execute(
"UPDATE ai_voice_clone SET train_status = :train_status, train_error = :train_error WHERE id = :id",
{"id": voice_id, "train_status": train_status, "train_error": train_error},
)
return await self.execute(
"UPDATE ai_voice_clone SET train_status = :train_status, train_error = :train_error, "
"voice_id = :speaker_id WHERE id = :id",
{
"id": voice_id,
"train_status": train_status,
"train_error": train_error,
"speaker_id": speaker_id,
},
)
async def get_model_config(self, model_id: str) -> dict[str, Any] | None:
return await self.fetch_one(
"SELECT id, model_name, config_json FROM ai_model_config WHERE id = :id LIMIT 1",
{"id": model_id},
)
async def get_model_name(self, model_id: str) -> str | None:
value = await self.scalar(
"SELECT model_name FROM ai_model_config WHERE id = :id LIMIT 1",
{"id": model_id},
)
return None if value is None else str(value)
async def get_usernames(self, user_ids: Sequence[int]) -> dict[int, str]:
if not user_ids:
return {}
unique_ids = list(dict.fromkeys(user_ids))
placeholders = ", ".join(f":user_{index}" for index in range(len(unique_ids)))
params = {f"user_{index}": value for index, value in enumerate(unique_ids)}
rows = await self.fetch_all(
f"SELECT id, username FROM sys_user WHERE id IN ({placeholders})",
params,
)
return {int(row["id"]): str(row["username"]) for row in rows}
async def get_username(self, user_id: int) -> str | None:
value = await self.scalar(
"SELECT username FROM sys_user WHERE id = :id LIMIT 1",
{"id": user_id},
)
return None if value is None else str(value)
async def get_tts_platforms(self) -> list[dict[str, Any]]:
return await self.fetch_all(
"SELECT id, model_name AS modelName FROM ai_model_config "
"WHERE model_type = 'TTS' AND JSON_EXTRACT(config_json, '$.type') = 'huoshan_double_stream'"
)
@staticmethod
def _filters(*, name: str | None, user_id: str | None) -> tuple[str, dict[str, Any]]:
clauses: list[str] = []
params: dict[str, Any] = {}
if user_id is not None and user_id.strip():
clauses.append("user_id = :user_id")
params["user_id"] = user_id
if name is not None and name.strip():
clauses.append("(name LIKE :name OR voice_id = :exact_name)")
params["name"] = f"%{name}%"
params["exact_name"] = name
return (" WHERE " + " AND ".join(clauses) if clauses else "", params)