罗德岛通讯与技术部

用户自定义表情包(上传图片版)

USER STICKERS

├── manifest.json # per-user 清单:[{id, file, label, llm:false}]

状态:🟡 规划 · 最后核对 2026-08-13

2026-08-13 · 讨论结论:上传自己的图片 + 仅用户自己发(干员不主动用)

当前状态

维度 现状 问题
贴纸来源 只有官方贴纸(frontend/assets/stickers/ 用户不能发自己的图
事实源 共享 manifest.json(前后端共用) 用户数据无处安放,且不能写进共享清单
图片上传 项目至今没有(头像全是预置 slot) 这是第一个真正的 UGC 二进制上传功能
令牌 [sticker:id] 全局唯一 id 用户贴纸 id 需隔离,防撞官方 / 防跨用户引用

设计结论

  • 形态:用户上传自己的图片当贴纸(QQ 式自定义表情)。
  • 干员可用性:仅用户自己发,干员不主动用。
  • 令牌:沿用 [sticker:id] 格式,正则 STICKER_RE 天然匹配、不改;用户贴纸 id = u + uuid4().hex[:12],随机不可碰撞、天然与官方 id 隔离。
  • label:显示名,用户可选填,默认「自定义」;id 是内部键,用户不可见。

「仅用户自己发」是白捡的安全边界

scrub_reply 现在只认官方 manifest(man.get(id) 拿不到就剥掉)。用户贴纸 id 带 u_ 前缀、不在官方清单里,所以即使 LLM 学舌回了 [sticker:u_xxx] 也会被自动剥掉——干员用不了用户贴纸这件事由现有清洗逻辑现成保证,不用额外写。

存储布局(数据隔离铁律)

data/users/{username}/stickers/
├── manifest.json          # per-user 清单:[{id, file, label, llm:false}]
└── <uuid>.png             # 图片,uuid 命名,丢弃原始文件名(防路径穿越)

官方 manifest 仍是 frontend/assets/stickers/manifest.json互不污染。per-user manifest 是「我的」贴纸的唯一事实源。

API 设计(新增 app/api/stickers_api.py

方法 路径 作用
POST /api/stickers/upload multipart 上传,校验后存盘 + 更新 manifest,返回 {id, file, label, url}
DELETE /api/stickers/{id} 删图 + 删 manifest 条目
GET /api/stickers/list 返回用户自己的 manifest(前端渲染「我的」区)
GET /api/stickers/img/{filename} 图片 serve(带鉴权,见下)

⚠️ protected_pathsstartswith 匹配(main.py),要加 /api/stickers——别踩 /api/operator 匹配到 /api/operators 那种坑。

图片 serve 与鉴权

<img> 标签不能带 Authorization 头,三个选择:

方案 做法 代价
query token(推荐) <img src="/api/stickers/img/{f}?token=..."> token 进 URL(Referer/日志泄露)。本应用 token 本就 localStorage 明文、19 人熟人内网,务实可接受
fetch + blob objectURL 渲染时 await fetch 拿 blob 给 img 最安全,但 applyStickerTokens 要改异步,历史渲染全要 await
Cookie 鉴权 改鉴权体系 改造最大,不做

推荐 query token:原生壳的 fetch 重写已覆盖 /api/ 前缀,img.src 手动拼 __ASSET_BASE(和头像同款),Web / 原生壳两栖一致。

未决:若要求更严,可升级为 blob 方案。

前端改动(chat.js 为主)

  1. 启动时多 fetch 一份 per-user manifest/api/stickers/list),和官方 _stickerMap 合并成渲染映射(按 id 区分 src 路径:官方 /assets/stickers/ vs 用户 /api/stickers/img/)。
  2. 选择器面板分「官方」「我的」两区,「我的」区带上传按钮(file input)和每张的删除
  3. applyStickerTokens / _setStickerSrc 已按 id 查映射,映射表里多一份用户贴纸即可复用渲染逻辑。
  4. 上传 UI:选图 → 前端先拦大小(>2MB 拒)→ POST → 成功后插进面板。

后端小改(除新增 API)

  • describe_user_stickers() 要认 per-user manifest,否则用户发的表情进摘要器会被剥成空、记忆压缩丢信息(现在 man.get(id) 拿不到返回 "")。

安全清单(项目第一次收二进制,必须一次做对)

  • magic bytes 校验(PNG/JPG/GIF/WebP),不信任扩展名和 Content-Type
  • 单张 ≤ 2MB,每用户 ≤ 50 张(配额可配)
  • 文件名用 uuid 重新生成,丢弃原始文件名(路径穿越)
  • 图片 serve 时校验 filename{username}/stickers/ 目录内,防 ../
  • 上传/删除都要过 protected_paths 中间件拿到 request_username

分阶段落地

  • 第一步:上传 + 删除 + 列表 + 渲染(用户能发自己的图),后端 scrub 不动。
  • 第二步(可选):describe_user_stickers 认 per-user manifest、配额做成 runtime_config 可调。

待确认

  1. 图片鉴权用 query token(务实、两栖一致)还是更严的 blob 方案?
  2. 配额单张 2MB / 每用户 50 张是否合适?