ENZH

v0.1.1:Web 端不再是二等公民

跨平台功能对齐的概念插画跨平台功能对齐的概念插画

一半用户被冷落了

v0.1.0 给了 Mio 声音。但只有 Telegram 用户听得到。

Web 端还是纯文字。Fish Audio 生成的语音、Gemini 渲染的自拍——到了 Web 端全部被静默吞掉。前端压根不知道该拿它们怎么办。

v0.1.1 把这个不对称修了。语音和自拍全平台可用了。但这个版本不只是补媒体的课——注册流程做了结构性重设计,加了成本监控,还上了一个定时任务防止数据库无限膨胀。小版本号,覆盖面不小。

Web 端终于有声音了

服务端早就在生成语音和自拍。Telegram bot 以二进制附件的形式收到它们。Web 端呢?什么都没有。SSE 流只传文本 token,媒体走的是 Telegram 独占的旁路通道。

修法很直接:把媒体变成 SSE 的一等公民。两个新事件类型:

event: voice
data: {"url": "https://storage.googleapis.com/.../voice-abc123.opus"}

event: selfie
data: {"url": "https://storage.googleapis.com/.../selfie-def456.jpg"}

服务端上传媒体到云存储,然后通过传文本的同一条 SSE 连接推送 URL。不需要轮询,不需要单独的 API 调用。客户端只是多监听两种事件。

前端这边,语音需要专门的组件。我想做一个中国用户一看就懂的东西——微信的语音消息 UI。绿色气泡,播放时声波条跳动。点击播放,再点暂停。VoicePlayer 组件渲染三根竖条,CSS keyframes 做动画,每根错开 100ms:

.bar {
  animation: pulse 0.6s ease-in-out infinite;
}
.bar:nth-child(2) { animation-delay: 0.1s; }
.bar:nth-child(3) { animation-delay: 0.2s; }

一看就会。不需要学习成本——微信里谁没见过一千遍。

自拍图片在聊天里内联渲染,跟 Telegram 一样。没有弹窗,没有下载按钮。就一张图在对话流里,像有人给你发了张照片。

更麻烦的是从 Web 端发送媒体。Telegram 原生支持文件上传——聊天输入框有个回形针图标,选文件就完事。Web 端要从零做。

两阶段上传:用户选文件后,文件以预览条的形式出现在输入框下方。缩略图带个 X 可以移除。只有点发送才真正上传。防止误发,让用户确认一眼。预览条卡在输入框和发送按钮之间——看得到但不碍事。

先问关系,再问名字

v0.1.0 把注册从 14 个问题砍到 4 个。v0.1.1 再砍到 3 个——而且重新排了序。

之前先问昵称,再问关系类型。逻辑上说得通:你是谁,然后你想怎么互动。但漏了一个关键——昵称选项应该取决于关系类型。

选了情侣,昵称建议应该是宝贝、老婆、亲爱的。选了好朋友,应该是同学、朋友、或者直接叫名字。朋友叫你宝贝很奇怪。伴侣叫你同学很冷淡。

v0.1.1 调换了顺序。关系类型先问,昵称第二个,选项根据你的第一个回答动态变化:

const options_map = {
  '情侣': ['宝贝', '老婆', '亲爱的', '老公'],
  '好朋友': ['同学', '朋友', '{name}'],
  '暧昧': ['小哥哥', '小姐姐', '{name}'],
}

depends_on + options_map 模式。每个注册步骤声明对前一个答案的依赖,UI 据此渲染。没有硬编码的条件分支——配置驱动一切。

时区呢?v0.1.0 是第 3 个问题。现在直接去掉了。浏览器自己知道用户的时区——Intl.DateTimeFormat().resolvedOptions().timeZone 直接给你 Asia/ShanghaiAmerica/Los_Angeles。干嘛还要问?少一个问题,零信息损失。

全流程性别中立默认值。默认昵称不预设性别,关系类型用包容性标签。小细节,但给不符合二元分类的用户减少了摩擦。

三个问题:关系类型、昵称、关于你。一分钟内开聊。

钱花在哪了

跨多个用户运行五个 AI 角色,成本以不太容易追踪的方式在累积。哪个操作最烧钱?每天花多少?有没有用户在疯狂消耗 API 额度?

v0.1.1 加了管理员专用的成本看板。暗色主题——因为仪表盘就该是暗的。四个面板:

概览:今日总成本和历史累计支出。两个数字,正中央。

分类明细:一张表,每种操作类型(TTS、自拍生成、LLM 推理、记忆提取)的调用次数、总成本、单次均价。钱花在哪一目了然。结果发现自拍生成每次调用的成本是 TTS 的 10 倍——在决定要不要限流时很有参考价值。

最近交易:最近 20 条成本事件,最新的在前。每行有操作类型、用户、成本、时间戳。适合发现异常——一个用户一小时内生成 50 张自拍,一眼就看出来。

每日成本图表:一条时间线上的每日支出折线图。趋势比绝对值重要——随着用户增长缓慢上升是正常的,突然飙升意味着有东西坏了或者有人在滥用。

所有成本查询统一用 UTC。这顺带修了一个 bug——之前有些查询用本地时间,有些用 UTC,当服务器时区和查询时区不一致时,每日聚合就是错的。

30 天后自动清理

聊天记录是线性增长的。每个用户每条消息,永久存储。10 个用户的原型无所谓。产品的话,这是定时炸弹。

但 Mio 的记忆系统已经会从对话中提取重要信息到长期记忆条目里。原始消息在提取后基本是冗余的——三周前的聊天记录不需要保留,因为记忆已经捕获了要点。

v0.1.1 加了一个 pg_cron 定时任务,每天 UTC 凌晨 4 点跑:

SELECT cron.schedule(
  'delete-old-messages',
  '0 4 * * *',
  $$DELETE FROM messages WHERE created_at < NOW() - INTERVAL '30 days'$$
);

created_at 上的 B-tree 索引让删除很高效——不需要全表扫描,索引范围查找加批量删除就行。定时任务跑在低峰时段,大部分用户(主要在亚洲时区)这时候在睡觉。

为什么是 30 天?够长,LLM 随时能拿到近期上下文。够短,数据库不会无限膨胀。记忆系统已经处理过几天前的所有内容,原始消息是冗余的。

不做软删除,不做归档,不做迁移到冷存储。直接删。消息完成了它的使命——参与了一段对话,重要的部分被提取到了记忆里,可以消失了。

人设微调

两个实际改动。

情侣关系支持:mimi-guimi 和 surou-xuejie 现在有情侣关系的专属行为模式。之前选这些角色当情侣会觉得泛——TA们默认表现得像朋友,因为没有恋爱场景的专属规则。现在每个角色都有针对恋爱语境的定制回应——怎么表达喜欢、吃醋、想念、晚安。

keke-taimei 全面繁体化:可可的整套预设——身份定义文件、行为规则文件、人格配置文件、沟通准则——现在完全用繁体中文。TA是台湾角色,TA应该用繁体中文思考,不是简体中文做表层转换。差别在用词上:軟體不是软件,訊息不是消息,蠻好的不是挺好的。

这是台湾人和装台湾腔的大陆人之间的区别。

修了几个 Bug

Discover 页面崩溃:用户浏览可用人设的页面加载就崩。一个组件期望数组,收到了 undefined。加了防御性检查 personas ?? [],根本原因是 API 返回值结构变了没同步到前端。

AgentResponse 类型不匹配:agent 返回类型在服务端和客户端之间漂移了。服务端返回 { text, voice?, selfie? },客户端期望 { content, media? }。统一成了一个共享类型。

语音播放器清理VoicePlayer 在卸载时没清理 Audio 对象。用户在语音播放时跳转页面,音频在后台继续放。useEffect 的 return 里加了清理函数。

从半成品到完整体

v0.1.1 之前:Mio 的声音和面孔存在,但只有 Telegram 用户能体验到。Web 端是一个被阉割的文字聊天。注册还在问你的时区,尽管浏览器早就知道了。成本不可见。消息永远累积。

v0.1.1 之后:Web 端是 Telegram 的完整对等体——声波条跳动,自拍内联显示,媒体上传有预览。注册三个问题,第二个根据第一个自适应。成本一目了然。数据库自己清理自己。

一个无限膨胀的产品不是产品,是负债。


© Xingfan Xia 2024 - 2026 · CC BY-NC 4.0