第七轮深度测试:修复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>
This commit is contained in:
Your Name
2026-09-05 12:18:25 +08:00
co-authored by Claude Sonnet 5
parent f42b8536e5
commit bede45fd7f
19 changed files with 281 additions and 90 deletions
+104 -41
View File
@@ -3382,6 +3382,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ disableYouthMode: function() { return /* binding */ disableYouthMode; },
/* harmony export */ enableYouthMode: function() { return /* binding */ enableYouthMode; },
/* harmony export */ favoriteMessages: function() { return /* binding */ favoriteMessages; },
/* harmony export */ fetchAllPages: function() { return /* binding */ fetchAllPages; },
/* harmony export */ fetchFileState: function() { return /* binding */ fetchFileState; },
/* harmony export */ forwardFavorites: function() { return /* binding */ forwardFavorites; },
/* harmony export */ forwardMessages: function() { return /* binding */ forwardMessages; },
@@ -3640,6 +3641,20 @@ function translateReason(reason, details) {
const idem = () => ({
'Idempotency-Key': requestId()
});
async function fetchAllPages(fetchPage, startCursor = '') {
let cursor = startCursor;
let items = [];
for (;;) {
const page = await fetchPage(cursor);
items = items.concat(page && page.items || []);
const nextCursor = page && page.next_cursor || '';
if (!nextCursor || nextCursor === cursor) {
break;
}
cursor = nextCursor;
}
return items;
}
const imGet = (path, opts) => imRequest(path, opts);
const imPost = (path, body, opts = {}) => imRequest(path, {
method: 'POST',
@@ -5583,7 +5598,7 @@ const PHONE_RE = /^\d{11}$/;
const INVITE_RE = /^[A-Za-z]{5}$/;
const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const navigate = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].navigateTo(view);
const navigate = (view, params) => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_1__["default"].composeUrlHash([view], params || {}));
function Icon({
children,
className = ''
@@ -6474,8 +6489,10 @@ function PasswordReset() {
}
const IDENTITY_CHECK_SCENE = 'set_independent_password';
function IdentityCheck({
authenticated
authenticated,
hashParams
}) {
const mode = hashParams && hashParams.ym;
const [phoneMask, setPhoneMask] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [challengeId, setChallengeId] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [code, setCode] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
@@ -6520,7 +6537,9 @@ function IdentityCheck({
try {
const data = await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.verifySecurityChallenge)(challengeId, code);
sessionStorage.setItem('im-action-token:' + IDENTITY_CHECK_SCENE, JSON.stringify(data));
navigate('independent-password');
navigate('independent-password', mode ? {
ym: mode
} : undefined);
} catch (err) {
setError(err.message || '验证码错误');
} finally {
@@ -6730,8 +6749,10 @@ function RealnameReminder() {
}, void 0, true);
}
function IndependentPassword({
authenticated
authenticated,
hashParams
}) {
const mode = hashParams && hashParams.ym;
const [pin, setPin] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [error, setError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [toast, setToast] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
@@ -6755,7 +6776,7 @@ function IndependentPassword({
sessionStorage.removeItem('im-action-token:' + IDENTITY_CHECK_SCENE);
setPin('');
const enableToken = result && result.enable_action_token;
if (enableToken) {
if (enableToken && mode === 'enable') {
try {
const current = await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.getYouthMode)();
await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.enableYouthMode)(current.policy_version, enableToken);
@@ -6769,6 +6790,14 @@ function IndependentPassword({
return;
}
}
if (mode === 'disable') {
setError('');
setToast('独立密码已重置,请使用新密码关闭青少年模式');
window.setTimeout(() => navigate('youth-mode-verify', {
ym: 'disable'
}), 800);
return;
}
setError('');
setToast('独立密码设置成功');
} catch (err) {
@@ -6818,6 +6847,7 @@ function IndependentPassword({
}
function AuthOnboardingView({
view,
hashParams,
onTenantResolved,
authenticated = false,
login,
@@ -6862,7 +6892,8 @@ function AuthOnboardingView({
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(PasswordReset, {}, void 0, false);
case 'identity-check':
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(IdentityCheck, {
authenticated: authenticated
authenticated: authenticated,
hashParams: hashParams
}, void 0, false);
case 'realname':
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(Realname, {
@@ -6876,7 +6907,8 @@ function AuthOnboardingView({
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(RealnameReminder, {}, void 0, false);
case 'independent-password':
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(IndependentPassword, {
authenticated: authenticated
authenticated: authenticated,
hashParams: hashParams
}, void 0, false);
default:
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(TenantEntry, {
@@ -7624,14 +7656,13 @@ function AddressBook({
const [bootstrap] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useCachedData)(myUserId ? 'bootstrap:' + myUserId : null, _lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBootstrap, [myUserId]);
const fileAssistantTopic = bootstrap && bootstrap.entries && bootstrap.entries.file_assistant && bootstrap.entries.file_assistant.topic;
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)().then(data => {
const list = data.items || [];
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)('', cursor)).then(list => {
if (friendsCacheKey) {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)(friendsCacheKey, list);
}
setFriends(list);
}).catch(() => {}).finally(() => setLoading(false));
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('inbound', 'pending').then(data => setPendingCount((data.items || []).length)).catch(() => {});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('inbound', 'pending', cursor)).then(list => setPendingCount(list.length)).catch(() => {});
}, []);
const visible = friends.filter(item => `${item.display_name}${item.remark || ''}`.includes(query));
const groups = visible.reduce((result, item) => {
@@ -7759,7 +7790,7 @@ function FriendRequests({
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('all', 'all').then(data => setItems(data.items || [])).catch(() => {}).finally(() => setLoading(false));
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('all', 'all', cursor)).then(setItems).catch(() => {}).finally(() => setLoading(false));
}, []);
const handle = (item, accept) => {
const nextStatus = accept ? 'accepted' : 'rejected';
@@ -8362,8 +8393,8 @@ function Settings({
}).catch(() => {});
}
});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)().then(data => {
const entry = (data.items || []).find(item => item.user_id === uid);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)('', cursor)).then(list => {
const entry = list.find(item => item.user_id === uid);
if (entry) setRemark(entry.remark || '');
}).catch(() => {});
}, [uid]);
@@ -8522,7 +8553,7 @@ function BlockList({
const [people, setPeople] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBlocks)().then(data => setPeople(data.items || [])).catch(() => {}).finally(() => setLoading(false));
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBlocks)(cursor)).then(setPeople).catch(() => {}).finally(() => setLoading(false));
}, []);
const remove = userId => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.unblockUser)(userId).then(() => {
@@ -12006,8 +12037,9 @@ function AdminList({
setOwner(d.owner);
setAdmins(d.admins);
}
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
const items = data.items || [];
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
cursor
})).then(items => {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('group-members:' + topic, items);
const d = deriveOwnerAndAdmins(items);
setOwner(d.owner);
@@ -12094,8 +12126,9 @@ function GroupMembers({
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!topic) return;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
const items = data.items || [];
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
cursor
})).then(items => {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('group-members:' + topic, items);
setMembers(items);
}).catch(() => {
@@ -12158,11 +12191,14 @@ function MemberPicker({
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!topic) return;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, mute ? {
muteEligible: true
} : {
eligibleRole: 'admin'
}).then(data => setMembers((data.items || []).filter(m => m.eligible !== false))).catch(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
cursor,
...(mute ? {
muteEligible: true
} : {
eligibleRole: 'admin'
})
})).then(items => setMembers(items.filter(m => m.eligible !== false))).catch(() => {
onToast('加载失败');
setLoadError(true);
});
@@ -12242,18 +12278,19 @@ function Muted({
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const load = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
if (!topic) return;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMutes)(topic).then(data => {
const list = data.items || [];
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMutes)(topic, cursor)).then(list => {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('group-muted:' + topic, list);
setItems(list);
}).catch(() => {
onToast('加载失败');
setLoadError(true);
});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
cursor
})).then(items => {
const map = {};
const avatarMap = {};
(data.items || []).forEach(m => {
items.forEach(m => {
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
if (m.avatar_file_id) avatarMap[m.user_id] = {
avatar_file_id: m.avatar_file_id
@@ -12324,9 +12361,10 @@ function OwnerSelect({
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!topic) return;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic, {
cursor,
eligibleRole: 'owner'
}).then(data => setMembers((data.items || []).filter(m => m.eligible !== false))).catch(() => {
})).then(items => setMembers(items.filter(m => m.eligible !== false))).catch(() => {
onToast('加载失败');
setLoadError(true);
});
@@ -12793,11 +12831,8 @@ function GroupManagementView({
}
const key = `group-my-role:${topic}:${myUserId}`;
setMyRole((0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)(key) || null);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
const items = data.items || [];
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('group-members:' + topic, items);
const me = items.find(m => m.user_id === myUserId);
const role = me ? me.role : null;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroup)(topic).then(group => {
const role = group ? group.my_role : null;
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)(key, role);
setMyRole(role);
}).catch(() => {});
@@ -15738,7 +15773,7 @@ function DeleteAccountConfirm({
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(ConfirmSheet, {
icon: "warning",
title: "注销账号",
body: "注销后账号数据将被永久删除且无法恢复,包括聊天记录、好友关系和收藏内容。确定要继续吗?",
body: "注销后账号数据将被永久删除且无法恢复,包括聊天记录、好友关系和收藏内容。如果你是某些群的群主,这些群也将被永久解散,其他成员将无法继续使用。确定要继续吗?",
confirmLabel: "继续",
onConfirm: () => setStep(2),
onCancel: () => navigate('android-settings')
@@ -16020,7 +16055,9 @@ function YouthMode({
if (!settings) return;
if (!settings.password_set) {
onToast('请先设置独立密码');
window.setTimeout(() => navigate('identity-check'), 400);
window.setTimeout(() => navigate('identity-check', {
ym: 'enable'
}), 400);
return;
}
navigate('youth-mode-verify', {
@@ -16151,7 +16188,9 @@ function YouthModeVerify({
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("button", {
type: "button",
className: "auth-text-button",
onClick: () => navigate('identity-check'),
onClick: () => navigate('identity-check', {
ym: mode
}),
children: "忘记独立密码?"
}, void 0, false)]
}, void 0, true)]
@@ -17252,6 +17291,7 @@ class SidepanelView extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
className: "auth-onboarding-panel",
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_25__.jsxDEV)(_auth_onboarding_view_jsx__WEBPACK_IMPORTED_MODULE_6__["default"], {
view: view,
hashParams: this.props.hashParams,
authenticated: !!this.props.myUserId,
login: this.props.login,
loginDisabled: this.props.loginDisabled,
@@ -19317,6 +19357,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
});
}
canStartCall(topicName) {
if (this.state.callState != _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE) {
return false;
}
const topic = this.tinode.getTopic(topicName);
if (!topic || topic._deleted || !tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Tinode.isP2PTopicName(topicName)) {
return false;
@@ -19729,7 +19772,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
callTopic: undefined,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
callAudioOnly: undefined,
callSeq: undefined
callSeq: undefined,
callShouldStart: false
});
}
handleCallAccept(topicName, setCallTopic, callSeq) {
@@ -19766,7 +19810,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
callTopic: null,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
callSeq: null,
callAudioOnly: undefined
callAudioOnly: undefined,
callShouldStart: false
});
return;
}
@@ -20426,8 +20471,8 @@ function resolveUserName(id, onDone) {
onDone(nameCache.get(id));
return;
}
const withPrefix = id.startsWith('usr') ? id : 'usr' + id;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getUserProfile)(id).catch(() => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getUserProfile)(withPrefix)).then(p => {
const withPrefix = id.startsWith('usr') ? null : 'usr' + id;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getUserProfile)(id).catch(err => withPrefix ? (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getUserProfile)(withPrefix) : Promise.reject(err)).then(p => {
const name = p && (p.display_name || p.nickname || p.public_account) || id;
nameCache.set(id, name);
onDone(name);
@@ -22458,6 +22503,7 @@ const BAR_COLOR = '#888A';
const BAR_COLOR_DARK = '#666C';
const THUMB_COLOR = '#1781d7';
const MIN_PREVIEW_LENGTH = 16;
let activeAudioPlayer = null;
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
icon_title_play: {
id: 'icon_title_play',
@@ -22498,6 +22544,9 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
this.initCanvas();
}
componentWillUnmount() {
if (activeAudioPlayer === this) {
activeAudioPlayer = null;
}
if (this.audioPlayer) {
this.audioPlayer.onloadedmetadata = null;
this.audioPlayer.ontimeupdate = null;
@@ -22506,6 +22555,14 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
this.audioPlayer = null;
}
}
pauseForOtherPlayer() {
if (this.audioPlayer) {
this.audioPlayer.pause();
}
this.setState({
playing: false
});
}
componentDidUpdate(prevProps) {
if (this.props.src != prevProps.src) {
this.initAudio();
@@ -22534,6 +22591,9 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
playing: false,
currentTime: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_2__.secondsToTime)(0, this.state.longMin)
});
if (activeAudioPlayer === this) {
activeAudioPlayer = null;
}
};
}
initCanvas() {
@@ -24962,6 +25022,9 @@ class ContextMenu extends (react__WEBPACK_IMPORTED_MODULE_0___default().Componen
return;
}
const msg = topic.createMessage(params.content, false);
if (params.head) {
msg.head = Object.assign(msg.head || {}, params.head);
}
return topic.publishDraft(msg).catch(err => {
if (errorHandler) {
errorHandler(err.message, 'err');