Skip to content

Commit 5bda6c8

Browse files
committed
新增分析日志面板和清空日志功能,增强WebUI的实时监控能力
1 parent 881a8d8 commit 5bda6c8

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- **LLM 安全审计升级**:在神盾/焦土/拦截模式下,自动触发结构化 JSON 判定并给出风险理由,支持根据置信度动态调节严重级别。
1717
- **拦截事件追踪**:新增实时统计面板与历史记录(默认记最近 100 条),帮助管理员快速定位攻击来源与触发原因。
1818
- **内置 WebUI 控制台**:零配置即可启动,提供状态总览、黑白名单管理、防护模式切换、配置修改、拦截记录浏览等功能,支持令牌加密访问。
19+
- **实时分析日志面板**:WebUI 增加分析日志区域,可即时查看最近检测结果,确认插件正在运行。
1920
- **更强的自动封禁链路**:启发式命中 + LLM 判定后自动进入黑名单,可配置封禁时长,强力阻断“反复越狱用户”。
2021

2122
---
@@ -44,6 +45,7 @@
4445
- 快捷操作:一键切换模式、切换 LLM 策略、清空历史等
4546
- 名单管理:可视化增删黑白名单(支持设置封禁时长)
4647
- 拦截记录:展示最近 N 条风险事件(时间、来源、触发规则、置信度、预览内容)
48+
- 分析日志:滚动显示最新检测结果(时间、来源、结果、触发器与原因)
4749

4850
---
4951

main.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,9 @@ def save():
542542
elif action == "clear_history":
543543
self.plugin.recent_incidents.clear()
544544
message = "已清空拦截记录"
545+
elif action == "clear_logs":
546+
self.plugin.analysis_logs.clear()
547+
message = "已清空分析日志"
545548
else:
546549
message = "未知操作"
547550
success = False
@@ -551,6 +554,7 @@ def _render_dashboard(self, token: str, notice: str, success: bool) -> str:
551554
config = self.plugin.config
552555
stats = self.plugin.stats
553556
incidents = list(self.plugin.recent_incidents)
557+
analysis_logs = list(self.plugin.analysis_logs)
554558
whitelist = config.get("whitelist", [])
555559
blacklist = config.get("blacklist", {})
556560
defense_mode = config.get("defense_mode", "sentry")
@@ -649,6 +653,11 @@ def _render_dashboard(self, token: str, notice: str, success: bool) -> str:
649653
f"<input type='hidden' name='action' value='clear_history'/>"
650654
f"<button class='btn danger' type='submit'>清空拦截记录</button></form>"
651655
)
656+
html_parts.append(
657+
f"<form class='inline-form' method='get' action='/'>{token_input}"
658+
f"<input type='hidden' name='action' value='clear_logs'/>"
659+
f"<button class='btn danger' type='submit'>清空分析日志</button></form>"
660+
)
652661
html_parts.append("</div></div>")
653662

654663
html_parts.append("</div>")
@@ -733,6 +742,31 @@ def _render_dashboard(self, token: str, notice: str, success: bool) -> str:
733742
html_parts.append("<div class='small'>尚未记录拦截事件。</div>")
734743
html_parts.append("</section>")
735744

745+
html_parts.append("<section class='card'><h3>分析日志</h3>")
746+
if analysis_logs:
747+
html_parts.append("<table><thead><tr><th>时间</th><th>来源</th><th>结果</th><th>严重级别</th><th>得分</th><th>触发</th><th>原因</th><th>内容预览</th></tr></thead><tbody>")
748+
for item in analysis_logs[:50]:
749+
timestamp = datetime.fromtimestamp(item["time"]).strftime("%Y-%m-%d %H:%M:%S")
750+
source = item["sender_id"]
751+
if item.get("group_id"):
752+
source = f"{source} @ {item['group_id']}"
753+
html_parts.append(
754+
"<tr>"
755+
f"<td>{escape(timestamp)}</td>"
756+
f"<td>{escape(str(source))}</td>"
757+
f"<td>{escape(item.get('result', ''))}</td>"
758+
f"<td>{escape(item.get('severity', ''))}</td>"
759+
f"<td>{escape(str(item.get('score', 0)))}</td>"
760+
f"<td>{escape(item.get('trigger', ''))}</td>"
761+
f"<td>{escape(item.get('reason', ''))}</td>"
762+
f"<td>{escape(item.get('prompt_preview', ''))}</td>"
763+
"</tr>"
764+
)
765+
html_parts.append("</tbody></table>")
766+
else:
767+
html_parts.append("<div class='small'>暂无分析日志,可等待消息经过后查看。</div>")
768+
html_parts.append("</section>")
769+
736770
html_parts.append("</div></body></html>")
737771
return "\n".join(html_parts)
738772

@@ -798,6 +832,7 @@ def __init__(self, context: Context, config: AstrBotConfig = None):
798832
self.detector = PromptThreatDetector()
799833
history_size = max(10, int(self.config.get("incident_history_size", 100)))
800834
self.recent_incidents: deque = deque(maxlen=history_size)
835+
self.analysis_logs: deque = deque(maxlen=200)
801836
self.stats: Dict[str, int] = {
802837
"total_intercepts": 0,
803838
"regex_hits": 0,
@@ -853,6 +888,20 @@ def _record_incident(self, event: AstrMessageEvent, analysis: Dict[str, Any], de
853888
else:
854889
self.stats["heuristic_hits"] += 1
855890

891+
def _append_analysis_log(self, event: AstrMessageEvent, analysis: Dict[str, Any], intercepted: bool):
892+
entry = {
893+
"time": time.time(),
894+
"sender_id": event.get_sender_id(),
895+
"group_id": event.get_group_id(),
896+
"severity": analysis.get("severity", "none"),
897+
"score": analysis.get("score", 0),
898+
"trigger": analysis.get("trigger", "scan"),
899+
"result": "拦截" if intercepted else "放行",
900+
"reason": analysis.get("reason") or ("未检测到明显风险" if not intercepted else "检测到风险"),
901+
"prompt_preview": self._make_prompt_preview(analysis.get("prompt", "")),
902+
}
903+
self.analysis_logs.appendleft(entry)
904+
856905
def _build_stats_summary(self) -> str:
857906
return (
858907
"🛡️ 反注入防护统计:\n"
@@ -1044,6 +1093,7 @@ async def intercept_llm_request(self, event: AstrMessageEvent, req: ProviderRequ
10441093
"trigger": "blacklist",
10451094
}
10461095
self._record_incident(event, analysis, self.config.get("defense_mode", "sentry"), "blacklist")
1096+
self._append_analysis_log(event, analysis, True)
10471097
event.stop_event()
10481098
return
10491099
del blacklist[sender_id]
@@ -1067,7 +1117,17 @@ async def intercept_llm_request(self, event: AstrMessageEvent, req: ProviderRequ
10671117
await self._apply_scorch_defense(req)
10681118
event.stop_event()
10691119

1120+
analysis["reason"] = reason
10701121
self._record_incident(event, analysis, defense_mode, defense_mode)
1122+
self._append_analysis_log(event, analysis, True)
1123+
else:
1124+
if not analysis.get("reason"):
1125+
analysis["reason"] = "未检测到明显风险"
1126+
if not analysis.get("severity"):
1127+
analysis["severity"] = "none"
1128+
if not analysis.get("trigger"):
1129+
analysis["trigger"] = "scan"
1130+
self._append_analysis_log(event, analysis, False)
10711131
except Exception as exc:
10721132
logger.error(f"⚠️ [拦截] 注入分析时发生错误: {exc}")
10731133
await self._apply_scorch_defense(req)

0 commit comments

Comments
 (0)