Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f7c86a4b | ||
|
|
6026b1279e | ||
|
|
57af1c7db1 |
@@ -2,7 +2,7 @@
|
||||
from fastapi import APIRouter, Query, HTTPException
|
||||
|
||||
from app.schemas import ApiResponse
|
||||
from app.services.avatar_service import avatar_service, get_session
|
||||
from app.services.avatar_service import avatar_service, get_session, is_available
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -15,6 +15,8 @@ def list_avatars(
|
||||
status: str = Query(default=None),
|
||||
):
|
||||
"""分页查询所有数字分身"""
|
||||
if not is_available():
|
||||
return ApiResponse(data={"total": 0, "page": page, "page_size": page_size, "items": []})
|
||||
db = get_session()
|
||||
try:
|
||||
total, items = avatar_service.list_avatars(
|
||||
@@ -35,6 +37,8 @@ def list_avatars(
|
||||
@router.get("/{avatar_id}")
|
||||
def get_avatar(avatar_id: str):
|
||||
"""获取单个数字分身详情"""
|
||||
if not is_available():
|
||||
raise HTTPException(status_code=503, detail="数字分身数据库尚未初始化")
|
||||
db = get_session()
|
||||
try:
|
||||
data = avatar_service.get_avatar(db, avatar_id)
|
||||
@@ -54,6 +58,8 @@ def update_avatar_status(avatar_id: str, body: dict):
|
||||
status_code=400,
|
||||
detail="status 必须是 active、inactive 或 training",
|
||||
)
|
||||
if not is_available():
|
||||
raise HTTPException(status_code=503, detail="数字分身数据库尚未初始化")
|
||||
db = get_session()
|
||||
try:
|
||||
data = avatar_service.update_status(db, avatar_id, status)
|
||||
|
||||
@@ -28,6 +28,9 @@ class Settings(BaseSettings):
|
||||
# 数字分身 SQLite 数据库路径(直连数字分身应用的 SQLite)
|
||||
AVATAR_DB_PATH: str = os.getenv("AVATAR_DB_PATH", "")
|
||||
|
||||
# 数字分身后端服务地址(用于拼接头像等文件 URL)
|
||||
AVATAR_BACKEND_URL: str = os.getenv("AVATAR_BACKEND_URL", "")
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR: str = "/app/logs"
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ def _get_engine_and_session():
|
||||
if not os.path.isabs(db_path):
|
||||
db_path = os.path.abspath(db_path)
|
||||
if not os.path.exists(db_path):
|
||||
raise FileNotFoundError(f"数字分身数据库不存在: {db_path}")
|
||||
# 数据库不存在时返回 None,由调用方处理
|
||||
return None, None
|
||||
_engine = create_engine(
|
||||
f"sqlite:///{db_path}",
|
||||
connect_args={"check_same_thread": False},
|
||||
@@ -32,11 +33,39 @@ def _get_engine_and_session():
|
||||
return _engine, _SessionLocal()
|
||||
|
||||
|
||||
def get_session() -> Session:
|
||||
def get_session() -> Optional[Session]:
|
||||
_, session = _get_engine_and_session()
|
||||
return session
|
||||
|
||||
|
||||
def is_available() -> bool:
|
||||
"""检查数字分身数据库是否可用"""
|
||||
engine, _ = _get_engine_and_session()
|
||||
return engine is not None
|
||||
|
||||
|
||||
def _resolve_photo_url(photo_url: str) -> str:
|
||||
"""将相对路径的头像 URL 补全为绝对路径"""
|
||||
if not photo_url:
|
||||
return ""
|
||||
if photo_url.startswith(("http://", "https://")):
|
||||
return photo_url
|
||||
base = settings.AVATAR_BACKEND_URL
|
||||
if base:
|
||||
base = base.rstrip("/")
|
||||
return f"{base}{photo_url}"
|
||||
return photo_url
|
||||
|
||||
|
||||
def _get_global_token_balance(db: Session) -> int:
|
||||
"""获取全局 token_account 余额(单行表)"""
|
||||
try:
|
||||
result = db.execute(text("SELECT balance FROM token_account LIMIT 1")).fetchone()
|
||||
return result.balance if result and result.balance else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
class AvatarService:
|
||||
|
||||
@staticmethod
|
||||
@@ -90,6 +119,7 @@ class AvatarService:
|
||||
rows = db.execute(query, params).fetchall()
|
||||
|
||||
items = []
|
||||
global_token = _get_global_token_balance(db)
|
||||
for row in rows:
|
||||
config = {}
|
||||
if row.config:
|
||||
@@ -106,10 +136,10 @@ class AvatarService:
|
||||
"name": row.name,
|
||||
"display_name": row.display_name,
|
||||
"description": row.description or "",
|
||||
"photo_url": row.photo_url or "",
|
||||
"photo_url": _resolve_photo_url(row.photo_url),
|
||||
"emoji": row.emoji or "🤖",
|
||||
"status": row.status or "active",
|
||||
"token_balance": row.token_balance or 0,
|
||||
"token_balance": global_token or (row.token_balance or 0),
|
||||
"config": config,
|
||||
"owner_nickname": row.owner_nickname or "",
|
||||
"owner_phone": row.owner_phone or "",
|
||||
@@ -148,15 +178,16 @@ class AvatarService:
|
||||
elif isinstance(row.config, dict):
|
||||
config = row.config
|
||||
|
||||
global_token = _get_global_token_balance(db)
|
||||
return {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"display_name": row.display_name,
|
||||
"description": row.description or "",
|
||||
"photo_url": row.photo_url or "",
|
||||
"photo_url": _resolve_photo_url(row.photo_url),
|
||||
"emoji": row.emoji or "🤖",
|
||||
"status": row.status or "active",
|
||||
"token_balance": row.token_balance or 0,
|
||||
"token_balance": global_token or (row.token_balance or 0),
|
||||
"config": config,
|
||||
"owner_nickname": row.owner_nickname or "",
|
||||
"owner_phone": row.owner_phone or "",
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
|
||||
<!-- Table -->
|
||||
<el-table :data="avatars" v-loading="loading" :row-class-name="rowClassName" class="data-table">
|
||||
<el-table-column label="分身" width="240">
|
||||
<el-table-column label="分身" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<el-avatar :size="40" :src="row.photo_url || ''" style="background:#30363d;flex-shrink:0">
|
||||
<el-avatar v-if="row.photo_url" :size="40" :src="row.photo_url" style="background:#30363d;flex-shrink:0" @error="handleAvatarError">
|
||||
{{ row.emoji || '🤖' }}
|
||||
</el-avatar>
|
||||
<el-avatar v-else :size="40" style="background:#30363d;flex-shrink:0">
|
||||
{{ row.emoji || '🤖' }}
|
||||
</el-avatar>
|
||||
<div>
|
||||
@@ -36,7 +39,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="归属用户" width="160">
|
||||
<el-table-column label="归属用户" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.owner_nickname">{{ row.owner_nickname }}</span>
|
||||
<span v-else style="color:var(--color-text-muted)">未绑定</span>
|
||||
@@ -52,7 +55,7 @@
|
||||
<span style="font-weight:600">{{ row.token_balance?.toLocaleString() ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="160">
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span style="font-size:12px;color:var(--color-text-muted)">{{ row.created_at ? new Date(row.created_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false}) : '--' }}</span>
|
||||
</template>
|
||||
@@ -92,9 +95,12 @@
|
||||
<div v-if="detailAvatar" class="detail-panel">
|
||||
<!-- Header with avatar -->
|
||||
<div class="detail-header">
|
||||
<el-avatar :size="72" :src="detailAvatar.photo_url || ''" style="background:#30363d">
|
||||
<el-avatar v-if="detailAvatar.photo_url" :size="72" :src="detailAvatar.photo_url" style="background:#30363d" @error="handleAvatarError">
|
||||
<span style="font-size:32px">{{ detailAvatar.emoji || '🤖' }}</span>
|
||||
</el-avatar>
|
||||
<el-avatar v-else :size="72" style="background:#30363d">
|
||||
<span style="font-size:32px">{{ detailAvatar.emoji || '' }}</span>
|
||||
</el-avatar>
|
||||
<div style="margin-left:16px">
|
||||
<div class="detail-name">{{ detailAvatar.name }}</div>
|
||||
<div v-if="detailAvatar.display_name" class="detail-display-name">{{ detailAvatar.display_name }}</div>
|
||||
@@ -249,6 +255,11 @@ function openDetail(row) {
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function handleAvatarError(e) {
|
||||
// 图片加载失败时隐藏 img,只保留 emoji fallback
|
||||
e.target.style.display = 'none'
|
||||
}
|
||||
|
||||
function rowClassName({ rowIndex }) {
|
||||
return rowIndex % 2 === 0 ? 'row-dark' : 'row-darker'
|
||||
}
|
||||
@@ -259,7 +270,7 @@ onUnmounted(() => window.removeEventListener('page-refresh', loadAvatars))
|
||||
|
||||
<style scoped>
|
||||
.filter-bar { display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.data-table { border-radius: 8px; overflow: hidden; }
|
||||
.data-table { border-radius: 8px; overflow: hidden; width: 100%; }
|
||||
|
||||
/* Detail panel */
|
||||
.detail-header { display: flex; align-items: center; }
|
||||
|
||||
Reference in New Issue
Block a user