v0.1.4-p4
parent
9699b4e7c9
commit
12ce92c6d9
|
|
@ -174,6 +174,43 @@ def _ensure_topic_columns() -> None:
|
||||||
conn.commit()
|
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:
|
def align_postgres_sequences() -> None:
|
||||||
if engine.dialect.name != "postgresql":
|
if engine.dialect.name != "postgresql":
|
||||||
return
|
return
|
||||||
|
|
@ -215,6 +252,7 @@ def init_database() -> None:
|
||||||
_ensure_bot_request_usage_columns()
|
_ensure_bot_request_usage_columns()
|
||||||
_ensure_botinstance_columns()
|
_ensure_botinstance_columns()
|
||||||
_ensure_topic_columns()
|
_ensure_topic_columns()
|
||||||
|
_ensure_platform_indexes()
|
||||||
align_postgres_sequences()
|
align_postgres_sequences()
|
||||||
finally:
|
finally:
|
||||||
_release_migration_lock(lock_conn)
|
_release_migration_lock(lock_conn)
|
||||||
|
|
|
||||||
|
|
@ -104,24 +104,34 @@ def list_activity_events(
|
||||||
|
|
||||||
|
|
||||||
def get_bot_activity_stats(session: Session) -> List[Dict[str, Any]]:
|
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.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 = (
|
stmt = (
|
||||||
select(BotInstance.id, BotInstance.name, func.count(BotActivityEvent.id).label("count"))
|
select(
|
||||||
.select_from(BotInstance)
|
BotInstance.id,
|
||||||
.join(
|
BotInstance.name,
|
||||||
BotActivityEvent,
|
func.coalesce(activity_counts.c.count, 0).label("count"),
|
||||||
and_(
|
|
||||||
BotActivityEvent.bot_id == BotInstance.id,
|
|
||||||
BotActivityEvent.request_id.is_not(None),
|
|
||||||
func.length(func.trim(BotActivityEvent.request_id)) > 0,
|
|
||||||
),
|
|
||||||
isouter=True,
|
|
||||||
)
|
)
|
||||||
|
.select_from(BotInstance)
|
||||||
|
.join(activity_counts, activity_counts.c.bot_id == BotInstance.id, isouter=True)
|
||||||
.where(BotInstance.enabled.is_(True))
|
.where(BotInstance.enabled.is_(True))
|
||||||
.group_by(BotInstance.id, BotInstance.name)
|
.order_by(func.coalesce(activity_counts.c.count, 0).desc(), BotInstance.name.asc(), BotInstance.id.asc())
|
||||||
.order_by(func.count(BotActivityEvent.id).desc(), BotInstance.name.asc(), BotInstance.id.asc())
|
|
||||||
)
|
)
|
||||||
results = session.exec(stmt).all()
|
results = session.exec(stmt).all()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
.platform-bot-list-scroll {
|
.platform-bot-list-scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
|
|
@ -57,8 +57,8 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
padding: 14px 14px 14px 18px;
|
padding: 10px 12px 10px 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--line);
|
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%));
|
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 {
|
.platform-bot-name-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-name {
|
.platform-bot-name {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
|
line-height: 1.2;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--title);
|
color: var(--title);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -162,9 +163,10 @@
|
||||||
|
|
||||||
.platform-bot-id,
|
.platform-bot-id,
|
||||||
.platform-bot-meta {
|
.platform-bot-meta {
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
color: var(--subtitle);
|
color: var(--subtitle);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
line-height: 1.25;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,17 +186,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-actions {
|
.platform-bot-actions {
|
||||||
margin-top: 10px;
|
margin-top: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-actions-main {
|
.platform-bot-actions-main {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-enable-switch {
|
.platform-enable-switch {
|
||||||
|
|
@ -214,8 +216,8 @@
|
||||||
|
|
||||||
.platform-enable-switch-track {
|
.platform-enable-switch-track {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 36px;
|
width: 32px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid color-mix(in oklab, var(--line) 82%, transparent);
|
border: 1px solid color-mix(in oklab, var(--line) 82%, transparent);
|
||||||
background: color-mix(in oklab, #9ca3b5 42%, var(--panel-soft) 58%);
|
background: color-mix(in oklab, #9ca3b5 42%, var(--panel-soft) 58%);
|
||||||
|
|
@ -227,8 +229,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
width: 14px;
|
width: 12px;
|
||||||
height: 14px;
|
height: 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: color-mix(in oklab, var(--text) 75%, #fff 25%);
|
background: color-mix(in oklab, var(--text) 75%, #fff 25%);
|
||||||
transition: transform 0.2s ease, background 0.2s ease;
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
|
@ -240,7 +242,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-enable-switch input:checked + .platform-enable-switch-track::after {
|
.platform-enable-switch input:checked + .platform-enable-switch-track::after {
|
||||||
transform: translateX(16px);
|
transform: translateX(14px);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,18 +252,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-icon-btn {
|
.platform-bot-icon-btn {
|
||||||
width: 36px;
|
width: 30px;
|
||||||
height: 36px;
|
height: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 10px;
|
border-radius: 9px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-icon-btn svg {
|
.platform-bot-icon-btn svg {
|
||||||
width: 17px;
|
width: 15px;
|
||||||
height: 17px;
|
height: 15px;
|
||||||
stroke-width: 2.1;
|
stroke-width: 2.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,9 +331,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-lock {
|
.platform-bot-lock {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
min-width: 16px;
|
min-width: 14px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -339,8 +341,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-bot-lock svg {
|
.platform-bot-lock svg {
|
||||||
width: 12px;
|
width: 11px;
|
||||||
height: 12px;
|
height: 11px;
|
||||||
stroke-width: 2.2;
|
stroke-width: 2.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function PlatformBotActivityAnalyticsSection({
|
||||||
<div>
|
<div>
|
||||||
<h2>{isZh ? 'Bot 活跃度分析' : 'Bot Activity Analytics'}</h2>
|
<h2>{isZh ? 'Bot 活跃度分析' : 'Bot Activity Analytics'}</h2>
|
||||||
<div className="platform-model-analytics-subtitle">
|
<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>
|
</div>
|
||||||
<div className="platform-model-analytics-total">
|
<div className="platform-model-analytics-total">
|
||||||
|
|
@ -37,7 +37,7 @@ export function PlatformBotActivityAnalyticsSection({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!loading && (!activityStats || activityStats.length === 0) ? (
|
{!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}
|
) : null}
|
||||||
|
|
||||||
{activityStats && activityStats.length > 0 ? (() => {
|
{activityStats && activityStats.length > 0 ? (() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue