用户自定义表情包(上传图片版)
USER STICKERS
状态:🟡 规划 · 最后核对 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_paths 是 startswith 匹配(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 为主)
- 启动时多 fetch 一份 per-user manifest(
/api/stickers/list),和官方_stickerMap合并成渲染映射(按 id 区分 src 路径:官方/assets/stickers/vs 用户/api/stickers/img/)。 - 选择器面板分「官方」「我的」两区,「我的」区带上传按钮(file input)和每张的删除。
applyStickerTokens/_setStickerSrc已按 id 查映射,映射表里多一份用户贴纸即可复用渲染逻辑。- 上传 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 可调。
待确认
- 图片鉴权用 query token(务实、两栖一致)还是更严的 blob 方案?
- 配额单张 2MB / 每用户 50 张是否合适?