Commit Graph
95 Commits
Author SHA1 Message Date
Your NameandClaude Sonnet 5 ddf23d40b2 修复新会话首次打开时消息历史被静默截断为1条、对方已读回执永不刷新
根因:tinode-web.jsx的prefetchLatestMessagePreviews()(会话列表"最后一条消息"预览)
会在会话列表一渲染就对每个有动态但本地无缓存摘要的话题调用
topic.subscribe(startMetaQuery().withLaterData(1)...),只为预览目的抓取"最新1条"
消息——这常常发生在用户真正点开这个会话之前。这次订阅会把tinode-sdk的
Topic._attached置为true,且把_maxSeq设成那一条消息的seq。

用户随后点开会话时,messages-view.jsx自己的subscribe()发现topic.isSubscribed()
已经是true就直接return,从未真正发出过完整的{sub}请求。即使去掉这个短路,
SDK的Topic.subscribe()本身在_attached为true时也会无条件短路成
Promise.resolve(this),完全不看传入的getParams——所以无论如何都不会把
withLaterSub()/withAux()等真正需要的查询发给服务端。

现场复现:全新注册的两个互为好友账号之间发4条真实消息,全新session下第一次点开
该会话,只渲染出最新1条,其余3条静默丢失;对方的已读回执(应变蓝色双勾)永远
不刷新——这两个现象都会在离开会话再重新打开后自愈(因为离开会清掉_attached),
但用户在此之前看到的是一个残缺、状态错误的会话。

修复分两处:
1. subscribe()内,若topic已经isSubscribed(),先topic.leave(false)(只清客户端和
   服务端的订阅态,不清本地已缓存消息),再执行真正的subscribe(),确保这次一定
   是货真价实的、服务端会认真处理getParams的订阅请求。
2. 把消息分页查询从withLaterData(MESSAGES_PAGE)("给我比本地_maxSeq更新的消息")
   改成withData(undefined, undefined, MESSAGES_PAGE)("给我最新的N条消息",不依赖
   本地缓存游标)。前者在_maxSeq被上述预览订阅污染后会查询"since:被污染的seq+1",
   服务端自然返回空;后者是绝对查询,对全新话题(_maxSeq为0,两者退化成同一个
   since:undefined查询)行为不变,对被污染的话题能正确要回完整历史。

第14+轮最小粒度测试 Ch3-4 发现(P2P单聊消息状态机场景)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:46:58 +08:00
Your Name 8087fd7e97 补上视频通话小窗拖动功能遗漏的CSS改动(.call-party.self 的 grab 光标提示)
上一个 commit(call-panel.jsx 里新增的"全屏连线中拖动本地小窗"功能,对应 8.9 双方
视频通话中.png 的"小窗应支持拖动"需求)只提交了 JSX/JS 那部分,配套的 CSS
(.call-party.self 增加 cursor:grab 提示可拖动,[disabled] 态改回 cursor:unset)
当时被同一文件里其他并发改动的暂存操作顶掉,没有一起进去——这个环境里多个并发
session 同时在改 css/base.css,共享 git index 导致 git add 经常把彼此的暂存内容
一起扫进对方的 commit,这是本轮测试期间反复确认到的现象,不是这次改动本身的问题。
JS 逻辑不受影响(拖动本身早就用真实指针事件验证过可用),这里只是补上缺的光标提示
样式,让用户在尝试拖动前就能看出这个小窗是可拖的。
2026-09-06 13:40:03 +08:00
Your NameandClaude Sonnet 5 e6e51f625e 群系统事件消息改为居中纯文本渲染,不再走普通气泡+作者名的展示逻辑
根因:群里的系统事件消息(成员加入、角色变更、公告发布等,x-im-type=system_action/
system_notice)服务端并不带真实的 `from`,但 messages-view.jsx 一直把它们和普通消息
一样交给 ChatMessage 渲染——chat-message.jsx 的 fullDisplay 逻辑会在气泡下方额外画一行
发送者名字,解析不到发送者时回退显示"未找到"。对照参考图 内容沟通对话3-开启进群验证与
拉人状态.png,这类系统事件应该是居中的纯灰字提示,不带头像、不带气泡、不带作者名——现状
既不符合参考图,也让"未找到"这种内部占位文案暴露给了用户。

怎么修:meta-message.jsx 新增 systemText 分支,复用日期分隔条同一套"居中提示"视觉语言
(.meta .bubble.system,无背景、纯居中灰字);messages-view.jsx 在消息渲染分支里识别
system_action/system_notice 且不带可操作按钮(group_invitation_created 那种带
"立即加入"按钮的除外,仍走完整气泡展示)的消息,改为渲染 MetaMessage 而不是 ChatMessage。

验证:用真实群(3 个真实账号)走了两遍"扫码申请入群→通过审批",会话里"有成员通过审批
加入了群聊。"正确显示为居中灰字,不再出现气泡、头像和"未找到"作者名;带按钮的
group_invitation_created 消息不受影响,仍是完整气泡+"立即加入"按钮。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:39:35 +08:00
Your Name e53af93409 修复收藏页多选模式下类型状态丢失、跨tab选择静默累加两个问题
根因1:'favorites-select' 是全部类型 tab 共用的同一个路由,本身不带类型信息,Favorites
组件原来进入多选模式时把 type 硬编码回退成 'file',不管进入前实际停在哪个 tab。实测在
"文本"tab 点"多选",列表和头部计数会静默换成"文件"tab 的数据,用户看不出发生了什么,
勾选/删除会作用在自己根本没看到的文件项上——测试过程中因此真的误删了一条文件收藏。
修复:进入多选前把当前 type 作为 hash 参数带上,多选页/分享页的返回箭头、"取消"、发送后
跳转都改用该参数换算的 tab,不再硬编码 'favorites-files'。

根因2:切换类型 tab 只更新 type,没有清空 selected——在"文件"tab 选中项目后切到"图片"tab,
头部仍显示旧的"已选择 N 项",实际操作的是已经不在当前列表里的旧类型收藏,删除/转发没有任何
可见提示。修复:切 tab 时一并清空 selected。

验证:真实浏览器逐步复现两个问题后确认修复生效——在"文本"tab进入多选保留文本列表、
切"图片"tab选择清零、"取消"正确回到多选前所在的图片tab;多选删除功能本身回归正常。
2026-09-06 13:38:25 +08:00
Your NameandClaude Sonnet 5 1228d6a534 补上组织架构部门蓝色高亮修复遗漏的CSS改动(.expanded重命名为.me)
上一个commit(1d47c586)只提交了JSX那部分(DeptRow/DeptNode改用containsMe),
对应的CSS改名(.organization-department.expanded → .me)当时被同一文件里
其他并发改动的暂存操作顶掉了,没有一起进去,导致上一commit单看JSX是完整的,
但CSS选择器名字还没改,实际不生效。这里补上,两个commit合起来才是完整修复。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:36:27 +08:00
Your NameandClaude Sonnet 5 1d47c586d3 修复组织架构页部门蓝色高亮跟"是否展开"挂钩,根节点被误判成"我的部门"
DeptRow原来的蓝色高亮(.organization-department.expanded)直接复用"展开/收起"
这个纯UI交互状态:depth 0的根节点默认就是展开的(DeptNode: useState(depth===0)),
导致根节点从打开页面那一刻起就被误染成"当前用户所在部门"的蓝色,与参考图
(docs/images/部门组织框架.png)不符——图里根节点"总裁办50人(一级)"明明是展开状态,
颜色却是黑色默认色,只有真正包含当前用户的"技术4人(二级)"是蓝色实心菱形。

根因是把"展开状态"和"是否是我的部门"这两个独立概念用同一个CSS类混在了一起。改为
新增containsMe(DeptNode根据自己已加载的members列表里有没有当前用户算出,与expanded
彻底解耦,expanded继续只管chevron方向),CSS类从.expanded改名成.me(与PersonRow已有的
.me保持同一套命名)。顺手把三处重复的"裸id/usr前缀id两种形式对比myUserId"的内联判断
提成共享的isMyUserId函数。

当前测试租户组织架构数据本身为空(GET /organization/departments、/members 都是真实
200+空数组,非账号权限问题——kcg051307交叉验证过同样为空),无法用真实多级树肉眼过一遍,
按测试要求改用桩数据验证:mock了总裁办→财务/销售/技术三个部门+技术下4个成员(含当前
用户)复现参考图的结构,展开技术后确认根节点保持黑色、技术正确变蓝,与参考截图逐像素
对照一致;修复前用同一份桩数据可复现根节点被误染蓝的问题。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:35:38 +08:00
Your NameandClaude Sonnet 5 a964d51ba5 修复回复/编辑消息触发风控弹窗时,"正在回复"预览条在弹窗结果出来前就被清空
现象:对一条历史消息点"回复",输入含链接的文本点发送,风控弹窗(安全提醒/警示级)
刚弹出的同时,composer 里"正在回复 XX"的预览条就已经消失了;如果这时候点"取消发送",
这条回复就被永久放弃——原来引用的消息内容和回复关系不再存在,也没有任何提示,唯一的
恢复办法是回到那条历史消息重新点一次"回复"。编辑消息(修改一条已发消息的内容后再次
包含链接)是同一根因的同款问题,只是编辑可以靠重新点"编辑"找回原文,没有回复那么彻底
不可逆。

根因:sendMessage() 原来在根据 this.state.reply / this.props.forwardMessage 组装好
msg/head 之后,同步、无条件地立刻调用了 this.handleCancelReply()(清空 reply/编辑
状态),这一步发生在下面的 URL 风控检查(POST /security/urls/check)之前——但风控
弹窗要等用户点"继续发送"或"取消发送"才会真正决定要不要走 doSend(),这中间的等待期里
UI 状态已经被提前清空了。

修法:把 handleCancelReply() 从三处提前调用点移到 doSend() 内部、真正调用
this.props.sendMessage(...) 之前。这样它只在真的要发送时才执行——不含链接的普通消息
里 doSend() 本来就是同步立即执行,时机与原来完全一样;命中风控的消息里,"继续发送"
时才会触发(此时清空回复状态是正确的,消息确实发出去了),"取消发送"时 doSend()
根本不会被调用,回复/编辑的预览条和引用内容都会完整保留,可以直接修改文本重试或
自己手动取消。转发场景不受影响:转发消息不会触发这条风控检查(originalText 恒为
null),移动调用时机对它是纯粹的时序等价替换。

验证:真实浏览器操作,对一条消息点"回复",输入含 https://example.com/a?token=abc123
的文本发送——警示弹窗弹出的同时预览条仍然可见;点"取消发送"后弹窗消失、预览条依旧
完整保留、composer 文本不变;再次点发送、这次点"我已知晓,继续发送",消息正确以
带引用卡片的回复形式发出,预览条在发送成功后才清空。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:32:59 +08:00
Your NameandClaude Sonnet 5 cdca509183 修复群邀请消息卡片丢弃"立即加入"按钮,被邀请人无法接受邀请
根因:POST /groups/{topic}/invitations 邀请成功后,后端会往邀请人与被邀请人的
单聊会话里发一条 x-im-type=system_action、template=group_invitation_created 的
消息,content.actions 里带着真实可用的 {type:"group_invitation_accept",
label:"立即加入", target_id:<invitation_id>}(对照参考图 6.3 加入群聊.png)。但
chat-message.jsx 原来对所有 system_action/system_notice 消息一律只取
content.text 渲染成纯文本,这个 action 数据被整体丢弃,消息只显示"邀请你加入群聊。"
没有任何按钮——im-api.js 里封装好的 acceptGroupInvitation() 因此从来没有调用点。

怎么修:chat-message.jsx 在原有的文本兜底之前,单独识别 content.actions 里
type=="group_invitation_accept" 的这一种模板,渲染一张带按钮的小卡片(其余
system_action/system_notice 模板的纯文本渲染保持不变,不受影响);
messages-view.jsx 新增 handleAcceptGroupInvitation,调用 acceptGroupInvitation()
并用已有的 showToast 反馈结果,逐层传给 ChatMessage 的新 onAcceptGroupInvitation
prop。按钮点击后按 busy/joined/pending_approval/error 四态展示,避免重复提交。

验证:用两个真实账号(A 建群,直接对 B 发起 POST /invitations)复现,重新构建后
B 的会话里正确出现"邀请你加入群聊。"+"立即加入"按钮;点击后正确调用
POST /group-invitations/{id}/accept 并把返回的失败原因("服务器繁忙,请稍后重试")
展示出来——这一步暴露了一个独立的后端 500 bug(accept 接口本身的实现有问题,
已记录在测试报告里,不在本次前端修复范围内),前端的请求发起、按钮状态机和错误展示
均已确认按预期工作。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:26:14 +08:00
Your NameandClaude Sonnet 5 a6014b84ee 修复桌面端拖动悬浮通话窗口后收窄浏览器至移动端断点,通话条卡在旧桌面坐标不跟随移动端布局
现象:视频通话最小化后处于桌面宽度(>640px)时,拖动悬浮小窗到任意位置(dragStart 会把
定位方式从 CSS 的 right/auto-left 切换成显式 left/top 内联样式,方便拖拽期间自由移动),
之后把浏览器窗口收窄到 640px 以下(真实场景:桌面用户把窗口拖窄到半屏和另一个应用并排),
悬浮条不会像预期那样贴顶变成通栏胶囊(base.css 里
#video-container.minimized:not(.audio-minimized) 的移动端规则:left:0/right:auto/
top:var(--caption-panel-height)/width:100%/height:11rem),而是继续停在拖拽结束时的旧
桌面坐标——因为内联 style 的优先级恒高于任何样式表选择器,不管后者的媒体查询是否匹配,
移动端规则里的 left/right/top 全部被内联值盖住,只有从未被内联样式接触过的 width/height
还能生效,导致宽/高不成比例、位置错位。

修复:CallPanel 新增 handleWindowResize,监听 window resize;每次触发时如果
matchMedia('(width<=640px)') 命中且当前处于 minimized,就清掉 containerRef 上手动写入的
left/right/top 内联样式,把定位权交还给样式表,让移动端规则完整生效。componentDidMount/
componentWillUnmount 里和已有的 pointerdown/move/up 监听一起注册/清理。从未拖拽过时这个
清理是空操作,不影响现有行为。

验证:真实 CallPanel 实例(React Fiber 直接获取),伪造 canvas/AudioContext 媒体流跑通整
个通话中状态,用 computer 工具做真实指针拖拽把悬浮窗从默认右上角拖到别处(确认
style.left/top 已写入、right:auto),再用 resize_window 把视口收窄到 375px 宽并确认应用
自身的 resize 监听正确触发(displayMobile 状态同步更新)——收窄后 style 只剩
"cursor: grab",left/right/top 均被清空,getComputedStyle 显示 left:0/top:53.33px(=
var(--caption-panel-height)按当前根字号换算)/width:375px(100%)/height:146.66px(11rem),
与移动端样式表规则完全一致;再放宽回桌面宽度确认悬浮窗正常回到默认右上角18rem方框位置,
未引入回归。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:22:06 +08:00
Your NameandClaude Sonnet 5 0748f30cc1 实名认证提交成功后补拉一次 GET,用服务端脱敏字段替换本地草稿回显(新增61)
现象(IM-后端问题清单-2026-09-04.md"新增61"):POST /security/realname/submissions
成功响应只有 {status, submission_id},从不带 masked_name/masked_id_number,前端此前
的兜底是回显用户本次会话里刚输入的原始姓名/证件号草稿——多数情况下看起来没问题,
但那是未脱敏的原始值,不是服务端认可的脱敏展示值,且只覆盖"这次会话内",换一个
从未在本会话提交过的场景(比如同一账号换设备/清缓存后第一次看提交结果)就没有任何
草稿可回退,只能空白。

之前的复核已经确认 GET /security/realname 从这个功能最初上线的 commit 起就一直带着
masked_name/masked_id_number 这两个字段,只是提交成功后从未再调用过它。

修法:submit() 里 POST 成功后追加一次 getRealnameStatus(),用响应里的
masked_name/masked_id_number 覆盖本地状态,展示服务端脱敏后的真实值;这次追加请求
失败时静默忽略(提交本身已经成功,不应该因为这次锦上添花的请求失败而回退),保留
原有的本会话草稿兜底作为这次请求解析完成前的过渡显示。

验证:注册全新测试账号(无历史提交),提交姓名"测试用户"+证件号
110105199003077856 后,抓包确认 POST 响应确实只有 {status,submission_id},紧接着
自动触发的 GET 响应带 masked_name:"测*"、masked_id_number:"110***********7856";
页面输入框读取到的实际 value 是这两个脱敏值,不是刚输入的原始文本,证实修复生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:21:05 +08:00
Your NameandClaude Sonnet 5 ded440078a 补上用户名注册路径缺失的"无法自助找回密码"提醒文案
需求文档 1.4 第5条硬性要求:用户名方式注册不发送短信,必须提示用户"未绑定手机号
无法自助找回密码,忘记密码后只能联系企业管理员重置"。这句话在注册表单里从未真正
写出来过——不是视觉上被样式盖住,是 JSX 里压根没有对应节点,选择用户名注册的人
完全不会被告知自己放弃了手机号找回密码的能力。

修法:在邀请码输入框下方补上这行提示,只在 usingPhone 为假(用户名注册路径)时
展示,不影响手机号注册路径(那条路径本来就能自助找回密码)。参考图"注册页面.png"
是两个字段都还没交互的初始态,不代表这条路径相关的提示不应该出现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:20:29 +08:00
Your NameandClaude Sonnet 5 46a98782fd 修复"退出当前登录"仍用原生 window.confirm(),与其余三个确认弹层不一致
现象:清除缓存/清除聊天记录/注销账号三处早前已经统一改成组件内 ConfirmSheet 弹层
(本文件其它三处组件自己的注释也在讲这次统一),唯独"退出当前登录"还留着最原始的
window.confirm()。本轮测试发现这在当前浏览器自动化环境下会被判定为"环境不支持同步
弹窗"而直接返回 false(控制台可见"Page dialog suppressed"警告),退出登录点了完全
没反应,且没有任何错误提示——这不只是测试环境的偶然状况:不少内嵌 WebView/嵌入式
容器场景本身就会禁用 confirm()/alert() 这类阻塞式原生对话框,真实用户在这些环境下
点"退出登录"会遇到一模一样的静默无效。

修法:新增 LogoutConfirm 组件,复用同一个 ConfirmSheet(图标 logout,标题"退出登录"),
"退出当前登录"按钮改成 setSheet('logout') 打开这个弹层,与 CacheClear/
ClearHistoryConfirm/DeleteAccountConfirm 完全同构,不再依赖浏览器原生对话框。

验证:npm run build:dev 后在浏览器实测,点击"退出当前登录"正确弹出本地弹层(标题/
文案/确认取消按钮与设计一致,logout 图标正常渲染为图形而非文字),点"确认"后真实
退出登录、回到企业码入口页;点"取消"正确关闭弹层且不登出。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:19:45 +08:00
Your NameandClaude Sonnet 5 eaafabe94f 修复用户名+邀请码注册(无手机号)必现崩溃:Tinode.credential(null) 抛异常
现象:企业码注册流程走"仅用户名+邀请码,不填手机号"这条路径时,点"注册"必然抛出
原始 JS 报错"Cannot destructure property 'val' of 'meth' as it is null",直接
展示在错误提示条上,注册无法完成。

根因:auth-onboarding-view.jsx 的 Register 组件在没有手机号凭证、只有邀请码时,
把 cred_ 显式传成 null(没有可附加的凭证)。tinode-sdk 的 Tinode.credential(meth,...)
用 `typeof meth == 'object'` 判断参数是不是一个 {meth,val,resp,params} 对象,但
JS 里 `typeof null === 'object'` 也是 true,导致对 null 无条件解构,直接抛异常。
所有走"登录"分支的调用点传的从来都是"字段可能是 undefined 的对象",从未传过字面量
null,所以这个 bug 只在"用户名注册且没有手机号凭证"这一条路径上必现。

修法:cred_ 为空时不再调用 Tinode.credential(),直接把 cred 字段设为 undefined,
绕开 SDK 这个不处理 null 输入的缺陷,不改动 vendored 的 tinode-sdk 代码。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:19:26 +08:00
Your NameandClaude Sonnet 5 fa2c6c2f07 修复会议详情页响应会议后toast文案"已已接受"/"已已拒绝"重复"已"字
MeetingDetail.respond()提交回应成功后用 `已${RESPONSE_LABEL[response]}` 拼toast文案,
但RESPONSE_LABEL的accepted/declined两项本身已经是"已接受"/"已拒绝"(给参与人列表
徽标用的),再拼一次"已"前缀就变成"已已接受"/"已已拒绝";tentative没有"已"前缀,
拼出来是"已待定",语义也不通顺。同页面另一个响应入口MeetingCard.respond()
早就手写了三条正确文案(已接受会议邀请/已拒绝会议邀请/已标记为待定),
两处本应一致却各写一套。

改为复用MeetingCard已有的正确文案,不再从RESPONSE_LABEL派生。

已用kcg051307发起、asn494180作为参会人的3场真实会议在浏览器逐一实测三种回应,
确认toast分别正确显示"已接受会议邀请"/"已拒绝会议邀请"/"已标记为待定"。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:18:19 +08:00
Your NameandClaude Sonnet 5 db75aaa25b 修复会议详情页"参会确认"统计遗漏"待定"响应,三项数字之和对不上参会人数
MeetingDetail 的 counts 对象本就统计了 tentative({accepted,pending,declined,tentative}),
但渲染只画了已接受/待响应/已拒绝三项,待定响应被计算出来后从未显示。真实复现:
组织者建会议、参会人回应"待定"后查看会议详情,"参会确认"区域三个数字之和小于
participant_count,且没有任何说明缺的那一份去了哪。

补充第4个统计项(待定),保持原有三项顺序不变;base.css 的
.meeting-status > div 网格列数从3改4(.report-statistics 复用同一条规则,
拆开单独设置避免影响日报/月报团队统计的应提交/已提交/未提交三列布局)。

已用 asn494180(组织者)/kcg051307(参会人) 两个真实账号在浏览器实测:kcg回应"待定"后
version从1变为2,asn的会议详情页刷新后正确显示"1 已接受/0 待响应/0 已拒绝/1 待定",
截图确认4列布局无溢出/挤压。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:14:10 +08:00
Your Name 693265658f fix(导航): 关闭聊天/信息面板改为真实返回,不再新增历史记录
用户反馈:从会话列表打开聊天A,点页面自己的"返回"关掉聊天,再打开聊天B,这时候点
浏览器的返回键,会先回到列表,再点一次返回又跳回了聊天A——因为"关闭"这个动作和"打开"
一样都在往浏览器历史里push新记录,而不是弹出(pop)"打开"时push的那条,导致聊天A那条
记录一直遗留在历史栈里,多按一次返回就会被重新翻出来。

两处受影响、同一根因:
- tinode-web.jsx 的 handleHideMessagesView(移动端聊天页左上角"←",messages-view.jsx
  的#hide-message-view)——打开聊天(handleTopicSelected)是真实push,关闭时原来无条件
  再push一条"没有话题"的状态,现在改成先尝试HashNavigation.goBack(),找不到可回退的
  历史才退回原来的push方式。
- info-view.jsx 的 handleBackNavigate(旧版InfoView自己的返回/关闭按钮,目前只有"已
  保存的消息"这一个话题还在用它,好友/群的资料页已经在更早的章节改用真正修过返回逻辑
  的friend-settings/group-settings)——同样的问题,同样的修法。

已在浏览器验证:连续"开聊天A→点返回关闭→开聊天B"后,浏览器返回键能一次性回到聊天A
从未存在过一样,不会再被重新翻出来;InfoView的关闭按钮点击后hash是异步变化的(真实
history.back()的特征),不再是原来同步的push。
2026-09-06 12:29:56 +08:00
Your Name 4fc65b0f40 fix(日报): "提醒未提交成员"改用组织架构接口凑候选名单,不再必现400
后端问题清单"新增2"记录过:POST /reports/daily/reminders 要求调用方传入完整候选
user_ids 名单(服务端只做"从候选里筛出未交的人再提醒",不会自己算全公司名单),
但前端一直传空数组,必然命中服务端"user_ids为空"的400校验。

重新核实时发现这条其实不是后端缺口——GET /organization/members 不传 q/department_id
时就会返回调用者可见范围内的全部在职成员(分页),这个接口本来就在会议参会人选择器
里用着,只是"提醒未提交成员"这个按钮没有复用它。改为点击时先分页拉取全部组织成员
user_id,再传给提醒接口;组织架构为空时给出明确提示而不是让请求再次落入400。

已在浏览器验证请求格式(limit=100,与后端实际接受的参数一致)和空组织架构下的
兜底提示;当前测试租户组织架构数据本身为空,未能覆盖"真的有人被提醒"这条路径,
但接口契约已对照 im_be 源码逐字段核实。
2026-09-06 12:07:24 +08:00
Your Name 623f7dc40f fix(导航): 排查全项目后修复其余同类tab切换污染历史记录问题
上一提交(e61eb402)只修了"我的收藏"/"查找聊天记录"两处,用户要求排查其他页面是否
也有同样问题。全项目排查<nav>/tabbar/active态切换元素,逐一确认onClick是否直接调用
HashNavigation.navigateTo()(每次都push一条历史记录),找到5处同类实例并修复:

- message-center-view.jsx / workplace-collaboration-view.jsx:底部导航"工作台"<->"消息"
  两个tab——这是全应用唯一的根级tab bar,也是点击频率最高的一处,之前同样每次切换都
  push一条历史记录。
- workplace-collaboration-view.jsx:日报/月报列表页的"我的xx"<->"团队统计"切换(同一个
  组件、同一个页面,只是team boolean不同),同样的问题。
- new-topic-view.jsx:"开始新会话"页的搜索/新群组/通过id三个tab。
- wallpapers.jsx:"壁纸"设置页的图案/图片两个tab。

均改用上一提交新增的HashNavigation.navigateReplace()。已在浏览器逐一实测:连续切换
多次tab后,无论真实浏览器返回还是应用自己的返回按钮,都能一次性返回到进入该页面之前
的真实上一页。

以下经排查确认不受影响,未改动:group-management-view.jsx的群聊分类tab(我创建的/
管理的/加入的)和contact-management-view.jsx的字母索引锚点均只用本地state/scrollIntoView,
不涉及hash跳转;auth-onboarding-view.jsx的登录<->注册切换、new-topic-view.jsx提交后的
三处跳转是真实的"离开当前页"操作,保留push语义符合预期。
2026-09-06 11:16:01 +08:00
Your Name e61eb40250 fix(收藏/聊天记录检索): 切换页内tab不再污染浏览器历史记录
用户反馈"我的收藏"页面点击tabs切换(全部/图片/视频/文件/语音/文本/链接)会记录到浏览器
历史记录里,导致点击返回一直在这个页面内切换tab,而不是真正返回上一页。

根因:content-library-view.jsx的Favorites/HistoryNav每次切tab都调用navigate(),
即HashNavigation.navigateTo() -> location.hash = ...,每次赋值都会被浏览器记为一条
新的历史记录。切N次tab就push N条记录,无论是系统返回键还是应用自己的"返回"按钮
(HashNavigation.goBack()),都要先把这N条tab切换记录走完才能真正离开页面。

修复:新增HashNavigation.navigateReplace(),用history.replaceState替换当前历史记录
而不是新增一条(hash该更新还是更新,刷新/复制链接不受影响),并手动派发hashchange事件
供tinode-web.jsx现有的监听器捕获——replaceState本身不会自动触发该事件。Favorites的
TYPE_LABELS tabs和HistoryNav("查找聊天记录"页的聊天记录/图片/视频/文件/链接tabs)
两处tab切换都改用这个新方法,不再计入navigationCount。

已在浏览器实测两处:连续切3次tab后,无论是原生history.back()还是应用返回按钮,都能
一次性返回到进入这个页面之前的真实上一页,不再卡在tab切换历史里。
2026-09-06 10:45:24 +08:00
Your Name 01325ad009 fix(css): 补齐按钮内标题/内容间距异常的遗漏实例
用户反馈"我的收藏-文件"tab标题(strong)与内容(small)间距过大,根因是全局
`button{line-height:2.2rem}`重置(面向单行居中按钮文案)继承进了改造成
卡片/多行行容器的<button>内部未显式覆盖line-height的<strong>/<small>。

本次除修复用户直接指出的.library-file-card/.library-record-header外,
系统排查全项目同类模式(button内嵌套strong+small),额外修复3处此前遗漏:
- .library-message-results(查找聊天记录-聊天记录tab搜索结果)
- .personal-identity("我"页头像+昵称+企业名身份卡片)
- .report-cards > button(月报列表行,日报列表用article非button故不受影响)

均已在浏览器实测line-height从2.2rem降至1.3,视觉间距恢复正常。
2026-09-06 10:40:40 +08:00
Your NameandClaude Sonnet 5 150db2fc02 重新构建dev产物(消息首页会话预览修复)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:16:55 +08:00
Your NameandClaude Sonnet 5 2ee8bac6a0 修复消息首页会话预览恒为"暂无消息",即使会话有真实历史消息
message-center-view.jsx 的预览文字读 topic.latestMessage(),这个方法
只会从话题自己的本地消息缓存里取值——而这个缓存只有在话题真正被
subscribe 过一次数据拉取后才会填充。me.contacts()(resetContactList)
给出的是每个联系人真实的、已缓存的Topic实例,包括用户这次会话里
从未打开过的——这些话题的latestMessage()恒为undefined,即使同一个
对象自己的touched/seq字段能证明服务端真实存在历史消息。

新增prefetchLatestMessagePreviews():对chatList里seq>0但本地缓存为空
的话题,一次性拉取最新1条消息填充预览。用一个Set跟踪已尝试过的话题,
避免resetContactList()(几乎每个{meta}/{pres}事件都会触发一次)反复
重新拉取同一个话题。

调试过程中发现并修正了自己犯的两个错误:(1) resetContactList()在
websocket握手完成前就会被本地持久化缓存恢复触发一次,此时的subscribe
必然503失败,加了isConnected()判断跳过,避免过早失败把话题永久标记
为"已尝试";(2) Topic.subscribe(getParams, setParams)的参数顺序搞反,
把查询对象传成了第二个参数(setParams),导致subscribe成功但发出的
{sub}包完全没有get子句,从未真正请求过消息数据。

已用真实浏览器验证:好友kcg051307会话(真实71条历史消息)刷新页面
后预览从"暂无消息"变为真实的最新消息内容,未读角标数字不受影响
(确认没有误将未读消息标记为已读)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:16:48 +08:00
Your NameandClaude Sonnet 5 ca71667975 重新构建dev产物(收藏文本折叠/搜索高亮、跳转定位消息两处修复)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:31:00 +08:00
Your NameandClaude Sonnet 5 68044fa1bb 修复"查找聊天记录"跳转定位到原消息在全部入口100%失效
componentDidUpdate里的jumpToSeq检查只在这个值发生变化时触发,但
tinode-web.jsx从不会同时渲染MessagesView和第7章侧边栏(!chapterSevenActive
门禁),所以从History的搜索结果/图片宫格/视频宫格跳转,每次都是卸载
第7章视图、挂载一个"首次渲染就已经带着jumpToSeq"的全新MessagesView
实例——componentDidUpdate根本不会跑(它不处理首次挂载)。7.12/7.15/
7.16全部"定位到消息来源"入口100%失效:jumpseq参数永久留在URL里,
会话固定停在最底部。

修复过程中还发现一个真实的竞态:首次挂载意味着componentSetup()刚刚
才发起topic订阅(异步、fire-and-forget),jumpToMessage自己的
topic.getMeta()调用要求订阅已建立,立即调用会抢在订阅完成前——实测
这个抢跑的getMeta()调用真实收到了服务端 {"code":403,"text":"permission
denied"} 拒绝(不是本地兜底,是真实的服务端往返),而真正的订阅请求
当时还在路上。改为轮询已有的isSubscribed()标志(而不是猜一个固定延迟
或改造subscribe()暴露其内部promise),封顶5秒,真正卡住的订阅仍会
落到jumpToMessage自己已有的错误处理上。

第十二轮最小粒度测试 Ch7 发现,已用真实浏览器验证:从工作台等其它
页面跳转过来时,消息正确加载、jumpseq参数正确从URL清除。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:30:53 +08:00
Your NameandClaude Sonnet 5 6882103461 实现收藏文本长文本折叠/展开/复制、收藏搜索关键词高亮
需求7.4两处此前完全没接:文本收藏是不限高度的纯<p>,长文本不折叠、
没有展开/收起,也没有复制按钮;收藏搜索的query从未传到文本收藏这一
层,History那边已有的Highlight组件在Favorites里从未被引用(实测:
搜索一个真实命中的关键词,结果列表0个<mark>)。

新增FavoriteText组件:用scrollHeight/clientHeight判断是否真的溢出
(避免给短文本也显示无用的"展开"按钮),复制走和context-menu.jsx
的copyMessageText一致的navigator.clipboard.writeText。

-webkit-line-clamp在当前环境对这个<p>不生效(getComputedStyle能读到
设置值但视觉不裁切),改用line-height反推的max-height+overflow:hidden;
外层容器不能用display:flex,会被UA blockify成flow-root导致同样失效。

第十二轮最小粒度测试 Ch7 发现,已用真实浏览器验证展开/收起切换正常、
搜索关键词高亮正确命中。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:30:40 +08:00
Your NameandClaude Sonnet 5 cccd21aff1 修复通讯录字母索引条位置错误,与好友列表重叠
.people-index 用了固定 top:13rem,是"新朋友/群组/文件助手/系统消息"
四行快捷入口成为现在这个样式之前遗留的旧值——实测索引条渲染在约173px
处,而按字母分组的好友列表实际从约375px才开始,索引条整整偏高200px,
盖在快捷入口行上而不是贴着好友列表。按真实布局(header+搜索框+四行
入口+分组标题外边距)重新算出27.5rem。

第十二轮最小粒度测试 Ch5-6 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:13:12 +08:00
Your NameandClaude Sonnet 5 5b836d50b2 修复通话发起后立即退出登录导致应用整体白屏崩溃
handleLogout()先断开WS连接、再重置界面状态,界面重置卸载
MessagesView时其componentWillUnmount会尝试发一条挂断信令,vendored
SDK的Connection.sendText()在已关闭的socket上同步抛错,componentWillUnmount
里没有error boundary兜底,导致整个应用被React卸载成白屏。

5处几乎相同的通话信令发送方法(ringing/hang-up/offer/ice-candidate/
answer)合并成一个sendCallSignal()辅助方法,发送前检查连接状态、
发送本身包一层try/catch。

第十二轮最小粒度测试 Ch8 发现(点击通话按钮后立即退出登录复现)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:09:23 +08:00
Your NameandClaude Sonnet 5 4a140e6c79 修复消息首页空状态误导文案、未登录完成时静默显示空列表
会话列表为空时统一显示"未找到相关会话"+搜索图标,即使用户根本没有
输入过搜索词——账号本身还没有任何会话时,也被说成"你的搜索没找到东
西"。现在区分:真正搜索无果才显示这句,否则显示"暂无会话"。

另外,myUserId尚未就绪(自动登录未完成/失败)但sidePanelSelected已
经是messages-home(比如收藏的旧链接、恢复的历史标签页)时,原来的
offline判断条件恒为false,页面会静默渲染一个和"账号确实是空的"没法
区分的空列表,用户以为账号没数据而不是还没登录成功。现在myUserId为
空时也按"未连接"处理,诚实展示"加载中"。

第十二轮最小粒度测试 Ch1-2 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:59:35 +08:00
Your NameandClaude Sonnet 5 8575c83980 修复举报原因列表文案裁切、举报详情页原因标签换行错位
两处grid都按"预留图标位"设计成3栏(2.7-2.8rem首列+内容+尾部),但
ReportReasons/ReportDetails实际渲染的都只是纯文字span,没有图标元素
——真实文案被挤进过窄的图标列:举报原因列表里"诈骗欺诈"等文案裁切成
看不懂的碎片(部分文字还叠在蓝底图标框上导致白底白字不可见);举报
详情页"举报原因"标签被挤到换行。改成按真实子元素数量分栏(2栏)。

第十二轮最小粒度测试 Ch10-11 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:58:36 +08:00
Your NameandClaude Sonnet 5 052cedb39b 重新构建dev产物(实名认证标题、会议响应按钮门禁两处修复)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:23:54 +08:00
Your NameandClaude Sonnet 5 22468edf6b 修复已取消/不可响应的会议列表卡片仍提供拒绝/接受按钮,点击后端403
MeetingCard对拒绝/接受按钮的渲染没有像MeetingDetail那样按
permissions.can_respond门禁,取消后的会议(my_response仍是
pending,属正常状态)会一直显示看似可用的按钮,点击后PUT
.../my-response正确返回403 PERMISSION_DENIED,用户点了却没反应。

第十二轮最小粒度测试 Ch9 发现,用真实取消会议(mtg_0w2TnfQiqaMwy8Bj)
复现(该agent进行中因会话限流中断,此项修复在中断前已完成,事后
确认无误后提交)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:23:48 +08:00
Your NameandClaude Sonnet 5 164a41270c 修复实名认证已认证状态显示臆造标题"认证信息"
参考图(实名认证.png / 实名认证-已实名.png)里两种状态标题文案完全
一致,都是"请您使用有效身份证信息认证"——原来按status分叉出的"认证
信息"这句是mock阶段从未核对过参考图的臆造文案。

第十二轮最小粒度测试 Ch1-2 发现(该agent进行中因会话限流中断,此项
修复在中断前已完成,事后确认无误后提交)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:23:44 +08:00
Your NameandClaude Sonnet 5 1e5e64ae2c 重新构建dev产物(撤回菜单误判、验证码文案、文本收藏撤回态三处修复)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:13:55 +08:00
Your NameandClaude Sonnet 5 b5531b0b7f 修复文本收藏/聊天记录检索不识别已撤回状态、P2P来源收藏名称解析404
问题13(收藏availability恒为available)后端修复后暴露的真实缺口:
text类型收藏和History的history-search结果都只判断item.type,从不看
availability,撤回的文本消息会原样一直显示旧内容(History里甚至会
显示后端messageText()对nil content兜底出的字面量"null")。

另外,P2P来源的收藏来源名称解析函数把复合p2p话题id当用户uid传给
/users/{id}/profile,必现404,永远显示原始话题字符串而非好友昵称;
改用已有的getConversationContext接口正确解析。

第十一轮最小粒度测试 Ch7 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:13:49 +08:00
Your NameandClaude Sonnet 5 117bf43c25 修复身份验证输错短信验证码时显示"独立密码不正确"
CODE_INVALID 这个reason同时被独立密码校验和短信验证码校验两个不相关
接口复用,错误文案只写了前者的措辞,在身份验证页答非所问。改成对两
种场景都准确的通用措辞。

第十一轮最小粒度测试 Ch10-11 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:13:40 +08:00
Your NameandClaude Sonnet 5 fc757cc043 修复消息发送失败/排队中时长按菜单仍提供"撤回"
判断只查 params.seq > 0,未排除 tinode-sdk 本地占位 seq(LOCAL_SEQID,
268435455,同样是正数)——发送失败/仍在排队的消息会被误判成"已持久化
的真实消息",点击撤回后端正确拒绝(无此消息),却给用户一个答非所问
的"请求内容有误"提示。

第十一轮最小粒度测试 Ch3-4 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:13:34 +08:00
Your NameandClaude Sonnet 5 89fdd31cd4 通讯录/新朋友/黑名单列表区分"加载失败"与"真的是空"
三处都用 .catch(() => {}) 吞掉网络错误,loading 结束后空数组直接落到
"暂无好友"/"暂无新朋友申请"/"黑名单为空",加载失败和真的没数据在界面
上无法区分。改成 group-management-view.jsx 里 11 处同类组件已经在用
的 loadError 状态模式。

第十轮最小粒度测试 Ch5-6 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:57:17 +08:00
Your NameandClaude Sonnet 5 4dbe76f2bd 重新构建dev产物(注册持久化登录、白屏崩溃、视频缩略图三处修复)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:49:13 +08:00
Your NameandClaude Sonnet 5 bab1f13e04 修复视频收藏/聊天记录缩略图恒为破损图片、时长换算错误
MediaThumb 把视频实体自身字节(.val/.ref,给 <video> 解码用)当图片
塞进 <img src>,浏览器无法解码,缩略图恒为损坏图标;改用后端已下发但
从未使用的 .preview/.premime 海报字段(lib/formatters.js 的
quoteImageOrVideo()/inlineVideoAttr() 早已用同一约定)。

时长换算:收藏页把毫秒当秒数传给 secondsToTime,2.4秒视频显示
"39:26";History 媒体宫格则完全没接上 duration 字段(mediaMessagePayload
是扁平结构,需要跟 history-files 一样重建 Drafty 实体才能取到)。

第十轮最小粒度测试 Ch7 发现(纠正第九轮"证据不足"的结论)。已用真实
收藏视频实测确认:缩略图正确解码(320x240/96x96),时长正确显示 0:02。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:49:04 +08:00
Your NameandClaude Sonnet 5 9d2db5a648 修复转发/切换话题时应用整体白屏崩溃
componentDidUpdate 的 initMessage 分支直接访问 this.messageEditArea
未判空,而输入框在 noInput(转发中)或 disabled(ACS未解析完)时会
被卸载——此时 initMessage(草稿/编辑内容)一变化就会解引用 null,
无 error boundary 兜底,整个应用白屏。

第十轮最小粒度测试 Ch3-4(实测复现2次)与 Ch8(同一处代码路径独立
命中)各自发现同一个bug。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:46:37 +08:00
Your NameandClaude Sonnet 5 35f659249e 修复注册流程不写入持久化登录态、注册失败500错误显示原始英文
Register组件缺少Login同款的keep-logged-in挂载副作用,导致注册产生的
新账号在下一次刷新时静默丢失会话(无报错、无跳转登录页)。
另外注册走原生WS {acc}协议而非REST,未接入CTRL_ERROR_TEXT_CN翻译表,
"internal error (500)"等错误会原样展示给用户。

第十轮最小粒度测试 Ch1-2 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:46:27 +08:00
Your NameandClaude Sonnet 5 eb4c1cd8da 用登录页现有logo重新生成PWA图标/favicon/OG分享图;顺手发现并重建了严重过期的生产构建产物
图标部分:logo.svg/logo96.png/logo192.png/logo32x32.png/logo32x32a.png原来画的是开源Tinode
项目自己的蓝绿橙"t"speech-bubble标志。用登录页AppMark组件的真实CSS参数(白色圆角方卡 + 旋转
-18deg的蓝色squircle + 加粗白色首字母,色值#1677ff,圆角50% 50% 50% 18%)原样重新画成独立
的SVG/PNG资源,不是另起一套新设计——文字/图标/圆角比例/旋转角度均按登录页实测的computed style
数值1:1还原。favicon(32x32)的"未读"变体(右下角绿点)沿用原有的visual language。

og-logo.jpeg(社交分享预览图,og:image meta标签用)问题比图标本身更明显:原图整个背景是金门大桥
照片,前景写着"Tinode / Open Source Instant Messenger",这两行文字是本次engagement里最直接的
一处Tinode品牌泄漏(没有隐藏在代码里,链接分享出去别人就直接看到)。改成深色渐变背景+同一套
logo mark+"MeetingIM"+"即时通讯",不再引用任何第三方项目的名字或素材。

意外发现:umd/index.prod.js(生产构建产物,index.html的<script>直接引用)从很早之前就没有
重新构建过,仍然包含"TinodeWeb"/"web.tinode.co"/tinode.co的隐私政策与服务条款链接/
support@tinode.co邮箱等一整套本轮及更早各轮已经在源码里修掉的Tinode字样——如果这份产物被
原样部署,之前所有品牌清理都不会真正体现在生产环境。已用`npm run build:prod`重新构建(干净
通过,只有两条跟本次改动无关的资源体积警告),确认重建后的产物里"MeetingIM"正确替换了旧品牌名、
真实域名替换了tinode.co。这次只重新生成了产物文件,没有执行任何部署动作。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 23:30:11 +08:00
Your NameandClaude Sonnet 5 ee2cf6f965 修复收藏/聊天记录检索的请求竞态、下载页按钮占一半宽度、两个类名零CSS覆盖
- Favorites/History 组件的 load() 原来没有过期响应保护:切换类型/输入搜索词都会触发新请求,
  但如果一个更早发出的、范围更宽的请求(比如"全部")比后发的窄范围搜索请求更晚返回,会无条件
  覆盖掉已经正确显示的搜索结果——实测复现过"搜索框显示查询词,列表却是全量未过滤结果"。改成
  每次调用 load() 时认领一个自增计数器,响应到达时如果计数器已经被更新的调用推进过,直接丢弃
  这个过期响应。History组件(按topic/tab/query切换)是同一个模式,一并修复。
- `.library-download-actions`(下载页loading/done两态的按钮容器)是2列等宽grid,但两个状态
  永远只渲染1个按钮——按钮固定占左侧50%宽度,右侧约一半容器宽度是死空白。改成单列。
- `.library-empty-hint`(收藏/聊天记录/下载页的加载中提示文字)和`.library-load-more`
  ("加载更多"按钮)在base.css里完全没有对应规则,退化成浏览器默认段落间距/按钮外观——
  补了基础样式,跟第八轮修复的.library-link-text/.library-record-header是同一类遗漏。

均已重新构建并实测验证(下载按钮宽度、空状态文字样式、加载更多按钮样式均用真实DOM测量确认;
请求竞态修复通过真实收藏页搜索/清空往返验证无回归,竞态本身的精确时序难以在测试环境里稳定
复现,但修复采用的是标准、成熟的自增计数器丢弃过期响应模式)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:46:29 +08:00
Your NameandClaude Sonnet 5 38b43936ce 修复桌面端会话内错误提示完全不可见、收藏发送面板显示空白/裸类型名、delivery-marker图标className拼出undefined
- 桌面布局下打开一个会话时,sidepanel-view.jsx 走的是 messages-home 分支(渲染
  MessageCenterView),这个分支从来没有渲染过ErrorPanel;messages-view.jsx自己的ErrorPanel
  又只在displayMobile时才渲染——两边一凑,桌面端(最常见的使用状态)里任何onError(...)调用
  (上传失败、收藏转发失败、URL风控拦截等)全部没有可见的展示出口,用户只会觉得"点了但什么
  反应都没有"。已用真实handleError()调用实测确认:修复前DOM里根本没有.info-box节点,修复后
  正确渲染且visible。
- "我的收藏"发送面板:favoriteSummary()对图片/视频收藏显示空白(真实数据里这两类的
  snapshot.content.txt是占位用的单个空格,被直接当文本展示)、对文本类收藏显示裸露的内部
  类型字符串"text"(FAVORITE_TYPE_LABEL没有text这个key,且文本收藏的content本身就是纯字符串
  而非{txt:...}对象,原逻辑没处理这种形状)。改成图片/视频/语音三类固定用类型标签、文本类型
  优先判断content本身是不是纯字符串、并给FAVORITE_TYPE_LABEL补上text条目。
- received-marker.jsx的已发送/已送达图标(deliveryMarker对这两种状态返回的对象没有color字段)
  拼接className时把undefined直接字符串化拼了进去,实际渲染成"material-icons small undefined"。

均已重新构建验证;received-marker/favoriteSummary为纯函数级修复,逻辑经过仔细核对但未逐一
截图复核(不依赖运行时状态,风险低)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:42:08 +08:00
Your NameandClaude Sonnet 5 47148aa75d 修复登录页浏览器后退卡死、已认证态下登录/注册的凭据校验绕过、系统消息分页丢失等第1-2章问题
- 最主要的一处:登录成功后点浏览器"后退",会回到一个仍然认为"已登录"的底层连接、但重新挂载出来
  的登录表单——按钮永久卡在禁用的"登录中…"(loginDisabled只有失败分支会重置,成功分支从来没有,
  因为正常路径下这个页面早就导航走了,没人会看到);如果不管这个直接切换企业码后再次提交,
  doLogin()/handleNewAccountRequest()开头的"已认证就直接放行"逻辑会让登录悄悄跳过凭据校验,
  或者让注册请求收到后端原始英文错误"already authenticated (409)"。根治方式是在
  handleHashRoute加一道门:hash指向登录/注册等纯"未登录态"视图、但连接其实已认证时,直接重定向
  离开,不让这类表单有机会被重新渲染出来——已用真实浏览器验证4个入口(login/register/tenant/
  reset/cred)全部正确重定向,且确认不影响真正的登录后安全类页面(如identity-check)。
  handleLoginSuccessful也顺手补了loginDisabled:false兜底。
- 系统消息列表:后端`GET /system-notifications`明明返回`next_cursor`表示还有更多,前端从来没用过,
  未读50+条永远只能看到最近20条、永远没法真正清零。补了"加载更多"分页——已用真实账号(60+条历史
  通知)验证,点击后正确追加下一页、游标耗尽后正确不再显示按钮。
- 已处理(通过/拒绝)的好友申请通知,原来只要event_type以"friend.request"开头就显示"查看新朋友"
  按钮——不区分"待处理"和"已是历史结果",点了跳到待处理列表也找不到对应项。改成只在
  `friend.request.created`时显示,已用真实账号里同时存在created/accepted/rejected三种通知
  验证行为符合预期。
- 注册页四处不统一的占位品牌名(讯工捷迅/捷信密使/捷讯/即时通讯,没有一个是真实品牌名)与
  系统消息页脚"即时通讯安全团队",统一改用BASE_APP_NAME("MeetingIM")。
- config.js的KNOWN_HOSTS.hosted从web.tinode.co改为web.118im.com(这是WS连接失败时的最终
  兜底地址,指向无关第三方域名不只是品牌问题,是真实会连错服务器);host-name.js的
  detectServerAddress()原来只要hostname是localhost就无条件返回硬编码的localhost:6060,
  完全不管页面实际监听的端口——本项目自己的开发代理跑在8093,清缓存后首次连接必然先撞一次
  死连接,改成和非localhost分支一样用页面自己的真实端口。
- 实名认证提交成功后,姓名/证件号输入框原来会变成空白(后端成功响应不含masked_name/
  masked_id_number,组件切到只读态时又没有回退到用户自己刚输入的草稿值)——改成没有脱敏字段时
  回退显示用户本次会话里刚输入的原始值,不是留白。
- Firebase推送通知初始化失败原来复用登录/注册表单的错误提示通道,可能盖住一个同时发生的真实
  登录错误,或在用户还没操作登录表单时就先弹出一条无关提示——推送本身是锦上添花功能,改成只
  console记录+静默关闭开关,不再抢占登录页的错误展示位。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:36:03 +08:00
Your NameandClaude Sonnet 5 8b51fd3eb8 修复通讯录顶部入口箭头图标错位、群管理员列表移除后读到服务端过期缓存
- .people-entry-list > button 是4列grid,最后一列专留给箭头图标,但"新朋友"未处理申请数为0/
  "群组"/"文件助手"/"系统消息"这几个入口没有badge时,JSX只渲染3个真实子元素,grid自动布局
  把箭头挤进第3列——实测空出623px空白,图标紧贴文字,完全不是贴右对齐效果。改成显式
  grid-column:4 钉住箭头位置,不再依赖前面到底渲染了几个兄弟节点。
- AdminList移除管理员后紧接着的load()重新查询命中了服务端对这个查询形状的过期缓存,界面上
  刚移除的管理员会短暂"复活"。改成移除成功后直接用已确认生效的结果本地更新state+patch共享
  缓存,不再指望立即重新查询能反映刚发生的变更(这个缓存问题本身是后端"新增7"已记录的问题,
  重新查询本身没有办法绕过,前端这次做的是不依赖它)。

均已用真实DOM结构/computed style确认修复生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:23:19 +08:00
Your NameandClaude Sonnet 5 7db687602b 修复移动端语音通话胶囊被撑成方块、来电页认证徽章在长姓名截断时完全不可见
- #video-container.minimized 的移动端媒体查询规则(宽度100%/高度11rem/方角,本是给视频悬浮窗
  修的)和 #video-container.audio-minimized(语音胶囊自己的auto/auto/无阴影)特异度相同
  (1,1,0),前者写在文件更后面、源码顺序上赢了——语音通话点最小化后,移动端下胶囊被强行撑成
  贯穿屏幕顶部的方形透明条,胶囊本体和还原按钮挤在左上角。补 :not(.audio-minimized) 排除。
- 来电全屏页认证徽章原来和姓名文字共用同一个 white-space:nowrap+ellipsis 的<h1>,姓名够长
  触发省略号时,排在姓名后面的徽章(同一行内容里靠后的部分)跟着一起被截断出可视区域之外——
  不是"字体显示不全",是整个徽章都看不见。改成flex布局:姓名单独一个可截断的flex子项,
  徽章作为flex:none的兄弟节点,不参与省略号裁切,姓名截多短徽章都能跟在后面。

均已用真实DOM结构/getComputedStyle确认修复生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:19:58 +08:00
Your NameandClaude Sonnet 5 d573b38d10 第九轮(部分):修复日报徽章配色、彻底重做PIN定位方案、清理静态文件品牌残留
- 日报卡片"已提交/未提交"状态徽章补色:DailyReportCard用<article><header><em>结构,原选择器
  要求.report-cards直接子<button>,日报卡片永远命中不了、只落到不分状态的兜底样式;月报因为
  行本身就是<button>所以没受影响。详情页.report-author em同理补了区分色(原来恒为绿色,"未
  提交"用绿色在语义上很怪)。
- 青少年模式/独立密码设置的PIN输入定位改用结构性方案,不再靠猜一个rem偏移量:给
  .auth-pin-boxes/.auth-pin-input包一层.auth-pin-wrap,输入框inset:0精确覆盖方块的真实
  渲染框,不管外层页面有没有标题、padding多少都自动对齐。上一轮的补丁(.youth-password-gate
  .auth-pin-input{top:1.75rem})把重叠率从0%提到32.5%,本轮实测确认还是不够(方块下方
  67.5%的面积点不中),根治后4个采样点(四角+中心)全部精确命中输入框。过程中额外发现并
  修复了两个更隐蔽的层叠问题:全局input[type=text]的height:2.5rem特异度反超裸class、
  textarea/input共享的margin:0.25rem重置——不清掉这两个会让inset:0看起来生效了、实际尺寸/
  位置还是被悄悄改掉。
- .report-loading(举报原因页加载态)补基础样式,原来在base.css里零匹配,吃浏览器默认<p>样式。
- 清理静态文件里的Tinode品牌残留:index.html/index-dev.html的<meta>标签(application-name/
  description/og:*/canonical,此前只改了<title>,meta标签一直没跟上)、manifest.json(name/
  short_name/description改MeetingIM;prefer_related_applications+related_applications整个
  删掉——这两个字段会让Android/Chrome"添加到主屏幕"时引导用户去装无关的第三方开源Tinode App,
  不只是文案问题);删除.well-known/assetlinks.json(声明了无关第三方App对本域名的URL处理/
  登录凭据代理权限,属于遗留模板残留,无真实对应关系,直接移除而非编造虚假内容)。

manifest.json里的图标文件(logo96.png/logo192.png/logo.svg)本身画的就是Tinode的蓝绿橙"t"标志,
是真实的视觉品牌泄漏,但重新设计一套图标涉及视觉识别决策,本次未动,留给用户决定。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:17:48 +08:00
Your NameandClaude Sonnet 5 f688578714 清除缓存/清除聊天记录/注销账号确认弹层改为叠加显示,不再是整页路由跳转
这三个"确认"操作原来是独立的hash路由,点击后"设置"整个列表(含标题栏)连同Header一起卸载,
只剩ConfirmSheet的半透明蒙层盖在空白背景上——蒙层本来是设计成衬着被弹出的页面看的,但因为
底下真的没有渲染任何东西,视觉上跟跳转到一个新的空白页面没有区别。

改为SettingsList自己的本地state(sheet: null|'cache'|'history'|'delete'),确认层作为兄弟
节点叠加渲染,列表本身不卸载,不再经过hash导航——照抄group-management-view.jsx的OwnerSelect
(选择新群主)已经在用的正确模式。同时清理了tinode-web.jsx两处硬编码视图白名单里对应的
三个route名(不再是独立路由,无需保留)。

contact-management-view.jsx同类的删除好友/拉黑/清空聊天记录确认层核实过不受影响——它的
ConfirmSheet会重新渲染Settings页面作为背景,视觉上本来就是正确的。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 17:32:39 +08:00
Your NameandClaude Sonnet 5 d759f67b11 第八轮最小粒度测试:修复CSS选择器/JSX结构不匹配的一整类bug,清理Tinode品牌残留
第八轮改用"读CSS选择器+核对JSX结构"的方式复测全部11章,命中了7轮纯接口测试从未发现的一类
问题——选择器写对了但目标元素的实际DOM结构/className对不上,样式静默失效:

- 语音通话最小化胶囊按钮尺寸被同特异度规则覆盖;移动端悬浮通话窗口left/right/width三值同时
  生效导致过度约束;来电全屏页认证徽章/姓名标题缺少专属样式覆盖
- 我的收藏-语音卡片三列grid对应实际只有1个子元素,播放器被压缩到卡片左侧一角;链接/记录头部
  完全没有基础样式,退化成浏览器默认外观
- 多选模式下"收到的"消息气泡被flex auto-margin一起拖到最右侧;.avatar-box/#send-message-panel
  两处逗号断句错误导致后半句选择器丢失祖先限定
- 按住说话悬浮反馈渲染成内联胶囊,而参考图要求的居中悬浮卡片样式其实已经存在(挂在了另一个非
  生产demo组件上);批量转发确认框沿用了80%视口高度;audio-player.jsx的playback类点在canvas
  上而非包裹层,波形和计时器可能纵向堆叠
- "消息已撤回"占位符误拼了日期分隔条的class而非专属的deleted

品牌清理:浏览器标签页标题、未读数标题更新均硬编码"Tinode";LINK_CONTACT_US等三个链接常量
指向开源Tinode项目自己的域名(联系我们改为跟用户条款/隐私协议一致的toast,不再发起外部mailto)。

以及:空会话丢失.chat-box样式类、青少年模式PIN定位、若干收藏/文件类型展示细节。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 17:09:42 +08:00
Your NameandClaude Sonnet 5 5e173c55e8 修复日报/月报详情页返回目标写死,团队视角进入会被送回"我的"列表
DailyReportCard/月报列表的报告条目在 team=true(团队统计)和 team=false(我的日报/月报)两种
模式下渲染的是同一个组件、同一个"查看详情"跳转,但 DailyReportDetail/MonthlyReportDetail
的返回目标写死成 daily-report-mine/monthly-report-mine——管理者从"团队统计"点进某条下属的
报告,返回却被送到自己的"我的日报/月报"而不是刚才浏览的团队列表。跳转时带上 team 参数,
详情页据此选择正确的返回目标,和同文件里 DailyReportList/MonthlyReportList 自己的
`team ? 'x' : 'workbench'` 返回逻辑保持一致模式。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 15:20:04 +08:00
Your NameandClaude Sonnet 5 290fc80f3e 群二维码行的小图标也应跟箭头贴在一起靠右,而不是单独留在文字标签后面
上一次修复把 margin-left:auto 放在了这一行的最后一个图标(箭头)身上,箭头确实到了最右,
但二维码小图标是这一行倒数第二个子元素,没吃到这个 auto margin,还留在原位——变成"二维码
图标贴左、箭头单独贴右"两边分家。auto margin 改放到这一组里最靠前的图标(二维码图标)身上,
箭头作为它后面的下一个兄弟元素自然跟上,两个图标现在整组一起贴右。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:30:43 +08:00
Your NameandClaude Sonnet 5 e66550867c 修复群资料页箭头图标参差不齐
"群头像"/"群二维码"两行没有其它行都有的 <small>(靠 margin-left:auto 把末尾箭头推到最右),
末尾图标紧贴在文字标签后面,导致整页箭头图标一个贴左一个贴右,参差不齐。给这两行末尾图标
直接加 margin-left:auto,跟其它行的箭头对齐到同一条竖线上。

(过程中试过给文字标签加 margin-right:auto 间接推挤,实测反而导致标签本身被推到行中间,
不是预期效果,改为直接在末尾图标自己身上加 margin-left:auto,语义更明确也更可靠。)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:25:42 +08:00
Your NameandClaude Sonnet 5 bd1d6ff0b0 修复群资料页头像被拉伸变形,成员数改为跟列表一致的可点击行
- .group-profile > button > span 漏排除 .group-avatar,把只该用于文字标签列对齐的
  min-width:6rem 也套到了头像 span 上,头像被横向拉伸成80px宽的长方形而不是正方形
- 底部"N位成员"原来是一个孤立的灰底纯文本 <p>,样式和上面的列表格格不入;改成与其它
  行一致的可点击行,点击跳转真实的群成员列表页(复用群聊设置页已验证过的同一个跳转)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:16:55 +08:00
Your NameandClaude Sonnet 5 dfb7ab53a5 修复群组管理页"设置群头像"按钮样式退化成浏览器默认外观
.group-governance 下所有行样式都挂在 "section > button" 选择器上,这一个按钮没包在
<section> 里,完全没吃到列表行样式(全宽/分割线/内边距),退化成一个居左的灰色圆角
小按钮。包一层 <section> 即可,其余4个链接本来就是这样。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:08:57 +08:00
Your NameandClaude Sonnet 5 f2218a822e 修复用户实测反馈的4处问题:品牌名泄露、修改密码页注释泄露、安全提醒页未居中、群相关行间距
- 品牌名:BASE_APP_NAME 从 "TinodeWeb" 改为 "MeetingIM"(关于页/青少年模式文案不再暴露 Tinode 字样);
  logo-view.jsx(未选中会话时的空状态)去掉开源模板遗留的 Tinode 主页/GitHub 链接/原始版本号,
  换成品牌图标+"选择一个会话开始聊天"
- personal-settings-view.jsx:删除 ChangePassword 页面里被误当正文渲染的开发注释(提及 Tinode 协议),
  改回真正的代码注释
- base.css:.risk-info-page(安全提醒整页)补 margin:0 auto——原来复用模态弹窗的 .risk-dialog 样式,
  宽屏下整块内容贴左显示,没有居中
- base.css:.group-member-picker/.group-settings-header/.group-member-section-title 的 strong/small
  漏加入既有的 line-height:1.3 覆盖名单,继承通用 button{line-height:2.2rem},导致群成员列表/群聊设置
  头部的姓名与角色/简介文字行间距异常大、贴底部分割线过近

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:06:18 +08:00
Your NameandClaude Sonnet 5 bede45fd7f 第七轮深度测试:修复7个agent发现的12处前端问题,重点是列表分页截断与呼叫/播放状态机
- 通话状态机(Ch8):canStartCall 补齐通话中重复发起的防护,callShouldStart 补齐放弃/失败路径的清理
- 分页截断(Ch5-6):新增 fetchAllPages 辅助函数,修复通讯录/黑名单/好友申请/群成员/管理员/禁言等
  11 处列表只读首页导致真实成员被遗漏的问题;群角色判断改用不受分页影响的 getGroup().my_role
- 消息与媒体(Ch3-4/Ch7/Ch9):录音超时污染时长、手动重试丢失 head 导致消息被错误分类、批量删除
  未取消上传中附件、语音播放器补齐同时只能播一条、workplace-collaboration-view 冗余重试请求
- 独立密码/青少年模式(Ch10-11):设置密码后按调用来源判断是否应自动开启青少年模式,避免关闭流程
  中途"忘记密码"被误当成开启;注销账号确认文案补充群主后果说明

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 12:18:25 +08:00
Your NameandClaude Sonnet 5 f42b8536e5 第六轮深度测试:7个并行agent覆盖全部11章,修复21处前端问题
Ch1-2: 用户名注册邀请码错误改为友好文案(原为原始TypeError泄露给用户);
  企业注册页扫描图标补上点击反馈。
Ch3-4: 发名片消息补传typeHint(此前被后端误分类成text,搜索/历史摘要显示成JSON);
  文件卡片按mime大类区分图标颜色(此前所有文件都是同一个灰色通用图标);
  表情面板补上专属退格/发送按钮(浅蓝禁用/深蓝启用两态);订正一处与截图不符的代码注释。
Ch5-6: 群聊设置页"群成员"补上可点击的完整成员列表页(复用MemberPicker字母分组);
  群文件页去掉无依据的硬编码"/10GB"容量上限显示。
Ch7: 收藏点击图片/视频缩略图补上预览;"查找聊天记录-链接"tab补上定位回原消息;
  文件下载页返回入口按来源tab纠正;批量转发成功计数改为按目标会话去重
  (原公式混用"收藏条数"和"目标会话数"两个维度,混选多条收藏转发时可能显示负数);
  收藏/视频tab切换补上URL路由同步。
Ch8: topic.onInfo在通话期间补上事件转发(此前通话中"对方正在输入"和已读回执静默失效);
  主叫等待接听页补上"正在等待对方接受邀请…"文案;语音通话最小化胶囊的还原按钮移出
  拖拽区域(原来嵌在draggable内部,指针捕获可能吞掉点击);修复中等窗宽(641-750px)下
  本地摄像头预览遮挡"切换摄像头"按钮的CSS重叠。
Ch9: 部门树展开时跳过child_count=0的注定空请求;月报字数上限从硬编码1000改为读取
  后端真实的10000;发起会议补上"结束时间需晚于开始时间"的本地校验和更具体的报错;
  日报/月报统计接口补上department_id透传(此前包装函数注释断言后端会忽略,实际已支持)。
Ch10-11: URL风险检测补上g标志(原来一条消息只检查第一个链接,第二个链接哪怕是高危
  也不会被拦截);补上"安全教育"设置入口(原功能完整实现但零入口,需手改hash才能打开);
  修改密码按钮补上busy态反馈;青少年模式PIN页补上"忘记独立密码"入口(后端已支持重置,
  前端未接);"关于我们"补上"联系我们"行。

7个agent同时对全部11章存量后端问题条目逐一复核,结论详见待整理的后端问题清单更新
(Ch1-2发现一条P0级后端问题:验证码固定值"123456",已记录未修复,不在此commit修改
im_be仓库范围内)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 03:48:28 +08:00
Your NameandClaude Sonnet 5 d42591284f 第五轮深度测试:管理员撤回、所有人@提醒、失效附件UI,并核对现存后端问题清单
- messages-view.jsx: 拖拽发送/回复菜单在编辑态下的旁路修复;群主/管理员可撤回他人消息
  (对齐后端 authorizeIMMessageMutation 的 allowAdmin);新增"所有人"@提醒(head.mention_all)。
- chat-message.jsx: system_action 头类型消息按 system_notice 同款渲染(此前显示"内容无效")。
- content-library-view.jsx: 语音/链接/图文收藏与聊天记录卡片补上 removed(已失效)态UI,
  之前只有 FileCard 处理了这个状态。
- call-panel.jsx: handleVideoOfferMsg(5步.then链,用reject sentinel跳过后续步骤)、
  handleToggleSpeakerClick 补组件卸载守卫,修完此前遗漏的两处。
- group-management-view.jsx: 治理开关(Governance.setField)和群主转让(OwnerSelect)
  补上乐观锁 version 参数(此前从不传,服务端 version<=0 时会跳过冲突检测);群文件搜索框
  文案承诺"搜索文件名或者上传者"但服务端q参数只匹配文件名,改为拉取全量列表本地按文件名+
  已解析的上传者昵称一起过滤;群公告详情页补上从未被调用过的已读上报(readGroupAnnouncement)。

以上均用真实账号在浏览器/直接请求验证:管理员撤回、system_action渲染、mention_all、
文件按上传者搜索、公告已读上报(200)、治理开关stale-version探测确认409冲突检测生效。

7个并行测试agent对全部11章做了第二轮回归+现存后端问题清单逐条复核,结论待整理进
docs/IM-后端问题清单*.md(全部现存条目复核后均建议保留,多条证据强度从推断升级为已验证)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:07:47 +08:00
Your NameandClaude Sonnet 5 4a30cfa6b3 第四轮深度测试:修复7个agent发现的问题,重点是编辑态附件覆盖的完整旁路和通话面板崩溃防护
再次派7个agent覆盖全部11章,验证第三轮修复+找新问题。本次委托agent专门交叉核对
上一轮每一项修复是否真的生效,多数确认有效,但抓出2处"看似修好、实则有旁路"的情况
和若干新问题。

**编辑消息+拖放文件仍可静默覆盖被编辑消息(上一轮修复的完整旁路,Ch3-4 agent发现)**
上一轮给`onAttachFile`/`onAttachImage`/`onAttachAudio`三个prop加了editingMessage门禁,
但`isDragEnabled()`(messages-view.jsx)没有检查这个状态——拖放文件到聊天窗口走的是
`handleFileDrop`直接调用`handleAttachImageOrVideo`/`handleAttachFile`类方法本身,完全
绕开了被置null的props。编辑态下不点"+"号、直接把文件拖进聊天窗口,依然会静默替换掉
被编辑消息的原文。已给`isDragEnabled()`补上同款判断,这是`handleDrop`和渲染层拖放提示
两处调用的唯一门禁函数,一次性堵住。

同一个agent还发现编辑态下点击"回复"另一条消息会静默放弃当前编辑(`handlePickReply`
覆盖`this.state.reply`但不清`contentToEdit`,发送时值走了`reply`分支而不是`editing`
分支,编辑框里的原文被当成对另一条消息的新回复发出去,原消息未被修改)——已在
`handleShowMessageContextMenu`里比照`menu_item_edit`的年龄过滤模式,编辑态下直接不
提供"回复"这个菜单项。

**实名认证驳回提醒的dismissKey修复上一轮实际没生效(Ch1-2 agent发现,已重新修复)**
上一轮把dismissKey从纯status改成"submission_id或status",但live验证发现
`bootstrap.security.realname`这个数据源根本不包含submission_id字段(后端serveIMBootstrap
的实现只有status/required/allow_skip/remind_after四个硬编码字段)——所以这个修复在
真实环境下永远退化回status,等于没有变化,两次独立驳回依然会互相误压制提醒。这次改为
额外调用专用的`GET /security/realname`接口(该接口的实现确实会返回submission_id),
用它的数据驱动dismissKey和提醒触发条件,bootstrap的字段只保留用于remind_after/
allow_skip这些它独有的字段。已用真实账号验证dismissKey现在正确从这个新数据源生成。

**呼叫面板未卸载防护补全到call-panel.jsx(Ch8 agent发现,同类问题上一轮只修了
audio-recorder.jsx)**
`start()`/`unmuteVideo()`/`handleSwitchCameraClick()`三处的getUserMedia/
enumerateDevices异步链,结尾都会touch `this.localRef.current`/`this.state.localStream`,
但都没检查组件是否已经卸载——权限弹窗未决出结果时通话结束(呼出超时/收到挂断/对方
主动挂断),这些promise resolve后会在死实例上抛TypeError,被各自的catch吞掉后又会展示
一句过期的原始英文报错。三处都补了`this._unmounted`检查,拿到的媒体流/轨道直接停止
而不是继续使用。

**其余修复**:`call-message.jsx`补上和`call-status.jsx`一致的"进行中"状态文案(原来
通话进行中且消息还没有最终时长时显示"0:00"而不是"进行中",已用真实渲染验证过两个
组件行为不一致);`tinode-web.jsx`的`handleCallClose`补上重置`callSeq`(避免跨通话
残留旧值)+ 更新`armCallRingTimeout`注释准确描述它是后端30秒建立超时的二级兜底而非
主要防线(后端`call_establishment_timeout`默认30秒,已有独立机制处理"直接不接"的
常见场景);`REASON_MESSAGES`补充`API_KEY_REQUIRED`/`METHOD_NOT_ALLOWED`(此前记录过
一次真实的部署期API Key配置漂移事故,属于这两个reason的真实触发场景);
`message-center-view.jsx`的`NOTIFICATION_TEXT`补充风险确认"取消"结果的文案(发消息
前风险提示点"取消发送"会产生这个系统通知,此前只收录了"继续");会话列表"置顶"从
只变背景色改为真正排到列表最前(原来只有CSS着色,没有任何排序逻辑,已用真实群聊
置顶验证过排序生效且系统消息/文件助手两个固定入口仍保持在最前);好友聊天设置页
置顶/免打扰开关在初始GET未完成前禁用(避免`version:null`打到后端400);
`content-library-view.jsx`的`DownloadPage`修复带外附件下载使用未做域名归一化的裸
相对路径(生产环境webapp和文件域名跨域时会必现下载失败,已拆出`resolveFileUrl`
辅助函数只做URL拼装不做预先fetch,跟`getFileUrl`的实际下载区分开);
`security-support-view.jsx`的意见反馈分类配置改用`useCachedData`获得重试能力(原来
独立实现的边车缓存逻辑没享受到本轮`use-cached-data.js`的重试修复)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 01:03:41 +08:00
Your NameandClaude Sonnet 5 93b856aec8 第三轮深度测试:修复7个并行agent发现的前端问题,含REASON_MESSAGES系统性补全
派了Ch1-2/3-4/5-6/7/8/9/10-11共7个agent对全部11章重新深度测试。后端问题已核实追加进
docs/IM-后端问题清单-2026-09-04.md(新增6-9条,发给用户);本次是前端问题的修复。

**REASON_MESSAGES系统性补全(最高优先级,多个agent独立发现同一个缺口)**
im-api.js的REASON_MESSAGES表新增22个条目,最重要的是PERMISSION_DENIED——后端
server/im_http.go里403响应统一用这个reason(30余处调用),但表里原来只有一个从未
被后端实际发送过的死条目FORBIDDEN,导致全站几乎每一个403从工程一开始就在展示原始
英文"permission denied"。一并补上VERSION_CONFLICT/MALFORMED_REQUEST(Ch9 agent实测
命中)、NOT_FOUND、VERIFICATION_RATE_LIMITED(动态文案,同TOO_MANY_ATTEMPTS模式)等。
新增translateReason()导出函数并用于send-message.jsx的sendFavorite——favorites/forward
等几个批量接口恒定200、真实成败包在items[].reason里,imRequest的自动翻译够不到这种
per-item场景,之前直接显示原始reason字符串。tinode-web.jsx同理补了CTRL_ERROR_TEXT_CN
(原生WS {ctrl}协议错误,同样绕开REASON_MESSAGES)并修了原表从未真正匹配过的bug——
CommError会把状态码拼进message变成"authentication failed (401)",原表按裸文本精确
匹配,看着正常纯粹是因为401/403兜底文案凑巧一样,一旦命中"unknown authentication
scheme"这类会被兜底文案误导、"policy violation"这类会直接漏出英文。uploadLargeFile/
createChatUploader/createFileDownloader同样绕开REASON_MESSAGES(走原生{ctrl}协议),
补了UPLOAD_CTRL_TEXT_CN小词典。已用真实账号端到端验证:favorites/forward到P2P目标
触发的PERMISSION_DENIED现在正确显示"没有权限执行此操作";draft接口版本冲突正确显示
"内容已被修改,请刷新后重试"。

**编辑消息时打开附件面板会静默覆盖被编辑消息(Ch3-4 agent发现,数据损坏级别)**
sendMessage对editing状态一律用head.replace,但onAttachFile/onAttachImage/onAttachAudio
原来只在forwardMessage时置空,编辑态没有同款门禁——编辑一条消息不改文字、点"+"选张图片
或名片,会直接把附件内容替换掉被编辑消息的原文,无任何确认。messages-view.jsx新增
editingMessage门禁,和forwardMessage一样把三个attach回调置空(等价于隐藏整个"+"按钮)。
已用真实浏览器验证:编辑态下"+"按钮消失,取消编辑后正常恢复。

**草稿防抖单一共享定时器丢字(Ch1-2、Ch3-4两个agent独立发现)**
handleComposerChange用单个this.draftSaveTimer,话题A打字触发600ms保存后300ms内切到
话题B,会把A的定时器clearTimeout掉,那次输入永久丢失;version也在定时器触发时才重新
读state,此时可能已经在别的话题上,退化成硬编码0——叠加后端draft接口对version=0"无条件
跳过乐观锁"的行为(已记入后端清单新增6),可能静默覆盖别的标签页写入的更新草稿。改为
按话题分开的定时器map,version在调用的当下(保证仍在该话题)同步捕获进闭包。

**呼叫状态机两处真实bug(Ch8 agent此前发现,本次重新核实live代码后确认+修复)**
1. 通话中导航离开messages-view.jsx整个组件树(切到工作台/个人设置,或移动端切回会话
   列表)时,没有任何hangup清理——componentSetup已有的"切换话题时正确挂断"逻辑只在
   componentDidUpdate里,组件真正卸载时不会触发。CallPanel自己的componentWillUnmount
   只做本地清理,不通知对方也不重置tinode-web.jsx顶层callState,导致下次切回通话话题
   时remount出一个callState卡在CALL_STATE_IN_PROGRESS的"僵尸"面板——start()走"我们刚
   接听"分支直接return,从不调getUserMedia,本地媒体永久拿不到。补了对称的
   componentWillUnmount挂断逻辑。
2. this.callTimeoutTimer声明后从未被真正setTimeout赋值——呼出无人接听或accept信令
   (fire-and-forget的{note}包)被丢会永久卡在"呼叫中"。补了armCallRingTimeout(),
   60秒未接通按跟手动取消完全一样的路径挂断(不发明新的通话结果语义),在收到对方
   accept信号的两处分支都正确清timer。

**其余前端修复(按文件)**
- messages-view.jsx: @提醒mentionedUsers只在切话题时清空,同一话题内后续消息里字面
  文本"@张三"会被静默解析成真实提醒(发送后清空);jumpToMessage点开已撤回/删除的
  搜索结果原来只console.error静默失败,补了用户可见的错误提示;批量收藏对
  favorites/batch共享Idempotency-Key导致的静默丢失(已知后端问题)不再照单全收
  "收藏成功",改为发送后重新拉取真实收藏列表核对每条是否真的落地。
- audio-recorder.jsx: getUserMedia权限弹窗未决出结果时组件被卸载,补_unmounted
  标记避免在死实例上继续跑mediaRecorder.start()。
- send-message.jsx: disabled变为true时(对方拉黑/被禁言等实时ACS变化)没有强制结束
  正在进行的按住录音,可能remount出无用户手势的"自动录音";mouseup/touchend覆盖不到
  拖出窗口/切换标签页/权限弹窗抢焦点的释放场景,补了blur/visibilitychange兜底。
- tinode-web.jsx: messages-menu视图和messages-home共用MessageCenterView实例无key,
  从messages-home以非整体刷新方式切到messages-menu时菜单打不开,补key={view}。
- message-center-view.jsx: 实名认证驳回提醒的dismiss key只按用户+状态区分,同一状态
  的两次独立驳回会互相误压制,改按submission_id区分。
- contact-management-view.jsx: Profile/Settings靠getUserProfile刷新拉黑状态,但该
  接口对被拉黑对象直接403,导致"之前会话就已拉黑"的场景完全感知不到——叠加
  getConversationContext兜底(该接口对拉黑目标正确返回200+relationship)。
- group-management-view.jsx: AdminList显示群主/管理员姓名漏了group_nickname兜底;
  JoinRequests从聊天页横幅进入时返回按钮硬编码回group-settings(应回聊天本身),
  从Governance进入时返回group-settings而非group-governance(与AdminList/Muted不一致)。
- workplace-collaboration-view.jsx: OrganizationDirectory/DeptNode的部门树加载失败
  被无条件当成"暂无数据"渲染,补loadError区分+重试链接。
- personal-settings-view.jsx: CacheClear对navigator.storage/caches整个不存在的情况
  只有react出了范围的.catch()覆盖不到的同步抛错,加了特性检测+try/catch;
  YouthModeVerify触发TOO_MANY_ATTEMPTS锁定后确定按钮没有禁用/倒计时,会继续发出
  注定失败的请求。
- security-support-view.jsx: 意见反馈category字段在config未加载完成前可为空但没有
  提交前校验,会打到后端400。
- use-cached-data.js: bootstrap等首次拉取失败且无缓存兜底时永久停在undefined,加了
  一次3秒后重试(仅在完全没有缓存值时触发,已有缓存值维持原"显示旧数据"行为不变)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 23:29:52 +08:00
Your NameandClaude Opus 5 505a6d7d56 补齐第3-4章composer功能缺口:跨话题草稿/@提醒/表情面板/附件面板/名片/收藏发送/按住说话/语音未读红点
第3-4章复测确认这批原型图要求的功能之前完全没做(真实聊天引擎 messages-view.jsx/
send-message.jsx 从Ch1开始一直是最基础的版本:两个附件图标+文本框+发送/麦克风,
没有@、没有表情面板、没有7宫格附件面板、没有名片/收藏发送、语音是点一下进入录音模式
不是按住说话)。这次一次性把8项都接上,除了新功能外,过程中顺带发现并修了4个真bug。

**跨话题草稿**(im-api.js 新增 getTopicDraft/updateTopicDraft,messages-view.jsx)
话题切换时拉取 GET /topics/{topic}/draft(真实验证过P2P用原生话题名——对方user_id——
可以正常读写,接口内部自己会查Subs.Get校验,不需要额外归一化),输入框内容600ms防抖
PUT回去;真正发送成功后立即清空服务端草稿,不等下一次防抖。真实验证:输入文字→切走
再切回→草稿还在;刷新整个页面(新WS会话)→草稿依然从服务端正确恢复。

**@提醒**(send-message.jsx 新增 mention 相关状态和UI,messages-view.jsx 新增
mentionSearch/mentionedUsers/applyMentionResolution)
只在群聊开放,检测"光标前最近一个未闭合的@"触发面板,调 getGroupMembers(topic,
{mentionable:true, query}) 做实时搜索。协议层面:Tinode原生Drafty.parse本来就会把
纯文本里的"@word"自动识别成MN实体,但data.val存的是匹配到的原始文字,不是uid——
真正的uid只有用户从选择器里点选时才知道,记在 messages-view.jsx 的
this.mentionedUsers,发送前重新parse一遍把匹配上的占位符换成真uid。真实验证:选
"kcg051307"发送后直接查后端存储,MN实体的data.val确认是真实uid
"usrYVeV0yZs8tw",不是原始文字。

**表情面板**(send-message.jsx)
2个tab(😊表情/♡常用),常用表情本地localStorage维护。图片没有拍到第二个tab具体
内容,判断"常用"是这类两tab设计里最常见、也是本项目里唯一有本地存储依据能撑起来的
语义。真实验证:点表情正确插入文本框,用过的表情正确出现在"常用"tab。

**7宫格附件面板**(send-message.jsx,替换原来常驻的照片/文件两个图标)
照片/拍摄/语音通话/名片/文件/收藏/视频通话。语音通话、视频通话复用第8章已有的
handleStartAudioCall/handleStartVideoCall(从 tinode-web.jsx 新抽出 canStartCall()
方法,同时替换掉"..."菜单里原来的内联判断,避免同一个判断逻辑两处维护),仅P2P且
真实通话功能可用时才不置灰——真实验证过群聊里这两项正确显示为禁用状态。

**名片消息**(send-message.jsx openCardPicker/sendCard,复用原生TheCard/
Drafty.appendTheCard)
选好友生成名片,uid必须用TheCard.setTinodeID写进vCard的comm数组(不是随便塞进某个
字段——widgets/the-card-mini.jsx渲染时用TheCard.getFirstTinodeID读这个协议项来判断
"发消息"按钮指向谁)。真实验证:选中好友后发出的名片消息真实带"聊天"按钮,点击可用。

**收藏发送到当前会话**(send-message.jsx openFavPicker/sendFavorite,复用Ch7的
forwardFavorites接口)
真实联调中发现并修复一个新bug:这个接口和其它几个批量接口一样,HTTP层恒定200,
真实成败要看items[].status——原来的写法完全没检查这个字段,等于假装总是成功。已改成
检查失败项并报错。同时真实测试还发现该接口对P2P目标会返回200+items[0].status:
"failed", reason:"PERMISSION_DENIED"(哪怕是真实好友、这个会话本身发消息完全正常),
看起来是后端这个接口自己的写权限判断有问题,不是前端能修的,已记录。

**按住说话+上滑取消录音**(send-message.jsx新增手势处理,audio-recorder.jsx新增
holdMode展示态)
mousedown/touchstart直接挂载AudioRecorder(挂载即开始录音,复用原有机制不动),
移动超过80px判定为"松开取消",松手时通过ref直接调组件内部的handleDone/handleDelete。
过程中发现并修复3个真bug:① injectIntl包裹AudioRecorder默认不支持ref转发
("Function components cannot be given refs"警告,ref静默失效),加
{forwardRef:true}修好;② handleDelete从来没有像handleDone那样判空mediaRecorder,
录音还没真正开始(getUserMedia尚未resolve)就点删除会直接抛TypeError,这是原有代码
一直存在的问题,这次因为按住松手可能在录音刚开始的瞬间发生而被真实触发;③
cleanUp()同理没判空this.stream。补上判空后,"从未真正开始录音就结束"这个场景会
直接当作取消处理并正确调用onDeleted,不会再把UI卡在录音态出不来(真实验证:这个
环境的浏览器面板本身会拦截麦克风访问,用这个"getUserMedia永远拒绝"的真实场景反而
完整测出了正常/取消两条路径在录音真正开始前结束时都不再卡死或报错)。

**语音消息未读红点**(chat-message.jsx/audio-player.jsx/formatters.js)
纯客户端概念,原生Tinode的recv/read只到"消息本身有没有被看到",跟"这条语音有没有
真的被点开听过"是两回事。audio-player.jsx的播放按钮新增可选onPlay回调(不传不影响
现有调用方),formatters.js的AU分支透传给它,chat-message.jsx按话题维护一份
localStorage"已播放seq集合",对收到的(非自己发的)语音消息,没播放过就在气泡右上角
显示红点。真实验证:用原生SDK的_routeData往话题本地缓存注入一条真实结构的语音消息
(浏览器面板拦截麦克风,没有真实语音消息可用,用这个方式验证渲染/播放回调/持久化
三段逻辑),红点正确出现;直接触发播放回调后红点消失且localStorage正确写入对应seq。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:59:09 +08:00
Your NameandClaude Opus 5 abef778c7f 月报补上团队统计视图,镜像已有的日报团队视图(第9章遗留缺口)
MonthlyReportList 原来只有"我的月报"一种形态,没有 team/managed 入口——日报有的
"团队统计"(应提交/已提交/未提交计数)月报完全没有,路由/组件都不支持。核对
im_be 源码确认后端本身是对称的:TenantMonthlyReportList 的 scope=managed 分支
跟日报字面上是同一段 canUseManagedReports(visibility) 权限校验(真实请求验证:
无权限账号收到 403 PERMISSION_DENIED,不是之前日报那种曾经误报全公司数据的
旧版本行为——那个越权问题后端已经修过了),GET /reports/monthly/statistics
的响应字段(expected_count/submitted_count/missing_count)也和日报完全一致。

唯一确认过的不对称点:月报没有对应的"提醒未提交成员"接口(daily有
POST /reports/daily/reminders,翻遍 im_http.go 月报没有任何同类路由)——团队视图
因此没有画这个按钮,画一个点了必然404的按钮不算镜像,是给用户挖坑。

路由从单一的 monthly-report-list 拆成 monthly-report-mine/monthly-report-team
(对齐 daily-report-mine/daily-report-team 的命名),两处 tinode-web.jsx 的硬编码
白名单、APP_ROUTES、详情/编辑页的返回目标都已同步更新。

联调中发现并顺手修正一个刚写的真bug:team 分支的空列表判断沿用了旧的无条件
"暂无月报"文案,没有区分"真的没数据"和"403加载失败"——用真实账号触发403验证时
第一次跑出来就是这个问题,同一轮改完立刻测出来并修正(跟本次会话更早修的
DailyReportList 同款问题是同一个模式,这次是抄自己代码时漏抄了一半)。

真实验证:我的月报视图不受影响;团队统计视图真实触发403(该账号只是普通员工),
正确显示"月报列表加载失败"而不是误导性的"暂无月报";点击"我的月报"correctly
跳回单人视图。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:27:03 +08:00
Your NameandClaude Opus 5 d01547d3de 重新生成 base.min.css(自9月3日起已落后11次真实CSS提交,生产环境从未见过)
生产环境的 index.html 引用的是 base.min.css,本地/测试用的 index-dev.html 引用的
是未压缩的 base.css——这次全需求复测(含第6-11章一系列真实bug修复+今天新增的
实名提醒弹窗/群主转让确认等样式)改的都是 base.css,没有一次顺手跑
`npm run build:css` 重新生成压缩版。也就是说,这段时间验证过的每一处CSS修复,
如果现在直接照旧部署 index.html 到生产,用户会一处都看不到。

跑 `npx postcss css/base.css > css/base.min.css` 补上这次遗漏,往前追平到
c4189646 之后累计的 11 次 base.css 提交。这个文件本身是纯构建产物(postcss确定性
输出,源头 base.css 早就在开发环境反复验证过),不涉及任何新的手写代码,但下次
真正部署生产环境前必须包含这次重新生成的版本,否则等于白修。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:21:33 +08:00
Your NameandClaude Opus 5 7728afe79f 群主转让加一步密码二次确认,跟账号注销/改密码等其它高风险操作保持一致(第6章复测发现)
serveIMGroupOwnerTransfer 核实过:只认 {target_user_id, version},没有任何
action_token/二次验证机制——不是文档滞后,是这个操作本身就没有服务端强制的二次
确认。但转让群主同样不可逆、后果同样重大("确定后你将立即失去群主身份"),本项目
对同一类场景已有明确先例:personal-settings-view.jsx 的 DeleteAccountConfirm 就是
纯前端加的密码输入门槛,不指望服务端校验它,只是让"手滑点确定"变成需要多打一遍
密码才会发生的事。

OwnerSelect 原来点目标人选后只有一层"确定/取消"的确认表,点确定直接调用真实转让
接口。现在拆成两步:第一步维持原有确认文案不变,第二步新增密码输入,留空点确定
会被拦下(不发请求),填了任意内容才真正调用 transferGroupOwner。

真实验证(真实群+真实候选人):空密码点确定确认没有发出请求;填入密码后
transferGroupOwner 真实调用成功,正确跳转回群组管理页。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:04:26 +08:00
Your NameandClaude Opus 5 542aca3dc2 接线1.10实名认证提醒弹窗到真实消息首页(第1章遗留项,此前一直是孤立demo路由)
auth-onboarding-view.jsx 的 RealnameReminder 组件此前是完全孤立的demo:自己画一套
假会话列表背景,只能靠手动改hash访问,没有任何真实入口会导航过去。核对参考图
"实名认证(安卓).png"确认过的弹窗文案本身是对的,只是从来没有接上真实触发条件。

抽出弹窗内容(不带假背景)做成 RealnameReminderModal,挂到真实 MessageHome 挂载时
按 bootstrap.security.realname 的真实状态判断——这个字段本来就在 MessageHome 已经
在用的同一个 bootstrap 请求里,不需要额外接口调用。

参考图本身画的是不可跳过的单按钮强制态,但真实核对 im_be 源码发现这只是
required:true 那个分支的样子;这个环境的真实配置(也是目前后端唯一会返回的默认值)
是 required:false / allow_skip:true。如果照抄参考图做成完全关不掉的弹层,等于把
"实名认证目前是可选的"这个真实产品配置,实现成挡住所有人使用消息功能的强制墙——
按这两个字段分叉:允许跳过时加一个参考图没画、但后端字段明确允许的关闭按钮;且
遵守后端给的 remind_after(当前值24h),关闭后在这个时长内不重复弹,不是每次进
消息首页都弹一次。

联调中发现关闭按钮初版样式是个跟全局 button 选一样宽的圆角方块(漏了覆盖
min-width/box-shadow/margin 这几个全局 button 规则),已修正为纯图标按钮。

真实验证(账号 asn494180,真实 status:"unverified"):弹窗内容与参考图逐句一致;
点击关闭按钮弹窗消失且 localStorage 正确记录时间戳;刷新页面重新连接后,24小时
窗口内弹窗不会重复出现;未清除记录时保持不弹,符合"提醒而非骚扰"的预期。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:40:27 +08:00
Your NameandClaude Opus 5 d21d046443 修复精准查找对拉黑关系显示"添加",跟实际能不能添加对不上(第5章复测发现)
/users/search 返回结果本身带 relationship 字段,服务端 imRelationshipAction() 对
blocked_by_me/blocked_by_peer 都明确映射成 action:"none"(不允许发起添加)——但
前端 actionLabel 只认 friend/pending_outbound/pending_inbound 三种,其余(含两种
拉黑关系)一律落到兜底的"添加"文案。点进去虽然会跳到资料页、由资料页自己的
cannotAddReason 挡住(不会真的误发出申请),但列表这一行本身的文案是误导的。
补上跟 Profile 组件里已有的 cannotAddReason 一致的两条文案(已拉黑/无法添加),
只是精简成适合一行列表项的长度。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:25:44 +08:00
Your NameandClaude Opus 5 7340b5e135 修复消息首页"置顶聊天"读了个不存在的字段,从来没有视觉效果(第2章复测发现)
normalizeConversations 里 pinned 读的是 item.pinned——原生 Tinode 订阅对象上根本
没有这个键,恒为 undefined。第5/6章的置顶开关点了之后,服务端确实存了,但这个
列表拿到的永远是"没置顶",用户会觉得这个开关是假的。

真实存放位置是订阅的原生 private.pinned 字段,但只对群聊成立:群聊置顶走
updateGroupMemberSettings,服务端真的写进 Subs.Private,会随原生 {meta} 同步下来
(真实验证:重连后能看到;同一连接内没有实时 {pres} 通知,所以点亮开关那一刻不会
立即反映在列表上,要等下一次重连,这个次要延迟这次没有顺手解决)。单聊置顶走的
是完全不同的 updateConversationSettings,服务端存进独立的 im_conversation_settings
表,从不写入原生 Subs.Private——不管等多久重连都不会在这里出现,这是真实的后端
架构缺口(没有批量查询会话设置的接口,前端不可能为渲染一屏列表去挨个 topic 发
请求核对置顶状态),已记录、这次没法在前端解决。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:25:23 +08:00
Your NameandClaude Opus 5 4337c45674 修复第9章工作台三处问题:日报列表错误吞没/月报重新编辑未接线/信箱附件超限(复测发现)
1. DailyReportList 的 load() 把请求失败(网络错误、403等)整个 catch 吞掉,只留一个
   空/旧列表,跟"今天真的没有日报"是同一种"暂无日报"空态展示——用户没法分辨到底是
   没数据还是加载失败了。补上 loadError + onToast,参照 DailyReportDetail/
   MonthlyReportList 已有的同款处理方式。

2. reopenMonthlyReport 早就在 im-api.js 里写好但零调用点:MonthlyReportDetail 一直
   没有"重新编辑"入口,已提交的月报只能干看着改不了。核对 im_be 源码发现原函数还多
   传了一个 reason 字段——serveIMMonthlyReportReopen 用 DisallowUnknownFields 只认
   {version},真调用起来会先 400。同时确认这是纯自助操作(SQL 的 UPDATE 条件里
   author_id=调用者自己),不存在"管理员帮别人退回"这回事,去掉多余字段后在
   MonthlyReportDetail 给本人的已提交月报加上这个入口。

   接上之后在真实账号上跑通整条链路时,又炸出一个新竞态:reopen 成功后跳转编辑器,
   编辑器用同一个月份的模块级缓存做"再次进入不清空"的初始渲染,但缓存里存的是
   reopen 之前的旧 version——首次自动保存草稿用这个旧 version 发出去,服务端真实
   返回 409,界面弹出"请检查网络后重新编辑"(其实跟网络毫不相干)。加了个 hydrated
   门槛:只有当组件自己这次挂载真正拿到新鲜数据确认过之后,才允许自动保存的
   effect 生效;没有旧缓存要甄别的场景(绝大多数情况)行为完全不变。修复前后各跑
   了一次完整的"提交→查看详情→重新编辑",前者复现了 409,后者干净无报错。

3. 总经理信箱的附件上传一直没有数量上限——真实 config.attachment_policy.max_files
   (联调确认默认租户是5,不是后端Go默认结构体里那个从未在这个环境出现过的
   max_count)从来没被读取过,界面上能一直点"上传文件"加到任意多个。补上跟
   意见反馈页一致的"数量/上限"展示与到达上限后禁用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:25:05 +08:00
Your NameandClaude Opus 5 3a78c40389 修复创建群聊名称长度上限与编辑页不一致的问题(第6章复测发现)
创建群聊弹窗的名称输入框 maxLength 是30,但群资料页的编辑器(GroupNameEditor)
是11——两者编辑的是同一个 group.name 字段,建群时能起最多30字的名字,进编辑页
却连原样保存都做不到(打不满30个字符,且无法回退到超过11字的原值)。

核对参考图 设置昵称(1).png:这个字段的编辑器明确画的是"0/11"上限,没有任何一张
图支持30。后端本身不做长度限制(实测 PATCH 17 字符的名称成功),11 纯粹是前端自
己的约束,应该是创建页和编辑页保持一致,而不是让创建页放宽成一个没有依据的数字。

改为创建弹窗也用 maxLength="11",跟编辑页对齐。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:46:25 +08:00
Your NameandClaude Opus 5 211d93a0e4 修复青少年模式独立密码校验报错显示英文原文的问题(第10-11章复测发现)
关闭/开启青少年模式都要先过 POST /settings/independent-password/verify 这一关,
PIN 输错、连续错误触发限流这两种失败,后端 reason 分别是 CODE_INVALID/
TOO_MANY_ATTEMPTS——REASON_MESSAGES 表里没收录这两个 key,直接透传英文原文
message("code invalid"/"too many attempts"),且限流的具体等待时长(details.
retry_after_seconds)完全没有展示给用户。

REASON_MESSAGES 原来只支持字符串值,限流提示需要按 retry_after_seconds 动态拼
文案,所以顺带把 imRequest 的取值逻辑改成支持函数值(传入 payload.details)。

已用真实账号端到端验证:对 https://api.118im.com 连续发送错误 PIN,前3次真实
返回 401 CODE_INVALID,UI 正确显示"独立密码不正确";第4次起后端真实触发限流
返回 429 TOO_MANY_ATTEMPTS(details.retry_after_seconds 从300开始递减),UI
正确显示"连续输错次数过多,请 240 秒后重试",秒数随后端返回值动态变化。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:46:08 +08:00
Your NameandClaude Sonnet 5 997f8f71d2 修复群成员非好友主页文案错误、添加好友按钮永不出现(第5章复测发现)
navigateToProfile 原来只从群成员列表数据({acs, display_name, group_nickname?, mentionable,
role, user_id})里摘字段做种子缓存,这份数据里根本没有 capabilities/relationship——Profile
组件正是靠这两个字段判断"要不要显示添加好友按钮"和"该显示哪句失败文案"。缺了它们,非好友的
群成员主页会落到 cannotAddReason 的默认分支:不管真实关系是什么,一律显示"好友申请正在等待
验证",添加好友按钮永远不出现。

Profile 组件自己的 getUserProfile(uid) 对非好友是 403(服务端隐私默认),403 时会保留种子
不报错——这个保留种子的兜底本身是对的,问题是种子从一开始就没给对数据。

改用 GET /groups/{topic}/members/{user_id}/profile(getGroupMemberProfile,早就封装好但
零调用点)——群上下文里权限判断更宽松,实测对方哪怕不是好友也能拿到
{capabilities, relationship}。navigateToProfile 现在异步补种:跳转后台发起这个请求,回来后
用真实数据覆盖种子并派发一个事件让已经挂载的 Profile 重新读取。

已用真实账号验证:点击群成员头像后,getGroupMemberProfile 真实请求 200,返回
relationship:"friend"、capabilities.can_add_friend:false;Profile 组件的 React state 和
最终渲染的 DOM 都正确更新成"发消息/音视频通话"(通过 React Fiber 直接读取 state 确认,不只是
看界面文字)。当前两个测试账号互为好友,没能用真正的陌生人群成员验证"添加好友"按钮出现的
那条分支,但接口返回结构已经证实包含判断所需的全部字段,逻辑上会正确生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:56:26 +08:00
Your NameandClaude Sonnet 5 0dcef54fed 修复置顶/免打扰开关未接后端、好友二维码死按钮(第5章复测发现)
置顶聊天/消息免打扰:原来是纯本地 useState,onChange 只切一下布尔值,从不调用任何接口——
GET/PATCH /conversations/{topic}/settings 早就封装好了,只是零调用点。打开设置页永远显示
"关",点了也只是那次会话内生效、离开页面就丢,和会话列表真正读取的置顶/免打扰状态是两套
毫不相关的东西。

实测确认真实字段:{pinned, mute_notifications, version},PATCH 需要带 version 做乐观锁。
接入 getConversationSettings/updateConversationSettings,失败时回滚开关视觉状态。
已用真实账号验证:点击后发出真实 PATCH 请求,带正确的 version。

好友个人主页"二维码"图标:参考图里是独立一行(不是贴在头像卡片里的小图标),点击生成这个
好友的二维码名片,对应接口 createUserQRCode() 早就封装好了,原来这个图标没有任何 onClick,
是个看着能点、点了没反应的死装饰。新增 friend-qr 路由和 FriendQr 组件(复用 QrPage 的结构),
CSS 复用了 .people-settings 那套现成的行样式,不是新造一套。
已用真实账号验证:点击后正确跳转、生成出真实二维码。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:43:22 +08:00
Your NameandClaude Sonnet 5 d3d7e11b08 修复会议详情页显示裸user_id的问题(第9章复测发现)
创建/详情/更新/取消/我的回应这5个会议接口的 organizer.display_name/participants[].display_name
实测恒等于裸 user_id,只有列表接口 GET /meetings 会真的解析姓名。之前的注释误以为"响应已带
姓名解析"对所有会议接口都成立(可能是当时只测过列表接口就下了结论),这次复测用真实创建的
会议逐个接口验证,证伪了这个假设。

MeetingDetail 接入已有的 useUserNames(日报/月报详情页已经在用同一套机制),发起人和参会人
姓名解析不出来时不再显示裸 uid。同时更正了 MeetingCard 上那条误导性注释,说清楚它只对列表
接口成立。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:31:55 +08:00
Your NameandClaude Sonnet 5 1d51e53a46 修复通话发起失败静默无反应的问题(第8章复测发现)
sendMessageToTopic 自己的 .catch 会吞掉发送失败——publishMessage 在服务端拒绝时只标记
pub._failed=true 让消息列表画红色感叹号(这是给聊天气泡设计的反馈方式),resolve 成 undefined
而不是 reject。通话邀请这条消息不会被用户看到(用户盯着的是全屏通话面板,不是消息列表),走
这条路径等于什么反馈都没有;而且原代码 `ctrl.code` 直接解引用,undefined.code 会抛
TypeError,变成一个没人 catch 的 rejected promise——handleCallClose() 根本不会被调用,
通话面板会卡在"呼叫中"状态出不来。

这条路径今天有了真实后果:后端新增的全局通话开关(im_client_feature)关闭时,服务端会拒绝
这条 {pub},正好命中这个从未被验证过的失败分支。

修复:判断 ctrl 是否存在、补 .catch,两处都调用 handleError 提示 + handleCallClose 复位。
顺带给通话按钮可见性加了一层预判——读共享的 bootstrap 缓存,features.audio_call/video_call
为 false 时不显示按钮;读不到缓存时按原行为放行(fail-open,只是预判不是安全边界)。

已用真实账号验证:通话菜单项在开关为真时正常显示,无回归。真实失败场景(开关被关闭)无法在
当前浏览器沙盒环境下端到端验证(摄像头/麦克风权限被拦截,此前已确认的环境限制),修复依据是
逐层读 tinode-sdk 源码精确追踪出的调用链,不是猜测。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:14:44 +08:00
Your NameandClaude Sonnet 5 8e703d86eb 修复第2章复测发现的3个问题:好友申请通知按钮/时间格式/新机器码样本
- 好友申请通知的"查看新朋友"按钮永远不出现:判断条件是 action.type==='friend_request',
  但实测 GET /system-notifications 拿到的 friend.request.created 通知,action 里只有
  {user_id},从来没有 type 字段——这个判断条件是当初凭对接文档猜的,不是照真实响应写的。
  改用 event_type.startsWith('friend.request')。
- 系统消息时间格式:默认 toLocaleString('zh-CN') 输出"2026/8/20 18:04:00"(年份/斜杠/秒),
  参考图是"6月28日 10:04"。新增 formatNotificationTime。
- 机器码兜底表补一条:新样本 security.risk.continue 事件的 summary 是 decision 字面量
  "continue",同样会被机器码识别但查表落空、原样显示英文单词。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:06:00 +08:00
Your NameandClaude Sonnet 5 02a565b8e5 修复登录失败显示英文原文的问题(第1章复测发现)
Tinode 原生 WS 登录走 {ctrl} 协议,不经过 im-api.js 的 REASON_MESSAGES 翻译表(那张表只在
REST 请求里生效)。ctrl.text 是服务端 datamodel.go 里固定的英文串("authentication failed"
等),ctrl.params 也没有 REST 层那种稳定的 reason 字段(已实测确认)。原来 doLogin 的 catch
直接把 err.message 原样传给 handleError,密码打错时用户看到的是英文 "authentication failed",
不是参考图要求的"账号或密码错误"。

新增 loginErrorMessage():已知的几个固定英文串映射成中文,未知文案按状态码兜底
(401/403 → 账号或密码错误)。

已用真实账号验证:错误密码显示"账号或密码错误",正确密码登录正常进入工作台。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 12:21:44 +08:00
Your NameandClaude Opus 5 5bc3fee20b 实现合并转发(第4章 4.20):后端已支持 bundle 模式,补上前后两端
后端把 POST /v1/im/messages/forward 的 mode="bundle" 实现了(此前直接返回 501,前端因此
只做了逐条转发)。这次把前端两侧都补上。

接收侧(新增 widgets/forward-bundle.jsx)
- 服务端生成的这条消息 head 带 x-im-type:"forward_bundle",content 是
  {bundle_id, title, item_count, preview, items, source_refs, comment},不是 Drafty。
  前端全仓搜这个类型零命中,收到会落到 chat-message 的 "invalid content" 兜底
  (⚠ + 灰字)。本端自己发不出 bundle,但别的端发过来就会踩到。
- 渲染成一张"聊天记录"卡片:标题 + 摘要行 + 展开/收起。items[] 里已经带了每条被转发消息的
  完整快照,所以"查看完整记录"是就地展开,不额外发请求。
- 快照里的 from 是不带 usr 前缀的裸 uid(与收藏快照一致),补前缀后查本地会话缓存拿显示名;
  查不到就不显示发送者,宁可少一行也不要把一串 uid 摆在摘要前面。
- 没有正文的媒体消息用 [图片]/[视频]/[语音]/[文件]/[链接] 标签兜底。

发送侧(batch-forward-dialog.jsx)
- 转发模式改为按"用哪种方式选的目标"推导,这是参考图给的规则:
  转发—逐条转发.png 是单选一个目标、占位写 [逐条转发]共2条消息;
  转发—合并转发—多选状态.png 是多选目标、占位写 [合并转发]共16条消息。
  原来 mode 硬编码 'separate'、占位也写死 [逐条转发]。

样式(base.css)
- 新增 .forward-bundle 一组规则,避免又造一批零样式类。

验证
- 用后端 buildForwardBundle 的真实产出结构,注入真实会话的消息缓存,由真正的 ChatMessage
  渲染:卡片正常显示、未落到 invalid content;展开显示全部 5 条、收起回到 3 条;截图确认视觉正常。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:15:33 +08:00
Your NameandClaude Opus 5 c709abbfa4 修复全页面走查发现的问题(第六批:交互反馈时序)
- contact-management-view: "分享给好友"走剪贴板分支时不给任何反馈,点了界面上毫无动静
  (同页 QrPage.share 是有 toast 的);生成二维码失败也被 .catch(()=>{}) 静默吞掉。
  三条路径都补上反馈。
- group-management-view: 添加管理员/禁言成功后 900ms 就导航,而 CenterToast 生命周期是
  1400ms —— 导航会让整棵子树重挂载把 toast 一起销毁,参考图里那个「设置成功」实际只闪一下
  就被截断。改成 1500ms。

回归验证(本轮全部改动)
- 41 个路由逐个访问:零 JS 错误、零 unhandled rejection、零空白页。
- 22 个页面布局扫描:无横向溢出、无文字零高度、无图标未居中。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:02:15 +08:00
Your NameandClaude Opus 5 08bd8915c5 修复全页面走查发现的问题(第五批:失效文件状态、日报搜索、client_context)
- content-library-view: 收藏列表的 FileCard 一直没传 removed,需求 7.3「失效文件需展示明确
  状态而非直接报错」在收藏页就没生效(聊天记录页本来就传了,只有这条漏了)。后端
  messageAvailability 在源消息被删/撤回时给 'removed'。

- workplace-collaboration-view: 日报"团队统计"的搜索占位符写着"搜索姓名/部门",但过滤用的
  department_name 后端根本不返回(dailyReportListPayload 只有 report_id/author_id/report_date/
  content_short/status/draft_updated_at/read_state/version),拼进来恒为空串,"部门"那半个
  搜索永远搜不到任何东西。改为按真实有的字段搜(姓名+摘要),占位符也改成如实的;
  卡片上同样读了这个不存在字段的角色徽章一并改掉。

- im-api: 企业注册申请 / 入口反馈 / 意见反馈三个接口后端都定义了 client_context 且会入库
  (im_http.go:255/263/367,落库点 :790/:840/:1478),需求与对接文档也都要求带设备版本信息,
  前端一直没发。在 im-api 层统一注入,三处调用点不用各写一遍。navigator.connection 不是所有
  浏览器都有,拿不到就不发 network 这一项。
  已用真实提交验证(返回 fb_G9GtMsAbSHQ6z6Li)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:59:09 +08:00
Your NameandClaude Opus 5 35d3e1cf6b 修复全页面走查发现的问题(第四批:精准查找、隐私锁定、搜索防抖)
精准查找搜不到手机号(第5章 5.5)
- 一直写死 type=exact_account,而该分支只比对 profile.public_account / u.username / u.id
  (tenant_adapter.go:9170-9172),**不匹配 u.phone**。参考图 5.5 里输的恰恰是手机号,
  这个场景必然搜不到。改为按输入形态分流:11 位 1 开头走 type=phone。
- phone 分支服务端不做 E.164 归一化(只有模糊的 account 分支才调 normalizeUserPhone),
  库里存的是 +86 开头,所以裸号码由前端补区号——注册流程本来也是这么拼的。
- 已用真实后端验证请求正确路由为 q=%2B8613800001188&type=phone。

隐私设置忽略 editable(第10章 10.12/10.13)
- 后端 userPrivacyPayload 返回 editable{add_me_methods, profile_visibility,
  last_seen_visibility, read_receipts} 表达"企业强制策略锁死了这一项",前端完全没读。
  被锁的开关照样能点,点了 400 再回滚,用户只看到开关自己弹回去。现在禁用并给出说明文案,
  同时补 .personal-switch:disabled 的视觉反馈(否则看不出为什么点不动)。

搜索防抖
- 新增共享的 useDebouncedValue,用在三处真正发服务端请求的搜索:收藏、会话内检索、
  群列表、群文件。原来 query 直接进 effect 依赖,每敲一个字符发一次请求;群列表更糟,
  reload 对三个分类各发一次,一个字符就是 3 个请求。而且返回顺序没有保证,后发的短前缀
  请求可能后到、把正确结果覆盖掉。
- 实测:群列表输入 4 个字,请求数从 12 降到 3。
- FavoriteShare 那处搜索是本地过滤 chatList、不发请求,没有动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:56:41 +08:00
Your NameandClaude Opus 5 c9b8125013 修复全页面走查发现的问题(第三批:转发、i18n、自聊天显示名)
第4章 批量转发
- 确认弹层里那栏留言是个只读 <div>,需求 4.19 与对接文档要求的"附加留言"根本没法填,
  comment 永远是空串。改成真实输入框(方括号那段是占位文案不是内容),并补 input 的样式。
- 无条数上限:服务端硬卡 sources ≤ 100(im_http.go:3560),超了直接 400 MALFORMED_REQUEST,
  用户只看到一句无从下手的"转发失败"。本地先拦并说清超了多少。
- 无目标数上限:bootstrap 下发 limits.max_forward_targets=9、服务端硬限 20,前端两个都不读。
  读各视图已在写的同一份 bootstrap 缓存,不额外发请求;选择视图也补上错误显示。

i18n(这几条影响真实页面,不只是演示页)
- menu_item_forward "重定向" → "转发",forward_to "重定向消息" → "转发给"。Forward 被当网络
  术语直译了,第4章通篇和参考图用的都是"转发"。
- last_seen_timestamp 中英两侧都丢了 {timestamp} 占位符,离线时头部只显示"最后可见"没有时间。
- peers_messaging_disabled "成员间消息已禁用" → "对方已关闭消息"(原文是 Peer's)。
- search_no_results "搜索返回任何结果" 缺"无";icon_title_send "发信息" → "发送消息"。
- 已跑 build:i18n 同步 src/i18n.min/(那份才是打包进产物的)。

自聊天显示名
- topicDisplayName 不处理 slf,消息首页把"已保存的消息"显示成"未命名会话"。该显示名靠
  Tinode.isSelfTopicName 特判得出、不在 public 里。集中到这个共享函数,所有调用点都受益。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:45:13 +08:00
Your NameandClaude Opus 5 02b98cceae 修复全页面走查发现的问题(第二批:登录、通讯录、群聊)
第1章
- tinode-web: 手输密码登录失败会 navigateTo('') 把用户弹回企业码页,刚输的账号丢了,而
  TenantEntry 不渲染 FormAlert,那句错误提示根本看不到。改成只有存储的 token 失效才回登录入口。
- auth-onboarding IdentityCheck: 限流 429 时服务端会连同未过期的 challenge_id 一起返回,
  原来只显示错误文案、challengeId 留空,于是 submit 开头的守卫静默吞掉点击——输完验证码点
  "确定"毫无反应也无提示。同文件 VerificationButton 早就处理对了,这里是漏抄。
- auth-onboarding Register: 手机号注册路径硬传 invite_code:'',把用户填的邀请码丢了。后端
  user.go:175-181 对邀请码的校验和 RecordUse 不区分注册路径,UI 上也摆着这个输入框。

第5章
- 好友列表主副行显示同一串字:后端在有备注时把 display_name 也设成备注(im_http.go:8500-8507),
  两个字段同值。加"不同才显示"的守卫。
- 拉黑确认弹窗把设置页当背景渲染并摆出"操作之后"的样子,但背景组件自己的 getUserProfile
  回来后又把开关改回未拉黑、"删除好友"红条重新冒出来。加 pinBlocked 挡住这次覆盖。

第6章
- 群列表把群名显示成 topic id、成员数恒为 0。根因在后端:handler 用 store.Users.GetSubs,而
  store 自己的注释就写着它 "Does not load Public/Trusted or Private"(store.go:834-835),
  groupListItemPayload 却去读 sub.GetPublic()/GetSubCnt()(应该用 GetTopics)。已记为后端问题;
  前端先用本地 Tinode 会话缓存兜底(消息首页一直这么取,是准的)。
- 禁言页每行头像画成群组图标(Avatar 兜底图标默认 'group',没传 icon="person"),
  且顺带从成员表带上真实头像。
- 禁言页"以上成员不允许发言"被当成空状态用,语义正好反了——有禁言成员时这句消失、
  一个都没有时反而显示。改为列表下方常驻注脚,空态另给文案,并补 .group-inline-note 样式。
- bytesToGB 只输出到 MB,1.4GB 的文件显示成 "1433.6 MB"。
- 群文件"来自"直接显示裸 uploader_id(groupFilePayload 不给昵称),拉成员表交叉引用。
- 群公告从聊天页横幅进入后返回,会把会话一起关掉:navigate 只写 query 参数,path[1] 仍为空,
  handleHashRoute 就把 topicSelected 清成 null。Header 支持 onBack,显式把 topic 写回 path[1]。
- 群成员点头像打不开非好友资料页:目标页先读 'profile-seed:' 缓存,因为 getUserProfile 对
  非好友直接 403。第5章入口一直有 seed,这里漏了。
- 群简介上限 225 超过后端硬限 200(im_http.go:4667),写满必被打回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:40:53 +08:00
Your NameandClaude Opus 5 5a07bb4e3b 修复全页面走查发现的 P0 前端问题(第一批)
八个 agent 对 11 章逐页对照设计稿+后端源码后,先修其中"功能不可用/崩溃/谎报成功"级别的:

崩溃与数据错误
- content-library-view: Highlight 按 [start,len] 数组解构后端返回的 {start,end} 对象,
  对象不可迭代会直接抛 TypeError,任何文本搜索命中都让整个结果区渲染崩掉。服务端偏移还叠加
  两个问题(Go 字节下标 vs JS UTF-16;偏移基准是全文但 snippet 会被开窗截断),改为在展示
  文本上本地匹配关键词。
- content-library-view: firstAttachment 用 Drafty.attachments,SDK 里硬过滤 tp=='EX'
  只认文件附件,图片(IM)/视频(VD)/语音(AU) 一个都取不到——收藏缩略图、语音播放源、聊天记录
  图片视频宫格全部拿不到资源。改用 Drafty.entities 按类型分派(chat-message.jsx 已有先例)。
- message-center-view: 系统通知的机器码兜底会把真实数据吞掉(日报提醒的 summary 就是日期
  字符串 "2026-08-20",会议通知的 summary 是标题),改为查表命中才替换。
- message-center-view: 会话列表漏了 avatar 字段,每行都只能落到兜底图标;同文件
  frequentContacts() 本来就是对的,照它补上。

功能不可用
- auth-onboarding-view: 首次开启青少年模式是死路——设完密码丢弃了服务端返回的
  enable_action_token,从不调 enableYouthMode,用户走完短信验证+设密码后模式仍是关的。
  现在拿着这个 token 把最后一步做完。
- group-management-view: "投诉/举报"是个 onToast('暂未开放') 死桩,而 target_type='group'
  这条链路早就通了。接上真实举报页。
- tinode-web: 刷新/深链会被弹回首页。两个原因叠加:初始化白名单还是上游 Tinode 的
  ['cred','reset','register'];handleLoginSuccessful 无条件把 hash 改写成 workbench,
  刷新触发的自动登录也走这条。抽出 SIDE_PANEL_VIEWS 常量复用,并区分登录流程与自动登录。
- widgets/meta-message: deleted 分支从未实现,撤回一条消息后时间线上什么都不剩。

谎报成功 / 判定错误
- tinode-web + personal-settings-view: 改密码不返回 Promise,服务端拒绝时照样弹"密码已更新"。
- group-management-view: 禁言批量读 result.succeeded,但该接口返回的是 {topic, items},
  succeeded 恒为 undefined,不管成没成都弹"设置成功"。两个接口结构不同,分开判定。
- workplace-collaboration-view: 会议取消状态写成 'cancelled',后端是 'canceled' 单 l,
  会议取消后"取消会议"按钮还挂着。

样式与显示
- base.css: 删掉旧版 .organization-department 的 padding-left !important,它压过按层级算出的
  内联缩进,导致部门行全平、只有人员行缩进。
- base.css: 补 .youth-password-gate(原来 0 条规则),PIN 框缺 position:relative 和居中。
- security-support-view: shield_lock 不在经典 Material Icons 里(只有 Symbols),会显示成
  字面文本;.risk-info-page 也是 0 条规则,整页渲染成裸项目符号列表。改用 verified_user 并
  补上 .risk-dialog。
- contact-management-view: 好友资料页性别直接输出后端枚举,显示成 "male"。
- workplace-collaboration-view: 层级标注两条路径两种格式(树形深三层变"3级"、搜索硬传"三"),
  统一成一个 helper。

已记录不在前端修的后端缺口:recallMessage 的 version 只能传 1,因为所有消息读取接口
(messagePayload/messageSearchPayload/mediaMessagePayload/favoriteSnapshot)都不下发 version,
冲突错误里也不回传真实值,被编辑过的消息撤回必然失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:31:26 +08:00
Your NameandClaude Opus 5 c4189646f4 消息首页:群聊头像图标、昨天的时间格式、常联系的人名字截断
对照 docs/images/消息.png 逐项核对消息首页,实测 DOM 后确认三处:

1. 群聊会话的兜底头像画成了单人像:normalizeConversations 没给任何会话设 icon,全部落到
   Avatar 的默认值 person,列表里分不出哪条是群。按 topic 的 grp 前缀区分,群用 group 图标。
   顺带把群的 online 传 undefined——群不存在"在线"概念,之前会给群也渲染一个状态点。

2. 昨天的会话只显示"昨天",参考图是"昨天21:03"带具体时间。实测修复后显示"昨天17:40"。

3. "常联系的人"的名字被截断:格子 3.75rem(50px),而 "kcg051307" 这类用户名需要 52px,
   显示成 "kcg0513...";参考图里这一栏名字是完整显示的。加宽到 4.4rem,实测 59px 可见宽、
   59px 实际宽,不再截断。

另外记录一个未改的观感偏差供决策:这一栏的头像实测占视口宽 10.1%(42.7px / 424px),参考图
里约占 17%,明显小一号。放大属于观感调整不是缺陷修复,没有自行改动。

本轮还做了一次全局层叠扫描(遍历 19 个页面,自动检测图标在容器内不居中、文字被压成 0 高度、
元素溢出视口、意外截断四类问题):除上面这处截断外没有新命中,说明之前群设置页那组 CSS 层叠
冲突是孤立的,没有蔓延到其他页面。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:09:00 +08:00
Your NameandClaude Opus 5 0ba3577c96 群头像图标贴顶不居中;成员格子文字区过窄
用户实机反馈的三点,逐项查证后:

1. 图标垂直不居中(真bug,已修):.group-settings-header span 这条规则把 .group-avatar 一起
   命中了——它本身就是个 span,被改成 flex-direction:column 之后,自己的 place-items:center
   只剩交叉轴(水平)生效,主轴(垂直)退回 flex-start,图标贴着方块顶部。实测头部群头像上边距
   0px、下边距 28.7px,左右各 14.3px,正好对应这个成因。选择器改为
   > span:not(.group-avatar) 精确排除;同时给 .group-avatar 补 align-content/justify-content,
   以后再被外部规则改成 flex 也不会散架。修完三处(头部群头像、成员头像、添加格)实测上下左右
   边距全部对称。

2. 文字区过窄只显示两个字(真问题,已修):上一版把成员格子宽度收到与头像等宽(3rem),昵称
   被截成"测试..."。按参考图比例重算——那张图里头像约占格子宽七成、相邻头像间隙约头像宽四成,
   且五字昵称完整显示——格子改 4.4rem、头像改 3.6rem、间距 0.5rem。实测"测试昵称A"完整显示
   不再截断。

3. 头像配色:上一版我把兜底头像从饱和蓝底白图标改成了浅蓝底蓝图标,这是自作主张、用户没要求,
   已还原成原来的 #4389df 底 + 白色图标。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 17:54:22 +08:00
Your NameandClaude Opus 5 f7bc98811e 群聊设置:重做成员预览区,修掉错位间距、多余横线和被压扁的名字
用户实机截图反馈这块"图标和间距有问题"。对照 docs/images/群聊设置.png 逐项查证,实际有四个独立
缺陷叠在一起:

1. 多余横线:成员容器 div 和邀请按钮都是 section 的直接子元素,命中了通用列表项样式
   (.group-settings section > button/div 带 border-bottom + min-height + padding),
   于是这一块被当成两个普通设置行渲染,凭空多出两截下划线。
2. 间距错位:原来是 flex 布局,成员列表 flex:1 占满剩余宽度,把邀请按钮一路挤到最右,两者之间
   出现一大片空白;参考图是等宽网格。
3. 名字看不见:全局 button 规则(base.css:1999)写死 height/line-height 2.2rem,成员格子没有
   覆盖它,整个格子被压到 29px 高——48px 的头像直接溢出,名字被压成 0 高度,实测
   getBoundingClientRect().height 为 0。
4. 图标撞车:成员头像的兜底图标沿用了 Avatar 的默认值 group(双人图标),跟上方群头像长得一模
   一样,分不清哪个是群哪个是人;改传 icon="person"。

改成参考图的结构:一行"群成员"标题(右侧显示人数)+ 5 列等宽网格,最后一格是与头像同尺寸、
同圆角的浅色"添加"块。新样式用 .group-settings section > .group-member-* 提高特异性,明确压过
那套列表项规则,并显式写 height:auto 覆盖全局 button 高度。

刻意没做的两点,都是因为本分支没有对应页面,不想做点了没反应或跳错地方的入口:参考图里"查看
15465群成员"那个可点链接(没有通用群成员列表页,这里只展示人数文字),以及"删除"格(没有移除
成员的页面)。这两个缺口需要单独补页面,不在这次范围内。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 17:43:22 +08:00
Your NameandClaude Opus 5 8225848493 系统消息:按通知自身内容决定入口和文案,不再一律显示"前往安全设置"
用户实机测试反馈:系统消息列表里每条通知底部都是同一个"前往安全设置"按钮,点进去跟通知内容毫不
相关;正文还直接显示 "group.settings.updated" 这种原始事件码。查证后两个问题性质不同:

入口按钮(前端问题,已修):原来无条件对所有通知渲染同一个按钮、固定跳 security 页。改为按通知
自己的 action 字段决定——action.topic 是 grp 开头的进对应群聊设置页,好友申请类(action.type
为 friend_request)进"新朋友"页,带 device/ip 的安全类才进安全设置页,都不匹配就不显示按钮。
注意 action.topic 不能直接当群用:好友申请通知的 action 里带的是 p2p 会话 topic
(db/mysql/tenant_adapter.go:8643),所以判断加了 grp 前缀检查。跳转目标都核对过在
tinode-web.jsx 的路由白名单里(friend-requests / group-settings),没有凭空造路由名。

正文文案(后端问题,前端只做兜底):后端多处把机器码直接当 summary 写进通知——群事件统一写
summary=event.EventType(im_group_events.go:42-44,7 种群事件还共用同一个 title"群聊动态"),
好友申请处理结果写 summary=status(im_http.go:5956 等,值就是 "accepted"/"rejected")。前端加
了一层兜底:summary 命中"纯小写字母数字点下划线"这种机器码特征时,按已知码翻译成中文,翻译不了
就退回通用文案;后端以后补上真正的文案会自动优先使用,不会被兜底覆盖。这条已记入后端问题清单。

实机验证:两条群通知正文由 "group.settings.updated" 变为"群设置有更新"、按钮变为"查看群聊"且
点击后正确进入 grpXDkK5cbXoK0 的群聊设置页;两条好友通知正文由 "accepted" 变为"对方已通过你的
好友申请",不再显示无关的安全设置按钮。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 17:32:36 +08:00
Your NameandClaude Opus 5 3bdb0bf40f 群昵称改用专门的接口读取,修好读不回已保存值的问题
上一个commit修好了"群资料"页把群名称当成个人群昵称显示的问题,但实机验证发现改完仍然显示"未设置"。
抓包确认根因:GET /groups/{topic}/members 列表接口的每一项里压根没有 group_nickname 字段(只有
acs/display_name/mentionable/role/user_id),所以任何从成员列表里读昵称的写法,不管代码逻辑对不对,
都永远拿不到值——这个文件里原有的 MyNicknameEditor 也是同样的写法,同样存在这个问题(打开编辑器时
不会回填已保存的昵称)。

真正能读到昵称的是保存用的那个路径上对应的 GET:GET /groups/{topic}/members/me/profile,返回
{nickname, version, updated_at}(server/im_http.go:4776-4782)。im-api.js 里之前只封装了 PUT,
补上了对应的 getMyGroupProfile,两处读取(群资料页预览行、昵称编辑器回填)都改成用它。

实机验证:保存"测试昵称A"后重新打开群资料页,"群昵称"正确显示"测试昵称A","群名称"独立显示
"扫码测试群"。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 17:20:36 +08:00
Your NameandClaude Sonnet 5 e4c6e1b246 Fix group-profile nickname preview showing group name instead of my own nickname
GroupProfile's "群昵称" preview row showed group.name -- the group's own display name -- instead
of the current user's personal nickname within that group (a real, distinct concept, correctly
handled by the existing MyNicknameEditor/group-my-nickname page, just not wired into this preview
row). Ported the substance of a bug dev independently found and fixed in their own reimplementation
of this same page ("show saved group nickname", found while evaluating what from dev's post-fork
commits was worth bringing over). Reuses the same getGroupMembers+filter-by-myUserId approach
MyNicknameEditor already uses in this file (there's no working "me" shortcut on the
member-profile-by-id endpoint that would let this be fetched more directly -- confirmed by reading
server/im_http.go's routing, which never special-cases that literal string).

Also renamed the adjacent "群昵称"->"群名称" label for the group's own display name field, since
having two rows both labeled "群昵称" (one for my personal nickname, one for the group's name) is
exactly the confusion that caused this bug in the first place.

Evaluated the rest of dev's post-fork commits (9 commits fixing this same page's nickname-save
reliability, plus the wholesale chapter-view reimplementation) for anything else worth porting:
the version-conflict retry logic they built doesn't apply here since this branch already sends
version:0 (confirmed server/im_http.go:4793 treats <=0 as "skip the check" -- the exact mechanism
their fix was working around a different way), and their nickname maxLength bump (11->20) is
already exceeded by this branch's existing 32, which matches the real backend limit
(server/im_http.go:4789). Nothing else from dev's branch was found applicable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:08:38 +08:00
Your NameandClaude Sonnet 5 2c77a8e229 Ignore local .claude/ dev-server config
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:01:13 +08:00
Your NameandClaude Sonnet 5 251c9de87c Convert im_webapp from static mock prototypes to real backend integration
Implements all 11 product chapters (auth, messaging, contacts, groups,
files/favorites, calls, workplace/reports, settings, security) against the
real /v1/im REST API and native Tinode WS protocol, replacing the hardcoded
mock data the ba8f48ee prototype commit shipped with. Adds src/lib/im-api.js
as the REST client, plus supporting fixes across messages-view.jsx and the
attachment/upload/download pipeline (wrong SDK auth headers, group topic
naming, seq/timezone edge cases) found via live end-to-end testing against
the real backend.

Backup checkpoint before reconciling with origin/dev's parallel rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:40:53 +08:00
Your Name ca5e74ee98 修复静态资源发布后不生效: /umd/缓存策略从immutable改为no-cache
构建产物文件名不带内容哈希,一年免验证的缓存策略导致每次蓝绿发布后
浏览器和CDN都会继续吃旧版本,用户需要清缓存或开无痕窗口才能看到更新。
改为no-cache后浏览器/CDN每次都会带条件请求向源站确认,未变化时只是
一次很快的304,一旦文件真的变了会立刻拿到新内容,不再需要手动清缓存。
2026-08-12 12:54:41 +08:00
Your Name 111d9040a7 修复健康检查失败: nginx补上IPv6监听
首次部署时健康检查一直失败——容器内nginx只listen了IPv4(0.0.0.0:80),
但/etc/hosts里localhost解析到::1在前,wget http://localhost/因而连不上。
补上 listen [::]:80 双栈监听解决。部署脚本那边也同步改成直连127.0.0.1,
避免再依赖localhost的解析顺序。
2026-08-11 12:52:06 +08:00
Your Name 85ac8ce43b 接入Gitea Actions蓝绿发布:API_KEY构建时注入、新增Dockerfile
- src/config.js 的 API_KEY 改为从构建时环境变量 TINODE_API_KEY 注入,
  不再依赖官方demo默认值;未设置时保留原demo key作为本地快速测试的兜底
- webpack.config.js 新增 DefinePlugin 完成注入,并自动加载(gitignored的)
  .env.local,开发者本地调试不需要每次手动export
- 新增 Dockerfile(node构建+nginx serve) 和 docker/nginx.conf
- 新增 .gitea/workflows/deploy.yml:手动触发的蓝绿发布流程
2026-08-11 12:40:12 +08:00