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