From 0eb6ba31c5dd2f2322db99131cbb6ddfb0867473 Mon Sep 17 00:00:00 2001
From: butubb <1422726308@qq.com>
Date: Thu, 8 Oct 2026 09:43:19 +0800
Subject: [PATCH] =?UTF-8?q?feat(monitor):=20=E4=BD=9C=E5=93=81=E6=A0=8F?=
=?UTF-8?q?=E6=8C=89=E5=8D=9A=E4=B8=BB=E5=88=86=E7=BB=84=E6=8A=98=E5=8F=A0?=
=?UTF-8?q?=EF=BC=8C=E8=AF=84=E8=AE=BA=E6=A0=8F=E6=94=B9=E4=B8=BA=20?=
=?UTF-8?q?=E5=8D=9A=E4=B8=BB/=E4=BD=9C=E5=93=81/=E8=AF=84=E8=AE=BA=20?=
=?UTF-8?q?=E4=B8=89=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
先回答现状:跳转作品的按钮本来就有(作品栏每行末尾的外链图标);
评论栏原本是**两级**(作品 -> 评论),第一级是作品不是博主,所以并不是三级。
- MonitorNote 新增 creator_name(爬虫产出的 jsonl 里本来就有昵称,只是没存)。
存的是**已脱敏**的值(张***三),与项目一贯的匿名化姿态一致 —— 爬虫刻意不落原始
user_id(tools/user_hash.py),所以 creator_hash 是唯一稳定的分组依据。
实测该哈希是无盐 sha256,能用任务目标的 external_id 反算配对。
- 入库时刷新 creator_name:作者改昵称是常事,只在首次写一次会一直显示旧的
- 作品栏:按 creator_hash 分组,组头可折叠(默认展开 —— 折叠的默认值不该藏数据),
行内跳转按钮加了 title 说明
- 评论栏:一级博主、二级作品、三级评论。_note_meta_map 补上博主维度,
评论流和作品分组都带上它
- 认不出博主的作品归到「未知博主」,不丢
顺带修一个语法错误:JSX 注释放在三元表达式分支里是非法的(那是子节点语法不是表达式),
移进 div 内。
---
api/monitor/ingest.py | 4 +
api/monitor/models.py | 5 +
api/monitor/service.py | 11 +
webui/src/components/monitor/CommentsFeed.tsx | 88 +++++++-
webui/src/components/monitor/NotesTable.tsx | 201 ++++++++++++------
webui/src/types/monitor.ts | 16 ++
6 files changed, 256 insertions(+), 69 deletions(-)
diff --git a/api/monitor/ingest.py b/api/monitor/ingest.py
index fe30907..432c075 100644
--- a/api/monitor/ingest.py
+++ b/api/monitor/ingest.py
@@ -300,6 +300,7 @@ async def _ingest_notes(
note_url=record.get("note_url") or "",
cover=cover,
creator_hash=record.get("creator_hash") or "",
+ creator_name=record.get("nickname") or "",
source_kind=record.get("type") or "",
published_at=_as_int(record.get("time")),
first_seen_run_id=run.id,
@@ -328,6 +329,9 @@ async def _ingest_notes(
# 重跑也修不回来。落盘那份由 covers.cache_pending 负责(网络操作不在本模块)。
if cover:
note.cover = cover
+ # 昵称也要刷:作者改昵称是常事,只在首次入库写一次会一直显示旧的。
+ if record.get("nickname"):
+ note.creator_name = record["nickname"]
note.last_seen_run_id = run.id
note.last_seen_at = now
diff --git a/api/monitor/models.py b/api/monitor/models.py
index b4a4ef5..6b50479 100644
--- a/api/monitor/models.py
+++ b/api/monitor/models.py
@@ -220,7 +220,12 @@ class MonitorNote(MonitorBase):
title: Mapped[str] = mapped_column(Text, nullable=False, default="")
note_url: Mapped[str] = mapped_column(Text, nullable=False, default="")
cover: Mapped[str] = mapped_column(Text, nullable=False, default="")
+ # 创作者匿名哈希。爬虫刻意不落原始 user_id(见 tools/user_hash.py),
+ # 所以这是唯一稳定的创作者标识 —— 按博主分组就靠它。
creator_hash: Mapped[str] = mapped_column(String(64), nullable=False, default="")
+ # 创作者昵称,**已由爬虫脱敏**(张***三 这种)。存的是脱敏后的值,与项目一贯的
+ # 匿名化姿态一致;不存的话分组只能显示一串哈希,根本认不出是谁。
+ creator_name: Mapped[str] = mapped_column(String(200), nullable=False, default="")
source_kind: Mapped[str] = mapped_column(String(16), nullable=False, default="")
published_at: Mapped[Optional[int]] = mapped_column(BigInteger)
diff --git a/api/monitor/service.py b/api/monitor/service.py
index 74d34b6..eaa8e04 100644
--- a/api/monitor/service.py
+++ b/api/monitor/service.py
@@ -397,6 +397,10 @@ async def list_notes(
"note_id": note.note_id,
"title": note.title,
"note_url": note.note_url,
+ # 按博主分组用。creator_hash 是唯一稳定的创作者标识(原始 user_id
+ # 被爬虫刻意匿名化了),creator_name 是已脱敏的昵称。
+ "creator_hash": note.creator_hash,
+ "creator_name": note.creator_name,
# 优先给本地缓存地址:远程地址带签名、会过期(实测隔天即 403),
# 本地那份不会。没有缓存时才退回远程,至少让图先显示出来。
"cover": covers.cover_url(note.note_id, note.cover),
@@ -478,6 +482,9 @@ async def _note_meta_map(
"note_cover": covers.cover_url(row.note_id, row.cover),
"note_url": row.note_url,
"task_id": row.task_id,
+ # 博主维度也带上,评论流才能按 博主 -> 作品 -> 评论 三级展开。
+ "creator_hash": row.creator_hash,
+ "creator_name": row.creator_name,
}
for row in rows
}
@@ -523,6 +530,8 @@ async def list_comments(
"note_title": meta.get(row.note_id, {}).get("note_title", ""),
"note_cover": meta.get(row.note_id, {}).get("note_cover", ""),
"note_url": meta.get(row.note_id, {}).get("note_url", ""),
+ "note_creator_hash": meta.get(row.note_id, {}).get("creator_hash", ""),
+ "note_creator_name": meta.get(row.note_id, {}).get("creator_name", ""),
}
for row in comments
]
@@ -573,6 +582,8 @@ async def comment_note_groups(
"note_title": meta.get(note_id, {}).get("note_title", ""),
"note_cover": meta.get(note_id, {}).get("note_cover", ""),
"note_url": meta.get(note_id, {}).get("note_url", ""),
+ "creator_hash": meta.get(note_id, {}).get("creator_hash", ""),
+ "creator_name": meta.get(note_id, {}).get("creator_name", ""),
"comment_count": count,
"latest_at": latest.get(note_id, 0),
}
diff --git a/webui/src/components/monitor/CommentsFeed.tsx b/webui/src/components/monitor/CommentsFeed.tsx
index af0b199..14ce38d 100644
--- a/webui/src/components/monitor/CommentsFeed.tsx
+++ b/webui/src/components/monitor/CommentsFeed.tsx
@@ -7,6 +7,7 @@ import {
LayoutList,
Rows3,
ThumbsUp,
+ Users,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
@@ -156,6 +157,78 @@ function CollapsibleGroup({
)
}
+/** 把作品分组按博主归并 —— 三级视图的第一级。 */
+function groupByCreator(buckets: CommentBucket[]) {
+ const byCreator = new Map
加载中…
@@ -59,7 +85,13 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) { ) } - const isExpanded = (note: MonitorNote) => expanded === `${note.task_id}:${note.note_id}` + const toggleCreator = (key: string) => + setCollapsed((prev) => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + return next + }) return (