from __future__ import annotations
import html
def _render_admin_dashboard(data,tasks=None):
esc=lambda x:html.escape(str(x if x is not None else ""))
summary=data.get("summary") or {}
users=data.get("top_users") or []
daily=data.get("daily") or []
event_types=data.get("event_types") or []
tasks=tasks or []
event_counts={
str(x.get("event_type") or ""):
int(x.get("count") or 0)
for x in event_types
}
data_operations=sum(
count
for name,count in event_counts.items()
if (
name.startswith("marine_")
or name.startswith("fisheries_")
or name.startswith("processing_")
or name.startswith("upload")
)
)
cards=[
("总用户",summary.get("total_users",0)),
("近24h活跃",summary.get("active_24h",0)),
("7天活跃",summary.get("active_7d",0)),
("30天活跃",summary.get("active_30d",0)),
("聊天次数",summary.get("total_chats",0)),
("会话数",summary.get("total_threads",0)),
("长期记忆",summary.get("total_memories",0)),
("数据请求",data_operations),
("数据查询",
event_counts.get("marine_query",0)
+ event_counts.get("fisheries_query",0)),
("数据处理",
event_counts.get("marine_subset",0)
+ event_counts.get("marine_export",0)
+ event_counts.get("processing_completed",0)),
("数据导出",event_counts.get("marine_export",0)),
("用户上传",event_counts.get("upload_completed",0)),
("数据资产",summary.get("total_assets",0)),
("点击下载",event_counts.get("download_clicked",0)),
]
latest_processing_html="""
"""
latest_task=None
for task in tasks:
if (
task.get("event_type")=="processing_completed"
or task.get("status")=="completed"
):
latest_task=task
break
if latest_task:
payload=latest_task.get("payload") or {}
result=payload.get("result") or {}
results=result.get("results") or []
uid=latest_task.get("user_id") or "-"
created=latest_task.get("created_at") or "-"
operation=latest_task.get("operation") or "-"
runtime=payload.get("runtime") or "-"
status=latest_task.get("status") or "completed"
filename="-"
rows="-"
cols="-"
missing=0
missing_fields="-"
duplicate_rows=0
month_text="-"
bad_lon=0
bad_lat=0
grid_groups=0
grid_rows=0
fishing_bad=0
if results:
r=results[0]
filename=r.get("filename") or "-"
rows=r.get("row_count","-")
cols=r.get("column_count","-")
mv=r.get("missing_values") or {}
missing=mv.get("total_missing_cells",0)
bycol=mv.get("by_column") or {}
if bycol:
missing_fields=";".join(
f"{k}={v}"
for k,v in bycol.items()
)
else:
missing_fields="无"
dup=r.get("exact_duplicates") or {}
duplicate_rows=dup.get("count",0)
month=r.get("month_check") or {}
if month.get("column"):
mm=month.get("missing_months") or []
month_text="无" if not mm else "、".join(map(str,mm))
else:
month_text="未识别到月份字段"
coord=r.get("coordinate_check") or {}
bad_lon=coord.get("invalid_longitude_count",0)
bad_lat=coord.get("invalid_latitude_count",0)
grid=r.get("duplicate_grid_check") or {}
grid_groups=grid.get("duplicate_group_count",0)
grid_rows=grid.get("duplicate_row_count",0)
fh=r.get("fishing_hours_check") or {}
fishing_bad=fh.get("invalid_count",0)
latest_processing_html=f"""
{esc(filename)}
{esc(rows)} 行 × {esc(cols)} 列
缺失值{esc(missing)} 个
完全重复行{esc(duplicate_rows)} 条
异常经度{esc(bad_lon)} 条
异常纬度{esc(bad_lat)} 条
重复格点{esc(grid_groups)} 组 / {esc(grid_rows)} 行
fishing > total{esc(fishing_bad)} 条
缺失字段
{esc(missing_fields)}
月份检查
{esc(month_text)}
"""
cards_html="".join(
f'
{esc(v)}{esc(k)}
'
for k,v in cards
)
user_rows="".join(
""
f'| {esc(x.get("user_id"))} | '
f"{esc(x.get('chats'))} | "
f"{esc(x.get('threads'))} | "
f"{esc(x.get('memories'))} | "
f"{esc(x.get('assets'))} | "
f"{esc(x.get('last_active') or '-')} | "
"
"
for x in users
)
daily_rows="".join(
""
f"| {esc(x.get('day'))} | "
f"{esc(x.get('active_users'))} | "
f"{esc(x.get('chats'))} | "
f"{esc(x.get('threads'))} | "
f"{esc(x.get('events'))} | "
"
"
for x in daily
)
return f"""
Squid 用户统计
🦑 Squid 用户使用统计
自动刷新:30 秒 · 数据来自学校服务器 memory.db
{cards_html}
{latest_processing_html}
用户使用情况
| User ID | 聊天 | 会话 |
长期记忆 | 数据资产 | 最近活跃 |
{user_rows}
最近每日活动
| 日期 | 活跃用户 | 聊天 |
新会话 | 全部事件 |
{daily_rows}
"""
def _render_admin_user_detail(data,tasks):
esc=lambda x:html.escape(str(x if x is not None else ""))
user=data.get("user") or {}
control=data.get("control") or {}
memories=data.get("memories") or []
assets=data.get("assets") or []
events=data.get("events") or []
uid=user.get("user_id") or control.get("user_id") or ""
user_tasks=[
x for x in (tasks or [])
if str(x.get("user_id") or "")==str(uid)
]
status_text="已禁用" if control.get("disabled") else "正常"
upload_text="允许" if control.get("allow_upload") else "禁止"
download_text="允许" if control.get("allow_download") else "禁止"
quota=control.get("daily_chat_quota",0)
quota_text="不限" if not quota else str(quota)
memory_rows=""
for m in memories[:30]:
text=(
m.get("content")
or m.get("text")
or m.get("value")
or m.get("memory")
or ""
)
kind=m.get("kind") or m.get("type") or ""
if text:
memory_rows += (
""
f"{esc(text)}"
f"{esc(kind)}"
"
"
)
if not memory_rows:
memory_rows="暂无长期记忆
"
asset_rows=""
for a in assets[:50]:
name=a.get("name") or a.get("filename") or "未命名"
op=a.get("operation") or "-"
status=a.get("status") or "-"
size=a.get("size_bytes") or a.get("size") or 0
created=a.get("created_at") or "-"
try:
size=int(size)
if size<1024:
size_text=f"{size} B"
elif size<1024**2:
size_text=f"{size/1024:.1f} KB"
elif size<1024**3:
size_text=f"{size/1024**2:.1f} MB"
else:
size_text=f"{size/1024**3:.2f} GB"
except Exception:
size_text="-"
asset_rows += (
""
f"| {esc(name)} | "
f"{esc(op)} | "
f"{esc(size_text)} | "
f"{esc(status)} | "
f"{esc(created)} | "
"
"
)
if not asset_rows:
asset_rows="| 暂无数据资产 |
"
task_html=""
for t in user_tasks[:30]:
payload=t.get("payload") or {}
result=payload.get("result") or {}
results=result.get("results") or []
runtime=payload.get("runtime") or "-"
status=t.get("status") or t.get("event_type") or "-"
operation=t.get("operation") or "-"
created=t.get("created_at") or "-"
abnormal=t.get("abnormal")
cancel=t.get("cancel_requested")
detail=""
if results:
r=results[0]
filename=r.get("filename") or "-"
rows=r.get("row_count","-")
cols=r.get("column_count","-")
mv=r.get("missing_values") or {}
miss=mv.get("total_missing_cells",0)
dup=r.get("exact_duplicates") or {}
dup_count=dup.get("count",0)
month=r.get("month_check") or {}
if month.get("column"):
mm=month.get("missing_months") or []
month_text="无" if not mm else ", ".join(map(str,mm))
else:
month_text="未识别月份字段"
coord=r.get("coordinate_check") or {}
bad_lon=coord.get("invalid_longitude_count",0)
bad_lat=coord.get("invalid_latitude_count",0)
grid=r.get("duplicate_grid_check") or {}
grid_groups=grid.get("duplicate_group_count",0)
fh=r.get("fishing_hours_check") or {}
fh_bad=fh.get("invalid_count",0)
detail=f"""
输入文件{esc(filename)}
数据规模{esc(rows)} 行 × {esc(cols)} 列
缺失值{esc(miss)} 个
完全重复行{esc(dup_count)} 条
月份检查{esc(month_text)}
异常经度{esc(bad_lon)} 条
异常纬度{esc(bad_lat)} 条
重复格点{esc(grid_groups)} 组
fishing > total{esc(fh_bad)} 条
"""
flags=[]
if abnormal:
flags.append("⚠️ 异常")
if cancel:
flags.append("已请求取消")
flag_text=" · ".join(flags)
task_html += f"""
{esc(operation)}
{esc(created)}
{esc(status)}
{esc(runtime)}
{f'{esc(flag_text)}' if flag_text else ''}
{detail}
"""
if not task_html:
task_html="暂无数据处理任务
"
names={
"thread_created":"新建对话",
"chat":"发送消息",
"upload_completed":"上传文件",
"attachment_used":"使用上传文件",
"processing_started":"开始数据处理",
"processing_completed":"完成数据处理",
"processing_failed":"数据处理失败",
"marine_query":"查询海洋数据",
"marine_subset":"裁剪海洋数据",
"marine_export":"导出海洋数据",
"fisheries_query":"查询渔业数据",
"download_clicked":"下载文件",
"admin_asset_deleted":"管理员删除资产记录",
}
timeline=""
for e in events[:40]:
et=e.get("event_type") or e.get("type") or "-"
tm=e.get("created_at") or "-"
timeline += (
""
f"{esc(tm)}"
f"{esc(names.get(et,et))}"
"
"
)
if not timeline:
timeline="暂无操作记录
"
return f"""
Squid 用户详情
{esc(status_text)}用户状态
{esc(control.get("today_chats",0))}今日聊天
{esc(quota_text)}每日聊天配额
{esc(upload_text)}上传权限
{esc(download_text)}下载权限
{len(assets)}数据资产
{len(memories)}长期记忆
{len(user_tasks)}数据处理任务
"""