feat(monitor): 给博主起备注 —— 作品栏里才认得出「这是谁」
作品栏按 creator_hash 把作品归到博主的组里,可那是个哈希;creator_name 是平台昵称,
粉丝少的号常常认不出。两样都没法把账号对上人。
* 新表 monitor_creator_alias,键取 **(platform, creator_hash)** 而不是按任务:
哈希对同一个 uid 是稳定的,所以同一个博主出现在多个任务里时,备注只填一次。
* list_notes 带上 creator_alias(整体查一次再在内存里取,不是每条作品查一次)。
* `PUT /monitor/creators/{creator_hash}`,空串即清掉那条备注。
* 作品栏的博主组头:**优先显示备注**,起过备注之后平台昵称降成副标题(它仍是有用的
对照);组头上一个铅笔按钮就地编辑,回车保存、失焦保存、Esc 取消。
Esc 那条要单独处理:取消之后紧接着的失焦会把刚放弃的内容存进去,所以用一个标记让那次
失焦闭嘴。
测试 +6:没起过时是空串、起了会跟着作品返回、**跨任务共用一条**、**不串到别的平台**、
空串清掉、前后空格会被去掉。
This commit is contained in:
@@ -320,6 +320,29 @@ class MonitorEvent(MonitorBase):
|
||||
is_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class MonitorCreatorAlias(MonitorBase):
|
||||
"""给博主起的备注。
|
||||
|
||||
作品栏和评论栏都按 ``creator_hash`` 把作品归到博主名下,可那是个哈希;
|
||||
``creator_name`` 是平台上的昵称(而且粉丝少的号常常没有)。两样都认不出"这是谁"。
|
||||
备注是**人自己起的名字**(「竞品A」「自家号-3」),用来把账号对上人。
|
||||
|
||||
键取 ``(platform, creator_hash)``:哈希对同一个 uid 是稳定的,所以同一个博主出现在
|
||||
多个任务里时备注也是同一个,不用每个任务各填一遍。
|
||||
"""
|
||||
|
||||
__tablename__ = "monitor_creator_alias"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("platform", "creator_hash", name="uq_creator_alias"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
platform: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
||||
creator_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
alias: Mapped[str] = mapped_column(String(128), nullable=False, default="")
|
||||
updated_at: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
|
||||
|
||||
class MonitorSetting(MonitorBase):
|
||||
"""Key/value store. Holds the XHS cookie for unattended runs."""
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from .models import (
|
||||
MODE_CREATOR,
|
||||
MODE_NOTE,
|
||||
MonitorComment,
|
||||
MonitorCreatorAlias,
|
||||
MonitorEvent,
|
||||
MonitorNote,
|
||||
MonitorNoteMetric,
|
||||
@@ -328,6 +329,48 @@ def trigger_manual_run(task_id: int) -> None:
|
||||
# Dashboard queries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _creator_alias_map(session: AsyncSession) -> Dict[tuple, str]:
|
||||
"""``(platform, creator_hash) -> 备注``。
|
||||
|
||||
整体读一次再在内存里取,而不是每条作品查一次 —— 作品列表动辄几十条。备注本身很少,
|
||||
表不会大。
|
||||
"""
|
||||
rows = (await session.scalars(select(MonitorCreatorAlias))).all()
|
||||
return {(row.platform, row.creator_hash): row.alias for row in rows if row.alias}
|
||||
|
||||
|
||||
async def set_creator_alias(
|
||||
session: AsyncSession, platform: str, creator_hash: str, alias: str
|
||||
) -> None:
|
||||
"""给博主起备注;传空串就是删掉这条备注(界面上的"清空")。"""
|
||||
alias = (alias or "").strip()[:128]
|
||||
existing = await session.scalar(
|
||||
select(MonitorCreatorAlias).where(
|
||||
MonitorCreatorAlias.platform == platform,
|
||||
MonitorCreatorAlias.creator_hash == creator_hash,
|
||||
)
|
||||
)
|
||||
|
||||
if not alias:
|
||||
if existing is not None:
|
||||
await session.delete(existing)
|
||||
return
|
||||
|
||||
if existing is None:
|
||||
session.add(
|
||||
MonitorCreatorAlias(
|
||||
platform=platform,
|
||||
creator_hash=creator_hash,
|
||||
alias=alias,
|
||||
updated_at=get_current_timestamp(),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
existing.alias = alias
|
||||
existing.updated_at = get_current_timestamp()
|
||||
|
||||
|
||||
async def _latest_successful_run_id(session: AsyncSession, task_id: int) -> Optional[int]:
|
||||
return await session.scalar(
|
||||
select(MonitorRun.id)
|
||||
@@ -387,6 +430,19 @@ async def list_notes(
|
||||
latest_run_ids: Dict[int, Optional[int]] = {}
|
||||
result: List[Dict[str, Any]] = []
|
||||
|
||||
# 博主的备注。键是 (platform, creator_hash) —— 作品的 platform 挂在它的任务上。
|
||||
task_platform = {
|
||||
row.id: row.platform
|
||||
for row in (
|
||||
await session.execute(
|
||||
select(MonitorTask.id, MonitorTask.platform).where(
|
||||
MonitorTask.id.in_({note.task_id for note in notes})
|
||||
)
|
||||
)
|
||||
).all()
|
||||
}
|
||||
aliases = await _creator_alias_map(session)
|
||||
|
||||
for note in notes:
|
||||
series = by_note.get(note.note_id, [])
|
||||
current = series[0] if series else None
|
||||
@@ -414,6 +470,10 @@ async def list_notes(
|
||||
# (见 config.MASK_NICKNAME),所以就是原文。
|
||||
"creator_hash": note.creator_hash,
|
||||
"creator_name": note.creator_name,
|
||||
# 人自己起的备注,界面上优先显示它 —— 昵称认不出是谁,哈希更认不出。
|
||||
"creator_alias": aliases.get(
|
||||
(task_platform.get(note.task_id, ""), note.creator_hash), ""
|
||||
),
|
||||
# 优先给本地缓存地址:远程地址带签名、会过期(实测隔天即 403),
|
||||
# 本地那份不会。没有缓存时才退回远程,至少让图先显示出来。
|
||||
"cover": covers.cover_url(note.note_id, note.cover),
|
||||
|
||||
@@ -38,6 +38,7 @@ from ..monitor.settings import (
|
||||
from ..monitor.models import SETTING_WECOM_WEBHOOK, MonitorTask
|
||||
from ..schemas.monitor import (
|
||||
CookiePayload,
|
||||
CreatorAliasPayload,
|
||||
MonitorTaskCreate,
|
||||
MonitorTaskUpdate,
|
||||
WebhookPayload,
|
||||
@@ -253,6 +254,27 @@ async def clear_cookie_endpoint(platform: str = Query(default=PLATFORM_XHS)):
|
||||
return {"message": "Cookie cleared"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 博主备注
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.put("/creators/{creator_hash}")
|
||||
async def set_creator_alias_endpoint(
|
||||
creator_hash: str,
|
||||
payload: CreatorAliasPayload,
|
||||
platform: str = Query(default=PLATFORM_XHS),
|
||||
):
|
||||
"""给博主起个备注(界面上的「备注」)。
|
||||
|
||||
作品栏按 creator_hash 归组,可那是个哈希、昵称又常常认不出是谁 —— 备注是人自己起的
|
||||
名字。空串表示清掉这条备注。
|
||||
"""
|
||||
async with get_session() as session:
|
||||
await service.set_creator_alias(session, platform, creator_hash, payload.alias)
|
||||
return {"creator_hash": creator_hash, "alias": payload.alias.strip()}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QR login
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -108,6 +108,12 @@ class CookiePayload(BaseModel):
|
||||
cookie: str = Field(min_length=1)
|
||||
|
||||
|
||||
class CreatorAliasPayload(BaseModel):
|
||||
"""给博主起的备注。空串表示清掉这条备注。"""
|
||||
|
||||
alias: str = Field(default="", max_length=128)
|
||||
|
||||
|
||||
class WebhookPayload(BaseModel):
|
||||
url: str = Field(default="", description="企业微信机器人 Webhook 地址,留空表示停用")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user