v0.1.4-p4

main
mula.liu 2026-04-02 12:40:17 +08:00
parent 9699b4e7c9
commit 12ce92c6d9
4 changed files with 90 additions and 40 deletions

View File

@ -174,6 +174,43 @@ def _ensure_topic_columns() -> None:
conn.commit()
def _ensure_platform_indexes() -> None:
inspector = inspect(engine)
with engine.connect() as conn:
if inspector.has_table(BOT_ACTIVITY_EVENT_TABLE):
try:
conn.execute(
text(
f"""
CREATE INDEX IF NOT EXISTS idx_bot_activity_event_bot_id_request_present
ON {BOT_ACTIVITY_EVENT_TABLE} (bot_id)
WHERE request_id IS NOT NULL AND request_id <> ''
"""
)
)
except Exception:
# Fall back silently when the current database dialect does not support partial indexes.
conn.execute(
text(
f"""
CREATE INDEX IF NOT EXISTS idx_bot_activity_event_bot_id
ON {BOT_ACTIVITY_EVENT_TABLE} (bot_id)
"""
)
)
if inspector.has_table(BOT_REQUEST_USAGE_TABLE):
conn.execute(
text(
f"""
CREATE INDEX IF NOT EXISTS idx_bot_request_usage_started_at_bot_id
ON {BOT_REQUEST_USAGE_TABLE} (started_at, bot_id)
"""
)
)
conn.commit()
def align_postgres_sequences() -> None:
if engine.dialect.name != "postgresql":
return
@ -215,6 +252,7 @@ def init_database() -> None:
_ensure_bot_request_usage_columns()
_ensure_botinstance_columns()
_ensure_topic_columns()
_ensure_platform_indexes()
align_postgres_sequences()
finally:
_release_migration_lock(lock_conn)

View File

@ -104,24 +104,34 @@ def list_activity_events(
def get_bot_activity_stats(session: Session) -> List[Dict[str, Any]]:
from sqlalchemy import and_, func
from sqlalchemy import func
from models.bot import BotInstance
from models.platform import BotRequestUsage
today = _utcnow().date()
first_day = today - timedelta(days=6)
first_started_at = datetime.combine(first_day, datetime.min.time())
activity_counts = (
select(
BotRequestUsage.bot_id.label("bot_id"),
func.count(BotRequestUsage.id).label("count"),
)
.where(BotRequestUsage.started_at >= first_started_at)
.group_by(BotRequestUsage.bot_id)
.subquery()
)
stmt = (
select(BotInstance.id, BotInstance.name, func.count(BotActivityEvent.id).label("count"))
.select_from(BotInstance)
.join(
BotActivityEvent,
and_(
BotActivityEvent.bot_id == BotInstance.id,
BotActivityEvent.request_id.is_not(None),
func.length(func.trim(BotActivityEvent.request_id)) > 0,
),
isouter=True,
select(
BotInstance.id,
BotInstance.name,
func.coalesce(activity_counts.c.count, 0).label("count"),
)
.select_from(BotInstance)
.join(activity_counts, activity_counts.c.bot_id == BotInstance.id, isouter=True)
.where(BotInstance.enabled.is_(True))
.group_by(BotInstance.id, BotInstance.name)
.order_by(func.count(BotActivityEvent.id).desc(), BotInstance.name.asc(), BotInstance.id.asc())
.order_by(func.coalesce(activity_counts.c.count, 0).desc(), BotInstance.name.asc(), BotInstance.id.asc())
)
results = session.exec(stmt).all()

View File

@ -46,7 +46,7 @@
.platform-bot-list-scroll {
display: flex;
flex-direction: column;
gap: 10px;
gap: 8px;
min-height: 0;
overflow: auto;
padding-right: 2px;
@ -57,8 +57,8 @@
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 14px 14px 18px;
gap: 6px;
padding: 10px 12px 10px 16px;
border-radius: 16px;
border: 1px solid var(--line);
background: linear-gradient(145deg, color-mix(in oklab, var(--panel-soft) 86%, var(--panel) 14%), color-mix(in oklab, var(--panel-soft) 94%, transparent 6%));
@ -147,11 +147,12 @@
.platform-bot-name-row {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 5px;
}
.platform-bot-name {
font-size: 16px;
font-size: 15px;
line-height: 1.2;
font-weight: 800;
color: var(--title);
min-width: 0;
@ -162,9 +163,10 @@
.platform-bot-id,
.platform-bot-meta {
margin-top: 2px;
margin-top: 0;
color: var(--subtitle);
font-size: 12px;
font-size: 11px;
line-height: 1.25;
font-weight: 600;
}
@ -184,17 +186,17 @@
}
.platform-bot-actions {
margin-top: 10px;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
gap: 4px;
}
.platform-bot-actions-main {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 4px;
}
.platform-enable-switch {
@ -214,8 +216,8 @@
.platform-enable-switch-track {
position: relative;
width: 36px;
height: 20px;
width: 32px;
height: 18px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--line) 82%, transparent);
background: color-mix(in oklab, #9ca3b5 42%, var(--panel-soft) 58%);
@ -227,8 +229,8 @@
position: absolute;
left: 2px;
top: 2px;
width: 14px;
height: 14px;
width: 12px;
height: 12px;
border-radius: 999px;
background: color-mix(in oklab, var(--text) 75%, #fff 25%);
transition: transform 0.2s ease, background 0.2s ease;
@ -240,7 +242,7 @@
}
.platform-enable-switch input:checked + .platform-enable-switch-track::after {
transform: translateX(16px);
transform: translateX(14px);
background: #fff;
}
@ -250,18 +252,18 @@
}
.platform-bot-icon-btn {
width: 36px;
height: 36px;
width: 30px;
height: 30px;
padding: 0;
border-radius: 10px;
border-radius: 9px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.platform-bot-icon-btn svg {
width: 17px;
height: 17px;
width: 15px;
height: 15px;
stroke-width: 2.1;
}
@ -329,9 +331,9 @@
}
.platform-bot-lock {
width: 16px;
height: 16px;
min-width: 16px;
width: 14px;
height: 14px;
min-width: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
@ -339,8 +341,8 @@
}
.platform-bot-lock svg {
width: 12px;
height: 12px;
width: 11px;
height: 11px;
stroke-width: 2.2;
}

View File

@ -23,7 +23,7 @@ export function PlatformBotActivityAnalyticsSection({
<div>
<h2>{isZh ? 'Bot 活跃度分析' : 'Bot Activity Analytics'}</h2>
<div className="platform-model-analytics-subtitle">
{isZh ? '基于 bot_activity_event 中 request_id 非空记录的活跃度统计' : 'Activity statistics based on non-empty request_id rows in bot_activity_event'}
{isZh ? '最近 7 天 · 基于模型调用记录的 Bot 请求统计' : 'Last 7 days · Bot request statistics based on model usage records'}
</div>
</div>
<div className="platform-model-analytics-total">
@ -37,7 +37,7 @@ export function PlatformBotActivityAnalyticsSection({
) : null}
{!loading && (!activityStats || activityStats.length === 0) ? (
<div className="ops-empty-inline">{isZh ? '暂无活跃度数据。' : 'No activity data.'}</div>
<div className="ops-empty-inline">{isZh ? '最近 7 天暂无活跃度数据。' : 'No activity data in the last 7 days.'}</div>
) : null}
{activityStats && activityStats.length > 0 ? (() => {